Skip to content

Commit 1d4325c

Browse files
CIclaude
andcommitted
test(octopus): pin overlay/feed equivalence and stop the rate_add_io_slots suite leaking fixture state
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 3cb4213 commit 1d4325c

3 files changed

Lines changed: 54 additions & 20 deletions

File tree

‎apps/predbat/tests/test_iog_early_slot_flat_battery.py‎

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,8 @@ def _optimise(my_predbat, charge_windows):
173173
def _run_case(my_predbat, name, source, pv_metric10_weight, overrides=None):
174174
"""
175175
Run one case and print the evidence. Returns (highest charge target in the dispatch-only windows,
176-
PV10 saving of topping up in the first dispatch window before the night charge).
176+
PV10 saving of topping up in the first dispatch window before the night charge, the whole optimised
177+
plan, the future minutes flagged io_adjusted).
177178
178179
The first dispatch window is inside the 30 minutes the PV10 case still trusts, so the top-up costs
179180
the dispatch rate in both scenarios; the saving is what it is worth if the rest of the dispatch
@@ -199,7 +200,7 @@ def _run_case(my_predbat, name, source, pv_metric10_weight, overrides=None):
199200
iog_limits = [limits[n] for n in iog_indices]
200201
night_limits = [limits[n] for n in night_indices]
201202
print(" optimised dispatch limits {} night limits {}".format(iog_limits, night_limits))
202-
return (max(iog_limits) if iog_limits else 0.0), cost10_night - cost10_top_up
203+
return (max(iog_limits) if iog_limits else 0.0), cost10_night - cost10_top_up, limits, flagged
203204

204205

205206
def run_iog_early_slot_flat_battery_tests(my_predbat):
@@ -236,9 +237,9 @@ def run_iog_early_slot_flat_battery_tests(my_predbat):
236237
try:
237238
load_to_night = DEFAULT_SCENARIO["load_kw"] * (DEFAULT_SCENARIO["night_start"] - DEFAULT_SCENARIO["minutes_now"]) / 60.0
238239

239-
slot_path, _ = _run_case(my_predbat, "dispatch from octopus_intelligent_slot", "slot", 0.15)
240-
feed_path, _ = _run_case(my_predbat, "same dispatch flagged by the rate feed", "feed", 0.15)
241-
peak_gap, _ = _run_case(
240+
slot_path, _, slot_plan, slot_flagged = _run_case(my_predbat, "dispatch from octopus_intelligent_slot", "slot", 0.15)
241+
feed_path, _, feed_plan, feed_flagged = _run_case(my_predbat, "same dispatch flagged by the rate feed", "feed", 0.15)
242+
peak_gap, _, _, _ = _run_case(
242243
my_predbat,
243244
"bonus dispatch 19:00-21:00 then peak until 23:30, 17:00 now",
244245
"feed",
@@ -247,8 +248,8 @@ def run_iog_early_slot_flat_battery_tests(my_predbat):
247248
)
248249

249250
car_hold = {"car_kw": 7.0}
250-
_, car_python = _run_case(my_predbat, "slot path, 7 kW car in the dispatch, car_charging_from_battery off", "slot", 0.15, car_hold)
251-
_, car_kernel = _run_case(my_predbat, "same with the C++ kernel", "slot", 0.15, dict(car_hold, kernel=True))
251+
_, car_python, _, _ = _run_case(my_predbat, "slot path, 7 kW car in the dispatch, car_charging_from_battery off", "slot", 0.15, car_hold)
252+
_, car_kernel, _, _ = _run_case(my_predbat, "same with the C++ kernel", "slot", 0.15, dict(car_hold, kernel=True))
252253

253254
if slot_path < load_to_night:
254255
print(" ERROR: octopus_intelligent_slot path: battery targets {} kWh in the dispatch-only slots, needs {} kWh to carry the house to 23:30 if the dispatch goes away".format(slot_path, load_to_night))
@@ -260,6 +261,16 @@ def run_iog_early_slot_flat_battery_tests(my_predbat):
260261
failed += 1
261262
else:
262263
print(" OK: rate feed path charges {} kWh in the dispatch before 23:30".format(feed_path))
264+
# The two paths must agree, not just each clear the bar: the overlay has to flag exactly the
265+
# minutes the feed flags, and the same dispatch must then produce the same plan
266+
if slot_flagged != feed_flagged:
267+
print(" ERROR: io_adjusted differs between paths: overlay flags {} future minutes, feed flags {}".format(len(slot_flagged), len(feed_flagged)))
268+
failed += 1
269+
elif slot_plan != feed_plan:
270+
print(" ERROR: the same dispatch plans differently by path: overlay {} feed {}".format(slot_plan, feed_plan))
271+
failed += 1
272+
else:
273+
print(" OK: overlay and rate feed paths flag the same minutes and plan the same charge")
263274
for engine, saving in (("python", car_python), ("kernel", car_kernel)):
264275
if saving < 1.0:
265276
print(" ERROR: car holding the battery ({}): a top-up in the first dispatch slot saves {:.2f}p in PV10 - a dispatch that goes away must release the hold so it can carry the house to 23:30".format(engine, saving))

‎apps/predbat/tests/test_kernel_parity.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -680,8 +680,10 @@ def run_edge_case_tests(my_predbat):
680680
]
681681

682682
# An Intelligent dispatch assumed gone in PV10: the car keeps charging at the slot rate but no longer
683-
# holds the battery, and joins the grid balance after the battery has acted. Kept last because the
684-
# overrides persist into later cases; io_adjusted is cleared after the loop.
683+
# holds the battery, and joins the grid balance after the battery has acted. Kept last because some
684+
# overrides persist into later cases: reset_inverter() removes the car (num_cars 0, slots cleared) at
685+
# the start of every case, including the pv90 block below, but car_charging_loss and
686+
# car_charging_limit_model stay set, harmless without a car. io_adjusted is cleared after the loop.
685687
dispatch_car = {
686688
"num_cars": 1,
687689
"car_charging_from_battery": False,

‎apps/predbat/tests/test_rate_add_io_slots.py‎

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,38 @@ def run_rate_add_io_slots_tests(my_predbat):
120120
"""
121121
Test for rate_add_io_slots - the function that adds Octopus Intelligent slots to rates
122122
and enforces the 6-hour (12 x 30-min slot) daily limit
123+
124+
Every case overwrites the octopus_slot_* args and, now that the overlay marks the minutes it
125+
lowers, adds to io_adjusted. All tests share one PredBat fixture, so start from a clean
126+
io_adjusted and put everything back afterwards (#5079).
123127
"""
128+
saved_args = {key: my_predbat.args[key] for key in ("octopus_slot_low_rate", "octopus_slot_max") if key in my_predbat.args}
129+
saved_io_adjusted = my_predbat.io_adjusted
130+
saved_effective = my_predbat.dynamic_load_car_effective
131+
original_forecast_minutes = my_predbat.forecast_minutes
132+
my_predbat.io_adjusted = {}
133+
try:
134+
failed = run_rate_add_io_slots_cases(my_predbat)
135+
finally:
136+
for key in ("octopus_slot_low_rate", "octopus_slot_max"):
137+
if key in saved_args:
138+
my_predbat.args[key] = saved_args[key]
139+
else:
140+
my_predbat.args.pop(key, None)
141+
my_predbat.io_adjusted = saved_io_adjusted
142+
my_predbat.dynamic_load_car_effective = saved_effective
143+
my_predbat.forecast_minutes = original_forecast_minutes
144+
145+
if failed:
146+
print("\n**** rate_add_io_slots tests: FAILED ****")
147+
else:
148+
print("\n**** rate_add_io_slots tests: PASSED ****")
149+
150+
return failed
151+
152+
153+
def run_rate_add_io_slots_cases(my_predbat):
154+
"""Run every rate_add_io_slots case; run_rate_add_io_slots_tests() owns the fixture state."""
124155
failed = 0
125156

126157
TIME_FORMAT = "%Y-%m-%dT%H:%M:%S%z"
@@ -129,8 +160,7 @@ def run_rate_add_io_slots_tests(my_predbat):
129160
my_predbat.minutes_now = int((now_utc - my_predbat.midnight_utc).total_seconds() / 60)
130161
midnight_utc = my_predbat.midnight_utc
131162

132-
# Save original forecast_minutes and extend it for multi-day tests
133-
original_forecast_minutes = my_predbat.forecast_minutes
163+
# Extend forecast_minutes for multi-day tests
134164
my_predbat.forecast_minutes = 3 * 24 * 60 # 3 days
135165

136166
reset_rates(my_predbat, 10, 5)
@@ -575,13 +605,4 @@ def run_rate_add_io_slots_tests(my_predbat):
575605
failed |= run_rate_add_io_slots_test("test25_completed_away_consumes_cap", my_predbat, slots, True, 2, expected_rates)
576606

577607
failed |= run_rate_add_io_slots_flag_tests(my_predbat, midnight_utc, TIME_FORMAT)
578-
579-
# Restore original forecast_minutes
580-
my_predbat.forecast_minutes = original_forecast_minutes
581-
582-
if failed:
583-
print("\n**** rate_add_io_slots tests: FAILED ****")
584-
else:
585-
print("\n**** rate_add_io_slots tests: PASSED ****")
586-
587608
return failed

0 commit comments

Comments
 (0)