Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
- 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)
- 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)
- 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)
Expand Down
45 changes: 45 additions & 0 deletions docs/user_guide/specifying_finance_parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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)
13 changes: 13 additions & 0 deletions examples/17_splitter_wind_doc_h2/plant_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,21 @@ finance_parameters:
electricity:
commodity: electricity
technologies: [wind]
electricity_doc:
commodity: electricity
commodity_stream: electricity_splitter
use_commodity_stream_timeseries: true
commodity_stream_output: electricity_out1
technologies: [wind, doc]
electricity_electrolyzer:
commodity: electricity
commodity_stream: electricity_splitter
use_commodity_stream_timeseries: true
commodity_stream_output: electricity_out2
technologies: [wind, electrolyzer]
hydrogen:
commodity: hydrogen
commodity_stream: electrolyzer
technologies: [wind, electrolyzer]
co2:
commodity: co2
Expand Down
102 changes: 101 additions & 1 deletion examples/test/test_all_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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_output": "hydrogen_out",
"technologies": ["wind", "electrolyzer"],
}
}

new_finance_subgroup_wind = {
"electricity_ts": {
"commodity": "electricity",
"commodity_stream": "wind",
"use_commodity_stream_timeseries": True,
"commodity_stream_output": "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()
Expand Down Expand Up @@ -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(
Expand All @@ -696,6 +744,58 @@ 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(
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(
Expand Down
55 changes: 43 additions & 12 deletions h2integrate/core/h2integrate_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -798,7 +798,6 @@ def create_finance_model(self):
)
tech_names = subgroup_params.get("technologies")
commodity_stream = subgroup_params.get("commodity_stream", None)

if isinstance(finance_group_names, str):
finance_group_names = [finance_group_names]

Expand Down Expand Up @@ -839,9 +838,16 @@ 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_output": subgroup_params.get(
"commodity_stream_output", None
),
}
}
)

finance_subgroup = om.Group()

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

if finance_subgroups[subgroup_name]["use_commodity_stream_timeseries"]:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice error message!

if (
finance_subgroups[subgroup_name].get("commodity_stream_output", None)
is None
):
msg = (
"`commodity_stream_output` is a required input if "
f"`use_commodity_stream_timeseries` is True. Please add the "
f"`commodity_stream_output` for finance subgroup `{subgroup_name}`"
)
raise ValueError(msg)

adj_cf_comp = AdjustedCapacityFactorComp(
plant_config=filtered_plant_config,
commodity_type=commodity,
)
finance_subgroup.add_subsystem("adjusted_cf_comp", adj_cf_comp, promotes=["*"])

# create the finance component
fin_comp = fin_model(
driver_config=self.driver_config,
Expand Down Expand Up @@ -1298,17 +1322,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("use_commodity_stream_timeseries", False):
# TODO: finish this logic
self.plant.connect(
f"{commodity_stream}.{group_configs.get('commodity_stream_output')}",
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(
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",
)

# Only connect technologies that are included in the finance stackup
for tech_name in tech_configs.keys():
Expand Down
33 changes: 33 additions & 0 deletions h2integrate/core/test/test_framework.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,39 @@
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_output from finace subgroup
plant_config["finance_parameters"]["finance_subgroups"]["electricity_doc"].pop(
"commodity_stream_output"
)
top_level_config = {
"plant_config": plant_config,
"technology_config": tech_config,
"driver_config": driver_config,
}

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
Comment thread
jaredthomas68 marked this conversation as resolved.


@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):
Expand Down
Loading
Loading