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
+# , /