From fd0d4f1b8980a07b6192a4d2d9f92f0785c271bd Mon Sep 17 00:00:00 2001 From: Mukta Hardikar Date: Mon, 21 Oct 2024 19:57:52 -0600 Subject: [PATCH 01/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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 cd5e92f4483bcb29bf976adc24f6a7df455cd421 Mon Sep 17 00:00:00 2001 From: Mukta Hardikar Date: Wed, 13 Nov 2024 15:27:08 -0700 Subject: [PATCH 52/76] solar energy base update, new pv costing, --- .../reflo/core/solar_energy_base.py | 3 + .../reflo/costing/solar/pv_surrogate.py | 215 ++++++++++++++++++ .../solar_models/surrogate/pv/pv_surrogate.py | 210 ++++++----------- 3 files changed, 288 insertions(+), 140 deletions(-) create mode 100644 src/watertap_contrib/reflo/costing/solar/pv_surrogate.py diff --git a/src/watertap_contrib/reflo/core/solar_energy_base.py b/src/watertap_contrib/reflo/core/solar_energy_base.py index f2c37582..a0433b35 100644 --- a/src/watertap_contrib/reflo/core/solar_energy_base.py +++ b/src/watertap_contrib/reflo/core/solar_energy_base.py @@ -381,6 +381,9 @@ def load_surrogate(self): oldstdout = sys.stdout sys.stdout = stream + if self.config.surrogate_model_file is not None: + self.surrogate_file = self.config.surrogate_model_file + self.surrogate_blk = SurrogateBlock(concrete=True) self.surrogate = PysmoSurrogate.load_from_file(self.surrogate_file) self.surrogate_blk.build_model( diff --git a/src/watertap_contrib/reflo/costing/solar/pv_surrogate.py b/src/watertap_contrib/reflo/costing/solar/pv_surrogate.py new file mode 100644 index 00000000..3ed6494c --- /dev/null +++ b/src/watertap_contrib/reflo/costing/solar/pv_surrogate.py @@ -0,0 +1,215 @@ +################################################################################# +# WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# National Renewable Energy Laboratory, and National Energy Technology +# Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +################################################################################# + +import pyomo.environ as pyo +from watertap.costing.util import register_costing_parameter_block +from watertap_contrib.reflo.costing.util import ( + make_capital_cost_var, + make_fixed_operating_cost_var, + make_variable_operating_cost_var, +) + + +def build_pv_surrogate_cost_param_block(blk): + + costing = blk.parent_block() + + blk.cost_per_watt_module = pyo.Var( + initialize=0.41, + units=costing.base_currency / pyo.units.watt, + bounds=(0, None), + doc="Cost per watt for solar module", + ) + + blk.cost_per_watt_inverter = pyo.Var( + initialize=0.05, + units=costing.base_currency / pyo.units.watt, + bounds=(0, None), + doc="Cost per watt for inverter", + ) + + blk.cost_per_watt_other = pyo.Var( + initialize=0.1, + units=costing.base_currency / pyo.units.watt, + bounds=(0, None), + doc="Cost per watt for other equipment, installation, and margin/overhead", + ) + + blk.cost_per_watt_indirect = pyo.Var( + initialize=0.13, + units=costing.base_currency / pyo.units.watt, + bounds=(0, None), + doc="Cost per watt for permitting, environmental studies, engineering, land prep, and grid interconnection", + ) + + blk.land_cost_per_acre = pyo.Var( + initialize=4000, + units=costing.base_currency / pyo.units.acre, + bounds=(0, None), + doc="Land cost per acre required", + ) + + blk.contingency_frac_direct_capital_cost = pyo.Var( + initialize=0.03, + units=pyo.units.dimensionless, + bounds=(0, 1), + doc="Fraction of direct costs for contingency", + ) + + blk.tax_frac_direct_capital_cost = pyo.Var( + initialize=0.05, + units=pyo.units.dimensionless, + bounds=(0, 1), + doc="Fraction of direct costs for sales tax", + ) + + blk.fixed_operating_by_capacity = pyo.Var( + initialize=0, + units=costing.base_currency / (pyo.units.kW * costing.base_period), + bounds=(0, None), + doc="Fixed operating cost of PV system per kW generated", + ) + + blk.variable_operating_by_generation = pyo.Var( + initialize=0, + units=costing.base_currency / (pyo.units.MWh * costing.base_period), + bounds=(0, None), + doc="Annual operating cost of PV system per MWh generated", + ) + + # blk.fix_all_vars() + + +@register_costing_parameter_block( + build_rule=build_pv_surrogate_cost_param_block, parameter_block_name="pv_surrogate" +) +def cost_pv_surrogate(blk): + + global_params = blk.costing_package + pv_params = blk.costing_package.pv_surrogate + make_capital_cost_var(blk) + make_variable_operating_cost_var(blk) + make_fixed_operating_cost_var(blk) + + blk.direct_capital_cost = pyo.Var( + initialize=0, + units=blk.config.flowsheet_costing_block.base_currency, + bounds=(0, None), + doc="Direct costs of PV system", + ) + + blk.indirect_capital_cost = pyo.Var( + initialize=0, + units=blk.config.flowsheet_costing_block.base_currency, + bounds=(0, None), + doc="Indirect costs of PV system", + ) + + # blk.direct_capital_cost = pyo.Var( + # initialize=0, + # units=blk.config.flowsheet_costing_block.base_currency, + # bounds=(0, None), + # doc="Direct costs of PV system", + # ) + + # blk.indirect_capital_cost = pyo.Var( + # initialize=0, + # units=blk.config.flowsheet_costing_block.base_currency, + # bounds=(0, None), + # doc="Indirect costs of PV system", + # ) + + blk.land_cost = pyo.Var( + initialize=0, + units=blk.config.flowsheet_costing_block.base_currency, + bounds=(0, None), + doc="Land costs of PV system", + ) + + blk.sales_tax = pyo.Var( + initialize=0, + units=blk.config.flowsheet_costing_block.base_currency, + bounds=(0, None), + doc="Sales tax for PV system", + ) + + blk.system_capacity = pyo.Var( + initialize=0, + units=pyo.units.watt, + bounds=(0, None), + doc="DC system capacity for PV system", + ) + + blk.land_area = pyo.Var( + initialize=0, + units=pyo.units.acre, + bounds=(0, None), + doc="Land area required for PV system", + ) + + blk.annual_generation = pyo.Var( + initialize=0, + units=pyo.units.kWh, + bounds=(0, None), + doc="Annual electricity generation of PV system", + ) + + blk.direct_capital_cost_constraint = pyo.Constraint( + expr=blk.direct_capital_cost + == blk.system_capacity + * ( + pv_params.cost_per_watt_module + + pv_params.cost_per_watt_inverter + + pv_params.cost_per_watt_other + ) + + ( + blk.system_capacity + * ( + pv_params.cost_per_watt_module + + pv_params.cost_per_watt_inverter + + pv_params.cost_per_watt_other + ) + ) + * pv_params.contingency_frac_direct_capital_cost + ) + + blk.indirect_capital_cost_constraint = pyo.Constraint( + expr=blk.indirect_capital_cost + == (blk.system_capacity * pv_params.cost_per_watt_indirect) + + (blk.land_area * pv_params.land_cost_per_acre) + ) + + blk.land_cost_constraint = pyo.Constraint( + expr=blk.land_cost == (blk.land_area * pv_params.land_cost_per_acre) + ) + + blk.sales_tax_constraint = pyo.Constraint( + expr=blk.sales_tax == blk.direct_capital_cost * global_params.sales_tax_frac + ) + + blk.capital_cost_constraint = pyo.Constraint( + expr=blk.capital_cost + == blk.direct_capital_cost + blk.indirect_capital_cost + blk.sales_tax + ) + + blk.fixed_operating_cost_constraint = pyo.Constraint( + expr=blk.fixed_operating_cost + == pv_params.fixed_operating_by_capacity + * pyo.units.convert(blk.system_capacity, to_units=pyo.units.kW) + ) + + blk.variable_operating_cost_constraint = pyo.Constraint( + expr=blk.variable_operating_cost + == pv_params.variable_operating_by_generation * blk.annual_generation + ) + + blk.costing_package.cost_flow(-1 * blk.unit_model.electricity, "electricity") diff --git a/src/watertap_contrib/reflo/solar_models/surrogate/pv/pv_surrogate.py b/src/watertap_contrib/reflo/solar_models/surrogate/pv/pv_surrogate.py index 501da2b6..b67ce81d 100644 --- a/src/watertap_contrib/reflo/solar_models/surrogate/pv/pv_surrogate.py +++ b/src/watertap_contrib/reflo/solar_models/surrogate/pv/pv_surrogate.py @@ -11,21 +11,24 @@ # ############################################################################### -import os -import sys -import time import pandas as pd -from io import StringIO - -from pyomo.environ import ConcreteModel, Var, Constraint, units as pyunits - -from idaes.core import FlowsheetBlock +from pyomo.environ import ( + Var, + Param, + Constraint, + Expression, + value, + check_optimal_termination, + units as pyunits, +) from idaes.core import declare_process_block_class -from idaes.core.surrogate.surrogate_block import SurrogateBlock -from idaes.core.surrogate.pysmo_surrogate import PysmoRBFTrainer, PysmoSurrogate -from idaes.core.surrogate.sampling.data_utils import split_training_validation +import idaes.core.util.scaling as iscale +from idaes.core.util.exceptions import InitializationError +import idaes.logger as idaeslog +from watertap.core.solvers import get_solver from watertap_contrib.reflo.core import SolarEnergyBaseData +from watertap_contrib.reflo.costing.solar.pv_surrogate import cost_pv_surrogate __author__ = "Zachary Binger, Matthew Boyd, Kurban Sitterley" @@ -42,147 +45,74 @@ def build(self): super().build() self._tech_type = "PV" - self.surrogate_file = os.path.join( - os.path.dirname(__file__), "pv_surrogate.json" - ) - - self.design_size = Var( - initialize=1000, - bounds=[1, 200000], - units=pyunits.kW, - doc="PV design size in kW", - ) - - self.annual_energy = Var( - initialize=1, - units=pyunits.kWh, - doc="Annual energy produced by the plant in kWh", - ) - - self.land_req = Var( - initialize=7e7, - units=pyunits.acre, - doc="Land area required by the plant in acres", - ) - - self.surrogate_inputs = [self.design_size] - self.surrogate_outputs = [self.annual_energy, self.land_req] - - self.input_labels = ["design_size"] - self.output_labels = ["annual_energy", "land_req"] + self.add_surrogate_variables() + self.get_surrogate_data() self.electricity_constraint = Constraint( expr=self.annual_energy - == -1 - * self.electricity - * pyunits.convert(1 * pyunits.year, to_units=pyunits.hour) + == pyunits.convert(self.electricity, to_units=pyunits.kWh / pyunits.year) ) + # self.electricity_constraint = Constraint( + # expr= -1 * self.electricity + # == pyunits.convert(self.annual_energy * (pyunits.kW / pyunits.kWh), to_units=pyunits.kW) + # ) - def load_surrogate(self): - print("Loading surrogate file...") - self.surrogate_file = os.path.join( - os.path.dirname(__file__), "pv_surrogate.json" - ) + def calculate_scaling_factors(self): - if os.path.exists(self.surrogate_file): - stream = StringIO() - oldstdout = sys.stdout - sys.stdout = stream - - self.surrogate_blk = SurrogateBlock(concrete=True) - self.surrogate = PysmoSurrogate.load_from_file(self.surrogate_file) - self.surrogate_blk.build_model( - self.surrogate, - input_vars=self.surrogate_inputs, - output_vars=self.surrogate_outputs, - ) - - # Revert back to standard output - sys.stdout = oldstdout - - def get_training_validation(self): - self.dataset_filename = os.path.join( - os.path.dirname(__file__), "data/dataset.pkl" - ) - print("Loading Training Data...\n") - time_start = time.process_time() - pkl_data = pd.read_pickle(self.dataset_filename) - data = pkl_data.sample(n=int(len(pkl_data))) # FIX default this to 100% of data - self.data_training, self.data_validation = split_training_validation( - data, self.training_fraction, seed=len(data) - ) - time_stop = time.process_time() - print("Data Loading Time:", time_stop - time_start, "\n") + if iscale.get_scaling_factor(self.design_size) is None: + sf = iscale.get_scaling_factor(self.design_size, default=1) + iscale.set_scaling_factor(self.design_size, sf) + + if iscale.get_scaling_factor(self.annual_energy) is None: + sf = iscale.get_scaling_factor(self.annual_energy, default=1, warning=True) + iscale.set_scaling_factor(self.annual_energy, sf) + + if iscale.get_scaling_factor(self.electricity) is None: + sf = iscale.get_scaling_factor(self.electricity, default=1, warning=True) + iscale.set_scaling_factor(self.electricity, sf) - def create_surrogate( + def initialize( self, - save=False, + outlvl=idaeslog.NOTSET, + solver=None, + optarg=None, ): - self.sample_fraction = 0.1 # fraction of the generated data to train with. More flexible than n_samples. - self.training_fraction = 0.8 - - self.get_training_validation() - time_start = time.process_time() - # Capture long output - stream = StringIO() - oldstdout = sys.stdout - sys.stdout = stream - - # Create PySMO trainer object - trainer = PysmoRBFTrainer( - input_labels=self.input_labels, - output_labels=self.output_labels, - training_dataframe=self.data_training, + """ + General wrapper for initialization routines + + Keyword Arguments: + outlvl : sets output level of initialization routine + optarg : solver options dictionary object (default=None) + solver : str indicating which solver to use during + initialization (default = None) + + Returns: None + """ + init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") + + if solver is None: + opt = get_solver(optarg) + + self.init_data = pd.DataFrame( + { + "design_size": [value(self.design_size)], + "annual_energy": [value(self.annual_energy)], + "land_req": [value(self.land_req)], + } ) + self.init_output = self.surrogate.evaluate_surrogate(self.init_data) - # Set PySMO options - trainer.config.basis_function = "gaussian" # default = gaussian - trainer.config.solution_method = "algebraic" # default = algebraic - trainer.config.regularization = True # default = True - - # Train surrogate - rbf_train = trainer.train_surrogate() - - # Remove autogenerated 'solution.pickle' file - try: - os.remove("solution.pickle") - except FileNotFoundError: - pass - except Exception as e: - raise e - # Create callable surrogate object - xmin, xmax = [self.design_size.bounds[0]], [self.design_size.bounds[1]] - input_bounds = { - self.input_labels[i]: (xmin[i], xmax[i]) - for i in range(len(self.input_labels)) - } - rbf_surr = PysmoSurrogate( - rbf_train, self.input_labels, self.output_labels, input_bounds - ) - - # Save model to JSON - if (self.surrogate_file is not None) and (save is True): - print(f"Writing surrogate model to {self.surrogate_file}") - model = rbf_surr.save_to_file(self.surrogate_file, overwrite=True) - - # Revert back to standard output - sys.stdout = oldstdout - - time_stop = time.process_time() - print("Model Training Time:", time_stop - time_start, "\n") - - return rbf_surr + self.electricity.set_value(value(self.electricity_annual) / 8766) + # Create solver + res = opt.solve(self) + init_log.info_high(f"Initialization Step 2 {idaeslog.condition(res)}") -if __name__ == "__main__": - m = ConcreteModel() - m.fs = FlowsheetBlock(dynamic=False) - m.fs.pv = PVSurrogate() - m.fs.pv.create_surrogate(save=False) + if not check_optimal_termination(res): + raise InitializationError(f"Unit model {self.name} failed to initialize") - m.fs.pv.load_surrogate() + init_log.info("Initialization Complete: {}".format(idaeslog.condition(res))) - results = m.fs.pv.surrogate.evaluate_surrogate( - m.fs.pv.data_validation[m.fs.pv.input_labels] - ) - print(results) + @property + def default_costing_method(self): + return cost_pv_surrogate From ac5f92a6f9ad7f8a47c054b9345bc05701ea8dcc Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Wed, 13 Nov 2024 15:39:02 -0700 Subject: [PATCH 53/76] 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 54/76] 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 e7a54c5698315d21c2dea104ae84a528b1914724 Mon Sep 17 00:00:00 2001 From: Mukta Hardikar Date: Wed, 13 Nov 2024 17:35:18 -0700 Subject: [PATCH 55/76] checkpoint --- .../reflo/costing/solar/photovoltaic.py | 49 ++++++++++--------- .../flat_plate/flat_plate_surrogate.py | 5 +- .../solar_models/surrogate/pv/pv_surrogate.py | 4 -- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/solar/photovoltaic.py b/src/watertap_contrib/reflo/costing/solar/photovoltaic.py index 66d71e00..87550307 100644 --- a/src/watertap_contrib/reflo/costing/solar/photovoltaic.py +++ b/src/watertap_contrib/reflo/costing/solar/photovoltaic.py @@ -94,6 +94,7 @@ def cost_pv(blk): global_params = blk.costing_package pv_params = blk.costing_package.photovoltaic + pv = blk.unit_model make_capital_cost_var(blk) make_variable_operating_cost_var(blk) make_fixed_operating_cost_var(blk) @@ -119,37 +120,37 @@ def cost_pv(blk): doc="Sales tax for PV system", ) - blk.system_capacity = pyo.Var( - initialize=0, - units=pyo.units.watt, - bounds=(0, None), - doc="DC system capacity for PV system", - ) + # pv.design_size = pyo.Var( + # initialize=0, + # units=pyo.units.watt, + # bounds=(0, None), + # doc="DC system capacity for PV system", + # ) - blk.land_area = pyo.Var( - initialize=0, - units=pyo.units.acre, - bounds=(0, None), - doc="Land area required for PV system", - ) + # blk.land_area = pyo.Var( + # initialize=0, + # units=pyo.units.acre, + # bounds=(0, None), + # doc="Land area required for PV system", + # ) - blk.annual_generation = pyo.Var( - initialize=0, - units=pyo.units.MWh, - bounds=(0, None), - doc="Annual electricity generation of PV system", - ) + # blk.annual_generation = pyo.Var( + # initialize=0, + # units=pyo.units.MWh, + # bounds=(0, None), + # doc="Annual electricity generation of PV system", + # ) blk.direct_cost_constraint = pyo.Constraint( expr=blk.direct_cost - == blk.system_capacity + == pv.design_size * ( pv_params.cost_per_watt_module + pv_params.cost_per_watt_inverter + pv_params.cost_per_watt_other ) + ( - blk.system_capacity + pv.design_size * ( pv_params.cost_per_watt_module + pv_params.cost_per_watt_inverter @@ -161,8 +162,8 @@ def cost_pv(blk): blk.indirect_cost_constraint = pyo.Constraint( expr=blk.indirect_cost - == (blk.system_capacity * pv_params.cost_per_watt_indirect) - + (blk.land_area * pv_params.land_cost_per_acre) + == (pv.design_size * pv_params.cost_per_watt_indirect) + + (pv.land_req * pv_params.land_cost_per_acre) ) blk.sales_tax_constraint = pyo.Constraint( @@ -177,12 +178,12 @@ def cost_pv(blk): blk.fixed_operating_cost_constraint = pyo.Constraint( expr=blk.fixed_operating_cost == pv_params.fixed_operating_by_capacity - * pyo.units.convert(blk.system_capacity, to_units=pyo.units.kW) + * pyo.units.convert(pv.design_size, to_units=pyo.units.kW) ) blk.variable_operating_cost_constraint = pyo.Constraint( expr=blk.variable_operating_cost - == pv_params.variable_operating_by_generation * blk.annual_generation + == pv_params.variable_operating_by_generation * pv.annual_energy ) blk.costing_package.cost_flow(blk.unit_model.electricity, "electricity") diff --git a/src/watertap_contrib/reflo/solar_models/surrogate/flat_plate/flat_plate_surrogate.py b/src/watertap_contrib/reflo/solar_models/surrogate/flat_plate/flat_plate_surrogate.py index 1669f951..e7d343ba 100644 --- a/src/watertap_contrib/reflo/solar_models/surrogate/flat_plate/flat_plate_surrogate.py +++ b/src/watertap_contrib/reflo/solar_models/surrogate/flat_plate/flat_plate_surrogate.py @@ -102,7 +102,10 @@ def build(self): doc="Annual electricity consumed by flat plate collector in kWh", ) - self.create_rbf_surrogate() + if self.config.surrogate_model_file is not None: + self.surrogate_file = self.config.surrogate_model_file + else: + self.create_rbf_surrogate() self.heat_constraint = Constraint( expr=self.heat_annual diff --git a/src/watertap_contrib/reflo/solar_models/surrogate/pv/pv_surrogate.py b/src/watertap_contrib/reflo/solar_models/surrogate/pv/pv_surrogate.py index b67ce81d..0c247e06 100644 --- a/src/watertap_contrib/reflo/solar_models/surrogate/pv/pv_surrogate.py +++ b/src/watertap_contrib/reflo/solar_models/surrogate/pv/pv_surrogate.py @@ -52,10 +52,6 @@ def build(self): expr=self.annual_energy == pyunits.convert(self.electricity, to_units=pyunits.kWh / pyunits.year) ) - # self.electricity_constraint = Constraint( - # expr= -1 * self.electricity - # == pyunits.convert(self.annual_energy * (pyunits.kW / pyunits.kWh), to_units=pyunits.kW) - # ) def calculate_scaling_factors(self): From 757919352fafa2ea3cdf8f034393713c91b8070f Mon Sep 17 00:00:00 2001 From: Mukta Hardikar Date: Wed, 13 Nov 2024 22:59:48 -0700 Subject: [PATCH 56/76] Updated FPC component file, surrogate model and pickle --- .../case_studies/KBHDP/components/FPC.py | 228 ++++++++++++++++++ .../data/flat_plate_data_heat_load_1_400.pkl | Bin 0 -> 315361 bytes .../flat_plate/data/pysam_run_flat_plate.py | 10 +- .../flat_plate/flat_plate_surrogate.py | 1 + 4 files changed, 234 insertions(+), 5 deletions(-) create mode 100644 src/watertap_contrib/reflo/analysis/case_studies/KBHDP/components/FPC.py create mode 100644 src/watertap_contrib/reflo/solar_models/surrogate/flat_plate/data/flat_plate_data_heat_load_1_400.pkl diff --git a/src/watertap_contrib/reflo/analysis/case_studies/KBHDP/components/FPC.py b/src/watertap_contrib/reflo/analysis/case_studies/KBHDP/components/FPC.py new file mode 100644 index 00000000..2d5c0eb2 --- /dev/null +++ b/src/watertap_contrib/reflo/analysis/case_studies/KBHDP/components/FPC.py @@ -0,0 +1,228 @@ +from pyomo.environ import ( + ConcreteModel, + value, + assert_optimal_termination, + units as pyunits, + Block, + Constraint, + SolverFactory, +) +import os + +from idaes.core import FlowsheetBlock, UnitModelCostingBlock +from idaes.core.solvers import get_solver + +from watertap.core.util.model_diagnostics.infeasible import * +from idaes.core.util.scaling import * + +from watertap_contrib.reflo.solar_models.surrogate.flat_plate.flat_plate_surrogate import ( + FlatPlateSurrogate, +) + +from idaes.core.util.model_statistics import ( + degrees_of_freedom, + number_variables, + number_total_constraints, + number_unused_variables, +) + +from watertap_contrib.reflo.costing import ( + EnergyCosting, +) + +__all__ = [ + "build_fpc", + "init_fpc", + "set_fpc_op_conditions", + "add_fpc_costing", + "report_fpc", + "report_fpc_costing", +] + + +def build_system(): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + m.fs.costing = EnergyCosting() + + m.fs.system_capacity = Var(initialize=6000, units=pyunits.m**3 / pyunits.day) + + m.fs.fpc = FlowsheetBlock(dynamic=False) + + return m + + +def build_fpc(blk, __file__=None): + + print(f'\n{"=======> BUILDING FPC SYSTEM <=======":^60}\n') + + if __file__ == None: + cwd = os.getcwd() + __file__ = cwd + r'\src\watertap_contrib\reflo\solar_models\surrogate\flat_plate\\' + + dataset_filename = os.path.join( + os.path.dirname(__file__), r"data\flat_plate_data_heat_load_1_400.pkl" + ) + surrogate_filename = os.path.join( + os.path.dirname(__file__), r"data\flat_plate_data_heat_load_1_400_heat_load_1_400_hours_storage_0_27_temperature_hot_50_102.json" + ) + + input_bounds = dict( + heat_load=[1, 400], hours_storage=[0, 27], temperature_hot=[50, 102] + ) + input_units = dict(heat_load="MW", hours_storage="hour", temperature_hot="degK") + input_variables = { + "labels": ["heat_load", "hours_storage", "temperature_hot"], + "bounds": input_bounds, + "units": input_units, + } + + output_units = dict(heat_annual_scaled="kWh", electricity_annual_scaled="kWh") + output_variables = { + "labels": ["heat_annual_scaled", "electricity_annual_scaled"], + "units": output_units, + } + + blk.unit = FlatPlateSurrogate( + surrogate_model_file = surrogate_filename, + dataset_filename=dataset_filename, + input_variables=input_variables, + output_variables=output_variables, + scale_training_data=True, + ) + + +def init_fpc(blk): + blk.unit.initialize() + + +def set_system_op_conditions(m): + m.fs.system_capacity.fix() + + +def set_fpc_op_conditions(blk, hours_storage=6, temperature_hot=80): + + blk.unit.hours_storage.fix(hours_storage) + # Assumes the hot temperature to the inlet of a 'MD HX' + blk.unit.temperature_hot.fix(temperature_hot) + # Assumes the cold temperature from the outlet temperature of a 'MD HX' + blk.unit.temperature_cold.set_value(20) + + +def add_fpc_costing(blk, costing_block): + blk.unit.costing = UnitModelCostingBlock(flowsheet_costing_block=costing_block) + + +def calc_costing(m, blk): + blk.costing.heat_cost.set_value(0) + blk.costing.cost_process() + blk.costing.initialize() + + # TODO: Connect to the treatment volume + blk.costing.add_LCOW(m.fs.system_capacity) + + +def report_fpc(m, blk): + # blk = m.fs.fpc + print(f"\n\n-------------------- FPC Report --------------------\n") + print("\n") + + print( + f'{"Number of collectors":<30s}{value(blk.number_collectors):<20,.2f}{pyunits.get_units(blk.number_collectors)}' + ) + + print( + f'{"Collector area":<30s}{value(blk.collector_area_total):<20,.2f}{pyunits.get_units(blk.collector_area_total)}' + ) + + print( + f'{"Storage volume":<30s}{value(blk.storage_volume):<20,.2f}{pyunits.get_units(blk.storage_volume)}' + ) + + print( + f'{"Heat load":<30s}{value(blk.heat_load):<20,.2f}{pyunits.get_units(blk.heat_load)}' + ) + + print( + f'{"Heat annual":<30s}{value(blk.heat_annual):<20,.2f}{pyunits.get_units(blk.heat_annual)}' + ) + + print(f'{"Heat":<30s}{value(blk.heat):<20,.2f}{pyunits.get_units(blk.heat)}') + + print( + f'{"Electricity annual":<30s}{value(blk.electricity_annual):<20,.2f}{pyunits.get_units(blk.electricity_annual)}' + ) + + print( + f'{"Electricity":<30s}{value(blk.electricity):<20,.2f}{pyunits.get_units(blk.electricity)}' + ) + + +def report_fpc_costing(m, blk): + print(f"\n\n-------------------- FPC Costing Report --------------------\n") + print("\n") + + print( + f'{"LCOW":<30s}{value(blk.costing.LCOW):<20,.2f}{pyunits.get_units(blk.costing.LCOW)}' + ) + + print( + f'{"Capital Cost":<30s}{value(blk.costing.total_capital_cost):<20,.2f}{pyunits.get_units(blk.costing.total_capital_cost)}' + ) + + print( + f'{"Fixed Operating Cost":<30s}{value(blk.costing.total_fixed_operating_cost):<20,.2f}{pyunits.get_units(blk.costing.total_fixed_operating_cost)}' + ) + + print( + f'{"Variable Operating Cost":<30s}{value(blk.costing.total_variable_operating_cost):<20,.2f}{pyunits.get_units(blk.costing.total_variable_operating_cost)}' + ) + + print( + f'{"Total Operating Cost":<30s}{value(blk.costing.total_operating_cost):<20,.2f}{pyunits.get_units(blk.costing.total_operating_cost)}' + ) + + # print( + # f'{"Aggregated Variable Operating Cost":<30s}{value(blk.costing.aggregate_variable_operating_cost):<20,.2f}{pyunits.get_units(blk.costing.aggregate_variable_operating_cost)}' + # ) + + # print( + # f'{"Heat flow":<30s}{value(blk.costing.aggregate_flow_heat):<20,.2f}{pyunits.get_units(blk.costing.aggregate_flow_heat)}' + # ) + + # print( + # f'{"Heat Cost":<30s}{value(blk.costing.aggregate_flow_costs["heat"]):<20,.2f}{pyunits.get_units(blk.costing.aggregate_flow_costs["heat"])}' + # ) + + # print( + # f'{"Elec Flow":<30s}{value(blk.costing.aggregate_flow_electricity):<20,.2f}{pyunits.get_units(blk.costing.aggregate_flow_electricity)}' + # ) + + # print( + # f'{"Elec Cost":<30s}{value(blk.costing.aggregate_flow_costs["electricity"]):<20,.2f}{pyunits.get_units(blk.costing.aggregate_flow_costs["electricity"])}' + # ) + + +if __name__ == "__main__": + + solver = get_solver() + solver = SolverFactory("ipopt") + + m = build_system() + + build_fpc(m.fs.fpc) + init_fpc(m.fs.fpc) + set_fpc_op_conditions(m.fs.fpc) + + print("Degrees of Freedom:", degrees_of_freedom(m)) + + add_fpc_costing(m.fs.fpc, costing_block=m.fs.costing) + calc_costing(m, m.fs) + m.fs.costing.aggregate_flow_heat.fix(-4000) + results = solver.solve(m) + + print(degrees_of_freedom(m)) + report_fpc(m, m.fs.fpc.unit) + report_fpc_costing(m, m.fs) + + # m.fs.costing.used_flows.display() diff --git a/src/watertap_contrib/reflo/solar_models/surrogate/flat_plate/data/flat_plate_data_heat_load_1_400.pkl b/src/watertap_contrib/reflo/solar_models/surrogate/flat_plate/data/flat_plate_data_heat_load_1_400.pkl new file mode 100644 index 0000000000000000000000000000000000000000..559ef9197c6569c820fb442857f9ab4890d09e7d GIT binary patch literal 315361 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 qP Date: Wed, 13 Nov 2024 23:56:34 -0700 Subject: [PATCH 57/76] updates to pv_surrogate, trough_surrogate, added working PV, CST component --- .../case_studies/KBHDP/components/CST.py | 218 ++++++++++++++ .../case_studies/KBHDP/components/PV.py | 275 ++++++++++++++++++ .../solar_models/surrogate/pv/pv_surrogate.py | 8 +- .../surrogate/trough/trough_surrogate.py | 6 +- 4 files changed, 505 insertions(+), 2 deletions(-) create mode 100644 src/watertap_contrib/reflo/analysis/case_studies/KBHDP/components/CST.py create mode 100644 src/watertap_contrib/reflo/analysis/case_studies/KBHDP/components/PV.py diff --git a/src/watertap_contrib/reflo/analysis/case_studies/KBHDP/components/CST.py b/src/watertap_contrib/reflo/analysis/case_studies/KBHDP/components/CST.py new file mode 100644 index 00000000..f5d6ddbe --- /dev/null +++ b/src/watertap_contrib/reflo/analysis/case_studies/KBHDP/components/CST.py @@ -0,0 +1,218 @@ +from pyomo.environ import ( + ConcreteModel, + value, + assert_optimal_termination, + units as pyunits, + Block, + Constraint, + SolverFactory, +) +import os + +from idaes.core import FlowsheetBlock, UnitModelCostingBlock +from idaes.core.solvers import get_solver + +from watertap.core.util.model_diagnostics.infeasible import * +from idaes.core.util.scaling import * + +from watertap_contrib.reflo.solar_models.surrogate.trough.trough_surrogate import ( + TroughSurrogate, +) + +from idaes.core.util.model_statistics import ( + degrees_of_freedom, + number_variables, + number_total_constraints, + number_unused_variables, +) + +from watertap_contrib.reflo.costing import ( + EnergyCosting, +) + +__all__ = [ + "build_cst", + "init_cst", + "set_cst_op_conditions", + "add_cst_costing", + "report_cst", + "report_cst_costing", +] + + +def build_system(): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + m.fs.costing = EnergyCosting() + + m.fs.system_capacity = Var(initialize=6000, units=pyunits.m**3 / pyunits.day) + + m.fs.cst = FlowsheetBlock(dynamic=False) + + return m + + +def build_cst(blk, __file__=None): + + print(f'\n{"=======> BUILDING CST SYSTEM <=======":^60}\n') + + if __file__ == None: + cwd = os.getcwd() + __file__ = cwd + r'\src\watertap_contrib\reflo\solar_models\surrogate\trough\\' + + dataset_filename = os.path.join( + os.path.dirname(__file__), r"data\test_trough_data.pkl" + ) + surrogate_filename = os.path.join( + os.path.dirname(__file__), r"data\test_trough_data_heat_load_100_500_hours_storage_0_26.json" + ) + + input_bounds = dict( + heat_load=[100, 500], hours_storage=[0, 26] + ) + input_units = dict(heat_load="MW", hours_storage="hour") + input_variables = { + "labels": ["heat_load", "hours_storage"], + "bounds": input_bounds, + "units": input_units, + } + + output_units = dict(heat_annual_scaled="kWh", electricity_annual_scaled="kWh") + output_variables = { + "labels": ["heat_annual_scaled", "electricity_annual_scaled"], + "units": output_units, + } + + + blk.unit = TroughSurrogate( + surrogate_model_file = surrogate_filename, + dataset_filename=dataset_filename, + input_variables=input_variables, + output_variables=output_variables, + scale_training_data=True + ) + + +def init_cst(blk): + # Fix input variables for initialization + blk.unit.hours_storage.fix() + blk.unit.heat_load.fix() + blk.unit.initialize() + + blk.unit.heat_load.unfix() + + +def set_system_op_conditions(m): + m.fs.system_capacity.fix() + + +def set_cst_op_conditions(blk, hours_storage=6): + blk.unit.hours_storage.fix(hours_storage) + + +def add_cst_costing(blk, costing_block): + blk.unit.costing = UnitModelCostingBlock(flowsheet_costing_block=costing_block) + + +def calc_costing(m, blk): + blk.costing.heat_cost.set_value(0) + blk.costing.cost_process() + blk.costing.initialize() + + # TODO: Connect to the treatment volume + blk.costing.add_LCOW(m.fs.system_capacity) + + +def report_cst(m, blk): + # blk = m.fs.cst + print(f"\n\n-------------------- CST Report --------------------\n") + print("\n") + + + print( + f'{"Heat load":<30s}{value(blk.heat_load):<20,.2f}{pyunits.get_units(blk.heat_load)}' + ) + + print( + f'{"Heat annual":<30s}{value(blk.heat_annual):<20,.2f}{pyunits.get_units(blk.heat_annual)}' + ) + + print(f'{"Heat":<30s}{value(blk.heat):<20,.2f}{pyunits.get_units(blk.heat)}') + + print( + f'{"Electricity annual":<30s}{value(blk.electricity_annual):<20,.2f}{pyunits.get_units(blk.electricity_annual)}' + ) + + print( + f'{"Electricity":<30s}{value(blk.electricity):<20,.2f}{pyunits.get_units(blk.electricity)}' + ) + + +def report_cst_costing(m, blk): + print(f"\n\n-------------------- CST Costing Report --------------------\n") + print("\n") + + print( + f'{"LCOW":<30s}{value(blk.costing.LCOW):<20,.2f}{pyunits.get_units(blk.costing.LCOW)}' + ) + + print( + f'{"Capital Cost":<30s}{value(blk.costing.total_capital_cost):<20,.2f}{pyunits.get_units(blk.costing.total_capital_cost)}' + ) + + print( + f'{"Fixed Operating Cost":<30s}{value(blk.costing.total_fixed_operating_cost):<20,.2f}{pyunits.get_units(blk.costing.total_fixed_operating_cost)}' + ) + + print( + f'{"Variable Operating Cost":<30s}{value(blk.costing.total_variable_operating_cost):<20,.2f}{pyunits.get_units(blk.costing.total_variable_operating_cost)}' + ) + + print( + f'{"Total Operating Cost":<30s}{value(blk.costing.total_operating_cost):<20,.2f}{pyunits.get_units(blk.costing.total_operating_cost)}' + ) + + # print( + # f'{"Aggregated Variable Operating Cost":<30s}{value(blk.costing.aggregate_variable_operating_cost):<20,.2f}{pyunits.get_units(blk.costing.aggregate_variable_operating_cost)}' + # ) + + # print( + # f'{"Heat flow":<30s}{value(blk.costing.aggregate_flow_heat):<20,.2f}{pyunits.get_units(blk.costing.aggregate_flow_heat)}' + # ) + + # print( + # f'{"Heat Cost":<30s}{value(blk.costing.aggregate_flow_costs["heat"]):<20,.2f}{pyunits.get_units(blk.costing.aggregate_flow_costs["heat"])}' + # ) + + # print( + # f'{"Elec Flow":<30s}{value(blk.costing.aggregate_flow_electricity):<20,.2f}{pyunits.get_units(blk.costing.aggregate_flow_electricity)}' + # ) + + # print( + # f'{"Elec Cost":<30s}{value(blk.costing.aggregate_flow_costs["electricity"]):<20,.2f}{pyunits.get_units(blk.costing.aggregate_flow_costs["electricity"])}' + # ) + + +if __name__ == "__main__": + + solver = get_solver() + solver = SolverFactory("ipopt") + + m = build_system() + + build_cst(m.fs.cst) + + init_cst(m.fs.cst) + + set_cst_op_conditions(m.fs.cst) + + add_cst_costing(m.fs.cst, costing_block=m.fs.costing) + calc_costing(m, m.fs) + m.fs.costing.aggregate_flow_heat.fix(-70000) + results = solver.solve(m) + + print(degrees_of_freedom(m)) + report_cst(m, m.fs.cst.unit) + report_cst_costing(m, m.fs) + + # m.fs.costing.used_flows.display() diff --git a/src/watertap_contrib/reflo/analysis/case_studies/KBHDP/components/PV.py b/src/watertap_contrib/reflo/analysis/case_studies/KBHDP/components/PV.py new file mode 100644 index 00000000..564a74e5 --- /dev/null +++ b/src/watertap_contrib/reflo/analysis/case_studies/KBHDP/components/PV.py @@ -0,0 +1,275 @@ +from pyomo.environ import ( + ConcreteModel, + value, + Constraint, + units as pyunits, +) +import os +from pyomo.util.check_units import assert_units_consistent +from idaes.core import FlowsheetBlock, UnitModelCostingBlock +from idaes.core.util.model_statistics import * +import idaes.core.util.scaling as iscale +from idaes.core.solvers import get_solver +from watertap.core.util.model_diagnostics.infeasible import * +from watertap.core.util.initialization import * +from watertap_contrib.reflo.solar_models.surrogate.pv.pv_surrogate import PVSurrogate +from watertap_contrib.reflo.costing import ( + TreatmentCosting, + EnergyCosting, + REFLOCosting, + REFLOSystemCosting, +) +# from watertap_contrib.reflo.analysis.case_studies.KBHDP.utils import ( + # check_jac, + # calc_scale, +# ) + +__all__ = [ + "build_pv", + "train_pv_surrogate", + "set_pv_constraints", + "add_pv_scaling", + "add_pv_costing_scaling", + "print_PV_costing_breakdown", + "report_PV", +] + + +def build_system(): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + energy = m.fs.energy = Block() + + print(f"Degrees of Freedom: {degrees_of_freedom(m)}") + return m + + +def build_pv(m): + energy = m.fs.energy + + # Get directory path + cwd = os.getcwd() + energy.pv = PVSurrogate( + surrogate_model_file= cwd + "/src/watertap_contrib/reflo/solar_models/surrogate/pv/pv_surrogate.json", + dataset_filename=cwd + "/src/watertap_contrib/reflo/solar_models/surrogate/pv/data/dataset.pkl", + input_variables={ + "labels": ["design_size"], + "bounds": {"design_size": [1, 200000]}, + "units": {"design_size": "kW"}, + }, + output_variables={ + "labels": ["annual_energy", "land_req"], + "units": {"annual_energy": "kWh", "land_req": "acre"}, + }, + scale_training_data=False, + ) + + +def train_pv_surrogate(m): + energy = m.fs.energy + + energy.pv.create_rbf_surrogate() + + assert False + + +def set_pv_constraints(m, focus="Size"): + energy = m.fs.energy + # m.fs.energy.pv.load_surrogate() + + m.fs.energy.pv.heat.fix(0) + + if focus == "Size": + m.fs.energy.pv.design_size.fix(500) + elif focus == "Energy": + m.fs.energy.pv.annual_energy.fix(10000000) + + m.fs.energy.pv.initialize() + + +def add_pv_costing(m, blk): + energy = m.fs.energy + energy.costing = EnergyCosting() + + energy.pv.costing = UnitModelCostingBlock( + flowsheet_costing_block=energy.costing, + ) + + +def add_pv_scaling(m, blk): + pv = blk + + # print(calc_scale(value(pv.annual_energy))) + + iscale.set_scaling_factor(pv.design_size, 1000) + # iscale.set_scaling_factor(pv.annual_energy, 1) + iscale.set_scaling_factor(pv.electricity, 1000) + iscale.set_scaling_factor(pv.land_req, 100) + + +def add_pv_costing_scaling(m, blk): + pv = blk + + iscale.set_scaling_factor(m.fs.energy.pv.costing.system_capacity, 1e-5) + + +def print_PV_costing_breakdown(pv): + print(f"\n\n-------------------- PV Costing Breakdown --------------------\n") + print(f'{"PV Capital Cost":<35s}{f"${value(pv.costing.capital_cost):<25,.0f}"}') + print( + f'{"PV Operating Cost":<35s}{f"${value(pv.costing.fixed_operating_cost):<25,.0f}"}' + ) + + +def report_PV(m): + elec = "electricity" + print(f"\n\n-------------------- PHOTOVOLTAIC SYSTEM --------------------\n\n") + print( + f'{"Land Requirement":<30s}{value(m.fs.energy.pv.land_req):<10.1f}{pyunits.get_units(m.fs.energy.pv.land_req)}' + ) + print( + f'{"System Agg. Flow Electricity":<30s}{value(m.fs.treatment.costing.aggregate_flow_electricity):<10.1f}{"kW"}' + ) + print( + f'{"PV Agg. Flow Elec.":<30s}{value(m.fs.energy.pv.design_size):<10.1f}{pyunits.get_units(m.fs.energy.pv.design_size)}' + ) + print( + f'{"Treatment Agg. Flow Elec.":<30s}{value(m.fs.treatment.costing.aggregate_flow_electricity):<10.1f}{"kW"}' + ) + + print( + f'{"PV Annual Energy":<30s}{value(m.fs.energy.pv.annual_energy):<10,.0f}{pyunits.get_units(m.fs.energy.pv.annual_energy)}' + ) + # print( + # f'{"Treatment Annual Energy":<30s}{value(m.fs.annual_treatment_energy):<10,.0f}{"kWh/yr"}' + # ) + print("\n") + print( + f'{"PV Annual Generation":<25s}{f"{pyunits.convert(m.fs.energy.pv.electricity, to_units=pyunits.kWh/pyunits.year)():<25,.0f}"}{"kWh/yr":<10s}' + ) + print( + f'{"Treatment Annual Demand":<25s}{f"{pyunits.convert(m.fs.treatment.costing.aggregate_flow_electricity, to_units=pyunits.kWh/pyunits.year)():<25,.0f}"}{"kWh/yr":<10s}' + ) + print( + f'{"Grid Electricity Frac":<25s}{f"{100*value(m.fs.costing.frac_elec_from_grid):<25,.3f} %"}' + ) + print( + f'{"Treatment Elec Cost":<25s}{f"${value(m.fs.treatment.costing.aggregate_flow_costs[elec]):<25,.0f}"}{"$/yr":<10s}' + ) + print( + f'{"Energy Elec Cost":<25s}{f"${value(m.fs.energy.costing.aggregate_flow_costs[elec]):<25,.0f}"}{"$/yr":<10s}' + ) + print("\nEnergy Balance") + print( + f'{"Treatment Agg. Flow Elec.":<30s}{value(m.fs.treatment.costing.aggregate_flow_electricity):<10.1f}{"kW"}' + ) + print( + f'{"PV Agg. Flow Elec.":<30s}{value(m.fs.energy.costing.aggregate_flow_electricity):<10.1f}{"kW"}' + ) + print( + f'{"Electricity Buy":<30s}{f"{value(m.fs.costing.aggregate_flow_electricity_purchased):<10,.0f}"}{"kW":<10s}' + ) + print( + f'{"Electricity Sold":<30s}{f"{value(m.fs.costing.aggregate_flow_electricity_sold):<10,.0f}"}{"kW":<10s}' + ) + print( + f'{"Electricity Cost":<29s}{f"${value(m.fs.costing.total_electric_operating_cost):<10,.0f}"}{"$/yr":<10s}' + ) + + # print(m.fs.energy.pv.annual_energy.display()) + # # print(m.fs.energy.pv.costing.annual_generation.display()) + # print(m.fs.costing.total_electric_operating_cost.display()) + + +def breakdown_dof(blk): + equalities = [c for c in activated_equalities_generator(blk)] + active_vars = variables_in_activated_equalities_set(blk) + fixed_active_vars = fixed_variables_in_activated_equalities_set(blk) + unfixed_active_vars = unfixed_variables_in_activated_equalities_set(blk) + print("\n ===============DOF Breakdown================\n") + print(f"Degrees of Freedom: {degrees_of_freedom(blk)}") + print(f"Activated Variables: ({len(active_vars)})") + for v in active_vars: + print(f" {v}") + print(f"Activated Equalities: ({len(equalities)})") + for c in equalities: + print(f" {c}") + + print(f"Fixed Active Vars: ({len(fixed_active_vars)})") + for v in fixed_active_vars: + print(f" {v}") + + print(f"Unfixed Active Vars: ({len(unfixed_active_vars)})") + for v in unfixed_active_vars: + print(f" {v}") + print("\n") + print(f" {f' Active Vars':<30s}{len(active_vars)}") + print(f"{'-'}{f' Fixed Active Vars':<30s}{len(fixed_active_vars)}") + print(f"{'-'}{f' Activated Equalities':<30s}{len(equalities)}") + print(f"{'='}{f' Degrees of Freedom':<30s}{degrees_of_freedom(blk)}") + print("\nSuggested Variables to Fix:") + + if degrees_of_freedom != 0: + unfixed_vars_without_constraint = [ + v for v in active_vars if v not in unfixed_active_vars + ] + for v in unfixed_vars_without_constraint: + if v.fixed is False: + print(f" {v}") + + +def initialize(m): + energy = m.fs.energy + + energy.costing.cost_process() + energy.costing.initialize() + + +def solve(m, solver=None, tee=True, raise_on_failure=True, debug=False): + # ---solving--- + if solver is None: + solver = get_solver() + solver.options["max_iter"] = 2000 + + print("\n--------- SOLVING ---------\n") + + results = solver.solve(m, tee=tee) + + if check_optimal_termination(results): + print("\n--------- OPTIMAL SOLVE!!! ---------\n") + if debug: + print("\n--------- CHECKING JACOBIAN ---------\n") + # check_jac(m) + + print("\n--------- CLOSE TO BOUNDS ---------\n") + print_close_to_bounds(m) + return results + msg = ( + "The current configuration is infeasible. Please adjust the decision variables." + ) + if raise_on_failure: + print('\n{"=======> INFEASIBLE BOUNDS <=======":^60}\n') + print_infeasible_bounds(m) + print('\n{"=======> INFEASIBLE CONSTRAINTS <=======":^60}\n') + print_infeasible_constraints(m) + print('\n{"=======> CLOSE TO BOUNDS <=======":^60}\n') + print_close_to_bounds(m) + + raise RuntimeError(msg) + else: + print("\n--------- FAILED SOLVE!!! ---------\n") + print(msg) + assert False + + +if __name__ == "__main__": + m = build_system() + build_pv(m) + set_pv_constraints(m, focus="Energy") + solve(m, debug=True) + add_pv_costing(m, m.fs.energy.pv) + add_pv_scaling(m, m.fs.energy.pv) + iscale.calculate_scaling_factors(m) + initialize(m) + solve(m, debug=True) + print(m.fs.energy.pv.display()) diff --git a/src/watertap_contrib/reflo/solar_models/surrogate/pv/pv_surrogate.py b/src/watertap_contrib/reflo/solar_models/surrogate/pv/pv_surrogate.py index 0c247e06..cfdc34ba 100644 --- a/src/watertap_contrib/reflo/solar_models/surrogate/pv/pv_surrogate.py +++ b/src/watertap_contrib/reflo/solar_models/surrogate/pv/pv_surrogate.py @@ -48,6 +48,12 @@ def build(self): self.add_surrogate_variables() self.get_surrogate_data() + if self.config.surrogate_model_file is not None: + self.surrogate_file = self.config.surrogate_model_file + self.load_surrogate() + else: + self.create_rbf_surrogate() + self.electricity_constraint = Constraint( expr=self.annual_energy == pyunits.convert(self.electricity, to_units=pyunits.kWh / pyunits.year) @@ -98,7 +104,7 @@ def initialize( ) self.init_output = self.surrogate.evaluate_surrogate(self.init_data) - self.electricity.set_value(value(self.electricity_annual) / 8766) + self.electricity.set_value(value(self.annual_energy) / 8766) # Create solver res = opt.solve(self) diff --git a/src/watertap_contrib/reflo/solar_models/surrogate/trough/trough_surrogate.py b/src/watertap_contrib/reflo/solar_models/surrogate/trough/trough_surrogate.py index 79939aaf..400438ad 100644 --- a/src/watertap_contrib/reflo/solar_models/surrogate/trough/trough_surrogate.py +++ b/src/watertap_contrib/reflo/solar_models/surrogate/trough/trough_surrogate.py @@ -58,7 +58,11 @@ def build(self): doc="Annual electricity consumed by trough in kWh", ) - self.create_rbf_surrogate() + if self.config.surrogate_model_file is not None: + self.surrogate_file = self.config.surrogate_model_file + self.load_surrogate() + else: + self.create_rbf_surrogate() self.heat_constraint = Constraint( expr=self.heat From 8ce70ce224f0a07bb85049c6d57ee6454616a444 Mon Sep 17 00:00:00 2001 From: Mukta Hardikar Date: Wed, 13 Nov 2024 23:57:05 -0700 Subject: [PATCH 58/76] black --- .../case_studies/KBHDP/components/CST.py | 21 ++++++++----------- .../case_studies/KBHDP/components/FPC.py | 9 +++++--- .../case_studies/KBHDP/components/PV.py | 11 ++++++---- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/watertap_contrib/reflo/analysis/case_studies/KBHDP/components/CST.py b/src/watertap_contrib/reflo/analysis/case_studies/KBHDP/components/CST.py index f5d6ddbe..db7a34d5 100644 --- a/src/watertap_contrib/reflo/analysis/case_studies/KBHDP/components/CST.py +++ b/src/watertap_contrib/reflo/analysis/case_studies/KBHDP/components/CST.py @@ -58,18 +58,17 @@ def build_cst(blk, __file__=None): if __file__ == None: cwd = os.getcwd() - __file__ = cwd + r'\src\watertap_contrib\reflo\solar_models\surrogate\trough\\' - + __file__ = cwd + r"\src\watertap_contrib\reflo\solar_models\surrogate\trough\\" + dataset_filename = os.path.join( os.path.dirname(__file__), r"data\test_trough_data.pkl" ) surrogate_filename = os.path.join( - os.path.dirname(__file__), r"data\test_trough_data_heat_load_100_500_hours_storage_0_26.json" + os.path.dirname(__file__), + r"data\test_trough_data_heat_load_100_500_hours_storage_0_26.json", ) - input_bounds = dict( - heat_load=[100, 500], hours_storage=[0, 26] - ) + input_bounds = dict(heat_load=[100, 500], hours_storage=[0, 26]) input_units = dict(heat_load="MW", hours_storage="hour") input_variables = { "labels": ["heat_load", "hours_storage"], @@ -83,14 +82,13 @@ def build_cst(blk, __file__=None): "units": output_units, } - blk.unit = TroughSurrogate( - surrogate_model_file = surrogate_filename, + surrogate_model_file=surrogate_filename, dataset_filename=dataset_filename, input_variables=input_variables, output_variables=output_variables, - scale_training_data=True - ) + scale_training_data=True, + ) def init_cst(blk): @@ -128,7 +126,6 @@ def report_cst(m, blk): print(f"\n\n-------------------- CST Report --------------------\n") print("\n") - print( f'{"Heat load":<30s}{value(blk.heat_load):<20,.2f}{pyunits.get_units(blk.heat_load)}' ) @@ -201,7 +198,7 @@ def report_cst_costing(m, blk): m = build_system() build_cst(m.fs.cst) - + init_cst(m.fs.cst) set_cst_op_conditions(m.fs.cst) diff --git a/src/watertap_contrib/reflo/analysis/case_studies/KBHDP/components/FPC.py b/src/watertap_contrib/reflo/analysis/case_studies/KBHDP/components/FPC.py index 2d5c0eb2..9984f65d 100644 --- a/src/watertap_contrib/reflo/analysis/case_studies/KBHDP/components/FPC.py +++ b/src/watertap_contrib/reflo/analysis/case_studies/KBHDP/components/FPC.py @@ -58,13 +58,16 @@ def build_fpc(blk, __file__=None): if __file__ == None: cwd = os.getcwd() - __file__ = cwd + r'\src\watertap_contrib\reflo\solar_models\surrogate\flat_plate\\' + __file__ = ( + cwd + r"\src\watertap_contrib\reflo\solar_models\surrogate\flat_plate\\" + ) dataset_filename = os.path.join( os.path.dirname(__file__), r"data\flat_plate_data_heat_load_1_400.pkl" ) surrogate_filename = os.path.join( - os.path.dirname(__file__), r"data\flat_plate_data_heat_load_1_400_heat_load_1_400_hours_storage_0_27_temperature_hot_50_102.json" + os.path.dirname(__file__), + r"data\flat_plate_data_heat_load_1_400_heat_load_1_400_hours_storage_0_27_temperature_hot_50_102.json", ) input_bounds = dict( @@ -84,7 +87,7 @@ def build_fpc(blk, __file__=None): } blk.unit = FlatPlateSurrogate( - surrogate_model_file = surrogate_filename, + surrogate_model_file=surrogate_filename, dataset_filename=dataset_filename, input_variables=input_variables, output_variables=output_variables, diff --git a/src/watertap_contrib/reflo/analysis/case_studies/KBHDP/components/PV.py b/src/watertap_contrib/reflo/analysis/case_studies/KBHDP/components/PV.py index 564a74e5..8bf5ed3e 100644 --- a/src/watertap_contrib/reflo/analysis/case_studies/KBHDP/components/PV.py +++ b/src/watertap_contrib/reflo/analysis/case_studies/KBHDP/components/PV.py @@ -19,9 +19,10 @@ REFLOCosting, REFLOSystemCosting, ) + # from watertap_contrib.reflo.analysis.case_studies.KBHDP.utils import ( - # check_jac, - # calc_scale, +# check_jac, +# calc_scale, # ) __all__ = [ @@ -50,8 +51,10 @@ def build_pv(m): # Get directory path cwd = os.getcwd() energy.pv = PVSurrogate( - surrogate_model_file= cwd + "/src/watertap_contrib/reflo/solar_models/surrogate/pv/pv_surrogate.json", - dataset_filename=cwd + "/src/watertap_contrib/reflo/solar_models/surrogate/pv/data/dataset.pkl", + surrogate_model_file=cwd + + "/src/watertap_contrib/reflo/solar_models/surrogate/pv/pv_surrogate.json", + dataset_filename=cwd + + "/src/watertap_contrib/reflo/solar_models/surrogate/pv/data/dataset.pkl", input_variables={ "labels": ["design_size"], "bounds": {"design_size": [1, 200000]}, From 63d097f73ab5fd6b61305832a5e52a56ec0063d2 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 14 Nov 2024 12:02:08 -0700 Subject: [PATCH 59/76] 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 c2bbe7b633700dda56fd530ccd8228edf0c49c35 Mon Sep 17 00:00:00 2001 From: zacharybinger Date: Thu, 14 Nov 2024 12:14:14 -0700 Subject: [PATCH 60/76] Refactor PV surrogate cost calculations and remove unused variables --- .../reflo/costing/solar/pv_surrogate.py | 54 ++++--------------- 1 file changed, 9 insertions(+), 45 deletions(-) diff --git a/src/watertap_contrib/reflo/costing/solar/pv_surrogate.py b/src/watertap_contrib/reflo/costing/solar/pv_surrogate.py index 3ed6494c..74211895 100644 --- a/src/watertap_contrib/reflo/costing/solar/pv_surrogate.py +++ b/src/watertap_contrib/reflo/costing/solar/pv_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. @@ -86,7 +86,7 @@ def build_pv_surrogate_cost_param_block(blk): doc="Annual operating cost of PV system per MWh generated", ) - # blk.fix_all_vars() + blk.fix_all_vars() @register_costing_parameter_block( @@ -114,20 +114,6 @@ def cost_pv_surrogate(blk): doc="Indirect costs of PV system", ) - # blk.direct_capital_cost = pyo.Var( - # initialize=0, - # units=blk.config.flowsheet_costing_block.base_currency, - # bounds=(0, None), - # doc="Direct costs of PV system", - # ) - - # blk.indirect_capital_cost = pyo.Var( - # initialize=0, - # units=blk.config.flowsheet_costing_block.base_currency, - # bounds=(0, None), - # doc="Indirect costs of PV system", - # ) - blk.land_cost = pyo.Var( initialize=0, units=blk.config.flowsheet_costing_block.base_currency, @@ -142,37 +128,16 @@ def cost_pv_surrogate(blk): doc="Sales tax for PV system", ) - blk.system_capacity = pyo.Var( - initialize=0, - units=pyo.units.watt, - bounds=(0, None), - doc="DC system capacity for PV system", - ) - - blk.land_area = pyo.Var( - initialize=0, - units=pyo.units.acre, - bounds=(0, None), - doc="Land area required for PV system", - ) - - blk.annual_generation = pyo.Var( - initialize=0, - units=pyo.units.kWh, - bounds=(0, None), - doc="Annual electricity generation of PV system", - ) - blk.direct_capital_cost_constraint = pyo.Constraint( expr=blk.direct_capital_cost - == blk.system_capacity + == blk.unit_model.design_size * ( pv_params.cost_per_watt_module + pv_params.cost_per_watt_inverter + pv_params.cost_per_watt_other ) + ( - blk.system_capacity + blk.unit_model.design_size * ( pv_params.cost_per_watt_module + pv_params.cost_per_watt_inverter @@ -184,12 +149,12 @@ def cost_pv_surrogate(blk): blk.indirect_capital_cost_constraint = pyo.Constraint( expr=blk.indirect_capital_cost - == (blk.system_capacity * pv_params.cost_per_watt_indirect) - + (blk.land_area * pv_params.land_cost_per_acre) + == (blk.unit_model.design_size * pv_params.cost_per_watt_indirect) + + (blk.unit_model.land_req * pv_params.land_cost_per_acre) ) blk.land_cost_constraint = pyo.Constraint( - expr=blk.land_cost == (blk.land_area * pv_params.land_cost_per_acre) + expr=blk.land_cost == (blk.unit_model.land_req * pv_params.land_cost_per_acre) ) blk.sales_tax_constraint = pyo.Constraint( @@ -203,13 +168,12 @@ def cost_pv_surrogate(blk): blk.fixed_operating_cost_constraint = pyo.Constraint( expr=blk.fixed_operating_cost - == pv_params.fixed_operating_by_capacity - * pyo.units.convert(blk.system_capacity, to_units=pyo.units.kW) + == pv_params.fixed_operating_by_capacity * blk.unit_model.design_size ) blk.variable_operating_cost_constraint = pyo.Constraint( expr=blk.variable_operating_cost - == pv_params.variable_operating_by_generation * blk.annual_generation + == pv_params.variable_operating_by_generation * blk.unit_model.annual_energy ) blk.costing_package.cost_flow(-1 * blk.unit_model.electricity, "electricity") From 0d31f825d31c30abecffa27178aac554868adaf8 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 14 Nov 2024 12:33:22 -0700 Subject: [PATCH 61/76] 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 62/76] 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 63/76] 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 64/76] 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 65/76] 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 66/76] 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 67/76] 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 68/76] 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 69/76] 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 70/76] 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 71/76] 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 72/76] 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 73/76] 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 74/76] 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 75/76] 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 76/76] 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 )