diff --git a/docs/source/conf.py b/docs/source/conf.py index cc735083..20cc44a7 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -31,7 +31,7 @@ # built documents. # # The short X.Y version. -version = '1.4.0' +version = '1.4.1' # The full version, including alpha/beta/rc tags. release = version diff --git a/qsdsan/_process.py b/qsdsan/_process.py index 5a5e768b..824e6ed1 100644 --- a/qsdsan/_process.py +++ b/qsdsan/_process.py @@ -1041,8 +1041,8 @@ def load_from_file(cls, path='', components=None, data=None, stoichio = proc[cmp_IDs] if data.columns[-1] in cmp_IDs: rate_eq = None else: - if pd.isna(proc[-1]): rate_eq = None - else: rate_eq = proc[-1] + if pd.isna(proc.iloc[-1]): rate_eq = None + else: rate_eq = proc.iloc[-1] stoichio = stoichio[-pd.isna(stoichio)].to_dict() ref = None for k,v in stoichio.items(): diff --git a/qsdsan/_sanunit.py b/qsdsan/_sanunit.py index 51cac1a7..4788b965 100644 --- a/qsdsan/_sanunit.py +++ b/qsdsan/_sanunit.py @@ -217,6 +217,7 @@ def __init__(self, ID='', ins=None, outs=(), thermo=None, init_with='WasteStream self._assert_compatible_property_package() self._utility_cost = None + self._recycle_system = None ##### qsdsan-specific ##### for i in (*construction, *transportation, *equipment): diff --git a/qsdsan/_waste_stream.py b/qsdsan/_waste_stream.py index ca737b98..4700c430 100644 --- a/qsdsan/_waste_stream.py +++ b/qsdsan/_waste_stream.py @@ -154,7 +154,11 @@ def __init__(self, dct, F_vol, MW, phase, phase_container): def output(self, index, value): '''Concentration flows, in mg/L (g/m3).''' f_mass = value * self.MW[index] - phase = self.phase or self.phase_container.phase + if self.phase: + phase = self.phase + else: + try: phase = self.phase_container._phase + except: phase = self.phase_container if phase != 'l': raise AttributeError('Concentration only valid for liquid phase.') V_sum = self.F_vol @@ -186,7 +190,7 @@ def by_conc(self, TP): check_data=False, ) return conc -indexer.ChemicalMolarFlowIndexer.by_conc = by_conc +ChemicalMolarFlowIndexer.by_conc = by_conc del by_conc @@ -450,7 +454,7 @@ def _wastestream_info(self, details=True, concentrations=None, N=15): _ws_info += '\n' # Only non-zero properties are shown _ws_info += int(bool(self.pH))*f' pH : {self.pH:.1f}\n' - _ws_info += int(bool(self.SAlk))*f' Alkalinity : {self.SAlk:.1f} mg/L\n' + _ws_info += int(bool(self.SAlk))*f' Alkalinity : {self.SAlk:.1f} mmol/L\n' if details: _ws_info += int(bool(self.COD)) *f' COD : {self.COD:.1f} mg/L\n' _ws_info += int(bool(self.BOD)) *f' BOD : {self.BOD:.1f} mg/L\n' @@ -1299,7 +1303,7 @@ def codstates_inf_model(cls, ID='', flow_tot=0., units = ('L/hr', 'mg/L'), ... 9.96e+04 WasteStream-specific properties: pH : 7.0 - Alkalinity : 10.0 mg/L + Alkalinity : 10.0 mmol/L COD : 430.0 mg/L BOD : 221.8 mg/L TC : 265.0 mg/L @@ -1522,7 +1526,7 @@ def codbased_inf_model(cls, ID='', flow_tot=0., units = ('L/hr', 'mg/L'), ... 9.96e+04 WasteStream-specific properties: pH : 7.0 - Alkalinity : 10.0 mg/L + Alkalinity : 10.0 mmol/L COD : 430.0 mg/L BOD : 249.4 mg/L TC : 265.0 mg/L @@ -1751,7 +1755,7 @@ def bodbased_inf_model(cls, ID='', flow_tot=0., units = ('L/hr', 'mg/L'), ... 9.96e+04 WasteStream-specific properties: pH : 7.0 - Alkalinity : 10.0 mg/L + Alkalinity : 10.0 mmol/L COD : 431.0 mg/L BOD : 250.0 mg/L TC : 264.9 mg/L @@ -1983,7 +1987,7 @@ def sludge_inf_model(cls, ID='', flow_tot=0., units = ('L/hr', 'mg/L'), ... 9.88e+04 WasteStream-specific properties: pH : 7.0 - Alkalinity : 10.0 mg/L + Alkalinity : 10.0 mmol/L COD : 10814.4 mg/L BOD : 1744.3 mg/L TC : 4246.5 mg/L diff --git a/qsdsan/data/process_data/_pm2asm2d.tsv b/qsdsan/data/process_data/_pm2asm2d.tsv new file mode 100644 index 00000000..7bc0ff7f --- /dev/null +++ b/qsdsan/data/process_data/_pm2asm2d.tsv @@ -0,0 +1,39 @@ + X_CHL X_ALG X_CH X_LI S_CO2 S_A S_F S_O2 S_NH S_NO S_P X_N_ALG X_P_ALG S_N2 S_ALK S_I X_I X_S X_H X_AUT +photoadaptation 1 +ammonium_uptake -1 1 +phosphorus_uptake -1 1 +growth_pho 1 ? 1 ? ? +carbohydrate_storage_pho 1 ? 1 +lipid_storage_pho 1 ? 1 +carbohydrate_growth_pho 1 (-Y_CH_PHO/Y_X_ALG_PHO) ? ? ? ? +lipid_growth_pho 1 (-Y_LI_PHO/Y_X_ALG_PHO) ? ? ? ? +carbohydrate_maintenance_pho -1 ? -1 +lipid_maintenance_pho -1 ? -1 +endogenous_respiration_pho -1 ? -1 ? ? +growth_ace 1 ? (-1)/Y_X_ALG_HET_ACE ? ? ? +carbohydrate_storage_ace 1 ? (-1)/Y_CH_ND_HET_ACE ? +lipid_storage_ace 1 ? (-1)/Y_LI_ND_HET_ACE ? +carbohydrate_growth_ace 1 (-Y_CH_NR_HET_ACE/Y_X_ALG_HET_ACE) ? ? ? ? +lipid_growth_ace 1 (-Y_LI_NR_HET_ACE/Y_X_ALG_HET_ACE) ? ? ? ? +carbohydrate_maintenance_ace -1 ? -1 +lipid_maintenance_ace -1 ? -1 +endogenous_respiration_ace -1 ? -1 ? ? +growth_glu 1 ? (-1)/Y_X_ALG_HET_GLU ? ? ? +carbohydrate_storage_glu 1 ? (-1)/Y_CH_ND_HET_GLU ? +lipid_storage_glu 1 ? (-1)/Y_LI_ND_HET_GLU ? +carbohydrate_growth_glu 1 (-Y_CH_NR_HET_GLU/Y_X_ALG_HET_GLU) ? ? ? ? +lipid_growth_glu 1 (-Y_LI_NR_HET_GLU/Y_X_ALG_HET_GLU) ? ? ? ? +carbohydrate_maintenance_glu -1 ? -1 +lipid_maintenance_glu -1 ? -1 +endogenous_respiration_glu -1 ? -1 ? ? +aero_hydrolysis 1-f_SI ? ? ? f_SI -1 +anox_hydrolysis 1-f_SI ? ? ? f_SI -1 +anae_hydrolysis 1-f_SI ? ? ? f_SI -1 +hetero_growth_S_F (-1)/Y_H 1-1/Y_H ? ? ? 1 +hetero_growth_S_A (-1)/Y_H 1-1/Y_H ? ? ? 1 +denitri_S_F (-1)/Y_H ? (Y_H-1)/(20/7*Y_H) ? (1-Y_H)/(20/7*Y_H) ? 1 +denitri_S_A (-1)/Y_H ? (Y_H-1)/(20/7*Y_H) ? (1-Y_H)/(20/7*Y_H) ? 1 +ferment 1 -1 ? ? ? +hetero_lysis ? ? ? f_XI_H 1-f_XI_H -1 +auto_aero_growth (Y_A-32/7)/Y_A ? 1/Y_A ? ? 1 +auto_lysis ? ? ? f_XI_AUT 1-f_XI_AUT -1 diff --git a/qsdsan/data/process_data/_pm2asm2d_1.tsv b/qsdsan/data/process_data/_pm2asm2d_1.tsv new file mode 100644 index 00000000..f3a977f5 --- /dev/null +++ b/qsdsan/data/process_data/_pm2asm2d_1.tsv @@ -0,0 +1,28 @@ + X_CHL X_ALG X_CH X_LI S_CO2 S_A S_F S_O2 S_NH S_NO S_P X_N_ALG X_P_ALG S_N2 S_ALK S_I X_I X_S X_H X_AUT +photoadaptation 1 +ammonium_uptake -1 1 +phosphorus_uptake -1 1 +growth_pho 1 ? 1 ? ? +carbohydrate_storage_pho 1 ? 1 +lipid_storage_pho 1 ? 1 +carbohydrate_growth_pho 1 (-Y_CH_PHO/Y_X_ALG_PHO) ? ? ? ? +lipid_growth_pho 1 (-Y_LI_PHO/Y_X_ALG_PHO) ? ? ? ? +carbohydrate_maintenance_pho -1 ? -1 +lipid_maintenance_pho -1 ? -1 +endogenous_respiration_pho -1 ? -1 ? ? +growth_ace 1 ? (-1)/Y_X_ALG_HET_ACE ? ? ? +carbohydrate_storage_ace 1 ? (-1)/Y_CH_ND_HET_ACE ? +lipid_storage_ace 1 ? (-1)/Y_LI_ND_HET_ACE ? +carbohydrate_growth_ace 1 (-Y_CH_NR_HET_ACE/Y_X_ALG_HET_ACE) ? ? ? ? +lipid_growth_ace 1 (-Y_LI_NR_HET_ACE/Y_X_ALG_HET_ACE) ? ? ? ? +carbohydrate_maintenance_ace -1 ? -1 +lipid_maintenance_ace -1 ? -1 +endogenous_respiration_ace -1 ? -1 ? ? +growth_glu 1 ? (-1)/Y_X_ALG_HET_GLU ? ? ? +carbohydrate_storage_glu 1 ? (-1)/Y_CH_ND_HET_GLU ? +lipid_storage_glu 1 ? (-1)/Y_LI_ND_HET_GLU ? +carbohydrate_growth_glu 1 (-Y_CH_NR_HET_GLU/Y_X_ALG_HET_GLU) ? ? ? ? +lipid_growth_glu 1 (-Y_LI_NR_HET_GLU/Y_X_ALG_HET_GLU) ? ? ? ? +carbohydrate_maintenance_glu -1 ? -1 +lipid_maintenance_glu -1 ? -1 +endogenous_respiration_glu -1 ? -1 ? ? diff --git a/qsdsan/data/process_data/_pm2asm2d_2.tsv b/qsdsan/data/process_data/_pm2asm2d_2.tsv new file mode 100644 index 00000000..8941b263 --- /dev/null +++ b/qsdsan/data/process_data/_pm2asm2d_2.tsv @@ -0,0 +1,12 @@ + X_CHL X_ALG X_CH X_LI S_CO2 S_A S_F S_O2 S_NH S_NO S_P X_N_ALG X_P_ALG S_N2 S_ALK S_I X_I X_S X_H X_AUT +aero_hydrolysis 1-f_SI ? ? ? f_SI -1 +anox_hydrolysis 1-f_SI ? ? ? f_SI -1 +anae_hydrolysis 1-f_SI ? ? ? f_SI -1 +hetero_growth_S_F (-1)/Y_H 1-1/Y_H ? ? ? 1 +hetero_growth_S_A (-1)/Y_H 1-1/Y_H ? ? ? 1 +denitri_S_F (-1)/Y_H ? (Y_H-1)/(20/7*Y_H) ? (1-Y_H)/(20/7*Y_H) ? 1 +denitri_S_A (-1)/Y_H ? (Y_H-1)/(20/7*Y_H) ? (1-Y_H)/(20/7*Y_H) ? 1 +ferment 1 -1 ? ? ? +hetero_lysis ? ? ? f_XI_H 1-f_XI_H -1 +auto_aero_growth (Y_A-32/7)/Y_A ? 1/Y_A ? ? 1 +auto_lysis ? ? ? f_XI_AUT 1-f_XI_AUT -1 diff --git a/qsdsan/processes/__init__.py b/qsdsan/processes/__init__.py index ba9beecb..2d2c7b80 100644 --- a/qsdsan/processes/__init__.py +++ b/qsdsan/processes/__init__.py @@ -10,6 +10,8 @@ Joy Zhang + Ga-Yeong Kim + This module is under the University of Illinois/NCSA Open Source License. Please refer to https://github.com/QSD-Group/QSDsan/blob/main/LICENSE.txt for license details. @@ -59,6 +61,7 @@ def __init__(self): #%% from ._aeration import * +from ._aerobic_digestion_addon import * from ._asm1 import * from ._asm2d import * from ._adm1 import * @@ -67,9 +70,11 @@ def __init__(self): from ._decay import * from ._kinetic_reaction import * from ._pm2 import * +from ._pm2asm2d import * from . import ( _aeration, + _aerobic_digestion_addon, _asm1, _asm2d, _adm1, @@ -77,11 +82,13 @@ def __init__(self): # _madm1, _decay, _kinetic_reaction, - _pm2 + _pm2, + _pm2asm2d, ) __all__ = ( *_aeration.__all__, + *_aerobic_digestion_addon.__all__, *_asm1.__all__, *_asm2d.__all__, *_adm1.__all__, @@ -90,4 +97,5 @@ def __init__(self): *_decay.__all__, *_kinetic_reaction.__all__, *_pm2.__all__, - ) + *_pm2asm2d.__all__, + ) \ No newline at end of file diff --git a/qsdsan/processes/_adm1_p_extension.py b/qsdsan/processes/_adm1_p_extension.py index aa4d1818..b850db4b 100644 --- a/qsdsan/processes/_adm1_p_extension.py +++ b/qsdsan/processes/_adm1_p_extension.py @@ -1037,17 +1037,18 @@ def __new__(cls, components=None, path=None, #!!! new parameter KS_IP*P_mw, np.array(k_mmp), Ksp_mass, np.array(K_dis), K_AlOH, K_FeOH])) - - def dydt_Sh2_AD(S_h2, state_arr, h, params, f_stoichio, V_liq, S_h2_in): + + def adm1p_dydt_Sh2_AD(S_h2, state_arr, h, params, f_stoichio, V_liq, S_h2_in): state_arr[7] = S_h2 Q = state_arr[45] rxn = _rhos_adm1p(state_arr, params, h=h) stoichio = f_stoichio(state_arr) # should return the stoichiometric coefficients of S_h2 for all processes return Q/V_liq*(S_h2_in - S_h2) + np.dot(rxn, stoichio) + grad_rhosp = np.zeros(5) X_biop = np.zeros(5) - def grad_dydt_Sh2_AD(S_h2, state_arr, h, params, f_stoichio, V_liq, S_h2_in): + def adm1p_grad_dydt_Sh2_AD(S_h2, state_arr, h, params, f_stoichio, V_liq, S_h2_in): state_arr[7] = S_h2 ks = params['rate_constants'][[5,6,7,8,10]] Ks = params['half_sat_coeffs'][2:6] @@ -1055,17 +1056,19 @@ def grad_dydt_Sh2_AD(S_h2, state_arr, h, params, f_stoichio, V_liq, S_h2_in): pH_ULs = params['pH_ULs'] pH_LLs = params['pH_LLs'] KS_IN = params['KS_IN'] + KS_IP = params['KS_IP'] KIs_h2 = params['KIs_h2'] kLa = params['kLa'] X_biop[:] = state_arr[[18,19,19,20,22]] substrates = state_arr[2:6] - S_va, S_bu, S_IN = state_arr[[3,4,10]] + S_va, S_bu, S_IN, S_IP = state_arr[[3,4,10,11]] Iph = Hill_inhibit(h, pH_ULs, pH_LLs)[[2,3,4,5,7]] Iin = substr_inhibit(S_IN, KS_IN) + Iip = substr_inhibit(S_IP, KS_IP) grad_Ih2 = grad_non_compet_inhibit(S_h2, KIs_h2) - grad_rhosp[:] = ks * X_biop * Iph * Iin + grad_rhosp[:] = ks * X_biop * Iph * Iin * Iip grad_rhosp[:-1] *= substr_inhibit(substrates, Ks) * grad_Ih2 if S_va > 0: grad_rhosp[1] *= 1/(1+S_bu/S_va) if S_bu > 0: grad_rhosp[2] *= 1/(1+S_va/S_bu) @@ -1074,12 +1077,12 @@ def grad_dydt_Sh2_AD(S_h2, state_arr, h, params, f_stoichio, V_liq, S_h2_in): stoichio = f_stoichio(state_arr) Q = state_arr[45] - return -Q/V_liq + np.dot(grad_rhos, stoichio[[5,6,7,8,10]]) + kLa*stoichio[-3] + return -Q/V_liq + np.dot(grad_rhosp, stoichio[[5,6,7,8,10]]) + kLa*stoichio[-3] dct['flex_rhos'] = _rhos_adm1p dct['solve_pH'] = adm1p_solve_pH - dct['dydt_Sh2_AD'] = dydt_Sh2_AD - dct['grad_dydt_Sh2_AD'] = grad_dydt_Sh2_AD + dct['dydt_Sh2_AD'] = adm1p_dydt_Sh2_AD + dct['grad_dydt_Sh2_AD'] = adm1p_grad_dydt_Sh2_AD return self def set_half_sat_K(self, K, process): diff --git a/qsdsan/processes/_aerobic_digestion_addon.py b/qsdsan/processes/_aerobic_digestion_addon.py new file mode 100644 index 00000000..bfdd267a --- /dev/null +++ b/qsdsan/processes/_aerobic_digestion_addon.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +''' +QSDsan: Quantitative Sustainable Design for sanitation and resource recovery systems + +This module is developed by: + Joy Zhang + +This module is under the University of Illinois/NCSA Open Source License. +Please refer to https://github.com/QSD-Group/QSDsan/blob/main/LICENSE.txt +for license details. +''' +import numpy as np +from qsdsan import Process +from thermosteam import settings +_load_components = settings.get_default_chemicals + +__all__ = ('ASM_AeDigAddOn',) + +class ASM_AeDigAddOn(Process): + ''' + Creates a `Process` object representing the degradation of particulate + inert organic materials that typically occur in an aerobic digester. + Stoichiometry is determined by rules of element conservation in corresponding + activated sludge models. + + Parameters + ---------- + k_dig : float, optional + The 1st-order degradation rate constant, in d^(-1). The default is 0.04. + + See Also + -------- + :class:`qsdsan.processes.ASM1` + :class:`qsdsan.processes.ASM2d` + :class:`qsdsan.processes.mASM2d` + + Examples + -------- + >>> import qsdsan.processes as pc + >>> cmps_asm1 = pc.create_asm1_cmps() + >>> dig_asm1 = pc.ASM_AeDigAddOn('dig_asm1') + >>> dig_asm1.show() + Process: dig_asm1 + [stoichiometry] X_I: -1 + X_S: 1 + S_NH: 0.06 + S_ALK: 0.0514 + [reference] X_I + [rate equation] X_I*k_dig + [parameters] k_dig: 0.04 + [dynamic parameters] + + >>> cmps_masm2d = pc.create_masm2d_cmps(set_thermo=False) + >>> dig_masm2d = pc.ASM_AeDigAddOn('dig_masm2d', components=cmps_masm2d) + >>> dig_masm2d.show() + Process: dig_masm2d + [stoichiometry] S_NH4: 0.0265 + S_PO4: 0.0009 + S_IC: 0.0433 + X_I: -1 + X_S: 1 + [reference] X_I + [rate equation] X_I*k_dig + [parameters] k_dig: 0.04 + [dynamic parameters] + + ''' + + def __init__(self, ID, k_dig=0.04, components=None): + cmps = _load_components(components) + rxn = 'X_I -> X_S' + consrv = [] + if 'S_ALK' in cmps.IDs: + consrv.append('charge') + rxn += ' + [?]S_ALK' + elif 'S_IC' in cmps.IDs: + consrv.append('C') + rxn += ' + [?]S_IC' + + if 'S_NH' in cmps.IDs: + consrv.append('N') + rxn += ' + [?]S_NH' + elif 'S_NH4' in cmps.IDs: + consrv.append('N') + rxn += ' + [?]S_NH4' + + if 'S_PO4' in cmps.IDs: + consrv.append('P') + rxn += ' +[?]S_PO4' + + super().__init__(ID=ID, reaction=rxn, + rate_equation='k_dig*X_I', + ref_component='X_I', + components=cmps, + conserved_for=consrv, + parameters=('k_dig',)) + self.k_dig=k_dig + self._stoichiometry = np.asarray(self._stoichiometry, dtype=float) + + @property + def k_dig(self): + '''[float] Degradation rate constant, in d^(-1).''' + return self._k + @k_dig.setter + def k_dig(self, k): + self._k = k + self.set_parameters(k_dig=k) \ No newline at end of file diff --git a/qsdsan/processes/_asm2d.py b/qsdsan/processes/_asm2d.py index c2a99b7e..3783b80e 100644 --- a/qsdsan/processes/_asm2d.py +++ b/qsdsan/processes/_asm2d.py @@ -12,7 +12,7 @@ import numpy as np from thermosteam.utils import chemicals_user from thermosteam import settings -from qsdsan import Component, Components, Processes, CompiledProcesses +from qsdsan import Component, Components, WasteStream, Processes, CompiledProcesses from ..utils import ospath, data_path, load_data from . import Monod, ion_speciation from scipy.optimize import brenth @@ -20,7 +20,8 @@ __all__ = ('create_asm2d_cmps', 'ASM2d', - 'create_masm2d_cmps', 'mASM2d') + 'create_masm2d_cmps', 'mASM2d', + 'create_masm2d_inf') _path = ospath.join(data_path, 'process_data/_asm2d.tsv') _load_components = settings.get_default_chemicals @@ -990,3 +991,113 @@ def set_pKsps(self, ps): K *= m2m**abs(v) Ksp_mass.append(K) self.rate_function._params['Ksp'] = np.array(Ksp_mass) + +#%% + +def create_masm2d_inf( + ID, Q, Q_unit='m3/d', T=298.15, + COD=430, NH4_N=25.0, PO4_P=8.0, alkalinity=7.0, + fr_SI=0.05, fr_SF=0.2, fr_SA=0.0, + fr_XI=0.13, fr_XH=0.0, fr_XAUT=0.0, + fr_XPAO=0.0, fr_XPHA=0.0, X_PP=0.0, + S_NO3=0.0, S_O2=0.0, S_N2=18, + S_Ca=140, S_Mg=50, S_K=28, S_Na=87, S_Cl=425, + X_AlOH=0, X_AlPO4=0, X_FeOH=0, X_FePO4=0, + X_CaCO3=0, X_ACP=0, X_MgCO3=0, X_newb=0, X_struv=0 + ): + ''' + Convenient function to create an influent `WasteStream` object with `mASM2d` + state variables based on specified bulk properties. + + Parameters + ---------- + ID : str + Unique identification for the `WasteStream` object. + Q : float + Total volumetric flow rate. + Q_unit : str, optional + Unit of measurement for flow rate. The default is 'm3/d'. + T : float, optional + Temperature, in K. The default is 298.15. + COD : float, optional + Total chemical oxygen demand, not accounting for electron acceptors like + dissvoled O2 or nitrate/nitrite, in mg-COD/L. The default is 430. + NH4_N : float, optional + Ammonium nitrogen, in mg-N/L. The default is 25.0. + PO4_P : float, optional + Ortho-phosphate, in mg-P/L. The default is 8.0. + alkalinity : float, optional + In mmol/L. The default is 7.0. + fr_SI : float, optional + Soluble inert fraction of total COD. The default is 0.05. + fr_SF : float, optional + Fermentable biodegradable fraction of total COD. The default is 0.2. + fr_SA : float, optional + VFA fraction of total COD. The default is 0.0. + fr_XI : float, optional + Particulate inert fraction of total COD. The default is 0.13. + fr_XH : float, optional + Heterotrophic biomass fraction of total COD. The default is 0.0. + fr_XAUT : float, optional + Autotrophic biomass fraction of total COD. The default is 0.0. + fr_XPAO : float, optional + Phosphorus accumulating biomass fraction of total COD. The default is 0.0. + fr_XPHA : float, optional + PHA fraction of total COD. The default is 0.0. + X_PP : float, optional + Poly-phosphate in mg-P/L. The default is 0.0. + S_NO3 : float, optional + Nitrate and nitrite in mg-N/L. The default is 0.0. + S_O2 : float, optional + Dissolved oxygen in mg-O2/L. The default is 0.0. + S_N2 : float, optional + Dissolved nitrogen gas in mg-N/L. The default is 18. + S_Ca : float, optional + Total soluble calcium in mg-Ca/L. The default is 140. + S_Mg : float, optional + Total soluble magnesium in mg-Mg/L. The default is 50. + S_K : float, optional + Total soluble potassium in mg-K/L. The default is 28. + S_Na : float, optional + Other cation, in mg-Na/L. The default is 87. + S_Cl : float, optional + Other anion, in mg-Cl/L. The default is 425. + X_AlOH : float, optional + Aluminum hydroxide [mg/L]. The default is 0. + X_AlPO4 : float, optional + Aluminum phosphate [mg/L]. The default is 0. + X_FeOH : float, optional + Iron hydroxide [mg/L]. The default is 0. + X_FePO4 : float, optional + Iron phosphate [mg/L]. The default is 0. + X_CaCO3 : float, optional + Calcium carbonate [mg/L]. The default is 0. + X_ACP : float, optional + Calcium phosphate [mg/L]. The default is 0. + X_MgCO3 : float, optional + Magnesium carbonate [mg/L]. The default is 0. + X_newb : float, optional + Newbryite [mg/L]. The default is 0. + X_struv : float, optional + Struvite [mg/L]. The default is 0. + + ''' + + fr_xs = 1.0-fr_SI-fr_SF-fr_SA-fr_XI-fr_XH-fr_XAUT-fr_XPAO-fr_XPHA + if fr_xs < 0: + raise ValueError('The sum of all COD fractions of organic materials must ' + 'not exceed 1.') + + inf = WasteStream(ID, T=T, SAlk=alkalinity) + concs = dict( + S_NH4=NH4_N, S_PO4=PO4_P, S_IC=alkalinity*12, + S_I=COD*fr_SI, S_F=COD*fr_SF, S_A=COD*fr_SA, + X_S=COD*fr_xs, X_I=COD*fr_XI, X_H=COD*fr_XH, + X_AUT=COD*fr_XAUT, X_PAO=COD*fr_XPAO, X_PHA=COD*fr_XPHA, + X_PP=X_PP, S_NO3=S_NO3, S_O2=S_O2, S_N2=S_N2, + S_Ca=S_Ca, S_Mg=S_Mg, S_K=S_K, S_Na=S_Na, S_Cl=S_Cl, + X_AlOH=X_AlOH, X_AlPO4=X_AlPO4, X_FeOH=X_FeOH, X_FePO4=X_FePO4, + X_CaCO3=X_CaCO3, X_ACP=X_ACP, X_MgCO3=X_MgCO3, X_newb=X_newb, X_struv=X_struv + ) + inf.set_flow_by_concentration(Q, concs, (Q_unit, 'mg/L')) + return inf diff --git a/qsdsan/processes/_pm2asm2d.py b/qsdsan/processes/_pm2asm2d.py new file mode 100644 index 00000000..223c10bd --- /dev/null +++ b/qsdsan/processes/_pm2asm2d.py @@ -0,0 +1,957 @@ +# -*- coding: utf-8 -*- +''' +QSDsan: Quantitative Sustainable Design for sanitation and resource recovery systems + +This module is developed by: + Ga-Yeong Kim + Joy Zhang + +This module is under the University of Illinois/NCSA Open Source License. +Please refer to https://github.com/QSD-Group/QSDsan/blob/main/LICENSE.txt +for license details. +''' +# from thermosteam.utils import chemicals_user +from thermosteam import settings +from qsdsan import Component, Components, Process, Processes, CompiledProcesses +from qsdsan.utils import ospath, data_path +import numpy as np + +__all__ = ('create_pm2asm2d_cmps', 'PM2ASM2d') + +_path = ospath.join(data_path, 'process_data/_pm2asm2d_1.tsv') +_path_2 = ospath.join(data_path, 'process_data/_pm2asm2d_2.tsv') + +# _load_components = settings.get_default_chemicals + +#%% +# ============================================================================= +# PM2ASM2d-specific components +# ============================================================================= + +def create_pm2asm2d_cmps(set_thermo=True): + cmps = Components.load_default() + + # X_CHL (g Chl/m^3) + X_CHL = Component(ID = 'X_CHL', + formula = 'C55H72MgN4O5', + description = 'Chlorophyll content of cells', + particle_size = 'Particulate', + degradability = 'Slowly', + organic = True) + + # X_ALG (g COD/m^3) + X_ALG = cmps.X_OHO.copy('X_ALG') + X_ALG.description = 'Concentration of carbon-accumulating mixotrophic organisms' + X_ALG.formula = 'CH1.8O0.5N0.2P0.018' + X_ALG.f_BOD5_COD = X_ALG.f_uBOD_COD = None + X_ALG.f_Vmass_Totmass = 0.89 + + # X_CH (g COD/m^3) + X_CH = cmps.X_GAO_Gly.copy('X_CH') + X_CH.description = 'Concentration of stored carbohydrates' + X_CH.formula = 'CH2O' + X_CH.f_BOD5_COD = X_CH.f_uBOD_COD = None + + # X_LI (g COD/m^3) + X_LI = cmps.X_GAO_Gly.copy('X_LI') + X_LI.description = 'Concentration of stored lipids' + X_LI.formula = 'CH1.92O0.118' + X_LI.f_BOD5_COD = X_LI.f_uBOD_COD = None + + # S_CO2 (g CO2/m^3) + S_CO2 = Component.from_chemical(ID = 'S_CO2', + chemical = 'CO2', + description = 'Soluble carbon dioxide', + particle_size = 'Soluble', + degradability = 'Undegradable', + organic = False) + + # S_A (g COD/m^3) + S_A = cmps.S_Ac.copy('S_A') + S_A.description = 'Concentration of extracellular dissolved organic carbon (acetate)' + + # S_F (g COD/m^3) + S_F = Component.from_chemical(ID = 'S_F', + chemical = 'glucose', + description = 'Concentration of extracellular dissolved organic carbon (glucose)', + measured_as = 'COD', + particle_size = 'Soluble', + degradability = 'Readily', + organic = True) + + # S_O2 (g O2/m^3) + S_O2 = cmps.S_O2.copy('S_O2') + S_O2.description = ('Concentration of dissolved oxygen') + + # S_NH (g N/m^3) + S_NH = cmps.S_NH4.copy('S_NH') + S_NH.description = ('Concentration of dissolved ammonium') + + # S_NO (g N/m^3) + S_NO = cmps.S_NO3.copy('S_NO') + S_NO.description = ('Concentration of dissolved nitrate/nitrite') + + # S_P (g P/m^3) + S_P = cmps.S_PO4.copy('S_P') + S_P.description = ('Concentration of dissolved phosphorus') + + # X_N_ALG (g N/m^3) + X_N_ALG = cmps.X_B_Subst.copy('X_N_ALG') + X_N_ALG.description = 'Concentration of algal cell-associated nitrogen' + X_N_ALG.measured_as = 'N' + X_N_ALG.i_C = X_N_ALG.i_P = X_N_ALG.i_COD = X_N_ALG.f_BOD5_COD = X_N_ALG.f_uBOD_COD = X_N_ALG.f_Vmass_Totmass = 0 + X_N_ALG.i_mass = 1 + + # X_P_ALG (g P/m^3) + X_P_ALG = cmps.X_B_Subst.copy('X_P_ALG') + X_P_ALG.description = 'Concentration of algal cell-associated phosphorus' + X_P_ALG.measured_as = 'P' + X_P_ALG.i_C = X_P_ALG.i_N = X_P_ALG.i_COD = X_P_ALG.f_BOD5_COD = X_P_ALG.f_uBOD_COD = X_P_ALG.f_Vmass_Totmass = 0 + X_P_ALG.i_mass = 1 + + '''added from asm2d''' + # S_N2 (g N/m^3) + S_N2 = cmps.S_N2.copy('S_N2') + S_N2.description = ('Concentration of dinitrogen') + + # S_ALK (g C/m^3) + S_ALK = cmps.S_CO3.copy('S_ALK') # measured as g C, not as mole HCO3- + S_ALK.description = ('Concentration of alkalinity') + + # S_I (g COD/m^3) + S_I = cmps.S_U_E.copy('S_I') + S_I.description = ('Concentration of inert soluble organic material') + + # X_I (g COD/m^3) + X_I = cmps.X_U_OHO_E.copy('X_I') + X_I.description = ('Concentration of inert particulate organic material') + + # X_S (g COD/m^3) + X_S = cmps.X_B_Subst.copy('X_S') + X_S.description = ('Concentration of slowly biodegradable substrates') + + # X_H (g COD/m^3) + X_H = cmps.X_OHO.copy('X_H') + X_H.description = ('Concentration of heterotrophic organisms (including denitrifer)') + + # X_AUT (g COD/m^3) + X_AUT = cmps.X_AOO.copy('X_AUT') + X_AUT.description = ('Concentration of nitrifying organisms') + + S_I.i_N = 0.01 + # S_F.i_N = 0.03 + X_I.i_N = 0.02 + X_S.i_N = 0.04 + X_H.i_N = X_AUT.i_N = 0.07 + + S_I.i_P = 0.00 + # S_F.i_P = 0.01 + X_I.i_P = 0.01 + X_S.i_P = 0.01 + X_H.i_P = X_AUT.i_P = 0.02 + + X_I.i_mass = 0.75 + X_S.i_mass = 0.75 + X_H.i_mass = X_AUT.i_mass = 0.9 + + cmps_pm2asm2d = Components([X_CHL, X_ALG, X_CH, X_LI, S_CO2, S_A, S_F, + S_O2, S_NH, S_NO, S_P, X_N_ALG, X_P_ALG, + S_N2, S_ALK, S_I, X_I, X_S, X_H, X_AUT, cmps.H2O]) + + cmps_pm2asm2d.default_compile() + + if set_thermo: settings.set_thermo(cmps_pm2asm2d) + return cmps_pm2asm2d + +# create_pm2asm2d_cmps() + +#%% +# ============================================================================= +# kinetic rate functions +# ============================================================================= + +# Calculation of ratio +def ratio(numerator, denominator, minimum, maximum): + return min(max(minimum, numerator / denominator), maximum) + +# Calculation of 'I_0' (for initial sensitivity analysis using calculated I_0) +def calc_irrad(t): + ''' + :param t: time [days] + :return: I_0, calculated irradiance [uE/m^2/s] + + -Assumes 14 hours of daylight + ''' + daylight_hours = 14.0 # hours + start_time = (12.0 - daylight_hours / 2) / 24.0 # days + end_time = (12.0 + daylight_hours / 2) / 24.0 # days + if t-np.floor(t) < start_time or t-np.floor(t) > end_time: + return 0 + else: + return 400.0 * (np.sin(2 * np.pi * (((t - np.floor(t)) - 5 / 24) / (14 / 24)) - np.pi / 2) + 1) / 2 + +# Calculation of 'I' from 'I_0' (Beer-Lambert) +def attenuation(light, X_TSS, a_c, b_reactor): + ''' + :param light: I_0, calculated irradiance from 'calc_irrad' method (for sensitivity analysis) or + photosynthetically active radiation (PAR) imported from input excel file (for calibration & validation) [uE/m^2/s] + :param X_TSS: total biomass concentration (X_ALG + X_CH + X_LI) * i_mass [g TSS/m^3] + :param a_c: PAR absorption coefficient on a TSS (total suspended solids) basis [m^2/g TSS] + :parma b_reactor: thickness of reactor along light path [m] + :return: I, depth-averaged irradiance [uE/m^2/s] + ''' + if X_TSS > 0: + i_avg = (light * (1 - np.exp(-a_c * X_TSS * b_reactor))) / (a_c * X_TSS * b_reactor) + return min(i_avg, light) + else: + return light + +# Calculation of 'f_I' from 'I' (Eilers & Peeters) +def irrad_response(i_avg, X_CHL, X_carbon, I_n, I_opt): + ''' + :param i_avg: I, depth-averaged irradiance (calculated from 'attenuation' method) [uE/m^2/s] + :param X_CHL: chlorophyll content of cells [g Chl/m^3] + :param X_carbon: carbon content of cells (X_ALG + X_CH + X_LI) * i_C [g C/m^3] + :param I_n: maximum incident PAR irradiance (“irradiance at noon”) [uE/m^2/s] + :param I_opt: optimal irradiance [uE/m^2/s] + :return: f_I, irradiance response function [unitless] + ''' + if X_carbon > 0: + f_I = i_avg / (i_avg + I_n * (0.25 - (5 * X_CHL/X_carbon)) * ((i_avg ** 2 / I_opt ** 2) - (2 * i_avg / I_opt) + 1)) + return min(1, max(0, f_I)) + else: + return 0 + +# Droop model +def droop(quota, subsistence_quota, exponent): + ''' + :param quota: Q_N or Q_P [g N or g P/g COD] + :param subsistence_quota: Q_N_min or Q_P_min [g N or g P/g COD] + :param exponent: exponent to allow for more rapid transitions from growth to storage (see Guest et al., 2013) [unitless] + :return: rate [unitless] + ''' + return 1 - (subsistence_quota / quota) ** exponent + +# Monod model +def monod(substrate, half_sat_const, exponent): + ''' + :param substrate: S_NH, S_NO or S_P [g N or g P/m^3] + :param half_sat_const: K_N or K_P [g N or g P/m^3] + :param exponent: exponent to allow for more rapid transitions from growth to storage (see Guest et al., 2013) [unitless] + :return: rate [unitless] + ''' + return (substrate / (half_sat_const + substrate)) ** exponent + +# Temperature model (Arrhenius) +def temperature(temp, arr_a, arr_e): + ''' + :param temp: temperature (will be imported from input excel file) [K] + :param arr_a: arrhenius constant (A) (Goldman et al., 1974) [unitless] + :param arr_e: arrhenius exponential constant (E/R) (Goldman et al., 1974) [K] + :return: temperature component of overall growth equation [unitless] + ''' + return arr_a * np.exp(-arr_e / temp) # Used equation from Goldman et al., 1974 + +# Photoadaptation (_p1) +def photoadaptation(i_avg, X_CHL, X_carbon, I_n, k_gamma): + ''' + :param i_avg: I, depth-averaged irradiance (calculated from 'attenuation' method) [uE/m^2/s] + :param X_CHL: chlorophyll content of cells [g Chl/m^3] + :param X_carbon: carbon content of cells (X_ALG + X_CH + X_LI) * i_C [g C/m^3] + :param I_n: maximum incident PAR irradiance (“irradiance at noon”) [uE/m^2/s] + :param k_gamma: photoadaptation coefficient [unitless] + :return: photoadaptation rate [g Chl/m^3/d] + ''' + if X_carbon > 0: + return 24 * ((0.2 * i_avg / I_n) / (k_gamma + (i_avg / I_n))) *\ + (0.01 + 0.03 * ((np.log(i_avg / I_n + 0.005)) / (np.log(0.01))) - X_CHL/X_carbon) * X_carbon + else: return 0 + +# Nutrients uptake (_p2, _p3, _p4, _p5, _p6) +def nutrient_uptake(X_ALG, quota, substrate, uptake_rate, half_sat_const, maximum_quota, subsistence_quota): + ''' + :param X_ALG: algae biomass concentration (i.e., no storage products) [g COD/m^3] + :param quota: Q_N or Q_P [g N or g P/g COD] + :param substrate: S_NH, S_NO or S_P [g N or g P/m^3] + :param uptake_rate: V_NH, V_NO or V_P [g N or g P/g COD/d] + :param half_sat_const: K_N or K_P [g N or g P/m^3] + :param maximum_quota: Q_N_max or Q_P_max [g N or g P/g COD] + :param subsistence_quota: Q_N_min or Q_P_min [g N or g P/g COD] + :return: nutrient uptake rate [g N or g P/m^3/d] + ''' + return uptake_rate * monod(substrate, half_sat_const, 1) * X_ALG * \ + ((maximum_quota - quota) / (maximum_quota - subsistence_quota)) ** 0.01 + +# Maximum total photoautotrophic or heterotrophic-acetate or heterotrophic-glucose growth rate (_p7, _p10, _p11, _p15, _p18, _p19, _p23, _p26, _p27) +def max_total_growth(X_ALG, mu_max, f_np, f_temp): + ''' + :param X_ALG: algae biomass concentration (i.e., no storage products) [g COD/m^3] + :param mu_max: maximum specific growth rate [d^(-1)] + :param f_np: inhibition factor by nitrogen or phosphorus (between 0 and 1) [unitless] + :param f_temp: temperature correction factor (between 0 and 1) [unitless] + :return: maximum total growth rate for a particular mechanism, + without considering carbon source or light inhibition (= product of shared terms in growth-related equations) [g COD/m^3/d] + ''' + return mu_max * f_np * X_ALG * f_temp + +# Split the total growth rate between three processes (_p7, _p10, _p11, _p15, _p18, _p19, _p23, _p26, _p27) +def growth_split(f_I, f_CH, f_LI, rho, Y_CH, Y_LI, K_STO): + ''' + :param f_I: irradiance response function (calculated from 'irrad_response' method) [unitless] + :param f_CH: ratio of stored carbohydrates to cells (X_CH / X_ALG) [g COD/g COD] + :param f_LI: ratio of stored lipids to cells (X_LI / X_ALG) [g COD/g COD] + :param rho: carbohydrate relative preference factor (calibrated in Guest et al., 2013) [unitless] + :param Y_CH: yield of storage carbohydrates (as polyglucose, PG), Y_CH_PHO, Y_CH_NR_HET_ACE, or Y_CH_NR_HET_GLU [g COD/g COD] + :param Y_LI: yield of storage lipids (as triacylglycerol, TAG), Y_LI_PHO, Y_LI_NR_HET_ACE, or Y_LI_NR_HET_GLU [g COD/g COD] + :param K_STO: half-saturation constant for stored organic carbon (calibrated in Guest et al., 2013) [g COD/g COD] + :return: splits the total growth rate between three processes, + growth, growth on stored carbohydrates, and growth on stored lipid (= process-specific terms) [unitless] + ''' + numerators = np.asarray([K_STO * (1 - f_I), rho * f_CH, f_LI * Y_CH / Y_LI]) + return numerators/(sum(numerators)) + +# Part of storage equations (_p8, _p9, _p16, _p17, _p24, _p25) +def storage_saturation(f, f_max, beta): + ''' + :param f: f_CH or f_LI [g COD/g COD] + :param f_max: f_CH_max or f_LI_max [g COD/g COD] + :param beta: beta_1 or beta_2 [unitless] + :return: part of storage equations [unitless] + ''' + return 1 - (f / f_max) ** beta + +# Maximum total photoautotrophic or heterotrophic-acetate or heterotrophic-glucose maintenance rate (_p12, _p13, _p14, _p20, _p21, _p22, _p28, _p29, _p30) +def max_total_maintenance(X_ALG, m_ATP): + ''' + :param X_ALG: algae biomass concentration (i.e., no storage products) [g COD/m^3] + :param m_ATP: specific maintenance rate [g ATP/g COD/d] + :return: maximum total maintenance rate for a particular mechanism + (= product of shared terms in maintenance-related equations) [g COD/m^3/d] + ''' + return m_ATP * X_ALG + +# Split the total maintenance rate between three processes (_p12, _p13, _p14, _p20, _p21, _p22, _p28, _p29, _p30) +def maintenance_split(f_CH, f_LI, rho, Y_CH, Y_LI, Y_X_ALG, Y_ATP, K_STO): + ''' + :param f_CH: ratio of stored carbohydrates to cells (X_CH / X_ALG) [g COD/g COD] + :param f_LI: ratio of stored lipids to cells (X_LI / X_ALG) [g COD/g COD] + :param rho: carbohydrate relative preference factor (calibrated in Guest et al., 2013) [unitless] + :param Y_CH: yield of storage carbohydrates (as polyglucose, PG), Y_CH_PHO, Y_CH_NR_HET_ACE, or Y_CH_NR_HET_GLU [g COD/g COD] + :param Y_LI: yield of storage lipids (as triacylglycerol, TAG), Y_LI_PHO, Y_LI_NR_HET_ACE, or Y_LI_NR_HET_GLU [g COD/g COD] + :param Y_X_ALG: yield of carbon-accumulating phototrophic organisms, Y_X_ALG_PHO, Y_X_ALG_HET_ACE, or Y_X_ALG_HET_GLU [g COD/g COD] + :param Y_ATP: yield of ATP, Y_ATP_PHO, Y_ATP_HET_ACE, or Y_ATP_HET_GLU [g ATP/g COD] + :param K_STO: half-saturation constant for stored organic carbon (calibrated in Guest et al., 2013) [g COD/g COD] + :return: splits the total maintenance rate between three processes, + stored carbohydrate degradation, stored lipid degradation, and endogenous respiration (= process-specific terms) [unitless] + ''' + numerators = np.asarray([rho * f_CH, f_LI * Y_CH / Y_LI, K_STO]) + yield_ratios = np.asarray([Y_CH, Y_LI, Y_X_ALG]) / Y_ATP + return numerators/(sum(numerators)) * yield_ratios + +# Storage of carbohydrate/lipid (_p8, _p9, _p16, _p17, _p24, _p25) +def storage(X_ALG, f_np, response, saturation, storage_rate): + ''' + :param X_ALG: algae biomass concentration (i.e., no storage products) [g COD/m^3] + :param f_np: inhibition factor by nitrogen or phosphorus (between 0 and 1) [unitless] + :param response: f_I (irradiance response function, calculated from 'irrad_response' method), acetate_response (monod(S_A, K_A, 1)), or glucose_response (monod(S_F, K_F, 1)) [unitless] + :param saturation: 1 - (f / f_max) ** beta (calculated from 'storage_saturation' method) [unitless] + :param storage_rate: q_CH or q_LI [g COD/g COD/d] + :return: storage rate [g COD/m^3/d] + ''' + return storage_rate * saturation * (1 - f_np) * response * X_ALG + +'''added from asm2d''' + +# Hydrolysis (_p31, _p32, _p33) +def hydrolysis(X_S, X_H, K_h, K_X): + ''' + :param X_S: concentration of slowly biodegradable substrates + :param X_H: concentration of heterotrophic organisms (including denitrifer) + :param K_h: hydrolysis rate constant + :param K_X: slowly biodegradable substrate half saturation coefficient for hydrolysis + :return: shared parts of hydrolysis equations + ''' + return K_h * (X_S/X_H) / (K_X + X_S/X_H) * X_H + +# Growth in ASM2d (_p34, _p35, _p36, _p37, _p40) +def growth_asm2d(S_NH, S_P, S_ALK, mu, X, K_NH4, K_P, K_ALK): + ''' + :param S_NH: concentration of dissolved ammonium + :param S_P: concentration of dissolved phosphorus + :param S_ALK: concentration of alkalinity + :param mu: maximum specific growth rate (mu_H or mu_AUT) + :param X: concentration of biomass (X_H or X_AUT) + :param K_NH4: ammonium (nutrient) half saturation coefficient (K_NH4_H or K_NH4_AUT) + :param K_P: phosphorus (nutrient) half saturation coefficient (K_P_H or K_P_AUT) + :param K_ALK: alkalinity half saturation coefficient (K_ALK_H or K_ALK_AUT) + :return: shared parts of growth-related equations + ''' + return mu * S_NH/(K_NH4+S_NH) * S_P/(K_P+S_P) * S_ALK/(K_ALK + S_ALK) * X + +def rhos_pm2asm2d(state_arr, params): + + # extract values of state variables + c_arr = state_arr[:21] + temp = state_arr[22] + light = state_arr[23] # imported from input file assumed + + # Q = state_arr[21] # Flow rate + # t = state_arr[22] # time + + X_CHL, X_ALG, X_CH, X_LI, S_CO2, S_A, S_F, S_O2, S_NH, S_NO, S_P, X_N_ALG, X_P_ALG, S_N2, S_ALK, S_I, X_I, X_S, X_H, X_AUT, H2O = c_arr + + # extract values of parameters + cmps = params['cmps'] + a_c = params['a_c'] + I_n = params['I_n'] + arr_a = params['arr_a'] + arr_e = params['arr_e'] + beta_1 = params['beta_1'] + beta_2 = params['beta_2'] + b_reactor = params['b_reactor'] + I_opt = params['I_opt'] + k_gamma = params['k_gamma'] + K_N = params['K_N'] + K_P = params['K_P'] + K_A = params['K_A'] + K_F = params['K_F'] + rho = params['rho'] + K_STO = params['K_STO'] + f_CH_max = params['f_CH_max'] + f_LI_max = params['f_LI_max'] + m_ATP = params['m_ATP'] + mu_max = params['mu_max'] + q_CH = params['q_CH'] + q_LI = params['q_LI'] + Q_N_max = params['Q_N_max'] + Q_N_min = params['Q_N_min'] + Q_P_max = params['Q_P_max'] + Q_P_min = params['Q_P_min'] + V_NH = params['V_NH'] + V_NO = params['V_NO'] + V_P = params['V_P'] + exponent = params['exponent'] + Y_ATP_PHO = params['Y_ATP_PHO'] + Y_CH_PHO = params['Y_CH_PHO'] + Y_LI_PHO = params['Y_LI_PHO'] + Y_X_ALG_PHO = params['Y_X_ALG_PHO'] + Y_ATP_HET_ACE = params['Y_ATP_HET_ACE'] + Y_CH_NR_HET_ACE = params['Y_CH_NR_HET_ACE'] + Y_LI_NR_HET_ACE = params['Y_LI_NR_HET_ACE'] + Y_X_ALG_HET_ACE = params['Y_X_ALG_HET_ACE'] + Y_ATP_HET_GLU = params['Y_ATP_HET_GLU'] + Y_CH_NR_HET_GLU = params['Y_CH_NR_HET_GLU'] + Y_LI_NR_HET_GLU = params['Y_LI_NR_HET_GLU'] + Y_X_ALG_HET_GLU = params['Y_X_ALG_HET_GLU'] + n_dark = params['n_dark'] + + '''added from asm2d''' + # f_SI = params['f_SI'] + # Y_H = params['Y_H'] + # f_XI_H = params['f_XI_H'] + # Y_A = params['Y_A'] + # f_XI_AUT = params['f_XI_AUT'] + K_h = params['K_h'] + eta_NO3 = params['eta_NO3'] + eta_fe = params['eta_fe'] + K_O2 = params['K_O2'] + K_NO3 = params['K_NO3'] + K_X = params['K_X'] + mu_H = params['mu_H'] + q_fe = params['q_fe'] + eta_NO3_H = params['eta_NO3_H'] + b_H = params['b_H'] + K_O2_H = params['K_O2_H'] + K_F_H = params['K_F_H'] # K_F overlaps with PM2 -> change into K_F_H + K_fe = params['K_fe'] + K_A_H = params['K_A_H'] + K_NO3_H = params['K_NO3_H'] + K_NH4_H = params['K_NH4_H'] + K_P_H = params['K_P_H'] + K_ALK_H = params['K_ALK_H'] + mu_AUT = params['mu_AUT'] + b_AUT = params['b_AUT'] + K_O2_AUT = params['K_O2_AUT'] + K_NH4_AUT = params['K_NH4_AUT'] + K_ALK_AUT = params['K_ALK_AUT'] + K_P_AUT = params['K_P_AUT'] + +# intermediate variables + f_CH = ratio(X_CH, X_ALG, 0, f_CH_max) + f_LI = ratio(X_LI, X_ALG, 0, f_LI_max) + + # Q_N = ratio(X_N_ALG, X_ALG, Q_N_min, Q_N_max) + # Q_P = ratio(X_P_ALG, X_ALG, Q_P_min, Q_P_max) + + alg_iN, alg_iP = cmps.X_ALG.i_N, cmps.X_ALG.i_P + Q_N = ratio(X_N_ALG+X_ALG*alg_iN, X_ALG, Q_N_min, Q_N_max) + Q_P = ratio(X_P_ALG+X_ALG*alg_iP, X_ALG, Q_P_min, Q_P_max) + + idx = cmps.indices(['X_ALG', 'X_CH', 'X_LI']) + X_bio = np.array([X_ALG, X_CH, X_LI]) + X_TSS = sum(X_bio * cmps.i_mass[idx]) + X_carbon = sum(X_bio * cmps.i_C[idx]) + + i_avg = attenuation(light, X_TSS, a_c, b_reactor) + f_I = irrad_response(i_avg, X_CHL, X_carbon, I_n, I_opt) + dark_response = max(f_I, n_dark) + acetate_response = monod(S_A, K_A, 1) + glucose_response = monod(S_F, K_F, 1) + + f_np = min(droop(Q_N, Q_N_min, exponent), droop(Q_P, Q_P_min, exponent)) + f_temp = temperature(temp, arr_a, arr_e) + + f_sat_CH = storage_saturation(f_CH, f_CH_max, beta_1) + f_sat_LI = storage_saturation(f_LI, f_LI_max, beta_2) + + max_total_growth_rho = max_total_growth(X_ALG, mu_max, f_np, f_temp) + max_maintenance_rho = max_total_maintenance(X_ALG, m_ATP) + # light = calc_irrad(t) + + # calculate kinetic rate values + rhos = np.empty(41) + + rhos[0] = photoadaptation(i_avg, X_CHL, X_carbon, I_n, k_gamma) + + rhos[1] = nutrient_uptake(X_ALG, Q_N, S_NH, V_NH, K_N, Q_N_max, Q_N_min) + rhos[[2,3,4]] = nutrient_uptake(X_ALG, Q_N, S_NO, V_NO, K_N, Q_N_max, Q_N_min) * (K_N/(K_N + S_NH)) + + rhos[5] = nutrient_uptake(X_ALG, Q_P, S_P, V_P, K_P, Q_P_max, Q_P_min) + + rhos[[6,9,10]] = max_total_growth_rho \ + * growth_split(f_I, f_CH, f_LI, rho, Y_CH_PHO, Y_LI_PHO, K_STO) + rhos[6] *= f_I + rhos[[9,10]] *= dark_response + + rhos[[14,17,18]] = max_total_growth_rho \ + * acetate_response \ + * growth_split(f_I, f_CH, f_LI, rho, Y_CH_NR_HET_ACE, Y_LI_NR_HET_ACE, K_STO) + + rhos[[22,25,26]] = max_total_growth_rho \ + * glucose_response \ + * growth_split(f_I, f_CH, f_LI, rho, Y_CH_NR_HET_GLU, Y_LI_NR_HET_GLU, K_STO) + + rhos[[11,12,13]] = max_maintenance_rho \ + * maintenance_split(f_CH, f_LI, rho, Y_CH_PHO, Y_LI_PHO, Y_X_ALG_PHO, Y_ATP_PHO, K_STO) + + rhos[[19,20,21]] = max_maintenance_rho \ + * maintenance_split(f_CH, f_LI, rho, Y_CH_NR_HET_ACE, Y_LI_NR_HET_ACE, Y_X_ALG_HET_ACE, Y_ATP_HET_ACE, K_STO) + + rhos[[27,28,29]] = max_maintenance_rho \ + * maintenance_split(f_CH, f_LI, rho, Y_CH_NR_HET_GLU, Y_LI_NR_HET_GLU, Y_X_ALG_HET_GLU, Y_ATP_HET_GLU, K_STO) + + rhos[7] = storage(X_ALG, f_np, f_I, f_sat_CH, q_CH) + rhos[8] = storage(X_ALG, f_np, f_I, f_sat_LI, q_LI) * (f_CH / f_CH_max) + rhos[15] = storage(X_ALG, f_np, acetate_response, f_sat_CH, q_CH) + rhos[16] = storage(X_ALG, f_np, acetate_response, f_sat_LI, q_LI) * (f_CH / f_CH_max) + rhos[23] = storage(X_ALG, f_np, glucose_response, f_sat_CH, q_CH) + rhos[24] = storage(X_ALG, f_np, glucose_response, f_sat_LI, q_LI) * (f_CH / f_CH_max) + + rhos[30] = hydrolysis(X_S, X_H, K_h, K_X) * monod(S_O2, K_O2, 1) + rhos[31] = hydrolysis(X_S, X_H, K_h, K_X) * eta_NO3 * monod(K_O2, S_O2, 1) * monod(S_NO, K_NO3, 1) + rhos[32] = hydrolysis(X_S, X_H, K_h, K_X) * eta_fe * monod(K_O2, S_O2, 1) * monod(K_NO3, S_NO, 1) + + rhos[33] = growth_asm2d(S_NH, S_P, S_ALK, mu_H, X_H, K_NH4_H, K_P_H, K_ALK_H) * monod(S_O2, K_O2_H, 1) * monod(S_F, K_F_H, 1) * monod(S_F, S_A, 1) + rhos[34] = growth_asm2d(S_NH, S_P, S_ALK, mu_H, X_H, K_NH4_H, K_P_H, K_ALK_H) * monod(S_O2, K_O2_H, 1) * monod(S_A, K_A_H, 1) * monod(S_A, S_F, 1) + rhos[35] = growth_asm2d(S_NH, S_P, S_ALK, mu_H, X_H, K_NH4_H, K_P_H, K_ALK_H) * eta_NO3_H * monod(K_O2_H, S_O2, 1) * monod(S_NO, K_NO3_H, 1) * monod(S_F, K_F_H, 1) * monod(S_F, S_A, 1) + rhos[36] = growth_asm2d(S_NH, S_P, S_ALK, mu_H, X_H, K_NH4_H, K_P_H, K_ALK_H) * eta_NO3_H * monod(K_O2_H, S_O2, 1) * monod(S_NO, K_NO3_H, 1) * monod(S_A, K_A_H, 1) * monod(S_A, S_F, 1) + rhos[37] = q_fe * monod(K_O2_H, S_O2, 1) * monod(K_NO3_H, S_NO, 1) * monod(S_F, K_fe, 1) * monod(S_ALK, K_ALK_H, 1) * X_H + rhos[38] = b_H * X_H + + rhos[39] = growth_asm2d(S_NH, S_P, S_ALK, mu_AUT, X_AUT, K_NH4_AUT, K_P_AUT, K_ALK_AUT) * monod(S_O2, K_O2_AUT, 1) + rhos[40] = b_AUT * X_AUT + + return rhos + +#%% +# ============================================================================= +# PM2ASM2d class +# ============================================================================= + +class PM2ASM2d(CompiledProcesses): + ''' + Parameters + ---------- + components: class:`CompiledComponents`, optional + Components corresponding to each entry in the stoichiometry array, + defaults to thermosteam.settings.chemicals. + a_c : float, optional + PAR absorption coefficient on a TSS (total suspended solids) basis, in [m^2/g TSS]. + The default is 0.049. + I_n : float, optional + Maximum incident PAR irradiance (“irradiance at noon”), in [uE/m^2/s]. + The default is 250. + arr_a : float, optional + Arrhenius constant (A), in [unitless]. + The default is 1.8 * 10**10. + arr_e : float, optional + Arrhenius exponential constant (E/R), in [K]. + The default is 6842. + beta_1 : float, optional + Power coefficient for carbohydrate storage inhibition, in [unitless]. + The default is 2.90. + beta_2 : float, optional + Power coefficient for lipid storage inhibition, in [unitless]. + The default is 3.50. + b_reactor : float, optional + Thickness of reactor along light path, in [m]. + The default is 0.03. + I_opt : float, optional + Optimal irradiance, in [uE/m^2/s]. + The default is 300. + k_gamma : float, optional + Photoadaptation coefficient, in [unitless]. + The default is 0.00001. + K_N : float, optional + Nitrogen half-saturation constant, in [g N/m^3]. + The default is 0.1. + K_P : float, optional + Phosphorus half-saturation constant, in [g P/m^3]. + The default is 1.0. + K_A : float, optional + Organic carbon half-saturation constant (acetate) (Wagner, 2016), in [g COD/m^3]. + The default is 6.3. + K_F : float, optional + Organic carbon half-saturation constant (glucose); assumes K_A = K_F, in [g COD/m^3]. + The default is 6.3. + rho : float, optional + Carbohydrate relative preference factor (calibrated in Guest et al., 2013), in [unitless]. + The default is 1.186. + K_STO : float, optional + Half-saturation constant for stored organic carbon (calibrated in Guest et al., 2013), in [g COD/g COD]. + The default is 1.566. + f_CH_max : float, optional + Maximum achievable ratio of stored carbohydrates to functional cells, in [g COD/g COD]. + The default is 0.819. + f_LI_max : float, optional + Maximum achievable ratio of stored lipids to functional cells, in [g COD/g COD]. + The default is 3.249. + m_ATP : float, optional + Specific maintenance rate, in [g ATP/g COD/d]. + The default is 15.835. + mu_max : float, optional + Maximum specific growth rate, in [d^(-1)]. + The default is 1.969. + q_CH : float, optional + Maximum specific carbohydrate storage rate, in [g COD/g COD/d]. + The default is 0.594. + q_LI : float, optional + Maximum specific lipid storage rate, in [g COD/g COD/d]. + The default is 0.910. + Q_N_max : float, optional + Maximum nitrogen quota, in [g N/g COD]. + The default is 0.417. + Q_N_min : float, optional + Nitrogen subsistence quota, in [g N/g COD]. + The default is 0.082. + Q_P_max : float, optional + Maximum phosphorus quota, in [g P/g COD]. + The default is 0.092. + Q_P_min : float, optional + Phosphorus subsistence quota; assumes N:P ratio of 5:1, in [g P/g COD]. + The default is 0.0163. + V_NH : float, optional + Maximum specific ammonium uptake rate (calibrated in Guest et al., 2013), in [g N/g COD/d]. + The default is 0.254. + V_NO : float, optional + Maximum specific nitrate uptake rate (calibrated in Guest et al., 2013), in [g N/g COD/d]. + The default is 0.254. + V_P : float, optional + Maximum specific phosphorus uptake rate (calibrated in Guest et al., 2013), in [g P/g COD/d]. + The default is 0.016. + exponent : float, optional + Exponent to allow for more rapid transitions from growth to storage (see Guest et al., 2013), in [unitless] + The default is 4. + Y_ATP_PHO : float, optional + Yield of ATP on CO2 fixed to G3P, in [g ATP/g CO2]. + The default is 55.073. + Y_CH_PHO : float, optional + Yield of storage carbohydrate (as polyglucose, PG) on CO2 fixed to G3P, in [g COD/g CO2]. + The default is 0.754. + Y_LI_PHO : float, optional + Yield of storage lipids (as triacylglycerol, TAG) on CO2 fixed to G3P, in [g COD/g CO2]. + The default is 0.901. + Y_X_ALG_PHO : float, optional + Yield of carbon-accumulating phototrophic organisms on CO2 fixed to G3P, in [g COD/g CO2]. + The default is 0.450. + Y_ATP_HET_ACE : float, optional + Yield of ATP on acetate fixed to acetyl-CoA, in [g ATP/g COD]. + The default is 39.623. + Y_CH_NR_HET_ACE : float, optional + Yield of storage carbohydrates (as polyglucose, PG) on acetate fixed to acetyl-CoA under nutrient-replete condition, in [g COD/g COD]. + The default is 0.625. + Y_CH_ND_HET_ACE : float, optional + Yield of storage carbohydrates (as polyglucose, PG) on acetate fixed to acetyl-CoA under nutrient-deplete condition, in [g COD/g COD]. + The default is 0.600. + Y_LI_NR_HET_ACE : float, optional + Yield of storage lipids (as triacylglycerol, TAG) on acetate fixed to acetyl-CoA under nutrient-replete condition, in [g COD/g COD]. + The default is 1.105. + Y_LI_ND_HET_ACE : float, optional + Yield of storage lipids (as triacylglycerol, TAG) on acetate fixed to acetyl-CoA under nutrient-deplete condition, in [g COD/g COD]. + The default is 0.713. + Y_X_ALG_HET_ACE : float, optional + Yield of carbon-accumulating phototrophic organisms on acetate fixed to acetyl-CoA, in [g COD/g COD]. + The default is 0.216. + Y_ATP_HET_GLU : float, optional + Yield of ATP on glucose fixed to G6P, in [g ATP/g COD]. + The default is 58.114. + Y_CH_NR_HET_GLU : float, optional + Yield of storage carbohydrates (as polyglucose, PG) on glucose fixed to G6P under nutrient-replete condition, in [g COD/g COD]. + The default is 0.917. + Y_CH_ND_HET_GLU : float, optional + Yield of storage carbohydrates (as polyglucose, PG) on glucose fixed to G6P under nutrient-deplete condition, in [g COD/g COD]. + The default is 0.880. + Y_LI_NR_HET_GLU : float, optional + Yield of storage lipids (as triacylglycerol, TAG) on glucose fixed to G6P under nutrient-replete condition, in [g COD/g COD]. + The default is 1.620. + Y_LI_ND_HET_GLU : float, optional + Yield of storage lipids (as triacylglycerol, TAG) on glucose fixed to G6P under nutrient-deplete condition, in [g COD/g COD]. + The default is 1.046. + Y_X_ALG_HET_GLU : float, optional + Yield of carbon-accumulating phototrophic organisms on glucose fixed to G6P, in [g COD/g COD]. + The default is 0.317. + n_dark: float, optional + Dark growth reduction factor, in [unitless] + The default is 0.7. + f_SI : float, optional + Production of soluble inerts in hydrolysis, in [g COD/g COD]. + The default is 0.0. + Y_H : float, optional + Heterotrophic yield coefficient, in[g COD/g COD]. + The default is 0.625. + f_XI_H : float, optional + Fraction of inert COD generated in heterotrophic biomass lysis, in [g COD/g COD]. + The default is 0.1. + Y_A : float, optional + Autotrophic yield, in [g COD/g N]. + The default is 0.24. + f_XI_AUT : float, optional + Fraction of inert COD generated in autotrophic biomass lysis, in [g COD/g COD]. + The default is 0.1. + K_h : float, optional + Hydrolysis rate constant, in [d^(-1)]. + The default is 3.0. + eta_NO3 : float, optional + Reduction factor for anoxic hydrolysis, dimensionless. + The default is 0.6. + eta_fe : float, optional + Anaerobic hydrolysis reduction factor, dimensionless. + The default is 0.4. + K_O2 : float, optional + O2 half saturation coefficient for hydrolysis, in [g O2/m^3]. + The default is 0.2. + K_NO3 : float, optional + Nitrate half saturation coefficient for hydrolysis, in [g N/m^3]. + The default is 0.5. + K_X : float, optional + Slowly biodegradable substrate half saturation coefficient for hydrolysis, in [g COD/g COD]. + The default is 0.1. + mu_H : float, optional + Heterotrophic maximum specific growth rate, in [d^(-1)]. + The default is 6.0. + q_fe : float, optional + Fermentation maximum rate, in [d^(-1)]. + The default is 3.0. + eta_NO3_H : float, optional + Reduction factor for anoxic heterotrophic growth, dimensionless. + The default is 0.8. + b_H : float, optional + Lysis and decay rate constant, in [d^(-1)]. + The default is 0.4. + K_O2_H : float, optional + O2 half saturation coefficient for heterotrophs, in [g O2/m^3]. + The default is 0.2. + K_F_H : float, optional + Fermentable substrate half saturation coefficient for heterotrophic growth (K_F in ASM2d), in [g COD/m^3]. + The default is 4.0. + K_fe : float, optional + Fermentable substrate half saturation coefficient for fermentation, in [g COD/m^3]. + The default is 4.0. + K_A_H : float, optional + VFA half saturation coefficient for heterotrophs, in [g COD/m^3]. + The default is 4.0. + K_NO3_H : float, optional + Nitrate half saturation coefficient for heterotrophs, in [g N/m^3]. + The default is 0.5. + K_NH4_H : float, optional + Ammonium (nutrient) half saturation coefficient for heterotrophs, in [g N/m^3]. + The default is 0.05. + K_P_H : float, optional + Phosphorus (nutrient) half saturation coefficient for heterotrophs, in [g P/m^3]. + The default is 0.01. + K_ALK_H : float, optional + Alkalinity half saturation coefficient for heterotrophs, in [mol(HCO3-)/m^3]. (user input unit, converted as C) + The default is 0.1. + mu_AUT : float, optional + Autotrophic maximum specific growth rate, in [d^(-1)]. + The default is 1.0. + b_AUT : float, optional + Autotrophic decay rate, in [d^(-1)]. + The default is 0.15. + K_O2_AUT : float, optional + O2 half saturation coefficient for autotrophs, in [g O2/m^3]. + The default is 0.5. + K_NH4_AUT : float, optional + Ammonium (nutrient) half saturation coefficient for autotrophs, in [g N/m^3]. + The default is 1.0. + K_ALK_AUT : float, optional + Alkalinity half saturation coefficient for autotrophs, in [mol(HCO3-)/m^3]. (user input unit, converted as C) + The default is 0.5. + K_P_AUT : float, optional + Phosphorus (nutrient) half saturation coefficient for autotrophs, in [g P/m^3]. + The default is 0.01. + path : str, optional + Alternative file path for the Petersen matrix. + The default is None. + + Examples + -------- + >>> from qsdsan import processes as pc + >>> cmps = pc.create_pm2asm2d_cmps() + >>> pm2asm2d = pc.PM2ASM2d() + >>> pm2asm2d.show() + PM2ASM2d([photoadaptation, ammonium_uptake, nitrate_uptake_pho, nitrate_uptake_ace, nitrate_uptake_glu, phosphorus_uptake, + growth_pho, carbohydrate_storage_pho, lipid_storage_pho, carbohydrate_growth_pho, lipid_growth_pho, + carbohydrate_maintenance_pho, lipid_maintenance_pho, endogenous_respiration_pho, + growth_ace, carbohydrate_storage_ace, lipid_storage_ace, carbohydrate_growth_ace, lipid_growth_ace, + carbohydrate_maintenance_ace, lipid_maintenance_ace, endogenous_respiration_ace, + growth_glu, carbohydrate_storage_glu, lipid_storage_glu, carbohydrate_growth_glu, lipid_growth_glu, + carbohydrate_maintenance_glu, lipid_maintenance_glu, endogenous_respiration_glu, + aero_hydrolysis, anox_hydrolysis, anae_hydrolysis, + hetero_growth_S_F, hetero_growth_S_A, denitri_S_F, denitri_S_A, ferment, hetero_lysis, + auto_aero_growth, auto_lysis]) + + References + ---------- + .. [1] Henze, M.; Gujer, W.; Mino, T.; Loosdrecht, M. van. Activated Sludge + Models: ASM1, ASM2, ASM2d and ASM3; IWA task group on mathematical modelling + for design and operation of biological wastewater treatment, Ed.; IWA + Publishing: London, 2000. + .. [2] Rieger, L.; Gillot, S.; Langergraber, G.; Ohtsuki, T.; Shaw, A.; Takács, + I.; Winkler, S. Guidelines for Using Activated Sludge Models; IWA Publishing: + London, New York, 2012; Vol. 11. + https://doi.org/10.2166/9781780401164. + ''' + + _shared_params = ('Y_CH_PHO', 'Y_LI_PHO', 'Y_X_ALG_PHO', + 'Y_CH_NR_HET_ACE', 'Y_LI_NR_HET_ACE', 'Y_X_ALG_HET_ACE', + 'Y_CH_NR_HET_GLU', 'Y_LI_NR_HET_GLU', 'Y_X_ALG_HET_GLU') + + _stoichio_params = ('Y_CH_ND_HET_ACE', 'Y_LI_ND_HET_ACE', 'Y_CH_ND_HET_GLU', 'Y_LI_ND_HET_GLU', + 'f_SI', 'Y_H', 'f_XI_H', 'Y_A', 'f_XI_AUT', + *_shared_params) + + _kinetic_params = ('a_c', 'I_n', 'arr_a', 'arr_e', 'beta_1', 'beta_2', 'b_reactor', 'I_opt', 'k_gamma', + 'K_N', 'K_P', 'K_A', 'K_F', 'rho', 'K_STO', 'f_CH_max', 'f_LI_max', 'm_ATP', 'mu_max', + 'q_CH', 'q_LI', 'Q_N_max', 'Q_N_min', 'Q_P_max', 'Q_P_min', 'V_NH', 'V_NO', 'V_P', 'exponent', + 'Y_ATP_PHO', 'Y_ATP_HET_ACE', 'Y_ATP_HET_GLU', *_shared_params, 'n_dark', 'cmps', + 'K_h', 'eta_NO3', 'eta_fe', 'K_O2', 'K_NO3', 'K_X', 'mu_H', 'q_fe', 'eta_NO3_H', + 'b_H', 'K_O2_H', 'K_F_H', 'K_fe', 'K_A_H', 'K_NO3_H', 'K_NH4_H', 'K_P_H', + 'K_ALK_H', 'mu_AUT', 'b_AUT', 'K_O2_AUT', 'K_NH4_AUT', 'K_ALK_AUT', 'K_P_AUT') + + def __new__(cls, components=None, + a_c=0.049, I_n=250, arr_a=1.8e10, arr_e=6842, beta_1=2.90, beta_2=3.50, b_reactor=0.03, I_opt=300, k_gamma=1e-5, + K_N=0.1, K_P=1.0, K_A=6.3, K_F=6.3, rho=1.186, K_STO=1.566, + f_CH_max=0.819, f_LI_max=3.249, m_ATP=15.835, mu_max=1.969, q_CH=0.594, q_LI=0.910, + Q_N_max=0.417, Q_N_min=0.082, Q_P_max=0.092, Q_P_min=0.0163, V_NH=0.254, V_NO=0.254, V_P=0.016, exponent=4, + Y_ATP_PHO=55.073, Y_CH_PHO=0.754, Y_LI_PHO=0.901, Y_X_ALG_PHO=0.450, + Y_ATP_HET_ACE=39.623, Y_CH_NR_HET_ACE=0.625, Y_CH_ND_HET_ACE=0.600, + Y_LI_NR_HET_ACE=1.105, Y_LI_ND_HET_ACE=0.713, Y_X_ALG_HET_ACE=0.216, + Y_ATP_HET_GLU=58.114, Y_CH_NR_HET_GLU=0.917, Y_CH_ND_HET_GLU=0.880, + Y_LI_NR_HET_GLU=1.620, Y_LI_ND_HET_GLU=1.046, Y_X_ALG_HET_GLU=0.317, n_dark=0.7, + f_SI=0.0, Y_H=0.625, f_XI_H=0.1, Y_A=0.24, f_XI_AUT=0.1, + K_h=3.0, eta_NO3=0.6, eta_fe=0.4, K_O2=0.2, K_NO3=0.5, K_X=0.1, + mu_H=6.0, q_fe=3.0, eta_NO3_H=0.8, b_H=0.4, K_O2_H=0.2, K_F_H=4.0, + K_fe=4.0, K_A_H=4.0, K_NO3_H=0.5, K_NH4_H=0.05, K_P_H=0.01, K_ALK_H=0.1, + mu_AUT=1.0, b_AUT=0.15, K_O2_AUT=0.5, K_NH4_AUT=1.0, K_ALK_AUT=0.5, K_P_AUT=0.01, + path=None, **kwargs): + + if not path: path = _path + + self = Processes.load_from_file(path, + components=components, + conserved_for=('COD', 'C', 'N', 'P'), + parameters=cls._stoichio_params, + compile=False) + + asm2d_processes = Processes.load_from_file(_path_2, + components=components, + conserved_for=('COD', 'N', 'P', 'charge'), + parameters=cls._stoichio_params, + compile=False) + self.extend(asm2d_processes) + + if path == _path: + _p3 = Process('nitrate_uptake_pho', + 'S_NO -> [?]S_O2 + X_N_ALG', + components=components, + ref_component='X_N_ALG', + conserved_for=('COD', 'C')) + + _p4 = Process('nitrate_uptake_ace', + 'S_NO + [?]S_A -> [?]S_CO2 + X_N_ALG', + components=components, + ref_component='X_N_ALG', + conserved_for=('COD', 'C')) + + _p5 = Process('nitrate_uptake_glu', + 'S_NO + [?]S_F -> [?]S_CO2 + X_N_ALG', + components=components, + ref_component='X_N_ALG', + conserved_for=('COD', 'C')) + + self.insert(2, _p3) + self.insert(3, _p4) + self.insert(4, _p5) + + self.compile(to_class=cls) + + self.set_rate_function(rhos_pm2asm2d) + shared_values = (Y_CH_PHO, Y_LI_PHO, Y_X_ALG_PHO, + Y_CH_NR_HET_ACE, Y_LI_NR_HET_ACE, Y_X_ALG_HET_ACE, + Y_CH_NR_HET_GLU, Y_LI_NR_HET_GLU, Y_X_ALG_HET_GLU) + stoichio_values = (Y_CH_ND_HET_ACE, Y_LI_ND_HET_ACE, Y_CH_ND_HET_GLU, Y_LI_ND_HET_GLU, + f_SI, Y_H, f_XI_H, Y_A, f_XI_AUT, + *shared_values) + Q_N_min = max(self.Th_Q_N_min, Q_N_min) + Q_P_min = max(self.Th_Q_P_min, Q_P_min) + kinetic_values = (a_c, I_n, arr_a, arr_e, beta_1, beta_2, b_reactor, I_opt, k_gamma, + K_N, K_P, K_A, K_F, rho, K_STO, f_CH_max, f_LI_max, m_ATP, mu_max, + q_CH, q_LI, Q_N_max, Q_N_min, Q_P_max, Q_P_min, V_NH, V_NO, V_P, exponent, + Y_ATP_PHO, Y_ATP_HET_ACE, Y_ATP_HET_GLU, + *shared_values, n_dark, self._components, + K_h, eta_NO3, eta_fe, K_O2, K_NO3, K_X, mu_H, q_fe, eta_NO3_H, + b_H, K_O2_H, K_F_H, K_fe, K_A_H, K_NO3_H, K_NH4_H, K_P_H, + K_ALK_H*12, mu_AUT, b_AUT, K_O2_AUT, K_NH4_AUT, K_ALK_AUT*12, K_P_AUT, + ) + + dct = self.__dict__ + dct.update(kwargs) + dct['_parameters'] = dict(zip(cls._stoichio_params, stoichio_values)) + self.rate_function._params = dict(zip(cls._kinetic_params, kinetic_values)) + + return self + + def set_parameters(self, **parameters): + '''Set values to stoichiometric and/or kinetic parameters.''' + stoichio_only = {k:v for k,v in parameters.items() if k in self._stoichio_params} + self._parameters.update(stoichio_only) + if self._stoichio_lambdified is not None: + self.__dict__['_stoichio_lambdified'] = None + if 'Q_N_min' in parameters.keys(): + if parameters['Q_N_min'] < self.Th_Q_N_min: + raise ValueError(f'Value for Q_N_min must not be less than the ' + f'theoretical minimum {self.Th_Q_N_min}') + if 'Q_P_min' in parameters.keys(): + if parameters['Q_P_min'] < self.Th_Q_P_min: + raise ValueError(f'Value for Q_P_min must not be less than the ' + f'theoretical minimum {self.Th_Q_P_min}') + self.rate_function.set_param(**parameters) + + @property + def Th_Q_N_min(self): + return abs(self.stoichiometry.loc['growth_pho', 'X_N_ALG'])*1.001 + + @property + def Th_Q_P_min(self): + return abs(self.stoichiometry.loc['growth_pho', 'X_P_ALG'])*1.001 \ No newline at end of file diff --git a/qsdsan/sanunits/_anaerobic_reactor.py b/qsdsan/sanunits/_anaerobic_reactor.py index 496cb061..c800af47 100644 --- a/qsdsan/sanunits/_anaerobic_reactor.py +++ b/qsdsan/sanunits/_anaerobic_reactor.py @@ -583,8 +583,9 @@ def h2_stoichio(state_arr): dydt_Sh2_AD = self.model.dydt_Sh2_AD grad_dydt_Sh2_AD = self.model.grad_dydt_Sh2_AD def solve_h2(QC, S_in, T, h=h): - Ka = params['Ka_base'] * T_correction_factor(params['T_base'], T, params['Ka_dH']) - if h == None: h = solve_pH(QC, Ka, unit_conversion) + if h == None: + Ka = params['Ka_base'] * T_correction_factor(params['T_base'], T, params['Ka_dH']) + h = solve_pH(QC, Ka, unit_conversion) # S_h2_0 = QC[h2_idx] S_h2_0 = 2.8309E-07 S_h2_in = S_in[h2_idx] @@ -596,7 +597,7 @@ def solve_h2(QC, S_in, T, h=h): def update_h2_dstate(dstate): dstate[h2_idx] = 0. else: - solve_h2 = lambda QC, S_ins, T: QC[h2_idx] + solve_h2 = lambda QC, S_in, T: QC[h2_idx] def update_h2_dstate(dstate): pass def dy_dt(t, QC_ins, QC, dQC_ins): diff --git a/qsdsan/sanunits/_clarifier.py b/qsdsan/sanunits/_clarifier.py index b0053903..663dcce9 100644 --- a/qsdsan/sanunits/_clarifier.py +++ b/qsdsan/sanunits/_clarifier.py @@ -1049,7 +1049,7 @@ class PrimaryClarifierBSM2(SanUnit): H2O 1e+06 WasteStream-specific properties: pH : 7.0 - Alkalinity : 2.5 mg/L + Alkalinity : 2.5 mmol/L COD : 23873.0 mg/L BOD : 14963.2 mg/L TC : 8298.3 mg/L @@ -1270,7 +1270,7 @@ class PrimaryClarifier(IdealClarifier): H2O 1e+06 WasteStream-specific properties: pH : 7.0 - Alkalinity : 2.5 mg/L + Alkalinity : 2.5 mmol/L COD : 23873.0 mg/L BOD : 14963.2 mg/L TC : 8298.3 mg/L diff --git a/qsdsan/sanunits/_electrochemical_cell.py b/qsdsan/sanunits/_electrochemical_cell.py index 026d7046..d3c9fae3 100644 --- a/qsdsan/sanunits/_electrochemical_cell.py +++ b/qsdsan/sanunits/_electrochemical_cell.py @@ -95,7 +95,7 @@ class ElectrochemicalCell(SanUnit): O2 0.104 WasteStream-specific properties: pH : 7.0 - Alkalinity : 2.5 mg/L + Alkalinity : 2.5 mmol/L TC : 1860.0 mg/L TP : 31.9 mg/L TK : 1470.0 mg/L diff --git a/qsdsan/sanunits/_excretion.py b/qsdsan/sanunits/_excretion.py index 07f8eaa4..22bf3064 100644 --- a/qsdsan/sanunits/_excretion.py +++ b/qsdsan/sanunits/_excretion.py @@ -5,7 +5,10 @@ QSDsan: Quantitative Sustainable Design for sanitation and resource recovery systems This module is developed by: + Yalin Li + + Joy Zhang This module is under the University of Illinois/NCSA Open Source License. Please refer to https://github.com/QSD-Group/QSDsan/blob/main/LICENSE.txt @@ -16,8 +19,11 @@ from .. import SanUnit from ..utils import ospath, load_data, data_path +from warnings import warn +# from scipy.linalg import solve as la_solve +import numpy as np -__all__ = ('Excretion',) +__all__ = ('Excretion', 'ExcretionmASM2d') excretion_path = ospath.join(data_path, 'sanunit_data/_excretion.tsv') @@ -44,7 +50,7 @@ class Excretion(SanUnit): [1] Trimmer et al., Navigating Multidimensional Social–Ecological System Trade-Offs across Sanitation Alternatives in an Urban Informal Settlement. Environ. Sci. Technol. 2020, 54 (19), 12641–12653. - https://doi.org/10.1021/acs.est.0c03296. + https://doi.org/10.1021/acs.est.0c03296 ''' _N_ins = 0 @@ -58,6 +64,8 @@ def __init__(self, ID='', ins=None, outs=(), thermo=None, init_with='WasteStream data = load_data(path=excretion_path) for para in data.index: value = float(data.loc[para]['expected']) + # value = float(data.loc[para]['low']) + # value = float(data.loc[para]['high']) setattr(self, '_'+para, value) del data @@ -317,4 +325,136 @@ def waste_ratio(self): return self._waste_ratio @waste_ratio.setter def waste_ratio(self, i): - self._waste_ratio = i \ No newline at end of file + self._waste_ratio = i + + +#%% + +class ExcretionmASM2d(Excretion): + + def __init__(self, ID='', ins=None, outs=(), thermo=None, init_with='WasteStream', + waste_ratio=0, **kwargs): + super().__init__(ID, ins, outs, thermo, init_with, waste_ratio, **kwargs) + isdyn = kwargs.pop('isdynamic', False) + if isdyn: self._init_dynamic() + + def _run(self): + ur, fec = self.outs + ur.empty() + fec.empty() + cmps = ur.components + sf_iN = cmps.S_F.i_N + xs_iN = cmps.X_S.i_N + xb_iN = cmps.X_H.i_N + sxi_iN = cmps.S_I.i_N + i_mass = cmps.i_mass + i_P = cmps.i_P + hco3_imass = cmps.S_IC.i_mass + + not_wasted = 1 - self.waste_ratio + factor = 24 * 1e3 # from g/cap/d to kg/hr(/cap) + e_cal = self.e_cal / 24 * not_wasted # kcal/cap/d --> kcal/cap/hr + ur_exc = self.ur_exc / factor + fec_exc = self.fec_exc / factor + + # 14 kJ/g COD, the average lower heating value of excreta + tot_COD = e_cal*self.e_exc*4.184/14/1e3 # in kg COD/hr + fec_COD = tot_COD*self.e_fec + ur_COD = tot_COD - fec_COD + + tot_N = (self.p_veg+self.p_anim)*self.N_prot/factor \ + * self.N_exc*not_wasted + ur_N = tot_N*self.N_ur + fec_N = tot_N - ur_N + + tot_P = (self.p_veg*self.P_prot_v+self.p_anim*self.P_prot_a)/factor \ + * self.P_exc*not_wasted + ur_P = tot_P*self.P_ur + fec_P = tot_P - ur_P + + # breakpoint() + ur.imass['S_NH4'] = ur_nh4 = ur_N * self.N_ur_NH3 + req_sf_cod = (ur_N - ur_nh4) / sf_iN + if req_sf_cod <= ur_COD: + ur.imass['S_F'] = sf = req_sf_cod + ur.imass['S_A'] = ur_COD - sf # contains no N or P + else: + req_si_cod = (ur_N - ur_nh4) / sxi_iN + if req_si_cod <= ur_COD: + ur.imass['S_F'] = sf = (sxi_iN * ur_COD - (ur_N - ur_nh4))/(sxi_iN - sf_iN) + ur.imass['S_I'] = ur_COD - sf + else: + ur.imass['S_F'] = sf = ur_COD + ur_other_n = ur_N - ur_nh4 - sf * sf_iN + warn(f"Excess non-NH3 nitrogen cannot be accounted for by organics " + f"in urine: {ur_other_n} kg/hr. Added to NH3-N.") + ur.imass['S_NH4'] += ur_other_n # debatable, has negative COD # raise warning/error + + ur.imass['S_PO4'] = ur_P - sum(ur.mass * i_P) + ur.imass['S_K'] = e_cal/1e3 * self.K_cal/1e3 * self.K_exc*self.K_ur + ur.imass['S_Mg'] = self.Mg_ur / factor + ur.imass['S_Ca'] = self.Ca_ur / factor + + ur.imass['H2O'] = self.ur_moi * ur_exc + ur_others = ur_exc - sum(ur.mass * i_mass) + ur.imass['S_IC'] = ur_others * 0.34 / hco3_imass + ur.imass['S_Na'] = ur_others * 0.35 + ur.imass['S_Cl'] = ur_others * 0.31 + + fec.imass['S_NH4'] = fec_nh4 = fec_N * self.N_fec_NH3 + req_xs_cod = (fec_N - fec_nh4) / xs_iN + if req_xs_cod <= fec_COD: + fec.imass['X_S'] = xs = req_xs_cod + fec.imass['S_A'] = fec_COD - xs + else: + req_xi_cod = (fec_N - fec_nh4) / sxi_iN + if req_xi_cod <= fec_COD: + fec.imass['X_S'] = xs = (sxi_iN * fec_COD - (fec_N - fec_nh4))/(sxi_iN - xs_iN) + fec.imass['X_I'] = fec_COD - xs + else: + req_xb_cod = (fec_N - fec_nh4) / xb_iN + if req_xb_cod <= fec_COD: + fec.imass['X_S'] = xs = (xb_iN * fec_COD - (fec_N - fec_nh4))/(xb_iN - xs_iN) + fec.imass['X_H'] = fec_COD - xs + else: + fec.imass['X_S'] = xs = fec_COD + fec_other_n = fec_N - fec_nh4 - xs * xs_iN + warn(f"Excess non-NH3 nitrogen cannot be accounted for by organics " + f"in feces: {fec_other_n} kg/hr. Added to NH3-N.") + fec.imass['S_NH4'] += fec_other_n # debatable, has negative COD + + fec.imass['S_PO4'] = fec_P - sum(fec.mass * i_P) + fec.imass['S_K'] = (1-self.K_ur)/self.K_ur * ur.imass['S_K'] + fec.imass['S_Mg'] = self.Mg_fec / factor + fec.imass['S_Ca'] = self.Ca_fec / factor + fec.imass['H2O'] = self.fec_moi * fec_exc + + fec_others = fec_exc - sum(fec.mass * i_mass) + fec.imass['S_IC'] = fec_others * 0.34 / hco3_imass + fec.imass['S_Na'] = fec_others * 0.35 + fec.imass['S_Cl'] = fec_others * 0.31 + + + @property + def AE(self): + if self._AE is None: + self._compile_AE() + return self._AE + + def _compile_AE(self): + def yt(t, QC_ins, dQC_ins): + pass + self._AE = yt + + def _init_state(self): + ur, fec = self.outs + self._state = np.append(ur.mass, fec.mass) + for ws in self.outs: + ws.state = np.append(ws.conc, ws.F_vol * 24) + ws.dstate = np.zeros_like(ws.state) + + def _update_state(self): + pass + + def _update_dstate(self): + pass \ No newline at end of file diff --git a/qsdsan/sanunits/_junction.py b/qsdsan/sanunits/_junction.py index f99809ed..56c4a0e8 100644 --- a/qsdsan/sanunits/_junction.py +++ b/qsdsan/sanunits/_junction.py @@ -2659,7 +2659,7 @@ def adm1p2masm2d(adm_vals): X_newb, X_ACP, X_MgCO3, X_AlOH, X_AlPO4, X_FeOH, X_FePO4, \ S_Na, S_Cl, H2O = _adm_vals - if S_h2 > 0 or S_ch4 > 0: warn('Ignored dissolved H2 or CH4.') + if S_h2 > 0 or S_ch4 > 0: warn('Ignored dissolved H2 or CH4 in ADM1p-to-mASM2d interface model.') S_NH4 = S_IN S_PO4 = S_IP @@ -2700,6 +2700,8 @@ def adm1p2masm2d(adm_vals): fraction_dissolve = max(0, min(1, - S_IC / xc_mmp)) asm_vals -= fraction_dissolve * X_CaCO3 * cac_sto asm_vals -= fraction_dissolve * X_MgCO3 * mgc_sto + if asm_vals[8] < 0: + asm_vals[8] = 0 if S_IN < 0: xn_mmp = sum(asm_vals[_mmp_idx] * mmp_in) if xn_mmp > 0: @@ -3022,6 +3024,8 @@ def masm2d2adm1p(asm_vals): fraction_dissolve = max(0, min(1, - S_IC / xc_mmp)) adm_vals -= fraction_dissolve * X_CaCO3 * cac_sto adm_vals -= fraction_dissolve * X_MgCO3 * mgc_sto + if adm_vals[9] < 0: + adm_vals[9] = 0 if S_IN < 0: xn_mmp = sum(adm_vals[_mmp_idx] * mmp_in) if xn_mmp > 0: diff --git a/qsdsan/sanunits/_membrane_bioreactor.py b/qsdsan/sanunits/_membrane_bioreactor.py index 1bd90c4f..92076b0c 100644 --- a/qsdsan/sanunits/_membrane_bioreactor.py +++ b/qsdsan/sanunits/_membrane_bioreactor.py @@ -5,8 +5,12 @@ QSDsan: Quantitative Sustainable Design for sanitation and resource recovery systems This module is developed by: + Yalin Li + Joy Zhang + + This module is under the University of Illinois/NCSA Open Source License. Please refer to https://github.com/QSD-Group/QSDsan/blob/main/LICENSE.txt for license details. @@ -16,7 +20,10 @@ from biosteam import Stream from biosteam.exceptions import DesignError from . import HXutility, WWTpump, InternalCirculationRx -from .. import SanStream, SanUnit +from .. import SanStream, WasteStream, SanUnit, Process, CompiledProcesses +from ..sanunits import CSTR, PFR, dydt_cstr +from warnings import warn +import numpy as np, pandas as pd from ..equipments import Blower from ..utils import ( auom, @@ -26,8 +33,12 @@ calculate_excavation_volume, ) -__all__ = ('AnMBR',) +__all__ = ('AnMBR', + 'CompletelyMixedMBR', + # 'PlugFlowMBR', + ) +#%% degassing = SanStream.degassing _ft_to_m = auom('ft').conversion_factor('m') @@ -1338,4 +1349,288 @@ def organic_rm(self): Qi, Qe = self._inf.F_vol, self.outs[1].F_vol Si = compute_stream_COD(self._inf, 'kg/m3') Se = compute_stream_COD(self.outs[1], 'kg/m3') - return 1 - Qe*Se/(Qi*Si) \ No newline at end of file + return 1 - Qe*Se/(Qi*Si) + +#%% +from numba import njit +@njit(cache=True) +def dydt_mbr(QC_ins, QC, V, Qp, f_rtn, xarr, _dstate): + Q_ins = QC_ins[:, -1] + C_ins = QC_ins[:, :-1] + Q = sum(Q_ins) + C = QC[:-1] + _dstate[-1] = 0 + _dstate[:-1] = (Q_ins @ C_ins - (Q*(1-xarr) + (Qp+(Q-Qp)*(1-f_rtn))*xarr)*C)/V + +class CompletelyMixedMBR(CSTR): + ''' + Completely mixed membrane bioreactor, equivalent to a CSTR with ideal + membrane filtration at the outlet. + + See Also + -------- + :class:`qsdsan.processes.DiffusedAeration` + :class:`qsdsan.sanunits.CSTR` + + Examples + -------- + >>> from qsdsan import WasteStream, processes as pc, sanunits as su + >>> cmps = pc.create_asm1_cmps() + >>> ww = WasteStream('ww') + >>> ww.set_flow_by_concentration( + ... flow_tot=2000, + ... concentrations=dict(S_I=21.6, S_S=86.4, X_I=32.4, X_S=129.6, + ... S_NH=25, S_ND=2.78, X_ND=6.28, S_ALK=84), + ... units=('m3/d', 'mg/L')) + >>> asm = pc.ASM1() + >>> M1 = su.CompletelyMixedMBR('M1', ins=ww, outs=['filtrate', 'pumped'], + ... V_max=400, pumped_flow=50, solids_capture_rate=0.999, + ... aeration=2.0, DO_ID='S_O', + ... suspended_growth_model=asm) + >>> M1.set_init_conc(X_I=1000, S_I=30, S_S=5.0, X_S=100, X_BH=500, + ... X_BA=100, X_P=100, S_O=2, S_NH=2, S_ND=1, + ... X_ND=1, S_NO=20, S_ALK=84) + >>> M1.simulate(t_span=(0,400), method='BDF') + >>> M1.show() + CompletelyMixedMBR: M1 + ins... + [0] ww + phase: 'l', T: 298.15 K, P: 101325 Pa + flow (g/hr): S_I 1.8e+03 + S_S 7.2e+03 + X_I 2.7e+03 + X_S 1.08e+04 + S_NH 2.08e+03 + S_ND 232 + X_ND 523 + S_ALK 7e+03 + H2O 8.31e+07 + WasteStream-specific properties: + pH : 7.0 + Alkalinity : 2.5 mmol/L + COD : 270.0 mg/L + BOD : 137.1 mg/L + TC : 170.4 mg/L + TOC : 86.4 mg/L + TN : 36.0 mg/L + TP : 2.7 mg/L + TSS : 121.5 mg/L + outs... + [0] filtrate + phase: 'l', T: 298.15 K, P: 101325 Pa + flow (g/hr): S_I 1.75e+03 + S_S 277 + X_I 101 + X_S 4.83 + X_BH 236 + X_BA 13.7 + X_P 44.1 + S_O 162 + S_NO 1.8e+03 + S_NH 61.7 + S_ND 60 + X_ND 0.323 + S_ALK 3.6e+03 + S_N2 254 + H2O 8.1e+07 + WasteStream-specific properties: + pH : 7.0 + COD : 29.9 mg/L + BOD : 4.2 mg/L + TC : 54.0 mg/L + TOC : 9.7 mg/L + TN : 24.0 mg/L + TP : 0.3 mg/L + TK : 0.0 mg/L + TSS : 3.7 mg/L + [1] pumped + phase: 'l', T: 298.15 K, P: 101325 Pa + flow (g/hr): S_I 45 + S_S 7.09 + X_I 2.6e+03 + X_S 124 + X_BH 6.06e+03 + X_BA 351 + X_P 1.13e+03 + S_O 4.17 + S_NO 46 + S_NH 1.58 + S_ND 1.54 + X_ND 8.29 + S_ALK 92.2 + S_N2 6.51 + H2O 2.07e+06 + WasteStream-specific properties: + pH : 7.0 + COD : 4950.0 mg/L + BOD : 1779.8 mg/L + TC : 1795.1 mg/L + TOC : 1750.8 mg/L + TN : 381.0 mg/L + TP : 77.2 mg/L + TK : 17.3 mg/L + TSS : 3693.8 mg/L + + >>> flt, rtn = M1.outs + >>> flt.get_TSS() / rtn.get_TSS() # doctest: +ELLIPSIS + 0.001... + + ''' + _N_ins = 1 + _N_outs = 2 # [0] filtrate, [1] pumped flow + _outs_size_is_fixed = True + + def __init__(self, ID='', ins=None, outs=(), thermo=None, + init_with='WasteStream', isdynamic=True, + pumped_flow=50, solids_capture_rate=0.999, + V_max=1000, crossflow_air=None, + **kwargs): + super().__init__(ID, ins, outs, split=None, thermo=thermo, + init_with=init_with, V_max=V_max, isdynamic=isdynamic, + **kwargs) + self.pumped_flow = pumped_flow + self.solids_capture_rate = solids_capture_rate + self.crossflow_air = crossflow_air + + @property + def pumped_flow(self): + '''[float] Pumped flow rate, in m3/d''' + return self._Q_pump + @pumped_flow.setter + def pumped_flow(self, Q): + self._Q_pump = Q + + @property + def solids_capture_rate(self): + '''[float] Membrane solid capture rate, i.e., + filtrate-to-internal solids concentration ratio, unitless.''' + return self._f_rtn + @solids_capture_rate.setter + def solids_capture_rate(self, f): + if f < 0 or f > 1: + raise ValueError(f'membrane solids capture rate must be within [0,1], not {f}') + self._f_rtn = f + cmps = self._mixed.components + self._flt2in_conc_ratio = (1-cmps.x) + cmps.x * (1-f) + + @property + def crossflow_air(self): + '''[:class:`qsdsan.Process` or NoneType] + Membrane cross flow air specified for process modeling, such as `qsdsan.processes.DiffusedAeration`. + Ignored if DO setpoint is specified by the `aeration` attribute. + ''' + return self._cfa + @crossflow_air.setter + def crossflow_air(self, cfa): + if cfa is None or isinstance(cfa, Process): + self._cfa = cfa + else: + raise TypeError('crossflow_air must be a `Process` object or None, ' + f'not {type(cfa)}') + + split = None + + def _run(self): + '''Only to converge volumetric flows.''' + mixed = self._mixed + mixed.mix_from(self.ins) + cmps = mixed.components + Q = mixed.F_vol*24 # m3/d + Qp = self._Q_pump + f_rtn = self._f_rtn + xsplit = Qp / ((1-f_rtn)*(Q-Qp) + Qp) # mass split of solids to pumped flow + qsplit = Qp / Q + flt, rtn = self.outs + mixed.split_to(rtn, flt, xsplit*cmps.x + qsplit*(1-cmps.x)) + + def _compile_ODE(self): + aer = self._aeration + cfa = self._cfa + isa = isinstance + cmps = self.components + if self._model is None: + warn(f'{self.ID} was initialized without a suspended growth model, ' + f'and thus run as a non-reactive unit') + r = lambda state_arr: np.zeros(cmps.size) + else: + r = self._model.production_rates_eval + + _dstate = self._dstate + _update_dstate = self._update_dstate + V = self._V_max + Qp = self.pumped_flow + f_rtn = self.solids_capture_rate + xarr = cmps.x + gstrip = self.gas_stripping + if gstrip: + gas_idx = self.components.indices(self.gas_IDs) + if isa(aer, Process): kLa = aer.kLa + else: kLa = 0. + if cfa: kLa += cfa.kLa + S_gas_air = np.asarray(self.K_Henry)*np.asarray(self.p_gas_atm) + kLa_stripping = np.maximum(kLa*self.D_gas/self._D_O2, self.stripping_kLa_min) + hasexo = bool(len(self._exovars)) + f_exovars = self.eval_exo_dynamic_vars + + if isa(aer, (float, int)): + i = cmps.index(self._DO_ID) + def dy_dt(t, QC_ins, QC, dQC_ins): + QC[i] = aer + dydt_mbr(QC_ins, QC, V, Qp, f_rtn, xarr, _dstate) + if hasexo: QC = np.append(QC, f_exovars(t)) + _dstate[:-1] += r(QC) + if gstrip: _dstate[gas_idx] -= kLa_stripping * (QC[gas_idx] - S_gas_air) + _dstate[i] = 0 + _update_dstate() + else: + if cfa: + cfa_stoi = cfa._stoichiometry + cfa_frho = cfa.rate_function + dy_cfa = lambda QC: cfa_stoi * cfa_frho(QC) + else: + dy_cfa = lambda QC: 0. + + if isa(aer, Process): + aer_stoi = aer._stoichiometry + aer_frho = aer.rate_function + dy_aer = lambda QC: aer_stoi * aer_frho(QC) + else: + dy_aer = lambda QC: 0. + + def dy_dt(t, QC_ins, QC, dQC_ins): + dydt_mbr(QC_ins, QC, V, Qp, f_rtn, xarr, _dstate) + if hasexo: QC = np.append(QC, f_exovars(t)) + _dstate[:-1] += r(QC) + dy_aer(QC) + dy_cfa(QC) + if gstrip: _dstate[gas_idx] -= kLa_stripping * (QC[gas_idx] - S_gas_air) + _update_dstate() + self._ODE = dy_dt + + def _update_state(self): + arr = self._state + arr[arr < 1e-16] = 0. + arr[-1] = sum(ws.state[-1] for ws in self.ins) + for ws in self.outs: + if ws.state is None: + ws.state = np.zeros_like(arr) + ws.dstate = np.zeros_like(arr) + flt, rtn = self.outs + Qp = self.pumped_flow + flt.state[:-1] = arr[:-1] * self._flt2in_conc_ratio + flt.state[-1] = arr[-1] - Qp + rtn.state[:-1] = arr[:-1] + rtn.state[-1] = Qp + + def _update_dstate(self): + arr = self._dstate + arr[-1] = sum(ws.dstate[-1] for ws in self.ins) + flt, rtn = self.outs + flt.dstate[:-1] = arr[:-1] * self._flt2in_conc_ratio + flt.dstate[-1] = arr[-1] + rtn.dstate[:-1] = arr[:-1] + rtn.dstate[-1] = 0 + + +#%% + +class PlugFlowMBR(PFR): + pass \ No newline at end of file diff --git a/qsdsan/sanunits/_sludge_treatment.py b/qsdsan/sanunits/_sludge_treatment.py index b075f2ed..c817830f 100644 --- a/qsdsan/sanunits/_sludge_treatment.py +++ b/qsdsan/sanunits/_sludge_treatment.py @@ -117,7 +117,7 @@ class Thickener(SanUnit): H2O 1e+06 WasteStream-specific properties: pH : 7.0 - Alkalinity : 2.5 mg/L + Alkalinity : 2.5 mmol/L COD : 23873.0 mg/L BOD : 14963.2 mg/L TC : 8298.3 mg/L @@ -510,7 +510,7 @@ class Incinerator(SanUnit): H2O 1e+06 WasteStream-specific properties: pH : 7.0 - Alkalinity : 2.5 mg/L + Alkalinity : 2.5 mmol/L COD : 23873.0 mg/L BOD : 14963.2 mg/L TC : 8298.3 mg/L diff --git a/qsdsan/sanunits/_suspended_growth_bioreactor.py b/qsdsan/sanunits/_suspended_growth_bioreactor.py index 4b8cc3e8..19c6b2cf 100644 --- a/qsdsan/sanunits/_suspended_growth_bioreactor.py +++ b/qsdsan/sanunits/_suspended_growth_bioreactor.py @@ -24,6 +24,7 @@ 'BatchExperiment', # 'SBR', 'PFR', + 'AerobicDigester', ) # def _add_aeration_to_growth_model(aer, model): @@ -332,19 +333,20 @@ def ODE(self): self._compile_ODE() return self._ODE - def _compile_ODE(self): - isa = isinstance - cmps = self.components - m = cmps.size - aer = self._aeration + def _init_model(self): if self._model is None: warn(f'{self.ID} was initialized without a suspended growth model, ' f'and thus run as a non-reactive unit') - r = lambda state_arr: np.zeros(m) - + r = lambda state_arr: np.zeros(self.components.size) else: # processes = _add_aeration_to_growth_model(aer, self._model) r = self._model.production_rates_eval + return r + + def _compile_ODE(self): + isa = isinstance + aer = self._aeration + r = self._init_model() _dstate = self._dstate _update_dstate = self._update_dstate @@ -889,7 +891,7 @@ class PFR(SanUnit): H2O 1.53e+09 WasteStream-specific properties: pH : 7.0 - Alkalinity : 2.5 mg/L + Alkalinity : 2.5 mmol/L COD : 4389.1 mg/L BOD : 1563.3 mg/L TC : 1599.8 mg/L @@ -1165,7 +1167,7 @@ def _init_state(self): def _update_state(self): out, = self.outs ncol = self._ncol - self._state[self._state < 2.2e-16] = 0. + self._state[self._state < 1e-16] = 0. self._state[self._Qs_idx] = self._Qs if out.state is None: out.state = np.zeros(ncol) out.state[:-1] = self._state[-ncol:-1] @@ -1286,4 +1288,152 @@ def get_retained_mass(self, biomass_IDs): return mass @ self.V_tanks def _design(self): - pass \ No newline at end of file + pass + +#%% +from ..processes import ASM_AeDigAddOn + +class AerobicDigester(CSTR): + """ + Models an aerobic digester with activated sludge models, is a subclass of CSTR. + An additional degradation process of particulate inert organic materials is + considered in addition to typical activated sludge processes. + + Parameters + ---------- + activated_sludge_model : :class:`CompiledProcesses`, optional + The activated sludge model used for the biochemical reactions. The default is None. + organic_particulate_inert_degradation_process : :class:`Process`, optional + The degradation process of the particulate inert organic materials. The default is None. + + See Also + -------- + :class:`qsdsan.processes.ASM1` + :class:`qsdsan.processes.ASM2d` + :class:`qsdsan.processes.mASM2d` + :class:`qsdsan.processes.ASM_AeDigAddOn` + :class:`qsdsan.sanunites.CSTR` + + Examples + -------- + >>> from qsdsan import WasteStream, processes as pc, sanunits as su + >>> cmps = pc.create_asm1_cmps() + >>> twas = WasteStream('thickened_WAS') + >>> twas.set_flow_by_concentration( + ... flow_tot=50, + ... concentrations=dict( + ... S_I=30, S_S=1, X_I=17000, X_S=800, X_BH=38000, X_BA=2300, + ... X_P=6500, S_O=0.5, S_NH=2, S_ND=1, X_ND=65, S_NO=10, + ... S_N2=25, S_ALK=84), + ... units=('m3/d', 'mg/L')) + >>> asm = pc.ASM1() + >>> AED = su.AerobicDigester('AED', ins=twas, outs=('digested_WAS',), + ... V_max=3000, activated_sludge_model=asm, + ... DO_ID='S_O', aeration=1.0) + >>> AED.simulate(t_span=(0, 400), method='BDF') + >>> AED.show() + AerobicDigester: AED + ins... + [0] thickened_WAS + phase: 'l', T: 298.15 K, P: 101325 Pa + flow (g/hr): S_I 62.5 + S_S 2.08 + X_I 3.54e+04 + X_S 1.67e+03 + X_BH 7.92e+04 + X_BA 4.79e+03 + X_P 1.35e+04 + S_O 1.04 + S_NO 20.8 + S_NH 4.17 + S_ND 2.08 + X_ND 135 + S_ALK 175 + S_N2 52.1 + H2O 1.99e+06 + WasteStream-specific properties: + pH : 7.0 + Alkalinity : 2.5 mmol/L + COD : 64631.0 mg/L + BOD : 23300.3 mg/L + TC : 22923.3 mg/L + TOC : 22839.3 mg/L + TN : 4712.0 mg/L + TP : 1009.0 mg/L + TK : 223.2 mg/L + TSS : 48450.0 mg/L + outs... + [0] digested_WAS + phase: 'l', T: 298.15 K, P: 101325 Pa + flow (g/hr): S_I 62.5 + S_S 3.58e+04 + X_I 1.04e+04 + X_S 123 + X_BH 9.6e+03 + X_BA 1.59e+03 + X_P 2.77e+04 + S_O 2.08 + S_NO 4.17e+03 + S_NH 0.101 + S_ND 0.975 + X_ND 8.74 + S_N2 2.51e+03 + H2O 2e+06 + WasteStream-specific properties: + pH : 7.0 + Alkalinity : 2.5 mmol/L + COD : 40987.6 mg/L + BOD : 15416.1 mg/L + TC : 13985.1 mg/L + TOC : 13985.1 mg/L + TN : 3534.1 mg/L + TP : 458.2 mg/L + TK : 89.3 mg/L + TSS : 17813.2 mg/L + + """ + def __init__(self, ID='', ins=None, outs=(), thermo=None, + init_with='WasteStream', V_max=1000, activated_sludge_model=None, + organic_particulate_inert_degradation_process=None, + aeration=1.0, DO_ID='S_O2', isdynamic=True, **kwargs): + super().__init__(ID, ins, outs, thermo=thermo, init_with=init_with, + V_max=V_max, aeration=aeration, DO_ID=DO_ID, + suspended_growth_model=activated_sludge_model, + isdynamic=isdynamic, **kwargs) + self.organic_particulate_inert_degradation_process = organic_particulate_inert_degradation_process + + @property + def organic_particulate_inert_degradation_process(self): + '''[:class:`Process` or NoneType] Process object for degradation of + particulate inert organic materials in the aerobic digester. If none + specified, will attempt to create a Process model according to components + by default.''' + return self._dig_addon + @organic_particulate_inert_degradation_process.setter + def organic_particulate_inert_degradation_process(self, proc): + if isinstance(proc, Process): + self._dig_addon = proc + elif proc is None: + if self._model is None: self._dig_addon = None + else: + ID = self._model.__class__.__name__ + '_particulate_inert_degrade' + self._dig_addon = ASM_AeDigAddOn( + ID=ID, + components=self.thermo.chemicals + ) + else: + raise TypeError('organic_particulate_inert_degradation_process must be' + f' a `Process` object if not None, not {type(proc)}') + + def _init_model(self): + if self._model is None: + warn(f'{self.ID} was initialized without an activated sludge model, ' + f'and thus run as a non-reactive unit') + r = lambda state_arr: np.zeros(self.components.size) + else: + dig = self.organic_particulate_inert_degradation_process + dig_stoi = dig._stoichiometry + dig_frho = dig.rate_function + asm_frate = self._model.production_rates_eval + r = lambda state_arr: asm_frate(state_arr) + dig_stoi * dig_frho(state_arr) + return r \ No newline at end of file diff --git a/setup.py b/setup.py index 2059114b..995ef6d5 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ setup( name='qsdsan', packages=['qsdsan'], - version='1.4.0', + version='1.4.1', license='UIUC', author='Quantitative Sustainable Design Group', author_email='quantitative.sustainable.design@gmail.com',