SAFETY-83: peak guard outranks the maintenance drain at N=1
checks / unit (pull_request) Successful in 16m54s
infra-pin / fresh (pull_request) Successful in 1s

At N=1 the drain is the site SET at P_MAINTENANCE (60), so the peak floor
(P_PEAK, 50) was not applied and the house imported what the drain left.
Where the floor asks for more than the drain rate, main.py now refiles the
peak claim at P_MAINTENANCE, so it wins; safety SETs still park everything.
Nothing else changes: safety78_identity --diff against 35cdffe changes only
cycles where the guard wanted more than the drain; N=2 is byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-29 22:40:14 +02:00
co-authored by Claude Opus 5.5
parent 35cdffe766
commit d77f1b1fdf
8 changed files with 402 additions and 27 deletions
+78 -2
View File
@@ -13,11 +13,18 @@ no watt still moves the digest. The `_modes` scenarios run battery modes on,
Cheap hours only, over opt11p3_identity's prices. Uses only the API release/1.0 5a4cdc2 has, so the same file runs on
both trees; test_safety78.py part 5 holds this tree to the digests printed
there.
SAFETY-83 adds the owner's settings (limit 3500, drain 2500, margin 50) with a
house that goes over drain + aim and back, and N=2 runs of both. At N=1 the
peak floor now outranks the drain where it asks for more, so the N=1 digests
moved; `--dump FILE` on the base tree and `--diff FILE` here list every
changed cycle and whether the peak guard wanted more than the drain on it.
"""
import functools
import hashlib
import os
import pickle
import sys
import tempfile
@@ -47,14 +54,25 @@ def store_cheap(n):
return s
def load_owner(t):
"""Below the 2500 W drain, above it, over drain + aim (5950 W), back."""
return (1200.0, 3000.0, 6500.0, 3000.0, -1500.0)[min(int(t // 480), 4)]
OWNER = {"peak_cap_w": 3500, "peak_margin_w": 50,
"maintenance_discharge_w": 2500}
SCENARIOS = {
"n1_drain": dict(n=1),
"n1_drain_modes": dict(n=1, modes=True),
"n1_drain_owner": dict(n=1, load=load_owner, extra=OWNER),
"n2_drain": dict(n=2),
"n2_drain_owner": dict(n=2, load=load_owner, extra=OWNER),
}
def run(n, *, modes=False, dur=DUR):
extra = dict(s79.P1, **DRAIN)
def run(n, *, modes=False, dur=DUR, load=load, extra=None):
extra = dict(s79.P1, **DRAIN, **(extra or {}))
if modes:
extra.update(p3.ARB, battery_modes_enabled=True)
seen, extra_rows = [], []
@@ -99,7 +117,65 @@ def digests() -> dict:
return {k: digest(run(**v)[0]) for k, v in SCENARIOS.items()}
FIELDS = ("t", "target", "commands", "loop reason", "integrator",
"saturation", "why", "allocation")
XFIELDS = ("claims", "guard", "roles", "trade", "phase", "log length",
"newest log line")
def wanted_more(row) -> bool:
"""Did the peak guard ask for more than the drain rate on this cycle?"""
claims, guard = row[-1][0] or (), row[-1][1]
rate = [c[3] for c in claims if c[0] == "maintenance" and c[2] == "set"]
return (bool(rate) and guard is not None and bool(guard[0])
and guard[1] is not None and guard[1] > max(rate))
def diff(base: dict, now: dict) -> dict:
"""Per scenario: (cycles, changed, changed where the guard wanted more on
either tree, changed cycles whose commands differ without that)."""
out = {}
for k, rows in now.items():
old = base.get(k)
if old is None:
continue
ch = [i for i, (a, b) in enumerate(zip(old, rows)) if a != b]
ch += list(range(min(len(old), len(rows)), max(len(old), len(rows))))
more = [i for i in ch if i < min(len(old), len(rows))
and (wanted_more(old[i]) or wanted_more(rows[i]))]
cmd = [i for i in ch if i not in more
and (i >= min(len(old), len(rows))
or old[i][1:3] != rows[i][1:3])]
fields = set()
for i in set(ch) - set(more) - set(cmd):
fields |= {FIELDS[j] for j in range(8) if old[i][j] != rows[i][j]}
fields |= {XFIELDS[j] for j in range(7)
if old[i][8][j] != rows[i][8][j]}
out[k] = (len(rows), ch, more, cmd, sorted(fields))
return out
def all_rows() -> dict:
return {k: run(**v)[0] for k, v in SCENARIOS.items()}
if __name__ == "__main__":
if "--dump" in sys.argv:
with open(sys.argv[sys.argv.index("--dump") + 1], "wb") as f:
pickle.dump(all_rows(), f)
sys.exit(0)
if "--diff" in sys.argv:
with open(sys.argv[sys.argv.index("--diff") + 1], "rb") as f:
base = pickle.load(f)
for k, (n, ch, more, cmd, fields) in diff(base, all_rows()).items():
rest = sorted(set(ch) - set(more))
print(f"{k}: {n} cycles, {len(ch)} changed, {len(more)} with the "
f"guard above the drain, {len(rest)} others "
f"({len(cmd)} with a different target or command"
+ (f"; the rest differ only in {', '.join(fields)}"
if fields else "") + ")"
+ (f", first changed t={ch[0] * rig.TICK:.0f} s" if ch else ""))
sys.exit(0)
if "--summary" in sys.argv:
for k, v in SCENARIOS.items():
rows = run(**v)[0]