Skip to content

Commit 6d5b517

Browse files
Add tests for commodity buying
1 parent b75f63f commit 6d5b517

File tree

2 files changed

+265
-25
lines changed

2 files changed

+265
-25
lines changed

h2integrate/control/control_strategies/optimized_pyomo_controller.py

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -80,34 +80,34 @@ class OptimizedDispatchControllerConfig(PyomoControllerBaseConfig):
8080
time_weighting_factor: float = field(validator=range_val(0, 1), default=0.995)
8181
time_duration: float = field(default=1.0) # hours
8282
# Can we set this to interconnection? do we want to?
83-
max_system_capacity: float = field(default=0)
84-
commodity_buy_price: int | float | list = field(default=0.0)
83+
max_system_capacity: float = field(default=None)
84+
commodity_buy_price: int | float | list = field(default=None)
8585

8686
def __attrs_post_init__(self):
87-
# # Handle demand_met_value list/float here
88-
# if isinstance(self.system_commodity_interface_limit, float | int):
89-
# self.system_commodity_interface_limit = [
90-
# self.system_commodity_interface_limit
91-
# ] * self.n_control_window
92-
9387
# Check inputs for grid parameters
9488
if self.allow_commodity_buying:
95-
# Check grid buy price
96-
if isinstance(self.commodity_buy_price, float | int):
97-
if self.commodity_buy_price == 0:
98-
raise ValueError(
99-
"commodity_buy_price must be defined as an input and >0 \
100-
if using grid charging"
101-
)
102-
if isinstance(self.commodity_buy_price, list):
103-
if all(self.commodity_buy_price) == 0:
104-
raise ValueError(
105-
"commodity_buy_price must be defined as an input and >0 \
106-
if using grid charging"
107-
)
89+
if self.commodity_buy_price:
90+
# Check grid buy price
91+
if isinstance(self.commodity_buy_price, float | int):
92+
if self.commodity_buy_price == 0:
93+
raise ValueError(
94+
"commodity_buy_price must be defined as an input and >0 \
95+
if using grid charging"
96+
)
97+
if isinstance(self.commodity_buy_price, list) or self.commodity_buy_price is None:
98+
if all(self.commodity_buy_price) == 0:
99+
raise ValueError(
100+
"commodity_buy_price must be defined as an input and >0 \
101+
if using grid charging"
102+
)
103+
else:
104+
raise ValueError(
105+
"commodity_buy_price must be defined as an input and >0 \
106+
if using grid charging"
107+
)
108108

109109
# Check max system capacity
110-
if self.max_system_capacity == 0:
110+
if self.max_system_capacity == 0 or self.max_system_capacity is None:
111111
raise ValueError(
112112
"max_system_capacity must be defined as an input and >0 if using grid charging"
113113
)
@@ -124,11 +124,13 @@ def make_dispatch_inputs(self):
124124
"charge_efficiency",
125125
"discharge_efficiency",
126126
"max_charge_rate",
127-
"max_system_capacity",
128127
"allow_commodity_buying",
129-
"commodity_buy_price",
130128
]
131129

130+
if self.allow_commodity_buying:
131+
dispatch_keys.append("max_system_capacity")
132+
dispatch_keys.append("commodity_buy_price")
133+
132134
dispatch_inputs = {k: self.as_dict()[k] for k in dispatch_keys}
133135
dispatch_inputs.update({"initial_soc_fraction": self.init_soc_fraction})
134136
return dispatch_inputs

h2integrate/control/test/test_optimal_controllers.py

Lines changed: 239 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from copy import deepcopy
2+
13
import numpy as np
24
import pytest
35
import openmdao.api as om
@@ -8,6 +10,7 @@
810
from h2integrate.storage.simple_storage_auto_sizing import StorageAutoSizingModel
911
from h2integrate.control.control_strategies.optimized_pyomo_controller import (
1012
OptimizedDispatchController,
13+
OptimizedDispatchControllerConfig,
1114
)
1215

1316

