WhatIf fast mode: plan four seasonal months, estimate the rest - #4602
Conversation
There was a problem hiding this comment.
Pull request overview
Adds an optional “fast mode” path to the WhatIf annual predictor to reduce runtime by planning only four anchor months and interpolating the remaining months from the already-fetched annual solar curve, with a volatility guard to auto-fallback to a full 12‑month plan on highly variable tariffs (e.g., Agile). This touches the annual engine, CLI, web UI, test suite, and documentation to expose/record the mode and to ensure interpolated months are included consistently in totals and visualisations.
Changes:
- Add
fast_modeplumbing across config validation, CLI (--fast), and web UI (Advanced checkbox), and persist the flag into run summaries. - Implement fast-mode execution in
AnnualPredictor.run()(anchor month planning + interpolation + volatility guard), plus shared “included statuses” for interpolated months. - Add/extend unit tests and ship reduced reference fixtures to lock in the volatility-guard decision and interpolation accuracy claims.
Reviewed changes
Copilot reviewed 16 out of 18 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/superpowers/specs/2026-08-18-annual-fast-mode-design.md | New design/spec for fast mode, anchors, interpolation basis, and volatility guard. |
| docs/superpowers/plans/2026-08-18-annual-fast-mode.md | New implementation plan/checklist for adding fast mode end-to-end. |
| docs/annual-prediction.md | User-facing documentation for fast mode behaviour, caveats, and CLI flag. |
| coverage/cases/annual_reference_cosy.json | Reduced reference fixture used to validate fast-mode guard and reconstruction accuracy. |
| coverage/cases/annual_reference_agile.json | Reduced reference fixture used to validate fast-mode guard (should decline) and accuracy constraints. |
| apps/predbat/web_annual.py | UI: add fast-mode checkbox, show run details, and render/interpolate months in table + chart via INCLUDED_STATUSES. |
| apps/predbat/unit_test.py | Register new tests (web fast mode, interpolation, curve reference, CLI fast flag). |
| apps/predbat/tests/test_web_annual.py | Add tests for checkbox round-trip, interpolated month rendering, and run-details reporting. |
| apps/predbat/tests/test_annual_results.py | Ensure interpolated months are treated as included in annual totals via INCLUDED_STATUSES. |
| apps/predbat/tests/test_annual_interpolate.py | Add interpolation/guard unit tests (affine fit, clamping, fallbacks, volatility metric). |
| apps/predbat/tests/test_annual_curve_reference.py | Add fixture-driven regression test for volatility guard + reconstruction accuracy. |
| apps/predbat/tests/test_annual_config.py | Add config validation tests for fast_mode default and string coercion. |
| apps/predbat/tests/test_annual_cli.py | Add CLI tests for --fast override behaviour. |
| apps/predbat/annual.py | Engine changes: fast_mode validation, fast-mode flow in run(), volatility guard, interpolation assembly, and INCLUDED_STATUSES usage. |
| apps/predbat/annual_weather.py | Add WeatherYear.monthly_actual_kwh() accessor for the monthly solar curve. |
| apps/predbat/annual_store.py | Persist fast_mode into stored run summaries for history/compare UI. |
| apps/predbat/annual_interpolate.py | New pure interpolation module (solar-affine fit + cyclic linear fallback + volatility metric). |
| apps/predbat/annual_cli.py | Add --fast flag and config override helper; include interpolated months in CLI formatting via INCLUDED_STATUSES. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Plan four seasonal anchor months and reconstruct the other eight from the solar curve the run already downloads, cutting a WhatIf run to about 2.5x faster. Curve chosen from a measured study over three recovered 12-month reference runs: affine in solar (per-day = a + b*PV) beats cyclic linear, Fourier and proportional-to-solar on per-month error, and is insensitive to which months are anchored. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five 12-month references scored (3x Intelligent Go, Cosy, Agile, plus an Agile run at 6 samples/month as low-noise ground truth). Confirms solar_affine as the default and rejects the mean-rate regressor: against noisy references it looked best, halving Agile's annual error, but leave-one-anchor-out cross-validation shows it overfits badly (362% vs 38% held-out error on Agile) because 4 anchors against 3 parameters leaves one degree of freedom. Decomposing Agile's error shows interpolation contributes under 1% to annual savings; the large Agile figures are sampling noise already present in today's 12-month run (-8.5%/+17.8% against the 6-sample reference), which fast mode inherits rather than creates. Recorded as a pre-existing limitation bounding what the caveat text may claim. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Seven tasks: config/CLI flag, the pure interpolation module, a shared included-status constant, engine wiring, web UI, a committed curve-scoring guard, and docs. Also commits the two reduced reference fixtures the curve guard scores against, built from the Agile (6 samples/month) and Cosy 12-month runs. Reduced to months, scenarios and monthly PV only - no plans, location, tariff URLs or keys. Verified solar_affine still beats linear on both (agile 18.39% vs 18.84%, cosy 9.56% vs 14.47%). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nsumers Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t mode Anchors are Mar/Jun/Sep/Dec, which measurably beat Jan/Apr/Jul/Oct on the savings figures (Cosy: -0.5%/+0.2% against -10.6% on the Predbat saving). Adds a volatility guard. Interpolation only holds while a month's economics follow the solar curve; on a tariff whose whole price level moves day to day the reconstructed savings are 20-30% out. The run measures the coefficient of variation of daily average import price over the anchor months and, above 0.10, silently becomes a full run rather than reporting a fast wrong number. Measured: Agile 0.21, Cosy 0.005. Verified end to end: Agile --fast produces a bit-identical full run (+0.0%/+0.0%), Cosy --fast reconstructs payback to 5.84 vs 5.81 years. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nths Checkbox in Advanced, parsed like the existing debug flag. Interpolated months are tagged in the month table and noted above the chart, following the rates-synthesised precedent, so an estimated month is never mistaken for a planned one. 'What this run used' distinguishes a run that used fast mode from one that asked for it and was given a full run instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… runs Asserts the volatility guard sorts the reference tariffs correctly (Agile 0.222 declined, Cosy 0.002 accepted) and that on the tariff it passes, four anchors reconstruct the savings to 0.48%/0.19%, with solar_affine beating linear on the Predbat saving (0.19% vs 4.38%) - which is why it is the default. Fixtures carry daily mean import rates so the guard threshold is regression-tested, not just the curve. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
User docs gain a Fast mode section covering what it does, its measured accuracy, when to turn it off, and that volatile tariffs get a full run automatically; plus the --fast flag and the config key. Corrects two things the spec got wrong before implementation. Anchors are Mar/Jun/Sep/Dec, not Jan/Apr/Jul/Oct: per-month cost error does not separate the candidates but the savings figures do, sharply, because a saving is a difference between two reconstructed scenarios. And the 'under 1% on annual savings' claim was a mean across anchor sets that hid a worst case of 33%. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- apply_fast_override no longer assumes the loaded YAML is a mapping. An empty config file loads as None, so --fast died with a bare TypeError before validate_config could explain the problem. Covered by a test. - fetch_month memoises months it has already loaded. It never did, and the month loop asks for most months twice anyway (month N, then N+1 for the 48 hour plan spill), so every repeat re-read the cached rows and rebuilt the stamped-rate structure for nothing. Fast mode's tariff check made that worse by asking for the anchors up front. Verified a real run is byte-identical with it in place. - Correct stale (1, 4, 7, 10) anchors in the _cyclic_linear docstring, the spec's module code block, and the plan's constraints and code sample. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
b3b36c9 to
dd06d00
Compare
Adds fast mode to the WhatIf annual prediction: plan four seasonal months and estimate the other eight from the solar curve the run already downloads. A run drops from one to three minutes to roughly 30 to 90 seconds, so comparing battery sizes or tariffs stops meaning a three minute wait per answer.
Available three ways, off by default:
annual.fast_mode: truein the config,--fastonannual_cli, and a checkbox under Advanced in the WhatIf form.How it works
Each scenario field's per-day value is fitted against that month's real solar yield, by least squares over the anchor months:
The twelve month solar curve comes from
WeatherYear, which the run has already fetched, so it costs no extra download. Each scenario and field is fitted independently —no_pvbatcost is load-driven and lands nearb ≈ 0,export_kwhis steeply solar-driven, and one shared shape would serve neither.Interpolated months carry
status: "interpolated"and aninterpolated_fromprovenance block, count toward the annual totals, and are marked in both the month table and the chart. Rate data is still downloaded for all twelve months, so a month with genuinely no rates staysunavailablerather than being quietly estimated over.Volatility guard
Estimating one month from another only holds while a month's economics follow the solar curve. On Agile they do not — the reconstructed savings come out 20 to 30% wrong, and no anchor set fixes it (every choice is 8 to 32% out on one savings figure or the other; six, seven and eight anchors land no better).
So rather than report a fast wrong number, the run measures the coefficient of variation of daily average import price over the anchor months and, above
0.10, plans all twelve months instead. The two cases separate by roughly fortyfold:The check runs before any month is planned, so progress reads
[0/12]…[12/12]honestly rather than jumping mid-run, and no work is wasted — the anchors a fast run would have planned are months a full run needs anyway.Verification
End to end against real twelve month reference runs on the same system, varying only the tariff:
--fast--fastTwo reduced reference fixtures are committed (months, scenarios, monthly PV and daily mean rates only — no location, URLs or keys) and a test asserts both claims: that the guard sorts the tariffs correctly, and that on the tariff it passes the savings reconstruct to 0.48% / 0.19%.
./run_pre_commitpasses: black, flake8, interrogate, cspell, markdownlint and the full quick suite.Two corrections worth flagging to a reviewer
The design doc was written before implementation and got two things wrong. Both are corrected in the spec, and both are the kind of thing worth knowing about when reading the code:
Anchor months are Mar/Jun/Sep/Dec, not Jan/Apr/Jul/Oct. Per-month cost error genuinely does not separate the candidates, which is what the original analysis measured. The savings figures separate them sharply, because a saving is a difference between two independently reconstructed scenarios and small relative errors in each are amplified in the difference. On the Cosy reference, the Predbat saving comes out +0.2% with the chosen anchors against −10.6% with the original ones.
The original "under 1% error on annual savings" claim was a mean across three anchor sets and hid a worst case of 33%. The caveat text now states what was actually measured — about 3% on savings, about 1% on payback, roughly 10% on individual months — for tariffs that pass the guard.
Also measured and rejected along the way: adding mean import rate as a second regressor looked best against noisy references but overfits badly (362% held-out error — four anchors against three parameters leaves one degree of freedom), and fitting the savings difference directly is mathematically identical to differencing two fits, since least squares is linear.
Out of scope, but worth recording
Separately from this change, today's full twelve month run is already 8 to 18% off on Agile at the default two samples per month, measured against a six-samples-per-month reference — and raising to six samples does not converge it. That is a property of the existing sampling design and affects every WhatIf run, not just fast ones. It bounds what fast mode can be held to and is probably worth its own issue.
🤖 Generated with Claude Code