diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..e2b507d --- /dev/null +++ b/.gitignore @@ -0,0 +1,89 @@ +# Apple's CRAP +.DS_Store +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +# C extensions +*.so +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec +# Installer logs +pip-log.txt +pip-delete-this-directory.txt +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml +# Translations +*.mo +*.pot +# Django stuff: +*.log +# Sphinx documentation +docs/_build/ +# PyBuilder +target/ +#Ropeproject stuff +.ropeproject/ +#Vim swap +.swp +#virtual environment files/folder +#setup.py +venv/ +bin/ +#other libs +include/ +lib/ +pyvenv.cfg +#project reference +Reference/ +#experimental stuff +Experiment/ +Archive/ +archive/ +.~lock.* + +#wiki +PooPyLab_Project.wiki/ +#github page +toogad.github.io/ + +#doxygen +docs/html/ +docs/latex/ + +#emacs: +.dir-locals.el + +#makefile: +Makefile* +compile_flag.txt +debug/ +*.o +*.out + +#generated files: +*.pfd +test*.json diff --git a/Class_Hierachy.dia b/Class_Hierachy.dia deleted file mode 100644 index 2c8268d..0000000 Binary files a/Class_Hierachy.dia and /dev/null differ diff --git a/LICENSE.txt b/LICENSE.txt old mode 100644 new mode 100755 diff --git a/PooPyLab/ASMModel/ASMModel_Test.py b/PooPyLab/ASMModel/ASMModel_Test.py deleted file mode 100755 index 398728f..0000000 --- a/PooPyLab/ASMModel/ASMModel_Test.py +++ /dev/null @@ -1,143 +0,0 @@ -#!/usr/bin/env python - -# This is a test program for the PooPyLab biological wastewater treatment -# software package. -# The main objective of this test program is to connect units and find out -# if they can communicate with each other appropriately. The main -# indicators will be the flow balance among all units. -# Last Update: -# June 24, 2015 KZ: rewrote a lines influenced by the updated Splitter -# and Pipe classes - -import influent, effluent, was, splitter, reactor, clarifier, pipe -from Test_Functions3 import * -#import constants -import os - -os.system('cls' if os.name == 'nt' else 'clear') - -WWTP = [] - -Inf = influent.Influent() - -Pipe1 = pipe.Pipe() -Pipe2 = pipe.Pipe() -Pipe3 = pipe.Pipe() -Pipe4 = pipe.Pipe() -Pipe5 = pipe.Pipe() -Pipe6 = pipe.Pipe() - -Reactor1 = reactor.ASMReactor() - -Clar1 = clarifier.Final_Clarifier() - -Splt1 = splitter.Splitter() - -Eff = effluent.Effluent() - -WAS1 = was.WAS() - - - -Inf.SetFlow(10)#TODO: Units to be noted in ''' ''' if a function asks for params - -#------------Connection Test------------ -def ConnectWWTPUnits(): - - WWTP.append(Inf) - - Inf.SetDownstreamMainUnit(Pipe1) - WWTP.append(Pipe1) - - Pipe1.SetDownstreamMainUnit(Reactor1) - WWTP.append(Reactor1) - - Reactor1.SetDownstreamMainUnit(Pipe2) - WWTP.append(Pipe2) - - Pipe2.SetDownstreamMainUnit(Clar1) - WWTP.append(Clar1) - - Clar1.SetDownstreamMainUnit(Pipe3) - WWTP.append(Pipe3) - - Pipe3.SetDownstreamMainUnit(Eff) - WWTP.append(Eff) - - #Clar1.SetupSidestream(Pipe4, 5) - Clar1.SetDownstreamSideUnit(Pipe4) - Clar1.SetSidestreamFlow(5) - WWTP.append(Pipe4) - - Pipe4.SetDownstreamMainUnit(Splt1) - WWTP.append(Splt1) - - Splt1.SetDownstreamMainUnit(Pipe5) - WWTP.append(Pipe5) - - Pipe5.SetDownstreamMainUnit(Reactor1) - - #Splt1.SetupSidestream(Pipe6, 1) - Splt1.SetDownstreamSideUnit(Pipe6) - Splt1.SetSidestreamFlow(1) - WWTP.append(Pipe6) - - Splt1.SetAsSRTController(True) - - Pipe6.SetDownstreamMainUnit(WAS1) - WWTP.append(WAS1) -#End of ConnectWWTPUnits() - - -response = raw_input("continue?") - -print "Begin Connection Test..." - -ConnectWWTPUnits() -CheckPlantConnection(WWTP) -CheckUpstream(WWTP) -CheckDownstream(WWTP) -print "End Connection Test" -# Connection Test Summary: SO FAR SO GOOD!! -response = raw_input("continue?") - -#------------Disconnection Test--------- -# PooPyLab is NOT Microsoft Visio. If the user is to re-direct the connected pipe to another -# object, he/she would need to remove that pipe first, and then redraw a new pipe to the -# desired object. -print "Begin Disconnection Test..." - -Reactor1.RemoveUpstreamUnit(Pipe1) -Pipe5.RemoveUpstreamUnit(Splt1) -WAS1.RemoveUpstreamUnit(Pipe6) -Splt1.RemoveUpstreamUnit(Pipe4) -Pipe6.RemoveUpstreamUnit(Splt1) -Clar1.RemoveUpstreamUnit(Pipe2) -Pipe2.RemoveUpstreamUnit(Reactor1) -Eff.RemoveUpstreamUnit(Pipe3) -Pipe3.RemoveUpstreamUnit(Clar1) - -CheckPlantConnection(WWTP) - -CheckUpstream(WWTP) -print -CheckDownstream(WWTP) -response = raw_input("continue?") - -print "Reconnecting..." -WWTP = [] -ConnectWWTPUnits() -CheckPlantConnection(WWTP) -CheckUpstream(WWTP) -CheckDownstream(WWTP) - -print "End Disconnection Test." -response = raw_input("continue?") -print len(WWTP) -#The connection test, disconnection test, and reconnection of the WWTP show that the -# PooPyLab classes were capable of communicate to one another in terms of upstream -# and downstream relations. - -print "=====Begin Receive and Discharge Tests===========" - - diff --git a/PooPyLab/ASMModel/DischargeTest.py b/PooPyLab/ASMModel/DischargeTest.py deleted file mode 100755 index adf03d4..0000000 --- a/PooPyLab/ASMModel/DischargeTest.py +++ /dev/null @@ -1,2 +0,0 @@ -#! /usr/bin/env python - diff --git a/PooPyLab/ASMModel/PartitionTearing_Test_3.py b/PooPyLab/ASMModel/PartitionTearing_Test_3.py deleted file mode 100755 index cf426db..0000000 --- a/PooPyLab/ASMModel/PartitionTearing_Test_3.py +++ /dev/null @@ -1,214 +0,0 @@ -#!/usr/bin/env python - -# This is a test program for the PooPyLab biological wastewater treatment -# software package. -# The main objective of this test program is to partition and tear the -# PFD in order to determine the calculation order. - -# The PFD (flow sheet) used in this test is from Chapter 8 of -# Systematic Methods of Chemical Process Design (1997) -# Lorenz T. Biegler, Ignacio E. Grossmann, Arthur W. Westerberg - -import influent, effluent, was, splitter, reactor, clarifier, pipe -from Test_Functions3 import * -#import constants -import os - -os.system('cls' if os.name == 'nt' else 'clear') - - -# MAIN TEST: -WWTP = [] -Inf, Pipe, React, Splt, Eff = [], [], [], [], [] - - -for i in range(4): - Inf.append(influent.influent()) - Inf[i].set_flow(10)#TODO: Units to be noted in ''' ''' if a function asks for params - Eff.append(effluent.effluent()) - -for i in range(35): - Pipe.append(pipe.pipe()) - -for i in range(11): - Splt.append(splitter.splitter()) - -for i in range(7): - React.append(reactor.asm_reactor()) - -Splt[0].__name__ = 'C' -Splt[1].__name__ = 'F' -Splt[2].__name__ = 'G' -Splt[3].__name__ = 'R' -Splt[4].__name__ = 'S' -Splt[5].__name__ = 'O' -Splt[6].__name__ = 'L' -Splt[7].__name__ = 'M' -Splt[8].__name__ = 'K' -Splt[9].__name__ = 'I' -Splt[10].__name__ = 'N' - -Clarifier = clarifier.final_clarifier() -Clarifier.__name__ = "D" - -WAS1 = was.WAS() - -React[0].__name__ = 'A' -React[1].__name__ = 'B' -React[2].__name__ = 'E' -React[3].__name__ = 'H' -React[4].__name__ = 'Q' -React[5].__name__ = 'J' -React[6].__name__ = 'P' - - -Inf[0].set_downstream_main_unit(Pipe[0]) -Inf[0].set_flow(40) -Pipe[0].set_downstream_main_unit(React[2]) -React[2].set_downstream_main_unit(Pipe[1]) -Pipe[1].set_downstream_main_unit(React[0]) - -Inf[1].set_downstream_main_unit(Pipe[30]) -Pipe[30].set_downstream_main_unit(React[0]) -React[0].set_downstream_main_unit(Pipe[2]) -Pipe[2].set_downstream_main_unit(React[1]) -React[1].set_downstream_main_unit(Pipe[3]) -Pipe[3].set_downstream_main_unit(Splt[0]) - -Splt[0].set_downstream_main_unit(Pipe[27]) -Splt[0].set_downstream_side_unit(Pipe[4]) -Splt[0].set_sidestream_flow(1) - -Pipe[27].set_downstream_main_unit(Clarifier) -Pipe[4].set_downstream_main_unit(Splt[1]) - -Splt[1].set_downstream_main_unit(Pipe[5]) -Splt[1].set_downstream_side_unit(Pipe[24]) -Splt[1].set_sidestream_flow(0.5) - -Pipe[24].set_downstream_main_unit(Splt[2]) -Pipe[5].set_downstream_main_unit(React[3]) - -Splt[2].set_downstream_main_unit(Pipe[26]) -Splt[2].set_downstream_side_unit(Pipe[25]) -Splt[2].set_sidestream_flow(0.1) - -Pipe[26].set_downstream_main_unit(Splt[0]) -Pipe[25].set_downstream_main_unit(Eff[3]) - -Clarifier.set_downstream_main_unit(Pipe[29]) -Clarifier.set_downstream_side_unit(Pipe[28]) -Clarifier.set_sidestream_flow(0.2) - -Pipe[29].set_downstream_main_unit(React[2]) -Pipe[28].set_downstream_main_unit(React[3]) - -React[3].set_downstream_main_unit(Pipe[6]) - -Pipe[6].set_downstream_main_unit(Splt[9]) - -Splt[9].set_downstream_main_unit(Pipe[23]) -Splt[9].set_downstream_side_unit(Pipe[7]) -Splt[9].set_sidestream_flow(0.02) - -Pipe[23].set_downstream_main_unit(React[4]) -Pipe[7].set_downstream_main_unit(React[5]) - -React[5].set_downstream_main_unit(Pipe[8]) -React[4].set_downstream_main_unit(Pipe[13]) - -Pipe[13].set_downstream_main_unit(Splt[3]) - -Splt[3].set_downstream_main_unit(Pipe[14]) -Splt[3].set_downstream_side_unit(Pipe[15]) -Splt[3].set_sidestream_flow(0.4) - -Pipe[14].set_downstream_main_unit(React[5]) -Pipe[15].set_downstream_main_unit(Eff[2]) - -Pipe[8].set_downstream_main_unit(Splt[8]) - -Splt[8].set_downstream_main_unit(Pipe[9]) -Splt[8].set_downstream_side_unit(Pipe[19]) -Splt[8].set_sidestream_flow(0.35) - -Pipe[19].set_downstream_main_unit(Eff[1]) - -Pipe[9].set_downstream_main_unit(Splt[6]) - -Splt[6].set_downstream_main_unit(Pipe[31]) -Splt[6].set_downstream_side_unit(Pipe[10]) -Splt[6].set_sidestream_flow(0.05) - -Pipe[31].set_downstream_main_unit(Splt[7]) -Pipe[10].set_downstream_main_unit(Splt[5]) - -Splt[7].set_downstream_main_unit(Pipe[22]) -Splt[7].set_downstream_side_unit(Pipe[32]) -Splt[7].set_sidestream_flow(0.002) - -Pipe[22].set_downstream_main_unit(Splt[4]) -Pipe[32].set_downstream_main_unit(Splt[10]) - -Splt[10].set_downstream_main_unit(Pipe[33]) -Splt[10].set_downstream_side_unit(Pipe[34]) -Splt[10].set_sidestream_flow(0.0001) -Pipe[33].set_downstream_main_unit(Eff[0]) -Pipe[34].set_downstream_main_unit(Splt[6]) - -Splt[4].set_downstream_main_unit(Pipe[12]) -Splt[4].set_downstream_side_unit(Pipe[16]) -Splt[4].set_sidestream_flow(0.03) -Splt[4].set_as_SRT_controller(True) - -Pipe[16].set_downstream_main_unit(WAS1) -Pipe[12].set_downstream_main_unit(React[4]) - -Splt[5].set_downstream_main_unit(Pipe[17]) -Splt[5].set_downstream_side_unit(Pipe[11]) -Splt[5].set_sidestream_flow(0.1) -Pipe[11].set_downstream_main_unit(Splt[4]) -Pipe[17].set_downstream_main_unit(React[6]) - -React[6].set_downstream_main_unit(Pipe[18]) -Pipe[18].set_downstream_main_unit(Splt[8]) - -Inf[2].set_downstream_main_unit(Pipe[20]) -Inf[2].set_flow(10) -Pipe[20].set_downstream_main_unit(Splt[5]) - -Inf[3].set_downstream_main_unit(Pipe[21]) -Inf[3].set_flow(20) -Pipe[21].set_downstream_main_unit(React[6]) - -WWTP = Inf + Pipe + React + Splt + Eff -WWTP.append(WAS1) -WWTP.append(Clarifier) - -response = input("continue?") - -print("Begin Connection Test...") - -CheckPlantConnection(WWTP) -CheckUpstream(WWTP) -CheckDownstream(WWTP) - -Groups = FindGroups(WWTP) - -for sub in Groups: - print("[ ", end=' ') - for unit in sub: - print(unit.__name__,";", end=' ') - print("]") - -ElementalCircuits = [] -import pdb -pdb.set_trace() -FindLoops(Groups, ElementalCircuits) -print(len(ElementalCircuits)) -for subgroup in ElementalCircuits: - print(len(subgroup)) - print("[ ", end=' ') - for t in subgroup: - print(t.__name__, "; ", end=' ') - print("]") diff --git a/PooPyLab/ASMModel/WAS.pmt b/PooPyLab/ASMModel/WAS.pmt new file mode 100644 index 0000000..cccc42d --- /dev/null +++ b/PooPyLab/ASMModel/WAS.pmt @@ -0,0 +1,14 @@ +#An WAS unit only deals with its own inlet since its main outlet +#feeds to nothing within the process boundaries. +# +#Its inlet flow data source can only be set to UPS or PRG. +# +#If the inlet flow data source is PRG, it would be either defined by +#the user or calculations. If calculated, the current WAS unit +#shall be the only WAS type for such estimate. All other WAS units, +#if present, shall be defined in terms of flows. +# +#The flow balance would be a global one across the entire plant, +#including the total of all influents, all effluents, and all other +#WAS units whose flows are defined, along with their solids +#concentrations. diff --git a/PooPyLab/ASMModel/asm.py b/PooPyLab/ASMModel/asm.py deleted file mode 100755 index ab649a2..0000000 --- a/PooPyLab/ASMModel/asm.py +++ /dev/null @@ -1,497 +0,0 @@ -# This file is part of PooPyLab. -# -# PooPyLab is a simulation software for biological wastewater treatment -# processes using International Water Association Activated Sludge Models. -# -# Copyright (C) 2017 Kai Zhang -# -# PooPyLab is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# PooPyLab is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with PooPyLab. If not, see . -# -# -# -# -# This is the definition of the ASM1 model to be imported -# as part of the Reactor object -# -# Change Log: -# Dec 13, 2017 KZ: Fixed a few mismatched parentheses -# Jul 20, 2017 KZ: Changed to pythonic style -# Mar 21, 2017 KZ: Changed to Python3 -# Jun 07, 2014 KZ: Spelling Fix -# -# NOMENCLATURE: -# rb: readily biodegradable -# nrb: non-readily biodegradable -# O: Oxygen -# NH: Ammonia -# L: Lysis -# H: Heterotrophs -# h: Hydrolysis -# a: Ammonification -# i: ratio -# f: fraction -# cf: correction factor -# A: Autotrophs -# B: Biomass (active) -# D: Debris -# NO: NOx (oxidized nitrogen) -# I: Inert -# ALK: Alkalinity -# S: Substrate (COD or TKN, mg/L) -# X: Particulate matter as COD ( mg/L) -# S: Soluble -# Inf: Influent -# Eff: Effluent -# TBD: To Be Determined by user - - -import constants -from scipy.optimize import fsolve - -class ASM1(): - - def __init__(self, Temp=20, DO=2): - - self._temperature = Temp - self._bulk_DO = DO - - # define the model parameters and stochoimetrics as dict() so that it - # is easier to keep track of names and values - self._params = {} - self._stoichs = {} - self._delta_t = 20 - self._temperature - - # define a Python Dictionary to store Parameters - self._set_params() - - # define a Python Dictionary to store Stochoimetrics - self._set_stoichs() - - # define a Python List to store ASM components - # The Components the ASM components IN THE REACTOR. JULY 10, 2013 - # For ASM #1: - # - # self._comps[0]: X_I - # self._comps[1]: X_S - # self._comps[2]: X_BH - # self._comps[3]: X_BA - # self._comps[4]: X_D - # self._comps[5]: S_I - # self._comps[6]: S_S - # self._comps[7]: S_DO - # self._comps[8]: S_NO - # self._comps[9]: S_NH - # self._comps[10]: S_NS - # self._comps[11]: X_NS - # self._comps[12]: S_ALK - # - self._comps = [0] * constants._NUM_ASM1_COMPONENTS - - return - - def _set_params(self): - - # Ideal Growth Rate of Heterotrophs (u_H, 1/DAY) - self._params['u_H'] = 6 * pow(1.08, self._delta_t) - - # Ideal Lysis Rate of Heterotrophs (b_LH, 1/DAY) - self._params['b_LH'] = 0.408 * pow(1.04, self._delta_t) - - # Ideal Growth Rate of Autotrophs (u_A, 1/DAY) - self._params['u_A'] = 0.768 * pow(1.11, self._delta_t) - - # Ideal Growth Rate of Autotrophs (b_LA, 1/DAY) - self._params['b_LA'] = 0.096 * pow(1.04, self._delta_t) - - # Half Growth Rate Concentration of Heterotrophs (K_s, mgCOD/L) - self._params['K_S'] = 20 - - # Switch Coefficient for Dissoved O2 of Hetero. (K_OH, mgO2/L) - self._params['K_OH'] = 0.1 - - # Association Conc. for Dissoved O2 of Auto. (K_OA, mgN/L) - self._params['K_OA'] = 0.75 - - # Association Conc. for NH3-N of Auto. (K_NH, mgN/L) - self._params['K_NH'] = 1 * pow(1.14, self._delta_t) - - # Association Conc. for NOx of Hetero. (K_NO, mgN/L) - self._params['K_NO'] = 0.2 - - # Hydrolysis Rate (k_h, mgCOD/mgBiomassCOD-day) - self._params['k_h'] = 2.208 * pow(1.08, self._delta_t) - - # Half Rate Conc. for Hetro. Growth on Part. COD - # (K_X, mgCOD/mgBiomassCOD) - self._params['K_X'] = 0.15 - - # Ammonification of Org-N in biomass (k_a, L/mgBiomassCOD-day) - self._params['k_a'] = 0.1608 * pow(1.08, self._delta_t) - - # Yield of Hetero. Growth on COD (Y_H, mgBiomassCOD/mgCODremoved) - self._params['Y_H'] = 0.6 - - # Yield of Auto. Growth on TKN (Y_A, mgBiomassCOD/mgTKNoxidized) - self._params['Y_A'] = 0.24 - - # Fract. of Debris in Lysed Biomass(f_D_, gDebrisCOD/gBiomassCOD) - self._params['f_D_'] = 0.08 - - # Correction Factor for Hydrolysis (cf_h, unitless) - self._params['cf_h'] = 0.4 - - # Correction Factor for Anoxic Heterotrophic Growth (cf_g, unitless) - self._params['cf_g'] = 0.8 - - # Ratio of N in Active Biomass (i_N_XB, mgN/mgActiveBiomassCOD) - self._params['i_N_XB'] = 0.086 - - # Ratio of N in Debris Biomass (i_N_XD, mgN/mgDebrisBiomassCOD) - self._params['i_N_XD'] = 0.06 - - return None - #=============================== End of Parameter Definition ============== - - # STOCHIOMETRIC MATRIX (Use the Table 6.1, p193, Grady Jr. et al 1999) - def _set_stoichs(self): - - self._stoichs['0_2'] = 1 - - self._stoichs['0_6'] = -1 / self._params['Y_H'] - - self._stoichs['0_7'] = -(1 - self._params['Y_H']) / self._params['Y_H'] - # multiply -1 to express as oxygen - - self._stoichs['0_9'] = -self._params['i_N_XB'] - - self._stoichs['0_12'] = -self._params['i_N_XB'] / 14 - - self._stoichs['1_2'] = 1 - - self._stoichs['1_6'] = -1 / self._params['Y_H'] - - self._stoichs['1_8'] = -(1 - self._params['Y_H']) \ - / (2.86 * self._params['Y_H']) - - self._stoichs['1_9'] = -self._params['i_N_XB'] - - self._stoichs['1_12'] = (1 - self._params['Y_H']) \ - / (14 * 2.86 * self._params['Y_H']) \ - - self._params['i_N_XB'] / 14 - - self._stoichs['2_3'] = 1 - - self._stoichs['2_7'] = -(4.57 - self._params['Y_A']) \ - / self._params['Y_A'] - # multiply -1 to express as oxygen - - self._stoichs['2_8'] = 1 / self._params['Y_A'] - - self._stoichs['2_9'] = -self._params['i_N_XB'] \ - - 1 / self._params['Y_A'] - - self._stoichs['2_12'] = -self._params['i_N_XB'] / 14 \ - - 1 / (7 * self._params['Y_A']) - - self._stoichs['3_1'] = 1 - self._params['f_D_'] - - self._stoichs['3_2'] = -1 - - self._stoichs['3_4'] = self._params['f_D_'] - - self._stoichs['3_11'] = self._params['i_N_XB'] - self._params['f_D_'] \ - * self._params['i_N_XD'] - - self._stoichs['4_1'] = 1 - self._params['f_D_'] - - self._stoichs['4_3'] = -1 - - self._stoichs['4_4'] = self._params['f_D_'] - - self._stoichs['4_11'] = self._params['i_N_XB'] - self._params['f_D_'] \ - * self._params['i_N_XD'] - - self._stoichs['5_9'] = 1 - - self._stoichs['5_10'] = -1 - - self._stoichs['5_12'] = 1 / 14 - - self._stoichs['6_1'] = -1 - - self._stoichs['6_6'] = 1 - - self._stoichs['7_10'] = 1 - - self._stoichs['7_11'] = -1 - - return None - ##=========================== End of Stoichiometrics ===================== - - # PROCESS RATE DEFINITIONS (Rj, M/L^3/T): - - # The following factors/swithes all use the _monod() function - # Monod Factor of Sol.BioDegrad.COD on Hetero. - # Monod Switch of Dissol. O2 on Hetero. - # Monod Switch of Dissol. O2 on Auto. - # Monod Factor of Ammonia-N on Autotrophs - # Monod Factor of NOx-N on Autotrophs - # - def _monod (self, My_Eff_S_TBD, My_K_TBD): - return My_Eff_S_TBD / (My_Eff_S_TBD + My_K_TBD) - - # Aerobic Growth Rate of Heterotrophs (_r0_AerGH, mgCOD/L/day) - def _r0_AerGH (self): - return self._params['u_H'] \ - * self._monod(self._comps[6], self._params['K_S']) \ - * self._monod(self._bulk_DO, self._params['K_OH']) \ - * self._comps[2] - - # Anoxic Growth Rate of Heterotrophs (_r1_AxGH, mgCOD/L/day) - def _r1_AxGH (self): - return self._params['u_H'] \ - * self._monod(self._comps[6], self._params['K_S']) \ - * self._monod(self._params['K_OH'], self._bulk_DO) \ - * self._monod(self._comps[8], self._params['K_NO']) \ - * self._params['cf_g'] \ - * self._comps[2] - - # Aerobic Growth Rate of Autotrophs (_r2_AerGA, mgCOD/L/day) - def _r2_AerGA(self): - return self._params['u_A'] \ - * self._monod(self._comps[9], self._params['K_NH']) \ - * self._monod(self._bulk_DO, self._params['K_OA']) \ - * self._comps[3] - - # Death and Lysis Rate of Heterotrophs (_r3_DLH, mgCOD/L/day) - def _r3_DLH(self): - return self._params['b_LH'] * self._comps[2] - - # Death and Lysis Rate of Autotrophs (_r4_DLA, mgCOD/L/day) - def _r4_DLA(self): - return self._params['b_LA'] * self._comps[3] - - # Ammonification Rate of Soluable Organic N (_r5_AmmSN, mgN/L/day) - def _r5_AmmSN(self): - return self._params['k_a'] \ - * self._comps[10] \ - * self._comps[2] - - # Hydrolysis Rate of Particulate Organics (_r6_HydX, mgCOD/L/day) - def _r6_HydX(self): - return self._params['k_h'] \ - * self._monod(self._comps[1] / self._comps[2], \ - self._params['K_X']) \ - * (self._monod(self._bulk_DO, \ - self._params['K_OH']) \ - + self._params['cf_h'] \ - * self._monod(self._params['K_OH'], self._bulk_DO) \ - * self._monod(self._comps[8], \ - self._params['K_NO'])) \ - * self._comps[2] - - # Hydrolysis Rate of Part. Organic N (_r7_HydXN, mgN/L/day) - def _r7_HydXN(self): - return self._r6_HydX() * self._comps[11] / self._comps[1] - - #---------Overall Process Rate Equations for Individual Components--- - - def _rate0_X_I(self): - return 0 - - def _rate1_X_S(self): - return self._stoichs['3_1'] * self._r3_DLH() \ - + self._stoichs['4_1'] * self._r4_DLA() \ - + self._stoichs['6_1'] * self._r6_HydX() - - def _rate2_X_BH(self): - return self._stoichs['0_2'] * self._r0_AerGH() \ - + self._stoichs['1_2'] * self._r1_AxGH() \ - + self._stoichs['3_2'] * self._r3_DLH() - - def _rate3_X_BA(self): - return self._stoichs['2_3'] * self._r2_AerGA() \ - + self._stoichs['4_3'] * self._r4_DLA() - - def _rate4_X_D(self): - return self._stoichs['3_4'] * self._r3_DLH() \ - + self._stoichs['4_4'] * self._r4_DLA() - - def _rate5_S_I(self): - return 0 - - def _rate6_S_S(self): - return self._stoichs['0_6'] * self._r0_AerGH() \ - + self._stoichs['1_6'] * self._r1_AxGH() \ - + self._stoichs['6_6'] * self._r6_HydX() - - def _rate7_S_DO(self): - return self._stoichs['0_7'] * self._r0_AerGH() \ - + self._stoichs['2_7'] * self._r2_AerGA() - - def _rate8_S_NO(self): - return self._stoichs['1_8'] * self._r1_AxGH() \ - + self._stoichs['2_8'] * self._r2_AerGA() - - def _rate9_S_NH(self): - return self._stoichs['0_9'] * self._r0_AerGH() \ - + self._stoichs['1_9'] * self._r1_AxGH() \ - + self._stoichs['2_9'] * self._r2_AerGA() \ - + self._stoichs['5_9'] * self._r5_AmmSN() - - def _rate10_S_NS(self): - return self._stoichs['5_10'] * self._r5_AmmSN() \ - + self._stoichs['7_10'] * self._r7_HydXN() - - def _rate11_X_NS(self): - return self._stoichs['3_11'] * self._r3_DLH() \ - + self._stoichs['4_11'] * self._r4_DLA() \ - + self._stoichs['7_11'] * self._r7_HydXN() - - def _rate12_S_Alk(self): - return self._stoichs['0_12'] * self._r0_AerGH() \ - + self._stoichs['1_12'] * self._r1_AxGH() \ - + self._stoichs['2_12'] * self._r2_AerGA() \ - + self._stoichs['5_123'] * self._r5_AmmSN() - - - def _steady(self, ExtCompList, InFlow, InfComp, Vol): - ''' - Defines the steady state mass balance: - Original variables in stand alone script: - (CompList, Parameters, Stoichiometrics, InfComp, SRT, Vol, Bulk_DO) - InfComp is a List that represent the Influent values - (get them from the ASMReactor) of the ASM1 Components - with the exception of InfComp[0] which is the Inf_Flow into the - reactor (m3/day), which may be different from - the overall plant influent. - - 0_X_I, 1_X_S, 2_X_BH, 3_X_BA, 4_X_D - 5_S_I, 6_S_S, 7_S_DO, 8_S_NO, 9_S_NH, 10_S_NS, 11_X_NS, 12_S_ALK - - ASM1._bulk_DO in mgO2/L - - ExtCompList is a representation of self._Components in the - definition of self._steady() in order to use scipy.optimize.fsolve(). - ''' - # TODO: The resulting ExtCompList values will need to be passed to - # self._Components - # Steady state equations that calculate the simulation results: - # FROM NOW ON: Reactor Influent Flow = Reactor Effluent Flow - # Overall steady state mass balance: - # InfFlow * (InfConc - EffConc) + GrowthRate * ActVol == 0 - - - # for C0_X_I, assume perfect solid-liquid separation - # (i.e. X_I = 0 mg/L in effluent) - result = [InFlow * (InfComp[0] - ExtCompList[0]) \ - + self._rate0_X_I() * Vol] - - # for C1_X_S, assume perfect solid-liquid separation - # (i.e. X_S = 0 mg/L in effluent) - result.append(InFlow * (InfComp[1] - ExtCompList[1]) \ - + self._rate1_X_S() * Vol) - - # for C2_X_BH, assume perfect solid-liquid separation - # (i.e. X_BH = 0 mg/L in effluent) - result.append(InFlow * (InfComp[2] - ExtCompList[2]) \ - + self._rate2_X_BH() * Vol) - - # for C3_X_BA, assume perfect solid-liquid separation - # (i.e. X_BA = 0 mg/L in effluent) - result.append(InFlow * (InfComp[3] - ExtCompList[3]) \ - + self._rate3_X_BA() * Vol) - - # for C4_X_D, assume perfect solid-liquid separation - # (i.e. X_D = 0.0 mg/L in effluent) - result.append(InFlow * (InfComp[4] - ExtCompList[4]) \ - + self._rate4_X_D() * Vol) - - # for C5_S_I - result.append(InFlow * (InfComp[5] - ExtCompList[5]) \ - + self._rate5_S_I() * Vol) - - # for C6_S_S - result.append(InFlow * (InfComp[6] - ExtCompList[6]) \ - + self._rate6_S_S() * Vol) - - # for C7_S_DO - result.append(InFlow * (InfComp[7] - ExtCompList[7]) \ - + self._rate7_S_DO() * Vol) - # result.append(CompList[8] - Bulk_DO) - - # for C8_S_NO - result.append(InFlow * (InfComp[8] - ExtCompList[8]) \ - + self._rate8_S_NO() * Vol) - - # for C9_S_NH - result.append(InFlow * (InfComp[9] - ExtCompList[9]) \ - + self._rate9_S_NH() * Vol) - - # for C10_S_NS - result.append(InFlow * (InfComp[10] - ExtCompList[10]) \ - + self._rate10_S_NS() * Vol) - - # for C11_X_NS - result.append(InFlow * (InfComp[11] - ExtCompList[11]) \ - + self._rate11_X_NS() * Vol) - - # for C12_S_ALK - result.append(InFlow * (InfComp[12] - ExtCompList[12]) \ - + self._rate12_S_Alk() * Vol) - - return result - #====================End of Private Fuction Definitions =================== - - - #====================== Public Interface ================================== - - def steady_step(self, guess, inflow, infcomp, vol): - ''' - Calculate the state of this round of iteration. - Pass the results to the storing List() in the ASMReactor. - The results of this calculation should be compared with those of - the last round, and determine whether the reactor has reached a - steady state. - ''' - current_state = fsolve(self._steady, guess, (inflow, infcomp, vol)) - return current_state - - def update(self, Temp, DO): - ''' update the ASM model with new Temperature and Bulk_DO''' - if Temp <= 4 or DO < 0: - print("Error: New temperature or Bulk_DO too low.", \ - "USING PREVIOUS VALUES FOR BOTH") - return -1 - - self._temperature = Temp - self._bulk_DO = DO - self._delta_t = 20 - self._temperature - self._param = self._set_params() - self._stoich = self._set_stoichs() - return None - - def get_params(self): - return self._params - - def get_stoichs(self): - return self._stoichs - - def get_all_comps(self): - #TODO: Need to determine where to provide GetAllComponents(), in - # ASMReactor or here? - return self._comps.copy() - - def get_bulk_DO(self): - return self._bulk_DO diff --git a/PooPyLab/ASMModel/asm_1.py b/PooPyLab/ASMModel/asm_1.py new file mode 100755 index 0000000..36310f5 --- /dev/null +++ b/PooPyLab/ASMModel/asm_1.py @@ -0,0 +1,704 @@ +# This file is part of PooPyLab. +# +# PooPyLab is a simulation software for biological wastewater treatment processes using International Water Association +# Activated Sludge Models. +# +# Copyright (C) Kai Zhang +# +# PooPyLab is free software: you can redistribute it and/or modify it under the terms of the GNU General Public +# License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with PooPyLab. If not, see +# . +# +# +# This is the definition of the ASM1 model to be imported as part of the Reactor object +# +# + + +"""Definition of the IWA Activated Sludge Model #1. + +Reference: + Grady Jr. et al, 1999: Biological Wastewater Treatment, 2nd Ed. + + IWA Task Group on Math. Modelling for Design and Operation of Biological Wastewater Treatment, 2000. Activated + Sludge Model No. 1, in Activated Sludge Models ASM1, ASM2, ASM2d, and ASM 3. +""" +## @namespace asm_1 +## @file asm_1.py + + +from ..ASMModel import constants +from .asmbase import asm_model + + +class ASM_1(asm_model): + """ + Kinetics and stoichiometrics of IWA ASM 1 model. + """ + + __id = 0 + + def __init__(self, ww_temp=20, DO=2): + """ + Initialize the ASM 1 model with water temperature and dissolved O2. + + Args: + ww_temp: wastewater temperature, degC; + DO: dissolved oxygen, mg/L + + Return: + None + + See: + _set_ideal_kinetics_20C(); + _set_params(); + _set_stoichs(). + """ + + asm_model.__init__(self) + self.__class__.__id += 1 + + self._set_ideal_kinetics_20C_to_defaults() + + # wastewater temperature used in the model, degC + self._temperature = ww_temp + # mixed liquor bulk dissolved oxygen, mg/L + self._bulk_DO = DO + + # temperature difference b/t what's used and baseline (20C), degC + self._delta_t = self._temperature - 20 + + self.update(ww_temp, DO) + + # The Components the ASM components IN THE REACTOR + # For ASM #1: + # + # self._comps[0]: S_DO as COD + # self._comps[1]: S_I + # self._comps[2]: S_S + # self._comps[3]: S_NH + # self._comps[4]: S_NS + # self._comps[5]: S_NO + # self._comps[6]: S_ALK + # self._comps[7]: X_I + # self._comps[8]: X_S + # self._comps[9]: X_BH + # self._comps[10]: X_BA + # self._comps[11]: X_D + # self._comps[12]: X_NS + # + # ASM model components + self._comps = [0.0] * constants._NUM_ASM1_COMPONENTS + + # Intermediate results of Monod or Inhibition Terms + self._monods = [1.0] * 7 + + # Intermediate results of rate expressions, M/L^3/T + # The list is to help speed up the calculation by reducing redundant calls of individual rate expressions in + # multiple mass balance equations for the model components. + # ASM1 has 8 bio processes. + self._rate_res = [0.0] * 8 + + return None + + + def _set_ideal_kinetics_20C_to_defaults(self): + """ + Set the kinetic params/consts @ 20C to default ideal values. + + See: + update(); + _set_params(); + _set_stoichs(). + """ + + # Ideal Growth Rate of Heterotrophs (u_max_H, 1/DAY) + self._kinetics_20C['u_max_H'] = 6.0 + + # Decay Rate of Heterotrophs (b_H, 1/DAY) + self._kinetics_20C['b_LH'] = 0.62 + + # Ideal Growth Rate of Autotrophs (u_max_A, 1/DAY) + self._kinetics_20C['u_max_A'] = 0.8 + + # Decay Rate of Autotrophs (b_A, 1/DAY) A wide range exists. Table 6.3 on Grady 1999 shows 0.096 (1/d). IWA's + # ASM report did not even show b_A on its table for typical value. ASIM software show a value of "0.000", + # probably cut off by the print function. I can only assume it was < 0.0005 (1/d) at 20C. + #self._kinetics_20C['b_LA'] = 0.096 + self._kinetics_20C['b_LA'] = 0.0007 + + # Half Growth Rate Concentration of Heterotrophs (K_s, mgCOD/L) + self._kinetics_20C['K_S'] = 20.0 + + # Switch Coefficient for Dissolved O2 of Hetero. (K_OH, mgO2/L) + self._kinetics_20C['K_OH'] = 0.2 + + # Association Conc. for Dissolved O2 of Auto. (K_OA, mgN/L) + self._kinetics_20C['K_OA'] = 0.4 + + # Association Conc. for NH3-N of Auto. (K_NH, mgN/L) + self._kinetics_20C['K_NH'] = 1.0 + + # Association Conc. for NOx of Hetero. (K_NO, mgN/L) + self._kinetics_20C['K_NO'] = 0.5 + + # Hydrolysis Rate (k_h, mgCOD/mgBiomassCOD-day) + self._kinetics_20C['k_h'] = 3.0 + + # Half Rate Conc. for Hetero. Growth on Part. COD + # (K_X, mgCOD/mgBiomassCOD) + self._kinetics_20C['K_X'] = 0.03 + + # Ammonification of Org-N in biomass (k_a, L/mgBiomassCOD-day) + self._kinetics_20C['k_a'] = 0.08 + + # Yield of Hetero. Growth on COD (Y_H, mgBiomassCOD/mgCODremoved) + self._kinetics_20C['Y_H'] = 0.67 + + # Yield of Auto. Growth on TKN (Y_A, mgBiomassCOD/mgTKNoxidized) + self._kinetics_20C['Y_A'] = 0.24 + + # Fract. of Debris in Lysed Biomass(f_D, gDebrisCOD/gBiomassCOD) + self._kinetics_20C['f_D'] = 0.08 + + # Correction Factor for Hydrolysis (cf_h, unitless) + self._kinetics_20C['cf_h'] = 0.4 + + # Correction Factor for Anoxic Heterotrophic Growth (cf_g, unitless) + self._kinetics_20C['cf_g'] = 0.8 + + # Ratio of N in Active Biomass (i_N_XB, mgN/mgActiveBiomassCOD) + self._kinetics_20C['i_N_XB'] = 0.086 + + # Ratio of N in Debris Biomass (i_N_XD, mgN/mgDebrisBiomassCOD) + self._kinetics_20C['i_N_XD'] = 0.06 + + return None + + + def _set_params(self): + """ + Set the kinetic parameters/constants @ project temperature. + + This function updates the self._params based on the model temperature and DO. + + See: + update(); + _set_ideal_kinetics_20C(); + _set_stoichs(). + """ + + # Ideal Growth Rate of Heterotrophs (u_max_H, 1/DAY) + self._params['u_max_H'] = self._kinetics_20C['u_max_H'] * pow(1.072, self._delta_t) + + # Decay Rate of Heterotrophs (b_H, 1/DAY) + self._params['b_LH'] = self._kinetics_20C['b_LH'] * pow(1.12, self._delta_t) + + # Ideal Growth Rate of Autotrophs (u_max_A, 1/DAY) + self._params['u_max_A'] = self._kinetics_20C['u_max_A'] * pow(1.103, self._delta_t) + + # Decay Rate of Autotrophs (b_A, 1/DAY) + self._params['b_LA'] = self._kinetics_20C['b_LA'] * pow(1.114, self._delta_t) + + # Half Growth Rate Concentration of Heterotrophs (K_s, mgCOD/L) + + self._params['K_S'] = self._kinetics_20C['K_S'] + + # Switch Coefficient for Dissolved O2 of Hetero. (K_OH, mgO2/L) + self._params['K_OH'] = self._kinetics_20C['K_OH'] + + # Association Conc. for Dissolved O2 of Auto. (K_OA, mgN/L) + self._params['K_OA'] = self._kinetics_20C['K_OA'] + + # Association Conc. for NH3-N of Auto. (K_NH, mgN/L) + self._params['K_NH'] = self._kinetics_20C['K_NH'] + + # Association Conc. for NOx of Hetero. (K_NO, mgN/L) + self._params['K_NO'] = self._kinetics_20C['K_NO'] + + # Hydrolysis Rate (k_h, mgCOD/mgBiomassCOD-day) + self._params['k_h'] = self._kinetics_20C['k_h'] * pow(1.116, self._delta_t) + + # Half Rate Conc. for Hetero. Growth on Part. COD + # (K_X, mgCOD/mgBiomassCOD) + self._params['K_X'] = self._kinetics_20C['K_X'] * pow(1.116, self._delta_t) + + # Ammonification of Org-N in biomass (k_a, L/mgBiomassCOD-day) + self._params['k_a'] = self._kinetics_20C['k_a'] * pow(1.072, self._delta_t) + + # Yield of Hetero. Growth on COD (Y_H, mgBiomassCOD/mgCODremoved) + self._params['Y_H'] = self._kinetics_20C['Y_H'] + + # Yield of Auto. Growth on TKN (Y_A, mgBiomassCOD/mgTKNoxidized) + self._params['Y_A'] = self._kinetics_20C['Y_A'] + + # Fract. of Debris in Lysed Biomass(f_D, gDebrisCOD/gBiomassCOD) + self._params['f_D'] = self._kinetics_20C['f_D'] + + # Correction Factor for Hydrolysis (cf_h, unitless) + self._params['cf_h'] = self._kinetics_20C['cf_h'] + + # Correction Factor for Anoxic Heterotrophic Growth (cf_g, unitless) + self._params['cf_g'] = self._kinetics_20C['cf_g'] + + # Ratio of N in Active Biomass (i_N_XB, mgN/mgActiveBiomassCOD) + self._params['i_N_XB'] = self._kinetics_20C['i_N_XB'] + + # Ratio of N in Debris Biomass (i_N_XD, mgN/mgDebrisBiomassCOD) + self._params['i_N_XD'] = self._kinetics_20C['i_N_XD'] + + return None + + + # STOCHIOMETRIC MATRIX + + + def _set_stoichs(self): + """ + Set the stoichiometrics for the model. + + Note: Make sure to match the .csv model template file in the model_builder folder, Sep 04, 2019): + + _stoichs['x_y'] ==> x is process rate id, and y is component id + + See: + _set_params(); + _set_ideal_kinetics_20C(); + update(). + """ + + # S_O for aerobic hetero. growth, as O2 + self._stoichs['0_0'] = (self._params['Y_H'] - 1.0) / self._params['Y_H'] + + # S_O for aerobic auto. growth, as O2 + self._stoichs['2_0'] = (self._params['Y_A'] - 4.57) / self._params['Y_A'] + + # S_S for aerobic hetero. growth, as COD + self._stoichs['0_2'] = -1.0 / self._params['Y_H'] + + # S_S for anoxic hetero. growth, as COD + self._stoichs['1_2'] = -1.0 / self._params['Y_H'] + + # S_S for hydrolysis of part. substrate + self._stoichs['6_2'] = 1.0 + + # S_NH required for aerobic hetero. growth, as N + self._stoichs['0_3'] = -self._params['i_N_XB'] + + # S_NH required for anoxic hetero. growth, as N + self._stoichs['1_3'] = -self._params['i_N_XB'] + + # S_NH required for aerobic auto. growth, as N + self._stoichs['2_3'] = -self._params['i_N_XB'] - 1.0 / self._params['Y_A'] + + # S_NH from ammonification, as N + self._stoichs['5_3'] = 1.0 + + # S_NS used by ammonification, as N + self._stoichs['5_4'] = -1.0 + + # S_NS from hydrolysis of part.TKN, as N + self._stoichs['7_4'] = 1.0 + + # S_NO for anoxic hetero. growth, as N + self._stoichs['1_5'] = (self._params['Y_H'] - 1.0) / (2.86 * self._params['Y_H']) + + # S_NO from nitrification, as N + self._stoichs['2_5'] = 1.0 / self._params['Y_A'] + + # S_ALK consumed by aerobic hetero. growth, as mM CaCO3 + self._stoichs['0_6'] = -self._params['i_N_XB'] / 14.0 + + # S_ALK generated by anoxic hetero. growth, as mM CaCO3 + self._stoichs['1_6'] = (1.0 - self._params['Y_H']) / (14.0 * 2.86 * self._params['Y_H']) \ + - self._params['i_N_XB'] / 14.0 + + # S_ALK consumed by aerobic auto. growth, as mM CaCO3 + self._stoichs['2_6'] = -self._params['i_N_XB'] / 14 - 1.0 / (7.0 * self._params['Y_A']) + + # S_ALK generated by ammonification, as mM CaCO3 + self._stoichs['5_6'] = 1.0 / 14.0 + + # X_S from hetero. decay, as COD + self._stoichs['3_8'] = 1.0 - self._params['f_D'] + + # X_S from auto. decay, as COD + self._stoichs['4_8'] = 1.0 - self._params['f_D'] + + # X_S consumed by hydrolysis of biomass + self._stoichs['6_8'] = -1.0 + + # X_BH from aerobic hetero. growth, as COD + self._stoichs['0_9'] = 1.0 + + # X_BH from anoxic hetero. growth, as COD + self._stoichs['1_9'] = 1.0 + + # X_BH lost in hetero. decay, as COD + self._stoichs['3_9'] = -1.0 + + # X_BA from aerobic auto. growth, as COD + self._stoichs['2_10'] = 1.0 + + # X_BA lost in auto. decay, as COD + self._stoichs['4_10'] = -1.0 + + + # X_D from hetero. decay, as COD + self._stoichs['3_11'] = self._params['f_D'] + + # X_D from auto. decay, as COD + self._stoichs['4_11'] = self._params['f_D'] + + # X_NS from hetero. decay, as N + self._stoichs['3_12'] = self._params['i_N_XB'] - self._params['f_D'] * self._params['i_N_XD'] + + # X_NS from auto. decay, as COD + self._stoichs['4_12'] = self._params['i_N_XB'] - self._params['f_D'] * self._params['i_N_XD'] + + # X_NS consumed in hydrolysis of part. TKN, as N + self._stoichs['7_12'] = -1.0 + + return None + + + # PROCESS RATE DEFINITIONS (Rj, M/L^3/T): + # + + + def _reaction_rate(self, comps): + """ + Normalized reaction rates for the biological processes. + + Args: + comps: list of current model components (concentrations). + + Return: + list of process rates M/L^3/T in self._rate_res[] + """ + + # Monod term for Heterotroph's substrate + self._monods[0] = self._monod(comps[2], self._params['K_S']) + + # Monod term for Heterotroph's O2 + self._monods[1] = self._monod(comps[0], self._params['K_OH']) + + # Monod term for Heterotroph's NOx-N + self._monods[2] = self._monod(comps[5], self._params['K_NO']) + + # O2 Inhibition term for anoxic Heterotrophs + self._monods[3] = self._monod(self._params['K_OH'], comps[0]) + + # Monod term for Autotroph's NH3-N + self._monods[4] = self._monod(comps[3], self._params['K_NH']) + + # Monod term for Autotroph's O2 + self._monods[5] = self._monod(comps[0], self._params['K_OA']) + + # Monod term for Hydrolysis + self._monods[6] = self._monod(comps[8] / comps[9], self._params['K_X']) + + # Aerobic Growth Rate of Heterotrophs (mgCOD/L/day). + self._rate_res[0] = self._params['u_max_H'] * self._monods[0] * self._monods[1] * comps[9] + + # Anoxic Growth Rate of Heterotrophs (mgCOD/L/day). + self._rate_res[1] = self._params['u_max_H'] * self._monods[0] * self._monods[2] \ + * self._monods[3] * self._params['cf_g'] * comps[9] + + # Aerobic Growth Rate of Autotrophs (mgCOD/L/day). + self._rate_res[2] = self._params['u_max_A'] * self._monods[4] * self._monods[5] * comps[10] + + # Death and Lysis Rate of Heterotrophs (mgCOD/L/day). + self._rate_res[3] = self._params['b_LH'] * comps[9] + + # Death and Lysis Rate of Autotrophs (mgCOD/L/day). + self._rate_res[4] = self._params['b_LA'] * comps[10] + + # Ammonification Rate of Soluable Organic N (mgN/L/day). + self._rate_res[5] = self._params['k_a'] * comps[4] * comps[9] + + # Hydrolysis Rate of Particulate Organics (mgCOD/L/day). + self._rate_res[6] = self._params['k_h'] * self._monods[6] \ + * (self._monods[1] + self._params['cf_h'] * self._monods[3] * self._monods[2]) \ + * comps[9] + + # Hydrolysis Rate of Particulate Organic N (mgN/L/day). + self._rate_res[7] = self._rate_res[6] * comps[12] / comps[8] + + return self._rate_res[:] + + + # OVERALL PROCESS RATE EQUATIONS FOR INDIVIDUAL COMPONENTS + + + def _rate0_S_DO(self): + """ + Overall process rate for dissolved O2 (mgCOD/L/d). + + Args: + None + + Return: + float + """ + return self._stoichs['0_0'] * self._rate_res[0] + self._stoichs['2_0'] * self._rate_res[2] + + + def _rate1_S_I(self): + """ + Overall process rate for inert soluble COD (mgCOD/L/d). + + Args: + None + + Return: + 0.0 + """ + return 0.0 + + + def _rate2_S_S(self): + """ + Overall process rate for soluble biodegradable COD (mgCOD/L/d). + + Args: + None + + Return: + float + """ + return (self._stoichs['0_2'] * self._rate_res[0] + + self._stoichs['1_2'] * self._rate_res[1] + + self._stoichs['6_2'] * self._rate_res[6]) + + + def _rate3_S_NH(self): + """ + Overall process rate for ammonia nitrogen (mgN/L/d). + + Args: + None + + Return: + float + """ + return (self._stoichs['0_3'] * self._rate_res[0] + + self._stoichs['1_3'] * self._rate_res[1] + + self._stoichs['2_3'] * self._rate_res[2] + + self._stoichs['5_3'] * self._rate_res[5]) + + + def _rate4_S_NS(self): + """ + Overall process rate for soluble organic nitrogen (mgN/L/d). + + Args: + None + + Return: + float + """ + return (self._stoichs['5_4'] * self._rate_res[5] + + self._stoichs['7_4'] * self._rate_res[7]) + + + def _rate5_S_NO(self): + """ + Overall process rate for nitrite/nitrate nitrogen (mgN/L/d). + + Args: + None + + Return: + float + """ + return (self._stoichs['1_5'] * self._rate_res[1] + + self._stoichs['2_5'] * self._rate_res[2]) + + + def _rate6_S_ALK(self): + """ + Overall process rate for alkalinity (mg/L/d as CaCO3) + + Args: + None + + Return: + float + """ + return (self._stoichs['0_6'] * self._rate_res[0] + + self._stoichs['1_6'] * self._rate_res[1] + + self._stoichs['2_6'] * self._rate_res[2] + + self._stoichs['5_6'] * self._rate_res[5]) + + + def _rate7_X_I(self): + """ + Overall process rate for inert particulate COD (mgCOD/L/d) + + Args: + None + + Return: + 0.0 + """ + return 0.0 + + + def _rate8_X_S(self): + """ + Overall process rate for particulate biodegradable COD (mgCOD/L/d). + + Args: + None + + Return: + float + """ + return (self._stoichs['3_8'] * self._rate_res[3] + + self._stoichs['4_8'] * self._rate_res[4] + + self._stoichs['6_8'] * self._rate_res[6]) + + + def _rate9_X_BH(self): + """ + Overall process rate for heterotrophic biomass (mgCOD/L/d). + + Args: + None + + Return: + float + """ + return (self._stoichs['0_9'] * self._rate_res[0] + + self._stoichs['1_9'] * self._rate_res[1] + + self._stoichs['3_9'] * self._rate_res[3]) + + + def _rate10_X_BA(self): + """ + Overall process rate for autotrophic biomass (mgCOD/L/d). + + Args: + None + + Return: + float + """ + return (self._stoichs['2_10'] * self._rate_res[2] + + self._stoichs['4_10'] * self._rate_res[4]) + + + def _rate11_X_D(self): + """ + Overall process rate for biomass debris (mgCOD/L/d). + + Args: + None + + Return: + float + """ + return (self._stoichs['3_11'] * self._rate_res[3] + + self._stoichs['4_11'] * self._rate_res[4]) + + + def _rate12_X_NS(self): + """ + Overall process rate for particulate organic nitrogen (mgN/L/d). + + Args: + None + + Return: + float + """ + return (self._stoichs['3_12'] * self._rate_res[3] + + self._stoichs['4_12'] * self._rate_res[4] + + self._stoichs['7_12'] * self._rate_res[7]) + + + def _dCdt(self, t, mo_comps, vol, flow, in_comps, fix_DO, DO_sat_T): + ''' + Defines dC/dt for the reactor based on mass balance. + + Overall mass balance: + dComp/dt == InfFlow / Actvol * (in_comps - mo_comps) + GrowthRate + == (in_comps - mo_comps) / HRT + GrowthRate + + Args: + t: time for use in ODE integration routine, d + mo_comps: list of model component for mainstream outlet, mg/L. + vol: reactor's active volume, m3; + flow: reactor's total inflow, m3/d + in_comps: list of model components for inlet, mg/L; + fix_DO: whether to use a fix DO setpoint, bool + DO_sat_T: saturation DO of the project elev. and temp, mg/L + + Return: + dC/dt of the system ([float]) + + ASM1 Components: + 0_S_DO, 1_S_I, 2_S_S, 3_S_NH, 4_S_NS, 5_S_NO, 6_S_ALK, + 7_X_I, 8_X_S, 9_X_BH, 10_X_BA, 11_X_D, 12_X_NS + ''' + + # Get all the Monod/Inhibition terms as well as the normalized reaction rates first + self._reaction_rate(mo_comps) + + _HRT = vol / flow + + # set DO rate to zero since DO is set to a fix conc., which is + # recommended for steady state simulation; alternatively, use the given + # KLa to dynamically estimate residual DO + if fix_DO or self._bulk_DO == 0: + result = [0.0] + else: #TODO: what if the user provides a fix scfm of air? + result = [(in_comps[0] - mo_comps[0]) / _HRT + + self._KLa * (DO_sat_T - mo_comps[0]) + + self._rate0_S_DO()] + + result.append((in_comps[1] - mo_comps[1]) / _HRT + + self._rate1_S_I()) + + result.append((in_comps[2] - mo_comps[2]) / _HRT + + self._rate2_S_S()) + + result.append((in_comps[3] - mo_comps[3]) / _HRT + + self._rate3_S_NH()) + + result.append((in_comps[4] - mo_comps[4]) / _HRT + + self._rate4_S_NS()) + + result.append((in_comps[5] - mo_comps[5]) / _HRT + + self._rate5_S_NO()) + + result.append((in_comps[6] - mo_comps[6]) / _HRT + + self._rate6_S_ALK()) + + result.append((in_comps[7] - mo_comps[7]) / _HRT + + self._rate7_X_I()) + + result.append((in_comps[8] - mo_comps[8]) / _HRT + + self._rate8_X_S()) + + result.append((in_comps[9] - mo_comps[9]) / _HRT + + self._rate9_X_BH()) + + result.append((in_comps[10] - mo_comps[10]) / _HRT + + self._rate10_X_BA()) + + result.append((in_comps[11] - mo_comps[11]) / _HRT + + self._rate11_X_D()) + + result.append((in_comps[12] - mo_comps[12]) / _HRT + + self._rate12_X_NS()) + + return result[:] + diff --git a/PooPyLab/ASMModel/asm_2d.py b/PooPyLab/ASMModel/asm_2d.py new file mode 100644 index 0000000..cee10af --- /dev/null +++ b/PooPyLab/ASMModel/asm_2d.py @@ -0,0 +1,736 @@ +# This file is part of PooPyLab. +# +# PooPyLab is a simulation software for biological wastewater treatment processes using International Water Association +# Activated Sludge Models. +# +# Copyright (C) Kai Zhang +# +# PooPyLab is free software: you can redistribute it and/or modify it under the terms of the GNU General Public +# License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with PooPyLab. If not, see +# . +# +# +# This is the definition of the ASM1 model to be imported as part of the Reactor object +# +# + + +"""Definition of the IWA Activated Sludge Model #1. + +Reference: + Henze, M; W.Gujer; T.Mino; M.v.Loosdrecht (edit) + IWA Task Group on Math. Modelling for Design and Operation of Biological Wastewater Treatment, 2000. Activated + Sludge Model No. 1, in Activated Sludge Models ASM1, ASM2, ASM2d, and ASM 3. +""" +## @namespace asm_1 +## @file asm_1.py + + +from ..ASMModel import constants +from .asmbase import asm_model + + +class ASM_2d(asm_model): + """ + Kinetics and stoichiometrics of IWA ASM 1 model. + """ + + __id = 0 + + def __init__(self, ww_temp=20, DO=2): + """ + Initialize the ASM 1 model with water temperature and dissolved O2. + + Args: + ww_temp: wastewater temperature, degC; + DO: dissolved oxygen, mg/L + + Return: + None + + See: + _set_ideal_kinetics_20C(); + _set_params(); + _set_stoichs(). + """ + + asm_model.__init__(self) + self.__class__.__id += 1 + + self._set_ideal_kinetics_20C_to_defaults() + + # wastewater temperature used in the model, degC + self._temperature = ww_temp + # mixed liquor bulk dissolved oxygen, mg/L + self._bulk_DO = DO + + # temperature difference b/t what's used and baseline (20C), degC + self._delta_t = self._temperature - 20 + + self.update(ww_temp, DO) + + #ASM2d Conversion Factors -- BEGIN + # Here are conversion factors that are not allowed to be altered by the user: + # COD: mg/L + self.__i_COD_SO2 = -1.0 + self.__i_COD_SF = self.__i_COD_SA = self.__i_COD_SI = 1.0 + self.__i_COD_SNO3 = -64.0/14.0 #O2 to fully nitrify + self.__i_COD_SN2 = -24.0/14.0 #O2 to oxidize NH3-N to N2-N, not the 2.86 mgO2/mgNO3-N in denite. + self.__i_COD_XI = self.__i_COD_XS = self.__i_COD_XH = 1.0 + self.__i_COD_XPAO = self.__i_COD_PHA = self.__i_COD_XAUT = 1.0 + # Nitrogen and Phosphorus: mg/L as N or P + self.__i_N_SNH4 = self.__i_N_SNO3 = self.__i_N_SN2 = 1.0 + self.__i_P_SPO4 = self.__i_P_XPP = 1.0 + self.__i_P_XMEP = 0.205 + # Charges in unit: mole + self.__i_CHG_SA = -1.0/64.0 + self.__i_CHG_SNH4 = 1.0/14.0 + self.__i_CHG_SNO3 = -1.0/14.0 + self.__i_CHG_SPO4 = -1.5/31.0 + self.__i_CHG_SALK = -1.0 + self.__i_CHG_XPP = -1.0/31.0 + # TSS: mg/L + self.__i_TSS_XPP = 3.23 + self.__i_TSS_XPHA = 0.6 + self.__i_TSS_TSS = -1.0 # yes, this is correct + self.__i_TSS_XMEOH = self.__i_TSS_XMEP = 1.0 + #ASM2d Conversion Factors --- END + + #user defined conversion factors: e.g. user_i_N_SF is for converted the ASM2d component SF (in COD) to organic N + self._model_conv_user = { + 'i_N_SF': 0.01, + 'i_P_SF': 0.002, + 'i_N_SI': 0.01, + 'i_P_SI': 0.002, + 'i_N_XI': 0.01, + 'i_P_XI': 0.002, + 'i_TSS_XI': 0.90, + 'i_N_XS': 0.01, + 'i_P_XS': 0.002, + 'i_TSS_XS': 0.90, + 'i_N_BM': 0.08, + 'i_P_BM': 0.016, + 'i_TSS_BM': 1.42 # same for all types of biomass, XH, XAUT, XPAO } + + self.__conv_factors = [ [self.__i_COD_O2, 0, 0, 0, 0], + [self.__i_COD_SF, self._model_conv['ASM2d']['i_N_SF'], self._model_conv['ASM2d']['i_P_SF'], 0, 0], + # TODO:continue here + ] + + # The Components the ASM components IN THE REACTOR + # + # ASM model components + self._comps = [0.0] * constants._NUM_ASM2d_COMPONENTS + + # Intermediate results of Monod or Inhibition Terms + self._monods = [1.0] * 7 # TODO: revise for asm2d + + # Intermediate results of rate expressions, M/L^3/T + # The list is to help speed up the calculation by reducing redundant calls of individual rate expressions in + # multiple mass balance equations for the model components. + # ASM1 has 8 bio processes. + self._rate_res = [0.0] * 8 # TODO: revise for asm2d + + return None + + ### TODO: ENTIRE INTERFACE TO BE REVISED FOR ASM2D + def _set_ideal_kinetics_20C_to_defaults(self): + """ + Set the kinetic params/consts @ 20C to default ideal values. + + See: + update(); + _set_params(); + _set_stoichs(). + """ + + # Ideal Growth Rate of Heterotrophs (u_max_H, 1/DAY) + self._kinetics_20C['u_max_H'] = 6.0 + + # Decay Rate of Heterotrophs (b_H, 1/DAY) + self._kinetics_20C['b_LH'] = 0.62 + + # Ideal Growth Rate of Autotrophs (u_max_A, 1/DAY) + self._kinetics_20C['u_max_A'] = 0.8 + + # Decay Rate of Autotrophs (b_A, 1/DAY) A wide range exists. Table 6.3 on Grady 1999 shows 0.096 (1/d). IWA's + # ASM report did not even show b_A on its table for typical value. ASIM software show a value of "0.000", + # probably cut off by the print function. I can only assume it was < 0.0005 (1/d) at 20C. + #self._kinetics_20C['b_LA'] = 0.096 + self._kinetics_20C['b_LA'] = 0.0007 + + # Half Growth Rate Concentration of Heterotrophs (K_s, mgCOD/L) + self._kinetics_20C['K_S'] = 20.0 + + # Switch Coefficient for Dissolved O2 of Hetero. (K_OH, mgO2/L) + self._kinetics_20C['K_OH'] = 0.2 + + # Association Conc. for Dissolved O2 of Auto. (K_OA, mgN/L) + self._kinetics_20C['K_OA'] = 0.4 + + # Association Conc. for NH3-N of Auto. (K_NH, mgN/L) + self._kinetics_20C['K_NH'] = 1.0 + + # Association Conc. for NOx of Hetero. (K_NO, mgN/L) + self._kinetics_20C['K_NO'] = 0.5 + + # Hydrolysis Rate (k_h, mgCOD/mgBiomassCOD-day) + self._kinetics_20C['k_h'] = 3.0 + + # Half Rate Conc. for Hetero. Growth on Part. COD + # (K_X, mgCOD/mgBiomassCOD) + self._kinetics_20C['K_X'] = 0.03 + + # Ammonification of Org-N in biomass (k_a, L/mgBiomassCOD-day) + self._kinetics_20C['k_a'] = 0.08 + + # Yield of Hetero. Growth on COD (Y_H, mgBiomassCOD/mgCODremoved) + self._kinetics_20C['Y_H'] = 0.67 + + # Yield of Auto. Growth on TKN (Y_A, mgBiomassCOD/mgTKNoxidized) + self._kinetics_20C['Y_A'] = 0.24 + + # Fract. of Debris in Lysed Biomass(f_D, gDebrisCOD/gBiomassCOD) + self._kinetics_20C['f_D'] = 0.08 + + # Correction Factor for Hydrolysis (cf_h, unitless) + self._kinetics_20C['cf_h'] = 0.4 + + # Correction Factor for Anoxic Heterotrophic Growth (cf_g, unitless) + self._kinetics_20C['cf_g'] = 0.8 + + # Ratio of N in Active Biomass (i_N_XB, mgN/mgActiveBiomassCOD) + self._kinetics_20C['i_N_XB'] = 0.086 + + # Ratio of N in Debris Biomass (i_N_XD, mgN/mgDebrisBiomassCOD) + self._kinetics_20C['i_N_XD'] = 0.06 + + return None + + + def _set_params(self): + """ + Set the kinetic parameters/constants @ project temperature. + + This function updates the self._params based on the model temperature and DO. + + See: + update(); + _set_ideal_kinetics_20C(); + _set_stoichs(). + """ + + # Ideal Growth Rate of Heterotrophs (u_max_H, 1/DAY) + self._params['u_max_H'] = self._kinetics_20C['u_max_H'] * pow(1.072, self._delta_t) + + # Decay Rate of Heterotrophs (b_H, 1/DAY) + self._params['b_LH'] = self._kinetics_20C['b_LH'] * pow(1.12, self._delta_t) + + # Ideal Growth Rate of Autotrophs (u_max_A, 1/DAY) + self._params['u_max_A'] = self._kinetics_20C['u_max_A'] * pow(1.103, self._delta_t) + + # Decay Rate of Autotrophs (b_A, 1/DAY) + self._params['b_LA'] = self._kinetics_20C['b_LA'] * pow(1.114, self._delta_t) + + # Half Growth Rate Concentration of Heterotrophs (K_s, mgCOD/L) + + self._params['K_S'] = self._kinetics_20C['K_S'] + + # Switch Coefficient for Dissolved O2 of Hetero. (K_OH, mgO2/L) + self._params['K_OH'] = self._kinetics_20C['K_OH'] + + # Association Conc. for Dissolved O2 of Auto. (K_OA, mgN/L) + self._params['K_OA'] = self._kinetics_20C['K_OA'] + + # Association Conc. for NH3-N of Auto. (K_NH, mgN/L) + self._params['K_NH'] = self._kinetics_20C['K_NH'] + + # Association Conc. for NOx of Hetero. (K_NO, mgN/L) + self._params['K_NO'] = self._kinetics_20C['K_NO'] + + # Hydrolysis Rate (k_h, mgCOD/mgBiomassCOD-day) + self._params['k_h'] = self._kinetics_20C['k_h'] * pow(1.116, self._delta_t) + + # Half Rate Conc. for Hetero. Growth on Part. COD + # (K_X, mgCOD/mgBiomassCOD) + self._params['K_X'] = self._kinetics_20C['K_X'] * pow(1.116, self._delta_t) + + # Ammonification of Org-N in biomass (k_a, L/mgBiomassCOD-day) + self._params['k_a'] = self._kinetics_20C['k_a'] * pow(1.072, self._delta_t) + + # Yield of Hetero. Growth on COD (Y_H, mgBiomassCOD/mgCODremoved) + self._params['Y_H'] = self._kinetics_20C['Y_H'] + + # Yield of Auto. Growth on TKN (Y_A, mgBiomassCOD/mgTKNoxidized) + self._params['Y_A'] = self._kinetics_20C['Y_A'] + + # Fract. of Debris in Lysed Biomass(f_D, gDebrisCOD/gBiomassCOD) + self._params['f_D'] = self._kinetics_20C['f_D'] + + # Correction Factor for Hydrolysis (cf_h, unitless) + self._params['cf_h'] = self._kinetics_20C['cf_h'] + + # Correction Factor for Anoxic Heterotrophic Growth (cf_g, unitless) + self._params['cf_g'] = self._kinetics_20C['cf_g'] + + # Ratio of N in Active Biomass (i_N_XB, mgN/mgActiveBiomassCOD) + self._params['i_N_XB'] = self._kinetics_20C['i_N_XB'] + + # Ratio of N in Debris Biomass (i_N_XD, mgN/mgDebrisBiomassCOD) + self._params['i_N_XD'] = self._kinetics_20C['i_N_XD'] + + return None + + + # STOCHIOMETRIC MATRIX + + + def _set_stoichs(self): + """ + Set the stoichiometrics for the model. + + Note: Make sure to match the .csv model template file in the model_builder folder, Sep 04, 2019): + + _stoichs['x_y'] ==> x is process rate id, and y is component id + + See: + _set_params(); + _set_ideal_kinetics_20C(); + update(). + """ + + # S_O for aerobic hetero. growth, as O2 + self._stoichs['0_0'] = (self._params['Y_H'] - 1.0) / self._params['Y_H'] + + # S_O for aerobic auto. growth, as O2 + self._stoichs['2_0'] = (self._params['Y_A'] - 4.57) / self._params['Y_A'] + + # S_S for aerobic hetero. growth, as COD + self._stoichs['0_2'] = -1.0 / self._params['Y_H'] + + # S_S for anoxic hetero. growth, as COD + self._stoichs['1_2'] = -1.0 / self._params['Y_H'] + + # S_S for hydrolysis of part. substrate + self._stoichs['6_2'] = 1.0 + + # S_NH required for aerobic hetero. growth, as N + self._stoichs['0_3'] = -self._params['i_N_XB'] + + # S_NH required for anoxic hetero. growth, as N + self._stoichs['1_3'] = -self._params['i_N_XB'] + + # S_NH required for aerobic auto. growth, as N + self._stoichs['2_3'] = -self._params['i_N_XB'] - 1.0 / self._params['Y_A'] + + # S_NH from ammonification, as N + self._stoichs['5_3'] = 1.0 + + # S_NS used by ammonification, as N + self._stoichs['5_4'] = -1.0 + + # S_NS from hydrolysis of part.TKN, as N + self._stoichs['7_4'] = 1.0 + + # S_NO for anoxic hetero. growth, as N + self._stoichs['1_5'] = (self._params['Y_H'] - 1.0) / (2.86 * self._params['Y_H']) + + # S_NO from nitrification, as N + self._stoichs['2_5'] = 1.0 / self._params['Y_A'] + + # S_ALK consumed by aerobic hetero. growth, as mM CaCO3 + self._stoichs['0_6'] = -self._params['i_N_XB'] / 14.0 + + # S_ALK generated by anoxic hetero. growth, as mM CaCO3 + self._stoichs['1_6'] = (1.0 - self._params['Y_H']) / (14.0 * 2.86 * self._params['Y_H']) \ + - self._params['i_N_XB'] / 14.0 + + # S_ALK consumed by aerobic auto. growth, as mM CaCO3 + self._stoichs['2_6'] = -self._params['i_N_XB'] / 14 - 1.0 / (7.0 * self._params['Y_A']) + + # S_ALK generated by ammonification, as mM CaCO3 + self._stoichs['5_6'] = 1.0 / 14.0 + + # X_S from hetero. decay, as COD + self._stoichs['3_8'] = 1.0 - self._params['f_D'] + + # X_S from auto. decay, as COD + self._stoichs['4_8'] = 1.0 - self._params['f_D'] + + # X_S consumed by hydrolysis of biomass + self._stoichs['6_8'] = -1.0 + + # X_BH from aerobic hetero. growth, as COD + self._stoichs['0_9'] = 1.0 + + # X_BH from anoxic hetero. growth, as COD + self._stoichs['1_9'] = 1.0 + + # X_BH lost in hetero. decay, as COD + self._stoichs['3_9'] = -1.0 + + # X_BA from aerobic auto. growth, as COD + self._stoichs['2_10'] = 1.0 + + # X_BA lost in auto. decay, as COD + self._stoichs['4_10'] = -1.0 + + + # X_D from hetero. decay, as COD + self._stoichs['3_11'] = self._params['f_D'] + + # X_D from auto. decay, as COD + self._stoichs['4_11'] = self._params['f_D'] + + # X_NS from hetero. decay, as N + self._stoichs['3_12'] = self._params['i_N_XB'] - self._params['f_D'] * self._params['i_N_XD'] + + # X_NS from auto. decay, as COD + self._stoichs['4_12'] = self._params['i_N_XB'] - self._params['f_D'] * self._params['i_N_XD'] + + # X_NS consumed in hydrolysis of part. TKN, as N + self._stoichs['7_12'] = -1.0 + + return None + + + # PROCESS RATE DEFINITIONS (Rj, M/L^3/T): + # + + + def _reaction_rate(self, comps): + """ + Normalized reaction rates for the biological processes. + + Args: + comps: list of current model components (concentrations). + + Return: + list of process rates M/L^3/T in self._rate_res[] + """ + + # Monod term for Heterotroph's substrate + self._monods[0] = self._monod(comps[2], self._params['K_S']) + + # Monod term for Heterotroph's O2 + self._monods[1] = self._monod(comps[0], self._params['K_OH']) + + # Monod term for Heterotroph's NOx-N + self._monods[2] = self._monod(comps[5], self._params['K_NO']) + + # O2 Inhibition term for anoxic Heterotrophs + self._monods[3] = self._monod(self._params['K_OH'], comps[0]) + + # Monod term for Autotroph's NH3-N + self._monods[4] = self._monod(comps[3], self._params['K_NH']) + + # Monod term for Autotroph's O2 + self._monods[5] = self._monod(comps[0], self._params['K_OA']) + + # Monod term for Hydrolysis + self._monods[6] = self._monod(comps[8] / comps[9], self._params['K_X']) + + # Aerobic Growth Rate of Heterotrophs (mgCOD/L/day). + self._rate_res[0] = self._params['u_max_H'] * self._monods[0] * self._monods[1] * comps[9] + + # Anoxic Growth Rate of Heterotrophs (mgCOD/L/day). + self._rate_res[1] = self._params['u_max_H'] * self._monods[0] * self._monods[2] \ + * self._monods[3] * self._params['cf_g'] * comps[9] + + # Aerobic Growth Rate of Autotrophs (mgCOD/L/day). + self._rate_res[2] = self._params['u_max_A'] * self._monods[4] * self._monods[5] * comps[10] + + # Death and Lysis Rate of Heterotrophs (mgCOD/L/day). + self._rate_res[3] = self._params['b_LH'] * comps[9] + + # Death and Lysis Rate of Autotrophs (mgCOD/L/day). + self._rate_res[4] = self._params['b_LA'] * comps[10] + + # Ammonification Rate of Soluable Organic N (mgN/L/day). + self._rate_res[5] = self._params['k_a'] * comps[4] * comps[9] + + # Hydrolysis Rate of Particulate Organics (mgCOD/L/day). + self._rate_res[6] = self._params['k_h'] * self._monods[6] \ + * (self._monods[1] + self._params['cf_h'] * self._monods[3] * self._monods[2]) \ + * comps[9] + + # Hydrolysis Rate of Particulate Organic N (mgN/L/day). + self._rate_res[7] = self._rate_res[6] * comps[12] / comps[8] + + return self._rate_res[:] + + + # OVERALL PROCESS RATE EQUATIONS FOR INDIVIDUAL COMPONENTS + + + def _rate0_S_DO(self): + """ + Overall process rate for dissolved O2 (mgCOD/L/d). + + Args: + None + + Return: + float + """ + return self._stoichs['0_0'] * self._rate_res[0] + self._stoichs['2_0'] * self._rate_res[2] + + + def _rate1_S_I(self): + """ + Overall process rate for inert soluble COD (mgCOD/L/d). + + Args: + None + + Return: + 0.0 + """ + return 0.0 + + + def _rate2_S_S(self): + """ + Overall process rate for soluble biodegradable COD (mgCOD/L/d). + + Args: + None + + Return: + float + """ + return (self._stoichs['0_2'] * self._rate_res[0] + + self._stoichs['1_2'] * self._rate_res[1] + + self._stoichs['6_2'] * self._rate_res[6]) + + + def _rate3_S_NH(self): + """ + Overall process rate for ammonia nitrogen (mgN/L/d). + + Args: + None + + Return: + float + """ + return (self._stoichs['0_3'] * self._rate_res[0] + + self._stoichs['1_3'] * self._rate_res[1] + + self._stoichs['2_3'] * self._rate_res[2] + + self._stoichs['5_3'] * self._rate_res[5]) + + + def _rate4_S_NS(self): + """ + Overall process rate for soluble organic nitrogen (mgN/L/d). + + Args: + None + + Return: + float + """ + return (self._stoichs['5_4'] * self._rate_res[5] + + self._stoichs['7_4'] * self._rate_res[7]) + + + def _rate5_S_NO(self): + """ + Overall process rate for nitrite/nitrate nitrogen (mgN/L/d). + + Args: + None + + Return: + float + """ + return (self._stoichs['1_5'] * self._rate_res[1] + + self._stoichs['2_5'] * self._rate_res[2]) + + + def _rate6_S_ALK(self): + """ + Overall process rate for alkalinity (mg/L/d as CaCO3) + + Args: + None + + Return: + float + """ + return (self._stoichs['0_6'] * self._rate_res[0] + + self._stoichs['1_6'] * self._rate_res[1] + + self._stoichs['2_6'] * self._rate_res[2] + + self._stoichs['5_6'] * self._rate_res[5]) + + + def _rate7_X_I(self): + """ + Overall process rate for inert particulate COD (mgCOD/L/d) + + Args: + None + + Return: + 0.0 + """ + return 0.0 + + + def _rate8_X_S(self): + """ + Overall process rate for particulate biodegradable COD (mgCOD/L/d). + + Args: + None + + Return: + float + """ + return (self._stoichs['3_8'] * self._rate_res[3] + + self._stoichs['4_8'] * self._rate_res[4] + + self._stoichs['6_8'] * self._rate_res[6]) + + + def _rate9_X_BH(self): + """ + Overall process rate for heterotrophic biomass (mgCOD/L/d). + + Args: + None + + Return: + float + """ + return (self._stoichs['0_9'] * self._rate_res[0] + + self._stoichs['1_9'] * self._rate_res[1] + + self._stoichs['3_9'] * self._rate_res[3]) + + + def _rate10_X_BA(self): + """ + Overall process rate for autotrophic biomass (mgCOD/L/d). + + Args: + None + + Return: + float + """ + return (self._stoichs['2_10'] * self._rate_res[2] + + self._stoichs['4_10'] * self._rate_res[4]) + + + def _rate11_X_D(self): + """ + Overall process rate for biomass debris (mgCOD/L/d). + + Args: + None + + Return: + float + """ + return (self._stoichs['3_11'] * self._rate_res[3] + + self._stoichs['4_11'] * self._rate_res[4]) + + + def _rate12_X_NS(self): + """ + Overall process rate for particulate organic nitrogen (mgN/L/d). + + Args: + None + + Return: + float + """ + return (self._stoichs['3_12'] * self._rate_res[3] + + self._stoichs['4_12'] * self._rate_res[4] + + self._stoichs['7_12'] * self._rate_res[7]) + + + def _dCdt(self, t, mo_comps, vol, flow, in_comps, fix_DO, DO_sat_T): + ''' + Defines dC/dt for the reactor based on mass balance. + + Overall mass balance: + dComp/dt == InfFlow / Actvol * (in_comps - mo_comps) + GrowthRate + == (in_comps - mo_comps) / HRT + GrowthRate + + Args: + t: time for use in ODE integration routine, d + mo_comps: list of model component for mainstream outlet, mg/L. + vol: reactor's active volume, m3; + flow: reactor's total inflow, m3/d + in_comps: list of model components for inlet, mg/L; + fix_DO: whether to use a fix DO setpoint, bool + DO_sat_T: saturation DO of the project elev. and temp, mg/L + + Return: + dC/dt of the system ([float]) + + ASM1 Components: + 0_S_DO, 1_S_I, 2_S_S, 3_S_NH, 4_S_NS, 5_S_NO, 6_S_ALK, + 7_X_I, 8_X_S, 9_X_BH, 10_X_BA, 11_X_D, 12_X_NS + ''' + + # Get all the Monod/Inhibition terms as well as the normalized reaction rates first + self._reaction_rate(mo_comps) + + _HRT = vol / flow + + # set DO rate to zero since DO is set to a fix conc., which is + # recommended for steady state simulation; alternatively, use the given + # KLa to dynamically estimate residual DO + if fix_DO or self._bulk_DO == 0: + result = [0.0] + else: #TODO: what if the user provides a fix scfm of air? + result = [(in_comps[0] - mo_comps[0]) / _HRT + + self._KLa * (DO_sat_T - mo_comps[0]) + + self._rate0_S_DO()] + + result.append((in_comps[1] - mo_comps[1]) / _HRT + + self._rate1_S_I()) + + result.append((in_comps[2] - mo_comps[2]) / _HRT + + self._rate2_S_S()) + + result.append((in_comps[3] - mo_comps[3]) / _HRT + + self._rate3_S_NH()) + + result.append((in_comps[4] - mo_comps[4]) / _HRT + + self._rate4_S_NS()) + + result.append((in_comps[5] - mo_comps[5]) / _HRT + + self._rate5_S_NO()) + + result.append((in_comps[6] - mo_comps[6]) / _HRT + + self._rate6_S_ALK()) + + result.append((in_comps[7] - mo_comps[7]) / _HRT + + self._rate7_X_I()) + + result.append((in_comps[8] - mo_comps[8]) / _HRT + + self._rate8_X_S()) + + result.append((in_comps[9] - mo_comps[9]) / _HRT + + self._rate9_X_BH()) + + result.append((in_comps[10] - mo_comps[10]) / _HRT + + self._rate10_X_BA()) + + result.append((in_comps[11] - mo_comps[11]) / _HRT + + self._rate11_X_D()) + + result.append((in_comps[12] - mo_comps[12]) / _HRT + + self._rate12_X_NS()) + + return result[:] + diff --git a/PooPyLab/ASMModel/asmbase.py b/PooPyLab/ASMModel/asmbase.py new file mode 100644 index 0000000..53a14d3 --- /dev/null +++ b/PooPyLab/ASMModel/asmbase.py @@ -0,0 +1,268 @@ +# This file is part of PooPyLab. +# +# PooPyLab is a simulation software for biological wastewater treatment processes using International Water Association +# Activated Sludge Models. +# +# Copyright (C) Kai Zhang +# +# PooPyLab is free software: you can redistribute it and/or modify it under the terms of the GNU General Public +# License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with PooPyLab. If not, see +# . +# +# +# This is the definition of the ASM1 model to be imported as part of the Reactor object +# +# + + + +"""Definition of the common interface for IWA Activated Sludge Models +""" +## @namespace asmbase +## @file asmbase.py + + +class asm_model(object): + """ + Common interface for all IWA Activated Sludge Models + """ + + def __init__(self, ww_temp=20, DO=2): + """ + Initialize the ASM 1 model with water temperature and dissolved O2. + + Args: + ww_temp: wastewater temperature, degC; + DO: dissoved oxygen, mg/L + + Return: + None + + See: + _set_ideal_kinetics_20C_to_defaults(); + _set_params(); + _set_stoichs(). + """ + + # wastewater temperature used in the model, degC + self._temperature = ww_temp + # mixed liquor bulk dissolved oxygen, mg/L + self._bulk_DO = DO + + # define the model parameters and stochoimetrics as dict() so that it + # is easier to keep track of names and values + + # default kinetic constants AT 20 degree Celcius under ideal conditions + self._kinetics_20C = {} + + # kinetic parameters AT PROJECT TEMPERATURE + self._params = {} + + # stoichiometrics + self._stoichs = {} + + # ASM model components + self._comps = [] + + # temperature difference b/t what's used and baseline (20C), degC + self._delta_t = self._temperature - 20 + + # oxygen transfer coefficient, mg/L-day + # placeholder for now, let + # OUR = 60 mg/L-hr; + # Saturation DO = 9 mg/L; and + # DO in mixed liquor = 2 mg/L + self._KLa = 60 * 24 / (9 - 2) + + return None + + + def alter_kinetic_20C(self, name, new_val): + """ + Alter a model kinetic constant at 20C (baseline temperature). + + Args: + name: name of the parameter to be altered (i.e. 'u_max_H'); + new_val: new parameter numeric value at 20C + + Return: + None + + See: + update() + """ + if new_val > 0 and name in self._kinetics_20C.keys(): + self._kinetics_20C[name] = new_val + else: + print('ERROR IN ALTERING 20C KINETICS. NO PARAMETER WAS CHANGED') + + return None + + + def update(self, ww_temp, DO): + """ + Update the ASM model with new water temperature and dissolved O2. + + Args: + ww_temp: wastewater temperature, degC; + DO: dissolved oxygen, mg/L + + Return: + None + + See: + _set_params(); + _set_stoichs(). + """ + self._temperature = ww_temp + self._bulk_DO = DO + self._delta_t = self._temperature - 20.0 + self._set_params() + self._set_stoichs() + return None + + + def set_KLa(self, kla): + """ + Set KLa value. + + Args: + kla: new KLa value, mg/m3-day + + Return: + None + """ + if kla > 0: + self._KLa = kla + else: + print("ERROR IN USER GIVEN KLa VALUE. KLa NOT CHANGED") + + return None + + + def get_params(self): + """ + Return the values of the kinetic parameter dictionary. + """ + return self._params.copy() + + + def get_stoichs(self): + """ + Return the values of the stoichiometric dictionary. + """ + return self._stoichs.copy() + + + def get_all_comps(self): + """ + Return a copy of the model components (concentrations). + """ + return self._comps[:] + + + def get_bulk_DO(self): + """ + Return the bulk dissolved O2 concentration, mg/L. + """ + return self._bulk_DO + + + def _set_ideal_kinetics_20C_to_defaults(self): + """ + Set the kinetic params/consts @ 20C to default ideal values. + + See: + update(); + _set_params(); + _set_stoichs(). + """ + pass + + + def _set_params(self): + """ + Set the kinetic parameters/constants to the project temperature & DO. + + See: + _set_ideal_kinetics_20C_to_defaults(); + _set_params(); + _set_stoichs(); + update(). + """ + pass + + + def _set_stoichs(self): + """ + Set the stoichiometrics for the model. + + Not implemented with details here but in actual models. + + Note: + Make sure to match the .csv model template file in the + model_builder folder, Sep 04, 2019): + + _stoichs['x_y'] ==> x is process rate id, and y is component id + + See: + _set_params(); + _set_ideal_kinetics_20C_to_defaults(); + update(). + """ + pass + + + def _monod(self, term_in_num_denum, term_only_in_denum): + """ + Template for Monod kinetics or switches. + + The following kinetics/swithes all use the _monod() function: + Monod kinetic of solube biodegradable COD on Heterotrophs; + Monod switch of Dissol. O2 on Heterotrophs; + Monod switch of Dissol. O2 on Autotrophs; + Monod kinetic of Ammonia-N on Autotrophs; + Monod kinetic of NOx-N on Autotrophs. + + Args: + term_in_num_denum: the term in both numerator & denumerator + term_only_in_denum: the term only in numerator + + Return: + float + """ + return term_in_num_denum / (term_in_num_denum + term_only_in_denum) + + + def _dCdt(self, t, mo_comps, vol, flow, in_comps): + ''' + Defines dC/dt for the reactor based on mass balance. + + Overall mass balance: + dComp/dt == InfFlow / Actvol * (in_comps - mo_comps) + GrowthRate + == (in_comps - mo_comps) / HRT + GrowthRate + + Args: + t: time for use in ODE integration routine, d + mo_comps: list of model component for mainstream outlet, mg/L. + vol: reactor's active volume, m3; + flow: reactor's total inflow, m3/d + in_comps: list of model compoennts for inlet, mg/L; + + Return: + dC/dt of the system ([float]) + + ASM1 Components: + 0_S_DO, 1_S_I, 2_S_S, 3_S_NH, 4_S_NS, 5_S_NO, 6_S_ALK, + 7_X_I, 8_X_S, 9_X_BH, 10_X_BA, 11_X_D, 12_X_NS + + Not implemented with details here but in the actual models. + ''' + pass + diff --git a/PooPyLab/ASMModel/asmreactor.pmt b/PooPyLab/ASMModel/asmreactor.pmt new file mode 100644 index 0000000..7b8d4e8 --- /dev/null +++ b/PooPyLab/ASMModel/asmreactor.pmt @@ -0,0 +1,12 @@ +Flow Balance: + 0 = IN - MO + 0 = IN - Sum(all dischargers' contribution) + + +Mass Balance: +Flow data source for the main outlet can be UPS, DNS, and PRG (e.g. pipe-->WAS). +Assign the main outlet flow as needed. +Sum up the dischargers contributions. + +Concentrations would be assigned from inlet (flow weighted averages) +to the main and side outlets. diff --git a/PooPyLab/ASMModel/base.py b/PooPyLab/ASMModel/base.py deleted file mode 100755 index e949c63..0000000 --- a/PooPyLab/ASMModel/base.py +++ /dev/null @@ -1,204 +0,0 @@ -# This file is part of PooPyLab. -# -# PooPyLab is a simulation software for biological wastewater treatment -# processes using the International Water Association Activated Sludge md -# Models. -# -# Copyright (C) Kai Zhang -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -# -# Definition of the Base object for WWTP components such as -# reactors. -# -# Update Log: -# Feb 03, 2018 KZ: reviewed -# Jul 21, 2017 KZ: made it more pythonic -# Mar 21, 2017 KZ: Migrated to Python3 -# Jun 16, 2015 KZ: Removed Set(Get)PreFix(), Set(Get)Group(); -# Renamed SetAs(Is)Done() to SetAs(Is)Visited() -# Mar 20, 2015 KZ: Added Set(Get)PreFix(), Set(Get)Group(), -# SetAs(Is)Done() for tracking status in loop finding -# Nov 20, 2014 KZ: Added UpstreamConnected() and MainOutletConnected() -# Jun 29, 2014 KZ: Replace Interpret() with GetXXXX() functions -# Jun 28, 2014 KZ: Added GetTSS(), getTotalCOD(), and getSoluableCOD() -# Mar 15, 2014 KZ: Moved AddUpstreamUnit(), RemoveUpstreamUnit(), and -# SetDownstreamMainUnit() to Pipe() -# Mar 06, 2014 KZ: Re-wrote the common interface and change Base into an -# abstract class -# Dec 25, 2013 KZ: commented out the BlendComponent() function in -# ReceiveFrom() -# Dec 17, 2013 KZ: added _PrevComp[0:12] to store state variables from -# previous iteration -# Dec 07, 2013 Kai Zhang (KZ) - - - -from abc import ABCMeta, abstractmethod - -class base(object): - ''' - base() Object defines the common interfaces for all PooPyLab objects. - ''' - - __metaclass__ = ABCMeta - - @abstractmethod - def get_upstream_units(self): - ''' Get the dict that stores all the upstream units that feed into - current one - Return Type: dict - ''' - pass - - @abstractmethod - def get_downstream_main_unit(self): - ''' Get the single unit downstream of the current one - Return Type: base.Base - ''' - pass - - @abstractmethod - def totalize_flow(self): - ''' Totalize all the flows entering the current unit. - Return type: NO Return - ''' - pass - - @abstractmethod - def blend_components(self): - ''' - blend_components() for Base mixes the contents in all inlet - components and send to the OUTLET, assuming no reaction - takes palce. - The definition is changed in ASMReactor where the mixture - is passed to the INLET of the reactor before reactions. - ''' - pass - - @abstractmethod - def update_combined_input(self): - ''' Combined the flows and loads into the current unit''' - pass - - @abstractmethod - def get_outlet_flow(self): - ''' Return the total out flow of the current unit (mainstream) - Return value type: float/double - ''' - pass - - @abstractmethod - def get_outlet_concs(self): - ''' Return the effluent concentrations of the current unit (mainstream) - Return type: list - ''' - pass - - @abstractmethod - def discharge(self): - ''' Pass the total flow and blended components to the next unit. - ''' - pass - - #@abstractmethod - #def Interpret(self): - # ''' Convert user input to model components''' - #for example, user may input BOD, TSS, VSS, TKN etc. this function - #coverts those into influent model components for the solver - # pass - - @abstractmethod - def has_sidestream(self): - ''' Check if the current unit has a sidestream discharge. - Default = False, i.e. no sidestream - Return type: boolean - ''' - pass - - @abstractmethod - def get_TSS(self): - ''' Return the Total Suspsended Solids (TSS) in the unit ''' - pass - - @abstractmethod - def get_VSS(self): - ''' Return the Volatile Suspended Solids (VSS) in the unit ''' - pass - - @abstractmethod - def get_total_COD(self): - ''' Return the Total COD (soluable and particulate) in the unit ''' - pass - - @abstractmethod - def get_soluble_COD(self): - ''' Return the SOLUABLE COD in the unit ''' - pass - - @abstractmethod - def get_particulate_COD(self): - ''' Return the PARTICULATE COD in the unit ''' - pass - - @abstractmethod - def get_TN(self): - ''' Return the Total Nitrogen of the unit ''' - pass - - @abstractmethod - def get_particulate_N(self): - ''' Return organic nitrogen of the unit ''' - pass - - @abstractmethod - def get_soluble_N(self): - ''' Return soluable nitrogen of the unit ''' - pass - - @abstractmethod - def get_organic_N(self): - ''' Return organic nitrogen of the unit ''' - pass - - @abstractmethod - def get_inorganic_N(self): - ''' Return inorganic nitrogen of the unit ''' - pass - - @abstractmethod - def upstream_connected(self): - ''' Return True if upstream is connected, False if not''' - pass - - @abstractmethod - def main_outlet_connected(self): - ''' Return True if the downstream main outlet is connected, - False if not. - ''' - pass - - @abstractmethod - def set_as_visited(self, Status): - ''' Set the unit as True when done visiting it in the loop finding - process. Status = False by default. - ''' - pass - - @abstractmethod - def is_visited(self): - ''' Return True if the unit is labelled as visited, False otherwise ''' - pass - diff --git a/PooPyLab/ASMModel/clarifier.py b/PooPyLab/ASMModel/clarifier.py deleted file mode 100755 index 2a23bc2..0000000 --- a/PooPyLab/ASMModel/clarifier.py +++ /dev/null @@ -1,48 +0,0 @@ -# This file is part of PooPyLab. -# -# PooPyLab is a simulation software for biological wastewater treatment -# processes using International Water Association Activated Sludge Models. -# -# Copyright (C) Kai Zhang -# -# PooPyLab is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# PooPyLab is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with PooPyLab. If not, see . -# -# -# Definition of Primary and Final Clarifier units. -# It is assumed that clarifiers are circular. -# Author: Kai Zhang -# -# Change Log: -# July 30, 2017 KZ: made it more pythonic. -# March 21, 2017 KZ: Migrated to Python3 -# May 26, 2014 KZ: Changed base class from Base to Splitter -# July 11, 2013 KZ: Initial commit -# - -import splitter - -class final_clarifier(splitter.splitter): - __id = 0 - def __init__(self, ActiveVol=380.0, SWD=3.5, Temperature=20.0, DO=2.0): - splitter.splitter.__init__(self) - self.__class__.__id += 1 - self.__name__ = "FinalClarifier_" + str(self.__id) - # SWD = side water depth in meters, default = ~12 ft - # ActiveVol in m^3, default value equals to 100,000 gallons - # Temperature = 20 C by default - # DO = dissolved oxygen, default = 2.0 mg/L - - self._active_vol = ActiveVol - self._SWD = SWD - self._area = self._active_vol / self._SWD diff --git a/PooPyLab/ASMModel/constants.py b/PooPyLab/ASMModel/constants.py index c7e4690..7e5dde0 100755 --- a/PooPyLab/ASMModel/constants.py +++ b/PooPyLab/ASMModel/constants.py @@ -1,32 +1,38 @@ # This file is part of PooPyLab. # -# PooPyLab is a simulation software for biological wastewater treatment -# processes using International Water Association Activated Sludge Models. -# +# PooPyLab is a simulation software for biological wastewater treatment processes using International Water Association +# Activated Sludge Models. +# # Copyright (C) Kai Zhang # -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -# -# This is the definition of some constants that will be used throughout -# the PooPyLab program -# -# Author: Kai Zhang -# Last Update: June 22, 2013 -# +# PooPyLab is free software: you can redistribute it and/or modify it under the terms of the GNU General Public +# License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with PooPyLab. If not, see +# . +# +# +# This is the definition of the ASM1 model to be imported as part of the Reactor object +# +# + + +"""Definitions of some constants (may change soon). +""" +## @namespace constants +## @file constants.py + +## number of ASM 1 model components _NUM_ASM1_COMPONENTS = 13 +## number of ASM 3 model components _NUM_ASM3_COMPONENTS = 13 +## number of ASM 2d model components _NUM_ASM2d_COMPONENTS = 21 +## convergence limit +_CONVERG_LIMIT = 1E-5 diff --git a/PooPyLab/ASMModel/effluent.pmt b/PooPyLab/ASMModel/effluent.pmt new file mode 100644 index 0000000..8991199 --- /dev/null +++ b/PooPyLab/ASMModel/effluent.pmt @@ -0,0 +1,19 @@ +#An Effluent unit only deals with its own inlet since its main outlet +#feeds to nothing within the process boundaries. +# +#Its inlet flow data source can only be set to UPS or PRG. +# +#The inlet flwo data source is UPS when there is no WAS units in the PFD. For instance, a CSTR. +# +#If the inlet flow data source is PRG, it would be either defined by +#the user or calculations. If calculated, the current effluent unit +#shall be the only effluent type (except for WAS) for such estimate. +# +#The flow balance would be a global one across the entire plant, +#including the total of all influents, WAS unit, the all other +#effluent units whose flows are defined. + +[PRG] +FLOW: ZERO = PLANT_IN_FLOW - PLANT_WAS_FLOW - MY_MO_FLOW +FLOW: ZERO = MY_IN_FLOW - MY_MO_FLOW +CONC: ZERO = MY_IN_CONC - MY_MO_CONC diff --git a/PooPyLab/ASMModel/effluent.py b/PooPyLab/ASMModel/effluent.py deleted file mode 100755 index 305a4ea..0000000 --- a/PooPyLab/ASMModel/effluent.py +++ /dev/null @@ -1,50 +0,0 @@ -# This file is part of PooPyLab. -# -# PooPyLab is a simulation software for biological wastewater treatment -# processes using International Water Association Activated Sludge Models. -# -# Copyright (C) Kai Zhang -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -# -# Definition of the plant Effluent unit -# -# Update Log: -# Jul 30, 2017 KZ: Made it more pythonic. -# Mar 21, 2017 KZ: Migrated to Python3 -# Sep 26, 2014 KZ: removed test code -# July 2, 2014 KZ: change base class to Pipe. -# June 29, 2014 KZ: removed Convert() that converts model parameters for -# user parameters -# December 25, 2013 KZ: commented out the BlendComponent() function in -# ReceiveFrom() -# December 17, 2013 KZ: Added _PrevComp[0..12] to store state variables from -# previous iteration -# -# December 07, 2013 Kai Zhang - - -import pipe - -class effluent(pipe.pipe): - __id = 0 - - def __init__(self): - pipe.pipe.__init__(self) - self.__class__.__id += 1 - self.__name__ = "Effluent_" + str(self.__id) - self._main_outlet_connected = True # dummy - print(self.__name__, "initialized successfully.") - diff --git a/PooPyLab/ASMModel/influent.pmt b/PooPyLab/ASMModel/influent.pmt new file mode 100644 index 0000000..cc99003 --- /dev/null +++ b/PooPyLab/ASMModel/influent.pmt @@ -0,0 +1,29 @@ +#Influent is essentially a Pipe. +# +#It provides the definitions of flows and concentrations. +# +#The inlet is NOT to be defined, only the main outlet will be used to supply +#information to the receiving unit. +# +#straightly define the main outlet flows and concentrations as known. +#build in fractionations for cod, n, p, s, etc. +# +#For a steady state simulation, define the influent condition in this file. +#For a dynamic simulation, the influent condition will be save in a separate +#.csv file, whose filename is stored in this model file. + +[UPS] +FLOW: ZERO = USER_DEFINED_IN_FLOW - MY_MO_FLOW +CONC: ZERO = USER_DEFINED_IN_CONC - MY_MO_CONC + +[TBD] +FLOW: ZERO = USER_DEFINED_IN_FLOW - MY_MO_FLOW +CONC: ZERO = USER_DEFINED_IN_CONC - MY_MO_CONC + +[DNS] +FLOW: ZERO = USER_DEFINED_IN_FLOW - MY_MO_FLOW +CONC: ZERO = USER_DEFINED_IN_CONC - MY_MO_CONC + +[PRG] +FLOW: ZERO = USER_DEFINED_IN_FLOW - MY_MO_FLOW +CONC: ZERO = USER_DEFINED_IN_CONC - MY_MO_CONC diff --git a/PooPyLab/ASMModel/influent.py b/PooPyLab/ASMModel/influent.py deleted file mode 100755 index f60c9ba..0000000 --- a/PooPyLab/ASMModel/influent.py +++ /dev/null @@ -1,306 +0,0 @@ -# This file is part of PooPyLab. -# -# PooPyLab is a simulation software for biological wastewater treatment -# processes using International Water Association Activated Sludge Models. -# -# Copyright (C) Kai Zhang -# -# PooPyLab is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# PooPyLab is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with PooPyLab. If not, see . -# -# -# Definition of the plant Influent unit. -# -# Change Log: -# July 31, 2017 KZ: Made it more pythonic and changed to python3. -# June 16, 2015 KZ: Removed _prefix, _group status and -# Set(Get)PreFixStatus(), Set(Get)GroupStatus; -# Renamed _Done to _visited, SetAs(Is)Done() to -# SetAs(Is)Visited(). -# March 20, 2015 KZ: Added _prefix, _group, _Done status and -# Set(Get)PreFixStatus(), Set(Get)GroupStatus, -# SetAs(Is)Done(). -# November 18, 2014 KZ: Added UpstreamConnected() and set to True -# November 12, 2014 KZ: added _main_outlet_connected flag -# and MainOutletConnected() function -# October 19, 2014 KZ: removed test code -# March 15, 2014: KZ: redefined for the new class structure. -# December 07, 2013 Kai Zhang: first draft - - -import base, constants - -class influent(base.base): - __id = 0 - - def __init__(self): - self.__class__.__id += 1 - self.__name__ = "Influent_" + str(self.__id) - - # Store the influent characteristics in a list() - # For ASM #1: - # - # self._inf_comp[0]: X_I - # self._inf_comp[1]: X_S - # self._inf_comp[2]: X_BH - # self._inf_comp[3]: X_BA - # self._inf_comp[4]: X_D - # self._inf_comp[5]: S_I - # self._inf_comp[6]: S_S - # self._inf_comp[7]: S_DO - # self._inf_comp[8]: S_NO - # self._inf_comp[9]: S_NH - # self._inf_comp[10]: S_NS - # self._inf_comp[11]: X_NS - # self._inf_comp[12]: S_ALK - self._inf_comp = [0.0] * constants._NUM_ASM1_COMPONENTS - - # Influent characteristics from user measurements/inputs - # Setting default values for municipal wastewater in USA - # Default Unit: mg/L except where noted differently - self._BOD5 = 250.0 - self._TSS = 250.0 - self._VSS = 200.0 - self._TNK = 40.0 - self._NH3 = 28.0 - self._NO = 0.0 - self._TP = 10.0 - self._Alk = 6.0 # in mmol/L as CaCO3 - self._DO = 0.0 - - # Plant influent flow in M3/DAY - # TODO: will be user-input from GUI. FROM THE GUI, USER - # will use MGD. DEFAULT VALUE = 10 MGD. - self._design_flow = 10 * 1000 / 3.78 # convert to M3/day - - # the reactor that receives the plant influent - self._outlet = None - - # the status about whether there is a downstream unit - self._main_outlet_connected = False - - # boolean on whether the unit is already visited by the loop - # finding process - self._prefix = False - - # _group is '' (empty string) if the unit has not been assigned to any - # specific group by the loop finding process. - # If the unit is assigned to a group, _group will record the group ID. - #TODO: IS THIS STILL USED AT ALL? - self._group = '' - - # boolean on whether the loop finding process has finished - # analyzing the unit - self._visited = False - - print(self.__name__,' initialized successfully.') - - def get_upstream_units(self): - ''' Get the {} that stores all the upstream units that feed into - the current one - Return Type: None (for Influent) or dict (for others) - ''' - return None - - def set_downstream_main_unit(self, rcvr): - ''' Set the downstream unit that will receive effluent from - the current unit - ''' - if self._outlet != rcvr: #if the specified rcvr has not been added - self._outlet = rcvr - self._main_outlet_connected = True - if rcvr != None: - rcvr.add_upstream_unit(self) - - def upstream_connected(self): - '''Placeholder, always return True for Influent as it - doesn't need an upstream unit - ''' - return True - - def main_outlet_connected(self): - ''' Return the status of outlet connection''' - return self._main_outlet_connected - - def get_downstream_main_unit(self): - ''' Get the single unit downstream of the current one - Return Type: base.Base - ''' - return self._outlet - - def set_as_visited(self, status=False): - self._visited = status - - def is_visited(self): - return self._visited - - def totalize_flow(self): - ''' Totalize all the flows entering the current unit. - Return type: NO Return - Does nothing for Influent object as the flow is fixed. - ''' - pass - - def blend_components(self): - ''' - blend_components() for Base mixes the contents in all inlet - components and send to the OUTLET, assuming no reaction - takes palce. - The definition is changed in ASMReactor where the mixture - is passed to the INLET of the reactor before reactions. - Does nothing for Inlfuent object as the components are in a - single source. - ''' - pass - - def update_combined_input(self): - ''' Combined the flows and loads into the current unit - Doesn't need to do anything since Influent object does not - receive any upstream input. - ''' - pass - - def get_outlet_flow(self): - ''' Return the total out flow of the current unit (mainstream) - ''' - return self._design_flow - - def get_outlet_concs(self): - ''' Return the effluent concentrations of the current unit (mainstream) - ''' - return self._inf_comp - - def discharge(self): - ''' Pass the total flow and blended components to the next unit. - ''' - if self._outlet != None: - self.get_downstream_main_unit().update_combined_input() - - def receive_from(self, dschgr=None): - ''' Influent doesn't need to receive anything''' - pass - - def interpret(self): - ''' Convert user input parameters into the ones that the ASM model - can understand - ''' - #TODO: the first set of conversion available here is for municipal - # wastewater. Industrial wastewater may have completely different - # conversion factors and needs to be tested. - - # influent biodegradable COD, BOD/COD = 1.71 for typ. muni.WW - Inf_CODb = self._BOD5 * 1.71 - # influent total COD, COD/BOD5 = 2.04 per BioWin - Inf_CODt = self._BOD5 * 2.04 - # influent total innert COD, - Inf_CODi = Inf_CODt - Inf_CODb - # influent soluble innert COD - Inf_S_I = 0.13 * Inf_CODt - # influent particulate innert COD - Inf_X_I = Inf_CODi - Inf_S_I - # influent particulate biodegradable COD - Inf_X_S = 1.6 * self._VSS - Inf_X_I - # influent soluble biodegradable COD - Inf_S_S = Inf_CODb - Inf_X_S - # influent Heterotrophs (mgCOD/L), - Inf_X_BH = 0.0 - # influent Autotrophs (mgCOD/L), - Inf_X_BA = 0.0 - # influent Biomass Debris (mgCOD/L) - Inf_X_D = 0.0 - - # influent TKN (mgN/L), NOT IN InfC - Inf_TKN = self._TNK - # influent Ammonia-N (mgN/L), - Inf_S_NH = self._NH3 - # subdividing TKN into: - # a) nonbiodegradable TKN - NonBiodegradable_TKN_Ratio = 0.03 # TODO: need to be configurable - # NON-BIODEGRADABLE TKN WILL HAVE TO BE ADDED BACK TO THE EFFLUENT TN - Inf_nb_TKN = Inf_TKN * NonBiodegradable_TKN_Ratio - # Grady 1999: - Soluble_Biodegradable_OrgN_Ratio = Inf_S_S / (Inf_S_S + Inf_X_S) - # b) soluble biodegrable TKN, - Inf_S_NS = (Inf_TKN - Inf_S_NH - Inf_nb_TKN)\ - * Soluble_Biodegradable_OrgN_Ratio - # c) particulate biodegradable TKN - Inf_X_NS = (Inf_TKN - Inf_S_NH - Inf_nb_TKN)\ - * (1.0 - Soluble_Biodegradable_OrgN_Ratio) - - # influent Nitrite + Nitrate (mgN/L) - Inf_S_NO = self._NO - - Inf_S_ALK = self._Alk - - Inf_S_DO = self._DO - - #store the converted information in the ASM Components for the influent - self._inf_comp = [Inf_X_I, Inf_X_S, Inf_X_BH, Inf_X_BA, Inf_X_D, \ - Inf_S_I, Inf_S_S, -Inf_S_DO, Inf_S_NO, Inf_S_NH, \ - Inf_S_NS, Inf_X_NS, Inf_S_ALK] - - def has_sidestream(self): - ''' Check if the current unit has a sidestream discharge. - Default = False, i.e. no sidestream - Return type: boolean - ''' - return False - - def set_flow(self, flow): - if flow >= 0.0: - self._design_flow = flow - return self._design_flow - - def get_TSS(self): - ''' Return the Total Suspsended Solids (TSS) in the unit ''' - return self._TSS - - def get_VSS(self): - ''' Return the Volatile Suspended Solids (VSS) in the unit ''' - return self._VSS - - def get_total_COD(self): - ''' Return the Total COD (both soluable and particulate) in the unit''' - return self._inf_comp[0] + self._inf_comp[1] + self._inf_comp[2] \ - + self._inf_comp[3] + self._inf_comp[4] + self._inf_comp[5] \ - + self._inf_comp[6] - - def get_soluble_COD(self): - ''' Return the SOLUABLE COD in the unit ''' - return self._inf_comp[5] + self._inf_comp[6] - - def get_particulate_COD(self): - ''' Return the PARTICULATE COD in the unit ''' - return self.get_total_COD() - self.get_soluble_COD() - - def get_TN(self): - ''' Return the Total Nitrogen of the unit ''' - return self._inf_comp[8] + self._inf_comp[9] \ - + self._inf_comp[10] + self._inf_comp[11] - - def get_particulate_N(self): - ''' Return organic nitrogen of the unit ''' - return self._inf_comp[11] - - def get_soluble_N(self): - ''' Return soluable nitrogen of the unit ''' - return self.get_TN() - self.get_particulate_N() - - def get_organic_N(self): - ''' Return organic nitrogen of the unit ''' - return self._inf_comp[10] + self._inf_comp[11] - - def get_inorganic_N(self): - ''' Return inorganic nitrogen of the unit ''' - return self._inf_comp[8] + self._inf_comp[9] - diff --git a/PooPyLab/ASMModel/pipe.pmt b/PooPyLab/ASMModel/pipe.pmt new file mode 100644 index 0000000..2440dc8 --- /dev/null +++ b/PooPyLab/ASMModel/pipe.pmt @@ -0,0 +1,22 @@ +#Mass Balance for a Pipe unit +# FWA = Flow Weighted Averages + +#Flow data source for the Main Outlet can be UPS, DNS, and PRG (e.g. pipe-->WAS). +[UPS] +FLOW: ZERO = MY_IN_FLOW - DISCHARGERS_SUM +FLOW: ZERO = MY_IN_FLOW - MY_MO_FLOW +CONC: ZERO = MY_IN_CONC - DISCHARGERS_FWA +CONC: ZERO = MY_IN_CONC - MY_MO_CONC + +[TBD] +FLOW: ZERO = MY_IN_FLOW - DISCHARGERS_SUM +FLOW: ZERO = MY_IN_FLOW - MY_MO_FLOW +CONC: ZERO = MY_IN_CONC - DISCHARGERS_FWA +CONC: ZERO = MY_IN_CONC - MY_MO_CONC + +#Flow data source for PRG is the same as that for UPS since a Pipe has no flow split to be determined during runtime +[PRG] +FLOW: ZERO = MY_IN_FLOW - DISCHARGERS_SUM +FLOW: ZERO = MY_IN_FLOW - MY_MO_FLOW +CONC: ZERO = MY_IN_CONC - DISCHARGERS_FWA +CONC: ZERO = MY_IN_CONC - MY_MO_CONC diff --git a/PooPyLab/ASMModel/pipe.py b/PooPyLab/ASMModel/pipe.py deleted file mode 100755 index 54072e5..0000000 --- a/PooPyLab/ASMModel/pipe.py +++ /dev/null @@ -1,281 +0,0 @@ -# This file is part of PooPyLab. -# -# PooPyLab is a simulation software for biological wastewater treatment -# processes using International Water Association Activated Sludge Models. -# -# Copyright (C) Kai Zhang -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -# -# This is the definition of the pipe() object. -# -# Update Log: -# July 28, 2017 KZ: made it more pythonic -# March 21, 2017 KZ: Migrated to Python3 -# June 24, 2015 KZ: Updated AddUpstreamUnit() to differential main/side -# June 16, 2015 KZ: Removed _PreFix, _Group status and -# Set(Get)PreFixStatus(), Set(Get)GroupStatus. -# Renamed _Done to _visited, SetAs(Is)Visited() to -# SetAs(Is)Visited() -# March 20, 2015 KZ: Added _PreFix, _Group, _Done status and -# Set(Get)PreFixStatus(), Set(Get)GroupStatus, -# SetAs(Is)Done(). -# Nov 24, 2014 KZ: revised RemoveUpstreamUnit() to be able to remove units -# with sidestream -# Nov 23, 2014 KZ: revised RemoveUpstreamUnit() to check availability to the -# upstream unit specified -# Nov 12, 2014 KZ: added: _upstream_connected and _MainOutletConnected flags; -# UpstreamConnected() and MainOutletConnected() functions -# Sep 26, 2014 KZ: removed test code -# June 29, 2014 KZ: Added GetXXX() definitions for solids and COD summary. -# March 15, 2014 KZ: AddUpstreamUnit(), RemoveUpstreamUnit(), and -# SetDownstreamMainUnit() begin here -# March 08, 2014 KZ: Rewrite according to the new class structure -# December 07, 2013 Kai Zhang - - -import base, constants - -class pipe(base.base): - __id = 0 - def __init__(self): - self.__class__.__id += 1 - self.__name__ = 'Pipe_' + str(self.__id) - - # _inlet store the upstream units and their flow contribution - # in the format of {unit, Flow} - self._inlet = {} - - # a SINGLE unit that receives all the flows from the - # current unit - self._main_outlet = None #By default this is the MAIN OUTLET. - - # a Boolean flag to indicate whether there are upstream units - self._upstream_connected = False - - # Boolean flag to indicate whether there are units in MAIN downstream - self._main_outlet_connected = False - - # the Total Inflow, which is the same as the outflow for a pipe obj. - self._total_flow = 0 - - # a Boolean flag to indicate whether _total_flow has been updated - self._flow_totalized = False - - # a Boolean flag to indicate whether _eff_comps[] have been blended - self._components_blended = False - - # a Boolean flag on whether the loop finding process has finished - # analyzing the unit - self._visited = False - - # THIS IS WHERE THE CURRENT STATE OF THE REACTOR IS STORED: - self._eff_comps = [0] * constants._NUM_ASM1_COMPONENTS - # _eff_comps[0]: X_I, - # _eff_comps[1]: X_S, - # _eff_comps[2]: X_BH, - # _eff_comps[3]: X_BA, - # _eff_comps[4]: X_D, - # _eff_comps[5]: S_I, - # _eff_comps[6]: S_S, - # _eff_comps[7]: -S_DO, COD = -DO - # _eff_comps[8]: S_NO, - # _eff_comps[9]: S_NH, - # _eff_comps[10]: S_NS, - # _eff_comps[11]: X_NS, - # _eff_comps[12]: S_ALK - print(self.__name__,' initialized successfully.') - # End of __init__() - - def add_upstream_unit(self, discharger, branch='Main'): - '''Add a single upstream unit to the current unit''' - if discharger not in self._inlet: - self._inlet[discharger] = 0.0 - # Line above: Setting the flow to 0.0 is a place-holder when - # setting up the Process Flow Diagram, because the actual total - # flow from upstream unit may not have been completely configured. - # The self.discharge() method of the upstream unit will totalize - # the flow and blend the components before passing them into the - # current unit. - self._flow_totalized= False - self._components_blended = False - self._upstream_connected = True - if branch == 'Main': - discharger.set_downstream_main_unit(self) - elif branch == 'Side': - discharger.set_downstream_side_unit(self) - - def remove_upstream_unit(self, discharger): - ''' Remove a single upstream unit from feeding into the current - unit - ''' - if discharger in self._inlet: - if discharger.has_sidestream() and \ - dischargerp.get_downstream_side_unit() == self: - self._inlet.pop(discharger) - discharger.set_downstream_side_unit(None) - else: - self._inlet.pop(discharger) - discharger.set_downstream_main_unit(None) - self._flow_totalized = False - self._components_blended = False - self._upstream_connected = False - - def set_downstream_main_unit(self, receiver): - ''' Set the mainstream unit that will receive effluent from the - current unit - ''' - if self._main_outlet != receiver: # if the receiver hasn't been added - self._main_outlet = receiver - self._main_outlet_connected = True - if receiver != None: - receiver.add_upstream_unit(self) - - def get_upstream_units(self): - return self._inlet - - def get_downstream_main_unit(self): - return self._main_outlet - - def totalize_flow(self): - ''' Totalize all the flows entering the current unit. - Return type: NO Return - ''' - self._total_flow = 0.0 - for unit in self._inlet: - self._total_flow += self._inlet[unit] - self._flow_totalized = True - - def blend_components(self): - ''' - blend_components() for Base mixes the contents in all inlet - components and send to the OUTLET, assuming no reaction - takes palce. - The definition is changed in ASMReactor where the mixture - is passed to the INLET of the reactor before reactions. - ''' - if self._flow_totalized == False: - self.totalize_flow() - if self._total_flow: - for index in range(constants._NUM_ASM1_COMPONENTS): - temp = 0.0 - for unit in self._inlet: - temp += unit.get_outlet_concs()[index] \ - * unit.get_outlet_flow() - self._eff_comps[index] = temp / self._total_flow - self._components_blended = True - - def update_combined_input(self): - ''' Combined the flows and loads into the current unit''' - if self._flow_totalized == False: - self.totalize_flow() - if self._components_blended == False: - self.blend_components() - - def get_outlet_flow(self): - ''' Return the total out flow of the current unit (mainstream) - Return value type: float/double - ''' - if self._flow_totalized == False: - self.totalize_flow() - return self._total_flow - - def get_outlet_concs(self): - ''' Return the effluent concentrations of the current unit (mainstream) - Return type: list - ''' - if self._components_blended == False: - self.blend_components() - return self._eff_comps - - def discharge(self): - ''' Pass the total flow and blended components to the next unit. - ''' - self.update_combined_input() - if self._main_outlet != None: - #TODO: need to make sure the downstream unit get the current - # unit's info - self.get_downstream_main_unit().update_combined_input() - - def upstream_connected(self): - ''' Get the status of upstream connection''' - return self._upstream_connected - - def main_outlet_connected(self): - ''' Get the status of downstream main connection''' - return self._main_outlet_connected - - def set_as_visited(self, status=False): - self._visited = status - - def is_visited(self): - return self._visited - - def _sum_helper(self, index_list=[]): - ''' sum up the model components indicated by the index_list''' - sum = 0.0 - for element in index_list: - sum += self._eff_comps[element] - return sum - - def get_TSS(self): - #TODO: need to make COD/TSS = 1.2 changeable for different type of - # sludge - index_list = [0, 1, 2, 3, 4] - return self._sum_helper(index_list) / 1.2 - - - def get_VSS(self): - #TODO: need to make COD/VSS = 1.42 changeable for diff. type of sludge - index_list = [0, 1, 2, 3, 4] - return self._sum_helper(index_list) / 1.42 - - def get_total_COD(self): - index_list = [0, 1, 2, 3, 4, 5, 6] - return self._sum_helper(index_list) - - def get_soluble_COD(self): - index_list = [5, 6] - return self._sum_helper(index_list) - - def get_particulate_COD(self): - return self.GetTotalCOD - self.getSoluableCOD() - - def get_TN(self): - index_list = [8, 9, 10, 11] - return self._sum_helper(index_list) - - def get_organic_N(self): - index_list = [10, 11] - return self._sum_helper(index_list) - - def get_inorganic_N(self): - index_list = [8, 9] - return self._sum_helper(index_list) - - def get_particulate_N(self): - return self._eff_comps[11] - - def get_soluble_N(self): - return self.GetTN() - self.getParticulateN() - - - def has_sidestream(self): - ''' Check if the current unit has a sidestream discharge. - Default = False, i.e. no sidestream - Return type: boolean - ''' - return False - diff --git a/PooPyLab/ASMModel/reactor.py b/PooPyLab/ASMModel/reactor.py deleted file mode 100755 index 5f9929e..0000000 --- a/PooPyLab/ASMModel/reactor.py +++ /dev/null @@ -1,129 +0,0 @@ -# This file is part of PooPyLab. -# -# PooPyLab is a simulation software for biological wastewater treatment -# processes using International Water Association Activated Sludge Models. -# -# Copyright (C) 2014 Kai Zhang -# -# PooPyLab is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# PooPyLab is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with PooPyLab. If not, see . -# -# This is the definition of asm_reactor class. -# -# Reactor dimensions, inlet_list, outlet_list, dissolved oxygen, -# and mass balance will be defined for the public interface -# -# Update Log: -# July 30, 2017 KZ: more pythonic style -# March 21, 2017 KZ: Migrated to Python3 -# May 26, 2014 KZ: Updated Definition -# December 17, 2013 KZ: Added/revised blend_components() definition. -# December 07, 2013 Kai Zhang - - -import asm, pipe -import constants - -class asm_reactor(pipe.pipe): - __id = 0 - def __init__(self, ActiveVol=380, swd=3.5, \ - Temperature=20, DO=2, *args, **kw): - pipe.pipe.__init__(self) - self.__class__.__id += 1 - self.__name__ = "Reactor_" + str(self.__id) - # swd = side water depth in meters, default = ~12 ft - # ActiveVol in m^3, default value equals to 100,000 gallons - # Temperature = 20 C by default - # DO = dissolved oxygen, default = 2.0 mg/L - - self._active_vol = ActiveVol - self._swd = swd - self._area = self._active_vol / self._swd - - # _reactor_inf_comps[0]: Inf_X_I, - # _reactor_inf_comps[1]: Inf_X_S, - # _reactor_inf_comps[2]: Inf_X_BH, - # _reactor_inf_comps[3]: Inf_X_BA, - # _reactor_inf_comps[4]: Inf_X_D, - # _reactor_inf_comps[5]: Inf_S_I, - # _reactor_inf_comps[6]: Inf_S_S, - # _reactor_inf_comps[7]: -Inf_S_DO, COD = -DO - # _reactor_inf_comps[8]: Inf_S_NO, - # _reactor_inf_comps[9]: Inf_S_NH, - # _reactor_inf_comps[10]: Inf_S_NS, - # _reactor_inf_comps[11]: Inf_X_NS, - # _reactor_inf_comps[12]: Inf_S_ALK - # - self._reactor_inf_comps = [0.0] * constants._NUM_ASM1_COMPONENTS - - # the core material the ASMReactor stores - self._sludge = asm.ASM1(Temperature, DO) - - # the max acceptable error for determining whether the simulation has converged. - self._error_tolerance = 10E-4 # temporary number just to hold the place - # a boolean flag to show convergence status - self._converged = False - - # model components from the previous round - self._pre_eff_comps = [0.0] * constants._NUM_ASM1_COMPONENTS - - print(self.__name__, " Initialized Successfully.") - return None - - def blend_components(self): - ''' - blend_components() for ASMReactor is different from that for Base. - Here it blends the contents from upstream and passes the mixture - to the reactor's INLET, while to the OUTLET in Base. - This is because the Base definition is used in non-reacting units - like Pipe, Splitter, etc. - ''' - for index in range(constants._NUM_ASM1_COMPONENTS): - temp = 0.0 - for unit in self._inlet: - temp += unit.get_eff_comps()[index] * unit.read_flow() - self._reactor_inf_comps[index] = temp / self._total_flow - - def get_active_vol(self): - return self._active_vol - - def get_eff_comps(self): - return self._eff_comps - - def get_inf_comps(self): - return self._reactor_inf_comp - - def get_ASM_params(self): - return self._sludge.get_params() - - def get_ASM_stoichs(self): - return self._sludge.get_stoichs() - - def update_condition(self, Temperature, DO): - self._sludge.update(Temperature, DO) - - def initial_guess(self): - #TODO: NEED TO PUT IN FIRST GUESS OF MODEL COMPONENTS HERE - - # store the initial guess as the current state of the reactor - self._eff_comps = self._pre_eff_comps[:] - - def EstimateCurrentState(self): - # store the current componets received in the most recent iteration. - self._pre_eff_comps = self._eff_comps[:] - # get the components from the next iteration. - self._eff_comps = self._sludge.steady_step(self._pre_eff_comps, \ - self._total_flow, \ - self._reactor_inf_comp, \ - self._active_vol) - diff --git a/PooPyLab/ASMModel/splitter.pmt b/PooPyLab/ASMModel/splitter.pmt new file mode 100644 index 0000000..9489c0b --- /dev/null +++ b/PooPyLab/ASMModel/splitter.pmt @@ -0,0 +1,57 @@ +#Flow Balance: +# 0 = IN - MO - SO +# 0 = IN - Sum(all dischargers' contribution) +# +# All three (3) sets of flows are to be solved for the splitter itself. +# The individual branch (outlet) will get its flow data according to its +# data source flag. +# +# For a splitter unit, its side outlet shall be defined if the splitter +# is NOT a SRT Controller. But if the splitter controls the SRT by +# connecting to a "pipe-WAS" structure, the side outlet flow is still +# considered defined by the program (PRG) runtime. +# +# Therefore, the FLOW_DATA_SRC for a splitter's side outlet can only +# be PRG (user defined or program runtime) onced it is defined. +# +# If FLOW_DATA_SRC for the main outlet is PRG or DNS, it shall be +# assigned such ways, either of which would lead to the determination of +# the inlet flow. +# +#Mass Balance: +# Concentrations would be assigned from inlet (flow weighted averages) +# to the main and side outlets. +# +#How to set up steady state vs dynamic conditions? + +[TBD] +FLOW: ZERO = MY_IN_FLOW - DISCHARGERS_SUM +FLOW: ZERO = MY_IN_FLOW - MY_MO_FLOW - MY_SO_FLOW +FLOW: ZERO = MY_SO_FLOW - USER_DEFINED_SO_FLOW +CONC: ZERO = MY_IN_CONC - DISCHARGERS_FWA +CONC: ZERO = MY_IN_CONC - MY_MO_CONC +CONC: ZERO = MY_IN_CONC - MY_SO_CONC + +[UPS] +FLOW: ZERO = MY_IN_FLOW - DISCHARGERS_SUM +FLOW: ZERO = MY_IN_FLOW - MY_MO_FLOW - MY_SO_FLOW +FLOW: ZERO = MY_SO_FLOW - USER_DEFINED_SO_FLOW +CONC: ZERO = MY_IN_CONC - DISCHARGERS_FWA +CONC: ZERO = MY_IN_CONC - MY_MO_CONC +CONC: ZERO = MY_IN_CONC - MY_SO_CONC + +[DNS] +FLOW: ZERO = MY_IN_FLOW - MY_MO_FLOW - MY_SO_FLOW +FLOW: ZERO = MY_MO_FLOW - MY_MO_RECEIVER_IN_FLOW +FLOW: ZERO = MY_SO_FLOW - USER_DEFINED_SO_FLOW +CONC: ZERO = MY_IN_CONC - DISCHARGERS_FWA +CONC: ZERO = MY_IN_CONC - MY_MO_CONC +CONC: ZERO = MY_IN_CONC - MY_SO_CONC + +[PRG] +FLOW: ZERO = MY_IN_FLOW - DISCHARGERS_SUM +FLOW: ZERO = MY_IN_FLOW - MY_MO_FLOW - MY_SO_FLOW +FLOW: ZERO = MY_SO_FLOW - MY_SO_RECEIVER_IN_FLOW +CONC: ZERO = MY_IN_CONC - DISCHARGERS_FWA +CONC: ZERO = MY_IN_CONC - MY_MO_CONC +CONC: ZERO = MY_IN_CONC - MY_SO_CONC diff --git a/PooPyLab/ASMModel/splitter.py b/PooPyLab/ASMModel/splitter.py deleted file mode 100755 index 649bdbf..0000000 --- a/PooPyLab/ASMModel/splitter.py +++ /dev/null @@ -1,152 +0,0 @@ -# This file is part of PooPyLab. -# -# PooPyLab is a simulation software for biological wastewater treatment -# processes using International Water Association Activated Sludge Models. -# -# Copyright (C) 2014 Kai Zhang -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -# -# -# This file provides the definition of Splitter. -# -# Update Log: -# Jul 30, 2017 KZ: pythonic style -# Mar 21, 2017 KZ: Migrated to Python3 -# Jun 24, 2015 KZ: Updated SetDownstreamSideUnit() to differential main/side -# Jun 23, 2015 KZ: Rewrote sidestream to eliminate Branch class -# Jun 18, 2015 KZ: Removed _PreFix and _Group status and -# Set(Get)PreFixStatus(), Set(Get)GroupStatus; -# Renamed _Done to _Visited and SetAs(Is)Done() to -# SetAs(Is)Visited() -# Mar 20, 2015 KZ: Added _PreFix, _Group, _Done status and -# Set(Get)PreFixStatus(), Set(Get)GroupStatus, -# SetAs(Is)Done(). -# Nov 18, 2014 KZ: renamed "SideStream" into "Sidestream"; -# Added _SidestreamConnected and SideOutletConnected() -# Sep 26, 2014 KZ: fixed pipe.Pipe.__init__ -# Apr 27, 2014 KZ: Change the sidestream class from Influent() to Sidestream() -# Apr 18, 2014 KZ: Rewrite definition based on the new class system structure -# Dec 25, 2013 KZ: commented out the BlendComponent() function in ReceiveFrom() -# Dec 07, 2013 -# - -import pipe - - -class splitter(pipe.pipe): - __id = 0 - - def __init__(self): - pipe.pipe.__init__(self) - self.__class__.__id += 1 - self.__name__ = "Splitter_" + str(self.__id) - - # the main outlet is defined in pipe.pipe as _main_outlet - # therefore add the _side_outlet only here. - self._side_outlet= None - - self._main_outlet_flow = 0.0 - self._side_outlet_flow = 0.0 - - self._side_outlet_connected = False - - # boolean on whether the loop finding process has finished - # analyzing the unit - self._visited = False - - self._SRT_controller = False - print(self.__name__, "initialized successfully.") - - def is_SRT_controller(self): - ''' Mark the splitter whether it controls the plant's Solids Retention - Time. - Default value: False - ''' - return self._SRT_controller - - def set_as_SRT_controller(self, setting=False): - ''' Take user-input to set whether the current Splitter control - plant's SRT - ''' - self._SRT_controller = setting - #TODO: HOW DOES THIS IMPACT WAS FLOW BASED ON USER SPECIFIED SRT? - - def set_sidestream_flow(self, flow): - self._side_outlet_flow = flow - #TODO: Need to be able to dynamically update the sidestream flow - - def totalize_flow(self): - ''' totalize the flow for the Splitter unit ''' - self._total_flow = self._main_outlet_flow = 0.0 - for unit in self._inlet: - self._total_flow += self._inlet[unit] - #TODO: Need to pay attention to the flow balance below during runtime - self._main_outlet_flow = self._total_flow - self._side_outlet_flow - self._flow_totalized = True - - - def set_downstream_side_unit(self, rcvr): - ''' Set the sidestream unit that will receive effluent from the - current unit - ''' - if self._side_outlet != rcvr: - self._side_outlet = rcvr - self._side_outlet_connected = True - if rcvr != None: - rcvr.add_upstream_unit(self, "Side") #TODO: OKAY?? - - def get_downstream_side_unit(self): - return self._side_outlet - - def discharge(self): - ''' Pass the total flow and blended components to the next unit. - Both mainstream and sidestream units shall receive their flows - and component concentratons. - ''' - self.update_combined_input() - if self._main_outlet != None and self.set_sidestream_flow != None: - self.get_downstream_main_unit().update_combined_input() - self.get_downstream_main_unit().update_combined_input() - else: - print("ERROR: ", self.__name__, "downstream unit setup incomplete") - - def has_sidestream(self): - return True - - def side_outlet_connected(self): - return self._side_outlet_connected - - def set_as_visited(self, status=False): - self._visited = status - - def is_visited(self): - return self._visited - - #def GetWAS(self, WWTP, TargetSRT): - # '''Get the mass of DRY solids to be wasted (WAS) in KiloGram''' - # #WWTP is a list that stores all the process units - # TotalSolids = 0.0 #as TSS in KiloGram - # if self._SRTController: - # for unit in WWTP: - # if isinstance(unit, ASMReactor): - # TotalSolids += unit.GetTSS() \ - # * unit.GetActiveVolume() / 1000.0 - # if TargetSRT > 0: - # return TotalSolids / TargetSRT - # else: - # print("Error in Target SRT <= 0 day; GetWAS() returns 0.") - # return 0; - diff --git a/PooPyLab/ASMModel/template/WAS.pmt b/PooPyLab/ASMModel/template/WAS.pmt new file mode 100644 index 0000000..cccc42d --- /dev/null +++ b/PooPyLab/ASMModel/template/WAS.pmt @@ -0,0 +1,14 @@ +#An WAS unit only deals with its own inlet since its main outlet +#feeds to nothing within the process boundaries. +# +#Its inlet flow data source can only be set to UPS or PRG. +# +#If the inlet flow data source is PRG, it would be either defined by +#the user or calculations. If calculated, the current WAS unit +#shall be the only WAS type for such estimate. All other WAS units, +#if present, shall be defined in terms of flows. +# +#The flow balance would be a global one across the entire plant, +#including the total of all influents, all effluents, and all other +#WAS units whose flows are defined, along with their solids +#concentrations. diff --git a/PooPyLab/ASMModel/template/asmreactor.pmt b/PooPyLab/ASMModel/template/asmreactor.pmt new file mode 100644 index 0000000..7b8d4e8 --- /dev/null +++ b/PooPyLab/ASMModel/template/asmreactor.pmt @@ -0,0 +1,12 @@ +Flow Balance: + 0 = IN - MO + 0 = IN - Sum(all dischargers' contribution) + + +Mass Balance: +Flow data source for the main outlet can be UPS, DNS, and PRG (e.g. pipe-->WAS). +Assign the main outlet flow as needed. +Sum up the dischargers contributions. + +Concentrations would be assigned from inlet (flow weighted averages) +to the main and side outlets. diff --git a/PooPyLab/ASMModel/template/effluent.pmt b/PooPyLab/ASMModel/template/effluent.pmt new file mode 100644 index 0000000..8991199 --- /dev/null +++ b/PooPyLab/ASMModel/template/effluent.pmt @@ -0,0 +1,19 @@ +#An Effluent unit only deals with its own inlet since its main outlet +#feeds to nothing within the process boundaries. +# +#Its inlet flow data source can only be set to UPS or PRG. +# +#The inlet flwo data source is UPS when there is no WAS units in the PFD. For instance, a CSTR. +# +#If the inlet flow data source is PRG, it would be either defined by +#the user or calculations. If calculated, the current effluent unit +#shall be the only effluent type (except for WAS) for such estimate. +# +#The flow balance would be a global one across the entire plant, +#including the total of all influents, WAS unit, the all other +#effluent units whose flows are defined. + +[PRG] +FLOW: ZERO = PLANT_IN_FLOW - PLANT_WAS_FLOW - MY_MO_FLOW +FLOW: ZERO = MY_IN_FLOW - MY_MO_FLOW +CONC: ZERO = MY_IN_CONC - MY_MO_CONC diff --git a/PooPyLab/ASMModel/template/influent.pmt b/PooPyLab/ASMModel/template/influent.pmt new file mode 100644 index 0000000..d8df7f0 --- /dev/null +++ b/PooPyLab/ASMModel/template/influent.pmt @@ -0,0 +1,65 @@ +#Influent is essentially a Pipe. +# +#It provides the definitions of flows and concentrations. +# +#The inlet is NOT to be defined, only the main outlet will be used to supply +#information to the receiving unit. +# +#straightly define the main outlet flows and concentrations as known. +#build in fractionations for cod, n, p, s, etc. +# +#For a steady state simulation, define the influent condition in this file. +#For a dynamic simulation, the influent condition will be save in a separate +#.csv file, whose filename is stored in this model file. + +[UPS] +FLOW: ZERO = USER_DEFINED_IN_FLOW - MY_MO_FLOW +CONC: ZERO = USER_DEFINED_IN_CONC - MY_MO_CONC + +[TBD] +FLOW: ZERO = USER_DEFINED_IN_FLOW - MY_MO_FLOW +CONC: ZERO = USER_DEFINED_IN_CONC - MY_MO_CONC + +[DNS] +FLOW: ZERO = USER_DEFINED_IN_FLOW - MY_MO_FLOW +CONC: ZERO = USER_DEFINED_IN_CONC - MY_MO_CONC + +[PRG] +FLOW: ZERO = USER_DEFINED_IN_FLOW - MY_MO_FLOW +CONC: ZERO = USER_DEFINED_IN_CONC - MY_MO_CONC + +[CONC] +COD: 800 +TSS: 150 +VSS: 115 +TKN: 40 +NH3N: 28 +TP: 8 +OP: 6 +Alk: 6 +pH: 7.6 +Total S: 10 +Reduced S: 3 + +[FRAC] +{ASM1} +COD:BOD5: 2.04 +SCOD:COD: 0.50 +RBCOD:SCOD: 0.80 +SBCOD:PCOD: 0.70 +SON:SCOD: 0.01 +RBON:SON: 0.8 +SBON:PON: 0.75 +{ASM2} +RBSCOD:COD: 0.10 +CBSCOD:COD: 0.30 +NBSCOD:COD: 0.10 +NBPCOD:PCOD: 0.20 +NH3N:TKN: 0.75 +SORGN:ORGN: 0.60 +SBORGN:SBCOD: 0.1 +PBORGN:PBCOD: 0.1 +PO4P:TP: 0.65 +SBORGP:SBCOD: 0.02 +PBORGP:PBCOD: 0.02 + diff --git a/PooPyLab/ASMModel/template/pipe.pmt b/PooPyLab/ASMModel/template/pipe.pmt new file mode 100644 index 0000000..2440dc8 --- /dev/null +++ b/PooPyLab/ASMModel/template/pipe.pmt @@ -0,0 +1,22 @@ +#Mass Balance for a Pipe unit +# FWA = Flow Weighted Averages + +#Flow data source for the Main Outlet can be UPS, DNS, and PRG (e.g. pipe-->WAS). +[UPS] +FLOW: ZERO = MY_IN_FLOW - DISCHARGERS_SUM +FLOW: ZERO = MY_IN_FLOW - MY_MO_FLOW +CONC: ZERO = MY_IN_CONC - DISCHARGERS_FWA +CONC: ZERO = MY_IN_CONC - MY_MO_CONC + +[TBD] +FLOW: ZERO = MY_IN_FLOW - DISCHARGERS_SUM +FLOW: ZERO = MY_IN_FLOW - MY_MO_FLOW +CONC: ZERO = MY_IN_CONC - DISCHARGERS_FWA +CONC: ZERO = MY_IN_CONC - MY_MO_CONC + +#Flow data source for PRG is the same as that for UPS since a Pipe has no flow split to be determined during runtime +[PRG] +FLOW: ZERO = MY_IN_FLOW - DISCHARGERS_SUM +FLOW: ZERO = MY_IN_FLOW - MY_MO_FLOW +CONC: ZERO = MY_IN_CONC - DISCHARGERS_FWA +CONC: ZERO = MY_IN_CONC - MY_MO_CONC diff --git a/PooPyLab/ASMModel/template/splitter.pmt b/PooPyLab/ASMModel/template/splitter.pmt new file mode 100644 index 0000000..9489c0b --- /dev/null +++ b/PooPyLab/ASMModel/template/splitter.pmt @@ -0,0 +1,57 @@ +#Flow Balance: +# 0 = IN - MO - SO +# 0 = IN - Sum(all dischargers' contribution) +# +# All three (3) sets of flows are to be solved for the splitter itself. +# The individual branch (outlet) will get its flow data according to its +# data source flag. +# +# For a splitter unit, its side outlet shall be defined if the splitter +# is NOT a SRT Controller. But if the splitter controls the SRT by +# connecting to a "pipe-WAS" structure, the side outlet flow is still +# considered defined by the program (PRG) runtime. +# +# Therefore, the FLOW_DATA_SRC for a splitter's side outlet can only +# be PRG (user defined or program runtime) onced it is defined. +# +# If FLOW_DATA_SRC for the main outlet is PRG or DNS, it shall be +# assigned such ways, either of which would lead to the determination of +# the inlet flow. +# +#Mass Balance: +# Concentrations would be assigned from inlet (flow weighted averages) +# to the main and side outlets. +# +#How to set up steady state vs dynamic conditions? + +[TBD] +FLOW: ZERO = MY_IN_FLOW - DISCHARGERS_SUM +FLOW: ZERO = MY_IN_FLOW - MY_MO_FLOW - MY_SO_FLOW +FLOW: ZERO = MY_SO_FLOW - USER_DEFINED_SO_FLOW +CONC: ZERO = MY_IN_CONC - DISCHARGERS_FWA +CONC: ZERO = MY_IN_CONC - MY_MO_CONC +CONC: ZERO = MY_IN_CONC - MY_SO_CONC + +[UPS] +FLOW: ZERO = MY_IN_FLOW - DISCHARGERS_SUM +FLOW: ZERO = MY_IN_FLOW - MY_MO_FLOW - MY_SO_FLOW +FLOW: ZERO = MY_SO_FLOW - USER_DEFINED_SO_FLOW +CONC: ZERO = MY_IN_CONC - DISCHARGERS_FWA +CONC: ZERO = MY_IN_CONC - MY_MO_CONC +CONC: ZERO = MY_IN_CONC - MY_SO_CONC + +[DNS] +FLOW: ZERO = MY_IN_FLOW - MY_MO_FLOW - MY_SO_FLOW +FLOW: ZERO = MY_MO_FLOW - MY_MO_RECEIVER_IN_FLOW +FLOW: ZERO = MY_SO_FLOW - USER_DEFINED_SO_FLOW +CONC: ZERO = MY_IN_CONC - DISCHARGERS_FWA +CONC: ZERO = MY_IN_CONC - MY_MO_CONC +CONC: ZERO = MY_IN_CONC - MY_SO_CONC + +[PRG] +FLOW: ZERO = MY_IN_FLOW - DISCHARGERS_SUM +FLOW: ZERO = MY_IN_FLOW - MY_MO_FLOW - MY_SO_FLOW +FLOW: ZERO = MY_SO_FLOW - MY_SO_RECEIVER_IN_FLOW +CONC: ZERO = MY_IN_CONC - DISCHARGERS_FWA +CONC: ZERO = MY_IN_CONC - MY_MO_CONC +CONC: ZERO = MY_IN_CONC - MY_SO_CONC diff --git a/PooPyLab/ASMModel/template/was.pmt b/PooPyLab/ASMModel/template/was.pmt new file mode 100644 index 0000000..cccc42d --- /dev/null +++ b/PooPyLab/ASMModel/template/was.pmt @@ -0,0 +1,14 @@ +#An WAS unit only deals with its own inlet since its main outlet +#feeds to nothing within the process boundaries. +# +#Its inlet flow data source can only be set to UPS or PRG. +# +#If the inlet flow data source is PRG, it would be either defined by +#the user or calculations. If calculated, the current WAS unit +#shall be the only WAS type for such estimate. All other WAS units, +#if present, shall be defined in terms of flows. +# +#The flow balance would be a global one across the entire plant, +#including the total of all influents, all effluents, and all other +#WAS units whose flows are defined, along with their solids +#concentrations. diff --git a/PooPyLab/ASMModel/was.pmt b/PooPyLab/ASMModel/was.pmt new file mode 100644 index 0000000..cccc42d --- /dev/null +++ b/PooPyLab/ASMModel/was.pmt @@ -0,0 +1,14 @@ +#An WAS unit only deals with its own inlet since its main outlet +#feeds to nothing within the process boundaries. +# +#Its inlet flow data source can only be set to UPS or PRG. +# +#If the inlet flow data source is PRG, it would be either defined by +#the user or calculations. If calculated, the current WAS unit +#shall be the only WAS type for such estimate. All other WAS units, +#if present, shall be defined in terms of flows. +# +#The flow balance would be a global one across the entire plant, +#including the total of all influents, all effluents, and all other +#WAS units whose flows are defined, along with their solids +#concentrations. diff --git a/PooPyLab/ASMModel/was.py b/PooPyLab/ASMModel/was.py deleted file mode 100755 index 9b43f60..0000000 --- a/PooPyLab/ASMModel/was.py +++ /dev/null @@ -1,89 +0,0 @@ -# This file is part of PooPyLab. -# -# PooPyLab is a simulation software for biological wastewater treatment -# processes using International Water Association Activated Sludge Models. - -# Copyright (C) 2014 Kai Zhang -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -# This is the definition of the WAS class. WAS = Waste Activated Sludge -# The control of Solids Retention Time (SRT) is through the WAS object. -# -# Author: Kai Zhang -# -# Update Log: -# Jul 30, 2017 KZ: made code more pythonic -# Mar 21, 2017 KZ: Migrated to Python3 -# Sep 26, 2014 KZ: Change inheritance back from Splitter to Effluent -# Aug 25, 2014 KZ: Added InformSRTController function. -# Aug 23, 2014 KZ: Changed its inheritance to Effluent; -# Added variables and functions for WAS Flow calculation -# December 06, 2013 Kai Zhang: Initial design - - -import effluent - -class WAS(effluent.effluent): - __id = 0 - def __init__(self): - - effluent.effluent.__init__(self) - self.__class__.__id += 1 - self.__name__ = 'WAS_' + str(self.__id) - self._WAS_flow = 0.0 # Unit: m3/d - print(self.__name__,' initialized successfully.') - - def get_solids_inventory(self, reactor_list=[]): - ''' Calculate the total solids mass in active reactors ''' - # reactor_list stores the asm_reactor units that contribute - # active biomass growth to the WWTP. - # The result of this function will be used to determine the WAS - # flow for the entire WWTP. - #TODO: In the MAIN program, we will need to maintain a list of - # reactors that contribute active solids to the WWTP. - inventory = 0.0 - for unit in reactor_list: - #TODO: IMPORTANT TO CHECK ALL THE UNITS IN THE reactor_list - # HAD UPDATED TSS!!! THIS SHOULD BE THE CASE BECAUSE THE - # WAS UNIT IS THE LAST ELEMENT IN THE ITERATION LOOP. BUT - # WILL NEED DOUBLE CHECK. - inventory += unit.get_TSS() * unit.get_active_vol() - inventory = inventory / 1000.0 # Convert unit to Kg - return inventory - - def get_WAS_flow(self, reactor_list=[], SRT=1): - #SRT is the plant's SRT from user input - self.update_combined_input() - self._WAS_flow = self.get_solids_inventory(reactor_list) * 1000 \ - / SRT / self.get_TSS() - #TODO: in MAIN function, we need to check whether the WAS flow - # is higher than the influent flow - return self._WAS_flow - - def inform_SRT_controller(self): - ''' Pass the WAS Flow to the upstream SRT Controlling Splitter ''' - upstream_unit = self.get_upstream_units().keys() - if len(upstream_unit) == 1 \ - and isinstance(upstream_unit[0], splitter.splitter): - if upstream_unit[0].is_SRT_controller(): - upstream_unit[0].setup_sidestream(self, self.get_WAS_flow()) - upstream_unit[0].totalize_flow() - upstream_unit[0].discharge() #TODO: IS THIS NEEDED?? - else: - print("The unit upstream of WAS is not SRT controlling") - else: - print("Check the unit upstream of WAS. \ - There shall be a splitter only") - diff --git a/PooPyLab/GUI/MainForm.ui b/PooPyLab/GUI/MainForm.ui deleted file mode 100644 index 44ea77d..0000000 --- a/PooPyLab/GUI/MainForm.ui +++ /dev/null @@ -1,86 +0,0 @@ - - - MainWindow - - - - 0 - 0 - 800 - 600 - - - - MainWindow - - - - - - 0 - 0 - 800 - 26 - - - - - File - - - - - - - - - - Simulation - - - - - - - Report - - - - - - - - - - Open... - - - - - Save - - - - - Save As... - - - - - Exit - - - - - Check Setup - - - - - Run Steady State - - - - - - diff --git a/PooPyLab/__init__.py b/PooPyLab/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/PooPyLab/model_builder/__init__.py b/PooPyLab/model_builder/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/PooPyLab/model_builder/sundials/__init__.py b/PooPyLab/model_builder/sundials/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/PooPyLab/model_builder/sundials/compiler_script.py b/PooPyLab/model_builder/sundials/compiler_script.py new file mode 100755 index 0000000..5375447 --- /dev/null +++ b/PooPyLab/model_builder/sundials/compiler_script.py @@ -0,0 +1,11 @@ +#!/usr/bin/python3 + +# This is to test the use of a py script to compile the SUNDIALS program. + +import subprocess + +if __name__ == '__main__': + subprocess.run(["make", "clean"]) + subprocess.run(["make"]) + subprocess.run(["./sundials_ida_trial.out"]) + diff --git a/PooPyLab/model_builder/sundials/generic_settings.h b/PooPyLab/model_builder/sundials/generic_settings.h new file mode 100644 index 0000000..2374d6d --- /dev/null +++ b/PooPyLab/model_builder/sundials/generic_settings.h @@ -0,0 +1,17 @@ +#ifndef __GENERIC_SETTINGS__ +#define __GENERIC_SETTINGS__ + +#include + +#if defined(SUNDIALS_EXTENDED_PRECISION) +#define GSYM "Lg" +#define ESYM "Le" +#define FSYM "Lf" +#else +#define GSYM "g" +#define ESYM "e" +#define FSYM "f" +#endif + + +#endif diff --git a/PooPyLab/model_builder/sundials/model_builder_common.py b/PooPyLab/model_builder/sundials/model_builder_common.py new file mode 100644 index 0000000..b4292de --- /dev/null +++ b/PooPyLab/model_builder/sundials/model_builder_common.py @@ -0,0 +1,232 @@ +# This file is part of PooPyLab. +# +# PooPyLab is a simulation software for biological wastewater treatment processes using International Water Association +# Activated Sludge Models. +# +# Copyright (C) Kai Zhang +# +# PooPyLab is free software: you can redistribute it and/or modify it under the terms of the GNU General Public +# License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with PooPyLab. If not, see +# . +# +# +# This is the definition of the ASM1 model to be imported as part of the Reactor object +# + + +def _create_array_name(proc_unit={}, branch='Inlet'): + """ Create the array name for a particular branch of a process unit + + Args: + proc_unit: a process unit's config {} + branch: type of the branch whose array is to be created, 'Inlet'|'Main'|'Side' + + Return: + str of the array name for the given branch + """ + # + # proc_unit['NUM_MODEL_COMPONENTS'] INCLUDES flow rate + # array[0] = flow rate + # + array_name = [ proc_unit[branch + '_Arrayname'] + '[' + proc_unit['Num_Model_Components'] + ']' ] + + return ''.join(array_name) + + +def define_branch_arrays(unit={}): + """ Define the array names for each available branch of a unit + + Args: + unit: a PooPyLab process unit, {} + + Return: + str of arraynames for the branches + """ + + array_defs = [] + + # A process unit may not have an inlet, e.g. an Influent + if unit['Inlet_Codenames'] != 'None': + array_defs.append(_create_array_name(unit, 'Inlet')) + + # A process unit may not have a main outlet, e.g. an Effluent or a WAS + if unit['Main_Outlet_Codename'] != 'None': + array_defs.append(_create_array_name(unit, 'Main_Outlet')) + + # A process unit may not have a side outlet, e.g. a Pipe or a CSTR + if unit['Side_Outlet_Codename'] != 'None': + array_defs.append(_create_array_name(unit, 'Side_Outlet')) + + if array_defs: + return ', '.join(array_defs) + + return '// unit ' + unit['Codename'] + ' with incomplete connection here' + + +def assign_solver_array(arraynames=[], num_model_components=14): + """ Construct C code for assigning the variable array into the obj function + + Args: + arraynames: list of array names + num_model_components: number of model components, int. + Return: + assignment C code, str. + """ + + assignment = [] + counter = 0 + for group in arraynames: + if group[:2] != '//': + arrs = group.split(',') + for branch_arr in arrs: + assignment.append( + 'for(i=0; i<' + str(num_model_components) + "; i++)\n" + + ' ' + branch_arr.split('[')[0] + '[i] = Ith(y, ' + str(counter) + "+i+1);\n") + counter += num_model_components + return assignment + + +def collect_inlet_arrays(pfd, unit): + """ Generate the array names that are discharging to the current unit + + Args: + pfd: dict storing the process flowsheet + unit: the process unit whose inlet streams are to be identified + + Return: + the identified inlet streams as an str + """ + inlet_streams = [] + myinlet = unit['Inlet_Codenames'].split(' ') + + #TODO: add ERROR Handling here + if myinlet == ['None']: + return inlet_streams + + for codename in myinlet: + discharger = pfd['Flowsheet'][codename] + if discharger['Main_Outlet_Codename'] == unit['Codename']: + inlet_streams.append(discharger['Main_Outlet_Arrayname']) + else: + inlet_streams.append(discharger['Side_Outlet_Arrayname']) + + return inlet_streams + + +def substr_for_flow(unit, flowstr, inlet_streams): + """ Generate the substitution str for the flow in a model template + + Args: + unit: the PooPyLab unit being worked on, {} + flowstr: the flow term used in the model template, '': + inlet_streams: the identified inlet streams for "unit", [] + + Return: + str of C array element to replace the flow term in the model template + """ + branch = '' + if flowstr == 'MY_IN_FLOW': + branch = 'Inlet' + elif flowstr == 'MY_MO_FLOW': + branch = 'Main_Outlet' + elif flowstr == 'MY_SO_FLOW': + branch = 'Side_Outlet' + + if branch != '': + return unit[branch + '_Arrayname'] + '[0]' + + if flowstr == 'DISCHARGERS_SUM': + totalizer_str = [] + nis = len(inlet_streams) + for discharger in inlet_streams: + totalizer_str.append(discharger + '[0]') + return '('*(nis>1) + ' + '.join(totalizer_str) + ')'*(nis>1) + 'ERROR!'*(nis==0) + '\n' + + if flowstr == 'USER_DEFINED_SO_FLOW': + user_defined_so_flow = unit['User_Defined_SO_Flow'] + try: + if float(user_defined_so_flow) >= 0: + return user_defined_so_flow + else: + return 'ERROR: Sidestream flow < 0!\n' + except ValueError: + #TODO: add try..except for .csv file handling + return 'ERROR!\n' + + return 'ERROR in ' + unit['Codename'] + "\n" + + +def substr_for_concs(unit, conc_terms, eq_id): + """ Generate the substitution str for the concentration terms in a model template + + Args: + unit: the PooPyLab unit being worked on, {} + conc_terms: the concentration terms used in the model template, ['']: + + Return: + str of C statements to replace the concentration term in the model template, + updated equation id + """ + branches = [] + + for x in conc_terms: + x = x.strip() + if x == 'MY_IN_CONC': + branches.append('Inlet') + elif x == 'MY_MO_CONC': + branches.append('Main_Outlet') + elif x == 'MY_SO_CONC': + branches.append('Side_Outlet') + else: + branches.append('ERROR') + + start_eq = eq_id + conc_assignment = 'for (i=1; i<' + unit['Num_Model_Components'] + '; i++)\n' + conc_assignment += ' LHS[' + str(start_eq-1) + '+i] = ' + ct = [unit[br+'_Arrayname']+'[i]' for br in branches] + conc_assignment += ' - '.join(ct) + '\n' + + start_eq = start_eq + int(unit['Num_Model_Components']) - 1 + + return conc_assignment, start_eq + + +def generate_inlet_flow_weighted_avg(unit, inlet_streams, start_eq_id): + """ Generate the flow weighted average inlet concentrations + + Args: + unit: the process unit whose inlet concs are being determined + inlet_streams: the inlet arrays for the 'unit' + start_eq_id: starting equation id for the LHS, int + + Return: + a str of C code to generate the flow weighted avg (model components), + an updated eq_id + + Example: + 'for(j=1; j<14; j++) + LHS[start_eq_id-1+j] = unit.in_comps[j] + - (inlet_A[j]*inlet_A[0] + inlet_B[j]*inlet_B[0]) / unit.in_comps[0];' + """ + n = len(inlet_streams) + if n == 0: + return '// ERROR in ' + unit['Codename'] + "'s inlet connection.", start_eq_id + + fwavg = [ 'for(i=1; i<' + unit['Num_Model_Components'] + '; i++)\n' ] + + calcs = ' ' + 'LHS[' + str(start_eq_id - 1) + '+i] = ' + unit['Inlet_Arrayname'] + '[i] - ' + if n > 1: + calcs += '(' + ''.join([dschg + '[i] * ' + dschg + '[0]' for dschg in inlet_streams]) + ')' + calcs += ' / ' + unit['Inlet_Arrayname'] + '[0];\n' + else: # n==1 + calcs += ''.join([dschg + '[i];\n' for dschg in inlet_streams]) + + fwavg.append(calcs) + + return ''.join(fwavg), start_eq_id+int(unit['Num_Model_Components'])-1 diff --git a/PooPyLab/model_builder/sundials/model_composer.py b/PooPyLab/model_builder/sundials/model_composer.py new file mode 100644 index 0000000..8a36108 --- /dev/null +++ b/PooPyLab/model_builder/sundials/model_composer.py @@ -0,0 +1,123 @@ +# This file is part of PooPyLab. +# +# PooPyLab is a simulation software for biological wastewater treatment processes using International Water Association +# Activated Sludge Models. +# +# Copyright (C) Kai Zhang +# +# PooPyLab is free software: you can redistribute it and/or modify it under the terms of the GNU General Public +# License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with PooPyLab. If not, see +# . +# +# +# This is the definition of the ASM1 model to be imported as part of the Reactor object +# +# + +from .model_builder_common import define_branch_arrays, assign_solver_array, collect_inlet_arrays +from .model_builder_common import substr_for_flow, substr_for_concs, generate_inlet_flow_weighted_avg + + +def select_model_template(unit): + """ Select the model template for the unit + + Args: + unit: the pipe unit under construction, {} + Return: + model template [''] + """ + selected_model = [] + accept = False + with open(unit['Model_File_Path'], 'rt') as tf: + for line in tf: + if unit['MO_Flow_Data_Source'] in line or accept == True: + accept = True + if accept == True and line[0]!='#' and line!="\n" and ('[' not in line) and (']' not in line): + selected_model.append(line) + elif selected_model and (unit['MO_Flow_Data_Source'] not in line) and ('[' in line and ']' in line): + break + return selected_model + + +def substitue_pipe_model(unit, selected_model, inlet_streams, start_eq_id): + """ Construct a pipe model based on the selected template + + Args: + unit: the pipe unit under construction, {} + selected_model: [''] + inlet_streams: list of inlet streams arrayname to "unit", [''] + start_eq_id: starting equation id, int + Return: + C code as model equations for a pipe (str) + updated eq_id (int) + """ + + pipe_model = '' + eq_id = start_eq_id + for line in selected_model: # e.g. line = 'FLOW : ZERO = MY_IN_FLOW - MY_MO_FLOW' + splt = line.split(':') # splt = ['FLOW ', 'ZERO = MY_IN_FLOW - MY_MO_FLOW'] + line = splt[1] # line = 'ZERO = MY_IN_FLOW - MY_MO_FLOW' + rhs = line.split('=')[1] # rhs = 'MY_IN_FLOW - MY_MO_FLOW' + model_terms = rhs.split('-') # model_terms = ['MY_IN_FLOW ', ' MY_MO_FLOW'] + if splt[0].strip() == 'FLOW': + for ft in model_terms: + fts = ft.strip() + line = line.replace(fts, substr_for_flow(unit, fts, inlet_streams)) + line = line.replace('ZERO','LHS[' + str(eq_id) + ']') + pipe_model += line + '\n' + eq_id += 1 + elif splt[0].strip() == 'CONC': + if 'FWA' in line: + line, eq_id = generate_inlet_flow_weighted_avg(unit, inlet_streams, eq_id) + else: + line, eq_id = substr_for_concs(unit, model_terms, eq_id) + pipe_model += line + '\n' + return pipe_model, eq_id + + +def compose_sys(pfd={}, tab=2): + """ Compose the units' variable/array declarations and mass balance equations + + Args: + pfd: dict storing the process flowsheet + tab: size of a "tab" + Return: + declaration of the arrays + equations of all the units in pfd + """ + # declare the arrays as SUNDIALS realtype + array_names = ['//Error in ' + unit['Codename'] + ' configs...' if define_branch_arrays(unit) == '' + else define_branch_arrays(unit) + ';' + for unit in pfd["Flowsheet"].values()] + declars = ['sunrealtype ' + aname if aname[:2] != '//' + else aname + for aname in array_names] + declars.append('sunindextype i;\n') + + nc = int(list(pfd['Flowsheet'].values())[0]['Num_Model_Components']) # No. of model Components + array_assign = assign_solver_array(array_names, nc) + + all_eqs = [] + id_eq = 0 + for c in pfd['Flowsheet'].values(): + inlet_streams = collect_inlet_arrays(pfd, c) + if c['Type'] == 'Pipe' or c['Type'] == 'Splitter': + selected_model = select_model_template(c) + eqs, id_eq = substitue_pipe_model(c, selected_model, inlet_streams, id_eq) + all_eqs.append(eqs) + + return declars, array_assign, all_eqs + + +def write_to_file(filename='syseqs.c', lines=[], write_mode='w'): + with open(filename, write_mode) as eqf: + for item in lines: + eqf.write(item) + eqf.write('\n') + return None diff --git a/PooPyLab/model_builder/sundials/model_constants.h b/PooPyLab/model_builder/sundials/model_constants.h new file mode 100644 index 0000000..5e82ef3 --- /dev/null +++ b/PooPyLab/model_builder/sundials/model_constants.h @@ -0,0 +1,18 @@ +/* Model Constants */ +/* TODO: Can be split into 2 files: + * one for stoichiometrics, and the other for kinetics + */ + +#ifndef MODEL__CONSTANTS +#define MODEL__CONSTANTS + +#include + +#define ZERO SUN_RCONST(0.0) +#define MU_MAX SUN_RCONST(6.0) +#define DECAY SUN_RCONST(0.62) +#define KD SUN_RCONST(0.08) +#define YH SUN_RCONST(0.67) +#define KS SUN_RCONST(10.0) + +#endif diff --git a/PooPyLab/model_builder/sundials/model_input.h b/PooPyLab/model_builder/sundials/model_input.h new file mode 100644 index 0000000..f5ea071 --- /dev/null +++ b/PooPyLab/model_builder/sundials/model_input.h @@ -0,0 +1,22 @@ +/* Influent Conditions - Steady State */ + +#ifndef MODEL__INPUT +#define MODEL__INPUT + +#include + +#define X0 SUN_RCONST(2000.0) /* initial y components */ +#define S0 SUN_RCONST(1.0) +#define D0 SUN_RCONST(1.0e2) +#define VOL SUN_RCONST(378000.0) +#define ONE SUN_RCONST(1.0) +#define P1_IN_F SUN_RCONST(7800.0) +#define P1_IN_X SUN_RCONST(0.0) +#define P1_IN_S SUN_RCONST(395.0) +#define P1_IN_D SUN_RCONST(0.0) +#define P2_IN_F SUN_RCONST(30000.0) +#define P2_IN_X SUN_RCONST(0.0) +#define P2_IN_S SUN_RCONST(395.0) +#define P2_IN_D SUN_RCONST(0.0) + +#endif diff --git a/PooPyLab/model_builder/sundials/sundials_ida_trial.c b/PooPyLab/model_builder/sundials/sundials_ida_trial.c new file mode 100644 index 0000000..ad4b9b1 --- /dev/null +++ b/PooPyLab/model_builder/sundials/sundials_ida_trial.c @@ -0,0 +1,111 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "utilities.h" +#include "system_equations.h" +#include "model_input.h" + + + +int main() +{ + + SUNContext ctx; + void *ida_mem; + sunrealtype t, t0, tf, tout, dt, tret; + N_Vector yy, yp, yid, yy0_mod, yp0_mod; + sunrealtype rtol, atol; + SUNMatrix A; + SUNLinearSolver LS; + SUNNonlinearSolver NLS; + int retval, i; + + ida_mem = NULL; + yy = NULL; + A = NULL; + LS = NULL; + NLS = NULL; + + t0 = 0.0; //move to macro + tf = 100.0; //move to macro + tout = 1.0; //move to macro + dt = 1.0; //move to macro + + rtol = 1.0e-14; //move to macro + atol = 1.0e-5; //move to macro + + retval = SUNContext_Create(SUN_COMM_NULL, &ctx); + + yy = N_VNew_Serial(NEQ, ctx); + /* Initial guess */ + Ith(yy,1) = 2000.0; Ith(yy,2) = X0; Ith(yy,3) = S0; Ith(yy,4) = D0; + Ith(yy,5) = 2000.0; Ith(yy,6) = X0; Ith(yy,7) = S0; Ith(yy,8) = D0; + Ith(yy,9) = 2000.0; Ith(yy,10) = X0; Ith(yy,11) = S0; Ith(yy,12) = D0; + Ith(yy,13) = 2000.0; Ith(yy,14) = X0; Ith(yy,15) = S0; Ith(yy,16) = D0; + + yp = N_VNew_Serial(NEQ, ctx); + for(i=1; i + +// number of equations +#define NEQ 16 + +// system equations +int funcGrowth(sunrealtype t, N_Vector y, N_Vector yp, N_Vector LHS, void *user_data); + +#endif diff --git a/PooPyLab/model_builder/sundials/system_equations_backup.c b/PooPyLab/model_builder/sundials/system_equations_backup.c new file mode 100644 index 0000000..38d1b05 --- /dev/null +++ b/PooPyLab/model_builder/sundials/system_equations_backup.c @@ -0,0 +1,64 @@ +/* ----------------------------------------------------------------- + Testing Sundials Nonlinear Solver with the following system: + Pipe_1: Flow_1, X_1, S_1, D_1 + Pipe_2: Flow_1, X_1, S_1, D_1 + + Pipe_1 & Pipe_2 Feed to Reactor R1 Inlet + + Reactor R1: + dX/dt = 0 = (Xi - X) / HRT + ( mu_max * (S/(Ks+S) - decay) * X + dS/dt = 0 = (Si - S) / HRT + ( -mu_max * (S/(Ks+S) / Yh) + (1-kd) * decay) * X + dD/dt = 0 = (Di - D) / HRT + kd * decay * X + */ + +#include "utilities.h" +#include "model_constants.h" +#include "system_equations.h" +#include "model_input.h" + + +int funcGrowth(sunrealtype t, N_Vector y, N_Vector yp, N_Vector LHS, void *user_data) +{ + sunrealtype P1_mo_X, P1_mo_S, P1_mo_D, P1_mo_F; + sunrealtype P2_mo_X, P2_mo_S, P2_mo_D, P2_mo_F; + sunrealtype R1_in_X, R1_in_S, R1_in_D, R1_in_F; + sunrealtype R1_mo_X, R1_mo_S, R1_mo_D, R1_mo_F; + + + P1_mo_F = Ith(y,1); P1_mo_X = Ith(y,2); P1_mo_S = Ith(y,3); P1_mo_D = Ith(y,4); + P2_mo_F = Ith(y,5); P2_mo_X = Ith(y,6); P2_mo_S = Ith(y,7); P2_mo_D = Ith(y,8); + R1_in_F = Ith(y,9); R1_in_X = Ith(y,10); R1_in_S = Ith(y,11); R1_in_D = Ith(y,12); + R1_mo_F = Ith(y,13); R1_mo_X = Ith(y,14); R1_mo_S = Ith(y,15); R1_mo_D = Ith(y,16); + + Ith(LHS,1) = P1_mo_F - P1_IN_F; + Ith(LHS,2) = P1_mo_X - P1_IN_X; + Ith(LHS,3) = P1_mo_S - P1_IN_S; + Ith(LHS,4) = P1_mo_D - P1_IN_D; + + Ith(LHS,5) = P2_mo_F - P2_IN_F; + Ith(LHS,6) = P2_mo_X - P2_IN_X; + Ith(LHS,7) = P2_mo_S - P2_IN_S; + Ith(LHS,8) = P2_mo_D - P2_IN_D; + + Ith(LHS,9) = R1_in_F - (P1_mo_F + P2_mo_F); + Ith(LHS,10) = R1_in_X - ((P1_mo_F * P1_mo_X + P2_mo_F * P2_mo_X) / R1_in_F); + Ith(LHS,11) = R1_in_S - ((P1_mo_F * P1_mo_S + P2_mo_F * P2_mo_S) / R1_in_F); + Ith(LHS,12) = R1_in_D - ((P1_mo_F * P1_mo_D + P2_mo_F * P2_mo_D) / R1_in_F); + + Ith(LHS,13) = R1_mo_F - R1_in_F; + + Ith(LHS,14) = R1_mo_F / VOL * (R1_in_X - R1_mo_X) + + (MU_MAX * R1_mo_S/(KS+R1_mo_S) - DECAY) * R1_mo_X + - Ith(yp,14); + + Ith(LHS,15) = R1_mo_F / VOL * (R1_in_S - R1_mo_S) + + (MU_MAX * R1_mo_S/(KS+R1_mo_S) / (-YH) + (ONE - KD) * DECAY) * R1_mo_X + - Ith(yp,15); + + Ith(LHS,16) = R1_mo_F / VOL * (R1_in_D - R1_mo_D) + + KD * DECAY * R1_mo_X + - Ith(yp,16); + + return(0); +} + diff --git a/PooPyLab/model_builder/sundials/template.c b/PooPyLab/model_builder/sundials/template.c new file mode 100644 index 0000000..f952edb --- /dev/null +++ b/PooPyLab/model_builder/sundials/template.c @@ -0,0 +1,81 @@ +/* ----------------------------------------------------------------- + Testing Sundials Nonlinear Solver with the following system: + Pipe_1: Flow_1, X_1, S_1, D_1 + Pipe_2: Flow_1, X_1, S_1, D_1 + + Pipe_1 & Pipe_2 Feed to Reactor R1 Inlet + + Reactor R1: + dX/dt = 0 = (Xi - X) / HRT + ( mu_max * (S/(Ks+S) - decay) * X + dS/dt = 0 = (Si - S) / HRT + ( -mu_max * (S/(Ks+S) / Yh) + (1-kd) * decay) * X + dD/dt = 0 = (Di - D) / HRT + kd * decay * X + */ + +#include "utilities.h" +#include "model_constants.h" +#include "system_equations.h" +#include "model_input.h" + +/* POOPYLAB: SYSTEM FUNCTION DEGINS */ +int funcGrowth(realtype t, N_Vector y, N_Vector yp, N_Vector LHS, void *user_data) +{ + // Definitions of the internal variables: + // Guidelines: + // 1. Each process unit should have a realtype array that represents + // the flow rate and the model component; + // 2. Format: UnitName_UnitTypeShort_Array[length_of_model + 1], where + // *_Array[0]: flow rate in [L^3/T] + // *_Array[1..length_of_model+1]: model component concentrations in [M/L^3] + // + + // POOPYLAB: SYSTEM FUNCTION VARIABLES + realtype P1_mo_X, P1_mo_S, P1_mo_D, P1_mo_F; + realtype P2_mo_X, P2_mo_S, P2_mo_D, P2_mo_F; + realtype R1_in_X, R1_in_S, R1_in_D, R1_in_F; + realtype R1_mo_X, R1_mo_S, R1_mo_D, R1_mo_F; + + + // POOPYLAB: VARIABLE ASSIGNMENTS + P1_mo_F = Ith(y,1); P1_mo_X = Ith(y,2); P1_mo_S = Ith(y,3); P1_mo_D = Ith(y,4); + P2_mo_F = Ith(y,5); P2_mo_X = Ith(y,6); P2_mo_S = Ith(y,7); P2_mo_D = Ith(y,8); + R1_in_F = Ith(y,9); R1_in_X = Ith(y,10); R1_in_S = Ith(y,11); R1_in_D = Ith(y,12); + R1_mo_F = Ith(y,13); R1_mo_X = Ith(y,14); R1_mo_S = Ith(y,15); R1_mo_D = Ith(y,16); + + // POOPYLAB: MASS BALANCE EQUATIONS + // + // POOPYLAB: P1_Pipe_Array: + Ith(LHS,1) = P1_mo_F - P1_IN_F; + Ith(LHS,2) = P1_mo_X - P1_IN_X; + Ith(LHS,3) = P1_mo_S - P1_IN_S; + Ith(LHS,4) = P1_mo_D - P1_IN_D; + + // POOPYLAB: P2_Pipe_Array: + Ith(LHS,5) = P2_mo_F - P2_IN_F; + Ith(LHS,6) = P2_mo_X - P2_IN_X; + Ith(LHS,7) = P2_mo_S - P2_IN_S; + Ith(LHS,8) = P2_mo_D - P2_IN_D; + + // POOPYLAB: R1_Rxn_Array: + Ith(LHS,9) = R1_in_F - (P1_mo_F + P2_mo_F); + Ith(LHS,10) = R1_in_X - ((P1_mo_F * P1_mo_X + P2_mo_F * P2_mo_X) / R1_in_F); + Ith(LHS,11) = R1_in_S - ((P1_mo_F * P1_mo_S + P2_mo_F * P2_mo_S) / R1_in_F); + Ith(LHS,12) = R1_in_D - ((P1_mo_F * P1_mo_D + P2_mo_F * P2_mo_D) / R1_in_F); + + Ith(LHS,13) = R1_mo_F - R1_in_F; + + Ith(LHS,14) = R1_mo_F / VOL * (R1_in_X - R1_mo_X) + + (MU_MAX * R1_mo_S/(KS+R1_mo_S) - DECAY) * R1_mo_X + - Ith(yp,14); + + Ith(LHS,15) = R1_mo_F / VOL * (R1_in_S - R1_mo_S) + + (MU_MAX * R1_mo_S/(KS+R1_mo_S) / (-YH) + (ONE - KD) * DECAY) * R1_mo_X + - Ith(yp,15); + + Ith(LHS,16) = R1_mo_F / VOL * (R1_in_D - R1_mo_D) + + KD * DECAY * R1_mo_X + - Ith(yp,16); + + // POOPYLAB: FUNCTION RETURN + return(0); +} +/* POOPYLAB: SYSTEM FUNCTION ENDS */ diff --git a/PooPyLab/model_builder/sundials/utilities.c b/PooPyLab/model_builder/sundials/utilities.c new file mode 100644 index 0000000..a7954eb --- /dev/null +++ b/PooPyLab/model_builder/sundials/utilities.c @@ -0,0 +1,59 @@ +#include "utilities.h" + +void PrintOutput(N_Vector y, int neq, sunrealtype vol) +{ + int i; + sunrealtype SolidsInventory; + + printf("y ="); +#if defined(SUNDIALS_EXTENDED_PRECISION) + for (i = 1; i < neq+1; i++) printf(" %14.6Le", Ith(y,i)); +#elif defined(SUNDIALS_DOUBLE_PRECISION) + for (i = 1; i < neq+1; i++) printf(" %14.6e", Ith(y,i)); +#else + for (i = 1; i < neq+1; i++) printf(" %14.6e", Ith(y,i)); +#endif + printf("\n"); + + SolidsInventory = (Ith(y,14) + Ith(y,16)) * vol * 1000; //mg as COD + printf("SRT = %8f, HRT = %8f, Solids Inventory = %14.6f\n", + SolidsInventory / (Ith(y,13) * 1000 * (Ith(y,14) + Ith(y,16))), + vol / Ith(y,13), + SolidsInventory); + return; +} + + +int check_retval(void *retvalvalue, const char *funcname, int opt) +{ + int *errretval; + + /* Check if SUNDIALS function returned NULL pointer - no memory allocated */ + if (opt == 0 && retvalvalue == NULL) { + fprintf(stderr, + "\nSUNDIALS_ERROR: %s() failed - returned NULL pointer\n\n", + funcname); + return(1); + } + + /* Check if retval < 0 */ + else if (opt == 1) { + errretval = (int *) retvalvalue; + if (*errretval < 0) { + fprintf(stderr, + "\nSUNDIALS_ERROR: %s() failed with retval = %d\n\n", + funcname, *errretval); + return(1); + } + } + + /* Check if function returned NULL pointer - no memory allocated */ + else if (opt == 2 && retvalvalue == NULL) { + fprintf(stderr, + "\nMEMORY_ERROR: %s() failed - returned NULL pointer\n\n", + funcname); + return(1); + } + + return(0); +} diff --git a/PooPyLab/model_builder/sundials/utilities.h b/PooPyLab/model_builder/sundials/utilities.h new file mode 100644 index 0000000..3bb70da --- /dev/null +++ b/PooPyLab/model_builder/sundials/utilities.h @@ -0,0 +1,20 @@ +#ifndef UTILITIES__ +#define UTILITIES__ + +#include + +/* User-defined vector accessor macro: Ith */ +/* This macro is defined in order to write code which exactly matches + the mathematical problem description given above. + + Ith(v,i) references the ith component of the vector v, where i is in + the range [1..NEQ] and NEQ is defined above. The Ith macro is defined + using the N_VIth macro in nvector.h. N_VIth numbers the components of + a vector starting from 0. +*/ +#define Ith(v,i) NV_Ith_S(v,i-1) /* Ith numbers components 1..NEQ */ + +void PrintOutput(N_Vector u, int neq, sunrealtype vol); +int check_retval(void *retvalvalue, const char *funcname, int opt); + +#endif diff --git a/PooPyLab/model_builder/tested_but_not_used/model_builder_pythonic/ASM_2d.csv b/PooPyLab/model_builder/tested_but_not_used/model_builder_pythonic/ASM_2d.csv new file mode 100644 index 0000000..13fd622 --- /dev/null +++ b/PooPyLab/model_builder/tested_but_not_used/model_builder_pythonic/ASM_2d.csv @@ -0,0 +1,41 @@ +"No.Components",19,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +"No.Rates",21,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +"Parameter","Name","ID",,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +"f_SI",,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +"K_h",,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +"K_OH",,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +"g_NOX",,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +"Component.ID",0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,,,,,,,,,,,, +"Component.Name","DUMMY","Dissolved O2","Fermentation Product (mg/L COD)","Acetate (mg/L COD)","Ammonium_N (mg/L N)","Nitrate_N (mg/L N)","Ortho-P (mg/L P)","Inert Soluble COD (mg/L COD)","Alkalinity (mmol/L CaCO3)","Dissolved N2 (mg/L)","Inert Particulate COD (mg/L COD)","Biodegradable Particulate COD (mg/L COD)","Ordinary Heterotroph Organisms (mg/L COD)","PAO (mg/L COD)","Stored Poly P (mg/L P)","Stored PHA (mg/L COD)","Autotrophic Nitrifiers (mg/L COD)","Total Suspended Solids (mg/L TSS)","Ferric Hydroxides (mg/L Fe(OH)3)","Ferric Phosphate (mg/L FePO4)",,,,,,,,,,,, +"Component.Symb","VOID","S_O2","S_F","S_A","S_NH4","S_NO3","S_PO4","S_I","S_ALK","S_N2","X_I","X_S","X_H","X_PAO","X_PP","X_PHA","X_AUT","X_TSS","X_MeOH","X_MeP","Rate ID","RATE DESCRIPTION",,,,,,,,,, +"Stoich.Row.0",0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,,,,,,,,,, +"Stoich.Row.1",0,,"1.0 – f_SI",,"V_1_NH4",,"V_1_PO4","f_SI","V_1_ALK",,,"-1.0",,,,,,"V_1_TSS",,,1,"Aerobic Hydrolysis","K_h","S_O2 / (KH_O2 + S_O2)","(X_S / X_H) / (KH_X + X_S / X_H)","X_H",,,,,, +"Stoich.Row.2",0,,"1.0 – f_SI",,"V_2_NH4",,"V_2_PO4","f_SI","V_2_ALK",,,"-1.0",,,,,,"V_2_TSS",,,2,"Anoxic Hydrolysis","K_h","gH_NOX","KH_O2 / (KH_O2 + S_O2)","S_NO3 / (KH_NO3 + S_NO3)","(X_S / X_H) / (KH_X + X_S / X_H)","X_H",,,, +"Stoich.Row.3",0,,"1.0 – f_SI",,"V_3_NH4",,"V_3_PO4","f_SI","V_3_ALK",,,"-1.0",,,,,,"V_3_TSS",,,3,"Anaerobic Hydrolysis","K_h","gH_fe","KH_O2 / (KH_O2 + S_O2)","KH_NO3 / (KH_NO3 + S_NO3)","(X_S / X_H) / (KH_X + X_S / X_H)","X_H",,,, +"Stoich.Row.4",0,"1.0 – Y_H","-1.0 / Y_H",,"V_4_NH4",,"V_4_PO4",,"V_4_ALK",,,,"1.0",,,,,"V_4_TSS",,,4,"X_H Growth on Fermentable Substrates","u_H","S_O2 / (KH_O2 + S_O2)","S_F / (KH_F + S_F)","S_F / (S_F + S_A)","S_NH4 / (KH_NH4 + S_NH4)","S_PO4 / (KH_P + S_PO4)","S_ALK / (KH_ALK + S_ALK)","X_H",, +"Stoich.Row.5",0,"1.0 – Y_H",,"-1.0 / Y_H",,,,,"V_5_ALK",,,,"1.0",,,,,"V_5_TSS",,,5,"X_H Growth on Fermentation Products","u_H","S_O2 / (KH_O2 + S_O2)","S_A / (KH_A + S_A)","S_A / (S_F + S_A)","S_NH4 / (KH_NH4 + S_NH4)","S_PO4 / (KH_P + S_PO4)","S_ALK / (KH_ALK + S_ALK)","X_H",, +"Stoich.Row.6",0,,"-1.0 / Y_H",,"V_6_NH4","-(1.0 -Y_H) / (2.86 * Y_H)","V_6_PO4",,"V_6_ALK","(1.0 -Y_H) / (2.86 * Y_H)",,,"1.0",,,,,"V_6_TSS",,,6,"X_H Denitrification with fermentable substrates","u_H","gH_NOX","KH_O2 / (KH_O2 + S_O2)","KH_NO3 / (KH_NO3 + S_NO3)","S_F / (KH_F + S_F)","S_F / (S_F + S_A)","S_NH4 / (KH_NH4 + S_NH4)","S_PO4 / (KH_P + S_PO4)","S_ALK / (KH_ALK + S_ALK)","X_H" +"Stoich.Row.7",0,,,"-1.0 / Y_H",,"-(1.0 -Y_H) / (2.86 * Y_H)",,,"V_7_ALK","(1.0 -Y_H) / (2.86 * Y_H)",,,"1.0",,,,,"V_7_TSS",,,7,"X_H Denitrification with fermentation products","u_H","gH_NOX","KH_O2 / (KH_O2 + S_O2)","KH_NO3 / (KH_NO3 + S_NO3)","S_A / (KH_A + S_A)","S_A / (S_F + S_A)","S_NH4 / (KH_NH4 + S_NH4)","S_PO4 / (KH_P + S_PO4)","S_ALK / (KH_ALK + S_ALK)","X_H" +"Stoich.Row.8",0,,"-1.0","1.0","V_8_NH4",,"V_8_PO4",,"V_8_ALK",,,,,,,,,"V_8_TSS",,,8,"X_H Fermentation","q_fe","S_O2 / (KH_O2 + S_O2)","KH_NO3 / (KH_NO3 + S_NO3)","S_F / (KH_F + S_F)","S_ALK / (KH_ALK + S_ALK)","X_H",,,, +"Stoich.Row.9",0,,,,"V_9_NH4",,"V_9_PO4",,"V_9_ALK",,"f_XI","1.0 – f_XI","-1.0",,,,,"V_9_TSS",,,9,"Lysis of X_H","b_H","X_H",,,,,,,, +"Stoich.Row.10",0,,,"-1.0",,,"Y_PO4",,"V_10_ALK",,,,,,"- Y_PO4","1.0",,"V_10_TSS",,,10,"X_PAO Storage of X_PHA","q_PHA","S_A / (KP_A + S_A)","S_ALK / (KP_ALK + S_ALK)","(X_PP / X_PAO) / (KP_PP + X_PP / X_PAO)","X_PAO",,,,, +"Stoich.Row.11",0,"- Y_PHA",,,,,"-1.0",,"V_11_ALK",,,,,,"1.0","- Y_PHA",,"V_11_TSS",,,11,"X_PAO Aerobic Storage of X_PP","q_PP","S_O2 / (KP_O2 + S_O2)","S_PO4 / (KP_PS + S_PO4)","S_ALK / (KP_ALK + S_ALK)","(X_PHA / X_PAO) / (KP_PHA + X_PHA / X_PAO)","(KP_MAX – X_PP / X_PAO) / (KP_PP + KP_MAX – XP_PP / X_PAO)","X_PAO",,, +"Stoich.Row.12",0,,,,,"V_12_NO3","-1.0",,"V_12_ALK","- V_12_NO3",,,,,"1.0","- Y_PHA",,"V_12_TSS",,,12,"X_PAO Anoxic Storage of X_PP","Rate_11","gP_NOX","KP_O2 / S_O2","S_NO3 / (KP_NO3 + S_NO3)",,,,,, +"Stoich.Row.13",0,"V_13_O2",,,"V_13_NH4",,"- i_P_BM",,"V_13_ALK",,,,,"1.0",,"-1.0 / Y_H",,"V_13_TSS",,,13,"X_PAO Aerobic Growth of X_PHA","u_PAO","S_O2 / (KP_O2 + S_O2)","S_NH4 / (KP_NH4 + S_NH4)","S_PO4 / (KP_P + S_PO4)","S_ALK / (KP_ALK + S_ALK)","(X_PHA / X_PAO) / (KP_PHA + X_PHA / X_PAO)","X_PAO",,, +"Stoich.Row.14",0,,,,"V_14_NH4","V_14_NO3","- i_P_BM",,"V_14_ALK","- V_14_NO3",,,,"1.0",,"-1.0 / Y_H",,"V_14_TSS",,,14,"X_PAO Anoxic Growth of X_PP","Rate_13","gP_NOX","KP_O2 / S_O2","S_NO3 / (KP_NO3 + S_NO3)",,,,,, +"Stoich.Row.15",0,,,,"V_15_NH4",,"V_15_PO4",,"V_15_ALK",,"f_XI","1.0 – f_XI",,"-1.0",,,,"V_15_TSS",,,15,"Lysis of X_PAO","b_PAO","S_ALK / (KP_ALK + S_ALK)","X_PAO",,,,,,, +"Stoich.Row.16",0,,,,,,"1.0",,"V_16_ALK",,,,,,"-1.0",,,"V_16_TSS",,,16,"Lysis of X_PP","b_PP","S_ALK / (KP_ALK + S_ALK)","X_PP",,,,,,, +"Stoich.Row.17",0,,,"1.0",,,,,"V_17_ALK",,,,,,,"-1.0",,"V_17_TSS",,,17,"Lysis of X_PHA","b_PHA","S_ALK / (KP_ALK + S_ALK)","X_PHA",,,,,,, +"Stoich.Row.18",0,"- (4.57 - Y_A) / Y_A",,,"V_18_NH4","1.0 / Y_A","- i_P_BM",,"V_18_ALK",,,,,,,,"1.0","V_18_TSS",,,18,"Aerobic Growth of X_AUT","u_AUT","S_O2 / (KA_O2 + S_O2)","S_NH4 / (KA_NH4 + S_NH4)","S_PO4 / (KA_P + S_PO4)","S_ALK / (KA_ALK + S_ALK)","X_AUT",,,, +"Stoich.Row.19",0,,,,"V_19_NH4",,"V_19_PO4",,"V_19_ALK",,"f_XI","1.0 – f_XI",,,,,"-1.0","V_19_TSS",,,19,"Lysis of X_AUT","b_AUT","X_AUT",,,,,,,, +"Stoich.Row.20",0,,,,,,"-1.0",,"V_20_ALK",,,,,,,,,"1.42","-3.45","4.87",20,"Precipitation","k_PRE","S_PO4","X_MeOH",,,,,,, +"Stoich.Row.21",0,,,,,,"1.0",,"V_21_ALK",,,,,,,,,"-1.42","3.45","-4.87",21,"Re-dissolution","k_RED","X_MeP","S_ALK / (KM_ALK + S_ALK)",,,,,,, +"CONVERSION FACTOR",,,,,,,,,,,,,,,,,,,,,,"CONVERSION FACTOR UNIT",,,,,,,,,, +"COD",0,"-1","1","1","0","-64/14","0","1","0","-24/14","1","1","1","1","0","1","1","0","0","0",,"g COD / g Component_i as COD or N",,,,,,,,,, +"N",0,"0","i_N_SF","0","1","1","0","i_N_SI","0","1","i_N_XI","i_N_XS","i_N_BM","i_N_BM","0","0","i_N_BM","0","0","0",,"g N / g Component_i as COD or N",,,,,,,,,, +"P",0,"0","i_P_SF","0","0","0","1","i_P_SI","0","0","i_P_XI","i_P_XS","i_P_BM","i_P_BM","1","0","i_P_BM","0","0","0.205",,"g P / g Component_i as COD or P",,,,,,,,,, +"Charge ",0,"0","0","-1/64","1/14","-1/14","-1.5/31","0","-1","0","0","0","0","0","-1/31","0","0","0","0","0",,"Charge / g of Component_i as [COD, N, or P] or mole HCO3-",,,,,,,,,, +"Mass (g TSS)",0,"0","0","0","0","0","0","0","0","0","i_TSS_XI","i_TSS_XS","i_TSS_BM","i_TSS_BM","3.23","0.6","i_TSS_BM","-1","1","1",,"g TSS / g of Component_i as COD, P or TSS",,,,,,,,,, diff --git a/PooPyLab/model_builder/tested_but_not_used/model_builder_pythonic/KZTest.py b/PooPyLab/model_builder/tested_but_not_used/model_builder_pythonic/KZTest.py new file mode 100644 index 0000000..e163140 --- /dev/null +++ b/PooPyLab/model_builder/tested_but_not_used/model_builder_pythonic/KZTest.py @@ -0,0 +1,53 @@ +# This file defines the model named KZTest +# +# Created Using: PooPyLab Model Builder +# User Specified Model in: template_asm1.csv +# Created at: 21:19:12 2021-October-27 +# + + +from .asmbase import asm_model + + +class KZTest(asm_model): + def __init__(self, ww_temp=20, DO=2): + """ + Args: + ww_temp: wastewater temperature, degC; + DO: dissoved oxygen, mg/L + + Return: + None + See: + _set_ideal_kinetics_20C(); + _set_params(); + _set_stoichs(). + """ + + asm_model.__init__(self) + self.__class__.__id += 1 + + self._set_ideal_kinetics_20C_to_defaults() + + # wastewater temperature used in the model, degC + self._temperature = ww_temp + + # mixed liquor bulk dissolved oxygen, mg/L + self._bulk_DO = DO + + # temperature difference b/t what's used and baseline (20C), degC + self._delta_t = self._temperature - 20 + + self.update(ww_temp, DO) + + # KZTest model components + self._comps = [0.0] * 13 + + # Intermediate results of rate expressions, M/L^3/T + # The list is to help speed up the calculation by reducing redundant + # calls of individual rate expressions in multiple mass balance equations + # for the model components. + # KZTest has 8 bio processes. + self._rate_res = [0.0] * 8 + + return None diff --git a/PooPyLab/model_builder/tested_but_not_used/model_builder_pythonic/equation_based_model.py b/PooPyLab/model_builder/tested_but_not_used/model_builder_pythonic/equation_based_model.py new file mode 100644 index 0000000..bb275ca --- /dev/null +++ b/PooPyLab/model_builder/tested_but_not_used/model_builder_pythonic/equation_based_model.py @@ -0,0 +1,296 @@ +#!/usr/bin/python3 + +# This file is part of PooPyLab. +# +# PooPyLab is a simulation software for biological wastewater treatment processes using International Water Association +# Activated Sludge Models. +# +# Copyright (C) Kai Zhang +# +# PooPyLab is free software: you can redistribute it and/or modify it under the terms of the GNU General Public +# License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# PooPyLab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with PooPyLab. If not, see +# . +# +# ----------------------------------------------------------------------------- +# Definition of a Complete-Mix Activated Sludge (CMAS) Process Flow Diagram. +# +# +# Process Flow Diagram: +# +# Inlet --p1-> reactor(ra) --p2-> final.clar --p3-> outlet +# ^ | +# | p4 +# | | +# | | +# | V +# +<--RAS-----splt*<------+ +# | +# +----------p5---------> waste (WAS) +# +# +# splt* is an SRT Controlling Splitter +# +# Author: Kai Zhang +# +# Change Log: +# 20220203 KZ: use for equation based simulation prototyping +# 20201129 KZ: re-run after package structure update +# 20190920 KZ: revised to match other testing results +# 20190724 KZ: init +# + +# Inlet (Influent_1): +# Known: IN_FLOW, model components from fractionation estimate, +# Flow_Source: Inlet = UPS, MainOutlet = UPS, SideOutlet = PRG +# Find: MO_FLOW, MO_COMPS + +# p1 (Pipe_1): +# Known: Flow_Source: Inlet = UPS, MainOutlet = UPS, SideOutlet = PRG +# Find: IN_FLOW, MO_FLOW, IN_COMPS, MO_COMPS + +# ra (ASMReactor_1): +# Known: Kinetic Parameters, Stoichiometrics, Mixed Liquor Temperature, Mixed Liquor DO, +# Flow_Source: Inlet = DNS, MainOutlet = DNS, SideOutlet = PRG +# Find: IN_FLOW, MO_FLOW, IN_COMPS, MO_COMPS + +# p2 (Pipe_2): +# Known: Flow_Source: Inlet = DNS, MainOutlet = DNS, SideOutlet = PRG +# Fine: IN_FLOW, MO_FLOW, IN_COMPS, MO_COMPS + +# fc (FinalClarifier_1): +# Known: Flow_Source: Inlet = DNS, MainOutlet = DNS, SideOutlet = DNS +# Find: IN_FLOW, MO_FLOW, SO_FLOW, IN_COMPS, MO_COMPS, SO_COMPS + +# p3 (Pipe_3): +# Known: Flow_Source: Inlet = DNS, MainOutlet = DNS, SideOutlet = PRG +# Find: IN_FLOW, MO_FLOW, IN_COMPS, MO_COMPS + +# outlet (Effluent_1): +# Known: Flow_Source: Inlet = DNS, MainOutlet = PRG, SideOutlet = PRG +# Find: IN_FLOW, IN_COMPS + +# p4 (Pipe_4): +# Known: Flow_Source: Inlet = DNS, MainOutlet = DNS, SideOutlet = PRG +# Find: IN_FLOW, MO_FLOW, IN_COMPS, MO_COMPS + +# splt* (Splitter_1): +# Known: SRT_Controller = True +# Flow_Source: Inlet = DNS, MainOutlet = PRG, SideOutlet = DNS +# Find: IN_FLOW, MO_FLOW, SO_FLOW, IN_COMPS, MO_COMPS, SO_COMPS + +# RAS: +# Known: Flow_Source: Inlet = DNS, MainOutlet = DNS, SideOutlet = PRG +# Find: IN_FLOW, MO_FLOW, IN_COMPS, MO_COMPS + +# p5 (Pipe_5): +# Known: Flow_Source: Inlet = DNS, MainOutlet = DNS, SideOutlet = PRG +# Find: IN_FLOW, MO_FLOW, IN_COMPS, MO_COMPS + +# waste (WAS_1): +# Known: Flow_Source: Inlet = DNS, MainOutlet = DNS, SideOutlet = PRG +# Find: IN_FLOW, IN_COMPS + + +def build_var_dictionary(wwtp): + """Document the variables in the wwtp's units and their indices used in the equation solving routine. + + Args: + wwtp: the collection(list) of all the process units to be analyzed + + Return: + {unit.__name__: [id_IN_FLOW, id_MO_FLOW, id_SO_FLOW, id_IN_COMPS, id_MO_COMPS, id_SO_COMPS]} + + """ + res = {} + + #TODO: should this fuction be built into the equation based version of the traverse routine instead? + return res + + +def build_model_const_dictionary(model_filename): + """Document the model constants and their indices used in the equation solving routine + + Args: + model_filename: path to the file that stores the Peterson matrix format of the model + + Return: + {unit.__name__: {const_name: value}} + """ + res = {} + return res + + +def define_initial_guess(wwtp, flows, comps): + """Pass the initial guess into x0 for the equation system's solver function + + Args: + wwtp: the collection(list) of all the process units to be analyzed + flows: list of flows to be used as initial guess + comps: list of concentrations to be used as initial guess + + Return: + list of initial flows and concentrations aligned with the result of build_var_dictionary() + + See Also: + build_var_dictionary() + """ + #TODO: develop this fuction + res = [] + return res + + +def eqs_sys(Influent_1_MO_FLOW, Influent_1_MO_COMPS, + Pipe_1_IN_FLOW, Pipe_1_MO_FLOW, Pipe_1_IN_COMPS, Pipe_1_MO_COMPS, + ASMReactor_1_IN_FLOW, ASMReactor_1_MO_FLOW, ASMReactor_1_IN_COMPS, ASMReactor_1_MO_COMPS): + # list of RHS below + RHS = [] + + # number of model components: + num_comps = len(Influent_1_MO_COMPS) + + + # Inlet: + RHS.append(Influent_1_MO_FLOW - 3780.0) + RHS.append(Influent_1_MO_COMPS[0] - 0) # MO_COMPS[0] is DO in this prototype + RHS.append(Influent_1_MO_COMPS[1] - 200) #MO_COMPS[1] is S_S + RHS.append(Influent_1_MO_COMPS[2] - 0) # MO_COMPS[2] is X_BH + + + # p1: + RHS.append(Pipe_1_IN_FLOW - Influent_1_MO_FLOW) + RHS.append(Pipe_1_MO_FLOW - Pipe_1_IN_FLOW) + for i in range(num_comps): + RHS.append(Pipe_1_IN_COMPS[i] - Influent_1_MO_COMPS[i]) + for i in range(num_comps): + RHS.append(Pipe_1_MO_COMPS[i] - Pipe_1_IN_COMPS[i]) + + + # ra: + RHS.append(ASMReactor_1_IN_FLOW - Influent_1_MO_FLOW - RAS_MO_FLOW) + RHS.append(ASMReactor_1_MO_FLOW - ASMReactor_1_IN_FLOW) + + for i in range(num_comps): + RHS.append(ASMReactor_1_IN_COMPS[i] + - (Influent_1_MO_COMPS[i] * Influent_1_MO_FLOW + Pipe_1_MO_COMPS[i] * Pipe_1_MO_FLOW) + / ASMReactor_1_IN_FLOW) + + ASMReactor_1_MONOD[0] = ASMReactor_1_MO_COMPS[1] / (ASMReactor_1_KS + ASMReactor_1_MO_COMPS[1]) + + ASMReactor_1_RATE[0] = ASMReactor_1_MUHAT * ASMReactor_1_MONOD[0] * ASMReactor_1_MO_COMPS[2] + + ASMReactor_1_OVERALLRATE[0] = (ASMReactor_1_YH - 1) / ASMReactor_1_YH * ASMReactor_1_RATE[0] + + ASMReactor_1_OVERALLRATE[1] = -1 / ASMReactor_1_YH * ASMReactor_1_RATE[0] + + ASMReactor_1_OVERALLRATE[2] = ASMReactor_1_RATE[0] + + for i in range(num_comps): + RHS.append((ASMReactor_1_IN_FLOW * ASMReactor_1_IN_COMPS[i] - ASMReactor_1_MO_FLOW * ASMReactor_1_MO_COMPS[i]) + + ASMReactor_1_OVERALLRATE[i]) + + + # p2: + RHS.append(Pipe_2_IN_FLOW - ASMReactor_1_MO_FLOW) + RHS.append(Pipe_2_MO_FLOW - Pipe_2_IN_FLOW) + for i in range(num_comps): + RHS.append(Pipe_2_IN_COMPS[i] - ASMReactor_1_MO_COMPS[i]) + for i in range(num_comps): + RHS.append(Pipe_2_MO_COMPS[i] - Pipe_2_IN_COMPS[i]) + + + # Final_Clar, assuming it is a perfect clarifier for prototype testing + RHS.append(FinalClarifier_1_IN_FLOW - Pipe_2_MO_FLOW) + RHS.append(FinalClarifier_1_IN_FLOW - FinalClarifier_1_SO_FLOW - FinalClarifier_1_MO_FLOW) + RHS.append(FinalClarifier_1_SO_FLOW - Pipe_4_IN_FLOW) + + for i in range(num_comps): + RHS.append(FinalClarifier_1_IN_COMPS[i] - Pipe_2_MO_COMPS[i]) + + RHS.append(FinalClarifier_1_MO_COMPS[0] - 0) # assume DO is depeleted + RHS.append(FinalClarifier_1_MO_COMPS[1] - FinalClarifier_1_IN_COMPS[1]) + RHS.append(FinalClarifier_1_MO_COMPS[2] - 0) # assume perfect solids-liquid separation + + RHS.append(FinalClarifier_1_SO_COMPS[0] - 0) + RHS.append(FinalClarifier_1_SO_COMPS[1] - FinalClarifier_1_IN_COMPS[1]) + RHS.append(FinalClarifier_1_SO_COMPS[2] + - (FinalClarifier_1_IN_FLOW * FinalClarifier_1_IN_COMPS[2] + - FinalClarifier_1_MO_FLOW * FinalClarifier_1_MO_COMPS[2]) + / FinalClarifier_1_SO_FLOW) + + + # p3 + RHS.append(Pipe_3_IN_FLOW - Pipe_3_MO_FLOW) + RHS.append(Pipe_3_MO_FLOW - Effluent_1_IN_FLOW) + for i in range(num_comps): + RHS.append(Pipe_3_IN_COMPS[i] - FinalClarifier_1_MO_COMPS[i]) + for i in range(num_comps): + RHS.append(Pipe_3_MO_COMPS[i] - Pipe_3_IN_COMPS[i]) + + + # p4 + RHS.append(Pipe_4_IN_FLOW - Splitter_1_IN_FLOW) + RHS.append(Pipe_4_MO_FLOW - FinalClarifier_1_IN_FLOW) + for i in range(num_comps): + RHS.append(Pipe_4_IN_COMPS[i] - FinalClarifier_1_MO_COMPS[i]) + for i in range(num_comps): + RHS.append(Pipe_4_MO_COMPS[i] - Pipe_4_IN_COMPS[i]) + + + # splt1, SRT Controlling + RHS.append(Splitter_1_IN_FLOW - Splitter_1_SO_FLOW - Splitter_1_MO_FLOW) + RHS.append(Splitter_1_MO_FLOW - RAS_IN_FLOW) + RHS.append(Splitter_1_SO_FLOW - Pipe_5_IN_FLOW) + + for i in range(num_comps): + RHS.append(Splitter_1_IN_COMPS[i] - FinalClarifier_1_SO_COMPS[i]) + for i in range(num_comps): + RHS.append(Splitter_1_MO_COMPS[i] - FinalClarifier_1_IN_COMPS[i]) + for i in range(num_comps): + RHS.append(Splitter_1_SO_COMPS[i] - FinalClarifier_1_IN_COMPS[i]) + + + # RAS + RHS.append(RAS_IN_FLOW - USER_SET_RAS_IN_FLOW) + RHS.append(RAS_MO_FLOW - RAS_IN_FLOW) + for i in range(num_comps): + RHS.append(RAS_IN_COMPS[i] - Splitter_1_MO_COMPS[i]) + for i in range(num_comps): + RHS.append(RAS_MO_COMPS[i] - RAS_IN_COMPS[i]) + + + # p5 + RHS.append(Pipe_5_IN_FLOW - Pipe_5_MO_FLOW) + RHS.append(Pipe_5_MO_FLOW - WAS_1_IN_FLOW) + for i in range(num_comps): + RHS.append(Pipe_5_IN_COMPS[i] - Splitter_1_SO_COMPS[i]) + for i in range(num_comps): + RHS.append(Pipe_5_MO_COMPS[i] - Pipe_5_IN_COMPS[i]) + + + # WAS + RHS.append(WAS_1_IN_FLOW + - (ASMReactor_1_MO_COMPS[2] * ASMReactor_1_VOLUME / Target_SRT + - Effluent_1_IN_COMPS[2] * Effluent_1_IN_FLOW) + * 1.42 / Pipe_5_MO_COMPS[2]) + for i in range(num_comps): + RHS.append(WAS_1_IN_COMPS[i] - Pipe_5_MO_COMPS[i]) + + + # Effluent + RHS.append(Effluent_1_IN_FLOW - (Influent_1_MO_FLOW - WAS_1_IN_FLOW)) + for i in range(num_comps): + RHS.append(Effluent_1_IN_COMPS[i] - Pipe_3_MO_COMPS[i]) + + return RHS[:] + #TODO: continue to build the model out + + + + diff --git a/PooPyLab/model_builder/tested_but_not_used/model_builder_pythonic/model_writer.py b/PooPyLab/model_builder/tested_but_not_used/model_builder_pythonic/model_writer.py new file mode 100755 index 0000000..ecc6041 --- /dev/null +++ b/PooPyLab/model_builder/tested_but_not_used/model_builder_pythonic/model_writer.py @@ -0,0 +1,319 @@ +#!/usr/bin/python3 + +# This file is part of PooPyLab. +# +# PooPyLab is a simulation software for biological wastewater treatment processes using the International Water +# Association Activated Sludge Models. +# +# Copyright (C) Kai Zhang +# +# This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public +# License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with this program. If not, see +# . +# +# +# Definition of the equation writing functions. +# + + +#==========Build Binary Tree from a Given Expression================ + +class expr_tree_node(): + def __init__(self, txt=''): + self.content = txt + self.parent = None + self.left = None + self.right = None + if self.content == '+' or self.content == '-': + self.priority = 2 + elif self.content == '*' or self.content == '/': + self.priority = 1 + else: + self.priority = 0 + return None + + +def is_operator(char=''): + """ check whether a character is '+', '-', '*', or '/' """ + return (char == '+' or char == '-' or char == '*' or char == '/') + + +def has_operator(term=[]): + """ check whether a term contain '+', '-', '*', or '/' """ + return ('+' in term or '-' in term or '*' in term or '/' in term) + + +def _found_empty_operator(node_var, treetop, unfinished): + u = unfinished.pop() + u.right = node_var + node_var.parent = u + return treetop, unfinished + + +def _found_empty_treetop(node_var, node_ops, treetop, unfinished): + treetop = [node_ops] + node_ops.left = node_var + node_var.parent = node_ops + unfinished = [node_ops] + return treetop, unfinished + + +def _connect_nodes(node_var, node_ops, treetop, unfinished): + if treetop[0].priority > node_ops.priority: + u = unfinished.pop() + if u.priority <= node_ops.priority: + u.right = node_var + node_var.parent = u + node_ops.parent = u.parent + node_ops.parent.right = node_ops + node_ops.left = u + unfinished = [node_ops] + else: + u.right = node_ops + node_ops.parent = u + node_ops.left = node_var + node_var.parent = node_ops + unfinished = [node_ops] + else: + node_ops.left = treetop[0] + treetop[0].parent = node_ops + u = unfinished.pop() + u.right = node_var + node_var.parent = u + treetop = [node_ops] + unfinished = [treetop[0]] + return treetop, unfinished + + +def build_tree(expr=' B * A / (Z* ( D+ C* F )/ K - (G+H)*V )'): + start = 0 + left_paren_count = 0 + queue = [] + print(expr) + return create_nodes(expr, start, left_paren_count, queue) + + +def create_nodes(expr='B*A/(Z*(D+C*F)/K-(G+H)*V)', start=0, left_paren_count=0, queue=[]): + temp = '' + treetop = [] + unfinished = [] + while start < len(expr): + ch = expr[start] + start += 1 + if ch.isalpha() or ch.isnumeric() or ch == '_': + temp += ch + elif is_operator(ch): + if temp != '': + queue.append(expr_tree_node(temp)) + node_ops = expr_tree_node(ch) + queue.append(node_ops) + temp = '' + elif ch == '(': + next_left_paren_count = left_paren_count + 1 + next_level_queue = [] + node_var, start = create_nodes(expr, start, next_left_paren_count, next_level_queue) + queue.append(node_var) + elif ch == ')': + if temp != '': + queue.append(expr_tree_node(temp)) + temp = '' + treetop = convert_queue_to_node(queue, [], []) + queue.append(treetop[0]) + treetop[0].priority = -left_paren_count + left_paren_count -= 1 + return treetop[0], start + elif ch != ' ': + print("INVALID CHARACTER DETECTED. ABORTED PROCESS.") + + if temp != '': + queue.append(expr_tree_node(temp)) + + treetop = convert_queue_to_node(queue, treetop, unfinished) + return treetop[0], start + + +def convert_queue_to_node(queue=[], local_treetop=[], unfinished=[]): + if len(queue) == 1 and len(unfinished) == 0: + return queue[:] + + if len(queue): + node_var = queue[0] + queue.pop(0) + else: + return local_treetop + + # if the queue is not empty yet, get another node as "node_ops" + if len(queue): + node_ops = queue[0] + queue.pop(0) + else: # reached the end of the queue + local_treetop, unfinished = _found_empty_operator(node_var, local_treetop, unfinished) + return local_treetop + + if len(local_treetop) == 0: + local_treetop, unfinished = _found_empty_treetop(node_var, node_ops, local_treetop, unfinished) + return convert_queue_to_node(queue, local_treetop, unfinished) + + local_treetop, unfinished = _connect_nodes(node_var, node_ops, local_treetop, unfinished) + + return convert_queue_to_node(queue, local_treetop, unfinished) + + + +def print_tree(treetop): + if treetop is None: + return + print(treetop.content) + print_tree(treetop.left) + print_tree(treetop.right) + return + + +##========================Binary Tree Building Ends================================ + + +def get_model_components(all_rows=[]): + """ Initialize lists for model components and their names.""" + + _num_comps = len([cell for cell in all_rows[0] if cell != '']) + _comps = [] + + for i in range(_num_comps): + _comps.append([all_rows[1][i], all_rows[2][i], all_rows[3][i]]) + + return _comps + + +def get_model_params(all_rows=[]): + """ Initialize the model parameters. """ + _params = {} + + # first row of actual parameter list + _frap = all_rows.index([_row for _row in all_rows if _row[0] == 'PARAMETERS'][0]) + 1 + for row in all_rows[_frap:]: + _params[row[0]] = row[1:5] + + return _params + + +def get_model_stoichs(all_rows=[], num_comps=13): + """ Initialize the stoichiometrics.""" + + starting_row_index = 4 + + # count the number of rate equations first + row_counter = 0 + while (not(all_rows[row_counter][0] == 'END_STOICH' or all_rows[row_counter][0] == 'END_EQTNS')): + # 'END_STOICH' is the same Stop sign as 'END_EQTNS' in the Peterson matrix format + row_counter += 1 + + num_eqs = row_counter - starting_row_index + + # _stoichs stores the stoichiometrics in the form of a 2-d array of text; + # the 1st index of _stoichs is the id of the model component, starting at 0 + # and the 2nd index of _stoichs is the id of the rate equation, starting at 0 + _stoichs = [] + + for comp_id in range(num_comps): + _stoichs.append([0]*num_eqs) + for eq_id in range(num_eqs): + _stoichs[comp_id][eq_id] = all_rows[starting_row_index + eq_id][comp_id] + + return _stoichs, num_eqs + + +def get_rate_equations(all_rows=[], num_comps=13, num_eqtns=8): + """ Extract the rate equation's individual terms """ + + # The definition of rate equations starting at Row 3 (Row Index = 2), one (1) column down from the last model + # component and its stoichiometric column: term type row and term unit row included + eqs_start_row_index = 2 + + # looks stupidly redundant, but for readability: convert to index + eqs_start_col_index = (num_comps - 1) + 1 + + # _rate_eqs stores the rate equations in the form of a 2-d array of text: + # the 1st row defines the types of the individual terms in the rate equations at the corresponding index; + # the 2nd row of _rate_eqs is the unit of the individual rate equation terms; + _rate_eqs = [] + + # all_rows[] indexing: added 2 due to the term type row and the term unit row + for row in all_rows[eqs_start_row_index:(eqs_start_row_index + 2 + num_eqtns)]: + _rate_eqs.append(row[eqs_start_col_index:]) + + return _rate_eqs + + +def create_model_class_init(model_name='User_Defined_Model', csv_file='template_asm1.csv', num_comps=13, num_eqs=8): + """ create the file that store the model components, stoichiometry, and parameters/constants """ + + #TODO: this is the key function of model writing, continue coding here + + from datetime import datetime + + _dt_stamp = datetime.now() + + _filename = model_name + '.py' + + _tab = ' ' * 4 + + with open(_filename, 'w') as asmfile: + asmwrite = asmfile.write + + asmwrite('# This file defines the model named ' + model_name + '\n') + asmwrite('#\n') + asmwrite('# Created Using: PooPyLab Model Builder\n') + asmwrite('# User Specified Model in: ' + csv_file + '\n') + asmwrite('# Created at: ' + _dt_stamp.strftime("%H:%M:%S %Y-%B-%d") + '\n') + asmwrite('#\n\n\n') + + #TODO: NEED TO HAVE THE MONOD TERMS PARSED FIRST + # Intermediate results of Monod or Inhibition Terms + # self._monods = [1.0] * 7 + #TODO_END + + asmfile.close() + + return None + + +if __name__ == '__main__': + + import csv + + csv_rows = [] + + with open("template_asm1.csv", 'r') as csvf: + r = csv.reader(csvf) + for row in r: + csv_rows.append(row) + csvf.close() + + print() + + model_comps = get_model_components(csv_rows) + num_comps = len(model_comps) + + print('Found', num_comps, 'Model Components:') + print(model_comps, '\n') + + model_params = get_model_params(csv_rows) + num_params = len(model_params) + print('Found', num_params, 'Model Parameters/Constants:') + print(model_params, '\n') + + model_stoichs, num_eqs = get_model_stoichs(csv_rows, num_comps) + print(model_stoichs, '\n') + + print('Found', num_eqs, 'Rate Equations:') + model_rate_eqs = get_rate_equations(csv_rows, num_comps, num_eqs) + print(model_rate_eqs, '\n') + + create_model_class_init('KZTest', 'template_asm1.csv', num_comps, num_eqs) diff --git a/PooPyLab/model_builder/tested_but_not_used/model_builder_pythonic/monod_parser_test.py b/PooPyLab/model_builder/tested_but_not_used/model_builder_pythonic/monod_parser_test.py new file mode 100755 index 0000000..8ddef5b --- /dev/null +++ b/PooPyLab/model_builder/tested_but_not_used/model_builder_pythonic/monod_parser_test.py @@ -0,0 +1,171 @@ +#!/usr/bin/python3 + +# =========Common Definitions Begin========================= +class expr_tree_node(): + def __init__(self, txt=''): + self.content = txt + self.parent = None + self.left = None + self.right = None + if self.content == '+' or self.content == '-': + self.priority = 2 + elif self.content == '*' or self.content == '/': + self.priority = 1 + else: + self.priority = 0 + return None + + +def is_operator(char=''): + """ check whether a character is '+', '-', '*', or '/' """ + return (char == '+' or char == '-' or char == '*' or char == '/') + + +def has_operator(term=[]): + """ check whether a term contain '+', '-', '*', or '/' """ + return ('+' in term or '-' in term or '*' in term or '/' in term) + + +def _found_empty_operator(node_var, treetop, unfinished): + u = unfinished.pop() + u.right = node_var + node_var.parent = u + return treetop, unfinished + + +def _found_empty_treetop(node_var, node_ops, treetop, unfinished): + treetop = [node_ops] + node_ops.left = node_var + node_var.parent = node_ops + unfinished = [node_ops] + return treetop, unfinished + + +def _connect_nodes(node_var, node_ops, treetop, unfinished): + if treetop[0].priority > node_ops.priority: + u = unfinished.pop() + if u.priority <= node_ops.priority: + u.right = node_var + node_var.parent = u + node_ops.parent = u.parent + node_ops.parent.right = node_ops + node_ops.left = u + unfinished = [node_ops] + else: + u.right = node_ops + node_ops.parent = u + node_ops.left = node_var + node_var.parent = node_ops + unfinished = [node_ops] + else: + node_ops.left = treetop[0] + treetop[0].parent = node_ops + u = unfinished.pop() + u.right = node_var + node_var.parent = u + treetop = [node_ops] + unfinished = [treetop[0]] + return treetop, unfinished + + +# ===========Common Definitions End========================= + + + +def build_tree(expr=' B * A / (Z* ( D+ C* F )/ K - (G+H)*V )'): + start = 0 + left_paren_count = 0 + queue = [] + print(expr) + return create_nodes(expr, start, left_paren_count, queue) + + +def create_nodes(expr='B*A/(Z*(D+C*F)/K-(G+H)*V)', start=0, left_paren_count=0, queue=[]): + temp = '' + treetop = [] + unfinished = [] + while start < len(expr): + ch = expr[start] + start += 1 + if ch.isalpha() or ch.isnumeric() or ch == '_': + temp += ch + elif is_operator(ch): + if temp != '': + queue.append(expr_tree_node(temp)) + node_ops = expr_tree_node(ch) + queue.append(node_ops) + temp = '' + elif ch == '(': + next_left_paren_count = left_paren_count + 1 + next_level_queue = [] + node_var, start = create_nodes(expr, start, next_left_paren_count, next_level_queue) + queue.append(node_var) + elif ch == ')': + if temp != '': + queue.append(expr_tree_node(temp)) + temp = '' + treetop = convert_queue_to_node(queue, [], []) + queue.append(treetop[0]) + treetop[0].priority = -left_paren_count + left_paren_count -= 1 + return treetop[0], start + elif ch != ' ': + print("INVALID CHARACTER DETECTED. ABORTED PROCESS.") + + if temp != '': + queue.append(expr_tree_node(temp)) + + treetop = convert_queue_to_node(queue, treetop, unfinished) + return treetop[0], start + + +def convert_queue_to_node(queue=[], local_treetop=[], unfinished=[]): + if len(queue) == 1 and len(unfinished) == 0: + return queue[:] + + if len(queue): + node_var = queue[0] + queue.pop(0) + else: + return local_treetop + + # if the queue is not empty yet, get another node as "node_ops" + if len(queue): + node_ops = queue[0] + queue.pop(0) + else: # reached the end of the queue + local_treetop, unfinished = _found_empty_operator(node_var, local_treetop, unfinished) + return local_treetop + + if len(local_treetop) == 0: + local_treetop, unfinished = _found_empty_treetop(node_var, node_ops, local_treetop, unfinished) + return convert_queue_to_node(queue, local_treetop, unfinished) + + local_treetop, unfinished = _connect_nodes(node_var, node_ops, local_treetop, unfinished) + + return convert_queue_to_node(queue, local_treetop, unfinished) + + + +def print_tree(treetop): + if treetop is None: + return + print(treetop.content) + print_tree(treetop.left) + print_tree(treetop.right) + return + + + +if __name__ == '__main__': + + monod = ' (( (((X_S)) /(X_BH+( S_O ))))) / (( K _ X +( (X_S ) / X_BH)) ) ' + #combo = ' ( S_O / ( K_OH + S_O ) + cf_h * K_OH / ( K_OH + S_O) * S_NO / (K_NO+S_NO))' + + #mytop = convert_expr_to_bin_subtree() + #mytop, _ = build_tree('(D+C*F)/K') + ##mytop, _ = build_tree() + #mytop, _ = build_tree(combo) + mytop, _ = build_tree(monod) + print("regen:") + print_tree(mytop) diff --git a/PooPyLab/model_builder/tested_but_not_used/model_builder_pythonic/splitter.ppm b/PooPyLab/model_builder/tested_but_not_used/model_builder_pythonic/splitter.ppm new file mode 100644 index 0000000..6bc87aa --- /dev/null +++ b/PooPyLab/model_builder/tested_but_not_used/model_builder_pythonic/splitter.ppm @@ -0,0 +1,42 @@ +[NAME_ID] +NAME_ID = Splitter_1 + +[CONFIGURATION] +# Whether I am a Waste Activated Sludge controller +WAS_CONTROLLER = False +TARGET_SRT = NA + +# Process Parameters: +ACTIVE_VOLUME = 0 +SIDE_WATER_DEPTH = 0 +BULK_DO = NA + +# Connections: +# Format: [ INLET | MAINOUTLET | SIDEOUTLET ] = [ unit_process_name_ID_branch | NONE ] +INLET = FinalClarifier_1_SO +MAINOUTLET = ASMReactor_1 +SIDEOUTLET = WAS_1 + +[FLOW_DATA_SOURCE] +# Possible Flow Data Source: [ UPS | DNS | PRG ], where +# UPS = Upstream, DNS = Downstream, PRG = Program (Runtime) + +# IN_FLOW = inlet flow +# MO_FLOW = main outlet flow +# SO_FLOW = side outlet flow + +INF_LOW = UPS +MO_FLOW = PRG +SO_FLOW = DNS + +[FLOW_BALANCE] +# The flow balance equation will be set accourding to the branch's flow data source +IN_FLOW - MO_FLOW - SO_FLOW = 0 + +[MASS_BALANCE] +# IN_COMP[i] = the i-th model component of the blended inlet +# MO_COMP[i] = the i-th model component of the main outlet +# SO_COMP[i] = the i-th model component of the side outlet +MO_COMP[i] = IN_COMP[i] +SO_COMP[i] = IN_COMP[i] + diff --git a/PooPyLab/model_builder/tested_but_not_used/model_builder_pythonic/template_asm1.csv b/PooPyLab/model_builder/tested_but_not_used/model_builder_pythonic/template_asm1.csv new file mode 100644 index 0000000..a1df786 --- /dev/null +++ b/PooPyLab/model_builder/tested_but_not_used/model_builder_pythonic/template_asm1.csv @@ -0,0 +1,35 @@ +0,1,2,3,4,5,6,7,8,9,10,11,12,,,,,,, +S_O,S_I,S_S,S_NH,S_NS,S_NO,S_ALK,X_I,X_S,X_BH,X_BA,X_D,X_NS,RATE_EQUATIONS,Rate_Equation_Description,Rate_Eq_Term_1,Rate_Eq_Term_2,Rate_Eq_Term_3,Rate_Eq_Term_4,Rate_Eq_Term_5 +Dissolved O2,Inert Soluble COD,Readily Biodegradable COD,Ammonia N,Dissolved Organic N,Nitrite/Nitrate N,Alkalinity,Inert Particulate COD,Slowly Biodegradable COD,Heterotrophic Biomass,Autotrophic Biomass,Biomass Debris due to Decay,Particulate Organic N,Rate_Equation_Id (_j_),Text,Generic_Type,Generic_Type,Monod_Type,Monod_Type,Monod_Type +mg/L as O2,mg/L as COD,mg/L as COD,mg/L as N,mg/L as N,mg/L as N,mmol/L as CaCO3,mg/L as COD,mg/L as COD,mg/L as COD,mg/L as COD,mg/L as COD,mg/L as N,NA,NA,1/d,mg/L,NA,NA,NA +( Y_H – 1 ) / Y_H,0,-1 / Y_H,-i_N_XB,0,0,-i_N_XB / 14,0,0,1,0,0,0,0,Growth of Aerobic Heterotrophs,u_max_H,X_BH,S_S / ( K_S + S_S ),S_O / ( K_OH + S_O ),1 +0,0,-1 / Y_H,-i_N_XB,0,( Y_H – 1 ) / ( 2.86 * Y_H ),( 1 – Y_H ) / ( 14 * 2.86 * Y_H ) - i_N_XB / 14,0,0,1,0,0,0,1,Growth of Anoxic Heterotrophs,u_max_H,cf_g * X_BH,S_S / ( K_S + S_S ),K_OH / ( K_OH + S_O ),S_NO / ( K_NO + S_NO ) +( Y_A – 4.57 ) / Y_A,0,0,-i_N_XB - 1 / Y_A,0,1 / Y_A,-i_N_XB / 14 - 1 / ( 7 * Y_A ),0,0,0,1,0,0,2,Growth of Aerobic Autotrophs,u_max_A,X_BA,S_NH / ( K_NH + S_NH ),S_O / ( K_OA + S_O ),1 +0,0,0,0,0,0,0,0,1 – f_D,-1,0,f_D,i_N_XB – f_D * i_N_XD,3,Decay of Heterotrophs,b_H,X_BH,1,1,1 +0,0,0,0,0,0,0,0,1 – f_D,0,-1,f_D,i_N_XB – f_D * i_N_XD,4,Decay of Autotrophs,b_A,X_BA,1,1,1 +0,0,0,1,-1,0,1 / 14,0,0,0,0,0,0,5,Ammonification of Dissolved Organic N,k_a * S_NS,X_BH,1,1,1 +0,0,1,0,0,0,0,0,-1,0,0,0,0,6,Hydrolysis of Particulate Substrate,k_h,X_BH,( X_S / X_BH ) / ( K_X + X_S / X_BH ),( S_O / ( K_OH + S_O ) + cf_h * K_OH / ( K_OH + S_O ) * S_NO / ( K_NO + S_NO ) ),1 +0,0,0,0,1,0,0,0,0,0,0,0,-1,7,Hydrolysis of Particulate Organic N,k_h,X_BH * X_NS / X_S,( X_S / X_BH ) / ( K_X + X_S / X_BH ),( S_O / ( K_OH + S_O ) + cf_h * K_OH / ( K_OH + S_O ) * S_NO / ( K_NO + S_NO ) ),1 +END_STOICH,END_STOICH,END_STOICH,END_STOICH,END_STOICH,END_STOICH,END_STOICH,END_STOICH,END_STOICH,END_STOICH,END_STOICH,END_STOICH,END_STOICH,END_EQTNS,END_EQTNS,END_EQTNS,END_EQTNS,END_EQTNS,END_EQTNS,END_EQTNS +,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,, +PARAMETERS,TYPICAL_VALUE_AT_20C,ARRHENIUS_THETA,PARAMETER_DESCRIPTION,UNIT,PARAMETER_ID,,,,,,,,,,,,,, +u_max_H,6,1.072,Ideal Specific Growth Rate of Heterotrophs,1/day,0,,,,,,,,,,,,,, +u_max_A,0.8,1.103,Ideal Specific Growth Rate of Autotrophs,1/day,1,,,,,,,,,,,,,, +K_S,20,1,Half Saturation Concentration for Organic Substrate,mg/L as COD,2,,,,,,,,,,,,,, +K_OH,0.2,1,Half Saturation Concentration for O2 for Heterotrophs,mg/L as O2,3,,,,,,,,,,,,,, +K_NH,1,1,Half Saturation Concentration for TKN,mg/L as N,4,,,,,,,,,,,,,, +K_OA,0.4,1,Half Saturation Concentration for O2 for Autotrophs (Nitrifiers),mg/L as O2,5,,,,,,,,,,,,,, +K_NO,0.5,1,Half Saturation Concentration for NOx-N,mg/L as N,6,,,,,,,,,,,,,, +K_X,0.03,1.116,Half Saturation Concentration for Hydrolysis,mg/L as COD,7,,,,,,,,,,,,,, +Y_H,0.67,1,True Yield for Heterotrophs,mgCOD / mgCOD,8,,,,,,,,,,,,,, +Y_A,0.24,1,True Yield for Autotrophs (Nitrifiers),mgCOD / mgN,9,,,,,,,,,,,,,, +b_H,0.62,1.12,Specific Decay Rate for Heterotrophs,1/day,10,,,,,,,,,,,,,, +b_A,0.096,1.12,Specific Decay Rate for Autotrophs (Nitrifiers),1/day,11,,,,,,,,,,,,,, +k_a,0.08,1.072,Specific Ammonification Rate,m3 COD / (g-day),12,,,,,,,,,,,,,, +k_h,3,1.116,Specific Hydrolysis Rate,gCOD/(gBiomass_COD-day),13,,,,,,,,,,,,,, +f_D,0.08,1,Fraction of Cell Debris in Decayed Biomass,NA,14,,,,,,,,,,,,,, +cf_g,0.8,1,Correction Factor for Heterotrophs Capable of Anoxic Growth,NA,15,,,,,,,,,,,,,, +cf_h,0.4,1,Correction Factor for Hydrolytic Activities,NA,16,,,,,,,,,,,,,, +i_N_XB,0.086,1,Fraction: N / Biomass_COD ,gN/gBiomass_COD,17,,,,,,,,,,,,,, +i_N_XD,0.06,1,Fraction: N / Debris_COD,gN/gDebris_COD,18,,,,,,,,,,,,,, diff --git a/PooPyLab/model_builder/tested_but_not_used/model_builder_pythonic/template_asm1_backup.csv b/PooPyLab/model_builder/tested_but_not_used/model_builder_pythonic/template_asm1_backup.csv new file mode 100644 index 0000000..294e294 --- /dev/null +++ b/PooPyLab/model_builder/tested_but_not_used/model_builder_pythonic/template_asm1_backup.csv @@ -0,0 +1,12 @@ +S_I,S_S,X_I,X_S,X_BH,X_BA,X_D,S_O,S_NO,S_NH,S_NS,X_NS,S_ALK,"RATE_EQUATION (r_j_, where j is Rate_Eq_ID starting at 0)",RATE_EQUATION_DESCRIPTION,RATE_EQUATION_ID +0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15 +0,-1 / Y_H,0,0,1,0,0,( Y_H – 1 ) / Y_H,0,-i_XB,0,0,-i_XB / 14,u_max_H * S_S / ( K_S + S_S ) * S_O / ( K_OH + S_O ) * X_BH,Growth of Aerobic Heterotrophs,0 +0,-1 / Y_H,0,0,1,0,0,0,( Y_H – 1 ) / ( 2.86 * Y_H ),-i_XB,0,0,( 1 – Y_H ) / ( 14 * 2.86 * Y_H ) - i_XB / 14,u_max_H * S_S / ( K_S + S_S ) * K_OH / ( K_OH + S_O) * S_NO / ( K_NO + S_NO ) * cf_g * X_BH,Growth of Anoxic Heterotrophs,1 +0,0,0,0,0,1,0,( Y_A – 4.57 ) / Y_A,1 / Y_A,-i_XB - 1 / Y_A,0,0,-i_XB / 14 - 1 / ( 7 * Y_A ),u_max_A * S_NH / ( K_NH + S_NH ) * S_O / ( K_OA + S_O ) * X_BA,Growth of Aerobic Autotrophs,2 +0,0,0,1 – f_D,-1,0,f_D,0,0,0,0,i_XB – f_D * i_XP,0,b_H * X_BH,Decay of Heterotrophs,3 +0,0,0,1 – f_D,0,-1,f_D,0,0,0,0,i_XB – f_D * i_XP,0,b_A * X_BA,Decay of Autotrophs,4 +0,0,0,0,0,0,0,0,0,1,-1,0,1 / 14,k_a * S_NS * X_BH,Ammonification of Dissolved Organic N,5 +0,1,0,-1,0,0,0,0,0,0,0,0,0,k_h * ( X_S / X_BH ) / ( K_X + X_S / X_BH ) * ( S_O / ( K_OH + S_O ) + cf_h * K_OH / ( K_OH + S_O ) * S_NO / ( K_NO + S_NO ) ) * X_BH,Hydrolysis of Particulate Substrate,6 +0,0,0,0,0,0,0,0,0,0,1,-1,0,k_h * ( X_S / X_BH ) / ( K_X + X_S / X_BH ) * ( S_O / ( K_OH + S_O ) + cf_h * K_OH / ( K_OH + S_O ) * S_NO / ( K_NO + S_NO ) ) * X_BH * X_NS / X_S,Hydrolysis of Particulate Organic N,7 +Inert Soluble COD,Readily Biodegradable COD,Inert Particulate COD,Slowly Biodegradable COD,Heterotrophic Biomass,Autotrophic Biomass,Biomass Debris due to Decay,Dissolved O2,Nitite/Nitrate N,Ammonia N,Dissolved Organic N,Particulate Organic N,Alkalinity,Rate_Eq_j,RATE_EQUATION_DESCRIPTION,(_j_) +mg/L as COD,mg/L as COD,mg/L as COD,mg/L as COD,mg/L as COD,mg/L as COD,mg/L as COD,mg/L as COD,mg/L as N,mg/L as N,mg/L as N,mg/L as N,mmol/L as CaCO3,mg/L-d,NA,NA diff --git a/PooPyLab/model_builder/tested_but_not_used/tested_for_no_parentheses b/PooPyLab/model_builder/tested_but_not_used/tested_for_no_parentheses new file mode 100644 index 0000000..5782ff7 --- /dev/null +++ b/PooPyLab/model_builder/tested_but_not_used/tested_for_no_parentheses @@ -0,0 +1,139 @@ +# BELOW HAS BEEN TESTED FOR EXPRESSIONS WITHOUT PARENTHESES +class expr_tree_node(): + def __init__(self, txt=''): + self.content = txt + self.parent = None + self.left = None + self.right = None + if self.content == '+' or self.content == '-': + self.priority = 2 + elif self.content == '*' or self.content == '/': + self.priority = 1 + #elif self.content == '(' or self.content == ')': + # self.priority = 1 + else: + self.priority = 0 + return None + + +def is_operator(char=''): + """ check whether a character is '+', '-', '*', or '/' """ + + return (char == '+' or char == '-' or char == '*' or char == '/') + + +def has_operator(term=[]): + """ check whether a term contain '+', '-', '*', or '/' """ + + return ('+' in term or '-' in term or '*' in term or '/' in term) + + + +def get_var_ops(expr='', start=0): + variable = '' + operator = '' + counter = 0 + for ch in expr[start:]: + counter += 1 + if ch.isalpha() or ch.isnumeric() or ch == '_': + variable += ch + elif is_operator(ch): + operator = ch + break + elif ch != ' ': + print("INVALID CHARACTER DETECTED. ABORTED PROCESS.") + return '', 0, '' + + return variable, counter, operator + + +def build_tree(expr='A-B+K-C*D+E/F*G', start=0, treetop=[], unfinished=[]): + if start >= len(expr): + return treetop[0] + + var, offset, operator = get_var_ops(expr, start) + + node_var = expr_tree_node(var) + node_ops = expr_tree_node(operator) + + if node_ops.content == '': + u = unfinished.pop() + u.right = node_var + node_var.parent = u + return treetop[0] + + new_start = start + offset + + if len(treetop) == 0: + treetop = [node_ops] + node_ops.left = node_var + node_var.parent = node_ops + unfinished = [node_ops] + return build_tree(expr, new_start, treetop, unfinished) + + if treetop[0].priority > node_ops.priority: + u = unfinished.pop() + if u.priority <= node_ops.priority: + u.right = node_var + node_var.parent = u + node_ops.parent = u.parent + node_ops.parent.right = node_ops + node_ops.left = u + unfinished = [node_ops] + else: + u.right = node_ops + node_ops.parent = u + node_ops.left = node_var + node_var.parent = node_ops + unfinished = [node_ops] + else: + node_ops.left = treetop[0] + treetop[0].parent = node_ops + u = unfinished.pop() + u.right = node_var + node_var.parent = u + treetop = [node_ops] + unfinished = [treetop[0]] + + return build_tree(expr, new_start, treetop, unfinished) + + +def convert_expr_to_bin_tree(expr=' A -B+K - C *D+E/F*G'): + start = 0 + treetop = [] + unfinished = [] + print(expr) + res = build_tree(expr, start, treetop, unfinished) + return res + + +def print_tree(treetop): + if treetop is None: + return + print(treetop.content) + print_tree(treetop.left) + print_tree(treetop.right) + return + + +if __name__ == '__main__': + + #from model_writer import parse_monod, find_num_denom + + #monod = ' (( (((X_S)) /(X_BH+( S_O ))))) / (( K _ X +( (X_S ) / X_BH)) ) ' + #combo = ' ( S_O / ( K_OH + S_O ) + cf_h * K_OH / ( K_OH + S_O) * S_NO / (K_NO+S_NO))' + #print('Monod term before parsing:', combo) + + #parsed = parse_monod(combo) + #print(parsed) + + #numerator, denominator = find_num_denom(parsed) + #print('Numerator =', numerator, '; Denominator =', denominator) + #print('Numerator is a sub-term in Denominator:', numerator in denominator) + + mytop = convert_expr_to_bin_tree() + print("regen:") + print_tree(mytop) + + #ABOVE HAS BEEN TESTED FOR EXPRESSIONS WITHOUT PARENTHESES""" + diff --git a/PooPyLab/unit_procs/__init__.py b/PooPyLab/unit_procs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/PooPyLab/unit_procs/base.py b/PooPyLab/unit_procs/base.py new file mode 100755 index 0000000..955799b --- /dev/null +++ b/PooPyLab/unit_procs/base.py @@ -0,0 +1,327 @@ +# This file is part of PooPyLab. +# +# PooPyLab is a simulation software for biological wastewater treatment processes using International Water Association +# Activated Sludge Models. +# +# Copyright (C) Kai Zhang +# +# PooPyLab is free software: you can redistribute it and/or modify it under the terms of the GNU General Public +# License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with PooPyLab. If not, see +# . +# +# + + + +""" Definitions of common interface for all PooPyLab objects. + +The documentations for the abstract interface here are also abstract. Please see more details in specific +implementations. +""" +## @namespace base +## @file base.py + + +from abc import ABCMeta, abstractmethod + + +class poopy_lab_obj(object): + """ + Conceptual definition of common interface. + """ + + __metaclass__ = ABCMeta + + @abstractmethod + def set_name(self, new_name): + pass + + + @abstractmethod + def get_name(self): + pass + + + @abstractmethod + def get_codename(self): + pass + + + @abstractmethod + def set_num_comps(self, nc): + pass + + + @abstractmethod + def get_num_comps(self): + pass + + + @abstractmethod + def set_flow_data_src(self, branch, flow_ds): + """ + Set the flow data source of the selected branch. + """ + pass + + + @abstractmethod + def get_flow_data_src(self): + """ + Get the flow data source tags of the unit. + """ + pass + + + @abstractmethod + def assign_initial_guess(self, init_guess_lst): + """ + Assign the intial guess to the unit before simulation. + """ + pass + + + @abstractmethod + def get_type(self): + """ + Return the type of the current object. + """ + pass + + + @abstractmethod + def has_sidestream(self): + """ + Check if the current unit has a sidestream discharge. + + Default = True, i.e. splitter always has a sidestream. + """ + pass + + + @abstractmethod + def add_upstream(self, discharger, branch): + """ + Add the discharger's branch to inlet. + """ + pass + + + @abstractmethod + def get_upstream(self): + pass + + + @abstractmethod + def inlet_connected(self): + """ + Return True if upstream is connected, False if not. + """ + pass + + + @abstractmethod + def remove_upstream(self, discharger): + """ + Remove an existing discharger from inlet. + """ + pass + + + @abstractmethod + def set_downstream_main(self, receiver): + """ + Define the main outlet by specifying the receiving process unit. + """ + pass + + + @abstractmethod + def main_outlet_connected(self): + """ + Return whether the main outlet of the unit is defined (connected). + """ + pass + + + @abstractmethod + def get_downstream_main(self): + """ + Return the process unit that is connected to the main outlet. + """ + pass + + + @abstractmethod + def set_downstream_side(self, receiver): + """ + Define the downstream side outlet's connection. + """ + pass + + + @abstractmethod + def side_outlet_connected(self): + """ + Return True if the main outlet is connected, False if not. + """ + pass + + + @abstractmethod + def get_downstream_side(self): + """ + Return the process unit connected to the side outlet. + """ + pass + + + @abstractmethod + def set_side_outlet_flow(self, soflow, info_type): + """ + Set the side outlet flow + """ + pass + + + @abstractmethod + def sidestream_flow_defined(self): + """ + Return whether the sidestream flow rate has been defined. + """ + pass + + + @abstractmethod + def get_side_outlet_concs(self): + """ + Return a copy of the sidestream outlet concentrations. + """ + pass + + + @abstractmethod + def get_TSS(self, branch='Main'): + """ + Return the Total Suspended Solids of the specified branch. + """ + pass + + + @abstractmethod + def get_VSS(self, branch='Main'): + """ + Return the Volatile Suspended Solids of the specified branch. + """ + pass + + + @abstractmethod + def get_COD(self, branch='Main'): + """ + Return the Chemical Oxygen Demand (total) of the specified branch. + """ + pass + + + @abstractmethod + def get_sCOD(self, branch='Main'): + """ + Return the soluble COD of the specified branch. + """ + pass + + + @abstractmethod + def get_pCOD(self, branch='Main'): + """ + Return the particultate COD of the specified branch. + """ + pass + + + @abstractmethod + def get_TN(self, branch='Main'): + """ + Return the total nitrogen of the specified branch. + """ + pass + + + @abstractmethod + def get_orgN(self, branch='Main'): + """ + Return the organic nitrogen of the specified branch. + """ + pass + + + @abstractmethod + def get_inorgN(self, branch='Main'): + """ + Return the inorganic nitrogen of the specified branch. + """ + pass + + + @abstractmethod + def get_pN(self, branch='Main'): + """ + Return the particulate nitrogen of the specified branch. + """ + pass + + + @abstractmethod + def get_sN(self, branch='Main'): + """ + Return the soluble nitrogen of the specified branch. + """ + pass + + + @abstractmethod + def update_proj_conditions(self, ww_temp, elevation, salinity): + """ + Set the project site conditions for the process unit. + """ + pass + + + @abstractmethod + def get_saturated_DO(self): + """ + Calculate the saturated DO concentration under the site conditions. + """ + #TODO: not sure why this needs to be here for polymorphism + pass + + + @abstractmethod + def get_config(self): + """ + Save the configuration of the unit to a file. + """ + pass + + + @abstractmethod + def set_model_file_path(self, newpath=""): + """ + Set the path of the model template file. + """ + pass + + + @abstractmethod + def get_model_file_path(self): + """ + Get the model template file's path. + """ + pass diff --git a/PooPyLab/unit_procs/bio.py b/PooPyLab/unit_procs/bio.py new file mode 100755 index 0000000..074de74 --- /dev/null +++ b/PooPyLab/unit_procs/bio.py @@ -0,0 +1,280 @@ +# This file is part of PooPyLab. +# +# PooPyLab is a simulation software for biological wastewater treatment processes using International Water Association +# Activated Sludge Models. +# +# Copyright (C) Kai Zhang +# +# PooPyLab is free software: you can redistribute it and/or modify it under the terms of the GNU General Public +# License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with PooPyLab. If not, see +# . +# +# +# This is the definition of the ASM1 model to be imported as part of the Reactor object +# +# + + +"""Defines classes for biological reactors used in an WWTP. + + 1) ASM Reactor (bioreactor using ASM models); + + 2) Aerobic Digester (#TODO: add); + + 3) ADM Reactor (bioreactor using Anaerobic Digestion Model) (#TODO: add) +""" +## @namespace bio +## @file bio.py + +from ..unit_procs.streams import pipe +from ..ASMModel.asm_1 import ASM_1 +#from ..ASMModel import constants + +#from scipy.integrate import solve_ivp + +# ---------------------------------------------------------------------------- + + +class asm_reactor(pipe): + """ + Bioreactor using ASM kinetics, derived as a "pipe" w/ active volume. + + Current design only uses ASM 1 model. Will need to add flexibility for using ASM 2d, ASM 3, and user revised + versions of them. + + The "asm_reactor" contain sludge mixed liquor of a certain kinetics described by the chosen model. + + The integration of the model is also done by the "asm_reactor". + """ + + __id = 0 + + def __init__(self, act_vol=38000, swd=3.5, ww_temp=20, DO=2, *args, **kw): + """ + Init w/ active volume, water depth, water temperature, & dissolved O2. + + Args: + act_vol: active process volume, m3 + swd: side water depth, m + ww_temp: wastewater temperature, degC + DO: dissolved oxygen, mg/L + args: (provision for other parameters for different models) + kw: (provision for other parameters) + + Return: + None + """ + + pipe.__init__(self) + self.__class__.__id += 1 + self._id = self.__class__.__id + self._type = 'ASMReactor' + self.__name__ = self._type + '_' + str(self._id) + self._codename = self.__name__ + + # active volume, m3 + self._active_vol = act_vol + # side water depth, m + self._swd = swd + # plan view section area, m2 + self._area = self._active_vol / self._swd + + # sludge mixed liquor contained in the reactor + self._sludge = ASM_1(ww_temp, DO) + + # storage of _sludge._dCdt for the current step + self._del_C_del_t = [0.0] * len(self._sludge._comps) + + self._in_comps = [0.0] * len(self._sludge._comps) + self._mo_comps = [0.0] * len(self._sludge._comps) + + # results of previous round + #self._prev_mo_comps = [0.0] * len(self._sludge._comps) + #self._prev_so_comps = self._prev_mo_comps + + self._upstream_set_mo_flow = True + + self._model_file_path = self.set_model_file_path() + + return None + + + # ADJUSTMENTS TO COMMON INTERFACE + # + + + + def assign_initial_guess(self, initial_guess): + """ + Assign the intial guess to the unit before simulation. + + This function is re-implemented for "asm_reactor" which contains the "sludge" whose kinetics are described by + the model. + + When passing the initial guess into an "asm_reactor", the reactor's inlet, mainstream outlet, and the "sludge" + in it all get the same list of model component concentrations. + + Args: + initial_guess: list of model components + + Return: + None + """ + self._sludge._comps = initial_guess[:] + self._mo_comps = initial_guess[:] # CSTR: outlet = mixed liquor + return None + + + def update_proj_conditions(self, ww_temp=20, elev=100, salinity=1.0): + """ + Update the site conditions for the process unit. + + Args: + ww_temp: water/wastewater temperature, degC + elev: site elevation above mean sea level, meter + salinity: salinity of w/ww, GRAM/L + + Return: + None + + See: + get_saturated_DO(). + """ + if ww_temp > 4 and ww_temp <= 40\ + and elev >= 0 and elev <= 3000\ + and salinity >= 0: + self._ww_temp = ww_temp + self._elev = elev + self._salinity = salinity + self._DO_sat_T = self.get_saturated_DO() + self.set_model_condition(self._ww_temp, self._sludge.get_bulk_DO()) + else: + print(self.__name__, ' ERROR IN NEW PROJECT CONDITIONS. NO UPDATES') + + return None + + + def get_config(self): + """ + Generate the config info of the unit to be saved to file. + + Args: + None + + Return: + a config dict for json + """ + + # All Units are METRIC + config = { + 'Codename': self._codename, + 'Name': self.__name__, + 'Type': self._type, + 'ID': str(self._id), + 'Num_Model_Components': str(self._num_comps), + 'Inlet_Arrayname': self._codename + '_in_comp', + 'Main_Outlet_Arrayname': self._codename + '_mo_comp', + 'Side_Outlet_Arrayname': self._codename + '_so_comp', + 'IN_Flow_Data_Source': str(self._in_flow_ds)[-3:], + 'MO_Flow_Data_Source': str(self._mo_flow_ds)[-3:], + 'SO_Flow_Data_Source': str(self._so_flow_ds)[-3:], + 'User_Defined_SO_Flow': self._so_flow, + 'Inlet_Codenames': ' '.join([k.get_codename() for k in self._inlet]) if self._inlet else 'None', + 'Main_Outlet_Codename': self._main_outlet.get_codename() if self._main_outlet else 'None', + 'Side_Outlet_Codename': self._side_outlet.get_codename() if self._side_outlet else 'None', + 'Is_SRT_Controller': 'True' if self._SRT_controller else 'False', + 'Active_Volume': str(self._active_vol), #unit: m3 + 'Side_Water_Depth': str(self._swd), #unit: m + 'Model_File_Path:': self.get_model_file_path() + } + + return config + # END OF ADJUSTMENTS TO COMMON INTERFACE + + + # FUNCTIONS UNIQUE TO THE ASM_REACTOR CLASS + # + # (INSERT CODE HERE) + # + + def set_active_vol(self, vol=380): + """ + Set the active process volume. + + Args: + vol: active volume to be used. (m3) + + Return: + None + """ + if vol > 0: + self._active_vol = vol + else: + print("ERROR:", self.__name__, "requires an active vol > 0 M3.") + return None + + + def get_active_vol(self): + """ + Return the active process volume. (m3) + """ + return self._active_vol + + + def set_model_condition(self, ww_temp, DO): + """ + Set the wastewater temperature and dissolved O2 for the model. + + This function updates the model conditions for the "sludge" the "asm_reactor" contains. + + Args: + ww_temp: wastewtaer temperature in degC; + DO: dissolved O2 concentration in mg/L. + + Return: + None + + See: + ASMModel.ASM_1.update(). + """ + if ww_temp > 4 and ww_temp <= 40 and DO >= 0: + self._sludge.update(ww_temp, DO) + else: + print("ERROR:", self.__name__, "given crazy temperature or DO.") + return None + + + def get_model_params(self): + """ + Return the kinetic parameters of the applied model. + + Return: + {param_name, param_val_adjusted} + + See: + ASMModel.ASM_1.get_params(). + """ + return self._sludge.get_params() + + + def get_model_stoichs(self): + """ + Return the stoichiometrics of the applied model. + + Return: + {id_of_stoich, val} + + See: + ASMModel.ASM_1.get_stoichs(). + """ + return self._sludge.get_stoichs() + + # + # END OF FUNCTIONS UNIQUE TO THE ASM_REACTOR CLASS + diff --git a/PooPyLab/unit_procs/physchem.py b/PooPyLab/unit_procs/physchem.py new file mode 100755 index 0000000..3becc1f --- /dev/null +++ b/PooPyLab/unit_procs/physchem.py @@ -0,0 +1,343 @@ +# This file is part of PooPyLab. +# +# PooPyLab is a simulation software for biological wastewater treatment processes using International Water Association +# Activated Sludge Models. +# +# Copyright (C) Kai Zhang +# +# PooPyLab is free software: you can redistribute it and/or modify it under the terms of the GNU General Public +# License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with PooPyLab. If not, see +# . +# +# +# This is the definition of the ASM1 model to be imported as part of the Reactor object +# +# + +"""Defines classes for physical/chemical treatment processes. + + 1) Final Clarifier; + + 2) Primary Clarifier (#TODO: add); + + 3) Dissolved Air Flotation (#TODO: add); + + 4) Media Filter (#TODO: add); + + 5) Membrane Filtration (#TODO: add); +""" +## @namespace physchem +## @file physchem.py + + +from ..unit_procs.streams import splitter + + +# ---------------------------------------------------------------------------- + + +class final_clarifier(splitter): + """ + A "splitter" w/ different particulate concentrations at inlet/outlets. + + In order to keep the PooPyLab package simple and focused on the biological processes, the final clarifier is + assumed to be an ideal one. No detail solids settling model is implemented, as least for now. + + However, surface overflow rate, solids loading rate, and HRT will be checked and warnings will be given to user if + they are out of normal ranges. Simulation will not proceed until all parameters are within proper ranges. + TODO: Add actual solids sedimentation model. + + By default, the mainstream and sidestream outlets are overflow and underflow, respectively, of the final + clarifier. + """ + + # ASM components + # The Components the ASM components IN THE REACTOR + # For ASM #1: + # + # self._comps[0]: S_DO as DO + # self._comps[1]: S_I + # self._comps[2]: S_S + # self._comps[3]: S_NH + # self._comps[4]: S_NS + # self._comps[5]: S_NO + # self._comps[6]: S_ALK + # self._comps[7]: X_I + # self._comps[8]: X_S + # self._comps[9]: X_BH + # self._comps[10]: X_BA + # self._comps[11]: X_D + # self._comps[12]: X_NS + + __id = 0 + + def __init__(self, active_vol=9500, SWD=3.5): + """ + Constructor for final_clarifier. + + The "final_clarifier" is modeled as an ideal solids-liquid separation process that capture the solids from the + inlet into the underflow as per the user given capture rate. + + Essentially, the "final_clarifier" is a "splitter" with different particulate concentrations among its three + branches, while those concentrations will be identical for all the branches of an ideal "splitter". + + Args: + active_vol: active clarifier volume excluding storage cone, m3; + SWD: side water depth, m. + + Return: + None + """ + splitter.__init__(self) + self.__class__.__id += 1 + self._id = self.__class__.__id + self._type = 'FinalClarifier' + self.__name__ = self._type +'_' + str(self._id) + self._codename = self.__name__ + + + ## clarifier active volume, bottom cone volume excluded, m3 + self._active_vol = active_vol + ## side water depth of the active volume, m + self._swd = SWD + ## plan section area, m2 + self._area = self._active_vol / self._swd + + self._upstream_set_mo_flow = True + + self._model_file_path = self.set_model_file_path() + + ## user defined solids capture rate, fraction less than 1.0; + self._capture_rate = 0.95 + + ## underflow solids, mg/L + self._under_TSS = 15000 + + return None + + + # ADJUSTMENTS TO COMMON INTERFACE TO FIT THE NEEDS OF FINAL_CLARIFIER + # + + + def set_as_SRT_controller(self, setting=False): + """ + Set the current splitter as an Solids Retention Time controller. + + This function is bypassed for "final_clarifier". + """ + print('ERROR:', self.__name__, "can't be set as SRT controller") + return None + + +## #NOTE: 2024-01-22 KZ, commented out since it is not needed in the equation based solving sys. +## def discharge(self, method_name='BDF', fix_DO=True, DO_sat_T=10): +## """ +## Pass the total flow and blended components to the downstreams. +## +## This function is re-implemented for "final_clarifier" because of the need to settle the solids (particulate) +## and concentrate them at the sidestream (underflow). The function first calls _branch_flow_helper() to set +## the flows for inlet, mainstream outlet, and sidestream outlet, then calls _settle_solids() to fractions the +## particulate components according to the branch flows and user set percent solids capture. +## +## Args: +## method_name: integration method as per scipy.integrate.solveivp; +## fix_DO: whether to simulate w/ a fix DO setpoint; +## DO_sat_T: saturated DO conc. under the site conditions (mg/L) +## (see note) +## +## Return: +## None +## +## Note: +## Argument method_name is not used as of now but will be applicable when a settling model is placed here in +## the final_clarifier class. +## +## Arguments of fix_DO and DO_sat_T are dummies for now because it is assumed that there is no biochemical +## reactions in the clarifier. +## +## See: +## _settle_solids(); +## set_capture_rate(); +## _branch_flow_helper(). +## """ +## # record last round's results before updating/discharging: +## self._prev_mo_comps = self._mo_comps[:] +## self._prev_so_comps = self._so_comps[:] +## +## self._branch_flow_helper() +## +## # for a clarifier, the main and side outlets have different solids +## # concentrations than the inlet's +## self._settle_solids() +## +## self._discharge_main_outlet() +## self._discharge_side_outlet() +## +## return None +## + + def get_config(self): + """ + Generate the config info of the unit to be saved to file. + + Args: + None + + Return: + a config dict for json + """ + + # All Units are METRIC + config = { + 'Codename': self._codename, + 'Name': self.__name__, + 'Type': self._type, + 'ID': str(self._id), + 'Num_Model_Components': str(self._num_comps), + 'Inlet_Arrayname': self._codename + '_in_comp', + 'Main_Outlet_Arrayname': self._codename + '_mo_comp', + 'Side_Outlet_Arrayname': self._codename + '_so_comp', + 'IN_Flow_Data_Source': str(self._in_flow_ds)[-3:], + 'MO_Flow_Data_Source': str(self._mo_flow_ds)[-3:], + 'SO_Flow_Data_Source': str(self._so_flow_ds)[-3:], + 'User_Defined_SO_Flow': self._so_flow, + 'Inlet_Codenames': ' '.join([k.get_codename() for k in self._inlet]) if self._inlet else 'None', + 'Main_Outlet_Codename': self._main_outlet.get_codename() if self._main_outlet else 'None', + 'Side_Outlet_Codename': self._side_outlet.get_codename() if self._side_outlet else 'None', + 'Is_SRT_Controller': 'True' if self._SRT_controller else 'False', + 'Active_Volume': str(self._active_vol), #unit: m3 + 'Side_Water_Depth': str(self._swd), #unit: m + 'Model_File_Path': self.get_model_file_path() + } + + return config + # + # END ADJUSTMENTS TO COMMON INTERFACE + + + # FUNCTIONS UNIQUE TO FINAL_CLARIFIER + # + # (INSERT CODE HERE) + + + def set_capture_rate(self, capture_rate=0.95): + """ + Set the percent solids capture for the final clarifier. + + This function is valid only when the "final_clarifier" is treated as a perfect solids-liquid separation + without actual modeling of the settling process (for simplicity purpose at this stage). + + Future update of PooPyLab will include settling model to determine how much solids can be captured based on + the configuration of the clarifier. + + Args: + capture_rate: fraction of total inlet solids captured (< 1.0). + + Return: + None + + See: + _settle_solids(). + """ + if 0 < capture_rate < 1: + self._capture_rate = capture_rate + else: + print('ERROR:', self.__name__, 'given unrealistic capture rate.') + return None + + + def _valid_under_TSS(self, uf_TSS): + """ + Check whether the underflow TSS is realistic. + + Args: + uf_TSS: current underflow TSS, mg/L. + + Return: + bool + + See: + update_combined_input(); + get_TSS(). + """ + self.update_combined_input() + _in_tss = self.get_TSS('Inlet') + return _in_tss <= uf_TSS <= 18000 + + + def _settle_solids(self, particulate_index=[7,8,9,10,11,12]): + """ + Split the incoming solids into the main- and sidestream outlets. + + Assumptions: + + 1) All particulate model components settle in the identical fashion in the clarifier. + + This function first calculate the updated inlet TSS concentration. + + Then based on the capture rate, split the inlet TSS to the mainstream and sidestream outlets, as if the + "final_clarifier" behaved exactly like a "splitter". + + The fractions of each particulate model components in inlet TSS is then calculated. These fractions is + then applied to the main- and sidestream outlet TSS to back calculate the corresponding particulate model + components for that branch. + + The soluble model components are identical for all three branches. + + Args: + particulate_index: list of index for particulate model components. + + Return: + None + + See: + get_TSS(); + _branch_flow_helper(); + totalize_inflow(); + update_combined_input(); + """ + + + #if not self._valid_under_TSS(self._under_TSS): + # print('WARN:', self.__name__, 'has unrealistic underflow TSS.') + # return None + + _in_tss = self.get_TSS('Inlet') + self._under_TSS = self._total_inflow * _in_tss * self._capture_rate / self._so_flow + + _of_tss = self._total_inflow * _in_tss * (1 - self._capture_rate) / self._mo_flow + + + # initiate _mo_comps and _so_comps so that all dissolved component (S_*) are identical among the three streams + self._mo_comps = self._in_comps[:] + self._so_comps = self._in_comps[:] + + # split the ASM model components associated with solids (X_*), assuming each component is split into the + # overflow and underflow keeping its fraction in clarifier inlet TSS. + for i in particulate_index: + if _in_tss > 0: + _frac = self._in_comps[i] / _in_tss + else: + _frac = 0 + self._mo_comps[i] = _of_tss * _frac + self._so_comps[i] = self._under_TSS * _frac + + # arbitarily adjust the DO according to the HRT of the final clarifier + + _HRT = self._active_vol / self._total_inflow + + if _HRT > 15/1440: # 15 min HRT + self._mo_comps[0] = 0.0 + self._so_comps[0] = 0.0 + + return None + + # + # END OF FUNCTIONS UNQIUE TO FINAL_CLARIFIER diff --git a/PooPyLab/unit_procs/streams.py b/PooPyLab/unit_procs/streams.py new file mode 100755 index 0000000..9c9b219 --- /dev/null +++ b/PooPyLab/unit_procs/streams.py @@ -0,0 +1,1646 @@ +# This file is part of PooPyLab. +# +# PooPyLab is a simulation software for biological wastewater treatment processes using International Water +# Association Activated Sludge Models. +# +# Copyright (C) Kai Zhang +# +# This program is free software: you can redistribute it and/or modify it under the terms of the GNU +# General Public License as published by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even +# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +# License for more details. +# +# You should have received a copy of the GNU General Public License along with this program. If not, see +# . +# +# +# This file defines the following classes related to streams: pipe, influent, effluent, splitter, WAS. +# +# ---------------------------------------------------------------------------- + + +"""Defines basic stream elements for a wastewater treatment plant (WWTP): + + 1) Splitter + 2) Pipe + 3) Influent + 4) Effluent + 5) Waste Activated Sludge (WAS) +""" +## @namespace streams +## @file streams.py + +import math +import numpy as np +from pathlib import PurePath + +from ..unit_procs.base import poopy_lab_obj +from ..utils.datatypes import flow_data_src +#from ..ASMModel import constants + +# ----------------------------------------------------------------------------- + + +class splitter(poopy_lab_obj): + """ + Stream element with an inlet, a mainstream outlet, and a sidestream outlet. + + There are three connection points for the flows to get in and out of a splitter: an inlet, a mainstream + outlet, and a sidestream outlet. + + General Functions: + + It is assumed that there is no significant biochemical reactions happening across a splitter unit. + It only maintains the flow balance around the three connections. Therefore, the model components (as + concentrations) are identical for all the connections after proper flow and load updates. + + Flow balance is maintained using the flow data source tags of two of the three connection points (see + below for more details). + + Special Functions: + + When specified as an SRT (solids retention time) controller, a splitter would have to be connected + with a WAS (waste activated sludge) unit at its sidestream. """ + + __id = 0 + + def __init__(self): + """ + Constructor for "splitter" + """ + ## type string of the process unit + self._type = "Splitter" + self.__class__.__id += 1 + self._id = self.__class__.__id + self.__name__ = self._type + '_' + str(self._id) + + ## code name is for use in system equation writing: + ## default as __name__, but will add user defined name to the front if that is given later + ## see set_name() + self._codename = self.__name__ + + ## a list of upstream units [OLD: and their flows in the format of {unit: Flow}] + self._inlet = [] + ## mainstream outlet, a single receiver + self._main_outlet = None + ## sidestream outlet, a single receiver + self._side_outlet = None + + ## flag on whether there are upstream units + self._inlet_connected = False + ## flag on whether there is a sidestream, always True for a splitter + self._has_sidestream = True + + ## flag on whether the mainstream outlet is connected + self._mo_connected = False + ## flag on whether the sidestream outlet is connected + self._so_connected = False + + ## whether the mainstream outflow is by inflow - sidestream outflow + self._upstream_set_mo_flow = False + + ## flow data source tag for inlet + self._in_flow_ds = flow_data_src.TBD + ## flow data source tag for mainstream outlet + self._mo_flow_ds = flow_data_src.TBD + ## flow data source tag for sidestream outlet + self._so_flow_ds = flow_data_src.TBD + + # sidestream flow definition, a str of number or file path is allowed + self._so_flow = '' + + ## flag to confirm it has received _so_flow definition + self._so_flow_defined = False + + # TODO: not sure why saturated DO estimate is here. + # site elevation, meter above MSL + self._elev = 100.0 + # water salinity, GRAM/L + self._salinity = 1.0 + # water temperature, degC + self._ww_temp = 20.0 + # saturated DO conc. under the side condition + self._DO_sat_T = self.get_saturated_DO() + + ## flag on whether this splitter is SRT controller + self._SRT_controller = False + + # inlet/main_outlet/side_oulet model components: + # _comps[0]: flow rate + # _comps[1]: S_DO as DO + # _comps[2]: S_I + # _comps[3]: S_S + # _comps[4]: S_NH + # _comps[5]: S_NS + # _comps[6]: S_NO + # _comps[7]: S_ALK + # _comps[8]: X_I + # _comps[9]: X_S + # _comps[10]: X_BH + # _comps[11]: X_BA + # _comps[12]: X_D + # _comps[13]: X_NS + # + # the number of components DOES include the flow rate of a branch + self._num_comps = 14 + ## TODO: With equation based solving system, these can probably simply be empty lists + ## inlet model components + self._in_comps = [] + ## mainstream outlet model components + self._mo_comps = [] + ## sidestream outlet model components + self._so_comps = [] + + # use the current dir as the starting path for the actual model template file + self._model_file_path = self.set_model_file_path() + + return None + + + # COMMON INTERFACES DEFINED IN POOPY_LAB_OBJ (BASE) + # + + def set_name(self, new_name='NoNewName'): + self.__name__ = new_name + self._codename = self.__name__ + '_' + self._type + '_' + str(self._id) + return None + + + def get_name(self): + return self.__name__ + + + def get_codename(self): + return self._codename + + + def set_num_comps(self, nc=14): + if nc>0 and type(nc)=='int': + self._num_comps = nc + else: + print('INVALID NUMBER OF COMPONENTS GIVEN. NO CHANGES MADE.') + return None + + def get_num_comps(self): + return self._num_comps + + + def set_flow_data_src(self, branch='Main', flow_ds=flow_data_src.TBD): + """ + Set the flow data source of the branch specified by the user. + + This function helps to decide how a stream process unit (splitter, pipe, influent, effluent, WAS, + etc.) performs flow balance calculations. + + For instance, if the user defines both the mainstream and sidestream outlet flow rates, then the + inlet flow rate will be calculated as the sum of the two branches. As another example, if the user + only defines the sidestream branch flow rate, then the mainstream branch flow will be determined as + (total_inflow - sidestream_outflow). + + When setting the flow data source of a branch, the function will check to see whether the flow data + sources for the other two brachnes can also be determined. + + Args: + branch: 'Main'|'Side'|'Inlet' + flow_ds: flow_data_source.TBD|.UPS|.DNS|.PRG + + Return: + None + + See: + _branch_flow_helper(), + set_mainstream_flow_by_upstream(), + totalize_inflow(), + utils.flow_data_src. + """ + + _in_flow_known = (self._in_flow_ds != flow_data_src.TBD) + + _so_flow_known = (self._so_flow_ds != flow_data_src.TBD) + + _mo_flow_known = (self._mo_flow_ds != flow_data_src.TBD) + + _chngd = False + + if branch == 'Main' and not _mo_flow_known: + self._mo_flow_ds = flow_ds + _chngd = True + elif branch == 'Side' and not _so_flow_known: + self._so_flow_ds = flow_ds + _chngd = True + elif branch == 'Inlet' and not _in_flow_known: + self._in_flow_ds = flow_ds + _chngd = True + #else: do nothing + + if not _chngd: + return None + + # auto evaluate the flow data source tags after setting one. + # first get an updated set of flags after the change: + _in_flow_known = (self._in_flow_ds != flow_data_src.TBD) + + _so_flow_known = (self._so_flow_ds != flow_data_src.TBD) + + _mo_flow_known = (self._mo_flow_ds != flow_data_src.TBD) + + _mo_flow_by_ext = (self._mo_flow_ds == flow_data_src.DNS or self._mo_flow_ds == flow_data_src.PRG) + + _so_flow_by_ext = (self._so_flow_ds == flow_data_src.DNS or self._so_flow_ds == flow_data_src.PRG) + + if _so_flow_known: + if _so_flow_by_ext: + if _mo_flow_by_ext: + self._upstream_set_mo_flow = False + if not _in_flow_known: + self._in_flow_ds = flow_data_src.DNS + else: + if _mo_flow_known: + # i.e. _upstream_set_mo_flow = True + # i.e. _mo_flow_ds = UPS + self._upstream_set_mo_flow = True + if not _in_flow_known: + self._in_flow_ds = flow_data_src.UPS + #else: + # pass + else: + if _in_flow_known: + self._upstream_set_mo_flow = True + self._mo_flow_ds = flow_data_src.UPS + #else: + # pass + else: + # i.e. _so_flow set by UPS + # i.e. _in_flow and _mo_flow must be set somewhere else + self._upstream_set_mo_flow = False + else: + if _in_flow_known and _mo_flow_known: + self._upstream_set_mo_flow = False + self._mo_flow_ds = flow_data_src.UPS + #else: + # pass + return None + + + def get_flow_data_src(self): + """ + Return the flow data source tags of all three branches. + """ + + return self._in_flow_ds, self._mo_flow_ds, self._so_flow_ds + + + def assign_initial_guess(self, init_guess_lst): + """ + Assign the intial guess to the unit before simulation. + + All three branches of a stream element will get the same model components. + + Args: + init_guess_lst: list of model components (concentrations) + + Return: + None + """ + + self._in_comps = init_guess_lst[:] + self._mo_comps = init_guess_lst[:] + self._so_comps = init_guess_lst[:] + return None + + + def get_type(self): + """ + Return the type string of the process unit. + + Args: + None + + Return: + str + """ + return self._type + + + def has_sidestream(self): + """ + Return whether the unit has a sidestream. + + Args: + None + + Return: + bool + """ + return self._has_sidestream + + + def add_upstream(self, discharger, upst_branch='Main'): + """ + Add the discharger's branch to inlet. + + This function add an upstream unit's (discharger's) outlet, as specified by the ups_branch parameter, + to the current inlet. + + The function first checks whether the specified discharger is already in self._inlet. If so, does + nothing. Otherwise, add the discharger into self._inlet and put 0.0 m3/d as a place holder for the + corresponding flow rate. + + An error message will display if upst_branch is neither 'Main' nor 'Side'. And the specified + discharger will NOT be added to self._inlet as a result. + + After adding the discharger into self._inlet. This function calls the discharger's + set_downstream_main()/set_downstream_side() to connect its mainstream/sidestream outlet to the + current unit's inlet. + + Upon sucessful addition of the specified discharger and its branch, the self._inlet_connected flag is + set to True. + + Args: + discharger: the process unit to be added to self._inlet; + upst_branch: branch of the discharger to get flow from. + + See: + set_downstream_main(), + set_downstream_side(), + remove_upstream(). + """ + + if discharger not in self._inlet: + self._inlet.append(discharger) + if upst_branch == 'Main': + self._inlet_connected = True + discharger.set_downstream_main(self) + elif upst_branch == 'Side': + self._inlet_connected = True + discharger.set_downstream_side(self) + else: + print("ERROR: UNKNOWN BRANCH SPECIFIED.") + + return None + + + def get_upstream(self): + """ + Return the _inlet {} of the unit. + + Args: + None + + Return: + set of units in the inlet + """ + return self._inlet + + + def inlet_connected(self): + """ + Return whether the unit's inlet has been connected. + + Args: + None + + Return: + bool + """ + return self._inlet_connected + + + def remove_upstream(self, discharger): + """ + Remove an existing discharger from inlet. + + This function first checks whether the specified discharger to be removed exists in self._inlet: + + If so, proceed and remove it from self._inlet. Then it finds out which branch of the discharger + is originally connected to current unit's inlet, inform the original discharger to update its + corresponding branch's connection. The _inlet_connected flag will be checked and updated when an + upstream discharger is removed successfully. + + If not, an error message will be displayed and nothing will be removed from the self._inlet. + + Args: + discharger: An inlet unit to be removed. + + Return: + None + + See: + add_upstream() + """ + + if discharger in self._inlet: + self._inlet.remove(discharger) + self._upstream_set_mo_flow = False + if discharger.get_downstream_main() == self: + discharger.set_downstream_main(None) + else: + discharger.set_downstream_side(None) + self._inlet_connected = len(self._inlet) > 0 + else: + print('ERROR:', self.__name__, 'inlet unit not found for removal.') + return None + + + def set_downstream_main(self, rcvr): + """ + Define the downstream main outlet by specifying the receiving process unit. + + An influent unit can not receive any flow from the current unit. An error message will be displayed + if the specified receiver is of the influent type. + + If the specified receiver is already connected to the current unit's mainstream outlet branch, + nothing will be done. + + Successful connection of the receiver and current unit's mainstream outlet will set the _mo_connected + flag to True. + + Args: + rcvr: A receiver of the current unit's mainstream outlet flow. + + Return: + None + + See: + add_upstream(), + set_downstream_side(). + """ + + if rcvr is None: + self._main_outlet = None + self._mo_connected = False + elif self._main_outlet != rcvr: + if not isinstance(rcvr, influent): + self._main_outlet = rcvr + self._mo_connected = True + rcvr.add_upstream(self) + else: + print('ERROR: Influent types CAN NOT be the main outlet of', + self.__name__) + return None + + + def main_outlet_connected(self): + """ + Return whether the mainstream outlet is connected. + + Args: + None + + Return: + bool + """ + return self._mo_connected + + + def get_downstream_main(self): + """ + Return the process unit connected at the mainstream outlet. + + Args: + None + + Return: + poopy_lab_obj + """ + return self._main_outlet + + + def set_downstream_side(self, rcvr): + """ + Define the downstream side outlet's connection. + + An influent unit can not receive any flow from the current unit. An error message will be displayed + if the specified receiver is of the influent type. + + If the specified receiver is already connected to the current unit's sidestream outlet branch, + nothing will be done. + + Successful connection of the receiver and current unit's sidestream outlet will set the _so_connected + flag to True. + + Args: + rcvr: receiver of the current unit's sidestream outlet flow. + + + See: + add_upstream(), + set_downstream_main(). + """ + + if rcvr is None: + self._side_outlet = None + self._so_connected = False + elif self._side_outlet != rcvr: + if not isinstance(rcvr, influent): + self._side_outlet = rcvr + self._so_connected = True + rcvr.add_upstream(self, 'Side') + else: + print("ERROR: Influent types CAN NOT be the side outlet of", self.__name__) + return None + + + def side_outlet_connected(self): + """ + Return True if the main outlet is connected, False if not. + + Args: + None + + Return: + bool + """ + return self._so_connected + + + def get_downstream_side(self): + """ + Return the process unit connected to the side outlet. + + Args: + None + + Return: + poopy_lab_obj + """ + return self._side_outlet + + + def set_side_outlet_flow(self, soflow, info_type): + """ + Set the side outlet flow with a NUMber string or CSV file path + + Args: + soflow: a str of float or path to a .csv file + info_type: 'NUM' or 'CSV' + + Return: + None + """ + self._so_flow_defined = True #initial assumption + + if info_type == 'NUM': + try: + float(soflow) + self._so_flow = soflow + except ValueError: + self._so_flow_defined = False + print(self._codename, ': Error in side outlet flow value. Side outlet flow undefined.') + elif info_type == 'CSV': + #TODO: insert code to handle file input (e.g. for dynamic simulation) + pass + + return None + + + def sidestream_flow_defined(self): + """ + Return whether the sidestream flow rate has been defined. + + Args: + None + + Return: + bool + """ + return self._so_flow_defined + + + def get_side_outlet_concs(self): + """ + Return a copy of the sidestream outlet concentrations. + + Args: + None + + Return: + list + """ + return self._so_comps[:] + + + def get_TSS(self, br='Main'): + """ + Return the Total Suspended Solids of the specified branch. + """ + #TODO: need to make COD/TSS = 1.2 changeable for different type of + # sludge + index_list = [7, 8, 9, 10, 11] + return self._sum_helper(br, index_list) / 1.2 + + + def get_VSS(self, br='Main'): + """ + Return the Volatile Suspended Solids of the specified branch. + """ + #TODO: need to make COD/VSS = 1.42 changeable for diff. type of sludge + index_list = [7, 8, 9, 10, 11] + return self._sum_helper(br, index_list) / 1.42 + + + def get_COD(self, br='Main'): + """ + Return the Chemical Oxygen Demand (total) of the specified branch. + """ + index_list = [1, 2, 7, 8, 9, 10, 11] + return self._sum_helper(br, index_list) + + + def get_sCOD(self, br='Main'): + """ + Return the soluble COD of the specified branch. + """ + index_list = [1, 2] + return self._sum_helper(br, index_list) + + + def get_pCOD(self, br='Main'): + """ + Return the particultate COD of the specified branch. + """ + return self.get_COD(br) - self.get_sCOD(br) + + + def get_TN(self, br='Main'): + """ + Return the total nitrogen of the specified branch. + + TN = TKN + NOx_N + """ + index_list = [3, 4, 5, 12] + return self._sum_helper(br, index_list) + + + def get_orgN(self, br='Main'): + """ + Return the organic nitrogen of the specified branch. + """ + index_list = [4, 12] + return self._sum_helper(br, index_list) + + + def get_inorgN(self, br='Main'): + """ + Return the inorganic nitrogen of the specified branch. + """ + return self.get_TN(br) - self.get_orgN(br) + + + def get_pN(self, br='Main'): + """ + Return the particulate nitrogen of the specified branch. + """ + return self._sum_helper(br, [12]) + + + def get_sN(self, br='Main'): + """ + Return the soluble nitrogen of the specified branch. + """ + return self.get_TN(br) - self.get_pN(br) + + + def update_proj_conditions(self, ww_temp=20, elev=100, salinity=1.0): + """ + Update the site conditions for the process unit. + + Args: + ww_temp: water/wastewater temperature, degC + elev: site elevation above mean sea level, meter + salinity: salinity of w/ww, GRAM/L + + Return: + None + + See: + get_saturated_DO(). + """ + if ww_temp > 4 and ww_temp <= 40\ + and elev >= 0 and elev <= 3000\ + and salinity >= 0: + self._ww_temp = ww_temp + self._elev = elev + self._salinity = salinity + self._DO_sat_T = self.get_saturated_DO() + else: + print(self.__name__, ' ERROR IN NEW PROJECT CONDITIONS. NO UPDATES') + + return None + + + def get_saturated_DO(self): + """ + Calculate the saturated DO conc. based on current project conditions. + + Current project conditions are defined in self._ww_temp, self._elev, and self._salinity + + Reference: + USCIS Office of Water Quality Technical Memorandum 2011.03 Change to Solubility Equations + for Oxygen in Water + + Args: + None + + Return: + O2 Solubility (saturation conc.), mg/L + """ + + T = self._ww_temp + 273.15 # convert degC to degK + + # first, estimate the DO_sat_0 at 0 salinity and 1 atm, which is done by + # using the Benson-Krause equations: + DO_sat_0 = math.exp(- 139.34411 + 1.575701E5 / T + - 6.642308E7 / T**2 + 1.2438E10 / T**3 + - 8.621949E11 / T**4) + + # second, estimate the salinity factor, Fs + Fs = math.exp(-self._salinity * (0.017674 - 10.754/T + 2140.7/T**2)) + + # third, estimate the pressure factor, Fp + Patm_in_bar = 1.01325 * (1 - 2.25577E-5 * self._elev)**5.25588 + + theta_0 = 0.000975 - 1.426E-5 * self._ww_temp + 6.436E-8 * self._ww_temp**2 + + vapor_pres_water = math.exp(11.8571 - 3840.7 / T - 216961.0 / T**2) + + Fp = (Patm_in_bar - vapor_pres_water) * (1.0 - theta_0 * Patm_in_bar)\ + / (1.0 - vapor_pres_water) / (1.0 - theta_0) + + # finally, calculate the DO_sat for the site conditions + DO_sat_T = DO_sat_0 * Fs * Fp + + return DO_sat_T + + + def get_config(self): + """ + Generate the config info of the unit to be saved to file. + + Args: + None + + Return: + a config dict for json + """ + + config = { + 'Codename': self._codename, + 'Name': self.__name__, + 'Type': self._type, + 'ID': str(self._id), + 'Num_Model_Components': str(self._num_comps), + 'Inlet_Arrayname': self._codename + '_in_comp', + 'Main_Outlet_Arrayname': self._codename + '_mo_comp', + 'Side_Outlet_Arrayname': self._codename + '_so_comp', + 'IN_Flow_Data_Source': str(self._in_flow_ds)[-3:], + 'MO_Flow_Data_Source': str(self._mo_flow_ds)[-3:], + 'SO_Flow_Data_Source': str(self._so_flow_ds)[-3:], + 'User_Defined_SO_Flow': self._so_flow, + 'Inlet_Codenames': ' '.join([k.get_codename() for k in self._inlet]) if self._inlet else 'None', + 'Main_Outlet_Codename': self._main_outlet.get_codename() if self._main_outlet else 'None', + 'Side_Outlet_Codename': self._side_outlet.get_codename() if self._side_outlet else 'None', + 'Is_SRT_Controller': 'True' if self._SRT_controller else 'False', + 'Model_File_Path': self.get_model_file_path() + } + return config + + + def set_model_file_path(self, newpath=None): + #TODO: need to add validity check for the new path given + _model_file_path = PurePath(__file__) + if newpath != None: + self._model_file_path = newpath + else: + dir = _model_file_path.parents[1] + default_model_filename = self._type.lower() + '.pmt' + _model_file_path = dir / 'ASMModel' / default_model_filename + return _model_file_path + + + def get_model_file_path(self): + return str(self._model_file_path) + + # END OF COMMON INTERFACE DEFINITIONS + + + # FUNCTIONS UNIQUE TO SPLITTER + # + + def set_as_SRT_controller(self, setting=False): + """ + Set the current splitter as an Solids Retention Time controller. + + There are specific rules for connected an SRT controlling splitter. Once set, the splitter shall have + a sidestream connected to a pipe followed by a WAS (waste activated sludge) unit. + + Once set True, the sidestream flow will be determined during simulation by the downstream WAS unit. + + Args: + setting: True/False + + Return: + None + + See: + utils.pfd.check(); + utils.pfd._check_connection(). + """ + + self._SRT_controller = setting + #TODO: add notes here + self._so_flow_defined = setting + #TODO: HOW DOES THIS IMPACT WAS FLOW BASED ON USER SPECIFIED SRT? + return None + + + def is_SRT_controller(self): + """ + Return whether a splitter is an SRT controller. + """ + return self._SRT_controller + + + def _sum_helper(self, branch='Main', index_list=[]): + """ + Sum up the model components indicated by the index_list. + + Args: + branch: {'Inlet'|'Main'|'Side'} + index_list: a list of indices for the model components to use + + Return: + float + """ + + _sum = 0.0 + if branch == 'Main': + _sum = sum(self._mo_comps[i] for i in index_list) + elif branch == 'Inlet': + _sum = sum(self._in_comps[i] for i in index_list) + elif branch == 'Side' and self.has_sidestream(): + _sum = sum(self._so_comps[i] for i in index_list) + return _sum + # + # END OF FUNCTIONS UNIQUE TO SPLITTER + + +# ---------------------------------------------------------------------------- + +class pipe(splitter): + """ + A derived "splitter" class with a blinded sidestream. + + No biochemical reactions are modelled in the pipe class. A pipe is only used to connect process units. + + A pipe can have multiple upstream dischargers but only one downstream (main) receiver. + """ + + __id = 0 + + def __init__(self): + """ + A few special steps are taken when initializing a "pipe": + + 1) Its sidestream is connected to None, with the sidestream flow is fixed at 0 m3/d, and the + _so_flow_defined set to True; + + 2) Its _has_sidestream flag is fixed to False; + + 3) Its sidestream flow data source (_so_flow_ds) is set to flow_data_src.PRG. + """ + + splitter.__init__(self) + + self.__class__.__id += 1 + + ## type string of the process unit + self._type = 'Pipe' + + self._id = self.__class__.__id + + self.__name__ = self._type + '_' + str(self._id) + + self._codename = self.__name__ + + # pipe has no sidestream + self._has_sidestream = False + + # flow data source tags + self._in_flow_ds = flow_data_src.TBD + self._mo_flow_ds = flow_data_src.TBD + self._so_flow_ds = flow_data_src.PRG + + # a pipe's sidestream flow IS DEFINED as ZERO + self._so_flow_defined = True + + self._SRT_controller = False + + # inlet and main outlet components are identical for a pipe + # make main outlet components an alias of the inlet components + self._mo_comps = self._in_comps + # side outlet components equal to the inlet, if they existed. + # make side outlet components an alias of the inlet components + self._so_comps = self._in_comps + + self._model_file_path = self.set_model_file_path() + + return None + + # ADJUSTMENTS TO COMMON INTERFACE TO FIT THE NEEDS OF PIPE: + # +## def _branch_flow_helper(self): +## """ +## Calculate 1 of the 3 branches' flow based on the other 2. +## +## For a "pipe", the sidestream flow is set to 0 m3/d. The mainstream outlet flow always equals to the +## total inlet flow. +## """ +## if self._upstream_set_mo_flow: +## self._mo_flow = self._total_inflow +## else: +## self._total_inflow = self._mo_flow +## return None +## + + def set_downstream_side(self, receiver): + """ + Define the downstream side outlet's connection. + + A "pipe" has no sidestream (set to "None"). This function is essentially by-passed with a warning + message if called. + """ + print("ERROR:", self.__name__, "has no sidestream.") + return None + + +## def set_sidestream_flow(self, flow): +## """ +## Define the flow rate for the sidestream. +## +## This function is bypassed for a "pipe" whose sidestream is set to "None" and sidestream flow 0 m3/d. +## A warning message is displayed if called. +## """ +## print("WARN:", self.__name__, "has sidestream flow of ZERO.") +## return None +## + # + # END OF ADJUSTMENT TO COMMON INTERFACE + + + # FUNCTIONS UNIQUE TO PIPE GO HERE: + # + # (INSERT CODE HERE) + # + # END OF FUNCTIONS UNIQUE TO PIPE + + +# ----------------------------------------------------------------------------- + +class influent(pipe): + """ + A derived "pipe" class with its inlet being "None". + """ + + __id = 0 + + def __init__(self): + """ + Special initialization steps for "influent". + + Reasons: + 1) has no further inlet; + 2) has no sidestream outlet; + 3) has no further upstream; + 4) convergence is irrelevant here; + 5) is the only source of flow/load to the WWTP. + """ + + pipe.__init__(self) + + self.__class__.__id += 1 + self._id = self.__class__.__id + self._type = 'Influent' + self.__name__ = self._type + '_' + str(self._id) + ## code name is for use in system equation writing: + self._codename = self.__name__ + + # influent has no further upstream discharger + self._inlet = None + + # Trick the system by setting True to _inlet_connected flag + self._inlet_connected = True + # influent has no sidestream + self._has_sidestream = False + + # flow data source tags + self._in_flow_ds = flow_data_src.UPS + self._mo_flow_ds = flow_data_src.UPS + self._so_flow_ds = flow_data_src.PRG + + self._upstream_set_mo_flow = True + + # an influent is always "converged" within the time frame of interest + self._converged = True + + self._model_file_path = self.set_model_file_path() + + # Influent characteristics from user measurements/inputs + # Setting default values for municipal wastewater in USA + # Default Unit: mg/L except where noted differently + self._BOD5 = 250.0 + self._TSS = 250.0 + self._VSS = 200.0 + self._TKN = 40.0 + self._NH3N = 28.0 + self._NOxN = 0.0 + self._TP = 10.0 + self._Alk = 6.0 # in mmol/L as CaCO3 + self._DO = 0.0 + + + # Fractionations of the influent. The fractions stored are for the raw + # influent wastewater without any active biomass. + # If influent with active biomass (X_BH, X_BA, etc.) is needed, + # a dedicated Bio-Augmentation unit should be used with only model + # components related to the biomass. + self._model_fracs = { + 'ASM1': + { + 'COD:BOD5': 2.04, + 'SCOD:COD': 0.50, #SCOD+PCOD = COD + 'RBCOD:SCOD': 0.80, #RBCOD + UBSCOD = SCOD + 'SBCOD:PCOD': 0.70, #SBCOD + UBPCOD = PCOD + 'SON:SCOD': 0.01, #Sol.Org.N as a frac of SCOD + 'RBON:SON': 0.8, #RBON + UBSON = SON + 'SBON:PON': 0.75 #SBON + UBPON = PON + }, + 'ASM2d': # TODO: as a starting point, let's just follow the IWA model for fractions + { + 'SA:COD': 0.05, # Acetate as COD + 'SF:COD': 0.20, # Fermentable COD, Acetate excluded + 'SC:COD': 0.15, # Biodegradable Complex Soluble COD + 'SI:COD': 0.05, # Nonbiodegradable Soluble + 'XS:COD': 0.30, # Biodegradable Particulate + 'XI:COD': 0.05, # NonBiodegradable Particulate + 'XOHO:COD': 0.001, # Ordinary Heterotrophs + 'XAUT:COD': 0.001, # Autotrophs (nitrifiers) + 'XPAO:COD': 0.001, # PAOs + 'XOHO:VSS': 1.42, # COD:VSS ratio for OHO + 'XAUT:VSS': 1.42, # COD:VSS ratio for AUT + 'XPAO:VSS': 1.42, # COD:VSS ratio for PAO + 'XI:VSS': 1.20, # COD:VSS ratio for XI + 'XS:VSS': 1.20 # COD:VSS ratio for XS + }, + 'ASM3': {} # TODO: define for ASM3 + } + + + + + # Plant influent flow in M3/DAY + # TODO: will be user-input from GUI. FROM THE GUI, USER + # will use MGD. DEFAULT VALUE = 10 MGD. + self._design_flow = 37800 + + return None + + # ADJUSTMENTS TO THE COMMON INTERFACE TO FIT THE NEEDS OF INFLUENT + # + + def assign_initial_guess(self, init_guess_lst): + """ + Assign the intial guess to the unit before simulation. + + There is no need for assigning any initial guesses to an "influent" unit. This function is by-passed + for "influent". + """ + pass + + + def add_upstream(self, discharger, branch): + """ + Add the discharger's branch to inlet. + + The "influent" has no further upstream within the context of simulation. This function is by-passed + with an ERROR message displayed. + """ + print("ERROR:", self.__name__, "has NO upstream.") + return None + + + def remove_upstream(self, discharger): + """ + Remove an existing discharger from inlet. + + This function is bypassed for the "influent" who has no further upstream. An error message displays + when called. + """ + print("ERROR:", self.__name__, "has no upstream") + return None + + +## def set_mainstream_flow(self, flow=37800): +## """ +## Define the mainstream outlet flow. +## +## This function is re-implemented for the "influent" and essentially becomes a wrapper for setting the +## design flow (m3/d). +## +## Args: +## flow: design flow of the influent, m3/d +## +## Return: +## None +## """ +## if flow > 0: +## self._design_flow = flow +## else: +## print("ERROR:", self.__name__, "shall have design flow > 0 M3/d." +## "Design flow NOT CHANGED due to error in user input.") +## return None + + + def set_mainstream_flow_by_upstream(self, f): + """ + Set whether the mainstream flow = (total inflow - side outflow). + + This function is essentially bypassed for the "influent" since the design flow (influent mainstream + outflow) is directly set by the user. + """ + pass + + # + # END OF ADJUSTMENT TO COMMON INTERFACE + + + # FUNCTIONS UNIQUE TO INFLUENT + # + # (INSERT CODE HERE) + # + + def set_constituents(self, asm_ver='ASM1', inf_concs=[]): + """ + Set the conventional influent constituents. + + Usually called at top level when the entire inf_concs is defined and validated. + + Args: + asm_ver: version of ASM: ASM1 | ASM2d | ASM3 + inf_concs: list of influent constituents (see Note) + + Return: + None + + Note: + There are 9 elements in inf_concs for ASM1: + inf_concs[0] : self._BOD5 + inf_concs[1] : self._TSS + inf_concs[2] : self._VSS + inf_concs[3] : self._TKN + inf_concs[4] : self._NH3N + inf_concs[5] : self._NOxN + inf_concs[6] : self._TP + inf_concs[7] : self._Alk + inf_concs[8] : self._DO + """ + + if asm_ver == 'ASM1' and len(inf_concs) == 9: + self._BOD5 = inf_concs[0] + self._TSS = inf_concs[1] + self._VSS = inf_concs[2] + self._TKN = inf_concs[3] + self._NH3N = inf_concs[4] + self._NOxN = inf_concs[5] + self._TP = inf_concs[6] + self._Alk = inf_concs[7] + self._DO = inf_concs[8] + + return None + + + def set_fractions(self, asm_ver, frac_name, frac_val): + """ + Set fractions to convert wastewater constituents into model components + + Args: + asm_ver: ASM version: 'ASM1' | 'ASM2d' | 'ASM3' + frac_name: name of the fractionation, ASM version dependent + frac_val: new value of the fraction specified by 'frac_name' + + Return: + self._model_fracs.copy() + """ + + if asm_ver in self._model_fracs.keys()\ + and frac_name in self._model_fracs[asm_ver]: + if (frac_name == 'COD:BOD5' and frac_val > 1.0)\ + or (frac_name != 'COD:BOD5' and 0 <= frac_val <= 1.0): + self._model_fracs[asm_ver][frac_name] = frac_val + else: + print('ERROR in new fraction value: FRACTIONS NOT UPDATED,' + 'DEFAULT FRACTIONS USED.') + + with open(self._model_file_path, 'w') as fp: + lines = fp.readlines() + newlines = [] + for oldline in lines: + newlines.append(oldline) + if 'FRACTIONS' in oldline: + fracs = self._model_fracs[asm_ver] + for key in fracs: + newlines.append(str(fracs[key])) + fp.writelines(newlines) + #TODO: test the above + + return self._model_fracs.copy() + + + def _convert_to_model_comps(self, asm_ver='ASM1', verbose=False): + """ + Fractions the wastewater constituents into model components. + + Wastewater constituents often are measured in units such as Biochemical Oxygen Demand (BOD), Total + Suspended Solids (TSS), Volatile SS (VSS), etc. Many mathematical models including IWA ASMs, however, + measured organic carbons in Chemical Oxygen Demand (COD). This applies to soluble and particulate + constituents. As a result, conversions between the two are needed. + + Currently this fuction is mainly set up to convert municipal wastewater's constituents into IWA ASM1. + Industrial wastewater can have very different conversions coefficients. Also, the fractions will need + to be revised for models different from IWA ASM1. + + Args: + asm_ver: ASM Version: 'ASM1' | 'ASM2d' | 'ASM3' + verbose: bool for whether to print the influent model components + + Return: + list of model components for the influent characteristics user + defined. + """ + + + _temp_comps = self._in_comps[:] + + if asm_ver == 'ASM1': + # total COD + _TCOD = self._model_fracs['ASM1']['COD:BOD5'] * self._BOD5 + # soluble COD + _SCOD = self._model_fracs['ASM1']['SCOD:COD'] * _TCOD + # particulate COD + _PCOD = _TCOD - _SCOD + # readily biodegradable COD (biodeg. soluble COD) + _RBCOD = self._model_fracs['ASM1']['RBCOD:SCOD'] * _SCOD + # nonbiodegradable soluble COD + _NBSCOD = _SCOD - _RBCOD + # slowly biodegradable COD (biodeg. particulate COD) + _SBCOD = self._model_fracs['ASM1']['SBCOD:PCOD'] * _PCOD + # nonbiodegradable particulate COD + _NBPCOD = _PCOD - _SBCOD + # total organic N + _TON = self._TKN - self._NH3N + # soluble organic N + _SON = self._model_fracs['ASM1']['SON:SCOD'] * _SCOD + # particulate organic N + _PON = _TON - _SON + # readily biodegradable organic N (biodeg.sol.org N) + # assuming RBON:SON = RBCOD:SCOD + _RBON = self._model_fracs['ASM1']['RBCOD:SCOD'] * _SON + # nonbiodeg. sol. org. N + _UBSON = _SON - _RBON + # slowly biodegradable organic N (biodeg.part.org. N) + # assuming SBON:PON = SBCOD:PCOD + _SBON = self._model_fracs['ASM1']['SBCOD:PCOD'] * _PON + # nonbiodeg. part. org. N + _UBPON = _PON - _SBON + + if verbose: + print("Model = ASM1, Influent Fractions Summary::") + print("Total COD = {} Soluble COD = {}".format(_TCOD, _SCOD), + " Particulate COD =", _PCOD) + print("Readily Biodegradable (biodeg. sol.) COD =", _RBCOD, + " Non-Biodegradable Sol. COD =", _NBSCOD) + print("Slowly Biodegradable (biodeg. part.) COD =", _SBCOD, + " Non-Biodegradable Part. COD =", _NBPCOD) + print("Total TKN = {} NH3-N = {} Total Org.N = {}".format(self._TKN, self._NH3N, _TON)) + print("Soluble Org. N = {} Part. Org. N = {}".format(_SON, _PON)) + print("Readily Biodegradable (biodeg. sol.) Org. N =", _RBON, + " Non-Biodegradable Sol. Org. N =", _UBSON) + print("Slowly Biodegradable (biodeg. part.) Org.N =", _SBON, + " Non-Biodegradable part.Org. N =", _UBPON) + + _temp_comps = [self._DO, + _NBSCOD, _RBCOD, self._NH3N, _RBON, self._NOxN, + self._Alk, + _NBPCOD, _SBCOD, 0.0, 0.0, 0.0, _SBON] + + elif asm_ver == 'ASM2d': + _TCOD = self._BOD5 * self._model_fracs['ASM2d']['COD:BOD5'] + _RBSCOD = self._model_fracs['ASM2d']['RBSCOD:COD'] * _TCOD + _CBSCOD = self._model_fracs['ASM2d']['CBSCOD:COD'] * _TCOD + _NBSCOD = self._model_fracs['ASM2d']['NBSCOD:COD'] * _TCOD + _PCOD = _TCOD - _RBSCOD - _CBSCOD - _NBSCOD + _NBPCOD = self._model_fracs['ASM2d']['NBPCOD:PCOD'] * _PCOD + _NH3N = self._model_fracs['ASM2d']['NH3N:TKN'] * self._TKN + _ORGN = self._TKN - _NH3N + _BORGN = self._model_fracs['ASM2d']['BORGN:ORGN'] * _ORGN + # TODO: CONTINUE HERE + + + + + 'ASM2d': + { + # rbsCOD + cbsCOD + nbsCOD + pCOD = COD + 'RBSCOD:COD': 0.10, + 'CBSCOD:COD': 0.30, # sCOD = rbsCOD + cbsCOD + nbsCOD + 'NBSCOD:COD': 0.10, # pCOD = COD - sCOD + 'NBPCOD:PCOD': 0.20, # nbpCOD = fraction * pCOD + # TKN = NH3N + OrgN + 'NH3N:TKN': 0.75, # OrgN = TKN - NH3N + 'BORGN:ORGN': 0.50, # bOrgN = fraction * OrgN; nbOrgN = OrgN - bOrgN + 'ORGN:COD': 0.10, # use the COD to divide the orgN + # TP = orthoP + orgP + 'PO4P:TP': 0.65, + 'ORGP:COD': 0.03 # use the COD to divide the orgP + #TODO: add ASM2d Fractions here + + # check if any negative values from the fractionation + for tc in _temp_comps: + if tc < 0: + print('ERROR in fractions resulting in negative model', + ' components. Influent components NOT UPDATED') + return self._in_comps[:] # nothing changed + + + + + return _temp_comps[:] + + + # + # END OF FUNTIONS UNIQUE TO INFLUENT + + +# ----------------------------------------------------------------------------- + + +class effluent(pipe): + """ + A derived "pipe" class whose main outlet is "None". + + The "effluent" class is another form of a "pipe". It differs from the "pipe" class with its outlet being + "None". + """ + + __id = 0 + + def __init__(self): + """ + Special initialization for the "effluent". + + Reason: + + 1) Mainstream outlet is "None". + """ + pipe.__init__(self) + + self.__class__.__id += 1 + self._id = self.__class__.__id + self._type = 'Effluent' + self.__name__ = self._type + '_' + str(self._id) + self._codename = self.__name__ + + # flow data source tags + self._in_flow_ds = flow_data_src.TBD + self._mo_flow_ds = flow_data_src.TBD + self._so_flow_ds = flow_data_src.PRG + + self._mo_connected = True # dummy + + self._model_file_path = self.set_model_file_path() + + return None + + # ADJUSTMENTS TO COMMON INTERFACE TO FIT THE NEEDS OF EFFLUENT + # + def set_downstream_main(self, rcvr): + print('ERROR:', self.__name__, 'has no downstream main outlet.') + return None + + + def set_downstream_side(self, rcvr): + print('ERROR:', self.__name__, 'has no downstream side outlet.') + + +## def _branch_flow_helper(self): +## """ +## Calculate 1 of the 3 branches' flow based on the other 2. +## +## This function is re-implemented for "effluent" because the actual effluent flow rate of a WWTP has +## to do with its waste sludge flow (WAS flow). The WAS flow is set during simulation by PooPyLab. As a +## result, the effluent flow rate is the balance of the plant influent flow and WAS flow. +## +## Occasionally, there may be a WWTP without dedicated WAS unit when the effluent flow rate equals to +## that of the influent. +## """ +## +## # the _mo_flow of an effluent is set externally (global main loop) +## if self._upstream_set_mo_flow: +## self._mo_flow = self._total_inflow # _so_flow = 0 +## return None + + +# def set_mainstream_flow(self, flow=0): +# """ +# Define the mainstream outlet flow. +# +# """ +# if flow >= 0: +# self._mo_flow = flow +# self.set_flow_data_src('Main', flow_data_src.PRG) +# self._upstream_set_mo_flow = False +# else: +# print("ERROR:", self.__name__, "receives flow < 0.") +# self._mo_flow = 0.0 +# return None +## +## +## def discharge(self, method_name='BDF', fix_DO=True, DO_sat_T=10): +## """ +## Pass the total flow and blended components to the downstreams. +## +## This function is re-implemented for "effluent" because there is no further downstream units on either +## the main or side outlet. +## +## Args: +## (see the note in the discharge() defined in the splitter class) +## +## Return: +## None +## +## """ +## self._prev_mo_comps = self._mo_comps[:] +## self._prev_so_comps = self._so_comps[:] +## +## self._branch_flow_helper() +## +## self._mo_comps = self._in_comps[:] +## self._so_comps = self._in_comps[:] +## +## return None +## +## # +## # END OF ADJUSTMENTS TO COMMON INTERFACE +## +## # FUNCTIONS UNIQUE TO EFFLUENT +## # +## # (INSERT CODE HERE) +## # +## # END OF FUNCTIONS UNIQUE TO EFFLUENT +## + + +# ------------------------------------------------------------------------------ + + + +class WAS(pipe): + """ + Waste Activated Sludge, basically a "pipe" with special functionality. + + A WAS unit has the capability of calculating its own flow based on: + + 1) User defined solids retention time (SRT); + + 2) Solids inventory in bioreactors it sees; and + + 3) Its inlet's total suspended solids (TSS). + + It also differs from a "pipe" at its (mainstream) outlet which can be set as "None" or another process + unit, a sludge digester for instance. The outlet of a "pipe", however, shall always be connected to + another process unit. + """ + + __id = 0 + + def __init__(self): + """ + Special initialization for "WAS". + + Reasons: + + 1) WAS flow is determined during simulation; + + 2) WAS flow rate is passed onto upstream units for plant flow balance; + + 3) WAS mainstream outlet can be either "None" or a process unit. + """ + + pipe.__init__(self) + + self.__class__.__id += 1 + self._id = self.__class__.__id + self._type = 'WAS' + self.__name__ = self._type +'_' + str(self._id) + self._codename = self.__name__ + + # flow data source tags + self._in_flow_ds = flow_data_src.DNS + self._mo_flow_ds = flow_data_src.PRG + self._so_flow_ds = flow_data_src.PRG + + # assume something always receives WAS + self._mo_connected = True + + self._model_file_path = self.set_model_file_path() + + return None + + # ADJUSTMENTS TO COMMON INTERFACE TO FIT THE NEEDS OF WAS OBJ. + # + def set_downstream_main(self, rcvr): + print('ERROR:', self.__name__, 'has no downstream main outlet.') + return None + + + def set_downstream_side(self, rcvr): + print('ERROR:', self.__name__, 'has no downstream side outlet.') + return None + + # + # END OF ADJUSTMENTS TO COMMON INTERFACE + + + # FUNCTIONS UNIQUE TO WAS + # + # (INSERT CODE HERE) + # + + +## def get_solids_inventory(self, reactor_list=[]): +## """ +## Calculate the total solids mass in active reactors. +## +## Args: +## reactor_list: list of the asm_reactors with active treatment; +## +## Return: +## solids inventory (float) in grams. +## +## See: +## set_WAS_flow(). +## """ +## +## inventory = 0.0 +## for unit in reactor_list: +## inventory += unit.get_TSS() * unit.get_active_vol() +## +## return inventory +## +## +## def set_WAS_flow(self, SRT=5, reactor_list=[], effluent_list=[]): +## """ +## Set the waste sludge flow to meet the WWTP's solids retention time. +## +## Args: +## SRT: WWTP's SRT in days; +## reactor_list: list of active asm_reactors; +## effluent_list: list of all effluent units in the WWTP. +## +## Return: +## Mainstream outflow in m3/d +## +## See: +## get_solids_inventory(). +## """ +## +## # TODO: Need to re-write this function +## # +## #self.update_combined_input() +## +## _eff_solids = 0.0 +## for _u in effluent_list: +## _eff_solids += _u.get_TSS() * _u.get_main_outflow() +## +## _wf = 0.0 +## if self.get_TSS() != 0: +## _wf = ((self.get_solids_inventory(reactor_list) / SRT +## - _eff_solids) / self.get_TSS()) +## +## # screen out the potential < 0 WAS flow +## if _wf < 0: +## print('WARN: SRT specified can not be achieved.') +## self._mo_flow = 0.0 +## else: +## self._mo_flow = _wf +## +## #TODO: in MAIN function, we need to check whether the WAS flow +## # is higher than the influent flow; The WAS flow is then passed to the +## # SRT controlling splitter by the main loop. +## return self._mo_flow +## + # + # END OF FUNCTIONS UNIQUE TO WAS diff --git a/PooPyLab/utils/__init__.py b/PooPyLab/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/PooPyLab/utils/datatypes.py b/PooPyLab/utils/datatypes.py new file mode 100644 index 0000000..beab4d2 --- /dev/null +++ b/PooPyLab/utils/datatypes.py @@ -0,0 +1,92 @@ +# This file is part of PooPyLab. +# +# PooPyLab is a simulation software for biological wastewater treatment processes using International Water Association +# Activated Sludge Models. +# +# Copyright (C) Kai Zhang +# +# This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public +# License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with this program. If not, see +# . +# +# +"""Definitions of specific data types used in the PooPyLab project. +""" +## @namespace datatypes +## @file datatype.py + + +from enum import Enum + +# Flow Data Source + + +class flow_data_src(Enum): + """ + Data type "flow_data_src" is an enumerate of four possible sources. + + TBD='ToBeDetermined' + UPS='Upstream' + DNS='Downstream' + PRG='Program' + + This is mainly used to specify the controlling source of flow of a particular branch of a process unit. + + TBD(ToBeDetermined): + + When a process unit is created, flow data sources for all branches are set to TBD. Some process units, such + as influent/effluent, may have different initial flow data source settings since they are known. The user can + specify flow data source while building the process flow diagram (PFD) by connecting process units and setting + desired flow rates to specific branches (e.g. RAS flow). + + UPS(Upstream): + + When set on the inlet of a process unit, the total inflow will be the sum of all the flows received from its + upstream. + + When set on either the mainstream or sidestream outlet branch, the branch's flow rate will be the delta of + total inflow and the third branch. The third branch's flow will then have to be specified by the user or + downstream (DNS) during run time. + + DNS(Downstream): + + When set on the inlet of a process unit, the total inflow will be the sum of its mainstream and sidestream + outlets. As a result, either the user or runtime will have to provide the flow rates for the mainstream and + sidestream outlet branches. + + When set on the mainstream/sidestream outlet branches, the flow rate data have to be provided by downstream + processes. This scenario can be encountered in a final clarifier's sidestream outlet which is the underflow. + This underflow feeds into a flow splitter inlet. The splitter's mainstream outlet branch is used as RAS stream + whose flow rate is set by the user. The sidestream of the splitter is used as WAS whose flow rate is set by + the overall treatment plant. As a result, the splitter's inlet flow as well as the final clarifier's underflow + (sidestream outlet) both get their flow data from downstreams (DNS). + + PRG(Program): + + When the flow rate of a particular branch is to be set by the simulation program run time, PRG will be set + to that branch. An example would be the WAS unit's flow rate which is calculated based on the design solids + retention time, effluent total suspended solids, and solids inventory in all bioreactors. + + + The main loop will analyze the process flow diagram before the simulation begins and set the flow Based upon the + flow sources, a process unit maintains its flow balance during simulation by: + + 1) total_inflow = mainstream_outflow + sidestream_outflow; + 2) mainstream_outflow = total_inflow - sidestream_outflow; or + 3) sidestream_outflow = total_inflow - mainstream_outflow. + + See Also: + streams.splitter.set_flow_data_src() + """ + + TBD = 'ToBeDetermined' + UPS = 'Upstream' + DNS = 'Downstream' + PRG = 'Program' diff --git a/PooPyLab/utils/pfd.py b/PooPyLab/utils/pfd.py new file mode 100644 index 0000000..ecb89a9 --- /dev/null +++ b/PooPyLab/utils/pfd.py @@ -0,0 +1,364 @@ +# This file is part of PooPyLab. +# +# PooPyLab is a simulation software for biological wastewater treatment processes using International Water Association +# Activated Sludge Models. +# +# Copyright (C) Kai Zhang +# +# PooPyLab is free software: you can redistribute it and/or modify it under the terms of the GNU General Public +# License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# PooPyLab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with PooPyLab. If not, see +# . +# +# +# Definition of global utility functions related to the process flow diagram (PFD). +# +# Author: Kai Zhang +# +# + +"""Global functions for process flow diagram related operations. +""" +## @namespace pfd +## @file pfd.py + +import json +from ..unit_procs.streams import influent, effluent, WAS, pipe, splitter +from ..unit_procs.bio import asm_reactor +from ..unit_procs.physchem import final_clarifier + + +def _check_connection(pfd=[]): + """ + Check the validity of connections on the process flow diagram. + + All units are connected for their main- and sidestream outlets other + than the exceptions made in initializations. This function checks and count + the loose ends found in the PFD. + + Args: + pfd: Process Flow Diagram (list of all unit processes in the WWTP); + + Return: + # of loose ends (int) + """ + + loose_end = 0 + for unit in pfd: + if not unit.inlet_connected(): + print(unit.__name__, 'upstream is not connected') + loose_end += 1 + if unit.has_sidestream(): + if not unit.side_outlet_connected(): + print(unit.__name__, 'sidestream is not connected') + loose_end += 1 + elif not unit.main_outlet_connected(): + print(unit.__name__, 'main downstream is not connected') + loose_end += 1 + if loose_end == 0: + print('The units on the PFD are all connected.') + else: + print(loose_end, 'connecton(s) to be fixed.') + return loose_end + + +def _id_upstream_type(me, upds): + """ + Identify the type of a discharger connected to the inlet. + + This function identifies the type of an upstream discharger (upds) of (me) + + Args: + me: a process unit + upds: an upstream discharger of "me" + + Return: + strings such as {'VOID'|'INFLUENT'|'SPLITTER_MAIN'|'SPLITTER_SIDE'} + """ + + if me.get_type() == 'Influent': + return 'VOID' + elif upds.get_type() == 'Influent': + return 'INFLUENT' + elif isinstance(upds, splitter): # splitter & all its derived types + if me == upds.get_downstream_main(): + return 'SPLITTER_MAIN' + else: + return 'SPLITTER_SIDE' + elif isinstance(upds, pipe): # pipe & its derived types + return 'PIPE' + + +def _check_WAS(mywas): + """ + Check the validity of the WAS units in the pfd. + + Rules: + 1) A WAS unit shall connect w/ a splitter's sidestream via ONE pipe; + + 2) Only 0 or 1 WAS can connect to an SRT controlling splitter; + + Args: + mywas: the WAS unit under analysis + + Return: + Whether the WAS connection is valid (bool), + + The SRT controlling splitter connected to this WAS unit + """ + #TODO: check WAS strategy: + # (definition file, constant flow, constant flow ratio, dynamic flow schedule) + + # SRT Controlling splitter to be found: + _srt_ctrl_splt = None + _was_connect_ok = True + _srt_ctrl_count = 0 + + for _u in mywas: + for _upds in _u.get_upstream(): + if _upds.get_type() == 'Pipe' and len(_upds.get_upstream()) == 1: + _xu = [q for q in _upds.get_upstream()][0] + if _id_upstream_type(_upds, _xu) != 'SPLITTER_SIDE': + print('CONNECTION ERROR:', _u.__name__, 'shall be [SIDESTREAM->PIPE->WAS].') + _was_connect_ok = False + break + if _xu.is_SRT_controller(): + _srt_ctrl_splt = _xu + _srt_ctrl_count += 1 + + if _srt_ctrl_count > 1: + print('PFD ERROR: More than ONE SRT controlling splitters.') + _was_connect_ok = False + elif _srt_ctrl_count == 0: + print('PFD ERROR: No SRT controlling splitter was specified.') + _was_connect_ok = False + + return _was_connect_ok, _srt_ctrl_splt + + +def _check_sidestream_flows(mysplitters): + """ + Check the validity of the sidestreams of all splitter types. + + Rules: + 1) Sidestream flows shall be defined by user; + + 2) SRT controlling splitters or final_clarifiers are exempted from 1); + + 3) A final clarifier's sidestream flow is determined during runtime; + + 4) The main loop shall update its side outlet or main outlet flows. + + Args: + mysplitters: Splitters to be checked; + + Return: + total # of sidestreams with undefined flow rates (int) + """ + _undefined = 0 + for _u in mysplitters: + if not _u.sidestream_flow_defined(): + _undefined += 1 + print('PFD ERROR: Sidestream flow undefined:', _u.__name__) + + return _undefined == 0 + + +def _find_main_only_prefix(cur, pms): + """ + Find the mainstream only loop in the PFD. + + Rules: + 1) A recycle/recirculation (loop) shall be via a splitter sidestream; + + Args: + cur: current process unit; + pms: list of mainstream units (prefixes) leading to "cur". + + Return: + bool + + See: + _has_main_only_loops(). + """ + if cur in pms: + # found a mainstream-only loop + return True + + if cur is None or cur.get_type() == 'Effluent' or cur.get_type() == 'WAS': + # current ms_prefix leads to dead end + if len(pms) > 0: + pms.pop() + return False + + pms.append(cur) + + if _find_main_only_prefix(cur.get_downstream_main(), pms): + return True + elif len(pms) > 0: + pms.pop() + return False + else: + return False + + +def _has_main_only_loops(pfd): + """ + Analyze a PFD and see whether it has a loop only via mainstream outlets. + + Rule: + 1) Loops with mainstreams only are not allowed in the PFD + + Args: + pfd: Process Flow Diagram (list of process units in the WWTP); + + Return: + bool + + See: + _find_main_only_prefix(). + """ + for _u in pfd: + prefix_ms = [] + if _find_main_only_prefix(_u, prefix_ms): + print('PFD ERROR: Found a mainstream-only loop.') + print(' Loop={}'.format([x.__name__ for x in prefix_ms])) + return True + else: + return False + + +def get_all_units(wwtp, type='ASMReactor'): + """ + Return all the units of a specific type in a treatment plant PFD. + + Args: + wwtp: a collection (list) of process units; + type: type of process units of interest. + + Return: + a list of process units + """ + return [w for w in wwtp if w.get_type() == type] + + +def check(wwtp): + """ + Check the validity of the PFD against the rules. + + Args: + wwtp: a collection (list) of process units; + + Return: + bool + + See: + _check_connection(); + _check_WAS(); + _check_sidestream_flows(); + """ + + _all_WAS = get_all_units(wwtp, 'WAS') + + _all_splitters = get_all_units(wwtp, 'Splitter') + + _le = _check_connection(wwtp) + _WAS_ok, _srt_ctrl_ = _check_WAS(_all_WAS) + _side_flow_defined = _check_sidestream_flows(_all_splitters) + _has_ms_loops = _has_main_only_loops(wwtp) + + if _le == 0 and _WAS_ok and _side_flow_defined and _has_ms_loops is False: + print('Found one SRT controller splitter; Moved to the back of PFD') + wwtp.remove(_srt_ctrl_) + wwtp.append(_srt_ctrl_) + print('PFD READY') + return True + else: + print('FIXED PFD ERRORS BEFORE PROCEEDING') + return False + + +def show(wwtp=[]): + """ + Show the verbal description of the PFD. + + Args: + wwtp: a collection (list) of process units; + + Return: + None + """ + + print('Current PFD Configuration:') + for unit in wwtp: + print(unit.__name__, ': receives from:', end=' ') + if unit.get_type() == 'Influent': + print("None") + else: + upstr = unit.get_upstream() + for dschgr in upstr: + print(dschgr.__name__, ',', end=' ') + print() + + print(' : discharges to:', end=' ') + if (isinstance(unit, effluent) or isinstance(unit, WAS) or not unit.main_outlet_connected()): + print('None') + elif unit.main_outlet_connected(): + print(unit.get_downstream_main().__name__, end='(main)') + else: + print('None') + if unit.has_sidestream(): + print(', and ', end=' ') + if unit.get_downstream_side() is None: + print('None', end='') + else: + print(unit.get_downstream_side().__name__, end='') + print('(side)') + else: + print() + return None + + +def save_wwtp(wwtp=[], global_params={}, filename='myWWTP.json'): + """ Save the plant configuratoin to a file in json + + Args: + wwtp: [all process units in the wastewater treatment plant] + global_params: {global parameters (e.g SRT) for the WWTP} + filename: file to save the json dump + + Return: + None + """ + plant_def = {'Flowsheet': {unit.get_codename(): unit.get_config() for unit in wwtp}, + 'Global Params': global_params + } + print(json.dumps(plant_def, sort_keys=False, indent=4)) + + with open(filename, "w") as savef: + savef.write(json.dumps(plant_def, sort_keys=False, indent=4)) + + return None + + +def read_wwtp(filename='myWWTP.json'): + """ Read in a WWTP's from a file + + Args: + filename: the file storing the WWTP's configs in json format + + Return: + {WWTP's configs} + """ + + with open(filename, 'r') as readf: + plant = json.load(readf) + print(json.dumps(plant, sort_keys=False, indent=4)) + + return plant diff --git a/PooPyLab/utils/run.py b/PooPyLab/utils/run.py new file mode 100644 index 0000000..77a0b31 --- /dev/null +++ b/PooPyLab/utils/run.py @@ -0,0 +1,699 @@ +# This file is part of PooPyLab. +# +# PooPyLab is a simulation software for biological wastewater treatment processes using International Water Association +# Activated Sludge Models. +# +# Copyright (C) Kai Zhang +# +# PooPyLab is free software: you can redistribute it and/or modify it under the terms of the GNU General Public +# License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# PooPyLab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with PooPyLab. If not, see +# . +# +# +# Definition of global utility functions related to the initial guess for the integration of the model's equation +# system. +# +# Author: Kai Zhang +# +# + +"""Global functions for running simulations. +""" +## @namespace run +## @file run.py + +from ..unit_procs.streams import pipe, influent, effluent, WAS, splitter +from ..unit_procs.bio import asm_reactor +from ..unit_procs.physchem import final_clarifier +from ..utils.datatypes import flow_data_src +from ..utils import pfd + +import cProfile + + +def input_inf_concs(asm_ver, inf_unit): + """ + Input the influent concentrations. + + The concentrations are measured as BOD5, TKN, NH3-N, NOx-N, TSS, VSS, Alkalinity before they are fractionated into + ASM model components such as S_S, X_S, S_NH, etc. + + Args: + asm_ver: ASM version: ASM1 | ASM2d | ASM3 + inf_unit: influent unit whose constituents to be defined + + Return: + None + + Note: + There are nine elements in inf_concs for ASM1: + inf_concs[0] : BOD5 + inf_concs[1] : TSS + inf_concs[2] : VSS + inf_concs[3] : TKN + inf_concs[4] : NH3N + inf_concs[5] : NOxN + inf_concs[6] : TP + inf_concs[7] : Alk, mmol/L as CaCO3 + inf_concs[8] : DO + """ + print("Please define the influent constituents...") + + inf_concs = [] + + neg = True + while neg: + inf_concs.append(eval(input('Carbonaceous BOD5 (mg/L) ='))) + inf_concs.append(eval(input('Total Suspended Solids (mg/L) ='))) + inf_concs.append(eval(input('Volatile Suspended Solids (mg/L) ='))) + inf_concs.append(eval(input('Total Kjeldahl Nitrogen (mg/L) ='))) + inf_concs.append(eval(input('Ammonium Nitrogen (mg/L) ='))) + inf_concs.append(eval(input('Nitrite and Nitrate Nitrogen (mg/L) ='))) + inf_concs.append(eval(input('Total Phosphorus (mg/L) ='))) + inf_concs.append(eval(input('Alkalinity (mmol/L as CaCO3) ='))) + inf_concs.append(eval(input('Dissolved Oxygen (mg/L) ='))) + + for conc in inf_concs: + if conc < 0: + print("Negative value detected. Please re-enter.") + neg = True + break + else: + neg = False + + inf_unit.set_constituents(asm_ver, inf_concs) + + return None + + +def check_global_cnvg(wwtp): + """ + Check global convergence of the WWTP PFD. + + Args: + wwtp: list of process units in a WWTP's PFD + + Return: + bool + """ + + #TODO: this function is unlikely to be needed in the equation-based solving system + # + for unit in wwtp: + if not unit.is_converged(): + #print(unit.__name__, 'not converged yet') + return False + return True + + +def show_concs(wwtp): + """ + Print the model components of the branches of each unit in the WWTP's PFD. + + Args: + wwtp: list of process units in a WWTP's PFD + + Return: + None + """ + # self._comps[0]: S_DO as DO + # self._comps[1]: S_I + # self._comps[2]: S_S + # self._comps[3]: S_NH + # self._comps[4]: S_NS + # self._comps[5]: S_NO + # self._comps[6]: S_ALK + # self._comps[7]: X_I + # self._comps[8]: X_S + # self._comps[9]: X_BH + # self._comps[10]: X_BA + # self._comps[11]: X_D + # self._comps[12]: X_NS + + col_name = ['FLOW', + 'S_DO', 'S_I', 'S_S', 'S_NH', 'S_NS', 'S_NO', 'S_ALK', + 'X_I', 'X_S', 'X_BH', 'X_BA', 'X_D', 'X_NS'] + format_cn = '{:>12s}' * len(col_name) + print(' ' * 8, end='') + print(format_cn.format(*col_name)) + + for elem in wwtp: + print(elem.__name__, '::') + print('__main {:>12.3f}'.format(elem.get_main_outflow()), end='') + for msconc in elem.get_main_outlet_concs(): + print('{:>12.3f}'.format(msconc), end='') + print() + if elem.has_sidestream(): + print('__side {:>12.3f}'.format(elem.get_side_outflow()), end='') + for ssconc in elem.get_side_outlet_concs(): + print("{:>12.3f}".format(ssconc), end='') + print() + + return None + + +def _wwtp_active_vol(reactors=[]): + """ + Return the sum of asm reactors' active volume. + + Args: + reactors: list of reactors whose active volumes are of interest. + + Return: + total active volume (float), m3 + """ + return sum([r.get_active_vol() for r in reactors]) + + +def initial_guess(params={}, reactors=[], inf_flow=1.0, plant_inf=[]): + """ + Return the initial guess as the start of integration towards steady state. + + The approach here is similar to that outlined in the IWA ASM1 report, where the total volume of all the reactors is + treated as one CSTR. + + Args: + params: model parameters adjusted to the project temperature; + reactors: reactors whose volume will be used; + inf_flow: wwtp's influent flow rate, m3/d; + plant_inf: model components for the entire wwtp's influent. + + Return: + list of ASM 1 component concentrations [float] + """ + + inf_S_S = plant_inf[2] + inf_S_NH = plant_inf[3] + inf_X_S = plant_inf[8] + inf_TKN = inf_S_NH + plant_inf[4] + plant_inf[12] + # assume outlet bsCOD + eff_S_S = 100.0 # mg/L + + # assume full nitrification (if inf TKN is sufficient) + eff_S_NH = 1.0 # mgN/L + + #Safety Factor + SF = 1.25 + + # OXIC SRT required for eff_S_S, assuming DO is sufficiently high + SRT_OXIC_H = 1.0 / (params['u_max_H'] * eff_S_S / (eff_S_S + params['K_S']) - params['b_LH']) + + # Oxic SRT required for eff_S_NH, assuming DO is sufficiently high + SRT_OXIC_A = 1.0 / (params['u_max_A'] * eff_S_NH / (eff_S_NH + params['K_NH']) - params['b_LA']) + + SRT_OXIC = max(SRT_OXIC_A, SRT_OXIC_H) * SF + + print('Min Oxic SRT for Heterotrophs = {:.2f} (day)'.format(SRT_OXIC_H)) + print('Min Oxic SRT for Autotrophs = {:.2f} (day)'.format(SRT_OXIC_A)) + print('SELECTED Oxic SRT = {:.2f} (day)'.format(SRT_OXIC)) + + # Initial guesses of S_S and S_NH based on the selected oxic SRT + + init_S_S = params['K_S'] * (1.0 / SRT_OXIC + params['b_LH']) \ + / (params['u_max_H'] - (1.0 / SRT_OXIC + params['b_LH'])) + + init_S_NH = params['K_NH'] * (1.0 / SRT_OXIC + params['b_LA']) \ + / (params['u_max_A'] - (1.0 / SRT_OXIC + params['b_LA'])) + + #print('eff. S_S = {:.3f} (mg/L COD)'.format(init_S_S)) + #print('eff. S_NH = {:.3f} (mg/L N)'.format(init_S_NH)) + + # daily active heterotrphic biomass production, unit: gCOD/day + daily_heter_biomass_prod = inf_flow * (inf_S_S + inf_X_S - init_S_S) * params['Y_H'] \ + / (1.0 + params['b_LH'] * SRT_OXIC) + + # Nitrogen Requried for assimilation + NR = 0.087 * params['Y_H'] * (1.0 + params['f_D'] * params['b_LH'] * SRT_OXIC) \ + / (1.0 + params['b_LH'] * SRT_OXIC) + + init_S_NO = (inf_TKN - NR * (inf_S_S + inf_X_S - init_S_S) - init_S_NH) + + # daily active autotrophic biomass production, unit: gCOD/day + daily_auto_biomass_prod = inf_flow * init_S_NO * params['Y_A'] / (1.0 + params['b_LA'] * SRT_OXIC) + + # daily heterotrphic debris production, unit: gCOD/day + daily_heter_debris_prod = daily_heter_biomass_prod * (params['f_D'] * params['b_LH'] * SRT_OXIC) + + # daily autotrophic debris production, unit: gCOD/day + daily_auto_debris_prod = daily_auto_biomass_prod * (params['f_D'] * params['b_LA'] * SRT_OXIC) + + + # treat the entire plant's reactor vol. as a single hypothetical CSTR + _hyp_cstr_vol = _wwtp_active_vol(reactors) + + # initial guesses of X_BH, X_BA, and X_D + init_X_BH = SRT_OXIC * daily_heter_biomass_prod / _hyp_cstr_vol + init_X_BA = SRT_OXIC * daily_auto_biomass_prod / _hyp_cstr_vol + init_X_D = SRT_OXIC * (daily_heter_debris_prod + daily_auto_debris_prod) / _hyp_cstr_vol + + # TODO: ALWAYS make sure the indecies are correct as per the model + init_S_DO = 2.0 # assume full aerobic for the hypothetical CSTR + init_S_I = plant_inf[1] + # init_S_S above + # init_S_NH above + init_S_NS = inf_TKN * 0.01 # TODO: assume 1% influent TKN as sol.org.N + # init_S_NO above + init_S_ALK = plant_inf[6] - 7.14 * (init_S_NO - plant_inf[5]) / 50.0 + init_X_I = plant_inf[7] + init_X_S = 0.1 * inf_X_S # assumed 10% remain + # init_X_BH above + # init_X_BA above + # init_X_D above + init_X_NS = params['i_N_XB'] * (init_X_BH + init_X_BA) + params['i_N_XD'] * init_X_D + + return [init_S_DO, init_S_I, init_S_S, init_S_NH, init_S_NS, init_S_NO, + init_S_ALK, + init_X_I, init_X_S, init_X_BH, init_X_BA, init_X_D, init_X_NS] + + +def _forward(me, visited=[]): + """ + Set the flow data source by visiting process units along the flow paths. + + This function is to be called by forward_set_flow(). It follows the flow paths and decide whether additional flow + data sources can be decided based on what's known. + + Args: + me: current process unit under analysis; + visisted: list of process units visited already. + + Return: + None + + See: + forward_set_flow(). + """ + if me in visited or me is None: + return None + + visited.append(me) + + _in = me.get_upstream() + _mo = me.get_downstream_main() + _so = me.get_downstream_side() + + _in_f_ds, _mo_f_ds, _so_f_ds = me.get_flow_data_src() + + _in_f_known = (_in_f_ds != flow_data_src.TBD) + + _mo_f_known = (_mo_f_ds != flow_data_src.TBD) + + _so_f_known = (_so_f_ds != flow_data_src.TBD) + + if _in_f_known: + if _so is None: + if not _mo_f_known: + me.set_flow_data_src('Main', flow_data_src.UPS) + _forward(_mo, visited) + #else: + #pass + else: + if not _mo_f_known: + if _so_f_known: + me.set_flow_data_src('Main', flow_data_src.UPS) + #else: + #pass + else: + if _so_f_known: + # both _mo_f_known and _so_f_known + return None + else: + me.set_flow_data_src('Side', flow_data_src.UPS) + _forward(_so, visited) + else: + # _in_flow_data_src == TBD, can it be determined? + _me_in_f_ds_known = True + for _f in _in: + _f_in_f_ds, _f_mo_f_ds, _f_so_f_ds = _f.get_flow_data_src() + if _f.get_downstream_main() == me: + if _f_mo_f_ds == flow_data_src.TBD: + _me_in_f_ds_known = False + break + else: + if _f_so_f_ds == flow_data_src.TBD: + _me_in_f_ds_known = False + break + if _me_in_f_ds_known: + me.set_flow_data_src('Inlet', flow_data_src.UPS) + _forward(me, visited) + return None + + +def forward_set_flow(wwtp): + """ + Set the _upstream_set_mo_flow flag of those influenced by the starters. + + Args: + wwtp: list of all units in a wwtp. + + Return: + None + + See: + _forward(). + backward_set_flow(); + _backward(); + _sum_of_known_inflows(). + """ + #TODO: this function is unlikely to be needed in the equation-based solving system + # + + _visited = [] + _starters = [] + + # find potential starters + for _u in wwtp: + _in_fds, _mo_fds, _so_fds = _u.get_flow_data_src() + + _in_f_known = (_in_fds == flow_data_src.UPS or _in_fds == flow_data_src.PRG) + + _mo_f_known = (_mo_fds == flow_data_src.UPS or _mo_fds == flow_data_src.PRG) + + _so_f_known = (_so_fds == flow_data_src.UPS or _so_fds == flow_data_src.PRG) + + if (_in_f_known or _mo_f_known or _so_f_known): + _starters.append(_u) + + for _s in _starters: + _forward(_s, _visited) + + return None + + +def _BFS(_to_visit, _visited, mn, fDO, DOsat): + """ + Breadth First Search type of traverse. + + Args: + _to_visit: list of process units to be visited; + _visited: list of process units visited. + mn: method name used in scipy.integrate.solveivp() + fDO: whether to simulate at a fix DO setpoint, bool + DOsat: DO saturation conc. under the site conditions, mg/L + + Return: + list of process units in their visited order. + """ + + if len(_to_visit) == 0: + return [_u.__name__ for _u in _visited] + + _next = _to_visit.pop(0) + + if _next not in _visited: + _visited.append(_next) + _next.update_combined_input() + _next.discharge(mn, fDO, DOsat) + if _next.has_sidestream(): + _next_s = _next.get_downstream_side() + if _next_s not in _visited: + _to_visit.append(_next_s) + _next_m = _next.get_downstream_main() + if _next_m not in _visited and _next_m is not None: + _to_visit.append(_next_m) + return _BFS(_to_visit, _visited, mn, fDO, DOsat) + else: + return [_u.__name__ for _u in _visited] + + +def traverse_plant(wwtp, plant_inf, mn, fDO, DOsat): + """ + Visit every process units on the PFD starting from the influent. + + Args: + wwtp: list of all process units on the WWTP's PFD; + plant_inf: plant influent unit; + mn: method name for scipy.integrate.solveivp(); + fDO: whether to simulate w/ a fix DO setpoint, bool, + DOsat: DO saturation conc. under the site coniditions, mg/L + + Return: + None + + See: + _BFS(); + """ + + _to_visit = [plant_inf] + _visited = [] + _finished = [] + + while len(_visited) < len(wwtp): + _finished = _BFS(_to_visit, _visited, mn, fDO, DOsat) + #print("visited:", _finished) + + return None + + +def _sum_of_known_inflows(me, my_inlet_of_unknown_flow): + """ + Return the sum of all known flow rates of the inlet of a process unit. + + Args: + me: current process unit; + my_inlet_of_unknown_flow: discharger w/ flow into "me" unknown. + + Return: + float, m3/d + """ + #TODO: this function would become an flow balance equation in the equation-based solving system + + _sum = 0.0 + for _inlet in me.get_upstream(): + if _inlet != my_inlet_of_unknown_flow: + if _inlet.get_downstream_main() == me: + _sum += _inlet.get_main_outflow() + else: + _sum += _inlet.get_side_outflow() + return _sum + + +def _backward(me): + """ + Set the flow data source by visiting process units against the flow paths. + + This function is to be called by backward_set_flow(). It decides whether additional flow data sources can be + determined based on (_mo_flow + _so_flow). If so, proceed and set the inflow and trace further upstream of "me". + + Args: + me: current process unit under analysis; + visisted: list of process units visited already. + + Return: + None + + See: + backward_set_flow(); + forward_set_flow(); + _forward(). + """ + + _in_f_ds, _mo_f_ds, _so_f_ds = me.get_flow_data_src() + + _in_f_known = (_in_f_ds != flow_data_src.TBD) + + _mo_f_known = (_mo_f_ds != flow_data_src.TBD) + + _so_f_known = (_so_f_ds != flow_data_src.TBD) + + if _so_f_known: + if _mo_f_known: + if _in_f_known: + if _in_f_ds == flow_data_src.UPS: + return None + else: + me.set_flow_data_src('Inlet', flow_data_src.DNS) + else: + if _in_f_known: + me.set_flow_data_src('Main', flow_data_src.UPS) + me._upstream_set_mo_flow = True + else: + return None + else: + if _mo_f_known: + if _in_f_known: + me.set_flow_data_src('Side', flow_data_src.UPS) + else: + return None + else: + return None + + _my_inlet = me.get_upstream() + _my_inlet_allow = [] + if _my_inlet is not None: + _my_inlet_allow = [u for u in _my_inlet + if ((u.get_flow_data_src()[1] == flow_data_src.TBD + or u.get_flow_data_src()[1] == flow_data_src.DNS) + and u.get_downstream_main() == me) + or + ((u.get_flow_data_src()[2] == flow_data_src.TBD + or u.get_flow_data_src()[2] == flow_data_src.DNS) + and u.get_downstream_side() == me)] + + _freedom = len(_my_inlet_allow) + if _freedom == 0: # all inlets have been set with flow source + return None + elif _freedom > 1: # too many units for setting flows + print('ERROR:{} has {} upstream units with undefined flows.'.format(me.__name__, _freedom)) + else: + _target = _my_inlet_allow[0] + _known_sum = _sum_of_known_inflows(me, _target) + _residual = me.totalize_inflow() - _known_sum + + if _target.get_downstream_main() == me: + #_target.set_mainstream_flow_by_upstream(False) + _target.set_flow_data_src('Main', flow_data_src.DNS) + _target.set_mainstream_flow(_residual) + else: + _target.set_flow_data_src('Side', flow_data_src.DNS) + _target.set_sidestream_flow(_residual) + + if _target.get_flow_data_src()[0] == flow_data_src.DNS: + _backward(_target) + + return None + + +def backward_set_flow(start=[]): + """ + Back tracing to set the flows of the inlet units for those in starters. + + Args: + start: list of units whose inflow = mainstream flow + sidestream flow. + + Return: + None + + See: + _backward(); + forward_set_flow(); + _forward(); + _sum_of_known_inflows(). + """ + for _u in start: + _backward(_u) + + return None + + +def get_steady_state(wwtp=[], target_SRT=5, verbose=False, diagnose=False, mn='BDF', fDO=True, DOsat=10): + """ + Integrate the entire plant towards a steady state at the target SRT. + + Constant influent flows and loads are required. If the user only knows the dynamic influent characteristics, the + averages should be used as the influent conditions. + + Args: + wwtp: all process units in a wastewater treatment plant + target_SRT: target solids retention time (d) for the steady state + verbose: flag for more detailed output + diagnose: flag for the use of cProfile for performance analysis + mn: method used in scipy.integrate.solveivp(), string + fDO: whether to simulate w/ a fix DO setpoint, bool + DOsat: DO saturation conc. under the site conditions, mg/L + + Return: + None + + See: + utils.pdf; + forward_set_flow(); + backward_set_flow(); + traverse_plant() + """ + + # identify units of different types + _inf = pfd.get_all_units(wwtp, 'Influent') + _reactors = pfd.get_all_units(wwtp, 'ASMReactor') + + # _WAS may be an empty [], see below (after intial guess) + _WAS = pfd.get_all_units(wwtp, 'WAS') + + _splitters = pfd.get_all_units(wwtp, 'Splitter') + _srt_ctrl = [_u for _u in _splitters if _u.is_SRT_controller()] + _final_clar = pfd.get_all_units(wwtp, 'Final_Clarifier') + _eff = pfd.get_all_units(wwtp, 'Effluent') + _plant_inf_flow = sum([_u.get_main_outflow() for _u in _inf]) + + if verbose: + print('Influent in the PFD: {}'.format([_u.__name__ for _u in _inf])) + print(' Total influent flow into plant:', _plant_inf_flow) + print('Reactors in the PFD: {}'.format([_u.__name__ for _u in _reactors])) + print('WAS units in the PFD: {}'.format([_u.__name__ for _u in _WAS])) + print('Splitters in the PFD: {}'.format( + [_u.__name__ for _u in _splitters])) + print('SRT Controlling Splitter in the PFD: {}'.format( + [_u.__name__ for _u in _srt_ctrl])) + print('Final Clarifier in the PFD: {}'.format( + [_u.__name__ for _u in _final_clar])) + print('Effluent in the PFD: {}'.format([_u.__name__ for _u in _eff])) + print() + for i in range(len(_reactors)): + print(_reactors[i].get_active_vol()) + print(_reactors[i].get_model_params()) + print(_reactors[i].get_model_stoichs()) + + + # start the main loop + _WAS_flow = 0.0 # M3/d + _SRT = target_SRT + + # get the influent ready + for _u in _inf: + _u.update_combined_input() + _u.discharge(mn, fDO, DOsat) + + # TODO: what if there are multiple influent units? + # An equation-based solving system would take care of that + if len(_reactors): + _params = _reactors[0].get_model_params() + _seed = initial_guess(_params, _reactors, _inf[0].get_main_outflow(), _inf[0].get_main_outlet_concs()) + format_sd = '{:<.3f}, ' * len(_seed) + print('Initial guess =', format_sd.format(*_seed), '\n\n') + for _r in wwtp: + _r.assign_initial_guess(_seed) + + for fc in _final_clar: + fc.set_capture_rate(0.992) + + forward_set_flow(wwtp) + + # collect all the possible starting points for backward flow setting + _backward_start_points = [_w for _w in _WAS] + [_e for _e in _eff] + + if diagnose: + profile = cProfile.Profile() + profile.enable() + + r = 0 + while True: + if len(_WAS) == 0: + _WAS_flow = 0 + else: + _WAS_flow = _WAS[0].set_WAS_flow(_SRT, _reactors, _eff) + _WAS[0].set_mainstream_flow(_WAS_flow) + _eff[0].set_mainstream_flow(_plant_inf_flow - _WAS_flow) + backward_set_flow(_backward_start_points) + traverse_plant(wwtp, _inf[0], mn, fDO, DOsat) + + if check_global_cnvg(wwtp): + break + r += 1 + + if diagnose: + profile.disable() + profile.print_stats() + + show_concs(wwtp) + + if verbose: + print("TOTAL ITERATION = ", r) + + diff --git a/README.md b/README.md index 7d04af2..d571378 100755 --- a/README.md +++ b/README.md @@ -1,276 +1,17 @@ -GENERAL DEVELOPMENT PLAN FOR POOPYLAB -******************************************************************************* -1.0 INTRODUCTION: +PooPyLab is a biological wastewater treatment software. - PooPyLab is a biological wastewater treatment software that is built upon - Activated Sludge Models (ASM 1,2d, & 3) developed by the International - Water Association (IWA). +Please see the project wiki for more details. Well, there has not been a whole lot of details on the wiki yet, but more +than what's on here. - There are commercial softwares like BioWin, GPS-X, and others that provide - similar core functionalities for wastewater engineers and students. However, - their model implementations may be proprietary and hidden from the users. - PooPyLab is a free open source software that offers a platform for ASM model - related research and integrating computer programming with environmental - science/engineering. - -1.1 BASIC FEATURES: +To install the PooPyLab package: pip3 install poopylab - 1) GUI for wastewater treatment plant process setup - - 2) Treatment units include: - a) Single Completely Mixed Stirred Tank (CSTR) reactor (For Plug Flow - Reactor, or PFR, please use CSTR-in-series to mimick) - b) Influent Unit - c) Effluent Unit - d) Flow Splitter - e) Waste Activated Sludge (WAS) Unit - f) Facility Life Cycle Assessment (LCA) +Please see the examples/ folder for examples about how to use the available classes/methods to construct your own +treatment plant process flow diagrams and get steady state results. - 3) Steady state simulation - - 4) Charts of simulation results - - 5) ASM 1, 2d, and 3 +More detailed documentation: +[toogad.github.io/brownbook](https://toogad.github.io/brownbook/index.html) -1.2 RUNNING ENVIRONMENT: - - 1) Python 3 - - 2) SciPy & NumPy - - 3) Matplotlib - - 4) PyQT4 - - 5) Linux (Windows and Mac environments to be tested) - -1.3 LICENSES: - - Please refer to the attached License.txt document for details. - - -1.4 OBJECTIVES OF THIS GENERAL DEVELOPMENT PLAN - - This Development Plan ("the Plan") is to provide general guidelines for - developers on the key fuctionalities, structure, and coordination - aspect of the project. It is not meant to provide detail instructions - on how to write the codes. Ideally, the whole PooPyLab package will - be subdivided into smaller sections that provide their own functionalities - and work with each other to complete the main objectives of the software. - For example, the Plan will define the core process units that are commonly - used in wastewater treatment plants (WWTP) that utilized activated sludge. - These may include biological reactors, clarifiers, waste activated sludge - (i.e. WAS) handling, etc. The Plan specifies the input, output, and - treatment process of each unit. The developer can then code to provide - the interface for these units to enable them take input, do steady state - and/or dynamic simulations, output results, communicate with up- and - downstream process units, and report errors. - - The Plan provide the following basic information for the development team: - - 1) Structure of the software; - - 2) Core functionalities; - - 3) Classes and procedures needed to accomplish the core functionalities; - - 4) Summary of what had been under development (note: this will be a - continuous update as the project moves forward); - - 5) Summary of what still needs to be added (note: this will be a continous - update as the project moves forward); - - 6) General guidelines for schedule/milestones/releases. - -=============================================================================== -2.0 STRUCTURE OF THE SOFTWARE - Please refer to the attached structure diagram (COMING SOON) - -=============================================================================== -3.0 FUNCTIONALITIES - - 3.1 Graphic User Interface (GUI): - 3.1.1 "Drag and Drop" to setup Process Flow Diagram (PFD): - 3.1.1.1 Automatic addition/removal of process units in the program - to match the PFD; - 3.1.1.2 Storage of the PFD in a file that can be used to re-create - the diagram; - 3.1.1.3 Re-create the PFD and project conditions from saved files. - 3.1.2 Automatic recongnition of flow directions for Pipes - 3.1.3 Edition of process unit's properties by double-clicking on the - units, such as reactor volumes, dimensions, dissoved oxygen, - local model parameters, etc. - 2.1.4 Validity check of the PFD. - - 3.2 Steady State Simulation - The current strategy for running steady state simulation is to solve - the mass balance equations in each process unit, the results of the - current iteration will be compared to the previous. If the two set of - results are close to each other within the preset convergence - criteria for all the units, the current results are considered the - steady state. If not, the current results will be used for the down- - stream unit's input until the maximum number of allowed iteration is - reached. - - Pseudo-Code: - IF ALL THE UNITS IN THE WWTP ARE NOT CONVERGED AND NUMBER OF - ITERATION IS LESS THAN THE MAXIMUM: - FROM THE INFLUENT OF THE WWTP: - LOOP: - GO TO THE NEXT UNIT ON THE PFD - SOLVE MODEL COMPONENTS FOR THIS UNIT --> UNIT_C_CURRENT[] - IF MAX(ABS(UNIT_C_CURRENT[] - UNIT_C_PREVIOUS[])) - <= CONVERGE_CRITERIA - MARK UNIT AS CONVERGED - ELSE - MARK UNIT AS NOT CONVERGED - UNIT_C_PREVIOUS[] = UNIT_C_CURRENT[] - LOOP CONTINUES UNTIL ALL THE UNITS HAVE BEEN CHECKED IN - THIS ROUND - ELSE: - OUTPUT CONVERGED RESULTS - TERMINTATE THE ITERATION - - 3.2.1 Initial Guess - The solver for the steady state simulation is Newton-Raphson - (or one of its variations), which requires initial guess to - begin. Good initial guess is a key for the success of the - solver runs. - - Currently, PooPyLab is using ASM1 which doesn't deal with - biological phosphorus (P) removal. Therefore, the initial - guess on the first reactor (either an anoxic or aerobic) is - relatively simple. When it comes to ASM2 and/or ASM3 where BPR - and/or cellular storage are a part of the model, initial guess - will need to be revisited and carefully developed. - - 3.3 Dynamic Simulation - The approach for the dynamic simulation is expected to be similar to - that of the steady state (Sequential Modular Approach), except that the - solver will be something like RK4. And there will be a time period - defined by the user for the length of the simulation. Influent - concentrations at a particular time may need to be interpolated based - on user input. - - Much thinking still need to be put into the dyanmic simulation. - - Another approach for the dynamic simulation is to generate the entire - group of equations for the overall PFD, and then solve simultaneously - for the time (Equation Based Approach). There are papers in the 1980's - comparing the pros and cons of the two approaches. I (Kai) personally - think that the Sequential Modular Approach may fit the object oriented - programming scheme better, since we are programming in Python, an OOP - language. - - 3.3.1 Initial Guess - For dynamic simulation, the initial guess will be the steady - state solution. - - 3.4 Model Modification and Extension - Not much plan has been developed for this PooPyLab functionality at - this point. It could make the software much more complicated if we - want to provide a GUI for it. - - However, since PooPyLab is open source, it may be easier for users to - simply use the source code to inherite, modify, and extend the models - directly. The users can save the revised models in a separate folder of - the PooPyLab project and share with others. That being said, it is - probably still desirable to have extension functionality via the GUI. - If that is to be implemented, there could be the need for code writing - code. - - ANY INPUT ON THIS TOPIC IS WELCOME! - - 3.5 Report Simulation Results - The results of either steady state or dynamic simulations will need to - be presented in tables and/or plots. - - 3.6 Storage and Reloading of Simulation Results - PooPyLab shall be able to store the simulation results, reload the - previous results when the user open his/her saved project. - - 3.7 ANY OTHER SUGGESTIONS AND IDEAS?? - -=============================================================================== -4.0 CURRENT STATUS (WHAT HAVE BEEN DONE) - This section shall be constantly edited by Kai Zhang to reflect the most - recent status of the development. - - 4.1 Classes - Please refer to the attached UML diagram and the source code for the - definitions of the key classes, and their relationship to each other. - - Please note that the UML diagram may lack behind the code development. - If there are any discrepency between the UML and the source code, - please use the source code as the accurate/correct information and - inform the development team about the discrepency. - - 4.2 Definition of ASM1 - The representation of the ASM models will be in the file asm.py. - Currently only ASM1 has been added. - - 4.3 Definitions of Classes - The following classes have been in place with initial definitions. - However, they are revised constantly. Please refer to the project's - repository on GITHUB to see their most recent status and any additional - files. - - 4.3.1 Base: Common abstract interfaces only. - 4.3.2 Pipe: Basic flow functions. Multiple inlets, single outlet. - 4.3.3 Reactor: A Pipe that has active volume and can react. - 4.3.4 Influent: A unique unit without any upstreams (inlets). - 4.3.5 Effluent: A unique unit without any downstream (outlet). - 4.3.6 Branch: Similar to Influent, but to be used as part of a - Splitter. - 4.3.7 Splitter: Combination of Pipe and Branch. - 4.3.8 Clarifier: Currently behave like a Splitter without settling - model built in. - 4.3.9 WAS: Waste sludge leaves the WWTP from here. Acts like an - Effluent, but with the capability of SRT control. - 4.3.10 pyASM1v0_6.py: Preliminary test of the ASM1 and - scipy.integrate.fsolve() on a CSTR reactor using an example - from Grady Jr. (1999): Biological Wastewater Treatment, - 2nd Edition. - 4.3.11 MainTest.py: Testing the connections among different process - units with the current class definitions. - -=============================================================================== -5.0 TO DO LIST - This section shall be constantly edited by Kai Zhang to reflect the current - need for the development. - - 5.1 THE CURRENT PLAN FOR BOTH STEADY STATE AND DYANMIC SIMULATIONS IS TO - USE THE SEQUENTIAL MODULAR APPROACH. ADDITIONAL THOUGHTS SHALL BE PUT - IN TO EVALUATE WHETHER THE EQUATION BASED APPROACH OFFERS ANY SPECIFIC - ADVANTAGES OVER THE CURRENT ONE. - - 5.2 CONTINUE TO TEST THE INTERACTION OF THE PROCESS UNITS, ESPECIALLY - 5.2.1 SLUDGE WASTING AND SRT CONTROL. - 5.2.2 RECEIVING, BLENDING, AND TRANSFERRING FLOW AND MODEL COMPONENTS - FROM UPSTREAM UNITS TO DOWNSTREAM ONE. - 5.2.3 SIDESTREAM INTERACTION WITH OTHER UNITS. - - 5.3 DEVELOP A CLASS FOR EXTERNAL CARBON ADDITION UNIT. - - 5.4 DEVELOP TESTING CASES FOR ASM1 STEADY STATE SIMULATION - 5.4.1 SINGLE CSTR FOR ORGANIC AND AMMONIA OXIDATION. - 5.4.2 CSTR IN SERIES FOR ORGANIC AND AMMONIA OXIDATION. - 5.4.3 MLE WITH SINGLE CSTR PRE-ANOXIC AND AERATION ZONES. - 5.4.4 MLE WITH CSTR-IN-SERIES PRE-ANOXIC AND AERATION ZONES. - 5.4.5 PRE-ANOXIC, AEROBIC, FOLLOWED BY POST-ANOXIC. EXTERNAL CARBON - ADDITION. - - 5.5 ADD CAPABILITY OF STORING AND READING PROCESS FLOW DIAGRAM AND MODEL - SETTINGS. - - 5.6 DEVELOP GRAPHIC USER INTERFACE (GUI) - 5.6.1 ICONS FOR EACH UNIT TYPE. - 5.6.2 DRAG AND DROP CAPABILITY. - 5.6.3 CONNECT PROCESS FLOW DIAGRAM TO CODE - - - - - +Wiki/Tutorial: +[PooPyLab Wiki](https://github.com/toogad/PooPyLab_Project/wiki) diff --git a/Reference/ASM_ADM_Papers/ADM1-WST.pdf b/Reference/ASM_ADM_Papers/ADM1-WST.pdf deleted file mode 100644 index f33ca92..0000000 Binary files a/Reference/ASM_ADM_Papers/ADM1-WST.pdf and /dev/null differ diff --git a/Reference/ASM_ADM_Papers/ASM 1.pdf b/Reference/ASM_ADM_Papers/ASM 1.pdf deleted file mode 100644 index c8fe8e5..0000000 Binary files a/Reference/ASM_ADM_Papers/ASM 1.pdf and /dev/null differ diff --git a/Reference/ASM_ADM_Papers/Activated sludge model 3.pdf b/Reference/ASM_ADM_Papers/Activated sludge model 3.pdf deleted file mode 100644 index 6925379..0000000 Binary files a/Reference/ASM_ADM_Papers/Activated sludge model 3.pdf and /dev/null differ diff --git a/Reference/ASM_ADM_Papers/Activated sludge model No2D.pdf b/Reference/ASM_ADM_Papers/Activated sludge model No2D.pdf deleted file mode 100644 index 892bb7b..0000000 Binary files a/Reference/ASM_ADM_Papers/Activated sludge model No2D.pdf and /dev/null differ diff --git a/Reference/LCA_Papers/Corominas_et_al_2013_Water_research.pdf b/Reference/LCA_Papers/Corominas_et_al_2013_Water_research.pdf deleted file mode 100644 index f741ad6..0000000 Binary files a/Reference/LCA_Papers/Corominas_et_al_2013_Water_research.pdf and /dev/null differ diff --git a/Reference/LCA_Papers/c3em00685a.pdf b/Reference/LCA_Papers/c3em00685a.pdf deleted file mode 100644 index 84b25e6..0000000 Binary files a/Reference/LCA_Papers/c3em00685a.pdf and /dev/null differ diff --git a/Reference/pyASM1v0_6.py b/Reference/pyASM1v0_6.py deleted file mode 100755 index 1fc3de0..0000000 --- a/Reference/pyASM1v0_6.py +++ /dev/null @@ -1,841 +0,0 @@ -#============================================================================= -# Python Implementation of the IWA Activated Sludge Model 1 (ASM1) -# Based on Grady Jr. et al (1999) -# ASM1 provides simulation basis for activated sludge processes with -# 1. COD removal -# 2. Nitrification -# 3. Denitrification -# Biological phosphorus removal (BPR) is not part of ASM1 -# -# This Python implementation is to focus on how to construct the model in -# computer programs. -# pyASM1v0_6.py has successfully model the steady state of a single CSTR -# with ASM1. -# -# Last Update: 2018-02-03...KZ..migrated to Python3 -#============================================================================= - - -# NOMENCLATURE: -# rb: readily biodegradable -# nrb: non-readily biodegradable -# O: Oxygen -# NH: Ammonia -# L: Lysis -# H: Heterotrophs -# h: Hydrolysis -# a: Ammonification -# i: ratio -# f: fraction -# cf: correction factor -# A: Autotrophs -# B: Biomass (active) -# D: Debris -# NO: NOx (oxidized nitrogen) -# I: Inert -# ALK: Alkalinity -# S: Substrate (COD or TKN, unit: mg/L) -# X: Particulate matter as COD (unit: mg/L) -# S: Soluble -# Inf: Influent -# Eff: Effluent -# TBD: To Be Determined by user - -import scipy.optimize - -# PARAMETER DEFINITIONS: - -# 0.0 Default Growth Rates at 20 C. -# THEY SHALL BE ADJUSTED TO PROJECT SPECIFIC TEMPERATURE -# S_DO, u_H, b_LH, u_A, b_LA -# K_S, K_OH, K_NH, K_OA, K_NO, k_h, K_X, k_a, Y_H, Y_A -# f_D_, cf_h, cf_g, i_N_XB, i_N_XD - -def SetGlobalParameters(Temp=20.0): # Temp = Temperature (unit: degree C) - - Delta_Temp = Temp - 20.0 - - # define a Python Dictionary to store Global Parameters - Global_Parameters = dict() - - #0.1 Ideal Growth Rate of Heterotrophs (u_H, unit: 1/DAY) - Global_Parameters['u_H'] = 6.0 * pow(1.08, Delta_Temp) - - #0.2 Ideal Lysis Rate of Heterotrophs (b_LH, unit: 1/DAY) - Global_Parameters['b_LH'] = 0.408 * pow(1.04, Delta_Temp) - - #0.3 Ideal Growth Rate of Autotrophs (u_A, unit: 1/DAY) - Global_Parameters['u_A'] = 0.768 * pow(1.11, Delta_Temp) - - #0.4 Ideal Growth Rate of Autotrophs (b_LA, unit: 1/DAY) - Global_Parameters['b_LA'] = 0.096 * pow(1.04, Delta_Temp) - - #0.5 Half Growth Rate Concentration of Heterotrophs (K_s, unit: mgCOD/L) - Global_Parameters['K_S'] = 20.0 - - #0.6 Switch Coefficient for Dissoved O2 of Hetero. (K_OH, unit: mgO2/L) - Global_Parameters['K_OH'] = 0.1 - - #0.7 Association Conc. for Dissoved O2 of Auto. (K_OA, unit: mgN/L) - Global_Parameters['K_OA'] = 0.75 - - #0.8 Association Conc. for NH3-N of Auto. (K_NH, unit: mgN/L) - Global_Parameters['K_NH'] = 1.0 * pow(1.14, Delta_Temp) - - # 0.9 Association Conc. for NOx of Hetero. (K_NO, unit: mgN/L) - Global_Parameters['K_NO'] = 0.2 - - #0.10 Hydrolysis Rate (k_h, unit: mgCOD/mgBiomassCOD-day) - Global_Parameters['k_h'] = 2.208 * pow(1.08, Delta_Temp) - - #0.11 Half Rate Conc. for Hetro. Growth on Part. COD - # (K_X, unit: mgCOD/mgBiomassCOD) - Global_Parameters['K_X'] = 0.15 - - #0.12 Ammonification of Org-N in biomass (k_a, unit: L/mgBiomassCOD-day) - Global_Parameters['k_a'] = 0.1608 * pow(1.08, Delta_Temp) - - #0.13 Yield of Hetero. Growth on COD (Y_H, unit: mgBiomassCOD/mgCOD-removed) - Global_Parameters['Y_H'] = 0.6 - - #0.14 Yield of Auto. Growth on TKN (Y_A, unit: mgBiomassCOD/mgTKN-oxidized) - Global_Parameters['Y_A'] = 0.24 - - #0.15 Fract. of Debris in Lysed Biomass(f_D_,unit: mgDebrisCOD/mgBiomassCOD) - Global_Parameters['f_D_'] = 0.08 - - #0.16 Correction Factor for Hydrolysis (cf_h, unitless) - Global_Parameters['cf_h'] = 0.4 - - #0.17 Correction Factor for Anoxic Heterotrophic Growth (cf_g, unitless) - Global_Parameters['cf_g'] = 0.8 - - #0.18 Ratio of N in Active Biomass (i_N_XB, unit: mgN/mgActiveBiomassCOD) - Global_Parameters['i_N_XB'] = 0.086 - - #0.19 Ratio of N in Debris Biomass (i_N_XD, unit: mgN/mgDebrisBiomassCOD) - Global_Parameters['i_N_XD'] = 0.06 - - return Global_Parameters -##==================================End of Section 0=========================== - - -##============================End of Section 1.0============================ - -# 2.0 PROCESS RATE DEFINITIONS (Rj, unit: M/L^3/T): - -# 2.1 Growth Rates: -# 2.1.1 Monod Factor of Sol.BioDegrad.COD on Hetero. (Monod) -# 2.1.2 Monod Switch of Dissol. O2 on Hetero. (Monod) -# 2.1.3 Monod Switch of Dissol. O2 on Auto. (Monod) -# 2.1.4 Monod Factor of Ammonia-N on Autotrophs (Monod) -# 2.1.5 Monod Factor of NOx-N on Autotrophs (Monod) - -def Monod (My_Eff_S_TBD, My_K_TBD): - return My_Eff_S_TBD / (My_Eff_S_TBD + My_K_TBD) - -# 2.1.6 Aerobic Growth Rate of Heterotrophs (r1_AerGH, unit: mgCOD/L/day) -def r1_AerGH (CompList, Parameters, Bulk_DO): - return Parameters['u_H'] * Monod(CompList[7], Parameters['K_S']) \ - * Monod(Bulk_DO, Parameters['K_OH']) * CompList[3] - -# 2.1.7 Anoxic Growth Rate of Heterotrophs (r2_AxGH, unit: mgCOD/L/day) -def r2_AxGH (CompList, Parameters, Bulk_DO): - return Parameters['u_H'] * Monod(CompList[7], Parameters['K_S']) \ - * Monod(Parameters['K_OH'], Bulk_DO) \ - * Monod(CompList[9], Parameters['K_NO']) \ - * Parameters['cf_g'] * CompList[3] - -#2.1.8 Aerobic Growth Rate of Autotrophs (r3_AerGA, unit: mgCOD/L/day) -def r3_AerGA(CompList, Parameters, Bulk_DO): - return Parameters['u_A'] * Monod(CompList[10], Parameters['K_NH']) \ - * Monod(Bulk_DO, Parameters['K_OA']) * CompList[4] - -#2.1.9 Death and Lysis Rate of Heterotrophs (r4_DLH, unit: mgCOD/L/day) -def r4_DLH(CompList, Parameters): - return Parameters['b_LH'] * CompList[3] - -#2.1.10 Death and Lysis Rate of Autotrophs (r5_DLA, unit: mgCOD/L/day) -def r5_DLA(CompList, Parameters): - return Parameters['b_LA'] * CompList[4] - -#2.1.11 Ammonification Rate of Soluable Organic N (r6_AmmSN, unit: mgN/L/day) -def r6_AmmSN(CompList, Parameters): - return Parameters['k_a'] * CompList[11] * CompList[3] - -#2.1.12 Hydrolysis Rate of Particulate Organics (r7_HydX, unit: mgCOD/L/day) -def r7_HydX(CompList, Parameters, Bulk_DO): - return Parameters['k_h'] \ - * Monod(CompList[2] / CompList[3], Parameters['K_X']) \ - * (Monod(Bulk_DO, Parameters['K_OH']) \ - + Parameters['cf_h'] * Monod(Parameters['K_OH'], Bulk_DO) \ - * Monod(CompList[9], Parameters['K_NO'])) \ - * CompList[3] - -#2.1.13 Hydrolysis Rate of Part. Organic N (r8_HydXN, unit: mgN/L/day) -def r8_HydXN(CompList, Parameters, Bulk_DO): - return r7_HydX(CompList, Parameters, Bulk_DO) * CompList[12] / CompList[2] - -##========================== End of Section 2 ================================= - - - -#3.0 STOCHIOMETRIC MATRIX (Use the Table 6.1, p193, Grady Jr. et al 1999) -def SetStochiometrics(Global_Parameters): - - Stochiometrics = dict() - - Stochiometrics['1_3'] = 1.0 - - Stochiometrics['1_7'] = -1.0 / Global_Parameters['Y_H'] - - Stochiometrics['1_8'] = -(1.0 - Global_Parameters['Y_H']) \ - / Global_Parameters['Y_H'] - #multiply -1.0 to express as oxygen - - Stochiometrics['1_10'] = -Global_Parameters['i_N_XB'] - - Stochiometrics['1_13'] = -Global_Parameters['i_N_XB'] / 14.0 - - Stochiometrics['2_3'] = 1.0 - - Stochiometrics['2_7'] = -1.0 / Global_Parameters['Y_H'] - - Stochiometrics['2_9'] = -(1.0 - Global_Parameters['Y_H']) \ - / (2.86 * Global_Parameters['Y_H']) - - Stochiometrics['2_10'] = -Global_Parameters['i_N_XB'] - - Stochiometrics['2_13'] = (1.0 - Global_Parameters['Y_H']) \ - / (14.0 * 2.86 * Global_Parameters['Y_H']) \ - - Global_Parameters['i_N_XB'] / 14.0 - - Stochiometrics['3_4'] = 1.0 - - Stochiometrics['3_8'] = -(4.57 - Global_Parameters['Y_A']) \ - / Global_Parameters['Y_A'] - #multiply -1.0 to express as oxygen - - Stochiometrics['3_9'] = 1.0 / Global_Parameters['Y_A'] - - Stochiometrics['3_10'] = -Global_Parameters['i_N_XB'] \ - - 1.0 / Global_Parameters['Y_A'] - - Stochiometrics['3_13'] = (-1.0) * Global_Parameters['i_N_XB'] / 14.0 \ - - 1.0 / (7.0 * Global_Parameters['Y_A']) - - Stochiometrics['4_2'] = 1.0 - Global_Parameters['f_D_'] - - Stochiometrics['4_3'] = -1.0 - - Stochiometrics['4_5'] = Global_Parameters['f_D_'] - - Stochiometrics['4_12'] = Global_Parameters['i_N_XB'] \ - - Global_Parameters['f_D_'] \ - * Global_Parameters['i_N_XD'] - - Stochiometrics['5_2'] = 1.0 - Global_Parameters['f_D_'] - - Stochiometrics['5_4'] = -1.0 - - Stochiometrics['5_5'] = Global_Parameters['f_D_'] - - Stochiometrics['5_12'] = Global_Parameters['i_N_XB'] \ - - Global_Parameters['f_D_'] \ - * Global_Parameters['i_N_XD'] - - Stochiometrics['6_10'] = 1.0 - - Stochiometrics['6_11'] = -1.0 - - Stochiometrics['6_13'] = 1.0 / 14.0 - - Stochiometrics['7_2'] = -1.0 - - Stochiometrics['7_7'] = 1.0 - - Stochiometrics['8_11'] = 1.0 - - Stochiometrics['8_12'] = -1.0 - - return Stochiometrics -##=========================== End of Stochiometrics =========================== - - - -#5.0 STEADY STATE EQUATION DEFINITION -# Calculation of Steady State -def Steady(CompList, Parameters, Stochiometrics, InfComp, SRT, Vol, Bulk_DO): - -# CompList is a Python List that represents key design parameters -# CompList[0]: WAS_FLOW (M3/day, already calculated) -# CompList[1]: X_I -# CompList[2]: X_S -# CompList[3]: X_BH -# CompList[4]: X_BA -# CompList[5]: X_D -# CompList[6]: S_I -# CompList[7]: S_S -# CompList[8]: S_DO -# CompList[9]: S_NO -# CompList[10]: S_NH -# CompList[11]: S_NS -# CompList[12]: X_NS -# CompList[13]: S_ALK -# - -# Parameters is a Python Dictionary that holds the ASM1 model parameters after initiation according to design temp. (C) - -# Stochiometrics is a Python Dictionary that holds the stochoimetrics - -# InfComp is Python List that represent the Influent values of the ASM1 Components -# with the exception of InfComp[0] which is the Inf_Flow (m3/day) -# 0_Inf_Flow -# 1_X_I, 2_X_S, 3_X_BH, 4_X_BA, 5_X_D -# 6_S_I, 7_S_S, 8_S_DO, 9_S_NO, 10_S_NH, 11_S_NS, 12_X_NS, 13_S_ALK - - -# MLSS is the Mixed Liquor Suspended Solids in mgCOD/L, within which X_I is fixed. - -# Bulk_DO in mgO2/L - - - #Steady state equations that calculate the simulation results: - # C[0] == WAS Flow for a SINGLE TANK REACTOR, DIRECT WASTING FROM REACTOR - result = [CompList[0] - Vol / SRT] - - # for C1_X_I, assume perfect solid-liquid separation (i.e. X_I = 0.0 mg/L in effluent) - result.append(InfComp[0] * InfComp[1] \ - - (InfComp[0] - CompList[0]) * 0.0 - CompList[0] * CompList[1] \ - + 0.0 * Vol) - - # for C2_X_S, assume perfect solid-liquid separation (i.e. X_S = 0.0 mg/L in effluent) - result.append(InfComp[0] * InfComp[2] \ - - (InfComp[0] - CompList[0]) * 0.0 - CompList[0] * CompList[2] \ - + (Stochiometrics['4_2'] * r4_DLH(CompList, Parameters) \ - + Stochiometrics['5_2'] * r5_DLA(CompList, Parameters) \ - + Stochiometrics['7_2'] * r7_HydX(CompList, Parameters, Bulk_DO)) \ - * Vol) - - - # for C3_X_BH, assume perfect solid-liquid separation (i.e. X_BH = 0.0 mg/L in effluent) - result.append(InfComp[0] * InfComp[3] \ - - (InfComp[0] - CompList[0]) * 0.0 - CompList[0] * CompList[3] \ - + (Stochiometrics['1_3'] * r1_AerGH(CompList, Parameters, Bulk_DO)\ - + Stochiometrics['2_3'] * r2_AxGH(CompList, Parameters, Bulk_DO) \ - + Stochiometrics['4_3'] * r4_DLH(CompList, Parameters)) \ - * Vol) - - # for C4_X_BA, assume perfect solid-liquid separation (i.e. X_BA = 0.0 mg/L in effluent) - result.append(InfComp[0] * InfComp[4] \ - - (InfComp[0] - CompList[0]) * 0.0 - CompList[0] * CompList[4] \ - + (Stochiometrics['3_4'] * r3_AerGA(CompList, Parameters, Bulk_DO)\ - + Stochiometrics['5_4'] * r5_DLA(CompList, Parameters)) \ - * Vol) - - # for C5_X_D, assume perfect solid-liquid separation (i.e. X_D = 0.0 mg/L in effluent) - result.append(InfComp[0] * InfComp[5] \ - - (InfComp[0] - CompList[0]) * 0.0 - CompList[0] * CompList[5] \ - + (Stochiometrics['4_5'] * r4_DLH(CompList, Parameters) \ - + Stochiometrics['5_5'] * r5_DLA(CompList, Parameters)) \ - * Vol) - - # for C6_S_I - result.append(InfComp[0] * InfComp[6] \ - - (InfComp[0] - CompList[0]) * CompList[6] \ - - CompList[0] * CompList[6] \ - + 0.0 * Vol) - - # for C7_S_S - result.append(InfComp[0] * InfComp[7] \ - - (InfComp[0] - CompList[0]) * CompList[7] \ - - CompList[0] * CompList[7] \ - + (Stochiometrics['1_7'] * r1_AerGH(CompList, Parameters, Bulk_DO)\ - + Stochiometrics['2_7'] * r2_AxGH(CompList, Parameters, Bulk_DO) \ - + Stochiometrics['7_7'] \ - * r7_HydX(CompList, Parameters, Bulk_DO)) \ - * Vol) - - # for C8_S_DO - result.append(InfComp[0] * InfComp[8] \ - - (InfComp[0] - CompList[0]) * CompList[8] \ - - CompList[0] * CompList[8] - + (Stochiometrics['1_8'] * r1_AerGH(CompList, Parameters, Bulk_DO)\ - + Stochiometrics['3_8'] \ - * r3_AerGA(CompList, Parameters, Bulk_DO)) \ - * Vol) - #result.append(CompList[8] - Bulk_DO) - - # for C9_S_NO - result.append(InfComp[0] * InfComp[9] \ - - (InfComp[0] - CompList[0]) * CompList[9] \ - - CompList[0] * CompList[9] \ - + (Stochiometrics['2_9'] * r2_AxGH(CompList, Parameters, Bulk_DO) \ - + Stochiometrics['3_9'] * r3_AerGA(CompList, Parameters, Bulk_DO))\ - * Vol) - - # for C10_S_NH - result.append(InfComp[0] * InfComp[10] \ - - (InfComp[0] - CompList[0]) * CompList[10] \ - - CompList[0] * CompList[10] - + (Stochiometrics['1_10'] * r1_AerGH(CompList, Parameters,Bulk_DO)\ - + Stochiometrics['2_10'] \ - * r2_AxGH(CompList, Parameters, Bulk_DO) \ - + Stochiometrics['3_10'] \ - * r3_AerGA(CompList, Parameters, Bulk_DO) \ - + Stochiometrics['6_10'] \ - * r6_AmmSN(CompList, Parameters)) \ - * Vol) - - # for C11_S_NS - result.append(InfComp[0] * InfComp[11] \ - - (InfComp[0] - CompList[0]) * CompList[11] \ - - CompList[0] * CompList[11] \ - + (Stochiometrics['6_11'] * r6_AmmSN(CompList, Parameters) \ - + Stochiometrics['8_11'] * r8_HydXN(CompList, Parameters,Bulk_DO))\ - * Vol) - - # for C12_X_NS - result.append(InfComp[0] * InfComp[12] \ - - (InfComp[0] - CompList[0]) * 0.0 \ - - CompList[0] * CompList[12] \ - + (Stochiometrics['4_12'] * r4_DLH(CompList, Parameters) \ - + Stochiometrics['5_12'] * r5_DLA(CompList, Parameters) \ - + Stochiometrics['8_12'] \ - * r8_HydXN(CompList, Parameters, Bulk_DO)) \ - * Vol) - - # for C13_S_ALK - result.append(InfComp[0] * InfComp[13] \ - - (InfComp[0] - CompList[0]) * CompList[13] \ - - CompList[0] * CompList[13] \ - + (Stochiometrics['1_13'] * r1_AerGH(CompList, Parameters,Bulk_DO)\ - + Stochiometrics['2_13'] * r2_AxGH(CompList, Parameters, Bulk_DO) \ - + Stochiometrics['3_13'] \ - * r3_AerGA(CompList, Parameters, Bulk_DO) \ - + Stochiometrics['6_13'] \ - * r6_AmmSN(CompList, Parameters)) \ - * Vol) - - return result - - - - -#=========================== TESTING PROGRAM ================================== - - -#6.0 USER-INPUT, PROJECT SPECIFIC DESIGN INFORMATION - -#wastewater minimum and maximum temperature -MinWaterTemp = eval(input('Minimum Water Temperature (C) = ')) -MaxWaterTemp = eval(input('Maximum Water Temperature (C) = ')) - -#Dissolved Oxygen in MLSS -Dissolved_Oxygen = eval(input('Reactor MLSS DO (mg/L) = ')) - -# Set Global Parameters according to the minimum wastewater temperature -GP = SetGlobalParameters(MinWaterTemp) -# Set stochiometrics constants -ST = SetStochiometrics(GP) - -print("Global ASM1 Params at Min. Water Temperature ", MinWaterTemp, " C:") -print(GP) -print("Global ASM1 Stochs at Min. Water Temperature ",MinWaterTemp,"C:") -print(ST) - -# Influent Flow, converted to M3/d -Inf_Flow = eval(input('Design Flow (MGD) = ')) * 1000.0 * 3.78 - -Inf_BOD5 = eval(input('Influent BOD5 (mg/L) = ')) #typical US Muni. WW = 240.0 -#influent biodegradable COD, BOD/COD = 1.71 for typ. muni.WW -Inf_CODb = Inf_BOD5 * 1.71 -#influent TSS (mgCOD/L), NOT IN InfC -Inf_TSS = eval(input('Influent TSS (mgTSS/L) = ')) #typ. US WW = 240.0 -#influent VSS (mgCOD/L), -Inf_VSS = 195.4 #eval(input('Influent VSS (mgVSS/L) = ')) -#influent total COD, COD/BOD5 = 2.04 per BioWin -Inf_CODt = Inf_BOD5 * 2.04 #eval(input('Influent COD (mg/L) = ')) -#influent total inner COD, -Inf_CODi = Inf_CODt - Inf_CODb - -Inf_S_I = 0.13 * Inf_CODt - -Inf_X_I = Inf_CODi - Inf_S_I - -Inf_X_S = 1.6 * Inf_VSS - Inf_X_I - -Inf_S_S = Inf_CODb - Inf_X_S - -#influent Heterotrophs (mgCOD/L), -Inf_X_BH = 0.0 -#influent Autotrophs (mgCOD/L), -Inf_X_BA = 0.0 -#influent Biomass Debris (mgCOD/L), -Inf_X_D = 0.0 - -print("Influent Total COD (mg/L) = ", Inf_CODt) -print("Influent total biodegradable COD (mg/L) = ", Inf_CODb) -print("Influent soluble biodegradable COD (Inf_S_S, mg/L) = ", Inf_S_S) -print("Influent part. biodegradable COD (Inf_X_S, mg/L) = ", Inf_X_S) -print("Influent sol. non-biodegradable COD (Inf_S_I, mg/L) = ", Inf_S_I) -print("Influent part. non-biodegradable COD (Inf_X_I, mg/L) = ", Inf_X_I) - -#assuming perfect solids-liquid separation -Eff_X_BH = 0.0 - -Eff_X_BA = 0.0 - -Eff_X_D = 0.0 - -Effluent_S_Total = eval(input("Effluent COD (mg/L) = ")) -Eff_S_S = Effluent_S_Total - Inf_S_I -#check the relationship among Total Effluent COD, Effluent Sol. biodegrad. COD, -# and sol. inert COD -while Eff_S_S <= 0.0: - print("Error in specified Eff. or Inf. S_S and/or S_I. Please re-enter:") - Effluent_S_Total = eval(input('Re-enter Eff. COD (mg/L) = ')) - Eff_S_S = Effluent_S_Total - Inf_S_I - -#influent TKN (mgN/L), NOT IN InfC -Inf_TKN = eval(input('Influent TKN (mgN/L) = ')) -#influent Ammonia-N (mgN/L), -Inf_S_NH = eval(input('Influent Ammonia-N (mgN/L) = ')) -#subdividing TKN into: -# a) nonbiodegradable TKN -NonBiodegradable_TKN_Ratio = 0.03 -#NON-BIODEGRADABLE TKN WILL HAVE TO BE ADDED BACK TO THE EFFLUENT TN -Inf_nb_TKN = Inf_TKN * NonBiodegradable_TKN_Ratio -Soluble_Biodegradable_OrgN_Ratio = Inf_S_S / (Inf_S_S + Inf_X_S)#Grady et al '99 -# b) soluble biodegrable TKN, -Inf_S_NS = (Inf_TKN - Inf_S_NH - Inf_nb_TKN) * Soluble_Biodegradable_OrgN_Ratio -# c) particulate biodegradable TKN, -Inf_X_NS = (Inf_TKN - Inf_S_NH - Inf_nb_TKN) \ - * (1.0 - Soluble_Biodegradable_OrgN_Ratio) - -print("Inf. Non-Biodeg. TKN = ", Inf_nb_TKN, " mgN/L") -print("Inf. Sol. Biodeg. Organic N (Inf_S_NS, mgN/L) = ", Inf_S_NS) -print("Inf. Part. Biodeg. Organic N (Inf_X_NS, mgN/L = ", Inf_X_NS) - -#Nowadays, ammonia removal is almost always required -Eff_S_NH = 1.0 #mgN/L - -#Safety Factor -SF = 1.25 -# convert b_LH and b_LA to b_H and b_A, respectively -b_H = GP['b_LH'] * (1 - GP['Y_H'] * (1.0 - GP['f_D_'])) -b_A = GP['b_LA'] * (1 - GP['Y_A'] * (1.0 - GP['f_D_'])) - -#print "b_H = ", b_H, " b_A = ", b_A - -#OXIC SRT required for Eff_S_S -SRT_OXIC_H = 1.0 / (GP['u_H'] * Monod(Eff_S_S, GP['K_S']) \ - * Monod(Dissolved_Oxygen, GP['K_OH']) - b_H) -#SRT required for Eff_S_NH -SRT_OXIC_A = 1.0 / (GP['u_A'] * Monod(Eff_S_NH, GP['K_NH']) \ - * Monod(Dissolved_Oxygen, GP['K_OA']) - b_A) -#Pick the larger of the two because nowadays nitrification is almost required -# in all WWTPs. -SRT_OXIC = max(SRT_OXIC_A, SRT_OXIC_H) * SF - -print("Min Oxic SRT for Heterotrophs = ", SRT_OXIC_H, " days") -print("Min Oxic SRT for Autotrophs = ", SRT_OXIC_A, " days") -print("SELECTED Oxic SRT = ", SRT_OXIC, " days") - -#Actual Effluent S_S and S_NH based on the actual oxic SRT -Eff_S_S = GP['K_S'] * (1.0 / SRT_OXIC + b_H) \ - / (GP['u_H'] - (1.0 / SRT_OXIC + b_H)) - -Eff_S_NH = GP['K_NH'] * (1.0 / SRT_OXIC + b_A) \ - / (GP['u_A'] - (1.0 / SRT_OXIC + b_A)) - -print("Eff. S_S = ", Eff_S_S, " (mgCOD/L)") -print("Eff. S_NH = ", Eff_S_NH, " (mgN/L)") - -#when Oxic SRT is infinity: -#Eff_S_S_Min = GP['K_S'] * b_H / (GP['u_H'] - b_H) -#Eff_S_NH_Min = GP['K_NH'] * b_A /(GP['u_A'] - b_A) - -#print "Min. Possible Eff. S_S = ", Eff_S_S_Min, " mgCOD/L" -#print "Min. Possible Eff. S_NH = ", Eff_S_NH_Min, " mgN/L" - -#influent Nitrite + Nitrate (mgN/L), -Inf_S_NO = eval(input('Influent Oxidized Nitrogen (NO2+NO3, mgN/L) = ')) - -Effluent_Total_N = eval(input('Effluent Total N (mgN/L) = ')) -#Assuming complete nitrification of all biodegradable TKN -#Assume 6.0% N in biomass: - -if Effluent_Total_N >= Inf_TKN - Inf_nb_TKN - Eff_S_NH \ - - GP['i_N_XD'] * (Inf_S_S + Inf_X_S - Effluent_S_Total) \ - * GP['Y_H']: - #which means denitrification is not required - Eff_S_NO = Inf_S_NO + Inf_TKN - Inf_nb_TKN - Eff_S_NH \ - - GP['i_N_XD'] * (Inf_S_S + Inf_X_S) * GP['Y_H'] - print("Denitrification is not required") - S_NO_2B_DN = 0.0 -else:# denitrification will be required - Eff_S_NO = Effluent_Total_N - Inf_nb_TKN - Eff_S_NH - print("DENITRIFICATION IS NEEDED:") - S_NO_2B_DN = Inf_S_NO + Inf_TKN - Inf_nb_TKN - Eff_S_NH \ - - GP['i_N_XD'] * (Inf_S_S + Inf_X_S) * GP['Y_H'] \ - - Eff_S_NO - -print("Oxic Basin Eff. S_NO = ", Eff_S_NO, " (mgN/L)") -print("S_NO to be Dentirified = ", S_NO_2B_DN, " (mgN/L)") - -#Sizing the reactor volume -#Assume perfect hydrolysis of influent biodegradable solids -# -#Global Parameters @ Max. Design Temperature -GP_MT = SetGlobalParameters(MaxWaterTemp) - -#print "ASM1 Parameters at Max. Water Temperature (", MaxWaterTemp, " C):" -#print GP_MT - -b_H_MT = GP_MT['b_LH'] * (1 - GP_MT['Y_H'] * (1.0 - GP_MT['f_D_'])) -b_A_MT = GP_MT['b_LA'] * (1 - GP_MT['Y_A'] * (1.0 - GP_MT['f_D_'])) - -#Oxygen Requirement at Max Design Temp., unit: gO2/day -#heterotrophic: -Oxygen_Required_H_MT = Inf_Flow * (Inf_S_S + Inf_X_S - Eff_S_S) \ - * (1.0 - (1.0 + GP_MT['f_D_'] * GP_MT['b_LH'] * SRT_OXIC) \ - * GP_MT['Y_H'] / (1.0 + b_H_MT * SRT_OXIC)) - -#Nitrogen Requried for Biomass Growth -NR_MT = 0.087 * (1.0 + GP_MT['f_D_'] * GP_MT['b_LH'] * SRT_OXIC) \ - * GP_MT['Y_H'] / (1.0 + b_H_MT * SRT_OXIC) -#autotrophic: -Oxygen_Required_A_MT = Inf_Flow \ - * (Inf_TKN + Inf_S_NO - NR_MT * (Inf_S_S + Inf_X_S - Eff_S_S)) \ - * (4.57 - (1.0 + GP_MT['f_D_'] * GP_MT['b_LA'] * SRT_OXIC) \ - * GP_MT['Y_A'] / (1.0 + b_A_MT * SRT_OXIC)) - -#Nowadays, nitrification is typically required, converted unit to kgO2/HR -Oxygen_Required_Total_MT = (Oxygen_Required_H_MT + Oxygen_Required_A_MT) / 1000.0 / 24.0 - -print("Summer Heterotrophic O2 Requirement = ", Oxygen_Required_H_MT, "gO2/day") -print("Summer Autotrophic O2 Requirement = ", Oxygen_Required_A_MT, "gO2/day") -print("Summer Total O2 Requirement = ", Oxygen_Required_Total_MT, "kgO2/HOUR") - -#Lower Limit of Aerobic Reactor Volume -#limited by Floc-Shear: -#(when oxygen demand is high, more aeration is needed, thus more floc shearing) -Air_Flow_MT = 6.0 * Oxygen_Required_Total_MT / 10.0 # m3/min -#assume 90m3Air/1000m3 can be accepted w/o floc tearing -Oxic_Vol_Min_FS = 1000.0 * Air_Flow_MT / 90.0 # m3 -#limited by sustainable Oxygen Transfer rate, assumed 10% O2 transfer efficiency -Oxic_Vol_Min_OT = Oxygen_Required_Total_MT / 0.1 -#use the larger of the above two as the minimum aerobic reactor volume -Oxic_Vol_Min = min(Oxic_Vol_Min_FS, Oxic_Vol_Min_OT) - -print("Minimum Oxic Volume (limited by shear force) = ", Oxic_Vol_Min, " m3") - - -#Oxygen Requirement at Min Design Temp., unit: gO2/day -#heterotrophic: -Oxygen_Required_H = Inf_Flow * (Inf_S_S + Inf_X_S - Eff_S_S) \ - * (1.0 - (1.0 + GP['f_D_'] * GP['b_LH'] * SRT_OXIC) * GP['Y_H'] \ - / (1.0 + b_H * SRT_OXIC)) - -#Nitrogen Requried for Biomass Growth -NR = 0.087 * (1.0 + GP['f_D_'] * GP['b_LH'] * SRT_OXIC) * GP['Y_H'] \ - / (1.0 + b_H * SRT_OXIC) -#autotrophic: -Oxygen_Required_A = Inf_Flow \ - * (Inf_TKN - NR * (Inf_S_S + Inf_X_S - Eff_S_S)) \ - * (4.57 - (1.0 + GP['f_D_'] * GP['b_LA'] * SRT_OXIC) * GP['Y_A'] \ - / (1.0 + b_A * SRT_OXIC)) - -#convert unit to kgO2/HR -Oxygen_Required_Total = (Oxygen_Required_H + Oxygen_Required_A) / 1000.0 / 24.0 - -print("Winter Heterotrophic O2 Requirement = ", Oxygen_Required_H, " gO2/day") -print("Winter Autotrophic O2 Requirement = ", Oxygen_Required_A, " gO2/day") -print("Winter Total O2 Requirement = ", Oxygen_Required_Total, " kgO2/HOUR") - -#Upper Limit of Aerobic Reactor Volume -#limited by biomass suspension: -#(when oxygen demand is low, less mixing energy is available) -Air_Flow = 6.0 * Oxygen_Required_Total / 10.0 # m3/min -#assuming 20m3 Air/1000m3 needed for biomass suspension -Oxic_Vol_Max = 1000.0 * Air_Flow / 20.0 # m3, - -print("Maximum Oxic Volume (limited by mixing energy) = ", Oxic_Vol_Max, "m3") -# NOW THE OXIC VOLUME IS BRACKETED BY Oxic_Vol_Min and Oxic_Vol_Max - - -#calculate daily solids production, unit: gCOD/day -Daily_Solids_Production_H = Inf_Flow \ - * (Inf_X_I + (1.0 + GP['f_D_'] * GP['b_LH'] * SRT_OXIC) \ - * GP['Y_H'] * (Inf_S_S + Inf_X_S - Eff_S_S) \ - / (1.0 + b_H * SRT_OXIC)) - -Daily_Solids_Production_A = Inf_Flow \ - * (1.0 + GP['f_D_'] * GP['b_LA'] * SRT_OXIC) * GP['Y_A'] \ - * (Inf_TKN - NR * (Inf_S_S + Inf_X_S - Eff_S_S) - Eff_S_NH) \ - / (1.0 + b_A * SRT_OXIC) - -Daily_Solids_Production_Total = Daily_Solids_Production_H \ - + Daily_Solids_Production_A + Inf_Flow * (Inf_TSS - Inf_VSS) * 1.2 - -print("Solids Production Based on Min. Water Temperature:") -print("Hetero. Solids Prod.= ", Daily_Solids_Production_H / 1.2 / 1000.0, "kgTSS/d") -print("Autot. Solids Prod.= ", Daily_Solids_Production_A / 1.2 / 1000.0, "kgTSS/d") -print("Total Solids Prod.= ", Daily_Solids_Production_Total / 1.2 / 1000.0, "kgTSS/d") - -#design MLSS in (mgCOD/L), convert to mgTSS/L -print("MLSS Range (mg/L): ", \ - Daily_Solids_Production_Total / 1.2 * SRT_OXIC / Oxic_Vol_Max, \ - " to ", Daily_Solids_Production_Total / 1.2 * SRT_OXIC / Oxic_Vol_Min) - -MLSS = eval(input("Design MLSS (mg/L) = ")) * 1.0 -Oxic_Vol = Daily_Solids_Production_Total / 1.20 * SRT_OXIC / MLSS - -while Oxic_Vol <= Oxic_Vol_Min or Oxic_Vol > Oxic_Vol_Max: - print("MLSS out of range, please re-enter") - MLSS = eval(input("Design MLSS (mg/L) = ")) * 1.0 - Oxic_Vol = Daily_Solids_Production_Total / 1.20 * SRT_OXIC / MLSS - - -print("Calculated Oxic Vol = ", Oxic_Vol, " m3") - -#calculate HRT of the Oxic Reactor -HRT_OXIC = Oxic_Vol / Inf_Flow - -print("Oxic Basin HRT = ", HRT_OXIC * 24.0 , " hours") - -#X_I within reactor -X_I = SRT_OXIC / HRT_OXIC * Inf_X_I - -print("Inert Solids (X_I) = ", X_I / 1.20, "mgTSS/L" ) -Eff_X_I = 0.0 #assume perfect solids-liquid separation -Eff_X_S = 0.0 -Eff_S_I = Inf_S_I - -Eff_S_DO = Dissolved_Oxygen - -#influent TP -Inf_TP = eval(input('Influent TP (mgP/L) = ')) - -#influent Alkalinity as CaCO3 (mg/L), -Inf_S_ALK = eval(input('Influent Alkalinity (mmol/L) = ')) - -#influent Dissolved Oxygen (mgO2/L), -Inf_S_DO = eval(input('Influent DO (mgO2/L) = ')) - - -#InfC is the Python List that represent the influent ASM1 model components, -# to be passed onto the fsolve() func. -InfC = list() -InfC = [Inf_Flow, \ - Inf_X_I, Inf_X_S, Inf_X_BH, Inf_X_BA, Inf_X_D, \ - Inf_S_I, Inf_S_S, -Inf_S_DO, Inf_S_NO, Inf_S_NH, \ - Inf_S_NS, Inf_X_NS, Inf_S_ALK] - - -#1.0 MODEL COMPONENTS (Ci, M/L^3) -# C is the ASM1 Model Components represented by a Python Dictionary -# C will store the results from the FSOLVE function in the main testing program -# C represent the concentrations WITHIN the reactor -C = dict() -#1.0 WAS Flow is set by the SRT, ASSUMING DIRECTLY WASTING FROM REACTOR -C['0_WAS_Flow'] = Oxic_Vol / SRT_OXIC #m3/day, -#print "WAS Flow = ", C['0_WAS_Flow'] -#1.1 Inert Particulate COD (C1_X_I, mgCOD/L) -C['1_X_I'] = X_I -#1.2 Slowly Biodegradable Particulate COD (C2_X_S, mgCOD/L) -# to be calculated in FSOLVE() -C['2_X_S'] = 0.0 -#1.7 Effluent Soluble Readily Biodegradable Substrate (C7_S_S, mgCOD/L) -C['7_S_S'] = Eff_S_S -#1.3 Active Heterotrophic Biomass (C3_X_BH, mgCOD/L) -C['3_X_BH'] = SRT_OXIC / HRT_OXIC \ - * (InfC[7] + InfC[2] - C['7_S_S']) * GP['Y_H'] \ - / (1.0 + b_H * SRT_OXIC) -#print "X_BH = ", C['3_X_BH'] - -#1.5 Debris from Biomass Decay (C5_X_D, mgCOD/L) -C['5_X_D'] = SRT_OXIC * GP['f_D_'] * GP['b_LH'] * SRT_OXIC / HRT_OXIC \ - * (InfC[7] - C['7_S_S']) * GP['Y_H'] / (1 + b_H * SRT_OXIC) -#1.6 Inert Soluble Readily Biodegradable Substrate (C6_S_I, mgCOD/L) -C['6_S_I'] = Inf_S_I -#print "X_D = ", C['5_X_D'] - -#1.8 Oxygen Requirement (C8_S_DO, mgCOD/L) -# to be calculated in FSOLVE() -C['8_S_DO'] = 0.0 -#1.9 Nitrite and Nitrate Combined Cocentration (C9_S_NO, mgN/L) -C['9_S_NO'] = Eff_S_NO -#1.10 Ammonia Concentration (C10_S_NH, mgN/L) -C['10_S_NH'] = Eff_S_NH -#1.11 Soluble Biodegradable Organic Nitrogen (C11_S_NS, mgN/L) -# to be calculated in FSOLVE() -C['11_S_NS'] = 0.0 -#1.12 Particulate Biodegradable Organic Nitrogen (C12_X_NS, mgN/L) -# to be calculated in FSOLVE() -C['12_X_NS'] = 0.0 -#1.13 Alkalinity (C13_S_ALK, mgCaCO3/L) -# to be calculated in FSOLVE() -C['13_S_ALK'] = 0.0 - -#1.4 Active Autotrophic Biomass (C4_X_BA, mgCOD/L) -C['4_X_BA'] = SRT_OXIC / HRT_OXIC * (InfC[10] - C['10_S_NH']) * GP['Y_A'] \ - / (1.0 + b_A * SRT_OXIC) -#print "X_BA = ", C['4_X_BA'] - - -#rename the model components to meet requirements for scipy.optimize.fsolve() -DesignComponents_Guess = list() -DesignComponents_Guess = [C['0_WAS_Flow'], \ - C['1_X_I'], InfC[2], C['3_X_BH'], C['4_X_BA'], \ - C['5_X_D'], InfC[6], C['7_S_S'], InfC[8], \ - C['9_S_NO'], C['10_S_NH'], InfC[11], InfC[12], \ - InfC[13]] - -SteadyState = scipy.optimize.fsolve(Steady, \ - DesignComponents_Guess, \ - (GP, ST, InfC, SRT_OXIC, Oxic_Vol, \ - Dissolved_Oxygen)) - -print("Influent Components = ", InfC) - -print("Steady State Results: " ) -#print SteadyState -C['0_WAS_Flow'] = SteadyState[0] #m3/day, -C['1_X_I'] = SteadyState[1] -C['2_X_S'] = SteadyState[2] -C['3_X_BH'] = SteadyState[3] -C['4_X_BA'] = SteadyState[4] -C['5_X_D'] = SteadyState[5] -C['6_S_I'] = SteadyState[6] -C['7_S_S'] = SteadyState[7] -C['8_S_DO'] = SteadyState[8] -C['9_S_NO'] = SteadyState[9] -C['10_S_NH'] = SteadyState[10] -C['11_S_NS'] = SteadyState[11] -C['12_X_NS'] = SteadyState[12] -C['13_S_ALK'] = SteadyState[13] - -print(C) -print() -print("Oxic Basin Solids Conc. (mgMLSS/L) = ", (SteadyState[1] + SteadyState[2] + \ - SteadyState[3] + SteadyState[4] + \ - SteadyState[5]) / 1.2 + \ - SRT_OXIC / HRT_OXIC * \ - (Inf_TSS - Inf_VSS)) - -print("Actual Oxygen Requirement (AOR, lbO2/day) = ", \ - - SteadyState[8] * Inf_Flow / 1000.0 * 2.2) - -print("Eff. COD (mg/L) = ", SteadyState[6] + SteadyState[7]) - -print("Eff. NH3-N (mgN/L) = ", SteadyState[10]) - -print("Eff. TKN (mgN/L) = ", SteadyState[10] + SteadyState[11]) - -print("Eff. TN (mgN/L) = ", SteadyState[9] + SteadyState[10] \ - + SteadyState[11] + Inf_nb_TKN) - -print("Eff. Alk. (mgCaCO3/L) = ", SteadyState[13] * 50.0) - diff --git a/docs/Doxyfile b/docs/Doxyfile new file mode 100644 index 0000000..095a5af --- /dev/null +++ b/docs/Doxyfile @@ -0,0 +1,2572 @@ + +# Doxyfile 1.8.19 + +# This file describes the settings to be used by the documentation system +# doxygen (www.doxygen.org) for a project. +# +# All text after a double hash (##) is considered a comment and is placed in +# front of the TAG it is preceding. +# +# All text after a single hash (#) is considered a comment and will be ignored. +# The format is: +# TAG = value [value, ...] +# For lists, items can also be appended using: +# TAG += value [value, ...] +# Values that contain spaces should be placed between quotes (\" \"). + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- + +# This tag specifies the encoding used for all characters in the configuration +# file that follow. The default is UTF-8 which is also the encoding used for all +# text before the first occurrence of this tag. Doxygen uses libiconv (or the +# iconv built into libc) for the transcoding. See +# https://www.gnu.org/software/libiconv/ for the list of possible encodings. +# The default value is: UTF-8. + +DOXYFILE_ENCODING = UTF-8 + +# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by +# double-quotes, unless you are using Doxywizard) that should identify the +# project for which the documentation is generated. This name is used in the +# title of most generated pages and in a few other places. +# The default value is: My Project. + +PROJECT_NAME = "The Brown Book" + +# The PROJECT_NUMBER tag can be used to enter a project or revision number. This +# could be handy for archiving the generated documentation or if some version +# control system is used. + +PROJECT_NUMBER = + +# Using the PROJECT_BRIEF tag one can provide an optional one line description +# for a project that appears at the top of each page and should give viewer a +# quick idea about the purpose of the project. Keep the description short. + +PROJECT_BRIEF = "Documentation for the PooPyLab project" + +# With the PROJECT_LOGO tag one can specify a logo or an icon that is included +# in the documentation. The maximum height of the logo should not exceed 55 +# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy +# the logo to the output directory. + +PROJECT_LOGO = + +# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path +# into which the generated documentation will be written. If a relative path is +# entered, it will be relative to the location where doxygen was started. If +# left blank the current directory will be used. + +OUTPUT_DIRECTORY = + +# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub- +# directories (in 2 levels) under the output directory of each output format and +# will distribute the generated files over these directories. Enabling this +# option can be useful when feeding doxygen a huge amount of source files, where +# putting all generated files in the same directory would otherwise causes +# performance problems for the file system. +# The default value is: NO. + +CREATE_SUBDIRS = NO + +# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII +# characters to appear in the names of generated files. If set to NO, non-ASCII +# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode +# U+3044. +# The default value is: NO. + +ALLOW_UNICODE_NAMES = NO + +# The OUTPUT_LANGUAGE tag is used to specify the language in which all +# documentation generated by doxygen is written. Doxygen will use this +# information to generate all constant output in the proper language. +# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese, +# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States), +# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian, +# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages), +# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian, +# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian, +# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish, +# Ukrainian and Vietnamese. +# The default value is: English. + +OUTPUT_LANGUAGE = English + +# The OUTPUT_TEXT_DIRECTION tag is used to specify the direction in which all +# documentation generated by doxygen is written. Doxygen will use this +# information to generate all generated output in the proper direction. +# Possible values are: None, LTR, RTL and Context. +# The default value is: None. + +OUTPUT_TEXT_DIRECTION = None + +# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member +# descriptions after the members that are listed in the file and class +# documentation (similar to Javadoc). Set to NO to disable this. +# The default value is: YES. + +BRIEF_MEMBER_DESC = YES + +# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief +# description of a member or function before the detailed description +# +# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the +# brief descriptions will be completely suppressed. +# The default value is: YES. + +REPEAT_BRIEF = YES + +# This tag implements a quasi-intelligent brief description abbreviator that is +# used to form the text in various listings. Each string in this list, if found +# as the leading text of the brief description, will be stripped from the text +# and the result, after processing the whole list, is used as the annotated +# text. Otherwise, the brief description is used as-is. If left blank, the +# following values are used ($name is automatically replaced with the name of +# the entity):The $name class, The $name widget, The $name file, is, provides, +# specifies, contains, represents, a, an and the. + +ABBREVIATE_BRIEF = "The $name class" \ + "The $name widget" \ + "The $name file" \ + is \ + provides \ + specifies \ + contains \ + represents \ + a \ + an \ + the + +# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then +# doxygen will generate a detailed section even if there is only a brief +# description. +# The default value is: NO. + +ALWAYS_DETAILED_SEC = NO + +# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all +# inherited members of a class in the documentation of that class as if those +# members were ordinary class members. Constructors, destructors and assignment +# operators of the base classes will not be shown. +# The default value is: NO. + +INLINE_INHERITED_MEMB = NO + +# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path +# before files name in the file list and in the header files. If set to NO the +# shortest path that makes the file name unique will be used +# The default value is: YES. + +FULL_PATH_NAMES = YES + +# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path. +# Stripping is only done if one of the specified strings matches the left-hand +# part of the path. The tag can be used to show relative paths in the file list. +# If left blank the directory from which doxygen is run is used as the path to +# strip. +# +# Note that you can specify absolute paths here, but also relative paths, which +# will be relative from the directory where doxygen is started. +# This tag requires that the tag FULL_PATH_NAMES is set to YES. + +STRIP_FROM_PATH = + +# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the +# path mentioned in the documentation of a class, which tells the reader which +# header file to include in order to use a class. If left blank only the name of +# the header file containing the class definition is used. Otherwise one should +# specify the list of include paths that are normally passed to the compiler +# using the -I flag. + +STRIP_FROM_INC_PATH = + +# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but +# less readable) file names. This can be useful is your file systems doesn't +# support long names like on DOS, Mac, or CD-ROM. +# The default value is: NO. + +SHORT_NAMES = NO + +# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the +# first line (until the first dot) of a Javadoc-style comment as the brief +# description. If set to NO, the Javadoc-style will behave just like regular Qt- +# style comments (thus requiring an explicit @brief command for a brief +# description.) +# The default value is: NO. + +JAVADOC_AUTOBRIEF = NO + +# If the JAVADOC_BANNER tag is set to YES then doxygen will interpret a line +# such as +# /*************** +# as being the beginning of a Javadoc-style comment "banner". If set to NO, the +# Javadoc-style will behave just like regular comments and it will not be +# interpreted by doxygen. +# The default value is: NO. + +JAVADOC_BANNER = NO + +# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first +# line (until the first dot) of a Qt-style comment as the brief description. If +# set to NO, the Qt-style will behave just like regular Qt-style comments (thus +# requiring an explicit \brief command for a brief description.) +# The default value is: NO. + +QT_AUTOBRIEF = NO + +# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a +# multi-line C++ special comment block (i.e. a block of //! or /// comments) as +# a brief description. This used to be the default behavior. The new default is +# to treat a multi-line C++ comment block as a detailed description. Set this +# tag to YES if you prefer the old behavior instead. +# +# Note that setting this tag to YES also means that rational rose comments are +# not recognized any more. +# The default value is: NO. + +MULTILINE_CPP_IS_BRIEF = NO + +# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the +# documentation from any documented member that it re-implements. +# The default value is: YES. + +INHERIT_DOCS = YES + +# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new +# page for each member. If set to NO, the documentation of a member will be part +# of the file/class/namespace that contains it. +# The default value is: NO. + +SEPARATE_MEMBER_PAGES = NO + +# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen +# uses this value to replace tabs by spaces in code fragments. +# Minimum value: 1, maximum value: 16, default value: 4. + +TAB_SIZE = 4 + +# This tag can be used to specify a number of aliases that act as commands in +# the documentation. An alias has the form: +# name=value +# For example adding +# "sideeffect=@par Side Effects:\n" +# will allow you to put the command \sideeffect (or @sideeffect) in the +# documentation, which will result in a user-defined paragraph with heading +# "Side Effects:". You can put \n's in the value part of an alias to insert +# newlines (in the resulting output). You can put ^^ in the value part of an +# alias to insert a newline as if a physical newline was in the original file. +# When you need a literal { or } or , in the value part of an alias you have to +# escape them by means of a backslash (\), this can lead to conflicts with the +# commands \{ and \} for these it is advised to use the version @{ and @} or use +# a double escape (\\{ and \\}) + +ALIASES = + +# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources +# only. Doxygen will then generate output that is more tailored for C. For +# instance, some of the names that are used will be different. The list of all +# members will be omitted, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_FOR_C = NO + +# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or +# Python sources only. Doxygen will then generate output that is more tailored +# for that language. For instance, namespaces will be presented as packages, +# qualified scopes will look different, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_JAVA = NO + +# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran +# sources. Doxygen will then generate output that is tailored for Fortran. +# The default value is: NO. + +OPTIMIZE_FOR_FORTRAN = NO + +# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL +# sources. Doxygen will then generate output that is tailored for VHDL. +# The default value is: NO. + +OPTIMIZE_OUTPUT_VHDL = NO + +# Set the OPTIMIZE_OUTPUT_SLICE tag to YES if your project consists of Slice +# sources only. Doxygen will then generate output that is more tailored for that +# language. For instance, namespaces will be presented as modules, types will be +# separated into more groups, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_SLICE = NO + +# Doxygen selects the parser to use depending on the extension of the files it +# parses. With this tag you can assign which parser to use for a given +# extension. Doxygen has a built-in mapping, but you can override or extend it +# using this tag. The format is ext=language, where ext is a file extension, and +# language is one of the parsers supported by doxygen: IDL, Java, JavaScript, +# Csharp (C#), C, C++, D, PHP, md (Markdown), Objective-C, Python, Slice, VHDL, +# Fortran (fixed format Fortran: FortranFixed, free formatted Fortran: +# FortranFree, unknown formatted Fortran: Fortran. In the later case the parser +# tries to guess whether the code is fixed or free formatted code, this is the +# default for Fortran type files). For instance to make doxygen treat .inc files +# as Fortran files (default is PHP), and .f files as C (default is Fortran), +# use: inc=Fortran f=C. +# +# Note: For files without extension you can use no_extension as a placeholder. +# +# Note that for custom extensions you also need to set FILE_PATTERNS otherwise +# the files are not read by doxygen. + +EXTENSION_MAPPING = + +# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments +# according to the Markdown format, which allows for more readable +# documentation. See https://daringfireball.net/projects/markdown/ for details. +# The output of markdown processing is further processed by doxygen, so you can +# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in +# case of backward compatibilities issues. +# The default value is: YES. + +MARKDOWN_SUPPORT = YES + +# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up +# to that level are automatically included in the table of contents, even if +# they do not have an id attribute. +# Note: This feature currently applies only to Markdown headings. +# Minimum value: 0, maximum value: 99, default value: 5. +# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. + +TOC_INCLUDE_HEADINGS = 3 + +# When enabled doxygen tries to link words that correspond to documented +# classes, or namespaces to their corresponding documentation. Such a link can +# be prevented in individual cases by putting a % sign in front of the word or +# globally by setting AUTOLINK_SUPPORT to NO. +# The default value is: YES. + +AUTOLINK_SUPPORT = YES + +# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want +# to include (a tag file for) the STL sources as input, then you should set this +# tag to YES in order to let doxygen match functions declarations and +# definitions whose arguments contain STL classes (e.g. func(std::string); +# versus func(std::string) {}). This also make the inheritance and collaboration +# diagrams that involve STL classes more complete and accurate. +# The default value is: NO. + +BUILTIN_STL_SUPPORT = NO + +# If you use Microsoft's C++/CLI language, you should set this option to YES to +# enable parsing support. +# The default value is: NO. + +CPP_CLI_SUPPORT = NO + +# Set the SIP_SUPPORT tag to YES if your project consists of sip (see: +# https://www.riverbankcomputing.com/software/sip/intro) sources only. Doxygen +# will parse them like normal C++ but will assume all classes use public instead +# of private inheritance when no explicit protection keyword is present. +# The default value is: NO. + +SIP_SUPPORT = NO + +# For Microsoft's IDL there are propget and propput attributes to indicate +# getter and setter methods for a property. Setting this option to YES will make +# doxygen to replace the get and set methods by a property in the documentation. +# This will only work if the methods are indeed getting or setting a simple +# type. If this is not the case, or you want to show the methods anyway, you +# should set this option to NO. +# The default value is: YES. + +IDL_PROPERTY_SUPPORT = YES + +# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC +# tag is set to YES then doxygen will reuse the documentation of the first +# member in the group (if any) for the other members of the group. By default +# all members of a group must be documented explicitly. +# The default value is: NO. + +DISTRIBUTE_GROUP_DOC = NO + +# If one adds a struct or class to a group and this option is enabled, then also +# any nested class or struct is added to the same group. By default this option +# is disabled and one has to add nested compounds explicitly via \ingroup. +# The default value is: NO. + +GROUP_NESTED_COMPOUNDS = NO + +# Set the SUBGROUPING tag to YES to allow class member groups of the same type +# (for instance a group of public functions) to be put as a subgroup of that +# type (e.g. under the Public Functions section). Set it to NO to prevent +# subgrouping. Alternatively, this can be done per class using the +# \nosubgrouping command. +# The default value is: YES. + +SUBGROUPING = YES + +# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions +# are shown inside the group in which they are included (e.g. using \ingroup) +# instead of on a separate page (for HTML and Man pages) or section (for LaTeX +# and RTF). +# +# Note that this feature does not work in combination with +# SEPARATE_MEMBER_PAGES. +# The default value is: NO. + +INLINE_GROUPED_CLASSES = NO + +# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions +# with only public data fields or simple typedef fields will be shown inline in +# the documentation of the scope in which they are defined (i.e. file, +# namespace, or group documentation), provided this scope is documented. If set +# to NO, structs, classes, and unions are shown on a separate page (for HTML and +# Man pages) or section (for LaTeX and RTF). +# The default value is: NO. + +INLINE_SIMPLE_STRUCTS = NO + +# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or +# enum is documented as struct, union, or enum with the name of the typedef. So +# typedef struct TypeS {} TypeT, will appear in the documentation as a struct +# with name TypeT. When disabled the typedef will appear as a member of a file, +# namespace, or class. And the struct will be named TypeS. This can typically be +# useful for C code in case the coding convention dictates that all compound +# types are typedef'ed and only the typedef is referenced, never the tag name. +# The default value is: NO. + +TYPEDEF_HIDES_STRUCT = NO + +# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This +# cache is used to resolve symbols given their name and scope. Since this can be +# an expensive process and often the same symbol appears multiple times in the +# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small +# doxygen will become slower. If the cache is too large, memory is wasted. The +# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range +# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536 +# symbols. At the end of a run doxygen will report the cache usage and suggest +# the optimal cache size from a speed point of view. +# Minimum value: 0, maximum value: 9, default value: 0. + +LOOKUP_CACHE_SIZE = 0 + +# The NUM_PROC_THREADS specifies the number threads doxygen is allowed to use +# during processing. When set to 0 doxygen will based this on the number of +# cores available in the system. You can set it explicitly to a value larger +# than 0 to get more control over the balance between CPU load and processing +# speed. At this moment only the input processing can be done using multiple +# threads. Since this is still an experimental feature the default is set to 1, +# which efficively disables parallel processing. Please report any issues you +# encounter. Generating dot graphs in parallel is controlled by the +# DOT_NUM_THREADS setting. +# Minimum value: 0, maximum value: 32, default value: 1. + +NUM_PROC_THREADS = 1 + +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- + +# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in +# documentation are documented, even if no documentation was available. Private +# class members and static file members will be hidden unless the +# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES. +# Note: This will also disable the warnings about undocumented members that are +# normally produced when WARNINGS is set to YES. +# The default value is: NO. + +EXTRACT_ALL = NO + +# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will +# be included in the documentation. +# The default value is: NO. + +EXTRACT_PRIVATE = YES + +# If the EXTRACT_PRIV_VIRTUAL tag is set to YES, documented private virtual +# methods of a class will be included in the documentation. +# The default value is: NO. + +EXTRACT_PRIV_VIRTUAL = YES + +# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal +# scope will be included in the documentation. +# The default value is: NO. + +EXTRACT_PACKAGE = YES + +# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be +# included in the documentation. +# The default value is: NO. + +EXTRACT_STATIC = YES + +# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined +# locally in source files will be included in the documentation. If set to NO, +# only classes defined in header files are included. Does not have any effect +# for Java sources. +# The default value is: YES. + +EXTRACT_LOCAL_CLASSES = YES + +# This flag is only useful for Objective-C code. If set to YES, local methods, +# which are defined in the implementation section but not in the interface are +# included in the documentation. If set to NO, only methods in the interface are +# included. +# The default value is: NO. + +EXTRACT_LOCAL_METHODS = NO + +# If this flag is set to YES, the members of anonymous namespaces will be +# extracted and appear in the documentation as a namespace called +# 'anonymous_namespace{file}', where file will be replaced with the base name of +# the file that contains the anonymous namespace. By default anonymous namespace +# are hidden. +# The default value is: NO. + +EXTRACT_ANON_NSPACES = NO + +# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all +# undocumented members inside documented classes or files. If set to NO these +# members will be included in the various overviews, but no documentation +# section is generated. This option has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_MEMBERS = NO + +# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all +# undocumented classes that are normally visible in the class hierarchy. If set +# to NO, these classes will be included in the various overviews. This option +# has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_CLASSES = NO + +# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend +# declarations. If set to NO, these declarations will be included in the +# documentation. +# The default value is: NO. + +HIDE_FRIEND_COMPOUNDS = NO + +# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any +# documentation blocks found inside the body of a function. If set to NO, these +# blocks will be appended to the function's detailed documentation block. +# The default value is: NO. + +HIDE_IN_BODY_DOCS = NO + +# The INTERNAL_DOCS tag determines if documentation that is typed after a +# \internal command is included. If the tag is set to NO then the documentation +# will be excluded. Set it to YES to include the internal documentation. +# The default value is: NO. + +INTERNAL_DOCS = NO + +# If the CASE_SENSE_NAMES tag is set to NO then doxygen will only generate file +# names in lower-case letters. If set to YES, upper-case letters are also +# allowed. This is useful if you have classes or files whose names only differ +# in case and if your file system supports case sensitive file names. Windows +# (including Cygwin) and Mac users are advised to set this option to NO. +# The default value is: system dependent. + +CASE_SENSE_NAMES = NO + +# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with +# their full class and namespace scopes in the documentation. If set to YES, the +# scope will be hidden. +# The default value is: NO. + +HIDE_SCOPE_NAMES = NO + +# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will +# append additional text to a page's title, such as Class Reference. If set to +# YES the compound reference will be hidden. +# The default value is: NO. + +HIDE_COMPOUND_REFERENCE= NO + +# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of +# the files that are included by a file in the documentation of that file. +# The default value is: YES. + +SHOW_INCLUDE_FILES = YES + +# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each +# grouped member an include statement to the documentation, telling the reader +# which file to include in order to use the member. +# The default value is: NO. + +SHOW_GROUPED_MEMB_INC = NO + +# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include +# files with double quotes in the documentation rather than with sharp brackets. +# The default value is: NO. + +FORCE_LOCAL_INCLUDES = NO + +# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the +# documentation for inline members. +# The default value is: YES. + +INLINE_INFO = YES + +# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the +# (detailed) documentation of file and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. +# The default value is: YES. + +SORT_MEMBER_DOCS = NO + +# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief +# descriptions of file, namespace and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. Note that +# this will also influence the order of the classes in the class list. +# The default value is: NO. + +SORT_BRIEF_DOCS = NO + +# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the +# (brief and detailed) documentation of class members so that constructors and +# destructors are listed first. If set to NO the constructors will appear in the +# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS. +# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief +# member documentation. +# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting +# detailed member documentation. +# The default value is: NO. + +SORT_MEMBERS_CTORS_1ST = NO + +# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy +# of group names into alphabetical order. If set to NO the group names will +# appear in their defined order. +# The default value is: NO. + +SORT_GROUP_NAMES = NO + +# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by +# fully-qualified names, including namespaces. If set to NO, the class list will +# be sorted only by class name, not including the namespace part. +# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. +# Note: This option applies only to the class list, not to the alphabetical +# list. +# The default value is: NO. + +SORT_BY_SCOPE_NAME = NO + +# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper +# type resolution of all parameters of a function it will reject a match between +# the prototype and the implementation of a member function even if there is +# only one candidate or it is obvious which candidate to choose by doing a +# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still +# accept a match between prototype and implementation in such cases. +# The default value is: NO. + +STRICT_PROTO_MATCHING = NO + +# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo +# list. This list is created by putting \todo commands in the documentation. +# The default value is: YES. + +GENERATE_TODOLIST = YES + +# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test +# list. This list is created by putting \test commands in the documentation. +# The default value is: YES. + +GENERATE_TESTLIST = YES + +# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug +# list. This list is created by putting \bug commands in the documentation. +# The default value is: YES. + +GENERATE_BUGLIST = YES + +# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO) +# the deprecated list. This list is created by putting \deprecated commands in +# the documentation. +# The default value is: YES. + +GENERATE_DEPRECATEDLIST= YES + +# The ENABLED_SECTIONS tag can be used to enable conditional documentation +# sections, marked by \if ... \endif and \cond +# ... \endcond blocks. + +ENABLED_SECTIONS = + +# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the +# initial value of a variable or macro / define can have for it to appear in the +# documentation. If the initializer consists of more lines than specified here +# it will be hidden. Use a value of 0 to hide initializers completely. The +# appearance of the value of individual variables and macros / defines can be +# controlled using \showinitializer or \hideinitializer command in the +# documentation regardless of this setting. +# Minimum value: 0, maximum value: 10000, default value: 30. + +MAX_INITIALIZER_LINES = 30 + +# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at +# the bottom of the documentation of classes and structs. If set to YES, the +# list will mention the files that were used to generate the documentation. +# The default value is: YES. + +SHOW_USED_FILES = YES + +# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This +# will remove the Files entry from the Quick Index and from the Folder Tree View +# (if specified). +# The default value is: YES. + +SHOW_FILES = YES + +# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces +# page. This will remove the Namespaces entry from the Quick Index and from the +# Folder Tree View (if specified). +# The default value is: YES. + +SHOW_NAMESPACES = YES + +# The FILE_VERSION_FILTER tag can be used to specify a program or script that +# doxygen should invoke to get the current version for each file (typically from +# the version control system). Doxygen will invoke the program by executing (via +# popen()) the command command input-file, where command is the value of the +# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided +# by doxygen. Whatever the program writes to standard output is used as the file +# version. For an example see the documentation. + +FILE_VERSION_FILTER = + +# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed +# by doxygen. The layout file controls the global structure of the generated +# output files in an output format independent way. To create the layout file +# that represents doxygen's defaults, run doxygen with the -l option. You can +# optionally specify a file name after the option, if omitted DoxygenLayout.xml +# will be used as the name of the layout file. +# +# Note that if you run doxygen from a directory containing a file called +# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE +# tag is left empty. + +LAYOUT_FILE =DoxygenLayout.xml + +# The CITE_BIB_FILES tag can be used to specify one or more bib files containing +# the reference definitions. This must be a list of .bib files. The .bib +# extension is automatically appended if omitted. This requires the bibtex tool +# to be installed. See also https://en.wikipedia.org/wiki/BibTeX for more info. +# For LaTeX the style of the bibliography can be controlled using +# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the +# search path. See also \cite for info how to create references. + +CITE_BIB_FILES = + +#--------------------------------------------------------------------------- +# Configuration options related to warning and progress messages +#--------------------------------------------------------------------------- + +# The QUIET tag can be used to turn on/off the messages that are generated to +# standard output by doxygen. If QUIET is set to YES this implies that the +# messages are off. +# The default value is: NO. + +QUIET = NO + +# The WARNINGS tag can be used to turn on/off the warning messages that are +# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES +# this implies that the warnings are on. +# +# Tip: Turn warnings on while writing the documentation. +# The default value is: YES. + +WARNINGS = YES + +# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate +# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag +# will automatically be disabled. +# The default value is: YES. + +WARN_IF_UNDOCUMENTED = YES + +# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for +# potential errors in the documentation, such as not documenting some parameters +# in a documented function, or documenting parameters that don't exist or using +# markup commands wrongly. +# The default value is: YES. + +WARN_IF_DOC_ERROR = YES + +# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that +# are documented, but have no documentation for their parameters or return +# value. If set to NO, doxygen will only warn about wrong or incomplete +# parameter documentation, but not about the absence of documentation. If +# EXTRACT_ALL is set to YES then this flag will automatically be disabled. +# The default value is: NO. + +WARN_NO_PARAMDOC = NO + +# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when +# a warning is encountered. +# The default value is: NO. + +WARN_AS_ERROR = NO + +# The WARN_FORMAT tag determines the format of the warning messages that doxygen +# can produce. The string should contain the $file, $line, and $text tags, which +# will be replaced by the file and line number from which the warning originated +# and the warning text. Optionally the format may contain $version, which will +# be replaced by the version of the file (if it could be obtained via +# FILE_VERSION_FILTER) +# The default value is: $file:$line: $text. + +WARN_FORMAT = "$file:$line: $text" + +# The WARN_LOGFILE tag can be used to specify a file to which warning and error +# messages should be written. If left blank the output is written to standard +# error (stderr). + +WARN_LOGFILE = + +#--------------------------------------------------------------------------- +# Configuration options related to the input files +#--------------------------------------------------------------------------- + +# The INPUT tag is used to specify the files and/or directories that contain +# documented source files. You may enter file names like myfile.cpp or +# directories like /usr/src/myproject. Separate the files or directories with +# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING +# Note: If this tag is empty the current directory is searched. + +INPUT = ./brownbook_readme.md \ + ../PooPyLab/unit_procs \ + ../PooPyLab/ASMModel \ + ../PooPyLab/utils + +# This tag can be used to specify the character encoding of the source files +# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses +# libiconv (or the iconv built into libc) for the transcoding. See the libiconv +# documentation (see: https://www.gnu.org/software/libiconv/) for the list of +# possible encodings. +# The default value is: UTF-8. + +INPUT_ENCODING = UTF-8 + +# If the value of the INPUT tag contains directories, you can use the +# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and +# *.h) to filter out the source-files in the directories. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# read by doxygen. +# +# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp, +# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, +# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc, +# *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C comment), +# *.doc (to be provided as doxygen C comment), *.txt (to be provided as doxygen +# C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd, +# *.vhdl, *.ucf, *.qsf and *.ice. + +FILE_PATTERNS = *.c \ + *.cc \ + *.cxx \ + *.cpp \ + *.c++ \ + *.java \ + *.ii \ + *.ixx \ + *.ipp \ + *.i++ \ + *.inl \ + *.idl \ + *.ddl \ + *.odl \ + *.h \ + *.hh \ + *.hxx \ + *.hpp \ + *.h++ \ + *.cs \ + *.d \ + *.php \ + *.php4 \ + *.php5 \ + *.phtml \ + *.inc \ + *.m \ + *.markdown \ + *.md \ + *.mm \ + *.dox \ + *.doc \ + *.txt \ + *.py \ + *.pyw \ + *.f90 \ + *.f95 \ + *.f03 \ + *.f08 \ + *.f18 \ + *.f \ + *.for \ + *.vhd \ + *.vhdl \ + *.ucf \ + *.qsf \ + *.ice + +# The RECURSIVE tag can be used to specify whether or not subdirectories should +# be searched for input files as well. +# The default value is: NO. + +RECURSIVE = YES + +# The EXCLUDE tag can be used to specify files and/or directories that should be +# excluded from the INPUT source files. This way you can easily exclude a +# subdirectory from a directory tree whose root is specified with the INPUT tag. +# +# Note that relative paths are relative to the directory from which doxygen is +# run. + +EXCLUDE = + +# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or +# directories that are symbolic links (a Unix file system feature) are excluded +# from the input. +# The default value is: NO. + +EXCLUDE_SYMLINKS = NO + +# If the value of the INPUT tag contains directories, you can use the +# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude +# certain files from those directories. +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories for example use the pattern */test/* + +EXCLUDE_PATTERNS = + +# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names +# (namespaces, classes, functions, etc.) that should be excluded from the +# output. The symbol name can be a fully qualified name, a word, or if the +# wildcard * is used, a substring. Examples: ANamespace, AClass, +# AClass::ANamespace, ANamespace::*Test +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories use the pattern */test/* + +EXCLUDE_SYMBOLS = + +# The EXAMPLE_PATH tag can be used to specify one or more files or directories +# that contain example code fragments that are included (see the \include +# command). + +EXAMPLE_PATH = + +# If the value of the EXAMPLE_PATH tag contains directories, you can use the +# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and +# *.h) to filter out the source-files in the directories. If left blank all +# files are included. + +EXAMPLE_PATTERNS = * + +# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be +# searched for input files to be used with the \include or \dontinclude commands +# irrespective of the value of the RECURSIVE tag. +# The default value is: NO. + +EXAMPLE_RECURSIVE = NO + +# The IMAGE_PATH tag can be used to specify one or more files or directories +# that contain images that are to be included in the documentation (see the +# \image command). + +IMAGE_PATH = + +# The INPUT_FILTER tag can be used to specify a program that doxygen should +# invoke to filter for each input file. Doxygen will invoke the filter program +# by executing (via popen()) the command: +# +# +# +# where is the value of the INPUT_FILTER tag, and is the +# name of an input file. Doxygen will then use the output that the filter +# program writes to standard output. If FILTER_PATTERNS is specified, this tag +# will be ignored. +# +# Note that the filter must not add or remove lines; it is applied before the +# code is scanned, but not when the output code is generated. If lines are added +# or removed, the anchors will not be placed correctly. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# properly processed by doxygen. + +INPUT_FILTER = + +# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern +# basis. Doxygen will compare the file name with each pattern and apply the +# filter if there is a match. The filters are a list of the form: pattern=filter +# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how +# filters are used. If the FILTER_PATTERNS tag is empty or if none of the +# patterns match the file name, INPUT_FILTER is applied. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# properly processed by doxygen. + +FILTER_PATTERNS = *.py=./py_filter + +# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using +# INPUT_FILTER) will also be used to filter the input files that are used for +# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES). +# The default value is: NO. + +FILTER_SOURCE_FILES = NO + +# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file +# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and +# it is also possible to disable source filtering for a specific pattern using +# *.ext= (so without naming a filter). +# This tag requires that the tag FILTER_SOURCE_FILES is set to YES. + +FILTER_SOURCE_PATTERNS = + +# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that +# is part of the input, its contents will be placed on the main page +# (index.html). This can be useful if you have a project on for instance GitHub +# and want to reuse the introduction page also for the doxygen output. + +USE_MDFILE_AS_MAINPAGE = ./brownbook_readme.md + +#--------------------------------------------------------------------------- +# Configuration options related to source browsing +#--------------------------------------------------------------------------- + +# If the SOURCE_BROWSER tag is set to YES then a list of source files will be +# generated. Documented entities will be cross-referenced with these sources. +# +# Note: To get rid of all source code in the generated output, make sure that +# also VERBATIM_HEADERS is set to NO. +# The default value is: NO. + +SOURCE_BROWSER = NO + +# Setting the INLINE_SOURCES tag to YES will include the body of functions, +# classes and enums directly into the documentation. +# The default value is: NO. + +INLINE_SOURCES = NO + +# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any +# special comment blocks from generated source code fragments. Normal C, C++ and +# Fortran comments will always remain visible. +# The default value is: YES. + +STRIP_CODE_COMMENTS = YES + +# If the REFERENCED_BY_RELATION tag is set to YES then for each documented +# entity all documented functions referencing it will be listed. +# The default value is: NO. + +REFERENCED_BY_RELATION = NO + +# If the REFERENCES_RELATION tag is set to YES then for each documented function +# all documented entities called/used by that function will be listed. +# The default value is: NO. + +REFERENCES_RELATION = NO + +# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set +# to YES then the hyperlinks from functions in REFERENCES_RELATION and +# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will +# link to the documentation. +# The default value is: YES. + +REFERENCES_LINK_SOURCE = YES + +# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the +# source code will show a tooltip with additional information such as prototype, +# brief description and links to the definition and documentation. Since this +# will make the HTML file larger and loading of large files a bit slower, you +# can opt to disable this feature. +# The default value is: YES. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +SOURCE_TOOLTIPS = YES + +# If the USE_HTAGS tag is set to YES then the references to source code will +# point to the HTML generated by the htags(1) tool instead of doxygen built-in +# source browser. The htags tool is part of GNU's global source tagging system +# (see https://www.gnu.org/software/global/global.html). You will need version +# 4.8.6 or higher. +# +# To use it do the following: +# - Install the latest version of global +# - Enable SOURCE_BROWSER and USE_HTAGS in the configuration file +# - Make sure the INPUT points to the root of the source tree +# - Run doxygen as normal +# +# Doxygen will invoke htags (and that will in turn invoke gtags), so these +# tools must be available from the command line (i.e. in the search path). +# +# The result: instead of the source browser generated by doxygen, the links to +# source code will now point to the output of htags. +# The default value is: NO. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +USE_HTAGS = NO + +# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a +# verbatim copy of the header file for each class for which an include is +# specified. Set to NO to disable this. +# See also: Section \class. +# The default value is: YES. + +VERBATIM_HEADERS = YES + +#--------------------------------------------------------------------------- +# Configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- + +# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all +# compounds will be generated. Enable this if the project contains a lot of +# classes, structs, unions or interfaces. +# The default value is: YES. + +ALPHABETICAL_INDEX = YES + +# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in +# which the alphabetical index list will be split. +# Minimum value: 1, maximum value: 20, default value: 5. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. + +COLS_IN_ALPHA_INDEX = 5 + +# In case all classes in a project start with a common prefix, all classes will +# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag +# can be used to specify a prefix (or a list of prefixes) that should be ignored +# while generating the index headers. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. + +IGNORE_PREFIX = + +#--------------------------------------------------------------------------- +# Configuration options related to the HTML output +#--------------------------------------------------------------------------- + +# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output +# The default value is: YES. + +GENERATE_HTML = YES + +# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a +# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of +# it. +# The default directory is: html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_OUTPUT = html + +# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each +# generated HTML page (for example: .htm, .php, .asp). +# The default value is: .html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FILE_EXTENSION = .html + +# The HTML_HEADER tag can be used to specify a user-defined HTML header file for +# each generated HTML page. If the tag is left blank doxygen will generate a +# standard header. +# +# To get valid HTML the header file that includes any scripts and style sheets +# that doxygen needs, which is dependent on the configuration options used (e.g. +# the setting GENERATE_TREEVIEW). It is highly recommended to start with a +# default header using +# doxygen -w html new_header.html new_footer.html new_stylesheet.css +# YourConfigFile +# and then modify the file new_header.html. See also section "Doxygen usage" +# for information on how to generate the default header that doxygen normally +# uses. +# Note: The header is subject to change so you typically have to regenerate the +# default header when upgrading to a newer version of doxygen. For a description +# of the possible markers and block names see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_HEADER = header.html + +# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each +# generated HTML page. If the tag is left blank doxygen will generate a standard +# footer. See HTML_HEADER for more information on how to generate a default +# footer and what special commands can be used inside the footer. See also +# section "Doxygen usage" for information on how to generate the default footer +# that doxygen normally uses. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FOOTER = footer.html + +# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style +# sheet that is used by each HTML page. It can be used to fine-tune the look of +# the HTML output. If left blank doxygen will generate a default style sheet. +# See also section "Doxygen usage" for information on how to generate the style +# sheet that doxygen normally uses. +# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as +# it is more robust and this tag (HTML_STYLESHEET) will in the future become +# obsolete. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_STYLESHEET = style.css + +# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined +# cascading style sheets that are included after the standard style sheets +# created by doxygen. Using this option one can overrule certain style aspects. +# This is preferred over using HTML_STYLESHEET since it does not replace the +# standard style sheet and is therefore more robust against future updates. +# Doxygen will copy the style sheet files to the output directory. +# Note: The order of the extra style sheet files is of importance (e.g. the last +# style sheet in the list overrules the setting of the previous ones in the +# list). For an example see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_STYLESHEET = + +# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or +# other source files which should be copied to the HTML output directory. Note +# that these files will be copied to the base HTML output directory. Use the +# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these +# files. In the HTML_STYLESHEET file, use the file name only. Also note that the +# files will be copied as-is; there are no commands or markers available. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_FILES = + +# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen +# will adjust the colors in the style sheet and background images according to +# this color. Hue is specified as an angle on a colorwheel, see +# https://en.wikipedia.org/wiki/Hue for more information. For instance the value +# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300 +# purple, and 360 is red again. +# Minimum value: 0, maximum value: 359, default value: 220. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_HUE = 220 + +# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors +# in the HTML output. For a value of 0 the output will use grayscales only. A +# value of 255 will produce the most vivid colors. +# Minimum value: 0, maximum value: 255, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_SAT = 100 + +# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the +# luminance component of the colors in the HTML output. Values below 100 +# gradually make the output lighter, whereas values above 100 make the output +# darker. The value divided by 100 is the actual gamma applied, so 80 represents +# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not +# change the gamma. +# Minimum value: 40, maximum value: 240, default value: 80. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_GAMMA = 80 + +# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML +# page will contain the date and time when the page was generated. Setting this +# to YES can help to show when doxygen was last run and thus if the +# documentation is up to date. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_TIMESTAMP = NO + +# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML +# documentation will contain a main index with vertical navigation menus that +# are dynamically created via JavaScript. If disabled, the navigation index will +# consists of multiple levels of tabs that are statically embedded in every HTML +# page. Disable this option to support browsers that do not have JavaScript, +# like the Qt help browser. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_DYNAMIC_MENUS = YES + +# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML +# documentation will contain sections that can be hidden and shown after the +# page has loaded. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_DYNAMIC_SECTIONS = NO + +# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries +# shown in the various tree structured indices initially; the user can expand +# and collapse entries dynamically later on. Doxygen will expand the tree to +# such a level that at most the specified number of entries are visible (unless +# a fully collapsed tree already exceeds this amount). So setting the number of +# entries 1 will produce a full collapsed tree by default. 0 is a special value +# representing an infinite number of entries and will result in a full expanded +# tree by default. +# Minimum value: 0, maximum value: 9999, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_INDEX_NUM_ENTRIES = 100 + +# If the GENERATE_DOCSET tag is set to YES, additional index files will be +# generated that can be used as input for Apple's Xcode 3 integrated development +# environment (see: https://developer.apple.com/xcode/), introduced with OSX +# 10.5 (Leopard). To create a documentation set, doxygen will generate a +# Makefile in the HTML output directory. Running make will produce the docset in +# that directory and running make install will install the docset in +# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at +# startup. See https://developer.apple.com/library/archive/featuredarticles/Doxy +# genXcode/_index.html for more information. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_DOCSET = NO + +# This tag determines the name of the docset feed. A documentation feed provides +# an umbrella under which multiple documentation sets from a single provider +# (such as a company or product suite) can be grouped. +# The default value is: Doxygen generated docs. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_FEEDNAME = "Doxygen generated docs" + +# This tag specifies a string that should uniquely identify the documentation +# set bundle. This should be a reverse domain-name style string, e.g. +# com.mycompany.MyDocSet. Doxygen will append .docset to the name. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_BUNDLE_ID = org.doxygen.Project + +# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify +# the documentation publisher. This should be a reverse domain-name style +# string, e.g. com.mycompany.MyDocSet.documentation. +# The default value is: org.doxygen.Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_ID = org.doxygen.Publisher + +# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher. +# The default value is: Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_NAME = Publisher + +# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three +# additional HTML index files: index.hhp, index.hhc, and index.hhk. The +# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop +# (see: https://www.microsoft.com/en-us/download/details.aspx?id=21138) on +# Windows. +# +# The HTML Help Workshop contains a compiler that can convert all HTML output +# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML +# files are now used as the Windows 98 help format, and will replace the old +# Windows help format (.hlp) on all Windows platforms in the future. Compressed +# HTML files also contain an index, a table of contents, and you can search for +# words in the documentation. The HTML workshop also contains a viewer for +# compressed HTML files. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_HTMLHELP = NO + +# The CHM_FILE tag can be used to specify the file name of the resulting .chm +# file. You can add a path in front of the file if the result should not be +# written to the html output directory. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_FILE = + +# The HHC_LOCATION tag can be used to specify the location (absolute path +# including file name) of the HTML help compiler (hhc.exe). If non-empty, +# doxygen will try to run the HTML help compiler on the generated index.hhp. +# The file has to be specified with full path. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +HHC_LOCATION = + +# The GENERATE_CHI flag controls if a separate .chi index file is generated +# (YES) or that it should be included in the main .chm file (NO). +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +GENERATE_CHI = NO + +# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc) +# and project file content. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_INDEX_ENCODING = + +# The BINARY_TOC flag controls whether a binary table of contents is generated +# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it +# enables the Previous and Next buttons. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +BINARY_TOC = NO + +# The TOC_EXPAND flag can be set to YES to add extra items for group members to +# the table of contents of the HTML help documentation and to the tree view. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +TOC_EXPAND = NO + +# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and +# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that +# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help +# (.qch) of the generated HTML documentation. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_QHP = NO + +# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify +# the file name of the resulting .qch file. The path specified is relative to +# the HTML output folder. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QCH_FILE = + +# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help +# Project output. For more information please see Qt Help Project / Namespace +# (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace). +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_NAMESPACE = org.doxygen.Project + +# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt +# Help Project output. For more information please see Qt Help Project / Virtual +# Folders (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual- +# folders). +# The default value is: doc. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_VIRTUAL_FOLDER = doc + +# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom +# filter to add. For more information please see Qt Help Project / Custom +# Filters (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom- +# filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_NAME = + +# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the +# custom filter to add. For more information please see Qt Help Project / Custom +# Filters (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom- +# filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_ATTRS = + +# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this +# project's filter section matches. Qt Help Project / Filter Attributes (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#filter-attributes). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_SECT_FILTER_ATTRS = + +# The QHG_LOCATION tag can be used to specify the location of Qt's +# qhelpgenerator. If non-empty doxygen will try to run qhelpgenerator on the +# generated .qhp file. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHG_LOCATION = + +# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be +# generated, together with the HTML files, they form an Eclipse help plugin. To +# install this plugin and make it available under the help contents menu in +# Eclipse, the contents of the directory containing the HTML and XML files needs +# to be copied into the plugins directory of eclipse. The name of the directory +# within the plugins directory should be the same as the ECLIPSE_DOC_ID value. +# After copying Eclipse needs to be restarted before the help appears. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_ECLIPSEHELP = NO + +# A unique identifier for the Eclipse help plugin. When installing the plugin +# the directory name containing the HTML and XML files should also have this +# name. Each documentation set should have its own identifier. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES. + +ECLIPSE_DOC_ID = org.doxygen.Project + +# If you want full control over the layout of the generated HTML pages it might +# be necessary to disable the index and replace it with your own. The +# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top +# of each HTML page. A value of NO enables the index and the value YES disables +# it. Since the tabs in the index contain the same information as the navigation +# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +DISABLE_INDEX = YES + +# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index +# structure should be generated to display hierarchical information. If the tag +# value is set to YES, a side panel will be generated containing a tree-like +# index structure (just like the one that is generated for HTML Help). For this +# to work a browser that supports JavaScript, DHTML, CSS and frames is required +# (i.e. any modern browser). Windows users are probably better off using the +# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can +# further fine-tune the look of the index. As an example, the default style +# sheet generated by doxygen has an example that shows how to put an image at +# the root of the tree instead of the PROJECT_NAME. Since the tree basically has +# the same information as the tab index, you could consider setting +# DISABLE_INDEX to YES when enabling this option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_TREEVIEW = YES + +# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that +# doxygen will group on one line in the generated HTML documentation. +# +# Note that a value of 0 will completely suppress the enum values from appearing +# in the overview section. +# Minimum value: 0, maximum value: 20, default value: 4. +# This tag requires that the tag GENERATE_HTML is set to YES. + +ENUM_VALUES_PER_LINE = 4 + +# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used +# to set the initial width (in pixels) of the frame in which the tree is shown. +# Minimum value: 0, maximum value: 1500, default value: 250. +# This tag requires that the tag GENERATE_HTML is set to YES. + +TREEVIEW_WIDTH = 350 + +# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to +# external symbols imported via tag files in a separate window. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +EXT_LINKS_IN_WINDOW = NO + +# If the HTML_FORMULA_FORMAT option is set to svg, doxygen will use the pdf2svg +# tool (see https://github.com/dawbarton/pdf2svg) or inkscape (see +# https://inkscape.org) to generate formulas as SVG images instead of PNGs for +# the HTML output. These images will generally look nicer at scaled resolutions. +# Possible values are: png (the default) and svg (looks nicer but requires the +# pdf2svg or inkscape tool). +# The default value is: png. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FORMULA_FORMAT = png + +# Use this tag to change the font size of LaTeX formulas included as images in +# the HTML documentation. When you change the font size after a successful +# doxygen run you need to manually remove any form_*.png images from the HTML +# output directory to force them to be regenerated. +# Minimum value: 8, maximum value: 50, default value: 10. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_FONTSIZE = 10 + +# Use the FORMULA_TRANSPARENT tag to determine whether or not the images +# generated for formulas are transparent PNGs. Transparent PNGs are not +# supported properly for IE 6.0, but are supported on all modern browsers. +# +# Note that when changing this option you need to delete any form_*.png files in +# the HTML output directory before the changes have effect. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_TRANSPARENT = YES + +# The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands +# to create new LaTeX commands to be used in formulas as building blocks. See +# the section "Including formulas" for details. + +FORMULA_MACROFILE = + +# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see +# https://www.mathjax.org) which uses client side JavaScript for the rendering +# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX +# installed or if you want to formulas look prettier in the HTML output. When +# enabled you may also need to install MathJax separately and configure the path +# to it using the MATHJAX_RELPATH option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +USE_MATHJAX = NO + +# When MathJax is enabled you can set the default output format to be used for +# the MathJax output. See the MathJax site (see: +# http://docs.mathjax.org/en/latest/output.html) for more details. +# Possible values are: HTML-CSS (which is slower, but has the best +# compatibility), NativeMML (i.e. MathML) and SVG. +# The default value is: HTML-CSS. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_FORMAT = HTML-CSS + +# When MathJax is enabled you need to specify the location relative to the HTML +# output directory using the MATHJAX_RELPATH option. The destination directory +# should contain the MathJax.js script. For instance, if the mathjax directory +# is located at the same level as the HTML output directory, then +# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax +# Content Delivery Network so you can quickly see the result without installing +# MathJax. However, it is strongly recommended to install a local copy of +# MathJax from https://www.mathjax.org before deployment. +# The default value is: https://cdn.jsdelivr.net/npm/mathjax@2. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_RELPATH = https://cdn.jsdelivr.net/npm/mathjax@2 + +# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax +# extension names that should be enabled during MathJax rendering. For example +# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_EXTENSIONS = + +# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces +# of code that will be used on startup of the MathJax code. See the MathJax site +# (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an +# example see the documentation. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_CODEFILE = + +# When the SEARCHENGINE tag is enabled doxygen will generate a search box for +# the HTML output. The underlying search engine uses javascript and DHTML and +# should work on any modern browser. Note that when using HTML help +# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET) +# there is already a search function so this one should typically be disabled. +# For large projects the javascript based search engine can be slow, then +# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to +# search using the keyboard; to jump to the search box use + S +# (what the is depends on the OS and browser, but it is typically +# , /