@@ -245,7 +248,6 @@ def test_min_operating_cost_load_following_battery_dispatch(
245248
assert pytest.approx(prob.model.get_val("battery.SOC")[0], rel=1e-2) == 50
246249

247250
# Find where the signal increases, decreases, and stays at zero
248-
print("SOC", prob.model.get_val("battery.SOC"))
249251
indx_soc_increase = np.argwhere(
250252
np.diff(prob.model.get_val("battery.SOC", units="unitless"), prepend=True) > 0
251253
).flatten()
@@ -627,3 +629,239 @@ def test_optimal_dispatch_with_autosizing_storage_demand_is_avg_in(
627629
expected_charge,
628630
rtol=1e-6,
629631
)
632+
633+
634+
@pytest.mark.regression
635+
def test_optimal_control_config_with_commodity_buying(subtests):
636+
config_data = {
637+
"tech_name": "h2_storage",
638+
"max_charge_rate": 10.0,
639+
"charge_efficiency": 1.0,
640+
"discharge_efficiency": 1.0,
641+
"commodity": "hydrogen",
642+
"commodity_rate_units": "kg/h",
643+
"max_capacity": 40.0,
644+
"init_soc_fraction": 0.2,
645+
"max_soc_fraction": 1.0,
646+
"min_soc_fraction": 0.1,
647+
"cost_per_charge": 0.03, # USD/kg
648+
"cost_per_discharge": 0.05, # USD/kg
649+
"demand_met_value": 0.1, # USD/kg
650+
"cost_per_production": 0.0, # USD/kg
651+
"time_weighting_factor": 0.995,
652+
"system_commodity_interface_limit": 10.0,
653+
"n_control_window": 24,
654+
"allow_commodity_buying": False,
655+
}
656+
657+
config = OptimizedDispatchControllerConfig.from_dict(config_data)
658+
659+
with subtests.test("check commodity_buy_price is None"):
660+
assert config.commodity_buy_price is None
661+
with subtests.test("check max_system_capacity is None"):
662+
assert config.max_system_capacity is None
663+
664+
config_data["allow_commodity_buying"] = True
665+
666+
with subtests.test("with invalid commodity_buy_price"):
667+
with pytest.raises(ValueError):
668+
data = deepcopy(config_data)
669+
data["allow_commodity_buying"] = True
670+
OptimizedDispatchControllerConfig.from_dict(data)
671+
672+
with pytest.raises(ValueError):
673+
data = deepcopy(config_data)
674+
data["allow_commodity_buying"] = True
675+
data["commodity_buy_price"] = 0.4
676+
OptimizedDispatchControllerConfig.from_dict(data)
677+
678+
with pytest.raises(ValueError):
679+
data = deepcopy(config_data)
680+
data["allow_commodity_buying"] = True
681+
data["commodity_buy_price"] = 0.4
682+
data["max_system_capacity"] = 0.0
683+
OptimizedDispatchControllerConfig.from_dict(data)
684+
685+
686+
@pytest.mark.regression
687+
def test_optimal_control_with_commodity_buying_generic_storage(
688+
plant_config_h2_storage, tech_config_generic, subtests
689+
):
690+
commodity_demand = np.full(48, 5.0)
691+
commodity_in = np.tile(np.concat([np.zeros(3), np.cumsum(np.ones(15)), np.full(6, 4.0)]), 2)
692+
commodity_buy_price = np.tile(np.concat([np.arange(-3, 9), np.arange(8, -4, -1)]), 2)
693+
max_system_capacity = 7
694+
695+
# Set grid charging parameters
696+
tech_config_generic["technologies"]["h2_storage"]["model_inputs"]["control_parameters"] = {
697+
"tech_name": "h2_storage",
698+
"cost_per_charge": 0.03, # USD/kg
699+
"cost_per_discharge": 0.05, # USD/kg
700+
"demand_met_value": 0.1, # USD/kg
701+
"cost_per_production": 0.0, # USD/kg
702+
"time_weighting_factor": 0.995,
703+
"system_commodity_interface_limit": 10.0,
704+
"n_control_window": 24,
705+
"allow_commodity_buying": True,
706+
"commodity_buy_price": 1,
707+
"max_system_capacity": max_system_capacity,
708+
}
709+
710+
# Setup the OpenMDAO problem and add subsystems
711+
prob = om.Problem()
712+
713+
prob.model.add_subsystem(
714+
"h2_storage_optimized_load_following_controller",
715+
OptimizedDispatchController(
716+
plant_config=plant_config_h2_storage,
717+
tech_config=tech_config_generic["technologies"]["h2_storage"],
718+
),
719+
promotes=["*"],
720+
)
721+
722+
prob.model.add_subsystem(
723+
"h2_storage",
724+
StoragePerformanceModel(
725+
plant_config=plant_config_h2_storage,
726+
tech_config=tech_config_generic["technologies"]["h2_storage"],
727+
),
728+
promotes=["*"],
729+
)
730+
731+
# Setup the system and required values
732+
prob.setup()
733+
prob.set_val("h2_storage.hydrogen_in", commodity_in)
734+
prob.set_val("h2_storage.hydrogen_demand", commodity_demand)
735+
prob.set_val("hydrogen_buy_price", commodity_buy_price)
736+
737+
# Run the model
738+
prob.run_model()
739+
740+
charge_rate = prob.get_val("h2_storage.max_charge_rate", units="kg/h")[0]
741+
discharge_rate = prob.get_val("h2_storage.max_charge_rate", units="kg/h")[0]
742+
capacity = prob.get_val("h2_storage.storage_capacity", units="kg")[0]
743+
744+
print("outputs: ", prob.get_val("storage_hydrogen_out"))
745+
print("discharge: ", prob.get_val("h2_storage.storage_hydrogen_discharge"))
746+
print("charge: ", prob.get_val("h2_storage.storage_hydrogen_charge"))
747+
print("commodity in: ", prob.get_val("h2_storage.hydrogen_in"))
748+
print("demand: ", prob.get_val("h2_storage.hydrogen_demand"))
749+
print("commodity_buy: ", prob.get_val("hydrogen_buy_price"))
750+
print("hydrogen_out: ", prob.get_val("hydrogen_out"))
751+
752+
# Test that discharge is always positive
753+
with subtests.test("Discharge is always positive"):
754+
assert np.all(prob.get_val("h2_storage.storage_hydrogen_discharge", units="kg/h") >= 0)
755+
with subtests.test("Charge is always negative"):
756+
assert np.all(prob.get_val("h2_storage.storage_hydrogen_charge", units="kg/h") <= 0)
757+
with subtests.test("Charge + Discharge == storage_hydrogen_out"):
758+
charge_plus_discharge = prob.get_val(
759+
"h2_storage.storage_hydrogen_charge", units="kg/h"
760+
) + prob.get_val("h2_storage.storage_hydrogen_discharge", units="kg/h")
761+
np.testing.assert_allclose(
762+
charge_plus_discharge, prob.get_val("storage_hydrogen_out", units="kg/h"), rtol=1e-6
763+
)
764+
with subtests.test("Initial SOC is correct"):
765+
assert (
766+
pytest.approx(prob.model.get_val("h2_storage.SOC", units="unitless")[0], rel=1e-6)
767+
== 0.375
768+
)
769+
770+
indx_soc_increase = np.argwhere(
771+
np.diff(prob.model.get_val("h2_storage.SOC", units="unitless"), prepend=True) > 0
772+
).flatten()
773+
indx_soc_decrease = np.argwhere(
774+
np.diff(prob.model.get_val("h2_storage.SOC", units="unitless"), prepend=False) < 0
775+
).flatten()
776+
indx_soc_same = np.argwhere(
777+
np.diff(prob.model.get_val("h2_storage.SOC", units="unitless"), prepend=True) == 0.0
778+
).flatten()
779+
780+
with subtests.test("SOC increases when charging"):
781+
assert np.all(
782+
prob.get_val("h2_storage.storage_hydrogen_charge", units="kg/h")[indx_soc_increase] < 0
783+
)
784+
assert np.all(
785+
prob.get_val("h2_storage.storage_hydrogen_charge", units="kg/h")[indx_soc_decrease] == 0
786+
)
787+
assert np.all(
788+
prob.get_val("h2_storage.storage_hydrogen_charge", units="kg/h")[indx_soc_same] == 0
789+
)
790+
791+
with subtests.test("SOC decreases when discharging"):
792+
assert np.all(
793+
prob.get_val("h2_storage.storage_hydrogen_discharge", units="kg/h")[indx_soc_decrease]
794+
> 0
795+
)
796+
assert np.all(
797+
prob.get_val("h2_storage.storage_hydrogen_discharge", units="kg/h")[indx_soc_increase]
798+
== 0
799+
)
800+
assert np.all(
801+
prob.get_val("h2_storage.storage_hydrogen_discharge", units="kg/h")[indx_soc_same] == 0
802+
)
803+
804+
with subtests.test("Max SOC <= Max storage percent"):
805+
assert prob.get_val("h2_storage.SOC", units="unitless").max() <= 1.0
806+
807+
with subtests.test("Min SOC >= Min storage percent"):
808+
assert prob.get_val("h2_storage.SOC", units="unitless").min() >= 0.1
809+
810+
with subtests.test("Charge never exceeds charge rate"):
811+
assert (
812+
prob.get_val("h2_storage.storage_hydrogen_charge", units="kg/h").min()
813+
>= -1 * charge_rate
814+
)
815+
816+
with subtests.test("Discharge never exceeds discharge rate"):
817+
assert (
818+
prob.get_val("h2_storage.storage_hydrogen_discharge", units="kg/h").max()
819+
<= discharge_rate
820+
)
821+
822+
with subtests.test("Discharge never exceeds demand"):
823+
np.testing.assert_allclose(
824+
prob.get_val("h2_storage.storage_hydrogen_discharge", units="kg/h").max(),
825+
commodity_demand,
826+
rtol=1e-6,
827+
)
828+
829+
with subtests.test("Sometimes discharges"):
830+
assert any(
831+
k > 1e-3 for k in prob.get_val("h2_storage.storage_hydrogen_discharge", units="kg/h")
832+
)
833+
834+
with subtests.test("Sometimes charges"):
835+
assert any(
836+
k < -1e-3 for k in prob.get_val("h2_storage.storage_hydrogen_charge", units="kg/h")
837+
)
838+
839+
with subtests.test("Cumulative charge/discharge does not exceed storage capacity"):
840+
assert np.cumsum(prob.get_val("storage_hydrogen_out", units="kg/h")).max() <= capacity
841+
assert np.cumsum(prob.get_val("storage_hydrogen_out", units="kg/h")).min() >= -1 * capacity
842+
843+
with subtests.test("Expected discharge from hour 10-30"):
844+
expected_discharge = np.concat(
845+
[np.zeros(8), np.ones(3), np.zeros(3), [5, 0], np.arange(5, 1, -1)]
846+
)
847+
np.testing.assert_allclose(
848+
prob.get_val("h2_storage.storage_hydrogen_discharge", units="kg/h")[10:30],
849+
expected_discharge,
850+
rtol=1e-6,
851+
atol=1e-6,
852+
)
853+
854+
with subtests.test("Expected charge hour 0-20"):
855+
expected_charge = np.concat([np.ones(3) * -7, np.zeros(5), [-1, -2], np.zeros(10)])
856+
np.testing.assert_allclose(
857+
prob.get_val("h2_storage.storage_hydrogen_charge", units="kg/h")[0:20],
858+
expected_charge,
859+
rtol=1e-6,
860+
)
861+
862+
with subtests.test("Output never exceeds system commodity draw limit"):
863+
np.testing.assert_allclose(
864+
prob.get_val("h2_storage.storage_hydrogen_charge", units="kg/h").min(),
865+
-max_system_capacity,
866+
rtol=1e-6,
867+
)

0 commit comments

Comments
 (0)