Skip to content
1 change: 1 addition & 0 deletions docs/_toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ parts:
chapters:
- file: user_guide/model_overview
- file: user_guide/how_to_set_up_an_analysis
- file: user_guide/expected_naming_convention
- file: user_guide/connecting_technologies
- file: user_guide/defining_sites_and_resources
- file: user_guide/design_optimization_in_h2i
Expand Down
35 changes: 35 additions & 0 deletions docs/user_guide/expected_naming_convention.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Expected Technology Naming Convention in H2I

Some logic within H2I is relies on some expected naming convention of technologies in the technology configuration file. The following details the naming convention required for H2I to run properly.

```yaml
technologies:
tech_name_A: #these are the keys with specific required naming convention
performance_model:
model:
cost_model:
model:
model_inputs:
performance_parameters:
```

- if using the `GenericCombinerPerformanceModel` as the technology performance model, the technology name must include the name `combiner`. Some examples of valid technology names are:
+ 'combiner'
+ 'combiner_1'
+ 'electricity_combiner'
+ 'hydrogen_combiner_2'
- if using the `GenericSplitterPerformanceModel` as the technology performance model, the technology name must include the name `splitter`. Some examples of valid technology names are:
+ 'splitter'
+ 'splitter_1'
+ 'electricity_splitter'
+ 'hydrogen_splitter_2'
- if using the `FeedstockPerformanceModel` as the technology performance model and the `FeedstockCostModel` as the technology cost model, the technology name must include the name `feedstock`. Some examples of valid technology names are:
+ 'feedstock'
+ 'water_feedstock'
+ 'feedstock_iron_ore'
+ 'natural_gas_feedstock_tank'
- If a transport component is defined in the technology configuration file (most likely to transport commodities that cannot be transported by 'pipe' or 'cable'), the technology name must include the name `transport`. Some examples of valid technology names are:
+ 'transport'
+ 'lime_transport'
+ 'transport_iron_ore'
+ 'reformer_catalyst_transport_tube'
5 changes: 5 additions & 0 deletions examples/01_onshore_steel_mn/run_onshore_steel_mn.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import os
from pathlib import Path

import numpy as np

from h2integrate.core.h2integrate_model import H2IntegrateModel


os.chdir(Path(__file__).parent)

# Create a H2Integrate model
model = H2IntegrateModel("01_onshore_steel_mn.yaml")
# Set battery demand profile to electrolyzer capacity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ def setup(self):
for tech in tech_config:
self.add_input(f"capex_adjusted_{tech}", val=0.0, units="USD")
self.add_input(f"opex_adjusted_{tech}", val=0.0, units="USD/year")
# Below is required for all system-level finance models but is unused in this one
self.add_input(
f"replacement_schedule_{tech}", val=0.0, shape=plant_life, units="unitless"
)
self.add_input(f"varopex_adjusted_{tech}", val=0.0, shape=plant_life, units="USD/year")

# add plant life to the input config dictionary
finance_config = self.options["plant_config"]["finance_parameters"]["model_inputs"]
Expand Down
4 changes: 4 additions & 0 deletions h2integrate/core/feedstocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ def setup(self):
additional_cls_name=self.__class__.__name__,
)
n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"]
plant_life = int(self.options["plant_config"]["plant"]["plant_life"])

super().setup()

Expand All @@ -107,6 +108,9 @@ def setup(self):
desc=f"Consumption profile of {self.config.commodity}",
)

# lifetime estimate of item replacements, represented as a fraction of the capacity.
self.add_output("replacement_schedule", val=0.0, shape=plant_life, units="unitless")

def compute(self, inputs, outputs, discrete_inputs, discrete_outputs):
price = inputs["price"]
hourly_consumption = inputs[f"{self.config.commodity}_consumed"]
Expand Down
47 changes: 29 additions & 18 deletions h2integrate/core/h2integrate_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -766,6 +766,7 @@ def create_finance_model(self):
"tech_configs": tech_configs,
"commodity": commodity,
"commodity_stream": commodity_stream,
"system_finance_model": True,
}
}
)
Expand Down Expand Up @@ -836,6 +837,9 @@ def create_finance_model(self):
"adjusted_capex_opex_comp", adjusted_capex_opex_comp, promotes=["*"]
)

# Initialize counter to check if invalid combination of finance
# groups exist within a finance subgroup
n_tech_finances_in_group = 0
for finance_group_name in finance_group_names:
# check if using tech-specific finance model
if any(
Expand All @@ -848,11 +852,13 @@ def create_finance_model(self):

# this is created in create_technologies()
if tech_finance_group_name is not None:
n_tech_finances_in_group += 1
# tech specific finance models are created in create_technologies()
# and do not need to be included in the general finance models.
# set commodity_stream to None so that inputs needed for system-level
# finance models are not connected to tech-specific finance models.
finance_subgroups[subgroup_name].update({"commodity_stream": None})
# finance_subgroups[subgroup_name].update({"commodity_stream": None})
finance_subgroups[subgroup_name].update({"system_finance_model": False})
continue

# if not using a tech-specific finance group, get the finance model and inputs for
Expand Down Expand Up @@ -902,6 +908,17 @@ def create_finance_model(self):
if k in self.plant_config["finance_parameters"]["finance_groups"]
]

