#################################################################################
# WaterTAP Copyright (c) 2020-2023, The Regents of the University of California,
# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory,
# National Renewable Energy Laboratory, and National Energy Technology
# Laboratory (subject to receipt of any required approvals from the U.S. Dept.
# of Energy). All rights reserved.
#
# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license
# information, respectively. These files are also available online at the URL
# "https://github.com/watertap-org/watertap/"
#################################################################################
# Import Pyomo libraries
from pyomo.environ import Constraint
from pyomo.common.config import Bool, ConfigDict, ConfigValue, ConfigBlock, In
from idaes.core import FlowDirection
from .MD_channel_base import (
ConcentrationPolarizationType,
TemperaturePolarizationType,
MassTransferCoefficient,
PressureChangeType,
FrictionFactor,
)
from idaes.core import (
FlowDirection,
EnergyBalanceType,
MaterialBalanceType,
MomentumBalanceType,
useDefault,
DistributedVars,
)
from idaes.core import declare_process_block_class
from .MD_channel_base import (
TemperaturePolarizationType,
MassTransferCoefficient,
PressureChangeType,
)
from .MD_channel_1D import MDChannel1DBlock
from .membrane_distillation_base import (
MembraneDistillationBaseData,
)
from idaes.core.util.config import is_physical_parameter_block
import idaes.logger as idaeslog
__author__ = "Elmira Shamlou"
# Set up logger
_log = idaeslog.getLogger(__name__)
[docs]@declare_process_block_class("MembraneDistillation1D")
class MembraneDistillationData(MembraneDistillationBaseData):
"""
Standard DCMD Unit Model Class:
- one dimensional model
- steady state only
"""
CONFIG = ConfigBlock()
CONFIG.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.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 = ConfigBlock()
_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(
"flow_direction",
ConfigValue(
default=FlowDirection.forward,
domain=In(FlowDirection),
description="Direction of flow",
doc="""
Options for the direction of flow:
**default** - ``FlowDirection.forward``
.. csv-table::
:header: "Configuration Options", "Description"
"``FlowDirection.forward``", "Flow is in the forward direction"
"``FlowDirection.backward``", "Flow is in the backward direction"
""",
),
)
_CONFIG_Template.declare(
"temperature_polarization_type",
ConfigValue(
default=TemperaturePolarizationType.calculated,
domain=In(TemperaturePolarizationType),
description="External temperature polarization effect",
doc="""
Options to account for temperature polarization.
**default** - ``TemperaturePolarizationType.calculated``
.. csv-table::
:header: "Configuration Options", "Description"
"``TemperaturePolarizationType.none``", "Simplifying assumption to ignore temperature polarization"
"``TemperaturePolarizationType.fixed``", "Specify an estimated value for the temperature polarization modulus"
"``TemperaturePolarizationType.calculated``", "Allow model to perform calculation of membrane-interface temperature"
""",
),
)
_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"
""",
),
)
_CONFIG_Template.declare(
"friction_factor",
ConfigValue(
default=FrictionFactor.flat_sheet,
domain=In(FrictionFactor),
description="Darcy friction factor correlation",
doc="""
Options to account for friction factor correlations.
**default** - ``FrictionFactor.flat_sheet``
.. csv-table::
:header: "Configuration Options", "Description"
"``FrictionFactor.flat_sheet``", "Friction factor correlation for flat-sheet membrane modules"
"``FrictionFactor.spiral_wound``", "Friction factor correlation for spiral-wound membranes"
""",
),
)
_CONFIG_Template.declare(
"property_package_vapor",
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_vapor",
ConfigBlock(
implicit=True,
description="Arguments to use for constructing property packages",
doc="""A ConfigBlock with arguments to be passed to a property block(s)
and used when constructing these,
**default** - None.
**Valid values:** {
see property package for documentation.}""",
),
)
CONFIG.declare("hot_ch", _CONFIG_Template(doc="hot channel config arguments"))
CONFIG.declare("cold_ch", _CONFIG_Template(doc="cold channel config arguments"))
# Common config args for both sides
CONFIG.declare(
"area_definition",
ConfigValue(
default=DistributedVars.uniform,
domain=In(DistributedVars),
description="Argument for defining form of area variable",
doc="""Argument defining whether area variable should be spatially
variant or not. **default** - DistributedVars.uniform.
**Valid values:** {
DistributedVars.uniform - area does not vary across spatial domain,
DistributedVars.variant - area can vary over the domain and is indexed
by time and space.}""",
),
)
CONFIG.declare(
"transformation_method",
ConfigValue(
default=useDefault,
description="Discretization method to use for DAE transformation",
doc="""Discretization method to use for DAE transformation. See
Pyomo documentation for supported transformations.""",
),
)
CONFIG.declare(
"transformation_scheme",
ConfigValue(
default=useDefault,
description="Discretization scheme to use for DAE transformation",
doc="""Discretization scheme to use when transformating domain. See
Pyomo documentation for supported schemes.""",
),
)
CONFIG.declare(
"finite_elements",
ConfigValue(
default=20,
domain=int,
description="Number of finite elements length domain",
doc="""Number of finite elements to use when discretizing length
domain (default=20)""",
),
)
CONFIG.declare(
"collocation_points",
ConfigValue(
default=5,
domain=int,
description="Number of collocation points per finite element",
doc="""Number of collocation points to use per finite element when
discretizing length domain (default=3)""",
),
)
def _make_MD_channel_control_volume(
self, name_ch, common_config, individual_config
):
if self.config.transformation_method is useDefault:
_log.warning(
"Discretization method was "
"not specified for the "
"membrane distillation module. "
"Defaulting to finite "
"difference method."
)
self.config.transformation_method = "dae.finite_difference"
if self.config.transformation_scheme is useDefault:
_log.warning(
"Discretization scheme was "
"not specified for the "
"membrane distillation module."
"Defaulting to backward finite "
"difference."
)
self.config.transformation_scheme = "BACKWARD"
if not isinstance(name_ch, str):
raise TypeError(
f"{name_ch} is not a string. Please provide a string for the name_ch argument."
)
# Build membrane channel control volume
self.add_component(
name_ch,
MDChannel1DBlock(
dynamic=False,
has_holdup=False,
area_definition=common_config.area_definition,
property_package=individual_config.property_package,
property_package_args=individual_config.property_package_args,
transformation_method=common_config.transformation_method,
transformation_scheme=common_config.transformation_scheme,
finite_elements=common_config.finite_elements,
collocation_points=common_config.collocation_points,
),
)
channel = getattr(self, name_ch)
if not hasattr(self, "length") and not hasattr(self, "width"):
self._add_length_and_width()
channel.add_geometry(
flow_direction=individual_config.flow_direction,
length_var=self.length,
width_var=self.width,
)
if not hasattr(self, "eq_area"):
add_eq_area = True
else:
add_eq_area = False
self._add_area(include_constraint=add_eq_area)
def _add_mass_transfer(self):
@self.Constraint(
self.flowsheet().config.time,
self.difference_elements,
self.config.cold_ch.property_package.phase_list,
self.config.cold_ch.property_package.component_list,
doc="Mass transfer from feed to permeate",
)
def eq_connect_mass_transfer(b, t, x, p, j):
if p == "Liq":
return (
b.cold_ch.mass_transfer_term[t, x, p, j]
== -b.hot_ch.mass_transfer_term[t, x, p, j]
)
else:
b.cold_ch.mass_transfer_term[t, x, p, j].fix(0)
return Constraint.Skip
@self.Constraint(
self.flowsheet().config.time,
self.difference_elements,
self.config.hot_ch.property_package.phase_list,
self.config.hot_ch.property_package.component_list,
doc="Permeate production",
)
def eq_permeate_production(b, t, x, p, j):
if j == "H2O":
return (
b.hot_ch.mass_transfer_term[t, x, p, j]
== -b.width * b.flux_mass[t, x]
)
else:
b.hot_ch.mass_transfer_term[t, x, p, j].fix(0)
return Constraint.Skip
def _add_heat_transfer(self):
units_meta = (
self.config.hot_ch.property_package.get_metadata().get_derived_units
)
@self.Constraint(
self.flowsheet().config.time,
self.difference_elements,
doc="Conductive heat transfer to cold channel",
)
def eq_conductive_heat_transfer_hot(b, t, x):
return b.hot_ch.heat[t, x] == -b.width * b.flux_conduction_heat[t, x]
@self.Constraint(
self.flowsheet().config.time,
self.difference_elements,
doc="Conductive heat transfer to cold channel",
)
def eq_conductive_heat_transfer_cold(b, t, x):
return b.cold_ch.heat[t, x] == -b.hot_ch.heat[t, x]
@self.Constraint(
self.flowsheet().config.time,
self.difference_elements,
doc="Enthalpy heat transfer from the hot channel",
)
def eq_enthalpy_transfer_hot(b, t, x):
return b.hot_ch.enthalpy_transfer[t, x] == -b.width * b.flux_enth_hot[t, x]
@self.Constraint(
self.flowsheet().config.time,
self.difference_elements,
doc="Enthalpy heat transfer to the cold channel",
)
def eq_enthalpy_transfer_cold(b, t, x):
return b.cold_ch.enthalpy_transfer[t, x] == b.width * b.flux_enth_cold[t, x]