#################################################################################
# The Institute for the Design of Advanced Energy Systems Integrated Platform
# Framework (IDAES IP) was produced under the DOE Institute for the
# Design of Advanced Energy Systems (IDAES).
#
# Copyright (c) 2018-2024 by the software owners: The Regents of the
# University of California, through Lawrence Berkeley National Laboratory,
# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon
# University, West Virginia University Research Corporation, et al.
# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md
# for full copyright and license information.
#################################################################################
"""
Thickener unit model.
Unit model is derived from:
R. Burger, F. Concha, K.H. Karlsen, A. Narvaez,
Numerical simulation of clarifier-thickener units treating ideal
suspensions with a flux density function having two inflection points,
Mathematical and Computer Modelling 44 (2006) 255–275
doi:10.1016/j.mcm.2005.11.008
Settling velocity function from:
N.G. Barton, C.-H. Li, S.J. Spencer, Control of a surface of discontinuity in continuous thickeners,
Journal of the Australian Mathematical Society Series B 33 (1992) 269–289
"""
# Import Python libraries
import logging
from pandas import DataFrame
# Import Pyomo libraries
from pyomo.environ import Expr_if, inequality, units, Var
from pyomo.common.config import ConfigBlock, ConfigValue, In
from pyomo.network import Port
# Import IDAES cores
from idaes.core import (
declare_process_block_class,
MaterialBalanceType,
MomentumBalanceType,
UnitModelBlockData,
useDefault,
)
from idaes.models.unit_models.separator import (
Separator,
SplittingType,
EnergySplittingType,
)
from idaes.core.initialization import BlockTriangularizationInitializer
from idaes.core.util.config import is_physical_parameter_block
from idaes.core.util.units_of_measurement import report_quantity
from idaes.core.util.constants import Constants as CONST
__author__ = "Andrew Lee"
# Set up logger
logger = logging.getLogger("idaes.unit_model")
[docs]
@declare_process_block_class("Thickener0D")
class Thickener0DData(UnitModelBlockData):
"""
Thickener0D Unit Model Class
"""
CONFIG = ConfigBlock()
CONFIG.declare(
"dynamic",
ConfigValue(
domain=In([False]),
default=False,
description="Dynamic model flag - must be False",
doc="""Indicates whether this model will be dynamic or not,
**default** = False. Flash units do not 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. Flash units do not have defined volume, thus
this must be False.""",
),
)
CONFIG.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** - MaterialBalanceType.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.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.declare(
"energy_split_basis",
ConfigValue(
default=EnergySplittingType.equal_temperature,
domain=EnergySplittingType,
description="Type of constraint to write for energy splitting",
doc="""Argument indicating basis to use for splitting energy this is
not used for when ideal_separation == True.
**default** - EnergySplittingType.equal_temperature.
**Valid values:** {
**EnergySplittingType.equal_temperature** - outlet temperatures equal inlet
**EnergySplittingType.equal_molar_enthalpy** - outlet molar enthalpies equal
inlet,
**EnergySplittingType.enthalpy_split** - apply split fractions to enthalpy
flows.}""",
),
)
CONFIG.declare(
"solid_property_package",
ConfigValue(
default=useDefault,
domain=is_physical_parameter_block,
description="Property package to use for solid phase",
doc="""Property parameter object used to define solid phase property
calculations,
**default** - useDefault.
**Valid values:** {
**useDefault** - use default package from parent model or flowsheet,
**PropertyParameterObject** - a PropertyParameterBlock object.}""",
),
)
CONFIG.declare(
"solid_property_package_args",
ConfigBlock(
implicit=True,
description="Arguments to use for constructing solid phase property packages",
doc="""A ConfigBlock with arguments to be passed to a solid phase property
block(s) and used when constructing these,
**default** - None.
**Valid values:** {
see property package for documentation.}""",
),
)
CONFIG.declare(
"liquid_property_package",
ConfigValue(
default=useDefault,
domain=is_physical_parameter_block,
description="Property package to use for liquid phase",
doc="""Property parameter object used to define liquid phase property
calculations,
**default** - useDefault.
**Valid values:** {
**useDefault** - use default package from parent model or flowsheet,
**PropertyParameterObject** - a PropertyParameterBlock object.}""",
),
)
CONFIG.declare(
"liquid_property_package_args",
ConfigBlock(
implicit=True,
description="Arguments to use for constructing liquid phase property packages",
doc="""A ConfigBlock with arguments to be passed to a liquid phase property
block(s) and used when constructing these,
**default** - None.
**Valid values:** {
see property package for documentation.}""",
),
)
default_initializer = BlockTriangularizationInitializer
[docs]
def build(self):
"""
Begin building model (pre-DAE transformation).
Args:
None
Returns:
None
"""
# Call super().build to setup dynamics
super().build()
# Build Solid Phase
# Setup StateBlock argument dict
tmp_dict = dict(**self.config.solid_property_package_args)
tmp_dict["has_phase_equilibrium"] = False
tmp_dict["defined_state"] = True
self.solid_inlet_state = self.config.solid_property_package.build_state_block(
self.flowsheet().time, doc="Solid properties in separator", **tmp_dict
)
# Add solid splitter
self.solid_split = Separator(
property_package=self.config.solid_property_package,
property_package_args=self.config.solid_property_package_args,
outlet_list=["underflow", "overflow"],
split_basis=SplittingType.totalFlow,
ideal_separation=False,
mixed_state_block=self.solid_inlet_state,
has_phase_equilibrium=False,
material_balance_type=self.config.material_balance_type,
momentum_balance_type=self.config.momentum_balance_type,
energy_split_basis=self.config.energy_split_basis,
)
# Add solid ports
self.add_port(
name="solid_inlet",
block=self.solid_inlet_state,
doc="Solid inlet to thickener",
)
self.solid_underflow = Port(extends=self.solid_split.underflow)
self.solid_overflow = Port(extends=self.solid_split.overflow)
# Build liquid Phase
# Setup StateBlock argument dict
tmp_dict = dict(**self.config.liquid_property_package_args)
tmp_dict["has_phase_equilibrium"] = False
tmp_dict["defined_state"] = True
self.liquid_inlet_state = self.config.liquid_property_package.build_state_block(
self.flowsheet().time, doc="liquid properties in separator", **tmp_dict
)
# Add liquid splitter
self.liquid_split = Separator(
property_package=self.config.liquid_property_package,
property_package_args=self.config.liquid_property_package_args,
outlet_list=["underflow", "overflow"],
split_basis=SplittingType.totalFlow,
ideal_separation=False,
mixed_state_block=self.liquid_inlet_state,
has_phase_equilibrium=False,
material_balance_type=self.config.material_balance_type,
momentum_balance_type=self.config.momentum_balance_type,
energy_split_basis=self.config.energy_split_basis,
)
# Add liquid ports
self.add_port(
name="liquid_inlet",
block=self.liquid_inlet_state,
doc="liquid inlet to thickener",
)
self.liquid_underflow = Port(extends=self.liquid_split.underflow)
self.liquid_overflow = Port(extends=self.liquid_split.overflow)
# Add additional variables and constraints
uom = self.solid_inlet_state.params.get_metadata().derived_units
self.area = Var(
initialize=1,
units=uom.AREA,
doc="Cross sectional area of thickener",
)
# Volumetric Flowrates
self.flow_vol_feed = Var(
self.flowsheet().time,
initialize=0.7,
units=uom.FLOW_VOL,
bounds=(0, None),
doc="Total volumetric flowrate of feed",
)
self.flow_vol_overflow = Var(
self.flowsheet().time,
initialize=0.7,
units=uom.FLOW_VOL,
bounds=(0, None),
doc="Total volumetric flowrate of overflow",
)
self.flow_vol_underflow = Var(
self.flowsheet().time,
initialize=0.7,
units=uom.FLOW_VOL,
bounds=(0, None),
doc="Total volumetric flowrate of underflow",
)
# Solid Fractions
self.solid_fraction_feed = Var(
self.flowsheet().time,
initialize=0.7,
units=units.dimensionless,
bounds=(0, None),
doc="Volume fraction of solids in feed",
)
self.solid_fraction_underflow = Var(
self.flowsheet().time,
initialize=0.7,
units=units.dimensionless,
bounds=(0, None),
doc="Volume fraction of solids in underflow",
)
self.solid_fraction_overflow = Var(
self.flowsheet().time,
initialize=0.7,
units=units.dimensionless,
bounds=(0, None),
doc="Volume fraction of solids in overflow",
)
# Flux densities
self.flux_density_underflow = Var(
self.flowsheet().time,
initialize=0,
units=uom.VELOCITY,
doc="Kynch flux density in underflow",
)
self.flux_density_overflow = Var(
self.flowsheet().time,
initialize=0,
units=uom.VELOCITY,
doc="Kynch flux density in overflow",
)
# Parameters
self.particle_size = Var(
self.flowsheet().time,
initialize=1e-5,
units=uom.LENGTH,
doc="Characteristic length of particle",
)
self.v0 = Var(
self.flowsheet().time,
initialize=1e-4,
units=uom.VELOCITY,
doc="Stokes velocity of individual particle",
)
self.v1 = Var(
initialize=1e-5,
units=uom.VELOCITY,
doc="Superficial velocity of a Darcy type flow of liquid",
)
self.C = Var(
initialize=5,
units=units.dimensionless,
bounds=(0, None),
doc="Settling velocity exponent",
)
self.solid_fraction_max = Var(
initialize=0.9,
units=units.dimensionless,
bounds=(0, 1),
doc="Maximum achievable solids volume fraction",
)
# ---------------------------------------------------------------------------------------------
# Constraints
@self.Constraint(self.flowsheet().time)
def feed_flowrate(b, t):
return b.flow_vol_feed[t] == (
b.solid_inlet_state[t].flow_vol
+ units.convert(b.liquid_inlet_state[t].flow_vol, to_units=uom.FLOW_VOL)
)
@self.Constraint(self.flowsheet().time)
def overflow_flowrate(b, t):
return b.flow_vol_overflow[t] == (
b.solid_split.overflow_state[t].flow_vol
+ units.convert(
b.liquid_split.overflow_state[t].flow_vol, to_units=uom.FLOW_VOL
)
)
@self.Constraint(self.flowsheet().time)
def underflow_flowrate(b, t):
return b.flow_vol_underflow[t] == (
b.solid_split.underflow_state[t].flow_vol
+ units.convert(
b.liquid_split.underflow_state[t].flow_vol, to_units=uom.FLOW_VOL
)
)
# Eqn 2.8 from [1]
@self.Constraint(self.flowsheet().time)
def flux_density_function_overflow(b, t):
u = b.solid_fraction_overflow
return b.flux_density_overflow[t] == Expr_if(
IF=inequality(0, u[t], b.solid_fraction_max),
THEN=b.v0[t] * u[t] * (1 - u[t] / b.solid_fraction_max) ** b.C
+ b.v1 * u[t] ** 2 * (b.solid_fraction_max - u[t]),
ELSE=0 * units.m * units.s**-1,
)
# Eqn 2.8 from [1]
@self.Constraint(self.flowsheet().time)
def flux_density_function_underflow(b, t):
u = b.solid_fraction_underflow
return b.flux_density_underflow[t] == Expr_if(
IF=inequality(0, u[t], b.solid_fraction_max),
THEN=b.v0[t] * u[t] * (1 - u[t] / b.solid_fraction_max) ** b.C
+ b.v1 * u[t] ** 2 * (b.solid_fraction_max - u[t]),
ELSE=0 * units.m * units.s**-1,
)
# Modified from 2.23 and 2.33 from [1]
@self.Constraint(self.flowsheet().time)
def solids_continuity(b, t):
return b.flow_vol_feed[t] * b.solid_fraction_feed[t] == (
b.area * (b.flux_density_overflow[t] + b.flux_density_underflow[t])
- b.flow_vol_overflow[t]
* (b.solid_fraction_overflow[t] - b.solid_fraction_feed[t])
+ b.flow_vol_underflow[t]
* (b.solid_fraction_underflow[t] - b.solid_fraction_feed[t])
)
@self.Constraint(self.flowsheet().time)
def solids_conservation(b, t):
return b.flow_vol_feed[t] * b.solid_fraction_feed[t] == (
+b.flow_vol_overflow[t] * b.solid_fraction_overflow[t]
+ b.flow_vol_underflow[t] * b.solid_fraction_underflow[t]
)
@self.Constraint(self.flowsheet().time)
def maximum_underflow_volume_fraction(b, t):
return b.solid_fraction_underflow[t] <= b.solid_fraction_max
@self.Constraint(self.flowsheet().time)
def maximum_overflow_volume_fraction(b, t):
return b.solid_fraction_overflow[t] <= b.solid_fraction_max
@self.Constraint(self.flowsheet().time)
def inlet_volume_fraction(b, t):
return b.solid_inlet_state[t].flow_vol == (
b.solid_fraction_feed[t]
* (
b.solid_inlet_state[t].flow_vol
+ units.convert(
b.liquid_inlet_state[t].flow_vol, to_units=uom.FLOW_VOL
)
)
)
@self.Constraint(self.flowsheet().time)
def underflow_volume_fraction(b, t):
return b.solid_split.underflow_state[t].flow_vol == (
b.solid_fraction_underflow[t]
* (
b.solid_split.underflow_state[t].flow_vol
+ units.convert(
b.liquid_split.underflow_state[t].flow_vol,
to_units=uom.FLOW_VOL,
)
)
)
@self.Constraint(self.flowsheet().time)
def stokes_law(b, t):
# Assuming constant properties, source from feed states
return 18 * b.v0[t] * units.convert(
b.liquid_inlet_state[t].visc_d, to_units=uom.DYNAMIC_VISCOSITY
) == (
(
b.solid_inlet_state[t].dens_mass
- units.convert(
b.liquid_inlet_state[t].dens_mass, to_units=uom.DENSITY_MASS
)
)
* units.convert(CONST.acceleration_gravity, to_units=uom.ACCELERATION)
* b.particle_size[t] ** 2
)
def _get_performance_contents(self, time_point=0):
return {
"vars": {
"Area": self.area,
"Liquid Recovery": self.liquid_split.split_fraction[
time_point, "overflow"
],
"Feed Solid Fraction": self.solid_fraction_feed[time_point],
"Underflow Solid Fraction": self.solid_fraction_underflow[time_point],
"particle size": self.particle_size[time_point],
"v0": self.v0[time_point],
"v1": self.v1,
"C": self.C,
"solid_fraction_max": self.solid_fraction_max,
},
}
def _get_stream_table_contents(self, time_point=0):
stream_attributes = {}
stream_attributes["Units"] = {}
sblocks = {
"Feed Solid": self.solid_inlet_state,
"Feed Liquid": self.liquid_inlet_state,
"Underflow Solid": self.solid_split.underflow_state,
"Underflow Liquid": self.liquid_split.underflow_state,
"Overflow Solid": self.solid_split.overflow_state,
"Overflow Liquid": self.liquid_split.overflow_state,
}
for n, v in sblocks.items():
dvars = v[time_point].define_display_vars()
stream_attributes[n] = {}
for k in dvars:
for i in dvars[k].keys():
stream_key = k if i is None else f"{k} {i}"
quant = report_quantity(dvars[k][i])
stream_attributes[n][stream_key] = quant.m
stream_attributes["Units"][stream_key] = quant.u
return DataFrame.from_dict(stream_attributes, orient="columns")
[docs]
def initialize(self, **kwargs):
raise NotImplementedError(
"The Thickener0D unit model does not support the old initialization API. "
"Please use the new API (InitializerObjects) instead."
)