if n_tech_finances_in_group > 0 and len(non_tech_finances) > 0:
tech_finances = [
k for k in finance_group_names if k not in non_tech_finances
]
msg = (
f"Cannot run a tech-specific finance model ({tech_finances}) in the "
f"same finance subgroup as a system-level finance model "
f"({non_tech_finances}). Please modify the finance_groups in finance "
f"subgroup {subgroup_name}."
)
raise ValueError(msg)
# if multiple non-tech specific finance model groups are specified for the
# subgroup, the outputs of the finance model must have unique names to
# avoid errors.
Expand Down Expand Up @@ -998,12 +1015,6 @@ def connect_technologies(self):
f"{connection_name}.{transport_item}_in",
)

elif "storage" in source_tech:
# Connect the source technology to the connection component
self.plant.connect(
f"{source_tech}.{transport_item}_out",
f"{connection_name}.{transport_item}_in",
)
else:
# Connect the source technology to the connection component
self.plant.connect(
Expand Down Expand Up @@ -1035,13 +1046,6 @@ def connect_technologies(self):
f"{dest_tech}.{transport_item}_capacity_factor{combiner_counts[dest_tech]}",
)

elif "storage" in dest_tech:
# Connect the connection component to the destination technology
self.plant.connect(
f"{connection_name}.{transport_item}_out",
f"{dest_tech}.{transport_item}_in",
)

