Skip to content

Commit 1cd1183

Browse files
fix(compare): restore the plan in force's pre-clip copy after the tariff comparison
Plan selection scores the pre-clip version of each plan (#4403), keeping the plan in force's copy in plan_preclip for the next re-plan to score as the previous plan. The tariff comparison runs calculate_plan() for every tariff, each replacing plan_preclip with that tariff's 48-hour plan, and restored the windows and limits but not plan_preclip. The first re-plan after the midnight comparison therefore scored the last tariff's plan as the plan in force: on 9 Oct 2026 (Sigenergy, IOG) live scored its previous plan at 300.97p where the same plan scores 30.71p without the comparison, and so switched plans regardless of metric_min_improvement_plan. Found with the forward log replay (#5363): running the comparison inside the replay reproduced the inflated score, and restoring each attribute the comparison changes, group by group, isolated plan_preclip. run_all() now saves and restores it with the windows; a test runs run_all() with a tariff that replaces it and checks it comes back (it fails without the fix). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 0dab350 commit 1cd1183

2 files changed

Lines changed: 48 additions & 0 deletions

File tree

‎apps/predbat/compare.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,9 @@ def run_all(self, debug=False, fetch_sensor=True):
618618
save_export_window_best = my_predbat.export_window_best
619619
save_export_limits_best = my_predbat.export_limits_best
620620
save_charge_limit_best = my_predbat.charge_limit_best
621+
# The pre-clip copy of the plan in force, which the next re-plan scores as the previous plan; each tariff's
622+
# calculate_plan() replaces it with its own plan
623+
save_plan_preclip = my_predbat.plan_preclip
621624
save_cost_today_sofar = my_predbat.cost_today_sofar
622625
save_carbon_today_sofar = my_predbat.carbon_today_sofar
623626
save_iboost_today = my_predbat.iboost_today
@@ -735,6 +738,7 @@ def run_all(self, debug=False, fetch_sensor=True):
735738
my_predbat.export_window_best = save_export_window_best
736739
my_predbat.export_limits_best = save_export_limits_best
737740
my_predbat.charge_limit_best = save_charge_limit_best
741+
my_predbat.plan_preclip = save_plan_preclip
738742
my_predbat.cost_today_sofar = save_cost_today_sofar
739743
my_predbat.carbon_today_sofar = save_carbon_today_sofar
740744
my_predbat.iboost_today = save_iboost_today

‎apps/predbat/tests/test_compare.py‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,54 @@ def _make_compare():
6868
return cmp, pb
6969

7070

71+
def test_run_all_restores_plan_preclip(my_predbat):
72+
"""run_all() puts back the pre-clip copy of the plan in force, which each tariff's calculate_plan() replaces.
73+
74+
Left as the last tariff's 48-hour plan, the first re-plan after the midnight comparison scored that plan as the
75+
previous one, so it always switched plans whatever metric_min_improvement_plan said (seen live on 9 Oct 2026:
76+
previous plan 300.97p against 30.71p when the comparison is not run).
77+
"""
78+
from compare import Compare
79+
80+
saved_compare_list = my_predbat.args.get("compare_list")
81+
saved_preclip = my_predbat.plan_preclip
82+
plan_in_force = ([18.08], [{"start": 0, "end": 90, "average": 7.62}], [], [])
83+
my_predbat.plan_preclip = plan_in_force
84+
my_predbat.args["compare_list"] = [{"id": "test_tariff", "name": "Test tariff"}]
85+
comparison = Compare.__new__(Compare)
86+
comparison.pb = my_predbat
87+
comparison.log = my_predbat.log
88+
comparison.comparisons = {}
89+
90+
def run_single(*args, **kwargs):
91+
"""Stand in for a tariff's run: its calculate_plan() replaces plan_preclip with that tariff's plan."""
92+
my_predbat.plan_preclip = ([0.0], [{"start": 0, "end": 2880, "average": 30.0}], [], [])
93+
return None
94+
95+
comparison.run_single = run_single
96+
comparison.select_best = lambda *args, **kwargs: None
97+
comparison.save_yaml = lambda *args, **kwargs: None
98+
comparison.publish_data = lambda *args, **kwargs: None
99+
try:
100+
comparison.run_all(fetch_sensor=False)
101+
if my_predbat.plan_preclip is not plan_in_force:
102+
print("ERROR: run_all() should restore the plan in force's pre-clip copy, got {}".format(my_predbat.plan_preclip))
103+
return 1
104+
print("PASS: run_all() restores plan_preclip")
105+
return 0
106+
finally:
107+
my_predbat.plan_preclip = saved_preclip
108+
if saved_compare_list is None:
109+
my_predbat.args.pop("compare_list", None)
110+
else:
111+
my_predbat.args["compare_list"] = saved_compare_list
112+
113+
71114
def test_compare(my_predbat):
72115
"""Run all compare unit tests."""
73116
failed = 0
74117
print("**** Running compare tests ****\n")
118+
failed += test_run_all_restores_plan_preclip(my_predbat)
75119

76120
# ------------------------------------------------------------------
77121
# T1: apply_hardware_overrides – soc_max override

0 commit comments

Comments
 (0)