#################################################################################
# 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/"
#################################################################################
from pyomo.common.config import ConfigValue, In
from pyomo.environ import Set
from idaes.core import (
declare_process_block_class,
DistributedVars,
FlowDirection,
useDefault,
)
from idaes.core.base.control_volume1d import ControlVolume1DBlockData
from idaes.core.util import scaling as iscale
from idaes.core.util.misc import add_object_reference
import idaes.logger as idaeslog
from watertap.core.membrane_channel_base import (
MembraneChannelMixin,
PressureChangeType,
CONFIG_Template as Base_CONFIG_Template,
)
CONFIG_Template = Base_CONFIG_Template()
CONFIG_Template.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_Template.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_Template.declare(
"transformation_scheme",
ConfigValue(
default=useDefault,
description="Discretization scheme to use for DAE transformation",
doc="""Discretization scheme to use when transforming domain. See
Pyomo documentation for supported schemes.""",
),
)
CONFIG_Template.declare(
"finite_elements",
ConfigValue(
default=10,
domain=int,
description="Number of finite elements in length domain",
doc="""Number of finite elements to use when discretizing length
domain (default=10)""",
),
)
CONFIG_Template.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=5)""",
),
)
[docs]@declare_process_block_class("MembraneChannel1DBlock")
class MembraneChannel1DBlockData(MembraneChannelMixin, ControlVolume1DBlockData):
def _skip_element(self, x):
if self.config.transformation_scheme != "FORWARD":
return x == self.length_domain.first()
else:
return x == self.length_domain.last()
[docs] def add_geometry(
self, length_var, width_var, flow_direction=FlowDirection.forward, **kwargs
):
"""
Method to create spatial domain and volume Var in ControlVolume.
Args:
length_var - An external variable to use for the length of
the channel. If a variable is provided, a
reference will be made to this in place of the length
Var.
width_var - An external variable to use for the width of
the channel. If a variable is provided, a
reference will be made to this in place of the length
Var.
flow_direction - argument indicating direction of material flow
relative to length domain. Valid values:
- FlowDirection.forward (default), flow goes
from 0 to 1.
- FlowDirection.backward, flow goes from 1 to 0
length_domain - (optional) a ContinuousSet to use as the length
domain for the ControlVolume. If not provided, a
new ContinuousSet will be created (default=None).
ContinuousSet should be normalized to run between
0 and 1.
length_domain_set - (optional) list of point to use to initialize
a new ContinuousSet if length_domain is not
provided (default = [0.0, 1.0]).
Returns:
None
"""
super().add_geometry(
length_var=length_var, flow_direction=flow_direction, **kwargs
)
self._add_var_reference(width_var, "width", "width_var")
[docs] def add_state_blocks(self, has_phase_equilibrium=None):
"""
This method constructs the state blocks for the
control volume.
Args:
has_phase_equilibrium: indicates whether equilibrium calculations
will be required in state blocks
Returns:
None
"""
super().add_state_blocks(has_phase_equilibrium=has_phase_equilibrium)
self._add_interface_stateblock(has_phase_equilibrium)
def _add_pressure_change(self, pressure_change_type=PressureChangeType.calculated):
add_object_reference(self, "dP_dx", self.deltaP)
[docs] def initialize(
self,
state_args=None,
outlvl=idaeslog.NOTSET,
optarg=None,
solver=None,
hold_state=True,
initialize_guess=None,
):
"""
Initialization routine for the membrane channel control volume
Keyword Arguments:
state_args : a dict of arguments to be passed to the property
package(s) to provide an initial state for
initialization (see documentation of the specific
property package) (default = {}).
outlvl : sets output log level of initialization routine
optarg : solver options dictionary object (default=None, use
default solver options)
solver : str indicating which solver to use during
initialization (default = None)
hold_state : flag indicating whether the initialization routine
should unfix any state variables fixed during
initialization, **default** - True. **Valid values:**
**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.
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})
Returns:
If hold_states is True, returns a dict containing flags for which
states were fixed during initialization.
"""
# Get inlet state if not provided
init_log = idaeslog.getInitLogger(self.name, outlvl, tag="control_volume")
solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="control_volume")
state_args = self._get_state_args(initialize_guess, state_args)
# intialize self.properties
state_args_properties_in = state_args["feed_side"]
if self._flow_direction == FlowDirection.forward:
state_args_properties_out = state_args["retentate"]
else:
state_args_properties_out = state_args["permeate"]
source_flags = super().initialize(
state_args=state_args_properties_in,
outlvl=outlvl,
optarg=optarg,
solver=solver,
hold_state=True,
)
state_args_interface = self._get_state_args_interface(
initialize_guess, state_args_properties_in, state_args_properties_out
)
self.properties_interface.initialize(
outlvl=outlvl,
optarg=optarg,
solver=solver,
state_args=state_args_interface,
)
if hold_state:
return source_flags
else:
self.release_state(source_flags, outlvl)
def calculate_scaling_factors(self):
super().calculate_scaling_factors()
# setting scaling factors for variables
# will not override if the user provides the scaling factor
## default of 1 set by ControlVolume1D
if iscale.get_scaling_factor(self.area) == 1:
iscale.set_scaling_factor(self.area, 100)
if hasattr(self, "pressure_change_total"):
for v in self.pressure_change_total.values():
if iscale.get_scaling_factor(v) is None:
iscale.set_scaling_factor(v, 1e-4)
if hasattr(self, "dP_dx"):
for v in self.pressure_dx.values():
iscale.set_scaling_factor(v, 1e-5)