From fd0d4f1b8980a07b6192a4d2d9f92f0785c271bd Mon Sep 17 00:00:00 2001 From: Mukta Hardikar Date: Mon, 21 Oct 2024 19:57:52 -0600 Subject: [PATCH 01/70] updates to reflo costing --- .../costing/watertap_reflo_costing_package.py | 120 ++++++++++++++++-- 1 file changed, 112 insertions(+), 8 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py index 8ada50da..ffdf480d 100644 --- a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py +++ b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py @@ -37,10 +37,13 @@ def build_global_params(self): self.heat_cost = pyo.Param( mutable=True, - initialize=0.01, + initialize=0.0, doc="Heat cost", units=pyo.units.USD_2018 / pyo.units.kWh, ) + + self.electricity_cost.fix(0.0) + self.register_flow_type("heat", self.heat_cost) self.plant_lifetime.fix(20) @@ -73,10 +76,90 @@ def build_global_params(self): self.base_currency = pyo.units.USD_2021 # Fix the parameters - self.fix_all_vars() + # self.fix_all_vars() self.plant_lifetime.fix(20) self.utilization_factor.fix(1) - self.electricity_cost.fix(0.0718) + self.electricity_cost.fix(0.0) + + self.electricity_cost_buy = pyo.Param( + mutable=True, + initialize=0.07, + doc="Electricity cost to buy", + units=pyo.units.USD_2018 / pyo.units.kWh, + ) + + self.electricity_cost_sell = pyo.Param( + mutable=True, + initialize=0.05, + doc="Electricity cost to sell", + units=pyo.units.USD_2018 / pyo.units.kWh, + ) + + self.heat_cost_buy = pyo.Param( + mutable=True, + initialize=0.07, + doc="Heat cost to buy", + units=pyo.units.USD_2018 / pyo.units.kWh, + ) + + self.heat_cost_sell = pyo.Param( + mutable=True, + initialize=0.05, + doc="Heat cost to sell", + units=pyo.units.USD_2018 / pyo.units.kWh, + ) + + # Heat balance of the system for sales and purchases of heat + treat_cost = self._get_treatment_cost_block() + en_cost = self._get_energy_cost_block() + + self.aggregate_flow_electricity_purchased = pyo.Var( + initialize=0, + domain=pyo.NonNegativeReals, + doc="Aggregated electricity consumed", + units=pyo.units.kW, + ) + + self.aggregate_flow_electricity_sold = pyo.Var( + initialize=0, + domain=pyo.NonNegativeReals, + doc="Aggregated electricity produced", + units=pyo.units.kW, + ) + + self.aggregate_flow_heat_purchased = pyo.Var( + initialize=0, + domain=pyo.NonNegativeReals, + doc="Aggregated heat consumed", + units=pyo.units.kW, + ) + + self.aggregate_flow_heat_sold = pyo.Var( + initialize=0, + domain=pyo.NonNegativeReals, + doc="Aggregated heat produced", + units=pyo.units.kW, + ) + + # energy producer's electricity flow is negative + self.aggregate_electricity_balance = pyo.Constraint( + expr=(self.aggregate_flow_electricity_purchased + -1 * en_cost.aggregate_flow_electricity + == treat_cost.aggregate_flow_electricity + self.aggregate_flow_electricity_sold) + ) + + self.aggregate_electricity_complement = pyo.Constraint( + expr=self.aggregate_flow_electricity_purchased * self.aggregate_flow_electricity_sold == 0 + ) + + # energy producer's heat flow is negative + self.aggregate_heat_balance = pyo.Constraint( + expr=(self.aggregate_flow_heat_purchased + -1 * en_cost.aggregate_flow_heat + == treat_cost.aggregate_flow_heat + self.aggregate_flow_heat_sold) + ) + + self.aggregate_heat_complement = pyo.Constraint( + expr=self.aggregate_flow_heat_purchased * self.aggregate_flow_heat_sold == 0 + ) # Build the integrated system costs self.build_integrated_costs() @@ -131,19 +214,40 @@ def build_integrated_costs(self): to_units=self.base_currency / self.base_period, ) ) + + # positive is for cost and negative for revenue + self.total_electric_operating_cost = pyo.Expression( + expr=(pyo.units.convert(self.aggregate_flow_electricity_purchased, to_units=pyo.units.kWh/pyo.units.year) * self.electricity_cost_buy + - pyo.units.convert(self.aggregate_flow_electricity_sold, to_units=pyo.units.kWh/pyo.units.year) * self.electricity_cost_sell) * self.utilization_factor + ) + + # positive is for cost and negative for revenue + self.total_heat_operating_cost = pyo.Expression( + expr=(pyo.units.convert(self.aggregate_flow_heat_purchased, to_units=pyo.units.kWh/pyo.units.year) * self.heat_cost_buy + - pyo.units.convert(self.aggregate_flow_heat_sold, to_units=pyo.units.kWh/pyo.units.year) * self.heat_cost_sell) * self.utilization_factor + ) + + # self.aggregate_flow_electricity_constraint = pyo.Constraint( + # expr=self.aggregate_flow_electricity + # == treat_cost.aggregate_flow_electricity + # + en_cost.aggregate_flow_electricity + # ) - self.aggregate_flow_electricity_constraint = pyo.Constraint( - expr=self.aggregate_flow_electricity - == treat_cost.aggregate_flow_electricity - + en_cost.aggregate_flow_electricity + # positive is for consumption + self.aggregate_flow_electricity = pyo.Expression( + expr=self.aggregate_flow_electricity_purchased - self.aggregate_flow_electricity_sold ) # if all("heat" in b.defined_flows for b in [treat_cost, en_cost]): if all(hasattr(b, "aggregate_flow_heat") for b in [treat_cost, en_cost]): self.aggregate_flow_heat_constraint = pyo.Constraint( expr=self.aggregate_flow_heat - == treat_cost.aggregate_flow_heat + en_cost.aggregate_flow_heat + == self.aggregate_flow_heat_purchased - self.aggregate_flow_heat_sold # treat_cost.aggregate_flow_heat + en_cost.aggregate_flow_heat ) + # self.aggregate_flow_heat = pyo.Expression( + # expr=self.aggregate_flow_heat_purchased - self.aggregate_flow_heat_sold + # ) + def add_LCOW(self, flow_rate, name="LCOW"): """ From a969c214e6f540675088959283c25dea7c8d1b97 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Tue, 22 Oct 2024 12:59:57 -0600 Subject: [PATCH 02/70] black --- .../costing/watertap_reflo_costing_package.py | 59 ++++++++++++++----- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py index ffdf480d..6481edae 100644 --- a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py +++ b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py @@ -143,18 +143,26 @@ def build_global_params(self): # energy producer's electricity flow is negative self.aggregate_electricity_balance = pyo.Constraint( - expr=(self.aggregate_flow_electricity_purchased + -1 * en_cost.aggregate_flow_electricity - == treat_cost.aggregate_flow_electricity + self.aggregate_flow_electricity_sold) + expr=( + self.aggregate_flow_electricity_purchased + + -1 * en_cost.aggregate_flow_electricity + == treat_cost.aggregate_flow_electricity + + self.aggregate_flow_electricity_sold + ) ) self.aggregate_electricity_complement = pyo.Constraint( - expr=self.aggregate_flow_electricity_purchased * self.aggregate_flow_electricity_sold == 0 + expr=self.aggregate_flow_electricity_purchased + * self.aggregate_flow_electricity_sold + == 0 ) # energy producer's heat flow is negative self.aggregate_heat_balance = pyo.Constraint( - expr=(self.aggregate_flow_heat_purchased + -1 * en_cost.aggregate_flow_heat - == treat_cost.aggregate_flow_heat + self.aggregate_flow_heat_sold) + expr=( + self.aggregate_flow_heat_purchased + -1 * en_cost.aggregate_flow_heat + == treat_cost.aggregate_flow_heat + self.aggregate_flow_heat_sold + ) ) self.aggregate_heat_complement = pyo.Constraint( @@ -214,17 +222,39 @@ def build_integrated_costs(self): to_units=self.base_currency / self.base_period, ) ) - + # positive is for cost and negative for revenue self.total_electric_operating_cost = pyo.Expression( - expr=(pyo.units.convert(self.aggregate_flow_electricity_purchased, to_units=pyo.units.kWh/pyo.units.year) * self.electricity_cost_buy - - pyo.units.convert(self.aggregate_flow_electricity_sold, to_units=pyo.units.kWh/pyo.units.year) * self.electricity_cost_sell) * self.utilization_factor + expr=( + pyo.units.convert( + self.aggregate_flow_electricity_purchased, + to_units=pyo.units.kWh / pyo.units.year, + ) + * self.electricity_cost_buy + - pyo.units.convert( + self.aggregate_flow_electricity_sold, + to_units=pyo.units.kWh / pyo.units.year, + ) + * self.electricity_cost_sell + ) + * self.utilization_factor ) # positive is for cost and negative for revenue self.total_heat_operating_cost = pyo.Expression( - expr=(pyo.units.convert(self.aggregate_flow_heat_purchased, to_units=pyo.units.kWh/pyo.units.year) * self.heat_cost_buy - - pyo.units.convert(self.aggregate_flow_heat_sold, to_units=pyo.units.kWh/pyo.units.year) * self.heat_cost_sell) * self.utilization_factor + expr=( + pyo.units.convert( + self.aggregate_flow_heat_purchased, + to_units=pyo.units.kWh / pyo.units.year, + ) + * self.heat_cost_buy + - pyo.units.convert( + self.aggregate_flow_heat_sold, + to_units=pyo.units.kWh / pyo.units.year, + ) + * self.heat_cost_sell + ) + * self.utilization_factor ) # self.aggregate_flow_electricity_constraint = pyo.Constraint( @@ -235,20 +265,21 @@ def build_integrated_costs(self): # positive is for consumption self.aggregate_flow_electricity = pyo.Expression( - expr=self.aggregate_flow_electricity_purchased - self.aggregate_flow_electricity_sold + expr=self.aggregate_flow_electricity_purchased + - self.aggregate_flow_electricity_sold ) # if all("heat" in b.defined_flows for b in [treat_cost, en_cost]): if all(hasattr(b, "aggregate_flow_heat") for b in [treat_cost, en_cost]): self.aggregate_flow_heat_constraint = pyo.Constraint( expr=self.aggregate_flow_heat - == self.aggregate_flow_heat_purchased - self.aggregate_flow_heat_sold # treat_cost.aggregate_flow_heat + en_cost.aggregate_flow_heat + == self.aggregate_flow_heat_purchased + - self.aggregate_flow_heat_sold # treat_cost.aggregate_flow_heat + en_cost.aggregate_flow_heat ) # self.aggregate_flow_heat = pyo.Expression( - # expr=self.aggregate_flow_heat_purchased - self.aggregate_flow_heat_sold + # expr=self.aggregate_flow_heat_purchased - self.aggregate_flow_heat_sold # ) - def add_LCOW(self, flow_rate, name="LCOW"): """ Add Levelized Cost of Water (LCOW) to costing block. From 2753c759b88faa9667b5afbb9e731a23072affe6 Mon Sep 17 00:00:00 2001 From: Mukta Hardikar Date: Thu, 24 Oct 2024 16:41:36 -0600 Subject: [PATCH 03/70] add frac_elec_from_grid --- .../costing/watertap_reflo_costing_package.py | 64 +++++++++++++------ 1 file changed, 43 insertions(+), 21 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py index 6481edae..ee074b9d 100644 --- a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py +++ b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py @@ -113,6 +113,21 @@ def build_global_params(self): treat_cost = self._get_treatment_cost_block() en_cost = self._get_energy_cost_block() + if all(hasattr(b, "aggregate_flow_heat") for b in [treat_cost, en_cost]): + self.frac_heat_from_grid = pyo.Var( + initialize=0, + domain=pyo.NonNegativeReals, + doc="Fraction of heat from grid", + units=pyo.units.dimensionless, + ) + + self.frac_elec_from_grid = pyo.Var( + initialize=0, + domain=pyo.NonNegativeReals, + doc="Fraction of heat from grid", + units=pyo.units.dimensionless, + ) + self.aggregate_flow_electricity_purchased = pyo.Var( initialize=0, domain=pyo.NonNegativeReals, @@ -151,23 +166,38 @@ def build_global_params(self): ) ) + self.frac_elec_from_grid_constraint = pyo.Constraint( + expr=( + self.frac_elec_from_grid + == self.aggregate_flow_electricity_purchased / treat_cost.aggregate_flow_electricity + ) + ) + self.aggregate_electricity_complement = pyo.Constraint( expr=self.aggregate_flow_electricity_purchased * self.aggregate_flow_electricity_sold == 0 ) - + + if all(hasattr(b, "aggregate_flow_heat") for b in [treat_cost, en_cost]): # energy producer's heat flow is negative - self.aggregate_heat_balance = pyo.Constraint( + self.aggregate_heat_balance = pyo.Constraint( + expr=( + self.aggregate_flow_heat_purchased + -1 * en_cost.aggregate_flow_heat + == treat_cost.aggregate_flow_heat + self.aggregate_flow_heat_sold + ) + ) + + self.frac_heat_from_grid_constraint = pyo.Constraint( expr=( - self.aggregate_flow_heat_purchased + -1 * en_cost.aggregate_flow_heat - == treat_cost.aggregate_flow_heat + self.aggregate_flow_heat_sold + self.frac_heat_from_grid + == self.aggregate_flow_heat_purchased / treat_cost.aggregate_flow_heat ) ) - self.aggregate_heat_complement = pyo.Constraint( - expr=self.aggregate_flow_heat_purchased * self.aggregate_flow_heat_sold == 0 - ) + self.aggregate_heat_complement = pyo.Constraint( + expr=self.aggregate_flow_heat_purchased * self.aggregate_flow_heat_sold == 0 + ) # Build the integrated system costs self.build_integrated_costs() @@ -218,7 +248,7 @@ def build_integrated_costs(self): self.total_operating_cost_constraint = pyo.Constraint( expr=self.total_operating_cost == pyo.units.convert( - treat_cost.total_operating_cost + en_cost.total_operating_cost, + treat_cost.total_operating_cost + en_cost.total_operating_cost + self.total_heat_operating_cost + self.total_electric_operating_cost, to_units=self.base_currency / self.base_period, ) ) @@ -237,7 +267,7 @@ def build_integrated_costs(self): ) * self.electricity_cost_sell ) - * self.utilization_factor + # * self.utilization_factor ) # positive is for cost and negative for revenue @@ -254,17 +284,11 @@ def build_integrated_costs(self): ) * self.heat_cost_sell ) - * self.utilization_factor + # * self.utilization_factor ) - # self.aggregate_flow_electricity_constraint = pyo.Constraint( - # expr=self.aggregate_flow_electricity - # == treat_cost.aggregate_flow_electricity - # + en_cost.aggregate_flow_electricity - # ) - # positive is for consumption - self.aggregate_flow_electricity = pyo.Expression( + self.aggregate_flow_electricity_constraint = pyo.Expression( expr=self.aggregate_flow_electricity_purchased - self.aggregate_flow_electricity_sold ) @@ -274,11 +298,9 @@ def build_integrated_costs(self): self.aggregate_flow_heat_constraint = pyo.Constraint( expr=self.aggregate_flow_heat == self.aggregate_flow_heat_purchased - - self.aggregate_flow_heat_sold # treat_cost.aggregate_flow_heat + en_cost.aggregate_flow_heat + - self.aggregate_flow_heat_sold ) - # self.aggregate_flow_heat = pyo.Expression( - # expr=self.aggregate_flow_heat_purchased - self.aggregate_flow_heat_sold - # ) + def add_LCOW(self, flow_rate, name="LCOW"): """ From cddf7979dfc3e7e6103d5910dade9640b8eeec4b Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 24 Oct 2024 18:43:04 -0400 Subject: [PATCH 04/70] add ability to load custom case study definition via yaml --- .../costing/watertap_reflo_costing_package.py | 44 +++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py index 6481edae..390e8600 100644 --- a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py +++ b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.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. @@ -10,6 +10,7 @@ # "https://github.com/watertap-org/watertap/" ################################################################################# +from pyomo.common.config import ConfigValue import pyomo.environ as pyo from idaes.core import declare_process_block_class @@ -18,14 +19,29 @@ WaterTAPCostingData, WaterTAPCostingBlockData, ) +from watertap.costing.zero_order_costing import _load_case_study_definition + from watertap_contrib.reflo.core import PySAMWaterTAP @declare_process_block_class("REFLOCosting") class REFLOCostingData(WaterTAPCostingData): + + CONFIG = WaterTAPCostingData.CONFIG() + CONFIG.declare( + "case_study_definition", + ConfigValue( + default=None, + doc="Path to YAML file defining global parameters for case study. If " + "not provided, WaterTAP-REFLO values are used.", + ), + ) + def build_global_params(self): + super().build_global_params() + # Override WaterTAP default value of USD_2018 self.base_currency = pyo.units.USD_2021 self.sales_tax_frac = pyo.Param( @@ -42,13 +58,35 @@ def build_global_params(self): units=pyo.units.USD_2018 / pyo.units.kWh, ) - self.electricity_cost.fix(0.0) - self.register_flow_type("heat", self.heat_cost) + self.electricity_cost.fix(0.0) self.plant_lifetime.fix(20) self.utilization_factor.fix(1) + # This should override default values + if self.config.case_study_definition is not None: + self.case_study_def = _load_case_study_definition(self) + # Register currency and conversion rates + if "currency_definitions" in self.case_study_def: + pyo.units.load_definitions_from_strings( + self._cs_def["currency_definitions"] + ) + # If currency definition is defined in case study yaml, + # we should be able to set it here. + if "base_currency" in self.case_study_def: + self.base_currency = getattr(pyo.units, self._cs_def["base_currency"]) + if "base_period" in self.case_study_def: + self.base_period = getattr(pyo.units, self._cs_def["base_period"]) + # Define expected flows + for f, v in self.case_study_def["defined_flows"].items(): + value = v["value"] + units = getattr(pyo.units, v["units"]) + if self.component(f + "_cost") is not None: + self.component(f + "_cost").fix(value * units) + else: + self.defined_flows[f] = value * units + @declare_process_block_class("TreatmentCosting") class TreatmentCostingData(REFLOCostingData): From 9dd25425dfa3bb6032b127e96a1eb4b2c3430823 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Fri, 25 Oct 2024 10:43:22 -0400 Subject: [PATCH 05/70] replace _cs_def --- .../reflo/costing/watertap_reflo_costing_package.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py index 8a9ca4b1..80dea288 100644 --- a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py +++ b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py @@ -70,14 +70,14 @@ def build_global_params(self): # Register currency and conversion rates if "currency_definitions" in self.case_study_def: pyo.units.load_definitions_from_strings( - self._cs_def["currency_definitions"] + self.case_study_def["currency_definitions"] ) # If currency definition is defined in case study yaml, # we should be able to set it here. if "base_currency" in self.case_study_def: - self.base_currency = getattr(pyo.units, self._cs_def["base_currency"]) + self.base_currency = getattr(pyo.units, self.case_study_def["base_currency"]) if "base_period" in self.case_study_def: - self.base_period = getattr(pyo.units, self._cs_def["base_period"]) + self.base_period = getattr(pyo.units, self.case_study_def["base_period"]) # Define expected flows for f, v in self.case_study_def["defined_flows"].items(): value = v["value"] From 7f2aa6f06f85c865bbdd3ec036a24b5b47425891 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Fri, 25 Oct 2024 10:43:38 -0400 Subject: [PATCH 06/70] black --- .../costing/watertap_reflo_costing_package.py | 41 +++++++++++-------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py index 80dea288..811d2674 100644 --- a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py +++ b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py @@ -75,9 +75,13 @@ def build_global_params(self): # If currency definition is defined in case study yaml, # we should be able to set it here. if "base_currency" in self.case_study_def: - self.base_currency = getattr(pyo.units, self.case_study_def["base_currency"]) + self.base_currency = getattr( + pyo.units, self.case_study_def["base_currency"] + ) if "base_period" in self.case_study_def: - self.base_period = getattr(pyo.units, self.case_study_def["base_period"]) + self.base_period = getattr( + pyo.units, self.case_study_def["base_period"] + ) # Define expected flows for f, v in self.case_study_def["defined_flows"].items(): value = v["value"] @@ -207,7 +211,8 @@ def build_global_params(self): self.frac_elec_from_grid_constraint = pyo.Constraint( expr=( self.frac_elec_from_grid - == self.aggregate_flow_electricity_purchased / treat_cost.aggregate_flow_electricity + == self.aggregate_flow_electricity_purchased + / treat_cost.aggregate_flow_electricity ) ) @@ -216,25 +221,28 @@ def build_global_params(self): * self.aggregate_flow_electricity_sold == 0 ) - + if all(hasattr(b, "aggregate_flow_heat") for b in [treat_cost, en_cost]): - # energy producer's heat flow is negative + # energy producer's heat flow is negative self.aggregate_heat_balance = pyo.Constraint( expr=( - self.aggregate_flow_heat_purchased + -1 * en_cost.aggregate_flow_heat + self.aggregate_flow_heat_purchased + + -1 * en_cost.aggregate_flow_heat == treat_cost.aggregate_flow_heat + self.aggregate_flow_heat_sold ) ) - + self.frac_heat_from_grid_constraint = pyo.Constraint( - expr=( - self.frac_heat_from_grid - == self.aggregate_flow_heat_purchased / treat_cost.aggregate_flow_heat + expr=( + self.frac_heat_from_grid + == self.aggregate_flow_heat_purchased + / treat_cost.aggregate_flow_heat + ) ) - ) self.aggregate_heat_complement = pyo.Constraint( - expr=self.aggregate_flow_heat_purchased * self.aggregate_flow_heat_sold == 0 + expr=self.aggregate_flow_heat_purchased * self.aggregate_flow_heat_sold + == 0 ) # Build the integrated system costs @@ -286,7 +294,10 @@ def build_integrated_costs(self): self.total_operating_cost_constraint = pyo.Constraint( expr=self.total_operating_cost == pyo.units.convert( - treat_cost.total_operating_cost + en_cost.total_operating_cost + self.total_heat_operating_cost + self.total_electric_operating_cost, + treat_cost.total_operating_cost + + en_cost.total_operating_cost + + self.total_heat_operating_cost + + self.total_electric_operating_cost, to_units=self.base_currency / self.base_period, ) ) @@ -335,11 +346,9 @@ def build_integrated_costs(self): if all(hasattr(b, "aggregate_flow_heat") for b in [treat_cost, en_cost]): self.aggregate_flow_heat_constraint = pyo.Constraint( expr=self.aggregate_flow_heat - == self.aggregate_flow_heat_purchased - - self.aggregate_flow_heat_sold + == self.aggregate_flow_heat_purchased - self.aggregate_flow_heat_sold ) - def add_LCOW(self, flow_rate, name="LCOW"): """ Add Levelized Cost of Water (LCOW) to costing block. From fafb2a701ae665a985366c5f0a83359f9f0421e1 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Sun, 27 Oct 2024 15:45:36 -0400 Subject: [PATCH 07/70] fix lt-med costing --- .../reflo/costing/units/lt_med_surrogate.py | 85 ++++++++++++------- .../unit_models/surrogate/lt_med_surrogate.py | 4 +- .../surrogate/tests/test_lt_med_surrogate.py | 23 +++-- 3 files changed, 71 insertions(+), 41 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/units/lt_med_surrogate.py b/src/watertap_contrib/reflo/costing/units/lt_med_surrogate.py index 97045d86..d851f4b8 100644 --- a/src/watertap_contrib/reflo/costing/units/lt_med_surrogate.py +++ b/src/watertap_contrib/reflo/costing/units/lt_med_surrogate.py @@ -37,14 +37,14 @@ def build_lt_med_surrogate_cost_param_block(blk): blk.cost_fraction_maintenance = pyo.Var( initialize=0.02, - units=pyo.units.dimensionless, + units=pyo.units.year**-1, bounds=(0, None), doc="Fraction of capital cost for maintenance", ) blk.cost_fraction_insurance = pyo.Var( initialize=0.005, - units=pyo.units.dimensionless, + units=pyo.units.year**-1, bounds=(0, None), doc="Fraction of capital cost for insurance", ) @@ -92,7 +92,7 @@ def build_lt_med_surrogate_cost_param_block(blk): blk.med_sys_A_coeff = pyo.Var( initialize=6291, - units=pyo.units.dimensionless, + units=pyo.units.USD_2018 / (pyo.units.m**3 / pyo.units.day), doc="LT-MED system specific capital A coeff", ) @@ -130,6 +130,7 @@ def cost_lt_med_surrogate(blk): dist = lt_med.distillate_props[0] brine = lt_med.brine_props[0] base_currency = blk.config.flowsheet_costing_block.base_currency + base_period = blk.config.flowsheet_costing_block.base_period blk.membrane_system_cost = pyo.Var( initialize=100, @@ -149,65 +150,89 @@ def cost_lt_med_surrogate(blk): initialize=100, bounds=(0, None), units=pyo.units.USD_2018 / (pyo.units.m**3 / pyo.units.day), + # units=pyo.units.USD_2018, doc="MED system cost per m3/day distillate", ) blk.capacity = pyo.units.convert( dist.flow_vol_phase["Liq"], to_units=pyo.units.m**3 / pyo.units.day ) + blk.capacity_dimensionless = pyo.units.convert( + blk.capacity * pyo.units.day * pyo.units.m**-3, to_units=pyo.units.dimensionless + ) blk.annual_dist_production = pyo.units.convert( dist.flow_vol_phase["Liq"], to_units=pyo.units.m**3 / pyo.units.year ) blk.med_specific_cost_constraint = pyo.Constraint( expr=blk.med_specific_cost - == (lt_med_params.med_sys_A_coeff * blk.capacity**lt_med_params.med_sys_B_coeff) + == pyo.units.convert( + ( + lt_med_params.med_sys_A_coeff + * blk.capacity_dimensionless**lt_med_params.med_sys_B_coeff + ), + to_units=pyo.units.USD_2018 / (pyo.units.m**3 / pyo.units.day), + ) ) blk.membrane_system_cost_constraint = pyo.Constraint( expr=blk.membrane_system_cost - == blk.capacity - * (blk.med_specific_cost * (1 - lt_med_params.cost_fraction_evaporator)) + == pyo.units.convert( + blk.capacity + * (blk.med_specific_cost * (1 - lt_med_params.cost_fraction_evaporator)), + to_units=base_currency, + ) ) blk.evaporator_system_cost_constraint = pyo.Constraint( expr=blk.evaporator_system_cost - == blk.capacity - * ( - blk.med_specific_cost + == pyo.units.convert( + blk.capacity * ( - lt_med_params.cost_fraction_evaporator + blk.med_specific_cost * ( - ( - lt_med.specific_area_per_kg_s - / lt_med_params.heat_exchanger_ref_area + lt_med_params.cost_fraction_evaporator + * ( + ( + lt_med.specific_area_per_kg_s + / lt_med_params.heat_exchanger_ref_area + ) + ** lt_med_params.heat_exchanger_exp ) - ** lt_med_params.heat_exchanger_exp ) - ) + ), + to_units=base_currency, ) ) + blk.costing_package.add_cost_factor(blk, None) blk.capital_cost_constraint = pyo.Constraint( - expr=blk.capital_cost == blk.membrane_system_cost + blk.evaporator_system_cost + expr=blk.capital_cost + == pyo.units.convert( + blk.membrane_system_cost + blk.evaporator_system_cost, + to_units=base_currency, + ) ) blk.fixed_operating_cost_constraint = pyo.Constraint( expr=blk.fixed_operating_cost - == blk.annual_dist_production - * ( - lt_med_params.cost_chemicals_per_vol_dist - + lt_med_params.cost_labor_per_vol_dist - + lt_med_params.cost_misc_per_vol_dist - ) - + blk.capital_cost - * ( - lt_med_params.cost_fraction_maintenance - + lt_med_params.cost_fraction_insurance - ) - + pyo.units.convert( - brine.flow_vol_phase["Liq"], to_units=pyo.units.m**3 / pyo.units.year + == pyo.units.convert( + blk.annual_dist_production + * ( + lt_med_params.cost_chemicals_per_vol_dist + + lt_med_params.cost_labor_per_vol_dist + + lt_med_params.cost_misc_per_vol_dist + ) + + blk.capital_cost + * ( + lt_med_params.cost_fraction_maintenance + + lt_med_params.cost_fraction_insurance + ) + + pyo.units.convert( + brine.flow_vol_phase["Liq"], to_units=pyo.units.m**3 / pyo.units.year + ) + * lt_med_params.cost_disposal_per_vol_brine, + to_units=base_currency / base_period, ) - * lt_med_params.cost_disposal_per_vol_brine ) blk.electricity_flow = pyo.Expression( diff --git a/src/watertap_contrib/reflo/unit_models/surrogate/lt_med_surrogate.py b/src/watertap_contrib/reflo/unit_models/surrogate/lt_med_surrogate.py index 2a141006..7302af8d 100644 --- a/src/watertap_contrib/reflo/unit_models/surrogate/lt_med_surrogate.py +++ b/src/watertap_contrib/reflo/unit_models/surrogate/lt_med_surrogate.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. @@ -328,7 +328,7 @@ def eq_feed_to_cooling_isobaric(b, t): self.specific_area_per_kg_s = Var( initialize=400, bounds=(0, None), - units=pyunits.m**2 / (pyunits.k / pyunits.s), + units=pyunits.m**2 / (pyunits.kg / pyunits.s), doc="Specific area (m2/kg/s))", ) diff --git a/src/watertap_contrib/reflo/unit_models/surrogate/tests/test_lt_med_surrogate.py b/src/watertap_contrib/reflo/unit_models/surrogate/tests/test_lt_med_surrogate.py index e01628c9..3e4bb13c 100644 --- a/src/watertap_contrib/reflo/unit_models/surrogate/tests/test_lt_med_surrogate.py +++ b/src/watertap_contrib/reflo/unit_models/surrogate/tests/test_lt_med_surrogate.py @@ -40,7 +40,7 @@ from watertap.property_models.water_prop_pack import WaterParameterBlock from watertap_contrib.reflo.unit_models.surrogate import LTMEDSurrogate -from watertap_contrib.reflo.costing import REFLOCosting +from watertap_contrib.reflo.costing import TreatmentCosting # Get default solver for testing solver = get_solver() @@ -271,7 +271,12 @@ def test_costing(self, LT_MED_frame): m = LT_MED_frame lt_med = m.fs.lt_med dist = lt_med.distillate_props[0] - m.fs.costing = REFLOCosting() + + m.fs.costing = TreatmentCosting() + # set heat and electricity costs to be non-zero + m.fs.costing.heat_cost.set_value(0.01) + m.fs.costing.electricity_cost.fix(0.07) + m.fs.costing.base_currency = pyunits.USD_2020 lt_med.costing = UnitModelCostingBlock(flowsheet_costing_block=m.fs.costing) @@ -294,24 +299,24 @@ def test_costing(self, LT_MED_frame): assert pytest.approx(2254.658, rel=1e-3) == value( m.fs.lt_med.costing.med_specific_cost ) - assert pytest.approx(4662455.768, rel=1e-3) == value( + assert pytest.approx(4609113.13, rel=1e-3) == value( m.fs.lt_med.costing.capital_cost ) - assert pytest.approx(2705589.357, rel=1e-3) == value( + assert pytest.approx(2674635.00, rel=1e-3) == value( m.fs.lt_med.costing.membrane_system_cost ) - assert pytest.approx(1956866.411, rel=1e-3) == value( + assert pytest.approx(1934478.12, rel=1e-3) == value( m.fs.lt_med.costing.evaporator_system_cost ) - assert pytest.approx(208604.394, rel=1e-3) == value( + assert pytest.approx(207270.82, rel=1e-3) == value( m.fs.lt_med.costing.fixed_operating_cost ) - assert pytest.approx(1.58295, rel=1e-3) == value(m.fs.costing.LCOW) - assert pytest.approx(748697.447, rel=1e-3) == value( + assert pytest.approx(1.57605, rel=1e-3) == value(m.fs.costing.LCOW) + assert pytest.approx(747363.88, rel=1e-3) == value( m.fs.costing.total_operating_cost ) - assert pytest.approx(4662455.768, rel=1e-3) == value( + assert pytest.approx(4609113.13, rel=1e-3) == value( m.fs.costing.total_capital_cost ) From 82474b9926ae7ee9e7ddff49eee5bc2243beb79b Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Sun, 27 Oct 2024 15:52:57 -0400 Subject: [PATCH 08/70] fix MED VAGMD semibatch class costing test --- .../ltmed_vagmd_semibatch/MED_VAGMD_semibatch_class.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/watertap_contrib/reflo/analysis/multiperiod/ltmed_vagmd_semibatch/MED_VAGMD_semibatch_class.py b/src/watertap_contrib/reflo/analysis/multiperiod/ltmed_vagmd_semibatch/MED_VAGMD_semibatch_class.py index fac867f6..13694972 100644 --- a/src/watertap_contrib/reflo/analysis/multiperiod/ltmed_vagmd_semibatch/MED_VAGMD_semibatch_class.py +++ b/src/watertap_contrib/reflo/analysis/multiperiod/ltmed_vagmd_semibatch/MED_VAGMD_semibatch_class.py @@ -500,6 +500,9 @@ def add_costing_packages(self): """ self.costing = TreatmentCosting() self.costing.base_currency = pyunits.USD_2020 + # set heat and electricity costs to be non-zero + self.costing.heat_cost.set_value(0.01) + self.costing.electricity_cost.fix(0.07) # The costing model is built upon the last time step blk = self.mp.get_active_process_blocks()[-1].fs From c96b0b7dd948d7e077b3e095aa8bed15d93a59d2 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Sun, 27 Oct 2024 15:53:13 -0400 Subject: [PATCH 09/70] fix VAGMD batch flowsheet multiperiod test --- .../test/test_VAGMD_batch_flowsheet_multiperiod.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/watertap_contrib/reflo/analysis/multiperiod/vagmd_batch/test/test_VAGMD_batch_flowsheet_multiperiod.py b/src/watertap_contrib/reflo/analysis/multiperiod/vagmd_batch/test/test_VAGMD_batch_flowsheet_multiperiod.py index b39b15ad..20faeb3c 100644 --- a/src/watertap_contrib/reflo/analysis/multiperiod/vagmd_batch/test/test_VAGMD_batch_flowsheet_multiperiod.py +++ b/src/watertap_contrib/reflo/analysis/multiperiod/vagmd_batch/test/test_VAGMD_batch_flowsheet_multiperiod.py @@ -28,7 +28,7 @@ from watertap.core.solvers import get_solver -from watertap_contrib.reflo.costing import REFLOCosting +from watertap_contrib.reflo.costing import TreatmentCosting from watertap_contrib.reflo.analysis.multiperiod.vagmd_batch.VAGMD_batch_multiperiod_unit_model import ( VAGMDbatchSurrogate, ) @@ -248,7 +248,10 @@ def test_costing(self, VAGMD_batch_frame_AS7C15L_Closed): # The costing model is built upon the last time step vagmd = m.fs.VAGMD.mp.get_active_process_blocks()[-1].fs.vagmd - m.fs.costing = REFLOCosting() + m.fs.costing = TreatmentCosting() + # set heat and electricity costs to be non-zero + m.fs.costing.heat_cost.set_value(0.01) + m.fs.costing.electricity_cost.fix(0.07) m.fs.costing.base_currency = pyunits.USD_2020 m.fs.VAGMD.add_costing_module(m.fs.costing) From 604bf6bb2a3f61b5d81b68d6310b6f8e1a0369ef Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Sun, 27 Oct 2024 15:53:25 -0400 Subject: [PATCH 10/70] fix trough surrogate test costing --- .../surrogate/trough/test_trough_surrogate.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/watertap_contrib/reflo/solar_models/surrogate/trough/test_trough_surrogate.py b/src/watertap_contrib/reflo/solar_models/surrogate/trough/test_trough_surrogate.py index 3055077e..db1cb103 100644 --- a/src/watertap_contrib/reflo/solar_models/surrogate/trough/test_trough_surrogate.py +++ b/src/watertap_contrib/reflo/solar_models/surrogate/trough/test_trough_surrogate.py @@ -26,10 +26,6 @@ ) from pyomo.network import Port -from watertap_contrib.reflo.solar_models.surrogate.trough import TroughSurrogate -from watertap_contrib.reflo.core import SolarEnergyBaseData -from watertap_contrib.reflo.costing import EnergyCosting - from idaes.core.surrogate.surrogate_block import SurrogateBlock from idaes.core import FlowsheetBlock, UnitModelCostingBlock from idaes.core.util.model_statistics import ( @@ -43,6 +39,10 @@ unscaled_variables_generator, ) +from watertap_contrib.reflo.solar_models.surrogate.trough import TroughSurrogate +from watertap_contrib.reflo.core import SolarEnergyBaseData +from watertap_contrib.reflo.costing import EnergyCosting + from watertap.core.solvers import get_solver # Get default solver for testing @@ -282,6 +282,9 @@ def test_costing(self, trough_frame): calculate_scaling_factors(m) m.fs.trough.initialize() m.fs.costing = EnergyCosting() + # set heat and electricity costs to be non-zero + m.fs.costing.heat_cost.set_value(0.01) + m.fs.costing.electricity_cost.fix(0.07) m.fs.trough.costing = UnitModelCostingBlock( flowsheet_costing_block=m.fs.costing ) From 00e1856717a54f3e691d03c6d503253de4743fb3 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Sun, 27 Oct 2024 15:59:45 -0400 Subject: [PATCH 11/70] fix flat plate physical test --- .../solar_models/zero_order/tests/test_flat_plate_physical.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/watertap_contrib/reflo/solar_models/zero_order/tests/test_flat_plate_physical.py b/src/watertap_contrib/reflo/solar_models/zero_order/tests/test_flat_plate_physical.py index 874065bb..851b9d51 100644 --- a/src/watertap_contrib/reflo/solar_models/zero_order/tests/test_flat_plate_physical.py +++ b/src/watertap_contrib/reflo/solar_models/zero_order/tests/test_flat_plate_physical.py @@ -197,6 +197,7 @@ def test_costing(self, flat_plate_frame): m.fs.test_flow = 0.01 * pyunits.Mgallons / pyunits.day m.fs.costing = EnergyCosting() + m.fs.costing.electricity_cost.fix(0.07) m.fs.costing.heat_cost.set_value(0) m.fs.flatplate.costing = UnitModelCostingBlock( flowsheet_costing_block=m.fs.costing From 952c772ff3c1b540fae700ed81c1d10f62545e72 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Sun, 27 Oct 2024 16:00:00 -0400 Subject: [PATCH 12/70] fix ltmed surrogate costing test --- .../reflo/unit_models/surrogate/tests/test_lt_med_surrogate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/watertap_contrib/reflo/unit_models/surrogate/tests/test_lt_med_surrogate.py b/src/watertap_contrib/reflo/unit_models/surrogate/tests/test_lt_med_surrogate.py index 3e4bb13c..3e935ead 100644 --- a/src/watertap_contrib/reflo/unit_models/surrogate/tests/test_lt_med_surrogate.py +++ b/src/watertap_contrib/reflo/unit_models/surrogate/tests/test_lt_med_surrogate.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. From 5e87303f8820cd421386fa58818a6a478e847804 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Sun, 27 Oct 2024 16:00:12 -0400 Subject: [PATCH 13/70] fix med-tvc costing test --- .../surrogate/tests/test_med_tvc_surrogate.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/surrogate/tests/test_med_tvc_surrogate.py b/src/watertap_contrib/reflo/unit_models/surrogate/tests/test_med_tvc_surrogate.py index 75424c9f..84ebafef 100644 --- a/src/watertap_contrib/reflo/unit_models/surrogate/tests/test_med_tvc_surrogate.py +++ b/src/watertap_contrib/reflo/unit_models/surrogate/tests/test_med_tvc_surrogate.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. @@ -41,7 +41,7 @@ from watertap.property_models.water_prop_pack import WaterParameterBlock from watertap_contrib.reflo.unit_models.surrogate import MEDTVCSurrogate -from watertap_contrib.reflo.costing import REFLOCosting +from watertap_contrib.reflo.costing import TreatmentCosting # ----------------------------------------------------------------------------- # Get default solver for testing @@ -311,7 +311,11 @@ def test_costing(self, MED_TVC_frame): m = MED_TVC_frame med_tvc = m.fs.med_tvc dist = med_tvc.distillate_props[0] - m.fs.costing = REFLOCosting() + m.fs.costing = TreatmentCosting() + # set heat and electricity costs to be non-zero + m.fs.costing.heat_cost.set_value(0.01) + m.fs.costing.electricity_cost.fix(0.07) + med_tvc.costing = UnitModelCostingBlock(flowsheet_costing_block=m.fs.costing) m.fs.costing.total_investment_factor.fix(1) From bcbd72ed3c22289efa76a5528c2e4289d5b4d1b7 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Sun, 27 Oct 2024 16:00:20 -0400 Subject: [PATCH 14/70] fix VAGMD surrogate test --- .../unit_models/surrogate/tests/test_vagmd_surrogate.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/watertap_contrib/reflo/unit_models/surrogate/tests/test_vagmd_surrogate.py b/src/watertap_contrib/reflo/unit_models/surrogate/tests/test_vagmd_surrogate.py index b96d5b37..fde951b7 100644 --- a/src/watertap_contrib/reflo/unit_models/surrogate/tests/test_vagmd_surrogate.py +++ b/src/watertap_contrib/reflo/unit_models/surrogate/tests/test_vagmd_surrogate.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. @@ -199,6 +199,9 @@ def test_costing(self, VAGMD_frame): vagmd = m.fs.vagmd m.fs.costing = TreatmentCosting() + # set heat and electricity costs to be non-zero + m.fs.costing.heat_cost.set_value(0.01) + m.fs.costing.electricity_cost.fix(0.07) m.fs.costing.base_currency = pyunits.USD_2020 vagmd.costing = UnitModelCostingBlock(flowsheet_costing_block=m.fs.costing) From cdd94eb04b5b489d67104e744553cc8b9986af9b Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Sun, 27 Oct 2024 16:00:26 -0400 Subject: [PATCH 15/70] fix air stripping test --- .../unit_models/tests/test_air_stripping_0D.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/tests/test_air_stripping_0D.py b/src/watertap_contrib/reflo/unit_models/tests/test_air_stripping_0D.py index b84148ad..6e134b12 100644 --- a/src/watertap_contrib/reflo/unit_models/tests/test_air_stripping_0D.py +++ b/src/watertap_contrib/reflo/unit_models/tests/test_air_stripping_0D.py @@ -46,7 +46,7 @@ from watertap.core.solvers import get_solver from watertap_contrib.reflo.property_models import AirWaterEq -from watertap_contrib.reflo.costing import REFLOCosting +from watertap_contrib.reflo.costing import TreatmentCosting from watertap_contrib.reflo.unit_models.air_stripping_0D import ( AirStripping0D, PackingMaterial, @@ -368,7 +368,11 @@ def test_costing1(self, ax_frame1): ax = m.fs.ax prop_out = ax.process_flow.properties_out[0] - m.fs.costing = REFLOCosting() + m.fs.costing = TreatmentCosting() + # set heat and electricity costs to be non-zero + m.fs.costing.heat_cost.set_value(0.01) + m.fs.costing.electricity_cost.fix(0.07) + ax.costing = UnitModelCostingBlock(flowsheet_costing_block=m.fs.costing) m.fs.costing.cost_process() m.fs.costing.add_LCOW(prop_out.flow_vol_phase["Liq"]) @@ -737,7 +741,11 @@ def test_costing2(self, ax_frame2): ax = m.fs.ax prop_out = ax.process_flow.properties_out[0] - m.fs.costing = REFLOCosting() + m.fs.costing = TreatmentCosting() + # set heat and electricity costs to be non-zero + m.fs.costing.heat_cost.set_value(0.01) + m.fs.costing.electricity_cost.fix(0.07) + ax.costing = UnitModelCostingBlock(flowsheet_costing_block=m.fs.costing) m.fs.costing.cost_process() m.fs.costing.add_LCOW(prop_out.flow_vol_phase["Liq"]) From 768e928717871ec2a376c918059dfc4653172dfb Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Sun, 27 Oct 2024 16:00:33 -0400 Subject: [PATCH 16/70] fix chem softening costing test --- .../unit_models/tests/test_chemical_softening.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/watertap_contrib/reflo/unit_models/tests/test_chemical_softening.py b/src/watertap_contrib/reflo/unit_models/tests/test_chemical_softening.py index f7be92b0..23831c9c 100644 --- a/src/watertap_contrib/reflo/unit_models/tests/test_chemical_softening.py +++ b/src/watertap_contrib/reflo/unit_models/tests/test_chemical_softening.py @@ -250,6 +250,9 @@ def test_costing(self, chem_soft_frame): m = chem_soft_frame prop_in = m.fs.soft.properties_in[0] m.fs.costing = TreatmentCosting() + # set heat and electricity costs to be non-zero + m.fs.costing.heat_cost.set_value(0.01) + m.fs.costing.electricity_cost.fix(0.07) m.fs.soft.costing = UnitModelCostingBlock(flowsheet_costing_block=m.fs.costing) m.fs.costing.cost_process() m.fs.costing.add_LCOW(prop_in.flow_vol) @@ -502,6 +505,9 @@ def test_costing(self, chem_soft_frame): prop_in = m.fs.soft.properties_in[0] m.fs.costing = TreatmentCosting() + # set heat and electricity costs to be non-zero + m.fs.costing.heat_cost.set_value(0.01) + m.fs.costing.electricity_cost.fix(0.07) m.fs.soft.costing = UnitModelCostingBlock(flowsheet_costing_block=m.fs.costing) m.fs.costing.cost_process() m.fs.costing.add_LCOW(prop_in.flow_vol) @@ -754,6 +760,9 @@ def test_costing(self, chem_soft_frame): m = chem_soft_frame prop_in = m.fs.soft.properties_in[0] m.fs.costing = TreatmentCosting() + # set heat and electricity costs to be non-zero + m.fs.costing.heat_cost.set_value(0.01) + m.fs.costing.electricity_cost.fix(0.07) m.fs.soft.costing = UnitModelCostingBlock(flowsheet_costing_block=m.fs.costing) m.fs.costing.cost_process() m.fs.costing.add_LCOW(prop_in.flow_vol) @@ -1009,6 +1018,9 @@ def test_costing(self, chem_soft_frame): m = chem_soft_frame prop_in = m.fs.soft.properties_in[0] m.fs.costing = TreatmentCosting() + # set heat and electricity costs to be non-zero + m.fs.costing.heat_cost.set_value(0.01) + m.fs.costing.electricity_cost.fix(0.07) m.fs.soft.costing = UnitModelCostingBlock(flowsheet_costing_block=m.fs.costing) m.fs.costing.cost_process() m.fs.costing.add_LCOW(prop_in.flow_vol) @@ -1265,6 +1277,9 @@ def test_costing(self, chem_soft_frame): m = chem_soft_frame prop_in = m.fs.soft.properties_in[0] m.fs.costing = TreatmentCosting() + # set heat and electricity costs to be non-zero + m.fs.costing.heat_cost.set_value(0.01) + m.fs.costing.electricity_cost.fix(0.07) m.fs.soft.costing = UnitModelCostingBlock(flowsheet_costing_block=m.fs.costing) m.fs.costing.cost_process() m.fs.costing.add_LCOW(prop_in.flow_vol) From 48feb7c56db2b06ddda2b4e9f54d22e83181c966 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Sun, 27 Oct 2024 16:00:41 -0400 Subject: [PATCH 17/70] fix cryst eff test costing --- .../reflo/unit_models/tests/test_crystallizer_effect.py | 3 +++ 1 file changed, 3 insertions(+) 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 83baade7..e39793f8 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 @@ -381,6 +381,9 @@ def test_solution(self, effect_frame): def test_costing(self, effect_frame): m = effect_frame m.fs.costing = TreatmentCosting() + # set heat and electricity costs to be non-zero + m.fs.costing.heat_cost.set_value(0.01) + m.fs.costing.electricity_cost.fix(0.07) m.fs.unit.costing = UnitModelCostingBlock( flowsheet_costing_block=m.fs.costing, ) From 1bfd9436c497f9ee06def581c626806e77c71b78 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Sun, 27 Oct 2024 16:00:47 -0400 Subject: [PATCH 18/70] fix DWI costing test --- .../unit_models/tests/test_deep_well_injection.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/watertap_contrib/reflo/unit_models/tests/test_deep_well_injection.py b/src/watertap_contrib/reflo/unit_models/tests/test_deep_well_injection.py index 3fca77ac..6986be4d 100644 --- a/src/watertap_contrib/reflo/unit_models/tests/test_deep_well_injection.py +++ b/src/watertap_contrib/reflo/unit_models/tests/test_deep_well_injection.py @@ -340,6 +340,9 @@ def test_solve_and_solution(self, dwi_frame): def test_costing(self, dwi_frame): m = dwi_frame m.fs.costing = TreatmentCosting() + # set heat and electricity costs to be non-zero + m.fs.costing.heat_cost.set_value(0.01) + m.fs.costing.electricity_cost.fix(0.07) # m.fs.costing.base_currency = pyunits.kUSD_2001 # for comparison to original BLM reference m.fs.unit.costing = UnitModelCostingBlock(flowsheet_costing_block=m.fs.costing) m.fs.costing.cost_process() @@ -413,6 +416,9 @@ def test_costing_as_capex(self, dwi_frame): m = dwi_frame m.fs.costing = TreatmentCosting() + # set heat and electricity costs to be non-zero + m.fs.costing.heat_cost.set_value(0.01) + m.fs.costing.electricity_cost.fix(0.07) m.fs.unit.costing = UnitModelCostingBlock( flowsheet_costing_block=m.fs.costing, costing_method_arguments={"cost_method": "as_capex"}, @@ -449,6 +455,9 @@ def test_costing_as_opex(self, dwi_frame): m = dwi_frame m.fs.costing = TreatmentCosting() + # set heat and electricity costs to be non-zero + m.fs.costing.heat_cost.set_value(0.01) + m.fs.costing.electricity_cost.fix(0.07) m.fs.unit.costing = UnitModelCostingBlock( flowsheet_costing_block=m.fs.costing, costing_method_arguments={"cost_method": "as_opex"}, @@ -561,6 +570,9 @@ def test_solve_and_solution(self, dwi_10000_frame): def test_costing(self, dwi_10000_frame): m = dwi_10000_frame m.fs.costing = TreatmentCosting() + # set heat and electricity costs to be non-zero + m.fs.costing.heat_cost.set_value(0.01) + m.fs.costing.electricity_cost.fix(0.07) # m.fs.costing.base_currency = pyunits.kUSD_2001 # for comparison to original BLM reference m.fs.unit.costing = UnitModelCostingBlock(flowsheet_costing_block=m.fs.costing) m.fs.costing.cost_process() From c15762e85aa600fa8fb445d85453660cba0b186e Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Sun, 27 Oct 2024 16:00:54 -0400 Subject: [PATCH 19/70] fix mec costing tests --- .../tests/test_multi_effect_crystallizer.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 8044c076..1b75e0c4 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 @@ -772,6 +772,9 @@ def test_solution(self, MEC2_frame): def test_costing(self, MEC2_frame): m = MEC2_frame m.fs.costing = TreatmentCosting() + # set heat and electricity costs to be non-zero + m.fs.costing.heat_cost.set_value(0.01) + m.fs.costing.electricity_cost.fix(0.07) # m.fs.costing.base_currency = pyunits.USD_2018 m.fs.unit.costing = UnitModelCostingBlock( flowsheet_costing_block=m.fs.costing, @@ -1311,6 +1314,9 @@ def test_solution(self, MEC3_frame): def test_costing(self, MEC3_frame): m = MEC3_frame m.fs.costing = TreatmentCosting() + # set heat and electricity costs to be non-zero + m.fs.costing.heat_cost.set_value(0.01) + m.fs.costing.electricity_cost.fix(0.07) # m.fs.costing.base_currency = pyunits.USD_2018 m.fs.unit.costing = UnitModelCostingBlock( flowsheet_costing_block=m.fs.costing, @@ -1899,6 +1905,9 @@ def test_solution(self, MEC4_frame): def test_costing(self, MEC4_frame): m = MEC4_frame m.fs.costing = TreatmentCosting() + # set heat and electricity costs to be non-zero + m.fs.costing.heat_cost.set_value(0.01) + m.fs.costing.electricity_cost.fix(0.07) m.fs.unit.costing = UnitModelCostingBlock( flowsheet_costing_block=m.fs.costing, ) @@ -1986,6 +1995,9 @@ def test_costing_by_volume(self): assert_optimal_termination(results) m.fs.costing = TreatmentCosting() + # set heat and electricity costs to be non-zero + m.fs.costing.heat_cost.set_value(0.01) + m.fs.costing.electricity_cost.fix(0.07) m.fs.unit.costing = UnitModelCostingBlock( flowsheet_costing_block=m.fs.costing, costing_method_arguments={"cost_type": "volume_basis"}, From 7e1bdcd92de1606a4d56cff93517040614ed397c Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Sun, 27 Oct 2024 16:01:45 -0400 Subject: [PATCH 20/70] black --- .../reflo/unit_models/tests/test_air_stripping_0D.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/watertap_contrib/reflo/unit_models/tests/test_air_stripping_0D.py b/src/watertap_contrib/reflo/unit_models/tests/test_air_stripping_0D.py index 6e134b12..9d0a0c76 100644 --- a/src/watertap_contrib/reflo/unit_models/tests/test_air_stripping_0D.py +++ b/src/watertap_contrib/reflo/unit_models/tests/test_air_stripping_0D.py @@ -745,7 +745,7 @@ def test_costing2(self, ax_frame2): # set heat and electricity costs to be non-zero m.fs.costing.heat_cost.set_value(0.01) m.fs.costing.electricity_cost.fix(0.07) - + ax.costing = UnitModelCostingBlock(flowsheet_costing_block=m.fs.costing) m.fs.costing.cost_process() m.fs.costing.add_LCOW(prop_out.flow_vol_phase["Liq"]) From 975a26b4c09a2aca789b0d619d62ce387215243d Mon Sep 17 00:00:00 2001 From: Mukta Hardikar Date: Thu, 7 Nov 2024 09:06:10 -0700 Subject: [PATCH 21/70] updating grid_frac equation for more stability. updated expressions to be constraints and added relevant vars --- .../costing/watertap_reflo_costing_package.py | 41 ++++++++++++++----- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py index 811d2674..3b243565 100644 --- a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py +++ b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py @@ -235,8 +235,12 @@ def build_global_params(self): self.frac_heat_from_grid_constraint = pyo.Constraint( expr=( self.frac_heat_from_grid - == self.aggregate_flow_heat_purchased - / treat_cost.aggregate_flow_heat + == 1 + - ( + -1 + * en_cost.aggregate_flow_heat + / treat_cost.aggregate_flow_heat + ) ) ) @@ -274,6 +278,20 @@ def build_integrated_costs(self): units=pyo.units.kW, ) + self.total_electric_operating_cost = pyo.Var( + initialize=1e3, + # domain=pyo.NonNegativeReals, + doc="Total electricity related operating cost", + units=self.base_currency / self.base_period, + ) + + self.total_heat_operating_cost = pyo.Var( + initialize=1e3, + # domain=pyo.NonNegativeReals, + doc="Total heat related operating cost", + units=self.base_currency / self.base_period, + ) + # if all("heat" in b.defined_flows for b in [treat_cost, en_cost]): if all(hasattr(b, "aggregate_flow_heat") for b in [treat_cost, en_cost]): self.aggregate_flow_heat = pyo.Var( @@ -296,15 +314,16 @@ def build_integrated_costs(self): == pyo.units.convert( treat_cost.total_operating_cost + en_cost.total_operating_cost - + self.total_heat_operating_cost - + self.total_electric_operating_cost, + + self.total_electric_operating_cost + + self.total_heat_operating_cost, to_units=self.base_currency / self.base_period, ) ) # positive is for cost and negative for revenue - self.total_electric_operating_cost = pyo.Expression( - expr=( + self.total_electric_operating_cost_constraint = pyo.Constraint( + expr=self.total_electric_operating_cost + == ( pyo.units.convert( self.aggregate_flow_electricity_purchased, to_units=pyo.units.kWh / pyo.units.year, @@ -320,8 +339,9 @@ def build_integrated_costs(self): ) # positive is for cost and negative for revenue - self.total_heat_operating_cost = pyo.Expression( - expr=( + self.total_heat_operating_cost_constraint = pyo.Constraint( + expr=self.total_heat_operating_cost + == ( pyo.units.convert( self.aggregate_flow_heat_purchased, to_units=pyo.units.kWh / pyo.units.year, @@ -337,8 +357,9 @@ def build_integrated_costs(self): ) # positive is for consumption - self.aggregate_flow_electricity_constraint = pyo.Expression( - expr=self.aggregate_flow_electricity_purchased + self.aggregate_flow_electricity_constraint = pyo.Constraint( + expr=self.aggregate_flow_electricity + == self.aggregate_flow_electricity_purchased - self.aggregate_flow_electricity_sold ) From b43a3573aa0e006c65721c78b2544deb026f0d1a Mon Sep 17 00:00:00 2001 From: Mukta Hardikar Date: Thu, 7 Nov 2024 09:18:48 -0700 Subject: [PATCH 22/70] updating electricity grid fraction --- .../reflo/costing/watertap_reflo_costing_package.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py index 3b243565..74d30bfc 100644 --- a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py +++ b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py @@ -211,8 +211,12 @@ def build_global_params(self): self.frac_elec_from_grid_constraint = pyo.Constraint( expr=( self.frac_elec_from_grid - == self.aggregate_flow_electricity_purchased - / treat_cost.aggregate_flow_electricity + == 1 + - ( + -1 + * en_cost.aggregate_flow_electricity + / treat_cost.aggregate_flow_electricity + ) ) ) From 68ff1d19492e15fe5602d0f232ad65e42ace3930 Mon Sep 17 00:00:00 2001 From: Mukta Hardikar Date: Fri, 8 Nov 2024 00:53:43 -0700 Subject: [PATCH 23/70] updated grid_frac_elec equation to check for PV --- .../costing/watertap_reflo_costing_package.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py index 74d30bfc..11aa6f03 100644 --- a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py +++ b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py @@ -208,17 +208,18 @@ def build_global_params(self): ) ) - self.frac_elec_from_grid_constraint = pyo.Constraint( - expr=( - self.frac_elec_from_grid - == 1 - - ( - -1 - * en_cost.aggregate_flow_electricity - / treat_cost.aggregate_flow_electricity + for b in self.model().component_objects(pyo.Block): + if str(b) == "fs.energy.pv": + self.frac_elec_from_grid_constraint = pyo.Constraint( + expr=( + self.frac_elec_from_grid + == 1 - ( + b.electricity + / (b.electricity + self.aggregate_flow_electricity_purchased) + ) + ) ) - ) - ) + self.aggregate_electricity_complement = pyo.Constraint( expr=self.aggregate_flow_electricity_purchased From 51b8d2d8b809159f507b1f2cd259e9830e20a4b8 Mon Sep 17 00:00:00 2001 From: Mukta Hardikar Date: Sun, 10 Nov 2024 19:46:43 -0700 Subject: [PATCH 24/70] run black and updated __init__ --- .../reflo/analysis/case_studies/KBHDP/__init__.py | 1 - .../reflo/costing/watertap_reflo_costing_package.py | 12 +++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/watertap_contrib/reflo/analysis/case_studies/KBHDP/__init__.py b/src/watertap_contrib/reflo/analysis/case_studies/KBHDP/__init__.py index 03cdaaa5..dfc3c948 100644 --- a/src/watertap_contrib/reflo/analysis/case_studies/KBHDP/__init__.py +++ b/src/watertap_contrib/reflo/analysis/case_studies/KBHDP/__init__.py @@ -1,2 +1 @@ from .components import * -from .KBHDP_SOA import * diff --git a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py index 11aa6f03..7802ecaf 100644 --- a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py +++ b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py @@ -208,18 +208,22 @@ def build_global_params(self): ) ) + # Calculate fraction of electricity from grid when PV is included for b in self.model().component_objects(pyo.Block): if str(b) == "fs.energy.pv": self.frac_elec_from_grid_constraint = pyo.Constraint( expr=( self.frac_elec_from_grid - == 1 - ( + == 1 + - ( b.electricity - / (b.electricity + self.aggregate_flow_electricity_purchased) + / ( + b.electricity + + self.aggregate_flow_electricity_purchased + ) ) ) ) - self.aggregate_electricity_complement = pyo.Constraint( expr=self.aggregate_flow_electricity_purchased @@ -340,7 +344,6 @@ def build_integrated_costs(self): ) * self.electricity_cost_sell ) - # * self.utilization_factor ) # positive is for cost and negative for revenue @@ -358,7 +361,6 @@ def build_integrated_costs(self): ) * self.heat_cost_sell ) - # * self.utilization_factor ) # positive is for consumption From 5f9ef79a29387c4e7277ecdf37a7dc2bee9f8fc0 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Mon, 11 Nov 2024 16:45:47 -0700 Subject: [PATCH 25/70] trigger tests --- .../reflo/costing/watertap_reflo_costing_package.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py index 7802ecaf..1fd9e1da 100644 --- a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py +++ b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py @@ -39,6 +39,7 @@ class REFLOCostingData(WaterTAPCostingData): def build_global_params(self): + super().build_global_params() # Override WaterTAP default value of USD_2018 From 0adc7db0024d13c5b31e526cb98225ea11c9ea12 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Mon, 11 Nov 2024 16:47:04 -0700 Subject: [PATCH 26/70] black trigger tests --- .../reflo/costing/watertap_reflo_costing_package.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py index 1fd9e1da..7802ecaf 100644 --- a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py +++ b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py @@ -39,7 +39,6 @@ class REFLOCostingData(WaterTAPCostingData): def build_global_params(self): - super().build_global_params() # Override WaterTAP default value of USD_2018 From f5e2b058dd77c2e9305d98f412bc3fcd843071ee Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Tue, 12 Nov 2024 11:25:06 -0700 Subject: [PATCH 27/70] move electricity/heat balances to build_integrated_costs --- .../costing/watertap_reflo_costing_package.py | 145 +++++++++--------- 1 file changed, 70 insertions(+), 75 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py index 7802ecaf..bd7fceaa 100644 --- a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py +++ b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py @@ -118,7 +118,6 @@ def build_global_params(self): self.base_currency = pyo.units.USD_2021 # Fix the parameters - # self.fix_all_vars() self.plant_lifetime.fix(20) self.utilization_factor.fix(1) self.electricity_cost.fix(0.0) @@ -151,14 +150,81 @@ def build_global_params(self): units=pyo.units.USD_2018 / pyo.units.kWh, ) - # Heat balance of the system for sales and purchases of heat + # Build the integrated system costs + self.build_integrated_costs() + + def build_process_costs(self): + pass + + def build_integrated_costs(self): treat_cost = self._get_treatment_cost_block() en_cost = self._get_energy_cost_block() + self.total_capital_cost = pyo.Var( + initialize=1e3, + # domain=pyo.NonNegativeReals, + doc="Total capital cost for integrated system", + units=self.base_currency, + ) + self.total_operating_cost = pyo.Var( + initialize=1e3, + # domain=pyo.NonNegativeReals, + doc="Total operating cost for integrated system", + units=self.base_currency / self.base_period, + ) + self.aggregate_flow_electricity = pyo.Var( + initialize=1e3, + # domain=pyo.NonNegativeReals, + doc="Aggregated electricity flow", + units=pyo.units.kW, + ) + + self.total_electric_operating_cost = pyo.Var( + initialize=1e3, + # domain=pyo.NonNegativeReals, + doc="Total electricity related operating cost", + units=self.base_currency / self.base_period, + ) + + self.total_heat_operating_cost = pyo.Var( + initialize=1e3, + # domain=pyo.NonNegativeReals, + doc="Total heat related operating cost", + units=self.base_currency / self.base_period, + ) + + if all(hasattr(b, "aggregate_flow_heat") for b in [treat_cost, en_cost]): + self.aggregate_flow_heat = pyo.Var( + initialize=1e3, + # domain=pyo.NonNegativeReals, + doc="Aggregated heat flow", + units=pyo.units.kW, + ) + + self.total_capital_cost_constraint = pyo.Constraint( + expr=self.total_capital_cost + == pyo.units.convert( + treat_cost.total_capital_cost + en_cost.total_capital_cost, + to_units=self.base_currency, + ) + ) + + self.total_operating_cost_constraint = pyo.Constraint( + expr=self.total_operating_cost + == pyo.units.convert( + treat_cost.total_operating_cost + + en_cost.total_operating_cost + + self.total_electric_operating_cost + + self.total_heat_operating_cost, + to_units=self.base_currency / self.base_period, + ) + ) + if all(hasattr(b, "aggregate_flow_heat") for b in [treat_cost, en_cost]): self.frac_heat_from_grid = pyo.Var( initialize=0, domain=pyo.NonNegativeReals, + bounds=(0, 1.00001), doc="Fraction of heat from grid", units=pyo.units.dimensionless, ) @@ -166,7 +232,8 @@ def build_global_params(self): self.frac_elec_from_grid = pyo.Var( initialize=0, domain=pyo.NonNegativeReals, - doc="Fraction of heat from grid", + bounds=(0, 1.00001), + doc="Fraction of electricity from grid", units=pyo.units.dimensionless, ) @@ -258,77 +325,6 @@ def build_global_params(self): == 0 ) - # Build the integrated system costs - self.build_integrated_costs() - - def build_process_costs(self): - pass - - def build_integrated_costs(self): - treat_cost = self._get_treatment_cost_block() - en_cost = self._get_energy_cost_block() - - self.total_capital_cost = pyo.Var( - initialize=1e3, - # domain=pyo.NonNegativeReals, - doc="Total capital cost for integrated system", - units=self.base_currency, - ) - self.total_operating_cost = pyo.Var( - initialize=1e3, - # domain=pyo.NonNegativeReals, - doc="Total operating cost for integrated system", - units=self.base_currency / self.base_period, - ) - self.aggregate_flow_electricity = pyo.Var( - initialize=1e3, - # domain=pyo.NonNegativeReals, - doc="Aggregated electricity flow", - units=pyo.units.kW, - ) - - self.total_electric_operating_cost = pyo.Var( - initialize=1e3, - # domain=pyo.NonNegativeReals, - doc="Total electricity related operating cost", - units=self.base_currency / self.base_period, - ) - - self.total_heat_operating_cost = pyo.Var( - initialize=1e3, - # domain=pyo.NonNegativeReals, - doc="Total heat related operating cost", - units=self.base_currency / self.base_period, - ) - - # if all("heat" in b.defined_flows for b in [treat_cost, en_cost]): - if all(hasattr(b, "aggregate_flow_heat") for b in [treat_cost, en_cost]): - self.aggregate_flow_heat = pyo.Var( - initialize=1e3, - # domain=pyo.NonNegativeReals, - doc="Aggregated heat flow", - units=pyo.units.kW, - ) - - self.total_capital_cost_constraint = pyo.Constraint( - expr=self.total_capital_cost - == pyo.units.convert( - treat_cost.total_capital_cost + en_cost.total_capital_cost, - to_units=self.base_currency, - ) - ) - - self.total_operating_cost_constraint = pyo.Constraint( - expr=self.total_operating_cost - == pyo.units.convert( - treat_cost.total_operating_cost - + en_cost.total_operating_cost - + self.total_electric_operating_cost - + self.total_heat_operating_cost, - to_units=self.base_currency / self.base_period, - ) - ) - # positive is for cost and negative for revenue self.total_electric_operating_cost_constraint = pyo.Constraint( expr=self.total_electric_operating_cost @@ -370,7 +366,6 @@ def build_integrated_costs(self): - self.aggregate_flow_electricity_sold ) - # if all("heat" in b.defined_flows for b in [treat_cost, en_cost]): if all(hasattr(b, "aggregate_flow_heat") for b in [treat_cost, en_cost]): self.aggregate_flow_heat_constraint = pyo.Constraint( expr=self.aggregate_flow_heat From 34f7e21c2934787c8f2852c099e0972b49c3d50b Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Tue, 12 Nov 2024 12:58:19 -0700 Subject: [PATCH 28/70] add check for electricity and heat in used_flows to system costing; heat cost a var --- .../costing/watertap_reflo_costing_package.py | 122 ++++++++++-------- 1 file changed, 69 insertions(+), 53 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py index bd7fceaa..45948b9a 100644 --- a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py +++ b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py @@ -51,15 +51,15 @@ def build_global_params(self): units=pyo.units.dimensionless, ) - self.heat_cost = pyo.Param( - mutable=True, + self.heat_cost = pyo.Var( initialize=0.0, doc="Heat cost", units=pyo.units.USD_2018 / pyo.units.kWh, ) - self.register_flow_type("heat", self.heat_cost) + self.defined_flows["heat"] = self.heat_cost + self.heat_cost.fix(0.0) self.electricity_cost.fix(0.0) self.plant_lifetime.fix(20) self.utilization_factor.fix(1) @@ -112,15 +112,16 @@ def build_process_costs(self): @declare_process_block_class("REFLOSystemCosting") class REFLOSystemCostingData(WaterTAPCostingBlockData): + def build_global_params(self): super().build_global_params() self.base_currency = pyo.units.USD_2021 # Fix the parameters + self.electricity_cost.fix(0.0) self.plant_lifetime.fix(20) self.utilization_factor.fix(1) - self.electricity_cost.fix(0.0) self.electricity_cost_buy = pyo.Param( mutable=True, @@ -128,6 +129,7 @@ def build_global_params(self): doc="Electricity cost to buy", units=pyo.units.USD_2018 / pyo.units.kWh, ) + self.defined_flows["electricity_buy"] = self.electricity_cost_buy self.electricity_cost_sell = pyo.Param( mutable=True, @@ -135,6 +137,7 @@ def build_global_params(self): doc="Electricity cost to sell", units=pyo.units.USD_2018 / pyo.units.kWh, ) + self.defined_flows["electricity_sell"] = self.electricity_cost_sell self.heat_cost_buy = pyo.Param( mutable=True, @@ -142,6 +145,7 @@ def build_global_params(self): doc="Heat cost to buy", units=pyo.units.USD_2018 / pyo.units.kWh, ) + self.defined_flows["heat_buy"] = self.heat_cost_buy self.heat_cost_sell = pyo.Param( mutable=True, @@ -149,6 +153,7 @@ def build_global_params(self): doc="Heat cost to sell", units=pyo.units.USD_2018 / pyo.units.kWh, ) + self.defined_flows["heat_sell"] = self.heat_cost_sell # Build the integrated system costs self.build_integrated_costs() @@ -157,46 +162,44 @@ def build_process_costs(self): pass def build_integrated_costs(self): + treat_cost = self._get_treatment_cost_block() - en_cost = self._get_energy_cost_block() + energy_cost = self._get_energy_cost_block() self.total_capital_cost = pyo.Var( initialize=1e3, - # domain=pyo.NonNegativeReals, + domain=pyo.NonNegativeReals, doc="Total capital cost for integrated system", units=self.base_currency, ) + self.total_operating_cost = pyo.Var( initialize=1e3, - # domain=pyo.NonNegativeReals, doc="Total operating cost for integrated system", units=self.base_currency / self.base_period, ) + self.aggregate_flow_electricity = pyo.Var( initialize=1e3, - # domain=pyo.NonNegativeReals, doc="Aggregated electricity flow", units=pyo.units.kW, ) self.total_electric_operating_cost = pyo.Var( initialize=1e3, - # domain=pyo.NonNegativeReals, doc="Total electricity related operating cost", units=self.base_currency / self.base_period, ) self.total_heat_operating_cost = pyo.Var( initialize=1e3, - # domain=pyo.NonNegativeReals, doc="Total heat related operating cost", units=self.base_currency / self.base_period, ) - if all(hasattr(b, "aggregate_flow_heat") for b in [treat_cost, en_cost]): + if all(hasattr(b, "aggregate_flow_heat") for b in [treat_cost, energy_cost]): self.aggregate_flow_heat = pyo.Var( initialize=1e3, - # domain=pyo.NonNegativeReals, doc="Aggregated heat flow", units=pyo.units.kW, ) @@ -204,7 +207,7 @@ def build_integrated_costs(self): self.total_capital_cost_constraint = pyo.Constraint( expr=self.total_capital_cost == pyo.units.convert( - treat_cost.total_capital_cost + en_cost.total_capital_cost, + treat_cost.total_capital_cost + energy_cost.total_capital_cost, to_units=self.base_currency, ) ) @@ -213,14 +216,14 @@ def build_integrated_costs(self): expr=self.total_operating_cost == pyo.units.convert( treat_cost.total_operating_cost - + en_cost.total_operating_cost + + energy_cost.total_operating_cost + self.total_electric_operating_cost + self.total_heat_operating_cost, to_units=self.base_currency / self.base_period, ) ) - if all(hasattr(b, "aggregate_flow_heat") for b in [treat_cost, en_cost]): + if all(hasattr(b, "aggregate_flow_heat") for b in [treat_cost, energy_cost]): self.frac_heat_from_grid = pyo.Var( initialize=0, domain=pyo.NonNegativeReals, @@ -269,7 +272,7 @@ def build_integrated_costs(self): self.aggregate_electricity_balance = pyo.Constraint( expr=( self.aggregate_flow_electricity_purchased - + -1 * en_cost.aggregate_flow_electricity + + -1 * energy_cost.aggregate_flow_electricity == treat_cost.aggregate_flow_electricity + self.aggregate_flow_electricity_sold ) @@ -298,12 +301,12 @@ def build_integrated_costs(self): == 0 ) - if all(hasattr(b, "aggregate_flow_heat") for b in [treat_cost, en_cost]): + if all(hasattr(b, "aggregate_flow_heat") for b in [treat_cost, energy_cost]): # energy producer's heat flow is negative self.aggregate_heat_balance = pyo.Constraint( expr=( self.aggregate_flow_heat_purchased - + -1 * en_cost.aggregate_flow_heat + + -1 * energy_cost.aggregate_flow_heat == treat_cost.aggregate_flow_heat + self.aggregate_flow_heat_sold ) ) @@ -314,7 +317,7 @@ def build_integrated_costs(self): == 1 - ( -1 - * en_cost.aggregate_flow_heat + * energy_cost.aggregate_flow_heat / treat_cost.aggregate_flow_heat ) ) @@ -366,12 +369,25 @@ def build_integrated_costs(self): - self.aggregate_flow_electricity_sold ) - if all(hasattr(b, "aggregate_flow_heat") for b in [treat_cost, en_cost]): + if all(hasattr(b, "aggregate_flow_heat") for b in [treat_cost, energy_cost]): self.aggregate_flow_heat_constraint = pyo.Constraint( expr=self.aggregate_flow_heat == self.aggregate_flow_heat_purchased - self.aggregate_flow_heat_sold ) + if not all( + "heat" in uf for uf in [treat_cost.used_flows, energy_cost.used_flows] + ): + self.aggregate_flow_heat_purchased.fix(0) + self.aggregate_flow_heat_sold.fix(0) + + if not all( + "electricity" in uf + for uf in [treat_cost.used_flows, energy_cost.used_flows] + ): + self.aggregate_flow_electricity_purchased.fix(0) + self.aggregate_flow_electricity_sold.fix(0) + def add_LCOW(self, flow_rate, name="LCOW"): """ Add Levelized Cost of Water (LCOW) to costing block. @@ -417,7 +433,7 @@ def add_LCOE(self, e_model="pysam"): "You must run the PySAM model before adding LCOE metric." ) - en_cost = self._get_energy_cost_block() + energy_cost = self._get_energy_cost_block() self.annual_energy_generated = pyo.Param( initialize=pysam.annual_energy, @@ -426,10 +442,10 @@ def add_LCOE(self, e_model="pysam"): ) LCOE_expr = pyo.Expression( expr=( - en_cost.total_capital_cost * self.capital_recovery_factor + energy_cost.total_capital_cost * self.capital_recovery_factor + ( - en_cost.aggregate_fixed_operating_cost - + en_cost.aggregate_variable_operating_cost + energy_cost.aggregate_fixed_operating_cost + + energy_cost.aggregate_variable_operating_cost ) ) / self.annual_energy_generated @@ -498,35 +514,35 @@ def add_specific_thermal_energy_consumption(self, flow_rate): specific_thermal_energy_consumption_constraint, ) - def add_defined_flow(self, flow_name, flow_cost): - """ - This method adds a defined flow to the costing block. - - NOTE: Use this method to add `defined_flows` to the costing block - to ensure updates to `flow_cost` get propagated in the model. - See https://github.com/IDAES/idaes-pse/pull/1014 for details. - - Args: - flow_name: string containing the name of the flow to register - flow_cost: Pyomo expression that represents the flow unit cost - - Returns: - None - """ - flow_cost_name = flow_name + "_cost" - current_flow_cost = self.component(flow_cost_name) - if current_flow_cost is None: - self.add_component(flow_cost_name, pyo.Expression(expr=flow_cost)) - self.defined_flows._setitem(flow_name, self.component(flow_cost_name)) - elif current_flow_cost is flow_cost: - self.defined_flows._setitem(flow_name, current_flow_cost) - else: - # if we get here then there's an attribute named - # flow_cost_name on the block, which is an error - raise RuntimeError( - f"Attribute {flow_cost_name} already exists " - f"on the costing block, but is not {flow_cost}" - ) + # def add_defined_flow(self, flow_name, flow_cost): + # """ + # This method adds a defined flow to the costing block. + + # NOTE: Use this method to add `defined_flows` to the costing block + # to ensure updates to `flow_cost` get propagated in the model. + # See https://github.com/IDAES/idaes-pse/pull/1014 for details. + + # Args: + # flow_name: string containing the name of the flow to register + # flow_cost: Pyomo expression that represents the flow unit cost + + # Returns: + # None + # """ + # flow_cost_name = flow_name + "_cost" + # current_flow_cost = self.component(flow_cost_name) + # if current_flow_cost is None: + # self.add_component(flow_cost_name, pyo.Expression(expr=flow_cost)) + # self.defined_flows._setitem(flow_name, self.component(flow_cost_name)) + # elif current_flow_cost is flow_cost: + # self.defined_flows._setitem(flow_name, current_flow_cost) + # else: + # # if we get here then there's an attribute named + # # flow_cost_name on the block, which is an error + # raise RuntimeError( + # f"Attribute {flow_cost_name} already exists " + # f"on the costing block, but is not {flow_cost}" + # ) def _get_treatment_cost_block(self): for b in self.model().component_objects(pyo.Block): From b9188ac3a4badfa4ebfe3df73962cf5b01793ada Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Tue, 12 Nov 2024 13:35:06 -0700 Subject: [PATCH 29/70] checkpoint --- .../costing/watertap_reflo_costing_package.py | 42 +++---------------- 1 file changed, 6 insertions(+), 36 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py index 45948b9a..8f78e028 100644 --- a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py +++ b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py @@ -302,6 +302,12 @@ def build_integrated_costs(self): ) if all(hasattr(b, "aggregate_flow_heat") for b in [treat_cost, energy_cost]): + + self.aggregate_flow_heat_constraint = pyo.Constraint( + expr=self.aggregate_flow_heat + == self.aggregate_flow_heat_purchased - self.aggregate_flow_heat_sold + ) + # energy producer's heat flow is negative self.aggregate_heat_balance = pyo.Constraint( expr=( @@ -369,12 +375,6 @@ def build_integrated_costs(self): - self.aggregate_flow_electricity_sold ) - if all(hasattr(b, "aggregate_flow_heat") for b in [treat_cost, energy_cost]): - self.aggregate_flow_heat_constraint = pyo.Constraint( - expr=self.aggregate_flow_heat - == self.aggregate_flow_heat_purchased - self.aggregate_flow_heat_sold - ) - if not all( "heat" in uf for uf in [treat_cost.used_flows, energy_cost.used_flows] ): @@ -514,36 +514,6 @@ def add_specific_thermal_energy_consumption(self, flow_rate): specific_thermal_energy_consumption_constraint, ) - # def add_defined_flow(self, flow_name, flow_cost): - # """ - # This method adds a defined flow to the costing block. - - # NOTE: Use this method to add `defined_flows` to the costing block - # to ensure updates to `flow_cost` get propagated in the model. - # See https://github.com/IDAES/idaes-pse/pull/1014 for details. - - # Args: - # flow_name: string containing the name of the flow to register - # flow_cost: Pyomo expression that represents the flow unit cost - - # Returns: - # None - # """ - # flow_cost_name = flow_name + "_cost" - # current_flow_cost = self.component(flow_cost_name) - # if current_flow_cost is None: - # self.add_component(flow_cost_name, pyo.Expression(expr=flow_cost)) - # self.defined_flows._setitem(flow_name, self.component(flow_cost_name)) - # elif current_flow_cost is flow_cost: - # self.defined_flows._setitem(flow_name, current_flow_cost) - # else: - # # if we get here then there's an attribute named - # # flow_cost_name on the block, which is an error - # raise RuntimeError( - # f"Attribute {flow_cost_name} already exists " - # f"on the costing block, but is not {flow_cost}" - # ) - def _get_treatment_cost_block(self): for b in self.model().component_objects(pyo.Block): if isinstance(b, TreatmentCostingData): From 840bed2d309be04727795afcf8a9116b6f3db4db Mon Sep 17 00:00:00 2001 From: Mukta Hardikar Date: Tue, 12 Nov 2024 14:13:35 -0700 Subject: [PATCH 30/70] includes costs when only treatment unit has heat --- .../costing/watertap_reflo_costing_package.py | 37 ++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py index 8f78e028..36160916 100644 --- a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py +++ b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py @@ -302,7 +302,7 @@ def build_integrated_costs(self): ) if all(hasattr(b, "aggregate_flow_heat") for b in [treat_cost, energy_cost]): - + self.aggregate_flow_heat_constraint = pyo.Constraint( expr=self.aggregate_flow_heat == self.aggregate_flow_heat_purchased - self.aggregate_flow_heat_sold @@ -334,6 +334,24 @@ def build_integrated_costs(self): == 0 ) + elif all(hasattr(b, "aggregate_flow_heat") for b in [treat_cost]): + + self.aggregate_heat_balance = pyo.Constraint( + expr=( + self.aggregate_flow_heat_purchased + == treat_cost.aggregate_flow_heat + self.aggregate_flow_heat_sold + ) + ) + + self.aggregate_heat_complement = pyo.Constraint( + expr=self.aggregate_flow_heat_purchased * self.aggregate_flow_heat_sold + == 0 + ) + + else: + self.aggregate_flow_heat_purchased.fix(0) + self.aggregate_flow_heat_sold.fix(0) + # positive is for cost and negative for revenue self.total_electric_operating_cost_constraint = pyo.Constraint( expr=self.total_electric_operating_cost @@ -375,18 +393,11 @@ def build_integrated_costs(self): - self.aggregate_flow_electricity_sold ) - if not all( - "heat" in uf for uf in [treat_cost.used_flows, energy_cost.used_flows] - ): - self.aggregate_flow_heat_purchased.fix(0) - self.aggregate_flow_heat_sold.fix(0) - - if not all( - "electricity" in uf - for uf in [treat_cost.used_flows, energy_cost.used_flows] - ): - self.aggregate_flow_electricity_purchased.fix(0) - self.aggregate_flow_electricity_sold.fix(0) + if all(hasattr(b, "aggregate_flow_heat") for b in [treat_cost, energy_cost]): + self.aggregate_flow_heat_constraint = pyo.Constraint( + expr=self.aggregate_flow_heat + == self.aggregate_flow_heat_purchased - self.aggregate_flow_heat_sold + ) def add_LCOW(self, flow_rate, name="LCOW"): """ From 0a491c35827e318f8b4a9c2e53d87d13123826b0 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Tue, 12 Nov 2024 14:24:20 -0700 Subject: [PATCH 31/70] remove duplicate constraint; correct frac_heat_from_grid constr --- .../reflo/costing/watertap_reflo_costing_package.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py index 36160916..5abe6e31 100644 --- a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py +++ b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py @@ -321,11 +321,7 @@ def build_integrated_costs(self): expr=( self.frac_heat_from_grid == 1 - - ( - -1 - * energy_cost.aggregate_flow_heat - / treat_cost.aggregate_flow_heat - ) + - energy_cost.aggregate_flow_heat / treat_cost.aggregate_flow_heat ) ) @@ -393,11 +389,6 @@ def build_integrated_costs(self): - self.aggregate_flow_electricity_sold ) - if all(hasattr(b, "aggregate_flow_heat") for b in [treat_cost, energy_cost]): - self.aggregate_flow_heat_constraint = pyo.Constraint( - expr=self.aggregate_flow_heat - == self.aggregate_flow_heat_purchased - self.aggregate_flow_heat_sold - ) def add_LCOW(self, flow_rate, name="LCOW"): """ From 1cecd0d69e9d032b213e362aee2004081b8a66c4 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Tue, 12 Nov 2024 14:40:28 -0700 Subject: [PATCH 32/70] system costing has _registered_unit_costing for aggregation of costing from both blocks --- .../reflo/costing/watertap_reflo_costing_package.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py index 5abe6e31..6fd0a794 100644 --- a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py +++ b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py @@ -166,6 +166,10 @@ def build_integrated_costs(self): treat_cost = self._get_treatment_cost_block() energy_cost = self._get_energy_cost_block() + for b in [treat_cost, energy_cost]: + for u in b._registered_unit_costing: + self._registered_unit_costing.append(u) + self.total_capital_cost = pyo.Var( initialize=1e3, domain=pyo.NonNegativeReals, From 32a54775eb5e05bb73a25131c875e4b6517a04e4 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Tue, 12 Nov 2024 14:48:50 -0700 Subject: [PATCH 33/70] remove buy/sell vars from defined_flows --- .../reflo/costing/watertap_reflo_costing_package.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py index 6fd0a794..da122691 100644 --- a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py +++ b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py @@ -129,7 +129,6 @@ def build_global_params(self): doc="Electricity cost to buy", units=pyo.units.USD_2018 / pyo.units.kWh, ) - self.defined_flows["electricity_buy"] = self.electricity_cost_buy self.electricity_cost_sell = pyo.Param( mutable=True, @@ -137,7 +136,6 @@ def build_global_params(self): doc="Electricity cost to sell", units=pyo.units.USD_2018 / pyo.units.kWh, ) - self.defined_flows["electricity_sell"] = self.electricity_cost_sell self.heat_cost_buy = pyo.Param( mutable=True, @@ -145,7 +143,6 @@ def build_global_params(self): doc="Heat cost to buy", units=pyo.units.USD_2018 / pyo.units.kWh, ) - self.defined_flows["heat_buy"] = self.heat_cost_buy self.heat_cost_sell = pyo.Param( mutable=True, @@ -153,7 +150,6 @@ def build_global_params(self): doc="Heat cost to sell", units=pyo.units.USD_2018 / pyo.units.kWh, ) - self.defined_flows["heat_sell"] = self.heat_cost_sell # Build the integrated system costs self.build_integrated_costs() From 94a4a89a13d838d4d27c0a10bacf1289a3bb7155 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Tue, 12 Nov 2024 15:04:12 -0700 Subject: [PATCH 34/70] revert frac_heat_from_grid constr --- .../reflo/costing/watertap_reflo_costing_package.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py index da122691..9a6e520d 100644 --- a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py +++ b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py @@ -321,7 +321,11 @@ def build_integrated_costs(self): expr=( self.frac_heat_from_grid == 1 - - energy_cost.aggregate_flow_heat / treat_cost.aggregate_flow_heat + - ( + -1 + * energy_cost.aggregate_flow_heat + / treat_cost.aggregate_flow_heat + ) ) ) @@ -389,7 +393,6 @@ def build_integrated_costs(self): - self.aggregate_flow_electricity_sold ) - def add_LCOW(self, flow_rate, name="LCOW"): """ Add Levelized Cost of Water (LCOW) to costing block. From b1606f63c10618b2e1d045c0f2eca335971c387e Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Tue, 12 Nov 2024 15:30:45 -0700 Subject: [PATCH 35/70] add dummy testing units --- .../costing/tests/costing_dummy_units.py | 421 ++++++++++++++++++ 1 file changed, 421 insertions(+) create mode 100644 src/watertap_contrib/reflo/costing/tests/costing_dummy_units.py diff --git a/src/watertap_contrib/reflo/costing/tests/costing_dummy_units.py b/src/watertap_contrib/reflo/costing/tests/costing_dummy_units.py new file mode 100644 index 00000000..89abc958 --- /dev/null +++ b/src/watertap_contrib/reflo/costing/tests/costing_dummy_units.py @@ -0,0 +1,421 @@ +from pyomo.environ import ( + Var, + Constraint, + Param, + units as pyunits, +) +from pyomo.common.config import ConfigBlock, ConfigValue, In + +import idaes.logger as idaeslog +from idaes.core import UnitModelBlockData, useDefault, declare_process_block_class +from idaes.core.util.config import is_physical_parameter_block + +from watertap.costing.util import register_costing_parameter_block +from watertap.core import InitializationMixin +from watertap_contrib.reflo.core import SolarEnergyBaseData +from watertap.core.solvers import get_solver + +from watertap_contrib.reflo.costing.util import ( + make_capital_cost_var, + make_variable_operating_cost_var, + make_fixed_operating_cost_var, +) + +solver = get_solver() + +""" +Set of dummy treatment and energy generation units used to test the costing package. +""" + +__author__ = "Kurban Sitterley" + +############################################################################ +############################################################################ + + +@declare_process_block_class("DummyTreatmentUnit") +class DummyTreatmentUnitData(InitializationMixin, UnitModelBlockData): + CONFIG = ConfigBlock() + + CONFIG.declare( + "dynamic", + ConfigValue(default=False, domain=In([False])), + ) + + CONFIG.declare( + "has_holdup", + ConfigValue(default=False, domain=In([False])), + ) + + CONFIG.declare( + "property_package", + ConfigValue( + default=useDefault, + domain=is_physical_parameter_block, + ), + ) + + CONFIG.declare( + "property_package_args", + ConfigBlock( + implicit=True, + ), + ) + + def build(self): + super().build() + + 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 = self.config.property_package.state_block_class( + self.flowsheet().config.time, doc="Unit properties", **tmp_dict + ) + + self.chemical_dose = Param( + initialize=2.2e-2, + mutable=True, + units=pyunits.kg / pyunits.m**3, + doc="Chemical dose", + ) + + self.design_var_a = Var( + initialize=42, + bounds=(0, None), + units=pyunits.dimensionless, + doc="Test treatment unit design variable", + ) + + self.design_var_b = Var( + initialize=1.23e-4, + bounds=(0, None), + units=pyunits.dimensionless, + doc="Test treatment unit design variable", + ) + + self.capital_var = Var( + initialize=99, + bounds=(0, None), + units=pyunits.dimensionless, + doc="Test treatment unit capital variable", + ) + + self.fixed_operating_var = Var( + initialize=202, + bounds=(0, None), + units=pyunits.dimensionless, + doc="Test treatment unit fixed operating variable", + ) + + self.variable_operating_var = Var( + initialize=1003, + bounds=(0, None), + units=pyunits.dimensionless, + doc="Test treatment unit variable operating variable", + ) + + self.energy_consumption = Var( + initialize=1e4, + units=pyunits.kilowatt, + bounds=(0, None), + doc="Constant energy consumption", + ) + + self.heat_consumption = Var( + initialize=2e4, + units=pyunits.kilowatt, + bounds=(0, None), + doc="Constant heat consumption", + ) + + @self.Constraint(doc="Capital variable calculation") + def eq_capital_var(b): + return ( + b.capital_var == b.design_var_a * b.properties[0].flow_vol_phase["Liq"] + ) + + @self.Constraint(doc="Fixed operating variable calculation") + def eq_fixed_operating_var(b): + return ( + b.fixed_operating_var + == b.design_var_b * b.properties[0].conc_mass_phase_comp["Liq", "TDS"] + ) + + @self.Constraint(doc="Variable operating variable calculation") + def eq_variable_operating_var(b): + return ( + b.variable_operating_var + == (b.design_var_a * b.design_var_b) + * b.properties[0].flow_mass_phase_comp["Liq", "TDS"] + ) + + def initialize_build(self): + solve_log = idaeslog.getSolveLogger(self.name, tag="unit") + + opt = get_solver() + + flags = self.properties.initialize(hold_state=True) + with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: + res = opt.solve(self, tee=slc.tee) + + self.properties.release_state(flags) + + @property + def default_costing_method(self): + return cost_dummy_treatment_unit + + +def build_dummy_treatment_unit_param_block(blk): + + blk.capital_cost_param = Var( + initialize=1e4, + units=pyunits.USD_2000, + bounds=(0, None), + doc="Capital cost param", + ) + + blk.fixed_operating_cost_param = Var( + initialize=3, + units=pyunits.USD_2011 / pyunits.m**3, + bounds=(0, None), + doc="Operating cost param", + ) + + blk.variable_operating_cost_param = Var( + initialize=4.2e-1, + units=pyunits.USD_2020 / pyunits.m**3, + bounds=(0, None), + doc="Operating cost param", + ) + + +def build_chem_cost_param_block(blk): + + blk.cost = Param( + mutable=True, + initialize=0.68, + units=pyunits.USD_2016 / pyunits.kg, + doc="Chemical unit cost", + ) + + blk.purity = Param( + mutable=True, + initialize=0.56, + units=pyunits.dimensionless, + doc="Chemical purity", + ) + + blk.parent_block().register_flow_type("chemical", blk.cost / blk.purity) + + +@register_costing_parameter_block( + build_rule=build_chem_cost_param_block, + parameter_block_name="test_chemical", +) +@register_costing_parameter_block( + build_rule=build_dummy_treatment_unit_param_block, + parameter_block_name="dummy_treatment_unit", +) +def cost_dummy_treatment_unit(blk): + + make_capital_cost_var(blk) + make_fixed_operating_cost_var(blk) + make_variable_operating_cost_var(blk) + + blk.costing_package.add_cost_factor(blk, "TIC") + + blk.capital_cost_constraint = Constraint( + expr=blk.capital_cost + == pyunits.convert( + blk.costing_package.dummy_treatment_unit.capital_cost_param + * blk.unit_model.capital_var, + to_units=blk.costing_package.base_currency, + ) + ) + + blk.fixed_operating_cost_constraint = Constraint( + expr=blk.fixed_operating_cost + == pyunits.convert( + blk.costing_package.dummy_treatment_unit.fixed_operating_cost_param + * blk.unit_model.properties[0].flow_vol_phase["Liq"] + * blk.unit_model.fixed_operating_var, + to_units=blk.costing_package.base_currency + / blk.costing_package.base_period, + ) + ) + + blk.variable_operating_cost_constraint = Constraint( + expr=blk.variable_operating_cost + == pyunits.convert( + blk.costing_package.dummy_treatment_unit.variable_operating_cost_param + * blk.unit_model.properties[0].flow_vol_phase["Liq"] + * blk.unit_model.variable_operating_var, + to_units=blk.costing_package.base_currency + / blk.costing_package.base_period, + ) + ) + + blk.costing_package.cost_flow( + blk.unit_model.energy_consumption, + "electricity", + ) + + blk.costing_package.cost_flow( + blk.unit_model.heat_consumption, + "heat", + ) + + blk.costing_package.cost_flow( + pyunits.convert( + blk.unit_model.chemical_dose + * blk.unit_model.properties[0].flow_vol_phase["Liq"], + to_units=pyunits.kg / blk.costing_package.base_period, + ), + "chemical", + ) + + +############################################################################ +############################################################################ + + +@declare_process_block_class("DummyElectricityUnit") +class DummyElectricityUnitData(SolarEnergyBaseData): + """ + Test unit for electricity generation. + Generates zero heat. + """ + + CONFIG = SolarEnergyBaseData.CONFIG() + CONFIG.solar_model_type = "physical" + + def build(self): + super().build() + + self.heat.fix(0) + + @property + def default_costing_method(self): + return cost_dummy_electricity_unit + + +def build_dummy_electricity_unit_param_block(blk): + + blk.capital_per_watt = Var( + initialize=0.3, + units=pyunits.USD_2019 / pyunits.watt, + bounds=(0, None), + doc="Cost per watt", + ) + + blk.fixed_operating_per_watt = Var( + initialize=0.042, + units=pyunits.USD_2019 / (pyunits.watt * pyunits.year), + bounds=(0, None), + doc="Cost per watt", + ) + + +@register_costing_parameter_block( + build_rule=build_dummy_electricity_unit_param_block, + parameter_block_name="dummy_electricity_unit", +) +def cost_dummy_electricity_unit(blk): + + make_capital_cost_var(blk) + make_fixed_operating_cost_var(blk) + + blk.costing_package.add_cost_factor(blk, None) + blk.capital_cost_constraint = Constraint( + expr=blk.capital_cost + == pyunits.convert( + blk.costing_package.dummy_electricity_unit.capital_per_watt + * blk.unit_model.electricity, + to_units=blk.costing_package.base_currency, + ) + ) + + blk.fixed_operating_cost_constraint = Constraint( + expr=blk.fixed_operating_cost + == pyunits.convert( + blk.costing_package.dummy_electricity_unit.fixed_operating_per_watt + * blk.unit_model.electricity, + to_units=blk.costing_package.base_currency + / blk.costing_package.base_period, + ) + ) + + # Generating is negative by convention + blk.costing_package.cost_flow(-1 * blk.unit_model.electricity, "electricity") + + +############################################################################ +############################################################################ + + +@declare_process_block_class("DummyHeatUnit") +class DummyHeatUnitData(SolarEnergyBaseData): + CONFIG = SolarEnergyBaseData.CONFIG() + CONFIG.solar_model_type = "physical" + + def build(self): + super().build() + + self.electricity.fix(20) + + @property + def default_costing_method(self): + return cost_dummy_heat_unit + + +def build_dummy_heat_unit_param_block(blk): + + blk.capital_per_watt = Var( + initialize=0.6, + units=pyunits.USD_2019 / pyunits.watt, + bounds=(0, None), + doc="Cost per watt", + ) + + blk.fixed_operating_per_watt = Var( + initialize=0.019, + units=pyunits.USD_2019 / (pyunits.watt * pyunits.year), + bounds=(0, None), + doc="Cost per watt", + ) + + +@register_costing_parameter_block( + build_rule=build_dummy_heat_unit_param_block, + parameter_block_name="dummy_heat_unit", +) +def cost_dummy_heat_unit(blk): + + make_capital_cost_var(blk) + make_fixed_operating_cost_var(blk) + + blk.costing_package.add_cost_factor(blk, None) + blk.capital_cost_constraint = Constraint( + expr=blk.capital_cost + == pyunits.convert( + blk.costing_package.dummy_heat_unit.capital_per_watt * blk.unit_model.heat, + to_units=blk.costing_package.base_currency, + ) + ) + + blk.fixed_operating_cost_constraint = Constraint( + expr=blk.fixed_operating_cost + == pyunits.convert( + blk.costing_package.dummy_heat_unit.fixed_operating_per_watt + * blk.unit_model.heat, + to_units=blk.costing_package.base_currency + / blk.costing_package.base_period, + ) + ) + + # Generating is negative by convention + blk.costing_package.cost_flow(-1 * blk.unit_model.heat, "heat") + # Heat generatig units could require energy + blk.costing_package.cost_flow(blk.unit_model.electricity, "electricity") From 3c58b48efe451d6c7dd2b4a81f80a430655d9702 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Tue, 12 Nov 2024 16:00:06 -0700 Subject: [PATCH 36/70] add dummy flowsheet --- .../costing/tests/costing_dummy_units.py | 104 +++++++++++++++++- 1 file changed, 102 insertions(+), 2 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/tests/costing_dummy_units.py b/src/watertap_contrib/reflo/costing/tests/costing_dummy_units.py index 89abc958..74c55702 100644 --- a/src/watertap_contrib/reflo/costing/tests/costing_dummy_units.py +++ b/src/watertap_contrib/reflo/costing/tests/costing_dummy_units.py @@ -116,7 +116,7 @@ def build(self): doc="Test treatment unit variable operating variable", ) - self.energy_consumption = Var( + self.electricity_consumption = Var( initialize=1e4, units=pyunits.kilowatt, bounds=(0, None), @@ -258,7 +258,7 @@ def cost_dummy_treatment_unit(blk): ) blk.costing_package.cost_flow( - blk.unit_model.energy_consumption, + blk.unit_model.electricity_consumption, "electricity", ) @@ -419,3 +419,103 @@ def cost_dummy_heat_unit(blk): blk.costing_package.cost_flow(-1 * blk.unit_model.heat, "heat") # Heat generatig units could require energy blk.costing_package.cost_flow(blk.unit_model.electricity, "electricity") + + +if __name__ == "__main__": + + from pyomo.environ import ConcreteModel, Block, assert_optimal_termination + + from idaes.core import FlowsheetBlock, UnitModelCostingBlock + from idaes.core.util.scaling import calculate_scaling_factors + from idaes.core.util.model_statistics import degrees_of_freedom + + from watertap.core.util.model_diagnostics.infeasible import * + from watertap.property_models.seawater_prop_pack import SeawaterParameterBlock + + from watertap_contrib.reflo.costing import ( + TreatmentCosting, + EnergyCosting, + REFLOCosting, + REFLOSystemCosting, + ) + + + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + m.fs.properties = SeawaterParameterBlock() + + #### TREATMENT BLOCK + m.fs.treatment = Block() + m.fs.treatment.costing = TreatmentCosting() + m.fs.treatment.costing.electricity_cost.fix(0.06) + m.fs.treatment.costing.heat_cost.set_value(0.01) + + m.fs.treatment.unit = DummyTreatmentUnit(property_package=m.fs.properties) + m.fs.treatment.unit.costing = UnitModelCostingBlock( + flowsheet_costing_block=m.fs.treatment.costing + ) + + m.fs.treatment.unit.design_var_a.fix() + m.fs.treatment.unit.design_var_b.fix() + m.fs.treatment.unit.electricity_consumption.fix(10) + m.fs.treatment.unit.heat_consumption.fix() + m.fs.treatment.costing.cost_process() + #### ENERGY BLOCK + m.fs.energy = Block() + m.fs.energy.costing = EnergyCosting() + m.fs.energy.pv = DummyElectricityUnit() + # m.fs.energy.pv.electricity.set_value(14000) + # m.fs.energy.pv.electricity.fix(14000) + m.fs.energy.pv.costing = UnitModelCostingBlock( + flowsheet_costing_block=m.fs.energy.costing + ) + m.fs.energy.costing.cost_process() + + #### SYSTEM COSTING + m.fs.costing = REFLOSystemCosting() + # m.fs.costing.aggregate_flow_electricity_purchased.fix(1) + # m.fs.costing.aggregate_flow_electricity_sold.fix(1) + m.fs.costing.frac_elec_from_grid.fix(0.5) + # m.fs.costing.frac_heat_from_grid.fix(0) + m.fs.costing.cost_process() + m.fs.treatment.costing.add_LCOW(m.fs.treatment.unit.properties[0].flow_vol_phase["Liq"]) + + #### SCALING + 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", "TDS")) + calculate_scaling_factors(m) + + + # #### INITIALIZE + + m.fs.treatment.unit.properties.calculate_state( + var_args={ + ("flow_vol_phase", "Liq"): 0.04381, + ("conc_mass_phase_comp", ("Liq", "TDS")): 35, + ("temperature", None): 293, + ("pressure", None): 101325, + }, + hold_state=True, + ) + + m.fs.treatment.unit.initialize() + m.fs.treatment.costing.initialize() + m.fs.energy.costing.initialize() + m.fs.costing.initialize() + + # assert degrees_of_freedom(m) == 0 + print(f"DOF = {degrees_of_freedom(m)}") + try: + results = solver.solve(m) + assert_optimal_termination(results) + except: + print_infeasible_constraints(m) + + # m.fs.costing.display() + m.fs.treatment.costing.aggregate_flow_electricity.display() + m.fs.energy.costing.aggregate_flow_electricity.display() + + m.fs.treatment.unit.electricity_consumption.display() + m.fs.energy.pv.electricity.display() + m.fs.costing.frac_elec_from_grid.display() + m.fs.costing.aggregate_flow_electricity_purchased.display() \ No newline at end of file From 8b7351524d9b0e7391f5725ec9551ab7d3a6983c Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Tue, 12 Nov 2024 16:44:36 -0700 Subject: [PATCH 37/70] aggregate_flow_heat at system is aggregate_flow_heat for treatment if no heat generated --- .../costing/watertap_reflo_costing_package.py | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py index 9a6e520d..fb6b5bf4 100644 --- a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py +++ b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py @@ -295,6 +295,18 @@ def build_integrated_costs(self): ) ) + # self.frac_elec_from_grid_constraint = pyo.Constraint( + # expr=( + # self.frac_elec_from_grid + # == 1 + # - ( + # -1 + # * energy_cost.aggregate_flow_electricity + # / treat_cost.aggregate_flow_electricity + # ) + # ) + # ) + self.aggregate_electricity_complement = pyo.Constraint( expr=self.aggregate_flow_electricity_purchased * self.aggregate_flow_electricity_sold @@ -303,6 +315,8 @@ def build_integrated_costs(self): if all(hasattr(b, "aggregate_flow_heat") for b in [treat_cost, energy_cost]): + # treatment block is consuming heat and energy block is generating it + self.aggregate_flow_heat_constraint = pyo.Constraint( expr=self.aggregate_flow_heat == self.aggregate_flow_heat_purchased - self.aggregate_flow_heat_sold @@ -334,21 +348,21 @@ def build_integrated_costs(self): == 0 ) - elif all(hasattr(b, "aggregate_flow_heat") for b in [treat_cost]): + elif hasattr(treat_cost, "aggregate_flow_heat"): + + # treatment block is consuming heat but energy block isn't generating + # we still want to cost the heat consumption + + self.aggregate_flow_heat_sold.fix(0) self.aggregate_heat_balance = pyo.Constraint( expr=( - self.aggregate_flow_heat_purchased - == treat_cost.aggregate_flow_heat + self.aggregate_flow_heat_sold + self.aggregate_flow_heat_purchased == treat_cost.aggregate_flow_heat ) ) - self.aggregate_heat_complement = pyo.Constraint( - expr=self.aggregate_flow_heat_purchased * self.aggregate_flow_heat_sold - == 0 - ) - else: + # treatment block isn't consuming heat and energy block isn't generating self.aggregate_flow_heat_purchased.fix(0) self.aggregate_flow_heat_sold.fix(0) From b261065f9f52758077e1f39ba2d09934e1dd262b Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Tue, 12 Nov 2024 16:44:47 -0700 Subject: [PATCH 38/70] scale dummy units --- .../costing/tests/costing_dummy_units.py | 52 ++++++++++++------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/tests/costing_dummy_units.py b/src/watertap_contrib/reflo/costing/tests/costing_dummy_units.py index 74c55702..2932b847 100644 --- a/src/watertap_contrib/reflo/costing/tests/costing_dummy_units.py +++ b/src/watertap_contrib/reflo/costing/tests/costing_dummy_units.py @@ -2,12 +2,14 @@ Var, Constraint, Param, + value, units as pyunits, ) from pyomo.common.config import ConfigBlock, ConfigValue, In import idaes.logger as idaeslog from idaes.core import UnitModelBlockData, useDefault, declare_process_block_class +from idaes.core.util.scaling import calculate_scaling_factors, set_scaling_factor from idaes.core.util.config import is_physical_parameter_block from watertap.costing.util import register_costing_parameter_block @@ -96,21 +98,21 @@ def build(self): ) self.capital_var = Var( - initialize=99, + initialize=1, bounds=(0, None), units=pyunits.dimensionless, doc="Test treatment unit capital variable", ) self.fixed_operating_var = Var( - initialize=202, + initialize=1, bounds=(0, None), units=pyunits.dimensionless, doc="Test treatment unit fixed operating variable", ) self.variable_operating_var = Var( - initialize=1003, + initialize=1, bounds=(0, None), units=pyunits.dimensionless, doc="Test treatment unit variable operating variable", @@ -162,6 +164,22 @@ def initialize_build(self): self.properties.release_state(flags) + def calculate_scaling_factors(self): + + set_scaling_factor(self.design_var_a, 1 / value(self.design_var_a)) + set_scaling_factor(self.design_var_b, 1 / value(self.design_var_b)) + set_scaling_factor(self.capital_var, 1 / value(self.capital_var)) + set_scaling_factor( + self.fixed_operating_var, 1 / value(self.fixed_operating_var) + ) + set_scaling_factor( + self.variable_operating_var, 1 / value(self.variable_operating_var) + ) + set_scaling_factor( + self.electricity_consumption, 1 / value(self.electricity_consumption) + ) + set_scaling_factor(self.heat_consumption, 1 / value(self.heat_consumption)) + @property def default_costing_method(self): return cost_dummy_treatment_unit @@ -435,11 +453,9 @@ def cost_dummy_heat_unit(blk): from watertap_contrib.reflo.costing import ( TreatmentCosting, EnergyCosting, - REFLOCosting, REFLOSystemCosting, ) - - + m = ConcreteModel() m.fs = FlowsheetBlock(dynamic=False) m.fs.properties = SeawaterParameterBlock() @@ -447,8 +463,6 @@ def cost_dummy_heat_unit(blk): #### TREATMENT BLOCK m.fs.treatment = Block() m.fs.treatment.costing = TreatmentCosting() - m.fs.treatment.costing.electricity_cost.fix(0.06) - m.fs.treatment.costing.heat_cost.set_value(0.01) m.fs.treatment.unit = DummyTreatmentUnit(property_package=m.fs.properties) m.fs.treatment.unit.costing = UnitModelCostingBlock( @@ -464,8 +478,6 @@ def cost_dummy_heat_unit(blk): m.fs.energy = Block() m.fs.energy.costing = EnergyCosting() m.fs.energy.pv = DummyElectricityUnit() - # m.fs.energy.pv.electricity.set_value(14000) - # m.fs.energy.pv.electricity.fix(14000) m.fs.energy.pv.costing = UnitModelCostingBlock( flowsheet_costing_block=m.fs.energy.costing ) @@ -475,17 +487,21 @@ def cost_dummy_heat_unit(blk): m.fs.costing = REFLOSystemCosting() # m.fs.costing.aggregate_flow_electricity_purchased.fix(1) # m.fs.costing.aggregate_flow_electricity_sold.fix(1) - m.fs.costing.frac_elec_from_grid.fix(0.5) - # m.fs.costing.frac_heat_from_grid.fix(0) + m.fs.costing.frac_elec_from_grid.fix(0.9) m.fs.costing.cost_process() - m.fs.treatment.costing.add_LCOW(m.fs.treatment.unit.properties[0].flow_vol_phase["Liq"]) + m.fs.treatment.costing.add_LCOW( + m.fs.treatment.unit.properties[0].flow_vol_phase["Liq"] + ) - #### SCALING - 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", "TDS")) + #### SCALING + 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", "TDS") + ) calculate_scaling_factors(m) - # #### INITIALIZE m.fs.treatment.unit.properties.calculate_state( @@ -518,4 +534,4 @@ def cost_dummy_heat_unit(blk): m.fs.treatment.unit.electricity_consumption.display() m.fs.energy.pv.electricity.display() m.fs.costing.frac_elec_from_grid.display() - m.fs.costing.aggregate_flow_electricity_purchased.display() \ No newline at end of file + m.fs.costing.aggregate_flow_electricity_purchased.display() From 9e946c1d3cddd95214dd5589a99357bd696fafb9 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Tue, 12 Nov 2024 18:19:42 -0700 Subject: [PATCH 39/70] raise error or Treatment/EnergyCosting blocks aren't found --- .../costing/watertap_reflo_costing_package.py | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py index fb6b5bf4..3943b140 100644 --- a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py +++ b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py @@ -295,18 +295,6 @@ def build_integrated_costs(self): ) ) - # self.frac_elec_from_grid_constraint = pyo.Constraint( - # expr=( - # self.frac_elec_from_grid - # == 1 - # - ( - # -1 - # * energy_cost.aggregate_flow_electricity - # / treat_cost.aggregate_flow_electricity - # ) - # ) - # ) - self.aggregate_electricity_complement = pyo.Constraint( expr=self.aggregate_flow_electricity_purchased * self.aggregate_flow_electricity_sold @@ -534,14 +522,28 @@ def add_specific_thermal_energy_consumption(self, flow_rate): ) def _get_treatment_cost_block(self): + tb = None for b in self.model().component_objects(pyo.Block): if isinstance(b, TreatmentCostingData): - return b + tb = b + if tb is None: + err_msg = "REFLOSystemCosting package requires a TreatmentCosting block" + err_msg += " but one was not found." + raise ValueError(err_msg) + else: + return tb def _get_energy_cost_block(self): + eb = None for b in self.model().component_objects(pyo.Block): if isinstance(b, EnergyCostingData): - return b + eb = b + if eb is None: + err_msg = "REFLOSystemCosting package requires a EnergyCosting block" + err_msg += " but one was not found." + raise ValueError(err_msg) + else: + return eb def _get_pysam(self): pysam_block_test_lst = [] From aecb937a8918fb94760c1277c1ae6cb103d57e3a Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Tue, 12 Nov 2024 18:46:48 -0700 Subject: [PATCH 40/70] implement new approach to handle frac_elec_from_grid --- .../reflo/costing/solar/photovoltaic.py | 1 + .../costing/watertap_reflo_costing_package.py | 48 ++++++++++++++----- 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/solar/photovoltaic.py b/src/watertap_contrib/reflo/costing/solar/photovoltaic.py index 66d71e00..beef36f6 100644 --- a/src/watertap_contrib/reflo/costing/solar/photovoltaic.py +++ b/src/watertap_contrib/reflo/costing/solar/photovoltaic.py @@ -92,6 +92,7 @@ def build_photovoltaic_cost_param_block(blk): ) def cost_pv(blk): + blk.costing_package.has_electricity_generation = True global_params = blk.costing_package pv_params = blk.costing_package.photovoltaic make_capital_cost_var(blk) diff --git a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py index 3943b140..b3f19cb2 100644 --- a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py +++ b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py @@ -22,6 +22,8 @@ from watertap.costing.zero_order_costing import _load_case_study_definition from watertap_contrib.reflo.core import PySAMWaterTAP +from watertap_contrib.reflo.solar_models.surrogate.pv.pv_surrogate import PVSurrogateData +from watertap_contrib.reflo.costing.tests.costing_dummy_units import DummyElectricityUnitData @declare_process_block_class("REFLOCosting") @@ -104,6 +106,10 @@ def build_process_costs(self): @declare_process_block_class("EnergyCosting") class EnergyCostingData(REFLOCostingData): def build_global_params(self): + # If creating an energy unit that generates electricity, + # set this flag to True in costing package. + # See PV costing package for example. + self.has_electricity_generation = False super().build_global_params() def build_process_costs(self): @@ -278,22 +284,24 @@ def build_integrated_costs(self): ) ) - # Calculate fraction of electricity from grid when PV is included - for b in self.model().component_objects(pyo.Block): - if str(b) == "fs.energy.pv": - self.frac_elec_from_grid_constraint = pyo.Constraint( - expr=( - self.frac_elec_from_grid - == 1 - - ( - b.electricity - / ( - b.electricity - + self.aggregate_flow_electricity_purchased - ) + # Calculate fraction of electricity from grid when an electricity generating unit is present + if energy_cost.has_electricity_generation: + elec_gen_unit = self._get_electricity_generation_unit() + self.frac_elec_from_grid_constraint = pyo.Constraint( + expr=( + self.frac_elec_from_grid + == 1 + - ( + elec_gen_unit.electricity + / ( + elec_gen_unit.electricity + + self.aggregate_flow_electricity_purchased ) ) ) + ) + else: + self.frac_elec_from_grid.fix(1) self.aggregate_electricity_complement = pyo.Constraint( expr=self.aggregate_flow_electricity_purchased @@ -544,6 +552,20 @@ def _get_energy_cost_block(self): raise ValueError(err_msg) else: return eb + + def _get_electricity_generation_unit(self): + elec_gen_unit = None + for b in self.model().component_objects(pyo.Block): + if isinstance(b, PVSurrogateData): # PV is only electricity generation model currently + elec_gen_unit = b + if isinstance(b, DummyElectricityUnitData): # only used for testing + elec_gen_unit = b + if elec_gen_unit is None: + err_msg = f"{self.name} indicated an electricity generation model was present " + err_msg += "on the flowsheet, but none was found." + raise ValueError(err_msg) + else: + return elec_gen_unit def _get_pysam(self): pysam_block_test_lst = [] From 6066d9313afebaf4eea055a13795e0c1f1034c3a Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Tue, 12 Nov 2024 19:06:14 -0700 Subject: [PATCH 41/70] check param equivalence for all costing blocks --- .../costing/watertap_reflo_costing_package.py | 72 ++++++++++++++++--- 1 file changed, 63 insertions(+), 9 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py index b3f19cb2..d9f18e35 100644 --- a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py +++ b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py @@ -22,8 +22,12 @@ from watertap.costing.zero_order_costing import _load_case_study_definition from watertap_contrib.reflo.core import PySAMWaterTAP -from watertap_contrib.reflo.solar_models.surrogate.pv.pv_surrogate import PVSurrogateData -from watertap_contrib.reflo.costing.tests.costing_dummy_units import DummyElectricityUnitData +from watertap_contrib.reflo.solar_models.surrogate.pv.pv_surrogate import ( + PVSurrogateData, +) +from watertap_contrib.reflo.costing.tests.costing_dummy_units import ( + DummyElectricityUnitData, +) @declare_process_block_class("REFLOCosting") @@ -160,14 +164,16 @@ def build_global_params(self): # Build the integrated system costs self.build_integrated_costs() - def build_process_costs(self): - pass - def build_integrated_costs(self): treat_cost = self._get_treatment_cost_block() energy_cost = self._get_energy_cost_block() + # Check if all parameters are equivalent + self._check_common_param_equivalence(treat_cost, energy_cost) + + # Add all treatment and energy units to _registered_unit_costing + # so aggregated costs can be calculated at system level. for b in [treat_cost, energy_cost]: for u in b._registered_unit_costing: self._registered_unit_costing.append(u) @@ -403,6 +409,12 @@ def build_integrated_costs(self): - self.aggregate_flow_electricity_sold ) + def build_process_costs(self): + """ + Not used in place of build_integrated_costs + """ + pass + def add_LCOW(self, flow_rate, name="LCOW"): """ Add Levelized Cost of Water (LCOW) to costing block. @@ -529,6 +541,44 @@ def add_specific_thermal_energy_consumption(self, flow_rate): specific_thermal_energy_consumption_constraint, ) + def _check_common_param_equivalence(self, treat_cost, energy_cost): + """ + Check if the common costing parameters across all three costing packages + (treatment, energy, and system) have the same value. + """ + + common_params = [ + "electricity_cost", + "heat_cost", + "electrical_carbon_intensity", + "maintenance_labor_chemical_factor", + "plant_lifetime", + "utilization_factor", + "base_currency", + "base_period", + "sales_tax_frac", + "TIC", + "TPEC", + ] + + for cp in common_params: + tp = getattr(treat_cost, cp) + ep = getattr(energy_cost, cp) + if not pyo.value(tp) == pyo.value(ep): + err_msg = f"The common costing parameter {cp} was found to have a different value " + err_msg += f"on the energy ({pyo.value(ep)}) and treatment ({pyo.value(tp)}) costing blocks. " + err_msg += "Common costing parameters must be equivalent across all costing blocks " + err_msg += "to use REFLOSystemCosting." + raise ValueError(err_msg) + if hasattr(self, cp): + # if REFLOSystemCosting has this parameter, + # we fix it to the treatment costing block value + p = getattr(self, cp) + if isinstance(p, pyo.Var): + p.fix(pyo.value(tp)) + elif isinstance(p, pyo.Param): + p.set_value(pyo.value(tp)) + def _get_treatment_cost_block(self): tb = None for b in self.model().component_objects(pyo.Block): @@ -552,16 +602,20 @@ def _get_energy_cost_block(self): raise ValueError(err_msg) else: return eb - + def _get_electricity_generation_unit(self): elec_gen_unit = None for b in self.model().component_objects(pyo.Block): - if isinstance(b, PVSurrogateData): # PV is only electricity generation model currently + if isinstance( + b, PVSurrogateData + ): # PV is only electricity generation model currently elec_gen_unit = b - if isinstance(b, DummyElectricityUnitData): # only used for testing + if isinstance(b, DummyElectricityUnitData): # only used for testing elec_gen_unit = b if elec_gen_unit is None: - err_msg = f"{self.name} indicated an electricity generation model was present " + err_msg = ( + f"{self.name} indicated an electricity generation model was present " + ) err_msg += "on the flowsheet, but none was found." raise ValueError(err_msg) else: From 20161a368cf74fd8d59a741a825fba17a699761c Mon Sep 17 00:00:00 2001 From: Mukta Hardikar Date: Wed, 13 Nov 2024 09:04:05 -0700 Subject: [PATCH 42/70] update to always calc aggregate_flow_heat --- .../costing/watertap_reflo_costing_package.py | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py index d9f18e35..e3dda519 100644 --- a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py +++ b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py @@ -209,12 +209,11 @@ def build_integrated_costs(self): units=self.base_currency / self.base_period, ) - if all(hasattr(b, "aggregate_flow_heat") for b in [treat_cost, energy_cost]): - self.aggregate_flow_heat = pyo.Var( - initialize=1e3, - doc="Aggregated heat flow", - units=pyo.units.kW, - ) + self.aggregate_flow_heat = pyo.Var( + initialize=1e3, + doc="Aggregated heat flow", + units=pyo.units.kW, + ) self.total_capital_cost_constraint = pyo.Constraint( expr=self.total_capital_cost @@ -235,15 +234,6 @@ def build_integrated_costs(self): ) ) - if all(hasattr(b, "aggregate_flow_heat") for b in [treat_cost, energy_cost]): - self.frac_heat_from_grid = pyo.Var( - initialize=0, - domain=pyo.NonNegativeReals, - bounds=(0, 1.00001), - doc="Fraction of heat from grid", - units=pyo.units.dimensionless, - ) - self.frac_elec_from_grid = pyo.Var( initialize=0, domain=pyo.NonNegativeReals, @@ -319,9 +309,12 @@ def build_integrated_costs(self): # treatment block is consuming heat and energy block is generating it - self.aggregate_flow_heat_constraint = pyo.Constraint( - expr=self.aggregate_flow_heat - == self.aggregate_flow_heat_purchased - self.aggregate_flow_heat_sold + self.frac_heat_from_grid = pyo.Var( + initialize=0, + domain=pyo.NonNegativeReals, + bounds=(0, 1.00001), + doc="Fraction of heat from grid", + units=pyo.units.dimensionless, ) # energy producer's heat flow is negative @@ -409,6 +402,11 @@ def build_integrated_costs(self): - self.aggregate_flow_electricity_sold ) + self.aggregate_flow_heat_constraint = pyo.Constraint( + expr=self.aggregate_flow_heat + == self.aggregate_flow_heat_purchased - self.aggregate_flow_heat_sold + ) + def build_process_costs(self): """ Not used in place of build_integrated_costs From ec4209db30381822a6a590c92a92dbf863c207f3 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Wed, 13 Nov 2024 09:19:28 -0700 Subject: [PATCH 43/70] add elec gen flag to costing --- src/watertap_contrib/reflo/costing/tests/costing_dummy_units.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/watertap_contrib/reflo/costing/tests/costing_dummy_units.py b/src/watertap_contrib/reflo/costing/tests/costing_dummy_units.py index 2932b847..1cbe9baa 100644 --- a/src/watertap_contrib/reflo/costing/tests/costing_dummy_units.py +++ b/src/watertap_contrib/reflo/costing/tests/costing_dummy_units.py @@ -342,6 +342,8 @@ def build_dummy_electricity_unit_param_block(blk): ) def cost_dummy_electricity_unit(blk): + blk.costing_package.has_electricity_generation = True + make_capital_cost_var(blk) make_fixed_operating_cost_var(blk) From 7c80926afa12974ff8aa1ceff7f8a035e2753969 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Wed, 13 Nov 2024 09:57:17 -0700 Subject: [PATCH 44/70] remove __main__ func --- .../costing/tests/costing_dummy_units.py | 98 ------------------- 1 file changed, 98 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/tests/costing_dummy_units.py b/src/watertap_contrib/reflo/costing/tests/costing_dummy_units.py index 1cbe9baa..117f2d37 100644 --- a/src/watertap_contrib/reflo/costing/tests/costing_dummy_units.py +++ b/src/watertap_contrib/reflo/costing/tests/costing_dummy_units.py @@ -439,101 +439,3 @@ def cost_dummy_heat_unit(blk): blk.costing_package.cost_flow(-1 * blk.unit_model.heat, "heat") # Heat generatig units could require energy blk.costing_package.cost_flow(blk.unit_model.electricity, "electricity") - - -if __name__ == "__main__": - - from pyomo.environ import ConcreteModel, Block, assert_optimal_termination - - from idaes.core import FlowsheetBlock, UnitModelCostingBlock - from idaes.core.util.scaling import calculate_scaling_factors - from idaes.core.util.model_statistics import degrees_of_freedom - - from watertap.core.util.model_diagnostics.infeasible import * - from watertap.property_models.seawater_prop_pack import SeawaterParameterBlock - - from watertap_contrib.reflo.costing import ( - TreatmentCosting, - EnergyCosting, - REFLOSystemCosting, - ) - - m = ConcreteModel() - m.fs = FlowsheetBlock(dynamic=False) - m.fs.properties = SeawaterParameterBlock() - - #### TREATMENT BLOCK - m.fs.treatment = Block() - m.fs.treatment.costing = TreatmentCosting() - - m.fs.treatment.unit = DummyTreatmentUnit(property_package=m.fs.properties) - m.fs.treatment.unit.costing = UnitModelCostingBlock( - flowsheet_costing_block=m.fs.treatment.costing - ) - - m.fs.treatment.unit.design_var_a.fix() - m.fs.treatment.unit.design_var_b.fix() - m.fs.treatment.unit.electricity_consumption.fix(10) - m.fs.treatment.unit.heat_consumption.fix() - m.fs.treatment.costing.cost_process() - #### ENERGY BLOCK - m.fs.energy = Block() - m.fs.energy.costing = EnergyCosting() - m.fs.energy.pv = DummyElectricityUnit() - m.fs.energy.pv.costing = UnitModelCostingBlock( - flowsheet_costing_block=m.fs.energy.costing - ) - m.fs.energy.costing.cost_process() - - #### SYSTEM COSTING - m.fs.costing = REFLOSystemCosting() - # m.fs.costing.aggregate_flow_electricity_purchased.fix(1) - # m.fs.costing.aggregate_flow_electricity_sold.fix(1) - m.fs.costing.frac_elec_from_grid.fix(0.9) - m.fs.costing.cost_process() - m.fs.treatment.costing.add_LCOW( - m.fs.treatment.unit.properties[0].flow_vol_phase["Liq"] - ) - - #### SCALING - 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", "TDS") - ) - calculate_scaling_factors(m) - - # #### INITIALIZE - - m.fs.treatment.unit.properties.calculate_state( - var_args={ - ("flow_vol_phase", "Liq"): 0.04381, - ("conc_mass_phase_comp", ("Liq", "TDS")): 35, - ("temperature", None): 293, - ("pressure", None): 101325, - }, - hold_state=True, - ) - - m.fs.treatment.unit.initialize() - m.fs.treatment.costing.initialize() - m.fs.energy.costing.initialize() - m.fs.costing.initialize() - - # assert degrees_of_freedom(m) == 0 - print(f"DOF = {degrees_of_freedom(m)}") - try: - results = solver.solve(m) - assert_optimal_termination(results) - except: - print_infeasible_constraints(m) - - # m.fs.costing.display() - m.fs.treatment.costing.aggregate_flow_electricity.display() - m.fs.energy.costing.aggregate_flow_electricity.display() - - m.fs.treatment.unit.electricity_consumption.display() - m.fs.energy.pv.electricity.display() - m.fs.costing.frac_elec_from_grid.display() - m.fs.costing.aggregate_flow_electricity_purchased.display() From dc4ecdfb5f443f82154d135676c4ad802dd29113 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Wed, 13 Nov 2024 10:04:25 -0700 Subject: [PATCH 45/70] initial update to costing test --- .../test_reflo_watertap_costing_package.py | 200 +++++++++++++++++- 1 file changed, 198 insertions(+), 2 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/tests/test_reflo_watertap_costing_package.py b/src/watertap_contrib/reflo/costing/tests/test_reflo_watertap_costing_package.py index 251d95b5..b5c618de 100644 --- a/src/watertap_contrib/reflo/costing/tests/test_reflo_watertap_costing_package.py +++ b/src/watertap_contrib/reflo/costing/tests/test_reflo_watertap_costing_package.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. @@ -12,12 +12,208 @@ import re import pytest +from pyomo.environ import ( + ConcreteModel, + Var, + Param, + Expression, + Block, + assert_optimal_termination, + value, + units as pyunits, +) +from idaes.core import FlowsheetBlock, UnitModelCostingBlock +from idaes.core.util.scaling import calculate_scaling_factors +from idaes.core.util.model_statistics import degrees_of_freedom + +from watertap.core.util.model_diagnostics.infeasible import * +from watertap.property_models.seawater_prop_pack import SeawaterParameterBlock + +from watertap_contrib.reflo.costing import ( + TreatmentCosting, + EnergyCosting, + REFLOCosting, + REFLOSystemCosting, +) from pyomo.environ import ConcreteModel, Var, Param, Expression, value, units as pyunits from idaes.core import FlowsheetBlock -from watertap_contrib.reflo.costing import REFLOCosting +from watertap.core.solvers import get_solver + +from watertap_contrib.reflo.costing.tests.costing_dummy_units import ( + DummyTreatmentUnit, + DummyElectricityUnit, + DummyHeatUnit, +) +from watertap_contrib.reflo.costing import ( + REFLOCosting, + TreatmentCosting, + EnergyCosting, + REFLOSystemCosting, +) + +solver = get_solver() + + +def build_electricity_gen_only(): + """ + Test flowsheet with only electricity generation units on energy block. + The treatment unit consumes both heat and electricity. + """ + + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + m.fs.properties = SeawaterParameterBlock() + + #### TREATMENT BLOCK + m.fs.treatment = Block() + m.fs.treatment.costing = TreatmentCosting() + + m.fs.treatment.unit = DummyTreatmentUnit(property_package=m.fs.properties) + m.fs.treatment.unit.costing = UnitModelCostingBlock( + flowsheet_costing_block=m.fs.treatment.costing + ) + + m.fs.treatment.unit.design_var_a.fix() + m.fs.treatment.unit.design_var_b.fix() + m.fs.treatment.unit.electricity_consumption.fix(100) + m.fs.treatment.unit.heat_consumption.fix() + m.fs.treatment.costing.cost_process() + + #### ENERGY BLOCK + m.fs.energy = Block() + m.fs.energy.costing = EnergyCosting() + m.fs.energy.unit = DummyElectricityUnit() + m.fs.energy.unit.costing = UnitModelCostingBlock( + flowsheet_costing_block=m.fs.energy.costing + ) + m.fs.energy.unit.electricity.fix() + m.fs.energy.costing.cost_process() + + #### SYSTEM COSTING + m.fs.costing = REFLOSystemCosting() + + m.fs.costing.cost_process() + m.fs.treatment.costing.add_LCOW( + m.fs.treatment.unit.properties[0].flow_vol_phase["Liq"] + ) + + #### SCALING + 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", "TDS") + ) + calculate_scaling_factors(m) + + #### INITIALIZE + + m.fs.treatment.unit.properties.calculate_state( + var_args={ + ("flow_vol_phase", "Liq"): 0.04381, + ("conc_mass_phase_comp", ("Liq", "TDS")): 35, + ("temperature", None): 293, + ("pressure", None): 101325, + }, + hold_state=True, + ) + + return m + + +class TestElectricityGenOnly: + + @pytest.fixture(scope="class") + def energy_gen_only(self): + + m = build_electricity_gen_only() + + return m + + @pytest.mark.unit + def test_build(slef, energy_gen_only): + + m = energy_gen_only + + assert degrees_of_freedom(m) == 0 + + assert m.fs.energy.costing.has_electricity_generation + + m.fs.treatment.unit.initialize() + m.fs.treatment.costing.initialize() + m.fs.energy.costing.initialize() + m.fs.costing.initialize() + + results = solver.solve(m) + assert_optimal_termination(results) + + +@pytest.mark.component +def test_no_energy_treatment_block(): + + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + m.fs.properties = SeawaterParameterBlock() + + m.fs.treatment = Block() + m.fs.treatment.costing = TreatmentCosting() + m.fs.treatment.unit = DummyTreatmentUnit(property_package=m.fs.properties) + + with pytest.raises( + ValueError, + match="REFLOSystemCosting package requires a EnergyCosting block but one was not found\\.", + ): + m.fs.costing = REFLOSystemCosting() + + +@pytest.mark.component +def test_common_params_not_equivalent(): + + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + m.fs.properties = SeawaterParameterBlock() + + m.fs.treatment = Block() + m.fs.treatment.costing = TreatmentCosting() + m.fs.treatment.unit = DummyTreatmentUnit(property_package=m.fs.properties) + + m.fs.energy = Block() + m.fs.energy.costing = EnergyCosting() + m.fs.energy.unit = DummyElectricityUnit() + + m.fs.energy.costing.electricity_cost.fix(0.02) + + with pytest.raises( + ValueError, + match="The common costing parameter electricity_cost was found to " + "have a different value on the energy \\(0\\.02\\) and treatment \\(0\\.0\\) costing " + "blocks\\. Common costing parameters must be equivalent across all" + " costing blocks to use REFLOSystemCosting\\.", + ): + m.fs.costing = REFLOSystemCosting() + + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + m.fs.properties = SeawaterParameterBlock() + + m.fs.treatment = Block() + m.fs.treatment.costing = TreatmentCosting() + m.fs.treatment.unit = DummyTreatmentUnit(property_package=m.fs.properties) + + m.fs.energy = Block() + m.fs.energy.costing = EnergyCosting() + m.fs.energy.unit = DummyElectricityUnit() + + m.fs.energy.costing.electricity_cost.fix(0.02) + m.fs.treatment.costing.electricity_cost.fix(0.02) + + m.fs.costing = REFLOSystemCosting() + + # assert value(m.fs.costing.electricity_cost) == value(m.fs.treatment.electricity_cost) + # assert value(m.fs.costing.electricity_cost) == value(m.fs.energy.electricity_cost) @pytest.mark.component From 2c6706f99714d5c84df98795ea9d00142a5cd4b5 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Wed, 13 Nov 2024 12:03:12 -0700 Subject: [PATCH 46/70] update initial values; add initialize_build routine --- .../costing/watertap_reflo_costing_package.py | 97 +++++++++++++++++-- 1 file changed, 89 insertions(+), 8 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py index e3dda519..175f00ed 100644 --- a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py +++ b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py @@ -12,6 +12,7 @@ from pyomo.common.config import ConfigValue import pyomo.environ as pyo +from pyomo.util.calc_var_value import calculate_variable_from_constraint from idaes.core import declare_process_block_class @@ -26,7 +27,7 @@ PVSurrogateData, ) from watertap_contrib.reflo.costing.tests.costing_dummy_units import ( - DummyElectricityUnitData, + DummyElectricityUnit, ) @@ -235,7 +236,7 @@ def build_integrated_costs(self): ) self.frac_elec_from_grid = pyo.Var( - initialize=0, + initialize=0.1, domain=pyo.NonNegativeReals, bounds=(0, 1.00001), doc="Fraction of electricity from grid", @@ -243,7 +244,7 @@ def build_integrated_costs(self): ) self.aggregate_flow_electricity_purchased = pyo.Var( - initialize=0, + initialize=100, domain=pyo.NonNegativeReals, doc="Aggregated electricity consumed", units=pyo.units.kW, @@ -257,7 +258,7 @@ def build_integrated_costs(self): ) self.aggregate_flow_heat_purchased = pyo.Var( - initialize=0, + initialize=100, domain=pyo.NonNegativeReals, doc="Aggregated heat consumed", units=pyo.units.kW, @@ -308,7 +309,7 @@ def build_integrated_costs(self): if all(hasattr(b, "aggregate_flow_heat") for b in [treat_cost, energy_cost]): # treatment block is consuming heat and energy block is generating it - + self.has_heat_flows = True self.frac_heat_from_grid = pyo.Var( initialize=0, domain=pyo.NonNegativeReals, @@ -348,6 +349,8 @@ def build_integrated_costs(self): # treatment block is consuming heat but energy block isn't generating # we still want to cost the heat consumption + self.has_heat_flows = True + self.aggregate_flow_heat_sold.fix(0) self.aggregate_heat_balance = pyo.Constraint( @@ -358,6 +361,7 @@ def build_integrated_costs(self): else: # treatment block isn't consuming heat and energy block isn't generating + self.has_heat_flows = False self.aggregate_flow_heat_purchased.fix(0) self.aggregate_flow_heat_sold.fix(0) @@ -407,6 +411,71 @@ def build_integrated_costs(self): == self.aggregate_flow_heat_purchased - self.aggregate_flow_heat_sold ) + def initialize_build(self): + + self.aggregate_flow_electricity_sold.fix(0) + self.aggregate_electricity_complement.deactivate() + + calculate_variable_from_constraint( + self.aggregate_flow_electricity_purchased, + self.aggregate_electricity_balance, + ) + + if hasattr(self, "frac_elec_from_grid_constraint"): + calculate_variable_from_constraint( + self.frac_elec_from_grid, self.frac_elec_from_grid_constraint + ) + + calculate_variable_from_constraint( + self.total_electric_operating_cost, + self.total_electric_operating_cost_constraint, + ) + + calculate_variable_from_constraint( + self.aggregate_flow_electricity, + self.aggregate_flow_electricity_constraint, + ) + + self.aggregate_flow_electricity_sold.unfix() + self.aggregate_electricity_complement.activate() + + if not self.has_heat_flows: + self.total_heat_operating_cost.fix(0) + self.total_heat_operating_cost_constraint.deactivate() + self.aggregate_flow_heat.fix(0) + self.aggregate_flow_heat_constraint.deactivate() + + else: + if hasattr(self, "aggregate_heat_complement"): + + self.aggregate_flow_heat_sold.fix(0) + self.aggregate_heat_complement.deactivate() + + calculate_variable_from_constraint( + self.frac_heat_from_grid, + self.frac_heat_from_grid_constraint, + ) + + if not self.aggregate_flow_heat_purchased.is_fixed(): + calculate_variable_from_constraint( + self.aggregate_flow_heat_purchased, + self.aggregate_heat_balance, + ) + + calculate_variable_from_constraint( + self.total_heat_operating_cost, + self.total_heat_operating_cost_constraint, + ) + calculate_variable_from_constraint( + self.aggregate_flow_heat, + self.aggregate_flow_heat_constraint, + ) + + super().initialize_build() + + + + def build_process_costs(self): """ Not used in place of build_integrated_costs @@ -557,26 +626,38 @@ def _check_common_param_equivalence(self, treat_cost, energy_cost): "sales_tax_frac", "TIC", "TPEC", + "wacc", ] for cp in common_params: tp = getattr(treat_cost, cp) ep = getattr(energy_cost, cp) - if not pyo.value(tp) == pyo.value(ep): + if (isinstance(tp, pyo.Var)) or isinstance(tp, pyo.Param): + param_is_equivalent = pyo.value(tp) == pyo.value(ep) + else: + param_is_equivalent = tp == ep + if not param_is_equivalent: err_msg = f"The common costing parameter {cp} was found to have a different value " - err_msg += f"on the energy ({pyo.value(ep)}) and treatment ({pyo.value(tp)}) costing blocks. " + err_msg += f"on the energy and treatment costing blocks. " err_msg += "Common costing parameters must be equivalent across all costing blocks " err_msg += "to use REFLOSystemCosting." raise ValueError(err_msg) + if hasattr(self, cp): # if REFLOSystemCosting has this parameter, # we fix it to the treatment costing block value p = getattr(self, cp) + # print(p.to_string()) if isinstance(p, pyo.Var): p.fix(pyo.value(tp)) elif isinstance(p, pyo.Param): p.set_value(pyo.value(tp)) + if cp == "base_currency": + self.base_currency = treat_cost.base_currency + if cp == "base_period": + self.base_period = treat_cost.base_period + def _get_treatment_cost_block(self): tb = None for b in self.model().component_objects(pyo.Block): @@ -608,7 +689,7 @@ def _get_electricity_generation_unit(self): b, PVSurrogateData ): # PV is only electricity generation model currently elec_gen_unit = b - if isinstance(b, DummyElectricityUnitData): # only used for testing + if isinstance(b, DummyElectricityUnit): # only used for testing elec_gen_unit = b if elec_gen_unit is None: err_msg = ( From 76ff8040a8cce125189f7ff46a2e1ffe3dce5e9d Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Wed, 13 Nov 2024 12:03:47 -0700 Subject: [PATCH 47/70] add no heat dummy treatment unit; black --- .../costing/tests/costing_dummy_units.py | 52 +++++++++++++++++++ .../costing/watertap_reflo_costing_package.py | 13 ++--- 2 files changed, 57 insertions(+), 8 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/tests/costing_dummy_units.py b/src/watertap_contrib/reflo/costing/tests/costing_dummy_units.py index 117f2d37..b00de741 100644 --- a/src/watertap_contrib/reflo/costing/tests/costing_dummy_units.py +++ b/src/watertap_contrib/reflo/costing/tests/costing_dummy_units.py @@ -3,6 +3,7 @@ Constraint, Param, value, + check_optimal_termination, units as pyunits, ) from pyomo.common.config import ConfigBlock, ConfigValue, In @@ -161,6 +162,8 @@ def initialize_build(self): flags = self.properties.initialize(hold_state=True) with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(self, tee=slc.tee) + if not check_optimal_termination(res): + res = opt.solve(self, tee=slc.tee) # just try again! self.properties.release_state(flags) @@ -299,6 +302,55 @@ def cost_dummy_treatment_unit(blk): ############################################################################ +@declare_process_block_class("DummyTreatmentNoHeatUnit") +class DummyTreatmentNoHeatUnitData(DummyTreatmentUnitData): + CONFIG = DummyTreatmentUnitData.CONFIG() + + def build(self): + super().build() + self.del_component(self.heat_consumption) + self.del_component(self.fixed_operating_var) + self.del_component(self.variable_operating_var) + + def calculate_scaling_factors(self): + + set_scaling_factor(self.design_var_a, 1 / value(self.design_var_a)) + set_scaling_factor(self.design_var_b, 1 / value(self.design_var_b)) + set_scaling_factor(self.capital_var, 1 / value(self.capital_var)) + set_scaling_factor( + self.electricity_consumption, 1 / value(self.electricity_consumption) + ) + + +@register_costing_parameter_block( + build_rule=build_dummy_treatment_unit_param_block, + parameter_block_name="dummy_treatment_no_heat_unit", +) +def cost_dummy_treatment_unit(blk): + + make_capital_cost_var(blk) + + blk.costing_package.add_cost_factor(blk, None) + + blk.capital_cost_constraint = Constraint( + expr=blk.capital_cost + == pyunits.convert( + blk.costing_package.dummy_treatment_no_heat_unit.capital_cost_param + * blk.unit_model.capital_var, + to_units=blk.costing_package.base_currency, + ) + ) + + blk.costing_package.cost_flow( + blk.unit_model.electricity_consumption, + "electricity", + ) + + +############################################################################ +############################################################################ + + @declare_process_block_class("DummyElectricityUnit") class DummyElectricityUnitData(SolarEnergyBaseData): """ diff --git a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py index 175f00ed..20dbc525 100644 --- a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py +++ b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py @@ -438,16 +438,16 @@ def initialize_build(self): self.aggregate_flow_electricity_sold.unfix() self.aggregate_electricity_complement.activate() - + if not self.has_heat_flows: self.total_heat_operating_cost.fix(0) self.total_heat_operating_cost_constraint.deactivate() self.aggregate_flow_heat.fix(0) self.aggregate_flow_heat_constraint.deactivate() - + else: if hasattr(self, "aggregate_heat_complement"): - + self.aggregate_flow_heat_sold.fix(0) self.aggregate_heat_complement.deactivate() @@ -456,7 +456,7 @@ def initialize_build(self): self.frac_heat_from_grid_constraint, ) - if not self.aggregate_flow_heat_purchased.is_fixed(): + if not self.aggregate_flow_heat_purchased.is_fixed(): calculate_variable_from_constraint( self.aggregate_flow_heat_purchased, self.aggregate_heat_balance, @@ -470,11 +470,8 @@ def initialize_build(self): self.aggregate_flow_heat, self.aggregate_flow_heat_constraint, ) - - super().initialize_build() - - + super().initialize_build() def build_process_costs(self): """ From 646414db1cb3338bcce1f7c5a6aa9d92c582fa7b Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Wed, 13 Nov 2024 12:13:41 -0700 Subject: [PATCH 48/70] add scaling factors --- .../costing/watertap_reflo_costing_package.py | 97 +++++++++++++------ 1 file changed, 70 insertions(+), 27 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py index 20dbc525..21ad6eb6 100644 --- a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py +++ b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py @@ -15,6 +15,7 @@ from pyomo.util.calc_var_value import calculate_variable_from_constraint from idaes.core import declare_process_block_class +from idaes.core.util.scaling import get_scaling_factor, set_scaling_factor from watertap.costing.watertap_costing_package import ( WaterTAPCostingData, @@ -198,6 +199,12 @@ def build_integrated_costs(self): units=pyo.units.kW, ) + self.aggregate_flow_heat = pyo.Var( + initialize=1e3, + doc="Aggregated heat flow", + units=pyo.units.kW, + ) + self.total_electric_operating_cost = pyo.Var( initialize=1e3, doc="Total electricity related operating cost", @@ -210,31 +217,6 @@ def build_integrated_costs(self): units=self.base_currency / self.base_period, ) - self.aggregate_flow_heat = pyo.Var( - initialize=1e3, - doc="Aggregated heat flow", - units=pyo.units.kW, - ) - - self.total_capital_cost_constraint = pyo.Constraint( - expr=self.total_capital_cost - == pyo.units.convert( - treat_cost.total_capital_cost + energy_cost.total_capital_cost, - to_units=self.base_currency, - ) - ) - - self.total_operating_cost_constraint = pyo.Constraint( - expr=self.total_operating_cost - == pyo.units.convert( - treat_cost.total_operating_cost - + energy_cost.total_operating_cost - + self.total_electric_operating_cost - + self.total_heat_operating_cost, - to_units=self.base_currency / self.base_period, - ) - ) - self.frac_elec_from_grid = pyo.Var( initialize=0.1, domain=pyo.NonNegativeReals, @@ -271,6 +253,25 @@ def build_integrated_costs(self): units=pyo.units.kW, ) + + self.total_capital_cost_constraint = pyo.Constraint( + expr=self.total_capital_cost + == pyo.units.convert( + treat_cost.total_capital_cost + energy_cost.total_capital_cost, + to_units=self.base_currency, + ) + ) + + self.total_operating_cost_constraint = pyo.Constraint( + expr=self.total_operating_cost + == pyo.units.convert( + treat_cost.total_operating_cost + + energy_cost.total_operating_cost + + self.total_electric_operating_cost + + self.total_heat_operating_cost, + to_units=self.base_currency / self.base_period, + ) + ) # energy producer's electricity flow is negative self.aggregate_electricity_balance = pyo.Constraint( expr=( @@ -350,9 +351,8 @@ def build_integrated_costs(self): # we still want to cost the heat consumption self.has_heat_flows = True - self.aggregate_flow_heat_sold.fix(0) - + self.aggregate_heat_balance = pyo.Constraint( expr=( self.aggregate_flow_heat_purchased == treat_cost.aggregate_flow_heat @@ -472,6 +472,49 @@ def initialize_build(self): ) super().initialize_build() + + def calculate_scaling_factors(self): + + if get_scaling_factor(self.total_capital_cost) is None: + set_scaling_factor(self.total_capital_cost, 1e-3) + + if get_scaling_factor(self.total_operating_cost) is None: + set_scaling_factor(self.total_operating_cost, 1e-3) + + if get_scaling_factor(self.total_electric_operating_cost) is None: + set_scaling_factor(self.total_electric_operating_cost, 1e-2) + + if get_scaling_factor(self.total_heat_operating_cost) is None: + set_scaling_factor(self.total_heat_operating_cost, 1) + + if get_scaling_factor(self.aggregate_flow_electricity) is None: + set_scaling_factor(self.aggregate_flow_electricity, 0.1) + + if get_scaling_factor(self.aggregate_flow_heat) is None: + set_scaling_factor(self.aggregate_flow_heat, 0.1) + + if get_scaling_factor(self.aggregate_flow_electricity_purchased) is None: + sf = get_scaling_factor(self.aggregate_flow_electricity) + set_scaling_factor(self.aggregate_flow_electricity_purchased, sf) + + if get_scaling_factor(self.aggregate_flow_electricity_sold) is None: + set_scaling_factor(self.aggregate_flow_electricity_sold, 1) + + if get_scaling_factor(self.aggregate_flow_heat_purchased) is None: + sf = get_scaling_factor(self.aggregate_flow_heat) + set_scaling_factor(self.aggregate_flow_heat_purchased, sf) + + if get_scaling_factor(self.aggregate_flow_electricity_sold) is None: + set_scaling_factor(self.aggregate_flow_electricity_sold, 1) + + if get_scaling_factor(self.frac_elec_from_grid) is None: + set_scaling_factor(self.frac_elec_from_grid, 1) + + if hasattr(self, "frac_heat_from_grid"): + if get_scaling_factor(self.frac_heat_from_grid) is None: + set_scaling_factor(self.frac_heat_from_grid, 1) + + def build_process_costs(self): """ From b007a7dec0761d47cf5114a3d9e36d2f9e7cecdd Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Wed, 13 Nov 2024 14:01:00 -0700 Subject: [PATCH 49/70] wrong costing method for no heat --- .../reflo/costing/tests/costing_dummy_units.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/tests/costing_dummy_units.py b/src/watertap_contrib/reflo/costing/tests/costing_dummy_units.py index b00de741..cef3de6d 100644 --- a/src/watertap_contrib/reflo/costing/tests/costing_dummy_units.py +++ b/src/watertap_contrib/reflo/costing/tests/costing_dummy_units.py @@ -321,12 +321,15 @@ def calculate_scaling_factors(self): self.electricity_consumption, 1 / value(self.electricity_consumption) ) + @property + def default_costing_method(self): + return cost_dummy_treatment_no_heat_unit @register_costing_parameter_block( build_rule=build_dummy_treatment_unit_param_block, parameter_block_name="dummy_treatment_no_heat_unit", ) -def cost_dummy_treatment_unit(blk): +def cost_dummy_treatment_no_heat_unit(blk): make_capital_cost_var(blk) @@ -377,14 +380,14 @@ def build_dummy_electricity_unit_param_block(blk): initialize=0.3, units=pyunits.USD_2019 / pyunits.watt, bounds=(0, None), - doc="Cost per watt", + doc="Capital cost per watt", ) blk.fixed_operating_per_watt = Var( initialize=0.042, units=pyunits.USD_2019 / (pyunits.watt * pyunits.year), bounds=(0, None), - doc="Cost per watt", + doc="Operating cost per watt", ) From a6c9ecaca5c00fe41a4751eba229029a099ab544 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Wed, 13 Nov 2024 14:02:43 -0700 Subject: [PATCH 50/70] capex only positive --- src/watertap_contrib/reflo/costing/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/watertap_contrib/reflo/costing/util.py b/src/watertap_contrib/reflo/costing/util.py index 4245aea7..c43b6275 100644 --- a/src/watertap_contrib/reflo/costing/util.py +++ b/src/watertap_contrib/reflo/costing/util.py @@ -16,7 +16,7 @@ def make_capital_cost_var(blk): blk.capital_cost = pyo.Var( initialize=1e5, - # domain=pyo.NonNegativeReals, + domain=pyo.NonNegativeReals, units=blk.costing_package.base_currency, doc="Unit capital cost", ) From b790a12a3d1eb5648a4c197bc6b85bf61cdac99b Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Wed, 13 Nov 2024 14:41:40 -0700 Subject: [PATCH 51/70] electricity gen tests complete --- ..._dummy_units.py => dummy_costing_units.py} | 1 + .../test_reflo_watertap_costing_package.py | 578 ++++++++++++++++-- .../costing/watertap_reflo_costing_package.py | 61 +- 3 files changed, 563 insertions(+), 77 deletions(-) rename src/watertap_contrib/reflo/costing/tests/{costing_dummy_units.py => dummy_costing_units.py} (99%) diff --git a/src/watertap_contrib/reflo/costing/tests/costing_dummy_units.py b/src/watertap_contrib/reflo/costing/tests/dummy_costing_units.py similarity index 99% rename from src/watertap_contrib/reflo/costing/tests/costing_dummy_units.py rename to src/watertap_contrib/reflo/costing/tests/dummy_costing_units.py index cef3de6d..4ea204e9 100644 --- a/src/watertap_contrib/reflo/costing/tests/costing_dummy_units.py +++ b/src/watertap_contrib/reflo/costing/tests/dummy_costing_units.py @@ -325,6 +325,7 @@ def calculate_scaling_factors(self): def default_costing_method(self): return cost_dummy_treatment_no_heat_unit + @register_costing_parameter_block( build_rule=build_dummy_treatment_unit_param_block, parameter_block_name="dummy_treatment_no_heat_unit", diff --git a/src/watertap_contrib/reflo/costing/tests/test_reflo_watertap_costing_package.py b/src/watertap_contrib/reflo/costing/tests/test_reflo_watertap_costing_package.py index b5c618de..c71fd504 100644 --- a/src/watertap_contrib/reflo/costing/tests/test_reflo_watertap_costing_package.py +++ b/src/watertap_contrib/reflo/costing/tests/test_reflo_watertap_costing_package.py @@ -18,6 +18,8 @@ Param, Expression, Block, + Reals, + NonNegativeReals, assert_optimal_termination, value, units as pyunits, @@ -27,37 +29,32 @@ from idaes.core.util.scaling import calculate_scaling_factors from idaes.core.util.model_statistics import degrees_of_freedom -from watertap.core.util.model_diagnostics.infeasible import * +from watertap.costing.watertap_costing_package import ( + WaterTAPCostingData, + WaterTAPCostingBlockData, +) +from watertap.core.solvers import get_solver from watertap.property_models.seawater_prop_pack import SeawaterParameterBlock from watertap_contrib.reflo.costing import ( + REFLOCosting, + REFLOCostingData, TreatmentCosting, EnergyCosting, - REFLOCosting, REFLOSystemCosting, ) -from pyomo.environ import ConcreteModel, Var, Param, Expression, value, units as pyunits -from idaes.core import FlowsheetBlock - -from watertap.core.solvers import get_solver - -from watertap_contrib.reflo.costing.tests.costing_dummy_units import ( +from watertap_contrib.reflo.costing.tests.dummy_costing_units import ( DummyTreatmentUnit, + DummyTreatmentNoHeatUnit, DummyElectricityUnit, DummyHeatUnit, ) -from watertap_contrib.reflo.costing import ( - REFLOCosting, - TreatmentCosting, - EnergyCosting, - REFLOSystemCosting, -) solver = get_solver() -def build_electricity_gen_only(): +def build_electricity_gen_only_with_heat(): """ Test flowsheet with only electricity generation units on energy block. The treatment unit consumes both heat and electricity. @@ -89,13 +86,79 @@ def build_electricity_gen_only(): m.fs.energy.unit.costing = UnitModelCostingBlock( flowsheet_costing_block=m.fs.energy.costing ) - m.fs.energy.unit.electricity.fix() + m.fs.energy.unit.electricity.fix(10) m.fs.energy.costing.cost_process() #### SYSTEM COSTING m.fs.costing = REFLOSystemCosting() + m.fs.costing.cost_process() + + m.fs.treatment.costing.add_LCOW( + m.fs.treatment.unit.properties[0].flow_vol_phase["Liq"] + ) + + #### SCALING + 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", "TDS") + ) + calculate_scaling_factors(m) + + #### INITIALIZE + + m.fs.treatment.unit.properties.calculate_state( + var_args={ + ("flow_vol_phase", "Liq"): 0.04381, + ("conc_mass_phase_comp", ("Liq", "TDS")): 35, + ("temperature", None): 293, + ("pressure", None): 101325, + }, + hold_state=True, + ) + + return m + + +def build_electricity_gen_only_no_heat(): + """ + Test flowsheet with only electricity generation units on energy block. + The treatment unit consumes only electricity. + """ + + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + m.fs.properties = SeawaterParameterBlock() + #### TREATMENT BLOCK + m.fs.treatment = Block() + m.fs.treatment.costing = TreatmentCosting() + + m.fs.treatment.unit = DummyTreatmentNoHeatUnit(property_package=m.fs.properties) + m.fs.treatment.unit.costing = UnitModelCostingBlock( + flowsheet_costing_block=m.fs.treatment.costing + ) + + m.fs.treatment.unit.design_var_a.fix() + m.fs.treatment.unit.design_var_b.fix() + m.fs.treatment.unit.electricity_consumption.fix(10000) + m.fs.treatment.costing.cost_process() + + #### ENERGY BLOCK + m.fs.energy = Block() + m.fs.energy.costing = EnergyCosting() + m.fs.energy.unit = DummyElectricityUnit() + m.fs.energy.unit.costing = UnitModelCostingBlock( + flowsheet_costing_block=m.fs.energy.costing + ) + m.fs.energy.unit.electricity.fix(7500) + m.fs.energy.costing.cost_process() + + #### SYSTEM COSTING + m.fs.costing = REFLOSystemCosting() m.fs.costing.cost_process() + m.fs.treatment.costing.add_LCOW( m.fs.treatment.unit.properties[0].flow_vol_phase["Liq"] ) @@ -124,23 +187,123 @@ def build_electricity_gen_only(): return m -class TestElectricityGenOnly: +def build_default(): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + m.fs.properties = SeawaterParameterBlock() + + m.fs.treatment = Block() + m.fs.treatment.costing = TreatmentCosting() + m.fs.treatment.unit = DummyTreatmentUnit(property_package=m.fs.properties) + m.fs.treatment.unit.costing = UnitModelCostingBlock( + flowsheet_costing_block=m.fs.treatment.costing + ) + m.fs.energy = Block() + m.fs.energy.costing = EnergyCosting() + m.fs.energy.unit = DummyElectricityUnit() + m.fs.energy.unit.costing = UnitModelCostingBlock( + flowsheet_costing_block=m.fs.energy.costing + ) + + return m + + +class TestCostingPackagesDefault: @pytest.fixture(scope="class") - def energy_gen_only(self): + def default_build(self): - m = build_electricity_gen_only() + m = build_default() + + m.fs.energy.costing.cost_process() + m.fs.treatment.costing.cost_process() + m.fs.costing = REFLOSystemCosting() + m.fs.costing.cost_process() + + return m + + def test_default_build(self, default_build): + m = default_build + + assert isinstance(m.fs.treatment.costing, REFLOCostingData) + assert isinstance(m.fs.energy.costing, REFLOCostingData) + assert isinstance(m.fs.costing, WaterTAPCostingBlockData) + + # no case study loaded by default + assert m.fs.treatment.costing.config.case_study_definition is None + assert m.fs.energy.costing.config.case_study_definition is None + assert not hasattr(m.fs.treatment.costing, "case_study_def") + assert not hasattr(m.fs.energy.costing, "case_study_def") + + assert m.fs.treatment.costing.base_currency is pyunits.USD_2021 + assert m.fs.energy.costing.base_currency is pyunits.USD_2021 + assert m.fs.costing.base_currency is pyunits.USD_2021 + + assert m.fs.treatment.costing.base_period is pyunits.year + assert m.fs.energy.costing.base_period is pyunits.year + assert m.fs.costing.base_period is pyunits.year + + assert hasattr(m.fs.treatment.costing, "sales_tax_frac") + assert hasattr(m.fs.energy.costing, "sales_tax_frac") + assert not hasattr(m.fs.costing, "sales_tax_frac") + + # general domain checks + assert m.fs.costing.total_heat_operating_cost.domain is Reals + assert m.fs.costing.total_electric_operating_cost.domain is Reals + assert m.fs.costing.aggregate_flow_electricity.domain is Reals + assert m.fs.costing.aggregate_flow_heat.domain is Reals + assert ( + m.fs.costing.aggregate_flow_electricity_purchased.domain is NonNegativeReals + ) + assert m.fs.costing.aggregate_flow_electricity_sold.domain is NonNegativeReals + assert m.fs.costing.aggregate_flow_heat_purchased.domain is NonNegativeReals + assert m.fs.costing.aggregate_flow_heat_sold.domain is NonNegativeReals + + # capital cost is only positive + assert m.fs.treatment.unit.costing.capital_cost.domain is NonNegativeReals + # operating costs can be negative + assert m.fs.treatment.unit.costing.fixed_operating_cost.domain is Reals + assert m.fs.treatment.unit.costing.variable_operating_cost.domain is Reals + + # default electricity cost is zero + assert value(m.fs.costing.electricity_cost) == 0 + assert value(m.fs.treatment.costing.electricity_cost) == 0 + assert value(m.fs.energy.costing.electricity_cost) == 0 + + # default heat cost is zero and there is no heat cost in system costing block + assert value(m.fs.treatment.costing.heat_cost) == 0 + assert value(m.fs.energy.costing.heat_cost) == 0 + assert not hasattr(m.fs.costing, "heat_cost") + assert hasattr(m.fs.costing, "heat_cost_buy") + + +class TestElectricityGenOnlyWithHeat: + + @pytest.fixture(scope="class") + def energy_gen_only_with_heat(self): + + m = build_electricity_gen_only_with_heat() return m @pytest.mark.unit - def test_build(slef, energy_gen_only): + def test_build(slef, energy_gen_only_with_heat): - m = energy_gen_only + m = energy_gen_only_with_heat assert degrees_of_freedom(m) == 0 + # still have heat flows + assert m.fs.costing.has_heat_flows + assert not m.fs.costing.aggregate_flow_heat.is_fixed() assert m.fs.energy.costing.has_electricity_generation + assert hasattr(m.fs.costing, "frac_elec_from_grid_constraint") + + assert not hasattr(m.fs.costing, "frac_heat_from_grid") + + @pytest.mark.component + def test_init_and_solve(self, energy_gen_only_with_heat): + m = energy_gen_only_with_heat m.fs.treatment.unit.initialize() m.fs.treatment.costing.initialize() @@ -150,6 +313,297 @@ def test_build(slef, energy_gen_only): results = solver.solve(m) assert_optimal_termination(results) + # no electricity is sold + assert ( + pytest.approx(value(m.fs.costing.aggregate_flow_electricity_sold), rel=1e-3) + == 1e-12 + ) + + assert pytest.approx(value(m.fs.costing.frac_elec_from_grid), rel=1e-3) == 0.9 + assert ( + pytest.approx( + value(m.fs.costing.aggregate_flow_electricity_purchased), rel=1e-3 + ) + == 90 + ) + assert pytest.approx( + value(m.fs.costing.aggregate_flow_electricity), rel=1e-3 + ) == value( + m.fs.costing.aggregate_flow_electricity_purchased + - m.fs.costing.aggregate_flow_electricity_sold + ) + assert pytest.approx( + value(m.fs.costing.aggregate_flow_electricity), rel=1e-3 + ) == value( + m.fs.treatment.costing.aggregate_flow_electricity + + m.fs.energy.costing.aggregate_flow_electricity + ) + assert pytest.approx( + value(m.fs.costing.frac_elec_from_grid), rel=1e-3 + ) == 1 - value(m.fs.energy.unit.electricity) / value( + m.fs.treatment.unit.electricity_consumption + ) + + # no heat is generated + assert pytest.approx( + value(m.fs.costing.aggregate_flow_heat), rel=1e-3 + ) == value(m.fs.treatment.unit.heat_consumption) + + @pytest.mark.component + def test_optimize_frac_from_grid(self): + + m = build_electricity_gen_only_with_heat() + + m.fs.energy.unit.electricity.unfix() + m.fs.costing.frac_elec_from_grid.fix(0.05) + + assert degrees_of_freedom(m) == 0 + + m.fs.treatment.unit.initialize() + m.fs.treatment.costing.initialize() + m.fs.energy.costing.initialize() + m.fs.costing.initialize() + + results = solver.solve(m) + assert_optimal_termination(results) + + assert ( + pytest.approx( + value(m.fs.costing.aggregate_flow_electricity_purchased), rel=1e-3 + ) + == 5 + ) + assert pytest.approx( + value(m.fs.costing.aggregate_flow_electricity), rel=1e-3 + ) == value( + m.fs.costing.aggregate_flow_electricity_purchased + - m.fs.costing.aggregate_flow_electricity_sold + ) + assert pytest.approx( + value(m.fs.costing.aggregate_flow_electricity), rel=1e-3 + ) == value( + m.fs.treatment.costing.aggregate_flow_electricity + + m.fs.energy.costing.aggregate_flow_electricity + ) + + +class TestElectricityGenOnlyNoHeat: + + @pytest.fixture(scope="class") + def energy_gen_only_no_heat(self): + + m = build_electricity_gen_only_no_heat() + + return m + + @pytest.mark.unit + def test_build(slef, energy_gen_only_no_heat): + + m = energy_gen_only_no_heat + + assert degrees_of_freedom(m) == 0 + + # no heat flows + assert not m.fs.costing.has_heat_flows + assert m.fs.costing.aggregate_flow_heat_purchased.is_fixed() + assert m.fs.costing.aggregate_flow_heat_sold.is_fixed() + assert m.fs.energy.costing.has_electricity_generation + assert hasattr(m.fs.costing, "frac_elec_from_grid_constraint") + + assert not hasattr(m.fs.costing, "frac_heat_from_grid") + + @pytest.mark.component + def test_init_and_solve(self, energy_gen_only_no_heat): + m = energy_gen_only_no_heat + + m.fs.treatment.unit.initialize() + m.fs.treatment.costing.initialize() + m.fs.energy.costing.initialize() + m.fs.costing.initialize() + + results = solver.solve(m) + assert_optimal_termination(results) + + # no electricity is sold + assert ( + pytest.approx(value(m.fs.costing.aggregate_flow_electricity_sold), rel=1e-3) + == 1e-12 + ) + + assert pytest.approx(value(m.fs.costing.frac_elec_from_grid), rel=1e-3) == 0.25 + assert ( + pytest.approx( + value(m.fs.costing.aggregate_flow_electricity_purchased), rel=1e-3 + ) + == 2500 + ) + assert pytest.approx( + value(m.fs.costing.aggregate_flow_electricity), rel=1e-3 + ) == value( + m.fs.costing.aggregate_flow_electricity_purchased + - m.fs.costing.aggregate_flow_electricity_sold + ) + assert pytest.approx( + value(m.fs.costing.aggregate_flow_electricity), rel=1e-3 + ) == value( + m.fs.treatment.costing.aggregate_flow_electricity + + m.fs.energy.costing.aggregate_flow_electricity + ) + assert pytest.approx( + value(m.fs.costing.frac_elec_from_grid), rel=1e-3 + ) == 1 - value(m.fs.energy.unit.electricity) / value( + m.fs.treatment.unit.electricity_consumption + ) + + # no heat is generated or consumed + assert pytest.approx(value(m.fs.costing.aggregate_flow_heat), rel=1e-3) == 0 + + @pytest.mark.component + def test_optimize_frac_from_grid(self): + + m = build_electricity_gen_only_no_heat() + + m.fs.energy.unit.electricity.unfix() + m.fs.costing.frac_elec_from_grid.fix(0.33) + + assert degrees_of_freedom(m) == 0 + + m.fs.treatment.unit.initialize() + m.fs.treatment.costing.initialize() + m.fs.energy.costing.initialize() + m.fs.costing.initialize() + + results = solver.solve(m) + assert_optimal_termination(results) + + assert ( + pytest.approx( + value(m.fs.costing.aggregate_flow_electricity_purchased), rel=1e-3 + ) + == 3300 + ) + assert pytest.approx( + value(m.fs.costing.aggregate_flow_electricity), rel=1e-3 + ) == value( + m.fs.costing.aggregate_flow_electricity_purchased + - m.fs.costing.aggregate_flow_electricity_sold + ) + assert pytest.approx( + value(m.fs.costing.aggregate_flow_electricity), rel=1e-3 + ) == value( + m.fs.treatment.costing.aggregate_flow_electricity + + m.fs.energy.costing.aggregate_flow_electricity + ) + + +class TestElectricityHeatGen: + + @pytest.fixture(scope="class") + def energy_gen_only_no_heat(self): + + m = build_electricity_gen_only_no_heat() + + return m + + @pytest.mark.unit + def test_build(slef, energy_gen_only_no_heat): + + m = energy_gen_only_no_heat + + assert degrees_of_freedom(m) == 0 + + # no heat flows + assert not m.fs.costing.has_heat_flows + assert m.fs.costing.aggregate_flow_heat_purchased.is_fixed() + assert m.fs.costing.aggregate_flow_heat_sold.is_fixed() + assert m.fs.energy.costing.has_electricity_generation + assert hasattr(m.fs.costing, "frac_elec_from_grid_constraint") + + assert not hasattr(m.fs.costing, "frac_heat_from_grid") + + @pytest.mark.component + def test_init_and_solve(self, energy_gen_only_no_heat): + m = energy_gen_only_no_heat + + m.fs.treatment.unit.initialize() + m.fs.treatment.costing.initialize() + m.fs.energy.costing.initialize() + m.fs.costing.initialize() + + results = solver.solve(m) + assert_optimal_termination(results) + + # no electricity is sold + assert ( + pytest.approx(value(m.fs.costing.aggregate_flow_electricity_sold), rel=1e-3) + == 1e-12 + ) + + assert pytest.approx(value(m.fs.costing.frac_elec_from_grid), rel=1e-3) == 0.25 + assert ( + pytest.approx( + value(m.fs.costing.aggregate_flow_electricity_purchased), rel=1e-3 + ) + == 2500 + ) + assert pytest.approx( + value(m.fs.costing.aggregate_flow_electricity), rel=1e-3 + ) == value( + m.fs.costing.aggregate_flow_electricity_purchased + - m.fs.costing.aggregate_flow_electricity_sold + ) + assert pytest.approx( + value(m.fs.costing.aggregate_flow_electricity), rel=1e-3 + ) == value( + m.fs.treatment.costing.aggregate_flow_electricity + + m.fs.energy.costing.aggregate_flow_electricity + ) + assert pytest.approx( + value(m.fs.costing.frac_elec_from_grid), rel=1e-3 + ) == 1 - value(m.fs.energy.unit.electricity) / value( + m.fs.treatment.unit.electricity_consumption + ) + + # no heat is generated or consumed + assert pytest.approx(value(m.fs.costing.aggregate_flow_heat), rel=1e-3) == 0 + + @pytest.mark.component + def test_optimize_frac_from_grid(self): + + m = build_electricity_gen_only_no_heat() + + m.fs.energy.unit.electricity.unfix() + m.fs.costing.frac_elec_from_grid.fix(0.33) + + assert degrees_of_freedom(m) == 0 + + m.fs.treatment.unit.initialize() + m.fs.treatment.costing.initialize() + m.fs.energy.costing.initialize() + m.fs.costing.initialize() + + results = solver.solve(m) + assert_optimal_termination(results) + + assert ( + pytest.approx( + value(m.fs.costing.aggregate_flow_electricity_purchased), rel=1e-3 + ) + == 3300 + ) + assert pytest.approx( + value(m.fs.costing.aggregate_flow_electricity), rel=1e-3 + ) == value( + m.fs.costing.aggregate_flow_electricity_purchased + - m.fs.costing.aggregate_flow_electricity_sold + ) + assert pytest.approx( + value(m.fs.costing.aggregate_flow_electricity), rel=1e-3 + ) == value( + m.fs.treatment.costing.aggregate_flow_electricity + + m.fs.energy.costing.aggregate_flow_electricity + ) + @pytest.mark.component def test_no_energy_treatment_block(): @@ -170,50 +624,80 @@ def test_no_energy_treatment_block(): @pytest.mark.component -def test_common_params_not_equivalent(): +def test_common_params_equivalent(): - m = ConcreteModel() - m.fs = FlowsheetBlock(dynamic=False) - m.fs.properties = SeawaterParameterBlock() + m = build_default() - m.fs.treatment = Block() - m.fs.treatment.costing = TreatmentCosting() - m.fs.treatment.unit = DummyTreatmentUnit(property_package=m.fs.properties) - - m.fs.energy = Block() - m.fs.energy.costing = EnergyCosting() - m.fs.energy.unit = DummyElectricityUnit() + m.fs.energy.costing.cost_process() + m.fs.treatment.costing.cost_process() m.fs.energy.costing.electricity_cost.fix(0.02) + # raise error when electricity costs aren't equivalent + with pytest.raises( ValueError, match="The common costing parameter electricity_cost was found to " - "have a different value on the energy \\(0\\.02\\) and treatment \\(0\\.0\\) costing " - "blocks\\. Common costing parameters must be equivalent across all" + "have a different value on the energy and treatment costing blocks\\. " + "Common costing parameters must be equivalent across all" " costing blocks to use REFLOSystemCosting\\.", ): m.fs.costing = REFLOSystemCosting() - m = ConcreteModel() - m.fs = FlowsheetBlock(dynamic=False) - m.fs.properties = SeawaterParameterBlock() - - m.fs.treatment = Block() - m.fs.treatment.costing = TreatmentCosting() - m.fs.treatment.unit = DummyTreatmentUnit(property_package=m.fs.properties) - - m.fs.energy = Block() - m.fs.energy.costing = EnergyCosting() - m.fs.energy.unit = DummyElectricityUnit() + m = build_default() m.fs.energy.costing.electricity_cost.fix(0.02) m.fs.treatment.costing.electricity_cost.fix(0.02) + m.fs.energy.costing.cost_process() + m.fs.treatment.costing.cost_process() + m.fs.costing = REFLOSystemCosting() + m.fs.costing.cost_process() + + # when they are equivalent, assert equivalency across all three costing packages + + assert value(m.fs.costing.electricity_cost) == value( + m.fs.treatment.costing.electricity_cost + ) + assert value(m.fs.costing.electricity_cost) == value( + m.fs.energy.costing.electricity_cost + ) + + m = build_default() + + m.fs.treatment.costing.base_currency = pyunits.USD_2011 + + m.fs.energy.costing.cost_process() + m.fs.treatment.costing.cost_process() + + # raise error when base currency isn't equivalent + + with pytest.raises( + ValueError, + match="The common costing parameter base_currency was found to " + "have a different value on the energy and treatment costing blocks\\. " + "Common costing parameters must be equivalent across all" + " costing blocks to use REFLOSystemCosting\\.", + ): + m.fs.costing = REFLOSystemCosting() + + m = build_default() + + m.fs.treatment.costing.base_currency = pyunits.USD_2011 + m.fs.energy.costing.base_currency = pyunits.USD_2011 + + m.fs.energy.costing.cost_process() + m.fs.treatment.costing.cost_process() + + m.fs.costing = REFLOSystemCosting() + m.fs.costing.cost_process() + + # when they are equivalent, assert equivalency across all three costing packages - # assert value(m.fs.costing.electricity_cost) == value(m.fs.treatment.electricity_cost) - # assert value(m.fs.costing.electricity_cost) == value(m.fs.energy.electricity_cost) + assert m.fs.costing.base_currency is pyunits.USD_2011 + assert m.fs.treatment.costing.base_currency is pyunits.USD_2011 + assert m.fs.energy.costing.base_currency is pyunits.USD_2011 @pytest.mark.component diff --git a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py index 21ad6eb6..76397dda 100644 --- a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py +++ b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py @@ -27,7 +27,7 @@ from watertap_contrib.reflo.solar_models.surrogate.pv.pv_surrogate import ( PVSurrogateData, ) -from watertap_contrib.reflo.costing.tests.costing_dummy_units import ( +from watertap_contrib.reflo.costing.tests.dummy_costing_units import ( DummyElectricityUnit, ) @@ -151,16 +151,16 @@ def build_global_params(self): self.heat_cost_buy = pyo.Param( mutable=True, - initialize=0.07, + initialize=0.01, doc="Heat cost to buy", - units=pyo.units.USD_2018 / pyo.units.kWh, + units=pyo.units.USD_2021 / pyo.units.kWh, ) self.heat_cost_sell = pyo.Param( mutable=True, - initialize=0.05, + initialize=0.01, doc="Heat cost to sell", - units=pyo.units.USD_2018 / pyo.units.kWh, + units=pyo.units.USD_2021 / pyo.units.kWh, ) # Build the integrated system costs @@ -253,7 +253,6 @@ def build_integrated_costs(self): units=pyo.units.kW, ) - self.total_capital_cost_constraint = pyo.Constraint( expr=self.total_capital_cost == pyo.units.convert( @@ -352,7 +351,7 @@ def build_integrated_costs(self): self.has_heat_flows = True self.aggregate_flow_heat_sold.fix(0) - + self.aggregate_heat_balance = pyo.Constraint( expr=( self.aggregate_flow_heat_purchased == treat_cost.aggregate_flow_heat @@ -421,10 +420,14 @@ def initialize_build(self): self.aggregate_electricity_balance, ) - if hasattr(self, "frac_elec_from_grid_constraint"): - calculate_variable_from_constraint( - self.frac_elec_from_grid, self.frac_elec_from_grid_constraint - ) + # Commented code remains as a PSA: + # If you send a fixed variable to calculate_variable_from_constraint, + # the variable will come out a different value but still be fixed! + + # if hasattr(self, "frac_elec_from_grid_constraint"): + # calculate_variable_from_constraint( + # self.frac_elec_from_grid, self.frac_elec_from_grid_constraint + # ) calculate_variable_from_constraint( self.total_electric_operating_cost, @@ -451,11 +454,6 @@ def initialize_build(self): self.aggregate_flow_heat_sold.fix(0) self.aggregate_heat_complement.deactivate() - calculate_variable_from_constraint( - self.frac_heat_from_grid, - self.frac_heat_from_grid_constraint, - ) - if not self.aggregate_flow_heat_purchased.is_fixed(): calculate_variable_from_constraint( self.aggregate_flow_heat_purchased, @@ -471,50 +469,53 @@ def initialize_build(self): self.aggregate_flow_heat_constraint, ) + if hasattr(self, "aggregate_heat_complement"): + + self.aggregate_flow_heat_sold.unfix() + self.aggregate_heat_complement.activate() + super().initialize_build() - + def calculate_scaling_factors(self): if get_scaling_factor(self.total_capital_cost) is None: set_scaling_factor(self.total_capital_cost, 1e-3) - + if get_scaling_factor(self.total_operating_cost) is None: set_scaling_factor(self.total_operating_cost, 1e-3) - + if get_scaling_factor(self.total_electric_operating_cost) is None: set_scaling_factor(self.total_electric_operating_cost, 1e-2) - + if get_scaling_factor(self.total_heat_operating_cost) is None: set_scaling_factor(self.total_heat_operating_cost, 1) - + if get_scaling_factor(self.aggregate_flow_electricity) is None: set_scaling_factor(self.aggregate_flow_electricity, 0.1) - + if get_scaling_factor(self.aggregate_flow_heat) is None: set_scaling_factor(self.aggregate_flow_heat, 0.1) - + if get_scaling_factor(self.aggregate_flow_electricity_purchased) is None: sf = get_scaling_factor(self.aggregate_flow_electricity) set_scaling_factor(self.aggregate_flow_electricity_purchased, sf) - + if get_scaling_factor(self.aggregate_flow_electricity_sold) is None: set_scaling_factor(self.aggregate_flow_electricity_sold, 1) - + if get_scaling_factor(self.aggregate_flow_heat_purchased) is None: sf = get_scaling_factor(self.aggregate_flow_heat) set_scaling_factor(self.aggregate_flow_heat_purchased, sf) - + if get_scaling_factor(self.aggregate_flow_electricity_sold) is None: set_scaling_factor(self.aggregate_flow_electricity_sold, 1) - + if get_scaling_factor(self.frac_elec_from_grid) is None: set_scaling_factor(self.frac_elec_from_grid, 1) - + if hasattr(self, "frac_heat_from_grid"): if get_scaling_factor(self.frac_heat_from_grid) is None: set_scaling_factor(self.frac_heat_from_grid, 1) - - def build_process_costs(self): """ From ac5f92a6f9ad7f8a47c054b9345bc05701ea8dcc Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Wed, 13 Nov 2024 15:39:02 -0700 Subject: [PATCH 52/70] heat gen only test --- .../costing/tests/dummy_costing_units.py | 2 +- .../test_reflo_watertap_costing_package.py | 309 +++++++++++++++--- 2 files changed, 263 insertions(+), 48 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/tests/dummy_costing_units.py b/src/watertap_contrib/reflo/costing/tests/dummy_costing_units.py index 4ea204e9..814e5bd0 100644 --- a/src/watertap_contrib/reflo/costing/tests/dummy_costing_units.py +++ b/src/watertap_contrib/reflo/costing/tests/dummy_costing_units.py @@ -449,7 +449,7 @@ def default_costing_method(self): def build_dummy_heat_unit_param_block(blk): blk.capital_per_watt = Var( - initialize=0.6, + initialize=0.15, units=pyunits.USD_2019 / pyunits.watt, bounds=(0, None), doc="Cost per watt", diff --git a/src/watertap_contrib/reflo/costing/tests/test_reflo_watertap_costing_package.py b/src/watertap_contrib/reflo/costing/tests/test_reflo_watertap_costing_package.py index c71fd504..4f192cbc 100644 --- a/src/watertap_contrib/reflo/costing/tests/test_reflo_watertap_costing_package.py +++ b/src/watertap_contrib/reflo/costing/tests/test_reflo_watertap_costing_package.py @@ -187,6 +187,72 @@ def build_electricity_gen_only_no_heat(): return m +def build_heat_gen_only(): + """ + Test flowsheet with only heat generation unit on energy block. + The treatment unit consumes both heat and electricity. + """ + + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + m.fs.properties = SeawaterParameterBlock() + + #### TREATMENT BLOCK + m.fs.treatment = Block() + m.fs.treatment.costing = TreatmentCosting() + + m.fs.treatment.unit = DummyTreatmentUnit(property_package=m.fs.properties) + m.fs.treatment.unit.costing = UnitModelCostingBlock( + flowsheet_costing_block=m.fs.treatment.costing + ) + + m.fs.treatment.unit.design_var_a.fix() + m.fs.treatment.unit.design_var_b.fix() + m.fs.treatment.unit.electricity_consumption.fix(1000) + m.fs.treatment.unit.heat_consumption.fix(25000) + m.fs.treatment.costing.cost_process() + + #### ENERGY BLOCK + m.fs.energy = Block() + m.fs.energy.costing = EnergyCosting() + m.fs.energy.unit = DummyHeatUnit() + m.fs.energy.unit.costing = UnitModelCostingBlock( + flowsheet_costing_block=m.fs.energy.costing + ) + m.fs.energy.unit.heat.fix(70) + m.fs.energy.costing.cost_process() + + #### SYSTEM COSTING + m.fs.costing = REFLOSystemCosting() + m.fs.costing.cost_process() + m.fs.treatment.costing.add_LCOW( + m.fs.treatment.unit.properties[0].flow_vol_phase["Liq"] + ) + + #### SCALING + 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", "TDS") + ) + calculate_scaling_factors(m) + + #### INITIALIZE + + m.fs.treatment.unit.properties.calculate_state( + var_args={ + ("flow_vol_phase", "Liq"): 0.4381, + ("conc_mass_phase_comp", ("Liq", "TDS")): 20, + ("temperature", None): 293, + ("pressure", None): 101325, + }, + hold_state=True, + ) + + return m + + def build_default(): m = ConcreteModel() m.fs = FlowsheetBlock(dynamic=False) @@ -201,7 +267,7 @@ def build_default(): m.fs.energy = Block() m.fs.energy.costing = EnergyCosting() - m.fs.energy.unit = DummyElectricityUnit() + m.fs.energy.unit = DummyHeatUnit() m.fs.energy.unit.costing = UnitModelCostingBlock( flowsheet_costing_block=m.fs.energy.costing ) @@ -235,6 +301,10 @@ def test_default_build(self, default_build): assert not hasattr(m.fs.treatment.costing, "case_study_def") assert not hasattr(m.fs.energy.costing, "case_study_def") + assert hasattr(m.fs.energy.costing, "has_electricity_generation") + assert not m.fs.energy.costing.has_electricity_generation + assert not hasattr(m.fs.treatment.costing, "has_electricity_generation") + assert m.fs.treatment.costing.base_currency is pyunits.USD_2021 assert m.fs.energy.costing.base_currency is pyunits.USD_2021 assert m.fs.costing.base_currency is pyunits.USD_2021 @@ -293,13 +363,16 @@ def test_build(slef, energy_gen_only_with_heat): assert degrees_of_freedom(m) == 0 - # still have heat flows + # have heat flows assert m.fs.costing.has_heat_flows assert not m.fs.costing.aggregate_flow_heat.is_fixed() + assert not m.fs.costing.aggregate_flow_heat_purchased.is_fixed() + # no heat generated so nothing to sell + assert m.fs.costing.aggregate_flow_heat_sold.is_fixed() assert m.fs.energy.costing.has_electricity_generation assert hasattr(m.fs.costing, "frac_elec_from_grid_constraint") - assert not hasattr(m.fs.costing, "frac_heat_from_grid") + assert not hasattr(m.fs.costing, "aggregate_heat_complement") @pytest.mark.component def test_init_and_solve(self, energy_gen_only_with_heat): @@ -310,6 +383,13 @@ def test_init_and_solve(self, energy_gen_only_with_heat): m.fs.energy.costing.initialize() m.fs.costing.initialize() + # check state after initialization + assert degrees_of_freedom(m) == 0 + + assert (m.fs.costing.aggregate_flow_heat_sold.is_fixed()) and ( + value(m.fs.costing.aggregate_flow_heat_sold) == 0 + ) + results = solver.solve(m) assert_optimal_termination(results) @@ -364,6 +444,8 @@ def test_optimize_frac_from_grid(self): m.fs.energy.costing.initialize() m.fs.costing.initialize() + assert degrees_of_freedom(m) == 0 + results = solver.solve(m) assert_optimal_termination(results) @@ -409,18 +491,33 @@ def test_build(slef, energy_gen_only_no_heat): assert m.fs.costing.aggregate_flow_heat_sold.is_fixed() assert m.fs.energy.costing.has_electricity_generation assert hasattr(m.fs.costing, "frac_elec_from_grid_constraint") - assert not hasattr(m.fs.costing, "frac_heat_from_grid") @pytest.mark.component def test_init_and_solve(self, energy_gen_only_no_heat): m = energy_gen_only_no_heat + # constraints are active before initialization + assert m.fs.costing.total_heat_operating_cost_constraint.active + assert m.fs.costing.aggregate_flow_heat_constraint.active + m.fs.treatment.unit.initialize() m.fs.treatment.costing.initialize() m.fs.energy.costing.initialize() m.fs.costing.initialize() + # check state after initialization + assert degrees_of_freedom(m) == 0 + + assert (m.fs.costing.total_heat_operating_cost.is_fixed()) and ( + value(m.fs.costing.total_heat_operating_cost) == 0 + ) + assert (m.fs.costing.aggregate_flow_heat.is_fixed()) and ( + value(m.fs.costing.aggregate_flow_heat) == 0 + ) + assert not m.fs.costing.total_heat_operating_cost_constraint.active + assert not m.fs.costing.aggregate_flow_heat_constraint.active + results = solver.solve(m) assert_optimal_termination(results) @@ -496,40 +593,46 @@ def test_optimize_frac_from_grid(self): ) -class TestElectricityHeatGen: - +class TestHeatGenOnly: @pytest.fixture(scope="class") - def energy_gen_only_no_heat(self): + def heat_gen_only(self): - m = build_electricity_gen_only_no_heat() + m = build_heat_gen_only() return m @pytest.mark.unit - def test_build(slef, energy_gen_only_no_heat): + def test_build(self, heat_gen_only): - m = energy_gen_only_no_heat + m = heat_gen_only assert degrees_of_freedom(m) == 0 - # no heat flows - assert not m.fs.costing.has_heat_flows - assert m.fs.costing.aggregate_flow_heat_purchased.is_fixed() - assert m.fs.costing.aggregate_flow_heat_sold.is_fixed() - assert m.fs.energy.costing.has_electricity_generation - assert hasattr(m.fs.costing, "frac_elec_from_grid_constraint") - - assert not hasattr(m.fs.costing, "frac_heat_from_grid") + # has heat flows, no electricity generation + assert m.fs.costing.has_heat_flows + assert not m.fs.costing.aggregate_flow_heat_purchased.is_fixed() + assert not m.fs.costing.aggregate_flow_heat_sold.is_fixed() + assert not m.fs.energy.costing.has_electricity_generation + assert not hasattr(m.fs.costing, "frac_elec_from_grid_constraint") + assert m.fs.costing.frac_elec_from_grid.is_fixed() + assert hasattr(m.fs.costing, "frac_heat_from_grid") + assert hasattr(m.fs.costing, "frac_heat_from_grid_constraint") + assert hasattr(m.fs.costing, "aggregate_heat_complement") @pytest.mark.component - def test_init_and_solve(self, energy_gen_only_no_heat): - m = energy_gen_only_no_heat + def test_init_and_solve(self, heat_gen_only): + + m = heat_gen_only m.fs.treatment.unit.initialize() m.fs.treatment.costing.initialize() m.fs.energy.costing.initialize() m.fs.costing.initialize() + assert degrees_of_freedom(m) == 0 + assert not m.fs.costing.aggregate_flow_heat_sold.is_fixed() + assert not m.fs.costing.aggregate_flow_heat_purchased.is_fixed() + results = solver.solve(m) assert_optimal_termination(results) @@ -538,42 +641,45 @@ def test_init_and_solve(self, energy_gen_only_no_heat): pytest.approx(value(m.fs.costing.aggregate_flow_electricity_sold), rel=1e-3) == 1e-12 ) - - assert pytest.approx(value(m.fs.costing.frac_elec_from_grid), rel=1e-3) == 0.25 + # no heat is sold assert ( - pytest.approx( - value(m.fs.costing.aggregate_flow_electricity_purchased), rel=1e-3 - ) - == 2500 + pytest.approx(value(m.fs.costing.aggregate_flow_heat_sold), rel=1e-3) + == 1e-12 ) + # all electricity comes from grid, none is generated + assert pytest.approx(value(m.fs.costing.frac_elec_from_grid), rel=1e-3) == 1 assert pytest.approx( - value(m.fs.costing.aggregate_flow_electricity), rel=1e-3 + value(m.fs.costing.aggregate_flow_electricity_purchased), rel=1e-3 ) == value( - m.fs.costing.aggregate_flow_electricity_purchased - - m.fs.costing.aggregate_flow_electricity_sold + m.fs.treatment.unit.electricity_consumption + m.fs.energy.unit.electricity ) + # both energy and treatment processes are consuming electricity assert pytest.approx( value(m.fs.costing.aggregate_flow_electricity), rel=1e-3 ) == value( - m.fs.treatment.costing.aggregate_flow_electricity - + m.fs.energy.costing.aggregate_flow_electricity + m.fs.treatment.unit.electricity_consumption + m.fs.energy.unit.electricity ) assert pytest.approx( - value(m.fs.costing.frac_elec_from_grid), rel=1e-3 - ) == 1 - value(m.fs.energy.unit.electricity) / value( - m.fs.treatment.unit.electricity_consumption + value(m.fs.costing.aggregate_flow_electricity), rel=1e-3 + ) == value(m.fs.costing.aggregate_flow_electricity_purchased) + assert pytest.approx( + value(m.fs.costing.frac_heat_from_grid), rel=1e-3 + ) == 1 - value(m.fs.energy.unit.heat / m.fs.treatment.unit.heat_consumption) + assert pytest.approx( + value(m.fs.costing.aggregate_flow_heat), rel=1e-3 + ) == value( + m.fs.treatment.costing.aggregate_flow_heat + * m.fs.costing.frac_heat_from_grid ) - # no heat is generated or consumed - assert pytest.approx(value(m.fs.costing.aggregate_flow_heat), rel=1e-3) == 0 @pytest.mark.component def test_optimize_frac_from_grid(self): - m = build_electricity_gen_only_no_heat() + m = build_heat_gen_only() - m.fs.energy.unit.electricity.unfix() - m.fs.costing.frac_elec_from_grid.fix(0.33) + m.fs.energy.unit.heat.unfix() + m.fs.costing.frac_heat_from_grid.fix(0.02) assert degrees_of_freedom(m) == 0 @@ -587,9 +693,15 @@ def test_optimize_frac_from_grid(self): assert ( pytest.approx( - value(m.fs.costing.aggregate_flow_electricity_purchased), rel=1e-3 + value(m.fs.costing.aggregate_flow_heat_purchased), rel=1e-3 ) - == 3300 + == 500 + ) + assert ( + pytest.approx( + value(m.fs.costing.aggregate_flow_heat), rel=1e-3 + ) + == 500 ) assert pytest.approx( value(m.fs.costing.aggregate_flow_electricity), rel=1e-3 @@ -597,12 +709,115 @@ def test_optimize_frac_from_grid(self): m.fs.costing.aggregate_flow_electricity_purchased - m.fs.costing.aggregate_flow_electricity_sold ) - assert pytest.approx( - value(m.fs.costing.aggregate_flow_electricity), rel=1e-3 - ) == value( - m.fs.treatment.costing.aggregate_flow_electricity - + m.fs.energy.costing.aggregate_flow_electricity - ) + + +# class TestElectricityHeatGen: + +# @pytest.fixture(scope="class") +# def energy_gen_only_no_heat(self): + +# m = build_electricity_gen_only_no_heat() + +# return m + +# @pytest.mark.unit +# def test_build(slef, energy_gen_only_no_heat): + +# m = energy_gen_only_no_heat + +# assert degrees_of_freedom(m) == 0 + +# # no heat flows +# assert not m.fs.costing.has_heat_flows +# assert m.fs.costing.aggregate_flow_heat_purchased.is_fixed() +# assert m.fs.costing.aggregate_flow_heat_sold.is_fixed() +# assert m.fs.energy.costing.has_electricity_generation +# assert hasattr(m.fs.costing, "frac_elec_from_grid_constraint") + +# assert not hasattr(m.fs.costing, "frac_heat_from_grid") + +# @pytest.mark.component +# def test_init_and_solve(self, energy_gen_only_no_heat): +# m = energy_gen_only_no_heat + +# m.fs.treatment.unit.initialize() +# m.fs.treatment.costing.initialize() +# m.fs.energy.costing.initialize() +# m.fs.costing.initialize() + +# results = solver.solve(m) +# assert_optimal_termination(results) + +# # no electricity is sold +# assert ( +# pytest.approx(value(m.fs.costing.aggregate_flow_electricity_sold), rel=1e-3) +# == 1e-12 +# ) + +# assert pytest.approx(value(m.fs.costing.frac_elec_from_grid), rel=1e-3) == 0.25 +# assert ( +# pytest.approx( +# value(m.fs.costing.aggregate_flow_electricity_purchased), rel=1e-3 +# ) +# == 2500 +# ) +# assert pytest.approx( +# value(m.fs.costing.aggregate_flow_electricity), rel=1e-3 +# ) == value( +# m.fs.costing.aggregate_flow_electricity_purchased +# - m.fs.costing.aggregate_flow_electricity_sold +# ) +# assert pytest.approx( +# value(m.fs.costing.aggregate_flow_electricity), rel=1e-3 +# ) == value( +# m.fs.treatment.costing.aggregate_flow_electricity +# + m.fs.energy.costing.aggregate_flow_electricity +# ) +# assert pytest.approx( +# value(m.fs.costing.frac_elec_from_grid), rel=1e-3 +# ) == 1 - value(m.fs.energy.unit.electricity) / value( +# m.fs.treatment.unit.electricity_consumption +# ) + +# # no heat is generated or consumed +# assert pytest.approx(value(m.fs.costing.aggregate_flow_heat), rel=1e-3) == 0 + +# @pytest.mark.component +# def test_optimize_frac_from_grid(self): + +# m = build_electricity_gen_only_no_heat() + +# m.fs.energy.unit.electricity.unfix() +# m.fs.costing.frac_elec_from_grid.fix(0.33) + +# assert degrees_of_freedom(m) == 0 + +# m.fs.treatment.unit.initialize() +# m.fs.treatment.costing.initialize() +# m.fs.energy.costing.initialize() +# m.fs.costing.initialize() + +# results = solver.solve(m) +# assert_optimal_termination(results) + +# assert ( +# pytest.approx( +# value(m.fs.costing.aggregate_flow_electricity_purchased), rel=1e-3 +# ) +# == 3300 +# ) +# assert pytest.approx( +# value(m.fs.costing.aggregate_flow_electricity), rel=1e-3 +# ) == value( +# m.fs.costing.aggregate_flow_electricity_purchased +# - m.fs.costing.aggregate_flow_electricity_sold +# ) +# assert pytest.approx( +# value(m.fs.costing.aggregate_flow_electricity), rel=1e-3 +# ) == value( +# m.fs.treatment.costing.aggregate_flow_electricity +# + m.fs.energy.costing.aggregate_flow_electricity +# ) @pytest.mark.component From d24c6451160529bcb5c2f9939711e89aa8b837d8 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Wed, 13 Nov 2024 16:07:13 -0700 Subject: [PATCH 53/70] add elec and heat generation tests --- .../test_reflo_watertap_costing_package.py | 325 +++++++++++------- 1 file changed, 208 insertions(+), 117 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/tests/test_reflo_watertap_costing_package.py b/src/watertap_contrib/reflo/costing/tests/test_reflo_watertap_costing_package.py index 4f192cbc..263ffdb0 100644 --- a/src/watertap_contrib/reflo/costing/tests/test_reflo_watertap_costing_package.py +++ b/src/watertap_contrib/reflo/costing/tests/test_reflo_watertap_costing_package.py @@ -253,6 +253,77 @@ def build_heat_gen_only(): return m +def build_heat_and_elec_gen(): + """ + Test flowsheet with both heat and electricity generation unit on energy block. + The heat generating unit also consumes electricity. + The treatment unit consumes both heat and electricity. + """ + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + m.fs.properties = SeawaterParameterBlock() + + #### TREATMENT BLOCK + m.fs.treatment = Block() + m.fs.treatment.costing = TreatmentCosting() + + m.fs.treatment.unit = DummyTreatmentUnit(property_package=m.fs.properties) + m.fs.treatment.unit.costing = UnitModelCostingBlock( + flowsheet_costing_block=m.fs.treatment.costing + ) + + m.fs.treatment.unit.design_var_a.fix() + m.fs.treatment.unit.design_var_b.fix() + m.fs.treatment.unit.electricity_consumption.fix(11000) + m.fs.treatment.unit.heat_consumption.fix(25000) + m.fs.treatment.costing.cost_process() + + #### ENERGY BLOCK + m.fs.energy = Block() + m.fs.energy.costing = EnergyCosting() + m.fs.energy.heat_unit = DummyHeatUnit() + m.fs.energy.elec_unit = DummyElectricityUnit() + m.fs.energy.heat_unit.costing = UnitModelCostingBlock( + flowsheet_costing_block=m.fs.energy.costing + ) + m.fs.energy.elec_unit.costing = UnitModelCostingBlock( + flowsheet_costing_block=m.fs.energy.costing + ) + m.fs.energy.heat_unit.heat.fix(5000) + m.fs.energy.elec_unit.electricity.fix(10000) + m.fs.energy.costing.cost_process() + + #### SYSTEM COSTING + m.fs.costing = REFLOSystemCosting() + m.fs.costing.cost_process() + m.fs.treatment.costing.add_LCOW( + m.fs.treatment.unit.properties[0].flow_vol_phase["Liq"] + ) + + #### SCALING + 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", "TDS") + ) + calculate_scaling_factors(m) + + #### INITIALIZE + + m.fs.treatment.unit.properties.calculate_state( + var_args={ + ("flow_vol_phase", "Liq"): 0.4381, + ("conc_mass_phase_comp", ("Liq", "TDS")): 20, + ("temperature", None): 293, + ("pressure", None): 101325, + }, + hold_state=True, + ) + + return m + + def build_default(): m = ConcreteModel() m.fs = FlowsheetBlock(dynamic=False) @@ -672,7 +743,6 @@ def test_init_and_solve(self, heat_gen_only): * m.fs.costing.frac_heat_from_grid ) - @pytest.mark.component def test_optimize_frac_from_grid(self): @@ -692,17 +762,10 @@ def test_optimize_frac_from_grid(self): assert_optimal_termination(results) assert ( - pytest.approx( - value(m.fs.costing.aggregate_flow_heat_purchased), rel=1e-3 - ) - == 500 - ) - assert ( - pytest.approx( - value(m.fs.costing.aggregate_flow_heat), rel=1e-3 - ) + pytest.approx(value(m.fs.costing.aggregate_flow_heat_purchased), rel=1e-3) == 500 ) + assert pytest.approx(value(m.fs.costing.aggregate_flow_heat), rel=1e-3) == 500 assert pytest.approx( value(m.fs.costing.aggregate_flow_electricity), rel=1e-3 ) == value( @@ -711,113 +774,141 @@ def test_optimize_frac_from_grid(self): ) -# class TestElectricityHeatGen: - -# @pytest.fixture(scope="class") -# def energy_gen_only_no_heat(self): - -# m = build_electricity_gen_only_no_heat() - -# return m - -# @pytest.mark.unit -# def test_build(slef, energy_gen_only_no_heat): - -# m = energy_gen_only_no_heat - -# assert degrees_of_freedom(m) == 0 - -# # no heat flows -# assert not m.fs.costing.has_heat_flows -# assert m.fs.costing.aggregate_flow_heat_purchased.is_fixed() -# assert m.fs.costing.aggregate_flow_heat_sold.is_fixed() -# assert m.fs.energy.costing.has_electricity_generation -# assert hasattr(m.fs.costing, "frac_elec_from_grid_constraint") - -# assert not hasattr(m.fs.costing, "frac_heat_from_grid") - -# @pytest.mark.component -# def test_init_and_solve(self, energy_gen_only_no_heat): -# m = energy_gen_only_no_heat - -# m.fs.treatment.unit.initialize() -# m.fs.treatment.costing.initialize() -# m.fs.energy.costing.initialize() -# m.fs.costing.initialize() - -# results = solver.solve(m) -# assert_optimal_termination(results) - -# # no electricity is sold -# assert ( -# pytest.approx(value(m.fs.costing.aggregate_flow_electricity_sold), rel=1e-3) -# == 1e-12 -# ) - -# assert pytest.approx(value(m.fs.costing.frac_elec_from_grid), rel=1e-3) == 0.25 -# assert ( -# pytest.approx( -# value(m.fs.costing.aggregate_flow_electricity_purchased), rel=1e-3 -# ) -# == 2500 -# ) -# assert pytest.approx( -# value(m.fs.costing.aggregate_flow_electricity), rel=1e-3 -# ) == value( -# m.fs.costing.aggregate_flow_electricity_purchased -# - m.fs.costing.aggregate_flow_electricity_sold -# ) -# assert pytest.approx( -# value(m.fs.costing.aggregate_flow_electricity), rel=1e-3 -# ) == value( -# m.fs.treatment.costing.aggregate_flow_electricity -# + m.fs.energy.costing.aggregate_flow_electricity -# ) -# assert pytest.approx( -# value(m.fs.costing.frac_elec_from_grid), rel=1e-3 -# ) == 1 - value(m.fs.energy.unit.electricity) / value( -# m.fs.treatment.unit.electricity_consumption -# ) - -# # no heat is generated or consumed -# assert pytest.approx(value(m.fs.costing.aggregate_flow_heat), rel=1e-3) == 0 - -# @pytest.mark.component -# def test_optimize_frac_from_grid(self): - -# m = build_electricity_gen_only_no_heat() - -# m.fs.energy.unit.electricity.unfix() -# m.fs.costing.frac_elec_from_grid.fix(0.33) - -# assert degrees_of_freedom(m) == 0 - -# m.fs.treatment.unit.initialize() -# m.fs.treatment.costing.initialize() -# m.fs.energy.costing.initialize() -# m.fs.costing.initialize() - -# results = solver.solve(m) -# assert_optimal_termination(results) - -# assert ( -# pytest.approx( -# value(m.fs.costing.aggregate_flow_electricity_purchased), rel=1e-3 -# ) -# == 3300 -# ) -# assert pytest.approx( -# value(m.fs.costing.aggregate_flow_electricity), rel=1e-3 -# ) == value( -# m.fs.costing.aggregate_flow_electricity_purchased -# - m.fs.costing.aggregate_flow_electricity_sold -# ) -# assert pytest.approx( -# value(m.fs.costing.aggregate_flow_electricity), rel=1e-3 -# ) == value( -# m.fs.treatment.costing.aggregate_flow_electricity -# + m.fs.energy.costing.aggregate_flow_electricity -# ) +class TestElectricityAndHeatGen: + + @pytest.fixture(scope="class") + def heat_and_elec_gen(self): + + m = build_heat_and_elec_gen() + + return m + + @pytest.mark.unit + def test_build(slef, heat_and_elec_gen): + + m = heat_and_elec_gen + + assert degrees_of_freedom(m) == 0 + + # has heat and electricity flows + assert m.fs.costing.has_heat_flows + assert not m.fs.costing.aggregate_flow_heat_purchased.is_fixed() + assert not m.fs.costing.aggregate_flow_heat_sold.is_fixed() + assert not m.fs.costing.aggregate_flow_electricity_purchased.is_fixed() + assert not m.fs.costing.aggregate_flow_electricity_sold.is_fixed() + assert m.fs.energy.costing.has_electricity_generation + assert hasattr(m.fs.costing, "frac_elec_from_grid_constraint") + assert not m.fs.costing.frac_elec_from_grid.is_fixed() + assert hasattr(m.fs.costing, "frac_heat_from_grid") + assert hasattr(m.fs.costing, "frac_heat_from_grid_constraint") + assert hasattr(m.fs.costing, "aggregate_heat_complement") + + @pytest.mark.component + def test_init_and_solve(self, heat_and_elec_gen): + m = heat_and_elec_gen + + m.fs.treatment.unit.initialize() + m.fs.treatment.costing.initialize() + m.fs.energy.costing.initialize() + m.fs.costing.initialize() + + assert degrees_of_freedom(m) == 0 + + assert not m.fs.costing.aggregate_flow_heat_sold.is_fixed() + assert m.fs.costing.aggregate_heat_complement.active + + results = solver.solve(m) + assert_optimal_termination(results) + + # no electricity is sold + assert ( + pytest.approx(value(m.fs.costing.aggregate_flow_electricity_sold), rel=1e-3) + == 1e-12 + ) + # no heat is sold + assert ( + pytest.approx(value(m.fs.costing.aggregate_flow_heat_sold), rel=1e-3) + == 1e-12 + ) + + assert pytest.approx( + value(m.fs.costing.aggregate_flow_electricity), rel=1e-3 + ) == value( + m.fs.treatment.costing.aggregate_flow_electricity + + m.fs.energy.costing.aggregate_flow_electricity + ) + # fraction from grid is generated electricity from elec_unit + # over consumed electricity from heat_unit and treatment unit + assert pytest.approx( + value(m.fs.costing.frac_elec_from_grid), rel=1e-3 + ) == 1 - value( + m.fs.energy.elec_unit.electricity + / ( + m.fs.treatment.unit.electricity_consumption + + m.fs.energy.heat_unit.electricity + ) + ) + # two equivalent ways to calculate fraction heat from grid + assert pytest.approx( + value(m.fs.costing.frac_heat_from_grid), rel=1e-3 + ) == 1 - value( + m.fs.energy.heat_unit.heat / m.fs.treatment.unit.heat_consumption + ) + assert pytest.approx( + value(m.fs.costing.frac_heat_from_grid), rel=1e-3 + ) == 1 - value( + -1 + * m.fs.energy.costing.aggregate_flow_heat + / m.fs.treatment.costing.aggregate_flow_heat + ) + + @pytest.mark.component + def test_optimize_frac_from_grid(self): + + m = build_heat_and_elec_gen() + + m.fs.energy.elec_unit.electricity.unfix() + m.fs.energy.heat_unit.heat.unfix() + + m.fs.costing.frac_elec_from_grid.fix(0.99) + m.fs.costing.frac_heat_from_grid.fix(0.85) + + assert degrees_of_freedom(m) == 0 + + m.fs.treatment.unit.initialize() + m.fs.treatment.costing.initialize() + m.fs.energy.costing.initialize() + m.fs.costing.initialize() + + assert degrees_of_freedom(m) == 0 + + results = solver.solve(m) + assert_optimal_termination(results) + + # fraction from grid is generated electricity from elec_unit + # over consumed electricity from heat_unit and treatment unit + assert pytest.approx( + value(m.fs.costing.frac_elec_from_grid), rel=1e-3 + ) == 1 - value( + m.fs.energy.elec_unit.electricity + / ( + m.fs.treatment.unit.electricity_consumption + + m.fs.energy.heat_unit.electricity + ) + ) + # two equivalent ways to calculate fraction heat from grid + assert pytest.approx( + value(m.fs.costing.frac_heat_from_grid), rel=1e-3 + ) == 1 - value( + m.fs.energy.heat_unit.heat / m.fs.treatment.unit.heat_consumption + ) + assert pytest.approx( + value(m.fs.costing.frac_heat_from_grid), rel=1e-3 + ) == 1 - value( + -1 + * m.fs.energy.costing.aggregate_flow_heat + / m.fs.treatment.costing.aggregate_flow_heat + ) @pytest.mark.component From 63d097f73ab5fd6b61305832a5e52a56ec0063d2 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 14 Nov 2024 12:02:08 -0700 Subject: [PATCH 54/70] add LCOE --- .../costing/watertap_reflo_costing_package.py | 86 ++++++++++++++++++- 1 file changed, 85 insertions(+), 1 deletion(-) diff --git a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py index 76397dda..ad76a86a 100644 --- a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py +++ b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py @@ -112,15 +112,99 @@ def build_process_costs(self): @declare_process_block_class("EnergyCosting") class EnergyCostingData(REFLOCostingData): def build_global_params(self): + super().build_global_params() + # If creating an energy unit that generates electricity, # set this flag to True in costing package. # See PV costing package for example. self.has_electricity_generation = False - super().build_global_params() + + self.base_energy_units = pyo.units.kilowatt * pyo.units.hour + + self.annual_system_degradation = pyo.Param( + initialize=0.005, + mutable=True, + units=pyo.units.dimensionless, + doc="Yearly performance degradation of electric energy system", + ) + + self.plant_lifetime_set = pyo.Set( + initialize=range(pyo.value(self.plant_lifetime) + 1) + ) + + self.yearly_electricity_production = pyo.Var( + self.plant_lifetime_set, + initialize=1e4, + domain=pyo.NonNegativeReals, + units=pyo.units.kilowatt * pyo.units.hour, + ) + + self.lifetime_electricity_production = pyo.Var( + initialize=1e6, + domain=pyo.NonNegativeReals, + units=pyo.units.kilowatt * pyo.units.hour, + ) def build_process_costs(self): super().build_process_costs() + def build_LCOE_params(self): + + def rule_yearly_electricity_production(b, y): + if y == 0: + return b.yearly_electricity_production[y] == pyo.units.convert( + self.aggregate_flow_electricity * -1 * pyo.units.year, + to_units=pyo.units.kilowatt * pyo.units.hour, + ) + else: + return b.yearly_electricity_production[ + y + ] == b.yearly_electricity_production[y - 1] * ( + 1 - b.annual_system_degradation + ) + + self.yearly_electricity_production_constraint = pyo.Constraint( + self.plant_lifetime_set, rule=rule_yearly_electricity_production + ) + + def rule_lifetime_electricity_production(b): + return ( + b.lifetime_electricity_production + == sum(b.yearly_electricity_production[y] for y in b.plant_lifetime_set) + * b.utilization_factor + ) + + self.lifetime_electricity_production_constraint = pyo.Constraint( + rule=rule_lifetime_electricity_production + ) + + def add_LCOE(self, name="LCOE"): + """ + Add Levelized Cost of Energy (LCOE) to costing block. + """ + + # https://www.nrel.gov/analysis/tech-lcoe-documentation.html + + self.build_LCOE_params() + + numerator = pyo.units.convert( + ( + self.total_capital_cost * self.capital_recovery_factor + + self.aggregate_fixed_operating_cost + ) + * self.plant_lifetime, + to_units=self.base_currency, + ) + + LCOE_expr = pyo.Expression( + expr=pyo.units.convert( + numerator / self.lifetime_electricity_production, + to_units=self.base_currency / self.base_energy_units, + ) + ) + + self.add_component(name, LCOE_expr) + @declare_process_block_class("REFLOSystemCosting") class REFLOSystemCostingData(WaterTAPCostingBlockData): From 0d31f825d31c30abecffa27178aac554868adaf8 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 14 Nov 2024 12:33:22 -0700 Subject: [PATCH 55/70] add_object_reference to LCOE on REFLOSystemCosting --- .../costing/watertap_reflo_costing_package.py | 45 ++++--------------- 1 file changed, 8 insertions(+), 37 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py index ad76a86a..1f756045 100644 --- a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py +++ b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py @@ -16,6 +16,7 @@ from idaes.core import declare_process_block_class from idaes.core.util.scaling import get_scaling_factor, set_scaling_factor +from idaes.core.util.misc import add_object_reference from watertap.costing.watertap_costing_package import ( WaterTAPCostingData, @@ -178,7 +179,7 @@ def rule_lifetime_electricity_production(b): rule=rule_lifetime_electricity_production ) - def add_LCOE(self, name="LCOE"): + def add_LCOE(self): """ Add Levelized Cost of Energy (LCOE) to costing block. """ @@ -203,7 +204,7 @@ def add_LCOE(self, name="LCOE"): ) ) - self.add_component(name, LCOE_expr) + self.add_component("LCOE", LCOE_expr) @declare_process_block_class("REFLOSystemCosting") @@ -636,46 +637,16 @@ def add_LCOW(self, flow_rate, name="LCOW"): ) self.add_component(name + "_constraint", LCOW_constraint) - def add_LCOE(self, e_model="pysam"): + def add_LCOE(self): """ Add Levelized Cost of Energy (LCOE) to costing block. - Args: - e_model - energy modeling approach used (PySAM or surrogate) """ - if e_model == "pysam": - pysam = self._get_pysam() - - if not pysam._has_been_run: - raise RuntimeError( - f"PySAM model {pysam._pysam_model_name} has not yet been run, so there is no annual_energy data available." - "You must run the PySAM model before adding LCOE metric." - ) - - energy_cost = self._get_energy_cost_block() - - self.annual_energy_generated = pyo.Param( - initialize=pysam.annual_energy, - units=pyo.units.kWh / pyo.units.year, - doc=f"Annual energy generated by {pysam._pysam_model_name}", - ) - LCOE_expr = pyo.Expression( - expr=( - energy_cost.total_capital_cost * self.capital_recovery_factor - + ( - energy_cost.aggregate_fixed_operating_cost - + energy_cost.aggregate_variable_operating_cost - ) - ) - / self.annual_energy_generated - * self.utilization_factor - ) - self.add_component("LCOE", LCOE_expr) + energy_cost = self._get_energy_cost_block() + if not hasattr(energy_cost, "LCOE"): + energy_cost.add_LCOE() - else: - raise NotImplementedError( - "add_LCOE for surrogate models not available yet." - ) + add_object_reference(self, "LCOE", energy_cost.LCOE) def add_specific_electric_energy_consumption(self, flow_rate): """ From 67636bf00143bcaad9cb7377a0c5f6621ab5feb7 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 14 Nov 2024 13:21:02 -0700 Subject: [PATCH 56/70] add SEEC, STEC, LCOH; scaling --- .../costing/watertap_reflo_costing_package.py | 216 ++++++++++++++---- 1 file changed, 174 insertions(+), 42 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py index 1f756045..af4d955e 100644 --- a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py +++ b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py @@ -109,6 +109,68 @@ def build_global_params(self): def build_process_costs(self): super().build_process_costs() + def add_specific_electric_energy_consumption( + self, flow_rate, name="specific_electric_energy_consumption" + ): + """ + Add specific electric energy consumption (kWh/m**3) to costing block. + Args: + flow_rate - flow rate of water (volumetric) to be used in + calculating specific electric energy consumption + """ + + specific_electric_energy_consumption = pyo.Var( + initialize=1, + units=pyo.units.kilowatt * pyo.units.hr * pyo.units.m**-3, + doc=f"Specific electric energy consumption based on flow {flow_rate.name}", + ) + + self.add_component(name, specific_electric_energy_consumption) + + specific_electric_energy_consumption_constraint = pyo.Constraint( + expr=specific_electric_energy_consumption + == pyo.units.convert( + self.aggregate_flow_electricity / flow_rate, + to_units=pyo.units.kilowatt * pyo.units.hr * pyo.units.m**-3, + ) + ) + + self.add_component( + "specific_electric_energy_consumption_constraint", + specific_electric_energy_consumption_constraint, + ) + + def add_specific_thermal_energy_consumption( + self, flow_rate, name="specific_thermal_energy_consumption" + ): + """ + Add specific thermal energy consumption (kWh/m**3) to costing block. + Args: + flow_rate - flow rate of water (volumetric) to be used in + calculating specific thermal energy consumption + """ + + specific_thermal_energy_consumption = pyo.Var( + initialize=1, + units=pyo.units.kilowatt * pyo.units.hr * pyo.units.m**-3, + doc=f"Specific thermal energy consumption based on flow {flow_rate.name}", + ) + + self.add_component(name, specific_thermal_energy_consumption) + + specific_thermal_energy_consumption_constraint = pyo.Constraint( + expr=specific_thermal_energy_consumption + == pyo.units.convert( + self.aggregate_flow_heat / flow_rate, + to_units=pyo.units.kilowatt * pyo.units.hr * pyo.units.m**-3, + ) + ) + + self.add_component( + "specific_thermal_energy_consumption_constraint", + specific_thermal_energy_consumption_constraint, + ) + @declare_process_block_class("EnergyCosting") class EnergyCostingData(REFLOCostingData): @@ -122,7 +184,14 @@ def build_global_params(self): self.base_energy_units = pyo.units.kilowatt * pyo.units.hour - self.annual_system_degradation = pyo.Param( + self.annual_electrical_system_degradation = pyo.Param( + initialize=0.005, + mutable=True, + units=pyo.units.dimensionless, + doc="Yearly performance degradation of electric energy system", + ) + + self.annual_heat_system_degradation = pyo.Param( initialize=0.005, mutable=True, units=pyo.units.dimensionless, @@ -146,6 +215,19 @@ def build_global_params(self): units=pyo.units.kilowatt * pyo.units.hour, ) + self.yearly_heat_production = pyo.Var( + self.plant_lifetime_set, + initialize=1e4, + domain=pyo.NonNegativeReals, + units=pyo.units.kilowatt * pyo.units.hour, + ) + + self.lifetime_heat_production = pyo.Var( + initialize=1e6, + domain=pyo.NonNegativeReals, + units=pyo.units.kilowatt * pyo.units.hour, + ) + def build_process_costs(self): super().build_process_costs() @@ -161,7 +243,7 @@ def rule_yearly_electricity_production(b, y): return b.yearly_electricity_production[ y ] == b.yearly_electricity_production[y - 1] * ( - 1 - b.annual_system_degradation + 1 - b.annual_electrical_system_degradation ) self.yearly_electricity_production_constraint = pyo.Constraint( @@ -179,6 +261,46 @@ def rule_lifetime_electricity_production(b): rule=rule_lifetime_electricity_production ) + if get_scaling_factor(self.yearly_electricity_production) is None: + set_scaling_factor(self.yearly_electricity_production, 1e-4) + + if get_scaling_factor(self.lifetime_electricity_production) is None: + set_scaling_factor(self.lifetime_electricity_production, 1e-4) + + def build_LCOH_params(self): + + def rule_yearly_heat_production(b, y): + if y == 0: + return b.yearly_heat_production[y] == pyo.units.convert( + self.aggregate_flow_heat * -1 * pyo.units.year, + to_units=pyo.units.kilowatt * pyo.units.hour, + ) + else: + return b.yearly_heat_production[y] == b.yearly_heat_production[ + y - 1 + ] * (1 - b.annual_heat_system_degradation) + + self.yearly_heat_production_constraint = pyo.Constraint( + self.plant_lifetime_set, rule=rule_yearly_heat_production + ) + + def rule_lifetime_heat_production(b): + return ( + b.lifetime_heat_production + == sum(b.yearly_heat_production[y] for y in b.plant_lifetime_set) + * b.utilization_factor + ) + + self.lifetime_heat_production_constraint = pyo.Constraint( + rule=rule_lifetime_heat_production + ) + + if get_scaling_factor(self.yearly_heat_production) is None: + set_scaling_factor(self.yearly_heat_production, 1e-4) + + if get_scaling_factor(self.lifetime_heat_production) is None: + set_scaling_factor(self.lifetime_heat_production, 1e-4) + def add_LCOE(self): """ Add Levelized Cost of Energy (LCOE) to costing block. @@ -206,6 +328,33 @@ def add_LCOE(self): self.add_component("LCOE", LCOE_expr) + def add_LCOH(self): + """ + Add Levelized Cost of Heat (LCOH) to costing block. + """ + + # https://www.nrel.gov/analysis/tech-lcoe-documentation.html + + self.build_LCOH_params() + + numerator = pyo.units.convert( + ( + self.total_capital_cost * self.capital_recovery_factor + + self.aggregate_fixed_operating_cost + ) + * self.plant_lifetime, + to_units=self.base_currency, + ) + + LCOH_expr = pyo.Expression( + expr=pyo.units.convert( + numerator / self.lifetime_heat_production, + to_units=self.base_currency / self.base_energy_units, + ) + ) + + self.add_component("LCOH", LCOH_expr) + @declare_process_block_class("REFLOSystemCosting") class REFLOSystemCostingData(WaterTAPCostingBlockData): @@ -648,61 +797,44 @@ def add_LCOE(self): add_object_reference(self, "LCOE", energy_cost.LCOE) - def add_specific_electric_energy_consumption(self, flow_rate): + def add_LCOH(self): + """ + Add Levelized Cost of Heat (LCOH) to costing block. + """ + + energy_cost = self._get_energy_cost_block() + if not hasattr(energy_cost, "LCOH"): + energy_cost.add_LCOH() + + add_object_reference(self, "LCOH", energy_cost.LCOH) + + def add_specific_electric_energy_consumption(self, *args, **kwargs): """ Add specific electric energy consumption (kWh/m**3) to costing block. Args: flow_rate - flow rate of water (volumetric) to be used in - calculating specific energy consumption + calculating specific electric energy consumption """ + treat_cost = self._get_treatment_cost_block() - specific_electric_energy_consumption = pyo.Var( - initialize=100, - doc=f"Specific electric energy consumption based on flow {flow_rate.name}", - ) - - self.add_component( - "specific_electric_energy_consumption", specific_electric_energy_consumption - ) - - specific_electric_energy_consumption_constraint = pyo.Constraint( - expr=specific_electric_energy_consumption - == self.aggregate_flow_electricity - / pyo.units.convert(flow_rate, to_units=pyo.units.m**3 / pyo.units.hr) - ) + if not hasattr(treat_cost, "specific_electric_energy_consumption_constraint"): + treat_cost.add_specific_electric_energy_consumption(*args, **kwargs) - self.add_component( - "specific_electric_energy_consumption_constraint", - specific_electric_energy_consumption_constraint, - ) + add_object_reference(self, kwargs["name"], getattr(treat_cost, kwargs["name"])) - def add_specific_thermal_energy_consumption(self, flow_rate): + def add_specific_thermal_energy_consumption(self, *args, **kwargs): """ Add specific thermal energy consumption (kWh/m**3) to costing block. Args: flow_rate - flow rate of water (volumetric) to be used in - calculating specific energy consumption + calculating specific thermal energy consumption """ + treat_cost = self._get_treatment_cost_block() - specific_thermal_energy_consumption = pyo.Var( - initialize=100, - doc=f"Specific thermal energy consumption based on flow {flow_rate.name}", - ) - - self.add_component( - "specific_thermal_energy_consumption", specific_thermal_energy_consumption - ) - - specific_thermal_energy_consumption_constraint = pyo.Constraint( - expr=specific_thermal_energy_consumption - == self.aggregate_flow_heat - / pyo.units.convert(flow_rate, to_units=pyo.units.m**3 / pyo.units.hr) - ) + if not hasattr(treat_cost, "specific_thermal_energy_consumption_constraint"): + treat_cost.add_specific_thermal_energy_consumption(*args, **kwargs) - self.add_component( - "specific_thermal_energy_consumption_constraint", - specific_thermal_energy_consumption_constraint, - ) + add_object_reference(self, kwargs["name"], getattr(treat_cost, kwargs["name"])) def _check_common_param_equivalence(self, treat_cost, energy_cost): """ From 1f18dcd9935290f60ffbc4a34ffbf747c3bd0163 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 14 Nov 2024 14:00:16 -0700 Subject: [PATCH 57/70] add LCOT, LCOW to REFLOSystemCosting; can't add LCOW to EnergyCosting --- .../costing/watertap_reflo_costing_package.py | 69 +++++++++++++------ 1 file changed, 48 insertions(+), 21 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py index af4d955e..7dd6e835 100644 --- a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py +++ b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py @@ -184,6 +184,10 @@ def build_global_params(self): self.base_energy_units = pyo.units.kilowatt * pyo.units.hour + self.plant_lifetime_set = pyo.Set( + initialize=range(pyo.value(self.plant_lifetime) + 1) + ) + self.annual_electrical_system_degradation = pyo.Param( initialize=0.005, mutable=True, @@ -198,10 +202,6 @@ def build_global_params(self): doc="Yearly performance degradation of electric energy system", ) - self.plant_lifetime_set = pyo.Set( - initialize=range(pyo.value(self.plant_lifetime) + 1) - ) - self.yearly_electricity_production = pyo.Var( self.plant_lifetime_set, initialize=1e4, @@ -355,6 +355,10 @@ def add_LCOH(self): self.add_component("LCOH", LCOH_expr) + def add_LCOW(self, *args, **kwargs): + + raise ValueError("Can't add LCOW to EnergyCosting package.") + @declare_process_block_class("REFLOSystemCosting") class REFLOSystemCostingData(WaterTAPCostingBlockData): @@ -757,23 +761,23 @@ def build_process_costs(self): """ pass - def add_LCOW(self, flow_rate, name="LCOW"): + def add_LCOT(self, flow_rate, name="LCOT"): """ - Add Levelized Cost of Water (LCOW) to costing block. + Add Levelized Cost of Treatment (LCOT) to costing block. Args: flow_rate - flow rate of water (volumetric) to be used in - calculating LCOW - name (optional) - name for the LCOW variable (default: LCOW) + calculating LCOT + name (optional) - name for the LCOT variable (default: LCOT) """ - LCOW = pyo.Var( - doc=f"Levelized Cost of Water based on flow {flow_rate.name}", + LCOT = pyo.Var( + doc=f"Levelized Cost of Treatment based on flow {flow_rate.name}", units=self.base_currency / pyo.units.m**3, ) - self.add_component(name, LCOW) + self.add_component(name, LCOT) - LCOW_constraint = pyo.Constraint( - expr=LCOW + LCOT_constraint = pyo.Constraint( + expr=LCOT == ( self.total_capital_cost * self.capital_recovery_factor + self.total_operating_cost @@ -782,9 +786,32 @@ def add_LCOW(self, flow_rate, name="LCOW"): pyo.units.convert(flow_rate, to_units=pyo.units.m**3 / self.base_period) * self.utilization_factor ), - doc=f"Constraint for Levelized Cost of Water based on flow {flow_rate.name}", + doc=f"Constraint for Levelized Cost of Treatment based on flow {flow_rate.name}", ) - self.add_component(name + "_constraint", LCOW_constraint) + self.add_component(name + "_constraint", LCOT_constraint) + + def add_LCOE(self): + """ + Add Levelized Cost of Energy (LCOE) to costing block. + """ + + energy_cost = self._get_energy_cost_block() + if not hasattr(energy_cost, "LCOE"): + energy_cost.add_LCOE() + + add_object_reference(self, "LCOE", energy_cost.LCOE) + + def add_LCOW(self, flow_rate, name="LCOW"): + """ + Add Levelized Cost of Water (LCOW) to costing block. + """ + + treat_cost = self._get_treatment_cost_block() + + if not hasattr(treat_cost, "LCOW"): + treat_cost.add_LCOW(flow_rate, name="LCOW") + + add_object_reference(self, name, getattr(treat_cost, name)) def add_LCOE(self): """ @@ -808,7 +835,7 @@ def add_LCOH(self): add_object_reference(self, "LCOH", energy_cost.LCOH) - def add_specific_electric_energy_consumption(self, *args, **kwargs): + def add_specific_electric_energy_consumption(self, flow_rate, name="SEEC"): """ Add specific electric energy consumption (kWh/m**3) to costing block. Args: @@ -818,11 +845,11 @@ def add_specific_electric_energy_consumption(self, *args, **kwargs): treat_cost = self._get_treatment_cost_block() if not hasattr(treat_cost, "specific_electric_energy_consumption_constraint"): - treat_cost.add_specific_electric_energy_consumption(*args, **kwargs) + treat_cost.add_specific_electric_energy_consumption(flow_rate, name=name) - add_object_reference(self, kwargs["name"], getattr(treat_cost, kwargs["name"])) + add_object_reference(self, name, getattr(treat_cost, name)) - def add_specific_thermal_energy_consumption(self, *args, **kwargs): + def add_specific_thermal_energy_consumption(self, flow_rate, name="STEC"): """ Add specific thermal energy consumption (kWh/m**3) to costing block. Args: @@ -832,9 +859,9 @@ def add_specific_thermal_energy_consumption(self, *args, **kwargs): treat_cost = self._get_treatment_cost_block() if not hasattr(treat_cost, "specific_thermal_energy_consumption_constraint"): - treat_cost.add_specific_thermal_energy_consumption(*args, **kwargs) + treat_cost.add_specific_thermal_energy_consumption(flow_rate, name=name) - add_object_reference(self, kwargs["name"], getattr(treat_cost, kwargs["name"])) + add_object_reference(self, name, getattr(treat_cost, name)) def _check_common_param_equivalence(self, treat_cost, energy_cost): """ From b400a40793676d17944c08c6b6edcbc80dde5758 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 14 Nov 2024 14:00:38 -0700 Subject: [PATCH 58/70] add metric tests --- .../test_reflo_watertap_costing_package.py | 65 ++++++++++++++++--- 1 file changed, 55 insertions(+), 10 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/tests/test_reflo_watertap_costing_package.py b/src/watertap_contrib/reflo/costing/tests/test_reflo_watertap_costing_package.py index 263ffdb0..533a293e 100644 --- a/src/watertap_contrib/reflo/costing/tests/test_reflo_watertap_costing_package.py +++ b/src/watertap_contrib/reflo/costing/tests/test_reflo_watertap_costing_package.py @@ -29,11 +29,8 @@ from idaes.core.util.scaling import calculate_scaling_factors from idaes.core.util.model_statistics import degrees_of_freedom -from watertap.costing.watertap_costing_package import ( - WaterTAPCostingData, - WaterTAPCostingBlockData, -) from watertap.core.solvers import get_solver +from watertap.costing.watertap_costing_package import WaterTAPCostingBlockData from watertap.property_models.seawater_prop_pack import SeawaterParameterBlock from watertap_contrib.reflo.costing import ( @@ -43,7 +40,6 @@ EnergyCosting, REFLOSystemCosting, ) - from watertap_contrib.reflo.costing.tests.dummy_costing_units import ( DummyTreatmentUnit, DummyTreatmentNoHeatUnit, @@ -274,8 +270,8 @@ def build_heat_and_elec_gen(): m.fs.treatment.unit.design_var_a.fix() m.fs.treatment.unit.design_var_b.fix() - m.fs.treatment.unit.electricity_consumption.fix(11000) - m.fs.treatment.unit.heat_consumption.fix(25000) + m.fs.treatment.unit.electricity_consumption.fix(110) + m.fs.treatment.unit.heat_consumption.fix(250) m.fs.treatment.costing.cost_process() #### ENERGY BLOCK @@ -289,8 +285,8 @@ def build_heat_and_elec_gen(): m.fs.energy.elec_unit.costing = UnitModelCostingBlock( flowsheet_costing_block=m.fs.energy.costing ) - m.fs.energy.heat_unit.heat.fix(5000) - m.fs.energy.elec_unit.electricity.fix(10000) + m.fs.energy.heat_unit.heat.fix(50) + m.fs.energy.elec_unit.electricity.fix(100) m.fs.energy.costing.cost_process() #### SYSTEM COSTING @@ -362,6 +358,7 @@ def default_build(self): def test_default_build(self, default_build): m = default_build + # check inheritance assert isinstance(m.fs.treatment.costing, REFLOCostingData) assert isinstance(m.fs.energy.costing, REFLOCostingData) assert isinstance(m.fs.costing, WaterTAPCostingBlockData) @@ -372,8 +369,17 @@ def test_default_build(self, default_build): assert not hasattr(m.fs.treatment.costing, "case_study_def") assert not hasattr(m.fs.energy.costing, "case_study_def") + # check unique properties of each assert hasattr(m.fs.energy.costing, "has_electricity_generation") - assert not m.fs.energy.costing.has_electricity_generation + assert not m.fs.energy.costing.has_electricity_generation # default is False + assert hasattr(m.fs.energy.costing, "base_energy_units") + assert hasattr(m.fs.energy.costing, "plant_lifetime_set") + assert hasattr(m.fs.energy.costing, "annual_electrical_system_degradation") + assert hasattr(m.fs.energy.costing, "annual_heat_system_degradation") + assert hasattr(m.fs.energy.costing, "yearly_electricity_production") + assert hasattr(m.fs.energy.costing, "lifetime_electricity_production") + assert hasattr(m.fs.energy.costing, "yearly_heat_production") + assert hasattr(m.fs.energy.costing, "lifetime_heat_production") assert not hasattr(m.fs.treatment.costing, "has_electricity_generation") assert m.fs.treatment.costing.base_currency is pyunits.USD_2021 @@ -781,6 +787,16 @@ def heat_and_elec_gen(self): m = build_heat_and_elec_gen() + m.fs.costing.add_LCOE() + m.fs.costing.add_LCOH() + m.fs.costing.add_LCOT(m.fs.treatment.unit.properties[0].flow_vol_phase["Liq"]) + m.fs.costing.add_specific_electric_energy_consumption( + m.fs.treatment.unit.properties[0].flow_vol_phase["Liq"] + ) + m.fs.costing.add_specific_thermal_energy_consumption( + m.fs.treatment.unit.properties[0].flow_vol_phase["Liq"] + ) + return m @pytest.mark.unit @@ -803,6 +819,20 @@ def test_build(slef, heat_and_elec_gen): assert hasattr(m.fs.costing, "frac_heat_from_grid_constraint") assert hasattr(m.fs.costing, "aggregate_heat_complement") + # all metrics end up as references on REFLOSystemCosting + assert hasattr(m.fs.costing, "LCOT") + assert hasattr(m.fs.costing, "LCOE") + assert hasattr(m.fs.costing, "LCOH") + assert hasattr(m.fs.costing, "SEEC") + assert hasattr(m.fs.costing, "STEC") + # energy metrics on EnergyCosting + assert hasattr(m.fs.energy.costing, "LCOE") + assert hasattr(m.fs.energy.costing, "LCOH") + # treatment metrics on TreatmentCosting + assert not hasattr(m.fs.treatment.costing, "LCOT") + assert hasattr(m.fs.treatment.costing, "SEEC") + assert hasattr(m.fs.treatment.costing, "STEC") + @pytest.mark.component def test_init_and_solve(self, heat_and_elec_gen): m = heat_and_elec_gen @@ -1006,6 +1036,21 @@ def test_common_params_equivalent(): assert m.fs.energy.costing.base_currency is pyunits.USD_2011 +@pytest.mark.component +def test_add_LCOW_to_energy_costing(): + + m = build_default() + + m.fs.energy.costing.cost_process() + m.fs.treatment.costing.cost_process() + + m.fs.costing = REFLOSystemCosting() + m.fs.costing.cost_process() + + with pytest.raises(ValueError, match="Can't add LCOW to EnergyCosting package\\."): + m.fs.energy.costing.add_LCOW() + + @pytest.mark.component def test_lazy_flow_costing(): m = ConcreteModel() From b042c6a8411a55b8df71630f57e0d7ee77e36684 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 14 Nov 2024 14:15:27 -0700 Subject: [PATCH 59/70] use TreatmentCosting so can add LCOW for test --- .../solar_models/zero_order/tests/test_flat_plate_physical.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/watertap_contrib/reflo/solar_models/zero_order/tests/test_flat_plate_physical.py b/src/watertap_contrib/reflo/solar_models/zero_order/tests/test_flat_plate_physical.py index 851b9d51..b985db08 100644 --- a/src/watertap_contrib/reflo/solar_models/zero_order/tests/test_flat_plate_physical.py +++ b/src/watertap_contrib/reflo/solar_models/zero_order/tests/test_flat_plate_physical.py @@ -40,7 +40,7 @@ from watertap.core.solvers import get_solver from watertap.property_models.water_prop_pack import WaterParameterBlock -from watertap_contrib.reflo.costing import EnergyCosting +from watertap_contrib.reflo.costing import TreatmentCosting from watertap_contrib.reflo.core import SolarModelType from watertap_contrib.reflo.solar_models.zero_order.flat_plate_physical import ( FlatPlatePhysical, @@ -196,7 +196,7 @@ def test_costing(self, flat_plate_frame): m.fs.test_flow = 0.01 * pyunits.Mgallons / pyunits.day - m.fs.costing = EnergyCosting() + m.fs.costing = TreatmentCosting() m.fs.costing.electricity_cost.fix(0.07) m.fs.costing.heat_cost.set_value(0) m.fs.flatplate.costing = UnitModelCostingBlock( From f2031342342f291bd6b76751ed00df10c2feca40 Mon Sep 17 00:00:00 2001 From: zacharybinger Date: Thu, 14 Nov 2024 17:07:00 -0700 Subject: [PATCH 60/70] corrected currency year --- .../reflo/costing/watertap_reflo_costing_package.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py index 7dd6e835..2e564888 100644 --- a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py +++ b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py @@ -63,7 +63,7 @@ def build_global_params(self): self.heat_cost = pyo.Var( initialize=0.0, doc="Heat cost", - units=pyo.units.USD_2018 / pyo.units.kWh, + units=self.base_currency / pyo.units.kWh, ) self.defined_flows["heat"] = self.heat_cost @@ -377,14 +377,14 @@ def build_global_params(self): mutable=True, initialize=0.07, doc="Electricity cost to buy", - units=pyo.units.USD_2018 / pyo.units.kWh, + units=self.base_currency / pyo.units.kWh, ) self.electricity_cost_sell = pyo.Param( mutable=True, initialize=0.05, doc="Electricity cost to sell", - units=pyo.units.USD_2018 / pyo.units.kWh, + units=self.base_currency / pyo.units.kWh, ) self.heat_cost_buy = pyo.Param( From e1d46a2d05d2e25322706a5fb44535455f773c39 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 14 Nov 2024 18:24:40 -0700 Subject: [PATCH 61/70] fix unit inconsistency issue --- .../costing/watertap_reflo_costing_package.py | 80 ++++++++++++------- 1 file changed, 51 insertions(+), 29 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py index 2e564888..c769bbd3 100644 --- a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py +++ b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py @@ -207,12 +207,14 @@ def build_global_params(self): initialize=1e4, domain=pyo.NonNegativeReals, units=pyo.units.kilowatt * pyo.units.hour, + doc="Yearly electricity production over facility lifetime", ) self.lifetime_electricity_production = pyo.Var( initialize=1e6, domain=pyo.NonNegativeReals, units=pyo.units.kilowatt * pyo.units.hour, + doc="Total electricity production over facility lifetime", ) self.yearly_heat_production = pyo.Var( @@ -220,12 +222,14 @@ def build_global_params(self): initialize=1e4, domain=pyo.NonNegativeReals, units=pyo.units.kilowatt * pyo.units.hour, + doc="Yearly heat production over facility lifetime", ) self.lifetime_heat_production = pyo.Var( initialize=1e6, domain=pyo.NonNegativeReals, units=pyo.units.kilowatt * pyo.units.hour, + doc="Total heat production over facility lifetime", ) def build_process_costs(self): @@ -391,14 +395,14 @@ def build_global_params(self): mutable=True, initialize=0.01, doc="Heat cost to buy", - units=pyo.units.USD_2021 / pyo.units.kWh, + units=self.base_currency / pyo.units.kWh, ) self.heat_cost_sell = pyo.Param( mutable=True, initialize=0.01, doc="Heat cost to sell", - units=pyo.units.USD_2021 / pyo.units.kWh, + units=self.base_currency / pyo.units.kWh, ) # Build the integrated system costs @@ -433,13 +437,13 @@ def build_integrated_costs(self): self.aggregate_flow_electricity = pyo.Var( initialize=1e3, - doc="Aggregated electricity flow", + doc="Aggregated system electricity flow", units=pyo.units.kW, ) self.aggregate_flow_heat = pyo.Var( initialize=1e3, - doc="Aggregated heat flow", + doc="Aggregated system heat flow", units=pyo.units.kW, ) @@ -535,6 +539,7 @@ def build_integrated_costs(self): ) ) ) + else: self.frac_elec_from_grid.fix(1) @@ -597,7 +602,7 @@ def build_integrated_costs(self): ) else: - # treatment block isn't consuming heat and energy block isn't generating + # treatment block isn't consuming heat and energy block isn't generating heat self.has_heat_flows = False self.aggregate_flow_heat_purchased.fix(0) self.aggregate_flow_heat_sold.fix(0) @@ -605,34 +610,40 @@ def build_integrated_costs(self): # positive is for cost and negative for revenue self.total_electric_operating_cost_constraint = pyo.Constraint( expr=self.total_electric_operating_cost - == ( - pyo.units.convert( - self.aggregate_flow_electricity_purchased, - to_units=pyo.units.kWh / pyo.units.year, - ) - * self.electricity_cost_buy - - pyo.units.convert( - self.aggregate_flow_electricity_sold, - to_units=pyo.units.kWh / pyo.units.year, - ) - * self.electricity_cost_sell + == pyo.units.convert( + ( + pyo.units.convert( + self.aggregate_flow_electricity_purchased, + to_units=pyo.units.kWh / pyo.units.year, + ) + * self.electricity_cost_buy + - pyo.units.convert( + self.aggregate_flow_electricity_sold, + to_units=pyo.units.kWh / pyo.units.year, + ) + * self.electricity_cost_sell + ), + to_units=self.base_currency / self.base_period, ) ) # positive is for cost and negative for revenue self.total_heat_operating_cost_constraint = pyo.Constraint( expr=self.total_heat_operating_cost - == ( - pyo.units.convert( - self.aggregate_flow_heat_purchased, - to_units=pyo.units.kWh / pyo.units.year, - ) - * self.heat_cost_buy - - pyo.units.convert( - self.aggregate_flow_heat_sold, - to_units=pyo.units.kWh / pyo.units.year, - ) - * self.heat_cost_sell + == pyo.units.convert( + ( + pyo.units.convert( + self.aggregate_flow_heat_purchased, + to_units=pyo.units.kWh / pyo.units.year, + ) + * self.heat_cost_buy + - pyo.units.convert( + self.aggregate_flow_heat_sold, + to_units=pyo.units.kWh / pyo.units.year, + ) + * self.heat_cost_sell + ), + to_units=self.base_currency / self.base_period, ) ) @@ -902,7 +913,6 @@ def _check_common_param_equivalence(self, treat_cost, energy_cost): # if REFLOSystemCosting has this parameter, # we fix it to the treatment costing block value p = getattr(self, cp) - # print(p.to_string()) if isinstance(p, pyo.Var): p.fix(pyo.value(tp)) elif isinstance(p, pyo.Param): @@ -914,6 +924,9 @@ def _check_common_param_equivalence(self, treat_cost, energy_cost): self.base_period = treat_cost.base_period def _get_treatment_cost_block(self): + """ + Get the TreatmentCosting block, if present. + """ tb = None for b in self.model().component_objects(pyo.Block): if isinstance(b, TreatmentCostingData): @@ -926,6 +939,9 @@ def _get_treatment_cost_block(self): return tb def _get_energy_cost_block(self): + """ + Get the EnergyCosting block, if present. + """ eb = None for b in self.model().component_objects(pyo.Block): if isinstance(b, EnergyCostingData): @@ -938,6 +954,9 @@ def _get_energy_cost_block(self): return eb def _get_electricity_generation_unit(self): + """ + Get the electricity generating unit on the flowsheet, if present. + """ elec_gen_unit = None for b in self.model().component_objects(pyo.Block): if isinstance( @@ -956,13 +975,16 @@ def _get_electricity_generation_unit(self): return elec_gen_unit def _get_pysam(self): + """ + Get the PySAMWaterTAP block on flowsheet. + """ pysam_block_test_lst = [] for k, v in vars(self.model()).items(): if isinstance(v, PySAMWaterTAP): pysam_block_test_lst.append(k) if len(pysam_block_test_lst) != 1: - raise Exception("There is no instance of PySAMWaterTAP on this model.") + raise ValueError("There is no instance of PySAMWaterTAP on this model.") else: pysam = getattr(self.model(), pysam_block_test_lst[0]) From 698af60f41437b4485644f88650bbf4abb0d49c8 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 14 Nov 2024 18:25:08 -0700 Subject: [PATCH 62/70] fix unit inconsistency issue --- .../costing/tests/dummy_costing_units.py | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/tests/dummy_costing_units.py b/src/watertap_contrib/reflo/costing/tests/dummy_costing_units.py index 814e5bd0..a49852a8 100644 --- a/src/watertap_contrib/reflo/costing/tests/dummy_costing_units.py +++ b/src/watertap_contrib/reflo/costing/tests/dummy_costing_units.py @@ -133,25 +133,36 @@ def build(self): doc="Constant heat consumption", ) + dimensionless_flow_vol = pyunits.convert( + self.properties[0].flow_vol_phase["Liq"] * pyunits.m**-3 * pyunits.s, + to_units=pyunits.dimensionless, + ) + dimensionless_flow_mass = pyunits.convert( + self.properties[0].flow_mass_phase_comp["Liq", "TDS"] + * pyunits.s + * pyunits.kg**-1, + to_units=pyunits.dimensionless, + ) + dimensionless_conc = pyunits.convert( + self.properties[0].conc_mass_phase_comp["Liq", "TDS"] + * pyunits.m**3 + * pyunits.kg**-1, + to_units=pyunits.dimensionless, + ) + @self.Constraint(doc="Capital variable calculation") def eq_capital_var(b): - return ( - b.capital_var == b.design_var_a * b.properties[0].flow_vol_phase["Liq"] - ) + return b.capital_var == b.design_var_a * dimensionless_flow_vol @self.Constraint(doc="Fixed operating variable calculation") def eq_fixed_operating_var(b): - return ( - b.fixed_operating_var - == b.design_var_b * b.properties[0].conc_mass_phase_comp["Liq", "TDS"] - ) + return b.fixed_operating_var == b.design_var_b * dimensionless_conc @self.Constraint(doc="Variable operating variable calculation") def eq_variable_operating_var(b): return ( b.variable_operating_var - == (b.design_var_a * b.design_var_b) - * b.properties[0].flow_mass_phase_comp["Liq", "TDS"] + == (b.design_var_a * b.design_var_b) * dimensionless_flow_mass ) def initialize_build(self): From f4ff080c061e00ec684da745df860c5c8b6763bf Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 14 Nov 2024 19:09:53 -0700 Subject: [PATCH 63/70] add initialize for LCOT, LCOH, LCOE --- .../costing/watertap_reflo_costing_package.py | 50 +++++++++++++++++-- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py index c769bbd3..eeed0b44 100644 --- a/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py +++ b/src/watertap_contrib/reflo/costing/watertap_reflo_costing_package.py @@ -661,6 +661,8 @@ def build_integrated_costs(self): def initialize_build(self): + energy_cost = self._get_energy_cost_block() + self.aggregate_flow_electricity_sold.fix(0) self.aggregate_electricity_complement.deactivate() @@ -725,6 +727,47 @@ def initialize_build(self): super().initialize_build() + if hasattr(self, "LCOT"): + calculate_variable_from_constraint( + self.LCOT, + self.LCOT_constraint, + ) + + if hasattr(self, "LCOE"): + for y, c in energy_cost.yearly_electricity_production_constraint.items(): + + calculate_variable_from_constraint( + energy_cost.yearly_electricity_production[y], + c, + ) + + calculate_variable_from_constraint( + energy_cost.lifetime_electricity_production, + energy_cost.lifetime_electricity_production_constraint, + ) + calculate_variable_from_constraint( + energy_cost.LCOE, + energy_cost.LCOE_constraint, + ) + + if hasattr(self, "LCOH"): + + for y, c in energy_cost.yearly_heat_production_constraint.items(): + + calculate_variable_from_constraint( + energy_cost.yearly_heat_production[y], + c, + ) + + calculate_variable_from_constraint( + energy_cost.lifetime_heat_production, + energy_cost.lifetime_heat_production_constraint, + ) + calculate_variable_from_constraint( + energy_cost.LCOH, + energy_cost.LCOH_constraint, + ) + def calculate_scaling_factors(self): if get_scaling_factor(self.total_capital_cost) is None: @@ -772,20 +815,19 @@ def build_process_costs(self): """ pass - def add_LCOT(self, flow_rate, name="LCOT"): + def add_LCOT(self, flow_rate): """ Add Levelized Cost of Treatment (LCOT) to costing block. Args: flow_rate - flow rate of water (volumetric) to be used in calculating LCOT - name (optional) - name for the LCOT variable (default: LCOT) """ LCOT = pyo.Var( doc=f"Levelized Cost of Treatment based on flow {flow_rate.name}", units=self.base_currency / pyo.units.m**3, ) - self.add_component(name, LCOT) + self.add_component("LCOT", LCOT) LCOT_constraint = pyo.Constraint( expr=LCOT @@ -799,7 +841,7 @@ def add_LCOT(self, flow_rate, name="LCOT"): ), doc=f"Constraint for Levelized Cost of Treatment based on flow {flow_rate.name}", ) - self.add_component(name + "_constraint", LCOT_constraint) + self.add_component("LCOT_constraint", LCOT_constraint) def add_LCOE(self): """ From e4023bbe807d01ba72d6a03d920c1ca848e0f757 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 14 Nov 2024 19:13:02 -0700 Subject: [PATCH 64/70] add assert_units_consistent test; heat/energy metric testing; aggregation tests --- .../test_reflo_watertap_costing_package.py | 144 +++++++++++++++--- 1 file changed, 123 insertions(+), 21 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/tests/test_reflo_watertap_costing_package.py b/src/watertap_contrib/reflo/costing/tests/test_reflo_watertap_costing_package.py index 533a293e..2e9a0946 100644 --- a/src/watertap_contrib/reflo/costing/tests/test_reflo_watertap_costing_package.py +++ b/src/watertap_contrib/reflo/costing/tests/test_reflo_watertap_costing_package.py @@ -24,9 +24,10 @@ value, units as pyunits, ) +from pyomo.util.check_units import assert_units_consistent from idaes.core import FlowsheetBlock, UnitModelCostingBlock -from idaes.core.util.scaling import calculate_scaling_factors +from idaes.core.util.scaling import calculate_scaling_factors, set_scaling_factor from idaes.core.util.model_statistics import degrees_of_freedom from watertap.core.solvers import get_solver @@ -74,6 +75,9 @@ def build_electricity_gen_only_with_heat(): m.fs.treatment.unit.electricity_consumption.fix(100) m.fs.treatment.unit.heat_consumption.fix() m.fs.treatment.costing.cost_process() + m.fs.treatment.costing.add_LCOW( + m.fs.treatment.unit.properties[0].flow_vol_phase["Liq"] + ) #### ENERGY BLOCK m.fs.energy = Block() @@ -88,10 +92,7 @@ def build_electricity_gen_only_with_heat(): #### SYSTEM COSTING m.fs.costing = REFLOSystemCosting() m.fs.costing.cost_process() - - m.fs.treatment.costing.add_LCOW( - m.fs.treatment.unit.properties[0].flow_vol_phase["Liq"] - ) + m.fs.costing.add_LCOE() #### SCALING m.fs.properties.set_default_scaling( @@ -102,8 +103,6 @@ def build_electricity_gen_only_with_heat(): ) calculate_scaling_factors(m) - #### INITIALIZE - m.fs.treatment.unit.properties.calculate_state( var_args={ ("flow_vol_phase", "Liq"): 0.04381, @@ -140,10 +139,14 @@ def build_electricity_gen_only_no_heat(): m.fs.treatment.unit.design_var_b.fix() m.fs.treatment.unit.electricity_consumption.fix(10000) m.fs.treatment.costing.cost_process() + m.fs.treatment.costing.add_LCOW( + m.fs.treatment.unit.properties[0].flow_vol_phase["Liq"] + ) #### ENERGY BLOCK m.fs.energy = Block() m.fs.energy.costing = EnergyCosting() + m.fs.energy.unit = DummyElectricityUnit() m.fs.energy.unit.costing = UnitModelCostingBlock( flowsheet_costing_block=m.fs.energy.costing @@ -154,10 +157,8 @@ def build_electricity_gen_only_no_heat(): #### SYSTEM COSTING m.fs.costing = REFLOSystemCosting() m.fs.costing.cost_process() - - m.fs.treatment.costing.add_LCOW( - m.fs.treatment.unit.properties[0].flow_vol_phase["Liq"] - ) + m.fs.costing.add_LCOE() + m.fs.costing.add_LCOT(m.fs.treatment.unit.properties[0].flow_vol_phase["Liq"]) #### SCALING m.fs.properties.set_default_scaling( @@ -168,7 +169,8 @@ def build_electricity_gen_only_no_heat(): ) calculate_scaling_factors(m) - #### INITIALIZE + set_scaling_factor(m.fs.energy.costing.yearly_electricity_production, 1e-7) + set_scaling_factor(m.fs.energy.costing.lifetime_electricity_production, 1e-9) m.fs.treatment.unit.properties.calculate_state( var_args={ @@ -205,8 +207,11 @@ def build_heat_gen_only(): m.fs.treatment.unit.design_var_a.fix() m.fs.treatment.unit.design_var_b.fix() m.fs.treatment.unit.electricity_consumption.fix(1000) - m.fs.treatment.unit.heat_consumption.fix(25000) + m.fs.treatment.unit.heat_consumption.fix(2500) m.fs.treatment.costing.cost_process() + m.fs.treatment.costing.add_LCOW( + m.fs.treatment.unit.properties[0].flow_vol_phase["Liq"] + ) #### ENERGY BLOCK m.fs.energy = Block() @@ -221,9 +226,11 @@ def build_heat_gen_only(): #### SYSTEM COSTING m.fs.costing = REFLOSystemCosting() m.fs.costing.cost_process() - m.fs.treatment.costing.add_LCOW( + m.fs.costing.add_specific_thermal_energy_consumption( m.fs.treatment.unit.properties[0].flow_vol_phase["Liq"] ) + m.fs.costing.add_LCOH() + m.fs.costing.add_LCOT(m.fs.treatment.unit.properties[0].flow_vol_phase["Liq"]) #### SCALING m.fs.properties.set_default_scaling( @@ -234,8 +241,6 @@ def build_heat_gen_only(): ) calculate_scaling_factors(m) - #### INITIALIZE - m.fs.treatment.unit.properties.calculate_state( var_args={ ("flow_vol_phase", "Liq"): 0.4381, @@ -262,6 +267,7 @@ def build_heat_and_elec_gen(): #### TREATMENT BLOCK m.fs.treatment = Block() m.fs.treatment.costing = TreatmentCosting() + m.fs.treatment.costing.base_currency = pyunits.USD_2002 m.fs.treatment.unit = DummyTreatmentUnit(property_package=m.fs.properties) m.fs.treatment.unit.costing = UnitModelCostingBlock( @@ -277,6 +283,8 @@ def build_heat_and_elec_gen(): #### ENERGY BLOCK m.fs.energy = Block() m.fs.energy.costing = EnergyCosting() + m.fs.energy.costing.base_currency = pyunits.USD_2002 + m.fs.energy.heat_unit = DummyHeatUnit() m.fs.energy.elec_unit = DummyElectricityUnit() m.fs.energy.heat_unit.costing = UnitModelCostingBlock( @@ -305,8 +313,6 @@ def build_heat_and_elec_gen(): ) calculate_scaling_factors(m) - #### INITIALIZE - m.fs.treatment.unit.properties.calculate_state( var_args={ ("flow_vol_phase", "Liq"): 0.4381, @@ -358,6 +364,8 @@ def default_build(self): def test_default_build(self, default_build): m = default_build + assert_units_consistent(m) + # check inheritance assert isinstance(m.fs.treatment.costing, REFLOCostingData) assert isinstance(m.fs.energy.costing, REFLOCostingData) @@ -438,6 +446,8 @@ def test_build(slef, energy_gen_only_with_heat): m = energy_gen_only_with_heat + assert_units_consistent(m) + assert degrees_of_freedom(m) == 0 # have heat flows @@ -545,6 +555,26 @@ def test_optimize_frac_from_grid(self): + m.fs.energy.costing.aggregate_flow_electricity ) + # test aggregation + assert pytest.approx( + value(m.fs.costing.aggregate_capital_cost), rel=1e-3 + ) == value( + m.fs.treatment.costing.aggregate_capital_cost + + m.fs.energy.costing.aggregate_capital_cost + ) + assert pytest.approx( + value(m.fs.costing.aggregate_fixed_operating_cost), rel=1e-3 + ) == value( + m.fs.treatment.costing.aggregate_fixed_operating_cost + + m.fs.energy.costing.aggregate_fixed_operating_cost + ) + assert pytest.approx( + value(m.fs.costing.aggregate_variable_operating_cost), rel=1e-3 + ) == value( + m.fs.treatment.costing.aggregate_variable_operating_cost + + m.fs.energy.costing.aggregate_variable_operating_cost + ) + class TestElectricityGenOnlyNoHeat: @@ -560,6 +590,8 @@ def test_build(slef, energy_gen_only_no_heat): m = energy_gen_only_no_heat + assert_units_consistent(m) + assert degrees_of_freedom(m) == 0 # no heat flows @@ -632,6 +664,26 @@ def test_init_and_solve(self, energy_gen_only_no_heat): # no heat is generated or consumed assert pytest.approx(value(m.fs.costing.aggregate_flow_heat), rel=1e-3) == 0 + # test aggregation + assert pytest.approx( + value(m.fs.costing.aggregate_capital_cost), rel=1e-3 + ) == value( + m.fs.treatment.costing.aggregate_capital_cost + + m.fs.energy.costing.aggregate_capital_cost + ) + assert pytest.approx( + value(m.fs.costing.aggregate_fixed_operating_cost), rel=1e-3 + ) == value( + m.fs.treatment.costing.aggregate_fixed_operating_cost + + m.fs.energy.costing.aggregate_fixed_operating_cost + ) + assert pytest.approx( + value(m.fs.costing.aggregate_variable_operating_cost), rel=1e-3 + ) == value( + m.fs.treatment.costing.aggregate_variable_operating_cost + + m.fs.energy.costing.aggregate_variable_operating_cost + ) + @pytest.mark.component def test_optimize_frac_from_grid(self): @@ -683,6 +735,8 @@ def test_build(self, heat_gen_only): m = heat_gen_only + assert_units_consistent(m) + assert degrees_of_freedom(m) == 0 # has heat flows, no electricity generation @@ -755,7 +809,7 @@ def test_optimize_frac_from_grid(self): m = build_heat_gen_only() m.fs.energy.unit.heat.unfix() - m.fs.costing.frac_heat_from_grid.fix(0.02) + m.fs.costing.frac_heat_from_grid.fix(0.002) assert degrees_of_freedom(m) == 0 @@ -769,15 +823,34 @@ def test_optimize_frac_from_grid(self): assert ( pytest.approx(value(m.fs.costing.aggregate_flow_heat_purchased), rel=1e-3) - == 500 + == 5 ) - assert pytest.approx(value(m.fs.costing.aggregate_flow_heat), rel=1e-3) == 500 + assert pytest.approx(value(m.fs.costing.aggregate_flow_heat), rel=1e-3) == 5 assert pytest.approx( value(m.fs.costing.aggregate_flow_electricity), rel=1e-3 ) == value( m.fs.costing.aggregate_flow_electricity_purchased - m.fs.costing.aggregate_flow_electricity_sold ) + # test aggregation + assert pytest.approx( + value(m.fs.costing.aggregate_capital_cost), rel=1e-3 + ) == value( + m.fs.treatment.costing.aggregate_capital_cost + + m.fs.energy.costing.aggregate_capital_cost + ) + assert pytest.approx( + value(m.fs.costing.aggregate_fixed_operating_cost), rel=1e-3 + ) == value( + m.fs.treatment.costing.aggregate_fixed_operating_cost + + m.fs.energy.costing.aggregate_fixed_operating_cost + ) + assert pytest.approx( + value(m.fs.costing.aggregate_variable_operating_cost), rel=1e-3 + ) == value( + m.fs.treatment.costing.aggregate_variable_operating_cost + + m.fs.energy.costing.aggregate_variable_operating_cost + ) class TestElectricityAndHeatGen: @@ -787,6 +860,8 @@ def heat_and_elec_gen(self): m = build_heat_and_elec_gen() + assert_units_consistent(m) + m.fs.costing.add_LCOE() m.fs.costing.add_LCOH() m.fs.costing.add_LCOT(m.fs.treatment.unit.properties[0].flow_vol_phase["Liq"]) @@ -804,6 +879,8 @@ def test_build(slef, heat_and_elec_gen): m = heat_and_elec_gen + assert_units_consistent(m) + assert degrees_of_freedom(m) == 0 # has heat and electricity flows @@ -939,6 +1016,25 @@ def test_optimize_frac_from_grid(self): * m.fs.energy.costing.aggregate_flow_heat / m.fs.treatment.costing.aggregate_flow_heat ) + # test aggregation + assert pytest.approx( + value(m.fs.costing.aggregate_capital_cost), rel=1e-3 + ) == value( + m.fs.treatment.costing.aggregate_capital_cost + + m.fs.energy.costing.aggregate_capital_cost + ) + assert pytest.approx( + value(m.fs.costing.aggregate_fixed_operating_cost), rel=1e-3 + ) == value( + m.fs.treatment.costing.aggregate_fixed_operating_cost + + m.fs.energy.costing.aggregate_fixed_operating_cost + ) + assert pytest.approx( + value(m.fs.costing.aggregate_variable_operating_cost), rel=1e-3 + ) == value( + m.fs.treatment.costing.aggregate_variable_operating_cost + + m.fs.energy.costing.aggregate_variable_operating_cost + ) @pytest.mark.component @@ -991,6 +1087,8 @@ def test_common_params_equivalent(): m.fs.costing = REFLOSystemCosting() m.fs.costing.cost_process() + assert_units_consistent(m) + # when they are equivalent, assert equivalency across all three costing packages assert value(m.fs.costing.electricity_cost) == value( @@ -1007,6 +1105,8 @@ def test_common_params_equivalent(): m.fs.energy.costing.cost_process() m.fs.treatment.costing.cost_process() + assert_units_consistent(m) + # raise error when base currency isn't equivalent with pytest.raises( @@ -1029,6 +1129,8 @@ def test_common_params_equivalent(): m.fs.costing = REFLOSystemCosting() m.fs.costing.cost_process() + assert_units_consistent(m) + # when they are equivalent, assert equivalency across all three costing packages assert m.fs.costing.base_currency is pyunits.USD_2011 From 8bcd94cbd2ac0c2b5478f9c72dec65293fb40ca7 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 14 Nov 2024 20:15:14 -0700 Subject: [PATCH 65/70] fix currency unit conversions --- .../reflo/costing/units/lt_med_surrogate.py | 15 +-- .../reflo/costing/units/med_tvc_surrogate.py | 99 +++++++++++-------- .../reflo/costing/units/vagmd_surrogate.py | 65 +++++++----- 3 files changed, 106 insertions(+), 73 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/units/lt_med_surrogate.py b/src/watertap_contrib/reflo/costing/units/lt_med_surrogate.py index d851f4b8..d056623c 100644 --- a/src/watertap_contrib/reflo/costing/units/lt_med_surrogate.py +++ b/src/watertap_contrib/reflo/costing/units/lt_med_surrogate.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. @@ -17,11 +17,13 @@ make_fixed_operating_cost_var, ) -# Costing equations from: -# Kosmadakis G, Papapetrou M, Ortega-Delgado B, Cipollina A, Alarcón-Padilla D-C. -# "Correlations for estimating the specific capital cost of multi-effect distillation plants -# considering the main design trends and operating conditions" -# doi: 10.1016/j.desal.2018.09.011 +""" +Costing equations from: +Kosmadakis G, Papapetrou M, Ortega-Delgado B, Cipollina A, Alarcón-Padilla D-C. +"Correlations for estimating the specific capital cost of multi-effect distillation plants + considering the main design trends and operating conditions" +doi: 10.1016/j.desal.2018.09.011 +""" def build_lt_med_surrogate_cost_param_block(blk): @@ -150,7 +152,6 @@ def cost_lt_med_surrogate(blk): initialize=100, bounds=(0, None), units=pyo.units.USD_2018 / (pyo.units.m**3 / pyo.units.day), - # units=pyo.units.USD_2018, doc="MED system cost per m3/day distillate", ) diff --git a/src/watertap_contrib/reflo/costing/units/med_tvc_surrogate.py b/src/watertap_contrib/reflo/costing/units/med_tvc_surrogate.py index 7af6efaf..004bf3f0 100644 --- a/src/watertap_contrib/reflo/costing/units/med_tvc_surrogate.py +++ b/src/watertap_contrib/reflo/costing/units/med_tvc_surrogate.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. @@ -17,11 +17,13 @@ make_fixed_operating_cost_var, ) -# Costing equations from: -# Kosmadakis G, Papapetrou M, Ortega-Delgado B, Cipollina A, Alarcón-Padilla D-C. -# "Correlations for estimating the specific capital cost of multi-effect distillation plants -# considering the main design trends and operating conditions" -# doi: 10.1016/j.desal.2018.09.011 +""" +Costing equations from: +Kosmadakis G, Papapetrou M, Ortega-Delgado B, Cipollina A, Alarcón-Padilla D-C. +"Correlations for estimating the specific capital cost of multi-effect distillation plants + considering the main design trends and operating conditions" +doi: 10.1016/j.desal.2018.09.011 +""" def build_med_tvc_surrogate_cost_param_block(blk): @@ -37,14 +39,14 @@ def build_med_tvc_surrogate_cost_param_block(blk): blk.cost_fraction_maintenance = pyo.Var( initialize=0.02, - units=pyo.units.dimensionless, + units=pyo.units.year**-1, bounds=(0, None), doc="Fraction of capital cost for maintenance", ) blk.cost_fraction_insurance = pyo.Var( initialize=0.005, - units=pyo.units.dimensionless, + units=pyo.units.year**-1, bounds=(0, None), doc="Fraction of capital cost for insurance", ) @@ -86,7 +88,7 @@ def build_med_tvc_surrogate_cost_param_block(blk): blk.med_sys_A_coeff = pyo.Var( initialize=6291, - units=pyo.units.dimensionless, + units=pyo.units.USD_2018 / (pyo.units.m**3 / pyo.units.day), doc="MED system specific capital A coeff", ) @@ -124,6 +126,7 @@ def cost_med_tvc_surrogate(blk): dist = med_tvc.distillate_props[0] brine = med_tvc.brine_props[0] base_currency = blk.config.flowsheet_costing_block.base_currency + base_period = blk.config.flowsheet_costing_block.base_period blk.membrane_system_cost = pyo.Var( initialize=100, @@ -146,64 +149,82 @@ def cost_med_tvc_surrogate(blk): blk.capacity = pyo.units.convert( dist.flow_vol_phase["Liq"], to_units=pyo.units.m**3 / pyo.units.day ) + blk.capacity_dimensionless = pyo.units.convert( + blk.capacity * pyo.units.day * pyo.units.m**-3, to_units=pyo.units.dimensionless + ) blk.annual_dist_production = pyo.units.convert( dist.flow_vol_phase["Liq"], to_units=pyo.units.m**3 / pyo.units.year ) blk.med_specific_cost_constraint = pyo.Constraint( expr=blk.med_specific_cost - == ( - med_tvc_params.med_sys_A_coeff - * blk.capacity**med_tvc_params.med_sys_B_coeff + == pyo.units.convert( + ( + med_tvc_params.med_sys_A_coeff + * blk.capacity_dimensionless**med_tvc_params.med_sys_B_coeff + ), + to_units=pyo.units.USD_2018 / (pyo.units.m**3 / pyo.units.day), ) ) blk.membrane_system_cost_constraint = pyo.Constraint( expr=blk.membrane_system_cost - == blk.capacity - * (blk.med_specific_cost * (1 - med_tvc_params.cost_fraction_evaporator)) + == pyo.units.convert( + blk.capacity + * (blk.med_specific_cost * (1 - med_tvc_params.cost_fraction_evaporator)), + to_units=base_currency, + ) ) blk.evaporator_system_cost_constraint = pyo.Constraint( expr=blk.evaporator_system_cost - == blk.capacity - * ( - blk.med_specific_cost + == pyo.units.convert( + blk.capacity * ( - med_tvc_params.cost_fraction_evaporator + blk.med_specific_cost * ( - ( - med_tvc.specific_area_per_kg_s - / med_tvc_params.heat_exchanger_ref_area + med_tvc_params.cost_fraction_evaporator + * ( + ( + med_tvc.specific_area_per_kg_s + / med_tvc_params.heat_exchanger_ref_area + ) + ** med_tvc_params.heat_exchanger_exp ) - ** med_tvc_params.heat_exchanger_exp ) - ) + ), + to_units=base_currency, ) ) blk.costing_package.add_cost_factor(blk, None) blk.capital_cost_constraint = pyo.Constraint( - expr=blk.capital_cost == blk.membrane_system_cost + blk.evaporator_system_cost + expr=blk.capital_cost + == pyo.units.convert( + blk.membrane_system_cost + blk.evaporator_system_cost, + to_units=base_currency, + ) ) blk.fixed_operating_cost_constraint = pyo.Constraint( expr=blk.fixed_operating_cost - == blk.annual_dist_production - * ( - med_tvc_params.cost_chemicals_per_vol_dist - + med_tvc_params.cost_labor_per_vol_dist - + med_tvc_params.cost_misc_per_vol_dist - ) - + blk.capital_cost - * ( - med_tvc_params.cost_fraction_maintenance - + med_tvc_params.cost_fraction_insurance - ) - + pyo.units.convert( - brine.flow_vol_phase["Liq"], to_units=pyo.units.m**3 / pyo.units.year + == pyo.units.convert( + blk.annual_dist_production + * ( + med_tvc_params.cost_chemicals_per_vol_dist + + med_tvc_params.cost_labor_per_vol_dist + + med_tvc_params.cost_misc_per_vol_dist + ) + + blk.capital_cost + * ( + med_tvc_params.cost_fraction_maintenance + + med_tvc_params.cost_fraction_insurance + ) + + pyo.units.convert( + brine.flow_vol_phase["Liq"], to_units=pyo.units.m**3 / pyo.units.year + ) + * med_tvc_params.cost_disposal_per_vol_brine, + to_units=base_currency / base_period, ) - * med_tvc_params.cost_disposal_per_vol_brine ) - blk.heat_flow = pyo.Expression( expr=med_tvc.specific_energy_consumption_thermal * pyo.units.convert(blk.capacity, to_units=pyo.units.m**3 / pyo.units.hr) diff --git a/src/watertap_contrib/reflo/costing/units/vagmd_surrogate.py b/src/watertap_contrib/reflo/costing/units/vagmd_surrogate.py index 9e1c06e6..7910466f 100644 --- a/src/watertap_contrib/reflo/costing/units/vagmd_surrogate.py +++ b/src/watertap_contrib/reflo/costing/units/vagmd_surrogate.py @@ -185,14 +185,14 @@ def build_vagmd_surrogate_cost_param_block(blk): blk.cost_fraction_maintenance = pyo.Var( initialize=0.013, - units=pyo.units.dimensionless, + units=pyo.units.year**-1, bounds=(0, None), doc="Fraction of capital cost for maintenance", ) blk.cost_fraction_insurance = pyo.Var( initialize=0.005, - units=pyo.units.dimensionless, + units=pyo.units.year**-1, bounds=(0, None), doc="Fraction of capital cost for insurance", ) @@ -224,6 +224,7 @@ def cost_vagmd_surrogate(blk): vagmd = blk.unit_model base_currency = blk.config.flowsheet_costing_block.base_currency + base_period = blk.config.flowsheet_costing_block.base_period blk.module_cost = pyo.Var( initialize=100000, @@ -246,44 +247,54 @@ def cost_vagmd_surrogate(blk): blk.module_cost_constraint = pyo.Constraint( expr=blk.module_cost - == vagmd_params.base_module_cost - * vagmd_params.base_module_capacity - * (vagmd.num_modules / vagmd_params.base_module_capacity) - ** vagmd_params.module_cost_index - + vagmd_params.membrane_cost * vagmd.module_area * vagmd.num_modules + == pyo.units.convert( + vagmd_params.base_module_cost + * vagmd_params.base_module_capacity + * (vagmd.num_modules / vagmd_params.base_module_capacity) + ** vagmd_params.module_cost_index + + vagmd_params.membrane_cost * vagmd.module_area * vagmd.num_modules, + to_units=base_currency, + ) ) blk.other_capital_cost_constraint = pyo.Constraint( expr=blk.other_capital_cost - == vagmd_params.base_housing_rack_cost - * (vagmd.num_modules / vagmd_params.base_housing_rack_capacity) - ** vagmd_params.housing_rack_cost_index - + vagmd_params.base_tank_cost - * (vagmd.num_modules / vagmd_params.base_tank_capacity) - ** vagmd_params.tank_cost_index - + vagmd_params.base_other_cost - * (vagmd.num_modules / vagmd_params.base_other_capacity) - ** vagmd_params.other_cost_index + == pyo.units.convert( + vagmd_params.base_housing_rack_cost + * (vagmd.num_modules / vagmd_params.base_housing_rack_capacity) + ** vagmd_params.housing_rack_cost_index + + vagmd_params.base_tank_cost + * (vagmd.num_modules / vagmd_params.base_tank_capacity) + ** vagmd_params.tank_cost_index + + vagmd_params.base_other_cost + * (vagmd.num_modules / vagmd_params.base_other_capacity) + ** vagmd_params.other_cost_index, + to_units=base_currency, + ) ) blk.costing_package.add_cost_factor(blk, None) blk.capital_cost_constraint = pyo.Constraint( - expr=blk.capital_cost == blk.module_cost + blk.other_capital_cost + expr=blk.capital_cost + == pyo.units.convert( + blk.module_cost + blk.other_capital_cost, to_units=base_currency + ) ) blk.fixed_operating_cost_constraint = pyo.Constraint( expr=blk.fixed_operating_cost == pyo.units.convert( - vagmd.system_capacity, to_units=pyo.units.m**3 / pyo.units.year - ) - * ( - vagmd_params.membrane_replacement_cost - + vagmd_params.specific_operational_cost - ) - + blk.capital_cost - * ( - vagmd_params.cost_fraction_maintenance - + vagmd_params.cost_fraction_insurance + blk.annual_dist_production + * ( + vagmd_params.membrane_replacement_cost + + vagmd_params.specific_operational_cost + ) + + blk.capital_cost + * ( + vagmd_params.cost_fraction_maintenance + + vagmd_params.cost_fraction_insurance + ), + to_units=base_currency / base_period, ) ) From 855ab5e66042aa620175ceeaadd65bc34fb84e65 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 14 Nov 2024 20:15:32 -0700 Subject: [PATCH 66/70] fix failing tests --- .../surrogate/trough/test_trough_surrogate.py | 4 ++-- .../surrogate/tests/test_lt_med_surrogate.py | 4 ++-- .../surrogate/tests/test_med_tvc_surrogate.py | 15 +++++++-------- .../surrogate/tests/test_vagmd_surrogate.py | 2 +- .../surrogate/tests/test_vagmd_surrogate_base.py | 2 +- 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/watertap_contrib/reflo/solar_models/surrogate/trough/test_trough_surrogate.py b/src/watertap_contrib/reflo/solar_models/surrogate/trough/test_trough_surrogate.py index db1cb103..170a423b 100644 --- a/src/watertap_contrib/reflo/solar_models/surrogate/trough/test_trough_surrogate.py +++ b/src/watertap_contrib/reflo/solar_models/surrogate/trough/test_trough_surrogate.py @@ -315,10 +315,10 @@ def test_costing(self, trough_frame): "aggregate_variable_operating_cost": 1313013.020, "aggregate_flow_heat": -149784.738, "aggregate_flow_electricity": 1843.139, - "aggregate_flow_costs": {"heat": -15413915.083, "electricity": 1327705.190}, + "aggregate_flow_costs": {"heat": -13130130.20, "electricity": 1327705.190}, "total_capital_cost": 249933275.0, "maintenance_labor_chemical_operating_cost": 0.0, - "total_operating_cost": -10773196.871, + "total_operating_cost": -8489411.99, "capital_recovery_factor": 0.11955949, "aggregate_direct_capital_cost": 249933275.0, } diff --git a/src/watertap_contrib/reflo/unit_models/surrogate/tests/test_lt_med_surrogate.py b/src/watertap_contrib/reflo/unit_models/surrogate/tests/test_lt_med_surrogate.py index 3e935ead..8b08275e 100644 --- a/src/watertap_contrib/reflo/unit_models/surrogate/tests/test_lt_med_surrogate.py +++ b/src/watertap_contrib/reflo/unit_models/surrogate/tests/test_lt_med_surrogate.py @@ -312,8 +312,8 @@ def test_costing(self, LT_MED_frame): m.fs.lt_med.costing.fixed_operating_cost ) - assert pytest.approx(1.57605, rel=1e-3) == value(m.fs.costing.LCOW) - assert pytest.approx(747363.88, rel=1e-3) == value( + assert pytest.approx(1.4818, rel=1e-3) == value(m.fs.costing.LCOW) + assert pytest.approx(678576.13, rel=1e-3) == value( m.fs.costing.total_operating_cost ) assert pytest.approx(4609113.13, rel=1e-3) == value( diff --git a/src/watertap_contrib/reflo/unit_models/surrogate/tests/test_med_tvc_surrogate.py b/src/watertap_contrib/reflo/unit_models/surrogate/tests/test_med_tvc_surrogate.py index 84ebafef..9bc5310f 100644 --- a/src/watertap_contrib/reflo/unit_models/surrogate/tests/test_med_tvc_surrogate.py +++ b/src/watertap_contrib/reflo/unit_models/surrogate/tests/test_med_tvc_surrogate.py @@ -183,7 +183,6 @@ def test_config(self, MED_TVC_frame): @pytest.mark.unit def test_build(self, MED_TVC_frame): m = MED_TVC_frame - # test ports port_lst = ["feed", "distillate", "brine", "steam", "motive"] for port_str in port_lst: @@ -335,25 +334,25 @@ def test_costing(self, MED_TVC_frame): assert pytest.approx(2254.658, rel=1e-3) == value( m.fs.med_tvc.costing.med_specific_cost ) - assert pytest.approx(5126761.859, rel=1e-3) == value( + assert pytest.approx(6018483.49, rel=1e-3) == value( m.fs.med_tvc.costing.capital_cost ) - assert pytest.approx(2705589.357, rel=1e-3) == value( + assert pytest.approx(3176185.15, rel=1e-3) == value( m.fs.med_tvc.costing.membrane_system_cost ) - assert pytest.approx(2421172.502, rel=1e-3) == value( + assert pytest.approx(2842298.34, rel=1e-3) == value( m.fs.med_tvc.costing.evaporator_system_cost ) - assert pytest.approx(239692.046, rel=1e-3) == value( + assert pytest.approx(261985.08, rel=1e-3) == value( m.fs.med_tvc.costing.fixed_operating_cost ) - assert pytest.approx(1.6905, rel=1e-3) == value(m.fs.costing.LCOW) + assert pytest.approx(1.7355, rel=1e-3) == value(m.fs.costing.LCOW) - assert pytest.approx(785633.993, rel=1e-3) == value( + assert pytest.approx(740379.40, rel=1e-3) == value( m.fs.costing.total_operating_cost ) - assert pytest.approx(5126761.859, rel=1e-3) == value( + assert pytest.approx(6018483.5, rel=1e-3) == value( m.fs.costing.total_capital_cost ) diff --git a/src/watertap_contrib/reflo/unit_models/surrogate/tests/test_vagmd_surrogate.py b/src/watertap_contrib/reflo/unit_models/surrogate/tests/test_vagmd_surrogate.py index fde951b7..3704c0d6 100644 --- a/src/watertap_contrib/reflo/unit_models/surrogate/tests/test_vagmd_surrogate.py +++ b/src/watertap_contrib/reflo/unit_models/surrogate/tests/test_vagmd_surrogate.py @@ -229,4 +229,4 @@ def test_costing(self, VAGMD_frame): assert pytest.approx(294352.922, rel=1e-3) == value( vagmd.costing.fixed_operating_cost ) - assert pytest.approx(2.648, rel=1e-3) == value(m.fs.costing.LCOW) + assert pytest.approx(2.37915, rel=1e-3) == value(m.fs.costing.LCOW) diff --git a/src/watertap_contrib/reflo/unit_models/surrogate/tests/test_vagmd_surrogate_base.py b/src/watertap_contrib/reflo/unit_models/surrogate/tests/test_vagmd_surrogate_base.py index 4396d80f..307d09ea 100644 --- a/src/watertap_contrib/reflo/unit_models/surrogate/tests/test_vagmd_surrogate_base.py +++ b/src/watertap_contrib/reflo/unit_models/surrogate/tests/test_vagmd_surrogate_base.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. From 92812df5224894cffa4d4fe693dccc645127cb5c Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 14 Nov 2024 20:15:48 -0700 Subject: [PATCH 67/70] fix unit --- .../reflo/unit_models/surrogate/med_tvc_surrogate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/surrogate/med_tvc_surrogate.py b/src/watertap_contrib/reflo/unit_models/surrogate/med_tvc_surrogate.py index 6735591b..d4243179 100644 --- a/src/watertap_contrib/reflo/unit_models/surrogate/med_tvc_surrogate.py +++ b/src/watertap_contrib/reflo/unit_models/surrogate/med_tvc_surrogate.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. @@ -348,7 +348,7 @@ def eq_feed_to_cooling_isobaric(b): self.specific_area_per_kg_s = Var( initialize=400, bounds=(0, None), - units=pyunits.m**2 / (pyunits.k / pyunits.s), + units=pyunits.m**2 / (pyunits.kg / pyunits.s), doc="Specific area (m2/kg/s))", ) From b0d59ba60b7ed3574e8d7753fdc0fa65c6ae9f2f Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 14 Nov 2024 20:29:12 -0700 Subject: [PATCH 68/70] remove return from test --- .../reflo/unit_models/tests/test_deep_well_injection.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/watertap_contrib/reflo/unit_models/tests/test_deep_well_injection.py b/src/watertap_contrib/reflo/unit_models/tests/test_deep_well_injection.py index 6986be4d..de0d030a 100644 --- a/src/watertap_contrib/reflo/unit_models/tests/test_deep_well_injection.py +++ b/src/watertap_contrib/reflo/unit_models/tests/test_deep_well_injection.py @@ -216,8 +216,6 @@ def test_smooth_bound_lower(): assert pytest.approx(value(m.fs.unit.pipe_diameter), rel=1e-3) == 2 - return m - @pytest.mark.component() def test_smooth_bound_upper(): @@ -262,8 +260,6 @@ def test_smooth_bound_upper(): assert pytest.approx(value(m.fs.unit.pipe_diameter), rel=1e-3) == 24 - return m - class TestDeepWellInjection_BLMCosting: @pytest.fixture(scope="class") From 15724cdec8679a8956a045cee16df04ea1f2948d Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 14 Nov 2024 20:30:46 -0700 Subject: [PATCH 69/70] fix test --- .../vagmd_batch/test/test_VAGMD_batch_flowsheet_multiperiod.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/watertap_contrib/reflo/analysis/multiperiod/vagmd_batch/test/test_VAGMD_batch_flowsheet_multiperiod.py b/src/watertap_contrib/reflo/analysis/multiperiod/vagmd_batch/test/test_VAGMD_batch_flowsheet_multiperiod.py index 20faeb3c..d0f0be2b 100644 --- a/src/watertap_contrib/reflo/analysis/multiperiod/vagmd_batch/test/test_VAGMD_batch_flowsheet_multiperiod.py +++ b/src/watertap_contrib/reflo/analysis/multiperiod/vagmd_batch/test/test_VAGMD_batch_flowsheet_multiperiod.py @@ -280,7 +280,7 @@ def test_costing(self, VAGMD_batch_frame_AS7C15L_Closed): assert pytest.approx(151892.658, rel=1e-3) == value( vagmd.costing.fixed_operating_cost ) - assert pytest.approx(2.777, rel=1e-3) == value(m.fs.costing.LCOW) + assert pytest.approx(2.500, rel=1e-3) == value(m.fs.costing.LCOW) class TestVAGMDbatchAS7C15L_HighSalinityClosed: From d989a0d5c12276c035e59f95409e63298a06dbf2 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 14 Nov 2024 20:30:53 -0700 Subject: [PATCH 70/70] fix test --- .../test/test_MED_VAGMD_semibatch_flowsheet_multiperiod.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/watertap_contrib/reflo/analysis/multiperiod/ltmed_vagmd_semibatch/test/test_MED_VAGMD_semibatch_flowsheet_multiperiod.py b/src/watertap_contrib/reflo/analysis/multiperiod/ltmed_vagmd_semibatch/test/test_MED_VAGMD_semibatch_flowsheet_multiperiod.py index 4a4f3471..1797534a 100644 --- a/src/watertap_contrib/reflo/analysis/multiperiod/ltmed_vagmd_semibatch/test/test_MED_VAGMD_semibatch_flowsheet_multiperiod.py +++ b/src/watertap_contrib/reflo/analysis/multiperiod/ltmed_vagmd_semibatch/test/test_MED_VAGMD_semibatch_flowsheet_multiperiod.py @@ -260,11 +260,11 @@ def test_costing(self, MED_VAGMD_semibatch_frame): 68657.789, rel=1e-3 ) assert cost_performance["Annual heat cost ($)"] == pytest.approx( - 150540.917, rel=1e-3 + 128236.19, rel=1e-3 ) assert cost_performance["Annual electricity cost ($)"] == pytest.approx( 4064.964, rel=1e-3 ) assert cost_performance["Overall LCOW ($/m3)"] == pytest.approx( - 1.76369, rel=1e-3 + 1.70261, rel=1e-3 )