SAFETY-121: a recurring overload warns every quarter
TEST-23 F-1: on a 5 s meter a steady overload warned once in 70 min. The
loop was at its bound 4-5 cycles a quarter, all inside SAFETY-119's settle
of 12 cycles (60 s) and under its budget of 8 per 900 s. The simulation
fed the meter a cycle behind the battery reading, so the guard and the
charge chased each other and gave ~20 bound cycles a quarter.
The settle is now 4 cycles and 22 s (HANDBACK_CYCLES, SHORT_SPAN_S), and
the rolling budget one number, 11 per 900 s, at every cycle length. The
cycle length is no longer tracked. Commands unchanged; the re-pinned
digests of test_safety84/87 differ from bee7431 in OUTPUT_CLAMP rows only.
test_safety119 runs the rig's shape (meter in the same cycle) at 1-15 s
and checks a stop keeps the budget (review F2, X3).
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
bee7431793
commit
8bf25696cb
@@ -1,6 +1,9 @@
|
|||||||
# SAFETY-119 - no "not keeping up" when the loop takes the wire back
|
# SAFETY-119 - no "not keeping up" when the loop takes the wire back
|
||||||
|
|
||||||
**Status: for review.** Tests: `test_safety119.py`.
|
**Status: merged (bee7431); the settle and the budget changed in SAFETY-121**
|
||||||
|
(DECISIONS/SAFETY-121-bound-every-quarter.md: 4 cycles and 22 s, one budget
|
||||||
|
of 11). The rolling-budget sections below describe bee7431. Tests:
|
||||||
|
`test_safety119.py`.
|
||||||
|
|
||||||
**Cause.** When another owner (the peak guard, a maintenance charge) has set
|
**Cause.** When another owner (the peak guard, a maintenance charge) has set
|
||||||
the wire and the loop wins again, `cycle()` re-seeds the integrator from that
|
the wire and the loop wins again, `cycle()` re-seeds the integrator from that
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# SAFETY-121 - a recurring overload warns every quarter
|
||||||
|
|
||||||
|
**Status: for review.** Tests: `test_safety119.py` (parts 2, 4, 5, 6).
|
||||||
|
Follows DECISIONS/SAFETY-119-bound-after-hand-back.md.
|
||||||
|
|
||||||
|
**The defect (TEST-23, SAFETY-119 rig test F-1).** 5 s meter, 2 x 2500 W,
|
||||||
|
cap 5000 W, maintenance charge, no forecast, house 5100 W. bee7431 warned
|
||||||
|
once in 70 min (04:31:01), then not at 04:45, 05:00 or 05:15. Base 75f0804
|
||||||
|
warned at every quarter start. The loop held the wire once a quarter, at its
|
||||||
|
start, and was at its bound on 4 or 5 cycles. SAFETY-119's settle (12
|
||||||
|
cycles, 60 s at 5 s) covered all of them, and 5 cycles a quarter fit in its
|
||||||
|
budget of 8 per 900 s.
|
||||||
|
|
||||||
|
**Why the simulation said "every quarter".** `test_safety119.py` fed the
|
||||||
|
meter one cycle behind the battery reading (`delay` counts cycles: 5 s at a
|
||||||
|
5 s meter). Just after the loop leaves its rail, the guard's charge
|
||||||
|
headroom (house = meter + battery) is then off by up to 5000 W, the charge
|
||||||
|
pulls 2000 W, the guard steps in again, and the two chase each other: 8 to
|
||||||
|
10 hand-backs and about 20 bound cycles a quarter at 5 s (rig: 1 and 4-5).
|
||||||
|
With the meter read in the same cycle (`delay` 0, the rig's shape) the sim
|
||||||
|
gives the rig's numbers: 3 to 5 bound cycles a quarter at 5 s, all inside
|
||||||
|
SAFETY-119's settle, and bee7431 warns in 0 to 2 of 7 quarters. The test
|
||||||
|
now runs both shapes.
|
||||||
|
|
||||||
|
**Decision.** After a hand-back the loop's bound is hidden until 4 cycles
|
||||||
|
(`HANDBACK_CYCLES`) and 22 s (`SHORT_SPAN_S`) have passed; across all
|
||||||
|
hand-backs on at most 11 cycles (`ROLLING_BOUND_CYCLES`) in any 900 s. The
|
||||||
|
budget no longer follows the cycle length, and the cycle length is not
|
||||||
|
tracked. A stop ends the settle and keeps the budget. An over-rating always
|
||||||
|
counts. Commands are unchanged.
|
||||||
|
|
||||||
|
**Why it holds for any number of bound cycles a quarter.** The settle is
|
||||||
|
fixed: at most 3 cycles or 22 s after a hand-back, whichever is longer. A
|
||||||
|
bound that lasts past it counts on its first cycle after it. So one
|
||||||
|
hand-back a quarter, with the loop at its rail past the settle, warns in
|
||||||
|
that quarter, whether it is 1 bound cycle or 100. A bound that never lasts
|
||||||
|
past a settle counts once more than 11 are hidden within 900 s. Hidden
|
||||||
|
are only shapes with at most 11 bound cycles in 900 s, each within 3 cycles
|
||||||
|
or 22 s of a hand-back. That is the quarter-edge kick: in every steady-meter
|
||||||
|
run below it is on the 1st to 3rd cycle after its hand-back (up to 30 s at
|
||||||
|
a 30 s meter). In the rig's shape at 5 s the bound comes 30 to 55 s after
|
||||||
|
the hand-back.
|
||||||
|
|
||||||
|
**Sweep** (`goodwe-addon-checks:1`, 2 h runs, scratch/SAFETY-121/dev/).
|
||||||
|
Quarter edge = test_safety119 part 1 (house 800 W, cap 2500 W, no
|
||||||
|
forecast). Overload = part 4 (2 x 2500 W, cap 5000 W, house 5050 or
|
||||||
|
5100 W, seeds 0-3). "Delay 1" is the old sim, "delay 0" the rig's shape.
|
||||||
|
|
||||||
|
| meter | quarter edge, runs with a warning (new / bee7431 / no fix) | overload delay 1, quarters warned of 7 (new / bee7431 / no fix) | overload delay 0, 5100 W (new / bee7431 / no fix) |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 s | 0/70 / 0/70 / 68/70 | 7 / 7 / 7 | 7 / 7 / 7 |
|
||||||
|
| 2 s | 0/70 / 0/70 / 39/70 | 7 / 7 / 7 | 7 / 7 / 7 |
|
||||||
|
| 3 s | 0/70 / 0/70 / 40/70 | 7 / 7 / 7 | 7 / 7 / 7 (5050 W: 7 / 1-7 / 7) |
|
||||||
|
| 5 s | 0/70 / 0/70 / 40/70 | 7 / 7 / 7 | 7 / 0-2 / 7 |
|
||||||
|
| 10 s | 0/70 / 0/70 / 40/70 | 7 / 7 / 7 | 7 / 0 / 7 |
|
||||||
|
| 15 s | 0/70 / 0/70 / 40/70 | 7 / 4-7 / 7 | no fix warns 0-1 (5300 W: 7 / 0 / 7) |
|
||||||
|
| 20 s | 0/60 / 0/60 / 30/60 | 7 / 5-7 / 7 | no fix warns 0 (5300 W: 7 / 0 / 7) |
|
||||||
|
| 30 s | 0/60 / 0/60 / 30/60 | 7 / 3-5 / 7 | no fix warns 0 (5300 W: 6-7 / 0 / 6-7) |
|
||||||
|
|
||||||
|
Quarter-edge seeds: 1-30 with the meter in the same cycle, and 1-40 (1-30
|
||||||
|
at 20 and 30 s) with the test's 1-2 cycle delay. 5300 W: seeds 1-5.
|
||||||
|
Commands identical to no fix and to bee7431 in all 1013 runs (these and
|
||||||
|
the limits below). Choosing the numbers: the runs above were recorded
|
||||||
|
without the fix and replayed through candidate rules; with 3, 4 or 5
|
||||||
|
cycles, budgets 9 to 13 met both cases at 1-30 s. 8 warns at the quarter
|
||||||
|
edge (2 s, seed 25) and 14 misses quarters in the rig's shape (1 s,
|
||||||
|
5050 W), both checked in part 4. 11 is the middle, and 4 the middle of 3
|
||||||
|
to 5; with a jittery meter (below) 3 gave more false alarms, 5 more quiet
|
||||||
|
quarters (replay).
|
||||||
|
|
||||||
|
**First warning after one hand-back** (the loop at its rail; later than
|
||||||
|
no fix by): 11 s at 1 s, 22 s at 2 s, 24 s at 3 s, 25 s at 5 s (default),
|
||||||
|
30 s at 10 s, 45 s at 15 s, 60 s at 20 s, 90 s at 30 s, 180 s at 60 s.
|
||||||
|
bee7431: 10, 20, 30, 40, 80, 90, 80, 120, 240 s. In the overload runs at
|
||||||
|
1 to 30 s, the first warning was at most 90 s later than without the fix.
|
||||||
|
|
||||||
|
**Limits (measured, not fixed).**
|
||||||
|
- A meter that jitters by 30 % with `cycle_floor_s` 15 (11 seeds): with
|
||||||
|
the test's 1-2 cycle meter delay the quarter edge warns on 2 of 11 seeds
|
||||||
|
at 15 s, 2 at 20 s and 5 at 30 s (bee7431: 4, 0, 10; no fix: 11 each);
|
||||||
|
with the meter in the same cycle on none (no fix: 10, 9, 11). The overload at 5100 W leaves up
|
||||||
|
to 4 quiet quarters in a row at 15 and 20 s (bee7431: no warning at all
|
||||||
|
on some seeds); at 5200 W at most 1.
|
||||||
|
- With cycles over about 40 s, a bound that is never past a settle cannot
|
||||||
|
use up 11 in 900 s, so it is never counted (part 5 prints it at 60 s).
|
||||||
|
- Hidden as designed: a real bound with the kick's shape (at most 11 cycles
|
||||||
|
in 900 s, each within 3 cycles or 22 s of a hand-back).
|
||||||
|
|
||||||
|
**Re-pinned.** test_safety84 (all six) and test_safety87 (`n1_crossing`,
|
||||||
|
`n1_above`, `n1_charge_low`): only OUTPUT_CLAMP log rows differ from
|
||||||
|
bee7431 and from no fix; commands, claims, guard and phases are the same.
|
||||||
@@ -299,14 +299,14 @@ REFUSED_PROGRESS_W = _ctl.PROBE_ANSWER_W / 2
|
|||||||
# was told over the same span as above (the plant's measured settle time), so
|
# was told over the same span as above (the plant's measured settle time), so
|
||||||
# a healthy battery still settling on a new command is not short.
|
# a healthy battery still settling on a new command is not short.
|
||||||
SHORT_CYCLES, SHORT_SPAN_S = _ctl.SETTLE_CYCLES, REFUSED_SPAN_S
|
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
|
# SAFETY-119/121: after a hand-back the loop's bound is hidden until
|
||||||
# ROLLING_BUDGET cycles in any ROLLING_S seconds, fewer on a slower cycle:
|
# HANDBACK_CYCLES cycles and SHORT_SPAN_S have passed; across all hand-backs
|
||||||
# (cycle seconds up to, cycles), measured in
|
# on at most ROLLING_BOUND_CYCLES cycles in any ROLLING_S seconds. Measured in
|
||||||
# DECISIONS/SAFETY-119-bound-after-hand-back.md. Without a monotonic clock
|
# DECISIONS/SAFETY-121-bound-every-quarter.md. Without a monotonic clock the
|
||||||
# the budget never refills.
|
# budget never refills.
|
||||||
|
HANDBACK_CYCLES = 4
|
||||||
ROLLING_S = 900.0
|
ROLLING_S = 900.0
|
||||||
ROLLING_BUDGET = ((3.5, 10), (11.0, 8), (16.0, 6), (float("inf"), 4))
|
ROLLING_BOUND_CYCLES = 11
|
||||||
ROLLING_BOUND_CYCLES = 8 # the budget while the cycle length is unknown
|
|
||||||
|
|
||||||
|
|
||||||
def _refused_interval(sign: int) -> tuple:
|
def _refused_interval(sign: int) -> tuple:
|
||||||
@@ -1282,12 +1282,11 @@ class Controller:
|
|||||||
self._short_run = 0
|
self._short_run = 0
|
||||||
# SAFETY-119: did another owner set the wire last cycle; the window
|
# SAFETY-119: did another owner set the wire last cycle; the window
|
||||||
# after a hand-back (monotonic s, the loop's cycles in a row); the
|
# after a hand-back (monotonic s, the loop's cycles in a row); the
|
||||||
# monotonic s of the bound cycles hidden in the last ROLLING_S; the
|
# monotonic s of the bound cycles hidden in the last ROLLING_S (a
|
||||||
# last cycle's monotonic s and the cycle length.
|
# stop does not clear them).
|
||||||
self._wire_lost = False
|
self._wire_lost = False
|
||||||
self._handback = None
|
self._handback = None
|
||||||
self._hidden = ()
|
self._hidden = ()
|
||||||
self._settle_t, self._settle_dt = None, None
|
|
||||||
self.stopping = False
|
self.stopping = False
|
||||||
# SAFETY-61. time.monotonic() at the end of the last run_control() pass.
|
# SAFETY-61. time.monotonic() at the end of the last run_control() pass.
|
||||||
self.loop_pass_mono = None
|
self.loop_pass_mono = None
|
||||||
@@ -5830,7 +5829,6 @@ class Controller:
|
|||||||
self._short_run = 0
|
self._short_run = 0
|
||||||
self._asked_hist = {}
|
self._asked_hist = {}
|
||||||
self._wire_lost, self._handback = False, None
|
self._wire_lost, self._handback = False, None
|
||||||
self._settle_t = None # a stop is not a cycle length
|
|
||||||
return
|
return
|
||||||
unmet = self.allocation.unmet_w if self.allocation is not None else 0
|
unmet = self.allocation.unmet_w if self.allocation is not None else 0
|
||||||
cut = (resolution.bound_by or "").split("(")[0]
|
cut = (resolution.bound_by or "").split("(")[0]
|
||||||
@@ -5871,20 +5869,15 @@ class Controller:
|
|||||||
"""SAFETY-119: does the loop's bound on this cycle count? Not within
|
"""SAFETY-119: does the loop's bound on this cycle count? Not within
|
||||||
a settle after another owner handed the wire back: its integrator is
|
a settle after another owner handed the wire back: its integrator is
|
||||||
re-seeded from that owner's command and kicked by a meter that does
|
re-seeded from that owner's command and kicked by a meter that does
|
||||||
not show it yet. The settle is SHORT_CYCLES cycles and SHORT_SPAN_S
|
not show it yet. SAFETY-121: the settle is HANDBACK_CYCLES cycles and
|
||||||
after the hand-back; losing the wire restarts it. Across all
|
SHORT_SPAN_S after the hand-back, so a bound that outlasts it counts
|
||||||
hand-backs the bound is hidden on at most ROLLING_BUDGET[cycle
|
at any cycle length; losing the wire restarts it. Across all
|
||||||
length] cycles in any ROLLING_S, so a bound that stays is counted
|
hand-backs the bound is hidden on at most ROLLING_BOUND_CYCLES cycles
|
||||||
however often the wire changes hands.
|
in any ROLLING_S, so a bound that never outlasts a settle is counted
|
||||||
DECISIONS/SAFETY-119-bound-after-hand-back.md."""
|
too. DECISIONS/SAFETY-121-bound-every-quarter.md."""
|
||||||
def aged(t0, span):
|
def aged(t0, span):
|
||||||
return now is None or t0 is None or now - t0 >= span
|
return now is None or t0 is None or now - t0 >= span
|
||||||
|
|
||||||
# The cycle length (a gap over two minutes is not one).
|
|
||||||
if now is not None and self._settle_t is not None \
|
|
||||||
and 0.0 < now - self._settle_t <= 120.0:
|
|
||||||
self._settle_dt = now - self._settle_t
|
|
||||||
self._settle_t = now
|
|
||||||
if not owns:
|
if not owns:
|
||||||
self._wire_lost, self._handback = True, None
|
self._wire_lost, self._handback = True, None
|
||||||
return False
|
return False
|
||||||
@@ -5895,16 +5888,13 @@ class Controller:
|
|||||||
tc, n = self._handback
|
tc, n = self._handback
|
||||||
n += 1
|
n += 1
|
||||||
self._handback = (tc, n)
|
self._handback = (tc, n)
|
||||||
if aged(tc, SHORT_SPAN_S) and n >= SHORT_CYCLES:
|
if aged(tc, SHORT_SPAN_S) and n >= HANDBACK_CYCLES:
|
||||||
self._handback = None
|
self._handback = None
|
||||||
return False
|
return False
|
||||||
if self.loop_clamped[0] and not self.loop_clamped[2]:
|
if self.loop_clamped[0] and not self.loop_clamped[2]:
|
||||||
kept = tuple(t for t in self._hidden if now is None or (
|
kept = tuple(t for t in self._hidden if now is None or (
|
||||||
t is not None and now - t < ROLLING_S))
|
t is not None and now - t < ROLLING_S))
|
||||||
dt = self._settle_dt
|
if len(kept) >= ROLLING_BOUND_CYCLES:
|
||||||
budget = ROLLING_BOUND_CYCLES if dt is None else next(
|
|
||||||
m for up, m in ROLLING_BUDGET if dt <= up)
|
|
||||||
if len(kept) >= budget:
|
|
||||||
self._hidden = kept
|
self._hidden = kept
|
||||||
return False
|
return False
|
||||||
self._hidden = kept + (now,)
|
self._hidden = kept + (now,)
|
||||||
|
|||||||
@@ -9,11 +9,12 @@ It crossed its rail for one or two cycles, the loop's ask beat the one-sided
|
|||||||
limit, so the cycle counted as the loop's own and raised
|
limit, so the cycle counted as the loop's own and raised
|
||||||
`goodwe_integrator_bound`. The battery gave what it was told. Now the loop's
|
`goodwe_integrator_bound`. The battery gave what it was told. Now the loop's
|
||||||
bound does not count until the plant's settle (SHORT_CYCLES cycles and
|
bound does not count until the plant's settle (SHORT_CYCLES cycles and
|
||||||
SHORT_SPAN_S) has passed since the hand-back. Round 3: across all
|
SHORT_SPAN_S) has passed since the hand-back. SAFETY-121: the settle is
|
||||||
hand-backs the bound is hidden on at most ROLLING_BUDGET[cycle length] cycles
|
HANDBACK_CYCLES (4) cycles and SHORT_SPAN_S (22 s), and across all
|
||||||
(10 up to a 3.5 s cycle, 8 up to 11 s, 6 up to 16 s, 4 beyond) in any
|
hand-backs the bound is hidden on at most ROLLING_BOUND_CYCLES (11) cycles in
|
||||||
ROLLING_S seconds, so a bound that stays warns when the guard and the charge
|
any ROLLING_S (900 s), at every cycle length.
|
||||||
keep taking turns. DECISIONS/SAFETY-119-bound-after-hand-back.md.
|
DECISIONS/SAFETY-119-bound-after-hand-back.md,
|
||||||
|
DECISIONS/SAFETY-121-bound-every-quarter.md.
|
||||||
|
|
||||||
1. The rig's shape (1 s meter that lags the battery, 1 s battery lag, house
|
1. The rig's shape (1 s meter that lags the battery, 1 s battery lag, house
|
||||||
800 W, rating 2500 W, limit 2500 W, margin 50, charge 2000 W, no
|
800 W, rating 2500 W, limit 2500 W, margin 50, charge 2000 W, no
|
||||||
@@ -25,12 +26,15 @@ keep taking turns. DECISIONS/SAFETY-119-bound-after-hand-back.md.
|
|||||||
3. With a usable forecast: the same commands and the same alarms as without
|
3. With a usable forecast: the same commands and the same alarms as without
|
||||||
the fix.
|
the fix.
|
||||||
4. Repeated hand-backs (N=2, the guard and the charge take turns), 2, 5, 10
|
4. Repeated hand-backs (N=2, the guard and the charge take turns), 2, 5, 10
|
||||||
and 15 s meters: it warns (every quarter at 2, 5 and 10 s, every other
|
and 15 s meters: it warns in every quarter, both with the meter a cycle
|
||||||
quarter at least at 15 s); the quarter-edge case stays silent at 2, 5, 10
|
behind the battery reading and in the rig's shape (SAFETY-121, F-1: the
|
||||||
and 15 s, including the 2 s seed where a fixed budget of 8 warns; a start
|
meter read in the same cycle, 1 to 5 bound cycles a quarter), where
|
||||||
or a blind cycle is not a hand-back; a slow meter delays by cycles.
|
SAFETY-119's settle of 12 cycles hid every quarter but one; the
|
||||||
5. The rule at its boundaries (`Controller._settling`): the settle, and the
|
quarter-edge case stays silent at 2, 5, 10 and 15 s; the budget's margins
|
||||||
rolling budget.
|
(8 warns at the quarter edge, 14 misses quarters); a start or a blind
|
||||||
|
cycle is not a hand-back; the delay after one hand-back.
|
||||||
|
5. The rule at its boundaries (`Controller._settling`): the settle, the
|
||||||
|
rolling budget, and a stop that does not give the budget back.
|
||||||
6. test_safety84's and test_safety87's re-pinned digests: the same commands
|
6. test_safety84's and test_safety87's re-pinned digests: the same commands
|
||||||
and claims; only OUTPUT_CLAMP log rows differ.
|
and claims; only OUTPUT_CLAMP log rows differ.
|
||||||
|
|
||||||
@@ -252,7 +256,7 @@ _first = notes(_hb[1])[:1]
|
|||||||
print(f" hand-back to a loop at its rail at 600 s: first notification "
|
print(f" hand-back to a loop at its rail at 600 s: first notification "
|
||||||
f"{_first}, without the fix {notes(_hb_old[1])[:1]}; command at the "
|
f"{_first}, without the fix {notes(_hb_old[1])[:1]}; command at the "
|
||||||
f"end {_hb[0][-1][1]}")
|
f"end {_hb[0][-1][1]}")
|
||||||
MB = 10 # the budget at a 1 s cycle
|
MB = 11 # the budget (at 1 s it ends the settle)
|
||||||
check(f"⭐ the loop at its rail after a hand-back still warns, once "
|
check(f"⭐ the loop at its rail after a hand-back still warns, once "
|
||||||
f"{MB} of its bound cycles are hidden (1 s meter: {MB} s)",
|
f"{MB} of its bound cycles are hidden (1 s meter: {MB} s)",
|
||||||
_first and 600.0 + MB <= _first[0] <= 600.0 + MB + 4.0
|
_first and 600.0 + MB <= _first[0] <= 600.0 + MB + 4.0
|
||||||
@@ -317,10 +321,18 @@ def round1(self, owns, now):
|
|||||||
# over the 5000 W rating from 300 s: the loop never held the wire for more
|
# over the 5000 W rating from 300 s: the loop never held the wire for more
|
||||||
# than 10 cycles. Round 1: no notification in 2 h; round 2: none at 10 s or
|
# than 10 cycles. Round 1: no notification in 2 h; round 2: none at 10 s or
|
||||||
# 15 s meters.
|
# 15 s meters.
|
||||||
def never(tick, hw=5050.0, fix=True):
|
def never(tick, hw=5050.0, fix=True, seed=0, delay=1):
|
||||||
|
"""`seed` 0: a constant house; else 800 W +- 30 W, `hw` +- 30 W from
|
||||||
|
300 s. `delay` 0 is the rig's shape: the meter read in the same cycle
|
||||||
|
as the battery (SAFETY-121)."""
|
||||||
rig.TICK = tick
|
rig.TICK = tick
|
||||||
|
if seed:
|
||||||
|
load = (lambda t, h=house(seed):
|
||||||
|
h(t) + (hw - 800.0 if t >= 300.0 else 0.0))
|
||||||
|
else:
|
||||||
|
load = house(1, step=(300.0, hw))
|
||||||
try:
|
try:
|
||||||
f = lambda: run(house(1, step=(300.0, hw)), n=2)
|
f = lambda: run(load, n=2, delay=delay)
|
||||||
return f() if fix else no_fix(f)
|
return f() if fix else no_fix(f)
|
||||||
finally:
|
finally:
|
||||||
rig.TICK = 1.0
|
rig.TICK = 1.0
|
||||||
@@ -331,6 +343,10 @@ def per_quarter(c):
|
|||||||
for q in range(8)]
|
for q in range(8)]
|
||||||
|
|
||||||
|
|
||||||
|
def every_quarter(c):
|
||||||
|
return all(n >= 1 for n in per_quarter(c)[1:])
|
||||||
|
|
||||||
|
|
||||||
_nv = {t: never(t) for t in (2.0, 5.0, 10.0, 15.0)}
|
_nv = {t: never(t) for t in (2.0, 5.0, 10.0, 15.0)}
|
||||||
_nv_old = {t: never(t, fix=False) for t in (2.0, 5.0, 10.0, 15.0)}
|
_nv_old = {t: never(t, fix=False) for t in (2.0, 5.0, 10.0, 15.0)}
|
||||||
_nv_r1 = patched(Controller, "_settling", round1)(lambda: never(5.0))
|
_nv_r1 = patched(Controller, "_settling", round1)(lambda: never(5.0))
|
||||||
@@ -341,16 +357,9 @@ for _t in _nv:
|
|||||||
f"{len(notes(_nv_old[_t][1]))}, highest quarter "
|
f"{len(notes(_nv_old[_t][1]))}, highest quarter "
|
||||||
f"{max(quarters(_nv[_t][1], _t)):.0f} W")
|
f"{max(quarters(_nv[_t][1], _t)):.0f} W")
|
||||||
print(f" round 1's rule at 5 s: {len(notes(_nv_r1[1]))}")
|
print(f" round 1's rule at 5 s: {len(notes(_nv_r1[1]))}")
|
||||||
check("⭐ 2 s and 5 s meters: it warns in every quarter after the first",
|
check("⭐ 2, 5, 10 and 15 s meters: it warns in every quarter after the "
|
||||||
all(per_quarter(_nv[t][1])[q] >= 1 for q in range(1, 8)
|
"first (round 2 gave none at 10 and 15 s)",
|
||||||
for t in (2.0, 5.0)))
|
all(every_quarter(_nv[t][1]) for t in _nv))
|
||||||
check("⭐ 10 s meter: it warns in every quarter after the first (round 2 "
|
|
||||||
"gave none)",
|
|
||||||
all(per_quarter(_nv[10.0][1])[q] >= 1 for q in range(1, 8)))
|
|
||||||
check("⭐ 15 s meter: it warns at least every other quarter after the first "
|
|
||||||
"(round 2 gave none)",
|
|
||||||
not any(a == 0 and b == 0 for a, b in zip(
|
|
||||||
per_quarter(_nv[15.0][1])[1:], per_quarter(_nv[15.0][1])[2:])))
|
|
||||||
check("non-vacuous: round 1's rule (a window per hand-back) never warned "
|
check("non-vacuous: round 1's rule (a window per hand-back) never warned "
|
||||||
"here, and the fix still hides some of what was raised without it",
|
"here, and the fix still hides some of what was raised without it",
|
||||||
notes(_nv_r1[1]) == [] and all(
|
notes(_nv_r1[1]) == [] and all(
|
||||||
@@ -363,15 +372,46 @@ check("...and at 5 s the cap held",
|
|||||||
len(quarters(_nv[5.0][1], 5.0)) == 8
|
len(quarters(_nv[5.0][1], 5.0)) == 8
|
||||||
and max(quarters(_nv[5.0][1], 5.0)) < 5000.0)
|
and max(quarters(_nv[5.0][1], 5.0)) < 5000.0)
|
||||||
|
|
||||||
|
# SAFETY-121, the rig's shape (TEST-23, F-1): the meter is read in the same
|
||||||
|
# cycle as the battery, so the guard's charge headroom is right after it
|
||||||
|
# hands back and the guard and the charge do not chase each other. The loop
|
||||||
|
# then holds the wire once a quarter, at its start, and is at its bound on
|
||||||
|
# 1 to 5 cycles (5 s), not 20. House 5100 W +- 30 W (5300 W at 15 s, where
|
||||||
|
# 5100 W is over the rating in one quarter only, without the fix too).
|
||||||
|
RIG = {1.0: 5100.0, 2.0: 5100.0, 5.0: 5100.0, 10.0: 5100.0, 15.0: 5300.0}
|
||||||
|
_rg = {t: never(t, hw, seed=1, delay=0) for t, hw in RIG.items()}
|
||||||
|
_rg_old = {t: never(t, hw, fix=False, seed=1, delay=0)
|
||||||
|
for t, hw in RIG.items()}
|
||||||
|
# SAFETY-119 as merged (bee7431) at 5 s: a settle of 12 cycles, budget 8.
|
||||||
|
_rg_119 = patched(app_main, "HANDBACK_CYCLES", app_main.SHORT_CYCLES)(
|
||||||
|
lambda: patched(app_main, "ROLLING_BOUND_CYCLES", 8)(
|
||||||
|
lambda: never(5.0, 5100.0, seed=1, delay=0)))
|
||||||
|
for _t in _rg:
|
||||||
|
print(f" rig shape, {_t:.0f} s meter, house {RIG[_t]:.0f} W: per "
|
||||||
|
f"quarter {per_quarter(_rg[_t][1])}, without the fix "
|
||||||
|
f"{per_quarter(_rg_old[_t][1])}, highest quarter "
|
||||||
|
f"{max(quarters(_rg[_t][1], _t)):.0f} W")
|
||||||
|
print(f" rig shape, 5 s, SAFETY-119's settle and budget: per quarter "
|
||||||
|
f"{per_quarter(_rg_119[1])}")
|
||||||
|
check("⭐ the rig's shape at 1, 2, 5, 10 and 15 s meters: it warns in every "
|
||||||
|
"quarter after the first, as without the fix",
|
||||||
|
all(every_quarter(_rg[t][1]) and every_quarter(_rg_old[t][1])
|
||||||
|
for t in _rg))
|
||||||
|
check("non-vacuous (F-1): with SAFETY-119's settle of 12 cycles and budget "
|
||||||
|
"of 8, the rig's shape at 5 s warns in at most one quarter",
|
||||||
|
sum(1 for n in per_quarter(_rg_119[1])[1:] if n) <= 1)
|
||||||
|
check("...every command the same as without the fix, and the cap held at "
|
||||||
|
"1 to 10 s",
|
||||||
|
all(commands(_rg[t][0]) == commands(_rg_old[t][0]) for t in _rg)
|
||||||
|
and all(max(quarters(_rg[t][1], t)) < 5000.0
|
||||||
|
for t in (1.0, 2.0, 5.0, 10.0)))
|
||||||
|
|
||||||
# The quarter-edge case (part 1) at the slower meters: still no warning.
|
# 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.
|
# The meter's own lag limits the cap there, the same with and without the fix.
|
||||||
_qe = {}
|
_qe = {}
|
||||||
# Seeds 1 to 3 are the brief's fixed set; 2 s seed 25 is the one where the
|
|
||||||
# sweep's fixed budget of 8 warned (DECISIONS), kept here so that stays seen.
|
|
||||||
for _t in (2.0, 5.0, 10.0, 15.0): # with part 1: 1, 2, 5, 10, 15 s
|
for _t in (2.0, 5.0, 10.0, 15.0): # with part 1: 1, 2, 5, 10, 15 s
|
||||||
rig.TICK = _t
|
rig.TICK = _t
|
||||||
for _seed, _dl in ((1, 1), (2, 1), (3, 2)) + (
|
for _seed, _dl in ((1, 1), (2, 1), (3, 2)):
|
||||||
((25, 2),) if _t == 2.0 else ()):
|
|
||||||
_qe[_t, _seed] = (run(house(_seed), delay=_dl),
|
_qe[_t, _seed] = (run(house(_seed), delay=_dl),
|
||||||
no_fix(lambda: run(house(_seed), delay=_dl)))
|
no_fix(lambda: run(house(_seed), delay=_dl)))
|
||||||
rig.TICK = 1.0
|
rig.TICK = 1.0
|
||||||
@@ -379,8 +419,8 @@ for _t in (2.0, 5.0, 10.0, 15.0): # with part 1: 1, 2, 5, 10, 15 s
|
|||||||
f"{[len(notes(r[0][1])) for k, r in _qe.items() if k[0] == _t]} "
|
f"{[len(notes(r[0][1])) for k, r in _qe.items() if k[0] == _t]} "
|
||||||
f"(without the fix "
|
f"(without the fix "
|
||||||
f"{[len(notes(r[1][1])) for k, r in _qe.items() if k[0] == _t]})")
|
f"{[len(notes(r[1][1])) for k, r in _qe.items() if k[0] == _t]})")
|
||||||
check("⭐ the quarter-edge case at 2, 5, 10 and 15 s meters, seeds 1 to 3 "
|
check("⭐ the quarter-edge case at 2, 5, 10 and 15 s meters, seeds 1 to 3: "
|
||||||
"and (2 s) 25: no notification",
|
"no notification",
|
||||||
all(notes(r[0][1]) == [] and r[0][1].clamp_wd.bound == 0
|
all(notes(r[0][1]) == [] and r[0][1].clamp_wd.bound == 0
|
||||||
for r in _qe.values()))
|
for r in _qe.values()))
|
||||||
check("...every command the same as without the fix; without it the false "
|
check("...every command the same as without the fix; without it the false "
|
||||||
@@ -388,20 +428,30 @@ check("...every command the same as without the fix; without it the false "
|
|||||||
all(commands(r[0][0]) == commands(r[1][0]) for r in _qe.values())
|
all(commands(r[0][0]) == commands(r[1][0]) for r in _qe.values())
|
||||||
and all(any(len(notes(_qe[_t, k][1][1])) >= 1 for k in (1, 2, 3))
|
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)))
|
for _t in (2.0, 5.0, 10.0, 15.0)))
|
||||||
|
|
||||||
|
# The budget's margins (DECISIONS/SAFETY-121): 9 to 13 met both cases in the
|
||||||
|
# sweep. 8 warns at the quarter edge (2 s, seed 25); 14 misses quarters in
|
||||||
|
# the rig's shape (1 s, house 5050 W, seed 1).
|
||||||
rig.TICK = 2.0
|
rig.TICK = 2.0
|
||||||
_q25_8 = patched(app_main, "ROLLING_BUDGET", ((math.inf, 8),))(
|
_q25 = (run(house(25), delay=2),
|
||||||
lambda: run(house(25), delay=2))
|
patched(app_main, "ROLLING_BOUND_CYCLES", 8)(
|
||||||
|
lambda: run(house(25), delay=2)))
|
||||||
rig.TICK = 1.0
|
rig.TICK = 1.0
|
||||||
print(f" 2 s meter, seed 25, a fixed budget of 8: notifications "
|
_m14 = patched(app_main, "ROLLING_BOUND_CYCLES", 14)(
|
||||||
f"{notes(_q25_8[1])}")
|
lambda: never(1.0, 5050.0, seed=1, delay=0))
|
||||||
check("...2 s seed 25 is the sweep's known case: a fixed budget of 8 warns "
|
_m11 = never(1.0, 5050.0, seed=1, delay=0)
|
||||||
"there (why the budget follows the cycle length)",
|
print(f" 2 s, quarter edge, seed 25: {notes(_q25[0][1])}, budget 8 "
|
||||||
len(notes(_q25_8[1])) >= 1)
|
f"{notes(_q25[1][1])}; rig shape 1 s 5050 W per quarter "
|
||||||
check("the budget is the one the sweep chose. Changing it means re-running "
|
f"{per_quarter(_m11[1])}, budget 14 {per_quarter(_m14[1])}")
|
||||||
"the sweep in DECISIONS/SAFETY-119-bound-after-hand-back.md",
|
check("...the budget's margins: 11 is silent at 2 s seed 25 and warns every "
|
||||||
app_main.ROLLING_BUDGET == ((3.5, 10), (11.0, 8), (16.0, 6),
|
"quarter in the rig's shape at 1 s and 5050 W; 8 warns there, 14 "
|
||||||
(math.inf, 4))
|
"misses a quarter there",
|
||||||
and app_main.ROLLING_BOUND_CYCLES == 8 and app_main.ROLLING_S == 900.0)
|
notes(_q25[0][1]) == [] and len(notes(_q25[1][1])) >= 1
|
||||||
|
and every_quarter(_m11[1]) and not every_quarter(_m14[1]))
|
||||||
|
check("the rule is the one the sweep chose. Changing it means re-running "
|
||||||
|
"the sweep in DECISIONS/SAFETY-121-bound-every-quarter.md",
|
||||||
|
app_main.HANDBACK_CYCLES == 4 and app_main.ROLLING_BOUND_CYCLES == 11
|
||||||
|
and app_main.ROLLING_S == 900.0 and app_main.SHORT_SPAN_S == 22.0)
|
||||||
|
|
||||||
# A start, or a blind cycle, is not a hand-back: the loop at its rail after
|
# A start, or a blind cycle, is not a hand-back: the loop at its rail after
|
||||||
# 20 s of not running warns at once (R2: a restart counted as a hand-back).
|
# 20 s of not running warns at once (R2: a restart counted as a hand-back).
|
||||||
@@ -415,19 +465,22 @@ check("⭐ a start after a blind spell is not a hand-back: it warns at the "
|
|||||||
notes(_bl[1])[:1] and 20.0 <= notes(_bl[1])[0] <= 26.0
|
notes(_bl[1])[:1] and 20.0 <= notes(_bl[1])[0] <= 26.0
|
||||||
and notes(_bl[1]) == notes(_bl_old[1]))
|
and notes(_bl[1]) == notes(_bl_old[1]))
|
||||||
|
|
||||||
# The budget is cycles, not seconds (R4): at a 5 s meter the loop at its
|
# One hand-back to a loop at its rail at 600 s: the first warning comes when
|
||||||
# rail after a hand-back warns 8 cycles (40 s) later, not 8 s.
|
# the settle ends, 22 s and 4 cycles after it (SAFETY-119: 12 cycles, 40 s at
|
||||||
MB = 8 # the budget at a 5 s cycle
|
# 5 s and 80 s at 10 s).
|
||||||
rig.TICK = 5.0
|
_sl = {}
|
||||||
_sl = run(house(1, step=(600.0, -6000.0)), dur=900.0,
|
for _t in (5.0, 10.0):
|
||||||
extra={"peak_cap_w": 20000})
|
rig.TICK = _t
|
||||||
rig.TICK = 1.0
|
_sl[_t] = run(house(1, step=(600.0, -6000.0)), dur=900.0,
|
||||||
_sl1 = [t for t in notes(_sl[1]) if t >= 600.0][:1]
|
extra={"peak_cap_w": 20000})
|
||||||
print(f" 5 s meter, hand-back to a loop at its rail at 600 s: first "
|
rig.TICK = 1.0
|
||||||
f"notification {_sl1}")
|
_sl1 = {t: [x for x in notes(r[1]) if x >= 600.0][:1] for t, r in _sl.items()}
|
||||||
check(f"⭐ at a 5 s meter the budget is cycles, not seconds: the first "
|
print(f" hand-back to a loop at its rail at 600 s, first notification: "
|
||||||
f"warning comes {MB} cycles ({MB * 5} s) after the hand-back",
|
f"5 s {_sl1[5.0]}, 10 s {_sl1[10.0]}")
|
||||||
_sl1 and 600.0 + MB * 5.0 <= _sl1[0] <= 600.0 + MB * 5.0 + 10.0)
|
check("⭐ one hand-back (the loop has the wire from the cycle after the "
|
||||||
|
"step): the first warning comes 25 s after it at a 5 s meter (the "
|
||||||
|
"22 s span), 30 s after it at 10 s (4 cycles)",
|
||||||
|
_sl1[5.0] == [630.0] and _sl1[10.0] == [640.0])
|
||||||
|
|
||||||
# ============================================================================ #
|
# ============================================================================ #
|
||||||
print("\n5. the rule at its boundaries")
|
print("\n5. the rule at its boundaries")
|
||||||
@@ -438,7 +491,6 @@ class _C:
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._wire_lost, self._handback, self._hidden = False, None, ()
|
self._wire_lost, self._handback, self._hidden = False, None, ()
|
||||||
self._settle_t, self._settle_dt = None, None
|
|
||||||
self.loop_clamped = (False, "", False)
|
self.loop_clamped = (False, "", False)
|
||||||
|
|
||||||
|
|
||||||
@@ -451,21 +503,30 @@ def seq(c, steps):
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
N, SPAN = app_main.SHORT_CYCLES, app_main.SHORT_SPAN_S
|
SPAN = app_main.SHORT_SPAN_S
|
||||||
M, ROLL = 8, 900.0 # the chosen budget, written out on purpose
|
K, M, ROLL = 4, 11, 900.0 # the chosen rule, written out on purpose
|
||||||
_c = _C()
|
_c = _C()
|
||||||
check("from the start, owned every cycle: never settling (no hand-back)",
|
check("from the start, owned every cycle: never settling (no hand-back)",
|
||||||
seq(_c, [(True, float(i)) for i in range(30)]) == [False] * 30)
|
seq(_c, [(True, float(i)) for i in range(30)]) == [False] * 30)
|
||||||
_c = _C()
|
_c = _C()
|
||||||
_out = seq(_c, [(False, 0.0)] + [(True, 2.0 * i) for i in range(1, N + 3)])
|
_out = seq(_c, [(False, 0.0)] + [(True, 2.0 * i) for i in range(1, 15)])
|
||||||
check(f"after another owner: settling for {N - 1} owned cycles, then not "
|
check(f"after another owner at 2 s: settling for 11 owned cycles, then not "
|
||||||
f"(12 cycles at 2 s span {SPAN:.0f} s)",
|
f"(the span, {SPAN:.0f} s, decides)",
|
||||||
_out == [False] + [True] * (N - 1) + [False] * 3)
|
_out == [False] + [True] * 11 + [False] * 3)
|
||||||
_c = _C()
|
_c = _C()
|
||||||
_out = seq(_c, [(False, 0.0)] + [(True, 0.5 * i) for i in range(1, 60)])
|
_out = seq(_c, [(False, 0.0)] + [(True, 0.5 * i) for i in range(1, 60)])
|
||||||
_end = _out.index(False, 1) if False in _out[1:] else -1
|
_end = _out.index(False, 1) if False in _out[1:] else -1
|
||||||
check(f"at a 0.5 s meter the span decides: settling until {SPAN:.0f} s",
|
check(f"at a 0.5 s meter the span decides: settling until {SPAN:.0f} s",
|
||||||
_end == 1 + int(SPAN / 0.5) and all(_out[1:_end]))
|
_end == 1 + int(SPAN / 0.5) and all(_out[1:_end]))
|
||||||
|
_set = {}
|
||||||
|
for _tick in (10.0, 30.0, 60.0):
|
||||||
|
_c = _C()
|
||||||
|
_set[_tick] = seq(_c, [(False, 0.0)]
|
||||||
|
+ [(True, _tick * i) for i in range(1, K + 2)])
|
||||||
|
check(f"at 10, 30 and 60 s the cycles decide: settling for {K - 1} owned "
|
||||||
|
f"cycles, then not (SAFETY-119: 11)",
|
||||||
|
all(o == [False] + [True] * (K - 1) + [False] * 2
|
||||||
|
for o in _set.values()))
|
||||||
_c = _C()
|
_c = _C()
|
||||||
_out = seq(_c, [(False, 0.0), (True, 2.0), (True, 4.0), (False, 6.0),
|
_out = seq(_c, [(False, 0.0), (True, 2.0), (True, 4.0), (False, 6.0),
|
||||||
(True, 8.0), (True, 10.0)])
|
(True, 8.0), (True, 10.0)])
|
||||||
@@ -474,12 +535,19 @@ check("losing the wire again restarts the settle",
|
|||||||
and _c._handback == (8.0, 2))
|
and _c._handback == (8.0, 2))
|
||||||
_c = _C()
|
_c = _C()
|
||||||
check("no monotonic clock: the cycle count alone decides",
|
check("no monotonic clock: the cycle count alone decides",
|
||||||
seq(_c, [(False, None)] + [(True, None)] * (N + 1))
|
seq(_c, [(False, None)] + [(True, None)] * (K + 1))
|
||||||
== [False] + [True] * (N - 1) + [False] * 2)
|
== [False] + [True] * (K - 1) + [False] * 2)
|
||||||
|
|
||||||
# The rolling budget: the loop's bound is hidden on at most M cycles in any
|
# One hand-back, the loop's bound on every cycle: the settle (5 s) or the
|
||||||
# ROLL seconds, across all hand-backs. T = the loop's bound that cycle.
|
# budget (1 s) ends the hiding.
|
||||||
assert M < N - 1, "the budget is meant to run out inside one settle"
|
_c = _C()
|
||||||
|
_o5 = seq(_c, [(False, 0.0)] + [(True, 5.0 * i, True) for i in range(1, 9)])
|
||||||
|
_c = _C()
|
||||||
|
_o1 = seq(_c, [(False, 0.0)] + [(True, 1.0 * i, True) for i in range(1, 21)])
|
||||||
|
check("one hand-back, the bound on every cycle: 5 hidden at 5 s (22 s), "
|
||||||
|
f"then counted; {M} hidden at 1 s (the budget), then counted",
|
||||||
|
_o5 == [False] + [True] * 5 + [False] * 3
|
||||||
|
and _o1 == [False] + [True] * M + [False] * (20 - M))
|
||||||
|
|
||||||
|
|
||||||
def turns(c, tick, lost, periods, first=0.0, bound=True):
|
def turns(c, tick, lost, periods, first=0.0, bound=True):
|
||||||
@@ -499,11 +567,8 @@ def turns(c, tick, lost, periods, first=0.0, bound=True):
|
|||||||
return out, t
|
return out, t
|
||||||
|
|
||||||
|
|
||||||
_c = _C()
|
# The rolling budget: the loop's bound is hidden on at most M cycles in any
|
||||||
_o = seq(_c, [(False, 0.0)] + [(True, 5.0 * i, True) for i in range(1, N)])
|
# ROLL seconds, across all hand-backs.
|
||||||
check(f"one hand-back at 5 s, the bound on every cycle: {M} are hidden, "
|
|
||||||
f"then it counts for the rest of the settle ({N - 1} cycles)",
|
|
||||||
_o == [False] + [True] * M + [False] * (N - 1 - M))
|
|
||||||
_c = _C()
|
_c = _C()
|
||||||
_o, _ = turns(_c, 5.0, 3, 60)
|
_o, _ = turns(_c, 5.0, 3, 60)
|
||||||
_hid = [t for t, h in _o if h]
|
_hid = [t for t, h in _o if h]
|
||||||
@@ -528,7 +593,7 @@ check(f"the budget refills {ROLL:.0f} s after the oldest hidden cycle: "
|
|||||||
_a is False and _b is True)
|
_a is False and _b is True)
|
||||||
_c = _C()
|
_c = _C()
|
||||||
_o1, _t = turns(_c, 2.0, 3, 20)
|
_o1, _t = turns(_c, 2.0, 3, 20)
|
||||||
for _ in range(N): # the wire away a whole settle
|
for _ in range(12): # the wire away a whole settle
|
||||||
_c._settling(False, _t)
|
_c._settling(False, _t)
|
||||||
_t += 2.0
|
_t += 2.0
|
||||||
_o2, _ = turns(_c, 2.0, 3, 5, first=_t)
|
_o2, _ = turns(_c, 2.0, 3, 5, first=_t)
|
||||||
@@ -539,7 +604,7 @@ check("the budget is not per hand-back or per run: after a whole settle "
|
|||||||
and not any(h for _t2, h in _o2))
|
and not any(h for _t2, h in _o2))
|
||||||
_c = _C()
|
_c = _C()
|
||||||
_o1, _t = turns(_c, 2.0, 3, 20)
|
_o1, _t = turns(_c, 2.0, 3, 20)
|
||||||
_ = [_c._settling(True, _t + 2.0 * i) for i in range(N + 1)]
|
_ = [_c._settling(True, _t + 2.0 * i) for i in range(13)]
|
||||||
_c._settling(False, _t + 40.0)
|
_c._settling(False, _t + 40.0)
|
||||||
_c.loop_clamped = (True, "", False)
|
_c.loop_clamped = (True, "", False)
|
||||||
check("the loop holding the wire a whole settle ends the window: the next "
|
check("the loop holding the wire a whole settle ends the window: the next "
|
||||||
@@ -558,24 +623,20 @@ _c.loop_clamped = (True, "", False)
|
|||||||
check("an over-rating does not use up the budget",
|
check("an over-rating does not use up the budget",
|
||||||
_c._settling(True, _t + 5.0) is True and len(_c._hidden) == 1)
|
_c._settling(True, _t + 5.0) is True and len(_c._hidden) == 1)
|
||||||
|
|
||||||
# The budget follows the cycle length: bound cycles hidden before the first
|
# The budget is one number at every cycle length (SAFETY-119 scaled it).
|
||||||
# one counts, one hand-back per turn.
|
|
||||||
_bud = {}
|
_bud = {}
|
||||||
for _tick in (1.0, 2.0, 3.0, 5.0, 10.0, 15.0, 20.0, 30.0, 60.0):
|
for _tick in (1.0, 2.0, 3.0, 5.0, 10.0, 15.0, 20.0, 30.0):
|
||||||
_c = _C()
|
_c = _C()
|
||||||
_o, _ = turns(_c, _tick, 1, min(14, int(880.0 // (2 * _tick))))
|
_o, _ = turns(_c, _tick, 1, 14)
|
||||||
_bud[_tick] = sum(1 for _t2, h in _o if h)
|
_bud[_tick] = sum(1 for _t2, h in _o if h)
|
||||||
print(f" hidden before the first counts, by cycle length: {_bud}")
|
|
||||||
check("the budget by cycle length: 10 at 1-3 s, 8 at 5-10 s, 6 at 15 s, 4 "
|
|
||||||
"at 20-60 s",
|
|
||||||
_bud == {1.0: 10, 2.0: 10, 3.0: 10, 5.0: 8, 10.0: 8, 15.0: 6,
|
|
||||||
20.0: 4, 30.0: 4, 60.0: 4})
|
|
||||||
_c = _C()
|
_c = _C()
|
||||||
_o, _t = turns(_c, 2.0, 3, 8)
|
_o60, _ = turns(_c, 60.0, 1, 14)
|
||||||
_o2, _ = turns(_c, 121.0, 1, 3, first=_t + 119.0)
|
print(f" hidden before the first counts, one bound cycle per turn: {_bud}; "
|
||||||
check("a gap over two minutes is not a cycle length: the 2 s budget (10) "
|
f"at 60 s, turns of 2 cycles: {sum(1 for _t2, h in _o60 if h)} of 14 "
|
||||||
"stays",
|
"hidden (the budget never runs out: DECISIONS limit)")
|
||||||
[h for _t2, h in _o + _o2] == [True] * 10 + [False])
|
check(f"the budget does not follow the cycle length: {M} hidden at 1 to "
|
||||||
|
"30 s, then counted",
|
||||||
|
set(_bud.values()) == {M})
|
||||||
|
|
||||||
# No monotonic clock: the budget cannot refill, so a bound that stays is
|
# No monotonic clock: the budget cannot refill, so a bound that stays is
|
||||||
# counted after the budget however often the wire changes hands.
|
# counted after the budget however often the wire changes hands.
|
||||||
@@ -586,8 +647,8 @@ for _ in range(40):
|
|||||||
_c._settling(False, None)
|
_c._settling(False, None)
|
||||||
_c.loop_clamped = (True, "", False)
|
_c.loop_clamped = (True, "", False)
|
||||||
_nc.append(_c._settling(True, None))
|
_nc.append(_c._settling(True, None))
|
||||||
check("no monotonic clock, repeated hand-backs: the budget holds (8 hidden, "
|
check(f"no monotonic clock, repeated hand-backs: the budget holds ({M} "
|
||||||
"then every bound counts)", _nc == [True] * M + [False] * (40 - M))
|
"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.
|
# An over-rating (a command over `max_w`) is a bug, never settled away.
|
||||||
_ctl = _hb[1]
|
_ctl = _hb[1]
|
||||||
@@ -613,13 +674,22 @@ _ctl._loop_alarm_input(_Res())
|
|||||||
check("...while the integrator bound on the next cycle is not counted",
|
check("...while the integrator bound on the next cycle is not counted",
|
||||||
_ctl.loop_clamped[0] is False)
|
_ctl.loop_clamped[0] is False)
|
||||||
|
|
||||||
|
# A stop ends the window but does not give the budget back (review F2, X3).
|
||||||
|
_now = _ctl._now_mono
|
||||||
|
_ctl._hidden = (_now,) * M
|
||||||
_ctl._wire_lost, _ctl._handback = True, (1.0, 3)
|
_ctl._wire_lost, _ctl._handback = True, (1.0, 3)
|
||||||
_ctl._loop_decision = None # stopped or blind
|
_ctl._loop_decision = None # stopped or blind
|
||||||
_ctl._loop_alarm_input(_Res())
|
_ctl._loop_alarm_input(_Res())
|
||||||
check("a stop clears it: the next start is not a hand-back, and the stop "
|
_st, _kept = (_ctl._wire_lost, _ctl._handback), _ctl._hidden
|
||||||
"is not a cycle length",
|
_ctl._settling(False, _now + 1.0)
|
||||||
(_ctl._wire_lost, _ctl._handback, _ctl._settle_t)
|
_ctl.loop_clamped = (True, "", False)
|
||||||
== (False, None, None))
|
_after = _ctl._settling(True, _now + 2.0)
|
||||||
|
check("a stop clears the window (the next start is not a hand-back) and "
|
||||||
|
f"keeps the budget: with {M} hidden, the bound after the next "
|
||||||
|
"hand-back counts",
|
||||||
|
_st == (False, None) and _kept == (_now,) * M and _after is False)
|
||||||
|
print(f" after the stop: hidden kept {len(_kept)}, the next hand-back's "
|
||||||
|
f"bound hidden {_after}")
|
||||||
|
|
||||||
# ============================================================================ #
|
# ============================================================================ #
|
||||||
print("\n6. the pinned digests of test_safety84 and test_safety87: only the "
|
print("\n6. the pinned digests of test_safety84 and test_safety87: only the "
|
||||||
|
|||||||
@@ -474,20 +474,21 @@ 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.
|
# 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
|
# 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
|
# loop's bound just after the guard hands back are gone; every command and
|
||||||
# claim is the same (test_safety119.py part 6).
|
# claim is the same (test_safety119.py part 6). SAFETY-121 re-pinned all six
|
||||||
|
# again: only OUTPUT_CLAMP rows (test_safety119.py part 6).
|
||||||
PINNED = {
|
PINNED = {
|
||||||
"n1_charge":
|
"n1_charge":
|
||||||
"19781f90b9751b10994dedeaafb38e42d48ad2551640792e601336b7f9b5bb4f",
|
"0ed25c856456b97acb07a39299a8cb7b1a762c4d0005b88a0823c03057a80be5",
|
||||||
"n1_charge_dear":
|
"n1_charge_dear":
|
||||||
"eeeb0fa19a6fdc04d44a0838bb2bb911f506a936b0f93d8a6f4d87159053ec74",
|
"c25340b56060d4581f7204e200b07e7c8618711be2baca3b1667b7ebeb600dc3",
|
||||||
"n1_charge_banked":
|
"n1_charge_banked":
|
||||||
"a51f125a8c1c98fdbb009fce72b6781d20d384bdf0f5ce7bbfe367d86e5477db",
|
"2619acdd06b04e56c000b168d64e1d044346c6ba3f929c94258d6ad8189ec609",
|
||||||
"n2_charge":
|
"n2_charge":
|
||||||
"e37e98390baa5dad9128991fabf8b170894395fe026d8a33c8a51de7ed0789cc",
|
"b16d692128f14bcce3d87b142d290e0d6b13724289680e44a05215c9eaa24a7c",
|
||||||
"n2_charge_dear":
|
"n2_charge_dear":
|
||||||
"7727372de423f62d77b4426a7226eae31a30f3bb89a3cf70272cc7c1823d7263",
|
"aa2bbc677a8e2274b25ba1170de1b1635e08a3bd93105e2a38a9bb1575b5b6db",
|
||||||
"n2_charge_banked":
|
"n2_charge_banked":
|
||||||
"c77e26006c026b953e9c3dcc9a02f3993589f885cb52c7cbceac6bd59b600551",
|
"a6973faea916e4c88ca1770047f94478346eeb1efa4889b154fad0747f91267f",
|
||||||
}
|
}
|
||||||
_now = s84.digests()
|
_now = s84.digests()
|
||||||
for k in PINNED:
|
for k in PINNED:
|
||||||
@@ -495,7 +496,7 @@ for k in PINNED:
|
|||||||
f"{_now[k][:16]}")
|
f"{_now[k][:16]}")
|
||||||
check("⭐ at N=1 a charge in and out of a cheap window and under a banked "
|
check("⭐ at N=1 a charge in and out of a cheap window and under a banked "
|
||||||
"month peak is byte-identical to 9d5b7d1 (commands, reasons and every "
|
"month peak is byte-identical to 9d5b7d1 (commands, reasons and every "
|
||||||
"claim; the log as re-pinned by SAFETY-119), and N=2 outside a cheap "
|
"claim; the log as re-pinned by SAFETY-121), and N=2 outside a cheap "
|
||||||
"window matches its SAFETY-85 pin",
|
"window matches its SAFETY-85 pin",
|
||||||
all(_now[k] == PINNED[k] for k in ("n1_charge", "n1_charge_dear",
|
all(_now[k] == PINNED[k] for k in ("n1_charge", "n1_charge_dear",
|
||||||
"n1_charge_banked",
|
"n1_charge_banked",
|
||||||
|
|||||||
@@ -521,18 +521,19 @@ 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
|
# 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
|
# 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
|
# rows are gone or count fewer; every command and claim is the same
|
||||||
# (test_safety119.py part 6).
|
# (test_safety119.py part 6). SAFETY-121 re-pinned the same three again:
|
||||||
|
# only OUTPUT_CLAMP rows (test_safety119.py part 6).
|
||||||
PINNED = { # tools/safety87_identity.py on b8ddfd7
|
PINNED = { # tools/safety87_identity.py on b8ddfd7
|
||||||
"n1_under":
|
"n1_under":
|
||||||
"d2395ef8d74fb23607ad864d5a4bf62c03044bbd9367e873675d7ad10e8c169f",
|
"d2395ef8d74fb23607ad864d5a4bf62c03044bbd9367e873675d7ad10e8c169f",
|
||||||
"n1_crossing":
|
"n1_crossing":
|
||||||
"20f8e542c1ee3eb3a58c8fd6ba3b3e5b6b1486800cd63b675abc68bc222a172b",
|
"97f7321017d9d34ee17a40dfd4abf73d915f25b9faef086a483d2b5cd124b198",
|
||||||
"n1_above":
|
"n1_above":
|
||||||
"3a79f1394b2b49fa88f3c054da1d9da450153ad5f0d2a2c2f960d02a7abe2208",
|
"9229b0f0e8addd1bd114461cc36c7f02e9734012b3b7e154d7747bd4cc69b773",
|
||||||
"n1_under_banked":
|
"n1_under_banked":
|
||||||
"a2876c7cc873bc4c974d2b4bd98e23bba2d007b84545587d2fe0b713b0a6249b",
|
"a2876c7cc873bc4c974d2b4bd98e23bba2d007b84545587d2fe0b713b0a6249b",
|
||||||
"n1_charge_low":
|
"n1_charge_low":
|
||||||
"2e1ee6db0d4f9f18836c4cbbe406fddd496656b54b97f329da513a1591f0ce6c",
|
"91336e3c0314985a3b00f314c79b5ffb5a740d218de3ad44f88a4db6e4235b32",
|
||||||
"n1_hold":
|
"n1_hold":
|
||||||
"0a1df347754c69166b20d04c15ee878bbcefb02abb824b9b452334f68f03e46d",
|
"0a1df347754c69166b20d04c15ee878bbcefb02abb824b9b452334f68f03e46d",
|
||||||
}
|
}
|
||||||
@@ -540,7 +541,7 @@ _now = s87.digests()
|
|||||||
for k in PINNED:
|
for k in PINNED:
|
||||||
print(f" {k} {'same' if _now[k] == PINNED[k] else 'DIFFERS'}")
|
print(f" {k} {'same' if _now[k] == PINNED[k] else 'DIFFERS'}")
|
||||||
check("⭐ every N=1 digest (claims, commands, the guard's decision and the "
|
check("⭐ every N=1 digest (claims, commands, the guard's decision and the "
|
||||||
"log) equals b8ddfd7's (the log as re-pinned by SAFETY-119), under, "
|
"log) equals b8ddfd7's (the log as re-pinned by SAFETY-121), under, "
|
||||||
"crossing and above the reserve, in a charge and a HOLD, and under a "
|
"crossing and above the reserve, in a charge and a HOLD, and under a "
|
||||||
"banked peak",
|
"banked peak",
|
||||||
_now == PINNED)
|
_now == PINNED)
|
||||||
|
|||||||
Reference in New Issue
Block a user