Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
cdf5f67
added capacity and price as inputs to feedstock component
elenya-grant Feb 10, 2026
de5ef00
updated feedstock mdoel to have standard outputs
elenya-grant Feb 11, 2026
2066e16
added commodity_amount_units to FeedstockCostConfig
elenya-grant Feb 11, 2026
3876418
added standard output to feedstock cost model and connected commodity…
elenya-grant Feb 11, 2026
dd16235
renamed config inputs for feedstocks
elenya-grant Feb 11, 2026
78506c1
updated ex 23 which is untested and cleaned up feedstocks.py
elenya-grant Feb 11, 2026
791e463
minor docstring updates and added comments
elenya-grant Feb 11, 2026
a1e94fa
added in notes of questions for reviewers to feedstocks.py
elenya-grant Feb 11, 2026
a04c309
Merge branch 'develop' into feedstock_updates
johnjasa Feb 11, 2026
4747d2f
updated MMBtu units to MMBtu/h
elenya-grant Feb 12, 2026
5744889
Merge branch 'feedstock_updates' of github.com:elenya-grant/GreenHEAR…
elenya-grant Feb 12, 2026
efd9044
Merge remote-tracking branch 'h2i_upstream/develop' into feedstock_up…
elenya-grant Feb 12, 2026
a42b637
fixed iron and steel tests
elenya-grant Feb 12, 2026
0944851
Merge remote-tracking branch 'h2i_upstream/develop' into feedstock_up…
elenya-grant Feb 13, 2026
d823344
Merge branch 'develop' into feedstock_updates
johnjasa Feb 13, 2026
ab5a2cf
Merge remote-tracking branch 'h2i_upstream/develop' into feedstock_up…
elenya-grant Feb 13, 2026
13c7dba
Merge branch 'feedstock_updates' of github.com:elenya-grant/GreenHEAR…
elenya-grant Feb 13, 2026
015c288
made changes to feedstock
elenya-grant Feb 13, 2026
d0da77b
Merge branch 'develop' into feedstock_updates
johnjasa Feb 16, 2026
282b533
merged in develop
elenya-grant Apr 6, 2026
9b9c10c
fixed tests
elenya-grant Apr 6, 2026
1bc915e
Merge branch 'feedstock_updates' of github.com:elenya-grant/GreenHEAR…
elenya-grant Apr 6, 2026
2e3c34f
removed unused comment
elenya-grant Apr 6, 2026
706d856
removed shape from price input
elenya-grant Apr 6, 2026
845db9a
Merge branch 'develop' into feedstock_updates
johnjasa Apr 7, 2026
1c1842a
added integration test for feedstock integrated with finance model
elenya-grant Apr 7, 2026
c9bf5f7
updated feedstocks.md
elenya-grant Apr 7, 2026
c7228c8
Merge branch 'develop' into feedstock_updates
johnjasa Apr 8, 2026
cb74b77
Merge branch 'develop' into feedstock_updates
johnjasa Apr 9, 2026
7d316b1
changed based on reviewer feedback
elenya-grant Apr 9, 2026
630e527
merged changelog
elenya-grant Apr 9, 2026
10b32b4
Merge branch 'feedstock_updates' of github.com:elenya-grant/GreenHEAR…
elenya-grant Apr 9, 2026
1637abb
added subtests to test_feedstocks
elenya-grant Apr 9, 2026
8fc7fee
Merge branch 'develop' into feedstock_updates
johnjasa Apr 10, 2026
3e5b254
small changes to feedstock doc
kbrunik Apr 10, 2026
c0615d6
docs
kbrunik Apr 10, 2026
e064d54
Merge branch 'develop' into feedstock_updates
kbrunik Apr 10, 2026
7d0ac16
Merge branch 'develop' into feedstock_updates
johnjasa Apr 11, 2026
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 @@ -46,6 +46,7 @@
data between functions in a module. [PR 590](https://github.com/NatLabRockies/H2Integrate/pull/590)
- Adds `H2IntegrateModel.state` as an `IntEnum` to handle setup and run status checks.
[PR 590](https://github.com/NatLabRockies/H2Integrate/pull/590)
- Added standardized outputs to feedstock model [PR 523](https://github.com/NatLabRockies/H2Integrate/pull/523)
- Reclassified open-loop converter control strategies as demand components and updated output naming convention to align with output naming convention in storage performance models [PR 631](https://github.com/NatLabRockies/H2Integrate/pull/631).
- The `FlexibleDemandOpenLoopConverterController` has been renamed to `FlexibleDemandComponent`
- The `DemandOpenLoopConverterController` has been renamed to `GenericDemandComponent`
Expand Down
15 changes: 12 additions & 3 deletions docs/technology_models/feedstocks.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Feedstock Models

Feedstock models in H2Integrate represent any resource input that is consumed by technologies in your plant, such as natural gas, water, electricity from the grid, or any other material input.
Feedstock models in H2Integrate represent any resource input that is consumed by technologies in your plant that comes from outside your designed system boundary (and not generated internally), such as natural gas, water, electricity from the grid, or any other material input.
The feedstock modeling approach provides a flexible way to track resource consumption and calculate associated costs for any type of input material or energy source.
Please see the example `16_natural_gas` in the `examples` directory for a complete setup using natural gas as a feedstock.

Expand All @@ -19,6 +19,7 @@ Each feedstock type requires two model components:
- Calculates consumption costs based on actual usage
- Takes `{commodity}_consumed` as input
- Located after all consuming technologies in the chain
- Calculates the capacity factor of the consumed feedstock

### Technology Interconnections

Expand Down Expand Up @@ -56,8 +57,8 @@ ng_feedstock:
commodity_amount_units: "MMBtu" # optional, if not specified defaults to `commodity_rate_units*h`
cost_year: 2023
price: 4.2 # cost in USD/commodity_amount_units
annual_cost: 0.
start_up_cost: 100000.
annual_cost: 0. #cost in USD/year
start_up_cost: 100000. #cost in USD
```

### Performance Model Parameters
Expand All @@ -81,3 +82,11 @@ ng_feedstock:
```{tip}
The `price` parameter is flexible - you can specify constant pricing with a single value or time-varying pricing with an array of values matching the number of simulation timesteps.
```

### Consumed Feedstock Outputs
The feedstock model outputs cost and performance information about the consumed feedstock. The most notable outputs are:
- `VarOpEx`: cost of the feedstock consumed (in `USD/yr`)
- `total_{commodity}_consumed`: total feedstock consumed over simulation (in `commodity_amount_units`)
- `annual_{commodity}_consumed`: annual feedstock consumed (in `commodity_amount_units/yr`)
- `rated_{commodity}_production`: this is equal to the the `rated_capacity` of the feedstock model (in `commodity_rate_units`)
- `capacity_factor`: ratio of the feedstock consumed to the maximum feedstock available
5 changes: 4 additions & 1 deletion examples/12_ammonia_synloop/plant_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,8 @@ finance_parameters:
- battery
- electrolyzer
- h2_storage
- n2_feedstock
- ammonia
n2:
commodity: nitrogen
commodity_stream: n2_feedstock
technologies: [n2_feedstock]
2 changes: 1 addition & 1 deletion examples/12_ammonia_synloop/tech_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ technologies:
rated_capacity: 50.0 # metric tonnes of N2/hour
cost_parameters:
cost_year: 2022
price: 0.0
price: 5.0
annual_cost: 0.
start_up_cost: 0.0
electricity_feedstock:
Expand Down
7 changes: 4 additions & 3 deletions examples/16_natural_gas/tech_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,10 @@ technologies:
rated_capacity: 750. # MMBtu/h
cost_parameters:
cost_year: 2023
price: 4.2
annual_cost: 0.
start_up_cost: 100000.
commodity_amount_units: MMBtu
price: 4.2 # USD/commodity_amount_units
annual_cost: 0. # USD
start_up_cost: 100000. # USD
natural_gas_plant:
performance_model:
model: NaturalGasPerformanceModel
Expand Down
11 changes: 11 additions & 0 deletions examples/test/test_all_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,13 @@ def test_ammonia_synloop_example(subtests, temp_copy_of_example):
)
== 1.1018637096646757
)
with subtests.test("Check LCON"):
assert (
pytest.approx(
model.prob.get_val("finance_subgroup_n2.LCON", units="USD/t")[0], rel=1e-6
)
== 5.03140888
)


@pytest.mark.integration
Expand Down Expand Up @@ -1058,6 +1065,10 @@ def test_natural_gas_example(subtests, temp_copy_of_example):
expected_opex = 4.2 * ng_consumed.sum() # price = 4.2 $/MMBtu
assert pytest.approx(ng_opex, rel=1e-6) == expected_opex

with subtests.test("Check feedstock capacity factor"):
ng_cf = model.prob.get_val("ng_feedstock.capacity_factor", units="unitless").mean()
assert pytest.approx(ng_cf, rel=1e-6) == 0.5676562763739097


@pytest.mark.integration
@pytest.mark.parametrize(
Expand Down
2 changes: 1 addition & 1 deletion h2integrate/converters/natural_gas/natural_gas_cc_ct.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def setup(self):
self.add_input(
"heat_rate_mmbtu_per_mwh",
val=self.config.heat_rate_mmbtu_per_mwh,
units="MMBtu/MW/h",
units="MMBtu/(MW*h)",
desc="Plant heat rate in MMBtu/MWh",
)

Expand Down
75 changes: 71 additions & 4 deletions h2integrate/core/feedstocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ class FeedstockCostConfig(CostModelBaseConfig):
price: int | float | list = field()
annual_cost: float = field(default=0.0)
start_up_cost: float = field(default=0.0)

commodity_amount_units: str | None = field(default=None)

def __attrs_post_init__(self):
Expand All @@ -89,29 +88,97 @@ def setup(self):
merge_shared_inputs(self.options["tech_config"]["model_inputs"], "cost"),
additional_cls_name=self.__class__.__name__,
)
n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"]
self.n_timesteps = int(self.options["plant_config"]["plant"]["simulation"]["n_timesteps"])
plant_life = int(self.options["plant_config"]["plant"]["plant_life"])

# Set cost outputs
super().setup()

self.add_input(
f"{self.config.commodity}_consumed",
val=0.0,
shape=int(n_timesteps),
shape=self.n_timesteps,
units=self.config.commodity_rate_units,
desc=f"Consumption profile of {self.config.commodity}",
)
self.add_input(
f"{self.config.commodity}_out",
val=0,
shape=self.n_timesteps,
units=self.config.commodity_rate_units,
)

self.add_input(
"price",
val=self.config.price,
units=f"USD/({self.config.commodity_amount_units})",
desc=f"Consumption profile of {self.config.commodity}",
desc=f"Price profile of {self.config.commodity}",
)

self.dt = self.options["plant_config"]["plant"]["simulation"]["dt"]
self.plant_life = int(self.options["plant_config"]["plant"]["plant_life"])
hours_per_year = 8760
hours_simulated = (self.dt / 3600) * self.n_timesteps
self.fraction_of_year_simulated = hours_simulated / hours_per_year
# since feedstocks are consumed, some outputs are appended
# with 'consumed' rather than 'produced'

self.add_output(
f"total_{self.config.commodity}_consumed",
val=0.0,
units=self.config.commodity_amount_units,
)

self.add_output(
f"annual_{self.config.commodity}_consumed",
val=0.0,
shape=self.plant_life,
units=f"({self.config.commodity_amount_units})/year",
)

# Capacity factor is feedstock_consumed/max_feedstock_available
self.add_output(
"capacity_factor",
val=0.0,
shape=self.plant_life,
units="unitless",
desc="Capacity factor",
)

# The should be equal to the commodity_capacity input of the FeedstockPerformanceModel
self.add_output(
f"rated_{self.config.commodity}_production",
val=0,
units=self.config.commodity_rate_units,
)

# 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):
# Capacity factor is the total amount consumed / the total amount available
outputs["capacity_factor"] = (
inputs[f"{self.config.commodity}_consumed"].sum()
/ inputs[f"{self.config.commodity}_out"].sum()
)

# Sum the amount consumed
outputs[f"total_{self.config.commodity}_consumed"] = inputs[
f"{self.config.commodity}_consumed"
].sum() * (self.dt / 3600)

# Estimate annual consumption based on consumption over the simulation
# NOTE: once we standardize feedstock consumption outputs in models, this should
# be updated to handle consumption that varies over years of operation
outputs[f"annual_{self.config.commodity}_consumed"] = outputs[
f"total_{self.config.commodity}_consumed"
] * (1 / self.fraction_of_year_simulated)

outputs[f"rated_{self.config.commodity}_production"] = inputs[
f"{self.config.commodity}_out"
].max()

# Calculate costs
price = inputs["price"]
hourly_consumption = inputs[f"{self.config.commodity}_consumed"]
cost_per_year = sum(price * hourly_consumption)
Expand Down
5 changes: 5 additions & 0 deletions h2integrate/core/h2integrate_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -1053,6 +1053,11 @@ def connect_technologies(self):
f"{dest_tech}.{transport_item}_consumed",
f"{source_tech}.{transport_item}_consumed",
)
# Connect the feedstock performance model output to the cost model input
self.plant.connect(
f"{source_tech}_source.{transport_item}_out",
f"{source_tech}.{transport_item}_out",
)

if perf_model_name == "FeedstockPerformanceModel":
source_tech = f"{source_tech}_source"
Expand Down
82 changes: 81 additions & 1 deletion h2integrate/core/test/test_feedstocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,90 @@
import numpy as np
import pytest
import openmdao.api as om
from pytest import fixture

from h2integrate.core.feedstocks import FeedstockCostModel, FeedstockPerformanceModel


@fixture
def plant_config():
return {
"plant": {
"plant_life": 30,
"simulation": {
"n_timesteps": 8760,
"dt": 3600,
},
},
}


@fixture
def ng_feedstock_input_config():
tech_config = {
"model_inputs": {
"shared_parameters": {
"commodity": "natural_gas",
"commodity_rate_units": "MMBtu/h",
},
"performance_parameters": {
"rated_capacity": 100.0,
},
"cost_parameters": {
"price": 4.2, # USD/MMBtu
"annual_cost": 0,
"start_up_cost": 0,
"cost_year": 2023,
"commodity_amount_units": "MMBtu", # optional
},
}
}
return tech_config


@pytest.mark.unit
def test_feedstock_standard_outputs(plant_config, ng_feedstock_input_config, subtests):
perf_model = FeedstockPerformanceModel(
plant_config=plant_config, tech_config=ng_feedstock_input_config, driver_config={}
)
cost_model = FeedstockCostModel(
plant_config=plant_config, tech_config=ng_feedstock_input_config, driver_config={}
)
prob = om.Problem()
prob.model.add_subsystem("ng_feedstock_source", perf_model)
prob.model.add_subsystem("ng_feedstock", cost_model)
# Connect the feedstock performance model output to the cost model input
prob.model.connect(
"ng_feedstock_source.natural_gas_out",
"ng_feedstock.natural_gas_out",
)

prob.setup()
# Set some consumption values
consumption = np.full(8760, 50.0) # 50 MMBtu/hour
prob.set_val("ng_feedstock.natural_gas_consumed", consumption)
prob.run_model()
with subtests.test("Check feedstock capacity factor"):
ng_cf = prob.get_val("ng_feedstock.capacity_factor", units="unitless").mean()
assert pytest.approx(ng_cf, rel=1e-6) == 0.5
with subtests.test("Check feedstock rated production"):
rated_production_source = prob.get_val(
"ng_feedstock_source.natural_gas_capacity", units="MMBtu/h"
)
rated_production = prob.get_val(
"ng_feedstock.rated_natural_gas_production", units="MMBtu/h"
)
assert pytest.approx(rated_production, rel=1e-6) == rated_production_source
with subtests.test("Check feedstock total consumption"):
total_consumption = prob.get_val("ng_feedstock.total_natural_gas_consumed", units="MMBtu")
assert pytest.approx(total_consumption, rel=1e-6) == consumption.sum()
with subtests.test("Check feedstock annual consumption"):
annual_consumption = prob.get_val(
"ng_feedstock.annual_natural_gas_consumed", units="MMBtu/yr"
)
assert pytest.approx(annual_consumption, rel=1e-6) == consumption.sum()


def create_basic_feedstock_config(
feedstock_type="natural_gas",
units="MMBtu/h",
Expand All @@ -38,7 +118,7 @@ def create_basic_feedstock_config(
},
}
}
plant_config = {"plant": {"plant_life": 30, "simulation": {"n_timesteps": 8760}}}
plant_config = {"plant": {"plant_life": 30, "simulation": {"n_timesteps": 8760, "dt": 3600}}}
driver_config = {}
return tech_config, plant_config, driver_config

Expand Down
15 changes: 10 additions & 5 deletions h2integrate/tools/profast_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,16 @@ def make_price_breakdown(price_breakdown, pf_config):
total_price_capex += cash_outflow_prices.loc[
cash_outflow_prices["Name"] == item, "NPV"
].tolist()[0]
for item in capital_items:
capex_fraction[item] = (
cash_outflow_prices.loc[cash_outflow_prices["Name"] == item, "NPV"].tolist()[0]
/ total_price_capex
)
if total_price_capex != 0:
for item in capital_items:
capex_fraction[item] = (
cash_outflow_prices.loc[cash_outflow_prices["Name"] == item, "NPV"].tolist()[0]
/ total_price_capex
)
else:
for item in capital_items:
capex_fraction[item] = 0

cap_expense = (
price_breakdown.loc[price_breakdown["Name"] == "Repayment of debt", "NPV"].tolist()[0]
+ price_breakdown.loc[price_breakdown["Name"] == "Interest expense", "NPV"].tolist()[0]
Expand Down
Loading