diff --git a/tutorials/BSM2.ipynb b/tutorials/BSM2.ipynb index 39cfd8f6a7..d0c595a12c 100644 --- a/tutorials/BSM2.ipynb +++ b/tutorials/BSM2.ipynb @@ -60,15 +60,12 @@ "metadata": {}, "outputs": [], "source": [ - "\n", "import pyomo.environ as pyo\n", "from pyomo.network import Arc, SequentialDecomposition\n", "from idaes.core import FlowsheetBlock\n", "import idaes.logger as idaeslog\n", "from idaes.core.solvers import get_solver\n", - "import idaes.core.util.scaling as iscale\n", - "\n", - "\n" + "import idaes.core.util.scaling as iscale" ] }, { @@ -119,7 +116,7 @@ "source": [ "### Step 1.3: Import all BSM2 required property models\n", "\n", - "Property block are an important building block in WaterTap as they are a Python class which contain information on units, physical properties, etc." + "Property blocks are an important building block in WaterTap as they are a Python class which contain information on units, physical properties, etc." ] }, { @@ -183,7 +180,7 @@ "id": "ac6e041d-ee0a-41e7-a194-6db689b2e92c", "metadata": {}, "source": [ - "We then include all the necessary property blocks we imported into the flowsheet" + "We then include all the necessary property blocks we imported into the flowsheet. Namely, we include the ASM1 and ADM1 models, which are separated into their respective property and reaction models. Additionally, the vapor phase of ADM1 was separated into its own property model." ] }, { @@ -209,7 +206,7 @@ "\n", "We will start by setting up the activated sludge process unit models and connectivity.\n", "\n", - "First, we set up a Feed model for our feed stream called Feedwater " + "First, we set up a Feed model for our feed stream and will name it `Feedwater`. " ] }, { @@ -618,6 +615,25 @@ "m.fs.RADM.liquid_outlet.temperature.fix(308.15)" ] }, + { + "cell_type": "markdown", + "id": "58558077", + "metadata": {}, + "source": [ + "Additionally, the dewatering unit includes an equation relating its hydraulic retention time to its volume and influent flowrate. We can choose to specify hydraulic retention time or the unit's volume to satisfy 0 degrees of freedom." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "982691fd", + "metadata": {}, + "outputs": [], + "source": [ + "# Dewatering unit\n", + "m.fs.DU.hydraulic_retention_time.fix(1800 * pyo.units.s)" + ] + }, { "cell_type": "markdown", "id": "663b9e78-c1b3-41b5-b9d1-fe1fc5acd274", diff --git a/watertap/costing/unit_models/dewatering.py b/watertap/costing/unit_models/dewatering.py new file mode 100644 index 0000000000..525ad75248 --- /dev/null +++ b/watertap/costing/unit_models/dewatering.py @@ -0,0 +1,194 @@ +################################################################################# +# 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 ..util import ( + register_costing_parameter_block, + make_capital_cost_var, +) +from idaes.core.util.misc import StrEnum +from idaes.core.util.exceptions import ConfigurationError + +""" +Ref: W. McGivney, S. Kawamura, Cost estimating manual for water treatment facilities, John Wiley & Sons, 2008. http://onlinelibrary.wiley.com/book/10.1002/9780470260036. +""" + + +class DewateringType(StrEnum): + filter_belt_press = "filter_belt_press" + filter_plate_press = "filter_plate_press" + centrifuge = "centrifuge" + + +def cost_dewatering( + blk, dewatering_type=DewateringType.centrifuge, cost_electricity_flow=True +): + + if dewatering_type == DewateringType.centrifuge: + cost_centrifuge(blk, dewatering_type, cost_electricity_flow) + + elif dewatering_type == DewateringType.filter_belt_press: + cost_filter_belt_press(blk, dewatering_type, cost_electricity_flow) + + elif dewatering_type == DewateringType.filter_plate_press: + cost_filter_plate_press(blk, dewatering_type, cost_electricity_flow) + else: + raise ConfigurationError( + f"{blk.unit_model.name} received invalid argument for dewatering_type:" + f" {dewatering_type}. Argument must be a member of the DewateringType Enum class." + ) + + +def build_centrifuge_cost_param_block(blk): + # NOTE: costing data are from McGivney & Kawamura, 2008 + blk.capital_a_parameter = pyo.Var( + initialize=328.03, + doc="A parameter for capital cost", + units=pyo.units.USD_2007 / (pyo.units.gallon / pyo.units.hour), + ) + blk.capital_b_parameter = pyo.Var( + initialize=751295, + doc="B parameter for capital cost", + units=pyo.units.USD_2007, + ) + + +def build_filter_belt_press_cost_param_block(blk): + # NOTE: costing data are from McGivney & Kawamura, 2008 + blk.capital_a_parameter = pyo.Var( + initialize=146.29, + doc="A parameter for capital cost", + units=pyo.units.USD_2007 / (pyo.units.gallon / pyo.units.hour), + ) + blk.capital_b_parameter = pyo.Var( + initialize=433972, + doc="B parameter for capital cost", + units=pyo.units.USD_2007, + ) + + +def build_filter_plate_press_cost_param_block(blk): + # NOTE: costing data are from McGivney & Kawamura, 2008 + blk.capital_a_parameter = pyo.Var( + initialize=102794, + doc="A parameter for capital cost", + units=pyo.units.USD_2007 / (pyo.units.gallon / pyo.units.hour), + ) + blk.capital_b_parameter = pyo.Var( + initialize=0.4216, + doc="B parameter for capital cost", + units=pyo.units.dimensionless, + ) + + +@register_costing_parameter_block( + build_rule=build_centrifuge_cost_param_block, + parameter_block_name="centrifuge", +) +def cost_centrifuge( + blk, dewatering_type=DewateringType.centrifuge, cost_electricity_flow=True +): + """ + Centrifuge costing method + """ + make_capital_cost_var(blk) + cost_blk = blk.costing_package.centrifuge + t0 = blk.flowsheet().time.first() + x = flow_in = pyo.units.convert( + blk.unit_model.inlet.flow_vol[t0], to_units=pyo.units.gallon / pyo.units.hr + ) + blk.capital_cost_constraint = pyo.Constraint( + expr=blk.capital_cost + == pyo.units.convert( + cost_blk.capital_a_parameter * x + cost_blk.capital_b_parameter, + to_units=blk.costing_package.base_currency, + ) + ) + if cost_electricity_flow: + blk.costing_package.cost_flow( + pyo.units.convert( + blk.unit_model.electricity_consumption[t0], + to_units=pyo.units.kW, + ), + "electricity", + ) + + +@register_costing_parameter_block( + build_rule=build_filter_belt_press_cost_param_block, + parameter_block_name="filter_belt_press", +) +def cost_filter_belt_press( + blk, dewatering_type=DewateringType.filter_belt_press, cost_electricity_flow=True +): + """ + Belt Press Filter costing method + """ + make_capital_cost_var(blk) + cost_blk = blk.costing_package.filter_belt_press + t0 = blk.flowsheet().time.first() + x = flow_in = pyo.units.convert( + blk.unit_model.inlet.flow_vol[t0], to_units=pyo.units.gallon / pyo.units.hr + ) + + blk.capital_cost_constraint = pyo.Constraint( + expr=blk.capital_cost + == pyo.units.convert( + cost_blk.capital_a_parameter * x + cost_blk.capital_b_parameter, + to_units=blk.costing_package.base_currency, + ) + ) + if cost_electricity_flow: + blk.costing_package.cost_flow( + pyo.units.convert( + blk.unit_model.electricity_consumption[t0], + to_units=pyo.units.kW, + ), + "electricity", + ) + + +@register_costing_parameter_block( + build_rule=build_filter_plate_press_cost_param_block, + parameter_block_name="filter_plate_press", +) +def cost_filter_plate_press( + blk, dewatering_type=DewateringType.filter_plate_press, cost_electricity_flow=True +): + """ + Plate Press Filter costing method + """ + make_capital_cost_var(blk) + + cost_blk = blk.costing_package.filter_plate_press + t0 = blk.flowsheet().time.first() + x_units = pyo.units.gallon / pyo.units.hr + x = flow_in = pyo.units.convert(blk.unit_model.inlet.flow_vol[t0], to_units=x_units) + + blk.capital_cost_constraint = pyo.Constraint( + expr=blk.capital_cost + == pyo.units.convert( + cost_blk.capital_a_parameter + * x_units + * (x / x_units) ** cost_blk.capital_b_parameter, + to_units=blk.costing_package.base_currency, + ) + ) + + if cost_electricity_flow: + blk.costing_package.cost_flow( + pyo.units.convert( + blk.unit_model.electricity_consumption[t0], + to_units=pyo.units.kW, + ), + "electricity", + ) diff --git a/watertap/data/techno_economic/centrifuge.yaml b/watertap/data/techno_economic/centrifuge.yaml index 486c330b81..340e1b12bb 100644 --- a/watertap/data/techno_economic/centrifuge.yaml +++ b/watertap/data/techno_economic/centrifuge.yaml @@ -8,7 +8,7 @@ default: HRT: # Hydraulic retention time value: 0.5 units: hr - sizing_cost: + sizing_cost: # TODO: Update to 430892.24 based on cost curve values in watertap/unit_models/tests/test_dewatering_unit? 1 $/m3 is too unrealistic. value: 1 units: USD_2020/m^3 recovery_frac_mass_H2O: diff --git a/watertap/examples/flowsheets/case_studies/full_water_resource_recovery_facility/BSM2.py b/watertap/examples/flowsheets/case_studies/full_water_resource_recovery_facility/BSM2.py index c1439877c4..6435fc351c 100644 --- a/watertap/examples/flowsheets/case_studies/full_water_resource_recovery_facility/BSM2.py +++ b/watertap/examples/flowsheets/case_studies/full_water_resource_recovery_facility/BSM2.py @@ -371,6 +371,9 @@ def set_operating_conditions(m): m.fs.RADM.volume_vapor.fix(300) m.fs.RADM.liquid_outlet.temperature.fix(308.15) + # Dewatering Unit - fix either HRT or volume. + m.fs.DU.hydraulic_retention_time.fix(1800 * pyo.units.s) + def initialize_system(m): # Initialize flowsheet diff --git a/watertap/examples/flowsheets/case_studies/full_water_resource_recovery_facility/BSM2_ui.py b/watertap/examples/flowsheets/case_studies/full_water_resource_recovery_facility/BSM2_ui.py index 523c15d4b0..003beb816a 100644 --- a/watertap/examples/flowsheets/case_studies/full_water_resource_recovery_facility/BSM2_ui.py +++ b/watertap/examples/flowsheets/case_studies/full_water_resource_recovery_facility/BSM2_ui.py @@ -608,6 +608,7 @@ def export_variables(flowsheet=None, exports=None): is_output=False, ) + # TODO: uncomment and revise below once costing is merged # System costing # exports.add( # obj=fs.costing.utilization_factor, @@ -2858,6 +2859,7 @@ def build_flowsheet(): results = solve(m) assert_optimal_termination(results) + # TODO: incorporate costing when merged # add_costing(m) # assert_degrees_of_freedom(m, 0) # m.fs.costing.initialize() diff --git a/watertap/unit_models/dewatering.py b/watertap/unit_models/dewatering.py index 3fa1850781..034e160109 100644 --- a/watertap/unit_models/dewatering.py +++ b/watertap/unit_models/dewatering.py @@ -37,12 +37,15 @@ from pyomo.environ import ( Param, units as pyunits, + Var, + NonNegativeReals, ) from pyomo.common.config import ConfigValue, In from idaes.core.util.exceptions import ( ConfigurationError, ) +from watertap.costing.unit_models.dewatering import cost_dewatering __author__ = "Alejandro Garciadiego, Adam Atia" @@ -128,6 +131,51 @@ def build(self): doc="Percentage of suspended solids removed", ) + self.electricity_consumption = Var( + self.flowsheet().time, + units=pyunits.kW, + bounds=(0, None), + doc="Electricity consumption of unit", + ) + + # 0.026 kWh/m3 average between averages of belt and screw presses & centrifuge in relation to flow capacity + self.energy_electric_flow_vol_inlet = Param( + self.flowsheet().time, + units=pyunits.kWh / (pyunits.m**3), + initialize=0.026, + mutable=True, + doc="Specific electricity intensity of unit", + ) + + @self.Constraint(self.flowsheet().time, doc="Electricity consumption equation") + def eq_electricity_consumption(blk, t): + return blk.electricity_consumption[t] == pyunits.convert( + blk.energy_electric_flow_vol_inlet[t] * blk.inlet.flow_vol[t], + to_units=pyunits.kW, + ) + + self.hydraulic_retention_time = Var( + self.flowsheet().time, + initialize=1800, + domain=NonNegativeReals, + units=pyunits.s, + doc="Hydraulic retention time", + ) + self.volume = Var( + self.flowsheet().time, + initialize=1800, + domain=NonNegativeReals, + units=pyunits.m**3, + doc="Hydraulic retention time", + ) + + @self.Constraint(self.flowsheet().time, doc="Hydraulic retention time equation") + def eq_hydraulic_retention(blk, t): + return ( + self.hydraulic_retention_time[t] + == self.volume[t] / self.inlet.flow_vol[t] + ) + @self.Expression(self.flowsheet().time, doc="Suspended solid concentration") def TSS_in(blk, t): if blk.config.activated_sludge_model == ActivatedSludgeModelType.ASM1: @@ -177,10 +225,17 @@ def non_particulate_components(blk, t, i): def _get_performance_contents(self, time_point=0): var_dict = {} + param_dict = {} for k in self.split_fraction.keys(): if k[0] == time_point: var_dict[f"Split Fraction [{str(k[1:])}]"] = self.split_fraction[k] - return {"vars": var_dict} + var_dict["Electricity consumption"] = self.electricity_consumption[time_point] + param_dict[ + "Specific electricity consumption" + ] = self.energy_electric_flow_vol_inlet[time_point] + var_dict["Unit Volume"] = self.volume[time_point] + var_dict["Hydraulic Retention Time"] = self.hydraulic_retention_time[time_point] + return {"vars": var_dict, "params": param_dict} def _get_stream_table_contents(self, time_point=0): outlet_list = self.create_outlet_list() @@ -195,3 +250,7 @@ def _get_stream_table_contents(self, time_point=0): io_dict[o] = getattr(self, o + "_state") return create_stream_table_dataframe(io_dict, time_point=time_point) + + @property + def default_costing_method(self): + return cost_dewatering diff --git a/watertap/unit_models/tests/test_dewatering_unit.py b/watertap/unit_models/tests/test_dewatering_unit.py index 30738f09a7..45f3f14a01 100644 --- a/watertap/unit_models/tests/test_dewatering_unit.py +++ b/watertap/unit_models/tests/test_dewatering_unit.py @@ -18,6 +18,7 @@ ConcreteModel, value, assert_optimal_termination, + units as pyunits, ) from idaes.core import ( @@ -60,6 +61,15 @@ ModifiedASM2dParameterBlock, ) from pyomo.util.check_units import assert_units_consistent +from watertap.costing import WaterTAPCosting +from watertap.costing.unit_models.dewatering import ( + cost_dewatering, + cost_centrifuge, + DewateringType, +) +from idaes.core import UnitModelCostingBlock +from watertap.costing import WaterTAPCosting + __author__ = "Alejandro Garciadiego, Adam Atia" @@ -145,12 +155,15 @@ def du(self): m.fs.unit.inlet.conc_mass_comp[0, "X_ND"].fix(100.8668 * units.mg / units.liter) m.fs.unit.inlet.alkalinity.fix(97.8459 * units.mol / units.m**3) + m.fs.unit.hydraulic_retention_time.fix() + return m @pytest.mark.build @pytest.mark.unit def test_build(self, du): - + assert hasattr(du.fs.unit, "hydraulic_retention_time") + assert hasattr(du.fs.unit, "volume") assert hasattr(du.fs.unit, "inlet") assert len(du.fs.unit.inlet.vars) == 5 assert hasattr(du.fs.unit.inlet, "flow_vol") @@ -175,8 +188,8 @@ def test_build(self, du): assert hasattr(du.fs.unit.overflow, "pressure") assert hasattr(du.fs.unit.overflow, "alkalinity") - assert number_variables(du) == 76 - assert number_total_constraints(du) == 60 + assert number_variables(du) == 79 + assert number_total_constraints(du) == 62 assert number_unused_variables(du) == 0 @pytest.mark.unit @@ -191,7 +204,6 @@ def test_units(self, du): @pytest.mark.skipif(solver is None, reason="Solver not available") @pytest.mark.component def test_initialize(self, du): - iscale.calculate_scaling_factors(du) initialization_tester(du) @@ -247,6 +259,7 @@ def test_solution(self, du): assert pytest.approx(0.09784, rel=1e-3) == value( du.fs.unit.overflow.alkalinity[0] ) + assert pytest.approx(3.718, rel=1e-3) == value(du.fs.unit.volume[0]) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -327,6 +340,8 @@ def du_asm2d(self): ) m.fs.unit.inlet.alkalinity[0].fix(4.6663 * units.mmol / units.liter) + m.fs.unit.hydraulic_retention_time.fix() + return m @pytest.mark.unit @@ -395,6 +410,7 @@ def du_mod_asm2d(self): m.fs.unit.inlet.conc_mass_comp[0, "X_AUT"].fix( 118.3582 * units.mg / units.liter ) + m.fs.unit.hydraulic_retention_time.fix() return m @@ -421,3 +437,377 @@ def test_solve(self, du_mod_asm2d): solver = get_solver() results = solver.solve(du_mod_asm2d) assert_optimal_termination(results) + + +@pytest.mark.solver +@pytest.mark.skipif(solver is None, reason="Solver not available") +@pytest.mark.component +def test_du_default_costing(): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.props = ASM1ParameterBlock() + + m.fs.unit = DewateringUnit(property_package=m.fs.props) + + m.fs.unit.inlet.flow_vol.fix(178.4674 * units.m**3 / units.day) + m.fs.unit.inlet.temperature.fix(308.15 * units.K) + m.fs.unit.inlet.pressure.fix(1 * units.atm) + + m.fs.unit.inlet.conc_mass_comp[0, "S_I"].fix(130.867 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_S"].fix(258.5789 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_I"].fix(17216.2434 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_S"].fix(2611.4843 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_BH"].fix(1e-6 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_BA"].fix(1e-6 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_P"].fix(626.0652 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_O"].fix(1e-6 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_NO"].fix(1e-6 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_NH"].fix(1442.7882 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_ND"].fix(0.54323 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_ND"].fix(100.8668 * units.mg / units.liter) + m.fs.unit.inlet.alkalinity.fix(97.8459 * units.mol / units.m**3) + + m.fs.unit.hydraulic_retention_time.fix() + + m.fs.costing = WaterTAPCosting() + + m.fs.unit.costing = UnitModelCostingBlock( + flowsheet_costing_block=m.fs.costing, + ) + + m.fs.costing.cost_process() + + assert degrees_of_freedom(m) == 0 + + results = solver.solve(m) + + assert_optimal_termination(results) + + assert hasattr(m.fs.costing, "centrifuge") + assert m.fs.unit.default_costing_method is cost_dewatering + assert value(m.fs.costing.centrifuge.capital_a_parameter) == 328.03 + assert value(m.fs.costing.centrifuge.capital_b_parameter) == 751295 + + # Check solutions + assert pytest.approx(1964.42, rel=1e-5) == value( + pyunits.convert(m.fs.unit.inlet.flow_vol[0], to_units=pyunits.gal / pyunits.hr) + ) + assert pytest.approx(1602087.9, rel=1e-5) == value(m.fs.unit.costing.capital_cost) + assert pytest.approx(1602087.9, rel=1e-5) == value( + pyunits.convert( + (328.03 * 1964.42 + 751295) * pyunits.USD_2007, + to_units=m.fs.costing.base_currency, + ) + ) + + +@pytest.mark.solver +@pytest.mark.skipif(solver is None, reason="Solver not available") +@pytest.mark.component +def test_du_centrifuge_costing(): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.props = ASM1ParameterBlock() + + m.fs.unit = DewateringUnit(property_package=m.fs.props) + + m.fs.unit.inlet.flow_vol.fix(178.4674 * units.m**3 / units.day) + m.fs.unit.inlet.temperature.fix(308.15 * units.K) + m.fs.unit.inlet.pressure.fix(1 * units.atm) + + m.fs.unit.inlet.conc_mass_comp[0, "S_I"].fix(130.867 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_S"].fix(258.5789 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_I"].fix(17216.2434 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_S"].fix(2611.4843 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_BH"].fix(1e-6 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_BA"].fix(1e-6 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_P"].fix(626.0652 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_O"].fix(1e-6 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_NO"].fix(1e-6 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_NH"].fix(1442.7882 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_ND"].fix(0.54323 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_ND"].fix(100.8668 * units.mg / units.liter) + m.fs.unit.inlet.alkalinity.fix(97.8459 * units.mol / units.m**3) + + m.fs.unit.hydraulic_retention_time.fix() + + m.fs.costing = WaterTAPCosting() + + m.fs.unit.costing = UnitModelCostingBlock( + flowsheet_costing_block=m.fs.costing, + costing_method=cost_centrifuge, + ) + # Using average specific energy consumption of 0.069 for centrifuge as a function of capacity + m.fs.unit.energy_electric_flow_vol_inlet[0] = 0.069 * pyunits.kWh / pyunits.m**3 + m.fs.costing.cost_process() + + assert degrees_of_freedom(m) == 0 + + results = solver.solve(m) + + assert_optimal_termination(results) + + assert hasattr(m.fs.costing, "centrifuge") + assert value(m.fs.costing.centrifuge.capital_a_parameter) == 328.03 + assert value(m.fs.costing.centrifuge.capital_b_parameter) == 751295 + + # Check solutions + assert pytest.approx(1602087.9, rel=1e-5) == value(m.fs.unit.costing.capital_cost) + assert pytest.approx(1602087.9, rel=1e-5) == value( + pyunits.convert( + (328.03 * 1964.42 + 751295) * pyunits.USD_2007, + to_units=m.fs.costing.base_currency, + ) + ) + assert pytest.approx(7.4361 * 0.069, rel=1e-5) == value( + m.fs.unit.electricity_consumption[0] + ) + + +@pytest.mark.solver +@pytest.mark.skipif(solver is None, reason="Solver not available") +@pytest.mark.component +def test_du_centrifuge_costing2(): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.props = ASM1ParameterBlock() + + m.fs.unit = DewateringUnit(property_package=m.fs.props) + + m.fs.unit.inlet.flow_vol.fix(178.4674 * units.m**3 / units.day) + m.fs.unit.inlet.temperature.fix(308.15 * units.K) + m.fs.unit.inlet.pressure.fix(1 * units.atm) + + m.fs.unit.inlet.conc_mass_comp[0, "S_I"].fix(130.867 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_S"].fix(258.5789 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_I"].fix(17216.2434 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_S"].fix(2611.4843 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_BH"].fix(1e-6 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_BA"].fix(1e-6 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_P"].fix(626.0652 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_O"].fix(1e-6 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_NO"].fix(1e-6 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_NH"].fix(1442.7882 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_ND"].fix(0.54323 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_ND"].fix(100.8668 * units.mg / units.liter) + m.fs.unit.inlet.alkalinity.fix(97.8459 * units.mol / units.m**3) + + m.fs.unit.hydraulic_retention_time.fix() + + m.fs.costing = WaterTAPCosting() + + m.fs.unit.costing = UnitModelCostingBlock( + flowsheet_costing_block=m.fs.costing, + costing_method=cost_dewatering, + costing_method_arguments={ + "dewatering_type": DewateringType.centrifuge, + "cost_electricity_flow": False, + }, + ) + + m.fs.costing.cost_process() + + assert degrees_of_freedom(m) == 0 + + results = solver.solve(m) + + assert_optimal_termination(results) + + assert hasattr(m.fs.costing, "centrifuge") + assert value(m.fs.costing.centrifuge.capital_a_parameter) == 328.03 + assert value(m.fs.costing.centrifuge.capital_b_parameter) == 751295 + assert "electricity" not in m.fs.costing.aggregate_flow_costs.keys() + + # Check solutions + assert pytest.approx(1602087.9, rel=1e-5) == value(m.fs.unit.costing.capital_cost) + assert pytest.approx(1602087.9, rel=1e-5) == value( + pyunits.convert( + (328.03 * 1964.42 + 751295) * pyunits.USD_2007, + to_units=m.fs.costing.base_currency, + ) + ) + + +@pytest.mark.solver +@pytest.mark.skipif(solver is None, reason="Solver not available") +@pytest.mark.component +def test_du_filter_plate_press_costing(): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.props = ASM1ParameterBlock() + + m.fs.unit = DewateringUnit(property_package=m.fs.props) + + m.fs.unit.inlet.flow_vol.fix(178.4674 * units.m**3 / units.day) + m.fs.unit.inlet.temperature.fix(308.15 * units.K) + m.fs.unit.inlet.pressure.fix(1 * units.atm) + + m.fs.unit.inlet.conc_mass_comp[0, "S_I"].fix(130.867 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_S"].fix(258.5789 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_I"].fix(17216.2434 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_S"].fix(2611.4843 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_BH"].fix(1e-6 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_BA"].fix(1e-6 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_P"].fix(626.0652 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_O"].fix(1e-6 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_NO"].fix(1e-6 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_NH"].fix(1442.7882 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_ND"].fix(0.54323 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_ND"].fix(100.8668 * units.mg / units.liter) + m.fs.unit.inlet.alkalinity.fix(97.8459 * units.mol / units.m**3) + + m.fs.unit.hydraulic_retention_time.fix() + + m.fs.costing = WaterTAPCosting() + + m.fs.unit.costing = UnitModelCostingBlock( + flowsheet_costing_block=m.fs.costing, + costing_method=cost_dewatering, + costing_method_arguments={ + "dewatering_type": DewateringType.filter_plate_press, + "cost_electricity_flow": True, + }, + ) + # Using average specific energy consumption of 0.0039 for screw press as a function of capacity + m.fs.unit.energy_electric_flow_vol_inlet[0] = 0.0039 * pyunits.kWh / pyunits.m**3 + m.fs.costing.cost_process() + + assert degrees_of_freedom(m) == 0 + + results = solver.solve(m) + + assert_optimal_termination(results) + assert_units_consistent(m) + assert hasattr(m.fs.costing, "filter_plate_press") + assert value(m.fs.costing.filter_plate_press.capital_a_parameter) == 102794 + assert value(m.fs.costing.filter_plate_press.capital_b_parameter) == 0.4216 + + # Check solutions + assert pytest.approx(2885989.2, rel=1e-5) == value(m.fs.unit.costing.capital_cost) + assert pytest.approx(2885989.2, rel=1e-5) == value( + pyunits.convert( + (102794 * 1964.42**0.4216) * pyunits.USD_2007, + to_units=m.fs.costing.base_currency, + ) + ) + assert pytest.approx(7.4361 * 0.0039, rel=1e-5) == value( + m.fs.unit.electricity_consumption[0] + ) + + +@pytest.mark.solver +@pytest.mark.skipif(solver is None, reason="Solver not available") +@pytest.mark.component +def test_du_filter_belt_press_costing(): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.props = ASM1ParameterBlock() + + m.fs.unit = DewateringUnit(property_package=m.fs.props) + + m.fs.unit.inlet.flow_vol.fix(178.4674 * units.m**3 / units.day) + m.fs.unit.inlet.temperature.fix(308.15 * units.K) + m.fs.unit.inlet.pressure.fix(1 * units.atm) + + m.fs.unit.inlet.conc_mass_comp[0, "S_I"].fix(130.867 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_S"].fix(258.5789 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_I"].fix(17216.2434 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_S"].fix(2611.4843 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_BH"].fix(1e-6 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_BA"].fix(1e-6 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_P"].fix(626.0652 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_O"].fix(1e-6 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_NO"].fix(1e-6 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_NH"].fix(1442.7882 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_ND"].fix(0.54323 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_ND"].fix(100.8668 * units.mg / units.liter) + m.fs.unit.inlet.alkalinity.fix(97.8459 * units.mol / units.m**3) + + m.fs.unit.hydraulic_retention_time.fix() + + m.fs.costing = WaterTAPCosting() + + m.fs.unit.costing = UnitModelCostingBlock( + flowsheet_costing_block=m.fs.costing, + costing_method=cost_dewatering, + costing_method_arguments={ + "dewatering_type": DewateringType.filter_belt_press, + "cost_electricity_flow": True, + }, + ) + # Using average specific energy consumption of 0.006 for screw press as a function of capacity + m.fs.unit.energy_electric_flow_vol_inlet[0] = 0.006 * pyunits.kWh / pyunits.m**3 + m.fs.costing.cost_process() + + assert degrees_of_freedom(m) == 0 + + results = solver.solve(m) + + assert_optimal_termination(results) + assert_units_consistent(m) + assert hasattr(m.fs.costing, "filter_belt_press") + assert value(m.fs.costing.filter_belt_press.capital_a_parameter) == 146.29 + assert value(m.fs.costing.filter_belt_press.capital_b_parameter) == 433972 + + # Check solutions + assert pytest.approx(828025.2, rel=1e-5) == value(m.fs.unit.costing.capital_cost) + assert pytest.approx(828025.2, rel=1e-5) == value( + pyunits.convert( + (146.29 * 1964.42 + 433972) * pyunits.USD_2007, + to_units=m.fs.costing.base_currency, + ) + ) + assert pytest.approx(7.4361 * 0.006, rel=1e-5) == value( + m.fs.unit.electricity_consumption[0] + ) + + +@pytest.mark.solver +@pytest.mark.skipif(solver is None, reason="Solver not available") +@pytest.mark.component +def test_du_costing_config_err(): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.props = ASM1ParameterBlock() + + m.fs.unit = DewateringUnit(property_package=m.fs.props) + + m.fs.unit.inlet.flow_vol.fix(178.4674 * units.m**3 / units.day) + m.fs.unit.inlet.temperature.fix(308.15 * units.K) + m.fs.unit.inlet.pressure.fix(1 * units.atm) + + m.fs.unit.inlet.conc_mass_comp[0, "S_I"].fix(130.867 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_S"].fix(258.5789 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_I"].fix(17216.2434 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_S"].fix(2611.4843 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_BH"].fix(1e-6 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_BA"].fix(1e-6 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_P"].fix(626.0652 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_O"].fix(1e-6 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_NO"].fix(1e-6 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_NH"].fix(1442.7882 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "S_ND"].fix(0.54323 * units.mg / units.liter) + m.fs.unit.inlet.conc_mass_comp[0, "X_ND"].fix(100.8668 * units.mg / units.liter) + m.fs.unit.inlet.alkalinity.fix(97.8459 * units.mol / units.m**3) + + m.fs.unit.hydraulic_retention_time.fix() + + m.fs.costing = WaterTAPCosting() + + with pytest.raises( + ConfigurationError, + match="fs.unit received invalid argument for dewatering_type: foo. Argument must be a member of the DewateringType Enum class.", + ): + m.fs.unit.costing = UnitModelCostingBlock( + flowsheet_costing_block=m.fs.costing, + costing_method=cost_dewatering, + costing_method_arguments={ + "dewatering_type": "foo", + }, + )