Commit 24fde5a
feat(plan): model-based clipping — prune plan slots that are dead in the central forecast (#4491)
* feat(plan): model-based clipping - prune plan slots dead in the central forecast
Follow-up to #4487. The heuristic clip branches each target one specific
failure shape (battery pinned empty, target unreachable, SoC flat above
limit), and #4453/#4478 showed new shapes keep appearing: every gate in the
pipeline is tuned for a different pathology, and dead slots thread between
them. Replace the guessing with a direct question to the model: remove the
slot, re-simulate, did the central forecast change?
prune_dead_plan_slots trials each active charge/export slot inside the
record window in turn - the slot is removed and the whole plan re-simulated
in the nominal (50%) scenario only, one simulation per trial via a new
run_prediction_metric(nominal_only=True) option (skips pv10 and pv90). The
removal is kept when the nominal metric does not get worse, so slots whose
value exists only in the pessimistic branches - or nowhere at all - are
dropped. If the pv10/pv90 conditions materialise in reality, the next plan
recompute re-creates a genuine slot from actual state.
The pass runs after the pre-clip scoring snapshot, so plan selection still
compares plans as optimised (#4403). In-progress windows are never trialled
(the #4402 commitment - the clip_export_slots phantom branch from #4487
remains as the complementary catch for that case), manual windows are
preserved, and each accepted removal updates the running baseline so one
removal cannot make the next look free.
Random benchmark over 20 scenarios against main: 114 slots pruned, nominal
cost of the executed plan never worse than +0.012p (the per-trial epsilon)
and dramatically better on two scenarios (-35.18p, -71.61p - plans were
carrying slots that cost real money in the central forecast for pessimistic
-branch insurance); the pv-weighted metric of the executed plan gives back
+0.62p on average (max +5.05p) where pure insurance was stripped, which is
the designed trade. Plan runtime unchanged (1.33s avg both sides).
Debug case expected files regenerated: agile1's pruned plan is 1.31p
cheaper nominally (metric-neutral), pre_saving1's is cost-identical with a
+0.21p pv10 residual.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(plan): drop the clip removal branches now subsumed by the prune
prune_dead_plan_slots decides whether a slot achieves anything by asking the
model, which makes the trajectory-based removal heuristics in clip_charge_slots
and clip_export_slots redundant. Instrumenting every branch across the 20
scenario benchmark shows the removal branches fire 31 times with the prune
disabled and 0 times with it enabled, while both clip-up branches keep firing
unchanged (111 fires). The same holds on the real captures from #4453/#4478.
Removed: freeze-export-at-100%, no-SoC-above-reserve (#4171/#4434), phantom
export (#4453/#4487), export target-unreachable, and charge
never-reaches-limit. What remains in both functions is limit adjustment only -
narrowing a requested limit to what the window can actually achieve, so the
target sent to the inverter matches the simulated plan and adjacent windows
merge. The charge freeze-to-charge-at-100% conversion is kept: it rewrites a
limit rather than removing a window, and covers windows the prune skips.
The prune deliberately does not trial a window covering the current minute
(#4402), so an in-progress phantom is no longer converted in place; it is
re-planned on the next cycle from real inverter state instead.
Random benchmark over 20 scenarios before vs after this removal: plan metric
and cost identical 20/20, runtime unchanged. Debug cases pass unchanged - the
expected files regenerated for the prune itself needed no further update, which
is independent evidence the branches were dead.
Tests that asserted the removed behaviour are rewritten to assert the new
contract (clipping adjusts limits and never removes); the removal behaviour
they covered is now exercised by the prune tests and the debug case regression.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(plan): let the prune trial in-progress slots too
The prune skipped any window covering the current minute, on the grounds that
#4402 forbids cancelling an in-progress export. That over-applied the guard:
#4402's regression came from writing back a change scored on optimise_export's
adjusted metric (commitment bonus plus tie-break weightings) with no check that
the whole plan improved, and its fix was to gate on the unadjusted whole-plan
metric - exactly what the prune trial already uses. An in-progress export worth
anything fails that gate and is kept.
Skipping it also left the worst gap: the in-progress window is the slot being
executed right now, so a dead one there is precisely the spurious command that
reaches the inverter - and since the clip removal branches were dropped in
favour of the prune, nothing covered it at all.
Probing the 20 scenario benchmark, all 15 in-progress slots are worth real
money to keep (+0.41p to +25.91p if removed), so none are pruned and the plans
are identical 20/20 on metric and cost. Runtime +0.03s average for the extra
trials.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(random): start scenarios part way through a plan slot
Every scenario ran from the template's minutes_now of 08:00, exactly on a
30 minute boundary, so a window covering the current minute always had its
full length remaining. Partially-elapsed slots - the shape that matters for
in-progress pruning and for anything that reasons about the remainder of the
window being executed - were never generated.
Scenarios now carry a clock offset sampled from 0/5/10/15/20/25 minutes (5
minutes being predbat's run cadence) and start that far into the slot. The
offset is drawn last in the generator's random sequence, so re-generating an
existing seed leaves every other parameter unchanged, and it is applied from
the slot boundary rather than the current clock so that applying scenarios in
a loop cannot accumulate offsets.
Scenario files written before this carry no "clock" entry and keep the
template's own minutes_now: re-running the committed cases/random_scenarios.yaml
gives metric and cost identical to before on all 20, so previously recorded
benchmark results stay comparable.
A freshly generated set has 16 of 20 scenarios starting mid-slot and produces
in-progress windows with 5, 15 and 25 minutes remaining (previously always 30).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(random): randomise scenario start time, and cover the whole PV horizon
Replaces the within-slot clock offset with a random minute-of-day (still on
predbat's 5 minute cadence), so scenarios start at every point of the tariff
and solar day - overnight cheap windows, the evening peak, mid-generation -
rather than all at the template's 08:00. Start times in the regenerated set
span 00:10 to 22:10 with 15 of 20 landing off a 30 minute boundary, so
partially-elapsed slots are now generated as a matter of course.
Fixes a harness bug the randomisation would otherwise have made much worse:
step_data_history reads forward series at (minute + minutes_now), but
expand_pv_forecast only generated 0..forecast_minutes, so the tail of every
horizon silently had no PV at all - 8 hours' worth even at the fixed 08:00
start, and nearly the whole horizon for a scenario starting late in the day.
It now runs to minutes_now + forecast_minutes.
cases/random_scenarios.yaml is regenerated, which resets the benchmark
baseline: the scenarios are substantially richer (mean optimise time 1.3s ->
12.9s, because the horizon now actually contains solar), so results are not
comparable with runs recorded before this commit.
Re-measured on the new set, prune vs the pre-prune base: nominal cost better
by 3.36p on average (best -43.21p, worst +0.06p, which is within the
accumulated per-trial epsilon), pv-weighted metric +0.25p on average as pure
pessimistic-branch insurance is stripped, optimise time +0.8s (6%).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>1 parent 4bb7841 commit 24fde5a
9 files changed
Lines changed: 25187 additions & 24817 deletions
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
20 | | - | |
| 20 | + | |
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
| |||
122 | 122 | | |
123 | 123 | | |
124 | 124 | | |
125 | | - | |
126 | | - | |
127 | | - | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
128 | 129 | | |
129 | 130 | | |
130 | 131 | | |
131 | 132 | | |
132 | 133 | | |
133 | 134 | | |
134 | | - | |
| 135 | + | |
135 | 136 | | |
136 | 137 | | |
137 | 138 | | |
138 | 139 | | |
139 | | - | |
140 | | - | |
141 | | - | |
142 | | - | |
143 | | - | |
| 140 | + | |
| 141 | + | |
144 | 142 | | |
145 | 143 | | |
146 | 144 | | |
| |||
457 | 455 | | |
458 | 456 | | |
459 | 457 | | |
460 | | - | |
| 458 | + | |
461 | 459 | | |
462 | 460 | | |
463 | 461 | | |
464 | 462 | | |
465 | 463 | | |
466 | 464 | | |
467 | | - | |
| 465 | + | |
468 | 466 | | |
469 | 467 | | |
470 | 468 | | |
| |||
476 | 474 | | |
477 | 475 | | |
478 | 476 | | |
479 | | - | |
480 | | - | |
481 | | - | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
482 | 480 | | |
483 | 481 | | |
484 | 482 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
18 | | - | |
19 | | - | |
| 18 | + | |
20 | 19 | | |
21 | | - | |
| 20 | + | |
22 | 21 | | |
23 | | - | |
24 | 22 | | |
25 | | - | |
26 | | - | |
27 | | - | |
28 | | - | |
| 23 | + | |
29 | 24 | | |
30 | 25 | | |
31 | 26 | | |
| |||
64 | 59 | | |
65 | 60 | | |
66 | 61 | | |
67 | | - | |
68 | | - | |
69 | | - | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
70 | 66 | | |
71 | 67 | | |
72 | 68 | | |
| |||
77 | 73 | | |
78 | 74 | | |
79 | 75 | | |
80 | | - | |
81 | | - | |
82 | | - | |
83 | | - | |
84 | | - | |
85 | | - | |
86 | | - | |
87 | | - | |
88 | | - | |
89 | | - | |
90 | | - | |
91 | | - | |
92 | | - | |
93 | | - | |
94 | | - | |
95 | | - | |
96 | | - | |
97 | | - | |
98 | | - | |
99 | | - | |
100 | | - | |
101 | | - | |
102 | | - | |
103 | | - | |
104 | | - | |
105 | | - | |
106 | | - | |
107 | | - | |
108 | | - | |
109 | 76 | | |
110 | | - | |
111 | | - | |
112 | | - | |
113 | | - | |
| 77 | + | |
114 | 78 | | |
115 | 79 | | |
116 | 80 | | |
| |||
141 | 105 | | |
142 | 106 | | |
143 | 107 | | |
144 | | - | |
145 | | - | |
146 | | - | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
147 | 113 | | |
148 | 114 | | |
149 | 115 | | |
150 | 116 | | |
151 | 117 | | |
152 | | - | |
153 | | - | |
154 | | - | |
| 118 | + | |
| 119 | + | |
155 | 120 | | |
156 | 121 | | |
157 | 122 | | |
158 | | - | |
159 | | - | |
160 | | - | |
161 | | - | |
162 | | - | |
| 123 | + | |
| 124 | + | |
163 | 125 | | |
164 | 126 | | |
165 | 127 | | |
| |||
191 | 153 | | |
192 | 154 | | |
193 | 155 | | |
194 | | - | |
195 | | - | |
196 | | - | |
197 | | - | |
198 | | - | |
199 | | - | |
200 | | - | |
201 | | - | |
202 | | - | |
203 | | - | |
204 | | - | |
205 | | - | |
206 | | - | |
207 | | - | |
208 | | - | |
209 | | - | |
210 | | - | |
211 | | - | |
212 | | - | |
213 | | - | |
214 | | - | |
215 | | - | |
216 | | - | |
217 | | - | |
218 | | - | |
219 | | - | |
220 | | - | |
221 | | - | |
222 | 156 | | |
223 | 157 | | |
224 | 158 | | |
| |||
244 | 178 | | |
245 | 179 | | |
246 | 180 | | |
247 | | - | |
248 | | - | |
249 | | - | |
250 | | - | |
251 | | - | |
252 | | - | |
253 | | - | |
254 | | - | |
255 | | - | |
256 | | - | |
257 | | - | |
258 | | - | |
259 | | - | |
260 | | - | |
261 | | - | |
262 | | - | |
263 | | - | |
264 | | - | |
265 | | - | |
266 | | - | |
267 | | - | |
268 | | - | |
269 | | - | |
270 | | - | |
271 | | - | |
272 | | - | |
273 | | - | |
274 | | - | |
275 | | - | |
276 | | - | |
277 | | - | |
278 | | - | |
279 | | - | |
280 | | - | |
281 | | - | |
282 | | - | |
283 | | - | |
284 | | - | |
285 | | - | |
286 | | - | |
287 | | - | |
288 | | - | |
289 | | - | |
290 | | - | |
291 | | - | |
292 | | - | |
293 | | - | |
294 | | - | |
295 | | - | |
296 | | - | |
297 | | - | |
298 | | - | |
299 | | - | |
300 | | - | |
301 | | - | |
302 | | - | |
303 | | - | |
304 | | - | |
305 | | - | |
306 | | - | |
307 | | - | |
308 | | - | |
309 | | - | |
310 | | - | |
311 | | - | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
312 | 185 | | |
313 | 186 | | |
314 | | - | |
315 | 187 | | |
316 | 188 | | |
317 | 189 | | |
318 | | - | |
319 | | - | |
| 190 | + | |
| 191 | + | |
320 | 192 | | |
321 | 193 | | |
322 | 194 | | |
323 | | - | |
324 | | - | |
| 195 | + | |
| 196 | + | |
325 | 197 | | |
326 | | - | |
327 | | - | |
328 | | - | |
329 | | - | |
330 | | - | |
331 | | - | |
332 | | - | |
333 | | - | |
334 | | - | |
335 | | - | |
336 | | - | |
337 | | - | |
338 | | - | |
339 | | - | |
340 | | - | |
341 | | - | |
342 | | - | |
343 | | - | |
344 | | - | |
345 | | - | |
346 | | - | |
347 | | - | |
348 | | - | |
349 | | - | |
350 | | - | |
351 | | - | |
| 198 | + | |
| 199 | + | |
352 | 200 | | |
353 | 201 | | |
354 | 202 | | |
| |||
411 | 259 | | |
412 | 260 | | |
413 | 261 | | |
414 | | - | |
415 | | - | |
416 | | - | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
417 | 265 | | |
418 | 266 | | |
419 | 267 | | |
| |||
427 | 275 | | |
428 | 276 | | |
429 | 277 | | |
430 | | - | |
431 | | - | |
432 | | - | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
433 | 281 | | |
434 | 282 | | |
435 | | - | |
| 283 | + | |
436 | 284 | | |
437 | 285 | | |
438 | 286 | | |
| |||
0 commit comments