From 4510b03fc706a452e4953b6d369f0135d2b2b660 Mon Sep 17 00:00:00 2001 From: zhuoran29 Date: Sun, 7 Jul 2024 16:36:39 -0400 Subject: [PATCH 01/80] save progress --- .../multi_effect_NaCl_crystallizer.py | 393 +++ .../reflo/property_models/cryst_prop_pack.py | 2157 +++++++++++++++++ .../tests/test_cryst_prop_pack.py | 806 ++++++ .../zero_order/crystallizer_zo_watertap.py | 868 +++++++ .../tests/test_crystallizer_watertap.py | 784 ++++++ 5 files changed, 5008 insertions(+) create mode 100644 src/watertap_contrib/reflo/analysis/flowsheets/multi_effect_NaCl_crystallizer.py create mode 100644 src/watertap_contrib/reflo/property_models/cryst_prop_pack.py create mode 100644 src/watertap_contrib/reflo/property_models/tests/test_cryst_prop_pack.py create mode 100644 src/watertap_contrib/reflo/unit_models/zero_order/crystallizer_zo_watertap.py create mode 100644 src/watertap_contrib/reflo/unit_models/zero_order/tests/test_crystallizer_watertap.py diff --git a/src/watertap_contrib/reflo/analysis/flowsheets/multi_effect_NaCl_crystallizer.py b/src/watertap_contrib/reflo/analysis/flowsheets/multi_effect_NaCl_crystallizer.py new file mode 100644 index 00000000..0ed293bf --- /dev/null +++ b/src/watertap_contrib/reflo/analysis/flowsheets/multi_effect_NaCl_crystallizer.py @@ -0,0 +1,393 @@ +import pandas as pd +import numpy as np +import pytest +from pyomo.environ import ( + ConcreteModel, + TerminationCondition, + SolverStatus, + Objective, + Expression, + maximize, + value, + Set, + Var, + log, + units as pyunits, +) +from pyomo.network import Port +from idaes.core import FlowsheetBlock +import idaes.core.util.scaling as iscale +from pyomo.util.check_units import assert_units_consistent +from watertap_contrib.reflo.unit_models.zero_order.crystallizer_zo_watertap import Crystallization +import watertap_contrib.reflo.property_models.cryst_prop_pack as props +from watertap.unit_models.mvc.components.lmtd_chen_callback import ( + delta_temperature_chen_callback, +) +from idaes.core.solvers import get_solver +from idaes.core.util.model_statistics import ( + degrees_of_freedom, + number_variables, + number_total_constraints, + number_unused_variables, +) +from idaes.models.unit_models import HeatExchanger +from idaes.models.unit_models.heat_exchanger import ( + delta_temperature_lmtd_callback, + delta_temperature_underwood_callback, +) +from idaes.core.util.testing import initialization_tester +from idaes.core.util.scaling import ( + calculate_scaling_factors, + unscaled_variables_generator, + badly_scaled_var_generator, +) +from idaes.core import UnitModelCostingBlock + +from watertap.costing import WaterTAPCosting, CrystallizerCostType + +solver = get_solver() + +def build_fs_multi_effect_crystallizer( + m=None, + # num_effect=3, + operating_pressure_eff1=0.78, # bar + operating_pressure_eff2=0.25, # bar + operating_pressure_eff3=0.208, # bar + operating_pressure_eff4=0.095, # bar + feed_flow_mass=1, # kg/s + feed_mass_frac_NaCl=0.3, + feed_pressure=101325, # Pa + feed_temperature=273.15 + 20, # K + crystallizer_yield=0.5, + steam_pressure=1.5, # bar (gauge pressure) +): + """ + This flowsheet depicts a 4-effect crystallizer, with brine fed in parallel + to each effect, and the operating pressure is specfied individually. + """ + + if m is None: + m = ConcreteModel() + + mfs = m.fs = FlowsheetBlock(dynamic=False) + m.fs.props = props.NaClParameterBlock() + + # Create 4 effects of crystallizer + eff_1 = m.fs.eff_1 = Crystallization(property_package=m.fs.props) + eff_2 = m.fs.eff_2 = Crystallization(property_package=m.fs.props) + eff_3 = m.fs.eff_3 = Crystallization(property_package=m.fs.props) + eff_4 = m.fs.eff_4 = Crystallization(property_package=m.fs.props) + + feed_mass_frac_H2O = 1- feed_mass_frac_NaCl + eps = 1e-6 + + eff_1.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"].fix( + feed_flow_mass * feed_mass_frac_NaCl + ) + eff_1.inlet.flow_mass_phase_comp[0, "Liq", "H2O"].fix( + feed_flow_mass * feed_mass_frac_H2O + ) + + for eff in [eff_1, eff_2, eff_3, eff_4]: + # Define feed for all effects + eff.inlet.flow_mass_phase_comp[0, "Sol", "NaCl"].fix(eps) + eff.inlet.flow_mass_phase_comp[0, "Vap", "H2O"].fix(eps) + + eff.inlet.pressure[0].fix(feed_pressure) + eff.inlet.temperature[0].fix(feed_temperature) + + # Fix growth rate, crystal length and Sounders brown constant to default values + eff.crystal_growth_rate.fix() + eff.souders_brown_constant.fix() + eff.crystal_median_length.fix() + + # Fix yield + eff.crystallization_yield["NaCl"].fix(crystallizer_yield) + + # Define operating conditions + m.fs.eff_1.pressure_operating.fix(operating_pressure_eff1 * pyunits.bar) + m.fs.eff_2.pressure_operating.fix(operating_pressure_eff2 * pyunits.bar) + m.fs.eff_3.pressure_operating.fix(operating_pressure_eff3 * pyunits.bar) + m.fs.eff_4.pressure_operating.fix(operating_pressure_eff4 * pyunits.bar) + + add_heat_exchanger_eff2(m) + add_heat_exchanger_eff3(m) + add_heat_exchanger_eff4(m) + return m + + +def add_heat_exchanger_eff2(m): + eff_1 = m.fs.eff_1 + eff_2 = m.fs.eff_2 + + eff_2.delta_temperature_in = Var( + eff_2.flowsheet().time, + initialize=35, + bounds=(None, None), + units=pyunits.K, + doc="Temperature differnce at the inlet side", + ) + eff_2.delta_temperature_out = Var( + eff_2.flowsheet().time, + initialize=35, + bounds=(None, None), + units=pyunits.K, + doc="Temperature differnce at the outlet side", + ) + delta_temperature_chen_callback(eff_2) + + eff_2.area = Var( + bounds=(0, None), + initialize=1000.0, + doc="Heat exchange area", + units=pyunits.m**2, + ) + + eff_2.overall_heat_transfer_coefficient = Var( + eff_2.flowsheet().time, + bounds=(0, None), + initialize=100.0, + doc="Overall heat transfer coefficient", + units=pyunits.W / pyunits.m**2 / pyunits.K, + ) + + eff_2.overall_heat_transfer_coefficient[0].fix(100) + + @m.Constraint(eff_2.flowsheet().time, doc="delta_temperature_in at the 2nd effect") + def delta_temperature_in_eff2(b, t): + return ( + b.fs.eff_2.delta_temperature_in[t] + == b.fs.eff_1.properties_vapor[0].temperature + - b.fs.eff_2.temperature_operating + ) + + @m.Constraint(eff_2.flowsheet().time, doc="delta_temperature_out at the 2nd effect") + def delta_temperature_out_eff2(b, t): + return ( + b.fs.eff_2.delta_temperature_out[t] + == b.fs.eff_1.properties_pure_water[0].temperature + - b.fs.eff_2.properties_in[0].temperature + ) + + @m.Constraint(eff_2.flowsheet().time) + def heat_transfer_equation_eff_2(b, t): + return b.fs.eff_1.energy_flow_superheated_vapor == ( + b.fs.eff_2.overall_heat_transfer_coefficient[t] + * b.fs.eff_2.area + * b.fs.eff_2.delta_temperature[0] + ) + + iscale.set_scaling_factor(eff_2.delta_temperature_in, 1e-1) + iscale.set_scaling_factor(eff_2.delta_temperature_out, 1e-1) + iscale.set_scaling_factor(eff_2.area, 1e-1) + iscale.set_scaling_factor(eff_2.overall_heat_transfer_coefficient, 1e-1) + +def add_heat_exchanger_eff3(m): + eff_2 = m.fs.eff_2 + eff_3 = m.fs.eff_3 + + eff_3.delta_temperature_in = Var( + eff_3.flowsheet().time, + initialize=35, + bounds=(None, None), + units=pyunits.K, + doc="Temperature differnce at the inlet side", + ) + eff_3.delta_temperature_out = Var( + eff_3.flowsheet().time, + initialize=35, + bounds=(None, None), + units=pyunits.K, + doc="Temperature differnce at the outlet side", + ) + delta_temperature_chen_callback(eff_3) + + eff_3.area = Var( + bounds=(0, None), + initialize=1000.0, + doc="Heat exchange area", + units=pyunits.m**2, + ) + + eff_3.overall_heat_transfer_coefficient = Var( + eff_3.flowsheet().time, + bounds=(0, None), + initialize=100.0, + doc="Overall heat transfer coefficient", + units=pyunits.W / pyunits.m**2 / pyunits.K, + ) + + eff_3.overall_heat_transfer_coefficient[0].fix(100) + + @m.Constraint(eff_3.flowsheet().time, doc="delta_temperature_in at the 2nd effect") + def delta_temperature_in_eff3(b, t): + return ( + eff_3.delta_temperature_in[t] + == eff_2.properties_vapor[0].temperature + - eff_3.temperature_operating + ) + + @m.Constraint(eff_3.flowsheet().time, doc="delta_temperature_out at the 2nd effect") + def delta_temperature_out_eff3(b, t): + return ( + eff_3.delta_temperature_out[t] + == eff_2.properties_pure_water[0].temperature + - eff_3.properties_in[0].temperature + ) + + @m.Constraint(eff_3.flowsheet().time) + def heat_transfer_equation_eff3(b, t): + return eff_2.energy_flow_superheated_vapor == ( + eff_3.overall_heat_transfer_coefficient[t] + * eff_3.area + * eff_3.delta_temperature[0] + ) + + iscale.set_scaling_factor(eff_3.delta_temperature_in, 1e-1) + iscale.set_scaling_factor(eff_3.delta_temperature_out, 1e-1) + iscale.set_scaling_factor(eff_3.area, 1e-1) + iscale.set_scaling_factor(eff_3.overall_heat_transfer_coefficient, 1e-1) + + +def add_heat_exchanger_eff4(m): + eff_3 = m.fs.eff_3 + eff_4 = m.fs.eff_4 + + eff_4.delta_temperature_in = Var( + eff_4.flowsheet().time, + initialize=35, + bounds=(None, None), + units=pyunits.K, + doc="Temperature differnce at the inlet side", + ) + eff_4.delta_temperature_out = Var( + eff_4.flowsheet().time, + initialize=35, + bounds=(None, None), + units=pyunits.K, + doc="Temperature differnce at the outlet side", + ) + delta_temperature_chen_callback(eff_4) + + eff_4.area = Var( + bounds=(0, None), + initialize=1000.0, + doc="Heat exchange area", + units=pyunits.m**2, + ) + + eff_4.overall_heat_transfer_coefficient = Var( + eff_4.flowsheet().time, + bounds=(0, None), + initialize=100.0, + doc="Overall heat transfer coefficient", + units=pyunits.W / pyunits.m**2 / pyunits.K, + ) + + eff_4.overall_heat_transfer_coefficient[0].fix(100) + + @m.Constraint(eff_4.flowsheet().time, doc="delta_temperature_in at the 2nd effect") + def delta_temperature_in_eff4(b, t): + return ( + eff_4.delta_temperature_in[t] + == eff_3.properties_vapor[0].temperature + - eff_4.temperature_operating + ) + + @m.Constraint(eff_4.flowsheet().time, doc="delta_temperature_out at the 2nd effect") + def delta_temperature_out_eff4(b, t): + return ( + eff_4.delta_temperature_out[t] + == eff_3.properties_pure_water[0].temperature + - eff_4.properties_in[0].temperature + ) + + @m.Constraint(eff_4.flowsheet().time) + def heat_transfer_equation_eff4(b, t): + return eff_3.energy_flow_superheated_vapor == ( + eff_4.overall_heat_transfer_coefficient[t] + * eff_4.area + * eff_4.delta_temperature[0] + ) + + iscale.set_scaling_factor(eff_4.delta_temperature_in, 1e-1) + iscale.set_scaling_factor(eff_4.delta_temperature_out, 1e-1) + iscale.set_scaling_factor(eff_4.area, 1e-1) + iscale.set_scaling_factor(eff_4.overall_heat_transfer_coefficient, 1e-1) + +def multi_effect_crystallizer_initialization(m): + # Set scaling factors + m.fs.props.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Liq", "H2O") + ) + m.fs.props.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Liq", "NaCl") + ) + m.fs.props.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Vap", "H2O") + ) + m.fs.props.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Sol", "NaCl") + ) + + calculate_scaling_factors(m) + + m.fs.eff_1.initialize() + m.fs.eff_2.initialize() + m.fs.eff_3.initialize() + m.fs.eff_4.initialize() + + # Unfix dof + brine_salinity = m.fs.eff_1.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"].value + + for eff in [m.fs.eff_2, m.fs.eff_3, m.fs.eff_4]: + eff.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"].unfix() + eff.inlet.flow_mass_phase_comp[0, "Liq", "H2O"].unfix() + eff.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"].fix(brine_salinity) + + # Energy is provided from the previous effect + @m.Constraint(doc="Energy supplied to the 2nd effect") + def eqn_energy_from_eff1(b): + return b.fs.eff_2.work_mechanical[0] == b.fs.eff_1.energy_flow_superheated_vapor + + @m.Constraint(doc="Energy supplied to the 3rd effect") + def eqn_energy_from_eff2(b): + return b.fs.eff_3.work_mechanical[0] == b.fs.eff_2.energy_flow_superheated_vapor + + @m.Constraint(doc="Energy supplied to the 4th effect") + def eqn_energy_from_eff3(b): + return b.fs.eff_4.work_mechanical[0] == b.fs.eff_3.energy_flow_superheated_vapor + + +if __name__ == "__main__": + m = build_fs_multi_effect_crystallizer( + operating_pressure_eff1=0.45, # bar + operating_pressure_eff2=0.25, # bar + operating_pressure_eff3=0.208, # bar + operating_pressure_eff4=0.095, # bar + feed_flow_mass=1, # kg/s + feed_mass_frac_NaCl=0.3, + feed_pressure=101325, # Pa + feed_temperature=273.15 + 20, # K + crystallizer_yield=0.5, + steam_pressure=1.5, # bar (gauge pressure) + ) + + multi_effect_crystallizer_initialization(m) + + print(degrees_of_freedom(m)) + solver = get_solver() + + results = solver.solve(m) + + assert results.solver.termination_condition == TerminationCondition.optimal + assert results.solver.status == SolverStatus.ok + + print(m.fs.eff_1.energy_flow_superheated_vapor.value) + print(m.fs.eff_2.energy_flow_superheated_vapor.value) + # print(m.fs.eff_3.energy_flow_superheated_vapor.value) + # print(m.fs.eff_4.energy_flow_superheated_vapor.value) + print(m.fs.eff_1.temperature_operating.value - 273.15) + print(m.fs.eff_2.temperature_operating.value - 273.15) + # print(m.fs.eff_3.temperature_operating.value - 273.15) + # print(m.fs.eff_4.temperature_operating.value - 273.15) diff --git a/src/watertap_contrib/reflo/property_models/cryst_prop_pack.py b/src/watertap_contrib/reflo/property_models/cryst_prop_pack.py new file mode 100644 index 00000000..83ee60c6 --- /dev/null +++ b/src/watertap_contrib/reflo/property_models/cryst_prop_pack.py @@ -0,0 +1,2157 @@ +################################################################################# +# WaterTAP Copyright (c) 2020-2024, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# National Renewable Energy Laboratory, and National Energy Technology +# Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +################################################################################# +""" +Initial crystallization property package for H2O-NaCl system +""" + +# Import Python libraries +import idaes.logger as idaeslog + +from enum import Enum, auto + +# Import Pyomo libraries +from pyomo.environ import ( + Constraint, + Expression, + Reals, + NonNegativeReals, + Var, + Param, + exp, + log, + value, + check_optimal_termination, +) +from pyomo.environ import units as pyunits +from pyomo.common.config import ConfigValue, In + +# Import IDAES cores +from idaes.core import ( + declare_process_block_class, + MaterialFlowBasis, + PhysicalParameterBlock, + StateBlockData, + StateBlock, + MaterialBalanceType, + EnergyBalanceType, +) +from idaes.core.base.components import Solute, Solvent +from idaes.core.base.phases import ( + LiquidPhase, + VaporPhase, + SolidPhase, + PhaseType as PT, +) +from idaes.core.util.constants import Constants +from idaes.core.util.initialization import ( + fix_state_vars, + revert_state_vars, + solve_indexed_blocks, +) +from idaes.core.util.misc import extract_data +from watertap.core.solvers import get_solver +from idaes.core.util.model_statistics import ( + degrees_of_freedom, + number_unfixed_variables, +) +from idaes.core.util.exceptions import ( + ConfigurationError, + InitializationError, + PropertyPackageError, +) +import idaes.core.util.scaling as iscale + + +# Set up logger +_log = idaeslog.getLogger(__name__) + +__author__ = "Oluwamayowa Amusat" + + +class HeatOfCrystallizationModel(Enum): + constant = auto() # Use constant heat of crystallization + zero = auto() # Assume heat of crystallization is zero + temp_dependent = auto() # Use temperature-dependent heat of crystallization + + +@declare_process_block_class("NaClParameterBlock") +class NaClParameterData(PhysicalParameterBlock): + CONFIG = PhysicalParameterBlock.CONFIG() + + CONFIG.declare( + "heat_of_crystallization_model", + ConfigValue( + default=HeatOfCrystallizationModel.constant, + domain=In(HeatOfCrystallizationModel), + description="Heat of crystallization construction flag", + doc=""" + Options to account for heat of crystallization value for NaCl. + + **default** - ``HeatOfCrystallizationModel.constant`` + + .. csv-table:: + :header: "Configuration Options", "Description" + + "``HeatOfCrystallizationModel.constant``", "Fixed heat of crystallization for NaCl based on literature" + "``HeatOfCrystallizationModel.zero``", "Zero heat of crystallization assumption" + "``HeatOfCrystallizationModel.temp_dependent``", "Temperature-dependent heat of crystallization for NaCl" + """, + ), + ) + + def build(self): + super().build() + self._state_block_class = NaClStateBlock + + # Component + self.H2O = Solvent(valid_phase_types=[PT.liquidPhase, PT.vaporPhase]) + self.NaCl = Solute(valid_phase_types=[PT.liquidPhase, PT.solidPhase]) + + # Phases + self.Liq = LiquidPhase(component_list=["H2O", "NaCl"]) + self.Vap = VaporPhase(component_list=["H2O"]) + self.Sol = SolidPhase(component_list=["NaCl"]) + + """ + References + This package was developed from the following references: + + - K.G.Nayar, M.H.Sharqawy, L.D.Banchik, and J.H.Lienhard V, "Thermophysical properties of seawater: A review and + new correlations that include pressure dependence,"Desalination, Vol.390, pp.1 - 24, 2016. + doi: 10.1016/j.desal.2016.02.024(preprint) + + - Mostafa H.Sharqawy, John H.Lienhard V, and Syed M.Zubair, "Thermophysical properties of seawater: A review of + existing correlations and data,"Desalination and Water Treatment, Vol.16, pp.354 - 380, April 2010. + (2017 corrections provided at http://web.mit.edu/seawater) + + - Laliberté, M. & Cooper, W. E. Model for Calculating the Density of Aqueous Electrolyte Solutions + Journal of Chemical & Engineering Data, American Chemical Society (ACS), 2004, 49, 1141-1151. + + - Laliberté, M. A Model for Calculating the Heat Capacity of Aqueous Solutions, with Updated Density and Viscosity Data + Journal of Chemical & Engineering Data, American Chemical Society (ACS), 2009, 54, 1725-1760 + Liquid NaCl heat capacity and density parameters available https://pubs.acs.org/doi/10.1021/je8008123 + + - Sparrow, B. S. Empirical equations for the thermodynamic properties of aqueous sodium chloride + Desalination, Elsevier BV, 2003, 159, 161-170 + + - Chase, M.W., Jr., NIST-JANAF Themochemical Tables, Fourth Edition, J. Phys. Chem. Ref. Data, Monograph 9, + 1998, 1-1951. + + - El-Dessouky, H. T. & Ettouney, H. M. (Eds.). Appendix A - Thermodynamic Properties. + Fundamentals of Salt Water Desalination, Elsevier Science B.V., 2002, 525-563 + + - Tavare, N. S. Industrial Crystallization, Springer US, 2013. + """ + + # Unit definitions + dens_units = pyunits.kg / pyunits.m**3 + t_inv_units = pyunits.K**-1 + enth_mass_units = pyunits.J / pyunits.kg + enth_mass_units_2 = pyunits.kJ / pyunits.kg + cp_units = pyunits.J / (pyunits.kg * pyunits.K) + cp_units_2 = pyunits.kJ / (pyunits.kg * pyunits.K) + cp_units_3 = pyunits.J / (pyunits.mol * pyunits.K) + + # molecular weights of solute and solvent + mw_comp_data = {"H2O": 18.01528e-3, "NaCl": 58.44e-3} + self.mw_comp = Param( + self.component_list, + initialize=extract_data(mw_comp_data), + units=pyunits.kg / pyunits.mol, + doc="Molecular weight kg/mol", + ) + + # Solubility parameters from (1) surrogate model (2) Sparrow paper + # # 1. Surrogate + # self.sol_param_A1 = Param(initialize= 526.706475, units = pyunits.g / pyunits.L, doc=' Solubility parameter A1 [g/L] for NaCl surrogate') + # self.sol_param_A2 = Param(initialize= -1.326952, units = (pyunits.g / pyunits.L) * pyunits.K ** -1, doc=' Solubility parameter A2 [g/L] for NaCl surrogate') + # self.sol_param_A3 = Param(initialize= 0.002574, units = (pyunits.g / pyunits.L) * pyunits.K ** -2, doc=' Solubility parameter A3 [g/L] for NaCl surrogate') + # 2. Sparrow + self.sol_param_A1 = Param( + initialize=0.2628, + units=pyunits.dimensionless, + doc=" Solubility parameter A1 for NaCl", + ) + self.sol_param_A2 = Param( + initialize=62.75e-6, + units=pyunits.K**-1, + doc=" Solubility parameter A2 for NaCl", + ) + self.sol_param_A3 = Param( + initialize=1.084e-6, + units=pyunits.K**-2, + doc=" Solubility parameter A3 for NaCl", + ) + + # Mass density value for NaCl crystals in solid phase: fixed for now at Tavare value - may not be accurate? + self.dens_mass_param_NaCl = Param( + initialize=2115, + units=pyunits.kg / pyunits.m**3, + doc="NaCl crystal density", + ) + + # Heat of crystallization parameter - fixed value based on heat of fusion from Perry (Table 2-147) + self.dh_crystallization_param = Param( + initialize=-520, units=enth_mass_units_2, doc="NaCl heat of crystallization" + ) + + # Mass density parameters for pure NaCl liquid based on Eq. 9 in Laliberte and Cooper (2004). + self.dens_mass_param_NaCl_liq_C0 = Var( + within=Reals, + initialize=-0.00433, + units=dens_units, + doc="Mass density parameter C0 for liquid NaCl", + ) + self.dens_mass_param_NaCl_liq_C1 = Var( + within=Reals, + initialize=0.06471, + units=dens_units, + doc="Mass density parameter C1 for liquid NaCl", + ) + self.dens_mass_param_NaCl_liq_C2 = Var( + within=Reals, + initialize=1.01660, + units=pyunits.dimensionless, + doc="Mass density parameter C2 for liquid NaCl", + ) + self.dens_mass_param_NaCl_liq_C3 = Var( + within=Reals, + initialize=0.014624, + units=t_inv_units, + doc="Mass density parameter C3 for liquid NaCl", + ) + self.dens_mass_param_NaCl_liq_C4 = Var( + within=Reals, + initialize=3315.6, + units=pyunits.K, + doc="Mass density parameter C4 for liquid NaCl", + ) + + # Mass density parameters for solvent in liquid phase, eq. 8 in Sharqawy et al. (2010) + self.dens_mass_param_A1 = Var( + within=Reals, + initialize=9.999e2, + units=dens_units, + doc="Mass density parameter A1", + ) + self.dens_mass_param_A2 = Var( + within=Reals, + initialize=2.034e-2, + units=dens_units * t_inv_units, + doc="Mass density parameter A2", + ) + self.dens_mass_param_A3 = Var( + within=Reals, + initialize=-6.162e-3, + units=dens_units * t_inv_units**2, + doc="Mass density parameter A3", + ) + self.dens_mass_param_A4 = Var( + within=Reals, + initialize=2.261e-5, + units=dens_units * t_inv_units**3, + doc="Mass density parameter A4", + ) + self.dens_mass_param_A5 = Var( + within=Reals, + initialize=-4.657e-8, + units=dens_units * t_inv_units**4, + doc="Mass density parameter A5", + ) + + # Latent heat of evaporation of pure water: Parameters from Sharqawy et al. (2010), eq. 54 + self.dh_vap_w_param_0 = Var( + within=Reals, + initialize=2.501e6, + units=enth_mass_units, + doc="Latent heat of pure water parameter 0", + ) + self.dh_vap_w_param_1 = Var( + within=Reals, + initialize=-2.369e3, + units=enth_mass_units * t_inv_units**1, + doc="Latent heat of pure water parameter 1", + ) + self.dh_vap_w_param_2 = Var( + within=Reals, + initialize=2.678e-1, + units=enth_mass_units * t_inv_units**2, + doc="Latent heat of pure water parameter 2", + ) + self.dh_vap_w_param_3 = Var( + within=Reals, + initialize=-8.103e-3, + units=enth_mass_units * t_inv_units**3, + doc="Latent heat of pure water parameter 3", + ) + self.dh_vap_w_param_4 = Var( + within=Reals, + initialize=-2.079e-5, + units=enth_mass_units * t_inv_units**4, + doc="Latent heat of pure water parameter 4", + ) + + # Specific heat parameters for Cp vapor from NIST Webbook - Chase, M.W., Jr., NIST-JANAF Themochemical Tables + self.cp_vap_param_A = Var( + within=Reals, + initialize=30.09200 / 18.01528e-3, + units=cp_units, + doc="Specific heat of water vapor parameter A", + ) + self.cp_vap_param_B = Var( + within=Reals, + initialize=6.832514 / 18.01528e-3, + units=cp_units * t_inv_units, + doc="Specific heat of water vapor parameter B", + ) + self.cp_vap_param_C = Var( + within=Reals, + initialize=6.793435 / 18.01528e-3, + units=cp_units * t_inv_units**2, + doc="Specific heat of water vapor parameter C", + ) + self.cp_vap_param_D = Var( + within=Reals, + initialize=-2.534480 / 18.01528e-3, + units=cp_units * t_inv_units**3, + doc="Specific heat of water vapor parameter D", + ) + self.cp_vap_param_E = Var( + within=Reals, + initialize=0.082139 / 18.01528e-3, + units=cp_units * t_inv_units**-2, + doc="Specific heat of water vapor parameter E", + ) + + # Specific heat parameters for pure water from eq (9) in Sharqawy et al. (2010) + self.cp_phase_param_A1 = Var( + within=Reals, + initialize=5.328, + units=cp_units, + doc="Specific heat of seawater parameter A1", + ) + self.cp_phase_param_B1 = Var( + within=Reals, + initialize=-6.913e-3, + units=cp_units * t_inv_units, + doc="Specific heat of seawater parameter B1", + ) + self.cp_phase_param_C1 = Var( + within=Reals, + initialize=9.6e-6, + units=cp_units * t_inv_units**2, + doc="Specific heat of seawater parameter C1", + ) + self.cp_phase_param_D1 = Var( + within=Reals, + initialize=2.5e-9, + units=cp_units * t_inv_units**3, + doc="Specific heat of seawater parameter D1", + ) + + # Specific heat parameters for liquid NaCl from eqs. (11) & (12) in Laliberte (2009). + self.cp_param_NaCl_liq_A1 = Var( + within=Reals, + initialize=-0.06936, + units=cp_units_2, + doc="Specific heat parameter A1 for liquid NaCl", + ) + self.cp_param_NaCl_liq_A2 = Var( + within=Reals, + initialize=-0.07821, + units=t_inv_units, + doc="Specific heat parameter A2 for liquid NaCl", + ) + self.cp_param_NaCl_liq_A3 = Var( + within=Reals, + initialize=3.8480, + units=pyunits.dimensionless, + doc="Specific heat parameter A3 for liquid NaCl", + ) + self.cp_param_NaCl_liq_A4 = Var( + within=Reals, + initialize=-11.2762, + units=pyunits.dimensionless, + doc="Specific heat parameter A4 for liquid NaCl", + ) + self.cp_param_NaCl_liq_A5 = Var( + within=Reals, + initialize=8.7319, + units=cp_units_2, + doc="Specific heat parameter A5 for liquid NaCl", + ) + self.cp_param_NaCl_liq_A6 = Var( + within=Reals, + initialize=1.8125, + units=pyunits.dimensionless, + doc="Specific heat parameter A6 for liquid NaCl", + ) + + # Specific heat parameters for solid NaCl : Shomate equation from NIST webbook (https://webbook.nist.gov/cgi/cbook.cgi?ID=C7647145&Mask=6F). + self.cp_param_NaCl_solid_A = Var( + within=Reals, + initialize=50.72389, + units=cp_units_3, + doc="Specific heat parameter A for solid NaCl", + ) + self.cp_param_NaCl_solid_B = Var( + within=Reals, + initialize=6.672267, + units=cp_units_3 / pyunits.K, + doc="Specific heat parameter B for solid NaCl", + ) + self.cp_param_NaCl_solid_C = Var( + within=Reals, + initialize=-2.517167, + units=cp_units_3 / pyunits.K**2, + doc="Specific heat parameter C for solid NaCl", + ) + self.cp_param_NaCl_solid_D = Var( + within=Reals, + initialize=10.15934, + units=cp_units_3 / pyunits.K**3, + doc="Specific heat parameter D for solid NaCl", + ) + self.cp_param_NaCl_solid_E = Var( + within=Reals, + initialize=-0.200675, + units=cp_units_3 * pyunits.K**2, + doc="Specific heat parameter E for solid NaCl", + ) + self.cp_param_NaCl_solid_F = Var( + within=Reals, + initialize=-427.2115, + units=cp_units_3 * pyunits.K, + doc="Specific heat parameter F for solid NaCl", + ) + # self.cp_param_NaCl_solid_G = Var(within=Reals, initialize=130.3973, units=cp_units_2, doc='Specific heat parameter G for solid NaCl') + self.cp_param_NaCl_solid_H = Var( + within=Reals, + initialize=-411.1203, + units=cp_units_3 * pyunits.K, + doc="Specific heat parameter H for solid NaCl", + ) + + # Vapour pressure parameters for NaCl solution from Sparrow (2003): 0 < T < 150 degC + self.pressure_sat_param_A1 = Var( + within=Reals, + initialize=0.9083e-3, + units=pyunits.MPa, + doc="Vapour pressure parameter A1", + ) + self.pressure_sat_param_A2 = Var( + within=Reals, + initialize=-0.569e-3, + units=pyunits.MPa, + doc="Vapour pressure parameter A2", + ) + self.pressure_sat_param_A3 = Var( + within=Reals, + initialize=0.1945e-3, + units=pyunits.MPa, + doc="Vapour pressure parameter A3", + ) + self.pressure_sat_param_A4 = Var( + within=Reals, + initialize=-3.736e-3, + units=pyunits.MPa, + doc="Vapour pressure parameter A4", + ) + self.pressure_sat_param_A5 = Var( + within=Reals, + initialize=2.82e-3, + units=pyunits.MPa, + doc="Vapour pressure parameter A5", + ) + self.pressure_sat_param_B1 = Var( + within=Reals, + initialize=-0.0669e-3, + units=pyunits.MPa / pyunits.K, + doc="Vapour pressure parameter B1", + ) + self.pressure_sat_param_B2 = Var( + within=Reals, + initialize=0.0582e-3, + units=pyunits.MPa / pyunits.K, + doc="Vapour pressure parameter B2", + ) + self.pressure_sat_param_B3 = Var( + within=Reals, + initialize=-0.1668e-3, + units=pyunits.MPa / pyunits.K, + doc="Vapour pressure parameter B3", + ) + self.pressure_sat_param_B4 = Var( + within=Reals, + initialize=0.6761e-3, + units=pyunits.MPa / pyunits.K, + doc="Vapour pressure parameter B4", + ) + self.pressure_sat_param_B5 = Var( + within=Reals, + initialize=-2.091e-3, + units=pyunits.MPa / pyunits.K, + doc="Vapour pressure parameter B5", + ) + self.pressure_sat_param_C1 = Var( + within=Reals, + initialize=7.541e-6, + units=pyunits.MPa / pyunits.K**2, + doc="Vapour pressure parameter C1", + ) + self.pressure_sat_param_C2 = Var( + within=Reals, + initialize=-5.143e-6, + units=pyunits.MPa / pyunits.K**2, + doc="Vapour pressure parameter C2", + ) + self.pressure_sat_param_C3 = Var( + within=Reals, + initialize=6.482e-6, + units=pyunits.MPa / pyunits.K**2, + doc="Vapour pressure parameter C3", + ) + self.pressure_sat_param_C4 = Var( + within=Reals, + initialize=-52.62e-6, + units=pyunits.MPa / pyunits.K**2, + doc="Vapour pressure parameter C4", + ) + self.pressure_sat_param_C5 = Var( + within=Reals, + initialize=115.7e-6, + units=pyunits.MPa / pyunits.K**2, + doc="Vapour pressure parameter C5", + ) + self.pressure_sat_param_D1 = Var( + within=Reals, + initialize=-0.0922e-6, + units=pyunits.MPa / pyunits.K**3, + doc="Vapour pressure parameter D1", + ) + self.pressure_sat_param_D2 = Var( + within=Reals, + initialize=0.0649e-6, + units=pyunits.MPa / pyunits.K**3, + doc="Vapour pressure parameter D2", + ) + self.pressure_sat_param_D3 = Var( + within=Reals, + initialize=-0.1313e-6, + units=pyunits.MPa / pyunits.K**3, + doc="Vapour pressure parameter D3", + ) + self.pressure_sat_param_D4 = Var( + within=Reals, + initialize=0.8024e-6, + units=pyunits.MPa / pyunits.K**3, + doc="Vapour pressure parameter D4", + ) + self.pressure_sat_param_D5 = Var( + within=Reals, + initialize=-1.986e-6, + units=pyunits.MPa / pyunits.K**3, + doc="Vapour pressure parameter D5", + ) + self.pressure_sat_param_E1 = Var( + within=Reals, + initialize=1.237e-9, + units=pyunits.MPa / pyunits.K**4, + doc="Vapour pressure parameter E1", + ) + self.pressure_sat_param_E2 = Var( + within=Reals, + initialize=-0.753e-9, + units=pyunits.MPa / pyunits.K**4, + doc="Vapour pressure parameter E2", + ) + self.pressure_sat_param_E3 = Var( + within=Reals, + initialize=0.1448e-9, + units=pyunits.MPa / pyunits.K**4, + doc="Vapour pressure parameter E3", + ) + self.pressure_sat_param_E4 = Var( + within=Reals, + initialize=-6.964e-9, + units=pyunits.MPa / pyunits.K**4, + doc="Vapour pressure parameter E4", + ) + self.pressure_sat_param_E5 = Var( + within=Reals, + initialize=14.61e-9, + units=pyunits.MPa / pyunits.K**4, + doc="Vapour pressure parameter E5", + ) + + # Parameters for saturation temperature of water vapour from eq. A.12 in El-Dessouky and Ettouney + self.temp_sat_solvent_A1 = Var( + within=Reals, + initialize=42.6776, + units=pyunits.K, + doc="Water boiling point parameter A1", + ) + self.temp_sat_solvent_A2 = Var( + within=Reals, + initialize=-3892.7, + units=pyunits.K, + doc="Water boiling point parameter A2", + ) + self.temp_sat_solvent_A3 = Var( + within=Reals, + initialize=1000, + units=pyunits.kPa, + doc="Water boiling point parameter A3", + ) + self.temp_sat_solvent_A4 = Var( + within=Reals, + initialize=-9.48654, + units=pyunits.dimensionless, + doc="Water boiling point parameter A4", + ) + + # Parameters for specific enthalpy of pure water in liquid phase from eq. 55 in Sharqawy et al. (2010) + self.enth_mass_solvent_param_A1 = Var( + within=Reals, + initialize=141.355, + units=enth_mass_units, + doc="Specific enthalpy parameter A1", + ) + self.enth_mass_solvent_param_A2 = Var( + within=Reals, + initialize=4202.07, + units=enth_mass_units * t_inv_units, + doc="Specific enthalpy parameter A2", + ) + self.enth_mass_solvent_param_A3 = Var( + within=Reals, + initialize=-0.535, + units=enth_mass_units * t_inv_units**2, + doc="Specific enthalpy parameter A3", + ) + self.enth_mass_solvent_param_A4 = Var( + within=Reals, + initialize=0.004, + units=enth_mass_units * t_inv_units**3, + doc="Specific enthalpy parameter A4", + ) + + # Enthalpy parameters for NaCl solution from Sparrow (2003): 0 < T < 300 degC + self.enth_phase_param_A1 = Var( + within=Reals, + initialize=0.0005e3, + units=enth_mass_units_2, + doc="Solution enthalpy parameter A1", + ) + self.enth_phase_param_A2 = Var( + within=Reals, + initialize=0.0378e3, + units=enth_mass_units_2, + doc="Solution enthalpy parameter A2", + ) + self.enth_phase_param_A3 = Var( + within=Reals, + initialize=-0.3682e3, + units=enth_mass_units_2, + doc="Solution enthalpy parameter A3", + ) + self.enth_phase_param_A4 = Var( + within=Reals, + initialize=-0.6529e3, + units=enth_mass_units_2, + doc="Solution enthalpy parameter A4", + ) + self.enth_phase_param_A5 = Var( + within=Reals, + initialize=2.89e3, + units=enth_mass_units_2, + doc="Solution enthalpy parameter A5", + ) + self.enth_phase_param_B1 = Var( + within=Reals, + initialize=4.145, + units=enth_mass_units_2 / pyunits.K, + doc="Solution enthalpy parameter B1", + ) + self.enth_phase_param_B2 = Var( + within=Reals, + initialize=-4.973, + units=enth_mass_units_2 / pyunits.K, + doc="Solution enthalpy parameter B2", + ) + self.enth_phase_param_B3 = Var( + within=Reals, + initialize=4.482, + units=enth_mass_units_2 / pyunits.K, + doc="Solution enthalpy parameter B3", + ) + self.enth_phase_param_B4 = Var( + within=Reals, + initialize=18.31, + units=enth_mass_units_2 / pyunits.K, + doc="Solution enthalpy parameter B4", + ) + self.enth_phase_param_B5 = Var( + within=Reals, + initialize=-46.41, + units=enth_mass_units_2 / pyunits.K, + doc="Solution enthalpy parameter B5", + ) + self.enth_phase_param_C1 = Var( + within=Reals, + initialize=0.0007, + units=enth_mass_units_2 / pyunits.K**2, + doc="Solution enthalpy parameter C1", + ) + self.enth_phase_param_C2 = Var( + within=Reals, + initialize=-0.0059, + units=enth_mass_units_2 / pyunits.K**2, + doc="Solution enthalpy parameter C2", + ) + self.enth_phase_param_C3 = Var( + within=Reals, + initialize=0.0854, + units=enth_mass_units_2 / pyunits.K**2, + doc="Solution enthalpy parameter C3", + ) + self.enth_phase_param_C4 = Var( + within=Reals, + initialize=-0.4951, + units=enth_mass_units_2 / pyunits.K**2, + doc="Solution enthalpy parameter C4", + ) + self.enth_phase_param_C5 = Var( + within=Reals, + initialize=0.8255, + units=enth_mass_units_2 / pyunits.K**2, + doc="Solution enthalpy parameter C5", + ) + self.enth_phase_param_D1 = Var( + within=Reals, + initialize=-0.0048e-3, + units=enth_mass_units_2 / pyunits.K**3, + doc="Solution enthalpy parameter D1", + ) + self.enth_phase_param_D2 = Var( + within=Reals, + initialize=0.0639e-3, + units=enth_mass_units_2 / pyunits.K**3, + doc="Solution enthalpy parameter D2", + ) + self.enth_phase_param_D3 = Var( + within=Reals, + initialize=-0.714e-3, + units=enth_mass_units_2 / pyunits.K**3, + doc="Solution enthalpy parameter D3", + ) + self.enth_phase_param_D4 = Var( + within=Reals, + initialize=3.273e-3, + units=enth_mass_units_2 / pyunits.K**3, + doc="Solution enthalpy parameter D4", + ) + self.enth_phase_param_D5 = Var( + within=Reals, + initialize=-4.85e-3, + units=enth_mass_units_2 / pyunits.K**3, + doc="Solution enthalpy parameter D5", + ) + self.enth_phase_param_E1 = Var( + within=Reals, + initialize=0.0202e-6, + units=enth_mass_units_2 / pyunits.K**4, + doc="Solution enthalpy parameter E1", + ) + self.enth_phase_param_E2 = Var( + within=Reals, + initialize=-0.2432e-6, + units=enth_mass_units_2 / pyunits.K**4, + doc="Solution enthalpy parameter E2", + ) + self.enth_phase_param_E3 = Var( + within=Reals, + initialize=2.054e-6, + units=enth_mass_units_2 / pyunits.K**4, + doc="Solution enthalpy parameter E3", + ) + self.enth_phase_param_E4 = Var( + within=Reals, + initialize=-8.211e-6, + units=enth_mass_units_2 / pyunits.K**4, + doc="Solution enthalpy parameter E4", + ) + self.enth_phase_param_E5 = Var( + within=Reals, + initialize=11.43e-6, + units=enth_mass_units_2 / pyunits.K**4, + doc="Solution enthalpy parameter E5", + ) + + for v in self.component_objects(Var): + v.fix() + + # ---default scaling--- + self.set_default_scaling("temperature", 1e-2) + self.set_default_scaling("pressure", 1e-6) + self.set_default_scaling("pressure_sat", 1e-5) + self.set_default_scaling("dens_mass_solvent", 1e-3, index="Liq") + self.set_default_scaling("dens_mass_solvent", 1, index="Vap") + self.set_default_scaling("dens_mass_solute", 1e-3, index="Sol") + self.set_default_scaling("dens_mass_solute", 1e-3, index="Liq") + self.set_default_scaling("dens_mass_phase", 1e-3, index="Liq") + self.set_default_scaling("enth_mass_solvent", 1e-2, index="Liq") + self.set_default_scaling("enth_mass_solvent", 1e-3, index="Vap") + self.set_default_scaling("cp_mass_phase", 1e-3, index="Liq") + self.set_default_scaling("dh_vap_mass_solvent", 1e-3) + self.set_default_scaling("dh_crystallization_mass_comp", 1e-2, index="NaCl") + + @classmethod + def define_metadata(cls, obj): + obj.add_default_units( + { + "time": pyunits.s, + "length": pyunits.m, + "mass": pyunits.kg, + "amount": pyunits.mol, + "temperature": pyunits.K, + } + ) + + obj.add_properties( + { + "flow_mass_phase_comp": {"method": None}, + "temperature": {"method": None}, + "pressure": {"method": None}, + "solubility_mass_phase_comp": {"method": "_solubility_mass_phase_comp"}, + "solubility_mass_frac_phase_comp": { + "method": "_solubility_mass_frac_phase_comp" + }, + "mass_frac_phase_comp": {"method": "_mass_frac_phase_comp"}, + "dens_mass_phase": {"method": "_dens_mass_phase"}, + "cp_mass_phase": {"method": "_cp_mass_phase"}, + "flow_vol_phase": {"method": "_flow_vol_phase"}, + "flow_vol": {"method": "_flow_vol"}, + "pressure_sat": {"method": "_pressure_sat"}, + "conc_mass_phase_comp": {"method": "_conc_mass_phase_comp"}, + "enth_mass_phase": {"method": "_enth_mass_phase"}, + "dh_crystallization_mass_comp": { + "method": "_dh_crystallization_mass_comp" + }, + "flow_mol_phase_comp": {"method": "_flow_mol_phase_comp"}, + "mole_frac_phase_comp": {"method": "_mole_frac_phase_comp"}, + } + ) + + obj.define_custom_properties( + { + "dens_mass_solvent": {"method": "_dens_mass_solvent"}, + "dens_mass_solute": {"method": "_dens_mass_solute"}, + "dh_vap_mass_solvent": {"method": "_dh_vap_mass_solvent"}, + "cp_mass_solvent": {"method": "_cp_mass_solvent"}, + "cp_mass_solute": {"method": "_cp_mass_solute"}, + "temperature_sat_solvent": {"method": "_temperature_sat_solvent"}, + "enth_mass_solvent": {"method": "_enth_mass_solvent"}, + "enth_mass_solute": {"method": "_enth_mass_solute"}, + "enth_flow": {"method": "_enth_flow"}, + } + ) + + +class _NaClStateBlock(StateBlock): + """ + This Class contains methods which should be applied to Property Blocks as a + whole, rather than individual elements of indexed Property Blocks. + """ + + def fix_initialization_states(self): + """ + Fixes state variables for state blocks. + + Returns: + None + """ + # Fix state variables + fix_state_vars(self) + + # Constraint on water concentration at outlet - unfix in these cases + for b in self.values(): + if b.config.defined_state is False: + b.conc_mol_comp["H2O"].unfix() + + def initialize( + self, + state_args=None, + state_vars_fixed=False, + hold_state=False, + outlvl=idaeslog.NOTSET, + solver=None, + optarg=None, + ): + """ + Initialization routine for property package. + Keyword Arguments: + state_args : Dictionary with initial guesses for the state vars + chosen. Note that if this method is triggered + through the control volume, and if initial guesses + were not provided at the unit model level, the + control volume passes the inlet values as initial + guess.The keys for the state_args dictionary are: + flow_mass_phase_comp : value at which to initialize + phase component flows + pressure : value at which to initialize pressure + temperature : value at which to initialize temperature + outlvl : sets output level of initialization routine (default=idaeslog.NOTSET) + optarg : solver options dictionary object (default=None) + state_vars_fixed: Flag to denote if state vars have already been + fixed. + - True - states have already been fixed by the + control volume 1D. Control volume 0D + does not fix the state vars, so will + be False if this state block is used + with 0D blocks. + - False - states have not been fixed. The state + block will deal with fixing/unfixing. + solver : Solver object to use during initialization if None is provided + it will use the default solver for IDAES (default = None) + hold_state : flag indicating whether the initialization routine + should unfix any state variables fixed during + initialization (default=False). + - True - states variables are not unfixed, and + a dict of returned containing flags for + which states were fixed during + initialization. + - False - state variables are unfixed after + initialization by calling the + release_state method + Returns: + If hold_states is True, returns a dict containing flags for + which states were fixed during initialization. + """ + # Get loggers + init_log = idaeslog.getInitLogger(self.name, outlvl, tag="properties") + solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="properties") + + # Set solver and options + opt = get_solver(solver, optarg) + + # Fix state variables + flags = fix_state_vars(self, state_args) + # Check when the state vars are fixed already result in dof 0 + for k in self.keys(): + dof = degrees_of_freedom(self[k]) + if dof != 0: + raise PropertyPackageError( + "\nWhile initializing {sb_name}, the degrees of freedom " + "are {dof}, when zero is required. \nInitialization assumes " + "that the state variables should be fixed and that no other " + "variables are fixed. \nIf other properties have a " + "predetermined value, use the calculate_state method " + "before using initialize to determine the values for " + "the state variables and avoid fixing the property variables." + "".format(sb_name=self.name, dof=dof) + ) + + # --------------------------------------------------------------------- + skip_solve = True # skip solve if only state variables are present + for k in self.keys(): + if number_unfixed_variables(self[k]) != 0: + skip_solve = False + + if not skip_solve: + # Initialize properties + with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: + results = solve_indexed_blocks(opt, [self], tee=slc.tee) + init_log.info_high( + "Property initialization: {}.".format(idaeslog.condition(results)) + ) + + # If input block, return flags, else release state + if state_vars_fixed is False: + if hold_state is True: + return flags + else: + self.release_state(flags) + + if (not skip_solve) and (not check_optimal_termination(results)): + raise InitializationError( + f"{self.name} failed to initialize successfully. Please " + f"check the output logs for more information." + ) + + def release_state(self, flags, outlvl=idaeslog.NOTSET): + """ + Method to release state variables fixed during initialisation. + Keyword Arguments: + flags : dict containing information of which state variables + were fixed during initialization, and should now be + unfixed. This dict is returned by initialize if + hold_state=True. + outlvl : sets output level of of logging + """ + # Unfix state variables + init_log = idaeslog.getInitLogger(self.name, outlvl, tag="properties") + revert_state_vars(self, flags) + init_log.info_high("{} State Released.".format(self.name)) + + def calculate_state( + self, + var_args=None, + hold_state=False, + outlvl=idaeslog.NOTSET, + solver=None, + optarg=None, + ): + """ + Solves state blocks given a set of variables and their values. These variables can + be state variables or properties. This method is typically used before + initialization to solve for state variables because non-state variables (i.e. properties) + cannot be fixed in initialization routines. + Keyword Arguments: + var_args : dictionary with variables and their values, they can be state variables or properties + {(VAR_NAME, INDEX): VALUE} + hold_state : flag indicating whether all of the state variables should be fixed after calculate state. + True - State variables will be fixed. + False - State variables will remain unfixed, unless already fixed. + outlvl : idaes logger object that sets output level of solve call (default=idaeslog.NOTSET) + solver : solver name string if None is provided the default solver + for IDAES will be used (default = None) + optarg : solver options dictionary object (default={}) + Returns: + results object from state block solve + """ + # Get logger + solve_log = idaeslog.getSolveLogger(self.name, level=outlvl, tag="properties") + + # Initialize at current state values (not user provided) + self.initialize(solver=solver, optarg=optarg, outlvl=outlvl) + + # Set solver and options + opt = get_solver(solver, optarg) + + # Fix variables and check degrees of freedom + flags = ( + {} + ) # dictionary noting which variables were fixed and their previous state + for k in self.keys(): + sb = self[k] + for (v_name, ind), val in var_args.items(): + var = getattr(sb, v_name) + if iscale.get_scaling_factor(var[ind]) is None: + _log.warning( + "While using the calculate_state method on {sb_name}, variable {v_name} " + "was provided as an argument in var_args, but it does not have a scaling " + "factor. This suggests that the calculate_scaling_factor method has not been " + "used or the variable was created on demand after the scaling factors were " + "calculated. It is recommended to touch all relevant variables (i.e. call " + "them or set an initial value) before using the calculate_scaling_factor " + "method.".format(v_name=v_name, sb_name=sb.name) + ) + if var[ind].is_fixed(): + flags[(k, v_name, ind)] = True + if value(var[ind]) != val: + raise ConfigurationError( + "While using the calculate_state method on {sb_name}, {v_name} was " + "fixed to a value {val}, but it was already fixed to value {val_2}. " + "Unfix the variable before calling the calculate_state " + "method or update var_args." + "".format( + sb_name=sb.name, + v_name=var.name, + val=val, + val_2=value(var[ind]), + ) + ) + else: + flags[(k, v_name, ind)] = False + var[ind].fix(val) + + if degrees_of_freedom(sb) != 0: + raise RuntimeError( + "While using the calculate_state method on {sb_name}, the degrees " + "of freedom were {dof}, but 0 is required. Check var_args and ensure " + "the correct fixed variables are provided." + "".format(sb_name=sb.name, dof=degrees_of_freedom(sb)) + ) + + # Solve + with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: + results = solve_indexed_blocks(opt, [self], tee=slc.tee) + solve_log.info_high( + "Calculate state: {}.".format(idaeslog.condition(results)) + ) + + if not check_optimal_termination(results): + _log.warning( + "While using the calculate_state method on {sb_name}, the solver failed " + "to converge to an optimal solution. This suggests that the user provided " + "infeasible inputs, or that the model is poorly scaled, poorly initialized, " + "or degenerate." + ) + + # unfix all variables fixed with var_args + for (k, v_name, ind), previously_fixed in flags.items(): + if not previously_fixed: + var = getattr(self[k], v_name) + var[ind].unfix() + + # fix state variables if hold_state + if hold_state: + fix_state_vars(self) + + return results + + +@declare_process_block_class("NaClStateBlock", block_class=_NaClStateBlock) +class NaClStateBlockData(StateBlockData): + def build(self): + """Callable method for Block construction.""" + super(NaClStateBlockData, self).build() + self._make_state_vars() + + def _make_state_vars(self): + # Create state variables + self.pressure = Var( + domain=NonNegativeReals, + initialize=101325, + units=pyunits.Pa, + doc="State pressure [Pa]", + ) + + self.temperature = Var( + domain=NonNegativeReals, + initialize=298.15, + bounds=(273.15, 393.15), + units=pyunits.degK, + doc="State temperature [K]", + ) + + self.flow_mass_phase_comp = Var( + self.phase_component_set, + initialize={ + ("Liq", "H2O"): 0.965, + ("Liq", "NaCl"): 0.035, + ("Vap", "H2O"): 0, + ("Sol", "NaCl"): 0, + }, + bounds=(0, None), + domain=NonNegativeReals, + units=pyunits.kg / pyunits.s, + doc="Mass flow rate", + ) + + # Property Methods + + # 1 Mass fraction: From NaCl property package + def _mass_frac_phase_comp(self): + self.mass_frac_phase_comp = Var( + self.phase_component_set, + domain=NonNegativeReals, + initialize={ + ("Liq", "H2O"): 0.965, + ("Liq", "NaCl"): 0.035, + ("Vap", "H2O"): 1.0, + ("Sol", "NaCl"): 1.0, + }, + bounds=(0, 1.0001), + units=pyunits.dimensionless, + doc="Mass fraction", + ) + + def rule_mass_frac_phase_comp(b, p, j): + phase_comp_list = [ + (p, j) + for j in self.params.component_list + if (p, j) in b.phase_component_set + ] + if len(phase_comp_list) == 1: # one component in this phase + return b.mass_frac_phase_comp[p, j] == 1 + else: + return b.mass_frac_phase_comp[p, j] == b.flow_mass_phase_comp[ + p, j + ] / sum(b.flow_mass_phase_comp[p_j] for p_j in phase_comp_list) + + self.eq_mass_frac_phase_comp = Constraint( + self.phase_component_set, rule=rule_mass_frac_phase_comp + ) + + # 2. Solubility in g/L: calculated from solubility mass fraction + def _solubility_mass_phase_comp(self): + self.solubility_mass_phase_comp = Var( + ["Liq"], + ["NaCl"], + domain=NonNegativeReals, + bounds=(300, 1000), + initialize=356.5, + units=pyunits.g / pyunits.L, + doc="solubility of NaCl in water, g/L", + ) + + def rule_solubility_mass_phase_comp(b, j): + return b.solubility_mass_phase_comp[ + "Liq", j + ] == b.solubility_mass_frac_phase_comp["Liq", j] * b.dens_mass_solvent[ + "Liq" + ] / ( + 1 - b.solubility_mass_frac_phase_comp["Liq", j] + ) + + self.eq_solubility_mass_phase_comp = Constraint( + ["NaCl"], rule=rule_solubility_mass_phase_comp + ) + + # 3. Solubility as mass fraction + def _solubility_mass_frac_phase_comp(self): + self.solubility_mass_frac_phase_comp = Var( + ["Liq"], + ["NaCl"], + domain=NonNegativeReals, + bounds=(0, 1.0001), + initialize=0.5, + units=pyunits.dimensionless, + doc="solubility (as mass fraction) of NaCl in water", + ) + + def rule_solubility_mass_frac_phase_comp(b, j): # Sparrow (2003) + t = b.temperature - 273.15 * pyunits.K + return ( + b.solubility_mass_frac_phase_comp["Liq", j] + == b.params.sol_param_A1 + + b.params.sol_param_A2 * t + + b.params.sol_param_A3 * t**2 + ) + + self.eq_solubility_mass_frac_phase_comp = Constraint( + ["NaCl"], rule=rule_solubility_mass_frac_phase_comp + ) + + # 4. Density of solvent (pure water in liquid and vapour phases) + def _dens_mass_solvent(self): + self.dens_mass_solvent = Var( + ["Liq", "Vap"], + initialize=1e3, + bounds=(1e-4, 1e4), + units=pyunits.kg * pyunits.m**-3, + doc="Mass density of pure water", + ) + + def rule_dens_mass_solvent(b, p): + if p == "Liq": # density, eq. 8 in Sharqawy + t = b.temperature - 273.15 * pyunits.K + dens_mass_w = ( + b.params.dens_mass_param_A1 + + b.params.dens_mass_param_A2 * t + + b.params.dens_mass_param_A3 * t**2 + + b.params.dens_mass_param_A4 * t**3 + + b.params.dens_mass_param_A5 * t**4 + ) + return b.dens_mass_solvent[p] == dens_mass_w + elif p == "Vap": + return b.dens_mass_solvent[p] == ( + b.params.mw_comp["H2O"] * b.pressure + ) / (Constants.gas_constant * b.temperature) + + self.eq_dens_mass_solvent = Constraint( + ["Liq", "Vap"], rule=rule_dens_mass_solvent + ) + + # 5. Density of NaCl crystals and liquid + def _dens_mass_solute(self): + self.dens_mass_solute = Var( + ["Sol", "Liq"], + initialize=1e3, + bounds=(1e-4, 1e4), + units=pyunits.kg * pyunits.m**-3, + doc="Mass density of solid NaCl crystals", + ) + + def rule_dens_mass_solute(b, p): + if p == "Sol": + return b.dens_mass_solute[p] == b.params.dens_mass_param_NaCl + elif p == "Liq": # Apparent density in eq. 9 of Laliberte paper + t = b.temperature - 273.15 * pyunits.K + v_app = ( + b.mass_frac_phase_comp["Liq", "NaCl"] + + b.params.dens_mass_param_NaCl_liq_C2 + + (b.params.dens_mass_param_NaCl_liq_C3 * t) + ) / ( + ( + b.mass_frac_phase_comp["Liq", "NaCl"] + * b.params.dens_mass_param_NaCl_liq_C0 + ) + + b.params.dens_mass_param_NaCl_liq_C1 + ) + v_app = v_app / exp( + 0.000001 + * pyunits.K**-2 + * (t + b.params.dens_mass_param_NaCl_liq_C4) ** 2 + ) + return b.dens_mass_solute[p] == 1 / v_app + + self.eq_dens_mass_solute = Constraint( + ["Sol", "Liq"], rule=rule_dens_mass_solute + ) + + # 6. Density of liquid solution (Water + NaCl) + def _dens_mass_phase(self): + self.dens_mass_phase = Var( + ["Liq"], + initialize=1e3, + bounds=(5e2, 1e4), + units=pyunits.kg * pyunits.m**-3, + doc="Mass density of liquid NaCl solution", + ) + + def rule_dens_mass_phase(b): # density, eq. 6 of Laliberte paper + return b.dens_mass_phase["Liq"] == 1 / ( + (b.mass_frac_phase_comp["Liq", "NaCl"] / b.dens_mass_solute["Liq"]) + + (b.mass_frac_phase_comp["Liq", "H2O"] / b.dens_mass_solvent["Liq"]) + ) + + self.eq_dens_mass_phase = Constraint(rule=rule_dens_mass_phase) + + # 7. Latent heat of vapourization of pure water + def _dh_vap_mass_solvent(self): + self.dh_vap_mass_solvent = Var( + initialize=2.4e3, + bounds=(1, 1e5), + units=pyunits.kJ / pyunits.kg, + doc="Latent heat of vaporization of pure water", + ) + + def rule_dh_vap_mass_solvent(b): + t = b.temperature - 273.15 * pyunits.K + dh_vap_sol = ( + b.params.dh_vap_w_param_0 + + b.params.dh_vap_w_param_1 * t + + b.params.dh_vap_w_param_2 * t**2 + + b.params.dh_vap_w_param_3 * t**3 + + b.params.dh_vap_w_param_4 * t**4 + ) + return b.dh_vap_mass_solvent == pyunits.convert( + dh_vap_sol, to_units=pyunits.kJ / pyunits.kg + ) + + self.eq_dh_vap_mass_solvent = Constraint(rule=rule_dh_vap_mass_solvent) + + # 8. Heat capacity of solvent (pure water in liquid and vapour phases) + def _cp_mass_solvent(self): + self.cp_mass_solvent = Var( + ["Liq", "Vap"], + initialize=4e3, + bounds=(1e-5, 1e5), + units=pyunits.J / pyunits.kg / pyunits.K, + doc="Specific heat capacity of pure solvent", + ) + + def rule_cp_mass_solvent(b, p): + if p == "Liq": + # specific heat, eq. 9 in Sharqawy et al. (2010) + # Convert T90 to T68, eq. 4 in Sharqawy et al. (2010); primary reference from Rusby (1991) + t = (b.temperature - 0.00025 * 273.15 * pyunits.K) / (1 - 0.00025) + A = b.params.cp_phase_param_A1 + B = b.params.cp_phase_param_B1 + C = b.params.cp_phase_param_C1 + D = b.params.cp_phase_param_D1 + return ( + b.cp_mass_solvent["Liq"] == (A + B * t + C * t**2 + D * t**3) * 1000 + ) + elif p == "Vap": + t = b.temperature / 1000 + return ( + b.cp_mass_solvent["Vap"] + == b.params.cp_vap_param_A + + b.params.cp_vap_param_B * t + + b.params.cp_vap_param_C * t**2 + + b.params.cp_vap_param_D * t**3 + + b.params.cp_vap_param_E / t**2 + ) + + self.eq_cp_mass_solvent = Constraint(["Liq", "Vap"], rule=rule_cp_mass_solvent) + + # 9. Heat capacity of solid-phase NaCl crystals + def _cp_mass_solute(self): + self.cp_mass_solute = Var( + ["Liq", "Sol"], + initialize=1e3, + bounds=(-1e4, 1e5), + units=pyunits.J / pyunits.kg / pyunits.K, + doc="Specific heat capacity of solid NaCl crystals", + ) + + def rule_cp_mass_solute(b, p): + if p == "Sol": # Shomate equation for NaCl, NIST + t = b.temperature / (1000 * pyunits.dimensionless) + cp_mass_solute_mol = ( + b.params.cp_param_NaCl_solid_A + + (b.params.cp_param_NaCl_solid_B * t) + + (b.params.cp_param_NaCl_solid_C * t**2) + + (b.params.cp_param_NaCl_solid_D * t**3) + + (b.params.cp_param_NaCl_solid_E / (t**2)) + ) + return ( + b.cp_mass_solute[p] == cp_mass_solute_mol / b.params.mw_comp["NaCl"] + ) + if ( + p == "Liq" + ): # NaCl liq. apparent specific heat capacity, eq. 11-12 of Laliberte (2009) + t = b.temperature - 273.15 * pyunits.K + alpha = ( + (b.params.cp_param_NaCl_liq_A2 * t) + + ( + b.params.cp_param_NaCl_liq_A4 + * (1 - b.mass_frac_phase_comp["Liq", "H2O"]) + ) + + (b.params.cp_param_NaCl_liq_A3 * exp(0.01 * pyunits.K**-1 * t)) + ) + cp_nacl_liq = b.params.cp_param_NaCl_liq_A1 * exp( + alpha + ) + b.params.cp_param_NaCl_liq_A5 * ( + (1 - b.mass_frac_phase_comp["Liq", "H2O"]) + ** b.params.cp_param_NaCl_liq_A6 + ) + return b.cp_mass_solute[p] == pyunits.convert( + cp_nacl_liq, to_units=pyunits.J / pyunits.kg / pyunits.K + ) + + self.eq_cp_mass_solute = Constraint(["Liq", "Sol"], rule=rule_cp_mass_solute) + + # 10. cp of liquid solution (Water + NaCl) + def _cp_mass_phase(self): + self.cp_mass_phase = Var( + ["Liq"], + initialize=4e3, + bounds=(1e-4, 1e5), + units=pyunits.J / pyunits.kg / pyunits.K, + doc="Specific heat capacity of liquid solution", + ) + + def rule_cp_mass_phase(b): # heat capacity, eq. 10 of Laliberte (2009) paper + return ( + b.cp_mass_phase["Liq"] + == b.mass_frac_phase_comp["Liq", "NaCl"] * b.cp_mass_solute["Liq"] + + b.mass_frac_phase_comp["Liq", "H2O"] * b.cp_mass_solvent["Liq"] + ) + + self.eq_cp_mass_phase = Constraint(rule=rule_cp_mass_phase) + + # 11. Volumetric flow rate for each phase + def _flow_vol_phase(self): + self.flow_vol_phase = Var( + self.params.phase_list, + initialize=1, + bounds=(0, None), + units=pyunits.m**3 / pyunits.s, + doc="Volumetric flow rate", + ) + + def rule_flow_vol_phase(b, p): + if p == "Liq": + return ( + b.flow_vol_phase[p] + == sum( + b.flow_mass_phase_comp[p, j] + for j in self.params.component_list + if (p, j) in self.phase_component_set + ) + / b.dens_mass_phase[p] + ) + elif p == "Sol": + return ( + b.flow_vol_phase[p] + == sum( + b.flow_mass_phase_comp[p, j] + for j in self.params.component_list + if (p, j) in self.phase_component_set + ) + / b.dens_mass_solute["Sol"] + ) + elif p == "Vap": + return ( + b.flow_vol_phase[p] + == sum( + b.flow_mass_phase_comp[p, j] + for j in self.params.component_list + if (p, j) in self.phase_component_set + ) + / b.dens_mass_solvent["Vap"] + ) + + self.eq_flow_vol_phase = Constraint( + self.params.phase_list, rule=rule_flow_vol_phase + ) + + # 12. Total volumetric flow rate + def _flow_vol(self): + def rule_flow_vol(b): + return sum(b.flow_vol_phase[p] for p in self.params.phase_list) + + self.flow_vol = Expression(rule=rule_flow_vol) + + # 13. Vapour pressure of the NaCl solution based on the boiling temperature + def _pressure_sat(self): + self.pressure_sat = Var( + initialize=1e3, + bounds=(0.001, 1e6), + units=pyunits.Pa, + doc="Vapor pressure of NaCl solution", + ) + + def rule_pressure_sat(b): # vapor pressure, eq6 in Sparrow (2003) + t = b.temperature - 273.15 * pyunits.K + x = b.mass_frac_phase_comp["Liq", "NaCl"] + + ps_a = ( + b.params.pressure_sat_param_A1 + + (b.params.pressure_sat_param_A2 * x) + + (b.params.pressure_sat_param_A3 * x**2) + + (b.params.pressure_sat_param_A4 * x**3) + + (b.params.pressure_sat_param_A5 * x**4) + ) + + ps_b = ( + b.params.pressure_sat_param_B1 + + (b.params.pressure_sat_param_B2 * x) + + (b.params.pressure_sat_param_B3 * x**2) + + (b.params.pressure_sat_param_B4 * x**3) + + (b.params.pressure_sat_param_B5 * x**4) + ) + + ps_c = ( + b.params.pressure_sat_param_C1 + + (b.params.pressure_sat_param_C2 * x) + + (b.params.pressure_sat_param_C3 * x**2) + + (b.params.pressure_sat_param_C4 * x**3) + + (b.params.pressure_sat_param_C5 * x**4) + ) + + ps_d = ( + b.params.pressure_sat_param_D1 + + (b.params.pressure_sat_param_D2 * x) + + (b.params.pressure_sat_param_D3 * x**2) + + (b.params.pressure_sat_param_D4 * x**3) + + (b.params.pressure_sat_param_D5 * x**4) + ) + + ps_e = ( + b.params.pressure_sat_param_E1 + + (b.params.pressure_sat_param_E2 * x) + + (b.params.pressure_sat_param_E3 * x**2) + + (b.params.pressure_sat_param_E4 * x**3) + + (b.params.pressure_sat_param_E5 * x**4) + ) + + p_sat = ps_a + (ps_b * t) + (ps_c * t**2) + (ps_d * t**3) + (ps_e * t**4) + return b.pressure_sat == pyunits.convert(p_sat, to_units=pyunits.Pa) + + self.eq_pressure_sat = Constraint(rule=rule_pressure_sat) + + # 14. Saturation temperature for water vapour at calculated boiling pressure + def _temperature_sat_solvent(self): + self.temperature_sat_solvent = Var( + initialize=298.15, + bounds=(273.15, 1000.15), + units=pyunits.K, + doc="Vapour (saturation) temperature of pure solvent at boiling (i.e. crystallization) pressure", + ) + + def rule_temperature_sat_solvent(b): + psat = pyunits.convert(b.pressure_sat, to_units=pyunits.kPa) + return ( + b.temperature_sat_solvent + == b.params.temp_sat_solvent_A1 + + b.params.temp_sat_solvent_A2 + / ( + log(psat / b.params.temp_sat_solvent_A3) + + b.params.temp_sat_solvent_A4 + ) + ) + + self.eq_temperature_sat_solvent = Constraint(rule=rule_temperature_sat_solvent) + + # 15. Mass concentration + def _conc_mass_phase_comp(self): + self.conc_mass_phase_comp = Var( + ["Liq"], + self.params.component_list, + initialize=10, + bounds=(0, 1e6), + units=pyunits.kg * pyunits.m**-3, + doc="Mass concentration", + ) + + def rule_conc_mass_phase_comp(b, j): + return ( + b.conc_mass_phase_comp["Liq", j] + == b.dens_mass_phase["Liq"] * b.mass_frac_phase_comp["Liq", j] + ) + + self.eq_conc_mass_phase_comp = Constraint( + self.params.component_list, rule=rule_conc_mass_phase_comp + ) + + # 16. Specific enthalpy of solvent (pure water in liquid and vapour phases) + def _enth_mass_solvent(self): + self.enth_mass_solvent = Var( + ["Liq", "Vap"], + initialize=1e3, + bounds=(1, 1e4), + units=pyunits.kJ * pyunits.kg**-1, + doc="Specific saturated enthalpy of pure solvent", + ) + + def rule_enth_mass_solvent(b, p): + t = b.temperature - 273.15 * pyunits.K + h_w = ( + b.params.enth_mass_solvent_param_A1 + + b.params.enth_mass_solvent_param_A2 * t + + b.params.enth_mass_solvent_param_A3 * t**2 + + b.params.enth_mass_solvent_param_A4 * t**3 + ) + if p == "Liq": # enthalpy, eq. 55 in Sharqawy + return b.enth_mass_solvent[p] == pyunits.convert( + h_w, to_units=pyunits.kJ * pyunits.kg**-1 + ) + elif p == "Vap": + + return ( + b.enth_mass_solvent[p] + == pyunits.convert(h_w, to_units=pyunits.kJ * pyunits.kg**-1) + + +b.dh_vap_mass_solvent + ) + + self.eq_enth_mass_solvent = Constraint( + ["Liq", "Vap"], rule=rule_enth_mass_solvent + ) + + # 17. Specific enthalpy of NaCl solution + def _enth_mass_phase(self): + self.enth_mass_phase = Var( + ["Liq"], + initialize=500, + bounds=(1, 1000), + units=pyunits.kJ * pyunits.kg**-1, + doc="Specific enthalpy of NaCl solution", + ) + + def rule_enth_mass_phase( + b, + ): # specific enthalpy calculation based on Sparrow (2003). + t = ( + b.temperature - 273.15 * pyunits.K + ) # temperature in degC, but pyunits in K + S = b.mass_frac_phase_comp["Liq", "NaCl"] + + enth_a = ( + b.params.enth_phase_param_A1 + + (b.params.enth_phase_param_A2 * S) + + (b.params.enth_phase_param_A3 * S**2) + + (b.params.enth_phase_param_A4 * S**3) + + (b.params.enth_phase_param_A5 * S**4) + ) + + enth_b = ( + b.params.enth_phase_param_B1 + + (b.params.enth_phase_param_B2 * S) + + (b.params.enth_phase_param_B3 * S**2) + + (b.params.enth_phase_param_B4 * S**3) + + (b.params.enth_phase_param_B5 * S**4) + ) + + enth_c = ( + b.params.enth_phase_param_C1 + + (b.params.enth_phase_param_C2 * S) + + (b.params.enth_phase_param_C3 * S**2) + + (b.params.enth_phase_param_C4 * S**3) + + (b.params.enth_phase_param_C5 * S**4) + ) + + enth_d = ( + b.params.enth_phase_param_D1 + + (b.params.enth_phase_param_D2 * S) + + (b.params.enth_phase_param_D3 * S**2) + + (b.params.enth_phase_param_D4 * S**3) + + (b.params.enth_phase_param_D5 * S**4) + ) + + enth_e = ( + b.params.enth_phase_param_E1 + + (b.params.enth_phase_param_E2 * S) + + (b.params.enth_phase_param_E3 * S**2) + + (b.params.enth_phase_param_E4 * S**3) + + (b.params.enth_phase_param_E5 * S**4) + ) + + return b.enth_mass_phase["Liq"] == enth_a + (enth_b * t) + ( + enth_c * t**2 + ) + (enth_d * t**3) + (enth_e * t**4) + + self.eq_enth_mass_phase = Constraint(rule=rule_enth_mass_phase) + + # 18. Heat of crystallization + def _dh_crystallization_mass_comp(self): + self.dh_crystallization_mass_comp = Var( + ["NaCl"], + initialize=1, + bounds=(-1e3, 1e3), + units=pyunits.kJ / pyunits.kg, + doc="NaCl heat of crystallization", + ) + + def rule_dh_crystallization_mass_comp(b): + if ( + b.params.config.heat_of_crystallization_model + == HeatOfCrystallizationModel.constant + ): + return ( + b.dh_crystallization_mass_comp["NaCl"] + == b.params.dh_crystallization_param + ) + elif ( + b.params.config.heat_of_crystallization_model + == HeatOfCrystallizationModel.zero + ): + return ( + b.dh_crystallization_mass_comp["NaCl"] + == 0 * pyunits.kJ / pyunits.kg + ) + elif ( + b.params.config.heat_of_crystallization_model + == HeatOfCrystallizationModel.temp_dependent + ): + raise NotImplementedError( + f"Temperature-dependent heat of crystallization model has not been implemented yet." + ) + + self.eq_dh_crystallization_mass_comp = Constraint( + rule=rule_dh_crystallization_mass_comp + ) + + # 19. Heat capacity of solid-phase NaCl crystals + def _enth_mass_solute(self): + self.enth_mass_solute = Var( + ["Sol"], + initialize=1e3, + bounds=(1e-3, 1e4), + units=pyunits.kJ / pyunits.kg, + doc="Specific enthalpy of solid NaCl crystals", + ) + + def rule_enth_mass_solute(b, p): + ############################ + # Shomate equation for molar enthalpy ofNaCl, NIST + # Note: Tref is 298 K, so changing the Tref to 273 K to match IAPWS is necessary. + # Computation formula for reference temperature change: + # Enthalpy at T relative to 273 K = Enthalpy change relative to 298 K + (Enthalpy at 298 K - Enthalpy at 273 K) + ############################ + + # (i) Enthalpy at original reference temperature (298 K) + enth_mass_solute_mol_tref_1 = 0 # Enthalpy at original temperature (298 K) + + # (ii) Enthalpy at new reference temperature (273.15 K) + tref_2 = 273.15 * pyunits.K / 1000 + enth_mass_solute_mol_tref_2 = ( + (b.params.cp_param_NaCl_solid_A * tref_2) + + ((1 / 2) * b.params.cp_param_NaCl_solid_B * tref_2**2) + + ((1 / 3) * b.params.cp_param_NaCl_solid_C * tref_2**3) + + ((1 / 4) * b.params.cp_param_NaCl_solid_D * tref_2**4) + - (b.params.cp_param_NaCl_solid_E / tref_2) + + b.params.cp_param_NaCl_solid_F + - b.params.cp_param_NaCl_solid_H + ) + + # (iii) Compute enthalpy change at temperature t relative to old reference temperature t_ref_1 + t = b.temperature / (1000 * pyunits.dimensionless) + dh_mass_solute_mol_tref_1 = ( + (b.params.cp_param_NaCl_solid_A * t) + + ((1 / 2) * b.params.cp_param_NaCl_solid_B * t**2) + + ((1 / 3) * b.params.cp_param_NaCl_solid_C * t**3) + + ((1 / 4) * b.params.cp_param_NaCl_solid_D * t**4) + - (b.params.cp_param_NaCl_solid_E / t) + + b.params.cp_param_NaCl_solid_F + - b.params.cp_param_NaCl_solid_H + ) + + # (iv) Compute enthalpy change at temperature t relative to new reference temperature t_ref_2 + enth_mass_solute_mol = dh_mass_solute_mol_tref_1 + ( + enth_mass_solute_mol_tref_1 - enth_mass_solute_mol_tref_2 + ) + + # (v) Convert from molar enthalpy to mass enthalpy + enth_mass_solute_mol = enth_mass_solute_mol / b.params.mw_comp["NaCl"] + return b.enth_mass_solute[p] == enth_mass_solute_mol * ( + pyunits.kJ / pyunits.J + ) + + self.eq_enth_mass_solute = Constraint(["Sol"], rule=rule_enth_mass_solute) + + # 20. Total enthalpy flow for any stream: adds up the enthalpies for the solid, liquid and vapour phases + # Assumes no NaCl is vapour stream or water in crystals + def _enth_flow(self): + # enthalpy flow expression for get_enthalpy_flow_terms method + + def rule_enth_flow(b): # enthalpy flow [J/s] + return ( + sum(b.flow_mass_phase_comp["Liq", j] for j in b.params.component_list) + * b.enth_mass_phase["Liq"] + + b.flow_mass_phase_comp["Vap", "H2O"] * b.enth_mass_solvent["Vap"] + + b.flow_mass_phase_comp["Sol", "NaCl"] * b.enth_mass_solute["Sol"] + ) + + self.enth_flow = Expression(rule=rule_enth_flow) + + # 21. Molar flows + def _flow_mol_phase_comp(self): + self.flow_mol_phase_comp = Var( + self.phase_component_set, + initialize=100, + bounds=(None, None), + domain=NonNegativeReals, + units=pyunits.mol / pyunits.s, + doc="Molar flowrate", + ) + + def rule_flow_mol_phase_comp(b, p, j): + return ( + b.flow_mol_phase_comp[p, j] + == b.flow_mass_phase_comp[p, j] / b.params.mw_comp[j] + ) + + self.eq_flow_mol_phase_comp = Constraint( + self.phase_component_set, rule=rule_flow_mol_phase_comp + ) + + # 22. Mole fractions + def _mole_frac_phase_comp(self): + self.mole_frac_phase_comp = Var( + self.phase_component_set, + initialize=0.1, + bounds=(0, 1.0001), + units=pyunits.dimensionless, + doc="Mole fraction", + ) + + def rule_mole_frac_phase_comp(b, p, j): + phase_comp_list = [ + (p, j) + for j in self.params.component_list + if (p, j) in b.phase_component_set + ] + if len(phase_comp_list) == 1: # one component in this phase + return b.mole_frac_phase_comp[p, j] == 1 + else: + return b.mole_frac_phase_comp[p, j] == b.flow_mol_phase_comp[ + p, j + ] / sum(b.flow_mol_phase_comp[p_j] for (p_j) in phase_comp_list) + + self.eq_mole_frac_phase_comp = Constraint( + self.phase_component_set, rule=rule_mole_frac_phase_comp + ) + + # ----------------------------------------------------------------------------- + # Boilerplate Methods + + def get_material_flow_terms(self, p, j): + """Create material flow terms for control volume.""" + return self.flow_mass_phase_comp[p, j] + + def get_enthalpy_flow_terms(self, p): + """Create enthalpy flow terms.""" + return self.enth_flow + + def default_material_balance_type(self): + return MaterialBalanceType.componentTotal + + def default_energy_balance_type(self): + return EnergyBalanceType.enthalpyTotal + + def get_material_flow_basis(self): + return MaterialFlowBasis.mass + + def define_state_vars(self): + """Define state vars.""" + return { + "flow_mass_phase_comp": self.flow_mass_phase_comp, + "temperature": self.temperature, + "pressure": self.pressure, + } + + # ----------------------------------------------------------------------------- + # Scaling methods + def calculate_scaling_factors(self): + super().calculate_scaling_factors() + + # default scaling factors have already been set with idaes.core.property_base.calculate_scaling_factors() + # for the following variables: flow_mass_phase_comp, pressure, temperature, dens_mass_phase, enth_mass_phase + + # These variables should have user input + if iscale.get_scaling_factor(self.flow_mass_phase_comp["Liq", "H2O"]) is None: + sf = iscale.get_scaling_factor( + self.flow_mass_phase_comp["Liq", "H2O"], default=1e0, warning=True + ) + iscale.set_scaling_factor(self.flow_mass_phase_comp["Liq", "H2O"], sf) + + if iscale.get_scaling_factor(self.flow_mass_phase_comp["Liq", "NaCl"]) is None: + sf = iscale.get_scaling_factor( + self.flow_mass_phase_comp["Liq", "NaCl"], default=1e2, warning=True + ) + iscale.set_scaling_factor(self.flow_mass_phase_comp["Liq", "NaCl"], sf) + + if iscale.get_scaling_factor(self.flow_mass_phase_comp["Sol", "NaCl"]) is None: + sf = iscale.get_scaling_factor( + self.flow_mass_phase_comp["Sol", "NaCl"], default=1e2, warning=True + ) + iscale.set_scaling_factor(self.flow_mass_phase_comp["Sol", "NaCl"], sf) + + if iscale.get_scaling_factor(self.flow_mass_phase_comp["Vap", "H2O"]) is None: + sf = iscale.get_scaling_factor( + self.flow_mass_phase_comp["Vap", "H2O"], default=1e0, warning=True + ) + iscale.set_scaling_factor(self.flow_mass_phase_comp["Vap", "H2O"], sf) + + # scaling factors for molecular weights + for j, v in self.params.mw_comp.items(): + if iscale.get_scaling_factor(v) is None: + iscale.set_scaling_factor(self.params.mw_comp, 1e3) + + # Scaling for solubility (g/L) parameters. Values typically about 300-500, so scale by 1e-3. + if self.is_property_constructed("solubility_mass_phase_comp"): + if iscale.get_scaling_factor(self.solubility_mass_phase_comp) is None: + iscale.set_scaling_factor(self.solubility_mass_phase_comp, 1e-3) + + # Scaling for solubility mass fraction. Values typically about 0-1, so scale by 1e0. + if self.is_property_constructed("solubility_mass_frac_phase_comp"): + if iscale.get_scaling_factor(self.solubility_mass_frac_phase_comp) is None: + iscale.set_scaling_factor(self.solubility_mass_frac_phase_comp, 1e0) + + # Scaling for flow_vol_phase: scaled as scale of dominant component in phase / density of phase + if self.is_property_constructed("flow_vol_phase"): + for p in self.params.phase_list: + if p == "Liq": + if iscale.get_scaling_factor(self.flow_vol_phase[p]) is None: + sf = iscale.get_scaling_factor( + self.flow_mass_phase_comp[p, "H2O"] + ) / iscale.get_scaling_factor(self.dens_mass_phase[p]) + iscale.set_scaling_factor(self.flow_vol_phase[p], sf) + elif p == "Vap": + if iscale.get_scaling_factor(self.flow_vol_phase[p]) is None: + sf = iscale.get_scaling_factor( + self.flow_mass_phase_comp[p, "H2O"] + ) / iscale.get_scaling_factor(self.dens_mass_solvent[p]) + iscale.set_scaling_factor(self.flow_vol_phase[p], sf) + elif p == "Sol": + if iscale.get_scaling_factor(self.flow_vol_phase[p]) is None: + sf = iscale.get_scaling_factor( + self.flow_mass_phase_comp[p, "NaCl"] + ) / iscale.get_scaling_factor(self.dens_mass_solute[p]) + iscale.set_scaling_factor(self.flow_vol_phase[p], sf) + + # Scaling material heat capacities + if self.is_property_constructed("cp_mass_solute"): + for p in ["Sol", "Liq"]: + if iscale.get_scaling_factor(self.cp_mass_solute[p]) is None: + iscale.set_scaling_factor( + self.cp_mass_solute[p], 1e-3 + ) # same as scaling factor of .cp_mass_phase['Liq'] + + if self.is_property_constructed("cp_mass_solvent"): + for p in ["Liq", "Vap"]: + if iscale.get_scaling_factor(self.cp_mass_solvent[p]) is None: + iscale.set_scaling_factor( + self.cp_mass_solvent[p], 1e-3 + ) # same as scaling factor of .cp_mass_phase['Liq'] + + # Scaling saturation temperature + if self.is_property_constructed("temperature_sat_solvent"): + if iscale.get_scaling_factor(self.temperature_sat_solvent) is None: + iscale.set_scaling_factor( + self.temperature_sat_solvent, + iscale.get_scaling_factor(self.temperature), + ) + + # Scaling solute and solvent enthalpies + if self.is_property_constructed("enth_mass_solute"): + if iscale.get_scaling_factor(self.enth_mass_solute) is None: + iscale.set_scaling_factor( + self.enth_mass_solute["Sol"], + iscale.get_scaling_factor(self.enth_mass_solvent["Liq"]), + ) + + if self.is_property_constructed("enth_mass_phase"): + if iscale.get_scaling_factor(self.enth_mass_phase) is None: + iscale.set_scaling_factor( + self.enth_mass_phase["Liq"], + iscale.get_scaling_factor(self.enth_mass_solvent["Liq"]), + ) + + # Scaling enthapy flow - not sure about this one + if self.is_property_constructed("enth_flow"): + iscale.set_scaling_factor( + self.enth_flow, + iscale.get_scaling_factor(self.flow_mass_phase_comp["Liq", "H2O"]) + * iscale.get_scaling_factor(self.enth_mass_phase["Liq"]), + ) + + # Scaling molar flows - derived from flow_mass + if self.is_property_constructed("flow_mol_phase_comp"): + for p, j in self.phase_component_set: + if iscale.get_scaling_factor(self.flow_mol_phase_comp[p, j]) is None: + sf = iscale.get_scaling_factor(self.flow_mass_phase_comp[p, j]) + sf /= iscale.get_scaling_factor(self.params.mw_comp[j]) + iscale.set_scaling_factor(self.flow_mol_phase_comp[p, j], sf) + + ###################################################### + # Scaling for mass fractions - needs verification! + if self.is_property_constructed("mass_frac_phase_comp"): + # Option 1: + for p, j in self.phase_component_set: + if iscale.get_scaling_factor(self.mass_frac_phase_comp[p, j]) is None: + if p == "Sol": + iscale.set_scaling_factor(self.mass_frac_phase_comp[p, j], 1e0) + else: + if j == "NaCl": + sf = iscale.get_scaling_factor( + self.flow_mass_phase_comp[p, j] + ) / iscale.get_scaling_factor( + self.flow_mass_phase_comp[p, "H2O"] + ) + iscale.set_scaling_factor( + self.mass_frac_phase_comp[p, j], sf + ) + elif j == "H2O": + iscale.set_scaling_factor( + self.mass_frac_phase_comp[p, j], 1e0 + ) + + # Scaling for mole fractions - same approach as mass fractions - needs verification! + # Appears to make things worse! + if self.is_property_constructed("mole_frac_phase_comp"): + # Option 1: + for p, j in self.phase_component_set: + if iscale.get_scaling_factor(self.mole_frac_phase_comp[p, j]) is None: + if p == "Sol": + iscale.set_scaling_factor(self.mole_frac_phase_comp[p, j], 1e-1) + else: + if j == "NaCl": + sf = iscale.get_scaling_factor( + self.flow_mol_phase_comp[p, j] + ) / iscale.get_scaling_factor( + self.flow_mol_phase_comp[p, "H2O"] + ) + iscale.set_scaling_factor( + self.mole_frac_phase_comp[p, j], sf + ) + elif j == "H2O": + iscale.set_scaling_factor( + self.mole_frac_phase_comp[p, j], 1e0 + ) + + # ######################################################## + + # Scaling for mass concentrations + if self.is_property_constructed("conc_mass_phase_comp"): + for j in self.params.component_list: + sf_dens = iscale.get_scaling_factor(self.dens_mass_phase["Liq"]) + if ( + iscale.get_scaling_factor(self.conc_mass_phase_comp["Liq", j]) + is None + ): + if j == "H2O": + iscale.set_scaling_factor( + self.conc_mass_phase_comp["Liq", j], sf_dens + ) + elif j == "NaCl": + iscale.set_scaling_factor( + self.conc_mass_phase_comp["Liq", j], + sf_dens + * iscale.get_scaling_factor( + self.mass_frac_phase_comp["Liq", j] + ), + ) + + # Transforming constraints + # property relationships with no index, simple constraint + v_str_lst_simple = [ + "pressure_sat", + "dh_vap_mass_solvent", + "temperature_sat_solvent", + ] + for v_str in v_str_lst_simple: + if self.is_property_constructed(v_str): + v = getattr(self, v_str) + sf = iscale.get_scaling_factor(v, default=1, warning=True) + c = getattr(self, "eq_" + v_str) + iscale.constraint_scaling_transform(c, sf) + + # Property relationships with phase index, but simple constraint + v_str_lst_phase = ["dens_mass_phase", "enth_mass_phase", "cp_mass_phase"] + for v_str in v_str_lst_phase: + if self.is_property_constructed(v_str): + v = getattr(self, v_str) + sf = iscale.get_scaling_factor(v["Liq"], default=1, warning=True) + c = getattr(self, "eq_" + v_str) + iscale.constraint_scaling_transform(c, sf) + + # Property relationship indexed by component + v_str_lst_comp = [ + "solubility_mass_phase_comp", + "solubility_mass_frac_phase_comp", + "conc_mass_phase_comp", + ] + for v_str in v_str_lst_comp: + if self.is_property_constructed(v_str): + v_comp = getattr(self, v_str) + c_comp = getattr(self, "eq_" + v_str) + for j, c in c_comp.items(): + sf = iscale.get_scaling_factor( + v_comp["Liq", j], default=1, warning=True + ) + iscale.constraint_scaling_transform(c, sf) + + # Property relationship indexed by single component + if self.is_property_constructed("dh_crystallization_mass_comp"): + sf = iscale.get_scaling_factor(self.dh_crystallization_mass_comp["NaCl"]) + iscale.constraint_scaling_transform( + self.eq_dh_crystallization_mass_comp, sf + ) + + # Property relationships with phase index and indexed constraints + v_str_lst_phase = [ + "dens_mass_solvent", + "dens_mass_solute", + "flow_vol_phase", + "enth_mass_solvent", + "enth_mass_solute", + "cp_mass_solvent", + "cp_mass_solute", + ] + for v_str in v_str_lst_phase: + if self.is_property_constructed(v_str): + v = getattr(self, v_str) + c_phase = getattr(self, "eq_" + v_str) + for ind, c in c_phase.items(): + sf = iscale.get_scaling_factor(v[ind], default=1, warning=True) + iscale.constraint_scaling_transform(c, sf) + + # Property relationships indexed by component and phase + v_str_lst_phase_comp = [ + "mass_frac_phase_comp", + "flow_mol_phase_comp", + "mole_frac_phase_comp", + ] + for v_str in v_str_lst_phase_comp: + if self.is_property_constructed(v_str): + v_comp = getattr(self, v_str) + c_comp = getattr(self, "eq_" + v_str) + for j, c in c_comp.items(): + sf = iscale.get_scaling_factor(v_comp[j], default=1, warning=True) + iscale.constraint_scaling_transform(c, sf) \ No newline at end of file diff --git a/src/watertap_contrib/reflo/property_models/tests/test_cryst_prop_pack.py b/src/watertap_contrib/reflo/property_models/tests/test_cryst_prop_pack.py new file mode 100644 index 00000000..239a8759 --- /dev/null +++ b/src/watertap_contrib/reflo/property_models/tests/test_cryst_prop_pack.py @@ -0,0 +1,806 @@ +################################################################################# +# WaterTAP Copyright (c) 2020-2024, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# National Renewable Energy Laboratory, and National Energy Technology +# Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +################################################################################# +import pytest +import watertap.property_models.unit_specific.cryst_prop_pack as props +from pyomo.environ import ConcreteModel +from idaes.core import FlowsheetBlock, ControlVolume0DBlock +from idaes.models.properties.tests.test_harness import ( + PropertyTestHarness as PropertyTestHarness_idaes, +) +from watertap.property_models.tests.property_test_harness import ( + PropertyTestHarness, + PropertyRegressionTest, + PropertyCalculateStateTest, +) + + +# ----------------------------------------------------------------------------- + + +class TestNaClProperty_idaes(PropertyTestHarness_idaes): + def configure(self): + self.prop_pack = props.NaClParameterBlock + self.param_args = {} + self.prop_args = {} + self.has_density_terms = False + + +class TestDefaultNaClwaterProperty: + + # Create block and stream for running default tests + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + m.fs.properties = props.NaClParameterBlock( + heat_of_crystallization_model=props.HeatOfCrystallizationModel.constant + ) + m.fs.stream = m.fs.properties.build_state_block([0], defined_state=True) + + m.fs.cv = ControlVolume0DBlock( + dynamic=False, has_holdup=False, property_package=m.fs.properties + ) + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_material_balances() + m.fs.cv.add_energy_balances() + m.fs.cv.add_momentum_balances() + + # Create instance of PropertyTesthARNESS class and add attributes needed for tests + xv = PropertyTestHarness() + + xv.stateblock_statistics = { + "number_variables": 43, + "number_total_constraints": 37, + "number_unused_variables": 0, + "default_degrees_of_freedom": 6, + } + + xv.scaling_args = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 1e0, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e1, + ("flow_mol_phase_comp", ("Sol", "NaCl")): 1e2, + ("flow_mol_phase_comp", ("Vap", "H2O")): 1e3, + } + + xv.default_solution = { + ("solubility_mass_phase_comp", ("Liq", "NaCl")): 359.51, + ("solubility_mass_frac_phase_comp", ("Liq", "NaCl")): 0.265, + ("dens_mass_solvent", "Liq"): 996.89, + ("dens_mass_solvent", "Vap"): 0.7363, + ("dens_mass_solute", "Liq"): 3199.471, + ("dens_mass_solute", "Sol"): 2115, + ("dens_mass_phase", "Liq"): 1021.50, + ("dh_vap_mass_solvent", None): 2441.80, + ("cp_mass_solvent", "Liq"): 4186.52, + ("cp_mass_solvent", "Vap"): 1864.52, + ("cp_mass_solute", "Sol"): 864.15, # cp_mass_solute liquid ignored for now + ("cp_mass_phase", "Liq"): 4008.30, + ("flow_vol_phase", "Liq"): 9.79e-4, + ("flow_vol_phase", "Sol"): 0, + ("flow_vol_phase", "Vap"): 0, + ("pressure_sat", None): 2932.43, + ("temperature_sat_solvent", None): 296.79, + ("conc_mass_phase_comp", ("Liq", "NaCl")): 35.753, + ("conc_mass_phase_comp", ("Liq", "H2O")): 985.753, + ("enth_mass_solvent", "Liq"): 104.9212, + ("enth_mass_solvent", "Vap"): 2546.7289, + ("enth_mass_solute", "Sol"): 21.4739, + ("enth_mass_phase", "Liq"): 101.0922, + ("dh_crystallization_mass_comp", "NaCl"): -520, + ("mass_frac_phase_comp", ("Liq", "H2O")): 0.965, + ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.035, + ("mass_frac_phase_comp", ("Sol", "NaCl")): 1.0, + ("mass_frac_phase_comp", ("Vap", "H2O")): 1.0, + ("flow_mol_phase_comp", ("Liq", "H2O")): 53.57, + ("flow_mol_phase_comp", ("Liq", "NaCl")): 0.5989, + ("flow_mol_phase_comp", ("Sol", "NaCl")): 0.0, + ("flow_mol_phase_comp", ("Vap", "H2O")): 0.0, + ("mole_frac_phase_comp", ("Liq", "H2O")): 0.9889, + ("mole_frac_phase_comp", ("Liq", "NaCl")): 0.01106, + ("mole_frac_phase_comp", ("Sol", "NaCl")): 1.0, + ("mole_frac_phase_comp", ("Vap", "H2O")): 1.0, + } + + # Configure class + xv.configure_class(m) + + @pytest.mark.unit + def test_components_phases(self): + self.xv.test_components_phases(self.m) + + @pytest.mark.unit + def test_parameters(self): + self.xv.test_parameters(self.m) + + @pytest.mark.unit + def test_state_variables(self): + self.xv.test_state_variables(self.m) + + @pytest.mark.unit + def test_permanent_properties(self): + self.xv.test_permanent_properties(self.m) + + @pytest.mark.unit + def test_on_demand_properties(self): + self.xv.test_on_demand_properties(self.m) + + @pytest.mark.unit + def test_stateblock_statistics(self): + self.xv.test_stateblock_statistics(self.m) + + @pytest.mark.unit + def test_units_consistent(self): + self.xv.test_units_consistent(self.m) + + @pytest.mark.unit + def test_scaling(self): + self.xv.test_scaling(self.m) + + @pytest.mark.component + def test_default_initialization(self): + self.xv.test_default_initialization(self.m) + + @pytest.mark.component + def test_property_control_volume(self): + self.xv.test_property_control_volume(self.m) + + +@pytest.mark.component +class TestNaClPropertySolution_1(PropertyRegressionTest): + # Test pure liquid solution 1 - same solution as NaCl prop pack + def configure(self): + self.prop_pack = props.NaClParameterBlock + self.param_args = {} + + self.scaling_args = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 1e0, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e2, + ("flow_mass_phase_comp", ("Vap", "H2O")): 1e2, + } + + self.state_args = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 0.95, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 0.05, + ("flow_mass_phase_comp", ("Sol", "NaCl")): 0.0, + ("flow_mass_phase_comp", ("Vap", "H2O")): 0.0, + ("temperature", None): 273.15 + 25, + ("pressure", None): 50e5, + } + + self.regression_solution = { + ("mass_frac_phase_comp", ("Liq", "H2O")): 0.95, + ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.05, + ("solubility_mass_phase_comp", ("Liq", "NaCl")): 359.50, + ("solubility_mass_frac_phase_comp", ("Liq", "NaCl")): 0.265, + ("dens_mass_solvent", "Liq"): 996.89, + ("dens_mass_solvent", "Vap"): 36.335, + ("dens_mass_phase", "Liq"): 1032.2, + ("flow_vol_phase", "Liq"): 9.687e-4, + ("conc_mass_phase_comp", ("Liq", "H2O")): 980.6, + ("conc_mass_phase_comp", ("Liq", "NaCl")): 51.61, + ("flow_mol_phase_comp", ("Liq", "H2O")): 52.73, + ("flow_mol_phase_comp", ("Liq", "NaCl")): 0.8556, + ("mole_frac_phase_comp", ("Liq", "H2O")): 0.9840, + ("mole_frac_phase_comp", ("Liq", "NaCl")): 1.597e-2, + ("cp_mass_solvent", "Liq"): 4186.52, + ("cp_mass_phase", "Liq"): 3940.03, + ("enth_mass_solvent", "Liq"): 104.92, + ("enth_mass_phase", "Liq"): 99.45, + } + + +@pytest.mark.component +class TestNaClPropertySolution_2(PropertyRegressionTest): + # Test pure liquid solution 2 - same solution as NaCl prop pack + def configure(self): + self.prop_pack = props.NaClParameterBlock + self.param_args = {} + + self.scaling_args = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 1e0, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e2, + ("flow_mass_phase_comp", ("Vap", "H2O")): 1e2, + } + + self.state_args = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 0.74, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 0.26, + ("flow_mass_phase_comp", ("Sol", "NaCl")): 0.0, + ("flow_mass_phase_comp", ("Vap", "H2O")): 0.0, + ("temperature", None): 273.15 + 25, + ("pressure", None): 50e5, + } + + self.regression_solution = { + ("mass_frac_phase_comp", ("Liq", "H2O")): 0.74, + ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.26, + ("solubility_mass_phase_comp", ("Liq", "NaCl")): 359.50, + ("solubility_mass_frac_phase_comp", ("Liq", "NaCl")): 0.265, + ("dens_mass_solvent", "Liq"): 996.89, + ("dens_mass_phase", "Liq"): 1193.4, + ("flow_vol_phase", "Liq"): 8.379e-4, + ("conc_mass_phase_comp", ("Liq", "H2O")): 883.14, + ("conc_mass_phase_comp", ("Liq", "NaCl")): 310.29, + ("flow_mol_phase_comp", ("Liq", "H2O")): 41.08, + ("flow_mol_phase_comp", ("Liq", "NaCl")): 4.449, + ("mole_frac_phase_comp", ("Liq", "H2O")): 0.9022, + ("mole_frac_phase_comp", ("Liq", "NaCl")): 9.773e-2, + ("cp_mass_solvent", "Liq"): 4186.52, + ("cp_mass_phase", "Liq"): 3276.56, + ("enth_mass_solvent", "Liq"): 104.92, + ("enth_mass_phase", "Liq"): 68.78, + } + + +@pytest.mark.component +class TestNaClPropertySolution_3(PropertyRegressionTest): + # Test pure liquid solution 3 - same solution as NaCl prop pack + def configure(self): + self.prop_pack = props.NaClParameterBlock + self.param_args = {} + + self.scaling_args = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 1e0, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e3, + ("flow_mass_phase_comp", ("Vap", "H2O")): 1e3, + } + + self.state_args = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 0.999, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 0.001, + ("flow_mass_phase_comp", ("Sol", "NaCl")): 0.0, + ("flow_mass_phase_comp", ("Vap", "H2O")): 0.0, + ("temperature", None): 273.15 + 25, + ("pressure", None): 10e5, + } + + self.regression_solution = { + ("mass_frac_phase_comp", ("Liq", "H2O")): 0.999, + ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.001, + ("solubility_mass_phase_comp", ("Liq", "NaCl")): 359.50, + ("solubility_mass_frac_phase_comp", ("Liq", "NaCl")): 0.265, + ("dens_mass_solvent", "Liq"): 996.89, + ("dens_mass_solvent", "Vap"): 7.267, + ("dens_mass_phase", "Liq"): 997.59, + ("flow_vol_phase", "Liq"): 1.002e-3, + ("conc_mass_phase_comp", ("Liq", "H2O")): 996.59, + ("conc_mass_phase_comp", ("Liq", "NaCl")): 0.9976, + ("flow_mol_phase_comp", ("Liq", "H2O")): 55.45, + ("flow_mol_phase_comp", ("Liq", "NaCl")): 1.711e-2, + ("mole_frac_phase_comp", ("Liq", "H2O")): 0.9997, + ("mole_frac_phase_comp", ("Liq", "NaCl")): 3.084e-4, + ("cp_mass_solvent", "Liq"): 4186.52, + ("cp_mass_phase", "Liq"): 4180.98, + ("enth_mass_solvent", "Liq"): 104.92, + ("enth_mass_phase", "Liq"): 104.40, + } + + +@pytest.mark.requires_idaes_solver +@pytest.mark.component +class TestNaClPropertySolution_4(PropertyRegressionTest): + # Test pure solid solution 1 - check solid properties + def configure(self): + + self.prop_pack = props.NaClParameterBlock + self.param_args = {} + + self.scaling_args = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 1e1, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e3, + ("flow_mass_phase_comp", ("Sol", "NaCl")): 1e0, + ("flow_mass_phase_comp", ("Vap", "H2O")): 1e2, + } + + self.state_args = { + ("flow_vol_phase", "Liq"): 0, + ("flow_vol_phase", "Vap"): 0, + ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.05, + ("flow_mass_phase_comp", ("Sol", "NaCl")): 1.0, + ("temperature", None): 273.15 + 25, + ("pressure", None): 5e5, + } + + self.regression_solution = { + ("mass_frac_phase_comp", ("Sol", "NaCl")): 1.0, + ("dens_mass_solute", "Sol"): 2115, + ("cp_mass_solute", "Sol"): 864.16, + ("flow_vol_phase", "Sol"): 1 / 2115, # mass floe / density + ("cp_mass_solute", "Sol"): 864.16, + ("enth_mass_solute", "Sol"): 21.474, + ("dh_crystallization_mass_comp", "NaCl"): -520, + ("flow_mol_phase_comp", ("Sol", "NaCl")): 1 / 58.44e-3, # mass flow / mw + ("mole_frac_phase_comp", ("Sol", "NaCl")): 1.0, + } + + +@pytest.mark.requires_idaes_solver +@pytest.mark.component +class TestNaClPropertySolution_5(PropertyRegressionTest): + # Test pure vapor solution 1 - check vapor properties + def configure(self): + + self.prop_pack = props.NaClParameterBlock + self.param_args = {} + + self.scaling_args = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 1e1, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e3, + ("flow_mass_phase_comp", ("Sol", "NaCl")): 1e3, + ("flow_mass_phase_comp", ("Vap", "H2O")): 1e0, + } + + self.state_args = { + ("flow_vol_phase", "Liq"): 0, + ("flow_vol_phase", "Sol"): 0, + ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.05, + ("flow_mass_phase_comp", ("Vap", "H2O")): 1.0, + ("temperature", None): 273.15 + 25, + ("pressure", None): 5e5, + } + + self.regression_solution = { + ("mass_frac_phase_comp", ("Vap", "H2O")): 1.0, + ("dens_mass_solvent", "Vap"): 3.633, + ("dh_vap_mass_solvent", None): 2441.808, + ("cp_mass_solvent", "Vap"): 1864.52, + ("flow_vol_phase", "Vap"): 1 / 3.633, # mass flow / density + ("pressure_sat", None): 2905.28, + ("flow_mol_phase_comp", ("Vap", "H2O")): 1 / 18.01528e-3, # mass flow / mw + ("mole_frac_phase_comp", ("Vap", "H2O")): 1.0, + } + + +@pytest.mark.component +class TestNaClPropertySolution_6(PropertyRegressionTest): + # Test for S-L-V solution 1 with similar magnitude flowrates in all phases and high liquid salt. conc. - check all properties + def configure(self): + self.prop_pack = props.NaClParameterBlock + self.param_args = {} + + self.scaling_args = { + ("flow_mass_phase_comp", ("Sol", "NaCl")): 10, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e1, + } + + self.state_args = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 0.75, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 0.25, + ("flow_mass_phase_comp", ("Sol", "NaCl")): 0.25, + ("flow_mass_phase_comp", ("Vap", "H2O")): 0.25, + ("temperature", None): 273.15 + 50, + ("pressure", None): 5e5, + } + + self.regression_solution = { + ("solubility_mass_phase_comp", ("Liq", "NaCl")): 362.93, + ("solubility_mass_frac_phase_comp", ("Liq", "NaCl")): 0.2686, + ("dens_mass_solvent", "Liq"): 988.04, + ("dens_mass_solvent", "Vap"): 3.352, + ("dens_mass_solute", "Liq"): 2645.21, + ("dens_mass_solute", "Sol"): 2115, + ("dens_mass_phase", "Liq"): 1171.53, + ("dh_vap_mass_solvent", None): 2382.08, + ("cp_mass_solvent", "Liq"): 4180.92, + ("cp_mass_solvent", "Vap"): 1871.21, + ("cp_mass_solute", "Sol"): 873.34, + ("cp_mass_phase", "Liq"): 3300.83, + ("flow_vol_phase", "Liq"): 8.53e-4, + ("flow_vol_phase", "Sol"): 1.182e-4, + ("flow_vol_phase", "Vap"): 7.46e-2, + ("pressure_sat", None): 9799.91, + ("temperature_sat_solvent", None): 318.52, + ("conc_mass_phase_comp", ("Liq", "NaCl")): 292.88, + ("conc_mass_phase_comp", ("Liq", "H2O")): 878.65, + ("enth_mass_solvent", "Liq"): 209.40, + ("enth_mass_solvent", "Vap"): 2591.48, + ("enth_mass_solute", "Sol"): 43.19, + ("enth_mass_phase", "Liq"): 152.36, + ("dh_crystallization_mass_comp", "NaCl"): -520, + ("mass_frac_phase_comp", ("Liq", "H2O")): 0.75, + ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.25, + ("mass_frac_phase_comp", ("Sol", "NaCl")): 1, + ("mass_frac_phase_comp", ("Vap", "H2O")): 1, + ("flow_mol_phase_comp", ("Liq", "H2O")): 41.63, + ("flow_mol_phase_comp", ("Liq", "NaCl")): 4.28, + ("flow_mol_phase_comp", ("Sol", "NaCl")): 4.28, + ("flow_mol_phase_comp", ("Vap", "H2O")): 13.88, + ("mole_frac_phase_comp", ("Liq", "H2O")): 0.9068, + ("mole_frac_phase_comp", ("Liq", "NaCl")): 0.0932, + ("mole_frac_phase_comp", ("Sol", "NaCl")): 1.0, + ("mole_frac_phase_comp", ("Vap", "H2O")): 1.0, + } + + +@pytest.mark.component +class TestNaClPropertySolution_7(PropertyRegressionTest): + # Test for S-L-V solution 2 with flowrates in all phases of same magnitude but low liquid salt. conc. - check all properties + def configure(self): + self.prop_pack = props.NaClParameterBlock + self.param_args = {} + + self.scaling_args = { + ("flow_mass_phase_comp", ("Sol", "NaCl")): 10, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e3, + } + + self.state_args = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 0.999, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 0.001, + ("flow_mass_phase_comp", ("Sol", "NaCl")): 0.25, + ("flow_mass_phase_comp", ("Vap", "H2O")): 0.25, + ("temperature", None): 273.15 + 50, + ("pressure", None): 5e5, + } + + self.regression_solution = { + ("solubility_mass_phase_comp", ("Liq", "NaCl")): 362.93, + ("solubility_mass_frac_phase_comp", ("Liq", "NaCl")): 0.2686, + ("dens_mass_solvent", "Liq"): 988.04, + ("dens_mass_solvent", "Vap"): 3.352, + ("dens_mass_solute", "Liq"): 3073.05, + ("dens_mass_solute", "Sol"): 2115, + ("dens_mass_phase", "Liq"): 988.72, + ("dh_vap_mass_solvent", None): 2382.08, + ("cp_mass_solvent", "Liq"): 4180.92, + ("cp_mass_solvent", "Vap"): 1871.21, + ("cp_mass_solute", "Sol"): 873.34, + ("cp_mass_phase", "Liq"): 4175.95, + ("flow_vol_phase", "Liq"): 1.011e-3, + ("flow_vol_phase", "Sol"): 1.182e-4, + ("flow_vol_phase", "Vap"): 7.46e-2, + ("pressure_sat", None): 12614.93, + ("temperature_sat_solvent", None): 323.55, + ("conc_mass_phase_comp", ("Liq", "NaCl")): 0.9887, + ("conc_mass_phase_comp", ("Liq", "H2O")): 987.73, + ("enth_mass_solvent", "Liq"): 209.40, + ("enth_mass_solvent", "Vap"): 2591.48, + ("enth_mass_solute", "Sol"): 43.19, + ("enth_mass_phase", "Liq"): 208.81, + ("dh_crystallization_mass_comp", "NaCl"): -520, + ("mass_frac_phase_comp", ("Liq", "H2O")): 0.999, + ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.001, + ("mass_frac_phase_comp", ("Sol", "NaCl")): 1, + ("mass_frac_phase_comp", ("Vap", "H2O")): 1, + ("flow_mol_phase_comp", ("Liq", "H2O")): 55.45, + ("flow_mol_phase_comp", ("Liq", "NaCl")): 0.0171, + ("flow_mol_phase_comp", ("Sol", "NaCl")): 4.28, + ("flow_mol_phase_comp", ("Vap", "H2O")): 13.88, + ("mole_frac_phase_comp", ("Liq", "H2O")): 0.999, + ("mole_frac_phase_comp", ("Liq", "NaCl")): 3.084e-4, + ("mole_frac_phase_comp", ("Sol", "NaCl")): 1.0, + ("mole_frac_phase_comp", ("Vap", "H2O")): 1.0, + } + + +@pytest.mark.component +class TestNaClPropertySolution_8(PropertyRegressionTest): + # Test for S-L-V solution 3 with outlet data from Dutta et al. - proper crystallization system + # # Dutta recorded properties for solids and liquids at crystallizer temperature: + # # Solubility @ 55C: 0.27 kg/kg + # # Heat of vaporization @ 55C: 2400 kJ/kg + # # Vapor density @ 55C: 68.7e-3 kg/m3 + # # Solid heat capacity: 877 J/kgK + # # Liquid heat capacity @ 20 C: 3290 kJ/kgK + # # Liquid density @ 20 C : 1185 kg/m3 + + def configure(self): + self.prop_pack = props.NaClParameterBlock + self.param_args = {} + + self.scaling_args = { + ("flow_mass_phase_comp", ("Sol", "NaCl")): 1e0, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e0, + ("flow_mass_phase_comp", ("Vap", "H2O")): 1e-1, + } + + self.state_args = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 18.37, # 84t/h + ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.2126, + ("flow_mass_phase_comp", ("Sol", "NaCl")): 5.55, # 20t/h + ("flow_mass_phase_comp", ("Vap", "H2O")): 20.55, # 74t/h + ("temperature", None): 273.15 + 20, + ("pressure", None): 10e3, + } + + self.regression_solution = { + ("solubility_mass_phase_comp", ("Liq", "NaCl")): 358.88, + ("solubility_mass_frac_phase_comp", ("Liq", "NaCl")): 0.2644, + ("dens_mass_solvent", "Liq"): 998.02, + ("dens_mass_solvent", "Vap"): 73.91e-3, + ("dens_mass_solute", "Liq"): 2847.63, + ("dens_mass_solute", "Sol"): 2115, + ("dens_mass_phase", "Liq"): 1157.91, + ("dh_vap_mass_solvent", None): 2453.66, + ("cp_mass_solvent", "Liq"): 4189.43, + ("cp_mass_solvent", "Vap"): 1863.46, + ("cp_mass_solute", "Sol"): 862.16, + ("cp_mass_phase", "Liq"): 3380.07, + ("flow_vol_phase", "Liq"): 0.02015, + ("flow_vol_phase", "Sol"): 2.624e-3, + ("flow_vol_phase", "Vap"): 278.04, + ("pressure_sat", None): 1679.64, + ("temperature_sat_solvent", None): 287.88, + ("conc_mass_phase_comp", ("Liq", "NaCl")): 246.17, + ("conc_mass_phase_comp", ("Liq", "H2O")): 911.74, + ("enth_mass_solvent", "Liq"): 84.00, + ("enth_mass_solvent", "Vap"): 2537.66, + ("enth_mass_solute", "Sol"): 17.16, + ("enth_mass_phase", "Liq"): 59.03, + ("dh_crystallization_mass_comp", "NaCl"): -520, + ("mass_frac_phase_comp", ("Liq", "H2O")): 0.7874, + ("mass_frac_phase_comp", ("Sol", "NaCl")): 1, + ("mass_frac_phase_comp", ("Vap", "H2O")): 1, + ("flow_mol_phase_comp", ("Liq", "H2O")): 1019.69, + ("flow_mol_phase_comp", ("Liq", "NaCl")): 84.88, + ("flow_mol_phase_comp", ("Sol", "NaCl")): 94.97, + ("flow_mol_phase_comp", ("Vap", "H2O")): 1140.698, + ("mole_frac_phase_comp", ("Liq", "H2O")): 0.9232, + ("mole_frac_phase_comp", ("Liq", "NaCl")): 0.0768, + ("mole_frac_phase_comp", ("Sol", "NaCl")): 1.0, + ("mole_frac_phase_comp", ("Vap", "H2O")): 1.0, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 4.96, + } + + +@pytest.mark.component +class TestNaClPropertySolution_9(PropertyRegressionTest): + # Test for S-L-V solution 4 with outlet data from Dutta et al. - proper crystallization system + # # Dutta recorded properties for solids and liquids at crystallizer temperature: + # # Solubility @ 55C: 0.27 kg/kg + # # Heat of vaporization @ 55C: 2400 kJ/kg + # # Vapor density @ 55C: 68.7e-3 kg/m3 + # # Solid heat capacity: 877 J/kgK + # # Liquid heat capacity @ 20 C: 3290 kJ/kgK + # # Liquid density @ 20 C : 1185 kg/m3 + + def configure(self): + self.prop_pack = props.NaClParameterBlock + self.param_args = {} + + self.scaling_args = { + ("flow_mass_phase_comp", ("Sol", "NaCl")): 1e0, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e0, + ("flow_mass_phase_comp", ("Vap", "H2O")): 1e-1, + } + + self.state_args = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 18.37, # 84t/h + ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.2126, + ("flow_mass_phase_comp", ("Sol", "NaCl")): 5.55, # 20t/h + ("flow_mass_phase_comp", ("Vap", "H2O")): 20.55, # 74t/h + ("temperature", None): 273.15 + 55, + ("pressure", None): 10e3, + } + + self.regression_solution = { + ("solubility_mass_phase_comp", ("Liq", "NaCl")): 363.71, + ("solubility_mass_frac_phase_comp", ("Liq", "NaCl")): 0.2695, + ("dens_mass_solvent", "Liq"): 985.71, + ("dens_mass_solvent", "Vap"): 66.03e-3, + ("dens_mass_solute", "Liq"): 2694.61, + ("dens_mass_solute", "Sol"): 2115, + ("dens_mass_phase", "Liq"): 1139.33, + ("dh_vap_mass_solvent", None): 2369.98, + ("cp_mass_solvent", "Liq"): 4181.59, + ("cp_mass_solvent", "Vap"): 1872.79, + ("cp_mass_solute", "Sol"): 875.04, + ("cp_mass_phase", "Liq"): 3390.43, + ("flow_vol_phase", "Liq"): 0.02047, + ("flow_vol_phase", "Sol"): 2.624e-3, + ("flow_vol_phase", "Vap"): 311.233, + ("pressure_sat", None): 13201.77, + ("temperature_sat_solvent", None): 324.47, + ("conc_mass_phase_comp", ("Liq", "NaCl")): 242.22, + ("conc_mass_phase_comp", ("Liq", "H2O")): 897.107, + ("enth_mass_solvent", "Liq"): 230.30, + ("enth_mass_solvent", "Vap"): 2600.279, + ("enth_mass_solute", "Sol"): 47.57, + ("enth_mass_phase", "Liq"): 177.39, + ("dh_crystallization_mass_comp", "NaCl"): -520, + ("mass_frac_phase_comp", ("Liq", "H2O")): 0.7874, + ("mass_frac_phase_comp", ("Sol", "NaCl")): 1, + ("mass_frac_phase_comp", ("Vap", "H2O")): 1, + ("flow_mol_phase_comp", ("Liq", "H2O")): 1019.69, + ("flow_mol_phase_comp", ("Liq", "NaCl")): 84.88, + ("flow_mol_phase_comp", ("Sol", "NaCl")): 94.97, + ("flow_mol_phase_comp", ("Vap", "H2O")): 1140.698, + ("mole_frac_phase_comp", ("Liq", "H2O")): 0.9232, + ("mole_frac_phase_comp", ("Liq", "NaCl")): 0.0768, + ("mole_frac_phase_comp", ("Sol", "NaCl")): 1.0, + ("mole_frac_phase_comp", ("Vap", "H2O")): 1.0, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 4.96, + } + + +@pytest.mark.component +class TestNaClCalculateState_1(PropertyCalculateStateTest): + # Test pure liquid solution with mass fractions + def configure(self): + self.prop_pack = props.NaClParameterBlock + self.param_args = {} + + self.scaling_args = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 1e-1, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e0, + ("flow_mass_phase_comp", ("Vap", "H2O")): 1, + ("flow_mass_phase_comp", ("Sol", "NaCl")): 1, + } + + self.var_args = { + ("flow_vol_phase", "Liq"): 2e-2, + ("flow_vol_phase", "Sol"): 0, + ("flow_vol_phase", "Vap"): 0, + ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.05, + ("temperature", None): 273.15 + 25, + ("pressure", None): 5e5, + } + + self.state_solution = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 19.6, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 1.032, + ("flow_mass_phase_comp", ("Sol", "NaCl")): 0, + ("flow_mass_phase_comp", ("Vap", "H2O")): 0, + } + + +@pytest.mark.component +class TestNaClCalculateState_2(PropertyCalculateStateTest): + # Test pure liquid with mole fractions + def configure(self): + self.prop_pack = props.NaClParameterBlock + self.param_args = {} + + self.scaling_args = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 1e1, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e2, + # The rest are expected to be zero + ("flow_mass_phase_comp", ("Sol", "NaCl")): 1, + ("flow_mass_phase_comp", ("Vap", "H2O")): 1, + } + self.var_args = { + ("flow_vol_phase", "Liq"): 2e-4, + ("flow_vol_phase", "Sol"): 0, + ("flow_vol_phase", "Vap"): 0, + ("mole_frac_phase_comp", ("Liq", "NaCl")): 0.05, + ("temperature", None): 273.15 + 25, + ("pressure", None): 5e5, + } + self.state_solution = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 18.84e-2, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 3.215e-2, + ("flow_mass_phase_comp", ("Sol", "NaCl")): 0, + ("flow_mass_phase_comp", ("Vap", "H2O")): 0, + } + + +@pytest.mark.component +class TestNaClCalculateState_3(PropertyCalculateStateTest): + # Test pure liquid solution with pressure_sat defined instead of temperature + def configure(self): + self.prop_pack = props.NaClParameterBlock + self.param_args = {} + + self.scaling_args = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 1e0, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e1, + ("flow_mass_phase_comp", ("Sol", "NaCl")): 1e-1, + ("flow_mass_phase_comp", ("Vap", "H2O")): 1e2, + } + + self.var_args = { + ("flow_vol_phase", "Liq"): 2e-2, + ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.05, + ("flow_vol_phase", "Sol"): 0, + ("flow_vol_phase", "Vap"): 0, + ("pressure_sat", None): 2905, + ("pressure", None): 5e5, + } + + self.state_solution = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 19.6, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 1.032, + ("flow_mass_phase_comp", ("Sol", "NaCl")): 0, + ("flow_mass_phase_comp", ("Vap", "H2O")): 0, + ("temperature", None): 273.15 + 25, + } + + +@pytest.mark.component +class TestNaClCalculateState_4(PropertyCalculateStateTest): + # Test pure solid solution with mass fractions + def configure(self): + + self.prop_pack = props.NaClParameterBlock + self.param_args = {} + + self.scaling_args = { + ("flow_mass_phase_comp", ("Sol", "NaCl")): 1e-1, + ("flow_mass_phase_comp", ("Vap", "H2O")): 1e-1, + } + + self.var_args = { + ("flow_vol_phase", "Liq"): 0, + ("mass_frac_phase_comp", ("Liq", "NaCl")): 0, + ("flow_vol_phase", "Sol"): 2e-2, + ("flow_vol_phase", "Vap"): 0, + ("temperature", None): 273.15 + 25, + ("pressure", None): 5e5, + } + + self.state_solution = { + ("flow_mass_phase_comp", ("Sol", "NaCl")): 2115 + * 2e-2, # solid density is constant + ("flow_mass_phase_comp", ("Liq", "H2O")): 0, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 0, + ("flow_mass_phase_comp", ("Vap", "H2O")): 0, + } + + +@pytest.mark.component +class TestNaClCalculateState_5(PropertyCalculateStateTest): + # Test solid-liquid-vapor mixture solution with mass fractions + def configure(self): + self.prop_pack = props.NaClParameterBlock + self.param_args = {} + + self.scaling_args = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 1e-1, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e1, + ("flow_mass_phase_comp", ("Sol", "NaCl")): 1e-2, + } + + self.var_args = { + ("flow_vol_phase", "Liq"): 2e-2, + ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.05, + ("flow_vol_phase", "Sol"): 2e-2, + ("flow_vol_phase", "Vap"): 2e-2, + ("temperature", None): 273.15 + 25, + ("pressure", None): 5e5, + } + + self.state_solution = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 19.6, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 1.032, + ("flow_mass_phase_comp", ("Vap", "H2O")): 0.07265, + ("flow_mass_phase_comp", ("Sol", "NaCl")): 42.3, + } + + +@pytest.mark.component +class TestNaClCalculateState_6(PropertyCalculateStateTest): + # Test liquid-solid-vapor mixture with mole fractions + def configure(self): + self.prop_pack = props.NaClParameterBlock + self.param_args = {} + + self.scaling_args = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 1e1, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e2, + ("flow_mass_phase_comp", ("Sol", "NaCl")): 1e2, + ("flow_mass_phase_comp", ("Vap", "H2O")): 1e3, + } + + self.var_args = { + ("flow_vol_phase", "Liq"): 2e-4, + ("flow_vol_phase", "Sol"): 2e-4, + ("flow_vol_phase", "Vap"): 2e-4, + ("mole_frac_phase_comp", ("Liq", "NaCl")): 0.05, + ("temperature", None): 273.15 + 25, + ("pressure", None): 5e5, + } + + self.state_solution = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 18.84e-2, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 3.215e-2, + ("flow_mass_phase_comp", ("Sol", "NaCl")): 0.423, + ("flow_mass_phase_comp", ("Vap", "H2O")): 3.632 + * 2e-4, # Density from ideal gas law * vol. flow + } \ No newline at end of file diff --git a/src/watertap_contrib/reflo/unit_models/zero_order/crystallizer_zo_watertap.py b/src/watertap_contrib/reflo/unit_models/zero_order/crystallizer_zo_watertap.py new file mode 100644 index 00000000..8b5db860 --- /dev/null +++ b/src/watertap_contrib/reflo/unit_models/zero_order/crystallizer_zo_watertap.py @@ -0,0 +1,868 @@ +################################################################################# +# WaterTAP Copyright (c) 2020-2024, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# National Renewable Energy Laboratory, and National Energy Technology +# Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +################################################################################# + +from copy import deepcopy + +# Import Pyomo libraries +from pyomo.environ import ( + Var, + check_optimal_termination, + Param, + Constraint, + Suffix, + units as pyunits, +) +from pyomo.common.config import ConfigBlock, ConfigValue, In + +# Import IDAES cores +from idaes.core import ( + declare_process_block_class, + UnitModelBlockData, + useDefault, +) +from watertap.core.solvers import get_solver +from idaes.core.util.tables import create_stream_table_dataframe +from idaes.core.util.constants import Constants +from idaes.core.util.config import is_physical_parameter_block + +from idaes.core.util.exceptions import InitializationError + +import idaes.core.util.scaling as iscale +import idaes.logger as idaeslog + +from watertap.core import InitializationMixin +from watertap.core.util.initialization import interval_initializer +from watertap.costing.unit_models.crystallizer import cost_crystallizer + +_log = idaeslog.getLogger(__name__) + +__author__ = "Oluwamayowa Amusat" + + +# when using this file the name "Filtration" is what is imported +@declare_process_block_class("Crystallization") +class CrystallizationData(InitializationMixin, UnitModelBlockData): + """ + Zero order crystallization model + """ + + # CONFIG are options for the unit model, this simple model only has the mandatory config options + 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. The filtration unit does not support dynamic + behavior, thus this must be False.""", + ), + ) + + 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. The filtration unit does not have defined volume, thus + this must be False.""", + ), + ) + + CONFIG.declare( + "property_package", + ConfigValue( + default=useDefault, + domain=is_physical_parameter_block, + description="Property package to use for control volume", + doc="""Property parameter object used to define property calculations, + **default** - useDefault. + **Valid values:** { + **useDefault** - use default package from parent model or flowsheet, + **PhysicalParameterObject** - a PhysicalParameterBlock object.}""", + ), + ) + + CONFIG.declare( + "property_package_args", + ConfigBlock( + implicit=True, + description="Arguments to use for constructing property packages", + doc="""A ConfigBlock with arguments to be passed to a property block(s) + and used when constructing these, + **default** - None. + **Valid values:** { + see property package for documentation.}""", + ), + ) + + def build(self): + super().build() + + solvent_set = self.config.property_package.solvent_set + solute_set = self.config.property_package.solute_set + + # this creates blank scaling factors, which are populated later + self.scaling_factor = Suffix(direction=Suffix.EXPORT) + + # Next, get the base units of measurement from the property definition + units_meta = self.config.property_package.get_metadata().get_derived_units + + # Add unit variables + + self.approach_temperature_heat_exchanger = Param( + initialize=4, + units=pyunits.K, + doc="Maximum temperature difference between inlet and outlet of a crystallizer heat exchanger.\ + Lewis et al. suggests 1-2 degC but use 5degC in example; Tavare example used 4 degC.\ + Default is 4 degC", + ) + + # ====== Crystallizer sizing parameters ================= # + self.dimensionless_crystal_length = Param( + initialize=3.67, # Parameter from population balance modeling for median crystal size + units=pyunits.dimensionless, + ) + + self.crystal_median_length = Var( + initialize=0.5e-3, # From Mersmann et al., Tavare et al. example + bounds=( + 0.2e-3, + 0.6e-3, + ), # Limits for FC crystallizers based on Bermingham et al. + units=pyunits.m, + doc="Desired median crystal size, m", + ) + + self.crystal_growth_rate = Var( + initialize=3.7e-8, # From Mersmann et al. for NaCl. Perry has values between 0.5e-8 to 13e-8 for NaCl + bounds=(1e-9, 1e-6), # Based on Mersmann and Kind diagram. + units=pyunits.m / pyunits.s, + doc="Crystal growth rate, m/s", + ) + + self.souders_brown_constant = Var( + initialize=0.04, + units=pyunits.m / pyunits.s, + doc="Constant for Souders-Brown equation, set at 0.04 m/s based on Dutta et al. \ + Lewis et al suggests 0.024 m/s, while Tavare suggests about 0.06 m/s ", + ) + + # ====== Model variables ================= # + self.crystallization_yield = Var( + solute_set, + initialize=0.5, + bounds=(0.0, 1), + units=pyunits.dimensionless, + doc="Crystallizer solids yield", + ) + + self.product_volumetric_solids_fraction = Var( + initialize=0.25, + bounds=(0.0, 1), + units=pyunits.dimensionless, + doc="Volumetric fraction of solids in slurry product (i.e. solids-liquid mixture).", + ) + + self.temperature_operating = Var( + initialize=298.15, + bounds=(273, 1000), + units=pyunits.K, + doc="Crystallizer operating temperature: boiling point of the solution.", + ) + + self.pressure_operating = Var( + initialize=1e3, + bounds=(0.001, 1e6), + units=pyunits.Pa, + doc="Operating pressure of the crystallizer.", + ) + + self.dens_mass_magma = Var( + initialize=250, + bounds=(1, 5000), + units=pyunits.kg / pyunits.m**3, + doc="Magma density, i.e. mass of crystals per unit volume of suspension", + ) + + self.dens_mass_slurry = Var( + initialize=1000, + bounds=(1, 5000), + units=pyunits.kg / pyunits.m**3, + doc="Suspension density, i.e. density of solid-liquid mixture before separation", + ) + + self.work_mechanical = Var( + self.flowsheet().config.time, + initialize=1e5, + bounds=(-5e6, 5e6), + units=pyunits.kJ / pyunits.s, + doc="Crystallizer thermal energy requirement", + ) + + self.energy_flow_superheated_vapor = Var( + initialize=1e5, + bounds=(-5e6, 5e6), + units=pyunits.kJ / pyunits.s, + doc="Energy could be supplied from vapor", + ) + + self.diameter_crystallizer = Var( + initialize=3, + bounds=(0, 25), + units=pyunits.m, + doc="Diameter of crystallizer", + ) + + self.height_slurry = Var( + initialize=3, + bounds=(0, 25), + units=pyunits.m, + doc="Slurry height in crystallizer", + ) + + self.height_crystallizer = Var( + initialize=3, bounds=(0, 25), units=pyunits.m, doc="Crystallizer height" + ) + + self.magma_circulation_flow_vol = Var( + initialize=1, + bounds=(0, 100), + units=pyunits.m**3 / pyunits.s, + doc="Minimum circulation flow rate through crystallizer heat exchanger", + ) + + self.relative_supersaturation = Var( + solute_set, initialize=0.1, bounds=(0, 100), units=pyunits.dimensionless + ) + + self.t_res = Var( + initialize=1, + bounds=(0, 10), + units=pyunits.hr, + doc="Residence time in crystallizer", + ) + + self.volume_suspension = Var( + initialize=1, + bounds=(0, None), + units=pyunits.m**3, + doc="Crystallizer minimum active volume, i.e. volume of liquid-solid suspension", + ) + + # Add state blocks for inlet, outlet, and waste + # These include the state variables and any other properties on demand + # Add inlet block + tmp_dict = dict(**self.config.property_package_args) + tmp_dict["has_phase_equilibrium"] = False + tmp_dict["parameters"] = self.config.property_package + tmp_dict["defined_state"] = True # inlet block is an inlet + self.properties_in = self.config.property_package.state_block_class( + self.flowsheet().config.time, doc="Material properties of inlet", **tmp_dict + ) + + # Add outlet and waste block + tmp_dict["defined_state"] = False # outlet and waste block is not an inlet + self.properties_out = self.config.property_package.state_block_class( + self.flowsheet().config.time, + doc="Material properties of liquid outlet", + **tmp_dict, + ) + + self.properties_solids = self.config.property_package.state_block_class( + self.flowsheet().config.time, + doc="Material properties of solid crystals at outlet", + **tmp_dict, + ) + + self.properties_vapor = self.config.property_package.state_block_class( + self.flowsheet().config.time, + doc="Material properties of water vapour at outlet", + **tmp_dict, + ) + + self.properties_pure_water = self.config.property_package.state_block_class( + self.flowsheet().config.time, + doc="Material properties of pure water vapour at outlet", + **tmp_dict, + ) + + # Add ports - oftentimes users interact with these rather than the state blocks + self.add_port(name="inlet", block=self.properties_in) + self.add_port(name="outlet", block=self.properties_out) + self.add_port(name="solids", block=self.properties_solids) + self.add_port(name="vapor", block=self.properties_vapor) + + # Add constraints + # 1. Material balances + @self.Constraint( + self.config.property_package.component_list, + doc="Mass balance for components", + ) + def eq_mass_balance_constraints(b, j): + return sum( + b.properties_in[0].flow_mass_phase_comp[p, j] + for p in self.config.property_package.phase_list + if (p, j) in b.properties_in[0].phase_component_set + ) == sum( + b.properties_out[0].flow_mass_phase_comp[p, j] + for p in self.config.property_package.phase_list + if (p, j) in b.properties_out[0].phase_component_set + ) + sum( + b.properties_vapor[0].flow_mass_phase_comp[p, j] + for p in self.config.property_package.phase_list + if (p, j) in b.properties_vapor[0].phase_component_set + ) + sum( + b.properties_solids[0].flow_mass_phase_comp[p, j] + for p in self.config.property_package.phase_list + if (p, j) in b.properties_solids[0].phase_component_set + ) + + @self.Constraint() + def eq_pure_vapor_flow_rate(b): + return ( + b.properties_pure_water[0].flow_mass_phase_comp["Vap", "H2O"] + == b.properties_vapor[0].flow_mass_phase_comp["Vap", "H2O"] + ) + + self.properties_pure_water[0].flow_mass_phase_comp["Liq", "H2O"].fix(1e-8) + self.properties_pure_water[0].flow_mass_phase_comp["Liq", "NaCl"].fix(0) + self.properties_pure_water[0].flow_mass_phase_comp["Sol", "NaCl"].fix(0) + self.properties_pure_water[0].mass_frac_phase_comp["Liq", "NaCl"] + self.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] + self.properties_in[0].flow_vol_phase["Liq"] + + # 2. Constraint on outlet liquid composition based on solubility requirements + @self.Constraint( + self.config.property_package.component_list, + doc="Solubility vs mass fraction constraint", + ) + def eq_solubility_massfrac_equality_constraint(b, j): + if j in solute_set: + return ( + b.properties_out[0].mass_frac_phase_comp["Liq", j] + - b.properties_out[0].solubility_mass_frac_phase_comp["Liq", j] + == 0 + ) + else: + return Constraint.Skip + + # 3. Performance equations + # (a) based on yield + @self.Constraint( + self.config.property_package.component_list, + doc="Component salt yield equation", + ) + def eq_removal_balance(b, j): + if j in solvent_set: + return Constraint.Skip + else: + return ( + b.properties_in[0].flow_mass_phase_comp["Liq", j] + * b.crystallization_yield[j] + == b.properties_in[0].flow_mass_phase_comp["Liq", j] + - b.properties_out[0].flow_mass_phase_comp["Liq", j] + ) + + # (b) Volumetric fraction constraint + @self.Constraint(doc="Solid volumetric fraction in outlet: constraint, 1-E") + def eq_vol_fraction_solids(b): + return self.product_volumetric_solids_fraction == b.properties_solids[ + 0 + ].flow_vol / ( + b.properties_solids[0].flow_vol + b.properties_out[0].flow_vol + ) + + # (c) Magma density constraint + @self.Constraint(doc="Slurry magma density") + def eq_dens_magma(b): + return ( + self.dens_mass_magma + == b.properties_solids[0].dens_mass_solute["Sol"] + * self.product_volumetric_solids_fraction + ) + + # (d) Operating pressure constraint + @self.Constraint(doc="Operating pressure constraint") + def eq_operating_pressure_constraint(b): + return self.pressure_operating - b.properties_out[0].pressure_sat == 0 + + # (e) Relative supersaturation + @self.Constraint( + solute_set, + doc="Relative supersaturation created via evaporation, g/g (solution)", + ) + def eq_relative_supersaturation(b, j): + # mass_frac_after_evap = SOLIDS IN + LIQUID IN - VAPOUR OUT + mass_frac_after_evap = b.properties_in[0].flow_mass_phase_comp["Liq", j] / ( + sum( + b.properties_in[0].flow_mass_phase_comp["Liq", k] + for k in solute_set + ) + + b.properties_in[0].flow_mass_phase_comp["Liq", "H2O"] + - b.properties_vapor[0].flow_mass_phase_comp["Vap", "H2O"] + ) + # return (b.relative_supersaturation[j] * b.properties_out[0].solubility_mass_frac_phase_comp['Liq', j] == + # (mass_frac_after_evap - b.properties_out[0].solubility_mass_frac_phase_comp['Liq', j]) + # ) + return ( + b.relative_supersaturation[j] + == ( + mass_frac_after_evap + - b.properties_out[0].solubility_mass_frac_phase_comp["Liq", j] + ) + / b.properties_out[0].solubility_mass_frac_phase_comp["Liq", j] + ) + + # 4. Fix flows of empty solid, liquid and vapour streams + # (i) Fix solids: liquid and vapour flows must be zero + for p, j in self.properties_solids[0].phase_component_set: + if p != "Sol": + self.properties_solids[0].flow_mass_phase_comp[p, j].fix(1e-8) + + # (ii) Fix liquids: solid and vapour flows must be zero + for p, j in self.properties_out[0].phase_component_set: + if p != "Liq": + self.properties_out[0].flow_mass_phase_comp[p, j].fix(1e-8) + + # (iii) Fix vapor: solid and vapour flows must be zero. + for p, j in self.properties_vapor[0].phase_component_set: + if p != "Vap": + self.properties_vapor[0].flow_mass_phase_comp[p, j].fix(1e-8) + + # 5. Add an energy balance for the system + ## (iii) Enthalpy balance: based on Lewis et al. Enthalpy is exothermic, and the hC in property package is -ve + @self.Constraint(doc="Enthalpy balance over crystallization system") + def eq_enthalpy_balance(b): + return ( + b.properties_in[0].enth_flow + - b.properties_out[0].enth_flow + - b.properties_vapor[0].enth_flow + - b.properties_solids[0].enth_flow + + self.work_mechanical[0] + - sum( + b.properties_solids[0].flow_mass_phase_comp["Sol", j] + * b.properties_solids[0].dh_crystallization_mass_comp[j] + for j in solute_set + ) + == 0 + ) + + # 6. Pressure and temperature balances - what is the pressure of the outlet solid and vapour? + # TO-DO: Figure out actual liquid and solid pressures. + @self.Constraint() + def eq_p_con1(b): + return b.properties_in[0].pressure == b.properties_out[0].pressure + + @self.Constraint() + def eq_p_con2(b): + return b.properties_in[0].pressure == b.properties_solids[0].pressure + + @self.Constraint() + def eq_p_con3(b): + return b.properties_vapor[0].pressure == self.pressure_operating + + @self.Constraint() + def eq_T_con1(b): + return self.temperature_operating == b.properties_solids[0].temperature + + @self.Constraint() + def eq_T_con2(b): + return self.temperature_operating == b.properties_vapor[0].temperature + + @self.Constraint() + def eq_T_con3(b): + return self.temperature_operating == b.properties_out[0].temperature + + @self.Constraint() + def eq_p_con4(b): + return b.properties_pure_water[0].pressure == self.pressure_operating + + @self.Constraint() + def eq_p_con5(b): + return b.properties_pure_water[0].pressure_sat == self.pressure_operating + + # 7. Heat exchanger minimum circulation flow rate calculations - see Lewis et al. or Tavare et al. + @self.Constraint( + doc="Constraint on mimimum circulation rate through crystallizer heat exchanger" + ) + def eq_minimum_hex_circulation_rate_constraint(b): + dens_cp_avg = self.approach_temperature_heat_exchanger * ( + b.product_volumetric_solids_fraction + * b.properties_solids[0].dens_mass_solute["Sol"] + * b.properties_solids[0].cp_mass_solute["Sol"] + + (1 - b.product_volumetric_solids_fraction) + * b.properties_out[0].dens_mass_phase["Liq"] + * b.properties_out[0].cp_mass_phase["Liq"] + ) + return b.magma_circulation_flow_vol * dens_cp_avg == pyunits.convert( + b.work_mechanical[0], to_units=pyunits.J / pyunits.s + ) + + # 8. Suspension density + @self.Constraint(doc="Slurry density calculation") + def eq_dens_mass_slurry(b): + return ( + self.dens_mass_slurry + == b.product_volumetric_solids_fraction + * b.properties_solids[0].dens_mass_solute["Sol"] + + (1 - b.product_volumetric_solids_fraction) + * b.properties_out[0].dens_mass_phase["Liq"] + ) + + # 9. Residence time calculation + @self.Constraint(doc="Residence time") + def eq_residence_time(b): + return b.t_res == b.crystal_median_length / ( + b.dimensionless_crystal_length + * pyunits.convert( + b.crystal_growth_rate, to_units=pyunits.m / pyunits.hr + ) + ) + + # 10. Suspension volume calculation + @self.Constraint(doc="Suspension volume") + def eq_suspension_volume(b): + return b.volume_suspension == ( + b.properties_solids[0].flow_vol + b.properties_out[0].flow_vol + ) * pyunits.convert(b.t_res, to_units=pyunits.s) + + # 11. Minimum diameter of evaporation zone + @self.Expression(doc="maximum allowable vapour linear velocity in m/s") + def eq_max_allowable_velocity(b): + return ( + b.souders_brown_constant + * ( + b.properties_out[0].dens_mass_phase["Liq"] + / b.properties_vapor[0].dens_mass_solvent["Vap"] + ) + ** 0.5 + ) + + @self.Constraint( + doc="Crystallizer diameter (based on minimum diameter of evaporation zone)" + ) + def eq_vapor_head_diameter_constraint(b): + return ( + self.diameter_crystallizer + == ( + 4 + * b.properties_vapor[0].flow_vol_phase["Vap"] + / (Constants.pi * b.eq_max_allowable_velocity) + ) + ** 0.5 + ) + + # 12. Minimum crystallizer height + @self.Constraint(doc="Slurry height based on crystallizer diameter") + def eq_slurry_height_constraint(b): + return self.height_slurry == 4 * b.volume_suspension / ( + Constants.pi * b.diameter_crystallizer**2 + ) + + @self.Expression( + doc="Recommended height of vapor space (0.75*D) based on Tavares et. al." + ) + def eq_vapor_space_height(b): + return 0.75 * b.diameter_crystallizer + + @self.Expression( + doc="Height to diameter ratio constraint for evaporative crystallizers (Wilson et. al.)" + ) + def eq_minimum_height_diameter_ratio(b): + return 1.5 * b.diameter_crystallizer + + @self.Constraint(doc="Crystallizer height") + def eq_crystallizer_height_constraint(b): + # Height is max(). Manual smooth max implementation used here: max(a,b) = 0.5(a + b + |a-b|) + a = b.eq_vapor_space_height + b.height_slurry + b = b.eq_minimum_height_diameter_ratio + eps = 1e-20 * pyunits.m + return self.height_crystallizer == 0.5 * ( + a + b + ((a - b) ** 2 + eps**2) ** 0.5 + ) + + # 13. Energy available from the vapor + @self.Constraint(doc="Thermal energy in the vapor") + def eq_vapor_energy_constraint(b): + return b.energy_flow_superheated_vapor == ( + b.properties_vapor[0].flow_mass_phase_comp["Vap", "H2O"] + * ( + b.properties_pure_water[ + 0 + ].dh_vap_mass_solvent # Latent heat from the vapor + + b.properties_vapor[0].enth_mass_solvent["Vap"] + - b.properties_pure_water[0].enth_mass_solvent["Vap"] + ) + ) + + def initialize_build( + self, + state_args=None, + outlvl=idaeslog.NOTSET, + solver=None, + optarg=None, + ): + """ + General wrapper for pressure changer initialization routines + + Keyword Arguments: + state_args : a dict of arguments to be passed to the property + package(s) to provide an initial state for + initialization (see documentation of the specific + property package) (default = {}). + outlvl : sets output level of initialization routine + optarg : solver options dictionary object (default=None) + solver : str indicating which solver to use during + initialization (default = None) + + Returns: None + """ + init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") + solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit") + + opt = get_solver(solver, optarg) + + # --------------------------------------------------------------------- + # Initialize holdup block + flags = self.properties_in.initialize( + outlvl=outlvl, + optarg=optarg, + solver=solver, + state_args=state_args, + hold_state=True, + ) + init_log.info_high("Initialization Step 1 Complete.") + # --------------------------------------------------------------------- + # Initialize other state blocks + # Set state_args from inlet state + if state_args is None: + state_args = {} + state_dict = self.properties_in[ + self.flowsheet().config.time.first() + ].define_port_members() + + for k in state_dict.keys(): + if state_dict[k].is_indexed(): + state_args[k] = {} + for m in state_dict[k].keys(): + state_args[k][m] = state_dict[k][m].value + else: + state_args[k] = state_dict[k].value + + self.properties_out.initialize( + outlvl=outlvl, + optarg=optarg, + solver=solver, + state_args=state_args, + ) + + state_args_solids = deepcopy(state_args) + for p, j in self.properties_solids.phase_component_set: + if p == "Sol": + state_args_solids["flow_mass_phase_comp"][p, j] = state_args[ + "flow_mass_phase_comp" + ]["Liq", j] + elif p == "Liq" or p == "Vap": + state_args_solids["flow_mass_phase_comp"][p, j] = 1e-8 + self.properties_solids.initialize( + outlvl=outlvl, + optarg=optarg, + solver=solver, + state_args=state_args_solids, + ) + + state_args_vapor = deepcopy(state_args) + for p, j in self.properties_vapor.phase_component_set: + if p == "Vap": + state_args_vapor["flow_mass_phase_comp"][p, j] = state_args[ + "flow_mass_phase_comp" + ]["Liq", j] + elif p == "Liq" or p == "Sol": + state_args_vapor["flow_mass_phase_comp"][p, j] = 1e-8 + self.properties_vapor.initialize( + outlvl=outlvl, + optarg=optarg, + solver=solver, + state_args=state_args_vapor, + ) + + self.properties_pure_water.initialize( + outlvl=outlvl, + optarg=optarg, + solver=solver, + state_args=state_args_vapor, + ) + init_log.info_high("Initialization Step 2 Complete.") + + interval_initializer(self) + # --------------------------------------------------------------------- + # Solve unit + with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: + res = opt.solve(self, tee=slc.tee) + init_log.info_high("Initialization Step 3 {}.".format(idaeslog.condition(res))) + # --------------------------------------------------------------------- + # Release Inlet state + self.properties_in.release_state(flags, outlvl=outlvl) + init_log.info("Initialization Complete: {}".format(idaeslog.condition(res))) + + if not check_optimal_termination(res): + raise InitializationError(f"Unit model {self.name} failed to initialize") + + def calculate_scaling_factors(self): + super().calculate_scaling_factors() + + iscale.set_scaling_factor( + self.crystal_growth_rate, 1e7 + ) # growth rates typically of order 1e-7 to 1e-9 m/s + iscale.set_scaling_factor( + self.crystal_median_length, 1e3 + ) # Crystal lengths typically in mm + iscale.set_scaling_factor( + self.souders_brown_constant, 1e2 + ) # Typical values are 0.0244, 0.04 and 0.06 + iscale.set_scaling_factor( + self.diameter_crystallizer, 1 + ) # Crystallizer diameters typically up to about 20 m + iscale.set_scaling_factor( + self.height_crystallizer, 1 + ) # H/D ratio maximum is about 1.5, so same scaling as diameter + iscale.set_scaling_factor(self.height_slurry, 1) # Same scaling as diameter + iscale.set_scaling_factor(self.magma_circulation_flow_vol, 1) + iscale.set_scaling_factor(self.relative_supersaturation, 10) + iscale.set_scaling_factor(self.t_res, 1) # Residence time is in hours + iscale.set_scaling_factor( + self.volume_suspension, 0.1 + ) # Suspension volume usually in tens to hundreds range + iscale.set_scaling_factor( + self.crystallization_yield, 1 + ) # Yield is between 0 and 1, usually in the 10-60% range + iscale.set_scaling_factor(self.product_volumetric_solids_fraction, 10) + iscale.set_scaling_factor( + self.temperature_operating, + iscale.get_scaling_factor(self.properties_in[0].temperature), + ) + iscale.set_scaling_factor(self.pressure_operating, 1e-3) + iscale.set_scaling_factor( + self.dens_mass_magma, 1e-3 + ) # scaling factor of dens_mass_phase['Liq'] + iscale.set_scaling_factor( + self.dens_mass_slurry, 1e-3 + ) # scaling factor of dens_mass_phase['Liq'] + iscale.set_scaling_factor( + self.work_mechanical[0], + iscale.get_scaling_factor( + self.properties_in[0].flow_mass_phase_comp["Vap", "H2O"] + ) + * iscale.get_scaling_factor(self.properties_in[0].enth_mass_solvent["Vap"]), + ) + iscale.set_scaling_factor( + self.energy_flow_superheated_vapor, + iscale.get_scaling_factor( + self.properties_vapor[0].flow_mass_phase_comp["Vap", "H2O"] + ) + * iscale.get_scaling_factor( + self.properties_vapor[0].enth_mass_solvent["Vap"] + ), + ) + # transforming constraints + for ind, c in self.eq_T_con1.items(): + sf = iscale.get_scaling_factor(self.properties_in[0].temperature) + iscale.constraint_scaling_transform(c, sf) + + for ind, c in self.eq_T_con2.items(): + sf = iscale.get_scaling_factor(self.properties_in[0].temperature) + iscale.constraint_scaling_transform(c, sf) + + for ind, c in self.eq_T_con3.items(): + sf = iscale.get_scaling_factor(self.properties_in[0].temperature) + iscale.constraint_scaling_transform(c, sf) + + for ind, c in self.eq_p_con1.items(): + sf = iscale.get_scaling_factor(self.properties_in[0].pressure) + iscale.constraint_scaling_transform(c, sf) + + for ind, c in self.eq_p_con2.items(): + sf = iscale.get_scaling_factor(self.properties_in[0].pressure) + iscale.constraint_scaling_transform(c, sf) + + for ind, c in self.eq_p_con3.items(): + sf = iscale.get_scaling_factor(self.properties_in[0].pressure) + iscale.constraint_scaling_transform(c, sf) + + for ind, c in self.eq_p_con4.items(): + sf = iscale.get_scaling_factor(self.properties_pure_water[0].pressure) + iscale.constraint_scaling_transform(c, sf) + + for j, c in self.eq_mass_balance_constraints.items(): + sf = iscale.get_scaling_factor( + self.properties_in[0].flow_mass_phase_comp["Liq", j] + ) + iscale.constraint_scaling_transform(c, sf) + + for j, c in self.eq_solubility_massfrac_equality_constraint.items(): + iscale.constraint_scaling_transform(c, 1e0) + + for j, c in self.eq_dens_magma.items(): + iscale.constraint_scaling_transform( + c, iscale.get_scaling_factor(self.dens_mass_magma) + ) + + for j, c in self.eq_removal_balance.items(): + sf = iscale.get_scaling_factor( + self.properties_in[0].flow_mass_phase_comp["Liq", j] + ) + iscale.constraint_scaling_transform(c, sf) + + def _get_stream_table_contents(self, time_point=0): + return create_stream_table_dataframe( + { + "Feed Inlet": self.inlet, + "Liquid Outlet": self.outlet, + "Vapor Outlet": self.vapor, + "Solid Outlet": self.solids, + }, + time_point=time_point, + ) + + def _get_performance_contents(self, time_point=0): + var_dict = {} + var_dict["Operating Temperature"] = self.temperature_operating + var_dict["Operating Pressure"] = self.pressure_operating + var_dict["Magma density of solution"] = self.dens_mass_magma + var_dict["Slurry density"] = self.dens_mass_slurry + var_dict["Heat requirement"] = self.work_mechanical[time_point] + var_dict["Crystallizer diameter"] = self.diameter_crystallizer + var_dict["Magma circulation flow rate"] = self.magma_circulation_flow_vol + var_dict["Vol. frac. of solids in suspension, 1-E"] = ( + self.product_volumetric_solids_fraction + ) + var_dict["Residence time"] = self.t_res + var_dict["Crystallizer minimum active volume"] = self.volume_suspension + var_dict["Suspension height in crystallizer"] = self.height_slurry + var_dict["Crystallizer height"] = self.height_crystallizer + + for j in self.config.property_package.solute_set: + yield_mem_name = f"{j} yield (fraction)" + var_dict[yield_mem_name] = self.crystallization_yield[j] + supersat_mem_name = f"{j} relative supersaturation (mass fraction basis)" + var_dict[supersat_mem_name] = self.relative_supersaturation[j] + + return {"vars": var_dict} + + @property + def default_costing_method(self): + return cost_crystallizer \ No newline at end of file diff --git a/src/watertap_contrib/reflo/unit_models/zero_order/tests/test_crystallizer_watertap.py b/src/watertap_contrib/reflo/unit_models/zero_order/tests/test_crystallizer_watertap.py new file mode 100644 index 00000000..e25ca338 --- /dev/null +++ b/src/watertap_contrib/reflo/unit_models/zero_order/tests/test_crystallizer_watertap.py @@ -0,0 +1,784 @@ +################################################################################# +# WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# National Renewable Energy Laboratory, and National Energy Technology +# Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +################################################################################# + +import pytest +from pyomo.environ import ( + ConcreteModel, + TerminationCondition, + SolverStatus, + value, + Var, +) +from pyomo.network import Port +from idaes.core import FlowsheetBlock +from pyomo.util.check_units import assert_units_consistent +from watertap_contrib.reflo.unit_models.zero_order.crystallizer_zo_watertap import Crystallization +import watertap_contrib.reflo.property_models.cryst_prop_pack as props + +from idaes.core.solvers import get_solver +# from watertap.core.solvers import get_solver +from idaes.core.util.model_statistics import ( + degrees_of_freedom, + number_variables, + number_total_constraints, + number_unused_variables, +) +from idaes.core.util.testing import initialization_tester +from idaes.core.util.scaling import ( + calculate_scaling_factors, + unscaled_variables_generator, + badly_scaled_var_generator, +) +from idaes.core import UnitModelCostingBlock + +# from watertap.costing import WaterTAPCosting, CrystallizerCostType + +# ----------------------------------------------------------------------------- +# Get default solver for testing +solver = get_solver() + + +class TestCrystallization: + @pytest.fixture(scope="class") + def Crystallizer_frame(self): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.properties = props.NaClParameterBlock() + + m.fs.unit = Crystallization(property_package=m.fs.properties) + + # fully specify system + feed_flow_mass = 1 + feed_mass_frac_NaCl = 0.2126 + feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl + feed_pressure = 101325 + feed_temperature = 273.15 + 20 + eps = 1e-6 + crystallizer_temperature = 273.15 + 55 + crystallizer_yield = 0.40 + + # Fully define feed + m.fs.unit.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"].fix( + feed_flow_mass * feed_mass_frac_NaCl + ) + m.fs.unit.inlet.flow_mass_phase_comp[0, "Liq", "H2O"].fix( + feed_flow_mass * feed_mass_frac_H2O + ) + m.fs.unit.inlet.flow_mass_phase_comp[0, "Sol", "NaCl"].fix(eps) + m.fs.unit.inlet.flow_mass_phase_comp[0, "Vap", "H2O"].fix(eps) + m.fs.unit.inlet.pressure[0].fix(feed_pressure) + m.fs.unit.inlet.temperature[0].fix(feed_temperature) + + # Define operating conditions + m.fs.unit.temperature_operating.fix(crystallizer_temperature) + m.fs.unit.crystallization_yield["NaCl"].fix(crystallizer_yield) + + # Fix growth rate, crystal length and Sounders brown constant to default values + m.fs.unit.crystal_growth_rate.fix() + m.fs.unit.souders_brown_constant.fix() + m.fs.unit.crystal_median_length.fix() + + assert_units_consistent(m) + + return m + + @pytest.fixture(scope="class") + def Crystallizer_frame_2(self): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.properties = props.NaClParameterBlock() + + m.fs.unit = Crystallization(property_package=m.fs.properties) + + # fully specify system + feed_flow_mass = 1 + feed_mass_frac_NaCl = 0.2126 + feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl + feed_pressure = 101325 + feed_temperature = 273.15 + 20 + eps = 1e-6 + crystallizer_temperature = 273.15 + 55 + crystallizer_yield = 0.40 + + # Fully define feed + m.fs.unit.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"].fix( + feed_flow_mass * feed_mass_frac_NaCl + ) + m.fs.unit.inlet.flow_mass_phase_comp[0, "Liq", "H2O"].fix( + feed_flow_mass * feed_mass_frac_H2O + ) + m.fs.unit.inlet.flow_mass_phase_comp[0, "Sol", "NaCl"].fix(eps) + m.fs.unit.inlet.flow_mass_phase_comp[0, "Vap", "H2O"].fix(eps) + m.fs.unit.inlet.pressure[0].fix(feed_pressure) + m.fs.unit.inlet.temperature[0].fix(feed_temperature) + + # Define operating conditions + m.fs.unit.temperature_operating.fix(crystallizer_temperature) + m.fs.unit.crystallization_yield["NaCl"].fix(crystallizer_yield) + + # Fix growth rate, crystal length and Sounders brown constant to default values + m.fs.unit.crystal_growth_rate.fix() + m.fs.unit.souders_brown_constant.fix() + m.fs.unit.crystal_median_length.fix() + + assert_units_consistent(m) + + return m + + @pytest.mark.unit + def test_config(self, Crystallizer_frame): + m = Crystallizer_frame + # check unit config arguments + assert len(m.fs.unit.config) == 4 + + assert not m.fs.unit.config.dynamic + assert not m.fs.unit.config.has_holdup + assert m.fs.unit.config.property_package is m.fs.properties + + @pytest.mark.unit + def test_build(self, Crystallizer_frame): + m = Crystallizer_frame + + # test ports and variables + port_lst = ["inlet", "outlet", "solids", "vapor"] + port_vars_lst = ["flow_mass_phase_comp", "pressure", "temperature"] + for port_str in port_lst: + assert hasattr(m.fs.unit, port_str) + port = getattr(m.fs.unit, port_str) + assert len(port.vars) == 3 + assert isinstance(port, Port) + for var_str in port_vars_lst: + assert hasattr(port, var_str) + var = getattr(port, var_str) + assert isinstance(var, Var) + + # test unit objects (including parameters, variables, and constraints) + # First, parameters + unit_objs_params_lst = [ + "approach_temperature_heat_exchanger", + "dimensionless_crystal_length", + ] + for obj_str in unit_objs_params_lst: + assert hasattr(m.fs.unit, obj_str) + # Next, variables + unit_objs_vars_lst = [ + "crystal_growth_rate", + "crystal_median_length", + "crystallization_yield", + "dens_mass_magma", + "dens_mass_slurry", + "diameter_crystallizer", + "height_crystallizer", + "height_slurry", + "magma_circulation_flow_vol", + "pressure_operating", + "product_volumetric_solids_fraction", + "relative_supersaturation", + "souders_brown_constant", + "t_res", + "temperature_operating", + "volume_suspension", + "work_mechanical", + ] + for obj_str in unit_objs_vars_lst: + assert hasattr(m.fs.unit, obj_str) + # Next, expressions + unit_objs_expr_lst = [ + "eq_max_allowable_velocity", + "eq_minimum_height_diameter_ratio", + "eq_vapor_space_height", + ] + for obj_str in unit_objs_expr_lst: + assert hasattr(m.fs.unit, obj_str) + # Finally, constraints + unit_objs_cons_lst = [ + "eq_T_con1", + "eq_T_con2", + "eq_T_con3", + "eq_crystallizer_height_constraint", + "eq_dens_magma", + "eq_dens_mass_slurry", + "eq_enthalpy_balance", + "eq_mass_balance_constraints", + "eq_minimum_hex_circulation_rate_constraint", + "eq_operating_pressure_constraint", + "eq_p_con1", + "eq_p_con2", + "eq_p_con3", + "eq_relative_supersaturation", + "eq_removal_balance", + "eq_residence_time", + "eq_slurry_height_constraint", + "eq_solubility_massfrac_equality_constraint", + "eq_suspension_volume", + "eq_vapor_head_diameter_constraint", + "eq_vol_fraction_solids", + ] + for obj_str in unit_objs_cons_lst: + assert hasattr(m.fs.unit, obj_str) + + # Test stateblocks + # List olf attributes on all stateblocks + stateblock_objs_lst = [ + "flow_mass_phase_comp", + "pressure", + "temperature", + "solubility_mass_phase_comp", + "solubility_mass_frac_phase_comp", + "mass_frac_phase_comp", + "dens_mass_solvent", + "dens_mass_solute", + "dens_mass_phase", + "cp_mass_phase", + "cp_mass_solvent", + "flow_vol_phase", + "flow_vol", + "enth_flow", + "enth_mass_solvent", + "dh_crystallization_mass_comp", + "eq_solubility_mass_phase_comp", + "eq_solubility_mass_frac_phase_comp", + "eq_mass_frac_phase_comp", + "eq_dens_mass_solvent", + "eq_dens_mass_solute", + "eq_dens_mass_phase", + "eq_cp_mass_solute", + "eq_cp_mass_phase", + "eq_flow_vol_phase", + "eq_enth_mass_solvent", + ] + # List of attributes for liquid stateblocks only + stateblock_objs_liq_lst = ["pressure_sat", "eq_pressure_sat"] + # Inlet block + assert hasattr(m.fs.unit, "properties_in") + blk = getattr(m.fs.unit, "properties_in") + for var_str in stateblock_objs_lst: + assert hasattr(blk[0], var_str) + for var_str in stateblock_objs_liq_lst: + assert hasattr(blk[0], var_str) + + # Liquid outlet block + assert hasattr(m.fs.unit, "properties_out") + blk = getattr(m.fs.unit, "properties_out") + for var_str in stateblock_objs_lst: + assert hasattr(blk[0], var_str) + for var_str in stateblock_objs_liq_lst: + assert hasattr(blk[0], var_str) + + # Vapor outlet block + assert hasattr(m.fs.unit, "properties_vapor") + blk = getattr(m.fs.unit, "properties_vapor") + for var_str in stateblock_objs_lst: + assert hasattr(blk[0], var_str) + + # Liquid outlet block + assert hasattr(m.fs.unit, "properties_solids") + blk = getattr(m.fs.unit, "properties_solids") + for var_str in stateblock_objs_lst: + assert hasattr(blk[0], var_str) + + # test statistics + assert number_variables(m) == 255 + assert number_total_constraints(m) == 138 + assert number_unused_variables(m) == 5 + + @pytest.mark.unit + def test_dof(self, Crystallizer_frame): + m = Crystallizer_frame + assert degrees_of_freedom(m) == 0 + + @pytest.mark.unit + def test_calculate_scaling(self, Crystallizer_frame): + m = Crystallizer_frame + + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Liq", "H2O") + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Liq", "NaCl") + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Vap", "H2O") + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Sol", "NaCl") + ) + calculate_scaling_factors(m) + + # check that all variables have scaling factors + unscaled_var_list = list(unscaled_variables_generator(m)) + assert len(unscaled_var_list) == 0 + + for _ in badly_scaled_var_generator(m): + assert False + + @pytest.mark.requires_idaes_solver + @pytest.mark.component + def test_initialize(self, Crystallizer_frame): + # Add costing function, then initialize + m = Crystallizer_frame + # m.fs.costing = WaterTAPCosting() + # m.fs.unit.costing = UnitModelCostingBlock( + # flowsheet_costing_block=m.fs.costing, + # costing_method_arguments={"cost_type": CrystallizerCostType.mass_basis}, + # ) + # m.fs.costing.cost_process() + + initialization_tester(Crystallizer_frame) + assert_units_consistent(m) + + # @pytest.mark.component + # def test_var_scaling(self, Crystallizer_frame): + # m = Crystallizer_frame + # badly_scaled_var_lst = list(badly_scaled_var_generator(m)) + # assert badly_scaled_var_lst == [] + + @pytest.mark.requires_idaes_solver + @pytest.mark.component + def test_solve(self, Crystallizer_frame): + m = Crystallizer_frame + results = solver.solve(m) + + # Check for optimal solution + assert results.solver.termination_condition == TerminationCondition.optimal + assert results.solver.status == SolverStatus.ok + + @pytest.mark.requires_idaes_solver + @pytest.mark.component + def test_conservation(self, Crystallizer_frame): + m = Crystallizer_frame + b = m.fs.unit + comp_lst = ["NaCl", "H2O"] + phase_lst = ["Sol", "Liq", "Vap"] + phase_comp_list = [ + (p, j) + for j in comp_lst + for p in phase_lst + if (p, j) in b.properties_in[0].phase_component_set + ] + + flow_mass_in = sum( + b.properties_in[0].flow_mass_phase_comp[p, j] + for p in phase_lst + for j in comp_lst + if (p, j) in phase_comp_list + ) + flow_mass_out = sum( + b.properties_out[0].flow_mass_phase_comp[p, j] + for p in phase_lst + for j in comp_lst + if (p, j) in phase_comp_list + ) + flow_mass_solids = sum( + b.properties_solids[0].flow_mass_phase_comp[p, j] + for p in phase_lst + for j in comp_lst + if (p, j) in phase_comp_list + ) + flow_mass_vapor = sum( + b.properties_vapor[0].flow_mass_phase_comp[p, j] + for p in phase_lst + for j in comp_lst + if (p, j) in phase_comp_list + ) + + assert ( + abs( + value(flow_mass_in - flow_mass_out - flow_mass_solids - flow_mass_vapor) + ) + <= 1e-6 + ) + + assert ( + abs( + value( + flow_mass_in * b.properties_in[0].enth_mass_phase["Liq"] + - flow_mass_out * b.properties_out[0].enth_mass_phase["Liq"] + - flow_mass_vapor * b.properties_vapor[0].enth_mass_solvent["Vap"] + - flow_mass_solids * b.properties_solids[0].enth_mass_solute["Sol"] + - flow_mass_solids + * b.properties_solids[0].dh_crystallization_mass_comp["NaCl"] + + b.work_mechanical[0] + ) + ) + <= 1e-2 + ) + + @pytest.mark.requires_idaes_solver + @pytest.mark.component + def test_solution(self, Crystallizer_frame): + m = Crystallizer_frame + b = m.fs.unit + # Check solid mass in solids stream + assert pytest.approx( + value( + b.crystallization_yield["NaCl"] + * b.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"] + ), + rel=1e-3, + ) == value(b.solids.flow_mass_phase_comp[0, "Sol", "NaCl"]) + # Check solid mass in liquid stream + assert pytest.approx( + value( + (1 - b.crystallization_yield["NaCl"]) + * b.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"] + ), + rel=1e-3, + ) == value(b.outlet.flow_mass_phase_comp[0, "Liq", "NaCl"]) + # Check outlet liquid stream composition which is set by solubility + assert pytest.approx(0.2695, rel=1e-3) == value( + b.properties_out[0].mass_frac_phase_comp["Liq", "NaCl"] + ) + # Check liquid stream solvent flow + assert pytest.approx(0.12756 * ((1 / 0.2695) - 1), rel=1e-3) == value( + b.outlet.flow_mass_phase_comp[0, "Liq", "H2O"] + ) + # Check saturation pressure + assert pytest.approx(11992, rel=1e-3) == value(b.pressure_operating) + # Check heat requirement + assert pytest.approx(1127.2, rel=1e-3) == value(b.work_mechanical[0]) + # Check crystallizer diameter + assert pytest.approx(1.205, rel=1e-3) == value(b.diameter_crystallizer) + # Minimum active volume + assert pytest.approx(1.619, rel=1e-3) == value(b.volume_suspension) + # Residence time + assert pytest.approx(1.0228, rel=1e-3) == value(b.t_res) + # Mass-basis costing + assert pytest.approx(2.0 * 300000, rel=1e-3) == value( + m.fs.costing.aggregate_capital_cost + ) + + @pytest.mark.requires_idaes_solver + @pytest.mark.component + def test_solution2_capcosting_by_mass(self, Crystallizer_frame): + m = Crystallizer_frame + b = m.fs.unit + b.crystal_growth_rate.fix(5e-8) + b.souders_brown_constant.fix(0.0244) + b.crystal_median_length.fix(0.4e-3) + results = solver.solve(m) + + # Test that report function works + b.report() + + # Check for optimal solution + assert results.solver.termination_condition == TerminationCondition.optimal + assert results.solver.status == SolverStatus.ok + + # Residence time + assert pytest.approx( + value(b.crystal_median_length / (3.67 * 3600 * b.crystal_growth_rate)), + rel=1e-3, + ) == value(b.t_res) + # Check crystallizer diameter + assert pytest.approx(1.5427, rel=1e-3) == value(b.diameter_crystallizer) + # Minimum active volume + assert pytest.approx(0.959, rel=1e-3) == value(b.volume_suspension) + # Mass-basis costing + assert pytest.approx(2.0 * 300000, rel=1e-3) == value( + m.fs.costing.aggregate_capital_cost + ) + + @pytest.mark.component + def test_solution2_capcosting_by_volume(self, Crystallizer_frame_2): + # Same problem as above, but different costing approach. + # Other results should remain the same. + m = Crystallizer_frame_2 + b = m.fs.unit + b.crystal_growth_rate.fix(5e-8) + b.souders_brown_constant.fix(0.0244) + b.crystal_median_length.fix(0.4e-3) + + assert degrees_of_freedom(m) == 0 + + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Liq", "H2O") + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Liq", "NaCl") + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Vap", "H2O") + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Sol", "NaCl") + ) + calculate_scaling_factors(m) + initialization_tester(Crystallizer_frame_2) + results = solver.solve(m) + + m.fs.costing = WaterTAPCosting() + m.fs.unit.costing = UnitModelCostingBlock( + flowsheet_costing_block=m.fs.costing, + costing_method_arguments={"cost_type": CrystallizerCostType.volume_basis}, + ) + m.fs.costing.cost_process() + assert_units_consistent(m) + results = solver.solve(m) + + # Test that report function works + b.report() + + # Check for optimal solution + assert results.solver.termination_condition == TerminationCondition.optimal + assert results.solver.status == SolverStatus.ok + + # Residence time + assert pytest.approx( + value(b.crystal_median_length / (3.67 * 3600 * b.crystal_growth_rate)), + rel=1e-3, + ) == value(b.t_res) + # Check crystallizer diameter + assert pytest.approx(1.5427, rel=1e-3) == value(b.diameter_crystallizer) + # Minimum active volume + assert pytest.approx(0.959, rel=1e-3) == value(b.volume_suspension) + # Volume-basis costing + assert pytest.approx(2.0 * 199000, rel=1e-3) == value( + m.fs.costing.aggregate_capital_cost + ) + + @pytest.mark.component + def test_solution2_operatingcost(self, Crystallizer_frame_2): + m = Crystallizer_frame_2 + b = m.fs.unit + b.crystal_growth_rate.fix(5e-8) + b.souders_brown_constant.fix(0.0244) + b.crystal_median_length.fix(0.4e-3) + results = solver.solve(m) + + # Check for optimal solution + assert results.solver.termination_condition == TerminationCondition.optimal + assert results.solver.status == SolverStatus.ok + + # Operating cost validation + assert pytest.approx(835.41, rel=1e-3) == value( + m.fs.costing.aggregate_flow_costs["electricity"] + ) + assert pytest.approx(30666.67, rel=1e-3) == value( + m.fs.costing.aggregate_flow_costs["steam"] + ) + assert pytest.approx(0, rel=1e-3) == value( + m.fs.costing.aggregate_flow_costs["NaCl"] + ) + + @pytest.mark.component + def test_solution2_operatingcost_steampressure(self, Crystallizer_frame_2): + m = Crystallizer_frame_2 + m.fs.costing.crystallizer.steam_pressure.fix(5) + b = m.fs.unit + b.crystal_growth_rate.fix(5e-8) + b.souders_brown_constant.fix(0.0244) + b.crystal_median_length.fix(0.4e-3) + results = solver.solve(m) + + # Check for optimal solution + assert results.solver.termination_condition == TerminationCondition.optimal + assert results.solver.status == SolverStatus.ok + + # Operating cost validation + assert pytest.approx(835.41, rel=1e-3) == value( + m.fs.costing.aggregate_flow_costs["electricity"] + ) + assert pytest.approx(21451.91, rel=1e-3) == value( + m.fs.costing.aggregate_flow_costs["steam"] + ) + assert pytest.approx(0, abs=1e-6) == value( + m.fs.costing.aggregate_flow_costs["NaCl"] + ) + + @pytest.mark.component + def test_solution2_operatingcost_NaCl_revenue(self, Crystallizer_frame_2): + m = Crystallizer_frame_2 + m.fs.costing.crystallizer.steam_pressure.fix(3) + m.fs.costing.crystallizer.NaCl_recovery_value.fix(-0.07) + + results = solver.solve(m) + + # Check for optimal solution + assert results.solver.termination_condition == TerminationCondition.optimal + assert results.solver.status == SolverStatus.ok + + # Operating cost validation + assert pytest.approx(835.41, rel=1e-3) == value( + m.fs.costing.aggregate_flow_costs["electricity"] + ) + assert pytest.approx(30666.67, rel=1e-3) == value( + m.fs.costing.aggregate_flow_costs["steam"] + ) + assert pytest.approx(-187858.2, rel=1e-3) == value( + m.fs.costing.aggregate_flow_costs["NaCl"] + ) + + +if __name__=="__main__": + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.properties = props.NaClParameterBlock() + + m.fs.unit = Crystallization(property_package=m.fs.properties) + + # fully specify system + feed_flow_mass = 1 + feed_mass_frac_NaCl = 0.2126 + feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl + feed_pressure = 101325 + feed_temperature = 273.15 + 20 + eps = 1e-6 + crystallizer_temperature = 273.15 + 55 + crystallizer_yield = 0.40 + + # Fully define feed + m.fs.unit.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"].fix( + feed_flow_mass * feed_mass_frac_NaCl + ) + m.fs.unit.inlet.flow_mass_phase_comp[0, "Liq", "H2O"].fix( + feed_flow_mass * feed_mass_frac_H2O + ) + m.fs.unit.inlet.flow_mass_phase_comp[0, "Sol", "NaCl"].fix(eps) + m.fs.unit.inlet.flow_mass_phase_comp[0, "Vap", "H2O"].fix(eps) + m.fs.unit.inlet.pressure[0].fix(feed_pressure) + m.fs.unit.inlet.temperature[0].fix(feed_temperature) + + # Define operating conditions + m.fs.unit.temperature_operating.fix(crystallizer_temperature) + m.fs.unit.crystallization_yield["NaCl"].fix(crystallizer_yield) + + # Fix growth rate, crystal length and Sounders brown constant to default values + m.fs.unit.crystal_growth_rate.fix() + m.fs.unit.souders_brown_constant.fix() + m.fs.unit.crystal_median_length.fix() + + assert_units_consistent(m) + + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Liq", "H2O") + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Liq", "NaCl") + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Vap", "H2O") + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Sol", "NaCl") + ) + calculate_scaling_factors(m) + + m.fs.unit.initialize() + + results = solver.solve(m) + + # Check for optimal solution + assert results.solver.termination_condition == TerminationCondition.optimal + assert results.solver.status == SolverStatus.ok + + +if __name__=="__main__": + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.properties = props.NaClParameterBlock() + + m.fs.unit = Crystallization(property_package=m.fs.properties) + + # fully specify system + feed_flow_mass = 1 + feed_mass_frac_NaCl = 0.2126 + feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl + feed_pressure = 101325 + feed_temperature = 273.15 + 20 + eps = 1e-6 + crystallizer_temperature = 273.15 + 55 + crystallizer_yield = 0.40 + + # Fully define feed + m.fs.unit.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"].fix( + feed_flow_mass * feed_mass_frac_NaCl + ) + m.fs.unit.inlet.flow_mass_phase_comp[0, "Liq", "H2O"].fix( + feed_flow_mass * feed_mass_frac_H2O + ) + m.fs.unit.inlet.flow_mass_phase_comp[0, "Sol", "NaCl"].fix(eps) + m.fs.unit.inlet.flow_mass_phase_comp[0, "Vap", "H2O"].fix(eps) + m.fs.unit.inlet.pressure[0].fix(feed_pressure) + m.fs.unit.inlet.temperature[0].fix(feed_temperature) + + # Define operating conditions + m.fs.unit.temperature_operating.fix(crystallizer_temperature) + m.fs.unit.crystallization_yield["NaCl"].fix(crystallizer_yield) + + # Fix growth rate, crystal length and Sounders brown constant to default values + m.fs.unit.crystal_growth_rate.fix() + m.fs.unit.souders_brown_constant.fix() + m.fs.unit.crystal_median_length.fix() + + assert_units_consistent(m) + + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Liq", "H2O") + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Liq", "NaCl") + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Vap", "H2O") + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Sol", "NaCl") + ) + calculate_scaling_factors(m) + + m.fs.unit.initialize() + + results = solver.solve(m) + + # Check for optimal solution + assert results.solver.termination_condition == TerminationCondition.optimal + assert results.solver.status == SolverStatus.ok + + b = m.fs.unit + assert pytest.approx( + value( + b.crystallization_yield["NaCl"] + * b.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"] + ), + rel=1e-3, + ) == value(b.solids.flow_mass_phase_comp[0, "Sol", "NaCl"]) + # Check solid mass in liquid stream + assert pytest.approx( + value( + (1 - b.crystallization_yield["NaCl"]) + * b.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"] + ), + rel=1e-3, + ) == value(b.outlet.flow_mass_phase_comp[0, "Liq", "NaCl"]) + # Check outlet liquid stream composition which is set by solubility + assert pytest.approx(0.2695, rel=1e-3) == value( + b.properties_out[0].mass_frac_phase_comp["Liq", "NaCl"] + ) + # Check liquid stream solvent flow + assert pytest.approx(0.12756 * ((1 / 0.2695) - 1), rel=1e-3) == value( + b.outlet.flow_mass_phase_comp[0, "Liq", "H2O"] + ) + # Check saturation pressure + assert pytest.approx(11992, rel=1e-3) == value(b.pressure_operating) + # Check heat requirement + assert pytest.approx(1127.2, rel=1e-3) == value(b.work_mechanical[0]) + # Check crystallizer diameter + assert pytest.approx(1.205, rel=1e-3) == value(b.diameter_crystallizer) + # Minimum active volume + assert pytest.approx(1.619, rel=1e-3) == value(b.volume_suspension) + # Residence time + assert pytest.approx(1.0228, rel=1e-3) == value(b.t_res) \ No newline at end of file From a468289055eb911ea205baeca8d92db63a922018 Mon Sep 17 00:00:00 2001 From: zhuoran29 Date: Wed, 24 Jul 2024 12:12:12 -0400 Subject: [PATCH 02/80] run black --- .../multi_effect_NaCl_crystallizer.py | 381 +++ .../reflo/property_models/cryst_prop_pack.py | 2160 +++++++++++++++++ .../tests/test_cryst_prop_pack.py | 806 ++++++ .../zero_order/crystallizer_zo_watertap.py | 868 +++++++ .../tests/test_crystallizer_watertap.py | 787 ++++++ 5 files changed, 5002 insertions(+) create mode 100644 src/watertap_contrib/reflo/analysis/flowsheets/multi_effect_NaCl_crystallizer.py create mode 100644 src/watertap_contrib/reflo/property_models/cryst_prop_pack.py create mode 100644 src/watertap_contrib/reflo/property_models/tests/test_cryst_prop_pack.py create mode 100644 src/watertap_contrib/reflo/unit_models/zero_order/crystallizer_zo_watertap.py create mode 100644 src/watertap_contrib/reflo/unit_models/zero_order/tests/test_crystallizer_watertap.py diff --git a/src/watertap_contrib/reflo/analysis/flowsheets/multi_effect_NaCl_crystallizer.py b/src/watertap_contrib/reflo/analysis/flowsheets/multi_effect_NaCl_crystallizer.py new file mode 100644 index 00000000..3a645e05 --- /dev/null +++ b/src/watertap_contrib/reflo/analysis/flowsheets/multi_effect_NaCl_crystallizer.py @@ -0,0 +1,381 @@ +import pandas as pd +import numpy as np +import pytest +from pyomo.environ import ( + ConcreteModel, + TerminationCondition, + SolverStatus, + Objective, + Expression, + maximize, + value, + Set, + Var, + log, + units as pyunits, +) +from pyomo.network import Port +from idaes.core import FlowsheetBlock +import idaes.core.util.scaling as iscale +from pyomo.util.check_units import assert_units_consistent +from watertap_contrib.reflo.unit_models.zero_order.crystallizer_zo_watertap import ( + Crystallization, +) +import watertap_contrib.reflo.property_models.cryst_prop_pack as props +from watertap.unit_models.mvc.components.lmtd_chen_callback import ( + delta_temperature_chen_callback, +) +from idaes.core.solvers import get_solver +from idaes.core.util.model_statistics import ( + degrees_of_freedom, + number_variables, + number_total_constraints, + number_unused_variables, +) +from idaes.models.unit_models import HeatExchanger +from idaes.models.unit_models.heat_exchanger import ( + delta_temperature_lmtd_callback, + delta_temperature_underwood_callback, +) +from idaes.core.util.testing import initialization_tester +from idaes.core.util.scaling import ( + calculate_scaling_factors, + unscaled_variables_generator, + badly_scaled_var_generator, +) +from idaes.core import UnitModelCostingBlock + +from watertap.costing import WaterTAPCosting, CrystallizerCostType + +solver = get_solver() + + +def build_fs_multi_effect_crystallizer( + m=None, + # num_effect=3, + operating_pressure_eff1=0.78, # bar + operating_pressure_eff2=0.25, # bar + operating_pressure_eff3=0.208, # bar + operating_pressure_eff4=0.095, # bar + feed_flow_mass=1, # kg/s + feed_mass_frac_NaCl=0.3, + feed_pressure=101325, # Pa + feed_temperature=273.15 + 20, # K + crystallizer_yield=0.5, + steam_pressure=1.5, # bar (gauge pressure) +): + """ + This flowsheet depicts a 4-effect crystallizer, with brine fed in parallel + to each effect, and the operating pressure is specfied individually. + """ + + if m is None: + m = ConcreteModel() + + mfs = m.fs = FlowsheetBlock(dynamic=False) + m.fs.props = props.NaClParameterBlock() + + # Create 4 effects of crystallizer + eff_1 = m.fs.eff_1 = Crystallization(property_package=m.fs.props) + eff_2 = m.fs.eff_2 = Crystallization(property_package=m.fs.props) + eff_3 = m.fs.eff_3 = Crystallization(property_package=m.fs.props) + eff_4 = m.fs.eff_4 = Crystallization(property_package=m.fs.props) + + feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl + eps = 1e-6 + + eff_1.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"].fix( + feed_flow_mass * feed_mass_frac_NaCl + ) + eff_1.inlet.flow_mass_phase_comp[0, "Liq", "H2O"].fix( + feed_flow_mass * feed_mass_frac_H2O + ) + + for eff in [eff_1, eff_2, eff_3, eff_4]: + # Define feed for all effects + eff.inlet.flow_mass_phase_comp[0, "Sol", "NaCl"].fix(eps) + eff.inlet.flow_mass_phase_comp[0, "Vap", "H2O"].fix(eps) + + eff.inlet.pressure[0].fix(feed_pressure) + eff.inlet.temperature[0].fix(feed_temperature) + + # Fix growth rate, crystal length and Sounders brown constant to default values + eff.crystal_growth_rate.fix() + eff.souders_brown_constant.fix() + eff.crystal_median_length.fix() + + # Fix yield + eff.crystallization_yield["NaCl"].fix(crystallizer_yield) + + # Define operating conditions + m.fs.eff_1.pressure_operating.fix(operating_pressure_eff1 * pyunits.bar) + m.fs.eff_2.pressure_operating.fix(operating_pressure_eff2 * pyunits.bar) + m.fs.eff_3.pressure_operating.fix(operating_pressure_eff3 * pyunits.bar) + m.fs.eff_4.pressure_operating.fix(operating_pressure_eff4 * pyunits.bar) + + add_heat_exchanger_eff2(m) + add_heat_exchanger_eff3(m) + add_heat_exchanger_eff4(m) + return m + + +def add_heat_exchanger_eff2(m): + eff_1 = m.fs.eff_1 + eff_2 = m.fs.eff_2 + + eff_2.delta_temperature_in = Var( + eff_2.flowsheet().time, + initialize=35, + bounds=(None, None), + units=pyunits.K, + doc="Temperature differnce at the inlet side", + ) + eff_2.delta_temperature_out = Var( + eff_2.flowsheet().time, + initialize=35, + bounds=(None, None), + units=pyunits.K, + doc="Temperature differnce at the outlet side", + ) + delta_temperature_chen_callback(eff_2) + + eff_2.area = Var( + bounds=(0, None), + initialize=1000.0, + doc="Heat exchange area", + units=pyunits.m**2, + ) + + eff_2.overall_heat_transfer_coefficient = Var( + eff_2.flowsheet().time, + bounds=(0, None), + initialize=100.0, + doc="Overall heat transfer coefficient", + units=pyunits.W / pyunits.m**2 / pyunits.K, + ) + + eff_2.overall_heat_transfer_coefficient[0].fix(100) + + @m.Constraint(eff_2.flowsheet().time, doc="delta_temperature_in at the 2nd effect") + def delta_temperature_in_eff2(b, t): + return ( + b.fs.eff_2.delta_temperature_in[t] + == b.fs.eff_1.properties_vapor[0].temperature + - b.fs.eff_2.temperature_operating + ) + + @m.Constraint(eff_2.flowsheet().time, doc="delta_temperature_out at the 2nd effect") + def delta_temperature_out_eff2(b, t): + return ( + b.fs.eff_2.delta_temperature_out[t] + == b.fs.eff_1.properties_pure_water[0].temperature + - b.fs.eff_2.properties_in[0].temperature + ) + + @m.Constraint(eff_2.flowsheet().time) + def heat_transfer_equation_eff_2(b, t): + return b.fs.eff_1.energy_flow_superheated_vapor == ( + b.fs.eff_2.overall_heat_transfer_coefficient[t] + * b.fs.eff_2.area + * b.fs.eff_2.delta_temperature[0] + ) + + iscale.set_scaling_factor(eff_2.delta_temperature_in, 1e-1) + iscale.set_scaling_factor(eff_2.delta_temperature_out, 1e-1) + iscale.set_scaling_factor(eff_2.area, 1e-1) + iscale.set_scaling_factor(eff_2.overall_heat_transfer_coefficient, 1e-1) + + +def add_heat_exchanger_eff3(m): + eff_2 = m.fs.eff_2 + eff_3 = m.fs.eff_3 + + eff_3.delta_temperature_in = Var( + eff_3.flowsheet().time, + initialize=35, + bounds=(None, None), + units=pyunits.K, + doc="Temperature differnce at the inlet side", + ) + eff_3.delta_temperature_out = Var( + eff_3.flowsheet().time, + initialize=35, + bounds=(None, None), + units=pyunits.K, + doc="Temperature differnce at the outlet side", + ) + delta_temperature_chen_callback(eff_3) + + eff_3.area = Var( + bounds=(0, None), + initialize=1000.0, + doc="Heat exchange area", + units=pyunits.m**2, + ) + + eff_3.overall_heat_transfer_coefficient = Var( + eff_3.flowsheet().time, + bounds=(0, None), + initialize=100.0, + doc="Overall heat transfer coefficient", + units=pyunits.W / pyunits.m**2 / pyunits.K, + ) + + eff_3.overall_heat_transfer_coefficient[0].fix(100) + + @m.Constraint(eff_3.flowsheet().time, doc="delta_temperature_in at the 2nd effect") + def delta_temperature_in_eff3(b, t): + return ( + eff_3.delta_temperature_in[t] + == eff_2.properties_vapor[0].temperature - eff_3.temperature_operating + ) + + @m.Constraint(eff_3.flowsheet().time, doc="delta_temperature_out at the 2nd effect") + def delta_temperature_out_eff3(b, t): + return ( + eff_3.delta_temperature_out[t] + == eff_2.properties_pure_water[0].temperature + - eff_3.properties_in[0].temperature + ) + + @m.Constraint(eff_3.flowsheet().time) + def heat_transfer_equation_eff3(b, t): + return eff_2.energy_flow_superheated_vapor == ( + eff_3.overall_heat_transfer_coefficient[t] + * eff_3.area + * eff_3.delta_temperature[0] + ) + + iscale.set_scaling_factor(eff_3.delta_temperature_in, 1e-1) + iscale.set_scaling_factor(eff_3.delta_temperature_out, 1e-1) + iscale.set_scaling_factor(eff_3.area, 1e-1) + iscale.set_scaling_factor(eff_3.overall_heat_transfer_coefficient, 1e-1) + + +def add_heat_exchanger_eff4(m): + eff_3 = m.fs.eff_3 + eff_4 = m.fs.eff_4 + + eff_4.delta_temperature_in = Var( + eff_4.flowsheet().time, + initialize=35, + bounds=(None, None), + units=pyunits.K, + doc="Temperature differnce at the inlet side", + ) + eff_4.delta_temperature_out = Var( + eff_4.flowsheet().time, + initialize=35, + bounds=(None, None), + units=pyunits.K, + doc="Temperature differnce at the outlet side", + ) + delta_temperature_chen_callback(eff_4) + + eff_4.area = Var( + bounds=(0, None), + initialize=1000.0, + doc="Heat exchange area", + units=pyunits.m**2, + ) + + eff_4.overall_heat_transfer_coefficient = Var( + eff_4.flowsheet().time, + bounds=(0, None), + initialize=100.0, + doc="Overall heat transfer coefficient", + units=pyunits.W / pyunits.m**2 / pyunits.K, + ) + + eff_4.overall_heat_transfer_coefficient[0].fix(100) + + @m.Constraint(eff_4.flowsheet().time, doc="delta_temperature_in at the 2nd effect") + def delta_temperature_in_eff4(b, t): + return ( + eff_4.delta_temperature_in[t] + == eff_3.properties_vapor[0].temperature - eff_4.temperature_operating + ) + + @m.Constraint(eff_4.flowsheet().time, doc="delta_temperature_out at the 2nd effect") + def delta_temperature_out_eff4(b, t): + return ( + eff_4.delta_temperature_out[t] + == eff_3.properties_pure_water[0].temperature + - eff_4.properties_in[0].temperature + ) + + @m.Constraint(eff_4.flowsheet().time) + def heat_transfer_equation_eff4(b, t): + return eff_3.energy_flow_superheated_vapor == ( + eff_4.overall_heat_transfer_coefficient[t] + * eff_4.area + * eff_4.delta_temperature[0] + ) + + iscale.set_scaling_factor(eff_4.delta_temperature_in, 1e-1) + iscale.set_scaling_factor(eff_4.delta_temperature_out, 1e-1) + iscale.set_scaling_factor(eff_4.area, 1e-1) + iscale.set_scaling_factor(eff_4.overall_heat_transfer_coefficient, 1e-1) + + +def multi_effect_crystallizer_initialization(m): + # Set scaling factors + m.fs.props.set_default_scaling("flow_mass_phase_comp", 1e-1, index=("Liq", "H2O")) + m.fs.props.set_default_scaling("flow_mass_phase_comp", 1e-1, index=("Liq", "NaCl")) + m.fs.props.set_default_scaling("flow_mass_phase_comp", 1e-1, index=("Vap", "H2O")) + m.fs.props.set_default_scaling("flow_mass_phase_comp", 1e-1, index=("Sol", "NaCl")) + + calculate_scaling_factors(m) + + m.fs.eff_1.initialize() + m.fs.eff_2.initialize() + m.fs.eff_3.initialize() + m.fs.eff_4.initialize() + + # Unfix dof + brine_salinity = ( + m.fs.eff_1.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"].value + ) + + for eff in [m.fs.eff_2, m.fs.eff_3, m.fs.eff_4]: + eff.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"].unfix() + eff.inlet.flow_mass_phase_comp[0, "Liq", "H2O"].unfix() + eff.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"].fix(brine_salinity) + + # Energy is provided from the previous effect + @m.Constraint(doc="Energy supplied to the 2nd effect") + def eqn_energy_from_eff1(b): + return b.fs.eff_2.work_mechanical[0] == b.fs.eff_1.energy_flow_superheated_vapor + + @m.Constraint(doc="Energy supplied to the 3rd effect") + def eqn_energy_from_eff2(b): + return b.fs.eff_3.work_mechanical[0] == b.fs.eff_2.energy_flow_superheated_vapor + + @m.Constraint(doc="Energy supplied to the 4th effect") + def eqn_energy_from_eff3(b): + return b.fs.eff_4.work_mechanical[0] == b.fs.eff_3.energy_flow_superheated_vapor + + +if __name__ == "__main__": + m = build_fs_multi_effect_crystallizer( + operating_pressure_eff1=0.45, # bar + operating_pressure_eff2=0.25, # bar + operating_pressure_eff3=0.208, # bar + operating_pressure_eff4=0.095, # bar + feed_flow_mass=1, # kg/s + feed_mass_frac_NaCl=0.3, + feed_pressure=101325, # Pa + feed_temperature=273.15 + 20, # K + crystallizer_yield=0.5, + steam_pressure=1.5, # bar (gauge pressure) + ) + + multi_effect_crystallizer_initialization(m) + + print(degrees_of_freedom(m)) + solver = get_solver() + + results = solver.solve(m) + + assert results.solver.termination_condition == TerminationCondition.optimal + assert results.solver.status == SolverStatus.ok diff --git a/src/watertap_contrib/reflo/property_models/cryst_prop_pack.py b/src/watertap_contrib/reflo/property_models/cryst_prop_pack.py new file mode 100644 index 00000000..54a863cc --- /dev/null +++ b/src/watertap_contrib/reflo/property_models/cryst_prop_pack.py @@ -0,0 +1,2160 @@ +################################################################################# +# WaterTAP Copyright (c) 2020-2024, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# National Renewable Energy Laboratory, and National Energy Technology +# Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +################################################################################# +""" +Initial crystallization property package for H2O-NaCl system +""" + +# Import Python libraries +import idaes.logger as idaeslog + +from enum import Enum, auto + +# Import Pyomo libraries +from pyomo.environ import ( + Constraint, + Expression, + Reals, + NonNegativeReals, + Var, + Param, + exp, + log, + value, + check_optimal_termination, +) +from pyomo.environ import units as pyunits +from pyomo.common.config import ConfigValue, In + +# Import IDAES cores +from idaes.core import ( + declare_process_block_class, + MaterialFlowBasis, + PhysicalParameterBlock, + StateBlockData, + StateBlock, + MaterialBalanceType, + EnergyBalanceType, +) +from idaes.core.base.components import Solute, Solvent +from idaes.core.base.phases import ( + LiquidPhase, + VaporPhase, + SolidPhase, + PhaseType as PT, +) +from idaes.core.util.constants import Constants +from idaes.core.util.initialization import ( + fix_state_vars, + revert_state_vars, + solve_indexed_blocks, +) +from idaes.core.util.misc import extract_data +from watertap.core.solvers import get_solver +from idaes.core.util.model_statistics import ( + degrees_of_freedom, + number_unfixed_variables, +) +from idaes.core.util.exceptions import ( + ConfigurationError, + InitializationError, + PropertyPackageError, +) +import idaes.core.util.scaling as iscale + + +# Set up logger +_log = idaeslog.getLogger(__name__) + +__author__ = "Oluwamayowa Amusat" + + +class HeatOfCrystallizationModel(Enum): + constant = auto() # Use constant heat of crystallization + zero = auto() # Assume heat of crystallization is zero + temp_dependent = auto() # Use temperature-dependent heat of crystallization + + +@declare_process_block_class("NaClParameterBlock") +class NaClParameterData(PhysicalParameterBlock): + CONFIG = PhysicalParameterBlock.CONFIG() + + CONFIG.declare( + "heat_of_crystallization_model", + ConfigValue( + default=HeatOfCrystallizationModel.constant, + domain=In(HeatOfCrystallizationModel), + description="Heat of crystallization construction flag", + doc=""" + Options to account for heat of crystallization value for NaCl. + + **default** - ``HeatOfCrystallizationModel.constant`` + + .. csv-table:: + :header: "Configuration Options", "Description" + + "``HeatOfCrystallizationModel.constant``", "Fixed heat of crystallization for NaCl based on literature" + "``HeatOfCrystallizationModel.zero``", "Zero heat of crystallization assumption" + "``HeatOfCrystallizationModel.temp_dependent``", "Temperature-dependent heat of crystallization for NaCl" + """, + ), + ) + + def build(self): + super().build() + self._state_block_class = NaClStateBlock + + # Component + self.H2O = Solvent(valid_phase_types=[PT.liquidPhase, PT.vaporPhase]) + self.NaCl = Solute(valid_phase_types=[PT.liquidPhase, PT.solidPhase]) + + # Phases + self.Liq = LiquidPhase(component_list=["H2O", "NaCl"]) + self.Vap = VaporPhase(component_list=["H2O"]) + self.Sol = SolidPhase(component_list=["NaCl"]) + + """ + References + This package was developed from the following references: + + - K.G.Nayar, M.H.Sharqawy, L.D.Banchik, and J.H.Lienhard V, "Thermophysical properties of seawater: A review and + new correlations that include pressure dependence,"Desalination, Vol.390, pp.1 - 24, 2016. + doi: 10.1016/j.desal.2016.02.024(preprint) + + - Mostafa H.Sharqawy, John H.Lienhard V, and Syed M.Zubair, "Thermophysical properties of seawater: A review of + existing correlations and data,"Desalination and Water Treatment, Vol.16, pp.354 - 380, April 2010. + (2017 corrections provided at http://web.mit.edu/seawater) + + - Laliberté, M. & Cooper, W. E. Model for Calculating the Density of Aqueous Electrolyte Solutions + Journal of Chemical & Engineering Data, American Chemical Society (ACS), 2004, 49, 1141-1151. + + - Laliberté, M. A Model for Calculating the Heat Capacity of Aqueous Solutions, with Updated Density and Viscosity Data + Journal of Chemical & Engineering Data, American Chemical Society (ACS), 2009, 54, 1725-1760 + Liquid NaCl heat capacity and density parameters available https://pubs.acs.org/doi/10.1021/je8008123 + + - Sparrow, B. S. Empirical equations for the thermodynamic properties of aqueous sodium chloride + Desalination, Elsevier BV, 2003, 159, 161-170 + + - Chase, M.W., Jr., NIST-JANAF Themochemical Tables, Fourth Edition, J. Phys. Chem. Ref. Data, Monograph 9, + 1998, 1-1951. + + - El-Dessouky, H. T. & Ettouney, H. M. (Eds.). Appendix A - Thermodynamic Properties. + Fundamentals of Salt Water Desalination, Elsevier Science B.V., 2002, 525-563 + + - Tavare, N. S. Industrial Crystallization, Springer US, 2013. + """ + + # Unit definitions + dens_units = pyunits.kg / pyunits.m**3 + t_inv_units = pyunits.K**-1 + enth_mass_units = pyunits.J / pyunits.kg + enth_mass_units_2 = pyunits.kJ / pyunits.kg + cp_units = pyunits.J / (pyunits.kg * pyunits.K) + cp_units_2 = pyunits.kJ / (pyunits.kg * pyunits.K) + cp_units_3 = pyunits.J / (pyunits.mol * pyunits.K) + + # molecular weights of solute and solvent + mw_comp_data = {"H2O": 18.01528e-3, "NaCl": 58.44e-3} + self.mw_comp = Param( + self.component_list, + initialize=extract_data(mw_comp_data), + units=pyunits.kg / pyunits.mol, + doc="Molecular weight kg/mol", + ) + + # Solubility parameters from (1) surrogate model (2) Sparrow paper + # # 1. Surrogate + # self.sol_param_A1 = Param(initialize= 526.706475, units = pyunits.g / pyunits.L, doc=' Solubility parameter A1 [g/L] for NaCl surrogate') + # self.sol_param_A2 = Param(initialize= -1.326952, units = (pyunits.g / pyunits.L) * pyunits.K ** -1, doc=' Solubility parameter A2 [g/L] for NaCl surrogate') + # self.sol_param_A3 = Param(initialize= 0.002574, units = (pyunits.g / pyunits.L) * pyunits.K ** -2, doc=' Solubility parameter A3 [g/L] for NaCl surrogate') + # 2. Sparrow + self.sol_param_A1 = Param( + initialize=0.2628, + units=pyunits.dimensionless, + doc=" Solubility parameter A1 for NaCl", + ) + self.sol_param_A2 = Param( + initialize=62.75e-6, + units=pyunits.K**-1, + doc=" Solubility parameter A2 for NaCl", + ) + self.sol_param_A3 = Param( + initialize=1.084e-6, + units=pyunits.K**-2, + doc=" Solubility parameter A3 for NaCl", + ) + + # Mass density value for NaCl crystals in solid phase: fixed for now at Tavare value - may not be accurate? + self.dens_mass_param_NaCl = Param( + initialize=2115, + units=pyunits.kg / pyunits.m**3, + doc="NaCl crystal density", + ) + + # Heat of crystallization parameter - fixed value based on heat of fusion from Perry (Table 2-147) + self.dh_crystallization_param = Param( + initialize=-520, units=enth_mass_units_2, doc="NaCl heat of crystallization" + ) + + # Mass density parameters for pure NaCl liquid based on Eq. 9 in Laliberte and Cooper (2004). + self.dens_mass_param_NaCl_liq_C0 = Var( + within=Reals, + initialize=-0.00433, + units=dens_units, + doc="Mass density parameter C0 for liquid NaCl", + ) + self.dens_mass_param_NaCl_liq_C1 = Var( + within=Reals, + initialize=0.06471, + units=dens_units, + doc="Mass density parameter C1 for liquid NaCl", + ) + self.dens_mass_param_NaCl_liq_C2 = Var( + within=Reals, + initialize=1.01660, + units=pyunits.dimensionless, + doc="Mass density parameter C2 for liquid NaCl", + ) + self.dens_mass_param_NaCl_liq_C3 = Var( + within=Reals, + initialize=0.014624, + units=t_inv_units, + doc="Mass density parameter C3 for liquid NaCl", + ) + self.dens_mass_param_NaCl_liq_C4 = Var( + within=Reals, + initialize=3315.6, + units=pyunits.K, + doc="Mass density parameter C4 for liquid NaCl", + ) + + # Mass density parameters for solvent in liquid phase, eq. 8 in Sharqawy et al. (2010) + self.dens_mass_param_A1 = Var( + within=Reals, + initialize=9.999e2, + units=dens_units, + doc="Mass density parameter A1", + ) + self.dens_mass_param_A2 = Var( + within=Reals, + initialize=2.034e-2, + units=dens_units * t_inv_units, + doc="Mass density parameter A2", + ) + self.dens_mass_param_A3 = Var( + within=Reals, + initialize=-6.162e-3, + units=dens_units * t_inv_units**2, + doc="Mass density parameter A3", + ) + self.dens_mass_param_A4 = Var( + within=Reals, + initialize=2.261e-5, + units=dens_units * t_inv_units**3, + doc="Mass density parameter A4", + ) + self.dens_mass_param_A5 = Var( + within=Reals, + initialize=-4.657e-8, + units=dens_units * t_inv_units**4, + doc="Mass density parameter A5", + ) + + # Latent heat of evaporation of pure water: Parameters from Sharqawy et al. (2010), eq. 54 + self.dh_vap_w_param_0 = Var( + within=Reals, + initialize=2.501e6, + units=enth_mass_units, + doc="Latent heat of pure water parameter 0", + ) + self.dh_vap_w_param_1 = Var( + within=Reals, + initialize=-2.369e3, + units=enth_mass_units * t_inv_units**1, + doc="Latent heat of pure water parameter 1", + ) + self.dh_vap_w_param_2 = Var( + within=Reals, + initialize=2.678e-1, + units=enth_mass_units * t_inv_units**2, + doc="Latent heat of pure water parameter 2", + ) + self.dh_vap_w_param_3 = Var( + within=Reals, + initialize=-8.103e-3, + units=enth_mass_units * t_inv_units**3, + doc="Latent heat of pure water parameter 3", + ) + self.dh_vap_w_param_4 = Var( + within=Reals, + initialize=-2.079e-5, + units=enth_mass_units * t_inv_units**4, + doc="Latent heat of pure water parameter 4", + ) + + # Specific heat parameters for Cp vapor from NIST Webbook - Chase, M.W., Jr., NIST-JANAF Themochemical Tables + self.cp_vap_param_A = Var( + within=Reals, + initialize=30.09200 / 18.01528e-3, + units=cp_units, + doc="Specific heat of water vapor parameter A", + ) + self.cp_vap_param_B = Var( + within=Reals, + initialize=6.832514 / 18.01528e-3, + units=cp_units * t_inv_units, + doc="Specific heat of water vapor parameter B", + ) + self.cp_vap_param_C = Var( + within=Reals, + initialize=6.793435 / 18.01528e-3, + units=cp_units * t_inv_units**2, + doc="Specific heat of water vapor parameter C", + ) + self.cp_vap_param_D = Var( + within=Reals, + initialize=-2.534480 / 18.01528e-3, + units=cp_units * t_inv_units**3, + doc="Specific heat of water vapor parameter D", + ) + self.cp_vap_param_E = Var( + within=Reals, + initialize=0.082139 / 18.01528e-3, + units=cp_units * t_inv_units**-2, + doc="Specific heat of water vapor parameter E", + ) + + # Specific heat parameters for pure water from eq (9) in Sharqawy et al. (2010) + self.cp_phase_param_A1 = Var( + within=Reals, + initialize=5.328, + units=cp_units, + doc="Specific heat of seawater parameter A1", + ) + self.cp_phase_param_B1 = Var( + within=Reals, + initialize=-6.913e-3, + units=cp_units * t_inv_units, + doc="Specific heat of seawater parameter B1", + ) + self.cp_phase_param_C1 = Var( + within=Reals, + initialize=9.6e-6, + units=cp_units * t_inv_units**2, + doc="Specific heat of seawater parameter C1", + ) + self.cp_phase_param_D1 = Var( + within=Reals, + initialize=2.5e-9, + units=cp_units * t_inv_units**3, + doc="Specific heat of seawater parameter D1", + ) + + # Specific heat parameters for liquid NaCl from eqs. (11) & (12) in Laliberte (2009). + self.cp_param_NaCl_liq_A1 = Var( + within=Reals, + initialize=-0.06936, + units=cp_units_2, + doc="Specific heat parameter A1 for liquid NaCl", + ) + self.cp_param_NaCl_liq_A2 = Var( + within=Reals, + initialize=-0.07821, + units=t_inv_units, + doc="Specific heat parameter A2 for liquid NaCl", + ) + self.cp_param_NaCl_liq_A3 = Var( + within=Reals, + initialize=3.8480, + units=pyunits.dimensionless, + doc="Specific heat parameter A3 for liquid NaCl", + ) + self.cp_param_NaCl_liq_A4 = Var( + within=Reals, + initialize=-11.2762, + units=pyunits.dimensionless, + doc="Specific heat parameter A4 for liquid NaCl", + ) + self.cp_param_NaCl_liq_A5 = Var( + within=Reals, + initialize=8.7319, + units=cp_units_2, + doc="Specific heat parameter A5 for liquid NaCl", + ) + self.cp_param_NaCl_liq_A6 = Var( + within=Reals, + initialize=1.8125, + units=pyunits.dimensionless, + doc="Specific heat parameter A6 for liquid NaCl", + ) + + # Specific heat parameters for solid NaCl : Shomate equation from NIST webbook (https://webbook.nist.gov/cgi/cbook.cgi?ID=C7647145&Mask=6F). + self.cp_param_NaCl_solid_A = Var( + within=Reals, + initialize=50.72389, + units=cp_units_3, + doc="Specific heat parameter A for solid NaCl", + ) + self.cp_param_NaCl_solid_B = Var( + within=Reals, + initialize=6.672267, + units=cp_units_3 / pyunits.K, + doc="Specific heat parameter B for solid NaCl", + ) + self.cp_param_NaCl_solid_C = Var( + within=Reals, + initialize=-2.517167, + units=cp_units_3 / pyunits.K**2, + doc="Specific heat parameter C for solid NaCl", + ) + self.cp_param_NaCl_solid_D = Var( + within=Reals, + initialize=10.15934, + units=cp_units_3 / pyunits.K**3, + doc="Specific heat parameter D for solid NaCl", + ) + self.cp_param_NaCl_solid_E = Var( + within=Reals, + initialize=-0.200675, + units=cp_units_3 * pyunits.K**2, + doc="Specific heat parameter E for solid NaCl", + ) + self.cp_param_NaCl_solid_F = Var( + within=Reals, + initialize=-427.2115, + units=cp_units_3 * pyunits.K, + doc="Specific heat parameter F for solid NaCl", + ) + # self.cp_param_NaCl_solid_G = Var(within=Reals, initialize=130.3973, units=cp_units_2, doc='Specific heat parameter G for solid NaCl') + self.cp_param_NaCl_solid_H = Var( + within=Reals, + initialize=-411.1203, + units=cp_units_3 * pyunits.K, + doc="Specific heat parameter H for solid NaCl", + ) + + # Vapour pressure parameters for NaCl solution from Sparrow (2003): 0 < T < 150 degC + self.pressure_sat_param_A1 = Var( + within=Reals, + initialize=0.9083e-3, + units=pyunits.MPa, + doc="Vapour pressure parameter A1", + ) + self.pressure_sat_param_A2 = Var( + within=Reals, + initialize=-0.569e-3, + units=pyunits.MPa, + doc="Vapour pressure parameter A2", + ) + self.pressure_sat_param_A3 = Var( + within=Reals, + initialize=0.1945e-3, + units=pyunits.MPa, + doc="Vapour pressure parameter A3", + ) + self.pressure_sat_param_A4 = Var( + within=Reals, + initialize=-3.736e-3, + units=pyunits.MPa, + doc="Vapour pressure parameter A4", + ) + self.pressure_sat_param_A5 = Var( + within=Reals, + initialize=2.82e-3, + units=pyunits.MPa, + doc="Vapour pressure parameter A5", + ) + self.pressure_sat_param_B1 = Var( + within=Reals, + initialize=-0.0669e-3, + units=pyunits.MPa / pyunits.K, + doc="Vapour pressure parameter B1", + ) + self.pressure_sat_param_B2 = Var( + within=Reals, + initialize=0.0582e-3, + units=pyunits.MPa / pyunits.K, + doc="Vapour pressure parameter B2", + ) + self.pressure_sat_param_B3 = Var( + within=Reals, + initialize=-0.1668e-3, + units=pyunits.MPa / pyunits.K, + doc="Vapour pressure parameter B3", + ) + self.pressure_sat_param_B4 = Var( + within=Reals, + initialize=0.6761e-3, + units=pyunits.MPa / pyunits.K, + doc="Vapour pressure parameter B4", + ) + self.pressure_sat_param_B5 = Var( + within=Reals, + initialize=-2.091e-3, + units=pyunits.MPa / pyunits.K, + doc="Vapour pressure parameter B5", + ) + self.pressure_sat_param_C1 = Var( + within=Reals, + initialize=7.541e-6, + units=pyunits.MPa / pyunits.K**2, + doc="Vapour pressure parameter C1", + ) + self.pressure_sat_param_C2 = Var( + within=Reals, + initialize=-5.143e-6, + units=pyunits.MPa / pyunits.K**2, + doc="Vapour pressure parameter C2", + ) + self.pressure_sat_param_C3 = Var( + within=Reals, + initialize=6.482e-6, + units=pyunits.MPa / pyunits.K**2, + doc="Vapour pressure parameter C3", + ) + self.pressure_sat_param_C4 = Var( + within=Reals, + initialize=-52.62e-6, + units=pyunits.MPa / pyunits.K**2, + doc="Vapour pressure parameter C4", + ) + self.pressure_sat_param_C5 = Var( + within=Reals, + initialize=115.7e-6, + units=pyunits.MPa / pyunits.K**2, + doc="Vapour pressure parameter C5", + ) + self.pressure_sat_param_D1 = Var( + within=Reals, + initialize=-0.0922e-6, + units=pyunits.MPa / pyunits.K**3, + doc="Vapour pressure parameter D1", + ) + self.pressure_sat_param_D2 = Var( + within=Reals, + initialize=0.0649e-6, + units=pyunits.MPa / pyunits.K**3, + doc="Vapour pressure parameter D2", + ) + self.pressure_sat_param_D3 = Var( + within=Reals, + initialize=-0.1313e-6, + units=pyunits.MPa / pyunits.K**3, + doc="Vapour pressure parameter D3", + ) + self.pressure_sat_param_D4 = Var( + within=Reals, + initialize=0.8024e-6, + units=pyunits.MPa / pyunits.K**3, + doc="Vapour pressure parameter D4", + ) + self.pressure_sat_param_D5 = Var( + within=Reals, + initialize=-1.986e-6, + units=pyunits.MPa / pyunits.K**3, + doc="Vapour pressure parameter D5", + ) + self.pressure_sat_param_E1 = Var( + within=Reals, + initialize=1.237e-9, + units=pyunits.MPa / pyunits.K**4, + doc="Vapour pressure parameter E1", + ) + self.pressure_sat_param_E2 = Var( + within=Reals, + initialize=-0.753e-9, + units=pyunits.MPa / pyunits.K**4, + doc="Vapour pressure parameter E2", + ) + self.pressure_sat_param_E3 = Var( + within=Reals, + initialize=0.1448e-9, + units=pyunits.MPa / pyunits.K**4, + doc="Vapour pressure parameter E3", + ) + self.pressure_sat_param_E4 = Var( + within=Reals, + initialize=-6.964e-9, + units=pyunits.MPa / pyunits.K**4, + doc="Vapour pressure parameter E4", + ) + self.pressure_sat_param_E5 = Var( + within=Reals, + initialize=14.61e-9, + units=pyunits.MPa / pyunits.K**4, + doc="Vapour pressure parameter E5", + ) + + # Parameters for saturation temperature of water vapour from eq. A.12 in El-Dessouky and Ettouney + self.temp_sat_solvent_A1 = Var( + within=Reals, + initialize=42.6776, + units=pyunits.K, + doc="Water boiling point parameter A1", + ) + self.temp_sat_solvent_A2 = Var( + within=Reals, + initialize=-3892.7, + units=pyunits.K, + doc="Water boiling point parameter A2", + ) + self.temp_sat_solvent_A3 = Var( + within=Reals, + initialize=1000, + units=pyunits.kPa, + doc="Water boiling point parameter A3", + ) + self.temp_sat_solvent_A4 = Var( + within=Reals, + initialize=-9.48654, + units=pyunits.dimensionless, + doc="Water boiling point parameter A4", + ) + + # Parameters for specific enthalpy of pure water in liquid phase from eq. 55 in Sharqawy et al. (2010) + self.enth_mass_solvent_param_A1 = Var( + within=Reals, + initialize=141.355, + units=enth_mass_units, + doc="Specific enthalpy parameter A1", + ) + self.enth_mass_solvent_param_A2 = Var( + within=Reals, + initialize=4202.07, + units=enth_mass_units * t_inv_units, + doc="Specific enthalpy parameter A2", + ) + self.enth_mass_solvent_param_A3 = Var( + within=Reals, + initialize=-0.535, + units=enth_mass_units * t_inv_units**2, + doc="Specific enthalpy parameter A3", + ) + self.enth_mass_solvent_param_A4 = Var( + within=Reals, + initialize=0.004, + units=enth_mass_units * t_inv_units**3, + doc="Specific enthalpy parameter A4", + ) + + # Enthalpy parameters for NaCl solution from Sparrow (2003): 0 < T < 300 degC + self.enth_phase_param_A1 = Var( + within=Reals, + initialize=0.0005e3, + units=enth_mass_units_2, + doc="Solution enthalpy parameter A1", + ) + self.enth_phase_param_A2 = Var( + within=Reals, + initialize=0.0378e3, + units=enth_mass_units_2, + doc="Solution enthalpy parameter A2", + ) + self.enth_phase_param_A3 = Var( + within=Reals, + initialize=-0.3682e3, + units=enth_mass_units_2, + doc="Solution enthalpy parameter A3", + ) + self.enth_phase_param_A4 = Var( + within=Reals, + initialize=-0.6529e3, + units=enth_mass_units_2, + doc="Solution enthalpy parameter A4", + ) + self.enth_phase_param_A5 = Var( + within=Reals, + initialize=2.89e3, + units=enth_mass_units_2, + doc="Solution enthalpy parameter A5", + ) + self.enth_phase_param_B1 = Var( + within=Reals, + initialize=4.145, + units=enth_mass_units_2 / pyunits.K, + doc="Solution enthalpy parameter B1", + ) + self.enth_phase_param_B2 = Var( + within=Reals, + initialize=-4.973, + units=enth_mass_units_2 / pyunits.K, + doc="Solution enthalpy parameter B2", + ) + self.enth_phase_param_B3 = Var( + within=Reals, + initialize=4.482, + units=enth_mass_units_2 / pyunits.K, + doc="Solution enthalpy parameter B3", + ) + self.enth_phase_param_B4 = Var( + within=Reals, + initialize=18.31, + units=enth_mass_units_2 / pyunits.K, + doc="Solution enthalpy parameter B4", + ) + self.enth_phase_param_B5 = Var( + within=Reals, + initialize=-46.41, + units=enth_mass_units_2 / pyunits.K, + doc="Solution enthalpy parameter B5", + ) + self.enth_phase_param_C1 = Var( + within=Reals, + initialize=0.0007, + units=enth_mass_units_2 / pyunits.K**2, + doc="Solution enthalpy parameter C1", + ) + self.enth_phase_param_C2 = Var( + within=Reals, + initialize=-0.0059, + units=enth_mass_units_2 / pyunits.K**2, + doc="Solution enthalpy parameter C2", + ) + self.enth_phase_param_C3 = Var( + within=Reals, + initialize=0.0854, + units=enth_mass_units_2 / pyunits.K**2, + doc="Solution enthalpy parameter C3", + ) + self.enth_phase_param_C4 = Var( + within=Reals, + initialize=-0.4951, + units=enth_mass_units_2 / pyunits.K**2, + doc="Solution enthalpy parameter C4", + ) + self.enth_phase_param_C5 = Var( + within=Reals, + initialize=0.8255, + units=enth_mass_units_2 / pyunits.K**2, + doc="Solution enthalpy parameter C5", + ) + self.enth_phase_param_D1 = Var( + within=Reals, + initialize=-0.0048e-3, + units=enth_mass_units_2 / pyunits.K**3, + doc="Solution enthalpy parameter D1", + ) + self.enth_phase_param_D2 = Var( + within=Reals, + initialize=0.0639e-3, + units=enth_mass_units_2 / pyunits.K**3, + doc="Solution enthalpy parameter D2", + ) + self.enth_phase_param_D3 = Var( + within=Reals, + initialize=-0.714e-3, + units=enth_mass_units_2 / pyunits.K**3, + doc="Solution enthalpy parameter D3", + ) + self.enth_phase_param_D4 = Var( + within=Reals, + initialize=3.273e-3, + units=enth_mass_units_2 / pyunits.K**3, + doc="Solution enthalpy parameter D4", + ) + self.enth_phase_param_D5 = Var( + within=Reals, + initialize=-4.85e-3, + units=enth_mass_units_2 / pyunits.K**3, + doc="Solution enthalpy parameter D5", + ) + self.enth_phase_param_E1 = Var( + within=Reals, + initialize=0.0202e-6, + units=enth_mass_units_2 / pyunits.K**4, + doc="Solution enthalpy parameter E1", + ) + self.enth_phase_param_E2 = Var( + within=Reals, + initialize=-0.2432e-6, + units=enth_mass_units_2 / pyunits.K**4, + doc="Solution enthalpy parameter E2", + ) + self.enth_phase_param_E3 = Var( + within=Reals, + initialize=2.054e-6, + units=enth_mass_units_2 / pyunits.K**4, + doc="Solution enthalpy parameter E3", + ) + self.enth_phase_param_E4 = Var( + within=Reals, + initialize=-8.211e-6, + units=enth_mass_units_2 / pyunits.K**4, + doc="Solution enthalpy parameter E4", + ) + self.enth_phase_param_E5 = Var( + within=Reals, + initialize=11.43e-6, + units=enth_mass_units_2 / pyunits.K**4, + doc="Solution enthalpy parameter E5", + ) + + for v in self.component_objects(Var): + v.fix() + + # ---default scaling--- + self.set_default_scaling("temperature", 1e-2) + self.set_default_scaling("pressure", 1e-6) + self.set_default_scaling("pressure_sat", 1e-5) + self.set_default_scaling("dens_mass_solvent", 1e-3, index="Liq") + self.set_default_scaling("dens_mass_solvent", 1, index="Vap") + self.set_default_scaling("dens_mass_solute", 1e-3, index="Sol") + self.set_default_scaling("dens_mass_solute", 1e-3, index="Liq") + self.set_default_scaling("dens_mass_phase", 1e-3, index="Liq") + self.set_default_scaling("enth_mass_solvent", 1e-2, index="Liq") + self.set_default_scaling("enth_mass_solvent", 1e-3, index="Vap") + self.set_default_scaling("cp_mass_phase", 1e-3, index="Liq") + self.set_default_scaling("dh_vap_mass_solvent", 1e-3) + self.set_default_scaling("dh_crystallization_mass_comp", 1e-2, index="NaCl") + + @classmethod + def define_metadata(cls, obj): + obj.add_default_units( + { + "time": pyunits.s, + "length": pyunits.m, + "mass": pyunits.kg, + "amount": pyunits.mol, + "temperature": pyunits.K, + } + ) + + obj.add_properties( + { + "flow_mass_phase_comp": {"method": None}, + "temperature": {"method": None}, + "pressure": {"method": None}, + "solubility_mass_phase_comp": {"method": "_solubility_mass_phase_comp"}, + "solubility_mass_frac_phase_comp": { + "method": "_solubility_mass_frac_phase_comp" + }, + "mass_frac_phase_comp": {"method": "_mass_frac_phase_comp"}, + "dens_mass_phase": {"method": "_dens_mass_phase"}, + "cp_mass_phase": {"method": "_cp_mass_phase"}, + "flow_vol_phase": {"method": "_flow_vol_phase"}, + "flow_vol": {"method": "_flow_vol"}, + "pressure_sat": {"method": "_pressure_sat"}, + "conc_mass_phase_comp": {"method": "_conc_mass_phase_comp"}, + "enth_mass_phase": {"method": "_enth_mass_phase"}, + "dh_crystallization_mass_comp": { + "method": "_dh_crystallization_mass_comp" + }, + "flow_mol_phase_comp": {"method": "_flow_mol_phase_comp"}, + "mole_frac_phase_comp": {"method": "_mole_frac_phase_comp"}, + } + ) + + obj.define_custom_properties( + { + "dens_mass_solvent": {"method": "_dens_mass_solvent"}, + "dens_mass_solute": {"method": "_dens_mass_solute"}, + "dh_vap_mass_solvent": {"method": "_dh_vap_mass_solvent"}, + "cp_mass_solvent": {"method": "_cp_mass_solvent"}, + "cp_mass_solute": {"method": "_cp_mass_solute"}, + "temperature_sat_solvent": {"method": "_temperature_sat_solvent"}, + "enth_mass_solvent": {"method": "_enth_mass_solvent"}, + "enth_mass_solute": {"method": "_enth_mass_solute"}, + "enth_flow": {"method": "_enth_flow"}, + } + ) + + +class _NaClStateBlock(StateBlock): + """ + This Class contains methods which should be applied to Property Blocks as a + whole, rather than individual elements of indexed Property Blocks. + """ + + def fix_initialization_states(self): + """ + Fixes state variables for state blocks. + + Returns: + None + """ + # Fix state variables + fix_state_vars(self) + + # Constraint on water concentration at outlet - unfix in these cases + for b in self.values(): + if b.config.defined_state is False: + b.conc_mol_comp["H2O"].unfix() + + def initialize( + self, + state_args=None, + state_vars_fixed=False, + hold_state=False, + outlvl=idaeslog.NOTSET, + solver=None, + optarg=None, + ): + """ + Initialization routine for property package. + Keyword Arguments: + state_args : Dictionary with initial guesses for the state vars + chosen. Note that if this method is triggered + through the control volume, and if initial guesses + were not provided at the unit model level, the + control volume passes the inlet values as initial + guess.The keys for the state_args dictionary are: + flow_mass_phase_comp : value at which to initialize + phase component flows + pressure : value at which to initialize pressure + temperature : value at which to initialize temperature + outlvl : sets output level of initialization routine (default=idaeslog.NOTSET) + optarg : solver options dictionary object (default=None) + state_vars_fixed: Flag to denote if state vars have already been + fixed. + - True - states have already been fixed by the + control volume 1D. Control volume 0D + does not fix the state vars, so will + be False if this state block is used + with 0D blocks. + - False - states have not been fixed. The state + block will deal with fixing/unfixing. + solver : Solver object to use during initialization if None is provided + it will use the default solver for IDAES (default = None) + hold_state : flag indicating whether the initialization routine + should unfix any state variables fixed during + initialization (default=False). + - True - states variables are not unfixed, and + a dict of returned containing flags for + which states were fixed during + initialization. + - False - state variables are unfixed after + initialization by calling the + release_state method + Returns: + If hold_states is True, returns a dict containing flags for + which states were fixed during initialization. + """ + # Get loggers + init_log = idaeslog.getInitLogger(self.name, outlvl, tag="properties") + solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="properties") + + # Set solver and options + opt = get_solver(solver, optarg) + + # Fix state variables + flags = fix_state_vars(self, state_args) + # Check when the state vars are fixed already result in dof 0 + for k in self.keys(): + dof = degrees_of_freedom(self[k]) + if dof != 0: + raise PropertyPackageError( + "\nWhile initializing {sb_name}, the degrees of freedom " + "are {dof}, when zero is required. \nInitialization assumes " + "that the state variables should be fixed and that no other " + "variables are fixed. \nIf other properties have a " + "predetermined value, use the calculate_state method " + "before using initialize to determine the values for " + "the state variables and avoid fixing the property variables." + "".format(sb_name=self.name, dof=dof) + ) + + # --------------------------------------------------------------------- + skip_solve = True # skip solve if only state variables are present + for k in self.keys(): + if number_unfixed_variables(self[k]) != 0: + skip_solve = False + + if not skip_solve: + # Initialize properties + with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: + results = solve_indexed_blocks(opt, [self], tee=slc.tee) + init_log.info_high( + "Property initialization: {}.".format(idaeslog.condition(results)) + ) + + # If input block, return flags, else release state + if state_vars_fixed is False: + if hold_state is True: + return flags + else: + self.release_state(flags) + + if (not skip_solve) and (not check_optimal_termination(results)): + raise InitializationError( + f"{self.name} failed to initialize successfully. Please " + f"check the output logs for more information." + ) + + def release_state(self, flags, outlvl=idaeslog.NOTSET): + """ + Method to release state variables fixed during initialisation. + Keyword Arguments: + flags : dict containing information of which state variables + were fixed during initialization, and should now be + unfixed. This dict is returned by initialize if + hold_state=True. + outlvl : sets output level of of logging + """ + # Unfix state variables + init_log = idaeslog.getInitLogger(self.name, outlvl, tag="properties") + revert_state_vars(self, flags) + init_log.info_high("{} State Released.".format(self.name)) + + def calculate_state( + self, + var_args=None, + hold_state=False, + outlvl=idaeslog.NOTSET, + solver=None, + optarg=None, + ): + """ + Solves state blocks given a set of variables and their values. These variables can + be state variables or properties. This method is typically used before + initialization to solve for state variables because non-state variables (i.e. properties) + cannot be fixed in initialization routines. + Keyword Arguments: + var_args : dictionary with variables and their values, they can be state variables or properties + {(VAR_NAME, INDEX): VALUE} + hold_state : flag indicating whether all of the state variables should be fixed after calculate state. + True - State variables will be fixed. + False - State variables will remain unfixed, unless already fixed. + outlvl : idaes logger object that sets output level of solve call (default=idaeslog.NOTSET) + solver : solver name string if None is provided the default solver + for IDAES will be used (default = None) + optarg : solver options dictionary object (default={}) + Returns: + results object from state block solve + """ + # Get logger + solve_log = idaeslog.getSolveLogger(self.name, level=outlvl, tag="properties") + + # Initialize at current state values (not user provided) + self.initialize(solver=solver, optarg=optarg, outlvl=outlvl) + + # Set solver and options + opt = get_solver(solver, optarg) + + # Fix variables and check degrees of freedom + flags = ( + {} + ) # dictionary noting which variables were fixed and their previous state + for k in self.keys(): + sb = self[k] + for (v_name, ind), val in var_args.items(): + var = getattr(sb, v_name) + if iscale.get_scaling_factor(var[ind]) is None: + _log.warning( + "While using the calculate_state method on {sb_name}, variable {v_name} " + "was provided as an argument in var_args, but it does not have a scaling " + "factor. This suggests that the calculate_scaling_factor method has not been " + "used or the variable was created on demand after the scaling factors were " + "calculated. It is recommended to touch all relevant variables (i.e. call " + "them or set an initial value) before using the calculate_scaling_factor " + "method.".format(v_name=v_name, sb_name=sb.name) + ) + if var[ind].is_fixed(): + flags[(k, v_name, ind)] = True + if value(var[ind]) != val: + raise ConfigurationError( + "While using the calculate_state method on {sb_name}, {v_name} was " + "fixed to a value {val}, but it was already fixed to value {val_2}. " + "Unfix the variable before calling the calculate_state " + "method or update var_args." + "".format( + sb_name=sb.name, + v_name=var.name, + val=val, + val_2=value(var[ind]), + ) + ) + else: + flags[(k, v_name, ind)] = False + var[ind].fix(val) + + if degrees_of_freedom(sb) != 0: + raise RuntimeError( + "While using the calculate_state method on {sb_name}, the degrees " + "of freedom were {dof}, but 0 is required. Check var_args and ensure " + "the correct fixed variables are provided." + "".format(sb_name=sb.name, dof=degrees_of_freedom(sb)) + ) + + # Solve + with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: + results = solve_indexed_blocks(opt, [self], tee=slc.tee) + solve_log.info_high( + "Calculate state: {}.".format(idaeslog.condition(results)) + ) + + if not check_optimal_termination(results): + _log.warning( + "While using the calculate_state method on {sb_name}, the solver failed " + "to converge to an optimal solution. This suggests that the user provided " + "infeasible inputs, or that the model is poorly scaled, poorly initialized, " + "or degenerate." + ) + + # unfix all variables fixed with var_args + for (k, v_name, ind), previously_fixed in flags.items(): + if not previously_fixed: + var = getattr(self[k], v_name) + var[ind].unfix() + + # fix state variables if hold_state + if hold_state: + fix_state_vars(self) + + return results + + +@declare_process_block_class("NaClStateBlock", block_class=_NaClStateBlock) +class NaClStateBlockData(StateBlockData): + def build(self): + """Callable method for Block construction.""" + super(NaClStateBlockData, self).build() + self._make_state_vars() + + def _make_state_vars(self): + # Create state variables + self.pressure = Var( + domain=NonNegativeReals, + initialize=101325, + units=pyunits.Pa, + doc="State pressure [Pa]", + ) + + self.temperature = Var( + domain=NonNegativeReals, + initialize=298.15, + bounds=(273.15, 393.15), + units=pyunits.degK, + doc="State temperature [K]", + ) + + self.flow_mass_phase_comp = Var( + self.phase_component_set, + initialize={ + ("Liq", "H2O"): 0.965, + ("Liq", "NaCl"): 0.035, + ("Vap", "H2O"): 0, + ("Sol", "NaCl"): 0, + }, + bounds=(0, None), + domain=NonNegativeReals, + units=pyunits.kg / pyunits.s, + doc="Mass flow rate", + ) + + # Property Methods + + # 1 Mass fraction: From NaCl property package + def _mass_frac_phase_comp(self): + self.mass_frac_phase_comp = Var( + self.phase_component_set, + domain=NonNegativeReals, + initialize={ + ("Liq", "H2O"): 0.965, + ("Liq", "NaCl"): 0.035, + ("Vap", "H2O"): 1.0, + ("Sol", "NaCl"): 1.0, + }, + bounds=(0, 1.0001), + units=pyunits.dimensionless, + doc="Mass fraction", + ) + + def rule_mass_frac_phase_comp(b, p, j): + phase_comp_list = [ + (p, j) + for j in self.params.component_list + if (p, j) in b.phase_component_set + ] + if len(phase_comp_list) == 1: # one component in this phase + return b.mass_frac_phase_comp[p, j] == 1 + else: + return b.mass_frac_phase_comp[p, j] == b.flow_mass_phase_comp[ + p, j + ] / sum(b.flow_mass_phase_comp[p_j] for p_j in phase_comp_list) + + self.eq_mass_frac_phase_comp = Constraint( + self.phase_component_set, rule=rule_mass_frac_phase_comp + ) + + # 2. Solubility in g/L: calculated from solubility mass fraction + def _solubility_mass_phase_comp(self): + self.solubility_mass_phase_comp = Var( + ["Liq"], + ["NaCl"], + domain=NonNegativeReals, + bounds=(300, 1000), + initialize=356.5, + units=pyunits.g / pyunits.L, + doc="solubility of NaCl in water, g/L", + ) + + def rule_solubility_mass_phase_comp(b, j): + return b.solubility_mass_phase_comp[ + "Liq", j + ] == b.solubility_mass_frac_phase_comp["Liq", j] * b.dens_mass_solvent[ + "Liq" + ] / ( + 1 - b.solubility_mass_frac_phase_comp["Liq", j] + ) + + self.eq_solubility_mass_phase_comp = Constraint( + ["NaCl"], rule=rule_solubility_mass_phase_comp + ) + + # 3. Solubility as mass fraction + def _solubility_mass_frac_phase_comp(self): + self.solubility_mass_frac_phase_comp = Var( + ["Liq"], + ["NaCl"], + domain=NonNegativeReals, + bounds=(0, 1.0001), + initialize=0.5, + units=pyunits.dimensionless, + doc="solubility (as mass fraction) of NaCl in water", + ) + + def rule_solubility_mass_frac_phase_comp(b, j): # Sparrow (2003) + t = b.temperature - 273.15 * pyunits.K + return ( + b.solubility_mass_frac_phase_comp["Liq", j] + == b.params.sol_param_A1 + + b.params.sol_param_A2 * t + + b.params.sol_param_A3 * t**2 + ) + + self.eq_solubility_mass_frac_phase_comp = Constraint( + ["NaCl"], rule=rule_solubility_mass_frac_phase_comp + ) + + # 4. Density of solvent (pure water in liquid and vapour phases) + def _dens_mass_solvent(self): + self.dens_mass_solvent = Var( + ["Liq", "Vap"], + initialize=1e3, + bounds=(1e-4, 1e4), + units=pyunits.kg * pyunits.m**-3, + doc="Mass density of pure water", + ) + + def rule_dens_mass_solvent(b, p): + if p == "Liq": # density, eq. 8 in Sharqawy + t = b.temperature - 273.15 * pyunits.K + dens_mass_w = ( + b.params.dens_mass_param_A1 + + b.params.dens_mass_param_A2 * t + + b.params.dens_mass_param_A3 * t**2 + + b.params.dens_mass_param_A4 * t**3 + + b.params.dens_mass_param_A5 * t**4 + ) + return b.dens_mass_solvent[p] == dens_mass_w + elif p == "Vap": + return b.dens_mass_solvent[p] == ( + b.params.mw_comp["H2O"] * b.pressure + ) / (Constants.gas_constant * b.temperature) + + self.eq_dens_mass_solvent = Constraint( + ["Liq", "Vap"], rule=rule_dens_mass_solvent + ) + + # 5. Density of NaCl crystals and liquid + def _dens_mass_solute(self): + self.dens_mass_solute = Var( + ["Sol", "Liq"], + initialize=1e3, + bounds=(1e-4, 1e4), + units=pyunits.kg * pyunits.m**-3, + doc="Mass density of solid NaCl crystals", + ) + + def rule_dens_mass_solute(b, p): + if p == "Sol": + return b.dens_mass_solute[p] == b.params.dens_mass_param_NaCl + elif p == "Liq": # Apparent density in eq. 9 of Laliberte paper + t = b.temperature - 273.15 * pyunits.K + v_app = ( + b.mass_frac_phase_comp["Liq", "NaCl"] + + b.params.dens_mass_param_NaCl_liq_C2 + + (b.params.dens_mass_param_NaCl_liq_C3 * t) + ) / ( + ( + b.mass_frac_phase_comp["Liq", "NaCl"] + * b.params.dens_mass_param_NaCl_liq_C0 + ) + + b.params.dens_mass_param_NaCl_liq_C1 + ) + v_app = v_app / exp( + 0.000001 + * pyunits.K**-2 + * (t + b.params.dens_mass_param_NaCl_liq_C4) ** 2 + ) + return b.dens_mass_solute[p] == 1 / v_app + + self.eq_dens_mass_solute = Constraint( + ["Sol", "Liq"], rule=rule_dens_mass_solute + ) + + # 6. Density of liquid solution (Water + NaCl) + def _dens_mass_phase(self): + self.dens_mass_phase = Var( + ["Liq"], + initialize=1e3, + bounds=(5e2, 1e4), + units=pyunits.kg * pyunits.m**-3, + doc="Mass density of liquid NaCl solution", + ) + + def rule_dens_mass_phase(b): # density, eq. 6 of Laliberte paper + return b.dens_mass_phase["Liq"] == 1 / ( + (b.mass_frac_phase_comp["Liq", "NaCl"] / b.dens_mass_solute["Liq"]) + + (b.mass_frac_phase_comp["Liq", "H2O"] / b.dens_mass_solvent["Liq"]) + ) + + self.eq_dens_mass_phase = Constraint(rule=rule_dens_mass_phase) + + # 7. Latent heat of vapourization of pure water + def _dh_vap_mass_solvent(self): + self.dh_vap_mass_solvent = Var( + initialize=2.4e3, + bounds=(1, 1e5), + units=pyunits.kJ / pyunits.kg, + doc="Latent heat of vaporization of pure water", + ) + + def rule_dh_vap_mass_solvent(b): + t = b.temperature - 273.15 * pyunits.K + dh_vap_sol = ( + b.params.dh_vap_w_param_0 + + b.params.dh_vap_w_param_1 * t + + b.params.dh_vap_w_param_2 * t**2 + + b.params.dh_vap_w_param_3 * t**3 + + b.params.dh_vap_w_param_4 * t**4 + ) + return b.dh_vap_mass_solvent == pyunits.convert( + dh_vap_sol, to_units=pyunits.kJ / pyunits.kg + ) + + self.eq_dh_vap_mass_solvent = Constraint(rule=rule_dh_vap_mass_solvent) + + # 8. Heat capacity of solvent (pure water in liquid and vapour phases) + def _cp_mass_solvent(self): + self.cp_mass_solvent = Var( + ["Liq", "Vap"], + initialize=4e3, + bounds=(1e-5, 1e5), + units=pyunits.J / pyunits.kg / pyunits.K, + doc="Specific heat capacity of pure solvent", + ) + + def rule_cp_mass_solvent(b, p): + if p == "Liq": + # specific heat, eq. 9 in Sharqawy et al. (2010) + # Convert T90 to T68, eq. 4 in Sharqawy et al. (2010); primary reference from Rusby (1991) + t = (b.temperature - 0.00025 * 273.15 * pyunits.K) / (1 - 0.00025) + A = b.params.cp_phase_param_A1 + B = b.params.cp_phase_param_B1 + C = b.params.cp_phase_param_C1 + D = b.params.cp_phase_param_D1 + return ( + b.cp_mass_solvent["Liq"] + == (A + B * t + C * t**2 + D * t**3) * 1000 + ) + elif p == "Vap": + t = b.temperature / 1000 + return ( + b.cp_mass_solvent["Vap"] + == b.params.cp_vap_param_A + + b.params.cp_vap_param_B * t + + b.params.cp_vap_param_C * t**2 + + b.params.cp_vap_param_D * t**3 + + b.params.cp_vap_param_E / t**2 + ) + + self.eq_cp_mass_solvent = Constraint(["Liq", "Vap"], rule=rule_cp_mass_solvent) + + # 9. Heat capacity of solid-phase NaCl crystals + def _cp_mass_solute(self): + self.cp_mass_solute = Var( + ["Liq", "Sol"], + initialize=1e3, + bounds=(-1e4, 1e5), + units=pyunits.J / pyunits.kg / pyunits.K, + doc="Specific heat capacity of solid NaCl crystals", + ) + + def rule_cp_mass_solute(b, p): + if p == "Sol": # Shomate equation for NaCl, NIST + t = b.temperature / (1000 * pyunits.dimensionless) + cp_mass_solute_mol = ( + b.params.cp_param_NaCl_solid_A + + (b.params.cp_param_NaCl_solid_B * t) + + (b.params.cp_param_NaCl_solid_C * t**2) + + (b.params.cp_param_NaCl_solid_D * t**3) + + (b.params.cp_param_NaCl_solid_E / (t**2)) + ) + return ( + b.cp_mass_solute[p] == cp_mass_solute_mol / b.params.mw_comp["NaCl"] + ) + if ( + p == "Liq" + ): # NaCl liq. apparent specific heat capacity, eq. 11-12 of Laliberte (2009) + t = b.temperature - 273.15 * pyunits.K + alpha = ( + (b.params.cp_param_NaCl_liq_A2 * t) + + ( + b.params.cp_param_NaCl_liq_A4 + * (1 - b.mass_frac_phase_comp["Liq", "H2O"]) + ) + + (b.params.cp_param_NaCl_liq_A3 * exp(0.01 * pyunits.K**-1 * t)) + ) + cp_nacl_liq = b.params.cp_param_NaCl_liq_A1 * exp( + alpha + ) + b.params.cp_param_NaCl_liq_A5 * ( + (1 - b.mass_frac_phase_comp["Liq", "H2O"]) + ** b.params.cp_param_NaCl_liq_A6 + ) + return b.cp_mass_solute[p] == pyunits.convert( + cp_nacl_liq, to_units=pyunits.J / pyunits.kg / pyunits.K + ) + + self.eq_cp_mass_solute = Constraint(["Liq", "Sol"], rule=rule_cp_mass_solute) + + # 10. cp of liquid solution (Water + NaCl) + def _cp_mass_phase(self): + self.cp_mass_phase = Var( + ["Liq"], + initialize=4e3, + bounds=(1e-4, 1e5), + units=pyunits.J / pyunits.kg / pyunits.K, + doc="Specific heat capacity of liquid solution", + ) + + def rule_cp_mass_phase(b): # heat capacity, eq. 10 of Laliberte (2009) paper + return ( + b.cp_mass_phase["Liq"] + == b.mass_frac_phase_comp["Liq", "NaCl"] * b.cp_mass_solute["Liq"] + + b.mass_frac_phase_comp["Liq", "H2O"] * b.cp_mass_solvent["Liq"] + ) + + self.eq_cp_mass_phase = Constraint(rule=rule_cp_mass_phase) + + # 11. Volumetric flow rate for each phase + def _flow_vol_phase(self): + self.flow_vol_phase = Var( + self.params.phase_list, + initialize=1, + bounds=(0, None), + units=pyunits.m**3 / pyunits.s, + doc="Volumetric flow rate", + ) + + def rule_flow_vol_phase(b, p): + if p == "Liq": + return ( + b.flow_vol_phase[p] + == sum( + b.flow_mass_phase_comp[p, j] + for j in self.params.component_list + if (p, j) in self.phase_component_set + ) + / b.dens_mass_phase[p] + ) + elif p == "Sol": + return ( + b.flow_vol_phase[p] + == sum( + b.flow_mass_phase_comp[p, j] + for j in self.params.component_list + if (p, j) in self.phase_component_set + ) + / b.dens_mass_solute["Sol"] + ) + elif p == "Vap": + return ( + b.flow_vol_phase[p] + == sum( + b.flow_mass_phase_comp[p, j] + for j in self.params.component_list + if (p, j) in self.phase_component_set + ) + / b.dens_mass_solvent["Vap"] + ) + + self.eq_flow_vol_phase = Constraint( + self.params.phase_list, rule=rule_flow_vol_phase + ) + + # 12. Total volumetric flow rate + def _flow_vol(self): + def rule_flow_vol(b): + return sum(b.flow_vol_phase[p] for p in self.params.phase_list) + + self.flow_vol = Expression(rule=rule_flow_vol) + + # 13. Vapour pressure of the NaCl solution based on the boiling temperature + def _pressure_sat(self): + self.pressure_sat = Var( + initialize=1e3, + bounds=(0.001, 1e6), + units=pyunits.Pa, + doc="Vapor pressure of NaCl solution", + ) + + def rule_pressure_sat(b): # vapor pressure, eq6 in Sparrow (2003) + t = b.temperature - 273.15 * pyunits.K + x = b.mass_frac_phase_comp["Liq", "NaCl"] + + ps_a = ( + b.params.pressure_sat_param_A1 + + (b.params.pressure_sat_param_A2 * x) + + (b.params.pressure_sat_param_A3 * x**2) + + (b.params.pressure_sat_param_A4 * x**3) + + (b.params.pressure_sat_param_A5 * x**4) + ) + + ps_b = ( + b.params.pressure_sat_param_B1 + + (b.params.pressure_sat_param_B2 * x) + + (b.params.pressure_sat_param_B3 * x**2) + + (b.params.pressure_sat_param_B4 * x**3) + + (b.params.pressure_sat_param_B5 * x**4) + ) + + ps_c = ( + b.params.pressure_sat_param_C1 + + (b.params.pressure_sat_param_C2 * x) + + (b.params.pressure_sat_param_C3 * x**2) + + (b.params.pressure_sat_param_C4 * x**3) + + (b.params.pressure_sat_param_C5 * x**4) + ) + + ps_d = ( + b.params.pressure_sat_param_D1 + + (b.params.pressure_sat_param_D2 * x) + + (b.params.pressure_sat_param_D3 * x**2) + + (b.params.pressure_sat_param_D4 * x**3) + + (b.params.pressure_sat_param_D5 * x**4) + ) + + ps_e = ( + b.params.pressure_sat_param_E1 + + (b.params.pressure_sat_param_E2 * x) + + (b.params.pressure_sat_param_E3 * x**2) + + (b.params.pressure_sat_param_E4 * x**3) + + (b.params.pressure_sat_param_E5 * x**4) + ) + + p_sat = ( + ps_a + (ps_b * t) + (ps_c * t**2) + (ps_d * t**3) + (ps_e * t**4) + ) + return b.pressure_sat == pyunits.convert(p_sat, to_units=pyunits.Pa) + + self.eq_pressure_sat = Constraint(rule=rule_pressure_sat) + + # 14. Saturation temperature for water vapour at calculated boiling pressure + def _temperature_sat_solvent(self): + self.temperature_sat_solvent = Var( + initialize=298.15, + bounds=(273.15, 1000.15), + units=pyunits.K, + doc="Vapour (saturation) temperature of pure solvent at boiling (i.e. crystallization) pressure", + ) + + def rule_temperature_sat_solvent(b): + psat = pyunits.convert(b.pressure_sat, to_units=pyunits.kPa) + return ( + b.temperature_sat_solvent + == b.params.temp_sat_solvent_A1 + + b.params.temp_sat_solvent_A2 + / ( + log(psat / b.params.temp_sat_solvent_A3) + + b.params.temp_sat_solvent_A4 + ) + ) + + self.eq_temperature_sat_solvent = Constraint(rule=rule_temperature_sat_solvent) + + # 15. Mass concentration + def _conc_mass_phase_comp(self): + self.conc_mass_phase_comp = Var( + ["Liq"], + self.params.component_list, + initialize=10, + bounds=(0, 1e6), + units=pyunits.kg * pyunits.m**-3, + doc="Mass concentration", + ) + + def rule_conc_mass_phase_comp(b, j): + return ( + b.conc_mass_phase_comp["Liq", j] + == b.dens_mass_phase["Liq"] * b.mass_frac_phase_comp["Liq", j] + ) + + self.eq_conc_mass_phase_comp = Constraint( + self.params.component_list, rule=rule_conc_mass_phase_comp + ) + + # 16. Specific enthalpy of solvent (pure water in liquid and vapour phases) + def _enth_mass_solvent(self): + self.enth_mass_solvent = Var( + ["Liq", "Vap"], + initialize=1e3, + bounds=(1, 1e4), + units=pyunits.kJ * pyunits.kg**-1, + doc="Specific saturated enthalpy of pure solvent", + ) + + def rule_enth_mass_solvent(b, p): + t = b.temperature - 273.15 * pyunits.K + h_w = ( + b.params.enth_mass_solvent_param_A1 + + b.params.enth_mass_solvent_param_A2 * t + + b.params.enth_mass_solvent_param_A3 * t**2 + + b.params.enth_mass_solvent_param_A4 * t**3 + ) + if p == "Liq": # enthalpy, eq. 55 in Sharqawy + return b.enth_mass_solvent[p] == pyunits.convert( + h_w, to_units=pyunits.kJ * pyunits.kg**-1 + ) + elif p == "Vap": + + return ( + b.enth_mass_solvent[p] + == pyunits.convert(h_w, to_units=pyunits.kJ * pyunits.kg**-1) + + +b.dh_vap_mass_solvent + ) + + self.eq_enth_mass_solvent = Constraint( + ["Liq", "Vap"], rule=rule_enth_mass_solvent + ) + + # 17. Specific enthalpy of NaCl solution + def _enth_mass_phase(self): + self.enth_mass_phase = Var( + ["Liq"], + initialize=500, + bounds=(1, 1000), + units=pyunits.kJ * pyunits.kg**-1, + doc="Specific enthalpy of NaCl solution", + ) + + def rule_enth_mass_phase( + b, + ): # specific enthalpy calculation based on Sparrow (2003). + t = ( + b.temperature - 273.15 * pyunits.K + ) # temperature in degC, but pyunits in K + S = b.mass_frac_phase_comp["Liq", "NaCl"] + + enth_a = ( + b.params.enth_phase_param_A1 + + (b.params.enth_phase_param_A2 * S) + + (b.params.enth_phase_param_A3 * S**2) + + (b.params.enth_phase_param_A4 * S**3) + + (b.params.enth_phase_param_A5 * S**4) + ) + + enth_b = ( + b.params.enth_phase_param_B1 + + (b.params.enth_phase_param_B2 * S) + + (b.params.enth_phase_param_B3 * S**2) + + (b.params.enth_phase_param_B4 * S**3) + + (b.params.enth_phase_param_B5 * S**4) + ) + + enth_c = ( + b.params.enth_phase_param_C1 + + (b.params.enth_phase_param_C2 * S) + + (b.params.enth_phase_param_C3 * S**2) + + (b.params.enth_phase_param_C4 * S**3) + + (b.params.enth_phase_param_C5 * S**4) + ) + + enth_d = ( + b.params.enth_phase_param_D1 + + (b.params.enth_phase_param_D2 * S) + + (b.params.enth_phase_param_D3 * S**2) + + (b.params.enth_phase_param_D4 * S**3) + + (b.params.enth_phase_param_D5 * S**4) + ) + + enth_e = ( + b.params.enth_phase_param_E1 + + (b.params.enth_phase_param_E2 * S) + + (b.params.enth_phase_param_E3 * S**2) + + (b.params.enth_phase_param_E4 * S**3) + + (b.params.enth_phase_param_E5 * S**4) + ) + + return b.enth_mass_phase["Liq"] == enth_a + (enth_b * t) + ( + enth_c * t**2 + ) + (enth_d * t**3) + (enth_e * t**4) + + self.eq_enth_mass_phase = Constraint(rule=rule_enth_mass_phase) + + # 18. Heat of crystallization + def _dh_crystallization_mass_comp(self): + self.dh_crystallization_mass_comp = Var( + ["NaCl"], + initialize=1, + bounds=(-1e3, 1e3), + units=pyunits.kJ / pyunits.kg, + doc="NaCl heat of crystallization", + ) + + def rule_dh_crystallization_mass_comp(b): + if ( + b.params.config.heat_of_crystallization_model + == HeatOfCrystallizationModel.constant + ): + return ( + b.dh_crystallization_mass_comp["NaCl"] + == b.params.dh_crystallization_param + ) + elif ( + b.params.config.heat_of_crystallization_model + == HeatOfCrystallizationModel.zero + ): + return ( + b.dh_crystallization_mass_comp["NaCl"] + == 0 * pyunits.kJ / pyunits.kg + ) + elif ( + b.params.config.heat_of_crystallization_model + == HeatOfCrystallizationModel.temp_dependent + ): + raise NotImplementedError( + f"Temperature-dependent heat of crystallization model has not been implemented yet." + ) + + self.eq_dh_crystallization_mass_comp = Constraint( + rule=rule_dh_crystallization_mass_comp + ) + + # 19. Heat capacity of solid-phase NaCl crystals + def _enth_mass_solute(self): + self.enth_mass_solute = Var( + ["Sol"], + initialize=1e3, + bounds=(1e-3, 1e4), + units=pyunits.kJ / pyunits.kg, + doc="Specific enthalpy of solid NaCl crystals", + ) + + def rule_enth_mass_solute(b, p): + ############################ + # Shomate equation for molar enthalpy ofNaCl, NIST + # Note: Tref is 298 K, so changing the Tref to 273 K to match IAPWS is necessary. + # Computation formula for reference temperature change: + # Enthalpy at T relative to 273 K = Enthalpy change relative to 298 K + (Enthalpy at 298 K - Enthalpy at 273 K) + ############################ + + # (i) Enthalpy at original reference temperature (298 K) + enth_mass_solute_mol_tref_1 = 0 # Enthalpy at original temperature (298 K) + + # (ii) Enthalpy at new reference temperature (273.15 K) + tref_2 = 273.15 * pyunits.K / 1000 + enth_mass_solute_mol_tref_2 = ( + (b.params.cp_param_NaCl_solid_A * tref_2) + + ((1 / 2) * b.params.cp_param_NaCl_solid_B * tref_2**2) + + ((1 / 3) * b.params.cp_param_NaCl_solid_C * tref_2**3) + + ((1 / 4) * b.params.cp_param_NaCl_solid_D * tref_2**4) + - (b.params.cp_param_NaCl_solid_E / tref_2) + + b.params.cp_param_NaCl_solid_F + - b.params.cp_param_NaCl_solid_H + ) + + # (iii) Compute enthalpy change at temperature t relative to old reference temperature t_ref_1 + t = b.temperature / (1000 * pyunits.dimensionless) + dh_mass_solute_mol_tref_1 = ( + (b.params.cp_param_NaCl_solid_A * t) + + ((1 / 2) * b.params.cp_param_NaCl_solid_B * t**2) + + ((1 / 3) * b.params.cp_param_NaCl_solid_C * t**3) + + ((1 / 4) * b.params.cp_param_NaCl_solid_D * t**4) + - (b.params.cp_param_NaCl_solid_E / t) + + b.params.cp_param_NaCl_solid_F + - b.params.cp_param_NaCl_solid_H + ) + + # (iv) Compute enthalpy change at temperature t relative to new reference temperature t_ref_2 + enth_mass_solute_mol = dh_mass_solute_mol_tref_1 + ( + enth_mass_solute_mol_tref_1 - enth_mass_solute_mol_tref_2 + ) + + # (v) Convert from molar enthalpy to mass enthalpy + enth_mass_solute_mol = enth_mass_solute_mol / b.params.mw_comp["NaCl"] + return b.enth_mass_solute[p] == enth_mass_solute_mol * ( + pyunits.kJ / pyunits.J + ) + + self.eq_enth_mass_solute = Constraint(["Sol"], rule=rule_enth_mass_solute) + + # 20. Total enthalpy flow for any stream: adds up the enthalpies for the solid, liquid and vapour phases + # Assumes no NaCl is vapour stream or water in crystals + def _enth_flow(self): + # enthalpy flow expression for get_enthalpy_flow_terms method + + def rule_enth_flow(b): # enthalpy flow [J/s] + return ( + sum(b.flow_mass_phase_comp["Liq", j] for j in b.params.component_list) + * b.enth_mass_phase["Liq"] + + b.flow_mass_phase_comp["Vap", "H2O"] * b.enth_mass_solvent["Vap"] + + b.flow_mass_phase_comp["Sol", "NaCl"] * b.enth_mass_solute["Sol"] + ) + + self.enth_flow = Expression(rule=rule_enth_flow) + + # 21. Molar flows + def _flow_mol_phase_comp(self): + self.flow_mol_phase_comp = Var( + self.phase_component_set, + initialize=100, + bounds=(None, None), + domain=NonNegativeReals, + units=pyunits.mol / pyunits.s, + doc="Molar flowrate", + ) + + def rule_flow_mol_phase_comp(b, p, j): + return ( + b.flow_mol_phase_comp[p, j] + == b.flow_mass_phase_comp[p, j] / b.params.mw_comp[j] + ) + + self.eq_flow_mol_phase_comp = Constraint( + self.phase_component_set, rule=rule_flow_mol_phase_comp + ) + + # 22. Mole fractions + def _mole_frac_phase_comp(self): + self.mole_frac_phase_comp = Var( + self.phase_component_set, + initialize=0.1, + bounds=(0, 1.0001), + units=pyunits.dimensionless, + doc="Mole fraction", + ) + + def rule_mole_frac_phase_comp(b, p, j): + phase_comp_list = [ + (p, j) + for j in self.params.component_list + if (p, j) in b.phase_component_set + ] + if len(phase_comp_list) == 1: # one component in this phase + return b.mole_frac_phase_comp[p, j] == 1 + else: + return b.mole_frac_phase_comp[p, j] == b.flow_mol_phase_comp[ + p, j + ] / sum(b.flow_mol_phase_comp[p_j] for (p_j) in phase_comp_list) + + self.eq_mole_frac_phase_comp = Constraint( + self.phase_component_set, rule=rule_mole_frac_phase_comp + ) + + # ----------------------------------------------------------------------------- + # Boilerplate Methods + + def get_material_flow_terms(self, p, j): + """Create material flow terms for control volume.""" + return self.flow_mass_phase_comp[p, j] + + def get_enthalpy_flow_terms(self, p): + """Create enthalpy flow terms.""" + return self.enth_flow + + def default_material_balance_type(self): + return MaterialBalanceType.componentTotal + + def default_energy_balance_type(self): + return EnergyBalanceType.enthalpyTotal + + def get_material_flow_basis(self): + return MaterialFlowBasis.mass + + def define_state_vars(self): + """Define state vars.""" + return { + "flow_mass_phase_comp": self.flow_mass_phase_comp, + "temperature": self.temperature, + "pressure": self.pressure, + } + + # ----------------------------------------------------------------------------- + # Scaling methods + def calculate_scaling_factors(self): + super().calculate_scaling_factors() + + # default scaling factors have already been set with idaes.core.property_base.calculate_scaling_factors() + # for the following variables: flow_mass_phase_comp, pressure, temperature, dens_mass_phase, enth_mass_phase + + # These variables should have user input + if iscale.get_scaling_factor(self.flow_mass_phase_comp["Liq", "H2O"]) is None: + sf = iscale.get_scaling_factor( + self.flow_mass_phase_comp["Liq", "H2O"], default=1e0, warning=True + ) + iscale.set_scaling_factor(self.flow_mass_phase_comp["Liq", "H2O"], sf) + + if iscale.get_scaling_factor(self.flow_mass_phase_comp["Liq", "NaCl"]) is None: + sf = iscale.get_scaling_factor( + self.flow_mass_phase_comp["Liq", "NaCl"], default=1e2, warning=True + ) + iscale.set_scaling_factor(self.flow_mass_phase_comp["Liq", "NaCl"], sf) + + if iscale.get_scaling_factor(self.flow_mass_phase_comp["Sol", "NaCl"]) is None: + sf = iscale.get_scaling_factor( + self.flow_mass_phase_comp["Sol", "NaCl"], default=1e2, warning=True + ) + iscale.set_scaling_factor(self.flow_mass_phase_comp["Sol", "NaCl"], sf) + + if iscale.get_scaling_factor(self.flow_mass_phase_comp["Vap", "H2O"]) is None: + sf = iscale.get_scaling_factor( + self.flow_mass_phase_comp["Vap", "H2O"], default=1e0, warning=True + ) + iscale.set_scaling_factor(self.flow_mass_phase_comp["Vap", "H2O"], sf) + + # scaling factors for molecular weights + for j, v in self.params.mw_comp.items(): + if iscale.get_scaling_factor(v) is None: + iscale.set_scaling_factor(self.params.mw_comp, 1e3) + + # Scaling for solubility (g/L) parameters. Values typically about 300-500, so scale by 1e-3. + if self.is_property_constructed("solubility_mass_phase_comp"): + if iscale.get_scaling_factor(self.solubility_mass_phase_comp) is None: + iscale.set_scaling_factor(self.solubility_mass_phase_comp, 1e-3) + + # Scaling for solubility mass fraction. Values typically about 0-1, so scale by 1e0. + if self.is_property_constructed("solubility_mass_frac_phase_comp"): + if iscale.get_scaling_factor(self.solubility_mass_frac_phase_comp) is None: + iscale.set_scaling_factor(self.solubility_mass_frac_phase_comp, 1e0) + + # Scaling for flow_vol_phase: scaled as scale of dominant component in phase / density of phase + if self.is_property_constructed("flow_vol_phase"): + for p in self.params.phase_list: + if p == "Liq": + if iscale.get_scaling_factor(self.flow_vol_phase[p]) is None: + sf = iscale.get_scaling_factor( + self.flow_mass_phase_comp[p, "H2O"] + ) / iscale.get_scaling_factor(self.dens_mass_phase[p]) + iscale.set_scaling_factor(self.flow_vol_phase[p], sf) + elif p == "Vap": + if iscale.get_scaling_factor(self.flow_vol_phase[p]) is None: + sf = iscale.get_scaling_factor( + self.flow_mass_phase_comp[p, "H2O"] + ) / iscale.get_scaling_factor(self.dens_mass_solvent[p]) + iscale.set_scaling_factor(self.flow_vol_phase[p], sf) + elif p == "Sol": + if iscale.get_scaling_factor(self.flow_vol_phase[p]) is None: + sf = iscale.get_scaling_factor( + self.flow_mass_phase_comp[p, "NaCl"] + ) / iscale.get_scaling_factor(self.dens_mass_solute[p]) + iscale.set_scaling_factor(self.flow_vol_phase[p], sf) + + # Scaling material heat capacities + if self.is_property_constructed("cp_mass_solute"): + for p in ["Sol", "Liq"]: + if iscale.get_scaling_factor(self.cp_mass_solute[p]) is None: + iscale.set_scaling_factor( + self.cp_mass_solute[p], 1e-3 + ) # same as scaling factor of .cp_mass_phase['Liq'] + + if self.is_property_constructed("cp_mass_solvent"): + for p in ["Liq", "Vap"]: + if iscale.get_scaling_factor(self.cp_mass_solvent[p]) is None: + iscale.set_scaling_factor( + self.cp_mass_solvent[p], 1e-3 + ) # same as scaling factor of .cp_mass_phase['Liq'] + + # Scaling saturation temperature + if self.is_property_constructed("temperature_sat_solvent"): + if iscale.get_scaling_factor(self.temperature_sat_solvent) is None: + iscale.set_scaling_factor( + self.temperature_sat_solvent, + iscale.get_scaling_factor(self.temperature), + ) + + # Scaling solute and solvent enthalpies + if self.is_property_constructed("enth_mass_solute"): + if iscale.get_scaling_factor(self.enth_mass_solute) is None: + iscale.set_scaling_factor( + self.enth_mass_solute["Sol"], + iscale.get_scaling_factor(self.enth_mass_solvent["Liq"]), + ) + + if self.is_property_constructed("enth_mass_phase"): + if iscale.get_scaling_factor(self.enth_mass_phase) is None: + iscale.set_scaling_factor( + self.enth_mass_phase["Liq"], + iscale.get_scaling_factor(self.enth_mass_solvent["Liq"]), + ) + + # Scaling enthapy flow - not sure about this one + if self.is_property_constructed("enth_flow"): + iscale.set_scaling_factor( + self.enth_flow, + iscale.get_scaling_factor(self.flow_mass_phase_comp["Liq", "H2O"]) + * iscale.get_scaling_factor(self.enth_mass_phase["Liq"]), + ) + + # Scaling molar flows - derived from flow_mass + if self.is_property_constructed("flow_mol_phase_comp"): + for p, j in self.phase_component_set: + if iscale.get_scaling_factor(self.flow_mol_phase_comp[p, j]) is None: + sf = iscale.get_scaling_factor(self.flow_mass_phase_comp[p, j]) + sf /= iscale.get_scaling_factor(self.params.mw_comp[j]) + iscale.set_scaling_factor(self.flow_mol_phase_comp[p, j], sf) + + ###################################################### + # Scaling for mass fractions - needs verification! + if self.is_property_constructed("mass_frac_phase_comp"): + # Option 1: + for p, j in self.phase_component_set: + if iscale.get_scaling_factor(self.mass_frac_phase_comp[p, j]) is None: + if p == "Sol": + iscale.set_scaling_factor(self.mass_frac_phase_comp[p, j], 1e0) + else: + if j == "NaCl": + sf = iscale.get_scaling_factor( + self.flow_mass_phase_comp[p, j] + ) / iscale.get_scaling_factor( + self.flow_mass_phase_comp[p, "H2O"] + ) + iscale.set_scaling_factor( + self.mass_frac_phase_comp[p, j], sf + ) + elif j == "H2O": + iscale.set_scaling_factor( + self.mass_frac_phase_comp[p, j], 1e0 + ) + + # Scaling for mole fractions - same approach as mass fractions - needs verification! + # Appears to make things worse! + if self.is_property_constructed("mole_frac_phase_comp"): + # Option 1: + for p, j in self.phase_component_set: + if iscale.get_scaling_factor(self.mole_frac_phase_comp[p, j]) is None: + if p == "Sol": + iscale.set_scaling_factor(self.mole_frac_phase_comp[p, j], 1e-1) + else: + if j == "NaCl": + sf = iscale.get_scaling_factor( + self.flow_mol_phase_comp[p, j] + ) / iscale.get_scaling_factor( + self.flow_mol_phase_comp[p, "H2O"] + ) + iscale.set_scaling_factor( + self.mole_frac_phase_comp[p, j], sf + ) + elif j == "H2O": + iscale.set_scaling_factor( + self.mole_frac_phase_comp[p, j], 1e0 + ) + + # ######################################################## + + # Scaling for mass concentrations + if self.is_property_constructed("conc_mass_phase_comp"): + for j in self.params.component_list: + sf_dens = iscale.get_scaling_factor(self.dens_mass_phase["Liq"]) + if ( + iscale.get_scaling_factor(self.conc_mass_phase_comp["Liq", j]) + is None + ): + if j == "H2O": + iscale.set_scaling_factor( + self.conc_mass_phase_comp["Liq", j], sf_dens + ) + elif j == "NaCl": + iscale.set_scaling_factor( + self.conc_mass_phase_comp["Liq", j], + sf_dens + * iscale.get_scaling_factor( + self.mass_frac_phase_comp["Liq", j] + ), + ) + + # Transforming constraints + # property relationships with no index, simple constraint + v_str_lst_simple = [ + "pressure_sat", + "dh_vap_mass_solvent", + "temperature_sat_solvent", + ] + for v_str in v_str_lst_simple: + if self.is_property_constructed(v_str): + v = getattr(self, v_str) + sf = iscale.get_scaling_factor(v, default=1, warning=True) + c = getattr(self, "eq_" + v_str) + iscale.constraint_scaling_transform(c, sf) + + # Property relationships with phase index, but simple constraint + v_str_lst_phase = ["dens_mass_phase", "enth_mass_phase", "cp_mass_phase"] + for v_str in v_str_lst_phase: + if self.is_property_constructed(v_str): + v = getattr(self, v_str) + sf = iscale.get_scaling_factor(v["Liq"], default=1, warning=True) + c = getattr(self, "eq_" + v_str) + iscale.constraint_scaling_transform(c, sf) + + # Property relationship indexed by component + v_str_lst_comp = [ + "solubility_mass_phase_comp", + "solubility_mass_frac_phase_comp", + "conc_mass_phase_comp", + ] + for v_str in v_str_lst_comp: + if self.is_property_constructed(v_str): + v_comp = getattr(self, v_str) + c_comp = getattr(self, "eq_" + v_str) + for j, c in c_comp.items(): + sf = iscale.get_scaling_factor( + v_comp["Liq", j], default=1, warning=True + ) + iscale.constraint_scaling_transform(c, sf) + + # Property relationship indexed by single component + if self.is_property_constructed("dh_crystallization_mass_comp"): + sf = iscale.get_scaling_factor(self.dh_crystallization_mass_comp["NaCl"]) + iscale.constraint_scaling_transform( + self.eq_dh_crystallization_mass_comp, sf + ) + + # Property relationships with phase index and indexed constraints + v_str_lst_phase = [ + "dens_mass_solvent", + "dens_mass_solute", + "flow_vol_phase", + "enth_mass_solvent", + "enth_mass_solute", + "cp_mass_solvent", + "cp_mass_solute", + ] + for v_str in v_str_lst_phase: + if self.is_property_constructed(v_str): + v = getattr(self, v_str) + c_phase = getattr(self, "eq_" + v_str) + for ind, c in c_phase.items(): + sf = iscale.get_scaling_factor(v[ind], default=1, warning=True) + iscale.constraint_scaling_transform(c, sf) + + # Property relationships indexed by component and phase + v_str_lst_phase_comp = [ + "mass_frac_phase_comp", + "flow_mol_phase_comp", + "mole_frac_phase_comp", + ] + for v_str in v_str_lst_phase_comp: + if self.is_property_constructed(v_str): + v_comp = getattr(self, v_str) + c_comp = getattr(self, "eq_" + v_str) + for j, c in c_comp.items(): + sf = iscale.get_scaling_factor(v_comp[j], default=1, warning=True) + iscale.constraint_scaling_transform(c, sf) diff --git a/src/watertap_contrib/reflo/property_models/tests/test_cryst_prop_pack.py b/src/watertap_contrib/reflo/property_models/tests/test_cryst_prop_pack.py new file mode 100644 index 00000000..0de9daa2 --- /dev/null +++ b/src/watertap_contrib/reflo/property_models/tests/test_cryst_prop_pack.py @@ -0,0 +1,806 @@ +################################################################################# +# WaterTAP Copyright (c) 2020-2024, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# National Renewable Energy Laboratory, and National Energy Technology +# Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +################################################################################# +import pytest +import watertap.property_models.unit_specific.cryst_prop_pack as props +from pyomo.environ import ConcreteModel +from idaes.core import FlowsheetBlock, ControlVolume0DBlock +from idaes.models.properties.tests.test_harness import ( + PropertyTestHarness as PropertyTestHarness_idaes, +) +from watertap.property_models.tests.property_test_harness import ( + PropertyTestHarness, + PropertyRegressionTest, + PropertyCalculateStateTest, +) + + +# ----------------------------------------------------------------------------- + + +class TestNaClProperty_idaes(PropertyTestHarness_idaes): + def configure(self): + self.prop_pack = props.NaClParameterBlock + self.param_args = {} + self.prop_args = {} + self.has_density_terms = False + + +class TestDefaultNaClwaterProperty: + + # Create block and stream for running default tests + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + m.fs.properties = props.NaClParameterBlock( + heat_of_crystallization_model=props.HeatOfCrystallizationModel.constant + ) + m.fs.stream = m.fs.properties.build_state_block([0], defined_state=True) + + m.fs.cv = ControlVolume0DBlock( + dynamic=False, has_holdup=False, property_package=m.fs.properties + ) + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_material_balances() + m.fs.cv.add_energy_balances() + m.fs.cv.add_momentum_balances() + + # Create instance of PropertyTesthARNESS class and add attributes needed for tests + xv = PropertyTestHarness() + + xv.stateblock_statistics = { + "number_variables": 43, + "number_total_constraints": 37, + "number_unused_variables": 0, + "default_degrees_of_freedom": 6, + } + + xv.scaling_args = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 1e0, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e1, + ("flow_mol_phase_comp", ("Sol", "NaCl")): 1e2, + ("flow_mol_phase_comp", ("Vap", "H2O")): 1e3, + } + + xv.default_solution = { + ("solubility_mass_phase_comp", ("Liq", "NaCl")): 359.51, + ("solubility_mass_frac_phase_comp", ("Liq", "NaCl")): 0.265, + ("dens_mass_solvent", "Liq"): 996.89, + ("dens_mass_solvent", "Vap"): 0.7363, + ("dens_mass_solute", "Liq"): 3199.471, + ("dens_mass_solute", "Sol"): 2115, + ("dens_mass_phase", "Liq"): 1021.50, + ("dh_vap_mass_solvent", None): 2441.80, + ("cp_mass_solvent", "Liq"): 4186.52, + ("cp_mass_solvent", "Vap"): 1864.52, + ("cp_mass_solute", "Sol"): 864.15, # cp_mass_solute liquid ignored for now + ("cp_mass_phase", "Liq"): 4008.30, + ("flow_vol_phase", "Liq"): 9.79e-4, + ("flow_vol_phase", "Sol"): 0, + ("flow_vol_phase", "Vap"): 0, + ("pressure_sat", None): 2932.43, + ("temperature_sat_solvent", None): 296.79, + ("conc_mass_phase_comp", ("Liq", "NaCl")): 35.753, + ("conc_mass_phase_comp", ("Liq", "H2O")): 985.753, + ("enth_mass_solvent", "Liq"): 104.9212, + ("enth_mass_solvent", "Vap"): 2546.7289, + ("enth_mass_solute", "Sol"): 21.4739, + ("enth_mass_phase", "Liq"): 101.0922, + ("dh_crystallization_mass_comp", "NaCl"): -520, + ("mass_frac_phase_comp", ("Liq", "H2O")): 0.965, + ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.035, + ("mass_frac_phase_comp", ("Sol", "NaCl")): 1.0, + ("mass_frac_phase_comp", ("Vap", "H2O")): 1.0, + ("flow_mol_phase_comp", ("Liq", "H2O")): 53.57, + ("flow_mol_phase_comp", ("Liq", "NaCl")): 0.5989, + ("flow_mol_phase_comp", ("Sol", "NaCl")): 0.0, + ("flow_mol_phase_comp", ("Vap", "H2O")): 0.0, + ("mole_frac_phase_comp", ("Liq", "H2O")): 0.9889, + ("mole_frac_phase_comp", ("Liq", "NaCl")): 0.01106, + ("mole_frac_phase_comp", ("Sol", "NaCl")): 1.0, + ("mole_frac_phase_comp", ("Vap", "H2O")): 1.0, + } + + # Configure class + xv.configure_class(m) + + @pytest.mark.unit + def test_components_phases(self): + self.xv.test_components_phases(self.m) + + @pytest.mark.unit + def test_parameters(self): + self.xv.test_parameters(self.m) + + @pytest.mark.unit + def test_state_variables(self): + self.xv.test_state_variables(self.m) + + @pytest.mark.unit + def test_permanent_properties(self): + self.xv.test_permanent_properties(self.m) + + @pytest.mark.unit + def test_on_demand_properties(self): + self.xv.test_on_demand_properties(self.m) + + @pytest.mark.unit + def test_stateblock_statistics(self): + self.xv.test_stateblock_statistics(self.m) + + @pytest.mark.unit + def test_units_consistent(self): + self.xv.test_units_consistent(self.m) + + @pytest.mark.unit + def test_scaling(self): + self.xv.test_scaling(self.m) + + @pytest.mark.component + def test_default_initialization(self): + self.xv.test_default_initialization(self.m) + + @pytest.mark.component + def test_property_control_volume(self): + self.xv.test_property_control_volume(self.m) + + +@pytest.mark.component +class TestNaClPropertySolution_1(PropertyRegressionTest): + # Test pure liquid solution 1 - same solution as NaCl prop pack + def configure(self): + self.prop_pack = props.NaClParameterBlock + self.param_args = {} + + self.scaling_args = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 1e0, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e2, + ("flow_mass_phase_comp", ("Vap", "H2O")): 1e2, + } + + self.state_args = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 0.95, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 0.05, + ("flow_mass_phase_comp", ("Sol", "NaCl")): 0.0, + ("flow_mass_phase_comp", ("Vap", "H2O")): 0.0, + ("temperature", None): 273.15 + 25, + ("pressure", None): 50e5, + } + + self.regression_solution = { + ("mass_frac_phase_comp", ("Liq", "H2O")): 0.95, + ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.05, + ("solubility_mass_phase_comp", ("Liq", "NaCl")): 359.50, + ("solubility_mass_frac_phase_comp", ("Liq", "NaCl")): 0.265, + ("dens_mass_solvent", "Liq"): 996.89, + ("dens_mass_solvent", "Vap"): 36.335, + ("dens_mass_phase", "Liq"): 1032.2, + ("flow_vol_phase", "Liq"): 9.687e-4, + ("conc_mass_phase_comp", ("Liq", "H2O")): 980.6, + ("conc_mass_phase_comp", ("Liq", "NaCl")): 51.61, + ("flow_mol_phase_comp", ("Liq", "H2O")): 52.73, + ("flow_mol_phase_comp", ("Liq", "NaCl")): 0.8556, + ("mole_frac_phase_comp", ("Liq", "H2O")): 0.9840, + ("mole_frac_phase_comp", ("Liq", "NaCl")): 1.597e-2, + ("cp_mass_solvent", "Liq"): 4186.52, + ("cp_mass_phase", "Liq"): 3940.03, + ("enth_mass_solvent", "Liq"): 104.92, + ("enth_mass_phase", "Liq"): 99.45, + } + + +@pytest.mark.component +class TestNaClPropertySolution_2(PropertyRegressionTest): + # Test pure liquid solution 2 - same solution as NaCl prop pack + def configure(self): + self.prop_pack = props.NaClParameterBlock + self.param_args = {} + + self.scaling_args = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 1e0, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e2, + ("flow_mass_phase_comp", ("Vap", "H2O")): 1e2, + } + + self.state_args = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 0.74, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 0.26, + ("flow_mass_phase_comp", ("Sol", "NaCl")): 0.0, + ("flow_mass_phase_comp", ("Vap", "H2O")): 0.0, + ("temperature", None): 273.15 + 25, + ("pressure", None): 50e5, + } + + self.regression_solution = { + ("mass_frac_phase_comp", ("Liq", "H2O")): 0.74, + ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.26, + ("solubility_mass_phase_comp", ("Liq", "NaCl")): 359.50, + ("solubility_mass_frac_phase_comp", ("Liq", "NaCl")): 0.265, + ("dens_mass_solvent", "Liq"): 996.89, + ("dens_mass_phase", "Liq"): 1193.4, + ("flow_vol_phase", "Liq"): 8.379e-4, + ("conc_mass_phase_comp", ("Liq", "H2O")): 883.14, + ("conc_mass_phase_comp", ("Liq", "NaCl")): 310.29, + ("flow_mol_phase_comp", ("Liq", "H2O")): 41.08, + ("flow_mol_phase_comp", ("Liq", "NaCl")): 4.449, + ("mole_frac_phase_comp", ("Liq", "H2O")): 0.9022, + ("mole_frac_phase_comp", ("Liq", "NaCl")): 9.773e-2, + ("cp_mass_solvent", "Liq"): 4186.52, + ("cp_mass_phase", "Liq"): 3276.56, + ("enth_mass_solvent", "Liq"): 104.92, + ("enth_mass_phase", "Liq"): 68.78, + } + + +@pytest.mark.component +class TestNaClPropertySolution_3(PropertyRegressionTest): + # Test pure liquid solution 3 - same solution as NaCl prop pack + def configure(self): + self.prop_pack = props.NaClParameterBlock + self.param_args = {} + + self.scaling_args = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 1e0, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e3, + ("flow_mass_phase_comp", ("Vap", "H2O")): 1e3, + } + + self.state_args = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 0.999, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 0.001, + ("flow_mass_phase_comp", ("Sol", "NaCl")): 0.0, + ("flow_mass_phase_comp", ("Vap", "H2O")): 0.0, + ("temperature", None): 273.15 + 25, + ("pressure", None): 10e5, + } + + self.regression_solution = { + ("mass_frac_phase_comp", ("Liq", "H2O")): 0.999, + ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.001, + ("solubility_mass_phase_comp", ("Liq", "NaCl")): 359.50, + ("solubility_mass_frac_phase_comp", ("Liq", "NaCl")): 0.265, + ("dens_mass_solvent", "Liq"): 996.89, + ("dens_mass_solvent", "Vap"): 7.267, + ("dens_mass_phase", "Liq"): 997.59, + ("flow_vol_phase", "Liq"): 1.002e-3, + ("conc_mass_phase_comp", ("Liq", "H2O")): 996.59, + ("conc_mass_phase_comp", ("Liq", "NaCl")): 0.9976, + ("flow_mol_phase_comp", ("Liq", "H2O")): 55.45, + ("flow_mol_phase_comp", ("Liq", "NaCl")): 1.711e-2, + ("mole_frac_phase_comp", ("Liq", "H2O")): 0.9997, + ("mole_frac_phase_comp", ("Liq", "NaCl")): 3.084e-4, + ("cp_mass_solvent", "Liq"): 4186.52, + ("cp_mass_phase", "Liq"): 4180.98, + ("enth_mass_solvent", "Liq"): 104.92, + ("enth_mass_phase", "Liq"): 104.40, + } + + +@pytest.mark.requires_idaes_solver +@pytest.mark.component +class TestNaClPropertySolution_4(PropertyRegressionTest): + # Test pure solid solution 1 - check solid properties + def configure(self): + + self.prop_pack = props.NaClParameterBlock + self.param_args = {} + + self.scaling_args = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 1e1, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e3, + ("flow_mass_phase_comp", ("Sol", "NaCl")): 1e0, + ("flow_mass_phase_comp", ("Vap", "H2O")): 1e2, + } + + self.state_args = { + ("flow_vol_phase", "Liq"): 0, + ("flow_vol_phase", "Vap"): 0, + ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.05, + ("flow_mass_phase_comp", ("Sol", "NaCl")): 1.0, + ("temperature", None): 273.15 + 25, + ("pressure", None): 5e5, + } + + self.regression_solution = { + ("mass_frac_phase_comp", ("Sol", "NaCl")): 1.0, + ("dens_mass_solute", "Sol"): 2115, + ("cp_mass_solute", "Sol"): 864.16, + ("flow_vol_phase", "Sol"): 1 / 2115, # mass floe / density + ("cp_mass_solute", "Sol"): 864.16, + ("enth_mass_solute", "Sol"): 21.474, + ("dh_crystallization_mass_comp", "NaCl"): -520, + ("flow_mol_phase_comp", ("Sol", "NaCl")): 1 / 58.44e-3, # mass flow / mw + ("mole_frac_phase_comp", ("Sol", "NaCl")): 1.0, + } + + +@pytest.mark.requires_idaes_solver +@pytest.mark.component +class TestNaClPropertySolution_5(PropertyRegressionTest): + # Test pure vapor solution 1 - check vapor properties + def configure(self): + + self.prop_pack = props.NaClParameterBlock + self.param_args = {} + + self.scaling_args = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 1e1, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e3, + ("flow_mass_phase_comp", ("Sol", "NaCl")): 1e3, + ("flow_mass_phase_comp", ("Vap", "H2O")): 1e0, + } + + self.state_args = { + ("flow_vol_phase", "Liq"): 0, + ("flow_vol_phase", "Sol"): 0, + ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.05, + ("flow_mass_phase_comp", ("Vap", "H2O")): 1.0, + ("temperature", None): 273.15 + 25, + ("pressure", None): 5e5, + } + + self.regression_solution = { + ("mass_frac_phase_comp", ("Vap", "H2O")): 1.0, + ("dens_mass_solvent", "Vap"): 3.633, + ("dh_vap_mass_solvent", None): 2441.808, + ("cp_mass_solvent", "Vap"): 1864.52, + ("flow_vol_phase", "Vap"): 1 / 3.633, # mass flow / density + ("pressure_sat", None): 2905.28, + ("flow_mol_phase_comp", ("Vap", "H2O")): 1 / 18.01528e-3, # mass flow / mw + ("mole_frac_phase_comp", ("Vap", "H2O")): 1.0, + } + + +@pytest.mark.component +class TestNaClPropertySolution_6(PropertyRegressionTest): + # Test for S-L-V solution 1 with similar magnitude flowrates in all phases and high liquid salt. conc. - check all properties + def configure(self): + self.prop_pack = props.NaClParameterBlock + self.param_args = {} + + self.scaling_args = { + ("flow_mass_phase_comp", ("Sol", "NaCl")): 10, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e1, + } + + self.state_args = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 0.75, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 0.25, + ("flow_mass_phase_comp", ("Sol", "NaCl")): 0.25, + ("flow_mass_phase_comp", ("Vap", "H2O")): 0.25, + ("temperature", None): 273.15 + 50, + ("pressure", None): 5e5, + } + + self.regression_solution = { + ("solubility_mass_phase_comp", ("Liq", "NaCl")): 362.93, + ("solubility_mass_frac_phase_comp", ("Liq", "NaCl")): 0.2686, + ("dens_mass_solvent", "Liq"): 988.04, + ("dens_mass_solvent", "Vap"): 3.352, + ("dens_mass_solute", "Liq"): 2645.21, + ("dens_mass_solute", "Sol"): 2115, + ("dens_mass_phase", "Liq"): 1171.53, + ("dh_vap_mass_solvent", None): 2382.08, + ("cp_mass_solvent", "Liq"): 4180.92, + ("cp_mass_solvent", "Vap"): 1871.21, + ("cp_mass_solute", "Sol"): 873.34, + ("cp_mass_phase", "Liq"): 3300.83, + ("flow_vol_phase", "Liq"): 8.53e-4, + ("flow_vol_phase", "Sol"): 1.182e-4, + ("flow_vol_phase", "Vap"): 7.46e-2, + ("pressure_sat", None): 9799.91, + ("temperature_sat_solvent", None): 318.52, + ("conc_mass_phase_comp", ("Liq", "NaCl")): 292.88, + ("conc_mass_phase_comp", ("Liq", "H2O")): 878.65, + ("enth_mass_solvent", "Liq"): 209.40, + ("enth_mass_solvent", "Vap"): 2591.48, + ("enth_mass_solute", "Sol"): 43.19, + ("enth_mass_phase", "Liq"): 152.36, + ("dh_crystallization_mass_comp", "NaCl"): -520, + ("mass_frac_phase_comp", ("Liq", "H2O")): 0.75, + ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.25, + ("mass_frac_phase_comp", ("Sol", "NaCl")): 1, + ("mass_frac_phase_comp", ("Vap", "H2O")): 1, + ("flow_mol_phase_comp", ("Liq", "H2O")): 41.63, + ("flow_mol_phase_comp", ("Liq", "NaCl")): 4.28, + ("flow_mol_phase_comp", ("Sol", "NaCl")): 4.28, + ("flow_mol_phase_comp", ("Vap", "H2O")): 13.88, + ("mole_frac_phase_comp", ("Liq", "H2O")): 0.9068, + ("mole_frac_phase_comp", ("Liq", "NaCl")): 0.0932, + ("mole_frac_phase_comp", ("Sol", "NaCl")): 1.0, + ("mole_frac_phase_comp", ("Vap", "H2O")): 1.0, + } + + +@pytest.mark.component +class TestNaClPropertySolution_7(PropertyRegressionTest): + # Test for S-L-V solution 2 with flowrates in all phases of same magnitude but low liquid salt. conc. - check all properties + def configure(self): + self.prop_pack = props.NaClParameterBlock + self.param_args = {} + + self.scaling_args = { + ("flow_mass_phase_comp", ("Sol", "NaCl")): 10, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e3, + } + + self.state_args = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 0.999, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 0.001, + ("flow_mass_phase_comp", ("Sol", "NaCl")): 0.25, + ("flow_mass_phase_comp", ("Vap", "H2O")): 0.25, + ("temperature", None): 273.15 + 50, + ("pressure", None): 5e5, + } + + self.regression_solution = { + ("solubility_mass_phase_comp", ("Liq", "NaCl")): 362.93, + ("solubility_mass_frac_phase_comp", ("Liq", "NaCl")): 0.2686, + ("dens_mass_solvent", "Liq"): 988.04, + ("dens_mass_solvent", "Vap"): 3.352, + ("dens_mass_solute", "Liq"): 3073.05, + ("dens_mass_solute", "Sol"): 2115, + ("dens_mass_phase", "Liq"): 988.72, + ("dh_vap_mass_solvent", None): 2382.08, + ("cp_mass_solvent", "Liq"): 4180.92, + ("cp_mass_solvent", "Vap"): 1871.21, + ("cp_mass_solute", "Sol"): 873.34, + ("cp_mass_phase", "Liq"): 4175.95, + ("flow_vol_phase", "Liq"): 1.011e-3, + ("flow_vol_phase", "Sol"): 1.182e-4, + ("flow_vol_phase", "Vap"): 7.46e-2, + ("pressure_sat", None): 12614.93, + ("temperature_sat_solvent", None): 323.55, + ("conc_mass_phase_comp", ("Liq", "NaCl")): 0.9887, + ("conc_mass_phase_comp", ("Liq", "H2O")): 987.73, + ("enth_mass_solvent", "Liq"): 209.40, + ("enth_mass_solvent", "Vap"): 2591.48, + ("enth_mass_solute", "Sol"): 43.19, + ("enth_mass_phase", "Liq"): 208.81, + ("dh_crystallization_mass_comp", "NaCl"): -520, + ("mass_frac_phase_comp", ("Liq", "H2O")): 0.999, + ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.001, + ("mass_frac_phase_comp", ("Sol", "NaCl")): 1, + ("mass_frac_phase_comp", ("Vap", "H2O")): 1, + ("flow_mol_phase_comp", ("Liq", "H2O")): 55.45, + ("flow_mol_phase_comp", ("Liq", "NaCl")): 0.0171, + ("flow_mol_phase_comp", ("Sol", "NaCl")): 4.28, + ("flow_mol_phase_comp", ("Vap", "H2O")): 13.88, + ("mole_frac_phase_comp", ("Liq", "H2O")): 0.999, + ("mole_frac_phase_comp", ("Liq", "NaCl")): 3.084e-4, + ("mole_frac_phase_comp", ("Sol", "NaCl")): 1.0, + ("mole_frac_phase_comp", ("Vap", "H2O")): 1.0, + } + + +@pytest.mark.component +class TestNaClPropertySolution_8(PropertyRegressionTest): + # Test for S-L-V solution 3 with outlet data from Dutta et al. - proper crystallization system + # # Dutta recorded properties for solids and liquids at crystallizer temperature: + # # Solubility @ 55C: 0.27 kg/kg + # # Heat of vaporization @ 55C: 2400 kJ/kg + # # Vapor density @ 55C: 68.7e-3 kg/m3 + # # Solid heat capacity: 877 J/kgK + # # Liquid heat capacity @ 20 C: 3290 kJ/kgK + # # Liquid density @ 20 C : 1185 kg/m3 + + def configure(self): + self.prop_pack = props.NaClParameterBlock + self.param_args = {} + + self.scaling_args = { + ("flow_mass_phase_comp", ("Sol", "NaCl")): 1e0, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e0, + ("flow_mass_phase_comp", ("Vap", "H2O")): 1e-1, + } + + self.state_args = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 18.37, # 84t/h + ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.2126, + ("flow_mass_phase_comp", ("Sol", "NaCl")): 5.55, # 20t/h + ("flow_mass_phase_comp", ("Vap", "H2O")): 20.55, # 74t/h + ("temperature", None): 273.15 + 20, + ("pressure", None): 10e3, + } + + self.regression_solution = { + ("solubility_mass_phase_comp", ("Liq", "NaCl")): 358.88, + ("solubility_mass_frac_phase_comp", ("Liq", "NaCl")): 0.2644, + ("dens_mass_solvent", "Liq"): 998.02, + ("dens_mass_solvent", "Vap"): 73.91e-3, + ("dens_mass_solute", "Liq"): 2847.63, + ("dens_mass_solute", "Sol"): 2115, + ("dens_mass_phase", "Liq"): 1157.91, + ("dh_vap_mass_solvent", None): 2453.66, + ("cp_mass_solvent", "Liq"): 4189.43, + ("cp_mass_solvent", "Vap"): 1863.46, + ("cp_mass_solute", "Sol"): 862.16, + ("cp_mass_phase", "Liq"): 3380.07, + ("flow_vol_phase", "Liq"): 0.02015, + ("flow_vol_phase", "Sol"): 2.624e-3, + ("flow_vol_phase", "Vap"): 278.04, + ("pressure_sat", None): 1679.64, + ("temperature_sat_solvent", None): 287.88, + ("conc_mass_phase_comp", ("Liq", "NaCl")): 246.17, + ("conc_mass_phase_comp", ("Liq", "H2O")): 911.74, + ("enth_mass_solvent", "Liq"): 84.00, + ("enth_mass_solvent", "Vap"): 2537.66, + ("enth_mass_solute", "Sol"): 17.16, + ("enth_mass_phase", "Liq"): 59.03, + ("dh_crystallization_mass_comp", "NaCl"): -520, + ("mass_frac_phase_comp", ("Liq", "H2O")): 0.7874, + ("mass_frac_phase_comp", ("Sol", "NaCl")): 1, + ("mass_frac_phase_comp", ("Vap", "H2O")): 1, + ("flow_mol_phase_comp", ("Liq", "H2O")): 1019.69, + ("flow_mol_phase_comp", ("Liq", "NaCl")): 84.88, + ("flow_mol_phase_comp", ("Sol", "NaCl")): 94.97, + ("flow_mol_phase_comp", ("Vap", "H2O")): 1140.698, + ("mole_frac_phase_comp", ("Liq", "H2O")): 0.9232, + ("mole_frac_phase_comp", ("Liq", "NaCl")): 0.0768, + ("mole_frac_phase_comp", ("Sol", "NaCl")): 1.0, + ("mole_frac_phase_comp", ("Vap", "H2O")): 1.0, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 4.96, + } + + +@pytest.mark.component +class TestNaClPropertySolution_9(PropertyRegressionTest): + # Test for S-L-V solution 4 with outlet data from Dutta et al. - proper crystallization system + # # Dutta recorded properties for solids and liquids at crystallizer temperature: + # # Solubility @ 55C: 0.27 kg/kg + # # Heat of vaporization @ 55C: 2400 kJ/kg + # # Vapor density @ 55C: 68.7e-3 kg/m3 + # # Solid heat capacity: 877 J/kgK + # # Liquid heat capacity @ 20 C: 3290 kJ/kgK + # # Liquid density @ 20 C : 1185 kg/m3 + + def configure(self): + self.prop_pack = props.NaClParameterBlock + self.param_args = {} + + self.scaling_args = { + ("flow_mass_phase_comp", ("Sol", "NaCl")): 1e0, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e0, + ("flow_mass_phase_comp", ("Vap", "H2O")): 1e-1, + } + + self.state_args = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 18.37, # 84t/h + ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.2126, + ("flow_mass_phase_comp", ("Sol", "NaCl")): 5.55, # 20t/h + ("flow_mass_phase_comp", ("Vap", "H2O")): 20.55, # 74t/h + ("temperature", None): 273.15 + 55, + ("pressure", None): 10e3, + } + + self.regression_solution = { + ("solubility_mass_phase_comp", ("Liq", "NaCl")): 363.71, + ("solubility_mass_frac_phase_comp", ("Liq", "NaCl")): 0.2695, + ("dens_mass_solvent", "Liq"): 985.71, + ("dens_mass_solvent", "Vap"): 66.03e-3, + ("dens_mass_solute", "Liq"): 2694.61, + ("dens_mass_solute", "Sol"): 2115, + ("dens_mass_phase", "Liq"): 1139.33, + ("dh_vap_mass_solvent", None): 2369.98, + ("cp_mass_solvent", "Liq"): 4181.59, + ("cp_mass_solvent", "Vap"): 1872.79, + ("cp_mass_solute", "Sol"): 875.04, + ("cp_mass_phase", "Liq"): 3390.43, + ("flow_vol_phase", "Liq"): 0.02047, + ("flow_vol_phase", "Sol"): 2.624e-3, + ("flow_vol_phase", "Vap"): 311.233, + ("pressure_sat", None): 13201.77, + ("temperature_sat_solvent", None): 324.47, + ("conc_mass_phase_comp", ("Liq", "NaCl")): 242.22, + ("conc_mass_phase_comp", ("Liq", "H2O")): 897.107, + ("enth_mass_solvent", "Liq"): 230.30, + ("enth_mass_solvent", "Vap"): 2600.279, + ("enth_mass_solute", "Sol"): 47.57, + ("enth_mass_phase", "Liq"): 177.39, + ("dh_crystallization_mass_comp", "NaCl"): -520, + ("mass_frac_phase_comp", ("Liq", "H2O")): 0.7874, + ("mass_frac_phase_comp", ("Sol", "NaCl")): 1, + ("mass_frac_phase_comp", ("Vap", "H2O")): 1, + ("flow_mol_phase_comp", ("Liq", "H2O")): 1019.69, + ("flow_mol_phase_comp", ("Liq", "NaCl")): 84.88, + ("flow_mol_phase_comp", ("Sol", "NaCl")): 94.97, + ("flow_mol_phase_comp", ("Vap", "H2O")): 1140.698, + ("mole_frac_phase_comp", ("Liq", "H2O")): 0.9232, + ("mole_frac_phase_comp", ("Liq", "NaCl")): 0.0768, + ("mole_frac_phase_comp", ("Sol", "NaCl")): 1.0, + ("mole_frac_phase_comp", ("Vap", "H2O")): 1.0, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 4.96, + } + + +@pytest.mark.component +class TestNaClCalculateState_1(PropertyCalculateStateTest): + # Test pure liquid solution with mass fractions + def configure(self): + self.prop_pack = props.NaClParameterBlock + self.param_args = {} + + self.scaling_args = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 1e-1, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e0, + ("flow_mass_phase_comp", ("Vap", "H2O")): 1, + ("flow_mass_phase_comp", ("Sol", "NaCl")): 1, + } + + self.var_args = { + ("flow_vol_phase", "Liq"): 2e-2, + ("flow_vol_phase", "Sol"): 0, + ("flow_vol_phase", "Vap"): 0, + ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.05, + ("temperature", None): 273.15 + 25, + ("pressure", None): 5e5, + } + + self.state_solution = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 19.6, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 1.032, + ("flow_mass_phase_comp", ("Sol", "NaCl")): 0, + ("flow_mass_phase_comp", ("Vap", "H2O")): 0, + } + + +@pytest.mark.component +class TestNaClCalculateState_2(PropertyCalculateStateTest): + # Test pure liquid with mole fractions + def configure(self): + self.prop_pack = props.NaClParameterBlock + self.param_args = {} + + self.scaling_args = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 1e1, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e2, + # The rest are expected to be zero + ("flow_mass_phase_comp", ("Sol", "NaCl")): 1, + ("flow_mass_phase_comp", ("Vap", "H2O")): 1, + } + self.var_args = { + ("flow_vol_phase", "Liq"): 2e-4, + ("flow_vol_phase", "Sol"): 0, + ("flow_vol_phase", "Vap"): 0, + ("mole_frac_phase_comp", ("Liq", "NaCl")): 0.05, + ("temperature", None): 273.15 + 25, + ("pressure", None): 5e5, + } + self.state_solution = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 18.84e-2, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 3.215e-2, + ("flow_mass_phase_comp", ("Sol", "NaCl")): 0, + ("flow_mass_phase_comp", ("Vap", "H2O")): 0, + } + + +@pytest.mark.component +class TestNaClCalculateState_3(PropertyCalculateStateTest): + # Test pure liquid solution with pressure_sat defined instead of temperature + def configure(self): + self.prop_pack = props.NaClParameterBlock + self.param_args = {} + + self.scaling_args = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 1e0, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e1, + ("flow_mass_phase_comp", ("Sol", "NaCl")): 1e-1, + ("flow_mass_phase_comp", ("Vap", "H2O")): 1e2, + } + + self.var_args = { + ("flow_vol_phase", "Liq"): 2e-2, + ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.05, + ("flow_vol_phase", "Sol"): 0, + ("flow_vol_phase", "Vap"): 0, + ("pressure_sat", None): 2905, + ("pressure", None): 5e5, + } + + self.state_solution = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 19.6, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 1.032, + ("flow_mass_phase_comp", ("Sol", "NaCl")): 0, + ("flow_mass_phase_comp", ("Vap", "H2O")): 0, + ("temperature", None): 273.15 + 25, + } + + +@pytest.mark.component +class TestNaClCalculateState_4(PropertyCalculateStateTest): + # Test pure solid solution with mass fractions + def configure(self): + + self.prop_pack = props.NaClParameterBlock + self.param_args = {} + + self.scaling_args = { + ("flow_mass_phase_comp", ("Sol", "NaCl")): 1e-1, + ("flow_mass_phase_comp", ("Vap", "H2O")): 1e-1, + } + + self.var_args = { + ("flow_vol_phase", "Liq"): 0, + ("mass_frac_phase_comp", ("Liq", "NaCl")): 0, + ("flow_vol_phase", "Sol"): 2e-2, + ("flow_vol_phase", "Vap"): 0, + ("temperature", None): 273.15 + 25, + ("pressure", None): 5e5, + } + + self.state_solution = { + ("flow_mass_phase_comp", ("Sol", "NaCl")): 2115 + * 2e-2, # solid density is constant + ("flow_mass_phase_comp", ("Liq", "H2O")): 0, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 0, + ("flow_mass_phase_comp", ("Vap", "H2O")): 0, + } + + +@pytest.mark.component +class TestNaClCalculateState_5(PropertyCalculateStateTest): + # Test solid-liquid-vapor mixture solution with mass fractions + def configure(self): + self.prop_pack = props.NaClParameterBlock + self.param_args = {} + + self.scaling_args = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 1e-1, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e1, + ("flow_mass_phase_comp", ("Sol", "NaCl")): 1e-2, + } + + self.var_args = { + ("flow_vol_phase", "Liq"): 2e-2, + ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.05, + ("flow_vol_phase", "Sol"): 2e-2, + ("flow_vol_phase", "Vap"): 2e-2, + ("temperature", None): 273.15 + 25, + ("pressure", None): 5e5, + } + + self.state_solution = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 19.6, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 1.032, + ("flow_mass_phase_comp", ("Vap", "H2O")): 0.07265, + ("flow_mass_phase_comp", ("Sol", "NaCl")): 42.3, + } + + +@pytest.mark.component +class TestNaClCalculateState_6(PropertyCalculateStateTest): + # Test liquid-solid-vapor mixture with mole fractions + def configure(self): + self.prop_pack = props.NaClParameterBlock + self.param_args = {} + + self.scaling_args = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 1e1, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e2, + ("flow_mass_phase_comp", ("Sol", "NaCl")): 1e2, + ("flow_mass_phase_comp", ("Vap", "H2O")): 1e3, + } + + self.var_args = { + ("flow_vol_phase", "Liq"): 2e-4, + ("flow_vol_phase", "Sol"): 2e-4, + ("flow_vol_phase", "Vap"): 2e-4, + ("mole_frac_phase_comp", ("Liq", "NaCl")): 0.05, + ("temperature", None): 273.15 + 25, + ("pressure", None): 5e5, + } + + self.state_solution = { + ("flow_mass_phase_comp", ("Liq", "H2O")): 18.84e-2, + ("flow_mass_phase_comp", ("Liq", "NaCl")): 3.215e-2, + ("flow_mass_phase_comp", ("Sol", "NaCl")): 0.423, + ("flow_mass_phase_comp", ("Vap", "H2O")): 3.632 + * 2e-4, # Density from ideal gas law * vol. flow + } diff --git a/src/watertap_contrib/reflo/unit_models/zero_order/crystallizer_zo_watertap.py b/src/watertap_contrib/reflo/unit_models/zero_order/crystallizer_zo_watertap.py new file mode 100644 index 00000000..6025e8e5 --- /dev/null +++ b/src/watertap_contrib/reflo/unit_models/zero_order/crystallizer_zo_watertap.py @@ -0,0 +1,868 @@ +################################################################################# +# WaterTAP Copyright (c) 2020-2024, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# National Renewable Energy Laboratory, and National Energy Technology +# Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +################################################################################# + +from copy import deepcopy + +# Import Pyomo libraries +from pyomo.environ import ( + Var, + check_optimal_termination, + Param, + Constraint, + Suffix, + units as pyunits, +) +from pyomo.common.config import ConfigBlock, ConfigValue, In + +# Import IDAES cores +from idaes.core import ( + declare_process_block_class, + UnitModelBlockData, + useDefault, +) +from watertap.core.solvers import get_solver +from idaes.core.util.tables import create_stream_table_dataframe +from idaes.core.util.constants import Constants +from idaes.core.util.config import is_physical_parameter_block + +from idaes.core.util.exceptions import InitializationError + +import idaes.core.util.scaling as iscale +import idaes.logger as idaeslog + +from watertap.core import InitializationMixin +from watertap.core.util.initialization import interval_initializer +from watertap.costing.unit_models.crystallizer import cost_crystallizer + +_log = idaeslog.getLogger(__name__) + +__author__ = "Oluwamayowa Amusat" + + +# when using this file the name "Filtration" is what is imported +@declare_process_block_class("Crystallization") +class CrystallizationData(InitializationMixin, UnitModelBlockData): + """ + Zero order crystallization model + """ + + # CONFIG are options for the unit model, this simple model only has the mandatory config options + 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. The filtration unit does not support dynamic + behavior, thus this must be False.""", + ), + ) + + 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. The filtration unit does not have defined volume, thus + this must be False.""", + ), + ) + + CONFIG.declare( + "property_package", + ConfigValue( + default=useDefault, + domain=is_physical_parameter_block, + description="Property package to use for control volume", + doc="""Property parameter object used to define property calculations, + **default** - useDefault. + **Valid values:** { + **useDefault** - use default package from parent model or flowsheet, + **PhysicalParameterObject** - a PhysicalParameterBlock object.}""", + ), + ) + + CONFIG.declare( + "property_package_args", + ConfigBlock( + implicit=True, + description="Arguments to use for constructing property packages", + doc="""A ConfigBlock with arguments to be passed to a property block(s) + and used when constructing these, + **default** - None. + **Valid values:** { + see property package for documentation.}""", + ), + ) + + def build(self): + super().build() + + solvent_set = self.config.property_package.solvent_set + solute_set = self.config.property_package.solute_set + + # this creates blank scaling factors, which are populated later + self.scaling_factor = Suffix(direction=Suffix.EXPORT) + + # Next, get the base units of measurement from the property definition + units_meta = self.config.property_package.get_metadata().get_derived_units + + # Add unit variables + + self.approach_temperature_heat_exchanger = Param( + initialize=4, + units=pyunits.K, + doc="Maximum temperature difference between inlet and outlet of a crystallizer heat exchanger.\ + Lewis et al. suggests 1-2 degC but use 5degC in example; Tavare example used 4 degC.\ + Default is 4 degC", + ) + + # ====== Crystallizer sizing parameters ================= # + self.dimensionless_crystal_length = Param( + initialize=3.67, # Parameter from population balance modeling for median crystal size + units=pyunits.dimensionless, + ) + + self.crystal_median_length = Var( + initialize=0.5e-3, # From Mersmann et al., Tavare et al. example + bounds=( + 0.2e-3, + 0.6e-3, + ), # Limits for FC crystallizers based on Bermingham et al. + units=pyunits.m, + doc="Desired median crystal size, m", + ) + + self.crystal_growth_rate = Var( + initialize=3.7e-8, # From Mersmann et al. for NaCl. Perry has values between 0.5e-8 to 13e-8 for NaCl + bounds=(1e-9, 1e-6), # Based on Mersmann and Kind diagram. + units=pyunits.m / pyunits.s, + doc="Crystal growth rate, m/s", + ) + + self.souders_brown_constant = Var( + initialize=0.04, + units=pyunits.m / pyunits.s, + doc="Constant for Souders-Brown equation, set at 0.04 m/s based on Dutta et al. \ + Lewis et al suggests 0.024 m/s, while Tavare suggests about 0.06 m/s ", + ) + + # ====== Model variables ================= # + self.crystallization_yield = Var( + solute_set, + initialize=0.5, + bounds=(0.0, 1), + units=pyunits.dimensionless, + doc="Crystallizer solids yield", + ) + + self.product_volumetric_solids_fraction = Var( + initialize=0.25, + bounds=(0.0, 1), + units=pyunits.dimensionless, + doc="Volumetric fraction of solids in slurry product (i.e. solids-liquid mixture).", + ) + + self.temperature_operating = Var( + initialize=298.15, + bounds=(273, 1000), + units=pyunits.K, + doc="Crystallizer operating temperature: boiling point of the solution.", + ) + + self.pressure_operating = Var( + initialize=1e3, + bounds=(0.001, 1e6), + units=pyunits.Pa, + doc="Operating pressure of the crystallizer.", + ) + + self.dens_mass_magma = Var( + initialize=250, + bounds=(1, 5000), + units=pyunits.kg / pyunits.m**3, + doc="Magma density, i.e. mass of crystals per unit volume of suspension", + ) + + self.dens_mass_slurry = Var( + initialize=1000, + bounds=(1, 5000), + units=pyunits.kg / pyunits.m**3, + doc="Suspension density, i.e. density of solid-liquid mixture before separation", + ) + + self.work_mechanical = Var( + self.flowsheet().config.time, + initialize=1e5, + bounds=(-5e6, 5e6), + units=pyunits.kJ / pyunits.s, + doc="Crystallizer thermal energy requirement", + ) + + self.energy_flow_superheated_vapor = Var( + initialize=1e5, + bounds=(-5e6, 5e6), + units=pyunits.kJ / pyunits.s, + doc="Energy could be supplied from vapor", + ) + + self.diameter_crystallizer = Var( + initialize=3, + bounds=(0, 25), + units=pyunits.m, + doc="Diameter of crystallizer", + ) + + self.height_slurry = Var( + initialize=3, + bounds=(0, 25), + units=pyunits.m, + doc="Slurry height in crystallizer", + ) + + self.height_crystallizer = Var( + initialize=3, bounds=(0, 25), units=pyunits.m, doc="Crystallizer height" + ) + + self.magma_circulation_flow_vol = Var( + initialize=1, + bounds=(0, 100), + units=pyunits.m**3 / pyunits.s, + doc="Minimum circulation flow rate through crystallizer heat exchanger", + ) + + self.relative_supersaturation = Var( + solute_set, initialize=0.1, bounds=(0, 100), units=pyunits.dimensionless + ) + + self.t_res = Var( + initialize=1, + bounds=(0, 10), + units=pyunits.hr, + doc="Residence time in crystallizer", + ) + + self.volume_suspension = Var( + initialize=1, + bounds=(0, None), + units=pyunits.m**3, + doc="Crystallizer minimum active volume, i.e. volume of liquid-solid suspension", + ) + + # Add state blocks for inlet, outlet, and waste + # These include the state variables and any other properties on demand + # Add inlet block + tmp_dict = dict(**self.config.property_package_args) + tmp_dict["has_phase_equilibrium"] = False + tmp_dict["parameters"] = self.config.property_package + tmp_dict["defined_state"] = True # inlet block is an inlet + self.properties_in = self.config.property_package.state_block_class( + self.flowsheet().config.time, doc="Material properties of inlet", **tmp_dict + ) + + # Add outlet and waste block + tmp_dict["defined_state"] = False # outlet and waste block is not an inlet + self.properties_out = self.config.property_package.state_block_class( + self.flowsheet().config.time, + doc="Material properties of liquid outlet", + **tmp_dict, + ) + + self.properties_solids = self.config.property_package.state_block_class( + self.flowsheet().config.time, + doc="Material properties of solid crystals at outlet", + **tmp_dict, + ) + + self.properties_vapor = self.config.property_package.state_block_class( + self.flowsheet().config.time, + doc="Material properties of water vapour at outlet", + **tmp_dict, + ) + + self.properties_pure_water = self.config.property_package.state_block_class( + self.flowsheet().config.time, + doc="Material properties of pure water vapour at outlet", + **tmp_dict, + ) + + # Add ports - oftentimes users interact with these rather than the state blocks + self.add_port(name="inlet", block=self.properties_in) + self.add_port(name="outlet", block=self.properties_out) + self.add_port(name="solids", block=self.properties_solids) + self.add_port(name="vapor", block=self.properties_vapor) + + # Add constraints + # 1. Material balances + @self.Constraint( + self.config.property_package.component_list, + doc="Mass balance for components", + ) + def eq_mass_balance_constraints(b, j): + return sum( + b.properties_in[0].flow_mass_phase_comp[p, j] + for p in self.config.property_package.phase_list + if (p, j) in b.properties_in[0].phase_component_set + ) == sum( + b.properties_out[0].flow_mass_phase_comp[p, j] + for p in self.config.property_package.phase_list + if (p, j) in b.properties_out[0].phase_component_set + ) + sum( + b.properties_vapor[0].flow_mass_phase_comp[p, j] + for p in self.config.property_package.phase_list + if (p, j) in b.properties_vapor[0].phase_component_set + ) + sum( + b.properties_solids[0].flow_mass_phase_comp[p, j] + for p in self.config.property_package.phase_list + if (p, j) in b.properties_solids[0].phase_component_set + ) + + @self.Constraint() + def eq_pure_vapor_flow_rate(b): + return ( + b.properties_pure_water[0].flow_mass_phase_comp["Vap", "H2O"] + == b.properties_vapor[0].flow_mass_phase_comp["Vap", "H2O"] + ) + + self.properties_pure_water[0].flow_mass_phase_comp["Liq", "H2O"].fix(1e-8) + self.properties_pure_water[0].flow_mass_phase_comp["Liq", "NaCl"].fix(0) + self.properties_pure_water[0].flow_mass_phase_comp["Sol", "NaCl"].fix(0) + self.properties_pure_water[0].mass_frac_phase_comp["Liq", "NaCl"] + self.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] + self.properties_in[0].flow_vol_phase["Liq"] + + # 2. Constraint on outlet liquid composition based on solubility requirements + @self.Constraint( + self.config.property_package.component_list, + doc="Solubility vs mass fraction constraint", + ) + def eq_solubility_massfrac_equality_constraint(b, j): + if j in solute_set: + return ( + b.properties_out[0].mass_frac_phase_comp["Liq", j] + - b.properties_out[0].solubility_mass_frac_phase_comp["Liq", j] + == 0 + ) + else: + return Constraint.Skip + + # 3. Performance equations + # (a) based on yield + @self.Constraint( + self.config.property_package.component_list, + doc="Component salt yield equation", + ) + def eq_removal_balance(b, j): + if j in solvent_set: + return Constraint.Skip + else: + return ( + b.properties_in[0].flow_mass_phase_comp["Liq", j] + * b.crystallization_yield[j] + == b.properties_in[0].flow_mass_phase_comp["Liq", j] + - b.properties_out[0].flow_mass_phase_comp["Liq", j] + ) + + # (b) Volumetric fraction constraint + @self.Constraint(doc="Solid volumetric fraction in outlet: constraint, 1-E") + def eq_vol_fraction_solids(b): + return self.product_volumetric_solids_fraction == b.properties_solids[ + 0 + ].flow_vol / ( + b.properties_solids[0].flow_vol + b.properties_out[0].flow_vol + ) + + # (c) Magma density constraint + @self.Constraint(doc="Slurry magma density") + def eq_dens_magma(b): + return ( + self.dens_mass_magma + == b.properties_solids[0].dens_mass_solute["Sol"] + * self.product_volumetric_solids_fraction + ) + + # (d) Operating pressure constraint + @self.Constraint(doc="Operating pressure constraint") + def eq_operating_pressure_constraint(b): + return self.pressure_operating - b.properties_out[0].pressure_sat == 0 + + # (e) Relative supersaturation + @self.Constraint( + solute_set, + doc="Relative supersaturation created via evaporation, g/g (solution)", + ) + def eq_relative_supersaturation(b, j): + # mass_frac_after_evap = SOLIDS IN + LIQUID IN - VAPOUR OUT + mass_frac_after_evap = b.properties_in[0].flow_mass_phase_comp["Liq", j] / ( + sum( + b.properties_in[0].flow_mass_phase_comp["Liq", k] + for k in solute_set + ) + + b.properties_in[0].flow_mass_phase_comp["Liq", "H2O"] + - b.properties_vapor[0].flow_mass_phase_comp["Vap", "H2O"] + ) + # return (b.relative_supersaturation[j] * b.properties_out[0].solubility_mass_frac_phase_comp['Liq', j] == + # (mass_frac_after_evap - b.properties_out[0].solubility_mass_frac_phase_comp['Liq', j]) + # ) + return ( + b.relative_supersaturation[j] + == ( + mass_frac_after_evap + - b.properties_out[0].solubility_mass_frac_phase_comp["Liq", j] + ) + / b.properties_out[0].solubility_mass_frac_phase_comp["Liq", j] + ) + + # 4. Fix flows of empty solid, liquid and vapour streams + # (i) Fix solids: liquid and vapour flows must be zero + for p, j in self.properties_solids[0].phase_component_set: + if p != "Sol": + self.properties_solids[0].flow_mass_phase_comp[p, j].fix(1e-8) + + # (ii) Fix liquids: solid and vapour flows must be zero + for p, j in self.properties_out[0].phase_component_set: + if p != "Liq": + self.properties_out[0].flow_mass_phase_comp[p, j].fix(1e-8) + + # (iii) Fix vapor: solid and vapour flows must be zero. + for p, j in self.properties_vapor[0].phase_component_set: + if p != "Vap": + self.properties_vapor[0].flow_mass_phase_comp[p, j].fix(1e-8) + + # 5. Add an energy balance for the system + ## (iii) Enthalpy balance: based on Lewis et al. Enthalpy is exothermic, and the hC in property package is -ve + @self.Constraint(doc="Enthalpy balance over crystallization system") + def eq_enthalpy_balance(b): + return ( + b.properties_in[0].enth_flow + - b.properties_out[0].enth_flow + - b.properties_vapor[0].enth_flow + - b.properties_solids[0].enth_flow + + self.work_mechanical[0] + - sum( + b.properties_solids[0].flow_mass_phase_comp["Sol", j] + * b.properties_solids[0].dh_crystallization_mass_comp[j] + for j in solute_set + ) + == 0 + ) + + # 6. Pressure and temperature balances - what is the pressure of the outlet solid and vapour? + # TO-DO: Figure out actual liquid and solid pressures. + @self.Constraint() + def eq_p_con1(b): + return b.properties_in[0].pressure == b.properties_out[0].pressure + + @self.Constraint() + def eq_p_con2(b): + return b.properties_in[0].pressure == b.properties_solids[0].pressure + + @self.Constraint() + def eq_p_con3(b): + return b.properties_vapor[0].pressure == self.pressure_operating + + @self.Constraint() + def eq_T_con1(b): + return self.temperature_operating == b.properties_solids[0].temperature + + @self.Constraint() + def eq_T_con2(b): + return self.temperature_operating == b.properties_vapor[0].temperature + + @self.Constraint() + def eq_T_con3(b): + return self.temperature_operating == b.properties_out[0].temperature + + @self.Constraint() + def eq_p_con4(b): + return b.properties_pure_water[0].pressure == self.pressure_operating + + @self.Constraint() + def eq_p_con5(b): + return b.properties_pure_water[0].pressure_sat == self.pressure_operating + + # 7. Heat exchanger minimum circulation flow rate calculations - see Lewis et al. or Tavare et al. + @self.Constraint( + doc="Constraint on mimimum circulation rate through crystallizer heat exchanger" + ) + def eq_minimum_hex_circulation_rate_constraint(b): + dens_cp_avg = self.approach_temperature_heat_exchanger * ( + b.product_volumetric_solids_fraction + * b.properties_solids[0].dens_mass_solute["Sol"] + * b.properties_solids[0].cp_mass_solute["Sol"] + + (1 - b.product_volumetric_solids_fraction) + * b.properties_out[0].dens_mass_phase["Liq"] + * b.properties_out[0].cp_mass_phase["Liq"] + ) + return b.magma_circulation_flow_vol * dens_cp_avg == pyunits.convert( + b.work_mechanical[0], to_units=pyunits.J / pyunits.s + ) + + # 8. Suspension density + @self.Constraint(doc="Slurry density calculation") + def eq_dens_mass_slurry(b): + return ( + self.dens_mass_slurry + == b.product_volumetric_solids_fraction + * b.properties_solids[0].dens_mass_solute["Sol"] + + (1 - b.product_volumetric_solids_fraction) + * b.properties_out[0].dens_mass_phase["Liq"] + ) + + # 9. Residence time calculation + @self.Constraint(doc="Residence time") + def eq_residence_time(b): + return b.t_res == b.crystal_median_length / ( + b.dimensionless_crystal_length + * pyunits.convert( + b.crystal_growth_rate, to_units=pyunits.m / pyunits.hr + ) + ) + + # 10. Suspension volume calculation + @self.Constraint(doc="Suspension volume") + def eq_suspension_volume(b): + return b.volume_suspension == ( + b.properties_solids[0].flow_vol + b.properties_out[0].flow_vol + ) * pyunits.convert(b.t_res, to_units=pyunits.s) + + # 11. Minimum diameter of evaporation zone + @self.Expression(doc="maximum allowable vapour linear velocity in m/s") + def eq_max_allowable_velocity(b): + return ( + b.souders_brown_constant + * ( + b.properties_out[0].dens_mass_phase["Liq"] + / b.properties_vapor[0].dens_mass_solvent["Vap"] + ) + ** 0.5 + ) + + @self.Constraint( + doc="Crystallizer diameter (based on minimum diameter of evaporation zone)" + ) + def eq_vapor_head_diameter_constraint(b): + return ( + self.diameter_crystallizer + == ( + 4 + * b.properties_vapor[0].flow_vol_phase["Vap"] + / (Constants.pi * b.eq_max_allowable_velocity) + ) + ** 0.5 + ) + + # 12. Minimum crystallizer height + @self.Constraint(doc="Slurry height based on crystallizer diameter") + def eq_slurry_height_constraint(b): + return self.height_slurry == 4 * b.volume_suspension / ( + Constants.pi * b.diameter_crystallizer**2 + ) + + @self.Expression( + doc="Recommended height of vapor space (0.75*D) based on Tavares et. al." + ) + def eq_vapor_space_height(b): + return 0.75 * b.diameter_crystallizer + + @self.Expression( + doc="Height to diameter ratio constraint for evaporative crystallizers (Wilson et. al.)" + ) + def eq_minimum_height_diameter_ratio(b): + return 1.5 * b.diameter_crystallizer + + @self.Constraint(doc="Crystallizer height") + def eq_crystallizer_height_constraint(b): + # Height is max(). Manual smooth max implementation used here: max(a,b) = 0.5(a + b + |a-b|) + a = b.eq_vapor_space_height + b.height_slurry + b = b.eq_minimum_height_diameter_ratio + eps = 1e-20 * pyunits.m + return self.height_crystallizer == 0.5 * ( + a + b + ((a - b) ** 2 + eps**2) ** 0.5 + ) + + # 13. Energy available from the vapor + @self.Constraint(doc="Thermal energy in the vapor") + def eq_vapor_energy_constraint(b): + return b.energy_flow_superheated_vapor == ( + b.properties_vapor[0].flow_mass_phase_comp["Vap", "H2O"] + * ( + b.properties_pure_water[ + 0 + ].dh_vap_mass_solvent # Latent heat from the vapor + + b.properties_vapor[0].enth_mass_solvent["Vap"] + - b.properties_pure_water[0].enth_mass_solvent["Vap"] + ) + ) + + def initialize_build( + self, + state_args=None, + outlvl=idaeslog.NOTSET, + solver=None, + optarg=None, + ): + """ + General wrapper for pressure changer initialization routines + + Keyword Arguments: + state_args : a dict of arguments to be passed to the property + package(s) to provide an initial state for + initialization (see documentation of the specific + property package) (default = {}). + outlvl : sets output level of initialization routine + optarg : solver options dictionary object (default=None) + solver : str indicating which solver to use during + initialization (default = None) + + Returns: None + """ + init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") + solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit") + + opt = get_solver(solver, optarg) + + # --------------------------------------------------------------------- + # Initialize holdup block + flags = self.properties_in.initialize( + outlvl=outlvl, + optarg=optarg, + solver=solver, + state_args=state_args, + hold_state=True, + ) + init_log.info_high("Initialization Step 1 Complete.") + # --------------------------------------------------------------------- + # Initialize other state blocks + # Set state_args from inlet state + if state_args is None: + state_args = {} + state_dict = self.properties_in[ + self.flowsheet().config.time.first() + ].define_port_members() + + for k in state_dict.keys(): + if state_dict[k].is_indexed(): + state_args[k] = {} + for m in state_dict[k].keys(): + state_args[k][m] = state_dict[k][m].value + else: + state_args[k] = state_dict[k].value + + self.properties_out.initialize( + outlvl=outlvl, + optarg=optarg, + solver=solver, + state_args=state_args, + ) + + state_args_solids = deepcopy(state_args) + for p, j in self.properties_solids.phase_component_set: + if p == "Sol": + state_args_solids["flow_mass_phase_comp"][p, j] = state_args[ + "flow_mass_phase_comp" + ]["Liq", j] + elif p == "Liq" or p == "Vap": + state_args_solids["flow_mass_phase_comp"][p, j] = 1e-8 + self.properties_solids.initialize( + outlvl=outlvl, + optarg=optarg, + solver=solver, + state_args=state_args_solids, + ) + + state_args_vapor = deepcopy(state_args) + for p, j in self.properties_vapor.phase_component_set: + if p == "Vap": + state_args_vapor["flow_mass_phase_comp"][p, j] = state_args[ + "flow_mass_phase_comp" + ]["Liq", j] + elif p == "Liq" or p == "Sol": + state_args_vapor["flow_mass_phase_comp"][p, j] = 1e-8 + self.properties_vapor.initialize( + outlvl=outlvl, + optarg=optarg, + solver=solver, + state_args=state_args_vapor, + ) + + self.properties_pure_water.initialize( + outlvl=outlvl, + optarg=optarg, + solver=solver, + state_args=state_args_vapor, + ) + init_log.info_high("Initialization Step 2 Complete.") + + interval_initializer(self) + # --------------------------------------------------------------------- + # Solve unit + with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: + res = opt.solve(self, tee=slc.tee) + init_log.info_high("Initialization Step 3 {}.".format(idaeslog.condition(res))) + # --------------------------------------------------------------------- + # Release Inlet state + self.properties_in.release_state(flags, outlvl=outlvl) + init_log.info("Initialization Complete: {}".format(idaeslog.condition(res))) + + if not check_optimal_termination(res): + raise InitializationError(f"Unit model {self.name} failed to initialize") + + def calculate_scaling_factors(self): + super().calculate_scaling_factors() + + iscale.set_scaling_factor( + self.crystal_growth_rate, 1e7 + ) # growth rates typically of order 1e-7 to 1e-9 m/s + iscale.set_scaling_factor( + self.crystal_median_length, 1e3 + ) # Crystal lengths typically in mm + iscale.set_scaling_factor( + self.souders_brown_constant, 1e2 + ) # Typical values are 0.0244, 0.04 and 0.06 + iscale.set_scaling_factor( + self.diameter_crystallizer, 1 + ) # Crystallizer diameters typically up to about 20 m + iscale.set_scaling_factor( + self.height_crystallizer, 1 + ) # H/D ratio maximum is about 1.5, so same scaling as diameter + iscale.set_scaling_factor(self.height_slurry, 1) # Same scaling as diameter + iscale.set_scaling_factor(self.magma_circulation_flow_vol, 1) + iscale.set_scaling_factor(self.relative_supersaturation, 10) + iscale.set_scaling_factor(self.t_res, 1) # Residence time is in hours + iscale.set_scaling_factor( + self.volume_suspension, 0.1 + ) # Suspension volume usually in tens to hundreds range + iscale.set_scaling_factor( + self.crystallization_yield, 1 + ) # Yield is between 0 and 1, usually in the 10-60% range + iscale.set_scaling_factor(self.product_volumetric_solids_fraction, 10) + iscale.set_scaling_factor( + self.temperature_operating, + iscale.get_scaling_factor(self.properties_in[0].temperature), + ) + iscale.set_scaling_factor(self.pressure_operating, 1e-3) + iscale.set_scaling_factor( + self.dens_mass_magma, 1e-3 + ) # scaling factor of dens_mass_phase['Liq'] + iscale.set_scaling_factor( + self.dens_mass_slurry, 1e-3 + ) # scaling factor of dens_mass_phase['Liq'] + iscale.set_scaling_factor( + self.work_mechanical[0], + iscale.get_scaling_factor( + self.properties_in[0].flow_mass_phase_comp["Vap", "H2O"] + ) + * iscale.get_scaling_factor(self.properties_in[0].enth_mass_solvent["Vap"]), + ) + iscale.set_scaling_factor( + self.energy_flow_superheated_vapor, + iscale.get_scaling_factor( + self.properties_vapor[0].flow_mass_phase_comp["Vap", "H2O"] + ) + * iscale.get_scaling_factor( + self.properties_vapor[0].enth_mass_solvent["Vap"] + ), + ) + # transforming constraints + for ind, c in self.eq_T_con1.items(): + sf = iscale.get_scaling_factor(self.properties_in[0].temperature) + iscale.constraint_scaling_transform(c, sf) + + for ind, c in self.eq_T_con2.items(): + sf = iscale.get_scaling_factor(self.properties_in[0].temperature) + iscale.constraint_scaling_transform(c, sf) + + for ind, c in self.eq_T_con3.items(): + sf = iscale.get_scaling_factor(self.properties_in[0].temperature) + iscale.constraint_scaling_transform(c, sf) + + for ind, c in self.eq_p_con1.items(): + sf = iscale.get_scaling_factor(self.properties_in[0].pressure) + iscale.constraint_scaling_transform(c, sf) + + for ind, c in self.eq_p_con2.items(): + sf = iscale.get_scaling_factor(self.properties_in[0].pressure) + iscale.constraint_scaling_transform(c, sf) + + for ind, c in self.eq_p_con3.items(): + sf = iscale.get_scaling_factor(self.properties_in[0].pressure) + iscale.constraint_scaling_transform(c, sf) + + for ind, c in self.eq_p_con4.items(): + sf = iscale.get_scaling_factor(self.properties_pure_water[0].pressure) + iscale.constraint_scaling_transform(c, sf) + + for j, c in self.eq_mass_balance_constraints.items(): + sf = iscale.get_scaling_factor( + self.properties_in[0].flow_mass_phase_comp["Liq", j] + ) + iscale.constraint_scaling_transform(c, sf) + + for j, c in self.eq_solubility_massfrac_equality_constraint.items(): + iscale.constraint_scaling_transform(c, 1e0) + + for j, c in self.eq_dens_magma.items(): + iscale.constraint_scaling_transform( + c, iscale.get_scaling_factor(self.dens_mass_magma) + ) + + for j, c in self.eq_removal_balance.items(): + sf = iscale.get_scaling_factor( + self.properties_in[0].flow_mass_phase_comp["Liq", j] + ) + iscale.constraint_scaling_transform(c, sf) + + def _get_stream_table_contents(self, time_point=0): + return create_stream_table_dataframe( + { + "Feed Inlet": self.inlet, + "Liquid Outlet": self.outlet, + "Vapor Outlet": self.vapor, + "Solid Outlet": self.solids, + }, + time_point=time_point, + ) + + def _get_performance_contents(self, time_point=0): + var_dict = {} + var_dict["Operating Temperature"] = self.temperature_operating + var_dict["Operating Pressure"] = self.pressure_operating + var_dict["Magma density of solution"] = self.dens_mass_magma + var_dict["Slurry density"] = self.dens_mass_slurry + var_dict["Heat requirement"] = self.work_mechanical[time_point] + var_dict["Crystallizer diameter"] = self.diameter_crystallizer + var_dict["Magma circulation flow rate"] = self.magma_circulation_flow_vol + var_dict[ + "Vol. frac. of solids in suspension, 1-E" + ] = self.product_volumetric_solids_fraction + var_dict["Residence time"] = self.t_res + var_dict["Crystallizer minimum active volume"] = self.volume_suspension + var_dict["Suspension height in crystallizer"] = self.height_slurry + var_dict["Crystallizer height"] = self.height_crystallizer + + for j in self.config.property_package.solute_set: + yield_mem_name = f"{j} yield (fraction)" + var_dict[yield_mem_name] = self.crystallization_yield[j] + supersat_mem_name = f"{j} relative supersaturation (mass fraction basis)" + var_dict[supersat_mem_name] = self.relative_supersaturation[j] + + return {"vars": var_dict} + + @property + def default_costing_method(self): + return cost_crystallizer diff --git a/src/watertap_contrib/reflo/unit_models/zero_order/tests/test_crystallizer_watertap.py b/src/watertap_contrib/reflo/unit_models/zero_order/tests/test_crystallizer_watertap.py new file mode 100644 index 00000000..543d254c --- /dev/null +++ b/src/watertap_contrib/reflo/unit_models/zero_order/tests/test_crystallizer_watertap.py @@ -0,0 +1,787 @@ +################################################################################# +# WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# National Renewable Energy Laboratory, and National Energy Technology +# Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +################################################################################# + +import pytest +from pyomo.environ import ( + ConcreteModel, + TerminationCondition, + SolverStatus, + value, + Var, +) +from pyomo.network import Port +from idaes.core import FlowsheetBlock +from pyomo.util.check_units import assert_units_consistent +from watertap_contrib.reflo.unit_models.zero_order.crystallizer_zo_watertap import ( + Crystallization, +) +import watertap_contrib.reflo.property_models.cryst_prop_pack as props + +from idaes.core.solvers import get_solver + +# from watertap.core.solvers import get_solver +from idaes.core.util.model_statistics import ( + degrees_of_freedom, + number_variables, + number_total_constraints, + number_unused_variables, +) +from idaes.core.util.testing import initialization_tester +from idaes.core.util.scaling import ( + calculate_scaling_factors, + unscaled_variables_generator, + badly_scaled_var_generator, +) +from idaes.core import UnitModelCostingBlock + +# from watertap.costing import WaterTAPCosting, CrystallizerCostType + +# ----------------------------------------------------------------------------- +# Get default solver for testing +solver = get_solver() + + +class TestCrystallization: + @pytest.fixture(scope="class") + def Crystallizer_frame(self): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.properties = props.NaClParameterBlock() + + m.fs.unit = Crystallization(property_package=m.fs.properties) + + # fully specify system + feed_flow_mass = 1 + feed_mass_frac_NaCl = 0.2126 + feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl + feed_pressure = 101325 + feed_temperature = 273.15 + 20 + eps = 1e-6 + crystallizer_temperature = 273.15 + 55 + crystallizer_yield = 0.40 + + # Fully define feed + m.fs.unit.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"].fix( + feed_flow_mass * feed_mass_frac_NaCl + ) + m.fs.unit.inlet.flow_mass_phase_comp[0, "Liq", "H2O"].fix( + feed_flow_mass * feed_mass_frac_H2O + ) + m.fs.unit.inlet.flow_mass_phase_comp[0, "Sol", "NaCl"].fix(eps) + m.fs.unit.inlet.flow_mass_phase_comp[0, "Vap", "H2O"].fix(eps) + m.fs.unit.inlet.pressure[0].fix(feed_pressure) + m.fs.unit.inlet.temperature[0].fix(feed_temperature) + + # Define operating conditions + m.fs.unit.temperature_operating.fix(crystallizer_temperature) + m.fs.unit.crystallization_yield["NaCl"].fix(crystallizer_yield) + + # Fix growth rate, crystal length and Sounders brown constant to default values + m.fs.unit.crystal_growth_rate.fix() + m.fs.unit.souders_brown_constant.fix() + m.fs.unit.crystal_median_length.fix() + + assert_units_consistent(m) + + return m + + @pytest.fixture(scope="class") + def Crystallizer_frame_2(self): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.properties = props.NaClParameterBlock() + + m.fs.unit = Crystallization(property_package=m.fs.properties) + + # fully specify system + feed_flow_mass = 1 + feed_mass_frac_NaCl = 0.2126 + feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl + feed_pressure = 101325 + feed_temperature = 273.15 + 20 + eps = 1e-6 + crystallizer_temperature = 273.15 + 55 + crystallizer_yield = 0.40 + + # Fully define feed + m.fs.unit.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"].fix( + feed_flow_mass * feed_mass_frac_NaCl + ) + m.fs.unit.inlet.flow_mass_phase_comp[0, "Liq", "H2O"].fix( + feed_flow_mass * feed_mass_frac_H2O + ) + m.fs.unit.inlet.flow_mass_phase_comp[0, "Sol", "NaCl"].fix(eps) + m.fs.unit.inlet.flow_mass_phase_comp[0, "Vap", "H2O"].fix(eps) + m.fs.unit.inlet.pressure[0].fix(feed_pressure) + m.fs.unit.inlet.temperature[0].fix(feed_temperature) + + # Define operating conditions + m.fs.unit.temperature_operating.fix(crystallizer_temperature) + m.fs.unit.crystallization_yield["NaCl"].fix(crystallizer_yield) + + # Fix growth rate, crystal length and Sounders brown constant to default values + m.fs.unit.crystal_growth_rate.fix() + m.fs.unit.souders_brown_constant.fix() + m.fs.unit.crystal_median_length.fix() + + assert_units_consistent(m) + + return m + + @pytest.mark.unit + def test_config(self, Crystallizer_frame): + m = Crystallizer_frame + # check unit config arguments + assert len(m.fs.unit.config) == 4 + + assert not m.fs.unit.config.dynamic + assert not m.fs.unit.config.has_holdup + assert m.fs.unit.config.property_package is m.fs.properties + + @pytest.mark.unit + def test_build(self, Crystallizer_frame): + m = Crystallizer_frame + + # test ports and variables + port_lst = ["inlet", "outlet", "solids", "vapor"] + port_vars_lst = ["flow_mass_phase_comp", "pressure", "temperature"] + for port_str in port_lst: + assert hasattr(m.fs.unit, port_str) + port = getattr(m.fs.unit, port_str) + assert len(port.vars) == 3 + assert isinstance(port, Port) + for var_str in port_vars_lst: + assert hasattr(port, var_str) + var = getattr(port, var_str) + assert isinstance(var, Var) + + # test unit objects (including parameters, variables, and constraints) + # First, parameters + unit_objs_params_lst = [ + "approach_temperature_heat_exchanger", + "dimensionless_crystal_length", + ] + for obj_str in unit_objs_params_lst: + assert hasattr(m.fs.unit, obj_str) + # Next, variables + unit_objs_vars_lst = [ + "crystal_growth_rate", + "crystal_median_length", + "crystallization_yield", + "dens_mass_magma", + "dens_mass_slurry", + "diameter_crystallizer", + "height_crystallizer", + "height_slurry", + "magma_circulation_flow_vol", + "pressure_operating", + "product_volumetric_solids_fraction", + "relative_supersaturation", + "souders_brown_constant", + "t_res", + "temperature_operating", + "volume_suspension", + "work_mechanical", + ] + for obj_str in unit_objs_vars_lst: + assert hasattr(m.fs.unit, obj_str) + # Next, expressions + unit_objs_expr_lst = [ + "eq_max_allowable_velocity", + "eq_minimum_height_diameter_ratio", + "eq_vapor_space_height", + ] + for obj_str in unit_objs_expr_lst: + assert hasattr(m.fs.unit, obj_str) + # Finally, constraints + unit_objs_cons_lst = [ + "eq_T_con1", + "eq_T_con2", + "eq_T_con3", + "eq_crystallizer_height_constraint", + "eq_dens_magma", + "eq_dens_mass_slurry", + "eq_enthalpy_balance", + "eq_mass_balance_constraints", + "eq_minimum_hex_circulation_rate_constraint", + "eq_operating_pressure_constraint", + "eq_p_con1", + "eq_p_con2", + "eq_p_con3", + "eq_relative_supersaturation", + "eq_removal_balance", + "eq_residence_time", + "eq_slurry_height_constraint", + "eq_solubility_massfrac_equality_constraint", + "eq_suspension_volume", + "eq_vapor_head_diameter_constraint", + "eq_vol_fraction_solids", + ] + for obj_str in unit_objs_cons_lst: + assert hasattr(m.fs.unit, obj_str) + + # Test stateblocks + # List olf attributes on all stateblocks + stateblock_objs_lst = [ + "flow_mass_phase_comp", + "pressure", + "temperature", + "solubility_mass_phase_comp", + "solubility_mass_frac_phase_comp", + "mass_frac_phase_comp", + "dens_mass_solvent", + "dens_mass_solute", + "dens_mass_phase", + "cp_mass_phase", + "cp_mass_solvent", + "flow_vol_phase", + "flow_vol", + "enth_flow", + "enth_mass_solvent", + "dh_crystallization_mass_comp", + "eq_solubility_mass_phase_comp", + "eq_solubility_mass_frac_phase_comp", + "eq_mass_frac_phase_comp", + "eq_dens_mass_solvent", + "eq_dens_mass_solute", + "eq_dens_mass_phase", + "eq_cp_mass_solute", + "eq_cp_mass_phase", + "eq_flow_vol_phase", + "eq_enth_mass_solvent", + ] + # List of attributes for liquid stateblocks only + stateblock_objs_liq_lst = ["pressure_sat", "eq_pressure_sat"] + # Inlet block + assert hasattr(m.fs.unit, "properties_in") + blk = getattr(m.fs.unit, "properties_in") + for var_str in stateblock_objs_lst: + assert hasattr(blk[0], var_str) + for var_str in stateblock_objs_liq_lst: + assert hasattr(blk[0], var_str) + + # Liquid outlet block + assert hasattr(m.fs.unit, "properties_out") + blk = getattr(m.fs.unit, "properties_out") + for var_str in stateblock_objs_lst: + assert hasattr(blk[0], var_str) + for var_str in stateblock_objs_liq_lst: + assert hasattr(blk[0], var_str) + + # Vapor outlet block + assert hasattr(m.fs.unit, "properties_vapor") + blk = getattr(m.fs.unit, "properties_vapor") + for var_str in stateblock_objs_lst: + assert hasattr(blk[0], var_str) + + # Liquid outlet block + assert hasattr(m.fs.unit, "properties_solids") + blk = getattr(m.fs.unit, "properties_solids") + for var_str in stateblock_objs_lst: + assert hasattr(blk[0], var_str) + + # test statistics + assert number_variables(m) == 255 + assert number_total_constraints(m) == 138 + assert number_unused_variables(m) == 5 + + @pytest.mark.unit + def test_dof(self, Crystallizer_frame): + m = Crystallizer_frame + assert degrees_of_freedom(m) == 0 + + @pytest.mark.unit + def test_calculate_scaling(self, Crystallizer_frame): + m = Crystallizer_frame + + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Liq", "H2O") + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Liq", "NaCl") + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Vap", "H2O") + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Sol", "NaCl") + ) + calculate_scaling_factors(m) + + # check that all variables have scaling factors + unscaled_var_list = list(unscaled_variables_generator(m)) + assert len(unscaled_var_list) == 0 + + for _ in badly_scaled_var_generator(m): + assert False + + @pytest.mark.requires_idaes_solver + @pytest.mark.component + def test_initialize(self, Crystallizer_frame): + # Add costing function, then initialize + m = Crystallizer_frame + # m.fs.costing = WaterTAPCosting() + # m.fs.unit.costing = UnitModelCostingBlock( + # flowsheet_costing_block=m.fs.costing, + # costing_method_arguments={"cost_type": CrystallizerCostType.mass_basis}, + # ) + # m.fs.costing.cost_process() + + initialization_tester(Crystallizer_frame) + assert_units_consistent(m) + + # @pytest.mark.component + # def test_var_scaling(self, Crystallizer_frame): + # m = Crystallizer_frame + # badly_scaled_var_lst = list(badly_scaled_var_generator(m)) + # assert badly_scaled_var_lst == [] + + @pytest.mark.requires_idaes_solver + @pytest.mark.component + def test_solve(self, Crystallizer_frame): + m = Crystallizer_frame + results = solver.solve(m) + + # Check for optimal solution + assert results.solver.termination_condition == TerminationCondition.optimal + assert results.solver.status == SolverStatus.ok + + @pytest.mark.requires_idaes_solver + @pytest.mark.component + def test_conservation(self, Crystallizer_frame): + m = Crystallizer_frame + b = m.fs.unit + comp_lst = ["NaCl", "H2O"] + phase_lst = ["Sol", "Liq", "Vap"] + phase_comp_list = [ + (p, j) + for j in comp_lst + for p in phase_lst + if (p, j) in b.properties_in[0].phase_component_set + ] + + flow_mass_in = sum( + b.properties_in[0].flow_mass_phase_comp[p, j] + for p in phase_lst + for j in comp_lst + if (p, j) in phase_comp_list + ) + flow_mass_out = sum( + b.properties_out[0].flow_mass_phase_comp[p, j] + for p in phase_lst + for j in comp_lst + if (p, j) in phase_comp_list + ) + flow_mass_solids = sum( + b.properties_solids[0].flow_mass_phase_comp[p, j] + for p in phase_lst + for j in comp_lst + if (p, j) in phase_comp_list + ) + flow_mass_vapor = sum( + b.properties_vapor[0].flow_mass_phase_comp[p, j] + for p in phase_lst + for j in comp_lst + if (p, j) in phase_comp_list + ) + + assert ( + abs( + value(flow_mass_in - flow_mass_out - flow_mass_solids - flow_mass_vapor) + ) + <= 1e-6 + ) + + assert ( + abs( + value( + flow_mass_in * b.properties_in[0].enth_mass_phase["Liq"] + - flow_mass_out * b.properties_out[0].enth_mass_phase["Liq"] + - flow_mass_vapor * b.properties_vapor[0].enth_mass_solvent["Vap"] + - flow_mass_solids * b.properties_solids[0].enth_mass_solute["Sol"] + - flow_mass_solids + * b.properties_solids[0].dh_crystallization_mass_comp["NaCl"] + + b.work_mechanical[0] + ) + ) + <= 1e-2 + ) + + @pytest.mark.requires_idaes_solver + @pytest.mark.component + def test_solution(self, Crystallizer_frame): + m = Crystallizer_frame + b = m.fs.unit + # Check solid mass in solids stream + assert pytest.approx( + value( + b.crystallization_yield["NaCl"] + * b.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"] + ), + rel=1e-3, + ) == value(b.solids.flow_mass_phase_comp[0, "Sol", "NaCl"]) + # Check solid mass in liquid stream + assert pytest.approx( + value( + (1 - b.crystallization_yield["NaCl"]) + * b.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"] + ), + rel=1e-3, + ) == value(b.outlet.flow_mass_phase_comp[0, "Liq", "NaCl"]) + # Check outlet liquid stream composition which is set by solubility + assert pytest.approx(0.2695, rel=1e-3) == value( + b.properties_out[0].mass_frac_phase_comp["Liq", "NaCl"] + ) + # Check liquid stream solvent flow + assert pytest.approx(0.12756 * ((1 / 0.2695) - 1), rel=1e-3) == value( + b.outlet.flow_mass_phase_comp[0, "Liq", "H2O"] + ) + # Check saturation pressure + assert pytest.approx(11992, rel=1e-3) == value(b.pressure_operating) + # Check heat requirement + assert pytest.approx(1127.2, rel=1e-3) == value(b.work_mechanical[0]) + # Check crystallizer diameter + assert pytest.approx(1.205, rel=1e-3) == value(b.diameter_crystallizer) + # Minimum active volume + assert pytest.approx(1.619, rel=1e-3) == value(b.volume_suspension) + # Residence time + assert pytest.approx(1.0228, rel=1e-3) == value(b.t_res) + # Mass-basis costing + assert pytest.approx(2.0 * 300000, rel=1e-3) == value( + m.fs.costing.aggregate_capital_cost + ) + + @pytest.mark.requires_idaes_solver + @pytest.mark.component + def test_solution2_capcosting_by_mass(self, Crystallizer_frame): + m = Crystallizer_frame + b = m.fs.unit + b.crystal_growth_rate.fix(5e-8) + b.souders_brown_constant.fix(0.0244) + b.crystal_median_length.fix(0.4e-3) + results = solver.solve(m) + + # Test that report function works + b.report() + + # Check for optimal solution + assert results.solver.termination_condition == TerminationCondition.optimal + assert results.solver.status == SolverStatus.ok + + # Residence time + assert pytest.approx( + value(b.crystal_median_length / (3.67 * 3600 * b.crystal_growth_rate)), + rel=1e-3, + ) == value(b.t_res) + # Check crystallizer diameter + assert pytest.approx(1.5427, rel=1e-3) == value(b.diameter_crystallizer) + # Minimum active volume + assert pytest.approx(0.959, rel=1e-3) == value(b.volume_suspension) + # Mass-basis costing + assert pytest.approx(2.0 * 300000, rel=1e-3) == value( + m.fs.costing.aggregate_capital_cost + ) + + @pytest.mark.component + def test_solution2_capcosting_by_volume(self, Crystallizer_frame_2): + # Same problem as above, but different costing approach. + # Other results should remain the same. + m = Crystallizer_frame_2 + b = m.fs.unit + b.crystal_growth_rate.fix(5e-8) + b.souders_brown_constant.fix(0.0244) + b.crystal_median_length.fix(0.4e-3) + + assert degrees_of_freedom(m) == 0 + + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Liq", "H2O") + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Liq", "NaCl") + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Vap", "H2O") + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Sol", "NaCl") + ) + calculate_scaling_factors(m) + initialization_tester(Crystallizer_frame_2) + results = solver.solve(m) + + m.fs.costing = WaterTAPCosting() + m.fs.unit.costing = UnitModelCostingBlock( + flowsheet_costing_block=m.fs.costing, + costing_method_arguments={"cost_type": CrystallizerCostType.volume_basis}, + ) + m.fs.costing.cost_process() + assert_units_consistent(m) + results = solver.solve(m) + + # Test that report function works + b.report() + + # Check for optimal solution + assert results.solver.termination_condition == TerminationCondition.optimal + assert results.solver.status == SolverStatus.ok + + # Residence time + assert pytest.approx( + value(b.crystal_median_length / (3.67 * 3600 * b.crystal_growth_rate)), + rel=1e-3, + ) == value(b.t_res) + # Check crystallizer diameter + assert pytest.approx(1.5427, rel=1e-3) == value(b.diameter_crystallizer) + # Minimum active volume + assert pytest.approx(0.959, rel=1e-3) == value(b.volume_suspension) + # Volume-basis costing + assert pytest.approx(2.0 * 199000, rel=1e-3) == value( + m.fs.costing.aggregate_capital_cost + ) + + @pytest.mark.component + def test_solution2_operatingcost(self, Crystallizer_frame_2): + m = Crystallizer_frame_2 + b = m.fs.unit + b.crystal_growth_rate.fix(5e-8) + b.souders_brown_constant.fix(0.0244) + b.crystal_median_length.fix(0.4e-3) + results = solver.solve(m) + + # Check for optimal solution + assert results.solver.termination_condition == TerminationCondition.optimal + assert results.solver.status == SolverStatus.ok + + # Operating cost validation + assert pytest.approx(835.41, rel=1e-3) == value( + m.fs.costing.aggregate_flow_costs["electricity"] + ) + assert pytest.approx(30666.67, rel=1e-3) == value( + m.fs.costing.aggregate_flow_costs["steam"] + ) + assert pytest.approx(0, rel=1e-3) == value( + m.fs.costing.aggregate_flow_costs["NaCl"] + ) + + @pytest.mark.component + def test_solution2_operatingcost_steampressure(self, Crystallizer_frame_2): + m = Crystallizer_frame_2 + m.fs.costing.crystallizer.steam_pressure.fix(5) + b = m.fs.unit + b.crystal_growth_rate.fix(5e-8) + b.souders_brown_constant.fix(0.0244) + b.crystal_median_length.fix(0.4e-3) + results = solver.solve(m) + + # Check for optimal solution + assert results.solver.termination_condition == TerminationCondition.optimal + assert results.solver.status == SolverStatus.ok + + # Operating cost validation + assert pytest.approx(835.41, rel=1e-3) == value( + m.fs.costing.aggregate_flow_costs["electricity"] + ) + assert pytest.approx(21451.91, rel=1e-3) == value( + m.fs.costing.aggregate_flow_costs["steam"] + ) + assert pytest.approx(0, abs=1e-6) == value( + m.fs.costing.aggregate_flow_costs["NaCl"] + ) + + @pytest.mark.component + def test_solution2_operatingcost_NaCl_revenue(self, Crystallizer_frame_2): + m = Crystallizer_frame_2 + m.fs.costing.crystallizer.steam_pressure.fix(3) + m.fs.costing.crystallizer.NaCl_recovery_value.fix(-0.07) + + results = solver.solve(m) + + # Check for optimal solution + assert results.solver.termination_condition == TerminationCondition.optimal + assert results.solver.status == SolverStatus.ok + + # Operating cost validation + assert pytest.approx(835.41, rel=1e-3) == value( + m.fs.costing.aggregate_flow_costs["electricity"] + ) + assert pytest.approx(30666.67, rel=1e-3) == value( + m.fs.costing.aggregate_flow_costs["steam"] + ) + assert pytest.approx(-187858.2, rel=1e-3) == value( + m.fs.costing.aggregate_flow_costs["NaCl"] + ) + + +if __name__ == "__main__": + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.properties = props.NaClParameterBlock() + + m.fs.unit = Crystallization(property_package=m.fs.properties) + + # fully specify system + feed_flow_mass = 1 + feed_mass_frac_NaCl = 0.2126 + feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl + feed_pressure = 101325 + feed_temperature = 273.15 + 20 + eps = 1e-6 + crystallizer_temperature = 273.15 + 55 + crystallizer_yield = 0.40 + + # Fully define feed + m.fs.unit.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"].fix( + feed_flow_mass * feed_mass_frac_NaCl + ) + m.fs.unit.inlet.flow_mass_phase_comp[0, "Liq", "H2O"].fix( + feed_flow_mass * feed_mass_frac_H2O + ) + m.fs.unit.inlet.flow_mass_phase_comp[0, "Sol", "NaCl"].fix(eps) + m.fs.unit.inlet.flow_mass_phase_comp[0, "Vap", "H2O"].fix(eps) + m.fs.unit.inlet.pressure[0].fix(feed_pressure) + m.fs.unit.inlet.temperature[0].fix(feed_temperature) + + # Define operating conditions + m.fs.unit.temperature_operating.fix(crystallizer_temperature) + m.fs.unit.crystallization_yield["NaCl"].fix(crystallizer_yield) + + # Fix growth rate, crystal length and Sounders brown constant to default values + m.fs.unit.crystal_growth_rate.fix() + m.fs.unit.souders_brown_constant.fix() + m.fs.unit.crystal_median_length.fix() + + assert_units_consistent(m) + + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Liq", "H2O") + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Liq", "NaCl") + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Vap", "H2O") + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Sol", "NaCl") + ) + calculate_scaling_factors(m) + + m.fs.unit.initialize() + + results = solver.solve(m) + + # Check for optimal solution + assert results.solver.termination_condition == TerminationCondition.optimal + assert results.solver.status == SolverStatus.ok + + +if __name__ == "__main__": + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.properties = props.NaClParameterBlock() + + m.fs.unit = Crystallization(property_package=m.fs.properties) + + # fully specify system + feed_flow_mass = 1 + feed_mass_frac_NaCl = 0.2126 + feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl + feed_pressure = 101325 + feed_temperature = 273.15 + 20 + eps = 1e-6 + crystallizer_temperature = 273.15 + 55 + crystallizer_yield = 0.40 + + # Fully define feed + m.fs.unit.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"].fix( + feed_flow_mass * feed_mass_frac_NaCl + ) + m.fs.unit.inlet.flow_mass_phase_comp[0, "Liq", "H2O"].fix( + feed_flow_mass * feed_mass_frac_H2O + ) + m.fs.unit.inlet.flow_mass_phase_comp[0, "Sol", "NaCl"].fix(eps) + m.fs.unit.inlet.flow_mass_phase_comp[0, "Vap", "H2O"].fix(eps) + m.fs.unit.inlet.pressure[0].fix(feed_pressure) + m.fs.unit.inlet.temperature[0].fix(feed_temperature) + + # Define operating conditions + m.fs.unit.temperature_operating.fix(crystallizer_temperature) + m.fs.unit.crystallization_yield["NaCl"].fix(crystallizer_yield) + + # Fix growth rate, crystal length and Sounders brown constant to default values + m.fs.unit.crystal_growth_rate.fix() + m.fs.unit.souders_brown_constant.fix() + m.fs.unit.crystal_median_length.fix() + + assert_units_consistent(m) + + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Liq", "H2O") + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Liq", "NaCl") + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Vap", "H2O") + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Sol", "NaCl") + ) + calculate_scaling_factors(m) + + m.fs.unit.initialize() + + results = solver.solve(m) + + # Check for optimal solution + assert results.solver.termination_condition == TerminationCondition.optimal + assert results.solver.status == SolverStatus.ok + + b = m.fs.unit + assert pytest.approx( + value( + b.crystallization_yield["NaCl"] + * b.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"] + ), + rel=1e-3, + ) == value(b.solids.flow_mass_phase_comp[0, "Sol", "NaCl"]) + # Check solid mass in liquid stream + assert pytest.approx( + value( + (1 - b.crystallization_yield["NaCl"]) + * b.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"] + ), + rel=1e-3, + ) == value(b.outlet.flow_mass_phase_comp[0, "Liq", "NaCl"]) + # Check outlet liquid stream composition which is set by solubility + assert pytest.approx(0.2695, rel=1e-3) == value( + b.properties_out[0].mass_frac_phase_comp["Liq", "NaCl"] + ) + # Check liquid stream solvent flow + assert pytest.approx(0.12756 * ((1 / 0.2695) - 1), rel=1e-3) == value( + b.outlet.flow_mass_phase_comp[0, "Liq", "H2O"] + ) + # Check saturation pressure + assert pytest.approx(11992, rel=1e-3) == value(b.pressure_operating) + # Check heat requirement + assert pytest.approx(1127.2, rel=1e-3) == value(b.work_mechanical[0]) + # Check crystallizer diameter + assert pytest.approx(1.205, rel=1e-3) == value(b.diameter_crystallizer) + # Minimum active volume + assert pytest.approx(1.619, rel=1e-3) == value(b.volume_suspension) + # Residence time + assert pytest.approx(1.0228, rel=1e-3) == value(b.t_res) From bfb53e60e8d916c986413409ec8c41caa1ab39a6 Mon Sep 17 00:00:00 2001 From: zhuoran29 Date: Mon, 12 Aug 2024 22:10:44 -0400 Subject: [PATCH 03/80] modify costing --- .../multi_effect_NaCl_crystallizer.py | 325 +++++++++++++++++- .../test_multi_effect_NaCl_crystallizer.py | 231 +++++++++++++ .../reflo/costing/__init__.py | 2 + .../costing/units/crystallizer_watertap.py | 289 ++++++++++++++++ .../zero_order/crystallizer_zo_watertap.py | 6 +- .../tests/test_crystallizer_watertap.py | 208 +---------- 6 files changed, 859 insertions(+), 202 deletions(-) rename src/watertap_contrib/reflo/analysis/{flowsheets => example_flowsheets}/multi_effect_NaCl_crystallizer.py (55%) create mode 100644 src/watertap_contrib/reflo/analysis/example_flowsheets/test/test_multi_effect_NaCl_crystallizer.py create mode 100644 src/watertap_contrib/reflo/costing/units/crystallizer_watertap.py diff --git a/src/watertap_contrib/reflo/analysis/flowsheets/multi_effect_NaCl_crystallizer.py b/src/watertap_contrib/reflo/analysis/example_flowsheets/multi_effect_NaCl_crystallizer.py similarity index 55% rename from src/watertap_contrib/reflo/analysis/flowsheets/multi_effect_NaCl_crystallizer.py rename to src/watertap_contrib/reflo/analysis/example_flowsheets/multi_effect_NaCl_crystallizer.py index 3a645e05..44777999 100644 --- a/src/watertap_contrib/reflo/analysis/flowsheets/multi_effect_NaCl_crystallizer.py +++ b/src/watertap_contrib/reflo/analysis/example_flowsheets/multi_effect_NaCl_crystallizer.py @@ -15,17 +15,13 @@ units as pyunits, ) from pyomo.network import Port +from pyomo.util.check_units import assert_units_consistent + from idaes.core import FlowsheetBlock import idaes.core.util.scaling as iscale -from pyomo.util.check_units import assert_units_consistent -from watertap_contrib.reflo.unit_models.zero_order.crystallizer_zo_watertap import ( - Crystallization, -) -import watertap_contrib.reflo.property_models.cryst_prop_pack as props from watertap.unit_models.mvc.components.lmtd_chen_callback import ( delta_temperature_chen_callback, ) -from idaes.core.solvers import get_solver from idaes.core.util.model_statistics import ( degrees_of_freedom, number_variables, @@ -45,7 +41,16 @@ ) from idaes.core import UnitModelCostingBlock -from watertap.costing import WaterTAPCosting, CrystallizerCostType +from watertap.core.solvers import get_solver +from watertap_contrib.reflo.unit_models.zero_order.crystallizer_zo_watertap import ( + Crystallization, +) +import watertap_contrib.reflo.property_models.cryst_prop_pack as props +from watertap_contrib.reflo.costing import ( + TreatmentCosting, + CrystallizerCostType, + _compute_steam_properties, +) solver = get_solver() @@ -113,12 +118,98 @@ def build_fs_multi_effect_crystallizer( m.fs.eff_3.pressure_operating.fix(operating_pressure_eff3 * pyunits.bar) m.fs.eff_4.pressure_operating.fix(operating_pressure_eff4 * pyunits.bar) + steam_temp = add_heat_exchanger_eff1(m, steam_pressure) add_heat_exchanger_eff2(m) add_heat_exchanger_eff3(m) add_heat_exchanger_eff4(m) + return m +def add_heat_exchanger_eff1(m, steam_pressure): + eff_1 = m.fs.eff_1 + + eff_1.delta_temperature_in = Var( + eff_1.flowsheet().time, + initialize=35, + bounds=(None, None), + units=pyunits.K, + doc="Temperature differnce at the inlet side", + ) + eff_1.delta_temperature_out = Var( + eff_1.flowsheet().time, + initialize=35, + bounds=(None, None), + units=pyunits.K, + doc="Temperature differnce at the outlet side", + ) + delta_temperature_chen_callback(eff_1) + + eff_1.area = Var( + bounds=(0, None), + initialize=1000.0, + doc="Heat exchange area", + units=pyunits.m**2, + ) + + eff_1.overall_heat_transfer_coefficient = Var( + eff_1.flowsheet().time, + bounds=(0, None), + initialize=100.0, + doc="Overall heat transfer coefficient", + units=pyunits.W / pyunits.m**2 / pyunits.K, + ) + + eff_1.overall_heat_transfer_coefficient[0].fix(100) + + # Compute saturation temperature of steam: computed from El-Dessouky expression + steam_pressure_sat = steam_pressure * pyunits.bar + + tsat_constants = [ + 42.6776 * pyunits.K, + -3892.7 * pyunits.K, + 1000 * pyunits.kPa, + -9.48654 * pyunits.dimensionless, + ] + psat = ( + pyunits.convert(steam_pressure_sat, to_units=pyunits.kPa) + + 101.325 * pyunits.kPa + ) + + temperature_sat = tsat_constants[0] + tsat_constants[1] / ( + log(psat / tsat_constants[2]) + tsat_constants[3] + ) + + @m.Constraint(eff_1.flowsheet().time, doc="delta_temperature_in at the 1st effect") + def delta_temperature_in_eff1(b, t): + return ( + b.fs.eff_1.delta_temperature_in[t] + == temperature_sat - b.fs.eff_1.temperature_operating + ) + + @m.Constraint(eff_1.flowsheet().time, doc="delta_temperature_out at the 1st effect") + def delta_temperature_out_eff1(b, t): + return ( + b.fs.eff_1.delta_temperature_out[t] + == temperature_sat - b.fs.eff_1.properties_in[0].temperature + ) + + @m.Constraint(eff_1.flowsheet().time) + def heat_transfer_equation_eff_1(b, t): + return b.fs.eff_1.work_mechanical[0] == ( + b.fs.eff_1.overall_heat_transfer_coefficient[t] + * b.fs.eff_1.area + * b.fs.eff_1.delta_temperature[0] + ) + + iscale.set_scaling_factor(eff_1.delta_temperature_in, 1e-1) + iscale.set_scaling_factor(eff_1.delta_temperature_out, 1e-1) + iscale.set_scaling_factor(eff_1.area, 1e-1) + iscale.set_scaling_factor(eff_1.overall_heat_transfer_coefficient, 1e-1) + + return temperature_sat() + + def add_heat_exchanger_eff2(m): eff_1 = m.fs.eff_1 eff_2 = m.fs.eff_2 @@ -318,6 +409,89 @@ def heat_transfer_equation_eff4(b, t): iscale.set_scaling_factor(eff_4.overall_heat_transfer_coefficient, 1e-1) +def add_costings(m): + effs = [m.fs.eff_1, m.fs.eff_2, m.fs.eff_3, m.fs.eff_4] + + m.fs.capex_heat_exchanger = Expression( + expr=(420 * sum(i.area for i in effs)), doc="Capital cost of heat exchangers" + ) + + m.fs.capex_end_plates = Expression( + expr=(1020 * (sum(i.area for i in effs) / 10) ** 0.6), + doc="Capital cost of heat exchanger endplates", + ) + + m.fs.costing = TreatmentCosting() + m.fs.eff_1.costing = UnitModelCostingBlock( + flowsheet_costing_block=m.fs.costing, + costing_method_arguments={"cost_type": CrystallizerCostType.mass_basis}, + ) + m.fs.eff_2.costing = UnitModelCostingBlock( + flowsheet_costing_block=m.fs.costing, + costing_method_arguments={"cost_type": CrystallizerCostType.mass_basis}, + ) + m.fs.eff_3.costing = UnitModelCostingBlock( + flowsheet_costing_block=m.fs.costing, + costing_method_arguments={"cost_type": CrystallizerCostType.mass_basis}, + ) + m.fs.eff_4.costing = UnitModelCostingBlock( + flowsheet_costing_block=m.fs.costing, + costing_method_arguments={"cost_type": CrystallizerCostType.mass_basis}, + ) + + # Effect 2-4 doesn't need additional heating steam and the flows are removed + m.fs.eff_2.costing.costing_package.cost_flow( + -pyunits.convert( + ( + m.fs.eff_2.work_mechanical[0] + / _compute_steam_properties(m.fs.eff_2.costing) + ), + to_units=pyunits.m**3 / pyunits.s, + ), + "steam", + ) + m.fs.eff_3.costing.costing_package.cost_flow( + -pyunits.convert( + ( + m.fs.eff_3.work_mechanical[0] + / _compute_steam_properties(m.fs.eff_3.costing) + ), + to_units=pyunits.m**3 / pyunits.s, + ), + "steam", + ) + + m.fs.eff_4.costing.costing_package.cost_flow( + -pyunits.convert( + ( + m.fs.eff_4.work_mechanical[0] + / _compute_steam_properties(m.fs.eff_4.costing) + ), + to_units=pyunits.m**3 / pyunits.s, + ), + "steam", + ) + + m.fs.costing.cost_process() + + feed_vol_flow_rates = sum(i.properties_in[0].flow_vol_phase["Liq"] for i in effs) + + # Add a term for the unit cost of treated brine ($/m3) + m.fs.levelized_cost_of_feed_brine = Expression( + expr=( + ( + m.fs.costing.total_annualized_cost + + m.fs.costing.capital_recovery_factor + * (m.fs.capex_heat_exchanger + m.fs.capex_end_plates) + ) + / pyunits.convert( + feed_vol_flow_rates, to_units=pyunits.m**3 / pyunits.year + ) + ), + doc="Levelized cost of feed brine", + ) + + def multi_effect_crystallizer_initialization(m): # Set scaling factors m.fs.props.set_default_scaling("flow_mass_phase_comp", 1e-1, index=("Liq", "H2O")) @@ -356,6 +530,131 @@ def eqn_energy_from_eff3(b): return b.fs.eff_4.work_mechanical[0] == b.fs.eff_3.energy_flow_superheated_vapor +def get_model_performance(m): + # Print result + effs = [m.fs.eff_1, m.fs.eff_2, m.fs.eff_3, m.fs.eff_4] + effect_names = ["Effect 1", "Effect 2", "Effect 3", "Effect 4"] + feed_salinities = [ + i.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"].value for i in effs + ] + feed_flow_rates = [ + sum( + i.properties_in[0].flow_mass_phase_comp["Liq", j].value + for j in ["H2O", "NaCl"] + ) + for i in effs + ] + feed_vol_flow_rates = [ + i.properties_in[0].flow_vol_phase["Liq"].value * 1000 for i in effs + ] + temp_operating = [i.temperature_operating.value - 273.15 for i in effs] + temp_vapor_cond = [ + i.properties_pure_water[0].temperature.value - 273.15 for i in effs + ] + p_operating = [i.pressure_operating.value / 1e5 for i in effs] + water_prod = [ + i.properties_vapor[0].flow_mass_phase_comp["Vap", "H2O"].value for i in effs + ] + solid_prod = [ + i.properties_solids[0].flow_mass_phase_comp["Sol", "NaCl"].value for i in effs + ] + liquid_prod = [ + sum( + i.properties_out[0].flow_mass_phase_comp["Liq", j].value + for j in ["H2O", "NaCl"] + ) + for i in effs + ] + liquid_flow_rate = [ + i.properties_out[0].flow_vol_phase["Liq"].value * 1000 for i in effs + ] + + liquid_salinity = [ + i.properties_out[0].conc_mass_phase_comp["Liq", "NaCl"].value for i in effs + ] + power_required = [i.work_mechanical[0].value for i in effs] + power_provided = [i.energy_flow_superheated_vapor.value for i in effs] + vapor_enth = [ + i.properties_vapor[0].dh_vap_mass_solvent.value + * i.properties_vapor[0].flow_mass_phase_comp["Vap", "H2O"].value + for i in effs + ] + STEC = [ + i.work_mechanical[0].value + / i.properties_in[0].flow_vol_phase["Liq"].value + / 3600 + for i in effs + ] + + overall_STEC = ( + m.fs.eff_1.work_mechanical[0].value + / sum(i.properties_in[0].flow_vol_phase["Liq"].value for i in effs) + / 3600 + ) + + area = [i.area.value for i in effs] + + model_output = np.array( + [ + feed_flow_rates, + feed_vol_flow_rates, + feed_salinities, + temp_operating, + temp_vapor_cond, + p_operating, + water_prod, + solid_prod, + liquid_prod, + liquid_flow_rate, + liquid_salinity, + power_required, + power_provided, + vapor_enth, + STEC, + area, + ] + ) + + data_table = pd.DataFrame( + data=model_output, + columns=effect_names, + index=[ + "Feed mass flow rate (kg/s)", + "Feed volumetric flow rate (L/s)", + "Feed salinities (g/L)", + "Operating temperature (C)", + "Vapor condensation temperature (C)", + "Operating pressure (bar)", + "Water production (kg/s)", + "Solid production (kg/s)", + "Liquid waste (kg/s)", + "Liquid waste volumetric flow rate (L/s)", + "Liquid waste salinity (g/L)", + "Thermal energy requirement (kW)", + "Thermal energy available from vapor (kW)", + "Vapor enthalpy (kJ)", + "STEC (kWh/m3 feed)", + "Heat transfer area (m2)", + ], + ) + overall_performance = { + "Capacity (m3/day)": sum(feed_vol_flow_rates) * 86400 / 1000, + "Feed brine salinity (g/L)": m.fs.eff_1.properties_in[0] + .conc_mass_phase_comp["Liq", "NaCl"] + .value, + "Total brine disposed (kg/s)": sum(feed_flow_rates), + "Total water production (kg/s)": sum(water_prod), + "Total solids collected (kg/s)": sum(solid_prod), + "Total waste water remained (kg/s)": sum(liquid_prod), + "Initial thermal energy consumption (kW)": m.fs.eff_1.work_mechanical[0].value, + "Overall STEC (kWh/m3 feed)": overall_STEC, + "Total heat transfer area (m2)": sum(i.area.value for i in effs), + "Levelized cost of feed brine ($/m3)": value(m.fs.levelized_cost_of_feed_brine), + } + + return data_table, overall_performance + + if __name__ == "__main__": m = build_fs_multi_effect_crystallizer( operating_pressure_eff1=0.45, # bar @@ -363,19 +662,23 @@ def eqn_energy_from_eff3(b): operating_pressure_eff3=0.208, # bar operating_pressure_eff4=0.095, # bar feed_flow_mass=1, # kg/s - feed_mass_frac_NaCl=0.3, + feed_mass_frac_NaCl=0.15, feed_pressure=101325, # Pa feed_temperature=273.15 + 20, # K crystallizer_yield=0.5, steam_pressure=1.5, # bar (gauge pressure) ) + add_costings(m) - multi_effect_crystallizer_initialization(m) + # Negative value for salt recovery value ($/kg) + m.fs.costing.crystallizer.NaCl_recovery_value.fix(-0.024) - print(degrees_of_freedom(m)) - solver = get_solver() + multi_effect_crystallizer_initialization(m) results = solver.solve(m) + # Check for optimal solution assert results.solver.termination_condition == TerminationCondition.optimal assert results.solver.status == SolverStatus.ok + + data_table, overall_performance = get_model_performance(m) diff --git a/src/watertap_contrib/reflo/analysis/example_flowsheets/test/test_multi_effect_NaCl_crystallizer.py b/src/watertap_contrib/reflo/analysis/example_flowsheets/test/test_multi_effect_NaCl_crystallizer.py new file mode 100644 index 00000000..9e73f782 --- /dev/null +++ b/src/watertap_contrib/reflo/analysis/example_flowsheets/test/test_multi_effect_NaCl_crystallizer.py @@ -0,0 +1,231 @@ +################################################################################# +# WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# National Renewable Energy Laboratory, and National Energy Technology +# Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +################################################################################# + +import pytest +from pyomo.environ import ( + ConcreteModel, + TerminationCondition, + SolverStatus, + value, + Var, + Objective, +) +from pyomo.network import Port +from pyomo.util.check_units import assert_units_consistent + +from idaes.core import FlowsheetBlock +from idaes.core.util.model_statistics import ( + degrees_of_freedom, + number_variables, + number_total_constraints, + number_unused_variables, +) +from idaes.core.util.testing import initialization_tester +from idaes.core.util.scaling import ( + calculate_scaling_factors, + unscaled_variables_generator, + badly_scaled_var_generator, +) +from idaes.core import UnitModelCostingBlock + +from watertap_contrib.reflo.analysis.example_flowsheets.multi_effect_NaCl_crystallizer import ( + build_fs_multi_effect_crystallizer, + add_costings, + multi_effect_crystallizer_initialization, + get_model_performance, +) +import watertap_contrib.reflo.property_models.cryst_prop_pack as props +from watertap.core.solvers import get_solver +from watertap_contrib.reflo.costing import TreatmentCosting, CrystallizerCostType + +solver = get_solver() + + +class TestMultiEffectCrystallization: + @pytest.fixture(scope="class") + def MultiEffectCrystallizer_frame(self): + m = build_fs_multi_effect_crystallizer( + operating_pressure_eff1=0.45, # bar + operating_pressure_eff2=0.25, # bar + operating_pressure_eff3=0.208, # bar + operating_pressure_eff4=0.095, # bar + feed_flow_mass=1, # kg/s + feed_mass_frac_NaCl=0.15, + feed_pressure=101325, # Pa + feed_temperature=273.15 + 20, # K + crystallizer_yield=0.5, + steam_pressure=1.5, # bar (gauge pressure) + ) + + add_costings(m) + # Negative value to represent salt recovery value ($/kg) + m.fs.costing.crystallizer.NaCl_recovery_value.fix(-0.024) + + multi_effect_crystallizer_initialization(m) + + return m + + @pytest.mark.unit + def test_dof(self, MultiEffectCrystallizer_frame): + m = MultiEffectCrystallizer_frame + assert degrees_of_freedom(m) == 0 + + @pytest.mark.component + def test_solve(self, MultiEffectCrystallizer_frame): + m = MultiEffectCrystallizer_frame + results = solver.solve(m) + + # Check for optimal solution + assert results.solver.termination_condition == TerminationCondition.optimal + assert results.solver.status == SolverStatus.ok + + @pytest.mark.component + def test_solution(self, MultiEffectCrystallizer_frame): + m = MultiEffectCrystallizer_frame + + data_table, overall_performance = get_model_performance(m) + + # Check solid mass in solids stream + assert ( + pytest.approx(overall_performance["Capacity (m3/day)"], rel=1e-3) == 277.39 + ) + assert ( + pytest.approx(overall_performance["Feed brine salinity (g/L)"], rel=1e-3) + == 166.3 + ) + assert ( + pytest.approx(overall_performance["Total brine disposed (kg/s)"], rel=1e-3) + == 3.559 + ) + assert ( + pytest.approx( + overall_performance["Total water production (kg/s)"], rel=1e-3 + ) + == 2.313 + ) + assert ( + pytest.approx( + overall_performance["Total solids collected (kg/s)"], rel=1e-3 + ) + == 0.267 + ) + assert ( + pytest.approx( + overall_performance["Total waste water remained (kg/s)"], rel=1e-3 + ) + == 0.979 + ) + assert ( + pytest.approx( + overall_performance["Initial thermal energy consumption (kW)"], rel=1e-3 + ) + == 1704.47 + ) + assert ( + pytest.approx(overall_performance["Overall STEC (kWh/m3 feed)"], rel=1e-3) + == 147.47 + ) + assert ( + pytest.approx( + overall_performance["Total heat transfer area (m2)"], rel=1e-3 + ) + == 2.03 + ) + assert ( + pytest.approx( + overall_performance["Levelized cost of feed brine ($/m3)"], rel=1e-3 + ) + == 1.719 + ) + + @pytest.mark.component + def test_optimization(self, MultiEffectCrystallizer_frame): + m = MultiEffectCrystallizer_frame + + # optimization scenario + m.fs.eff_1.pressure_operating.unfix() + m.fs.eff_1.pressure_operating.setub(1.2 * 1e5) + m.fs.eff_2.pressure_operating.unfix() + m.fs.eff_3.pressure_operating.unfix() + m.fs.eff_4.pressure_operating.unfix() + m.fs.eff_4.pressure_operating.setlb(0.02 * 1e5) + + effs = [m.fs.eff_1, m.fs.eff_2, m.fs.eff_3, m.fs.eff_4] + total_area = sum(i.area for i in effs) + + # Optimize the heat transfer area + m.fs.objective = Objective(expr=total_area) + + @m.Constraint(doc="Pressure decreasing") + def pressure_bound1(b): + return b.fs.eff_2.pressure_operating <= b.fs.eff_1.pressure_operating + + @m.Constraint(doc="Pressure decreasing") + def pressure_bound2(b): + return b.fs.eff_3.pressure_operating <= b.fs.eff_2.pressure_operating + + @m.Constraint(doc="Temperature difference") + def temp_bound1(b): + return ( + b.fs.eff_2.temperature_operating + >= b.fs.eff_1.temperature_operating - 12 + ) + + @m.Constraint(doc="Temperature difference") + def temp_bound2(b): + return ( + b.fs.eff_3.temperature_operating + >= b.fs.eff_2.temperature_operating - 12 + ) + + @m.Constraint(doc="Pressure decreasing") + def pressure_bound3(b): + return b.fs.eff_4.pressure_operating <= b.fs.eff_3.pressure_operating + + @m.Constraint(doc="Temperature difference") + def temp_bound3(b): + return ( + b.fs.eff_4.temperature_operating + >= b.fs.eff_3.temperature_operating - 12 + ) + + optimization_results = solver.solve(m, tee=False) + assert ( + optimization_results.solver.termination_condition + == TerminationCondition.optimal + ) + data_table2, overall_performance2 = get_model_performance(m) + + assert ( + pytest.approx( + data_table2["Effect 1"]["Operating temperature (C)"], rel=1e-3 + ) + == 113.86 + ) + assert ( + pytest.approx( + data_table2["Effect 2"]["Operating temperature (C)"], rel=1e-3 + ) + == 101.86 + ) + assert ( + pytest.approx( + data_table2["Effect 3"]["Operating temperature (C)"], rel=1e-3 + ) + == 89.86 + ) + assert ( + pytest.approx( + data_table2["Effect 4"]["Operating temperature (C)"], rel=1e-3 + ) + == 77.86 + ) diff --git a/src/watertap_contrib/reflo/costing/__init__.py b/src/watertap_contrib/reflo/costing/__init__.py index b80c8fac..90602c65 100644 --- a/src/watertap_contrib/reflo/costing/__init__.py +++ b/src/watertap_contrib/reflo/costing/__init__.py @@ -17,3 +17,5 @@ TreatmentCosting, EnergyCosting, ) + +from .units.crystallizer_watertap import CrystallizerCostType, _compute_steam_properties diff --git a/src/watertap_contrib/reflo/costing/units/crystallizer_watertap.py b/src/watertap_contrib/reflo/costing/units/crystallizer_watertap.py new file mode 100644 index 00000000..67d26c98 --- /dev/null +++ b/src/watertap_contrib/reflo/costing/units/crystallizer_watertap.py @@ -0,0 +1,289 @@ +################################################################################# +# WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# National Renewable Energy Laboratory, and National Energy Technology +# Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +################################################################################# + +import pyomo.environ as pyo +from idaes.core.util.misc import StrEnum +from idaes.core.util.constants import Constants +from watertap.costing.util import register_costing_parameter_block +from watertap_contrib.reflo.costing.util import ( + make_capital_cost_var, + make_fixed_operating_cost_var, +) + + +class CrystallizerCostType(StrEnum): + default = "default" + mass_basis = "mass_basis" + volume_basis = "volume_basis" + + +def build_crystallizer_watertap_cost_param_block(blk): + + blk.steam_pressure = pyo.Var( + initialize=3, + units=pyo.units.bar, + doc="Steam pressure (gauge) for crystallizer heating: 3 bar default based on Dutta example", + ) + + blk.efficiency_pump = pyo.Var( + initialize=0.7, + units=pyo.units.dimensionless, + doc="Crystallizer pump efficiency - assumed", + ) + + blk.pump_head_height = pyo.Var( + initialize=1, + units=pyo.units.m, + doc="Crystallizer pump head height - assumed, unvalidated", + ) + + # Crystallizer operating cost information from literature + blk.fob_unit_cost = pyo.Var( + initialize=675000, + doc="Forced circulation crystallizer reference free-on-board cost (Woods, 2007)", + units=pyo.units.USD_2007, + ) + + blk.ref_capacity = pyo.Var( + initialize=1, + doc="Forced circulation crystallizer reference crystal capacity (Woods, 2007)", + units=pyo.units.kg / pyo.units.s, + ) + + blk.ref_exponent = pyo.Var( + initialize=0.53, + doc="Forced circulation crystallizer cost exponent factor (Woods, 2007)", + units=pyo.units.dimensionless, + ) + + blk.iec_percent = pyo.Var( + initialize=1.43, + doc="Forced circulation crystallizer installed equipment cost (Diab and Gerogiorgis, 2017)", + units=pyo.units.dimensionless, + ) + + blk.volume_cost = pyo.Var( + initialize=16320, + doc="Forced circulation crystallizer cost per volume (Yusuf et al., 2019)", + units=pyo.units.USD_2007, ## TODO: Needs confirmation, but data is from Perry apparently + ) + + blk.vol_basis_exponent = pyo.Var( + initialize=0.47, + doc="Forced circulation crystallizer volume-based cost exponent (Yusuf et al., 2019)", + units=pyo.units.dimensionless, + ) + + blk.steam_cost = pyo.Var( + initialize=0.004, + units=pyo.units.USD_2018 / (pyo.units.meter**3), + doc="Steam cost, Panagopoulos (2019)", + ) + + blk.NaCl_recovery_value = pyo.Var( + initialize=0, + units=pyo.units.USD_2018 / pyo.units.kg, + doc="Unit recovery value of NaCl", + ) + + costing = blk.parent_block() + costing.register_flow_type("steam", blk.steam_cost) + costing.register_flow_type("NaCl", blk.NaCl_recovery_value) + + +def cost_crystallizer_watertap(blk, cost_type=CrystallizerCostType.default): + """ + Function for costing the FC crystallizer by the mass flow of produced crystals. + The operating cost model assumes that heat is supplied via condensation of saturated steam (see Dutta et al.) + + Args: + cost_type: Option for crystallizer cost function type - volume or mass basis + """ + if ( + cost_type == CrystallizerCostType.default + or cost_type == CrystallizerCostType.mass_basis + ): + cost_crystallizer_by_crystal_mass(blk) + elif cost_type == CrystallizerCostType.volume_basis: + cost_crystallizer_by_volume(blk) + else: + raise ConfigurationError( + f"{blk.unit_model.name} received invalid argument for cost_type:" + f" {cost_type}. Argument must be a member of the CrystallizerCostType Enum." + ) + + +def _cost_crystallizer_flows(blk): + blk.costing_package.cost_flow( + pyo.units.convert( + ( + blk.unit_model.magma_circulation_flow_vol + * blk.unit_model.dens_mass_slurry + * Constants.acceleration_gravity + * blk.costing_package.crystallizer.pump_head_height + / blk.costing_package.crystallizer.efficiency_pump + ), + to_units=pyo.units.kW, + ), + "electricity", + ) + + blk.costing_package.cost_flow( + pyo.units.convert( + (blk.unit_model.work_mechanical[0] / _compute_steam_properties(blk)), + to_units=pyo.units.m**3 / pyo.units.s, + ), + "steam", + ) + + blk.costing_package.cost_flow( + blk.unit_model.solids.flow_mass_phase_comp[0, "Sol", "NaCl"], + "NaCl", + ) + + +@register_costing_parameter_block( + build_rule=build_crystallizer_watertap_cost_param_block, + parameter_block_name="crystallizer", +) +def cost_crystallizer_by_crystal_mass(blk): + """ + Mass-based capital cost for FC crystallizer + """ + make_capital_cost_var(blk) + blk.costing_package.add_cost_factor(blk, "TIC") + blk.capital_cost_constraint = pyo.Constraint( + expr=blk.capital_cost + == blk.cost_factor + * pyo.units.convert( + ( + blk.costing_package.crystallizer.iec_percent + * blk.costing_package.crystallizer.fob_unit_cost + * ( + sum( + blk.unit_model.solids.flow_mass_phase_comp[0, "Sol", j] + for j in blk.unit_model.config.property_package.solute_set + ) + / blk.costing_package.crystallizer.ref_capacity + ) + ** blk.costing_package.crystallizer.ref_exponent + ), + to_units=blk.costing_package.base_currency, + ) + ) + _cost_crystallizer_flows(blk) + + +@register_costing_parameter_block( + build_rule=build_crystallizer_watertap_cost_param_block, + parameter_block_name="crystallizer", +) +def cost_crystallizer_by_volume(blk): + """ + Volume-based capital cost for FC crystallizer + """ + make_capital_cost_var(blk) + blk.costing_package.add_cost_factor(blk, "TIC") + blk.capital_cost_constraint = pyo.Constraint( + expr=blk.capital_cost + == blk.cost_factor + * pyo.units.convert( + ( + blk.costing_package.crystallizer.volume_cost + * ( + ( + pyo.units.convert( + blk.unit_model.volume_suspension + * ( + blk.unit_model.height_crystallizer + / blk.unit_model.height_slurry + ), + to_units=(pyo.units.ft) ** 3, + ) + ) + / pyo.units.ft**3 + ) + ** blk.costing_package.crystallizer.vol_basis_exponent + ), + to_units=blk.costing_package.base_currency, + ) + ) + _cost_crystallizer_flows(blk) + + +def _compute_steam_properties(blk): + """ + Function for computing saturated steam properties for thermal heating estimation. + + Args: + pressure_sat: Steam gauge pressure in bar + + Out: + Steam thermal capacity (latent heat of condensation * density) in kJ/m3 + """ + pressure_sat = blk.costing_package.crystallizer.steam_pressure + # 1. Compute saturation temperature of steam: computed from El-Dessouky expression + tsat_constants = [ + 42.6776 * pyo.units.K, + -3892.7 * pyo.units.K, + 1000 * pyo.units.kPa, + -9.48654 * pyo.units.dimensionless, + ] + psat = ( + pyo.units.convert(pressure_sat, to_units=pyo.units.kPa) + + 101.325 * pyo.units.kPa + ) + temperature_sat = tsat_constants[0] + tsat_constants[1] / ( + pyo.log(psat / tsat_constants[2]) + tsat_constants[3] + ) + + # 2. Compute latent heat of condensation/vaporization: computed from Sharqawy expression + t = temperature_sat - 273.15 * pyo.units.K + enth_mass_units = pyo.units.J / pyo.units.kg + t_inv_units = pyo.units.K**-1 + dh_constants = [ + 2.501e6 * enth_mass_units, + -2.369e3 * enth_mass_units * t_inv_units**1, + 2.678e-1 * enth_mass_units * t_inv_units**2, + -8.103e-3 * enth_mass_units * t_inv_units**3, + -2.079e-5 * enth_mass_units * t_inv_units**4, + ] + dh_vap = ( + dh_constants[0] + + dh_constants[1] * t + + dh_constants[2] * t**2 + + dh_constants[3] * t**3 + + dh_constants[4] * t**4 + ) + dh_vap = pyo.units.convert(dh_vap, to_units=pyo.units.kJ / pyo.units.kg) + + # 3. Compute specific volume: computed from Affandi expression (Eq 5) + t_critical = 647.096 * pyo.units.K + t_red = temperature_sat / t_critical # Reduced temperature + sp_vol_constants = [ + -7.75883 * pyo.units.dimensionless, + 3.23753 * pyo.units.dimensionless, + 2.05755 * pyo.units.dimensionless, + -0.06052 * pyo.units.dimensionless, + 0.00529 * pyo.units.dimensionless, + ] + log_sp_vol = ( + sp_vol_constants[0] + + sp_vol_constants[1] * (pyo.log(1 / t_red)) ** 0.4 + + sp_vol_constants[2] / (t_red**2) + + sp_vol_constants[3] / (t_red**4) + + sp_vol_constants[4] / (t_red**5) + ) + sp_vol = pyo.exp(log_sp_vol) * pyo.units.m**3 / pyo.units.kg + + # 4. Return specific energy: density * latent heat + return dh_vap / sp_vol diff --git a/src/watertap_contrib/reflo/unit_models/zero_order/crystallizer_zo_watertap.py b/src/watertap_contrib/reflo/unit_models/zero_order/crystallizer_zo_watertap.py index 6025e8e5..553a9be0 100644 --- a/src/watertap_contrib/reflo/unit_models/zero_order/crystallizer_zo_watertap.py +++ b/src/watertap_contrib/reflo/unit_models/zero_order/crystallizer_zo_watertap.py @@ -41,7 +41,9 @@ from watertap.core import InitializationMixin from watertap.core.util.initialization import interval_initializer -from watertap.costing.unit_models.crystallizer import cost_crystallizer +from watertap_contrib.reflo.costing.units.crystallizer_watertap import ( + cost_crystallizer_watertap, +) _log = idaeslog.getLogger(__name__) @@ -865,4 +867,4 @@ def _get_performance_contents(self, time_point=0): @property def default_costing_method(self): - return cost_crystallizer + return cost_crystallizer_watertap diff --git a/src/watertap_contrib/reflo/unit_models/zero_order/tests/test_crystallizer_watertap.py b/src/watertap_contrib/reflo/unit_models/zero_order/tests/test_crystallizer_watertap.py index 543d254c..aa714dda 100644 --- a/src/watertap_contrib/reflo/unit_models/zero_order/tests/test_crystallizer_watertap.py +++ b/src/watertap_contrib/reflo/unit_models/zero_order/tests/test_crystallizer_watertap.py @@ -26,9 +26,8 @@ ) import watertap_contrib.reflo.property_models.cryst_prop_pack as props -from idaes.core.solvers import get_solver +from watertap.core.solvers import get_solver -# from watertap.core.solvers import get_solver from idaes.core.util.model_statistics import ( degrees_of_freedom, number_variables, @@ -42,8 +41,7 @@ badly_scaled_var_generator, ) from idaes.core import UnitModelCostingBlock - -# from watertap.costing import WaterTAPCosting, CrystallizerCostType +from watertap_contrib.reflo.costing import TreatmentCosting, CrystallizerCostType # ----------------------------------------------------------------------------- # Get default solver for testing @@ -326,17 +324,16 @@ def test_calculate_scaling(self, Crystallizer_frame): for _ in badly_scaled_var_generator(m): assert False - @pytest.mark.requires_idaes_solver @pytest.mark.component def test_initialize(self, Crystallizer_frame): # Add costing function, then initialize m = Crystallizer_frame - # m.fs.costing = WaterTAPCosting() - # m.fs.unit.costing = UnitModelCostingBlock( - # flowsheet_costing_block=m.fs.costing, - # costing_method_arguments={"cost_type": CrystallizerCostType.mass_basis}, - # ) - # m.fs.costing.cost_process() + m.fs.costing = TreatmentCosting() + m.fs.unit.costing = UnitModelCostingBlock( + flowsheet_costing_block=m.fs.costing, + costing_method_arguments={"cost_type": CrystallizerCostType.mass_basis}, + ) + m.fs.costing.cost_process() initialization_tester(Crystallizer_frame) assert_units_consistent(m) @@ -347,7 +344,6 @@ def test_initialize(self, Crystallizer_frame): # badly_scaled_var_lst = list(badly_scaled_var_generator(m)) # assert badly_scaled_var_lst == [] - @pytest.mark.requires_idaes_solver @pytest.mark.component def test_solve(self, Crystallizer_frame): m = Crystallizer_frame @@ -357,7 +353,6 @@ def test_solve(self, Crystallizer_frame): assert results.solver.termination_condition == TerminationCondition.optimal assert results.solver.status == SolverStatus.ok - @pytest.mark.requires_idaes_solver @pytest.mark.component def test_conservation(self, Crystallizer_frame): m = Crystallizer_frame @@ -418,7 +413,6 @@ def test_conservation(self, Crystallizer_frame): <= 1e-2 ) - @pytest.mark.requires_idaes_solver @pytest.mark.component def test_solution(self, Crystallizer_frame): m = Crystallizer_frame @@ -458,11 +452,10 @@ def test_solution(self, Crystallizer_frame): # Residence time assert pytest.approx(1.0228, rel=1e-3) == value(b.t_res) # Mass-basis costing - assert pytest.approx(2.0 * 300000, rel=1e-3) == value( + assert pytest.approx(704557, rel=1e-3) == value( m.fs.costing.aggregate_capital_cost ) - @pytest.mark.requires_idaes_solver @pytest.mark.component def test_solution2_capcosting_by_mass(self, Crystallizer_frame): m = Crystallizer_frame @@ -489,7 +482,7 @@ def test_solution2_capcosting_by_mass(self, Crystallizer_frame): # Minimum active volume assert pytest.approx(0.959, rel=1e-3) == value(b.volume_suspension) # Mass-basis costing - assert pytest.approx(2.0 * 300000, rel=1e-3) == value( + assert pytest.approx(704557, rel=1e-3) == value( m.fs.costing.aggregate_capital_cost ) @@ -521,7 +514,7 @@ def test_solution2_capcosting_by_volume(self, Crystallizer_frame_2): initialization_tester(Crystallizer_frame_2) results = solver.solve(m) - m.fs.costing = WaterTAPCosting() + m.fs.costing = TreatmentCosting() m.fs.unit.costing = UnitModelCostingBlock( flowsheet_costing_block=m.fs.costing, costing_method_arguments={"cost_type": CrystallizerCostType.volume_basis}, @@ -547,7 +540,7 @@ def test_solution2_capcosting_by_volume(self, Crystallizer_frame_2): # Minimum active volume assert pytest.approx(0.959, rel=1e-3) == value(b.volume_suspension) # Volume-basis costing - assert pytest.approx(2.0 * 199000, rel=1e-3) == value( + assert pytest.approx(467490, rel=1e-3) == value( m.fs.costing.aggregate_capital_cost ) @@ -565,10 +558,10 @@ def test_solution2_operatingcost(self, Crystallizer_frame_2): assert results.solver.status == SolverStatus.ok # Operating cost validation - assert pytest.approx(835.41, rel=1e-3) == value( + assert pytest.approx(980.77, rel=1e-3) == value( m.fs.costing.aggregate_flow_costs["electricity"] ) - assert pytest.approx(30666.67, rel=1e-3) == value( + assert pytest.approx(36000.75, rel=1e-3) == value( m.fs.costing.aggregate_flow_costs["steam"] ) assert pytest.approx(0, rel=1e-3) == value( @@ -590,10 +583,10 @@ def test_solution2_operatingcost_steampressure(self, Crystallizer_frame_2): assert results.solver.status == SolverStatus.ok # Operating cost validation - assert pytest.approx(835.41, rel=1e-3) == value( + assert pytest.approx(980.77, rel=1e-3) == value( m.fs.costing.aggregate_flow_costs["electricity"] ) - assert pytest.approx(21451.91, rel=1e-3) == value( + assert pytest.approx(25183.20, rel=1e-3) == value( m.fs.costing.aggregate_flow_costs["steam"] ) assert pytest.approx(0, abs=1e-6) == value( @@ -613,175 +606,12 @@ def test_solution2_operatingcost_NaCl_revenue(self, Crystallizer_frame_2): assert results.solver.status == SolverStatus.ok # Operating cost validation - assert pytest.approx(835.41, rel=1e-3) == value( + assert pytest.approx(980.77, rel=1e-3) == value( m.fs.costing.aggregate_flow_costs["electricity"] ) - assert pytest.approx(30666.67, rel=1e-3) == value( + assert pytest.approx(36000.75, rel=1e-3) == value( m.fs.costing.aggregate_flow_costs["steam"] ) - assert pytest.approx(-187858.2, rel=1e-3) == value( + assert pytest.approx(-220533.25, rel=1e-3) == value( m.fs.costing.aggregate_flow_costs["NaCl"] ) - - -if __name__ == "__main__": - m = ConcreteModel() - m.fs = FlowsheetBlock(dynamic=False) - - m.fs.properties = props.NaClParameterBlock() - - m.fs.unit = Crystallization(property_package=m.fs.properties) - - # fully specify system - feed_flow_mass = 1 - feed_mass_frac_NaCl = 0.2126 - feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl - feed_pressure = 101325 - feed_temperature = 273.15 + 20 - eps = 1e-6 - crystallizer_temperature = 273.15 + 55 - crystallizer_yield = 0.40 - - # Fully define feed - m.fs.unit.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"].fix( - feed_flow_mass * feed_mass_frac_NaCl - ) - m.fs.unit.inlet.flow_mass_phase_comp[0, "Liq", "H2O"].fix( - feed_flow_mass * feed_mass_frac_H2O - ) - m.fs.unit.inlet.flow_mass_phase_comp[0, "Sol", "NaCl"].fix(eps) - m.fs.unit.inlet.flow_mass_phase_comp[0, "Vap", "H2O"].fix(eps) - m.fs.unit.inlet.pressure[0].fix(feed_pressure) - m.fs.unit.inlet.temperature[0].fix(feed_temperature) - - # Define operating conditions - m.fs.unit.temperature_operating.fix(crystallizer_temperature) - m.fs.unit.crystallization_yield["NaCl"].fix(crystallizer_yield) - - # Fix growth rate, crystal length and Sounders brown constant to default values - m.fs.unit.crystal_growth_rate.fix() - m.fs.unit.souders_brown_constant.fix() - m.fs.unit.crystal_median_length.fix() - - assert_units_consistent(m) - - m.fs.properties.set_default_scaling( - "flow_mass_phase_comp", 1e-1, index=("Liq", "H2O") - ) - m.fs.properties.set_default_scaling( - "flow_mass_phase_comp", 1e-1, index=("Liq", "NaCl") - ) - m.fs.properties.set_default_scaling( - "flow_mass_phase_comp", 1e-1, index=("Vap", "H2O") - ) - m.fs.properties.set_default_scaling( - "flow_mass_phase_comp", 1e-1, index=("Sol", "NaCl") - ) - calculate_scaling_factors(m) - - m.fs.unit.initialize() - - results = solver.solve(m) - - # Check for optimal solution - assert results.solver.termination_condition == TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok - - -if __name__ == "__main__": - m = ConcreteModel() - m.fs = FlowsheetBlock(dynamic=False) - - m.fs.properties = props.NaClParameterBlock() - - m.fs.unit = Crystallization(property_package=m.fs.properties) - - # fully specify system - feed_flow_mass = 1 - feed_mass_frac_NaCl = 0.2126 - feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl - feed_pressure = 101325 - feed_temperature = 273.15 + 20 - eps = 1e-6 - crystallizer_temperature = 273.15 + 55 - crystallizer_yield = 0.40 - - # Fully define feed - m.fs.unit.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"].fix( - feed_flow_mass * feed_mass_frac_NaCl - ) - m.fs.unit.inlet.flow_mass_phase_comp[0, "Liq", "H2O"].fix( - feed_flow_mass * feed_mass_frac_H2O - ) - m.fs.unit.inlet.flow_mass_phase_comp[0, "Sol", "NaCl"].fix(eps) - m.fs.unit.inlet.flow_mass_phase_comp[0, "Vap", "H2O"].fix(eps) - m.fs.unit.inlet.pressure[0].fix(feed_pressure) - m.fs.unit.inlet.temperature[0].fix(feed_temperature) - - # Define operating conditions - m.fs.unit.temperature_operating.fix(crystallizer_temperature) - m.fs.unit.crystallization_yield["NaCl"].fix(crystallizer_yield) - - # Fix growth rate, crystal length and Sounders brown constant to default values - m.fs.unit.crystal_growth_rate.fix() - m.fs.unit.souders_brown_constant.fix() - m.fs.unit.crystal_median_length.fix() - - assert_units_consistent(m) - - m.fs.properties.set_default_scaling( - "flow_mass_phase_comp", 1e-1, index=("Liq", "H2O") - ) - m.fs.properties.set_default_scaling( - "flow_mass_phase_comp", 1e-1, index=("Liq", "NaCl") - ) - m.fs.properties.set_default_scaling( - "flow_mass_phase_comp", 1e-1, index=("Vap", "H2O") - ) - m.fs.properties.set_default_scaling( - "flow_mass_phase_comp", 1e-1, index=("Sol", "NaCl") - ) - calculate_scaling_factors(m) - - m.fs.unit.initialize() - - results = solver.solve(m) - - # Check for optimal solution - assert results.solver.termination_condition == TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok - - b = m.fs.unit - assert pytest.approx( - value( - b.crystallization_yield["NaCl"] - * b.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"] - ), - rel=1e-3, - ) == value(b.solids.flow_mass_phase_comp[0, "Sol", "NaCl"]) - # Check solid mass in liquid stream - assert pytest.approx( - value( - (1 - b.crystallization_yield["NaCl"]) - * b.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"] - ), - rel=1e-3, - ) == value(b.outlet.flow_mass_phase_comp[0, "Liq", "NaCl"]) - # Check outlet liquid stream composition which is set by solubility - assert pytest.approx(0.2695, rel=1e-3) == value( - b.properties_out[0].mass_frac_phase_comp["Liq", "NaCl"] - ) - # Check liquid stream solvent flow - assert pytest.approx(0.12756 * ((1 / 0.2695) - 1), rel=1e-3) == value( - b.outlet.flow_mass_phase_comp[0, "Liq", "H2O"] - ) - # Check saturation pressure - assert pytest.approx(11992, rel=1e-3) == value(b.pressure_operating) - # Check heat requirement - assert pytest.approx(1127.2, rel=1e-3) == value(b.work_mechanical[0]) - # Check crystallizer diameter - assert pytest.approx(1.205, rel=1e-3) == value(b.diameter_crystallizer) - # Minimum active volume - assert pytest.approx(1.619, rel=1e-3) == value(b.volume_suspension) - # Residence time - assert pytest.approx(1.0228, rel=1e-3) == value(b.t_res) From 43a30398587cfd83a74e18f54f874eba0de06c49 Mon Sep 17 00:00:00 2001 From: zhuoran29 Date: Mon, 12 Aug 2024 22:36:40 -0400 Subject: [PATCH 04/80] change prop package path to reflo --- .../reflo/property_models/tests/test_cryst_prop_pack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/watertap_contrib/reflo/property_models/tests/test_cryst_prop_pack.py b/src/watertap_contrib/reflo/property_models/tests/test_cryst_prop_pack.py index 0de9daa2..a27c0847 100644 --- a/src/watertap_contrib/reflo/property_models/tests/test_cryst_prop_pack.py +++ b/src/watertap_contrib/reflo/property_models/tests/test_cryst_prop_pack.py @@ -10,7 +10,7 @@ # "https://github.com/watertap-org/watertap/" ################################################################################# import pytest -import watertap.property_models.unit_specific.cryst_prop_pack as props +import watertap_contrib.reflo.property_models.cryst_prop_pack as props from pyomo.environ import ConcreteModel from idaes.core import FlowsheetBlock, ControlVolume0DBlock from idaes.models.properties.tests.test_harness import ( From 901aa664edb01f501ebdbd22f0c472467075d1d5 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Wed, 11 Sep 2024 09:38:03 -0600 Subject: [PATCH 05/80] new black --- .../example_flowsheets/multi_effect_NaCl_crystallizer.py | 4 +--- .../reflo/property_models/cryst_prop_pack.py | 7 ++----- .../unit_models/zero_order/crystallizer_zo_watertap.py | 6 +++--- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/watertap_contrib/reflo/analysis/example_flowsheets/multi_effect_NaCl_crystallizer.py b/src/watertap_contrib/reflo/analysis/example_flowsheets/multi_effect_NaCl_crystallizer.py index 44777999..7077d913 100644 --- a/src/watertap_contrib/reflo/analysis/example_flowsheets/multi_effect_NaCl_crystallizer.py +++ b/src/watertap_contrib/reflo/analysis/example_flowsheets/multi_effect_NaCl_crystallizer.py @@ -484,9 +484,7 @@ def add_costings(m): + m.fs.costing.capital_recovery_factor * (m.fs.capex_heat_exchanger + m.fs.capex_end_plates) ) - / pyunits.convert( - feed_vol_flow_rates, to_units=pyunits.m**3 / pyunits.year - ) + / pyunits.convert(feed_vol_flow_rates, to_units=pyunits.m**3 / pyunits.year) ), doc="Levelized cost of feed brine", ) diff --git a/src/watertap_contrib/reflo/property_models/cryst_prop_pack.py b/src/watertap_contrib/reflo/property_models/cryst_prop_pack.py index 54a863cc..a81eb8de 100644 --- a/src/watertap_contrib/reflo/property_models/cryst_prop_pack.py +++ b/src/watertap_contrib/reflo/property_models/cryst_prop_pack.py @@ -1363,8 +1363,7 @@ def rule_cp_mass_solvent(b, p): C = b.params.cp_phase_param_C1 D = b.params.cp_phase_param_D1 return ( - b.cp_mass_solvent["Liq"] - == (A + B * t + C * t**2 + D * t**3) * 1000 + b.cp_mass_solvent["Liq"] == (A + B * t + C * t**2 + D * t**3) * 1000 ) elif p == "Vap": t = b.temperature / 1000 @@ -1551,9 +1550,7 @@ def rule_pressure_sat(b): # vapor pressure, eq6 in Sparrow (2003) + (b.params.pressure_sat_param_E5 * x**4) ) - p_sat = ( - ps_a + (ps_b * t) + (ps_c * t**2) + (ps_d * t**3) + (ps_e * t**4) - ) + p_sat = ps_a + (ps_b * t) + (ps_c * t**2) + (ps_d * t**3) + (ps_e * t**4) return b.pressure_sat == pyunits.convert(p_sat, to_units=pyunits.Pa) self.eq_pressure_sat = Constraint(rule=rule_pressure_sat) diff --git a/src/watertap_contrib/reflo/unit_models/zero_order/crystallizer_zo_watertap.py b/src/watertap_contrib/reflo/unit_models/zero_order/crystallizer_zo_watertap.py index 553a9be0..77cccf5b 100644 --- a/src/watertap_contrib/reflo/unit_models/zero_order/crystallizer_zo_watertap.py +++ b/src/watertap_contrib/reflo/unit_models/zero_order/crystallizer_zo_watertap.py @@ -849,9 +849,9 @@ def _get_performance_contents(self, time_point=0): var_dict["Heat requirement"] = self.work_mechanical[time_point] var_dict["Crystallizer diameter"] = self.diameter_crystallizer var_dict["Magma circulation flow rate"] = self.magma_circulation_flow_vol - var_dict[ - "Vol. frac. of solids in suspension, 1-E" - ] = self.product_volumetric_solids_fraction + var_dict["Vol. frac. of solids in suspension, 1-E"] = ( + self.product_volumetric_solids_fraction + ) var_dict["Residence time"] = self.t_res var_dict["Crystallizer minimum active volume"] = self.volume_suspension var_dict["Suspension height in crystallizer"] = self.height_slurry From 7c62034a2ca0c4a240b43d8a78d1d4937cbb0a9b Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Wed, 11 Sep 2024 14:00:05 -0600 Subject: [PATCH 06/80] clean up imports; spelling --- .../multi_effect_NaCl_crystallizer.py | 57 ++++++------------- 1 file changed, 17 insertions(+), 40 deletions(-) diff --git a/src/watertap_contrib/reflo/analysis/example_flowsheets/multi_effect_NaCl_crystallizer.py b/src/watertap_contrib/reflo/analysis/example_flowsheets/multi_effect_NaCl_crystallizer.py index 7077d913..b14157b2 100644 --- a/src/watertap_contrib/reflo/analysis/example_flowsheets/multi_effect_NaCl_crystallizer.py +++ b/src/watertap_contrib/reflo/analysis/example_flowsheets/multi_effect_NaCl_crystallizer.py @@ -1,47 +1,24 @@ import pandas as pd import numpy as np -import pytest + from pyomo.environ import ( ConcreteModel, TerminationCondition, SolverStatus, - Objective, Expression, - maximize, value, - Set, Var, log, units as pyunits, ) -from pyomo.network import Port -from pyomo.util.check_units import assert_units_consistent -from idaes.core import FlowsheetBlock +from idaes.core import FlowsheetBlock, UnitModelCostingBlock import idaes.core.util.scaling as iscale + +from watertap.core.solvers import get_solver from watertap.unit_models.mvc.components.lmtd_chen_callback import ( delta_temperature_chen_callback, ) -from idaes.core.util.model_statistics import ( - degrees_of_freedom, - number_variables, - number_total_constraints, - number_unused_variables, -) -from idaes.models.unit_models import HeatExchanger -from idaes.models.unit_models.heat_exchanger import ( - delta_temperature_lmtd_callback, - delta_temperature_underwood_callback, -) -from idaes.core.util.testing import initialization_tester -from idaes.core.util.scaling import ( - calculate_scaling_factors, - unscaled_variables_generator, - badly_scaled_var_generator, -) -from idaes.core import UnitModelCostingBlock - -from watertap.core.solvers import get_solver from watertap_contrib.reflo.unit_models.zero_order.crystallizer_zo_watertap import ( Crystallization, ) @@ -134,14 +111,14 @@ def add_heat_exchanger_eff1(m, steam_pressure): initialize=35, bounds=(None, None), units=pyunits.K, - doc="Temperature differnce at the inlet side", + doc="Temperature difference at the inlet side", ) eff_1.delta_temperature_out = Var( eff_1.flowsheet().time, initialize=35, bounds=(None, None), units=pyunits.K, - doc="Temperature differnce at the outlet side", + doc="Temperature difference at the outlet side", ) delta_temperature_chen_callback(eff_1) @@ -219,30 +196,30 @@ def add_heat_exchanger_eff2(m): initialize=35, bounds=(None, None), units=pyunits.K, - doc="Temperature differnce at the inlet side", + doc="Temperature difference at the inlet side", ) eff_2.delta_temperature_out = Var( eff_2.flowsheet().time, initialize=35, bounds=(None, None), units=pyunits.K, - doc="Temperature differnce at the outlet side", + doc="Temperature difference at the outlet side", ) delta_temperature_chen_callback(eff_2) eff_2.area = Var( bounds=(0, None), initialize=1000.0, - doc="Heat exchange area", units=pyunits.m**2, + doc="Heat exchange area", ) eff_2.overall_heat_transfer_coefficient = Var( eff_2.flowsheet().time, bounds=(0, None), initialize=100.0, - doc="Overall heat transfer coefficient", units=pyunits.W / pyunits.m**2 / pyunits.K, + doc="Overall heat transfer coefficient", ) eff_2.overall_heat_transfer_coefficient[0].fix(100) @@ -286,30 +263,30 @@ def add_heat_exchanger_eff3(m): initialize=35, bounds=(None, None), units=pyunits.K, - doc="Temperature differnce at the inlet side", + doc="Temperature difference at the inlet side", ) eff_3.delta_temperature_out = Var( eff_3.flowsheet().time, initialize=35, bounds=(None, None), units=pyunits.K, - doc="Temperature differnce at the outlet side", + doc="Temperature difference at the outlet side", ) delta_temperature_chen_callback(eff_3) eff_3.area = Var( bounds=(0, None), initialize=1000.0, - doc="Heat exchange area", units=pyunits.m**2, + doc="Heat exchange area", ) eff_3.overall_heat_transfer_coefficient = Var( eff_3.flowsheet().time, bounds=(0, None), initialize=100.0, - doc="Overall heat transfer coefficient", units=pyunits.W / pyunits.m**2 / pyunits.K, + doc="Overall heat transfer coefficient", ) eff_3.overall_heat_transfer_coefficient[0].fix(100) @@ -366,16 +343,16 @@ def add_heat_exchanger_eff4(m): eff_4.area = Var( bounds=(0, None), initialize=1000.0, - doc="Heat exchange area", units=pyunits.m**2, + doc="Heat exchange area", ) eff_4.overall_heat_transfer_coefficient = Var( eff_4.flowsheet().time, bounds=(0, None), initialize=100.0, - doc="Overall heat transfer coefficient", units=pyunits.W / pyunits.m**2 / pyunits.K, + doc="Overall heat transfer coefficient", ) eff_4.overall_heat_transfer_coefficient[0].fix(100) @@ -497,7 +474,7 @@ def multi_effect_crystallizer_initialization(m): m.fs.props.set_default_scaling("flow_mass_phase_comp", 1e-1, index=("Vap", "H2O")) m.fs.props.set_default_scaling("flow_mass_phase_comp", 1e-1, index=("Sol", "NaCl")) - calculate_scaling_factors(m) + iscale.calculate_scaling_factors(m) m.fs.eff_1.initialize() m.fs.eff_2.initialize() From fe63f64d124eac06dca4b787193fb77b363377d9 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Wed, 11 Sep 2024 17:15:38 -0600 Subject: [PATCH 07/80] try to use WaterTAP cryst_prop_pack --- .../example_flowsheets/multi_effect_NaCl_crystallizer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/watertap_contrib/reflo/analysis/example_flowsheets/multi_effect_NaCl_crystallizer.py b/src/watertap_contrib/reflo/analysis/example_flowsheets/multi_effect_NaCl_crystallizer.py index b14157b2..8c2f2ff4 100644 --- a/src/watertap_contrib/reflo/analysis/example_flowsheets/multi_effect_NaCl_crystallizer.py +++ b/src/watertap_contrib/reflo/analysis/example_flowsheets/multi_effect_NaCl_crystallizer.py @@ -22,7 +22,8 @@ from watertap_contrib.reflo.unit_models.zero_order.crystallizer_zo_watertap import ( Crystallization, ) -import watertap_contrib.reflo.property_models.cryst_prop_pack as props +# import watertap_contrib.reflo.property_models.cryst_prop_pack as props +import watertap.property_models.unit_specific.cryst_prop_pack as props from watertap_contrib.reflo.costing import ( TreatmentCosting, CrystallizerCostType, From 6179f4a8ca95b163a3b128adb896649e443545bf Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Wed, 11 Sep 2024 17:20:08 -0600 Subject: [PATCH 08/80] black --- .../example_flowsheets/multi_effect_NaCl_crystallizer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/watertap_contrib/reflo/analysis/example_flowsheets/multi_effect_NaCl_crystallizer.py b/src/watertap_contrib/reflo/analysis/example_flowsheets/multi_effect_NaCl_crystallizer.py index 8c2f2ff4..691b8a00 100644 --- a/src/watertap_contrib/reflo/analysis/example_flowsheets/multi_effect_NaCl_crystallizer.py +++ b/src/watertap_contrib/reflo/analysis/example_flowsheets/multi_effect_NaCl_crystallizer.py @@ -22,6 +22,7 @@ from watertap_contrib.reflo.unit_models.zero_order.crystallizer_zo_watertap import ( Crystallization, ) + # import watertap_contrib.reflo.property_models.cryst_prop_pack as props import watertap.property_models.unit_specific.cryst_prop_pack as props from watertap_contrib.reflo.costing import ( From d4b39e7bc6436e68009163e70c982bf27294dbf0 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 12 Sep 2024 10:27:44 -0600 Subject: [PATCH 09/80] try watertap==1.0.0rc0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 750b967f..d8565034 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ python_requires=">=3.8", install_requires=[ # "watertap @ https://github.com/watertap-org/watertap/archive/main.zip", # uncomment if we need to point to main mid release cycle - "watertap>=1.0.0rc0", + "watertap==1.0.0rc0", "idaes-pse==2.5.0", "pyomo==6.7.3", "nrel-pysam == 5.1.0", From 0a4ffdd5e2ae4ea43edea18006e2db8e2508376c Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 12 Sep 2024 16:45:23 -0600 Subject: [PATCH 10/80] initial crystallizer_effect model --- .../reflo/unit_models/crystallizer_effect.py | 522 ++++++++++++++++++ 1 file changed, 522 insertions(+) create mode 100644 src/watertap_contrib/reflo/unit_models/crystallizer_effect.py diff --git a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py new file mode 100644 index 00000000..f1efa56c --- /dev/null +++ b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py @@ -0,0 +1,522 @@ +################################################################################# +# WaterTAP Copyright (c) 2020-2024, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# National Renewable Energy Laboratory, and National Energy Technology +# Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +################################################################################# + +from copy import deepcopy + +# Import Pyomo libraries +from pyomo.environ import ( + ConcreteModel, + Var, + check_optimal_termination, + Param, + Constraint, + Suffix, + units as pyunits, +) +from pyomo.common.config import ConfigBlock, ConfigValue, In + +# Import IDAES cores +from idaes.core import ( + declare_process_block_class, + UnitModelBlockData, + useDefault, + FlowsheetBlock, +) +from watertap.core.solvers import get_solver +from idaes.core.util.tables import create_stream_table_dataframe +from idaes.core.util.constants import Constants +from idaes.core.util.config import is_physical_parameter_block + +from idaes.core.util.exceptions import InitializationError + +import idaes.core.util.scaling as iscale +import idaes.logger as idaeslog + +from watertap.core import InitializationMixin +from watertap.core.util.initialization import interval_initializer +from watertap.unit_models.crystallizer import Crystallization, CrystallizationData +from watertap_contrib.reflo.costing.units.crystallizer_watertap import ( + cost_crystallizer_watertap, +) +from idaes.core.util.model_statistics import ( + degrees_of_freedom, + number_variables, + number_total_constraints, + number_unused_variables, +) +from idaes.core.util.testing import initialization_tester +from idaes.core.util.scaling import ( + calculate_scaling_factors, + unscaled_variables_generator, + badly_scaled_var_generator, +) +from watertap.unit_models.mvc.components.lmtd_chen_callback import ( + delta_temperature_chen_callback, +) + +_log = idaeslog.getLogger(__name__) + +__author__ = "Oluwamayowa Amusat, Zhuoran Zhang, Kurban Sitterley" + + +@declare_process_block_class("CrystallizerEffect") +class CrystallizerEffectData(CrystallizationData): + """ + Zero-order model for crystallizer effect + """ + + CONFIG = CrystallizationData.CONFIG() + + CONFIG.declare( + "property_package_vapor", + ConfigValue( + default=useDefault, + domain=is_physical_parameter_block, + description="Property package to use for heating and motive steam properties", + doc="""Property parameter object used to define steasm property calculations, + **default** - useDefault. + **Valid values:** { + **useDefault** - use default package from parent model or flowsheet, + **PhysicalParameterObject** - a PhysicalParameterBlock object.}""", + ), + ) + + def build(self): + super().build() + + # self.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] + # self.properties_in[0].flow_vol_phase["Liq"] + + # self.tsat_constants_dict = { + # "tsat_c1": (42.6776, pyunits.K), + # "tsat_c2": (-3892.7, pyunits.K), + # "tsat_c3": (1000, pyunits.kPa), + # "tsat_c4": (-9.48654, pyunits.dimensionless), + # } + + # for c, (v, u) in self.tsat_constants_dict.items(): + # self.properties_in[0].add_component(c, Param(initialize=v, units=u)) + + # self.properties_in[0].add_component("steam_pressure_sat", Param(initialize=150, units=pyunits.kPa, doc="Steam pressure saturated")) + + # self.properties_in[0].add_component("test", Var()) + + tmp_dict = dict(**self.config.property_package_args) + tmp_dict["has_phase_equilibrium"] = False + tmp_dict["parameters"] = self.config.property_package + tmp_dict["defined_state"] = False + + self.properties_pure_water = self.config.property_package.state_block_class( + self.flowsheet().config.time, + doc="Material properties of pure water vapour at outlet", + **tmp_dict, + ) + + self.add_port(name="pure_water", block=self.properties_pure_water) + + self.properties_pure_water[0].flow_mass_phase_comp["Liq", "H2O"].fix(1e-8) + self.properties_pure_water[0].flow_mass_phase_comp["Liq", "NaCl"].fix(0) + self.properties_pure_water[0].flow_mass_phase_comp["Sol", "NaCl"].fix(0) + self.properties_pure_water[0].mass_frac_phase_comp["Liq", "NaCl"] + + self.heating_steam = self.config.property_package.state_block_class( + self.flowsheet().config.time, + doc="Material properties of inlet heating steam", + **tmp_dict, + ) + + self.add_port(name="steam", block=self.heating_steam) + + # self.heating_steam[0].flow_mass_phase_comp["Liq", "H2O"].fix(1e-8) + # self.heating_steam[0].flow_mass_phase_comp["Liq", "NaCl"].fix(0) + self.heating_steam[0].flow_mass_phase_comp["Sol", "NaCl"].fix(0) + # self.heating_steam[0].mass_frac_phase_comp["Liq", "NaCl"] + + self.inlet.temperature.setub(1000) + self.outlet.temperature.setub(1000) + self.solids.temperature.setub(1000) + self.vapor.temperature.setub(1000) + self.pure_water.temperature.setub(1000) + self.steam.temperature.setub(1000) + + self.energy_flow_superheated_vapor = Var( + initialize=1e5, + bounds=(-5e6, 5e6), + units=pyunits.kJ / pyunits.s, + doc="Energy could be supplied from vapor", + ) + + self.delta_temperature_in = Var( + self.flowsheet().time, + initialize=35, + bounds=(None, None), + units=pyunits.K, + doc="Temperature difference at the inlet side", + ) + self.delta_temperature_out = Var( + self.flowsheet().time, + initialize=35, + bounds=(None, None), + units=pyunits.K, + doc="Temperature difference at the outlet side", + ) + delta_temperature_chen_callback(self) + + self.area = Var( + initialize=1000.0, + bounds=(0, None), + units=pyunits.m**2, + doc="Heat exchange area", + ) + + self.overall_heat_transfer_coefficient = Var( + # self.flowsheet().time, + initialize=100.0, + bounds=(0, None), + units=pyunits.W / pyunits.m**2 / pyunits.K, + doc="Overall heat transfer coefficient", + ) + + # self.overall_heat_transfer_coefficient.fix(100) + # @self.Constraint() + # def eq_mass_frac_nacl_steam(b): + # return ( + # b.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] + # == b.heating_steam[0].conc_mass_phase_comp["Liq", "NaCl"] + # ) + # @self.Constraint(doc="Flow rate of liquid motive steam is zero") + # def eq_motive_steam_liquid_mass(b): + # return b.heating_steam[0].flow_mass_phase_comp["Liq", "H2O"] == 0 + @self.Constraint() + def eq_pure_vapor_flow_rate(b): + return ( + b.properties_pure_water[0].flow_mass_phase_comp["Vap", "H2O"] + == b.properties_in[0].flow_mass_phase_comp["Vap", "H2O"] + ) + + @self.Constraint(doc="Thermal energy in the vapor") + def eq_vapor_energy_constraint(b): + return b.energy_flow_superheated_vapor == ( + b.properties_vapor[0].flow_mass_phase_comp["Vap", "H2O"] + * ( + b.properties_pure_water[ + 0 + ].dh_vap_mass_solvent # Latent heat from the vapor + + b.properties_vapor[0].enth_mass_solvent["Vap"] + - b.properties_pure_water[0].enth_mass_solvent["Vap"] + ) + ) + + @self.Constraint(doc="Change in temperature at inlet") + def eq_delta_temperature_in(b): + return ( + b.delta_temperature_in[0] + == b.heating_steam[0].temperature_sat_solvent - b.temperature_operating + ) + + @self.Constraint(doc="Change in temperature at outlet") + def eq_delta_temperature_out(b): + return ( + b.delta_temperature_out[0] + == b.heating_steam[0].temperature_sat_solvent + - b.properties_in[0].temperature + ) + + # self.del_component(self.eq_p_con1) + # self.del_component(self.eq_p_con2) + # @self.Constraint() + # def eq_p_con1(b): + # return b.pressure_operating == b.properties_out[0].pressure + + # @self.Constraint() + # def eq_p_con2(b): + # return b.pressure_operating == b.properties_solids[0].pressure + + # @self.Constraint() + # def eq_temp_222(b): + # return b.properties_in[0].temperature == b.temperature_operating + @self.Constraint() + def eq_p_con4(b): + return b.properties_pure_water[0].pressure == self.pressure_operating + + @self.Constraint() + def eq_p_con5(b): + return b.properties_pure_water[0].pressure_sat == self.pressure_operating + + def initialize_build( + self, + state_args=None, + outlvl=idaeslog.NOTSET, + solver=None, + optarg=None, + ): + """ + General wrapper for pressure changer initialization routines + + Keyword Arguments: + state_args : a dict of arguments to be passed to the property + package(s) to provide an initial state for + initialization (see documentation of the specific + property package) (default = {}). + outlvl : sets output level of initialization routine + optarg : solver options dictionary object (default=None) + solver : str indicating which solver to use during + initialization (default = None) + + Returns: None + """ + init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") + solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit") + + opt = get_solver(solver, optarg) + + # --------------------------------------------------------------------- + # Initialize holdup block + flags = self.properties_in.initialize( + outlvl=outlvl, + optarg=optarg, + solver=solver, + state_args=state_args, + hold_state=True, + ) + init_log.info_high("Initialization Step 1 Complete.") + # --------------------------------------------------------------------- + # Initialize other state blocks + # Set state_args from inlet state + if state_args is None: + state_args = {} + state_dict = self.properties_in[ + self.flowsheet().config.time.first() + ].define_port_members() + + for k in state_dict.keys(): + if state_dict[k].is_indexed(): + state_args[k] = {} + for m in state_dict[k].keys(): + state_args[k][m] = state_dict[k][m].value + else: + state_args[k] = state_dict[k].value + + self.properties_out.initialize( + outlvl=outlvl, + optarg=optarg, + solver=solver, + state_args=state_args, + ) + + state_args_solids = deepcopy(state_args) + + for p, j in self.properties_solids.phase_component_set: + if p == "Sol": + state_args_solids["flow_mass_phase_comp"][p, j] = state_args[ + "flow_mass_phase_comp" + ]["Liq", j] + elif p == "Liq" or p == "Vap": + state_args_solids["flow_mass_phase_comp"][p, j] = 1e-8 + + self.properties_solids.initialize( + outlvl=outlvl, + optarg=optarg, + solver=solver, + state_args=state_args_solids, + ) + + state_args_vapor = deepcopy(state_args) + + for p, j in self.properties_vapor.phase_component_set: + if p == "Vap": + state_args_vapor["flow_mass_phase_comp"][p, j] = state_args[ + "flow_mass_phase_comp" + ]["Liq", j] + elif p == "Liq" or p == "Sol": + state_args_vapor["flow_mass_phase_comp"][p, j] = 1e-8 + + self.properties_vapor.initialize( + outlvl=outlvl, + optarg=optarg, + solver=solver, + state_args=state_args_vapor, + ) + + self.properties_pure_water.initialize( + outlvl=outlvl, + optarg=optarg, + solver=solver, + state_args=state_args_vapor, + ) + + state_args_steam = deepcopy(state_args) + + for p, j in self.properties_vapor.phase_component_set: + state_args_steam["flow_mass_phase_comp"][p, j] = 1e-8 + + self.heating_steam.initialize( + outlvl=outlvl, + optarg=optarg, + solver=solver, + state_args=state_args_steam, + ) + + init_log.info_high("Initialization Step 2 Complete.") + + interval_initializer(self) + # --------------------------------------------------------------------- + # Solve unit + with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: + res = opt.solve(self, tee=slc.tee) + init_log.info_high("Initialization Step 3 {}.".format(idaeslog.condition(res))) + # --------------------------------------------------------------------- + # Release Inlet state + self.properties_in.release_state(flags, outlvl=outlvl) + init_log.info("Initialization Complete: {}".format(idaeslog.condition(res))) + + if not check_optimal_termination(res): + raise InitializationError(f"Unit model {self.name} failed to initialize") + + def calculate_scaling_factors(self): + super().calculate_scaling_factors() + if iscale.get_scaling_factor(self.work_mechanical[0]) is None: + iscale.set_scaling_factor( + self.work_mechanical[0], + iscale.get_scaling_factor( + self.properties_in[0].flow_mass_phase_comp["Vap", "H2O"] + ) + * iscale.get_scaling_factor( + self.properties_in[0].enth_mass_solvent["Vap"] + ) + * 1e3, + ) + if iscale.get_scaling_factor(self.energy_flow_superheated_vapor) is None: + iscale.set_scaling_factor( + self.energy_flow_superheated_vapor, + iscale.get_scaling_factor( + self.properties_vapor[0].flow_mass_phase_comp["Vap", "H2O"] + ) + * iscale.get_scaling_factor( + self.properties_vapor[0].enth_mass_solvent["Vap"] + ), + ) + + if iscale.get_scaling_factor(self.delta_temperature_in[0]) is None: + iscale.set_scaling_factor(self.delta_temperature_in[0], 0.1) + + if iscale.get_scaling_factor(self.delta_temperature_out[0]) is None: + iscale.set_scaling_factor(self.delta_temperature_out[0], 0.1) + + if iscale.get_scaling_factor(self.area) is None: + iscale.set_scaling_factor(self.area, 0.1) + + if iscale.get_scaling_factor(self.overall_heat_transfer_coefficient) is None: + iscale.set_scaling_factor(self.overall_heat_transfer_coefficient, 0.1) + + for ind, c in self.eq_p_con4.items(): + sf = iscale.get_scaling_factor(self.properties_pure_water[0].pressure) + iscale.constraint_scaling_transform(c, sf) + + def _calculate_saturation_temperature(self): + pass + + +if __name__ == "__main__": + import watertap.property_models.unit_specific.cryst_prop_pack as props + from watertap.property_models.water_prop_pack import WaterParameterBlock + + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + m.fs.props = props.NaClParameterBlock() + m.fs.vapor = WaterParameterBlock() + + m.fs.eff = eff = CrystallizerEffect(property_package=m.fs.props) + # m.fs.eff.display() + + feed_flow_mass = 1 + feed_mass_frac_NaCl = 0.15 + feed_pressure = 101325 + feed_temperature = 273.15 + 20 + crystallizer_yield = 0.5 + operating_pressure_eff1 = 0.78 + + feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl + eps = 1e-6 + + eff.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"].fix( + feed_flow_mass * feed_mass_frac_NaCl + ) + eff.inlet.flow_mass_phase_comp[0, "Liq", "H2O"].fix( + feed_flow_mass * feed_mass_frac_H2O + ) + eff.inlet.flow_mass_phase_comp[0, "Sol", "NaCl"].fix(eps) + eff.inlet.flow_mass_phase_comp[0, "Vap", "H2O"].fix(eps) + + eff.inlet.pressure[0].fix(feed_pressure) + eff.inlet.temperature[0].fix(feed_temperature) + eff.inlet.temperature[0].fix(404.5) + eff.properties_in[0].pressure_sat + + # eff.steam.flow_mass_phase_comp[0, "Liq", "NaCl"].fix( + # eps + # ) + # eff.steam.flow_mass_phase_comp[0, "Liq", "H2O"].fix( + # eps + # ) + # eff.heating_steam[0].temperature.fix(404.5) + # eff.steam.temperature[0].fix(404.5) + # eff.heating_steam[0].flow_mass_phase_comp.fix(eps) + # eff.heating_steam[0].mass_frac_phase_comp.fix(eps) + # eff.heating_steam[0].pressure.fix() + + eff.heating_steam.calculate_state( + var_args={ + ("pressure", None): 101325, + # ("pressure_sat", None): 28100 + ("temperature", None): 393, + ("mass_frac_phase_comp", ("Liq", "H2O")): 1 - eps, + ("mass_frac_phase_comp", ("Liq", "NaCl")): eps, + ("flow_vol_phase", ("Liq")): 10, + }, + hold_state=True, + ) + + ###################### + eff.crystallization_yield["NaCl"].fix(crystallizer_yield) + eff.crystal_growth_rate.fix() + eff.souders_brown_constant.fix() + eff.crystal_median_length.fix() + eff.overall_heat_transfer_coefficient.fix(100) + + eff.pressure_operating.fix(operating_pressure_eff1 * pyunits.bar) + + print(f"dof = {degrees_of_freedom(m.fs.eff)}") + + eff.initialize() + + print("\nPROPERTIES IN\n") + eff.properties_in[0].flow_mass_phase_comp.display() + eff.properties_in[0].mass_frac_phase_comp.display() + eff.properties_in[0].temperature.display() + eff.properties_in[0].pressure_sat.display() + eff.properties_in[0].temperature_sat_solvent.display() + + # print("\nPROPERTIES VAPOR\n") + # eff.properties_vapor[0].temperature.display() + # eff.properties_vapor[0].pressure_sat.display() + # eff.properties_vapor[0].temperature_sat_solvent.display() + + print("\nPROPERTIES HEATING STEAM\n") + eff.heating_steam[0].flow_mass_phase_comp.display() + eff.heating_steam[0].mass_frac_phase_comp.display() + eff.heating_steam[0].temperature.display() + eff.heating_steam[0].pressure_sat.display() + eff.heating_steam[0].temperature_sat_solvent.display() + + eff.temperature_operating.display() + # eff.heating_steam.display() From e99d461b9b2a53d20c59168316c410ffbbc9a7ad Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 12 Sep 2024 17:04:22 -0600 Subject: [PATCH 11/80] add steam props --- .../reflo/unit_models/crystallizer_effect.py | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py index f1efa56c..b7066311 100644 --- a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py +++ b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py @@ -17,6 +17,7 @@ ConcreteModel, Var, check_optimal_termination, + assert_optimal_termination, Param, Constraint, Suffix, @@ -128,7 +129,10 @@ def build(self): self.properties_pure_water[0].flow_mass_phase_comp["Sol", "NaCl"].fix(0) self.properties_pure_water[0].mass_frac_phase_comp["Liq", "NaCl"] - self.heating_steam = self.config.property_package.state_block_class( + tmp_dict["parameters"] = self.config.property_package_vapor + tmp_dict["defined_state"] = False + + self.heating_steam = self.config.property_package_vapor.state_block_class( self.flowsheet().config.time, doc="Material properties of inlet heating steam", **tmp_dict, @@ -138,7 +142,7 @@ def build(self): # self.heating_steam[0].flow_mass_phase_comp["Liq", "H2O"].fix(1e-8) # self.heating_steam[0].flow_mass_phase_comp["Liq", "NaCl"].fix(0) - self.heating_steam[0].flow_mass_phase_comp["Sol", "NaCl"].fix(0) + # self.heating_steam[0].flow_mass_phase_comp["Sol", "NaCl"].fix(0) # self.heating_steam[0].mass_frac_phase_comp["Liq", "NaCl"] self.inlet.temperature.setub(1000) @@ -220,15 +224,14 @@ def eq_vapor_energy_constraint(b): def eq_delta_temperature_in(b): return ( b.delta_temperature_in[0] - == b.heating_steam[0].temperature_sat_solvent - b.temperature_operating + == b.heating_steam[0].temperature - b.temperature_operating ) @self.Constraint(doc="Change in temperature at outlet") def eq_delta_temperature_out(b): return ( b.delta_temperature_out[0] - == b.heating_steam[0].temperature_sat_solvent - - b.properties_in[0].temperature + == b.heating_steam[0].temperature - b.properties_in[0].temperature ) # self.del_component(self.eq_p_con1) @@ -314,7 +317,7 @@ def initialize_build( ) state_args_solids = deepcopy(state_args) - + for p, j in self.properties_solids.phase_component_set: if p == "Sol": state_args_solids["flow_mass_phase_comp"][p, j] = state_args[ @@ -322,7 +325,7 @@ def initialize_build( ]["Liq", j] elif p == "Liq" or p == "Vap": state_args_solids["flow_mass_phase_comp"][p, j] = 1e-8 - + self.properties_solids.initialize( outlvl=outlvl, optarg=optarg, @@ -435,7 +438,9 @@ def _calculate_saturation_temperature(self): m.fs.props = props.NaClParameterBlock() m.fs.vapor = WaterParameterBlock() - m.fs.eff = eff = CrystallizerEffect(property_package=m.fs.props) + m.fs.eff = eff = CrystallizerEffect( + property_package=m.fs.props, property_package_vapor=m.fs.vapor + ) # m.fs.eff.display() feed_flow_mass = 1 @@ -459,8 +464,7 @@ def _calculate_saturation_temperature(self): eff.inlet.pressure[0].fix(feed_pressure) eff.inlet.temperature[0].fix(feed_temperature) - eff.inlet.temperature[0].fix(404.5) - eff.properties_in[0].pressure_sat + # eff.properties_in[0].pressure_sat # eff.steam.flow_mass_phase_comp[0, "Liq", "NaCl"].fix( # eps @@ -473,20 +477,18 @@ def _calculate_saturation_temperature(self): # eff.heating_steam[0].flow_mass_phase_comp.fix(eps) # eff.heating_steam[0].mass_frac_phase_comp.fix(eps) # eff.heating_steam[0].pressure.fix() + eff.heating_steam[0].pressure_sat eff.heating_steam.calculate_state( var_args={ ("pressure", None): 101325, # ("pressure_sat", None): 28100 ("temperature", None): 393, - ("mass_frac_phase_comp", ("Liq", "H2O")): 1 - eps, - ("mass_frac_phase_comp", ("Liq", "NaCl")): eps, - ("flow_vol_phase", ("Liq")): 10, }, hold_state=True, ) - ###################### + # ###################### eff.crystallization_yield["NaCl"].fix(crystallizer_yield) eff.crystal_growth_rate.fix() eff.souders_brown_constant.fix() @@ -498,6 +500,9 @@ def _calculate_saturation_temperature(self): print(f"dof = {degrees_of_freedom(m.fs.eff)}") eff.initialize() + solver = get_solver() + results = solver.solve(m) + assert_optimal_termination(results) print("\nPROPERTIES IN\n") eff.properties_in[0].flow_mass_phase_comp.display() @@ -513,10 +518,11 @@ def _calculate_saturation_temperature(self): print("\nPROPERTIES HEATING STEAM\n") eff.heating_steam[0].flow_mass_phase_comp.display() - eff.heating_steam[0].mass_frac_phase_comp.display() + + # eff.heating_steam[0].mass_frac_phase_comp.display() eff.heating_steam[0].temperature.display() eff.heating_steam[0].pressure_sat.display() - eff.heating_steam[0].temperature_sat_solvent.display() + # eff.heating_steam[0].temperature_sat_solvent.display() eff.temperature_operating.display() # eff.heating_steam.display() From da58c8d877b2431fe91b8c5dd2bb87847cb81310 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 12 Sep 2024 17:19:10 -0600 Subject: [PATCH 12/80] clean up; add pressure balance constraints --- .../reflo/unit_models/crystallizer_effect.py | 55 ++++--------------- 1 file changed, 10 insertions(+), 45 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py index b7066311..71eaa0b4 100644 --- a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py +++ b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py @@ -94,23 +94,6 @@ class CrystallizerEffectData(CrystallizationData): def build(self): super().build() - # self.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] - # self.properties_in[0].flow_vol_phase["Liq"] - - # self.tsat_constants_dict = { - # "tsat_c1": (42.6776, pyunits.K), - # "tsat_c2": (-3892.7, pyunits.K), - # "tsat_c3": (1000, pyunits.kPa), - # "tsat_c4": (-9.48654, pyunits.dimensionless), - # } - - # for c, (v, u) in self.tsat_constants_dict.items(): - # self.properties_in[0].add_component(c, Param(initialize=v, units=u)) - - # self.properties_in[0].add_component("steam_pressure_sat", Param(initialize=150, units=pyunits.kPa, doc="Steam pressure saturated")) - - # self.properties_in[0].add_component("test", Var()) - tmp_dict = dict(**self.config.property_package_args) tmp_dict["has_phase_equilibrium"] = False tmp_dict["parameters"] = self.config.property_package @@ -140,11 +123,6 @@ def build(self): self.add_port(name="steam", block=self.heating_steam) - # self.heating_steam[0].flow_mass_phase_comp["Liq", "H2O"].fix(1e-8) - # self.heating_steam[0].flow_mass_phase_comp["Liq", "NaCl"].fix(0) - # self.heating_steam[0].flow_mass_phase_comp["Sol", "NaCl"].fix(0) - # self.heating_steam[0].mass_frac_phase_comp["Liq", "NaCl"] - self.inlet.temperature.setub(1000) self.outlet.temperature.setub(1000) self.solids.temperature.setub(1000) @@ -173,6 +151,7 @@ def build(self): units=pyunits.K, doc="Temperature difference at the outlet side", ) + delta_temperature_chen_callback(self) self.area = Var( @@ -183,23 +162,12 @@ def build(self): ) self.overall_heat_transfer_coefficient = Var( - # self.flowsheet().time, initialize=100.0, bounds=(0, None), units=pyunits.W / pyunits.m**2 / pyunits.K, doc="Overall heat transfer coefficient", ) - # self.overall_heat_transfer_coefficient.fix(100) - # @self.Constraint() - # def eq_mass_frac_nacl_steam(b): - # return ( - # b.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] - # == b.heating_steam[0].conc_mass_phase_comp["Liq", "NaCl"] - # ) - # @self.Constraint(doc="Flow rate of liquid motive steam is zero") - # def eq_motive_steam_liquid_mass(b): - # return b.heating_steam[0].flow_mass_phase_comp["Liq", "H2O"] == 0 @self.Constraint() def eq_pure_vapor_flow_rate(b): return ( @@ -234,19 +202,16 @@ def eq_delta_temperature_out(b): == b.heating_steam[0].temperature - b.properties_in[0].temperature ) - # self.del_component(self.eq_p_con1) - # self.del_component(self.eq_p_con2) - # @self.Constraint() - # def eq_p_con1(b): - # return b.pressure_operating == b.properties_out[0].pressure + self.del_component(self.eq_p_con1) + self.del_component(self.eq_p_con2) + @self.Constraint() + def eq_p_con1(b): + return b.pressure_operating == b.properties_out[0].pressure - # @self.Constraint() - # def eq_p_con2(b): - # return b.pressure_operating == b.properties_solids[0].pressure + @self.Constraint() + def eq_p_con2(b): + return b.pressure_operating == b.properties_solids[0].pressure - # @self.Constraint() - # def eq_temp_222(b): - # return b.properties_in[0].temperature == b.temperature_operating @self.Constraint() def eq_p_con4(b): return b.properties_pure_water[0].pressure == self.pressure_operating @@ -360,7 +325,7 @@ def initialize_build( state_args_steam = deepcopy(state_args) for p, j in self.properties_vapor.phase_component_set: - state_args_steam["flow_mass_phase_comp"][p, j] = 1e-8 + state_args_steam["flow_mass_phase_comp"][p, j] = 1 self.heating_steam.initialize( outlvl=outlvl, From 4b7e386fd535dbf3af5b0988a3b9da00f3911d59 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 12 Sep 2024 17:48:42 -0600 Subject: [PATCH 13/80] add heat transfer equation --- .../reflo/unit_models/crystallizer_effect.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py index 71eaa0b4..12be0449 100644 --- a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py +++ b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py @@ -201,6 +201,14 @@ def eq_delta_temperature_out(b): b.delta_temperature_out[0] == b.heating_steam[0].temperature - b.properties_in[0].temperature ) + + @self.Constraint(doc="Heat transfer equation") + def eq_heat_transfer(b): + return b.work_mechanical[0] == ( + b.overall_heat_transfer_coefficient + * b.area + * b.delta_temperature[0] + ) self.del_component(self.eq_p_con1) self.del_component(self.eq_p_con2) From 8a3ee8be9ac74688af3218b8c36064749c6f8f01 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Fri, 13 Sep 2024 11:52:54 -0600 Subject: [PATCH 14/80] initial muli_effect_crystallizer model --- .../unit_models/multi_effect_crystallizer.py | 400 ++++++++++++++++++ 1 file changed, 400 insertions(+) create mode 100644 src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py diff --git a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py new file mode 100644 index 00000000..6301a0b0 --- /dev/null +++ b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py @@ -0,0 +1,400 @@ +################################################################################# +# WaterTAP Copyright (c) 2020-2024, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# National Renewable Energy Laboratory, and National Energy Technology +# Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +################################################################################# + +from copy import deepcopy + +# Import Pyomo libraries +from pyomo.environ import ( + ConcreteModel, + Var, + check_optimal_termination, + Param, + Constraint, + Suffix, + RangeSet, + units as pyunits, +) +from pyomo.common.config import ConfigBlock, ConfigValue, In, PositiveInt + +# Import IDAES cores +from idaes.core import ( + declare_process_block_class, + UnitModelBlockData, + useDefault, + FlowsheetBlock, +) +from watertap.core.solvers import get_solver +from idaes.core.util.tables import create_stream_table_dataframe +from idaes.core.util.constants import Constants +from idaes.core.util.config import is_physical_parameter_block + +from idaes.core.util.exceptions import InitializationError + +import idaes.core.util.scaling as iscale +import idaes.logger as idaeslog + +from watertap.core import InitializationMixin +from watertap.core.util.initialization import interval_initializer +from watertap.unit_models.crystallizer import Crystallization, CrystallizationData +from watertap_contrib.reflo.costing.units.crystallizer_watertap import ( + cost_crystallizer_watertap, +) +from idaes.core.util.model_statistics import ( + degrees_of_freedom, + number_variables, + number_total_constraints, + number_unused_variables, +) +from idaes.core.util.testing import initialization_tester +from idaes.core.util.scaling import ( + calculate_scaling_factors, + unscaled_variables_generator, + badly_scaled_var_generator, +) +from watertap.unit_models.mvc.components.lmtd_chen_callback import ( + delta_temperature_chen_callback, +) + +from watertap_contrib.reflo.unit_models.crystallizer_effect import CrystallizerEffect + + +_log = idaeslog.getLogger(__name__) + +__author__ = "Oluwamayowa Amusat, Zhuoran Zhang, Kurban Sitterley" + + +@declare_process_block_class("MultiEffectCrystallizer") +class MultiEffectCrystallizerData(InitializationMixin, UnitModelBlockData): + + 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. The filtration unit does not support dynamic + behavior, thus this must be False.""", + ), + ) + + 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. The filtration unit does not have defined volume, thus + this must be False.""", + ), + ) + + CONFIG.declare( + "property_package", + ConfigValue( + default=useDefault, + domain=is_physical_parameter_block, + description="Property package to use for control volume", + doc="""Property parameter object used to define property calculations, + **default** - useDefault. + **Valid values:** { + **useDefault** - use default package from parent model or flowsheet, + **PhysicalParameterObject** - a PhysicalParameterBlock object.}""", + ), + ) + + CONFIG.declare( + "property_package_args", + ConfigBlock( + implicit=True, + description="Arguments to use for constructing property packages", + doc="""A ConfigBlock with arguments to be passed to a property block(s) + and used when constructing these, + **default** - None. + **Valid values:** { + see property package for documentation.}""", + ), + ) + + CONFIG.declare( + "property_package_vapor", + ConfigValue( + default=useDefault, + domain=is_physical_parameter_block, + description="Property package to use for heating and motive steam properties", + doc="""Property parameter object used to define steasm property calculations, + **default** - useDefault. + **Valid values:** { + **useDefault** - use default package from parent model or flowsheet, + **PhysicalParameterObject** - a PhysicalParameterBlock object.}""", + ), + ) + + CONFIG.declare( + "number_effects", + ConfigValue( + default=4, + domain=PositiveInt, + description="Number of effects of the multi-effect crystallizer system", + doc="""A ConfigBlock specifying the number of effects, which can only be 4.""", + ), + ) + + def build(self): + super().build() + + self.scaling_factor = Suffix(direction=Suffix.EXPORT) + + self.number_effects = self.config.number_effects + self.Effects = RangeSet(self.config.number_effects) + + self.first_effect = self.Effects.first() + self.last_effect = self.Effects.last() + + self.effects = FlowsheetBlock(self.Effects, dynamic=False) + + for n, eff in self.effects.items(): + eff.effect = effect = CrystallizerEffect( + property_package=self.config.property_package, + property_package_vapor=self.config.property_package_vapor, + standalone=False, + ) + if n == self.first_effect: + + @effect.Constraint( + doc="Change in temperature at inlet for first effect." + ) + def eq_delta_temperature_inlet_effect_1(b): + return ( + b.delta_temperature_in[0] + == b.heating_steam[0].temperature - b.temperature_operating + ) + + @effect.Constraint( + doc="Change in temperature at outlet for first effect." + ) + def eq_delta_temperature_outlet_effect_1(b): + return ( + b.delta_temperature_out[0] + == b.heating_steam[0].temperature + - b.properties_in[0].temperature + ) + + @effect.Constraint(doc="Heat transfer equation for first effect.") + def eq_heat_transfer_effect_1(b): + return b.work_mechanical[0] == ( + b.overall_heat_transfer_coefficient + * b.area + * b.delta_temperature[0] + ) + + else: + + prev_effect = self.effects[n - 1].effect + + del_temp_in_constr = Constraint( + expr=effect.delta_temperature_in[0] + == prev_effect.properties_vapor[0].temperature + - effect.temperature_operating, + doc=f"Change in temperature at inlet for effect {n}", + ) + effect.add_component( + f"eq_delta_temperature_inlet_effect_{n}", del_temp_in_constr + ) + + del_temp_out_constr = Constraint( + expr=effect.delta_temperature_out[0] + == prev_effect.properties_pure_water[0].temperature + - effect.properties_in[0].temperature, + doc=f"Change in temperature at outlet for effect {n}", + ) + effect.add_component( + f"eq_delta_temperature_outlet_effect_{n}", del_temp_out_constr + ) + + hx_constr = Constraint( + expr=prev_effect.energy_flow_superheated_vapor + == effect.overall_heat_transfer_coefficient + * effect.area + * effect.delta_temperature[0], + doc=f"Heat transfer equation for effect {n}", + ) + effect.add_component(f"eq_heat_transfer_effect_{n}", hx_constr) + + energy_flow_constr = Constraint( + expr=effect.work_mechanical[0] + == prev_effect.energy_flow_superheated_vapor, + doc=f"Energy supplied to effect {n}", + ) + effect.add_component( + f"eq_energy_for_effect_{n}_from_effect_{n - 1}", energy_flow_constr + ) + + mass_flow_solid_nacl_constr = Constraint( + expr=effect.properties_in[0].flow_mass_phase_comp["Sol", "NaCl"] + == prev_effect.properties_in[0].flow_mass_phase_comp["Sol", "NaCl"], + doc="Mass flow of solid NaCl for effect {n}", + ) + effect.add_component( + f"eq_equiv_mass_flow_sol_nacl_effect_{n}", mass_flow_solid_nacl_constr + ) + + mass_flow_vap_water_constr = Constraint( + expr=effect.properties_in[0].flow_mass_phase_comp["Vap", "H2O"] + == prev_effect.properties_in[0].flow_mass_phase_comp["Vap", "H2O"], + doc="Mass flow of water vapor for effect {n}", + ) + effect.add_component( + f"eq_equiv_mass_flow_vap_water_effect_{n}", mass_flow_vap_water_constr + ) + + prop_in_press_constr = Constraint( + expr=effect.properties_in[0].pressure + == prev_effect.properties_in[0].pressure, + doc="Inlet properties pressure for effect {n}", + ) + effect.add_component( + f"eq_equiv_temp_effect_{n}", prop_in_press_constr + ) + + prop_in_temp_constr = Constraint( + expr=effect.properties_in[0].temperature + == prev_effect.properties_in[0].temperature, + doc="Inlet properties temperature for effect {n}", + ) + effect.add_component( + f"eq_equiv_pressure_effect_{n}", prop_in_temp_constr + ) + + steam_temp_sat_constr = Constraint( + expr=effect.heating_steam[0].temperature + == prev_effect.heating_steam[0].temperature, + doc="Steam saturation temperature for effect {n}", + ) + effect.add_component( + f"eq_equiv_steam_temp_sat_effect_{n}", steam_temp_sat_constr + ) + + steam_press_sat_constr = Constraint( + expr=effect.heating_steam[0].pressure_sat + == prev_effect.heating_steam[0].pressure_sat, + doc="Steam saturation pressure for effect {n}", + ) + effect.add_component( + f"eq_equiv_steam_press_sat_effect_{n}", steam_press_sat_constr + ) + + cryst_growth_rate_constr = Constraint( + expr=effect.crystal_growth_rate == prev_effect.crystal_growth_rate, + doc="Equivalent crystal growth rate effect {n}", + ) + effect.add_component( + f"eq_equiv_crystal_growth_rate_effect_{n}", cryst_growth_rate_constr + ) + + souders_brown_constr = Constraint( + expr=effect.souders_brown_constant + == prev_effect.souders_brown_constant, + doc="Equivalent Sounders Brown constant effect {n}", + ) + effect.add_component( + f"eq_equiv_souders_brown_constant_effect_{n}", souders_brown_constr + ) + + cryst_med_len_constr = Constraint( + expr=effect.crystal_median_length + == prev_effect.crystal_median_length, + doc="Equivalent crystal median length effect {n}", + ) + effect.add_component( + f"eq_equiv_crystal_median_length_effect_{n}", cryst_med_len_constr + ) + + cryst_yield_constr = Constraint( + expr=effect.crystallization_yield["NaCl"] + == prev_effect.crystallization_yield["NaCl"], + doc="Equivalent crystallization yield effect {n}", + ) + effect.add_component( + f"eq_equiv_crystallization_yield_effect_{n}", cryst_yield_constr + ) + + op_press_constr = Constraint( + expr=effect.pressure_operating == prev_effect.pressure_operating, + doc=f"Equivalent operating pressure effect {n}", + ) + effect.add_component( + f"eq_equiv_pressure_operating_effect_{n}", op_press_constr + ) + + +print(list(range(1, 5, 1))) +if __name__ == "__main__": + import watertap.property_models.unit_specific.cryst_prop_pack as props + from watertap.property_models.water_prop_pack import WaterParameterBlock + + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + m.fs.props = props.NaClParameterBlock() + m.fs.vapor = WaterParameterBlock() + + m.fs.mec = mec = MultiEffectCrystallizer( + property_package=m.fs.props, property_package_vapor=m.fs.vapor + ) + for n, effects in mec.effects.items(): + print(n, effects) + + feed_flow_mass = 1 + feed_mass_frac_NaCl = 0.15 + feed_pressure = 101325 + feed_temperature = 273.15 + 20 + crystallizer_yield = 0.5 + operating_pressure_eff1 = 0.78 + + feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl + eps = 1e-6 + + eff = mec.effects[1].effect + + eff.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"].fix( + feed_flow_mass * feed_mass_frac_NaCl + ) + eff.inlet.flow_mass_phase_comp[0, "Liq", "H2O"].fix( + feed_flow_mass * feed_mass_frac_H2O + ) + eff.inlet.flow_mass_phase_comp[0, "Sol", "NaCl"].fix(eps) + eff.inlet.flow_mass_phase_comp[0, "Vap", "H2O"].fix(eps) + + eff.inlet.pressure[0].fix(feed_pressure) + eff.inlet.temperature[0].fix(feed_temperature) + + eff.heating_steam[0].pressure_sat + + eff.heating_steam.calculate_state( + var_args={ + ("pressure", None): 101325, + # ("pressure_sat", None): 28100 + ("temperature", None): 393, + }, + hold_state=True, + ) + eff.crystallization_yield["NaCl"].fix(crystallizer_yield) + eff.crystal_growth_rate.fix() + eff.souders_brown_constant.fix() + eff.crystal_median_length.fix() + eff.overall_heat_transfer_coefficient.fix(100) + + eff.pressure_operating.fix(operating_pressure_eff1 * pyunits.bar) + print(f"dof = {degrees_of_freedom(m)}") From 94c738f422e574e4e69180ce9643e2f750a21302 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Fri, 13 Sep 2024 11:54:33 -0600 Subject: [PATCH 15/80] black --- .../reflo/unit_models/multi_effect_crystallizer.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py index 6301a0b0..53560204 100644 --- a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py +++ b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py @@ -201,9 +201,9 @@ def eq_heat_transfer_effect_1(b): ) else: - + prev_effect = self.effects[n - 1].effect - + del_temp_in_constr = Constraint( expr=effect.delta_temperature_in[0] == prev_effect.properties_vapor[0].temperature @@ -248,7 +248,8 @@ def eq_heat_transfer_effect_1(b): doc="Mass flow of solid NaCl for effect {n}", ) effect.add_component( - f"eq_equiv_mass_flow_sol_nacl_effect_{n}", mass_flow_solid_nacl_constr + f"eq_equiv_mass_flow_sol_nacl_effect_{n}", + mass_flow_solid_nacl_constr, ) mass_flow_vap_water_constr = Constraint( @@ -257,7 +258,8 @@ def eq_heat_transfer_effect_1(b): doc="Mass flow of water vapor for effect {n}", ) effect.add_component( - f"eq_equiv_mass_flow_vap_water_effect_{n}", mass_flow_vap_water_constr + f"eq_equiv_mass_flow_vap_water_effect_{n}", + mass_flow_vap_water_constr, ) prop_in_press_constr = Constraint( @@ -265,9 +267,7 @@ def eq_heat_transfer_effect_1(b): == prev_effect.properties_in[0].pressure, doc="Inlet properties pressure for effect {n}", ) - effect.add_component( - f"eq_equiv_temp_effect_{n}", prop_in_press_constr - ) + effect.add_component(f"eq_equiv_temp_effect_{n}", prop_in_press_constr) prop_in_temp_constr = Constraint( expr=effect.properties_in[0].temperature From 912ddaccc74abdf0a2a63af84f52cbad934cd10c Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Fri, 13 Sep 2024 11:56:43 -0600 Subject: [PATCH 16/80] add standalone config option and rearrange constraints, remove ports; clean up --- .../reflo/unit_models/crystallizer_effect.py | 76 +++++++++++++------ 1 file changed, 52 insertions(+), 24 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py index 12be0449..c8f66e47 100644 --- a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py +++ b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py @@ -82,7 +82,7 @@ class CrystallizerEffectData(CrystallizationData): ConfigValue( default=useDefault, domain=is_physical_parameter_block, - description="Property package to use for heating and motive steam properties", + description="Property package to use for heating steam properties", doc="""Property parameter object used to define steasm property calculations, **default** - useDefault. **Valid values:** { @@ -90,6 +90,16 @@ class CrystallizerEffectData(CrystallizationData): **PhysicalParameterObject** - a PhysicalParameterBlock object.}""", ), ) + CONFIG.declare( + "standalone", + ConfigValue( + default=True, + domain=bool, + description="Property package to use for heating and motive steam properties", + doc="""Property parameter object used to define steasm property calculations, + **default** - True.""", + ), + ) def build(self): super().build() @@ -188,30 +198,9 @@ def eq_vapor_energy_constraint(b): ) ) - @self.Constraint(doc="Change in temperature at inlet") - def eq_delta_temperature_in(b): - return ( - b.delta_temperature_in[0] - == b.heating_steam[0].temperature - b.temperature_operating - ) - - @self.Constraint(doc="Change in temperature at outlet") - def eq_delta_temperature_out(b): - return ( - b.delta_temperature_out[0] - == b.heating_steam[0].temperature - b.properties_in[0].temperature - ) - - @self.Constraint(doc="Heat transfer equation") - def eq_heat_transfer(b): - return b.work_mechanical[0] == ( - b.overall_heat_transfer_coefficient - * b.area - * b.delta_temperature[0] - ) - self.del_component(self.eq_p_con1) self.del_component(self.eq_p_con2) + @self.Constraint() def eq_p_con1(b): return b.pressure_operating == b.properties_out[0].pressure @@ -228,6 +217,45 @@ def eq_p_con4(b): def eq_p_con5(b): return b.properties_pure_water[0].pressure_sat == self.pressure_operating + if self.config.standalone: + + @self.Constraint(doc="Change in temperature at inlet") + def eq_delta_temperature_inlet(b): + return ( + b.delta_temperature_in[0] + == b.heating_steam[0].temperature - b.temperature_operating + ) + + @self.Constraint(doc="Change in temperature at outlet") + def eq_delta_temperature_outlet(b): + return ( + b.delta_temperature_out[0] + == b.heating_steam[0].temperature - b.properties_in[0].temperature + ) + + @self.Constraint(doc="Heat transfer equation") + def eq_heat_transfer(b): + return b.work_mechanical[0] == ( + b.overall_heat_transfer_coefficient + * b.area + * b.delta_temperature[0] + ) + + # else: + # self.del_component(self.inlet) + # self.del_component(self.outlet) + # self.del_component(self.solids) + # self.del_component(self.vapor) + # self.del_component(self.pure_water) + # self.del_component(self.steam) + + # self.inlet.temperature.setub(1000) + # self.outlet.temperature.setub(1000) + # self.solids.temperature.setub(1000) + # self.vapor.temperature.setub(1000) + # self.pure_water.temperature.setub(1000) + # self.steam.temperature.setub(1000) + def initialize_build( self, state_args=None, @@ -412,7 +440,7 @@ def _calculate_saturation_temperature(self): m.fs.vapor = WaterParameterBlock() m.fs.eff = eff = CrystallizerEffect( - property_package=m.fs.props, property_package_vapor=m.fs.vapor + property_package=m.fs.props, property_package_vapor=m.fs.vapor, standalone=True ) # m.fs.eff.display() From 6d4a1f796ca6d97bbfd65bd613e3ef8fe90d57af Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Fri, 13 Sep 2024 12:12:52 -0600 Subject: [PATCH 17/80] remove ports from non-standalone cryst effect model, add ports to first effect in MEC model --- .../unit_models/multi_effect_crystallizer.py | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py index 53560204..adac5ba0 100644 --- a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py +++ b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py @@ -200,6 +200,13 @@ def eq_heat_transfer_effect_1(b): * b.delta_temperature[0] ) + self.add_port(name="inlet", block=effect.properties_in) + self.add_port(name="outlet", block=effect.properties_out) + self.add_port(name="solids", block=effect.properties_solids) + self.add_port(name="vapor", block=effect.properties_vapor) + self.add_port(name="pure_water", block=effect.properties_pure_water) + self.add_port(name="steam", block=effect.heating_steam) + else: prev_effect = self.effects[n - 1].effect @@ -332,7 +339,7 @@ def eq_heat_transfer_effect_1(b): ) op_press_constr = Constraint( - expr=effect.pressure_operating == prev_effect.pressure_operating, + expr=effect.pressure_operating <= prev_effect.pressure_operating, doc=f"Equivalent operating pressure effect {n}", ) effect.add_component( @@ -361,24 +368,28 @@ def eq_heat_transfer_effect_1(b): feed_pressure = 101325 feed_temperature = 273.15 + 20 crystallizer_yield = 0.5 - operating_pressure_eff1 = 0.78 + operating_pressures = [0.78, 0.25, 0.208, 0.095] + operating_pressure_eff1 = 0.78 # bar + operating_pressure_eff2 = 0.25 # bar + operating_pressure_eff3 = 0.208 # bar + operating_pressure_eff4 = 0.095 # bar feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl eps = 1e-6 eff = mec.effects[1].effect - eff.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"].fix( + mec.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"].fix( feed_flow_mass * feed_mass_frac_NaCl ) - eff.inlet.flow_mass_phase_comp[0, "Liq", "H2O"].fix( + mec.inlet.flow_mass_phase_comp[0, "Liq", "H2O"].fix( feed_flow_mass * feed_mass_frac_H2O ) - eff.inlet.flow_mass_phase_comp[0, "Sol", "NaCl"].fix(eps) - eff.inlet.flow_mass_phase_comp[0, "Vap", "H2O"].fix(eps) + mec.inlet.flow_mass_phase_comp[0, "Sol", "NaCl"].fix(eps) + mec.inlet.flow_mass_phase_comp[0, "Vap", "H2O"].fix(eps) - eff.inlet.pressure[0].fix(feed_pressure) - eff.inlet.temperature[0].fix(feed_temperature) + mec.inlet.pressure[0].fix(feed_pressure) + mec.inlet.temperature[0].fix(feed_temperature) eff.heating_steam[0].pressure_sat @@ -396,5 +407,9 @@ def eq_heat_transfer_effect_1(b): eff.crystal_median_length.fix() eff.overall_heat_transfer_coefficient.fix(100) - eff.pressure_operating.fix(operating_pressure_eff1 * pyunits.bar) + # eff.pressure_operating.fix(operating_pressure_eff1 * pyunits.bar) + for (_, eff), op_pressure in zip(mec.effects.items(), operating_pressures): + eff.effect.pressure_operating.fix( + pyunits.convert(op_pressure * pyunits.bar, to_units=pyunits.Pa) + ) print(f"dof = {degrees_of_freedom(m)}") From e18e89b054ff21a6d4451b3112ff6c2b45bf21e7 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Fri, 13 Sep 2024 14:06:28 -0600 Subject: [PATCH 18/80] only make heating steam in standalone --- .../reflo/unit_models/crystallizer_effect.py | 116 +++++++++--------- 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py index c8f66e47..8fc0ea6e 100644 --- a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py +++ b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py @@ -121,24 +121,14 @@ def build(self): self.properties_pure_water[0].flow_mass_phase_comp["Liq", "NaCl"].fix(0) self.properties_pure_water[0].flow_mass_phase_comp["Sol", "NaCl"].fix(0) self.properties_pure_water[0].mass_frac_phase_comp["Liq", "NaCl"] - - tmp_dict["parameters"] = self.config.property_package_vapor - tmp_dict["defined_state"] = False - - self.heating_steam = self.config.property_package_vapor.state_block_class( - self.flowsheet().config.time, - doc="Material properties of inlet heating steam", - **tmp_dict, - ) - - self.add_port(name="steam", block=self.heating_steam) + self.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] + self.properties_in[0].flow_vol_phase["Liq"] self.inlet.temperature.setub(1000) self.outlet.temperature.setub(1000) self.solids.temperature.setub(1000) self.vapor.temperature.setub(1000) self.pure_water.temperature.setub(1000) - self.steam.temperature.setub(1000) self.energy_flow_superheated_vapor = Var( initialize=1e5, @@ -219,6 +209,18 @@ def eq_p_con5(b): if self.config.standalone: + tmp_dict["parameters"] = self.config.property_package_vapor + tmp_dict["defined_state"] = False + + self.heating_steam = self.config.property_package_vapor.state_block_class( + self.flowsheet().config.time, + doc="Material properties of inlet heating steam", + **tmp_dict, + ) + + self.add_port(name="steam", block=self.heating_steam) + self.steam.temperature.setub(1000) + @self.Constraint(doc="Change in temperature at inlet") def eq_delta_temperature_inlet(b): return ( @@ -241,20 +243,13 @@ def eq_heat_transfer(b): * b.delta_temperature[0] ) - # else: - # self.del_component(self.inlet) - # self.del_component(self.outlet) - # self.del_component(self.solids) - # self.del_component(self.vapor) - # self.del_component(self.pure_water) - # self.del_component(self.steam) - - # self.inlet.temperature.setub(1000) - # self.outlet.temperature.setub(1000) - # self.solids.temperature.setub(1000) - # self.vapor.temperature.setub(1000) - # self.pure_water.temperature.setub(1000) - # self.steam.temperature.setub(1000) + else: + self.del_component(self.inlet) + self.del_component(self.outlet) + self.del_component(self.solids) + self.del_component(self.vapor) + self.del_component(self.pure_water) + # self.del_component(self.steam) def initialize_build( self, @@ -358,17 +353,20 @@ def initialize_build( state_args=state_args_vapor, ) - state_args_steam = deepcopy(state_args) + try: + state_args_steam = deepcopy(state_args) - for p, j in self.properties_vapor.phase_component_set: - state_args_steam["flow_mass_phase_comp"][p, j] = 1 + for p, j in self.properties_vapor.phase_component_set: + state_args_steam["flow_mass_phase_comp"][p, j] = 1 - self.heating_steam.initialize( - outlvl=outlvl, - optarg=optarg, - solver=solver, - state_args=state_args_steam, - ) + self.heating_steam.initialize( + outlvl=outlvl, + optarg=optarg, + solver=solver, + state_args=state_args_steam, + ) + except: + pass init_log.info_high("Initialization Step 2 Complete.") @@ -504,26 +502,28 @@ def _calculate_saturation_temperature(self): solver = get_solver() results = solver.solve(m) assert_optimal_termination(results) - - print("\nPROPERTIES IN\n") - eff.properties_in[0].flow_mass_phase_comp.display() - eff.properties_in[0].mass_frac_phase_comp.display() - eff.properties_in[0].temperature.display() - eff.properties_in[0].pressure_sat.display() - eff.properties_in[0].temperature_sat_solvent.display() - - # print("\nPROPERTIES VAPOR\n") - # eff.properties_vapor[0].temperature.display() - # eff.properties_vapor[0].pressure_sat.display() - # eff.properties_vapor[0].temperature_sat_solvent.display() - - print("\nPROPERTIES HEATING STEAM\n") - eff.heating_steam[0].flow_mass_phase_comp.display() - - # eff.heating_steam[0].mass_frac_phase_comp.display() - eff.heating_steam[0].temperature.display() - eff.heating_steam[0].pressure_sat.display() - # eff.heating_steam[0].temperature_sat_solvent.display() - - eff.temperature_operating.display() - # eff.heating_steam.display() + eff.properties_in[0].display() + # eff.heating_steam[0].display() + + # print("\nPROPERTIES IN\n") + # eff.properties_in[0].flow_mass_phase_comp.display() + # eff.properties_in[0].mass_frac_phase_comp.display() + # eff.properties_in[0].temperature.display() + # eff.properties_in[0].pressure_sat.display() + # eff.properties_in[0].temperature_sat_solvent.display() + + # # print("\nPROPERTIES VAPOR\n") + # # eff.properties_vapor[0].temperature.display() + # # eff.properties_vapor[0].pressure_sat.display() + # # eff.properties_vapor[0].temperature_sat_solvent.display() + + # print("\nPROPERTIES HEATING STEAM\n") + # eff.heating_steam[0].flow_mass_phase_comp.display() + + # # eff.heating_steam[0].mass_frac_phase_comp.display() + # eff.heating_steam[0].temperature.display() + # eff.heating_steam[0].pressure_sat.display() + # # eff.heating_steam[0].temperature_sat_solvent.display() + + # eff.temperature_operating.display() + # # eff.heating_steam.display() From 2a5a38f6bafd954ce80c86c641bb71ea35b62785 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Fri, 13 Sep 2024 14:07:03 -0600 Subject: [PATCH 19/80] only make heating steam in first effect; checkpoint --- .../unit_models/multi_effect_crystallizer.py | 149 ++++++++++++++---- 1 file changed, 114 insertions(+), 35 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py index adac5ba0..eb57476b 100644 --- a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py +++ b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py @@ -12,6 +12,7 @@ from copy import deepcopy +from watertap.core.util.model_diagnostics.infeasible import * # Import Pyomo libraries from pyomo.environ import ( ConcreteModel, @@ -24,7 +25,7 @@ units as pyunits, ) from pyomo.common.config import ConfigBlock, ConfigValue, In, PositiveInt - +from pyomo.util.calc_var_value import calculate_variable_from_constraint as cvc # Import IDAES cores from idaes.core import ( declare_process_block_class, @@ -165,7 +166,10 @@ def build(self): self.effects = FlowsheetBlock(self.Effects, dynamic=False) + self.current_effect = Var() + for n, eff in self.effects.items(): + self.current_effect.fix(n) eff.effect = effect = CrystallizerEffect( property_package=self.config.property_package, property_package_vapor=self.config.property_package_vapor, @@ -173,6 +177,20 @@ def build(self): ) if n == self.first_effect: + tmp_dict = dict(**self.config.property_package_args) + tmp_dict["has_phase_equilibrium"] = False + tmp_dict["parameters"] = self.config.property_package_vapor + tmp_dict["defined_state"] = False + + effect.heating_steam = self.config.property_package_vapor.state_block_class( + self.flowsheet().config.time, + doc="Material properties of inlet heating steam", + **tmp_dict, + ) + + # self.add_port(name="steam", block=effect.heating_steam) + + @effect.Constraint( doc="Change in temperature at inlet for first effect." ) @@ -206,6 +224,7 @@ def eq_heat_transfer_effect_1(b): self.add_port(name="vapor", block=effect.properties_vapor) self.add_port(name="pure_water", block=effect.properties_pure_water) self.add_port(name="steam", block=effect.heating_steam) + self.steam.temperature.setub(1000) else: @@ -274,35 +293,53 @@ def eq_heat_transfer_effect_1(b): == prev_effect.properties_in[0].pressure, doc="Inlet properties pressure for effect {n}", ) - effect.add_component(f"eq_equiv_temp_effect_{n}", prop_in_press_constr) + effect.add_component(f"eq_equiv_press_effect_{n}", prop_in_press_constr) - prop_in_temp_constr = Constraint( - expr=effect.properties_in[0].temperature - == prev_effect.properties_in[0].temperature, - doc="Inlet properties temperature for effect {n}", + prop_in_flow_mass_water_constr = Constraint( + expr=effect.properties_in[0].flow_mass_phase_comp["Liq", "H2O"] + == prev_effect.properties_in[0].flow_mass_phase_comp["Liq", "H2O"], + doc="Inlet properties mass flow water for effect {n}", ) effect.add_component( - f"eq_equiv_pressure_effect_{n}", prop_in_temp_constr + f"eq_equiv_mass_flow_liq_water_effect_{n}", prop_in_flow_mass_water_constr ) - steam_temp_sat_constr = Constraint( - expr=effect.heating_steam[0].temperature - == prev_effect.heating_steam[0].temperature, - doc="Steam saturation temperature for effect {n}", + prop_in_flow_mass_nacl_constr = Constraint( + expr=effect.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"] + == prev_effect.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"], + doc="Inlet properties mass flow NaCl for effect {n}", ) effect.add_component( - f"eq_equiv_steam_temp_sat_effect_{n}", steam_temp_sat_constr + f"eq_equiv_mass_flow_liq_nacl_effect_{n}", prop_in_flow_mass_nacl_constr ) - steam_press_sat_constr = Constraint( - expr=effect.heating_steam[0].pressure_sat - == prev_effect.heating_steam[0].pressure_sat, - doc="Steam saturation pressure for effect {n}", + prop_in_temp_constr = Constraint( + expr=effect.properties_in[0].temperature + == prev_effect.properties_in[0].temperature, + doc="Inlet properties temperature for effect {n}", ) effect.add_component( - f"eq_equiv_steam_press_sat_effect_{n}", steam_press_sat_constr + f"eq_equiv_temp_effect_{n}", prop_in_temp_constr ) + # steam_temp_sat_constr = Constraint( + # expr=effect.heating_steam[0].temperature + # == prev_effect.heating_steam[0].temperature, + # doc="Steam saturation temperature for effect {n}", + # ) + # effect.add_component( + # f"eq_equiv_steam_temp_sat_effect_{n}", steam_temp_sat_constr + # ) + + # steam_press_sat_constr = Constraint( + # expr=effect.heating_steam[0].pressure_sat + # == prev_effect.heating_steam[0].pressure_sat, + # doc="Steam saturation pressure for effect {n}", + # ) + # effect.add_component( + # f"eq_equiv_steam_press_sat_effect_{n}", steam_press_sat_constr + # ) + cryst_growth_rate_constr = Constraint( expr=effect.crystal_growth_rate == prev_effect.crystal_growth_rate, doc="Equivalent crystal growth rate effect {n}", @@ -338,13 +375,13 @@ def eq_heat_transfer_effect_1(b): f"eq_equiv_crystallization_yield_effect_{n}", cryst_yield_constr ) - op_press_constr = Constraint( - expr=effect.pressure_operating <= prev_effect.pressure_operating, - doc=f"Equivalent operating pressure effect {n}", - ) - effect.add_component( - f"eq_equiv_pressure_operating_effect_{n}", op_press_constr - ) + # op_press_constr = Constraint( + # expr=effect.pressure_operating <= prev_effect.pressure_operating, + # doc=f"Equivalent operating pressure effect {n}", + # ) + # effect.add_component( + # f"eq_equiv_pressure_operating_effect_{n}", op_press_constr + # ) print(list(range(1, 5, 1))) @@ -356,14 +393,17 @@ def eq_heat_transfer_effect_1(b): m.fs = FlowsheetBlock(dynamic=False) m.fs.props = props.NaClParameterBlock() m.fs.vapor = WaterParameterBlock() - + m.fs.props.set_default_scaling("flow_mass_phase_comp", 1e-1, index=("Liq", "H2O")) + m.fs.props.set_default_scaling("flow_mass_phase_comp", 1e-1, index=("Liq", "NaCl")) + m.fs.props.set_default_scaling("flow_mass_phase_comp", 1e-1, index=("Vap", "H2O")) + m.fs.props.set_default_scaling("flow_mass_phase_comp", 1e-1, index=("Sol", "NaCl")) m.fs.mec = mec = MultiEffectCrystallizer( property_package=m.fs.props, property_package_vapor=m.fs.vapor ) for n, effects in mec.effects.items(): print(n, effects) - feed_flow_mass = 1 + feed_flow_mass = 2 feed_mass_frac_NaCl = 0.15 feed_pressure = 101325 feed_temperature = 273.15 + 20 @@ -377,7 +417,10 @@ def eq_heat_transfer_effect_1(b): feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl eps = 1e-6 - eff = mec.effects[1].effect + eff1 = mec.effects[1].effect + eff2 = mec.effects[2].effect + eff3 = mec.effects[3].effect + eff4 = mec.effects[4].effect mec.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"].fix( feed_flow_mass * feed_mass_frac_NaCl @@ -391,9 +434,9 @@ def eq_heat_transfer_effect_1(b): mec.inlet.pressure[0].fix(feed_pressure) mec.inlet.temperature[0].fix(feed_temperature) - eff.heating_steam[0].pressure_sat + eff1.heating_steam[0].pressure_sat - eff.heating_steam.calculate_state( + eff1.heating_steam.calculate_state( var_args={ ("pressure", None): 101325, # ("pressure_sat", None): 28100 @@ -401,15 +444,51 @@ def eq_heat_transfer_effect_1(b): }, hold_state=True, ) - eff.crystallization_yield["NaCl"].fix(crystallizer_yield) - eff.crystal_growth_rate.fix() - eff.souders_brown_constant.fix() - eff.crystal_median_length.fix() - eff.overall_heat_transfer_coefficient.fix(100) + eff1.crystallization_yield["NaCl"].fix(crystallizer_yield) + eff1.crystal_growth_rate.fix() + eff1.souders_brown_constant.fix() + eff1.crystal_median_length.fix() + eff1.overall_heat_transfer_coefficient.fix(100) # eff.pressure_operating.fix(operating_pressure_eff1 * pyunits.bar) - for (_, eff), op_pressure in zip(mec.effects.items(), operating_pressures): + for (n, eff), op_pressure in zip(mec.effects.items(), operating_pressures): eff.effect.pressure_operating.fix( pyunits.convert(op_pressure * pyunits.bar, to_units=pyunits.Pa) ) + print(f"dof effect {n} = {degrees_of_freedom(eff.effect)}") + print(f"dof = {degrees_of_freedom(m)}") + calculate_scaling_factors(m) + eff1.initialize() + # eff1.properties_in[0].display() + cvc(eff2.properties_in[0].flow_mass_phase_comp["Liq", "H2O"], eff2.eq_equiv_mass_flow_liq_water_effect_2) + cvc(eff2.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"], eff2.eq_equiv_mass_flow_liq_nacl_effect_2) + cvc(eff2.properties_in[0].flow_mass_phase_comp["Sol", "NaCl"], eff2.eq_equiv_mass_flow_sol_nacl_effect_2) + cvc(eff2.properties_in[0].flow_mass_phase_comp["Vap", "H2O"], eff2.eq_equiv_mass_flow_vap_water_effect_2) + cvc(eff2.properties_in[0].temperature, eff2.eq_equiv_temp_effect_2) + # eff2.initialize() + # eff2.properties_in[0].display() + # mec.current_effect.display() + # eff2.del_component(eff2.properties_pure_water) + # eff2.properties_pure_water.display() + # try: + flags = eff2.properties_in.initialize(hold_state=True) + eff2.properties_out.initialize(hold_state=False) + eff2.properties_solids.initialize(hold_state=False) + eff2.properties_vapor.initialize(hold_state=False) + eff2.properties_out[0].display() + eff2.properties_solids[0].phase_component_set.display() + # print(flags) + # except: + # eff2.properties_in[0].display() + # eff1.properties_in[0].display() + # eff1.heating_steam[0].display() + # print(f"dof = {degrees_of_freedom(m)}") + # for _, eff in mec.effects.items(): + # try: + # eff.effect.initialize() + # except: + # pass + # print_infeasible_constraints(m) + # print_variables_close_to_bounds(m) + # raise From 33e1789846893a3d406f58cdd4178c77d59649b3 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Fri, 13 Sep 2024 16:52:31 -0600 Subject: [PATCH 20/80] solving with 0 DOF --- .../unit_models/multi_effect_crystallizer.py | 391 +++++++++--------- 1 file changed, 207 insertions(+), 184 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py index eb57476b..aa1b81b8 100644 --- a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py +++ b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py @@ -12,7 +12,8 @@ from copy import deepcopy -from watertap.core.util.model_diagnostics.infeasible import * +from watertap.core.util.model_diagnostics.infeasible import * + # Import Pyomo libraries from pyomo.environ import ( ConcreteModel, @@ -26,6 +27,7 @@ ) from pyomo.common.config import ConfigBlock, ConfigValue, In, PositiveInt from pyomo.util.calc_var_value import calculate_variable_from_constraint as cvc + # Import IDAES cores from idaes.core import ( declare_process_block_class, @@ -175,6 +177,8 @@ def build(self): property_package_vapor=self.config.property_package_vapor, standalone=False, ) + eff.effect.properties_in[0].conc_mass_phase_comp + if n == self.first_effect: tmp_dict = dict(**self.config.property_package_args) @@ -182,14 +186,15 @@ def build(self): tmp_dict["parameters"] = self.config.property_package_vapor tmp_dict["defined_state"] = False - effect.heating_steam = self.config.property_package_vapor.state_block_class( - self.flowsheet().config.time, - doc="Material properties of inlet heating steam", - **tmp_dict, + effect.heating_steam = ( + self.config.property_package_vapor.state_block_class( + self.flowsheet().config.time, + doc="Material properties of inlet heating steam", + **tmp_dict, + ) ) # self.add_port(name="steam", block=effect.heating_steam) - @effect.Constraint( doc="Change in temperature at inlet for first effect." @@ -217,7 +222,17 @@ def eq_heat_transfer_effect_1(b): * b.area * b.delta_temperature[0] ) - + + @effect.Constraint(doc="Calculate mass flow rate of heating steam") + def eq_heating_steam_flow_rate(b): + return b.work_mechanical[0] == ( + pyunits.convert( + b.heating_steam[0].dh_vap_mass + * b.heating_steam[0].flow_mass_phase_comp["Vap", "H2O"], + to_units = pyunits.kJ / pyunits.s + ) + ) + self.add_port(name="inlet", block=effect.properties_in) self.add_port(name="outlet", block=effect.properties_out) self.add_port(name="solids", block=effect.properties_solids) @@ -268,120 +283,17 @@ def eq_heat_transfer_effect_1(b): f"eq_energy_for_effect_{n}_from_effect_{n - 1}", energy_flow_constr ) - mass_flow_solid_nacl_constr = Constraint( - expr=effect.properties_in[0].flow_mass_phase_comp["Sol", "NaCl"] - == prev_effect.properties_in[0].flow_mass_phase_comp["Sol", "NaCl"], - doc="Mass flow of solid NaCl for effect {n}", - ) - effect.add_component( - f"eq_equiv_mass_flow_sol_nacl_effect_{n}", - mass_flow_solid_nacl_constr, - ) - - mass_flow_vap_water_constr = Constraint( - expr=effect.properties_in[0].flow_mass_phase_comp["Vap", "H2O"] - == prev_effect.properties_in[0].flow_mass_phase_comp["Vap", "H2O"], - doc="Mass flow of water vapor for effect {n}", - ) - effect.add_component( - f"eq_equiv_mass_flow_vap_water_effect_{n}", - mass_flow_vap_water_constr, - ) - - prop_in_press_constr = Constraint( - expr=effect.properties_in[0].pressure - == prev_effect.properties_in[0].pressure, - doc="Inlet properties pressure for effect {n}", - ) - effect.add_component(f"eq_equiv_press_effect_{n}", prop_in_press_constr) - - prop_in_flow_mass_water_constr = Constraint( - expr=effect.properties_in[0].flow_mass_phase_comp["Liq", "H2O"] - == prev_effect.properties_in[0].flow_mass_phase_comp["Liq", "H2O"], - doc="Inlet properties mass flow water for effect {n}", - ) - effect.add_component( - f"eq_equiv_mass_flow_liq_water_effect_{n}", prop_in_flow_mass_water_constr - ) - - prop_in_flow_mass_nacl_constr = Constraint( - expr=effect.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"] - == prev_effect.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"], - doc="Inlet properties mass flow NaCl for effect {n}", - ) - effect.add_component( - f"eq_equiv_mass_flow_liq_nacl_effect_{n}", prop_in_flow_mass_nacl_constr - ) - - prop_in_temp_constr = Constraint( - expr=effect.properties_in[0].temperature - == prev_effect.properties_in[0].temperature, - doc="Inlet properties temperature for effect {n}", - ) - effect.add_component( - f"eq_equiv_temp_effect_{n}", prop_in_temp_constr - ) - - # steam_temp_sat_constr = Constraint( - # expr=effect.heating_steam[0].temperature - # == prev_effect.heating_steam[0].temperature, - # doc="Steam saturation temperature for effect {n}", - # ) - # effect.add_component( - # f"eq_equiv_steam_temp_sat_effect_{n}", steam_temp_sat_constr - # ) - - # steam_press_sat_constr = Constraint( - # expr=effect.heating_steam[0].pressure_sat - # == prev_effect.heating_steam[0].pressure_sat, - # doc="Steam saturation pressure for effect {n}", + # brine_conc_constr = Constraint( + # expr=effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] + # >= 0.95* prev_effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] # ) - # effect.add_component( - # f"eq_equiv_steam_press_sat_effect_{n}", steam_press_sat_constr + # effect.add_component(f"eq_equiv_brine_conc_effect_{n}_lb", brine_conc_constr) + # brine_conc_constr = Constraint( + # expr=effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] + # <= 1.05 * prev_effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] # ) + # effect.add_component(f"eq_equiv_brine_conc_effect_{n}_ub", brine_conc_constr) - cryst_growth_rate_constr = Constraint( - expr=effect.crystal_growth_rate == prev_effect.crystal_growth_rate, - doc="Equivalent crystal growth rate effect {n}", - ) - effect.add_component( - f"eq_equiv_crystal_growth_rate_effect_{n}", cryst_growth_rate_constr - ) - - souders_brown_constr = Constraint( - expr=effect.souders_brown_constant - == prev_effect.souders_brown_constant, - doc="Equivalent Sounders Brown constant effect {n}", - ) - effect.add_component( - f"eq_equiv_souders_brown_constant_effect_{n}", souders_brown_constr - ) - - cryst_med_len_constr = Constraint( - expr=effect.crystal_median_length - == prev_effect.crystal_median_length, - doc="Equivalent crystal median length effect {n}", - ) - effect.add_component( - f"eq_equiv_crystal_median_length_effect_{n}", cryst_med_len_constr - ) - - cryst_yield_constr = Constraint( - expr=effect.crystallization_yield["NaCl"] - == prev_effect.crystallization_yield["NaCl"], - doc="Equivalent crystallization yield effect {n}", - ) - effect.add_component( - f"eq_equiv_crystallization_yield_effect_{n}", cryst_yield_constr - ) - - # op_press_constr = Constraint( - # expr=effect.pressure_operating <= prev_effect.pressure_operating, - # doc=f"Equivalent operating pressure effect {n}", - # ) - # effect.add_component( - # f"eq_equiv_pressure_operating_effect_{n}", op_press_constr - # ) print(list(range(1, 5, 1))) @@ -391,25 +303,28 @@ def eq_heat_transfer_effect_1(b): m = ConcreteModel() m.fs = FlowsheetBlock(dynamic=False) + m.fs.props = props.NaClParameterBlock() m.fs.vapor = WaterParameterBlock() + m.fs.props.set_default_scaling("flow_mass_phase_comp", 1e-1, index=("Liq", "H2O")) m.fs.props.set_default_scaling("flow_mass_phase_comp", 1e-1, index=("Liq", "NaCl")) m.fs.props.set_default_scaling("flow_mass_phase_comp", 1e-1, index=("Vap", "H2O")) m.fs.props.set_default_scaling("flow_mass_phase_comp", 1e-1, index=("Sol", "NaCl")) + m.fs.mec = mec = MultiEffectCrystallizer( property_package=m.fs.props, property_package_vapor=m.fs.vapor ) for n, effects in mec.effects.items(): print(n, effects) - feed_flow_mass = 2 + feed_flow_mass = 1 feed_mass_frac_NaCl = 0.15 feed_pressure = 101325 feed_temperature = 273.15 + 20 crystallizer_yield = 0.5 - operating_pressures = [0.78, 0.25, 0.208, 0.095] - operating_pressure_eff1 = 0.78 # bar + operating_pressures = [0.45, 0.25, 0.208, 0.095] + operating_pressure_eff1 = 0.45 # bar operating_pressure_eff2 = 0.25 # bar operating_pressure_eff3 = 0.208 # bar operating_pressure_eff4 = 0.095 # bar @@ -422,73 +337,181 @@ def eq_heat_transfer_effect_1(b): eff3 = mec.effects[3].effect eff4 = mec.effects[4].effect - mec.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"].fix( - feed_flow_mass * feed_mass_frac_NaCl - ) - mec.inlet.flow_mass_phase_comp[0, "Liq", "H2O"].fix( - feed_flow_mass * feed_mass_frac_H2O - ) - mec.inlet.flow_mass_phase_comp[0, "Sol", "NaCl"].fix(eps) - mec.inlet.flow_mass_phase_comp[0, "Vap", "H2O"].fix(eps) - - mec.inlet.pressure[0].fix(feed_pressure) - mec.inlet.temperature[0].fix(feed_temperature) - - eff1.heating_steam[0].pressure_sat - - eff1.heating_steam.calculate_state( - var_args={ - ("pressure", None): 101325, - # ("pressure_sat", None): 28100 - ("temperature", None): 393, - }, - hold_state=True, - ) - eff1.crystallization_yield["NaCl"].fix(crystallizer_yield) - eff1.crystal_growth_rate.fix() - eff1.souders_brown_constant.fix() - eff1.crystal_median_length.fix() - eff1.overall_heat_transfer_coefficient.fix(100) - - # eff.pressure_operating.fix(operating_pressure_eff1 * pyunits.bar) for (n, eff), op_pressure in zip(mec.effects.items(), operating_pressures): + eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "H2O"].fix( + feed_flow_mass * feed_mass_frac_H2O + ) + eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"].fix( + feed_flow_mass * feed_mass_frac_NaCl + ) + eff.effect.properties_in[0].pressure.fix(feed_pressure) + eff.effect.properties_in[0].temperature.fix(feed_temperature) + + eff.effect.properties_in[0].flow_mass_phase_comp["Sol", "NaCl"].fix(eps) + eff.effect.properties_in[0].flow_mass_phase_comp["Vap", "H2O"].fix(eps) + eff.effect.crystallization_yield["NaCl"].fix(crystallizer_yield) + eff.effect.crystal_growth_rate.fix() + eff.effect.souders_brown_constant.fix() + eff.effect.crystal_median_length.fix() eff.effect.pressure_operating.fix( pyunits.convert(op_pressure * pyunits.bar, to_units=pyunits.Pa) ) + if n == 1: + eff.effect.overall_heat_transfer_coefficient.fix(100) + eff.effect.heating_steam[0].pressure_sat + eff.effect.heating_steam.calculate_state( + var_args={ + ("flow_mass_phase_comp", ("Liq", "H2O")): 0, # All vapor, no liquid + ("pressure", None): 101325, + # ("pressure_sat", None): 28100 + ("temperature", None): 393, + }, + hold_state=True, + ) + eff.effect.heating_steam[0].flow_mass_phase_comp["Vap", "H2O"].unfix() print(f"dof effect {n} = {degrees_of_freedom(eff.effect)}") - print(f"dof = {degrees_of_freedom(m)}") + print(f"dof before init = {degrees_of_freedom(m)}") calculate_scaling_factors(m) - eff1.initialize() - # eff1.properties_in[0].display() - cvc(eff2.properties_in[0].flow_mass_phase_comp["Liq", "H2O"], eff2.eq_equiv_mass_flow_liq_water_effect_2) - cvc(eff2.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"], eff2.eq_equiv_mass_flow_liq_nacl_effect_2) - cvc(eff2.properties_in[0].flow_mass_phase_comp["Sol", "NaCl"], eff2.eq_equiv_mass_flow_sol_nacl_effect_2) - cvc(eff2.properties_in[0].flow_mass_phase_comp["Vap", "H2O"], eff2.eq_equiv_mass_flow_vap_water_effect_2) - cvc(eff2.properties_in[0].temperature, eff2.eq_equiv_temp_effect_2) - # eff2.initialize() - # eff2.properties_in[0].display() - # mec.current_effect.display() - # eff2.del_component(eff2.properties_pure_water) - # eff2.properties_pure_water.display() - # try: - flags = eff2.properties_in.initialize(hold_state=True) - eff2.properties_out.initialize(hold_state=False) - eff2.properties_solids.initialize(hold_state=False) - eff2.properties_vapor.initialize(hold_state=False) - eff2.properties_out[0].display() - eff2.properties_solids[0].phase_component_set.display() - # print(flags) - # except: - # eff2.properties_in[0].display() - # eff1.properties_in[0].display() - # eff1.heating_steam[0].display() - # print(f"dof = {degrees_of_freedom(m)}") - # for _, eff in mec.effects.items(): - # try: - # eff.effect.initialize() - # except: - # pass - # print_infeasible_constraints(m) - # print_variables_close_to_bounds(m) - # raise + + for n, eff in mec.effects.items(): + try: + eff.effect.initialize() + except: + # print_infeasible_constraints(eff.effect) + pass + + brine_conc = eff1.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"].value + + for n, eff in mec.effects.items(): + print(f"\nEFFECT {n}\n") + # eff.effect.properties_pure_water[0].temperature.display() + eff.effect.pressure_operating.display() + eff.effect.overall_heat_transfer_coefficient.display() + eff.effect.properties_vapor[0].temperature.display() + eff.effect.temperature_operating.display() + eff.effect.properties_in[0].flow_mass_phase_comp.display() + eff.effect.properties_in[0].conc_mass_phase_comp.display() + eff.effect.properties_out[0].flow_mass_phase_comp.display() + eff.effect.overall_heat_transfer_coefficient.setlb(99) + if n != 1: + eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "H2O"].unfix() + eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"].unfix() + eff.effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"].fix(brine_conc) + + solver = get_solver() + print(f"dof before solve 1 = {degrees_of_freedom(m)}") + results = solver.solve(m) + print(f"termination {results.solver.termination_condition}") + + for n, eff in mec.effects.items(): + eff.effect.overall_heat_transfer_coefficient.fix(100) + + print(f"dof before solve 2 = {degrees_of_freedom(m)}") + results = solver.solve(m) + print(f"termination {results.solver.termination_condition}") + + for n, eff in mec.effects.items(): + print(f"\nEFFECT {n}\n") + # eff.effect.properties_pure_water[0].temperature.display() + eff.effect.pressure_operating.display() + eff.effect.overall_heat_transfer_coefficient.display() + eff.effect.properties_vapor[0].temperature.display() + eff.effect.temperature_operating.display() + eff.effect.properties_in[0].flow_mass_phase_comp.display() + eff.effect.properties_in[0].conc_mass_phase_comp.display() + eff.effect.properties_out[0].flow_mass_phase_comp.display() + + + # mass_flow_solid_nacl_constr = Constraint( + # expr=effect.properties_in[0].flow_mass_phase_comp["Sol", "NaCl"] + # == prev_effect.properties_in[0].flow_mass_phase_comp["Sol", "NaCl"], + # doc="Mass flow of solid NaCl for effect {n}", + # ) + # effect.add_component( + # f"eq_equiv_mass_flow_sol_nacl_effect_{n}", + # mass_flow_solid_nacl_constr, + # ) + + # mass_flow_vap_water_constr = Constraint( + # expr=effect.properties_in[0].flow_mass_phase_comp["Vap", "H2O"] + # == prev_effect.properties_in[0].flow_mass_phase_comp["Vap", "H2O"], + # doc="Mass flow of water vapor for effect {n}", + # ) + # effect.add_component( + # f"eq_equiv_mass_flow_vap_water_effect_{n}", + # mass_flow_vap_water_constr, + # ) + + # prop_in_press_constr = Constraint( + # expr=effect.properties_in[0].pressure + # == prev_effect.properties_in[0].pressure, + # doc="Inlet properties pressure for effect {n}", + # ) + # effect.add_component(f"eq_equiv_press_effect_{n}", prop_in_press_constr) + + # prop_in_press_constr_ub = Constraint( + # expr=effect.properties_in[0].pressure + # <= 1.0001 * prev_effect.properties_in[0].pressure, + # doc="Inlet properties pressure for effect {n}", + # ) + # effect.add_component(f"eq_equiv_press_effect_{n}_ub", prop_in_press_constr_ub) + + # prop_in_press_constr_lb = Constraint( + # expr=effect.properties_in[0].pressure + # >= 0.9999 * prev_effect.properties_in[0].pressure, + # doc="Inlet properties pressure for effect {n}", + # ) + # effect.add_component(f"eq_equiv_press_effect_{n}_lb", prop_in_press_constr_lb) + + # prop_in_temp_constr = Constraint( + # expr=effect.properties_in[0].temperature + # == prev_effect.properties_in[0].temperature, + # doc="Inlet properties temperature for effect {n}", + # ) + # effect.add_component( + # f"eq_equiv_temp_effect_{n}", prop_in_temp_constr + # ) + + # cryst_growth_rate_constr = Constraint( + # expr=effect.crystal_growth_rate == prev_effect.crystal_growth_rate, + # doc="Equivalent crystal growth rate effect {n}", + # ) + # effect.add_component( + # f"eq_equiv_crystal_growth_rate_effect_{n}", cryst_growth_rate_constr + # ) + + # souders_brown_constr = Constraint( + # expr=effect.souders_brown_constant + # == prev_effect.souders_brown_constant, + # doc="Equivalent Sounders Brown constant effect {n}", + # ) + # effect.add_component( + # f"eq_equiv_souders_brown_constant_effect_{n}", souders_brown_constr + # ) + + # cryst_med_len_constr = Constraint( + # expr=effect.crystal_median_length + # == prev_effect.crystal_median_length, + # doc="Equivalent crystal median length effect {n}", + # ) + # effect.add_component( + # f"eq_equiv_crystal_median_length_effect_{n}", cryst_med_len_constr + # ) + + # cryst_yield_constr = Constraint( + # expr=effect.crystallization_yield["NaCl"] + # == prev_effect.crystallization_yield["NaCl"], + # doc="Equivalent crystallization yield effect {n}", + # ) + # effect.add_component( + # f"eq_equiv_crystallization_yield_effect_{n}", cryst_yield_constr + # ) + + # op_press_constr = Constraint( + # expr=effect.pressure_operating <= prev_effect.pressure_operating, + # doc=f"Equivalent operating pressure effect {n}", + # ) + # effect.add_component( + # f"eq_equiv_pressure_operating_effect_{n}", op_press_constr + # ) \ No newline at end of file From 3ebfdb9ed9d5cb6b606ffcd8af2d2f6df2c4b05d Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Fri, 13 Sep 2024 17:09:04 -0600 Subject: [PATCH 21/80] initial initialization(); clean up --- .../unit_models/multi_effect_crystallizer.py | 243 ++++++++++-------- 1 file changed, 138 insertions(+), 105 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py index aa1b81b8..74e9d524 100644 --- a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py +++ b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py @@ -168,10 +168,7 @@ def build(self): self.effects = FlowsheetBlock(self.Effects, dynamic=False) - self.current_effect = Var() - for n, eff in self.effects.items(): - self.current_effect.fix(n) eff.effect = effect = CrystallizerEffect( property_package=self.config.property_package, property_package_vapor=self.config.property_package_vapor, @@ -222,17 +219,17 @@ def eq_heat_transfer_effect_1(b): * b.area * b.delta_temperature[0] ) - + @effect.Constraint(doc="Calculate mass flow rate of heating steam") def eq_heating_steam_flow_rate(b): return b.work_mechanical[0] == ( pyunits.convert( - b.heating_steam[0].dh_vap_mass - * b.heating_steam[0].flow_mass_phase_comp["Vap", "H2O"], - to_units = pyunits.kJ / pyunits.s + b.heating_steam[0].dh_vap_mass + * b.heating_steam[0].flow_mass_phase_comp["Vap", "H2O"], + to_units=pyunits.kJ / pyunits.s, ) ) - + self.add_port(name="inlet", block=effect.properties_in) self.add_port(name="outlet", block=effect.properties_out) self.add_port(name="solids", block=effect.properties_solids) @@ -294,9 +291,43 @@ def eq_heating_steam_flow_rate(b): # ) # effect.add_component(f"eq_equiv_brine_conc_effect_{n}_ub", brine_conc_constr) + def initialize_build( + self, + state_args=None, + outlvl=idaeslog.NOTSET, + solver=None, + optarg=None, + ): + """ + General wrapper for pressure changer initialization routines + + Keyword Arguments: + state_args : a dict of arguments to be passed to the property + package(s) to provide an initial state for + initialization (see documentation of the specific + property package) (default = {}). + outlvl : sets output level of initialization routine + optarg : solver options dictionary object (default=None) + solver : str indicating which solver to use during + initialization (default = None) + + Returns: None + """ + init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") + solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit") + + opt = get_solver(solver, optarg) + + for n, eff in self.effects.items(): + eff.effect.initialize() + init_log.info(f"Initialization of Effect {n} Complete.") + # if n != 1: + # conc = self.effects[1].effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"].value + # eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "H2O"].unfix() + # eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"].unfix() + # eff.effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"].fix(conc) -print(list(range(1, 5, 1))) if __name__ == "__main__": import watertap.property_models.unit_specific.cryst_prop_pack as props from watertap.property_models.water_prop_pack import WaterParameterBlock @@ -361,7 +392,7 @@ def eq_heating_steam_flow_rate(b): eff.effect.heating_steam[0].pressure_sat eff.effect.heating_steam.calculate_state( var_args={ - ("flow_mass_phase_comp", ("Liq", "H2O")): 0, # All vapor, no liquid + ("flow_mass_phase_comp", ("Liq", "H2O")): 0, # All vapor, no liquid ("pressure", None): 101325, # ("pressure_sat", None): 28100 ("temperature", None): 393, @@ -373,6 +404,7 @@ def eq_heating_steam_flow_rate(b): print(f"dof before init = {degrees_of_freedom(m)}") calculate_scaling_factors(m) + # mec.initialize() for n, eff in mec.effects.items(): try: @@ -397,7 +429,9 @@ def eq_heating_steam_flow_rate(b): if n != 1: eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "H2O"].unfix() eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"].unfix() - eff.effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"].fix(brine_conc) + eff.effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"].fix( + brine_conc + ) solver = get_solver() print(f"dof before solve 1 = {degrees_of_freedom(m)}") @@ -410,7 +444,7 @@ def eq_heating_steam_flow_rate(b): print(f"dof before solve 2 = {degrees_of_freedom(m)}") results = solver.solve(m) print(f"termination {results.solver.termination_condition}") - + for n, eff in mec.effects.items(): print(f"\nEFFECT {n}\n") # eff.effect.properties_pure_water[0].temperature.display() @@ -422,96 +456,95 @@ def eq_heating_steam_flow_rate(b): eff.effect.properties_in[0].conc_mass_phase_comp.display() eff.effect.properties_out[0].flow_mass_phase_comp.display() - - # mass_flow_solid_nacl_constr = Constraint( - # expr=effect.properties_in[0].flow_mass_phase_comp["Sol", "NaCl"] - # == prev_effect.properties_in[0].flow_mass_phase_comp["Sol", "NaCl"], - # doc="Mass flow of solid NaCl for effect {n}", - # ) - # effect.add_component( - # f"eq_equiv_mass_flow_sol_nacl_effect_{n}", - # mass_flow_solid_nacl_constr, - # ) - - # mass_flow_vap_water_constr = Constraint( - # expr=effect.properties_in[0].flow_mass_phase_comp["Vap", "H2O"] - # == prev_effect.properties_in[0].flow_mass_phase_comp["Vap", "H2O"], - # doc="Mass flow of water vapor for effect {n}", - # ) - # effect.add_component( - # f"eq_equiv_mass_flow_vap_water_effect_{n}", - # mass_flow_vap_water_constr, - # ) - - # prop_in_press_constr = Constraint( - # expr=effect.properties_in[0].pressure - # == prev_effect.properties_in[0].pressure, - # doc="Inlet properties pressure for effect {n}", - # ) - # effect.add_component(f"eq_equiv_press_effect_{n}", prop_in_press_constr) - - # prop_in_press_constr_ub = Constraint( - # expr=effect.properties_in[0].pressure - # <= 1.0001 * prev_effect.properties_in[0].pressure, - # doc="Inlet properties pressure for effect {n}", - # ) - # effect.add_component(f"eq_equiv_press_effect_{n}_ub", prop_in_press_constr_ub) - - # prop_in_press_constr_lb = Constraint( - # expr=effect.properties_in[0].pressure - # >= 0.9999 * prev_effect.properties_in[0].pressure, - # doc="Inlet properties pressure for effect {n}", - # ) - # effect.add_component(f"eq_equiv_press_effect_{n}_lb", prop_in_press_constr_lb) - - # prop_in_temp_constr = Constraint( - # expr=effect.properties_in[0].temperature - # == prev_effect.properties_in[0].temperature, - # doc="Inlet properties temperature for effect {n}", - # ) - # effect.add_component( - # f"eq_equiv_temp_effect_{n}", prop_in_temp_constr - # ) - - # cryst_growth_rate_constr = Constraint( - # expr=effect.crystal_growth_rate == prev_effect.crystal_growth_rate, - # doc="Equivalent crystal growth rate effect {n}", - # ) - # effect.add_component( - # f"eq_equiv_crystal_growth_rate_effect_{n}", cryst_growth_rate_constr - # ) - - # souders_brown_constr = Constraint( - # expr=effect.souders_brown_constant - # == prev_effect.souders_brown_constant, - # doc="Equivalent Sounders Brown constant effect {n}", - # ) - # effect.add_component( - # f"eq_equiv_souders_brown_constant_effect_{n}", souders_brown_constr - # ) - - # cryst_med_len_constr = Constraint( - # expr=effect.crystal_median_length - # == prev_effect.crystal_median_length, - # doc="Equivalent crystal median length effect {n}", - # ) - # effect.add_component( - # f"eq_equiv_crystal_median_length_effect_{n}", cryst_med_len_constr - # ) - - # cryst_yield_constr = Constraint( - # expr=effect.crystallization_yield["NaCl"] - # == prev_effect.crystallization_yield["NaCl"], - # doc="Equivalent crystallization yield effect {n}", - # ) - # effect.add_component( - # f"eq_equiv_crystallization_yield_effect_{n}", cryst_yield_constr - # ) - - # op_press_constr = Constraint( - # expr=effect.pressure_operating <= prev_effect.pressure_operating, - # doc=f"Equivalent operating pressure effect {n}", - # ) - # effect.add_component( - # f"eq_equiv_pressure_operating_effect_{n}", op_press_constr - # ) \ No newline at end of file + # mass_flow_solid_nacl_constr = Constraint( + # expr=effect.properties_in[0].flow_mass_phase_comp["Sol", "NaCl"] + # == prev_effect.properties_in[0].flow_mass_phase_comp["Sol", "NaCl"], + # doc="Mass flow of solid NaCl for effect {n}", + # ) + # effect.add_component( + # f"eq_equiv_mass_flow_sol_nacl_effect_{n}", + # mass_flow_solid_nacl_constr, + # ) + + # mass_flow_vap_water_constr = Constraint( + # expr=effect.properties_in[0].flow_mass_phase_comp["Vap", "H2O"] + # == prev_effect.properties_in[0].flow_mass_phase_comp["Vap", "H2O"], + # doc="Mass flow of water vapor for effect {n}", + # ) + # effect.add_component( + # f"eq_equiv_mass_flow_vap_water_effect_{n}", + # mass_flow_vap_water_constr, + # ) + + # prop_in_press_constr = Constraint( + # expr=effect.properties_in[0].pressure + # == prev_effect.properties_in[0].pressure, + # doc="Inlet properties pressure for effect {n}", + # ) + # effect.add_component(f"eq_equiv_press_effect_{n}", prop_in_press_constr) + + # prop_in_press_constr_ub = Constraint( + # expr=effect.properties_in[0].pressure + # <= 1.0001 * prev_effect.properties_in[0].pressure, + # doc="Inlet properties pressure for effect {n}", + # ) + # effect.add_component(f"eq_equiv_press_effect_{n}_ub", prop_in_press_constr_ub) + + # prop_in_press_constr_lb = Constraint( + # expr=effect.properties_in[0].pressure + # >= 0.9999 * prev_effect.properties_in[0].pressure, + # doc="Inlet properties pressure for effect {n}", + # ) + # effect.add_component(f"eq_equiv_press_effect_{n}_lb", prop_in_press_constr_lb) + + # prop_in_temp_constr = Constraint( + # expr=effect.properties_in[0].temperature + # == prev_effect.properties_in[0].temperature, + # doc="Inlet properties temperature for effect {n}", + # ) + # effect.add_component( + # f"eq_equiv_temp_effect_{n}", prop_in_temp_constr + # ) + + # cryst_growth_rate_constr = Constraint( + # expr=effect.crystal_growth_rate == prev_effect.crystal_growth_rate, + # doc="Equivalent crystal growth rate effect {n}", + # ) + # effect.add_component( + # f"eq_equiv_crystal_growth_rate_effect_{n}", cryst_growth_rate_constr + # ) + + # souders_brown_constr = Constraint( + # expr=effect.souders_brown_constant + # == prev_effect.souders_brown_constant, + # doc="Equivalent Sounders Brown constant effect {n}", + # ) + # effect.add_component( + # f"eq_equiv_souders_brown_constant_effect_{n}", souders_brown_constr + # ) + + # cryst_med_len_constr = Constraint( + # expr=effect.crystal_median_length + # == prev_effect.crystal_median_length, + # doc="Equivalent crystal median length effect {n}", + # ) + # effect.add_component( + # f"eq_equiv_crystal_median_length_effect_{n}", cryst_med_len_constr + # ) + + # cryst_yield_constr = Constraint( + # expr=effect.crystallization_yield["NaCl"] + # == prev_effect.crystallization_yield["NaCl"], + # doc="Equivalent crystallization yield effect {n}", + # ) + # effect.add_component( + # f"eq_equiv_crystallization_yield_effect_{n}", cryst_yield_constr + # ) + + # op_press_constr = Constraint( + # expr=effect.pressure_operating <= prev_effect.pressure_operating, + # doc=f"Equivalent operating pressure effect {n}", + # ) + # effect.add_component( + # f"eq_equiv_pressure_operating_effect_{n}", op_press_constr + # ) From 5aae54f8904a4a5481ed40a78efc32dc87d56768 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Sat, 14 Sep 2024 08:37:59 -0600 Subject: [PATCH 22/80] initial costing package --- .../units/multi_effect_crystallizer.py | 286 ++++++++++++++++++ 1 file changed, 286 insertions(+) create mode 100644 src/watertap_contrib/reflo/costing/units/multi_effect_crystallizer.py diff --git a/src/watertap_contrib/reflo/costing/units/multi_effect_crystallizer.py b/src/watertap_contrib/reflo/costing/units/multi_effect_crystallizer.py new file mode 100644 index 00000000..d063619c --- /dev/null +++ b/src/watertap_contrib/reflo/costing/units/multi_effect_crystallizer.py @@ -0,0 +1,286 @@ +################################################################################# +# WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# National Renewable Energy Laboratory, and National Energy Technology +# Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +################################################################################# + +import pyomo.environ as pyo +from idaes.core.util.misc import StrEnum +from idaes.core.util.constants import Constants +from idaes.core.util.exceptions import ConfigurationError +from watertap.costing.util import register_costing_parameter_block +from watertap_contrib.reflo.costing.util import ( + make_capital_cost_var, + make_fixed_operating_cost_var, +) + + +class MultiEffectCrystallizerCostType(StrEnum): + mass_basis = "mass_basis" + volume_basis = "volume_basis" + + +def build_multi_effect_crystallizer_cost_param_block(blk): + + blk.steam_pressure = pyo.Var( + initialize=3, + units=pyo.units.bar, + doc="Steam pressure (gauge) for crystallizer heating: 3 bar default based on Dutta example", + ) + + blk.efficiency_pump = pyo.Var( + initialize=0.7, + units=pyo.units.dimensionless, + doc="Crystallizer pump efficiency - assumed", + ) + + blk.pump_head_height = pyo.Var( + initialize=1, + units=pyo.units.m, + doc="Crystallizer pump head height - assumed, unvalidated", + ) + + # Crystallizer operating cost information from literature + blk.fob_unit_cost = pyo.Var( + initialize=675000, + doc="Forced circulation crystallizer reference free-on-board cost (Woods, 2007)", + units=pyo.units.USD_2007, + ) + + blk.ref_capacity = pyo.Var( + initialize=1, + doc="Forced circulation crystallizer reference crystal capacity (Woods, 2007)", + units=pyo.units.kg / pyo.units.s, + ) + + blk.ref_exponent = pyo.Var( + initialize=0.53, + doc="Forced circulation crystallizer cost exponent factor (Woods, 2007)", + units=pyo.units.dimensionless, + ) + + blk.iec_percent = pyo.Var( + initialize=1.43, + doc="Forced circulation crystallizer installed equipment cost (Diab and Gerogiorgis, 2017)", + units=pyo.units.dimensionless, + ) + + blk.volume_cost = pyo.Var( + initialize=16320, + doc="Forced circulation crystallizer cost per volume (Yusuf et al., 2019)", + units=pyo.units.USD_2007, ## TODO: Needs confirmation, but data is from Perry apparently + ) + + blk.vol_basis_exponent = pyo.Var( + initialize=0.47, + doc="Forced circulation crystallizer volume-based cost exponent (Yusuf et al., 2019)", + units=pyo.units.dimensionless, + ) + + blk.steam_cost = pyo.Var( + initialize=0.004, + units=pyo.units.USD_2018 / (pyo.units.meter**3), + doc="Steam cost, Panagopoulos (2019)", + ) + + blk.NaCl_recovery_value = pyo.Var( + initialize=0, + units=pyo.units.USD_2018 / pyo.units.kg, + doc="Unit recovery value of NaCl", + ) + + costing = blk.parent_block() + costing.register_flow_type("steam", blk.steam_cost) + costing.register_flow_type("NaCl", blk.NaCl_recovery_value) + + +def cost_multi_effect_crystallizer(blk, cost_type=MultiEffectCrystallizerCostType.mass_basis): + """ + Function for costing the FC crystallizer by the mass flow of produced crystals. + The operating cost model assumes that heat is supplied via condensation of saturated steam (see Dutta et al.) + + Args: + cost_type: Option for crystallizer cost function type - volume or mass basis + """ + if cost_type == MultiEffectCrystallizerCostType.mass_basis: + cost_crystallizer_by_crystal_mass(blk) + elif cost_type == MultiEffectCrystallizerCostType.volume_basis: + cost_crystallizer_by_volume(blk) + else: + raise ConfigurationError( + f"{blk.unit_model.name} received invalid argument for cost_type:" + f" {cost_type}. Argument must be a member of the CrystallizerCostType Enum." + ) + + +def _cost_crystallizer_flows(blk): + blk.costing_package.cost_flow( + pyo.units.convert( + ( + blk.unit_model.magma_circulation_flow_vol + * blk.unit_model.dens_mass_slurry + * Constants.acceleration_gravity + * blk.costing_package.crystallizer.pump_head_height + / blk.costing_package.crystallizer.efficiency_pump + ), + to_units=pyo.units.kW, + ), + "electricity", + ) + + blk.costing_package.cost_flow( + pyo.units.convert( + (blk.unit_model.work_mechanical[0] / _compute_steam_properties(blk)), + to_units=pyo.units.m**3 / pyo.units.s, + ), + "steam", + ) + + blk.costing_package.cost_flow( + blk.unit_model.solids.flow_mass_phase_comp[0, "Sol", "NaCl"], + "NaCl", + ) + + +@register_costing_parameter_block( + build_rule=build_multi_effect_crystallizer_cost_param_block, + parameter_block_name="crystallizer", +) +def cost_crystallizer_by_crystal_mass(blk): + """ + Mass-based capital cost for FC crystallizer + """ + make_capital_cost_var(blk) + blk.costing_package.add_cost_factor(blk, "TIC") + blk.capital_cost_constraint = pyo.Constraint( + expr=blk.capital_cost + == blk.cost_factor + * blk.unit_model.config.number_effects * (pyo.units.convert( + ( + blk.costing_package.crystallizer.iec_percent + * blk.costing_package.crystallizer.fob_unit_cost + * ( + sum( + blk.unit_model.solids.flow_mass_phase_comp[0, "Sol", j] + for j in blk.unit_model.config.property_package.solute_set + ) + / blk.costing_package.crystallizer.ref_capacity + ) + ** blk.costing_package.crystallizer.ref_exponent + ), + to_units=blk.costing_package.base_currency, + )) + ) + _cost_crystallizer_flows(blk) + + +@register_costing_parameter_block( + build_rule=build_multi_effect_crystallizer_cost_param_block, + parameter_block_name="crystallizer", +) +def cost_crystallizer_by_volume(blk): + """ + Volume-based capital cost for FC crystallizer + """ + make_capital_cost_var(blk) + blk.costing_package.add_cost_factor(blk, "TIC") + blk.capital_cost_constraint = pyo.Constraint( + expr=blk.capital_cost + == blk.unit_model.config.number_effects * blk.cost_factor + * pyo.units.convert( + ( + blk.costing_package.crystallizer.volume_cost + * ( + ( + pyo.units.convert( + blk.unit_model.volume_suspension + * ( + blk.unit_model.height_crystallizer + / blk.unit_model.height_slurry + ), + to_units=(pyo.units.ft) ** 3, + ) + ) + / pyo.units.ft**3 + ) + ** blk.costing_package.crystallizer.vol_basis_exponent + ), + to_units=blk.costing_package.base_currency, + ) + ) + _cost_crystallizer_flows(blk) + + +def _compute_steam_properties(blk): + """ + Function for computing saturated steam properties for thermal heating estimation. + + Args: + pressure_sat: Steam gauge pressure in bar + + Out: + Steam thermal capacity (latent heat of condensation * density) in kJ/m3 + """ + pressure_sat = blk.costing_package.crystallizer.steam_pressure + # 1. Compute saturation temperature of steam: computed from El-Dessouky expression + tsat_constants = [ + 42.6776 * pyo.units.K, + -3892.7 * pyo.units.K, + 1000 * pyo.units.kPa, + -9.48654 * pyo.units.dimensionless, + ] + psat = ( + pyo.units.convert(pressure_sat, to_units=pyo.units.kPa) + + 101.325 * pyo.units.kPa + ) + temperature_sat = tsat_constants[0] + tsat_constants[1] / ( + pyo.log(psat / tsat_constants[2]) + tsat_constants[3] + ) + + # 2. Compute latent heat of condensation/vaporization: computed from Sharqawy expression + t = temperature_sat - 273.15 * pyo.units.K + enth_mass_units = pyo.units.J / pyo.units.kg + t_inv_units = pyo.units.K**-1 + dh_constants = [ + 2.501e6 * enth_mass_units, + -2.369e3 * enth_mass_units * t_inv_units**1, + 2.678e-1 * enth_mass_units * t_inv_units**2, + -8.103e-3 * enth_mass_units * t_inv_units**3, + -2.079e-5 * enth_mass_units * t_inv_units**4, + ] + dh_vap = ( + dh_constants[0] + + dh_constants[1] * t + + dh_constants[2] * t**2 + + dh_constants[3] * t**3 + + dh_constants[4] * t**4 + ) + dh_vap = pyo.units.convert(dh_vap, to_units=pyo.units.kJ / pyo.units.kg) + + # 3. Compute specific volume: computed from Affandi expression (Eq 5) + t_critical = 647.096 * pyo.units.K + t_red = temperature_sat / t_critical # Reduced temperature + sp_vol_constants = [ + -7.75883 * pyo.units.dimensionless, + 3.23753 * pyo.units.dimensionless, + 2.05755 * pyo.units.dimensionless, + -0.06052 * pyo.units.dimensionless, + 0.00529 * pyo.units.dimensionless, + ] + log_sp_vol = ( + sp_vol_constants[0] + + sp_vol_constants[1] * (pyo.log(1 / t_red)) ** 0.4 + + sp_vol_constants[2] / (t_red**2) + + sp_vol_constants[3] / (t_red**4) + + sp_vol_constants[4] / (t_red**5) + ) + sp_vol = pyo.exp(log_sp_vol) * pyo.units.m**3 / pyo.units.kg + + # 4. Return specific energy: density * latent heat + return dh_vap / sp_vol From 2b237e9c3b0d589f4b9ea0280248a80f52c788e5 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Sat, 14 Sep 2024 09:40:46 -0600 Subject: [PATCH 23/80] MEC costing pkg checkpoint --- .../units/multi_effect_crystallizer.py | 154 ++++++++++++------ 1 file changed, 103 insertions(+), 51 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/units/multi_effect_crystallizer.py b/src/watertap_contrib/reflo/costing/units/multi_effect_crystallizer.py index d063619c..cfacf972 100644 --- a/src/watertap_contrib/reflo/costing/units/multi_effect_crystallizer.py +++ b/src/watertap_contrib/reflo/costing/units/multi_effect_crystallizer.py @@ -28,23 +28,23 @@ class MultiEffectCrystallizerCostType(StrEnum): def build_multi_effect_crystallizer_cost_param_block(blk): - blk.steam_pressure = pyo.Var( - initialize=3, - units=pyo.units.bar, - doc="Steam pressure (gauge) for crystallizer heating: 3 bar default based on Dutta example", - ) - - blk.efficiency_pump = pyo.Var( - initialize=0.7, - units=pyo.units.dimensionless, - doc="Crystallizer pump efficiency - assumed", - ) - - blk.pump_head_height = pyo.Var( - initialize=1, - units=pyo.units.m, - doc="Crystallizer pump head height - assumed, unvalidated", - ) + # blk.steam_pressure = pyo.Var( + # initialize=3, + # units=pyo.units.bar, + # doc="Steam pressure (gauge) for crystallizer heating: 3 bar default based on Dutta example", + # ) + + # blk.efficiency_pump = pyo.Var( + # initialize=0.7, + # units=pyo.units.dimensionless, + # doc="Crystallizer pump efficiency - assumed", + # ) + + # blk.pump_head_height = pyo.Var( + # initialize=1, + # units=pyo.units.m, + # doc="Crystallizer pump head height - assumed, unvalidated", + # ) # Crystallizer operating cost information from literature blk.fob_unit_cost = pyo.Var( @@ -99,7 +99,10 @@ def build_multi_effect_crystallizer_cost_param_block(blk): costing.register_flow_type("steam", blk.steam_cost) costing.register_flow_type("NaCl", blk.NaCl_recovery_value) - +@register_costing_parameter_block( + build_rule=build_multi_effect_crystallizer_cost_param_block, + parameter_block_name="multi_effect_crystallizer", +) def cost_multi_effect_crystallizer(blk, cost_type=MultiEffectCrystallizerCostType.mass_basis): """ Function for costing the FC crystallizer by the mass flow of produced crystals. @@ -108,76 +111,125 @@ def cost_multi_effect_crystallizer(blk, cost_type=MultiEffectCrystallizerCostTyp Args: cost_type: Option for crystallizer cost function type - volume or mass basis """ + global costing_package + costing_package = blk.costing_package + make_capital_cost_var(blk) + blk.costing_package.add_cost_factor(blk, "TIC") + if cost_type == MultiEffectCrystallizerCostType.mass_basis: - cost_crystallizer_by_crystal_mass(blk) + costing_method = cost_crystallizer_effect_by_crystal_mass elif cost_type == MultiEffectCrystallizerCostType.volume_basis: - cost_crystallizer_by_volume(blk) + costing_method = cost_crystallizer_by_volume else: raise ConfigurationError( f"{blk.unit_model.name} received invalid argument for cost_type:" f" {cost_type}. Argument must be a member of the CrystallizerCostType Enum." ) + + # costing_method(blk) + total_capex_expr = 0 + for n, eff in blk.unit_model.effects.items(): + capex_var = pyo.Var( + initialize=1e5, + units=costing_package.base_currency, + doc=f"Capital cost effect {n}", + ) + capital_cost_effect = costing_method(eff.effect) + capex_constr = pyo.Constraint(expr=capex_var == capital_cost_effect) + blk.add_component(f"capital_cost_effect_{n}", capex_var) + blk.add_component(f"capital_cost_effect_{n}_constraint", capex_constr) + total_capex_expr += capital_cost_effect + + blk.capital_cost_constraint = pyo.Constraint(expr=blk.capital_cost == blk.cost_factor * total_capex_expr) def _cost_crystallizer_flows(blk): - blk.costing_package.cost_flow( + + # costing_package = blk.flowsheet().flowsheet().costing + costing_package.cost_flow( pyo.units.convert( ( - blk.unit_model.magma_circulation_flow_vol - * blk.unit_model.dens_mass_slurry + blk.magma_circulation_flow_vol + * blk.dens_mass_slurry * Constants.acceleration_gravity - * blk.costing_package.crystallizer.pump_head_height - / blk.costing_package.crystallizer.efficiency_pump + * blk.pump_head_height + / blk.efficiency_pump ), to_units=pyo.units.kW, ), "electricity", ) - blk.costing_package.cost_flow( + costing_package.cost_flow( pyo.units.convert( - (blk.unit_model.work_mechanical[0] / _compute_steam_properties(blk)), + (blk.work_mechanical[0] / _compute_steam_properties(blk)), to_units=pyo.units.m**3 / pyo.units.s, ), "steam", ) - blk.costing_package.cost_flow( - blk.unit_model.solids.flow_mass_phase_comp[0, "Sol", "NaCl"], + costing_package.cost_flow( + blk.properties_solids[0].flow_mass_phase_comp["Sol", "NaCl"], "NaCl", ) -@register_costing_parameter_block( - build_rule=build_multi_effect_crystallizer_cost_param_block, - parameter_block_name="crystallizer", -) -def cost_crystallizer_by_crystal_mass(blk): +# @register_costing_parameter_block( +# build_rule=build_multi_effect_crystallizer_cost_param_block, +# parameter_block_name="multi_effect_crystallizer", +# ) +def cost_crystallizer_effect_by_crystal_mass(blk): """ Mass-based capital cost for FC crystallizer """ - make_capital_cost_var(blk) - blk.costing_package.add_cost_factor(blk, "TIC") - blk.capital_cost_constraint = pyo.Constraint( - expr=blk.capital_cost - == blk.cost_factor - * blk.unit_model.config.number_effects * (pyo.units.convert( + + # blk.capital_cost = pyo.Var( + # initialize=1e5, + # # domain=pyo.NonNegativeReals, + # units=costing_package.base_currency, + # doc="Unit capital cost", + # ) + # costing_package = blk.flowsheet().flowsheet().costing + # costing_package.add_cost_factor(blk, None) + # blk.capital_cost_constraint = pyo.Constraint( + # expr=blk.capital_cost + # == blk.cost_factor + # * (pyo.units.convert( + # ( + # costing_package.multi_effect_crystallizer.iec_percent + # * costing_package.multi_effect_crystallizer.fob_unit_cost + # * ( + # sum( + # blk.properties_solids[0].flow_mass_phase_comp["Sol", j] + # for j in blk.config.property_package.solute_set + # ) + # / costing_package.multi_effect_crystallizer.ref_capacity + # ) + # ** costing_package.multi_effect_crystallizer.ref_exponent + # ), + # to_units=costing_package.base_currency, + # )) + # ) + + # for n, eff in blk.unit_model.effects.items(): + + _cost_crystallizer_flows(blk) + capital_cost_effect = pyo.units.convert( ( - blk.costing_package.crystallizer.iec_percent - * blk.costing_package.crystallizer.fob_unit_cost + costing_package.multi_effect_crystallizer.iec_percent + * costing_package.multi_effect_crystallizer.fob_unit_cost * ( sum( - blk.unit_model.solids.flow_mass_phase_comp[0, "Sol", j] - for j in blk.unit_model.config.property_package.solute_set + blk.properties_solids[0].flow_mass_phase_comp["Sol", j] + for j in blk.config.property_package.solute_set ) - / blk.costing_package.crystallizer.ref_capacity + / costing_package.multi_effect_crystallizer.ref_capacity ) - ** blk.costing_package.crystallizer.ref_exponent + ** costing_package.multi_effect_crystallizer.ref_exponent ), - to_units=blk.costing_package.base_currency, - )) - ) - _cost_crystallizer_flows(blk) + to_units=costing_package.base_currency, + ) + return capital_cost_effect @register_costing_parameter_block( @@ -227,7 +279,7 @@ def _compute_steam_properties(blk): Out: Steam thermal capacity (latent heat of condensation * density) in kJ/m3 """ - pressure_sat = blk.costing_package.crystallizer.steam_pressure + pressure_sat = blk.steam_pressure # 1. Compute saturation temperature of steam: computed from El-Dessouky expression tsat_constants = [ 42.6776 * pyo.units.K, From 352dde32e953ec1d9ad384657ea77acfcca4ccbb Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Sat, 14 Sep 2024 09:41:02 -0600 Subject: [PATCH 24/80] add params for costing to effect unit model --- .../reflo/unit_models/crystallizer_effect.py | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py index 8fc0ea6e..3d378d82 100644 --- a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py +++ b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py @@ -130,6 +130,27 @@ def build(self): self.vapor.temperature.setub(1000) self.pure_water.temperature.setub(1000) + self.steam_pressure = Param( + initialize=3, + mutable=True, + units=pyunits.bar, + doc="Steam pressure (gauge) for crystallizer heating: 3 bar default based on Dutta example", + ) + + self.efficiency_pump = Param( + initialize=0.7, + mutable=True, + units=pyunits.dimensionless, + doc="Crystallizer pump efficiency - assumed", + ) + + self.pump_head_height = Param( + initialize=1, + mutable=True, + units=pyunits.m, + doc="Crystallizer pump head height - assumed, unvalidated", + ) + self.energy_flow_superheated_vapor = Var( initialize=1e5, bounds=(-5e6, 5e6), @@ -418,15 +439,12 @@ def calculate_scaling_factors(self): iscale.set_scaling_factor(self.area, 0.1) if iscale.get_scaling_factor(self.overall_heat_transfer_coefficient) is None: - iscale.set_scaling_factor(self.overall_heat_transfer_coefficient, 0.1) + iscale.set_scaling_factor(self.overall_heat_transfer_coefficient, 0.01) for ind, c in self.eq_p_con4.items(): sf = iscale.get_scaling_factor(self.properties_pure_water[0].pressure) iscale.constraint_scaling_transform(c, sf) - def _calculate_saturation_temperature(self): - pass - if __name__ == "__main__": import watertap.property_models.unit_specific.cryst_prop_pack as props From c74c94a2be57249953b89bc8e0691fc48af51875 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Sat, 14 Sep 2024 09:41:32 -0600 Subject: [PATCH 25/80] add total_flow_vol expr; costing attempt --- .../unit_models/multi_effect_crystallizer.py | 65 +++++++++++++------ 1 file changed, 45 insertions(+), 20 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py index 74e9d524..c6128afc 100644 --- a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py +++ b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py @@ -21,6 +21,7 @@ check_optimal_termination, Param, Constraint, + Expression, Suffix, RangeSet, units as pyunits, @@ -34,6 +35,7 @@ UnitModelBlockData, useDefault, FlowsheetBlock, + UnitModelCostingBlock ) from watertap.core.solvers import get_solver from idaes.core.util.tables import create_stream_table_dataframe @@ -68,7 +70,7 @@ ) from watertap_contrib.reflo.unit_models.crystallizer_effect import CrystallizerEffect - +from watertap_contrib.reflo.costing.units.multi_effect_crystallizer import cost_multi_effect_crystallizer _log = idaeslog.getLogger(__name__) @@ -168,13 +170,16 @@ def build(self): self.effects = FlowsheetBlock(self.Effects, dynamic=False) + total_flow_vol_in_expr = 0 + for n, eff in self.effects.items(): eff.effect = effect = CrystallizerEffect( property_package=self.config.property_package, property_package_vapor=self.config.property_package_vapor, standalone=False, ) - eff.effect.properties_in[0].conc_mass_phase_comp + effect.properties_in[0].conc_mass_phase_comp + total_flow_vol_in_expr += effect.properties_in[0].flow_vol_phase["Liq"] if n == self.first_effect: @@ -291,6 +296,9 @@ def eq_heating_steam_flow_rate(b): # ) # effect.add_component(f"eq_equiv_brine_conc_effect_{n}_ub", brine_conc_constr) + self.total_flow_vol_in = Expression(expr=total_flow_vol_in_expr) + + def initialize_build( self, state_args=None, @@ -326,9 +334,13 @@ def initialize_build( # eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "H2O"].unfix() # eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"].unfix() # eff.effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"].fix(conc) - + + @property + def default_costing_method(self): + return cost_multi_effect_crystallizer if __name__ == "__main__": + from watertap_contrib.reflo.costing import TreatmentCosting import watertap.property_models.unit_specific.cryst_prop_pack as props from watertap.property_models.water_prop_pack import WaterParameterBlock @@ -418,13 +430,13 @@ def initialize_build( for n, eff in mec.effects.items(): print(f"\nEFFECT {n}\n") # eff.effect.properties_pure_water[0].temperature.display() - eff.effect.pressure_operating.display() - eff.effect.overall_heat_transfer_coefficient.display() - eff.effect.properties_vapor[0].temperature.display() - eff.effect.temperature_operating.display() - eff.effect.properties_in[0].flow_mass_phase_comp.display() - eff.effect.properties_in[0].conc_mass_phase_comp.display() - eff.effect.properties_out[0].flow_mass_phase_comp.display() + # eff.effect.pressure_operating.display() + # eff.effect.overall_heat_transfer_coefficient.display() + # eff.effect.properties_vapor[0].temperature.display() + # eff.effect.temperature_operating.display() + # eff.effect.properties_in[0].flow_mass_phase_comp.display() + # eff.effect.properties_in[0].conc_mass_phase_comp.display() + # eff.effect.properties_out[0].flow_mass_phase_comp.display() eff.effect.overall_heat_transfer_coefficient.setlb(99) if n != 1: eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "H2O"].unfix() @@ -445,16 +457,29 @@ def initialize_build( results = solver.solve(m) print(f"termination {results.solver.termination_condition}") - for n, eff in mec.effects.items(): - print(f"\nEFFECT {n}\n") - # eff.effect.properties_pure_water[0].temperature.display() - eff.effect.pressure_operating.display() - eff.effect.overall_heat_transfer_coefficient.display() - eff.effect.properties_vapor[0].temperature.display() - eff.effect.temperature_operating.display() - eff.effect.properties_in[0].flow_mass_phase_comp.display() - eff.effect.properties_in[0].conc_mass_phase_comp.display() - eff.effect.properties_out[0].flow_mass_phase_comp.display() + + # mec.effects[1].effect.flowsheet().flowsheet().mec.display() + m.fs.costing = TreatmentCosting() + m.fs.mec.costing = UnitModelCostingBlock(flowsheet_costing_block=m.fs.costing) + + m.fs.costing.multi_effect_crystallizer.NaCl_recovery_value.fix(-0.024) + m.fs.costing.cost_process() + m.fs.costing.add_LCOW(mec.total_flow_vol_in) + print(f"dof after costing = {degrees_of_freedom(m)}") + results = solver.solve(m) + print(f"termination {results.solver.termination_condition}") + print(f"LCOW = {m.fs.costing.LCOW()}") + mec.costing.display() + # for n, eff in mec.effects.items(): + # print(f"\nEFFECT {n}\n") + # # eff.effect.properties_pure_water[0].temperature.display() + # eff.effect.pressure_operating.display() + # eff.effect.overall_heat_transfer_coefficient.display() + # eff.effect.properties_vapor[0].temperature.display() + # eff.effect.temperature_operating.display() + # eff.effect.properties_in[0].flow_mass_phase_comp.display() + # eff.effect.properties_in[0].conc_mass_phase_comp.display() + # eff.effect.properties_out[0].flow_mass_phase_comp.display() # mass_flow_solid_nacl_constr = Constraint( # expr=effect.properties_in[0].flow_mass_phase_comp["Sol", "NaCl"] From 409bd16d3f84b9347025a1164a3f317ab9e6e450 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Mon, 16 Sep 2024 18:17:07 -0600 Subject: [PATCH 26/80] area -> heat_exchanger_area --- .../reflo/unit_models/crystallizer_effect.py | 4 +- .../unit_models/multi_effect_crystallizer.py | 241 ++++++++++-------- 2 files changed, 135 insertions(+), 110 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py index 3d378d82..7c94a7ad 100644 --- a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py +++ b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py @@ -520,7 +520,9 @@ def calculate_scaling_factors(self): solver = get_solver() results = solver.solve(m) assert_optimal_termination(results) - eff.properties_in[0].display() + # eff.properties_in[0].display() + eff.work_mechanical.display() + # eff.costing.display() # eff.heating_steam[0].display() # print("\nPROPERTIES IN\n") diff --git a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py index c6128afc..ee203b28 100644 --- a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py +++ b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py @@ -21,7 +21,7 @@ check_optimal_termination, Param, Constraint, - Expression, + Expression, Suffix, RangeSet, units as pyunits, @@ -35,7 +35,7 @@ UnitModelBlockData, useDefault, FlowsheetBlock, - UnitModelCostingBlock + UnitModelCostingBlock, ) from watertap.core.solvers import get_solver from idaes.core.util.tables import create_stream_table_dataframe @@ -70,7 +70,9 @@ ) from watertap_contrib.reflo.unit_models.crystallizer_effect import CrystallizerEffect -from watertap_contrib.reflo.costing.units.multi_effect_crystallizer import cost_multi_effect_crystallizer +from watertap_contrib.reflo.costing.units.multi_effect_crystallizer import ( + cost_multi_effect_crystallizer, +) _log = idaeslog.getLogger(__name__) @@ -221,7 +223,7 @@ def eq_delta_temperature_outlet_effect_1(b): def eq_heat_transfer_effect_1(b): return b.work_mechanical[0] == ( b.overall_heat_transfer_coefficient - * b.area + * b.heat_exchanger_area * b.delta_temperature[0] ) @@ -270,7 +272,7 @@ def eq_heating_steam_flow_rate(b): hx_constr = Constraint( expr=prev_effect.energy_flow_superheated_vapor == effect.overall_heat_transfer_coefficient - * effect.area + * effect.heat_exchanger_area * effect.delta_temperature[0], doc=f"Heat transfer equation for effect {n}", ) @@ -298,7 +300,6 @@ def eq_heating_steam_flow_rate(b): self.total_flow_vol_in = Expression(expr=total_flow_vol_in_expr) - def initialize_build( self, state_args=None, @@ -327,18 +328,26 @@ def initialize_build( opt = get_solver(solver, optarg) for n, eff in self.effects.items(): + if n != 1: + prev_effect = self.effects[n - 1].effect + work_mechanical = prev_effect.work_mechanical[0].value + eff.effect.energy_flow_superheated_vapor.set_value(work_mechanical) + else: + ec = getattr(self, f"") eff.effect.initialize() + init_log.info(f"Initialization of Effect {n} Complete.") # if n != 1: # conc = self.effects[1].effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"].value # eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "H2O"].unfix() # eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"].unfix() # eff.effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"].fix(conc) - + @property def default_costing_method(self): return cost_multi_effect_crystallizer + if __name__ == "__main__": from watertap_contrib.reflo.costing import TreatmentCosting import watertap.property_models.unit_specific.cryst_prop_pack as props @@ -399,6 +408,7 @@ def default_costing_method(self): eff.effect.pressure_operating.fix( pyunits.convert(op_pressure * pyunits.bar, to_units=pyunits.Pa) ) + eff.effect.overall_heat_transfer_coefficient.set_value(100) if n == 1: eff.effect.overall_heat_transfer_coefficient.fix(100) eff.effect.heating_steam[0].pressure_sat @@ -416,19 +426,29 @@ def default_costing_method(self): print(f"dof before init = {degrees_of_freedom(m)}") calculate_scaling_factors(m) - # mec.initialize() + mec.initialize() - for n, eff in mec.effects.items(): - try: - eff.effect.initialize() - except: - # print_infeasible_constraints(eff.effect) - pass + # for n, eff in mec.effects.items(): + # try: + # eff.effect.initialize() + # eff.effect.energy_flow_superheated_vapor.display() + # eff.effect.overall_heat_transfer_coefficient.display() + # except: + # print_infeasible_constraints(eff.effect) + # eff.effect.energy_flow_superheated_vapor.display() + # eff.effect.overall_heat_transfer_coefficient.display() + # assert False + # pass + # for n, eff in mec.effects.items(): + # eff.effect.work_mechanical.display() + # eff.effect.energy_flow_superheated_vapor.display() + # eff.effect.overall_heat_transfer_coefficient.display() + # assert False brine_conc = eff1.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"].value for n, eff in mec.effects.items(): - print(f"\nEFFECT {n}\n") + # print(f"\nEFFECT {n}\n") # eff.effect.properties_pure_water[0].temperature.display() # eff.effect.pressure_operating.display() # eff.effect.overall_heat_transfer_coefficient.display() @@ -457,7 +477,6 @@ def default_costing_method(self): results = solver.solve(m) print(f"termination {results.solver.termination_condition}") - # mec.effects[1].effect.flowsheet().flowsheet().mec.display() m.fs.costing = TreatmentCosting() m.fs.mec.costing = UnitModelCostingBlock(flowsheet_costing_block=m.fs.costing) @@ -469,7 +488,11 @@ def default_costing_method(self): results = solver.solve(m) print(f"termination {results.solver.termination_condition}") print(f"LCOW = {m.fs.costing.LCOW()}") - mec.costing.display() + # mec.costing.display() + # eff1.work_mechanical.display() + # for n, eff in mec.effects.items(): + # eff.effect.energy_flow_superheated_vapor.display() + # eff.effect.overall_heat_transfer_coefficient.display() # for n, eff in mec.effects.items(): # print(f"\nEFFECT {n}\n") # # eff.effect.properties_pure_water[0].temperature.display() @@ -481,95 +504,95 @@ def default_costing_method(self): # eff.effect.properties_in[0].conc_mass_phase_comp.display() # eff.effect.properties_out[0].flow_mass_phase_comp.display() - # mass_flow_solid_nacl_constr = Constraint( - # expr=effect.properties_in[0].flow_mass_phase_comp["Sol", "NaCl"] - # == prev_effect.properties_in[0].flow_mass_phase_comp["Sol", "NaCl"], - # doc="Mass flow of solid NaCl for effect {n}", - # ) - # effect.add_component( - # f"eq_equiv_mass_flow_sol_nacl_effect_{n}", - # mass_flow_solid_nacl_constr, - # ) - - # mass_flow_vap_water_constr = Constraint( - # expr=effect.properties_in[0].flow_mass_phase_comp["Vap", "H2O"] - # == prev_effect.properties_in[0].flow_mass_phase_comp["Vap", "H2O"], - # doc="Mass flow of water vapor for effect {n}", - # ) - # effect.add_component( - # f"eq_equiv_mass_flow_vap_water_effect_{n}", - # mass_flow_vap_water_constr, - # ) - - # prop_in_press_constr = Constraint( - # expr=effect.properties_in[0].pressure - # == prev_effect.properties_in[0].pressure, - # doc="Inlet properties pressure for effect {n}", - # ) - # effect.add_component(f"eq_equiv_press_effect_{n}", prop_in_press_constr) - - # prop_in_press_constr_ub = Constraint( - # expr=effect.properties_in[0].pressure - # <= 1.0001 * prev_effect.properties_in[0].pressure, - # doc="Inlet properties pressure for effect {n}", - # ) - # effect.add_component(f"eq_equiv_press_effect_{n}_ub", prop_in_press_constr_ub) - - # prop_in_press_constr_lb = Constraint( - # expr=effect.properties_in[0].pressure - # >= 0.9999 * prev_effect.properties_in[0].pressure, - # doc="Inlet properties pressure for effect {n}", - # ) - # effect.add_component(f"eq_equiv_press_effect_{n}_lb", prop_in_press_constr_lb) - - # prop_in_temp_constr = Constraint( - # expr=effect.properties_in[0].temperature - # == prev_effect.properties_in[0].temperature, - # doc="Inlet properties temperature for effect {n}", - # ) - # effect.add_component( - # f"eq_equiv_temp_effect_{n}", prop_in_temp_constr - # ) - - # cryst_growth_rate_constr = Constraint( - # expr=effect.crystal_growth_rate == prev_effect.crystal_growth_rate, - # doc="Equivalent crystal growth rate effect {n}", - # ) - # effect.add_component( - # f"eq_equiv_crystal_growth_rate_effect_{n}", cryst_growth_rate_constr - # ) - - # souders_brown_constr = Constraint( - # expr=effect.souders_brown_constant - # == prev_effect.souders_brown_constant, - # doc="Equivalent Sounders Brown constant effect {n}", - # ) - # effect.add_component( - # f"eq_equiv_souders_brown_constant_effect_{n}", souders_brown_constr - # ) - - # cryst_med_len_constr = Constraint( - # expr=effect.crystal_median_length - # == prev_effect.crystal_median_length, - # doc="Equivalent crystal median length effect {n}", - # ) - # effect.add_component( - # f"eq_equiv_crystal_median_length_effect_{n}", cryst_med_len_constr - # ) - - # cryst_yield_constr = Constraint( - # expr=effect.crystallization_yield["NaCl"] - # == prev_effect.crystallization_yield["NaCl"], - # doc="Equivalent crystallization yield effect {n}", - # ) - # effect.add_component( - # f"eq_equiv_crystallization_yield_effect_{n}", cryst_yield_constr - # ) - - # op_press_constr = Constraint( - # expr=effect.pressure_operating <= prev_effect.pressure_operating, - # doc=f"Equivalent operating pressure effect {n}", - # ) - # effect.add_component( - # f"eq_equiv_pressure_operating_effect_{n}", op_press_constr - # ) + # mass_flow_solid_nacl_constr = Constraint( + # expr=effect.properties_in[0].flow_mass_phase_comp["Sol", "NaCl"] + # == prev_effect.properties_in[0].flow_mass_phase_comp["Sol", "NaCl"], + # doc="Mass flow of solid NaCl for effect {n}", + # ) + # effect.add_component( + # f"eq_equiv_mass_flow_sol_nacl_effect_{n}", + # mass_flow_solid_nacl_constr, + # ) + + # mass_flow_vap_water_constr = Constraint( + # expr=effect.properties_in[0].flow_mass_phase_comp["Vap", "H2O"] + # == prev_effect.properties_in[0].flow_mass_phase_comp["Vap", "H2O"], + # doc="Mass flow of water vapor for effect {n}", + # ) + # effect.add_component( + # f"eq_equiv_mass_flow_vap_water_effect_{n}", + # mass_flow_vap_water_constr, + # ) + + # prop_in_press_constr = Constraint( + # expr=effect.properties_in[0].pressure + # == prev_effect.properties_in[0].pressure, + # doc="Inlet properties pressure for effect {n}", + # ) + # effect.add_component(f"eq_equiv_press_effect_{n}", prop_in_press_constr) + + # prop_in_press_constr_ub = Constraint( + # expr=effect.properties_in[0].pressure + # <= 1.0001 * prev_effect.properties_in[0].pressure, + # doc="Inlet properties pressure for effect {n}", + # ) + # effect.add_component(f"eq_equiv_press_effect_{n}_ub", prop_in_press_constr_ub) + + # prop_in_press_constr_lb = Constraint( + # expr=effect.properties_in[0].pressure + # >= 0.9999 * prev_effect.properties_in[0].pressure, + # doc="Inlet properties pressure for effect {n}", + # ) + # effect.add_component(f"eq_equiv_press_effect_{n}_lb", prop_in_press_constr_lb) + + # prop_in_temp_constr = Constraint( + # expr=effect.properties_in[0].temperature + # == prev_effect.properties_in[0].temperature, + # doc="Inlet properties temperature for effect {n}", + # ) + # effect.add_component( + # f"eq_equiv_temp_effect_{n}", prop_in_temp_constr + # ) + + # cryst_growth_rate_constr = Constraint( + # expr=effect.crystal_growth_rate == prev_effect.crystal_growth_rate, + # doc="Equivalent crystal growth rate effect {n}", + # ) + # effect.add_component( + # f"eq_equiv_crystal_growth_rate_effect_{n}", cryst_growth_rate_constr + # ) + + # souders_brown_constr = Constraint( + # expr=effect.souders_brown_constant + # == prev_effect.souders_brown_constant, + # doc="Equivalent Sounders Brown constant effect {n}", + # ) + # effect.add_component( + # f"eq_equiv_souders_brown_constant_effect_{n}", souders_brown_constr + # ) + + # cryst_med_len_constr = Constraint( + # expr=effect.crystal_median_length + # == prev_effect.crystal_median_length, + # doc="Equivalent crystal median length effect {n}", + # ) + # effect.add_component( + # f"eq_equiv_crystal_median_length_effect_{n}", cryst_med_len_constr + # ) + + # cryst_yield_constr = Constraint( + # expr=effect.crystallization_yield["NaCl"] + # == prev_effect.crystallization_yield["NaCl"], + # doc="Equivalent crystallization yield effect {n}", + # ) + # effect.add_component( + # f"eq_equiv_crystallization_yield_effect_{n}", cryst_yield_constr + # ) + + # op_press_constr = Constraint( + # expr=effect.pressure_operating <= prev_effect.pressure_operating, + # doc=f"Equivalent operating pressure effect {n}", + # ) + # effect.add_component( + # f"eq_equiv_pressure_operating_effect_{n}", op_press_constr + # ) From 4e84488c1d45124e545affc5dc96f13902415af9 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Mon, 16 Sep 2024 18:19:15 -0600 Subject: [PATCH 27/80] area -> heat_exchanger_area --- src/watertap_contrib/reflo/unit_models/crystallizer_effect.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py index 7c94a7ad..01ad696b 100644 --- a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py +++ b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py @@ -175,11 +175,11 @@ def build(self): delta_temperature_chen_callback(self) - self.area = Var( + self.heat_exchanger_area = Var( initialize=1000.0, bounds=(0, None), units=pyunits.m**2, - doc="Heat exchange area", + doc="Heat exchanger area", ) self.overall_heat_transfer_coefficient = Var( From 8e40d1079f989290e1fda4795b4046d00e3b24d9 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Tue, 17 Sep 2024 09:02:13 -0400 Subject: [PATCH 28/80] finish changing area var name --- .../reflo/unit_models/crystallizer_effect.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py index 01ad696b..f25bb13a 100644 --- a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py +++ b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py @@ -155,7 +155,7 @@ def build(self): initialize=1e5, bounds=(-5e6, 5e6), units=pyunits.kJ / pyunits.s, - doc="Energy could be supplied from vapor", + doc="Energy that could be supplied from vapor", ) self.delta_temperature_in = Var( @@ -179,7 +179,7 @@ def build(self): initialize=1000.0, bounds=(0, None), units=pyunits.m**2, - doc="Heat exchanger area", + doc="Heat exchanger heat_exchanger_area", ) self.overall_heat_transfer_coefficient = Var( @@ -260,7 +260,7 @@ def eq_delta_temperature_outlet(b): def eq_heat_transfer(b): return b.work_mechanical[0] == ( b.overall_heat_transfer_coefficient - * b.area + * b.heat_exchanger_area * b.delta_temperature[0] ) @@ -435,8 +435,8 @@ def calculate_scaling_factors(self): if iscale.get_scaling_factor(self.delta_temperature_out[0]) is None: iscale.set_scaling_factor(self.delta_temperature_out[0], 0.1) - if iscale.get_scaling_factor(self.area) is None: - iscale.set_scaling_factor(self.area, 0.1) + if iscale.get_scaling_factor(self.heat_exchanger_area) is None: + iscale.set_scaling_factor(self.heat_exchanger_area, 0.1) if iscale.get_scaling_factor(self.overall_heat_transfer_coefficient) is None: iscale.set_scaling_factor(self.overall_heat_transfer_coefficient, 0.01) From 0a490b13352fd53fb8602151077f2e8b65247dbd Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Tue, 17 Sep 2024 10:29:33 -0400 Subject: [PATCH 29/80] working initialize_build routine --- .../unit_models/multi_effect_crystallizer.py | 107 ++++++++---------- 1 file changed, 48 insertions(+), 59 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py index ee203b28..755711b0 100644 --- a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py +++ b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py @@ -19,6 +19,7 @@ ConcreteModel, Var, check_optimal_termination, + assert_optimal_termination, Param, Constraint, Expression, @@ -75,6 +76,7 @@ ) _log = idaeslog.getLogger(__name__) +from idaes.core.util.scaling import * __author__ = "Oluwamayowa Amusat, Zhuoran Zhang, Kurban Sitterley" @@ -184,7 +186,6 @@ def build(self): total_flow_vol_in_expr += effect.properties_in[0].flow_vol_phase["Liq"] if n == self.first_effect: - tmp_dict = dict(**self.config.property_package_args) tmp_dict["has_phase_equilibrium"] = False tmp_dict["parameters"] = self.config.property_package_vapor @@ -326,22 +327,40 @@ def initialize_build( solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit") opt = get_solver(solver, optarg) - for n, eff in self.effects.items(): - if n != 1: - prev_effect = self.effects[n - 1].effect - work_mechanical = prev_effect.work_mechanical[0].value - eff.effect.energy_flow_superheated_vapor.set_value(work_mechanical) + if n == 1: + assert degrees_of_freedom(eff.effect) == 0 + eff.effect.initialize() + inlet_conc = ( + eff.effect.properties_in[0] + .conc_mass_phase_comp["Liq", "NaCl"] + .value + ) + mass_transfer_coeff = eff.effect.overall_heat_transfer_coefficient.value else: - ec = getattr(self, f"") - eff.effect.initialize() + c = getattr( + eff.effect, f"eq_energy_for_effect_{n}_from_effect_{n - 1}" + ) + c.deactivate() + eff.effect.initialize() + c.activate() + eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "H2O"].unfix() + eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"].unfix() + eff.effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"].fix( + inlet_conc + ) + eff.effect.overall_heat_transfer_coefficient.fix(mass_transfer_coeff) init_log.info(f"Initialization of Effect {n} Complete.") - # if n != 1: - # conc = self.effects[1].effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"].value - # eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "H2O"].unfix() - # eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"].unfix() - # eff.effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"].fix(conc) + + with idaeslog.solver_log(init_log, idaeslog.DEBUG) as slc: + res = opt.solve(self, tee=slc.tee) + print(f"\n\ntermination {res.solver.termination_condition}") + init_log.info_high("Initialization Step 3 {}.".format(idaeslog.condition(res))) + # --------------------------------------------------------------------- + + if not check_optimal_termination(res): + raise InitializationError(f"Unit model {self.name} failed to initialize") @property def default_costing_method(self): @@ -349,9 +368,12 @@ def default_costing_method(self): if __name__ == "__main__": + from watertap_contrib.reflo.costing import TreatmentCosting import watertap.property_models.unit_specific.cryst_prop_pack as props from watertap.property_models.water_prop_pack import WaterParameterBlock + + solver = get_solver() m = ConcreteModel() m.fs = FlowsheetBlock(dynamic=False) @@ -401,6 +423,7 @@ def default_costing_method(self): eff.effect.properties_in[0].flow_mass_phase_comp["Sol", "NaCl"].fix(eps) eff.effect.properties_in[0].flow_mass_phase_comp["Vap", "H2O"].fix(eps) + eff.effect.properties_in[0].conc_mass_phase_comp[...] eff.effect.crystallization_yield["NaCl"].fix(crystallizer_yield) eff.effect.crystal_growth_rate.fix() eff.effect.souders_brown_constant.fix() @@ -408,13 +431,14 @@ def default_costing_method(self): eff.effect.pressure_operating.fix( pyunits.convert(op_pressure * pyunits.bar, to_units=pyunits.Pa) ) + # eff.effect.overall_heat_transfer_coefficient.setlb(99) eff.effect.overall_heat_transfer_coefficient.set_value(100) if n == 1: eff.effect.overall_heat_transfer_coefficient.fix(100) eff.effect.heating_steam[0].pressure_sat eff.effect.heating_steam.calculate_state( var_args={ - ("flow_mass_phase_comp", ("Liq", "H2O")): 0, # All vapor, no liquid + ("flow_mass_phase_comp", ("Liq", "H2O")): 0, ("pressure", None): 101325, # ("pressure_sat", None): 28100 ("temperature", None): 393, @@ -425,59 +449,23 @@ def default_costing_method(self): print(f"dof effect {n} = {degrees_of_freedom(eff.effect)}") print(f"dof before init = {degrees_of_freedom(m)}") + calculate_scaling_factors(m) + mec.initialize() - - # for n, eff in mec.effects.items(): - # try: - # eff.effect.initialize() - # eff.effect.energy_flow_superheated_vapor.display() - # eff.effect.overall_heat_transfer_coefficient.display() - # except: - # print_infeasible_constraints(eff.effect) - # eff.effect.energy_flow_superheated_vapor.display() - # eff.effect.overall_heat_transfer_coefficient.display() - # assert False - # pass - - # for n, eff in mec.effects.items(): - # eff.effect.work_mechanical.display() - # eff.effect.energy_flow_superheated_vapor.display() - # eff.effect.overall_heat_transfer_coefficient.display() - # assert False - brine_conc = eff1.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"].value - - for n, eff in mec.effects.items(): - # print(f"\nEFFECT {n}\n") - # eff.effect.properties_pure_water[0].temperature.display() - # eff.effect.pressure_operating.display() - # eff.effect.overall_heat_transfer_coefficient.display() - # eff.effect.properties_vapor[0].temperature.display() - # eff.effect.temperature_operating.display() - # eff.effect.properties_in[0].flow_mass_phase_comp.display() - # eff.effect.properties_in[0].conc_mass_phase_comp.display() - # eff.effect.properties_out[0].flow_mass_phase_comp.display() - eff.effect.overall_heat_transfer_coefficient.setlb(99) - if n != 1: - eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "H2O"].unfix() - eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"].unfix() - eff.effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"].fix( - brine_conc - ) - - solver = get_solver() - print(f"dof before solve 1 = {degrees_of_freedom(m)}") + + print(f"dof before solve = {degrees_of_freedom(m)}") results = solver.solve(m) print(f"termination {results.solver.termination_condition}") + assert_optimal_termination(results) for n, eff in mec.effects.items(): - eff.effect.overall_heat_transfer_coefficient.fix(100) + print(f"\nEFFECT {n}\n") + eff.effect.overall_heat_transfer_coefficient.display() + # eff.effect.properties_vapor[0].temperature.display() + # eff.effect.temperature_operating.display() - print(f"dof before solve 2 = {degrees_of_freedom(m)}") - results = solver.solve(m) - print(f"termination {results.solver.termination_condition}") - # mec.effects[1].effect.flowsheet().flowsheet().mec.display() m.fs.costing = TreatmentCosting() m.fs.mec.costing = UnitModelCostingBlock(flowsheet_costing_block=m.fs.costing) @@ -488,6 +476,7 @@ def default_costing_method(self): results = solver.solve(m) print(f"termination {results.solver.termination_condition}") print(f"LCOW = {m.fs.costing.LCOW()}") + # mec.costing.display() # eff1.work_mechanical.display() # for n, eff in mec.effects.items(): From c34d9f3e65c87c9f882a529100efc9ab8450cf53 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Tue, 17 Sep 2024 10:54:12 -0400 Subject: [PATCH 30/80] black --- .../units/multi_effect_crystallizer.py | 44 +++++++++++-------- .../unit_models/multi_effect_crystallizer.py | 13 +++--- 2 files changed, 30 insertions(+), 27 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/units/multi_effect_crystallizer.py b/src/watertap_contrib/reflo/costing/units/multi_effect_crystallizer.py index cfacf972..49f7a864 100644 --- a/src/watertap_contrib/reflo/costing/units/multi_effect_crystallizer.py +++ b/src/watertap_contrib/reflo/costing/units/multi_effect_crystallizer.py @@ -99,11 +99,14 @@ def build_multi_effect_crystallizer_cost_param_block(blk): costing.register_flow_type("steam", blk.steam_cost) costing.register_flow_type("NaCl", blk.NaCl_recovery_value) + @register_costing_parameter_block( build_rule=build_multi_effect_crystallizer_cost_param_block, parameter_block_name="multi_effect_crystallizer", ) -def cost_multi_effect_crystallizer(blk, cost_type=MultiEffectCrystallizerCostType.mass_basis): +def cost_multi_effect_crystallizer( + blk, cost_type=MultiEffectCrystallizerCostType.mass_basis +): """ Function for costing the FC crystallizer by the mass flow of produced crystals. The operating cost model assumes that heat is supplied via condensation of saturated steam (see Dutta et al.) @@ -115,7 +118,7 @@ def cost_multi_effect_crystallizer(blk, cost_type=MultiEffectCrystallizerCostTyp costing_package = blk.costing_package make_capital_cost_var(blk) blk.costing_package.add_cost_factor(blk, "TIC") - + if cost_type == MultiEffectCrystallizerCostType.mass_basis: costing_method = cost_crystallizer_effect_by_crystal_mass elif cost_type == MultiEffectCrystallizerCostType.volume_basis: @@ -125,7 +128,7 @@ def cost_multi_effect_crystallizer(blk, cost_type=MultiEffectCrystallizerCostTyp f"{blk.unit_model.name} received invalid argument for cost_type:" f" {cost_type}. Argument must be a member of the CrystallizerCostType Enum." ) - + # costing_method(blk) total_capex_expr = 0 for n, eff in blk.unit_model.effects.items(): @@ -140,7 +143,9 @@ def cost_multi_effect_crystallizer(blk, cost_type=MultiEffectCrystallizerCostTyp blk.add_component(f"capital_cost_effect_{n}_constraint", capex_constr) total_capex_expr += capital_cost_effect - blk.capital_cost_constraint = pyo.Constraint(expr=blk.capital_cost == blk.cost_factor * total_capex_expr) + blk.capital_cost_constraint = pyo.Constraint( + expr=blk.capital_cost == blk.cost_factor * total_capex_expr + ) def _cost_crystallizer_flows(blk): @@ -182,7 +187,7 @@ def cost_crystallizer_effect_by_crystal_mass(blk): """ Mass-based capital cost for FC crystallizer """ - + # blk.capital_cost = pyo.Var( # initialize=1e5, # # domain=pyo.NonNegativeReals, @@ -215,20 +220,20 @@ def cost_crystallizer_effect_by_crystal_mass(blk): _cost_crystallizer_flows(blk) capital_cost_effect = pyo.units.convert( - ( - costing_package.multi_effect_crystallizer.iec_percent - * costing_package.multi_effect_crystallizer.fob_unit_cost - * ( - sum( - blk.properties_solids[0].flow_mass_phase_comp["Sol", j] - for j in blk.config.property_package.solute_set - ) - / costing_package.multi_effect_crystallizer.ref_capacity + ( + costing_package.multi_effect_crystallizer.iec_percent + * costing_package.multi_effect_crystallizer.fob_unit_cost + * ( + sum( + blk.properties_solids[0].flow_mass_phase_comp["Sol", j] + for j in blk.config.property_package.solute_set ) - ** costing_package.multi_effect_crystallizer.ref_exponent - ), - to_units=costing_package.base_currency, - ) + / costing_package.multi_effect_crystallizer.ref_capacity + ) + ** costing_package.multi_effect_crystallizer.ref_exponent + ), + to_units=costing_package.base_currency, + ) return capital_cost_effect @@ -244,7 +249,8 @@ def cost_crystallizer_by_volume(blk): blk.costing_package.add_cost_factor(blk, "TIC") blk.capital_cost_constraint = pyo.Constraint( expr=blk.capital_cost - == blk.unit_model.config.number_effects * blk.cost_factor + == blk.unit_model.config.number_effects + * blk.cost_factor * pyo.units.convert( ( blk.costing_package.crystallizer.volume_cost diff --git a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py index 755711b0..a2a23e44 100644 --- a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py +++ b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py @@ -338,9 +338,7 @@ def initialize_build( ) mass_transfer_coeff = eff.effect.overall_heat_transfer_coefficient.value else: - c = getattr( - eff.effect, f"eq_energy_for_effect_{n}_from_effect_{n - 1}" - ) + c = getattr(eff.effect, f"eq_energy_for_effect_{n}_from_effect_{n - 1}") c.deactivate() eff.effect.initialize() c.activate() @@ -372,7 +370,7 @@ def default_costing_method(self): from watertap_contrib.reflo.costing import TreatmentCosting import watertap.property_models.unit_specific.cryst_prop_pack as props from watertap.property_models.water_prop_pack import WaterParameterBlock - + solver = get_solver() m = ConcreteModel() @@ -449,11 +447,11 @@ def default_costing_method(self): print(f"dof effect {n} = {degrees_of_freedom(eff.effect)}") print(f"dof before init = {degrees_of_freedom(m)}") - + calculate_scaling_factors(m) - + mec.initialize() - + print(f"dof before solve = {degrees_of_freedom(m)}") results = solver.solve(m) print(f"termination {results.solver.termination_condition}") @@ -465,7 +463,6 @@ def default_costing_method(self): # eff.effect.properties_vapor[0].temperature.display() # eff.effect.temperature_operating.display() - m.fs.costing = TreatmentCosting() m.fs.mec.costing = UnitModelCostingBlock(flowsheet_costing_block=m.fs.costing) From 135cf9d8d02f053f4192394a21a9f9d02969983c Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Wed, 18 Sep 2024 09:44:49 -0400 Subject: [PATCH 31/80] costing by volume working; use heating_steam for steam spec energy; clean up --- .../units/multi_effect_crystallizer.py | 347 ++++++++++-------- 1 file changed, 192 insertions(+), 155 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/units/multi_effect_crystallizer.py b/src/watertap_contrib/reflo/costing/units/multi_effect_crystallizer.py index 49f7a864..51d4882c 100644 --- a/src/watertap_contrib/reflo/costing/units/multi_effect_crystallizer.py +++ b/src/watertap_contrib/reflo/costing/units/multi_effect_crystallizer.py @@ -15,10 +15,7 @@ from idaes.core.util.constants import Constants from idaes.core.util.exceptions import ConfigurationError from watertap.costing.util import register_costing_parameter_block -from watertap_contrib.reflo.costing.util import ( - make_capital_cost_var, - make_fixed_operating_cost_var, -) +from watertap_contrib.reflo.costing.util import make_capital_cost_var class MultiEffectCrystallizerCostType(StrEnum): @@ -26,25 +23,33 @@ class MultiEffectCrystallizerCostType(StrEnum): volume_basis = "volume_basis" -def build_multi_effect_crystallizer_cost_param_block(blk): +def build_recovered_nacl_cost_param_block(blk): - # blk.steam_pressure = pyo.Var( - # initialize=3, - # units=pyo.units.bar, - # doc="Steam pressure (gauge) for crystallizer heating: 3 bar default based on Dutta example", - # ) + blk.cost = pyo.Param( + mutable=True, + initialize=0, + doc="Recovered cost (sale price) of NaCl", + units=pyo.units.USD_2020 / pyo.units.kg, + ) - # blk.efficiency_pump = pyo.Var( - # initialize=0.7, - # units=pyo.units.dimensionless, - # doc="Crystallizer pump efficiency - assumed", - # ) + costing = blk.parent_block() + costing.register_flow_type("NaCl_recovered", blk.cost) - # blk.pump_head_height = pyo.Var( - # initialize=1, - # units=pyo.units.m, - # doc="Crystallizer pump head height - assumed, unvalidated", - # ) + +def build_steam_cost_param_block(blk): + + blk.cost = pyo.Param( + mutable=True, + initialize=0.004, + doc="Steam cost", + units=pyo.units.USD_2018 * pyo.units.meter**-3, + ) + + costing = blk.parent_block() + costing.register_flow_type("steam", blk.cost) + + +def build_multi_effect_crystallizer_cost_param_block(blk): # Crystallizer operating cost information from literature blk.fob_unit_cost = pyo.Var( @@ -83,23 +88,39 @@ def build_multi_effect_crystallizer_cost_param_block(blk): units=pyo.units.dimensionless, ) - blk.steam_cost = pyo.Var( - initialize=0.004, - units=pyo.units.USD_2018 / (pyo.units.meter**3), - doc="Steam cost, Panagopoulos (2019)", + blk.heat_exchanger_capital_factor = pyo.Var( + initialize=420, + units=pyo.units.USD_2018 / (pyo.units.meter**2), + doc="Heat exchanger cost per area", # TODO: need reference and costing year ) - blk.NaCl_recovery_value = pyo.Var( - initialize=0, - units=pyo.units.USD_2018 / pyo.units.kg, - doc="Unit recovery value of NaCl", + blk.heat_exchanger_endplates_capital_factor = pyo.Var( + initialize=1020, + units=pyo.units.USD_2018, + doc="Heat exchanger endplates cost per area", # TODO: need reference and costing year ) - costing = blk.parent_block() - costing.register_flow_type("steam", blk.steam_cost) - costing.register_flow_type("NaCl", blk.NaCl_recovery_value) + blk.heat_exchanger_endplates_capital_basis = pyo.Var( + initialize=10, + units=pyo.units.meter**2, + doc="Heat exchanger endplates cost per area basis", # TODO: need reference and costing year + ) + + blk.heat_exchanger_endplates_capital_exponent = pyo.Var( + initialize=0.6, + units=pyo.units.dimensionless, + doc="Heat exchanger endplates cost per area basis", # TODO: need reference and costing year + ) +@register_costing_parameter_block( + build_rule=build_recovered_nacl_cost_param_block, + parameter_block_name="nacl_recovered", +) +@register_costing_parameter_block( + build_rule=build_steam_cost_param_block, + parameter_block_name="steam", +) @register_costing_parameter_block( build_rule=build_multi_effect_crystallizer_cost_param_block, parameter_block_name="multi_effect_crystallizer", @@ -108,125 +129,106 @@ def cost_multi_effect_crystallizer( blk, cost_type=MultiEffectCrystallizerCostType.mass_basis ): """ - Function for costing the FC crystallizer by the mass flow of produced crystals. + Function for costing the forced circulation crystallizer by the mass flow of produced crystals. The operating cost model assumes that heat is supplied via condensation of saturated steam (see Dutta et al.) Args: cost_type: Option for crystallizer cost function type - volume or mass basis """ global costing_package + costing_package = blk.costing_package make_capital_cost_var(blk) - blk.costing_package.add_cost_factor(blk, "TIC") + costing_package.add_cost_factor(blk, "TIC") if cost_type == MultiEffectCrystallizerCostType.mass_basis: - costing_method = cost_crystallizer_effect_by_crystal_mass + effect_costing_method = cost_crystallizer_effect_by_crystal_mass elif cost_type == MultiEffectCrystallizerCostType.volume_basis: - costing_method = cost_crystallizer_by_volume + effect_costing_method = cost_crystallizer_effect_by_volume else: raise ConfigurationError( f"{blk.unit_model.name} received invalid argument for cost_type:" - f" {cost_type}. Argument must be a member of the CrystallizerCostType Enum." + f" {cost_type}. Argument must be a member of the MultiEffectCrystallizerCostType Enum." ) - # costing_method(blk) total_capex_expr = 0 - for n, eff in blk.unit_model.effects.items(): - capex_var = pyo.Var( + + for effect_number, eff in blk.unit_model.effects.items(): + + effect_capex_expr = 0 + + effect_capex_var = pyo.Var( initialize=1e5, units=costing_package.base_currency, - doc=f"Capital cost effect {n}", + doc=f"Capital cost effect {effect_number}", ) - capital_cost_effect = costing_method(eff.effect) - capex_constr = pyo.Constraint(expr=capex_var == capital_cost_effect) - blk.add_component(f"capital_cost_effect_{n}", capex_var) - blk.add_component(f"capital_cost_effect_{n}_constraint", capex_constr) - total_capex_expr += capital_cost_effect + + # add capital of crystallizer + effect_capex_expr += effect_costing_method(eff.effect) + # add capital of heat exchangers + effect_capex_expr += cost_crystallizer_heat_exchanger(eff.effect) + + effect_capex_constr = pyo.Constraint( + expr=effect_capex_var == effect_capex_expr, + doc="Constraint for capital cost of effect {effect_number}.", + ) + blk.add_component(f"capital_cost_effect_{effect_number}", effect_capex_var) + blk.add_component( + f"capital_cost_effect_{effect_number}_constraint", effect_capex_constr + ) + total_capex_expr += effect_capex_expr + _cost_effect_flows(eff.effect, effect_number) blk.capital_cost_constraint = pyo.Constraint( expr=blk.capital_cost == blk.cost_factor * total_capex_expr ) -def _cost_crystallizer_flows(blk): +def cost_crystallizer_heat_exchanger(effect): - # costing_package = blk.flowsheet().flowsheet().costing - costing_package.cost_flow( - pyo.units.convert( - ( - blk.magma_circulation_flow_vol - * blk.dens_mass_slurry - * Constants.acceleration_gravity - * blk.pump_head_height - / blk.efficiency_pump - ), - to_units=pyo.units.kW, - ), - "electricity", + capital_cost_hx_effect = 0 + + capital_cost_hx = pyo.units.convert( + costing_package.multi_effect_crystallizer.heat_exchanger_capital_factor + * effect.heat_exchanger_area, + to_units=costing_package.base_currency, ) - costing_package.cost_flow( - pyo.units.convert( - (blk.work_mechanical[0] / _compute_steam_properties(blk)), - to_units=pyo.units.m**3 / pyo.units.s, + capital_cost_hx_effect += capital_cost_hx + + dimensionless_hx_area = pyo.units.convert( + ( + effect.heat_exchanger_area + / costing_package.multi_effect_crystallizer.heat_exchanger_endplates_capital_basis ), - "steam", + to_units=pyo.units.dimensionless, ) - costing_package.cost_flow( - blk.properties_solids[0].flow_mass_phase_comp["Sol", "NaCl"], - "NaCl", + capital_cost_hx_endplates = pyo.units.convert( + costing_package.multi_effect_crystallizer.heat_exchanger_endplates_capital_factor + * (dimensionless_hx_area) + ** costing_package.multi_effect_crystallizer.heat_exchanger_endplates_capital_exponent, + to_units=costing_package.base_currency, ) + capital_cost_hx_effect += capital_cost_hx_endplates -# @register_costing_parameter_block( -# build_rule=build_multi_effect_crystallizer_cost_param_block, -# parameter_block_name="multi_effect_crystallizer", -# ) -def cost_crystallizer_effect_by_crystal_mass(blk): - """ - Mass-based capital cost for FC crystallizer - """ + return capital_cost_hx_effect - # blk.capital_cost = pyo.Var( - # initialize=1e5, - # # domain=pyo.NonNegativeReals, - # units=costing_package.base_currency, - # doc="Unit capital cost", - # ) - # costing_package = blk.flowsheet().flowsheet().costing - # costing_package.add_cost_factor(blk, None) - # blk.capital_cost_constraint = pyo.Constraint( - # expr=blk.capital_cost - # == blk.cost_factor - # * (pyo.units.convert( - # ( - # costing_package.multi_effect_crystallizer.iec_percent - # * costing_package.multi_effect_crystallizer.fob_unit_cost - # * ( - # sum( - # blk.properties_solids[0].flow_mass_phase_comp["Sol", j] - # for j in blk.config.property_package.solute_set - # ) - # / costing_package.multi_effect_crystallizer.ref_capacity - # ) - # ** costing_package.multi_effect_crystallizer.ref_exponent - # ), - # to_units=costing_package.base_currency, - # )) - # ) - # for n, eff in blk.unit_model.effects.items(): +def cost_crystallizer_effect_by_crystal_mass(effect): + """ + Mass-based capital cost for forced circulation crystallizer + """ - _cost_crystallizer_flows(blk) capital_cost_effect = pyo.units.convert( ( costing_package.multi_effect_crystallizer.iec_percent * costing_package.multi_effect_crystallizer.fob_unit_cost * ( sum( - blk.properties_solids[0].flow_mass_phase_comp["Sol", j] - for j in blk.config.property_package.solute_set + effect.properties_solids[0].flow_mass_phase_comp["Sol", j] + for j in effect.config.property_package.solute_set ) / costing_package.multi_effect_crystallizer.ref_capacity ) @@ -234,50 +236,74 @@ def cost_crystallizer_effect_by_crystal_mass(blk): ), to_units=costing_package.base_currency, ) + return capital_cost_effect -@register_costing_parameter_block( - build_rule=build_multi_effect_crystallizer_cost_param_block, - parameter_block_name="crystallizer", -) -def cost_crystallizer_by_volume(blk): +def cost_crystallizer_effect_by_volume(effect): """ - Volume-based capital cost for FC crystallizer + Volume-based capital cost for forced circulation crystallizer """ - make_capital_cost_var(blk) - blk.costing_package.add_cost_factor(blk, "TIC") - blk.capital_cost_constraint = pyo.Constraint( - expr=blk.capital_cost - == blk.unit_model.config.number_effects - * blk.cost_factor - * pyo.units.convert( - ( - blk.costing_package.crystallizer.volume_cost - * ( - ( - pyo.units.convert( - blk.unit_model.volume_suspension - * ( - blk.unit_model.height_crystallizer - / blk.unit_model.height_slurry - ), - to_units=(pyo.units.ft) ** 3, - ) + + capital_cost_effect = pyo.units.convert( + ( + costing_package.multi_effect_crystallizer.volume_cost + * ( + ( + pyo.units.convert( + effect.volume_suspension + * (effect.height_crystallizer / effect.height_slurry), + to_units=(pyo.units.ft) ** 3, ) - / pyo.units.ft**3 ) - ** blk.costing_package.crystallizer.vol_basis_exponent + / pyo.units.ft**3 + ) + ** costing_package.multi_effect_crystallizer.vol_basis_exponent + ), + to_units=costing_package.base_currency, + ) + + return capital_cost_effect + + +def _cost_effect_flows(effect, effect_number): + + costing_package.cost_flow( + pyo.units.convert( + ( + effect.magma_circulation_flow_vol + * effect.dens_mass_slurry + * Constants.acceleration_gravity + * effect.pump_head_height + / effect.efficiency_pump ), - to_units=blk.costing_package.base_currency, - ) + to_units=pyo.units.kW, + ), + "electricity", ) - _cost_crystallizer_flows(blk) + costing_package.cost_flow( + effect.properties_solids[0].flow_mass_phase_comp["Sol", "NaCl"], + "NaCl_recovered", + ) -def _compute_steam_properties(blk): + if effect_number == 1: + effect.steam_flow = pyo.units.convert( + (effect.work_mechanical[0] / _compute_steam_specific_energy(effect)), + to_units=pyo.units.m**3 / pyo.units.s, + ) + costing_package.cost_flow( + effect.steam_flow, + "steam", + ) + # costing_package.cost_flow(effect.heating_steam[0].flow_vol_phase["Vap"], + # "steam", + # ) + + +def _compute_steam_specific_energy(effect): """ - Function for computing saturated steam properties for thermal heating estimation. + Function for computing saturated steam specific energy for thermal heating estimation. Args: pressure_sat: Steam gauge pressure in bar @@ -285,24 +311,28 @@ def _compute_steam_properties(blk): Out: Steam thermal capacity (latent heat of condensation * density) in kJ/m3 """ - pressure_sat = blk.steam_pressure + # TODO: add specific volume to property package? + + # pressure_sat = effect.steam_pressure + # pressure_sat = effect.heating_steam[0].pressure_sat # 1. Compute saturation temperature of steam: computed from El-Dessouky expression - tsat_constants = [ - 42.6776 * pyo.units.K, - -3892.7 * pyo.units.K, - 1000 * pyo.units.kPa, - -9.48654 * pyo.units.dimensionless, - ] - psat = ( - pyo.units.convert(pressure_sat, to_units=pyo.units.kPa) - + 101.325 * pyo.units.kPa - ) - temperature_sat = tsat_constants[0] + tsat_constants[1] / ( - pyo.log(psat / tsat_constants[2]) + tsat_constants[3] - ) + # tsat_constants = [ + # 42.6776 * pyo.units.K, + # -3892.7 * pyo.units.K, + # 1000 * pyo.units.kPa, + # -9.48654 * pyo.units.dimensionless, + # ] + # psat = ( + # pyo.units.convert(pressure_sat, to_units=pyo.units.kPa) + # # + 101.325 * pyo.units.kPa + # ) + # temperature_sat = tsat_constants[0] + tsat_constants[1] / ( + # pyo.log(psat / tsat_constants[2]) + tsat_constants[3] + # ) # 2. Compute latent heat of condensation/vaporization: computed from Sharqawy expression - t = temperature_sat - 273.15 * pyo.units.K + # t = temperature_sat - 273.15 * pyo.units.K + t = effect.heating_steam[0].temperature - 273.15 * pyo.units.K enth_mass_units = pyo.units.J / pyo.units.kg t_inv_units = pyo.units.K**-1 dh_constants = [ @@ -320,10 +350,13 @@ def _compute_steam_properties(blk): + dh_constants[4] * t**4 ) dh_vap = pyo.units.convert(dh_vap, to_units=pyo.units.kJ / pyo.units.kg) + effect.dh_vap = dh_vap # 3. Compute specific volume: computed from Affandi expression (Eq 5) t_critical = 647.096 * pyo.units.K - t_red = temperature_sat / t_critical # Reduced temperature + # t_red = temperature_sat / t_critical # Reduced temperature + t_red = effect.heating_steam[0].temperature / t_critical # Reduced temperature + effect.t_red = t_red sp_vol_constants = [ -7.75883 * pyo.units.dimensionless, 3.23753 * pyo.units.dimensionless, @@ -341,4 +374,8 @@ def _compute_steam_properties(blk): sp_vol = pyo.exp(log_sp_vol) * pyo.units.m**3 / pyo.units.kg # 4. Return specific energy: density * latent heat - return dh_vap / sp_vol + # return dh_vap / sp_vol + return pyo.units.convert( + effect.heating_steam[0].dh_vap_mass / sp_vol, + to_units=pyo.units.kJ / pyo.units.m**3, + ) From feb76e369e188784e5f56ae6cc6edc2ca564c2df Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Wed, 18 Sep 2024 09:45:21 -0400 Subject: [PATCH 32/80] checkpoint --- .../unit_models/multi_effect_crystallizer.py | 225 ++++++++++-------- 1 file changed, 123 insertions(+), 102 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py index a2a23e44..500d4628 100644 --- a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py +++ b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py @@ -423,8 +423,10 @@ def default_costing_method(self): eff.effect.properties_in[0].flow_mass_phase_comp["Vap", "H2O"].fix(eps) eff.effect.properties_in[0].conc_mass_phase_comp[...] eff.effect.crystallization_yield["NaCl"].fix(crystallizer_yield) + # eff.effect.crystal_growth_rate.fix(5e-9) eff.effect.crystal_growth_rate.fix() eff.effect.souders_brown_constant.fix() + # eff.effect.crystal_median_length.fix(0.6e-3) eff.effect.crystal_median_length.fix() eff.effect.pressure_operating.fix( pyunits.convert(op_pressure * pyunits.bar, to_units=pyunits.Pa) @@ -434,6 +436,7 @@ def default_costing_method(self): if n == 1: eff.effect.overall_heat_transfer_coefficient.fix(100) eff.effect.heating_steam[0].pressure_sat + eff.effect.heating_steam[0].dh_vap_mass eff.effect.heating_steam.calculate_state( var_args={ ("flow_mass_phase_comp", ("Liq", "H2O")): 0, @@ -444,13 +447,17 @@ def default_costing_method(self): hold_state=True, ) eff.effect.heating_steam[0].flow_mass_phase_comp["Vap", "H2O"].unfix() + eff.effect.heating_steam[0].flow_vol_phase print(f"dof effect {n} = {degrees_of_freedom(eff.effect)}") print(f"dof before init = {degrees_of_freedom(m)}") calculate_scaling_factors(m) - - mec.initialize() + try: + mec.initialize() + except: + print_infeasible_constraints(m) + # assert False print(f"dof before solve = {degrees_of_freedom(m)}") results = solver.solve(m) @@ -460,13 +467,17 @@ def default_costing_method(self): for n, eff in mec.effects.items(): print(f"\nEFFECT {n}\n") eff.effect.overall_heat_transfer_coefficient.display() - # eff.effect.properties_vapor[0].temperature.display() + eff.effect.properties_solids[0].flow_mass_phase_comp.display() # eff.effect.temperature_operating.display() + # assert False m.fs.costing = TreatmentCosting() - m.fs.mec.costing = UnitModelCostingBlock(flowsheet_costing_block=m.fs.costing) + m.fs.mec.costing = UnitModelCostingBlock( + flowsheet_costing_block=m.fs.costing, + costing_method_arguments={"cost_type": "mass_basis"}, + ) - m.fs.costing.multi_effect_crystallizer.NaCl_recovery_value.fix(-0.024) + m.fs.costing.nacl_recovered.cost.set_value(-0.024) m.fs.costing.cost_process() m.fs.costing.add_LCOW(mec.total_flow_vol_in) print(f"dof after costing = {degrees_of_freedom(m)}") @@ -474,15 +485,20 @@ def default_costing_method(self): print(f"termination {results.solver.termination_condition}") print(f"LCOW = {m.fs.costing.LCOW()}") + # m.fs.costing.display() # mec.costing.display() # eff1.work_mechanical.display() - # for n, eff in mec.effects.items(): - # eff.effect.energy_flow_superheated_vapor.display() - # eff.effect.overall_heat_transfer_coefficient.display() + # eff1.pressure_operating.display() + # eff1.heating_steam[0].pressure.display() + # eff1.heating_steam[0].pressure_sat.display() + # for n, eff in mec.effects.items(): # print(f"\nEFFECT {n}\n") - # # eff.effect.properties_pure_water[0].temperature.display() - # eff.effect.pressure_operating.display() + # eff.effect.properties_pure_water[0].temperature.display() + # eff.effect.height_crystallizer.display() + # eff.effect.height_slurry.display() + # eff.effect.volume_suspension.display() + # eff.effect.t_res.display() # eff.effect.overall_heat_transfer_coefficient.display() # eff.effect.properties_vapor[0].temperature.display() # eff.effect.temperature_operating.display() @@ -490,95 +506,100 @@ def default_costing_method(self): # eff.effect.properties_in[0].conc_mass_phase_comp.display() # eff.effect.properties_out[0].flow_mass_phase_comp.display() - # mass_flow_solid_nacl_constr = Constraint( - # expr=effect.properties_in[0].flow_mass_phase_comp["Sol", "NaCl"] - # == prev_effect.properties_in[0].flow_mass_phase_comp["Sol", "NaCl"], - # doc="Mass flow of solid NaCl for effect {n}", - # ) - # effect.add_component( - # f"eq_equiv_mass_flow_sol_nacl_effect_{n}", - # mass_flow_solid_nacl_constr, - # ) - - # mass_flow_vap_water_constr = Constraint( - # expr=effect.properties_in[0].flow_mass_phase_comp["Vap", "H2O"] - # == prev_effect.properties_in[0].flow_mass_phase_comp["Vap", "H2O"], - # doc="Mass flow of water vapor for effect {n}", - # ) - # effect.add_component( - # f"eq_equiv_mass_flow_vap_water_effect_{n}", - # mass_flow_vap_water_constr, - # ) - - # prop_in_press_constr = Constraint( - # expr=effect.properties_in[0].pressure - # == prev_effect.properties_in[0].pressure, - # doc="Inlet properties pressure for effect {n}", - # ) - # effect.add_component(f"eq_equiv_press_effect_{n}", prop_in_press_constr) - - # prop_in_press_constr_ub = Constraint( - # expr=effect.properties_in[0].pressure - # <= 1.0001 * prev_effect.properties_in[0].pressure, - # doc="Inlet properties pressure for effect {n}", - # ) - # effect.add_component(f"eq_equiv_press_effect_{n}_ub", prop_in_press_constr_ub) - - # prop_in_press_constr_lb = Constraint( - # expr=effect.properties_in[0].pressure - # >= 0.9999 * prev_effect.properties_in[0].pressure, - # doc="Inlet properties pressure for effect {n}", - # ) - # effect.add_component(f"eq_equiv_press_effect_{n}_lb", prop_in_press_constr_lb) - - # prop_in_temp_constr = Constraint( - # expr=effect.properties_in[0].temperature - # == prev_effect.properties_in[0].temperature, - # doc="Inlet properties temperature for effect {n}", - # ) - # effect.add_component( - # f"eq_equiv_temp_effect_{n}", prop_in_temp_constr - # ) - - # cryst_growth_rate_constr = Constraint( - # expr=effect.crystal_growth_rate == prev_effect.crystal_growth_rate, - # doc="Equivalent crystal growth rate effect {n}", - # ) - # effect.add_component( - # f"eq_equiv_crystal_growth_rate_effect_{n}", cryst_growth_rate_constr - # ) - - # souders_brown_constr = Constraint( - # expr=effect.souders_brown_constant - # == prev_effect.souders_brown_constant, - # doc="Equivalent Sounders Brown constant effect {n}", - # ) - # effect.add_component( - # f"eq_equiv_souders_brown_constant_effect_{n}", souders_brown_constr - # ) - - # cryst_med_len_constr = Constraint( - # expr=effect.crystal_median_length - # == prev_effect.crystal_median_length, - # doc="Equivalent crystal median length effect {n}", - # ) - # effect.add_component( - # f"eq_equiv_crystal_median_length_effect_{n}", cryst_med_len_constr - # ) - - # cryst_yield_constr = Constraint( - # expr=effect.crystallization_yield["NaCl"] - # == prev_effect.crystallization_yield["NaCl"], - # doc="Equivalent crystallization yield effect {n}", - # ) - # effect.add_component( - # f"eq_equiv_crystallization_yield_effect_{n}", cryst_yield_constr - # ) - - # op_press_constr = Constraint( - # expr=effect.pressure_operating <= prev_effect.pressure_operating, - # doc=f"Equivalent operating pressure effect {n}", - # ) - # effect.add_component( - # f"eq_equiv_pressure_operating_effect_{n}", op_press_constr - # ) + +################################################################################################## +# "Linking" constraints below that weren't working well but want to keep them here until model is ready for merge +# just in case so I don't have to write them again. + +# mass_flow_solid_nacl_constr = Constraint( +# expr=effect.properties_in[0].flow_mass_phase_comp["Sol", "NaCl"] +# == prev_effect.properties_in[0].flow_mass_phase_comp["Sol", "NaCl"], +# doc="Mass flow of solid NaCl for effect {n}", +# ) +# effect.add_component( +# f"eq_equiv_mass_flow_sol_nacl_effect_{n}", +# mass_flow_solid_nacl_constr, +# ) + +# mass_flow_vap_water_constr = Constraint( +# expr=effect.properties_in[0].flow_mass_phase_comp["Vap", "H2O"] +# == prev_effect.properties_in[0].flow_mass_phase_comp["Vap", "H2O"], +# doc="Mass flow of water vapor for effect {n}", +# ) +# effect.add_component( +# f"eq_equiv_mass_flow_vap_water_effect_{n}", +# mass_flow_vap_water_constr, +# ) + +# prop_in_press_constr = Constraint( +# expr=effect.properties_in[0].pressure +# == prev_effect.properties_in[0].pressure, +# doc="Inlet properties pressure for effect {n}", +# ) +# effect.add_component(f"eq_equiv_press_effect_{n}", prop_in_press_constr) + +# prop_in_press_constr_ub = Constraint( +# expr=effect.properties_in[0].pressure +# <= 1.0001 * prev_effect.properties_in[0].pressure, +# doc="Inlet properties pressure for effect {n}", +# ) +# effect.add_component(f"eq_equiv_press_effect_{n}_ub", prop_in_press_constr_ub) + +# prop_in_press_constr_lb = Constraint( +# expr=effect.properties_in[0].pressure +# >= 0.9999 * prev_effect.properties_in[0].pressure, +# doc="Inlet properties pressure for effect {n}", +# ) +# effect.add_component(f"eq_equiv_press_effect_{n}_lb", prop_in_press_constr_lb) + +# prop_in_temp_constr = Constraint( +# expr=effect.properties_in[0].temperature +# == prev_effect.properties_in[0].temperature, +# doc="Inlet properties temperature for effect {n}", +# ) +# effect.add_component( +# f"eq_equiv_temp_effect_{n}", prop_in_temp_constr +# ) + +# cryst_growth_rate_constr = Constraint( +# expr=effect.crystal_growth_rate == prev_effect.crystal_growth_rate, +# doc="Equivalent crystal growth rate effect {n}", +# ) +# effect.add_component( +# f"eq_equiv_crystal_growth_rate_effect_{n}", cryst_growth_rate_constr +# ) + +# souders_brown_constr = Constraint( +# expr=effect.souders_brown_constant +# == prev_effect.souders_brown_constant, +# doc="Equivalent Sounders Brown constant effect {n}", +# ) +# effect.add_component( +# f"eq_equiv_souders_brown_constant_effect_{n}", souders_brown_constr +# ) + +# cryst_med_len_constr = Constraint( +# expr=effect.crystal_median_length +# == prev_effect.crystal_median_length, +# doc="Equivalent crystal median length effect {n}", +# ) +# effect.add_component( +# f"eq_equiv_crystal_median_length_effect_{n}", cryst_med_len_constr +# ) + +# cryst_yield_constr = Constraint( +# expr=effect.crystallization_yield["NaCl"] +# == prev_effect.crystallization_yield["NaCl"], +# doc="Equivalent crystallization yield effect {n}", +# ) +# effect.add_component( +# f"eq_equiv_crystallization_yield_effect_{n}", cryst_yield_constr +# ) + +# op_press_constr = Constraint( +# expr=effect.pressure_operating <= prev_effect.pressure_operating, +# doc=f"Equivalent operating pressure effect {n}", +# ) +# effect.add_component( +# f"eq_equiv_pressure_operating_effect_{n}", op_press_constr +# ) From be92ded2d1df98c3b2957f8fe19d3d118876c1dc Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Wed, 18 Sep 2024 10:08:02 -0400 Subject: [PATCH 33/80] clean up imports; add init_args --- .../unit_models/multi_effect_crystallizer.py | 100 ++++++++---------- 1 file changed, 43 insertions(+), 57 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py index 500d4628..8976806a 100644 --- a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py +++ b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py @@ -10,17 +10,12 @@ # "https://github.com/watertap-org/watertap/" ################################################################################# -from copy import deepcopy - -from watertap.core.util.model_diagnostics.infeasible import * # Import Pyomo libraries from pyomo.environ import ( ConcreteModel, - Var, check_optimal_termination, assert_optimal_termination, - Param, Constraint, Expression, Suffix, @@ -28,7 +23,6 @@ units as pyunits, ) from pyomo.common.config import ConfigBlock, ConfigValue, In, PositiveInt -from pyomo.util.calc_var_value import calculate_variable_from_constraint as cvc # Import IDAES cores from idaes.core import ( @@ -38,37 +32,19 @@ FlowsheetBlock, UnitModelCostingBlock, ) -from watertap.core.solvers import get_solver -from idaes.core.util.tables import create_stream_table_dataframe -from idaes.core.util.constants import Constants -from idaes.core.util.config import is_physical_parameter_block - from idaes.core.util.exceptions import InitializationError - -import idaes.core.util.scaling as iscale -import idaes.logger as idaeslog - -from watertap.core import InitializationMixin -from watertap.core.util.initialization import interval_initializer -from watertap.unit_models.crystallizer import Crystallization, CrystallizationData -from watertap_contrib.reflo.costing.units.crystallizer_watertap import ( - cost_crystallizer_watertap, -) +from idaes.core.util.config import is_physical_parameter_block from idaes.core.util.model_statistics import ( degrees_of_freedom, number_variables, number_total_constraints, number_unused_variables, ) -from idaes.core.util.testing import initialization_tester -from idaes.core.util.scaling import ( - calculate_scaling_factors, - unscaled_variables_generator, - badly_scaled_var_generator, -) -from watertap.unit_models.mvc.components.lmtd_chen_callback import ( - delta_temperature_chen_callback, -) +from idaes.core.util.tables import create_stream_table_dataframe +import idaes.logger as idaeslog + +from watertap.core import InitializationMixin +from watertap.core.solvers import get_solver from watertap_contrib.reflo.unit_models.crystallizer_effect import CrystallizerEffect from watertap_contrib.reflo.costing.units.multi_effect_crystallizer import ( @@ -76,7 +52,6 @@ ) _log = idaeslog.getLogger(__name__) -from idaes.core.util.scaling import * __author__ = "Oluwamayowa Amusat, Zhuoran Zhang, Kurban Sitterley" @@ -288,17 +263,6 @@ def eq_heating_steam_flow_rate(b): f"eq_energy_for_effect_{n}_from_effect_{n - 1}", energy_flow_constr ) - # brine_conc_constr = Constraint( - # expr=effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] - # >= 0.95* prev_effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] - # ) - # effect.add_component(f"eq_equiv_brine_conc_effect_{n}_lb", brine_conc_constr) - # brine_conc_constr = Constraint( - # expr=effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] - # <= 1.05 * prev_effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] - # ) - # effect.add_component(f"eq_equiv_brine_conc_effect_{n}_ub", brine_conc_constr) - self.total_flow_vol_in = Expression(expr=total_flow_vol_in_expr) def initialize_build( @@ -309,8 +273,6 @@ def initialize_build( optarg=None, ): """ - General wrapper for pressure changer initialization routines - Keyword Arguments: state_args : a dict of arguments to be passed to the property package(s) to provide an initial state for @@ -323,6 +285,14 @@ def initialize_build( Returns: None """ + + init_args = dict( + state_args=state_args, + outlvl=outlvl, + solver=solver, + optarg=optarg, + ) + init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit") @@ -330,7 +300,7 @@ def initialize_build( for n, eff in self.effects.items(): if n == 1: assert degrees_of_freedom(eff.effect) == 0 - eff.effect.initialize() + eff.effect.initialize(**init_args) inlet_conc = ( eff.effect.properties_in[0] .conc_mass_phase_comp["Liq", "NaCl"] @@ -340,7 +310,7 @@ def initialize_build( else: c = getattr(eff.effect, f"eq_energy_for_effect_{n}_from_effect_{n - 1}") c.deactivate() - eff.effect.initialize() + eff.effect.initialize(**init_args) c.activate() eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "H2O"].unfix() eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"].unfix() @@ -353,9 +323,7 @@ def initialize_build( with idaeslog.solver_log(init_log, idaeslog.DEBUG) as slc: res = opt.solve(self, tee=slc.tee) - print(f"\n\ntermination {res.solver.termination_condition}") init_log.info_high("Initialization Step 3 {}.".format(idaeslog.condition(res))) - # --------------------------------------------------------------------- if not check_optimal_termination(res): raise InitializationError(f"Unit model {self.name} failed to initialize") @@ -370,6 +338,15 @@ def default_costing_method(self): from watertap_contrib.reflo.costing import TreatmentCosting import watertap.property_models.unit_specific.cryst_prop_pack as props from watertap.property_models.water_prop_pack import WaterParameterBlock + from watertap.core.util.model_diagnostics.infeasible import * + from pyomo.util.calc_var_value import calculate_variable_from_constraint as cvc + from idaes.core.util.scaling import * + from idaes.core.util.testing import initialization_tester + from idaes.core.util.scaling import ( + calculate_scaling_factors, + unscaled_variables_generator, + badly_scaled_var_generator, + ) solver = get_solver() @@ -431,7 +408,6 @@ def default_costing_method(self): eff.effect.pressure_operating.fix( pyunits.convert(op_pressure * pyunits.bar, to_units=pyunits.Pa) ) - # eff.effect.overall_heat_transfer_coefficient.setlb(99) eff.effect.overall_heat_transfer_coefficient.set_value(100) if n == 1: eff.effect.overall_heat_transfer_coefficient.fix(100) @@ -457,20 +433,18 @@ def default_costing_method(self): mec.initialize() except: print_infeasible_constraints(m) - # assert False - print(f"dof before solve = {degrees_of_freedom(m)}") + print(f"DOF before solve = {degrees_of_freedom(m)}") results = solver.solve(m) print(f"termination {results.solver.termination_condition}") assert_optimal_termination(results) - for n, eff in mec.effects.items(): - print(f"\nEFFECT {n}\n") - eff.effect.overall_heat_transfer_coefficient.display() - eff.effect.properties_solids[0].flow_mass_phase_comp.display() + # for n, eff in mec.effects.items(): + # print(f"\nEFFECT {n}\n") + # eff.effect.overall_heat_transfer_coefficient.display() + # eff.effect.properties_solids[0].flow_mass_phase_comp.display() # eff.effect.temperature_operating.display() - # assert False m.fs.costing = TreatmentCosting() m.fs.mec.costing = UnitModelCostingBlock( flowsheet_costing_block=m.fs.costing, @@ -480,7 +454,8 @@ def default_costing_method(self): m.fs.costing.nacl_recovered.cost.set_value(-0.024) m.fs.costing.cost_process() m.fs.costing.add_LCOW(mec.total_flow_vol_in) - print(f"dof after costing = {degrees_of_freedom(m)}") + + print(f"DOF after costing = {degrees_of_freedom(m)}") results = solver.solve(m) print(f"termination {results.solver.termination_condition}") print(f"LCOW = {m.fs.costing.LCOW()}") @@ -603,3 +578,14 @@ def default_costing_method(self): # effect.add_component( # f"eq_equiv_pressure_operating_effect_{n}", op_press_constr # ) + +# brine_conc_constr = Constraint( +# expr=effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] +# >= 0.95* prev_effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] +# ) +# effect.add_component(f"eq_equiv_brine_conc_effect_{n}_lb", brine_conc_constr) +# brine_conc_constr = Constraint( +# expr=effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] +# <= 1.05 * prev_effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] +# ) +# effect.add_component(f"eq_equiv_brine_conc_effect_{n}_ub", brine_conc_constr) From a05cd2232edbddb6f337c65bf57b0ad50303f33c Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Wed, 18 Sep 2024 10:08:13 -0400 Subject: [PATCH 34/80] clean up --- .../reflo/unit_models/crystallizer_effect.py | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py index f25bb13a..caa95335 100644 --- a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py +++ b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py @@ -19,22 +19,18 @@ check_optimal_termination, assert_optimal_termination, Param, - Constraint, - Suffix, units as pyunits, ) -from pyomo.common.config import ConfigBlock, ConfigValue, In +from pyomo.common.config import ConfigValue # Import IDAES cores from idaes.core import ( declare_process_block_class, - UnitModelBlockData, useDefault, FlowsheetBlock, ) from watertap.core.solvers import get_solver from idaes.core.util.tables import create_stream_table_dataframe -from idaes.core.util.constants import Constants from idaes.core.util.config import is_physical_parameter_block from idaes.core.util.exceptions import InitializationError @@ -42,12 +38,9 @@ import idaes.core.util.scaling as iscale import idaes.logger as idaeslog -from watertap.core import InitializationMixin from watertap.core.util.initialization import interval_initializer from watertap.unit_models.crystallizer import Crystallization, CrystallizationData -from watertap_contrib.reflo.costing.units.crystallizer_watertap import ( - cost_crystallizer_watertap, -) + from idaes.core.util.model_statistics import ( degrees_of_freedom, number_variables, @@ -374,7 +367,7 @@ def initialize_build( state_args=state_args_vapor, ) - try: + if hasattr(self, "heating_steam"): state_args_steam = deepcopy(state_args) for p, j in self.properties_vapor.phase_component_set: @@ -386,8 +379,6 @@ def initialize_build( solver=solver, state_args=state_args_steam, ) - except: - pass init_log.info_high("Initialization Step 2 Complete.") @@ -400,7 +391,9 @@ def initialize_build( # --------------------------------------------------------------------- # Release Inlet state self.properties_in.release_state(flags, outlvl=outlvl) - init_log.info("Initialization Complete: {}".format(idaeslog.condition(res))) + init_log.info_high( + "Initialization Complete: {}".format(idaeslog.condition(res)) + ) if not check_optimal_termination(res): raise InitializationError(f"Unit model {self.name} failed to initialize") @@ -441,7 +434,7 @@ def calculate_scaling_factors(self): if iscale.get_scaling_factor(self.overall_heat_transfer_coefficient) is None: iscale.set_scaling_factor(self.overall_heat_transfer_coefficient, 0.01) - for ind, c in self.eq_p_con4.items(): + for _, c in self.eq_p_con4.items(): sf = iscale.get_scaling_factor(self.properties_pure_water[0].pressure) iscale.constraint_scaling_transform(c, sf) From 6d28dda28a1cac017f3d70f1050ef7987e918562 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Fri, 20 Sep 2024 15:32:36 -0400 Subject: [PATCH 35/80] clean up --- .../reflo/costing/units/multi_effect_crystallizer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/units/multi_effect_crystallizer.py b/src/watertap_contrib/reflo/costing/units/multi_effect_crystallizer.py index 51d4882c..ce805875 100644 --- a/src/watertap_contrib/reflo/costing/units/multi_effect_crystallizer.py +++ b/src/watertap_contrib/reflo/costing/units/multi_effect_crystallizer.py @@ -170,7 +170,7 @@ def cost_multi_effect_crystallizer( effect_capex_constr = pyo.Constraint( expr=effect_capex_var == effect_capex_expr, - doc="Constraint for capital cost of effect {effect_number}.", + doc=f"Constraint for capital cost of effect {effect_number}.", ) blk.add_component(f"capital_cost_effect_{effect_number}", effect_capex_var) blk.add_component( @@ -349,14 +349,14 @@ def _compute_steam_specific_energy(effect): + dh_constants[3] * t**3 + dh_constants[4] * t**4 ) - dh_vap = pyo.units.convert(dh_vap, to_units=pyo.units.kJ / pyo.units.kg) + # dh_vap = pyo.units.convert(dh_vap, to_units=pyo.units.kJ / pyo.units.kg) effect.dh_vap = dh_vap # 3. Compute specific volume: computed from Affandi expression (Eq 5) t_critical = 647.096 * pyo.units.K # t_red = temperature_sat / t_critical # Reduced temperature t_red = effect.heating_steam[0].temperature / t_critical # Reduced temperature - effect.t_red = t_red + sp_vol_constants = [ -7.75883 * pyo.units.dimensionless, 3.23753 * pyo.units.dimensionless, From f38d5f8057f4ba959a60ccc6e7ea862cef1bda15 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Fri, 20 Sep 2024 13:58:33 -0600 Subject: [PATCH 36/80] add test file for multi effect crystallizer --- .../tests/test_multi_effect_crystallizer.py | 1101 +++++++++++++++++ 1 file changed, 1101 insertions(+) create mode 100644 src/watertap_contrib/reflo/unit_models/zero_order/tests/test_multi_effect_crystallizer.py diff --git a/src/watertap_contrib/reflo/unit_models/zero_order/tests/test_multi_effect_crystallizer.py b/src/watertap_contrib/reflo/unit_models/zero_order/tests/test_multi_effect_crystallizer.py new file mode 100644 index 00000000..66f60c99 --- /dev/null +++ b/src/watertap_contrib/reflo/unit_models/zero_order/tests/test_multi_effect_crystallizer.py @@ -0,0 +1,1101 @@ +################################################################################# +# WaterTAP Copyright (c) 2020-2024, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# National Renewable Energy Laboratory, and National Energy Technology +# Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +################################################################################# + +import pytest + +from pyomo.util.check_units import assert_units_consistent +from pyomo.environ import ( + ConcreteModel, + Var, + assert_optimal_termination, + value, + units as pyunits, +) +from pyomo.network import Port +from idaes.core import FlowsheetBlock + +from idaes.core.util.model_statistics import ( + degrees_of_freedom, + number_variables, + number_total_constraints, + number_unused_variables, +) +from idaes.core.util.testing import initialization_tester +from idaes.core.util.scaling import ( + calculate_scaling_factors, + unscaled_variables_generator, + badly_scaled_var_generator, +) +from idaes.core import UnitModelCostingBlock + +from watertap.core.solvers import get_solver +from watertap.property_models.unit_specific.cryst_prop_pack import ( + NaClParameterBlock, + NaClStateBlock, +) +from watertap.property_models.water_prop_pack import ( + WaterParameterBlock, + WaterStateBlock, +) + +from watertap_contrib.reflo.costing import TreatmentCosting, CrystallizerCostType +from watertap_contrib.reflo.unit_models.multi_effect_crystallizer import ( + MultiEffectCrystallizer, +) +from watertap_contrib.reflo.costing.units.multi_effect_crystallizer import ( + MultiEffectCrystallizerCostType, +) + +from watertap_contrib.reflo.unit_models.crystallizer_effect import CrystallizerEffect + +__author__ = "Kurban Sitterley" + +solver = get_solver() + +feed_pressure = 101325 +feed_temperature = 273.15 + 20 +eps = 1e-6 + + +def build_mec4(): + + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.properties = NaClParameterBlock() + m.fs.vapor_properties = WaterParameterBlock() + + m.fs.unit = mec = MultiEffectCrystallizer( + property_package=m.fs.properties, property_package_vapor=m.fs.vapor_properties + ) + + feed_flow_mass = 1 + feed_mass_frac_NaCl = 0.15 + crystallizer_yield = 0.5 + operating_pressures = [0.45, 0.25, 0.208, 0.095] + feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl + + for (_, eff), op_pressure in zip(mec.effects.items(), operating_pressures): + eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "H2O"].fix( + feed_flow_mass * feed_mass_frac_H2O + ) + eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"].fix( + feed_flow_mass * feed_mass_frac_NaCl + ) + eff.effect.properties_in[0].pressure.fix(feed_pressure) + eff.effect.properties_in[0].temperature.fix(feed_temperature) + + eff.effect.properties_in[0].flow_mass_phase_comp["Sol", "NaCl"].fix(eps) + eff.effect.properties_in[0].flow_mass_phase_comp["Vap", "H2O"].fix(eps) + eff.effect.properties_in[0].conc_mass_phase_comp[...] + eff.effect.crystallization_yield["NaCl"].fix(crystallizer_yield) + eff.effect.crystal_growth_rate.fix() + eff.effect.souders_brown_constant.fix() + eff.effect.crystal_median_length.fix() + eff.effect.pressure_operating.fix( + pyunits.convert(op_pressure * pyunits.bar, to_units=pyunits.Pa) + ) + eff.effect.overall_heat_transfer_coefficient.set_value(0.1) + + first_effect = m.fs.unit.effects[1].effect + + first_effect.overall_heat_transfer_coefficient.fix(0.1) + first_effect.heating_steam[0].pressure_sat + first_effect.heating_steam[0].dh_vap_mass + first_effect.heating_steam.calculate_state( + var_args={ + ("flow_mass_phase_comp", ("Liq", "H2O")): 0, + ("pressure", None): 101325, + ("temperature", None): 393, + }, + hold_state=True, + ) + first_effect.heating_steam[0].flow_vol_phase + first_effect.heating_steam[0].flow_mass_phase_comp["Vap", "H2O"].unfix() + + return m + + +def build_mec3(): + + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.properties = NaClParameterBlock() + m.fs.vapor_properties = WaterParameterBlock() + + m.fs.unit = mec = MultiEffectCrystallizer( + property_package=m.fs.properties, + property_package_vapor=m.fs.vapor_properties, + number_effects=3, + ) + + feed_flow_mass = 10 + feed_mass_frac_NaCl = 0.25 + crystallizer_yield = 0.55 + operating_pressures = [0.85, 0.25, 0.208] + feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl + + for (_, eff), op_pressure in zip(mec.effects.items(), operating_pressures): + eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "H2O"].fix( + feed_flow_mass * feed_mass_frac_H2O + ) + eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"].fix( + feed_flow_mass * feed_mass_frac_NaCl + ) + eff.effect.properties_in[0].pressure.fix(feed_pressure) + eff.effect.properties_in[0].temperature.fix(feed_temperature) + + eff.effect.properties_in[0].flow_mass_phase_comp["Sol", "NaCl"].fix(eps) + eff.effect.properties_in[0].flow_mass_phase_comp["Vap", "H2O"].fix(eps) + eff.effect.properties_in[0].conc_mass_phase_comp[...] + eff.effect.crystallization_yield["NaCl"].fix(crystallizer_yield) + eff.effect.crystal_growth_rate.fix() + eff.effect.souders_brown_constant.fix() + eff.effect.crystal_median_length.fix() + eff.effect.pressure_operating.fix( + pyunits.convert(op_pressure * pyunits.bar, to_units=pyunits.Pa) + ) + eff.effect.overall_heat_transfer_coefficient.set_value(0.1) + + first_effect = m.fs.unit.effects[1].effect + + first_effect.overall_heat_transfer_coefficient.fix(0.1) + first_effect.heating_steam[0].pressure_sat + first_effect.heating_steam[0].dh_vap_mass + first_effect.heating_steam.calculate_state( + var_args={ + ("flow_mass_phase_comp", ("Liq", "H2O")): 0, + ("pressure", None): 101325, + ("temperature", None): 500, + }, + hold_state=True, + ) + first_effect.heating_steam[0].flow_vol_phase + first_effect.heating_steam[0].flow_mass_phase_comp["Vap", "H2O"].unfix() + + return m + + +class TestMultiEffectCrystallizer_4Effects: + @pytest.fixture(scope="class") + def MEC4_frame(self): + """ + Test crystallizer with 4 effects + """ + m = build_mec4() + return m + + @pytest.mark.unit + def test_config(self, MEC4_frame): + m = MEC4_frame + assert len(m.fs.unit.config) == 6 + + assert not m.fs.unit.config.dynamic + assert not m.fs.unit.config.has_holdup + assert m.fs.unit.config.property_package is m.fs.properties + assert m.fs.unit.config.property_package_vapor is m.fs.vapor_properties + assert m.fs.unit.config.number_effects == 4 + assert m.fs.unit.config.number_effects == len(m.fs.unit.effects) + + for _, eff in m.fs.unit.effects.items(): + assert isinstance(eff.effect, CrystallizerEffect) + + @pytest.mark.unit + def test_build(self, MEC4_frame): + m = MEC4_frame + + # test ports and variables + port_lst = ["inlet", "outlet", "solids", "vapor", "steam", "pure_water"] + port_vars_lst = ["flow_mass_phase_comp", "pressure", "temperature"] + state_blks = [ + "properties_in", + "properties_out", + "properties_pure_water", + "properties_solids", + "properties_vapor", + ] + + for port_str in port_lst: + assert hasattr(m.fs.unit, port_str) + port = getattr(m.fs.unit, port_str) + assert len(port.vars) == 3 + assert isinstance(port, Port) + for var_str in port_vars_lst: + assert hasattr(port, var_str) + var = getattr(port, var_str) + assert isinstance(var, Var) + + for n, eff in m.fs.unit.effects.items(): + for b in state_blks: + assert hasattr(eff.effect, b) + sb = getattr(eff.effect, b) + assert isinstance(sb, NaClStateBlock) + assert sb[0].temperature.ub == 1000 + + effect_params = [ + "approach_temperature_heat_exchanger", + "dimensionless_crystal_length", + ] + effect_vars = [ + "crystal_growth_rate", + "crystal_median_length", + "crystallization_yield", + "dens_mass_magma", + "dens_mass_slurry", + "diameter_crystallizer", + "height_crystallizer", + "height_slurry", + "magma_circulation_flow_vol", + "pressure_operating", + "product_volumetric_solids_fraction", + "relative_supersaturation", + "souders_brown_constant", + "t_res", + "temperature_operating", + "volume_suspension", + "work_mechanical", + ] + + effect_exprs = [ + "delta_temperature", + "eq_max_allowable_velocity", + "eq_minimum_height_diameter_ratio", + "eq_vapor_space_height", + ] + + effect_constr = [ + "eq_T_con1", + "eq_T_con2", + "eq_T_con3", + "eq_crystallizer_height_constraint", + "eq_dens_magma", + "eq_dens_mass_slurry", + "eq_enthalpy_balance", + "eq_mass_balance_constraints", + "eq_minimum_hex_circulation_rate_constraint", + "eq_operating_pressure_constraint", + "eq_p_con1", + "eq_p_con2", + "eq_p_con3", + "eq_p_con4", + "eq_p_con5", + "eq_pure_vapor_flow_rate", + "eq_relative_supersaturation", + "eq_removal_balance", + "eq_residence_time", + "eq_slurry_height_constraint", + "eq_solubility_massfrac_equality_constraint", + "eq_suspension_volume", + "eq_vapor_energy_constraint", + "eq_vapor_head_diameter_constraint", + "eq_vol_fraction_solids", + ] + + for n, eff in m.fs.unit.effects.items(): + for p in effect_params: + assert hasattr(eff.effect, p) + for v in effect_vars: + assert hasattr(eff.effect, v) + for e in effect_exprs: + assert hasattr(eff.effect, e) + for c in effect_constr: + assert hasattr(eff.effect, c) + assert hasattr(eff.effect, f"eq_delta_temperature_inlet_effect_{n}") + assert hasattr(eff.effect, f"eq_delta_temperature_outlet_effect_{n}") + assert hasattr(eff.effect, f"eq_heat_transfer_effect_{n}") + + if n == 1: + assert number_variables(eff.effect) == 150 + assert number_total_constraints(eff.effect) == 124 + assert number_unused_variables(eff.effect) == 1 + assert hasattr(eff.effect, "heating_steam") + assert isinstance(eff.effect.heating_steam, WaterStateBlock) + assert eff.effect.heating_steam[0].temperature.ub == 1000 + assert hasattr(eff.effect, "eq_heating_steam_flow_rate") + + if n != 1: + assert number_variables(eff.effect) == 140 + assert number_total_constraints(eff.effect) == 118 + assert number_unused_variables(eff.effect) == 1 + assert hasattr( + eff.effect, f"eq_energy_for_effect_{n}_from_effect_{n - 1}" + ) + + assert number_variables(m) == 712 + assert number_total_constraints(m) == 478 + assert number_unused_variables(m) == 37 + + assert_units_consistent(m) + + @pytest.mark.unit + def test_dof(self, MEC4_frame): + m = MEC4_frame + assert degrees_of_freedom(m) == 0 + for n, eff in m.fs.unit.effects.items(): + if n == 1: + assert degrees_of_freedom(eff.effect) == 0 + else: + assert degrees_of_freedom(eff.effect) == 3 + + @pytest.mark.unit + def test_calculate_scaling(self, MEC4_frame): + m = MEC4_frame + + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Liq", "H2O") + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Liq", "NaCl") + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Vap", "H2O") + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Sol", "NaCl") + ) + m.fs.vapor_properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Vap", "H2O") + ) + m.fs.vapor_properties.set_default_scaling( + "flow_mass_phase_comp", 1, index=("Liq", "H2O") + ) + + calculate_scaling_factors(m) + unscaled_var_list = list(unscaled_variables_generator(m)) + assert len(unscaled_var_list) == 0 + badly_scaled_var_list = list(badly_scaled_var_generator(m)) + assert len(badly_scaled_var_list) == 0 + + @pytest.mark.component + def test_initialize(self, MEC4_frame): + m = MEC4_frame + m.fs.unit.initialize() + for n, eff in m.fs.unit.effects.items(): + if n == 1: + htc = value(eff.effect.overall_heat_transfer_coefficient) + c0 = value( + eff.effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] + ) + assert degrees_of_freedom(eff.effect) == 0 + if n != 1: + assert ( + not eff.effect.properties_in[0] + .flow_mass_phase_comp["Liq", "H2O"] + .is_fixed() + ) + assert ( + not eff.effect.properties_in[0] + .flow_mass_phase_comp["Liq", "NaCl"] + .is_fixed() + ) + assert ( + eff.effect.properties_in[0] + .conc_mass_phase_comp["Liq", "NaCl"] + .is_fixed() + ) + assert ( + value( + eff.effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] + ) + == c0 + ) + c = getattr(eff.effect, f"eq_energy_for_effect_{n}_from_effect_{n - 1}") + assert c.active + assert eff.effect.overall_heat_transfer_coefficient.is_fixed() + assert value(eff.effect.overall_heat_transfer_coefficient) == htc + assert degrees_of_freedom(eff.effect) == 3 + + @pytest.mark.component + def test_solve(self, MEC4_frame): + + m = MEC4_frame + + results = solver.solve(m) + assert_optimal_termination(results) + + @pytest.mark.component + def test_conservation(self, MEC4_frame): + + m = MEC4_frame + + comp_lst = ["NaCl", "H2O"] + phase_lst = ["Sol", "Liq", "Vap"] + + for _, e in m.fs.unit.effects.items(): + eff = e.effect + + phase_comp_list = [ + (p, j) + for j in comp_lst + for p in phase_lst + if (p, j) in eff.properties_in[0].phase_component_set + ] + flow_mass_in = sum( + eff.properties_in[0].flow_mass_phase_comp[p, j] + for p in phase_lst + for j in comp_lst + if (p, j) in phase_comp_list + ) + flow_mass_out = sum( + eff.properties_out[0].flow_mass_phase_comp[p, j] + for p in phase_lst + for j in comp_lst + if (p, j) in phase_comp_list + ) + flow_mass_solids = sum( + eff.properties_solids[0].flow_mass_phase_comp[p, j] + for p in phase_lst + for j in comp_lst + if (p, j) in phase_comp_list + ) + flow_mass_vapor = sum( + eff.properties_vapor[0].flow_mass_phase_comp[p, j] + for p in phase_lst + for j in comp_lst + if (p, j) in phase_comp_list + ) + + assert ( + abs( + value( + flow_mass_in + - flow_mass_out + - flow_mass_solids + - flow_mass_vapor + ) + ) + <= 1e-6 + ) + + assert ( + abs( + value( + flow_mass_in * eff.properties_in[0].enth_mass_phase["Liq"] + - flow_mass_out * eff.properties_out[0].enth_mass_phase["Liq"] + - flow_mass_vapor + * eff.properties_vapor[0].enth_mass_solvent["Vap"] + - flow_mass_solids + * eff.properties_solids[0].enth_mass_solute["Sol"] + - flow_mass_solids + * eff.properties_solids[0].dh_crystallization_mass_comp["NaCl"] + + eff.work_mechanical[0] + ) + ) + <= 1e-2 + ) + + assert ( + pytest.approx(value(m.fs.unit.total_flow_vol_in), rel=1e-3) == 0.00321056811 + ) + + assert ( + pytest.approx( + sum( + value(eff.effect.properties_in[0].flow_vol_phase["Liq"]) + for _, eff in m.fs.unit.effects.items() + ), + rel=1e-3, + ) + == 0.00321056811 + ) + + @pytest.mark.component + def test_solution(self, MEC4_frame): + m = MEC4_frame + + unit_results_dict = { + 1: { + "delta_temperature": {0.0: 60.65}, + "delta_temperature_in": {0.0: 33.51}, + "delta_temperature_out": {0.0: 99.85}, + "dens_mass_magma": 281.24, + "dens_mass_slurry": 1298.23, + "diameter_crystallizer": 1.0799, + "energy_flow_superheated_vapor": 1518.73, + "eq_max_allowable_velocity": 2.6304, + "eq_minimum_height_diameter_ratio": 1.6199, + "eq_vapor_space_height": 0.809971, + "heat_exchanger_area": 281, + "height_crystallizer": 1.88304, + "height_slurry": 1.0730, + "magma_circulation_flow_vol": 0.119395, + "product_volumetric_solids_fraction": 0.1329756, + "relative_supersaturation": {"NaCl": 0.56703}, + "temperature_operating": 359.48, + "volume_suspension": 0.98296, + "work_mechanical": {0.0: 1704.47}, + }, + 2: { + "delta_temperature": {0.0: 31.59}, + "delta_temperature_in": {0.0: 14.61}, + "delta_temperature_out": {0.0: 58.77}, + "dens_mass_magma": 279.46, + "dens_mass_slurry": 1301.86, + "diameter_crystallizer": 1.1773, + "energy_flow_superheated_vapor": 1396.71, + "eq_max_allowable_velocity": 3.46414453, + "eq_minimum_height_diameter_ratio": 1.7660549, + "eq_vapor_space_height": 0.883027, + "heat_exchanger_area": 480.72, + "height_crystallizer": 1.7660549, + "height_slurry": 0.828680, + "magma_circulation_flow_vol": 0.105985, + "product_volumetric_solids_fraction": 0.132132, + "relative_supersaturation": {"NaCl": 0.571251}, + "t_res": 1.0228, + "temperature_operating": 344.86, + "volume_suspension": 0.9022, + "work_mechanical": {0.0: 1518.73}, + }, + 3: { + "delta_temperature": {0.0: 16.853}, + "delta_temperature_in": {0.0: 4.3421}, + "delta_temperature_out": {0.0: 44.835}, + "dens_mass_magma": 279.07, + "dens_mass_slurry": 1303.08, + "diameter_crystallizer": 1.1811, + "energy_flow_superheated_vapor": 1296.7, + "eq_max_allowable_velocity": 3.776, + "eq_minimum_height_diameter_ratio": 1.7717, + "eq_vapor_space_height": 0.88588, + "heat_exchanger_area": 828.74, + "height_crystallizer": 1.7717, + "height_slurry": 0.763759, + "magma_circulation_flow_vol": 0.097350, + "product_volumetric_solids_fraction": 0.13195, + "relative_supersaturation": {"NaCl": 0.57239}, + "temperature_operating": 340.5214, + "volume_suspension": 0.836915, + "work_mechanical": {0.0: 1396.71}, + }, + 4: { + "delta_temperature": {0.0: 27.3299}, + "delta_temperature_in": {0.0: 17.299}, + "delta_temperature_out": {0.0: 40.695}, + "dens_mass_magma": 278.12, + "dens_mass_slurry": 1308.54, + "diameter_crystallizer": 1.3795, + "energy_flow_superheated_vapor": 1250.3, + "eq_max_allowable_velocity": 5.4596, + "eq_minimum_height_diameter_ratio": 2.0692, + "eq_vapor_space_height": 1.03464, + "heat_exchanger_area": 474.4, + "height_crystallizer": 2.0692, + "height_slurry": 0.53750, + "magma_circulation_flow_vol": 0.089893, + "product_volumetric_solids_fraction": 0.131503, + "relative_supersaturation": {"NaCl": 0.576471}, + "t_res": 1.0228, + "temperature_operating": 323.2, + "volume_suspension": 0.803391, + "work_mechanical": {0.0: 1296.70}, + }, + } + + for n, d in unit_results_dict.items(): + eff = m.fs.unit.effects[n].effect + for v, r in d.items(): + effv = getattr(eff, v) + if effv.is_indexed(): + for i, s in r.items(): + assert pytest.approx(value(effv[i]), rel=1e-3) == s + else: + assert pytest.approx(value(effv), rel=1e-3) == r + + steam_results_dict = { + "dens_mass_phase": {"Liq": 943.14, "Vap": 0.55862}, + "dh_vap_mass": 2202682, + "flow_mass_phase_comp": { + ("Liq", "H2O"): 0.0, + ("Vap", "H2O"): 0.77381, + }, + "flow_vol_phase": {"Liq": 0.0, "Vap": 1.3852}, + "pressure_sat": 197743, + "temperature": 393, + } + + for v, r in steam_results_dict.items(): + sv = getattr(m.fs.unit.effects[1].effect.heating_steam[0], v) + if sv.is_indexed(): + for i, s in r.items(): + assert pytest.approx(value(sv[i]), rel=1e-3) == s + else: + assert pytest.approx(value(sv), rel=1e-3) == r + + @pytest.mark.component + def test_costing(self, MEC4_frame): + m = MEC4_frame + m.fs.costing = TreatmentCosting() + m.fs.unit.costing = UnitModelCostingBlock( + flowsheet_costing_block=m.fs.costing, + ) + + m.fs.costing.nacl_recovered.cost.set_value(-0.024) + m.fs.costing.cost_process() + m.fs.costing.add_LCOW(m.fs.unit.total_flow_vol_in) + m.fs.costing.add_specific_energy_consumption( + m.fs.unit.total_flow_vol_in, name="SEC" + ) + results = solver.solve(m) + assert_optimal_termination(results) + + sys_costing_dict = { + "LCOW": 5.1605, + "SEC": 0.651465, + "NaCl_recovered_cost": -0.024, + "aggregate_capital_cost": 4613044.1, + "aggregate_direct_capital_cost": 2306522, + "aggregate_flow_NaCl_recovered": 0.26696, + "aggregate_flow_costs": { + "NaCl_recovered": -240108.0, + "electricity": 5423.99, + "steam": 102690.74, + }, + "aggregate_flow_electricity": 7.5296, + "aggregate_flow_steam": 0.69298, + "capital_recovery_factor": 0.111955, + "maintenance_labor_chemical_operating_cost": 138391.3, + "total_annualized_cost": 522855.76, + "total_capital_cost": 4613044.1, + "total_fixed_operating_cost": 138391.32, + "total_operating_cost": 6398.03, + "total_variable_operating_cost": -131993.29, + } + + for v, r in sys_costing_dict.items(): + cv = getattr(m.fs.costing, v) + if cv.is_indexed(): + for i, s in r.items(): + assert pytest.approx(value(cv[i]), rel=1e-3) == s + else: + assert pytest.approx(value(cv), rel=1e-3) == r + + eff_costing_dict = { + "capital_cost": 4613044.11, + "capital_cost_effect_1": 476995.87, + "capital_cost_effect_2": 562980.66, + "capital_cost_effect_3": 726747.44, + "capital_cost_effect_4": 539798.07, + "direct_capital_cost": 2306522.05, + } + + for v, r in eff_costing_dict.items(): + cv = getattr(m.fs.unit.costing, v) + if cv.is_indexed(): + for i, s in r.items(): + assert pytest.approx(value(cv[i]), rel=1e-3) == s + else: + assert pytest.approx(value(cv), rel=1e-3) == r + + @pytest.mark.component + def test_costing_by_volume(self): + + m = build_mec4() + + for _, eff in m.fs.unit.effects.items(): + eff.effect.crystal_median_length.fix(0.6e-3) + eff.effect.crystal_growth_rate.fix(5e-9) + + m.fs.unit.initialize() + + results = solver.solve(m) + assert_optimal_termination(results) + + m.fs.costing = TreatmentCosting() + m.fs.unit.costing = UnitModelCostingBlock( + flowsheet_costing_block=m.fs.costing, + costing_method_arguments={"cost_type": "volume_basis"}, + ) + + m.fs.costing.nacl_recovered.cost.set_value(-0.024) + m.fs.costing.cost_process() + m.fs.costing.add_LCOW(m.fs.unit.total_flow_vol_in) + m.fs.costing.add_specific_energy_consumption( + m.fs.unit.total_flow_vol_in, name="SEC" + ) + results = solver.solve(m) + + assert_optimal_termination(results) + + sys_costing_dict = { + "LCOW": 5.364, + "SEC": 0.65146, + "aggregate_capital_cost": 4758270.1, + "aggregate_direct_capital_cost": 2379135.0, + "aggregate_flow_NaCl_recovered": 0.26696, + "aggregate_flow_costs": { + "NaCl_recovered": -240108.02, + "electricity": 5423.9, + "steam": 102690.74, + }, + "aggregate_flow_electricity": 7.529, + "aggregate_flow_steam": 0.69298, + "capital_recovery_factor": 0.11195, + "maintenance_labor_chemical_operating_cost": 142748.1, + "total_annualized_cost": 543471.45, + "total_capital_cost": 4758270.10, + "total_fixed_operating_cost": 142748.1, + "total_operating_cost": 10754.8, + "total_variable_operating_cost": -131993.29, + } + + for v, r in sys_costing_dict.items(): + cv = getattr(m.fs.costing, v) + if cv.is_indexed(): + for i, s in r.items(): + assert pytest.approx(value(cv[i]), rel=1e-3) == s + else: + assert pytest.approx(value(cv), rel=1e-3) == r + + eff_costing_dict = { + "capital_cost": 4758270.1, + "capital_cost_effect_1": 485242.6, + "capital_cost_effect_2": 578618.5, + "capital_cost_effect_3": 744925.5, + "capital_cost_effect_4": 570348.2, + "direct_capital_cost": 2379135.0, + } + + for v, r in eff_costing_dict.items(): + cv = getattr(m.fs.unit.costing, v) + if cv.is_indexed(): + for i, s in r.items(): + assert pytest.approx(value(cv[i]), rel=1e-3) == s + else: + assert pytest.approx(value(cv), rel=1e-3) == r + + +class TestMultiEffectCrystallizer_3Effects: + @pytest.fixture(scope="class") + def MEC3_frame(self): + """ + Test 3 effect crystallizer + """ + m = build_mec3() + return m + + @pytest.mark.unit + def test_config(self, MEC3_frame): + m = MEC3_frame + assert len(m.fs.unit.config) == 6 + + assert not m.fs.unit.config.dynamic + assert not m.fs.unit.config.has_holdup + assert m.fs.unit.config.property_package is m.fs.properties + assert m.fs.unit.config.property_package_vapor is m.fs.vapor_properties + assert m.fs.unit.config.number_effects == 3 + assert m.fs.unit.config.number_effects == len(m.fs.unit.effects) + + assert isinstance(m.fs.unit.effects, FlowsheetBlock) + + for _, eff in m.fs.unit.effects.items(): + assert isinstance(eff.effect, CrystallizerEffect) + + @pytest.mark.unit + def test_dof(self, MEC3_frame): + m = MEC3_frame + assert degrees_of_freedom(m) == 0 + for n, eff in m.fs.unit.effects.items(): + if n == 1: + assert degrees_of_freedom(eff.effect) == 0 + else: + assert degrees_of_freedom(eff.effect) == 3 + + @pytest.mark.component + def test_initialize(self, MEC3_frame): + m = MEC3_frame + m.fs.unit.initialize() + for n, eff in m.fs.unit.effects.items(): + if n == 1: + htc = value(eff.effect.overall_heat_transfer_coefficient) + c0 = value( + eff.effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] + ) + assert degrees_of_freedom(eff.effect) == 0 + if n != 1: + assert ( + not eff.effect.properties_in[0] + .flow_mass_phase_comp["Liq", "H2O"] + .is_fixed() + ) + assert ( + not eff.effect.properties_in[0] + .flow_mass_phase_comp["Liq", "NaCl"] + .is_fixed() + ) + assert ( + eff.effect.properties_in[0] + .conc_mass_phase_comp["Liq", "NaCl"] + .is_fixed() + ) + assert ( + value( + eff.effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] + ) + == c0 + ) + c = getattr(eff.effect, f"eq_energy_for_effect_{n}_from_effect_{n - 1}") + assert c.active + assert eff.effect.overall_heat_transfer_coefficient.is_fixed() + assert value(eff.effect.overall_heat_transfer_coefficient) == htc + assert degrees_of_freedom(eff.effect) == 3 + + @pytest.mark.component + def test_solve(self, MEC3_frame): + + m = MEC3_frame + + results = solver.solve(m) + assert_optimal_termination(results) + + @pytest.mark.component + def test_conservation(self, MEC3_frame): + + m = MEC3_frame + + comp_lst = ["NaCl", "H2O"] + phase_lst = ["Sol", "Liq", "Vap"] + + for _, e in m.fs.unit.effects.items(): + eff = e.effect + + phase_comp_list = [ + (p, j) + for j in comp_lst + for p in phase_lst + if (p, j) in eff.properties_in[0].phase_component_set + ] + flow_mass_in = sum( + eff.properties_in[0].flow_mass_phase_comp[p, j] + for p in phase_lst + for j in comp_lst + if (p, j) in phase_comp_list + ) + flow_mass_out = sum( + eff.properties_out[0].flow_mass_phase_comp[p, j] + for p in phase_lst + for j in comp_lst + if (p, j) in phase_comp_list + ) + flow_mass_solids = sum( + eff.properties_solids[0].flow_mass_phase_comp[p, j] + for p in phase_lst + for j in comp_lst + if (p, j) in phase_comp_list + ) + flow_mass_vapor = sum( + eff.properties_vapor[0].flow_mass_phase_comp[p, j] + for p in phase_lst + for j in comp_lst + if (p, j) in phase_comp_list + ) + + assert ( + abs( + value( + flow_mass_in + - flow_mass_out + - flow_mass_solids + - flow_mass_vapor + ) + ) + <= 1e-6 + ) + + assert ( + abs( + value( + flow_mass_in * eff.properties_in[0].enth_mass_phase["Liq"] + - flow_mass_out * eff.properties_out[0].enth_mass_phase["Liq"] + - flow_mass_vapor + * eff.properties_vapor[0].enth_mass_solvent["Vap"] + - flow_mass_solids + * eff.properties_solids[0].enth_mass_solute["Sol"] + - flow_mass_solids + * eff.properties_solids[0].dh_crystallization_mass_comp["NaCl"] + + eff.work_mechanical[0] + ) + ) + <= 1e-2 + ) + + assert pytest.approx(value(m.fs.unit.total_flow_vol_in), rel=1e-3) == 0.02321134 + + assert ( + pytest.approx( + sum( + value(eff.effect.properties_in[0].flow_vol_phase["Liq"]) + for _, eff in m.fs.unit.effects.items() + ), + rel=1e-3, + ) + == 0.02321134 + ) + + @pytest.mark.component + def test_solution(self, MEC3_frame): + m = MEC3_frame + + unit_results_dict = { + 1: { + "delta_temperature": {0.0: 161.4093}, + "delta_temperature_in": {0.0: 123.1938}, + "delta_temperature_out": {0.0: 206.85}, + "dens_mass_magma": 337.00, + "dens_mass_slurry": 1318.39, + "diameter_crystallizer": 2.4814, + "energy_flow_superheated_vapor": 10546.62, + "eq_max_allowable_velocity": 1.95486, + "eq_minimum_height_diameter_ratio": 3.72212, + "eq_vapor_space_height": 1.8610, + "heat_exchanger_area": 776.59, + "height_crystallizer": 4.96774, + "height_slurry": 3.10668, + "magma_circulation_flow_vol": 0.89371, + "product_volumetric_solids_fraction": 0.15933, + "relative_supersaturation": {"NaCl": 0.654196}, + "temperature_operating": 376.80, + "volume_suspension": 15.0239, + "work_mechanical": {0.0: 12535}, + }, + 2: { + "delta_temperature": {0.0: 50.4936}, + "delta_temperature_in": {0.0: 31.9425}, + "delta_temperature_out": {0.0: 75.2194}, + "dens_mass_magma": 331.37, + "dens_mass_slurry": 1324.86, + "diameter_crystallizer": 3.0991, + "energy_flow_superheated_vapor": 9677.8, + "eq_max_allowable_velocity": 3.4641, + "eq_minimum_height_diameter_ratio": 4.6487, + "eq_vapor_space_height": 2.3243, + "heat_exchanger_area": 2088.70, + "height_crystallizer": 4.6487, + "height_slurry": 1.8467, + "magma_circulation_flow_vol": 0.7461, + "product_volumetric_solids_fraction": 0.15667, + "relative_supersaturation": {"NaCl": 0.6664}, + "temperature_operating": 344.86, + "volume_suspension": 13.9312, + "work_mechanical": {0.0: 10546.6}, + }, + 3: { + "delta_temperature": {0.0: 16.8533}, + "delta_temperature_in": {0.0: 4.3421}, + "delta_temperature_out": {0.0: 44.8351}, + "dens_mass_magma": 330.8, + "dens_mass_slurry": 1325.9, + "diameter_crystallizer": 3.1102, + "energy_flow_superheated_vapor": 8990.6, + "eq_max_allowable_velocity": 3.7763, + "eq_minimum_height_diameter_ratio": 4.6653, + "eq_vapor_space_height": 2.3326, + "heat_exchanger_area": 5742.36, + "height_crystallizer": 4.6653, + "height_slurry": 1.7045, + "magma_circulation_flow_vol": 0.68382, + "product_volumetric_solids_fraction": 0.156410, + "relative_supersaturation": {"NaCl": 0.667858}, + "temperature_operating": 340.5, + "volume_suspension": 12.950, + "work_mechanical": {0.0: 9677.8}, + }, + } + + for n, d in unit_results_dict.items(): + eff = m.fs.unit.effects[n].effect + for v, r in d.items(): + effv = getattr(eff, v) + if effv.is_indexed(): + for i, s in r.items(): + assert pytest.approx(value(effv[i]), rel=1e-3) == s + else: + assert pytest.approx(value(effv), rel=1e-3) == r + + steam_results_dict = { + "dens_mass_phase": {"Liq": 828.03, "Vap": 0.439081}, + "dh_vap_mass": 1827723.26, + "flow_mass_phase_comp": {("Liq", "H2O"): 0.0, ("Vap", "H2O"): 6.858}, + "flow_vol_phase": {"Liq": 0.0, "Vap": 15.619}, + "pressure_sat": 2639870.3, + } + + for v, r in steam_results_dict.items(): + sv = getattr(m.fs.unit.effects[1].effect.heating_steam[0], v) + if sv.is_indexed(): + for i, s in r.items(): + assert pytest.approx(value(sv[i]), rel=1e-3) == s + else: + assert pytest.approx(value(sv), rel=1e-3) == r + + @pytest.mark.component + def test_costing(self, MEC3_frame): + m = MEC3_frame + m.fs.costing = TreatmentCosting() + m.fs.unit.costing = UnitModelCostingBlock( + flowsheet_costing_block=m.fs.costing, + ) + + m.fs.costing.nacl_recovered.cost.set_value(-0.004) + m.fs.costing.cost_process() + m.fs.costing.add_LCOW(m.fs.unit.total_flow_vol_in) + m.fs.costing.add_specific_energy_consumption( + m.fs.unit.total_flow_vol_in, name="SEC" + ) + results = solver.solve(m) + assert_optimal_termination(results) + + sys_costing_dict = { + "LCOW": 2.7665, + "SEC": 0.51529, + "aggregate_capital_cost": 17518907.2, + "aggregate_direct_capital_cost": 8759453.6, + "aggregate_flow_NaCl_recovered": 3.7919, + "aggregate_flow_costs": { + "NaCl_recovered": -568416.4, + "electricity": 31017.1, + "steam": 76976.0, + }, + "aggregate_flow_electricity": 43.05, + "aggregate_flow_steam": 0.51945, + "capital_recovery_factor": 0.11195, + "total_annualized_cost": 2026489.8, + "total_capital_cost": 17518907.2, + "total_fixed_operating_cost": 525567.2, + "total_operating_cost": 65143.9, + "total_variable_operating_cost": -460423.2, + } + + for v, r in sys_costing_dict.items(): + cv = getattr(m.fs.costing, v) + if cv.is_indexed(): + for i, s in r.items(): + assert pytest.approx(value(cv[i]), rel=1e-3) == s + else: + assert pytest.approx(value(cv), rel=1e-3) == r + + eff_costing_dict = { + "capital_cost": 17518907.2, + "capital_cost_effect_1": 1939078.0, + "capital_cost_effect_2": 2525607.6, + "capital_cost_effect_3": 4294767.8, + "direct_capital_cost": 8759453.6, + } + + for v, r in eff_costing_dict.items(): + cv = getattr(m.fs.unit.costing, v) + if cv.is_indexed(): + for i, s in r.items(): + assert pytest.approx(value(cv[i]), rel=1e-3) == s + else: + assert pytest.approx(value(cv), rel=1e-3) == r From 3d1fbb7c92a272edd758cae9958e174333a0f2e2 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Fri, 20 Sep 2024 13:59:19 -0600 Subject: [PATCH 37/80] change units for consistency; clean up --- .../reflo/unit_models/crystallizer_effect.py | 36 ++++++++----------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py index caa95335..675474f1 100644 --- a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py +++ b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py @@ -65,7 +65,7 @@ @declare_process_block_class("CrystallizerEffect") class CrystallizerEffectData(CrystallizationData): """ - Zero-order model for crystallizer effect + Zero dimensional model for crystallizer effect """ CONFIG = CrystallizationData.CONFIG() @@ -76,7 +76,7 @@ class CrystallizerEffectData(CrystallizationData): default=useDefault, domain=is_physical_parameter_block, description="Property package to use for heating steam properties", - doc="""Property parameter object used to define steasm property calculations, + doc="""Property parameter object used to define steam property calculations, **default** - useDefault. **Valid values:** { **useDefault** - use default package from parent model or flowsheet, @@ -88,8 +88,8 @@ class CrystallizerEffectData(CrystallizationData): ConfigValue( default=True, domain=bool, - description="Property package to use for heating and motive steam properties", - doc="""Property parameter object used to define steasm property calculations, + description="Flag to indicate if model is used alone or as part of MultiEffectCrystallizer.", + doc="""Flag to indicate if model is used alone or as part of MultiEffectCrystallizer unit model, **default** - True.""", ), ) @@ -147,7 +147,7 @@ def build(self): self.energy_flow_superheated_vapor = Var( initialize=1e5, bounds=(-5e6, 5e6), - units=pyunits.kJ / pyunits.s, + units=pyunits.kilowatt, doc="Energy that could be supplied from vapor", ) @@ -155,14 +155,15 @@ def build(self): self.flowsheet().time, initialize=35, bounds=(None, None), - units=pyunits.K, + units=pyunits.degK, doc="Temperature difference at the inlet side", ) + self.delta_temperature_out = Var( self.flowsheet().time, initialize=35, bounds=(None, None), - units=pyunits.K, + units=pyunits.degK, doc="Temperature difference at the outlet side", ) @@ -172,14 +173,14 @@ def build(self): initialize=1000.0, bounds=(0, None), units=pyunits.m**2, - doc="Heat exchanger heat_exchanger_area", + doc="Heat exchanger area", ) self.overall_heat_transfer_coefficient = Var( initialize=100.0, bounds=(0, None), - units=pyunits.W / pyunits.m**2 / pyunits.K, - doc="Overall heat transfer coefficient", + units=pyunits.kilowatt / pyunits.m**2 / pyunits.degK, + doc="Overall heat transfer coefficient for heat exchangers", ) @self.Constraint() @@ -263,7 +264,6 @@ def eq_heat_transfer(b): self.del_component(self.solids) self.del_component(self.vapor) self.del_component(self.pure_water) - # self.del_component(self.steam) def initialize_build( self, @@ -292,8 +292,6 @@ def initialize_build( opt = get_solver(solver, optarg) - # --------------------------------------------------------------------- - # Initialize holdup block flags = self.properties_in.initialize( outlvl=outlvl, optarg=optarg, @@ -302,9 +300,7 @@ def initialize_build( hold_state=True, ) init_log.info_high("Initialization Step 1 Complete.") - # --------------------------------------------------------------------- - # Initialize other state blocks - # Set state_args from inlet state + if state_args is None: state_args = {} state_dict = self.properties_in[ @@ -333,7 +329,7 @@ def initialize_build( state_args_solids["flow_mass_phase_comp"][p, j] = state_args[ "flow_mass_phase_comp" ]["Liq", j] - elif p == "Liq" or p == "Vap": + elif p in ["Liq", "Vap"]: state_args_solids["flow_mass_phase_comp"][p, j] = 1e-8 self.properties_solids.initialize( @@ -383,13 +379,11 @@ def initialize_build( init_log.info_high("Initialization Step 2 Complete.") interval_initializer(self) - # --------------------------------------------------------------------- - # Solve unit + with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(self, tee=slc.tee) init_log.info_high("Initialization Step 3 {}.".format(idaeslog.condition(res))) - # --------------------------------------------------------------------- - # Release Inlet state + self.properties_in.release_state(flags, outlvl=outlvl) init_log.info_high( "Initialization Complete: {}".format(idaeslog.condition(res)) From b4ce29ba50ed30e80b3201ba398103358019b0db Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Fri, 20 Sep 2024 14:00:19 -0600 Subject: [PATCH 38/80] fix for units consistency; clean up --- .../unit_models/multi_effect_crystallizer.py | 139 +++++++++++++++--- 1 file changed, 118 insertions(+), 21 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py index 8976806a..7c1f7310 100644 --- a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py +++ b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py @@ -34,14 +34,10 @@ ) from idaes.core.util.exceptions import InitializationError from idaes.core.util.config import is_physical_parameter_block -from idaes.core.util.model_statistics import ( - degrees_of_freedom, - number_variables, - number_total_constraints, - number_unused_variables, -) +from idaes.core.util.model_statistics import degrees_of_freedom from idaes.core.util.tables import create_stream_table_dataframe import idaes.logger as idaeslog +from idaes.core.util.constants import Constants from watertap.core import InitializationMixin from watertap.core.solvers import get_solver @@ -158,6 +154,7 @@ def build(self): standalone=False, ) effect.properties_in[0].conc_mass_phase_comp + total_flow_vol_in_expr += effect.properties_in[0].flow_vol_phase["Liq"] if n == self.first_effect: @@ -174,10 +171,8 @@ def build(self): ) ) - # self.add_port(name="steam", block=effect.heating_steam) - @effect.Constraint( - doc="Change in temperature at inlet for first effect." + doc="Change in temperature at inlet for first effect" ) def eq_delta_temperature_inlet_effect_1(b): return ( @@ -186,7 +181,7 @@ def eq_delta_temperature_inlet_effect_1(b): ) @effect.Constraint( - doc="Change in temperature at outlet for first effect." + doc="Change in temperature at outlet for first effect" ) def eq_delta_temperature_outlet_effect_1(b): return ( @@ -195,10 +190,17 @@ def eq_delta_temperature_outlet_effect_1(b): - b.properties_in[0].temperature ) - @effect.Constraint(doc="Heat transfer equation for first effect.") + @effect.Constraint(doc="Heat transfer equation for first effect") def eq_heat_transfer_effect_1(b): - return b.work_mechanical[0] == ( - b.overall_heat_transfer_coefficient + # return b.work_mechanical[0] == pyunits.convert( + # b.overall_heat_transfer_coefficient + # * b.heat_exchanger_area + # * b.delta_temperature[0] + # # , to_units=pyunits.kilowatt + # ) + return ( + b.work_mechanical[0] + == b.overall_heat_transfer_coefficient * b.heat_exchanger_area * b.delta_temperature[0] ) @@ -292,7 +294,7 @@ def initialize_build( solver=solver, optarg=optarg, ) - + init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit") @@ -347,6 +349,7 @@ def default_costing_method(self): unscaled_variables_generator, badly_scaled_var_generator, ) + from pyomo.util.check_units import assert_units_consistent solver = get_solver() @@ -408,9 +411,9 @@ def default_costing_method(self): eff.effect.pressure_operating.fix( pyunits.convert(op_pressure * pyunits.bar, to_units=pyunits.Pa) ) - eff.effect.overall_heat_transfer_coefficient.set_value(100) + eff.effect.overall_heat_transfer_coefficient.set_value(0.100) if n == 1: - eff.effect.overall_heat_transfer_coefficient.fix(100) + eff.effect.overall_heat_transfer_coefficient.fix(0.100) eff.effect.heating_steam[0].pressure_sat eff.effect.heating_steam[0].dh_vap_mass eff.effect.heating_steam.calculate_state( @@ -429,6 +432,7 @@ def default_costing_method(self): print(f"dof before init = {degrees_of_freedom(m)}") calculate_scaling_factors(m) + assert_units_consistent(m) try: mec.initialize() except: @@ -438,13 +442,14 @@ def default_costing_method(self): results = solver.solve(m) print(f"termination {results.solver.termination_condition}") assert_optimal_termination(results) - + print(mec.effects[mec.last_effect].effect.height_crystallizer.name) # for n, eff in mec.effects.items(): # print(f"\nEFFECT {n}\n") + # print(n, eff.effect.name) # eff.effect.overall_heat_transfer_coefficient.display() # eff.effect.properties_solids[0].flow_mass_phase_comp.display() # eff.effect.temperature_operating.display() - + # assert False m.fs.costing = TreatmentCosting() m.fs.mec.costing = UnitModelCostingBlock( flowsheet_costing_block=m.fs.costing, @@ -467,9 +472,20 @@ def default_costing_method(self): # eff1.heating_steam[0].pressure.display() # eff1.heating_steam[0].pressure_sat.display() - # for n, eff in mec.effects.items(): - # print(f"\nEFFECT {n}\n") - # eff.effect.properties_pure_water[0].temperature.display() + for n, eff in mec.effects.items(): + print(f"\nEFFECT {n}\n") + eff.effect.work_mechanical.display() + eff.effect.energy_flow_superheated_vapor.display() + # eff.effect.properties_pure_water[0].temperature.display() + # eff.effect.vapor_head_diam_expr.display() + # eff.effect.properties_out[0].dens_mass_phase.display() + # eff.effect.properties_vapor[0].dens_mass_solvent.display() + # eff.effect.eq_max_allowable_velocity.display() + # eff.effect.height_crystallizer.display() + # eff.effect.height_slurry.display() + # eff.effect.diameter_crystallizer.display() + # eff.effect.volume_suspension.display() + # eff.effect.height_crystallizer.display() # eff.effect.height_slurry.display() # eff.effect.volume_suspension.display() @@ -589,3 +605,84 @@ def default_costing_method(self): # <= 1.05 * prev_effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] # ) # effect.add_component(f"eq_equiv_brine_conc_effect_{n}_ub", brine_conc_constr) + + +# height_cryst_constr = Constraint( +# expr=effect.height_crystallizer == self.effects[self.last_effect].effect.height_crystallizer +# ) +# effect.add_component( +# f"eq_equiv_height_cryst_effect_{n}", height_cryst_constr +# ) +# effect.eq_crystallizer_height_constraint.deactivate() + +# diam_constr = Constraint( +# expr=effect.diameter_crystallizer +# == prev_effect.diameter_crystallizer +# ) +# effect.add_component(f"eq_equiv_diam_effect_{n}", diam_constr) +# effect.eq_vapor_head_diameter_constraint.deactivate() + +# height_slurry_constr = Constraint( +# expr=effect.height_slurry +# == 4 +# * prev_effect.volume_suspension +# / (Constants.pi * prev_effect.diameter_crystallizer) +# ) +# effect.add_component( +# f"eq_equiv_height_slurry_effect_{n}", height_slurry_constr +# ) +# effect.eq_slurry_height_constraint.deactivate() + +# vol_cryst_expr = Expression( +# expr=Constants.pi +# * (effect.diameter_crystallizer / 2) ** 2 +# * effect.height_crystallizer, +# doc=f"Actual volume of effect {n}", +# ) +# effect.add_component(f"total_volume_effect_{n}", vol_cryst_expr) + +# vapor_head_diam_expr = Expression( +# expr=( +# 4 +# * effect.properties_vapor[0].flow_vol_phase["Vap"] +# / (Constants.pi * effect.eq_max_allowable_velocity) +# ) +# ** 0.5 +# ) +# effect.add_component( +# f"vapor_head_diameter_effect_{n}", vapor_head_diam_expr +# ) + +# actual_vapor_vel_expr = Expression(expr=pyunits.convert( +# (effect.properties_vapor[0].flow_vol_phase["Vap"] * 4) +# / (effect.diameter_crystallizer**2 * Constants.pi), +# to_units=pyunits.m / pyunits.s, +# )) +# effect.add_component( +# f"actual_vapor_velocity_effect_{n}", actual_vapor_vel_expr +# ) + + +# @effect.Expression(doc="Total volume of effect 1") +# def total_volume_effect_1(b): +# return ( +# Constants.pi +# * (effect.diameter_crystallizer / 2) ** 2 +# * effect.height_crystallizer +# ) + +# @effect.Expression(doc="Total volume of effect 1.") +# def vapor_head_diameter_effect_1(b): +# return ( +# 4 +# * effect.properties_vapor[0].flow_vol_phase["Vap"] +# / (Constants.pi * effect.eq_max_allowable_velocity) +# ) ** 0.5 + +# @effect.Expression(doc="Actual vapor velocity") +# def actual_vapor_velocity_effect_1(b): +# return pyunits.convert( +# (b.properties_vapor[0].flow_vol_phase["Vap"] * 4) +# / (b.diameter_crystallizer**2 * Constants.pi), +# to_units=pyunits.m / pyunits.s, +# ) From bdc74cc39be802b4b56f781f1218c1a29f6554f3 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Fri, 20 Sep 2024 14:01:17 -0600 Subject: [PATCH 39/80] move test file out of ZO folder --- .../{zero_order => }/tests/test_multi_effect_crystallizer.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/watertap_contrib/reflo/unit_models/{zero_order => }/tests/test_multi_effect_crystallizer.py (100%) diff --git a/src/watertap_contrib/reflo/unit_models/zero_order/tests/test_multi_effect_crystallizer.py b/src/watertap_contrib/reflo/unit_models/tests/test_multi_effect_crystallizer.py similarity index 100% rename from src/watertap_contrib/reflo/unit_models/zero_order/tests/test_multi_effect_crystallizer.py rename to src/watertap_contrib/reflo/unit_models/tests/test_multi_effect_crystallizer.py From 828fea8f700c390fa6e2dec2f7362c458c44088a Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Fri, 20 Sep 2024 14:02:11 -0600 Subject: [PATCH 40/80] comment test file before removal --- .../tests/test_crystallizer_watertap.py | 1234 ++++++++--------- 1 file changed, 617 insertions(+), 617 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/zero_order/tests/test_crystallizer_watertap.py b/src/watertap_contrib/reflo/unit_models/zero_order/tests/test_crystallizer_watertap.py index aa714dda..a5e553d0 100644 --- a/src/watertap_contrib/reflo/unit_models/zero_order/tests/test_crystallizer_watertap.py +++ b/src/watertap_contrib/reflo/unit_models/zero_order/tests/test_crystallizer_watertap.py @@ -1,617 +1,617 @@ -################################################################################# -# WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, -# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, -# National Renewable Energy Laboratory, and National Energy Technology -# Laboratory (subject to receipt of any required approvals from the U.S. Dept. -# of Energy). All rights reserved. -# -# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license -# information, respectively. These files are also available online at the URL -# "https://github.com/watertap-org/watertap/" -################################################################################# - -import pytest -from pyomo.environ import ( - ConcreteModel, - TerminationCondition, - SolverStatus, - value, - Var, -) -from pyomo.network import Port -from idaes.core import FlowsheetBlock -from pyomo.util.check_units import assert_units_consistent -from watertap_contrib.reflo.unit_models.zero_order.crystallizer_zo_watertap import ( - Crystallization, -) -import watertap_contrib.reflo.property_models.cryst_prop_pack as props - -from watertap.core.solvers import get_solver - -from idaes.core.util.model_statistics import ( - degrees_of_freedom, - number_variables, - number_total_constraints, - number_unused_variables, -) -from idaes.core.util.testing import initialization_tester -from idaes.core.util.scaling import ( - calculate_scaling_factors, - unscaled_variables_generator, - badly_scaled_var_generator, -) -from idaes.core import UnitModelCostingBlock -from watertap_contrib.reflo.costing import TreatmentCosting, CrystallizerCostType - -# ----------------------------------------------------------------------------- -# Get default solver for testing -solver = get_solver() - - -class TestCrystallization: - @pytest.fixture(scope="class") - def Crystallizer_frame(self): - m = ConcreteModel() - m.fs = FlowsheetBlock(dynamic=False) - - m.fs.properties = props.NaClParameterBlock() - - m.fs.unit = Crystallization(property_package=m.fs.properties) - - # fully specify system - feed_flow_mass = 1 - feed_mass_frac_NaCl = 0.2126 - feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl - feed_pressure = 101325 - feed_temperature = 273.15 + 20 - eps = 1e-6 - crystallizer_temperature = 273.15 + 55 - crystallizer_yield = 0.40 - - # Fully define feed - m.fs.unit.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"].fix( - feed_flow_mass * feed_mass_frac_NaCl - ) - m.fs.unit.inlet.flow_mass_phase_comp[0, "Liq", "H2O"].fix( - feed_flow_mass * feed_mass_frac_H2O - ) - m.fs.unit.inlet.flow_mass_phase_comp[0, "Sol", "NaCl"].fix(eps) - m.fs.unit.inlet.flow_mass_phase_comp[0, "Vap", "H2O"].fix(eps) - m.fs.unit.inlet.pressure[0].fix(feed_pressure) - m.fs.unit.inlet.temperature[0].fix(feed_temperature) - - # Define operating conditions - m.fs.unit.temperature_operating.fix(crystallizer_temperature) - m.fs.unit.crystallization_yield["NaCl"].fix(crystallizer_yield) - - # Fix growth rate, crystal length and Sounders brown constant to default values - m.fs.unit.crystal_growth_rate.fix() - m.fs.unit.souders_brown_constant.fix() - m.fs.unit.crystal_median_length.fix() - - assert_units_consistent(m) - - return m - - @pytest.fixture(scope="class") - def Crystallizer_frame_2(self): - m = ConcreteModel() - m.fs = FlowsheetBlock(dynamic=False) - - m.fs.properties = props.NaClParameterBlock() - - m.fs.unit = Crystallization(property_package=m.fs.properties) - - # fully specify system - feed_flow_mass = 1 - feed_mass_frac_NaCl = 0.2126 - feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl - feed_pressure = 101325 - feed_temperature = 273.15 + 20 - eps = 1e-6 - crystallizer_temperature = 273.15 + 55 - crystallizer_yield = 0.40 - - # Fully define feed - m.fs.unit.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"].fix( - feed_flow_mass * feed_mass_frac_NaCl - ) - m.fs.unit.inlet.flow_mass_phase_comp[0, "Liq", "H2O"].fix( - feed_flow_mass * feed_mass_frac_H2O - ) - m.fs.unit.inlet.flow_mass_phase_comp[0, "Sol", "NaCl"].fix(eps) - m.fs.unit.inlet.flow_mass_phase_comp[0, "Vap", "H2O"].fix(eps) - m.fs.unit.inlet.pressure[0].fix(feed_pressure) - m.fs.unit.inlet.temperature[0].fix(feed_temperature) - - # Define operating conditions - m.fs.unit.temperature_operating.fix(crystallizer_temperature) - m.fs.unit.crystallization_yield["NaCl"].fix(crystallizer_yield) - - # Fix growth rate, crystal length and Sounders brown constant to default values - m.fs.unit.crystal_growth_rate.fix() - m.fs.unit.souders_brown_constant.fix() - m.fs.unit.crystal_median_length.fix() - - assert_units_consistent(m) - - return m - - @pytest.mark.unit - def test_config(self, Crystallizer_frame): - m = Crystallizer_frame - # check unit config arguments - assert len(m.fs.unit.config) == 4 - - assert not m.fs.unit.config.dynamic - assert not m.fs.unit.config.has_holdup - assert m.fs.unit.config.property_package is m.fs.properties - - @pytest.mark.unit - def test_build(self, Crystallizer_frame): - m = Crystallizer_frame - - # test ports and variables - port_lst = ["inlet", "outlet", "solids", "vapor"] - port_vars_lst = ["flow_mass_phase_comp", "pressure", "temperature"] - for port_str in port_lst: - assert hasattr(m.fs.unit, port_str) - port = getattr(m.fs.unit, port_str) - assert len(port.vars) == 3 - assert isinstance(port, Port) - for var_str in port_vars_lst: - assert hasattr(port, var_str) - var = getattr(port, var_str) - assert isinstance(var, Var) - - # test unit objects (including parameters, variables, and constraints) - # First, parameters - unit_objs_params_lst = [ - "approach_temperature_heat_exchanger", - "dimensionless_crystal_length", - ] - for obj_str in unit_objs_params_lst: - assert hasattr(m.fs.unit, obj_str) - # Next, variables - unit_objs_vars_lst = [ - "crystal_growth_rate", - "crystal_median_length", - "crystallization_yield", - "dens_mass_magma", - "dens_mass_slurry", - "diameter_crystallizer", - "height_crystallizer", - "height_slurry", - "magma_circulation_flow_vol", - "pressure_operating", - "product_volumetric_solids_fraction", - "relative_supersaturation", - "souders_brown_constant", - "t_res", - "temperature_operating", - "volume_suspension", - "work_mechanical", - ] - for obj_str in unit_objs_vars_lst: - assert hasattr(m.fs.unit, obj_str) - # Next, expressions - unit_objs_expr_lst = [ - "eq_max_allowable_velocity", - "eq_minimum_height_diameter_ratio", - "eq_vapor_space_height", - ] - for obj_str in unit_objs_expr_lst: - assert hasattr(m.fs.unit, obj_str) - # Finally, constraints - unit_objs_cons_lst = [ - "eq_T_con1", - "eq_T_con2", - "eq_T_con3", - "eq_crystallizer_height_constraint", - "eq_dens_magma", - "eq_dens_mass_slurry", - "eq_enthalpy_balance", - "eq_mass_balance_constraints", - "eq_minimum_hex_circulation_rate_constraint", - "eq_operating_pressure_constraint", - "eq_p_con1", - "eq_p_con2", - "eq_p_con3", - "eq_relative_supersaturation", - "eq_removal_balance", - "eq_residence_time", - "eq_slurry_height_constraint", - "eq_solubility_massfrac_equality_constraint", - "eq_suspension_volume", - "eq_vapor_head_diameter_constraint", - "eq_vol_fraction_solids", - ] - for obj_str in unit_objs_cons_lst: - assert hasattr(m.fs.unit, obj_str) - - # Test stateblocks - # List olf attributes on all stateblocks - stateblock_objs_lst = [ - "flow_mass_phase_comp", - "pressure", - "temperature", - "solubility_mass_phase_comp", - "solubility_mass_frac_phase_comp", - "mass_frac_phase_comp", - "dens_mass_solvent", - "dens_mass_solute", - "dens_mass_phase", - "cp_mass_phase", - "cp_mass_solvent", - "flow_vol_phase", - "flow_vol", - "enth_flow", - "enth_mass_solvent", - "dh_crystallization_mass_comp", - "eq_solubility_mass_phase_comp", - "eq_solubility_mass_frac_phase_comp", - "eq_mass_frac_phase_comp", - "eq_dens_mass_solvent", - "eq_dens_mass_solute", - "eq_dens_mass_phase", - "eq_cp_mass_solute", - "eq_cp_mass_phase", - "eq_flow_vol_phase", - "eq_enth_mass_solvent", - ] - # List of attributes for liquid stateblocks only - stateblock_objs_liq_lst = ["pressure_sat", "eq_pressure_sat"] - # Inlet block - assert hasattr(m.fs.unit, "properties_in") - blk = getattr(m.fs.unit, "properties_in") - for var_str in stateblock_objs_lst: - assert hasattr(blk[0], var_str) - for var_str in stateblock_objs_liq_lst: - assert hasattr(blk[0], var_str) - - # Liquid outlet block - assert hasattr(m.fs.unit, "properties_out") - blk = getattr(m.fs.unit, "properties_out") - for var_str in stateblock_objs_lst: - assert hasattr(blk[0], var_str) - for var_str in stateblock_objs_liq_lst: - assert hasattr(blk[0], var_str) - - # Vapor outlet block - assert hasattr(m.fs.unit, "properties_vapor") - blk = getattr(m.fs.unit, "properties_vapor") - for var_str in stateblock_objs_lst: - assert hasattr(blk[0], var_str) - - # Liquid outlet block - assert hasattr(m.fs.unit, "properties_solids") - blk = getattr(m.fs.unit, "properties_solids") - for var_str in stateblock_objs_lst: - assert hasattr(blk[0], var_str) - - # test statistics - assert number_variables(m) == 255 - assert number_total_constraints(m) == 138 - assert number_unused_variables(m) == 5 - - @pytest.mark.unit - def test_dof(self, Crystallizer_frame): - m = Crystallizer_frame - assert degrees_of_freedom(m) == 0 - - @pytest.mark.unit - def test_calculate_scaling(self, Crystallizer_frame): - m = Crystallizer_frame - - m.fs.properties.set_default_scaling( - "flow_mass_phase_comp", 1e-1, index=("Liq", "H2O") - ) - m.fs.properties.set_default_scaling( - "flow_mass_phase_comp", 1e-1, index=("Liq", "NaCl") - ) - m.fs.properties.set_default_scaling( - "flow_mass_phase_comp", 1e-1, index=("Vap", "H2O") - ) - m.fs.properties.set_default_scaling( - "flow_mass_phase_comp", 1e-1, index=("Sol", "NaCl") - ) - calculate_scaling_factors(m) - - # check that all variables have scaling factors - unscaled_var_list = list(unscaled_variables_generator(m)) - assert len(unscaled_var_list) == 0 - - for _ in badly_scaled_var_generator(m): - assert False - - @pytest.mark.component - def test_initialize(self, Crystallizer_frame): - # Add costing function, then initialize - m = Crystallizer_frame - m.fs.costing = TreatmentCosting() - m.fs.unit.costing = UnitModelCostingBlock( - flowsheet_costing_block=m.fs.costing, - costing_method_arguments={"cost_type": CrystallizerCostType.mass_basis}, - ) - m.fs.costing.cost_process() - - initialization_tester(Crystallizer_frame) - assert_units_consistent(m) - - # @pytest.mark.component - # def test_var_scaling(self, Crystallizer_frame): - # m = Crystallizer_frame - # badly_scaled_var_lst = list(badly_scaled_var_generator(m)) - # assert badly_scaled_var_lst == [] - - @pytest.mark.component - def test_solve(self, Crystallizer_frame): - m = Crystallizer_frame - results = solver.solve(m) - - # Check for optimal solution - assert results.solver.termination_condition == TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok - - @pytest.mark.component - def test_conservation(self, Crystallizer_frame): - m = Crystallizer_frame - b = m.fs.unit - comp_lst = ["NaCl", "H2O"] - phase_lst = ["Sol", "Liq", "Vap"] - phase_comp_list = [ - (p, j) - for j in comp_lst - for p in phase_lst - if (p, j) in b.properties_in[0].phase_component_set - ] - - flow_mass_in = sum( - b.properties_in[0].flow_mass_phase_comp[p, j] - for p in phase_lst - for j in comp_lst - if (p, j) in phase_comp_list - ) - flow_mass_out = sum( - b.properties_out[0].flow_mass_phase_comp[p, j] - for p in phase_lst - for j in comp_lst - if (p, j) in phase_comp_list - ) - flow_mass_solids = sum( - b.properties_solids[0].flow_mass_phase_comp[p, j] - for p in phase_lst - for j in comp_lst - if (p, j) in phase_comp_list - ) - flow_mass_vapor = sum( - b.properties_vapor[0].flow_mass_phase_comp[p, j] - for p in phase_lst - for j in comp_lst - if (p, j) in phase_comp_list - ) - - assert ( - abs( - value(flow_mass_in - flow_mass_out - flow_mass_solids - flow_mass_vapor) - ) - <= 1e-6 - ) - - assert ( - abs( - value( - flow_mass_in * b.properties_in[0].enth_mass_phase["Liq"] - - flow_mass_out * b.properties_out[0].enth_mass_phase["Liq"] - - flow_mass_vapor * b.properties_vapor[0].enth_mass_solvent["Vap"] - - flow_mass_solids * b.properties_solids[0].enth_mass_solute["Sol"] - - flow_mass_solids - * b.properties_solids[0].dh_crystallization_mass_comp["NaCl"] - + b.work_mechanical[0] - ) - ) - <= 1e-2 - ) - - @pytest.mark.component - def test_solution(self, Crystallizer_frame): - m = Crystallizer_frame - b = m.fs.unit - # Check solid mass in solids stream - assert pytest.approx( - value( - b.crystallization_yield["NaCl"] - * b.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"] - ), - rel=1e-3, - ) == value(b.solids.flow_mass_phase_comp[0, "Sol", "NaCl"]) - # Check solid mass in liquid stream - assert pytest.approx( - value( - (1 - b.crystallization_yield["NaCl"]) - * b.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"] - ), - rel=1e-3, - ) == value(b.outlet.flow_mass_phase_comp[0, "Liq", "NaCl"]) - # Check outlet liquid stream composition which is set by solubility - assert pytest.approx(0.2695, rel=1e-3) == value( - b.properties_out[0].mass_frac_phase_comp["Liq", "NaCl"] - ) - # Check liquid stream solvent flow - assert pytest.approx(0.12756 * ((1 / 0.2695) - 1), rel=1e-3) == value( - b.outlet.flow_mass_phase_comp[0, "Liq", "H2O"] - ) - # Check saturation pressure - assert pytest.approx(11992, rel=1e-3) == value(b.pressure_operating) - # Check heat requirement - assert pytest.approx(1127.2, rel=1e-3) == value(b.work_mechanical[0]) - # Check crystallizer diameter - assert pytest.approx(1.205, rel=1e-3) == value(b.diameter_crystallizer) - # Minimum active volume - assert pytest.approx(1.619, rel=1e-3) == value(b.volume_suspension) - # Residence time - assert pytest.approx(1.0228, rel=1e-3) == value(b.t_res) - # Mass-basis costing - assert pytest.approx(704557, rel=1e-3) == value( - m.fs.costing.aggregate_capital_cost - ) - - @pytest.mark.component - def test_solution2_capcosting_by_mass(self, Crystallizer_frame): - m = Crystallizer_frame - b = m.fs.unit - b.crystal_growth_rate.fix(5e-8) - b.souders_brown_constant.fix(0.0244) - b.crystal_median_length.fix(0.4e-3) - results = solver.solve(m) - - # Test that report function works - b.report() - - # Check for optimal solution - assert results.solver.termination_condition == TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok - - # Residence time - assert pytest.approx( - value(b.crystal_median_length / (3.67 * 3600 * b.crystal_growth_rate)), - rel=1e-3, - ) == value(b.t_res) - # Check crystallizer diameter - assert pytest.approx(1.5427, rel=1e-3) == value(b.diameter_crystallizer) - # Minimum active volume - assert pytest.approx(0.959, rel=1e-3) == value(b.volume_suspension) - # Mass-basis costing - assert pytest.approx(704557, rel=1e-3) == value( - m.fs.costing.aggregate_capital_cost - ) - - @pytest.mark.component - def test_solution2_capcosting_by_volume(self, Crystallizer_frame_2): - # Same problem as above, but different costing approach. - # Other results should remain the same. - m = Crystallizer_frame_2 - b = m.fs.unit - b.crystal_growth_rate.fix(5e-8) - b.souders_brown_constant.fix(0.0244) - b.crystal_median_length.fix(0.4e-3) - - assert degrees_of_freedom(m) == 0 - - m.fs.properties.set_default_scaling( - "flow_mass_phase_comp", 1e-1, index=("Liq", "H2O") - ) - m.fs.properties.set_default_scaling( - "flow_mass_phase_comp", 1e-1, index=("Liq", "NaCl") - ) - m.fs.properties.set_default_scaling( - "flow_mass_phase_comp", 1e-1, index=("Vap", "H2O") - ) - m.fs.properties.set_default_scaling( - "flow_mass_phase_comp", 1e-1, index=("Sol", "NaCl") - ) - calculate_scaling_factors(m) - initialization_tester(Crystallizer_frame_2) - results = solver.solve(m) - - m.fs.costing = TreatmentCosting() - m.fs.unit.costing = UnitModelCostingBlock( - flowsheet_costing_block=m.fs.costing, - costing_method_arguments={"cost_type": CrystallizerCostType.volume_basis}, - ) - m.fs.costing.cost_process() - assert_units_consistent(m) - results = solver.solve(m) - - # Test that report function works - b.report() - - # Check for optimal solution - assert results.solver.termination_condition == TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok - - # Residence time - assert pytest.approx( - value(b.crystal_median_length / (3.67 * 3600 * b.crystal_growth_rate)), - rel=1e-3, - ) == value(b.t_res) - # Check crystallizer diameter - assert pytest.approx(1.5427, rel=1e-3) == value(b.diameter_crystallizer) - # Minimum active volume - assert pytest.approx(0.959, rel=1e-3) == value(b.volume_suspension) - # Volume-basis costing - assert pytest.approx(467490, rel=1e-3) == value( - m.fs.costing.aggregate_capital_cost - ) - - @pytest.mark.component - def test_solution2_operatingcost(self, Crystallizer_frame_2): - m = Crystallizer_frame_2 - b = m.fs.unit - b.crystal_growth_rate.fix(5e-8) - b.souders_brown_constant.fix(0.0244) - b.crystal_median_length.fix(0.4e-3) - results = solver.solve(m) - - # Check for optimal solution - assert results.solver.termination_condition == TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok - - # Operating cost validation - assert pytest.approx(980.77, rel=1e-3) == value( - m.fs.costing.aggregate_flow_costs["electricity"] - ) - assert pytest.approx(36000.75, rel=1e-3) == value( - m.fs.costing.aggregate_flow_costs["steam"] - ) - assert pytest.approx(0, rel=1e-3) == value( - m.fs.costing.aggregate_flow_costs["NaCl"] - ) - - @pytest.mark.component - def test_solution2_operatingcost_steampressure(self, Crystallizer_frame_2): - m = Crystallizer_frame_2 - m.fs.costing.crystallizer.steam_pressure.fix(5) - b = m.fs.unit - b.crystal_growth_rate.fix(5e-8) - b.souders_brown_constant.fix(0.0244) - b.crystal_median_length.fix(0.4e-3) - results = solver.solve(m) - - # Check for optimal solution - assert results.solver.termination_condition == TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok - - # Operating cost validation - assert pytest.approx(980.77, rel=1e-3) == value( - m.fs.costing.aggregate_flow_costs["electricity"] - ) - assert pytest.approx(25183.20, rel=1e-3) == value( - m.fs.costing.aggregate_flow_costs["steam"] - ) - assert pytest.approx(0, abs=1e-6) == value( - m.fs.costing.aggregate_flow_costs["NaCl"] - ) - - @pytest.mark.component - def test_solution2_operatingcost_NaCl_revenue(self, Crystallizer_frame_2): - m = Crystallizer_frame_2 - m.fs.costing.crystallizer.steam_pressure.fix(3) - m.fs.costing.crystallizer.NaCl_recovery_value.fix(-0.07) - - results = solver.solve(m) - - # Check for optimal solution - assert results.solver.termination_condition == TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok - - # Operating cost validation - assert pytest.approx(980.77, rel=1e-3) == value( - m.fs.costing.aggregate_flow_costs["electricity"] - ) - assert pytest.approx(36000.75, rel=1e-3) == value( - m.fs.costing.aggregate_flow_costs["steam"] - ) - assert pytest.approx(-220533.25, rel=1e-3) == value( - m.fs.costing.aggregate_flow_costs["NaCl"] - ) +# ################################################################################# +# # WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, +# # through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# # National Renewable Energy Laboratory, and National Energy Technology +# # Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# # of Energy). All rights reserved. +# # +# # Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# # information, respectively. These files are also available online at the URL +# # "https://github.com/watertap-org/watertap/" +# ################################################################################# + +# import pytest +# from pyomo.environ import ( +# ConcreteModel, +# TerminationCondition, +# SolverStatus, +# value, +# Var, +# ) +# from pyomo.network import Port +# from idaes.core import FlowsheetBlock +# from pyomo.util.check_units import assert_units_consistent +# from watertap_contrib.reflo.unit_models.zero_order.crystallizer_zo_watertap import ( +# Crystallization, +# ) +# import watertap_contrib.reflo.property_models.cryst_prop_pack as props + +# from watertap.core.solvers import get_solver + +# from idaes.core.util.model_statistics import ( +# degrees_of_freedom, +# number_variables, +# number_total_constraints, +# number_unused_variables, +# ) +# from idaes.core.util.testing import initialization_tester +# from idaes.core.util.scaling import ( +# calculate_scaling_factors, +# unscaled_variables_generator, +# badly_scaled_var_generator, +# ) +# from idaes.core import UnitModelCostingBlock +# from watertap_contrib.reflo.costing import TreatmentCosting, CrystallizerCostType + +# # ----------------------------------------------------------------------------- +# # Get default solver for testing +# solver = get_solver() + + +# class TestCrystallization: +# @pytest.fixture(scope="class") +# def Crystallizer_frame(self): +# m = ConcreteModel() +# m.fs = FlowsheetBlock(dynamic=False) + +# m.fs.properties = props.NaClParameterBlock() + +# m.fs.unit = Crystallization(property_package=m.fs.properties) + +# # fully specify system +# feed_flow_mass = 1 +# feed_mass_frac_NaCl = 0.2126 +# feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl +# feed_pressure = 101325 +# feed_temperature = 273.15 + 20 +# eps = 1e-6 +# crystallizer_temperature = 273.15 + 55 +# crystallizer_yield = 0.40 + +# # Fully define feed +# m.fs.unit.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"].fix( +# feed_flow_mass * feed_mass_frac_NaCl +# ) +# m.fs.unit.inlet.flow_mass_phase_comp[0, "Liq", "H2O"].fix( +# feed_flow_mass * feed_mass_frac_H2O +# ) +# m.fs.unit.inlet.flow_mass_phase_comp[0, "Sol", "NaCl"].fix(eps) +# m.fs.unit.inlet.flow_mass_phase_comp[0, "Vap", "H2O"].fix(eps) +# m.fs.unit.inlet.pressure[0].fix(feed_pressure) +# m.fs.unit.inlet.temperature[0].fix(feed_temperature) + +# # Define operating conditions +# m.fs.unit.temperature_operating.fix(crystallizer_temperature) +# m.fs.unit.crystallization_yield["NaCl"].fix(crystallizer_yield) + +# # Fix growth rate, crystal length and Sounders brown constant to default values +# m.fs.unit.crystal_growth_rate.fix() +# m.fs.unit.souders_brown_constant.fix() +# m.fs.unit.crystal_median_length.fix() + +# assert_units_consistent(m) + +# return m + +# @pytest.fixture(scope="class") +# def Crystallizer_frame_2(self): +# m = ConcreteModel() +# m.fs = FlowsheetBlock(dynamic=False) + +# m.fs.properties = props.NaClParameterBlock() + +# m.fs.unit = Crystallization(property_package=m.fs.properties) + +# # fully specify system +# feed_flow_mass = 1 +# feed_mass_frac_NaCl = 0.2126 +# feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl +# feed_pressure = 101325 +# feed_temperature = 273.15 + 20 +# eps = 1e-6 +# crystallizer_temperature = 273.15 + 55 +# crystallizer_yield = 0.40 + +# # Fully define feed +# m.fs.unit.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"].fix( +# feed_flow_mass * feed_mass_frac_NaCl +# ) +# m.fs.unit.inlet.flow_mass_phase_comp[0, "Liq", "H2O"].fix( +# feed_flow_mass * feed_mass_frac_H2O +# ) +# m.fs.unit.inlet.flow_mass_phase_comp[0, "Sol", "NaCl"].fix(eps) +# m.fs.unit.inlet.flow_mass_phase_comp[0, "Vap", "H2O"].fix(eps) +# m.fs.unit.inlet.pressure[0].fix(feed_pressure) +# m.fs.unit.inlet.temperature[0].fix(feed_temperature) + +# # Define operating conditions +# m.fs.unit.temperature_operating.fix(crystallizer_temperature) +# m.fs.unit.crystallization_yield["NaCl"].fix(crystallizer_yield) + +# # Fix growth rate, crystal length and Sounders brown constant to default values +# m.fs.unit.crystal_growth_rate.fix() +# m.fs.unit.souders_brown_constant.fix() +# m.fs.unit.crystal_median_length.fix() + +# assert_units_consistent(m) + +# return m + +# @pytest.mark.unit +# def test_config(self, Crystallizer_frame): +# m = Crystallizer_frame +# # check unit config arguments +# assert len(m.fs.unit.config) == 4 + +# assert not m.fs.unit.config.dynamic +# assert not m.fs.unit.config.has_holdup +# assert m.fs.unit.config.property_package is m.fs.properties + +# @pytest.mark.unit +# def test_build(self, Crystallizer_frame): +# m = Crystallizer_frame + +# # test ports and variables +# port_lst = ["inlet", "outlet", "solids", "vapor"] +# port_vars_lst = ["flow_mass_phase_comp", "pressure", "temperature"] +# for port_str in port_lst: +# assert hasattr(m.fs.unit, port_str) +# port = getattr(m.fs.unit, port_str) +# assert len(port.vars) == 3 +# assert isinstance(port, Port) +# for var_str in port_vars_lst: +# assert hasattr(port, var_str) +# var = getattr(port, var_str) +# assert isinstance(var, Var) + +# # test unit objects (including parameters, variables, and constraints) +# # First, parameters +# unit_objs_params_lst = [ +# "approach_temperature_heat_exchanger", +# "dimensionless_crystal_length", +# ] +# for obj_str in unit_objs_params_lst: +# assert hasattr(m.fs.unit, obj_str) +# # Next, variables +# unit_objs_vars_lst = [ +# "crystal_growth_rate", +# "crystal_median_length", +# "crystallization_yield", +# "dens_mass_magma", +# "dens_mass_slurry", +# "diameter_crystallizer", +# "height_crystallizer", +# "height_slurry", +# "magma_circulation_flow_vol", +# "pressure_operating", +# "product_volumetric_solids_fraction", +# "relative_supersaturation", +# "souders_brown_constant", +# "t_res", +# "temperature_operating", +# "volume_suspension", +# "work_mechanical", +# ] +# for obj_str in unit_objs_vars_lst: +# assert hasattr(m.fs.unit, obj_str) +# # Next, expressions +# unit_objs_expr_lst = [ +# "eq_max_allowable_velocity", +# "eq_minimum_height_diameter_ratio", +# "eq_vapor_space_height", +# ] +# for obj_str in unit_objs_expr_lst: +# assert hasattr(m.fs.unit, obj_str) +# # Finally, constraints +# unit_objs_cons_lst = [ +# "eq_T_con1", +# "eq_T_con2", +# "eq_T_con3", +# "eq_crystallizer_height_constraint", +# "eq_dens_magma", +# "eq_dens_mass_slurry", +# "eq_enthalpy_balance", +# "eq_mass_balance_constraints", +# "eq_minimum_hex_circulation_rate_constraint", +# "eq_operating_pressure_constraint", +# "eq_p_con1", +# "eq_p_con2", +# "eq_p_con3", +# "eq_relative_supersaturation", +# "eq_removal_balance", +# "eq_residence_time", +# "eq_slurry_height_constraint", +# "eq_solubility_massfrac_equality_constraint", +# "eq_suspension_volume", +# "eq_vapor_head_diameter_constraint", +# "eq_vol_fraction_solids", +# ] +# for obj_str in unit_objs_cons_lst: +# assert hasattr(m.fs.unit, obj_str) + +# # Test stateblocks +# # List olf attributes on all stateblocks +# stateblock_objs_lst = [ +# "flow_mass_phase_comp", +# "pressure", +# "temperature", +# "solubility_mass_phase_comp", +# "solubility_mass_frac_phase_comp", +# "mass_frac_phase_comp", +# "dens_mass_solvent", +# "dens_mass_solute", +# "dens_mass_phase", +# "cp_mass_phase", +# "cp_mass_solvent", +# "flow_vol_phase", +# "flow_vol", +# "enth_flow", +# "enth_mass_solvent", +# "dh_crystallization_mass_comp", +# "eq_solubility_mass_phase_comp", +# "eq_solubility_mass_frac_phase_comp", +# "eq_mass_frac_phase_comp", +# "eq_dens_mass_solvent", +# "eq_dens_mass_solute", +# "eq_dens_mass_phase", +# "eq_cp_mass_solute", +# "eq_cp_mass_phase", +# "eq_flow_vol_phase", +# "eq_enth_mass_solvent", +# ] +# # List of attributes for liquid stateblocks only +# stateblock_objs_liq_lst = ["pressure_sat", "eq_pressure_sat"] +# # Inlet block +# assert hasattr(m.fs.unit, "properties_in") +# blk = getattr(m.fs.unit, "properties_in") +# for var_str in stateblock_objs_lst: +# assert hasattr(blk[0], var_str) +# for var_str in stateblock_objs_liq_lst: +# assert hasattr(blk[0], var_str) + +# # Liquid outlet block +# assert hasattr(m.fs.unit, "properties_out") +# blk = getattr(m.fs.unit, "properties_out") +# for var_str in stateblock_objs_lst: +# assert hasattr(blk[0], var_str) +# for var_str in stateblock_objs_liq_lst: +# assert hasattr(blk[0], var_str) + +# # Vapor outlet block +# assert hasattr(m.fs.unit, "properties_vapor") +# blk = getattr(m.fs.unit, "properties_vapor") +# for var_str in stateblock_objs_lst: +# assert hasattr(blk[0], var_str) + +# # Liquid outlet block +# assert hasattr(m.fs.unit, "properties_solids") +# blk = getattr(m.fs.unit, "properties_solids") +# for var_str in stateblock_objs_lst: +# assert hasattr(blk[0], var_str) + +# # test statistics +# assert number_variables(m) == 255 +# assert number_total_constraints(m) == 138 +# assert number_unused_variables(m) == 5 + +# @pytest.mark.unit +# def test_dof(self, Crystallizer_frame): +# m = Crystallizer_frame +# assert degrees_of_freedom(m) == 0 + +# @pytest.mark.unit +# def test_calculate_scaling(self, Crystallizer_frame): +# m = Crystallizer_frame + +# m.fs.properties.set_default_scaling( +# "flow_mass_phase_comp", 1e-1, index=("Liq", "H2O") +# ) +# m.fs.properties.set_default_scaling( +# "flow_mass_phase_comp", 1e-1, index=("Liq", "NaCl") +# ) +# m.fs.properties.set_default_scaling( +# "flow_mass_phase_comp", 1e-1, index=("Vap", "H2O") +# ) +# m.fs.properties.set_default_scaling( +# "flow_mass_phase_comp", 1e-1, index=("Sol", "NaCl") +# ) +# calculate_scaling_factors(m) + +# # check that all variables have scaling factors +# unscaled_var_list = list(unscaled_variables_generator(m)) +# assert len(unscaled_var_list) == 0 + +# for _ in badly_scaled_var_generator(m): +# assert False + +# @pytest.mark.component +# def test_initialize(self, Crystallizer_frame): +# # Add costing function, then initialize +# m = Crystallizer_frame +# m.fs.costing = TreatmentCosting() +# m.fs.unit.costing = UnitModelCostingBlock( +# flowsheet_costing_block=m.fs.costing, +# costing_method_arguments={"cost_type": CrystallizerCostType.mass_basis}, +# ) +# m.fs.costing.cost_process() + +# initialization_tester(Crystallizer_frame) +# assert_units_consistent(m) + +# # @pytest.mark.component +# # def test_var_scaling(self, Crystallizer_frame): +# # m = Crystallizer_frame +# # badly_scaled_var_lst = list(badly_scaled_var_generator(m)) +# # assert badly_scaled_var_lst == [] + +# @pytest.mark.component +# def test_solve(self, Crystallizer_frame): +# m = Crystallizer_frame +# results = solver.solve(m) + +# # Check for optimal solution +# assert results.solver.termination_condition == TerminationCondition.optimal +# assert results.solver.status == SolverStatus.ok + +# @pytest.mark.component +# def test_conservation(self, Crystallizer_frame): +# m = Crystallizer_frame +# b = m.fs.unit +# comp_lst = ["NaCl", "H2O"] +# phase_lst = ["Sol", "Liq", "Vap"] +# phase_comp_list = [ +# (p, j) +# for j in comp_lst +# for p in phase_lst +# if (p, j) in b.properties_in[0].phase_component_set +# ] + +# flow_mass_in = sum( +# b.properties_in[0].flow_mass_phase_comp[p, j] +# for p in phase_lst +# for j in comp_lst +# if (p, j) in phase_comp_list +# ) +# flow_mass_out = sum( +# b.properties_out[0].flow_mass_phase_comp[p, j] +# for p in phase_lst +# for j in comp_lst +# if (p, j) in phase_comp_list +# ) +# flow_mass_solids = sum( +# b.properties_solids[0].flow_mass_phase_comp[p, j] +# for p in phase_lst +# for j in comp_lst +# if (p, j) in phase_comp_list +# ) +# flow_mass_vapor = sum( +# b.properties_vapor[0].flow_mass_phase_comp[p, j] +# for p in phase_lst +# for j in comp_lst +# if (p, j) in phase_comp_list +# ) + +# assert ( +# abs( +# value(flow_mass_in - flow_mass_out - flow_mass_solids - flow_mass_vapor) +# ) +# <= 1e-6 +# ) + +# assert ( +# abs( +# value( +# flow_mass_in * b.properties_in[0].enth_mass_phase["Liq"] +# - flow_mass_out * b.properties_out[0].enth_mass_phase["Liq"] +# - flow_mass_vapor * b.properties_vapor[0].enth_mass_solvent["Vap"] +# - flow_mass_solids * b.properties_solids[0].enth_mass_solute["Sol"] +# - flow_mass_solids +# * b.properties_solids[0].dh_crystallization_mass_comp["NaCl"] +# + b.work_mechanical[0] +# ) +# ) +# <= 1e-2 +# ) + +# @pytest.mark.component +# def test_solution(self, Crystallizer_frame): +# m = Crystallizer_frame +# b = m.fs.unit +# # Check solid mass in solids stream +# assert pytest.approx( +# value( +# b.crystallization_yield["NaCl"] +# * b.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"] +# ), +# rel=1e-3, +# ) == value(b.solids.flow_mass_phase_comp[0, "Sol", "NaCl"]) +# # Check solid mass in liquid stream +# assert pytest.approx( +# value( +# (1 - b.crystallization_yield["NaCl"]) +# * b.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"] +# ), +# rel=1e-3, +# ) == value(b.outlet.flow_mass_phase_comp[0, "Liq", "NaCl"]) +# # Check outlet liquid stream composition which is set by solubility +# assert pytest.approx(0.2695, rel=1e-3) == value( +# b.properties_out[0].mass_frac_phase_comp["Liq", "NaCl"] +# ) +# # Check liquid stream solvent flow +# assert pytest.approx(0.12756 * ((1 / 0.2695) - 1), rel=1e-3) == value( +# b.outlet.flow_mass_phase_comp[0, "Liq", "H2O"] +# ) +# # Check saturation pressure +# assert pytest.approx(11992, rel=1e-3) == value(b.pressure_operating) +# # Check heat requirement +# assert pytest.approx(1127.2, rel=1e-3) == value(b.work_mechanical[0]) +# # Check crystallizer diameter +# assert pytest.approx(1.205, rel=1e-3) == value(b.diameter_crystallizer) +# # Minimum active volume +# assert pytest.approx(1.619, rel=1e-3) == value(b.volume_suspension) +# # Residence time +# assert pytest.approx(1.0228, rel=1e-3) == value(b.t_res) +# # Mass-basis costing +# assert pytest.approx(704557, rel=1e-3) == value( +# m.fs.costing.aggregate_capital_cost +# ) + +# @pytest.mark.component +# def test_solution2_capcosting_by_mass(self, Crystallizer_frame): +# m = Crystallizer_frame +# b = m.fs.unit +# b.crystal_growth_rate.fix(5e-8) +# b.souders_brown_constant.fix(0.0244) +# b.crystal_median_length.fix(0.4e-3) +# results = solver.solve(m) + +# # Test that report function works +# b.report() + +# # Check for optimal solution +# assert results.solver.termination_condition == TerminationCondition.optimal +# assert results.solver.status == SolverStatus.ok + +# # Residence time +# assert pytest.approx( +# value(b.crystal_median_length / (3.67 * 3600 * b.crystal_growth_rate)), +# rel=1e-3, +# ) == value(b.t_res) +# # Check crystallizer diameter +# assert pytest.approx(1.5427, rel=1e-3) == value(b.diameter_crystallizer) +# # Minimum active volume +# assert pytest.approx(0.959, rel=1e-3) == value(b.volume_suspension) +# # Mass-basis costing +# assert pytest.approx(704557, rel=1e-3) == value( +# m.fs.costing.aggregate_capital_cost +# ) + +# @pytest.mark.component +# def test_solution2_capcosting_by_volume(self, Crystallizer_frame_2): +# # Same problem as above, but different costing approach. +# # Other results should remain the same. +# m = Crystallizer_frame_2 +# b = m.fs.unit +# b.crystal_growth_rate.fix(5e-8) +# b.souders_brown_constant.fix(0.0244) +# b.crystal_median_length.fix(0.4e-3) + +# assert degrees_of_freedom(m) == 0 + +# m.fs.properties.set_default_scaling( +# "flow_mass_phase_comp", 1e-1, index=("Liq", "H2O") +# ) +# m.fs.properties.set_default_scaling( +# "flow_mass_phase_comp", 1e-1, index=("Liq", "NaCl") +# ) +# m.fs.properties.set_default_scaling( +# "flow_mass_phase_comp", 1e-1, index=("Vap", "H2O") +# ) +# m.fs.properties.set_default_scaling( +# "flow_mass_phase_comp", 1e-1, index=("Sol", "NaCl") +# ) +# calculate_scaling_factors(m) +# initialization_tester(Crystallizer_frame_2) +# results = solver.solve(m) + +# m.fs.costing = TreatmentCosting() +# m.fs.unit.costing = UnitModelCostingBlock( +# flowsheet_costing_block=m.fs.costing, +# costing_method_arguments={"cost_type": CrystallizerCostType.volume_basis}, +# ) +# m.fs.costing.cost_process() +# assert_units_consistent(m) +# results = solver.solve(m) + +# # Test that report function works +# b.report() + +# # Check for optimal solution +# assert results.solver.termination_condition == TerminationCondition.optimal +# assert results.solver.status == SolverStatus.ok + +# # Residence time +# assert pytest.approx( +# value(b.crystal_median_length / (3.67 * 3600 * b.crystal_growth_rate)), +# rel=1e-3, +# ) == value(b.t_res) +# # Check crystallizer diameter +# assert pytest.approx(1.5427, rel=1e-3) == value(b.diameter_crystallizer) +# # Minimum active volume +# assert pytest.approx(0.959, rel=1e-3) == value(b.volume_suspension) +# # Volume-basis costing +# assert pytest.approx(467490, rel=1e-3) == value( +# m.fs.costing.aggregate_capital_cost +# ) + +# @pytest.mark.component +# def test_solution2_operatingcost(self, Crystallizer_frame_2): +# m = Crystallizer_frame_2 +# b = m.fs.unit +# b.crystal_growth_rate.fix(5e-8) +# b.souders_brown_constant.fix(0.0244) +# b.crystal_median_length.fix(0.4e-3) +# results = solver.solve(m) + +# # Check for optimal solution +# assert results.solver.termination_condition == TerminationCondition.optimal +# assert results.solver.status == SolverStatus.ok + +# # Operating cost validation +# assert pytest.approx(980.77, rel=1e-3) == value( +# m.fs.costing.aggregate_flow_costs["electricity"] +# ) +# assert pytest.approx(36000.75, rel=1e-3) == value( +# m.fs.costing.aggregate_flow_costs["steam"] +# ) +# assert pytest.approx(0, rel=1e-3) == value( +# m.fs.costing.aggregate_flow_costs["NaCl"] +# ) + +# @pytest.mark.component +# def test_solution2_operatingcost_steampressure(self, Crystallizer_frame_2): +# m = Crystallizer_frame_2 +# m.fs.costing.crystallizer.steam_pressure.fix(5) +# b = m.fs.unit +# b.crystal_growth_rate.fix(5e-8) +# b.souders_brown_constant.fix(0.0244) +# b.crystal_median_length.fix(0.4e-3) +# results = solver.solve(m) + +# # Check for optimal solution +# assert results.solver.termination_condition == TerminationCondition.optimal +# assert results.solver.status == SolverStatus.ok + +# # Operating cost validation +# assert pytest.approx(980.77, rel=1e-3) == value( +# m.fs.costing.aggregate_flow_costs["electricity"] +# ) +# assert pytest.approx(25183.20, rel=1e-3) == value( +# m.fs.costing.aggregate_flow_costs["steam"] +# ) +# assert pytest.approx(0, abs=1e-6) == value( +# m.fs.costing.aggregate_flow_costs["NaCl"] +# ) + +# @pytest.mark.component +# def test_solution2_operatingcost_NaCl_revenue(self, Crystallizer_frame_2): +# m = Crystallizer_frame_2 +# m.fs.costing.crystallizer.steam_pressure.fix(3) +# m.fs.costing.crystallizer.NaCl_recovery_value.fix(-0.07) + +# results = solver.solve(m) + +# # Check for optimal solution +# assert results.solver.termination_condition == TerminationCondition.optimal +# assert results.solver.status == SolverStatus.ok + +# # Operating cost validation +# assert pytest.approx(980.77, rel=1e-3) == value( +# m.fs.costing.aggregate_flow_costs["electricity"] +# ) +# assert pytest.approx(36000.75, rel=1e-3) == value( +# m.fs.costing.aggregate_flow_costs["steam"] +# ) +# assert pytest.approx(-220533.25, rel=1e-3) == value( +# m.fs.costing.aggregate_flow_costs["NaCl"] +# ) From 58ff838bbdb29fc8d31221c8490df37a5d9e0186 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Fri, 20 Sep 2024 14:02:58 -0600 Subject: [PATCH 41/80] comment MEC fs test before removal --- .../test_multi_effect_NaCl_crystallizer.py | 462 +++++++++--------- 1 file changed, 231 insertions(+), 231 deletions(-) diff --git a/src/watertap_contrib/reflo/analysis/example_flowsheets/test/test_multi_effect_NaCl_crystallizer.py b/src/watertap_contrib/reflo/analysis/example_flowsheets/test/test_multi_effect_NaCl_crystallizer.py index 9e73f782..ce65339d 100644 --- a/src/watertap_contrib/reflo/analysis/example_flowsheets/test/test_multi_effect_NaCl_crystallizer.py +++ b/src/watertap_contrib/reflo/analysis/example_flowsheets/test/test_multi_effect_NaCl_crystallizer.py @@ -1,231 +1,231 @@ -################################################################################# -# WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, -# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, -# National Renewable Energy Laboratory, and National Energy Technology -# Laboratory (subject to receipt of any required approvals from the U.S. Dept. -# of Energy). All rights reserved. -# -# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license -# information, respectively. These files are also available online at the URL -# "https://github.com/watertap-org/watertap/" -################################################################################# - -import pytest -from pyomo.environ import ( - ConcreteModel, - TerminationCondition, - SolverStatus, - value, - Var, - Objective, -) -from pyomo.network import Port -from pyomo.util.check_units import assert_units_consistent - -from idaes.core import FlowsheetBlock -from idaes.core.util.model_statistics import ( - degrees_of_freedom, - number_variables, - number_total_constraints, - number_unused_variables, -) -from idaes.core.util.testing import initialization_tester -from idaes.core.util.scaling import ( - calculate_scaling_factors, - unscaled_variables_generator, - badly_scaled_var_generator, -) -from idaes.core import UnitModelCostingBlock - -from watertap_contrib.reflo.analysis.example_flowsheets.multi_effect_NaCl_crystallizer import ( - build_fs_multi_effect_crystallizer, - add_costings, - multi_effect_crystallizer_initialization, - get_model_performance, -) -import watertap_contrib.reflo.property_models.cryst_prop_pack as props -from watertap.core.solvers import get_solver -from watertap_contrib.reflo.costing import TreatmentCosting, CrystallizerCostType - -solver = get_solver() - - -class TestMultiEffectCrystallization: - @pytest.fixture(scope="class") - def MultiEffectCrystallizer_frame(self): - m = build_fs_multi_effect_crystallizer( - operating_pressure_eff1=0.45, # bar - operating_pressure_eff2=0.25, # bar - operating_pressure_eff3=0.208, # bar - operating_pressure_eff4=0.095, # bar - feed_flow_mass=1, # kg/s - feed_mass_frac_NaCl=0.15, - feed_pressure=101325, # Pa - feed_temperature=273.15 + 20, # K - crystallizer_yield=0.5, - steam_pressure=1.5, # bar (gauge pressure) - ) - - add_costings(m) - # Negative value to represent salt recovery value ($/kg) - m.fs.costing.crystallizer.NaCl_recovery_value.fix(-0.024) - - multi_effect_crystallizer_initialization(m) - - return m - - @pytest.mark.unit - def test_dof(self, MultiEffectCrystallizer_frame): - m = MultiEffectCrystallizer_frame - assert degrees_of_freedom(m) == 0 - - @pytest.mark.component - def test_solve(self, MultiEffectCrystallizer_frame): - m = MultiEffectCrystallizer_frame - results = solver.solve(m) - - # Check for optimal solution - assert results.solver.termination_condition == TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok - - @pytest.mark.component - def test_solution(self, MultiEffectCrystallizer_frame): - m = MultiEffectCrystallizer_frame - - data_table, overall_performance = get_model_performance(m) - - # Check solid mass in solids stream - assert ( - pytest.approx(overall_performance["Capacity (m3/day)"], rel=1e-3) == 277.39 - ) - assert ( - pytest.approx(overall_performance["Feed brine salinity (g/L)"], rel=1e-3) - == 166.3 - ) - assert ( - pytest.approx(overall_performance["Total brine disposed (kg/s)"], rel=1e-3) - == 3.559 - ) - assert ( - pytest.approx( - overall_performance["Total water production (kg/s)"], rel=1e-3 - ) - == 2.313 - ) - assert ( - pytest.approx( - overall_performance["Total solids collected (kg/s)"], rel=1e-3 - ) - == 0.267 - ) - assert ( - pytest.approx( - overall_performance["Total waste water remained (kg/s)"], rel=1e-3 - ) - == 0.979 - ) - assert ( - pytest.approx( - overall_performance["Initial thermal energy consumption (kW)"], rel=1e-3 - ) - == 1704.47 - ) - assert ( - pytest.approx(overall_performance["Overall STEC (kWh/m3 feed)"], rel=1e-3) - == 147.47 - ) - assert ( - pytest.approx( - overall_performance["Total heat transfer area (m2)"], rel=1e-3 - ) - == 2.03 - ) - assert ( - pytest.approx( - overall_performance["Levelized cost of feed brine ($/m3)"], rel=1e-3 - ) - == 1.719 - ) - - @pytest.mark.component - def test_optimization(self, MultiEffectCrystallizer_frame): - m = MultiEffectCrystallizer_frame - - # optimization scenario - m.fs.eff_1.pressure_operating.unfix() - m.fs.eff_1.pressure_operating.setub(1.2 * 1e5) - m.fs.eff_2.pressure_operating.unfix() - m.fs.eff_3.pressure_operating.unfix() - m.fs.eff_4.pressure_operating.unfix() - m.fs.eff_4.pressure_operating.setlb(0.02 * 1e5) - - effs = [m.fs.eff_1, m.fs.eff_2, m.fs.eff_3, m.fs.eff_4] - total_area = sum(i.area for i in effs) - - # Optimize the heat transfer area - m.fs.objective = Objective(expr=total_area) - - @m.Constraint(doc="Pressure decreasing") - def pressure_bound1(b): - return b.fs.eff_2.pressure_operating <= b.fs.eff_1.pressure_operating - - @m.Constraint(doc="Pressure decreasing") - def pressure_bound2(b): - return b.fs.eff_3.pressure_operating <= b.fs.eff_2.pressure_operating - - @m.Constraint(doc="Temperature difference") - def temp_bound1(b): - return ( - b.fs.eff_2.temperature_operating - >= b.fs.eff_1.temperature_operating - 12 - ) - - @m.Constraint(doc="Temperature difference") - def temp_bound2(b): - return ( - b.fs.eff_3.temperature_operating - >= b.fs.eff_2.temperature_operating - 12 - ) - - @m.Constraint(doc="Pressure decreasing") - def pressure_bound3(b): - return b.fs.eff_4.pressure_operating <= b.fs.eff_3.pressure_operating - - @m.Constraint(doc="Temperature difference") - def temp_bound3(b): - return ( - b.fs.eff_4.temperature_operating - >= b.fs.eff_3.temperature_operating - 12 - ) - - optimization_results = solver.solve(m, tee=False) - assert ( - optimization_results.solver.termination_condition - == TerminationCondition.optimal - ) - data_table2, overall_performance2 = get_model_performance(m) - - assert ( - pytest.approx( - data_table2["Effect 1"]["Operating temperature (C)"], rel=1e-3 - ) - == 113.86 - ) - assert ( - pytest.approx( - data_table2["Effect 2"]["Operating temperature (C)"], rel=1e-3 - ) - == 101.86 - ) - assert ( - pytest.approx( - data_table2["Effect 3"]["Operating temperature (C)"], rel=1e-3 - ) - == 89.86 - ) - assert ( - pytest.approx( - data_table2["Effect 4"]["Operating temperature (C)"], rel=1e-3 - ) - == 77.86 - ) +# ################################################################################# +# # WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, +# # through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# # National Renewable Energy Laboratory, and National Energy Technology +# # Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# # of Energy). All rights reserved. +# # +# # Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# # information, respectively. These files are also available online at the URL +# # "https://github.com/watertap-org/watertap/" +# ################################################################################# + +# import pytest +# from pyomo.environ import ( +# ConcreteModel, +# TerminationCondition, +# SolverStatus, +# value, +# Var, +# Objective, +# ) +# from pyomo.network import Port +# from pyomo.util.check_units import assert_units_consistent + +# from idaes.core import FlowsheetBlock +# from idaes.core.util.model_statistics import ( +# degrees_of_freedom, +# number_variables, +# number_total_constraints, +# number_unused_variables, +# ) +# from idaes.core.util.testing import initialization_tester +# from idaes.core.util.scaling import ( +# calculate_scaling_factors, +# unscaled_variables_generator, +# badly_scaled_var_generator, +# ) +# from idaes.core import UnitModelCostingBlock + +# from watertap_contrib.reflo.analysis.example_flowsheets.multi_effect_NaCl_crystallizer import ( +# build_fs_multi_effect_crystallizer, +# add_costings, +# multi_effect_crystallizer_initialization, +# get_model_performance, +# ) +# import watertap_contrib.reflo.property_models.cryst_prop_pack as props +# from watertap.core.solvers import get_solver +# from watertap_contrib.reflo.costing import TreatmentCosting, CrystallizerCostType + +# solver = get_solver() + + +# class TestMultiEffectCrystallization: +# @pytest.fixture(scope="class") +# def MultiEffectCrystallizer_frame(self): +# m = build_fs_multi_effect_crystallizer( +# operating_pressure_eff1=0.45, # bar +# operating_pressure_eff2=0.25, # bar +# operating_pressure_eff3=0.208, # bar +# operating_pressure_eff4=0.095, # bar +# feed_flow_mass=1, # kg/s +# feed_mass_frac_NaCl=0.15, +# feed_pressure=101325, # Pa +# feed_temperature=273.15 + 20, # K +# crystallizer_yield=0.5, +# steam_pressure=1.5, # bar (gauge pressure) +# ) + +# add_costings(m) +# # Negative value to represent salt recovery value ($/kg) +# m.fs.costing.crystallizer.NaCl_recovery_value.fix(-0.024) + +# multi_effect_crystallizer_initialization(m) + +# return m + +# @pytest.mark.unit +# def test_dof(self, MultiEffectCrystallizer_frame): +# m = MultiEffectCrystallizer_frame +# assert degrees_of_freedom(m) == 0 + +# @pytest.mark.component +# def test_solve(self, MultiEffectCrystallizer_frame): +# m = MultiEffectCrystallizer_frame +# results = solver.solve(m) + +# # Check for optimal solution +# assert results.solver.termination_condition == TerminationCondition.optimal +# assert results.solver.status == SolverStatus.ok + +# @pytest.mark.component +# def test_solution(self, MultiEffectCrystallizer_frame): +# m = MultiEffectCrystallizer_frame + +# data_table, overall_performance = get_model_performance(m) + +# # Check solid mass in solids stream +# assert ( +# pytest.approx(overall_performance["Capacity (m3/day)"], rel=1e-3) == 277.39 +# ) +# assert ( +# pytest.approx(overall_performance["Feed brine salinity (g/L)"], rel=1e-3) +# == 166.3 +# ) +# assert ( +# pytest.approx(overall_performance["Total brine disposed (kg/s)"], rel=1e-3) +# == 3.559 +# ) +# assert ( +# pytest.approx( +# overall_performance["Total water production (kg/s)"], rel=1e-3 +# ) +# == 2.313 +# ) +# assert ( +# pytest.approx( +# overall_performance["Total solids collected (kg/s)"], rel=1e-3 +# ) +# == 0.267 +# ) +# assert ( +# pytest.approx( +# overall_performance["Total waste water remained (kg/s)"], rel=1e-3 +# ) +# == 0.979 +# ) +# assert ( +# pytest.approx( +# overall_performance["Initial thermal energy consumption (kW)"], rel=1e-3 +# ) +# == 1704.47 +# ) +# assert ( +# pytest.approx(overall_performance["Overall STEC (kWh/m3 feed)"], rel=1e-3) +# == 147.47 +# ) +# assert ( +# pytest.approx( +# overall_performance["Total heat transfer area (m2)"], rel=1e-3 +# ) +# == 2.03 +# ) +# assert ( +# pytest.approx( +# overall_performance["Levelized cost of feed brine ($/m3)"], rel=1e-3 +# ) +# == 1.719 +# ) + +# @pytest.mark.component +# def test_optimization(self, MultiEffectCrystallizer_frame): +# m = MultiEffectCrystallizer_frame + +# # optimization scenario +# m.fs.eff_1.pressure_operating.unfix() +# m.fs.eff_1.pressure_operating.setub(1.2 * 1e5) +# m.fs.eff_2.pressure_operating.unfix() +# m.fs.eff_3.pressure_operating.unfix() +# m.fs.eff_4.pressure_operating.unfix() +# m.fs.eff_4.pressure_operating.setlb(0.02 * 1e5) + +# effs = [m.fs.eff_1, m.fs.eff_2, m.fs.eff_3, m.fs.eff_4] +# total_area = sum(i.area for i in effs) + +# # Optimize the heat transfer area +# m.fs.objective = Objective(expr=total_area) + +# @m.Constraint(doc="Pressure decreasing") +# def pressure_bound1(b): +# return b.fs.eff_2.pressure_operating <= b.fs.eff_1.pressure_operating + +# @m.Constraint(doc="Pressure decreasing") +# def pressure_bound2(b): +# return b.fs.eff_3.pressure_operating <= b.fs.eff_2.pressure_operating + +# @m.Constraint(doc="Temperature difference") +# def temp_bound1(b): +# return ( +# b.fs.eff_2.temperature_operating +# >= b.fs.eff_1.temperature_operating - 12 +# ) + +# @m.Constraint(doc="Temperature difference") +# def temp_bound2(b): +# return ( +# b.fs.eff_3.temperature_operating +# >= b.fs.eff_2.temperature_operating - 12 +# ) + +# @m.Constraint(doc="Pressure decreasing") +# def pressure_bound3(b): +# return b.fs.eff_4.pressure_operating <= b.fs.eff_3.pressure_operating + +# @m.Constraint(doc="Temperature difference") +# def temp_bound3(b): +# return ( +# b.fs.eff_4.temperature_operating +# >= b.fs.eff_3.temperature_operating - 12 +# ) + +# optimization_results = solver.solve(m, tee=False) +# assert ( +# optimization_results.solver.termination_condition +# == TerminationCondition.optimal +# ) +# data_table2, overall_performance2 = get_model_performance(m) + +# assert ( +# pytest.approx( +# data_table2["Effect 1"]["Operating temperature (C)"], rel=1e-3 +# ) +# == 113.86 +# ) +# assert ( +# pytest.approx( +# data_table2["Effect 2"]["Operating temperature (C)"], rel=1e-3 +# ) +# == 101.86 +# ) +# assert ( +# pytest.approx( +# data_table2["Effect 3"]["Operating temperature (C)"], rel=1e-3 +# ) +# == 89.86 +# ) +# assert ( +# pytest.approx( +# data_table2["Effect 4"]["Operating temperature (C)"], rel=1e-3 +# ) +# == 77.86 +# ) From 8d6b9fe2b28a8786dfb678a0332fc78358722fa0 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Wed, 25 Sep 2024 09:07:27 -0600 Subject: [PATCH 42/80] remove multi effect crystallizer flowsheet and test file --- .../multi_effect_NaCl_crystallizer.py | 661 ------------------ .../test_multi_effect_NaCl_crystallizer.py | 231 ------ 2 files changed, 892 deletions(-) delete mode 100644 src/watertap_contrib/reflo/analysis/example_flowsheets/multi_effect_NaCl_crystallizer.py delete mode 100644 src/watertap_contrib/reflo/analysis/example_flowsheets/test/test_multi_effect_NaCl_crystallizer.py diff --git a/src/watertap_contrib/reflo/analysis/example_flowsheets/multi_effect_NaCl_crystallizer.py b/src/watertap_contrib/reflo/analysis/example_flowsheets/multi_effect_NaCl_crystallizer.py deleted file mode 100644 index 691b8a00..00000000 --- a/src/watertap_contrib/reflo/analysis/example_flowsheets/multi_effect_NaCl_crystallizer.py +++ /dev/null @@ -1,661 +0,0 @@ -import pandas as pd -import numpy as np - -from pyomo.environ import ( - ConcreteModel, - TerminationCondition, - SolverStatus, - Expression, - value, - Var, - log, - units as pyunits, -) - -from idaes.core import FlowsheetBlock, UnitModelCostingBlock -import idaes.core.util.scaling as iscale - -from watertap.core.solvers import get_solver -from watertap.unit_models.mvc.components.lmtd_chen_callback import ( - delta_temperature_chen_callback, -) -from watertap_contrib.reflo.unit_models.zero_order.crystallizer_zo_watertap import ( - Crystallization, -) - -# import watertap_contrib.reflo.property_models.cryst_prop_pack as props -import watertap.property_models.unit_specific.cryst_prop_pack as props -from watertap_contrib.reflo.costing import ( - TreatmentCosting, - CrystallizerCostType, - _compute_steam_properties, -) - -solver = get_solver() - - -def build_fs_multi_effect_crystallizer( - m=None, - # num_effect=3, - operating_pressure_eff1=0.78, # bar - operating_pressure_eff2=0.25, # bar - operating_pressure_eff3=0.208, # bar - operating_pressure_eff4=0.095, # bar - feed_flow_mass=1, # kg/s - feed_mass_frac_NaCl=0.3, - feed_pressure=101325, # Pa - feed_temperature=273.15 + 20, # K - crystallizer_yield=0.5, - steam_pressure=1.5, # bar (gauge pressure) -): - """ - This flowsheet depicts a 4-effect crystallizer, with brine fed in parallel - to each effect, and the operating pressure is specfied individually. - """ - - if m is None: - m = ConcreteModel() - - mfs = m.fs = FlowsheetBlock(dynamic=False) - m.fs.props = props.NaClParameterBlock() - - # Create 4 effects of crystallizer - eff_1 = m.fs.eff_1 = Crystallization(property_package=m.fs.props) - eff_2 = m.fs.eff_2 = Crystallization(property_package=m.fs.props) - eff_3 = m.fs.eff_3 = Crystallization(property_package=m.fs.props) - eff_4 = m.fs.eff_4 = Crystallization(property_package=m.fs.props) - - feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl - eps = 1e-6 - - eff_1.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"].fix( - feed_flow_mass * feed_mass_frac_NaCl - ) - eff_1.inlet.flow_mass_phase_comp[0, "Liq", "H2O"].fix( - feed_flow_mass * feed_mass_frac_H2O - ) - - for eff in [eff_1, eff_2, eff_3, eff_4]: - # Define feed for all effects - eff.inlet.flow_mass_phase_comp[0, "Sol", "NaCl"].fix(eps) - eff.inlet.flow_mass_phase_comp[0, "Vap", "H2O"].fix(eps) - - eff.inlet.pressure[0].fix(feed_pressure) - eff.inlet.temperature[0].fix(feed_temperature) - - # Fix growth rate, crystal length and Sounders brown constant to default values - eff.crystal_growth_rate.fix() - eff.souders_brown_constant.fix() - eff.crystal_median_length.fix() - - # Fix yield - eff.crystallization_yield["NaCl"].fix(crystallizer_yield) - - # Define operating conditions - m.fs.eff_1.pressure_operating.fix(operating_pressure_eff1 * pyunits.bar) - m.fs.eff_2.pressure_operating.fix(operating_pressure_eff2 * pyunits.bar) - m.fs.eff_3.pressure_operating.fix(operating_pressure_eff3 * pyunits.bar) - m.fs.eff_4.pressure_operating.fix(operating_pressure_eff4 * pyunits.bar) - - steam_temp = add_heat_exchanger_eff1(m, steam_pressure) - add_heat_exchanger_eff2(m) - add_heat_exchanger_eff3(m) - add_heat_exchanger_eff4(m) - - return m - - -def add_heat_exchanger_eff1(m, steam_pressure): - eff_1 = m.fs.eff_1 - - eff_1.delta_temperature_in = Var( - eff_1.flowsheet().time, - initialize=35, - bounds=(None, None), - units=pyunits.K, - doc="Temperature difference at the inlet side", - ) - eff_1.delta_temperature_out = Var( - eff_1.flowsheet().time, - initialize=35, - bounds=(None, None), - units=pyunits.K, - doc="Temperature difference at the outlet side", - ) - delta_temperature_chen_callback(eff_1) - - eff_1.area = Var( - bounds=(0, None), - initialize=1000.0, - doc="Heat exchange area", - units=pyunits.m**2, - ) - - eff_1.overall_heat_transfer_coefficient = Var( - eff_1.flowsheet().time, - bounds=(0, None), - initialize=100.0, - doc="Overall heat transfer coefficient", - units=pyunits.W / pyunits.m**2 / pyunits.K, - ) - - eff_1.overall_heat_transfer_coefficient[0].fix(100) - - # Compute saturation temperature of steam: computed from El-Dessouky expression - steam_pressure_sat = steam_pressure * pyunits.bar - - tsat_constants = [ - 42.6776 * pyunits.K, - -3892.7 * pyunits.K, - 1000 * pyunits.kPa, - -9.48654 * pyunits.dimensionless, - ] - psat = ( - pyunits.convert(steam_pressure_sat, to_units=pyunits.kPa) - + 101.325 * pyunits.kPa - ) - - temperature_sat = tsat_constants[0] + tsat_constants[1] / ( - log(psat / tsat_constants[2]) + tsat_constants[3] - ) - - @m.Constraint(eff_1.flowsheet().time, doc="delta_temperature_in at the 1st effect") - def delta_temperature_in_eff1(b, t): - return ( - b.fs.eff_1.delta_temperature_in[t] - == temperature_sat - b.fs.eff_1.temperature_operating - ) - - @m.Constraint(eff_1.flowsheet().time, doc="delta_temperature_out at the 1st effect") - def delta_temperature_out_eff1(b, t): - return ( - b.fs.eff_1.delta_temperature_out[t] - == temperature_sat - b.fs.eff_1.properties_in[0].temperature - ) - - @m.Constraint(eff_1.flowsheet().time) - def heat_transfer_equation_eff_1(b, t): - return b.fs.eff_1.work_mechanical[0] == ( - b.fs.eff_1.overall_heat_transfer_coefficient[t] - * b.fs.eff_1.area - * b.fs.eff_1.delta_temperature[0] - ) - - iscale.set_scaling_factor(eff_1.delta_temperature_in, 1e-1) - iscale.set_scaling_factor(eff_1.delta_temperature_out, 1e-1) - iscale.set_scaling_factor(eff_1.area, 1e-1) - iscale.set_scaling_factor(eff_1.overall_heat_transfer_coefficient, 1e-1) - - return temperature_sat() - - -def add_heat_exchanger_eff2(m): - eff_1 = m.fs.eff_1 - eff_2 = m.fs.eff_2 - - eff_2.delta_temperature_in = Var( - eff_2.flowsheet().time, - initialize=35, - bounds=(None, None), - units=pyunits.K, - doc="Temperature difference at the inlet side", - ) - eff_2.delta_temperature_out = Var( - eff_2.flowsheet().time, - initialize=35, - bounds=(None, None), - units=pyunits.K, - doc="Temperature difference at the outlet side", - ) - delta_temperature_chen_callback(eff_2) - - eff_2.area = Var( - bounds=(0, None), - initialize=1000.0, - units=pyunits.m**2, - doc="Heat exchange area", - ) - - eff_2.overall_heat_transfer_coefficient = Var( - eff_2.flowsheet().time, - bounds=(0, None), - initialize=100.0, - units=pyunits.W / pyunits.m**2 / pyunits.K, - doc="Overall heat transfer coefficient", - ) - - eff_2.overall_heat_transfer_coefficient[0].fix(100) - - @m.Constraint(eff_2.flowsheet().time, doc="delta_temperature_in at the 2nd effect") - def delta_temperature_in_eff2(b, t): - return ( - b.fs.eff_2.delta_temperature_in[t] - == b.fs.eff_1.properties_vapor[0].temperature - - b.fs.eff_2.temperature_operating - ) - - @m.Constraint(eff_2.flowsheet().time, doc="delta_temperature_out at the 2nd effect") - def delta_temperature_out_eff2(b, t): - return ( - b.fs.eff_2.delta_temperature_out[t] - == b.fs.eff_1.properties_pure_water[0].temperature - - b.fs.eff_2.properties_in[0].temperature - ) - - @m.Constraint(eff_2.flowsheet().time) - def heat_transfer_equation_eff_2(b, t): - return b.fs.eff_1.energy_flow_superheated_vapor == ( - b.fs.eff_2.overall_heat_transfer_coefficient[t] - * b.fs.eff_2.area - * b.fs.eff_2.delta_temperature[0] - ) - - iscale.set_scaling_factor(eff_2.delta_temperature_in, 1e-1) - iscale.set_scaling_factor(eff_2.delta_temperature_out, 1e-1) - iscale.set_scaling_factor(eff_2.area, 1e-1) - iscale.set_scaling_factor(eff_2.overall_heat_transfer_coefficient, 1e-1) - - -def add_heat_exchanger_eff3(m): - eff_2 = m.fs.eff_2 - eff_3 = m.fs.eff_3 - - eff_3.delta_temperature_in = Var( - eff_3.flowsheet().time, - initialize=35, - bounds=(None, None), - units=pyunits.K, - doc="Temperature difference at the inlet side", - ) - eff_3.delta_temperature_out = Var( - eff_3.flowsheet().time, - initialize=35, - bounds=(None, None), - units=pyunits.K, - doc="Temperature difference at the outlet side", - ) - delta_temperature_chen_callback(eff_3) - - eff_3.area = Var( - bounds=(0, None), - initialize=1000.0, - units=pyunits.m**2, - doc="Heat exchange area", - ) - - eff_3.overall_heat_transfer_coefficient = Var( - eff_3.flowsheet().time, - bounds=(0, None), - initialize=100.0, - units=pyunits.W / pyunits.m**2 / pyunits.K, - doc="Overall heat transfer coefficient", - ) - - eff_3.overall_heat_transfer_coefficient[0].fix(100) - - @m.Constraint(eff_3.flowsheet().time, doc="delta_temperature_in at the 2nd effect") - def delta_temperature_in_eff3(b, t): - return ( - eff_3.delta_temperature_in[t] - == eff_2.properties_vapor[0].temperature - eff_3.temperature_operating - ) - - @m.Constraint(eff_3.flowsheet().time, doc="delta_temperature_out at the 2nd effect") - def delta_temperature_out_eff3(b, t): - return ( - eff_3.delta_temperature_out[t] - == eff_2.properties_pure_water[0].temperature - - eff_3.properties_in[0].temperature - ) - - @m.Constraint(eff_3.flowsheet().time) - def heat_transfer_equation_eff3(b, t): - return eff_2.energy_flow_superheated_vapor == ( - eff_3.overall_heat_transfer_coefficient[t] - * eff_3.area - * eff_3.delta_temperature[0] - ) - - iscale.set_scaling_factor(eff_3.delta_temperature_in, 1e-1) - iscale.set_scaling_factor(eff_3.delta_temperature_out, 1e-1) - iscale.set_scaling_factor(eff_3.area, 1e-1) - iscale.set_scaling_factor(eff_3.overall_heat_transfer_coefficient, 1e-1) - - -def add_heat_exchanger_eff4(m): - eff_3 = m.fs.eff_3 - eff_4 = m.fs.eff_4 - - eff_4.delta_temperature_in = Var( - eff_4.flowsheet().time, - initialize=35, - bounds=(None, None), - units=pyunits.K, - doc="Temperature differnce at the inlet side", - ) - eff_4.delta_temperature_out = Var( - eff_4.flowsheet().time, - initialize=35, - bounds=(None, None), - units=pyunits.K, - doc="Temperature differnce at the outlet side", - ) - delta_temperature_chen_callback(eff_4) - - eff_4.area = Var( - bounds=(0, None), - initialize=1000.0, - units=pyunits.m**2, - doc="Heat exchange area", - ) - - eff_4.overall_heat_transfer_coefficient = Var( - eff_4.flowsheet().time, - bounds=(0, None), - initialize=100.0, - units=pyunits.W / pyunits.m**2 / pyunits.K, - doc="Overall heat transfer coefficient", - ) - - eff_4.overall_heat_transfer_coefficient[0].fix(100) - - @m.Constraint(eff_4.flowsheet().time, doc="delta_temperature_in at the 2nd effect") - def delta_temperature_in_eff4(b, t): - return ( - eff_4.delta_temperature_in[t] - == eff_3.properties_vapor[0].temperature - eff_4.temperature_operating - ) - - @m.Constraint(eff_4.flowsheet().time, doc="delta_temperature_out at the 2nd effect") - def delta_temperature_out_eff4(b, t): - return ( - eff_4.delta_temperature_out[t] - == eff_3.properties_pure_water[0].temperature - - eff_4.properties_in[0].temperature - ) - - @m.Constraint(eff_4.flowsheet().time) - def heat_transfer_equation_eff4(b, t): - return eff_3.energy_flow_superheated_vapor == ( - eff_4.overall_heat_transfer_coefficient[t] - * eff_4.area - * eff_4.delta_temperature[0] - ) - - iscale.set_scaling_factor(eff_4.delta_temperature_in, 1e-1) - iscale.set_scaling_factor(eff_4.delta_temperature_out, 1e-1) - iscale.set_scaling_factor(eff_4.area, 1e-1) - iscale.set_scaling_factor(eff_4.overall_heat_transfer_coefficient, 1e-1) - - -def add_costings(m): - effs = [m.fs.eff_1, m.fs.eff_2, m.fs.eff_3, m.fs.eff_4] - - m.fs.capex_heat_exchanger = Expression( - expr=(420 * sum(i.area for i in effs)), doc="Capital cost of heat exchangers" - ) - - m.fs.capex_end_plates = Expression( - expr=(1020 * (sum(i.area for i in effs) / 10) ** 0.6), - doc="Capital cost of heat exchanger endplates", - ) - - m.fs.costing = TreatmentCosting() - m.fs.eff_1.costing = UnitModelCostingBlock( - flowsheet_costing_block=m.fs.costing, - costing_method_arguments={"cost_type": CrystallizerCostType.mass_basis}, - ) - m.fs.eff_2.costing = UnitModelCostingBlock( - flowsheet_costing_block=m.fs.costing, - costing_method_arguments={"cost_type": CrystallizerCostType.mass_basis}, - ) - m.fs.eff_3.costing = UnitModelCostingBlock( - flowsheet_costing_block=m.fs.costing, - costing_method_arguments={"cost_type": CrystallizerCostType.mass_basis}, - ) - m.fs.eff_4.costing = UnitModelCostingBlock( - flowsheet_costing_block=m.fs.costing, - costing_method_arguments={"cost_type": CrystallizerCostType.mass_basis}, - ) - - # Effect 2-4 doesn't need additional heating steam and the flows are removed - m.fs.eff_2.costing.costing_package.cost_flow( - -pyunits.convert( - ( - m.fs.eff_2.work_mechanical[0] - / _compute_steam_properties(m.fs.eff_2.costing) - ), - to_units=pyunits.m**3 / pyunits.s, - ), - "steam", - ) - m.fs.eff_3.costing.costing_package.cost_flow( - -pyunits.convert( - ( - m.fs.eff_3.work_mechanical[0] - / _compute_steam_properties(m.fs.eff_3.costing) - ), - to_units=pyunits.m**3 / pyunits.s, - ), - "steam", - ) - - m.fs.eff_4.costing.costing_package.cost_flow( - -pyunits.convert( - ( - m.fs.eff_4.work_mechanical[0] - / _compute_steam_properties(m.fs.eff_4.costing) - ), - to_units=pyunits.m**3 / pyunits.s, - ), - "steam", - ) - - m.fs.costing.cost_process() - - feed_vol_flow_rates = sum(i.properties_in[0].flow_vol_phase["Liq"] for i in effs) - - # Add a term for the unit cost of treated brine ($/m3) - m.fs.levelized_cost_of_feed_brine = Expression( - expr=( - ( - m.fs.costing.total_annualized_cost - + m.fs.costing.capital_recovery_factor - * (m.fs.capex_heat_exchanger + m.fs.capex_end_plates) - ) - / pyunits.convert(feed_vol_flow_rates, to_units=pyunits.m**3 / pyunits.year) - ), - doc="Levelized cost of feed brine", - ) - - -def multi_effect_crystallizer_initialization(m): - # Set scaling factors - m.fs.props.set_default_scaling("flow_mass_phase_comp", 1e-1, index=("Liq", "H2O")) - m.fs.props.set_default_scaling("flow_mass_phase_comp", 1e-1, index=("Liq", "NaCl")) - m.fs.props.set_default_scaling("flow_mass_phase_comp", 1e-1, index=("Vap", "H2O")) - m.fs.props.set_default_scaling("flow_mass_phase_comp", 1e-1, index=("Sol", "NaCl")) - - iscale.calculate_scaling_factors(m) - - m.fs.eff_1.initialize() - m.fs.eff_2.initialize() - m.fs.eff_3.initialize() - m.fs.eff_4.initialize() - - # Unfix dof - brine_salinity = ( - m.fs.eff_1.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"].value - ) - - for eff in [m.fs.eff_2, m.fs.eff_3, m.fs.eff_4]: - eff.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"].unfix() - eff.inlet.flow_mass_phase_comp[0, "Liq", "H2O"].unfix() - eff.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"].fix(brine_salinity) - - # Energy is provided from the previous effect - @m.Constraint(doc="Energy supplied to the 2nd effect") - def eqn_energy_from_eff1(b): - return b.fs.eff_2.work_mechanical[0] == b.fs.eff_1.energy_flow_superheated_vapor - - @m.Constraint(doc="Energy supplied to the 3rd effect") - def eqn_energy_from_eff2(b): - return b.fs.eff_3.work_mechanical[0] == b.fs.eff_2.energy_flow_superheated_vapor - - @m.Constraint(doc="Energy supplied to the 4th effect") - def eqn_energy_from_eff3(b): - return b.fs.eff_4.work_mechanical[0] == b.fs.eff_3.energy_flow_superheated_vapor - - -def get_model_performance(m): - # Print result - effs = [m.fs.eff_1, m.fs.eff_2, m.fs.eff_3, m.fs.eff_4] - effect_names = ["Effect 1", "Effect 2", "Effect 3", "Effect 4"] - feed_salinities = [ - i.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"].value for i in effs - ] - feed_flow_rates = [ - sum( - i.properties_in[0].flow_mass_phase_comp["Liq", j].value - for j in ["H2O", "NaCl"] - ) - for i in effs - ] - feed_vol_flow_rates = [ - i.properties_in[0].flow_vol_phase["Liq"].value * 1000 for i in effs - ] - temp_operating = [i.temperature_operating.value - 273.15 for i in effs] - temp_vapor_cond = [ - i.properties_pure_water[0].temperature.value - 273.15 for i in effs - ] - p_operating = [i.pressure_operating.value / 1e5 for i in effs] - water_prod = [ - i.properties_vapor[0].flow_mass_phase_comp["Vap", "H2O"].value for i in effs - ] - solid_prod = [ - i.properties_solids[0].flow_mass_phase_comp["Sol", "NaCl"].value for i in effs - ] - liquid_prod = [ - sum( - i.properties_out[0].flow_mass_phase_comp["Liq", j].value - for j in ["H2O", "NaCl"] - ) - for i in effs - ] - liquid_flow_rate = [ - i.properties_out[0].flow_vol_phase["Liq"].value * 1000 for i in effs - ] - - liquid_salinity = [ - i.properties_out[0].conc_mass_phase_comp["Liq", "NaCl"].value for i in effs - ] - power_required = [i.work_mechanical[0].value for i in effs] - power_provided = [i.energy_flow_superheated_vapor.value for i in effs] - vapor_enth = [ - i.properties_vapor[0].dh_vap_mass_solvent.value - * i.properties_vapor[0].flow_mass_phase_comp["Vap", "H2O"].value - for i in effs - ] - STEC = [ - i.work_mechanical[0].value - / i.properties_in[0].flow_vol_phase["Liq"].value - / 3600 - for i in effs - ] - - overall_STEC = ( - m.fs.eff_1.work_mechanical[0].value - / sum(i.properties_in[0].flow_vol_phase["Liq"].value for i in effs) - / 3600 - ) - - area = [i.area.value for i in effs] - - model_output = np.array( - [ - feed_flow_rates, - feed_vol_flow_rates, - feed_salinities, - temp_operating, - temp_vapor_cond, - p_operating, - water_prod, - solid_prod, - liquid_prod, - liquid_flow_rate, - liquid_salinity, - power_required, - power_provided, - vapor_enth, - STEC, - area, - ] - ) - - data_table = pd.DataFrame( - data=model_output, - columns=effect_names, - index=[ - "Feed mass flow rate (kg/s)", - "Feed volumetric flow rate (L/s)", - "Feed salinities (g/L)", - "Operating temperature (C)", - "Vapor condensation temperature (C)", - "Operating pressure (bar)", - "Water production (kg/s)", - "Solid production (kg/s)", - "Liquid waste (kg/s)", - "Liquid waste volumetric flow rate (L/s)", - "Liquid waste salinity (g/L)", - "Thermal energy requirement (kW)", - "Thermal energy available from vapor (kW)", - "Vapor enthalpy (kJ)", - "STEC (kWh/m3 feed)", - "Heat transfer area (m2)", - ], - ) - overall_performance = { - "Capacity (m3/day)": sum(feed_vol_flow_rates) * 86400 / 1000, - "Feed brine salinity (g/L)": m.fs.eff_1.properties_in[0] - .conc_mass_phase_comp["Liq", "NaCl"] - .value, - "Total brine disposed (kg/s)": sum(feed_flow_rates), - "Total water production (kg/s)": sum(water_prod), - "Total solids collected (kg/s)": sum(solid_prod), - "Total waste water remained (kg/s)": sum(liquid_prod), - "Initial thermal energy consumption (kW)": m.fs.eff_1.work_mechanical[0].value, - "Overall STEC (kWh/m3 feed)": overall_STEC, - "Total heat transfer area (m2)": sum(i.area.value for i in effs), - "Levelized cost of feed brine ($/m3)": value(m.fs.levelized_cost_of_feed_brine), - } - - return data_table, overall_performance - - -if __name__ == "__main__": - m = build_fs_multi_effect_crystallizer( - operating_pressure_eff1=0.45, # bar - operating_pressure_eff2=0.25, # bar - operating_pressure_eff3=0.208, # bar - operating_pressure_eff4=0.095, # bar - feed_flow_mass=1, # kg/s - feed_mass_frac_NaCl=0.15, - feed_pressure=101325, # Pa - feed_temperature=273.15 + 20, # K - crystallizer_yield=0.5, - steam_pressure=1.5, # bar (gauge pressure) - ) - add_costings(m) - - # Negative value for salt recovery value ($/kg) - m.fs.costing.crystallizer.NaCl_recovery_value.fix(-0.024) - - multi_effect_crystallizer_initialization(m) - - results = solver.solve(m) - - # Check for optimal solution - assert results.solver.termination_condition == TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok - - data_table, overall_performance = get_model_performance(m) diff --git a/src/watertap_contrib/reflo/analysis/example_flowsheets/test/test_multi_effect_NaCl_crystallizer.py b/src/watertap_contrib/reflo/analysis/example_flowsheets/test/test_multi_effect_NaCl_crystallizer.py deleted file mode 100644 index ce65339d..00000000 --- a/src/watertap_contrib/reflo/analysis/example_flowsheets/test/test_multi_effect_NaCl_crystallizer.py +++ /dev/null @@ -1,231 +0,0 @@ -# ################################################################################# -# # WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, -# # through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, -# # National Renewable Energy Laboratory, and National Energy Technology -# # Laboratory (subject to receipt of any required approvals from the U.S. Dept. -# # of Energy). All rights reserved. -# # -# # Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license -# # information, respectively. These files are also available online at the URL -# # "https://github.com/watertap-org/watertap/" -# ################################################################################# - -# import pytest -# from pyomo.environ import ( -# ConcreteModel, -# TerminationCondition, -# SolverStatus, -# value, -# Var, -# Objective, -# ) -# from pyomo.network import Port -# from pyomo.util.check_units import assert_units_consistent - -# from idaes.core import FlowsheetBlock -# from idaes.core.util.model_statistics import ( -# degrees_of_freedom, -# number_variables, -# number_total_constraints, -# number_unused_variables, -# ) -# from idaes.core.util.testing import initialization_tester -# from idaes.core.util.scaling import ( -# calculate_scaling_factors, -# unscaled_variables_generator, -# badly_scaled_var_generator, -# ) -# from idaes.core import UnitModelCostingBlock - -# from watertap_contrib.reflo.analysis.example_flowsheets.multi_effect_NaCl_crystallizer import ( -# build_fs_multi_effect_crystallizer, -# add_costings, -# multi_effect_crystallizer_initialization, -# get_model_performance, -# ) -# import watertap_contrib.reflo.property_models.cryst_prop_pack as props -# from watertap.core.solvers import get_solver -# from watertap_contrib.reflo.costing import TreatmentCosting, CrystallizerCostType - -# solver = get_solver() - - -# class TestMultiEffectCrystallization: -# @pytest.fixture(scope="class") -# def MultiEffectCrystallizer_frame(self): -# m = build_fs_multi_effect_crystallizer( -# operating_pressure_eff1=0.45, # bar -# operating_pressure_eff2=0.25, # bar -# operating_pressure_eff3=0.208, # bar -# operating_pressure_eff4=0.095, # bar -# feed_flow_mass=1, # kg/s -# feed_mass_frac_NaCl=0.15, -# feed_pressure=101325, # Pa -# feed_temperature=273.15 + 20, # K -# crystallizer_yield=0.5, -# steam_pressure=1.5, # bar (gauge pressure) -# ) - -# add_costings(m) -# # Negative value to represent salt recovery value ($/kg) -# m.fs.costing.crystallizer.NaCl_recovery_value.fix(-0.024) - -# multi_effect_crystallizer_initialization(m) - -# return m - -# @pytest.mark.unit -# def test_dof(self, MultiEffectCrystallizer_frame): -# m = MultiEffectCrystallizer_frame -# assert degrees_of_freedom(m) == 0 - -# @pytest.mark.component -# def test_solve(self, MultiEffectCrystallizer_frame): -# m = MultiEffectCrystallizer_frame -# results = solver.solve(m) - -# # Check for optimal solution -# assert results.solver.termination_condition == TerminationCondition.optimal -# assert results.solver.status == SolverStatus.ok - -# @pytest.mark.component -# def test_solution(self, MultiEffectCrystallizer_frame): -# m = MultiEffectCrystallizer_frame - -# data_table, overall_performance = get_model_performance(m) - -# # Check solid mass in solids stream -# assert ( -# pytest.approx(overall_performance["Capacity (m3/day)"], rel=1e-3) == 277.39 -# ) -# assert ( -# pytest.approx(overall_performance["Feed brine salinity (g/L)"], rel=1e-3) -# == 166.3 -# ) -# assert ( -# pytest.approx(overall_performance["Total brine disposed (kg/s)"], rel=1e-3) -# == 3.559 -# ) -# assert ( -# pytest.approx( -# overall_performance["Total water production (kg/s)"], rel=1e-3 -# ) -# == 2.313 -# ) -# assert ( -# pytest.approx( -# overall_performance["Total solids collected (kg/s)"], rel=1e-3 -# ) -# == 0.267 -# ) -# assert ( -# pytest.approx( -# overall_performance["Total waste water remained (kg/s)"], rel=1e-3 -# ) -# == 0.979 -# ) -# assert ( -# pytest.approx( -# overall_performance["Initial thermal energy consumption (kW)"], rel=1e-3 -# ) -# == 1704.47 -# ) -# assert ( -# pytest.approx(overall_performance["Overall STEC (kWh/m3 feed)"], rel=1e-3) -# == 147.47 -# ) -# assert ( -# pytest.approx( -# overall_performance["Total heat transfer area (m2)"], rel=1e-3 -# ) -# == 2.03 -# ) -# assert ( -# pytest.approx( -# overall_performance["Levelized cost of feed brine ($/m3)"], rel=1e-3 -# ) -# == 1.719 -# ) - -# @pytest.mark.component -# def test_optimization(self, MultiEffectCrystallizer_frame): -# m = MultiEffectCrystallizer_frame - -# # optimization scenario -# m.fs.eff_1.pressure_operating.unfix() -# m.fs.eff_1.pressure_operating.setub(1.2 * 1e5) -# m.fs.eff_2.pressure_operating.unfix() -# m.fs.eff_3.pressure_operating.unfix() -# m.fs.eff_4.pressure_operating.unfix() -# m.fs.eff_4.pressure_operating.setlb(0.02 * 1e5) - -# effs = [m.fs.eff_1, m.fs.eff_2, m.fs.eff_3, m.fs.eff_4] -# total_area = sum(i.area for i in effs) - -# # Optimize the heat transfer area -# m.fs.objective = Objective(expr=total_area) - -# @m.Constraint(doc="Pressure decreasing") -# def pressure_bound1(b): -# return b.fs.eff_2.pressure_operating <= b.fs.eff_1.pressure_operating - -# @m.Constraint(doc="Pressure decreasing") -# def pressure_bound2(b): -# return b.fs.eff_3.pressure_operating <= b.fs.eff_2.pressure_operating - -# @m.Constraint(doc="Temperature difference") -# def temp_bound1(b): -# return ( -# b.fs.eff_2.temperature_operating -# >= b.fs.eff_1.temperature_operating - 12 -# ) - -# @m.Constraint(doc="Temperature difference") -# def temp_bound2(b): -# return ( -# b.fs.eff_3.temperature_operating -# >= b.fs.eff_2.temperature_operating - 12 -# ) - -# @m.Constraint(doc="Pressure decreasing") -# def pressure_bound3(b): -# return b.fs.eff_4.pressure_operating <= b.fs.eff_3.pressure_operating - -# @m.Constraint(doc="Temperature difference") -# def temp_bound3(b): -# return ( -# b.fs.eff_4.temperature_operating -# >= b.fs.eff_3.temperature_operating - 12 -# ) - -# optimization_results = solver.solve(m, tee=False) -# assert ( -# optimization_results.solver.termination_condition -# == TerminationCondition.optimal -# ) -# data_table2, overall_performance2 = get_model_performance(m) - -# assert ( -# pytest.approx( -# data_table2["Effect 1"]["Operating temperature (C)"], rel=1e-3 -# ) -# == 113.86 -# ) -# assert ( -# pytest.approx( -# data_table2["Effect 2"]["Operating temperature (C)"], rel=1e-3 -# ) -# == 101.86 -# ) -# assert ( -# pytest.approx( -# data_table2["Effect 3"]["Operating temperature (C)"], rel=1e-3 -# ) -# == 89.86 -# ) -# assert ( -# pytest.approx( -# data_table2["Effect 4"]["Operating temperature (C)"], rel=1e-3 -# ) -# == 77.86 -# ) From a2d4a564f9880911c0f7f80d7da8deff31c02a90 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Wed, 25 Sep 2024 09:08:08 -0600 Subject: [PATCH 43/80] remove cryst prop pack and test --- .../reflo/property_models/cryst_prop_pack.py | 2157 ----------------- .../tests/test_cryst_prop_pack.py | 806 ------ 2 files changed, 2963 deletions(-) delete mode 100644 src/watertap_contrib/reflo/property_models/cryst_prop_pack.py delete mode 100644 src/watertap_contrib/reflo/property_models/tests/test_cryst_prop_pack.py diff --git a/src/watertap_contrib/reflo/property_models/cryst_prop_pack.py b/src/watertap_contrib/reflo/property_models/cryst_prop_pack.py deleted file mode 100644 index a81eb8de..00000000 --- a/src/watertap_contrib/reflo/property_models/cryst_prop_pack.py +++ /dev/null @@ -1,2157 +0,0 @@ -################################################################################# -# WaterTAP Copyright (c) 2020-2024, The Regents of the University of California, -# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, -# National Renewable Energy Laboratory, and National Energy Technology -# Laboratory (subject to receipt of any required approvals from the U.S. Dept. -# of Energy). All rights reserved. -# -# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license -# information, respectively. These files are also available online at the URL -# "https://github.com/watertap-org/watertap/" -################################################################################# -""" -Initial crystallization property package for H2O-NaCl system -""" - -# Import Python libraries -import idaes.logger as idaeslog - -from enum import Enum, auto - -# Import Pyomo libraries -from pyomo.environ import ( - Constraint, - Expression, - Reals, - NonNegativeReals, - Var, - Param, - exp, - log, - value, - check_optimal_termination, -) -from pyomo.environ import units as pyunits -from pyomo.common.config import ConfigValue, In - -# Import IDAES cores -from idaes.core import ( - declare_process_block_class, - MaterialFlowBasis, - PhysicalParameterBlock, - StateBlockData, - StateBlock, - MaterialBalanceType, - EnergyBalanceType, -) -from idaes.core.base.components import Solute, Solvent -from idaes.core.base.phases import ( - LiquidPhase, - VaporPhase, - SolidPhase, - PhaseType as PT, -) -from idaes.core.util.constants import Constants -from idaes.core.util.initialization import ( - fix_state_vars, - revert_state_vars, - solve_indexed_blocks, -) -from idaes.core.util.misc import extract_data -from watertap.core.solvers import get_solver -from idaes.core.util.model_statistics import ( - degrees_of_freedom, - number_unfixed_variables, -) -from idaes.core.util.exceptions import ( - ConfigurationError, - InitializationError, - PropertyPackageError, -) -import idaes.core.util.scaling as iscale - - -# Set up logger -_log = idaeslog.getLogger(__name__) - -__author__ = "Oluwamayowa Amusat" - - -class HeatOfCrystallizationModel(Enum): - constant = auto() # Use constant heat of crystallization - zero = auto() # Assume heat of crystallization is zero - temp_dependent = auto() # Use temperature-dependent heat of crystallization - - -@declare_process_block_class("NaClParameterBlock") -class NaClParameterData(PhysicalParameterBlock): - CONFIG = PhysicalParameterBlock.CONFIG() - - CONFIG.declare( - "heat_of_crystallization_model", - ConfigValue( - default=HeatOfCrystallizationModel.constant, - domain=In(HeatOfCrystallizationModel), - description="Heat of crystallization construction flag", - doc=""" - Options to account for heat of crystallization value for NaCl. - - **default** - ``HeatOfCrystallizationModel.constant`` - - .. csv-table:: - :header: "Configuration Options", "Description" - - "``HeatOfCrystallizationModel.constant``", "Fixed heat of crystallization for NaCl based on literature" - "``HeatOfCrystallizationModel.zero``", "Zero heat of crystallization assumption" - "``HeatOfCrystallizationModel.temp_dependent``", "Temperature-dependent heat of crystallization for NaCl" - """, - ), - ) - - def build(self): - super().build() - self._state_block_class = NaClStateBlock - - # Component - self.H2O = Solvent(valid_phase_types=[PT.liquidPhase, PT.vaporPhase]) - self.NaCl = Solute(valid_phase_types=[PT.liquidPhase, PT.solidPhase]) - - # Phases - self.Liq = LiquidPhase(component_list=["H2O", "NaCl"]) - self.Vap = VaporPhase(component_list=["H2O"]) - self.Sol = SolidPhase(component_list=["NaCl"]) - - """ - References - This package was developed from the following references: - - - K.G.Nayar, M.H.Sharqawy, L.D.Banchik, and J.H.Lienhard V, "Thermophysical properties of seawater: A review and - new correlations that include pressure dependence,"Desalination, Vol.390, pp.1 - 24, 2016. - doi: 10.1016/j.desal.2016.02.024(preprint) - - - Mostafa H.Sharqawy, John H.Lienhard V, and Syed M.Zubair, "Thermophysical properties of seawater: A review of - existing correlations and data,"Desalination and Water Treatment, Vol.16, pp.354 - 380, April 2010. - (2017 corrections provided at http://web.mit.edu/seawater) - - - Laliberté, M. & Cooper, W. E. Model for Calculating the Density of Aqueous Electrolyte Solutions - Journal of Chemical & Engineering Data, American Chemical Society (ACS), 2004, 49, 1141-1151. - - - Laliberté, M. A Model for Calculating the Heat Capacity of Aqueous Solutions, with Updated Density and Viscosity Data - Journal of Chemical & Engineering Data, American Chemical Society (ACS), 2009, 54, 1725-1760 - Liquid NaCl heat capacity and density parameters available https://pubs.acs.org/doi/10.1021/je8008123 - - - Sparrow, B. S. Empirical equations for the thermodynamic properties of aqueous sodium chloride - Desalination, Elsevier BV, 2003, 159, 161-170 - - - Chase, M.W., Jr., NIST-JANAF Themochemical Tables, Fourth Edition, J. Phys. Chem. Ref. Data, Monograph 9, - 1998, 1-1951. - - - El-Dessouky, H. T. & Ettouney, H. M. (Eds.). Appendix A - Thermodynamic Properties. - Fundamentals of Salt Water Desalination, Elsevier Science B.V., 2002, 525-563 - - - Tavare, N. S. Industrial Crystallization, Springer US, 2013. - """ - - # Unit definitions - dens_units = pyunits.kg / pyunits.m**3 - t_inv_units = pyunits.K**-1 - enth_mass_units = pyunits.J / pyunits.kg - enth_mass_units_2 = pyunits.kJ / pyunits.kg - cp_units = pyunits.J / (pyunits.kg * pyunits.K) - cp_units_2 = pyunits.kJ / (pyunits.kg * pyunits.K) - cp_units_3 = pyunits.J / (pyunits.mol * pyunits.K) - - # molecular weights of solute and solvent - mw_comp_data = {"H2O": 18.01528e-3, "NaCl": 58.44e-3} - self.mw_comp = Param( - self.component_list, - initialize=extract_data(mw_comp_data), - units=pyunits.kg / pyunits.mol, - doc="Molecular weight kg/mol", - ) - - # Solubility parameters from (1) surrogate model (2) Sparrow paper - # # 1. Surrogate - # self.sol_param_A1 = Param(initialize= 526.706475, units = pyunits.g / pyunits.L, doc=' Solubility parameter A1 [g/L] for NaCl surrogate') - # self.sol_param_A2 = Param(initialize= -1.326952, units = (pyunits.g / pyunits.L) * pyunits.K ** -1, doc=' Solubility parameter A2 [g/L] for NaCl surrogate') - # self.sol_param_A3 = Param(initialize= 0.002574, units = (pyunits.g / pyunits.L) * pyunits.K ** -2, doc=' Solubility parameter A3 [g/L] for NaCl surrogate') - # 2. Sparrow - self.sol_param_A1 = Param( - initialize=0.2628, - units=pyunits.dimensionless, - doc=" Solubility parameter A1 for NaCl", - ) - self.sol_param_A2 = Param( - initialize=62.75e-6, - units=pyunits.K**-1, - doc=" Solubility parameter A2 for NaCl", - ) - self.sol_param_A3 = Param( - initialize=1.084e-6, - units=pyunits.K**-2, - doc=" Solubility parameter A3 for NaCl", - ) - - # Mass density value for NaCl crystals in solid phase: fixed for now at Tavare value - may not be accurate? - self.dens_mass_param_NaCl = Param( - initialize=2115, - units=pyunits.kg / pyunits.m**3, - doc="NaCl crystal density", - ) - - # Heat of crystallization parameter - fixed value based on heat of fusion from Perry (Table 2-147) - self.dh_crystallization_param = Param( - initialize=-520, units=enth_mass_units_2, doc="NaCl heat of crystallization" - ) - - # Mass density parameters for pure NaCl liquid based on Eq. 9 in Laliberte and Cooper (2004). - self.dens_mass_param_NaCl_liq_C0 = Var( - within=Reals, - initialize=-0.00433, - units=dens_units, - doc="Mass density parameter C0 for liquid NaCl", - ) - self.dens_mass_param_NaCl_liq_C1 = Var( - within=Reals, - initialize=0.06471, - units=dens_units, - doc="Mass density parameter C1 for liquid NaCl", - ) - self.dens_mass_param_NaCl_liq_C2 = Var( - within=Reals, - initialize=1.01660, - units=pyunits.dimensionless, - doc="Mass density parameter C2 for liquid NaCl", - ) - self.dens_mass_param_NaCl_liq_C3 = Var( - within=Reals, - initialize=0.014624, - units=t_inv_units, - doc="Mass density parameter C3 for liquid NaCl", - ) - self.dens_mass_param_NaCl_liq_C4 = Var( - within=Reals, - initialize=3315.6, - units=pyunits.K, - doc="Mass density parameter C4 for liquid NaCl", - ) - - # Mass density parameters for solvent in liquid phase, eq. 8 in Sharqawy et al. (2010) - self.dens_mass_param_A1 = Var( - within=Reals, - initialize=9.999e2, - units=dens_units, - doc="Mass density parameter A1", - ) - self.dens_mass_param_A2 = Var( - within=Reals, - initialize=2.034e-2, - units=dens_units * t_inv_units, - doc="Mass density parameter A2", - ) - self.dens_mass_param_A3 = Var( - within=Reals, - initialize=-6.162e-3, - units=dens_units * t_inv_units**2, - doc="Mass density parameter A3", - ) - self.dens_mass_param_A4 = Var( - within=Reals, - initialize=2.261e-5, - units=dens_units * t_inv_units**3, - doc="Mass density parameter A4", - ) - self.dens_mass_param_A5 = Var( - within=Reals, - initialize=-4.657e-8, - units=dens_units * t_inv_units**4, - doc="Mass density parameter A5", - ) - - # Latent heat of evaporation of pure water: Parameters from Sharqawy et al. (2010), eq. 54 - self.dh_vap_w_param_0 = Var( - within=Reals, - initialize=2.501e6, - units=enth_mass_units, - doc="Latent heat of pure water parameter 0", - ) - self.dh_vap_w_param_1 = Var( - within=Reals, - initialize=-2.369e3, - units=enth_mass_units * t_inv_units**1, - doc="Latent heat of pure water parameter 1", - ) - self.dh_vap_w_param_2 = Var( - within=Reals, - initialize=2.678e-1, - units=enth_mass_units * t_inv_units**2, - doc="Latent heat of pure water parameter 2", - ) - self.dh_vap_w_param_3 = Var( - within=Reals, - initialize=-8.103e-3, - units=enth_mass_units * t_inv_units**3, - doc="Latent heat of pure water parameter 3", - ) - self.dh_vap_w_param_4 = Var( - within=Reals, - initialize=-2.079e-5, - units=enth_mass_units * t_inv_units**4, - doc="Latent heat of pure water parameter 4", - ) - - # Specific heat parameters for Cp vapor from NIST Webbook - Chase, M.W., Jr., NIST-JANAF Themochemical Tables - self.cp_vap_param_A = Var( - within=Reals, - initialize=30.09200 / 18.01528e-3, - units=cp_units, - doc="Specific heat of water vapor parameter A", - ) - self.cp_vap_param_B = Var( - within=Reals, - initialize=6.832514 / 18.01528e-3, - units=cp_units * t_inv_units, - doc="Specific heat of water vapor parameter B", - ) - self.cp_vap_param_C = Var( - within=Reals, - initialize=6.793435 / 18.01528e-3, - units=cp_units * t_inv_units**2, - doc="Specific heat of water vapor parameter C", - ) - self.cp_vap_param_D = Var( - within=Reals, - initialize=-2.534480 / 18.01528e-3, - units=cp_units * t_inv_units**3, - doc="Specific heat of water vapor parameter D", - ) - self.cp_vap_param_E = Var( - within=Reals, - initialize=0.082139 / 18.01528e-3, - units=cp_units * t_inv_units**-2, - doc="Specific heat of water vapor parameter E", - ) - - # Specific heat parameters for pure water from eq (9) in Sharqawy et al. (2010) - self.cp_phase_param_A1 = Var( - within=Reals, - initialize=5.328, - units=cp_units, - doc="Specific heat of seawater parameter A1", - ) - self.cp_phase_param_B1 = Var( - within=Reals, - initialize=-6.913e-3, - units=cp_units * t_inv_units, - doc="Specific heat of seawater parameter B1", - ) - self.cp_phase_param_C1 = Var( - within=Reals, - initialize=9.6e-6, - units=cp_units * t_inv_units**2, - doc="Specific heat of seawater parameter C1", - ) - self.cp_phase_param_D1 = Var( - within=Reals, - initialize=2.5e-9, - units=cp_units * t_inv_units**3, - doc="Specific heat of seawater parameter D1", - ) - - # Specific heat parameters for liquid NaCl from eqs. (11) & (12) in Laliberte (2009). - self.cp_param_NaCl_liq_A1 = Var( - within=Reals, - initialize=-0.06936, - units=cp_units_2, - doc="Specific heat parameter A1 for liquid NaCl", - ) - self.cp_param_NaCl_liq_A2 = Var( - within=Reals, - initialize=-0.07821, - units=t_inv_units, - doc="Specific heat parameter A2 for liquid NaCl", - ) - self.cp_param_NaCl_liq_A3 = Var( - within=Reals, - initialize=3.8480, - units=pyunits.dimensionless, - doc="Specific heat parameter A3 for liquid NaCl", - ) - self.cp_param_NaCl_liq_A4 = Var( - within=Reals, - initialize=-11.2762, - units=pyunits.dimensionless, - doc="Specific heat parameter A4 for liquid NaCl", - ) - self.cp_param_NaCl_liq_A5 = Var( - within=Reals, - initialize=8.7319, - units=cp_units_2, - doc="Specific heat parameter A5 for liquid NaCl", - ) - self.cp_param_NaCl_liq_A6 = Var( - within=Reals, - initialize=1.8125, - units=pyunits.dimensionless, - doc="Specific heat parameter A6 for liquid NaCl", - ) - - # Specific heat parameters for solid NaCl : Shomate equation from NIST webbook (https://webbook.nist.gov/cgi/cbook.cgi?ID=C7647145&Mask=6F). - self.cp_param_NaCl_solid_A = Var( - within=Reals, - initialize=50.72389, - units=cp_units_3, - doc="Specific heat parameter A for solid NaCl", - ) - self.cp_param_NaCl_solid_B = Var( - within=Reals, - initialize=6.672267, - units=cp_units_3 / pyunits.K, - doc="Specific heat parameter B for solid NaCl", - ) - self.cp_param_NaCl_solid_C = Var( - within=Reals, - initialize=-2.517167, - units=cp_units_3 / pyunits.K**2, - doc="Specific heat parameter C for solid NaCl", - ) - self.cp_param_NaCl_solid_D = Var( - within=Reals, - initialize=10.15934, - units=cp_units_3 / pyunits.K**3, - doc="Specific heat parameter D for solid NaCl", - ) - self.cp_param_NaCl_solid_E = Var( - within=Reals, - initialize=-0.200675, - units=cp_units_3 * pyunits.K**2, - doc="Specific heat parameter E for solid NaCl", - ) - self.cp_param_NaCl_solid_F = Var( - within=Reals, - initialize=-427.2115, - units=cp_units_3 * pyunits.K, - doc="Specific heat parameter F for solid NaCl", - ) - # self.cp_param_NaCl_solid_G = Var(within=Reals, initialize=130.3973, units=cp_units_2, doc='Specific heat parameter G for solid NaCl') - self.cp_param_NaCl_solid_H = Var( - within=Reals, - initialize=-411.1203, - units=cp_units_3 * pyunits.K, - doc="Specific heat parameter H for solid NaCl", - ) - - # Vapour pressure parameters for NaCl solution from Sparrow (2003): 0 < T < 150 degC - self.pressure_sat_param_A1 = Var( - within=Reals, - initialize=0.9083e-3, - units=pyunits.MPa, - doc="Vapour pressure parameter A1", - ) - self.pressure_sat_param_A2 = Var( - within=Reals, - initialize=-0.569e-3, - units=pyunits.MPa, - doc="Vapour pressure parameter A2", - ) - self.pressure_sat_param_A3 = Var( - within=Reals, - initialize=0.1945e-3, - units=pyunits.MPa, - doc="Vapour pressure parameter A3", - ) - self.pressure_sat_param_A4 = Var( - within=Reals, - initialize=-3.736e-3, - units=pyunits.MPa, - doc="Vapour pressure parameter A4", - ) - self.pressure_sat_param_A5 = Var( - within=Reals, - initialize=2.82e-3, - units=pyunits.MPa, - doc="Vapour pressure parameter A5", - ) - self.pressure_sat_param_B1 = Var( - within=Reals, - initialize=-0.0669e-3, - units=pyunits.MPa / pyunits.K, - doc="Vapour pressure parameter B1", - ) - self.pressure_sat_param_B2 = Var( - within=Reals, - initialize=0.0582e-3, - units=pyunits.MPa / pyunits.K, - doc="Vapour pressure parameter B2", - ) - self.pressure_sat_param_B3 = Var( - within=Reals, - initialize=-0.1668e-3, - units=pyunits.MPa / pyunits.K, - doc="Vapour pressure parameter B3", - ) - self.pressure_sat_param_B4 = Var( - within=Reals, - initialize=0.6761e-3, - units=pyunits.MPa / pyunits.K, - doc="Vapour pressure parameter B4", - ) - self.pressure_sat_param_B5 = Var( - within=Reals, - initialize=-2.091e-3, - units=pyunits.MPa / pyunits.K, - doc="Vapour pressure parameter B5", - ) - self.pressure_sat_param_C1 = Var( - within=Reals, - initialize=7.541e-6, - units=pyunits.MPa / pyunits.K**2, - doc="Vapour pressure parameter C1", - ) - self.pressure_sat_param_C2 = Var( - within=Reals, - initialize=-5.143e-6, - units=pyunits.MPa / pyunits.K**2, - doc="Vapour pressure parameter C2", - ) - self.pressure_sat_param_C3 = Var( - within=Reals, - initialize=6.482e-6, - units=pyunits.MPa / pyunits.K**2, - doc="Vapour pressure parameter C3", - ) - self.pressure_sat_param_C4 = Var( - within=Reals, - initialize=-52.62e-6, - units=pyunits.MPa / pyunits.K**2, - doc="Vapour pressure parameter C4", - ) - self.pressure_sat_param_C5 = Var( - within=Reals, - initialize=115.7e-6, - units=pyunits.MPa / pyunits.K**2, - doc="Vapour pressure parameter C5", - ) - self.pressure_sat_param_D1 = Var( - within=Reals, - initialize=-0.0922e-6, - units=pyunits.MPa / pyunits.K**3, - doc="Vapour pressure parameter D1", - ) - self.pressure_sat_param_D2 = Var( - within=Reals, - initialize=0.0649e-6, - units=pyunits.MPa / pyunits.K**3, - doc="Vapour pressure parameter D2", - ) - self.pressure_sat_param_D3 = Var( - within=Reals, - initialize=-0.1313e-6, - units=pyunits.MPa / pyunits.K**3, - doc="Vapour pressure parameter D3", - ) - self.pressure_sat_param_D4 = Var( - within=Reals, - initialize=0.8024e-6, - units=pyunits.MPa / pyunits.K**3, - doc="Vapour pressure parameter D4", - ) - self.pressure_sat_param_D5 = Var( - within=Reals, - initialize=-1.986e-6, - units=pyunits.MPa / pyunits.K**3, - doc="Vapour pressure parameter D5", - ) - self.pressure_sat_param_E1 = Var( - within=Reals, - initialize=1.237e-9, - units=pyunits.MPa / pyunits.K**4, - doc="Vapour pressure parameter E1", - ) - self.pressure_sat_param_E2 = Var( - within=Reals, - initialize=-0.753e-9, - units=pyunits.MPa / pyunits.K**4, - doc="Vapour pressure parameter E2", - ) - self.pressure_sat_param_E3 = Var( - within=Reals, - initialize=0.1448e-9, - units=pyunits.MPa / pyunits.K**4, - doc="Vapour pressure parameter E3", - ) - self.pressure_sat_param_E4 = Var( - within=Reals, - initialize=-6.964e-9, - units=pyunits.MPa / pyunits.K**4, - doc="Vapour pressure parameter E4", - ) - self.pressure_sat_param_E5 = Var( - within=Reals, - initialize=14.61e-9, - units=pyunits.MPa / pyunits.K**4, - doc="Vapour pressure parameter E5", - ) - - # Parameters for saturation temperature of water vapour from eq. A.12 in El-Dessouky and Ettouney - self.temp_sat_solvent_A1 = Var( - within=Reals, - initialize=42.6776, - units=pyunits.K, - doc="Water boiling point parameter A1", - ) - self.temp_sat_solvent_A2 = Var( - within=Reals, - initialize=-3892.7, - units=pyunits.K, - doc="Water boiling point parameter A2", - ) - self.temp_sat_solvent_A3 = Var( - within=Reals, - initialize=1000, - units=pyunits.kPa, - doc="Water boiling point parameter A3", - ) - self.temp_sat_solvent_A4 = Var( - within=Reals, - initialize=-9.48654, - units=pyunits.dimensionless, - doc="Water boiling point parameter A4", - ) - - # Parameters for specific enthalpy of pure water in liquid phase from eq. 55 in Sharqawy et al. (2010) - self.enth_mass_solvent_param_A1 = Var( - within=Reals, - initialize=141.355, - units=enth_mass_units, - doc="Specific enthalpy parameter A1", - ) - self.enth_mass_solvent_param_A2 = Var( - within=Reals, - initialize=4202.07, - units=enth_mass_units * t_inv_units, - doc="Specific enthalpy parameter A2", - ) - self.enth_mass_solvent_param_A3 = Var( - within=Reals, - initialize=-0.535, - units=enth_mass_units * t_inv_units**2, - doc="Specific enthalpy parameter A3", - ) - self.enth_mass_solvent_param_A4 = Var( - within=Reals, - initialize=0.004, - units=enth_mass_units * t_inv_units**3, - doc="Specific enthalpy parameter A4", - ) - - # Enthalpy parameters for NaCl solution from Sparrow (2003): 0 < T < 300 degC - self.enth_phase_param_A1 = Var( - within=Reals, - initialize=0.0005e3, - units=enth_mass_units_2, - doc="Solution enthalpy parameter A1", - ) - self.enth_phase_param_A2 = Var( - within=Reals, - initialize=0.0378e3, - units=enth_mass_units_2, - doc="Solution enthalpy parameter A2", - ) - self.enth_phase_param_A3 = Var( - within=Reals, - initialize=-0.3682e3, - units=enth_mass_units_2, - doc="Solution enthalpy parameter A3", - ) - self.enth_phase_param_A4 = Var( - within=Reals, - initialize=-0.6529e3, - units=enth_mass_units_2, - doc="Solution enthalpy parameter A4", - ) - self.enth_phase_param_A5 = Var( - within=Reals, - initialize=2.89e3, - units=enth_mass_units_2, - doc="Solution enthalpy parameter A5", - ) - self.enth_phase_param_B1 = Var( - within=Reals, - initialize=4.145, - units=enth_mass_units_2 / pyunits.K, - doc="Solution enthalpy parameter B1", - ) - self.enth_phase_param_B2 = Var( - within=Reals, - initialize=-4.973, - units=enth_mass_units_2 / pyunits.K, - doc="Solution enthalpy parameter B2", - ) - self.enth_phase_param_B3 = Var( - within=Reals, - initialize=4.482, - units=enth_mass_units_2 / pyunits.K, - doc="Solution enthalpy parameter B3", - ) - self.enth_phase_param_B4 = Var( - within=Reals, - initialize=18.31, - units=enth_mass_units_2 / pyunits.K, - doc="Solution enthalpy parameter B4", - ) - self.enth_phase_param_B5 = Var( - within=Reals, - initialize=-46.41, - units=enth_mass_units_2 / pyunits.K, - doc="Solution enthalpy parameter B5", - ) - self.enth_phase_param_C1 = Var( - within=Reals, - initialize=0.0007, - units=enth_mass_units_2 / pyunits.K**2, - doc="Solution enthalpy parameter C1", - ) - self.enth_phase_param_C2 = Var( - within=Reals, - initialize=-0.0059, - units=enth_mass_units_2 / pyunits.K**2, - doc="Solution enthalpy parameter C2", - ) - self.enth_phase_param_C3 = Var( - within=Reals, - initialize=0.0854, - units=enth_mass_units_2 / pyunits.K**2, - doc="Solution enthalpy parameter C3", - ) - self.enth_phase_param_C4 = Var( - within=Reals, - initialize=-0.4951, - units=enth_mass_units_2 / pyunits.K**2, - doc="Solution enthalpy parameter C4", - ) - self.enth_phase_param_C5 = Var( - within=Reals, - initialize=0.8255, - units=enth_mass_units_2 / pyunits.K**2, - doc="Solution enthalpy parameter C5", - ) - self.enth_phase_param_D1 = Var( - within=Reals, - initialize=-0.0048e-3, - units=enth_mass_units_2 / pyunits.K**3, - doc="Solution enthalpy parameter D1", - ) - self.enth_phase_param_D2 = Var( - within=Reals, - initialize=0.0639e-3, - units=enth_mass_units_2 / pyunits.K**3, - doc="Solution enthalpy parameter D2", - ) - self.enth_phase_param_D3 = Var( - within=Reals, - initialize=-0.714e-3, - units=enth_mass_units_2 / pyunits.K**3, - doc="Solution enthalpy parameter D3", - ) - self.enth_phase_param_D4 = Var( - within=Reals, - initialize=3.273e-3, - units=enth_mass_units_2 / pyunits.K**3, - doc="Solution enthalpy parameter D4", - ) - self.enth_phase_param_D5 = Var( - within=Reals, - initialize=-4.85e-3, - units=enth_mass_units_2 / pyunits.K**3, - doc="Solution enthalpy parameter D5", - ) - self.enth_phase_param_E1 = Var( - within=Reals, - initialize=0.0202e-6, - units=enth_mass_units_2 / pyunits.K**4, - doc="Solution enthalpy parameter E1", - ) - self.enth_phase_param_E2 = Var( - within=Reals, - initialize=-0.2432e-6, - units=enth_mass_units_2 / pyunits.K**4, - doc="Solution enthalpy parameter E2", - ) - self.enth_phase_param_E3 = Var( - within=Reals, - initialize=2.054e-6, - units=enth_mass_units_2 / pyunits.K**4, - doc="Solution enthalpy parameter E3", - ) - self.enth_phase_param_E4 = Var( - within=Reals, - initialize=-8.211e-6, - units=enth_mass_units_2 / pyunits.K**4, - doc="Solution enthalpy parameter E4", - ) - self.enth_phase_param_E5 = Var( - within=Reals, - initialize=11.43e-6, - units=enth_mass_units_2 / pyunits.K**4, - doc="Solution enthalpy parameter E5", - ) - - for v in self.component_objects(Var): - v.fix() - - # ---default scaling--- - self.set_default_scaling("temperature", 1e-2) - self.set_default_scaling("pressure", 1e-6) - self.set_default_scaling("pressure_sat", 1e-5) - self.set_default_scaling("dens_mass_solvent", 1e-3, index="Liq") - self.set_default_scaling("dens_mass_solvent", 1, index="Vap") - self.set_default_scaling("dens_mass_solute", 1e-3, index="Sol") - self.set_default_scaling("dens_mass_solute", 1e-3, index="Liq") - self.set_default_scaling("dens_mass_phase", 1e-3, index="Liq") - self.set_default_scaling("enth_mass_solvent", 1e-2, index="Liq") - self.set_default_scaling("enth_mass_solvent", 1e-3, index="Vap") - self.set_default_scaling("cp_mass_phase", 1e-3, index="Liq") - self.set_default_scaling("dh_vap_mass_solvent", 1e-3) - self.set_default_scaling("dh_crystallization_mass_comp", 1e-2, index="NaCl") - - @classmethod - def define_metadata(cls, obj): - obj.add_default_units( - { - "time": pyunits.s, - "length": pyunits.m, - "mass": pyunits.kg, - "amount": pyunits.mol, - "temperature": pyunits.K, - } - ) - - obj.add_properties( - { - "flow_mass_phase_comp": {"method": None}, - "temperature": {"method": None}, - "pressure": {"method": None}, - "solubility_mass_phase_comp": {"method": "_solubility_mass_phase_comp"}, - "solubility_mass_frac_phase_comp": { - "method": "_solubility_mass_frac_phase_comp" - }, - "mass_frac_phase_comp": {"method": "_mass_frac_phase_comp"}, - "dens_mass_phase": {"method": "_dens_mass_phase"}, - "cp_mass_phase": {"method": "_cp_mass_phase"}, - "flow_vol_phase": {"method": "_flow_vol_phase"}, - "flow_vol": {"method": "_flow_vol"}, - "pressure_sat": {"method": "_pressure_sat"}, - "conc_mass_phase_comp": {"method": "_conc_mass_phase_comp"}, - "enth_mass_phase": {"method": "_enth_mass_phase"}, - "dh_crystallization_mass_comp": { - "method": "_dh_crystallization_mass_comp" - }, - "flow_mol_phase_comp": {"method": "_flow_mol_phase_comp"}, - "mole_frac_phase_comp": {"method": "_mole_frac_phase_comp"}, - } - ) - - obj.define_custom_properties( - { - "dens_mass_solvent": {"method": "_dens_mass_solvent"}, - "dens_mass_solute": {"method": "_dens_mass_solute"}, - "dh_vap_mass_solvent": {"method": "_dh_vap_mass_solvent"}, - "cp_mass_solvent": {"method": "_cp_mass_solvent"}, - "cp_mass_solute": {"method": "_cp_mass_solute"}, - "temperature_sat_solvent": {"method": "_temperature_sat_solvent"}, - "enth_mass_solvent": {"method": "_enth_mass_solvent"}, - "enth_mass_solute": {"method": "_enth_mass_solute"}, - "enth_flow": {"method": "_enth_flow"}, - } - ) - - -class _NaClStateBlock(StateBlock): - """ - This Class contains methods which should be applied to Property Blocks as a - whole, rather than individual elements of indexed Property Blocks. - """ - - def fix_initialization_states(self): - """ - Fixes state variables for state blocks. - - Returns: - None - """ - # Fix state variables - fix_state_vars(self) - - # Constraint on water concentration at outlet - unfix in these cases - for b in self.values(): - if b.config.defined_state is False: - b.conc_mol_comp["H2O"].unfix() - - def initialize( - self, - state_args=None, - state_vars_fixed=False, - hold_state=False, - outlvl=idaeslog.NOTSET, - solver=None, - optarg=None, - ): - """ - Initialization routine for property package. - Keyword Arguments: - state_args : Dictionary with initial guesses for the state vars - chosen. Note that if this method is triggered - through the control volume, and if initial guesses - were not provided at the unit model level, the - control volume passes the inlet values as initial - guess.The keys for the state_args dictionary are: - flow_mass_phase_comp : value at which to initialize - phase component flows - pressure : value at which to initialize pressure - temperature : value at which to initialize temperature - outlvl : sets output level of initialization routine (default=idaeslog.NOTSET) - optarg : solver options dictionary object (default=None) - state_vars_fixed: Flag to denote if state vars have already been - fixed. - - True - states have already been fixed by the - control volume 1D. Control volume 0D - does not fix the state vars, so will - be False if this state block is used - with 0D blocks. - - False - states have not been fixed. The state - block will deal with fixing/unfixing. - solver : Solver object to use during initialization if None is provided - it will use the default solver for IDAES (default = None) - hold_state : flag indicating whether the initialization routine - should unfix any state variables fixed during - initialization (default=False). - - True - states variables are not unfixed, and - a dict of returned containing flags for - which states were fixed during - initialization. - - False - state variables are unfixed after - initialization by calling the - release_state method - Returns: - If hold_states is True, returns a dict containing flags for - which states were fixed during initialization. - """ - # Get loggers - init_log = idaeslog.getInitLogger(self.name, outlvl, tag="properties") - solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="properties") - - # Set solver and options - opt = get_solver(solver, optarg) - - # Fix state variables - flags = fix_state_vars(self, state_args) - # Check when the state vars are fixed already result in dof 0 - for k in self.keys(): - dof = degrees_of_freedom(self[k]) - if dof != 0: - raise PropertyPackageError( - "\nWhile initializing {sb_name}, the degrees of freedom " - "are {dof}, when zero is required. \nInitialization assumes " - "that the state variables should be fixed and that no other " - "variables are fixed. \nIf other properties have a " - "predetermined value, use the calculate_state method " - "before using initialize to determine the values for " - "the state variables and avoid fixing the property variables." - "".format(sb_name=self.name, dof=dof) - ) - - # --------------------------------------------------------------------- - skip_solve = True # skip solve if only state variables are present - for k in self.keys(): - if number_unfixed_variables(self[k]) != 0: - skip_solve = False - - if not skip_solve: - # Initialize properties - with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: - results = solve_indexed_blocks(opt, [self], tee=slc.tee) - init_log.info_high( - "Property initialization: {}.".format(idaeslog.condition(results)) - ) - - # If input block, return flags, else release state - if state_vars_fixed is False: - if hold_state is True: - return flags - else: - self.release_state(flags) - - if (not skip_solve) and (not check_optimal_termination(results)): - raise InitializationError( - f"{self.name} failed to initialize successfully. Please " - f"check the output logs for more information." - ) - - def release_state(self, flags, outlvl=idaeslog.NOTSET): - """ - Method to release state variables fixed during initialisation. - Keyword Arguments: - flags : dict containing information of which state variables - were fixed during initialization, and should now be - unfixed. This dict is returned by initialize if - hold_state=True. - outlvl : sets output level of of logging - """ - # Unfix state variables - init_log = idaeslog.getInitLogger(self.name, outlvl, tag="properties") - revert_state_vars(self, flags) - init_log.info_high("{} State Released.".format(self.name)) - - def calculate_state( - self, - var_args=None, - hold_state=False, - outlvl=idaeslog.NOTSET, - solver=None, - optarg=None, - ): - """ - Solves state blocks given a set of variables and their values. These variables can - be state variables or properties. This method is typically used before - initialization to solve for state variables because non-state variables (i.e. properties) - cannot be fixed in initialization routines. - Keyword Arguments: - var_args : dictionary with variables and their values, they can be state variables or properties - {(VAR_NAME, INDEX): VALUE} - hold_state : flag indicating whether all of the state variables should be fixed after calculate state. - True - State variables will be fixed. - False - State variables will remain unfixed, unless already fixed. - outlvl : idaes logger object that sets output level of solve call (default=idaeslog.NOTSET) - solver : solver name string if None is provided the default solver - for IDAES will be used (default = None) - optarg : solver options dictionary object (default={}) - Returns: - results object from state block solve - """ - # Get logger - solve_log = idaeslog.getSolveLogger(self.name, level=outlvl, tag="properties") - - # Initialize at current state values (not user provided) - self.initialize(solver=solver, optarg=optarg, outlvl=outlvl) - - # Set solver and options - opt = get_solver(solver, optarg) - - # Fix variables and check degrees of freedom - flags = ( - {} - ) # dictionary noting which variables were fixed and their previous state - for k in self.keys(): - sb = self[k] - for (v_name, ind), val in var_args.items(): - var = getattr(sb, v_name) - if iscale.get_scaling_factor(var[ind]) is None: - _log.warning( - "While using the calculate_state method on {sb_name}, variable {v_name} " - "was provided as an argument in var_args, but it does not have a scaling " - "factor. This suggests that the calculate_scaling_factor method has not been " - "used or the variable was created on demand after the scaling factors were " - "calculated. It is recommended to touch all relevant variables (i.e. call " - "them or set an initial value) before using the calculate_scaling_factor " - "method.".format(v_name=v_name, sb_name=sb.name) - ) - if var[ind].is_fixed(): - flags[(k, v_name, ind)] = True - if value(var[ind]) != val: - raise ConfigurationError( - "While using the calculate_state method on {sb_name}, {v_name} was " - "fixed to a value {val}, but it was already fixed to value {val_2}. " - "Unfix the variable before calling the calculate_state " - "method or update var_args." - "".format( - sb_name=sb.name, - v_name=var.name, - val=val, - val_2=value(var[ind]), - ) - ) - else: - flags[(k, v_name, ind)] = False - var[ind].fix(val) - - if degrees_of_freedom(sb) != 0: - raise RuntimeError( - "While using the calculate_state method on {sb_name}, the degrees " - "of freedom were {dof}, but 0 is required. Check var_args and ensure " - "the correct fixed variables are provided." - "".format(sb_name=sb.name, dof=degrees_of_freedom(sb)) - ) - - # Solve - with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: - results = solve_indexed_blocks(opt, [self], tee=slc.tee) - solve_log.info_high( - "Calculate state: {}.".format(idaeslog.condition(results)) - ) - - if not check_optimal_termination(results): - _log.warning( - "While using the calculate_state method on {sb_name}, the solver failed " - "to converge to an optimal solution. This suggests that the user provided " - "infeasible inputs, or that the model is poorly scaled, poorly initialized, " - "or degenerate." - ) - - # unfix all variables fixed with var_args - for (k, v_name, ind), previously_fixed in flags.items(): - if not previously_fixed: - var = getattr(self[k], v_name) - var[ind].unfix() - - # fix state variables if hold_state - if hold_state: - fix_state_vars(self) - - return results - - -@declare_process_block_class("NaClStateBlock", block_class=_NaClStateBlock) -class NaClStateBlockData(StateBlockData): - def build(self): - """Callable method for Block construction.""" - super(NaClStateBlockData, self).build() - self._make_state_vars() - - def _make_state_vars(self): - # Create state variables - self.pressure = Var( - domain=NonNegativeReals, - initialize=101325, - units=pyunits.Pa, - doc="State pressure [Pa]", - ) - - self.temperature = Var( - domain=NonNegativeReals, - initialize=298.15, - bounds=(273.15, 393.15), - units=pyunits.degK, - doc="State temperature [K]", - ) - - self.flow_mass_phase_comp = Var( - self.phase_component_set, - initialize={ - ("Liq", "H2O"): 0.965, - ("Liq", "NaCl"): 0.035, - ("Vap", "H2O"): 0, - ("Sol", "NaCl"): 0, - }, - bounds=(0, None), - domain=NonNegativeReals, - units=pyunits.kg / pyunits.s, - doc="Mass flow rate", - ) - - # Property Methods - - # 1 Mass fraction: From NaCl property package - def _mass_frac_phase_comp(self): - self.mass_frac_phase_comp = Var( - self.phase_component_set, - domain=NonNegativeReals, - initialize={ - ("Liq", "H2O"): 0.965, - ("Liq", "NaCl"): 0.035, - ("Vap", "H2O"): 1.0, - ("Sol", "NaCl"): 1.0, - }, - bounds=(0, 1.0001), - units=pyunits.dimensionless, - doc="Mass fraction", - ) - - def rule_mass_frac_phase_comp(b, p, j): - phase_comp_list = [ - (p, j) - for j in self.params.component_list - if (p, j) in b.phase_component_set - ] - if len(phase_comp_list) == 1: # one component in this phase - return b.mass_frac_phase_comp[p, j] == 1 - else: - return b.mass_frac_phase_comp[p, j] == b.flow_mass_phase_comp[ - p, j - ] / sum(b.flow_mass_phase_comp[p_j] for p_j in phase_comp_list) - - self.eq_mass_frac_phase_comp = Constraint( - self.phase_component_set, rule=rule_mass_frac_phase_comp - ) - - # 2. Solubility in g/L: calculated from solubility mass fraction - def _solubility_mass_phase_comp(self): - self.solubility_mass_phase_comp = Var( - ["Liq"], - ["NaCl"], - domain=NonNegativeReals, - bounds=(300, 1000), - initialize=356.5, - units=pyunits.g / pyunits.L, - doc="solubility of NaCl in water, g/L", - ) - - def rule_solubility_mass_phase_comp(b, j): - return b.solubility_mass_phase_comp[ - "Liq", j - ] == b.solubility_mass_frac_phase_comp["Liq", j] * b.dens_mass_solvent[ - "Liq" - ] / ( - 1 - b.solubility_mass_frac_phase_comp["Liq", j] - ) - - self.eq_solubility_mass_phase_comp = Constraint( - ["NaCl"], rule=rule_solubility_mass_phase_comp - ) - - # 3. Solubility as mass fraction - def _solubility_mass_frac_phase_comp(self): - self.solubility_mass_frac_phase_comp = Var( - ["Liq"], - ["NaCl"], - domain=NonNegativeReals, - bounds=(0, 1.0001), - initialize=0.5, - units=pyunits.dimensionless, - doc="solubility (as mass fraction) of NaCl in water", - ) - - def rule_solubility_mass_frac_phase_comp(b, j): # Sparrow (2003) - t = b.temperature - 273.15 * pyunits.K - return ( - b.solubility_mass_frac_phase_comp["Liq", j] - == b.params.sol_param_A1 - + b.params.sol_param_A2 * t - + b.params.sol_param_A3 * t**2 - ) - - self.eq_solubility_mass_frac_phase_comp = Constraint( - ["NaCl"], rule=rule_solubility_mass_frac_phase_comp - ) - - # 4. Density of solvent (pure water in liquid and vapour phases) - def _dens_mass_solvent(self): - self.dens_mass_solvent = Var( - ["Liq", "Vap"], - initialize=1e3, - bounds=(1e-4, 1e4), - units=pyunits.kg * pyunits.m**-3, - doc="Mass density of pure water", - ) - - def rule_dens_mass_solvent(b, p): - if p == "Liq": # density, eq. 8 in Sharqawy - t = b.temperature - 273.15 * pyunits.K - dens_mass_w = ( - b.params.dens_mass_param_A1 - + b.params.dens_mass_param_A2 * t - + b.params.dens_mass_param_A3 * t**2 - + b.params.dens_mass_param_A4 * t**3 - + b.params.dens_mass_param_A5 * t**4 - ) - return b.dens_mass_solvent[p] == dens_mass_w - elif p == "Vap": - return b.dens_mass_solvent[p] == ( - b.params.mw_comp["H2O"] * b.pressure - ) / (Constants.gas_constant * b.temperature) - - self.eq_dens_mass_solvent = Constraint( - ["Liq", "Vap"], rule=rule_dens_mass_solvent - ) - - # 5. Density of NaCl crystals and liquid - def _dens_mass_solute(self): - self.dens_mass_solute = Var( - ["Sol", "Liq"], - initialize=1e3, - bounds=(1e-4, 1e4), - units=pyunits.kg * pyunits.m**-3, - doc="Mass density of solid NaCl crystals", - ) - - def rule_dens_mass_solute(b, p): - if p == "Sol": - return b.dens_mass_solute[p] == b.params.dens_mass_param_NaCl - elif p == "Liq": # Apparent density in eq. 9 of Laliberte paper - t = b.temperature - 273.15 * pyunits.K - v_app = ( - b.mass_frac_phase_comp["Liq", "NaCl"] - + b.params.dens_mass_param_NaCl_liq_C2 - + (b.params.dens_mass_param_NaCl_liq_C3 * t) - ) / ( - ( - b.mass_frac_phase_comp["Liq", "NaCl"] - * b.params.dens_mass_param_NaCl_liq_C0 - ) - + b.params.dens_mass_param_NaCl_liq_C1 - ) - v_app = v_app / exp( - 0.000001 - * pyunits.K**-2 - * (t + b.params.dens_mass_param_NaCl_liq_C4) ** 2 - ) - return b.dens_mass_solute[p] == 1 / v_app - - self.eq_dens_mass_solute = Constraint( - ["Sol", "Liq"], rule=rule_dens_mass_solute - ) - - # 6. Density of liquid solution (Water + NaCl) - def _dens_mass_phase(self): - self.dens_mass_phase = Var( - ["Liq"], - initialize=1e3, - bounds=(5e2, 1e4), - units=pyunits.kg * pyunits.m**-3, - doc="Mass density of liquid NaCl solution", - ) - - def rule_dens_mass_phase(b): # density, eq. 6 of Laliberte paper - return b.dens_mass_phase["Liq"] == 1 / ( - (b.mass_frac_phase_comp["Liq", "NaCl"] / b.dens_mass_solute["Liq"]) - + (b.mass_frac_phase_comp["Liq", "H2O"] / b.dens_mass_solvent["Liq"]) - ) - - self.eq_dens_mass_phase = Constraint(rule=rule_dens_mass_phase) - - # 7. Latent heat of vapourization of pure water - def _dh_vap_mass_solvent(self): - self.dh_vap_mass_solvent = Var( - initialize=2.4e3, - bounds=(1, 1e5), - units=pyunits.kJ / pyunits.kg, - doc="Latent heat of vaporization of pure water", - ) - - def rule_dh_vap_mass_solvent(b): - t = b.temperature - 273.15 * pyunits.K - dh_vap_sol = ( - b.params.dh_vap_w_param_0 - + b.params.dh_vap_w_param_1 * t - + b.params.dh_vap_w_param_2 * t**2 - + b.params.dh_vap_w_param_3 * t**3 - + b.params.dh_vap_w_param_4 * t**4 - ) - return b.dh_vap_mass_solvent == pyunits.convert( - dh_vap_sol, to_units=pyunits.kJ / pyunits.kg - ) - - self.eq_dh_vap_mass_solvent = Constraint(rule=rule_dh_vap_mass_solvent) - - # 8. Heat capacity of solvent (pure water in liquid and vapour phases) - def _cp_mass_solvent(self): - self.cp_mass_solvent = Var( - ["Liq", "Vap"], - initialize=4e3, - bounds=(1e-5, 1e5), - units=pyunits.J / pyunits.kg / pyunits.K, - doc="Specific heat capacity of pure solvent", - ) - - def rule_cp_mass_solvent(b, p): - if p == "Liq": - # specific heat, eq. 9 in Sharqawy et al. (2010) - # Convert T90 to T68, eq. 4 in Sharqawy et al. (2010); primary reference from Rusby (1991) - t = (b.temperature - 0.00025 * 273.15 * pyunits.K) / (1 - 0.00025) - A = b.params.cp_phase_param_A1 - B = b.params.cp_phase_param_B1 - C = b.params.cp_phase_param_C1 - D = b.params.cp_phase_param_D1 - return ( - b.cp_mass_solvent["Liq"] == (A + B * t + C * t**2 + D * t**3) * 1000 - ) - elif p == "Vap": - t = b.temperature / 1000 - return ( - b.cp_mass_solvent["Vap"] - == b.params.cp_vap_param_A - + b.params.cp_vap_param_B * t - + b.params.cp_vap_param_C * t**2 - + b.params.cp_vap_param_D * t**3 - + b.params.cp_vap_param_E / t**2 - ) - - self.eq_cp_mass_solvent = Constraint(["Liq", "Vap"], rule=rule_cp_mass_solvent) - - # 9. Heat capacity of solid-phase NaCl crystals - def _cp_mass_solute(self): - self.cp_mass_solute = Var( - ["Liq", "Sol"], - initialize=1e3, - bounds=(-1e4, 1e5), - units=pyunits.J / pyunits.kg / pyunits.K, - doc="Specific heat capacity of solid NaCl crystals", - ) - - def rule_cp_mass_solute(b, p): - if p == "Sol": # Shomate equation for NaCl, NIST - t = b.temperature / (1000 * pyunits.dimensionless) - cp_mass_solute_mol = ( - b.params.cp_param_NaCl_solid_A - + (b.params.cp_param_NaCl_solid_B * t) - + (b.params.cp_param_NaCl_solid_C * t**2) - + (b.params.cp_param_NaCl_solid_D * t**3) - + (b.params.cp_param_NaCl_solid_E / (t**2)) - ) - return ( - b.cp_mass_solute[p] == cp_mass_solute_mol / b.params.mw_comp["NaCl"] - ) - if ( - p == "Liq" - ): # NaCl liq. apparent specific heat capacity, eq. 11-12 of Laliberte (2009) - t = b.temperature - 273.15 * pyunits.K - alpha = ( - (b.params.cp_param_NaCl_liq_A2 * t) - + ( - b.params.cp_param_NaCl_liq_A4 - * (1 - b.mass_frac_phase_comp["Liq", "H2O"]) - ) - + (b.params.cp_param_NaCl_liq_A3 * exp(0.01 * pyunits.K**-1 * t)) - ) - cp_nacl_liq = b.params.cp_param_NaCl_liq_A1 * exp( - alpha - ) + b.params.cp_param_NaCl_liq_A5 * ( - (1 - b.mass_frac_phase_comp["Liq", "H2O"]) - ** b.params.cp_param_NaCl_liq_A6 - ) - return b.cp_mass_solute[p] == pyunits.convert( - cp_nacl_liq, to_units=pyunits.J / pyunits.kg / pyunits.K - ) - - self.eq_cp_mass_solute = Constraint(["Liq", "Sol"], rule=rule_cp_mass_solute) - - # 10. cp of liquid solution (Water + NaCl) - def _cp_mass_phase(self): - self.cp_mass_phase = Var( - ["Liq"], - initialize=4e3, - bounds=(1e-4, 1e5), - units=pyunits.J / pyunits.kg / pyunits.K, - doc="Specific heat capacity of liquid solution", - ) - - def rule_cp_mass_phase(b): # heat capacity, eq. 10 of Laliberte (2009) paper - return ( - b.cp_mass_phase["Liq"] - == b.mass_frac_phase_comp["Liq", "NaCl"] * b.cp_mass_solute["Liq"] - + b.mass_frac_phase_comp["Liq", "H2O"] * b.cp_mass_solvent["Liq"] - ) - - self.eq_cp_mass_phase = Constraint(rule=rule_cp_mass_phase) - - # 11. Volumetric flow rate for each phase - def _flow_vol_phase(self): - self.flow_vol_phase = Var( - self.params.phase_list, - initialize=1, - bounds=(0, None), - units=pyunits.m**3 / pyunits.s, - doc="Volumetric flow rate", - ) - - def rule_flow_vol_phase(b, p): - if p == "Liq": - return ( - b.flow_vol_phase[p] - == sum( - b.flow_mass_phase_comp[p, j] - for j in self.params.component_list - if (p, j) in self.phase_component_set - ) - / b.dens_mass_phase[p] - ) - elif p == "Sol": - return ( - b.flow_vol_phase[p] - == sum( - b.flow_mass_phase_comp[p, j] - for j in self.params.component_list - if (p, j) in self.phase_component_set - ) - / b.dens_mass_solute["Sol"] - ) - elif p == "Vap": - return ( - b.flow_vol_phase[p] - == sum( - b.flow_mass_phase_comp[p, j] - for j in self.params.component_list - if (p, j) in self.phase_component_set - ) - / b.dens_mass_solvent["Vap"] - ) - - self.eq_flow_vol_phase = Constraint( - self.params.phase_list, rule=rule_flow_vol_phase - ) - - # 12. Total volumetric flow rate - def _flow_vol(self): - def rule_flow_vol(b): - return sum(b.flow_vol_phase[p] for p in self.params.phase_list) - - self.flow_vol = Expression(rule=rule_flow_vol) - - # 13. Vapour pressure of the NaCl solution based on the boiling temperature - def _pressure_sat(self): - self.pressure_sat = Var( - initialize=1e3, - bounds=(0.001, 1e6), - units=pyunits.Pa, - doc="Vapor pressure of NaCl solution", - ) - - def rule_pressure_sat(b): # vapor pressure, eq6 in Sparrow (2003) - t = b.temperature - 273.15 * pyunits.K - x = b.mass_frac_phase_comp["Liq", "NaCl"] - - ps_a = ( - b.params.pressure_sat_param_A1 - + (b.params.pressure_sat_param_A2 * x) - + (b.params.pressure_sat_param_A3 * x**2) - + (b.params.pressure_sat_param_A4 * x**3) - + (b.params.pressure_sat_param_A5 * x**4) - ) - - ps_b = ( - b.params.pressure_sat_param_B1 - + (b.params.pressure_sat_param_B2 * x) - + (b.params.pressure_sat_param_B3 * x**2) - + (b.params.pressure_sat_param_B4 * x**3) - + (b.params.pressure_sat_param_B5 * x**4) - ) - - ps_c = ( - b.params.pressure_sat_param_C1 - + (b.params.pressure_sat_param_C2 * x) - + (b.params.pressure_sat_param_C3 * x**2) - + (b.params.pressure_sat_param_C4 * x**3) - + (b.params.pressure_sat_param_C5 * x**4) - ) - - ps_d = ( - b.params.pressure_sat_param_D1 - + (b.params.pressure_sat_param_D2 * x) - + (b.params.pressure_sat_param_D3 * x**2) - + (b.params.pressure_sat_param_D4 * x**3) - + (b.params.pressure_sat_param_D5 * x**4) - ) - - ps_e = ( - b.params.pressure_sat_param_E1 - + (b.params.pressure_sat_param_E2 * x) - + (b.params.pressure_sat_param_E3 * x**2) - + (b.params.pressure_sat_param_E4 * x**3) - + (b.params.pressure_sat_param_E5 * x**4) - ) - - p_sat = ps_a + (ps_b * t) + (ps_c * t**2) + (ps_d * t**3) + (ps_e * t**4) - return b.pressure_sat == pyunits.convert(p_sat, to_units=pyunits.Pa) - - self.eq_pressure_sat = Constraint(rule=rule_pressure_sat) - - # 14. Saturation temperature for water vapour at calculated boiling pressure - def _temperature_sat_solvent(self): - self.temperature_sat_solvent = Var( - initialize=298.15, - bounds=(273.15, 1000.15), - units=pyunits.K, - doc="Vapour (saturation) temperature of pure solvent at boiling (i.e. crystallization) pressure", - ) - - def rule_temperature_sat_solvent(b): - psat = pyunits.convert(b.pressure_sat, to_units=pyunits.kPa) - return ( - b.temperature_sat_solvent - == b.params.temp_sat_solvent_A1 - + b.params.temp_sat_solvent_A2 - / ( - log(psat / b.params.temp_sat_solvent_A3) - + b.params.temp_sat_solvent_A4 - ) - ) - - self.eq_temperature_sat_solvent = Constraint(rule=rule_temperature_sat_solvent) - - # 15. Mass concentration - def _conc_mass_phase_comp(self): - self.conc_mass_phase_comp = Var( - ["Liq"], - self.params.component_list, - initialize=10, - bounds=(0, 1e6), - units=pyunits.kg * pyunits.m**-3, - doc="Mass concentration", - ) - - def rule_conc_mass_phase_comp(b, j): - return ( - b.conc_mass_phase_comp["Liq", j] - == b.dens_mass_phase["Liq"] * b.mass_frac_phase_comp["Liq", j] - ) - - self.eq_conc_mass_phase_comp = Constraint( - self.params.component_list, rule=rule_conc_mass_phase_comp - ) - - # 16. Specific enthalpy of solvent (pure water in liquid and vapour phases) - def _enth_mass_solvent(self): - self.enth_mass_solvent = Var( - ["Liq", "Vap"], - initialize=1e3, - bounds=(1, 1e4), - units=pyunits.kJ * pyunits.kg**-1, - doc="Specific saturated enthalpy of pure solvent", - ) - - def rule_enth_mass_solvent(b, p): - t = b.temperature - 273.15 * pyunits.K - h_w = ( - b.params.enth_mass_solvent_param_A1 - + b.params.enth_mass_solvent_param_A2 * t - + b.params.enth_mass_solvent_param_A3 * t**2 - + b.params.enth_mass_solvent_param_A4 * t**3 - ) - if p == "Liq": # enthalpy, eq. 55 in Sharqawy - return b.enth_mass_solvent[p] == pyunits.convert( - h_w, to_units=pyunits.kJ * pyunits.kg**-1 - ) - elif p == "Vap": - - return ( - b.enth_mass_solvent[p] - == pyunits.convert(h_w, to_units=pyunits.kJ * pyunits.kg**-1) - + +b.dh_vap_mass_solvent - ) - - self.eq_enth_mass_solvent = Constraint( - ["Liq", "Vap"], rule=rule_enth_mass_solvent - ) - - # 17. Specific enthalpy of NaCl solution - def _enth_mass_phase(self): - self.enth_mass_phase = Var( - ["Liq"], - initialize=500, - bounds=(1, 1000), - units=pyunits.kJ * pyunits.kg**-1, - doc="Specific enthalpy of NaCl solution", - ) - - def rule_enth_mass_phase( - b, - ): # specific enthalpy calculation based on Sparrow (2003). - t = ( - b.temperature - 273.15 * pyunits.K - ) # temperature in degC, but pyunits in K - S = b.mass_frac_phase_comp["Liq", "NaCl"] - - enth_a = ( - b.params.enth_phase_param_A1 - + (b.params.enth_phase_param_A2 * S) - + (b.params.enth_phase_param_A3 * S**2) - + (b.params.enth_phase_param_A4 * S**3) - + (b.params.enth_phase_param_A5 * S**4) - ) - - enth_b = ( - b.params.enth_phase_param_B1 - + (b.params.enth_phase_param_B2 * S) - + (b.params.enth_phase_param_B3 * S**2) - + (b.params.enth_phase_param_B4 * S**3) - + (b.params.enth_phase_param_B5 * S**4) - ) - - enth_c = ( - b.params.enth_phase_param_C1 - + (b.params.enth_phase_param_C2 * S) - + (b.params.enth_phase_param_C3 * S**2) - + (b.params.enth_phase_param_C4 * S**3) - + (b.params.enth_phase_param_C5 * S**4) - ) - - enth_d = ( - b.params.enth_phase_param_D1 - + (b.params.enth_phase_param_D2 * S) - + (b.params.enth_phase_param_D3 * S**2) - + (b.params.enth_phase_param_D4 * S**3) - + (b.params.enth_phase_param_D5 * S**4) - ) - - enth_e = ( - b.params.enth_phase_param_E1 - + (b.params.enth_phase_param_E2 * S) - + (b.params.enth_phase_param_E3 * S**2) - + (b.params.enth_phase_param_E4 * S**3) - + (b.params.enth_phase_param_E5 * S**4) - ) - - return b.enth_mass_phase["Liq"] == enth_a + (enth_b * t) + ( - enth_c * t**2 - ) + (enth_d * t**3) + (enth_e * t**4) - - self.eq_enth_mass_phase = Constraint(rule=rule_enth_mass_phase) - - # 18. Heat of crystallization - def _dh_crystallization_mass_comp(self): - self.dh_crystallization_mass_comp = Var( - ["NaCl"], - initialize=1, - bounds=(-1e3, 1e3), - units=pyunits.kJ / pyunits.kg, - doc="NaCl heat of crystallization", - ) - - def rule_dh_crystallization_mass_comp(b): - if ( - b.params.config.heat_of_crystallization_model - == HeatOfCrystallizationModel.constant - ): - return ( - b.dh_crystallization_mass_comp["NaCl"] - == b.params.dh_crystallization_param - ) - elif ( - b.params.config.heat_of_crystallization_model - == HeatOfCrystallizationModel.zero - ): - return ( - b.dh_crystallization_mass_comp["NaCl"] - == 0 * pyunits.kJ / pyunits.kg - ) - elif ( - b.params.config.heat_of_crystallization_model - == HeatOfCrystallizationModel.temp_dependent - ): - raise NotImplementedError( - f"Temperature-dependent heat of crystallization model has not been implemented yet." - ) - - self.eq_dh_crystallization_mass_comp = Constraint( - rule=rule_dh_crystallization_mass_comp - ) - - # 19. Heat capacity of solid-phase NaCl crystals - def _enth_mass_solute(self): - self.enth_mass_solute = Var( - ["Sol"], - initialize=1e3, - bounds=(1e-3, 1e4), - units=pyunits.kJ / pyunits.kg, - doc="Specific enthalpy of solid NaCl crystals", - ) - - def rule_enth_mass_solute(b, p): - ############################ - # Shomate equation for molar enthalpy ofNaCl, NIST - # Note: Tref is 298 K, so changing the Tref to 273 K to match IAPWS is necessary. - # Computation formula for reference temperature change: - # Enthalpy at T relative to 273 K = Enthalpy change relative to 298 K + (Enthalpy at 298 K - Enthalpy at 273 K) - ############################ - - # (i) Enthalpy at original reference temperature (298 K) - enth_mass_solute_mol_tref_1 = 0 # Enthalpy at original temperature (298 K) - - # (ii) Enthalpy at new reference temperature (273.15 K) - tref_2 = 273.15 * pyunits.K / 1000 - enth_mass_solute_mol_tref_2 = ( - (b.params.cp_param_NaCl_solid_A * tref_2) - + ((1 / 2) * b.params.cp_param_NaCl_solid_B * tref_2**2) - + ((1 / 3) * b.params.cp_param_NaCl_solid_C * tref_2**3) - + ((1 / 4) * b.params.cp_param_NaCl_solid_D * tref_2**4) - - (b.params.cp_param_NaCl_solid_E / tref_2) - + b.params.cp_param_NaCl_solid_F - - b.params.cp_param_NaCl_solid_H - ) - - # (iii) Compute enthalpy change at temperature t relative to old reference temperature t_ref_1 - t = b.temperature / (1000 * pyunits.dimensionless) - dh_mass_solute_mol_tref_1 = ( - (b.params.cp_param_NaCl_solid_A * t) - + ((1 / 2) * b.params.cp_param_NaCl_solid_B * t**2) - + ((1 / 3) * b.params.cp_param_NaCl_solid_C * t**3) - + ((1 / 4) * b.params.cp_param_NaCl_solid_D * t**4) - - (b.params.cp_param_NaCl_solid_E / t) - + b.params.cp_param_NaCl_solid_F - - b.params.cp_param_NaCl_solid_H - ) - - # (iv) Compute enthalpy change at temperature t relative to new reference temperature t_ref_2 - enth_mass_solute_mol = dh_mass_solute_mol_tref_1 + ( - enth_mass_solute_mol_tref_1 - enth_mass_solute_mol_tref_2 - ) - - # (v) Convert from molar enthalpy to mass enthalpy - enth_mass_solute_mol = enth_mass_solute_mol / b.params.mw_comp["NaCl"] - return b.enth_mass_solute[p] == enth_mass_solute_mol * ( - pyunits.kJ / pyunits.J - ) - - self.eq_enth_mass_solute = Constraint(["Sol"], rule=rule_enth_mass_solute) - - # 20. Total enthalpy flow for any stream: adds up the enthalpies for the solid, liquid and vapour phases - # Assumes no NaCl is vapour stream or water in crystals - def _enth_flow(self): - # enthalpy flow expression for get_enthalpy_flow_terms method - - def rule_enth_flow(b): # enthalpy flow [J/s] - return ( - sum(b.flow_mass_phase_comp["Liq", j] for j in b.params.component_list) - * b.enth_mass_phase["Liq"] - + b.flow_mass_phase_comp["Vap", "H2O"] * b.enth_mass_solvent["Vap"] - + b.flow_mass_phase_comp["Sol", "NaCl"] * b.enth_mass_solute["Sol"] - ) - - self.enth_flow = Expression(rule=rule_enth_flow) - - # 21. Molar flows - def _flow_mol_phase_comp(self): - self.flow_mol_phase_comp = Var( - self.phase_component_set, - initialize=100, - bounds=(None, None), - domain=NonNegativeReals, - units=pyunits.mol / pyunits.s, - doc="Molar flowrate", - ) - - def rule_flow_mol_phase_comp(b, p, j): - return ( - b.flow_mol_phase_comp[p, j] - == b.flow_mass_phase_comp[p, j] / b.params.mw_comp[j] - ) - - self.eq_flow_mol_phase_comp = Constraint( - self.phase_component_set, rule=rule_flow_mol_phase_comp - ) - - # 22. Mole fractions - def _mole_frac_phase_comp(self): - self.mole_frac_phase_comp = Var( - self.phase_component_set, - initialize=0.1, - bounds=(0, 1.0001), - units=pyunits.dimensionless, - doc="Mole fraction", - ) - - def rule_mole_frac_phase_comp(b, p, j): - phase_comp_list = [ - (p, j) - for j in self.params.component_list - if (p, j) in b.phase_component_set - ] - if len(phase_comp_list) == 1: # one component in this phase - return b.mole_frac_phase_comp[p, j] == 1 - else: - return b.mole_frac_phase_comp[p, j] == b.flow_mol_phase_comp[ - p, j - ] / sum(b.flow_mol_phase_comp[p_j] for (p_j) in phase_comp_list) - - self.eq_mole_frac_phase_comp = Constraint( - self.phase_component_set, rule=rule_mole_frac_phase_comp - ) - - # ----------------------------------------------------------------------------- - # Boilerplate Methods - - def get_material_flow_terms(self, p, j): - """Create material flow terms for control volume.""" - return self.flow_mass_phase_comp[p, j] - - def get_enthalpy_flow_terms(self, p): - """Create enthalpy flow terms.""" - return self.enth_flow - - def default_material_balance_type(self): - return MaterialBalanceType.componentTotal - - def default_energy_balance_type(self): - return EnergyBalanceType.enthalpyTotal - - def get_material_flow_basis(self): - return MaterialFlowBasis.mass - - def define_state_vars(self): - """Define state vars.""" - return { - "flow_mass_phase_comp": self.flow_mass_phase_comp, - "temperature": self.temperature, - "pressure": self.pressure, - } - - # ----------------------------------------------------------------------------- - # Scaling methods - def calculate_scaling_factors(self): - super().calculate_scaling_factors() - - # default scaling factors have already been set with idaes.core.property_base.calculate_scaling_factors() - # for the following variables: flow_mass_phase_comp, pressure, temperature, dens_mass_phase, enth_mass_phase - - # These variables should have user input - if iscale.get_scaling_factor(self.flow_mass_phase_comp["Liq", "H2O"]) is None: - sf = iscale.get_scaling_factor( - self.flow_mass_phase_comp["Liq", "H2O"], default=1e0, warning=True - ) - iscale.set_scaling_factor(self.flow_mass_phase_comp["Liq", "H2O"], sf) - - if iscale.get_scaling_factor(self.flow_mass_phase_comp["Liq", "NaCl"]) is None: - sf = iscale.get_scaling_factor( - self.flow_mass_phase_comp["Liq", "NaCl"], default=1e2, warning=True - ) - iscale.set_scaling_factor(self.flow_mass_phase_comp["Liq", "NaCl"], sf) - - if iscale.get_scaling_factor(self.flow_mass_phase_comp["Sol", "NaCl"]) is None: - sf = iscale.get_scaling_factor( - self.flow_mass_phase_comp["Sol", "NaCl"], default=1e2, warning=True - ) - iscale.set_scaling_factor(self.flow_mass_phase_comp["Sol", "NaCl"], sf) - - if iscale.get_scaling_factor(self.flow_mass_phase_comp["Vap", "H2O"]) is None: - sf = iscale.get_scaling_factor( - self.flow_mass_phase_comp["Vap", "H2O"], default=1e0, warning=True - ) - iscale.set_scaling_factor(self.flow_mass_phase_comp["Vap", "H2O"], sf) - - # scaling factors for molecular weights - for j, v in self.params.mw_comp.items(): - if iscale.get_scaling_factor(v) is None: - iscale.set_scaling_factor(self.params.mw_comp, 1e3) - - # Scaling for solubility (g/L) parameters. Values typically about 300-500, so scale by 1e-3. - if self.is_property_constructed("solubility_mass_phase_comp"): - if iscale.get_scaling_factor(self.solubility_mass_phase_comp) is None: - iscale.set_scaling_factor(self.solubility_mass_phase_comp, 1e-3) - - # Scaling for solubility mass fraction. Values typically about 0-1, so scale by 1e0. - if self.is_property_constructed("solubility_mass_frac_phase_comp"): - if iscale.get_scaling_factor(self.solubility_mass_frac_phase_comp) is None: - iscale.set_scaling_factor(self.solubility_mass_frac_phase_comp, 1e0) - - # Scaling for flow_vol_phase: scaled as scale of dominant component in phase / density of phase - if self.is_property_constructed("flow_vol_phase"): - for p in self.params.phase_list: - if p == "Liq": - if iscale.get_scaling_factor(self.flow_vol_phase[p]) is None: - sf = iscale.get_scaling_factor( - self.flow_mass_phase_comp[p, "H2O"] - ) / iscale.get_scaling_factor(self.dens_mass_phase[p]) - iscale.set_scaling_factor(self.flow_vol_phase[p], sf) - elif p == "Vap": - if iscale.get_scaling_factor(self.flow_vol_phase[p]) is None: - sf = iscale.get_scaling_factor( - self.flow_mass_phase_comp[p, "H2O"] - ) / iscale.get_scaling_factor(self.dens_mass_solvent[p]) - iscale.set_scaling_factor(self.flow_vol_phase[p], sf) - elif p == "Sol": - if iscale.get_scaling_factor(self.flow_vol_phase[p]) is None: - sf = iscale.get_scaling_factor( - self.flow_mass_phase_comp[p, "NaCl"] - ) / iscale.get_scaling_factor(self.dens_mass_solute[p]) - iscale.set_scaling_factor(self.flow_vol_phase[p], sf) - - # Scaling material heat capacities - if self.is_property_constructed("cp_mass_solute"): - for p in ["Sol", "Liq"]: - if iscale.get_scaling_factor(self.cp_mass_solute[p]) is None: - iscale.set_scaling_factor( - self.cp_mass_solute[p], 1e-3 - ) # same as scaling factor of .cp_mass_phase['Liq'] - - if self.is_property_constructed("cp_mass_solvent"): - for p in ["Liq", "Vap"]: - if iscale.get_scaling_factor(self.cp_mass_solvent[p]) is None: - iscale.set_scaling_factor( - self.cp_mass_solvent[p], 1e-3 - ) # same as scaling factor of .cp_mass_phase['Liq'] - - # Scaling saturation temperature - if self.is_property_constructed("temperature_sat_solvent"): - if iscale.get_scaling_factor(self.temperature_sat_solvent) is None: - iscale.set_scaling_factor( - self.temperature_sat_solvent, - iscale.get_scaling_factor(self.temperature), - ) - - # Scaling solute and solvent enthalpies - if self.is_property_constructed("enth_mass_solute"): - if iscale.get_scaling_factor(self.enth_mass_solute) is None: - iscale.set_scaling_factor( - self.enth_mass_solute["Sol"], - iscale.get_scaling_factor(self.enth_mass_solvent["Liq"]), - ) - - if self.is_property_constructed("enth_mass_phase"): - if iscale.get_scaling_factor(self.enth_mass_phase) is None: - iscale.set_scaling_factor( - self.enth_mass_phase["Liq"], - iscale.get_scaling_factor(self.enth_mass_solvent["Liq"]), - ) - - # Scaling enthapy flow - not sure about this one - if self.is_property_constructed("enth_flow"): - iscale.set_scaling_factor( - self.enth_flow, - iscale.get_scaling_factor(self.flow_mass_phase_comp["Liq", "H2O"]) - * iscale.get_scaling_factor(self.enth_mass_phase["Liq"]), - ) - - # Scaling molar flows - derived from flow_mass - if self.is_property_constructed("flow_mol_phase_comp"): - for p, j in self.phase_component_set: - if iscale.get_scaling_factor(self.flow_mol_phase_comp[p, j]) is None: - sf = iscale.get_scaling_factor(self.flow_mass_phase_comp[p, j]) - sf /= iscale.get_scaling_factor(self.params.mw_comp[j]) - iscale.set_scaling_factor(self.flow_mol_phase_comp[p, j], sf) - - ###################################################### - # Scaling for mass fractions - needs verification! - if self.is_property_constructed("mass_frac_phase_comp"): - # Option 1: - for p, j in self.phase_component_set: - if iscale.get_scaling_factor(self.mass_frac_phase_comp[p, j]) is None: - if p == "Sol": - iscale.set_scaling_factor(self.mass_frac_phase_comp[p, j], 1e0) - else: - if j == "NaCl": - sf = iscale.get_scaling_factor( - self.flow_mass_phase_comp[p, j] - ) / iscale.get_scaling_factor( - self.flow_mass_phase_comp[p, "H2O"] - ) - iscale.set_scaling_factor( - self.mass_frac_phase_comp[p, j], sf - ) - elif j == "H2O": - iscale.set_scaling_factor( - self.mass_frac_phase_comp[p, j], 1e0 - ) - - # Scaling for mole fractions - same approach as mass fractions - needs verification! - # Appears to make things worse! - if self.is_property_constructed("mole_frac_phase_comp"): - # Option 1: - for p, j in self.phase_component_set: - if iscale.get_scaling_factor(self.mole_frac_phase_comp[p, j]) is None: - if p == "Sol": - iscale.set_scaling_factor(self.mole_frac_phase_comp[p, j], 1e-1) - else: - if j == "NaCl": - sf = iscale.get_scaling_factor( - self.flow_mol_phase_comp[p, j] - ) / iscale.get_scaling_factor( - self.flow_mol_phase_comp[p, "H2O"] - ) - iscale.set_scaling_factor( - self.mole_frac_phase_comp[p, j], sf - ) - elif j == "H2O": - iscale.set_scaling_factor( - self.mole_frac_phase_comp[p, j], 1e0 - ) - - # ######################################################## - - # Scaling for mass concentrations - if self.is_property_constructed("conc_mass_phase_comp"): - for j in self.params.component_list: - sf_dens = iscale.get_scaling_factor(self.dens_mass_phase["Liq"]) - if ( - iscale.get_scaling_factor(self.conc_mass_phase_comp["Liq", j]) - is None - ): - if j == "H2O": - iscale.set_scaling_factor( - self.conc_mass_phase_comp["Liq", j], sf_dens - ) - elif j == "NaCl": - iscale.set_scaling_factor( - self.conc_mass_phase_comp["Liq", j], - sf_dens - * iscale.get_scaling_factor( - self.mass_frac_phase_comp["Liq", j] - ), - ) - - # Transforming constraints - # property relationships with no index, simple constraint - v_str_lst_simple = [ - "pressure_sat", - "dh_vap_mass_solvent", - "temperature_sat_solvent", - ] - for v_str in v_str_lst_simple: - if self.is_property_constructed(v_str): - v = getattr(self, v_str) - sf = iscale.get_scaling_factor(v, default=1, warning=True) - c = getattr(self, "eq_" + v_str) - iscale.constraint_scaling_transform(c, sf) - - # Property relationships with phase index, but simple constraint - v_str_lst_phase = ["dens_mass_phase", "enth_mass_phase", "cp_mass_phase"] - for v_str in v_str_lst_phase: - if self.is_property_constructed(v_str): - v = getattr(self, v_str) - sf = iscale.get_scaling_factor(v["Liq"], default=1, warning=True) - c = getattr(self, "eq_" + v_str) - iscale.constraint_scaling_transform(c, sf) - - # Property relationship indexed by component - v_str_lst_comp = [ - "solubility_mass_phase_comp", - "solubility_mass_frac_phase_comp", - "conc_mass_phase_comp", - ] - for v_str in v_str_lst_comp: - if self.is_property_constructed(v_str): - v_comp = getattr(self, v_str) - c_comp = getattr(self, "eq_" + v_str) - for j, c in c_comp.items(): - sf = iscale.get_scaling_factor( - v_comp["Liq", j], default=1, warning=True - ) - iscale.constraint_scaling_transform(c, sf) - - # Property relationship indexed by single component - if self.is_property_constructed("dh_crystallization_mass_comp"): - sf = iscale.get_scaling_factor(self.dh_crystallization_mass_comp["NaCl"]) - iscale.constraint_scaling_transform( - self.eq_dh_crystallization_mass_comp, sf - ) - - # Property relationships with phase index and indexed constraints - v_str_lst_phase = [ - "dens_mass_solvent", - "dens_mass_solute", - "flow_vol_phase", - "enth_mass_solvent", - "enth_mass_solute", - "cp_mass_solvent", - "cp_mass_solute", - ] - for v_str in v_str_lst_phase: - if self.is_property_constructed(v_str): - v = getattr(self, v_str) - c_phase = getattr(self, "eq_" + v_str) - for ind, c in c_phase.items(): - sf = iscale.get_scaling_factor(v[ind], default=1, warning=True) - iscale.constraint_scaling_transform(c, sf) - - # Property relationships indexed by component and phase - v_str_lst_phase_comp = [ - "mass_frac_phase_comp", - "flow_mol_phase_comp", - "mole_frac_phase_comp", - ] - for v_str in v_str_lst_phase_comp: - if self.is_property_constructed(v_str): - v_comp = getattr(self, v_str) - c_comp = getattr(self, "eq_" + v_str) - for j, c in c_comp.items(): - sf = iscale.get_scaling_factor(v_comp[j], default=1, warning=True) - iscale.constraint_scaling_transform(c, sf) diff --git a/src/watertap_contrib/reflo/property_models/tests/test_cryst_prop_pack.py b/src/watertap_contrib/reflo/property_models/tests/test_cryst_prop_pack.py deleted file mode 100644 index a27c0847..00000000 --- a/src/watertap_contrib/reflo/property_models/tests/test_cryst_prop_pack.py +++ /dev/null @@ -1,806 +0,0 @@ -################################################################################# -# WaterTAP Copyright (c) 2020-2024, The Regents of the University of California, -# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, -# National Renewable Energy Laboratory, and National Energy Technology -# Laboratory (subject to receipt of any required approvals from the U.S. Dept. -# of Energy). All rights reserved. -# -# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license -# information, respectively. These files are also available online at the URL -# "https://github.com/watertap-org/watertap/" -################################################################################# -import pytest -import watertap_contrib.reflo.property_models.cryst_prop_pack as props -from pyomo.environ import ConcreteModel -from idaes.core import FlowsheetBlock, ControlVolume0DBlock -from idaes.models.properties.tests.test_harness import ( - PropertyTestHarness as PropertyTestHarness_idaes, -) -from watertap.property_models.tests.property_test_harness import ( - PropertyTestHarness, - PropertyRegressionTest, - PropertyCalculateStateTest, -) - - -# ----------------------------------------------------------------------------- - - -class TestNaClProperty_idaes(PropertyTestHarness_idaes): - def configure(self): - self.prop_pack = props.NaClParameterBlock - self.param_args = {} - self.prop_args = {} - self.has_density_terms = False - - -class TestDefaultNaClwaterProperty: - - # Create block and stream for running default tests - m = ConcreteModel() - m.fs = FlowsheetBlock(dynamic=False) - m.fs.properties = props.NaClParameterBlock( - heat_of_crystallization_model=props.HeatOfCrystallizationModel.constant - ) - m.fs.stream = m.fs.properties.build_state_block([0], defined_state=True) - - m.fs.cv = ControlVolume0DBlock( - dynamic=False, has_holdup=False, property_package=m.fs.properties - ) - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_material_balances() - m.fs.cv.add_energy_balances() - m.fs.cv.add_momentum_balances() - - # Create instance of PropertyTesthARNESS class and add attributes needed for tests - xv = PropertyTestHarness() - - xv.stateblock_statistics = { - "number_variables": 43, - "number_total_constraints": 37, - "number_unused_variables": 0, - "default_degrees_of_freedom": 6, - } - - xv.scaling_args = { - ("flow_mass_phase_comp", ("Liq", "H2O")): 1e0, - ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e1, - ("flow_mol_phase_comp", ("Sol", "NaCl")): 1e2, - ("flow_mol_phase_comp", ("Vap", "H2O")): 1e3, - } - - xv.default_solution = { - ("solubility_mass_phase_comp", ("Liq", "NaCl")): 359.51, - ("solubility_mass_frac_phase_comp", ("Liq", "NaCl")): 0.265, - ("dens_mass_solvent", "Liq"): 996.89, - ("dens_mass_solvent", "Vap"): 0.7363, - ("dens_mass_solute", "Liq"): 3199.471, - ("dens_mass_solute", "Sol"): 2115, - ("dens_mass_phase", "Liq"): 1021.50, - ("dh_vap_mass_solvent", None): 2441.80, - ("cp_mass_solvent", "Liq"): 4186.52, - ("cp_mass_solvent", "Vap"): 1864.52, - ("cp_mass_solute", "Sol"): 864.15, # cp_mass_solute liquid ignored for now - ("cp_mass_phase", "Liq"): 4008.30, - ("flow_vol_phase", "Liq"): 9.79e-4, - ("flow_vol_phase", "Sol"): 0, - ("flow_vol_phase", "Vap"): 0, - ("pressure_sat", None): 2932.43, - ("temperature_sat_solvent", None): 296.79, - ("conc_mass_phase_comp", ("Liq", "NaCl")): 35.753, - ("conc_mass_phase_comp", ("Liq", "H2O")): 985.753, - ("enth_mass_solvent", "Liq"): 104.9212, - ("enth_mass_solvent", "Vap"): 2546.7289, - ("enth_mass_solute", "Sol"): 21.4739, - ("enth_mass_phase", "Liq"): 101.0922, - ("dh_crystallization_mass_comp", "NaCl"): -520, - ("mass_frac_phase_comp", ("Liq", "H2O")): 0.965, - ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.035, - ("mass_frac_phase_comp", ("Sol", "NaCl")): 1.0, - ("mass_frac_phase_comp", ("Vap", "H2O")): 1.0, - ("flow_mol_phase_comp", ("Liq", "H2O")): 53.57, - ("flow_mol_phase_comp", ("Liq", "NaCl")): 0.5989, - ("flow_mol_phase_comp", ("Sol", "NaCl")): 0.0, - ("flow_mol_phase_comp", ("Vap", "H2O")): 0.0, - ("mole_frac_phase_comp", ("Liq", "H2O")): 0.9889, - ("mole_frac_phase_comp", ("Liq", "NaCl")): 0.01106, - ("mole_frac_phase_comp", ("Sol", "NaCl")): 1.0, - ("mole_frac_phase_comp", ("Vap", "H2O")): 1.0, - } - - # Configure class - xv.configure_class(m) - - @pytest.mark.unit - def test_components_phases(self): - self.xv.test_components_phases(self.m) - - @pytest.mark.unit - def test_parameters(self): - self.xv.test_parameters(self.m) - - @pytest.mark.unit - def test_state_variables(self): - self.xv.test_state_variables(self.m) - - @pytest.mark.unit - def test_permanent_properties(self): - self.xv.test_permanent_properties(self.m) - - @pytest.mark.unit - def test_on_demand_properties(self): - self.xv.test_on_demand_properties(self.m) - - @pytest.mark.unit - def test_stateblock_statistics(self): - self.xv.test_stateblock_statistics(self.m) - - @pytest.mark.unit - def test_units_consistent(self): - self.xv.test_units_consistent(self.m) - - @pytest.mark.unit - def test_scaling(self): - self.xv.test_scaling(self.m) - - @pytest.mark.component - def test_default_initialization(self): - self.xv.test_default_initialization(self.m) - - @pytest.mark.component - def test_property_control_volume(self): - self.xv.test_property_control_volume(self.m) - - -@pytest.mark.component -class TestNaClPropertySolution_1(PropertyRegressionTest): - # Test pure liquid solution 1 - same solution as NaCl prop pack - def configure(self): - self.prop_pack = props.NaClParameterBlock - self.param_args = {} - - self.scaling_args = { - ("flow_mass_phase_comp", ("Liq", "H2O")): 1e0, - ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e2, - ("flow_mass_phase_comp", ("Vap", "H2O")): 1e2, - } - - self.state_args = { - ("flow_mass_phase_comp", ("Liq", "H2O")): 0.95, - ("flow_mass_phase_comp", ("Liq", "NaCl")): 0.05, - ("flow_mass_phase_comp", ("Sol", "NaCl")): 0.0, - ("flow_mass_phase_comp", ("Vap", "H2O")): 0.0, - ("temperature", None): 273.15 + 25, - ("pressure", None): 50e5, - } - - self.regression_solution = { - ("mass_frac_phase_comp", ("Liq", "H2O")): 0.95, - ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.05, - ("solubility_mass_phase_comp", ("Liq", "NaCl")): 359.50, - ("solubility_mass_frac_phase_comp", ("Liq", "NaCl")): 0.265, - ("dens_mass_solvent", "Liq"): 996.89, - ("dens_mass_solvent", "Vap"): 36.335, - ("dens_mass_phase", "Liq"): 1032.2, - ("flow_vol_phase", "Liq"): 9.687e-4, - ("conc_mass_phase_comp", ("Liq", "H2O")): 980.6, - ("conc_mass_phase_comp", ("Liq", "NaCl")): 51.61, - ("flow_mol_phase_comp", ("Liq", "H2O")): 52.73, - ("flow_mol_phase_comp", ("Liq", "NaCl")): 0.8556, - ("mole_frac_phase_comp", ("Liq", "H2O")): 0.9840, - ("mole_frac_phase_comp", ("Liq", "NaCl")): 1.597e-2, - ("cp_mass_solvent", "Liq"): 4186.52, - ("cp_mass_phase", "Liq"): 3940.03, - ("enth_mass_solvent", "Liq"): 104.92, - ("enth_mass_phase", "Liq"): 99.45, - } - - -@pytest.mark.component -class TestNaClPropertySolution_2(PropertyRegressionTest): - # Test pure liquid solution 2 - same solution as NaCl prop pack - def configure(self): - self.prop_pack = props.NaClParameterBlock - self.param_args = {} - - self.scaling_args = { - ("flow_mass_phase_comp", ("Liq", "H2O")): 1e0, - ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e2, - ("flow_mass_phase_comp", ("Vap", "H2O")): 1e2, - } - - self.state_args = { - ("flow_mass_phase_comp", ("Liq", "H2O")): 0.74, - ("flow_mass_phase_comp", ("Liq", "NaCl")): 0.26, - ("flow_mass_phase_comp", ("Sol", "NaCl")): 0.0, - ("flow_mass_phase_comp", ("Vap", "H2O")): 0.0, - ("temperature", None): 273.15 + 25, - ("pressure", None): 50e5, - } - - self.regression_solution = { - ("mass_frac_phase_comp", ("Liq", "H2O")): 0.74, - ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.26, - ("solubility_mass_phase_comp", ("Liq", "NaCl")): 359.50, - ("solubility_mass_frac_phase_comp", ("Liq", "NaCl")): 0.265, - ("dens_mass_solvent", "Liq"): 996.89, - ("dens_mass_phase", "Liq"): 1193.4, - ("flow_vol_phase", "Liq"): 8.379e-4, - ("conc_mass_phase_comp", ("Liq", "H2O")): 883.14, - ("conc_mass_phase_comp", ("Liq", "NaCl")): 310.29, - ("flow_mol_phase_comp", ("Liq", "H2O")): 41.08, - ("flow_mol_phase_comp", ("Liq", "NaCl")): 4.449, - ("mole_frac_phase_comp", ("Liq", "H2O")): 0.9022, - ("mole_frac_phase_comp", ("Liq", "NaCl")): 9.773e-2, - ("cp_mass_solvent", "Liq"): 4186.52, - ("cp_mass_phase", "Liq"): 3276.56, - ("enth_mass_solvent", "Liq"): 104.92, - ("enth_mass_phase", "Liq"): 68.78, - } - - -@pytest.mark.component -class TestNaClPropertySolution_3(PropertyRegressionTest): - # Test pure liquid solution 3 - same solution as NaCl prop pack - def configure(self): - self.prop_pack = props.NaClParameterBlock - self.param_args = {} - - self.scaling_args = { - ("flow_mass_phase_comp", ("Liq", "H2O")): 1e0, - ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e3, - ("flow_mass_phase_comp", ("Vap", "H2O")): 1e3, - } - - self.state_args = { - ("flow_mass_phase_comp", ("Liq", "H2O")): 0.999, - ("flow_mass_phase_comp", ("Liq", "NaCl")): 0.001, - ("flow_mass_phase_comp", ("Sol", "NaCl")): 0.0, - ("flow_mass_phase_comp", ("Vap", "H2O")): 0.0, - ("temperature", None): 273.15 + 25, - ("pressure", None): 10e5, - } - - self.regression_solution = { - ("mass_frac_phase_comp", ("Liq", "H2O")): 0.999, - ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.001, - ("solubility_mass_phase_comp", ("Liq", "NaCl")): 359.50, - ("solubility_mass_frac_phase_comp", ("Liq", "NaCl")): 0.265, - ("dens_mass_solvent", "Liq"): 996.89, - ("dens_mass_solvent", "Vap"): 7.267, - ("dens_mass_phase", "Liq"): 997.59, - ("flow_vol_phase", "Liq"): 1.002e-3, - ("conc_mass_phase_comp", ("Liq", "H2O")): 996.59, - ("conc_mass_phase_comp", ("Liq", "NaCl")): 0.9976, - ("flow_mol_phase_comp", ("Liq", "H2O")): 55.45, - ("flow_mol_phase_comp", ("Liq", "NaCl")): 1.711e-2, - ("mole_frac_phase_comp", ("Liq", "H2O")): 0.9997, - ("mole_frac_phase_comp", ("Liq", "NaCl")): 3.084e-4, - ("cp_mass_solvent", "Liq"): 4186.52, - ("cp_mass_phase", "Liq"): 4180.98, - ("enth_mass_solvent", "Liq"): 104.92, - ("enth_mass_phase", "Liq"): 104.40, - } - - -@pytest.mark.requires_idaes_solver -@pytest.mark.component -class TestNaClPropertySolution_4(PropertyRegressionTest): - # Test pure solid solution 1 - check solid properties - def configure(self): - - self.prop_pack = props.NaClParameterBlock - self.param_args = {} - - self.scaling_args = { - ("flow_mass_phase_comp", ("Liq", "H2O")): 1e1, - ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e3, - ("flow_mass_phase_comp", ("Sol", "NaCl")): 1e0, - ("flow_mass_phase_comp", ("Vap", "H2O")): 1e2, - } - - self.state_args = { - ("flow_vol_phase", "Liq"): 0, - ("flow_vol_phase", "Vap"): 0, - ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.05, - ("flow_mass_phase_comp", ("Sol", "NaCl")): 1.0, - ("temperature", None): 273.15 + 25, - ("pressure", None): 5e5, - } - - self.regression_solution = { - ("mass_frac_phase_comp", ("Sol", "NaCl")): 1.0, - ("dens_mass_solute", "Sol"): 2115, - ("cp_mass_solute", "Sol"): 864.16, - ("flow_vol_phase", "Sol"): 1 / 2115, # mass floe / density - ("cp_mass_solute", "Sol"): 864.16, - ("enth_mass_solute", "Sol"): 21.474, - ("dh_crystallization_mass_comp", "NaCl"): -520, - ("flow_mol_phase_comp", ("Sol", "NaCl")): 1 / 58.44e-3, # mass flow / mw - ("mole_frac_phase_comp", ("Sol", "NaCl")): 1.0, - } - - -@pytest.mark.requires_idaes_solver -@pytest.mark.component -class TestNaClPropertySolution_5(PropertyRegressionTest): - # Test pure vapor solution 1 - check vapor properties - def configure(self): - - self.prop_pack = props.NaClParameterBlock - self.param_args = {} - - self.scaling_args = { - ("flow_mass_phase_comp", ("Liq", "H2O")): 1e1, - ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e3, - ("flow_mass_phase_comp", ("Sol", "NaCl")): 1e3, - ("flow_mass_phase_comp", ("Vap", "H2O")): 1e0, - } - - self.state_args = { - ("flow_vol_phase", "Liq"): 0, - ("flow_vol_phase", "Sol"): 0, - ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.05, - ("flow_mass_phase_comp", ("Vap", "H2O")): 1.0, - ("temperature", None): 273.15 + 25, - ("pressure", None): 5e5, - } - - self.regression_solution = { - ("mass_frac_phase_comp", ("Vap", "H2O")): 1.0, - ("dens_mass_solvent", "Vap"): 3.633, - ("dh_vap_mass_solvent", None): 2441.808, - ("cp_mass_solvent", "Vap"): 1864.52, - ("flow_vol_phase", "Vap"): 1 / 3.633, # mass flow / density - ("pressure_sat", None): 2905.28, - ("flow_mol_phase_comp", ("Vap", "H2O")): 1 / 18.01528e-3, # mass flow / mw - ("mole_frac_phase_comp", ("Vap", "H2O")): 1.0, - } - - -@pytest.mark.component -class TestNaClPropertySolution_6(PropertyRegressionTest): - # Test for S-L-V solution 1 with similar magnitude flowrates in all phases and high liquid salt. conc. - check all properties - def configure(self): - self.prop_pack = props.NaClParameterBlock - self.param_args = {} - - self.scaling_args = { - ("flow_mass_phase_comp", ("Sol", "NaCl")): 10, - ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e1, - } - - self.state_args = { - ("flow_mass_phase_comp", ("Liq", "H2O")): 0.75, - ("flow_mass_phase_comp", ("Liq", "NaCl")): 0.25, - ("flow_mass_phase_comp", ("Sol", "NaCl")): 0.25, - ("flow_mass_phase_comp", ("Vap", "H2O")): 0.25, - ("temperature", None): 273.15 + 50, - ("pressure", None): 5e5, - } - - self.regression_solution = { - ("solubility_mass_phase_comp", ("Liq", "NaCl")): 362.93, - ("solubility_mass_frac_phase_comp", ("Liq", "NaCl")): 0.2686, - ("dens_mass_solvent", "Liq"): 988.04, - ("dens_mass_solvent", "Vap"): 3.352, - ("dens_mass_solute", "Liq"): 2645.21, - ("dens_mass_solute", "Sol"): 2115, - ("dens_mass_phase", "Liq"): 1171.53, - ("dh_vap_mass_solvent", None): 2382.08, - ("cp_mass_solvent", "Liq"): 4180.92, - ("cp_mass_solvent", "Vap"): 1871.21, - ("cp_mass_solute", "Sol"): 873.34, - ("cp_mass_phase", "Liq"): 3300.83, - ("flow_vol_phase", "Liq"): 8.53e-4, - ("flow_vol_phase", "Sol"): 1.182e-4, - ("flow_vol_phase", "Vap"): 7.46e-2, - ("pressure_sat", None): 9799.91, - ("temperature_sat_solvent", None): 318.52, - ("conc_mass_phase_comp", ("Liq", "NaCl")): 292.88, - ("conc_mass_phase_comp", ("Liq", "H2O")): 878.65, - ("enth_mass_solvent", "Liq"): 209.40, - ("enth_mass_solvent", "Vap"): 2591.48, - ("enth_mass_solute", "Sol"): 43.19, - ("enth_mass_phase", "Liq"): 152.36, - ("dh_crystallization_mass_comp", "NaCl"): -520, - ("mass_frac_phase_comp", ("Liq", "H2O")): 0.75, - ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.25, - ("mass_frac_phase_comp", ("Sol", "NaCl")): 1, - ("mass_frac_phase_comp", ("Vap", "H2O")): 1, - ("flow_mol_phase_comp", ("Liq", "H2O")): 41.63, - ("flow_mol_phase_comp", ("Liq", "NaCl")): 4.28, - ("flow_mol_phase_comp", ("Sol", "NaCl")): 4.28, - ("flow_mol_phase_comp", ("Vap", "H2O")): 13.88, - ("mole_frac_phase_comp", ("Liq", "H2O")): 0.9068, - ("mole_frac_phase_comp", ("Liq", "NaCl")): 0.0932, - ("mole_frac_phase_comp", ("Sol", "NaCl")): 1.0, - ("mole_frac_phase_comp", ("Vap", "H2O")): 1.0, - } - - -@pytest.mark.component -class TestNaClPropertySolution_7(PropertyRegressionTest): - # Test for S-L-V solution 2 with flowrates in all phases of same magnitude but low liquid salt. conc. - check all properties - def configure(self): - self.prop_pack = props.NaClParameterBlock - self.param_args = {} - - self.scaling_args = { - ("flow_mass_phase_comp", ("Sol", "NaCl")): 10, - ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e3, - } - - self.state_args = { - ("flow_mass_phase_comp", ("Liq", "H2O")): 0.999, - ("flow_mass_phase_comp", ("Liq", "NaCl")): 0.001, - ("flow_mass_phase_comp", ("Sol", "NaCl")): 0.25, - ("flow_mass_phase_comp", ("Vap", "H2O")): 0.25, - ("temperature", None): 273.15 + 50, - ("pressure", None): 5e5, - } - - self.regression_solution = { - ("solubility_mass_phase_comp", ("Liq", "NaCl")): 362.93, - ("solubility_mass_frac_phase_comp", ("Liq", "NaCl")): 0.2686, - ("dens_mass_solvent", "Liq"): 988.04, - ("dens_mass_solvent", "Vap"): 3.352, - ("dens_mass_solute", "Liq"): 3073.05, - ("dens_mass_solute", "Sol"): 2115, - ("dens_mass_phase", "Liq"): 988.72, - ("dh_vap_mass_solvent", None): 2382.08, - ("cp_mass_solvent", "Liq"): 4180.92, - ("cp_mass_solvent", "Vap"): 1871.21, - ("cp_mass_solute", "Sol"): 873.34, - ("cp_mass_phase", "Liq"): 4175.95, - ("flow_vol_phase", "Liq"): 1.011e-3, - ("flow_vol_phase", "Sol"): 1.182e-4, - ("flow_vol_phase", "Vap"): 7.46e-2, - ("pressure_sat", None): 12614.93, - ("temperature_sat_solvent", None): 323.55, - ("conc_mass_phase_comp", ("Liq", "NaCl")): 0.9887, - ("conc_mass_phase_comp", ("Liq", "H2O")): 987.73, - ("enth_mass_solvent", "Liq"): 209.40, - ("enth_mass_solvent", "Vap"): 2591.48, - ("enth_mass_solute", "Sol"): 43.19, - ("enth_mass_phase", "Liq"): 208.81, - ("dh_crystallization_mass_comp", "NaCl"): -520, - ("mass_frac_phase_comp", ("Liq", "H2O")): 0.999, - ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.001, - ("mass_frac_phase_comp", ("Sol", "NaCl")): 1, - ("mass_frac_phase_comp", ("Vap", "H2O")): 1, - ("flow_mol_phase_comp", ("Liq", "H2O")): 55.45, - ("flow_mol_phase_comp", ("Liq", "NaCl")): 0.0171, - ("flow_mol_phase_comp", ("Sol", "NaCl")): 4.28, - ("flow_mol_phase_comp", ("Vap", "H2O")): 13.88, - ("mole_frac_phase_comp", ("Liq", "H2O")): 0.999, - ("mole_frac_phase_comp", ("Liq", "NaCl")): 3.084e-4, - ("mole_frac_phase_comp", ("Sol", "NaCl")): 1.0, - ("mole_frac_phase_comp", ("Vap", "H2O")): 1.0, - } - - -@pytest.mark.component -class TestNaClPropertySolution_8(PropertyRegressionTest): - # Test for S-L-V solution 3 with outlet data from Dutta et al. - proper crystallization system - # # Dutta recorded properties for solids and liquids at crystallizer temperature: - # # Solubility @ 55C: 0.27 kg/kg - # # Heat of vaporization @ 55C: 2400 kJ/kg - # # Vapor density @ 55C: 68.7e-3 kg/m3 - # # Solid heat capacity: 877 J/kgK - # # Liquid heat capacity @ 20 C: 3290 kJ/kgK - # # Liquid density @ 20 C : 1185 kg/m3 - - def configure(self): - self.prop_pack = props.NaClParameterBlock - self.param_args = {} - - self.scaling_args = { - ("flow_mass_phase_comp", ("Sol", "NaCl")): 1e0, - ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e0, - ("flow_mass_phase_comp", ("Vap", "H2O")): 1e-1, - } - - self.state_args = { - ("flow_mass_phase_comp", ("Liq", "H2O")): 18.37, # 84t/h - ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.2126, - ("flow_mass_phase_comp", ("Sol", "NaCl")): 5.55, # 20t/h - ("flow_mass_phase_comp", ("Vap", "H2O")): 20.55, # 74t/h - ("temperature", None): 273.15 + 20, - ("pressure", None): 10e3, - } - - self.regression_solution = { - ("solubility_mass_phase_comp", ("Liq", "NaCl")): 358.88, - ("solubility_mass_frac_phase_comp", ("Liq", "NaCl")): 0.2644, - ("dens_mass_solvent", "Liq"): 998.02, - ("dens_mass_solvent", "Vap"): 73.91e-3, - ("dens_mass_solute", "Liq"): 2847.63, - ("dens_mass_solute", "Sol"): 2115, - ("dens_mass_phase", "Liq"): 1157.91, - ("dh_vap_mass_solvent", None): 2453.66, - ("cp_mass_solvent", "Liq"): 4189.43, - ("cp_mass_solvent", "Vap"): 1863.46, - ("cp_mass_solute", "Sol"): 862.16, - ("cp_mass_phase", "Liq"): 3380.07, - ("flow_vol_phase", "Liq"): 0.02015, - ("flow_vol_phase", "Sol"): 2.624e-3, - ("flow_vol_phase", "Vap"): 278.04, - ("pressure_sat", None): 1679.64, - ("temperature_sat_solvent", None): 287.88, - ("conc_mass_phase_comp", ("Liq", "NaCl")): 246.17, - ("conc_mass_phase_comp", ("Liq", "H2O")): 911.74, - ("enth_mass_solvent", "Liq"): 84.00, - ("enth_mass_solvent", "Vap"): 2537.66, - ("enth_mass_solute", "Sol"): 17.16, - ("enth_mass_phase", "Liq"): 59.03, - ("dh_crystallization_mass_comp", "NaCl"): -520, - ("mass_frac_phase_comp", ("Liq", "H2O")): 0.7874, - ("mass_frac_phase_comp", ("Sol", "NaCl")): 1, - ("mass_frac_phase_comp", ("Vap", "H2O")): 1, - ("flow_mol_phase_comp", ("Liq", "H2O")): 1019.69, - ("flow_mol_phase_comp", ("Liq", "NaCl")): 84.88, - ("flow_mol_phase_comp", ("Sol", "NaCl")): 94.97, - ("flow_mol_phase_comp", ("Vap", "H2O")): 1140.698, - ("mole_frac_phase_comp", ("Liq", "H2O")): 0.9232, - ("mole_frac_phase_comp", ("Liq", "NaCl")): 0.0768, - ("mole_frac_phase_comp", ("Sol", "NaCl")): 1.0, - ("mole_frac_phase_comp", ("Vap", "H2O")): 1.0, - ("flow_mass_phase_comp", ("Liq", "NaCl")): 4.96, - } - - -@pytest.mark.component -class TestNaClPropertySolution_9(PropertyRegressionTest): - # Test for S-L-V solution 4 with outlet data from Dutta et al. - proper crystallization system - # # Dutta recorded properties for solids and liquids at crystallizer temperature: - # # Solubility @ 55C: 0.27 kg/kg - # # Heat of vaporization @ 55C: 2400 kJ/kg - # # Vapor density @ 55C: 68.7e-3 kg/m3 - # # Solid heat capacity: 877 J/kgK - # # Liquid heat capacity @ 20 C: 3290 kJ/kgK - # # Liquid density @ 20 C : 1185 kg/m3 - - def configure(self): - self.prop_pack = props.NaClParameterBlock - self.param_args = {} - - self.scaling_args = { - ("flow_mass_phase_comp", ("Sol", "NaCl")): 1e0, - ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e0, - ("flow_mass_phase_comp", ("Vap", "H2O")): 1e-1, - } - - self.state_args = { - ("flow_mass_phase_comp", ("Liq", "H2O")): 18.37, # 84t/h - ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.2126, - ("flow_mass_phase_comp", ("Sol", "NaCl")): 5.55, # 20t/h - ("flow_mass_phase_comp", ("Vap", "H2O")): 20.55, # 74t/h - ("temperature", None): 273.15 + 55, - ("pressure", None): 10e3, - } - - self.regression_solution = { - ("solubility_mass_phase_comp", ("Liq", "NaCl")): 363.71, - ("solubility_mass_frac_phase_comp", ("Liq", "NaCl")): 0.2695, - ("dens_mass_solvent", "Liq"): 985.71, - ("dens_mass_solvent", "Vap"): 66.03e-3, - ("dens_mass_solute", "Liq"): 2694.61, - ("dens_mass_solute", "Sol"): 2115, - ("dens_mass_phase", "Liq"): 1139.33, - ("dh_vap_mass_solvent", None): 2369.98, - ("cp_mass_solvent", "Liq"): 4181.59, - ("cp_mass_solvent", "Vap"): 1872.79, - ("cp_mass_solute", "Sol"): 875.04, - ("cp_mass_phase", "Liq"): 3390.43, - ("flow_vol_phase", "Liq"): 0.02047, - ("flow_vol_phase", "Sol"): 2.624e-3, - ("flow_vol_phase", "Vap"): 311.233, - ("pressure_sat", None): 13201.77, - ("temperature_sat_solvent", None): 324.47, - ("conc_mass_phase_comp", ("Liq", "NaCl")): 242.22, - ("conc_mass_phase_comp", ("Liq", "H2O")): 897.107, - ("enth_mass_solvent", "Liq"): 230.30, - ("enth_mass_solvent", "Vap"): 2600.279, - ("enth_mass_solute", "Sol"): 47.57, - ("enth_mass_phase", "Liq"): 177.39, - ("dh_crystallization_mass_comp", "NaCl"): -520, - ("mass_frac_phase_comp", ("Liq", "H2O")): 0.7874, - ("mass_frac_phase_comp", ("Sol", "NaCl")): 1, - ("mass_frac_phase_comp", ("Vap", "H2O")): 1, - ("flow_mol_phase_comp", ("Liq", "H2O")): 1019.69, - ("flow_mol_phase_comp", ("Liq", "NaCl")): 84.88, - ("flow_mol_phase_comp", ("Sol", "NaCl")): 94.97, - ("flow_mol_phase_comp", ("Vap", "H2O")): 1140.698, - ("mole_frac_phase_comp", ("Liq", "H2O")): 0.9232, - ("mole_frac_phase_comp", ("Liq", "NaCl")): 0.0768, - ("mole_frac_phase_comp", ("Sol", "NaCl")): 1.0, - ("mole_frac_phase_comp", ("Vap", "H2O")): 1.0, - ("flow_mass_phase_comp", ("Liq", "NaCl")): 4.96, - } - - -@pytest.mark.component -class TestNaClCalculateState_1(PropertyCalculateStateTest): - # Test pure liquid solution with mass fractions - def configure(self): - self.prop_pack = props.NaClParameterBlock - self.param_args = {} - - self.scaling_args = { - ("flow_mass_phase_comp", ("Liq", "H2O")): 1e-1, - ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e0, - ("flow_mass_phase_comp", ("Vap", "H2O")): 1, - ("flow_mass_phase_comp", ("Sol", "NaCl")): 1, - } - - self.var_args = { - ("flow_vol_phase", "Liq"): 2e-2, - ("flow_vol_phase", "Sol"): 0, - ("flow_vol_phase", "Vap"): 0, - ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.05, - ("temperature", None): 273.15 + 25, - ("pressure", None): 5e5, - } - - self.state_solution = { - ("flow_mass_phase_comp", ("Liq", "H2O")): 19.6, - ("flow_mass_phase_comp", ("Liq", "NaCl")): 1.032, - ("flow_mass_phase_comp", ("Sol", "NaCl")): 0, - ("flow_mass_phase_comp", ("Vap", "H2O")): 0, - } - - -@pytest.mark.component -class TestNaClCalculateState_2(PropertyCalculateStateTest): - # Test pure liquid with mole fractions - def configure(self): - self.prop_pack = props.NaClParameterBlock - self.param_args = {} - - self.scaling_args = { - ("flow_mass_phase_comp", ("Liq", "H2O")): 1e1, - ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e2, - # The rest are expected to be zero - ("flow_mass_phase_comp", ("Sol", "NaCl")): 1, - ("flow_mass_phase_comp", ("Vap", "H2O")): 1, - } - self.var_args = { - ("flow_vol_phase", "Liq"): 2e-4, - ("flow_vol_phase", "Sol"): 0, - ("flow_vol_phase", "Vap"): 0, - ("mole_frac_phase_comp", ("Liq", "NaCl")): 0.05, - ("temperature", None): 273.15 + 25, - ("pressure", None): 5e5, - } - self.state_solution = { - ("flow_mass_phase_comp", ("Liq", "H2O")): 18.84e-2, - ("flow_mass_phase_comp", ("Liq", "NaCl")): 3.215e-2, - ("flow_mass_phase_comp", ("Sol", "NaCl")): 0, - ("flow_mass_phase_comp", ("Vap", "H2O")): 0, - } - - -@pytest.mark.component -class TestNaClCalculateState_3(PropertyCalculateStateTest): - # Test pure liquid solution with pressure_sat defined instead of temperature - def configure(self): - self.prop_pack = props.NaClParameterBlock - self.param_args = {} - - self.scaling_args = { - ("flow_mass_phase_comp", ("Liq", "H2O")): 1e0, - ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e1, - ("flow_mass_phase_comp", ("Sol", "NaCl")): 1e-1, - ("flow_mass_phase_comp", ("Vap", "H2O")): 1e2, - } - - self.var_args = { - ("flow_vol_phase", "Liq"): 2e-2, - ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.05, - ("flow_vol_phase", "Sol"): 0, - ("flow_vol_phase", "Vap"): 0, - ("pressure_sat", None): 2905, - ("pressure", None): 5e5, - } - - self.state_solution = { - ("flow_mass_phase_comp", ("Liq", "H2O")): 19.6, - ("flow_mass_phase_comp", ("Liq", "NaCl")): 1.032, - ("flow_mass_phase_comp", ("Sol", "NaCl")): 0, - ("flow_mass_phase_comp", ("Vap", "H2O")): 0, - ("temperature", None): 273.15 + 25, - } - - -@pytest.mark.component -class TestNaClCalculateState_4(PropertyCalculateStateTest): - # Test pure solid solution with mass fractions - def configure(self): - - self.prop_pack = props.NaClParameterBlock - self.param_args = {} - - self.scaling_args = { - ("flow_mass_phase_comp", ("Sol", "NaCl")): 1e-1, - ("flow_mass_phase_comp", ("Vap", "H2O")): 1e-1, - } - - self.var_args = { - ("flow_vol_phase", "Liq"): 0, - ("mass_frac_phase_comp", ("Liq", "NaCl")): 0, - ("flow_vol_phase", "Sol"): 2e-2, - ("flow_vol_phase", "Vap"): 0, - ("temperature", None): 273.15 + 25, - ("pressure", None): 5e5, - } - - self.state_solution = { - ("flow_mass_phase_comp", ("Sol", "NaCl")): 2115 - * 2e-2, # solid density is constant - ("flow_mass_phase_comp", ("Liq", "H2O")): 0, - ("flow_mass_phase_comp", ("Liq", "NaCl")): 0, - ("flow_mass_phase_comp", ("Vap", "H2O")): 0, - } - - -@pytest.mark.component -class TestNaClCalculateState_5(PropertyCalculateStateTest): - # Test solid-liquid-vapor mixture solution with mass fractions - def configure(self): - self.prop_pack = props.NaClParameterBlock - self.param_args = {} - - self.scaling_args = { - ("flow_mass_phase_comp", ("Liq", "H2O")): 1e-1, - ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e1, - ("flow_mass_phase_comp", ("Sol", "NaCl")): 1e-2, - } - - self.var_args = { - ("flow_vol_phase", "Liq"): 2e-2, - ("mass_frac_phase_comp", ("Liq", "NaCl")): 0.05, - ("flow_vol_phase", "Sol"): 2e-2, - ("flow_vol_phase", "Vap"): 2e-2, - ("temperature", None): 273.15 + 25, - ("pressure", None): 5e5, - } - - self.state_solution = { - ("flow_mass_phase_comp", ("Liq", "H2O")): 19.6, - ("flow_mass_phase_comp", ("Liq", "NaCl")): 1.032, - ("flow_mass_phase_comp", ("Vap", "H2O")): 0.07265, - ("flow_mass_phase_comp", ("Sol", "NaCl")): 42.3, - } - - -@pytest.mark.component -class TestNaClCalculateState_6(PropertyCalculateStateTest): - # Test liquid-solid-vapor mixture with mole fractions - def configure(self): - self.prop_pack = props.NaClParameterBlock - self.param_args = {} - - self.scaling_args = { - ("flow_mass_phase_comp", ("Liq", "H2O")): 1e1, - ("flow_mass_phase_comp", ("Liq", "NaCl")): 1e2, - ("flow_mass_phase_comp", ("Sol", "NaCl")): 1e2, - ("flow_mass_phase_comp", ("Vap", "H2O")): 1e3, - } - - self.var_args = { - ("flow_vol_phase", "Liq"): 2e-4, - ("flow_vol_phase", "Sol"): 2e-4, - ("flow_vol_phase", "Vap"): 2e-4, - ("mole_frac_phase_comp", ("Liq", "NaCl")): 0.05, - ("temperature", None): 273.15 + 25, - ("pressure", None): 5e5, - } - - self.state_solution = { - ("flow_mass_phase_comp", ("Liq", "H2O")): 18.84e-2, - ("flow_mass_phase_comp", ("Liq", "NaCl")): 3.215e-2, - ("flow_mass_phase_comp", ("Sol", "NaCl")): 0.423, - ("flow_mass_phase_comp", ("Vap", "H2O")): 3.632 - * 2e-4, # Density from ideal gas law * vol. flow - } From 3e9360767cbc4615ecd8236b3eb56b5e85704a85 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Wed, 25 Sep 2024 09:08:45 -0600 Subject: [PATCH 44/80] remove watertap crystallizer and test --- .../zero_order/crystallizer_zo_watertap.py | 870 ------------------ .../tests/test_crystallizer_watertap.py | 617 ------------- 2 files changed, 1487 deletions(-) delete mode 100644 src/watertap_contrib/reflo/unit_models/zero_order/crystallizer_zo_watertap.py delete mode 100644 src/watertap_contrib/reflo/unit_models/zero_order/tests/test_crystallizer_watertap.py diff --git a/src/watertap_contrib/reflo/unit_models/zero_order/crystallizer_zo_watertap.py b/src/watertap_contrib/reflo/unit_models/zero_order/crystallizer_zo_watertap.py deleted file mode 100644 index 77cccf5b..00000000 --- a/src/watertap_contrib/reflo/unit_models/zero_order/crystallizer_zo_watertap.py +++ /dev/null @@ -1,870 +0,0 @@ -################################################################################# -# WaterTAP Copyright (c) 2020-2024, The Regents of the University of California, -# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, -# National Renewable Energy Laboratory, and National Energy Technology -# Laboratory (subject to receipt of any required approvals from the U.S. Dept. -# of Energy). All rights reserved. -# -# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license -# information, respectively. These files are also available online at the URL -# "https://github.com/watertap-org/watertap/" -################################################################################# - -from copy import deepcopy - -# Import Pyomo libraries -from pyomo.environ import ( - Var, - check_optimal_termination, - Param, - Constraint, - Suffix, - units as pyunits, -) -from pyomo.common.config import ConfigBlock, ConfigValue, In - -# Import IDAES cores -from idaes.core import ( - declare_process_block_class, - UnitModelBlockData, - useDefault, -) -from watertap.core.solvers import get_solver -from idaes.core.util.tables import create_stream_table_dataframe -from idaes.core.util.constants import Constants -from idaes.core.util.config import is_physical_parameter_block - -from idaes.core.util.exceptions import InitializationError - -import idaes.core.util.scaling as iscale -import idaes.logger as idaeslog - -from watertap.core import InitializationMixin -from watertap.core.util.initialization import interval_initializer -from watertap_contrib.reflo.costing.units.crystallizer_watertap import ( - cost_crystallizer_watertap, -) - -_log = idaeslog.getLogger(__name__) - -__author__ = "Oluwamayowa Amusat" - - -# when using this file the name "Filtration" is what is imported -@declare_process_block_class("Crystallization") -class CrystallizationData(InitializationMixin, UnitModelBlockData): - """ - Zero order crystallization model - """ - - # CONFIG are options for the unit model, this simple model only has the mandatory config options - 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. The filtration unit does not support dynamic - behavior, thus this must be False.""", - ), - ) - - 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. The filtration unit does not have defined volume, thus - this must be False.""", - ), - ) - - CONFIG.declare( - "property_package", - ConfigValue( - default=useDefault, - domain=is_physical_parameter_block, - description="Property package to use for control volume", - doc="""Property parameter object used to define property calculations, - **default** - useDefault. - **Valid values:** { - **useDefault** - use default package from parent model or flowsheet, - **PhysicalParameterObject** - a PhysicalParameterBlock object.}""", - ), - ) - - CONFIG.declare( - "property_package_args", - ConfigBlock( - implicit=True, - description="Arguments to use for constructing property packages", - doc="""A ConfigBlock with arguments to be passed to a property block(s) - and used when constructing these, - **default** - None. - **Valid values:** { - see property package for documentation.}""", - ), - ) - - def build(self): - super().build() - - solvent_set = self.config.property_package.solvent_set - solute_set = self.config.property_package.solute_set - - # this creates blank scaling factors, which are populated later - self.scaling_factor = Suffix(direction=Suffix.EXPORT) - - # Next, get the base units of measurement from the property definition - units_meta = self.config.property_package.get_metadata().get_derived_units - - # Add unit variables - - self.approach_temperature_heat_exchanger = Param( - initialize=4, - units=pyunits.K, - doc="Maximum temperature difference between inlet and outlet of a crystallizer heat exchanger.\ - Lewis et al. suggests 1-2 degC but use 5degC in example; Tavare example used 4 degC.\ - Default is 4 degC", - ) - - # ====== Crystallizer sizing parameters ================= # - self.dimensionless_crystal_length = Param( - initialize=3.67, # Parameter from population balance modeling for median crystal size - units=pyunits.dimensionless, - ) - - self.crystal_median_length = Var( - initialize=0.5e-3, # From Mersmann et al., Tavare et al. example - bounds=( - 0.2e-3, - 0.6e-3, - ), # Limits for FC crystallizers based on Bermingham et al. - units=pyunits.m, - doc="Desired median crystal size, m", - ) - - self.crystal_growth_rate = Var( - initialize=3.7e-8, # From Mersmann et al. for NaCl. Perry has values between 0.5e-8 to 13e-8 for NaCl - bounds=(1e-9, 1e-6), # Based on Mersmann and Kind diagram. - units=pyunits.m / pyunits.s, - doc="Crystal growth rate, m/s", - ) - - self.souders_brown_constant = Var( - initialize=0.04, - units=pyunits.m / pyunits.s, - doc="Constant for Souders-Brown equation, set at 0.04 m/s based on Dutta et al. \ - Lewis et al suggests 0.024 m/s, while Tavare suggests about 0.06 m/s ", - ) - - # ====== Model variables ================= # - self.crystallization_yield = Var( - solute_set, - initialize=0.5, - bounds=(0.0, 1), - units=pyunits.dimensionless, - doc="Crystallizer solids yield", - ) - - self.product_volumetric_solids_fraction = Var( - initialize=0.25, - bounds=(0.0, 1), - units=pyunits.dimensionless, - doc="Volumetric fraction of solids in slurry product (i.e. solids-liquid mixture).", - ) - - self.temperature_operating = Var( - initialize=298.15, - bounds=(273, 1000), - units=pyunits.K, - doc="Crystallizer operating temperature: boiling point of the solution.", - ) - - self.pressure_operating = Var( - initialize=1e3, - bounds=(0.001, 1e6), - units=pyunits.Pa, - doc="Operating pressure of the crystallizer.", - ) - - self.dens_mass_magma = Var( - initialize=250, - bounds=(1, 5000), - units=pyunits.kg / pyunits.m**3, - doc="Magma density, i.e. mass of crystals per unit volume of suspension", - ) - - self.dens_mass_slurry = Var( - initialize=1000, - bounds=(1, 5000), - units=pyunits.kg / pyunits.m**3, - doc="Suspension density, i.e. density of solid-liquid mixture before separation", - ) - - self.work_mechanical = Var( - self.flowsheet().config.time, - initialize=1e5, - bounds=(-5e6, 5e6), - units=pyunits.kJ / pyunits.s, - doc="Crystallizer thermal energy requirement", - ) - - self.energy_flow_superheated_vapor = Var( - initialize=1e5, - bounds=(-5e6, 5e6), - units=pyunits.kJ / pyunits.s, - doc="Energy could be supplied from vapor", - ) - - self.diameter_crystallizer = Var( - initialize=3, - bounds=(0, 25), - units=pyunits.m, - doc="Diameter of crystallizer", - ) - - self.height_slurry = Var( - initialize=3, - bounds=(0, 25), - units=pyunits.m, - doc="Slurry height in crystallizer", - ) - - self.height_crystallizer = Var( - initialize=3, bounds=(0, 25), units=pyunits.m, doc="Crystallizer height" - ) - - self.magma_circulation_flow_vol = Var( - initialize=1, - bounds=(0, 100), - units=pyunits.m**3 / pyunits.s, - doc="Minimum circulation flow rate through crystallizer heat exchanger", - ) - - self.relative_supersaturation = Var( - solute_set, initialize=0.1, bounds=(0, 100), units=pyunits.dimensionless - ) - - self.t_res = Var( - initialize=1, - bounds=(0, 10), - units=pyunits.hr, - doc="Residence time in crystallizer", - ) - - self.volume_suspension = Var( - initialize=1, - bounds=(0, None), - units=pyunits.m**3, - doc="Crystallizer minimum active volume, i.e. volume of liquid-solid suspension", - ) - - # Add state blocks for inlet, outlet, and waste - # These include the state variables and any other properties on demand - # Add inlet block - tmp_dict = dict(**self.config.property_package_args) - tmp_dict["has_phase_equilibrium"] = False - tmp_dict["parameters"] = self.config.property_package - tmp_dict["defined_state"] = True # inlet block is an inlet - self.properties_in = self.config.property_package.state_block_class( - self.flowsheet().config.time, doc="Material properties of inlet", **tmp_dict - ) - - # Add outlet and waste block - tmp_dict["defined_state"] = False # outlet and waste block is not an inlet - self.properties_out = self.config.property_package.state_block_class( - self.flowsheet().config.time, - doc="Material properties of liquid outlet", - **tmp_dict, - ) - - self.properties_solids = self.config.property_package.state_block_class( - self.flowsheet().config.time, - doc="Material properties of solid crystals at outlet", - **tmp_dict, - ) - - self.properties_vapor = self.config.property_package.state_block_class( - self.flowsheet().config.time, - doc="Material properties of water vapour at outlet", - **tmp_dict, - ) - - self.properties_pure_water = self.config.property_package.state_block_class( - self.flowsheet().config.time, - doc="Material properties of pure water vapour at outlet", - **tmp_dict, - ) - - # Add ports - oftentimes users interact with these rather than the state blocks - self.add_port(name="inlet", block=self.properties_in) - self.add_port(name="outlet", block=self.properties_out) - self.add_port(name="solids", block=self.properties_solids) - self.add_port(name="vapor", block=self.properties_vapor) - - # Add constraints - # 1. Material balances - @self.Constraint( - self.config.property_package.component_list, - doc="Mass balance for components", - ) - def eq_mass_balance_constraints(b, j): - return sum( - b.properties_in[0].flow_mass_phase_comp[p, j] - for p in self.config.property_package.phase_list - if (p, j) in b.properties_in[0].phase_component_set - ) == sum( - b.properties_out[0].flow_mass_phase_comp[p, j] - for p in self.config.property_package.phase_list - if (p, j) in b.properties_out[0].phase_component_set - ) + sum( - b.properties_vapor[0].flow_mass_phase_comp[p, j] - for p in self.config.property_package.phase_list - if (p, j) in b.properties_vapor[0].phase_component_set - ) + sum( - b.properties_solids[0].flow_mass_phase_comp[p, j] - for p in self.config.property_package.phase_list - if (p, j) in b.properties_solids[0].phase_component_set - ) - - @self.Constraint() - def eq_pure_vapor_flow_rate(b): - return ( - b.properties_pure_water[0].flow_mass_phase_comp["Vap", "H2O"] - == b.properties_vapor[0].flow_mass_phase_comp["Vap", "H2O"] - ) - - self.properties_pure_water[0].flow_mass_phase_comp["Liq", "H2O"].fix(1e-8) - self.properties_pure_water[0].flow_mass_phase_comp["Liq", "NaCl"].fix(0) - self.properties_pure_water[0].flow_mass_phase_comp["Sol", "NaCl"].fix(0) - self.properties_pure_water[0].mass_frac_phase_comp["Liq", "NaCl"] - self.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] - self.properties_in[0].flow_vol_phase["Liq"] - - # 2. Constraint on outlet liquid composition based on solubility requirements - @self.Constraint( - self.config.property_package.component_list, - doc="Solubility vs mass fraction constraint", - ) - def eq_solubility_massfrac_equality_constraint(b, j): - if j in solute_set: - return ( - b.properties_out[0].mass_frac_phase_comp["Liq", j] - - b.properties_out[0].solubility_mass_frac_phase_comp["Liq", j] - == 0 - ) - else: - return Constraint.Skip - - # 3. Performance equations - # (a) based on yield - @self.Constraint( - self.config.property_package.component_list, - doc="Component salt yield equation", - ) - def eq_removal_balance(b, j): - if j in solvent_set: - return Constraint.Skip - else: - return ( - b.properties_in[0].flow_mass_phase_comp["Liq", j] - * b.crystallization_yield[j] - == b.properties_in[0].flow_mass_phase_comp["Liq", j] - - b.properties_out[0].flow_mass_phase_comp["Liq", j] - ) - - # (b) Volumetric fraction constraint - @self.Constraint(doc="Solid volumetric fraction in outlet: constraint, 1-E") - def eq_vol_fraction_solids(b): - return self.product_volumetric_solids_fraction == b.properties_solids[ - 0 - ].flow_vol / ( - b.properties_solids[0].flow_vol + b.properties_out[0].flow_vol - ) - - # (c) Magma density constraint - @self.Constraint(doc="Slurry magma density") - def eq_dens_magma(b): - return ( - self.dens_mass_magma - == b.properties_solids[0].dens_mass_solute["Sol"] - * self.product_volumetric_solids_fraction - ) - - # (d) Operating pressure constraint - @self.Constraint(doc="Operating pressure constraint") - def eq_operating_pressure_constraint(b): - return self.pressure_operating - b.properties_out[0].pressure_sat == 0 - - # (e) Relative supersaturation - @self.Constraint( - solute_set, - doc="Relative supersaturation created via evaporation, g/g (solution)", - ) - def eq_relative_supersaturation(b, j): - # mass_frac_after_evap = SOLIDS IN + LIQUID IN - VAPOUR OUT - mass_frac_after_evap = b.properties_in[0].flow_mass_phase_comp["Liq", j] / ( - sum( - b.properties_in[0].flow_mass_phase_comp["Liq", k] - for k in solute_set - ) - + b.properties_in[0].flow_mass_phase_comp["Liq", "H2O"] - - b.properties_vapor[0].flow_mass_phase_comp["Vap", "H2O"] - ) - # return (b.relative_supersaturation[j] * b.properties_out[0].solubility_mass_frac_phase_comp['Liq', j] == - # (mass_frac_after_evap - b.properties_out[0].solubility_mass_frac_phase_comp['Liq', j]) - # ) - return ( - b.relative_supersaturation[j] - == ( - mass_frac_after_evap - - b.properties_out[0].solubility_mass_frac_phase_comp["Liq", j] - ) - / b.properties_out[0].solubility_mass_frac_phase_comp["Liq", j] - ) - - # 4. Fix flows of empty solid, liquid and vapour streams - # (i) Fix solids: liquid and vapour flows must be zero - for p, j in self.properties_solids[0].phase_component_set: - if p != "Sol": - self.properties_solids[0].flow_mass_phase_comp[p, j].fix(1e-8) - - # (ii) Fix liquids: solid and vapour flows must be zero - for p, j in self.properties_out[0].phase_component_set: - if p != "Liq": - self.properties_out[0].flow_mass_phase_comp[p, j].fix(1e-8) - - # (iii) Fix vapor: solid and vapour flows must be zero. - for p, j in self.properties_vapor[0].phase_component_set: - if p != "Vap": - self.properties_vapor[0].flow_mass_phase_comp[p, j].fix(1e-8) - - # 5. Add an energy balance for the system - ## (iii) Enthalpy balance: based on Lewis et al. Enthalpy is exothermic, and the hC in property package is -ve - @self.Constraint(doc="Enthalpy balance over crystallization system") - def eq_enthalpy_balance(b): - return ( - b.properties_in[0].enth_flow - - b.properties_out[0].enth_flow - - b.properties_vapor[0].enth_flow - - b.properties_solids[0].enth_flow - + self.work_mechanical[0] - - sum( - b.properties_solids[0].flow_mass_phase_comp["Sol", j] - * b.properties_solids[0].dh_crystallization_mass_comp[j] - for j in solute_set - ) - == 0 - ) - - # 6. Pressure and temperature balances - what is the pressure of the outlet solid and vapour? - # TO-DO: Figure out actual liquid and solid pressures. - @self.Constraint() - def eq_p_con1(b): - return b.properties_in[0].pressure == b.properties_out[0].pressure - - @self.Constraint() - def eq_p_con2(b): - return b.properties_in[0].pressure == b.properties_solids[0].pressure - - @self.Constraint() - def eq_p_con3(b): - return b.properties_vapor[0].pressure == self.pressure_operating - - @self.Constraint() - def eq_T_con1(b): - return self.temperature_operating == b.properties_solids[0].temperature - - @self.Constraint() - def eq_T_con2(b): - return self.temperature_operating == b.properties_vapor[0].temperature - - @self.Constraint() - def eq_T_con3(b): - return self.temperature_operating == b.properties_out[0].temperature - - @self.Constraint() - def eq_p_con4(b): - return b.properties_pure_water[0].pressure == self.pressure_operating - - @self.Constraint() - def eq_p_con5(b): - return b.properties_pure_water[0].pressure_sat == self.pressure_operating - - # 7. Heat exchanger minimum circulation flow rate calculations - see Lewis et al. or Tavare et al. - @self.Constraint( - doc="Constraint on mimimum circulation rate through crystallizer heat exchanger" - ) - def eq_minimum_hex_circulation_rate_constraint(b): - dens_cp_avg = self.approach_temperature_heat_exchanger * ( - b.product_volumetric_solids_fraction - * b.properties_solids[0].dens_mass_solute["Sol"] - * b.properties_solids[0].cp_mass_solute["Sol"] - + (1 - b.product_volumetric_solids_fraction) - * b.properties_out[0].dens_mass_phase["Liq"] - * b.properties_out[0].cp_mass_phase["Liq"] - ) - return b.magma_circulation_flow_vol * dens_cp_avg == pyunits.convert( - b.work_mechanical[0], to_units=pyunits.J / pyunits.s - ) - - # 8. Suspension density - @self.Constraint(doc="Slurry density calculation") - def eq_dens_mass_slurry(b): - return ( - self.dens_mass_slurry - == b.product_volumetric_solids_fraction - * b.properties_solids[0].dens_mass_solute["Sol"] - + (1 - b.product_volumetric_solids_fraction) - * b.properties_out[0].dens_mass_phase["Liq"] - ) - - # 9. Residence time calculation - @self.Constraint(doc="Residence time") - def eq_residence_time(b): - return b.t_res == b.crystal_median_length / ( - b.dimensionless_crystal_length - * pyunits.convert( - b.crystal_growth_rate, to_units=pyunits.m / pyunits.hr - ) - ) - - # 10. Suspension volume calculation - @self.Constraint(doc="Suspension volume") - def eq_suspension_volume(b): - return b.volume_suspension == ( - b.properties_solids[0].flow_vol + b.properties_out[0].flow_vol - ) * pyunits.convert(b.t_res, to_units=pyunits.s) - - # 11. Minimum diameter of evaporation zone - @self.Expression(doc="maximum allowable vapour linear velocity in m/s") - def eq_max_allowable_velocity(b): - return ( - b.souders_brown_constant - * ( - b.properties_out[0].dens_mass_phase["Liq"] - / b.properties_vapor[0].dens_mass_solvent["Vap"] - ) - ** 0.5 - ) - - @self.Constraint( - doc="Crystallizer diameter (based on minimum diameter of evaporation zone)" - ) - def eq_vapor_head_diameter_constraint(b): - return ( - self.diameter_crystallizer - == ( - 4 - * b.properties_vapor[0].flow_vol_phase["Vap"] - / (Constants.pi * b.eq_max_allowable_velocity) - ) - ** 0.5 - ) - - # 12. Minimum crystallizer height - @self.Constraint(doc="Slurry height based on crystallizer diameter") - def eq_slurry_height_constraint(b): - return self.height_slurry == 4 * b.volume_suspension / ( - Constants.pi * b.diameter_crystallizer**2 - ) - - @self.Expression( - doc="Recommended height of vapor space (0.75*D) based on Tavares et. al." - ) - def eq_vapor_space_height(b): - return 0.75 * b.diameter_crystallizer - - @self.Expression( - doc="Height to diameter ratio constraint for evaporative crystallizers (Wilson et. al.)" - ) - def eq_minimum_height_diameter_ratio(b): - return 1.5 * b.diameter_crystallizer - - @self.Constraint(doc="Crystallizer height") - def eq_crystallizer_height_constraint(b): - # Height is max(). Manual smooth max implementation used here: max(a,b) = 0.5(a + b + |a-b|) - a = b.eq_vapor_space_height + b.height_slurry - b = b.eq_minimum_height_diameter_ratio - eps = 1e-20 * pyunits.m - return self.height_crystallizer == 0.5 * ( - a + b + ((a - b) ** 2 + eps**2) ** 0.5 - ) - - # 13. Energy available from the vapor - @self.Constraint(doc="Thermal energy in the vapor") - def eq_vapor_energy_constraint(b): - return b.energy_flow_superheated_vapor == ( - b.properties_vapor[0].flow_mass_phase_comp["Vap", "H2O"] - * ( - b.properties_pure_water[ - 0 - ].dh_vap_mass_solvent # Latent heat from the vapor - + b.properties_vapor[0].enth_mass_solvent["Vap"] - - b.properties_pure_water[0].enth_mass_solvent["Vap"] - ) - ) - - def initialize_build( - self, - state_args=None, - outlvl=idaeslog.NOTSET, - solver=None, - optarg=None, - ): - """ - General wrapper for pressure changer initialization routines - - Keyword Arguments: - state_args : a dict of arguments to be passed to the property - package(s) to provide an initial state for - initialization (see documentation of the specific - property package) (default = {}). - outlvl : sets output level of initialization routine - optarg : solver options dictionary object (default=None) - solver : str indicating which solver to use during - initialization (default = None) - - Returns: None - """ - init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") - solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit") - - opt = get_solver(solver, optarg) - - # --------------------------------------------------------------------- - # Initialize holdup block - flags = self.properties_in.initialize( - outlvl=outlvl, - optarg=optarg, - solver=solver, - state_args=state_args, - hold_state=True, - ) - init_log.info_high("Initialization Step 1 Complete.") - # --------------------------------------------------------------------- - # Initialize other state blocks - # Set state_args from inlet state - if state_args is None: - state_args = {} - state_dict = self.properties_in[ - self.flowsheet().config.time.first() - ].define_port_members() - - for k in state_dict.keys(): - if state_dict[k].is_indexed(): - state_args[k] = {} - for m in state_dict[k].keys(): - state_args[k][m] = state_dict[k][m].value - else: - state_args[k] = state_dict[k].value - - self.properties_out.initialize( - outlvl=outlvl, - optarg=optarg, - solver=solver, - state_args=state_args, - ) - - state_args_solids = deepcopy(state_args) - for p, j in self.properties_solids.phase_component_set: - if p == "Sol": - state_args_solids["flow_mass_phase_comp"][p, j] = state_args[ - "flow_mass_phase_comp" - ]["Liq", j] - elif p == "Liq" or p == "Vap": - state_args_solids["flow_mass_phase_comp"][p, j] = 1e-8 - self.properties_solids.initialize( - outlvl=outlvl, - optarg=optarg, - solver=solver, - state_args=state_args_solids, - ) - - state_args_vapor = deepcopy(state_args) - for p, j in self.properties_vapor.phase_component_set: - if p == "Vap": - state_args_vapor["flow_mass_phase_comp"][p, j] = state_args[ - "flow_mass_phase_comp" - ]["Liq", j] - elif p == "Liq" or p == "Sol": - state_args_vapor["flow_mass_phase_comp"][p, j] = 1e-8 - self.properties_vapor.initialize( - outlvl=outlvl, - optarg=optarg, - solver=solver, - state_args=state_args_vapor, - ) - - self.properties_pure_water.initialize( - outlvl=outlvl, - optarg=optarg, - solver=solver, - state_args=state_args_vapor, - ) - init_log.info_high("Initialization Step 2 Complete.") - - interval_initializer(self) - # --------------------------------------------------------------------- - # Solve unit - with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: - res = opt.solve(self, tee=slc.tee) - init_log.info_high("Initialization Step 3 {}.".format(idaeslog.condition(res))) - # --------------------------------------------------------------------- - # Release Inlet state - self.properties_in.release_state(flags, outlvl=outlvl) - init_log.info("Initialization Complete: {}".format(idaeslog.condition(res))) - - if not check_optimal_termination(res): - raise InitializationError(f"Unit model {self.name} failed to initialize") - - def calculate_scaling_factors(self): - super().calculate_scaling_factors() - - iscale.set_scaling_factor( - self.crystal_growth_rate, 1e7 - ) # growth rates typically of order 1e-7 to 1e-9 m/s - iscale.set_scaling_factor( - self.crystal_median_length, 1e3 - ) # Crystal lengths typically in mm - iscale.set_scaling_factor( - self.souders_brown_constant, 1e2 - ) # Typical values are 0.0244, 0.04 and 0.06 - iscale.set_scaling_factor( - self.diameter_crystallizer, 1 - ) # Crystallizer diameters typically up to about 20 m - iscale.set_scaling_factor( - self.height_crystallizer, 1 - ) # H/D ratio maximum is about 1.5, so same scaling as diameter - iscale.set_scaling_factor(self.height_slurry, 1) # Same scaling as diameter - iscale.set_scaling_factor(self.magma_circulation_flow_vol, 1) - iscale.set_scaling_factor(self.relative_supersaturation, 10) - iscale.set_scaling_factor(self.t_res, 1) # Residence time is in hours - iscale.set_scaling_factor( - self.volume_suspension, 0.1 - ) # Suspension volume usually in tens to hundreds range - iscale.set_scaling_factor( - self.crystallization_yield, 1 - ) # Yield is between 0 and 1, usually in the 10-60% range - iscale.set_scaling_factor(self.product_volumetric_solids_fraction, 10) - iscale.set_scaling_factor( - self.temperature_operating, - iscale.get_scaling_factor(self.properties_in[0].temperature), - ) - iscale.set_scaling_factor(self.pressure_operating, 1e-3) - iscale.set_scaling_factor( - self.dens_mass_magma, 1e-3 - ) # scaling factor of dens_mass_phase['Liq'] - iscale.set_scaling_factor( - self.dens_mass_slurry, 1e-3 - ) # scaling factor of dens_mass_phase['Liq'] - iscale.set_scaling_factor( - self.work_mechanical[0], - iscale.get_scaling_factor( - self.properties_in[0].flow_mass_phase_comp["Vap", "H2O"] - ) - * iscale.get_scaling_factor(self.properties_in[0].enth_mass_solvent["Vap"]), - ) - iscale.set_scaling_factor( - self.energy_flow_superheated_vapor, - iscale.get_scaling_factor( - self.properties_vapor[0].flow_mass_phase_comp["Vap", "H2O"] - ) - * iscale.get_scaling_factor( - self.properties_vapor[0].enth_mass_solvent["Vap"] - ), - ) - # transforming constraints - for ind, c in self.eq_T_con1.items(): - sf = iscale.get_scaling_factor(self.properties_in[0].temperature) - iscale.constraint_scaling_transform(c, sf) - - for ind, c in self.eq_T_con2.items(): - sf = iscale.get_scaling_factor(self.properties_in[0].temperature) - iscale.constraint_scaling_transform(c, sf) - - for ind, c in self.eq_T_con3.items(): - sf = iscale.get_scaling_factor(self.properties_in[0].temperature) - iscale.constraint_scaling_transform(c, sf) - - for ind, c in self.eq_p_con1.items(): - sf = iscale.get_scaling_factor(self.properties_in[0].pressure) - iscale.constraint_scaling_transform(c, sf) - - for ind, c in self.eq_p_con2.items(): - sf = iscale.get_scaling_factor(self.properties_in[0].pressure) - iscale.constraint_scaling_transform(c, sf) - - for ind, c in self.eq_p_con3.items(): - sf = iscale.get_scaling_factor(self.properties_in[0].pressure) - iscale.constraint_scaling_transform(c, sf) - - for ind, c in self.eq_p_con4.items(): - sf = iscale.get_scaling_factor(self.properties_pure_water[0].pressure) - iscale.constraint_scaling_transform(c, sf) - - for j, c in self.eq_mass_balance_constraints.items(): - sf = iscale.get_scaling_factor( - self.properties_in[0].flow_mass_phase_comp["Liq", j] - ) - iscale.constraint_scaling_transform(c, sf) - - for j, c in self.eq_solubility_massfrac_equality_constraint.items(): - iscale.constraint_scaling_transform(c, 1e0) - - for j, c in self.eq_dens_magma.items(): - iscale.constraint_scaling_transform( - c, iscale.get_scaling_factor(self.dens_mass_magma) - ) - - for j, c in self.eq_removal_balance.items(): - sf = iscale.get_scaling_factor( - self.properties_in[0].flow_mass_phase_comp["Liq", j] - ) - iscale.constraint_scaling_transform(c, sf) - - def _get_stream_table_contents(self, time_point=0): - return create_stream_table_dataframe( - { - "Feed Inlet": self.inlet, - "Liquid Outlet": self.outlet, - "Vapor Outlet": self.vapor, - "Solid Outlet": self.solids, - }, - time_point=time_point, - ) - - def _get_performance_contents(self, time_point=0): - var_dict = {} - var_dict["Operating Temperature"] = self.temperature_operating - var_dict["Operating Pressure"] = self.pressure_operating - var_dict["Magma density of solution"] = self.dens_mass_magma - var_dict["Slurry density"] = self.dens_mass_slurry - var_dict["Heat requirement"] = self.work_mechanical[time_point] - var_dict["Crystallizer diameter"] = self.diameter_crystallizer - var_dict["Magma circulation flow rate"] = self.magma_circulation_flow_vol - var_dict["Vol. frac. of solids in suspension, 1-E"] = ( - self.product_volumetric_solids_fraction - ) - var_dict["Residence time"] = self.t_res - var_dict["Crystallizer minimum active volume"] = self.volume_suspension - var_dict["Suspension height in crystallizer"] = self.height_slurry - var_dict["Crystallizer height"] = self.height_crystallizer - - for j in self.config.property_package.solute_set: - yield_mem_name = f"{j} yield (fraction)" - var_dict[yield_mem_name] = self.crystallization_yield[j] - supersat_mem_name = f"{j} relative supersaturation (mass fraction basis)" - var_dict[supersat_mem_name] = self.relative_supersaturation[j] - - return {"vars": var_dict} - - @property - def default_costing_method(self): - return cost_crystallizer_watertap diff --git a/src/watertap_contrib/reflo/unit_models/zero_order/tests/test_crystallizer_watertap.py b/src/watertap_contrib/reflo/unit_models/zero_order/tests/test_crystallizer_watertap.py deleted file mode 100644 index a5e553d0..00000000 --- a/src/watertap_contrib/reflo/unit_models/zero_order/tests/test_crystallizer_watertap.py +++ /dev/null @@ -1,617 +0,0 @@ -# ################################################################################# -# # WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, -# # through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, -# # National Renewable Energy Laboratory, and National Energy Technology -# # Laboratory (subject to receipt of any required approvals from the U.S. Dept. -# # of Energy). All rights reserved. -# # -# # Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license -# # information, respectively. These files are also available online at the URL -# # "https://github.com/watertap-org/watertap/" -# ################################################################################# - -# import pytest -# from pyomo.environ import ( -# ConcreteModel, -# TerminationCondition, -# SolverStatus, -# value, -# Var, -# ) -# from pyomo.network import Port -# from idaes.core import FlowsheetBlock -# from pyomo.util.check_units import assert_units_consistent -# from watertap_contrib.reflo.unit_models.zero_order.crystallizer_zo_watertap import ( -# Crystallization, -# ) -# import watertap_contrib.reflo.property_models.cryst_prop_pack as props - -# from watertap.core.solvers import get_solver - -# from idaes.core.util.model_statistics import ( -# degrees_of_freedom, -# number_variables, -# number_total_constraints, -# number_unused_variables, -# ) -# from idaes.core.util.testing import initialization_tester -# from idaes.core.util.scaling import ( -# calculate_scaling_factors, -# unscaled_variables_generator, -# badly_scaled_var_generator, -# ) -# from idaes.core import UnitModelCostingBlock -# from watertap_contrib.reflo.costing import TreatmentCosting, CrystallizerCostType - -# # ----------------------------------------------------------------------------- -# # Get default solver for testing -# solver = get_solver() - - -# class TestCrystallization: -# @pytest.fixture(scope="class") -# def Crystallizer_frame(self): -# m = ConcreteModel() -# m.fs = FlowsheetBlock(dynamic=False) - -# m.fs.properties = props.NaClParameterBlock() - -# m.fs.unit = Crystallization(property_package=m.fs.properties) - -# # fully specify system -# feed_flow_mass = 1 -# feed_mass_frac_NaCl = 0.2126 -# feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl -# feed_pressure = 101325 -# feed_temperature = 273.15 + 20 -# eps = 1e-6 -# crystallizer_temperature = 273.15 + 55 -# crystallizer_yield = 0.40 - -# # Fully define feed -# m.fs.unit.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"].fix( -# feed_flow_mass * feed_mass_frac_NaCl -# ) -# m.fs.unit.inlet.flow_mass_phase_comp[0, "Liq", "H2O"].fix( -# feed_flow_mass * feed_mass_frac_H2O -# ) -# m.fs.unit.inlet.flow_mass_phase_comp[0, "Sol", "NaCl"].fix(eps) -# m.fs.unit.inlet.flow_mass_phase_comp[0, "Vap", "H2O"].fix(eps) -# m.fs.unit.inlet.pressure[0].fix(feed_pressure) -# m.fs.unit.inlet.temperature[0].fix(feed_temperature) - -# # Define operating conditions -# m.fs.unit.temperature_operating.fix(crystallizer_temperature) -# m.fs.unit.crystallization_yield["NaCl"].fix(crystallizer_yield) - -# # Fix growth rate, crystal length and Sounders brown constant to default values -# m.fs.unit.crystal_growth_rate.fix() -# m.fs.unit.souders_brown_constant.fix() -# m.fs.unit.crystal_median_length.fix() - -# assert_units_consistent(m) - -# return m - -# @pytest.fixture(scope="class") -# def Crystallizer_frame_2(self): -# m = ConcreteModel() -# m.fs = FlowsheetBlock(dynamic=False) - -# m.fs.properties = props.NaClParameterBlock() - -# m.fs.unit = Crystallization(property_package=m.fs.properties) - -# # fully specify system -# feed_flow_mass = 1 -# feed_mass_frac_NaCl = 0.2126 -# feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl -# feed_pressure = 101325 -# feed_temperature = 273.15 + 20 -# eps = 1e-6 -# crystallizer_temperature = 273.15 + 55 -# crystallizer_yield = 0.40 - -# # Fully define feed -# m.fs.unit.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"].fix( -# feed_flow_mass * feed_mass_frac_NaCl -# ) -# m.fs.unit.inlet.flow_mass_phase_comp[0, "Liq", "H2O"].fix( -# feed_flow_mass * feed_mass_frac_H2O -# ) -# m.fs.unit.inlet.flow_mass_phase_comp[0, "Sol", "NaCl"].fix(eps) -# m.fs.unit.inlet.flow_mass_phase_comp[0, "Vap", "H2O"].fix(eps) -# m.fs.unit.inlet.pressure[0].fix(feed_pressure) -# m.fs.unit.inlet.temperature[0].fix(feed_temperature) - -# # Define operating conditions -# m.fs.unit.temperature_operating.fix(crystallizer_temperature) -# m.fs.unit.crystallization_yield["NaCl"].fix(crystallizer_yield) - -# # Fix growth rate, crystal length and Sounders brown constant to default values -# m.fs.unit.crystal_growth_rate.fix() -# m.fs.unit.souders_brown_constant.fix() -# m.fs.unit.crystal_median_length.fix() - -# assert_units_consistent(m) - -# return m - -# @pytest.mark.unit -# def test_config(self, Crystallizer_frame): -# m = Crystallizer_frame -# # check unit config arguments -# assert len(m.fs.unit.config) == 4 - -# assert not m.fs.unit.config.dynamic -# assert not m.fs.unit.config.has_holdup -# assert m.fs.unit.config.property_package is m.fs.properties - -# @pytest.mark.unit -# def test_build(self, Crystallizer_frame): -# m = Crystallizer_frame - -# # test ports and variables -# port_lst = ["inlet", "outlet", "solids", "vapor"] -# port_vars_lst = ["flow_mass_phase_comp", "pressure", "temperature"] -# for port_str in port_lst: -# assert hasattr(m.fs.unit, port_str) -# port = getattr(m.fs.unit, port_str) -# assert len(port.vars) == 3 -# assert isinstance(port, Port) -# for var_str in port_vars_lst: -# assert hasattr(port, var_str) -# var = getattr(port, var_str) -# assert isinstance(var, Var) - -# # test unit objects (including parameters, variables, and constraints) -# # First, parameters -# unit_objs_params_lst = [ -# "approach_temperature_heat_exchanger", -# "dimensionless_crystal_length", -# ] -# for obj_str in unit_objs_params_lst: -# assert hasattr(m.fs.unit, obj_str) -# # Next, variables -# unit_objs_vars_lst = [ -# "crystal_growth_rate", -# "crystal_median_length", -# "crystallization_yield", -# "dens_mass_magma", -# "dens_mass_slurry", -# "diameter_crystallizer", -# "height_crystallizer", -# "height_slurry", -# "magma_circulation_flow_vol", -# "pressure_operating", -# "product_volumetric_solids_fraction", -# "relative_supersaturation", -# "souders_brown_constant", -# "t_res", -# "temperature_operating", -# "volume_suspension", -# "work_mechanical", -# ] -# for obj_str in unit_objs_vars_lst: -# assert hasattr(m.fs.unit, obj_str) -# # Next, expressions -# unit_objs_expr_lst = [ -# "eq_max_allowable_velocity", -# "eq_minimum_height_diameter_ratio", -# "eq_vapor_space_height", -# ] -# for obj_str in unit_objs_expr_lst: -# assert hasattr(m.fs.unit, obj_str) -# # Finally, constraints -# unit_objs_cons_lst = [ -# "eq_T_con1", -# "eq_T_con2", -# "eq_T_con3", -# "eq_crystallizer_height_constraint", -# "eq_dens_magma", -# "eq_dens_mass_slurry", -# "eq_enthalpy_balance", -# "eq_mass_balance_constraints", -# "eq_minimum_hex_circulation_rate_constraint", -# "eq_operating_pressure_constraint", -# "eq_p_con1", -# "eq_p_con2", -# "eq_p_con3", -# "eq_relative_supersaturation", -# "eq_removal_balance", -# "eq_residence_time", -# "eq_slurry_height_constraint", -# "eq_solubility_massfrac_equality_constraint", -# "eq_suspension_volume", -# "eq_vapor_head_diameter_constraint", -# "eq_vol_fraction_solids", -# ] -# for obj_str in unit_objs_cons_lst: -# assert hasattr(m.fs.unit, obj_str) - -# # Test stateblocks -# # List olf attributes on all stateblocks -# stateblock_objs_lst = [ -# "flow_mass_phase_comp", -# "pressure", -# "temperature", -# "solubility_mass_phase_comp", -# "solubility_mass_frac_phase_comp", -# "mass_frac_phase_comp", -# "dens_mass_solvent", -# "dens_mass_solute", -# "dens_mass_phase", -# "cp_mass_phase", -# "cp_mass_solvent", -# "flow_vol_phase", -# "flow_vol", -# "enth_flow", -# "enth_mass_solvent", -# "dh_crystallization_mass_comp", -# "eq_solubility_mass_phase_comp", -# "eq_solubility_mass_frac_phase_comp", -# "eq_mass_frac_phase_comp", -# "eq_dens_mass_solvent", -# "eq_dens_mass_solute", -# "eq_dens_mass_phase", -# "eq_cp_mass_solute", -# "eq_cp_mass_phase", -# "eq_flow_vol_phase", -# "eq_enth_mass_solvent", -# ] -# # List of attributes for liquid stateblocks only -# stateblock_objs_liq_lst = ["pressure_sat", "eq_pressure_sat"] -# # Inlet block -# assert hasattr(m.fs.unit, "properties_in") -# blk = getattr(m.fs.unit, "properties_in") -# for var_str in stateblock_objs_lst: -# assert hasattr(blk[0], var_str) -# for var_str in stateblock_objs_liq_lst: -# assert hasattr(blk[0], var_str) - -# # Liquid outlet block -# assert hasattr(m.fs.unit, "properties_out") -# blk = getattr(m.fs.unit, "properties_out") -# for var_str in stateblock_objs_lst: -# assert hasattr(blk[0], var_str) -# for var_str in stateblock_objs_liq_lst: -# assert hasattr(blk[0], var_str) - -# # Vapor outlet block -# assert hasattr(m.fs.unit, "properties_vapor") -# blk = getattr(m.fs.unit, "properties_vapor") -# for var_str in stateblock_objs_lst: -# assert hasattr(blk[0], var_str) - -# # Liquid outlet block -# assert hasattr(m.fs.unit, "properties_solids") -# blk = getattr(m.fs.unit, "properties_solids") -# for var_str in stateblock_objs_lst: -# assert hasattr(blk[0], var_str) - -# # test statistics -# assert number_variables(m) == 255 -# assert number_total_constraints(m) == 138 -# assert number_unused_variables(m) == 5 - -# @pytest.mark.unit -# def test_dof(self, Crystallizer_frame): -# m = Crystallizer_frame -# assert degrees_of_freedom(m) == 0 - -# @pytest.mark.unit -# def test_calculate_scaling(self, Crystallizer_frame): -# m = Crystallizer_frame - -# m.fs.properties.set_default_scaling( -# "flow_mass_phase_comp", 1e-1, index=("Liq", "H2O") -# ) -# m.fs.properties.set_default_scaling( -# "flow_mass_phase_comp", 1e-1, index=("Liq", "NaCl") -# ) -# m.fs.properties.set_default_scaling( -# "flow_mass_phase_comp", 1e-1, index=("Vap", "H2O") -# ) -# m.fs.properties.set_default_scaling( -# "flow_mass_phase_comp", 1e-1, index=("Sol", "NaCl") -# ) -# calculate_scaling_factors(m) - -# # check that all variables have scaling factors -# unscaled_var_list = list(unscaled_variables_generator(m)) -# assert len(unscaled_var_list) == 0 - -# for _ in badly_scaled_var_generator(m): -# assert False - -# @pytest.mark.component -# def test_initialize(self, Crystallizer_frame): -# # Add costing function, then initialize -# m = Crystallizer_frame -# m.fs.costing = TreatmentCosting() -# m.fs.unit.costing = UnitModelCostingBlock( -# flowsheet_costing_block=m.fs.costing, -# costing_method_arguments={"cost_type": CrystallizerCostType.mass_basis}, -# ) -# m.fs.costing.cost_process() - -# initialization_tester(Crystallizer_frame) -# assert_units_consistent(m) - -# # @pytest.mark.component -# # def test_var_scaling(self, Crystallizer_frame): -# # m = Crystallizer_frame -# # badly_scaled_var_lst = list(badly_scaled_var_generator(m)) -# # assert badly_scaled_var_lst == [] - -# @pytest.mark.component -# def test_solve(self, Crystallizer_frame): -# m = Crystallizer_frame -# results = solver.solve(m) - -# # Check for optimal solution -# assert results.solver.termination_condition == TerminationCondition.optimal -# assert results.solver.status == SolverStatus.ok - -# @pytest.mark.component -# def test_conservation(self, Crystallizer_frame): -# m = Crystallizer_frame -# b = m.fs.unit -# comp_lst = ["NaCl", "H2O"] -# phase_lst = ["Sol", "Liq", "Vap"] -# phase_comp_list = [ -# (p, j) -# for j in comp_lst -# for p in phase_lst -# if (p, j) in b.properties_in[0].phase_component_set -# ] - -# flow_mass_in = sum( -# b.properties_in[0].flow_mass_phase_comp[p, j] -# for p in phase_lst -# for j in comp_lst -# if (p, j) in phase_comp_list -# ) -# flow_mass_out = sum( -# b.properties_out[0].flow_mass_phase_comp[p, j] -# for p in phase_lst -# for j in comp_lst -# if (p, j) in phase_comp_list -# ) -# flow_mass_solids = sum( -# b.properties_solids[0].flow_mass_phase_comp[p, j] -# for p in phase_lst -# for j in comp_lst -# if (p, j) in phase_comp_list -# ) -# flow_mass_vapor = sum( -# b.properties_vapor[0].flow_mass_phase_comp[p, j] -# for p in phase_lst -# for j in comp_lst -# if (p, j) in phase_comp_list -# ) - -# assert ( -# abs( -# value(flow_mass_in - flow_mass_out - flow_mass_solids - flow_mass_vapor) -# ) -# <= 1e-6 -# ) - -# assert ( -# abs( -# value( -# flow_mass_in * b.properties_in[0].enth_mass_phase["Liq"] -# - flow_mass_out * b.properties_out[0].enth_mass_phase["Liq"] -# - flow_mass_vapor * b.properties_vapor[0].enth_mass_solvent["Vap"] -# - flow_mass_solids * b.properties_solids[0].enth_mass_solute["Sol"] -# - flow_mass_solids -# * b.properties_solids[0].dh_crystallization_mass_comp["NaCl"] -# + b.work_mechanical[0] -# ) -# ) -# <= 1e-2 -# ) - -# @pytest.mark.component -# def test_solution(self, Crystallizer_frame): -# m = Crystallizer_frame -# b = m.fs.unit -# # Check solid mass in solids stream -# assert pytest.approx( -# value( -# b.crystallization_yield["NaCl"] -# * b.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"] -# ), -# rel=1e-3, -# ) == value(b.solids.flow_mass_phase_comp[0, "Sol", "NaCl"]) -# # Check solid mass in liquid stream -# assert pytest.approx( -# value( -# (1 - b.crystallization_yield["NaCl"]) -# * b.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"] -# ), -# rel=1e-3, -# ) == value(b.outlet.flow_mass_phase_comp[0, "Liq", "NaCl"]) -# # Check outlet liquid stream composition which is set by solubility -# assert pytest.approx(0.2695, rel=1e-3) == value( -# b.properties_out[0].mass_frac_phase_comp["Liq", "NaCl"] -# ) -# # Check liquid stream solvent flow -# assert pytest.approx(0.12756 * ((1 / 0.2695) - 1), rel=1e-3) == value( -# b.outlet.flow_mass_phase_comp[0, "Liq", "H2O"] -# ) -# # Check saturation pressure -# assert pytest.approx(11992, rel=1e-3) == value(b.pressure_operating) -# # Check heat requirement -# assert pytest.approx(1127.2, rel=1e-3) == value(b.work_mechanical[0]) -# # Check crystallizer diameter -# assert pytest.approx(1.205, rel=1e-3) == value(b.diameter_crystallizer) -# # Minimum active volume -# assert pytest.approx(1.619, rel=1e-3) == value(b.volume_suspension) -# # Residence time -# assert pytest.approx(1.0228, rel=1e-3) == value(b.t_res) -# # Mass-basis costing -# assert pytest.approx(704557, rel=1e-3) == value( -# m.fs.costing.aggregate_capital_cost -# ) - -# @pytest.mark.component -# def test_solution2_capcosting_by_mass(self, Crystallizer_frame): -# m = Crystallizer_frame -# b = m.fs.unit -# b.crystal_growth_rate.fix(5e-8) -# b.souders_brown_constant.fix(0.0244) -# b.crystal_median_length.fix(0.4e-3) -# results = solver.solve(m) - -# # Test that report function works -# b.report() - -# # Check for optimal solution -# assert results.solver.termination_condition == TerminationCondition.optimal -# assert results.solver.status == SolverStatus.ok - -# # Residence time -# assert pytest.approx( -# value(b.crystal_median_length / (3.67 * 3600 * b.crystal_growth_rate)), -# rel=1e-3, -# ) == value(b.t_res) -# # Check crystallizer diameter -# assert pytest.approx(1.5427, rel=1e-3) == value(b.diameter_crystallizer) -# # Minimum active volume -# assert pytest.approx(0.959, rel=1e-3) == value(b.volume_suspension) -# # Mass-basis costing -# assert pytest.approx(704557, rel=1e-3) == value( -# m.fs.costing.aggregate_capital_cost -# ) - -# @pytest.mark.component -# def test_solution2_capcosting_by_volume(self, Crystallizer_frame_2): -# # Same problem as above, but different costing approach. -# # Other results should remain the same. -# m = Crystallizer_frame_2 -# b = m.fs.unit -# b.crystal_growth_rate.fix(5e-8) -# b.souders_brown_constant.fix(0.0244) -# b.crystal_median_length.fix(0.4e-3) - -# assert degrees_of_freedom(m) == 0 - -# m.fs.properties.set_default_scaling( -# "flow_mass_phase_comp", 1e-1, index=("Liq", "H2O") -# ) -# m.fs.properties.set_default_scaling( -# "flow_mass_phase_comp", 1e-1, index=("Liq", "NaCl") -# ) -# m.fs.properties.set_default_scaling( -# "flow_mass_phase_comp", 1e-1, index=("Vap", "H2O") -# ) -# m.fs.properties.set_default_scaling( -# "flow_mass_phase_comp", 1e-1, index=("Sol", "NaCl") -# ) -# calculate_scaling_factors(m) -# initialization_tester(Crystallizer_frame_2) -# results = solver.solve(m) - -# m.fs.costing = TreatmentCosting() -# m.fs.unit.costing = UnitModelCostingBlock( -# flowsheet_costing_block=m.fs.costing, -# costing_method_arguments={"cost_type": CrystallizerCostType.volume_basis}, -# ) -# m.fs.costing.cost_process() -# assert_units_consistent(m) -# results = solver.solve(m) - -# # Test that report function works -# b.report() - -# # Check for optimal solution -# assert results.solver.termination_condition == TerminationCondition.optimal -# assert results.solver.status == SolverStatus.ok - -# # Residence time -# assert pytest.approx( -# value(b.crystal_median_length / (3.67 * 3600 * b.crystal_growth_rate)), -# rel=1e-3, -# ) == value(b.t_res) -# # Check crystallizer diameter -# assert pytest.approx(1.5427, rel=1e-3) == value(b.diameter_crystallizer) -# # Minimum active volume -# assert pytest.approx(0.959, rel=1e-3) == value(b.volume_suspension) -# # Volume-basis costing -# assert pytest.approx(467490, rel=1e-3) == value( -# m.fs.costing.aggregate_capital_cost -# ) - -# @pytest.mark.component -# def test_solution2_operatingcost(self, Crystallizer_frame_2): -# m = Crystallizer_frame_2 -# b = m.fs.unit -# b.crystal_growth_rate.fix(5e-8) -# b.souders_brown_constant.fix(0.0244) -# b.crystal_median_length.fix(0.4e-3) -# results = solver.solve(m) - -# # Check for optimal solution -# assert results.solver.termination_condition == TerminationCondition.optimal -# assert results.solver.status == SolverStatus.ok - -# # Operating cost validation -# assert pytest.approx(980.77, rel=1e-3) == value( -# m.fs.costing.aggregate_flow_costs["electricity"] -# ) -# assert pytest.approx(36000.75, rel=1e-3) == value( -# m.fs.costing.aggregate_flow_costs["steam"] -# ) -# assert pytest.approx(0, rel=1e-3) == value( -# m.fs.costing.aggregate_flow_costs["NaCl"] -# ) - -# @pytest.mark.component -# def test_solution2_operatingcost_steampressure(self, Crystallizer_frame_2): -# m = Crystallizer_frame_2 -# m.fs.costing.crystallizer.steam_pressure.fix(5) -# b = m.fs.unit -# b.crystal_growth_rate.fix(5e-8) -# b.souders_brown_constant.fix(0.0244) -# b.crystal_median_length.fix(0.4e-3) -# results = solver.solve(m) - -# # Check for optimal solution -# assert results.solver.termination_condition == TerminationCondition.optimal -# assert results.solver.status == SolverStatus.ok - -# # Operating cost validation -# assert pytest.approx(980.77, rel=1e-3) == value( -# m.fs.costing.aggregate_flow_costs["electricity"] -# ) -# assert pytest.approx(25183.20, rel=1e-3) == value( -# m.fs.costing.aggregate_flow_costs["steam"] -# ) -# assert pytest.approx(0, abs=1e-6) == value( -# m.fs.costing.aggregate_flow_costs["NaCl"] -# ) - -# @pytest.mark.component -# def test_solution2_operatingcost_NaCl_revenue(self, Crystallizer_frame_2): -# m = Crystallizer_frame_2 -# m.fs.costing.crystallizer.steam_pressure.fix(3) -# m.fs.costing.crystallizer.NaCl_recovery_value.fix(-0.07) - -# results = solver.solve(m) - -# # Check for optimal solution -# assert results.solver.termination_condition == TerminationCondition.optimal -# assert results.solver.status == SolverStatus.ok - -# # Operating cost validation -# assert pytest.approx(980.77, rel=1e-3) == value( -# m.fs.costing.aggregate_flow_costs["electricity"] -# ) -# assert pytest.approx(36000.75, rel=1e-3) == value( -# m.fs.costing.aggregate_flow_costs["steam"] -# ) -# assert pytest.approx(-220533.25, rel=1e-3) == value( -# m.fs.costing.aggregate_flow_costs["NaCl"] -# ) From 4dd9b04c64643338e935f7ec490751505e83af7c Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Wed, 25 Sep 2024 09:09:56 -0600 Subject: [PATCH 45/80] remove __main__ func --- .../reflo/unit_models/crystallizer_effect.py | 103 ------------------ 1 file changed, 103 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py index 675474f1..858b7960 100644 --- a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py +++ b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py @@ -431,106 +431,3 @@ def calculate_scaling_factors(self): for _, c in self.eq_p_con4.items(): sf = iscale.get_scaling_factor(self.properties_pure_water[0].pressure) iscale.constraint_scaling_transform(c, sf) - - -if __name__ == "__main__": - import watertap.property_models.unit_specific.cryst_prop_pack as props - from watertap.property_models.water_prop_pack import WaterParameterBlock - - m = ConcreteModel() - m.fs = FlowsheetBlock(dynamic=False) - m.fs.props = props.NaClParameterBlock() - m.fs.vapor = WaterParameterBlock() - - m.fs.eff = eff = CrystallizerEffect( - property_package=m.fs.props, property_package_vapor=m.fs.vapor, standalone=True - ) - # m.fs.eff.display() - - feed_flow_mass = 1 - feed_mass_frac_NaCl = 0.15 - feed_pressure = 101325 - feed_temperature = 273.15 + 20 - crystallizer_yield = 0.5 - operating_pressure_eff1 = 0.78 - - feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl - eps = 1e-6 - - eff.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"].fix( - feed_flow_mass * feed_mass_frac_NaCl - ) - eff.inlet.flow_mass_phase_comp[0, "Liq", "H2O"].fix( - feed_flow_mass * feed_mass_frac_H2O - ) - eff.inlet.flow_mass_phase_comp[0, "Sol", "NaCl"].fix(eps) - eff.inlet.flow_mass_phase_comp[0, "Vap", "H2O"].fix(eps) - - eff.inlet.pressure[0].fix(feed_pressure) - eff.inlet.temperature[0].fix(feed_temperature) - # eff.properties_in[0].pressure_sat - - # eff.steam.flow_mass_phase_comp[0, "Liq", "NaCl"].fix( - # eps - # ) - # eff.steam.flow_mass_phase_comp[0, "Liq", "H2O"].fix( - # eps - # ) - # eff.heating_steam[0].temperature.fix(404.5) - # eff.steam.temperature[0].fix(404.5) - # eff.heating_steam[0].flow_mass_phase_comp.fix(eps) - # eff.heating_steam[0].mass_frac_phase_comp.fix(eps) - # eff.heating_steam[0].pressure.fix() - eff.heating_steam[0].pressure_sat - - eff.heating_steam.calculate_state( - var_args={ - ("pressure", None): 101325, - # ("pressure_sat", None): 28100 - ("temperature", None): 393, - }, - hold_state=True, - ) - - # ###################### - eff.crystallization_yield["NaCl"].fix(crystallizer_yield) - eff.crystal_growth_rate.fix() - eff.souders_brown_constant.fix() - eff.crystal_median_length.fix() - eff.overall_heat_transfer_coefficient.fix(100) - - eff.pressure_operating.fix(operating_pressure_eff1 * pyunits.bar) - - print(f"dof = {degrees_of_freedom(m.fs.eff)}") - - eff.initialize() - solver = get_solver() - results = solver.solve(m) - assert_optimal_termination(results) - # eff.properties_in[0].display() - eff.work_mechanical.display() - # eff.costing.display() - # eff.heating_steam[0].display() - - # print("\nPROPERTIES IN\n") - # eff.properties_in[0].flow_mass_phase_comp.display() - # eff.properties_in[0].mass_frac_phase_comp.display() - # eff.properties_in[0].temperature.display() - # eff.properties_in[0].pressure_sat.display() - # eff.properties_in[0].temperature_sat_solvent.display() - - # # print("\nPROPERTIES VAPOR\n") - # # eff.properties_vapor[0].temperature.display() - # # eff.properties_vapor[0].pressure_sat.display() - # # eff.properties_vapor[0].temperature_sat_solvent.display() - - # print("\nPROPERTIES HEATING STEAM\n") - # eff.heating_steam[0].flow_mass_phase_comp.display() - - # # eff.heating_steam[0].mass_frac_phase_comp.display() - # eff.heating_steam[0].temperature.display() - # eff.heating_steam[0].pressure_sat.display() - # # eff.heating_steam[0].temperature_sat_solvent.display() - - # eff.temperature_operating.display() - # # eff.heating_steam.display() From 23cbb6309ea74f34eb7383f710ff3df457060296 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Wed, 25 Sep 2024 09:11:19 -0600 Subject: [PATCH 46/80] remove __main__ func --- .../unit_models/multi_effect_crystallizer.py | 353 ------------------ 1 file changed, 353 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py index 7c1f7310..2e1f0b88 100644 --- a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py +++ b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py @@ -333,356 +333,3 @@ def initialize_build( @property def default_costing_method(self): return cost_multi_effect_crystallizer - - -if __name__ == "__main__": - - from watertap_contrib.reflo.costing import TreatmentCosting - import watertap.property_models.unit_specific.cryst_prop_pack as props - from watertap.property_models.water_prop_pack import WaterParameterBlock - from watertap.core.util.model_diagnostics.infeasible import * - from pyomo.util.calc_var_value import calculate_variable_from_constraint as cvc - from idaes.core.util.scaling import * - from idaes.core.util.testing import initialization_tester - from idaes.core.util.scaling import ( - calculate_scaling_factors, - unscaled_variables_generator, - badly_scaled_var_generator, - ) - from pyomo.util.check_units import assert_units_consistent - - solver = get_solver() - - m = ConcreteModel() - m.fs = FlowsheetBlock(dynamic=False) - - m.fs.props = props.NaClParameterBlock() - m.fs.vapor = WaterParameterBlock() - - m.fs.props.set_default_scaling("flow_mass_phase_comp", 1e-1, index=("Liq", "H2O")) - m.fs.props.set_default_scaling("flow_mass_phase_comp", 1e-1, index=("Liq", "NaCl")) - m.fs.props.set_default_scaling("flow_mass_phase_comp", 1e-1, index=("Vap", "H2O")) - m.fs.props.set_default_scaling("flow_mass_phase_comp", 1e-1, index=("Sol", "NaCl")) - - m.fs.mec = mec = MultiEffectCrystallizer( - property_package=m.fs.props, property_package_vapor=m.fs.vapor - ) - for n, effects in mec.effects.items(): - print(n, effects) - - feed_flow_mass = 1 - feed_mass_frac_NaCl = 0.15 - feed_pressure = 101325 - feed_temperature = 273.15 + 20 - crystallizer_yield = 0.5 - operating_pressures = [0.45, 0.25, 0.208, 0.095] - operating_pressure_eff1 = 0.45 # bar - operating_pressure_eff2 = 0.25 # bar - operating_pressure_eff3 = 0.208 # bar - operating_pressure_eff4 = 0.095 # bar - - feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl - eps = 1e-6 - - eff1 = mec.effects[1].effect - eff2 = mec.effects[2].effect - eff3 = mec.effects[3].effect - eff4 = mec.effects[4].effect - - for (n, eff), op_pressure in zip(mec.effects.items(), operating_pressures): - eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "H2O"].fix( - feed_flow_mass * feed_mass_frac_H2O - ) - eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"].fix( - feed_flow_mass * feed_mass_frac_NaCl - ) - eff.effect.properties_in[0].pressure.fix(feed_pressure) - eff.effect.properties_in[0].temperature.fix(feed_temperature) - - eff.effect.properties_in[0].flow_mass_phase_comp["Sol", "NaCl"].fix(eps) - eff.effect.properties_in[0].flow_mass_phase_comp["Vap", "H2O"].fix(eps) - eff.effect.properties_in[0].conc_mass_phase_comp[...] - eff.effect.crystallization_yield["NaCl"].fix(crystallizer_yield) - # eff.effect.crystal_growth_rate.fix(5e-9) - eff.effect.crystal_growth_rate.fix() - eff.effect.souders_brown_constant.fix() - # eff.effect.crystal_median_length.fix(0.6e-3) - eff.effect.crystal_median_length.fix() - eff.effect.pressure_operating.fix( - pyunits.convert(op_pressure * pyunits.bar, to_units=pyunits.Pa) - ) - eff.effect.overall_heat_transfer_coefficient.set_value(0.100) - if n == 1: - eff.effect.overall_heat_transfer_coefficient.fix(0.100) - eff.effect.heating_steam[0].pressure_sat - eff.effect.heating_steam[0].dh_vap_mass - eff.effect.heating_steam.calculate_state( - var_args={ - ("flow_mass_phase_comp", ("Liq", "H2O")): 0, - ("pressure", None): 101325, - # ("pressure_sat", None): 28100 - ("temperature", None): 393, - }, - hold_state=True, - ) - eff.effect.heating_steam[0].flow_mass_phase_comp["Vap", "H2O"].unfix() - eff.effect.heating_steam[0].flow_vol_phase - print(f"dof effect {n} = {degrees_of_freedom(eff.effect)}") - - print(f"dof before init = {degrees_of_freedom(m)}") - - calculate_scaling_factors(m) - assert_units_consistent(m) - try: - mec.initialize() - except: - print_infeasible_constraints(m) - - print(f"DOF before solve = {degrees_of_freedom(m)}") - results = solver.solve(m) - print(f"termination {results.solver.termination_condition}") - assert_optimal_termination(results) - print(mec.effects[mec.last_effect].effect.height_crystallizer.name) - # for n, eff in mec.effects.items(): - # print(f"\nEFFECT {n}\n") - # print(n, eff.effect.name) - # eff.effect.overall_heat_transfer_coefficient.display() - # eff.effect.properties_solids[0].flow_mass_phase_comp.display() - # eff.effect.temperature_operating.display() - # assert False - m.fs.costing = TreatmentCosting() - m.fs.mec.costing = UnitModelCostingBlock( - flowsheet_costing_block=m.fs.costing, - costing_method_arguments={"cost_type": "mass_basis"}, - ) - - m.fs.costing.nacl_recovered.cost.set_value(-0.024) - m.fs.costing.cost_process() - m.fs.costing.add_LCOW(mec.total_flow_vol_in) - - print(f"DOF after costing = {degrees_of_freedom(m)}") - results = solver.solve(m) - print(f"termination {results.solver.termination_condition}") - print(f"LCOW = {m.fs.costing.LCOW()}") - - # m.fs.costing.display() - # mec.costing.display() - # eff1.work_mechanical.display() - # eff1.pressure_operating.display() - # eff1.heating_steam[0].pressure.display() - # eff1.heating_steam[0].pressure_sat.display() - - for n, eff in mec.effects.items(): - print(f"\nEFFECT {n}\n") - eff.effect.work_mechanical.display() - eff.effect.energy_flow_superheated_vapor.display() - # eff.effect.properties_pure_water[0].temperature.display() - # eff.effect.vapor_head_diam_expr.display() - # eff.effect.properties_out[0].dens_mass_phase.display() - # eff.effect.properties_vapor[0].dens_mass_solvent.display() - # eff.effect.eq_max_allowable_velocity.display() - # eff.effect.height_crystallizer.display() - # eff.effect.height_slurry.display() - # eff.effect.diameter_crystallizer.display() - # eff.effect.volume_suspension.display() - - # eff.effect.height_crystallizer.display() - # eff.effect.height_slurry.display() - # eff.effect.volume_suspension.display() - # eff.effect.t_res.display() - # eff.effect.overall_heat_transfer_coefficient.display() - # eff.effect.properties_vapor[0].temperature.display() - # eff.effect.temperature_operating.display() - # eff.effect.properties_in[0].flow_mass_phase_comp.display() - # eff.effect.properties_in[0].conc_mass_phase_comp.display() - # eff.effect.properties_out[0].flow_mass_phase_comp.display() - - -################################################################################################## -# "Linking" constraints below that weren't working well but want to keep them here until model is ready for merge -# just in case so I don't have to write them again. - -# mass_flow_solid_nacl_constr = Constraint( -# expr=effect.properties_in[0].flow_mass_phase_comp["Sol", "NaCl"] -# == prev_effect.properties_in[0].flow_mass_phase_comp["Sol", "NaCl"], -# doc="Mass flow of solid NaCl for effect {n}", -# ) -# effect.add_component( -# f"eq_equiv_mass_flow_sol_nacl_effect_{n}", -# mass_flow_solid_nacl_constr, -# ) - -# mass_flow_vap_water_constr = Constraint( -# expr=effect.properties_in[0].flow_mass_phase_comp["Vap", "H2O"] -# == prev_effect.properties_in[0].flow_mass_phase_comp["Vap", "H2O"], -# doc="Mass flow of water vapor for effect {n}", -# ) -# effect.add_component( -# f"eq_equiv_mass_flow_vap_water_effect_{n}", -# mass_flow_vap_water_constr, -# ) - -# prop_in_press_constr = Constraint( -# expr=effect.properties_in[0].pressure -# == prev_effect.properties_in[0].pressure, -# doc="Inlet properties pressure for effect {n}", -# ) -# effect.add_component(f"eq_equiv_press_effect_{n}", prop_in_press_constr) - -# prop_in_press_constr_ub = Constraint( -# expr=effect.properties_in[0].pressure -# <= 1.0001 * prev_effect.properties_in[0].pressure, -# doc="Inlet properties pressure for effect {n}", -# ) -# effect.add_component(f"eq_equiv_press_effect_{n}_ub", prop_in_press_constr_ub) - -# prop_in_press_constr_lb = Constraint( -# expr=effect.properties_in[0].pressure -# >= 0.9999 * prev_effect.properties_in[0].pressure, -# doc="Inlet properties pressure for effect {n}", -# ) -# effect.add_component(f"eq_equiv_press_effect_{n}_lb", prop_in_press_constr_lb) - -# prop_in_temp_constr = Constraint( -# expr=effect.properties_in[0].temperature -# == prev_effect.properties_in[0].temperature, -# doc="Inlet properties temperature for effect {n}", -# ) -# effect.add_component( -# f"eq_equiv_temp_effect_{n}", prop_in_temp_constr -# ) - -# cryst_growth_rate_constr = Constraint( -# expr=effect.crystal_growth_rate == prev_effect.crystal_growth_rate, -# doc="Equivalent crystal growth rate effect {n}", -# ) -# effect.add_component( -# f"eq_equiv_crystal_growth_rate_effect_{n}", cryst_growth_rate_constr -# ) - -# souders_brown_constr = Constraint( -# expr=effect.souders_brown_constant -# == prev_effect.souders_brown_constant, -# doc="Equivalent Sounders Brown constant effect {n}", -# ) -# effect.add_component( -# f"eq_equiv_souders_brown_constant_effect_{n}", souders_brown_constr -# ) - -# cryst_med_len_constr = Constraint( -# expr=effect.crystal_median_length -# == prev_effect.crystal_median_length, -# doc="Equivalent crystal median length effect {n}", -# ) -# effect.add_component( -# f"eq_equiv_crystal_median_length_effect_{n}", cryst_med_len_constr -# ) - -# cryst_yield_constr = Constraint( -# expr=effect.crystallization_yield["NaCl"] -# == prev_effect.crystallization_yield["NaCl"], -# doc="Equivalent crystallization yield effect {n}", -# ) -# effect.add_component( -# f"eq_equiv_crystallization_yield_effect_{n}", cryst_yield_constr -# ) - -# op_press_constr = Constraint( -# expr=effect.pressure_operating <= prev_effect.pressure_operating, -# doc=f"Equivalent operating pressure effect {n}", -# ) -# effect.add_component( -# f"eq_equiv_pressure_operating_effect_{n}", op_press_constr -# ) - -# brine_conc_constr = Constraint( -# expr=effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] -# >= 0.95* prev_effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] -# ) -# effect.add_component(f"eq_equiv_brine_conc_effect_{n}_lb", brine_conc_constr) -# brine_conc_constr = Constraint( -# expr=effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] -# <= 1.05 * prev_effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] -# ) -# effect.add_component(f"eq_equiv_brine_conc_effect_{n}_ub", brine_conc_constr) - - -# height_cryst_constr = Constraint( -# expr=effect.height_crystallizer == self.effects[self.last_effect].effect.height_crystallizer -# ) -# effect.add_component( -# f"eq_equiv_height_cryst_effect_{n}", height_cryst_constr -# ) -# effect.eq_crystallizer_height_constraint.deactivate() - -# diam_constr = Constraint( -# expr=effect.diameter_crystallizer -# == prev_effect.diameter_crystallizer -# ) -# effect.add_component(f"eq_equiv_diam_effect_{n}", diam_constr) -# effect.eq_vapor_head_diameter_constraint.deactivate() - -# height_slurry_constr = Constraint( -# expr=effect.height_slurry -# == 4 -# * prev_effect.volume_suspension -# / (Constants.pi * prev_effect.diameter_crystallizer) -# ) -# effect.add_component( -# f"eq_equiv_height_slurry_effect_{n}", height_slurry_constr -# ) -# effect.eq_slurry_height_constraint.deactivate() - -# vol_cryst_expr = Expression( -# expr=Constants.pi -# * (effect.diameter_crystallizer / 2) ** 2 -# * effect.height_crystallizer, -# doc=f"Actual volume of effect {n}", -# ) -# effect.add_component(f"total_volume_effect_{n}", vol_cryst_expr) - -# vapor_head_diam_expr = Expression( -# expr=( -# 4 -# * effect.properties_vapor[0].flow_vol_phase["Vap"] -# / (Constants.pi * effect.eq_max_allowable_velocity) -# ) -# ** 0.5 -# ) -# effect.add_component( -# f"vapor_head_diameter_effect_{n}", vapor_head_diam_expr -# ) - -# actual_vapor_vel_expr = Expression(expr=pyunits.convert( -# (effect.properties_vapor[0].flow_vol_phase["Vap"] * 4) -# / (effect.diameter_crystallizer**2 * Constants.pi), -# to_units=pyunits.m / pyunits.s, -# )) -# effect.add_component( -# f"actual_vapor_velocity_effect_{n}", actual_vapor_vel_expr -# ) - - -# @effect.Expression(doc="Total volume of effect 1") -# def total_volume_effect_1(b): -# return ( -# Constants.pi -# * (effect.diameter_crystallizer / 2) ** 2 -# * effect.height_crystallizer -# ) - -# @effect.Expression(doc="Total volume of effect 1.") -# def vapor_head_diameter_effect_1(b): -# return ( -# 4 -# * effect.properties_vapor[0].flow_vol_phase["Vap"] -# / (Constants.pi * effect.eq_max_allowable_velocity) -# ) ** 0.5 - -# @effect.Expression(doc="Actual vapor velocity") -# def actual_vapor_velocity_effect_1(b): -# return pyunits.convert( -# (b.properties_vapor[0].flow_vol_phase["Vap"] * 4) -# / (b.diameter_crystallizer**2 * Constants.pi), -# to_units=pyunits.m / pyunits.s, -# ) From 045b9aba745d94a2b10bda9795297689cdf57635 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Wed, 25 Sep 2024 09:13:14 -0600 Subject: [PATCH 47/80] update initial value and scaling factor for overall_heat_transfer_coeff --- src/watertap_contrib/reflo/unit_models/crystallizer_effect.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py index 858b7960..02d7f0e8 100644 --- a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py +++ b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py @@ -177,7 +177,7 @@ def build(self): ) self.overall_heat_transfer_coefficient = Var( - initialize=100.0, + initialize=0.1, bounds=(0, None), units=pyunits.kilowatt / pyunits.m**2 / pyunits.degK, doc="Overall heat transfer coefficient for heat exchangers", @@ -426,7 +426,7 @@ def calculate_scaling_factors(self): iscale.set_scaling_factor(self.heat_exchanger_area, 0.1) if iscale.get_scaling_factor(self.overall_heat_transfer_coefficient) is None: - iscale.set_scaling_factor(self.overall_heat_transfer_coefficient, 0.01) + iscale.set_scaling_factor(self.overall_heat_transfer_coefficient, 10) for _, c in self.eq_p_con4.items(): sf = iscale.get_scaling_factor(self.properties_pure_water[0].pressure) From bb7b67955bf48d597a6943410db84b3dd562c0ba Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Wed, 25 Sep 2024 10:39:31 -0600 Subject: [PATCH 48/80] revert overall_heat_transfer_coefficient sf --- .../reflo/unit_models/crystallizer_effect.py | 2 +- .../unit_models/tests/test_multi_effect_crystallizer.py | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py index 02d7f0e8..505ba661 100644 --- a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py +++ b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py @@ -426,7 +426,7 @@ def calculate_scaling_factors(self): iscale.set_scaling_factor(self.heat_exchanger_area, 0.1) if iscale.get_scaling_factor(self.overall_heat_transfer_coefficient) is None: - iscale.set_scaling_factor(self.overall_heat_transfer_coefficient, 10) + iscale.set_scaling_factor(self.overall_heat_transfer_coefficient, 0.01) for _, c in self.eq_p_con4.items(): sf = iscale.get_scaling_factor(self.properties_pure_water[0].pressure) diff --git a/src/watertap_contrib/reflo/unit_models/tests/test_multi_effect_crystallizer.py b/src/watertap_contrib/reflo/unit_models/tests/test_multi_effect_crystallizer.py index 66f60c99..17ba2f13 100644 --- a/src/watertap_contrib/reflo/unit_models/tests/test_multi_effect_crystallizer.py +++ b/src/watertap_contrib/reflo/unit_models/tests/test_multi_effect_crystallizer.py @@ -29,7 +29,6 @@ number_total_constraints, number_unused_variables, ) -from idaes.core.util.testing import initialization_tester from idaes.core.util.scaling import ( calculate_scaling_factors, unscaled_variables_generator, @@ -47,14 +46,10 @@ WaterStateBlock, ) -from watertap_contrib.reflo.costing import TreatmentCosting, CrystallizerCostType +from watertap_contrib.reflo.costing import TreatmentCosting from watertap_contrib.reflo.unit_models.multi_effect_crystallizer import ( MultiEffectCrystallizer, ) -from watertap_contrib.reflo.costing.units.multi_effect_crystallizer import ( - MultiEffectCrystallizerCostType, -) - from watertap_contrib.reflo.unit_models.crystallizer_effect import CrystallizerEffect __author__ = "Kurban Sitterley" From 8c994845f3ba2352c6aa81ed582be92211aa3659 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 26 Sep 2024 13:56:25 -0600 Subject: [PATCH 49/80] modify to accomodate costing CrystallizerEffect and MulitEffectCrystallizer models --- .../units/multi_effect_crystallizer.py | 106 ++++++++++++++---- 1 file changed, 85 insertions(+), 21 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/units/multi_effect_crystallizer.py b/src/watertap_contrib/reflo/costing/units/multi_effect_crystallizer.py index ce805875..0595d1ce 100644 --- a/src/watertap_contrib/reflo/costing/units/multi_effect_crystallizer.py +++ b/src/watertap_contrib/reflo/costing/units/multi_effect_crystallizer.py @@ -1,5 +1,5 @@ ################################################################################# -# WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, +# WaterTAP Copyright (c) 2020-2024, The Regents of the University of California, # through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, # National Renewable Energy Laboratory, and National Energy Technology # Laboratory (subject to receipt of any required approvals from the U.S. Dept. @@ -153,35 +153,99 @@ def cost_multi_effect_crystallizer( total_capex_expr = 0 - for effect_number, eff in blk.unit_model.effects.items(): + if not hasattr(blk.unit_model, "effects"): + assert blk.unit_model.config.standalone + # blk.unit_model is CrystallizerEffect as standalone unit effect_capex_expr = 0 - effect_capex_var = pyo.Var( - initialize=1e5, - units=costing_package.base_currency, - doc=f"Capital cost effect {effect_number}", + # Add capital of crystallizer + effect_capex_expr += effect_costing_method(blk.unit_model) + # separate expression for crystallizer capex for reporting + blk.add_component( + f"capital_cost_crystallizer", + pyo.Expression( + expr=blk.cost_factor * effect_costing_method(blk.unit_model), + doc=f"Capital cost for only crystallizer including TIC", + ), ) - # add capital of crystallizer - effect_capex_expr += effect_costing_method(eff.effect) - # add capital of heat exchangers + # Add capital of heat exchangers effect_capex_expr += cost_crystallizer_heat_exchanger(eff.effect) - - effect_capex_constr = pyo.Constraint( - expr=effect_capex_var == effect_capex_expr, - doc=f"Constraint for capital cost of effect {effect_number}.", - ) - blk.add_component(f"capital_cost_effect_{effect_number}", effect_capex_var) + # separate expression for heat exchanger for reporting blk.add_component( - f"capital_cost_effect_{effect_number}_constraint", effect_capex_constr + f"capital_cost_heat_exchanger", + pyo.Expression( + expr=blk.cost_factor * cost_crystallizer_heat_exchanger(blk.unit_model), + doc=f"Capital cost for only heat exchanger including TIC", + ), ) - total_capex_expr += effect_capex_expr - _cost_effect_flows(eff.effect, effect_number) - blk.capital_cost_constraint = pyo.Constraint( - expr=blk.capital_cost == blk.cost_factor * total_capex_expr - ) + blk.capital_cost_constraint = pyo.Constraint( + expr=blk.capital_cost == blk.cost_factor * effect_capex_expr, + doc=f"Constraint for total capital cost of crystallizer.", + ) + + _cost_effect_flows(blk.unit_model, 1) + + elif hasattr(blk.unit_model, "effects"): + + # blk.unit_model is MultiEffectCrystallizer + + for effect_number, eff in blk.unit_model.effects.items(): + + effect_capex_expr = 0 + + effect_capex_var = pyo.Var( + initialize=1e5, + units=costing_package.base_currency, + doc=f"Capital cost effect {effect_number}", + ) + + # Add capital of crystallizer + effect_capex_expr += effect_costing_method(eff.effect) + # separate expression for crystallizer capex for reporting + blk.add_component( + f"capital_cost_crystallizer_effect_{effect_number}", + pyo.Expression( + expr=blk.cost_factor * effect_costing_method(eff.effect), + doc=f"Capital cost for only crystallizer for effect {effect_number} including TIC", + ), + ) + + # Add capital of heat exchangers + effect_capex_expr += cost_crystallizer_heat_exchanger(eff.effect) + # separate expression for heat exchanger for reporting + blk.add_component( + f"capital_cost_heat_exchanger_effect_{effect_number}", + pyo.Expression( + expr=blk.cost_factor * cost_crystallizer_heat_exchanger(eff.effect), + doc=f"Capital cost for only heat exchanger for effect {effect_number} including TIC", + ), + ) + + # Total effect CAPEX constraint + effect_capex_constr = pyo.Constraint( + expr=effect_capex_var == effect_capex_expr, + doc=f"Constraint for total capital cost of effect {effect_number}.", + ) + # Add effect Var and Constraint to costing blk + blk.add_component(f"capital_cost_effect_{effect_number}", effect_capex_var) + blk.add_component( + f"total_capital_cost_effect_{effect_number}_constraint", + effect_capex_constr, + ) + total_capex_expr += effect_capex_expr + _cost_effect_flows(eff.effect, effect_number) + + blk.capital_cost_constraint = pyo.Constraint( + expr=blk.capital_cost == blk.cost_factor * total_capex_expr + ) + else: + raise ConfigurationError( + f"{blk.unit_model.name} is not a valid unit model type for this costing package:" + f" {type(blk.unit_model)}. Must be either CrystallizerEffect or MultiEffectCrystallizer." + ) def cost_crystallizer_heat_exchanger(effect): From 27402c1e4556a780171314e353a79f2067eb09f1 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 26 Sep 2024 13:58:57 -0600 Subject: [PATCH 50/80] add costing method; clean up imports --- .../reflo/unit_models/crystallizer_effect.py | 31 ++++++------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py index 505ba661..420d130d 100644 --- a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py +++ b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py @@ -14,11 +14,9 @@ # Import Pyomo libraries from pyomo.environ import ( - ConcreteModel, Var, - check_optimal_termination, - assert_optimal_termination, Param, + check_optimal_termination, units as pyunits, ) from pyomo.common.config import ConfigValue @@ -27,10 +25,7 @@ from idaes.core import ( declare_process_block_class, useDefault, - FlowsheetBlock, ) -from watertap.core.solvers import get_solver -from idaes.core.util.tables import create_stream_table_dataframe from idaes.core.util.config import is_physical_parameter_block from idaes.core.util.exceptions import InitializationError @@ -38,26 +33,16 @@ import idaes.core.util.scaling as iscale import idaes.logger as idaeslog +from watertap.core.solvers import get_solver from watertap.core.util.initialization import interval_initializer -from watertap.unit_models.crystallizer import Crystallization, CrystallizationData - -from idaes.core.util.model_statistics import ( - degrees_of_freedom, - number_variables, - number_total_constraints, - number_unused_variables, -) -from idaes.core.util.testing import initialization_tester -from idaes.core.util.scaling import ( - calculate_scaling_factors, - unscaled_variables_generator, - badly_scaled_var_generator, -) +from watertap.unit_models.crystallizer import CrystallizationData from watertap.unit_models.mvc.components.lmtd_chen_callback import ( delta_temperature_chen_callback, ) -_log = idaeslog.getLogger(__name__) +from watertap_contrib.reflo.costing.units.multi_effect_crystallizer import ( + cost_multi_effect_crystallizer, +) __author__ = "Oluwamayowa Amusat, Zhuoran Zhang, Kurban Sitterley" @@ -431,3 +416,7 @@ def calculate_scaling_factors(self): for _, c in self.eq_p_con4.items(): sf = iscale.get_scaling_factor(self.properties_pure_water[0].pressure) iscale.constraint_scaling_transform(c, sf) + + @property + def default_costing_method(self): + return cost_multi_effect_crystallizer \ No newline at end of file From 771ea248c41717129ff82c306f25e5ffba3e680e Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 26 Sep 2024 13:59:27 -0600 Subject: [PATCH 51/80] add to test for indivdiual effect component capex --- .../tests/test_multi_effect_crystallizer.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/watertap_contrib/reflo/unit_models/tests/test_multi_effect_crystallizer.py b/src/watertap_contrib/reflo/unit_models/tests/test_multi_effect_crystallizer.py index 17ba2f13..c17b1f48 100644 --- a/src/watertap_contrib/reflo/unit_models/tests/test_multi_effect_crystallizer.py +++ b/src/watertap_contrib/reflo/unit_models/tests/test_multi_effect_crystallizer.py @@ -204,6 +204,7 @@ def test_config(self, MEC4_frame): for _, eff in m.fs.unit.effects.items(): assert isinstance(eff.effect, CrystallizerEffect) + assert not eff.effect.config.standalone @pytest.mark.unit def test_build(self, MEC4_frame): @@ -677,11 +678,19 @@ def test_costing(self, MEC4_frame): eff_costing_dict = { "capital_cost": 4613044.11, + "direct_capital_cost": 2306522.05, + "capital_cost_crystallizer_effect_1": 659171.48, + "capital_cost_heat_exchanger_effect_1": 294820.27, "capital_cost_effect_1": 476995.87, + "capital_cost_crystallizer_effect_2": 627459.32, + "capital_cost_heat_exchanger_effect_2": 498502.0, "capital_cost_effect_2": 562980.66, + "capital_cost_crystallizer_effect_3": 602356.42, + "capital_cost_heat_exchanger_effect_3": 851138.46, "capital_cost_effect_3": 726747.44, + "capital_cost_crystallizer_effect_4": 587458.93, + "capital_cost_heat_exchanger_effect_4": 492137.2, "capital_cost_effect_4": 539798.07, - "direct_capital_cost": 2306522.05, } for v, r in eff_costing_dict.items(): @@ -795,6 +804,7 @@ def test_config(self, MEC3_frame): for _, eff in m.fs.unit.effects.items(): assert isinstance(eff.effect, CrystallizerEffect) + assert not eff.effect.config.standalone @pytest.mark.unit def test_dof(self, MEC3_frame): @@ -1081,9 +1091,15 @@ def test_costing(self, MEC3_frame): eff_costing_dict = { "capital_cost": 17518907.2, + "capital_cost_crystallizer_effect_1": 3079736.6, + "capital_cost_crystallizer_effect_2": 2932488.6, + "capital_cost_crystallizer_effect_3": 2818630.8, "capital_cost_effect_1": 1939078.0, "capital_cost_effect_2": 2525607.6, "capital_cost_effect_3": 4294767.8, + "capital_cost_heat_exchanger_effect_1": 798419.4, + "capital_cost_heat_exchanger_effect_2": 2118726.6, + "capital_cost_heat_exchanger_effect_3": 5770904.9, "direct_capital_cost": 8759453.6, } From 93ff68cbc54b4c738a80785149c76ee8799d30db Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 26 Sep 2024 14:51:15 -0600 Subject: [PATCH 52/80] add constraint for heating steam flow rate --- .../reflo/unit_models/crystallizer_effect.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py index 420d130d..3deb71e2 100644 --- a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py +++ b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py @@ -242,7 +242,16 @@ def eq_heat_transfer(b): * b.heat_exchanger_area * b.delta_temperature[0] ) - + + @self.Constraint(doc="Calculate mass flow rate of heating steam") + def eq_heating_steam_flow_rate(b): + return b.work_mechanical[0] == ( + pyunits.convert( + b.heating_steam[0].dh_vap_mass + * b.heating_steam[0].flow_mass_phase_comp["Vap", "H2O"], + to_units=pyunits.kJ / pyunits.s, + ) + ) else: self.del_component(self.inlet) self.del_component(self.outlet) From 97460b4708b56d7eda713eba27e035d86ebe8ff9 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 26 Sep 2024 14:51:51 -0600 Subject: [PATCH 53/80] cost unit not effect --- .../reflo/costing/units/multi_effect_crystallizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/watertap_contrib/reflo/costing/units/multi_effect_crystallizer.py b/src/watertap_contrib/reflo/costing/units/multi_effect_crystallizer.py index 0595d1ce..3fa9eed5 100644 --- a/src/watertap_contrib/reflo/costing/units/multi_effect_crystallizer.py +++ b/src/watertap_contrib/reflo/costing/units/multi_effect_crystallizer.py @@ -171,7 +171,7 @@ def cost_multi_effect_crystallizer( ) # Add capital of heat exchangers - effect_capex_expr += cost_crystallizer_heat_exchanger(eff.effect) + effect_capex_expr += cost_crystallizer_heat_exchanger(blk.unit_model) # separate expression for heat exchanger for reporting blk.add_component( f"capital_cost_heat_exchanger", From 75dea2c56d47238d600e68a67b754b9557af308d Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 26 Sep 2024 14:52:04 -0600 Subject: [PATCH 54/80] add test file for crystallizer_effect --- .../tests/test_crystallizer_effect.py | 419 ++++++++++++++++++ 1 file changed, 419 insertions(+) create mode 100644 src/watertap_contrib/reflo/unit_models/tests/test_crystallizer_effect.py diff --git a/src/watertap_contrib/reflo/unit_models/tests/test_crystallizer_effect.py b/src/watertap_contrib/reflo/unit_models/tests/test_crystallizer_effect.py new file mode 100644 index 00000000..a158ed10 --- /dev/null +++ b/src/watertap_contrib/reflo/unit_models/tests/test_crystallizer_effect.py @@ -0,0 +1,419 @@ +################################################################################# +# WaterTAP Copyright (c) 2020-2024, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# National Renewable Energy Laboratory, and National Energy Technology +# Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +################################################################################# + +import pytest + +from pyomo.environ import ( + ConcreteModel, + Var, + Param, + value, + assert_optimal_termination, + units as pyunits, +) +from pyomo.network import Port + +from idaes.core import FlowsheetBlock, UnitModelCostingBlock, MaterialFlowBasis +from idaes.core.util.testing import initialization_tester +from idaes.core.util.model_statistics import ( + degrees_of_freedom, + number_variables, + number_total_constraints, + number_unused_variables, +) +from idaes.core.util.scaling import ( + calculate_scaling_factors, + unscaled_variables_generator, + badly_scaled_var_generator, +) +from pyomo.util.check_units import assert_units_consistent +import idaes.logger as idaeslog + +from watertap.core.solvers import get_solver +from watertap.property_models.unit_specific.cryst_prop_pack import ( + NaClParameterBlock, + NaClParameterData, + NaClStateBlock, +) +from watertap.property_models.water_prop_pack import ( + WaterParameterBlock, + WaterStateBlock, +) + +from watertap_contrib.reflo.unit_models.crystallizer_effect import CrystallizerEffect +from watertap_contrib.reflo.costing import TreatmentCosting + +# Get default solver for testing +solver = get_solver() + + +def build_effect(): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + m.fs.properties = NaClParameterBlock() + m.fs.vapor_properties = WaterParameterBlock() + + m.fs.unit = eff = CrystallizerEffect( + property_package=m.fs.properties, + property_package_vapor=m.fs.vapor_properties, + standalone=True, + ) + + feed_flow_mass = 1 + feed_mass_frac_NaCl = 0.15 + feed_pressure = 101325 + feed_temperature = 273.15 + 20 + crystallizer_yield = 0.5 + operating_pressure_eff = 0.45 + operating_temperature = 393 # degK + + feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl + eps = 1e-6 + + eff.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"].fix( + feed_flow_mass * feed_mass_frac_NaCl + ) + eff.inlet.flow_mass_phase_comp[0, "Liq", "H2O"].fix( + feed_flow_mass * feed_mass_frac_H2O + ) + eff.inlet.flow_mass_phase_comp[0, "Sol", "NaCl"].fix(eps) + eff.inlet.flow_mass_phase_comp[0, "Vap", "H2O"].fix(eps) + + eff.inlet.pressure[0].fix(feed_pressure) + eff.inlet.temperature[0].fix(feed_temperature) + eff.heating_steam[0].pressure_sat + + eff.heating_steam.calculate_state( + var_args={ + ("flow_mass_phase_comp", ("Liq", "H2O")): 0, + ("pressure", None): feed_pressure, + ("temperature", None): operating_temperature, + }, + hold_state=True, + ) + eff.heating_steam[0].flow_mass_phase_comp["Vap", "H2O"].unfix() + eff.heating_steam[0].flow_vol_phase[...] + + eff.crystallization_yield["NaCl"].fix(crystallizer_yield) + eff.crystal_growth_rate.fix() + eff.souders_brown_constant.fix() + eff.crystal_median_length.fix() + eff.overall_heat_transfer_coefficient.fix(0.1) + + eff.pressure_operating.fix(operating_pressure_eff * pyunits.bar) + + return m + + +class TestCrystallizerEffect: + @pytest.fixture(scope="class") + def effect_frame(self): + m = build_effect() + return m + + @pytest.mark.unit + def test_config(self, effect_frame): + m = effect_frame + + assert len(m.fs.unit.config) == 6 + + assert not m.fs.unit.config.dynamic + assert not m.fs.unit.config.has_holdup + assert m.fs.unit.config.property_package is m.fs.properties + assert m.fs.unit.config.property_package_vapor is m.fs.vapor_properties + assert m.fs.unit.config.standalone + assert_units_consistent(m) + + @pytest.mark.unit + def test_build(self, effect_frame): + m = effect_frame + + # test ports and variables + port_lst = ["inlet", "outlet", "solids", "vapor", "steam", "pure_water"] + port_vars_lst = ["flow_mass_phase_comp", "pressure", "temperature"] + state_blks = [ + "properties_in", + "properties_out", + "properties_pure_water", + "properties_solids", + "properties_vapor", + "heating_steam", + ] + + for port_str in port_lst: + assert hasattr(m.fs.unit, port_str) + port = getattr(m.fs.unit, port_str) + assert len(port.vars) == 3 + assert isinstance(port, Port) + for var_str in port_vars_lst: + assert hasattr(port, var_str) + var = getattr(port, var_str) + assert isinstance(var, Var) + + for b in state_blks: + assert hasattr(m.fs.unit, b) + sb = getattr(m.fs.unit, b) + assert sb[0].temperature.ub == 1000 + if b == "heating_steam": + assert isinstance(sb, WaterStateBlock) + else: + assert isinstance(sb, NaClStateBlock) + + effect_vars = [ + "crystal_median_length", + "crystal_growth_rate", + "souders_brown_constant", + "crystallization_yield", + "product_volumetric_solids_fraction", + "temperature_operating", + "pressure_operating", + "dens_mass_magma", + "dens_mass_slurry", + "work_mechanical", + "diameter_crystallizer", + "height_slurry", + "height_crystallizer", + "magma_circulation_flow_vol", + "relative_supersaturation", + "energy_flow_superheated_vapor", + "delta_temperature_in", + "delta_temperature_out", + "heat_exchanger_area", + "overall_heat_transfer_coefficient", + ] + + for ev in effect_vars: + v = getattr(m.fs.unit, ev) + assert isinstance(v, Var) + + effect_params = [ + "approach_temperature_heat_exchanger", + "dimensionless_crystal_length", + "steam_pressure", + "efficiency_pump", + "pump_head_height", + ] + + for ep in effect_params: + p = getattr(m.fs.unit, ep) + assert isinstance(p, Param) + + assert number_variables(m) == 292 + assert number_total_constraints(m) == 124 + assert number_unused_variables(m) == 34 + + assert_units_consistent(m) + + @pytest.mark.unit + def test_dof(self, effect_frame): + m = effect_frame + assert degrees_of_freedom(m) == 0 + + @pytest.mark.unit + def test_calculate_scaling(self, effect_frame): + m = effect_frame + + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Liq", "H2O") + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Liq", "NaCl") + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Vap", "H2O") + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Sol", "NaCl") + ) + m.fs.vapor_properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Vap", "H2O") + ) + m.fs.vapor_properties.set_default_scaling( + "flow_mass_phase_comp", 1, index=("Liq", "H2O") + ) + + calculate_scaling_factors(m) + unscaled_var_list = list(unscaled_variables_generator(m)) + assert len(unscaled_var_list) == 0 + badly_scaled_var_list = list(badly_scaled_var_generator(m)) + assert len(badly_scaled_var_list) == 0 + + @pytest.mark.component + def test_initialize(self, effect_frame): + m = effect_frame + initialization_tester(m) + + @pytest.mark.component + def test_solve(self, effect_frame): + m = effect_frame + results = solver.solve(m) + assert_optimal_termination(results) + + @pytest.mark.unit + def test_conservation(self, effect_frame): + + m = effect_frame + + comp_lst = ["NaCl", "H2O"] + phase_lst = ["Sol", "Liq", "Vap"] + + phase_comp_list = [ + (p, j) + for j in comp_lst + for p in phase_lst + if (p, j) in m.fs.unit.properties_in[0].phase_component_set + ] + flow_mass_in = sum( + m.fs.unit.properties_in[0].flow_mass_phase_comp[p, j] + for p in phase_lst + for j in comp_lst + if (p, j) in phase_comp_list + ) + flow_mass_out = sum( + m.fs.unit.properties_out[0].flow_mass_phase_comp[p, j] + for p in phase_lst + for j in comp_lst + if (p, j) in phase_comp_list + ) + flow_mass_solids = sum( + m.fs.unit.properties_solids[0].flow_mass_phase_comp[p, j] + for p in phase_lst + for j in comp_lst + if (p, j) in phase_comp_list + ) + flow_mass_vapor = sum( + m.fs.unit.properties_vapor[0].flow_mass_phase_comp[p, j] + for p in phase_lst + for j in comp_lst + if (p, j) in phase_comp_list + ) + + assert ( + abs( + value(flow_mass_in - flow_mass_out - flow_mass_solids - flow_mass_vapor) + ) + <= 1e-6 + ) + + assert ( + abs( + value( + flow_mass_in * m.fs.unit.properties_in[0].enth_mass_phase["Liq"] + - flow_mass_out * m.fs.unit.properties_out[0].enth_mass_phase["Liq"] + - flow_mass_vapor + * m.fs.unit.properties_vapor[0].enth_mass_solvent["Vap"] + - flow_mass_solids + * m.fs.unit.properties_solids[0].enth_mass_solute["Sol"] + - flow_mass_solids + * m.fs.unit.properties_solids[0].dh_crystallization_mass_comp[ + "NaCl" + ] + + m.fs.unit.work_mechanical[0] + ) + ) + <= 1e-2 + ) + + @pytest.mark.unit + def test_solution(self, effect_frame): + m = effect_frame + + eff_dict = { + "product_volumetric_solids_fraction": 0.132975, + "temperature_operating": 359.48, + "pressure_operating": 45000.0, + "dens_mass_magma": 281.24, + "dens_mass_slurry": 1298.23, + "work_mechanical": {0.0: 1704.47}, + "diameter_crystallizer": 1.0799, + "height_slurry": 1.073, + "height_crystallizer": 1.883, + "magma_circulation_flow_vol": 0.119395, + "relative_supersaturation": {"NaCl": 0.567038}, + "t_res": 1.0228, + "volume_suspension": 0.982965, + "eq_max_allowable_velocity": 2.6304, + "eq_vapor_space_height": 0.809971, + "eq_minimum_height_diameter_ratio": 1.6199, + "energy_flow_superheated_vapor": 1518.73, + "delta_temperature_in": {0.0: 33.51}, + "delta_temperature_out": {0.0: 99.85}, + "delta_temperature": {0.0: 60.65}, + "heat_exchanger_area": 281.0, + } + for v, r in eff_dict.items(): + effv = getattr(m.fs.unit, v) + if effv.is_indexed(): + for i, s in r.items(): + assert pytest.approx(value(effv[i]), rel=1e-3) == s + else: + assert pytest.approx(value(effv), rel=1e-3) == r + + @pytest.mark.component + def test_costing(self, effect_frame): + m = effect_frame + m.fs.costing = TreatmentCosting() + m.fs.unit.costing = UnitModelCostingBlock( + flowsheet_costing_block=m.fs.costing, + ) + m.fs.costing.nacl_recovered.cost.set_value(-0.024) + m.fs.costing.cost_process() + m.fs.costing.add_LCOW(m.fs.unit.properties_in[0].flow_vol_phase["Liq"]) + m.fs.costing.add_specific_energy_consumption( + m.fs.unit.properties_in[0].flow_vol_phase["Liq"], name="SEC" + ) + results = solver.solve(m) + assert_optimal_termination(results) + + sys_costing_dict = { + "aggregate_capital_cost": 953991.75, + "aggregate_flow_electricity": 2.1715, + "aggregate_flow_NaCl_recovered": 0.075, + "aggregate_flow_steam": 0.692984, + "aggregate_flow_costs": { + "electricity": 1564.26, + "NaCl_recovered": -67456.42, + "steam": 102690.74, + }, + "aggregate_direct_capital_cost": 476995.87, + "total_capital_cost": 953991.75, + "total_operating_cost": 65418.33, + "maintenance_labor_chemical_operating_cost": 28619.75, + "total_fixed_operating_cost": 28619.75, + "total_variable_operating_cost": 36798.58, + "total_annualized_cost": 172223.38, + "LCOW": 6.0504, + "SEC": 0.66875, + } + + for v, r in sys_costing_dict.items(): + cv = getattr(m.fs.costing, v) + if cv.is_indexed(): + for i, s in r.items(): + assert pytest.approx(value(cv[i]), rel=1e-3) == s + else: + assert pytest.approx(value(cv), rel=1e-3) == r + + eff_costing_dict = { + "capital_cost": 953991.75, + "direct_capital_cost": 476995.87, + "capital_cost_crystallizer": 659171.48, + "capital_cost_heat_exchanger": 294820.27, + } + + for v, r in eff_costing_dict.items(): + cv = getattr(m.fs.unit.costing, v) + if cv.is_indexed(): + for i, s in r.items(): + assert pytest.approx(value(cv[i]), rel=1e-3) == s + else: + assert pytest.approx(value(cv), rel=1e-3) == r From c5c3a85d965a9604b644a075f9f009ec3b17da03 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 26 Sep 2024 14:53:19 -0600 Subject: [PATCH 55/80] black --- .../reflo/unit_models/crystallizer_effect.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py index 3deb71e2..d55317bf 100644 --- a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py +++ b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py @@ -242,7 +242,7 @@ def eq_heat_transfer(b): * b.heat_exchanger_area * b.delta_temperature[0] ) - + @self.Constraint(doc="Calculate mass flow rate of heating steam") def eq_heating_steam_flow_rate(b): return b.work_mechanical[0] == ( @@ -252,6 +252,7 @@ def eq_heating_steam_flow_rate(b): to_units=pyunits.kJ / pyunits.s, ) ) + else: self.del_component(self.inlet) self.del_component(self.outlet) @@ -428,4 +429,4 @@ def calculate_scaling_factors(self): @property def default_costing_method(self): - return cost_multi_effect_crystallizer \ No newline at end of file + return cost_multi_effect_crystallizer From 00124ecd96846d464c240e21068b455da7bf7fc2 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 26 Sep 2024 14:54:18 -0600 Subject: [PATCH 56/80] clean up; black! --- src/watertap_contrib/reflo/unit_models/crystallizer_effect.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py index d55317bf..c6ce1a92 100644 --- a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py +++ b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py @@ -27,9 +27,7 @@ useDefault, ) from idaes.core.util.config import is_physical_parameter_block - from idaes.core.util.exceptions import InitializationError - import idaes.core.util.scaling as iscale import idaes.logger as idaeslog From 6fe16b998ed08c59bf401b630aed3dec4a920857 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 26 Sep 2024 18:32:59 -0600 Subject: [PATCH 57/80] remove steam prop calculation func from costing pkg --- .../units/multi_effect_crystallizer.py | 90 +------------------ 1 file changed, 2 insertions(+), 88 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/units/multi_effect_crystallizer.py b/src/watertap_contrib/reflo/costing/units/multi_effect_crystallizer.py index 3fa9eed5..49ec215b 100644 --- a/src/watertap_contrib/reflo/costing/units/multi_effect_crystallizer.py +++ b/src/watertap_contrib/reflo/costing/units/multi_effect_crystallizer.py @@ -352,94 +352,8 @@ def _cost_effect_flows(effect, effect_number): ) if effect_number == 1: - effect.steam_flow = pyo.units.convert( - (effect.work_mechanical[0] / _compute_steam_specific_energy(effect)), - to_units=pyo.units.m**3 / pyo.units.s, - ) - costing_package.cost_flow( - effect.steam_flow, + costing_package.cost_flow(effect.heating_steam[0].flow_vol_phase["Vap"], "steam", ) - # costing_package.cost_flow(effect.heating_steam[0].flow_vol_phase["Vap"], - # "steam", - # ) - - -def _compute_steam_specific_energy(effect): - """ - Function for computing saturated steam specific energy for thermal heating estimation. - - Args: - pressure_sat: Steam gauge pressure in bar - Out: - Steam thermal capacity (latent heat of condensation * density) in kJ/m3 - """ - # TODO: add specific volume to property package? - - # pressure_sat = effect.steam_pressure - # pressure_sat = effect.heating_steam[0].pressure_sat - # 1. Compute saturation temperature of steam: computed from El-Dessouky expression - # tsat_constants = [ - # 42.6776 * pyo.units.K, - # -3892.7 * pyo.units.K, - # 1000 * pyo.units.kPa, - # -9.48654 * pyo.units.dimensionless, - # ] - # psat = ( - # pyo.units.convert(pressure_sat, to_units=pyo.units.kPa) - # # + 101.325 * pyo.units.kPa - # ) - # temperature_sat = tsat_constants[0] + tsat_constants[1] / ( - # pyo.log(psat / tsat_constants[2]) + tsat_constants[3] - # ) - - # 2. Compute latent heat of condensation/vaporization: computed from Sharqawy expression - # t = temperature_sat - 273.15 * pyo.units.K - t = effect.heating_steam[0].temperature - 273.15 * pyo.units.K - enth_mass_units = pyo.units.J / pyo.units.kg - t_inv_units = pyo.units.K**-1 - dh_constants = [ - 2.501e6 * enth_mass_units, - -2.369e3 * enth_mass_units * t_inv_units**1, - 2.678e-1 * enth_mass_units * t_inv_units**2, - -8.103e-3 * enth_mass_units * t_inv_units**3, - -2.079e-5 * enth_mass_units * t_inv_units**4, - ] - dh_vap = ( - dh_constants[0] - + dh_constants[1] * t - + dh_constants[2] * t**2 - + dh_constants[3] * t**3 - + dh_constants[4] * t**4 - ) - # dh_vap = pyo.units.convert(dh_vap, to_units=pyo.units.kJ / pyo.units.kg) - effect.dh_vap = dh_vap - - # 3. Compute specific volume: computed from Affandi expression (Eq 5) - t_critical = 647.096 * pyo.units.K - # t_red = temperature_sat / t_critical # Reduced temperature - t_red = effect.heating_steam[0].temperature / t_critical # Reduced temperature - - sp_vol_constants = [ - -7.75883 * pyo.units.dimensionless, - 3.23753 * pyo.units.dimensionless, - 2.05755 * pyo.units.dimensionless, - -0.06052 * pyo.units.dimensionless, - 0.00529 * pyo.units.dimensionless, - ] - log_sp_vol = ( - sp_vol_constants[0] - + sp_vol_constants[1] * (pyo.log(1 / t_red)) ** 0.4 - + sp_vol_constants[2] / (t_red**2) - + sp_vol_constants[3] / (t_red**4) - + sp_vol_constants[4] / (t_red**5) - ) - sp_vol = pyo.exp(log_sp_vol) * pyo.units.m**3 / pyo.units.kg - - # 4. Return specific energy: density * latent heat - # return dh_vap / sp_vol - return pyo.units.convert( - effect.heating_steam[0].dh_vap_mass / sp_vol, - to_units=pyo.units.kJ / pyo.units.m**3, - ) + \ No newline at end of file From 4dc24e9a064d6cf23a0febd3b6adb1e97869a903 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 26 Sep 2024 18:33:45 -0600 Subject: [PATCH 58/80] update test files for new model run approach --- .../tests/test_crystallizer_effect.py | 84 +++-- .../tests/test_multi_effect_crystallizer.py | 339 ++++++++++-------- 2 files changed, 238 insertions(+), 185 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/tests/test_crystallizer_effect.py b/src/watertap_contrib/reflo/unit_models/tests/test_crystallizer_effect.py index a158ed10..ccda1350 100644 --- a/src/watertap_contrib/reflo/unit_models/tests/test_crystallizer_effect.py +++ b/src/watertap_contrib/reflo/unit_models/tests/test_crystallizer_effect.py @@ -70,11 +70,14 @@ def build_effect(): feed_flow_mass = 1 feed_mass_frac_NaCl = 0.15 - feed_pressure = 101325 + atm_pressure = 101325 * pyunits.Pa + saturated_steam_pressure_gage = 3 * pyunits.bar + saturated_steam_pressure = atm_pressure + pyunits.convert( + saturated_steam_pressure_gage, to_units=pyunits.Pa + ) feed_temperature = 273.15 + 20 crystallizer_yield = 0.5 operating_pressure_eff = 0.45 - operating_temperature = 393 # degK feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl eps = 1e-6 @@ -88,20 +91,20 @@ def build_effect(): eff.inlet.flow_mass_phase_comp[0, "Sol", "NaCl"].fix(eps) eff.inlet.flow_mass_phase_comp[0, "Vap", "H2O"].fix(eps) - eff.inlet.pressure[0].fix(feed_pressure) + eff.inlet.pressure[0].fix(atm_pressure) eff.inlet.temperature[0].fix(feed_temperature) + eff.heating_steam[0].pressure_sat eff.heating_steam.calculate_state( var_args={ ("flow_mass_phase_comp", ("Liq", "H2O")): 0, - ("pressure", None): feed_pressure, - ("temperature", None): operating_temperature, + ("pressure", None): saturated_steam_pressure, + ("pressure_sat", None): saturated_steam_pressure, }, hold_state=True, ) eff.heating_steam[0].flow_mass_phase_comp["Vap", "H2O"].unfix() - eff.heating_steam[0].flow_vol_phase[...] eff.crystallization_yield["NaCl"].fix(crystallizer_yield) eff.crystal_growth_rate.fix() @@ -207,9 +210,9 @@ def test_build(self, effect_frame): p = getattr(m.fs.unit, ep) assert isinstance(p, Param) - assert number_variables(m) == 292 - assert number_total_constraints(m) == 124 - assert number_unused_variables(m) == 34 + assert number_variables(m) == 288 + assert number_total_constraints(m) == 120 + assert number_unused_variables(m) == 43 assert_units_consistent(m) @@ -330,7 +333,6 @@ def test_solution(self, effect_frame): eff_dict = { "product_volumetric_solids_fraction": 0.132975, "temperature_operating": 359.48, - "pressure_operating": 45000.0, "dens_mass_magma": 281.24, "dens_mass_slurry": 1298.23, "work_mechanical": {0.0: 1704.47}, @@ -345,11 +347,12 @@ def test_solution(self, effect_frame): "eq_vapor_space_height": 0.809971, "eq_minimum_height_diameter_ratio": 1.6199, "energy_flow_superheated_vapor": 1518.73, - "delta_temperature_in": {0.0: 33.51}, - "delta_temperature_out": {0.0: 99.85}, - "delta_temperature": {0.0: 60.65}, - "heat_exchanger_area": 281.0, + "delta_temperature_in": {0.0: 57.39}, + "delta_temperature_out": {0.0: 123.72}, + "delta_temperature": {0.0: 86.31}, + "heat_exchanger_area": 197.47, } + for v, r in eff_dict.items(): effv = getattr(m.fs.unit, v) if effv.is_indexed(): @@ -358,6 +361,22 @@ def test_solution(self, effect_frame): else: assert pytest.approx(value(effv), rel=1e-3) == r + steam_dict = { + "flow_mass_phase_comp": {("Liq", "H2O"): 0.0, ("Vap", "H2O"): 0.799053}, + "temperature": 416.87, + "pressure": 401325.0, + "dh_vap_mass": 2133119.0, + "pressure_sat": 401325.0, + } + + for v, r in steam_dict.items(): + sv = getattr(m.fs.unit.heating_steam[0], v) + if sv.is_indexed(): + for i, s in r.items(): + assert pytest.approx(value(sv[i]), rel=1e-3) == s + else: + assert pytest.approx(value(sv), rel=1e-3) == r + @pytest.mark.component def test_costing(self, effect_frame): m = effect_frame @@ -375,23 +394,23 @@ def test_costing(self, effect_frame): assert_optimal_termination(results) sys_costing_dict = { - "aggregate_capital_cost": 953991.75, + "aggregate_capital_cost": 868242.67, "aggregate_flow_electricity": 2.1715, "aggregate_flow_NaCl_recovered": 0.075, - "aggregate_flow_steam": 0.692984, + "aggregate_flow_steam": 0.383078, "aggregate_flow_costs": { "electricity": 1564.26, "NaCl_recovered": -67456.42, - "steam": 102690.74, + "steam": 56766.9, }, - "aggregate_direct_capital_cost": 476995.87, - "total_capital_cost": 953991.75, - "total_operating_cost": 65418.33, - "maintenance_labor_chemical_operating_cost": 28619.75, - "total_fixed_operating_cost": 28619.75, - "total_variable_operating_cost": 36798.58, - "total_annualized_cost": 172223.38, - "LCOW": 6.0504, + "aggregate_direct_capital_cost": 434121.33, + "total_capital_cost": 868242.67, + "total_operating_cost": 16922.02, + "maintenance_labor_chemical_operating_cost": 26047.28, + "total_fixed_operating_cost": 26047.28, + "total_variable_operating_cost": -9125.25, + "total_annualized_cost": 114126.95, + "LCOW": 4.0094, "SEC": 0.66875, } @@ -404,10 +423,10 @@ def test_costing(self, effect_frame): assert pytest.approx(value(cv), rel=1e-3) == r eff_costing_dict = { - "capital_cost": 953991.75, - "direct_capital_cost": 476995.87, + "capital_cost": 868242.67, + "direct_capital_cost": 434121.33, "capital_cost_crystallizer": 659171.48, - "capital_cost_heat_exchanger": 294820.27, + "capital_cost_heat_exchanger": 209071.19, } for v, r in eff_costing_dict.items(): @@ -417,3 +436,12 @@ def test_costing(self, effect_frame): assert pytest.approx(value(cv[i]), rel=1e-3) == s else: assert pytest.approx(value(cv), rel=1e-3) == r + + assert ( + pytest.approx( + value(m.fs.unit.heating_steam[0].flow_vol_phase["Vap"]), rel=1e-3 + ) + == 0.383078 + ) + + assert value(m.fs.unit.heating_steam[0].flow_vol_phase["Liq"]) == 0 diff --git a/src/watertap_contrib/reflo/unit_models/tests/test_multi_effect_crystallizer.py b/src/watertap_contrib/reflo/unit_models/tests/test_multi_effect_crystallizer.py index c17b1f48..c21b567d 100644 --- a/src/watertap_contrib/reflo/unit_models/tests/test_multi_effect_crystallizer.py +++ b/src/watertap_contrib/reflo/unit_models/tests/test_multi_effect_crystallizer.py @@ -73,12 +73,19 @@ def build_mec4(): property_package=m.fs.properties, property_package_vapor=m.fs.vapor_properties ) + operating_pressures = [0.45, 0.25, 0.208, 0.095] + feed_flow_mass = 1 feed_mass_frac_NaCl = 0.15 crystallizer_yield = 0.5 - operating_pressures = [0.45, 0.25, 0.208, 0.095] feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl + atm_pressure = 101325 * pyunits.Pa + saturated_steam_pressure_gage = 3 * pyunits.bar + saturated_steam_pressure = atm_pressure + pyunits.convert( + saturated_steam_pressure_gage, to_units=pyunits.Pa + ) + for (_, eff), op_pressure in zip(mec.effects.items(), operating_pressures): eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "H2O"].fix( feed_flow_mass * feed_mass_frac_H2O @@ -104,17 +111,15 @@ def build_mec4(): first_effect = m.fs.unit.effects[1].effect first_effect.overall_heat_transfer_coefficient.fix(0.1) - first_effect.heating_steam[0].pressure_sat first_effect.heating_steam[0].dh_vap_mass first_effect.heating_steam.calculate_state( var_args={ ("flow_mass_phase_comp", ("Liq", "H2O")): 0, - ("pressure", None): 101325, - ("temperature", None): 393, + ("pressure", None): saturated_steam_pressure, + ("pressure_sat", None): saturated_steam_pressure, }, hold_state=True, ) - first_effect.heating_steam[0].flow_vol_phase first_effect.heating_steam[0].flow_mass_phase_comp["Vap", "H2O"].unfix() return m @@ -140,6 +145,12 @@ def build_mec3(): operating_pressures = [0.85, 0.25, 0.208] feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl + atm_pressure = 101325 * pyunits.Pa + saturated_steam_pressure_gage = 2.5 * pyunits.bar + saturated_steam_pressure = atm_pressure + pyunits.convert( + saturated_steam_pressure_gage, to_units=pyunits.Pa + ) + for (_, eff), op_pressure in zip(mec.effects.items(), operating_pressures): eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "H2O"].fix( feed_flow_mass * feed_mass_frac_H2O @@ -165,17 +176,15 @@ def build_mec3(): first_effect = m.fs.unit.effects[1].effect first_effect.overall_heat_transfer_coefficient.fix(0.1) - first_effect.heating_steam[0].pressure_sat first_effect.heating_steam[0].dh_vap_mass first_effect.heating_steam.calculate_state( var_args={ ("flow_mass_phase_comp", ("Liq", "H2O")): 0, - ("pressure", None): 101325, - ("temperature", None): 500, + ("pressure", None): saturated_steam_pressure, + ("pressure_sat", None): saturated_steam_pressure, }, hold_state=True, ) - first_effect.heating_steam[0].flow_vol_phase first_effect.heating_steam[0].flow_mass_phase_comp["Vap", "H2O"].unfix() return m @@ -311,9 +320,9 @@ def test_build(self, MEC4_frame): assert hasattr(eff.effect, f"eq_heat_transfer_effect_{n}") if n == 1: - assert number_variables(eff.effect) == 150 - assert number_total_constraints(eff.effect) == 124 - assert number_unused_variables(eff.effect) == 1 + assert number_variables(eff.effect) == 146 + assert number_total_constraints(eff.effect) == 120 + assert number_unused_variables(eff.effect) == 3 assert hasattr(eff.effect, "heating_steam") assert isinstance(eff.effect.heating_steam, WaterStateBlock) assert eff.effect.heating_steam[0].temperature.ub == 1000 @@ -327,9 +336,9 @@ def test_build(self, MEC4_frame): eff.effect, f"eq_energy_for_effect_{n}_from_effect_{n - 1}" ) - assert number_variables(m) == 712 - assert number_total_constraints(m) == 478 - assert number_unused_variables(m) == 37 + assert number_variables(m) == 708 + assert number_total_constraints(m) == 474 + assert number_unused_variables(m) == 46 assert_units_consistent(m) @@ -511,9 +520,9 @@ def test_solution(self, MEC4_frame): unit_results_dict = { 1: { - "delta_temperature": {0.0: 60.65}, - "delta_temperature_in": {0.0: 33.51}, - "delta_temperature_out": {0.0: 99.85}, + "delta_temperature": {0.0: 86.31}, + "delta_temperature_in": {0.0: 57.39}, + "delta_temperature_out": {0.0: 123.72}, "dens_mass_magma": 281.24, "dens_mass_slurry": 1298.23, "diameter_crystallizer": 1.0799, @@ -521,14 +530,15 @@ def test_solution(self, MEC4_frame): "eq_max_allowable_velocity": 2.6304, "eq_minimum_height_diameter_ratio": 1.6199, "eq_vapor_space_height": 0.809971, - "heat_exchanger_area": 281, - "height_crystallizer": 1.88304, - "height_slurry": 1.0730, + "heat_exchanger_area": 197.47, + "height_crystallizer": 1.883, + "height_slurry": 1.073, "magma_circulation_flow_vol": 0.119395, - "product_volumetric_solids_fraction": 0.1329756, - "relative_supersaturation": {"NaCl": 0.56703}, + "product_volumetric_solids_fraction": 0.132975, + "relative_supersaturation": {"NaCl": 0.567038}, + "t_res": 1.0228, "temperature_operating": 359.48, - "volume_suspension": 0.98296, + "volume_suspension": 0.982965, "work_mechanical": {0.0: 1704.47}, }, 2: { @@ -539,12 +549,12 @@ def test_solution(self, MEC4_frame): "dens_mass_slurry": 1301.86, "diameter_crystallizer": 1.1773, "energy_flow_superheated_vapor": 1396.71, - "eq_max_allowable_velocity": 3.46414453, - "eq_minimum_height_diameter_ratio": 1.7660549, + "eq_max_allowable_velocity": 3.4641, + "eq_minimum_height_diameter_ratio": 1.766, "eq_vapor_space_height": 0.883027, "heat_exchanger_area": 480.72, - "height_crystallizer": 1.7660549, - "height_slurry": 0.828680, + "height_crystallizer": 1.766, + "height_slurry": 0.82868, "magma_circulation_flow_vol": 0.105985, "product_volumetric_solids_fraction": 0.132132, "relative_supersaturation": {"NaCl": 0.571251}, @@ -554,47 +564,48 @@ def test_solution(self, MEC4_frame): "work_mechanical": {0.0: 1518.73}, }, 3: { - "delta_temperature": {0.0: 16.853}, + "delta_temperature": {0.0: 16.85}, "delta_temperature_in": {0.0: 4.3421}, - "delta_temperature_out": {0.0: 44.835}, + "delta_temperature_out": {0.0: 44.83}, "dens_mass_magma": 279.07, "dens_mass_slurry": 1303.08, "diameter_crystallizer": 1.1811, "energy_flow_superheated_vapor": 1296.7, - "eq_max_allowable_velocity": 3.776, + "eq_max_allowable_velocity": 3.7763, "eq_minimum_height_diameter_ratio": 1.7717, - "eq_vapor_space_height": 0.88588, + "eq_vapor_space_height": 0.885888, "heat_exchanger_area": 828.74, "height_crystallizer": 1.7717, "height_slurry": 0.763759, - "magma_circulation_flow_vol": 0.097350, - "product_volumetric_solids_fraction": 0.13195, - "relative_supersaturation": {"NaCl": 0.57239}, - "temperature_operating": 340.5214, + "magma_circulation_flow_vol": 0.09735, + "product_volumetric_solids_fraction": 0.131951, + "relative_supersaturation": {"NaCl": 0.572396}, + "t_res": 1.0228, + "temperature_operating": 340.52, "volume_suspension": 0.836915, "work_mechanical": {0.0: 1396.71}, }, 4: { - "delta_temperature": {0.0: 27.3299}, - "delta_temperature_in": {0.0: 17.299}, - "delta_temperature_out": {0.0: 40.695}, + "delta_temperature": {0.0: 27.32}, + "delta_temperature_in": {0.0: 17.29}, + "delta_temperature_out": {0.0: 40.69}, "dens_mass_magma": 278.12, "dens_mass_slurry": 1308.54, "diameter_crystallizer": 1.3795, - "energy_flow_superheated_vapor": 1250.3, + "energy_flow_superheated_vapor": 1250.34, "eq_max_allowable_velocity": 5.4596, "eq_minimum_height_diameter_ratio": 2.0692, - "eq_vapor_space_height": 1.03464, - "heat_exchanger_area": 474.4, + "eq_vapor_space_height": 1.0346, + "heat_exchanger_area": 474.46, "height_crystallizer": 2.0692, - "height_slurry": 0.53750, + "height_slurry": 0.537503, "magma_circulation_flow_vol": 0.089893, "product_volumetric_solids_fraction": 0.131503, "relative_supersaturation": {"NaCl": 0.576471}, "t_res": 1.0228, - "temperature_operating": 323.2, + "temperature_operating": 323.22, "volume_suspension": 0.803391, - "work_mechanical": {0.0: 1296.70}, + "work_mechanical": {0.0: 1296.7}, }, } @@ -609,15 +620,11 @@ def test_solution(self, MEC4_frame): assert pytest.approx(value(effv), rel=1e-3) == r steam_results_dict = { - "dens_mass_phase": {"Liq": 943.14, "Vap": 0.55862}, - "dh_vap_mass": 2202682, - "flow_mass_phase_comp": { - ("Liq", "H2O"): 0.0, - ("Vap", "H2O"): 0.77381, - }, - "flow_vol_phase": {"Liq": 0.0, "Vap": 1.3852}, - "pressure_sat": 197743, - "temperature": 393, + "flow_mass_phase_comp": {("Liq", "H2O"): 0.0, ("Vap", "H2O"): 0.799053}, + "temperature": 416.87, + "pressure": 401325.0, + "dh_vap_mass": 2133119.0, + "pressure_sat": 401325.0, } for v, r in steam_results_dict.items(): @@ -646,26 +653,24 @@ def test_costing(self, MEC4_frame): assert_optimal_termination(results) sys_costing_dict = { - "LCOW": 5.1605, - "SEC": 0.651465, - "NaCl_recovered_cost": -0.024, - "aggregate_capital_cost": 4613044.1, - "aggregate_direct_capital_cost": 2306522, - "aggregate_flow_NaCl_recovered": 0.26696, + "aggregate_capital_cost": 4527295.03, + "aggregate_flow_electricity": 7.5296, + "aggregate_flow_NaCl_recovered": 0.266962, + "aggregate_flow_steam": 0.383078, "aggregate_flow_costs": { - "NaCl_recovered": -240108.0, "electricity": 5423.99, - "steam": 102690.74, + "NaCl_recovered": -240108.02, + "steam": 56766.9, }, - "aggregate_flow_electricity": 7.5296, - "aggregate_flow_steam": 0.69298, - "capital_recovery_factor": 0.111955, - "maintenance_labor_chemical_operating_cost": 138391.3, - "total_annualized_cost": 522855.76, - "total_capital_cost": 4613044.1, - "total_fixed_operating_cost": 138391.32, - "total_operating_cost": 6398.03, - "total_variable_operating_cost": -131993.29, + "aggregate_direct_capital_cost": 2263647.51, + "total_capital_cost": 4527295.03, + "total_operating_cost": -42098.28, + "maintenance_labor_chemical_operating_cost": 135818.85, + "total_fixed_operating_cost": 135818.85, + "total_variable_operating_cost": -177917.13, + "total_annualized_cost": 464759.33, + "LCOW": 4.5871, + "SEC": 0.651465, } for v, r in sys_costing_dict.items(): @@ -677,11 +682,11 @@ def test_costing(self, MEC4_frame): assert pytest.approx(value(cv), rel=1e-3) == r eff_costing_dict = { - "capital_cost": 4613044.11, - "direct_capital_cost": 2306522.05, + "capital_cost": 4527295.03, + "direct_capital_cost": 2263647.51, "capital_cost_crystallizer_effect_1": 659171.48, - "capital_cost_heat_exchanger_effect_1": 294820.27, - "capital_cost_effect_1": 476995.87, + "capital_cost_heat_exchanger_effect_1": 209071.19, + "capital_cost_effect_1": 434121.33, "capital_cost_crystallizer_effect_2": 627459.32, "capital_cost_heat_exchanger_effect_2": 498502.0, "capital_cost_effect_2": 562980.66, @@ -732,25 +737,26 @@ def test_costing_by_volume(self): assert_optimal_termination(results) sys_costing_dict = { - "LCOW": 5.364, - "SEC": 0.65146, - "aggregate_capital_cost": 4758270.1, - "aggregate_direct_capital_cost": 2379135.0, - "aggregate_flow_NaCl_recovered": 0.26696, + "aggregate_capital_cost": 4672521.02, + "aggregate_fixed_operating_cost": 0.0, + "aggregate_variable_operating_cost": 0.0, + "aggregate_flow_electricity": 7.5296, + "aggregate_flow_NaCl_recovered": 0.266962, + "aggregate_flow_steam": 0.383078, "aggregate_flow_costs": { + "electricity": 5423.99, "NaCl_recovered": -240108.02, - "electricity": 5423.9, - "steam": 102690.74, + "steam": 56766.9, }, - "aggregate_flow_electricity": 7.529, - "aggregate_flow_steam": 0.69298, - "capital_recovery_factor": 0.11195, - "maintenance_labor_chemical_operating_cost": 142748.1, - "total_annualized_cost": 543471.45, - "total_capital_cost": 4758270.10, - "total_fixed_operating_cost": 142748.1, - "total_operating_cost": 10754.8, - "total_variable_operating_cost": -131993.29, + "aggregate_direct_capital_cost": 2336260.51, + "total_capital_cost": 4672521.02, + "total_operating_cost": -37741.5, + "maintenance_labor_chemical_operating_cost": 140175.63, + "total_fixed_operating_cost": 140175.63, + "total_variable_operating_cost": -177917.13, + "total_annualized_cost": 485375.02, + "LCOW": 4.7906, + "SEC": 0.651465, } for v, r in sys_costing_dict.items(): @@ -762,12 +768,20 @@ def test_costing_by_volume(self): assert pytest.approx(value(cv), rel=1e-3) == r eff_costing_dict = { - "capital_cost": 4758270.1, - "capital_cost_effect_1": 485242.6, - "capital_cost_effect_2": 578618.5, - "capital_cost_effect_3": 744925.5, - "capital_cost_effect_4": 570348.2, - "direct_capital_cost": 2379135.0, + "capital_cost": 4672521.02, + "direct_capital_cost": 2336260.51, + "capital_cost_crystallizer_effect_1": 675665.09, + "capital_cost_heat_exchanger_effect_1": 209071.19, + "capital_cost_effect_1": 442368.14, + "capital_cost_crystallizer_effect_2": 658735.08, + "capital_cost_heat_exchanger_effect_2": 498502.0, + "capital_cost_effect_2": 578618.54, + "capital_cost_crystallizer_effect_3": 638712.62, + "capital_cost_heat_exchanger_effect_3": 851138.46, + "capital_cost_effect_3": 744925.54, + "capital_cost_crystallizer_effect_4": 648559.35, + "capital_cost_heat_exchanger_effect_4": 492137.2, + "capital_cost_effect_4": 570348.28, } for v, r in eff_costing_dict.items(): @@ -844,11 +858,17 @@ def test_initialize(self, MEC3_frame): .is_fixed() ) assert ( - value( - eff.effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] + pytest.approx( + value( + eff.effect.properties_in[0].conc_mass_phase_comp[ + "Liq", "NaCl" + ] + ), + rel=1e-3, ) == c0 ) + c = getattr(eff.effect, f"eq_energy_for_effect_{n}_from_effect_{n - 1}") assert c.active assert eff.effect.overall_heat_transfer_coefficient.is_fixed() @@ -953,53 +973,55 @@ def test_solution(self, MEC3_frame): unit_results_dict = { 1: { - "delta_temperature": {0.0: 161.4093}, - "delta_temperature_in": {0.0: 123.1938}, - "delta_temperature_out": {0.0: 206.85}, - "dens_mass_magma": 337.00, + "delta_temperature": {0.0: 68.7}, + "delta_temperature_in": {0.0: 35.33}, + "delta_temperature_out": {0.0: 118.98}, + "dens_mass_magma": 337.0, "dens_mass_slurry": 1318.39, "diameter_crystallizer": 2.4814, "energy_flow_superheated_vapor": 10546.62, - "eq_max_allowable_velocity": 1.95486, - "eq_minimum_height_diameter_ratio": 3.72212, - "eq_vapor_space_height": 1.8610, - "heat_exchanger_area": 776.59, - "height_crystallizer": 4.96774, - "height_slurry": 3.10668, - "magma_circulation_flow_vol": 0.89371, - "product_volumetric_solids_fraction": 0.15933, + "eq_max_allowable_velocity": 1.9548, + "eq_minimum_height_diameter_ratio": 3.7221, + "eq_vapor_space_height": 1.861, + "heat_exchanger_area": 1824.34, + "height_crystallizer": 4.9677, + "height_slurry": 3.1066, + "magma_circulation_flow_vol": 0.893716, + "product_volumetric_solids_fraction": 0.159339, "relative_supersaturation": {"NaCl": 0.654196}, - "temperature_operating": 376.80, - "volume_suspension": 15.0239, - "work_mechanical": {0.0: 12535}, + "t_res": 1.0228, + "temperature_operating": 376.8, + "volume_suspension": 15.02, + "work_mechanical": {0.0: 12535.0}, }, 2: { - "delta_temperature": {0.0: 50.4936}, - "delta_temperature_in": {0.0: 31.9425}, - "delta_temperature_out": {0.0: 75.2194}, + "delta_temperature": {0.0: 50.49}, + "delta_temperature_in": {0.0: 31.94}, + "delta_temperature_out": {0.0: 75.21}, "dens_mass_magma": 331.37, "dens_mass_slurry": 1324.86, "diameter_crystallizer": 3.0991, - "energy_flow_superheated_vapor": 9677.8, + "energy_flow_superheated_vapor": 9677.81, "eq_max_allowable_velocity": 3.4641, "eq_minimum_height_diameter_ratio": 4.6487, "eq_vapor_space_height": 2.3243, - "heat_exchanger_area": 2088.70, + "heat_exchanger_area": 2088.7, "height_crystallizer": 4.6487, "height_slurry": 1.8467, - "magma_circulation_flow_vol": 0.7461, - "product_volumetric_solids_fraction": 0.15667, - "relative_supersaturation": {"NaCl": 0.6664}, + "magma_circulation_flow_vol": 0.746131, + "product_volumetric_solids_fraction": 0.156677, + "relative_supersaturation": {"NaCl": 0.666441}, + "t_res": 1.0228, "temperature_operating": 344.86, - "volume_suspension": 13.9312, - "work_mechanical": {0.0: 10546.6}, + "volume_suspension": 13.93, + "work_mechanical": {0.0: 10546.62}, }, 3: { - "delta_temperature": {0.0: 16.8533}, + "delta_temperature": {0.0: 16.85}, "delta_temperature_in": {0.0: 4.3421}, - "delta_temperature_out": {0.0: 44.8351}, + "delta_temperature_out": {0.0: 44.83}, "dens_mass_magma": 330.8, - "dens_mass_slurry": 1325.9, + "dens_mass_slurry": 1325.96, "diameter_crystallizer": 3.1102, "energy_flow_superheated_vapor": 8990.6, "eq_max_allowable_velocity": 3.7763, @@ -1008,12 +1030,13 @@ def test_solution(self, MEC3_frame): "heat_exchanger_area": 5742.36, "height_crystallizer": 4.6653, "height_slurry": 1.7045, - "magma_circulation_flow_vol": 0.68382, - "product_volumetric_solids_fraction": 0.156410, + "magma_circulation_flow_vol": 0.683822, + "product_volumetric_solids_fraction": 0.15641, "relative_supersaturation": {"NaCl": 0.667858}, - "temperature_operating": 340.5, - "volume_suspension": 12.950, - "work_mechanical": {0.0: 9677.8}, + "t_res": 1.0228, + "temperature_operating": 340.52, + "volume_suspension": 12.95, + "work_mechanical": {0.0: 9677.81}, }, } @@ -1028,11 +1051,11 @@ def test_solution(self, MEC3_frame): assert pytest.approx(value(effv), rel=1e-3) == r steam_results_dict = { - "dens_mass_phase": {"Liq": 828.03, "Vap": 0.439081}, - "dh_vap_mass": 1827723.26, - "flow_mass_phase_comp": {("Liq", "H2O"): 0.0, ("Vap", "H2O"): 6.858}, - "flow_vol_phase": {"Liq": 0.0, "Vap": 15.619}, - "pressure_sat": 2639870.3, + "flow_mass_phase_comp": {("Liq", "H2O"): 0.0, ("Vap", "H2O"): 5.8372}, + "temperature": 412.13, + "pressure": 351325.0, + "dh_vap_mass": 2147398.27, + "pressure_sat": 351325.0, } for v, r in steam_results_dict.items(): @@ -1061,24 +1084,26 @@ def test_costing(self, MEC3_frame): assert_optimal_termination(results) sys_costing_dict = { - "LCOW": 2.7665, - "SEC": 0.51529, - "aggregate_capital_cost": 17518907.2, - "aggregate_direct_capital_cost": 8759453.6, - "aggregate_flow_NaCl_recovered": 3.7919, + "LCOW": 3.505301, + "aggregate_capital_cost": 18573931.258, + "aggregate_direct_capital_cost": 9286965.629, + "aggregate_flow_NaCl_recovered": 3.791939, "aggregate_flow_costs": { - "NaCl_recovered": -568416.4, - "electricity": 31017.1, - "steam": 76976.0, + "NaCl_recovered": -568416.44, + "electricity": 31017.111, + "steam": 468332.767, }, - "aggregate_flow_electricity": 43.05, - "aggregate_flow_steam": 0.51945, - "capital_recovery_factor": 0.11195, - "total_annualized_cost": 2026489.8, - "total_capital_cost": 17518907.2, - "total_fixed_operating_cost": 525567.2, - "total_operating_cost": 65143.9, - "total_variable_operating_cost": -460423.2, + "aggregate_flow_electricity": 43.058, + "aggregate_flow_steam": 3.160433, + "capital_recovery_factor": 0.111955949, + "maintenance_labor_chemical_operating_cost": 557217.937, + "total_annualized_cost": 2567613.48, + "total_capital_cost": 18573931.258, + "total_fixed_operating_cost": 557217.937, + "total_operating_cost": 488151.375, + "total_variable_operating_cost": -69066.562, + "utilization_factor": 1.0, + "wacc": 0.0930733, } for v, r in sys_costing_dict.items(): @@ -1090,17 +1115,17 @@ def test_costing(self, MEC3_frame): assert pytest.approx(value(cv), rel=1e-3) == r eff_costing_dict = { - "capital_cost": 17518907.2, + "capital_cost": 18573931.2, "capital_cost_crystallizer_effect_1": 3079736.6, "capital_cost_crystallizer_effect_2": 2932488.6, "capital_cost_crystallizer_effect_3": 2818630.8, - "capital_cost_effect_1": 1939078.0, + "capital_cost_effect_1": 2466590.0, "capital_cost_effect_2": 2525607.6, "capital_cost_effect_3": 4294767.8, - "capital_cost_heat_exchanger_effect_1": 798419.4, + "capital_cost_heat_exchanger_effect_1": 1853443.4, "capital_cost_heat_exchanger_effect_2": 2118726.6, "capital_cost_heat_exchanger_effect_3": 5770904.9, - "direct_capital_cost": 8759453.6, + "direct_capital_cost": 9286965.6, } for v, r in eff_costing_dict.items(): From b137a9fb298964c5d0b30b55c88bab465ca3235c Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 26 Sep 2024 18:34:40 -0600 Subject: [PATCH 59/80] black! --- .../reflo/costing/units/multi_effect_crystallizer.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/units/multi_effect_crystallizer.py b/src/watertap_contrib/reflo/costing/units/multi_effect_crystallizer.py index 49ec215b..f5191ebe 100644 --- a/src/watertap_contrib/reflo/costing/units/multi_effect_crystallizer.py +++ b/src/watertap_contrib/reflo/costing/units/multi_effect_crystallizer.py @@ -352,8 +352,7 @@ def _cost_effect_flows(effect, effect_number): ) if effect_number == 1: - costing_package.cost_flow(effect.heating_steam[0].flow_vol_phase["Vap"], + costing_package.cost_flow( + effect.heating_steam[0].flow_vol_phase["Vap"], "steam", ) - - \ No newline at end of file From 1cfc2d2ccf320df2ad239a7b9dcba13a2bcc2402 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 26 Sep 2024 18:35:12 -0600 Subject: [PATCH 60/80] remove watertap crystallizer costing pkg --- .../costing/units/crystallizer_watertap.py | 289 ------------------ 1 file changed, 289 deletions(-) delete mode 100644 src/watertap_contrib/reflo/costing/units/crystallizer_watertap.py diff --git a/src/watertap_contrib/reflo/costing/units/crystallizer_watertap.py b/src/watertap_contrib/reflo/costing/units/crystallizer_watertap.py deleted file mode 100644 index 67d26c98..00000000 --- a/src/watertap_contrib/reflo/costing/units/crystallizer_watertap.py +++ /dev/null @@ -1,289 +0,0 @@ -################################################################################# -# WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, -# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, -# National Renewable Energy Laboratory, and National Energy Technology -# Laboratory (subject to receipt of any required approvals from the U.S. Dept. -# of Energy). All rights reserved. -# -# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license -# information, respectively. These files are also available online at the URL -# "https://github.com/watertap-org/watertap/" -################################################################################# - -import pyomo.environ as pyo -from idaes.core.util.misc import StrEnum -from idaes.core.util.constants import Constants -from watertap.costing.util import register_costing_parameter_block -from watertap_contrib.reflo.costing.util import ( - make_capital_cost_var, - make_fixed_operating_cost_var, -) - - -class CrystallizerCostType(StrEnum): - default = "default" - mass_basis = "mass_basis" - volume_basis = "volume_basis" - - -def build_crystallizer_watertap_cost_param_block(blk): - - blk.steam_pressure = pyo.Var( - initialize=3, - units=pyo.units.bar, - doc="Steam pressure (gauge) for crystallizer heating: 3 bar default based on Dutta example", - ) - - blk.efficiency_pump = pyo.Var( - initialize=0.7, - units=pyo.units.dimensionless, - doc="Crystallizer pump efficiency - assumed", - ) - - blk.pump_head_height = pyo.Var( - initialize=1, - units=pyo.units.m, - doc="Crystallizer pump head height - assumed, unvalidated", - ) - - # Crystallizer operating cost information from literature - blk.fob_unit_cost = pyo.Var( - initialize=675000, - doc="Forced circulation crystallizer reference free-on-board cost (Woods, 2007)", - units=pyo.units.USD_2007, - ) - - blk.ref_capacity = pyo.Var( - initialize=1, - doc="Forced circulation crystallizer reference crystal capacity (Woods, 2007)", - units=pyo.units.kg / pyo.units.s, - ) - - blk.ref_exponent = pyo.Var( - initialize=0.53, - doc="Forced circulation crystallizer cost exponent factor (Woods, 2007)", - units=pyo.units.dimensionless, - ) - - blk.iec_percent = pyo.Var( - initialize=1.43, - doc="Forced circulation crystallizer installed equipment cost (Diab and Gerogiorgis, 2017)", - units=pyo.units.dimensionless, - ) - - blk.volume_cost = pyo.Var( - initialize=16320, - doc="Forced circulation crystallizer cost per volume (Yusuf et al., 2019)", - units=pyo.units.USD_2007, ## TODO: Needs confirmation, but data is from Perry apparently - ) - - blk.vol_basis_exponent = pyo.Var( - initialize=0.47, - doc="Forced circulation crystallizer volume-based cost exponent (Yusuf et al., 2019)", - units=pyo.units.dimensionless, - ) - - blk.steam_cost = pyo.Var( - initialize=0.004, - units=pyo.units.USD_2018 / (pyo.units.meter**3), - doc="Steam cost, Panagopoulos (2019)", - ) - - blk.NaCl_recovery_value = pyo.Var( - initialize=0, - units=pyo.units.USD_2018 / pyo.units.kg, - doc="Unit recovery value of NaCl", - ) - - costing = blk.parent_block() - costing.register_flow_type("steam", blk.steam_cost) - costing.register_flow_type("NaCl", blk.NaCl_recovery_value) - - -def cost_crystallizer_watertap(blk, cost_type=CrystallizerCostType.default): - """ - Function for costing the FC crystallizer by the mass flow of produced crystals. - The operating cost model assumes that heat is supplied via condensation of saturated steam (see Dutta et al.) - - Args: - cost_type: Option for crystallizer cost function type - volume or mass basis - """ - if ( - cost_type == CrystallizerCostType.default - or cost_type == CrystallizerCostType.mass_basis - ): - cost_crystallizer_by_crystal_mass(blk) - elif cost_type == CrystallizerCostType.volume_basis: - cost_crystallizer_by_volume(blk) - else: - raise ConfigurationError( - f"{blk.unit_model.name} received invalid argument for cost_type:" - f" {cost_type}. Argument must be a member of the CrystallizerCostType Enum." - ) - - -def _cost_crystallizer_flows(blk): - blk.costing_package.cost_flow( - pyo.units.convert( - ( - blk.unit_model.magma_circulation_flow_vol - * blk.unit_model.dens_mass_slurry - * Constants.acceleration_gravity - * blk.costing_package.crystallizer.pump_head_height - / blk.costing_package.crystallizer.efficiency_pump - ), - to_units=pyo.units.kW, - ), - "electricity", - ) - - blk.costing_package.cost_flow( - pyo.units.convert( - (blk.unit_model.work_mechanical[0] / _compute_steam_properties(blk)), - to_units=pyo.units.m**3 / pyo.units.s, - ), - "steam", - ) - - blk.costing_package.cost_flow( - blk.unit_model.solids.flow_mass_phase_comp[0, "Sol", "NaCl"], - "NaCl", - ) - - -@register_costing_parameter_block( - build_rule=build_crystallizer_watertap_cost_param_block, - parameter_block_name="crystallizer", -) -def cost_crystallizer_by_crystal_mass(blk): - """ - Mass-based capital cost for FC crystallizer - """ - make_capital_cost_var(blk) - blk.costing_package.add_cost_factor(blk, "TIC") - blk.capital_cost_constraint = pyo.Constraint( - expr=blk.capital_cost - == blk.cost_factor - * pyo.units.convert( - ( - blk.costing_package.crystallizer.iec_percent - * blk.costing_package.crystallizer.fob_unit_cost - * ( - sum( - blk.unit_model.solids.flow_mass_phase_comp[0, "Sol", j] - for j in blk.unit_model.config.property_package.solute_set - ) - / blk.costing_package.crystallizer.ref_capacity - ) - ** blk.costing_package.crystallizer.ref_exponent - ), - to_units=blk.costing_package.base_currency, - ) - ) - _cost_crystallizer_flows(blk) - - -@register_costing_parameter_block( - build_rule=build_crystallizer_watertap_cost_param_block, - parameter_block_name="crystallizer", -) -def cost_crystallizer_by_volume(blk): - """ - Volume-based capital cost for FC crystallizer - """ - make_capital_cost_var(blk) - blk.costing_package.add_cost_factor(blk, "TIC") - blk.capital_cost_constraint = pyo.Constraint( - expr=blk.capital_cost - == blk.cost_factor - * pyo.units.convert( - ( - blk.costing_package.crystallizer.volume_cost - * ( - ( - pyo.units.convert( - blk.unit_model.volume_suspension - * ( - blk.unit_model.height_crystallizer - / blk.unit_model.height_slurry - ), - to_units=(pyo.units.ft) ** 3, - ) - ) - / pyo.units.ft**3 - ) - ** blk.costing_package.crystallizer.vol_basis_exponent - ), - to_units=blk.costing_package.base_currency, - ) - ) - _cost_crystallizer_flows(blk) - - -def _compute_steam_properties(blk): - """ - Function for computing saturated steam properties for thermal heating estimation. - - Args: - pressure_sat: Steam gauge pressure in bar - - Out: - Steam thermal capacity (latent heat of condensation * density) in kJ/m3 - """ - pressure_sat = blk.costing_package.crystallizer.steam_pressure - # 1. Compute saturation temperature of steam: computed from El-Dessouky expression - tsat_constants = [ - 42.6776 * pyo.units.K, - -3892.7 * pyo.units.K, - 1000 * pyo.units.kPa, - -9.48654 * pyo.units.dimensionless, - ] - psat = ( - pyo.units.convert(pressure_sat, to_units=pyo.units.kPa) - + 101.325 * pyo.units.kPa - ) - temperature_sat = tsat_constants[0] + tsat_constants[1] / ( - pyo.log(psat / tsat_constants[2]) + tsat_constants[3] - ) - - # 2. Compute latent heat of condensation/vaporization: computed from Sharqawy expression - t = temperature_sat - 273.15 * pyo.units.K - enth_mass_units = pyo.units.J / pyo.units.kg - t_inv_units = pyo.units.K**-1 - dh_constants = [ - 2.501e6 * enth_mass_units, - -2.369e3 * enth_mass_units * t_inv_units**1, - 2.678e-1 * enth_mass_units * t_inv_units**2, - -8.103e-3 * enth_mass_units * t_inv_units**3, - -2.079e-5 * enth_mass_units * t_inv_units**4, - ] - dh_vap = ( - dh_constants[0] - + dh_constants[1] * t - + dh_constants[2] * t**2 - + dh_constants[3] * t**3 - + dh_constants[4] * t**4 - ) - dh_vap = pyo.units.convert(dh_vap, to_units=pyo.units.kJ / pyo.units.kg) - - # 3. Compute specific volume: computed from Affandi expression (Eq 5) - t_critical = 647.096 * pyo.units.K - t_red = temperature_sat / t_critical # Reduced temperature - sp_vol_constants = [ - -7.75883 * pyo.units.dimensionless, - 3.23753 * pyo.units.dimensionless, - 2.05755 * pyo.units.dimensionless, - -0.06052 * pyo.units.dimensionless, - 0.00529 * pyo.units.dimensionless, - ] - log_sp_vol = ( - sp_vol_constants[0] - + sp_vol_constants[1] * (pyo.log(1 / t_red)) ** 0.4 - + sp_vol_constants[2] / (t_red**2) - + sp_vol_constants[3] / (t_red**4) - + sp_vol_constants[4] / (t_red**5) - ) - sp_vol = pyo.exp(log_sp_vol) * pyo.units.m**3 / pyo.units.kg - - # 4. Return specific energy: density * latent heat - return dh_vap / sp_vol From d71d7f9a078215d382bf8b5514417892aa1ea8be Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 26 Sep 2024 18:40:09 -0600 Subject: [PATCH 61/80] remove crystallizer_watertap from init --- src/watertap_contrib/reflo/costing/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/watertap_contrib/reflo/costing/__init__.py b/src/watertap_contrib/reflo/costing/__init__.py index 90602c65..bced7895 100644 --- a/src/watertap_contrib/reflo/costing/__init__.py +++ b/src/watertap_contrib/reflo/costing/__init__.py @@ -18,4 +18,3 @@ EnergyCosting, ) -from .units.crystallizer_watertap import CrystallizerCostType, _compute_steam_properties From 184f4a83c30fef91cfa816f6954aba9de29cc60c Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 26 Sep 2024 18:41:01 -0600 Subject: [PATCH 62/80] blackblackblackblackblackblackblackblackblackblackblackblackblackblackblackblackblackblackblackblackblackblackblackblackblackblackblackblackblackblackblackblackblackblackblackblackblackblackblackblack --- src/watertap_contrib/reflo/costing/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/watertap_contrib/reflo/costing/__init__.py b/src/watertap_contrib/reflo/costing/__init__.py index bced7895..b80c8fac 100644 --- a/src/watertap_contrib/reflo/costing/__init__.py +++ b/src/watertap_contrib/reflo/costing/__init__.py @@ -17,4 +17,3 @@ TreatmentCosting, EnergyCosting, ) - From 78108aad3ee27b87e0b6a763b61da73c093b8211 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Fri, 27 Sep 2024 10:02:02 -0600 Subject: [PATCH 63/80] fix test for windows --- .../tests/test_multi_effect_crystallizer.py | 150 ++++++++---------- 1 file changed, 70 insertions(+), 80 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/tests/test_multi_effect_crystallizer.py b/src/watertap_contrib/reflo/unit_models/tests/test_multi_effect_crystallizer.py index c21b567d..d024641a 100644 --- a/src/watertap_contrib/reflo/unit_models/tests/test_multi_effect_crystallizer.py +++ b/src/watertap_contrib/reflo/unit_models/tests/test_multi_effect_crystallizer.py @@ -142,11 +142,11 @@ def build_mec3(): feed_flow_mass = 10 feed_mass_frac_NaCl = 0.25 crystallizer_yield = 0.55 - operating_pressures = [0.85, 0.25, 0.208] + operating_pressures = [0.45, 0.25, 0.208] feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl atm_pressure = 101325 * pyunits.Pa - saturated_steam_pressure_gage = 2.5 * pyunits.bar + saturated_steam_pressure_gage = 3.5 * pyunits.bar saturated_steam_pressure = atm_pressure + pyunits.convert( saturated_steam_pressure_gage, to_units=pyunits.Pa ) @@ -954,7 +954,7 @@ def test_conservation(self, MEC3_frame): <= 1e-2 ) - assert pytest.approx(value(m.fs.unit.total_flow_vol_in), rel=1e-3) == 0.02321134 + assert pytest.approx(value(m.fs.unit.total_flow_vol_in), rel=1e-3) == 0.0232595 assert ( pytest.approx( @@ -964,7 +964,7 @@ def test_conservation(self, MEC3_frame): ), rel=1e-3, ) - == 0.02321134 + == 0.0232595 ) @pytest.mark.component @@ -973,70 +973,61 @@ def test_solution(self, MEC3_frame): unit_results_dict = { 1: { - "delta_temperature": {0.0: 68.7}, - "delta_temperature_in": {0.0: 35.33}, - "delta_temperature_out": {0.0: 118.98}, - "dens_mass_magma": 337.0, - "dens_mass_slurry": 1318.39, - "diameter_crystallizer": 2.4814, - "energy_flow_superheated_vapor": 10546.62, - "eq_max_allowable_velocity": 1.9548, - "eq_minimum_height_diameter_ratio": 3.7221, - "eq_vapor_space_height": 1.861, - "heat_exchanger_area": 1824.34, - "height_crystallizer": 4.9677, - "height_slurry": 3.1066, - "magma_circulation_flow_vol": 0.893716, - "product_volumetric_solids_fraction": 0.159339, - "relative_supersaturation": {"NaCl": 0.654196}, + "dens_mass_magma": 333.63, + "dens_mass_slurry": 1321.57, + "diameter_crystallizer": 2.8505, + "energy_flow_superheated_vapor": 10580.97, + "eq_max_allowable_velocity": 2.6304, + "eq_minimum_height_diameter_ratio": 4.2758, + "eq_vapor_space_height": 2.1379, + "heat_exchanger_area": 1322.35, + "height_crystallizer": 4.5158, + "height_slurry": 2.3779, + "magma_circulation_flow_vol": 0.852729, + "product_volumetric_solids_fraction": 0.157748, + "relative_supersaturation": {"NaCl": 0.661231}, "t_res": 1.0228, - "temperature_operating": 376.8, - "volume_suspension": 15.02, - "work_mechanical": {0.0: 12535.0}, + "temperature_operating": 359.48, + "volume_suspension": 15.17, + "work_mechanical": {0.0: 12008.03}, }, 2: { - "delta_temperature": {0.0: 50.49}, - "delta_temperature_in": {0.0: 31.94}, - "delta_temperature_out": {0.0: 75.21}, "dens_mass_magma": 331.37, "dens_mass_slurry": 1324.86, - "diameter_crystallizer": 3.0991, - "energy_flow_superheated_vapor": 9677.81, + "diameter_crystallizer": 3.1042, + "energy_flow_superheated_vapor": 9709.33, "eq_max_allowable_velocity": 3.4641, - "eq_minimum_height_diameter_ratio": 4.6487, - "eq_vapor_space_height": 2.3243, - "heat_exchanger_area": 2088.7, - "height_crystallizer": 4.6487, + "eq_minimum_height_diameter_ratio": 4.6563, + "eq_vapor_space_height": 2.3281, + "heat_exchanger_area": 3349.19, + "height_crystallizer": 4.6563, "height_slurry": 1.8467, - "magma_circulation_flow_vol": 0.746131, + "magma_circulation_flow_vol": 0.748561, "product_volumetric_solids_fraction": 0.156677, "relative_supersaturation": {"NaCl": 0.666441}, "t_res": 1.0228, "temperature_operating": 344.86, - "volume_suspension": 13.93, - "work_mechanical": {0.0: 10546.62}, + "volume_suspension": 13.97, + "work_mechanical": {0.0: 10580.97}, }, 3: { - "delta_temperature": {0.0: 16.85}, - "delta_temperature_in": {0.0: 4.3421}, - "delta_temperature_out": {0.0: 44.83}, "dens_mass_magma": 330.8, "dens_mass_slurry": 1325.96, - "diameter_crystallizer": 3.1102, - "energy_flow_superheated_vapor": 8990.6, + "diameter_crystallizer": 3.1152, + "energy_flow_superheated_vapor": 9019.89, "eq_max_allowable_velocity": 3.7763, - "eq_minimum_height_diameter_ratio": 4.6653, - "eq_vapor_space_height": 2.3326, - "heat_exchanger_area": 5742.36, - "height_crystallizer": 4.6653, + "eq_minimum_height_diameter_ratio": 4.6729, + "eq_vapor_space_height": 2.3364, + "heat_exchanger_area": 5761.07, + "height_crystallizer": 4.6729, "height_slurry": 1.7045, - "magma_circulation_flow_vol": 0.683822, + "magma_circulation_flow_vol": 0.686049, "product_volumetric_solids_fraction": 0.15641, "relative_supersaturation": {"NaCl": 0.667858}, "t_res": 1.0228, "temperature_operating": 340.52, - "volume_suspension": 12.95, - "work_mechanical": {0.0: 9677.81}, + "volume_suspension": 12.99, + "work_mechanical": {0.0: 9709.33}, }, } @@ -1051,11 +1042,11 @@ def test_solution(self, MEC3_frame): assert pytest.approx(value(effv), rel=1e-3) == r steam_results_dict = { - "flow_mass_phase_comp": {("Liq", "H2O"): 0.0, ("Vap", "H2O"): 5.8372}, - "temperature": 412.13, - "pressure": 351325.0, - "dh_vap_mass": 2147398.27, - "pressure_sat": 351325.0, + "flow_mass_phase_comp": {("Liq", "H2O"): 0.0, ("Vap", "H2O"): 5.66421262}, + "temperature": 421.1592, + "pressure": 451325.0, + "dh_vap_mass": 2119982.2092, + "pressure_sat": 451324.9999, } for v, r in steam_results_dict.items(): @@ -1084,26 +1075,25 @@ def test_costing(self, MEC3_frame): assert_optimal_termination(results) sys_costing_dict = { - "LCOW": 3.505301, - "aggregate_capital_cost": 18573931.258, - "aggregate_direct_capital_cost": 9286965.629, - "aggregate_flow_NaCl_recovered": 3.791939, + "LCOW": 3.502332, + "aggregate_capital_cost": 19360250.738, + "aggregate_direct_capital_cost": 9680125.369, + "aggregate_flow_NaCl_recovered": 3.799812, "aggregate_flow_costs": { - "NaCl_recovered": -568416.44, - "electricity": 31017.111, - "steam": 468332.767, + "NaCl_recovered": -569596.699, + "electricity": 30561.497, + "steam": 361498.197, }, - "aggregate_flow_electricity": 43.058, - "aggregate_flow_steam": 3.160433, + "aggregate_flow_electricity": 42.425, + "aggregate_flow_steam": 2.439485, "capital_recovery_factor": 0.111955949, - "maintenance_labor_chemical_operating_cost": 557217.937, - "total_annualized_cost": 2567613.48, - "total_capital_cost": 18573931.258, - "total_fixed_operating_cost": 557217.937, - "total_operating_cost": 488151.375, - "total_variable_operating_cost": -69066.562, - "utilization_factor": 1.0, - "wacc": 0.0930733, + "maintenance_labor_chemical_operating_cost": 580807.522, + "total_annualized_cost": 2570765.766, + "total_capital_cost": 19360250.738, + "total_fixed_operating_cost": 580807.522, + "total_operating_cost": 403270.518, + "total_variable_operating_cost": -177537.004, + "wacc": 0.093073397, } for v, r in sys_costing_dict.items(): @@ -1115,17 +1105,17 @@ def test_costing(self, MEC3_frame): assert pytest.approx(value(cv), rel=1e-3) == r eff_costing_dict = { - "capital_cost": 18573931.2, - "capital_cost_crystallizer_effect_1": 3079736.6, - "capital_cost_crystallizer_effect_2": 2932488.6, - "capital_cost_crystallizer_effect_3": 2818630.8, - "capital_cost_effect_1": 2466590.0, - "capital_cost_effect_2": 2525607.6, - "capital_cost_effect_3": 4294767.8, - "capital_cost_heat_exchanger_effect_1": 1853443.4, - "capital_cost_heat_exchanger_effect_2": 2118726.6, - "capital_cost_heat_exchanger_effect_3": 5770904.9, - "direct_capital_cost": 9286965.6, + "capital_cost": 19360250.738, + "capital_cost_crystallizer_effect_1": 3079736.682, + "capital_cost_crystallizer_effect_2": 2937547.941, + "capital_cost_crystallizer_effect_3": 2823493.637, + "capital_cost_effect_1": 2214301.874, + "capital_cost_effect_2": 3159294.994, + "capital_cost_effect_3": 4306528.5, + "capital_cost_heat_exchanger_effect_1": 1348867.065, + "capital_cost_heat_exchanger_effect_2": 3381042.046, + "capital_cost_heat_exchanger_effect_3": 5789563.363, + "direct_capital_cost": 9680125.369, } for v, r in eff_costing_dict.items(): From 8c83a5619ad769a88e9f1e048d5ea0df1a91d7f2 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Mon, 7 Oct 2024 15:23:58 -0600 Subject: [PATCH 64/80] remove commented code --- .../reflo/unit_models/multi_effect_crystallizer.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py index 2e1f0b88..03ae0cdc 100644 --- a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py +++ b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py @@ -192,12 +192,6 @@ def eq_delta_temperature_outlet_effect_1(b): @effect.Constraint(doc="Heat transfer equation for first effect") def eq_heat_transfer_effect_1(b): - # return b.work_mechanical[0] == pyunits.convert( - # b.overall_heat_transfer_coefficient - # * b.heat_exchanger_area - # * b.delta_temperature[0] - # # , to_units=pyunits.kilowatt - # ) return ( b.work_mechanical[0] == b.overall_heat_transfer_coefficient From b166843c4e0858b5fb86f4e7def500e19f09d4b9 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Tue, 8 Oct 2024 14:10:01 -0600 Subject: [PATCH 65/80] prop_pure_water vapor flow = 0, liq flow = prop_vapor vapor flow --- .../reflo/unit_models/crystallizer_effect.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py index c6ce1a92..9f9e8aa7 100644 --- a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py +++ b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py @@ -93,10 +93,9 @@ def build(self): self.add_port(name="pure_water", block=self.properties_pure_water) - self.properties_pure_water[0].flow_mass_phase_comp["Liq", "H2O"].fix(1e-8) + self.properties_pure_water[0].flow_mass_phase_comp["Vap", "H2O"].fix(0) self.properties_pure_water[0].flow_mass_phase_comp["Liq", "NaCl"].fix(0) self.properties_pure_water[0].flow_mass_phase_comp["Sol", "NaCl"].fix(0) - self.properties_pure_water[0].mass_frac_phase_comp["Liq", "NaCl"] self.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] self.properties_in[0].flow_vol_phase["Liq"] @@ -166,11 +165,18 @@ def build(self): doc="Overall heat transfer coefficient for heat exchangers", ) + # @self.Constraint() + # def eq_pure_vapor_flow_rate(b): + # return ( + # b.properties_pure_water[0].flow_mass_phase_comp["Vap", "H2O"] + # == b.properties_vapor[0].flow_mass_phase_comp["Vap", "H2O"] + # ) + @self.Constraint() - def eq_pure_vapor_flow_rate(b): + def eq_pure_water_mass_flow_rate(b): return ( - b.properties_pure_water[0].flow_mass_phase_comp["Vap", "H2O"] - == b.properties_in[0].flow_mass_phase_comp["Vap", "H2O"] + b.properties_pure_water[0].flow_mass_phase_comp["Liq", "H2O"] + == b.properties_vapor[0].flow_mass_phase_comp["Vap", "H2O"] ) @self.Constraint(doc="Thermal energy in the vapor") From 651dba5869eaa5ee416879ca676be81b276de4a7 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Tue, 8 Oct 2024 14:21:57 -0600 Subject: [PATCH 66/80] add 0D control volume to multi_effect_crystallizer --- .../unit_models/multi_effect_crystallizer.py | 129 +++++++++++++++--- 1 file changed, 111 insertions(+), 18 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py index 03ae0cdc..415e6630 100644 --- a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py +++ b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py @@ -27,6 +27,7 @@ # Import IDAES cores from idaes.core import ( declare_process_block_class, + MaterialBalanceType, UnitModelBlockData, useDefault, FlowsheetBlock, @@ -39,7 +40,7 @@ import idaes.logger as idaeslog from idaes.core.util.constants import Constants -from watertap.core import InitializationMixin +from watertap.core import InitializationMixin, ControlVolume0DBlock from watertap.core.solvers import get_solver from watertap_contrib.reflo.unit_models.crystallizer_effect import CrystallizerEffect @@ -95,6 +96,20 @@ class MultiEffectCrystallizerData(InitializationMixin, UnitModelBlockData): ), ) + CONFIG.declare( + "property_package_vapor", + ConfigValue( + default=useDefault, + domain=is_physical_parameter_block, + description="Property package to use for heating steam properties", + doc="""Property parameter object used to define steam property calculations, + **default** - useDefault. + **Valid values:** { + **useDefault** - use default package from parent model or flowsheet, + **PhysicalParameterObject** - a PhysicalParameterBlock object.}""", + ), + ) + CONFIG.declare( "property_package_args", ConfigBlock( @@ -108,20 +123,6 @@ class MultiEffectCrystallizerData(InitializationMixin, UnitModelBlockData): ), ) - CONFIG.declare( - "property_package_vapor", - ConfigValue( - default=useDefault, - domain=is_physical_parameter_block, - description="Property package to use for heating and motive steam properties", - doc="""Property parameter object used to define steasm property calculations, - **default** - useDefault. - **Valid values:** { - **useDefault** - use default package from parent model or flowsheet, - **PhysicalParameterObject** - a PhysicalParameterBlock object.}""", - ), - ) - CONFIG.declare( "number_effects", ConfigValue( @@ -137,6 +138,20 @@ def build(self): self.scaling_factor = Suffix(direction=Suffix.EXPORT) + self.control_volume = ControlVolume0DBlock( + dynamic=False, + has_holdup=False, + property_package=self.config.property_package, + property_package_args=self.config.property_package_args, + ) + + self.control_volume.add_state_blocks(has_phase_equilibrium=False) + self.control_volume.add_material_balances( + balance_type=MaterialBalanceType.componentPhase, has_mass_transfer=True + ) + self.add_inlet_port(name="inlet", block=self.control_volume) + self.add_outlet_port(name="outlet", block=self.control_volume) + self.number_effects = self.config.number_effects self.Effects = RangeSet(self.config.number_effects) @@ -145,7 +160,19 @@ def build(self): self.effects = FlowsheetBlock(self.Effects, dynamic=False) + # There is no solid NaCl coming in + self.control_volume.properties_in[0].flow_mass_phase_comp["Sol", "NaCl"].fix(0) + # All solid NaCl accessed through properties_solids + self.control_volume.mass_transfer_term[0, "Sol", "NaCl"].fix(0) + + # Local expressions to aggregate material flows for all effects + total_mass_flow_water_in_expr = 0 + total_mass_flow_salt_in_expr = 0 + total_mass_flow_pure_water_out_expr = 0 + total_mass_flow_water_out_expr = 0 + total_mass_flow_salt_out_expr = 0 total_flow_vol_in_expr = 0 + total_flow_vol_out_expr = 0 for n, eff in self.effects.items(): eff.effect = effect = CrystallizerEffect( @@ -156,6 +183,30 @@ def build(self): effect.properties_in[0].conc_mass_phase_comp total_flow_vol_in_expr += effect.properties_in[0].flow_vol_phase["Liq"] + total_flow_vol_out_expr += effect.properties_pure_water[0].flow_vol_phase[ + "Liq" + ] + + total_mass_flow_water_in_expr += effect.properties_in[ + 0 + ].flow_mass_phase_comp["Liq", "H2O"] + + total_mass_flow_salt_in_expr += effect.properties_in[ + 0 + ].flow_mass_phase_comp["Liq", "NaCl"] + + total_mass_flow_pure_water_out_expr += effect.properties_pure_water[ + 0 + ].flow_mass_phase_comp["Liq", "H2O"] + + total_mass_flow_water_out_expr += effect.properties_out[ + 0 + ].flow_mass_phase_comp["Liq", "H2O"] + + total_mass_flow_salt_out_expr += ( + effect.properties_out[0].flow_mass_phase_comp["Liq", "NaCl"] + + effect.properties_solids[0].flow_mass_phase_comp["Sol", "NaCl"] + ) if n == self.first_effect: tmp_dict = dict(**self.config.property_package_args) @@ -209,8 +260,6 @@ def eq_heating_steam_flow_rate(b): ) ) - self.add_port(name="inlet", block=effect.properties_in) - self.add_port(name="outlet", block=effect.properties_out) self.add_port(name="solids", block=effect.properties_solids) self.add_port(name="vapor", block=effect.properties_vapor) self.add_port(name="pure_water", block=effect.properties_pure_water) @@ -259,6 +308,47 @@ def eq_heating_steam_flow_rate(b): f"eq_energy_for_effect_{n}_from_effect_{n - 1}", energy_flow_constr ) + @self.Constraint(doc="Mass transfer term for liquid water") + def eq_mass_transfer_term_liq_water(b): + return b.control_volume.mass_transfer_term[0, "Liq", "H2O"] == -1 * ( + total_mass_flow_water_out_expr + ) + + @self.Constraint(doc="Mass transfer term for vapor water") + def eq_mass_transfer_term_vap_water(b): + return b.control_volume.mass_transfer_term[0, "Vap", "H2O"] == -1 * ( + b.effects[1].effect.heating_steam[0].flow_mass_phase_comp["Vap", "H2O"] + ) + + @self.Constraint(doc="Mass transfer term for salt in liquid phase") + def eq_mass_transfer_term_liq_salt(b): + return b.control_volume.mass_transfer_term[0, "Liq", "NaCl"] == -1 * ( + total_mass_flow_salt_out_expr + ) + + @self.Constraint(doc="Steam flow") + def eq_overall_steam_flow(b): + return ( + b.control_volume.properties_in[0].flow_mass_phase_comp["Vap", "H2O"] + == b.effects[1] + .effect.heating_steam[0] + .flow_mass_phase_comp["Vap", "H2O"] + ) + + @self.Constraint(doc="Mass balance of water for all effects") + def eq_overall_mass_balance_water_in(b): + return ( + b.control_volume.properties_in[0].flow_mass_phase_comp["Liq", "H2O"] + == total_mass_flow_water_in_expr + ) + + @self.Constraint(doc="Mass balance of salt for all effects") + def eq_overall_mass_balance_salt_in(b): + return ( + b.control_volume.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"] + == total_mass_flow_salt_in_expr + ) + self.total_flow_vol_in = Expression(expr=total_flow_vol_in_expr) def initialize_build( @@ -294,8 +384,9 @@ def initialize_build( opt = get_solver(solver, optarg) for n, eff in self.effects.items(): + eff.effect.properties_in[0].flow_mass_phase_comp.fix() if n == 1: - assert degrees_of_freedom(eff.effect) == 0 + # assert degrees_of_freedom(eff.effect) == 0 eff.effect.initialize(**init_args) inlet_conc = ( eff.effect.properties_in[0] @@ -303,6 +394,8 @@ def initialize_build( .value ) mass_transfer_coeff = eff.effect.overall_heat_transfer_coefficient.value + eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "H2O"].unfix() + eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"].unfix() else: c = getattr(eff.effect, f"eq_energy_for_effect_{n}_from_effect_{n - 1}") c.deactivate() From a4976f2ae9d7205af6d4a2bad468eae8f86689f9 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Tue, 8 Oct 2024 14:44:36 -0600 Subject: [PATCH 67/80] improve initialization routine --- .../unit_models/multi_effect_crystallizer.py | 47 +++++++++++++++---- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py index 415e6630..c5b8d717 100644 --- a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py +++ b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py @@ -380,13 +380,36 @@ def initialize_build( ) init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") - solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit") + + flow_mass_water_initial = ( + self.control_volume.properties_in[0] + .flow_mass_phase_comp["Liq", "H2O"] + .value + / self.number_effects + ) + flow_mass_salt_initial = ( + self.control_volume.properties_in[0] + .flow_mass_phase_comp["Liq", "NaCl"] + .value + / self.number_effects + ) opt = get_solver(solver, optarg) + for n, eff in self.effects.items(): - eff.effect.properties_in[0].flow_mass_phase_comp.fix() + # Each effect is first solved in a vacuum with linking constraints deactivated + eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "H2O"].fix( + flow_mass_water_initial + ) + eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"].fix( + flow_mass_salt_initial + ) if n == 1: - # assert degrees_of_freedom(eff.effect) == 0 + if not degrees_of_freedom(eff.effect) == 0: + raise InitializationError( + f"Degrees of freedom in first effect must be zero during initialization, " + f"but has {degrees_of_freedom(eff.effect)}. Check inlet conditions and re-initialize." + ) eff.effect.initialize(**init_args) inlet_conc = ( eff.effect.properties_in[0] @@ -394,20 +417,24 @@ def initialize_build( .value ) mass_transfer_coeff = eff.effect.overall_heat_transfer_coefficient.value - eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "H2O"].unfix() - eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"].unfix() else: - c = getattr(eff.effect, f"eq_energy_for_effect_{n}_from_effect_{n - 1}") - c.deactivate() + # Deactivate contraint that links energy flow between effects + linking_constr = getattr( + eff.effect, f"eq_energy_for_effect_{n}_from_effect_{n - 1}" + ) + linking_constr.deactivate() eff.effect.initialize(**init_args) - c.activate() - eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "H2O"].unfix() - eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"].unfix() + linking_constr.activate() + # Fix inlet feed concentration to be equal across all effects eff.effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"].fix( inlet_conc ) eff.effect.overall_heat_transfer_coefficient.fix(mass_transfer_coeff) + # Unfix inlet mass flow rates to allow unit model to determine based on energy flows + eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "H2O"].unfix() + eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"].unfix() + init_log.info(f"Initialization of Effect {n} Complete.") with idaeslog.solver_log(init_log, idaeslog.DEBUG) as slc: From e6e4c528591dfc000bb9788521e13308aa508981 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Tue, 8 Oct 2024 14:52:37 -0600 Subject: [PATCH 68/80] add CV temperature and pressure at outlet --- .../unit_models/multi_effect_crystallizer.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py index c5b8d717..1df2f864 100644 --- a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py +++ b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py @@ -349,6 +349,20 @@ def eq_overall_mass_balance_salt_in(b): == total_mass_flow_salt_in_expr ) + @self.Constraint(doc="Control volume temperature at outlet") + def eq_temperature_outlet(b): + return ( + b.control_volume.properties_out[0].temperature + == b.effects[b.last_effect].effect.properties_pure_water[0].temperature + ) + + @self.Constraint(doc="Control volume temperature out") + def eq_isobaric(b): + return ( + b.control_volume.properties_out[0].pressure + == b.control_volume.properties_in[0].pressure + ) + self.total_flow_vol_in = Expression(expr=total_flow_vol_in_expr) def initialize_build( @@ -395,7 +409,7 @@ def initialize_build( ) opt = get_solver(solver, optarg) - + for n, eff in self.effects.items(): # Each effect is first solved in a vacuum with linking constraints deactivated eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "H2O"].fix( From 189a42cb90be52fb0c0b2ceaed9aae1c7692aa54 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Tue, 8 Oct 2024 17:34:48 -0600 Subject: [PATCH 69/80] add overall recovery vol phase --- .../unit_models/multi_effect_crystallizer.py | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py index 1df2f864..4b5024c0 100644 --- a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py +++ b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py @@ -16,6 +16,7 @@ ConcreteModel, check_optimal_termination, assert_optimal_termination, + Var, Constraint, Expression, Suffix, @@ -34,6 +35,7 @@ UnitModelCostingBlock, ) from idaes.core.util.exceptions import InitializationError +import idaes.core.util.scaling as iscale from idaes.core.util.config import is_physical_parameter_block from idaes.core.util.model_statistics import degrees_of_freedom from idaes.core.util.tables import create_stream_table_dataframe @@ -356,7 +358,7 @@ def eq_temperature_outlet(b): == b.effects[b.last_effect].effect.properties_pure_water[0].temperature ) - @self.Constraint(doc="Control volume temperature out") + @self.Constraint(doc="Control volume pressure at outlet") def eq_isobaric(b): return ( b.control_volume.properties_out[0].pressure @@ -365,6 +367,21 @@ def eq_isobaric(b): self.total_flow_vol_in = Expression(expr=total_flow_vol_in_expr) + self.recovery_vol_phase = Var( + ["Liq"], + initialize=0.75, + bounds=(0, 1), + units=pyunits.dimensionless, + doc="Overall unit recovery on volumetric basis", + ) + + @self.Constraint(doc="Volumetric recovery") + def eq_recovery_vol_phase(b): + return ( + b.recovery_vol_phase["Liq"] + == total_flow_vol_out_expr / total_flow_vol_in_expr + ) + def initialize_build( self, state_args=None, @@ -453,11 +470,17 @@ def initialize_build( with idaeslog.solver_log(init_log, idaeslog.DEBUG) as slc: res = opt.solve(self, tee=slc.tee) - init_log.info_high("Initialization Step 3 {}.".format(idaeslog.condition(res))) + init_log.info("Initialization Step 3 {}.".format(idaeslog.condition(res))) if not check_optimal_termination(res): raise InitializationError(f"Unit model {self.name} failed to initialize") + def calculate_scaling_factors(self): + super().calculate_scaling_factors() + + if iscale.get_scaling_factor(self.recovery_vol_phase) is None: + iscale.set_scaling_factor(self.recovery_vol_phase, 10) + @property def default_costing_method(self): return cost_multi_effect_crystallizer From 4d2bb63041c482e4799831c8ea6e2a5775c4e916 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Tue, 8 Oct 2024 17:35:35 -0600 Subject: [PATCH 70/80] update test to reflect unit model changes --- .../tests/test_multi_effect_crystallizer.py | 971 ++++++++++++------ 1 file changed, 640 insertions(+), 331 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/tests/test_multi_effect_crystallizer.py b/src/watertap_contrib/reflo/unit_models/tests/test_multi_effect_crystallizer.py index d024641a..0c382aff 100644 --- a/src/watertap_contrib/reflo/unit_models/tests/test_multi_effect_crystallizer.py +++ b/src/watertap_contrib/reflo/unit_models/tests/test_multi_effect_crystallizer.py @@ -55,10 +55,9 @@ __author__ = "Kurban Sitterley" solver = get_solver() - feed_pressure = 101325 feed_temperature = 273.15 + 20 -eps = 1e-6 +eps = 1e-12 def build_mec4(): @@ -80,6 +79,8 @@ def build_mec4(): crystallizer_yield = 0.5 feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl + total_feed_flow_mass = 4 + atm_pressure = 101325 * pyunits.Pa saturated_steam_pressure_gage = 3 * pyunits.bar saturated_steam_pressure = atm_pressure + pyunits.convert( @@ -111,6 +112,7 @@ def build_mec4(): first_effect = m.fs.unit.effects[1].effect first_effect.overall_heat_transfer_coefficient.fix(0.1) + first_effect.heating_steam[0].pressure_sat first_effect.heating_steam[0].dh_vap_mass first_effect.heating_steam.calculate_state( var_args={ @@ -122,6 +124,16 @@ def build_mec4(): ) first_effect.heating_steam[0].flow_mass_phase_comp["Vap", "H2O"].unfix() + m.fs.unit.control_volume.properties_in[0].flow_mass_phase_comp["Liq", "H2O"].fix( + total_feed_flow_mass * feed_mass_frac_H2O + ) + m.fs.unit.control_volume.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"].fix( + total_feed_flow_mass * feed_mass_frac_NaCl + ) + m.fs.unit.control_volume.properties_in[0].flow_mass_phase_comp["Sol", "NaCl"].fix(0) + m.fs.unit.control_volume.properties_in[0].pressure.fix(feed_pressure) + m.fs.unit.control_volume.properties_in[0].temperature.fix(feed_temperature) + return m @@ -139,7 +151,9 @@ def build_mec3(): number_effects=3, ) + num_effects = 3 feed_flow_mass = 10 + total_feed_flow_mass = num_effects * feed_flow_mass feed_mass_frac_NaCl = 0.25 crystallizer_yield = 0.55 operating_pressures = [0.45, 0.25, 0.208] @@ -174,7 +188,6 @@ def build_mec3(): eff.effect.overall_heat_transfer_coefficient.set_value(0.1) first_effect = m.fs.unit.effects[1].effect - first_effect.overall_heat_transfer_coefficient.fix(0.1) first_effect.heating_steam[0].dh_vap_mass first_effect.heating_steam.calculate_state( @@ -187,6 +200,16 @@ def build_mec3(): ) first_effect.heating_steam[0].flow_mass_phase_comp["Vap", "H2O"].unfix() + m.fs.unit.control_volume.properties_in[0].flow_mass_phase_comp["Liq", "H2O"].fix( + total_feed_flow_mass * feed_mass_frac_H2O + ) + m.fs.unit.control_volume.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"].fix( + total_feed_flow_mass * feed_mass_frac_NaCl + ) + m.fs.unit.control_volume.properties_in[0].flow_mass_phase_comp["Sol", "NaCl"].fix(0) + m.fs.unit.control_volume.properties_in[0].pressure.fix(feed_pressure) + m.fs.unit.control_volume.properties_in[0].temperature.fix(feed_temperature) + return m @@ -220,7 +243,7 @@ def test_build(self, MEC4_frame): m = MEC4_frame # test ports and variables - port_lst = ["inlet", "outlet", "solids", "vapor", "steam", "pure_water"] + port_lst = ["inlet", "outlet", "solids", "vapor", "steam"] port_vars_lst = ["flow_mass_phase_comp", "pressure", "temperature"] state_blks = [ "properties_in", @@ -279,33 +302,36 @@ def test_build(self, MEC4_frame): ] effect_constr = [ + "eq_mass_balance_constraints", + "eq_solubility_massfrac_equality_constraint", + "eq_removal_balance", + "eq_vol_fraction_solids", + "eq_dens_magma", + "eq_operating_pressure_constraint", + "eq_relative_supersaturation", + "eq_enthalpy_balance", + "eq_p_con3", "eq_T_con1", "eq_T_con2", "eq_T_con3", - "eq_crystallizer_height_constraint", - "eq_dens_magma", - "eq_dens_mass_slurry", - "eq_enthalpy_balance", - "eq_mass_balance_constraints", "eq_minimum_hex_circulation_rate_constraint", - "eq_operating_pressure_constraint", + "eq_dens_mass_slurry", + "eq_residence_time", + "eq_suspension_volume", + "eq_vapor_head_diameter_constraint", + "eq_slurry_height_constraint", + "eq_crystallizer_height_constraint", + "eq_pure_water_mass_flow_rate", + "eq_vapor_energy_constraint", "eq_p_con1", "eq_p_con2", - "eq_p_con3", "eq_p_con4", "eq_p_con5", - "eq_pure_vapor_flow_rate", - "eq_relative_supersaturation", - "eq_removal_balance", - "eq_residence_time", - "eq_slurry_height_constraint", - "eq_solubility_massfrac_equality_constraint", - "eq_suspension_volume", - "eq_vapor_energy_constraint", - "eq_vapor_head_diameter_constraint", - "eq_vol_fraction_solids", ] + assert hasattr(m.fs.unit, "recovery_vol_phase") + assert isinstance(m.fs.unit.recovery_vol_phase, Var) + for n, eff in m.fs.unit.effects.items(): for p in effect_params: assert hasattr(eff.effect, p) @@ -318,39 +344,62 @@ def test_build(self, MEC4_frame): assert hasattr(eff.effect, f"eq_delta_temperature_inlet_effect_{n}") assert hasattr(eff.effect, f"eq_delta_temperature_outlet_effect_{n}") assert hasattr(eff.effect, f"eq_heat_transfer_effect_{n}") - if n == 1: - assert number_variables(eff.effect) == 146 - assert number_total_constraints(eff.effect) == 120 - assert number_unused_variables(eff.effect) == 3 + assert number_variables(eff.effect) == 154 + assert number_total_constraints(eff.effect) == 128 + assert number_unused_variables(eff.effect) == 2 assert hasattr(eff.effect, "heating_steam") assert isinstance(eff.effect.heating_steam, WaterStateBlock) assert eff.effect.heating_steam[0].temperature.ub == 1000 assert hasattr(eff.effect, "eq_heating_steam_flow_rate") - if n != 1: - assert number_variables(eff.effect) == 140 - assert number_total_constraints(eff.effect) == 118 - assert number_unused_variables(eff.effect) == 1 + assert number_variables(eff.effect) == 148 + assert number_total_constraints(eff.effect) == 126 + assert number_unused_variables(eff.effect) == 0 assert hasattr( eff.effect, f"eq_energy_for_effect_{n}_from_effect_{n - 1}" ) - assert number_variables(m) == 708 - assert number_total_constraints(m) == 474 - assert number_unused_variables(m) == 46 + assert number_variables(m) == 757 + assert number_total_constraints(m) == 519 + assert number_unused_variables(m) == 43 assert_units_consistent(m) @pytest.mark.unit def test_dof(self, MEC4_frame): + m = MEC4_frame + # With mass flow rates into each of the effects fixed, + # and the control_volume flows fixed, the model is over specified + assert degrees_of_freedom(m) == -2 + # Unfixing the mass flow rates for CV will result in 0 DOF + # (This is only done for testing purposes) + m.fs.unit.control_volume.properties_in[0].flow_mass_phase_comp[ + "Liq", "H2O" + ].unfix() + m.fs.unit.control_volume.properties_in[0].flow_mass_phase_comp[ + "Liq", "NaCl" + ].unfix() assert degrees_of_freedom(m) == 0 for n, eff in m.fs.unit.effects.items(): if n == 1: assert degrees_of_freedom(eff.effect) == 0 else: assert degrees_of_freedom(eff.effect) == 3 + m.fs.unit.control_volume.properties_in[0].flow_mass_phase_comp[ + "Liq", "H2O" + ].fix() + m.fs.unit.control_volume.properties_in[0].flow_mass_phase_comp[ + "Liq", "NaCl" + ].fix() + assert degrees_of_freedom(m) == -2 + # Alternatively, one can .set_value() each of the mass flows for the individual effects, + # resulting in 6 DOF: + for n, eff in m.fs.unit.effects.items(): + eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "H2O"].unfix() + eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"].unfix() + assert degrees_of_freedom(m) == 6 @pytest.mark.unit def test_calculate_scaling(self, MEC4_frame): @@ -385,37 +434,45 @@ def test_calculate_scaling(self, MEC4_frame): def test_initialize(self, MEC4_frame): m = MEC4_frame m.fs.unit.initialize() + c0 = value( + m.fs.unit.effects[1] + .effect.properties_in[0] + .conc_mass_phase_comp["Liq", "NaCl"] + ) + htc = value(m.fs.unit.effects[1].effect.overall_heat_transfer_coefficient) + assert degrees_of_freedom(m) == 0 for n, eff in m.fs.unit.effects.items(): - if n == 1: - htc = value(eff.effect.overall_heat_transfer_coefficient) - c0 = value( - eff.effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] + assert ( + not eff.effect.properties_in[0] + .flow_mass_phase_comp["Liq", "H2O"] + .is_fixed() + ) + assert ( + not eff.effect.properties_in[0] + .flow_mass_phase_comp["Liq", "NaCl"] + .is_fixed() + ) + assert ( + pytest.approx( + value( + eff.effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] + ), + rel=1e-6, ) - assert degrees_of_freedom(eff.effect) == 0 + == c0 + ) + if n == 1: + assert degrees_of_freedom(eff.effect) == 2 if n != 1: - assert ( - not eff.effect.properties_in[0] - .flow_mass_phase_comp["Liq", "H2O"] - .is_fixed() - ) - assert ( - not eff.effect.properties_in[0] - .flow_mass_phase_comp["Liq", "NaCl"] - .is_fixed() - ) assert ( eff.effect.properties_in[0] .conc_mass_phase_comp["Liq", "NaCl"] .is_fixed() ) - assert ( - value( - eff.effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] - ) - == c0 + linking_constr = getattr( + eff.effect, f"eq_energy_for_effect_{n}_from_effect_{n - 1}" ) - c = getattr(eff.effect, f"eq_energy_for_effect_{n}_from_effect_{n - 1}") - assert c.active + assert linking_constr.active assert eff.effect.overall_heat_transfer_coefficient.is_fixed() assert value(eff.effect.overall_heat_transfer_coefficient) == htc assert degrees_of_freedom(eff.effect) == 3 @@ -436,6 +493,10 @@ def test_conservation(self, MEC4_frame): comp_lst = ["NaCl", "H2O"] phase_lst = ["Sol", "Liq", "Vap"] + total_mass_flow_water_in = 0 + total_mass_flow_salt_in = 0 + total_mass_flow_water_out = 0 + for _, e in m.fs.unit.effects.items(): eff = e.effect @@ -499,113 +560,147 @@ def test_conservation(self, MEC4_frame): <= 1e-2 ) + total_mass_flow_water_in += value( + eff.properties_in[0].flow_mass_phase_comp["Liq", "H2O"] + ) + total_mass_flow_salt_in += value( + eff.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"] + ) + total_mass_flow_water_out += value( + eff.properties_pure_water[0].flow_mass_phase_comp["Liq", "H2O"] + ) + + # Test control volume mass balance assert ( - pytest.approx(value(m.fs.unit.total_flow_vol_in), rel=1e-3) == 0.00321056811 + pytest.approx( + m.fs.unit.control_volume.properties_in[0] + .flow_mass_phase_comp["Liq", "H2O"] + .value, + rel=1e-6, + ) + == total_mass_flow_water_in ) - assert ( pytest.approx( - sum( - value(eff.effect.properties_in[0].flow_vol_phase["Liq"]) - for _, eff in m.fs.unit.effects.items() - ), - rel=1e-3, + m.fs.unit.control_volume.properties_in[0] + .flow_mass_phase_comp["Liq", "NaCl"] + .value, + rel=1e-6, ) - == 0.00321056811 + == total_mass_flow_salt_in + ) + assert ( + pytest.approx( + m.fs.unit.control_volume.properties_out[0] + .flow_mass_phase_comp["Liq", "H2O"] + .value, + rel=1e-6, + ) + == total_mass_flow_water_out ) @pytest.mark.component def test_solution(self, MEC4_frame): m = MEC4_frame + assert ( + pytest.approx(value(m.fs.unit.recovery_vol_phase["Liq"]), rel=1e-3) + == 0.734452 + ) + unit_results_dict = { 1: { - "delta_temperature": {0.0: 86.31}, - "delta_temperature_in": {0.0: 57.39}, - "delta_temperature_out": {0.0: 123.72}, - "dens_mass_magma": 281.24, - "dens_mass_slurry": 1298.23, - "diameter_crystallizer": 1.0799, - "energy_flow_superheated_vapor": 1518.73, - "eq_max_allowable_velocity": 2.6304, - "eq_minimum_height_diameter_ratio": 1.6199, - "eq_vapor_space_height": 0.809971, - "heat_exchanger_area": 197.47, - "height_crystallizer": 1.883, + "crystallization_yield": {"NaCl": 0.5}, + "product_volumetric_solids_fraction": 0.132962, + "temperature_operating": 359.48, + "pressure_operating": 45000.0, + "dens_mass_magma": 281.21, + "dens_mass_slurry": 1298.22, + "work_mechanical": {0.0: 1915.44}, + "diameter_crystallizer": 1.1448, "height_slurry": 1.073, - "magma_circulation_flow_vol": 0.119395, - "product_volumetric_solids_fraction": 0.132975, - "relative_supersaturation": {"NaCl": 0.567038}, + "height_crystallizer": 1.9316, + "magma_circulation_flow_vol": 0.134172, + "relative_supersaturation": {"NaCl": 0.567033}, "t_res": 1.0228, - "temperature_operating": 359.48, - "volume_suspension": 0.982965, - "work_mechanical": {0.0: 1704.47}, + "volume_suspension": 1.1045, + "eq_max_allowable_velocity": 2.6304, + "eq_vapor_space_height": 0.858635, + "eq_minimum_height_diameter_ratio": 1.7172, + "energy_flow_superheated_vapor": 1706.7, + "delta_temperature_in": {0.0: 57.39}, + "delta_temperature_out": {0.0: 123.72}, + "delta_temperature": {0.0: 86.31}, + "heat_exchanger_area": 221.91, }, 2: { - "delta_temperature": {0.0: 31.59}, + "product_volumetric_solids_fraction": 0.132109, + "temperature_operating": 344.86, + "pressure_operating": 25000.0, + "dens_mass_magma": 279.41, + "dens_mass_slurry": 1301.84, + "work_mechanical": {0.0: 1706.7}, + "diameter_crystallizer": 1.2481, + "height_slurry": 0.828632, + "height_crystallizer": 1.8721, + "magma_circulation_flow_vol": 0.119101, + "relative_supersaturation": {"NaCl": 0.571246}, + "t_res": 1.0228, + "volume_suspension": 1.0138, + "eq_max_allowable_velocity": 3.4641, + "eq_vapor_space_height": 0.936079, + "eq_minimum_height_diameter_ratio": 1.8721, + "energy_flow_superheated_vapor": 1569.57, "delta_temperature_in": {0.0: 14.61}, "delta_temperature_out": {0.0: 58.77}, - "dens_mass_magma": 279.46, - "dens_mass_slurry": 1301.86, - "diameter_crystallizer": 1.1773, - "energy_flow_superheated_vapor": 1396.71, - "eq_max_allowable_velocity": 3.4641, - "eq_minimum_height_diameter_ratio": 1.766, - "eq_vapor_space_height": 0.883027, - "heat_exchanger_area": 480.72, - "height_crystallizer": 1.766, - "height_slurry": 0.82868, - "magma_circulation_flow_vol": 0.105985, - "product_volumetric_solids_fraction": 0.132132, - "relative_supersaturation": {"NaCl": 0.571251}, - "t_res": 1.0228, - "temperature_operating": 344.86, - "volume_suspension": 0.9022, - "work_mechanical": {0.0: 1518.73}, + "delta_temperature": {0.0: 31.59}, + "heat_exchanger_area": 540.22, }, 3: { - "delta_temperature": {0.0: 16.85}, + "product_volumetric_solids_fraction": 0.131922, + "temperature_operating": 340.52, + "pressure_operating": 20800.0, + "dens_mass_magma": 279.01, + "dens_mass_slurry": 1303.05, + "work_mechanical": {0.0: 1569.57}, + "diameter_crystallizer": 1.2521, + "height_slurry": 0.763702, + "height_crystallizer": 1.8782, + "magma_circulation_flow_vol": 0.109397, + "relative_supersaturation": {"NaCl": 0.572391}, + "t_res": 1.0228, + "volume_suspension": 0.940428, + "eq_max_allowable_velocity": 3.7763, + "eq_vapor_space_height": 0.939111, + "eq_minimum_height_diameter_ratio": 1.8782, + "energy_flow_superheated_vapor": 1457.19, "delta_temperature_in": {0.0: 4.3421}, "delta_temperature_out": {0.0: 44.83}, - "dens_mass_magma": 279.07, - "dens_mass_slurry": 1303.08, - "diameter_crystallizer": 1.1811, - "energy_flow_superheated_vapor": 1296.7, - "eq_max_allowable_velocity": 3.7763, - "eq_minimum_height_diameter_ratio": 1.7717, - "eq_vapor_space_height": 0.885888, - "heat_exchanger_area": 828.74, - "height_crystallizer": 1.7717, - "height_slurry": 0.763759, - "magma_circulation_flow_vol": 0.09735, - "product_volumetric_solids_fraction": 0.131951, - "relative_supersaturation": {"NaCl": 0.572396}, - "t_res": 1.0228, - "temperature_operating": 340.52, - "volume_suspension": 0.836915, - "work_mechanical": {0.0: 1396.71}, + "delta_temperature": {0.0: 16.85}, + "heat_exchanger_area": 931.31, }, 4: { - "delta_temperature": {0.0: 27.32}, + "product_volumetric_solids_fraction": 0.131442, + "temperature_operating": 323.22, + "pressure_operating": 9500.0, + "dens_mass_magma": 278.0, + "dens_mass_slurry": 1308.49, + "work_mechanical": {0.0: 1457.19}, + "diameter_crystallizer": 1.4623, + "height_slurry": 0.537417, + "height_crystallizer": 2.1935, + "magma_circulation_flow_vol": 0.101015, + "relative_supersaturation": {"NaCl": 0.576466}, + "t_res": 1.0228, + "volume_suspension": 0.902678, + "eq_max_allowable_velocity": 5.4596, + "eq_vapor_space_height": 1.0967, + "eq_minimum_height_diameter_ratio": 2.1935, + "energy_flow_superheated_vapor": 1405.09, "delta_temperature_in": {0.0: 17.29}, "delta_temperature_out": {0.0: 40.69}, - "dens_mass_magma": 278.12, - "dens_mass_slurry": 1308.54, - "diameter_crystallizer": 1.3795, - "energy_flow_superheated_vapor": 1250.34, - "eq_max_allowable_velocity": 5.4596, - "eq_minimum_height_diameter_ratio": 2.0692, - "eq_vapor_space_height": 1.0346, - "heat_exchanger_area": 474.46, - "height_crystallizer": 2.0692, - "height_slurry": 0.537503, - "magma_circulation_flow_vol": 0.089893, - "product_volumetric_solids_fraction": 0.131503, - "relative_supersaturation": {"NaCl": 0.576471}, - "t_res": 1.0228, - "temperature_operating": 323.22, - "volume_suspension": 0.803391, - "work_mechanical": {0.0: 1296.7}, + "delta_temperature": {0.0: 27.32}, + "heat_exchanger_area": 533.18, }, } @@ -620,8 +715,8 @@ def test_solution(self, MEC4_frame): assert pytest.approx(value(effv), rel=1e-3) == r steam_results_dict = { - "flow_mass_phase_comp": {("Liq", "H2O"): 0.0, ("Vap", "H2O"): 0.799053}, - "temperature": 416.87, + "flow_mass_phase_comp": {("Liq", "H2O"): 0.0, ("Vap", "H2O"): 0.89795}, + "temperature": 416.8, "pressure": 401325.0, "dh_vap_mass": 2133119.0, "pressure_sat": 401325.0, @@ -653,24 +748,24 @@ def test_costing(self, MEC4_frame): assert_optimal_termination(results) sys_costing_dict = { - "aggregate_capital_cost": 4527295.03, - "aggregate_flow_electricity": 7.5296, - "aggregate_flow_NaCl_recovered": 0.266962, - "aggregate_flow_steam": 0.383078, + "aggregate_capital_cost": 4934127.42, + "aggregate_flow_electricity": 8.4612, + "aggregate_flow_NaCl_recovered": 0.299999, + "aggregate_flow_steam": 0.4304, "aggregate_flow_costs": { - "electricity": 5423.99, - "NaCl_recovered": -240108.02, - "steam": 56766.9, + "electricity": 6095.09, + "NaCl_recovered": -269822.09, + "steam": 63793.11, }, - "aggregate_direct_capital_cost": 2263647.51, - "total_capital_cost": 4527295.03, - "total_operating_cost": -42098.28, - "maintenance_labor_chemical_operating_cost": 135818.85, - "total_fixed_operating_cost": 135818.85, - "total_variable_operating_cost": -177917.13, - "total_annualized_cost": 464759.33, - "LCOW": 4.5871, - "SEC": 0.651465, + "aggregate_direct_capital_cost": 2467063.71, + "total_capital_cost": 4934127.42, + "total_operating_cost": -51910.07, + "maintenance_labor_chemical_operating_cost": 148023.82, + "total_fixed_operating_cost": 148023.82, + "total_variable_operating_cost": -199933.89, + "total_annualized_cost": 500494.84, + "LCOW": 4.3957, + "SEC": 0.65144, } for v, r in sys_costing_dict.items(): @@ -682,20 +777,21 @@ def test_costing(self, MEC4_frame): assert pytest.approx(value(cv), rel=1e-3) == r eff_costing_dict = { - "capital_cost": 4527295.03, - "direct_capital_cost": 2263647.51, - "capital_cost_crystallizer_effect_1": 659171.48, - "capital_cost_heat_exchanger_effect_1": 209071.19, - "capital_cost_effect_1": 434121.33, - "capital_cost_crystallizer_effect_2": 627459.32, - "capital_cost_heat_exchanger_effect_2": 498502.0, - "capital_cost_effect_2": 562980.66, - "capital_cost_crystallizer_effect_3": 602356.42, - "capital_cost_heat_exchanger_effect_3": 851138.46, - "capital_cost_effect_3": 726747.44, - "capital_cost_crystallizer_effect_4": 587458.93, - "capital_cost_heat_exchanger_effect_4": 492137.2, - "capital_cost_effect_4": 539798.07, + "capital_cost": 4934127.42, + "cost_factor": 2.0, + "direct_capital_cost": 2467063.71, + "capital_cost_crystallizer_effect_1": 701221.05, + "capital_cost_heat_exchanger_effect_1": 234213.59, + "capital_cost_effect_1": 467717.32, + "capital_cost_crystallizer_effect_2": 667484.84, + "capital_cost_heat_exchanger_effect_2": 558948.69, + "capital_cost_effect_2": 613216.76, + "capital_cost_crystallizer_effect_3": 640779.56, + "capital_cost_heat_exchanger_effect_3": 954745.08, + "capital_cost_effect_3": 797762.32, + "capital_cost_crystallizer_effect_4": 624930.83, + "capital_cost_heat_exchanger_effect_4": 551803.75, + "capital_cost_effect_4": 588367.29, } for v, r in eff_costing_dict.items(): @@ -715,8 +811,12 @@ def test_costing_by_volume(self): eff.effect.crystal_median_length.fix(0.6e-3) eff.effect.crystal_growth_rate.fix(5e-9) + self.test_calculate_scaling(m) + m.fs.unit.initialize() + assert degrees_of_freedom(m) == 0 + results = solver.solve(m) assert_optimal_termination(results) @@ -736,27 +836,27 @@ def test_costing_by_volume(self): assert_optimal_termination(results) + # return m + sys_costing_dict = { - "aggregate_capital_cost": 4672521.02, - "aggregate_fixed_operating_cost": 0.0, - "aggregate_variable_operating_cost": 0.0, - "aggregate_flow_electricity": 7.5296, - "aggregate_flow_NaCl_recovered": 0.266962, - "aggregate_flow_steam": 0.383078, + "aggregate_capital_cost": 5078402.93, + "aggregate_flow_electricity": 8.4612, + "aggregate_flow_NaCl_recovered": 0.299999, + "aggregate_flow_steam": 0.430492, "aggregate_flow_costs": { - "electricity": 5423.99, - "NaCl_recovered": -240108.02, - "steam": 56766.9, + "electricity": 6095.09, + "NaCl_recovered": -269822.09, + "steam": 63793.11, }, - "aggregate_direct_capital_cost": 2336260.51, - "total_capital_cost": 4672521.02, - "total_operating_cost": -37741.5, - "maintenance_labor_chemical_operating_cost": 140175.63, - "total_fixed_operating_cost": 140175.63, - "total_variable_operating_cost": -177917.13, - "total_annualized_cost": 485375.02, - "LCOW": 4.7906, - "SEC": 0.651465, + "aggregate_direct_capital_cost": 2539201.46, + "total_capital_cost": 5078402.93, + "total_operating_cost": -47581.81, + "maintenance_labor_chemical_operating_cost": 152352.08, + "total_fixed_operating_cost": 152352.08, + "total_variable_operating_cost": -199933.89, + "total_annualized_cost": 520975.61, + "LCOW": 4.5756, + "SEC": 0.65144, } for v, r in sys_costing_dict.items(): @@ -768,20 +868,21 @@ def test_costing_by_volume(self): assert pytest.approx(value(cv), rel=1e-3) == r eff_costing_dict = { - "capital_cost": 4672521.02, - "direct_capital_cost": 2336260.51, - "capital_cost_crystallizer_effect_1": 675665.09, - "capital_cost_heat_exchanger_effect_1": 209071.19, - "capital_cost_effect_1": 442368.14, - "capital_cost_crystallizer_effect_2": 658735.08, - "capital_cost_heat_exchanger_effect_2": 498502.0, - "capital_cost_effect_2": 578618.54, - "capital_cost_crystallizer_effect_3": 638712.62, - "capital_cost_heat_exchanger_effect_3": 851138.46, - "capital_cost_effect_3": 744925.54, - "capital_cost_crystallizer_effect_4": 648559.35, - "capital_cost_heat_exchanger_effect_4": 492137.2, - "capital_cost_effect_4": 570348.28, + "capital_cost": 5078402.93, + "cost_factor": 2.0, + "direct_capital_cost": 2539201.46, + "capital_cost_crystallizer_effect_1": 715324.02, + "capital_cost_heat_exchanger_effect_1": 234213.59, + "capital_cost_effect_1": 474768.8, + "capital_cost_crystallizer_effect_2": 697956.34, + "capital_cost_heat_exchanger_effect_2": 558948.69, + "capital_cost_effect_2": 628452.52, + "capital_cost_crystallizer_effect_3": 676895.98, + "capital_cost_heat_exchanger_effect_3": 954745.08, + "capital_cost_effect_3": 815820.53, + "capital_cost_crystallizer_effect_4": 688515.45, + "capital_cost_heat_exchanger_effect_4": 551803.75, + "capital_cost_effect_4": 620159.6, } for v, r in eff_costing_dict.items(): @@ -820,57 +921,240 @@ def test_config(self, MEC3_frame): assert isinstance(eff.effect, CrystallizerEffect) assert not eff.effect.config.standalone + @pytest.mark.unit + def test_build(self, MEC3_frame): + m = MEC3_frame + + # test ports and variables + port_lst = ["inlet", "outlet", "solids", "vapor", "steam"] + port_vars_lst = ["flow_mass_phase_comp", "pressure", "temperature"] + state_blks = [ + "properties_in", + "properties_out", + "properties_pure_water", + "properties_solids", + "properties_vapor", + ] + + for port_str in port_lst: + assert hasattr(m.fs.unit, port_str) + port = getattr(m.fs.unit, port_str) + assert len(port.vars) == 3 + assert isinstance(port, Port) + for var_str in port_vars_lst: + assert hasattr(port, var_str) + var = getattr(port, var_str) + assert isinstance(var, Var) + + for n, eff in m.fs.unit.effects.items(): + for b in state_blks: + assert hasattr(eff.effect, b) + sb = getattr(eff.effect, b) + assert isinstance(sb, NaClStateBlock) + assert sb[0].temperature.ub == 1000 + + effect_params = [ + "approach_temperature_heat_exchanger", + "dimensionless_crystal_length", + ] + effect_vars = [ + "crystal_growth_rate", + "crystal_median_length", + "crystallization_yield", + "dens_mass_magma", + "dens_mass_slurry", + "diameter_crystallizer", + "height_crystallizer", + "height_slurry", + "magma_circulation_flow_vol", + "pressure_operating", + "product_volumetric_solids_fraction", + "relative_supersaturation", + "souders_brown_constant", + "t_res", + "temperature_operating", + "volume_suspension", + "work_mechanical", + ] + + effect_exprs = [ + "delta_temperature", + "eq_max_allowable_velocity", + "eq_minimum_height_diameter_ratio", + "eq_vapor_space_height", + ] + + effect_constr = [ + "eq_mass_balance_constraints", + "eq_solubility_massfrac_equality_constraint", + "eq_removal_balance", + "eq_vol_fraction_solids", + "eq_dens_magma", + "eq_operating_pressure_constraint", + "eq_relative_supersaturation", + "eq_enthalpy_balance", + "eq_p_con3", + "eq_T_con1", + "eq_T_con2", + "eq_T_con3", + "eq_minimum_hex_circulation_rate_constraint", + "eq_dens_mass_slurry", + "eq_residence_time", + "eq_suspension_volume", + "eq_vapor_head_diameter_constraint", + "eq_slurry_height_constraint", + "eq_crystallizer_height_constraint", + "eq_pure_water_mass_flow_rate", + "eq_vapor_energy_constraint", + "eq_p_con1", + "eq_p_con2", + "eq_p_con4", + "eq_p_con5", + ] + + assert hasattr(m.fs.unit, "recovery_vol_phase") + assert isinstance(m.fs.unit.recovery_vol_phase, Var) + + for n, eff in m.fs.unit.effects.items(): + for p in effect_params: + assert hasattr(eff.effect, p) + for v in effect_vars: + assert hasattr(eff.effect, v) + for e in effect_exprs: + assert hasattr(eff.effect, e) + for c in effect_constr: + assert hasattr(eff.effect, c) + assert hasattr(eff.effect, f"eq_delta_temperature_inlet_effect_{n}") + assert hasattr(eff.effect, f"eq_delta_temperature_outlet_effect_{n}") + assert hasattr(eff.effect, f"eq_heat_transfer_effect_{n}") + if n == 1: + assert number_variables(eff.effect) == 154 + assert number_total_constraints(eff.effect) == 128 + assert number_unused_variables(eff.effect) == 2 + assert hasattr(eff.effect, "heating_steam") + assert isinstance(eff.effect.heating_steam, WaterStateBlock) + assert eff.effect.heating_steam[0].temperature.ub == 1000 + assert hasattr(eff.effect, "eq_heating_steam_flow_rate") + if n != 1: + assert number_variables(eff.effect) == 148 + assert number_total_constraints(eff.effect) == 126 + assert number_unused_variables(eff.effect) == 0 + assert hasattr( + eff.effect, f"eq_energy_for_effect_{n}_from_effect_{n - 1}" + ) + + assert number_variables(m) == 609 + assert number_total_constraints(m) == 393 + assert number_unused_variables(m) == 43 + + assert_units_consistent(m) + @pytest.mark.unit def test_dof(self, MEC3_frame): m = MEC3_frame + # With mass flow rates into each of the effects fixed, + # the model is over specified + assert degrees_of_freedom(m) == -2 + # Unfixing the mass flow rates for CV will result in 0 DOF + # (This is only done for testing purposes) + m.fs.unit.control_volume.properties_in[0].flow_mass_phase_comp[ + "Liq", "H2O" + ].unfix() + m.fs.unit.control_volume.properties_in[0].flow_mass_phase_comp[ + "Liq", "NaCl" + ].unfix() assert degrees_of_freedom(m) == 0 for n, eff in m.fs.unit.effects.items(): if n == 1: assert degrees_of_freedom(eff.effect) == 0 else: assert degrees_of_freedom(eff.effect) == 3 + m.fs.unit.control_volume.properties_in[0].flow_mass_phase_comp[ + "Liq", "H2O" + ].fix() + m.fs.unit.control_volume.properties_in[0].flow_mass_phase_comp[ + "Liq", "NaCl" + ].fix() + assert degrees_of_freedom(m) == -2 + # Alternatively, one can .set_value() each of the mass flows for the individual effects, + # resulting in 4 DOF: + for n, eff in m.fs.unit.effects.items(): + eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "H2O"].unfix() + eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"].unfix() + assert degrees_of_freedom(m) == 4 + + @pytest.mark.component + def test_calculate_scaling(self, MEC3_frame): + m = MEC3_frame + + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1, index=("Liq", "H2O") + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1, index=("Liq", "NaCl") + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1, index=("Vap", "H2O") + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1, index=("Sol", "NaCl") + ) + m.fs.vapor_properties.set_default_scaling( + "flow_mass_phase_comp", 1, index=("Vap", "H2O") + ) + m.fs.vapor_properties.set_default_scaling( + "flow_mass_phase_comp", 1, index=("Liq", "H2O") + ) + + calculate_scaling_factors(m) + unscaled_var_list = list(unscaled_variables_generator(m)) + assert len(unscaled_var_list) == 0 + badly_scaled_var_list = list(badly_scaled_var_generator(m)) + assert len(badly_scaled_var_list) == 0 @pytest.mark.component def test_initialize(self, MEC3_frame): m = MEC3_frame m.fs.unit.initialize() + c0 = value( + m.fs.unit.effects[1] + .effect.properties_in[0] + .conc_mass_phase_comp["Liq", "NaCl"] + ) + htc = value(m.fs.unit.effects[1].effect.overall_heat_transfer_coefficient) + assert degrees_of_freedom(m) == 0 for n, eff in m.fs.unit.effects.items(): - if n == 1: - htc = value(eff.effect.overall_heat_transfer_coefficient) - c0 = value( - eff.effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] + assert ( + not eff.effect.properties_in[0] + .flow_mass_phase_comp["Liq", "H2O"] + .is_fixed() + ) + assert ( + not eff.effect.properties_in[0] + .flow_mass_phase_comp["Liq", "NaCl"] + .is_fixed() + ) + assert ( + pytest.approx( + value( + eff.effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] + ), + rel=1e-6, ) - assert degrees_of_freedom(eff.effect) == 0 + == c0 + ) + if n == 1: + assert degrees_of_freedom(eff.effect) == 2 if n != 1: - assert ( - not eff.effect.properties_in[0] - .flow_mass_phase_comp["Liq", "H2O"] - .is_fixed() - ) - assert ( - not eff.effect.properties_in[0] - .flow_mass_phase_comp["Liq", "NaCl"] - .is_fixed() - ) assert ( eff.effect.properties_in[0] .conc_mass_phase_comp["Liq", "NaCl"] .is_fixed() ) - assert ( - pytest.approx( - value( - eff.effect.properties_in[0].conc_mass_phase_comp[ - "Liq", "NaCl" - ] - ), - rel=1e-3, - ) - == c0 + linking_constr = getattr( + eff.effect, f"eq_energy_for_effect_{n}_from_effect_{n - 1}" ) - - c = getattr(eff.effect, f"eq_energy_for_effect_{n}_from_effect_{n - 1}") - assert c.active + assert linking_constr.active assert eff.effect.overall_heat_transfer_coefficient.is_fixed() assert value(eff.effect.overall_heat_transfer_coefficient) == htc assert degrees_of_freedom(eff.effect) == 3 @@ -885,12 +1169,16 @@ def test_solve(self, MEC3_frame): @pytest.mark.component def test_conservation(self, MEC3_frame): - m = MEC3_frame comp_lst = ["NaCl", "H2O"] phase_lst = ["Sol", "Liq", "Vap"] + total_mass_flow_water_in = 0 + total_mass_flow_salt_in = 0 + total_mass_flow_water_out = 0 + + # Test mass balance for each effect for _, e in m.fs.unit.effects.items(): eff = e.effect @@ -937,97 +1225,117 @@ def test_conservation(self, MEC3_frame): <= 1e-6 ) - assert ( - abs( - value( - flow_mass_in * eff.properties_in[0].enth_mass_phase["Liq"] - - flow_mass_out * eff.properties_out[0].enth_mass_phase["Liq"] - - flow_mass_vapor - * eff.properties_vapor[0].enth_mass_solvent["Vap"] - - flow_mass_solids - * eff.properties_solids[0].enth_mass_solute["Sol"] - - flow_mass_solids - * eff.properties_solids[0].dh_crystallization_mass_comp["NaCl"] - + eff.work_mechanical[0] - ) - ) - <= 1e-2 + total_mass_flow_water_in += value( + eff.properties_in[0].flow_mass_phase_comp["Liq", "H2O"] + ) + total_mass_flow_salt_in += value( + eff.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"] + ) + total_mass_flow_water_out += value( + eff.properties_pure_water[0].flow_mass_phase_comp["Liq", "H2O"] ) - assert pytest.approx(value(m.fs.unit.total_flow_vol_in), rel=1e-3) == 0.0232595 - + # Test control volume mass balance + assert ( + pytest.approx( + m.fs.unit.control_volume.properties_in[0] + .flow_mass_phase_comp["Liq", "H2O"] + .value, + rel=1e-6, + ) + == total_mass_flow_water_in + ) assert ( pytest.approx( - sum( - value(eff.effect.properties_in[0].flow_vol_phase["Liq"]) - for _, eff in m.fs.unit.effects.items() - ), - rel=1e-3, + m.fs.unit.control_volume.properties_in[0] + .flow_mass_phase_comp["Liq", "NaCl"] + .value, + rel=1e-6, ) - == 0.0232595 + == total_mass_flow_salt_in + ) + assert ( + pytest.approx( + m.fs.unit.control_volume.properties_out[0] + .flow_mass_phase_comp["Liq", "H2O"] + .value, + rel=1e-6, + ) + == total_mass_flow_water_out ) @pytest.mark.component def test_solution(self, MEC3_frame): m = MEC3_frame + assert ( + pytest.approx(value(m.fs.unit.recovery_vol_phase["Liq"]), rel=1e-3) + == 0.5485 + ) + unit_results_dict = { 1: { - "dens_mass_magma": 333.63, - "dens_mass_slurry": 1321.57, - "diameter_crystallizer": 2.8505, - "energy_flow_superheated_vapor": 10580.97, - "eq_max_allowable_velocity": 2.6304, - "eq_minimum_height_diameter_ratio": 4.2758, - "eq_vapor_space_height": 2.1379, - "heat_exchanger_area": 1322.35, - "height_crystallizer": 4.5158, - "height_slurry": 2.3779, - "magma_circulation_flow_vol": 0.852729, - "product_volumetric_solids_fraction": 0.157748, - "relative_supersaturation": {"NaCl": 0.661231}, - "t_res": 1.0228, - "temperature_operating": 359.48, - "volume_suspension": 15.17, - "work_mechanical": {0.0: 12008.03}, + "product_volumetric_solids_fraction": 0.15774, + "temperature_operating": 359.4, + "pressure_operating": 45000.0, + "dens_mass_magma": 333.6, + "dens_mass_slurry": 1321.5, + "work_mechanical": {0.0: 13035.6}, + "diameter_crystallizer": 2.970, + "height_slurry": 2.377, + "height_crystallizer": 4.605, + "magma_circulation_flow_vol": 0.92570, + "relative_supersaturation": {"NaCl": 0.66123}, + "t_res": 1.02282118, + "volume_suspension": 16.4746, + "eq_vapor_space_height": 2.227, + "eq_minimum_height_diameter_ratio": 4.455, + "energy_flow_superheated_vapor": 11486.5, + "delta_temperature_in": {0.0: 61.67}, + "delta_temperature_out": {0.0: 128.0}, + "delta_temperature": {0.0: 90.8}, + "heat_exchanger_area": 1435.5, }, 2: { - "dens_mass_magma": 331.37, - "dens_mass_slurry": 1324.86, - "diameter_crystallizer": 3.1042, - "energy_flow_superheated_vapor": 9709.33, - "eq_max_allowable_velocity": 3.4641, - "eq_minimum_height_diameter_ratio": 4.6563, - "eq_vapor_space_height": 2.3281, - "heat_exchanger_area": 3349.19, - "height_crystallizer": 4.6563, - "height_slurry": 1.8467, - "magma_circulation_flow_vol": 0.748561, - "product_volumetric_solids_fraction": 0.156677, - "relative_supersaturation": {"NaCl": 0.666441}, + "product_volumetric_solids_fraction": 0.156676, + "temperature_operating": 344.8, + "pressure_operating": 25000.0, + "dens_mass_magma": 331.3, + "dens_mass_slurry": 1324.8, + "work_mechanical": {0.0: 11486.5}, + "diameter_crystallizer": 3.234, + "height_slurry": 1.846, + "height_crystallizer": 4.85, + "magma_circulation_flow_vol": 0.81262, + "relative_supersaturation": {"NaCl": 0.66644}, "t_res": 1.0228, - "temperature_operating": 344.86, - "volume_suspension": 13.97, - "work_mechanical": {0.0: 10580.97}, + "volume_suspension": 15.1726, + "eq_minimum_height_diameter_ratio": 4.851, + "energy_flow_superheated_vapor": 10540.2, + "delta_temperature_in": {0.0: 14.61}, + "delta_temperature_out": {0.0: 58.77}, + "delta_temperature": {0.0: 31.59}, + "heat_exchanger_area": 3635.8, }, 3: { + "product_volumetric_solids_fraction": 0.15640, + "temperature_operating": 340.5, + "pressure_operating": 20800.0, "dens_mass_magma": 330.8, "dens_mass_slurry": 1325.96, - "diameter_crystallizer": 3.1152, - "energy_flow_superheated_vapor": 9019.89, - "eq_max_allowable_velocity": 3.7763, - "eq_minimum_height_diameter_ratio": 4.6729, - "eq_vapor_space_height": 2.3364, - "heat_exchanger_area": 5761.07, - "height_crystallizer": 4.6729, - "height_slurry": 1.7045, - "magma_circulation_flow_vol": 0.686049, - "product_volumetric_solids_fraction": 0.15641, - "relative_supersaturation": {"NaCl": 0.667858}, - "t_res": 1.0228, - "temperature_operating": 340.52, - "volume_suspension": 12.99, - "work_mechanical": {0.0: 9709.33}, + "work_mechanical": {0.0: 10540.2}, + "diameter_crystallizer": 3.2458, + "height_slurry": 1.704, + "height_crystallizer": 4.868, + "magma_circulation_flow_vol": 0.744761, + "relative_supersaturation": {"NaCl": 0.66785}, + "t_res": 1.022, + "volume_suspension": 14.10, + "energy_flow_superheated_vapor": 9791.82, + "delta_temperature_in": {0.0: 4.342}, + "delta_temperature_out": {0.0: 44.8}, + "delta_temperature": {0.0: 16.85}, + "heat_exchanger_area": 6254.11, }, } @@ -1042,11 +1350,11 @@ def test_solution(self, MEC3_frame): assert pytest.approx(value(effv), rel=1e-3) == r steam_results_dict = { - "flow_mass_phase_comp": {("Liq", "H2O"): 0.0, ("Vap", "H2O"): 5.66421262}, - "temperature": 421.1592, + "flow_mass_phase_comp": {("Vap", "H2O"): 6.14896}, + "temperature": 421.1, "pressure": 451325.0, - "dh_vap_mass": 2119982.2092, - "pressure_sat": 451324.9999, + "dh_vap_mass": 2119982.2, + "pressure_sat": 451324.9, } for v, r in steam_results_dict.items(): @@ -1061,11 +1369,13 @@ def test_solution(self, MEC3_frame): def test_costing(self, MEC3_frame): m = MEC3_frame m.fs.costing = TreatmentCosting() + # m.fs.costing.base_currency = pyunits.USD_2018 m.fs.unit.costing = UnitModelCostingBlock( flowsheet_costing_block=m.fs.costing, + costing_method_arguments={"cost_type": "mass_basis"}, ) - m.fs.costing.nacl_recovered.cost.set_value(-0.004) + m.fs.costing.nacl_recovered.cost.set_value(-0.024) m.fs.costing.cost_process() m.fs.costing.add_LCOW(m.fs.unit.total_flow_vol_in) m.fs.costing.add_specific_energy_consumption( @@ -1075,25 +1385,24 @@ def test_costing(self, MEC3_frame): assert_optimal_termination(results) sys_costing_dict = { - "LCOW": 3.502332, - "aggregate_capital_cost": 19360250.738, - "aggregate_direct_capital_cost": 9680125.369, - "aggregate_flow_NaCl_recovered": 3.799812, + "aggregate_capital_cost": 20645631.14, + "aggregate_flow_electricity": 46.05, + "aggregate_flow_NaCl_recovered": 4.1249, + "aggregate_flow_steam": 2.6482, "aggregate_flow_costs": { - "NaCl_recovered": -569596.699, - "electricity": 30561.497, - "steam": 361498.197, + "electricity": 33176.93, + "NaCl_recovered": -3710055.74, + "steam": 392435.52, }, - "aggregate_flow_electricity": 42.425, - "aggregate_flow_steam": 2.439485, - "capital_recovery_factor": 0.111955949, - "maintenance_labor_chemical_operating_cost": 580807.522, - "total_annualized_cost": 2570765.766, - "total_capital_cost": 19360250.738, - "total_fixed_operating_cost": 580807.522, - "total_operating_cost": 403270.518, - "total_variable_operating_cost": -177537.004, - "wacc": 0.093073397, + "aggregate_direct_capital_cost": 10322815.57, + "total_capital_cost": 20645631.14, + "total_operating_cost": -2665074.35, + "maintenance_labor_chemical_operating_cost": 619368.93, + "total_fixed_operating_cost": 619368.93, + "total_variable_operating_cost": -3284443.28, + "total_annualized_cost": -353673.11, + "LCOW": -0.443848, + "SEC": 0.506672, } for v, r in sys_costing_dict.items(): @@ -1105,17 +1414,17 @@ def test_costing(self, MEC3_frame): assert pytest.approx(value(cv), rel=1e-3) == r eff_costing_dict = { - "capital_cost": 19360250.738, - "capital_cost_crystallizer_effect_1": 3079736.682, - "capital_cost_crystallizer_effect_2": 2937547.941, - "capital_cost_crystallizer_effect_3": 2823493.637, - "capital_cost_effect_1": 2214301.874, - "capital_cost_effect_2": 3159294.994, - "capital_cost_effect_3": 4306528.5, - "capital_cost_heat_exchanger_effect_1": 1348867.065, - "capital_cost_heat_exchanger_effect_2": 3381042.046, - "capital_cost_heat_exchanger_effect_3": 5789563.363, - "direct_capital_cost": 9680125.369, + "capital_cost": 20645631.14, + "direct_capital_cost": 10322815.57, + "capital_cost_crystallizer_effect_1": 3216728.22, + "capital_cost_heat_exchanger_effect_1": 1462729.88, + "capital_cost_effect_1": 2339729.05, + "capital_cost_crystallizer_effect_2": 3068214.17, + "capital_cost_heat_exchanger_effect_2": 3667644.07, + "capital_cost_effect_2": 3367929.12, + "capital_cost_crystallizer_effect_3": 2949086.04, + "capital_cost_heat_exchanger_effect_3": 6281228.74, + "capital_cost_effect_3": 4615157.39, } for v, r in eff_costing_dict.items(): From 34ebf581754a407ee254b787265fc75da9bde0dd Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Tue, 8 Oct 2024 18:08:52 -0600 Subject: [PATCH 71/80] add test for 2 effects --- .../tests/test_multi_effect_crystallizer.py | 1151 +++++++++++++---- 1 file changed, 875 insertions(+), 276 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/tests/test_multi_effect_crystallizer.py b/src/watertap_contrib/reflo/unit_models/tests/test_multi_effect_crystallizer.py index 0c382aff..7bd77fb0 100644 --- a/src/watertap_contrib/reflo/unit_models/tests/test_multi_effect_crystallizer.py +++ b/src/watertap_contrib/reflo/unit_models/tests/test_multi_effect_crystallizer.py @@ -60,7 +60,7 @@ eps = 1e-12 -def build_mec4(): +def build_mec2(): m = ConcreteModel() m.fs = FlowsheetBlock(dynamic=False) @@ -69,29 +69,30 @@ def build_mec4(): m.fs.vapor_properties = WaterParameterBlock() m.fs.unit = mec = MultiEffectCrystallizer( - property_package=m.fs.properties, property_package_vapor=m.fs.vapor_properties + property_package=m.fs.properties, + property_package_vapor=m.fs.vapor_properties, + number_effects=2, ) - operating_pressures = [0.45, 0.25, 0.208, 0.095] - - feed_flow_mass = 1 - feed_mass_frac_NaCl = 0.15 - crystallizer_yield = 0.5 + num_effects = 2 + feed_flow_mass = 2 + total_feed_flow_mass = num_effects * feed_flow_mass + feed_mass_frac_NaCl = 0.45 + crystallizer_yield = 0.6 + operating_pressures = [0.45, 0.25] feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl - total_feed_flow_mass = 4 - atm_pressure = 101325 * pyunits.Pa - saturated_steam_pressure_gage = 3 * pyunits.bar + saturated_steam_pressure_gage = 5 * pyunits.bar saturated_steam_pressure = atm_pressure + pyunits.convert( saturated_steam_pressure_gage, to_units=pyunits.Pa ) for (_, eff), op_pressure in zip(mec.effects.items(), operating_pressures): - eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "H2O"].fix( + eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "H2O"].set_value( feed_flow_mass * feed_mass_frac_H2O ) - eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"].fix( + eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"].set_value( feed_flow_mass * feed_mass_frac_NaCl ) eff.effect.properties_in[0].pressure.fix(feed_pressure) @@ -110,9 +111,7 @@ def build_mec4(): eff.effect.overall_heat_transfer_coefficient.set_value(0.1) first_effect = m.fs.unit.effects[1].effect - first_effect.overall_heat_transfer_coefficient.fix(0.1) - first_effect.heating_steam[0].pressure_sat first_effect.heating_steam[0].dh_vap_mass first_effect.heating_steam.calculate_state( var_args={ @@ -213,25 +212,102 @@ def build_mec3(): return m -class TestMultiEffectCrystallizer_4Effects: +def build_mec4(): + + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.properties = NaClParameterBlock() + m.fs.vapor_properties = WaterParameterBlock() + + m.fs.unit = mec = MultiEffectCrystallizer( + property_package=m.fs.properties, property_package_vapor=m.fs.vapor_properties + ) + + operating_pressures = [0.45, 0.25, 0.208, 0.095] + + feed_flow_mass = 1 + feed_mass_frac_NaCl = 0.15 + crystallizer_yield = 0.5 + feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl + + total_feed_flow_mass = 4 + + atm_pressure = 101325 * pyunits.Pa + saturated_steam_pressure_gage = 3 * pyunits.bar + saturated_steam_pressure = atm_pressure + pyunits.convert( + saturated_steam_pressure_gage, to_units=pyunits.Pa + ) + + for (_, eff), op_pressure in zip(mec.effects.items(), operating_pressures): + eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "H2O"].fix( + feed_flow_mass * feed_mass_frac_H2O + ) + eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"].fix( + feed_flow_mass * feed_mass_frac_NaCl + ) + eff.effect.properties_in[0].pressure.fix(feed_pressure) + eff.effect.properties_in[0].temperature.fix(feed_temperature) + + eff.effect.properties_in[0].flow_mass_phase_comp["Sol", "NaCl"].fix(eps) + eff.effect.properties_in[0].flow_mass_phase_comp["Vap", "H2O"].fix(eps) + eff.effect.properties_in[0].conc_mass_phase_comp[...] + eff.effect.crystallization_yield["NaCl"].fix(crystallizer_yield) + eff.effect.crystal_growth_rate.fix() + eff.effect.souders_brown_constant.fix() + eff.effect.crystal_median_length.fix() + eff.effect.pressure_operating.fix( + pyunits.convert(op_pressure * pyunits.bar, to_units=pyunits.Pa) + ) + eff.effect.overall_heat_transfer_coefficient.set_value(0.1) + + first_effect = m.fs.unit.effects[1].effect + + first_effect.overall_heat_transfer_coefficient.fix(0.1) + first_effect.heating_steam[0].pressure_sat + first_effect.heating_steam[0].dh_vap_mass + first_effect.heating_steam.calculate_state( + var_args={ + ("flow_mass_phase_comp", ("Liq", "H2O")): 0, + ("pressure", None): saturated_steam_pressure, + ("pressure_sat", None): saturated_steam_pressure, + }, + hold_state=True, + ) + first_effect.heating_steam[0].flow_mass_phase_comp["Vap", "H2O"].unfix() + + m.fs.unit.control_volume.properties_in[0].flow_mass_phase_comp["Liq", "H2O"].fix( + total_feed_flow_mass * feed_mass_frac_H2O + ) + m.fs.unit.control_volume.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"].fix( + total_feed_flow_mass * feed_mass_frac_NaCl + ) + m.fs.unit.control_volume.properties_in[0].flow_mass_phase_comp["Sol", "NaCl"].fix(0) + m.fs.unit.control_volume.properties_in[0].pressure.fix(feed_pressure) + m.fs.unit.control_volume.properties_in[0].temperature.fix(feed_temperature) + + return m + + +class TestMultiEffectCrystallizer_2Effects: @pytest.fixture(scope="class") - def MEC4_frame(self): + def MEC2_frame(self): """ - Test crystallizer with 4 effects + Test crystallizer with 2 effects """ - m = build_mec4() + m = build_mec2() return m @pytest.mark.unit - def test_config(self, MEC4_frame): - m = MEC4_frame + def test_config(self, MEC2_frame): + m = MEC2_frame assert len(m.fs.unit.config) == 6 assert not m.fs.unit.config.dynamic assert not m.fs.unit.config.has_holdup assert m.fs.unit.config.property_package is m.fs.properties assert m.fs.unit.config.property_package_vapor is m.fs.vapor_properties - assert m.fs.unit.config.number_effects == 4 + assert m.fs.unit.config.number_effects == 2 assert m.fs.unit.config.number_effects == len(m.fs.unit.effects) for _, eff in m.fs.unit.effects.items(): @@ -239,8 +315,8 @@ def test_config(self, MEC4_frame): assert not eff.effect.config.standalone @pytest.mark.unit - def test_build(self, MEC4_frame): - m = MEC4_frame + def test_build(self, MEC2_frame): + m = MEC2_frame # test ports and variables port_lst = ["inlet", "outlet", "solids", "vapor", "steam"] @@ -360,65 +436,52 @@ def test_build(self, MEC4_frame): eff.effect, f"eq_energy_for_effect_{n}_from_effect_{n - 1}" ) - assert number_variables(m) == 757 - assert number_total_constraints(m) == 519 + assert number_variables(m) == 461 + assert number_total_constraints(m) == 267 assert number_unused_variables(m) == 43 assert_units_consistent(m) @pytest.mark.unit - def test_dof(self, MEC4_frame): + def test_dof(self, MEC2_frame): + m = MEC2_frame + # With mass flow rates into each of the effects unfixed, + # the model is under specified + assert degrees_of_freedom(m) == 2 - m = MEC4_frame - # With mass flow rates into each of the effects fixed, - # and the control_volume flows fixed, the model is over specified - assert degrees_of_freedom(m) == -2 - # Unfixing the mass flow rates for CV will result in 0 DOF - # (This is only done for testing purposes) - m.fs.unit.control_volume.properties_in[0].flow_mass_phase_comp[ - "Liq", "H2O" - ].unfix() - m.fs.unit.control_volume.properties_in[0].flow_mass_phase_comp[ - "Liq", "NaCl" - ].unfix() - assert degrees_of_freedom(m) == 0 + # Fixing flow rates into individual effects will reduce DOF... for n, eff in m.fs.unit.effects.items(): + eff.effect.properties_in[0].flow_mass_phase_comp.fix() if n == 1: assert degrees_of_freedom(eff.effect) == 0 else: assert degrees_of_freedom(eff.effect) == 3 - m.fs.unit.control_volume.properties_in[0].flow_mass_phase_comp[ - "Liq", "H2O" - ].fix() - m.fs.unit.control_volume.properties_in[0].flow_mass_phase_comp[ - "Liq", "NaCl" - ].fix() + # ... and result in an overspecified model. assert degrees_of_freedom(m) == -2 - # Alternatively, one can .set_value() each of the mass flows for the individual effects, - # resulting in 6 DOF: + for n, eff in m.fs.unit.effects.items(): eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "H2O"].unfix() eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"].unfix() - assert degrees_of_freedom(m) == 6 + assert degrees_of_freedom(m) == 2 @pytest.mark.unit - def test_calculate_scaling(self, MEC4_frame): - m = MEC4_frame + def test_calculate_scaling(self, MEC2_frame): + m = MEC2_frame m.fs.properties.set_default_scaling( - "flow_mass_phase_comp", 1e-1, index=("Liq", "H2O") + "flow_mass_phase_comp", 1, index=("Liq", "H2O") ) m.fs.properties.set_default_scaling( - "flow_mass_phase_comp", 1e-1, index=("Liq", "NaCl") + "flow_mass_phase_comp", 1, index=("Liq", "NaCl") ) m.fs.properties.set_default_scaling( - "flow_mass_phase_comp", 1e-1, index=("Vap", "H2O") + "flow_mass_phase_comp", 10, index=("Vap", "H2O") ) m.fs.properties.set_default_scaling( - "flow_mass_phase_comp", 1e-1, index=("Sol", "NaCl") + "flow_mass_phase_comp", 1, index=("Sol", "NaCl") ) m.fs.vapor_properties.set_default_scaling( - "flow_mass_phase_comp", 1e-1, index=("Vap", "H2O") + "flow_mass_phase_comp", 1, index=("Vap", "H2O") ) m.fs.vapor_properties.set_default_scaling( "flow_mass_phase_comp", 1, index=("Liq", "H2O") @@ -431,8 +494,8 @@ def test_calculate_scaling(self, MEC4_frame): assert len(badly_scaled_var_list) == 0 @pytest.mark.component - def test_initialize(self, MEC4_frame): - m = MEC4_frame + def test_initialize(self, MEC2_frame): + m = MEC2_frame m.fs.unit.initialize() c0 = value( m.fs.unit.effects[1] @@ -478,17 +541,17 @@ def test_initialize(self, MEC4_frame): assert degrees_of_freedom(eff.effect) == 3 @pytest.mark.component - def test_solve(self, MEC4_frame): + def test_solve(self, MEC2_frame): - m = MEC4_frame + m = MEC2_frame results = solver.solve(m) assert_optimal_termination(results) @pytest.mark.component - def test_conservation(self, MEC4_frame): + def test_conservation(self, MEC2_frame): - m = MEC4_frame + m = MEC2_frame comp_lst = ["NaCl", "H2O"] phase_lst = ["Sol", "Liq", "Vap"] @@ -600,107 +663,58 @@ def test_conservation(self, MEC4_frame): ) @pytest.mark.component - def test_solution(self, MEC4_frame): - m = MEC4_frame + def test_solution(self, MEC2_frame): + m = MEC2_frame assert ( pytest.approx(value(m.fs.unit.recovery_vol_phase["Liq"]), rel=1e-3) - == 0.734452 + == 0.10289 ) unit_results_dict = { 1: { - "crystallization_yield": {"NaCl": 0.5}, - "product_volumetric_solids_fraction": 0.132962, - "temperature_operating": 359.48, + "temperature_operating": 359.4, "pressure_operating": 45000.0, - "dens_mass_magma": 281.21, - "dens_mass_slurry": 1298.22, - "work_mechanical": {0.0: 1915.44}, - "diameter_crystallizer": 1.1448, - "height_slurry": 1.073, - "height_crystallizer": 1.9316, - "magma_circulation_flow_vol": 0.134172, - "relative_supersaturation": {"NaCl": 0.567033}, + "dens_mass_magma": 395.317, + "dens_mass_slurry": 1349.047, + "work_mechanical": {0.0: 370.3}, + "diameter_crystallizer": 0.50292, + "height_slurry": 22.853, + "height_crystallizer": 23.23, + "magma_circulation_flow_vol": 0.026733, + "relative_supersaturation": {"NaCl": 0.76747}, "t_res": 1.0228, - "volume_suspension": 1.1045, - "eq_max_allowable_velocity": 2.6304, - "eq_vapor_space_height": 0.858635, - "eq_minimum_height_diameter_ratio": 1.7172, - "energy_flow_superheated_vapor": 1706.7, - "delta_temperature_in": {0.0: 57.39}, - "delta_temperature_out": {0.0: 123.72}, - "delta_temperature": {0.0: 86.31}, - "heat_exchanger_area": 221.91, + "volume_suspension": 4.5398, + "eq_max_allowable_velocity": 2.630, + "eq_vapor_space_height": 0.37719, + "eq_minimum_height_diameter_ratio": 0.75438, + "energy_flow_superheated_vapor": 329.356, + "delta_temperature_in": {0.0: 72.57}, + "delta_temperature_out": {0.0: 138.9}, + "delta_temperature": {0.0: 102.1}, + "heat_exchanger_area": 36.253, }, 2: { - "product_volumetric_solids_fraction": 0.132109, - "temperature_operating": 344.86, + "temperature_operating": 344.8, "pressure_operating": 25000.0, - "dens_mass_magma": 279.41, - "dens_mass_slurry": 1301.84, - "work_mechanical": {0.0: 1706.7}, - "diameter_crystallizer": 1.2481, - "height_slurry": 0.828632, - "height_crystallizer": 1.8721, - "magma_circulation_flow_vol": 0.119101, - "relative_supersaturation": {"NaCl": 0.571246}, + "dens_mass_magma": 392.7, + "dens_mass_slurry": 1352.0, + "work_mechanical": {0.0: 329.3}, + "diameter_crystallizer": 0.6011, + "height_slurry": 19.582, + "height_crystallizer": 20.03, + "magma_circulation_flow_vol": 0.02368, + "relative_supersaturation": {"NaCl": 0.7739}, "t_res": 1.0228, - "volume_suspension": 1.0138, - "eq_max_allowable_velocity": 3.4641, - "eq_vapor_space_height": 0.936079, - "eq_minimum_height_diameter_ratio": 1.8721, - "energy_flow_superheated_vapor": 1569.57, + "volume_suspension": 5.558, + "eq_max_allowable_velocity": 3.464, + "eq_vapor_space_height": 0.45086, + "eq_minimum_height_diameter_ratio": 0.9017, + "energy_flow_superheated_vapor": 364.13, "delta_temperature_in": {0.0: 14.61}, "delta_temperature_out": {0.0: 58.77}, "delta_temperature": {0.0: 31.59}, - "heat_exchanger_area": 540.22, - }, - 3: { - "product_volumetric_solids_fraction": 0.131922, - "temperature_operating": 340.52, - "pressure_operating": 20800.0, - "dens_mass_magma": 279.01, - "dens_mass_slurry": 1303.05, - "work_mechanical": {0.0: 1569.57}, - "diameter_crystallizer": 1.2521, - "height_slurry": 0.763702, - "height_crystallizer": 1.8782, - "magma_circulation_flow_vol": 0.109397, - "relative_supersaturation": {"NaCl": 0.572391}, - "t_res": 1.0228, - "volume_suspension": 0.940428, - "eq_max_allowable_velocity": 3.7763, - "eq_vapor_space_height": 0.939111, - "eq_minimum_height_diameter_ratio": 1.8782, - "energy_flow_superheated_vapor": 1457.19, - "delta_temperature_in": {0.0: 4.3421}, - "delta_temperature_out": {0.0: 44.83}, - "delta_temperature": {0.0: 16.85}, - "heat_exchanger_area": 931.31, - }, - 4: { - "product_volumetric_solids_fraction": 0.131442, - "temperature_operating": 323.22, - "pressure_operating": 9500.0, - "dens_mass_magma": 278.0, - "dens_mass_slurry": 1308.49, - "work_mechanical": {0.0: 1457.19}, - "diameter_crystallizer": 1.4623, - "height_slurry": 0.537417, - "height_crystallizer": 2.1935, - "magma_circulation_flow_vol": 0.101015, - "relative_supersaturation": {"NaCl": 0.576466}, - "t_res": 1.0228, - "volume_suspension": 0.902678, - "eq_max_allowable_velocity": 5.4596, - "eq_vapor_space_height": 1.0967, - "eq_minimum_height_diameter_ratio": 2.1935, - "energy_flow_superheated_vapor": 1405.09, - "delta_temperature_in": {0.0: 17.29}, - "delta_temperature_out": {0.0: 40.69}, - "delta_temperature": {0.0: 27.32}, - "heat_exchanger_area": 533.18, + "heat_exchanger_area": 104.251, }, } @@ -715,11 +729,11 @@ def test_solution(self, MEC4_frame): assert pytest.approx(value(effv), rel=1e-3) == r steam_results_dict = { - "flow_mass_phase_comp": {("Liq", "H2O"): 0.0, ("Vap", "H2O"): 0.89795}, - "temperature": 416.8, - "pressure": 401325.0, - "dh_vap_mass": 2133119.0, - "pressure_sat": 401325.0, + "flow_mass_phase_comp": {("Vap", "H2O"): 0.177583}, + "temperature": 432.06, + "pressure": 601325.0, + "dh_vap_mass": 2085530.7, + "pressure_sat": 601325.0, } for v, r in steam_results_dict.items(): @@ -731,14 +745,16 @@ def test_solution(self, MEC4_frame): assert pytest.approx(value(sv), rel=1e-3) == r @pytest.mark.component - def test_costing(self, MEC4_frame): - m = MEC4_frame + def test_costing(self, MEC2_frame): + m = MEC2_frame m.fs.costing = TreatmentCosting() + # m.fs.costing.base_currency = pyunits.USD_2018 m.fs.unit.costing = UnitModelCostingBlock( flowsheet_costing_block=m.fs.costing, + costing_method_arguments={"cost_type": "mass_basis"}, ) - m.fs.costing.nacl_recovered.cost.set_value(-0.024) + m.fs.costing.nacl_recovered.cost.set_value(-0.011) m.fs.costing.cost_process() m.fs.costing.add_LCOW(m.fs.unit.total_flow_vol_in) m.fs.costing.add_specific_energy_consumption( @@ -748,115 +764,24 @@ def test_costing(self, MEC4_frame): assert_optimal_termination(results) sys_costing_dict = { - "aggregate_capital_cost": 4934127.42, - "aggregate_flow_electricity": 8.4612, - "aggregate_flow_NaCl_recovered": 0.299999, - "aggregate_flow_steam": 0.4304, + "aggregate_capital_cost": 3902338.7, + "aggregate_flow_electricity": 0.95390, + "aggregate_flow_NaCl_recovered": 1.0799, + "aggregate_flow_steam": 0.05888, "aggregate_flow_costs": { - "electricity": 6095.09, - "NaCl_recovered": -269822.09, - "steam": 63793.11, + "electricity": 687.144, + "NaCl_recovered": -445206.6, + "steam": 8726.6, }, - "aggregate_direct_capital_cost": 2467063.71, - "total_capital_cost": 4934127.42, - "total_operating_cost": -51910.07, - "maintenance_labor_chemical_operating_cost": 148023.82, - "total_fixed_operating_cost": 148023.82, - "total_variable_operating_cost": -199933.89, - "total_annualized_cost": 500494.84, - "LCOW": 4.3957, - "SEC": 0.65144, - } - - for v, r in sys_costing_dict.items(): - cv = getattr(m.fs.costing, v) - if cv.is_indexed(): - for i, s in r.items(): - assert pytest.approx(value(cv[i]), rel=1e-3) == s - else: - assert pytest.approx(value(cv), rel=1e-3) == r - - eff_costing_dict = { - "capital_cost": 4934127.42, - "cost_factor": 2.0, - "direct_capital_cost": 2467063.71, - "capital_cost_crystallizer_effect_1": 701221.05, - "capital_cost_heat_exchanger_effect_1": 234213.59, - "capital_cost_effect_1": 467717.32, - "capital_cost_crystallizer_effect_2": 667484.84, - "capital_cost_heat_exchanger_effect_2": 558948.69, - "capital_cost_effect_2": 613216.76, - "capital_cost_crystallizer_effect_3": 640779.56, - "capital_cost_heat_exchanger_effect_3": 954745.08, - "capital_cost_effect_3": 797762.32, - "capital_cost_crystallizer_effect_4": 624930.83, - "capital_cost_heat_exchanger_effect_4": 551803.75, - "capital_cost_effect_4": 588367.29, - } - - for v, r in eff_costing_dict.items(): - cv = getattr(m.fs.unit.costing, v) - if cv.is_indexed(): - for i, s in r.items(): - assert pytest.approx(value(cv[i]), rel=1e-3) == s - else: - assert pytest.approx(value(cv), rel=1e-3) == r - - @pytest.mark.component - def test_costing_by_volume(self): - - m = build_mec4() - - for _, eff in m.fs.unit.effects.items(): - eff.effect.crystal_median_length.fix(0.6e-3) - eff.effect.crystal_growth_rate.fix(5e-9) - - self.test_calculate_scaling(m) - - m.fs.unit.initialize() - - assert degrees_of_freedom(m) == 0 - - results = solver.solve(m) - assert_optimal_termination(results) - - m.fs.costing = TreatmentCosting() - m.fs.unit.costing = UnitModelCostingBlock( - flowsheet_costing_block=m.fs.costing, - costing_method_arguments={"cost_type": "volume_basis"}, - ) - - m.fs.costing.nacl_recovered.cost.set_value(-0.024) - m.fs.costing.cost_process() - m.fs.costing.add_LCOW(m.fs.unit.total_flow_vol_in) - m.fs.costing.add_specific_energy_consumption( - m.fs.unit.total_flow_vol_in, name="SEC" - ) - results = solver.solve(m) - - assert_optimal_termination(results) - - # return m - - sys_costing_dict = { - "aggregate_capital_cost": 5078402.93, - "aggregate_flow_electricity": 8.4612, - "aggregate_flow_NaCl_recovered": 0.299999, - "aggregate_flow_steam": 0.430492, - "aggregate_flow_costs": { - "electricity": 6095.09, - "NaCl_recovered": -269822.09, - "steam": 63793.11, - }, - "aggregate_direct_capital_cost": 2539201.46, - "total_capital_cost": 5078402.93, - "total_operating_cost": -47581.81, - "maintenance_labor_chemical_operating_cost": 152352.08, - "total_fixed_operating_cost": 152352.08, - "total_variable_operating_cost": -199933.89, - "total_annualized_cost": 520975.61, - "LCOW": 4.5756, - "SEC": 0.65144, + "aggregate_direct_capital_cost": 1951169.3, + "total_capital_cost": 3902338.7, + "total_operating_cost": -318722.72, + "maintenance_labor_chemical_operating_cost": 117070.16, + "total_fixed_operating_cost": 117070.16, + "total_variable_operating_cost": -435792.88, + "total_annualized_cost": 118167.30, + "LCOW": 1.27058636, + "SEC": 0.08991097, } for v, r in sys_costing_dict.items(): @@ -868,21 +793,14 @@ def test_costing_by_volume(self): assert pytest.approx(value(cv), rel=1e-3) == r eff_costing_dict = { - "capital_cost": 5078402.93, - "cost_factor": 2.0, - "direct_capital_cost": 2539201.46, - "capital_cost_crystallizer_effect_1": 715324.02, - "capital_cost_heat_exchanger_effect_1": 234213.59, - "capital_cost_effect_1": 474768.8, - "capital_cost_crystallizer_effect_2": 697956.34, - "capital_cost_heat_exchanger_effect_2": 558948.69, - "capital_cost_effect_2": 628452.52, - "capital_cost_crystallizer_effect_3": 676895.98, - "capital_cost_heat_exchanger_effect_3": 954745.08, - "capital_cost_effect_3": 815820.53, - "capital_cost_crystallizer_effect_4": 688515.45, - "capital_cost_heat_exchanger_effect_4": 551803.75, - "capital_cost_effect_4": 620159.6, + "capital_cost": 3902338.7, + "direct_capital_cost": 1951169.3, + "capital_cost_crystallizer_effect_1": 1777273.3, + "capital_cost_heat_exchanger_effect_1": 40936.8, + "capital_cost_effect_1": 909105.0, + "capital_cost_crystallizer_effect_2": 1971550.7, + "capital_cost_heat_exchanger_effect_2": 112577.7, + "capital_cost_effect_2": 1042064.2, } for v, r in eff_costing_dict.items(): @@ -1434,3 +1352,684 @@ def test_costing(self, MEC3_frame): assert pytest.approx(value(cv[i]), rel=1e-3) == s else: assert pytest.approx(value(cv), rel=1e-3) == r + + +class TestMultiEffectCrystallizer_4Effects: + @pytest.fixture(scope="class") + def MEC4_frame(self): + """ + Test crystallizer with 4 effects + """ + m = build_mec4() + return m + + @pytest.mark.unit + def test_config(self, MEC4_frame): + m = MEC4_frame + assert len(m.fs.unit.config) == 6 + + assert not m.fs.unit.config.dynamic + assert not m.fs.unit.config.has_holdup + assert m.fs.unit.config.property_package is m.fs.properties + assert m.fs.unit.config.property_package_vapor is m.fs.vapor_properties + assert m.fs.unit.config.number_effects == 4 + assert m.fs.unit.config.number_effects == len(m.fs.unit.effects) + + for _, eff in m.fs.unit.effects.items(): + assert isinstance(eff.effect, CrystallizerEffect) + assert not eff.effect.config.standalone + + @pytest.mark.unit + def test_build(self, MEC4_frame): + m = MEC4_frame + + # test ports and variables + port_lst = ["inlet", "outlet", "solids", "vapor", "steam"] + port_vars_lst = ["flow_mass_phase_comp", "pressure", "temperature"] + state_blks = [ + "properties_in", + "properties_out", + "properties_pure_water", + "properties_solids", + "properties_vapor", + ] + + for port_str in port_lst: + assert hasattr(m.fs.unit, port_str) + port = getattr(m.fs.unit, port_str) + assert len(port.vars) == 3 + assert isinstance(port, Port) + for var_str in port_vars_lst: + assert hasattr(port, var_str) + var = getattr(port, var_str) + assert isinstance(var, Var) + + for n, eff in m.fs.unit.effects.items(): + for b in state_blks: + assert hasattr(eff.effect, b) + sb = getattr(eff.effect, b) + assert isinstance(sb, NaClStateBlock) + assert sb[0].temperature.ub == 1000 + + effect_params = [ + "approach_temperature_heat_exchanger", + "dimensionless_crystal_length", + ] + effect_vars = [ + "crystal_growth_rate", + "crystal_median_length", + "crystallization_yield", + "dens_mass_magma", + "dens_mass_slurry", + "diameter_crystallizer", + "height_crystallizer", + "height_slurry", + "magma_circulation_flow_vol", + "pressure_operating", + "product_volumetric_solids_fraction", + "relative_supersaturation", + "souders_brown_constant", + "t_res", + "temperature_operating", + "volume_suspension", + "work_mechanical", + ] + + effect_exprs = [ + "delta_temperature", + "eq_max_allowable_velocity", + "eq_minimum_height_diameter_ratio", + "eq_vapor_space_height", + ] + + effect_constr = [ + "eq_mass_balance_constraints", + "eq_solubility_massfrac_equality_constraint", + "eq_removal_balance", + "eq_vol_fraction_solids", + "eq_dens_magma", + "eq_operating_pressure_constraint", + "eq_relative_supersaturation", + "eq_enthalpy_balance", + "eq_p_con3", + "eq_T_con1", + "eq_T_con2", + "eq_T_con3", + "eq_minimum_hex_circulation_rate_constraint", + "eq_dens_mass_slurry", + "eq_residence_time", + "eq_suspension_volume", + "eq_vapor_head_diameter_constraint", + "eq_slurry_height_constraint", + "eq_crystallizer_height_constraint", + "eq_pure_water_mass_flow_rate", + "eq_vapor_energy_constraint", + "eq_p_con1", + "eq_p_con2", + "eq_p_con4", + "eq_p_con5", + ] + + assert hasattr(m.fs.unit, "recovery_vol_phase") + assert isinstance(m.fs.unit.recovery_vol_phase, Var) + + for n, eff in m.fs.unit.effects.items(): + for p in effect_params: + assert hasattr(eff.effect, p) + for v in effect_vars: + assert hasattr(eff.effect, v) + for e in effect_exprs: + assert hasattr(eff.effect, e) + for c in effect_constr: + assert hasattr(eff.effect, c) + assert hasattr(eff.effect, f"eq_delta_temperature_inlet_effect_{n}") + assert hasattr(eff.effect, f"eq_delta_temperature_outlet_effect_{n}") + assert hasattr(eff.effect, f"eq_heat_transfer_effect_{n}") + if n == 1: + assert number_variables(eff.effect) == 154 + assert number_total_constraints(eff.effect) == 128 + assert number_unused_variables(eff.effect) == 2 + assert hasattr(eff.effect, "heating_steam") + assert isinstance(eff.effect.heating_steam, WaterStateBlock) + assert eff.effect.heating_steam[0].temperature.ub == 1000 + assert hasattr(eff.effect, "eq_heating_steam_flow_rate") + if n != 1: + assert number_variables(eff.effect) == 148 + assert number_total_constraints(eff.effect) == 126 + assert number_unused_variables(eff.effect) == 0 + assert hasattr( + eff.effect, f"eq_energy_for_effect_{n}_from_effect_{n - 1}" + ) + + assert number_variables(m) == 757 + assert number_total_constraints(m) == 519 + assert number_unused_variables(m) == 43 + + assert_units_consistent(m) + + @pytest.mark.unit + def test_dof(self, MEC4_frame): + + m = MEC4_frame + # With mass flow rates into each of the effects fixed, + # and the control_volume flows fixed, the model is over specified + assert degrees_of_freedom(m) == -2 + # Unfixing the mass flow rates for CV will result in 0 DOF + # (This is only done for testing purposes) + m.fs.unit.control_volume.properties_in[0].flow_mass_phase_comp[ + "Liq", "H2O" + ].unfix() + m.fs.unit.control_volume.properties_in[0].flow_mass_phase_comp[ + "Liq", "NaCl" + ].unfix() + assert degrees_of_freedom(m) == 0 + for n, eff in m.fs.unit.effects.items(): + if n == 1: + assert degrees_of_freedom(eff.effect) == 0 + else: + assert degrees_of_freedom(eff.effect) == 3 + m.fs.unit.control_volume.properties_in[0].flow_mass_phase_comp[ + "Liq", "H2O" + ].fix() + m.fs.unit.control_volume.properties_in[0].flow_mass_phase_comp[ + "Liq", "NaCl" + ].fix() + assert degrees_of_freedom(m) == -2 + # Alternatively, one can .set_value() each of the mass flows for the individual effects, + # resulting in 6 DOF: + for n, eff in m.fs.unit.effects.items(): + eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "H2O"].unfix() + eff.effect.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"].unfix() + assert degrees_of_freedom(m) == 6 + + @pytest.mark.unit + def test_calculate_scaling(self, MEC4_frame): + m = MEC4_frame + + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Liq", "H2O") + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Liq", "NaCl") + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Vap", "H2O") + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Sol", "NaCl") + ) + m.fs.vapor_properties.set_default_scaling( + "flow_mass_phase_comp", 1e-1, index=("Vap", "H2O") + ) + m.fs.vapor_properties.set_default_scaling( + "flow_mass_phase_comp", 1, index=("Liq", "H2O") + ) + + calculate_scaling_factors(m) + unscaled_var_list = list(unscaled_variables_generator(m)) + assert len(unscaled_var_list) == 0 + badly_scaled_var_list = list(badly_scaled_var_generator(m)) + assert len(badly_scaled_var_list) == 0 + + @pytest.mark.component + def test_initialize(self, MEC4_frame): + m = MEC4_frame + m.fs.unit.initialize() + c0 = value( + m.fs.unit.effects[1] + .effect.properties_in[0] + .conc_mass_phase_comp["Liq", "NaCl"] + ) + htc = value(m.fs.unit.effects[1].effect.overall_heat_transfer_coefficient) + assert degrees_of_freedom(m) == 0 + for n, eff in m.fs.unit.effects.items(): + assert ( + not eff.effect.properties_in[0] + .flow_mass_phase_comp["Liq", "H2O"] + .is_fixed() + ) + assert ( + not eff.effect.properties_in[0] + .flow_mass_phase_comp["Liq", "NaCl"] + .is_fixed() + ) + assert ( + pytest.approx( + value( + eff.effect.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] + ), + rel=1e-6, + ) + == c0 + ) + if n == 1: + assert degrees_of_freedom(eff.effect) == 2 + if n != 1: + assert ( + eff.effect.properties_in[0] + .conc_mass_phase_comp["Liq", "NaCl"] + .is_fixed() + ) + linking_constr = getattr( + eff.effect, f"eq_energy_for_effect_{n}_from_effect_{n - 1}" + ) + assert linking_constr.active + assert eff.effect.overall_heat_transfer_coefficient.is_fixed() + assert value(eff.effect.overall_heat_transfer_coefficient) == htc + assert degrees_of_freedom(eff.effect) == 3 + + @pytest.mark.component + def test_solve(self, MEC4_frame): + + m = MEC4_frame + + results = solver.solve(m) + assert_optimal_termination(results) + + @pytest.mark.component + def test_conservation(self, MEC4_frame): + + m = MEC4_frame + + comp_lst = ["NaCl", "H2O"] + phase_lst = ["Sol", "Liq", "Vap"] + + total_mass_flow_water_in = 0 + total_mass_flow_salt_in = 0 + total_mass_flow_water_out = 0 + + for _, e in m.fs.unit.effects.items(): + eff = e.effect + + phase_comp_list = [ + (p, j) + for j in comp_lst + for p in phase_lst + if (p, j) in eff.properties_in[0].phase_component_set + ] + flow_mass_in = sum( + eff.properties_in[0].flow_mass_phase_comp[p, j] + for p in phase_lst + for j in comp_lst + if (p, j) in phase_comp_list + ) + flow_mass_out = sum( + eff.properties_out[0].flow_mass_phase_comp[p, j] + for p in phase_lst + for j in comp_lst + if (p, j) in phase_comp_list + ) + flow_mass_solids = sum( + eff.properties_solids[0].flow_mass_phase_comp[p, j] + for p in phase_lst + for j in comp_lst + if (p, j) in phase_comp_list + ) + flow_mass_vapor = sum( + eff.properties_vapor[0].flow_mass_phase_comp[p, j] + for p in phase_lst + for j in comp_lst + if (p, j) in phase_comp_list + ) + + assert ( + abs( + value( + flow_mass_in + - flow_mass_out + - flow_mass_solids + - flow_mass_vapor + ) + ) + <= 1e-6 + ) + + assert ( + abs( + value( + flow_mass_in * eff.properties_in[0].enth_mass_phase["Liq"] + - flow_mass_out * eff.properties_out[0].enth_mass_phase["Liq"] + - flow_mass_vapor + * eff.properties_vapor[0].enth_mass_solvent["Vap"] + - flow_mass_solids + * eff.properties_solids[0].enth_mass_solute["Sol"] + - flow_mass_solids + * eff.properties_solids[0].dh_crystallization_mass_comp["NaCl"] + + eff.work_mechanical[0] + ) + ) + <= 1e-2 + ) + + total_mass_flow_water_in += value( + eff.properties_in[0].flow_mass_phase_comp["Liq", "H2O"] + ) + total_mass_flow_salt_in += value( + eff.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"] + ) + total_mass_flow_water_out += value( + eff.properties_pure_water[0].flow_mass_phase_comp["Liq", "H2O"] + ) + + # Test control volume mass balance + assert ( + pytest.approx( + m.fs.unit.control_volume.properties_in[0] + .flow_mass_phase_comp["Liq", "H2O"] + .value, + rel=1e-6, + ) + == total_mass_flow_water_in + ) + assert ( + pytest.approx( + m.fs.unit.control_volume.properties_in[0] + .flow_mass_phase_comp["Liq", "NaCl"] + .value, + rel=1e-6, + ) + == total_mass_flow_salt_in + ) + assert ( + pytest.approx( + m.fs.unit.control_volume.properties_out[0] + .flow_mass_phase_comp["Liq", "H2O"] + .value, + rel=1e-6, + ) + == total_mass_flow_water_out + ) + + @pytest.mark.component + def test_solution(self, MEC4_frame): + m = MEC4_frame + + assert ( + pytest.approx(value(m.fs.unit.recovery_vol_phase["Liq"]), rel=1e-3) + == 0.734452 + ) + + unit_results_dict = { + 1: { + "crystallization_yield": {"NaCl": 0.5}, + "product_volumetric_solids_fraction": 0.132962, + "temperature_operating": 359.48, + "pressure_operating": 45000.0, + "dens_mass_magma": 281.21, + "dens_mass_slurry": 1298.22, + "work_mechanical": {0.0: 1915.44}, + "diameter_crystallizer": 1.1448, + "height_slurry": 1.073, + "height_crystallizer": 1.9316, + "magma_circulation_flow_vol": 0.134172, + "relative_supersaturation": {"NaCl": 0.567033}, + "t_res": 1.0228, + "volume_suspension": 1.1045, + "eq_max_allowable_velocity": 2.6304, + "eq_vapor_space_height": 0.858635, + "eq_minimum_height_diameter_ratio": 1.7172, + "energy_flow_superheated_vapor": 1706.7, + "delta_temperature_in": {0.0: 57.39}, + "delta_temperature_out": {0.0: 123.72}, + "delta_temperature": {0.0: 86.31}, + "heat_exchanger_area": 221.91, + }, + 2: { + "product_volumetric_solids_fraction": 0.132109, + "temperature_operating": 344.86, + "pressure_operating": 25000.0, + "dens_mass_magma": 279.41, + "dens_mass_slurry": 1301.84, + "work_mechanical": {0.0: 1706.7}, + "diameter_crystallizer": 1.2481, + "height_slurry": 0.828632, + "height_crystallizer": 1.8721, + "magma_circulation_flow_vol": 0.119101, + "relative_supersaturation": {"NaCl": 0.571246}, + "t_res": 1.0228, + "volume_suspension": 1.0138, + "eq_max_allowable_velocity": 3.4641, + "eq_vapor_space_height": 0.936079, + "eq_minimum_height_diameter_ratio": 1.8721, + "energy_flow_superheated_vapor": 1569.57, + "delta_temperature_in": {0.0: 14.61}, + "delta_temperature_out": {0.0: 58.77}, + "delta_temperature": {0.0: 31.59}, + "heat_exchanger_area": 540.22, + }, + 3: { + "product_volumetric_solids_fraction": 0.131922, + "temperature_operating": 340.52, + "pressure_operating": 20800.0, + "dens_mass_magma": 279.01, + "dens_mass_slurry": 1303.05, + "work_mechanical": {0.0: 1569.57}, + "diameter_crystallizer": 1.2521, + "height_slurry": 0.763702, + "height_crystallizer": 1.8782, + "magma_circulation_flow_vol": 0.109397, + "relative_supersaturation": {"NaCl": 0.572391}, + "t_res": 1.0228, + "volume_suspension": 0.940428, + "eq_max_allowable_velocity": 3.7763, + "eq_vapor_space_height": 0.939111, + "eq_minimum_height_diameter_ratio": 1.8782, + "energy_flow_superheated_vapor": 1457.19, + "delta_temperature_in": {0.0: 4.3421}, + "delta_temperature_out": {0.0: 44.83}, + "delta_temperature": {0.0: 16.85}, + "heat_exchanger_area": 931.31, + }, + 4: { + "product_volumetric_solids_fraction": 0.131442, + "temperature_operating": 323.22, + "pressure_operating": 9500.0, + "dens_mass_magma": 278.0, + "dens_mass_slurry": 1308.49, + "work_mechanical": {0.0: 1457.19}, + "diameter_crystallizer": 1.4623, + "height_slurry": 0.537417, + "height_crystallizer": 2.1935, + "magma_circulation_flow_vol": 0.101015, + "relative_supersaturation": {"NaCl": 0.576466}, + "t_res": 1.0228, + "volume_suspension": 0.902678, + "eq_max_allowable_velocity": 5.4596, + "eq_vapor_space_height": 1.0967, + "eq_minimum_height_diameter_ratio": 2.1935, + "energy_flow_superheated_vapor": 1405.09, + "delta_temperature_in": {0.0: 17.29}, + "delta_temperature_out": {0.0: 40.69}, + "delta_temperature": {0.0: 27.32}, + "heat_exchanger_area": 533.18, + }, + } + + for n, d in unit_results_dict.items(): + eff = m.fs.unit.effects[n].effect + for v, r in d.items(): + effv = getattr(eff, v) + if effv.is_indexed(): + for i, s in r.items(): + assert pytest.approx(value(effv[i]), rel=1e-3) == s + else: + assert pytest.approx(value(effv), rel=1e-3) == r + + steam_results_dict = { + "flow_mass_phase_comp": {("Liq", "H2O"): 0.0, ("Vap", "H2O"): 0.89795}, + "temperature": 416.8, + "pressure": 401325.0, + "dh_vap_mass": 2133119.0, + "pressure_sat": 401325.0, + } + + for v, r in steam_results_dict.items(): + sv = getattr(m.fs.unit.effects[1].effect.heating_steam[0], v) + if sv.is_indexed(): + for i, s in r.items(): + assert pytest.approx(value(sv[i]), rel=1e-3) == s + else: + assert pytest.approx(value(sv), rel=1e-3) == r + + @pytest.mark.component + def test_costing(self, MEC4_frame): + m = MEC4_frame + m.fs.costing = TreatmentCosting() + m.fs.unit.costing = UnitModelCostingBlock( + flowsheet_costing_block=m.fs.costing, + ) + + m.fs.costing.nacl_recovered.cost.set_value(-0.024) + m.fs.costing.cost_process() + m.fs.costing.add_LCOW(m.fs.unit.total_flow_vol_in) + m.fs.costing.add_specific_energy_consumption( + m.fs.unit.total_flow_vol_in, name="SEC" + ) + results = solver.solve(m) + assert_optimal_termination(results) + + sys_costing_dict = { + "aggregate_capital_cost": 4934127.42, + "aggregate_flow_electricity": 8.4612, + "aggregate_flow_NaCl_recovered": 0.299999, + "aggregate_flow_steam": 0.4304, + "aggregate_flow_costs": { + "electricity": 6095.09, + "NaCl_recovered": -269822.09, + "steam": 63793.11, + }, + "aggregate_direct_capital_cost": 2467063.71, + "total_capital_cost": 4934127.42, + "total_operating_cost": -51910.07, + "maintenance_labor_chemical_operating_cost": 148023.82, + "total_fixed_operating_cost": 148023.82, + "total_variable_operating_cost": -199933.89, + "total_annualized_cost": 500494.84, + "LCOW": 4.3957, + "SEC": 0.65144, + } + + for v, r in sys_costing_dict.items(): + cv = getattr(m.fs.costing, v) + if cv.is_indexed(): + for i, s in r.items(): + assert pytest.approx(value(cv[i]), rel=1e-3) == s + else: + assert pytest.approx(value(cv), rel=1e-3) == r + + eff_costing_dict = { + "capital_cost": 4934127.42, + "cost_factor": 2.0, + "direct_capital_cost": 2467063.71, + "capital_cost_crystallizer_effect_1": 701221.05, + "capital_cost_heat_exchanger_effect_1": 234213.59, + "capital_cost_effect_1": 467717.32, + "capital_cost_crystallizer_effect_2": 667484.84, + "capital_cost_heat_exchanger_effect_2": 558948.69, + "capital_cost_effect_2": 613216.76, + "capital_cost_crystallizer_effect_3": 640779.56, + "capital_cost_heat_exchanger_effect_3": 954745.08, + "capital_cost_effect_3": 797762.32, + "capital_cost_crystallizer_effect_4": 624930.83, + "capital_cost_heat_exchanger_effect_4": 551803.75, + "capital_cost_effect_4": 588367.29, + } + + for v, r in eff_costing_dict.items(): + cv = getattr(m.fs.unit.costing, v) + if cv.is_indexed(): + for i, s in r.items(): + assert pytest.approx(value(cv[i]), rel=1e-3) == s + else: + assert pytest.approx(value(cv), rel=1e-3) == r + + @pytest.mark.component + def test_costing_by_volume(self): + + m = build_mec4() + + for _, eff in m.fs.unit.effects.items(): + eff.effect.crystal_median_length.fix(0.6e-3) + eff.effect.crystal_growth_rate.fix(5e-9) + + self.test_calculate_scaling(m) + + m.fs.unit.initialize() + + assert degrees_of_freedom(m) == 0 + + results = solver.solve(m) + assert_optimal_termination(results) + + m.fs.costing = TreatmentCosting() + m.fs.unit.costing = UnitModelCostingBlock( + flowsheet_costing_block=m.fs.costing, + costing_method_arguments={"cost_type": "volume_basis"}, + ) + + m.fs.costing.nacl_recovered.cost.set_value(-0.024) + m.fs.costing.cost_process() + m.fs.costing.add_LCOW(m.fs.unit.total_flow_vol_in) + m.fs.costing.add_specific_energy_consumption( + m.fs.unit.total_flow_vol_in, name="SEC" + ) + results = solver.solve(m) + + assert_optimal_termination(results) + + # return m + + sys_costing_dict = { + "aggregate_capital_cost": 5078402.93, + "aggregate_flow_electricity": 8.4612, + "aggregate_flow_NaCl_recovered": 0.299999, + "aggregate_flow_steam": 0.430492, + "aggregate_flow_costs": { + "electricity": 6095.09, + "NaCl_recovered": -269822.09, + "steam": 63793.11, + }, + "aggregate_direct_capital_cost": 2539201.46, + "total_capital_cost": 5078402.93, + "total_operating_cost": -47581.81, + "maintenance_labor_chemical_operating_cost": 152352.08, + "total_fixed_operating_cost": 152352.08, + "total_variable_operating_cost": -199933.89, + "total_annualized_cost": 520975.61, + "LCOW": 4.5756, + "SEC": 0.65144, + } + + for v, r in sys_costing_dict.items(): + cv = getattr(m.fs.costing, v) + if cv.is_indexed(): + for i, s in r.items(): + assert pytest.approx(value(cv[i]), rel=1e-3) == s + else: + assert pytest.approx(value(cv), rel=1e-3) == r + + eff_costing_dict = { + "capital_cost": 5078402.93, + "cost_factor": 2.0, + "direct_capital_cost": 2539201.46, + "capital_cost_crystallizer_effect_1": 715324.02, + "capital_cost_heat_exchanger_effect_1": 234213.59, + "capital_cost_effect_1": 474768.8, + "capital_cost_crystallizer_effect_2": 697956.34, + "capital_cost_heat_exchanger_effect_2": 558948.69, + "capital_cost_effect_2": 628452.52, + "capital_cost_crystallizer_effect_3": 676895.98, + "capital_cost_heat_exchanger_effect_3": 954745.08, + "capital_cost_effect_3": 815820.53, + "capital_cost_crystallizer_effect_4": 688515.45, + "capital_cost_heat_exchanger_effect_4": 551803.75, + "capital_cost_effect_4": 620159.6, + } + + for v, r in eff_costing_dict.items(): + cv = getattr(m.fs.unit.costing, v) + if cv.is_indexed(): + for i, s in r.items(): + assert pytest.approx(value(cv[i]), rel=1e-3) == s + else: + assert pytest.approx(value(cv), rel=1e-3) == r From a84ceeacded43048d330f4f08d40b84881fac05b Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Tue, 8 Oct 2024 18:15:52 -0600 Subject: [PATCH 72/80] add ConfigurationError for number_effects=1 --- .../reflo/unit_models/multi_effect_crystallizer.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py index 4b5024c0..d153e490 100644 --- a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py +++ b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py @@ -34,7 +34,7 @@ FlowsheetBlock, UnitModelCostingBlock, ) -from idaes.core.util.exceptions import InitializationError +from idaes.core.util.exceptions import InitializationError, ConfigurationError import idaes.core.util.scaling as iscale from idaes.core.util.config import is_physical_parameter_block from idaes.core.util.model_statistics import degrees_of_freedom @@ -138,6 +138,13 @@ class MultiEffectCrystallizerData(InitializationMixin, UnitModelBlockData): def build(self): super().build() + if self.config.number_effects <= 1: + raise ConfigurationError( + "The MultiEffectCrystallizer model requires more than 1 effect." + "To model a crystallizer with one effect, use the CrystallizerEffect model with 'standalone=True'." + + ) + self.scaling_factor = Suffix(direction=Suffix.EXPORT) self.control_volume = ControlVolume0DBlock( From e0b5b44ca69cb2a526d05fb1446b3c670fbe8838 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Tue, 8 Oct 2024 18:16:04 -0600 Subject: [PATCH 73/80] add test for single effect to multi effect crystallizer --- .../tests/test_multi_effect_crystallizer.py | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/watertap_contrib/reflo/unit_models/tests/test_multi_effect_crystallizer.py b/src/watertap_contrib/reflo/unit_models/tests/test_multi_effect_crystallizer.py index 7bd77fb0..35379487 100644 --- a/src/watertap_contrib/reflo/unit_models/tests/test_multi_effect_crystallizer.py +++ b/src/watertap_contrib/reflo/unit_models/tests/test_multi_effect_crystallizer.py @@ -21,8 +21,8 @@ units as pyunits, ) from pyomo.network import Port -from idaes.core import FlowsheetBlock +from idaes.core import FlowsheetBlock from idaes.core.util.model_statistics import ( degrees_of_freedom, number_variables, @@ -35,6 +35,7 @@ badly_scaled_var_generator, ) from idaes.core import UnitModelCostingBlock +from idaes.core.util.exceptions import ConfigurationError from watertap.core.solvers import get_solver from watertap.property_models.unit_specific.cryst_prop_pack import ( @@ -289,6 +290,28 @@ def build_mec4(): return m +@pytest.mark.component +def test_single_effect_error(): + + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.properties = NaClParameterBlock() + m.fs.vapor_properties = WaterParameterBlock() + + error_msg = ( + "The MultiEffectCrystallizer model requires more than 1 effect." + "To model a crystallizer with one effect, use the CrystallizerEffect model with 'standalone=True'." + ) + + with pytest.raises(ConfigurationError, match=error_msg): + m.fs.unit = MultiEffectCrystallizer( + property_package=m.fs.properties, + property_package_vapor=m.fs.vapor_properties, + number_effects=1, + ) + + class TestMultiEffectCrystallizer_2Effects: @pytest.fixture(scope="class") def MEC2_frame(self): From 0bdc651f746ce2bca3d5b5b8fd509197f73b32e2 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Tue, 8 Oct 2024 18:17:24 -0600 Subject: [PATCH 74/80] blackification --- .../reflo/unit_models/multi_effect_crystallizer.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py index d153e490..a65263f3 100644 --- a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py +++ b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py @@ -142,7 +142,6 @@ def build(self): raise ConfigurationError( "The MultiEffectCrystallizer model requires more than 1 effect." "To model a crystallizer with one effect, use the CrystallizerEffect model with 'standalone=True'." - ) self.scaling_factor = Suffix(direction=Suffix.EXPORT) From e0743db7e5ded1134bb52bb1089be9cab4e82bfa Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Tue, 8 Oct 2024 18:35:17 -0600 Subject: [PATCH 75/80] remove pure_water port --- .../reflo/unit_models/multi_effect_crystallizer.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py index a65263f3..97e60b06 100644 --- a/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py +++ b/src/watertap_contrib/reflo/unit_models/multi_effect_crystallizer.py @@ -270,7 +270,6 @@ def eq_heating_steam_flow_rate(b): self.add_port(name="solids", block=effect.properties_solids) self.add_port(name="vapor", block=effect.properties_vapor) - self.add_port(name="pure_water", block=effect.properties_pure_water) self.add_port(name="steam", block=effect.heating_steam) self.steam.temperature.setub(1000) From 53303878cfb8f9f6d98587fa45f8abad85380088 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Tue, 8 Oct 2024 18:39:04 -0600 Subject: [PATCH 76/80] change number_unused_variables test --- .../reflo/unit_models/tests/test_crystallizer_effect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/watertap_contrib/reflo/unit_models/tests/test_crystallizer_effect.py b/src/watertap_contrib/reflo/unit_models/tests/test_crystallizer_effect.py index ccda1350..83baade7 100644 --- a/src/watertap_contrib/reflo/unit_models/tests/test_crystallizer_effect.py +++ b/src/watertap_contrib/reflo/unit_models/tests/test_crystallizer_effect.py @@ -212,7 +212,7 @@ def test_build(self, effect_frame): assert number_variables(m) == 288 assert number_total_constraints(m) == 120 - assert number_unused_variables(m) == 43 + assert number_unused_variables(m) == 44 assert_units_consistent(m) From c383d97c9819b096503adc5e6f8ed1beb27bb195 Mon Sep 17 00:00:00 2001 From: Kurban Sitterley Date: Wed, 9 Oct 2024 08:12:39 -0600 Subject: [PATCH 77/80] remove commented code Co-authored-by: Adam Atia --- .../reflo/unit_models/crystallizer_effect.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py index 9f9e8aa7..026c6484 100644 --- a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py +++ b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py @@ -165,12 +165,6 @@ def build(self): doc="Overall heat transfer coefficient for heat exchangers", ) - # @self.Constraint() - # def eq_pure_vapor_flow_rate(b): - # return ( - # b.properties_pure_water[0].flow_mass_phase_comp["Vap", "H2O"] - # == b.properties_vapor[0].flow_mass_phase_comp["Vap", "H2O"] - # ) @self.Constraint() def eq_pure_water_mass_flow_rate(b): From 8fb814b6c12f84771d06ca083d660e26cbf238cc Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Wed, 9 Oct 2024 08:16:23 -0600 Subject: [PATCH 78/80] black --- src/watertap_contrib/reflo/unit_models/crystallizer_effect.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py index 026c6484..da3956ca 100644 --- a/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py +++ b/src/watertap_contrib/reflo/unit_models/crystallizer_effect.py @@ -165,7 +165,6 @@ def build(self): doc="Overall heat transfer coefficient for heat exchangers", ) - @self.Constraint() def eq_pure_water_mass_flow_rate(b): return ( From 672d4793f5e753ce1378fdd5990a7943d6bc1b23 Mon Sep 17 00:00:00 2001 From: zhuoran29 Date: Thu, 10 Oct 2024 10:45:51 -0400 Subject: [PATCH 79/80] add optimization --- .../tests/test_multi_effect_crystallizer.py | 142 +++++++++++++++++- 1 file changed, 140 insertions(+), 2 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/tests/test_multi_effect_crystallizer.py b/src/watertap_contrib/reflo/unit_models/tests/test_multi_effect_crystallizer.py index 35379487..0f93d8e4 100644 --- a/src/watertap_contrib/reflo/unit_models/tests/test_multi_effect_crystallizer.py +++ b/src/watertap_contrib/reflo/unit_models/tests/test_multi_effect_crystallizer.py @@ -16,6 +16,7 @@ from pyomo.environ import ( ConcreteModel, Var, + Objective, assert_optimal_termination, value, units as pyunits, @@ -2000,8 +2001,6 @@ def test_costing_by_volume(self): assert_optimal_termination(results) - # return m - sys_costing_dict = { "aggregate_capital_cost": 5078402.93, "aggregate_flow_electricity": 8.4612, @@ -2056,3 +2055,142 @@ def test_costing_by_volume(self): assert pytest.approx(value(cv[i]), rel=1e-3) == s else: assert pytest.approx(value(cv), rel=1e-3) == r + + @pytest.mark.component + def test_optimization(self, MEC4_frame): + m = MEC4_frame + + # optimization scenario + n_effects = m.fs.unit.Effects + + [m.fs.unit.effects[n].effect.pressure_operating.unfix() for n in n_effects] + m.fs.unit.effects[1].effect.pressure_operating.setub(1.2 * 1e5) + m.fs.unit.effects[4].effect.pressure_operating.setlb(0.02 * 1e5) + + @m.Constraint([2, 3, 4], doc="Pressure decreasing across effects") + def pressure_bound(b, n): + return ( + b.fs.unit.effects[n].effect.pressure_operating + <= b.fs.unit.effects[n - 1].effect.pressure_operating + ) + + @m.Constraint( + [2, 3, 4], + doc="Temperature difference limit (based on industrial convention)", + ) + def temp_bound(b, n): + return ( + b.fs.unit.effects[n].effect.temperature_operating + >= b.fs.unit.effects[n - 1].effect.temperature_operating - 12 + ) + + m.fs.objective = Objective(expr=m.fs.costing.LCOW) + + opt_results = solver.solve(m, tee=False) + assert_optimal_termination(opt_results) + + @pytest.mark.component + def test_optimization(self, MEC4_frame): + m = MEC4_frame + + # Optimization scenario + + # Looking for the best operating pressures within a typical range + n_effects = m.fs.unit.Effects + [m.fs.unit.effects[n].effect.pressure_operating.unfix() for n in n_effects] + m.fs.unit.effects[1].effect.pressure_operating.setub(1.2 * 1e5) + m.fs.unit.effects[4].effect.pressure_operating.setlb(0.02 * 1e5) + + # Add constraints + @m.Constraint([2, 3, 4], doc="Pressure decreasing across effects") + def pressure_bound(b, n): + return ( + b.fs.unit.effects[n].effect.pressure_operating + <= b.fs.unit.effects[n - 1].effect.pressure_operating + ) + + @m.Constraint( + [2, 3, 4], + doc="Temperature difference limit (based on industrial convention)", + ) + def temp_bound(b, n): + return ( + b.fs.unit.effects[n].effect.temperature_operating + >= b.fs.unit.effects[n - 1].effect.temperature_operating - 12 + ) + + # Set objective to minimize cost + m.fs.objective = Objective(expr=m.fs.costing.LCOW) + + opt_results = solver.solve(m, tee=False) + assert_optimal_termination(opt_results) + + @pytest.mark.component + def test_optimization_solution(self, MEC4_frame): + m = MEC4_frame + + # Check the optimized LCOW + assert pytest.approx(value(m.fs.costing.LCOW), rel=1e-3) == 3.865 + + assert ( + pytest.approx(value(m.fs.unit.recovery_vol_phase["Liq"]), rel=1e-3) + == 0.7544 + ) + + unit_results_dict = { + 1: { + "crystallization_yield": {"NaCl": 0.5}, + "product_volumetric_solids_fraction": 0.135236, + "temperature_operating": 387.01, + "pressure_operating": 120000.0, + "work_mechanical": {0.0: 2163.65}, + "heat_exchanger_area": 329.25, + }, + 2: { + "product_volumetric_solids_fraction": 0.134156, + "temperature_operating": 375.01, + "pressure_operating": 79812.1, + "work_mechanical": {0.0: 1818.52}, + "heat_exchanger_area": 495.94, + }, + 3: { + "product_volumetric_solids_fraction": 0.13323, + "temperature_operating": 363.01, + "pressure_operating": 51490.6, + "work_mechanical": {0.0: 1567.57}, + "heat_exchanger_area": 467.40, + }, + 4: { + "product_volumetric_solids_fraction": 0.132473, + "temperature_operating": 351.01, + "pressure_operating": 32193.7, + "work_mechanical": {0.0: 1386.22}, + "heat_exchanger_area": 458.50, + }, + } + + for n, d in unit_results_dict.items(): + eff = m.fs.unit.effects[n].effect + for v, r in d.items(): + effv = getattr(eff, v) + if effv.is_indexed(): + for i, s in r.items(): + assert pytest.approx(value(effv[i]), rel=1e-3) == s + else: + assert pytest.approx(value(effv), rel=1e-3) == r + + steam_results_dict = { + "flow_mass_phase_comp": {("Liq", "H2O"): 0.0, ("Vap", "H2O"): 1.014}, + "temperature": 416.8, + "pressure": 401325.0, + "dh_vap_mass": 2133119.0, + "pressure_sat": 401325.0, + } + + for v, r in steam_results_dict.items(): + sv = getattr(m.fs.unit.effects[1].effect.heating_steam[0], v) + if sv.is_indexed(): + for i, s in r.items(): + assert pytest.approx(value(sv[i]), rel=1e-3) == s + else: + assert pytest.approx(value(sv), rel=1e-3) == r From 2ce649e551368c43454a2776f5262bc419a6dd77 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 10 Oct 2024 11:52:13 -0600 Subject: [PATCH 80/80] remove duplicate optimization test; clarify conditions; add opt conservation test --- .../tests/test_multi_effect_crystallizer.py | 190 +++++++++++++----- 1 file changed, 141 insertions(+), 49 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/tests/test_multi_effect_crystallizer.py b/src/watertap_contrib/reflo/unit_models/tests/test_multi_effect_crystallizer.py index 0f93d8e4..8044c076 100644 --- a/src/watertap_contrib/reflo/unit_models/tests/test_multi_effect_crystallizer.py +++ b/src/watertap_contrib/reflo/unit_models/tests/test_multi_effect_crystallizer.py @@ -2060,69 +2060,48 @@ def test_costing_by_volume(self): def test_optimization(self, MEC4_frame): m = MEC4_frame - # optimization scenario - n_effects = m.fs.unit.Effects + # Optimization scenario + # Looking for the best operating pressures within a typical range - [m.fs.unit.effects[n].effect.pressure_operating.unfix() for n in n_effects] - m.fs.unit.effects[1].effect.pressure_operating.setub(1.2 * 1e5) - m.fs.unit.effects[4].effect.pressure_operating.setlb(0.02 * 1e5) + for n, eff in m.fs.unit.effects.items(): + eff.effect.pressure_operating.unfix() + if n == m.fs.unit.first_effect: + eff.effect.pressure_operating.setub(1.2e5) + if n == m.fs.unit.last_effect: + eff.effect.pressure_operating.setlb(0.02e5) - @m.Constraint([2, 3, 4], doc="Pressure decreasing across effects") - def pressure_bound(b, n): - return ( - b.fs.unit.effects[n].effect.pressure_operating - <= b.fs.unit.effects[n - 1].effect.pressure_operating - ) + eff_idx = [n for n in m.fs.unit.Effects if n != m.fs.unit.first_effect] - @m.Constraint( - [2, 3, 4], - doc="Temperature difference limit (based on industrial convention)", - ) - def temp_bound(b, n): + @m.fs.unit.Constraint(eff_idx, doc="Pressure decreasing across effects") + def eq_pressure_decreasing_across_eff(b, n): return ( - b.fs.unit.effects[n].effect.temperature_operating - >= b.fs.unit.effects[n - 1].effect.temperature_operating - 12 + b.effects[n].effect.pressure_operating + <= b.effects[n - 1].effect.pressure_operating ) - m.fs.objective = Objective(expr=m.fs.costing.LCOW) - - opt_results = solver.solve(m, tee=False) - assert_optimal_termination(opt_results) - - @pytest.mark.component - def test_optimization(self, MEC4_frame): - m = MEC4_frame - - # Optimization scenario + m.fs.unit.temperature_diff_typical = Var( + initialize=12, + bounds=(0, None), + units=pyunits.degK, + doc="Typical temperature difference limit between effects in industry", + ) - # Looking for the best operating pressures within a typical range - n_effects = m.fs.unit.Effects - [m.fs.unit.effects[n].effect.pressure_operating.unfix() for n in n_effects] - m.fs.unit.effects[1].effect.pressure_operating.setub(1.2 * 1e5) - m.fs.unit.effects[4].effect.pressure_operating.setlb(0.02 * 1e5) - - # Add constraints - @m.Constraint([2, 3, 4], doc="Pressure decreasing across effects") - def pressure_bound(b, n): - return ( - b.fs.unit.effects[n].effect.pressure_operating - <= b.fs.unit.effects[n - 1].effect.pressure_operating - ) + m.fs.unit.temperature_diff_typical.fix() - @m.Constraint( - [2, 3, 4], + @m.fs.unit.Constraint( + eff_idx, doc="Temperature difference limit (based on industrial convention)", ) - def temp_bound(b, n): + def eq_temperature_difference_limit(b, n): return ( - b.fs.unit.effects[n].effect.temperature_operating - >= b.fs.unit.effects[n - 1].effect.temperature_operating - 12 + b.effects[n].effect.temperature_operating + >= b.effects[n - 1].effect.temperature_operating + - b.temperature_diff_typical ) - # Set objective to minimize cost m.fs.objective = Objective(expr=m.fs.costing.LCOW) - opt_results = solver.solve(m, tee=False) + opt_results = solver.solve(m) assert_optimal_termination(opt_results) @pytest.mark.component @@ -2139,7 +2118,6 @@ def test_optimization_solution(self, MEC4_frame): unit_results_dict = { 1: { - "crystallization_yield": {"NaCl": 0.5}, "product_volumetric_solids_fraction": 0.135236, "temperature_operating": 387.01, "pressure_operating": 120000.0, @@ -2194,3 +2172,117 @@ def test_optimization_solution(self, MEC4_frame): assert pytest.approx(value(sv[i]), rel=1e-3) == s else: assert pytest.approx(value(sv), rel=1e-3) == r + + @pytest.mark.component + def test_optimization_conservation(self, MEC4_frame): + + m = MEC4_frame + + comp_lst = ["NaCl", "H2O"] + phase_lst = ["Sol", "Liq", "Vap"] + + total_mass_flow_water_in = 0 + total_mass_flow_salt_in = 0 + total_mass_flow_water_out = 0 + + for _, e in m.fs.unit.effects.items(): + eff = e.effect + + phase_comp_list = [ + (p, j) + for j in comp_lst + for p in phase_lst + if (p, j) in eff.properties_in[0].phase_component_set + ] + flow_mass_in = sum( + eff.properties_in[0].flow_mass_phase_comp[p, j] + for p in phase_lst + for j in comp_lst + if (p, j) in phase_comp_list + ) + flow_mass_out = sum( + eff.properties_out[0].flow_mass_phase_comp[p, j] + for p in phase_lst + for j in comp_lst + if (p, j) in phase_comp_list + ) + flow_mass_solids = sum( + eff.properties_solids[0].flow_mass_phase_comp[p, j] + for p in phase_lst + for j in comp_lst + if (p, j) in phase_comp_list + ) + flow_mass_vapor = sum( + eff.properties_vapor[0].flow_mass_phase_comp[p, j] + for p in phase_lst + for j in comp_lst + if (p, j) in phase_comp_list + ) + + assert ( + abs( + value( + flow_mass_in + - flow_mass_out + - flow_mass_solids + - flow_mass_vapor + ) + ) + <= 1e-6 + ) + + assert ( + abs( + value( + flow_mass_in * eff.properties_in[0].enth_mass_phase["Liq"] + - flow_mass_out * eff.properties_out[0].enth_mass_phase["Liq"] + - flow_mass_vapor + * eff.properties_vapor[0].enth_mass_solvent["Vap"] + - flow_mass_solids + * eff.properties_solids[0].enth_mass_solute["Sol"] + - flow_mass_solids + * eff.properties_solids[0].dh_crystallization_mass_comp["NaCl"] + + eff.work_mechanical[0] + ) + ) + <= 1e-2 + ) + + total_mass_flow_water_in += value( + eff.properties_in[0].flow_mass_phase_comp["Liq", "H2O"] + ) + total_mass_flow_salt_in += value( + eff.properties_in[0].flow_mass_phase_comp["Liq", "NaCl"] + ) + total_mass_flow_water_out += value( + eff.properties_pure_water[0].flow_mass_phase_comp["Liq", "H2O"] + ) + + # Test control volume mass balance + assert ( + pytest.approx( + m.fs.unit.control_volume.properties_in[0] + .flow_mass_phase_comp["Liq", "H2O"] + .value, + rel=1e-6, + ) + == total_mass_flow_water_in + ) + assert ( + pytest.approx( + m.fs.unit.control_volume.properties_in[0] + .flow_mass_phase_comp["Liq", "NaCl"] + .value, + rel=1e-6, + ) + == total_mass_flow_salt_in + ) + assert ( + pytest.approx( + m.fs.unit.control_volume.properties_out[0] + .flow_mass_phase_comp["Liq", "H2O"] + .value, + rel=1e-6, + ) + == total_mass_flow_water_out + )