Skip to content

Commit 7b46163

Browse files
Sync Storage Autosizing Model and Pass-Through controller to Align with Pyomo Controllers (#608)
* updated the storage autosizing and pass through controller for new control and performance model structure * added subtest for integration with pass through controller * updated to use commodity_set_point instead of input-demand * made set_demand_as_avg_commodity_in a required input parameter * moved passthrough controller to storage subfolder * Added minor changes that got missed in PR 600 --------- Co-authored-by: kbrunik <102193481+kbrunik@users.noreply.github.com>
1 parent b3754a7 commit 7b46163

15 files changed

Lines changed: 1094 additions & 246 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- Add documentation for Rosner iron DRI and steel EAF models
99
- Breaks out pyomo controller simulation code from base class to individual controllers. [PR 587](https://github.com/NatLabRockies/H2Integrate/pull/587)
1010
- Add tests for non-one valued charge, discharge, and round-trip efficiencies for the open-loop demand controller [PR 610](https://github.com/NatLabRockies/H2Integrate/pull/610)
11+
- Updated the `StorageAutoSizingModel` and `PassThroughOpenLoopController` so that `commodity_set_point` is used as the storage dispatch command [PR 608](https://github.com/NatLabRockies/H2Integrate/pull/608)
1112

1213
## 0.7.1 [March 13, 2026]
1314

examples/01_onshore_steel_mn/tech_config.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,13 @@ technologies:
126126
shared_parameters:
127127
commodity: hydrogen
128128
commodity_rate_units: kg/h
129+
set_demand_as_avg_commodity_in: true
130+
performance_parameters:
131+
min_charge_fraction: 0.0
132+
max_charge_fraction: 1.0
133+
charge_efficiency: 1.0
134+
discharge_efficiency: 1.0
135+
commodity_amount_units: kg
129136
cost_parameters:
130137
# since the storage is being auto-sized by the performance model,
131138
# we set the sizing mode to 'auto' rather than defining the capacities

examples/02_texas_ammonia/tech_config.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,13 @@ technologies:
126126
shared_parameters:
127127
commodity: hydrogen
128128
commodity_rate_units: kg/h
129+
set_demand_as_avg_commodity_in: true
130+
performance_parameters:
131+
min_charge_fraction: 0.0
132+
max_charge_fraction: 1.0
133+
charge_efficiency: 1.0
134+
discharge_efficiency: 1.0
135+
commodity_amount_units: kg
129136
cost_parameters:
130137
# since the storage is being auto-sized by the performance model,
131138
# we set the sizing mode to 'auto' rather than defining the capacities

examples/12_ammonia_synloop/tech_config.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,13 @@ technologies:
126126
shared_parameters:
127127
commodity: hydrogen
128128
commodity_rate_units: kg/h
129+
set_demand_as_avg_commodity_in: true
130+
performance_parameters:
131+
min_charge_fraction: 0.0
132+
max_charge_fraction: 1.0
133+
charge_efficiency: 1.0
134+
discharge_efficiency: 1.0
135+
commodity_amount_units: kg
129136
cost_parameters:
130137
# since the storage is being auto-sized by the performance model,
131138
# we set the sizing mode to 'auto' rather than defining the capacities

examples/test/test_all_examples.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def test_steel_example(subtests, temp_copy_of_example):
8888

8989
with subtests.test("Check H2 Storage capacity"):
9090
assert (
91-
pytest.approx(model.prob.get_val("h2_storage.max_capacity", units="kg"), rel=1e-3)
91+
pytest.approx(model.prob.get_val("h2_storage.storage_capacity", units="kg"), rel=1e-3)
9292
== 2559669.7759292
9393
)
9494

h2integrate/control/control_strategies/passthrough_openloop_controller.py

Lines changed: 0 additions & 92 deletions
This file was deleted.
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import numpy as np
2+
import openmdao.api as om
3+
from attrs import field, define
4+
5+
from h2integrate.core.utilities import BaseConfig, merge_shared_inputs
6+
7+
8+
@define(kw_only=True)
9+
class PassThroughOpenLoopControllerConfig(BaseConfig):
10+
"""Configuration class for the PassThroughOpenLoopController
11+
12+
Attributes:
13+
commodity (str): name of commodity
14+
commodity_rate_units (str): Units of the commodity (e.g., kW or kg/h).
15+
set_demand_as_avg_commodity_in (bool): If True, assume the demand is
16+
equal to the mean input commodity. If False, uses the demand input.
17+
demand_profile (int | float | list, optional): Demand values for each timestep, in
18+
the same units as `commodity_rate_units`. May be a scalar for constant
19+
demand or a list/array for time-varying demand.
20+
Only used if `set_demand_as_avg_commodity_in` is False. Defaults to 0.
21+
22+
"""
23+
24+
commodity: str = field()
25+
commodity_rate_units: str = field()
26+
set_demand_as_avg_commodity_in: bool = field()
27+
demand_profile: int | float | list = field(default=0.0)
28+
29+
def __attrs_post_init__(self):
30+
if isinstance(self.demand_profile, list | np.ndarray):
31+
user_input_dmd = True if sum(self.demand_profile) > 0 else False
32+
else:
33+
user_input_dmd = True if self.demand_profile > 0 else False
34+
35+
if self.set_demand_as_avg_commodity_in and user_input_dmd:
36+
# If using the average commodity in as the demand,
37+
# warn users if they input the demand profile
38+
msg = (
39+
"A non-zero demand profile was provided but set_demand_as_avg_commodity_in is True."
40+
" The provided demand profile will not be used, the demand profile will be "
41+
f"calculated as the mean of ``{self.commodity}_in``. "
42+
)
43+
raise ValueError(msg)
44+
45+
46+
class PassThroughOpenLoopController(om.ExplicitComponent):
47+
"""
48+
A simple pass-through controller for open-loop systems.
49+
50+
This controller directly sets a storage control set point as the difference between the
51+
demand and the available input commodity. It is useful for testing, as a placeholder for
52+
more complex storage controllers, and for maintaining consistency between controlled and
53+
uncontrolled frameworks.
54+
"""
55+
56+
def initialize(self):
57+
"""
58+
Declare options for the component. See "Attributes" section in class doc strings for
59+
details.
60+
"""
61+
62+
self.options.declare("driver_config", types=dict)
63+
self.options.declare("plant_config", types=dict)
64+
self.options.declare("tech_config", types=dict)
65+
66+
def setup(self):
67+
self.config = PassThroughOpenLoopControllerConfig.from_dict(
68+
merge_shared_inputs(self.options["tech_config"]["model_inputs"], "control"),
69+
additional_cls_name=self.__class__.__name__,
70+
)
71+
72+
self.n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"]
73+
74+
self.add_input(
75+
f"{self.config.commodity}_in",
76+
shape_by_conn=True,
77+
units=self.config.commodity_rate_units,
78+
desc=f"{self.config.commodity} input timeseries from production to storage",
79+
)
80+
81+
self.add_input(
82+
f"{self.config.commodity}_demand",
83+
units=f"{self.config.commodity_rate_units}",
84+
val=self.config.demand_profile,
85+
shape=self.n_timesteps,
86+
desc=f"{self.config.commodity} demand profile timeseries",
87+
)
88+
89+
self.add_output(
90+
f"{self.config.commodity}_set_point",
91+
copy_shape=f"{self.config.commodity}_in",
92+
units=self.config.commodity_rate_units,
93+
desc=f"{self.config.commodity} output timeseries from plant after storage",
94+
)
95+
96+
def compute(self, inputs, outputs):
97+
"""
98+
Simple controller.
99+
100+
Args:
101+
inputs (dict): Dictionary of input values.
102+
- {commodity}_in: Input commodity flow.
103+
- {commodity}_demand: Commodity demand profile.
104+
Only used if `set_demand_as_avg_commodity_in` is False.
105+
outputs (dict): Dictionary of output values.
106+
- {commodity}_set_point: Dispatch set-points for each timestep
107+
in `commodity_rate_units`. Negative values command charging;
108+
positive values command discharging.
109+
"""
110+
111+
if (
112+
self.config.set_demand_as_avg_commodity_in
113+
and inputs[f"{self.config.commodity}_demand"].sum() > 0
114+
):
115+
msg = (
116+
"A non-zero demand profile was input but set_demand_as_avg_commodity_in is True."
117+
" The input demand profile will not be used, the demand profile will be "
118+
f"calculated as the mean of ``{self.config.commodity}_in``. "
119+
)
120+
raise ValueError(msg)
121+
122+
if self.config.set_demand_as_avg_commodity_in:
123+
# Assume the demand is the average of the input commodity
124+
commodity_demand = np.mean(inputs[f"{self.config.commodity}_in"]) * np.ones(
125+
self.n_timesteps
126+
)
127+
else:
128+
commodity_demand = inputs[f"{self.config.commodity}_demand"]
129+
130+
# Assign the set point as the difference between the demand and the input commodity
131+
# when demand > input, the set point is positive to command discharging
132+
# when demand < input, the set point is negative to command charging
133+
outputs[f"{self.config.commodity}_set_point"] = (
134+
commodity_demand - inputs[f"{self.config.commodity}_in"]
135+
)

0 commit comments

Comments
 (0)