else:
# Connect the connection component to the destination technology
self.plant.connect(
Expand Down Expand Up @@ -1126,7 +1130,11 @@ def connect_technologies(self):
tech_configs = group_configs.get("tech_configs")
primary_commodity_type = group_configs.get("commodity")
commodity_stream = group_configs.get("commodity_stream")
if commodity_stream is not None:
is_system_finance_model = group_configs.get("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",
Expand All @@ -1142,6 +1150,8 @@ def connect_technologies(self):
# For now, assume splitters and combiners do not add any costs
if "splitter" in tech_name or "combiner" in tech_name:
continue
if tech_name == "cable" or tech_name == "pipe":
continue

self.plant.connect(
f"{tech_name}.CapEx",
Expand All @@ -1158,10 +1168,11 @@ def connect_technologies(self):
f"finance_subgroup_{group_id}.cost_year_{tech_name}",
)

if "electrolyzer" in tech_name:
if is_system_finance_model and "transport" not in tech_name:
# connect replacement schedule to system-level finance models
self.plant.connect(
f"{tech_name}.time_until_replacement",
f"finance_subgroup_{group_id}.{tech_name}_time_until_replacement",
f"{tech_name}.replacement_schedule",
f"finance_subgroup_{group_id}.replacement_schedule_{tech_name}",
)

self.plant.options["auto_order"] = True
Expand Down
38 changes: 38 additions & 0 deletions h2integrate/core/test/test_framework.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,3 +395,41 @@ def test_reports_turned_off(temp_dir):
assert (
len(report_dirs) == 0
), f"Report directories were created despite create_om_reports=False: {report_dirs}"


@pytest.mark.unit
def test_invalid_finance_group_combination(subtests):
driver_config = load_driver_yaml(EXAMPLE_DIR / "01_onshore_steel_mn" / "driver_config.yaml")
tech_config = load_tech_yaml(EXAMPLE_DIR / "01_onshore_steel_mn" / "tech_config.yaml")
plant_config = load_plant_yaml(EXAMPLE_DIR / "01_onshore_steel_mn" / "plant_config.yaml")

invalid_finance_subgroup = {
"commodity": "steel",
"finance_groups": ["steel", "profast_model"],
"technologies": ["steel"],
}

plant_config["finance_parameters"]["finance_subgroups"].update(
{"steel_buggy": invalid_finance_subgroup}
)

h2i_config = {
"name": "H2I",
"system_summary": "",
"driver_config": driver_config,
"technology_config": tech_config,
"plant_config": plant_config,
}

with subtests.test("Test invalid finance groups"):
expected_msg = (
"Cannot run a tech-specific finance model (['steel']) in the "
"same finance subgroup as a system-level finance model "
"(['profast_model']). Please modify the finance_groups in finance "
"subgroup steel_buggy."
)

with pytest.raises(ValueError) as excinfo:
h2i = H2IntegrateModel(h2i_config)
h2i.setup()
assert expected_msg == str(excinfo.value)
27 changes: 13 additions & 14 deletions h2integrate/finances/numpy_financial_npv.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,9 @@ def setup(self):
shape=plant_config["plant"]["plant_life"],
units="USD/year",
)

# TODO: update below with standardized naming
if "electrolyzer" in tech_config:
self.add_input("electrolyzer_time_until_replacement", units="h")
self.add_input(
f"replacement_schedule_{tech}", val=0.0, shape=plant_life, units="unitless"
)

self.add_input(
f"sell_price_{self.output_txt}",
Expand Down Expand Up @@ -238,20 +237,20 @@ def compute(self, inputs, outputs):

# Find refurbishment period either from explicit config or calculated from hours
if "refurbishment_period_years" in tech_capex_info:
refurb_schedule = np.zeros(self.params.plant_life)
refurb_period = tech_capex_info["refurbishment_period_years"]
# Set replacement cost at regular intervals (every refurb_period years)
# replacement_cost_percent is fraction of original CAPEX (e.g., 0.15 = 15%)
refurb_schedule[refurb_period : self.params.plant_life : refurb_period] = (
tech_capex_info["replacement_cost_percent"]
)
else:
# Calculate from hours until replacement (e.g., electrolyzer lifetime hours)
# Convert hours to years: divide by (24 hours/day * 365 days/year)
refurb_period = round(
float(inputs[f"{tech}_time_until_replacement"][0]) / (24 * 365)
# Multiply the technology replacement schedule by the replacement cost
refurb_schedule = (
inputs[f"replacement_schedule_{tech}"]
* tech_capex_info["replacement_cost_percent"]
)

# Set replacement cost at regular intervals (every refurb_period years)
# replacement_cost_percent is fraction of original CAPEX (e.g., 0.15 = 15%)
refurb_schedule[refurb_period : self.config.plant_life : refurb_period] = (
tech_capex_info["replacement_cost_percent"]
)

# Calculate actual replacement costs by multiplying CAPEX by schedule percentages
# capex is negative, so refurb_cost will also be negative (cash outflow)
refurb_cost = capex * refurb_schedule
Expand Down
36 changes: 22 additions & 14 deletions h2integrate/finances/profast_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -473,10 +473,14 @@ class ProFastBase(om.ExplicitComponent):
user-defined technology, in USD.
opex_adjusted_{tech} (float): Adjusted operational expenditure for each
user-defined technology, in USD/year.
total_{commodity}_produced (float): Total annual production of the selected commodity
varopex_adjusted_{tech} (np.ndarray): Adjusted variable operational expenditure
for each user-defined technology, in USD/year.
rated_{commodity}_production (float): Rated production of the selected commodity
(units depend on commodity type).
{tech}_time_until_replacement (float): Time until technology is replaced, in hours
(currently only supported if "electrolyzer" is in tech_config).
capacity_factor (np.ndarray): Capacity factor of the commodity producing tech(s)
per year of the plant life.
replacement_schedule_{tech} (np.ndarray): Fraction of the technology capacity that
is replaced in each year of the plant life.


Methods:
Expand Down Expand Up @@ -547,10 +551,9 @@ def setup(self):
self.add_input(f"capex_adjusted_{tech}", val=0.0, units="USD")
self.add_input(f"opex_adjusted_{tech}", val=0.0, units="USD/year")
self.add_input(f"varopex_adjusted_{tech}", val=0.0, shape=plant_life, units="USD/year")

# Include electrolyzer replacement time if applicable
if tech.startswith("electrolyzer"):
self.add_input(f"{tech}_time_until_replacement", units="h")
self.add_input(
f"replacement_schedule_{tech}", val=0.0, shape=plant_life, units="unitless"
)

# Load plant configuration and financial parameters
plant_config = self.options["plant_config"]
Expand Down Expand Up @@ -666,18 +669,23 @@ def populate_profast(self, inputs):

# see if any refurbishment information was input
if "replacement_cost_percent" in tech_capex_info:
refurb_schedule = np.zeros(self.params.plant_life)

if "refurbishment_period_years" in tech_capex_info:
# Calculate replacement schedule using a user-defined replacement period
refurb_schedule = np.zeros(self.params.plant_life)
refurb_period = tech_capex_info["refurbishment_period_years"]
# Multiply the replacement schedule by the replacement cost
# replacement_cost_percent is fraction of original CAPEX (e.g., 0.15 = 15%)
refurb_schedule[refurb_period : self.params.plant_life : refurb_period] = (
tech_capex_info["replacement_cost_percent"]
)

else:
refurb_period = round(
float(inputs[f"{tech}_time_until_replacement"][0]) / (24 * 365)
# Use the replacement schedule from the technology performance model
refurb_schedule = (
inputs[f"replacement_schedule_{tech}"]
* tech_capex_info["replacement_cost_percent"]
)

refurb_schedule[refurb_period : self.params.plant_life : refurb_period] = (
tech_capex_info["replacement_cost_percent"]
)
# add refurbishment schedule to tech-specific capital item entry
tech_capex_info["refurb"] = list(refurb_schedule)

Expand Down
Loading