From 2715c9d3c14307f57c80b226a4aa863779aed1ed Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Tue, 12 Dec 2023 11:21:00 -0500 Subject: [PATCH 01/10] Adding beta warning --- .../solid_liquid/tests/test_thickener.py | 20 +++++++++++++++++++ .../unit_models/solid_liquid/thickener.py | 5 +++++ 2 files changed, 25 insertions(+) diff --git a/idaes/models/unit_models/solid_liquid/tests/test_thickener.py b/idaes/models/unit_models/solid_liquid/tests/test_thickener.py index 91716ea608..96d5225de6 100644 --- a/idaes/models/unit_models/solid_liquid/tests/test_thickener.py +++ b/idaes/models/unit_models/solid_liquid/tests/test_thickener.py @@ -155,6 +155,26 @@ def get_material_flow_basis(b): # ----------------------------------------------------------------------------- +@pytest.mark.unit +def test_beta_logger(caplog): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.properties = TestParameterBlock() + + m.fs.unit = Thickener0D( + solid_property_package=m.fs.properties, + liquid_property_package=m.fs.properties, + ) + expected = ( + "The Thickener0D model is currently a beta capability and will " + "likely change in the next release as a more predictive version is " + "developed." + ) + + assert expected in caplog.text + + class TestThickener0DBasic: @pytest.fixture(scope="class") def model(self): diff --git a/idaes/models/unit_models/solid_liquid/thickener.py b/idaes/models/unit_models/solid_liquid/thickener.py index 66097380fa..3d86aa204a 100644 --- a/idaes/models/unit_models/solid_liquid/thickener.py +++ b/idaes/models/unit_models/solid_liquid/thickener.py @@ -58,6 +58,11 @@ def build(self): Returns: None """ + logger.warning( + "The Thickener0D model is currently a beta capability and will " + "likely change in the next release as a more predictive version is " + "developed." + ) # Call super().build to setup dynamics super().build() From 43679400231dbefe8fd4349f92e45d5b5a5a00ad Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Tue, 12 Dec 2023 11:25:17 -0500 Subject: [PATCH 02/10] Adding beta warning to docs --- .../generic/unit_models/solid_liquid/thickener0d.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/reference_guides/model_libraries/generic/unit_models/solid_liquid/thickener0d.rst b/docs/reference_guides/model_libraries/generic/unit_models/solid_liquid/thickener0d.rst index aae5e2ed92..d04a479be5 100644 --- a/docs/reference_guides/model_libraries/generic/unit_models/solid_liquid/thickener0d.rst +++ b/docs/reference_guides/model_libraries/generic/unit_models/solid_liquid/thickener0d.rst @@ -1,6 +1,9 @@ Thickener (0D) ============== +.. warning:: + The Thickener model is currently in beta status and will likely change in the next release as a more predictive version is developed. + The ``Thickener0D`` unit model is an extension of the :ref:`SLSeparator ` model which adds constraints to estimate the area and height of a vessel required to achieve the desired separation of solid and liquid based on experimental measurements of the settling velocity. This model is based on correlations described in: [1] Coulson & Richardson's Chemical Engineering, Volume 2 Particle Technology & Separation Processes (4th Ed.), Butterworth-Heinemann (2001) From 6653e3332ae9f6844b0d336752452623b25f6f42 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Tue, 12 Dec 2023 14:52:00 -0500 Subject: [PATCH 03/10] Updating to predicitve thickener --- .../solid_liquid/tests/test_thickener.py | 453 +++++++++--------- .../unit_models/solid_liquid/thickener.py | 427 +++++++++++++---- 2 files changed, 583 insertions(+), 297 deletions(-) diff --git a/idaes/models/unit_models/solid_liquid/tests/test_thickener.py b/idaes/models/unit_models/solid_liquid/tests/test_thickener.py index 96d5225de6..f5a03cd75e 100644 --- a/idaes/models/unit_models/solid_liquid/tests/test_thickener.py +++ b/idaes/models/unit_models/solid_liquid/tests/test_thickener.py @@ -14,7 +14,7 @@ Tests for thickener unit model. Authors: Andrew Lee """ - +from math import isnan import pytest from pyomo.environ import ( @@ -27,7 +27,6 @@ Var, ) from pyomo.network import Port -from pyomo.util.check_units import assert_units_consistent, assert_units_equivalent from idaes.core import ( FlowsheetBlock, @@ -39,20 +38,10 @@ StateBlockData, StateBlock, LiquidPhase, + SolidPhase, Component, ) from idaes.models.unit_models.solid_liquid import Thickener0D -from idaes.models.unit_models.separator import ( - Separator, - SeparatorData, - EnergySplittingType, -) -from idaes.core.util.model_statistics import ( - degrees_of_freedom, - number_variables, - number_total_constraints, - number_unused_variables, -) from idaes.core.solvers import get_solver from idaes.core.initialization import ( BlockTriangularizationInitializer, @@ -65,18 +54,18 @@ solver = get_solver() -@declare_process_block_class("TestParameterBlock") -class TestParameterData(PhysicalParameterBlock): +@declare_process_block_class("SolidParameterBlock") +class SolidParameterData(PhysicalParameterBlock): def build(self): """ Callable method for Block construction. """ super().build() - self._state_block_class = TestStateBlock + self._state_block_class = SolidStateBlock # Add Phase objects - self.Liq = LiquidPhase() + self.sol = SolidPhase() # Add Component objects self.a = Component() @@ -95,8 +84,8 @@ def define_metadata(cls, obj): ) -@declare_process_block_class("TestStateBlock", block_class=StateBlock) -class TestStateBlockData(StateBlockData): +@declare_process_block_class("SolidStateBlock", block_class=StateBlock) +class SolidStateBlockData(StateBlockData): def build(self): super().build() @@ -105,6 +94,10 @@ def build(self): initialize=1.0, units=units.kg / units.s, ) + self.flow_vol = Var( + initialize=1.0, + units=units.m**3 / units.s, + ) self.pressure = Var( initialize=101325.0, bounds=(1e3, 1e6), @@ -127,10 +120,14 @@ def build(self): ) if self.config.defined_state is False: - self.summ_mass_frac_eqn = Constraint( + self.sum_mass_frac_eqn = Constraint( expr=sum(self.mass_frac_comp[j] for j in self.component_list) == 1 ) + self.volumetric_flow = Constraint( + expr=self.flow_vol * 2400 * units.kg * units.m**-3 == self.flow_mass + ) + def get_material_flow_terms(b, p, j): return b.flow_mass * b.mass_frac_comp[j] @@ -154,166 +151,145 @@ def get_material_flow_basis(b): return MaterialFlowBasis.mass -# ----------------------------------------------------------------------------- -@pytest.mark.unit -def test_beta_logger(caplog): - m = ConcreteModel() - m.fs = FlowsheetBlock(dynamic=False) +@declare_process_block_class("LiquidParameterBlock") +class LiquidParameterData(PhysicalParameterBlock): + def build(self): + """ + Callable method for Block construction. + """ + super().build() + + self._state_block_class = LiquidStateBlock + + # Add Phase objects + self.liq = LiquidPhase() + + # Add Component objects + self.c = Component() + self.d = Component() + + @classmethod + def define_metadata(cls, obj): + obj.add_default_units( + { + "time": units.s, + "length": units.m, + "mass": units.kg, + "amount": units.mol, + "temperature": units.K, + } + ) + + +@declare_process_block_class("LiquidStateBlock", block_class=StateBlock) +class LiquidStateBlockData(StateBlockData): + def build(self): + super().build() + + # Create state variables + self.flow_mass = Var( + initialize=1.0, + units=units.kg / units.s, + ) + self.flow_vol = Var( + initialize=1.0, + units=units.m**3 / units.s, + ) + self.pressure = Var( + initialize=101325.0, + bounds=(1e3, 1e6), + units=units.Pa, + ) + self.temperature = Var( + initialize=298.15, + bounds=(298.15, 323.15), + units=units.K, + ) + self.mass_frac_comp = Var( + self.params.component_list, + initialize=0.1, + units=units.dimensionless, + ) + + self.dens_mass = Param( + initialize=1000, + units=units.kg / units.m**3, + ) + + if self.config.defined_state is False: + self.sum_mass_frac_eqn = Constraint( + expr=sum(self.mass_frac_comp[j] for j in self.component_list) == 1 + ) + + self.volumetric_flow = Constraint( + expr=self.flow_vol * 1000 * units.kg * units.m**-3 == self.flow_mass + ) + + def get_material_flow_terms(b, p, j): + return b.flow_mass * b.mass_frac_comp[j] + + def get_enthalpy_flow_terms(b, p): + return ( + b.flow_mass * 42e3 * units.J / units.kg * (b.temperature - 273.15 * units.K) + ) - m.fs.properties = TestParameterBlock() + def default_material_balance_type(self): + return MaterialBalanceType.componentTotal - m.fs.unit = Thickener0D( - solid_property_package=m.fs.properties, - liquid_property_package=m.fs.properties, - ) - expected = ( - "The Thickener0D model is currently a beta capability and will " - "likely change in the next release as a more predictive version is " - "developed." - ) + def define_state_vars(b): + return { + "flow_mass": b.flow_mass, + "mass_frac_comp": b.mass_frac_comp, + "temperature": b.temperature, + "pressure": b.pressure, + } - assert expected in caplog.text + def get_material_flow_basis(b): + return MaterialFlowBasis.mass +# ----------------------------------------------------------------------------- class TestThickener0DBasic: @pytest.fixture(scope="class") def model(self): m = ConcreteModel() m.fs = FlowsheetBlock(dynamic=False) - m.fs.properties = TestParameterBlock() + m.fs.solid = SolidParameterBlock() + m.fs.liquid = LiquidParameterBlock() m.fs.unit = Thickener0D( - solid_property_package=m.fs.properties, - liquid_property_package=m.fs.properties, + solid_property_package=m.fs.solid, + liquid_property_package=m.fs.liquid, ) - m.fs.unit.solid_inlet.flow_mass.fix(1.33) + m.fs.unit.solid_inlet.flow_mass.fix(7e-6 * 2400) m.fs.unit.solid_inlet.mass_frac_comp[0, "a"].fix(0.2) m.fs.unit.solid_inlet.mass_frac_comp[0, "b"].fix(0.8) m.fs.unit.solid_inlet.temperature.fix(303.15) m.fs.unit.solid_inlet.pressure.fix(101325.0) - m.fs.unit.liquid_inlet.flow_mass.fix(6.65) - m.fs.unit.liquid_inlet.mass_frac_comp[0, "a"].fix(0.4) - m.fs.unit.liquid_inlet.mass_frac_comp[0, "b"].fix(0.6) - m.fs.unit.liquid_inlet.temperature.fix(320) - m.fs.unit.liquid_inlet.pressure.fix(2e5) + m.fs.unit.liquid_inlet.flow_mass.fix(0.3 / 0.7 * 7e-6 * 1000) + m.fs.unit.liquid_inlet.mass_frac_comp[0, "c"].fix(0.3) + m.fs.unit.liquid_inlet.mass_frac_comp[0, "d"].fix(0.7) + m.fs.unit.liquid_inlet.temperature.fix(310) + m.fs.unit.liquid_inlet.pressure.fix(1.5e5) - # Operating conditions based on Example 5.1 (pg 199) - # Coulson & Richardson's Chemical Engineering Vol 2 (4th Ed.) - m.fs.unit.liquid_recovery.fix(0.7) + m.fs.unit.solid_underflow.flow_mass.fix(5e-6 * 2400) - # Thickener specific parameters - m.fs.unit.height_clarification.fix(1.5) - m.fs.unit.settling_velocity_pinch.fix(0.94e-4) - m.fs.unit.liquid_solid_pinch.fix(3.7) - m.fs.unit.settling_time.fix(200) + # Parameters + m.fs.unit.area.fix(1.2) + m.fs.unit.v0.fix(1.18e-4) + m.fs.unit.v1.fix(1e-5) + m.fs.unit.C.fix(5) + m.fs.unit.solid_fraction_max.fix(0.9) return m - @pytest.mark.unit - def test_config(self, model): - # Check unit config arguments - assert len(model.fs.unit.config) == 9 - - assert not model.fs.unit.config.dynamic - assert not model.fs.unit.config.has_holdup - assert ( - model.fs.unit.config.material_balance_type == MaterialBalanceType.useDefault - ) - assert ( - model.fs.unit.config.energy_split_basis - == EnergySplittingType.equal_temperature - ) - assert ( - model.fs.unit.config.momentum_balance_type - == MomentumBalanceType.pressureTotal - ) - - assert model.fs.unit.config.solid_property_package is model.fs.properties - assert model.fs.unit.config.liquid_property_package is model.fs.properties - - assert model.fs.unit.default_initializer is BlockTriangularizationInitializer - - @pytest.mark.build - @pytest.mark.unit - def test_build(self, model): - - assert isinstance(model.fs.unit.solid_inlet, Port) - assert len(model.fs.unit.solid_inlet.vars) == 4 - assert hasattr(model.fs.unit.solid_inlet, "flow_mass") - assert hasattr(model.fs.unit.solid_inlet, "mass_frac_comp") - assert hasattr(model.fs.unit.solid_inlet, "temperature") - assert hasattr(model.fs.unit.solid_inlet, "pressure") - - assert isinstance(model.fs.unit.solid_outlet, Port) - assert len(model.fs.unit.solid_outlet.vars) == 4 - assert hasattr(model.fs.unit.solid_outlet, "flow_mass") - assert hasattr(model.fs.unit.solid_outlet, "mass_frac_comp") - assert hasattr(model.fs.unit.solid_outlet, "temperature") - assert hasattr(model.fs.unit.solid_outlet, "pressure") - - assert isinstance(model.fs.unit.liquid_inlet, Port) - assert len(model.fs.unit.liquid_inlet.vars) == 4 - assert hasattr(model.fs.unit.liquid_inlet, "flow_mass") - assert hasattr(model.fs.unit.liquid_inlet, "mass_frac_comp") - assert hasattr(model.fs.unit.liquid_inlet, "temperature") - assert hasattr(model.fs.unit.liquid_inlet, "pressure") - - assert isinstance(model.fs.unit.retained_liquid_outlet, Port) - assert len(model.fs.unit.retained_liquid_outlet.vars) == 4 - assert hasattr(model.fs.unit.retained_liquid_outlet, "flow_mass") - assert hasattr(model.fs.unit.retained_liquid_outlet, "mass_frac_comp") - assert hasattr(model.fs.unit.retained_liquid_outlet, "temperature") - assert hasattr(model.fs.unit.retained_liquid_outlet, "pressure") - - assert isinstance(model.fs.unit.recovered_liquid_outlet, Port) - assert len(model.fs.unit.recovered_liquid_outlet.vars) == 4 - assert hasattr(model.fs.unit.recovered_liquid_outlet, "flow_mass") - assert hasattr(model.fs.unit.recovered_liquid_outlet, "mass_frac_comp") - assert hasattr(model.fs.unit.recovered_liquid_outlet, "temperature") - assert hasattr(model.fs.unit.recovered_liquid_outlet, "pressure") - - assert isinstance(model.fs.unit.split, SeparatorData) - assert isinstance(model.fs.unit.liquid_recovery, Var) - - assert isinstance(model.fs.unit.area, Var) - assert isinstance(model.fs.unit.height, Var) - assert isinstance(model.fs.unit.height_clarification, Var) - assert isinstance(model.fs.unit.settling_velocity_pinch, Var) - assert isinstance(model.fs.unit.liquid_solid_pinch, Var) - assert isinstance(model.fs.unit.liquid_solid_underflow, Var) - assert isinstance(model.fs.unit.settling_time, Var) - - assert isinstance(model.fs.unit.underflow_sl_constraint, Constraint) - assert isinstance(model.fs.unit.cross_sectional_area_constraint, Constraint) - assert isinstance(model.fs.unit.height_constraint, Constraint) - - assert number_variables(model) == 29 - assert number_total_constraints(model) == 14 - # These are the solid properties, as they do not appear in constraints - assert number_unused_variables(model) == 4 - - @pytest.mark.component - def test_units(self, model): - assert_units_consistent(model) - - assert_units_equivalent(model.fs.unit.height_clarification, units.m) - assert_units_equivalent( - model.fs.unit.settling_velocity_pinch, units.m / units.s - ) - assert_units_equivalent(model.fs.unit.settling_time, units.s) - - @pytest.mark.unit - def test_dof(self, model): - assert degrees_of_freedom(model) == 0 - @pytest.mark.component def test_structural_issues(self, model): dt = DiagnosticsToolbox(model) dt.report_structural_issues() - dt.display_underconstrained_set() dt.assert_no_structural_warnings() @pytest.mark.component @@ -343,66 +319,16 @@ def test_numerical_issues(self, model): @pytest.mark.skipif(solver is None, reason="Solver not available") @pytest.mark.component def test_solution(self, model): - # Solid outlet - assert pytest.approx(101325.0, rel=1e-8) == value( - model.fs.unit.solid_outlet.pressure[0] - ) - assert pytest.approx(303.15, rel=1e-8) == value( - model.fs.unit.solid_outlet.temperature[0] - ) - assert pytest.approx(0.2, rel=1e-8) == value( - model.fs.unit.solid_outlet.mass_frac_comp[0, "a"] - ) - assert pytest.approx(0.8, rel=1e-8) == value( - model.fs.unit.solid_outlet.mass_frac_comp[0, "b"] - ) - - assert pytest.approx(1.33, rel=1e-8) == value( - model.fs.unit.solid_outlet.flow_mass[0] - ) - - # Retained liquid - assert pytest.approx(2e5, rel=1e-8) == value( - model.fs.unit.retained_liquid_outlet.pressure[0] - ) - assert pytest.approx(320, rel=1e-8) == value( - model.fs.unit.retained_liquid_outlet.temperature[0] - ) - assert pytest.approx(0.4, rel=1e-8) == value( - model.fs.unit.retained_liquid_outlet.mass_frac_comp[0, "a"] + assert value(model.fs.unit.solid_fraction_feed[0]) == pytest.approx( + 0.7, rel=1e-5 ) - assert pytest.approx(0.6, rel=1e-8) == value( - model.fs.unit.retained_liquid_outlet.mass_frac_comp[0, "b"] + assert value(model.fs.unit.solid_fraction_underflow[0]) == pytest.approx( + 0.801065, rel=1e-5 ) - assert pytest.approx(6.65 * 0.3, rel=1e-8) == value( - model.fs.unit.retained_liquid_outlet.flow_mass[0] + assert value(model.fs.unit.solid_fraction_overflow[0]) == pytest.approx( + 0.447338, rel=1e-5 ) - # Recovered liquid - assert pytest.approx(2e5, rel=1e-8) == value( - model.fs.unit.recovered_liquid_outlet.pressure[0] - ) - assert pytest.approx(320, rel=1e-8) == value( - model.fs.unit.recovered_liquid_outlet.temperature[0] - ) - assert pytest.approx(0.4, rel=1e-8) == value( - model.fs.unit.recovered_liquid_outlet.mass_frac_comp[0, "a"] - ) - assert pytest.approx(0.6, rel=1e-8) == value( - model.fs.unit.recovered_liquid_outlet.mass_frac_comp[0, "b"] - ) - assert pytest.approx(6.65 * 0.7, rel=1e-8) == value( - model.fs.unit.recovered_liquid_outlet.flow_mass[0] - ) - - # Thickener Vars - assert pytest.approx(1.5, rel=1e-6) == value( - model.fs.unit.liquid_solid_underflow[0] - ) - assert pytest.approx(31.12766, rel=1e-6) == value(model.fs.unit.area) - model.fs.unit.height.display() - assert pytest.approx(1.536318, rel=1e-6) == value(model.fs.unit.height) - @pytest.mark.ui @pytest.mark.unit def test_get_performance_contents(self, model): @@ -411,11 +337,110 @@ def test_get_performance_contents(self, model): assert perf_dict == { "vars": { "Area": model.fs.unit.area, - "Height": model.fs.unit.height, - "Liquid Recovery": model.fs.unit.liquid_recovery[0], - "Underflow L/S": model.fs.unit.liquid_solid_underflow[0], - "Pinch L/S": model.fs.unit.liquid_solid_pinch[0], - "Critical Settling Velocity": model.fs.unit.settling_velocity_pinch[0], - "Settling Time": model.fs.unit.settling_time[0], - } + "Liquid Recovery": model.fs.unit.liquid_split.split_fraction[ + 0, "overflow" + ], + "Feed Solid Fraction": model.fs.unit.solid_fraction_feed[0], + "Underflow Solid Fraction": model.fs.unit.solid_fraction_underflow[0], + }, + "params": { + "v0": model.fs.unit.v0, + "v1": model.fs.unit.v1, + "C": model.fs.unit.C, + "solid_fraction_max": model.fs.unit.solid_fraction_max, + }, + } + + @pytest.mark.ui + @pytest.mark.component + def test_get_stream_table_contents(self, model): + stable = model.fs.unit._get_stream_table_contents() + + expected = { + "Units": { + "flow_mass": getattr(units.pint_registry, "kg/second"), + "mass_frac_comp a": getattr(units.pint_registry, "dimensionless"), + "mass_frac_comp b": getattr(units.pint_registry, "dimensionless"), + "temperature": getattr(units.pint_registry, "K"), + "pressure": getattr(units.pint_registry, "Pa"), + "mass_frac_comp c": getattr(units.pint_registry, "dimensionless"), + "mass_frac_comp d": getattr(units.pint_registry, "dimensionless"), + }, + "Feed Solid": { + "flow_mass": pytest.approx(0.0168, rel=1e-4), + "mass_frac_comp a": pytest.approx(0.2, rel=1e-4), + "mass_frac_comp b": pytest.approx(0.8, rel=1e-4), + "temperature": pytest.approx(303.15, rel=1e-4), + "pressure": pytest.approx(101325, rel=1e-4), + "mass_frac_comp c": float("nan"), + "mass_frac_comp d": float("nan"), + }, + "Feed Liquid": { + "flow_mass": pytest.approx(0.003, rel=1e-4), + "mass_frac_comp a": float("nan"), + "mass_frac_comp b": float("nan"), + "temperature": pytest.approx(310, rel=1e-4), + "pressure": pytest.approx(1.5e5, rel=1e-4), + "mass_frac_comp c": pytest.approx(0.3, rel=1e-4), + "mass_frac_comp d": pytest.approx(0.7, rel=1e-4), + }, + "Underflow Solid": { + "flow_mass": pytest.approx(0.012, rel=1e-4), + "mass_frac_comp a": pytest.approx(0.2, rel=1e-4), + "mass_frac_comp b": pytest.approx(0.8, rel=1e-4), + "temperature": pytest.approx(303.15, rel=1e-4), + "pressure": pytest.approx(101325, rel=1e-4), + "mass_frac_comp c": float("nan"), + "mass_frac_comp d": float("nan"), + }, + "Underflow Liquid": { + "flow_mass": pytest.approx(0.0012417, rel=1e-4), + "mass_frac_comp a": float("nan"), + "mass_frac_comp b": float("nan"), + "temperature": pytest.approx(310, rel=1e-4), + "pressure": pytest.approx(1.5e5, rel=1e-4), + "mass_frac_comp c": pytest.approx(0.3, rel=1e-4), + "mass_frac_comp d": pytest.approx(0.7, rel=1e-4), + }, + "Overflow Solid": { + "flow_mass": pytest.approx(0.0048, rel=1e-4), + "mass_frac_comp a": pytest.approx(0.2, rel=1e-4), + "mass_frac_comp b": pytest.approx(0.8, rel=1e-4), + "temperature": pytest.approx(303.15, rel=1e-4), + "pressure": pytest.approx(101325, rel=1e-4), + "mass_frac_comp c": float("nan"), + "mass_frac_comp d": float("nan"), + }, + "Overflow Liquid": { + "flow_mass": pytest.approx(0.0017583, rel=1e-4), + "mass_frac_comp a": float("nan"), + "mass_frac_comp b": float("nan"), + "temperature": pytest.approx(310, rel=1e-4), + "pressure": pytest.approx(1.5e5, rel=1e-4), + "mass_frac_comp c": pytest.approx(0.3, rel=1e-4), + "mass_frac_comp d": pytest.approx(0.7, rel=1e-4), + }, } + + stb = stable.to_dict() + for k, d in stb.items(): + if k == "Units": + assert d == expected["Units"] + else: + for i, dd in d.items(): + if not isnan(dd): + assert dd == expected[k][i] + else: + if "Liquid" in k: + assert i in ["mass_frac_comp a", "mass_frac_comp b"] + else: + assert i in ["mass_frac_comp c", "mass_frac_comp d"] + + @pytest.mark.unit + def test_deprecate_initialize(self, model): + with pytest.raises( + NotImplementedError, + match="The Thickener0D unit model does not support the old initialization API. " + "Please use the new API \(InitializerObjects\) instead.", + ): + model.fs.unit.initialize() diff --git a/idaes/models/unit_models/solid_liquid/thickener.py b/idaes/models/unit_models/solid_liquid/thickener.py index 3d86aa204a..0d84e762f8 100644 --- a/idaes/models/unit_models/solid_liquid/thickener.py +++ b/idaes/models/unit_models/solid_liquid/thickener.py @@ -19,17 +19,29 @@ """ # Import Python libraries import logging +from pandas import DataFrame # Import Pyomo libraries -from pyomo.environ import Var, units +from pyomo.environ import Constraint, 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.solid_liquid.sl_separator import ( - SLSeparatorData, +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 __author__ = "Andrew Lee" @@ -40,13 +52,138 @@ @declare_process_block_class("Thickener0D") -class Thickener0DData(SLSeparatorData): +class Thickener0DData(UnitModelBlockData): """ Thickener0D Unit Model Class """ - CONFIG = SLSeparatorData.CONFIG() - # TODO: Method to calculate pinch settling velocity + 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 def build(self): """ @@ -58,118 +195,242 @@ def build(self): Returns: None """ - logger.warning( - "The Thickener0D model is currently a beta capability and will " - "likely change in the next release as a more predictive version is " - "developed." - ) # Call super().build to setup dynamics super().build() - # Add additional variables and constraints - s_metadata = self.solid_state.params.get_metadata() - # TODO: Add support for molar basis + # 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.area = Var( - initialize=1, - units=s_metadata.get_derived_units("area"), - doc="Cross sectional area of thickener", + self.solid_inlet_state = self.config.solid_property_package.build_state_block( + self.flowsheet().time, doc="Solid properties in separator", **tmp_dict ) - self.height = Var( - initialize=1, - units=s_metadata.get_derived_units("length"), - doc="Total depth of thickener", + # Add solid spliter + 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, ) - self.height_clarification = Var( - initialize=1, - units=s_metadata.get_derived_units("length"), - doc="Depth of clarification zone", + # 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) - self.settling_velocity_pinch = Var( - self.flowsheet().time, + # 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 spliter + 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 + # TODO: Add support for molar basis + + self.area = Var( initialize=1, - units=s_metadata.get_derived_units("velocity"), - doc="Settling velocity of suspension at pinch point", + units=uom.AREA, + doc="Cross sectional area of thickener", ) - self.liquid_solid_pinch = Var( + # Solid Fractions + self.solid_fraction_feed = Var( self.flowsheet().time, - initialize=0.2, + initialize=0.7, units=units.dimensionless, - doc="Liquid-solid ratio (mass basis) at pinch point", + bounds=(0, None), ) - - self.liquid_solid_underflow = Var( + self.solid_fraction_underflow = Var( self.flowsheet().time, - initialize=0.2, + initialize=0.7, units=units.dimensionless, - doc="Liquid-solid ratio (mass basis) at underflow", + bounds=(0, None), ) - - self.settling_time = Var( + self.solid_fraction_overflow = Var( self.flowsheet().time, - initialize=1, - units=s_metadata.get_derived_units("time"), - doc="Settling time of suspension", + initialize=0.7, + units=units.dimensionless, + bounds=(0, None), ) - @self.Constraint( - self.flowsheet().time, doc="Constraint to calculate L/S ratio at underflow" + # Flux densities + self.flux_density_underflow = Var( + self.flowsheet().time, initialize=0, units=uom.VELOCITY + ) + self.flux_density_overflow = Var( + self.flowsheet().time, initialize=0, units=uom.VELOCITY ) - def underflow_sl_constraint(b, t): - return b.liquid_solid_underflow[t] * b.solid_state[ - t - ].flow_mass == units.convert( - b.split.retained_state[t].flow_mass, - to_units=s_metadata.get_derived_units("flow_mass"), + + # Parameters + self.v0 = Var(initialize=1e-4, units=uom.VELOCITY) + self.v1 = Var(initialize=1e-5, units=uom.VELOCITY) + self.C = Var(initialize=5, units=units.dimensionless) + self.solid_fraction_max = Var(initialize=0.9, units=units.dimensionless) + + # --------------------------------------------------------------------------------------------- + # Constraints + # 2.8 + @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 * 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, ) - @self.Constraint( - self.flowsheet().time, doc="Constraint to estimate cross-sectional area" - ) - def cross_sectional_area_constraint(b, t): - return b.area * b.liquid_inlet_state[ - t - ].dens_mass * b.settling_velocity_pinch[t] == b.solid_state[t].flow_mass * ( - b.liquid_solid_pinch[t] - b.liquid_solid_underflow[t] + @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 * 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, ) - @self.Constraint( - self.flowsheet().time, doc="Constraint to estimate height of thickener" - ) - def height_constraint(b, t): - return (b.height - b.height_clarification) * b.area * b.solid_state[ - t - ].dens_mass == units.convert( - b.settling_time[t] + @self.Constraint(self.flowsheet().time) + def solids_continuity(b, t): + return b.solid_inlet_state[t].flow_vol * b.solid_fraction_feed[t] == ( + b.area * (b.flux_density_overflow[t] + b.flux_density_underflow[t]) + - b.solid_split.overflow_state[t].flow_vol + * (b.solid_fraction_overflow[t] - b.solid_fraction_feed[t]) + + b.solid_split.underflow_state[t].flow_vol + * (b.solid_fraction_underflow[t] - b.solid_fraction_feed[t]) + ) + + @self.Constraint(self.flowsheet().time) + def solids_conservation(b, t): + return b.solid_inlet_state[t].flow_vol * b.solid_fraction_feed[t] == ( + +b.solid_split.overflow_state[t].flow_vol * b.solid_fraction_overflow[t] + + b.solid_split.underflow_state[t].flow_vol + * 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_state[t].flow_mass + b.solid_inlet_state[t].flow_vol + units.convert( - b.solid_state[t].dens_mass / b.liquid_inlet_state[t].dens_mass, - to_units=units.dimensionless, + b.liquid_inlet_state[t].flow_vol, to_units=uom.FLOW_VOL ) - * ( - b.liquid_inlet_state[t].flow_mass - + b.split.retained_state[t].flow_mass + ) + ) + + @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, ) - / 2 - ), - to_units=s_metadata.get_derived_units("mass"), + ) ) def _get_performance_contents(self, time_point=0): - return { "vars": { "Area": self.area, - "Height": self.height, - "Liquid Recovery": self.liquid_recovery[time_point], - "Underflow L/S": self.liquid_solid_underflow[time_point], - "Pinch L/S": self.liquid_solid_pinch[time_point], - "Critical Settling Velocity": self.settling_velocity_pinch[time_point], - "Settling Time": self.settling_time[time_point], - } + "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], + }, + "params": { + "v0": self.v0, + "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") + + def initialize(self, **kwargs): + raise NotImplementedError( + "The Thickener0D unit model does not support the old initialization API. " + "Please use the new API (InitializerObjects) instead." + ) From 0d582b7f91eff94abdf00256796f376e850f9d23 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Tue, 12 Dec 2023 15:54:16 -0500 Subject: [PATCH 04/10] Adding Stokes' Law --- .../solid_liquid/tests/test_thickener.py | 81 ++++++++++++++++--- .../unit_models/solid_liquid/thickener.py | 80 ++++++++++++++---- 2 files changed, 135 insertions(+), 26 deletions(-) diff --git a/idaes/models/unit_models/solid_liquid/tests/test_thickener.py b/idaes/models/unit_models/solid_liquid/tests/test_thickener.py index f5a03cd75e..710c79406b 100644 --- a/idaes/models/unit_models/solid_liquid/tests/test_thickener.py +++ b/idaes/models/unit_models/solid_liquid/tests/test_thickener.py @@ -48,6 +48,15 @@ InitializationStatus, ) from idaes.core.util import DiagnosticsToolbox +from idaes.models.unit_models.separator import ( + SeparatorData, + EnergySplittingType, +) +from idaes.core.util.model_statistics import ( + number_variables, + number_total_constraints, + number_unused_variables, +) # ----------------------------------------------------------------------------- # Get default solver for testing @@ -115,7 +124,7 @@ def build(self): ) self.dens_mass = Param( - initialize=1000, + initialize=2500, units=units.kg / units.m**3, ) @@ -125,7 +134,7 @@ def build(self): ) self.volumetric_flow = Constraint( - expr=self.flow_vol * 2400 * units.kg * units.m**-3 == self.flow_mass + expr=self.flow_vol * self.dens_mass == self.flow_mass ) def get_material_flow_terms(b, p, j): @@ -215,6 +224,10 @@ def build(self): initialize=1000, units=units.kg / units.m**3, ) + self.visc_d = Param( + initialize=1e-3, + units=units.Pa * units.s, + ) if self.config.defined_state is False: self.sum_mass_frac_eqn = Constraint( @@ -222,7 +235,7 @@ def build(self): ) self.volumetric_flow = Constraint( - expr=self.flow_vol * 1000 * units.kg * units.m**-3 == self.flow_mass + expr=self.flow_vol * self.dens_mass == self.flow_mass ) def get_material_flow_terms(b, p, j): @@ -263,7 +276,7 @@ def model(self): liquid_property_package=m.fs.liquid, ) - m.fs.unit.solid_inlet.flow_mass.fix(7e-6 * 2400) + m.fs.unit.solid_inlet.flow_mass.fix(7e-6 * 2500) m.fs.unit.solid_inlet.mass_frac_comp[0, "a"].fix(0.2) m.fs.unit.solid_inlet.mass_frac_comp[0, "b"].fix(0.8) m.fs.unit.solid_inlet.temperature.fix(303.15) @@ -275,7 +288,7 @@ def model(self): m.fs.unit.liquid_inlet.temperature.fix(310) m.fs.unit.liquid_inlet.pressure.fix(1.5e5) - m.fs.unit.solid_underflow.flow_mass.fix(5e-6 * 2400) + m.fs.unit.solid_underflow.flow_mass.fix(5e-6 * 2500) # Parameters m.fs.unit.area.fix(1.2) @@ -286,10 +299,51 @@ def model(self): return m + @pytest.mark.unit + def test_config(self, model): + # Check unit config arguments + assert len(model.fs.unit.config) == 9 + + assert not model.fs.unit.config.dynamic + assert not model.fs.unit.config.has_holdup + assert ( + model.fs.unit.config.material_balance_type == MaterialBalanceType.useDefault + ) + assert ( + model.fs.unit.config.energy_split_basis + == EnergySplittingType.equal_temperature + ) + assert ( + model.fs.unit.config.momentum_balance_type + == MomentumBalanceType.pressureTotal + ) + + assert model.fs.unit.config.solid_property_package is model.fs.solid + assert model.fs.unit.config.liquid_property_package is model.fs.liquid + + assert model.fs.unit.default_initializer is BlockTriangularizationInitializer + + @pytest.mark.build + @pytest.mark.unit + def test_build(self, model): + assert isinstance(model.fs.unit.solid_inlet, Port) + assert isinstance(model.fs.unit.solid_underflow, Port) + assert isinstance(model.fs.unit.solid_overflow, Port) + + assert isinstance(model.fs.unit.liquid_inlet, Port) + assert isinstance(model.fs.unit.liquid_underflow, Port) + assert isinstance(model.fs.unit.liquid_overflow, Port) + + assert isinstance(model.fs.unit.solid_split, SeparatorData) + assert isinstance(model.fs.unit.liquid_split, SeparatorData) + + assert number_variables(model) == 51 + assert number_total_constraints(model) == 37 + assert number_unused_variables(model) == 0 + @pytest.mark.component def test_structural_issues(self, model): dt = DiagnosticsToolbox(model) - dt.report_structural_issues() dt.assert_no_structural_warnings() @pytest.mark.component @@ -319,6 +373,7 @@ def test_numerical_issues(self, model): @pytest.mark.skipif(solver is None, reason="Solver not available") @pytest.mark.component def test_solution(self, model): + model.display() assert value(model.fs.unit.solid_fraction_feed[0]) == pytest.approx( 0.7, rel=1e-5 ) @@ -328,6 +383,9 @@ def test_solution(self, model): assert value(model.fs.unit.solid_fraction_overflow[0]) == pytest.approx( 0.447338, rel=1e-5 ) + assert value(model.fs.unit.particle_size[0]) == pytest.approx( + 1.2018e-5, rel=1e-5 + ) @pytest.mark.ui @pytest.mark.unit @@ -342,9 +400,8 @@ def test_get_performance_contents(self, model): ], "Feed Solid Fraction": model.fs.unit.solid_fraction_feed[0], "Underflow Solid Fraction": model.fs.unit.solid_fraction_underflow[0], - }, - "params": { - "v0": model.fs.unit.v0, + "particle size": model.fs.unit.particle_size[0], + "v0": model.fs.unit.v0[0], "v1": model.fs.unit.v1, "C": model.fs.unit.C, "solid_fraction_max": model.fs.unit.solid_fraction_max, @@ -367,7 +424,7 @@ def test_get_stream_table_contents(self, model): "mass_frac_comp d": getattr(units.pint_registry, "dimensionless"), }, "Feed Solid": { - "flow_mass": pytest.approx(0.0168, rel=1e-4), + "flow_mass": pytest.approx(0.0175, rel=1e-4), "mass_frac_comp a": pytest.approx(0.2, rel=1e-4), "mass_frac_comp b": pytest.approx(0.8, rel=1e-4), "temperature": pytest.approx(303.15, rel=1e-4), @@ -385,7 +442,7 @@ def test_get_stream_table_contents(self, model): "mass_frac_comp d": pytest.approx(0.7, rel=1e-4), }, "Underflow Solid": { - "flow_mass": pytest.approx(0.012, rel=1e-4), + "flow_mass": pytest.approx(0.0125, rel=1e-4), "mass_frac_comp a": pytest.approx(0.2, rel=1e-4), "mass_frac_comp b": pytest.approx(0.8, rel=1e-4), "temperature": pytest.approx(303.15, rel=1e-4), @@ -403,7 +460,7 @@ def test_get_stream_table_contents(self, model): "mass_frac_comp d": pytest.approx(0.7, rel=1e-4), }, "Overflow Solid": { - "flow_mass": pytest.approx(0.0048, rel=1e-4), + "flow_mass": pytest.approx(0.005, rel=1e-4), "mass_frac_comp a": pytest.approx(0.2, rel=1e-4), "mass_frac_comp b": pytest.approx(0.8, rel=1e-4), "temperature": pytest.approx(303.15, rel=1e-4), diff --git a/idaes/models/unit_models/solid_liquid/thickener.py b/idaes/models/unit_models/solid_liquid/thickener.py index 0d84e762f8..d2fd6989f5 100644 --- a/idaes/models/unit_models/solid_liquid/thickener.py +++ b/idaes/models/unit_models/solid_liquid/thickener.py @@ -13,8 +13,18 @@ """ Thickener unit model. -This model extends the SLSeparator unit model by adding constraints that relate -area and vessel height to the liquid recovery fraction. +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, +J. Aust. Math. Soc. Ser. B 33 (1992) 269–289 """ # Import Python libraries @@ -42,6 +52,7 @@ 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" @@ -266,7 +277,6 @@ def build(self): # Add additional variables and constraints uom = self.solid_inlet_state.params.get_metadata().derived_units - # TODO: Add support for molar basis self.area = Var( initialize=1, @@ -280,33 +290,67 @@ def build(self): 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 + 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 + self.flowsheet().time, + initialize=0, + units=uom.VELOCITY, + doc="Kynch flux density in overflow", ) # Parameters - self.v0 = Var(initialize=1e-4, units=uom.VELOCITY) - self.v1 = Var(initialize=1e-5, units=uom.VELOCITY) - self.C = Var(initialize=5, units=units.dimensionless) - self.solid_fraction_max = Var(initialize=0.9, units=units.dimensionless) + 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 @@ -316,7 +360,7 @@ 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 * u[t] * (1 - u[t] / b.solid_fraction_max) ** b.C + 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, ) @@ -326,7 +370,7 @@ 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 * u[t] * (1 - u[t] / b.solid_fraction_max) ** b.C + 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, ) @@ -382,6 +426,15 @@ def underflow_volume_fraction(b, t): ) ) + @self.Constraint(self.flowsheet().time) + def stokes_law(b, t): + # Assuming constant properties, source from feed states + return 18 * b.v0[t] * b.liquid_inlet_state[t].visc_d == ( + (b.solid_inlet_state[t].dens_mass - b.liquid_inlet_state[t].dens_mass) + * CONST.acceleration_gravity + * b.particle_size[t] ** 2 + ) + def _get_performance_contents(self, time_point=0): return { "vars": { @@ -391,9 +444,8 @@ def _get_performance_contents(self, time_point=0): ], "Feed Solid Fraction": self.solid_fraction_feed[time_point], "Underflow Solid Fraction": self.solid_fraction_underflow[time_point], - }, - "params": { - "v0": self.v0, + "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, From fd64764ebda564fbc09004b4444abdc04c419631 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Wed, 13 Dec 2023 13:12:29 -0500 Subject: [PATCH 05/10] Working on updating docs --- .../unit_models/solid_liquid/thickener0d.rst | 55 ++++++++----------- 1 file changed, 24 insertions(+), 31 deletions(-) diff --git a/docs/reference_guides/model_libraries/generic/unit_models/solid_liquid/thickener0d.rst b/docs/reference_guides/model_libraries/generic/unit_models/solid_liquid/thickener0d.rst index d04a479be5..cdba451d13 100644 --- a/docs/reference_guides/model_libraries/generic/unit_models/solid_liquid/thickener0d.rst +++ b/docs/reference_guides/model_libraries/generic/unit_models/solid_liquid/thickener0d.rst @@ -1,62 +1,55 @@ Thickener (0D) ============== -.. warning:: - The Thickener model is currently in beta status and will likely change in the next release as a more predictive version is developed. +The ``Thickener0D`` unit model is a predictive model for clarifiers and thickeners based on the following references: -The ``Thickener0D`` unit model is an extension of the :ref:`SLSeparator ` model which adds constraints to estimate the area and height of a vessel required to achieve the desired separation of solid and liquid based on experimental measurements of the settling velocity. This model is based on correlations described in: +[1] 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 -[1] Coulson & Richardson's Chemical Engineering, Volume 2 Particle Technology & Separation Processes (4th Ed.), Butterworth-Heinemann (2001) +[2] N.G. Barton, C.-H. Li, S.J. Spencer, Control of a surface of discontinuity in continuous thickeners, J. Aust. Math. Soc. Ser. B 33 (1992) 269–289 -Sizing Thickeners and Pinch Point ---------------------------------- - -The approach for sizing the thickener vessel used in this model relies on identifying a pinch point in the thickener which is the limiting condition for the settling velocity of the suspension as described in [1]. The pinch point is described by the condition: - -.. math:: max \left( \frac{(Y - Y_{under})}{u(Y)} \right) - -where :math:`Y` is the mass based liquid-to-solid ratio, :math:`u(Y)` is the settling velocity of the suspension as a function of :math:`Y` and :math:`Y_{under}` is the liquid-solid ratio at the thickener underflow. Correlations exist which can predict :math:`u(Y)` however in many cases it is necessary to measure this experimentally. Additionally, whilst there are techniques for embedding the maximization operation within an equation-oriented model, these approaches tend to be highly non-linear and require the user to provide good values for scaling parameters and thus have not been implemented yet. - -In the current implementation of the model, :math:`Y_{pinch}` and :math:`u_{pinch}` are created as user-defined input variables which should generally be fixed at appropriate values. Users may choose to add a correlation for :math:`u(Y)` if they choose. Degrees of Freedom ------------------ -The ``Thickener0D`` model has 5 degrees of freedom, which are generally chosen from: +The ``Thickener0D`` model has 6 degrees of freedom, which are generally chosen from: -* the liquid recovery fraction or underflow liquid-to-solid ratio, -* the liquid-to-solid ratio and settling velocity at the pinch (critical) point, -* the cross-sectional area of the thickener, -* the depth of the clarification zone, -* the depth of the sedimentation zone or the required settling time (experimental). +* the cross-sectional area of the settler ``area``, +* the solid particle size ``particle_size``, +* the maximum achievable solid fraction ``solid_fraction_max``, +* two empirical coefficients for calcuating the settling velocity ``v1``, ``C``, +* the volumetric flowrate or volume fraction of solids at the underflow. Model Structure --------------- -The ``Thickener0D`` model has the same structure as the :ref:`SLSeparator `. +The ``Thickener0D`` model contains two separators, ``solid_split`` and ``liquid_spit`` to separate the solid and liquid flows. The separations are assumed to be based on total flow with no change in composition, temperature or pressure. The ``Thickener0D`` model has 6 Ports (2 inlet and 4 outlet): + +* solid_inlet, +* liquid_inlet, +* solid_overflow, +* liquid_overflow, +* solid_underflow, +* liquid_underflow. Additional Constraints ---------------------- -The ``Thickener0D`` model adds the following additional constraints beyond those written by the :ref:`SLSeparator `. +The ``Thickener0D`` model adds the following constraints to calcuate the split fractions of the solid and liquid stream. -The cross-sectional area of the thickener is calculated using: +The flux density at the overflow and underflow are calcuated using the following constraint: -.. math:: A = \frac{S_t}{\rho_{liq, t}} \times \frac{(Y_{pinch, t}-Y_{under, t})}{u_{pinch, t}} +If :math:`0 \leqslant \epsilon_{x,t} \leqslant \epsilon_{max}`: -where :math:`A` is the cross-section area of the thickener, :math:`S_t` is the mass flowrate of solids entering the thickener, :math:`\rho_{liq}` is the mass density of the liquid phase, :math:`Y_{pinch}` and :math:`Y_{under}` are the mass-based liquid-to-solid ratios at the pinch point and underflow respectively and :math:`u_{pinch}` is the sedimentation velocity of the suspension at the pinch point (Eqn 5.54, pg. 198 in [1]). +.. math:: F_{x,t} = v_0 \times \epsilon_{x, t} \times(1- \frac{\epsilon_{x,t}}{\epsilon_{max}})^C + v_1 \times \epsilon_{x,t}^2 \times(\epsilon_{max}-\epsilon_{x,t}) -The liquid-solid ratio at the underflow can be calculated using: +otherwise: -.. math:: Y_{under, t} = \frac{L_{under, t}}{S_t} +.. math:: F_{x,t} = 0 -where :math:`L_{under}` is the liquid mass flowrate at the underflow. +where :math:`x` represents the overflow or underflow, :math:`F` is the flux density, :math:`v_0` is the Stokes velocity of a single particle, :math:`\epsilon` is the solid volume fraction, :math:`\epsilon_{max}` os the maximum attainable solid volume fraction, and :math:`v_1` and :math:`C` are empirical constants [2]. -The total height of the thickener is calculated using: -.. math:: H = \frac{S_t \tau_{t}}{A\rho_{sol, t}} \times \left( 1+\frac{\rho_{sol}}{\rho_{liq}} \times Y_{avg} \right) + H_{clarified} -where :math:`H` is the total height of the thickener, :math:`H_{clarified}` is the height of the clarification zone, :math:`\tau` is the empirically measured settling time required to achieve the desired underflow conditions and :math:`Y_avg` is the average liquid-solid ratio in the thickener (assumed to be linear) (Eqn 5.55, pg. 198 in [1]). Variables --------- From e3350b48415eb766547d62b2f3885eb7d54be73f Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Wed, 13 Dec 2023 15:25:45 -0500 Subject: [PATCH 06/10] Finishing docs and fixing bugs --- .../unit_models/solid_liquid/thickener0d.rst | 82 ++++++++++++++----- .../solid_liquid/tests/test_thickener.py | 73 +++++++++++++---- .../unit_models/solid_liquid/thickener.py | 74 ++++++++++++++--- 3 files changed, 181 insertions(+), 48 deletions(-) diff --git a/docs/reference_guides/model_libraries/generic/unit_models/solid_liquid/thickener0d.rst b/docs/reference_guides/model_libraries/generic/unit_models/solid_liquid/thickener0d.rst index cdba451d13..627eeaded0 100644 --- a/docs/reference_guides/model_libraries/generic/unit_models/solid_liquid/thickener0d.rst +++ b/docs/reference_guides/model_libraries/generic/unit_models/solid_liquid/thickener0d.rst @@ -11,13 +11,17 @@ The ``Thickener0D`` unit model is a predictive model for clarifiers and thickene Degrees of Freedom ------------------ -The ``Thickener0D`` model has 6 degrees of freedom, which are generally chosen from: +The ``Thickener0D`` model has 6 degrees of freedom. Four of these are used to define the solids settling velocity and must be provided by the user. -* the cross-sectional area of the settler ``area``, * the solid particle size ``particle_size``, * the maximum achievable solid fraction ``solid_fraction_max``, -* two empirical coefficients for calcuating the settling velocity ``v1``, ``C``, -* the volumetric flowrate or volume fraction of solids at the underflow. +* two empirical coefficients for calculating the settling velocity ``v1``, ``C``. + +Additionally, the user must provide 2 additional degrees of freedom regarding the design and/or operation of the thickener such as: + +* the cross-sectional area of the settler ``area``, +* the total volumetric flowrate or volume fraction of solids at the underflow, or +* the total volumetric flowrate or volume fraction of solids at the overflow. Model Structure --------------- @@ -34,9 +38,15 @@ The ``Thickener0D`` model contains two separators, ``solid_split`` and ``liquid_ Additional Constraints ---------------------- -The ``Thickener0D`` model adds the following constraints to calcuate the split fractions of the solid and liquid stream. +The ``Thickener0D`` model adds the following constraints to calculate the split fractions of the solid and liquid stream. + +Total volumetric flowrates at the feed overflow and underflow are calculated as the sum of the volumetric flowrates of the solid and liquid streams at each location. -The flux density at the overflow and underflow are calcuated using the following constraint: +.. math:: Q_{x,t} = Q_{solid,x,t} + Q_{liquid,x,t} + +where :math:`Q` represents volumetric flowrate and :math:`x` represents the feed, overflow or underflow point. + +The flux density at the overflow and underflow are calculated using the following constraint: If :math:`0 \leqslant \epsilon_{x,t} \leqslant \epsilon_{max}`: @@ -46,37 +56,65 @@ otherwise: .. math:: F_{x,t} = 0 -where :math:`x` represents the overflow or underflow, :math:`F` is the flux density, :math:`v_0` is the Stokes velocity of a single particle, :math:`\epsilon` is the solid volume fraction, :math:`\epsilon_{max}` os the maximum attainable solid volume fraction, and :math:`v_1` and :math:`C` are empirical constants [2]. +where :math:`F` is the flux density, :math:`v_0` is the Stokes velocity of a single particle, :math:`\epsilon` is the solid volume fraction, :math:`\epsilon_{max}` is the maximum attainable solid volume fraction, and :math:`v_1` and :math:`C` are empirical constants [2]. + +The solids fraction at the overflow and underflow is described using the following equation (derived from [1]). + +.. math:: Q_{feed, t} \times \epsilon_{feed,t} = A \times (F_{overflow,t} + F_{underflow,t}) - Q_{overflow,t} \times (\epsilon_{overflow,t} - \epsilon_{feed,t}) + Q_{underflow,t} \times (\epsilon_{underflow,t} - \epsilon_{feed,t}) +where :math:`A` is the cross-sectional area of the thickener (assumed constant in space and time). +Conservation of solids is enforced using the following constraint: +.. math:: Q_{feed,t} \times \epsilon_{feed,t} = Q_{overflow,t} \times \epsilon_{overflow,t} + Q_{underflow,t} \times \epsilon_{underflow,t} + +Two constraints are written to define the solid volume fraction at the feed and underflow point (no constraint is written for the overflow point). + +.. math:: Q_{solid,x,t} = \epsilon_{x,t} \times (Q_{solid,x,t} + Q_{liquid,x,t}) + +The solids volume fraction at the overflow and underflow are bounded by the following inequality constraints: + +.. math:: \epsilon_{t,x} \leqslant \epsilon_{max} + +Finally, the Stokes velocity is calculated using Stokes Law: + +.. math:: 18 \times v0_t \times \mu_{liquid,t} = (\rho_{solid,t} - \rho_{liquid,t}) \times g \times d_p^2 + +where :math:`\mu_{liquid}` is the viscosity of the liquid phase, :math:`\rho_{liquid}` and :math:`\rho_{solid}` are the densities of the liquid and solid phases, :math:`g` is the acceleration due to gravity and :math:`d_p` is the solid particle diameter. Variables --------- -The ``Thickener0D`` adds the following variables in addition to those in the :ref:`SLSeparator `. - -===================== ======================== ====== ===================================================================== -Variable Name Index Notes -===================== ======================== ====== ===================================================================== -:math:`A` area None Cross-sectional area of thickener -:math:`H` height None Total height of thickener -:math:`H_{clarified}` height_clarified None Height of clarification zone in thickener (height above feed point) -:math:`u_{pinch}` settling_velocity_pinch time Settling velocity of suspension at pinch point -:math:`Y_{pinch}` liquid_solid_pinch time Liquid-solid ratio at pinch point -:math:`Y_{under}` liquid_solid_underflow time Liquid-solid ratio at underflow -:math:`\tau` settling_time time Settling time in thickener -===================== ======================== ====== ===================================================================== +The ``Thickener0D`` adds the following variables to those contained in the Separators. + +============================= ========================= ====== ======================================================== +Variable Name Index Notes +============================= ========================= ====== ======================================================== +:math:`A` area None Cross-sectional area of thickener +:math:`Q_{feed}` flow_vol_feed time Total volumetric flowrate at feed point +:math:`Q_{overflow}` flow_vol_overflow time Total volumetric flowrate at overflow point +:math:`Q_{underflow}` flow_vol_underflow time Total volumetric flowrate at underflow point +:math:`\epsilon_{feed}` solid_fraction_feed time Volumetric solids fraction at feed point +:math:`\epsilon_{overflow}` solid_fraction_overflow time Volumetric solids fraction at overflow point +:math:`\epsilon_{underflow}` solid_fraction_underflow time Volumetric solids fraction at underflow point +:math:`F_{overflow}` flux_density_overflow time Solids flux density at overflow point +:math:`F_{underflow}` flux_density_underflow time Solids flux density at underflow point +:math:`d_p` particle size time Solid particle size +:math:`v0` v0 time Stokes velocity of isolated particle +:math:`v1` v1 None Empirical parameter in settling velocity correlation +:math:`C` C None Empirical parameter in settling velocity correlation +math:`\epsilon_{max}` solid_fraction_max None Maximum achievable volumetric solids fraction +============================= ========================= ====== ======================================================== .. module:: idaes.models.unit_models.solid_liquid.thickener -SLSeparator Class +Thickener0D Class ----------------- .. autoclass:: Thickener0D :members: -SLSeparatorData Class +Thickener0DData Class --------------------- .. autoclass:: Thickener0DData diff --git a/idaes/models/unit_models/solid_liquid/tests/test_thickener.py b/idaes/models/unit_models/solid_liquid/tests/test_thickener.py index 710c79406b..61e9ae4f73 100644 --- a/idaes/models/unit_models/solid_liquid/tests/test_thickener.py +++ b/idaes/models/unit_models/solid_liquid/tests/test_thickener.py @@ -276,22 +276,22 @@ def model(self): liquid_property_package=m.fs.liquid, ) - m.fs.unit.solid_inlet.flow_mass.fix(7e-6 * 2500) + m.fs.unit.solid_inlet.flow_mass.fix(0.7 * 7e-6 * 2500) m.fs.unit.solid_inlet.mass_frac_comp[0, "a"].fix(0.2) m.fs.unit.solid_inlet.mass_frac_comp[0, "b"].fix(0.8) m.fs.unit.solid_inlet.temperature.fix(303.15) m.fs.unit.solid_inlet.pressure.fix(101325.0) - m.fs.unit.liquid_inlet.flow_mass.fix(0.3 / 0.7 * 7e-6 * 1000) + m.fs.unit.liquid_inlet.flow_mass.fix(0.3 * 7e-6 * 1000) m.fs.unit.liquid_inlet.mass_frac_comp[0, "c"].fix(0.3) m.fs.unit.liquid_inlet.mass_frac_comp[0, "d"].fix(0.7) m.fs.unit.liquid_inlet.temperature.fix(310) m.fs.unit.liquid_inlet.pressure.fix(1.5e5) - m.fs.unit.solid_underflow.flow_mass.fix(5e-6 * 2500) + m.fs.unit.flow_vol_underflow.fix(5e-6) # Parameters - m.fs.unit.area.fix(1.2) + m.fs.unit.area.fix(1) m.fs.unit.v0.fix(1.18e-4) m.fs.unit.v1.fix(1e-5) m.fs.unit.C.fix(5) @@ -337,8 +337,8 @@ def test_build(self, model): assert isinstance(model.fs.unit.solid_split, SeparatorData) assert isinstance(model.fs.unit.liquid_split, SeparatorData) - assert number_variables(model) == 51 - assert number_total_constraints(model) == 37 + assert number_variables(model) == 54 + assert number_total_constraints(model) == 40 assert number_unused_variables(model) == 0 @pytest.mark.component @@ -373,19 +373,60 @@ def test_numerical_issues(self, model): @pytest.mark.skipif(solver is None, reason="Solver not available") @pytest.mark.component def test_solution(self, model): - model.display() assert value(model.fs.unit.solid_fraction_feed[0]) == pytest.approx( 0.7, rel=1e-5 ) assert value(model.fs.unit.solid_fraction_underflow[0]) == pytest.approx( - 0.801065, rel=1e-5 + 0.816954, rel=1e-5 ) assert value(model.fs.unit.solid_fraction_overflow[0]) == pytest.approx( - 0.447338, rel=1e-5 + 0.407615, rel=1e-5 ) assert value(model.fs.unit.particle_size[0]) == pytest.approx( - 1.2018e-5, rel=1e-5 + 1.20163e-5, rel=1e-5 ) + so = model.fs.unit.solid_split.overflow_state[0].flow_vol + lo = model.fs.unit.liquid_split.overflow_state[0].flow_vol + su = model.fs.unit.solid_split.underflow_state[0].flow_vol + lu = model.fs.unit.liquid_split.underflow_state[0].flow_vol + + assert value(su / (lu + su)) == pytest.approx( + value(model.fs.unit.solid_fraction_underflow[0]), rel=1e-5 + ) + assert value(so / (lo + so)) == pytest.approx( + value(model.fs.unit.solid_fraction_overflow[0]), rel=1e-5 + ) + + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_conservation(self, model): + sf = model.fs.unit.solid_inlet_state[0] + lf = model.fs.unit.liquid_inlet_state[0] + so = model.fs.unit.solid_split.overflow_state[0] + lo = model.fs.unit.liquid_split.overflow_state[0] + su = model.fs.unit.solid_split.underflow_state[0] + lu = model.fs.unit.liquid_split.underflow_state[0] + + # Solid mass conservation + assert value(sf.flow_mass - so.flow_mass - su.flow_mass) <= 1e-8 + # Liquid mass conservation + assert value(lf.flow_mass - lo.flow_mass - lu.flow_mass) <= 1e-8 + + # Solid volume conservation + assert value(sf.flow_vol - so.flow_vol - su.flow_vol) <= 1e-8 + # Liquid volume conservation + assert value(lf.flow_vol - lo.flow_vol - lu.flow_vol) <= 1e-8 + + assert value(so.temperature) == pytest.approx(303.15, rel=1e-5) + assert value(su.temperature) == pytest.approx(303.15, rel=1e-5) + assert value(so.pressure) == pytest.approx(101325, rel=1e-5) + assert value(su.pressure) == pytest.approx(101325, rel=1e-5) + + assert value(lo.temperature) == pytest.approx(310, rel=1e-5) + assert value(lu.temperature) == pytest.approx(310, rel=1e-5) + assert value(lo.pressure) == pytest.approx(1.5e5, rel=1e-5) + assert value(lu.pressure) == pytest.approx(1.5e5, rel=1e-5) @pytest.mark.ui @pytest.mark.unit @@ -424,7 +465,7 @@ def test_get_stream_table_contents(self, model): "mass_frac_comp d": getattr(units.pint_registry, "dimensionless"), }, "Feed Solid": { - "flow_mass": pytest.approx(0.0175, rel=1e-4), + "flow_mass": pytest.approx(0.01225, rel=1e-4), "mass_frac_comp a": pytest.approx(0.2, rel=1e-4), "mass_frac_comp b": pytest.approx(0.8, rel=1e-4), "temperature": pytest.approx(303.15, rel=1e-4), @@ -433,7 +474,7 @@ def test_get_stream_table_contents(self, model): "mass_frac_comp d": float("nan"), }, "Feed Liquid": { - "flow_mass": pytest.approx(0.003, rel=1e-4), + "flow_mass": pytest.approx(0.0021, rel=1e-4), "mass_frac_comp a": float("nan"), "mass_frac_comp b": float("nan"), "temperature": pytest.approx(310, rel=1e-4), @@ -442,7 +483,7 @@ def test_get_stream_table_contents(self, model): "mass_frac_comp d": pytest.approx(0.7, rel=1e-4), }, "Underflow Solid": { - "flow_mass": pytest.approx(0.0125, rel=1e-4), + "flow_mass": pytest.approx(0.010212, rel=1e-4), "mass_frac_comp a": pytest.approx(0.2, rel=1e-4), "mass_frac_comp b": pytest.approx(0.8, rel=1e-4), "temperature": pytest.approx(303.15, rel=1e-4), @@ -451,7 +492,7 @@ def test_get_stream_table_contents(self, model): "mass_frac_comp d": float("nan"), }, "Underflow Liquid": { - "flow_mass": pytest.approx(0.0012417, rel=1e-4), + "flow_mass": pytest.approx(0.00091523, rel=1e-4), "mass_frac_comp a": float("nan"), "mass_frac_comp b": float("nan"), "temperature": pytest.approx(310, rel=1e-4), @@ -460,7 +501,7 @@ def test_get_stream_table_contents(self, model): "mass_frac_comp d": pytest.approx(0.7, rel=1e-4), }, "Overflow Solid": { - "flow_mass": pytest.approx(0.005, rel=1e-4), + "flow_mass": pytest.approx(0.0020381, rel=1e-4), "mass_frac_comp a": pytest.approx(0.2, rel=1e-4), "mass_frac_comp b": pytest.approx(0.8, rel=1e-4), "temperature": pytest.approx(303.15, rel=1e-4), @@ -469,7 +510,7 @@ def test_get_stream_table_contents(self, model): "mass_frac_comp d": float("nan"), }, "Overflow Liquid": { - "flow_mass": pytest.approx(0.0017583, rel=1e-4), + "flow_mass": pytest.approx(0.0011848, rel=1e-4), "mass_frac_comp a": float("nan"), "mass_frac_comp b": float("nan"), "temperature": pytest.approx(310, rel=1e-4), diff --git a/idaes/models/unit_models/solid_liquid/thickener.py b/idaes/models/unit_models/solid_liquid/thickener.py index d2fd6989f5..7065de4f2b 100644 --- a/idaes/models/unit_models/solid_liquid/thickener.py +++ b/idaes/models/unit_models/solid_liquid/thickener.py @@ -284,6 +284,29 @@ def build(self): 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, @@ -354,6 +377,31 @@ def build(self): # --------------------------------------------------------------------------------------------- # 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 + ) + ) + # 2.8 @self.Constraint(self.flowsheet().time) def flux_density_function_overflow(b, t): @@ -377,20 +425,19 @@ def flux_density_function_underflow(b, t): @self.Constraint(self.flowsheet().time) def solids_continuity(b, t): - return b.solid_inlet_state[t].flow_vol * b.solid_fraction_feed[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.solid_split.overflow_state[t].flow_vol + - b.flow_vol_overflow[t] * (b.solid_fraction_overflow[t] - b.solid_fraction_feed[t]) - + b.solid_split.underflow_state[t].flow_vol + + 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.solid_inlet_state[t].flow_vol * b.solid_fraction_feed[t] == ( - +b.solid_split.overflow_state[t].flow_vol * b.solid_fraction_overflow[t] - + b.solid_split.underflow_state[t].flow_vol - * b.solid_fraction_underflow[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) @@ -429,9 +476,16 @@ def underflow_volume_fraction(b, t): @self.Constraint(self.flowsheet().time) def stokes_law(b, t): # Assuming constant properties, source from feed states - return 18 * b.v0[t] * b.liquid_inlet_state[t].visc_d == ( - (b.solid_inlet_state[t].dens_mass - b.liquid_inlet_state[t].dens_mass) - * CONST.acceleration_gravity + 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 ) From 2c816e3a4705830395d5de4098713374b2c25f0d Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Wed, 13 Dec 2023 15:36:30 -0500 Subject: [PATCH 07/10] Expanding reference source to avoid typo failures --- .../generic/unit_models/solid_liquid/thickener0d.rst | 2 +- idaes/models/unit_models/solid_liquid/thickener.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference_guides/model_libraries/generic/unit_models/solid_liquid/thickener0d.rst b/docs/reference_guides/model_libraries/generic/unit_models/solid_liquid/thickener0d.rst index 627eeaded0..98c3ed561c 100644 --- a/docs/reference_guides/model_libraries/generic/unit_models/solid_liquid/thickener0d.rst +++ b/docs/reference_guides/model_libraries/generic/unit_models/solid_liquid/thickener0d.rst @@ -5,7 +5,7 @@ The ``Thickener0D`` unit model is a predictive model for clarifiers and thickene [1] 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 -[2] N.G. Barton, C.-H. Li, S.J. Spencer, Control of a surface of discontinuity in continuous thickeners, J. Aust. Math. Soc. Ser. B 33 (1992) 269–289 +[2] 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 Degrees of Freedom diff --git a/idaes/models/unit_models/solid_liquid/thickener.py b/idaes/models/unit_models/solid_liquid/thickener.py index 7065de4f2b..30a540c3df 100644 --- a/idaes/models/unit_models/solid_liquid/thickener.py +++ b/idaes/models/unit_models/solid_liquid/thickener.py @@ -24,7 +24,7 @@ Settling velocity function from: N.G. Barton, C.-H. Li, S.J. Spencer, Control of a surface of discontinuity in continuous thickeners, -J. Aust. Math. Soc. Ser. B 33 (1992) 269–289 +Journal of the Australian Mathematical Society Series B 33 (1992) 269–289 """ # Import Python libraries From 09fcaf1600c44faa8349dd8e98b1c3784f4509f7 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Wed, 13 Dec 2023 15:44:27 -0500 Subject: [PATCH 08/10] Fixing typos and referencing equations --- idaes/models/unit_models/solid_liquid/thickener.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/idaes/models/unit_models/solid_liquid/thickener.py b/idaes/models/unit_models/solid_liquid/thickener.py index b7d8e8525c..c765d97122 100644 --- a/idaes/models/unit_models/solid_liquid/thickener.py +++ b/idaes/models/unit_models/solid_liquid/thickener.py @@ -224,7 +224,7 @@ def build(self): self.flowsheet().time, doc="Solid properties in separator", **tmp_dict ) - # Add solid spliter + # Add solid splitter self.solid_split = Separator( property_package=self.config.solid_property_package, property_package_args=self.config.solid_property_package_args, @@ -257,7 +257,7 @@ def build(self): self.flowsheet().time, doc="liquid properties in separator", **tmp_dict ) - # Add liquid spliter + # Add liquid splitter self.liquid_split = Separator( property_package=self.config.liquid_property_package, property_package_args=self.config.liquid_property_package_args, @@ -407,7 +407,7 @@ def underflow_flowrate(b, t): ) ) - # 2.8 + # Eqn 2.8 from [1] @self.Constraint(self.flowsheet().time) def flux_density_function_overflow(b, t): u = b.solid_fraction_overflow @@ -418,6 +418,7 @@ def flux_density_function_overflow(b, 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 @@ -428,6 +429,7 @@ def flux_density_function_underflow(b, 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] == ( From 5d7e4cf49a6e8c7a17df3bd28d29b6855ebc469e Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Wed, 13 Dec 2023 16:07:20 -0500 Subject: [PATCH 09/10] Removing unused import --- idaes/models/unit_models/solid_liquid/thickener.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/idaes/models/unit_models/solid_liquid/thickener.py b/idaes/models/unit_models/solid_liquid/thickener.py index c765d97122..9a2a46d6ee 100644 --- a/idaes/models/unit_models/solid_liquid/thickener.py +++ b/idaes/models/unit_models/solid_liquid/thickener.py @@ -32,7 +32,7 @@ from pandas import DataFrame # Import Pyomo libraries -from pyomo.environ import Constraint, Expr_if, inequality, units, Var +from pyomo.environ import Expr_if, inequality, units, Var from pyomo.common.config import ConfigBlock, ConfigValue, In from pyomo.network import Port From e8d4ce7e617816de2cdb457a5abb9759dce6627f Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Wed, 13 Dec 2023 16:09:15 -0500 Subject: [PATCH 10/10] Removing beta warning --- .../solid_liquid/tests/test_thickener.py | 20 ------------------- .../unit_models/solid_liquid/thickener.py | 5 ----- 2 files changed, 25 deletions(-) diff --git a/idaes/models/unit_models/solid_liquid/tests/test_thickener.py b/idaes/models/unit_models/solid_liquid/tests/test_thickener.py index cb1c7b55f0..61e9ae4f73 100644 --- a/idaes/models/unit_models/solid_liquid/tests/test_thickener.py +++ b/idaes/models/unit_models/solid_liquid/tests/test_thickener.py @@ -262,26 +262,6 @@ def get_material_flow_basis(b): # ----------------------------------------------------------------------------- -@pytest.mark.unit -def test_beta_logger(caplog): - m = ConcreteModel() - m.fs = FlowsheetBlock(dynamic=False) - - m.fs.properties = TestParameterBlock() - - m.fs.unit = Thickener0D( - solid_property_package=m.fs.properties, - liquid_property_package=m.fs.properties, - ) - expected = ( - "The Thickener0D model is currently a beta capability and will " - "likely change in the next release as a more predictive version is " - "developed." - ) - - assert expected in caplog.text - - class TestThickener0DBasic: @pytest.fixture(scope="class") def model(self): diff --git a/idaes/models/unit_models/solid_liquid/thickener.py b/idaes/models/unit_models/solid_liquid/thickener.py index 9a2a46d6ee..3af0f24b1e 100644 --- a/idaes/models/unit_models/solid_liquid/thickener.py +++ b/idaes/models/unit_models/solid_liquid/thickener.py @@ -206,11 +206,6 @@ def build(self): Returns: None """ - logger.warning( - "The Thickener0D model is currently a beta capability and will " - "likely change in the next release as a more predictive version is " - "developed." - ) # Call super().build to setup dynamics super().build()