Skip to content

Commit def0b1e

Browse files
committed
Add unit tests
1 parent fa12be3 commit def0b1e

File tree

4 files changed

+2146
-1
lines changed

4 files changed

+2146
-1
lines changed

src/pownet/optim_model/constraints/system_constr.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import gurobipy as gp
44
import pandas as pd
5-
import networkx as nx
65

76
from pownet.data_utils import get_capacity_value
87

src/test_pownet/test_coupler.py

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
"test_coupler.py"
2+
3+
import unittest
4+
from unittest.mock import MagicMock, PropertyMock, call
5+
import logging
6+
7+
# Assuming coupler.py is in the same directory or accessible via PYTHONPATH
8+
from pownet.coupler import PowerWaterCoupler
9+
10+
# For type hinting if needed
11+
from pownet import ModelBuilder as ActualModelBuilder
12+
from pownet.reservoir import ReservoirManager as ActualReservoirManager
13+
14+
# Disable logging for cleaner test output
15+
logging.disable(logging.CRITICAL)
16+
17+
18+
class TestPowerWaterCoupler(unittest.TestCase):
19+
20+
def setUp(self):
21+
"""Common setup for all tests."""
22+
self.mock_model_builder = MagicMock(spec=ActualModelBuilder)
23+
self.mock_reservoir_manager = MagicMock(spec=ActualReservoirManager)
24+
25+
# --- Configure ModelBuilder Mock ---
26+
mock_mb_inputs = MagicMock()
27+
mock_mb_inputs.sim_horizon = 48 # Allows num_days_in_step = 2
28+
type(self.mock_model_builder).inputs = PropertyMock(return_value=mock_mb_inputs)
29+
30+
self.mock_power_system_model = MagicMock()
31+
self.mock_power_system_model.get_runtime.return_value = 0.5
32+
self.mock_model_builder.update_daily_hydropower_capacity.return_value = (
33+
self.mock_power_system_model
34+
)
35+
self.mock_model_builder.get_phydro.return_value = {}
36+
37+
# --- Configure ReservoirManager Mock ---
38+
self.mock_reservoir_manager.simulation_order = ["H1", "H2"]
39+
self.mock_reservoir_manager.reoperate.return_value = {}
40+
41+
self.coupler = PowerWaterCoupler(
42+
model_builder=self.mock_model_builder,
43+
reservoir_manager=self.mock_reservoir_manager,
44+
solver="test_solver",
45+
mip_gap=0.001,
46+
timelimit=300,
47+
log_to_console=False,
48+
)
49+
# num_days_in_step should be 2 based on sim_horizon = 48
50+
self.assertEqual(self.coupler.num_days_in_step, 2)
51+
52+
def _create_mock_dispatch_var(self, value):
53+
mock_var = MagicMock()
54+
mock_var.X = value
55+
return mock_var
56+
57+
def test_initialization(self):
58+
self.assertIs(self.coupler.model_builder, self.mock_model_builder)
59+
self.assertEqual(self.coupler.num_days_in_step, 2)
60+
self.assertEqual(self.coupler.reop_iter, [])
61+
self.assertEqual(self.coupler.reop_opt_time, 0.0)
62+
63+
def test_getters(self):
64+
self.coupler.reop_opt_time = 10.5
65+
self.assertEqual(self.coupler.get_reop_opt_time(), 10.5)
66+
self.coupler.reop_iter = [1, 2, 3]
67+
self.assertEqual(self.coupler.get_reop_iter(), [1, 2, 3])
68+
69+
def test_reoperate_converges_immediately_multi_day(self):
70+
step_k = 10 # 1-indexed global start day
71+
# num_days_in_step is 2, so days_in_step = range(10, 12) -> global days 10, 11
72+
73+
# Hydropower dispatch from ModelBuilder (hourly, spanning 48 hours)
74+
# varname[1] is 1-indexed hour in ModelBuilder's 48h horizon
75+
initial_phydro = {
76+
# Day step_k (Global Day 10)
77+
("H1", 1): self._create_mock_dispatch_var(1.0), # Hour 1, Day 10
78+
("H1", 24): self._create_mock_dispatch_var(1.0), # Hour 24, Day 10
79+
("H2", 1): self._create_mock_dispatch_var(0.5), # Hour 1, Day 10
80+
("H2", 24): self._create_mock_dispatch_var(0.5), # Hour 24, Day 10
81+
# Day step_k + 1 (Global Day 11)
82+
("H1", 25): self._create_mock_dispatch_var(2.0), # Hour 25, Day 11
83+
("H1", 48): self._create_mock_dispatch_var(2.0), # Hour 48, Day 11
84+
("H2", 25): self._create_mock_dispatch_var(1.5), # Hour 25, Day 11
85+
("H2", 48): self._create_mock_dispatch_var(1.5), # Hour 48, Day 11
86+
}
87+
# Assuming 1.0 MW for all 24h of Day 10 for H1, 0.5 MW for H2 on Day 10
88+
# Assuming 2.0 MW for all 24h of Day 11 for H1, 1.5 MW for H2 on Day 11
89+
h1_day10_total = 24 * 1.0
90+
h2_day10_total = 24 * 0.5
91+
h1_day11_total = 24 * 2.0
92+
h2_day11_total = 24 * 1.5
93+
94+
# Populate full hourly data for mock
95+
current_phydro_mock = {}
96+
for h in range(1, 25): # Global Day step_k
97+
current_phydro_mock[("H1", h)] = self._create_mock_dispatch_var(1.0)
98+
current_phydro_mock[("H2", h)] = self._create_mock_dispatch_var(0.5)
99+
for h in range(25, 49): # Global Day step_k + 1
100+
current_phydro_mock[("H1", h)] = self._create_mock_dispatch_var(2.0)
101+
current_phydro_mock[("H2", h)] = self._create_mock_dispatch_var(1.5)
102+
self.mock_model_builder.get_phydro.return_value = current_phydro_mock
103+
104+
proposed_capacity_match = {
105+
("H1", step_k): h1_day10_total, # Global Day 10
106+
("H2", step_k): h2_day10_total, # Global Day 10
107+
("H1", step_k + 1): h1_day11_total, # Global Day 11
108+
("H2", step_k + 1): h2_day11_total, # Global Day 11
109+
}
110+
self.mock_reservoir_manager.reoperate.return_value = proposed_capacity_match
111+
112+
self.coupler.reoperate(step_k=step_k)
113+
114+
expected_daily_dispatch = (
115+
proposed_capacity_match # In this case, they are identical
116+
)
117+
self.mock_reservoir_manager.reoperate.assert_called_once_with(
118+
daily_dispatch=expected_daily_dispatch,
119+
days_in_step=range(step_k, step_k + 2), # Global days 10, 11
120+
)
121+
self.mock_model_builder.update_daily_hydropower_capacity.assert_called_once_with(
122+
step_k=step_k, new_capacity=proposed_capacity_match
123+
)
124+
self.assertEqual(self.coupler.reop_opt_time, 0.5)
125+
self.assertEqual(self.coupler.reop_iter, [1])
126+
127+
def test_reoperate_converges_after_iterations_single_day_focus(self):
128+
"""Test convergence over iterations, focusing on one day for simplicity."""
129+
step_k = 50
130+
# Temporarily override sim_horizon for this test to focus on single day num_days_in_step = 1
131+
mock_mb_inputs_24h = MagicMock()
132+
mock_mb_inputs_24h.sim_horizon = 24
133+
type(self.mock_model_builder).inputs = PropertyMock(
134+
return_value=mock_mb_inputs_24h
135+
)
136+
# Re-initialize coupler with this specific setup for num_days_in_step = 1
137+
coupler_single_day = PowerWaterCoupler(
138+
model_builder=self.mock_model_builder,
139+
reservoir_manager=self.mock_reservoir_manager,
140+
)
141+
self.assertEqual(coupler_single_day.num_days_in_step, 1)
142+
self.mock_reservoir_manager.simulation_order = ["H1"]
143+
144+
# Iteration 1: PowNet: 100 (sum for day step_k), Reservoir: 120
145+
phydro_iter1_hourly = {
146+
("H1", h): self._create_mock_dispatch_var(100 / 24) for h in range(1, 25)
147+
}
148+
capacity_iter1 = {("H1", step_k): 120.0}
149+
150+
# Iteration 2: PowNet: 115, Reservoir: 116 (converged: |116-115|=1 <= 0.05*115=5.75)
151+
phydro_iter2_hourly = {
152+
("H1", h): self._create_mock_dispatch_var(115 / 24) for h in range(1, 25)
153+
}
154+
capacity_iter2 = {("H1", step_k): 116.0}
155+
156+
self.mock_model_builder.get_phydro.side_effect = [
157+
phydro_iter1_hourly,
158+
phydro_iter2_hourly,
159+
]
160+
self.mock_reservoir_manager.reoperate.side_effect = [
161+
capacity_iter1,
162+
capacity_iter2,
163+
]
164+
165+
initial_optimize_call_count = self.mock_power_system_model.optimize.call_count
166+
167+
coupler_single_day.reoperate(step_k=step_k)
168+
169+
# --- MODIFIED ASSERTION START ---
170+
self.assertEqual(
171+
len(self.mock_reservoir_manager.reoperate.call_args_list),
172+
2,
173+
"Incorrect number of calls to reoperate",
174+
)
175+
176+
# Expected conceptual values for dispatch sums
177+
expected_dispatch_val1 = 100.0
178+
expected_dispatch_val2 = 115.0
179+
180+
# Check Call 1
181+
call1_args, call1_kwargs = self.mock_reservoir_manager.reoperate.call_args_list[
182+
0
183+
]
184+
self.assertEqual(call1_kwargs["days_in_step"], range(step_k, step_k + 1))
185+
self.assertIn(("H1", step_k), call1_kwargs["daily_dispatch"])
186+
self.assertAlmostEqual(
187+
call1_kwargs["daily_dispatch"][("H1", step_k)],
188+
expected_dispatch_val1,
189+
places=7, # Default is 7, adjust if needed
190+
msg="Mismatch in daily_dispatch for call 1",
191+
)
192+
193+
# Check Call 2
194+
call2_args, call2_kwargs = self.mock_reservoir_manager.reoperate.call_args_list[
195+
1
196+
]
197+
self.assertEqual(call2_kwargs["days_in_step"], range(step_k, step_k + 1))
198+
self.assertIn(("H1", step_k), call2_kwargs["daily_dispatch"])
199+
self.assertAlmostEqual(
200+
call2_kwargs["daily_dispatch"][("H1", step_k)],
201+
expected_dispatch_val2,
202+
places=7, # Default is 7, adjust if needed
203+
msg="Mismatch in daily_dispatch for call 2",
204+
)
205+
# --- MODIFIED ASSERTION END ---
206+
207+
self.assertEqual(coupler_single_day.reop_iter, [2])
208+
self.assertEqual(
209+
self.mock_power_system_model.optimize.call_count
210+
- initial_optimize_call_count,
211+
2,
212+
)
213+
214+
def test_reoperate_max_iterations_reached(self):
215+
step_k = 300
216+
test_max_reop_iter = 5 # Use a small number for this test
217+
expected_optimize_calls = test_max_reop_iter + 1
218+
219+
# Reset call count before this specific test action
220+
self.mock_power_system_model.optimize.reset_mock() # Reset call count and other call attributes
221+
222+
self.mock_reservoir_manager.simulation_order = ["H1"]
223+
224+
daily_target_dispatch_sum = sum(
225+
100 / 24 for _ in range(24)
226+
) # ~100.00000000000003
227+
phydro_hourly_no_converge = {}
228+
for h in range(1, 25): # Day step_k
229+
phydro_hourly_no_converge[("H1", h)] = self._create_mock_dispatch_var(
230+
daily_target_dispatch_sum / 24
231+
)
232+
if self.coupler.num_days_in_step > 1: # Check if default coupler is multi-day
233+
for h in range(
234+
25, 49
235+
): # Day step_k+1 (assuming sim_horizon=48 for self.coupler)
236+
phydro_hourly_no_converge[("H1", h)] = self._create_mock_dispatch_var(
237+
daily_target_dispatch_sum / 24
238+
)
239+
240+
self.mock_model_builder.get_phydro.return_value = phydro_hourly_no_converge
241+
242+
# Reservoir proposes something far off for day step_k
243+
reoperate_return_value = {("H1", step_k): 50.0}
244+
if self.coupler.num_days_in_step > 1:
245+
reoperate_return_value[("H1", step_k + 1)] = (
246+
daily_target_dispatch_sum # Matches for day step_k+1
247+
)
248+
249+
self.mock_reservoir_manager.reoperate.return_value = reoperate_return_value
250+
251+
with self.assertRaisesRegex(
252+
ValueError,
253+
f"Reservoirs reoperation did not converge after {test_max_reop_iter} iterations",
254+
):
255+
self.coupler.reoperate(step_k=step_k, max_reop_iter=test_max_reop_iter)
256+
257+
self.assertEqual(
258+
self.mock_power_system_model.optimize.call_count, expected_optimize_calls
259+
)
260+
self.assertEqual(
261+
self.coupler.reop_iter, []
262+
) # Should not append if error raised
263+
264+
def test_hydropower_dispatch_aggregation_multi_day(self):
265+
step_k = 1
266+
phydro_data = {}
267+
for h_model in range(1, 25):
268+
phydro_data[("H1", h_model)] = self._create_mock_dispatch_var(1.0)
269+
for h_model in range(25, 49):
270+
phydro_data[("H1", h_model)] = self._create_mock_dispatch_var(2.0)
271+
phydro_data[("H2", 1)] = self._create_mock_dispatch_var(0.0)
272+
self.mock_model_builder.get_phydro.return_value = phydro_data
273+
self.mock_reservoir_manager.simulation_order = ["H1", "H2"]
274+
275+
self.mock_reservoir_manager.reoperate.return_value = {
276+
("H1", step_k): 24.0,
277+
("H1", step_k + 1): 48.0,
278+
("H2", step_k): 0.0,
279+
("H2", step_k + 1): 0.0,
280+
}
281+
282+
# This test uses the default max_reop_iter = 100
283+
self.coupler.reoperate(step_k=step_k)
284+
285+
expected_dispatch_to_reservoir = {
286+
("H1", step_k): 24.0,
287+
("H1", step_k + 1): 48.0,
288+
("H2", step_k): 0.0,
289+
("H2", step_k + 1): 0.0,
290+
}
291+
self.mock_reservoir_manager.reoperate.assert_called_once_with(
292+
daily_dispatch=expected_dispatch_to_reservoir,
293+
days_in_step=range(step_k, step_k + 2),
294+
)
295+
self.assertEqual(self.coupler.reop_iter, [1])
296+
297+
298+
if __name__ == "__main__":
299+
unittest.main(argv=["first-arg-is-ignored"], exit=False)

0 commit comments

Comments
 (0)