From 3c03c3b8a6695eec68ecb38e35613bb0516a44a2 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Mon, 6 Jul 2026 14:34:21 -0600 Subject: [PATCH 1/3] made it so profast npv can take in multi-year sell price costs --- h2integrate/finances/profast_npv.py | 22 ++++- h2integrate/finances/test/test_profast_npv.py | 84 +++++++++++++++++++ 2 files changed, 102 insertions(+), 4 deletions(-) diff --git a/h2integrate/finances/profast_npv.py b/h2integrate/finances/profast_npv.py index 67179f5d9..a1879033f 100644 --- a/h2integrate/finances/profast_npv.py +++ b/h2integrate/finances/profast_npv.py @@ -1,3 +1,4 @@ +import numpy as np from openmdao.utils.units import convert_units from h2integrate.finances.tools import _compute_rate_units @@ -53,8 +54,6 @@ def setup(self): self.commodity_sell_price = model_inputs.get("commodity_sell_price", None) self.commodity_sell_price_units = model_inputs.get("commodity_sell_price_units", None) - if self.commodity_sell_price is None: - raise ValueError("commodity_sell_price is missing as an input") if self.commodity_sell_price_units is None: raise ValueError( "commodity_sell_price_units is missing as an input. " @@ -65,9 +64,22 @@ def setup(self): super().setup() + if isinstance(self.commodity_sell_price, float | int): + if self.commodity_sell_price is None: + raise ValueError("commodity_sell_price is missing as an input") + else: + if len(self.commodity_sell_price) != int(self.params.plant_life): + raise ValueError( + f"`commodity_sell_price` has an invalid length of " + f"{len(self.commodity_sell_price)}. `commodity_sell_price`" + f"must be the same length as the plant life ({self.params.plant_life}) " + "or a single value." + ) + self.add_input( f"sell_price_{self.output_txt}", val=self.commodity_sell_price, + shape=int(self.params.plant_life), units=self.commodity_sell_price_units, ) @@ -112,6 +124,8 @@ def compute(self, inputs, outputs): else: pf = self.populate_profast(inputs) - outputs[f"NPV_{self.output_txt}"] = pf.cash_flow( - price=inputs[f"sell_price_{self.output_txt}"][0] + non_op_Nyears = int(np.ceil(self.params.installation_time / 12) + 1) + sell_profile = np.concatenate( + [np.zeros(non_op_Nyears), inputs[f"sell_price_{self.output_txt}"]] ) + outputs[f"NPV_{self.output_txt}"] = pf.cash_flow(price=sell_profile) diff --git a/h2integrate/finances/test/test_profast_npv.py b/h2integrate/finances/test/test_profast_npv.py index a09b0009c..6e7a55b78 100644 --- a/h2integrate/finances/test/test_profast_npv.py +++ b/h2integrate/finances/test/test_profast_npv.py @@ -332,3 +332,87 @@ def test_profast_npv_nonstandard_price_units( pytest.approx(prob.get_val("pf.NPV_electricity_no2", units="USD")[0], rel=1e-6) == 611288384.412 ) + + +@pytest.mark.regression +def test_profast_npv_multi_year_sell_price( + profast_inputs_no2, fake_filtered_tech_config, fake_cost_dict, subtests +): + mean_hourly_production = 500000.0 + prob = om.Problem() + profast_inputs_no2["commodity_sell_price"] = [0.07] * 30 + plant_config = { + "plant": { + "plant_life": 30, + }, + "finance_parameters": {"model_inputs": profast_inputs_no2}, + } + pf = ProFastNPV( + driver_config={}, + plant_config=plant_config, + tech_config=fake_filtered_tech_config, + commodity_type="electricity", + description="no2", + ) + + ivc = om.IndepVarComp() + ivc.add_output("rated_electricity_production", mean_hourly_production, units="kW") + ivc.add_output("capacity_factor", [1.0] * plant_config["plant"]["plant_life"], units="unitless") + + prob.model.add_subsystem("ivc", ivc, promotes=["*"]) + prob.model.add_subsystem("pf", pf, promotes=["rated_electricity_production", "capacity_factor"]) + prob.setup() + for variable, cost in fake_cost_dict.items(): + units = "USD" if "capex" in variable else "USD/year" + prob.set_val(f"pf.{variable}", cost, units=units) + + prob.run_model() + + with subtests.test("Sell price"): + assert ( + pytest.approx( + prob.get_val("pf.sell_price_electricity_no2", units="USD/(kW*h)"), rel=1e-6 + ) + == profast_inputs_no2["commodity_sell_price"] + ) + + with subtests.test("NPV"): + assert ( + pytest.approx(prob.get_val("pf.NPV_electricity_no2", units="USD")[0], rel=1e-6) + == 611288384.412 + ) + + +@pytest.mark.regression +def test_profast_npv_multi_year_error( + profast_inputs_no2, fake_filtered_tech_config, fake_cost_dict, subtests +): + prob = om.Problem() + profast_inputs_no2["commodity_sell_price"] = [0.07] * 10 + plant_config = { + "plant": { + "plant_life": 30, + }, + "finance_parameters": {"model_inputs": profast_inputs_no2}, + } + mean_hourly_production = 500000.0 + pf = ProFastNPV( + driver_config={}, + plant_config=plant_config, + tech_config=fake_filtered_tech_config, + commodity_type="electricity", + description="no2", + ) + + ivc = om.IndepVarComp() + ivc.add_output("rated_electricity_production", mean_hourly_production, units="kW") + ivc.add_output("capacity_factor", [1.0] * plant_config["plant"]["plant_life"], units="unitless") + + prob.model.add_subsystem("ivc", ivc, promotes=["*"]) + prob.model.add_subsystem("pf", pf, promotes=["rated_electricity_production", "capacity_factor"]) + + expected_message = "`commodity_sell_price` has an invalid length of 10" + with subtests.test("Incorrect sell price length"): + with pytest.raises(ValueError) as excinfo: + prob.setup() + assert expected_message in str(excinfo.value) From 62ae4787821dbc01c0e6a8236913c10ef27fe7a8 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:57:35 -0600 Subject: [PATCH 2/3] updated error handling for misisng sell price value --- h2integrate/finances/profast_npv.py | 13 ++++--- h2integrate/finances/test/test_profast_npv.py | 37 +++++++++++++++++-- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/h2integrate/finances/profast_npv.py b/h2integrate/finances/profast_npv.py index a1879033f..643b1f63e 100644 --- a/h2integrate/finances/profast_npv.py +++ b/h2integrate/finances/profast_npv.py @@ -1,3 +1,5 @@ +from collections.abc import Iterable + import numpy as np from openmdao.utils.units import convert_units @@ -41,7 +43,8 @@ def setup(self): Retrieves the commodity sell price and its units from the plant configuration and registers it as an input for the component. Calls the base `setup()` method - to initialize other ProFAST inputs and outputs. + to initialize other ProFAST inputs and outputs. The commodity sell price can be + either a scalar value or a list with the same length as plant life. Raises: ValueError: If `commodity_sell_price` or `commodity_sell_price_units` is not @@ -64,10 +67,10 @@ def setup(self): super().setup() - if isinstance(self.commodity_sell_price, float | int): - if self.commodity_sell_price is None: - raise ValueError("commodity_sell_price is missing as an input") - else: + if self.commodity_sell_price is None: + raise ValueError("commodity_sell_price is missing as an input") + + if isinstance(self.commodity_sell_price, Iterable): if len(self.commodity_sell_price) != int(self.params.plant_life): raise ValueError( f"`commodity_sell_price` has an invalid length of " diff --git a/h2integrate/finances/test/test_profast_npv.py b/h2integrate/finances/test/test_profast_npv.py index 6e7a55b78..f2ebc5844 100644 --- a/h2integrate/finances/test/test_profast_npv.py +++ b/h2integrate/finances/test/test_profast_npv.py @@ -384,9 +384,7 @@ def test_profast_npv_multi_year_sell_price( @pytest.mark.regression -def test_profast_npv_multi_year_error( - profast_inputs_no2, fake_filtered_tech_config, fake_cost_dict, subtests -): +def test_profast_npv_multi_year_error(profast_inputs_no2, fake_filtered_tech_config, subtests): prob = om.Problem() profast_inputs_no2["commodity_sell_price"] = [0.07] * 10 plant_config = { @@ -416,3 +414,36 @@ def test_profast_npv_multi_year_error( with pytest.raises(ValueError) as excinfo: prob.setup() assert expected_message in str(excinfo.value) + + +@pytest.mark.regression +def test_profast_npv_missing_sell_price(profast_inputs_no2, fake_filtered_tech_config, subtests): + prob = om.Problem() + profast_inputs_no2["commodity_sell_price"] = None + plant_config = { + "plant": { + "plant_life": 30, + }, + "finance_parameters": {"model_inputs": profast_inputs_no2}, + } + mean_hourly_production = 500000.0 + pf = ProFastNPV( + driver_config={}, + plant_config=plant_config, + tech_config=fake_filtered_tech_config, + commodity_type="electricity", + description="no2", + ) + + ivc = om.IndepVarComp() + ivc.add_output("rated_electricity_production", mean_hourly_production, units="kW") + ivc.add_output("capacity_factor", [1.0] * plant_config["plant"]["plant_life"], units="unitless") + + prob.model.add_subsystem("ivc", ivc, promotes=["*"]) + prob.model.add_subsystem("pf", pf, promotes=["rated_electricity_production", "capacity_factor"]) + + expected_message = "commodity_sell_price is missing as an input" + with subtests.test("Missing sell price"): + with pytest.raises(ValueError) as excinfo: + prob.setup() + assert expected_message in str(excinfo.value) From cf985dcf993fbfb08c30e02b93227e9175b111e5 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:47:35 -0600 Subject: [PATCH 3/3] added subtest for inflation test --- h2integrate/finances/test/test_profast_npv.py | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/h2integrate/finances/test/test_profast_npv.py b/h2integrate/finances/test/test_profast_npv.py index f2ebc5844..798ffd92c 100644 --- a/h2integrate/finances/test/test_profast_npv.py +++ b/h2integrate/finances/test/test_profast_npv.py @@ -1,3 +1,4 @@ +import numpy as np import pytest import openmdao.api as om from pytest import fixture @@ -447,3 +448,71 @@ def test_profast_npv_missing_sell_price(profast_inputs_no2, fake_filtered_tech_c with pytest.raises(ValueError) as excinfo: prob.setup() assert expected_message in str(excinfo.value) + + +@pytest.mark.regression +def test_profast_npv_with_inflation( + profast_inputs_no2, fake_filtered_tech_config, fake_cost_dict, subtests +): + mean_hourly_production = 500000.0 + prob = om.Problem() + price_escalation = 0.02 # 2% inflation in price + profast_inputs_no2["commodity_sell_price"] = [0.07] * 30 + profast_inputs_no2["params"]["inflation_rate"] = price_escalation # 2% inflation + plant_config = { + "plant": { + "plant_life": 30, + }, + "finance_parameters": {"model_inputs": profast_inputs_no2}, + } + pf = ProFastNPV( + driver_config={}, + plant_config=plant_config, + tech_config=fake_filtered_tech_config, + commodity_type="electricity", + description="no2", + ) + + ivc = om.IndepVarComp() + ivc.add_output("rated_electricity_production", mean_hourly_production, units="kW") + ivc.add_output("capacity_factor", [1.0] * plant_config["plant"]["plant_life"], units="unitless") + + prob.model.add_subsystem("ivc", ivc, promotes=["*"]) + prob.model.add_subsystem("pf", pf, promotes=["rated_electricity_production", "capacity_factor"]) + prob.setup() + for variable, cost in fake_cost_dict.items(): + units = "USD" if "capex" in variable else "USD/year" + prob.set_val(f"pf.{variable}", cost, units=units) + + prob.run_model() + + with subtests.test("Sell price (flat rate)"): + assert ( + pytest.approx( + prob.get_val("pf.sell_price_electricity_no2", units="USD/(kW*h)"), rel=1e-6 + ) + == profast_inputs_no2["commodity_sell_price"] + ) + + # This NPV is about 2.2x times the NPV when inflation is zero + with subtests.test("NPV"): + assert ( + pytest.approx(prob.get_val("pf.NPV_electricity_no2", units="USD")[0], rel=1e-6) + == 1336544230.1139889 + ) + + nominal_price = np.concatenate( + [np.zeros(4), prob.get_val("pf.sell_price_electricity_no2", units="USD/(kW*h)")] + ) + years = np.arange(0, 34, 1) + # Remove inflation from nominal price + real_price = nominal_price / ((1.0 + price_escalation) ** (years - 1)) + + prob.set_val("pf.sell_price_electricity_no2", real_price[4:], units="USD/(kW*h)") + prob.run_model() + # Removing inflation from nominal price gives NPV of 0.4 what is what before + with subtests.test("NPV (real sell price)"): + assert ( + pytest.approx(prob.get_val("pf.NPV_electricity_no2", units="USD")[0], rel=1e-6) + == 520290489.52904046 + )