Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -788,3 +788,101 @@ def test_battery_pyomo_h2s_openloop(subtests, plant_config):
h2s_expected_discharge,
rtol=1e-6,
)


@pytest.mark.regression
@pytest.mark.parametrize("pyo_controllers", ["bat"])
def test_battery_pyomo_battery_openloop(subtests, plant_config):
bat2_expected_discharge = np.concat([np.zeros(18), np.ones(6)])
bat2_expected_charge = np.concat([np.zeros(8), np.arange(-1, -9, -1), np.zeros(8)])
bat_expected_charge = np.concat(
[
np.zeros(12),
np.array(
[
-3988.62235554,
-3989.2357847,
-3989.76832626,
-3990.26170521,
-3990.71676106,
-3991.13573086,
-3991.52143699,
-3991.87684905,
-3992.20485715,
-3992.50815603,
-3992.78920148,
-3993.05020268,
]
),
]
)
bat_expected_discharge = np.concat(
[
np.array(
[
5999.99995059,
5990.56676743,
5990.138959,
5989.64831176,
5989.08548217,
5988.44193888,
5987.70577962,
5986.86071125,
5985.88493352,
5984.7496388,
5983.41717191,
5981.839478,
]
),
np.zeros(12),
]
)

prob = om.Problem()

# make h2 storage group
h2s_group = prob.model.add_subsystem("battery_2", om.Group())
h2s_ivc_comp, h2s_perf_comp, h2s_control_comp = make_h2_storage_openloop_group(plant_config)
h2s_group.add_subsystem("IVC1", h2s_ivc_comp, promotes=["*"])
h2s_group.add_subsystem("control", h2s_control_comp, promotes=["*"])
h2s_group.add_subsystem("perf", h2s_perf_comp, promotes=["*"])

# make battery group
bat_rule_comp, bat_perf_comp, bat_control_comp, electricity_in = make_battery_pyo_group(
plant_config
)
bat_group = prob.model.add_subsystem("battery", om.Group())
bat_group.add_subsystem("IVC2", electricity_in, promotes=["*"])
bat_group.add_subsystem("rule", bat_rule_comp, promotes=["*"])
bat_group.add_subsystem("control", bat_control_comp, promotes=["*"])
bat_group.add_subsystem("perf", bat_perf_comp, promotes=["*"])

prob.setup()
prob.run_model()

with subtests.test("Battery #1: Expected charge"):
np.testing.assert_allclose(
prob.get_val("battery.storage_electricity_charge", units="kW")[:24],
bat_expected_charge,
rtol=1e-6,
)
with subtests.test("Battery #1: Expected discharge"):
np.testing.assert_allclose(
prob.get_val("battery.storage_electricity_discharge", units="kW")[:24],
bat_expected_discharge,
rtol=1e-6,
)

# battery_2 is a "hydrogen battery"
with subtests.test("Battery #2: Expected charge"):
np.testing.assert_allclose(
prob.get_val("battery_2.storage_hydrogen_charge", units="kg/h")[:24],
bat2_expected_charge,
rtol=1e-6,
)
with subtests.test("Battery #2: Expected discharge"):
np.testing.assert_allclose(
prob.get_val("battery_2.storage_hydrogen_discharge", units="kg/h")[:24],
bat2_expected_discharge,
rtol=1e-6,
)
20 changes: 20 additions & 0 deletions h2integrate/control/test/test_optimal_controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,11 @@ def test_min_operating_cost_load_following_battery_dispatch(
rtol=1e-2,
)

with subtests.test("Charge never exceeds available commodity"):
charge_profile = prob.get_val("battery.storage_electricity_charge", units="kW")
indx_charging = np.argwhere(charge_profile).flatten()
assert np.all(np.abs(charge_profile)[indx_charging] <= electricity_in[indx_charging])


