#################################################################################
# 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 math
from pyomo.common.config import Bool, ConfigValue
from pyomo.environ import (
NonNegativeReals,
Param,
Suffix,
Var,
Constraint,
check_optimal_termination,
exp,
units as pyunits,
value,
)
from idaes.core import UnitModelBlockData
from watertap.core.solvers import get_solver
from idaes.core.util import scaling as iscale
from idaes.core.util.exceptions import ConfigurationError, InitializationError
from idaes.core.util.misc import add_object_reference
from idaes.core.util.model_statistics import degrees_of_freedom
from idaes.core.util.tables import create_stream_table_dataframe
import idaes.logger as idaeslog
from watertap.core.membrane_channel_base import (
validate_membrane_config_args,
ConcentrationPolarizationType,
TransportModel,
ModuleType,
)
from watertap.core import InitializationMixin
from watertap.costing.unit_models.osmotically_assisted_reverse_osmosis import (
cost_osmotically_assisted_reverse_osmosis,
)
def _add_has_full_reporting(config_obj):
config_obj.declare(
"has_full_reporting",
ConfigValue(
default=False,
domain=Bool,
description="Level of reporting results",
doc="""Level of reporting results.
**default** - False.
**Valid values:** {
**False** - include minimal reporting of results,
**True** - report additional properties of interest that aren't constructed by
the unit model by default. Also, report averaged expression values""",
),
)
[docs]class OsmoticallyAssistedReverseOsmosisBaseData(
InitializationMixin, UnitModelBlockData
):
"""
Osmotically Assisted Reverse Osmosis base class
"""
def _process_config(self):
if len(self.config.property_package.solvent_set) > 1:
raise ConfigurationError(
"Membrane models only support 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.phase_list) > 1 or "Liq" not in [
p for p in self.config.property_package.phase_list
]:
raise ConfigurationError(
"Membrane models only support one liquid phase ['Liq'],"
"the property package has specified the following phases {}".format(
[p for p in self.config.property_package.phase_list]
)
)
[docs] def build(self):
"""
Common variables and constraints for an OARO unit model
"""
super().build()
# Check configuration errors
self._process_config()
# Raise exception if any of configuration arguments are provided incorrectly
validate_membrane_config_args(self)
# --------------------------------------------------------------
# Add feed side and permeate side MembraneChannel Control Volume and setup
self._add_membrane_channels_and_geometry()
self.feed_side.add_state_blocks(has_phase_equilibrium=False)
self.feed_side.add_material_balances(
balance_type=self.config.material_balance_type, has_mass_transfer=True
)
self.feed_side.add_momentum_balances(
balance_type=self.config.momentum_balance_type,
pressure_change_type=self.config.pressure_change_type,
has_pressure_change=self.config.has_pressure_change,
module_type=self.config.module_type,
friction_factor=self.config.friction_factor,
)
# Add constraint for equal temperature between bulk and interface
self.feed_side.add_interface_isothermal_conditions()
# Add constraint for equal temperature between inlet and outlet
self.feed_side.add_control_volume_isothermal_conditions()
# Add constraint for volumetric flow equality between interface and bulk
self.feed_side.add_extensive_flow_to_interface()
# Concentration polarization constraint is not accounted for in the below method; it is
# written later in the base model (eq_concentration_polarization)
self.feed_side.add_concentration_polarization(
concentration_polarization_type=self.config.concentration_polarization_type,
mass_transfer_coefficient=self.config.mass_transfer_coefficient,
)
# Pass in 0D, applied in 1D
self.feed_side.apply_transformation()
self.feed_side.add_expressions()
# --------------------------------------------------------------
# Add permeate side MembraneChannel Control Volume and setup
self.permeate_side.add_state_blocks(has_phase_equilibrium=False)
self.permeate_side.add_material_balances(
balance_type=self.config.material_balance_type, has_mass_transfer=True
)
self.permeate_side.add_momentum_balances(
balance_type=self.config.momentum_balance_type,
pressure_change_type=self.config.pressure_change_type,
has_pressure_change=self.config.has_pressure_change,
module_type=self.config.module_type,
friction_factor=self.config.friction_factor,
)
# Add constraint for equal temperature between bulk and interface
self.permeate_side.add_interface_isothermal_conditions()
# NOTE: We do *not* add a constraint for equal temperature
# between inlet and outlet, that is handled by
# eq_permeate_isothermal below and checks in initialization
# Add constraint for volumetric flow equality between interface and bulk
self.permeate_side.add_extensive_flow_to_interface()
# Concentration polarization constraint is not accounted for in the below method; it is
# written later in the base model (eq_concentration_polarization)
self.permeate_side.add_concentration_polarization(
concentration_polarization_type=self.config.concentration_polarization_type,
mass_transfer_coefficient=self.config.mass_transfer_coefficient,
)
# Pass in 0D, applied in 1D
self.permeate_side.apply_transformation()
self.permeate_side.add_expressions()
add_object_reference(self, "length_domain", self.feed_side.length_domain)
add_object_reference(
self, "difference_elements", self.feed_side.difference_elements
)
add_object_reference(self, "first_element", self.feed_side.first_element)
add_object_reference(self, "nfe", self.feed_side.nfe)
# Add Ports
self.add_inlet_port(name="feed_inlet", block=self.feed_side)
self.add_outlet_port(name="feed_outlet", block=self.feed_side)
self.add_inlet_port(name="permeate_inlet", block=self.permeate_side)
self.add_outlet_port(name="permeate_outlet", block=self.permeate_side)
# # ==========================================================================
# Feed and permeate-side isothermal conditions
@self.Constraint(
self.flowsheet().config.time,
self.length_domain,
doc="Isothermal assumption for permeate",
)
def eq_permeate_isothermal(b, t, x):
if x == self.length_domain.last():
return Constraint.Skip
return (
b.permeate_side.properties[t, x].temperature
== 0.5
* b.feed_side.properties[
t, b.feed_side.length_domain.first()
].temperature
+ 0.5
* b.permeate_side.properties[
t, b.permeate_side.length_domain.last()
].temperature
)
# ==========================================================================
# Volumetric Recovery rate
self.recovery_vol_phase = Var(
self.flowsheet().config.time,
self.config.property_package.phase_list,
initialize=0.4,
bounds=(1e-2, 1 - 1e-6),
units=pyunits.dimensionless,
doc="Volumetric recovery rate",
)
# ==========================================================================
@self.Constraint(self.flowsheet().config.time)
def eq_recovery_vol_phase(b, t):
return (
b.recovery_vol_phase[t, "Liq"]
== (
b.permeate_side.properties[t, b.first_element].flow_vol_phase["Liq"]
- b.permeate_side.properties[
t, b.permeate_side.length_domain.last()
].flow_vol_phase["Liq"]
)
/ b.feed_side.properties[t, b.first_element].flow_vol_phase["Liq"]
)
solvent_set = self.config.property_package.solvent_set
solute_set = self.config.property_package.solute_set
self.recovery_mass_phase_comp = Var(
self.flowsheet().config.time,
self.config.property_package.phase_list,
self.config.property_package.component_list,
initialize=lambda b, t, p, j: 0.4037 if j in solvent_set else 0.0033,
bounds=lambda b, t, p, j: (
(0, 1 - 1e-6) if j in solvent_set else (1e-5, 1 - 1e-6)
),
units=pyunits.dimensionless,
doc="Mass-based component recovery",
)
self.rejection_phase_comp = Var(
self.flowsheet().config.time,
self.config.property_package.phase_list,
solute_set,
initialize=0.9,
bounds=(0, 1 - 1e-6),
units=pyunits.dimensionless,
doc="Observed solute rejection",
)
# ==========================================================================
# Mass-based Component Recovery rate
@self.Constraint(
self.flowsheet().config.time, self.config.property_package.component_list
)
def eq_recovery_mass_phase_comp(b, t, j):
return (
b.recovery_mass_phase_comp[t, "Liq", j]
* b.feed_side.properties[t, b.first_element].flow_mass_phase_comp[
"Liq", j
]
== b.permeate_side.properties[t, b.first_element].flow_mass_phase_comp[
"Liq", j
]
- b.permeate_side.properties[
t, b.permeate_side.length_domain.last()
].flow_mass_phase_comp["Liq", j]
)
# rejection
# TODO: consider importance of rejection in OARO; for now,
# using outlet permeate concentration in the constraint; could calculate
# as a function of length but need to decide on whether that's overkill
@self.Constraint(self.flowsheet().config.time, solute_set)
def eq_rejection_phase_comp(b, t, j):
return b.rejection_phase_comp[t, "Liq", j] == 1 - (
b.permeate_side.properties[t, b.first_element].conc_mass_phase_comp[
"Liq", j
]
/ b.feed_side.properties[t, b.first_element].conc_mass_phase_comp[
"Liq", j
]
)
self._add_flux_balance()
if self.config.has_pressure_change:
self._add_deltaP(side="feed_side")
self._add_deltaP(side="permeate_side")
self._add_mass_transfer()
self.scaling_factor = Suffix(direction=Suffix.EXPORT)
def _add_mass_transfer(self):
raise NotImplementedError()
def _add_membrane_channels_and_geometry(self):
raise NotImplementedError()
def _add_length_and_width(self):
units_meta = self.config.property_package.get_metadata().get_derived_units
self.length = Var(
initialize=10,
bounds=(0.1, 5e2),
domain=NonNegativeReals,
units=units_meta("length"),
doc="Effective membrane length",
)
self.width = Var(
initialize=1,
bounds=(1e-1, 1e3),
domain=NonNegativeReals,
units=units_meta("length"),
doc="Membrane width",
)
def _add_area(self, include_constraint=True):
units_meta = self.config.property_package.get_metadata().get_derived_units
if not hasattr(self, "area"):
self.area = Var(
initialize=10,
bounds=(1e-1, 1e5),
domain=NonNegativeReals,
units=units_meta("length") ** 2,
doc="Total Membrane area",
)
if include_constraint:
if not hasattr(self, "eq_area"):
if self.config.module_type == ModuleType.flat_sheet:
# Membrane area equation for flat plate membranes
@self.Constraint(doc="Total Membrane area")
def eq_area(b):
return b.area == b.length * b.width
elif self.config.module_type == ModuleType.spiral_wound:
# Membrane area equation
@self.Constraint(doc="Total Membrane area")
def eq_area(b):
return b.area == b.length * 2 * b.width
else:
raise ConfigurationError(
"Unsupported membrane module type: {}".format(
self.config.module_type
)
)
def _add_flux_balance(self):
solvent_set = self.config.property_package.solvent_set
solute_set = self.config.property_package.solute_set
units_meta = self.config.property_package.get_metadata().get_derived_units
self.A_comp = Var(
self.flowsheet().config.time,
solvent_set,
initialize=1e-12,
bounds=(1e-18, 1e-6),
domain=NonNegativeReals,
units=units_meta("length")
* units_meta("pressure") ** -1
* units_meta("time") ** -1,
doc="Solvent permeability coeff.",
)
self.B_comp = Var(
self.flowsheet().config.time,
solute_set,
initialize=1e-8,
bounds=(1e-11, 1e-5),
domain=NonNegativeReals,
units=units_meta("length") * units_meta("time") ** -1,
doc="Solute permeability coeff.",
)
# TODO: add water density to NaCl prop model and remove here (or use IDAES version)
self.dens_solvent = Param(
initialize=1000,
units=units_meta("mass") * units_meta("length") ** -3,
doc="Pure water density",
)
if self.config.transport_model == TransportModel.SKK:
self.reflect_coeff = Var(
initialize=0.9,
domain=NonNegativeReals,
units=pyunits.dimensionless,
doc="Reflection coefficient of the membrane",
)
self.alpha = Var(
initialize=1e8,
domain=NonNegativeReals,
units=units_meta("time") * units_meta("length") ** -1,
doc="Alpha coefficient of the membrane",
)
self.flux_mass_phase_comp = Var(
self.flowsheet().config.time,
self.difference_elements,
self.config.property_package.phase_list,
self.config.property_package.component_list,
initialize=lambda b, t, x, p, j: 5e-4 if j in solvent_set else 1e-6,
bounds=lambda b, t, x, p, j: (0, 3e-2) if j in solvent_set else (0, 1e-3),
units=units_meta("mass")
* units_meta("length") ** -2
* units_meta("time") ** -1,
doc="Mass flux across membrane at inlet and outlet",
)
if self.config.transport_model == TransportModel.SD:
@self.Constraint(
self.flowsheet().config.time,
self.difference_elements,
self.config.property_package.phase_list,
self.config.property_package.component_list,
doc="Solvent and solute mass flux",
)
def eq_flux_mass(b, t, x, p, j):
prop_feed = b.feed_side.properties[t, x]
prop_perm = b.permeate_side.properties[t, x]
interface_feed = b.feed_side.properties_interface[t, x]
interface_perm = b.permeate_side.properties_interface[t, x]
comp = self.config.property_package.get_component(j)
if comp.is_solvent():
return b.flux_mass_phase_comp[t, x, p, j] == b.A_comp[
t, j
] * b.dens_solvent * (
(prop_feed.pressure - prop_perm.pressure)
- (
interface_feed.pressure_osm_phase[p]
- interface_perm.pressure_osm_phase[p]
)
)
elif comp.is_solute():
return b.flux_mass_phase_comp[t, x, p, j] == b.B_comp[t, j] * (
interface_feed.conc_mass_phase_comp[p, j]
- interface_perm.conc_mass_phase_comp[p, j]
)
elif self.config.transport_model == TransportModel.SKK:
@self.Constraint(
self.flowsheet().config.time, solute_set, doc="SKK alpha coeff."
)
def eq_alpha(b, t, j):
return b.alpha == (1 - b.reflect_coeff) / b.B_comp[t, j]
@self.Constraint(
self.flowsheet().config.time,
self.difference_elements,
self.config.property_package.phase_list,
self.config.property_package.component_list,
doc="Solvent and solute mass flux using SKK model",
)
def eq_flux_mass(b, t, x, p, j):
prop_feed = b.feed_side.properties[t, x]
prop_perm = b.permeate_side.properties[t, x]
interface_feed = b.feed_side.properties_interface[t, x]
interface_perm = b.permeate_side.properties_interface[t, x]
comp = self.config.property_package.get_component(j)
if comp.is_solvent():
return b.flux_mass_phase_comp[t, x, p, j] == b.A_comp[
t, j
] * b.dens_solvent * (
(prop_feed.pressure - prop_perm.pressure)
- b.reflect_coeff
* (
interface_feed.pressure_osm_phase[p]
- interface_perm.pressure_osm_phase[p]
)
)
elif comp.is_solute():
return b.flux_mass_phase_comp[t, x, p, j] == b.B_comp[t, j] * (
interface_feed.conc_mass_phase_comp[p, j]
- interface_perm.conc_mass_phase_comp[p, j]
) + (1 - b.reflect_coeff) * (
(
(b.flux_mass_phase_comp[t, x, p, "H2O"] / b.dens_solvent)
* interface_feed.conc_mass_phase_comp[p, j]
)
)
else:
raise ConfigurationError(
"Unsupported transport model: {}".format(self.config.transport_model)
)
@self.Expression(
self.flowsheet().config.time,
self.config.property_package.phase_list,
self.config.property_package.component_list,
doc="Average flux expression",
)
def flux_mass_phase_comp_avg(b, t, p, j):
return (
sum(
b.flux_mass_phase_comp[t, x, p, j] for x in self.difference_elements
)
/ self.nfe
)
if (
self.config.concentration_polarization_type
== ConcentrationPolarizationType.calculated
):
@self.Constraint(
self.flowsheet().config.time,
self.difference_elements,
solute_set,
doc="External Concentration polarization in feed side",
)
def eq_concentration_polarization_feed(b, t, x, j):
jw = b.flux_mass_phase_comp[t, x, "Liq", "H2O"] / self.dens_solvent
js = b.flux_mass_phase_comp[t, x, "Liq", j]
exponent = jw / self.feed_side.K[t, x, j]
return b.feed_side.properties_interface[t, x].conc_mass_phase_comp[
"Liq", j
] == b.feed_side.properties[t, x].conc_mass_phase_comp["Liq", j] * exp(
exponent
) - js / jw * (
exp(exponent) - 1
)
self.structural_parameter = Var(
initialize=1200e-6,
units=pyunits.m,
domain=NonNegativeReals,
doc="Membrane structural parameter (i.e., effective thickness)",
)
@self.Constraint(
self.flowsheet().config.time,
self.difference_elements,
solute_set,
doc="Internal Concentration polarization in permeate side",
)
def eq_concentration_polarization_permeate(b, t, x, j):
jw = b.flux_mass_phase_comp[t, x, "Liq", "H2O"] / self.dens_solvent
js = b.flux_mass_phase_comp[t, x, "Liq", j]
exponent = -jw * (
b.structural_parameter
/ b.permeate_side.properties[t, x].diffus_phase_comp["Liq", j]
+ 1 / b.permeate_side.K[t, x, j]
)
return b.permeate_side.properties_interface[t, x].conc_mass_phase_comp[
"Liq", j
] == b.permeate_side.properties[t, x].conc_mass_phase_comp[
"Liq", j
] * exp(
exponent
) - js / jw * (
exp(exponent) - 1
)
return self.eq_flux_mass
[docs] def initialize_build(
self,
initialize_guess=None,
state_args_feed=None,
state_args_permeate=None,
outlvl=idaeslog.NOTSET,
solver=None,
optarg=None,
raise_on_isothermal_violation=True,
):
"""
General wrapper for RO initialization routines
Keyword 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_feed : 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) (default = None).
state_args_permeate : a dict of arguments to be passed to the property
package(s) to provide an initial state for the inlet
permeate side state block (see documentation of the specific
property package) (default = None).
outlvl : sets output level of initialization routine
optarg : solver options dictionary object (default=None)
solver : solver object or string indicating which solver to use during
initialization, if None provided the default solver will be used
(default = None)
Returns:
None
"""
init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit")
solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit")
for t in self.flowsheet().config.time:
if not math.isclose(
value(self.feed_inlet.temperature[t]),
value(self.permeate_inlet.temperature[t]),
):
msg = f"Feed temperatures are different at time {t}, but OARO makes isothermal assumption"
if raise_on_isothermal_violation:
raise InitializationError(msg)
else:
init_log.warning(msg)
# set them equal
self.permeate_inlet.temperature[t].set_value(
value(self.feed_inlet.temperature[t])
)
feed_flags = self.feed_side.initialize(
state_args=state_args_feed,
outlvl=outlvl,
optarg=optarg,
solver=solver,
initialize_guess=initialize_guess,
)
init_log.info_high("Initialization Step 1a (feed side) Complete")
permeate_flags = self.permeate_side.initialize(
state_args=state_args_permeate,
outlvl=outlvl,
optarg=optarg,
solver=solver,
initialize_guess=initialize_guess,
)
init_log.info_high("Initialization Step 1b (permeate side) Complete")
if degrees_of_freedom(self) != 0:
# TODO: should we have a separate error for DoF?
raise Exception(
f"{self.name} degrees of freedom were not 0 at the beginning "
f"of initialization. DoF = {degrees_of_freedom(self)}"
)
# Create solver
opt = get_solver(solver, optarg)
# Solve unit *without* flux equation
self.eq_flux_mass.deactivate()
with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
res = opt.solve(self, tee=slc.tee)
init_log.info_high(f"Initialization Step 2 {idaeslog.condition(res)}")
# Solve unit *with* flux equation
self.eq_flux_mass.activate()
with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
res = opt.solve(self, tee=slc.tee)
init_log.info_high(f"Initialization Step 3 {idaeslog.condition(res)}")
# release inlet state, in case this error is caught
self.permeate_side.release_state(permeate_flags, outlvl)
self.feed_side.release_state(feed_flags, outlvl)
init_log.info(f"Initialization Complete: {idaeslog.condition(res)}")
if not check_optimal_termination(res):
raise InitializationError(f"Unit model {self.name} failed to initialize")
def _get_stream_table_contents(self, time_point=0):
return create_stream_table_dataframe(
{
"Feed Inlet": self.feed_inlet,
"Feed Outlet": self.feed_outlet,
"Permeate Inlet": self.permeate_inlet,
"Permeate Outlet": self.permeate_outlet,
},
time_point=time_point,
)
# TODO: sort out first/last element for feed/permeate
def _get_performance_contents(self, time_point=0):
x_in = self.first_element
x_interface_in = self.difference_elements.first()
x_out = self.length_domain.last()
feed_inlet = self.feed_side.properties[time_point, x_in]
feed_outlet = self.feed_side.properties[time_point, x_out]
feed_interface_inlet = self.feed_side.properties_interface[
time_point, x_interface_in
]
feed_interface_outlet = self.feed_side.properties_interface[time_point, x_out]
permeate_inlet = self.permeate_side.properties[time_point, x_in]
permeate_outlet = self.permeate_side.properties[time_point, x_out]
permeate_interface_inlet = self.permeate_side.properties_interface[
time_point, x_interface_in
]
permeate_interface_outlet = self.permeate_side.properties_interface[
time_point, x_out
]
var_dict = {}
expr_dict = {}
var_dict["Volumetric Recovery Rate"] = self.recovery_vol_phase[
time_point, "Liq"
]
var_dict["Solvent Mass Recovery Rate"] = self.recovery_mass_phase_comp[
time_point, "Liq", "H2O"
]
var_dict["Membrane Area"] = self.area
if hasattr(self, "length") and self.config.has_full_reporting:
var_dict["Membrane Length"] = self.length
if hasattr(self, "width") and self.config.has_full_reporting:
var_dict["Membrane Width"] = self.width
if hasattr(self, "deltaP") and self.config.has_full_reporting:
var_dict["Pressure Change"] = self.deltaP[time_point]
if hasattr(self.feed_side, "N_Re") and self.config.has_full_reporting:
var_dict["Reynolds Number @Inlet"] = self.feed_side.N_Re[time_point, x_in]
var_dict["Reynolds Number @Outlet"] = self.feed_side.N_Re[time_point, x_out]
if hasattr(self.feed_side, "velocity") and self.config.has_full_reporting:
var_dict["Velocity @Inlet"] = self.feed_side.velocity[time_point, x_in]
var_dict["Velocity @Outlet"] = self.feed_side.velocity[time_point, x_out]
for j in self.config.property_package.solute_set:
if (
feed_interface_inlet.is_property_constructed("conc_mass_phase_comp")
and self.config.has_full_reporting
):
var_dict[f"{j} Feed Concentration @Inlet,Membrane-Interface "] = (
feed_interface_inlet.conc_mass_phase_comp["Liq", j]
)
if (
feed_interface_outlet.is_property_constructed("conc_mass_phase_comp")
and self.config.has_full_reporting
):
var_dict[f"{j} Feed Concentration @Outlet,Membrane-Interface "] = (
feed_interface_outlet.conc_mass_phase_comp["Liq", j]
)
if (
feed_inlet.is_property_constructed("conc_mass_phase_comp")
and self.config.has_full_reporting
):
var_dict[f"{j} Feed Concentration @Inlet,Bulk"] = (
feed_inlet.conc_mass_phase_comp["Liq", j]
)
if (
feed_outlet.is_property_constructed("conc_mass_phase_comp")
and self.config.has_full_reporting
):
var_dict[f"{j} Feed Concentration @Outlet,Bulk"] = (
feed_outlet.conc_mass_phase_comp["Liq", j]
)
if (
permeate_interface_inlet.is_property_constructed("conc_mass_phase_comp")
and self.config.has_full_reporting
):
var_dict[f"{j} Permeate Concentration @Inlet,Membrane-Interface "] = (
permeate_interface_inlet.conc_mass_phase_comp["Liq", j]
)
if (
permeate_interface_outlet.is_property_constructed(
"conc_mass_phase_comp"
)
and self.config.has_full_reporting
):
var_dict[f"{j} Permeate Concentration @Outlet,Membrane-Interface "] = (
permeate_interface_outlet.conc_mass_phase_comp["Liq", j]
)
if (
permeate_inlet.is_property_constructed("conc_mass_phase_comp")
and self.config.has_full_reporting
):
var_dict[f"{j} Permeate Concentration @Inlet,Bulk"] = (
permeate_inlet.conc_mass_phase_comp["Liq", j]
)
if (
permeate_outlet.is_property_constructed("conc_mass_phase_comp")
and self.config.has_full_reporting
):
var_dict[f"{j} Permeate Concentration @Outlet,Bulk"] = (
permeate_outlet.conc_mass_phase_comp["Liq", j]
)
if (
feed_interface_outlet.is_property_constructed("pressure_osm_phase")
and self.config.has_full_reporting
):
var_dict["Feed Osmotic Pressure @Outlet,Membrane-Interface "] = (
feed_interface_outlet.pressure_osm_phase["Liq"]
)
if (
permeate_outlet.is_property_constructed("pressure_osm_phase")
and self.config.has_full_reporting
):
var_dict["Feed Osmotic Pressure @Outlet,Bulk"] = (
feed_outlet.pressure_osm_phase["Liq"]
)
if (
feed_interface_inlet.is_property_constructed("pressure_osm_phase")
and self.config.has_full_reporting
):
var_dict["Feed Osmotic Pressure @Inlet,Membrane-Interface"] = (
feed_interface_inlet.pressure_osm_phase["Liq"]
)
if (
feed_inlet.is_property_constructed("pressure_osm_phase")
and self.config.has_full_reporting
):
var_dict["Feed Osmotic Pressure @Inlet,Bulk"] = (
feed_inlet.pressure_osm_phase["Liq"]
)
# TODO: add all corresponding values for permeate side for relevant
# vars/expressions from osmotic pressure and whatever is below
if (
feed_inlet.is_property_constructed("flow_vol_phase")
and self.config.has_full_reporting
):
var_dict["Feed Volumetric Flowrate @Inlet"] = feed_inlet.flow_vol_phase[
"Liq"
]
if (
feed_outlet.is_property_constructed("flow_vol_phase")
and self.config.has_full_reporting
):
var_dict["Feed Volumetric Flowrate @Outlet"] = feed_outlet.flow_vol_phase[
"Liq"
]
if hasattr(self.feed_side, "dh") and self.config.has_full_reporting:
var_dict["Feed-side Hydraulic Diameter"] = self.feed_side.dh
if self.config.has_full_reporting:
expr_dict["Average Solvent Flux (LMH)"] = (
self.flux_mass_phase_comp_avg[time_point, "Liq", "H2O"] * 3.6e3
)
if hasattr(self.feed_side, "N_Re_avg"):
expr_dict["Average Feed-side Reynolds Number"] = (
self.feed_side.N_Re_avg[time_point]
)
for j in self.config.property_package.solute_set:
expr_dict[f"{j} Average Solute Flux (GMH)"] = (
self.flux_mass_phase_comp_avg[time_point, "Liq", j] * 3.6e6
)
if hasattr(self.feed_side, "K_avg"):
expr_dict[
f"{j} Average Feed-side Mass Transfer Coefficient (mm/h)"
] = (self.feed_side.K_avg[time_point, j] * 3.6e6)
# TODO: add more vars
return {"vars": var_dict, "exprs": expr_dict}
def calculate_scaling_factors(self):
if iscale.get_scaling_factor(self.dens_solvent) is None:
sf = iscale.get_scaling_factor(
self.feed_side.properties[0, self.first_element].dens_mass_phase["Liq"]
)
iscale.set_scaling_factor(self.dens_solvent, sf)
super().calculate_scaling_factors()
# these variables should have user input, if not there will be a warning
if iscale.get_scaling_factor(self.area) is None:
sf = iscale.get_scaling_factor(self.area, default=10, warning=True)
iscale.set_scaling_factor(self.area, sf)
if iscale.get_scaling_factor(self.A_comp) is None:
iscale.set_scaling_factor(self.A_comp, 1e12)
if iscale.get_scaling_factor(self.B_comp) is None:
iscale.set_scaling_factor(self.B_comp, 1e8)
if iscale.get_scaling_factor(self.recovery_vol_phase) is None:
iscale.set_scaling_factor(self.recovery_vol_phase, 1)
if self.config.transport_model == TransportModel.SKK:
if iscale.get_scaling_factor(self.alpha) is None:
iscale.set_scaling_factor(self.alpha, 1e-8)
if iscale.get_scaling_factor(self.reflect_coeff) is None:
iscale.set_scaling_factor(self.reflect_coeff, 1)
for (t, p, j), v in self.recovery_mass_phase_comp.items():
if j in self.config.property_package.solvent_set:
sf = 1
elif j in self.config.property_package.solute_set:
sf = 100
if iscale.get_scaling_factor(v) is None:
iscale.set_scaling_factor(v, sf)
for v in self.rejection_phase_comp.values():
if iscale.get_scaling_factor(v) is None:
iscale.set_scaling_factor(v, 1)
if hasattr(self, "length"):
if iscale.get_scaling_factor(self.length) is None:
iscale.set_scaling_factor(self.length, 1)
if hasattr(self, "width"):
if iscale.get_scaling_factor(self.width) is None:
iscale.set_scaling_factor(self.width, 1)
if hasattr(self, "structural_parameter"):
if iscale.get_scaling_factor(self.structural_parameter) is None:
# Structural parameter expected to be ~ 1200 microns
iscale.set_scaling_factor(self.structural_parameter, 1e3)
for (t, x, p, j), v in self.flux_mass_phase_comp.items():
if iscale.get_scaling_factor(v) is None:
comp = self.config.property_package.get_component(j)
if comp.is_solvent(): # scaling based on solvent flux equation
sf = (
iscale.get_scaling_factor(self.A_comp[t, j])
* iscale.get_scaling_factor(self.dens_solvent)
* iscale.get_scaling_factor(
self.feed_side.properties[t, x].pressure
)
)
iscale.set_scaling_factor(v, sf)
elif comp.is_solute(): # scaling based on solute flux equation
sf = iscale.get_scaling_factor(
self.B_comp[t, j]
) * iscale.get_scaling_factor(
self.feed_side.properties[t, x].conc_mass_phase_comp[p, j]
)
iscale.set_scaling_factor(v, sf)
sf = iscale.get_scaling_factor(v)
iscale.constraint_scaling_transform(self.eq_flux_mass[t, x, p, j], sf)
@property
def default_costing_method(self):
return cost_osmotically_assisted_reverse_osmosis