Skip to content

Commit 7302bbf

Browse files
committed
Create test_energy_storage_constr.py
1 parent 6db2e33 commit 7302bbf

2 files changed

Lines changed: 368 additions & 1 deletion

File tree

src/pownet/optim_model/constraints/energy_storage_constr.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""energy_storage_constr.py: Constraints for energy storage units."""
22

33
import gurobipy as gp
4-
import pandas as pd
54

65

76
def add_c_link_ess_charge(
Lines changed: 368 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,368 @@
1+
import unittest
2+
import gurobipy as gp
3+
4+
5+
from pownet.optim_model.constraints import energy_storage_constr
6+
7+
8+
class TestEnergyStorageConstraints(unittest.TestCase):
9+
"""Test cases for energy storage unit constraints."""
10+
11+
def setUp(self):
12+
"""Set up common resources for each test method."""
13+
self.model = gp.Model("test_energy_storage_constraints")
14+
self.model.setParam("OutputFlag", 0) # Suppress Gurobi output
15+
16+
self.timesteps = range(1, 4) # Timesteps: 1, 2, 3
17+
self.sim_horizon = 3 # Corresponds to the end of self.timesteps
18+
self.storage_units = ["ESS1", "ESS2"]
19+
20+
self.max_charge_cap = {
21+
unit: 50.0 + idx * 10 for idx, unit in enumerate(self.storage_units)
22+
}
23+
self.max_discharge_cap = {
24+
unit: 40.0 + idx * 10 for idx, unit in enumerate(self.storage_units)
25+
}
26+
27+
self.charge_state_init = {
28+
unit: 100.0 + idx * 20 for idx, unit in enumerate(self.storage_units)
29+
}
30+
self.charge_efficiency = {
31+
unit: 0.95 - idx * 0.02 for idx, unit in enumerate(self.storage_units)
32+
}
33+
# Ensure discharge_efficiency is not zero
34+
self.discharge_efficiency = {
35+
unit: 0.90 - idx * 0.02 for idx, unit in enumerate(self.storage_units)
36+
}
37+
self.self_discharge_rate = {
38+
unit: 0.01 + idx * 0.005 for idx, unit in enumerate(self.storage_units)
39+
}
40+
41+
# Variables
42+
self.pcharge = self.model.addVars(
43+
self.storage_units, self.timesteps, vtype=gp.GRB.CONTINUOUS, name="pcharge"
44+
)
45+
self.ucharge = self.model.addVars(
46+
self.storage_units, self.timesteps, vtype=gp.GRB.BINARY, name="ucharge"
47+
)
48+
self.pdischarge = self.model.addVars(
49+
self.storage_units,
50+
self.timesteps,
51+
vtype=gp.GRB.CONTINUOUS,
52+
name="pdischarge",
53+
)
54+
self.udischarge = self.model.addVars(
55+
self.storage_units, self.timesteps, vtype=gp.GRB.BINARY, name="udischarge"
56+
)
57+
self.charge_state = self.model.addVars(
58+
self.storage_units,
59+
self.timesteps,
60+
vtype=gp.GRB.CONTINUOUS,
61+
name="charge_state",
62+
)
63+
64+
self.model.update()
65+
66+
def tearDown(self):
67+
"""Clean up resources after each test method."""
68+
self.model.dispose()
69+
70+
def _check_constr_details(
71+
self,
72+
constr_name_base,
73+
constr_dict,
74+
expected_keys_iterable,
75+
expected_sense,
76+
get_expected_rhs_func,
77+
get_expected_lhs_coeffs_func,
78+
):
79+
"""Helper to check constraint details."""
80+
self.assertEqual(len(constr_dict), len(list(expected_keys_iterable)))
81+
82+
for key_tuple in expected_keys_iterable:
83+
# Construct the full constraint name as Gurobi creates it
84+
if isinstance(key_tuple, str): # Single key for some constraints
85+
full_constr_name = f"{constr_name_base}[{key_tuple}]"
86+
key_for_data = key_tuple # used for fetching data from dicts
87+
else: # Multiple keys (unit, t)
88+
full_constr_name = (
89+
f"{constr_name_base}[{','.join(map(str, key_tuple))}]"
90+
)
91+
key_for_data = key_tuple
92+
93+
current_constr = self.model.getConstrByName(full_constr_name)
94+
self.assertIsNotNone(
95+
current_constr, f"Constraint {full_constr_name} not found."
96+
)
97+
98+
self.assertEqual(
99+
current_constr.Sense,
100+
expected_sense,
101+
f"Sense mismatch for {full_constr_name}",
102+
)
103+
104+
expected_rhs = get_expected_rhs_func(key_for_data)
105+
self.assertAlmostEqual(
106+
current_constr.RHS,
107+
expected_rhs,
108+
msg=f"RHS mismatch for {full_constr_name}",
109+
)
110+
111+
row = self.model.getRow(current_constr)
112+
expected_lhs_coeffs = get_expected_lhs_coeffs_func(key_for_data)
113+
114+
actual_coeffs = {
115+
row.getVar(i).VarName: row.getCoeff(i) for i in range(row.size())
116+
}
117+
self.assertEqual(
118+
row.size(),
119+
len(expected_lhs_coeffs),
120+
f"LHS term count mismatch for {full_constr_name}",
121+
)
122+
self.assertDictEqual(
123+
actual_coeffs,
124+
expected_lhs_coeffs,
125+
f"LHS coeffs mismatch for {full_constr_name}",
126+
)
127+
128+
def test_add_c_link_ess_charge(self):
129+
constrs = energy_storage_constr.add_c_link_ess_charge(
130+
self.model,
131+
self.pcharge,
132+
self.ucharge,
133+
self.timesteps,
134+
self.storage_units,
135+
self.max_charge_cap,
136+
)
137+
self.model.update()
138+
139+
def get_rhs(key): # key is (unit, t)
140+
return 0.0
141+
142+
def get_lhs(key): # key is (unit, t)
143+
unit, t = key
144+
return {
145+
self.pcharge[unit, t].VarName: 1.0,
146+
self.ucharge[unit, t].VarName: -self.max_charge_cap[unit],
147+
}
148+
149+
expected_keys = [(u, t) for u in self.storage_units for t in self.timesteps]
150+
self._check_constr_details(
151+
"link_ess_charge",
152+
constrs,
153+
expected_keys,
154+
gp.GRB.LESS_EQUAL,
155+
get_rhs,
156+
get_lhs,
157+
)
158+
159+
# Test with empty units
160+
empty_constrs = energy_storage_constr.add_c_link_ess_charge(
161+
self.model,
162+
self.pcharge,
163+
self.ucharge,
164+
self.timesteps,
165+
[],
166+
self.max_charge_cap,
167+
)
168+
self.assertEqual(len(empty_constrs), 0)
169+
170+
def test_add_c_link_ess_discharge(self):
171+
constrs = energy_storage_constr.add_c_link_ess_discharge(
172+
self.model,
173+
self.pdischarge,
174+
self.udischarge,
175+
self.timesteps,
176+
self.storage_units,
177+
self.max_discharge_cap,
178+
)
179+
self.model.update()
180+
181+
def get_rhs(key): # key is (unit, t)
182+
return 0.0
183+
184+
def get_lhs(key): # key is (unit, t)
185+
unit, t = key
186+
return {
187+
self.pdischarge[unit, t].VarName: 1.0,
188+
self.udischarge[unit, t].VarName: -self.max_discharge_cap[unit],
189+
}
190+
191+
expected_keys = [(u, t) for u in self.storage_units for t in self.timesteps]
192+
self._check_constr_details(
193+
"link_ess_discharge",
194+
constrs,
195+
expected_keys,
196+
gp.GRB.LESS_EQUAL,
197+
get_rhs,
198+
get_lhs,
199+
)
200+
201+
# Test with empty units
202+
empty_constrs = energy_storage_constr.add_c_link_ess_discharge(
203+
self.model,
204+
self.pdischarge,
205+
self.udischarge,
206+
self.timesteps,
207+
[],
208+
self.max_discharge_cap,
209+
)
210+
self.assertEqual(len(empty_constrs), 0)
211+
212+
def test_add_c_link_ess_state(self):
213+
constrs = energy_storage_constr.add_c_link_ess_state(
214+
self.model,
215+
self.ucharge,
216+
self.udischarge,
217+
self.timesteps,
218+
self.storage_units,
219+
)
220+
self.model.update()
221+
222+
def get_rhs(key): # key is (unit, t)
223+
return 1.0
224+
225+
def get_lhs(key): # key is (unit, t)
226+
unit, t = key
227+
return {
228+
self.ucharge[unit, t].VarName: 1.0,
229+
self.udischarge[unit, t].VarName: 1.0,
230+
}
231+
232+
expected_keys = [(u, t) for u in self.storage_units for t in self.timesteps]
233+
self._check_constr_details(
234+
"link_ess_state",
235+
constrs,
236+
expected_keys,
237+
gp.GRB.LESS_EQUAL,
238+
get_rhs,
239+
get_lhs,
240+
)
241+
242+
# Test with empty units
243+
empty_constrs = energy_storage_constr.add_c_link_ess_state(
244+
self.model, self.ucharge, self.udischarge, self.timesteps, []
245+
)
246+
self.assertEqual(len(empty_constrs), 0)
247+
248+
def test_add_c_unit_ess_balance_init(self):
249+
# Fixed t=1
250+
t_init = 1
251+
constrs = energy_storage_constr.add_c_unit_ess_balance_init(
252+
self.model,
253+
self.pcharge,
254+
self.pdischarge,
255+
self.charge_state,
256+
self.storage_units,
257+
self.charge_state_init,
258+
self.charge_efficiency,
259+
self.discharge_efficiency,
260+
self.self_discharge_rate,
261+
)
262+
self.model.update()
263+
264+
def get_rhs(unit): # key is unit
265+
return (1 - self.self_discharge_rate[unit]) * self.charge_state_init[unit]
266+
267+
def get_lhs(unit): # key is unit
268+
# charge_state[u,1] - CE[u]*pcharge[u,1] + (1/DE[u])*pdischarge[u,1] == RHS
269+
return {
270+
self.charge_state[unit, t_init].VarName: 1.0,
271+
self.pcharge[unit, t_init].VarName: -self.charge_efficiency[unit],
272+
self.pdischarge[unit, t_init].VarName: 1.0
273+
/ self.discharge_efficiency[unit],
274+
}
275+
276+
# For this constraint, keys are just units as t is fixed to 1 inside the function
277+
expected_keys = [u for u in self.storage_units]
278+
self._check_constr_details(
279+
"unit_ess_balance_init",
280+
constrs,
281+
expected_keys,
282+
gp.GRB.EQUAL,
283+
get_rhs,
284+
get_lhs,
285+
)
286+
287+
# Test with empty units
288+
empty_constrs = energy_storage_constr.add_c_unit_ess_balance_init(
289+
self.model,
290+
self.pcharge,
291+
self.pdischarge,
292+
self.charge_state,
293+
[],
294+
self.charge_state_init,
295+
self.charge_efficiency,
296+
self.discharge_efficiency,
297+
self.self_discharge_rate,
298+
)
299+
self.assertEqual(len(empty_constrs), 0)
300+
301+
def test_add_c_unit_ess_balance(self):
302+
constrs = energy_storage_constr.add_c_unit_ess_balance(
303+
self.model,
304+
self.pcharge,
305+
self.pdischarge,
306+
self.charge_state,
307+
self.storage_units,
308+
self.sim_horizon,
309+
self.charge_efficiency,
310+
self.discharge_efficiency,
311+
self.self_discharge_rate,
312+
)
313+
self.model.update()
314+
315+
def get_rhs(key): # key is (unit, t)
316+
return 0.0
317+
318+
def get_lhs(key): # key is (unit, t)
319+
unit, t = key
320+
# CS[u,t] - (1-SDR[u])*CS[u,t-1] - CE[u]*Pch[u,t] + (1/DE[u])*Pdch[u,t] == 0
321+
return {
322+
self.charge_state[unit, t].VarName: 1.0,
323+
self.charge_state[unit, t - 1].VarName: -(
324+
1 - self.self_discharge_rate[unit]
325+
),
326+
self.pcharge[unit, t].VarName: -self.charge_efficiency[unit],
327+
self.pdischarge[unit, t].VarName: 1.0 / self.discharge_efficiency[unit],
328+
}
329+
330+
# Constraints are for t = 2 to sim_horizon
331+
expected_keys = [
332+
(u, t) for u in self.storage_units for t in range(2, self.sim_horizon + 1)
333+
]
334+
self._check_constr_details(
335+
"unit_ess_balance", constrs, expected_keys, gp.GRB.EQUAL, get_rhs, get_lhs
336+
)
337+
338+
# Test with sim_horizon = 1 (should add no constraints)
339+
constrs_horizon1 = energy_storage_constr.add_c_unit_ess_balance(
340+
self.model,
341+
self.pcharge,
342+
self.pdischarge,
343+
self.charge_state,
344+
self.storage_units,
345+
1,
346+
self.charge_efficiency, # sim_horizon = 1
347+
self.discharge_efficiency,
348+
self.self_discharge_rate,
349+
)
350+
self.assertEqual(len(constrs_horizon1), 0)
351+
352+
# Test with empty units
353+
empty_constrs = energy_storage_constr.add_c_unit_ess_balance(
354+
self.model,
355+
self.pcharge,
356+
self.pdischarge,
357+
self.charge_state,
358+
[],
359+
self.sim_horizon,
360+
self.charge_efficiency,
361+
self.discharge_efficiency,
362+
self.self_discharge_rate,
363+
)
364+
self.assertEqual(len(empty_constrs), 0)
365+
366+
367+
if __name__ == "__main__":
368+
unittest.main()

0 commit comments

Comments
 (0)