diff --git a/docs/technical_reference/property_models/index.rst b/docs/technical_reference/property_models/index.rst index e3d9bad97f..7cb736f31d 100644 --- a/docs/technical_reference/property_models/index.rst +++ b/docs/technical_reference/property_models/index.rst @@ -8,7 +8,6 @@ Property Models NaCl NaCl_T_dep seawater - seawater_ion_generic coagulation ASM1 ASM2D diff --git a/docs/technical_reference/property_models/seawater_ion_generic.rst b/docs/technical_reference/property_models/seawater_ion_generic.rst deleted file mode 100644 index ed752c475a..0000000000 --- a/docs/technical_reference/property_models/seawater_ion_generic.rst +++ /dev/null @@ -1,93 +0,0 @@ -Seawater Ion Generic Property Package -===================================== - -This package implements property relationships for artificial seawater as provided in `Islam et al. (2021) `_. - -This artificial seawater property package: - * supports only 'H2O', 'Na_+', 'Ca_2+', 'Mg_2+', 'Cl\_-', 'SO4_2-' - * supports only liquid phase - * is formulated on a molar basis - * does not support dynamics - -This package uses the eNRTL equation of state and has been used to predict mineral scaling in full treatment -where reverse osmosis is the primary desalination technology. Additionally, this property package is -formulated as an extension of `IDAES's modular property framework `_ (GenericParameterBlock). -In other words, this property package is a configuration file that gets passed into IDAES's generic/modular property framework. - -Sets ----- -.. csv-table:: - :header: "Description", "Symbol", "Indices" - - "Components", ":math:`j`", "['H2O', 'Na_+', 'Ca_2+', 'Mg_2+', 'Cl\_-', 'SO4_2-']" - "Phases", ":math:`p`", "['Liq']" - -State variables ---------------- -.. csv-table:: - :header: "Description", "Symbol", "Variable", "Index", "Units" - - "Component molar flowrate", ":math:`N_j`", "flow_mol_phase_comp", "[p, j]", ":math:`\text{mole/s}`" - "Temperature", ":math:`T`", "temperature", "None", ":math:`\text{K}`" - "Pressure", ":math:`P`", "pressure", "None", ":math:`\text{Pa}`" - -Properties ----------- -.. csv-table:: - :header: "Description", "Symbol", "Variable", "Index", "Units" - - "Component mole fraction", ":math:`y_j`", "mole_frac_phase_comp", "[p, j]", ":math:`\text{dimensionless}`" - "Component mole flowrate", ":math:`N_j`", "flow_mol_phase_comp", "[p, j]", ":math:`\text{mole/s}`" - "Interaction parameter", ":math:`τ`", "Liq_tau", "[j_1, j_2, j_3]", ":math:`\text{dimensionless}`" - "Component molar volume", ":math:`V_j`", "vol_mol_liq_comp", "[j]", ":math:`\text{m}^3\text{/mole}`" - -Relationships -------------- -.. csv-table:: - :header: "Description", "Equation" - - "Component mole flowrate", ":math:`N_j = \frac{M_j}{MW_j}`" - "Component mole fraction", ":math:`y_j = \frac{N_j}{\sum_{j} N_j}`" - - - -Scaling -------- -This artificial seawater property package includes support for scaling, such as default or user-supplied scaling factors for all variables. - -The default scaling factors are as follows: - - * 1e1 for 'Na_+' mole flowrate - * 1e3 for 'Ca_2+' mole flowrate - * 1e2 for 'Mg_2+' mole flowrate - * 1e2 for 'SO4_2-' mole flowrate - * 1e1 for 'Cl\_-' mole flowrate - * 1e-1 for 'H2O' mole flowrate - * 1e2 for 'Na_+' mole fraction - * 1e4 for 'Ca_2+' mole fraction - * 1e3 for 'Mg_2+' mole fraction - * 1e3 for 'SO4_2-' mole fraction - * 1e2 for 'Cl\_-' mole fraction - * 1 for 'H2O' mole fraction - * 1e1 for 'NaCl' apparent mole flowrate - * 1e2 for 'Na2SO4' apparent mole flowrate - * 1e2 for 'CaCl2' apparent mole flowrate - * 1e3 for 'CaSO4' apparent mole flowrate - * 1e2 for 'MgCl2' apparent mole flowrate - * 1e3 for 'MgSO4' apparent mole flowrate - * 1e-1 for 'H2O' apparent mole flowrate - * 1e3 for 'NaCl' apparent mole fraction - * 1e4 for 'Na2SO4' apparent mole fraction - * 1e4 for 'CaCl2' apparent mole fraction - * 1e5 for 'CaSO4' apparent mole fraction - * 1e4 for 'MgCl2' apparent mole fraction - * 1e5 for 'MgSO4' apparent mole fraction - * 1 for 'H2O' apparent mole fraction - -Scaling factors for other variables can be calculated based on their relationships with the user-supplied or default scaling factors. - -Reference ---------- - -M.R.Islam, I.Hsieh, B.Lin, A.K.Thakur, C.Chen, M.Malmali, Molecular thermodynamics for scaling prediction: Case of membrane distillation, Separation and Purification Technology, 2021,Vol. 276. https://www.sciencedirect.com/science/article/abs/pii/S1383586621009412 - diff --git a/watertap/property_models/seawater_ion_generic.py b/watertap/property_models/seawater_ion_generic.py deleted file mode 100644 index 95f270dbe9..0000000000 --- a/watertap/property_models/seawater_ion_generic.py +++ /dev/null @@ -1,263 +0,0 @@ -################################################################################# -# WaterTAP Copyright (c) 2020-2024, 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/" -################################################################################# -""" -Artificial seawater properties comprising Na+, Cl-, Ca_2+, SO4_2-, Mg_2+ -Equation of state: eNRTL - -eNRTL property configuration dicts for synthetic hard water based on [1] - -References: - -[1] Islam, R.I., et al., Molecular thermodynamics for scaling prediction: Case -of membrane distillation, Separation and Purification Technology, 2021, -Vol. 276. -""" - -from pyomo.environ import Param, units as pyunits - -from idaes.core import ( - AqueousPhase, - Solvent, - Apparent, - Anion, - Cation, - MaterialFlowBasis, -) -from idaes.models.properties.modular_properties.eos.enrtl import ENRTL -from idaes.models.properties.modular_properties.eos.enrtl_reference_states import ( - Symmetric, -) -from idaes.models.properties.modular_properties.base.generic_property import StateIndex -from idaes.models.properties.modular_properties.state_definitions import FpcTP -from idaes.models.properties.modular_properties.pure.electrolyte import ( - relative_permittivity_constant, -) -import idaes.logger as idaeslog - -# Set up logger -_log = idaeslog.getLogger(__name__) - - -class VolMolH2O: - def build_parameters(self): - self.vol_mol_pure = Param( - initialize=18e-6, units=pyunits.m**3 / pyunits.mol, mutable=True - ) - - def return_expression(self, cobj, T): - return cobj.vol_mol_pure - - -class VolMolNaCl: - def build_parameters(self): - self.vol_mol_pure = Param( - initialize=58.44e-6, units=pyunits.m**3 / pyunits.mol, mutable=True - ) - - def return_expression(self, cobj, T): - return cobj.vol_mol_pure - - -class VolMolNa2SO4: - def build_parameters(self): - self.vol_mol_pure = Param( - initialize=142.04e-6, units=pyunits.m**3 / pyunits.mol, mutable=True - ) - - def return_expression(self, cobj, T): - return cobj.vol_mol_pure - - -class VolMolCaCl2: - def build_parameters(self): - self.vol_mol_pure = Param( - initialize=110.98e-6, units=pyunits.m**3 / pyunits.mol, mutable=True - ) - - def return_expression(self, cobj, T): - return cobj.vol_mol_pure - - -class VolMolCaSO4: - def build_parameters(self): - self.vol_mol_pure = Param( - initialize=136.14e-6, units=pyunits.m**3 / pyunits.mol, mutable=True - ) - - def return_expression(self, cobj, T): - return cobj.vol_mol_pure - - -class VolMolMgCl2: - def build_parameters(self): - self.vol_mol_pure = Param( - initialize=95.21e-6, units=pyunits.m**3 / pyunits.mol, mutable=True - ) - - def return_expression(self, cobj, T): - return cobj.vol_mol_pure - - -class VolMolMgSO4: - def build_parameters(self): - self.vol_mol_pure = Param( - initialize=120.37e-6, units=pyunits.m**3 / pyunits.mol, mutable=True - ) - - def return_expression(self, cobj, T): - return cobj.vol_mol_pure - - -configuration = { - "components": { - "H2O": { - "type": Solvent, - "vol_mol_liq_comp": VolMolH2O, - "relative_permittivity_liq_comp": relative_permittivity_constant, - "parameter_data": { - "mw": (18e-3, pyunits.kg / pyunits.mol), - "relative_permittivity_liq_comp": 78.54, - }, - }, - "NaCl": { - "type": Apparent, - "dissociation_species": {"Na_+": 1, "Cl_-": 1}, - "vol_mol_liq_comp": VolMolNaCl, - "parameter_data": {"mw": (58.44e-3, pyunits.kg / pyunits.mol)}, - }, - "Na2SO4": { - "type": Apparent, - "dissociation_species": {"Na_+": 2, "SO4_2-": 1}, - "vol_mol_liq_comp": VolMolNa2SO4, - "parameter_data": {"mw": (142.04e-3, pyunits.kg / pyunits.mol)}, - }, - "CaCl2": { - "type": Apparent, - "dissociation_species": {"Ca_2+": 1, "Cl_-": 2}, - "vol_mol_liq_comp": VolMolCaCl2, - "parameter_data": {"mw": (110.98e-3, pyunits.kg / pyunits.mol)}, - }, - "CaSO4": { - "type": Apparent, - "dissociation_species": {"Ca_2+": 1, "SO4_2-": 1}, - "vol_mol_liq_comp": VolMolCaSO4, - "parameter_data": {"mw": (136.14e-3, pyunits.kg / pyunits.mol)}, - }, - "MgCl2": { - "type": Apparent, - "dissociation_species": {"Mg_2+": 1, "Cl_-": 2}, - "vol_mol_liq_comp": VolMolMgCl2, - "parameter_data": {"mw": (95.21e-3, pyunits.kg / pyunits.mol)}, - }, - "MgSO4": { - "type": Apparent, - "dissociation_species": {"Mg_2+": 1, "SO4_2-": 1}, - "vol_mol_liq_comp": VolMolMgSO4, - "parameter_data": {"mw": (120.37e-3, pyunits.kg / pyunits.mol)}, - }, - "Na_+": {"type": Cation, "charge": +1, "parameter_data": {"mw": 23e-3}}, - "Ca_2+": {"type": Cation, "charge": +2, "parameter_data": {"mw": 40e-3}}, - "Mg_2+": {"type": Cation, "charge": +2, "parameter_data": {"mw": 24.3e-3}}, - "Cl_-": {"type": Anion, "charge": -1, "parameter_data": {"mw": 35.45e-3}}, - "SO4_2-": {"type": Anion, "charge": -2, "parameter_data": {"mw": 96.06e-3}}, - }, - "phases": { - "Liq": { - "type": AqueousPhase, - "equation_of_state": ENRTL, - "equation_of_state_options": {"reference_state": Symmetric}, - } - }, - "base_units": { - "time": pyunits.s, - "length": pyunits.m, - "mass": pyunits.kg, - "amount": pyunits.mol, - "temperature": pyunits.K, - }, - "state_definition": FpcTP, - "state_components": StateIndex.true, - "reaction_basis": MaterialFlowBasis.molar, - "pressure_ref": 1e5, - "temperature_ref": 298.15, - "parameter_data": { - "Liq_tau": { # Table 1 [1] - ("H2O", "Na_+, Cl_-"): 8.866, - ("Na_+, Cl_-", "H2O"): -4.451, - ("H2O", "Ca_2+, Cl_-"): 10.478, - ("Ca_2+, Cl_-", "H2O"): -5.231, - ("H2O", "Mg_2+, Cl_-"): 10.854, - ("Mg_2+, Cl_-", "H2O"): -5.409, - ("H2O", "Na_+, SO4_2-"): 8.012, - ("Na_+, SO4_2-", "H2O"): -3.903, - ("H2O", "Ca_2+, SO4_2-"): 6.932, - ("Ca_2+, SO4_2-", "H2O"): -3.466, - ("H2O", "Mg_2+, SO4_2-"): 8.808, - ("Mg_2+, SO4_2-", "H2O"): -4.383, - ("Na_+, Cl_-", "Ca_2+, Cl_-"): -0.468, - ("Ca_2+, Cl_-", "Na_+, Cl_-"): 0.41, - ("Na_+, Cl_-", "Mg_2+, Cl_-"): -0.328, - ("Mg_2+, Cl_-", "Na_+, Cl_-"): -0.981, - ("Ca_2+, Cl_-", "Mg_2+, Cl_-"): 0.22, - ("Mg_2+, Cl_-", "Ca_2+, Cl_-"): 0.322, - ("Na_+, SO4_2-", "Ca_2+, SO4_2-"): -0.761, - ("Ca_2+, SO4_2-", "Na_+, SO4_2-"): 0.368, - ("Na_+, SO4_2-", "Mg_2+, SO4_2-"): -0.327, - ("Mg_2+, SO4_2-", "Na_+, SO4_2-"): 0.799, - ("Ca_2+, SO4_2-", "Mg_2+, SO4_2-"): 0, - ("Mg_2+, SO4_2-", "Ca_2+, SO4_2-"): 0.383, - ("Na_+, Cl_-", "Na_+, SO4_2-"): 0.803, - ("Na_+, SO4_2-", "Na_+, Cl_-"): -0.634, - ("Ca_2+, Cl_-", "Ca_2+, SO4_2-"): 0, - ("Ca_2+, SO4_2-", "Ca_2+, Cl_-"): -0.264, - ("Mg_2+, Cl_-", "Mg_2+, SO4_2-"): -0.707, - ("Mg_2+, SO4_2-", "Mg_2+, Cl_-"): -0.841, - } - }, - "default_scaling_factors": { - ("flow_mol_phase_comp", ("Liq", "Na_+")): 1e1, - ("flow_mol_phase_comp", ("Liq", "Ca_2+")): 1e3, - ("flow_mol_phase_comp", ("Liq", "Mg_2+")): 1e2, - ("flow_mol_phase_comp", ("Liq", "SO4_2-")): 1e2, - ("flow_mol_phase_comp", ("Liq", "Cl_-")): 1e1, - ("flow_mol_phase_comp", ("Liq", "H2O")): 1e-1, - ("mole_frac_comp", "Na_+"): 1e2, - ("mole_frac_comp", "Ca_2+"): 1e4, - ("mole_frac_comp", "Mg_2+"): 1e3, - ("mole_frac_comp", "SO4_2-"): 1e3, - ("mole_frac_comp", "Cl_-"): 1e2, - ("mole_frac_comp", "H2O"): 1, - ("mole_frac_phase_comp", ("Liq", "Na_+")): 1e2, - ("mole_frac_phase_comp", ("Liq", "Ca_2+")): 1e4, - ("mole_frac_phase_comp", ("Liq", "Mg_2+")): 1e3, - ("mole_frac_phase_comp", ("Liq", "SO4_2-")): 1e3, - ("mole_frac_phase_comp", ("Liq", "Cl_-")): 1e2, - ("mole_frac_phase_comp", ("Liq", "H2O")): 1, - ("flow_mol_phase_comp_apparent", ("Liq", "NaCl")): 1e1, - ("flow_mol_phase_comp_apparent", ("Liq", "Na2SO4")): 1e2, - ("flow_mol_phase_comp_apparent", ("Liq", "CaCl2")): 1e2, - ("flow_mol_phase_comp_apparent", ("Liq", "CaSO4")): 1e3, - ("flow_mol_phase_comp_apparent", ("Liq", "MgCl2")): 1e2, - ("flow_mol_phase_comp_apparent", ("Liq", "MgSO4")): 1e3, - ("flow_mol_phase_comp_apparent", ("Liq", "H2O")): 1e-1, - ( - "mole_frac_phase_comp_apparent", - ("Liq", "NaCl"), - ): 1e3, # TODO: these seem to be 1 orders of magnitude too low - ("mole_frac_phase_comp_apparent", ("Liq", "Na2SO4")): 1e4, - ("mole_frac_phase_comp_apparent", ("Liq", "CaCl2")): 1e4, - ("mole_frac_phase_comp_apparent", ("Liq", "CaSO4")): 1e5, - ("mole_frac_phase_comp_apparent", ("Liq", "MgCl2")): 1e4, - ("mole_frac_phase_comp_apparent", ("Liq", "MgSO4")): 1e5, - ("mole_frac_phase_comp_apparent", ("Liq", "H2O")): 1, - }, -} diff --git a/watertap/property_models/seawater_ion_prop_pack.py b/watertap/property_models/seawater_ion_prop_pack.py deleted file mode 100644 index 00fb4d3de4..0000000000 --- a/watertap/property_models/seawater_ion_prop_pack.py +++ /dev/null @@ -1,580 +0,0 @@ -################################################################################# -# WaterTAP Copyright (c) 2020-2024, 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/" -################################################################################# - -""" -Simple property package for Na-Ca-Mg-SO4-Cl solution represented with ions -""" - -from pyomo.environ import ( - Constraint, - Var, - Param, - Expression, - NonNegativeReals, - Suffix, -) -from pyomo.environ import units as pyunits - -# Import IDAES cores -from idaes.core import ( - declare_process_block_class, - MaterialFlowBasis, - PhysicalParameterBlock, - StateBlockData, - StateBlock, - MaterialBalanceType, - EnergyBalanceType, -) -from idaes.core.base.components import Solute, Solvent -from idaes.core.base.phases import LiquidPhase -from idaes.core.util.initialization import ( - fix_state_vars, - revert_state_vars, - solve_indexed_blocks, -) -from idaes.core.util.model_statistics import ( - degrees_of_freedom, - number_unfixed_variables, -) -from idaes.core.util.exceptions import PropertyPackageError -from idaes.core.util.misc import extract_data -import idaes.core.util.scaling as iscale -import idaes.logger as idaeslog -from idaes.core.solvers import get_solver - -# Set up logger -_log = idaeslog.getLogger(__name__) - - -@declare_process_block_class("PropParameterBlock") -class PropParameterData(PhysicalParameterBlock): - CONFIG = PhysicalParameterBlock.CONFIG() - - def build(self): - """ - Callable method for Block construction. - """ - super(PropParameterData, self).build() - - self._state_block_class = PropStateBlock - - # phases - self.Liq = LiquidPhase() - - # components - self.H2O = Solvent() - self.Na = Solute() - self.Ca = Solute() - self.Mg = Solute() - self.SO4 = Solute() - self.Cl = Solute() - - # molecular weight - mw_comp_data = { - "H2O": 18.015e-3, - "Na": 22.990e-3, - "Ca": 40.078e-3, - "Mg": 24.305e-3, - "SO4": 96.06e-3, - "Cl": 35.453e-3, - } - - self.mw_comp = Param( - self.component_list, - initialize=extract_data(mw_comp_data), - units=pyunits.kg / pyunits.mol, - doc="Molecular weight", - ) - - self.dens_mass = Param( - initialize=1000, - units=pyunits.kg / pyunits.m**3, - doc="Density", - ) - - self.cp = Param(initialize=4.2e3, units=pyunits.J / (pyunits.kg * pyunits.K)) - - # ---default scaling--- - self.set_default_scaling("temperature", 1e-2) - self.set_default_scaling("pressure", 1e-6) - - @classmethod - def define_metadata(cls, obj): - """Define properties supported and units.""" - obj.add_properties( - { - "flow_mass_phase_comp": {"method": None}, - "temperature": {"method": None}, - "pressure": {"method": None}, - "mass_frac_phase_comp": {"method": "_mass_frac_phase_comp"}, - "flow_vol": {"method": "_flow_vol"}, - "flow_mol_phase_comp": {"method": "_flow_mol_phase_comp"}, - "conc_mol_phase_comp": {"method": "_conc_mol_phase_comp"}, - } - ) - - obj.define_custom_properties( - { - "enth_flow": {"method": "_enth_flow"}, - } - ) - - obj.add_default_units( - { - "time": pyunits.s, - "length": pyunits.m, - "mass": pyunits.kg, - "amount": pyunits.mol, - "temperature": pyunits.K, - } - ) - - -class _PropStateBlock(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_mass_phase_comp : value at which to initialize - phase component flows - pressure : value at which to initialize pressure - temperature : value at which to initialize temperature - outlvl : sets output level of initialization routine (default=idaeslog.NOTSET) - optarg : solver options dictionary object (default=None) - state_vars_fixed: Flag to denote if state vars have already been - fixed. - - True - states have already been fixed by the - control volume 1D. Control volume 0D - does not fix the state vars, so will - be False if this state block is used - with 0D blocks. - - False - states have not been fixed. The state - block will deal with fixing/unfixing. - solver : Solver object to use during initialization if None is provided - it will use the default solver for IDAES (default = None) - 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. - """ - # Get loggers - init_log = idaeslog.getInitLogger(self.name, outlvl, tag="properties") - solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="properties") - - # Set solver and options - opt = get_solver(solver, optarg) - - # Fix state variables - flags = fix_state_vars(self, state_args) - # Check when the state vars are fixed already result in dof 0 - for k in self.keys(): - dof = degrees_of_freedom(self[k]) - if dof != 0: - raise PropertyPackageError( - "\nWhile initializing {sb_name}, the degrees of freedom " - "are {dof}, when zero is required. \nInitialization assumes " - "that the state variables should be fixed and that no other " - "variables are fixed. \nIf other properties have a " - "predetermined value, use the calculate_state method " - "before using initialize to determine the values for " - "the state variables and avoid fixing the property variables." - "".format(sb_name=self.name, dof=dof) - ) - - # --------------------------------------------------------------------- - skip_solve = True # skip solve if only state variables are present - for k in self.keys(): - if number_unfixed_variables(self[k]) != 0: - skip_solve = False - - if not skip_solve: - # Initialize properties - with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: - results = solve_indexed_blocks(opt, [self], tee=slc.tee) - init_log.info_high( - "Property initialization: {}.".format(idaeslog.condition(results)) - ) - - # --------------------------------------------------------------------- - # If input block, return flags, else release state - if state_vars_fixed is False: - if hold_state is True: - return flags - else: - self.release_state(flags) - - def release_state(self, flags, outlvl=idaeslog.NOTSET): - """ - Method to release state variables fixed during initialisation. - - 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 of logging - """ - # Unfix state variables - init_log = idaeslog.getInitLogger(self.name, outlvl, tag="properties") - revert_state_vars(self, flags) - init_log.info_high("{} State Released.".format(self.name)) - - -@declare_process_block_class("PropStateBlock", block_class=_PropStateBlock) -class PropStateBlockData(StateBlockData): - def build(self): - """Callable method for Block construction.""" - super(PropStateBlockData, self).build() - - self.scaling_factor = Suffix(direction=Suffix.EXPORT) - - seawater_mass_frac_dict = { - ("Liq", "Na"): 11122e-6, - ("Liq", "Ca"): 382e-6, - ("Liq", "Mg"): 1394e-6, - ("Liq", "SO4"): 2136e-6, - ("Liq", "Cl"): 20300e-6, - } - seawater_mass_frac_dict[("Liq", "H2O")] = 1 - sum( - x for x in seawater_mass_frac_dict.values() - ) - - # Add state variables - self.flow_mass_phase_comp = Var( - self.params.phase_list, - self.params.component_list, - initialize=seawater_mass_frac_dict, - bounds=(0.0, 100), - domain=NonNegativeReals, - units=pyunits.kg / pyunits.s, - doc="Mass flow rate", - ) - - self.temperature = Var( - initialize=298.15, - bounds=(273.15, 1000), - domain=NonNegativeReals, - units=pyunits.degK, - doc="State temperature", - ) - - self.pressure = Var( - initialize=101325, - bounds=(1e5, 5e7), - domain=NonNegativeReals, - units=pyunits.Pa, - doc="State pressure", - ) - - # ----------------------------------------------------------------------------- - # Property Methods - def _mass_frac_phase_comp(self): - self.mass_frac_phase_comp = Var( - self.params.phase_list, - self.params.component_list, - initialize=0.1, - bounds=(0.0, None), - units=pyunits.dimensionless, - doc="Mass fraction", - ) - - def rule_mass_frac_phase_comp(b, j): - return b.mass_frac_phase_comp["Liq", j] == b.flow_mass_phase_comp[ - "Liq", j - ] / sum( - b.flow_mass_phase_comp["Liq", j] for j in self.params.component_list - ) - - self.eq_mass_frac_phase_comp = Constraint( - self.params.component_list, rule=rule_mass_frac_phase_comp - ) - - def _flow_vol(self): - self.flow_vol = Var( - initialize=1e-3, - bounds=(0.0, None), - units=pyunits.m**3 / pyunits.s, - doc="Volumetric flow rate", - ) - - def rule_flow_vol(b): - return ( - b.flow_vol - == sum( - b.flow_mass_phase_comp["Liq", j] for j in b.params.component_list - ) - / b.params.dens_mass - ) - - self.eq_flow_vol = Constraint(rule=rule_flow_vol) - - def _flow_mol_phase_comp(self): - self.flow_mol_phase_comp = Var( - self.params.phase_list, - self.params.component_list, - initialize=1, - bounds=(0.0, None), - units=pyunits.mol / pyunits.s, - doc="Molar flowrate", - ) - - def rule_flow_mol_phase_comp(b, j): - return ( - b.flow_mol_phase_comp["Liq", j] - == b.flow_mass_phase_comp["Liq", j] / b.params.mw_comp[j] - ) - - self.eq_flow_mol_phase_comp = Constraint( - self.params.component_list, rule=rule_flow_mol_phase_comp - ) - - def _conc_mol_phase_comp(self): - self.conc_mol_phase_comp = Var( - self.params.phase_list, - self.params.component_list, - initialize=1, - bounds=(0.0, 1e6), - units=pyunits.mol / pyunits.m**3, - doc="Molarity", - ) - - def rule_conc_mol_phase_comp(b, j): - return ( - b.flow_vol * b.conc_mol_phase_comp["Liq", j] - == b.flow_mol_phase_comp["Liq", j] - ) - - self.eq_conc_mol_phase_comp = Constraint( - self.params.component_list, rule=rule_conc_mol_phase_comp - ) - - def _enth_flow(self): - # enthalpy flow expression for get_enthalpy_flow_terms method - temperature_ref = 273.15 * pyunits.K - - def rule_enth_flow(b): # enthalpy flow [J/s] - return ( - b.params.cp - * sum(b.flow_mass_phase_comp["Liq", j] for j in b.params.component_list) - * (b.temperature - temperature_ref) - ) - - self.enth_flow = Expression(rule=rule_enth_flow) - - # ----------------------------------------------------------------------------- - # General Methods - # NOTE: For scaling in the control volume to work properly, these methods must - # return a pyomo Var or Expression - - def get_material_flow_terms(self, p, j): - """Create material flow terms for control volume.""" - return self.flow_mass_phase_comp[p, j] - - def get_enthalpy_flow_terms(self, p): - """Create enthalpy flow terms.""" - return self.enth_flow - - # TODO: make property package compatible with dynamics - # def get_material_density_terms(self, p, j): - # """Create material density terms.""" - - # def get_enthalpy_density_terms(self, p): - # """Create enthalpy density terms.""" - - def default_material_balance_type(self): - return MaterialBalanceType.componentTotal - - def default_energy_balance_type(self): - return EnergyBalanceType.enthalpyTotal - - def get_material_flow_basis(self): - return MaterialFlowBasis.mass - - def define_state_vars(self): - """Define state vars.""" - return { - "flow_mass_phase_comp": self.flow_mass_phase_comp, - "temperature": self.temperature, - "pressure": self.pressure, - } - - # ----------------------------------------------------------------------------- - # Scaling methods - def calculate_scaling_factors(self): - super().calculate_scaling_factors() - - # setting scaling factors for variables - - # default scaling factors have already been set with - # idaes.core.property_base.calculate_scaling_factors() - - # scaling factors for parameters - for j, v in self.params.mw_comp.items(): - if iscale.get_scaling_factor(v) is None: - iscale.set_scaling_factor(self.params.mw_comp[j], 1e2) - - if iscale.get_scaling_factor(self.params.dens_mass) is None: - iscale.set_scaling_factor(self.params.dens_mass, 1e-3) - - if iscale.get_scaling_factor(self.params.cp) is None: - iscale.set_scaling_factor(self.params.cp, 1e-3) - - # these variables should have user input - if iscale.get_scaling_factor(self.flow_mass_phase_comp["Liq", "H2O"]) is None: - sf = iscale.get_scaling_factor( - self.flow_mass_phase_comp["Liq", "H2O"], default=1, warning=True - ) - iscale.set_scaling_factor(self.flow_mass_phase_comp["Liq", "H2O"], sf) - - if iscale.get_scaling_factor(self.flow_mass_phase_comp["Liq", "Na"]) is None: - sf = iscale.get_scaling_factor( - self.flow_mass_phase_comp["Liq", "Na"], default=1e2, warning=True - ) - iscale.set_scaling_factor(self.flow_mass_phase_comp["Liq", "Na"], sf) - - if iscale.get_scaling_factor(self.flow_mass_phase_comp["Liq", "Ca"]) is None: - sf = iscale.get_scaling_factor( - self.flow_mass_phase_comp["Liq", "Ca"], default=1e4, warning=True - ) - iscale.set_scaling_factor(self.flow_mass_phase_comp["Liq", "Ca"], sf) - - if iscale.get_scaling_factor(self.flow_mass_phase_comp["Liq", "Mg"]) is None: - sf = iscale.get_scaling_factor( - self.flow_mass_phase_comp["Liq", "Mg"], default=1e3, warning=True - ) - iscale.set_scaling_factor(self.flow_mass_phase_comp["Liq", "Mg"], sf) - - if iscale.get_scaling_factor(self.flow_mass_phase_comp["Liq", "SO4"]) is None: - sf = iscale.get_scaling_factor( - self.flow_mass_phase_comp["Liq", "SO4"], default=1e3, warning=True - ) - iscale.set_scaling_factor(self.flow_mass_phase_comp["Liq", "SO4"], sf) - - if iscale.get_scaling_factor(self.flow_mass_phase_comp["Liq", "Cl"]) is None: - sf = iscale.get_scaling_factor( - self.flow_mass_phase_comp["Liq", "Cl"], default=1e2, warning=True - ) - iscale.set_scaling_factor(self.flow_mass_phase_comp["Liq", "Cl"], sf) - - # these variables do not typically require user input, - # will not override if the user does provide the scaling factor - if self.is_property_constructed("mass_frac_phase_comp"): - for j in self.params.component_list: - if ( - iscale.get_scaling_factor(self.mass_frac_phase_comp["Liq", j]) - is None - ): - if j == "H2O": - iscale.set_scaling_factor( - self.mass_frac_phase_comp["Liq", j], 1 - ) - else: - sf = iscale.get_scaling_factor( - self.flow_mass_phase_comp["Liq", j] - ) / iscale.get_scaling_factor( - self.flow_mass_phase_comp["Liq", "H2O"] - ) - iscale.set_scaling_factor( - self.mass_frac_phase_comp["Liq", j], sf - ) - - if self.is_property_constructed("flow_vol"): - sf = iscale.get_scaling_factor( - self.flow_mass_phase_comp["Liq", "H2O"] - ) / iscale.get_scaling_factor(self.params.dens_mass) - iscale.set_scaling_factor(self.flow_vol, sf) - - if self.is_property_constructed("flow_mol_phase_comp"): - for j in self.params.component_list: - if ( - iscale.get_scaling_factor(self.flow_mol_phase_comp["Liq", j]) - is None - ): - sf = iscale.get_scaling_factor( - self.flow_mass_phase_comp["Liq", j] - ) / iscale.get_scaling_factor(self.params.mw_comp[j]) - iscale.set_scaling_factor(self.flow_mol_phase_comp["Liq", j], sf) - - if self.is_property_constructed("conc_mol_phase_comp"): - for j in self.params.component_list: - if ( - iscale.get_scaling_factor(self.conc_mol_phase_comp["Liq", j]) - is None - ): - sf = iscale.get_scaling_factor( - self.flow_mol_phase_comp["Liq", j] - ) / iscale.get_scaling_factor(self.flow_vol) - iscale.set_scaling_factor(self.conc_mol_phase_comp["Liq", j], sf) - - if self.is_property_constructed("enth_flow"): - if iscale.get_scaling_factor(self.enth_flow) is None: - sf = ( - iscale.get_scaling_factor(self.params.cp) - * iscale.get_scaling_factor(self.flow_mass_phase_comp["Liq", "H2O"]) - * 1e-1 - ) # temperature change on the order of 1e1 - iscale.set_scaling_factor(self.enth_flow, sf) - - # transforming constraints - # property relationships with no index, simple constraint - v_str_lst_simple = ["flow_vol"] - for v_str in v_str_lst_simple: - if self.is_property_constructed(v_str): - v = getattr(self, v_str) - sf = iscale.get_scaling_factor(v, default=1, warning=True) - c = getattr(self, "eq_" + v_str) - iscale.constraint_scaling_transform(c, sf) - - # property relationships indexed by component and phase - v_str_lst_phase_comp = [ - "mass_frac_phase_comp", - "flow_mol_phase_comp", - "conc_mol_phase_comp", - ] - for v_str in v_str_lst_phase_comp: - if self.is_property_constructed(v_str): - v_comp = getattr(self, v_str) - c_comp = getattr(self, "eq_" + v_str) - for j, c in c_comp.items(): - sf = iscale.get_scaling_factor( - v_comp["Liq", j], default=1, warning=True - ) - iscale.constraint_scaling_transform(c, sf) diff --git a/watertap/property_models/tests/test_seawater_ion_generic.py b/watertap/property_models/tests/test_seawater_ion_generic.py deleted file mode 100644 index 9f683e86c2..0000000000 --- a/watertap/property_models/tests/test_seawater_ion_generic.py +++ /dev/null @@ -1,81 +0,0 @@ -################################################################################# -# WaterTAP Copyright (c) 2020-2024, 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 -from idaes.core import FlowsheetBlock -import idaes.core.util.scaling as iscale -from pyomo.util.check_units import assert_units_consistent -from idaes.models.properties.modular_properties.base.generic_property import ( - GenericParameterBlock, -) -from watertap.property_models.seawater_ion_generic import configuration -from watertap.core.util.initialization import check_dof -from idaes.core.solvers import get_solver - -solver = get_solver() -# ----------------------------------------------------------------------------- - - -@pytest.mark.component -def test_property_seawater_ions(): - m = ConcreteModel() - - m.fs = FlowsheetBlock(dynamic=False) - m.fs.properties = GenericParameterBlock(**configuration) - m.fs.stream = m.fs.properties.build_state_block([0], defined_state=True) - - # specify - m.fs.stream[0].flow_mol_phase_comp["Liq", "Na_+"].fix(0.008845) - m.fs.stream[0].flow_mol_phase_comp["Liq", "Ca_2+"].fix(0.000174) - m.fs.stream[0].flow_mol_phase_comp["Liq", "Mg_2+"].fix(0.001049) - m.fs.stream[0].flow_mol_phase_comp["Liq", "SO4_2-"].fix(0.000407) - m.fs.stream[0].flow_mol_phase_comp["Liq", "Cl_-"].fix(0.010479) - m.fs.stream[0].flow_mol_phase_comp["Liq", "H2O"].fix(0.979046) - m.fs.stream[0].temperature.fix(273.15 + 25) - m.fs.stream[0].pressure.fix(101325) - - # # scaling - iscale.calculate_scaling_factors(m.fs) - - # checking state block - assert_units_consistent(m) - - # check dof = 0 - check_dof(m, fail_flag=True) - - # initialize - m.fs.stream.initialize() - - # check solve - results = solver.solve(m) - assert_optimal_termination(results) - - # # check values - assert value(m.fs.stream[0].mole_frac_phase_comp["Liq", "Na_+"]) == pytest.approx( - 8.845e-3, rel=1e-3 - ) - assert value(m.fs.stream[0].mole_frac_phase_comp["Liq", "Ca_2+"]) == pytest.approx( - 1.74e-4, rel=1e-3 - ) - assert value(m.fs.stream[0].mole_frac_phase_comp["Liq", "Cl_-"]) == pytest.approx( - 1.048e-2, rel=1e-3 - ) - assert value(m.fs.stream[0].mole_frac_phase_comp["Liq", "H2O"]) == pytest.approx( - 0.9790, rel=1e-3 - ) - assert value(m.fs.stream[0].mole_frac_phase_comp["Liq", "Mg_2+"]) == pytest.approx( - 1.049e-3, rel=1e-3 - ) - assert value(m.fs.stream[0].mole_frac_phase_comp["Liq", "SO4_2-"]) == pytest.approx( - 4.07e-4, rel=1e-3 - ) diff --git a/watertap/unit_models/tests/test_nanofiltration_ZO.py b/watertap/unit_models/tests/test_nanofiltration_ZO.py index ba376ee5b5..df2a74f684 100644 --- a/watertap/unit_models/tests/test_nanofiltration_ZO.py +++ b/watertap/unit_models/tests/test_nanofiltration_ZO.py @@ -15,7 +15,6 @@ ConcreteModel, value, Var, - Constraint, assert_optimal_termination, ) from pyomo.network import Port @@ -26,11 +25,8 @@ MomentumBalanceType, ) from watertap.unit_models.nanofiltration_ZO import NanofiltrationZO -from idaes.models.properties.modular_properties.base.generic_property import ( - GenericParameterBlock, -) -from watertap.property_models.seawater_ion_generic import configuration -import watertap.property_models.seawater_ion_prop_pack as props + +import watertap.property_models.multicomp_aq_sol_prop_pack as props from watertap.core.util.initialization import assert_no_degrees_of_freedom from pyomo.util.check_units import assert_units_consistent @@ -44,7 +40,6 @@ from idaes.core.util.testing import initialization_tester from idaes.core.util.scaling import ( calculate_scaling_factors, - constraint_scaling_transform, unscaled_variables_generator, unscaled_constraints_generator, badly_scaled_var_generator, @@ -60,7 +55,9 @@ def test_config(): m = ConcreteModel() m.fs = FlowsheetBlock(dynamic=False) - m.fs.properties = props.PropParameterBlock() + m.fs.properties = props.MCASParameterBlock( + solute_list=["Na_+", "Ca_2+", "Mg_2+", "SO4_2-", "Cl_-"] + ) m.fs.unit = NanofiltrationZO(property_package=m.fs.properties) assert len(m.fs.unit.config) == 9 @@ -78,7 +75,9 @@ def test_config(): def test_option_has_pressure_change(): m = ConcreteModel() m.fs = FlowsheetBlock(dynamic=False) - m.fs.properties = props.PropParameterBlock() + m.fs.properties = props.MCASParameterBlock( + solute_list=["Na_+", "Ca_2+", "Mg_2+", "SO4_2-", "Cl_-"] + ) m.fs.unit = NanofiltrationZO( property_package=m.fs.properties, has_pressure_change=True ) @@ -93,18 +92,21 @@ def unit_frame(self): m = ConcreteModel() m.fs = FlowsheetBlock(dynamic=False) - m.fs.properties = props.PropParameterBlock() + m.fs.properties = props.MCASParameterBlock( + solute_list=["Na_+", "Ca_2+", "Mg_2+", "SO4_2-", "Cl_-"], + material_flow_basis=props.MaterialFlowBasis.mass, + ) m.fs.unit = NanofiltrationZO(property_package=m.fs.properties) # fully specify system feed_flow_mass = 1 feed_mass_frac = { - "Na": 11122e-6, - "Ca": 382e-6, - "Mg": 1394e-6, - "SO4": 2136e-6, - "Cl": 20316.88e-6, + "Na_+": 11122e-6, + "Ca_2+": 382e-6, + "Mg_2+": 1394e-6, + "SO4_2-": 2136e-6, + "Cl_-": 20316.88e-6, } m.fs.unit.feed_side.properties_in[0].flow_mass_phase_comp["Liq", "H2O"].fix( feed_flow_mass * (1 - sum(x for x in feed_mass_frac.values())) @@ -120,23 +122,15 @@ def unit_frame(self): m.fs.unit.area.fix(500) m.fs.unit.properties_permeate[0].pressure.fix(101325) - m.fs.unit.rejection_phase_comp[0, "Liq", "Na"].fix(0.01) - m.fs.unit.rejection_phase_comp[0, "Liq", "Ca"].fix(0.79) - m.fs.unit.rejection_phase_comp[0, "Liq", "Mg"].fix(0.94) - m.fs.unit.rejection_phase_comp[0, "Liq", "SO4"].fix(0.87) - m.fs.unit.rejection_phase_comp[0, "Liq", "Cl"] = ( - 0.15 # guess, but electroneutrality enforced below - ) - charge_comp = {"Na": 1, "Ca": 2, "Mg": 2, "SO4": -2, "Cl": -1} - m.fs.unit.eq_electroneutrality = Constraint( - expr=0 - == sum( - charge_comp[j] - * m.fs.unit.feed_side.properties_out[0].conc_mol_phase_comp["Liq", j] - for j in charge_comp - ) + m.fs.unit.rejection_phase_comp[0, "Liq", "Na_+"].fix(0.01) + m.fs.unit.rejection_phase_comp[0, "Liq", "Ca_2+"].fix(0.79) + m.fs.unit.rejection_phase_comp[0, "Liq", "Mg_2+"].fix(0.94) + m.fs.unit.rejection_phase_comp[0, "Liq", "SO4_2-"].fix(0.87) + m.fs.unit.rejection_phase_comp[0, "Liq", "Cl_-"].fix(0.15) + + m.fs.unit.feed_side.properties_in[0].assert_electroneutrality( + defined_state=True, adjust_by_ion="Cl_-" ) - constraint_scaling_transform(m.fs.unit.eq_electroneutrality, 1) return m @@ -154,9 +148,10 @@ def test_build(self, unit_frame): assert isinstance(port, Port) # test statistics - assert number_variables(m) == 89 - assert number_total_constraints(m) == 74 - assert number_unused_variables(m) == 1 + assert number_variables(m) == 117 + assert number_total_constraints(m) == 86 + assert number_unused_variables(m) == 16 + assert_units_consistent(m) @pytest.mark.unit def test_dof(self, unit_frame): @@ -171,19 +166,19 @@ def test_calculate_scaling(self, unit_frame): "flow_mass_phase_comp", 1, index=("Liq", "H2O") ) m.fs.properties.set_default_scaling( - "flow_mass_phase_comp", 1e2, index=("Liq", "Na") + "flow_mass_phase_comp", 1e2, index=("Liq", "Na_+") ) m.fs.properties.set_default_scaling( - "flow_mass_phase_comp", 1e4, index=("Liq", "Ca") + "flow_mass_phase_comp", 1e4, index=("Liq", "Ca_2+") ) m.fs.properties.set_default_scaling( - "flow_mass_phase_comp", 1e3, index=("Liq", "Mg") + "flow_mass_phase_comp", 1e3, index=("Liq", "Mg_2+") ) m.fs.properties.set_default_scaling( - "flow_mass_phase_comp", 1e3, index=("Liq", "SO4") + "flow_mass_phase_comp", 1e3, index=("Liq", "SO4_2-") ) m.fs.properties.set_default_scaling( - "flow_mass_phase_comp", 1e2, index=("Liq", "Cl") + "flow_mass_phase_comp", 1e2, index=("Liq", "Cl_-") ) calculate_scaling_factors(m) @@ -243,19 +238,19 @@ def test_solution(self, unit_frame): m.fs.unit.properties_permeate[0].flow_mass_phase_comp["Liq", "H2O"] ) assert pytest.approx(2.002, rel=1e-3) == value( - m.fs.unit.properties_permeate[0].conc_mol_phase_comp["Liq", "Ca"] + m.fs.unit.properties_permeate[0].conc_mol_phase_comp["Liq", "Ca_2+"] ) - assert pytest.approx(484.0, rel=1e-3) == value( - m.fs.unit.properties_permeate[0].conc_mol_phase_comp["Liq", "Cl"] + assert pytest.approx(487.116, rel=1e-3) == value( + m.fs.unit.properties_permeate[0].conc_mol_phase_comp["Liq", "Cl_-"] ) assert pytest.approx(3.441, rel=1e-3) == value( - m.fs.unit.properties_permeate[0].conc_mol_phase_comp["Liq", "Mg"] + m.fs.unit.properties_permeate[0].conc_mol_phase_comp["Liq", "Mg_2+"] ) assert pytest.approx(478.9, rel=1e-3) == value( - m.fs.unit.properties_permeate[0].conc_mol_phase_comp["Liq", "Na"] + m.fs.unit.properties_permeate[0].conc_mol_phase_comp["Liq", "Na_+"] ) assert pytest.approx(2.891, rel=1e-3) == value( - m.fs.unit.properties_permeate[0].conc_mol_phase_comp["Liq", "SO4"] + m.fs.unit.properties_permeate[0].conc_mol_phase_comp["Liq", "SO4_2-"] ) assert pytest.approx(0.86, rel=1e-3) == value( m.fs.unit.recovery_vol_phase[0, "Liq"] @@ -264,77 +259,3 @@ def test_solution(self, unit_frame): @pytest.mark.unit def test_report(self, unit_frame): unit_frame.fs.unit.report() - - @pytest.mark.component - def test_NF_with_generic_property_model(self): - m = ConcreteModel() - m.fs = FlowsheetBlock(dynamic=False) - m.fs.properties = GenericParameterBlock(**configuration) - m.fs.unit = NanofiltrationZO( - property_package=m.fs.properties, has_pressure_change=False - ) - - # fully specify system - m.fs.unit.inlet.flow_mol_phase_comp[0, "Liq", "Na_+"].fix(0.008845) - m.fs.unit.inlet.flow_mol_phase_comp[0, "Liq", "Ca_2+"].fix(0.000174) - m.fs.unit.inlet.flow_mol_phase_comp[0, "Liq", "Mg_2+"].fix(0.001049) - m.fs.unit.inlet.flow_mol_phase_comp[0, "Liq", "SO4_2-"].fix(0.000407) - m.fs.unit.inlet.flow_mol_phase_comp[0, "Liq", "Cl_-"].fix(0.010479) - m.fs.unit.inlet.flow_mol_phase_comp[0, "Liq", "H2O"].fix(0.979046) - m.fs.unit.feed_side.properties_in[0].pressure.fix(4e5) - m.fs.unit.feed_side.properties_in[0].temperature.fix(298.15) - - m.fs.unit.flux_vol_solvent.fix(1.67e-6) - m.fs.unit.recovery_vol_phase.fix(0.86) - m.fs.unit.properties_permeate[0].pressure.fix(101325) - - m.fs.unit.rejection_phase_comp[0, "Liq", "Na_+"].fix(0.01) - m.fs.unit.rejection_phase_comp[0, "Liq", "Ca_2+"].fix(0.79) - m.fs.unit.rejection_phase_comp[0, "Liq", "Mg_2+"].fix(0.94) - m.fs.unit.rejection_phase_comp[0, "Liq", "SO4_2-"].fix(0.87) - m.fs.unit.rejection_phase_comp[0, "Liq", "Cl_-"] = ( - 0.15 # guess, but electroneutrality enforced below - ) - charge_comp = {"Na_+": 1, "Ca_2+": 2, "Mg_2+": 2, "SO4_2-": -2, "Cl_-": -1} - m.fs.unit.eq_electroneutrality = Constraint( - expr=0 - == sum( - charge_comp[j] - * m.fs.unit.feed_side.properties_out[0].conc_mol_phase_comp["Liq", j] - for j in charge_comp - ) - ) - constraint_scaling_transform(m.fs.unit.eq_electroneutrality, 1) - - assert_units_consistent(m) - - assert_no_degrees_of_freedom(m) - - calculate_scaling_factors(m) - - initialization_tester(m) - - results = solver.solve(m) - - # Check for optimal solution - assert_optimal_termination(results) - - assert pytest.approx(0.868, rel=1e-3) == value( - m.fs.unit.properties_permeate[0].flow_mass_phase_comp["Liq", "H2O"] - / m.fs.unit.feed_side.properties_in[0].flow_mass_phase_comp["Liq", "H2O"] - ) - assert pytest.approx(1.978, rel=1e-3) == value( - m.fs.unit.properties_permeate[0].conc_mol_phase_comp["Liq", "Ca_2+"] - ) - assert pytest.approx(479.1, rel=1e-3) == value( - m.fs.unit.properties_permeate[0].conc_mol_phase_comp["Liq", "Cl_-"] - ) - assert pytest.approx(3.407, rel=1e-3) == value( - m.fs.unit.properties_permeate[0].conc_mol_phase_comp["Liq", "Mg_2+"] - ) - assert pytest.approx(473.9, rel=1e-3) == value( - m.fs.unit.properties_permeate[0].conc_mol_phase_comp["Liq", "Na_+"] - ) - assert pytest.approx(2.864, rel=1e-3) == value( - m.fs.unit.properties_permeate[0].conc_mol_phase_comp["Liq", "SO4_2-"] - )