Sync 0.19: the Forecast tab extends a cheap run with control's gap (TARIFF-24)

UX-54's Forecast tab (release/1.0) re-plans tomorrow's cheap run as control
will, with the gap `required`. It still added the "no weather" x1.5 always,
while control (TARIFF-24, from master) drops it when a forecast is read. In
the UX-54 scenarios the tab asked 0.225 where control needs 0.15. It now asks
`arbitrage.weather_uncertainty(True, c.solar_fc)`, so the two agree again, as
they did on release/1.0 alone. Display only; no control change. The tab's
blocks in every UX-54 scenario, every 2 h, are identical to release/1.0.

test_ux54.py pins it (the old line fails it). The TARIFF-24 DECISIONS note
names the tab as the fourth place that reads the rule.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
glenn schrooyen
2026-10-30 08:11:42 +01:00
co-authored by Claude
parent 8cca671a05
commit 1c9fefd9dd
3 changed files with 37 additions and 4 deletions
@@ -50,9 +50,10 @@ was one input judged twice.
**The rule** (`arbitrage.weather_uncertainty()`). With battery modes on and a
usable forecast (`solar_fc` not None, the day plan's own test) the doubt is
not added: the gap needed is the cycle cost, widened by the price product only.
No forecast, or a stale or unusable one: 1.5 x, as before. It is read in three
No forecast, or a stale or unusable one: 1.5 x, as before. It is read in four
places that must agree: the run plan (`_price_runs()`), the charge gate
(`_mode_roles()`) and Home's mirror (`homeview.trade_verdict()`). The forecast
(`_mode_roles()`), Home's mirror (`homeview.trade_verdict()`) and the Forecast
tab's longer cheap run (`dayview.py`, from release/1.0). The forecast
is read at most every 60 s and the plan key already holds "forecast or not", so
the gap follows in the cycle the read changes, both ways, with one exception:
a run planned with a forecast that is under way keeps that gap to its end
+5 -2
View File
@@ -23,7 +23,7 @@ from . import arbitrage, dayplan, homeview as hv, modetrade, priceruns
from . import solarfc
from .daycurve import Batt, curve
from .packmode import SOLAR_FIRST
from .price import Uncertainty, brussels, required_spread, uncertainty_of
from .price import brussels, required_spread, uncertainty_of
BLOCKS = ("grid", "give", "cycle") # the kinds a battery's blocks carry
LEVEL_DP = 1 # the tab shows a level to 0.1 %
@@ -204,9 +204,12 @@ def _levels(c, est, fc, trust, now, end, runs, floor, ceil, notes_b, shadow,
end_t = None
if plan_on and cheap is not None and rp.charge_useful() \
and n0 <= cheap.start < n1 and plan is not None:
# The gap control's run plan needs (TARIFF-24): no x1.5 with a
# forecast read.
required = required_spread(
arbitrage.threshold_eur_kwh(c.o),
uncertainty_of(plan.read.product) | {Uncertainty.NO_WEATHER})
uncertainty_of(plan.read.product)
| arbitrage.weather_uncertainty(True, c.solar_fc))
end_t = cheap.end
for _ in range(24):
ext = dataclasses.replace(rp, cheap=priceruns.Run(
+29
View File
@@ -1456,6 +1456,35 @@ hi_ = tab(c_t, at(10, 7))["days"][0]["batteries"][0]["levels"]
check("on a controller the curve is built with the plan's trust "
"(`solar_trust_pct`)", hi_ and max(v for _h, v in hi_) > hi_[0][1])
# TARIFF-24 (sync 0.19): the tab extends tomorrow's cheap run as control
# re-plans it, so it asks control's gap: the cycle cost with a forecast read,
# not 1.5 x it.
from app.price import required_spread, uncertainty_of # noqa: E402
gaps, _plan_real = [], dayview.priceruns.plan
def _spy(*a, **k):
gaps.append(k.get("required_eur_kwh"))
return _plan_real(*a, **k)
c_g = S.make("dynamic", at(17, 0))
dayview.priceruns.plan = _spy
try:
tab(c_g, at(17, 0))
finally:
dayview.priceruns.plan = _plan_real
_p = c_g.fresh_tariff_plan(at(17, 0))
want = required_spread(arbitrage.threshold_eur_kwh(c_g.o),
uncertainty_of(_p.read.product)
| arbitrage.weather_uncertainty(True, c_g.solar_fc))
print(f" dynamic 17:00: the tab's extension asks {gaps}, control needs {want}")
check("the tab extends tomorrow's cheap run with control's gap (with a "
"forecast: the cycle cost, not 1.5 x it)",
c_g.solar_fc is not None and bool(gaps)
and all(abs(g - want) < 1e-9 for g in gaps)
and abs(want - arbitrage.threshold_eur_kwh(c_g.o)) < 1e-9)
print()
if fails:
print(f"{len(fails)} FAILED")