SAFETY-119: no "not keeping up" when the loop takes the wire back #203

Merged
admin merged 11 commits from SAFETY-119 into release/1.0 2026-10-21 03:32:39 +02:00
5 changed files with 41 additions and 12 deletions
Showing only changes of commit e27e62ff72 - Show all commits
@@ -58,6 +58,22 @@ and 15 s the meter's lag puts the highest quarter 20 to 55 W over 5000 W,
the same with and without the fix. Quarter-edge case, 15 s meter: the highest
quarter is 2327 W to 2680 W, the same with and without the fix.
**Accepted limits (owner, 2026-10-20, with N=8).** The owner accepted N=8
knowing that:
- at a 2 s meter the quarter-edge false alarm still shows on about 1 of 30
seeds (one notification in 2 h);
- at a 15 s meter the case above warns in only 1 to 3 of the 7 quarters
(some runs are silent for more than one quarter in a row);
- at a 30 s meter and slower it gives no warning in that case (0 in 2 h,
against 7 and 11 without the fix): a real bound is suppressed there;
- the cap is exceeded at 10 s and slower in that case (and up to 2680 W of
2500 W at 15 s on the quarter edge), the same with and without the fix.
A budget that follows the cycle length (10 cycles up to 3.5 s, 8 to 11 s, 6
to 16 s, 4 beyond) met both cases on every swept seed at 1 to 20 s (at
30 s: no false alarm, 3 to 5 of 7 quarters) in the dev round-3 sweep; it was
not adopted. Without a monotonic clock the budget
never refills, so a bound that stays is counted after 8 hidden cycles.
**Kept.** The loop in charge at its rail warns on the same cycle as before.
The start and a restart after a stop are not hand-backs. The quarter-edge
kick of `test_safety119.py` part 1 stays hidden (0 warnings in 2 h at
+4 -2
View File
@@ -301,7 +301,8 @@ REFUSED_PROGRESS_W = _ctl.PROBE_ANSWER_W / 2
SHORT_CYCLES, SHORT_SPAN_S = _ctl.SETTLE_CYCLES, REFUSED_SPAN_S
# SAFETY-119: across all hand-backs, the loop's bound is hidden on at most
# this many cycles in any ROLLING_S seconds (a budget measured in
# DECISIONS/SAFETY-119-bound-after-hand-back.md).
# DECISIONS/SAFETY-119-bound-after-hand-back.md). Without a monotonic clock
# the budget never refills.
ROLLING_BOUND_CYCLES, ROLLING_S = 8, 900.0
@@ -5886,7 +5887,8 @@ class Controller:
self._handback = None
return False
if self.loop_clamped[0] and not self.loop_clamped[2]:
kept = tuple(t for t in self._hidden if not aged(t, ROLLING_S))
kept = tuple(t for t in self._hidden if now is None or (
t is not None and now - t < ROLLING_S))
if len(kept) >= ROLLING_BOUND_CYCLES:
self._hidden = kept
return False
+19 -8
View File
@@ -361,11 +361,9 @@ check("...and at 5 s the cap held",
# The quarter-edge case (part 1) at the slower meters: still no warning.
# The meter's own lag limits the cap there, the same with and without the fix.
_qe = {}
for _t, _seeds in ((10.0, ((1, 1), (2, 1), (3, 2))),
(15.0, ((1, 1), (2, 1), (3, 2))),
(2.0, ((8, 1), (9, 2)))): # a budget of 7 warns here
for _t in (2.0, 5.0, 10.0, 15.0): # with part 1: 1, 2, 5, 10, 15 s
rig.TICK = _t
for _seed, _dl in _seeds:
for _seed, _dl in ((1, 1), (2, 1), (3, 2)): # the brief's fixed set
_qe[_t, _seed] = (run(house(_seed), delay=_dl),
no_fix(lambda: run(house(_seed), delay=_dl)))
rig.TICK = 1.0
@@ -373,14 +371,15 @@ for _t, _seeds in ((10.0, ((1, 1), (2, 1), (3, 2))),
f"{[len(notes(r[0][1])) for k, r in _qe.items() if k[0] == _t]} "
f"(without the fix "
f"{[len(notes(r[1][1])) for k, r in _qe.items() if k[0] == _t]})")
check("⭐ the quarter-edge case at 10 s and 15 s meters, and at 2 s on two "
"more seeds: no notification",
check("⭐ the quarter-edge case at 2, 5, 10 and 15 s meters, seeds 1 to 3: "
"no notification",
all(notes(r[0][1]) == [] and r[0][1].clamp_wd.bound == 0
for r in _qe.values()))
check("...every command the same as without the fix; without it the false "
"alarm is there (non-vacuous)",
"alarm is there at each meter (non-vacuous)",
all(commands(r[0][0]) == commands(r[1][0]) for r in _qe.values())
and all(len(notes(r[1][1])) >= 1 for r in _qe.values()))
and all(any(len(notes(_qe[_t, k][1][1])) >= 1 for k in (1, 2, 3))
for _t in (2.0, 5.0, 10.0, 15.0)))
check("the budget is the one the sweep chose (8). Changing it means "
"re-running the sweep in DECISIONS/SAFETY-119-bound-after-hand-back.md",
app_main.ROLLING_BOUND_CYCLES == 8 and app_main.ROLLING_S == 900.0)
@@ -538,6 +537,18 @@ _c.loop_clamped = (True, "", False)
check("an over-rating does not use up the budget",
_c._settling(True, _t + 5.0) is True and len(_c._hidden) == 1)
# No monotonic clock: the budget cannot refill, so a bound that stays is
# counted after the budget however often the wire changes hands.
_c = _C()
_nc = []
for _ in range(40):
for _ in range(3):
_c._settling(False, None)
_c.loop_clamped = (True, "", False)
_nc.append(_c._settling(True, None))
check("no monotonic clock, repeated hand-backs: the budget holds (8 hidden, "
"then every bound counts)", _nc == [True] * M + [False] * (40 - M))
# An over-rating (a command over `max_w`) is a bug, never settled away.
_ctl = _hb[1]
_ctl.loop_clamped = (True, "x", True)
+1 -1
View File
@@ -474,7 +474,7 @@ print("\n8. the N=1 and N=2 digests")
# pin lifts (t=1800) and B no longer jumps 1390 W, and after the surplus step.
# SAFETY-119 re-pinned all six: only the log. OUTPUT_CLAMP rows from the
# loop's bound just after the guard hands back are gone; every command and
# claim is the same (test_safety119.py part 5).
# claim is the same (test_safety119.py part 6).
PINNED = {
"n1_charge":
"19781f90b9751b10994dedeaafb38e42d48ad2551640792e601336b7f9b5bb4f",
+1 -1
View File
@@ -521,7 +521,7 @@ print("\n9. N=1 is byte-identical to release/1.0 b8ddfd7")
# SAFETY-119 re-pinned n1_crossing, n1_above and n1_charge_low: only the
# log. The loop's bound just after a hand-back is not counted, so OUTPUT_CLAMP
# rows are gone or count fewer; every command and claim is the same
# (test_safety119.py part 5).
# (test_safety119.py part 6).
PINNED = { # tools/safety87_identity.py on b8ddfd7
"n1_under":
"d2395ef8d74fb23607ad864d5a4bf62c03044bbd9367e873675d7ad10e8c169f",