Skip to content

Commit 1caa9f6

Browse files
elenya-grantjohnjasakbrunik
authored
Standardize Feedstock Outputs (follow-on to 463) (#523)
* added capacity and price as inputs to feedstock component * updated feedstock mdoel to have standard outputs * added commodity_amount_units to FeedstockCostConfig * added standard output to feedstock cost model and connected commodity_out between performance and cost model * renamed config inputs for feedstocks * updated ex 23 which is untested and cleaned up feedstocks.py * minor docstring updates and added comments * added in notes of questions for reviewers to feedstocks.py * updated MMBtu units to MMBtu/h * fixed iron and steel tests * made changes to feedstock * fixed tests * removed unused comment * removed shape from price input * added integration test for feedstock integrated with finance model * updated feedstocks.md * changed based on reviewer feedback * added subtests to test_feedstocks * small changes to feedstock doc * docs --------- Co-authored-by: John Jasa <johnjasa11@gmail.com> Co-authored-by: kbrunik <kbrunik@gmail.com> Co-authored-by: kbrunik <102193481+kbrunik@users.noreply.github.com>
1 parent 9892c55 commit 1caa9f6

11 files changed

Lines changed: 201 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
data between functions in a module. [PR 590](https://github.com/NatLabRockies/H2Integrate/pull/590)
4747
- Adds `H2IntegrateModel.state` as an `IntEnum` to handle setup and run status checks.
4848
[PR 590](https://github.com/NatLabRockies/H2Integrate/pull/590)
49+
- Added standardized outputs to feedstock model [PR 523](https://github.com/NatLabRockies/H2Integrate/pull/523)
4950
- 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).
5051
- The `FlexibleDemandOpenLoopConverterController` has been renamed to `FlexibleDemandComponent`
5152
- The `DemandOpenLoopConverterController` has been renamed to `GenericDemandComponent`

docs/technology_models/feedstocks.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Feedstock Models
22

3-
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.
3+
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.
44
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.
55
Please see the example `16_natural_gas` in the `examples` directory for a complete setup using natural gas as a feedstock.
66

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

2324
### Technology Interconnections
2425

@@ -56,8 +57,8 @@ ng_feedstock:
5657
commodity_amount_units: "MMBtu" # optional, if not specified defaults to `commodity_rate_units*h`
5758
cost_year: 2023
5859
price: 4.2 # cost in USD/commodity_amount_units
59-
annual_cost: 0.
60-
start_up_cost: 100000.
60+
annual_cost: 0. #cost in USD/year
61+
start_up_cost: 100000. #cost in USD
6162
```
6263
6364
### Performance Model Parameters
@@ -81,3 +82,11 @@ ng_feedstock:
8182
```{tip}
8283
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.
8384
```
85+
86+
### Consumed Feedstock Outputs
87+
The feedstock model outputs cost and performance information about the consumed feedstock. The most notable outputs are:
88+
- `VarOpEx`: cost of the feedstock consumed (in `USD/yr`)
89+
- `total_{commodity}_consumed`: total feedstock consumed over simulation (in `commodity_amount_units`)
90+
- `annual_{commodity}_consumed`: annual feedstock consumed (in `commodity_amount_units/yr`)
91+
- `rated_{commodity}_production`: this is equal to the the `rated_capacity` of the feedstock model (in `commodity_rate_units`)
92+
- `capacity_factor`: ratio of the feedstock consumed to the maximum feedstock available

examples/12_ammonia_synloop/plant_config.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,5 +74,8 @@ finance_parameters:
7474
- battery
7575
- electrolyzer
7676
- h2_storage
77-
- n2_feedstock
7877
- ammonia
78+
n2:
79+
commodity: nitrogen
80+
commodity_stream: n2_feedstock
81+
technologies: [n2_feedstock]

examples/12_ammonia_synloop/tech_config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ technologies:
149149
rated_capacity: 50.0 # metric tonnes of N2/hour
150150
cost_parameters:
151151
cost_year: 2022
152-
price: 0.0
152+
price: 5.0
153153
annual_cost: 0.
154154
start_up_cost: 0.0
155155
electricity_feedstock:

examples/16_natural_gas/tech_config.yaml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,10 @@ technologies:
6767
rated_capacity: 750. # MMBtu/h
6868
cost_parameters:
6969
cost_year: 2023
70-
price: 4.2
71-
annual_cost: 0.
72-
start_up_cost: 100000.
70+
commodity_amount_units: MMBtu
71+
price: 4.2 # USD/commodity_amount_units
72+
annual_cost: 0. # USD
73+
start_up_cost: 100000. # USD
7374
natural_gas_plant:
7475
performance_model:
7576
model: NaturalGasPerformanceModel

examples/test/test_all_examples.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,13 @@ def test_ammonia_synloop_example(subtests, temp_copy_of_example):
375375
)
376376
== 1.1018637096646757
377377
)
378+
with subtests.test("Check LCON"):
379+
assert (
380+
pytest.approx(
381+
model.prob.get_val("finance_subgroup_n2.LCON", units="USD/t")[0], rel=1e-6
382+
)
383+
== 5.03140888
384+
)
378385

379386

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

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

10621073
@pytest.mark.integration
10631074
@pytest.mark.parametrize(

h2integrate/converters/natural_gas/natural_gas_cc_ct.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ def setup(self):
8484
self.add_input(
8585
"heat_rate_mmbtu_per_mwh",
8686
val=self.config.heat_rate_mmbtu_per_mwh,
87-
units="MMBtu/MW/h",
87+
units="MMBtu/(MW*h)",
8888
desc="Plant heat rate in MMBtu/MWh",
8989
)
9090

h2integrate/core/feedstocks.py

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ class FeedstockCostConfig(CostModelBaseConfig):
7575
price: int | float | list = field()
7676
annual_cost: float = field(default=0.0)
7777
start_up_cost: float = field(default=0.0)
78-
7978
commodity_amount_units: str | None = field(default=None)
8079

8180
def __attrs_post_init__(self):
@@ -89,29 +88,97 @@ def setup(self):
8988
merge_shared_inputs(self.options["tech_config"]["model_inputs"], "cost"),
9089
additional_cls_name=self.__class__.__name__,
9190
)
92-
n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"]
91+
self.n_timesteps = int(self.options["plant_config"]["plant"]["simulation"]["n_timesteps"])
9392
plant_life = int(self.options["plant_config"]["plant"]["plant_life"])
9493

94+
# Set cost outputs
9595
super().setup()
9696

9797
self.add_input(
9898
f"{self.config.commodity}_consumed",
9999
val=0.0,
100-
shape=int(n_timesteps),
100+
shape=self.n_timesteps,
101101
units=self.config.commodity_rate_units,
102102
desc=f"Consumption profile of {self.config.commodity}",
103103
)
104+
self.add_input(
105+
f"{self.config.commodity}_out",
106+
val=0,
107+
shape=self.n_timesteps,
108+
units=self.config.commodity_rate_units,
109+
)
110+
104111
self.add_input(
105112
"price",
106113
val=self.config.price,
107114
units=f"USD/({self.config.commodity_amount_units})",
108-
desc=f"Consumption profile of {self.config.commodity}",
115+
desc=f"Price profile of {self.config.commodity}",
116+
)
117+
118+
self.dt = self.options["plant_config"]["plant"]["simulation"]["dt"]
119+
self.plant_life = int(self.options["plant_config"]["plant"]["plant_life"])
120+
hours_per_year = 8760
121+
hours_simulated = (self.dt / 3600) * self.n_timesteps
122+
self.fraction_of_year_simulated = hours_simulated / hours_per_year
123+
# since feedstocks are consumed, some outputs are appended
124+
# with 'consumed' rather than 'produced'
125+
126+
self.add_output(
127+
f"total_{self.config.commodity}_consumed",
128+
val=0.0,
129+
units=self.config.commodity_amount_units,
130+
)
131+
132+
self.add_output(
133+
f"annual_{self.config.commodity}_consumed",
134+
val=0.0,
135+
shape=self.plant_life,
136+
units=f"({self.config.commodity_amount_units})/year",
137+
)
138+
139+
# Capacity factor is feedstock_consumed/max_feedstock_available
140+
self.add_output(
141+
"capacity_factor",
142+
val=0.0,
143+
shape=self.plant_life,
144+
units="unitless",
145+
desc="Capacity factor",
146+
)
147+
148+
# The should be equal to the commodity_capacity input of the FeedstockPerformanceModel
149+
self.add_output(
150+
f"rated_{self.config.commodity}_production",
151+
val=0,
152+
units=self.config.commodity_rate_units,
109153
)
110154

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

114158
def compute(self, inputs, outputs, discrete_inputs, discrete_outputs):
159+
# Capacity factor is the total amount consumed / the total amount available
160+
outputs["capacity_factor"] = (
161+
inputs[f"{self.config.commodity}_consumed"].sum()
162+
/ inputs[f"{self.config.commodity}_out"].sum()
163+
)
164+
165+
# Sum the amount consumed
166+
outputs[f"total_{self.config.commodity}_consumed"] = inputs[
167+
f"{self.config.commodity}_consumed"
168+
].sum() * (self.dt / 3600)
169+
170+
# Estimate annual consumption based on consumption over the simulation
171+
# NOTE: once we standardize feedstock consumption outputs in models, this should
172+
# be updated to handle consumption that varies over years of operation
173+
outputs[f"annual_{self.config.commodity}_consumed"] = outputs[
174+
f"total_{self.config.commodity}_consumed"
175+
] * (1 / self.fraction_of_year_simulated)
176+
177+
outputs[f"rated_{self.config.commodity}_production"] = inputs[
178+
f"{self.config.commodity}_out"
179+
].max()
180+
181+
# Calculate costs
115182
price = inputs["price"]
116183
hourly_consumption = inputs[f"{self.config.commodity}_consumed"]
117184
cost_per_year = sum(price * hourly_consumption)

h2integrate/core/h2integrate_model.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1053,6 +1053,11 @@ def connect_technologies(self):
10531053
f"{dest_tech}.{transport_item}_consumed",
10541054
f"{source_tech}.{transport_item}_consumed",
10551055
)
1056+
# Connect the feedstock performance model output to the cost model input
1057+
self.plant.connect(
1058+
f"{source_tech}_source.{transport_item}_out",
1059+
f"{source_tech}.{transport_item}_out",
1060+
)
10561061

10571062
if perf_model_name == "FeedstockPerformanceModel":
10581063
source_tech = f"{source_tech}_source"

h2integrate/core/test/test_feedstocks.py

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,90 @@
88
import numpy as np
99
import pytest
1010
import openmdao.api as om
11+
from pytest import fixture
1112

1213
from h2integrate.core.feedstocks import FeedstockCostModel, FeedstockPerformanceModel
1314

1415

16+
@fixture
17+
def plant_config():
18+
return {
19+
"plant": {
20+
"plant_life": 30,
21+
"simulation": {
22+
"n_timesteps": 8760,
23+
"dt": 3600,
24+
},
25+
},
26+
}
27+
28+
29+
@fixture
30+
def ng_feedstock_input_config():
31+
tech_config = {
32+
"model_inputs": {
33+
"shared_parameters": {
34+
"commodity": "natural_gas",
35+
"commodity_rate_units": "MMBtu/h",
36+
},
37+
"performance_parameters": {
38+
"rated_capacity": 100.0,
39+
},
40+
"cost_parameters": {
41+
"price": 4.2, # USD/MMBtu
42+
"annual_cost": 0,
43+
"start_up_cost": 0,
44+
"cost_year": 2023,
45+
"commodity_amount_units": "MMBtu", # optional
46+
},
47+
}
48+
}
49+
return tech_config
50+
51+
52+
@pytest.mark.unit
53+
def test_feedstock_standard_outputs(plant_config, ng_feedstock_input_config, subtests):
54+
perf_model = FeedstockPerformanceModel(
55+
plant_config=plant_config, tech_config=ng_feedstock_input_config, driver_config={}
56+
)
57+
cost_model = FeedstockCostModel(
58+
plant_config=plant_config, tech_config=ng_feedstock_input_config, driver_config={}
59+
)
60+
prob = om.Problem()
61+
prob.model.add_subsystem("ng_feedstock_source", perf_model)
62+
prob.model.add_subsystem("ng_feedstock", cost_model)
63+
# Connect the feedstock performance model output to the cost model input
64+
prob.model.connect(
65+
"ng_feedstock_source.natural_gas_out",
66+
"ng_feedstock.natural_gas_out",
67+
)
68+
69+
prob.setup()
70+
# Set some consumption values
71+
consumption = np.full(8760, 50.0) # 50 MMBtu/hour
72+
prob.set_val("ng_feedstock.natural_gas_consumed", consumption)
73+
prob.run_model()
74+
with subtests.test("Check feedstock capacity factor"):
75+
ng_cf = prob.get_val("ng_feedstock.capacity_factor", units="unitless").mean()
76+
assert pytest.approx(ng_cf, rel=1e-6) == 0.5
77+
with subtests.test("Check feedstock rated production"):
78+
rated_production_source = prob.get_val(
79+
"ng_feedstock_source.natural_gas_capacity", units="MMBtu/h"
80+
)
81+
rated_production = prob.get_val(
82+
"ng_feedstock.rated_natural_gas_production", units="MMBtu/h"
83+
)
84+
assert pytest.approx(rated_production, rel=1e-6) == rated_production_source
85+
with subtests.test("Check feedstock total consumption"):
86+
total_consumption = prob.get_val("ng_feedstock.total_natural_gas_consumed", units="MMBtu")
87+
assert pytest.approx(total_consumption, rel=1e-6) == consumption.sum()
88+
with subtests.test("Check feedstock annual consumption"):
89+
annual_consumption = prob.get_val(
90+
"ng_feedstock.annual_natural_gas_consumed", units="MMBtu/yr"
91+
)
92+
assert pytest.approx(annual_consumption, rel=1e-6) == consumption.sum()
93+
94+
1595
def create_basic_feedstock_config(
1696
feedstock_type="natural_gas",
1797
units="MMBtu/h",
@@ -38,7 +118,7 @@ def create_basic_feedstock_config(
38118
},
39119
}
40120
}
41-
plant_config = {"plant": {"plant_life": 30, "simulation": {"n_timesteps": 8760}}}
121+
plant_config = {"plant": {"plant_life": 30, "simulation": {"n_timesteps": 8760, "dt": 3600}}}
42122
driver_config = {}
43123
return tech_config, plant_config, driver_config
44124

0 commit comments

Comments
 (0)