OPT-11 P4: tests for eight surviving mutants; guarded reads
checks / unit (pull_request) Successful in 23m10s
infra-pin / fresh (pull_request) Successful in 2s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-09-29 20:22:16 +02:00
parent eb10537aee
commit aabc2a27fd
2 changed files with 65 additions and 7 deletions
@@ -114,5 +114,9 @@ P1 is its section 6 P1, and the pull request lists what was measured.
- the days' real 2026 quarter-hourly prices (held out): P2 32.26, option A - the days' real 2026 quarter-hourly prices (held out): P2 32.26, option A
32.01, old trade 31.40, P4 29.19 (perfect 29.21, too high 29.28, missing 32.01, old trade 31.40, P4 29.19 (perfect 29.21, too high 29.28, missing
32.01). 32.01).
- Solar first + Cheap hours only: 19.57 on both P3 and P4 (year-earlier
prices); on real 2026 prices P4 29.72 against P3's 29.41. Skipping the
sun's share does not pay for Cheap hours there; Leftover solar is the
mode the forecast pays for.
No threshold was tuned to these days. The 2026-09-24 PV recording could not No threshold was tuned to these days. The 2026-09-24 PV recording could not
be replayed: it is only in the live Home Assistant. be replayed: it is only in the live Home Assistant.
+61 -7
View File
@@ -36,6 +36,7 @@ sys.path.insert(0, HERE)
sys.path.insert(0, os.path.join(HERE, "tools")) sys.path.insert(0, os.path.join(HERE, "tools"))
logging.disable(logging.WARNING) logging.disable(logging.WARNING)
from named_absence import nth, present # noqa: E402
import opt11p3_replay as rp # noqa: E402 import opt11p3_replay as rp # noqa: E402
from app import arbitrage, dayplan, homeview, modetrade, packmode # noqa from app import arbitrage, dayplan, homeview, modetrade, packmode # noqa
from app import priceruns, solarfc, validate # noqa: E402 from app import priceruns, solarfc, validate # noqa: E402
@@ -320,8 +321,21 @@ check("Cheap hours: dark fills to the ceiling (as P3)",
check("...and skips the part the sun covers on a partly sunny day", check("...and skips the part the sun covers on a partly sunny day",
ch.targets[B] < 95.0 and ch.share_kwh[B] > 0 ch.targets[B] < 95.0 and ch.share_kwh[B] > 0
and abs(ch.targets[B] - (95.0 - ch.share_kwh[B] * 10.0)) < 0.01) and abs(ch.targets[B] - (95.0 - ch.share_kwh[B] * 10.0)) < 0.01)
check("trust 100 % counts more sun than 70 %: a lower or equal level", check("trust 100 % counts more sun than 70 %: a lower level",
dplan(12.0, trust=100).targets[B] <= partly.targets[B]) dplan(12.0, trust=100).targets[B] < partly.targets[B])
check("...and the sun counted scales with the trust exactly",
abs(dplan(12.0, trust=100).sun_kwh * 0.7 - partly.sun_kwh) < 0.01)
_room_a = (95.0 - 60.0) / 10.0 * 10.0 / 10.0 # A: 60 % -> 95 % of 10 kWh
check("Solar first takes the sun first: B's share is what is left after "
f"A's {_room_a:.1f} kWh of room",
abs(partly.share_kwh[B] - max(0.0, partly.sun_kwh - _room_a)) < 0.01
and partly.share_kwh[B] < partly.sun_kwh)
_flat = (lambda h: 600.0, 600.0)
_sunny_net = (lambda h: -2000.0 if 9 <= h < 17 else 600.0, 600.0)
check("the house's net use in sunny hours (negative) is lifted to the night "
"floor: the same sun as a flat 600 W house",
abs(dplan(12.0, est=_flat).sun_kwh - dplan(12.0, est=_sunny_net)
.sun_kwh) < 1e-6)
check("no forecast, no house profile or no price plan: no plan", check("no forecast, no house profile or no price plan: no plan",
dayplan.plan(None, EST, R0, T0, PACKS, trust_pct=70, floor_pct=20, dayplan.plan(None, EST, R0, T0, PACKS, trust_pct=70, floor_pct=20,
ceiling_pct=95) is None ceiling_pct=95) is None
@@ -530,7 +544,8 @@ def invariants(tag, rows, c):
# Sunny: 40 kWh forecast. B (Leftover solar) at 30 % needs nothing. # Sunny: 40 kWh forecast. B (Leftover solar) at 30 % needs nothing.
rows, st_, c = run_day(SF_LS, fc=40.0, soc0={"a": 60.0, "b": 30.0}) rows, st_, c = run_day(SF_LS, fc=40.0, soc0={"a": 60.0, "b": 30.0})
charge = invariants("sunny", rows, c) charge = invariants("sunny", rows, c)
print(f" sunny: verdict {rows[-1]['plan'] and rows[-1]['plan'].verdict}, " _last = nth(rows, -1, "a cycle")
print(f" sunny: verdict {present(_last) and _last['plan'] and _last['plan'].verdict}, "
f"{len(charge)} charge cycles") f"{len(charge)} charge cycles")
check("sunny: a day plan is made, and says sunny", check("sunny: a day plan is made, and says sunny",
any(r["plan"] is not None and r["plan"].verdict == dayplan.SUNNY any(r["plan"] is not None and r["plan"].verdict == dayplan.SUNNY
@@ -540,7 +555,7 @@ check("sunny: no grid charge at all", not charge)
# Dark: 1 kWh. B buys the evening's shortfall only, in the cheap run. # Dark: 1 kWh. B buys the evening's shortfall only, in the cheap run.
rows, st_, c = run_day(SF_LS, fc=1.0, soc0={"a": 60.0, "b": 30.0}) rows, st_, c = run_day(SF_LS, fc=1.0, soc0={"a": 60.0, "b": 30.0})
charge = invariants("dark", rows, c) charge = invariants("dark", rows, c)
tgt = next(r["targets"][B] for r in rows if r["targets"]) tgt = nth([r["targets"][B] for r in rows if r["targets"]], 0, "a level")
top = max(r["soc"]["b"] for r in rows) top = max(r["soc"]["b"] for r in rows)
print(f" dark: B level {tgt:.1f} %, B reached {top:.1f} %, " print(f" dark: B level {tgt:.1f} %, B reached {top:.1f} %, "
f"{len(charge)} charge cycles") f"{len(charge)} charge cycles")
@@ -548,18 +563,18 @@ check("dark: Leftover solar charges from the grid in the cheap run",
charge and all(r["runs"].cheap.holds(r["now"]) for r in charge) charge and all(r["runs"].cheap.holds(r["now"]) for r in charge)
and all(r["roles"][B] == modetrade.CHARGE for r in charge)) and all(r["roles"][B] == modetrade.CHARGE for r in charge))
check("dark: ...only up to its level (the shortfall), not 95 %", check("dark: ...only up to its level (the shortfall), not 95 %",
tgt < 60.0 and top <= tgt + 2.0) present(tgt) and tgt < 60.0 and top <= tgt + 2.0)
check("dark: A rests while B charges", all(r["roles"][A] == modetrade.REST check("dark: A rests while B charges", all(r["roles"][A] == modetrade.REST
for r in charge)) for r in charge))
# Cheap hours on a partly sunny day: skips the part the sun covers. # Cheap hours on a partly sunny day: skips the part the sun covers.
rows, st_, c = run_day(SF_CH, fc=14.0, soc0={"a": 60.0, "b": 30.0}) rows, st_, c = run_day(SF_CH, fc=14.0, soc0={"a": 60.0, "b": 30.0})
charge = invariants("cheap hours, partly", rows, c) charge = invariants("cheap hours, partly", rows, c)
tgt = next(r["targets"][B] for r in rows if r["targets"]) tgt = nth([r["targets"][B] for r in rows if r["targets"]], 0, "a level")
top = max(r["soc"]["b"] for r in rows) top = max(r["soc"]["b"] for r in rows)
print(f" cheap hours partly: B level {tgt:.1f} %, reached {top:.1f} %") print(f" cheap hours partly: B level {tgt:.1f} %, reached {top:.1f} %")
check("Cheap hours with sun forecast: charges, but stops below 95 %", check("Cheap hours with sun forecast: charges, but stops below 95 %",
charge and tgt < 90.0 and top <= tgt + 2.0) charge and present(tgt) and tgt < 90.0 and top <= tgt + 2.0)
rows_nf, _s, c_nf = run_day(SF_CH, fc=None, soc0={"a": 60.0, "b": 30.0}) rows_nf, _s, c_nf = run_day(SF_CH, fc=None, soc0={"a": 60.0, "b": 30.0})
check("...and with the forecast down it fills towards 95 % as in P3", check("...and with the forecast down it fills towards 95 % as in P3",
max(r["soc"]["b"] for r in rows_nf) > top + 10.0) max(r["soc"]["b"] for r in rows_nf) > top + 10.0)
@@ -614,6 +629,30 @@ check("...while the profile learns and the store keeps each closed hour",
c.store.data.get("house_profile") or {}) >= 1) c.store.data.get("house_profile") or {}) >= 1)
check("selling stays off (option A)", packmode.SELLING_ENABLED is False) check("selling stays off (option A)", packmode.SELLING_ENABLED is False)
# A forecast arriving mid-period makes the plan at once, not at the next
# 15-minute period.
COME = datetime(2026, 9, 21, 1, 3, tzinfo=L).astimezone(timezone.utc)
rows, st_, c = run_day(SF_LS, fc=lambda now: None if now < COME else 1.0,
soc0={"a": 60.0, "b": 30.0}, hours=0.5)
first = next((r["now"] for r in rows if r["targets"]), None)
check("a forecast that arrives at 01:03 makes the plan before 01:15",
first is not None and first < COME + timedelta(minutes=3))
# Battery modes off: the forecast is never read.
READS = [0]
_real_sensor = rp.Hass.state
async def _counted(self, e):
READS[0] += 1
return await _real_sensor(self, e)
rp.Hass.state = _counted
FC_KWH[0] = 1.0
rp.run("modes_off", house_flat(800.0), WPTS, UTC0 + timedelta(hours=1),
600.0, extra=dict(FC_OPTS))
rp.Hass.state = _real_sensor
check("battery modes off: the forecast sensor is never read", READS[0] == 0)
# ============================================================================ # # ============================================================================ #
print("7. the Batteries screen and the Simple view") print("7. the Batteries screen and the Simple view")
rows, st_, c = run_day(SF_LS, fc=1.0, soc0={"a": 60.0, "b": 30.0}, rows, st_, c = run_day(SF_LS, fc=1.0, soc0={"a": 60.0, "b": 30.0},
@@ -627,7 +666,22 @@ why = homeview.day_key(c, B)
check("B's Why key (plan, dark) is tops_up", why == "tops_up") check("B's Why key (plan, dark) is tops_up", why == "tops_up")
check("...and says so in plain words", homeview.why_words(why) check("...and says so in plain words", homeview.why_words(why)
== "Too little sun is forecast, so it charges some from the grid.") == "Too little sun is forecast, so it charges some from the grid.")
_d0, _d1 = homeview.local_day(c.solar_fc.at)
_blocks, _ = homeview.mode_runs(c, _d0, _d1, c.solar_fc.at, False)
_grid = [b for b in _blocks if b["kind"] == "grid" and b["battery"] == 1]
_need = homeview.plan_need(c, B)
check(f"Today: B's grid block is the planned shortfall ({_need:.2f} kWh), "
"not its room to 95 %", _grid and abs(_grid[0]["kwh"] - _need) < 0.01
and _need < 3.0)
_run = c.mode_runs.cheap
_packs = c._mode_packs()
_m1 = c._run_mark("charge", _run, _packs, lambda p: True, ((B, 40.0),))
_m2 = c._run_mark("charge", _run, _packs, lambda p: False, ((B, 50.0),))
check("a level the hysteresis moved opens the run to that battery again",
_m1 and not _m2)
c.solar_fc = None c.solar_fc = None
check("the levels are dropped as soon as the forecast goes, even before "
"the plan is rebuilt", c._plan_targets and c.plan_targets() is None)
check("a lost forecast says so: no forecast, Leftover solar solar only", check("a lost forecast says so: no forecast, Leftover solar solar only",
homeview.solar_block(c, datetime.now(timezone.utc))["state"] homeview.solar_block(c, datetime.now(timezone.utc))["state"]
== "no_forecast" and homeview.day_key(c, B) == "no_forecast") == "no_forecast" and homeview.day_key(c, B) == "no_forecast")