Skip to content

WhatIf fast mode: plan four seasonal months, estimate the rest - #4602

Merged
springfall2008 merged 11 commits into
mainfrom
feat/annual-fast-mode
Aug 20, 2026
Merged

WhatIf fast mode: plan four seasonal months, estimate the rest#4602
springfall2008 merged 11 commits into
mainfrom
feat/annual-fast-mode

Conversation

@springfall2008

Copy link
Copy Markdown
Owner

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: true in the config, --fast on annual_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:

per_day(m) = a + b · pv_per_day(m)
value(m)   = per_day(m) · days_in_month(m)

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_pvbat cost is load-driven and lands near b ≈ 0, export_kwh is steeply solar-driven, and one shared shape would serve neither.

Interpolated months carry status: "interpolated" and an interpolated_from provenance 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 stays unavailable rather 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:

Tariff Rate variability Verdict
Cosy 0.002 fast mode runs
Banded tariffs (Intelligent Go, etc.) ~0 by construction fast mode runs
Agile 0.222 full run

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:

Case Result
Cosy --fast Payback 5.84 yr vs 5.81 full; savings −0.9% / +2.5%
Agile --fast Declined, produced a bit-identical full run (+0.0% / +0.0%)

Two 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_commit passes: 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

Copilot AI lite review requested due to automatic review settings August 19, 2026 19:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_mode plumbing 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.

Comment thread apps/predbat/annual_cli.py
Comment thread apps/predbat/annual.py
Comment thread apps/predbat/annual_interpolate.py
Comment thread docs/superpowers/specs/2026-08-18-annual-fast-mode-design.md
Comment thread docs/superpowers/plans/2026-08-18-annual-fast-mode.md
springfall2008 and others added 11 commits August 19, 2026 20:51
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>
@springfall2008
springfall2008 merged commit f40fb40 into main Aug 20, 2026
2 checks passed
@springfall2008
springfall2008 deleted the feat/annual-fast-mode branch August 20, 2026 07:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants