From 5a769bc94f98298940339e219774627d146d5871 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Mon, 4 May 2026 14:54:07 -0600 Subject: [PATCH 01/13] added adjusted capacity factor comp, still need to update connections to it --- .../17_splitter_wind_doc_h2/plant_config.yaml | 8 ++++ h2integrate/core/h2integrate_model.py | 14 ++++++- h2integrate/finances/finances.py | 40 +++++++++++++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/examples/17_splitter_wind_doc_h2/plant_config.yaml b/examples/17_splitter_wind_doc_h2/plant_config.yaml index 187c6cd12..86f17cc55 100644 --- a/examples/17_splitter_wind_doc_h2/plant_config.yaml +++ b/examples/17_splitter_wind_doc_h2/plant_config.yaml @@ -52,6 +52,14 @@ finance_parameters: electricity: commodity: electricity technologies: [wind] + electricity_doc: + commodity: electricity + commodity_stream: splitter + commodity_stream_name: electricity_out1 + electricity_electrolyzer: + commodity: electricity + commodity_stream: splitter + commodity_stream_name: electricity_out2 hydrogen: commodity: hydrogen technologies: [wind, electrolyzer] diff --git a/h2integrate/core/h2integrate_model.py b/h2integrate/core/h2integrate_model.py index 67bd83085..aecd7deff 100644 --- a/h2integrate/core/h2integrate_model.py +++ b/h2integrate/core/h2integrate_model.py @@ -11,7 +11,7 @@ from h2integrate.core.utilities import create_xdsm_from_config from h2integrate.core.dict_utils import check_inputs from h2integrate.core.file_utils import get_path, find_file, load_yaml -from h2integrate.finances.finances import AdjustedCapexOpexComp +from h2integrate.finances.finances import AdjustedCapexOpexComp, AdjustedCapacityFactorComp from h2integrate.core.supported_models import ( no_cost_models, supported_models, @@ -798,7 +798,7 @@ def create_finance_model(self): ) tech_names = subgroup_params.get("technologies") commodity_stream = subgroup_params.get("commodity_stream", None) - + (False if subgroup_params.get("commodity_stream_name", None) is None else True) if isinstance(finance_group_names, str): finance_group_names = [finance_group_names] @@ -839,6 +839,7 @@ def create_finance_model(self): "commodity": commodity, "commodity_stream": commodity_stream, "is_system_finance_model": True, + "commodity_stream_name": subgroup_params.get("commodity_stream_name", None), } } ) @@ -1001,6 +1002,15 @@ def create_finance_model(self): # update the description to include the finance model name to ensure # uniquely named outputs commodity_output_desc = commodity_output_desc + f"_{finance_group_name}" + if finance_subgroups[subgroup_name].get("commodity_stream_name", None) is not None: + adj_cf_comp = AdjustedCapacityFactorComp( + plant_config=filtered_plant_config, + commodity_stream_name=finance_subgroups[subgroup_name][ + "commodity_stream_name" + ], + commodity_type=commodity, + ) + finance_subgroup.add_subsystem("adjusted_cf_comp", adj_cf_comp, promotes=["*"]) # create the finance component fin_comp = fin_model( diff --git a/h2integrate/finances/finances.py b/h2integrate/finances/finances.py index d5dfe8f2a..a2d2db9cf 100644 --- a/h2integrate/finances/finances.py +++ b/h2integrate/finances/finances.py @@ -85,3 +85,43 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): outputs["total_capex_adjusted"] = total_capex_adjusted outputs["total_opex_adjusted"] = total_opex_adjusted outputs["total_varopex_adjusted"] = total_varopex_adjusted + + +class AdjustedCapacityFactorComp(om.ExplicitComponent): + def initialize(self): + self.options.declare("plant_config", types=dict) + self.options.declare("commodity_stream_name", types=str) + self.options.declare("commodity_type", types=str) + + def setup(self): + plant_life = int(self.options["plant_config"]["plant"]["plant_life"]) + self.n_timesteps = int(self.options["plant_config"]["simulation"]["n_timesteps"]) + self.dt = int(self.options["plant_config"]["simulation"]["dt"]) + + self.add_input( + self.options["commodity_stream_name"], + val=0.0, + units_by_conn=True, + shape=self.n_timesteps, + ) + + self.add_output( + f"rated_{self.options['commodity_type']}_production", + val=0.0, + copy_units=self.options["commodity_stream_name"], + shape=1, + ) + + self.add_output( + "capacity_factor", + val=1.0, + units="unitless", + shape=plant_life, + ) + + def compute(self, inputs, outputs): + outputs[f"rated_{self.options['commodity_type']}_production"] = np.mean( + inputs[self.options["commodity_stream_name"]] + ) + + outputs["capacity_factor"] = 1.0 From 423551a8d6b2c8938f679afaa4e8faa3e405e8b9 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Mon, 4 May 2026 14:58:52 -0600 Subject: [PATCH 02/13] attempted to add connection but work-in-progress --- h2integrate/core/h2integrate_model.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/h2integrate/core/h2integrate_model.py b/h2integrate/core/h2integrate_model.py index aecd7deff..ace40c2db 100644 --- a/h2integrate/core/h2integrate_model.py +++ b/h2integrate/core/h2integrate_model.py @@ -1308,17 +1308,24 @@ def connect_technologies(self): is_system_finance_model = group_configs.get("is_system_finance_model") if is_system_finance_model: - # Connect the rated commodity production and capacity factor - # for system-level finance models - self.plant.connect( - f"{commodity_stream}.rated_{primary_commodity_type}_production", - f"finance_subgroup_{group_id}.rated_{primary_commodity_type}_production", - ) + if group_configs.get("commodity_stream_name", None) is None: + # Connect the rated commodity production and capacity factor + # for system-level finance models + self.plant.connect( + f"{commodity_stream}.rated_{primary_commodity_type}_production", + f"finance_subgroup_{group_id}.rated_{primary_commodity_type}_production", + ) - self.plant.connect( - f"{commodity_stream}.capacity_factor", - f"finance_subgroup_{group_id}.capacity_factor", - ) + self.plant.connect( + f"{commodity_stream}.capacity_factor", + f"finance_subgroup_{group_id}.capacity_factor", + ) + else: + # TODO: finish this logic + self.plant.connect( + f"{commodity_stream}.{group_configs.get('commodity_stream_name')}", + f"finance_subgroup_{group_id}.capacity_factor", + ) # Only connect technologies that are included in the finance stackup for tech_name in tech_configs.keys(): From 0a7208364ab6d5e3c436b23c4256d97d3a5ca4fb Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Mon, 4 May 2026 16:02:01 -0600 Subject: [PATCH 03/13] updated example 17 and added subtests, finished integration --- .../17_splitter_wind_doc_h2/plant_config.yaml | 8 +++- examples/test/test_all_examples.py | 43 +++++++++++++++++++ h2integrate/core/h2integrate_model.py | 32 +++++++++----- h2integrate/finances/finances.py | 16 +++---- 4 files changed, 78 insertions(+), 21 deletions(-) diff --git a/examples/17_splitter_wind_doc_h2/plant_config.yaml b/examples/17_splitter_wind_doc_h2/plant_config.yaml index 86f17cc55..eb29302c8 100644 --- a/examples/17_splitter_wind_doc_h2/plant_config.yaml +++ b/examples/17_splitter_wind_doc_h2/plant_config.yaml @@ -54,12 +54,16 @@ finance_parameters: technologies: [wind] electricity_doc: commodity: electricity - commodity_stream: splitter + commodity_stream: electricity_splitter + use_commodity_stream_timeseries: true commodity_stream_name: electricity_out1 + technologies: [wind, doc] electricity_electrolyzer: commodity: electricity - commodity_stream: splitter + commodity_stream: electricity_splitter + use_commodity_stream_timeseries: true commodity_stream_name: electricity_out2 + technologies: [wind, electrolyzer] hydrogen: commodity: hydrogen technologies: [wind, electrolyzer] diff --git a/examples/test/test_all_examples.py b/examples/test/test_all_examples.py index a417d2670..7df6f03ef 100644 --- a/examples/test/test_all_examples.py +++ b/examples/test/test_all_examples.py @@ -696,6 +696,49 @@ def test_splitter_wind_doc_h2_example(subtests, temp_copy_of_example): == 132.395036462 ) + with subtests.test("Check LCOE (doc)"): + assert ( + pytest.approx( + model.prob.get_val("finance_subgroup_electricity_doc.LCOE", units="USD/(MW*h)")[0], + rel=1e-3, + ) + == 674.2414136935529 + ) + + with subtests.test("Check finance_subgroup_electricity_doc electricity inputs"): + assert ( + pytest.approx( + model.prob.get_val( + "finance_subgroup_electricity_doc.rated_electricity_production", units="kW" + ), + rel=1e-6, + ) + == model.prob.get_val("doc.electricity_in", units="kW").mean() + ) + + with subtests.test("Check LCOE (electrolyzer)"): + assert ( + pytest.approx( + model.prob.get_val( + "finance_subgroup_electricity_electrolyzer.LCOE", units="USD/(MW*h)" + )[0], + rel=1e-3, + ) + == 182.8942790183688 + ) + + with subtests.test("Check finance_subgroup_electricity_electrolyzer electricity inputs"): + assert ( + pytest.approx( + model.prob.get_val( + "finance_subgroup_electricity_electrolyzer.rated_electricity_production", + units="kW", + ), + rel=1e-6, + ) + == model.prob.get_val("electrolyzer.electricity_in", units="kW").mean() + ) + @pytest.mark.integration @pytest.mark.parametrize( diff --git a/h2integrate/core/h2integrate_model.py b/h2integrate/core/h2integrate_model.py index ace40c2db..90f5ef756 100644 --- a/h2integrate/core/h2integrate_model.py +++ b/h2integrate/core/h2integrate_model.py @@ -839,10 +839,14 @@ def create_finance_model(self): "commodity": commodity, "commodity_stream": commodity_stream, "is_system_finance_model": True, + "use_commodity_stream_timeseries": subgroup_params.get( + "use_commodity_stream_timeseries", False + ), "commodity_stream_name": subgroup_params.get("commodity_stream_name", None), } } ) + finance_subgroup = om.Group() # Default logic for handling cases without specified commodity streams @@ -1002,12 +1006,18 @@ def create_finance_model(self): # update the description to include the finance model name to ensure # uniquely named outputs commodity_output_desc = commodity_output_desc + f"_{finance_group_name}" - if finance_subgroups[subgroup_name].get("commodity_stream_name", None) is not None: + + if finance_subgroups[subgroup_name]["use_commodity_stream_timeseries"]: + if "commodity_stream_name" not in finance_subgroups[subgroup_name]: + msg = ( + "`commodity_stream_name` is a required input if " + f"`use_commodity_stream_timeseries` is True. Please add the " + f"`commodity_stream_name` for finance subgroup {subgroup_name}" + ) + raise ValueError(msg) + adj_cf_comp = AdjustedCapacityFactorComp( plant_config=filtered_plant_config, - commodity_stream_name=finance_subgroups[subgroup_name][ - "commodity_stream_name" - ], commodity_type=commodity, ) finance_subgroup.add_subsystem("adjusted_cf_comp", adj_cf_comp, promotes=["*"]) @@ -1308,7 +1318,13 @@ def connect_technologies(self): is_system_finance_model = group_configs.get("is_system_finance_model") if is_system_finance_model: - if group_configs.get("commodity_stream_name", None) is None: + if group_configs.get("use_commodity_stream_timeseries", False): + # TODO: finish this logic + self.plant.connect( + f"{commodity_stream}.{group_configs.get('commodity_stream_name')}", + f"finance_subgroup_{group_id}.{primary_commodity_type}_produced", + ) + else: # Connect the rated commodity production and capacity factor # for system-level finance models self.plant.connect( @@ -1320,12 +1336,6 @@ def connect_technologies(self): f"{commodity_stream}.capacity_factor", f"finance_subgroup_{group_id}.capacity_factor", ) - else: - # TODO: finish this logic - self.plant.connect( - f"{commodity_stream}.{group_configs.get('commodity_stream_name')}", - f"finance_subgroup_{group_id}.capacity_factor", - ) # Only connect technologies that are included in the finance stackup for tech_name in tech_configs.keys(): diff --git a/h2integrate/finances/finances.py b/h2integrate/finances/finances.py index a2d2db9cf..d8f125715 100644 --- a/h2integrate/finances/finances.py +++ b/h2integrate/finances/finances.py @@ -90,25 +90,25 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): class AdjustedCapacityFactorComp(om.ExplicitComponent): def initialize(self): self.options.declare("plant_config", types=dict) - self.options.declare("commodity_stream_name", types=str) self.options.declare("commodity_type", types=str) def setup(self): + self.commodity = self.options["commodity_type"] plant_life = int(self.options["plant_config"]["plant"]["plant_life"]) - self.n_timesteps = int(self.options["plant_config"]["simulation"]["n_timesteps"]) - self.dt = int(self.options["plant_config"]["simulation"]["dt"]) + self.n_timesteps = int(self.options["plant_config"]["plant"]["simulation"]["n_timesteps"]) + self.dt = int(self.options["plant_config"]["plant"]["simulation"]["dt"]) self.add_input( - self.options["commodity_stream_name"], + f"{self.commodity}_produced", val=0.0, units_by_conn=True, shape=self.n_timesteps, ) self.add_output( - f"rated_{self.options['commodity_type']}_production", + f"rated_{self.commodity}_production", val=0.0, - copy_units=self.options["commodity_stream_name"], + copy_units=f"{self.commodity}_produced", shape=1, ) @@ -120,8 +120,8 @@ def setup(self): ) def compute(self, inputs, outputs): - outputs[f"rated_{self.options['commodity_type']}_production"] = np.mean( - inputs[self.options["commodity_stream_name"]] + outputs[f"rated_{self.commodity}_production"] = np.mean( + inputs[f"{self.commodity}_produced"] ) outputs["capacity_factor"] = 1.0 From 322b9004a184fd7c5541623790ea2218e11ab67e Mon Sep 17 00:00:00 2001 From: John Jasa Date: Tue, 5 May 2026 09:27:57 -0600 Subject: [PATCH 04/13] Added docstring to new class --- h2integrate/finances/finances.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/h2integrate/finances/finances.py b/h2integrate/finances/finances.py index d8f125715..a983a33cc 100644 --- a/h2integrate/finances/finances.py +++ b/h2integrate/finances/finances.py @@ -88,6 +88,23 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): class AdjustedCapacityFactorComp(om.ExplicitComponent): + """OpenMDAO component to compute an adjusted capacity factor for a given commodity. + + This component takes in a timeseries of commodity production values and computes + the rated (mean) production and a capacity factor for each year of the plant's + lifetime. Currently, the capacity factor is set to 1.0 by default. + + Inputs: + {commodity}_produced (array): Timeseries of commodity production values + with shape ``(n_timesteps,)``. Units are determined by connection. + + Outputs: + rated_{commodity}_production (float): Mean commodity production across all + timesteps. Units are copied from the input. + capacity_factor (array): Capacity factor for each year of plant life, + with shape ``(plant_life,)``. Unitless. + """ + def initialize(self): self.options.declare("plant_config", types=dict) self.options.declare("commodity_type", types=str) @@ -95,14 +112,13 @@ def initialize(self): def setup(self): self.commodity = self.options["commodity_type"] plant_life = int(self.options["plant_config"]["plant"]["plant_life"]) - self.n_timesteps = int(self.options["plant_config"]["plant"]["simulation"]["n_timesteps"]) - self.dt = int(self.options["plant_config"]["plant"]["simulation"]["dt"]) + n_timesteps = int(self.options["plant_config"]["plant"]["simulation"]["n_timesteps"]) self.add_input( f"{self.commodity}_produced", val=0.0, units_by_conn=True, - shape=self.n_timesteps, + shape=n_timesteps, ) self.add_output( From 4284851a2926b2b63abdcebac0977a653557f652 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 5 May 2026 09:39:20 -0600 Subject: [PATCH 05/13] added test for new component --- .../finances/test/test_finance_components.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 h2integrate/finances/test/test_finance_components.py diff --git a/h2integrate/finances/test/test_finance_components.py b/h2integrate/finances/test/test_finance_components.py new file mode 100644 index 000000000..6c4f888db --- /dev/null +++ b/h2integrate/finances/test/test_finance_components.py @@ -0,0 +1,52 @@ +import numpy as np +import pytest +import openmdao.api as om + +from h2integrate.finances.finances import AdjustedCapacityFactorComp + + +@pytest.fixture +def plant_config(n_timesteps): + plant = { + "plant": { + "plant_life": 30, + "simulation": { + "dt": 3600, + "n_timesteps": n_timesteps, + }, + }, + } + return plant + + +@pytest.mark.unit +@pytest.mark.parametrize("n_timesteps", [8760]) +def test_adjusted_cf_comp(plant_config, subtests): + rng = np.random.default_rng(seed=0) + electricity_input = rng.random(8760) * 1e3 + + prob = om.Problem() + comp = AdjustedCapacityFactorComp(plant_config=plant_config, commodity_type="electricity") + prob.model.add_subsystem("comp", comp, promotes=["*"]) + + ivc = om.IndepVarComp() + ivc.add_output("electricity_produced", val=np.zeros(8760), units="kW") + prob.model.add_subsystem("ivc", ivc, promotes=["*"]) + + prob.setup() + prob.set_val("ivc.electricity_produced", val=electricity_input, units="kW") + prob.run_model() + + with subtests.test("Check rated_electricity_production calc"): + assert ( + pytest.approx( + prob.model.get_val("comp.rated_electricity_production", units="kW"), rel=1e-6 + ) + == electricity_input.mean() + ) + + with subtests.test("Check capacity factor"): + assert np.all(prob.model.get_val("comp.capacity_factor", units="unitless")) == 1.0 + + with subtests.test("Capacity factor length"): + assert len(prob.model.get_val("comp.capacity_factor", units="unitless")) == 30 From b02db6c1245e69844e6107eadc2632b29c12748a Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 5 May 2026 09:40:18 -0600 Subject: [PATCH 06/13] added doc string --- h2integrate/finances/finances.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/h2integrate/finances/finances.py b/h2integrate/finances/finances.py index d8f125715..b1debb06c 100644 --- a/h2integrate/finances/finances.py +++ b/h2integrate/finances/finances.py @@ -88,6 +88,21 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): class AdjustedCapacityFactorComp(om.ExplicitComponent): + """OpenMDAO component to calculate the capacity factor and + rated commodity production based on a time-series profile. This + component should be used when the desired commodity stream of a + technology does not have an associated capacity factor or rated + production output. + + Inputs: + {commodity}_produced (np.ndarray): timeseries profile of some commodity. + + Outputs: + rated_{commodity}_produced (float): The average commodity produced. + capacity_factor (np.ndarray): An array of ones, same length as the plant life. + + """ + def initialize(self): self.options.declare("plant_config", types=dict) self.options.declare("commodity_type", types=str) @@ -120,8 +135,10 @@ def setup(self): ) def compute(self, inputs, outputs): + # Take the average of the timeseries profile outputs[f"rated_{self.commodity}_production"] = np.mean( inputs[f"{self.commodity}_produced"] ) + # Set capacity factor equal to one outputs["capacity_factor"] = 1.0 From 76b9831a18d850333b1487ab19767e5ae36a0554 Mon Sep 17 00:00:00 2001 From: John Jasa Date: Tue, 5 May 2026 09:44:50 -0600 Subject: [PATCH 07/13] Apply suggestion from @johnjasa --- h2integrate/core/h2integrate_model.py | 1 - 1 file changed, 1 deletion(-) diff --git a/h2integrate/core/h2integrate_model.py b/h2integrate/core/h2integrate_model.py index 90f5ef756..e1a63e181 100644 --- a/h2integrate/core/h2integrate_model.py +++ b/h2integrate/core/h2integrate_model.py @@ -798,7 +798,6 @@ def create_finance_model(self): ) tech_names = subgroup_params.get("technologies") commodity_stream = subgroup_params.get("commodity_stream", None) - (False if subgroup_params.get("commodity_stream_name", None) is None else True) if isinstance(finance_group_names, str): finance_group_names = [finance_group_names] From d02f01dbaaee6f94cb55d5bec257dd1f955162d6 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 5 May 2026 09:59:51 -0600 Subject: [PATCH 08/13] added test for error --- h2integrate/core/h2integrate_model.py | 4 ++-- h2integrate/core/test/test_framework.py | 29 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/h2integrate/core/h2integrate_model.py b/h2integrate/core/h2integrate_model.py index 90f5ef756..e311050e9 100644 --- a/h2integrate/core/h2integrate_model.py +++ b/h2integrate/core/h2integrate_model.py @@ -1008,11 +1008,11 @@ def create_finance_model(self): commodity_output_desc = commodity_output_desc + f"_{finance_group_name}" if finance_subgroups[subgroup_name]["use_commodity_stream_timeseries"]: - if "commodity_stream_name" not in finance_subgroups[subgroup_name]: + if finance_subgroups[subgroup_name].get("commodity_stream_name", None) is None: msg = ( "`commodity_stream_name` is a required input if " f"`use_commodity_stream_timeseries` is True. Please add the " - f"`commodity_stream_name` for finance subgroup {subgroup_name}" + f"`commodity_stream_name` for finance subgroup `{subgroup_name}`" ) raise ValueError(msg) diff --git a/h2integrate/core/test/test_framework.py b/h2integrate/core/test/test_framework.py index b080e935b..0b9ad960b 100644 --- a/h2integrate/core/test/test_framework.py +++ b/h2integrate/core/test/test_framework.py @@ -14,6 +14,35 @@ from h2integrate.core.inputs.validation import load_tech_yaml, load_plant_yaml, load_driver_yaml +@pytest.mark.integration +@pytest.mark.parametrize( + "example_folder,resource_example_folder", [("17_splitter_wind_doc_h2", None)] +) +def test_use_commodity_stream_timeseries_finances_error(subtests, temp_copy_of_example): + example_folder = temp_copy_of_example + plant_config = load_plant_yaml(example_folder / "plant_config.yaml") + driver_config = load_driver_yaml(example_folder / "driver_config.yaml") + tech_config = load_tech_yaml(example_folder / "tech_config.yaml") + + # Remove commodity_stream_name from finace subgroup + plant_config["finance_parameters"]["finance_subgroups"]["electricity_doc"].pop( + "commodity_stream_name" + ) + top_level_config = { + "plant_config": plant_config, + "technology_config": tech_config, + "driver_config": driver_config, + } + + with subtests.test("Commodity stream name is missing"): + with pytest.raises(ValueError) as excinfo: + H2IntegrateModel(top_level_config) + err = str(excinfo.value) + assert "`commodity_stream_name` is a required input" in err + assert "`use_commodity_stream_timeseries` is True" in err + assert "finance subgroup `electricity_doc`" in err + + @pytest.mark.integration @pytest.mark.parametrize("example_folder,resource_example_folder", [("01_onshore_steel_mn", None)]) def test_check_tech_interconnections(subtests, temp_copy_of_example): From 167e49e6873008fe4f167e246d974827d31d06e0 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 5 May 2026 10:26:50 -0600 Subject: [PATCH 09/13] added more subtests for example 17 --- .../17_splitter_wind_doc_h2/plant_config.yaml | 1 + examples/test/test_all_examples.py | 59 ++++++++++++++++++- 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/examples/17_splitter_wind_doc_h2/plant_config.yaml b/examples/17_splitter_wind_doc_h2/plant_config.yaml index eb29302c8..b78d5e778 100644 --- a/examples/17_splitter_wind_doc_h2/plant_config.yaml +++ b/examples/17_splitter_wind_doc_h2/plant_config.yaml @@ -66,6 +66,7 @@ finance_parameters: technologies: [wind, electrolyzer] hydrogen: commodity: hydrogen + commodity_stream: electrolyzer technologies: [wind, electrolyzer] co2: commodity: co2 diff --git a/examples/test/test_all_examples.py b/examples/test/test_all_examples.py index 7df6f03ef..62a67ba60 100644 --- a/examples/test/test_all_examples.py +++ b/examples/test/test_all_examples.py @@ -10,6 +10,7 @@ from h2integrate import ROOT_DIR from h2integrate.core.file_utils import load_yaml from h2integrate.core.h2integrate_model import H2IntegrateModel +from h2integrate.core.inputs.validation import load_plant_yaml ROOT = Path(__file__).parents[1] @@ -646,8 +647,38 @@ def test_wind_wave_doc_example(subtests, temp_copy_of_example): def test_splitter_wind_doc_h2_example(subtests, temp_copy_of_example): example_folder = temp_copy_of_example + new_finance_subgroup_h2 = { + "hydrogen_ts": { + "commodity": "hydrogen", + "commodity_stream": "electrolyzer", + "use_commodity_stream_timeseries": True, + "commodity_stream_name": "hydrogen_out", + "technologies": ["wind", "electrolyzer"], + } + } + + new_finance_subgroup_wind = { + "electricity_ts": { + "commodity": "electricity", + "commodity_stream": "wind", + "use_commodity_stream_timeseries": True, + "commodity_stream_name": "electricity_out", + "technologies": ["wind"], + } + } + + plant_config = load_plant_yaml(example_folder / "plant_config.yaml") + + plant_config["finance_parameters"]["finance_subgroups"].update(new_finance_subgroup_h2) + plant_config["finance_parameters"]["finance_subgroups"].update(new_finance_subgroup_wind) + + top_level_config = { + "plant_config": plant_config, + "technology_config": example_folder / "tech_config.yaml", + "driver_config": example_folder / "driver_config.yaml", + } # Create a H2Integrate model - model = H2IntegrateModel(example_folder / "offshore_plant_splitter_doc_h2.yaml") + model = H2IntegrateModel(top_level_config) # Run the model model.run() @@ -679,6 +710,23 @@ def test_splitter_wind_doc_h2_example(subtests, temp_copy_of_example): == 9.8059083 ) + with subtests.test( + "Check LCOH (using timeseries) is less than LCOH using lifetime performance" + ): + assert ( + model.prob.get_val("finance_subgroup_hydrogen_ts.LCOH", units="USD/kg")[0] + < model.prob.get_val("finance_subgroup_hydrogen.LCOH", units="USD/kg")[0] + ) + + with subtests.test("Check LCOH (using timeseries)"): + assert ( + pytest.approx( + model.prob.get_val("finance_subgroup_hydrogen_ts.LCOH", units="USD/kg")[0], + rel=1e-3, + ) + == 9.34595395123 + ) + with subtests.test("Check LCOC"): assert ( pytest.approx( @@ -696,6 +744,15 @@ def test_splitter_wind_doc_h2_example(subtests, temp_copy_of_example): == 132.395036462 ) + with subtests.test("Check LCOE (using timeseries)"): + assert ( + pytest.approx( + model.prob.get_val("finance_subgroup_electricity_ts.LCOE", units="USD/(MW*h)")[0], + rel=1e-3, + ) + == model.prob.get_val("finance_subgroup_electricity.LCOE", units="USD/(MW*h)")[0] + ) + with subtests.test("Check LCOE (doc)"): assert ( pytest.approx( From a9d4ae91b0fa64fb4e361f402d6ecdfee22e21d0 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 5 May 2026 10:43:28 -0600 Subject: [PATCH 10/13] renamed variable to commodity_stream_output --- .../17_splitter_wind_doc_h2/plant_config.yaml | 4 ++-- examples/test/test_all_examples.py | 4 ++-- h2integrate/core/h2integrate_model.py | 15 ++++++++++----- h2integrate/core/test/test_framework.py | 6 +++--- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/examples/17_splitter_wind_doc_h2/plant_config.yaml b/examples/17_splitter_wind_doc_h2/plant_config.yaml index b78d5e778..227a66922 100644 --- a/examples/17_splitter_wind_doc_h2/plant_config.yaml +++ b/examples/17_splitter_wind_doc_h2/plant_config.yaml @@ -56,13 +56,13 @@ finance_parameters: commodity: electricity commodity_stream: electricity_splitter use_commodity_stream_timeseries: true - commodity_stream_name: electricity_out1 + commodity_stream_output: electricity_out1 technologies: [wind, doc] electricity_electrolyzer: commodity: electricity commodity_stream: electricity_splitter use_commodity_stream_timeseries: true - commodity_stream_name: electricity_out2 + commodity_stream_output: electricity_out2 technologies: [wind, electrolyzer] hydrogen: commodity: hydrogen diff --git a/examples/test/test_all_examples.py b/examples/test/test_all_examples.py index 62a67ba60..69b3e40a3 100644 --- a/examples/test/test_all_examples.py +++ b/examples/test/test_all_examples.py @@ -652,7 +652,7 @@ def test_splitter_wind_doc_h2_example(subtests, temp_copy_of_example): "commodity": "hydrogen", "commodity_stream": "electrolyzer", "use_commodity_stream_timeseries": True, - "commodity_stream_name": "hydrogen_out", + "commodity_stream_output": "hydrogen_out", "technologies": ["wind", "electrolyzer"], } } @@ -662,7 +662,7 @@ def test_splitter_wind_doc_h2_example(subtests, temp_copy_of_example): "commodity": "electricity", "commodity_stream": "wind", "use_commodity_stream_timeseries": True, - "commodity_stream_name": "electricity_out", + "commodity_stream_output": "electricity_out", "technologies": ["wind"], } } diff --git a/h2integrate/core/h2integrate_model.py b/h2integrate/core/h2integrate_model.py index 37f174090..84654549c 100644 --- a/h2integrate/core/h2integrate_model.py +++ b/h2integrate/core/h2integrate_model.py @@ -841,7 +841,9 @@ def create_finance_model(self): "use_commodity_stream_timeseries": subgroup_params.get( "use_commodity_stream_timeseries", False ), - "commodity_stream_name": subgroup_params.get("commodity_stream_name", None), + "commodity_stream_output": subgroup_params.get( + "commodity_stream_output", None + ), } } ) @@ -1007,11 +1009,14 @@ def create_finance_model(self): commodity_output_desc = commodity_output_desc + f"_{finance_group_name}" if finance_subgroups[subgroup_name]["use_commodity_stream_timeseries"]: - if finance_subgroups[subgroup_name].get("commodity_stream_name", None) is None: + if ( + finance_subgroups[subgroup_name].get("commodity_stream_output", None) + is None + ): msg = ( - "`commodity_stream_name` is a required input if " + "`commodity_stream_output` is a required input if " f"`use_commodity_stream_timeseries` is True. Please add the " - f"`commodity_stream_name` for finance subgroup `{subgroup_name}`" + f"`commodity_stream_output` for finance subgroup `{subgroup_name}`" ) raise ValueError(msg) @@ -1320,7 +1325,7 @@ def connect_technologies(self): if group_configs.get("use_commodity_stream_timeseries", False): # TODO: finish this logic self.plant.connect( - f"{commodity_stream}.{group_configs.get('commodity_stream_name')}", + f"{commodity_stream}.{group_configs.get('commodity_stream_output')}", f"finance_subgroup_{group_id}.{primary_commodity_type}_produced", ) else: diff --git a/h2integrate/core/test/test_framework.py b/h2integrate/core/test/test_framework.py index 0b9ad960b..6305d4478 100644 --- a/h2integrate/core/test/test_framework.py +++ b/h2integrate/core/test/test_framework.py @@ -24,9 +24,9 @@ def test_use_commodity_stream_timeseries_finances_error(subtests, temp_copy_of_e driver_config = load_driver_yaml(example_folder / "driver_config.yaml") tech_config = load_tech_yaml(example_folder / "tech_config.yaml") - # Remove commodity_stream_name from finace subgroup + # Remove commodity_stream_output from finace subgroup plant_config["finance_parameters"]["finance_subgroups"]["electricity_doc"].pop( - "commodity_stream_name" + "commodity_stream_output" ) top_level_config = { "plant_config": plant_config, @@ -38,7 +38,7 @@ def test_use_commodity_stream_timeseries_finances_error(subtests, temp_copy_of_e with pytest.raises(ValueError) as excinfo: H2IntegrateModel(top_level_config) err = str(excinfo.value) - assert "`commodity_stream_name` is a required input" in err + assert "`commodity_stream_output` is a required input" in err assert "`use_commodity_stream_timeseries` is True" in err assert "finance subgroup `electricity_doc`" in err From 0daa108c46ebc48f47ce90a985115abb8286428f Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 5 May 2026 11:13:41 -0600 Subject: [PATCH 11/13] updated documentation --- .../specifying_finance_parameters.md | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/docs/user_guide/specifying_finance_parameters.md b/docs/user_guide/specifying_finance_parameters.md index 3f12b4e4c..2a375386b 100644 --- a/docs/user_guide/specifying_finance_parameters.md +++ b/docs/user_guide/specifying_finance_parameters.md @@ -45,6 +45,9 @@ Within this framework, there are two distinct layers, **finance groups** and **f A text label to further distinguish outputs for a commodity. This is particularly useful when multiple finance models or subgroups reference the same commodity but need to produce separate outputs. - `commodity_stream` (optional): A text label of a technology that outputs the specified ``commodity`` to use as the commodity production stream in finance calculations. This is particularly useful when wanting to choose a specific commodity stream to use in finance calculations (such as the outputs of combiners or splitters) + - The below two parameters are only needed to [use a specified timeseries profile for finance calculations](fin:commodity_output_streams) + - `use_commodity_stream_timeseries` (optional): A boolean that defaults to False. If True, then flags to use a timeseries profile for the finance calculation rather than the capacity factor and rated commodity production of the `commodity_stream` technology. + - `commodity_stream_output`: The name of timeseries profile output variable from `commodity_stream` to use for the finance calculation(s). This parameter is required if `use_commodity_stream_timeseries` is True. ```{important} If no subgroups are defined, a **default subgroup** is created that contains *all technologies* and references the default finance model and commodity defined in `finance_groups`. @@ -159,3 +162,45 @@ Examples: - Finance groups must not include a key named "default", as this is reserved for internal use. - Each subgroup must reference valid technology keys from technology_config['technologies']. Invalid keys raise errors. - Finance models must be listed in `self.supported_models`. Unknown models raise errors. + +(fin:commodity_output_streams)= +## Using custom output streams for finance calculations +Typically, the finance models use the capacity factor and rated commodity production rate of the technology specified as the `commodity_stream` for the finance calculations. + +In some cases, the capacity factor or rated commodity production rate are not available or may not contain the information desired for the finance calculation. Some technologies, such as splitters, don't output the capacity factor or rated commodity production, they just output two timeseries profiles. Other technologies, such as the grid, output the capacity factor based on the electricity bought, but not the electricity sold. Below illustrates how to use a specified timeseries profile for the finance calculations rather than the capacity factor and rated commodity production rate output from the `commodity_stream` technology. + +Below shows three different finance subgroups that use the timeseries outputs. +- `subgroup_a` is using the `wind.electricity_out` profile for the finance calculation. +- `subgroup_b` is using the `splitter.electricity_out1` profile for the finance calculation. +- `subgroup_c` is using the `grid.electricity_sold` profile for the finance calculation. + +To use this functionality, `use_commodity_stream_timeseries` must be True and `commodity_stream_output` must be specified. + +General format: +```yaml +finance_parameters: + finance_groups: + finance_model: ProFastLCO + model_inputs: #dictionary of inputs for ProFastLCO + finance_subgroups: + subgroup_a: + commodity: electricity #required + commodity_stream: wind # technology that electricity is output from + use_commodity_stream_timeseries: true + commodity_stream_output: electricity_out + technologies: [wind] + subgroup_b: + commodity: electricity #required + commodity_stream: splitter + use_commodity_stream_timeseries: true + commodity_stream_output: electricity_out1 + technologies: [wind, electrolyzer] + subgroup_c: + commodity: electricity #required + commodity_stream: grid + use_commodity_stream_timeseries: true + commodity_stream_output: electricity_sold + technologies: [grid] +``` + +- [Example 17](https://github.com/NatLabRockies/H2Integrate/tree/develop/examples/17_splitter_wind_doc_h2/plant_config.yaml) From 6ac8ce93780a3fd6705d363bd6bd50eb735b7619 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Wed, 6 May 2026 14:28:14 -0600 Subject: [PATCH 12/13] updated test --- h2integrate/core/test/test_framework.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/h2integrate/core/test/test_framework.py b/h2integrate/core/test/test_framework.py index 6305d4478..b02718f21 100644 --- a/h2integrate/core/test/test_framework.py +++ b/h2integrate/core/test/test_framework.py @@ -34,12 +34,16 @@ def test_use_commodity_stream_timeseries_finances_error(subtests, temp_copy_of_e "driver_config": driver_config, } - with subtests.test("Commodity stream name is missing"): - with pytest.raises(ValueError) as excinfo: - H2IntegrateModel(top_level_config) - err = str(excinfo.value) + with pytest.raises(ValueError) as excinfo: + H2IntegrateModel(top_level_config) + err = str(excinfo.value) + with subtests.test("Commodity stream name is missing (commodity_stream_output is required)"): assert "`commodity_stream_output` is a required input" in err + with subtests.test( + "Commodity stream name is missing (use_commodity_stream_timeseries is True)" + ): assert "`use_commodity_stream_timeseries` is True" in err + with subtests.test("Commodity stream name is missing (finance subgroup `electricity_doc`)"): assert "finance subgroup `electricity_doc`" in err From a705e8fc0e18195d486560401ea79c54fc10effb Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Wed, 6 May 2026 16:35:37 -0600 Subject: [PATCH 13/13] updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff25ce10e..f81491843 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - Rename `n_control_window` to `n_control_window_hours` for unit clarity [PR 712](https://github.com/NatLabRockies/H2Integrate/pull/712) - Update N2 diagram for demand openloop control from static and outdated to dynamic and interactive [PR 714](https://github.com/NatLabRockies/H2Integrate/pull/714) - Added basic check of 4-length connections in `technology_interconnections` [PR 720](https://github.com/NatLabRockies/H2Integrate/pull/720) +- Added ability to use timeseries for finance calculations [PR 725](https://github.com/NatLabRockies/H2Integrate/pull/725) ## 0.8 [April 15, 2026] - Updated README and docs intro page with expanded H2I description, reorganized sections, and streamlined installation instructions [PR 677](https://github.com/NatLabRockies/H2Integrate/pull/677)