###############################################################################
# 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/"
#
###############################################################################
from copy import deepcopy
from enum import Enum, auto
from pyomo.common.config import Bool, ConfigDict, ConfigValue, In
from pyomo.environ import (
Constraint,
Expression,
Param,
NegativeReals,
NonNegativeReals,
Var,
check_optimal_termination,
exp,
units as pyunits,
)
from idaes.core import (
FlowDirection,
EnergyBalanceType,
MaterialBalanceType,
MomentumBalanceType,
useDefault,
)
from idaes.core.util import scaling as iscale
from idaes.core.util.config import is_physical_parameter_block
from idaes.core.util.exceptions import ConfigurationError, InitializationError
from idaes.core.util.misc import add_object_reference
[docs]class ConcentrationPolarizationType(Enum):
"""
none: no concentration polarization
fixed: concentration polarization modulus is a user specified value
calculated: calculate concentration polarization (concentration at membrane interface)
"""
none = auto()
fixed = auto()
calculated = auto()
[docs]class MassTransferCoefficient(Enum):
"""
none: mass transfer coefficient not utilized for concentration polarization effect
fixed: mass transfer coefficient is a user specified value
calculated: mass transfer coefficient is calculated
"""
none = auto()
fixed = auto()
calculated = auto()
# TODO: add option for users to define their own relationship?
[docs]class PressureChangeType(Enum):
"""
fixed_per_stage: pressure drop across membrane channel is a user-specified value
fixed_per_unit_length: pressure drop per unit length across membrane channel is a user-specified value
calculated: pressure drop across membrane channel is calculated
"""
fixed_per_stage = auto()
fixed_per_unit_length = auto()
calculated = auto()
CONFIG_Template = ConfigDict()
CONFIG_Template.declare(
"dynamic",
ConfigValue(
default=False,
domain=In([False]),
description="Dynamic model flag - must be False",
doc="""Indicates whether this model will be dynamic or not.
**default** - False. Membrane units do not yet support dynamic
behavior.""",
),
)
CONFIG_Template.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. Membrane units do not have defined volume, thus
this must be False.""",
),
)
CONFIG_Template.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_Template.declare(
"property_package_args",
ConfigDict(
implicit=True,
description="Arguments to use for constructing property packages",
doc="""A ConfigDict with arguments to be passed to a property block(s)
and used when constructing these.
**default** - None.
**Valid values:** {
see property package for documentation.}""",
),
)
CONFIG_Template.declare(
"material_balance_type",
ConfigValue(
default=MaterialBalanceType.useDefault,
domain=In(MaterialBalanceType),
description="Material balance construction flag",
doc="""Indicates what type of mass balance should be constructed,
**default** - useDefault.
**Valid values:** {
**MaterialBalanceType.useDefault** - refer to property package for default
balance type
**MaterialBalanceType.none** - exclude material balances,
**MaterialBalanceType.componentPhase** - use phase component balances,
**MaterialBalanceType.componentTotal** - use total component balances,
**MaterialBalanceType.elementTotal** - use total element balances,
**MaterialBalanceType.total** - use total material balance.}""",
),
)
CONFIG_Template.declare(
"energy_balance_type",
ConfigValue(
default=EnergyBalanceType.useDefault,
domain=In(EnergyBalanceType),
description="Energy balance construction flag",
doc="""Indicates what type of energy balance should be constructed.
**default** - useDefault.
**Valid values:** {
**EnergyBalanceType.useDefault** - refer to property package for default
balance type
**EnergyBalanceType.none** - exclude energy balances,
**EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material,
**EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase,
**EnergyBalanceType.energyTotal** - single energy balance for material,
**EnergyBalanceType.energyPhase** - energy balances for each phase.}""",
),
)
CONFIG_Template.declare(
"momentum_balance_type",
ConfigValue(
default=MomentumBalanceType.pressureTotal,
domain=In(MomentumBalanceType),
description="Momentum balance construction flag",
doc="""Indicates what type of momentum balance should be constructed,
**default** - MomentumBalanceType.pressureTotal.
**Valid values:** {
**MomentumBalanceType.none** - exclude momentum balances,
**MomentumBalanceType.pressureTotal** - single pressure balance for material,
**MomentumBalanceType.pressurePhase** - pressure balances for each phase,
**MomentumBalanceType.momentumTotal** - single momentum balance for material,
**MomentumBalanceType.momentumPhase** - momentum balances for each phase.}""",
),
)
CONFIG_Template.declare(
"concentration_polarization_type",
ConfigValue(
default=ConcentrationPolarizationType.calculated,
domain=In(ConcentrationPolarizationType),
description="External concentration polarization effect in RO",
doc="""
Options to account for concentration polarization.
**default** - ``ConcentrationPolarizationType.calculated``
.. csv-table::
:header: "Configuration Options", "Description"
"``ConcentrationPolarizationType.none``", "Simplifying assumption to ignore concentration polarization"
"``ConcentrationPolarizationType.fixed``", "Specify an estimated value for the concentration polarization modulus"
"``ConcentrationPolarizationType.calculated``", "Allow model to perform calculation of membrane-interface concentration"
""",
),
)
CONFIG_Template.declare(
"mass_transfer_coefficient",
ConfigValue(
default=MassTransferCoefficient.calculated,
domain=In(MassTransferCoefficient),
description="Mass transfer coefficient in RO feed channel",
doc="""
Options to account for mass transfer coefficient.
**default** - ``MassTransferCoefficient.calculated``
.. csv-table::
:header: "Configuration Options", "Description"
"``MassTransferCoefficient.none``", "Mass transfer coefficient not used in calculations"
"``MassTransferCoefficient.fixed``", "Specify an estimated value for the mass transfer coefficient in the feed channel"
"``MassTransferCoefficient.calculated``", "Allow model to perform calculation of mass transfer coefficient"
""",
),
)
CONFIG_Template.declare(
"has_pressure_change",
ConfigValue(
default=False,
domain=Bool,
description="Pressure change term construction flag",
doc="""Indicates whether terms for pressure change should be
constructed,
**default** - False.
**Valid values:** {
**True** - include pressure change terms,
**False** - exclude pressure change terms.}""",
),
)
CONFIG_Template.declare(
"pressure_change_type",
ConfigValue(
default=PressureChangeType.fixed_per_stage,
domain=In(PressureChangeType),
description="Pressure change term construction flag",
doc="""
Indicates what type of pressure change calculation will be made. To use any of the
``pressure_change_type`` options to account for pressure drop, the configuration keyword
``has_pressure_change`` must also be set to ``True``. Also, if a value is specified for pressure
change, it should be negative to represent pressure drop.
**default** - ``PressureChangeType.fixed_per_stage``
.. csv-table::
:header: "Configuration Options", "Description"
"``PressureChangeType.fixed_per_stage``", "Specify an estimated value for pressure drop across the membrane feed channel"
"``PressureChangeType.fixed_per_unit_length``", "Specify an estimated value for pressure drop per unit length across the membrane feed channel"
"``PressureChangeType.calculated``", "Allow model to perform calculation of pressure drop across the membrane feed channel"
""",
),
)
class MembraneChannelMixin:
def _add_pressure_change(self, pressure_change_type=PressureChangeType.calculated):
raise NotImplementedError()
def _skip_element(self, x):
raise NotImplementedError()
def _add_var_reference(self, pyomo_var, reference_name, param_name):
if pyomo_var is not None:
# Validate pyomo_var and add a reference
if not isinstance(pyomo_var, (Var, Param, Expression)):
raise ConfigurationError(
f"{self.name} {param_name} must be a Pyomo Var, Param or "
"Expression."
)
elif pyomo_var.is_indexed():
raise ConfigurationError(
f"{self.name} {param_name} must be a scalar (unindexed) "
"component."
)
add_object_reference(self, reference_name, pyomo_var)
def _set_nfe(self):
self.first_element = self.length_domain.first()
self.last_element = self.length_domain.last()
self.nfe = Param(
initialize=(len(self.difference_elements)),
units=pyunits.dimensionless,
doc="Number of finite elements",
)
def add_total_pressure_balances(
self,
has_pressure_change=True,
pressure_change_type=PressureChangeType.calculated,
custom_term=None,
):
super().add_total_pressure_balances(
has_pressure_change=has_pressure_change, custom_term=custom_term
)
@self.Constraint(
self.flowsheet().config.time,
self.length_domain,
doc="Pressure at interface",
)
def eq_equal_pressure_interface(b, t, x):
if b._skip_element(x):
return Constraint.Skip
return b.properties_interface[t, x].pressure == b.properties[t, x].pressure
if has_pressure_change:
self._add_pressure_change(pressure_change_type=pressure_change_type)
if pressure_change_type == PressureChangeType.calculated:
self._add_calculated_pressure_change()
def add_isothermal_conditions(self):
# ==========================================================================
# Bulk and interface connections on the feed-side
# TEMPERATURE
@self.Constraint(
self.flowsheet().config.time,
self.length_domain,
doc="Temperature at interface",
)
def eq_equal_temp_interface(b, t, x):
if b._skip_element(x):
return Constraint.Skip
return (
b.properties[t, x].temperature
== b.properties_interface[t, x].temperature
)
def add_extensive_flow_to_interface(self):
# VOLUMETRIC FLOWRATE
@self.Constraint(
self.flowsheet().config.time,
self.length_domain,
doc="Volumetric flow at interface of inlet",
)
def eq_equal_flow_vol_interface(b, t, x):
if b._skip_element(x):
return Constraint.Skip
return (
b.properties_interface[t, x].flow_vol_phase["Liq"]
== b.properties[t, x].flow_vol_phase["Liq"]
)
def add_concentration_polarization(
self,
concentration_polarization_type=ConcentrationPolarizationType.calculated,
mass_transfer_coefficient=MassTransferCoefficient.calculated,
):
solute_set = self.config.property_package.solute_set
units_meta = self.config.property_package.get_metadata().get_derived_units
if concentration_polarization_type == ConcentrationPolarizationType.none:
@self.Constraint(
self.flowsheet().config.time,
self.length_domain,
solute_set,
doc="Unit concentration polarization modulus",
)
def eq_cp_modulus(b, t, x, j):
if b._skip_element(x):
return Constraint.Skip
return (
b.properties_interface[t, x].conc_mass_phase_comp["Liq", j]
== b.properties[t, x].conc_mass_phase_comp["Liq", j]
)
return self.eq_cp_modulus
if concentration_polarization_type not in (
ConcentrationPolarizationType.fixed,
ConcentrationPolarizationType.calculated,
):
raise ConfigurationError(
f"Unrecognized concentration_polarization_type {concentration_polarization_type}"
)
self.cp_modulus = Var(
self.flowsheet().config.time,
self.length_domain,
solute_set,
initialize=1.1,
bounds=(0.1, 3),
domain=NonNegativeReals,
units=pyunits.dimensionless,
doc="Concentration polarization modulus",
)
@self.Constraint(
self.flowsheet().config.time,
self.length_domain,
solute_set,
doc="Concentration polarization modulus",
)
def eq_cp_modulus(b, t, x, j):
if b._skip_element(x):
return Constraint.Skip
return (
self.properties_interface[t, x].conc_mass_phase_comp["Liq", j]
== self.properties[t, x].conc_mass_phase_comp["Liq", j]
* self.cp_modulus[t, x, j]
)
if concentration_polarization_type == ConcentrationPolarizationType.calculated:
if mass_transfer_coefficient == MassTransferCoefficient.none:
raise ConfigurationError()
# mass_transfer_coefficient is either calculated or fixed
self.K = Var(
self.flowsheet().config.time,
self.length_domain,
solute_set,
initialize=5e-5,
bounds=(1e-6, 1e-3),
domain=NonNegativeReals,
units=units_meta("length") * units_meta("time") ** -1,
doc="Membrane channel mass transfer coefficient",
)
if mass_transfer_coefficient == MassTransferCoefficient.calculated:
self._add_calculated_mass_transfer_coefficient()
return self.eq_cp_modulus
def add_expressions(self):
"""
Generate expressions for additional results desired for full report
"""
if hasattr(self, "N_Re"):
@self.Expression(
self.flowsheet().config.time, doc="Average Reynolds Number expression"
)
def N_Re_avg(b, t):
return sum(b.N_Re[t, x] for x in self.length_domain) / self.nfe
if hasattr(self, "K"):
@self.Expression(
self.flowsheet().config.time,
self.config.property_package.solute_set,
doc="Average mass transfer coefficient expression",
)
def K_avg(b, t, j):
return sum(b.K[t, x, j] for x in self.difference_elements) / self.nfe
## should be called by add concentration polarization
def _add_calculated_mass_transfer_coefficient(self):
self._add_calculated_pressure_change_mass_transfer_components()
self.N_Sc = Var(
self.flowsheet().config.time,
self.length_domain,
initialize=5e2,
bounds=(1e2, 2e3),
domain=NonNegativeReals,
units=pyunits.dimensionless,
doc="Schmidt number in membrane channel",
)
self.N_Sh = Var(
self.flowsheet().config.time,
self.length_domain,
initialize=1e2,
bounds=(1, 3e2),
domain=NonNegativeReals,
units=pyunits.dimensionless,
doc="Sherwood number in membrane channel",
)
@self.Constraint(
self.flowsheet().config.time,
self.length_domain,
self.config.property_package.solute_set,
doc="Mass transfer coefficient in membrane channel",
)
def eq_K(b, t, x, j):
if b._skip_element(x):
return Constraint.Skip
return (
b.K[t, x, j] * b.dh
# TODO: add diff coefficient to SW prop and consider multi-components
== b.properties[t, x].diffus_phase_comp["Liq", j] * b.N_Sh[t, x]
)
@self.Constraint(
self.flowsheet().config.time, self.length_domain, doc="Sherwood number"
)
def eq_N_Sh(b, t, x):
return b.N_Sh[t, x] == 0.46 * (b.N_Re[t, x] * b.N_Sc[t, x]) ** 0.36
@self.Constraint(
self.flowsheet().config.time, self.length_domain, doc="Schmidt number"
)
def eq_N_Sc(b, t, x):
# # TODO: This needs to be revisted. Diffusion is now by component, but
# not H2O and this var should also be by component, but the implementation
# is not immediately clear.
return (
b.N_Sc[t, x]
* b.properties[t, x].dens_mass_phase["Liq"]
* b.properties[t, x].diffus_phase_comp[
"Liq", b.properties[t, x].params.component_list.last()
]
== b.properties[t, x].visc_d_phase["Liq"]
)
return self.eq_K
def _add_calculated_pressure_change_mass_transfer_components(self):
# NOTE: This function could be called by either
# `_add_calculated_pressure_change` *and/or*
# `_add_calculated_mass_transfer_coefficient`.
# Therefore, we add this simple gaurd against it being called twice.
if hasattr(self, "channel_height"):
return
if not hasattr(self, "width"):
raise ConfigurationError(
f"Due to either a calculated mass transfer coefficient or a calculated pressure change, a ``width`` variable needs to be supplied to `add_geometry` for this MembraneChannel"
)
units_meta = self.config.property_package.get_metadata().get_derived_units
if not hasattr(self, "area"):
# comes from ControlVolume1D for 1DMC
self.area = Var(
initialize=1e-3 * 1 * 0.95,
bounds=(0, 1e3),
domain=NonNegativeReals,
units=units_meta("length") ** 2,
doc="Cross sectional area",
)
self.channel_height = Var(
initialize=1e-3,
bounds=(1e-4, 5e-3),
domain=NonNegativeReals,
units=units_meta("length"),
doc="membrane-channel height",
)
self.dh = Var(
initialize=1e-3,
bounds=(1e-4, 5e-3),
domain=NonNegativeReals,
units=units_meta("length"),
doc="Hydraulic diameter of membrane channel",
)
self.spacer_porosity = Var(
initialize=0.95,
bounds=(0.1, 0.99),
domain=NonNegativeReals,
units=pyunits.dimensionless,
doc="membrane-channel spacer porosity",
)
self.N_Re = Var(
self.flowsheet().config.time,
self.length_domain,
initialize=5e2,
bounds=(10, 5e3),
domain=NonNegativeReals,
units=pyunits.dimensionless,
doc="Reynolds number in membrane channel",
)
@self.Constraint(doc="Hydraulic diameter") # eqn. 17 in Schock & Miquel, 1987
def eq_dh(b):
return b.dh == 4 * b.spacer_porosity / (
2 / b.channel_height + (1 - b.spacer_porosity) * 8 / b.channel_height
)
@self.Constraint(doc="Cross-sectional area")
def eq_area(b):
return b.area == b.channel_height * b.width * b.spacer_porosity
@self.Constraint(
self.flowsheet().config.time, self.length_domain, doc="Reynolds number"
)
def eq_N_Re(b, t, x):
return (
b.N_Re[t, x] * b.area * b.properties[t, x].visc_d_phase["Liq"]
== sum(
b.properties[t, x].flow_mass_phase_comp["Liq", j]
for j in b.config.property_package.component_list
)
* b.dh
)
def _add_interface_stateblock(self, has_phase_equilibrium=None):
"""
This method constructs the interface state blocks for the
control volume.
Args:
has_phase_equilibrium: indicates whether equilibrium calculations
will be required in state blocks
Returns:
None
"""
tmp_dict = dict(**self.config.property_package_args)
tmp_dict["has_phase_equilibrium"] = has_phase_equilibrium
tmp_dict["defined_state"] = False # these blocks are not inlets or outlets
self.properties_interface = self.config.property_package.build_state_block(
self.flowsheet().config.time,
self.length_domain,
doc="Material properties of feed-side membrane interface",
**tmp_dict,
)
def _add_calculated_pressure_change(self):
self._add_calculated_pressure_change_mass_transfer_components()
units_meta = self.config.property_package.get_metadata().get_derived_units
self.velocity = Var(
self.flowsheet().config.time,
self.length_domain,
initialize=0.5,
bounds=(1e-2, 5),
domain=NonNegativeReals,
units=units_meta("length") / units_meta("time"),
doc="Crossflow velocity in feed channel",
)
self.friction_factor_darcy = Var(
self.flowsheet().config.time,
self.length_domain,
initialize=0.5,
bounds=(1e-2, 5),
domain=NonNegativeReals,
units=pyunits.dimensionless,
doc="Darcy friction factor in feed channel",
)
# Constraints active when MassTransferCoefficient.calculated
# Mass transfer coefficient calculation
## ==========================================================================
# Crossflow velocity
@self.Constraint(
self.flowsheet().config.time,
self.length_domain,
doc="Crossflow velocity constraint",
)
def eq_velocity(b, t, x):
return b.velocity[t, x] * b.area == b.properties[t, x].flow_vol_phase["Liq"]
## ==========================================================================
# Darcy friction factor based on eq. S27 in SI for Cost Optimization of Osmotically Assisted Reverse Osmosis
# TODO: this relationship for friction factor is specific to a particular spacer geometry. Add alternatives.
@self.Constraint(
self.flowsheet().config.time,
self.length_domain,
doc="Darcy friction factor constraint",
)
def eq_friction_factor_darcy(b, t, x):
return (b.friction_factor_darcy[t, x] - 0.42) * b.N_Re[t, x] == 189.3
## ==========================================================================
# Pressure change per unit length due to friction
# -1/2*f/dh*density*velocity^2
@self.Constraint(
self.flowsheet().config.time,
self.length_domain,
doc="pressure change per unit length due to friction",
)
def eq_dP_dx(b, t, x):
return (
b.dP_dx[t, x] * b.dh
== -0.5
* b.friction_factor_darcy[t, x]
* b.properties[t, x].dens_mass_phase["Liq"]
* b.velocity[t, x] ** 2
)
def _get_state_args(self, initialize_guess, state_args):
"""
Arguments:
initialize_guess : a dict of guesses for solvent_recovery, solute_recovery,
and cp_modulus. These guesses offset the initial values
for the retentate, permeate, and membrane interface
state blocks from the inlet feed
(default =
{'deltaP': -1e4,
'solvent_recovery': 0.5,
'solute_recovery': 0.01,
'cp_modulus': 1.1})
state_args : a dict of arguments to be passed to the property
package(s) to provide an initial state for the inlet
feed side state block (see documentation of the specific
property package).
"""
# assumptions
if initialize_guess is None:
initialize_guess = {}
if "deltaP" not in initialize_guess:
initialize_guess["deltaP"] = -1e4
if "solvent_recovery" not in initialize_guess:
initialize_guess["solvent_recovery"] = 0.5
if "solute_recovery" not in initialize_guess:
initialize_guess["solute_recovery"] = 0.01
if "cp_modulus" not in initialize_guess:
if hasattr(self, "cp_modulus"):
initialize_guess["cp_modulus"] = 1.1
else:
initialize_guess["cp_modulus"] = 1
# Get source block
# TODO: need to re-visit for counterflow
if self._flow_direction == FlowDirection.forward:
source_idx = self.length_domain.first()
else:
source_idx = self.length_domain.last()
source = self.properties[self.flowsheet().config.time.first(), source_idx]
if state_args is None:
state_args = {}
state_dict = source.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
if "flow_mass_phase_comp" not in state_args.keys():
raise ConfigurationError(
f"{self.__class__.__name__} initialization routine expects "
"flow_mass_phase_comp as a state variable. Check "
"that the property package supports this state "
"variable or that the state_args provided to the "
"initialize call includes this state variable"
)
# slightly modify initial values for other state blocks
state_args_retentate = deepcopy(state_args)
state_args_retentate["pressure"] += initialize_guess["deltaP"]
for j in self.config.property_package.solvent_set:
state_args_retentate["flow_mass_phase_comp"][("Liq", j)] *= (
1 - initialize_guess["solvent_recovery"]
)
for j in self.config.property_package.solute_set:
state_args_retentate["flow_mass_phase_comp"][("Liq", j)] *= (
1 - initialize_guess["solute_recovery"]
)
state_args_interface_in = deepcopy(state_args)
state_args_interface_out = deepcopy(state_args_retentate)
for j in self.config.property_package.solute_set:
state_args_interface_in["flow_mass_phase_comp"][
("Liq", j)
] *= initialize_guess["cp_modulus"]
state_args_interface_out["flow_mass_phase_comp"][
("Liq", j)
] *= initialize_guess["cp_modulus"]
# TODO: I think this is what we'd like to do, but IDAES needs to be changed
# state_args_interface = {}
# for t in self.flowsheet().config.time:
# for x in self.length_domain:
# assert 0.0 <= x <= 1.0
# state_args_tx = {}
# for k in state_args_interface_in:
# if isinstance(state_args_interface_in[k], dict):
# if k not in state_args_tx:
# state_args_tx[k] = {}
# for index in state_args_interface_in[k]:
# state_args_tx[k][index] = (
# 1.0 - x
# ) * state_args_interface_in[k][
# index
# ] + x * state_args_interface_out[
# k
# ][
# index
# ]
# else:
# state_args_tx[k] = (1.0 - x) * state_args_interface_in[
# k
# ] + x * state_args_interface_out[k]
# state_args_interface[t, x] = state_args_tx
x = 0.5
state_args_tx = {}
for k in state_args_interface_in:
if isinstance(state_args_interface_in[k], dict):
if k not in state_args_tx:
state_args_tx[k] = {}
for index in state_args_interface_in[k]:
state_args_tx[k][index] = (1.0 - x) * state_args_interface_in[k][
index
] + x * state_args_interface_out[k][index]
else:
state_args_tx[k] = (1.0 - x) * state_args_interface_in[
k
] + x * state_args_interface_out[k]
state_args_interface = state_args_tx
return {
"feed_side": state_args,
"retentate": state_args_retentate,
"interface": state_args_interface,
}
def calculate_scaling_factors(self):
super().calculate_scaling_factors()
if hasattr(self, "channel_height"):
if iscale.get_scaling_factor(self.channel_height) is None:
iscale.set_scaling_factor(self.channel_height, 1e3)
if hasattr(self, "spacer_porosity"):
if iscale.get_scaling_factor(self.spacer_porosity) is None:
iscale.set_scaling_factor(self.spacer_porosity, 1)
if hasattr(self, "dh"):
if iscale.get_scaling_factor(self.dh) is None:
iscale.set_scaling_factor(self.dh, 1e3)
if hasattr(self, "K"):
for v in self.K.values():
if iscale.get_scaling_factor(v) is None:
iscale.set_scaling_factor(v, 1e4)
if hasattr(self, "N_Re"):
for t, x in self.N_Re.keys():
if iscale.get_scaling_factor(self.N_Re[t, x]) is None:
iscale.set_scaling_factor(self.N_Re[t, x], 1e-2)
if hasattr(self, "N_Sc"):
for t, x in self.N_Sc.keys():
if iscale.get_scaling_factor(self.N_Sc[t, x]) is None:
iscale.set_scaling_factor(self.N_Sc[t, x], 1e-2)
if hasattr(self, "N_Sh"):
for t, x in self.N_Sh.keys():
if iscale.get_scaling_factor(self.N_Sh[t, x]) is None:
iscale.set_scaling_factor(self.N_Sh[t, x], 1e-2)
if hasattr(self, "velocity"):
for v in self.velocity.values():
if iscale.get_scaling_factor(v) is None:
iscale.set_scaling_factor(v, 1)
if hasattr(self, "friction_factor_darcy"):
for v in self.friction_factor_darcy.values():
if iscale.get_scaling_factor(v) is None:
iscale.set_scaling_factor(v, 1)
if hasattr(self, "cp_modulus"):
for v in self.cp_modulus.values():
if iscale.get_scaling_factor(v) is None:
iscale.set_scaling_factor(v, 1)
# helper for validating configuration arguments for this CV
def validate_membrane_config_args(unit):
if (
unit.config.pressure_change_type is not PressureChangeType.fixed_per_stage
and unit.config.has_pressure_change is False
):
raise ConfigurationError(
"\nConflict between configuration options:\n"
"'has_pressure_change' cannot be False "
"while 'pressure_change_type' is set to {}.\n\n"
"'pressure_change_type' must be set to PressureChangeType.fixed_per_stage\nor "
"'has_pressure_change' must be set to True".format(
unit.config.pressure_change_type
)
)
if (
unit.config.concentration_polarization_type
== ConcentrationPolarizationType.calculated
and unit.config.mass_transfer_coefficient == MassTransferCoefficient.none
):
raise ConfigurationError(
"\n'mass_transfer_coefficient' and 'concentration_polarization_type' options configured incorrectly:\n"
"'mass_transfer_coefficient' cannot be set to MassTransferCoefficient.none "
"while 'concentration_polarization_type' is set to ConcentrationPolarizationType.calculated.\n "
"\n\nSet 'mass_transfer_coefficient' to MassTransferCoefficient.fixed or "
"MassTransferCoefficient.calculated "
"\nor set 'concentration_polarization_type' to ConcentrationPolarizationType.fixed or "
"ConcentrationPolarizationType.none"
)
if (
unit.config.concentration_polarization_type
!= ConcentrationPolarizationType.calculated
and unit.config.mass_transfer_coefficient != MassTransferCoefficient.none
):
raise ConfigurationError(
"\nConflict between configuration options:\n"
"'mass_transfer_coefficient' cannot be set to {} "
"while 'concentration_polarization_type' is set to {}.\n\n"
"'mass_transfer_coefficient' must be set to MassTransferCoefficient.none\nor "
"'concentration_polarization_type' must be set to ConcentrationPolarizationType.calculated".format(
unit.config.mass_transfer_coefficient,
unit.config.concentration_polarization_type,
)
)