Skip to content

Commit 598a341

Browse files
Ability to use timeseries profiles for finance calcs (#725)
* added adjusted capacity factor comp, still need to update connections to it * attempted to add connection but work-in-progress * updated example 17 and added subtests, finished integration * Added docstring to new class * added test for new component * added doc string * Apply suggestion from @johnjasa * added test for error * added more subtests for example 17 * renamed variable to commodity_stream_output * updated documentation * updated test --------- Co-authored-by: John Jasa <johnjasa11@gmail.com> Co-authored-by: John Jasa <john.jasa@nrel.gov>
1 parent da34364 commit 598a341

8 files changed

Lines changed: 346 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
- Update N2 diagram for demand openloop control from static and outdated to dynamic and interactive [PR 714](https://github.com/NatLabRockies/H2Integrate/pull/714)
2020
- Added basic check of 4-length connections in `technology_interconnections` [PR 720](https://github.com/NatLabRockies/H2Integrate/pull/720)
2121
- Update N2 diagram for Pyomo heuristic control from static image to dynamic and interactive embedded diagram [PR 726](https://github.com/NatLabRockies/H2Integrate/pull/726)
22+
- Added ability to use timeseries for finance calculations [PR 725](https://github.com/NatLabRockies/H2Integrate/pull/725)
2223

2324
## 0.8 [April 15, 2026]
2425
- 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)

docs/user_guide/specifying_finance_parameters.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ Within this framework, there are two distinct layers, **finance groups** and **f
4545
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.
4646
- `commodity_stream` (optional):
4747
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)
48+
- The below two parameters are only needed to [use a specified timeseries profile for finance calculations](fin:commodity_output_streams)
49+
- `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.
50+
- `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.
4851

4952
```{important}
5053
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:
159162
- Finance groups must not include a key named "default", as this is reserved for internal use.
160163
- Each subgroup must reference valid technology keys from technology_config['technologies']. Invalid keys raise errors.
161164
- Finance models must be listed in `self.supported_models`. Unknown models raise errors.
165+
166+
(fin:commodity_output_streams)=
167+
## Using custom output streams for finance calculations
168+
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.
169+
170+
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.
171+
172+
Below shows three different finance subgroups that use the timeseries outputs.
173+
- `subgroup_a` is using the `wind.electricity_out` profile for the finance calculation.
174+
- `subgroup_b` is using the `splitter.electricity_out1` profile for the finance calculation.
175+
- `subgroup_c` is using the `grid.electricity_sold` profile for the finance calculation.
176+
177+
To use this functionality, `use_commodity_stream_timeseries` must be True and `commodity_stream_output` must be specified.
178+
179+
General format:
180+
```yaml
181+
finance_parameters:
182+
finance_groups:
183+
finance_model: ProFastLCO
184+
model_inputs: #dictionary of inputs for ProFastLCO
185+
finance_subgroups:
186+
subgroup_a:
187+
commodity: electricity #required
188+
commodity_stream: wind # technology that electricity is output from
189+
use_commodity_stream_timeseries: true
190+
commodity_stream_output: electricity_out
191+
technologies: [wind]
192+
subgroup_b:
193+
commodity: electricity #required
194+
commodity_stream: splitter
195+
use_commodity_stream_timeseries: true
196+
commodity_stream_output: electricity_out1
197+
technologies: [wind, electrolyzer]
198+
subgroup_c:
199+
commodity: electricity #required
200+
commodity_stream: grid
201+
use_commodity_stream_timeseries: true
202+
commodity_stream_output: electricity_sold
203+
technologies: [grid]
204+
```
205+
206+
- [Example 17](https://github.com/NatLabRockies/H2Integrate/tree/develop/examples/17_splitter_wind_doc_h2/plant_config.yaml)

examples/17_splitter_wind_doc_h2/plant_config.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,21 @@ finance_parameters:
5252
electricity:
5353
commodity: electricity
5454
technologies: [wind]
55+
electricity_doc:
56+
commodity: electricity
57+
commodity_stream: electricity_splitter
58+
use_commodity_stream_timeseries: true
59+
commodity_stream_output: electricity_out1
60+
technologies: [wind, doc]
61+
electricity_electrolyzer:
62+
commodity: electricity
63+
commodity_stream: electricity_splitter
64+
use_commodity_stream_timeseries: true
65+
commodity_stream_output: electricity_out2
66+
technologies: [wind, electrolyzer]
5567
hydrogen:
5668
commodity: hydrogen
69+
commodity_stream: electrolyzer
5770
technologies: [wind, electrolyzer]
5871
co2:
5972
commodity: co2

examples/test/test_all_examples.py

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from h2integrate import ROOT_DIR
1111
from h2integrate.core.file_utils import load_yaml
1212
from h2integrate.core.h2integrate_model import H2IntegrateModel
13+
from h2integrate.core.inputs.validation import load_plant_yaml
1314

1415

1516
ROOT = Path(__file__).parents[1]
@@ -646,8 +647,38 @@ def test_wind_wave_doc_example(subtests, temp_copy_of_example):
646647
def test_splitter_wind_doc_h2_example(subtests, temp_copy_of_example):
647648
example_folder = temp_copy_of_example
648649

650+
new_finance_subgroup_h2 = {
651+
"hydrogen_ts": {
652+
"commodity": "hydrogen",
653+
"commodity_stream": "electrolyzer",
654+
"use_commodity_stream_timeseries": True,
655+
"commodity_stream_output": "hydrogen_out",
656+
"technologies": ["wind", "electrolyzer"],
657+
}
658+
}
659+
660+
new_finance_subgroup_wind = {
661+
"electricity_ts": {
662+
"commodity": "electricity",
663+
"commodity_stream": "wind",
664+
"use_commodity_stream_timeseries": True,
665+
"commodity_stream_output": "electricity_out",
666+
"technologies": ["wind"],
667+
}
668+
}
669+
670+
plant_config = load_plant_yaml(example_folder / "plant_config.yaml")
671+
672+
plant_config["finance_parameters"]["finance_subgroups"].update(new_finance_subgroup_h2)
673+
plant_config["finance_parameters"]["finance_subgroups"].update(new_finance_subgroup_wind)
674+
675+
top_level_config = {
676+
"plant_config": plant_config,
677+
"technology_config": example_folder / "tech_config.yaml",
678+
"driver_config": example_folder / "driver_config.yaml",
679+
}
649680
# Create a H2Integrate model
650-
model = H2IntegrateModel(example_folder / "offshore_plant_splitter_doc_h2.yaml")
681+
model = H2IntegrateModel(top_level_config)
651682

652683
# Run the model
653684
model.run()
@@ -679,6 +710,23 @@ def test_splitter_wind_doc_h2_example(subtests, temp_copy_of_example):
679710
== 9.8059083
680711
)
681712

713+
with subtests.test(
714+
"Check LCOH (using timeseries) is less than LCOH using lifetime performance"
715+
):
716+
assert (
717+
model.prob.get_val("finance_subgroup_hydrogen_ts.LCOH", units="USD/kg")[0]
718+
< model.prob.get_val("finance_subgroup_hydrogen.LCOH", units="USD/kg")[0]
719+
)
720+
721+
with subtests.test("Check LCOH (using timeseries)"):
722+
assert (
723+
pytest.approx(
724+
model.prob.get_val("finance_subgroup_hydrogen_ts.LCOH", units="USD/kg")[0],
725+
rel=1e-3,
726+
)
727+
== 9.34595395123
728+
)
729+
682730
with subtests.test("Check LCOC"):
683731
assert (
684732
pytest.approx(
@@ -696,6 +744,58 @@ def test_splitter_wind_doc_h2_example(subtests, temp_copy_of_example):
696744
== 132.395036462
697745
)
698746

747+
with subtests.test("Check LCOE (using timeseries)"):
748+
assert (
749+
pytest.approx(
750+
model.prob.get_val("finance_subgroup_electricity_ts.LCOE", units="USD/(MW*h)")[0],
751+
rel=1e-3,
752+
)
753+
== model.prob.get_val("finance_subgroup_electricity.LCOE", units="USD/(MW*h)")[0]
754+
)
755+
756+
with subtests.test("Check LCOE (doc)"):
757+
assert (
758+
pytest.approx(
759+
model.prob.get_val("finance_subgroup_electricity_doc.LCOE", units="USD/(MW*h)")[0],
760+
rel=1e-3,
761+
)
762+
== 674.2414136935529
763+
)
764+
765+
with subtests.test("Check finance_subgroup_electricity_doc electricity inputs"):
766+
assert (
767+
pytest.approx(
768+
model.prob.get_val(
769+
"finance_subgroup_electricity_doc.rated_electricity_production", units="kW"
770+
),
771+
rel=1e-6,
772+
)
773+
== model.prob.get_val("doc.electricity_in", units="kW").mean()
774+
)
775+
776+
with subtests.test("Check LCOE (electrolyzer)"):
777+
assert (
778+
pytest.approx(
779+
model.prob.get_val(
780+
"finance_subgroup_electricity_electrolyzer.LCOE", units="USD/(MW*h)"
781+
)[0],
782+
rel=1e-3,
783+
)
784+
== 182.8942790183688
785+
)
786+
787+
with subtests.test("Check finance_subgroup_electricity_electrolyzer electricity inputs"):
788+
assert (
789+
pytest.approx(
790+
model.prob.get_val(
791+
"finance_subgroup_electricity_electrolyzer.rated_electricity_production",
792+
units="kW",
793+
),
794+
rel=1e-6,
795+
)
796+
== model.prob.get_val("electrolyzer.electricity_in", units="kW").mean()
797+
)
798+
699799

700800
@pytest.mark.integration
701801
@pytest.mark.parametrize(

h2integrate/core/h2integrate_model.py

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from h2integrate.core.utilities import create_xdsm_from_config
1212
from h2integrate.core.dict_utils import check_inputs
1313
from h2integrate.core.file_utils import get_path, find_file, load_yaml
14-
from h2integrate.finances.finances import AdjustedCapexOpexComp
14+
from h2integrate.finances.finances import AdjustedCapexOpexComp, AdjustedCapacityFactorComp
1515
from h2integrate.core.supported_models import (
1616
no_cost_models,
1717
supported_models,
@@ -798,7 +798,6 @@ def create_finance_model(self):
798798
)
799799
tech_names = subgroup_params.get("technologies")
800800
commodity_stream = subgroup_params.get("commodity_stream", None)
801-
802801
if isinstance(finance_group_names, str):
803802
finance_group_names = [finance_group_names]
804803

@@ -839,9 +838,16 @@ def create_finance_model(self):
839838
"commodity": commodity,
840839
"commodity_stream": commodity_stream,
841840
"is_system_finance_model": True,
841+
"use_commodity_stream_timeseries": subgroup_params.get(
842+
"use_commodity_stream_timeseries", False
843+
),
844+
"commodity_stream_output": subgroup_params.get(
845+
"commodity_stream_output", None
846+
),
842847
}
843848
}
844849
)
850+
845851
finance_subgroup = om.Group()
846852

847853
# Default logic for handling cases without specified commodity streams
@@ -1002,6 +1008,24 @@ def create_finance_model(self):
10021008
# uniquely named outputs
10031009
commodity_output_desc = commodity_output_desc + f"_{finance_group_name}"
10041010

1011+
if finance_subgroups[subgroup_name]["use_commodity_stream_timeseries"]:
1012+
if (
1013+
finance_subgroups[subgroup_name].get("commodity_stream_output", None)
1014+
is None
1015+
):
1016+
msg = (
1017+
"`commodity_stream_output` is a required input if "
1018+
f"`use_commodity_stream_timeseries` is True. Please add the "
1019+
f"`commodity_stream_output` for finance subgroup `{subgroup_name}`"
1020+
)
1021+
raise ValueError(msg)
1022+
1023+
adj_cf_comp = AdjustedCapacityFactorComp(
1024+
plant_config=filtered_plant_config,
1025+
commodity_type=commodity,
1026+
)
1027+
finance_subgroup.add_subsystem("adjusted_cf_comp", adj_cf_comp, promotes=["*"])
1028+
10051029
# create the finance component
10061030
fin_comp = fin_model(
10071031
driver_config=self.driver_config,
@@ -1298,17 +1322,24 @@ def connect_technologies(self):
12981322
is_system_finance_model = group_configs.get("is_system_finance_model")
12991323

13001324
if is_system_finance_model:
1301-
# Connect the rated commodity production and capacity factor
1302-
# for system-level finance models
1303-
self.plant.connect(
1304-
f"{commodity_stream}.rated_{primary_commodity_type}_production",
1305-
f"finance_subgroup_{group_id}.rated_{primary_commodity_type}_production",
1306-
)
1325+
if group_configs.get("use_commodity_stream_timeseries", False):
1326+
# TODO: finish this logic
1327+
self.plant.connect(
1328+
f"{commodity_stream}.{group_configs.get('commodity_stream_output')}",
1329+
f"finance_subgroup_{group_id}.{primary_commodity_type}_produced",
1330+
)
1331+
else:
1332+
# Connect the rated commodity production and capacity factor
1333+
# for system-level finance models
1334+
self.plant.connect(
1335+
f"{commodity_stream}.rated_{primary_commodity_type}_production",
1336+
f"finance_subgroup_{group_id}.rated_{primary_commodity_type}_production",
1337+
)
13071338

1308-
self.plant.connect(
1309-
f"{commodity_stream}.capacity_factor",
1310-
f"finance_subgroup_{group_id}.capacity_factor",
1311-
)
1339+
self.plant.connect(
1340+
f"{commodity_stream}.capacity_factor",
1341+
f"finance_subgroup_{group_id}.capacity_factor",
1342+
)
13121343

13131344
# Only connect technologies that are included in the finance stackup
13141345
for tech_name in tech_configs.keys():

h2integrate/core/test/test_framework.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,39 @@
1414
from h2integrate.core.inputs.validation import load_tech_yaml, load_plant_yaml, load_driver_yaml
1515

1616

17+
@pytest.mark.integration
18+
@pytest.mark.parametrize(
19+
"example_folder,resource_example_folder", [("17_splitter_wind_doc_h2", None)]
20+
)
21+
def test_use_commodity_stream_timeseries_finances_error(subtests, temp_copy_of_example):
22+
example_folder = temp_copy_of_example
23+
plant_config = load_plant_yaml(example_folder / "plant_config.yaml")
24+
driver_config = load_driver_yaml(example_folder / "driver_config.yaml")
25+
tech_config = load_tech_yaml(example_folder / "tech_config.yaml")
26+
27+
# Remove commodity_stream_output from finace subgroup
28+
plant_config["finance_parameters"]["finance_subgroups"]["electricity_doc"].pop(
29+
"commodity_stream_output"
30+
)
31+
top_level_config = {
32+
"plant_config": plant_config,
33+
"technology_config": tech_config,
34+
"driver_config": driver_config,
35+
}
36+
37+
with pytest.raises(ValueError) as excinfo:
38+
H2IntegrateModel(top_level_config)
39+
err = str(excinfo.value)
40+
with subtests.test("Commodity stream name is missing (commodity_stream_output is required)"):
41+
assert "`commodity_stream_output` is a required input" in err
42+
with subtests.test(
43+
"Commodity stream name is missing (use_commodity_stream_timeseries is True)"
44+
):
45+
assert "`use_commodity_stream_timeseries` is True" in err
46+
with subtests.test("Commodity stream name is missing (finance subgroup `electricity_doc`)"):
47+
assert "finance subgroup `electricity_doc`" in err
48+
49+
1750
@pytest.mark.integration
1851
@pytest.mark.parametrize("example_folder,resource_example_folder", [("01_onshore_steel_mn", None)])
1952
def test_check_tech_interconnections(subtests, temp_copy_of_example):

0 commit comments

Comments
 (0)