@pytest.mark.regression
def test_optimal_control_with_generic_storage(
Expand Down Expand Up @@ -477,6 +482,11 @@ def test_optimal_control_with_generic_storage(
rtol=1e-6,
)

with subtests.test("Charge never exceeds available commodity"):
charge_profile = prob.get_val("h2_storage.storage_hydrogen_charge", units="kg/h")
indx_charging = np.argwhere(charge_profile).flatten()
assert np.all(np.abs(charge_profile)[indx_charging] <= commodity_in[indx_charging])


@pytest.mark.regression
def test_optimal_dispatch_with_autosizing_storage_demand_less_than_avg_in(
Expand Down Expand Up @@ -547,6 +557,11 @@ def test_optimal_dispatch_with_autosizing_storage_demand_less_than_avg_in(
rtol=1e-6,
)

with subtests.test("Charge never exceeds available commodity"):
charge_profile = prob.get_val("h2_storage.storage_hydrogen_charge", units="kg/h")
indx_charging = np.argwhere(charge_profile).flatten()
assert np.all(np.abs(charge_profile)[indx_charging] <= commodity_in[indx_charging])


@pytest.mark.regression
def test_optimal_dispatch_with_autosizing_storage_demand_is_avg_in(
Expand Down Expand Up @@ -624,3 +639,8 @@ def test_optimal_dispatch_with_autosizing_storage_demand_is_avg_in(
expected_charge,
rtol=1e-6,
)

with subtests.test("Charge never exceeds available commodity"):
charge_profile = prob.get_val("h2_storage.storage_hydrogen_charge", units="kg/h")
indx_charging = np.argwhere(charge_profile).flatten()
assert np.all(np.abs(charge_profile)[indx_charging] <= commodity_in[indx_charging])
6 changes: 5 additions & 1 deletion h2integrate/storage/battery/pysam_battery.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ def simulate(
charge_rate: float,
discharge_rate: float,
storage_capacity: float,
commodity_available=list | np.ndarray,
sim_start_index: int = 0,
):
"""Run the PySAM BatteryStateful model over a control window.
Expand Down Expand Up @@ -241,13 +242,16 @@ def simulate(
# expressed as a rate (commodity_rate_units).
headroom = (soc_max - soc) * storage_capacity / self.dt_hr

# charge available based on the available input commodity
charge_available = commodity_available[sim_start_index + t]

# Calculate the max charge according to the charge rate and the simulation
max_charge_input = min([charge_rate, -self.system_model.value("P_chargeable")])

# Clip to the most restrictive limit,
# max(0, ...) guards against negative headroom when SOC
# slightly exceeds soc_max.
actual_charge = max(0.0, min(headroom, max_charge_input, -cmd))
actual_charge = max(0.0, min(headroom, max_charge_input, -cmd, charge_available))

# Update the charge command for the PySAM batttery
cmd = -actual_charge
Expand Down
14 changes: 14 additions & 0 deletions h2integrate/storage/battery/test/test_pysam_battery.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,10 @@ def test_pysam_battery_performance_model_without_controller(plant_config, subtes
expected_unused_electricity,
rtol=1e-2,
)
with subtests.test("Charge never exceeds available commodity"):
charge_profile = prob.get_val("storage_electricity_charge", units="kW")
indx_charging = np.argwhere(charge_profile).flatten()
assert np.all(np.abs(charge_profile)[indx_charging] <= electricity_in[indx_charging])


@pytest.mark.regression
Expand Down Expand Up @@ -382,6 +386,11 @@ def test_pysam_battery_no_controller_change_capacity(plant_config, subtests):
== init_charge_rate
)

with subtests.test("Charge never exceeds available commodity"):
charge_profile = prob_init.get_val("pysam_battery.storage_electricity_charge", units="kW")
indx_charging = np.argwhere(charge_profile).flatten()
assert np.all(np.abs(charge_profile)[indx_charging] <= electricity_in[indx_charging])

# Re-run and set the charge rate as half of what it was before
prob = om.Problem()
prob.model.add_subsystem(
Expand Down Expand Up @@ -458,3 +467,8 @@ def test_pysam_battery_no_controller_change_capacity(plant_config, subtests):
)
== 2.5
)

with subtests.test("Charge never exceeds available commodity"):
charge_profile = prob.get_val("pysam_battery.storage_electricity_charge", units="kW")
indx_charging = np.argwhere(charge_profile).flatten()
assert np.all(np.abs(charge_profile)[indx_charging] <= electricity_in[indx_charging])
16 changes: 14 additions & 2 deletions h2integrate/storage/storage_baseclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ def setup(self):
for _source_tech, intended_dispatch_tech in self.options["plant_config"][
"tech_to_dispatch_connections"
]:
if any(intended_dispatch_tech in name for name in self.tech_group_name):
if any(intended_dispatch_tech == name for name in self.tech_group_name):
self.add_discrete_input("pyomo_dispatch_solver", val=lambda: None)
# set the using feedback control variable to True
using_feedback_control = True
Expand Down Expand Up @@ -274,6 +274,7 @@ def run_storage(
"charge_rate": charge_rate,
"discharge_rate": discharge_rate,
"storage_capacity": storage_capacity,
"commodity_available": inputs[f"{self.commodity}_in"],
}
storage_commodity_out, soc = dispatch(self.simulate, kwargs, inputs)

Expand All @@ -283,6 +284,7 @@ def run_storage(
charge_rate=charge_rate,
discharge_rate=discharge_rate,
storage_capacity=storage_capacity,
commodity_available=inputs[f"{self.commodity}_in"],
)

# determine storage charge and discharge
Expand Down Expand Up @@ -347,6 +349,7 @@ def simulate(
charge_rate: float,
discharge_rate: float,
storage_capacity: float,
commodity_available: list | np.ndarray,
sim_start_index: int = 0,
):
"""Run the storage model over a control window of ``n_control_window`` timesteps.
Expand Down Expand Up @@ -385,6 +388,8 @@ def simulate(
``commodity_rate_units`` (before discharge efficiency is applied).
storage_capacity (float):
Rated storage capacity in ``commodity_amount_units``.
commodity_available (list | np.ndarray): the input commodity available
to charge storage.
sim_start_index (int, optional):
Starting index for writing into persistent output arrays.
Defaults to 0.
Expand Down Expand Up @@ -427,11 +432,18 @@ def simulate(
# expressed as a rate (commodity_rate_units).
headroom = (soc_max - soc) * storage_capacity / self.dt_hr

# charge available based on the available input commodity
charge_available = commodity_available[sim_start_index + t]

# Clip to the most restrictive limit, then apply efficiency.
# max(0, ...) guards against negative headroom when SOC
# slightly exceeds soc_max.
# correct headroom to not include charge_eff.
actual_charge = max(0.0, min(headroom / charge_eff, charge_rate, -cmd)) * charge_eff

actual_charge = (
max(0.0, min(headroom / charge_eff, charge_rate, -cmd, charge_available))
* charge_eff
)

# Update SOC (actual_charge is in post-efficiency units)
soc += actual_charge / storage_capacity
Expand Down
17 changes: 17 additions & 0 deletions h2integrate/storage/test/test_storage_auto_sizing.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,10 @@ def test_storage_autosizing_basic_performance_no_losses(plant_config, subtests):
assert (
pytest.approx(prob.get_val("unused_hydrogen_out", units="kg/h").sum(), rel=1e-6) == 5.0
)
with subtests.test("Charge never exceeds available commodity"):
charge_profile = prob.get_val("storage.storage_hydrogen_charge", units="kg/h")
indx_charging = np.argwhere(charge_profile).flatten()
assert np.all(np.abs(charge_profile)[indx_charging] <= commodity_in[indx_charging])


@pytest.mark.regression
Expand Down Expand Up @@ -285,6 +289,10 @@ def test_storage_autosizing_soc_bounds(plant_config, subtests):
np.testing.assert_allclose(
prob.get_val("hydrogen_out", units="kg/h"), commodity_demand, rtol=1e-6, atol=1e-10
)
with subtests.test("Charge never exceeds available commodity"):
charge_profile = prob.get_val("storage.storage_hydrogen_charge", units="kg/h")
indx_charging = np.argwhere(charge_profile).flatten()
assert np.all(np.abs(charge_profile)[indx_charging] <= commodity_in[indx_charging])


@pytest.mark.regression
Expand Down Expand Up @@ -416,6 +424,11 @@ def test_storage_autosizing_losses(plant_config, subtests):
atol=1e-10,
)

with subtests.test("Charge never exceeds available commodity"):
charge_profile = prob.get_val("storage.storage_hydrogen_charge", units="kg/h")
indx_charging = np.argwhere(charge_profile).flatten()
assert np.all(np.abs(charge_profile)[indx_charging] <= commodity_in[indx_charging])


@pytest.mark.regression
@pytest.mark.parametrize("n_timesteps", [24])
Expand Down Expand Up @@ -507,3 +520,7 @@ def test_storage_autosizing_with_passthrough_controller(plant_config, subtests):
rtol=1e-6,
atol=1e-10,
)
with subtests.test("Charge never exceeds available commodity"):
charge_profile = prob.get_val("storage.storage_hydrogen_charge", units="kg/h")
indx_charging = np.argwhere(charge_profile).flatten()
assert np.all(np.abs(charge_profile)[indx_charging] <= commodity_in[indx_charging])
Loading
Loading