Repository navigation
feat(config): add a performance_tweaks toggle and merge the two plan passes - #4524
Merged
Merged
Conversation
…passes Two related changes to the optimiser's final pass and how it is configured. Merge tweak_plan and optimise_full_second_pass into optimise_plan_pass(end_record, budget). They were the same loop - same window sort, same optimise_charge_limit/optimise_export calls, same keep_window_change_if_improved - differing only in tweak_plan's count >= 8 cap and in the export-window start_orig reset, which only tweak_plan performed. The reset now applies on both paths: without it a window trimmed by an earlier pass can only ever be trimmed further, so the pass cannot recover a window it narrowed on a plan that has since changed underneath it. calculate_second_pass now selects budget=0 (every window in the record) rather than a second implementation. Add switch.predbat_performance_tweaks, gating the options that trade planning time for plan quality. It works like expert_mode, but everything behind it defaults to On, so a user who never touches it gets the best plan Predbat can produce; the toggle exists so someone on slow hardware can reveal the switches and turn features off. calculate_second_pass and calculate_pv90_plan move behind it and both now default to On. Note the sharp edge this inherits from the config layer: get_ha_config substitutes an item's default when the item is disabled rather than returning None, so a hidden option is pinned to its default and cannot be held Off from apps.yaml without revealing the toggle. Covered by tests. predbat_debug_pre_saving1's expected plan is regenerated: those captures predate PV90 and do not pin pv_metric90_weight, so the new default switches PV90 on for them. agile1 is unchanged. Adds test_performance_tweaks covering the toggle, its gated items' defaults, hide/reveal behaviour and the fetch_config_options read. test_pv90's default-off assertions are rewritten to force the Off case now that On is the default. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR refactors Predbat’s optimiser “final pass” by merging the previous tweak/second-pass implementations into a single optimise_plan_pass(...) with a window budget, and introduces a new performance_tweaks gating switch that hides (but defaults-on) the more CPU-intensive plan-quality features.
Changes:
- Merge the old tweak/second-pass loops into
optimise_plan_pass(end_record, budget=...), and drive the pass viaPLAN_PASS_WINDOW_BUDGETvs “unlimited” for the full second pass. - Add
switch.predbat_performance_tweaksand movecalculate_second_pass+calculate_pv90_planbehind it, flipping both defaults to On while hidden. - Update docs and tests to reflect the new toggle, new defaults, and the merged pass behavior.
Reviewed changes
Copilot reviewed 8 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/customisation.md | Documents the new performance tweaks toggle and updated defaults/visibility rules. |
| coverage/cases/predbat_debug_pre_saving1.yaml.expected.json | Updates golden output to match new default planning behavior. |
| apps/predbat/unit_test.py | Registers the new performance tweaks test suite. |
| apps/predbat/tests/test_pv90.py | Updates PV90 tests for new gating/defaults and adds coverage for hidden-default behavior. |
| apps/predbat/tests/test_performance_tweaks.py | Adds new unit tests covering the performance_tweaks gate, defaults, and hide/reveal semantics. |
| apps/predbat/tests/test_optimise_swap_export.py | Updates pass-order assertions for the merged plan pass. |
| apps/predbat/tests/test_export_commitment.py | Updates monotonicity tests to exercise optimise_plan_pass in budgeted and unbudgeted modes. |
| apps/predbat/plan.py | Implements optimise_plan_pass, introduces PLAN_PASS_WINDOW_BUDGET, and removes the duplicate second-pass implementation. |
| apps/predbat/config.py | Adds performance_tweaks config item and gates/updates defaults for second pass and PV90 switches. |
Suppressed comments (1)
apps/predbat/plan.py:2895
- The optimise_plan_pass docstring says
optimise_swap_export runs before this, but optimise_all_windows calls optimise_plan_pass before optimise_swap_export. This makes the rationale for re-baselining the metric misleading.
The metric is measured from the plan we were handed rather than taken from the caller:
optimise_swap_export runs before this and mutates the plan without its return value being used,
so the caller's metric is already stale.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
30
to
31
| from utils import calc_percent_limit, dp0, dp1, dp2, dp3, dp4, remove_intersecting_windows, calc_percent_limit, in_car_slot | ||
| from prediction import Prediction, wrapped_run_prediction_single, wrapped_run_prediction_charge, wrapped_run_prediction_charge_min_max, wrapped_run_prediction_export, wrapped_run_prediction_charge_min_max |
Comment on lines
+998
to
+1000
| The switch defaults On now, so the Off state is produced the way the product produces it - by switching it | ||
| off and letting fetch_config_options' override drive pv_metric90_weight to 0.0 - rather than by setting the | ||
| weight by hand. A regression that broke the switch-to-weight wiring is still caught here. |
…ss default apply_scenario_to_predbat pinned calculate_second_pass to a literal False, so the random benchmark always measured the budgeted planning path regardless of what users actually run - and silently ignored the switch even when a template set it, which made a second-pass benchmark run look like a null result rather than an experiment that never happened. Read the CONFIG_ITEMS default instead. That keeps the point of pinning it here - every scenario is planned the same way regardless of which template it was run against - while tracking the product default rather than drifting from it. With calculate_second_pass now defaulting On, the 20-scenario benchmark measures the real shipped path: 50.94s total vs 38.21s on main (+33.3%), metric average -3.1704, cost average -3.4125. Those match the earlier stand-in measurement (via PLAN_PASS_WINDOW_BUDGET) to within run-to-run noise. Also rewords one docstring to avoid a cspell miss. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…bled Both captures pinned calculate_second_pass false in their flat dump, so the golden regression cases exercised only the budgeted planning path. With the switch now defaulting On, that left the shipped path - optimise_plan_pass at budget 0, every window in the record revisited - with no golden coverage at all: a regression there would pass CI untouched. agile1's plan is unchanged by the second pass, so only pre_saving1's expected plan is regenerated. There it is worth having: metric 471.1906 -> 469.2904, found by adding a 15 minute export at 23:45-00:00 (window trimmed from start_orig 1410 to 1425) and pulling the neighbouring export 30 minutes earlier - the same short-late-export shape the #4478 work showed the window budget was missing. Debug cases planning time 5.02s -> 5.91s in the full suite. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… too The benchmark scored each plan only on the central forecast, which cannot judge a hedge. A plan that weights the PV90 scenario deliberately spends money the central forecast says is wasted, so scoring it there marks it down however well it does on the day it was hedging for - the reason the PV90 default looked like a wash on this suite while being a clear win out of sample. run_scenario now records the same plan's cost in all three simulated futures. The PV10 run already existed for compute_metric and was simply discarded; only the PV90 run is new, and two extra predictions per scenario are noise beside calculate_plan (suite mean runtime is unchanged). compare_results grows a per-future summary. Result files written before this contribute nothing to those columns rather than breaking, so older baselines stay comparable on what they do carry. Main vs this branch, 20 scenarios (- = branch cheaper in that future): PV10 (cloudy, heavier load) avg -4.66 7 better, 6 worse nominal avg -3.41 7 better, 5 worse PV90 (sunny, lighter load) avg -10.56 11 better, 2 worse Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…aults apps-yaml.md still said the 90% estimate is "not used by default", that the switch is "Off by default" and that it sits outside expert mode "as we would like this tested widely". All three are now wrong: PV90 is on by default and the switch lives behind performance_tweaks. The performance section only told users to turn off the second pass. PV90 is the larger of the two costs, so name both, say what each buys, and give a sense of the scale - including that the second pass gets relatively more expensive as the plan horizon lengthens, which a single percentage would hide. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…seded in part tweak_plan and optimise_full_second_pass were merged into optimise_plan_pass, so the two function names this spec is written around no longer exist. Left as the dated record it is, with a note saying what changed and confirming the monotonicity guarantee it specifies still holds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three findings from the Copilot review, all accurate: optimise_plan_pass' docstring claimed "optimise_swap_export runs before this", which was true of tweak_plan when that sentence was written but stopped being true when #4513 moved the swap to run last. Carried over unchanged into the merged pass. Reworded to say what actually holds: the metric is measured from the plan in hand rather than accepted from the caller, so it cannot be stale. test_pv90_switch_off_skips_all_launch_paths' docstring said the Off state was produced by letting fetch_config_options drive pv_metric90_weight to 0.0, but the test sets both the switch and the weight directly and never calls fetch_config_options. Reworded to describe what it does test, and to point at test_pv90_switch_off_forces_weight_zero, which does exercise that wiring. PLAN_PASS_WINDOW_BUDGET was left sitting between two import lines. Moved below the import block. Also removes two duplicated import symbols (calc_percent_limit, wrapped_run_prediction_charge_min_max) flagged in the same review. Those pre-date this PR rather than coming from it, but they are a one-word fix and the review surfaced them here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two related changes to the optimiser's final pass and how it is configured.
1. Merge
tweak_planandoptimise_full_second_passThey were the same loop — same window sort, same
optimise_charge_limit/optimise_exportcalls, samekeep_window_change_if_improved— differing only in:tweak_plan'scount >= 8capstart_origreset, which onlytweak_planperformedNow one function,
optimise_plan_pass(end_record, budget).calculate_second_passselectsbudget=0(every window in the record) instead of a second implementation. Net −48 lines.The reset now applies on both paths. Without it a window trimmed by an earlier pass can only ever be trimmed further, so the pass cannot recover a window it narrowed on a plan that has since changed underneath it.
This part is provably behaviour-preserving. With PV90 held off, the merged pass reproduces
mainexactly across the 20-scenario random benchmark: all 20 identical on metric and cost, total runtime 38.21s vs 38.21s.2.
switch.predbat_performance_tweaksA second gating toggle alongside
expert_mode, defaulting Off. Behind it sit the options that trade planning time for plan quality — and unlike expert mode, everything behind it defaults to On. A user who never touches it gets the best plan Predbat can produce; the toggle exists so someone on slow hardware can reveal the switches and turn features off.Moved behind it, both now defaulting to On:
switch.predbat_calculate_second_pass(was expert-gated, default Off)switch.predbat_calculate_pv90_plan(was ungated, default Off)Sharp edge worth knowing
get_ha_configsubstitutes an item's default when the item is disabled rather than returningNone, soget_argnever reaches itsself.argslookup. A hidden option is pinned to its default and cannot be held Off fromapps.yamlwithout revealing the toggle. This is pre-existing behaviour for every expert-gated item, but it matters more now that hidden means "on". Documented and covered by tests.Evidence for the default flips
PV90
The random benchmark scored every plan only on the central forecast, which cannot judge a hedge: a plan that weights PV90 deliberately spends money the central forecast says is wasted, so scoring it there marks it down however well it does on the day it was hedging for. This PR extends the harness to record each plan's cost in all three simulated futures, so the effect is reproducible from the repo rather than from a one-off script.
Main vs this branch, 20 scenarios (− = branch cheaper in that future):
Cheaper on average in all three futures, and strongly so in the one PV90 exists for (11–2). Isolating PV90 alone — second pass off, both plans built from identical data — gives the same shape at smaller magnitude: −1.00 / −1.17 / −5.24, winning 10–2 in the sunny future.
Caveat on magnitude: the benchmark's P90 series is synthetic (
pv90 = pv × 1.25,PV90_FACTORfixed in the harness) and the sunny case also appliesload_scaling90 = 0.7, so the size of the win is an artefact of those factors even though the direction is not.Second pass
apply_scenario_to_predbatpinnedcalculate_second_passto a literalFalse, so the random benchmark could never measure this switch — and silently ignored a template that set it, which made an earlier second-pass run look like a null result rather than an experiment that never happened. This PR changes the harness to read the CONFIG_ITEMS default instead, so the benchmark measures the shipped planning path.With that fixed, main vs this branch on the real config read: metric average −3.1704, cost average −3.4125, 6 better / 7 worse / 7 unchanged. Those match the earlier stand-in measurement (forcing
PLAN_PASS_WINDOW_BUDGET = 0, which is exactly what the switch selects) to within run-to-run noise, so both measurements agree.Raising the budget also never regressed a scenario in isolation: at budget 40, 2 better / 0 worse / 18 unchanged; unbudgeted, 5 better / 0 worse / 15 unchanged.
Runtime cost
20-scenario random benchmark, total wall-clock across the suite:
The final row is a real config read via the fixed harness, not a stand-in. Worst single scenario: +2.75s (2.07x). The cost scales with horizon length — a 48-hour plan pays more than these 24-hour scenarios (the #4478 capture went 11.0s → 15.6s for the second pass alone).
This is the whole reason for the toggle: users on a Pi can turn either feature off and get the old planning time back.
Golden cases
predbat_debug_pre_saving1's expected plan is regenerated. Those captures predate PV90 and do not pinpv_metric90_weight, so the new default switches PV90 on for them. The change is modest — exports slightly more (export_limits_best[7.0, 85]→[6.0, 85], target 12 → 10) with a mildly restructured charge plan, at the same nominal metric of 471.1906.agile1is unchanged.Testing
./run_allpasses; pre-commit clean.test_performance_tweakscovers the toggle, its gated items' defaults, hide/reveal behaviour, and thefetch_config_optionsread.test_export_commitment's two monotonicity tests now exercise the merged pass atbudget=8andbudget=0.test_pv90's default-off assertions rewritten to force the Off case now that On is the default, plus a new test pinning the hidden-item-resolves-to-default mechanism.🤖 Generated with Claude Code