diff --git a/README.md b/README.md index d4fc796826..9c81cd134d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # WaterTAP + This is the WaterTAP development repository. WaterTAP is developed as part of the [National Alliance for Water Innovation](https://nawihub.org/) project. @@ -16,6 +17,8 @@ WaterTAP is developed as part of the [National Alliance for Water Innovation](ht To quickly install and start using WaterTAP, head over to the [Getting Started](https://watertap.readthedocs.io/en/stable/getting_started.html) section of the documentation. +If you wish to use the **WaterTAP UI**, you can [download and install](https://watertap-org.github.io) it from this page: https://watertap-org.github.io. + ## Documentation The WaterTAP documentation is available online at . diff --git a/docs/_static/unit_models/translators/mapping_step_a.jpg b/docs/_static/unit_models/translators/mapping_step_a.jpg new file mode 100644 index 0000000000..74a3905fc5 Binary files /dev/null and b/docs/_static/unit_models/translators/mapping_step_a.jpg differ diff --git a/docs/_static/unit_models/translators/mapping_step_b.jpg b/docs/_static/unit_models/translators/mapping_step_b.jpg new file mode 100644 index 0000000000..9a842d467d Binary files /dev/null and b/docs/_static/unit_models/translators/mapping_step_b.jpg differ diff --git a/docs/_static/unit_models/translators/mapping_step_c.jpg b/docs/_static/unit_models/translators/mapping_step_c.jpg new file mode 100644 index 0000000000..1a0ff1d58b Binary files /dev/null and b/docs/_static/unit_models/translators/mapping_step_c.jpg differ diff --git a/docs/_static/unit_models/translators/mapping_step_final.jpg b/docs/_static/unit_models/translators/mapping_step_final.jpg new file mode 100644 index 0000000000..b2cae704cb Binary files /dev/null and b/docs/_static/unit_models/translators/mapping_step_final.jpg differ diff --git a/docs/technical_reference/property_models/ADM1.rst b/docs/technical_reference/property_models/ADM1.rst index 7b8b94eaa6..666b61f6f5 100644 --- a/docs/technical_reference/property_models/ADM1.rst +++ b/docs/technical_reference/property_models/ADM1.rst @@ -47,9 +47,9 @@ Components "Particulate inerts, X_I", ":math:`X_I`", "X_I" "Total cation equivalents concentration, S_cat", ":math:`S_{cat}`", "S_cat" "Total anion equivalents concentration, S_an", ":math:`S_{an}`", "S_an" - "Carbon dioxide carbon, S_co2", ":math:`S_{co2}`", "S_co2" + "Carbon dioxide, S_co2", ":math:`S_{co2}`", "S_co2" -**NOTE: S_h2 and S_ch4 have vapor phase and liquid phase, S_co2 only has vapor phase, and the other components only have liquid phase** +**NOTE: S_h2 and S_ch4 have vapor phase and liquid phase, S_co2 only has vapor phase, and the other components only have liquid phase. The amount of CO2 dissolved in the liquid phase is equivalent to S_IC - S_HCO3-.** State variables --------------- @@ -62,8 +62,7 @@ State variables "Component mass concentrations", ":math:`C_j`", "conc_mass_comp", "[p]", ":math:`\text{kg/}\text{m}^3`" "Anions in molar concentrations", ":math:`M_a`", "anions", "None", ":math:`\text{kmol/}\text{m}^3`" "Cations in molar concentrations", ":math:`M_c`", "cations", "None", ":math:`\text{kmol/}\text{m}^3`" - "Water pressure", ":math:`P_{w,sat}`", "p_w_sat", "None", ":math:`\text{Pa}`" - "Component pressure", ":math:`P_{j,sat}`", "p_sat", "[p]", ":math:`\text{Pa}`" + "Component pressure", ":math:`P_{j,sat}`", "pressure_sat", "[p]", ":math:`\text{Pa}`" Stoichiometric Parameters ------------------------- @@ -142,7 +141,6 @@ Kinetic Parameters "Valerate acid-base equilibrium constant, K_a_va", ":math:`K_{a,va}`", "K_a_va", 1.38e-5, ":math:`\text{kmol/}\text{m}^3`" "Butyrate acid-base equilibrium constant, K_a_bu", ":math:`K_{a,bu}`", "K_a_bu", 1.5e-5, ":math:`\text{kmol/}\text{m}^3`" "Propionate acid-base equilibrium constant, K_a_pro", ":math:`K_{a,pro}`", "K_a_bu", 1.32e-5, ":math:`\text{kmol/}\text{m}^3`" - "Propionate acid-base equilibrium constant, K_a_ac", ":math:`K_{a,ac}`", "K_a_ac", 1.74e-5, ":math:`\text{kmol/}\text{m}^3`" "Acetate acid-base equilibrium constant, K_a_ac", ":math:`K_{a,ac}`", "K_a_ac", 1.74e-5, ":math:`\text{kmol/}\text{m}^3`" "Carbon dioxide acid-base equilibrium constant, K_a_co2", ":math:`K_{a,co2}`", "K_a_co2", 4.94e-7, ":math:`\text{kmol/}\text{m}^3`" "Inorganic nitrogen acid-base equilibrium constant, K_a_IN", ":math:`K_{a,IN}`", "K_a_IN", 1.11e-9, ":math:`\text{kmol/}\text{m}^3`" diff --git a/docs/technical_reference/property_models/index.rst b/docs/technical_reference/property_models/index.rst index eefa053089..4374901f51 100644 --- a/docs/technical_reference/property_models/index.rst +++ b/docs/technical_reference/property_models/index.rst @@ -12,4 +12,5 @@ Property Models ASM1 ASM2D ADM1 + modified_ADM1 mc_aq_sol diff --git a/docs/technical_reference/property_models/modified_ADM1.rst b/docs/technical_reference/property_models/modified_ADM1.rst new file mode 100644 index 0000000000..f922ed9488 --- /dev/null +++ b/docs/technical_reference/property_models/modified_ADM1.rst @@ -0,0 +1,352 @@ +Modified ADM1 Property Package +============================== +.. raw:: html + + + + + +.. role:: red + +.. role:: lime + +.. role:: blue + +This package is an extension of the `base Anaerobic Digestion Model no.1 (ADM1) `_ and implements properties and reactions of an anaerobic digestion model for wastewater treatment using an anaerobic digester as provided in +`Batstone, D. J. et al. (2002) `_ and `Rosen and Jeppsson (2006) `_. + +Throughout this documentation, text in :red:`red` has been removed in the Modified ADM1 model, text in :lime:`lime` has been added, and text in :blue:`blue` has been modified from its base ADM1 implementation. + +The following modifications have been made to the base ADM1 model as provided in `Flores-Alsina, X. et al. (2016) `_: + * tracks inorganic phosphorus (S_IP), polyhydroxyalkanoates (X_PHA), polyphosphates (X_PP), phosphorus accumulating organisms (X_PAO), potassium (S_K), and magnesium (S_Mg) + * removes the composite material variable (X_C) and the associated disintegration reaction + * adds 7 additional reactions + +This modified ADM1 property/reaction package: + * supports 'H2O', 'S_su', 'S_aa', 'S_fa', 'S_va', 'S_bu', 'S_pro', 'S_ac', 'S_h2', 'S_ch4', 'S_IC', 'S_IN', 'S_IP', 'S_I', 'X_ch', 'X_pr', 'X_li', 'X_su', 'X_aa', 'X_fa', 'X_c4', 'X_pro', 'X_ac', 'X_h2', 'X_I', 'X_PHA', 'X_PP', 'X_PAO', 'S_K', 'S_Mg', 'S_cat', 'S_an', and 'S_co2' as components + * supports only liquid and vapor phase + * only makes changes to the liquid phase modelling + +Sets +---- +.. csv-table:: + :header: "Description", "Symbol", "Indices" + + "Components", ":math:`j`", "['H2O', 'S_su', 'S_aa', 'S_fa', 'S_va', 'S_bu', 'S_pro', 'S_ac', 'S_h2', 'S_ch4', 'S_IC', 'S_IN', 'S_IP', 'S_I', 'X_ch', 'X_pr', 'X_li', 'X_su', 'X_aa', 'X_fa', 'X_c4', 'X_pro', 'X_ac', 'X_h2', 'X_I', 'X_PHA', 'X_PP', 'X_PAO', 'S_K', 'S_Mg', 'S_cat', 'S_an', 'S_co2']" + "Phases", ":math:`p`", "['Liq', 'Vap']" + +Components +---------- +:red:`Red` text indicates the component has been removed in the Modified ADM1 model, and :lime:`lime` text indicates the component has been added. + +.. csv-table:: + :header: "Description", "Symbol", "Variable" + + "Monosaccharides, S_su", ":math:`S_{su}`", "S_su" + "Amino acids, S_aa", ":math:`S_{aa}`", "S_aa" + "Long chain fatty acids, S_fa", ":math:`S_{fa}`", "S_fa" + "Total valerate, S_va", ":math:`S_{va}`", "S_va" + "Total butyrate, S_bu", ":math:`S_{bu}`", "S_bu" + "Total propionate, S_pro", ":math:`S_{pro}`", "S_pro" + "Total acetate, S_ac", ":math:`S_{ac}`", "S_ac" + "Hydrogen gas, S_h2", ":math:`S_{h2}`", "S_h2" + "Methane gas, S_ch4", ":math:`S_{ch4}`", "S_ch4" + "Inorganic carbon, S_IC", ":math:`S_{IC}`", "S_IC" + "Inorganic nitrogen, S_IN", ":math:`S_{IN}`", "S_IN" + ":lime:`Inorganic phosphorus, S_IP`", ":math:`S_{IP}`", "S_IP" + "Soluble inerts, S_I", ":math:`S_I`", "S_I" + ":red:`Composites, X_c`", ":math:`X_c`", "X_c" + "Carbohydrates, X_ch", ":math:`X_{ch}`", "X_ch" + "Proteins, X_pr", ":math:`X_{pr}`", "X_pr" + "Lipids, X_li", ":math:`X_{li}`", "X_li" + "Sugar degraders, X_su", ":math:`X_{su}`", "X_su" + "Amino acid degraders, X_aa", ":math:`X_{aa}`", "X_aa" + "Long chain fatty acid (LCFA) degraders, X_fa", ":math:`X_{fa}`", "X_fa" + "Valerate and butyrate degraders, X_c4", ":math:`X_{c4}`", "X_c4" + "Propionate degraders, X_pro", ":math:`X_{pro}`", "X_pro" + "Acetate degraders, X_ac", ":math:`X_{ac}`", "X_ac" + "Hydrogen degraders, X_h2", ":math:`X_{h2}`", "X_h2" + "Particulate inerts, X_I", ":math:`X_I`", "X_I" + ":lime:`Polyhydroxyalkanoates, X_PHA`", ":math:`X_{PHA}`", "X_PHA" + ":lime:`Polyphosphates, X_PP`", ":math:`X_{PP}`", "X_PP" + ":lime:`Phosphorus accumulating organisms, X_PAO`", ":math:`X_{PAO}`", "X_PAO" + ":lime:`Potassium, S_K`", ":math:`S_K`", "S_K" + ":lime:`Magnesium, S_Mg`", ":math:`S_{Mg}`", "S_Mg" + "Total cation equivalents concentration, S_cat", ":math:`S_{cat}`", "S_cat" + "Total anion equivalents concentration, S_an", ":math:`S_{an}`", "S_an" + "Carbon dioxide, S_co2", ":math:`S_{co2}`", "S_co2" + +**NOTE: S_h2 and S_ch4 have vapor phase and liquid phase, S_co2 only has vapor phase, and the other components only have liquid phase. The amount of CO2 dissolved in the liquid phase is equivalent to S_IC - S_HCO3-.** + +State variables +--------------- +.. csv-table:: + :header: "Description", "Symbol", "Variable", "Index", "Units" + + "Total volumetric flowrate", ":math:`Q`", "flow_vol", "None", ":math:`\text{m}^3\text{/s}`" + "Temperature", ":math:`T`", "temperature", "None", ":math:`\text{K}`" + "Pressure", ":math:`P`", "pressure", "None", ":math:`\text{Pa}`" + "Component mass concentrations", ":math:`C_j`", "conc_mass_comp", "[p]", ":math:`\text{kg/}\text{m}^3`" + "Anions in molar concentrations", ":math:`M_a`", "anions", "None", ":math:`\text{kmol/}\text{m}^3`" + "Cations in molar concentrations", ":math:`M_c`", "cations", "None", ":math:`\text{kmol/}\text{m}^3`" + "Component pressure", ":math:`P_{j,sat}`", "pressure_sat", "[p]", ":math:`\text{Pa}`" + +Stoichiometric Parameters +------------------------- +:red:`Red` text indicates the parameter has been removed in the Modified ADM1 model, and :lime:`lime` text indicates the parameter has been added. + +.. csv-table:: + :header: "Description", "Symbol", "Parameter", "Value at 20 C", "Units" + + ":red:`Soluble inerts from composites, f_sI_xc`", ":math:`f_{sI,xc}`", "f_sI_xc", 0.1, ":math:`\text{dimensionless}`" + ":red:`Particulate inerts from composites, f_xI_xc`", ":math:`f_{xI,xc}`", "f_xI_xc", 0.2, ":math:`\text{dimensionless}`" + ":red:`Carbohydrates from composites, f_ch_xc`", ":math:`f_{ch,xc}`", "f_ch_xc", 0.2, ":math:`\text{dimensionless}`" + ":red:`Proteins from composites, f_pr_xc`", ":math:`f_{pr,xc}`", "f_pr_xc", 0.2, ":math:`\text{dimensionless}`" + ":red:`Lipids from composites, f_li_xc`", ":math:`f_{li,xc}`", "f_li_xc", 0.3, ":math:`\text{dimensionless}`" + ":red:`Nitrogen content of composites, N_xc`", ":math:`N_{xc}`", "N_xc", 0.0376/14, ":math:`\text{kmol-N/}\text{kg-COD}`" + ":red:`Nitrogen content of inerts, N_I`", ":math:`N_I`", "N_I", 0.06/14, ":math:`\text{kmol-N/}\text{kg-COD}`" + ":red:`Nitrogen in amino acids and proteins, N_aa`", ":math:`N_{aa}`", "N_aa", 0.007, ":math:`\text{kmol-N/}\text{kg-COD}`" + ":red:`Nitrogen content in bacteria, N_bac`", ":math:`N_{bac}`", "N_bac", 0.08/14, ":math:`\text{kmol-N/}\text{kg-COD}`" + ":lime:`Reference component mass concentration of hydrogen sulfide, Z_h2s`", ":math:`Z_{h2s}`", "Z_h2s", 0, ":math:`\text{kg/}\text{m}^3`" + ":lime:`Fraction of inert particulate organics from biomass, f_xi_xb`", ":math:`f_{xi,xb}`", "f_xi_xb", 0.1, ":math:`\text{dimensionless}`" + ":lime:`Fraction of carbohydrates from biomass, f_ch_xb`", ":math:`f_{ch,xb}`", "f_ch_xb", 0.275, ":math:`\text{dimensionless}`" + ":lime:`Fraction of lipids from biomass, f_li_xb`", ":math:`f_{li,xb}`", "f_li_xb", 0.35, ":math:`\text{dimensionless}`" + ":lime:`Fraction of proteins from biomass, f_pr_xb`", ":math:`f_{pr,xb}`", "f_pr_xb", 0.275, ":math:`\text{dimensionless}`" + ":lime:`Fraction of soluble inerts from biomass, f_si_xb`", ":math:`f_{si,xb}`", "f_si_xb", 0, ":math:`\text{dimensionless}`" + "Fatty acids from lipids, f_fa_li", ":math:`f_{fa,li}`", "f_fa_li", 0.95, ":math:`\text{dimensionless}`" + "Hydrogen from sugars, f_h2_su", ":math:`f_{h2,su}`", "f_h2_su", 0.19, ":math:`\text{dimensionless}`" + "Butyrate from sugars, f_bu_su", ":math:`f_{bu,su}`", "f_bu_su", 0.13, ":math:`\text{dimensionless}`" + "Propionate from sugars, f_pro_su", ":math:`f_{pro,su}`", "f_pro_su", 0.27, ":math:`\text{dimensionless}`" + "Acetate from sugars, f_ac_su", ":math:`f_{ac,su}`", "f_ac_su", 0.41, ":math:`\text{dimensionless}`" + "Hydrogen from amino acids, f_h2_aa", ":math:`f_{h2,aa}`", "f_h2_aa", 0.06, ":math:`\text{dimensionless}`" + "Valerate from amino acids, f_va_aa", ":math:`f_{va,aa}`", "f_va_aa", 0.23, ":math:`\text{dimensionless}`" + "Butyrate from amino acids, f_bu_aa", ":math:`f_{bu,aa}`", "f_bu_aa", 0.26, ":math:`\text{dimensionless}`" + "Propionate from amino acids, f_pro_aa", ":math:`f_{pro,aa}`", "f_pro_aa", 0.05, ":math:`\text{dimensionless}`" + "Acetate from amino acids, f_ac_aa", ":math:`f_{ac,aa}`", "f_ac_aa", 0.4, ":math:`\text{dimensionless}`" + "Yield of biomass on sugar substrate, Y_su", ":math:`Y_{su}`", "Y_su", 0.1, ":math:`\text{kg-COD X/}\text{kg-COD S}`" + "Yield of biomass on amino acid substrate, Y_aa", ":math:`Y_{aa}`", "Y_aa", 0.08, ":math:`\text{kg-COD X/}\text{kg-COD S}`" + "Yield of biomass on fatty acid substrate, Y_fa", ":math:`Y_{fa}`", "Y_fa", 0.06, ":math:`\text{kg-COD X/}\text{kg-COD S}`" + "Yield of biomass on valerate and butyrate substrate, Y_c4", ":math:`Y_{c4}`", "Y_c4", 0.06, ":math:`\text{kg-COD X/}\text{kg-COD S}`" + "Yield of biomass on propionate substrate, Y_pro", ":math:`Y_{pro}`", "Y_pro", 0.04, ":math:`\text{kg-COD X/}\text{kg-COD S}`" + "Yield of biomass on acetate substrate, Y_ac", ":math:`Y_{ac}`", "Y_ac", 0.05, ":math:`\text{kg-COD X/}\text{kg-COD S}`" + "Yield of hydrogen per biomass, Y_h2", ":math:`Y_{h2}`", "Y_h2", 0.06, ":math:`\text{kg-COD X/}\text{kg-COD S}`" + +Kinetic Parameters +------------------ +:red:`Red` text indicates the parameter has been removed in the Modified ADM1 model, and :lime:`lime` text indicates the parameter has been added. + +.. csv-table:: + :header: "Description", "Symbol", "Parameter", "Value at 20 C", "Units" + + ":red:`First-order kinetic parameter for disintegration, k_dis`", ":math:`k_{dis}`", "k_dis", 0.5, ":math:`\text{d}^{-1}`" + "First-order kinetic parameter for hydrolysis of carbohydrates, k_hyd_ch", ":math:`k_{hyd,ch}`", "k_hyd_ch", 10, ":math:`\text{d}^{-1}`" + "First-order kinetic parameter for hydrolysis of proteins, k_hyd_pr", ":math:`k_{hyd,pr}`", "k_hyd_pr", 10, ":math:`\text{d}^{-1}`" + "First-order kinetic parameter for hydrolysis of lipids, k_hyd_li", ":math:`k_{hyd,li}`", "k_hyd_li", 10, ":math:`\text{d}^{-1}`" + "Inhibition parameter for inorganic nitrogen, K_S_IN", ":math:`K_{S_{IN}}`", "K_S_IN", 1e-4, ":math:`\text{kmol/}\text{m}^3`" + "Monod maximum specific uptake rate of sugars, k_m_su", ":math:`k_{m_{su}}`", "k_m_su", 30, ":math:`\text{d}^{-1}`" + "Half saturation value for uptake of sugars, K_S_su", ":math:`K_{S_{su}}`", "K_S_su", 0.5, ":math:`\text{kg/}\text{m}^3`" + "Upper limit of pH for uptake rate of amino acids, pH_UL_aa", ":math:`pH_{UL,aa}`", "pH_UL_aa", 5.5, ":math:`\text{dimensionless}`" + "Lower limit of pH for uptake rate of amino acids, pH_LL_aa", ":math:`pH_{LL,aa}`", "pH_LL_aa", 4, ":math:`\text{dimensionless}`" + "Monod maximum specific uptake rate of amino acids, k_m_aa", ":math:`k_{m_{aa}}`", "k_m_aa", 50, ":math:`\text{d}^{-1}`" + "Half saturation value for uptake of amino acids, K_S_aa", ":math:`K_{S_{aa}}`", "K_S_aa", 0.3, ":math:`\text{kg/}\text{m}^3`" + "Monod maximum specific uptake rate of fatty acids, k_m_fa", ":math:`k_{m_{fa}}`", "k_m_fa", 6, ":math:`\text{d}^{-1}`" + "Half saturation value for uptake of fatty acids, K_S_fa", ":math:`K_{S_{fa}}`", "K_S_fa", 0.4, ":math:`\text{kg/}\text{m}^3`" + "Inhibition parameter for hydrogen during uptake of fatty acids, K_I_h2_fa", ":math:`K_{I,h2_{fa}}`", "K_I_h2_fa", 5e-6, ":math:`\text{kg/}\text{m}^3`" + "Monod maximum specific uptake rate of valerate and butyrate, k_m_c4", ":math:`k_{m_{c4}}`", "k_m_c4", 20, ":math:`\text{d}^{-1}`" + "Half saturation value for uptake of valerate and butyrate, K_S_c4", ":math:`K_{S_{c4}}`", "K_S_c4", 0.2, ":math:`\text{kg/}\text{m}^3`" + "Inhibition parameter for hydrogen during uptake of valerate and butyrate, K_I_h2_c4", ":math:`K_{I,h2_{c4}}`", "K_I_h2_c4", 1e-5, ":math:`\text{kg/}\text{m}^3`" + "Monod maximum specific uptake rate of propionate, k_m_pro", ":math:`k_{m_{pro}}`", "k_m_pro", 13, ":math:`\text{d}^{-1}`" + "Half saturation value for uptake of propionate, K_S_pro", ":math:`K_{S_{pro}}`", "K_S_pro", 0.1, ":math:`\text{kg/}\text{m}^3`" + "Inhibition parameter for hydrogen during uptake of propionate, K_I_h2_pro", ":math:`K_{I,h2_{pro}}`", "K_I_h2_pro", 3.5e-6, ":math:`\text{kg/}\text{m}^3`" + "Monod maximum specific uptake rate of acetate, k_m_ac", ":math:`k_{m_{ac}}`", "k_m_ac", 8, ":math:`\text{d}^{-1}`" + "Half saturation value for uptake of acetate, K_S_ac", ":math:`K_{S_{ac}}`", "K_S_ac", 0.15, ":math:`\text{kg/}\text{m}^3`" + "Inhibition parameter for ammonia during uptake of acetate, K_I_nh3", ":math:`K_{I,nh3}`", "K_I_nh3", 0.0018, ":math:`\text{kg/}\text{m}^3`" + "Upper limit of pH for uptake rate of acetate, pH_UL_ac", ":math:`pH_{UL,ac}`", "pH_UL_ac", 7, ":math:`\text{dimensionless}`" + "Lower limit of pH for uptake rate of acetate, pH_LL_ac", ":math:`pH_{LL,ac}`", "pH_LL_ac", 6, ":math:`\text{dimensionless}`" + "Monod maximum specific uptake rate of hydrogen, k_m_h2", ":math:`k_{m_{h2}}`", "k_m_h2", 35, ":math:`\text{d}^{-1}`" + "Half saturation value for uptake of hydrogen, K_S_h2", ":math:`K_{S_{h2}}`", "K_S_h2", 7e-6, ":math:`\text{kg/}\text{m}^3`" + "Upper limit of pH for uptake rate of hydrogen, pH_UL_h2", ":math:`pH_{UL,h2}`", "pH_UL_h2", 6, ":math:`\text{dimensionless}`" + "Lower limit of pH for uptake rate of hydrogen, pH_LL_h2", ":math:`pH_{LL,h2}`", "pH_LL_h2", 5, ":math:`\text{dimensionless}`" + "First-order decay rate for X_su, k_dec_X_su", ":math:`k_{dec,X_{su}}`", "k_dec_X_su", 0.02, ":math:`\text{d}^{-1}`" + "First-order decay rate for X_aa, k_dec_X_aa", ":math:`k_{dec,X_{aa}}`", "k_dec_X_aa", 0.02, ":math:`\text{d}^{-1}`" + "First-order decay rate for X_fa, k_dec_X_fa", ":math:`k_{dec,X_{fa}}`", "k_dec_X_fa", 0.02, ":math:`\text{d}^{-1}`" + "First-order decay rate for X_c4, k_dec_X_c4", ":math:`k_{dec,X_{c4}}`", "k_dec_X_c4", 0.02, ":math:`\text{d}^{-1}`" + "First-order decay rate for X_pro, k_dec_X_pro", ":math:`k_{dec,X_{pro}}`", "k_dec_X_pro", 0.02, ":math:`\text{d}^{-1}`" + "First-order decay rate for X_ac, k_dec_X_ac", ":math:`k_{dec,X_{ac}}`", "k_dec_X_ac", 0.02, ":math:`\text{d}^{-1}`" + "First-order decay rate for X_h2, k_dec_X_h2", ":math:`k_{dec,X_{h2}}`", "k_dec_X_h2", 0.02, ":math:`\text{d}^{-1}`" + "Dissociation constant, KW", ":math:`KW`", "KW", 2.08e-14, ":math:`(\text{kmol/}\text{m}^3)^2`" + "Valerate acid-base equilibrium constant, K_a_va", ":math:`K_{a,va}`", "K_a_va", 1.38e-5, ":math:`\text{kmol/}\text{m}^3`" + "Butyrate acid-base equilibrium constant, K_a_bu", ":math:`K_{a,bu}`", "K_a_bu", 1.5e-5, ":math:`\text{kmol/}\text{m}^3`" + "Propionate acid-base equilibrium constant, K_a_pro", ":math:`K_{a,pro}`", "K_a_bu", 1.32e-5, ":math:`\text{kmol/}\text{m}^3`" + "Acetate acid-base equilibrium constant, K_a_ac", ":math:`K_{a,ac}`", "K_a_ac", 1.74e-5, ":math:`\text{kmol/}\text{m}^3`" + ":lime:`50% inhibitory concentration of H2S on acetogens, K_I_h2s_ac`", ":math:`K_{I,h2s_{ac}}`", "K_I_h2s_ac", 460e-3, ":math:`\text{kg/}\text{m}^3`" + ":lime:`50% inhibitory concentration of H2S on c4 degraders, K_I_h2s_c4`", ":math:`K_{I,h2s_{c4}}`", "K_I_h2s_c4", 481e-3, ":math:`\text{kg/}\text{m}^3`" + ":lime:`50% inhibitory concentration of H2S on hydrogenotrophic methanogens, K_I_h2s_h2`", ":math:`K_{I,h2s_{h2}}`", "K_I_h2s_h2", 481e-3, ":math:`\text{kg/}\text{m}^3`" + ":lime:`50% inhibitory concentration of H2S on propionate degraders, K_I_h2s_pro`", ":math:`K_{I,h2s_{pro}}`", "K_I_h2s_pro", 481e-3, ":math:`\text{kg/}\text{m}^3`" + ":lime:`Phosphorus limitation for inorganic phosphorus, K_S_IP`", ":math:`K_{s,IP}`", "K_S_IP", 2e-5, ":math:`\text{kmol/}\text{m}^3`" + ":lime:`Lysis rate of phosphorus accumulating organisms, b_PAO`", ":math:`b_{PAO}`", "b_PAO", 0.2, ":math:`\text{d}^{-1}`" + ":lime:`Lysis rate of polyhydroxyalkanoates, b_PHA`", ":math:`b_{PHA}`", "b_PHA", 0.2, ":math:`\text{d}^{-1}`" + ":lime:`Lysis rate of polyphosphates, b_PP`", ":math:`b_{PP}`", "b_PP", 0.2, ":math:`\text{d}^{-1}`" + ":lime:`Yield of acetate on polyhydroxyalkanoates, f_ac_PHA`", ":math:`f_{ac,PHA}`", "f_ac_PHA", 0.4, ":math:`\text{dimensionless}`" + ":lime:`Yield of butyrate on polyhydroxyalkanoates, f_bu_PHA`", ":math:`f_{bu,PHA}`", "f_bu_PHA", 0.1, ":math:`\text{dimensionless}`" + ":lime:`Yield of propionate on polyhydroxyalkanoates, f_pro_PHA`", ":math:`f_{pro,PHA}`", "f_pro_PHA", 0.4, ":math:`\text{dimensionless}`" + ":lime:`Yield of valerate on polyhydroxyalkanoates, f_va_PHA`", ":math:`f_{va,PHA}`", "f_va_PHA", 0.1, ":math:`\text{dimensionless}`" + ":lime:`Saturation coefficient for acetate, K_A`", ":math:`K_{A}`", "K_A", 4e-3, ":math:`\text{kg/}\text{m}^3`" + ":lime:`Saturation coefficient for polyphosphate, K_PP`", ":math:`K_{PP}`", "k_PP", 0.32e-3, ":math:`\text{dimensionless}`" + ":lime:`Rate constant for storage of polyhydroxyalkanoates, q_PHA`", ":math:`q_{PHA}`", "q_PHA", 3, ":math:`\text{d}^{-1}`" + ":lime:`Yield of biomass on phosphate (kmol P/kg COD), Y_PO4`", ":math:`Y_{PO4}`", "Y_PO4", 12.903e-3, ":math:`\text{dimensionless}`" + ":lime:`Potassium coefficient for polyphosphates, K_PP`", ":math:`K_{PP}`", "K_PP", 1/3, ":math:`\text{dimensionless}`" + ":lime:`Magnesium coefficient for polyphosphates, Mg_PP`", ":math:`Mg_{PP}`", "Mg_PP", 1/3, ":math:`\text{dimensionless}`" + "Carbon dioxide acid-base equilibrium constant, K_a_co2", ":math:`K_{a,co2}`", "K_a_co2", 4.94e-7, ":math:`\text{kmol/}\text{m}^3`" + "Inorganic nitrogen acid-base equilibrium constant, K_a_IN", ":math:`K_{a,IN}`", "K_a_IN", 1.11e-9, ":math:`\text{kmol/}\text{m}^3`" + +Properties +---------- +.. csv-table:: + :header: "Description", "Symbol", "Variable", "Index", "Units" + + "Fluid specific heat capacity", ":math:`c_p`", "cp", "None", ":math:`\text{J/kg/K}`" + "Mass density", ":math:`\rho`", "dens_mass", "[p]", ":math:`\text{kg/}\text{m}^3`" + +Process Rate Equations +---------------------- +:red:`Red` text indicates the equation has been removed in the Modified ADM1 model, :lime:`lime` text indicates the equation has been added, and :blue:`blue` text indicates the equation has been modified from its base ADM1 implementation. + +.. csv-table:: + :header: "Description", "Equation" + + ":red:`Disintegration`", ":math:`\rho_1 = k_{dis} C_{X_c}`" + "Hydrolysis of carbohydrates", ":math:`\rho_1 = k_{hyd,ch} C_{X_{ch}}`" + "Hydrolysis of proteins", ":math:`\rho_2 = k_{hyd,pr} C_{X_{pr}}`" + "Hydrolysis of lipids", ":math:`\rho_3 = k_{hyd,li} C_{X_{li}}`" + ":blue:`Uptake of sugars`", ":math:`\rho_4 = k_{m_{su}} \frac{C_{S_{su}}}{K_{S_{su}}+C_{S_{su}}} C_{X_{su}} \frac{1}{1 + K_{S_{IN}}/C_{S_{IN}}/14} \cdot \frac{1}{1 + K_{S_{IP}}/C_{S_{IP}}/31} I_{pH,aa}`" + ":blue:`Uptake of amino acids`", ":math:`\rho_5 = k_{m_{aa}} \frac{C_{S_{aa}}}{K_{S_{aa}}+C_{S_{aa}}} C_{X_{aa}} \frac{1}{1 + K_{S_{IN}}/C_{S_{IN}}/14} \cdot \frac{1}{1 + K_{S_{IP}}/C_{S_{IP}}/31} I_{pH,aa}`" + ":blue:`Uptake of long chain fatty acids (LCFAs)`", ":math:`\rho_6 = k_{m_{fa}} \frac{C_{S_{fa}}}{K_{S_{fa}}+C_{S_{fa}}} C_{X_{fa}} \frac{1}{1 + K_{S_{IN}}/C_{S_{IN}}/14} \cdot \frac{1}{1 + C_{S_{h2}}/K_{I,h2_{fa}}} \cdot \frac{1}{1 + K_{S_{IP}}/C_{S_{IP}}/31} I_{pH,aa}`" + ":blue:`Uptake of valerate`", ":math:`\rho_7 = k_{m_{c4}} \frac{C_{S_{va}}}{K_{S_{c4}}+C_{S_{va}}} C_{X_{c4}} \frac{C_{S_{va}}}{C_{S_{bu}} + C_{S_{va}}} \cdot \frac{1}{1 + K_{S_{IN}}/C_{S_{IN}}/14} \cdot \frac{1}{1 + C_{S_{h2}}/K_{I,h2_{c4}}} \cdot \frac{1}{1 + K_{S_{IP}}/C_{S_{IP}}/31} I_{pH,aa} I_{h2s, c4}`" + ":blue:`Uptake of butyrate`", ":math:`\rho_8 = k_{m_{c4}} \frac{C_{S_{bu}}}{K_{S_{c4}}+C_{S_{bu}}} C_{X_{c4}} \frac{C_{S_{bu}}}{C_{S_{bu}} + C_{S_{va}}} \cdot \frac{1}{1 + K_{S_{IN}}/C_{S_{IN}}/14} \cdot \frac{1}{1 + C_{S_{h2}}/K_{I,h2_{c4}}} \cdot \frac{1}{1 + K_{S_{IP}}/C_{S_{IP}}/31} I_{pH,aa} I_{h2s, c4}`" + ":blue:`Uptake of propionate`", ":math:`\rho_9 = k_{m_{pro}} \frac{C_{S_{pro}}}{K_{S_{pro}}+C_{S_{pro}}} C_{X_{pro}} \cdot \frac{1}{1 + K_{S_{IN}}/C_{S_{IN}}/14} \cdot \frac{1}{1 + C_{S_{h2}}/K_{I,h2_{pro}}} \cdot \frac{1}{1 + K_{S_{IP}}/C_{S_{IP}}/31} I_{pH,aa} I_{h2s, pro}`" + ":blue:`Uptake of acetate`", ":math:`\rho_{10} = k_{m_{ac}} \frac{C_{S_{ac}}}{K_{S_{ac}}+C_{S_{ac}}} C_{X_{ac}} \cdot \frac{1}{1 + K_{S_{IN}}/C_{S_{IN}}/14} \cdot \frac{1}{1 + C_{NH3}/K_{I,nh3}} \cdot \frac{1}{1 + K_{S_{IP}}/C_{S_{IP}}/31} I_{pH,ac} I_{h2s, ac}`" + ":blue:`Uptake of hydrogen`", ":math:`\rho_{11} = k_{m_{h2}} \frac{C_{S_{h2}}}{K_{S_{h2}}+C_{S_{h2}}} C_{X_{h2}} \cdot \frac{1}{1 + K_{S_{IN}}/C_{S_{IN}}/14} \cdot \frac{1}{1 + K_{S_{IP}}/C_{S_{IP}}/31} I_{pH,h2} I_{h2s, h2}`" + "Decay of X_su", ":math:`\rho_{12} = k_{dec, X_{su}} C_{X_{su}}`" + "Decay of X_aa", ":math:`\rho_{13} = k_{dec, X_{aa}} C_{X_{aa}}`" + "Decay of X_fa", ":math:`\rho_{14} = k_{dec, X_{fa}} C_{X_{fa}}`" + "Decay of X_c4", ":math:`\rho_{15} = k_{dec, X_{c4}} C_{X_{c4}}`" + "Decay of X_pro", ":math:`\rho_{16} = k_{dec, X_{pro}} C_{X_{pro}}`" + "Decay of X_ac", ":math:`\rho_{17} = k_{dec, X_{ac}} C_{X_{ac}}`" + "Decay of X_h2", ":math:`\rho_{18} = k_{dec, X_{h2}} C_{X_{h2}}`" + ":lime:`Storage of S_va in X_PHA`", ":math:`\rho_{19} = q_{PHA} \frac{C_{S_{va}}}{K_{A} + C_{S{va}}} \cdot \frac{C_{X_{PP}} / C_{X_{PAO}}}{K_{PP} + \frac{C_{X_{PP}}}{C_{X_{PAO}}}} C_{X_{PAO}} \frac{C_{S_{va}}}{C_{S_{va}} + C_{S_{bu}} + C_{S_{pro}} + C_{S_{ac}}}`" + ":lime:`Storage of S_bu in X_PHA`", ":math:`\rho_{20} = q_{PHA} \frac{C_{S_{bu}}}{K_{A} + C_{S{bu}}} \cdot \frac{C_{X_{PP}} / C_{X_{PAO}}}{K_{PP} + \frac{C_{X_{PP}}}{C_{X_{PAO}}}} C_{X_{PAO}} \frac{C_{S_{bu}}}{C_{S_{va}} + C_{S_{bu}} + C_{S_{pro}} + C_{S_{ac}}}`" + ":lime:`Storage of S_pro in X_PHA`", ":math:`\rho_{21} = q_{PHA} \frac{C_{S_{pro}}}{K_{A} + C_{S{pro}}} \cdot \frac{C_{X_{PP}} / C_{X_{PAO}}}{K_{PP} + \frac{C_{X_{PP}}}{C_{X_{PAO}}}} C_{X_{PAO}} \frac{C_{S_{pro}}}{C_{S_{va}} + C_{S_{bu}} + C_{S_{pro}} + C_{S_{ac}}}`" + ":lime:`Storage of S_ac in X_PHA`", ":math:`\rho_{22} = q_{PHA} \frac{C_{S_{ac}}}{K_{A} + C_{S{ac}}} \cdot \frac{C_{X_{PP}} / C_{X_{PAO}}}{K_{PP} + \frac{C_{X_{PP}}}{C_{X_{PAO}}}} C_{X_{PAO}} \frac{C_{S_{ac}}}{C_{S_{va}} + C_{S_{bu}} + C_{S_{pro}} + C_{S_{ac}}}`" + ":lime:`Lysis of X_PAO`", ":math:`\rho_{23} = b_{PAO} C_{X_{PAO}}`" + ":lime:`Lysis of X_PP`", ":math:`\rho_{24} = b_{PP} C_{X_{PP}}`" + ":lime:`Lysis of X_PHA`", ":math:`\rho_{25} = b_{PHA} C_{X_{PHA}}`" + + +The rules for pH inhibition of amino-acid-utilizing microorganisms (:math:`I_{pH,aa}`), acetate-utilizing microorganisms (:math:`I_{pH,ac}`), hydrogen-utilizing microorganisms (:math:`I_{pH,h2}`) are: + + .. math:: + + I_{pH,aa}= + \begin{cases} + \exp{-3 (\frac{pH - pH_{UL,aa}}{pH_{UL,aa} - pH_{LL,aa}})^2} & \text{for } pH \le pH_{UL,aa}\\ + 1 & \text{for } pH > pH_{UL,aa} + \end{cases} + + I_{pH,ac}= + \begin{cases} + \exp{-3 (\frac{pH - pH_{UL,ac}}{pH_{UL,ac} - pH_{LL,ac}})^2} & \text{for } pH \le pH_{UL,ac}\\ + 1 & \text{for } pH > pH_{UL,ac} + \end{cases} + + I_{pH,aa}= + \begin{cases} + \exp{-3 (\frac{pH - pH_{UL,h2}}{pH_{UL,h2} - pH_{LL,h2}})^2} & \text{for } pH \le pH_{UL,h2}\\ + 1 & \text{for } pH > pH_{UL,h2} + \end{cases} + +The rules for hydrogen sulfide inhibition factors are shown below; however, since :math:`Z_{h2s}` is assumed to be 0, all of these inhibition factors are negligible + + .. math:: + + I_{h2s, ac} = \frac{1}{1 + \frac{Z_{h2s}}{K_{I,h2s,ac}}} + + I_{h2s, c4}= \frac{1}{1 + \frac{Z_{h2s}}{K_{I,h2s,c4}}} + + I_{h2s, h2}= \frac{1}{1 + \frac{Z_{h2s}}{K_{I,h2s,h2}}} + + I_{h2s, pro}= \frac{1}{1 + \frac{Z_{h2s}}{K_{I,h2s,pro}}} + +Classes +------- +.. currentmodule:: watertap.property_models.anaerobic_digestion.modified_adm1_properties + +.. autoclass:: ModifiedADM1ParameterBlock + :members: + :noindex: + +.. autoclass:: ModifiedADM1ParameterData + :members: + :noindex: + +.. autoclass:: _ModifiedADM1StateBlock + :members: + :noindex: + +.. autoclass:: ModifiedADM1StateBlockData + :members: + :noindex: + +.. currentmodule:: watertap.property_models.anaerobic_digestion.adm1_properties_vapor + +.. autoclass:: ADM1_vaporParameterBlock + :members: + :noindex: + +.. autoclass:: ADM1_vaporParameterData + :members: + :noindex: + +.. autoclass:: _ADM1_vaporStateBlock + :members: + :noindex: + +.. autoclass:: ADM1_vaporStateBlockData + :members: + :noindex: + +.. currentmodule:: watertap.property_models.anaerobic_digestion.modified_adm1_reactions + +.. autoclass:: ModifiedADM1ReactionParameterBlock + :members: + :noindex: + +.. autoclass:: ModifiedADM1ReactionParameterData + :members: + :noindex: + +.. autoclass:: _ModifiedADM1ReactionBlock + :members: + :noindex: + +.. autoclass:: ModifiedADM1ReactionBlockData + :members: + :noindex: + + +References +---------- +[1] Batstone, D.J., Keller, J., Angelidaki, I., Kalyuzhnyi, S.V., Pavlostathis, S.G., Rozzi, A., Sanders, W.T.M., Siegrist, H.A. and Vavilin, V.A., 2002. +The IWA anaerobic digestion model no 1 (ADM1). +Water Science and technology, 45(10), pp.65-73. +https://iwaponline.com/wst/article-abstract/45/10/65/6034 + +[2] Rosen, C. and Jeppsson, U., 2006. +Aspects on ADM1 Implementation within the BSM2 Framework. +Department of Industrial Electrical Engineering and Automation, Lund University, Lund, Sweden, pp.1-35. +https://www.iea.lth.se/WWTmodels_download/TR_ADM1.pdf + +[3] X. Flores-Alsina, K. Solon, C.K. Mbamba, S. Tait, K.V. Gernaey, U. Jeppsson, D.J. Batstone, 2016. +Modelling phosphorus (P), sulfur (S) and iron (Fe) interactions for dynamic simulations of anaerobic digestion processes, +Water Research. 95 370-382. +https://www.sciencedirect.com/science/article/pii/S0043135416301397 \ No newline at end of file diff --git a/docs/technical_reference/unit_models/gac.rst b/docs/technical_reference/unit_models/gac.rst index 2b87c6d6a9..c1aa0b1b5e 100644 --- a/docs/technical_reference/unit_models/gac.rst +++ b/docs/technical_reference/unit_models/gac.rst @@ -245,33 +245,27 @@ The following parameters are constructed when applying the GAC costing method in "Number of GAC contactors in operation in parallel", ":math:`N_{op}`", "num_contactors_op", "1", ":math:`\text{dimensionless}`" "Number of off-line redundant GAC contactors in parallel", ":math:`N_{red}`", "num_contactors_redundant", "1", ":math:`\text{dimensionless}`" - "GAC steel pressure contactor polynomial cost coefficient 0 (U.S. EPA, 2021)", ":math:`x_{p0}`", "pres_contactor_cost_coeff_0", "10010.9", ":math:`$`" - "GAC steel pressure contactor polynomial cost coefficient 1 (U.S. EPA, 2021)", ":math:`x_{p1}`", "pres_contactor_cost_coeff_1", "2204.95", ":math:`$/m^3`" - "GAC steel pressure contactor polynomial cost coefficient 2 (U.S. EPA, 2021)", ":math:`x_{p2}`", "pres_contactor_cost_coeff_2", "-15.9378", ":math:`$/\left( m^3 \right)^2`" - "GAC steel pressure contactor polynomial cost coefficient 3 (U.S. EPA, 2021)", ":math:`x_{p3}`", "pres_contactor_cost_coeff_3", "0.110592", ":math:`$/\left( m^3 \right)^3`" - "GAC concrete gravity contactor polynomial cost coefficient 0", ":math:`x_{g0}`", "grav_contactor_cost_coeff_0", "75131.3", ":math:`$`" - "GAC concrete gravity contactor polynomial cost coefficient 1", ":math:`x_{g1}`", "grav_contactor_cost_coeff_1", "735.550", ":math:`$/m^3`" - "GAC concrete gravity contactor polynomial cost coefficient 2", ":math:`x_{g2}`", "grav_contactor_cost_coeff_2", "-1.01827", ":math:`$/\left( m^3 \right)^2`" - "GAC concrete gravity contactor polynomial cost coefficient 3", ":math:`x_{g3}`", "grav_contactor_cost_coeff_3", "0", ":math:`$/\left( m^3 \right)^3`" - "Reference maximum value of GAC mass needed for initial charge where economy of scale no longer discounts the unit price (U.S. EPA, 2021)", ":math:`M_{GAC}^{ref}`", "bed_mass_gac_max_ref", "18143.7", ":math:`kg`" - "GAC adsorbent exponential cost pre-exponential coefficient (U.S. EPA, 2021)", ":math:`y_0`", "adsorbent_unit_cost_coeff", "4.58342", ":math:`$/kg`" - "GAC adsorbent exponential cost parameter coefficient (U.S. EPA, 2021)", ":math:`y_1`", "adsorbent_unit_cost_exp_coeff ", "-1.25311e-5", ":math:`kg^{-1}`" - "GAC pressure other cost power law coefficient", ":math:`z_{p0}`", "pres_other_cost_coeff", "16660.7", ":math:`$/\left( m^3 \right)^{z_1}`" - "GAC pressure other cost power law exponent", ":math:`z_{p1}`", "pres_other_cost_exp", "0.552207", ":math:`\text{dimensionless}`" - "GAC gravity other cost power law coefficient", ":math:`z_{g0}`", "grav_other_cost_coeff", "38846.9", ":math:`$/\left( m^3 \right)^{z_1}`" - "GAC gravity other cost power law exponent", ":math:`z_{g1}`", "grav_other_cost_exp", "0.490571", ":math:`\text{dimensionless}`" "Fraction of spent GAC adsorbent that can be regenerated for reuse", ":math:`f_{regen}`", "regen_frac", "0.70", ":math:`\text{dimensionless}`" + "Reference maximum value of GAC mass needed for initial charge where economy of scale no longer discounts the unit price (U.S. EPA, 2021)", ":math:`M_{GAC}^{ref}`", "bed_mass_gac_max_ref", "18143.7", ":math:`kg`" + "Contactor polynomial cost coefficients", ":math:`x`", "contactor_cost_coeff", "tabulated", ":math:`\text{dimensionless}`" + "Adsorbent exponential cost coefficients", ":math:`y`", "adsorbent_unit_cost_coeff", "tabulated", ":math:`\text{dimensionless}`" + "Other process costs power law coefficients", ":math:`z`", "other_cost_param", "tabulated", ":math:`\text{dimensionless}`" "Unit cost to regenerate spent GAC adsorbent by an offsite regeneration facility", ":math:`C_{regen}`", "regen_unit_cost", "4.28352", ":math:`$/kg`" "Unit cost to makeup spent GAC adsorbent with fresh adsorbent", ":math:`C_{makeup}`", "makeup_unit_cost", "4.58223", ":math:`$/kg`" - "GAC steel pressure contactor polynomial energy consumption coefficient 0", ":math:`\alpha_{p0}`", "pres_energy_coeff_0", "8.09926e-4", ":math:`kW`" - "GAC steel pressure contactor polynomial energy consumption coefficient 1", ":math:`\alpha_{p1}`", "pres_energy_coeff_1", "8.70577e-4", ":math:`kW/m^3`" - "GAC steel pressure contactor polynomial energy consumption coefficient 2", ":math:`\alpha_{p2}`", "pres_energy_coeff_2", "0", ":math:`kW/\left( m^3 \right)^2`" - "GAC concrete gravity contactor polynomial energy consumption coefficient 0", ":math:`\alpha_{g0}`", "grav_energy_coeff_0", "0.123782", ":math:`kW`" - "GAC concrete gravity contactor polynomial energy consumption coefficient 1", ":math:`\alpha_{g1}`", "grav_energy_coeff_1", "0.132403", ":math:`kW/m^3`" - "GAC concrete gravity contactor polynomial energy consumption coefficient 2", ":math:`\alpha_{g2}`", "grav_energy_coeff_2", "-1.41512e-5", ":math:`kW/\left( m^3 \right)^2`" + "Energy consumption polynomial coefficients", ":math:`alpha`", "energy_consumption_coeff", "tabulated", ":math:`\text{dimensionless}`" + +Costing methods are available for steel pressure vessel contactors (default) and concrete gravity basin contactors. Given that the form of the costing component equations are different (polynomial, exponential, and power law), the units associated with the parameters are embedded in the constraints and not directly applied to the variable. Additionally, the index is generalized to its position ``([0:len(parameter_data)])`` in the list, although some parameters are coefficients while others are exponents (see equations below for details). Variables with the (U.S. EPA, 2021) citation are directly taken from previously determined expressions. Other variables are regressed from higher detailed costing methods in (U.S. EPA, 2021). The variations in costing parameters are tabulated below: + +.. csv-table:: + :header: "Variable Name", "Contactor Type", "Index 0", "Index 1", "Index 2", "Index 3" -Variables with the (U.S. EPA, 2021) citation are directly taken from previously determined expressions. Other variables -are regressed from higher detailed costing methods in (U.S. EPA, 2021). + "adsorbent_unit_cost_coeff (U.S. EPA, 2021)", "n/a", "4.58342", "-1.25311e-5", "", "" + "contactor_cost_coeff (U.S. EPA, 2021)", "pressure", "10010.9", "2204.95", "-15.9378", "0.110592" + "contactor_cost_coeff", "gravity", "75131.3", "735.550", "-1.01827", "0" + "other_cost_param", "pressure", "16660.7", "0.552207", "", "" + "other_cost_param", "gravity", "38846.9", "0.490571", "", "" + "energy_consumption_coeff_data", "pressure", "8.09926e-4", "8.70577e-4", "0", "" + "energy_consumption_coeff_data", "gravity", "0.123782", "0.132403", "-1.41512e-5", "" Costing GAC contactors is defaulted to purchasing 1 operational and 1 redundant contactor for alternating operation. For large systems this may be a poor assumption considering vessel sizing and achieving pseudo-steady state. The number of contactors input by the user should justify reasonable @@ -357,4 +351,4 @@ Crittenden, J. C., Berrigan, J. K., Hand, D. W., & Lykins, B. (1987). Design of Nonconstant Diffusivities. Journal of Environmental Engineering, 113(2), 243–259. United States Environmental Protection Agency. (2021). Work Breakdown Structure-Based Cost Model for Granular Activated -Carbon Drinking Water Treatment. \ No newline at end of file +Carbon Drinking Water Treatment. https://www.epa.gov/system/files/documents/2022-03/gac-documentation-.pdf_0.pdf \ No newline at end of file diff --git a/docs/technical_reference/unit_models/translators/index.rst b/docs/technical_reference/unit_models/translators/index.rst index 02ad14f6ad..9a383044ec 100644 --- a/docs/technical_reference/unit_models/translators/index.rst +++ b/docs/technical_reference/unit_models/translators/index.rst @@ -5,3 +5,5 @@ Translators :maxdepth: 1 translator_adm1_asm1 + translator_adm1_asm2d + translator_asm1_adm1 diff --git a/docs/technical_reference/unit_models/translators/translator_adm1_asm1.rst b/docs/technical_reference/unit_models/translators/translator_adm1_asm1.rst index 7cdcf1c260..b74e07ed91 100644 --- a/docs/technical_reference/unit_models/translators/translator_adm1_asm1.rst +++ b/docs/technical_reference/unit_models/translators/translator_adm1_asm1.rst @@ -54,6 +54,64 @@ Sets **Notes** :sup:`*` Ion" is a subset of "Component" and uses the same symbol j. +ADM1 Components +--------------- +Additional documentation on the ADM1 property model can be found here: `Anaerobic Digestion Model 1 Documentation `_ + +.. csv-table:: + :header: "Description", "Symbol", "Variable" + + "Monosaccharides, S_su", ":math:`S_{su}`", "S_su" + "Amino acids, S_aa", ":math:`S_{aa}`", "S_aa" + "Long chain fatty acids, S_fa", ":math:`S_{fa}`", "S_fa" + "Total valerate, S_va", ":math:`S_{va}`", "S_va" + "Total butyrate, S_bu", ":math:`S_{bu}`", "S_bu" + "Total propionate, S_pro", ":math:`S_{pro}`", "S_pro" + "Total acetate, S_ac", ":math:`S_{ac}`", "S_ac" + "Hydrogen gas, S_h2", ":math:`S_{h2}`", "S_h2" + "Methane gas, S_ch4", ":math:`S_{ch4}`", "S_ch4" + "Inorganic carbon, S_IC", ":math:`S_{IC}`", "S_IC" + "Inorganic nitrogen, S_IN", ":math:`S_{IN}`", "S_IN" + "Soluble inerts, S_I", ":math:`S_I`", "S_I" + "Composites, X_c", ":math:`X_c`", "X_c" + "Carbohydrates, X_ch", ":math:`X_{ch}`", "X_ch" + "Proteins, X_pr", ":math:`X_{pr}`", "X_pr" + "Lipids, X_li", ":math:`X_{li}`", "X_li" + "Sugar degraders, X_su", ":math:`X_{su}`", "X_su" + "Amino acid degraders, X_aa", ":math:`X_{aa}`", "X_aa" + "Long chain fatty acid (LCFA) degraders, X_fa", ":math:`X_{fa}`", "X_fa" + "Valerate and butyrate degraders, X_c4", ":math:`X_{c4}`", "X_c4" + "Propionate degraders, X_pro", ":math:`X_{pro}`", "X_pro" + "Acetate degraders, X_ac", ":math:`X_{ac}`", "X_ac" + "Hydrogen degraders, X_h2", ":math:`X_{h2}`", "X_h2" + "Particulate inerts, X_I", ":math:`X_I`", "X_I" + "Total cation equivalents concentration, S_cat", ":math:`S_{cat}`", "S_cat" + "Total anion equivalents concentration, S_an", ":math:`S_{an}`", "S_an" + "Carbon dioxide, S_co2", ":math:`S_{co2}`", "S_co2" + +**NOTE: S_h2 and S_ch4 have vapor phase and liquid phase, S_co2 only has vapor phase, and the other components only have liquid phase. The amount of CO2 dissolved in the liquid phase is equivalent to S_IC - S_HCO3-.** + +ASM1 Components +--------------- +Additional documentation on the ASM1 property model can be found here: `Activated Sludge Model 1 Documentation `_ + +.. csv-table:: + :header: "Description", "Symbol", "Variable" + + "Soluble inert organic matter, S_I", ":math:`S_I`", "S_I" + "Readily biodegradable substrate S_S", ":math:`S_S`", "S_S" + "Particulate inert organic matter, X_I", ":math:`X_I`", "X_I" + "Slowly biodegradable substrate X_S", ":math:`X_S`", "X_S" + "Active heterotrophic biomass X_B,H", ":math:`X_{B,H}`", "X_BH" + "Active autotrophic biomass X_B,A", ":math:`X_{B,A}`", "X_BA" + "Particulate products arising from biomass decay, X_P", ":math:`X_P`", "X_P" + "Oxygen, S_O", ":math:`S_O`", "S_O" + "Nitrate and nitrite nitrogen, S_NO", ":math:`S_{NO}`", "S_NO" + "NH4 :math:`^{+}` + NH :math:`_{3}` Nitrogen, S_NH", ":math:`S_{NH}`", "S_NH" + "Soluble biodegradable organic nitrogen, S_ND", ":math:`S_{ND}`", "S_ND" + "Particulate biodegradable organic nitrogen, X_ND", ":math:`X_{ND}`", "X_ND" + "Alkalinity, S_ALK", ":math:`S_{ALK}`", "S_ALK" + .. _Translator_ADM1_ASM1_equations: Equations and Relationships @@ -87,4 +145,4 @@ References [1] Copp J. and Jeppsson, U., Rosen, C., 2006. Towards an ASM1 - ADM1 State Variable Interface for Plant-Wide Wastewater Treatment Modeling. Proceedings of the Water Environment Federation, 2003, pp 498-510. -https://www.accesswater.org/publications/-290550/towards-an-asm1--ndash--adm1-state-variable-interface-for-plant-wide-wastewater-treatment-modeling +https://www.accesswater.org/publications/proceedings/-290550/towards-an-asm1---adm1-state-variable-interface-for-plant-wide-wastewater-treatment-modeling diff --git a/docs/technical_reference/unit_models/translators/translator_adm1_asm2d.rst b/docs/technical_reference/unit_models/translators/translator_adm1_asm2d.rst new file mode 100644 index 0000000000..08bfa5398f --- /dev/null +++ b/docs/technical_reference/unit_models/translators/translator_adm1_asm2d.rst @@ -0,0 +1,97 @@ +ADM1 to ASM2d Translator +======================== + +Introduction +------------ + +A link is required to translate between biological based and physical or chemical mediated processes +to develop plant-wide modeling of wastewater treatment. This model mediates the interaction between +the Modified Anaerobic Digestor Model 1 (ADM1) and the Modified Activated Sludge Model 2d (ASM2d). + +The model relies on the following key assumption: + + * supports only liquid phase + * supports only Modified ADM1 to Modified ASM2d translations + +.. index:: + pair: watertap.unit_models.translators.translator_adm1_asm2d;translator_adm1_asm2d + +.. currentmodule:: watertap.unit_models.translators.translator_adm1_asm2d + +Degrees of Freedom +------------------ +The translator degrees of freedom are the inlet feed state variables: + + * temperature + * pressure + * volumetric flowrate + * solute compositions + * cations + * anions + +Ports +----- + +This model provides two ports: + +* inlet +* outlet + +Sets +---- + +.. csv-table:: + :header: "Description", "Symbol", "Indices" + + "Time", ":math:`t`", "[0]" + "Inlet/outlet", ":math:`x`", "['in', 'out']" + "Phases", ":math:`p`", "['Liq']" + "Inlet Components", ":math:`j`", "['H2O', 'S_su', 'S_aa', 'S_fa', 'S_va', 'S_bu', 'S_pro', 'S_ac','S_h2','S_ch4','S_IC','S_IN','S_IP','S_I','X_ch','X_pr','X_li','X_su','X_aa','X_fa','X_c4','X_pro','X_ac','X_h2','X_I','X_PHA','X_PP','X_PAO','S_K','S_Mg']" + "Ion", ":math:`j`", "['S_cat', 'S_an'] \ :sup:`1`" + "Outlet Components", ":math:`j`", "['H2O', 'S_A','S_F','S_I','S_N2','S_NH4','S_NO3','S_O2','S_PO4','S_ALK','X_AUT','X_H','X_I','X_MeOH','X_MeP','X_PAO','X_PHA','X_PP','X_S','X_TSS'] \ :sup:`2`" + "Readily Biodegradable COD", ":math:`k`", "['S_su', 'S_aa', 'S_fa', 'S_va', 'S_bu', 'S_pro', 'S_ac']" + "Slowly Biodegradable COD", ":math:`m`", "['X_ch', 'X_pr', 'X_li']" + "Unchanged Components", ":math:`j`", "['S_I','X_I','X_PP','X_PHA']" + "Zero Flow Components", ":math:`j`", "['S_N2','S_NO3','S_O2','X_AUT','X_H','X_PAO','X_TSS','X_MeOH','X_MeP']" + +**Notes** + :sup:`1` Ion" is a subset of "Inlet Components" and uses the same symbol j. + :sup:`2` "Outlet Components" also includes any additional solutes that the user specifies for Modified ASM2d. + +.. _Translator_ADM1_ASM2d_equations: + +Equations and Relationships +--------------------------- + +.. csv-table:: + :header: "Description", "Equation" + + "Volumetric flow equality", ":math:`F_{out} = F_{in}`" + "Temperature balance", ":math:`T_{out} = T_{in}`" + "Pressure balance", ":math:`P_{out} = P_{in}`" + "Fermentable substrate conversion", ":math:`S_{F, out} = S_{su, in} + S_{aa, in} + S_{fa, in}`" + "Acetic acid conversion", ":math:`S_{A, out} = S_{va, in} + S_{bu, in} + S_{pro, in} + S_{ac, in}`" + "Inert soluble COD conversion", ":math:`S_{I, out} = S_{I, in}`" + "Inert particulate COD conversion", ":math:`X_{I, out} = X_{I, in}`" + "Polyphosphate conversion", ":math:`X_{PP, out} = X_{PP, in}`" + "Polyhydroxyalkanoates conversion", ":math:`X_{PHA, out} = X_{PHA, in}`" + "Ammonium conversion", ":math:`S_{NH4, out} = S_{IN, in}`" + "Phosphate conversion", ":math:`S_{PO4, out} = S_{IP, in}`" + "Alkalinity conversion", ":math:`S_{ALK, out} = \frac{S_{IC, in}}{12}`" + "Biodegradable particulate organics conversion", ":math:`X_{S, out} = X_{ch, in} + X_{pr, in} + X_{li, in}`" + + +Classes +------- +.. currentmodule:: watertap.unit_models.translators.translator_adm1_asm2d + +.. autoclass:: TranslatorDataADM1ASM2D + :members: + :noindex: + +References +---------- +[1] Flores-Alsina, X., Solon, K., Mbamba, C.K., Tait, S., Gernaey, K.V., Jeppsson, U. and Batstone, D.J., 2016. +Modelling phosphorus (P), sulfur (S) and iron (Fe) interactions for dynamic simulations of anaerobic digestion processes. +Water Research, 95, pp.370-382. + diff --git a/docs/technical_reference/unit_models/translators/translator_asm1_adm1.rst b/docs/technical_reference/unit_models/translators/translator_asm1_adm1.rst new file mode 100644 index 0000000000..781f64b6d1 --- /dev/null +++ b/docs/technical_reference/unit_models/translators/translator_asm1_adm1.rst @@ -0,0 +1,247 @@ +ASM1 to ADM1 Translator +======================= + +Introduction +------------ + +A link is required to translate between biological and physically- or chemically-mediated processes +to develop whole-plant modeling of wastewater treatment. This model mediates the interaction between +the Activated Sludge Model 1 (ASM1) and the Anaerobic Digestor Model 1 (ADM1). + +The model relies on the following key assumptions: + + * supports only liquid phase + * supports only ASM1 to ADM1 translations + +.. index:: + pair: watertap.unit_models.translators.translator_asm1_adm1;translator_asm1_adm1 + +.. currentmodule:: watertap.unit_models.translators.translator_asm1_adm1 + +Degrees of Freedom +------------------ +The translator degrees of freedom are the inlet feed state variables: + + * temperature + * pressure + * volumetric flowrate + * solute compositions + * cations + * anions + +Ports +----- + +This model provides two ports: + +* inlet +* outlet + +Sets +---- +.. csv-table:: + :header: "Description", "Symbol", "Indices" + + "Time", ":math:`t`", "[0]" + "Inlet/outlet", ":math:`x`", "['in', 'out']" + "Phases", ":math:`p`", "['Liq']" + "Inlet Components", ":math:`j`", "['H2O', 'S_I', 'S_S', 'X_I', 'X_S', 'X_BH', 'X_BA', 'X_P', 'S_O', 'S_NO', 'S_NH', 'S_ND', 'X_ND', 'S_ALK']" + "Ion", ":math:`j`", "['S_cat', 'S_an'] \ :sup:`*`" + "Outlet Components", ":math:`j`", "['H2O', 'S_su', 'S_aa', 'S_fa', 'S_va', 'S_bu', 'S_pro', 'S_ac', 'S_h2', 'S_ch4', 'S_IC', 'S_IN', 'S_I', 'X_c', 'X_ch', 'X_pr', 'X_li', 'X_su', 'X_aa', 'X_fa', 'X_c4', 'X_pro', 'X_ac', 'X_h2', 'X_I', 'S_cat', 'S_an', 'S_co2']" + +**Notes** + :sup:`*` Ion" is a subset of "Component" and uses the same symbol j. + +ASM1 Components +--------------- +Additional documentation on the ASM1 property model can be found here: `Activated Sludge Model 1 Documentation `_ + +.. csv-table:: + :header: "Description", "Symbol", "Variable" + + "Soluble inert organic matter, S_I", ":math:`S_I`", "S_I" + "Readily biodegradable substrate S_S", ":math:`S_S`", "S_S" + "Particulate inert organic matter, X_I", ":math:`X_I`", "X_I" + "Slowly biodegradable substrate X_S", ":math:`X_S`", "X_S" + "Active heterotrophic biomass X_B,H", ":math:`X_{B,H}`", "X_BH" + "Active autotrophic biomass X_B,A", ":math:`X_{B,A}`", "X_BA" + "Particulate products arising from biomass decay, X_P", ":math:`X_P`", "X_P" + "Oxygen, S_O", ":math:`S_O`", "S_O" + "Nitrate and nitrite nitrogen, S_NO", ":math:`S_{NO}`", "S_NO" + "NH4 :math:`^{+}` + NH :math:`_{3}` Nitrogen, S_NH", ":math:`S_{NH}`", "S_NH" + "Soluble biodegradable organic nitrogen, S_ND", ":math:`S_{ND}`", "S_ND" + "Particulate biodegradable organic nitrogen, X_ND", ":math:`X_{ND}`", "X_ND" + "Alkalinity, S_ALK", ":math:`S_{ALK}`", "S_ALK" + +ADM1 Components +--------------- +Additional documentation on the ADM1 property model can be found here: `Anaerobic Digestion Model 1 Documentation `_ + +.. csv-table:: + :header: "Description", "Symbol", "Variable" + + "Monosaccharides, S_su", ":math:`S_{su}`", "S_su" + "Amino acids, S_aa", ":math:`S_{aa}`", "S_aa" + "Long chain fatty acids, S_fa", ":math:`S_{fa}`", "S_fa" + "Total valerate, S_va", ":math:`S_{va}`", "S_va" + "Total butyrate, S_bu", ":math:`S_{bu}`", "S_bu" + "Total propionate, S_pro", ":math:`S_{pro}`", "S_pro" + "Total acetate, S_ac", ":math:`S_{ac}`", "S_ac" + "Hydrogen gas, S_h2", ":math:`S_{h2}`", "S_h2" + "Methane gas, S_ch4", ":math:`S_{ch4}`", "S_ch4" + "Inorganic carbon, S_IC", ":math:`S_{IC}`", "S_IC" + "Inorganic nitrogen, S_IN", ":math:`S_{IN}`", "S_IN" + "Soluble inerts, S_I", ":math:`S_I`", "S_I" + "Composites, X_c", ":math:`X_c`", "X_c" + "Carbohydrates, X_ch", ":math:`X_{ch}`", "X_ch" + "Proteins, X_pr", ":math:`X_{pr}`", "X_pr" + "Lipids, X_li", ":math:`X_{li}`", "X_li" + "Sugar degraders, X_su", ":math:`X_{su}`", "X_su" + "Amino acid degraders, X_aa", ":math:`X_{aa}`", "X_aa" + "Long chain fatty acid (LCFA) degraders, X_fa", ":math:`X_{fa}`", "X_fa" + "Valerate and butyrate degraders, X_c4", ":math:`X_{c4}`", "X_c4" + "Propionate degraders, X_pro", ":math:`X_{pro}`", "X_pro" + "Acetate degraders, X_ac", ":math:`X_{ac}`", "X_ac" + "Hydrogen degraders, X_h2", ":math:`X_{h2}`", "X_h2" + "Particulate inerts, X_I", ":math:`X_I`", "X_I" + "Total cation equivalents concentration, S_cat", ":math:`S_{cat}`", "S_cat" + "Total anion equivalents concentration, S_an", ":math:`S_{an}`", "S_an" + "Carbon dioxide, S_co2", ":math:`S_{co2}`", "S_co2" + +**NOTE: S_h2 and S_ch4 have vapor phase and liquid phase, S_co2 only has vapor phase, and the other components only have liquid phase. The amount of CO2 dissolved in the liquid phase is equivalent to S_IC - S_HCO3-.** +Parameters +---------- + +.. csv-table:: + :header: "Description", "Symbol", "Parameter Name", "Value", "Units" + + "Nitrogen fraction in particulate products", ":math:`i_{xe}`", "i_xe", 0.06, ":math:`\text{dimensionless}`" + "Nitrogen fraction in biomass", ":math:`i_{xb}`", "i_xb", 0.08, ":math:`\text{dimensionless}`" + "Anaerobic degradable fraction of X_I and X_P", ":math:`f_{xI}`", "f_xI", 0.05, ":math:`\text{dimensionless}`" + +Equations and Relationships +--------------------------- +.. csv-table:: + :header: "Description", "Equation" + + "Pressure balance", ":math:`P_{out} = P_{in}`" + "Temperature balance", ":math:`T_{out} = T_{in}`" + "Volumetric flow equality", ":math:`F_{out} = F_{in}`" + "Total Kjeldahl nitrogen", ":math:`TKN = S_{NH} + S_{ND} + X_{ND} + i_{xb}(X_{BH} + X_{BA}) + i_{xe}(X_{I} + X_{P})`" + +COD Equations +------------- +The total incoming COD is reduced in a step-wise manner until the COD demand has been satisfied. The reduction is based on a +hierarchy of ASM1 state variables such that :math:`S_s` is reduced by the COD demand first. If there is insufficient +Ss present, then :math:`S_s` is reduced to zero and the remaining demand is subtracted from :math:`X_s`. If necessary, :math:`X_BH` and :math:`X_BA` may also need to be reduced. + +.. csv-table:: + :header: "Description", "Equation" + + "COD demand", ":math:`COD_{demand} = S_{o} + 2.86S_{NO}`" + "Readily biodegradable substrate remaining (step 1)", ":math:`S_{S, inter} = S_{S} - COD_{demand}`" + "Slowly biodegradable substrate remaining (step 2)", ":math:`X_{S, inter} = X_{S} - COD_{demand, 2}`" + "Active heterotrophic biomass remaining (step 3)", ":math:`X_{BH, inter} = X_{BH} - COD_{demand, 3}`" + "Active autotrophic biomass remaining (step 4)", ":math:`X_{BA, inter} = X_{BA} - COD_{demand, 4}`" + "Soluble COD", ":math:`COD_{s} = S_{I} + S_{S, inter}`" + "Particulate COD", ":math:`COD_{p} = X_{I} + X_{S, inter} + X_{BH, inter} + X_{BA, inter} + X_{P}`" + "Total COD", ":math:`COD_{t} = COD_{s} + COD_{p}`" + +S_nd and S_s Mapping Equations +------------------------------ + +.. figure:: ../../../_static/unit_models/translators/mapping_step_a.jpg + :width: 800 + :align: center + + Figure 1. Schematic illustration of Snd and Ss mapping (Copp et al. 2006) + +.. csv-table:: + :header: "Description", "Equation" + + "Required soluble COD", ":math:`ReqCOD_{s} = \frac{S_{ND}}{N_{aa}/14}`" + "Amino acids mapping (if :math:`S_{S,inter} > ReqCOD_{s}`)", ":math:`S_{aa} = ReqCOD_{s}`" + "Amino acids mapping (if :math:`S_{S,inter} ≤ ReqCOD_{s}`)", ":math:`S_{aa} = S_{S, inter}`" + "Monosaccharides mapping step A (if :math:`S_{S,inter} > ReqCOD_{s}`)", ":math:`S_{su, A} = S_{S, inter} - ReqCOD_{s}`" + "Monosaccharides mapping step A (if :math:`S_{S,inter} ≤ ReqCOD_{s}`)", ":math:`S_{su, A} = 0`" + "COD remaining from step A", ":math:`COD_{remain, A} = COD_{t} - S_{S,inter}`" + "Organic nitrogen pool remaining from step A", ":math:`OrgN_{remain, A} = TKN - (S_{aa} * N_{aa} * 14) - S_{NH}`" + +Soluble Inert COD Mapping Equations +----------------------------------- + +.. figure:: ../../../_static/unit_models/translators/mapping_step_b.jpg + :width: 800 + :align: center + + Figure 2. Schematic illustration of soluble inert COD mapping (Copp et al. 2006) + +.. csv-table:: + :header: "Description", "Equation" + + "Required soluble inert organic nitrogen", ":math:`OrgN_{s, req} = S_{I} * N_{I} * 14`" + "Soluble inert mapping step B (if :math:`OrgN_{remain, A} > OrgN_{s, req}`)", ":math:`S_{I, ADM1} = S_{I}`" + "Soluble inert mapping step B (if :math:`OrgN_{remain, A} ≤ OrgN_{s, req}`)", ":math:`S_{I, ADM1} = \frac{OrgN_{remain, A}}{N_{I}/14}`" + "Monosaccharides mapping step B (if :math:`OrgN_{remain, A} > OrgN_{s, req}`)", ":math:`S_{su} = S_{su, A}`" + "Monosaccharides mapping step B (if :math:`OrgN_{remain, A} ≤ OrgN_{s, req}`)", ":math:`S_{su} = S_{su, A} + S_{I} - S_{I, ADM1}`" + "COD remaining from step B", ":math:`COD_{remain, B} = COD_{remain, A} - S_{I}`" + "Organic nitrogen pool remaining from step B", ":math:`OrgN_{remain, B} = OrgN_{remain, A} - (S_{I, ADM1} * N_{I} * 14)`" + + +Particulate Inert COD Mapping Equations +--------------------------------------- + +.. figure:: ../../../_static/unit_models/translators/mapping_step_c.jpg + :width: 800 + :align: center + + Figure 3. Schematic illustration of particulate inert COD mapping (Copp et al. 2006) + +.. csv-table:: + :header: "Description", "Equation" + + "Required particulate inert material", ":math:`OrgN_{x, req} = f_{xi} * (X_{P} + X_{I}) * N_{I} * 14`" + "Particulate inert mapping step B (if :math:`OrgN_{remain, B} > OrgN_{x, req}`)", ":math:`X_{I, ADM1} = f_{xi} * (X_{P} + X_{I})`" + "Particulate inert mapping step B (if :math:`OrgN_{remain, B} ≤ OrgN_{x, req}`)", ":math:`X_{I, ADM1} = \frac{OrgN_{remain, B}}{N_{I}/14}`" + "COD remaining from step C", ":math:`COD_{remain, C} = COD_{remain, B} - X_{I, ADM1}`" + "Organic nitrogen pool remaining from step C", ":math:`OrgN_{remain, C} = OrgN_{remain, B} - (X_{I_ADM1} * N_{I} * 14)`" + +Final COD and TKN Mapping Equations +----------------------------------- + +.. figure:: ../../../_static/unit_models/translators/mapping_step_final.jpg + :width: 800 + :align: center + + Figure 4. Schematic illustration of final COD and TKN mapping (Copp et al. 2006) + +.. csv-table:: + :header: "Description", "Equation" + + "Required soluble COD", ":math:`COD_{Xc, req} = \frac{OrgN_{remain, C}}{N_{xc}/14}`" + "Composites mapping (if :math:`COD_{remain, C} > COD_{Xc, req}`)", ":math:`X_{C} = COD_{Xc, req}`" + "Composites mapping (if :math:`COD_{remain, C} ≤ COD_{Xc, req}`)", ":math:`X_{C} = COD_{remain, C}`" + "Carbohydrates mapping (if :math:`COD_{remain, C} > COD_{Xc, req}`)", ":math:`X_{ch} = \frac{f_{ch, xc} * (COD_{remain, C} - X_{C})}{f_{ch, xc} - f_{li, xc}}`" + "Carbohydrates mapping (if :math:`COD_{remain, C} ≤ COD_{Xc, req}`)", ":math:`X_{ch} = 0`" + "Lipids mapping (if :math:`COD_{remain, C} > COD_{Xc, req}`)", ":math:`X_{li} = \frac{f_{li, xc} * (COD_{remain, C} - X_{C})}{f_{ch, xc} - f_{li, xc}}`" + "Lipdis mapping (if :math:`COD_{remain, C} ≤ COD_{Xc, req}`)", ":math:`X_{li} = 0`" + "Inorganic nitrogen mapping (if :math:`COD_{remain, C} > COD_{Xc, req}`)", ":math:`S_{IN} = S_{NH, in}`" + "Inorganic nitrogen mapping (if :math:`COD_{remain, C} ≤ COD_{Xc, req}`)", ":math:`S_{IN} = S_{NH, in} + (OrgN_{remain, C} - X_{C} * N_{xc} * 14)`" + "Anions balance", ":math:`S_{an} = \frac{S_{IN}}{14}`" + "Cations balance", ":math:`S_{cat} = \frac{S_{IC}}{12}`" + + +Classes +------- +.. currentmodule:: watertap.unit_models.translators.translator_asm1_adm1 + +.. autoclass:: TranslatorDataASM1ADM1 + :members: + :noindex: + +References +---------- +[1] Copp J. and Jeppsson, U., Rosen, C., 2006. +Towards an ASM1 - ADM1 State Variable Interface for Plant-Wide Wastewater Treatment Modeling. +Proceedings of the Water Environment Federation, 2003, pp 498-510. +https://www.accesswater.org/publications/proceedings/-290550/towards-an-asm1---adm1-state-variable-interface-for-plant-wide-wastewater-treatment-modeling diff --git a/pyproject.toml b/pyproject.toml index ba9ee77274..9305c2e7e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,4 +16,5 @@ disable = [ # for a list of all messages enable = [ "unnecessary-pass", + "unused-import", ] diff --git a/setup.py b/setup.py index 7117e2129b..eeecf64e39 100644 --- a/setup.py +++ b/setup.py @@ -15,24 +15,10 @@ # Always prefer setuptools over distutils from setuptools import setup, find_namespace_packages -import pathlib - -cwd = pathlib.Path(__file__).parent.resolve() # this will come in handy, probably - -long_description = """WaterTAP is an open-source, integrated suite of predictive multi-scale models -for design and optimization of water treatment processes and systems. Specifically, WaterTAP is a new -library of water treatment-specific property, process unit, and network models that depend on the IDAES Platform, -an open source, next generation process systems engineering platform developed at the National Energy Technology -Laboratory with other partners. The WaterTAP project is funded by the NAWI as a part of U.S. Department of -Energy’s Energy-Water Desalination Hub. The goal of WaterTAP is to assist the hub and the broader water R&D -community in assessing existing and emerging water treatment technologies by 1) providing predictive capabilities -involving the design, optimization, and performance of water treatment systems that will lead to improved energy -efficiency and lower cost, 2) advancing the state of the art for the design of water treatment components, systems -and networks to be comparable with, or even surpass, that in the chemical industry, and 3) disseminating these tools -for active use by water treatment researchers and engineers.""".replace( - "\n", " " -).strip() +from pathlib import Path +cwd = Path(__file__).parent +long_description = (cwd / "README.md").read_text() SPECIAL_DEPENDENCIES_FOR_RELEASE = [ "idaes-pse==2.0.*", # from PyPI @@ -54,7 +40,7 @@ version="0.9.dev0", description="WaterTAP modeling library", long_description=long_description, - long_description_content_type="text/plain", + long_description_content_type="text/markdown", author="NAWI team", license="BSD", # Classifiers help users find your project by categorizing it. @@ -123,6 +109,7 @@ "jinja2<3.1.0", # see watertap-org/watertap#449 "Sphinx", # docs "sphinx_rtd_theme", # docs + "urllib3 < 2", # see watertap-org/watertap#1021, # other requirements "linkify-it-py", "json-schema-for-humans", # pretty JSON schema in HTML diff --git a/watertap/core/membrane_channel0d.py b/watertap/core/membrane_channel0d.py index ed4500dd9f..cd998ad855 100644 --- a/watertap/core/membrane_channel0d.py +++ b/watertap/core/membrane_channel0d.py @@ -28,9 +28,11 @@ from watertap.core.membrane_channel_base import ( MembraneChannelMixin, PressureChangeType, - CONFIG_Template, + CONFIG_Template as Base_CONFIG_Template, ) +CONFIG_Template = Base_CONFIG_Template() + @declare_process_block_class("MembraneChannel0DBlock") class MembraneChannel0DBlockData(MembraneChannelMixin, ControlVolume0DBlockData): diff --git a/watertap/core/membrane_channel1d.py b/watertap/core/membrane_channel1d.py index abccd5b0f9..fdda992b8f 100644 --- a/watertap/core/membrane_channel1d.py +++ b/watertap/core/membrane_channel1d.py @@ -10,13 +10,8 @@ # "https://github.com/watertap-org/watertap/" ################################################################################# -from copy import deepcopy - from pyomo.common.config import ConfigValue, In -from pyomo.environ import ( - Constraint, - Set, -) +from pyomo.environ import Set from idaes.core import ( declare_process_block_class, DistributedVars, diff --git a/watertap/core/tests/test_zero_order_costing.py b/watertap/core/tests/test_zero_order_costing.py index d1f0b8e665..c406f59b7c 100644 --- a/watertap/core/tests/test_zero_order_costing.py +++ b/watertap/core/tests/test_zero_order_costing.py @@ -26,7 +26,7 @@ Var, ) from pyomo.util.check_units import assert_units_consistent -from pyomo.common.config import ConfigBlock, ConfigValue +from pyomo.common.config import ConfigValue from idaes.core import FlowsheetBlock, declare_process_block_class from idaes.core.solvers import get_solver diff --git a/watertap/core/util/tests/test_initialization.py b/watertap/core/util/tests/test_initialization.py index 3e16a69097..233d5a709e 100644 --- a/watertap/core/util/tests/test_initialization.py +++ b/watertap/core/util/tests/test_initialization.py @@ -12,7 +12,7 @@ import pytest -from pyomo.environ import ConcreteModel, Var, Constraint, Block, SolverFactory +from pyomo.environ import ConcreteModel, Var, Constraint from idaes.core.solvers import get_solver from idaes.core.util.exceptions import InitializationError diff --git a/watertap/core/util/tests/test_scaling.py b/watertap/core/util/tests/test_scaling.py index be447469f3..47b325902b 100644 --- a/watertap/core/util/tests/test_scaling.py +++ b/watertap/core/util/tests/test_scaling.py @@ -22,7 +22,6 @@ import watertap.property_models.NaCl_prop_pack as props -from watertap.core.util.scaling import transform_property_constraints import idaes.logger as idaeslog __author__ = "Marcus Holly" diff --git a/watertap/costing/units/electroNP.py b/watertap/costing/units/electroNP.py new file mode 100644 index 0000000000..6a4003d488 --- /dev/null +++ b/watertap/costing/units/electroNP.py @@ -0,0 +1,89 @@ +################################################################################# +# WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# National Renewable Energy Laboratory, and National Energy Technology +# Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +################################################################################# + +import pyomo.environ as pyo +from ..util import ( + register_costing_parameter_block, + make_capital_cost_var, +) + + +def build_electroNP_cost_param_block(blk): + + blk.HRT = pyo.Var( + initialize=1.3333, + doc="Hydraulic retention time", + units=pyo.units.hr, + ) + blk.sizing_cost = pyo.Var( + initialize=1.25, + doc="Reactor sizing cost", + units=pyo.units.USD_2020 / pyo.units.m**3, + ) + + +@register_costing_parameter_block( + build_rule=build_electroNP_cost_param_block, + parameter_block_name="electroNP", +) +def cost_electroNP(blk, cost_electricity_flow=True, cost_MgCl2_flow=True): + """ + ElectroNP costing method + """ + cost_electroNP_capital( + blk, + blk.costing_package.electroNP.HRT, + blk.costing_package.electroNP.sizing_cost, + ) + + t0 = blk.flowsheet().time.first() + if cost_electricity_flow: + blk.costing_package.cost_flow( + pyo.units.convert( + blk.unit_model.electricity[t0], + to_units=pyo.units.kW, + ), + "electricity", + ) + + if cost_MgCl2_flow: + blk.costing_package.cost_flow( + pyo.units.convert( + blk.unit_model.MgCl2_flowrate[t0], + to_units=pyo.units.kg / pyo.units.hr, + ), + "magnesium chloride", + ) + + +def cost_electroNP_capital(blk, HRT, sizing_cost): + """ + Generic function for costing an ElectroNP system. + """ + make_capital_cost_var(blk) + + blk.HRT = pyo.Expression(expr=HRT) + blk.sizing_cost = pyo.Expression(expr=sizing_cost) + + flow_in = pyo.units.convert( + blk.unit_model.properties_in[0].flow_vol, + to_units=pyo.units.m**3 / pyo.units.hr, + ) + + print(f"base_currency: {blk.costing_package.base_currency}") + blk.capital_cost_constraint = pyo.Constraint( + expr=blk.capital_cost + == pyo.units.convert( + blk.HRT * flow_in * blk.sizing_cost, + to_units=blk.costing_package.base_currency, + ) + ) diff --git a/watertap/costing/units/gac.py b/watertap/costing/units/gac.py index 1c37687532..c7f45f2a76 100644 --- a/watertap/costing/units/gac.py +++ b/watertap/costing/units/gac.py @@ -28,136 +28,80 @@ class ContactorType(StrEnum): def build_gac_cost_param_block(blk): + # --------------------------------------------------------------------- + # design options + blk.num_contactors_op = pyo.Var( initialize=1, units=pyo.units.dimensionless, - doc="Number of GAC contactors in operation in parallel", + doc="number of GAC contactors in operation in parallel", ) blk.num_contactors_redundant = pyo.Var( initialize=1, units=pyo.units.dimensionless, - doc="Number of off-line redundant GAC contactors in parallel", - ) - blk.pres_contactor_cost_coeff_0 = pyo.Var( - initialize=10010.9, - units=pyo.units.USD_2020, - doc="GAC steel pressure contactor polynomial cost coefficient 0", - ) - blk.pres_contactor_cost_coeff_1 = pyo.Var( - initialize=2204.95, - units=pyo.units.USD_2020 * (pyo.units.m**3) ** -1, - doc="GAC steel pressure contactor polynomial cost coefficient 1", - ) - blk.pres_contactor_cost_coeff_2 = pyo.Var( - initialize=-15.9378, - units=pyo.units.USD_2020 * (pyo.units.m**3) ** -2, - doc="GAC steel pressure contactor polynomial cost coefficient 2", - ) - blk.pres_contactor_cost_coeff_3 = pyo.Var( - initialize=0.110592, - units=pyo.units.USD_2020 * (pyo.units.m**3) ** -3, - doc="GAC steel pressure contactor polynomial cost coefficient 3", - ) - blk.grav_contactor_cost_coeff_0 = pyo.Var( - initialize=75131.3, - units=pyo.units.USD_2020, - doc="GAC concrete gravity contactor polynomial cost coefficient 0", - ) - blk.grav_contactor_cost_coeff_1 = pyo.Var( - initialize=735.550, - units=pyo.units.USD_2020 * (pyo.units.m**3) ** -1, - doc="GAC concrete gravity contactor polynomial cost coefficient 1", - ) - blk.grav_contactor_cost_coeff_2 = pyo.Var( - initialize=-1.01827, - units=pyo.units.USD_2020 * (pyo.units.m**3) ** -2, - doc="GAC concrete gravity contactor polynomial cost coefficient 2", - ) - blk.grav_contactor_cost_coeff_3 = pyo.Var( - initialize=0, - units=pyo.units.USD_2020 * (pyo.units.m**3) ** -3, - doc="GAC concrete gravity contactor polynomial cost coefficient 3", + doc="number of off-line redundant GAC contactors in parallel", ) + blk.regen_frac = pyo.Var( + initialize=0.70, + units=pyo.units.dimensionless, + doc="fraction of spent GAC adsorbent that can be regenerated for reuse", + ) + + # --------------------------------------------------------------------- + # correlation reference points + blk.bed_mass_max_ref = pyo.Var( initialize=18143.7, units=pyo.units.kg, - doc="Reference maximum value of GAC mass needed for initial charge where " + doc="reference maximum value of GAC mass needed for initial charge where " "economy of scale no longer discounts the unit price", ) - blk.adsorbent_unit_cost_coeff = pyo.Var( - initialize=4.58342, - units=pyo.units.USD_2020 * pyo.units.kg**-1, - doc="GAC adsorbent exponential cost pre-exponential coefficient", - ) - blk.adsorbent_unit_cost_exp_coeff = pyo.Var( - initialize=-1.25311e-5, - units=pyo.units.kg**-1, - doc="GAC adsorbent exponential cost parameter coefficient", - ) - blk.pres_other_cost_coeff = pyo.Var( - initialize=16660.7, - units=pyo.units.USD_2020, - doc="GAC pressure other cost power law coefficient", - ) - blk.pres_other_cost_exp = pyo.Var( - initialize=0.552207, + + # --------------------------------------------------------------------- + # correlation parameter data + + # dummy data is used to initialize, fixed in cost_gac based on the ContactorType + # USD_2020 embedded in equation + contactor_cost_coeff_dummy = {0: 10000, 1: 1000, 2: -10, 3: 0.1} + blk.contactor_cost_coeff = pyo.Var( + contactor_cost_coeff_dummy.keys(), + initialize=contactor_cost_coeff_dummy, units=pyo.units.dimensionless, - doc="GAC pressure other cost power law exponent", - ) - blk.grav_other_cost_coeff = pyo.Var( - initialize=38846.9, - units=pyo.units.USD_2020, - doc="GAC gravity other cost power law coefficient", + doc="contactor polynomial cost coefficients", ) - blk.grav_other_cost_exp = pyo.Var( - initialize=0.490571, + # USD_2020 * kg**-1 embedded in equation adsorbent_unit_cost_constraint + adsorbent_unit_cost_coeff_dummy = {0: 1, 1: -1e-5} + blk.adsorbent_unit_cost_coeff = pyo.Var( + adsorbent_unit_cost_coeff_dummy.keys(), + initialize=adsorbent_unit_cost_coeff_dummy, units=pyo.units.dimensionless, - doc="GAC gravity other cost power law exponent", + doc="GAC adsorbent cost exponential function parameters", ) - blk.regen_frac = pyo.Var( - initialize=0.70, + # USD_2020 embedded in equation other_process_cost_constraint + other_cost_param_dummy = {0: 10000, 1: 0.1} + blk.other_cost_param = pyo.Var( + other_cost_param_dummy.keys(), + initialize=other_cost_param_dummy, units=pyo.units.dimensionless, - doc="Fraction of spent GAC adsorbent that can be regenerated for reuse", + doc="other process cost power law parameters", ) blk.regen_unit_cost = pyo.Var( initialize=4.28352, units=pyo.units.USD_2020 * pyo.units.kg**-1, - doc="Unit cost to regenerate spent GAC adsorbent by an offsite regeneration facility", + doc="unit cost to regenerate spent GAC adsorbent by an offsite regeneration facility", ) blk.makeup_unit_cost = pyo.Var( initialize=4.58223, units=pyo.units.USD_2020 * pyo.units.kg**-1, - doc="Unit cost to makeup spent GAC adsorbent with fresh adsorbent", - ) - blk.pres_energy_coeff_0 = pyo.Var( - initialize=8.09926e-4, - units=pyo.units.kW, - doc="GAC steel pressure contactor polynomial energy consumption coefficient 0", + doc="unit cost to makeup spent GAC adsorbent with fresh adsorbent", ) - blk.pres_energy_coeff_1 = pyo.Var( - initialize=8.70577e-4, - units=pyo.units.kW * (pyo.units.m**3) ** -1, - doc="GAC steel pressure contactor polynomial energy consumption coefficient 1", - ) - blk.pres_energy_coeff_2 = pyo.Var( - initialize=0, - units=pyo.units.kW * (pyo.units.m**3) ** -2, - doc="GAC steel pressure contactor polynomial energy consumption coefficient 2", - ) - blk.grav_energy_coeff_0 = pyo.Var( - initialize=0.123782, - units=pyo.units.kW, - doc="GAC concrete gravity contactor polynomial energy consumption coefficient 0", - ) - blk.grav_energy_coeff_1 = pyo.Var( - initialize=0.132403, - units=pyo.units.kW * (pyo.units.m**3) ** -1, - doc="GAC concrete gravity contactor polynomial energy consumption coefficient 1", - ) - blk.grav_energy_coeff_2 = pyo.Var( - initialize=-1.41512e-5, - units=pyo.units.kW * (pyo.units.m**3) ** -2, - doc="GAC concrete gravity contactor polynomial energy consumption coefficient 2", + # kW embedded in equation energy_consumption_constraint + energy_consumption_coeff_dummy = {0: 1e-3, 1: 1e-3, 2: 0} + blk.energy_consumption_coeff = pyo.Var( + energy_consumption_coeff_dummy.keys(), + initialize=energy_consumption_coeff_dummy, + units=pyo.units.dimensionless, + doc="energy consumption polynomial coefficients", ) @@ -178,6 +122,43 @@ def cost_gac(blk, contactor_type=ContactorType.pressure): contactor_type: ContactorType Enum indicating whether to cost based on steel pressure vessels or concrete, default = ContactorType.pressure """ + + # --------------------------------------------------------------------- + # with ContactorType not assigned in build_gac_cost_param_block and blk.costing_package.gac variables + # fixed when register_costing_parameter_block, refix parameters based on contactor type here + + # costing data parameters based on contactor type + adsorbent_unit_cost_coeff_data = {0: 4.58342, 1: -1.25311e-5} + if contactor_type == ContactorType.pressure: + contactor_cost_coeff_data = {0: 10010.9, 1: 2204.95, 2: -15.9378, 3: 0.110592} + other_cost_param_data = {0: 16660.7, 1: 0.552207} + energy_consumption_coeff_data = {0: 8.09926e-4, 1: 8.70577e-4, 2: 0} + elif contactor_type == ContactorType.gravity: + contactor_cost_coeff_data = {0: 75131.3, 1: 735.550, 2: -1.01827, 3: 0.000000} + other_cost_param_data = {0: 38846.9, 1: 0.490571} + energy_consumption_coeff_data = {0: 0.123782, 1: 0.132403, 2: -1.41512e-5} + else: + raise ConfigurationError( + f"{blk.unit_model.name} received invalid argument for contactor_type:" + f" {contactor_type}. Argument must be a member of the ContactorType Enum." + ) + + # iterable matching coeff_data to vars + gac_cost = blk.costing_package.gac + cost_params = ( + (adsorbent_unit_cost_coeff_data, gac_cost.adsorbent_unit_cost_coeff), + (contactor_cost_coeff_data, gac_cost.contactor_cost_coeff), + (other_cost_param_data, gac_cost.other_cost_param), + (energy_consumption_coeff_data, gac_cost.energy_consumption_coeff), + ) + + # refix variables to appropriate costing parameters + for indexed_data, indexed_var in cost_params: + for index, var in indexed_var.items(): + var.fix(indexed_data[index]) + + # --------------------------------------------------------------------- + make_capital_cost_var(blk) blk.contactor_cost = pyo.Var( initialize=1e5, @@ -227,40 +208,26 @@ def cost_gac(blk, contactor_type=ContactorType.pressure): ) total_bed_volume = num_contactors * unit_contactor_volume - def rule_contactor_cost(b): - if contactor_type == ContactorType.pressure: - return b.contactor_cost == num_contactors * pyo.units.convert( - ( - b.costing_package.gac.pres_contactor_cost_coeff_3 - * unit_contactor_volume**3 - + b.costing_package.gac.pres_contactor_cost_coeff_2 - * unit_contactor_volume**2 - + b.costing_package.gac.pres_contactor_cost_coeff_1 - * unit_contactor_volume**1 - + b.costing_package.gac.pres_contactor_cost_coeff_0 - ), - to_units=b.costing_package.base_currency, - ) - elif contactor_type == ContactorType.gravity: - return b.contactor_cost == num_contactors * pyo.units.convert( - ( - b.costing_package.gac.grav_contactor_cost_coeff_3 - * unit_contactor_volume**3 - + b.costing_package.gac.grav_contactor_cost_coeff_2 - * unit_contactor_volume**2 - + b.costing_package.gac.grav_contactor_cost_coeff_1 - * unit_contactor_volume**1 - + b.costing_package.gac.grav_contactor_cost_coeff_0 - ), - to_units=b.costing_package.base_currency, - ) - else: - raise ConfigurationError( - f"{blk.unit_model.name} received invalid argument for contactor_type:" - f" {contactor_type}. Argument must be a member of the ContactorType Enum." + blk.contactor_cost_constraint = pyo.Constraint( + expr=blk.contactor_cost + == num_contactors + * pyo.units.convert( + ( + blk.costing_package.gac.contactor_cost_coeff[3] + * (pyo.units.m**3) ** -3 + * unit_contactor_volume**3 + + blk.costing_package.gac.contactor_cost_coeff[2] + * (pyo.units.m**3) ** -2 + * unit_contactor_volume**2 + + blk.costing_package.gac.contactor_cost_coeff[1] + * (pyo.units.m**3) ** -1 + * unit_contactor_volume**1 + + blk.costing_package.gac.contactor_cost_coeff[0] ) - - blk.contactor_cost_constraint = pyo.Constraint(expr=rule_contactor_cost) + * pyo.units.USD_2020, + to_units=blk.costing_package.base_currency, + ) + ) blk.bed_mass_gac_ref_constraint = pyo.Constraint( expr=blk.bed_mass_gac_ref @@ -274,11 +241,14 @@ def rule_contactor_cost(b): blk.adsorbent_unit_cost_constraint = pyo.Constraint( expr=blk.adsorbent_unit_cost == pyo.units.convert( - blk.costing_package.gac.adsorbent_unit_cost_coeff + blk.costing_package.gac.adsorbent_unit_cost_coeff[0] * pyo.exp( blk.bed_mass_gac_ref - * blk.costing_package.gac.adsorbent_unit_cost_exp_coeff - ), + * pyo.units.kg**-1 + * blk.costing_package.gac.adsorbent_unit_cost_coeff[1] + ) + * pyo.units.USD_2020 + * pyo.units.kg**-1, to_units=blk.costing_package.base_currency * pyo.units.kg**-1, ) ) @@ -286,27 +256,18 @@ def rule_contactor_cost(b): expr=blk.adsorbent_cost == blk.adsorbent_unit_cost * blk.unit_model.bed_mass_gac ) - def rule_other_process_cost(b): - if contactor_type == ContactorType.pressure: - return b.other_process_cost == pyo.units.convert( - ( - b.costing_package.gac.pres_other_cost_coeff - * ((pyo.units.m**3) ** -b.costing_package.gac.pres_other_cost_exp) - * total_bed_volume**b.costing_package.gac.pres_other_cost_exp - ), - to_units=b.costing_package.base_currency, - ) - elif contactor_type == ContactorType.gravity: - return b.other_process_cost == pyo.units.convert( - ( - b.costing_package.gac.grav_other_cost_coeff - * ((pyo.units.m**3) ** -b.costing_package.gac.grav_other_cost_exp) - * total_bed_volume**b.costing_package.gac.grav_other_cost_exp - ), - to_units=b.costing_package.base_currency, + blk.other_process_cost_constraint = pyo.Constraint( + expr=blk.other_process_cost + == pyo.units.convert( + ( + blk.costing_package.gac.other_cost_param[0] + * (total_bed_volume * pyo.units.m**-3) + ** blk.costing_package.gac.other_cost_param[1] ) - - blk.other_process_cost_constraint = pyo.Constraint(expr=rule_other_process_cost) + * pyo.units.USD_2020, + to_units=blk.costing_package.base_currency, + ) + ) blk.capital_cost_constraint = pyo.Constraint( expr=blk.capital_cost @@ -356,21 +317,12 @@ def rule_other_process_cost(b): expr=blk.fixed_operating_cost == blk.gac_regen_cost + blk.gac_makeup_cost ) - def rule_energy_consumption(b): - if contactor_type == ContactorType.pressure: - return b.energy_consumption == ( - b.costing_package.gac.pres_energy_coeff_2 * (total_bed_volume**2) - ) + (b.costing_package.gac.pres_energy_coeff_1 * total_bed_volume) + ( - b.costing_package.gac.pres_energy_coeff_0 - ) - elif contactor_type == ContactorType.gravity: - return b.energy_consumption == ( - b.costing_package.gac.grav_energy_coeff_2 * (total_bed_volume**2) - ) + (b.costing_package.gac.grav_energy_coeff_1 * total_bed_volume) + ( - b.costing_package.gac.grav_energy_coeff_0 - ) - - blk.energy_consumption_constraint = pyo.Constraint(expr=rule_energy_consumption) + blk.energy_consumption_constraint = pyo.Constraint( + expr=blk.energy_consumption + == blk.costing_package.gac.energy_consumption_coeff[2] * total_bed_volume**2 + + blk.costing_package.gac.energy_consumption_coeff[1] * total_bed_volume + + blk.costing_package.gac.energy_consumption_coeff[0] + ) blk.costing_package.cost_flow( pyo.units.convert(blk.energy_consumption, to_units=pyo.units.kW), diff --git a/watertap/costing/watertap_costing_package.py b/watertap/costing/watertap_costing_package.py index fde27cba5b..f568fd3452 100644 --- a/watertap/costing/watertap_costing_package.py +++ b/watertap/costing/watertap_costing_package.py @@ -37,6 +37,7 @@ EnergyRecoveryDevice, Electrodialysis0D, Electrodialysis1D, + ElectroNPZO, IonExchange0D, GAC, ) @@ -55,6 +56,7 @@ from .units.pump import cost_pump from .units.reverse_osmosis import cost_reverse_osmosis from .units.uv_aop import cost_uv_aop +from .units.electroNP import cost_electroNP class _DefinedFlowsDict(MutableMapping, dict): @@ -95,6 +97,7 @@ class WaterTAPCostingData(FlowsheetCostingBlockData): Ultraviolet0D: cost_uv_aop, Electrodialysis0D: cost_electrodialysis, Electrodialysis1D: cost_electrodialysis, + ElectroNPZO: cost_electroNP, IonExchange0D: cost_ion_exchange, GAC: cost_gac, } @@ -159,6 +162,14 @@ def build_global_params(self): units=pyo.units.kg / pyo.units.kWh, ) + self.magnesium_chloride_cost = pyo.Param( + mutable=True, + initialize=0.0786, + doc="Magnesium chloride cost", + units=pyo.units.USD_2020 / pyo.units.kg, + ) + self.add_defined_flow("magnesium chloride", self.magnesium_chloride_cost) + # fix the parameters self.fix_all_vars() diff --git a/watertap/edb/tests/test_commands.py b/watertap/edb/tests/test_commands.py index 58d3439923..8791756dae 100644 --- a/watertap/edb/tests/test_commands.py +++ b/watertap/edb/tests/test_commands.py @@ -20,7 +20,6 @@ import pytest from click import Command from click.testing import CliRunner, Result -from _pytest.monkeypatch import MonkeyPatch import pymongo from watertap.edb import commands, ElectrolyteDB diff --git a/watertap/examples/chemistry/tests/test_chlorination.py b/watertap/examples/chemistry/tests/test_chlorination.py index 0510223d3e..556c8a5b8e 100644 --- a/watertap/examples/chemistry/tests/test_chlorination.py +++ b/watertap/examples/chemistry/tests/test_chlorination.py @@ -72,12 +72,6 @@ log_power_law_equil, ) -# Import k-value functions -from idaes.models.properties.modular_properties.reactions.equilibrium_constant import ( - gibbs_energy, - van_t_hoff, -) - # Import built-in van't Hoff function from idaes.models.properties.modular_properties.reactions.equilibrium_constant import ( van_t_hoff, @@ -129,10 +123,8 @@ # Import scaling helper functions from watertap.examples.chemistry.chem_scaling_utils import ( - _set_eps_vals, _set_equ_rxn_scaling, _set_inherent_rxn_scaling, - _set_mat_bal_scaling_FpcTP, _set_mat_bal_scaling_FTPx, _set_ene_bal_scaling, ) diff --git a/watertap/examples/chemistry/tests/test_enrtl_water_pH.py b/watertap/examples/chemistry/tests/test_enrtl_water_pH.py index f932fe3555..e2e652cd71 100644 --- a/watertap/examples/chemistry/tests/test_enrtl_water_pH.py +++ b/watertap/examples/chemistry/tests/test_enrtl_water_pH.py @@ -75,14 +75,7 @@ # Import idaes methods to check the model during construction from idaes.core.solvers import get_solver -from idaes.core.util.model_statistics import ( - degrees_of_freedom, - fixed_variables_set, - activated_constraints_set, - number_variables, - number_total_constraints, - number_unused_variables, -) +from idaes.core.util.model_statistics import degrees_of_freedom # Import the idaes objects for Generic Properties and Reactions from idaes.models.properties.modular_properties.base.generic_property import ( diff --git a/watertap/examples/chemistry/tests/test_pH_dependent_solubility.py b/watertap/examples/chemistry/tests/test_pH_dependent_solubility.py index 4856631d06..ccce31b334 100644 --- a/watertap/examples/chemistry/tests/test_pH_dependent_solubility.py +++ b/watertap/examples/chemistry/tests/test_pH_dependent_solubility.py @@ -64,7 +64,6 @@ # Importing the object for units from pyomo from pyomo.environ import units as pyunits -from pyomo.environ import Var # Imports from idaes core from idaes.core import AqueousPhase, SolidPhase, FlowsheetBlock, EnergyBalanceType @@ -72,10 +71,8 @@ from idaes.core.base.phases import PhaseType as PT # Imports from idaes generic models -import idaes -import idaes.models.properties.modular_properties.pure.Perrys as Perrys from idaes.models.properties.modular_properties.pure.ConstantProperties import Constant -from idaes.models.properties.modular_properties.state_definitions import FTPx, FpcTP +from idaes.models.properties.modular_properties.state_definitions import FpcTP from idaes.models.properties.modular_properties.eos.ideal import Ideal # Importing the enum for concentration unit basis used in the 'get_concentration_term' function @@ -89,7 +86,6 @@ # Import safe log power law equation from idaes.models.properties.modular_properties.reactions.equilibrium_forms import ( log_power_law_equil, - power_law_equil, ) # Import built-in van't Hoff function @@ -98,12 +94,7 @@ ) from idaes.models.properties.modular_properties.reactions.equilibrium_forms import ( - solubility_product, log_solubility_product, - log_power_law_equil, -) -from idaes.models.properties.modular_properties.reactions.equilibrium_constant import ( - ConstantKeq, ) # Import specific pyomo objects @@ -116,23 +107,12 @@ ) from idaes.core.util import scaling as iscale -from idaes.core.util.initialization import fix_state_vars, revert_state_vars import idaes.logger as idaeslog -# Import pyomo methods to check the system units -from pyomo.util.check_units import assert_units_consistent - # Import idaes methods to check the model during construction from idaes.core.solvers import get_solver -from idaes.core.util.model_statistics import ( - degrees_of_freedom, - fixed_variables_set, - activated_constraints_set, - number_variables, - number_total_constraints, - number_unused_variables, -) +from idaes.core.util.model_statistics import degrees_of_freedom # Import the idaes objects for Generic Properties and Reactions from idaes.models.properties.modular_properties.base.generic_property import ( @@ -153,7 +133,6 @@ _set_eps_vals, _set_equ_rxn_scaling, _set_mat_bal_scaling_FpcTP, - _set_mat_bal_scaling_FTPx, _set_ene_bal_scaling, ) diff --git a/watertap/examples/chemistry/tests/test_pure_water_pH.py b/watertap/examples/chemistry/tests/test_pure_water_pH.py index ec6616df91..ef25cf9d7d 100644 --- a/watertap/examples/chemistry/tests/test_pure_water_pH.py +++ b/watertap/examples/chemistry/tests/test_pure_water_pH.py @@ -24,8 +24,7 @@ # Imports from idaes core from idaes.core import AqueousPhase -from idaes.core.base.components import Solvent, Solute, Cation, Anion -from idaes.core.base.phases import PhaseType as PT +from idaes.core.base.components import Solvent, Cation, Anion # Imports from idaes generic models import idaes.models.properties.modular_properties.pure.Perrys as Perrys @@ -47,7 +46,6 @@ # Import k-value functions from idaes.models.properties.modular_properties.reactions.equilibrium_constant import ( - gibbs_energy, van_t_hoff, ) diff --git a/watertap/examples/chemistry/tests/test_recarbonation_process.py b/watertap/examples/chemistry/tests/test_recarbonation_process.py index f6fbc08077..1f2f79d1c9 100644 --- a/watertap/examples/chemistry/tests/test_recarbonation_process.py +++ b/watertap/examples/chemistry/tests/test_recarbonation_process.py @@ -51,7 +51,6 @@ from idaes.models.properties.modular_properties.reactions.dh_rxn import constant_dh_rxn from idaes.models.properties.modular_properties.reactions.equilibrium_constant import ( - gibbs_energy, van_t_hoff, ) @@ -87,10 +86,8 @@ # Import scaling helper functions from watertap.examples.chemistry.chem_scaling_utils import ( - _set_eps_vals, _set_equ_rxn_scaling, _set_mat_bal_scaling_FpcTP, - _set_mat_bal_scaling_FTPx, _set_ene_bal_scaling, ) diff --git a/watertap/examples/chemistry/tests/test_remineralization.py b/watertap/examples/chemistry/tests/test_remineralization.py index d827104c9a..ade0012dfc 100644 --- a/watertap/examples/chemistry/tests/test_remineralization.py +++ b/watertap/examples/chemistry/tests/test_remineralization.py @@ -51,7 +51,7 @@ from pyomo.util.check_units import assert_units_consistent # Imports from idaes core -from idaes.core import AqueousPhase, VaporPhase, FlowsheetBlock, EnergyBalanceType +from idaes.core import AqueousPhase, FlowsheetBlock, EnergyBalanceType from idaes.core.base.components import Solvent, Solute, Cation, Anion, Apparent from idaes.core.base.phases import PhaseType as PT @@ -61,10 +61,7 @@ from idaes.models.properties.modular_properties.eos.ideal import Ideal from idaes.models.properties.modular_properties.pure.ConstantProperties import Constant from idaes.models.properties.modular_properties.base.generic_property import StateIndex -from idaes.models.properties.modular_properties.phase_equil import SmoothVLE -from idaes.models.properties.modular_properties.phase_equil.bubble_dew import ( - IdealBubbleDew, -) + from idaes.models.properties.modular_properties.phase_equil.forms import fugacity # Importing the generic model information and objects @@ -79,7 +76,6 @@ power_law_rate, ) from idaes.models.properties.modular_properties.reactions.equilibrium_constant import ( - gibbs_energy, van_t_hoff, ) from idaes.models.properties.modular_properties.reactions.rate_constant import arrhenius @@ -103,8 +99,6 @@ # Import idaes methods to check the model during construction from idaes.core.util import scaling as iscale -from idaes.core.util.initialization import fix_state_vars, revert_state_vars -from idaes.core.util.scaling import badly_scaled_var_generator from idaes.core.solvers import get_solver from idaes.core.util.model_statistics import degrees_of_freedom @@ -115,7 +109,6 @@ _set_inherent_rxn_scaling, _set_rate_rxn_scaling, _set_mat_bal_scaling_FTPx, - _set_ene_bal_scaling, ) __author__ = "Austin Ladshaw" diff --git a/watertap/examples/chemistry/tests/test_seawater_alkalinity.py b/watertap/examples/chemistry/tests/test_seawater_alkalinity.py index 94c708ffe4..d45c342a76 100644 --- a/watertap/examples/chemistry/tests/test_seawater_alkalinity.py +++ b/watertap/examples/chemistry/tests/test_seawater_alkalinity.py @@ -56,7 +56,6 @@ # Import k-value functions from idaes.models.properties.modular_properties.reactions.equilibrium_constant import ( - gibbs_energy, van_t_hoff, ) @@ -82,7 +81,6 @@ activated_constraints_set, number_variables, number_total_constraints, - number_unused_variables, ) # Import the idaes objects for Generic Properties and Reactions diff --git a/watertap/examples/chemistry/tests/test_solids.py b/watertap/examples/chemistry/tests/test_solids.py index 5d36a73ddd..d0d3a818aa 100644 --- a/watertap/examples/chemistry/tests/test_solids.py +++ b/watertap/examples/chemistry/tests/test_solids.py @@ -34,15 +34,13 @@ # Importing the object for units from pyomo from pyomo.environ import units as pyunits -from pyomo.environ import Var # Imports from idaes core from idaes.core import AqueousPhase, SolidPhase, FlowsheetBlock, EnergyBalanceType -from idaes.core.base.components import Solvent, Solute, Cation, Anion, Component +from idaes.core.base.components import Solvent, Solute, Component from idaes.core.base.phases import PhaseType as PT # Imports from idaes generic models -import idaes.models.properties.modular_properties.pure.Perrys as Perrys from idaes.models.properties.modular_properties.pure.ConstantProperties import Constant from idaes.models.properties.modular_properties.state_definitions import FTPx, FpcTP from idaes.models.properties.modular_properties.eos.ideal import Ideal @@ -55,12 +53,6 @@ # Import the object/function for heat of reaction from idaes.models.properties.modular_properties.reactions.dh_rxn import constant_dh_rxn -# Import safe log power law equation -from idaes.models.properties.modular_properties.reactions.equilibrium_forms import ( - log_power_law_equil, - power_law_equil, -) - # Import built-in van't Hoff function from idaes.models.properties.modular_properties.reactions.equilibrium_constant import ( van_t_hoff, @@ -69,10 +61,6 @@ from idaes.models.properties.modular_properties.reactions.equilibrium_forms import ( solubility_product, log_solubility_product, - log_power_law_equil, -) -from idaes.models.properties.modular_properties.reactions.equilibrium_constant import ( - ConstantKeq, ) # Import specific pyomo objects @@ -94,14 +82,7 @@ # Import idaes methods to check the model during construction from idaes.core.solvers import get_solver -from idaes.core.util.model_statistics import ( - degrees_of_freedom, - fixed_variables_set, - activated_constraints_set, - number_variables, - number_total_constraints, - number_unused_variables, -) +from idaes.core.util.model_statistics import degrees_of_freedom # Import the idaes objects for Generic Properties and Reactions from idaes.models.properties.modular_properties.base.generic_property import ( @@ -114,16 +95,12 @@ # Import the idaes object for the EquilibriumReactor unit model from idaes.models.unit_models.equilibrium_reactor import EquilibriumReactor -# Import log10 function from pyomo -from pyomo.environ import log10 - # Import scaling helper functions from watertap.examples.chemistry.chem_scaling_utils import ( _set_eps_vals, _set_equ_rxn_scaling, _set_mat_bal_scaling_FpcTP, _set_mat_bal_scaling_FTPx, - _set_ene_bal_scaling, ) __author__ = "Austin Ladshaw" diff --git a/watertap/examples/chemistry/tests/test_water_softening.py b/watertap/examples/chemistry/tests/test_water_softening.py index de8f3790d2..6beda005c2 100644 --- a/watertap/examples/chemistry/tests/test_water_softening.py +++ b/watertap/examples/chemistry/tests/test_water_softening.py @@ -88,9 +88,6 @@ log_power_law_equil, ) -# Import log10 function from pyomo -from pyomo.environ import log10 - __author__ = "Srikanth Allu, Austin Ladshaw" thermo_config = { diff --git a/watertap/examples/custom_model_demo/tests/test_demo_simple_filter.py b/watertap/examples/custom_model_demo/tests/test_demo_simple_filter.py index 8950c5503a..6b85265234 100644 --- a/watertap/examples/custom_model_demo/tests/test_demo_simple_filter.py +++ b/watertap/examples/custom_model_demo/tests/test_demo_simple_filter.py @@ -11,12 +11,8 @@ ################################################################################# import pytest -from pyomo.environ import ConcreteModel, assert_optimal_termination, value -from pyomo.util.check_units import assert_units_consistent -from idaes.core import FlowsheetBlock +from pyomo.environ import value from idaes.core.solvers import get_solver -from idaes.core.util.model_statistics import degrees_of_freedom -import idaes.core.util.scaling as iscale from watertap.examples.custom_model_demo.demo_simple_filter import main diff --git a/watertap/examples/custom_model_demo/tests/test_demo_simple_prop_pack.py b/watertap/examples/custom_model_demo/tests/test_demo_simple_prop_pack.py index 15d3732b5a..c796ee74cd 100644 --- a/watertap/examples/custom_model_demo/tests/test_demo_simple_prop_pack.py +++ b/watertap/examples/custom_model_demo/tests/test_demo_simple_prop_pack.py @@ -11,18 +11,14 @@ ################################################################################# import pytest -from pyomo.environ import ConcreteModel, assert_optimal_termination, value -from pyomo.util.check_units import assert_units_consistent -from idaes.core import FlowsheetBlock +from pyomo.environ import value from idaes.core.solvers import get_solver -from idaes.core.util.model_statistics import degrees_of_freedom -import idaes.core.util.scaling as iscale from watertap.examples.custom_model_demo.demo_simple_prop_pack import main solver = get_solver() -# ----------------------------------------------------------------------------- + @pytest.mark.component def test_demo_simple_prop_pack(): m = main() diff --git a/watertap/examples/edb/simple_acid.py b/watertap/examples/edb/simple_acid.py index c4bc8d0064..92065960a8 100644 --- a/watertap/examples/edb/simple_acid.py +++ b/watertap/examples/edb/simple_acid.py @@ -95,16 +95,11 @@ # ========================== (3 & 4) ================================ # Import ElectrolyteDB object -from watertap.edb import ElectrolyteDB -from watertap.examples.edb.the_basics import ( - connect_to_edb, - is_thermo_config_valid, - grab_base_reaction_config, - is_thermo_reaction_pair_valid, -) +from watertap.examples.edb.the_basics import grab_base_reaction_config __author__ = "Austin Ladshaw" + # ========================== (5) ================================ # Grab a new base config for our thermo, but this time we will use # one of the newer bases that will use the FpcTP state vars and diff --git a/watertap/examples/edb/solid_precipitation_reactions.py b/watertap/examples/edb/solid_precipitation_reactions.py index 12edc58f8b..7cd6730754 100644 --- a/watertap/examples/edb/solid_precipitation_reactions.py +++ b/watertap/examples/edb/solid_precipitation_reactions.py @@ -75,46 +75,16 @@ """ -# ========= These imports (below) are for testing the configs from EDB =============== -# Import specific pyomo objects -from pyomo.environ import ( - ConcreteModel, -) - -# Import the idaes objects for Generic Properties and Reactions -from idaes.models.properties.modular_properties.base.generic_property import ( - GenericParameterBlock, -) -from idaes.models.properties.modular_properties.base.generic_reaction import ( - GenericReactionParameterBlock, -) - -# Import the idaes object for the EquilibriumReactor unit model -from idaes.models.unit_models.equilibrium_reactor import EquilibriumReactor - -# Import the core idaes objects for Flowsheets and types of balances -from idaes.core import FlowsheetBlock - -# ========= These imports (above) are for testing the configs from EDB =============== - - -# ========================== (3 & 4) ================================ # Import ElectrolyteDB object -from watertap.edb import ElectrolyteDB -from watertap.examples.edb.the_basics import ( - connect_to_edb, - is_thermo_config_valid, - grab_base_reaction_config, - is_thermo_reaction_pair_valid, -) +from watertap.examples.edb.the_basics import grab_base_reaction_config from watertap.examples.edb.simple_acid import ( get_components_and_add_to_idaes_config, - add_equilibrium_reactions_to_react_base, build_equilibrium_model, ) __author__ = "Austin Ladshaw" + # ========================== (5) ================================ # Grab a new base config for our thermo, but this time we will use # one of the newer bases that will use the FpcTP state vars and diff --git a/watertap/examples/edb/vapor_liquid_equilibrium.py b/watertap/examples/edb/vapor_liquid_equilibrium.py index 2f0f8e3ca1..caead3043f 100644 --- a/watertap/examples/edb/vapor_liquid_equilibrium.py +++ b/watertap/examples/edb/vapor_liquid_equilibrium.py @@ -72,38 +72,8 @@ """ -# ========= These imports (below) are for testing the configs from EDB =============== -# Import specific pyomo objects -from pyomo.environ import ( - ConcreteModel, -) - -# Import the idaes objects for Generic Properties and Reactions -from idaes.models.properties.modular_properties.base.generic_property import ( - GenericParameterBlock, -) -from idaes.models.properties.modular_properties.base.generic_reaction import ( - GenericReactionParameterBlock, -) - -# Import the idaes object for the EquilibriumReactor unit model -from idaes.models.unit_models.equilibrium_reactor import EquilibriumReactor - -# Import the core idaes objects for Flowsheets and types of balances -from idaes.core import FlowsheetBlock - -# ========= These imports (above) are for testing the configs from EDB =============== - - -# ========================== (3 & 4) ================================ # Import ElectrolyteDB object -from watertap.edb import ElectrolyteDB -from watertap.examples.edb.the_basics import ( - connect_to_edb, - is_thermo_config_valid, - grab_base_reaction_config, - is_thermo_reaction_pair_valid, -) +from watertap.examples.edb.the_basics import grab_base_reaction_config from watertap.examples.edb.simple_acid import ( get_components_and_add_to_idaes_config, add_equilibrium_reactions_to_react_base, @@ -112,6 +82,7 @@ __author__ = "Austin Ladshaw" + # ========================== (5) ================================ # Grab a new base config for our thermo, but this time we will use # one of the newer bases that will use the FpcTP state vars and diff --git a/watertap/examples/flowsheets/RO_with_energy_recovery/tests/test_RO_with_energy_recovery_simulation.py b/watertap/examples/flowsheets/RO_with_energy_recovery/tests/test_RO_with_energy_recovery_simulation.py index 4937497005..9393045859 100644 --- a/watertap/examples/flowsheets/RO_with_energy_recovery/tests/test_RO_with_energy_recovery_simulation.py +++ b/watertap/examples/flowsheets/RO_with_energy_recovery/tests/test_RO_with_energy_recovery_simulation.py @@ -15,26 +15,15 @@ Block, Var, Constraint, - TerminationCondition, - SolverStatus, value, - SolverFactory, Expression, - TransformationFactory, - units as pyunits, ) -from pyomo.network import Arc, Port +from pyomo.network import Port from idaes.core import FlowsheetBlock from idaes.core.solvers import get_solver from idaes.core.util.model_statistics import degrees_of_freedom, number_total_objectives -from idaes.core.util.initialization import solve_indexed_blocks, propagate_state from idaes.models.unit_models import Mixer, Separator, Product, Feed -from idaes.models.unit_models.mixer import MomentumMixingType from pyomo.util.check_units import assert_units_consistent -from idaes.core.util.scaling import ( - unscaled_variables_generator, - unscaled_constraints_generator, -) import watertap.property_models.NaCl_prop_pack as props from watertap.unit_models.reverse_osmosis_0D import ReverseOsmosis0D diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/GLSD_anaerobic_digester/GLSD_anaerobic_digestion_ui.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/GLSD_anaerobic_digester/GLSD_anaerobic_digestion_ui.py index 5d87f4d020..5b4aa79931 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/GLSD_anaerobic_digester/GLSD_anaerobic_digestion_ui.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/GLSD_anaerobic_digester/GLSD_anaerobic_digestion_ui.py @@ -18,7 +18,6 @@ solve, add_costing, ) -from idaes.core.solvers import get_solver from pyomo.environ import units as pyunits, assert_optimal_termination from pyomo.util.check_units import assert_units_consistent @@ -444,7 +443,7 @@ def build_flowsheet(): results = solve(m) assert_optimal_termination(results) - return m.fs + return m def solve_flowsheet(flowsheet=None): diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1575_hrcs/hrcs_ui.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1575_hrcs/hrcs_ui.py index 4163b20d8c..dcb1f06c74 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1575_hrcs/hrcs_ui.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1575_hrcs/hrcs_ui.py @@ -18,7 +18,6 @@ solve, add_costing, ) -from idaes.core.solvers import get_solver from pyomo.environ import units as pyunits, assert_optimal_termination from pyomo.util.check_units import assert_units_consistent @@ -780,7 +779,7 @@ def build_flowsheet(): results = solve(m) assert_optimal_termination(results) - return m.fs + return m def solve_flowsheet(flowsheet=None): diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1575_hrcs/multi_sweep.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1575_hrcs/multi_sweep.py index db621d1aac..decf4aca81 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1575_hrcs/multi_sweep.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1575_hrcs/multi_sweep.py @@ -9,16 +9,13 @@ # information, respectively. These files are also available online at the URL # "https://github.com/watertap-org/watertap/" ################################################################################# -import os -import sys from watertap.tools.parameter_sweep import LinearSample, parameter_sweep - import watertap.examples.flowsheets.case_studies.wastewater_resource_recovery.amo_1575_hrcs.hrcs as hrcs def set_up_sensitivity(m): outputs = {} - optimize_kwargs = {"check_termination": False} + optimize_kwargs = {"fail_flag": False} opt_function = hrcs.solve # create outputs @@ -61,14 +58,4 @@ def run_analysis(case_num=1, nx=11, interpolate_nan_outputs=True, results_path=N if __name__ == "__main__": - if len(sys.argv) == 1: - print( - "Usage: Specify the conditions in the run_analysis function and then run 'python multi_sweep.py' " - "Case number (case_num) is an integer, number_of_samples (nx) is an integer, interpolate_nan_outputs is a" - "boolean and results_path is the file path where the results will be created and displayed." - ) - print( - f"Results will be written to {os.path.dirname(os.path.abspath(__file__))}" - ) - else: - results, sweep_params, m = run_analysis(*sys.argv[1:]) + results, sweep_params, m = run_analysis() diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1575_hrcs/tests/test_multi_sweep.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1575_hrcs/tests/test_multi_sweep.py index 48ae9590be..4e6416b5c1 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1575_hrcs/tests/test_multi_sweep.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1575_hrcs/tests/test_multi_sweep.py @@ -17,12 +17,12 @@ multi_sweep, ) -sweep_list = [] -for case_num in [1]: - sweep_list.append(case_num) +pytest_parameterize_list = [] +for case_num in [1, 2]: + pytest_parameterize_list.append(case_num) -@pytest.mark.parametrize("case_num", sweep_list) +@pytest.mark.parametrize("case_num", pytest_parameterize_list) @pytest.mark.integration def test_multi_sweep(case_num, tmp_path): cwd = os.getcwd() diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1575_magprex/magprex_ui.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1575_magprex/magprex_ui.py index 30ee6c7c9b..5a2d1cb846 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1575_magprex/magprex_ui.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1575_magprex/magprex_ui.py @@ -18,7 +18,6 @@ solve, add_costing, ) -from idaes.core.solvers import get_solver from pyomo.environ import units as pyunits, assert_optimal_termination from pyomo.util.check_units import assert_units_consistent @@ -714,7 +713,7 @@ def build_flowsheet(): results = solve(m) assert_optimal_termination(results) - return m.fs + return m def solve_flowsheet(flowsheet=None): diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1575_magprex/multi_sweep.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1575_magprex/multi_sweep.py index e26ceb74a6..6025d0f221 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1575_magprex/multi_sweep.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1575_magprex/multi_sweep.py @@ -9,15 +9,13 @@ # information, respectively. These files are also available online at the URL # "https://github.com/watertap-org/watertap/" ################################################################################# -import os -import sys from watertap.tools.parameter_sweep import LinearSample, parameter_sweep import watertap.examples.flowsheets.case_studies.wastewater_resource_recovery.amo_1575_magprex.magprex as magprex def set_up_sensitivity(m): outputs = {} - optimize_kwargs = {"check_termination": False} + optimize_kwargs = {"fail_flag": False} opt_function = magprex.solve # create outputs @@ -61,14 +59,4 @@ def run_analysis(case_num=1, nx=11, interpolate_nan_outputs=True, results_path=N if __name__ == "__main__": - if len(sys.argv) == 1: - print( - "Usage: Specify the conditions in the run_analysis function and then run 'python multi_sweep.py' " - "Case number (case_num) is an integer, number_of_samples (nx) is an integer, interpolate_nan_outputs is a" - "boolean and results_path is the file path where the results will be created and displayed." - ) - print( - f"Results will be written to {os.path.dirname(os.path.abspath(__file__))}" - ) - else: - results, sweep_params, m = run_analysis(*sys.argv[1:]) + results, sweep_params, m = run_analysis() diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1575_magprex/tests/test_multi_sweep.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1575_magprex/tests/test_multi_sweep.py index 1a7cdd8403..b695073234 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1575_magprex/tests/test_multi_sweep.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1575_magprex/tests/test_multi_sweep.py @@ -17,12 +17,12 @@ multi_sweep, ) -sweep_list = [] -for case_num in [1]: - sweep_list.append(case_num) +pytest_parameterize_list = [] +for case_num in [1, 2]: + pytest_parameterize_list.append(case_num) -@pytest.mark.parametrize("case_num", sweep_list) +@pytest.mark.parametrize("case_num", pytest_parameterize_list) @pytest.mark.integration def test_multi_sweep(case_num, tmp_path): cwd = os.getcwd() diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1595_photothermal_membrane_candoP/amo_1595_sweep.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1595_photothermal_membrane_candoP/amo_1595_sweep.py index 88616c3f93..28905c168a 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1595_photothermal_membrane_candoP/amo_1595_sweep.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1595_photothermal_membrane_candoP/amo_1595_sweep.py @@ -9,17 +9,13 @@ # information, respectively. These files are also available online at the URL # "https://github.com/watertap-org/watertap/" ################################################################################# - -import os, sys - from watertap.tools.parameter_sweep import LinearSample, parameter_sweep - import watertap.examples.flowsheets.case_studies.wastewater_resource_recovery.amo_1595_photothermal_membrane_candoP.amo_1595 as amo_1595 def set_up_sensitivity(m): outputs = {} - optimize_kwargs = {"check_termination": False} + optimize_kwargs = {"fail_flag": False} opt_function = amo_1595.solve # create outputs @@ -64,14 +60,4 @@ def run_analysis(case_num=1, nx=2, interpolate_nan_outputs=True, save_outputs=No if __name__ == "__main__": - if len(sys.argv) == 1: - print( - "Usage: Specify the conditions in the run_analysis function and then run 'python multi_sweep.py' " - "Case number (case_num) is an integer, number_of_samples (nx) is an integer, interpolate_nan_outputs is a" - "boolean and results_path is the file path where the results will be created and displayed." - ) - print( - f"Results will be written to {os.path.dirname(os.path.abspath(__file__))}" - ) - else: - results, sweep_params, m = run_analysis(*sys.argv[1:]) + results, sweep_params, m = run_analysis() diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1595_photothermal_membrane_candoP/amo_1595_ui.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1595_photothermal_membrane_candoP/amo_1595_ui.py index d513520dd8..272c396370 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1595_photothermal_membrane_candoP/amo_1595_ui.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1595_photothermal_membrane_candoP/amo_1595_ui.py @@ -18,7 +18,6 @@ solve, add_costing, ) -from idaes.core.solvers import get_solver from pyomo.environ import units as pyunits, assert_optimal_termination from pyomo.util.check_units import assert_units_consistent @@ -827,7 +826,7 @@ def build_flowsheet(): results = solve(m) assert_optimal_termination(results) - return m.fs + return m def solve_flowsheet(flowsheet=None): diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1690/amo_1690_sweep.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1690/amo_1690_sweep.py index bb30ec2268..f41fbbce2e 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1690/amo_1690_sweep.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1690/amo_1690_sweep.py @@ -9,17 +9,13 @@ # information, respectively. These files are also available online at the URL # "https://github.com/watertap-org/watertap/" ################################################################################# - -import sys - from watertap.tools.parameter_sweep import LinearSample, parameter_sweep - import watertap.examples.flowsheets.case_studies.wastewater_resource_recovery.amo_1690.amo_1690 as amo_1690 def set_up_sensitivity(m): outputs = {} - optimize_kwargs = {"check_termination": False} + optimize_kwargs = {"fail_flag": False} opt_function = amo_1690.solve # create outputs @@ -61,20 +57,8 @@ def run_analysis(case_num=1, nx=11, interpolate_nan_outputs=True, save_path=None interpolate_nan_outputs=interpolate_nan_outputs, ) - print(global_results) - return global_results, sweep_params, m if __name__ == "__main__": - if len(sys.argv) == 1: - print( - "Usage: Specify the conditions in the run_analysis function and then run 'python multi_sweep.py' " - "Case number (case_num) is an integer, number_of_samples (nx) is an integer, interpolate_nan_outputs is a" - "boolean and results_path is the file path where the results will be created and displayed." - ) - print( - f"Results will be written to {os.path.dirname(os.path.abspath(__file__))}" - ) - else: - results, sweep_params, m = run_analysis(*sys.argv[1:]) + results, sweep_params, m = run_analysis() diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1690/amo_1690_ui.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1690/amo_1690_ui.py index 8cb09c3c5c..47815ab341 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1690/amo_1690_ui.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1690/amo_1690_ui.py @@ -18,7 +18,6 @@ solve, add_costing, ) -from idaes.core.solvers import get_solver from pyomo.environ import units as pyunits, assert_optimal_termination from pyomo.util.check_units import assert_units_consistent @@ -926,7 +925,7 @@ def build_flowsheet(): results = solve(m) assert_optimal_termination(results) - return m.fs + return m def solve_flowsheet(flowsheet=None): diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1690/tests/test_amo_1690_sweep.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1690/tests/test_amo_1690_sweep.py index 7432c55852..bcf69e8530 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1690/tests/test_amo_1690_sweep.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1690/tests/test_amo_1690_sweep.py @@ -39,4 +39,3 @@ def test_sweep(case_num, tmp_path): ) os.remove(temp.name) os.chdir(cwd) - return diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/biomembrane_filtration/biomembrane_filtration_ui.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/biomembrane_filtration/biomembrane_filtration_ui.py index 4331bef55f..93ac5a5185 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/biomembrane_filtration/biomembrane_filtration_ui.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/biomembrane_filtration/biomembrane_filtration_ui.py @@ -18,7 +18,6 @@ solve, add_costing, ) -from idaes.core.solvers import get_solver from pyomo.environ import units as pyunits, assert_optimal_termination from pyomo.util.check_units import assert_units_consistent @@ -677,7 +676,7 @@ def build_flowsheet(): results = solve(m) assert_optimal_termination(results) - return m.fs + return m def solve_flowsheet(flowsheet=None): diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/biomembrane_filtration/multi_sweep.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/biomembrane_filtration/multi_sweep.py index e4de94bf60..46c2482735 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/biomembrane_filtration/multi_sweep.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/biomembrane_filtration/multi_sweep.py @@ -9,8 +9,6 @@ # information, respectively. These files are also available online at the URL # "https://github.com/watertap-org/watertap/" ################################################################################# -import os -import sys from watertap.tools.parameter_sweep import ( LinearSample, parameter_sweep, @@ -20,7 +18,7 @@ def set_up_sensitivity(m): outputs = {} - optimize_kwargs = {"check_termination": False} + optimize_kwargs = {"fail_flag": False} opt_function = biomembrane_filtration.solve # create outputs @@ -73,14 +71,4 @@ def run_analysis(case_num=1, nx=11, interpolate_nan_outputs=True, save_outputs=N if __name__ == "__main__": - if len(sys.argv) == 1: - print( - "Usage: Specify the conditions in the run_analysis function and then run 'python multi_sweep.py' " - "Case number (case_num) is an integer, number_of_samples (nx) is an integer, interpolate_nan_outputs is a" - "boolean and results_path is the file path where the results will be created and displayed." - ) - print( - f"Results will be written to {os.path.dirname(os.path.abspath(__file__))}" - ) - else: - results, sweep_params, m = run_analysis(*sys.argv[1:]) + results, sweep_params, m = run_analysis() diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/dye_desalination/dye_desalination_ui.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/dye_desalination/dye_desalination_ui.py index 376cb145df..4e0ffa99c4 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/dye_desalination/dye_desalination_ui.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/dye_desalination/dye_desalination_ui.py @@ -18,7 +18,6 @@ solve, add_costing, ) -from idaes.core.solvers import get_solver from pyomo.environ import units as pyunits, assert_optimal_termination from pyomo.util.check_units import assert_units_consistent @@ -742,7 +741,7 @@ def build_flowsheet(): results = solve(m) assert_optimal_termination(results) - return m.fs + return m def solve_flowsheet(flowsheet=None): diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/dye_desalination/dye_sweep.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/dye_desalination/dye_sweep.py index b8f8ba3cac..56a60db640 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/dye_desalination/dye_sweep.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/dye_desalination/dye_sweep.py @@ -9,10 +9,6 @@ # information, respectively. These files are also available online at the URL # "https://github.com/watertap-org/watertap/" ################################################################################# - -import sys -import os - from watertap.tools.parameter_sweep import LinearSample, parameter_sweep import watertap.examples.flowsheets.case_studies.wastewater_resource_recovery.dye_desalination.dye_desalination as dye_desalination import watertap.examples.flowsheets.case_studies.wastewater_resource_recovery.dye_desalination.dye_desalination_withRO as dye_desalination_withRO @@ -31,7 +27,7 @@ def set_up_sensitivity(m, withRO): else: opt_function = dye_desalination.solve - optimize_kwargs = {"check_termination": False} + optimize_kwargs = {"fail_flag": False} return outputs, optimize_kwargs, opt_function @@ -189,14 +185,4 @@ def run_analysis( if __name__ == "__main__": - if len(sys.argv) == 1: - print( - "Usage: Specify the conditions in the run_analysis function and then run 'python multi_sweep.py' " - "Case number (case_num) is an integer, number_of_samples (nx) is an integer, interpolate_nan_outputs is a" - "boolean and results_path is the file path where the results will be created and displayed." - ) - print( - f"Results will be written to {os.path.dirname(os.path.abspath(__file__))}" - ) - else: - results, sweep_params, m = run_analysis(*sys.argv[1:]) + results, sweep_params, m = run_analysis() diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/electrochemical_nutrient_removal/electrochemical_nutrient_removal_ui.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/electrochemical_nutrient_removal/electrochemical_nutrient_removal_ui.py index de0478fa8e..f877ab9f72 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/electrochemical_nutrient_removal/electrochemical_nutrient_removal_ui.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/electrochemical_nutrient_removal/electrochemical_nutrient_removal_ui.py @@ -18,7 +18,6 @@ solve, add_costing, ) -from idaes.core.solvers import get_solver from pyomo.environ import units as pyunits, assert_optimal_termination from pyomo.util.check_units import assert_units_consistent @@ -561,7 +560,7 @@ def build_flowsheet(): results = solve(m) assert_optimal_termination(results) - return m.fs + return m def solve_flowsheet(flowsheet=None): diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/electrochemical_nutrient_removal/multi_sweep.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/electrochemical_nutrient_removal/multi_sweep.py index 49e416f90a..1e8a370f22 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/electrochemical_nutrient_removal/multi_sweep.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/electrochemical_nutrient_removal/multi_sweep.py @@ -9,8 +9,6 @@ # information, respectively. These files are also available online at the URL # "https://github.com/watertap-org/watertap/" ################################################################################# -import os -import sys from watertap.tools.parameter_sweep import ( LinearSample, parameter_sweep, @@ -20,7 +18,7 @@ def set_up_sensitivity(m): outputs = {} - optimize_kwargs = {"check_termination": False} + optimize_kwargs = {"fail_flag": False} opt_function = electrochemical_nutrient_removal.solve # create outputs @@ -59,14 +57,4 @@ def run_analysis(case_num=1, nx=11, interpolate_nan_outputs=True, results_path=N if __name__ == "__main__": - if len(sys.argv) == 1: - print( - "Usage: Specify the conditions in the run_analysis function and then run 'python multi_sweep.py' " - "Case number (case_num) is an integer, number_of_samples (nx) is an integer, interpolate_nan_outputs is a" - "boolean and results_path is the file path where the results will be created and displayed." - ) - print( - f"Results will be written to {os.path.dirname(os.path.abspath(__file__))}" - ) - else: - results, sweep_params, m = run_analysis(*sys.argv[1:]) + results, sweep_params, m = run_analysis() diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/groundwater_treatment/groundwater_treatment_sweep.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/groundwater_treatment/groundwater_treatment_sweep.py index e545aea474..6129adc979 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/groundwater_treatment/groundwater_treatment_sweep.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/groundwater_treatment/groundwater_treatment_sweep.py @@ -9,17 +9,13 @@ # information, respectively. These files are also available online at the URL # "https://github.com/watertap-org/watertap/" ################################################################################# - -import os, sys - from watertap.tools.parameter_sweep import LinearSample, parameter_sweep - import watertap.examples.flowsheets.case_studies.wastewater_resource_recovery.groundwater_treatment.groundwater_treatment as groundwater_treatment def set_up_sensitivity(m): outputs = {} - optimize_kwargs = {"check_termination": False} + optimize_kwargs = {"fail_flag": False} opt_function = groundwater_treatment.solve # create outputs @@ -45,16 +41,12 @@ def run_analysis(case_num=1, nx=2, interpolate_nan_outputs=True): raise ValueError(f"{case_num} is not yet implemented") output_filename = "sensitivity_" + str(case_num) + ".csv" - output_path = os.path.join( - os.path.dirname(os.path.abspath(__file__)), - output_filename, - ) global_results = parameter_sweep( m, sweep_params, outputs, - csv_results_file_name=output_path, + csv_results_file_name=output_filename, optimize_function=opt_function, optimize_kwargs=optimize_kwargs, interpolate_nan_outputs=interpolate_nan_outputs, @@ -66,14 +58,4 @@ def run_analysis(case_num=1, nx=2, interpolate_nan_outputs=True): if __name__ == "__main__": - if len(sys.argv) == 1: - print( - "Usage: Specify the conditions in the run_analysis function and then run 'python multi_sweep.py' " - "Case number (case_num) is an integer, number_of_samples (nx) is an integer, interpolate_nan_outputs is a" - "boolean and results_path is the file path where the results will be created and displayed." - ) - print( - f"Results will be written to {os.path.dirname(os.path.abspath(__file__))}" - ) - else: - results, sweep_params, m = run_analysis(*sys.argv[1:]) + results, sweep_params, m = run_analysis() diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/groundwater_treatment/groundwater_treatment_ui.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/groundwater_treatment/groundwater_treatment_ui.py index fbe648d3e0..56ba333eba 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/groundwater_treatment/groundwater_treatment_ui.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/groundwater_treatment/groundwater_treatment_ui.py @@ -18,7 +18,6 @@ solve, add_costing, ) -from idaes.core.solvers import get_solver from pyomo.environ import units as pyunits, assert_optimal_termination from pyomo.util.check_units import assert_units_consistent @@ -745,7 +744,7 @@ def build_flowsheet(): results = solve(m) assert_optimal_termination(results) - return m.fs + return m def solve_flowsheet(flowsheet=None): diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/metab/metab_ui.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/metab/metab_ui.py index 1de6f7c1f1..fc83603de1 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/metab/metab_ui.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/metab/metab_ui.py @@ -19,7 +19,6 @@ add_costing, adjust_default_parameters, ) -from idaes.core.solvers import get_solver from pyomo.environ import units as pyunits, assert_optimal_termination from pyomo.util.check_units import assert_units_consistent @@ -909,7 +908,7 @@ def build_flowsheet(): results = solve(m) assert_optimal_termination(results) - return m.fs + return m def solve_flowsheet(flowsheet=None): diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/metab/multi_sweep.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/metab/multi_sweep.py index f5c172eed0..cc065bcf83 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/metab/multi_sweep.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/metab/multi_sweep.py @@ -9,10 +9,7 @@ # information, respectively. These files are also available online at the URL # "https://github.com/watertap-org/watertap/" ################################################################################# -import sys -import os import time - from pyomo.environ import Constraint from watertap.tools.parameter_sweep import LinearSample, parameter_sweep import watertap.tools.MPI as MPI @@ -21,7 +18,7 @@ def set_up_sensitivity(m): outputs = {} - optimize_kwargs = {"check_termination": False} # None + optimize_kwargs = {"fail_flag": False} # None opt_function = metab.solve # tie parameters together @@ -39,14 +36,11 @@ def set_up_sensitivity(m): # new baseline parameters m.fs.costing.metab.bead_cost["hydrogen"].fix(14.4) - # m.fs.costing.metab.hydraulic_retention_time['hydrogen'].fix() - # m.fs.costing.metab.hydraulic_retention_time['methane'].fix() # create outputs outputs["LCOW"] = m.fs.costing.LCOW outputs["LCOH"] = m.fs.costing.LCOH outputs["LCOM"] = m.fs.costing.LCOM - # outputs["Effluent Volumetric Flow Rate"] = m.fs.product_H2O.properties[0].flow_vol return outputs, optimize_kwargs, opt_function @@ -137,7 +131,6 @@ def run_analysis(case_num=1, nx=11, interpolate_nan_outputs=True): csv_results_file_name=output_filename, optimize_function=opt_function, optimize_kwargs=optimize_kwargs, - # debugging_data_dir=os.path.split(output_filename)[0] + '/local', interpolate_nan_outputs=interpolate_nan_outputs, ) @@ -174,14 +167,4 @@ def main(case_num=1, nx=11, interpolate_nan_outputs=True): if __name__ == "__main__": - if len(sys.argv) == 1: - print( - "Usage: Specify the conditions in the run_analysis function and then run 'python multi_sweep.py' " - "Case number (case_num) is an integer, number_of_samples (nx) is an integer, interpolate_nan_outputs is a" - "boolean and results_path is the file path where the results will be created and displayed." - ) - print( - f"Results will be written to {os.path.dirname(os.path.abspath(__file__))}" - ) - else: - results, sweep_params, m = run_analysis(*sys.argv[1:]) + results, sweep_params = run_analysis() diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/metab/tests/test_metab.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/metab/tests/test_metab.py index 1fd1af54e5..3b72189216 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/metab/tests/test_metab.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/metab/tests/test_metab.py @@ -11,36 +11,14 @@ ################################################################################# import pytest from pyomo.environ import ( - ConcreteModel, - Block, - Var, - Constraint, - TerminationCondition, - SolverStatus, value, assert_optimal_termination, - SolverFactory, - Expression, - TransformationFactory, - units as pyunits, ) -from pyomo.network import Arc, Port -from idaes.core import FlowsheetBlock from idaes.core.solvers import get_solver -from idaes.core.util.model_statistics import degrees_of_freedom -from idaes.core.util.initialization import solve_indexed_blocks, propagate_state -from idaes.models.unit_models import Mixer, Separator, Product, Feed -from idaes.models.unit_models.mixer import MomentumMixingType from pyomo.util.check_units import assert_units_consistent -from idaes.core.util.scaling import ( - unscaled_variables_generator, - unscaled_constraints_generator, -) - from watertap.core.util.initialization import assert_degrees_of_freedom from watertap.examples.flowsheets.case_studies.wastewater_resource_recovery.metab.metab import ( - main, build, set_operating_conditions, initialize_system, @@ -51,10 +29,9 @@ display_additional_results, ) - solver = get_solver() -# ----------------------------------------------------------------------------- + class TestMetabFlowsheet: @pytest.fixture(scope="class") def system_frame(self): diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/peracetic_acid_disinfection/peracetic_acid_disinfection_sweep.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/peracetic_acid_disinfection/peracetic_acid_disinfection_sweep.py index d6797be36f..66e94faeba 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/peracetic_acid_disinfection/peracetic_acid_disinfection_sweep.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/peracetic_acid_disinfection/peracetic_acid_disinfection_sweep.py @@ -9,17 +9,13 @@ # information, respectively. These files are also available online at the URL # "https://github.com/watertap-org/watertap/" ################################################################################# - -import os, sys - from watertap.tools.parameter_sweep import LinearSample, parameter_sweep - import watertap.examples.flowsheets.case_studies.wastewater_resource_recovery.peracetic_acid_disinfection.peracetic_acid_disinfection as peracetic_acid_disinfection def set_up_sensitivity(m): outputs = {} - optimize_kwargs = {"check_termination": False} + optimize_kwargs = {"fail_flag": False} opt_function = peracetic_acid_disinfection.solve # create outputs @@ -50,20 +46,8 @@ def run_analysis(case_num=1, nx=11, interpolate_nan_outputs=True, save_path=None interpolate_nan_outputs=interpolate_nan_outputs, ) - print(global_results) - return global_results, sweep_params, m if __name__ == "__main__": - if len(sys.argv) == 1: - print( - "Usage: Specify the conditions in the run_analysis function and then run 'python multi_sweep.py' " - "Case number (case_num) is an integer, number_of_samples (nx) is an integer, interpolate_nan_outputs is a" - "boolean and results_path is the file path where the results will be created and displayed." - ) - print( - f"Results will be written to {os.path.dirname(os.path.abspath(__file__))}" - ) - else: - results, sweep_params, m = run_analysis(*sys.argv[1:]) + results, sweep_params, m = run_analysis() diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/peracetic_acid_disinfection/peracetic_acid_disinfection_ui.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/peracetic_acid_disinfection/peracetic_acid_disinfection_ui.py index 7ac06019bb..f3491efb84 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/peracetic_acid_disinfection/peracetic_acid_disinfection_ui.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/peracetic_acid_disinfection/peracetic_acid_disinfection_ui.py @@ -18,7 +18,6 @@ solve, add_costing, ) -from idaes.core.solvers import get_solver from pyomo.environ import units as pyunits, assert_optimal_termination from pyomo.util.check_units import assert_units_consistent @@ -482,7 +481,7 @@ def build_flowsheet(): results = solve(m) assert_optimal_termination(results) - return m.fs + return m def solve_flowsheet(flowsheet=None): diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/suboxic_activated_sludge_process/multi_sweep.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/suboxic_activated_sludge_process/multi_sweep.py index dd81a10e2c..e2f6048c1e 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/suboxic_activated_sludge_process/multi_sweep.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/suboxic_activated_sludge_process/multi_sweep.py @@ -9,8 +9,6 @@ # information, respectively. These files are also available online at the URL # "https://github.com/watertap-org/watertap/" ################################################################################# -import os -import sys from watertap.tools.parameter_sweep import ( LinearSample, parameter_sweep, @@ -20,7 +18,7 @@ def set_up_sensitivity(m): outputs = {} - optimize_kwargs = {"check_termination": False} + optimize_kwargs = {"fail_flag": False} opt_function = suboxic_activated_sludge_process.solve # create outputs @@ -61,16 +59,11 @@ def run_analysis(case_num=1, nx=11, interpolate_nan_outputs=True): output_filename = "sensitivity_" + str(case_num) + ".csv" - output_path = os.path.join( - os.path.dirname(os.path.abspath(__file__)), - output_filename, - ) - global_results = parameter_sweep( m, sweep_params, outputs, - csv_results_file_name=output_path, + csv_results_file_name=output_filename, optimize_function=opt_function, optimize_kwargs=optimize_kwargs, interpolate_nan_outputs=interpolate_nan_outputs, @@ -80,14 +73,4 @@ def run_analysis(case_num=1, nx=11, interpolate_nan_outputs=True): if __name__ == "__main__": - if len(sys.argv) == 1: - print( - "Usage: Specify the conditions in the run_analysis function and then run 'python multi_sweep.py' " - "Case number (case_num) is an integer, number_of_samples (nx) is an integer, interpolate_nan_outputs is a" - "boolean and results_path is the file path where the results will be created and displayed." - ) - print( - f"Results will be written to {os.path.dirname(os.path.abspath(__file__))}" - ) - else: - results, sweep_params, m = run_analysis(*sys.argv[1:]) + results, sweep_params, m = run_analysis() diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/suboxic_activated_sludge_process/suboxic_ASM_ui.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/suboxic_activated_sludge_process/suboxic_ASM_ui.py index 0996717eae..acedd49757 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/suboxic_activated_sludge_process/suboxic_ASM_ui.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/suboxic_activated_sludge_process/suboxic_ASM_ui.py @@ -18,7 +18,6 @@ solve, add_costing, ) -from idaes.core.solvers import get_solver from pyomo.environ import units as pyunits, assert_optimal_termination from pyomo.util.check_units import assert_units_consistent @@ -558,7 +557,7 @@ def build_flowsheet(): results = solve(m) assert_optimal_termination(results) - return m.fs + return m def solve_flowsheet(flowsheet=None): diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/supercritical_sludge_to_gas/multi_sweep.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/supercritical_sludge_to_gas/multi_sweep.py index c1f7356ba1..8ba3c1ed8a 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/supercritical_sludge_to_gas/multi_sweep.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/supercritical_sludge_to_gas/multi_sweep.py @@ -9,8 +9,6 @@ # information, respectively. These files are also available online at the URL # "https://github.com/watertap-org/watertap/" ################################################################################# -import os -import sys from watertap.tools.parameter_sweep import ( LinearSample, parameter_sweep, @@ -20,7 +18,7 @@ def set_up_sensitivity(m): outputs = {} - optimize_kwargs = {"check_termination": False} + optimize_kwargs = {"fail_flag": False} opt_function = supercritical_sludge_to_gas.solve # create outputs @@ -71,14 +69,4 @@ def run_analysis(case_num=1, nx=11, interpolate_nan_outputs=True, save_outputs=N if __name__ == "__main__": - if len(sys.argv) == 1: - print( - "Usage: Specify the conditions in the run_analysis function and then run 'python multi_sweep.py' " - "Case number (case_num) is an integer, number_of_samples (nx) is an integer, interpolate_nan_outputs is a" - "boolean and results_path is the file path where the results will be created and displayed." - ) - print( - f"Results will be written to {os.path.dirname(os.path.abspath(__file__))}" - ) - else: - results, sweep_params, m = run_analysis(*sys.argv[1:]) + results, sweep_params, m = run_analysis() diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/supercritical_sludge_to_gas/supercritical_sludge_to_gas_ui.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/supercritical_sludge_to_gas/supercritical_sludge_to_gas_ui.py index de35b5826a..6a1c84d4a8 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/supercritical_sludge_to_gas/supercritical_sludge_to_gas_ui.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/supercritical_sludge_to_gas/supercritical_sludge_to_gas_ui.py @@ -627,7 +627,7 @@ def build_flowsheet(): results = solve(m) assert_optimal_termination(results) - return m.fs + return m def solve_flowsheet(flowsheet=None): diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/swine_wwt/multi_sweep.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/swine_wwt/multi_sweep.py index d021f3ba11..d481e47d45 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/swine_wwt/multi_sweep.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/swine_wwt/multi_sweep.py @@ -9,8 +9,6 @@ # information, respectively. These files are also available online at the URL # "https://github.com/watertap-org/watertap/" ################################################################################# -import os -import sys from watertap.tools.parameter_sweep import ( LinearSample, parameter_sweep, @@ -20,7 +18,7 @@ def set_up_sensitivity(m): outputs = {} - optimize_kwargs = {"check_termination": False} + optimize_kwargs = {"fail_flag": False} opt_function = swine_wwt.solve # create outputs @@ -88,14 +86,4 @@ def run_analysis(case_num=1, nx=5, interpolate_nan_outputs=True, save_outputs=No if __name__ == "__main__": - if len(sys.argv) == 1: - print( - "Usage: Specify the conditions in the run_analysis function and then run 'python multi_sweep.py' " - "Case number (case_num) is an integer, number_of_samples (nx) is an integer, interpolate_nan_outputs is a" - "boolean and results_path is the file path where the results will be created and displayed." - ) - print( - f"Results will be written to {os.path.dirname(os.path.abspath(__file__))}" - ) - else: - results, sweep_params, m = run_analysis(*sys.argv[1:]) + results, sweep_params, m = run_analysis() diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/swine_wwt/swine_wwt_ui.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/swine_wwt/swine_wwt_ui.py index 8597656b7c..1af64af479 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/swine_wwt/swine_wwt_ui.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/swine_wwt/swine_wwt_ui.py @@ -1173,7 +1173,7 @@ def build_flowsheet(): results = solve(m) assert_optimal_termination(results) - return m.fs + return m def solve_flowsheet(flowsheet=None): diff --git a/watertap/examples/flowsheets/electrodialysis/tests/test_electrodialysis_1stack.py b/watertap/examples/flowsheets/electrodialysis/tests/test_electrodialysis_1stack.py index fb5218ffa4..dd400bc62c 100644 --- a/watertap/examples/flowsheets/electrodialysis/tests/test_electrodialysis_1stack.py +++ b/watertap/examples/flowsheets/electrodialysis/tests/test_electrodialysis_1stack.py @@ -14,39 +14,24 @@ from pyomo.environ import ( ConcreteModel, value, - Set, Var, - Constraint, Objective, - TransformationFactory, - assert_optimal_termination, Block, ) -from pyomo.network import Arc, Port +from pyomo.network import Port from idaes.core import ( FlowsheetBlock, - UnitModelCostingBlock, MaterialBalanceType, MomentumBalanceType, ) -from idaes.core.solvers import get_solver -from idaes.core.util.initialization import propagate_state -from idaes.core.util.model_statistics import degrees_of_freedom, report_statistics +from idaes.core.util.model_statistics import degrees_of_freedom from idaes.models.unit_models import Feed, Product, Separator -from pandas import DataFrame -import idaes.core.util.scaling as iscale -import idaes.logger as idaeslogger from watertap.unit_models.electrodialysis_1D import ( ElectricalOperationMode, Electrodialysis1D, ) -from watertap.core.util.initialization import ( - assert_no_degrees_of_freedom, - assert_degrees_of_freedom, -) -from watertap.costing import WaterTAPCosting from watertap.property_models.multicomp_aq_sol_prop_pack import MCASParameterBlock import watertap.examples.flowsheets.electrodialysis.electrodialysis_1stack as edfs diff --git a/watertap/examples/flowsheets/full_treatment_train/analysis/flowsheet_two_stage.py b/watertap/examples/flowsheets/full_treatment_train/analysis/flowsheet_two_stage.py index 872f9c67a1..4ba0080017 100644 --- a/watertap/examples/flowsheets/full_treatment_train/analysis/flowsheet_two_stage.py +++ b/watertap/examples/flowsheets/full_treatment_train/analysis/flowsheet_two_stage.py @@ -29,7 +29,7 @@ import watertap.examples.flowsheets.full_treatment_train.analysis.flowsheet_single_stage as single_stage from watertap.examples.flowsheets.full_treatment_train.analysis.flowsheet_single_stage import ( build, - build_components, + build_components as build_components_single_stage, scale, initialize, report, @@ -46,6 +46,8 @@ "RO_level": "detailed", } +build_components = build_components_single_stage + def set_optimization_components(m, system_recovery, **kwargs): single_stage.set_optimization_components(m, system_recovery, **kwargs) diff --git a/watertap/examples/flowsheets/full_treatment_train/chemical_flowsheet_util.py b/watertap/examples/flowsheets/full_treatment_train/chemical_flowsheet_util.py index db47b8e2b9..e394c30bc8 100644 --- a/watertap/examples/flowsheets/full_treatment_train/chemical_flowsheet_util.py +++ b/watertap/examples/flowsheets/full_treatment_train/chemical_flowsheet_util.py @@ -18,7 +18,6 @@ """ from pyomo.network import SequentialDecomposition -from idaes.core.util import scaling as iscale from idaes.core.solvers import get_solver from pyomo.environ import value diff --git a/watertap/examples/flowsheets/full_treatment_train/electrolyte_scaling_utils.py b/watertap/examples/flowsheets/full_treatment_train/electrolyte_scaling_utils.py index 9af2a141f4..b7f2f3d39b 100644 --- a/watertap/examples/flowsheets/full_treatment_train/electrolyte_scaling_utils.py +++ b/watertap/examples/flowsheets/full_treatment_train/electrolyte_scaling_utils.py @@ -20,7 +20,7 @@ from idaes.core.util.initialization import fix_state_vars, revert_state_vars # Import specific pyomo objects -from pyomo.environ import value, Suffix +from pyomo.environ import value __author__ = "Austin Ladshaw" diff --git a/watertap/examples/flowsheets/full_treatment_train/flowsheet_components/examples/full_example.py b/watertap/examples/flowsheets/full_treatment_train/flowsheet_components/examples/full_example.py index 5435406396..e2b842096d 100644 --- a/watertap/examples/flowsheets/full_treatment_train/flowsheet_components/examples/full_example.py +++ b/watertap/examples/flowsheets/full_treatment_train/flowsheet_components/examples/full_example.py @@ -18,11 +18,9 @@ TransformationFactory, value, Param, - Block, ) from pyomo.environ import units as pyunits from pyomo.network import Arc -from pyomo.util import infeasible from idaes.core import FlowsheetBlock from idaes.core.util.scaling import calculate_scaling_factors from idaes.core.util.initialization import propagate_state @@ -30,7 +28,6 @@ pretreatment_NF, desalination, translator_block, - feed_block, gypsum_saturation_index, costing, ) @@ -40,8 +37,6 @@ from watertap.examples.flowsheets.full_treatment_train.util import ( solve_block, check_dof, - check_build, - check_scaling, ) """Flowsheet example that satisfy minimum viable product requirements""" diff --git a/watertap/examples/flowsheets/full_treatment_train/model_components/property_models.py b/watertap/examples/flowsheets/full_treatment_train/model_components/property_models.py index 6727944187..1e6689756d 100644 --- a/watertap/examples/flowsheets/full_treatment_train/model_components/property_models.py +++ b/watertap/examples/flowsheets/full_treatment_train/model_components/property_models.py @@ -19,9 +19,9 @@ ) from idaes.core.util.scaling import calculate_scaling_factors from watertap.property_models import seawater_prop_pack +from watertap.property_models import seawater_ion_prop_pack from watertap.examples.flowsheets.full_treatment_train.model_components import ( seawater_salt_prop_pack, - seawater_ion_prop_pack, ) from watertap.examples.flowsheets.full_treatment_train.model_components.eNRTL import ( entrl_config_FpcTP, diff --git a/watertap/examples/flowsheets/full_treatment_train/model_components/tests/test_prop_pack.py b/watertap/examples/flowsheets/full_treatment_train/model_components/tests/test_prop_pack.py index 458d5d112f..8227b58c88 100644 --- a/watertap/examples/flowsheets/full_treatment_train/model_components/tests/test_prop_pack.py +++ b/watertap/examples/flowsheets/full_treatment_train/model_components/tests/test_prop_pack.py @@ -11,28 +11,25 @@ ################################################################################# import pytest -from watertap.property_models.tests.property_test_harness import ( - PropertyTestHarness, - PropertyRegressionTest, -) +from watertap.property_models.tests.property_test_harness import PropertyTestHarness from pyomo.environ import ConcreteModel, value from idaes.core import FlowsheetBlock import idaes.core.util.scaling as iscale from pyomo.util.check_units import assert_units_consistent +from watertap.property_models import seawater_ion_prop_pack from watertap.examples.flowsheets.full_treatment_train.util import ( solve_block, check_dof, ) -import watertap.examples.flowsheets.full_treatment_train.model_components.seawater_ion_prop_pack as property_seawater_ions import watertap.examples.flowsheets.full_treatment_train.model_components.seawater_salt_prop_pack as property_seawater_salts -# ----------------------------------------------------------------------------- + @pytest.mark.component def test_property_seawater_ions(): m = ConcreteModel() m.fs = FlowsheetBlock(dynamic=False) - m.fs.properties = property_seawater_ions.PropParameterBlock() + m.fs.properties = seawater_ion_prop_pack.PropParameterBlock() m.fs.stream = m.fs.properties.build_state_block([0]) # specify @@ -100,7 +97,7 @@ def test_property_seawater_ions(): class TestPropertySeawaterIons(PropertyTestHarness): def configure(self): - self.prop_pack = property_seawater_ions.PropParameterBlock + self.prop_pack = seawater_ion_prop_pack.PropParameterBlock self.param_args = {} self.scaling_args = { ("flow_mass_phase_comp", ("Liq", "H2O")): 1, diff --git a/watertap/examples/flowsheets/full_treatment_train/util.py b/watertap/examples/flowsheets/full_treatment_train/util.py index bc2dd984b8..b3e8e0ec46 100644 --- a/watertap/examples/flowsheets/full_treatment_train/util.py +++ b/watertap/examples/flowsheets/full_treatment_train/util.py @@ -10,13 +10,11 @@ # "https://github.com/watertap-org/watertap/" ################################################################################# -from pyomo.environ import check_optimal_termination, TransformationFactory +from pyomo.environ import TransformationFactory from pyomo.util.check_units import assert_units_consistent from idaes.core.solvers import get_solver -from idaes.core.util.model_statistics import degrees_of_freedom from idaes.core.util.scaling import ( unscaled_variables_generator, - unscaled_constraints_generator, calculate_scaling_factors, ) from watertap.core.util.initialization import ( diff --git a/watertap/examples/flowsheets/ion_exchange/tests/test_ion_exchange_demo.py b/watertap/examples/flowsheets/ion_exchange/tests/test_ion_exchange_demo.py index 72778ce4d2..18aff09ff2 100644 --- a/watertap/examples/flowsheets/ion_exchange/tests/test_ion_exchange_demo.py +++ b/watertap/examples/flowsheets/ion_exchange/tests/test_ion_exchange_demo.py @@ -25,7 +25,6 @@ from idaes.core.solvers import get_solver from idaes.core.util.model_statistics import degrees_of_freedom from idaes.models.unit_models import Feed, Product -import idaes.logger as idaeslogger from watertap.unit_models.ion_exchange_0D import ( IonExchange0D, IonExchangeType, diff --git a/watertap/examples/flowsheets/lsrro/lsrro.py b/watertap/examples/flowsheets/lsrro/lsrro.py index 524a1cc005..eca63565e8 100644 --- a/watertap/examples/flowsheets/lsrro/lsrro.py +++ b/watertap/examples/flowsheets/lsrro/lsrro.py @@ -81,6 +81,7 @@ def run_lsrro_case( number_of_stages, water_recovery=None, Cin=None, + Qin=None, Cbrine=None, A_case=ACase.fixed, B_case=BCase.optimize, @@ -93,6 +94,7 @@ def run_lsrro_case( AB_gamma_factor=None, B_max=None, number_of_RO_finite_elements=10, + set_default_bounds_on_module_dimensions=True, ): m = build( number_of_stages, @@ -102,7 +104,7 @@ def run_lsrro_case( number_of_RO_finite_elements, B_max, ) - set_operating_conditions(m, Cin) + set_operating_conditions(m, Cin, Qin) initialize(m) solve(m) @@ -113,6 +115,7 @@ def run_lsrro_case( optimize_set_up( m, + set_default_bounds_on_module_dimensions, water_recovery, Cbrine, A_case, @@ -577,8 +580,10 @@ def cost_high_pressure_pump_lsrro(blk, cost_electricity_flow=True): ) -def set_operating_conditions(m, Cin=None): +def set_operating_conditions(m, Cin=None, Qin=None): # ---specifications--- + if Qin is None: + Qin = 1e-3 # parameters pump_efi = 0.75 # pump efficiency [-] erd_efi = 0.8 # energy recovery device efficiency [-] @@ -586,14 +591,15 @@ def set_operating_conditions(m, Cin=None): mem_B = 3.5e-8 # membrane salt permeability coefficient [m/s] height = 1e-3 # channel height in membrane stage [m] spacer_porosity = 0.85 # spacer porosity in membrane stage [-] - width = 5 # effective membrane width [m] - area = 100 # membrane area [m^2] + width = 5 * Qin / 1e-3 # effective membrane width [m] + area = 100 * Qin / 1e-3 # membrane area [m^2] pressure_atm = 101325 # atmospheric pressure [Pa] # feed # feed_flow_mass = 1*pyunits.kg/pyunits.s if Cin is None: Cin = 70 + feed_temperature = 273.15 + 20 # initialize feed @@ -605,7 +611,7 @@ def set_operating_conditions(m, Cin=None): ("conc_mass_phase_comp", ("Liq", "NaCl")): value( m.fs.feed.properties[0].conc_mass_phase_comp["Liq", "NaCl"] ), # feed mass concentration - ("flow_vol_phase", "Liq"): 1e-3, + ("flow_vol_phase", "Liq"): Qin, }, # volumetric feed flowrate [-] hold_state=True, # fixes the calculated component mass flow rates ) @@ -831,6 +837,7 @@ def solve(model, solver=None, tee=False, raise_on_failure=False): def optimize_set_up( m, + set_default_bounds_on_module_dimensions, water_recovery=None, Cbrine=None, A_case=ACase.fixed, @@ -942,6 +949,8 @@ def optimize_set_up( or AB_tradeoff == ABTradeoff.inequality_constraint ): m.fs.AB_tradeoff_coeff = Param(initialize=0.01333, mutable=True) + if AB_gamma_factor is None: + AB_gamma_factor = 1 m.fs.AB_tradeoff_coeff.set_value( AB_gamma_factor * value(m.fs.AB_tradeoff_coeff) ) @@ -950,10 +959,18 @@ def optimize_set_up( for idx, stage in m.fs.ROUnits.items(): stage.area.unfix() stage.width.unfix() - stage.area.setlb(1) - stage.area.setub(20000) - stage.width.setlb(0.1) - stage.width.setub(1000) + if set_default_bounds_on_module_dimensions: + # bounds originally set for Cost Optimization of LSRRO paper + stage.area.setlb(1) + stage.area.setub(20000) + stage.width.setlb(0.1) + stage.width.setub(1000) + else: + stage.area.setlb(1) + stage.area.setub(None) + stage.width.setlb(1) + stage.width.setub(None) + stage.length.setlb(1) if ( stage.config.mass_transfer_coefficient == MassTransferCoefficient.calculated @@ -1209,3 +1226,25 @@ def display_system(m): def display_RO_reports(m): for stage in m.fs.ROUnits.values(): stage.report() + + +if __name__ == "__main__": + m, results = run_lsrro_case( + number_of_stages=3, + water_recovery=0.50, + Cin=70, # inlet NaCl conc kg/m3, + Qin=1e-3, # inlet feed flowrate m3/s + Cbrine=None, # brine conc kg/m3 + A_case=ACase.optimize, + B_case=BCase.optimize, + AB_tradeoff=ABTradeoff.equality_constraint, + # A_value=4.2e-12, #membrane water permeability coeff m/s-Pa + has_NaCl_solubility_limit=True, + has_calculated_concentration_polarization=True, + has_calculated_ro_pressure_drop=True, + permeate_quality_limit=500e-6, + AB_gamma_factor=1, + B_max=3.5e-6, + number_of_RO_finite_elements=10, + set_default_bounds_on_module_dimensions=True, + ) diff --git a/watertap/examples/flowsheets/lsrro/multi_sweep.py b/watertap/examples/flowsheets/lsrro/multi_sweep.py index e2fd0825de..b035c263e5 100644 --- a/watertap/examples/flowsheets/lsrro/multi_sweep.py +++ b/watertap/examples/flowsheets/lsrro/multi_sweep.py @@ -35,7 +35,10 @@ def _lsrro_presweep( m.fs.feed.properties[0].conc_mass_phase_comp["Liq", "NaCl"].fix() m.fs.feed.properties[0].flow_vol_phase["Liq"].fix() lsrro.optimize_set_up( - m, A_value=A_value, permeate_quality_limit=permeate_quality_limit + m, + set_default_bounds_on_module_dimensions=True, + A_value=A_value, + permeate_quality_limit=permeate_quality_limit, ) return m diff --git a/watertap/examples/flowsheets/lsrro/tests/test_lssro_paper_analysis.py b/watertap/examples/flowsheets/lsrro/tests/test_lssro_paper_analysis.py index 7b9afc770c..c0f3ae624b 100644 --- a/watertap/examples/flowsheets/lsrro/tests/test_lssro_paper_analysis.py +++ b/watertap/examples/flowsheets/lsrro/tests/test_lssro_paper_analysis.py @@ -101,7 +101,7 @@ def test_against_paper_analysis(csv_file, row_index): for property_name, flowsheet_attribute in _results_headers.items(): assert value(model.find_component(flowsheet_attribute)) == pytest.approx( float(row[property_name]), - rel=1e-3, + rel=1e-2, ) else: for property_name in _results_headers: diff --git a/watertap/examples/flowsheets/oaro/tests/test_oaro.py b/watertap/examples/flowsheets/oaro/tests/test_oaro.py index 8dad44dd9f..1cb2a4a60f 100644 --- a/watertap/examples/flowsheets/oaro/tests/test_oaro.py +++ b/watertap/examples/flowsheets/oaro/tests/test_oaro.py @@ -18,7 +18,7 @@ from idaes.core import FlowsheetBlock from idaes.core.solvers import get_solver from idaes.core.util.model_statistics import degrees_of_freedom -from idaes.models.unit_models import Mixer, Separator, Product, Feed +from idaes.models.unit_models import Product, Feed from pyomo.util.check_units import assert_units_consistent import watertap.property_models.NaCl_prop_pack as props from watertap.unit_models.reverse_osmosis_0D import ReverseOsmosis0D diff --git a/watertap/property_models/activated_sludge/modified_asm2d_properties.py b/watertap/property_models/activated_sludge/modified_asm2d_properties.py new file mode 100644 index 0000000000..3d3bc0ea70 --- /dev/null +++ b/watertap/property_models/activated_sludge/modified_asm2d_properties.py @@ -0,0 +1,81 @@ +################################################################################# +# WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# National Renewable Energy Laboratory, and National Energy Technology +# Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +################################################################################# +""" +Thermophysical property package to be used in conjunction with modified ASM2d +reactions for compatibility with the modified ADM1 model. + +Reference: +[1] Henze, M., Gujer, W., Mino, T., Matsuo, T., Wentzel, M.C., Marais, G.v.R., +Van Loosdrecht, M.C.M., "Activated Sludge Model No.2D, ASM2D", 1999, +Wat. Sci. Tech. Vol. 39, No. 1, pp. 165-182 + +""" + +# Import IDAES cores +from idaes.core import ( + declare_process_block_class, + Solute, +) +from pyomo.common.config import ConfigValue +import idaes.logger as idaeslog +from watertap.property_models.activated_sludge.asm2d_properties import ( + ASM2dParameterData, + _ASM2dStateBlock, + ASM2dStateBlockData, +) + +from idaes.core.util.exceptions import ConfigurationError + +# Some more information about this module +__author__ = "Chenyu Wang" + + +# Set up logger +_log = idaeslog.getLogger(__name__) + + +@declare_process_block_class("ModifiedASM2dParameterBlock") +class ModifiedASM2dParameterData(ASM2dParameterData): + CONFIG = ASM2dParameterData.CONFIG() + + CONFIG.declare( + "additional_solute_list", + ConfigValue( + domain=list, + description="List of additional solute species names apart from conventional ASM2D", + ), + ) + + def build(self): + super().build() + self._state_block_class = ModifiedASM2dStateBlock + # Group components into different sets + if self.config.additional_solute_list is not None: + for j in self.config.additional_solute_list: + if j == "H2O": + raise ConfigurationError( + "'H2O' is reserved as the default solvent and cannot be a solute." + ) + # Add valid members of solute_list into IDAES's Solute() class. + # This triggers the addition of j into component_list and solute_set. + self.add_component(j, Solute()) + + +class _ModifiedASM2dStateBlock(_ASM2dStateBlock): + pass + + +@declare_process_block_class( + "ModifiedASM2dStateBlock", block_class=_ModifiedASM2dStateBlock +) +class ModifiedASM2dStateBlockData(ASM2dStateBlockData): + pass diff --git a/watertap/property_models/activated_sludge/modified_asm2d_reactions.py b/watertap/property_models/activated_sludge/modified_asm2d_reactions.py new file mode 100644 index 0000000000..fdced47802 --- /dev/null +++ b/watertap/property_models/activated_sludge/modified_asm2d_reactions.py @@ -0,0 +1,90 @@ +################################################################################# +# WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# National Renewable Energy Laboratory, and National Energy Technology +# Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +################################################################################# +""" +Modified ASM2d reaction package for compatibility with modified ADM1. + + +Reference: + +[1] Henze, M., Gujer, W., Mino, T., Matsuo, T., Wentzel, M.C., Marais, G.v.R., +Van Loosdrecht, M.C.M., "Activated Sludge Model No.2D, ASM2D", 1999, +Wat. Sci. Tech. Vol. 39, No. 1, pp. 165-182 +[2] Flores-Alsina, X., Solon, K., Mbamba, C.K., Tait, S., Gernaey, K.V., Jeppsson, U. and Batstone, D.J., +"Modelling phosphorus (P), sulfur (S) and iron (Fe) interactions for dynamic simulations of +anaerobic digestion processes", 2016, Water Research, 95, pp.370-382. +""" + +# Import IDAES cores +from idaes.core import declare_process_block_class +import idaes.logger as idaeslog +from watertap.property_models.activated_sludge.asm2d_reactions import ( + ASM2dReactionParameterData, + _ASM2dReactionBlock, + ASM2dReactionBlockData, +) + +# Some more information about this module +__author__ = "Chenyu Wang" + +# Set up logger +_log = idaeslog.getLogger(__name__) + + +@declare_process_block_class("ModifiedASM2dReactionParameterBlock") +class ModifiedASM2dReactionParameterData(ASM2dReactionParameterData): + def build(self): + super().build() + + self._reaction_block_class = ModifiedASM2dReactionBlock + + if self.config.property_package.config.additional_solute_list is not None: + for j in self.config.property_package.config.additional_solute_list: + + rate_reaction_stoichiometry_additional = { + ("R1", "Liq", j): 0, + ("R2", "Liq", j): 0, + ("R3", "Liq", j): 0, + ("R4", "Liq", j): 0, + ("R5", "Liq", j): 0, + ("R6", "Liq", j): 0, + ("R7", "Liq", j): 0, + ("R8", "Liq", j): 0, + ("R9", "Liq", j): 0, + ("R10", "Liq", j): 0, + ("R11", "Liq", j): 0, + ("R12", "Liq", j): 0, + ("R13", "Liq", j): 0, + ("R14", "Liq", j): 0, + ("R15", "Liq", j): 0, + ("R16", "Liq", j): 0, + ("R17", "Liq", j): 0, + ("R18", "Liq", j): 0, + ("R19", "Liq", j): 0, + ("R20", "Liq", j): 0, + ("R21", "Liq", j): 0, + } + new_rate_reaction_stoichiometry = { + **self.rate_reaction_stoichiometry, + **rate_reaction_stoichiometry_additional, + } + self.rate_reaction_stoichiometry = new_rate_reaction_stoichiometry + + +class _ModifiedASM2dReactionBlock(_ASM2dReactionBlock): + pass + + +@declare_process_block_class( + "ModifiedASM2dReactionBlock", block_class=_ModifiedASM2dReactionBlock +) +class ModifiedASM2dReactionBlockData(ASM2dReactionBlockData): + pass diff --git a/watertap/property_models/activated_sludge/tests/test_asm1_reaction.py b/watertap/property_models/activated_sludge/tests/test_asm1_reaction.py index 5b1a0f064e..4b3be99cd1 100644 --- a/watertap/property_models/activated_sludge/tests/test_asm1_reaction.py +++ b/watertap/property_models/activated_sludge/tests/test_asm1_reaction.py @@ -155,7 +155,7 @@ def test_get_reaction_rate_basis(self, model): def test_initialize(self, model): assert model.rxns.initialize() is None - @pytest.mark.component + @pytest.mark.unit def check_units(self, model): assert_units_consistent(model) @@ -194,11 +194,11 @@ def model(self): return m - @pytest.mark.component + @pytest.mark.unit def test_dof(self, model): assert degrees_of_freedom(model) == 0 - @pytest.mark.component + @pytest.mark.unit def test_unit_consistency(self, model): assert_units_consistent(model) == 0 diff --git a/watertap/property_models/activated_sludge/tests/test_asm1_thermo.py b/watertap/property_models/activated_sludge/tests/test_asm1_thermo.py index 67c38cba06..6e5be273fb 100644 --- a/watertap/property_models/activated_sludge/tests/test_asm1_thermo.py +++ b/watertap/property_models/activated_sludge/tests/test_asm1_thermo.py @@ -265,7 +265,7 @@ def test_define_display_vars(self, model): "Pressure", ] - @pytest.mark.unit + @pytest.mark.component def test_initialize(self, model): orig_fixed_vars = fixed_variables_set(model) orig_act_consts = activated_constraints_set(model) @@ -283,6 +283,6 @@ def test_initialize(self, model): for v in fin_fixed_vars: assert v in orig_fixed_vars - @pytest.mark.component + @pytest.mark.unit def check_units(self, model): assert_units_consistent(model) diff --git a/watertap/property_models/activated_sludge/tests/test_asm2d_reaction.py b/watertap/property_models/activated_sludge/tests/test_asm2d_reaction.py index 675a343907..79ff683a0e 100644 --- a/watertap/property_models/activated_sludge/tests/test_asm2d_reaction.py +++ b/watertap/property_models/activated_sludge/tests/test_asm2d_reaction.py @@ -328,7 +328,7 @@ def test_get_reaction_rate_basis(self, model): def test_initialize(self, model): assert model.rxns.initialize() is None - @pytest.mark.component + @pytest.mark.unit def check_units(self, model): assert_units_consistent(model) @@ -384,11 +384,11 @@ def model(self): return m - @pytest.mark.component + @pytest.mark.unit def test_dof(self, model): assert degrees_of_freedom(model) == 0 - @pytest.mark.component + @pytest.mark.unit def test_unit_consistency(self, model): assert_units_consistent(model) == 0 @@ -521,11 +521,11 @@ def model(self): return m - @pytest.mark.component + @pytest.mark.unit def test_dof(self, model): assert degrees_of_freedom(model) == 0 - @pytest.mark.component + @pytest.mark.unit def test_unit_consistency(self, model): assert_units_consistent(model) == 0 @@ -670,11 +670,11 @@ def model(self): return m - @pytest.mark.component + @pytest.mark.unit def test_dof(self, model): assert degrees_of_freedom(model) == 0 - @pytest.mark.component + @pytest.mark.unit def test_unit_consistency(self, model): assert_units_consistent(model) == 0 @@ -817,11 +817,11 @@ def model(self): return m - @pytest.mark.component + @pytest.mark.unit def test_dof(self, model): assert degrees_of_freedom(model) == 0 - @pytest.mark.component + @pytest.mark.unit def test_unit_consistency(self, model): assert_units_consistent(model) == 0 diff --git a/watertap/property_models/activated_sludge/tests/test_asm2d_thermo.py b/watertap/property_models/activated_sludge/tests/test_asm2d_thermo.py index 6ed6efd199..54e1e34e5a 100644 --- a/watertap/property_models/activated_sludge/tests/test_asm2d_thermo.py +++ b/watertap/property_models/activated_sludge/tests/test_asm2d_thermo.py @@ -255,7 +255,7 @@ def test_define_display_vars(self, model): "Pressure", ] - @pytest.mark.unit + @pytest.mark.component def test_initialize(self, model): orig_fixed_vars = fixed_variables_set(model) orig_act_consts = activated_constraints_set(model) @@ -273,6 +273,6 @@ def test_initialize(self, model): for v in fin_fixed_vars: assert v in orig_fixed_vars - @pytest.mark.component + @pytest.mark.unit def check_units(self, model): assert_units_consistent(model) diff --git a/watertap/property_models/activated_sludge/tests/test_modified_asm2d_reaction.py b/watertap/property_models/activated_sludge/tests/test_modified_asm2d_reaction.py new file mode 100644 index 0000000000..f77e88f048 --- /dev/null +++ b/watertap/property_models/activated_sludge/tests/test_modified_asm2d_reaction.py @@ -0,0 +1,956 @@ +################################################################################# +# WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# National Renewable Energy Laboratory, and National Energy Technology +# Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +################################################################################# +""" +Tests for modified ASM2d reaction package. +Authors: Andrew Lee, Alejandro Garciadiego, Chenyu Wang + +References: + +[1] Henze, M., Gujer, W., Mino, T., Matsuo, T., Wentzel, M.C., Marais, G.v.R., +Van Loosdrecht, M.C.M., "Activated Sludge Model No.2D, ASM2D", 1999, +Wat. Sci. Tech. Vol. 39, No. 1, pp. 165-182 + +[2] Flores-Alsina X., Gernaey K.V. and Jeppsson, U. "Benchmarking biological +nutrient removal in wastewater treatment plants: influence of mathematical model +assumptions", 2012, Wat. Sci. Tech., Vol. 65 No. 8, pp. 1496-1505 +""" +import pytest + +from pyomo.environ import ( + check_optimal_termination, + ConcreteModel, + Constraint, + units, + value, + Var, +) +from pyomo.util.check_units import assert_units_consistent + +from idaes.core import FlowsheetBlock +from idaes.models.unit_models import CSTR +from idaes.core import MaterialFlowBasis +from idaes.core.solvers import get_solver +from idaes.core.util.model_statistics import degrees_of_freedom + +from watertap.property_models.activated_sludge.modified_asm2d_properties import ( + ModifiedASM2dParameterBlock, +) +from watertap.property_models.activated_sludge.modified_asm2d_reactions import ( + ModifiedASM2dReactionParameterBlock, + ModifiedASM2dReactionBlock, +) +import idaes.core.util.scaling as iscale + +# ----------------------------------------------------------------------------- +# Get default solver for testing +solver = get_solver() + + +class TestParamBlock(object): + @pytest.fixture(scope="class") + def model(self): + model = ConcreteModel() + model.pparams = ModifiedASM2dParameterBlock( + additional_solute_list=["S_K", "S_Mg"] + ) + model.rparams = ModifiedASM2dReactionParameterBlock( + property_package=model.pparams + ) + + return model + + @pytest.mark.unit + def test_build(self, model): + assert model.rparams.reaction_block_class is ModifiedASM2dReactionBlock + + assert len(model.rparams.rate_reaction_idx) == 21 + for i in model.rparams.rate_reaction_idx: + assert i in [ + "R1", + "R2", + "R3", + "R4", + "R5", + "R6", + "R7", + "R8", + "R9", + "R10", + "R11", + "R12", + "R13", + "R14", + "R15", + "R16", + "R17", + "R18", + "R19", + "R20", + "R21", + ] + + # Expected non-zero stoichiometries + # Values from table 11 in reference + stoic = { + ("R1", "Liq", "S_F"): 1, + ("R1", "Liq", "S_NH4"): 0.01, + ("R1", "Liq", "S_ALK"): 0.01 * 61 / 14, # ~0.001*61 + ("R1", "Liq", "X_S"): -1, + ("R1", "Liq", "X_TSS"): -0.75, + ("R2", "Liq", "S_F"): 1, + ("R2", "Liq", "S_NH4"): 0.01, + ("R2", "Liq", "S_ALK"): 0.01 * 61 / 14, # ~0.001*61 + ("R2", "Liq", "X_S"): -1, + ("R2", "Liq", "X_TSS"): -0.75, + ("R3", "Liq", "S_F"): 1, + ("R3", "Liq", "S_NH4"): 0.01, + ("R3", "Liq", "S_ALK"): 0.01 * 61 / 14, # ~0.001*61 + ("R3", "Liq", "X_S"): -1, + ("R3", "Liq", "X_TSS"): -0.75, + ("R4", "Liq", "S_O2"): -0.6, + ("R4", "Liq", "S_F"): -1.6, + ("R4", "Liq", "S_NH4"): -0.022, + ("R4", "Liq", "S_PO4"): -0.004, + ("R4", "Liq", "S_ALK"): -0.022 * 61 / 14 + + 1.5 * 0.004 * 61 / 31, # ~ -0.001*61 + ("R4", "Liq", "X_H"): 1, + ("R4", "Liq", "X_TSS"): 0.9, + ("R5", "Liq", "S_O2"): -0.6, + ("R5", "Liq", "S_A"): -1.6, + ("R5", "Liq", "S_NH4"): -0.07, + ("R5", "Liq", "S_PO4"): -0.02, + ("R5", "Liq", "S_ALK"): -0.07 * 61 / 14 + + 1.5 * 0.02 * 61 / 31 + + 1.6 * 61 / 64, # ~0.021*61 + ("R5", "Liq", "X_H"): 1, + ("R5", "Liq", "X_TSS"): 0.9, + ("R6", "Liq", "S_F"): -1.6, + ("R6", "Liq", "S_NH4"): -0.022, + ("R6", "Liq", "S_N2"): 0.21, + ("R6", "Liq", "S_NO3"): -0.21, + ("R6", "Liq", "S_PO4"): -0.004, + ("R6", "Liq", "S_ALK"): -0.022 * 61 / 14 + + 0.21 * 61 / 14 + + 0.004 * 1.5 * 61 / 31, # ~0.014*61 + ("R6", "Liq", "X_H"): 1, + ("R6", "Liq", "X_TSS"): 0.9, + ("R7", "Liq", "S_A"): -1.6, + ("R7", "Liq", "S_NH4"): -0.07, + ("R7", "Liq", "S_N2"): 0.21, + ("R7", "Liq", "S_NO3"): -0.21, + ("R7", "Liq", "S_PO4"): -0.02, + ("R7", "Liq", "S_ALK"): 1.6 * 61 / 64 + + (-0.07 + 0.21) * 61 / 14 + + 0.02 * 1.5 * 61 / 31, # ~0.036*61 + ("R7", "Liq", "X_H"): 1, + ("R7", "Liq", "X_TSS"): 0.9, + ("R8", "Liq", "S_F"): -1, + ("R8", "Liq", "S_A"): 1, + ("R8", "Liq", "S_NH4"): 0.03, + ("R8", "Liq", "S_PO4"): 0.01, + ("R8", "Liq", "S_ALK"): -1 / 64 + + 0.03 * 61 / 14 + - 0.01 * 1.5 * 61 / 31, # ~-0.0014*61 + ("R9", "Liq", "S_NH4"): 0.032, + ("R9", "Liq", "S_PO4"): 0.01, + ("R9", "Liq", "S_ALK"): 0.032 * 61 / 14 - 0.01 * 1.5 * 61 / 31, # ~0.002*61 + ("R9", "Liq", "X_I"): 0.1, + ("R9", "Liq", "X_S"): 0.9, + ("R9", "Liq", "X_H"): -1, + ("R9", "Liq", "X_TSS"): -0.15, + ("R10", "Liq", "S_A"): -1, + ("R10", "Liq", "S_PO4"): 0.4, + ("R10", "Liq", "S_ALK"): 61 / 64 - 0.4 * 0.5 * 61 / 31, # ~0.009*61 + ("R10", "Liq", "X_PP"): -0.4, + ("R10", "Liq", "X_PHA"): 1, + ("R10", "Liq", "X_TSS"): -0.69, + ("R11", "Liq", "S_O2"): -0.2, + ("R11", "Liq", "S_PO4"): -1, + ("R11", "Liq", "S_ALK"): 0.016 * 61, + ("R11", "Liq", "X_PP"): 1, + ("R11", "Liq", "X_PHA"): -0.2, + ("R11", "Liq", "X_TSS"): 3.11, + ("R12", "Liq", "S_N2"): 0.07, + ("R12", "Liq", "S_NO3"): -0.07, + ("R12", "Liq", "S_PO4"): -1, + ("R12", "Liq", "S_ALK"): 0.021 * 61, + ("R12", "Liq", "X_PP"): 1, + ("R12", "Liq", "X_PHA"): -0.2, + ("R12", "Liq", "X_TSS"): 3.11, + ("R13", "Liq", "S_O2"): -0.6, + ("R13", "Liq", "S_NH4"): -0.07, + ("R13", "Liq", "S_PO4"): -0.02, + ("R13", "Liq", "S_ALK"): -0.07 * 61 / 14 + + 0.02 * 1.5 * 61 / 31, # ~-0.004*61 + ("R13", "Liq", "X_PAO"): 1, + ("R13", "Liq", "X_PHA"): -1.6, + ("R13", "Liq", "X_TSS"): -0.06, + ("R14", "Liq", "S_NH4"): -0.07, + ("R14", "Liq", "S_N2"): 0.21, + ("R14", "Liq", "S_NO3"): -0.21, + ("R14", "Liq", "S_PO4"): -0.02, + ("R14", "Liq", "S_ALK"): (-0.07 + 0.21) * 61 / 14 + + 0.02 * 61 / 31, # ~0.011*61 + ("R14", "Liq", "X_PAO"): 1, + ("R14", "Liq", "X_PHA"): -1.6, + ("R14", "Liq", "X_TSS"): -0.06, + ("R15", "Liq", "S_NH4"): 0.032, + ("R15", "Liq", "S_PO4"): 0.01, + ("R15", "Liq", "S_ALK"): 0.032 * 61 / 14 + - 0.01 * 1.5 * 61 / 31, # ~0.002*61 + ("R15", "Liq", "X_I"): 0.1, + ("R15", "Liq", "X_S"): 0.9, + ("R15", "Liq", "X_PAO"): -1, + ("R15", "Liq", "X_TSS"): -0.15, + ("R16", "Liq", "S_PO4"): 1, + ("R16", "Liq", "S_ALK"): -0.016 * 61, + ("R16", "Liq", "X_PP"): -1, + ("R16", "Liq", "X_TSS"): -3.23, + ("R17", "Liq", "S_A"): 1, + ("R17", "Liq", "S_ALK"): 61 * (1 / 64 - 1 / 31), # ~-0.016*61 + ("R17", "Liq", "X_PHA"): -1, + ("R17", "Liq", "X_TSS"): -0.6, + ("R18", "Liq", "S_O2"): -18, + ("R18", "Liq", "S_NH4"): -4.24, + ("R18", "Liq", "S_NO3"): 4.17, + ("R18", "Liq", "S_PO4"): -0.02, + ("R18", "Liq", "S_ALK"): -0.6 * 61, + ("R18", "Liq", "X_AUT"): 1, + ("R18", "Liq", "X_TSS"): 0.9, + ("R19", "Liq", "S_NH4"): 0.032, + ("R19", "Liq", "S_PO4"): 0.01, + ("R19", "Liq", "S_ALK"): 0.032 * 61 / 14 - 0.01 * 61 / 31, # ~0.002*61 + ("R19", "Liq", "X_I"): 0.1, + ("R19", "Liq", "X_S"): 0.9, + ("R19", "Liq", "X_AUT"): -1, + ("R19", "Liq", "X_TSS"): -0.15, + ("R20", "Liq", "S_PO4"): -1, + ("R20", "Liq", "S_ALK"): 0.048 * 61, + ("R20", "Liq", "X_TSS"): 1.42, + ("R20", "Liq", "X_MeOH"): -3.45, + ("R20", "Liq", "X_MeP"): 4.87, + ("R21", "Liq", "S_PO4"): 1, + ("R21", "Liq", "S_ALK"): -0.048 * 61, + ("R21", "Liq", "X_TSS"): -1.42, + ("R21", "Liq", "X_MeOH"): 3.45, + ("R21", "Liq", "X_MeP"): -4.87, + } + + assert len(model.rparams.rate_reaction_stoichiometry) == 22 * 21 + for i, v in model.rparams.rate_reaction_stoichiometry.items(): + assert i[0] in [ + "R1", + "R2", + "R3", + "R4", + "R5", + "R6", + "R7", + "R8", + "R9", + "R10", + "R11", + "R12", + "R13", + "R14", + "R15", + "R16", + "R17", + "R18", + "R19", + "R20", + "R21", + ] + assert i[1] == "Liq" + assert i[2] in [ + "H2O", + "S_A", + "S_F", + "S_I", + "S_N2", + "S_NH4", + "S_NO3", + "S_O2", + "S_PO4", + "S_ALK", + "X_AUT", + "X_H", + "X_I", + "X_MeOH", + "X_MeP", + "X_PAO", + "X_PHA", + "X_PP", + "X_S", + "X_TSS", + "S_K", + "S_Mg", + ] + + if i in stoic: + assert pytest.approx(stoic[i], rel=1e-2) == value(v) + else: + assert value(v) == 0 + + +class TestReactionBlock(object): + @pytest.fixture(scope="class") + def model(self): + model = ConcreteModel() + model.pparams = ModifiedASM2dParameterBlock( + additional_solute_list=["S_K", "S_Mg"] + ) + model.rparams = ModifiedASM2dReactionParameterBlock( + property_package=model.pparams + ) + + model.props = model.pparams.build_state_block([1]) + + model.rxns = model.rparams.build_reaction_block([1], state_block=model.props) + + return model + + @pytest.mark.unit + def test_build(self, model): + assert model.rxns[1].conc_mass_comp_ref is model.props[1].conc_mass_comp + + @pytest.mark.unit + def test_rxn_rate(self, model): + assert isinstance(model.rxns[1].reaction_rate, Var) + assert len(model.rxns[1].reaction_rate) == 21 + assert isinstance(model.rxns[1].rate_expression, Constraint) + assert len(model.rxns[1].rate_expression) == 21 + + @pytest.mark.unit + def test_get_reaction_rate_basis(self, model): + assert model.rxns[1].get_reaction_rate_basis() == MaterialFlowBasis.mass + + @pytest.mark.component + def test_initialize(self, model): + assert model.rxns.initialize() is None + + @pytest.mark.unit + def check_units(self, model): + assert_units_consistent(model) + + +class TestAerobic: + @pytest.fixture(scope="class") + def model(self): + m = ConcreteModel() + + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.props = ModifiedASM2dParameterBlock(additional_solute_list=["S_K", "S_Mg"]) + m.fs.rxn_props = ModifiedASM2dReactionParameterBlock( + property_package=m.fs.props + ) + + m.fs.R1 = CSTR(property_package=m.fs.props, reaction_package=m.fs.rxn_props) + + iscale.calculate_scaling_factors(m.fs) + + # NOTE: Concentrations of exactly 0 result in singularities, use EPS instead + EPS = 1e-8 + + # Feed conditions based on manual mass balance of inlet and recycle streams + m.fs.R1.inlet.flow_vol.fix(18446 * units.m**3 / units.day) + m.fs.R1.inlet.temperature.fix(298.15 * units.K) + m.fs.R1.inlet.pressure.fix(1 * units.atm) + # For aerobic operation, the final spec on O2 will be on the outlet concentration + # This is to account for O2 addition under aerobic operation + # For now, pick a reasonable positive value for initialization + m.fs.R1.inlet.conc_mass_comp[0, "S_O2"].fix(10 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "S_N2"].fix(EPS * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "S_NH4"].fix(16 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "S_NO3"].fix(EPS * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "S_PO4"].fix(3.6 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "S_F"].fix(30 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "S_A"].fix(20 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "S_I"].fix(30 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "X_I"].fix(25 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "X_S"].fix(125 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "X_H"].fix(30 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "X_PAO"].fix(EPS * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "X_PP"].fix(EPS * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "X_PHA"].fix(EPS * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "X_AUT"].fix(EPS * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "X_MeOH"].fix(EPS * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "X_MeP"].fix(EPS * units.mg / units.liter) + # No data on TSS, K and Mg from EXPOsan at this point + m.fs.R1.inlet.conc_mass_comp[0, "X_TSS"].fix(EPS * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "S_K"].fix(EPS * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "S_Mg"].fix(EPS * units.mg / units.liter) + + # Alkalinity was givien in mg/L based on C + m.fs.R1.inlet.alkalinity[0].fix(61 / 12 * units.mmol / units.liter) + + m.fs.R1.volume.fix(1333 * units.m**3) + + return m + + @pytest.mark.unit + def test_dof(self, model): + assert degrees_of_freedom(model) == 0 + + @pytest.mark.unit + def test_unit_consistency(self, model): + assert_units_consistent(model) == 0 + + @pytest.mark.component + def test_solve(self, model): + model.fs.R1.initialize() + + # Change spec on O2 to outlet concentration to allow for O2 addition + model.fs.R1.inlet.conc_mass_comp[0, "S_O2"].unfix() + model.fs.R1.outlet.conc_mass_comp[0, "S_O2"].fix(2 * units.mg / units.liter) + + solver = get_solver() + results = solver.solve(model, tee=True) + assert check_optimal_termination(results) + + @pytest.mark.component + def test_solution(self, model): + + assert value(model.fs.R1.outlet.flow_vol[0]) == pytest.approx(0.21350, rel=1e-4) + + assert value(model.fs.R1.outlet.temperature[0]) == pytest.approx( + 298.15, rel=1e-4 + ) + assert value(model.fs.R1.outlet.pressure[0]) == pytest.approx(101325, rel=1e-4) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_A"]) == pytest.approx( + 13.440e-3, rel=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_F"]) == pytest.approx( + 23.543e-3, rel=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_I"]) == pytest.approx( + 30e-3, rel=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_N2"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_NH4"]) == pytest.approx( + 15.632e-3, rel=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_NO3"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_O2"]) == pytest.approx( + 2e-3, rel=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_PO4"]) == pytest.approx( + 3.4932e-3, rel=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_AUT"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_H"]) == pytest.approx( + 42.128e-3, rel=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_I"]) == pytest.approx( + 25.122e-3, rel=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_MeOH"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_MeP"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_PAO"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_PHA"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_PP"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_S"]) == pytest.approx( + 117.76e-3, rel=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_TSS"]) == pytest.approx( + 5.5762e-3, rel=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_Mg"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_K"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(model.fs.R1.outlet.alkalinity[0]) == pytest.approx( + 5.1754e-3, rel=1e-4 + ) + + +class TestAnoxic: + @pytest.fixture(scope="class") + def model(self): + m = ConcreteModel() + + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.props = ModifiedASM2dParameterBlock(additional_solute_list=["S_K", "S_Mg"]) + m.fs.rxn_props = ModifiedASM2dReactionParameterBlock( + property_package=m.fs.props + ) + + m.fs.R1 = CSTR(property_package=m.fs.props, reaction_package=m.fs.rxn_props) + + iscale.calculate_scaling_factors(m.fs) + + # NOTE: Concentrations of exactly 0 result in singularities, use EPS instead + EPS = 1e-8 + + # Feed conditions based on manual mass balance of inlet and recycle streams + m.fs.R1.inlet.flow_vol.fix(18446 * units.m**3 / units.day) + m.fs.R1.inlet.temperature.fix(298.15 * units.K) + m.fs.R1.inlet.pressure.fix(1 * units.atm) + m.fs.R1.inlet.conc_mass_comp[0, "S_O2"].fix(EPS * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "S_N2"].fix(EPS * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "S_NH4"].fix(16 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "S_NO3"].fix(EPS * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "S_PO4"].fix(3.6 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "S_F"].fix(30 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "S_A"].fix(20 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "S_I"].fix(30 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "X_I"].fix(25 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "X_S"].fix(125 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "X_H"].fix(30 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "X_PAO"].fix(EPS * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "X_PP"].fix(EPS * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "X_PHA"].fix(EPS * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "X_AUT"].fix(EPS * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "X_MeOH"].fix(EPS * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "X_MeP"].fix(EPS * units.mg / units.liter) + # No data on TSS from EXPOsan at this point + # However, TSS is needed for this reaction + m.fs.R1.inlet.conc_mass_comp[0, "X_TSS"].fix(100 * units.mg / units.liter) + + m.fs.R1.inlet.conc_mass_comp[0, "S_K"].fix(EPS * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "S_Mg"].fix(EPS * units.mg / units.liter) + + # Alkalinity was given in mg/L based on C + m.fs.R1.inlet.alkalinity[0].fix(61 / 12 * units.mmol / units.liter) + + m.fs.R1.volume.fix(1000 * units.m**3) + + return m + + @pytest.mark.unit + def test_dof(self, model): + assert degrees_of_freedom(model) == 0 + + @pytest.mark.unit + def test_unit_consistency(self, model): + assert_units_consistent(model) == 0 + + @pytest.mark.component + def test_solve(self, model): + model.fs.R1.initialize(optarg={"bound_push": 1e-8, "mu_init": 1e-8}) + + solver = get_solver() + solver.options = {"bound_push": 1e-8, "mu_init": 1e-8} + results = solver.solve(model, tee=True) + + assert check_optimal_termination(results) + + @pytest.mark.component + def test_solution(self, model): + # EXPOsan calculations appear to be slightly off from this implementation + # It is supected that this is due to an error in the EXPOsan stoichiometric + # coefficient for alkalinity + assert value(model.fs.R1.outlet.flow_vol[0]) == pytest.approx(0.21350, rel=1e-4) + + assert value(model.fs.R1.outlet.temperature[0]) == pytest.approx( + 298.15, rel=1e-4 + ) + assert value(model.fs.R1.outlet.pressure[0]) == pytest.approx(101325, rel=1e-4) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_A"]) == pytest.approx( + 24.093e-3, rel=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_F"]) == pytest.approx( + 27.773e-3, rel=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_I"]) == pytest.approx( + 30e-3, rel=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_N2"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_NH4"]) == pytest.approx( + 16.162e-3, rel=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_NO3"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_O2"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_PO4"]) == pytest.approx( + 3.6473e-3, rel=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_AUT"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_H"]) == pytest.approx( + 29.363e-3, rel=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_I"]) == pytest.approx( + 25.064e-3, rel=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_MeOH"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_MeP"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_PAO"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_PHA"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_PP"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_S"]) == pytest.approx( + 123.71e-3, rel=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_TSS"]) == pytest.approx( + 98.505e-3, rel=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_Mg"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_K"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(model.fs.R1.outlet.alkalinity[0]) == pytest.approx( + 5.0916e-3, rel=1e-4 + ) + + +class TestAerobic15C: + @pytest.fixture(scope="class") + def model(self): + m = ConcreteModel() + + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.props = ModifiedASM2dParameterBlock(additional_solute_list=["S_K", "S_Mg"]) + m.fs.rxn_props = ModifiedASM2dReactionParameterBlock( + property_package=m.fs.props + ) + + m.fs.rxn_props.K_H.fix(2.5 * 1 / units.day) + m.fs.rxn_props.mu_H.fix(4.5 * 1 / units.day) + m.fs.rxn_props.q_fe.fix(2.25 * 1 / units.day) + m.fs.rxn_props.b_H.fix(0.3 * 1 / units.day) + m.fs.rxn_props.q_PHA.fix(2.5 * 1 / units.day) + m.fs.rxn_props.q_PP.fix(1.25 * 1 / units.day) + m.fs.rxn_props.mu_PAO.fix(0.88 * 1 / units.day) + m.fs.rxn_props.b_PAO.fix(0.15 * 1 / units.day) + m.fs.rxn_props.b_PP.fix(0.15 * 1 / units.day) + m.fs.rxn_props.b_PHA.fix(0.15 * 1 / units.day) + m.fs.rxn_props.mu_AUT.fix(0.675 * 1 / units.day) + m.fs.rxn_props.b_AUT.fix(0.1 * 1 / units.day) + + m.fs.R1 = CSTR(property_package=m.fs.props, reaction_package=m.fs.rxn_props) + + iscale.calculate_scaling_factors(m.fs) + + # NOTE: Concentrations of exactly 0 result in singularities, use EPS instead + EPS = 1e-8 + + # Feed conditions based on manual mass balance of inlet and recycle streams + m.fs.R1.inlet.flow_vol.fix(92230 * units.m**3 / units.day) + m.fs.R1.inlet.temperature.fix(298.15 * units.K) + m.fs.R1.inlet.pressure.fix(1 * units.atm) + m.fs.R1.inlet.conc_mass_comp[0, "S_O2"].fix(7.9707 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "S_N2"].fix(29.0603 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "S_NH4"].fix(8.0209 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "S_NO3"].fix(6.6395 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "S_PO4"].fix(7.8953 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "S_F"].fix(0.4748 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "S_A"].fix(0.0336 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "S_I"].fix(30 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "X_I"].fix(1695.7695 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "X_S"].fix(68.2975 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "X_H"].fix(1855.5067 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "X_PAO"].fix(214.5319 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "X_PP"].fix(63.5316 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "X_PHA"].fix(2.7381 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "X_AUT"].fix(118.3582 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "X_MeOH"].fix(EPS * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "X_MeP"].fix(EPS * units.mg / units.liter) + + m.fs.R1.inlet.conc_mass_comp[0, "X_TSS"].fix(3525.429 * units.mg / units.liter) + + m.fs.R1.inlet.conc_mass_comp[0, "S_K"].fix(EPS * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "S_Mg"].fix(EPS * units.mg / units.liter) + + # Alkalinity was given in mg/L based on C + m.fs.R1.inlet.alkalinity[0].fix(4.6663 * units.mmol / units.liter) + + m.fs.R1.volume.fix(1000 * units.m**3) + + return m + + @pytest.mark.unit + def test_dof(self, model): + assert degrees_of_freedom(model) == 0 + + @pytest.mark.unit + def test_unit_consistency(self, model): + assert_units_consistent(model) == 0 + + @pytest.mark.component + def test_solve(self, model): + model.fs.R1.initialize(optarg={"bound_push": 1e-8, "mu_init": 1e-8}) + + solver = get_solver() + solver.options = {"bound_push": 1e-8, "mu_init": 1e-8} + results = solver.solve(model, tee=True) + + assert check_optimal_termination(results) + + @pytest.mark.component + def test_solution(self, model): + + assert value(model.fs.R1.outlet.flow_vol[0]) == pytest.approx(1.06747, rel=1e-3) + + assert value(model.fs.R1.outlet.temperature[0]) == pytest.approx( + 298.15, rel=1e-4 + ) + assert value(model.fs.R1.outlet.pressure[0]) == pytest.approx(101325, rel=1e-4) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_A"]) == pytest.approx( + 4.6374e-5, rel=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_F"]) == pytest.approx( + 4.555e-4, rel=1e-2 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_I"]) == pytest.approx( + 30e-3, rel=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_N2"]) == pytest.approx( + 29.748e-3, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_NH4"]) == pytest.approx( + 6.8070e-3, rel=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_NO3"]) == pytest.approx( + 7.273e-3, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_O2"]) == pytest.approx( + 1.210e-4, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_PO4"]) == pytest.approx( + 7.4478e-3, rel=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_AUT"]) == pytest.approx( + 118.547e-3, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_H"]) == pytest.approx( + 1.8554, rel=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_I"]) == pytest.approx( + 1.6964, rel=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_MeOH"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_MeP"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_PAO"]) == pytest.approx( + 214.821e-3, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_PHA"]) == pytest.approx( + 1.668e-3, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_PP"]) == pytest.approx( + 64.001e-3, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_S"]) == pytest.approx( + 64.513e-3, rel=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_TSS"]) == pytest.approx( + 3.524, rel=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_Mg"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_K"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(model.fs.R1.outlet.alkalinity[0]) == pytest.approx( + 4.5433e-3, rel=1e-4 + ) + + +class TestAnoxicPHA: + @pytest.fixture(scope="class") + def model(self): + m = ConcreteModel() + + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.props = ModifiedASM2dParameterBlock(additional_solute_list=["S_K", "S_Mg"]) + m.fs.rxn_props = ModifiedASM2dReactionParameterBlock( + property_package=m.fs.props + ) + + m.fs.rxn_props.K_H.fix(2.5 * 1 / units.day) + m.fs.rxn_props.mu_H.fix(4.5 * 1 / units.day) + m.fs.rxn_props.q_fe.fix(2.25 * 1 / units.day) + m.fs.rxn_props.b_H.fix(0.3 * 1 / units.day) + m.fs.rxn_props.q_PHA.fix(2.5 * 1 / units.day) + m.fs.rxn_props.q_PP.fix(1.25 * 1 / units.day) + m.fs.rxn_props.mu_PAO.fix(0.88 * 1 / units.day) + m.fs.rxn_props.b_PAO.fix(0.15 * 1 / units.day) + m.fs.rxn_props.b_PP.fix(0.15 * 1 / units.day) + m.fs.rxn_props.b_PHA.fix(0.15 * 1 / units.day) + m.fs.rxn_props.mu_AUT.fix(0.675 * 1 / units.day) + m.fs.rxn_props.b_AUT.fix(0.1 * 1 / units.day) + + m.fs.R1 = CSTR(property_package=m.fs.props, reaction_package=m.fs.rxn_props) + + iscale.calculate_scaling_factors(m.fs) + + # NOTE: Concentrations of exactly 0 result in singularities, use EPS instead + EPS = 1e-8 + + # Feed conditions based on manual mass balance of inlet and recycle streams + m.fs.R1.inlet.flow_vol.fix(36892 * units.m**3 / units.day) + m.fs.R1.inlet.temperature.fix(298.15 * units.K) + m.fs.R1.inlet.pressure.fix(1 * units.atm) + m.fs.R1.inlet.conc_mass_comp[0, "S_O2"].fix(0.0041 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "S_N2"].fix(20.2931 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "S_NH4"].fix(21.4830 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "S_NO3"].fix(0.2331 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "S_PO4"].fix(10.3835 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "S_F"].fix(2.9275 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "S_A"].fix(4.9273 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "S_I"].fix(30 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "X_I"].fix(1686.7928 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "X_S"].fix(141.1854 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "X_H"].fix(1846.1747 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "X_PAO"].fix(210.1226 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "X_PP"].fix(60.6935 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "X_PHA"].fix(6.4832 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "X_AUT"].fix(115.4611 * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "X_MeOH"].fix(EPS * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "X_MeP"].fix(EPS * units.mg / units.liter) + + m.fs.R1.inlet.conc_mass_comp[0, "X_TSS"].fix(3525.429 * units.mg / units.liter) + + m.fs.R1.inlet.conc_mass_comp[0, "S_K"].fix(EPS * units.mg / units.liter) + m.fs.R1.inlet.conc_mass_comp[0, "S_Mg"].fix(EPS * units.mg / units.liter) + + # Alkalinity was given in mg/L based on C + m.fs.R1.inlet.alkalinity[0].fix(5.980 * units.mmol / units.liter) + + m.fs.R1.volume.fix(1000 * units.m**3) + + return m + + @pytest.mark.unit + def test_dof(self, model): + assert degrees_of_freedom(model) == 0 + + @pytest.mark.unit + def test_unit_consistency(self, model): + assert_units_consistent(model) == 0 + + @pytest.mark.component + def test_solve(self, model): + model.fs.R1.initialize(optarg={"bound_push": 1e-8, "mu_init": 1e-8}) + + solver = get_solver() + solver.options = {"bound_push": 1e-8, "mu_init": 1e-8} + results = solver.solve(model, tee=True) + + assert check_optimal_termination(results) + + @pytest.mark.component + def test_solution(self, model): + + assert value(model.fs.R1.outlet.flow_vol[0]) == pytest.approx(0.4266, rel=1e-3) + + assert value(model.fs.R1.outlet.temperature[0]) == pytest.approx( + 298.15, rel=1e-4 + ) + assert value(model.fs.R1.outlet.pressure[0]) == pytest.approx(101325, rel=1e-4) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_A"]) == pytest.approx( + 15.592e-3, rel=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_F"]) == pytest.approx( + 1.0699e-3, rel=1e-2 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_I"]) == pytest.approx( + 30e-3, rel=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_N2"]) == pytest.approx( + 20.524e-3, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_NH4"]) == pytest.approx( + 22.821e-3, rel=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_NO3"]) == pytest.approx( + 4.3e-5, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_O2"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_PO4"]) == pytest.approx( + 15.23e-3, rel=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_AUT"]) == pytest.approx( + 115.149e-3, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_H"]) == pytest.approx( + 1.8323, rel=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_I"]) == pytest.approx( + 1.6883, rel=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_MeOH"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_MeP"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_PAO"]) == pytest.approx( + 209.312e-3, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_PHA"]) == pytest.approx( + 17.074e-3, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_PP"]) == pytest.approx( + 56.310e-3, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_S"]) == pytest.approx( + 134.494e-3, rel=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "X_TSS"]) == pytest.approx( + 3.500, rel=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_Mg"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(model.fs.R1.outlet.conc_mass_comp[0, "S_K"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(model.fs.R1.outlet.alkalinity[0]) == pytest.approx( + 6.188e-3, rel=1e-4 + ) diff --git a/watertap/property_models/activated_sludge/tests/test_modified_asm2d_thermo.py b/watertap/property_models/activated_sludge/tests/test_modified_asm2d_thermo.py new file mode 100644 index 0000000000..f4bfa42d10 --- /dev/null +++ b/watertap/property_models/activated_sludge/tests/test_modified_asm2d_thermo.py @@ -0,0 +1,286 @@ +################################################################################# +# WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# National Renewable Energy Laboratory, and National Energy Technology +# Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +################################################################################# +""" +Tests for ASM2d thermo property package. +Authors: Chenyu Wang +""" + +import pytest +from pyomo.environ import ConcreteModel, Param, units, value, Var +from pyomo.util.check_units import assert_units_consistent +from idaes.core import MaterialBalanceType, EnergyBalanceType, MaterialFlowBasis + +from watertap.property_models.activated_sludge.modified_asm2d_properties import ( + ModifiedASM2dParameterBlock, + ModifiedASM2dStateBlock, +) +from idaes.core.util.model_statistics import ( + fixed_variables_set, + activated_constraints_set, +) + +from idaes.core.solvers import get_solver + + +# ----------------------------------------------------------------------------- +# Get default solver for testing +solver = get_solver() + + +class TestParamBlock(object): + @pytest.fixture(scope="class") + def model(self): + model = ConcreteModel() + model.params = ModifiedASM2dParameterBlock( + additional_solute_list=["S_K", "S_Mg"] + ) + + return model + + @pytest.mark.unit + def test_build(self, model): + assert model.params.state_block_class is ModifiedASM2dStateBlock + + assert len(model.params.phase_list) == 1 + for i in model.params.phase_list: + assert i == "Liq" + + assert len(model.params.component_list) == 22 + for i in model.params.component_list: + assert i in [ + "H2O", + "S_A", + "S_F", + "S_I", + "S_N2", + "S_NH4", + "S_NO3", + "S_O2", + "S_PO4", + "S_ALK", + "X_AUT", + "X_H", + "X_I", + "X_MeOH", + "X_MeP", + "X_PAO", + "X_PHA", + "X_PP", + "X_S", + "X_TSS", + "S_K", + "S_Mg", + ] + + assert isinstance(model.params.cp_mass, Param) + assert value(model.params.cp_mass) == 4182 + + assert isinstance(model.params.dens_mass, Param) + assert value(model.params.dens_mass) == 997 + + assert isinstance(model.params.pressure_ref, Param) + assert value(model.params.pressure_ref) == 101325 + + assert isinstance(model.params.temperature_ref, Param) + assert value(model.params.temperature_ref) == 298.15 + + +class TestStateBlock(object): + @pytest.fixture(scope="class") + def model(self): + model = ConcreteModel() + model.params = ModifiedASM2dParameterBlock( + additional_solute_list=["S_K", "S_Mg"] + ) + + model.props = model.params.build_state_block([1]) + + return model + + @pytest.mark.unit + def test_build(self, model): + assert isinstance(model.props[1].flow_vol, Var) + assert value(model.props[1].flow_vol) == 1 + + assert isinstance(model.props[1].pressure, Var) + assert value(model.props[1].pressure) == 101325 + + assert isinstance(model.props[1].temperature, Var) + assert value(model.props[1].temperature) == 298.15 + + assert isinstance(model.props[1].alkalinity, Var) + assert value(model.props[1].alkalinity) == 1 + + assert isinstance(model.props[1].conc_mass_comp, Var) + # H2O should not appear in conc_mass_comp + assert len(model.props[1].conc_mass_comp) == 20 + for i in model.props[1].conc_mass_comp: + assert i in [ + "S_A", + "S_F", + "S_I", + "S_N2", + "S_NH4", + "S_NO3", + "S_O2", + "S_PO4", + "X_AUT", + "X_H", + "X_I", + "X_MeOH", + "X_MeP", + "X_PAO", + "X_PHA", + "X_PP", + "X_S", + "X_TSS", + "S_K", + "S_Mg", + ] + assert value(model.props[1].conc_mass_comp[i]) == 0.1 + + @pytest.mark.unit + def test_get_material_flow_terms(self, model): + for p in model.params.phase_list: + for j in model.params.component_list: + if j == "H2O": + assert str(model.props[1].get_material_flow_terms(p, j)) == str( + model.props[1].flow_vol * model.props[1].params.dens_mass + ) + elif j == "S_ALK": + assert str(model.props[1].get_material_flow_terms(p, j)) == str( + model.props[1].flow_vol + * model.props[1].alkalinity + * (61 * units.kg / units.kmol) + ) + else: + assert str(model.props[1].get_material_flow_terms(p, j)) == str( + model.props[1].flow_vol * model.props[1].conc_mass_comp[j] + ) + + @pytest.mark.unit + def test_get_enthalpy_flow_terms(self, model): + for p in model.params.phase_list: + assert str(model.props[1].get_enthalpy_flow_terms(p)) == str( + model.props[1].flow_vol + * model.props[1].params.dens_mass + * model.props[1].params.cp_mass + * (model.props[1].temperature - model.props[1].params.temperature_ref) + ) + + @pytest.mark.unit + def test_get_material_density_terms(self, model): + for p in model.params.phase_list: + for j in model.params.component_list: + if j == "H2O": + assert str(model.props[1].get_material_density_terms(p, j)) == str( + model.props[1].params.dens_mass + ) + elif j == "S_ALK": + assert str(model.props[1].get_material_density_terms(p, j)) == str( + model.props[1].alkalinity * (61 * units.kg / units.kmol) + ) + else: + assert str(model.props[1].get_material_density_terms(p, j)) == str( + model.props[1].conc_mass_comp[j] + ) + + @pytest.mark.unit + def test_get_energy_density_terms(self, model): + for p in model.params.phase_list: + assert str(model.props[1].get_energy_density_terms(p)) == str( + model.props[1].params.dens_mass + * model.props[1].params.cp_mass + * (model.props[1].temperature - model.props[1].params.temperature_ref) + ) + + @pytest.mark.unit + def test_default_material_balance_type(self, model): + assert ( + model.props[1].default_material_balance_type() + == MaterialBalanceType.componentPhase + ) + + @pytest.mark.unit + def test_default_energy_balance_type(self, model): + assert ( + model.props[1].default_energy_balance_type() + == EnergyBalanceType.enthalpyTotal + ) + + @pytest.mark.unit + def test_get_material_flow_basis(self, model): + assert model.props[1].get_material_flow_basis() == MaterialFlowBasis.mass + + @pytest.mark.unit + def test_define_state_vars(self, model): + sv = model.props[1].define_state_vars() + + assert len(sv) == 5 + for i in sv: + assert i in [ + "flow_vol", + "alkalinity", + "conc_mass_comp", + "temperature", + "pressure", + ] + + @pytest.mark.unit + def test_define_port_members(self, model): + sv = model.props[1].define_state_vars() + + assert len(sv) == 5 + for i in sv: + assert i in [ + "flow_vol", + "alkalinity", + "conc_mass_comp", + "temperature", + "pressure", + ] + + @pytest.mark.unit + def test_define_display_vars(self, model): + sv = model.props[1].define_display_vars() + + assert len(sv) == 5 + for i in sv: + assert i in [ + "Volumetric Flowrate", + "Molar Alkalinity", + "Mass Concentration", + "Temperature", + "Pressure", + ] + + @pytest.mark.component + def test_initialize(self, model): + orig_fixed_vars = fixed_variables_set(model) + orig_act_consts = activated_constraints_set(model) + + model.props.initialize(hold_state=False) + + fin_fixed_vars = fixed_variables_set(model) + fin_act_consts = activated_constraints_set(model) + + assert len(fin_act_consts) == len(orig_act_consts) + assert len(fin_fixed_vars) == len(orig_fixed_vars) + + for c in fin_act_consts: + assert c in orig_act_consts + for v in fin_fixed_vars: + assert v in orig_fixed_vars + + @pytest.mark.unit + def check_units(self, model): + assert_units_consistent(model) diff --git a/watertap/property_models/anaerobic_digestion/adm1_properties_vapor.py b/watertap/property_models/anaerobic_digestion/adm1_properties_vapor.py index 3b62898e4f..ecde41df57 100644 --- a/watertap/property_models/anaerobic_digestion/adm1_properties_vapor.py +++ b/watertap/property_models/anaerobic_digestion/adm1_properties_vapor.py @@ -66,7 +66,7 @@ def build(self): # All soluble components on kg COD/m^3 basis self.S_h2 = Solute(doc="Hydrogen gas") self.S_ch4 = Solute(doc="Methane gas") - self.S_co2 = Solute(doc="Carbon dioxide carbon") + self.S_co2 = Solute(doc="Carbon dioxide") # Heat capacity of water self.cp_mass = pyo.Param( @@ -108,11 +108,7 @@ def define_metadata(cls, obj): "pressure": {"method": None}, "temperature": {"method": None}, "conc_mass_comp": {"method": None}, - } - ) - obj.define_custom_properties( - { - "p_sat": {"method": None}, + "pressure_sat": {"method": None}, } ) obj.add_default_units( @@ -265,26 +261,19 @@ def build(self): units=pyo.units.kg / pyo.units.m**3, ) - self.p_w_sat = pyo.Var( - domain=pyo.NonNegativeReals, - initialize=5643.8025, - doc="Water pressure", - units=pyo.units.Pa, - ) - - init = {"S_ch4": 65077, "S_co2": 36255, "S_h2": 1.639} + init = {"S_ch4": 65077, "S_co2": 36255, "S_h2": 1.639, "H2O": 5643.8025} - self.p_sat = pyo.Var( - self.params.solute_set, + self.pressure_sat = pyo.Var( + self.params.component_list, domain=pyo.NonNegativeReals, initialize=init, doc="Component pressure", units=pyo.units.Pa, ) - def p_sat_rule(b, j): + def pressure_sat_rule(b, j): if j == "S_h2": - return self.p_sat[j] == pyo.units.convert( + return b.pressure_sat[j] == pyo.units.convert( b.conc_mass_comp[j] * (1000 * pyo.units.g / pyo.units.kg) * Constants.gas_constant @@ -293,7 +282,7 @@ def p_sat_rule(b, j): to_units=pyo.units.Pa, ) elif j == "S_ch4": - return self.p_sat[j] == pyo.units.convert( + return b.pressure_sat[j] == pyo.units.convert( b.conc_mass_comp[j] * (1000 * pyo.units.g / pyo.units.kg) * Constants.gas_constant @@ -301,8 +290,19 @@ def p_sat_rule(b, j): / (64 * pyo.units.g / pyo.units.mole), to_units=pyo.units.Pa, ) + elif j == "H2O": + return b.pressure_sat[j] == ( + 0.0313 + * pyo.exp( + 5290 + * pyo.units.K + * ((1 / b.params.temperature_ref) - (1 / b.temperature)) + ) + * 101325 + * pyo.units.Pa + ) else: - return self.p_sat[j] == pyo.units.convert( + return b.pressure_sat[j] == pyo.units.convert( b.conc_mass_comp[j] * (1000 * pyo.units.g / pyo.units.kg) * Constants.gas_constant @@ -311,29 +311,12 @@ def p_sat_rule(b, j): to_units=pyo.units.Pa, ) - self._p_sat = pyo.Constraint( - self.params.solute_set, - rule=p_sat_rule, + self._pressure_sat = pyo.Constraint( + self.params.component_list, + rule=pressure_sat_rule, doc="Saturation pressure for components", ) - def p_w_sat_rule(b, t): - return ( - self.p_w_sat - == 0.0313 - * pyo.exp( - 5290 - * pyo.units.K - * ((1 / self.params.temperature_ref) - (1 / self.temperature[t])) - ) - * 101325 - * pyo.units.Pa - ) - - self._p_w_sat = pyo.Constraint( - rule=p_w_sat_rule, doc="Saturation pressure for water" - ) - def material_flow_expression(self, j): if j == "H2O": return self.flow_vol * self.params.dens_mass @@ -385,10 +368,10 @@ def energy_density_expression(self): iscale.set_scaling_factor(self.temperature, 1e-2) iscale.set_scaling_factor(self.pressure, 1e-4) iscale.set_scaling_factor(self.conc_mass_comp, 1e1) - iscale.set_scaling_factor(self.p_sat["S_ch4"], 1e-4) - iscale.set_scaling_factor(self.p_sat["S_co2"], 1e-4) - iscale.set_scaling_factor(self.p_sat["S_h2"], 1e-1) - iscale.set_scaling_factor(self.p_w_sat, 1e-3) + iscale.set_scaling_factor(self.pressure_sat["S_ch4"], 1e-4) + iscale.set_scaling_factor(self.pressure_sat["S_co2"], 1e-4) + iscale.set_scaling_factor(self.pressure_sat["S_h2"], 1e-1) + iscale.set_scaling_factor(self.pressure_sat["H2O"], 1e-3) def get_material_flow_terms(self, p, j): return self.material_flow_expression[j] @@ -455,21 +438,11 @@ def calculate_scaling_factors(self): ) iscale.set_scaling_factor(self.energy_density_expression, sf_rho_cp * sf_T) - for t, v in self._p_sat.items(): - iscale.constraint_scaling_transform( - v, - iscale.get_scaling_factor( - self.p_sat, - default=1, - warning=True, - ), - ) - - for t, v in self._p_w_sat.items(): + for t, v in self._pressure_sat.items(): iscale.constraint_scaling_transform( v, iscale.get_scaling_factor( - self.p_w_sat, + self.pressure_sat, default=1, warning=True, ), diff --git a/watertap/property_models/anaerobic_digestion/modified_adm1_properties.py b/watertap/property_models/anaerobic_digestion/modified_adm1_properties.py new file mode 100644 index 0000000000..66649e4b0b --- /dev/null +++ b/watertap/property_models/anaerobic_digestion/modified_adm1_properties.py @@ -0,0 +1,457 @@ +################################################################################# +# WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# National Renewable Energy Laboratory, and National Energy Technology +# Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +################################################################################# +""" +Thermophysical property package to be used in conjunction with ADM1 reactions. +""" + +# Import Pyomo libraries +import pyomo.environ as pyo + +# Import IDAES cores +from idaes.core import ( + declare_process_block_class, + MaterialFlowBasis, + PhysicalParameterBlock, + StateBlockData, + StateBlock, + MaterialBalanceType, + EnergyBalanceType, + LiquidPhase, + Component, + Solute, + Solvent, +) +from idaes.core.util.model_statistics import degrees_of_freedom +from idaes.core.util.initialization import fix_state_vars, revert_state_vars +import idaes.logger as idaeslog +import idaes.core.util.scaling as iscale + +# Some more information about this module +__author__ = "Chenyu Wang, Marcus Holly" +# Using Andrew Lee's formulation of ASM1 as a template + +# Set up logger +_log = idaeslog.getLogger(__name__) + + +@declare_process_block_class("ModifiedADM1ParameterBlock") +class ModifiedADM1ParameterData(PhysicalParameterBlock): + """ + Property Parameter Block Class + """ + + def build(self): + """ + Callable method for Block construction. + """ + super().build() + + self._state_block_class = ModifiedADM1StateBlock + + # Add Phase objects + self.Liq = LiquidPhase() + + # Add Component objects + self.H2O = Solvent() + + # All soluble components on kg COD/m^3 basis + self.S_su = Solute(doc="Monosaccharides") + self.S_aa = Solute(doc="Amino acids") + self.S_fa = Solute(doc="Long chain fatty acids") + self.S_va = Solute(doc="Total valerate") + self.S_bu = Solute(doc="Total butyrate") + self.S_pro = Solute(doc="Total propionate") + self.S_ac = Solute(doc="Total acetate") + self.S_h2 = Solute(doc="Hydrogen gas") + self.S_ch4 = Solute(doc="Methane gas") + self.S_IC = Solute(doc="Inorganic carbon") + self.S_IN = Solute(doc="Inorganic nitrogen") + self.S_IP = Solute(doc="Inorganic phosphorus") + self.S_I = Solute(doc="Soluble inerts") + + self.X_ch = Solute(doc="Carbohydrates") + self.X_pr = Solute(doc="Proteins") + self.X_li = Solute(doc="Lipids") + self.X_su = Solute(doc="Sugar degraders") + self.X_aa = Solute(doc="Amino acid degraders") + self.X_fa = Solute(doc="Long chain fatty acid (LCFA) degraders") + self.X_c4 = Solute(doc="Valerate and butyrate degraders") + self.X_pro = Solute(doc="Propionate degraders") + self.X_ac = Solute(doc="Acetate degraders") + self.X_h2 = Solute(doc="Hydrogen degraders") + self.X_I = Solute(doc="Particulate inerts") + + self.X_PHA = Solute(doc="Polyhydroxyalkanoates") + self.X_PP = Solute(doc="Polyphosphates") + self.X_PAO = Solute(doc="Phosphorus accumulating organisms") + self.S_K = Solute(doc="Potassium") + self.S_Mg = Solute(doc="Magnesium") + + self.S_cat = Component(doc="Total cation equivalents concentration") + self.S_an = Component(doc="Total anion equivalents concentration") + + # Heat capacity of water + self.cp_mass = pyo.Param( + mutable=False, + initialize=4182, + doc="Specific heat capacity of water", + units=pyo.units.J / pyo.units.kg / pyo.units.K, + ) + # Density of water + self.dens_mass = pyo.Param( + mutable=False, + initialize=997, + doc="Density of water", + units=pyo.units.kg / pyo.units.m**3, + ) + + # Thermodynamic reference state + self.pressure_ref = pyo.Param( + within=pyo.PositiveReals, + mutable=True, + default=101325.0, + doc="Reference pressure", + units=pyo.units.Pa, + ) + self.temperature_ref = pyo.Param( + within=pyo.PositiveReals, + mutable=True, + default=298.15, + doc="Reference temperature", + units=pyo.units.K, + ) + + @classmethod + def define_metadata(cls, obj): + obj.add_properties( + { + "flow_vol": {"method": None}, + "pressure": {"method": None}, + "temperature": {"method": None}, + "conc_mass_comp": {"method": None}, + } + ) + obj.define_custom_properties( + { + "anions": {"method": None}, + "cations": {"method": None}, + } + ) + obj.add_default_units( + { + "time": pyo.units.s, + "length": pyo.units.m, + "mass": pyo.units.kg, + "amount": pyo.units.kmol, + "temperature": pyo.units.K, + } + ) + + +class _ModifiedADM1StateBlock(StateBlock): + """ + This Class contains methods which should be applied to Property Blocks as a + whole, rather than individual elements of indexed Property Blocks. + """ + + def initialize( + self, + state_args=None, + state_vars_fixed=False, + hold_state=False, + outlvl=idaeslog.NOTSET, + solver=None, + optarg=None, + ): + """ + Initialization routine for property package. + + Keyword Arguments: + state_args : Dictionary with initial guesses for the state vars + chosen. Note that if this method is triggered + through the control volume, and if initial guesses + were not provided at the unit model level, the + control volume passes the inlet values as initial + guess.The keys for the state_args dictionary are: + flow_mol_comp : value at which to initialize component flows (default=None) + pressure : value at which to initialize pressure (default=None) + temperature : value at which to initialize temperature (default=None) + outlvl : sets output level of initialization routine + state_vars_fixed: Flag to denote if state vars have already been fixed. + True - states have already been fixed and + initialization does not need to worry + about fixing and unfixing variables. + False - states have not been fixed. The state + block will deal with fixing/unfixing. + optarg : solver options dictionary object (default=None, use + default solver options) + solver : str indicating which solver to use during + initialization (default = None, use default solver) + hold_state : flag indicating whether the initialization routine + should unfix any state variables fixed during + initialization (default=False). + True - states variables are not unfixed, and + a dict of returned containing flags for + which states were fixed during initialization. + False - state variables are unfixed after + initialization by calling the release_state method. + + Returns: + If hold_states is True, returns a dict containing flags for + which states were fixed during initialization. + """ + init_log = idaeslog.getInitLogger(self.name, outlvl, tag="properties") + + if state_vars_fixed is False: + # Fix state variables if not already fixed + flags = fix_state_vars(self, state_args) + + else: + # Check when the state vars are fixed already result in dof 0 + for k in self.keys(): + if degrees_of_freedom(self[k]) != 0: + raise Exception( + "State vars fixed but degrees of freedom " + "for state block is not zero during " + "initialization." + ) + + if state_vars_fixed is False: + if hold_state is True: + return flags + else: + self.release_state(flags) + + init_log.info("Initialization Complete.") + + def release_state(self, flags, outlvl=idaeslog.NOTSET): + """ + Method to release state variables fixed during initialization. + + Keyword Arguments: + flags : dict containing information of which state variables + were fixed during initialization, and should now be + unfixed. This dict is returned by initialize if + hold_state=True. + outlvl : sets output level of logging + """ + init_log = idaeslog.getInitLogger(self.name, outlvl, tag="properties") + + if flags is None: + return + # Unfix state variables + revert_state_vars(self, flags) + init_log.info("State Released.") + + +@declare_process_block_class( + "ModifiedADM1StateBlock", block_class=_ModifiedADM1StateBlock +) +class ModifiedADM1StateBlockData(StateBlockData): + """ + StateBlock for calculating thermophysical properties associated with the modified ADM1 + reaction system. + """ + + def build(self): + """ + Callable method for Block construction + """ + super().build() + + # Create state variables + self.flow_vol = pyo.Var( + initialize=1, + domain=pyo.NonNegativeReals, + doc="Total volumetric flowrate", + units=pyo.units.m**3 / pyo.units.s, + ) + self.pressure = pyo.Var( + domain=pyo.NonNegativeReals, + initialize=101325.0, + bounds=(1e1, 1e6), + doc="Pressure", + units=pyo.units.Pa, + ) + self.temperature = pyo.Var( + domain=pyo.NonNegativeReals, + initialize=298.15, + bounds=(298.15, 323.15), + doc="Temperature", + units=pyo.units.K, + ) + self.conc_mass_comp = pyo.Var( + self.params.solute_set, + domain=pyo.NonNegativeReals, + initialize=0.001, + doc="Component mass concentrations", + units=pyo.units.kg / pyo.units.m**3, + ) + self.anions = pyo.Var( + domain=pyo.NonNegativeReals, + initialize=0.02, + doc="Anions in molar concentration", + units=pyo.units.kmol / pyo.units.m**3, + ) + self.cations = pyo.Var( + domain=pyo.NonNegativeReals, + initialize=0.04, + doc="Cations in molar concentration", + units=pyo.units.kmol / pyo.units.m**3, + ) + + def material_flow_expression(self, j): + if j == "H2O": + return self.flow_vol * self.params.dens_mass + elif j == "S_an": + # Convert moles of anions to mass assuming all is CL- + return ( + self.flow_vol * self.anions * (35 * pyo.units.kg / pyo.units.kmol) + ) + elif j == "S_cat": + # Convert moles of cations to mass assuming all is Na+ + return ( + self.flow_vol * self.cations * (23 * pyo.units.kg / pyo.units.kmol) + ) + else: + return self.flow_vol * self.conc_mass_comp[j] + + self.material_flow_expression = pyo.Expression( + self.component_list, + rule=material_flow_expression, + doc="Material flow terms", + ) + + def enthalpy_flow_expression(self): + return ( + self.flow_vol + * self.params.dens_mass + * self.params.cp_mass + * (self.temperature - self.params.temperature_ref) + ) + + self.enthalpy_flow_expression = pyo.Expression( + rule=enthalpy_flow_expression, doc="Enthalpy flow term" + ) + + def material_density_expression(self, j): + if j == "H2O": + return self.params.dens_mass + elif j == "S_cat": + # Convert moles of alkalinity to mass of cations assuming all is Na + return self.cations * (23 * pyo.units.kg / pyo.units.kmol) + elif j == "S_an": + # Convert moles of alkalinity to mass of anions assuming all is Cl + return self.anions * (35 * pyo.units.kg / pyo.units.kmol) + else: + return self.conc_mass_comp[j] + + self.material_density_expression = pyo.Expression( + self.component_list, + rule=material_density_expression, + doc="Material density terms", + ) + + def energy_density_expression(self): + return ( + self.params.dens_mass + * self.params.cp_mass + * (self.temperature - self.params.temperature_ref) + ) + + self.energy_density_expression = pyo.Expression( + rule=energy_density_expression, doc="Energy density term" + ) + + iscale.set_scaling_factor(self.flow_vol, 1e1) + iscale.set_scaling_factor(self.temperature, 1e-1) + iscale.set_scaling_factor(self.pressure, 1e-3) + iscale.set_scaling_factor(self.conc_mass_comp, 1e1) + iscale.set_scaling_factor(self.anions, 1e1) + iscale.set_scaling_factor(self.cations, 1e1) + + def get_material_flow_terms(self, p, j): + return self.material_flow_expression[j] + + def get_enthalpy_flow_terms(self, p): + return self.enthalpy_flow_expression + + def get_material_density_terms(self, p, j): + return self.material_density_expression[j] + + def get_energy_density_terms(self, p): + return self.energy_density_expression + + def default_material_balance_type(self): + return MaterialBalanceType.componentPhase + + def default_energy_balance_type(self): + return EnergyBalanceType.enthalpyTotal + + def define_state_vars(self): + return { + "flow_vol": self.flow_vol, + "anions": self.anions, + "cations": self.cations, + "conc_mass_comp": self.conc_mass_comp, + "temperature": self.temperature, + "pressure": self.pressure, + } + + def define_display_vars(self): + return { + "Volumetric Flowrate": self.flow_vol, + "Molar anions": self.anions, + "Molar cations": self.cations, + "Mass Concentration": self.conc_mass_comp, + "Temperature": self.temperature, + "Pressure": self.pressure, + } + + def get_material_flow_basis(self): + return MaterialFlowBasis.mass + + def calculate_scaling_factors(self): + # Get default scale factors and do calculations from base classes + super().calculate_scaling_factors() + + # No constraints in this model as yet, just need to set scaling factors + # for expressions + sf_F = iscale.get_scaling_factor(self.flow_vol, default=1, warning=True) + sf_T = iscale.get_scaling_factor(self.temperature, default=1, warning=True) + + # Mass flow and density terms + for j in self.component_list: + if j == "H2O": + sf_C = pyo.value(1 / self.params.dens_mass) + elif j == "S_cat": + sf_C = 1e-1 * iscale.get_scaling_factor( + self.cations, default=1, warning=True + ) + elif j == "S_an": + sf_C = 1e-1 * iscale.get_scaling_factor( + self.anions, default=1, warning=True + ) + else: + sf_C = iscale.get_scaling_factor( + self.conc_mass_comp[j], default=1e2, warning=True + ) + + iscale.set_scaling_factor(self.material_flow_expression[j], sf_F * sf_C) + iscale.set_scaling_factor(self.material_density_expression[j], sf_C) + + # Enthalpy and energy terms + sf_rho_cp = pyo.value(1 / (self.params.dens_mass * self.params.cp_mass)) + iscale.set_scaling_factor( + self.enthalpy_flow_expression, sf_F * sf_rho_cp * sf_T + ) + iscale.set_scaling_factor(self.energy_density_expression, sf_rho_cp * sf_T) diff --git a/watertap/property_models/anaerobic_digestion/modified_adm1_reactions.py b/watertap/property_models/anaerobic_digestion/modified_adm1_reactions.py new file mode 100644 index 0000000000..73e56be519 --- /dev/null +++ b/watertap/property_models/anaerobic_digestion/modified_adm1_reactions.py @@ -0,0 +1,2573 @@ +################################################################################# +# WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# National Renewable Energy Laboratory, and National Energy Technology +# Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +################################################################################# +""" +Modified ADM1 reaction package. + +Reference: + +X. Flores-Alsina, K. Solon, C.K. Mbamba, S. Tait, K.V. Gernaey, U. Jeppsson, D.J. Batstone, +Modelling phosphorus (P), sulfur (S) and iron (Fe) interactions fordynamic simulations of anaerobic digestion processes, +Water Research. 95 (2016) 370-382. https://www.sciencedirect.com/science/article/pii/S0043135416301397 + +""" + +# Import Pyomo libraries +import pyomo.environ as pyo +from pyomo.environ import Suffix + +# Import IDAES cores +from idaes.core import ( + declare_process_block_class, + MaterialFlowBasis, + ReactionParameterBlock, + ReactionBlockDataBase, + ReactionBlockBase, +) +from idaes.core.util.constants import Constants +from idaes.core.util.misc import add_object_reference +from idaes.core.util.exceptions import BurntToast +import idaes.logger as idaeslog +import idaes.core.util.scaling as iscale + +# Some more information about this module +__author__ = "Chenyu Wang, Marcus Holly" + +# Set up logger +_log = idaeslog.getLogger(__name__) + + +@declare_process_block_class("ModifiedADM1ReactionParameterBlock") +class ModifiedADM1ReactionParameterData(ReactionParameterBlock): + """ + Property Parameter Block Class + """ + + def build(self): + """ + Callable method for Block construction. + """ + super().build() + + self.scaling_factor = Suffix(direction=Suffix.EXPORT) + + self._reaction_block_class = ModifiedADM1ReactionBlock + + # Reaction Index + # Reaction names based on standard numbering in ADM1 + # R1: Hydrolysis of carbohydrates + # R2: Hydrolysis of proteins + # R3: Hydrolysis of lipids + # R4: Uptake of sugars + # R5: Uptake of amino acids + # R6: Uptake of long chain fatty acids (LCFAs) + # R7: Uptake of valerate + # R8: Uptake of butyrate + # R9: Uptake of propionate + # R10: Uptake of acetate + # R11: Uptake of hydrogen + # R12: Decay of X_su + # R13: Decay of X_aa + # R14: Decay of X_fa + # R15: Decay of X_c4 + # R16: Decay of X_pro + # R17: Decay of X_ac + # R18: Decay of X_h2 + # R19: Storage of S_va in X_PHA + # R20: Storage of S_bu in X_PHA + # R21: Storage of S_pro in X_PHA + # R22: Storage of S_ac in X_PHA + # R23: Lysis of X_PAO + # R24: Lysis of X_PP + # R25: Lysis of X_PHA + + self.rate_reaction_idx = pyo.Set( + initialize=[ + "R1", + "R2", + "R3", + "R4", + "R5", + "R6", + "R7", + "R8", + "R9", + "R10", + "R11", + "R12", + "R13", + "R14", + "R15", + "R16", + "R17", + "R18", + "R19", + "R20", + "R21", + "R22", + "R23", + "R24", + "R25", + ] + ) + + # Carbon content + Ci_dict = { + "S_su": 0.031250000, + "S_aa": 0.030741549, + "S_fa": 0.021404110, + "S_va": 0.024038462, + "S_bu": 0.02500000, + "S_pro": 0.026785714, + "S_ac": 0.0312500000, + "S_ch4": 0.015625000, + "S_I": 0.030148202, + "X_ch": 0.031250000, + "X_pr": 0.030741549, + "X_li": 0.021925591, + "X_su": 0.030509792, + "X_aa": 0.030509792, + "X_fa": 0.030509792, + "X_c4": 0.030509792, + "X_pro": 0.030509792, + "X_ac": 0.030509792, + "X_h2": 0.030509792, + "X_I": 0.030148202, + "X_PHA": 0.02500000, + "X_PAO": 0.030509792, + } + + self.Ci = pyo.Var( + Ci_dict.keys(), + initialize=Ci_dict, + units=pyo.units.kmol / pyo.units.kg, + domain=pyo.PositiveReals, + doc="Carbon content of component [kmole C/kg COD]", + ) + + # Nitrogen content + Ni_dict = { + "S_aa": 0.0079034, + "S_I": 0.0042876, + "X_pr": 0.0079034, + "X_su": 0.0061532, + "X_aa": 0.0061532, + "X_fa": 0.0061532, + "X_c4": 0.0061532, + "X_pro": 0.0061532, + "X_ac": 0.0061532, + "X_h2": 0.0061532, + "X_I": 0.0042876, + "X_PAO": 0.0061532, + } + + self.Ni = pyo.Var( + Ni_dict.keys(), + initialize=Ni_dict, + units=pyo.units.kmol / pyo.units.kg, + domain=pyo.PositiveReals, + doc="Nitrogen content of component [kmole N/kg COD]", + ) + + # Phosphorus content + Pi_dict = { + "S_I": 0.0002093322, + "X_li": 0.0003440808, + "X_su": 0.0006947201, + "X_aa": 0.0006947201, + "X_fa": 0.0006947201, + "X_c4": 0.0006947201, + "X_pro": 0.0006947201, + "X_ac": 0.0006947201, + "X_h2": 0.0006947201, + "X_I": 0.0002093322, + "X_PP": 1, + "X_PAO": 0.0006947201, + } + + self.Pi = pyo.Var( + Pi_dict.keys(), + initialize=Pi_dict, + units=pyo.units.kmol / pyo.units.kg, + domain=pyo.PositiveReals, + doc="Phosphorus content of component [kmole P/kg COD]", + ) + + mw_n = 14 * pyo.units.kg / pyo.units.kmol + mw_c = 12 * pyo.units.kg / pyo.units.kmol + mw_p = 31 * pyo.units.kg / pyo.units.kmol + + # TODO: Consider inheriting these parameters from ADM1 such that there is less repeated code + + # Stoichiometric Parameters (Table 1.1 and 2.1 in Flores-Alsina et al., 2016) + self.Z_h2s = pyo.Param( + within=pyo.NonNegativeReals, + mutable=True, + default=0, + doc="Reference component mass concentration of hydrogen sulfide", + units=pyo.units.kg / pyo.units.m**3, + ) + self.f_xi_xb = pyo.Var( + initialize=0.1, + units=pyo.units.dimensionless, + domain=pyo.PositiveReals, + doc="Fraction of inert particulate organics from biomass", + ) + self.f_ch_xb = pyo.Var( + initialize=0.275, + units=pyo.units.dimensionless, + domain=pyo.PositiveReals, + doc="Fraction of carbohydrates from biomass", + ) + self.f_li_xb = pyo.Var( + initialize=0.350, + units=pyo.units.dimensionless, + domain=pyo.PositiveReals, + doc="Fraction of lipids from biomass", + ) + self.f_pr_xb = pyo.Var( + initialize=0.275, + units=pyo.units.dimensionless, + domain=pyo.PositiveReals, + doc="Fraction of proteins from biomass", + ) + self.f_si_xb = pyo.Var( + initialize=0, + units=pyo.units.dimensionless, + domain=pyo.NonNegativeReals, + doc="Fraction of soluble inerts from biomass", + ) + self.f_fa_li = pyo.Var( + initialize=0.95, + units=pyo.units.dimensionless, + domain=pyo.PositiveReals, + doc="Fatty acids from lipids", + ) + self.f_h2_su = pyo.Var( + initialize=0.1906, + units=pyo.units.dimensionless, + domain=pyo.PositiveReals, + doc="Hydrogen from sugars", + ) + self.f_bu_su = pyo.Var( + initialize=0.1328, + units=pyo.units.dimensionless, + domain=pyo.PositiveReals, + doc="Butyrate from sugars", + ) + self.f_pro_su = pyo.Var( + initialize=0.2691, + units=pyo.units.dimensionless, + domain=pyo.PositiveReals, + doc="Propionate from sugars", + ) + self.f_ac_su = pyo.Var( + initialize=0.4076, + units=pyo.units.dimensionless, + domain=pyo.PositiveReals, + doc="Acetate from sugars", + ) + self.f_h2_aa = pyo.Var( + initialize=0.06, + units=pyo.units.dimensionless, + domain=pyo.PositiveReals, + doc="Hydrogen from amino acids", + ) + + self.f_va_aa = pyo.Var( + initialize=0.23, + units=pyo.units.dimensionless, + domain=pyo.PositiveReals, + doc="Valerate from amino acids", + ) + self.f_bu_aa = pyo.Var( + initialize=0.26, + units=pyo.units.dimensionless, + domain=pyo.PositiveReals, + doc="Butyrate from amino acids", + ) + self.f_pro_aa = pyo.Var( + initialize=0.05, + units=pyo.units.dimensionless, + domain=pyo.PositiveReals, + doc="Propionate from amino acids", + ) + self.f_ac_aa = pyo.Var( + initialize=0.40, + units=pyo.units.dimensionless, + domain=pyo.PositiveReals, + doc="Acetate from amino acids", + ) + self.Y_su = pyo.Var( + initialize=0.10, + units=pyo.units.dimensionless, + domain=pyo.PositiveReals, + doc="Yield of biomass on sugar substrate [kg COD X/ kg COD S]", + ) + self.Y_aa = pyo.Var( + initialize=0.08, + units=pyo.units.dimensionless, + domain=pyo.PositiveReals, + doc="Yield of biomass on amino acid substrate [kg COD X/ kg COD S]", + ) + self.Y_fa = pyo.Var( + initialize=0.06, + units=pyo.units.dimensionless, + domain=pyo.PositiveReals, + doc="Yield of biomass on fatty acid substrate [kg COD X/ kg COD S]", + ) + self.Y_c4 = pyo.Var( + initialize=0.06, + units=pyo.units.dimensionless, + domain=pyo.PositiveReals, + doc="Yield of biomass on valerate and butyrate substrate [kg COD X/ kg COD S]", + ) + self.Y_pro = pyo.Var( + initialize=0.04, + units=pyo.units.dimensionless, + domain=pyo.PositiveReals, + doc="Yield of biomass on propionate substrate [kg COD X/ kg COD S]", + ) + self.Y_ac = pyo.Var( + initialize=0.05, + units=pyo.units.dimensionless, + domain=pyo.PositiveReals, + doc="Yield of biomass on acetate substrate [kg COD X/ kg COD S]", + ) + self.Y_h2 = pyo.Var( + initialize=0.06, + units=pyo.units.dimensionless, + domain=pyo.PositiveReals, + doc="Yield of hydrogen per biomass [kg COD S/ kg COD X]", + ) + # Biochemical Parameters + self.k_hyd_ch = pyo.Var( + initialize=10, + units=pyo.units.day**-1, + domain=pyo.PositiveReals, + doc="First-order kinetic parameter for hydrolysis of carbohydrates", + ) + self.k_hyd_pr = pyo.Var( + initialize=10, + units=pyo.units.day**-1, + domain=pyo.PositiveReals, + doc="First-order kinetic parameter for hydrolysis of proteins", + ) + self.k_hyd_li = pyo.Var( + initialize=10, + units=pyo.units.day**-1, + domain=pyo.PositiveReals, + doc="First-order kinetic parameter for hydrolysis of lipids", + ) + self.K_S_IN = pyo.Var( + initialize=1e-4, + units=pyo.units.kmol * pyo.units.m**-3, + domain=pyo.PositiveReals, + doc="Inhibition parameter for inorganic nitrogen", + ) + self.k_m_su = pyo.Var( + initialize=30, + units=pyo.units.day**-1, + domain=pyo.PositiveReals, + doc="Monod maximum specific uptake rate of sugars", + ) + self.K_S_su = pyo.Var( + initialize=0.5, + units=pyo.units.kg * pyo.units.m**-3, + domain=pyo.PositiveReals, + doc="Half saturation value for uptake of sugars", + ) + self.pH_UL_aa = pyo.Var( + initialize=5.5, + units=pyo.units.dimensionless, + domain=pyo.PositiveReals, + doc="Upper limit of pH for uptake rate of amino acids", + ) + self.pH_LL_aa = pyo.Var( + initialize=4, + units=pyo.units.dimensionless, + domain=pyo.PositiveReals, + doc="Lower limit of pH for uptake rate of amino acids", + ) + self.k_m_aa = pyo.Var( + initialize=50, + units=pyo.units.day**-1, + domain=pyo.PositiveReals, + doc="Monod maximum specific uptake rate of amino acids", + ) + + self.K_S_aa = pyo.Var( + initialize=0.3, + units=pyo.units.kg * pyo.units.m**-3, + domain=pyo.PositiveReals, + doc="Half saturation value for uptake of amino acids", + ) + self.k_m_fa = pyo.Var( + initialize=6, + units=pyo.units.day**-1, + domain=pyo.PositiveReals, + doc="Monod maximum specific uptake rate of fatty acids", + ) + self.K_S_fa = pyo.Var( + initialize=0.4, + units=pyo.units.kg * pyo.units.m**-3, + domain=pyo.PositiveReals, + doc="Half saturation value for uptake of fatty acids", + ) + self.K_I_h2_fa = pyo.Var( + initialize=5e-6, + units=pyo.units.kg * pyo.units.m**-3, + domain=pyo.PositiveReals, + doc="Inhibition parameter for hydrogen during uptake of fatty acids", + ) + self.k_m_c4 = pyo.Var( + initialize=20, + units=pyo.units.day**-1, + domain=pyo.PositiveReals, + doc="Monod maximum specific uptake rate of valerate and butyrate", + ) + self.K_S_c4 = pyo.Var( + initialize=0.2, + units=pyo.units.kg * pyo.units.m**-3, + domain=pyo.PositiveReals, + doc="Half saturation value for uptake of valerate and butyrate", + ) + self.K_I_h2_c4 = pyo.Var( + initialize=1e-5, + units=pyo.units.kg * pyo.units.m**-3, + domain=pyo.PositiveReals, + doc="Inhibition parameter for hydrogen during uptake of valerate and butyrate", + ) + self.k_m_pro = pyo.Var( + initialize=13, + units=pyo.units.day**-1, + domain=pyo.PositiveReals, + doc="Monod maximum specific uptake rate of propionate", + ) + self.K_S_pro = pyo.Var( + initialize=0.1, + units=pyo.units.kg * pyo.units.m**-3, + domain=pyo.PositiveReals, + doc="Half saturation value for uptake of propionate", + ) + self.K_I_h2_pro = pyo.Var( + initialize=3.5e-6, + units=pyo.units.kg * pyo.units.m**-3, + domain=pyo.PositiveReals, + doc="Inhibition parameter for hydrogen during uptake of propionate", + ) + self.k_m_ac = pyo.Var( + initialize=8, + units=pyo.units.day**-1, + domain=pyo.PositiveReals, + doc="Monod maximum specific uptake rate of acetate", + ) + self.K_S_ac = pyo.Var( + initialize=0.15, + units=pyo.units.kg * pyo.units.m**-3, + domain=pyo.PositiveReals, + doc="Half saturation value for uptake of acetate", + ) + self.K_I_nh3 = pyo.Var( + initialize=0.0018, + units=pyo.units.kmol * pyo.units.m**-3, + domain=pyo.PositiveReals, + doc="Inhibition parameter for ammonia during uptake of acetate", + ) + self.pH_UL_ac = pyo.Var( + initialize=7, + units=pyo.units.dimensionless, + domain=pyo.PositiveReals, + doc="Upper limit of pH for uptake rate of acetate", + ) + self.pH_LL_ac = pyo.Var( + initialize=6, + units=pyo.units.dimensionless, + domain=pyo.PositiveReals, + doc="Lower limit of pH for uptake rate of acetate", + ) + self.k_m_h2 = pyo.Var( + initialize=35, + units=pyo.units.day**-1, + domain=pyo.PositiveReals, + doc="Monod maximum specific uptake rate of hydrogen", + ) + self.K_S_h2 = pyo.Var( + initialize=7e-6, + units=pyo.units.kg * pyo.units.m**-3, + domain=pyo.PositiveReals, + doc="Half saturation value for uptake of hydrogen", + ) + self.pH_UL_h2 = pyo.Var( + initialize=6, + units=pyo.units.dimensionless, + domain=pyo.PositiveReals, + doc="Upper limit of pH for uptake rate of hydrogen", + ) + self.pH_LL_h2 = pyo.Var( + initialize=5, + units=pyo.units.dimensionless, + domain=pyo.PositiveReals, + doc="Lower limit of pH for uptake rate of hydrogen", + ) + self.k_dec_X_su = pyo.Var( + initialize=0.02, + units=pyo.units.day**-1, + domain=pyo.PositiveReals, + doc="First-order decay rate for X_su", + ) + self.k_dec_X_aa = pyo.Var( + initialize=0.02, + units=pyo.units.day**-1, + domain=pyo.PositiveReals, + doc="First-order decay rate for X_aa", + ) + self.k_dec_X_fa = pyo.Var( + initialize=0.02, + units=pyo.units.day**-1, + domain=pyo.PositiveReals, + doc="First-order decay rate for X_fa", + ) + self.k_dec_X_c4 = pyo.Var( + initialize=0.02, + units=pyo.units.day**-1, + domain=pyo.PositiveReals, + doc="First-order decay rate for X_c4", + ) + self.k_dec_X_pro = pyo.Var( + initialize=0.02, + units=pyo.units.day**-1, + domain=pyo.PositiveReals, + doc="First-order decay rate for X_pro", + ) + self.k_dec_X_ac = pyo.Var( + initialize=0.02, + units=pyo.units.day**-1, + domain=pyo.PositiveReals, + doc="First-order decay rate for X_ac", + ) + self.k_dec_X_h2 = pyo.Var( + initialize=0.02, + units=pyo.units.day**-1, + domain=pyo.PositiveReals, + doc="First-order decay rate for X_h2", + ) + self.K_a_va = pyo.Var( + initialize=1.38e-5, + units=pyo.units.kmol / pyo.units.m**3, + domain=pyo.PositiveReals, + doc="Valerate acid-base equilibrium constant", + ) + self.K_a_bu = pyo.Var( + initialize=1.5e-5, + units=pyo.units.kmol / pyo.units.m**3, + domain=pyo.PositiveReals, + doc="Butyrate acid-base equilibrium constant", + ) + self.K_a_pro = pyo.Var( + initialize=1.32e-5, + units=pyo.units.kmol / pyo.units.m**3, + domain=pyo.PositiveReals, + doc="Propionate acid-base equilibrium constant", + ) + self.K_a_ac = pyo.Var( + initialize=1.74e-5, + units=pyo.units.kmol / pyo.units.m**3, + domain=pyo.PositiveReals, + doc="Acetate acid-base equilibrium constant", + ) + self.K_I_h2s_ac = pyo.Var( + initialize=460e-3, + units=pyo.units.kg / pyo.units.m**3, + domain=pyo.PositiveReals, + doc="50% inhibitory concentration of H2S on acetogens", + ) + self.K_I_h2s_c4 = pyo.Var( + initialize=481e-3, + units=pyo.units.kg / pyo.units.m**3, + domain=pyo.PositiveReals, + doc="50% inhibitory concentration of H2S on c4 degraders", + ) + self.K_I_h2s_h2 = pyo.Var( + initialize=400e-3, + units=pyo.units.kg / pyo.units.m**3, + domain=pyo.PositiveReals, + doc="50% inhibitory concentration of H2S on hydrogenotrophic methanogens", + ) + self.K_I_h2s_pro = pyo.Var( + initialize=481e-3, + units=pyo.units.kg / pyo.units.m**3, + domain=pyo.PositiveReals, + doc="50% inhibitory concentration of propionate degraders", + ) + self.K_S_IP = pyo.Var( + initialize=2e-5, + units=pyo.units.kmol / pyo.units.m**3, + domain=pyo.PositiveReals, + doc="P limitation for inorganic phosphorus", + ) + self.b_PAO = pyo.Var( + initialize=0.2, + units=pyo.units.day**-1, + domain=pyo.PositiveReals, + doc="Lysis rate of phosphorus accumulating organisms", + ) + self.b_PHA = pyo.Var( + initialize=0.2, + units=pyo.units.day**-1, + domain=pyo.PositiveReals, + doc="Lysis rate of polyhydroxyalkanoates", + ) + self.b_PP = pyo.Var( + initialize=0.2, + units=pyo.units.day**-1, + domain=pyo.PositiveReals, + doc="Lysis rate of polyphosphates", + ) + self.f_ac_PHA = pyo.Var( + initialize=0.4, + units=pyo.units.dimensionless, + domain=pyo.PositiveReals, + doc="Yield of acetate on polyhydroxyalkanoates", + ) + self.f_bu_PHA = pyo.Var( + initialize=0.1, + units=pyo.units.dimensionless, + domain=pyo.PositiveReals, + doc="Yield of butyrate on polyhydroxyalkanoates", + ) + self.f_pro_PHA = pyo.Var( + initialize=0.4, + units=pyo.units.dimensionless, + domain=pyo.PositiveReals, + doc="Yield of propionate on polyhydroxyalkanoates", + ) + self.f_va_PHA = pyo.Var( + initialize=0.1, + units=pyo.units.dimensionless, + domain=pyo.PositiveReals, + doc="Yield of valerate on polyhydroxyalkanoates", + ) + self.K_A = pyo.Var( + initialize=4e-3, + units=pyo.units.kg * pyo.units.m**-3, + domain=pyo.PositiveReals, + doc="Saturation coefficient for acetate", + ) + self.K_PP = pyo.Var( + initialize=0.32e-3, + units=pyo.units.dimensionless, + domain=pyo.PositiveReals, + doc="Saturation coefficient for polyphosphate", + ) + self.q_PHA = pyo.Var( + initialize=3.0, + units=pyo.units.day**-1, + domain=pyo.PositiveReals, + doc="Rate constant for storage of polyhydroxyalkanoates", + ) + self.Y_PO4 = pyo.Var( + initialize=12.903e-3, + units=pyo.units.dimensionless, + domain=pyo.PositiveReals, + doc="Yield of biomass on phosphate (kmol P/kg COD)", + ) + self.K_XPP = pyo.Var( + initialize=1 / 3, + units=pyo.units.dimensionless, + domain=pyo.PositiveReals, + doc="Potassium coefficient for polyphosphates", + ) + self.Mg_XPP = pyo.Var( + initialize=1 / 3, + units=pyo.units.dimensionless, + domain=pyo.PositiveReals, + doc="Magnesium coefficient for polyphosphates", + ) + self.temperature_ref = pyo.Param( + within=pyo.PositiveReals, + mutable=True, + default=298.15, + doc="Reference temperature", + units=pyo.units.K, + ) + + # Reaction Stoichiometry + # This is the stoichiometric part of the Peterson matrix in dict form. + # See Table 1.1 and 2.1 in Flores-Alsina et al., 2016. + + # Exclude non-zero stoichiometric coefficients for S_IC initially since they depend on other stoichiometric coefficients. + self.rate_reaction_stoichiometry = { + # R1: Hydrolysis of carbohydrates + ("R1", "Liq", "H2O"): 0, + ("R1", "Liq", "S_su"): 1, + ("R1", "Liq", "S_aa"): 0, + ("R1", "Liq", "S_fa"): 0, + ("R1", "Liq", "S_va"): 0, + ("R1", "Liq", "S_bu"): 0, + ("R1", "Liq", "S_pro"): 0, + ("R1", "Liq", "S_ac"): 0, + ("R1", "Liq", "S_h2"): 0, + ("R1", "Liq", "S_ch4"): 0, + ("R1", "Liq", "S_IC"): -(self.Ci["S_su"] - self.Ci["X_ch"]) * mw_c, + ("R1", "Liq", "S_IN"): 0, + ("R1", "Liq", "S_IP"): 0, + ("R1", "Liq", "S_I"): 0, + ("R1", "Liq", "X_ch"): -1, + ("R1", "Liq", "X_pr"): 0, + ("R1", "Liq", "X_li"): 0, + ("R1", "Liq", "X_su"): 0, + ("R1", "Liq", "X_aa"): 0, + ("R1", "Liq", "X_fa"): 0, + ("R1", "Liq", "X_c4"): 0, + ("R1", "Liq", "X_pro"): 0, + ("R1", "Liq", "X_ac"): 0, + ("R1", "Liq", "X_h2"): 0, + ("R1", "Liq", "X_I"): 0, + ("R1", "Liq", "X_PHA"): 0, + ("R1", "Liq", "X_PP"): 0, + ("R1", "Liq", "X_PAO"): 0, + ("R1", "Liq", "S_K"): 0, + ("R1", "Liq", "S_Mg"): 0, + # R2: Hydrolysis of proteins + ("R2", "Liq", "H2O"): 0, + ("R2", "Liq", "S_su"): 0, + ("R2", "Liq", "S_aa"): 1, + ("R2", "Liq", "S_fa"): 0, + ("R2", "Liq", "S_va"): 0, + ("R2", "Liq", "S_bu"): 0, + ("R2", "Liq", "S_pro"): 0, + ("R2", "Liq", "S_ac"): 0, + ("R2", "Liq", "S_h2"): 0, + ("R2", "Liq", "S_ch4"): 0, + ("R2", "Liq", "S_IC"): -(self.Ci["S_aa"] - self.Ci["X_pr"]) * mw_c, + ("R2", "Liq", "S_IN"): -(self.Ni["S_aa"] - self.Ni["X_pr"]) * mw_n, + ("R2", "Liq", "S_IP"): 0, + ("R2", "Liq", "S_I"): 0, + ("R2", "Liq", "X_ch"): 0, + ("R2", "Liq", "X_pr"): -1, + ("R2", "Liq", "X_li"): 0, + ("R2", "Liq", "X_su"): 0, + ("R2", "Liq", "X_aa"): 0, + ("R2", "Liq", "X_fa"): 0, + ("R2", "Liq", "X_c4"): 0, + ("R2", "Liq", "X_pro"): 0, + ("R2", "Liq", "X_ac"): 0, + ("R2", "Liq", "X_h2"): 0, + ("R2", "Liq", "X_I"): 0, + ("R2", "Liq", "X_PHA"): 0, + ("R2", "Liq", "X_PP"): 0, + ("R2", "Liq", "X_PAO"): 0, + ("R2", "Liq", "S_K"): 0, + ("R2", "Liq", "S_Mg"): 0, + # R3: Hydrolysis of lipids + ("R3", "Liq", "H2O"): 0, + ("R3", "Liq", "S_su"): 1 - self.f_fa_li, + ("R3", "Liq", "S_aa"): 0, + ("R3", "Liq", "S_fa"): self.f_fa_li, + ("R3", "Liq", "S_va"): 0, + ("R3", "Liq", "S_bu"): 0, + ("R3", "Liq", "S_pro"): 0, + ("R3", "Liq", "S_ac"): 0, + ("R3", "Liq", "S_h2"): 0, + ("R3", "Liq", "S_ch4"): 0, + ("R3", "Liq", "S_IC"): ( + self.Ci["X_li"] + - (1 - self.f_fa_li) * self.Ci["S_su"] + - self.f_fa_li * self.Ci["S_fa"] + ) + * mw_c, + ("R3", "Liq", "S_IN"): 0, + ("R3", "Liq", "S_IP"): self.Pi["X_li"] * mw_p, + ("R3", "Liq", "S_I"): 0, + ("R3", "Liq", "X_ch"): 0, + ("R3", "Liq", "X_pr"): 0, + ("R3", "Liq", "X_li"): -1, + ("R3", "Liq", "X_su"): 0, + ("R3", "Liq", "X_aa"): 0, + ("R3", "Liq", "X_fa"): 0, + ("R3", "Liq", "X_c4"): 0, + ("R3", "Liq", "X_pro"): 0, + ("R3", "Liq", "X_ac"): 0, + ("R3", "Liq", "X_h2"): 0, + ("R3", "Liq", "X_I"): 0, + ("R3", "Liq", "X_PHA"): 0, + ("R3", "Liq", "X_PP"): 0, + ("R3", "Liq", "X_PAO"): 0, + ("R3", "Liq", "S_K"): 0, + ("R3", "Liq", "S_Mg"): 0, + # R4: Uptake of sugars + ("R4", "Liq", "H2O"): 0, + ("R4", "Liq", "S_su"): -1, + ("R4", "Liq", "S_aa"): 0, + ("R4", "Liq", "S_fa"): 0, + ("R4", "Liq", "S_va"): 0, + ("R4", "Liq", "S_bu"): (1 - self.Y_su) * self.f_bu_su, + ("R4", "Liq", "S_pro"): (1 - self.Y_su) * self.f_pro_su, + ("R4", "Liq", "S_ac"): (1 - self.Y_su) * self.f_ac_su, + ("R4", "Liq", "S_h2"): (1 - self.Y_su) * self.f_h2_su, + ("R4", "Liq", "S_ch4"): 0, + ("R4", "Liq", "S_IC"): ( + self.Ci["S_su"] + - (1 - self.Y_su) + * ( + self.f_bu_su * self.Ci["S_bu"] + + self.f_pro_su * self.Ci["S_pro"] + + self.f_ac_su * self.Ci["S_ac"] + ) + - self.Y_su * self.Ci["X_su"] + ) + * mw_c, + ("R4", "Liq", "S_IN"): (-self.Y_su * self.Ni["X_su"]) * mw_n, + ("R4", "Liq", "S_IP"): (-self.Y_su * self.Pi["X_su"]) * mw_p, + ("R4", "Liq", "S_I"): 0, + ("R4", "Liq", "X_ch"): 0, + ("R4", "Liq", "X_pr"): 0, + ("R4", "Liq", "X_li"): 0, + ("R4", "Liq", "X_su"): self.Y_su, + ("R4", "Liq", "X_aa"): 0, + ("R4", "Liq", "X_fa"): 0, + ("R4", "Liq", "X_c4"): 0, + ("R4", "Liq", "X_pro"): 0, + ("R4", "Liq", "X_ac"): 0, + ("R4", "Liq", "X_h2"): 0, + ("R4", "Liq", "X_I"): 0, + ("R4", "Liq", "X_PHA"): 0, + ("R4", "Liq", "X_PP"): 0, + ("R4", "Liq", "X_PAO"): 0, + ("R4", "Liq", "S_K"): 0, + ("R4", "Liq", "S_Mg"): 0, + # R5: Uptake of amino acids + ("R5", "Liq", "H2O"): 0, + ("R5", "Liq", "S_su"): 0, + ("R5", "Liq", "S_aa"): -1, + ("R5", "Liq", "S_fa"): 0, + ("R5", "Liq", "S_va"): (1 - self.Y_aa) * self.f_va_aa, + ("R5", "Liq", "S_bu"): (1 - self.Y_aa) * self.f_bu_aa, + ("R5", "Liq", "S_pro"): (1 - self.Y_aa) * self.f_pro_aa, + ("R5", "Liq", "S_ac"): (1 - self.Y_aa) * self.f_ac_aa, + ("R5", "Liq", "S_h2"): (1 - self.Y_aa) * self.f_h2_aa, + ("R5", "Liq", "S_ch4"): 0, + ("R5", "Liq", "S_IC"): ( + self.Ci["S_aa"] + - (1 - self.Y_aa) + * ( + self.f_va_aa * self.Ci["S_va"] + + self.f_bu_aa * self.Ci["S_bu"] + + self.f_pro_aa * self.Ci["S_pro"] + + self.f_ac_aa * self.Ci["S_ac"] + ) + - self.Y_aa * self.Ci["X_aa"] + ) + * mw_c, + ("R5", "Liq", "S_IN"): -(-self.Ni["S_aa"] + self.Y_aa * self.Ni["X_aa"]) + * mw_n, + ("R5", "Liq", "S_IP"): -(self.Y_aa * self.Pi["X_aa"]) * mw_p, + ("R5", "Liq", "S_I"): 0, + ("R5", "Liq", "X_ch"): 0, + ("R5", "Liq", "X_pr"): 0, + ("R5", "Liq", "X_li"): 0, + ("R5", "Liq", "X_su"): 0, + ("R5", "Liq", "X_aa"): self.Y_aa, + ("R5", "Liq", "X_fa"): 0, + ("R5", "Liq", "X_c4"): 0, + ("R5", "Liq", "X_pro"): 0, + ("R5", "Liq", "X_ac"): 0, + ("R5", "Liq", "X_h2"): 0, + ("R5", "Liq", "X_I"): 0, + ("R5", "Liq", "X_PHA"): 0, + ("R5", "Liq", "X_PP"): 0, + ("R5", "Liq", "X_PAO"): 0, + ("R5", "Liq", "S_K"): 0, + ("R5", "Liq", "S_Mg"): 0, + # R6: Uptake of long chain fatty acids (LCFAs) + ("R6", "Liq", "H2O"): 0, + ("R6", "Liq", "S_su"): 0, + ("R6", "Liq", "S_aa"): 0, + ("R6", "Liq", "S_fa"): -1, + ("R6", "Liq", "S_va"): 0, + ("R6", "Liq", "S_bu"): 0, + ("R6", "Liq", "S_pro"): 0, + ("R6", "Liq", "S_ac"): (1 - self.Y_fa) * 0.7, + ("R6", "Liq", "S_h2"): (1 - self.Y_fa) * 0.3, + ("R6", "Liq", "S_ch4"): 0, + ("R6", "Liq", "S_IC"): ( + self.Ci["S_fa"] + - (1 - self.Y_fa) * 0.7 * self.Ci["S_ac"] + - self.Y_fa * self.Ci["X_fa"] + ) + * mw_c, + ("R6", "Liq", "S_IN"): (-self.Y_fa * self.Ni["X_fa"]) * mw_n, + ("R6", "Liq", "S_IP"): (-self.Y_fa * self.Pi["X_fa"]) * mw_p, + ("R6", "Liq", "S_I"): 0, + ("R6", "Liq", "X_ch"): 0, + ("R6", "Liq", "X_pr"): 0, + ("R6", "Liq", "X_li"): 0, + ("R6", "Liq", "X_su"): 0, + ("R6", "Liq", "X_aa"): 0, + ("R6", "Liq", "X_fa"): self.Y_fa, + ("R6", "Liq", "X_c4"): 0, + ("R6", "Liq", "X_pro"): 0, + ("R6", "Liq", "X_ac"): 0, + ("R6", "Liq", "X_h2"): 0, + ("R6", "Liq", "X_I"): 0, + ("R6", "Liq", "X_PHA"): 0, + ("R6", "Liq", "X_PP"): 0, + ("R6", "Liq", "X_PAO"): 0, + ("R6", "Liq", "S_K"): 0, + ("R6", "Liq", "S_Mg"): 0, + # R7: Uptake of valerate + ("R7", "Liq", "H2O"): 0, + ("R7", "Liq", "S_su"): 0, + ("R7", "Liq", "S_aa"): 0, + ("R7", "Liq", "S_fa"): 0, + ("R7", "Liq", "S_va"): -1, + ("R7", "Liq", "S_bu"): 0, + ("R7", "Liq", "S_pro"): (1 - self.Y_c4) * 0.54, + ("R7", "Liq", "S_ac"): (1 - self.Y_c4) * 0.31, + ("R7", "Liq", "S_h2"): (1 - self.Y_c4) * 0.15, + ("R7", "Liq", "S_ch4"): 0, + ("R7", "Liq", "S_IC"): ( + self.Ci["S_va"] + - (1 - self.Y_c4) * 0.54 * self.Ci["S_pro"] + - (1 - self.Y_c4) * 0.31 * self.Ci["S_ac"] + - self.Y_c4 * self.Ci["X_c4"] + ) + * mw_c, + ("R7", "Liq", "S_IN"): (-self.Y_c4 * self.Ni["X_c4"]) * mw_n, + ("R7", "Liq", "S_IP"): (-self.Y_c4 * self.Pi["X_c4"]) * mw_p, + ("R7", "Liq", "S_I"): 0, + ("R7", "Liq", "X_ch"): 0, + ("R7", "Liq", "X_pr"): 0, + ("R7", "Liq", "X_li"): 0, + ("R7", "Liq", "X_su"): 0, + ("R7", "Liq", "X_aa"): 0, + ("R7", "Liq", "X_fa"): 0, + ("R7", "Liq", "X_c4"): self.Y_c4, + ("R7", "Liq", "X_pro"): 0, + ("R7", "Liq", "X_ac"): 0, + ("R7", "Liq", "X_h2"): 0, + ("R7", "Liq", "X_I"): 0, + ("R7", "Liq", "X_PHA"): 0, + ("R7", "Liq", "X_PP"): 0, + ("R7", "Liq", "X_PAO"): 0, + ("R7", "Liq", "S_K"): 0, + ("R7", "Liq", "S_Mg"): 0, + # R8: Uptake of butyrate + ("R8", "Liq", "H2O"): 0, + ("R8", "Liq", "S_su"): 0, + ("R8", "Liq", "S_aa"): 0, + ("R8", "Liq", "S_fa"): 0, + ("R8", "Liq", "S_va"): 0, + ("R8", "Liq", "S_bu"): -1, + ("R8", "Liq", "S_pro"): 0, + ("R8", "Liq", "S_ac"): (1 - self.Y_c4) * 0.8, + ("R8", "Liq", "S_h2"): (1 - self.Y_c4) * 0.2, + ("R8", "Liq", "S_ch4"): 0, + ("R8", "Liq", "S_IC"): ( + self.Ci["S_bu"] + - (1 - self.Y_c4) * 0.8 * self.Ci["S_ac"] + - self.Y_c4 * self.Ci["X_c4"] + ) + * mw_c, + ("R8", "Liq", "S_IN"): (-self.Y_c4 * self.Ni["X_c4"]) * mw_n, + ("R8", "Liq", "S_IP"): (-self.Y_c4 * self.Pi["X_c4"]) * mw_p, + ("R8", "Liq", "S_I"): 0, + ("R8", "Liq", "X_ch"): 0, + ("R8", "Liq", "X_pr"): 0, + ("R8", "Liq", "X_li"): 0, + ("R8", "Liq", "X_su"): 0, + ("R8", "Liq", "X_aa"): 0, + ("R8", "Liq", "X_fa"): 0, + ("R8", "Liq", "X_c4"): self.Y_c4, + ("R8", "Liq", "X_pro"): 0, + ("R8", "Liq", "X_ac"): 0, + ("R8", "Liq", "X_h2"): 0, + ("R8", "Liq", "X_I"): 0, + ("R8", "Liq", "X_PHA"): 0, + ("R8", "Liq", "X_PP"): 0, + ("R8", "Liq", "X_PAO"): 0, + ("R8", "Liq", "S_K"): 0, + ("R8", "Liq", "S_Mg"): 0, + # R9: Uptake of propionate + ("R9", "Liq", "H2O"): 0, + ("R9", "Liq", "S_su"): 0, + ("R9", "Liq", "S_aa"): 0, + ("R9", "Liq", "S_fa"): 0, + ("R9", "Liq", "S_va"): 0, + ("R9", "Liq", "S_bu"): 0, + ("R9", "Liq", "S_pro"): -1, + ("R9", "Liq", "S_ac"): (1 - self.Y_pro) * 0.57, + ("R9", "Liq", "S_h2"): (1 - self.Y_pro) * 0.43, + ("R9", "Liq", "S_ch4"): 0, + ("R9", "Liq", "S_IC"): ( + self.Ci["S_pro"] + - (1 - self.Y_pro) * 0.57 * self.Ci["S_ac"] + - self.Y_pro * self.Ci["X_pro"] + ) + * mw_c, + ("R9", "Liq", "S_IN"): (-self.Y_pro * self.Ni["X_pro"]) * mw_n, + ("R9", "Liq", "S_IP"): (-self.Y_pro * self.Pi["X_pro"]) * mw_p, + ("R9", "Liq", "S_I"): 0, + ("R9", "Liq", "X_ch"): 0, + ("R9", "Liq", "X_pr"): 0, + ("R9", "Liq", "X_li"): 0, + ("R9", "Liq", "X_su"): 0, + ("R9", "Liq", "X_aa"): 0, + ("R9", "Liq", "X_fa"): 0, + ("R9", "Liq", "X_c4"): 0, + ("R9", "Liq", "X_pro"): self.Y_pro, + ("R9", "Liq", "X_ac"): 0, + ("R9", "Liq", "X_h2"): 0, + ("R9", "Liq", "X_I"): 0, + ("R9", "Liq", "X_PHA"): 0, + ("R9", "Liq", "X_PP"): 0, + ("R9", "Liq", "X_PAO"): 0, + ("R9", "Liq", "S_K"): 0, + ("R9", "Liq", "S_Mg"): 0, + # R10: Uptake of acetate + ("R10", "Liq", "H2O"): 0, + ("R10", "Liq", "S_su"): 0, + ("R10", "Liq", "S_aa"): 0, + ("R10", "Liq", "S_fa"): 0, + ("R10", "Liq", "S_va"): 0, + ("R10", "Liq", "S_bu"): 0, + ("R10", "Liq", "S_pro"): 0, + ("R10", "Liq", "S_ac"): -1, + ("R10", "Liq", "S_h2"): 0, + ("R10", "Liq", "S_ch4"): 1 - self.Y_ac, + ("R10", "Liq", "S_IC"): ( + self.Ci["S_ac"] + - (1 - self.Y_ac) * self.Ci["S_ch4"] + - self.Y_ac * self.Ci["X_ac"] + ) + * mw_c, + ("R10", "Liq", "S_IN"): (-self.Y_ac * self.Ni["X_ac"]) * mw_n, + ("R10", "Liq", "S_IP"): (-self.Y_ac * self.Pi["X_ac"]) * mw_p, + ("R10", "Liq", "S_I"): 0, + ("R10", "Liq", "X_ch"): 0, + ("R10", "Liq", "X_pr"): 0, + ("R10", "Liq", "X_li"): 0, + ("R10", "Liq", "X_su"): 0, + ("R10", "Liq", "X_aa"): 0, + ("R10", "Liq", "X_fa"): 0, + ("R10", "Liq", "X_c4"): 0, + ("R10", "Liq", "X_pro"): 0, + ("R10", "Liq", "X_ac"): self.Y_ac, + ("R10", "Liq", "X_h2"): 0, + ("R10", "Liq", "X_I"): 0, + ("R10", "Liq", "X_PHA"): 0, + ("R10", "Liq", "X_PP"): 0, + ("R10", "Liq", "X_PAO"): 0, + ("R10", "Liq", "S_K"): 0, + ("R10", "Liq", "S_Mg"): 0, + # R11: Uptake of hydrogen + ("R11", "Liq", "H2O"): 0, + ("R11", "Liq", "S_su"): 0, + ("R11", "Liq", "S_aa"): 0, + ("R11", "Liq", "S_fa"): 0, + ("R11", "Liq", "S_va"): 0, + ("R11", "Liq", "S_bu"): 0, + ("R11", "Liq", "S_pro"): 0, + ("R11", "Liq", "S_ac"): 0, + ("R11", "Liq", "S_h2"): -1, + ("R11", "Liq", "S_ch4"): 1 - self.Y_h2, + ("R11", "Liq", "S_IC"): ( + -(1 - self.Y_h2) * self.Ci["S_ch4"] - self.Y_h2 * self.Ci["X_h2"] + ) + * mw_c, + ("R11", "Liq", "S_IN"): (-self.Y_h2 * self.Ni["X_h2"]) * mw_n, + ("R11", "Liq", "S_IP"): (-self.Y_h2 * self.Pi["X_h2"]) * mw_p, + ("R11", "Liq", "S_I"): 0, + ("R11", "Liq", "X_ch"): 0, + ("R11", "Liq", "X_pr"): 0, + ("R11", "Liq", "X_li"): 0, + ("R11", "Liq", "X_su"): 0, + ("R11", "Liq", "X_aa"): 0, + ("R11", "Liq", "X_fa"): 0, + ("R11", "Liq", "X_c4"): 0, + ("R11", "Liq", "X_pro"): 0, + ("R11", "Liq", "X_ac"): 0, + ("R11", "Liq", "X_h2"): self.Y_h2, + ("R11", "Liq", "X_I"): 0, + ("R11", "Liq", "X_PHA"): 0, + ("R11", "Liq", "X_PP"): 0, + ("R11", "Liq", "X_PAO"): 0, + ("R11", "Liq", "S_K"): 0, + ("R11", "Liq", "S_Mg"): 0, + # R12: Decay of X_su + ("R12", "Liq", "H2O"): 0, + ("R12", "Liq", "S_su"): 0, + ("R12", "Liq", "S_aa"): 0, + ("R12", "Liq", "S_fa"): 0, + ("R12", "Liq", "S_va"): 0, + ("R12", "Liq", "S_bu"): 0, + ("R12", "Liq", "S_pro"): 0, + ("R12", "Liq", "S_ac"): 0, + ("R12", "Liq", "S_h2"): 0, + ("R12", "Liq", "S_ch4"): 0, + ("R12", "Liq", "S_IC"): ( + self.Ci["X_su"] + - self.f_ch_xb * self.Ci["X_ch"] + - self.f_pr_xb * self.Ci["X_pr"] + - self.f_li_xb * self.Ci["X_li"] + - self.f_xi_xb * self.Ci["X_I"] + ) + * mw_c, + ("R12", "Liq", "S_IN"): ( + self.Ni["X_su"] + - self.f_pr_xb * self.Ni["X_pr"] + - self.f_xi_xb * self.Ni["X_I"] + ) + * mw_n, + ("R12", "Liq", "S_IP"): ( + self.Pi["X_su"] + - self.f_li_xb * self.Pi["X_li"] + - self.f_xi_xb * self.Pi["X_I"] + ) + * mw_p, + ("R12", "Liq", "S_I"): self.f_si_xb, + ("R12", "Liq", "X_ch"): self.f_ch_xb, + ("R12", "Liq", "X_pr"): self.f_pr_xb, + ("R12", "Liq", "X_li"): self.f_li_xb, + ("R12", "Liq", "X_su"): -1, + ("R12", "Liq", "X_aa"): 0, + ("R12", "Liq", "X_fa"): 0, + ("R12", "Liq", "X_c4"): 0, + ("R12", "Liq", "X_pro"): 0, + ("R12", "Liq", "X_ac"): 0, + ("R12", "Liq", "X_h2"): 0, + ("R12", "Liq", "X_I"): self.f_xi_xb, + ("R12", "Liq", "X_PHA"): 0, + ("R12", "Liq", "X_PP"): 0, + ("R12", "Liq", "X_PAO"): 0, + ("R12", "Liq", "S_K"): 0, + ("R12", "Liq", "S_Mg"): 0, + # R13: Decay of X_aa + ("R13", "Liq", "H2O"): 0, + ("R13", "Liq", "S_su"): 0, + ("R13", "Liq", "S_aa"): 0, + ("R13", "Liq", "S_fa"): 0, + ("R13", "Liq", "S_va"): 0, + ("R13", "Liq", "S_bu"): 0, + ("R13", "Liq", "S_pro"): 0, + ("R13", "Liq", "S_ac"): 0, + ("R13", "Liq", "S_h2"): 0, + ("R13", "Liq", "S_ch4"): 0, + ("R13", "Liq", "S_IC"): ( + self.Ci["X_aa"] + - self.f_ch_xb * self.Ci["X_ch"] + - self.f_pr_xb * self.Ci["X_pr"] + - self.f_li_xb * self.Ci["X_li"] + - self.f_xi_xb * self.Ci["X_I"] + ) + * mw_c, + ("R13", "Liq", "S_IN"): ( + self.Ni["X_aa"] + - self.f_pr_xb * self.Ni["X_pr"] + - self.f_xi_xb * self.Ni["X_I"] + ) + * mw_n, + ("R13", "Liq", "S_IP"): ( + self.Pi["X_aa"] + - self.f_li_xb * self.Pi["X_li"] + - self.f_xi_xb * self.Pi["X_I"] + ) + * mw_p, + ("R13", "Liq", "S_I"): self.f_si_xb, + ("R13", "Liq", "X_ch"): self.f_ch_xb, + ("R13", "Liq", "X_pr"): self.f_pr_xb, + ("R13", "Liq", "X_li"): self.f_li_xb, + ("R13", "Liq", "X_su"): 0, + ("R13", "Liq", "X_aa"): -1, + ("R13", "Liq", "X_fa"): 0, + ("R13", "Liq", "X_c4"): 0, + ("R13", "Liq", "X_pro"): 0, + ("R13", "Liq", "X_ac"): 0, + ("R13", "Liq", "X_h2"): 0, + ("R13", "Liq", "X_I"): self.f_xi_xb, + ("R13", "Liq", "X_PHA"): 0, + ("R13", "Liq", "X_PP"): 0, + ("R13", "Liq", "X_PAO"): 0, + ("R13", "Liq", "S_K"): 0, + ("R13", "Liq", "S_Mg"): 0, + # R14: Decay of X_fa + ("R14", "Liq", "H2O"): 0, + ("R14", "Liq", "S_su"): 0, + ("R14", "Liq", "S_aa"): 0, + ("R14", "Liq", "S_fa"): 0, + ("R14", "Liq", "S_va"): 0, + ("R14", "Liq", "S_bu"): 0, + ("R14", "Liq", "S_pro"): 0, + ("R14", "Liq", "S_ac"): 0, + ("R14", "Liq", "S_h2"): 0, + ("R14", "Liq", "S_ch4"): 0, + ("R14", "Liq", "S_IC"): ( + self.Ci["X_fa"] + - self.f_ch_xb * self.Ci["X_ch"] + - self.f_pr_xb * self.Ci["X_pr"] + - self.f_li_xb * self.Ci["X_li"] + - self.f_xi_xb * self.Ci["X_I"] + ) + * mw_c, + ("R14", "Liq", "S_IN"): ( + self.Ni["X_fa"] + - self.f_pr_xb * self.Ni["X_pr"] + - self.f_xi_xb * self.Ni["X_I"] + ) + * mw_n, + ("R14", "Liq", "S_IP"): ( + self.Pi["X_fa"] + - self.f_li_xb * self.Pi["X_li"] + - self.f_xi_xb * self.Pi["X_I"] + ) + * mw_p, + ("R14", "Liq", "S_I"): self.f_si_xb, + ("R14", "Liq", "X_ch"): self.f_ch_xb, + ("R14", "Liq", "X_pr"): self.f_pr_xb, + ("R14", "Liq", "X_li"): self.f_li_xb, + ("R14", "Liq", "X_su"): 0, + ("R14", "Liq", "X_aa"): 0, + ("R14", "Liq", "X_fa"): -1, + ("R14", "Liq", "X_c4"): 0, + ("R14", "Liq", "X_pro"): 0, + ("R14", "Liq", "X_ac"): 0, + ("R14", "Liq", "X_h2"): 0, + ("R14", "Liq", "X_I"): self.f_xi_xb, + ("R14", "Liq", "X_PHA"): 0, + ("R14", "Liq", "X_PP"): 0, + ("R14", "Liq", "X_PAO"): 0, + ("R14", "Liq", "S_K"): 0, + ("R14", "Liq", "S_Mg"): 0, + # R15: Decay of X_c4 + ("R15", "Liq", "H2O"): 0, + ("R15", "Liq", "S_su"): 0, + ("R15", "Liq", "S_aa"): 0, + ("R15", "Liq", "S_fa"): 0, + ("R15", "Liq", "S_va"): 0, + ("R15", "Liq", "S_bu"): 0, + ("R15", "Liq", "S_pro"): 0, + ("R15", "Liq", "S_ac"): 0, + ("R15", "Liq", "S_h2"): 0, + ("R15", "Liq", "S_ch4"): 0, + ("R15", "Liq", "S_IC"): ( + self.Ci["X_c4"] + - self.f_ch_xb * self.Ci["X_ch"] + - self.f_pr_xb * self.Ci["X_pr"] + - self.f_li_xb * self.Ci["X_li"] + - self.f_xi_xb * self.Ci["X_I"] + ) + * mw_c, + ("R15", "Liq", "S_IN"): ( + self.Ni["X_c4"] + - self.f_pr_xb * self.Ni["X_pr"] + - self.f_xi_xb * self.Ni["X_I"] + ) + * mw_n, + ("R15", "Liq", "S_IP"): ( + self.Pi["X_c4"] + - self.f_li_xb * self.Pi["X_li"] + - self.f_xi_xb * self.Pi["X_I"] + ) + * mw_p, + ("R15", "Liq", "S_I"): self.f_si_xb, + ("R15", "Liq", "X_ch"): self.f_ch_xb, + ("R15", "Liq", "X_pr"): self.f_pr_xb, + ("R15", "Liq", "X_li"): self.f_li_xb, + ("R15", "Liq", "X_su"): 0, + ("R15", "Liq", "X_aa"): 0, + ("R15", "Liq", "X_fa"): 0, + ("R15", "Liq", "X_c4"): -1, + ("R15", "Liq", "X_pro"): 0, + ("R15", "Liq", "X_ac"): 0, + ("R15", "Liq", "X_h2"): 0, + ("R15", "Liq", "X_I"): self.f_xi_xb, + ("R15", "Liq", "X_PHA"): 0, + ("R15", "Liq", "X_PP"): 0, + ("R15", "Liq", "X_PAO"): 0, + ("R15", "Liq", "S_K"): 0, + ("R15", "Liq", "S_Mg"): 0, + # R16: Decay of X_pro + ("R16", "Liq", "H2O"): 0, + ("R16", "Liq", "S_su"): 0, + ("R16", "Liq", "S_aa"): 0, + ("R16", "Liq", "S_fa"): 0, + ("R16", "Liq", "S_va"): 0, + ("R16", "Liq", "S_bu"): 0, + ("R16", "Liq", "S_pro"): 0, + ("R16", "Liq", "S_ac"): 0, + ("R16", "Liq", "S_h2"): 0, + ("R16", "Liq", "S_ch4"): 0, + ("R16", "Liq", "S_IC"): ( + self.Ci["X_pro"] + - self.f_ch_xb * self.Ci["X_ch"] + - self.f_pr_xb * self.Ci["X_pr"] + - self.f_li_xb * self.Ci["X_li"] + - self.f_xi_xb * self.Ci["X_I"] + ) + * mw_c, + ("R16", "Liq", "S_IN"): ( + self.Ni["X_pro"] + - self.f_pr_xb * self.Ni["X_pr"] + - self.f_xi_xb * self.Ni["X_I"] + ) + * mw_n, + ("R16", "Liq", "S_IP"): ( + self.Pi["X_pro"] + - self.f_li_xb * self.Pi["X_li"] + - self.f_xi_xb * self.Pi["X_I"] + ) + * mw_p, + ("R16", "Liq", "S_I"): self.f_si_xb, + ("R16", "Liq", "X_ch"): self.f_ch_xb, + ("R16", "Liq", "X_pr"): self.f_pr_xb, + ("R16", "Liq", "X_li"): self.f_li_xb, + ("R16", "Liq", "X_su"): 0, + ("R16", "Liq", "X_aa"): 0, + ("R16", "Liq", "X_fa"): 0, + ("R16", "Liq", "X_c4"): 0, + ("R16", "Liq", "X_pro"): -1, + ("R16", "Liq", "X_ac"): 0, + ("R16", "Liq", "X_h2"): 0, + ("R16", "Liq", "X_I"): self.f_xi_xb, + ("R16", "Liq", "X_PHA"): 0, + ("R16", "Liq", "X_PP"): 0, + ("R16", "Liq", "X_PAO"): 0, + ("R16", "Liq", "S_K"): 0, + ("R16", "Liq", "S_Mg"): 0, + # R17: Decay of X_ac + ("R17", "Liq", "H2O"): 0, + ("R17", "Liq", "S_su"): 0, + ("R17", "Liq", "S_aa"): 0, + ("R17", "Liq", "S_fa"): 0, + ("R17", "Liq", "S_va"): 0, + ("R17", "Liq", "S_bu"): 0, + ("R17", "Liq", "S_pro"): 0, + ("R17", "Liq", "S_ac"): 0, + ("R17", "Liq", "S_h2"): 0, + ("R17", "Liq", "S_ch4"): 0, + ("R17", "Liq", "S_IC"): ( + self.Ci["X_ac"] + - self.f_ch_xb * self.Ci["X_ch"] + - self.f_pr_xb * self.Ci["X_pr"] + - self.f_li_xb * self.Ci["X_li"] + - self.f_xi_xb * self.Ci["X_I"] + ) + * mw_c, + ("R17", "Liq", "S_IN"): ( + self.Ni["X_ac"] + - self.f_pr_xb * self.Ni["X_pr"] + - self.f_xi_xb * self.Ni["X_I"] + ) + * mw_n, + ("R17", "Liq", "S_IP"): ( + self.Pi["X_ac"] + - self.f_li_xb * self.Pi["X_li"] + - self.f_xi_xb * self.Pi["X_I"] + ) + * mw_p, + ("R17", "Liq", "S_I"): self.f_si_xb, + ("R17", "Liq", "X_ch"): self.f_ch_xb, + ("R17", "Liq", "X_pr"): self.f_pr_xb, + ("R17", "Liq", "X_li"): self.f_li_xb, + ("R17", "Liq", "X_su"): 0, + ("R17", "Liq", "X_aa"): 0, + ("R17", "Liq", "X_fa"): 0, + ("R17", "Liq", "X_c4"): 0, + ("R17", "Liq", "X_pro"): 0, + ("R17", "Liq", "X_ac"): -1, + ("R17", "Liq", "X_h2"): 0, + ("R17", "Liq", "X_I"): self.f_xi_xb, + ("R17", "Liq", "X_PHA"): 0, + ("R17", "Liq", "X_PP"): 0, + ("R17", "Liq", "X_PAO"): 0, + ("R17", "Liq", "S_K"): 0, + ("R17", "Liq", "S_Mg"): 0, + # R18: Decay of X_h2 + ("R18", "Liq", "H2O"): 0, + ("R18", "Liq", "S_su"): 0, + ("R18", "Liq", "S_aa"): 0, + ("R18", "Liq", "S_fa"): 0, + ("R18", "Liq", "S_va"): 0, + ("R18", "Liq", "S_bu"): 0, + ("R18", "Liq", "S_pro"): 0, + ("R18", "Liq", "S_ac"): 0, + ("R18", "Liq", "S_h2"): 0, + ("R18", "Liq", "S_ch4"): 0, + ("R18", "Liq", "S_IC"): ( + self.Ci["X_h2"] + - self.f_ch_xb * self.Ci["X_ch"] + - self.f_pr_xb * self.Ci["X_pr"] + - self.f_li_xb * self.Ci["X_li"] + - self.f_xi_xb * self.Ci["X_I"] + ) + * mw_c, + ("R18", "Liq", "S_IN"): ( + self.Ni["X_h2"] + - self.f_pr_xb * self.Ni["X_pr"] + - self.f_xi_xb * self.Ni["X_I"] + ) + * mw_n, + ("R18", "Liq", "S_IP"): ( + self.Pi["X_h2"] + - self.f_li_xb * self.Pi["X_li"] + - self.f_xi_xb * self.Pi["X_I"] + ) + * mw_p, + ("R18", "Liq", "S_I"): self.f_si_xb, + ("R18", "Liq", "X_ch"): self.f_ch_xb, + ("R18", "Liq", "X_pr"): self.f_pr_xb, + ("R18", "Liq", "X_li"): self.f_li_xb, + ("R18", "Liq", "X_su"): 0, + ("R18", "Liq", "X_aa"): 0, + ("R18", "Liq", "X_fa"): 0, + ("R18", "Liq", "X_c4"): 0, + ("R18", "Liq", "X_pro"): 0, + ("R18", "Liq", "X_ac"): 0, + ("R18", "Liq", "X_h2"): -1, + ("R18", "Liq", "X_I"): self.f_xi_xb, + ("R18", "Liq", "X_PHA"): 0, + ("R18", "Liq", "X_PP"): 0, + ("R18", "Liq", "X_PAO"): 0, + ("R18", "Liq", "S_K"): 0, + ("R18", "Liq", "S_Mg"): 0, + # R19: Storage of S_va in X_PHA + ("R19", "Liq", "H2O"): 0, + ("R19", "Liq", "S_su"): 0, + ("R19", "Liq", "S_aa"): 0, + ("R19", "Liq", "S_fa"): 0, + ("R19", "Liq", "S_va"): -1, + ("R19", "Liq", "S_bu"): 0, + ("R19", "Liq", "S_pro"): 0, + ("R19", "Liq", "S_ac"): 0, + ("R19", "Liq", "S_h2"): 0, + ("R19", "Liq", "S_ch4"): 0, + ("R19", "Liq", "S_IC"): (self.Ci["S_va"] - self.Ci["X_PHA"]) * mw_c, + ("R19", "Liq", "S_IN"): 0, + ("R19", "Liq", "S_IP"): (self.Y_PO4 * self.Pi["X_PP"]) * mw_p, + ("R19", "Liq", "S_I"): 0, + ("R19", "Liq", "X_ch"): 0, + ("R19", "Liq", "X_pr"): 0, + ("R19", "Liq", "X_li"): 0, + ("R19", "Liq", "X_su"): 0, + ("R19", "Liq", "X_aa"): 0, + ("R19", "Liq", "X_fa"): 0, + ("R19", "Liq", "X_c4"): 0, + ("R19", "Liq", "X_pro"): 0, + ("R19", "Liq", "X_ac"): 0, + ("R19", "Liq", "X_h2"): 0, + ("R19", "Liq", "X_I"): 0, + ("R19", "Liq", "X_PHA"): 1, + ("R19", "Liq", "X_PP"): -self.Y_PO4, + ("R19", "Liq", "X_PAO"): 0, + ("R19", "Liq", "S_K"): self.Y_PO4 * self.K_XPP, + ("R19", "Liq", "S_Mg"): self.Y_PO4 * self.Mg_XPP, + # R20: Storage of S_bu in X_PHA + ("R20", "Liq", "H2O"): 0, + ("R20", "Liq", "S_su"): 0, + ("R20", "Liq", "S_aa"): 0, + ("R20", "Liq", "S_fa"): 0, + ("R20", "Liq", "S_va"): 0, + ("R20", "Liq", "S_bu"): -1, + ("R20", "Liq", "S_pro"): 0, + ("R20", "Liq", "S_ac"): 0, + ("R20", "Liq", "S_h2"): 0, + ("R20", "Liq", "S_ch4"): 0, + ("R20", "Liq", "S_IC"): (self.Ci["S_bu"] - self.Ci["X_PHA"]) * mw_c, + ("R20", "Liq", "S_IN"): 0, + ("R20", "Liq", "S_IP"): (self.Y_PO4 * self.Pi["X_PP"]) * mw_p, + ("R20", "Liq", "S_I"): 0, + ("R20", "Liq", "X_ch"): 0, + ("R20", "Liq", "X_pr"): 0, + ("R20", "Liq", "X_li"): 0, + ("R20", "Liq", "X_su"): 0, + ("R20", "Liq", "X_aa"): 0, + ("R20", "Liq", "X_fa"): 0, + ("R20", "Liq", "X_c4"): 0, + ("R20", "Liq", "X_pro"): 0, + ("R20", "Liq", "X_ac"): 0, + ("R20", "Liq", "X_h2"): 0, + ("R20", "Liq", "X_I"): 0, + ("R20", "Liq", "X_PHA"): 1, + ("R20", "Liq", "X_PP"): -self.Y_PO4, + ("R20", "Liq", "X_PAO"): 0, + ("R20", "Liq", "S_K"): self.Y_PO4 * self.K_XPP, + ("R20", "Liq", "S_Mg"): self.Y_PO4 * self.Mg_XPP, + # R21: Storage of S_pro in X_PHA + ("R21", "Liq", "H2O"): 0, + ("R21", "Liq", "S_su"): 0, + ("R21", "Liq", "S_aa"): 0, + ("R21", "Liq", "S_fa"): 0, + ("R21", "Liq", "S_va"): 0, + ("R21", "Liq", "S_bu"): 0, + ("R21", "Liq", "S_pro"): -1, + ("R21", "Liq", "S_ac"): 0, + ("R21", "Liq", "S_h2"): 0, + ("R21", "Liq", "S_ch4"): 0, + ("R21", "Liq", "S_IC"): (self.Ci["S_pro"] - self.Ci["X_PHA"]) * mw_c, + ("R21", "Liq", "S_IN"): 0, + ("R21", "Liq", "S_IP"): (self.Y_PO4 * self.Pi["X_PP"]) * mw_p, + ("R21", "Liq", "S_I"): 0, + ("R21", "Liq", "X_ch"): 0, + ("R21", "Liq", "X_pr"): 0, + ("R21", "Liq", "X_li"): 0, + ("R21", "Liq", "X_su"): 0, + ("R21", "Liq", "X_aa"): 0, + ("R21", "Liq", "X_fa"): 0, + ("R21", "Liq", "X_c4"): 0, + ("R21", "Liq", "X_pro"): 0, + ("R21", "Liq", "X_ac"): 0, + ("R21", "Liq", "X_h2"): 0, + ("R21", "Liq", "X_I"): 0, + ("R21", "Liq", "X_PHA"): 1, + ("R21", "Liq", "X_PP"): -self.Y_PO4, + ("R21", "Liq", "X_PAO"): 0, + ("R21", "Liq", "S_K"): self.Y_PO4 * self.K_XPP, + ("R21", "Liq", "S_Mg"): self.Y_PO4 * self.Mg_XPP, + # R22: Storage of S_ac in X_PHA + ("R22", "Liq", "H2O"): 0, + ("R22", "Liq", "S_su"): 0, + ("R22", "Liq", "S_aa"): 0, + ("R22", "Liq", "S_fa"): 0, + ("R22", "Liq", "S_va"): 0, + ("R22", "Liq", "S_bu"): 0, + ("R22", "Liq", "S_pro"): 0, + ("R22", "Liq", "S_ac"): -1, + ("R22", "Liq", "S_h2"): 0, + ("R22", "Liq", "S_ch4"): 0, + ("R22", "Liq", "S_IC"): (self.Ci["S_ac"] - self.Ci["X_PHA"]) * mw_c, + ("R22", "Liq", "S_IN"): 0, + ("R22", "Liq", "S_IP"): (self.Y_PO4 * self.Pi["X_PP"]) * mw_p, + ("R22", "Liq", "S_I"): 0, + ("R22", "Liq", "X_ch"): 0, + ("R22", "Liq", "X_pr"): 0, + ("R22", "Liq", "X_li"): 0, + ("R22", "Liq", "X_su"): 0, + ("R22", "Liq", "X_aa"): 0, + ("R22", "Liq", "X_fa"): 0, + ("R22", "Liq", "X_c4"): 0, + ("R22", "Liq", "X_pro"): 0, + ("R22", "Liq", "X_ac"): 0, + ("R22", "Liq", "X_h2"): 0, + ("R22", "Liq", "X_I"): 0, + ("R22", "Liq", "X_PHA"): 1, + ("R22", "Liq", "X_PP"): -self.Y_PO4, + ("R22", "Liq", "X_PAO"): 0, + ("R22", "Liq", "S_K"): self.Y_PO4 * self.K_XPP, + ("R22", "Liq", "S_Mg"): self.Y_PO4 * self.Mg_XPP, + # R23: Lysis of X_PAO + ("R23", "Liq", "H2O"): 0, + ("R23", "Liq", "S_su"): 0, + ("R23", "Liq", "S_aa"): 0, + ("R23", "Liq", "S_fa"): 0, + ("R23", "Liq", "S_va"): 0, + ("R23", "Liq", "S_bu"): 0, + ("R23", "Liq", "S_pro"): 0, + ("R23", "Liq", "S_ac"): 0, + ("R23", "Liq", "S_h2"): 0, + ("R23", "Liq", "S_ch4"): 0, + ("R23", "Liq", "S_IC"): ( + self.Ci["X_PAO"] + - self.f_ch_xb * self.Ci["X_ch"] + - self.f_pr_xb * self.Ci["X_pr"] + - self.f_li_xb * self.Ci["X_li"] + - self.f_xi_xb * self.Ci["X_I"] + ) + * mw_c, + ("R23", "Liq", "S_IN"): ( + self.Ni["X_PAO"] + - self.f_pr_xb * self.Ni["X_pr"] + - self.f_xi_xb * self.Ni["X_I"] + ) + * mw_n, + ("R23", "Liq", "S_IP"): ( + self.Pi["X_PAO"] + - self.f_li_xb * self.Pi["X_li"] + - self.f_xi_xb * self.Pi["X_I"] + ) + * mw_p, + ("R23", "Liq", "S_I"): self.f_si_xb, + ("R23", "Liq", "X_ch"): self.f_ch_xb, + ("R23", "Liq", "X_pr"): self.f_pr_xb, + ("R23", "Liq", "X_li"): self.f_li_xb, + ("R23", "Liq", "X_su"): 0, + ("R23", "Liq", "X_aa"): 0, + ("R23", "Liq", "X_fa"): 0, + ("R23", "Liq", "X_c4"): 0, + ("R23", "Liq", "X_pro"): 0, + ("R23", "Liq", "X_ac"): 0, + ("R23", "Liq", "X_h2"): 0, + ("R23", "Liq", "X_I"): self.f_xi_xb, + ("R23", "Liq", "X_PHA"): 0, + ("R23", "Liq", "X_PP"): 0, + ("R23", "Liq", "X_PAO"): -1, + ("R23", "Liq", "S_K"): 0, + ("R23", "Liq", "S_Mg"): 0, + # R24: Lysis of X_PP + ("R24", "Liq", "H2O"): 0, + ("R24", "Liq", "S_su"): 0, + ("R24", "Liq", "S_aa"): 0, + ("R24", "Liq", "S_fa"): 0, + ("R24", "Liq", "S_va"): 0, + ("R24", "Liq", "S_bu"): 0, + ("R24", "Liq", "S_pro"): 0, + ("R24", "Liq", "S_ac"): 0, + ("R24", "Liq", "S_h2"): 0, + ("R24", "Liq", "S_ch4"): 0, + ("R24", "Liq", "S_IC"): 0, + ("R24", "Liq", "S_IN"): 0, + ("R24", "Liq", "S_IP"): self.Pi["X_PP"] * mw_p, + ("R24", "Liq", "S_I"): 0, + ("R24", "Liq", "X_ch"): 0, + ("R24", "Liq", "X_pr"): 0, + ("R24", "Liq", "X_li"): 0, + ("R24", "Liq", "X_su"): 0, + ("R24", "Liq", "X_aa"): 0, + ("R24", "Liq", "X_fa"): 0, + ("R24", "Liq", "X_c4"): 0, + ("R24", "Liq", "X_pro"): 0, + ("R24", "Liq", "X_ac"): 0, + ("R24", "Liq", "X_h2"): 0, + ("R24", "Liq", "X_I"): 0, + ("R24", "Liq", "X_PHA"): 0, + ("R24", "Liq", "X_PP"): -1, + ("R24", "Liq", "X_PAO"): 0, + ("R24", "Liq", "S_K"): self.K_XPP, + ("R24", "Liq", "S_Mg"): self.Mg_XPP, + # R25: Lysis of X_PHA + ("R25", "Liq", "H2O"): 0, + ("R25", "Liq", "S_su"): 0, + ("R25", "Liq", "S_aa"): 0, + ("R25", "Liq", "S_fa"): 0, + ("R25", "Liq", "S_va"): self.f_va_PHA, + ("R25", "Liq", "S_bu"): self.f_bu_PHA, + ("R25", "Liq", "S_pro"): self.f_pro_PHA, + ("R25", "Liq", "S_ac"): self.f_ac_PHA, + ("R25", "Liq", "S_h2"): 0, + ("R25", "Liq", "S_ch4"): 0, + ("R25", "Liq", "S_IC"): ( + self.Ci["X_PHA"] + - self.f_va_PHA * self.Ci["S_va"] + - self.f_bu_PHA * self.Ci["S_bu"] + - self.f_pro_PHA * self.Ci["S_pro"] + - self.f_ac_PHA * self.Ci["S_ac"] + ) + * mw_c, + ("R25", "Liq", "S_IN"): 0, + ("R25", "Liq", "S_IP"): 0, + ("R25", "Liq", "S_I"): 0, + ("R25", "Liq", "X_ch"): 0, + ("R25", "Liq", "X_pr"): 0, + ("R25", "Liq", "X_li"): 0, + ("R25", "Liq", "X_su"): 0, + ("R25", "Liq", "X_aa"): 0, + ("R25", "Liq", "X_fa"): 0, + ("R25", "Liq", "X_c4"): 0, + ("R25", "Liq", "X_pro"): 0, + ("R25", "Liq", "X_ac"): 0, + ("R25", "Liq", "X_h2"): 0, + ("R25", "Liq", "X_I"): 0, + ("R25", "Liq", "X_PHA"): -1, + ("R25", "Liq", "X_PP"): 0, + ("R25", "Liq", "X_PAO"): 0, + ("R25", "Liq", "S_K"): 0, + ("R25", "Liq", "S_Mg"): 0, + } + + for R in self.rate_reaction_idx: + self.rate_reaction_stoichiometry[R, "Liq", "S_cat"] = 0 + self.rate_reaction_stoichiometry[R, "Liq", "S_an"] = 0 + + # Fix all the variables we just created + for v in self.component_objects(pyo.Var, descend_into=False): + v.fix() + + @classmethod + def define_metadata(cls, obj): + obj.add_properties( + { + "reaction_rate": {"method": "_rxn_rate"}, + } + ) + obj.define_custom_properties( + { + "conc_mol_co2": {"method": "_rxn_rate"}, + "I": {"method": "_I"}, + } + ) + obj.add_default_units( + { + "time": pyo.units.s, + "length": pyo.units.m, + "mass": pyo.units.kg, + "amount": pyo.units.kmol, + "temperature": pyo.units.K, + } + ) + + +class _ModifiedADM1ReactionBlock(ReactionBlockBase): + """ + This Class contains methods which should be applied to Reaction Blocks as a + whole, rather than individual elements of indexed Reaction Blocks. + """ + + def initialize(self, outlvl=idaeslog.NOTSET, **kwargs): + """ + Initialization routine for reaction package. + + Keyword Arguments: + outlvl : sets output level of initialization routine + + Returns: + None + """ + init_log = idaeslog.getInitLogger(self.name, outlvl, tag="properties") + init_log.info("Initialization Complete.") + + +@declare_process_block_class( + "ModifiedADM1ReactionBlock", block_class=_ModifiedADM1ReactionBlock +) +class ModifiedADM1ReactionBlockData(ReactionBlockDataBase): + """ + ReactionBlock for ADM1. + """ + + def build(self): + """ + Callable method for Block construction + """ + super().build() + + # Create references to state vars + # Concentration + add_object_reference(self, "conc_mass_comp_ref", self.state_ref.conc_mass_comp) + add_object_reference(self, "temperature", self.state_ref.temperature) + + # Initial values of rates of reaction derived from Flores-Alsina 2016 GitHub + self.rates = { + "R1": 1.651e-04, + "R2": 1.723e-04, + "R3": 2.290e-04, + "R4": 5.312e-06, + "R5": 5.176e-06, + "R6": 6.436e-06, + "R7": 1.074e-06, + "R8": 1.790e-06, + "R9": 2.355e-06, + "R10": 5.531e-06, + "R11": 4.472e-06, + "R12": 1.294e-07, + "R13": 1.009e-07, + "R14": 9.406e-08, + "R15": 4.185e-08, + "R16": 2.294e-08, + "R17": 1.259e-07, + "R18": 6.535e-08, + "R19": 1.507e-06, + "R20": 2.078e-06, + "R21": 2.630e-06, + "R22": 1.195e-05, + "R23": 1.901e-06, + "R24": 1.481e-09, + "R25": 1.425e-06, + } + + # Rate of reaction method + def _rxn_rate(self): + self.reaction_rate = pyo.Var( + self.params.rate_reaction_idx, + initialize=self.rates, + domain=pyo.NonNegativeReals, + doc="Rate of reaction", + units=pyo.units.kg / pyo.units.m**3 / pyo.units.s, + ) + self.KW = pyo.Var( + initialize=2.08e-14, + units=(pyo.units.kmol / pyo.units.m**3) ** 2, + domain=pyo.PositiveReals, + doc="Water dissociation constant", + ) + self.K_a_co2 = pyo.Var( + initialize=4.94e-7, + units=pyo.units.kmol / pyo.units.m**3, + domain=pyo.PositiveReals, + doc="Carbon dioxide acid-base equilibrium constant", + ) + self.K_a_IN = pyo.Var( + initialize=1.11e-9, + units=pyo.units.kmol / pyo.units.m**3, + domain=pyo.PositiveReals, + doc="Inorganic nitrogen acid-base equilibrium constant", + ) + self.conc_mass_va = pyo.Var( + initialize=0.01159624, + domain=pyo.NonNegativeReals, + doc="molar concentration of va-", + units=pyo.units.kg / pyo.units.m**3, + ) + self.conc_mass_bu = pyo.Var( + initialize=0.0132208, + domain=pyo.NonNegativeReals, + doc="molar concentration of bu-", + units=pyo.units.kg / pyo.units.m**3, + ) + self.conc_mass_pro = pyo.Var( + initialize=0.015742, + domain=pyo.NonNegativeReals, + doc="molar concentration of pro-", + units=pyo.units.kg / pyo.units.m**3, + ) + self.conc_mass_ac = pyo.Var( + initialize=0.1972, + domain=pyo.NonNegativeReals, + doc="molar concentration of ac-", + units=pyo.units.kg / pyo.units.m**3, + ) + self.conc_mol_hco3 = pyo.Var( + initialize=0.142777, + domain=pyo.NonNegativeReals, + doc="molar concentration of hco3", + units=pyo.units.kmol / pyo.units.m**3, + ) + self.conc_mol_nh3 = pyo.Var( + initialize=0.004, + domain=pyo.NonNegativeReals, + doc="molar concentration of nh3", + units=pyo.units.kmol / pyo.units.m**3, + ) + self.conc_mol_co2 = pyo.Var( + initialize=0.0099, + domain=pyo.NonNegativeReals, + doc="molar concentration of co2", + units=pyo.units.kmol / pyo.units.m**3, + ) + self.conc_mol_nh4 = pyo.Var( + initialize=0.1261, + domain=pyo.NonNegativeReals, + doc="molar concentration of nh4", + units=pyo.units.kmol / pyo.units.m**3, + ) + self.conc_mol_Mg = pyo.Var( + initialize=4.5822e-05, + domain=pyo.NonNegativeReals, + doc="molar concentration of Mg+2", + units=pyo.units.kmol / pyo.units.m**3, + ) + self.conc_mol_K = pyo.Var( + initialize=0.010934, + domain=pyo.NonNegativeReals, + doc="molar concentration of K+", + units=pyo.units.kmol / pyo.units.m**3, + ) + self.S_H = pyo.Var( + initialize=3.4e-8, + domain=pyo.NonNegativeReals, + doc="molar concentration of H", + units=pyo.units.kmol / pyo.units.m**3, + ) + self.S_OH = pyo.Var( + initialize=3.4e-8, + domain=pyo.NonNegativeReals, + doc="molar concentration of OH", + units=pyo.units.kmol / pyo.units.m**3, + ) + + def Dissociation_rule(self, t): + return ( + self.KW + == ( + 1e-14 + * pyo.exp( + 55900 + / pyo.units.mole + * pyo.units.joule + / (Constants.gas_constant) + * ((1 / self.params.temperature_ref) - (1 / self.temperature)) + ) + ) + * pyo.units.kilomole**2 + / pyo.units.meter**6 + ) + + self.Dissociation = pyo.Constraint( + rule=Dissociation_rule, + doc="Dissociation constant constraint", + ) + + def CO2_acid_base_equilibrium_rule(self, t): + return ( + self.K_a_co2 + == ( + 4.46684e-07 + * pyo.exp( + 7646 + / pyo.units.mole + * pyo.units.joule + / (Constants.gas_constant) + * ((1 / self.params.temperature_ref) - (1 / self.temperature)) + ) + ) + * pyo.units.kilomole + / pyo.units.meter**3 + ) + + self.CO2_acid_base_equilibrium = pyo.Constraint( + rule=CO2_acid_base_equilibrium_rule, + doc="Carbon dioxide acid-base equilibrium constraint", + ) + + def IN_acid_base_equilibrium_rule(self, t): + return ( + self.K_a_IN + == ( + 5.62341e-10 + * pyo.exp( + 51965 + / pyo.units.mole + * pyo.units.joule + / (Constants.gas_constant) + * ((1 / self.params.temperature_ref) - (1 / self.temperature)) + ) + ) + * pyo.units.kilomole + / pyo.units.meter**3 + ) + + self.IN_acid_base_equilibrium = pyo.Constraint( + rule=IN_acid_base_equilibrium_rule, + doc="Nitrogen acid-base equilibrium constraint", + ) + + mw_n = 14 * pyo.units.kg / pyo.units.kmol + mw_p = 31 * pyo.units.kg / pyo.units.kmol + + def concentration_of_va_rule(self): + return self.conc_mass_va == self.params.K_a_va * self.conc_mass_comp_ref[ + "S_va" + ] / (self.params.K_a_va + self.S_H) + + self.concentration_of_va = pyo.Constraint( + rule=concentration_of_va_rule, + doc="constraint concentration of va-", + ) + + def concentration_of_bu_rule(self): + return self.conc_mass_bu == self.params.K_a_bu * self.conc_mass_comp_ref[ + "S_bu" + ] / (self.params.K_a_bu + self.S_H) + + self.concentration_of_bu = pyo.Constraint( + rule=concentration_of_bu_rule, + doc="constraint concentration of bu-", + ) + + def concentration_of_pro_rule(self): + return self.conc_mass_pro == self.params.K_a_pro * self.conc_mass_comp_ref[ + "S_pro" + ] / (self.params.K_a_pro + self.S_H) + + self.concentration_of_pro = pyo.Constraint( + rule=concentration_of_pro_rule, + doc="constraint concentration of pro-", + ) + + def concentration_of_ac_rule(self): + return self.conc_mass_ac == self.params.K_a_ac * self.conc_mass_comp_ref[ + "S_ac" + ] / (self.params.K_a_ac + self.S_H) + + self.concentration_of_ac = pyo.Constraint( + rule=concentration_of_ac_rule, + doc="constraint concentration of ac-", + ) + + def concentration_of_hco3_rule(self): + return self.conc_mol_hco3 == self.K_a_co2 * ( + self.conc_mass_comp_ref["S_IC"] / (12 * pyo.units.kg / pyo.units.kmol) + ) / (self.K_a_co2 + self.S_H) + + self.concentration_of_hco3 = pyo.Constraint( + rule=concentration_of_hco3_rule, + doc="constraint concentration of hco3", + ) + + def concentration_of_nh3_rule(self): + return self.conc_mol_nh3 == self.K_a_IN * ( + self.conc_mass_comp_ref["S_IN"] / (14 * pyo.units.kg / pyo.units.kmol) + ) / (self.K_a_IN + self.S_H) + + self.concentration_of_nh3 = pyo.Constraint( + rule=concentration_of_nh3_rule, + doc="constraint concentration of nh3", + ) + + # TO DO: use correct conversion number + def concentration_of_co2_rule(self): + return ( + self.conc_mol_co2 + == self.conc_mass_comp_ref["S_IC"] + / (12 * pyo.units.kg / pyo.units.kmol) + - self.conc_mol_hco3 + ) + + self.concentration_of_co2 = pyo.Constraint( + rule=concentration_of_co2_rule, + doc="constraint concentration of co2", + ) + + def concentration_of_nh4_rule(self): + return ( + self.conc_mol_nh4 + == self.conc_mass_comp_ref["S_IN"] + / (14 * pyo.units.kg / pyo.units.kmol) + - self.conc_mol_nh3 + ) + + self.concentration_of_nh4 = pyo.Constraint( + rule=concentration_of_nh4_rule, + doc="constraint concentration of nh4", + ) + + def concentration_of_Mg_rule(self): + return self.conc_mol_Mg == self.conc_mass_comp_ref["X_PP"] / ( + 300.41 * pyo.units.kg / pyo.units.kmol + ) + + self.concentration_of_Mg = pyo.Constraint( + rule=concentration_of_Mg_rule, + doc="constraint concentration of Mg", + ) + + def concentration_of_K_rule(self): + return self.conc_mol_K == self.conc_mass_comp_ref["X_PP"] / ( + 300.41 * pyo.units.kg / pyo.units.kmol + ) + + self.concentration_of_K = pyo.Constraint( + rule=concentration_of_K_rule, + doc="constraint concentration of K", + ) + + def S_OH_rule(self): + return self.S_OH == self.KW / self.S_H + + self.S_OH_cons = pyo.Constraint( + rule=S_OH_rule, + doc="constraint concentration of OH", + ) + + def S_H_rule(self): + return ( + self.state_ref.cations + + self.conc_mol_nh4 + + self.conc_mol_Mg + + self.conc_mol_K + + self.S_H + - self.conc_mol_hco3 + - self.conc_mass_ac / (64 * pyo.units.kg / pyo.units.kmol) + - self.conc_mass_pro / (112 * pyo.units.kg / pyo.units.kmol) + - self.conc_mass_bu / (160 * pyo.units.kg / pyo.units.kmol) + - self.conc_mass_va / (208 * pyo.units.kg / pyo.units.kmol) + - self.S_OH + - self.state_ref.anions + == 0 + ) + + self.S_H_cons = pyo.Constraint( + rule=S_H_rule, + doc="constraint concentration of H", + ) + + def rule_pH(self): + return -pyo.log10(self.S_H / pyo.units.kmol * pyo.units.m**3) + + self.pH = pyo.Expression(rule=rule_pH, doc="pH of solution") + + def rule_I_IN_lim(self): + return 1 / ( + 1 + self.params.K_S_IN / (self.conc_mass_comp_ref["S_IN"] / mw_n) + ) + + self.I_IN_lim = pyo.Expression( + rule=rule_I_IN_lim, + doc="Inhibition function related to secondary substrate; inhibit uptake when inorganic nitrogen S_IN~ 0", + ) + + def rule_I_IP_lim(self): + return 1 / ( + 1 + self.params.K_S_IP / (self.conc_mass_comp_ref["S_IP"] / mw_p) + ) + + self.I_IP_lim = pyo.Expression( + rule=rule_I_IP_lim, + doc="Inhibition function related to secondary substrate; inhibit uptake when inorganic phosphorus S_IP~ 0", + ) + + def rule_I_h2_fa(self): + return 1 / (1 + self.conc_mass_comp_ref["S_h2"] / self.params.K_I_h2_fa) + + self.I_h2_fa = pyo.Expression( + rule=rule_I_h2_fa, + doc="hydrogen inhibition attributed to long chain fatty acids", + ) + + def rule_I_h2_c4(self): + return 1 / (1 + self.conc_mass_comp_ref["S_h2"] / self.params.K_I_h2_c4) + + self.I_h2_c4 = pyo.Expression( + rule=rule_I_h2_c4, + doc="hydrogen inhibition attributed to valerate and butyrate uptake", + ) + + def rule_I_h2_pro(self): + return 1 / (1 + self.conc_mass_comp_ref["S_h2"] / self.params.K_I_h2_pro) + + self.I_h2_pro = pyo.Expression( + rule=rule_I_h2_pro, + doc="hydrogen inhibition attributed to propionate uptake", + ) + + # TODO: revisit Z_h2s value if we have ref state for S_h2s (currently assumed to be 0) + def rule_I_h2s_ac(self): + return 1 / (1 + self.params.Z_h2s / self.params.K_I_h2s_ac) + + self.I_h2s_ac = pyo.Expression( + rule=rule_I_h2s_ac, + doc="hydrogen sulfide inhibition attributed to acetate uptake", + ) + + def rule_I_h2s_c4(self): + return 1 / (1 + self.params.Z_h2s / self.params.K_I_h2s_c4) + + self.I_h2s_c4 = pyo.Expression( + rule=rule_I_h2s_c4, + doc="hydrogen sulfide inhibition attributed to valerate and butyrate uptake", + ) + + def rule_I_h2s_h2(self): + return 1 / (1 + self.params.Z_h2s / self.params.K_I_h2s_h2) + + self.I_h2s_h2 = pyo.Expression( + rule=rule_I_h2s_h2, + doc="hydrogen sulfide inhibition attributed to hydrogen uptake", + ) + + def rule_I_h2s_pro(self): + return 1 / (1 + self.params.Z_h2s / self.params.K_I_h2s_pro) + + self.I_h2s_pro = pyo.Expression( + rule=rule_I_h2s_pro, + doc="hydrogen sulfide inhibition attributed to propionate uptake", + ) + + def rule_I_nh3(self): + return 1 / (1 + self.conc_mol_nh3 / self.params.K_I_nh3) + + self.I_nh3 = pyo.Expression( + rule=rule_I_nh3, doc="ammonia inibition attributed to acetate uptake" + ) + + def rule_I_pH_aa(self): + return pyo.Expr_if( + self.pH > self.params.pH_UL_aa, + 1, + pyo.exp( + -3 + * ( + (self.pH - self.params.pH_UL_aa) + / (self.params.pH_UL_aa - self.params.pH_LL_aa) + ) + ** 2 + ), + ) + + self.I_pH_aa = pyo.Expression( + rule=rule_I_pH_aa, + doc="pH inhibition of amino-acid-utilizing microorganisms", + ) + + def rule_I_pH_ac(self): + return pyo.Expr_if( + self.pH > self.params.pH_UL_ac, + 1, + pyo.exp( + -3 + * ( + (self.pH - self.params.pH_UL_ac) + / (self.params.pH_UL_ac - self.params.pH_LL_ac) + ) + ** 2 + ), + ) + + self.I_pH_ac = pyo.Expression( + rule=rule_I_pH_ac, doc="pH inhibition of acetate-utilizing microorganisms" + ) + + def rule_I_pH_h2(self): + return pyo.Expr_if( + self.pH > self.params.pH_UL_h2, + 1, + pyo.exp( + -3 + * ( + (self.pH - self.params.pH_UL_h2) + / (self.params.pH_UL_h2 - self.params.pH_LL_h2) + ) + ** 2 + ), + ) + + self.I_pH_h2 = pyo.Expression( + rule=rule_I_pH_h2, doc="pH inhibition of hydrogen-utilizing microorganisms" + ) + + def rule_I(self, r): + if r == "R4" or r == "R5": + return self.I_pH_aa * self.I_IN_lim * self.I_IP_lim + elif r == "R6": + return self.I_pH_aa * self.I_IN_lim * self.I_h2_fa * self.I_IP_lim + elif r == "R7" or r == "R8": + return self.I_pH_aa * self.I_IN_lim * self.I_h2_c4 * self.I_IP_lim + elif r == "R9": + return self.I_pH_aa * self.I_IN_lim * self.I_h2_pro * self.I_IP_lim + elif r == "R10": + return self.I_pH_ac * self.I_IN_lim * self.I_nh3 * self.I_IP_lim + elif r == "R11": + return self.I_pH_h2 * self.I_IN_lim * self.I_IP_lim + else: + raise BurntToast() + + self.I = pyo.Expression( + [f"R{i}" for i in range(4, 12)], + rule=rule_I, + doc="Process inhibition functions", + ) + + try: + + def rate_expression_rule(b, r): + if r == "R1": + # R1: Hydrolysis of carbohydrates + return b.reaction_rate[r] == pyo.units.convert( + b.params.k_hyd_ch * b.conc_mass_comp_ref["X_ch"], + to_units=pyo.units.kg / pyo.units.m**3 / pyo.units.s, + ) + elif r == "R2": + # R2: Hydrolysis of proteins + return b.reaction_rate[r] == pyo.units.convert( + b.params.k_hyd_pr * b.conc_mass_comp_ref["X_pr"], + to_units=pyo.units.kg / pyo.units.m**3 / pyo.units.s, + ) + elif r == "R3": + # R3: Hydrolysis of lipids + return b.reaction_rate[r] == pyo.units.convert( + b.params.k_hyd_li * b.conc_mass_comp_ref["X_li"], + to_units=pyo.units.kg / pyo.units.m**3 / pyo.units.s, + ) + elif r == "R4": + # R4: Uptake of sugars + return b.reaction_rate[r] == pyo.units.convert( + b.params.k_m_su + * b.conc_mass_comp_ref["S_su"] + / (b.params.K_S_su + b.conc_mass_comp_ref["S_su"]) + * b.conc_mass_comp_ref["X_su"] + * b.I[r], + to_units=pyo.units.kg / pyo.units.m**3 / pyo.units.s, + ) + elif r == "R5": + # R5: Uptake of amino acids + return b.reaction_rate[r] == pyo.units.convert( + b.params.k_m_aa + * b.conc_mass_comp_ref["S_aa"] + / (b.params.K_S_aa + b.conc_mass_comp_ref["S_aa"]) + * b.conc_mass_comp_ref["X_aa"] + * b.I[r], + to_units=pyo.units.kg / pyo.units.m**3 / pyo.units.s, + ) + elif r == "R6": + # R6: Uptake of long chain fatty acids (LCFAs) + return b.reaction_rate[r] == pyo.units.convert( + b.params.k_m_fa + * b.conc_mass_comp_ref["S_fa"] + / (b.params.K_S_fa + b.conc_mass_comp_ref["S_fa"]) + * b.conc_mass_comp_ref["X_fa"] + * b.I[r], + to_units=pyo.units.kg / pyo.units.m**3 / pyo.units.s, + ) + elif r == "R7": + # R7: Uptake of valerate + return b.reaction_rate[r] == pyo.units.convert( + b.params.k_m_c4 + * b.conc_mass_comp_ref["S_va"] + / (b.params.K_S_c4 + b.conc_mass_comp_ref["S_va"]) + * b.conc_mass_comp_ref["X_c4"] + * ( + b.conc_mass_comp_ref["S_va"] + / ( + b.conc_mass_comp_ref["S_va"] + + b.conc_mass_comp_ref["S_bu"] + ) + ) + * b.I[r] + * b.I_h2s_c4, + to_units=pyo.units.kg / pyo.units.m**3 / pyo.units.s, + ) + elif r == "R8": + # R8: Uptake of butyrate + return b.reaction_rate[r] == pyo.units.convert( + b.params.k_m_c4 + * b.conc_mass_comp_ref["S_bu"] + / (b.params.K_S_c4 + b.conc_mass_comp_ref["S_bu"]) + * b.conc_mass_comp_ref["X_c4"] + * ( + b.conc_mass_comp_ref["S_bu"] + / ( + b.conc_mass_comp_ref["S_va"] + + b.conc_mass_comp_ref["S_bu"] + ) + ) + * b.I[r] + * b.I_h2s_c4, + to_units=pyo.units.kg / pyo.units.m**3 / pyo.units.s, + ) + elif r == "R9": + # R9: Uptake of propionate + return b.reaction_rate[r] == pyo.units.convert( + b.params.k_m_pro + * b.conc_mass_comp_ref["S_pro"] + / (b.params.K_S_pro + b.conc_mass_comp_ref["S_pro"]) + * b.conc_mass_comp_ref["X_pro"] + * b.I[r] + * b.I_h2s_pro, + to_units=pyo.units.kg / pyo.units.m**3 / pyo.units.s, + ) + elif r == "R10": + # R10: Uptake of acetate + return b.reaction_rate[r] == pyo.units.convert( + b.params.k_m_ac + * b.conc_mass_comp_ref["S_ac"] + / (b.params.K_S_ac + b.conc_mass_comp_ref["S_ac"]) + * b.conc_mass_comp_ref["X_ac"] + * b.I[r] + * b.I_h2s_ac, + to_units=pyo.units.kg / pyo.units.m**3 / pyo.units.s, + ) + elif r == "R11": + # R11: Uptake of hydrogen + return b.reaction_rate[r] == pyo.units.convert( + b.params.k_m_h2 + * b.conc_mass_comp_ref["S_h2"] + / (b.params.K_S_h2 + b.conc_mass_comp_ref["S_h2"]) + * b.conc_mass_comp_ref["X_h2"] + * b.I[r] + * b.I_h2s_h2, + to_units=pyo.units.kg / pyo.units.m**3 / pyo.units.s, + ) + elif r == "R12": + # R12: Decay of X_su + return b.reaction_rate[r] == pyo.units.convert( + b.params.k_dec_X_su * b.conc_mass_comp_ref["X_su"], + to_units=pyo.units.kg / pyo.units.m**3 / pyo.units.s, + ) + elif r == "R13": + # R13: Decay of X_aa + return b.reaction_rate[r] == pyo.units.convert( + b.params.k_dec_X_aa * b.conc_mass_comp_ref["X_aa"], + to_units=pyo.units.kg / pyo.units.m**3 / pyo.units.s, + ) + elif r == "R14": + # R14: Decay of X_fa + return b.reaction_rate[r] == pyo.units.convert( + b.params.k_dec_X_fa * b.conc_mass_comp_ref["X_fa"], + to_units=pyo.units.kg / pyo.units.m**3 / pyo.units.s, + ) + elif r == "R15": + # R15: Decay of X_c4 + return b.reaction_rate[r] == pyo.units.convert( + b.params.k_dec_X_c4 * b.conc_mass_comp_ref["X_c4"], + to_units=pyo.units.kg / pyo.units.m**3 / pyo.units.s, + ) + elif r == "R16": + # R16: Decay of X_pro + return b.reaction_rate[r] == pyo.units.convert( + b.params.k_dec_X_pro * b.conc_mass_comp_ref["X_pro"], + to_units=pyo.units.kg / pyo.units.m**3 / pyo.units.s, + ) + elif r == "R17": + # R17: Decay of X_ac + return b.reaction_rate[r] == pyo.units.convert( + b.params.k_dec_X_ac * b.conc_mass_comp_ref["X_ac"], + to_units=pyo.units.kg / pyo.units.m**3 / pyo.units.s, + ) + elif r == "R18": + # R18: Decay of X_h2 + return b.reaction_rate[r] == pyo.units.convert( + b.params.k_dec_X_h2 * b.conc_mass_comp_ref["X_h2"], + to_units=pyo.units.kg / pyo.units.m**3 / pyo.units.s, + ) + elif r == "R19": + # R19: Storage of S_va in X_PHA + return b.reaction_rate[r] == pyo.units.convert( + b.params.q_PHA + * b.conc_mass_comp_ref["S_va"] + / (b.params.K_A + b.conc_mass_comp_ref["S_va"]) + * (b.conc_mass_comp_ref["X_PP"] / b.conc_mass_comp_ref["X_PAO"]) + / ( + b.params.K_PP + + b.conc_mass_comp_ref["X_PP"] + / b.conc_mass_comp_ref["X_PAO"] + ) + * b.conc_mass_comp_ref["X_PAO"] + * b.conc_mass_comp_ref["S_va"] + / ( + b.conc_mass_comp_ref["S_va"] + + b.conc_mass_comp_ref["S_bu"] + + b.conc_mass_comp_ref["S_pro"] + + b.conc_mass_comp_ref["S_ac"] + ), + to_units=pyo.units.kg / pyo.units.m**3 / pyo.units.s, + ) + elif r == "R20": + # R20: Storage of S_bu in X_PHA + return b.reaction_rate[r] == pyo.units.convert( + b.params.q_PHA + * b.conc_mass_comp_ref["S_bu"] + / (b.params.K_A + b.conc_mass_comp_ref["S_bu"]) + * (b.conc_mass_comp_ref["X_PP"] / b.conc_mass_comp_ref["X_PAO"]) + / ( + b.params.K_PP + + b.conc_mass_comp_ref["X_PP"] + / b.conc_mass_comp_ref["X_PAO"] + ) + * b.conc_mass_comp_ref["X_PAO"] + * b.conc_mass_comp_ref["S_bu"] + / ( + b.conc_mass_comp_ref["S_va"] + + b.conc_mass_comp_ref["S_bu"] + + b.conc_mass_comp_ref["S_pro"] + + b.conc_mass_comp_ref["S_ac"] + ), + to_units=pyo.units.kg / pyo.units.m**3 / pyo.units.s, + ) + elif r == "R21": + # R21: Storage of S_pro in X_PHA + return b.reaction_rate[r] == pyo.units.convert( + b.params.q_PHA + * b.conc_mass_comp_ref["S_pro"] + / (b.params.K_A + b.conc_mass_comp_ref["S_pro"]) + * (b.conc_mass_comp_ref["X_PP"] / b.conc_mass_comp_ref["X_PAO"]) + / ( + b.params.K_PP + + b.conc_mass_comp_ref["X_PP"] + / b.conc_mass_comp_ref["X_PAO"] + ) + * b.conc_mass_comp_ref["X_PAO"] + * b.conc_mass_comp_ref["S_pro"] + / ( + b.conc_mass_comp_ref["S_va"] + + b.conc_mass_comp_ref["S_bu"] + + b.conc_mass_comp_ref["S_pro"] + + b.conc_mass_comp_ref["S_ac"] + ), + to_units=pyo.units.kg / pyo.units.m**3 / pyo.units.s, + ) + elif r == "R22": + # R22: Storage of S_ac in X_PHA + return b.reaction_rate[r] == pyo.units.convert( + b.params.q_PHA + * b.conc_mass_comp_ref["S_ac"] + / (b.params.K_A + b.conc_mass_comp_ref["S_ac"]) + * (b.conc_mass_comp_ref["X_PP"] / b.conc_mass_comp_ref["X_PAO"]) + / ( + b.params.K_PP + + b.conc_mass_comp_ref["X_PP"] + / b.conc_mass_comp_ref["X_PAO"] + ) + * b.conc_mass_comp_ref["X_PAO"] + * b.conc_mass_comp_ref["S_ac"] + / ( + b.conc_mass_comp_ref["S_va"] + + b.conc_mass_comp_ref["S_bu"] + + b.conc_mass_comp_ref["S_pro"] + + b.conc_mass_comp_ref["S_ac"] + ), + to_units=pyo.units.kg / pyo.units.m**3 / pyo.units.s, + ) + elif r == "R23": + # R23: Lysis of X_PAO + return b.reaction_rate[r] == pyo.units.convert( + b.params.b_PAO * b.conc_mass_comp_ref["X_PAO"], + to_units=pyo.units.kg / pyo.units.m**3 / pyo.units.s, + ) + elif r == "R24": + # R24: Lysis of X_PP + return b.reaction_rate[r] == pyo.units.convert( + b.params.b_PP * b.conc_mass_comp_ref["X_PP"], + to_units=pyo.units.kg / pyo.units.m**3 / pyo.units.s, + ) + elif r == "R25": + # R25: Lysis of X_PHA + return b.reaction_rate[r] == pyo.units.convert( + b.params.b_PHA * b.conc_mass_comp_ref["X_PHA"], + to_units=pyo.units.kg / pyo.units.m**3 / pyo.units.s, + ) + else: + raise BurntToast() + + self.rate_expression = pyo.Constraint( + self.params.rate_reaction_idx, + rule=rate_expression_rule, + doc="ADM1 rate expressions", + ) + + except AttributeError: + # If constraint fails, clean up so that DAE can try again later + self.del_component(self.reaction_rate) + self.del_component(self.rate_expression) + raise + + def get_reaction_rate_basis(self): + return MaterialFlowBasis.mass + + def calculate_scaling_factors(self): + super().calculate_scaling_factors() + + iscale.set_scaling_factor(self.conc_mass_va, 1e2) + iscale.set_scaling_factor(self.conc_mass_bu, 1e2) + iscale.set_scaling_factor(self.conc_mass_pro, 1e2) + iscale.set_scaling_factor(self.conc_mass_ac, 1e1) + iscale.set_scaling_factor(self.conc_mol_hco3, 1e1) + iscale.set_scaling_factor(self.conc_mol_nh3, 1e1) + iscale.set_scaling_factor(self.conc_mol_co2, 1e1) + iscale.set_scaling_factor(self.conc_mol_nh4, 1e1) + iscale.set_scaling_factor(self.conc_mol_Mg, 1e2) + iscale.set_scaling_factor(self.conc_mol_K, 1e2) + iscale.set_scaling_factor(self.S_H, 1e7) + iscale.set_scaling_factor(self.S_OH, 1e-1) + iscale.set_scaling_factor(self.KW, 1e7) + iscale.set_scaling_factor(self.K_a_co2, 1e7) + iscale.set_scaling_factor(self.K_a_IN, 1e9) + + for var in self.reaction_rate.values(): + iscale.set_variable_scaling_from_current_value(var) + # iscale.set_scaling_factor(self.reaction_rate, 1e6) + + for i, c in self.rate_expression.items(): + # TODO: Need to work out how to calculate good scaling factors + # instead of a fixed 1e3. + iscale.constraint_scaling_transform(c, 1e5, overwrite=True) + + iscale.constraint_scaling_transform(self.Dissociation, 1e7) + iscale.constraint_scaling_transform(self.CO2_acid_base_equilibrium, 1e7) + iscale.constraint_scaling_transform(self.IN_acid_base_equilibrium, 1e9) diff --git a/watertap/property_models/anaerobic_digestion/tests/test_adm1_reaction.py b/watertap/property_models/anaerobic_digestion/tests/test_adm1_reaction.py index 73700104f0..06f5fd6cae 100644 --- a/watertap/property_models/anaerobic_digestion/tests/test_adm1_reaction.py +++ b/watertap/property_models/anaerobic_digestion/tests/test_adm1_reaction.py @@ -27,7 +27,6 @@ check_optimal_termination, ConcreteModel, Constraint, - units, value, Var, ) @@ -37,7 +36,6 @@ from watertap.unit_models.anaerobic_digestor import AD from idaes.core import MaterialFlowBasis from idaes.core.solvers import get_solver -import idaes.logger as idaeslog import idaes.core.util.scaling as iscale from idaes.core.util.model_statistics import degrees_of_freedom @@ -426,7 +424,7 @@ def test_get_reaction_rate_basis(self, model): def test_initialize(self, model): assert model.rxns.initialize() is None - @pytest.mark.component + @pytest.mark.unit def check_units(self, model): assert_units_consistent(model) @@ -490,15 +488,15 @@ def model(self): return m - @pytest.mark.component + @pytest.mark.unit def test_dof(self, model): assert degrees_of_freedom(model) == 0 - @pytest.mark.component + @pytest.mark.unit def test_unit_consistency(self, model): assert_units_consistent(model) == 0 - @pytest.mark.component + @pytest.mark.unit def test_scaling_factors(self, model): m = model diff --git a/watertap/property_models/anaerobic_digestion/tests/test_adm1_thermo.py b/watertap/property_models/anaerobic_digestion/tests/test_adm1_thermo.py index d38d194add..2c66b3cba4 100644 --- a/watertap/property_models/anaerobic_digestion/tests/test_adm1_thermo.py +++ b/watertap/property_models/anaerobic_digestion/tests/test_adm1_thermo.py @@ -16,7 +16,7 @@ import pytest -from pyomo.environ import ConcreteModel, Param, units, value, Var +from pyomo.environ import ConcreteModel, Param, value, Var from pyomo.util.check_units import assert_units_consistent from idaes.core import MaterialBalanceType, EnergyBalanceType, MaterialFlowBasis @@ -251,7 +251,7 @@ def test_define_display_vars(self, model): "Pressure", ] - @pytest.mark.unit + @pytest.mark.component def test_initialize(self, model): orig_fixed_vars = fixed_variables_set(model) orig_act_consts = activated_constraints_set(model) @@ -269,6 +269,6 @@ def test_initialize(self, model): for v in fin_fixed_vars: assert v in orig_fixed_vars - @pytest.mark.component + @pytest.mark.unit def check_units(self, model): assert_units_consistent(model) diff --git a/watertap/property_models/anaerobic_digestion/tests/test_adm1_vapor_thermo.py b/watertap/property_models/anaerobic_digestion/tests/test_adm1_vapor_thermo.py index b86c5341ca..89683224f2 100644 --- a/watertap/property_models/anaerobic_digestion/tests/test_adm1_vapor_thermo.py +++ b/watertap/property_models/anaerobic_digestion/tests/test_adm1_vapor_thermo.py @@ -19,7 +19,6 @@ from pyomo.environ import ( ConcreteModel, Param, - units, value, Var, check_optimal_termination, @@ -194,7 +193,7 @@ def test_define_display_vars(self, model): "Pressure", ] - @pytest.mark.unit + @pytest.mark.component def test_initialize(self, model): orig_fixed_vars = fixed_variables_set(model) orig_act_consts = activated_constraints_set(model) @@ -228,7 +227,7 @@ def test_solve(self, model): results = solver.solve(model, tee=True) assert check_optimal_termination(results) - @pytest.mark.unit + @pytest.mark.component def test_pressures(self, model): assert value(model.props[1].conc_mass_comp["S_h2"]) == pytest.approx( @@ -241,16 +240,20 @@ def test_pressures(self, model): 0.1692, rel=1e-4 ) - assert value(model.props[1].p_sat["S_h2"]) == pytest.approx(1.6397, rel=1e-4) - assert value(model.props[1].p_sat["S_ch4"]) == pytest.approx( + assert value(model.props[1].pressure_sat["S_h2"]) == pytest.approx( + 1.6397, rel=1e-4 + ) + assert value(model.props[1].pressure_sat["S_ch4"]) == pytest.approx( 65077.382, rel=1e-4 ) - assert value(model.props[1].p_sat["S_co2"]) == pytest.approx( + assert value(model.props[1].pressure_sat["S_co2"]) == pytest.approx( 36125.633, rel=1e-4 ) - assert value(model.props[1].p_w_sat) == pytest.approx(5640.534, rel=1e-4) + assert value(model.props[1].pressure_sat["H2O"]) == pytest.approx( + 5640.5342, rel=1e-4 + ) - @pytest.mark.component + @pytest.mark.unit def check_units(self, model): assert_units_consistent(model) diff --git a/watertap/property_models/anaerobic_digestion/tests/test_modified_adm1_reaction.py b/watertap/property_models/anaerobic_digestion/tests/test_modified_adm1_reaction.py new file mode 100644 index 0000000000..bb21aefdd3 --- /dev/null +++ b/watertap/property_models/anaerobic_digestion/tests/test_modified_adm1_reaction.py @@ -0,0 +1,813 @@ +################################################################################# +# WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# National Renewable Energy Laboratory, and National Energy Technology +# Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +################################################################################# +""" +Tests for ADM1 reaction package. + +Verified against results from: + +X. Flores-Alsina, K. Solon, C.K. Mbamba, S. Tait, K.V. Gernaey, U. Jeppsson, D.J. Batstone, +Modelling phosphorus (P), sulfur (S) and iron (Fe) interactions for dynamic simulations of anaerobic digestion processes, +Water Research. 95 (2016) 370-382. https://www.sciencedirect.com/science/article/pii/S0043135416301397 + +Authors: Chenyu Wang, Marcus Holly +""" + +import pytest + +from pyomo.environ import ( + check_optimal_termination, + ConcreteModel, + Param, +) +from pyomo.util.check_units import assert_units_consistent +from idaes.core import FlowsheetBlock +from watertap.unit_models.anaerobic_digestor import AD +from idaes.core import MaterialFlowBasis +from idaes.core.solvers import get_solver +import idaes.core.util.scaling as iscale +from idaes.core.util.model_statistics import degrees_of_freedom, large_residuals_set + +from watertap.property_models.anaerobic_digestion.modified_adm1_properties import ( + ModifiedADM1ParameterBlock, +) +from watertap.property_models.anaerobic_digestion.adm1_properties_vapor import ( + ADM1_vaporParameterBlock, +) +from watertap.property_models.anaerobic_digestion.modified_adm1_reactions import ( + ModifiedADM1ReactionParameterBlock, + ModifiedADM1ReactionBlock, +) +from watertap.core.util.model_diagnostics.infeasible import * +from idaes.core.util.testing import initialization_tester + +# ----------------------------------------------------------------------------- +# Get default solver for testing +solver = get_solver() + + +class TestParamBlock(object): + @pytest.fixture(scope="class") + def model(self): + model = ConcreteModel() + model.pparams = ModifiedADM1ParameterBlock() + model.rparams = ModifiedADM1ReactionParameterBlock( + property_package=model.pparams + ) + + return model + + @pytest.mark.unit + def test_build(self, model): + assert model.rparams.reaction_block_class is ModifiedADM1ReactionBlock + + assert len(model.rparams.rate_reaction_idx) == 25 + for i in model.rparams.rate_reaction_idx: + assert i in [ + "R1", + "R2", + "R3", + "R4", + "R5", + "R6", + "R7", + "R8", + "R9", + "R10", + "R11", + "R12", + "R13", + "R14", + "R15", + "R16", + "R17", + "R18", + "R19", + "R20", + "R21", + "R22", + "R23", + "R24", + "R25", + ] + + # Expected non-zero stoichiometries + # Values from table 11 in reference + + mw_n = 14 + mw_c = 12 + mw_p = 31 + + stoic = { + # R1: Hydrolysis of carbohydrates + ("R1", "Liq", "S_su"): 1, + ("R1", "Liq", "X_ch"): -1, + # R2: Hydrolysis of proteins + ("R2", "Liq", "S_aa"): 1, + ("R2", "Liq", "X_pr"): -1, + # R3: Hydrolysis of lipids + ("R3", "Liq", "S_su"): 0.05, + ("R3", "Liq", "S_fa"): 0.95, + ("R3", "Liq", "X_li"): -1, + ("R3", "Liq", "S_IC"): 0.000029 * mw_c, + ("R3", "Liq", "S_IP"): 0.0003441 * mw_p, + # R4: Uptake of sugars + ("R4", "Liq", "S_su"): -1, + ("R4", "Liq", "S_bu"): 0.1195, + ("R4", "Liq", "S_pro"): 0.2422, + ("R4", "Liq", "S_ac"): 0.3668, + ("R4", "Liq", "S_h2"): 0.1715, + ("R4", "Liq", "S_IC"): 0.00726 * mw_c, + ("R4", "Liq", "S_IN"): -0.000615 * mw_n, + ("R4", "Liq", "S_IP"): -0.000069 * mw_p, + ("R4", "Liq", "X_su"): 0.10, + # R5: Uptake of amino acids + ("R5", "Liq", "S_aa"): -1, + ("R5", "Liq", "S_va"): 0.2116, + ("R5", "Liq", "S_bu"): 0.2392, + ("R5", "Liq", "S_pro"): 0.0460, + ("R5", "Liq", "S_ac"): 0.3680, + ("R5", "Liq", "S_h2"): 0.0552, + ("R5", "Liq", "S_IC"): 0.00450 * mw_c, + ("R5", "Liq", "S_IN"): 0.00741 * mw_n, + ("R5", "Liq", "S_IP"): -0.0000556 * mw_p, + ("R5", "Liq", "X_aa"): 0.08, + # R6: Uptake of long chain fatty acids (LCFAs) + ("R6", "Liq", "S_fa"): -1, + ("R6", "Liq", "S_ac"): 0.6580, + ("R6", "Liq", "S_h2"): 0.2820, + ("R6", "Liq", "S_IC"): -0.000989 * mw_c, + ("R6", "Liq", "S_IN"): -0.000369 * mw_n, + ("R6", "Liq", "S_IP"): -0.0000417 * mw_p, + ("R6", "Liq", "X_fa"): 0.06, + # R7: Uptake of valerate + ("R7", "Liq", "S_va"): -1, + ("R7", "Liq", "S_pro"): 0.5076, + ("R7", "Liq", "S_ac"): 0.2914, + ("R7", "Liq", "S_h2"): 0.1410, + ("R7", "Liq", "S_IC"): -0.000495 * mw_c, + ("R7", "Liq", "S_IN"): -0.000369 * mw_n, + ("R7", "Liq", "S_IP"): -0.0000417 * mw_p, + ("R7", "Liq", "X_c4"): 0.06, + # R8: Uptake of butyrate + ("R8", "Liq", "S_bu"): -1, + ("R8", "Liq", "S_ac"): 0.7520, + ("R8", "Liq", "S_h2"): 0.1880, + ("R8", "Liq", "S_IC"): -0.000331 * mw_c, + ("R8", "Liq", "S_IN"): -0.000369 * mw_n, + ("R8", "Liq", "S_IP"): -0.0000417 * mw_p, + ("R8", "Liq", "X_c4"): 0.06, + # R9: Uptake of propionate + ("R9", "Liq", "S_pro"): -1, + ("R9", "Liq", "S_ac"): 0.5472, + ("R9", "Liq", "S_h2"): 0.4128, + ("R9", "Liq", "S_IC"): 0.008465 * mw_c, + ("R9", "Liq", "S_IN"): -0.000246 * mw_n, + ("R9", "Liq", "S_IP"): -0.0000278 * mw_p, + ("R9", "Liq", "X_pro"): 0.04, + # R10: Uptake of acetate + ("R10", "Liq", "S_ac"): -1, + ("R10", "Liq", "S_ch4"): 0.95, + ("R10", "Liq", "S_IC"): 0.014881 * mw_c, + ("R10", "Liq", "S_IN"): -0.000308 * mw_n, + ("R10", "Liq", "S_IP"): -0.0000347 * mw_p, + ("R10", "Liq", "X_ac"): 0.05, + # R11: Uptake of hydrogen + ("R11", "Liq", "S_h2"): -1, + ("R11", "Liq", "S_ch4"): 0.94, + ("R11", "Liq", "S_IC"): -0.016518 * mw_c, + ("R11", "Liq", "S_IN"): -0.000369 * mw_n, + ("R11", "Liq", "S_IP"): -0.0000417 * mw_p, + ("R11", "Liq", "X_h2"): 0.06, + # R12: Decay of X_su + ("R12", "Liq", "S_IC"): 0.002773 * mw_c, + ("R12", "Liq", "S_IN"): 0.003551 * mw_n, + ("R12", "Liq", "S_IP"): 0.0005534 * mw_p, + ("R12", "Liq", "X_ch"): 0.275, + ("R12", "Liq", "X_pr"): 0.275, + ("R12", "Liq", "X_li"): 0.35, + ("R12", "Liq", "X_su"): -1, + ("R12", "Liq", "X_I"): 0.1, + # R13: Decay of X_aa + ("R13", "Liq", "S_IC"): 0.002773 * mw_c, + ("R13", "Liq", "S_IN"): 0.003551 * mw_n, + ("R13", "Liq", "S_IP"): 0.0005534 * mw_p, + ("R13", "Liq", "X_ch"): 0.275, + ("R13", "Liq", "X_pr"): 0.275, + ("R13", "Liq", "X_li"): 0.35, + ("R13", "Liq", "X_aa"): -1, + ("R13", "Liq", "X_I"): 0.1, + # R14: Decay of X_fa + ("R14", "Liq", "S_IC"): 0.002773 * mw_c, + ("R14", "Liq", "S_IN"): 0.003551 * mw_n, + ("R14", "Liq", "S_IP"): 0.0005534 * mw_p, + ("R14", "Liq", "X_ch"): 0.275, + ("R14", "Liq", "X_pr"): 0.275, + ("R14", "Liq", "X_li"): 0.35, + ("R14", "Liq", "X_fa"): -1, + ("R14", "Liq", "X_I"): 0.1, + # R15: Decay of X_c4 + ("R15", "Liq", "S_IC"): 0.002773 * mw_c, + ("R15", "Liq", "S_IN"): 0.003551 * mw_n, + ("R15", "Liq", "S_IP"): 0.0005534 * mw_p, + ("R15", "Liq", "X_ch"): 0.275, + ("R15", "Liq", "X_pr"): 0.275, + ("R15", "Liq", "X_li"): 0.35, + ("R15", "Liq", "X_c4"): -1, + ("R15", "Liq", "X_I"): 0.1, + # R16: Decay of X_pro + ("R16", "Liq", "S_IC"): 0.002773 * mw_c, + ("R16", "Liq", "S_IN"): 0.003551 * mw_n, + ("R16", "Liq", "S_IP"): 0.0005534 * mw_p, + ("R16", "Liq", "X_ch"): 0.275, + ("R16", "Liq", "X_pr"): 0.275, + ("R16", "Liq", "X_li"): 0.35, + ("R16", "Liq", "X_pro"): -1, + ("R16", "Liq", "X_I"): 0.1, + # R17: Decay of X_ac + ("R17", "Liq", "S_IC"): 0.002773 * mw_c, + ("R17", "Liq", "S_IN"): 0.003551 * mw_n, + ("R17", "Liq", "S_IP"): 0.0005534 * mw_p, + ("R17", "Liq", "X_ch"): 0.275, + ("R17", "Liq", "X_pr"): 0.275, + ("R17", "Liq", "X_li"): 0.35, + ("R17", "Liq", "X_ac"): -1, + ("R17", "Liq", "X_I"): 0.1, + # R18: Decay of X_h2 + ("R18", "Liq", "S_IC"): 0.002773 * mw_c, + ("R18", "Liq", "S_IN"): 0.003551 * mw_n, + ("R18", "Liq", "S_IP"): 0.0005534 * mw_p, + ("R18", "Liq", "X_ch"): 0.275, + ("R18", "Liq", "X_pr"): 0.275, + ("R18", "Liq", "X_li"): 0.35, + ("R18", "Liq", "X_h2"): -1, + ("R18", "Liq", "X_I"): 0.1, + # R19: Storage of S_va in X_PHA + ("R19", "Liq", "S_va"): -1, + ("R19", "Liq", "S_IC"): -0.000962 * mw_c, + ("R19", "Liq", "S_IP"): 0.012903 * mw_p, + ("R19", "Liq", "X_PHA"): 1, + ("R19", "Liq", "X_PP"): -0.012903, + ("R19", "Liq", "S_K"): 0.004301, + ("R19", "Liq", "S_Mg"): 0.004301, + # R20: Storage of S_bu in X_PHA + ("R20", "Liq", "S_bu"): -1, + ("R20", "Liq", "S_IP"): 0.012903 * mw_p, + ("R20", "Liq", "X_PHA"): 1, + ("R20", "Liq", "X_PP"): -0.012903, + ("R20", "Liq", "S_K"): 0.004301, + ("R20", "Liq", "S_Mg"): 0.004301, + # R21: Storage of S_pro in X_PHA + ("R21", "Liq", "S_pro"): -1, + ("R21", "Liq", "S_IC"): 0.001786 * mw_c, + ("R21", "Liq", "S_IP"): 0.012903 * mw_p, + ("R21", "Liq", "X_PHA"): 1, + ("R21", "Liq", "X_PP"): -0.012903, + ("R21", "Liq", "S_K"): 0.004301, + ("R21", "Liq", "S_Mg"): 0.004301, + # R22: Storage of S_ac in X_PHA + ("R22", "Liq", "S_ac"): -1, + ("R22", "Liq", "S_IC"): 0.006250 * mw_c, + ("R22", "Liq", "S_IP"): 0.012903 * mw_p, + ("R22", "Liq", "X_PHA"): 1, + ("R22", "Liq", "X_PP"): -0.012903, + ("R22", "Liq", "S_K"): 0.004301, + ("R22", "Liq", "S_Mg"): 0.004301, + # R23: Lysis of X_PAO + ("R23", "Liq", "S_IC"): 0.002773 * mw_c, + ("R23", "Liq", "S_IN"): 0.003551 * mw_n, + ("R23", "Liq", "S_IP"): 0.000553 * mw_p, + ("R23", "Liq", "X_ch"): 0.275, + ("R23", "Liq", "X_pr"): 0.275, + ("R23", "Liq", "X_li"): 0.35, + ("R23", "Liq", "X_I"): 0.1, + ("R23", "Liq", "X_PAO"): -1, + # R24: Lysis of X_PP + ("R24", "Liq", "S_IP"): 1 * mw_p, + ("R24", "Liq", "X_PP"): -1, + ("R24", "Liq", "S_K"): 1 / 3, + ("R24", "Liq", "S_Mg"): 1 / 3, + # R25: Lysis of X_PAO + ("R25", "Liq", "S_va"): 0.1, + ("R25", "Liq", "S_bu"): 0.1, + ("R25", "Liq", "S_pro"): 0.4, + ("R25", "Liq", "S_ac"): 0.4, + ("R25", "Liq", "S_IC"): -0.003118 * mw_c, + ("R25", "Liq", "X_PHA"): -1, + } + + assert len(model.rparams.rate_reaction_stoichiometry) == 25 * 32 + for i, v in model.rparams.rate_reaction_stoichiometry.items(): + + assert i[0] in [ + "R1", + "R2", + "R3", + "R4", + "R5", + "R6", + "R7", + "R8", + "R9", + "R10", + "R11", + "R12", + "R13", + "R14", + "R15", + "R16", + "R17", + "R18", + "R19", + "R20", + "R21", + "R22", + "R23", + "R24", + "R25", + ] + if i in stoic: + assert pytest.approx(stoic[i], rel=1e-2) == value(v) + else: + assert pytest.approx(value(v), rel=1e-2) == 0 + + assert isinstance(model.rparams.Z_h2s, Param) + assert isinstance(model.rparams.f_xi_xb, Var) + assert value(model.rparams.f_xi_xb) == 0.1 + assert isinstance(model.rparams.f_ch_xb, Var) + assert value(model.rparams.f_ch_xb) == 0.275 + assert isinstance(model.rparams.f_li_xb, Var) + assert value(model.rparams.f_li_xb) == 0.35 + assert isinstance(model.rparams.f_pr_xb, Var) + assert value(model.rparams.f_pr_xb) == 0.275 + assert isinstance(model.rparams.f_si_xb, Var) + assert value(model.rparams.f_si_xb) == 0 + + assert isinstance(model.rparams.f_fa_li, Var) + assert value(model.rparams.f_fa_li) == 0.95 + assert isinstance(model.rparams.f_h2_su, Var) + assert value(model.rparams.f_h2_su) == 0.1906 + assert isinstance(model.rparams.f_bu_su, Var) + assert value(model.rparams.f_bu_su) == 0.1328 + assert isinstance(model.rparams.f_pro_su, Var) + assert value(model.rparams.f_pro_su) == 0.2691 + assert isinstance(model.rparams.f_ac_su, Var) + assert value(model.rparams.f_ac_su) == 0.4076 + assert isinstance(model.rparams.f_h2_aa, Var) + assert value(model.rparams.f_h2_aa) == 0.06 + assert isinstance(model.rparams.f_va_aa, Var) + assert value(model.rparams.f_va_aa) == 0.23 + assert isinstance(model.rparams.f_bu_aa, Var) + assert value(model.rparams.f_bu_aa) == 0.26 + assert isinstance(model.rparams.f_pro_aa, Var) + assert value(model.rparams.f_pro_aa) == 0.05 + assert isinstance(model.rparams.f_ac_aa, Var) + assert value(model.rparams.f_ac_aa) == 0.40 + + assert isinstance(model.rparams.Y_su, Var) + assert value(model.rparams.Y_su) == 0.1 + assert isinstance(model.rparams.Y_aa, Var) + assert value(model.rparams.Y_aa) == 0.08 + assert isinstance(model.rparams.Y_fa, Var) + assert value(model.rparams.Y_fa) == 0.06 + assert isinstance(model.rparams.Y_c4, Var) + assert value(model.rparams.Y_c4) == 0.06 + assert isinstance(model.rparams.Y_pro, Var) + assert value(model.rparams.Y_pro) == 0.04 + assert isinstance(model.rparams.Y_ac, Var) + assert value(model.rparams.Y_ac) == 0.05 + assert isinstance(model.rparams.Y_h2, Var) + assert value(model.rparams.Y_h2) == 0.06 + + assert isinstance(model.rparams.k_hyd_ch, Var) + assert value(model.rparams.k_hyd_ch) == 10 + assert isinstance(model.rparams.k_hyd_pr, Var) + assert value(model.rparams.k_hyd_pr) == 10 + assert isinstance(model.rparams.k_hyd_li, Var) + assert value(model.rparams.k_hyd_li) == 10 + assert isinstance(model.rparams.K_S_IN, Var) + assert value(model.rparams.K_S_IN) == 1e-4 + assert isinstance(model.rparams.k_m_su, Var) + assert value(model.rparams.k_m_su) == 30 + assert isinstance(model.rparams.K_S_su, Var) + assert value(model.rparams.K_S_su) == 0.5 + + assert isinstance(model.rparams.pH_UL_aa, Var) + assert value(model.rparams.pH_UL_aa) == 5.5 + assert isinstance(model.rparams.pH_LL_aa, Var) + assert value(model.rparams.pH_LL_aa) == 4 + + assert isinstance(model.rparams.k_m_aa, Var) + assert value(model.rparams.k_m_aa) == 50 + assert isinstance(model.rparams.K_S_aa, Var) + assert value(model.rparams.K_S_aa) == 0.3 + assert isinstance(model.rparams.k_m_fa, Var) + assert value(model.rparams.k_m_fa) == 6 + assert isinstance(model.rparams.K_S_fa, Var) + assert value(model.rparams.K_S_fa) == 0.4 + assert isinstance(model.rparams.K_I_h2_fa, Var) + assert value(model.rparams.K_I_h2_fa) == 5e-6 + assert isinstance(model.rparams.k_m_c4, Var) + assert value(model.rparams.k_m_c4) == 20 + assert isinstance(model.rparams.K_S_c4, Var) + assert value(model.rparams.K_S_c4) == 0.2 + assert isinstance(model.rparams.K_I_h2_c4, Var) + assert value(model.rparams.K_I_h2_c4) == 1e-5 + assert isinstance(model.rparams.k_m_pro, Var) + assert value(model.rparams.k_m_pro) == 13 + assert isinstance(model.rparams.K_S_pro, Var) + assert value(model.rparams.K_S_pro) == 0.1 + assert isinstance(model.rparams.K_I_h2_pro, Var) + assert value(model.rparams.K_I_h2_pro) == 3.5e-6 + assert isinstance(model.rparams.k_m_ac, Var) + assert value(model.rparams.k_m_ac) == 8 + assert isinstance(model.rparams.K_S_ac, Var) + assert value(model.rparams.K_S_ac) == 0.15 + assert isinstance(model.rparams.K_I_nh3, Var) + assert value(model.rparams.K_I_nh3) == 0.0018 + + assert isinstance(model.rparams.pH_UL_ac, Var) + assert value(model.rparams.pH_UL_ac) == 7 + assert isinstance(model.rparams.pH_LL_ac, Var) + assert value(model.rparams.pH_LL_ac) == 6 + + assert isinstance(model.rparams.k_m_h2, Var) + assert value(model.rparams.k_m_h2) == 35 + assert isinstance(model.rparams.K_S_h2, Var) + assert value(model.rparams.K_S_h2) == 7e-6 + + assert isinstance(model.rparams.pH_UL_h2, Var) + assert value(model.rparams.pH_UL_h2) == 6 + assert isinstance(model.rparams.pH_LL_h2, Var) + assert value(model.rparams.pH_LL_h2) == 5 + + assert isinstance(model.rparams.k_dec_X_su, Var) + assert value(model.rparams.k_dec_X_su) == 0.02 + assert isinstance(model.rparams.k_dec_X_aa, Var) + assert value(model.rparams.k_dec_X_aa) == 0.02 + assert isinstance(model.rparams.k_dec_X_fa, Var) + assert value(model.rparams.k_dec_X_fa) == 0.02 + assert isinstance(model.rparams.k_dec_X_c4, Var) + assert value(model.rparams.k_dec_X_c4) == 0.02 + assert isinstance(model.rparams.k_dec_X_pro, Var) + assert value(model.rparams.k_dec_X_pro) == 0.02 + assert isinstance(model.rparams.k_dec_X_ac, Var) + assert value(model.rparams.k_dec_X_ac) == 0.02 + assert isinstance(model.rparams.k_dec_X_h2, Var) + assert value(model.rparams.k_dec_X_h2) == 0.02 + + assert isinstance(model.rparams.K_a_va, Var) + assert value(model.rparams.K_a_va) == 1.38e-5 + assert isinstance(model.rparams.K_a_bu, Var) + assert value(model.rparams.K_a_bu) == 1.5e-5 + assert isinstance(model.rparams.K_a_pro, Var) + assert value(model.rparams.K_a_pro) == 1.32e-5 + assert isinstance(model.rparams.K_a_ac, Var) + assert value(model.rparams.K_a_ac) == 1.74e-5 + + assert isinstance(model.rparams.K_I_h2s_ac, Var) + assert value(model.rparams.K_I_h2s_ac) == 460e-3 + assert isinstance(model.rparams.K_I_h2s_c4, Var) + assert value(model.rparams.K_I_h2s_c4) == 481e-3 + assert isinstance(model.rparams.K_I_h2s_h2, Var) + assert value(model.rparams.K_I_h2s_h2) == 400e-3 + assert isinstance(model.rparams.K_I_h2s_pro, Var) + assert value(model.rparams.K_I_h2s_pro) == 481e-3 + assert isinstance(model.rparams.K_S_IP, Var) + assert value(model.rparams.K_S_IP) == 2e-5 + + assert isinstance(model.rparams.b_PAO, Var) + assert value(model.rparams.b_PAO) == 0.2 + assert isinstance(model.rparams.b_PHA, Var) + assert value(model.rparams.b_PHA) == 0.2 + assert isinstance(model.rparams.b_PP, Var) + assert value(model.rparams.b_PP) == 0.2 + + assert isinstance(model.rparams.f_ac_PHA, Var) + assert value(model.rparams.f_ac_PHA) == 0.4 + assert isinstance(model.rparams.f_bu_PHA, Var) + assert value(model.rparams.f_bu_PHA) == 0.1 + assert isinstance(model.rparams.f_pro_PHA, Var) + assert value(model.rparams.f_pro_PHA) == 0.4 + assert isinstance(model.rparams.f_va_PHA, Var) + assert value(model.rparams.f_va_PHA) == 0.1 + + assert isinstance(model.rparams.K_A, Var) + assert value(model.rparams.K_A) == 4e-3 + assert isinstance(model.rparams.K_PP, Var) + assert value(model.rparams.K_PP) == 0.32e-3 + assert isinstance(model.rparams.q_PHA, Var) + assert value(model.rparams.q_PHA) == 3 + assert isinstance(model.rparams.Y_PO4, Var) + assert value(model.rparams.Y_PO4) == 12.903e-3 + assert isinstance(model.rparams.K_XPP, Var) + assert value(model.rparams.K_XPP) == 1 / 3 + assert isinstance(model.rparams.Mg_XPP, Var) + assert value(model.rparams.Mg_XPP) == 1 / 3 + + +class TestReactionBlock(object): + @pytest.fixture(scope="class") + def model(self): + model = ConcreteModel() + model.pparams = ModifiedADM1ParameterBlock() + model.vparams = ADM1_vaporParameterBlock() + model.rparams = ModifiedADM1ReactionParameterBlock( + property_package=model.pparams + ) + + model.props = model.pparams.build_state_block([1]) + model.props_vap = model.vparams.build_state_block([1]) + model.rxns = model.rparams.build_reaction_block([1], state_block=model.props) + + return model + + @pytest.mark.unit + def test_build(self, model): + assert model.rxns[1].conc_mass_comp_ref is model.props[1].conc_mass_comp + + @pytest.mark.unit + def test_rxn_rate(self, model): + assert isinstance(model.rxns[1].reaction_rate, Var) + assert len(model.rxns[1].reaction_rate) == 25 + assert isinstance(model.rxns[1].rate_expression, Constraint) + assert len(model.rxns[1].rate_expression) == 25 + + @pytest.mark.unit + def test_get_reaction_rate_basis(self, model): + assert model.rxns[1].get_reaction_rate_basis() == MaterialFlowBasis.mass + + @pytest.mark.component + def test_initialize(self, model): + assert model.rxns.initialize() is None + + @pytest.mark.unit + def check_units(self, model): + assert_units_consistent(model) + + +class TestReactor: + @pytest.fixture(scope="class") + def model(self): + m = ConcreteModel() + + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.props = ModifiedADM1ParameterBlock() + m.fs.props_vap = ADM1_vaporParameterBlock() + m.fs.rxn_props = ModifiedADM1ReactionParameterBlock(property_package=m.fs.props) + + m.fs.unit = AD( + liquid_property_package=m.fs.props, + vapor_property_package=m.fs.props_vap, + reaction_package=m.fs.rxn_props, + has_heat_transfer=True, + has_pressure_change=False, + ) + + # Feed conditions based on mass balance in Flores-Alsina, where 0 terms are expressed as 1e-9 + m.fs.unit.inlet.flow_vol.fix(0.001967593) # Double check this value + m.fs.unit.inlet.temperature.fix(308.15) + m.fs.unit.inlet.pressure.fix(101325) + + m.fs.unit.inlet.conc_mass_comp[0, "S_su"].fix(0.034597) + m.fs.unit.inlet.conc_mass_comp[0, "S_aa"].fix(0.015037) + m.fs.unit.inlet.conc_mass_comp[0, "S_fa"].fix(1e-6) + m.fs.unit.inlet.conc_mass_comp[0, "S_va"].fix(1e-6) + m.fs.unit.inlet.conc_mass_comp[0, "S_bu"].fix(1e-6) + m.fs.unit.inlet.conc_mass_comp[0, "S_pro"].fix(1e-6) + m.fs.unit.inlet.conc_mass_comp[0, "S_ac"].fix(0.025072) + m.fs.unit.inlet.conc_mass_comp[0, "S_h2"].fix(1e-6) + m.fs.unit.inlet.conc_mass_comp[0, "S_ch4"].fix(1e-6) + m.fs.unit.inlet.conc_mass_comp[0, "S_IC"].fix(0.34628) + m.fs.unit.inlet.conc_mass_comp[0, "S_IN"].fix(0.60014) + m.fs.unit.inlet.conc_mass_comp[0, "S_IP"].fix(0.22677) + m.fs.unit.inlet.conc_mass_comp[0, "S_I"].fix(0.026599) + + m.fs.unit.inlet.conc_mass_comp[0, "X_ch"].fix(7.3687) + m.fs.unit.inlet.conc_mass_comp[0, "X_pr"].fix(7.7308) + m.fs.unit.inlet.conc_mass_comp[0, "X_li"].fix(10.3288) + m.fs.unit.inlet.conc_mass_comp[0, "X_su"].fix(1e-6) + m.fs.unit.inlet.conc_mass_comp[0, "X_aa"].fix(1e-6) + m.fs.unit.inlet.conc_mass_comp[0, "X_fa"].fix(1e-6) + m.fs.unit.inlet.conc_mass_comp[0, "X_c4"].fix(1e-6) + m.fs.unit.inlet.conc_mass_comp[0, "X_pro"].fix(1e-6) + m.fs.unit.inlet.conc_mass_comp[0, "X_ac"].fix(1e-6) + m.fs.unit.inlet.conc_mass_comp[0, "X_h2"].fix(1e-6) + m.fs.unit.inlet.conc_mass_comp[0, "X_I"].fix(12.7727) + m.fs.unit.inlet.conc_mass_comp[0, "X_PHA"].fix(0.0022493) + m.fs.unit.inlet.conc_mass_comp[0, "X_PP"].fix(1.04110) + m.fs.unit.inlet.conc_mass_comp[0, "X_PAO"].fix(3.4655) + m.fs.unit.inlet.conc_mass_comp[0, "S_K"].fix(0.02268) + m.fs.unit.inlet.conc_mass_comp[0, "S_Mg"].fix(0.02893) + + m.fs.unit.inlet.cations[0].fix(0.04) + m.fs.unit.inlet.anions[0].fix(0.02) + + m.fs.unit.volume_liquid.fix(3400) + m.fs.unit.volume_vapor.fix(300) + m.fs.unit.liquid_outlet.temperature.fix(308.15) + + return m + + @pytest.mark.unit + def test_dof(self, model): + assert degrees_of_freedom(model) == 0 + + @pytest.mark.unit + def test_unit_consistency(self, model): + assert_units_consistent(model) == 0 + + @pytest.mark.unit + def test_scaling_factors(self, model): + m = model + iscale.calculate_scaling_factors(m) + + # check that all variables have scaling factors + unscaled_var_list = list(iscale.unscaled_variables_generator(m)) + assert len(unscaled_var_list) == 0 + + # TODO: resolving "badly scaled vars" in this case doesn't help resolve; revisit scaling + # for _ in iscale.badly_scaled_var_generator(m): + # assert False + + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + @pytest.mark.requires_idaes_solver + def test_initialize(self, model): + initialization_tester(model) + + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + @pytest.mark.requires_idaes_solver + def test_solve(self, model): + solver = get_solver(options={"bound_push": 1e-8}) + results = solver.solve(model, tee=True) + model.display() + print(large_residuals_set(model)) + + assert check_optimal_termination(results) + + # TO DO: retest after conversion changes + @pytest.mark.component + @pytest.mark.requires_idaes_solver + def test_solution(self, model): + assert value(model.fs.unit.liquid_outlet.flow_vol[0]) == pytest.approx( + 0.0019675, rel=1e-2 + ) + assert value(model.fs.unit.liquid_outlet.temperature[0]) == pytest.approx( + 308.15, rel=1e-2 + ) + assert value(model.fs.unit.liquid_outlet.pressure[0]) == pytest.approx( + 101325, rel=1e-2 + ) + assert value( + model.fs.unit.liquid_outlet.conc_mass_comp[0, "S_su"] + ) == pytest.approx(8.744124, rel=1e-2) + assert value( + model.fs.unit.liquid_outlet.conc_mass_comp[0, "S_aa"] + ) == pytest.approx(5.31e-3, rel=1e-2) + assert value( + model.fs.unit.liquid_outlet.conc_mass_comp[0, "S_fa"] + ) == pytest.approx(10.7453, rel=1e-2) + assert value( + model.fs.unit.liquid_outlet.conc_mass_comp[0, "S_va"] + ) == pytest.approx(0.015746, rel=1e-2) + assert value( + model.fs.unit.liquid_outlet.conc_mass_comp[0, "S_bu"] + ) == pytest.approx(0.0163891, rel=1e-2) + assert value( + model.fs.unit.liquid_outlet.conc_mass_comp[0, "S_pro"] + ) == pytest.approx(0.0356794, rel=1e-2) + assert value( + model.fs.unit.liquid_outlet.conc_mass_comp[0, "S_ac"] + ) == pytest.approx(0.0431179, rel=1e-2) + assert value( + model.fs.unit.liquid_outlet.conc_mass_comp[0, "S_h2"] + ) == pytest.approx(0.0112939, rel=1e-2) + assert value( + model.fs.unit.liquid_outlet.conc_mass_comp[0, "S_ch4"] + ) == pytest.approx(3.28736e-7, abs=1e-5) + assert value( + model.fs.unit.liquid_outlet.conc_mass_comp[0, "S_IC"] + ) == pytest.approx(0.09373 * 12, rel=1e-2) + assert value( + model.fs.unit.liquid_outlet.conc_mass_comp[0, "S_IN"] + ) == pytest.approx(0.11650 * 14, rel=1e-2) + assert value( + model.fs.unit.liquid_outlet.conc_mass_comp[0, "S_IP"] + ) == pytest.approx(29.11478, rel=1e-2) + assert value( + model.fs.unit.liquid_outlet.conc_mass_comp[0, "S_I"] + ) == pytest.approx(0.02660, rel=1e-2) + assert value( + model.fs.unit.liquid_outlet.conc_mass_comp[0, "X_ch"] + ) == pytest.approx(0.0407196, rel=1e-2) + assert value( + model.fs.unit.liquid_outlet.conc_mass_comp[0, "X_pr"] + ) == pytest.approx(0.0424521, rel=1e-2) + assert value( + model.fs.unit.liquid_outlet.conc_mass_comp[0, "X_li"] + ) == pytest.approx(0.0565536, rel=1e-2) + assert value( + model.fs.unit.liquid_outlet.conc_mass_comp[0, "X_su"] + ) == pytest.approx(8.369087e-10, abs=1e-5) + assert value( + model.fs.unit.liquid_outlet.conc_mass_comp[0, "X_aa"] + ) == pytest.approx(0.486516, rel=1e-2) + assert value( + model.fs.unit.liquid_outlet.conc_mass_comp[0, "X_fa"] + ) == pytest.approx(4.35411e-6, abs=1e-5) + assert value( + model.fs.unit.liquid_outlet.conc_mass_comp[0, "X_c4"] + ) == pytest.approx(4.0393379e-6, abs=1e-5) + assert value( + model.fs.unit.liquid_outlet.conc_mass_comp[0, "X_pro"] + ) == pytest.approx(4.18807e-6, abs=1e-5) + assert value( + model.fs.unit.liquid_outlet.conc_mass_comp[0, "X_ac"] + ) == pytest.approx(4.274973e-6, abs=1e-5) + assert value( + model.fs.unit.liquid_outlet.conc_mass_comp[0, "X_h2"] + ) == pytest.approx(1.378092e-8, abs=1e-5) + assert value( + model.fs.unit.liquid_outlet.conc_mass_comp[0, "X_I"] + ) == pytest.approx(13.0695, rel=1e-2) + assert value( + model.fs.unit.liquid_outlet.conc_mass_comp[0, "X_PHA"] + ) == pytest.approx(7.2792898, rel=1e-2) + assert value( + model.fs.unit.liquid_outlet.conc_mass_comp[0, "X_PP"] + ) == pytest.approx(0.1143025, rel=1e-2) + assert value( + model.fs.unit.liquid_outlet.conc_mass_comp[0, "X_PAO"] + ) == pytest.approx(0.69310, rel=1e-2) + assert value( + model.fs.unit.liquid_outlet.conc_mass_comp[0, "S_K"] + ) == pytest.approx(0.33162, rel=1e-2) + assert value( + model.fs.unit.liquid_outlet.conc_mass_comp[0, "S_Mg"] + ) == pytest.approx(0.33787, rel=1e-2) + assert value(model.fs.unit.liquid_outlet.anions[0]) == pytest.approx( + 2e-2, rel=1e-2 + ) + assert value(model.fs.unit.liquid_outlet.cations[0]) == pytest.approx( + 4e-2, rel=1e-2 + ) + + assert value( + model.fs.unit.liquid_phase.reactions[0].conc_mass_va + ) == pytest.approx(0.0157436, rel=1e-2) + assert value( + model.fs.unit.liquid_phase.reactions[0].conc_mass_bu + ) == pytest.approx(0.0163960, rel=1e-2) + assert value( + model.fs.unit.liquid_phase.reactions[0].conc_mass_ac + ) == pytest.approx(0.0431131, rel=1e-2) + assert value( + model.fs.unit.liquid_phase.reactions[0].conc_mass_pro + ) == pytest.approx(0.03567417, rel=1e-2) + assert value( + model.fs.unit.liquid_phase.reactions[0].conc_mol_hco3 + ) == pytest.approx(0.09336803, rel=1e-2) + assert value( + model.fs.unit.liquid_phase.reactions[0].conc_mol_nh3 + ) == pytest.approx(0.04271418, rel=1e-2) + assert value( + model.fs.unit.liquid_phase.reactions[0].conc_mol_co2 + ) == pytest.approx(0.00036273, rel=1e-2) + assert value( + model.fs.unit.liquid_phase.reactions[0].conc_mol_nh4 + ) == pytest.approx(0.07378798, rel=1e-2) + assert value( + model.fs.unit.liquid_phase.reactions[0].conc_mol_Mg + ) == pytest.approx(0.00038049, rel=1e-2) + assert value( + model.fs.unit.liquid_phase.reactions[0].conc_mol_K + ) == pytest.approx(0.00038049, rel=1e-2) + + assert value(model.fs.unit.liquid_phase.reactions[0].S_H) == pytest.approx( + 1.9180052e-9, rel=1e-2 + ) + assert value(model.fs.unit.liquid_phase.reactions[0].S_OH) == pytest.approx( + 1.08364612e-5, rel=1e-2 + ) + assert value(model.fs.unit.liquid_phase.reactions[0].KW, Var) == pytest.approx( + 2.08e-14, rel=1e-2 + ) + assert value( + model.fs.unit.liquid_phase.reactions[0].K_a_co2, Var + ) == pytest.approx(4.94e-7, rel=1e-2) + assert value( + model.fs.unit.liquid_phase.reactions[0].K_a_IN, Var + ) == pytest.approx(1.11e-9, rel=1e-2) diff --git a/watertap/property_models/anaerobic_digestion/tests/test_modified_adm1_thermo.py b/watertap/property_models/anaerobic_digestion/tests/test_modified_adm1_thermo.py new file mode 100644 index 0000000000..5e6b413bac --- /dev/null +++ b/watertap/property_models/anaerobic_digestion/tests/test_modified_adm1_thermo.py @@ -0,0 +1,284 @@ +################################################################################# +# WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# National Renewable Energy Laboratory, and National Energy Technology +# Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +################################################################################# +""" +Tests for ADM1 thermo property package. +Authors: Chenyu Wang, Marcus Holly +""" + +import pytest + +from pyomo.environ import ConcreteModel, Param, value, Var +from pyomo.util.check_units import assert_units_consistent + +from idaes.core import MaterialBalanceType, EnergyBalanceType, MaterialFlowBasis + +from watertap.property_models.anaerobic_digestion.modified_adm1_properties import ( + ModifiedADM1ParameterBlock, + ModifiedADM1StateBlock, +) +from idaes.core.util.model_statistics import ( + fixed_variables_set, + activated_constraints_set, +) + +from idaes.core.solvers import get_solver + + +# ----------------------------------------------------------------------------- +# Get default solver for testing +solver = get_solver() + + +class TestParamBlock(object): + @pytest.fixture(scope="class") + def model(self): + model = ConcreteModel() + model.params = ModifiedADM1ParameterBlock() + + return model + + @pytest.mark.unit + def test_build(self, model): + assert model.params.state_block_class is ModifiedADM1StateBlock + + assert len(model.params.phase_list) == 1 + for i in model.params.phase_list: + assert i == "Liq" + + assert len(model.params.component_list) == 32 + for i in model.params.component_list: + assert i in [ + "H2O", + "S_su", + "S_aa", + "S_fa", + "S_va", + "S_bu", + "S_pro", + "S_ac", + "S_h2", + "S_ch4", + "S_IC", + "S_IN", + "S_IP", + "S_I", + "X_ch", + "X_pr", + "X_li", + "X_su", + "X_aa", + "X_fa", + "X_c4", + "X_pro", + "X_ac", + "X_h2", + "X_I", + "X_PHA", + "X_PP", + "X_PAO", + "S_K", + "S_Mg", + "S_cat", + "S_an", + ] + + assert isinstance(model.params.cp_mass, Param) + assert value(model.params.cp_mass) == 4182 + + assert isinstance(model.params.dens_mass, Param) + assert value(model.params.dens_mass) == 997 + + assert isinstance(model.params.pressure_ref, Param) + assert value(model.params.pressure_ref) == 101325 + + assert isinstance(model.params.temperature_ref, Param) + assert value(model.params.temperature_ref) == 298.15 + + +class TestStateBlock(object): + @pytest.fixture(scope="class") + def model(self): + model = ConcreteModel() + model.params = ModifiedADM1ParameterBlock() + + model.props = model.params.build_state_block([1]) + + return model + + @pytest.mark.unit + def test_build(self, model): + assert isinstance(model.props[1].flow_vol, Var) + assert value(model.props[1].flow_vol) == 1 + + assert isinstance(model.props[1].pressure, Var) + assert value(model.props[1].pressure) == 101325 + + assert isinstance(model.props[1].temperature, Var) + assert value(model.props[1].temperature) == 298.15 + + assert isinstance(model.props[1].anions, Var) + assert value(model.props[1].anions) == 0.02 + + assert isinstance(model.props[1].cations, Var) + assert value(model.props[1].cations) == 0.04 + + assert isinstance(model.props[1].conc_mass_comp, Var) + + assert len(model.props[1].conc_mass_comp) == 29 + for i in model.props[1].conc_mass_comp: + assert i in [ + "S_su", + "S_aa", + "S_fa", + "S_va", + "S_bu", + "S_pro", + "S_ac", + "S_h2", + "S_ch4", + "S_IC", + "S_IN", + "S_IP", + "S_I", + "X_ch", + "X_pr", + "X_li", + "X_su", + "X_aa", + "X_fa", + "X_c4", + "X_pro", + "X_ac", + "X_h2", + "X_I", + "X_PHA", + "X_PP", + "X_PAO", + "S_K", + "S_Mg", + ] + assert value(model.props[1].conc_mass_comp[i]) == 0.001 + + @pytest.mark.unit + def test_get_material_flow_terms(self, model): + for p in model.params.phase_list: + for j in model.params.component_list: + assert model.props[1].get_material_flow_terms(p, j) is ( + model.props[1].material_flow_expression[j] + ) + + @pytest.mark.unit + def test_get_enthalpy_flow_terms(self, model): + for p in model.params.phase_list: + assert model.props[1].get_enthalpy_flow_terms(p) is ( + model.props[1].enthalpy_flow_expression + ) + + @pytest.mark.unit + def test_get_material_density_terms(self, model): + for p in model.params.phase_list: + for j in model.params.component_list: + assert model.props[1].get_material_density_terms(p, j) is ( + model.props[1].material_density_expression[j] + ) + + @pytest.mark.unit + def test_get_energy_density_terms(self, model): + for p in model.params.phase_list: + assert model.props[1].get_energy_density_terms(p) is ( + model.props[1].energy_density_expression + ) + + @pytest.mark.unit + def test_default_material_balance_type(self, model): + assert ( + model.props[1].default_material_balance_type() + == MaterialBalanceType.componentPhase + ) + + @pytest.mark.unit + def test_default_energy_balance_type(self, model): + assert ( + model.props[1].default_energy_balance_type() + == EnergyBalanceType.enthalpyTotal + ) + + @pytest.mark.unit + def test_get_material_flow_basis(self, model): + assert model.props[1].get_material_flow_basis() == MaterialFlowBasis.mass + + @pytest.mark.unit + def test_define_state_vars(self, model): + sv = model.props[1].define_state_vars() + + assert len(sv) == 6 + for i in sv: + assert i in [ + "flow_vol", + "pressure", + "temperature", + "conc_mass_comp", + "anions", + "cations", + ] + + @pytest.mark.unit + def test_define_port_members(self, model): + sv = model.props[1].define_state_vars() + + assert len(sv) == 6 + for i in sv: + assert i in [ + "flow_vol", + "pressure", + "temperature", + "conc_mass_comp", + "anions", + "cations", + ] + + @pytest.mark.unit + def test_define_display_vars(self, model): + sv = model.props[1].define_display_vars() + + assert len(sv) == 6 + for i in sv: + assert i in [ + "Volumetric Flowrate", + "Molar anions", + "Molar cations", + "Mass Concentration", + "Temperature", + "Pressure", + ] + + @pytest.mark.component + def test_initialize(self, model): + orig_fixed_vars = fixed_variables_set(model) + orig_act_consts = activated_constraints_set(model) + + model.props.initialize(hold_state=False) + + fin_fixed_vars = fixed_variables_set(model) + fin_act_consts = activated_constraints_set(model) + + assert len(fin_act_consts) == len(orig_act_consts) + assert len(fin_fixed_vars) == len(orig_fixed_vars) + + for c in fin_act_consts: + assert c in orig_act_consts + for v in fin_fixed_vars: + assert v in orig_fixed_vars + + @pytest.mark.unit + def check_units(self, model): + assert_units_consistent(model) diff --git a/watertap/property_models/multicomp_aq_sol_prop_pack.py b/watertap/property_models/multicomp_aq_sol_prop_pack.py index ba5c38a968..cac12e50d9 100644 --- a/watertap/property_models/multicomp_aq_sol_prop_pack.py +++ b/watertap/property_models/multicomp_aq_sol_prop_pack.py @@ -54,7 +54,7 @@ MaterialBalanceType, EnergyBalanceType, ) -from idaes.core.base.components import Component, Ion, Solute, Solvent, Cation, Anion +from idaes.core.base.components import Solute, Solvent, Cation, Anion from idaes.core.base.phases import AqueousPhase from idaes.core.util.constants import Constants from idaes.core.util.initialization import ( diff --git a/watertap/examples/flowsheets/full_treatment_train/model_components/seawater_ion_prop_pack.py b/watertap/property_models/seawater_ion_prop_pack.py similarity index 100% rename from watertap/examples/flowsheets/full_treatment_train/model_components/seawater_ion_prop_pack.py rename to watertap/property_models/seawater_ion_prop_pack.py diff --git a/watertap/property_models/selective_oil_permeation_prop_pack.py b/watertap/property_models/selective_oil_permeation_prop_pack.py index 7c734aafdd..b55e47abc4 100644 --- a/watertap/property_models/selective_oil_permeation_prop_pack.py +++ b/watertap/property_models/selective_oil_permeation_prop_pack.py @@ -15,7 +15,7 @@ Simple property package for selective oil permeation """ -from pyomo.environ import Constraint, Var, Param, NonNegativeReals, Suffix, value +from pyomo.environ import Constraint, Var, Param, NonNegativeReals, Suffix from pyomo.environ import units # Import IDAES cores diff --git a/watertap/property_models/water_prop_pack.py b/watertap/property_models/water_prop_pack.py index 812e7b1043..cd2f9bba56 100644 --- a/watertap/property_models/water_prop_pack.py +++ b/watertap/property_models/water_prop_pack.py @@ -42,7 +42,7 @@ MaterialBalanceType, EnergyBalanceType, ) -from idaes.core.base.components import Component, Solute, Solvent +from idaes.core.base.components import Component from idaes.core.base.phases import LiquidPhase, VaporPhase from idaes.core.util.initialization import ( fix_state_vars, diff --git a/watertap/tools/parameter_sweep/parameter_sweep.py b/watertap/tools/parameter_sweep/parameter_sweep.py index d934a1ae7c..76bae6852e 100644 --- a/watertap/tools/parameter_sweep/parameter_sweep.py +++ b/watertap/tools/parameter_sweep/parameter_sweep.py @@ -473,6 +473,11 @@ def _param_sweep_kernel(self, model, reinitialize_values): results = optimize_function(model, **optimize_kwargs) pyo.assert_optimal_termination(results) + except TypeError: + # this happens if the optimize_kwargs are misspecified, + # which is an error we want to raise + raise + except: # run_successful remains false. We try to reinitialize and solve again if reinitialize_function is not None: @@ -485,6 +490,11 @@ def _param_sweep_kernel(self, model, reinitialize_values): results = optimize_function(model, **optimize_kwargs) pyo.assert_optimal_termination(results) + except TypeError: + # this happens if the reinitialize_kwargs are misspecified, + # which is an error we want to raise + raise + except: pass # run_successful is still False else: diff --git a/watertap/tools/parameter_sweep/tests/test_differential_parameter_sweep.py b/watertap/tools/parameter_sweep/tests/test_differential_parameter_sweep.py index 64b8e1abfc..02e58aa1e3 100644 --- a/watertap/tools/parameter_sweep/tests/test_differential_parameter_sweep.py +++ b/watertap/tools/parameter_sweep/tests/test_differential_parameter_sweep.py @@ -16,8 +16,6 @@ import numpy as np import pyomo.environ as pyo -from pyomo.environ import value - from watertap.tools.parameter_sweep.sampling_types import ( NormalSample, GeomSample, diff --git a/watertap/tools/parameter_sweep/tests/test_parameter_sweep.py b/watertap/tools/parameter_sweep/tests/test_parameter_sweep.py index 00796b6ea1..59146d0b15 100644 --- a/watertap/tools/parameter_sweep/tests/test_parameter_sweep.py +++ b/watertap/tools/parameter_sweep/tests/test_parameter_sweep.py @@ -825,6 +825,41 @@ def test_parameter_sweep_optimize(self, model, tmp_path): _assert_dictionary_correctness(truth_dict, read_dict) _assert_h5_csv_agreement(csv_results_file_name, read_dict) + @pytest.mark.component + def test_parameter_sweep_bad_reinitialize_call_2(self, model, tmp_path): + comm = MPI.COMM_WORLD + + tmp_path = _get_rank0_path(comm, tmp_path) + results_fname = os.path.join(tmp_path, "global_results") + csv_results_file_name = str(results_fname) + ".csv" + h5_results_file_name = str(results_fname) + ".h5" + + ps = ParameterSweep( + optimize_function=_optimization, + reinitialize_function=_reinitialize, + reinitialize_kwargs={"slack_penalty": 10.0, "foo": "bar"}, + csv_results_file_name=csv_results_file_name, + h5_results_file_name=h5_results_file_name, + debugging_data_dir=tmp_path, + interpolate_nan_outputs=True, + ) + + m = model + m.fs.slack_penalty = 1000.0 + m.fs.slack.setub(0) + + A = m.fs.input["a"] + B = m.fs.input["b"] + sweep_params = {A.name: (A, 0.1, 0.9, 3), B.name: (B, 0.0, 0.5, 3)} + + with pytest.raises(TypeError): + # Call the parameter_sweep function + _ = ps.parameter_sweep( + m, + sweep_params, + outputs=None, + ) + @pytest.mark.component def test_parameter_sweep_recover(self, model, tmp_path): comm = MPI.COMM_WORLD @@ -1485,6 +1520,58 @@ def test_parameter_sweep_bad_force_initialize(self, model, tmp_path): outputs=None, ) + @pytest.mark.component + def test_parameter_sweep_bad_optimization_call(self, model, tmp_path): + + ps = ParameterSweep( + optimize_function=_optimization, + optimize_kwargs={"foo": "bar"}, + ) + + m = model + m.fs.slack_penalty = 1000.0 + m.fs.slack.setub(0) + + A = m.fs.input["a"] + B = m.fs.input["b"] + sweep_params = {A.name: (A, 0.1, 0.9, 3), B.name: (B, 0.0, 0.5, 3)} + + with pytest.raises(TypeError): + # Call the parameter_sweep function + ps.parameter_sweep( + m, + sweep_params, + outputs=None, + ) + + @pytest.mark.component + def test_parameter_sweep_bad_reinitialize_call(self, model, tmp_path): + def reinit(a=42): + pass + + ps = ParameterSweep( + optimize_function=_optimization, + reinitialize_before_sweep=True, + reinitialize_function=reinit, + reinitialize_kwargs={"foo": "bar"}, + ) + + m = model + m.fs.slack_penalty = 1000.0 + m.fs.slack.setub(0) + + A = m.fs.input["a"] + B = m.fs.input["b"] + sweep_params = {A.name: (A, 0.1, 0.9, 3), B.name: (B, 0.0, 0.5, 3)} + + with pytest.raises(TypeError): + # Call the parameter_sweep function + ps.parameter_sweep( + m, + sweep_params, + outputs=None, + ) + @pytest.mark.component def test_parameter_sweep_probe_fail(self, model, tmp_path): diff --git a/watertap/ui/fsapi.py b/watertap/ui/fsapi.py index 3b87a72bf9..c3222bc784 100644 --- a/watertap/ui/fsapi.py +++ b/watertap/ui/fsapi.py @@ -182,6 +182,7 @@ def set_obj_key_default(cls, v, values): class FlowsheetExport(BaseModel): """A flowsheet and its contained exported model objects.""" + m: object = Field(default=None, exclude=True) obj: object = Field(default=None, exclude=True) name: str = "" description: str = "" @@ -189,6 +190,7 @@ class FlowsheetExport(BaseModel): version: int = 2 requires_idaes_solver: bool = False dof: int = 0 + sweep_results: Union[None, dict] = {} # set name dynamically from object @validator("name", always=True) @@ -474,7 +476,9 @@ def action_wrapper(**kwargs): f"Flowsheet `{Actions.build}` action failed. " f"See logs for details." ) - self.fs_exp.obj = action_result + self.fs_exp.obj = action_result.fs + self.fs_exp.m = action_result + # [re-]create exports (new model object) if Actions.export not in self._actions: raise KeyError( @@ -541,6 +545,7 @@ def export_values(self): """ _log.info("Exporting values from flowsheet model to UI") u = pyo.units + self.fs_exp.dof = degrees_of_freedom(self.fs_exp.obj) for key, mo in self.fs_exp.model_objects.items(): mo.value = pyo.value(u.convert(mo.obj, to_units=mo.ui_units)) if not isinstance( @@ -565,6 +570,7 @@ def export_values(self): tmp = pyo.Var(initialize=mo.obj.lb, units=u.get_units(mo.obj)) tmp.construct() mo.lb = pyo.value(u.convert(tmp, to_units=mo.ui_units)) + mo.fixed = mo.obj.fixed else: mo.has_bounds = False diff --git a/watertap/ui/tests/test_fsapi.py b/watertap/ui/tests/test_fsapi.py index 38c3152004..b8e221d578 100644 --- a/watertap/ui/tests/test_fsapi.py +++ b/watertap/ui/tests/test_fsapi.py @@ -37,6 +37,11 @@ # Fake status=OK solver result +class FAKE_FLOWSHEET: + fs = "fs" + trash = "true" + + class SOLVE_RESULT_OK: class SOLVE_STATUS: status = SolverStatus.ok @@ -47,7 +52,7 @@ class SOLVE_STATUS: def build_ro(**kwargs): model = RO.build_flowsheet(erd_type=ERD_TYPE) - return model.fs + return model def solve_ro(flowsheet=None): @@ -149,7 +154,9 @@ def test_build(): def test_actions(add_variant: str): fsi = flowsheet_interface() built = False - garbage = {"trash": True} + # garbage = {"trash": True} + garbage = FAKE_FLOWSHEET + m = FAKE_FLOWSHEET v1 = Var(name="variable1") v1.construct() v1.value = 1 @@ -158,11 +165,13 @@ def test_actions(add_variant: str): def fake_build(): nonlocal built built = True - return garbage + nonlocal m + m = build_ro() + return m def fake_solve(flowsheet=None): # flowsheet passed in here should be what fake_build() returns - assert flowsheet == garbage + assert flowsheet == m.fs return SOLVE_RESULT_OK def fake_export(flowsheet=None, exports=None): @@ -289,8 +298,8 @@ def test_export_values_build(): def test_empty_solve(): # try a fake solve fsi = flowsheet_interface() - fsi.build() with pytest.raises(RuntimeError) as excinfo: + fsi.build() fsi.solve() print(f"* RuntimeError: {excinfo.value}") diff --git a/watertap/unit_models/__init__.py b/watertap/unit_models/__init__.py index 7d0c4d7b12..ed3d867e8d 100644 --- a/watertap/unit_models/__init__.py +++ b/watertap/unit_models/__init__.py @@ -24,3 +24,6 @@ from .electrodialysis_1D import Electrodialysis1D from .gac import GAC from .ion_exchange_0D import IonExchange0D +from .thickener import Thickener +from .dewatering import DewateringUnit +from .electroNP_ZO import ElectroNPZO diff --git a/watertap/unit_models/anaerobic_digestor.py b/watertap/unit_models/anaerobic_digestor.py index 62819496eb..6e8cb1017e 100644 --- a/watertap/unit_models/anaerobic_digestor.py +++ b/watertap/unit_models/anaerobic_digestor.py @@ -39,6 +39,7 @@ units as pyunits, check_optimal_termination, exp, + Suffix, ) @@ -64,6 +65,7 @@ from idaes.core.util.model_statistics import degrees_of_freedom from idaes.core.util.constants import Constants from idaes.core.util.exceptions import ConfigurationError, InitializationError +from idaes.core.util.tables import create_stream_table_dataframe __author__ = "Alejandro Garciadiego, Andrew Lee" @@ -286,6 +288,8 @@ def build(self): # Call UnitModel.build to setup dynamics super(ADData, self).build() + self.scaling_factor = Suffix(direction=Suffix.EXPORT) + # Check phase lists match assumptions if self.config.vapor_property_package.phase_list != ["Vap"]: raise ConfigurationError( @@ -542,10 +546,9 @@ def H2_Henrys_law_rule(self, t): def outlet_P_rule(self, t): return self.vapor_phase[t].pressure == ( - self.vapor_phase[t].p_w_sat - + sum( - self.vapor_phase[t].p_sat[j] - for j in self.config.vapor_property_package.solute_set + sum( + self.vapor_phase[t].pressure_sat[j] + for j in self.config.vapor_property_package.component_list ) ) @@ -596,7 +599,7 @@ def Sh2_conc_rule(self, t): self.KH_h2[t], to_units=pyunits.kmol / pyunits.m**3 * pyunits.Pa**-1, ) - * self.vapor_phase[t].p_sat["S_h2"] + * self.vapor_phase[t].pressure_sat["S_h2"] ) * self.volume_liquid[t] ) @@ -619,7 +622,7 @@ def Sch4_conc_rule(self, t): self.KH_ch4[t], to_units=pyunits.kmol / pyunits.m**3 * pyunits.Pa**-1, ) - * self.vapor_phase[t].p_sat["S_ch4"] + * self.vapor_phase[t].pressure_sat["S_ch4"] ) * self.volume_liquid[t] ) @@ -639,7 +642,7 @@ def Sco2_conc_rule(self, t): self.KH_co2[t], to_units=pyunits.kmol / pyunits.m**3 * pyunits.Pa**-1, ) - * self.vapor_phase[t].p_sat["S_co2"] + * self.vapor_phase[t].pressure_sat["S_co2"] ) * self.volume_liquid[t] ) * (1 * pyunits.kg / pyunits.kmole) @@ -755,7 +758,18 @@ def rule_energy_balance(self, t): iscale.set_scaling_factor(self.KH_ch4, 1e3) iscale.set_scaling_factor(self.KH_h2, 1e4) + def _get_stream_table_contents(self, time_point=0): + return create_stream_table_dataframe( + { + "Liquid Inlet": self.inlet, + "Liquid Outlet": self.liquid_outlet, + "Vapor Outlet": self.vapor_outlet, + }, + time_point=time_point, + ) + def _get_performance_contents(self, time_point=0): + # TODO: add aggregated quantities/key metrics var_dict = {"Volume": self.volume_AD[time_point]} if hasattr(self, "heat_duty"): var_dict["Heat Duty"] = self.heat_duty[time_point] @@ -772,6 +786,16 @@ def calculate_scaling_factors(self): & self.liquid_phase.properties_out.component_list ) + # TODO: improve this later; for now, this resolved some scaling issues for modified adm1 test file + if "S_IP" in self.config.liquid_property_package.component_list: + iscale.set_scaling_factor(self.liquid_phase.heat, 1e-6) + iscale.set_scaling_factor( + self.liquid_phase.properties_out[0].conc_mass_comp["S_IP"], 1e-5 + ) + iscale.set_scaling_factor( + self.liquid_phase.properties_out[0].conc_mass_comp["S_IN"], 1e-5 + ) + for t, v in self.flow_vol_vap.items(): iscale.constraint_scaling_transform( v, @@ -948,10 +972,14 @@ def initialize_build( init_log.info_high("Initialization Step 2 Complete.") # --------------------------------------------------------------------- - # Solve unit model + # # Solve unit model with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: - results = solverobj.solve(self) - + results = solverobj.solve(self, tee=slc.tee) + if not check_optimal_termination(results): + init_log.warning( + f"Trouble solving unit model {self.name}, trying one more time" + ) + results = solverobj.solve(self, tee=slc.tee) init_log.info_high( "Initialization Step 3 {}.".format(idaeslog.condition(results)) ) diff --git a/watertap/unit_models/crystallizer.py b/watertap/unit_models/crystallizer.py index 0775951a23..dd660b558c 100644 --- a/watertap/unit_models/crystallizer.py +++ b/watertap/unit_models/crystallizer.py @@ -34,7 +34,7 @@ from idaes.core.util.constants import Constants from idaes.core.util.config import is_physical_parameter_block -from idaes.core.util.exceptions import ConfigurationError, InitializationError +from idaes.core.util.exceptions import InitializationError import idaes.core.util.scaling as iscale import idaes.logger as idaeslog diff --git a/watertap/unit_models/cstr_injection.py b/watertap/unit_models/cstr_injection.py index 4ef69a5218..470107450d 100644 --- a/watertap/unit_models/cstr_injection.py +++ b/watertap/unit_models/cstr_injection.py @@ -18,8 +18,7 @@ # Import Pyomo libraries from pyomo.common.config import ConfigBlock, ConfigValue, In, Bool -from pyomo.environ import Reference, Block, Var, Constraint -from pyomo.common.deprecation import deprecated +from pyomo.environ import Reference # Import IDAES cores from idaes.core import ( diff --git a/watertap/unit_models/dewatering.py b/watertap/unit_models/dewatering.py new file mode 100644 index 0000000000..9ed7205b19 --- /dev/null +++ b/watertap/unit_models/dewatering.py @@ -0,0 +1,163 @@ +############################################################################### +# WaterTAP Copyright (c) 2021, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National +# Laboratory, National Renewable Energy Laboratory, and National Energy +# Technology Laboratory (subject to receipt of any required approvals from +# the U.S. Dept. of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +# +############################################################################### +""" +Dewatering unit model for BSM2. Based on IDAES separator unit + +Model based on + +J. Alex, L. Benedetti, J.B. Copp, K.V. Gernaey, U. Jeppsson, +I. Nopens, M.N. Pons, C. Rosen, J.P. Steyer and +P. A. Vanrolleghem +Benchmark Simulation Model no. 2 (BSM2) +""" +# Import IDAES cores +from idaes.core import ( + declare_process_block_class, +) +from idaes.models.unit_models.separator import SeparatorData, SplittingType + +from idaes.core.util.tables import create_stream_table_dataframe +import idaes.logger as idaeslog + +from pyomo.environ import ( + Param, + units as pyunits, + Set, +) + +from idaes.core.util.exceptions import ( + ConfigurationError, +) + +__author__ = "Alejandro Garciadiego" + + +# Set up logger +_log = idaeslog.getLogger(__name__) + + +@declare_process_block_class("DewateringUnit") +class DewateringData(SeparatorData): + """ + Dewatering unit block for BSM2 + """ + + CONFIG = SeparatorData.CONFIG() + CONFIG.outlet_list = ["underflow", "overflow"] + CONFIG.split_basis = SplittingType.componentFlow + + def build(self): + """ + Begin building model. + Args: + None + Returns: + None + """ + + # Call UnitModel.build to set up dynamics + super(DewateringData, self).build() + + if "underflow" and "overflow" not in self.config.outlet_list: + raise ConfigurationError( + "{} encountered unrecognised " + "outlet_list. This should not " + "occur - please use overflow " + "and underflow as outlets.".format(self.name) + ) + + self.p_dewat = Param( + initialize=0.28, + units=pyunits.dimensionless, + mutable=True, + doc="Percentage of suspended solids in the underflow", + ) + + self.TSS_rem = Param( + initialize=0.98, + units=pyunits.dimensionless, + mutable=True, + doc="Percentage of suspended solids removed", + ) + + @self.Expression(self.flowsheet().time, doc="Suspended solid concentration") + def TSS(blk, t): + return 0.75 * ( + blk.inlet.conc_mass_comp[t, "X_I"] + + blk.inlet.conc_mass_comp[t, "X_P"] + + blk.inlet.conc_mass_comp[t, "X_BH"] + + blk.inlet.conc_mass_comp[t, "X_BA"] + + blk.inlet.conc_mass_comp[t, "X_S"] + ) + + @self.Expression(self.flowsheet().time, doc="Dewatering factor") + def f_dewat(blk, t): + return blk.p_dewat * (10 / (blk.TSS[t])) + + @self.Expression(self.flowsheet().time, doc="Remove factor") + def f_q_du(blk, t): + return blk.TSS_rem / (pyunits.kg / pyunits.m**3) / 100 / blk.f_dewat[t] + + self.non_particulate_components = Set( + initialize=[ + "S_I", + "S_S", + "S_O", + "S_NO", + "S_NH", + "S_ND", + "H2O", + "S_ALK", + ] + ) + + self.particulate_components = Set( + initialize=["X_I", "X_S", "X_P", "X_BH", "X_BA", "X_ND"] + ) + + @self.Constraint( + self.flowsheet().time, + self.particulate_components, + doc="particulate fraction", + ) + def overflow_particulate_fraction(blk, t, i): + return blk.split_fraction[t, "overflow", i] == 1 - blk.TSS_rem + + @self.Constraint( + self.flowsheet().time, + self.non_particulate_components, + doc="soluble fraction", + ) + def non_particulate_components(blk, t, i): + return blk.split_fraction[t, "overflow", i] == 1 - blk.f_q_du[t] + + def _get_performance_contents(self, time_point=0): + var_dict = {} + for k in self.split_fraction.keys(): + if k[0] == time_point: + var_dict[f"Split Fraction [{str(k[1:])}]"] = self.split_fraction[k] + return {"vars": var_dict} + + def _get_stream_table_contents(self, time_point=0): + outlet_list = self.create_outlet_list() + + io_dict = {} + if self.config.mixed_state_block is None: + io_dict["Inlet"] = self.mixed_state + else: + io_dict["Inlet"] = self.config.mixed_state_block + + for o in outlet_list: + io_dict[o] = getattr(self, o + "_state") + + return create_stream_table_dataframe(io_dict, time_point=time_point) diff --git a/watertap/unit_models/electroNP_ZO.py b/watertap/unit_models/electroNP_ZO.py new file mode 100644 index 0000000000..4ae8d58dfa --- /dev/null +++ b/watertap/unit_models/electroNP_ZO.py @@ -0,0 +1,503 @@ +################################################################################# +# WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# National Renewable Energy Laboratory, and National Energy Technology +# Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +################################################################################# + +# Import Pyomo libraries +from pyomo.environ import ( + Var, + check_optimal_termination, + Param, + Suffix, + NonNegativeReals, + units as pyunits, +) +from pyomo.common.config import ConfigBlock, ConfigValue, In + +# Import IDAES cores +from idaes.core import ( + declare_process_block_class, + UnitModelBlockData, + useDefault, +) +from idaes.core.solvers import get_solver +from idaes.core.util.tables import create_stream_table_dataframe +from idaes.core.util.config import is_physical_parameter_block +from idaes.core.util.exceptions import ConfigurationError, InitializationError +import idaes.core.util.scaling as iscale +import idaes.logger as idaeslog + +from watertap.core import InitializationMixin + +__author__ = "Chenyu Wang" + +_log = idaeslog.getLogger(__name__) + + +@declare_process_block_class("ElectroNPZO") +class ElectroNPZOData(InitializationMixin, UnitModelBlockData): + """ + Zero order electrochemical nutrient removal (ElectroNP) model based on specified water flux and ion rejection. + """ + + CONFIG = ConfigBlock() + + CONFIG.declare( + "dynamic", + ConfigValue( + domain=In([False]), + default=False, + description="Dynamic model flag - must be False", + doc="""Indicates whether this model will be dynamic or not, + **default** = False. NF units do not support dynamic + behavior.""", + ), + ) + CONFIG.declare( + "has_holdup", + ConfigValue( + default=False, + domain=In([False]), + description="Holdup construction flag - must be False", + doc="""Indicates whether holdup terms should be constructed or not. + **default** - False. NF units do not have defined volume, thus + this must be False.""", + ), + ) + CONFIG.declare( + "property_package", + ConfigValue( + default=useDefault, + domain=is_physical_parameter_block, + description="Property package to use for control volume", + doc="""Property parameter object used to define property calculations, + **default** - useDefault. + **Valid values:** { + **useDefault** - use default package from parent model or flowsheet, + **PhysicalParameterObject** - a PhysicalParameterBlock object.}""", + ), + ) + CONFIG.declare( + "property_package_args", + ConfigBlock( + implicit=True, + description="Arguments to use for constructing property packages", + doc="""A ConfigBlock with arguments to be passed to a property block(s) + and used when constructing these, + **default** - None. + **Valid values:** { + see property package for documentation.}""", + ), + ) + + def _process_config(self): + if len(self.config.property_package.solvent_set) > 1: + raise ConfigurationError( + "ElectroNP model only supports one solvent component," + "the provided property package has specified {} solvent components".format( + len(self.config.property_package.solvent_set) + ) + ) + + if len(self.config.property_package.solvent_set) == 0: + raise ConfigurationError( + "The ElectroNP model was expecting a solvent and did not receive it." + ) + + if ( + len(self.config.property_package.solute_set) == 0 + and len(self.config.property_package.ion_set) == 0 + ): + raise ConfigurationError( + "The ElectroNP model was expecting at least one solute or ion and did not receive any." + ) + + def build(self): + # Call UnitModel.build to setup dynamics + super().build() + + self.scaling_factor = Suffix(direction=Suffix.EXPORT) + + units_meta = self.config.property_package.get_metadata().get_derived_units + + # Check configs for errors + self._process_config() + + # Create state blocks for inlet and outlets + tmp_dict = dict(**self.config.property_package_args) + tmp_dict["has_phase_equilibrium"] = False + tmp_dict["defined_state"] = True + + self.properties_in = self.config.property_package.build_state_block( + self.flowsheet().time, doc="Material properties at inlet", **tmp_dict + ) + + tmp_dict_2 = dict(**tmp_dict) + tmp_dict_2["defined_state"] = False + + self.properties_treated = self.config.property_package.build_state_block( + self.flowsheet().time, + doc="Material properties of treated water", + **tmp_dict_2, + ) + self.properties_byproduct = self.config.property_package.build_state_block( + self.flowsheet().time, + doc="Material properties of byproduct stream", + **tmp_dict_2, + ) + + # Create Ports + self.add_port("inlet", self.properties_in, doc="Inlet port") + self.add_port( + "treated", self.properties_treated, doc="Treated water outlet port" + ) + self.add_port( + "byproduct", self.properties_byproduct, doc="Byproduct outlet port" + ) + + # Add isothermal constraints + @self.Constraint( + self.flowsheet().config.time, + doc="Isothermal assumption for treated flow", + ) + def eq_treated_isothermal(b, t): + return b.properties_in[t].temperature == b.properties_treated[t].temperature + + @self.Constraint( + self.flowsheet().config.time, + doc="Isothermal assumption for byproduct flow", + ) + def eq_byproduct_isothermal(b, t): + return ( + b.properties_in[t].temperature == b.properties_byproduct[t].temperature + ) + + # Add performance variables + self.recovery_frac_mass_H2O = Var( + self.flowsheet().time, + initialize=1, + domain=NonNegativeReals, + units=pyunits.dimensionless, + bounds=(0.0, 1.0000001), + doc="Mass recovery fraction of water in the treated stream", + ) + self.recovery_frac_mass_H2O.fix() + + self.removal_frac_mass_comp = Var( + self.flowsheet().time, + self.config.property_package.solute_set, + domain=NonNegativeReals, + initialize=0.01, + units=pyunits.dimensionless, + doc="Solute removal fraction on a mass basis", + ) + + # Add performance constraints + # Water recovery + @self.Constraint(self.flowsheet().time, doc="Water recovery equation") + def water_recovery_equation(b, t): + return b.recovery_frac_mass_H2O[t] * b.properties_in[ + t + ].get_material_flow_terms("Liq", "H2O") == b.properties_treated[ + t + ].get_material_flow_terms( + "Liq", "H2O" + ) + + # Flow balance + @self.Constraint(self.flowsheet().time, doc="Overall flow balance") + def water_balance(b, t): + return b.properties_in[t].get_material_flow_terms( + "Liq", "H2O" + ) == b.properties_treated[t].get_material_flow_terms( + "Liq", "H2O" + ) + b.properties_byproduct[ + t + ].get_material_flow_terms( + "Liq", "H2O" + ) + + # Solute removal + @self.Constraint( + self.flowsheet().time, + self.config.property_package.phase_list, + self.config.property_package.solute_set, + doc="Solute removal equations", + ) + def solute_removal_equation(b, t, p, j): + return b.removal_frac_mass_comp[t, j] * b.properties_in[ + t + ].get_material_flow_terms(p, j) == b.properties_byproduct[ + t + ].get_material_flow_terms( + p, j + ) + + # Solute concentration of treated stream + @self.Constraint( + self.flowsheet().time, + self.config.property_package.phase_list, + self.config.property_package.solute_set, + doc="Constraint for solute concentration in treated stream.", + ) + def solute_treated_equation(b, t, p, j): + return (1 - b.removal_frac_mass_comp[t, j]) * b.properties_in[ + t + ].get_material_flow_terms(p, j) == b.properties_treated[ + t + ].get_material_flow_terms( + p, j + ) + + # Default solute concentration + self.P_removal = Param( + within=NonNegativeReals, + mutable=True, + default=0.98, + doc="Reference phosphorus removal fraction on a mass basis", + units=pyunits.dimensionless, + ) + + self.N_removal = Param( + within=NonNegativeReals, + mutable=True, + default=0.3, + doc="Reference ammonia removal fraction on a mass basis", + units=pyunits.dimensionless, + ) + + # NOTE: revisit if we should sum soluble nitrogen as total nitrogen + @self.Constraint( + self.flowsheet().time, + self.config.property_package.solute_set, + doc="Default solute removal equations", + ) + def default_solute_removal_equation(b, t, j): + if j == "S_PO4": + return b.removal_frac_mass_comp[t, j] == b.P_removal + elif j == "S_NH4": + return b.removal_frac_mass_comp[t, j] == b.N_removal + else: + return b.removal_frac_mass_comp[t, j] == 0 + + self._stream_table_dict = { + "Inlet": self.inlet, + "Treated": self.treated, + "Byproduct": self.byproduct, + } + + self.electricity = Var( + self.flowsheet().time, + units=pyunits.kW, + bounds=(0, None), + doc="Electricity consumption of unit", + ) + + self.energy_electric_flow_mass = Var( + units=pyunits.kWh / pyunits.kg, + doc="Electricity intensity with respect to phosphorus removal", + ) + + @self.Constraint( + self.flowsheet().time, + doc="Constraint for electricity consumption based on phosphorus removal", + ) + def electricity_consumption(b, t): + return b.electricity[t] == ( + b.energy_electric_flow_mass + * pyunits.convert( + b.properties_treated[t].get_material_flow_terms("Liq", "S_PO4"), + to_units=pyunits.kg / pyunits.hour, + ) + ) + + self.magnesium_chloride_dosage = Var( + units=pyunits.dimensionless, + bounds=(0, None), + doc="Dosage of magnesium chloride per treated phosphorus", + ) + + self.MgCl2_flowrate = Var( + self.flowsheet().time, + units=pyunits.kg / pyunits.hr, + bounds=(0, None), + doc="Magnesium chloride flowrate", + ) + + @self.Constraint( + self.flowsheet().time, + doc="Constraint for magnesium chloride demand based on phosphorus removal.", + ) + def MgCl2_demand(b, t): + return b.MgCl2_flowrate[t] == ( + b.magnesium_chloride_dosage + * pyunits.convert( + b.properties_treated[t].get_material_flow_terms("Liq", "S_PO4"), + to_units=pyunits.kg / pyunits.hour, + ) + ) + + def initialize_build( + self, state_args=None, outlvl=idaeslog.NOTSET, solver=None, optarg=None + ): + """ + Initialization routine for single inlet-double outlet unit models. + + Keyword Arguments: + state_args : a dict of arguments to be passed to the property + package(s) to provide an initial state for + initialization (see documentation of the specific + property package) (default = {}). + outlvl : sets output level of initialization routine + optarg : solver options dictionary object (default=None, use + default solver options) + solver : str indicating which solver to use during + initialization (default = None, use default IDAES solver) + + Returns: + None + """ + if optarg is None: + optarg = {} + + # Set solver options + init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") + solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit") + + solver_obj = get_solver(solver, optarg) + + # Get initial guesses for inlet if none provided + if state_args is None: + state_args = {} + state_dict = self.properties_in[ + self.flowsheet().time.first() + ].define_port_members() + + for k in state_dict.keys(): + if state_dict[k].is_indexed(): + state_args[k] = {} + for m in state_dict[k].keys(): + state_args[k][m] = state_dict[k][m].value + else: + state_args[k] = state_dict[k].value + + # --------------------------------------------------------------------- + # Initialize control volume block + flags = self.properties_in.initialize( + outlvl=outlvl, + optarg=optarg, + solver=solver, + state_args=state_args, + hold_state=True, + ) + self.properties_treated.initialize( + outlvl=outlvl, + optarg=optarg, + solver=solver, + state_args=state_args, + hold_state=False, + ) + self.properties_byproduct.initialize( + outlvl=outlvl, + optarg=optarg, + solver=solver, + state_args=state_args, + hold_state=False, + ) + + init_log.info_high("Initialization Step 1 Complete.") + + # --------------------------------------------------------------------- + # Solve unit + with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: + results = solver_obj.solve(self, tee=slc.tee) + + init_log.info_high( + "Initialization Step 2 {}.".format(idaeslog.condition(results)) + ) + + # --------------------------------------------------------------------- + # Release Inlet state + self.properties_in.release_state(flags, outlvl) + + init_log.info("Initialization Complete: {}".format(idaeslog.condition(results))) + + if not check_optimal_termination(results): + raise InitializationError( + f"{self.name} failed to initialize successfully. Please check " + f"the output logs for more information." + ) + + def _get_performance_contents(self, time_point=0): + var_dict = {} + var_dict["Water Recovery"] = self.recovery_frac_mass_H2O[time_point] + for j in self.config.property_package.solute_set: + var_dict[f"Solute Removal {j}"] = self.removal_frac_mass_comp[time_point, j] + var_dict["Electricity Demand"] = self.electricity[time_point] + var_dict["Electricity Intensity"] = self.energy_electric_flow_mass + var_dict[ + "Dosage of magnesium chloride per treated phosphorus" + ] = self.magnesium_chloride_dosage + var_dict["Magnesium Chloride Demand"] = self.MgCl2_flowrate[time_point] + return {"vars": var_dict} + + def _get_stream_table_contents(self, time_point=0): + return create_stream_table_dataframe( + { + "Inlet": self.inlet, + "Treated": self.treated, + "Byproduct": self.byproduct, + }, + time_point=time_point, + ) + + def calculate_scaling_factors(self): + # Get default scale factors and do calculations from base classes + for t, v in self.water_recovery_equation.items(): + iscale.constraint_scaling_transform( + v, + iscale.get_scaling_factor( + self.properties_in[t].get_material_flow_terms("Liq", "H2O"), + default=1, + warning=True, + hint=" for water recovery", + ), + ) + + for t, v in self.water_balance.items(): + iscale.constraint_scaling_transform( + v, + iscale.get_scaling_factor( + self.properties_in[t].get_material_flow_terms("Liq", "H2O"), + default=1, + warning=False, + ), + ) # would just be a duplicate of above + + for (t, p, j), v in self.solute_removal_equation.items(): + iscale.constraint_scaling_transform( + v, + iscale.get_scaling_factor( + self.properties_in[t].get_material_flow_terms(p, j), + default=1, + warning=True, + hint=" for solute removal", + ), + ) + + for (t, p, j), v in self.solute_treated_equation.items(): + iscale.constraint_scaling_transform( + v, + iscale.get_scaling_factor( + self.properties_in[t].get_material_flow_terms(p, j), + default=1, + warning=False, + ), + ) # would just be a duplicate of above diff --git a/watertap/unit_models/electrodialysis_0D.py b/watertap/unit_models/electrodialysis_0D.py index 4c13285e1b..0904c0d6e4 100644 --- a/watertap/unit_models/electrodialysis_0D.py +++ b/watertap/unit_models/electrodialysis_0D.py @@ -23,7 +23,7 @@ Constraint, units as pyunits, ) -from pyomo.common.config import Bool, ConfigBlock, ConfigValue, In, Bool +from pyomo.common.config import Bool, ConfigBlock, ConfigValue, In # Import IDAES cores from idaes.core import ( @@ -33,7 +33,6 @@ MomentumBalanceType, UnitModelBlockData, useDefault, - MaterialFlowBasis, ) from idaes.core.util.misc import add_object_reference from idaes.core.solvers import get_solver diff --git a/watertap/unit_models/ion_exchange_0D.py b/watertap/unit_models/ion_exchange_0D.py index e7167e038c..3a2eb2e427 100644 --- a/watertap/unit_models/ion_exchange_0D.py +++ b/watertap/unit_models/ion_exchange_0D.py @@ -36,7 +36,7 @@ from idaes.core.util.config import is_physical_parameter_block from idaes.core.util.misc import StrEnum -from idaes.core.util.exceptions import ConfigurationError, InitializationError +from idaes.core.util.exceptions import InitializationError import idaes.core.util.scaling as iscale import idaes.logger as idaeslog diff --git a/watertap/unit_models/osmotically_assisted_reverse_osmosis_0D.py b/watertap/unit_models/osmotically_assisted_reverse_osmosis_0D.py index eaedcc430b..1a70bc1793 100644 --- a/watertap/unit_models/osmotically_assisted_reverse_osmosis_0D.py +++ b/watertap/unit_models/osmotically_assisted_reverse_osmosis_0D.py @@ -14,7 +14,6 @@ from pyomo.environ import ( Var, NonNegativeReals, - NegativeReals, value, ) diff --git a/watertap/unit_models/pressure_exchanger.py b/watertap/unit_models/pressure_exchanger.py index 76eef0379f..1a64ded231 100644 --- a/watertap/unit_models/pressure_exchanger.py +++ b/watertap/unit_models/pressure_exchanger.py @@ -34,7 +34,6 @@ from idaes.core.solvers import get_solver from idaes.core.util.config import is_physical_parameter_block from idaes.core.util.exceptions import ConfigurationError, InitializationError -from idaes.core.util.initialization import revert_state_vars from idaes.core.util.tables import create_stream_table_dataframe import idaes.core.util.scaling as iscale diff --git a/watertap/unit_models/reverse_osmosis_0D.py b/watertap/unit_models/reverse_osmosis_0D.py index c69ada8919..6e61214ed0 100644 --- a/watertap/unit_models/reverse_osmosis_0D.py +++ b/watertap/unit_models/reverse_osmosis_0D.py @@ -20,7 +20,7 @@ from idaes.core import declare_process_block_class from idaes.core.util import scaling as iscale from idaes.core.util.misc import add_object_reference -from watertap.core import ( +from watertap.core import ( # noqa # pylint: disable=unused-import ConcentrationPolarizationType, MembraneChannel0DBlock, MassTransferCoefficient, @@ -32,7 +32,6 @@ _add_has_full_reporting, ) - __author__ = "Tim Bartholomew, Adam Atia" @@ -72,7 +71,6 @@ def _add_deltaP(self): add_object_reference(self, "deltaP", self.feed_side.deltaP) def _add_mass_transfer(self): - units_meta = self.config.property_package.get_metadata().get_derived_units # not in 1DRO diff --git a/watertap/unit_models/reverse_osmosis_1D.py b/watertap/unit_models/reverse_osmosis_1D.py index 21831a6367..f009e36a67 100644 --- a/watertap/unit_models/reverse_osmosis_1D.py +++ b/watertap/unit_models/reverse_osmosis_1D.py @@ -26,7 +26,7 @@ from idaes.core.util import scaling as iscale import idaes.logger as idaeslog -from watertap.core import ( +from watertap.core import ( # noqa # pylint: disable=unused-import ConcentrationPolarizationType, MassTransferCoefficient, MembraneChannel1DBlock, @@ -125,7 +125,6 @@ def eq_pressure_drop(b, t): ) def _add_mass_transfer(self): - units_meta = self.config.property_package.get_metadata().get_derived_units def mass_transfer_phase_comp_initialize(b, t, x, p, j): diff --git a/watertap/unit_models/reverse_osmosis_base.py b/watertap/unit_models/reverse_osmosis_base.py index 1b398a0d3d..2431275fbd 100644 --- a/watertap/unit_models/reverse_osmosis_base.py +++ b/watertap/unit_models/reverse_osmosis_base.py @@ -34,7 +34,6 @@ from watertap.core import InitializationMixin from watertap.core.membrane_channel_base import ( validate_membrane_config_args, - CONFIG_Template, ConcentrationPolarizationType, ) diff --git a/watertap/unit_models/tests/test_anaerobic_digestor.py b/watertap/unit_models/tests/test_anaerobic_digestor.py index 4c67f4ec84..75638fd76d 100644 --- a/watertap/unit_models/tests/test_anaerobic_digestor.py +++ b/watertap/unit_models/tests/test_anaerobic_digestor.py @@ -329,3 +329,7 @@ def test_get_performance_contents(self, adm): "Heat Duty": adm.fs.unit.heat_duty[0], } } + + @pytest.mark.unit + def test_report(self, adm): + adm.fs.unit.report() diff --git a/watertap/unit_models/tests/test_cstr_injection.py b/watertap/unit_models/tests/test_cstr_injection.py index 04b3c0b7f9..bf9c1bb017 100644 --- a/watertap/unit_models/tests/test_cstr_injection.py +++ b/watertap/unit_models/tests/test_cstr_injection.py @@ -15,7 +15,7 @@ """ import pytest -from pyomo.environ import check_optimal_termination, ConcreteModel, units, value, Var +from pyomo.environ import check_optimal_termination, ConcreteModel, units, value from idaes.core import ( FlowsheetBlock, MaterialBalanceType, diff --git a/watertap/unit_models/tests/test_dewatering_unit.py b/watertap/unit_models/tests/test_dewatering_unit.py new file mode 100644 index 0000000000..fa97744c64 --- /dev/null +++ b/watertap/unit_models/tests/test_dewatering_unit.py @@ -0,0 +1,272 @@ +################################################################################# +# WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# National Renewable Energy Laboratory, and National Energy Technology +# Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +################################################################################# +""" +Tests for dewatering unit example. +""" + +import pytest +from pyomo.environ import ( + ConcreteModel, + value, + assert_optimal_termination, +) + +from idaes.core import ( + FlowsheetBlock, + MaterialBalanceType, + MomentumBalanceType, +) + +from idaes.models.unit_models.separator import SplittingType + +from pyomo.environ import ( + units, +) + +from idaes.core.solvers import get_solver +from idaes.core.util.model_statistics import ( + degrees_of_freedom, + number_variables, + number_total_constraints, + number_unused_variables, +) +import idaes.core.util.scaling as iscale +from idaes.core.util.testing import ( + initialization_tester, +) + +from idaes.core.util.exceptions import ( + ConfigurationError, +) + +from watertap.unit_models.dewatering import DewateringUnit +from watertap.property_models.activated_sludge.asm1_properties import ( + ASM1ParameterBlock, +) + +from pyomo.util.check_units import assert_units_consistent + +# ----------------------------------------------------------------------------- +# Get default solver for testing +solver = get_solver() + +# ----------------------------------------------------------------------------- +@pytest.mark.unit +def test_config(): + m = ConcreteModel() + + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.props = ASM1ParameterBlock() + + m.fs.unit = DewateringUnit(property_package=m.fs.props) + + assert len(m.fs.unit.config) == 15 + + assert not m.fs.unit.config.dynamic + assert not m.fs.unit.config.has_holdup + assert m.fs.unit.config.material_balance_type == MaterialBalanceType.useDefault + assert m.fs.unit.config.momentum_balance_type == MomentumBalanceType.pressureTotal + assert "underflow" in m.fs.unit.config.outlet_list + assert "overflow" in m.fs.unit.config.outlet_list + assert SplittingType.componentFlow is m.fs.unit.config.split_basis + + +@pytest.mark.unit +def test_list_error(): + m = ConcreteModel() + + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.props = ASM1ParameterBlock() + + with pytest.raises( + ConfigurationError, + match="fs.unit encountered unrecognised " + "outlet_list. This should not " + "occur - please use overflow " + "and underflow as outlets.", + ): + m.fs.unit = DewateringUnit( + property_package=m.fs.props, + outlet_list=["outlet1", "outlet2"], + ) + + +# ----------------------------------------------------------------------------- +class TestDu(object): + @pytest.fixture(scope="class") + def du(self): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.props = ASM1ParameterBlock() + + m.fs.unit = DewateringUnit(property_package=m.fs.props) + + m.fs.unit.inlet.flow_vol.fix(178.4674 * units.m**3 / units.day) + m.fs.unit.inlet.temperature.fix(308.15 * units.K) + m.fs.unit.inlet.pressure.fix(1 * units.atm) + + m.fs.unit.inlet.conc_mass_comp[0, "S_I"].fix(130.867 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_S"].fix(258.5789 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_I"].fix( + 17216.2434 * units.mg / units.liter + ) + m.fs.unit.inlet.conc_mass_comp[0, "X_S"].fix(2611.4843 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_BH"].fix(1e-6 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_BA"].fix(1e-6 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_P"].fix(626.0652 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_O"].fix(1e-6 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_NO"].fix(1e-6 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_NH"].fix( + 1442.7882 * units.mg / units.liter + ) + m.fs.unit.inlet.conc_mass_comp[0, "S_ND"].fix(0.54323 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_ND"].fix(100.8668 * units.mg / units.liter) + m.fs.unit.inlet.alkalinity.fix(97.8459 * units.mol / units.m**3) + + return m + + @pytest.mark.build + @pytest.mark.unit + def test_build(self, du): + + assert hasattr(du.fs.unit, "inlet") + assert len(du.fs.unit.inlet.vars) == 5 + assert hasattr(du.fs.unit.inlet, "flow_vol") + assert hasattr(du.fs.unit.inlet, "conc_mass_comp") + assert hasattr(du.fs.unit.inlet, "temperature") + assert hasattr(du.fs.unit.inlet, "pressure") + assert hasattr(du.fs.unit.inlet, "alkalinity") + + assert hasattr(du.fs.unit, "underflow") + assert len(du.fs.unit.underflow.vars) == 5 + assert hasattr(du.fs.unit.underflow, "flow_vol") + assert hasattr(du.fs.unit.underflow, "conc_mass_comp") + assert hasattr(du.fs.unit.underflow, "temperature") + assert hasattr(du.fs.unit.underflow, "pressure") + assert hasattr(du.fs.unit.underflow, "alkalinity") + + assert hasattr(du.fs.unit, "overflow") + assert len(du.fs.unit.overflow.vars) == 5 + assert hasattr(du.fs.unit.overflow, "flow_vol") + assert hasattr(du.fs.unit.overflow, "conc_mass_comp") + assert hasattr(du.fs.unit.overflow, "temperature") + assert hasattr(du.fs.unit.overflow, "pressure") + assert hasattr(du.fs.unit.overflow, "alkalinity") + + assert number_variables(du) == 76 + assert number_total_constraints(du) == 60 + assert number_unused_variables(du) == 0 + + @pytest.mark.unit + def test_dof(self, du): + assert degrees_of_freedom(du) == 0 + + @pytest.mark.unit + def test_units(self, du): + assert_units_consistent(du) + + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_initialize(self, du): + + iscale.calculate_scaling_factors(du) + initialization_tester(du) + + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_solve(self, du): + solver = get_solver() + results = solver.solve(du) + assert_optimal_termination(results) + + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_solution(self, du): + assert pytest.approx(101325.0, rel=1e-3) == value( + du.fs.unit.overflow.pressure[0] + ) + assert pytest.approx(308.15, rel=1e-3) == value( + du.fs.unit.overflow.temperature[0] + ) + assert pytest.approx(0.001954, rel=1e-3) == value( + du.fs.unit.overflow.flow_vol[0] + ) + assert pytest.approx(0.1308, rel=1e-3) == value( + du.fs.unit.overflow.conc_mass_comp[0, "S_I"] + ) + assert pytest.approx(0.2585, rel=1e-3) == value( + du.fs.unit.overflow.conc_mass_comp[0, "S_S"] + ) + assert pytest.approx(0.3638, rel=1e-3) == value( + du.fs.unit.overflow.conc_mass_comp[0, "X_I"] + ) + assert pytest.approx(0.0552, rel=1e-3) == value( + du.fs.unit.overflow.conc_mass_comp[0, "X_S"] + ) + assert value(du.fs.unit.overflow.conc_mass_comp[0, "X_BH"]) <= 1e-6 + assert value(du.fs.unit.overflow.conc_mass_comp[0, "X_BA"]) <= 1e-6 + assert pytest.approx(0.01323, rel=1e-3) == value( + du.fs.unit.overflow.conc_mass_comp[0, "X_P"] + ) + assert value(du.fs.unit.overflow.conc_mass_comp[0, "S_O"]) <= 1e-6 + assert value(du.fs.unit.overflow.conc_mass_comp[0, "S_NO"]) <= 1e-6 + assert pytest.approx(1.4427, rel=1e-3) == value( + du.fs.unit.overflow.conc_mass_comp[0, "S_NH"] + ) + assert pytest.approx(0.000543, rel=1e-3) == value( + du.fs.unit.overflow.conc_mass_comp[0, "S_ND"] + ) + assert pytest.approx(0.00213, rel=1e-3) == value( + du.fs.unit.overflow.conc_mass_comp[0, "X_ND"] + ) + assert pytest.approx(0.09784, rel=1e-3) == value( + du.fs.unit.overflow.alkalinity[0] + ) + + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_conservation(self, du): + assert ( + abs( + value( + du.fs.unit.inlet.flow_vol[0] * du.fs.props.dens_mass + - du.fs.unit.overflow.flow_vol[0] * du.fs.props.dens_mass + - du.fs.unit.underflow.flow_vol[0] * du.fs.props.dens_mass + ) + ) + <= 1e-6 + ) + for i in du.fs.props.solute_set: + assert ( + abs( + value( + du.fs.unit.inlet.flow_vol[0] + * du.fs.unit.inlet.conc_mass_comp[0, i] + - du.fs.unit.overflow.flow_vol[0] + * du.fs.unit.overflow.conc_mass_comp[0, i] + - du.fs.unit.underflow.flow_vol[0] + * du.fs.unit.underflow.conc_mass_comp[0, i] + ) + ) + <= 1e-6 + ) + + @pytest.mark.unit + def test_report(self, du): + du.fs.unit.report() diff --git a/watertap/unit_models/tests/test_electroNP_ZO.py b/watertap/unit_models/tests/test_electroNP_ZO.py new file mode 100644 index 0000000000..8d7e425a09 --- /dev/null +++ b/watertap/unit_models/tests/test_electroNP_ZO.py @@ -0,0 +1,247 @@ +################################################################################# +# WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# National Renewable Energy Laboratory, and National Energy Technology +# Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +################################################################################# + +import pytest +from pyomo.environ import ( + ConcreteModel, + assert_optimal_termination, + value, + units, +) +from idaes.core import FlowsheetBlock +from watertap.unit_models.electroNP_ZO import ElectroNPZO +from watertap.property_models.activated_sludge.modified_asm2d_properties import ( + ModifiedASM2dParameterBlock, +) +from idaes.core.solvers import get_solver +from idaes.core.util.model_statistics import degrees_of_freedom +from idaes.core.util.testing import initialization_tester +from idaes.core.util.scaling import calculate_scaling_factors +from pyomo.util.check_units import assert_units_consistent +from idaes.core import UnitModelCostingBlock +from watertap.costing import WaterTAPCosting + +# ----------------------------------------------------------------------------- +# Get default solver for testing +solver = get_solver() + + +class TestElectroNP: + @pytest.fixture(scope="class") + def ElectroNP_frame(self): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.properties = ModifiedASM2dParameterBlock( + additional_solute_list=["S_K", "S_Mg"] + ) + + m.fs.unit = ElectroNPZO(property_package=m.fs.properties) + + EPS = 1e-8 + + m.fs.unit.inlet.temperature.fix(298.15 * units.K) + m.fs.unit.inlet.pressure.fix(1 * units.atm) + + m.fs.unit.inlet.flow_vol.fix(18446 * units.m**3 / units.day) + m.fs.unit.inlet.conc_mass_comp[0, "S_O2"].fix(10 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_N2"].fix(EPS * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_NH4"].fix(16 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_NO3"].fix(EPS * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_PO4"].fix(3.6 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_F"].fix(30 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_A"].fix(20 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_I"].fix(30 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_I"].fix(25 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_S"].fix(125 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_H"].fix(30 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_PAO"].fix(EPS * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_PP"].fix(EPS * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_PHA"].fix(EPS * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_AUT"].fix(EPS * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_MeOH"].fix(EPS * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_MeP"].fix(EPS * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_TSS"].fix(EPS * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_K"].fix(EPS * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_Mg"].fix(EPS * units.mg / units.liter) + + # Alkalinity was givien in mg/L based on C + m.fs.unit.inlet.alkalinity[0].fix(61 / 12 * units.mmol / units.liter) + + # Unit option + m.fs.unit.energy_electric_flow_mass.fix(0.044 * units.kWh / units.kg) + m.fs.unit.magnesium_chloride_dosage.fix(0.388) + + return m + + @pytest.mark.unit + def test_dof(self, ElectroNP_frame): + m = ElectroNP_frame + assert degrees_of_freedom(m) == 0 + + @pytest.mark.unit + def test_units(self, ElectroNP_frame): + assert_units_consistent(ElectroNP_frame) + + @pytest.mark.unit + def test_calculate_scaling(self, ElectroNP_frame): + m = ElectroNP_frame + calculate_scaling_factors(m) + + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_initialize(self, ElectroNP_frame): + initialization_tester(ElectroNP_frame) + + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_solve(self, ElectroNP_frame): + m = ElectroNP_frame + results = solver.solve(m) + + # Check for optimal solution + assert_optimal_termination(results) + + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_conservation(self, ElectroNP_frame): + m = ElectroNP_frame + assert ( + abs( + value( + m.fs.unit.inlet.flow_vol[0] * m.fs.properties.dens_mass + - m.fs.unit.treated.flow_vol[0] * m.fs.properties.dens_mass + - m.fs.unit.byproduct.flow_vol[0] * m.fs.properties.dens_mass + ) + ) + <= 1e-6 + ) + for j in m.fs.properties.solute_set: + assert 1e-6 >= abs( + value( + m.fs.unit.inlet.flow_vol[0] * m.fs.unit.inlet.conc_mass_comp[0, j] + - m.fs.unit.treated.flow_vol[0] + * m.fs.unit.treated.conc_mass_comp[0, j] + - m.fs.unit.byproduct.flow_vol[0] + * m.fs.unit.byproduct.conc_mass_comp[0, j] + ) + ) + + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_solution(self, ElectroNP_frame): + m = ElectroNP_frame + assert value(m.fs.unit.treated.flow_vol[0]) == pytest.approx(0.213495, rel=1e-3) + + assert value(m.fs.unit.treated.temperature[0]) == pytest.approx( + 298.15, rel=1e-4 + ) + assert value(m.fs.unit.treated.pressure[0]) == pytest.approx(101325, rel=1e-4) + assert value(m.fs.unit.treated.conc_mass_comp[0, "S_A"]) == pytest.approx( + 0.02, rel=1e-4 + ) + assert value(m.fs.unit.treated.conc_mass_comp[0, "S_F"]) == pytest.approx( + 0.03, rel=1e-2 + ) + assert value(m.fs.unit.treated.conc_mass_comp[0, "S_I"]) == pytest.approx( + 0.03, rel=1e-4 + ) + assert value(m.fs.unit.treated.conc_mass_comp[0, "S_N2"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(m.fs.unit.treated.conc_mass_comp[0, "S_NH4"]) == pytest.approx( + 0.0112, rel=1e-4 + ) + assert value(m.fs.unit.treated.conc_mass_comp[0, "S_NO3"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(m.fs.unit.treated.conc_mass_comp[0, "S_O2"]) == pytest.approx( + 0.01, rel=1e-4 + ) + assert value(m.fs.unit.treated.conc_mass_comp[0, "S_PO4"]) == pytest.approx( + 7.2e-5, rel=1e-4 + ) + assert value(m.fs.unit.treated.conc_mass_comp[0, "X_AUT"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(m.fs.unit.treated.conc_mass_comp[0, "X_H"]) == pytest.approx( + 0.03, rel=1e-4 + ) + assert value(m.fs.unit.treated.conc_mass_comp[0, "X_I"]) == pytest.approx( + 0.025, rel=1e-4 + ) + assert value(m.fs.unit.treated.conc_mass_comp[0, "X_MeOH"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(m.fs.unit.treated.conc_mass_comp[0, "X_MeP"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(m.fs.unit.treated.conc_mass_comp[0, "X_PAO"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(m.fs.unit.treated.conc_mass_comp[0, "X_PHA"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(m.fs.unit.treated.conc_mass_comp[0, "X_PP"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(m.fs.unit.treated.conc_mass_comp[0, "X_S"]) == pytest.approx( + 0.125, rel=1e-4 + ) + assert value(m.fs.unit.treated.conc_mass_comp[0, "X_TSS"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(m.fs.unit.treated.conc_mass_comp[0, "S_Mg"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(m.fs.unit.treated.conc_mass_comp[0, "S_K"]) == pytest.approx( + 0, abs=1e-4 + ) + assert value(m.fs.unit.byproduct.conc_mass_comp[0, "S_NH4"]) == pytest.approx( + 4.5289e14, rel=1e-4 + ) + assert value(m.fs.unit.byproduct.conc_mass_comp[0, "S_PO4"]) == pytest.approx( + 3.328756e14, rel=1e-4 + ) + assert value(m.fs.unit.treated.alkalinity[0]) == pytest.approx( + 0.005083, rel=1e-4 + ) + + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_costing(self, ElectroNP_frame): + m = ElectroNP_frame + + m.fs.costing = WaterTAPCosting() + + m.fs.unit.costing = UnitModelCostingBlock(flowsheet_costing_block=m.fs.costing) + m.fs.costing.cost_process() + m.fs.costing.add_LCOW(m.fs.unit.properties_treated[0].flow_vol) + results = solver.solve(m) + + assert_optimal_termination(results) + + # Check solutions + assert pytest.approx(1295.765, rel=1e-5) == value( + m.fs.unit.costing.capital_cost + ) + assert pytest.approx(5.800325e-5, rel=1e-5) == value(m.fs.costing.LCOW) + + @pytest.mark.unit + def test_report(self, ElectroNP_frame): + m = ElectroNP_frame + m.fs.unit.report() diff --git a/watertap/unit_models/tests/test_gac.py b/watertap/unit_models/tests/test_gac.py index b4d7b66845..afb714f2b1 100644 --- a/watertap/unit_models/tests/test_gac.py +++ b/watertap/unit_models/tests/test_gac.py @@ -12,36 +12,21 @@ import pytest import pyomo.environ as pyo -from pyomo.environ import ( - ConcreteModel, - check_optimal_termination, - value, -) +import idaes.core.util.scaling as iscale +import idaes.core.util.model_statistics as istat + from pyomo.network import Port from pyomo.util.check_units import assert_units_consistent - from idaes.core import ( FlowsheetBlock, EnergyBalanceType, MaterialBalanceType, MomentumBalanceType, + UnitModelCostingBlock, ) from idaes.core.solvers import get_solver -from idaes.core.util.model_statistics import ( - degrees_of_freedom, - number_variables, - number_total_constraints, - number_unused_variables, -) from idaes.core.util.testing import initialization_tester -from idaes.core.util.scaling import ( - calculate_scaling_factors, - unscaled_variables_generator, - badly_scaled_var_generator, -) from idaes.core.util.exceptions import ConfigurationError -from idaes.core import UnitModelCostingBlock - from watertap.property_models.multicomp_aq_sol_prop_pack import ( MCASParameterBlock, DiffusivityCalculation, @@ -57,16 +42,11 @@ solver = get_solver() -# inputs for badly_scaled_var_generator used across test frames -sv_large = 1e2 -sv_small = 1e-2 -sv_zero = 1e-8 - # ----------------------------------------------------------------------------- class TestGACSimplified: @pytest.fixture(scope="class") def gac_frame_simplified(self): - ms = ConcreteModel() + ms = pyo.ConcreteModel() ms.fs = FlowsheetBlock(dynamic=False) ms.fs.properties = MCASParameterBlock( @@ -80,18 +60,11 @@ def gac_frame_simplified(self): ) # feed specifications - ms.fs.unit.process_flow.properties_in[0].pressure.fix( - 101325 - ) # feed pressure [Pa] - ms.fs.unit.process_flow.properties_in[0].temperature.fix( - 273.15 + 25 - ) # feed temperature [K] - ms.fs.unit.process_flow.properties_in[0].flow_mol_phase_comp["Liq", "H2O"].fix( - 55555.55426666667 - ) - ms.fs.unit.process_flow.properties_in[0].flow_mol_phase_comp["Liq", "DCE"].fix( - 0.0002344381568310428 - ) + unit_feed = ms.fs.unit.process_flow.properties_in[0] + unit_feed.pressure.fix(101325) + unit_feed.temperature.fix(273.15 + 25) + unit_feed.flow_mol_phase_comp["Liq", "H2O"].fix(55555.55426666667) + unit_feed.flow_mol_phase_comp["Liq", "DCE"].fix(0.0002344381568310428) # trial problem from Hand, 1984 for removal of trace DCE # adsorption isotherm @@ -122,31 +95,29 @@ def gac_frame_simplified(self): @pytest.mark.unit def test_simplified_config(self, gac_frame_simplified): ms = gac_frame_simplified - # check unit config arguments - assert len(ms.fs.unit.config) == 12 + u_config = ms.fs.unit.config - assert not ms.fs.unit.config.dynamic - assert not ms.fs.unit.config.has_holdup - assert ms.fs.unit.config.material_balance_type == MaterialBalanceType.useDefault - assert ms.fs.unit.config.energy_balance_type == EnergyBalanceType.none - assert ( - ms.fs.unit.config.momentum_balance_type == MomentumBalanceType.pressureTotal - ) + # check unit config arguments + assert len(u_config) == 12 + + assert not u_config.dynamic + assert not u_config.has_holdup + assert u_config.material_balance_type == MaterialBalanceType.useDefault + assert u_config.energy_balance_type == EnergyBalanceType.none + assert u_config.momentum_balance_type == MomentumBalanceType.pressureTotal + assert u_config.finite_elements_ss_approximation == 5 assert ( - ms.fs.unit.config.film_transfer_coefficient_type - == FilmTransferCoefficientType.fixed + u_config.film_transfer_coefficient_type == FilmTransferCoefficientType.fixed ) assert ( - ms.fs.unit.config.surface_diffusion_coefficient_type + u_config.surface_diffusion_coefficient_type == SurfaceDiffusionCoefficientType.fixed ) - assert ms.fs.unit.config.finite_elements_ss_approximation == 5 # check properties - assert ms.fs.unit.config.property_package is ms.fs.properties - assert ms.fs.unit.config.property_package is ms.fs.properties - assert len(ms.fs.unit.config.property_package.solute_set) == 1 - assert len(ms.fs.unit.config.property_package.solvent_set) == 1 + assert u_config.property_package is ms.fs.properties + assert len(u_config.property_package.solute_set) == 1 + assert len(u_config.property_package.solvent_set) == 1 assert ms.fs.properties.config.diffus_calculation == DiffusivityCalculation.none @pytest.mark.unit @@ -160,34 +131,30 @@ def test_simplified_build(self, gac_frame_simplified): port_lst = ["inlet", "outlet", "adsorbed"] for port_str in port_lst: port = getattr(ms.fs.unit, port_str) - assert len(port.vars) == 3 # number of state variables for property package + assert len(port.vars) == 3 assert isinstance(port, Port) # test statistics - assert number_variables(ms) == 100 - assert number_total_constraints(ms) == 64 - assert number_unused_variables(ms) == 11 # dens parameters from properties + assert istat.number_variables(ms) == 100 + assert istat.number_total_constraints(ms) == 64 + assert istat.number_unused_variables(ms) == 11 @pytest.mark.unit def test_simplified_dof(self, gac_frame_simplified): ms = gac_frame_simplified - assert degrees_of_freedom(ms) == 0 + assert istat.degrees_of_freedom(ms) == 0 @pytest.mark.unit def test_simplified_calculate_scaling(self, gac_frame_simplified): ms = gac_frame_simplified - ms.fs.properties.set_default_scaling( - "flow_mol_phase_comp", 1e-4, index=("Liq", "H2O") - ) - ms.fs.properties.set_default_scaling( - "flow_mol_phase_comp", 1e4, index=("Liq", "DCE") - ) - calculate_scaling_factors(ms) + prop = ms.fs.properties + prop.set_default_scaling("flow_mol_phase_comp", 1e-4, index=("Liq", "H2O")) + prop.set_default_scaling("flow_mol_phase_comp", 1e4, index=("Liq", "DCE")) + iscale.calculate_scaling_factors(ms) # check that all variables have scaling factors - unscaled_var_list = list(unscaled_variables_generator(ms)) - assert len(unscaled_var_list) == 0 + assert len(list(iscale.unscaled_variables_generator(ms))) == 0 @pytest.mark.component def test_simplified_initialize(self, gac_frame_simplified): @@ -196,11 +163,7 @@ def test_simplified_initialize(self, gac_frame_simplified): @pytest.mark.component def test_simplified_var_scaling_init(self, gac_frame_simplified): ms = gac_frame_simplified - badly_scaled_var_lst = list( - badly_scaled_var_generator(ms, large=sv_large, small=sv_small, zero=sv_zero) - ) - print([(x[0].name, x[1]) for x in badly_scaled_var_lst]) - assert badly_scaled_var_lst == [] + assert len(list(iscale.badly_scaled_var_generator(ms, zero=1e-8))) == 0 @pytest.mark.component def test_simplified_solve(self, gac_frame_simplified): @@ -208,42 +171,38 @@ def test_simplified_solve(self, gac_frame_simplified): results = solver.solve(ms) # Check for optimal solution - assert check_optimal_termination(results) + assert pyo.check_optimal_termination(results) @pytest.mark.component def test_simplified_var_scaling_solve(self, gac_frame_simplified): ms = gac_frame_simplified - badly_scaled_var_lst = list( - badly_scaled_var_generator(ms, large=sv_large, small=sv_small, zero=sv_zero) - ) - assert badly_scaled_var_lst == [] + assert len(list(iscale.badly_scaled_var_generator(ms, zero=1e-8))) == 0 @pytest.mark.component def test_simplified_solution(self, gac_frame_simplified): ms = gac_frame_simplified + gac = ms.fs.unit # Approx data pulled from graph in Hand, 1984 at ~30 days # 30 days adjusted to actual solution to account for web plot data extraction error within reason # values calculated by hand and match those reported in Hand, 1984 - assert pytest.approx(0.0005178, rel=1e-3) == value(ms.fs.unit.equil_conc) - assert pytest.approx(19780, rel=1e-3) == value(ms.fs.unit.dg) - assert pytest.approx(6.113, rel=1e-3) == value(ms.fs.unit.N_Bi) - assert pytest.approx(35.68, rel=1e-3) == value(ms.fs.unit.min_N_St) - assert pytest.approx(0.9882, rel=1e-3) == value(ms.fs.unit.throughput) - assert pytest.approx(468.4, rel=1e-3) == value(ms.fs.unit.min_residence_time) - assert pytest.approx(134.7, rel=1e-3) == value(ms.fs.unit.residence_time) - assert pytest.approx(9153000, rel=1e-3) == value( - ms.fs.unit.min_operational_time - ) - assert pytest.approx(2554000, rel=1e-3) == value(ms.fs.unit.operational_time) - assert pytest.approx(8514, rel=1e-3) == value(ms.fs.unit.bed_volumes_treated) + assert pytest.approx(0.0005178, rel=1e-3) == pyo.value(gac.equil_conc) + assert pytest.approx(19780, rel=1e-3) == pyo.value(gac.dg) + assert pytest.approx(6.113, rel=1e-3) == pyo.value(gac.N_Bi) + assert pytest.approx(35.68, rel=1e-3) == pyo.value(gac.min_N_St) + assert pytest.approx(0.9882, rel=1e-3) == pyo.value(gac.throughput) + assert pytest.approx(468.4, rel=1e-3) == pyo.value(gac.min_residence_time) + assert pytest.approx(134.7, rel=1e-3) == pyo.value(gac.residence_time) + assert pytest.approx(9153000, rel=1e-3) == pyo.value(gac.min_operational_time) + assert pytest.approx(2554000, rel=1e-3) == pyo.value(gac.operational_time) + assert pytest.approx(8514, rel=1e-3) == pyo.value(gac.bed_volumes_treated) # ----------------------------------------------------------------------------- class TestGACRobust: @pytest.fixture(scope="class") def gac_frame_robust(self): - mr = ConcreteModel() + mr = pyo.ConcreteModel() mr.fs = FlowsheetBlock(dynamic=False) mr.fs.properties = MCASParameterBlock( @@ -262,18 +221,11 @@ def gac_frame_robust(self): ) # feed specifications - mr.fs.unit.process_flow.properties_in[0].pressure.fix( - 101325 - ) # feed pressure [Pa] - mr.fs.unit.process_flow.properties_in[0].temperature.fix( - 273.15 + 25 - ) # feed temperature [K] - mr.fs.unit.process_flow.properties_in[0].flow_mol_phase_comp["Liq", "H2O"].fix( - 823.8 - ) - mr.fs.unit.process_flow.properties_in[0].flow_mol_phase_comp["Liq", "TCE"].fix( - 5.6444e-05 - ) + unit_feed = mr.fs.unit.process_flow.properties_in[0] + unit_feed.pressure.fix(101325) + unit_feed.temperature.fix(273.15 + 25) + unit_feed.flow_mol_phase_comp["Liq", "H2O"].fix(823.8) + unit_feed.flow_mol_phase_comp["Liq", "TCE"].fix(5.6444e-05) # trial problem from Crittenden, 2012 for removal of TCE # adsorption isotherm @@ -304,30 +256,29 @@ def gac_frame_robust(self): @pytest.mark.unit def test_robust_config(self, gac_frame_robust): mr = gac_frame_robust - # check unit config arguments - assert len(mr.fs.unit.config) == 12 + u_config = mr.fs.unit.config - assert not mr.fs.unit.config.dynamic - assert not mr.fs.unit.config.has_holdup - assert mr.fs.unit.config.material_balance_type == MaterialBalanceType.useDefault - assert mr.fs.unit.config.energy_balance_type == EnergyBalanceType.none - assert ( - mr.fs.unit.config.momentum_balance_type == MomentumBalanceType.pressureTotal - ) + # check unit config arguments + assert len(u_config) == 12 + + assert not u_config.dynamic + assert not u_config.has_holdup + assert u_config.material_balance_type == MaterialBalanceType.useDefault + assert u_config.energy_balance_type == EnergyBalanceType.none + assert u_config.momentum_balance_type == MomentumBalanceType.pressureTotal + assert u_config.finite_elements_ss_approximation == 9 assert ( - mr.fs.unit.config.film_transfer_coefficient_type - == FilmTransferCoefficientType.fixed + u_config.film_transfer_coefficient_type == FilmTransferCoefficientType.fixed ) assert ( - mr.fs.unit.config.surface_diffusion_coefficient_type + u_config.surface_diffusion_coefficient_type == SurfaceDiffusionCoefficientType.fixed ) - assert mr.fs.unit.config.finite_elements_ss_approximation == 9 # check properties - assert mr.fs.unit.config.property_package is mr.fs.properties - assert len(mr.fs.unit.config.property_package.solute_set) == 1 - assert len(mr.fs.unit.config.property_package.solvent_set) == 1 + assert u_config.property_package is mr.fs.properties + assert len(u_config.property_package.solute_set) == 1 + assert len(u_config.property_package.solvent_set) == 1 assert ( mr.fs.properties.config.diffus_calculation == DiffusivityCalculation.HaydukLaudie @@ -344,34 +295,30 @@ def test_robust_build(self, gac_frame_robust): port_lst = ["inlet", "outlet", "adsorbed"] for port_str in port_lst: port = getattr(mr.fs.unit, port_str) - assert len(port.vars) == 3 # number of state variables for property package + assert len(port.vars) == 3 assert isinstance(port, Port) # test statistics - assert number_variables(mr) == 119 - assert number_total_constraints(mr) == 84 - assert number_unused_variables(mr) == 10 # dens parameters from properties + assert istat.number_variables(mr) == 119 + assert istat.number_total_constraints(mr) == 84 + assert istat.number_unused_variables(mr) == 10 @pytest.mark.unit def test_robust_dof(self, gac_frame_robust): mr = gac_frame_robust - assert degrees_of_freedom(mr) == 0 + assert istat.degrees_of_freedom(mr) == 0 @pytest.mark.unit def test_robust_calculate_scaling(self, gac_frame_robust): mr = gac_frame_robust - mr.fs.properties.set_default_scaling( - "flow_mol_phase_comp", 1e-2, index=("Liq", "H2O") - ) - mr.fs.properties.set_default_scaling( - "flow_mol_phase_comp", 1e5, index=("Liq", "TCE") - ) - calculate_scaling_factors(mr) + prop = mr.fs.properties + prop.set_default_scaling("flow_mol_phase_comp", 1e-2, index=("Liq", "H2O")) + prop.set_default_scaling("flow_mol_phase_comp", 1e5, index=("Liq", "TCE")) + iscale.calculate_scaling_factors(mr) # check that all variables have scaling factors - unscaled_var_list = list(unscaled_variables_generator(mr)) - assert len(unscaled_var_list) == 0 + assert len(list(iscale.unscaled_variables_generator(mr))) == 0 @pytest.mark.component def test_robust_initialize(self, gac_frame_robust): @@ -380,10 +327,7 @@ def test_robust_initialize(self, gac_frame_robust): @pytest.mark.component def test_robust_var_scaling_init(self, gac_frame_robust): mr = gac_frame_robust - badly_scaled_var_lst = list( - badly_scaled_var_generator(mr, large=sv_large, small=sv_small, zero=sv_zero) - ) - assert badly_scaled_var_lst == [] + assert len(list(iscale.badly_scaled_var_generator(mr, zero=1e-8))) == 0 @pytest.mark.component def test_robust_solve(self, gac_frame_robust): @@ -391,43 +335,39 @@ def test_robust_solve(self, gac_frame_robust): results = solver.solve(mr) # Check for optimal solution - assert check_optimal_termination(results) + assert pyo.check_optimal_termination(results) @pytest.mark.component def test_robust_var_scaling_solve(self, gac_frame_robust): mr = gac_frame_robust - badly_scaled_var_lst = list( - badly_scaled_var_generator(mr, large=sv_large, small=sv_small, zero=sv_zero) - ) - assert badly_scaled_var_lst == [] + assert len(list(iscale.badly_scaled_var_generator(mr, zero=1e-8))) == 0 @pytest.mark.component def test_robust_solution(self, gac_frame_robust): mr = gac_frame_robust + gac = mr.fs.unit # values calculated by hand and match those reported in Crittenden, 2012 - assert pytest.approx(0.02097, rel=1e-3) == value(mr.fs.unit.equil_conc) - assert pytest.approx(42890, rel=1e-3) == value(mr.fs.unit.dg) - assert pytest.approx(45.79, rel=1e-3) == value(mr.fs.unit.N_Bi) - assert pytest.approx(36.64, rel=1e-3) == value(mr.fs.unit.min_N_St) - assert pytest.approx(1.139, rel=1e-3) == value(mr.fs.unit.throughput) - assert pytest.approx(395.9, rel=1e-3) == value(mr.fs.unit.min_residence_time) - assert pytest.approx(264.0, rel=1e-3) == value(mr.fs.unit.residence_time) - assert pytest.approx(19340000, rel=1e-3) == value( - mr.fs.unit.min_operational_time - ) - assert pytest.approx(13690000, rel=1e-3) == value(mr.fs.unit.operational_time) - assert pytest.approx(22810, rel=1e-3) == value(mr.fs.unit.bed_volumes_treated) - assert pytest.approx(0.003157, rel=1e-3) == value(mr.fs.unit.velocity_int) - assert pytest.approx(0.8333, rel=1e-3) == value(mr.fs.unit.bed_length) - assert pytest.approx(10.68, rel=1e-3) == value(mr.fs.unit.bed_area) - assert pytest.approx(8.900, rel=1e-3) == value(mr.fs.unit.bed_volume) - assert pytest.approx(3.688, rel=1e-3) == value(mr.fs.unit.bed_diameter) - assert pytest.approx(4004, rel=1e-3) == value(mr.fs.unit.bed_mass_gac) - assert pytest.approx(6462000, rel=1e-3) == value( - mr.fs.unit.ele_operational_time[1] + assert pytest.approx(0.02097, rel=1e-3) == pyo.value(gac.equil_conc) + assert pytest.approx(42890, rel=1e-3) == pyo.value(gac.dg) + assert pytest.approx(45.79, rel=1e-3) == pyo.value(gac.N_Bi) + assert pytest.approx(36.64, rel=1e-3) == pyo.value(gac.min_N_St) + assert pytest.approx(1.139, rel=1e-3) == pyo.value(gac.throughput) + assert pytest.approx(395.9, rel=1e-3) == pyo.value(gac.min_residence_time) + assert pytest.approx(264.0, rel=1e-3) == pyo.value(gac.residence_time) + assert pytest.approx(19340000, rel=1e-3) == pyo.value(gac.min_operational_time) + assert pytest.approx(13690000, rel=1e-3) == pyo.value(gac.operational_time) + assert pytest.approx(22810, rel=1e-3) == pyo.value(gac.bed_volumes_treated) + assert pytest.approx(0.003157, rel=1e-3) == pyo.value(gac.velocity_int) + assert pytest.approx(0.8333, rel=1e-3) == pyo.value(gac.bed_length) + assert pytest.approx(10.68, rel=1e-3) == pyo.value(gac.bed_area) + assert pytest.approx(8.900, rel=1e-3) == pyo.value(gac.bed_volume) + assert pytest.approx(3.688, rel=1e-3) == pyo.value(gac.bed_diameter) + assert pytest.approx(4004, rel=1e-3) == pyo.value(gac.bed_mass_gac) + assert pytest.approx(0.2287, rel=1e-3) == pyo.value(gac.conc_ratio_avg) + assert pytest.approx(6462000, rel=1e-3) == pyo.value( + gac.ele_operational_time[1] ) - assert pytest.approx(0.2287, rel=1e-3) == value(mr.fs.unit.conc_ratio_avg) @pytest.mark.component def test_robust_reporting(self, gac_frame_robust): @@ -446,43 +386,28 @@ def test_robust_costing_pressure(self, gac_frame_robust): ) # testing gac costing block dof and initialization - assert degrees_of_freedom(mr) == 0 + assert istat.degrees_of_freedom(mr) == 0 mr.fs.unit.costing.initialize() # solve results = solver.solve(mr) # Check for optimal solution - assert check_optimal_termination(results) + assert pyo.check_optimal_termination(results) + cost = mr.fs.unit.costing # Check for known cost solution of default twin alternating contactors - assert value(mr.fs.costing.gac.num_contactors_op) == 1 - assert value(mr.fs.costing.gac.num_contactors_redundant) == 1 - assert pytest.approx(56900, rel=1e-3) == value( - mr.fs.unit.costing.contactor_cost - ) - assert pytest.approx(4.359, rel=1e-3) == value( - mr.fs.unit.costing.adsorbent_unit_cost - ) - assert pytest.approx(17450, rel=1e-3) == value( - mr.fs.unit.costing.adsorbent_cost - ) - assert pytest.approx(81690, rel=1e-3) == value( - mr.fs.unit.costing.other_process_cost - ) - assert pytest.approx(156000, rel=1e-3) == value(mr.fs.unit.costing.capital_cost) - assert pytest.approx(12680, rel=1e-3) == value( - mr.fs.unit.costing.gac_makeup_cost - ) - assert pytest.approx(27660, rel=1e-3) == value( - mr.fs.unit.costing.gac_regen_cost - ) - assert pytest.approx(0.01631, rel=1e-3) == value( - mr.fs.unit.costing.energy_consumption - ) - assert pytest.approx(40370, rel=1e-3) == value( - mr.fs.unit.costing.fixed_operating_cost - ) + assert pyo.value(mr.fs.costing.gac.num_contactors_op) == 1 + assert pyo.value(mr.fs.costing.gac.num_contactors_redundant) == 1 + assert pytest.approx(56900, rel=1e-3) == pyo.value(cost.contactor_cost) + assert pytest.approx(4.359, rel=1e-3) == pyo.value(cost.adsorbent_unit_cost) + assert pytest.approx(17450, rel=1e-3) == pyo.value(cost.adsorbent_cost) + assert pytest.approx(81690, rel=1e-3) == pyo.value(cost.other_process_cost) + assert pytest.approx(156000, rel=1e-3) == pyo.value(cost.capital_cost) + assert pytest.approx(12680, rel=1e-3) == pyo.value(cost.gac_makeup_cost) + assert pytest.approx(27660, rel=1e-3) == pyo.value(cost.gac_regen_cost) + assert pytest.approx(0.01631, rel=1e-3) == pyo.value(cost.energy_consumption) + assert pytest.approx(40370, rel=1e-3) == pyo.value(cost.fixed_operating_cost) @pytest.mark.component def test_robust_costing_gravity(self, gac_frame_robust): @@ -499,38 +424,21 @@ def test_robust_costing_gravity(self, gac_frame_robust): results = solver.solve(mr_grav) # Check for optimal solution - assert check_optimal_termination(results) + assert pyo.check_optimal_termination(results) + cost = mr_grav.fs.unit.costing # Check for known cost solution of default twin alternating contactors - assert value(mr_grav.fs.costing.gac.num_contactors_op) == 1 - assert value(mr_grav.fs.costing.gac.num_contactors_redundant) == 1 - assert pytest.approx(163200, rel=1e-3) == value( - mr_grav.fs.unit.costing.contactor_cost - ) - assert pytest.approx(4.359, rel=1e-3) == value( - mr_grav.fs.unit.costing.adsorbent_unit_cost - ) - assert pytest.approx(17450, rel=1e-3) == value( - mr_grav.fs.unit.costing.adsorbent_cost - ) - assert pytest.approx(159500, rel=1e-3) == value( - mr_grav.fs.unit.costing.other_process_cost - ) - assert pytest.approx(340200, rel=1e-3) == value( - mr_grav.fs.unit.costing.capital_cost - ) - assert pytest.approx(12680, rel=1e-3) == value( - mr_grav.fs.unit.costing.gac_makeup_cost - ) - assert pytest.approx(27660, rel=1e-3) == value( - mr_grav.fs.unit.costing.gac_regen_cost - ) - assert pytest.approx(2.476, rel=1e-3) == value( - mr_grav.fs.unit.costing.energy_consumption - ) - assert pytest.approx(40370, rel=1e-3) == value( - mr_grav.fs.unit.costing.fixed_operating_cost - ) + assert pyo.value(mr_grav.fs.costing.gac.num_contactors_op) == 1 + assert pyo.value(mr_grav.fs.costing.gac.num_contactors_redundant) == 1 + assert pytest.approx(163200, rel=1e-3) == pyo.value(cost.contactor_cost) + assert pytest.approx(4.359, rel=1e-3) == pyo.value(cost.adsorbent_unit_cost) + assert pytest.approx(17450, rel=1e-3) == pyo.value(cost.adsorbent_cost) + assert pytest.approx(159500, rel=1e-3) == pyo.value(cost.other_process_cost) + assert pytest.approx(340200, rel=1e-3) == pyo.value(cost.capital_cost) + assert pytest.approx(12680, rel=1e-3) == pyo.value(cost.gac_makeup_cost) + assert pytest.approx(27660, rel=1e-3) == pyo.value(cost.gac_regen_cost) + assert pytest.approx(2.476, rel=1e-3) == pyo.value(cost.energy_consumption) + assert pytest.approx(40370, rel=1e-3) == pyo.value(cost.fixed_operating_cost) @pytest.mark.component def test_robust_costing_modular_contactors(self, gac_frame_robust): @@ -549,28 +457,22 @@ def test_robust_costing_modular_contactors(self, gac_frame_robust): results = solver.solve(mr) + cost = mr.fs.unit.costing # Check for known cost solution when changing volume scale of vessels in parallel - assert value(mr.fs.costing.gac.num_contactors_op) == 4 - assert value(mr.fs.costing.gac.num_contactors_redundant) == 2 - assert pytest.approx(89040, rel=1e-3) == value( - mr.fs.unit.costing.contactor_cost - ) - assert pytest.approx(69690, rel=1e-3) == value( - mr.fs.unit.costing.other_process_cost - ) - assert pytest.approx(176200, rel=1e-3) == value(mr.fs.unit.costing.capital_cost) + assert pyo.value(mr.fs.costing.gac.num_contactors_op) == 4 + assert pyo.value(mr.fs.costing.gac.num_contactors_redundant) == 2 + assert pytest.approx(89040, rel=1e-3) == pyo.value(cost.contactor_cost) + assert pytest.approx(69690, rel=1e-3) == pyo.value(cost.other_process_cost) + assert pytest.approx(176200, rel=1e-3) == pyo.value(cost.capital_cost) @pytest.mark.component def test_robust_costing_max_gac_ref(self, gac_frame_robust): mr = gac_frame_robust # scale flow up 10x - mr.fs.unit.process_flow.properties_in[0].flow_mol_phase_comp["Liq", "H2O"].fix( - 10 * 824.0736620370348 - ) - mr.fs.unit.process_flow.properties_in[0].flow_mol_phase_comp["Liq", "TCE"].fix( - 10 * 5.644342973110135e-05 - ) + unit_feed = mr.fs.unit.process_flow.properties_in[0] + unit_feed.flow_mol_phase_comp["Liq", "H2O"].fix(10 * 824.0736620370348) + unit_feed.flow_mol_phase_comp["Liq", "TCE"].fix(10 * 5.644342973110135e-05) mr.fs.costing = WaterTAPCosting() mr.fs.costing.base_currency = pyo.units.USD_2020 @@ -579,15 +481,17 @@ def test_robust_costing_max_gac_ref(self, gac_frame_robust): flowsheet_costing_block=mr.fs.costing ) mr.fs.costing.cost_process() - # not necessarily an optimum solution because poor scaling but just checking the conditional + # not necessarily an optimum solution because poor scaling + # but just checking the conditional results = solver.solve(mr) - # Check for bed_mass_gac_cost_ref to be overwritten if bed_mass_gac is greater than bed_mass_gac_cost_max_ref - assert value(mr.fs.unit.bed_mass_gac) > value( + # Check for bed_mass_gac_cost_ref to be overwritten + # if bed_mass_gac is greater than bed_mass_gac_cost_max_ref + assert pyo.value(mr.fs.unit.bed_mass_gac) > pyo.value( mr.fs.costing.gac.bed_mass_max_ref ) - assert value(mr.fs.unit.costing.bed_mass_gac_ref) == ( - pytest.approx(value(mr.fs.costing.gac.bed_mass_max_ref), 1e-5) + assert pyo.value(mr.fs.unit.costing.bed_mass_gac_ref) == ( + pytest.approx(pyo.value(mr.fs.costing.gac.bed_mass_max_ref), 1e-5) ) @@ -595,7 +499,7 @@ def test_robust_costing_max_gac_ref(self, gac_frame_robust): class TestGACMulti: @pytest.fixture(scope="class") def gac_frame_multi(self): - mm = ConcreteModel() + mm = pyo.ConcreteModel() mm.fs = FlowsheetBlock(dynamic=False) # inserting arbitrary BackGround Solutes, Cations, and Anions to check handling @@ -628,28 +532,15 @@ def gac_frame_multi(self): target_species={"TCE"}, ) + unit_feed = mm.fs.unit.process_flow.properties_in[0] # feed specifications - mm.fs.unit.process_flow.properties_in[0].pressure.fix( - 101325 - ) # feed pressure [Pa] - mm.fs.unit.process_flow.properties_in[0].temperature.fix( - 273.15 + 25 - ) # feed temperature [K] - mm.fs.unit.process_flow.properties_in[0].flow_mol_phase_comp["Liq", "H2O"].fix( - 824.0736620370348 - ) - mm.fs.unit.process_flow.properties_in[0].flow_mol_phase_comp["Liq", "TCE"].fix( - 5.644342973110135e-05 - ) - mm.fs.unit.process_flow.properties_in[0].flow_mol_phase_comp[ - "Liq", "BGSOL" - ].fix(5e-05) - mm.fs.unit.process_flow.properties_in[0].flow_mol_phase_comp[ - "Liq", "BGCAT" - ].fix(2e-05) - mm.fs.unit.process_flow.properties_in[0].flow_mol_phase_comp["Liq", "BGAN"].fix( - 1e-05 - ) + unit_feed.pressure.fix(101325) # feed pressure [Pa] + unit_feed.temperature.fix(273.15 + 25) # feed temperature [K] + unit_feed.flow_mol_phase_comp["Liq", "H2O"].fix(824.0736620370348) + unit_feed.flow_mol_phase_comp["Liq", "TCE"].fix(5.644342973110135e-05) + unit_feed.flow_mol_phase_comp["Liq", "BGSOL"].fix(5e-05) + unit_feed.flow_mol_phase_comp["Liq", "BGCAT"].fix(2e-05) + unit_feed.flow_mol_phase_comp["Liq", "BGAN"].fix(1e-05) # trial problem from Crittenden, 2012 for removal of TCE # adsorption isotherm @@ -682,52 +573,46 @@ def gac_frame_multi(self): @pytest.mark.unit def test_multi_config(self, gac_frame_multi): mm = gac_frame_multi + u_config = mm.fs.unit.config # checking non-unity solute set and nonzero ion set handling - assert len(mm.fs.unit.config.property_package.solute_set) == 4 - assert len(mm.fs.unit.config.property_package.solvent_set) == 1 - assert len(mm.fs.unit.config.property_package.ion_set) == 2 - assert ( - mm.fs.properties.config.diffus_calculation - == DiffusivityCalculation.HaydukLaudie - ) + assert len(u_config.property_package.solute_set) == 4 + assert len(u_config.property_package.solvent_set) == 1 + assert len(u_config.property_package.ion_set) == 2 assert ( - mm.fs.unit.config.film_transfer_coefficient_type + u_config.film_transfer_coefficient_type == FilmTransferCoefficientType.calculated ) assert ( - mm.fs.unit.config.surface_diffusion_coefficient_type + u_config.surface_diffusion_coefficient_type == SurfaceDiffusionCoefficientType.calculated ) + assert ( + mm.fs.properties.config.diffus_calculation + == DiffusivityCalculation.HaydukLaudie + ) - assert degrees_of_freedom(mm) == 0 + assert istat.degrees_of_freedom(mm) == 0 @pytest.mark.unit def test_multi_calculate_scaling(self, gac_frame_multi): mm = gac_frame_multi - mm.fs.properties.set_default_scaling( - "flow_mol_phase_comp", 1e-2, index=("Liq", "H2O") - ) - for j in mm.fs.properties.ion_set | mm.fs.properties.solute_set: - mm.fs.properties.set_default_scaling( - "flow_mol_phase_comp", 1e5, index=("Liq", j) - ) + prop = mm.fs.properties + prop.set_default_scaling("flow_mol_phase_comp", 1e-2, index=("Liq", "H2O")) + for j in prop.ion_set | prop.solute_set: + prop.set_default_scaling("flow_mol_phase_comp", 1e5, index=("Liq", j)) - calculate_scaling_factors(mm) + iscale.calculate_scaling_factors(mm) initialization_tester(gac_frame_multi) # check that all variables have scaling factors - unscaled_var_list = list(unscaled_variables_generator(mm)) - assert len(unscaled_var_list) == 0 + assert len(list(iscale.unscaled_variables_generator(mm))) == 0 @pytest.mark.unit def test_multi_var_scaling_init(self, gac_frame_multi): mm = gac_frame_multi - badly_scaled_var_lst = list( - badly_scaled_var_generator(mm, large=sv_large, small=sv_small, zero=sv_zero) - ) - assert badly_scaled_var_lst == [] + assert len(list(iscale.badly_scaled_var_generator(mm, zero=1e-8))) == 0 @pytest.mark.component def test_multi_solve(self, gac_frame_multi): @@ -735,25 +620,22 @@ def test_multi_solve(self, gac_frame_multi): results = solver.solve(mm) # Check for optimal solution - assert check_optimal_termination(results) + assert pyo.check_optimal_termination(results) @pytest.mark.unit def test_multi_var_scaling_solve(self, gac_frame_multi): mm = gac_frame_multi - badly_scaled_var_lst = list( - badly_scaled_var_generator(mm, large=sv_large, small=sv_small, zero=sv_zero) - ) - assert badly_scaled_var_lst == [] + assert len(list(iscale.badly_scaled_var_generator(mm, zero=1e-8))) == 0 @pytest.mark.component def test_multi_solution(self, gac_frame_multi): mm = gac_frame_multi # only checking for variables new to configuration options - assert pytest.approx(2.473, rel=1e-3) == value(mm.fs.unit.N_Re) - assert pytest.approx(2001, rel=1e-3) == value(mm.fs.unit.N_Sc) - assert pytest.approx(2.600e-5, rel=1e-3) == value(mm.fs.unit.kf) - assert pytest.approx(1.245e-14, rel=1e-3) == value(mm.fs.unit.ds) + assert pytest.approx(2.473, rel=1e-3) == pyo.value(mm.fs.unit.N_Re) + assert pytest.approx(2001, rel=1e-3) == pyo.value(mm.fs.unit.N_Sc) + assert pytest.approx(2.600e-5, rel=1e-3) == pyo.value(mm.fs.unit.kf) + assert pytest.approx(1.245e-14, rel=1e-3) == pyo.value(mm.fs.unit.ds) @pytest.mark.component def test_multi_reporting(self, gac_frame_multi): @@ -772,7 +654,7 @@ def test_error(self): "either specify 'target species' argument or reduce solute set " "to a single component", ): - me = ConcreteModel() + me = pyo.ConcreteModel() me.fs = FlowsheetBlock(dynamic=False) # inserting arbitrary BackGround Solutes, Cations, and Anions to check handling @@ -810,7 +692,7 @@ def test_error(self): match="fs.unit received invalid argument for contactor_type:" " vessel. Argument must be a member of the ContactorType Enum.", ): - me = ConcreteModel() + me = pyo.ConcreteModel() me.fs = FlowsheetBlock(dynamic=False) me.fs.properties = MCASParameterBlock( @@ -840,7 +722,7 @@ def test_error(self): ConfigurationError, match="item 0 within 'target_species' list is not of data type str", ): - me = ConcreteModel() + me = pyo.ConcreteModel() me.fs = FlowsheetBlock(dynamic=False) # inserting arbitrary BackGround Solutes, Cations, and Anions to check handling @@ -878,7 +760,7 @@ def test_error(self): ConfigurationError, match="item species within 'target_species' list is not in 'component_list", ): - me = ConcreteModel() + me = pyo.ConcreteModel() me.fs = FlowsheetBlock(dynamic=False) # inserting arbitrary BackGround Solutes, Cations, and Anions to check handling diff --git a/watertap/unit_models/tests/test_nanofiltration_ZO.py b/watertap/unit_models/tests/test_nanofiltration_ZO.py index fe3e4968ff..d82befdf40 100644 --- a/watertap/unit_models/tests/test_nanofiltration_ZO.py +++ b/watertap/unit_models/tests/test_nanofiltration_ZO.py @@ -30,7 +30,8 @@ GenericParameterBlock, ) from watertap.property_models.seawater_ion_generic import configuration -import watertap.examples.flowsheets.full_treatment_train.model_components.seawater_ion_prop_pack as props +import watertap.property_models.seawater_ion_prop_pack as props + from watertap.core.util.initialization import assert_no_degrees_of_freedom from pyomo.util.check_units import assert_units_consistent diff --git a/watertap/unit_models/tests/test_osmotically_assisted_reverse_osmosis_0D.py b/watertap/unit_models/tests/test_osmotically_assisted_reverse_osmosis_0D.py index 4de9b7a7a9..0d8db42df1 100644 --- a/watertap/unit_models/tests/test_osmotically_assisted_reverse_osmosis_0D.py +++ b/watertap/unit_models/tests/test_osmotically_assisted_reverse_osmosis_0D.py @@ -26,7 +26,6 @@ MaterialBalanceType, EnergyBalanceType, MomentumBalanceType, - ControlVolume0DBlock, FlowDirection, ) from watertap.unit_models.osmotically_assisted_reverse_osmosis_0D import ( @@ -45,7 +44,6 @@ from idaes.core.util.scaling import ( calculate_scaling_factors, unscaled_variables_generator, - unscaled_constraints_generator, badly_scaled_var_generator, ) diff --git a/watertap/unit_models/tests/test_pressure_exchanger.py b/watertap/unit_models/tests/test_pressure_exchanger.py index 09f33346a7..03024b303c 100644 --- a/watertap/unit_models/tests/test_pressure_exchanger.py +++ b/watertap/unit_models/tests/test_pressure_exchanger.py @@ -28,7 +28,7 @@ ) from watertap.unit_models.pressure_exchanger import PressureExchanger import watertap.property_models.seawater_prop_pack as props -import watertap.examples.flowsheets.full_treatment_train.model_components.seawater_ion_prop_pack as property_seawater_ions +import watertap.property_models.seawater_ion_prop_pack as property_seawater_ions from idaes.core.util.model_statistics import ( degrees_of_freedom, diff --git a/watertap/unit_models/tests/test_thickener_unit.py b/watertap/unit_models/tests/test_thickener_unit.py new file mode 100644 index 0000000000..6229baa7c5 --- /dev/null +++ b/watertap/unit_models/tests/test_thickener_unit.py @@ -0,0 +1,278 @@ +################################################################################# +# WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# National Renewable Energy Laboratory, and National Energy Technology +# Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +################################################################################# +""" +Tests for thickener unit example. +""" + +import pytest +from pyomo.environ import ( + ConcreteModel, + value, + assert_optimal_termination, +) + +from idaes.core import ( + FlowsheetBlock, + MaterialBalanceType, + MomentumBalanceType, +) + +from idaes.models.unit_models.separator import SplittingType + +from pyomo.environ import ( + units, +) + +from idaes.core.solvers import get_solver +from idaes.core.util.model_statistics import ( + degrees_of_freedom, + number_variables, + number_total_constraints, + number_unused_variables, +) +import idaes.core.util.scaling as iscale +from idaes.core.util.testing import ( + initialization_tester, +) + +from idaes.core.util.exceptions import ( + ConfigurationError, +) +from watertap.unit_models.thickener import Thickener +from watertap.property_models.activated_sludge.asm1_properties import ( + ASM1ParameterBlock, +) + +from pyomo.util.check_units import assert_units_consistent + + +# ----------------------------------------------------------------------------- +# Get default solver for testing +solver = get_solver() + + +# ----------------------------------------------------------------------------- +@pytest.mark.unit +def test_config(): + m = ConcreteModel() + + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.props = ASM1ParameterBlock() + + m.fs.unit = Thickener(property_package=m.fs.props) + + assert len(m.fs.unit.config) == 15 + + assert not m.fs.unit.config.dynamic + assert not m.fs.unit.config.has_holdup + assert m.fs.unit.config.material_balance_type == MaterialBalanceType.useDefault + assert m.fs.unit.config.momentum_balance_type == MomentumBalanceType.pressureTotal + assert "underflow" in m.fs.unit.config.outlet_list + assert "overflow" in m.fs.unit.config.outlet_list + assert SplittingType.componentFlow is m.fs.unit.config.split_basis + + +@pytest.mark.unit +def test_list_error(): + m = ConcreteModel() + + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.props = ASM1ParameterBlock() + + with pytest.raises( + ConfigurationError, + match="fs.unit encountered unrecognised " + "outlet_list. This should not " + "occur - please use overflow " + "and underflow as outlets.", + ): + m.fs.unit = Thickener( + property_package=m.fs.props, outlet_list=["outlet1", "outlet2"] + ) + + +# ----------------------------------------------------------------------------- +class TestThick(object): + @pytest.fixture(scope="class") + def tu(self): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.props = ASM1ParameterBlock() + + m.fs.unit = Thickener(property_package=m.fs.props) + + m.fs.unit.inlet.flow_vol.fix(300 * units.m**3 / units.day) + m.fs.unit.inlet.temperature.fix(308.15 * units.K) + m.fs.unit.inlet.pressure.fix(1 * units.atm) + + m.fs.unit.inlet.conc_mass_comp[0, "S_I"].fix(28.0643 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_S"].fix(0.67336 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_I"].fix(3036.2175 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_S"].fix(63.2392 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_BH"].fix( + 4442.8377 * units.mg / units.liter + ) + m.fs.unit.inlet.conc_mass_comp[0, "X_BA"].fix(332.5958 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_P"].fix(1922.8108 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_O"].fix(1.3748 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_NO"].fix(9.1948 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_NH"].fix(0.15845 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_ND"].fix(0.55943 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_ND"].fix(4.7411 * units.mg / units.liter) + m.fs.unit.inlet.alkalinity.fix(4.5646 * units.mol / units.m**3) + + return m + + @pytest.mark.build + @pytest.mark.unit + def test_build(self, tu): + + assert hasattr(tu.fs.unit, "inlet") + assert len(tu.fs.unit.inlet.vars) == 5 + assert hasattr(tu.fs.unit.inlet, "flow_vol") + assert hasattr(tu.fs.unit.inlet, "conc_mass_comp") + assert hasattr(tu.fs.unit.inlet, "temperature") + assert hasattr(tu.fs.unit.inlet, "pressure") + assert hasattr(tu.fs.unit.inlet, "alkalinity") + + assert hasattr(tu.fs.unit, "underflow") + assert len(tu.fs.unit.underflow.vars) == 5 + assert hasattr(tu.fs.unit.underflow, "flow_vol") + assert hasattr(tu.fs.unit.underflow, "conc_mass_comp") + assert hasattr(tu.fs.unit.underflow, "temperature") + assert hasattr(tu.fs.unit.underflow, "pressure") + assert hasattr(tu.fs.unit.underflow, "alkalinity") + + assert hasattr(tu.fs.unit, "overflow") + assert len(tu.fs.unit.overflow.vars) == 5 + assert hasattr(tu.fs.unit.overflow, "flow_vol") + assert hasattr(tu.fs.unit.overflow, "conc_mass_comp") + assert hasattr(tu.fs.unit.overflow, "temperature") + assert hasattr(tu.fs.unit.overflow, "pressure") + assert hasattr(tu.fs.unit.overflow, "alkalinity") + + assert number_variables(tu) == 76 + assert number_total_constraints(tu) == 60 + assert number_unused_variables(tu) == 0 + + @pytest.mark.unit + def test_dof(self, tu): + assert degrees_of_freedom(tu) == 0 + + @pytest.mark.unit + def test_units(self, tu): + assert_units_consistent(tu) + + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_initialize(self, tu): + + iscale.calculate_scaling_factors(tu) + initialization_tester(tu) + + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_solve(self, tu): + solver = get_solver() + results = solver.solve(tu) + assert_optimal_termination(results) + + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_solution(self, tu): + assert pytest.approx(101325.0, rel=1e-3) == value( + tu.fs.unit.overflow.pressure[0] + ) + assert pytest.approx(308.15, rel=1e-3) == value( + tu.fs.unit.overflow.temperature[0] + ) + assert pytest.approx(0.003115, rel=1e-3) == value( + tu.fs.unit.overflow.flow_vol[0] + ) + assert pytest.approx(0.02806, rel=1e-3) == value( + tu.fs.unit.overflow.conc_mass_comp[0, "S_I"] + ) + assert pytest.approx(0.000673, rel=1e-3) == value( + tu.fs.unit.overflow.conc_mass_comp[0, "S_S"] + ) + assert pytest.approx(0.06768, rel=1e-3) == value( + tu.fs.unit.overflow.conc_mass_comp[0, "X_I"] + ) + assert pytest.approx(0.001409, rel=1e-3) == value( + tu.fs.unit.overflow.conc_mass_comp[0, "X_S"] + ) + assert pytest.approx(0.04286, rel=1e-3) == value( + tu.fs.unit.overflow.conc_mass_comp[0, "X_P"] + ) + assert pytest.approx(0.099046, rel=1e-3) == value( + tu.fs.unit.overflow.conc_mass_comp[0, "X_BH"] + ) + assert pytest.approx(0.007414, rel=1e-3) == value( + tu.fs.unit.overflow.conc_mass_comp[0, "X_BA"] + ) + assert pytest.approx(0.001374, rel=1e-3) == value( + tu.fs.unit.overflow.conc_mass_comp[0, "S_O"] + ) + assert pytest.approx(0.009194, rel=1e-3) == value( + tu.fs.unit.overflow.conc_mass_comp[0, "S_NO"] + ) + assert pytest.approx(0.0001584, rel=1e-3) == value( + tu.fs.unit.overflow.conc_mass_comp[0, "S_NH"] + ) + assert pytest.approx(0.0005594, rel=1e-3) == value( + tu.fs.unit.overflow.conc_mass_comp[0, "S_ND"] + ) + assert pytest.approx(0.0001056, rel=1e-3) == value( + tu.fs.unit.overflow.conc_mass_comp[0, "X_ND"] + ) + assert pytest.approx(0.004564, rel=1e-3) == value( + tu.fs.unit.overflow.alkalinity[0] + ) + + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_conservation(self, tu): + assert ( + abs( + value( + tu.fs.unit.inlet.flow_vol[0] * tu.fs.props.dens_mass + - tu.fs.unit.overflow.flow_vol[0] * tu.fs.props.dens_mass + - tu.fs.unit.underflow.flow_vol[0] * tu.fs.props.dens_mass + ) + ) + <= 1e-6 + ) + for i in tu.fs.props.solute_set: + assert ( + abs( + value( + tu.fs.unit.inlet.flow_vol[0] + * tu.fs.unit.inlet.conc_mass_comp[0, i] + - tu.fs.unit.overflow.flow_vol[0] + * tu.fs.unit.overflow.conc_mass_comp[0, i] + - tu.fs.unit.underflow.flow_vol[0] + * tu.fs.unit.underflow.conc_mass_comp[0, i] + ) + ) + <= 1e-6 + ) + + @pytest.mark.unit + def test_report(self, tu): + tu.fs.unit.report() diff --git a/watertap/unit_models/thickener.py b/watertap/unit_models/thickener.py new file mode 100644 index 0000000000..721c88f3bf --- /dev/null +++ b/watertap/unit_models/thickener.py @@ -0,0 +1,167 @@ +############################################################################### +# WaterTAP Copyright (c) 2021, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National +# Laboratory, National Renewable Energy Laboratory, and National Energy +# Technology Laboratory (subject to receipt of any required approvals from +# the U.S. Dept. of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +# +############################################################################### +""" +Thickener unit model for BSM2. Based on IDAES separator unit + +Model based on + +J. Alex, L. Benedetti, J.B. Copp, K.V. Gernaey, U. Jeppsson, +I. Nopens, M.N. Pons, C. Rosen, J.P. Steyer and +P. A. Vanrolleghem +Benchmark Simulation Model no. 2 (BSM2) +""" + +# Import IDAES cores +from idaes.core import ( + declare_process_block_class, +) +from idaes.models.unit_models.separator import SeparatorData, SplittingType + +from idaes.core.util.tables import create_stream_table_dataframe +import idaes.logger as idaeslog + +from pyomo.environ import ( + Param, + units as pyunits, + Set, +) + +from idaes.core.util.exceptions import ( + ConfigurationError, +) + +__author__ = "Alejandro Garciadiego" + + +# Set up logger +_log = idaeslog.getLogger(__name__) + + +@declare_process_block_class("Thickener") +class ThickenerData(SeparatorData): + """ + Thickener unit block for BSM2 + """ + + CONFIG = SeparatorData.CONFIG() + CONFIG.outlet_list = ["underflow", "overflow"] + CONFIG.split_basis = SplittingType.componentFlow + + def build(self): + """ + Begin building model. + Args: + None + Returns: + None + """ + + # Call UnitModel.build to set up dynamics + super(ThickenerData, self).build() + + if "underflow" and "overflow" not in self.config.outlet_list: + raise ConfigurationError( + "{} encountered unrecognised " + "outlet_list. This should not " + "occur - please use overflow " + "and underflow as outlets.".format(self.name) + ) + + self.p_thick = Param( + initialize=0.07, + units=pyunits.dimensionless, + mutable=True, + doc="Percentage of suspended solids in the underflow", + ) + + self.TSS_rem = Param( + initialize=0.98, + units=pyunits.dimensionless, + mutable=True, + doc="Percentage of suspended solids removed", + ) + + @self.Expression(self.flowsheet().time, doc="Suspended solid concentration") + def TSS(blk, t): + return 0.75 * ( + blk.inlet.conc_mass_comp[t, "X_I"] + + blk.inlet.conc_mass_comp[t, "X_P"] + + blk.inlet.conc_mass_comp[t, "X_BH"] + + blk.inlet.conc_mass_comp[t, "X_BA"] + + blk.inlet.conc_mass_comp[t, "X_S"] + ) + + @self.Expression(self.flowsheet().time, doc="Thickening factor") + def f_thick(blk, t): + return blk.p_thick * (10 / (blk.TSS[t])) + + @self.Expression(self.flowsheet().time, doc="Remove factor") + def f_q_du(blk, t): + return blk.TSS_rem / (pyunits.kg / pyunits.m**3) / 100 / blk.f_thick[t] + + self.non_particulate_components = Set( + initialize=[ + "S_I", + "S_S", + "S_O", + "S_NO", + "S_NH", + "S_ND", + "H2O", + "S_ALK", + ] + ) + + self.particulate_components = Set( + initialize=["X_I", "X_S", "X_P", "X_BH", "X_BA", "X_ND"] + ) + + @self.Constraint( + self.flowsheet().time, + self.particulate_components, + doc="particulate fraction", + ) + def overflow_particulate_fraction(blk, t, i): + return blk.split_fraction[t, "overflow", i] == 1 - blk.TSS_rem + + @self.Constraint( + self.flowsheet().time, + self.non_particulate_components, + doc="soluble fraction", + ) + def non_particulate_components(blk, t, i): + return blk.split_fraction[t, "overflow", i] == 1 - blk.f_q_du[t] + + def _get_performance_contents(self, time_point=0): + if hasattr(self, "split_fraction"): + var_dict = {} + for k in self.split_fraction.keys(): + if k[0] == time_point: + var_dict[f"Split Fraction [{str(k[1:])}]"] = self.split_fraction[k] + return {"vars": var_dict} + else: + return None + + def _get_stream_table_contents(self, time_point=0): + outlet_list = self.create_outlet_list() + + io_dict = {} + if self.config.mixed_state_block is None: + io_dict["Inlet"] = self.mixed_state + else: + io_dict["Inlet"] = self.config.mixed_state_block + + for o in outlet_list: + io_dict[o] = getattr(self, o + "_state") + + return create_stream_table_dataframe(io_dict, time_point=time_point) diff --git a/watertap/unit_models/translators/tests/test_translator_adm1_asm1.py b/watertap/unit_models/translators/tests/test_translator_adm1_asm1.py index f46b0b4c17..115bd18c55 100644 --- a/watertap/unit_models/translators/tests/test_translator_adm1_asm1.py +++ b/watertap/unit_models/translators/tests/test_translator_adm1_asm1.py @@ -26,16 +26,9 @@ assert_optimal_termination, ) -from idaes.core import ( - FlowsheetBlock, - MaterialBalanceType, - EnergyBalanceType, - MomentumBalanceType, -) +from idaes.core import FlowsheetBlock -from pyomo.environ import ( - units, -) +from pyomo.environ import units from idaes.core.solvers import get_solver from idaes.core.util.model_statistics import ( @@ -43,11 +36,6 @@ number_variables, number_total_constraints, number_unused_variables, - unused_variables_set, -) - -from idaes.core.util.scaling import ( - unscaled_variables_generator, ) from idaes.core.util.testing import initialization_tester @@ -66,7 +54,7 @@ ) -from pyomo.util.check_units import assert_units_consistent, assert_units_equivalent +from pyomo.util.check_units import assert_units_consistent # ----------------------------------------------------------------------------- # Get default solver for testing diff --git a/watertap/unit_models/translators/tests/test_translator_adm1_asm2d.py b/watertap/unit_models/translators/tests/test_translator_adm1_asm2d.py new file mode 100644 index 0000000000..b4f1141b42 --- /dev/null +++ b/watertap/unit_models/translators/tests/test_translator_adm1_asm2d.py @@ -0,0 +1,332 @@ +################################################################################# +# WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# National Renewable Energy Laboratory, and National Energy Technology +# Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +################################################################################# +""" +Tests for Translator ADM1-ASM2D unit model. +Verified against approximated results from: +Flores-Alsina, X., Solon, K., Mbamba, C.K., Tait, S., Gernaey, K.V., Jeppsson, U. and Batstone, D.J., 2016. +Modelling phosphorus (P), sulfur (S) and iron (Fe) interactions for dynamic simulations of anaerobic digestion processes. +Water Research, 95, pp.370-382. +""" + +import pytest +from pyomo.environ import ( + ConcreteModel, + value, + assert_optimal_termination, +) + +from idaes.core import ( + FlowsheetBlock, + MaterialBalanceType, + EnergyBalanceType, + MomentumBalanceType, +) + +from pyomo.environ import ( + units, +) + +from idaes.core.solvers import get_solver +from idaes.core.util.model_statistics import ( + degrees_of_freedom, + number_variables, + number_total_constraints, + number_unused_variables, + unused_variables_set, +) + +from idaes.core.util.scaling import ( + unscaled_variables_generator, +) + +from idaes.core.util.testing import initialization_tester + +from watertap.unit_models.translators.translator_adm1_asm2d import Translator_ADM1_ASM2D +from watertap.property_models.anaerobic_digestion.modified_adm1_properties import ( + ModifiedADM1ParameterBlock, +) + +from watertap.property_models.activated_sludge.modified_asm2d_properties import ( + ModifiedASM2dParameterBlock, +) + +from watertap.property_models.anaerobic_digestion.modified_adm1_reactions import ( + ModifiedADM1ReactionParameterBlock, +) + + +from pyomo.util.check_units import assert_units_consistent, assert_units_equivalent + +# ----------------------------------------------------------------------------- +# Get default solver for testing +solver = get_solver() + +# ----------------------------------------------------------------------------- +@pytest.mark.unit +def test_config(): + m = ConcreteModel() + + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.props_ASM2D = ModifiedASM2dParameterBlock( + additional_solute_list=["S_K", "S_Mg"] + ) + m.fs.props_ADM1 = ModifiedADM1ParameterBlock() + m.fs.ADM1_rxn_props = ModifiedADM1ReactionParameterBlock( + property_package=m.fs.props_ADM1 + ) + + m.fs.unit = Translator_ADM1_ASM2D( + inlet_property_package=m.fs.props_ADM1, + outlet_property_package=m.fs.props_ASM2D, + reaction_package=m.fs.ADM1_rxn_props, + has_phase_equilibrium=False, + outlet_state_defined=True, + ) + + assert len(m.fs.unit.config) == 10 + + assert m.fs.unit.config.outlet_state_defined == True + assert not m.fs.unit.config.dynamic + assert not m.fs.unit.config.has_holdup + assert not m.fs.unit.config.has_phase_equilibrium + assert m.fs.unit.config.inlet_property_package is m.fs.props_ADM1 + assert m.fs.unit.config.outlet_property_package is m.fs.props_ASM2D + assert m.fs.unit.config.reaction_package is m.fs.ADM1_rxn_props + + +# ----------------------------------------------------------------------------- +class TestAsm2dAdm1(object): + @pytest.fixture(scope="class") + def asmadm(self): + m = ConcreteModel() + + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.props_ASM2D = ModifiedASM2dParameterBlock( + additional_solute_list=["S_K", "S_Mg"] + ) + m.fs.props_ADM1 = ModifiedADM1ParameterBlock() + m.fs.ADM1_rxn_props = ModifiedADM1ReactionParameterBlock( + property_package=m.fs.props_ADM1 + ) + + m.fs.unit = Translator_ADM1_ASM2D( + inlet_property_package=m.fs.props_ADM1, + outlet_property_package=m.fs.props_ASM2D, + reaction_package=m.fs.ADM1_rxn_props, + has_phase_equilibrium=False, + outlet_state_defined=True, + ) + + m.fs.unit.inlet.flow_vol.fix(170 * units.m**3 / units.day) + m.fs.unit.inlet.temperature.fix(308.15 * units.K) + m.fs.unit.inlet.pressure.fix(1 * units.atm) + + m.fs.unit.inlet.conc_mass_comp[0, "S_su"].fix(0.034597) + m.fs.unit.inlet.conc_mass_comp[0, "S_aa"].fix(0.015037) + m.fs.unit.inlet.conc_mass_comp[0, "S_fa"].fix(1e-6) + m.fs.unit.inlet.conc_mass_comp[0, "S_va"].fix(1e-6) + m.fs.unit.inlet.conc_mass_comp[0, "S_bu"].fix(1e-6) + m.fs.unit.inlet.conc_mass_comp[0, "S_pro"].fix(1e-6) + m.fs.unit.inlet.conc_mass_comp[0, "S_ac"].fix(0.025072) + m.fs.unit.inlet.conc_mass_comp[0, "S_h2"].fix(1e-6) + m.fs.unit.inlet.conc_mass_comp[0, "S_ch4"].fix(1e-6) + m.fs.unit.inlet.conc_mass_comp[0, "S_IC"].fix(0.34628) + m.fs.unit.inlet.conc_mass_comp[0, "S_IN"].fix(0.60014) + m.fs.unit.inlet.conc_mass_comp[0, "S_IP"].fix(0.22677) + m.fs.unit.inlet.conc_mass_comp[0, "S_I"].fix(0.026599) + + m.fs.unit.inlet.conc_mass_comp[0, "X_ch"].fix(7.3687) + m.fs.unit.inlet.conc_mass_comp[0, "X_pr"].fix(7.7308) + m.fs.unit.inlet.conc_mass_comp[0, "X_li"].fix(10.3288) + m.fs.unit.inlet.conc_mass_comp[0, "X_su"].fix(1e-6) + m.fs.unit.inlet.conc_mass_comp[0, "X_aa"].fix(1e-6) + m.fs.unit.inlet.conc_mass_comp[0, "X_fa"].fix(1e-6) + m.fs.unit.inlet.conc_mass_comp[0, "X_c4"].fix(1e-6) + m.fs.unit.inlet.conc_mass_comp[0, "X_pro"].fix(1e-6) + m.fs.unit.inlet.conc_mass_comp[0, "X_ac"].fix(1e-6) + m.fs.unit.inlet.conc_mass_comp[0, "X_h2"].fix(1e-6) + m.fs.unit.inlet.conc_mass_comp[0, "X_I"].fix(12.7727) + m.fs.unit.inlet.conc_mass_comp[0, "X_PHA"].fix(0.0022493) + m.fs.unit.inlet.conc_mass_comp[0, "X_PP"].fix(1.04110) + m.fs.unit.inlet.conc_mass_comp[0, "X_PAO"].fix(3.4655) + m.fs.unit.inlet.conc_mass_comp[0, "S_K"].fix(0.02268) + m.fs.unit.inlet.conc_mass_comp[0, "S_Mg"].fix(0.02893) + + m.fs.unit.inlet.cations[0].fix(0.04) + m.fs.unit.inlet.anions[0].fix(0.02) + + return m + + @pytest.mark.build + @pytest.mark.unit + def test_build(self, asmadm): + + assert hasattr(asmadm.fs.unit, "inlet") + assert len(asmadm.fs.unit.inlet.vars) == 6 + assert hasattr(asmadm.fs.unit.inlet, "flow_vol") + assert hasattr(asmadm.fs.unit.inlet, "conc_mass_comp") + assert hasattr(asmadm.fs.unit.inlet, "temperature") + assert hasattr(asmadm.fs.unit.inlet, "pressure") + assert hasattr(asmadm.fs.unit.inlet, "anions") + assert hasattr(asmadm.fs.unit.inlet, "cations") + + assert hasattr(asmadm.fs.unit, "outlet") + assert len(asmadm.fs.unit.outlet.vars) == 5 + assert hasattr(asmadm.fs.unit.outlet, "flow_vol") + assert hasattr(asmadm.fs.unit.outlet, "conc_mass_comp") + assert hasattr(asmadm.fs.unit.outlet, "temperature") + assert hasattr(asmadm.fs.unit.outlet, "pressure") + assert hasattr(asmadm.fs.unit.outlet, "alkalinity") + + assert number_variables(asmadm) == 183 + assert number_total_constraints(asmadm) == 24 + + assert number_unused_variables(asmadm.fs.unit) == 12 + + @pytest.mark.component + def test_units(self, asmadm): + assert_units_consistent(asmadm) + + @pytest.mark.unit + def test_dof(self, asmadm): + assert degrees_of_freedom(asmadm) == 0 + + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_initialize(self, asmadm): + initialization_tester(asmadm) + + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_solve(self, asmadm): + solver = get_solver() + results = solver.solve(asmadm) + assert_optimal_termination(results) + + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_solution(self, asmadm): + assert pytest.approx(101325.0, rel=1e-3) == value( + asmadm.fs.unit.outlet.pressure[0] + ) + assert pytest.approx(308.15, rel=1e-3) == value( + asmadm.fs.unit.outlet.temperature[0] + ) + assert pytest.approx(0.025075, rel=1e-3) == value( + asmadm.fs.unit.outlet.conc_mass_comp[0, "S_A"] + ) + assert pytest.approx(0.049635, rel=1e-3) == value( + asmadm.fs.unit.outlet.conc_mass_comp[0, "S_F"] + ) + assert pytest.approx(0.026599, rel=1e-3) == value( + asmadm.fs.unit.outlet.conc_mass_comp[0, "S_I"] + ) + assert pytest.approx(0.60014, rel=1e-3) == value( + asmadm.fs.unit.outlet.conc_mass_comp[0, "S_NH4"] + ) + assert pytest.approx(1e-6, rel=1e-3) == value( + asmadm.fs.unit.outlet.conc_mass_comp[0, "S_N2"] + ) + assert pytest.approx(1e-6, rel=1e-3) == value( + asmadm.fs.unit.outlet.conc_mass_comp[0, "S_NO3"] + ) + assert pytest.approx(1e-6, rel=1e-3) == value( + asmadm.fs.unit.outlet.conc_mass_comp[0, "S_O2"] + ) + assert pytest.approx(0.22677, rel=1e-3) == value( + asmadm.fs.unit.outlet.conc_mass_comp[0, "S_PO4"] + ) + assert pytest.approx(1e-6, rel=1e-3) == value( + asmadm.fs.unit.outlet.conc_mass_comp[0, "X_AUT"] + ) + assert pytest.approx(1e-6, rel=1e-3) == value( + asmadm.fs.unit.outlet.conc_mass_comp[0, "X_H"] + ) + assert pytest.approx(12.7727, rel=1e-3) == value( + asmadm.fs.unit.outlet.conc_mass_comp[0, "X_I"] + ) + assert pytest.approx(1e-6, rel=1e-3) == value( + asmadm.fs.unit.outlet.conc_mass_comp[0, "X_MeOH"] + ) + assert pytest.approx(1e-6, rel=1e-3) == value( + asmadm.fs.unit.outlet.conc_mass_comp[0, "X_MeP"] + ) + assert pytest.approx(1e-6, rel=1e-3) == value( + asmadm.fs.unit.outlet.conc_mass_comp[0, "X_PAO"] + ) + assert pytest.approx(0.0022493, rel=1e-3) == value( + asmadm.fs.unit.outlet.conc_mass_comp[0, "X_PHA"] + ) + assert pytest.approx(1.0411, rel=1e-3) == value( + asmadm.fs.unit.outlet.conc_mass_comp[0, "X_PP"] + ) + assert pytest.approx(25.4283, rel=1e-3) == value( + asmadm.fs.unit.outlet.conc_mass_comp[0, "X_S"] + ) + assert pytest.approx(1e-6, rel=1e-3) == value( + asmadm.fs.unit.outlet.conc_mass_comp[0, "X_TSS"] + ) + assert pytest.approx(0.02268, rel=1e-3) == value( + asmadm.fs.unit.outlet.conc_mass_comp[0, "S_K"] + ) + assert pytest.approx(0.02893, rel=1e-3) == value( + asmadm.fs.unit.outlet.conc_mass_comp[0, "S_Mg"] + ) + assert pytest.approx(0.028857, rel=1e-3) == value( + asmadm.fs.unit.outlet.alkalinity[0] + ) + + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_conservation(self, asmadm): + assert ( + abs( + value( + asmadm.fs.unit.inlet.flow_vol[0] * asmadm.fs.props_ADM1.dens_mass + - asmadm.fs.unit.outlet.flow_vol[0] + * asmadm.fs.props_ASM2D.dens_mass + ) + ) + <= 1e-6 + ) + + assert ( + abs( + value( + ( + asmadm.fs.unit.inlet.flow_vol[0] + * asmadm.fs.props_ADM1.dens_mass + * asmadm.fs.props_ADM1.cp_mass + * ( + asmadm.fs.unit.inlet.temperature[0] + - asmadm.fs.props_ADM1.temperature_ref + ) + ) + - ( + asmadm.fs.unit.outlet.flow_vol[0] + * asmadm.fs.props_ASM2D.dens_mass + * asmadm.fs.props_ASM2D.cp_mass + * ( + asmadm.fs.unit.outlet.temperature[0] + - asmadm.fs.props_ASM2D.temperature_ref + ) + ) + ) + ) + <= 1e-6 + ) diff --git a/watertap/unit_models/translators/tests/test_translator_asm1_adm1.py b/watertap/unit_models/translators/tests/test_translator_asm1_adm1.py index 8c47ee7d59..50f1aaa049 100644 --- a/watertap/unit_models/translators/tests/test_translator_asm1_adm1.py +++ b/watertap/unit_models/translators/tests/test_translator_asm1_adm1.py @@ -23,16 +23,9 @@ import pytest from pyomo.environ import ConcreteModel, value, assert_optimal_termination, Param -from idaes.core import ( - FlowsheetBlock, - MaterialBalanceType, - EnergyBalanceType, - MomentumBalanceType, -) +from idaes.core import FlowsheetBlock -from pyomo.environ import ( - units, -) +from pyomo.environ import units from idaes.core.solvers import get_solver from idaes.core.util.model_statistics import ( @@ -40,12 +33,8 @@ number_variables, number_total_constraints, number_unused_variables, - unused_variables_set, ) -from idaes.core.util.scaling import ( - unscaled_variables_generator, -) import idaes.logger as idaeslog from idaes.core.util.testing import initialization_tester @@ -63,7 +52,7 @@ ) -from pyomo.util.check_units import assert_units_consistent, assert_units_equivalent +from pyomo.util.check_units import assert_units_consistent # ----------------------------------------------------------------------------- # Get default solver for testing diff --git a/watertap/unit_models/translators/translator_adm1_asm1.py b/watertap/unit_models/translators/translator_adm1_asm1.py index eb2707e749..b45a048f1d 100644 --- a/watertap/unit_models/translators/translator_adm1_asm1.py +++ b/watertap/unit_models/translators/translator_adm1_asm1.py @@ -23,10 +23,10 @@ """ # Import Pyomo libraries -from pyomo.common.config import ConfigBlock, ConfigValue, In, Bool +from pyomo.common.config import ConfigBlock, ConfigValue # Import IDAES cores -from idaes.core import declare_process_block_class, UnitModelBlockData +from idaes.core import declare_process_block_class from idaes.models.unit_models.translator import TranslatorData from idaes.core.util.config import ( is_reaction_parameter_block, @@ -38,7 +38,6 @@ from idaes.core.util.exceptions import InitializationError from pyomo.environ import ( - Constraint, Param, units as pyunits, check_optimal_termination, diff --git a/watertap/unit_models/translators/translator_adm1_asm2d.py b/watertap/unit_models/translators/translator_adm1_asm2d.py new file mode 100644 index 0000000000..a511737370 --- /dev/null +++ b/watertap/unit_models/translators/translator_adm1_asm2d.py @@ -0,0 +1,318 @@ +################################################################################# +# WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# National Renewable Energy Laboratory, and National Energy Technology +# Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +################################################################################# +""" +Translator block representing the ADM1/ASM2d interface. +This is copied from the Generic template for a translator block. + +Assumptions: + * Steady-state only + +Model formulated from: + +Flores-Alsina, X., Solon, K., Mbamba, C.K., Tait, S., Gernaey, K.V., Jeppsson, U. and Batstone, D.J., 2016. +Modelling phosphorus (P), sulfur (S) and iron (Fe) interactions for dynamic simulations of anaerobic digestion processes. +Water Research, 95, pp.370-382. +""" + +# Import Pyomo libraries +from pyomo.common.config import ConfigBlock, ConfigValue + +# Import IDAES cores +from idaes.core import declare_process_block_class +from idaes.models.unit_models.translator import TranslatorData +from idaes.core.util.config import ( + is_reaction_parameter_block, +) +from idaes.core.util.model_statistics import degrees_of_freedom +from idaes.core.solvers import get_solver +import idaes.logger as idaeslog + +from idaes.core.util.exceptions import InitializationError + +from pyomo.environ import ( + units as pyunits, + check_optimal_termination, + Set, +) + +__author__ = "Chenyu Wang, Marcus Holly" + + +# Set up logger +_log = idaeslog.getLogger(__name__) + + +@declare_process_block_class("Translator_ADM1_ASM2D") +class TranslatorDataADM1ASM2D(TranslatorData): + """ + Translator block representing the ADM1/ASM2D interface + """ + + CONFIG = TranslatorData.CONFIG() + CONFIG.declare( + "reaction_package", + ConfigValue( + default=None, + domain=is_reaction_parameter_block, + description="Reaction package to use for control volume", + doc="""Reaction parameter object used to define reaction calculations, + **default** - None. + **Valid values:** { + **None** - no reaction package, + **ReactionParameterBlock** - a ReactionParameterBlock object.}""", + ), + ) + CONFIG.declare( + "reaction_package_args", + ConfigBlock( + implicit=True, + description="Arguments to use for constructing reaction packages", + doc="""A ConfigBlock with arguments to be passed to a reaction block(s) + and used when constructing these, + **default** - None. + **Valid values:** { + see reaction package for documentation.}""", + ), + ) + + def build(self): + """ + Begin building model. + Args: + None + Returns: + None + """ + # Call UnitModel.build to setup dynamics + super(TranslatorDataADM1ASM2D, self).build() + + mw_c = 12 * pyunits.kg / pyunits.kmol + + @self.Constraint( + self.flowsheet().time, + doc="Equality volumetric flow equation", + ) + def eq_flow_vol_rule(blk, t): + return blk.properties_out[t].flow_vol == blk.properties_in[t].flow_vol + + @self.Constraint( + self.flowsheet().time, + doc="Equality temperature equation", + ) + def eq_temperature_rule(blk, t): + return blk.properties_out[t].temperature == blk.properties_in[t].temperature + + @self.Constraint( + self.flowsheet().time, + doc="Equality pressure equation", + ) + def eq_pressure_rule(blk, t): + return blk.properties_out[t].pressure == blk.properties_in[t].pressure + + self.readily_biodegradable = Set(initialize=["S_su", "S_aa", "S_fa"]) + + @self.Constraint( + self.flowsheet().time, + doc="Equality S_F equation", + ) + def eq_SF_conc(blk, t): + return blk.properties_out[t].conc_mass_comp["S_F"] == sum( + blk.properties_in[t].conc_mass_comp[i] + for i in blk.readily_biodegradable + ) + + self.readily_biodegradable2 = Set(initialize=["S_va", "S_bu", "S_pro", "S_ac"]) + + @self.Constraint( + self.flowsheet().time, + doc="Equality S_A equation", + ) + def eq_SA_conc(blk, t): + return blk.properties_out[t].conc_mass_comp["S_A"] == sum( + blk.properties_in[t].conc_mass_comp[i] + for i in blk.readily_biodegradable2 + ) + + self.unchanged_component = Set(initialize=["S_I", "X_I", "X_PP", "X_PHA"]) + + @self.Constraint( + self.flowsheet().time, + self.unchanged_component, + doc="Equality equation for unchanged components", + ) + def eq_unchanged_conc(blk, t, i): + return ( + blk.properties_out[t].conc_mass_comp[i] + == blk.properties_in[t].conc_mass_comp[i] + ) + + @self.Constraint( + self.flowsheet().time, + doc="Equality S_NH4 equation", + ) + def eq_SNH4_conc(blk, t): + return ( + blk.properties_out[t].conc_mass_comp["S_NH4"] + == blk.properties_in[t].conc_mass_comp["S_IN"] + ) + + @self.Constraint( + self.flowsheet().time, + doc="Equality S_PO4 equation", + ) + def eq_SPO4_conc(blk, t): + return ( + blk.properties_out[t].conc_mass_comp["S_PO4"] + == blk.properties_in[t].conc_mass_comp["S_IP"] + ) + + # TODO: No S_IC for current ASM2D, need to revisit it later + @self.Constraint( + self.flowsheet().time, + doc="Equality alkalinity equation", + ) + def return_Salk(blk, t): + return ( + blk.properties_out[t].alkalinity + == blk.properties_in[t].conc_mass_comp["S_IC"] / mw_c + ) + + self.slowly_biodegradable = Set( + initialize=[ + "X_ch", + "X_pr", + "X_li", + ] + ) + + @self.Constraint( + self.flowsheet().time, + doc="Equality X_S equation", + ) + def eq_XS_conc(blk, t): + return blk.properties_out[t].conc_mass_comp["X_S"] == sum( + blk.properties_in[t].conc_mass_comp[i] for i in blk.slowly_biodegradable + ) + + # TODO: check if we track S_SO4, S_Na, S_Cl, S_Ca, X_Ca2(PO4)3, X_MgNH4PO4 + + # TODO: X_TSS, X_MeOH and X_MeP are not given in Flores-Alsina's paper, need to check how to address them + self.zero_flow_components = Set( + initialize=[ + "S_N2", + "S_NO3", + "S_O2", + "X_AUT", + "X_H", + "X_PAO", + "X_TSS", + "X_MeOH", + "X_MeP", + ] + ) + + @self.Constraint( + self.flowsheet().time, + self.zero_flow_components, + doc="Components with no flow equation", + ) + def return_zero_flow_comp(blk, t, i): + return ( + blk.properties_out[t].conc_mass_comp[i] + == 1e-6 * pyunits.kg / pyunits.m**3 + ) + + if ( + self.config.outlet_property_package.config.additional_solute_list + is not None + ): + + @self.Constraint( + self.flowsheet().time, + self.config.outlet_property_package.config.additional_solute_list, + doc="Equality ASM2D additional solute equation", + ) + def eq_ASM2D_additional_conc(blk, t, i): + return ( + blk.properties_out[t].conc_mass_comp[i] + == blk.properties_in[t].conc_mass_comp[i] + ) + + def initialize_build( + self, + state_args_in=None, + state_args_out=None, + outlvl=idaeslog.NOTSET, + solver=None, + optarg=None, + ): + """ + This method calls the initialization method of the state blocks. + + Keyword Arguments: + state_args_in : a dict of arguments to be passed to the inlet + property package (to provide an initial state for + initialization (see documentation of the specific + property package) (default = None). + state_args_out : a dict of arguments to be passed to the outlet + property package (to provide an initial state for + initialization (see documentation of the specific + property package) (default = None). + outlvl : sets output level of initialization routine + optarg : solver options dictionary object (default=None, use + default solver options) + solver : str indicating which solver to use during + initialization (default = None, use default solver) + + Returns: + None + """ + init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") + + # Create solver + opt = get_solver(solver, optarg) + + # --------------------------------------------------------------------- + # Initialize state block + flags = self.properties_in.initialize( + outlvl=outlvl, + optarg=optarg, + solver=solver, + state_args=state_args_in, + hold_state=True, + ) + + self.properties_out.initialize( + outlvl=outlvl, + optarg=optarg, + solver=solver, + state_args=state_args_out, + ) + + if degrees_of_freedom(self) != 0: + raise Exception( + f"{self.name} degrees of freedom were not 0 at the beginning " + f"of initialization. DoF = {degrees_of_freedom(self)}" + ) + + with idaeslog.solver_log(init_log, idaeslog.DEBUG) as slc: + res = opt.solve(self, tee=slc.tee) + + self.properties_in.release_state(flags=flags, outlvl=outlvl) + + init_log.info(f"Initialization Complete: {idaeslog.condition(res)}") + + if not check_optimal_termination(res): + raise InitializationError( + f"{self.name} failed to initialize successfully. Please check " + f"the output logs for more information." + ) diff --git a/watertap/unit_models/translators/translator_asm1_adm1.py b/watertap/unit_models/translators/translator_asm1_adm1.py index 680c7f4fcf..44fe5eead3 100644 --- a/watertap/unit_models/translators/translator_asm1_adm1.py +++ b/watertap/unit_models/translators/translator_asm1_adm1.py @@ -24,10 +24,10 @@ """ # Import Pyomo libraries -from pyomo.common.config import ConfigBlock, ConfigValue, In, Bool +from pyomo.common.config import ConfigBlock, ConfigValue # Import IDAES cores -from idaes.core import declare_process_block_class, UnitModelBlockData +from idaes.core import declare_process_block_class from idaes.models.unit_models.translator import TranslatorData from idaes.core.util.config import ( is_reaction_parameter_block, @@ -37,7 +37,6 @@ import idaes.logger as idaeslog from pyomo.environ import ( - Constraint, Param, PositiveReals, Var, @@ -365,9 +364,7 @@ def ReqOrgNS(blk, t): * mw_n ) - @self.Expression( - self.flowsheet().time, doc="Inert organic nitrogen mapping step B" - ) + @self.Expression(self.flowsheet().time, doc="Soluble inert mapping step B") def si_mapping_B(blk, t): return Expr_if( blk.ORGN_remain_a[t] > blk.ReqOrgNS[t], @@ -375,7 +372,7 @@ def si_mapping_B(blk, t): blk.ORGN_remain_a[t] / blk.config.reaction_package.N_I / mw_n, ) - @self.Expression(self.flowsheet().time, doc="Monosacharides mapping step B") + @self.Expression(self.flowsheet().time, doc="Monosaccharides mapping step B") def ssu_mapping_B(blk, t): return Expr_if( blk.ORGN_remain_a[t] > blk.ReqOrgNS[t], @@ -432,7 +429,10 @@ def ReqOrgNx(blk, t): * mw_n ) - @self.Expression(self.flowsheet().time, doc="Monosacharides mapping step B") + @self.Expression( + self.flowsheet().time, + doc="Inert particulate organic material mapping step C", + ) def xi_mapping(blk, t): return Expr_if( blk.ORGN_remain_b[t] > blk.ReqOrgNx[t], diff --git a/watertap/unit_models/zero_order/fixed_bed_zo.py b/watertap/unit_models/zero_order/fixed_bed_zo.py index 6b3d9280b5..fcb79a5134 100644 --- a/watertap/unit_models/zero_order/fixed_bed_zo.py +++ b/watertap/unit_models/zero_order/fixed_bed_zo.py @@ -14,7 +14,6 @@ operation. """ -import pyomo.environ as pyo from pyomo.environ import units as pyunits, Var from idaes.core import declare_process_block_class from watertap.core import build_siso, constant_intensity, ZeroOrderBaseData