You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(plan): value left-over battery on the base tariff, not saving-session prices (#4488)
battery_value_rate ceilinged the end-of-plan battery credit on rate_max and took its
export-recovery ratio from rate_export_max, both whole-horizon figures that include
saving sessions. On a flat tariff running a session that put the credit above what
discharging a stored kWh can realise, so the planner scored freeze charge as profit
and froze every window up to end_record while the battery sat nearly full.
A reported case had a flat 29.2p import with a 2p session bonus the next evening
(rate_max 31.2 vs rate_max_base 29.2) and a 100p export session expiring 19 minutes
after minutes_now (rate_export_max 100, export 0p for the rest of the horizon). The
session bonus raised the ceiling while the expiring export event switched the discount
off, leaving freeze charge worth +2.00p per kWh of load - exactly the session bonus -
and 5 of 6 charge windows frozen.
Read the base tariff for both terms instead. rate_max_base is already captured before
sessions inflate rate_max; rate_export_max_forward is new, built by fetch from
rate_export_base at the point that copy is taken, and forward-looking so an export
price that has passed stops counting. On the reported case the credit drops from
29.05p to 21.75p per kWh, freeze charge goes to -5.84p per kWh of load, and no window
freezes. Both terms fall back to their whole-horizon equivalents when the base data is
absent, so replaying an older debug file is unchanged.
run_single_debug now resets both fields alongside the existing dynamic_load_baseline
resets: the planner consumes them now, debug files written before they existed carry
neither, and inside the full suite they leaked from earlier tests into the replay.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Flat tariff: rate_min_forward == rate_max, so the ceiling MUST bind and pull the value
162
184
# below the gross-up. Without the ceiling this returns 1.0739*R instead of 0.9312*R.
163
-
my_predbat.rate_max=6.9
185
+
my_predbat.rate_max=my_predbat.rate_max_base=6.9
164
186
expected_capped=6.9*0.96*0.97
165
187
got=my_predbat.battery_value_rate(10)
166
188
ifabs(got-expected_capped) >1e-6:
167
189
print("ERROR: battery_value_rate is {} on a flat tariff, expected the capped {} - the rate_max ceiling is not being applied".format(got, expected_capped))
168
190
failed=True
169
191
170
192
# The 1p floor applies when the forward rate is negative (plunge pricing)
# A saving session pushes rate_export_max to 100p, but the export tariff itself pays nothing,
329
+
# so the forward base view is 0 and the full haircut must still apply.
330
+
my_predbat.rate_export_max=100.0
331
+
my_predbat.rate_export_max_forward= {10: 0.0}
332
+
expected=full*0.8
333
+
got=my_predbat.battery_value_rate(10)
334
+
ifabs(got-expected) >1e-6:
335
+
print("ERROR: battery_value_rate is {} with a saving session in rate_export_max, expected the haircut {} - a one-off event is standing in for the tariff".format(got, expected))
336
+
failed=True
337
+
338
+
# Base tariff that genuinely recovers half the import cost takes half the haircut
339
+
my_predbat.rate_export_max_forward= {10: 3.45}
340
+
expected=full*0.9
341
+
got=my_predbat.battery_value_rate(10)
342
+
ifabs(got-expected) >1e-6:
343
+
print("ERROR: battery_value_rate is {} with a base export of 3.45, expected {}".format(got, expected))
344
+
failed=True
345
+
346
+
# A real export tariff matching the cheapest import still takes no haircut - the forward view
347
+
# must not be read as "export is always worthless"
348
+
my_predbat.rate_export_max_forward= {10: 6.9}
349
+
got=my_predbat.battery_value_rate(10)
350
+
ifabs(got-full) >1e-6:
351
+
print("ERROR: battery_value_rate is {} with a base export matching import, expected the undiscounted {}".format(got, full))
352
+
failed=True
353
+
354
+
# No forward data at all (an older debug file being replayed) falls back to rate_export_max
355
+
my_predbat.rate_export_max=3.45
356
+
my_predbat.rate_export_max_forward= {}
357
+
expected=full*0.9
358
+
got=my_predbat.battery_value_rate(10)
359
+
ifabs(got-expected) >1e-6:
360
+
print("ERROR: battery_value_rate is {} with no forward export data, expected the rate_export_max fallback {}".format(got, expected))
0 commit comments