SAFETY-119: no "not keeping up" when the loop takes the wire back #203
@@ -19,65 +19,61 @@ decided always counts. No command changes, and no text was added or changed.
|
|||||||
|
|
||||||
**A rolling budget (round 3).** The settle above is per hand-back, and
|
**A rolling budget (round 3).** The settle above is per hand-back, and
|
||||||
losing the wire restarts it. Across all hand-backs the bound is hidden on at
|
losing the wire restarts it. Across all hand-backs the bound is hidden on at
|
||||||
most 8 cycles in any 15 minutes (`ROLLING_BOUND_CYCLES`, `ROLLING_S`). A cycle
|
most a budget of cycles in any 15 minutes (`ROLLING_BUDGET`, `ROLLING_S`); a
|
||||||
over the budget counts. Round 1 restarted the settle at
|
cycle over the budget counts. Round 1 restarted the settle at every
|
||||||
every hand-back, so a guard and a charge taking turns hid the bound for good
|
hand-back, so a guard and a charge taking turns hid the bound for good (0
|
||||||
(0 warnings in 2 h where 28 were raised before). Round 2 gave each run of
|
warnings in 2 h where 28 were raised before). Round 2 gave each run of
|
||||||
hand-backs its own budget; at 10 s and 15 s meters each quarter then started
|
hand-backs its own budget; at 10 s and 15 s meters each quarter then started
|
||||||
a fresh one and the loop's bound was never counted. A quarter-edge kick is a
|
a fresh one and the loop's bound was never counted. A quarter-edge kick is a
|
||||||
few bound cycles in one quarter, a real bound that stays is many: a budget
|
few bound cycles in one quarter, a real bound that stays is many: a budget
|
||||||
per 15 minutes tells them apart.
|
per 15 minutes tells them apart.
|
||||||
|
|
||||||
**Why 8.** Measured over 2 h (`goodwe-addon-checks:1`), budgets 3 to 10, the
|
**The budget follows the cycle length.** 10 cycles up to a 3.5 s cycle, 8 up
|
||||||
quarter-edge case (house 800 W, cap 2500 W, 1 to 15 s meters) and the case
|
to 11 s, 6 up to 16 s, 4 beyond. The cycle length is the time between two
|
||||||
below. 3, 4, 5 and 6 let the false alarm back (at 2 s, for each). 7 let it
|
decided cycles (a stop, or a gap over two minutes, is not one); until it is
|
||||||
back on 10 of 30 seeds at 2 s. 8: 1 of 30 seeds at 2 s, none at 1, 5, 10 and 15 s.
|
known, and without a monotonic clock, the budget is 8. Without a monotonic
|
||||||
9 and 10: none, but the case below then misses quarters at 10 s. So 8 is the
|
clock the budget never refills.
|
||||||
largest budget that still warns every quarter at 2, 5 and 10 s, and the
|
|
||||||
margin is narrow: 7 and 9 each fail one side.
|
|
||||||
|
|
||||||
**What holds with 8.** Single hand-back, the loop at its rail: the first
|
**Why not one number.** Measured over 2 h (`goodwe-addon-checks:1`), the
|
||||||
counted bound cycle is the 9th, so 8 cycles after the hand-back (measured,
|
quarter-edge case (house 800 W, cap 2500 W, no forecast, 30 seeds) and the
|
||||||
real Controller): 8 s at 1 s, 16 s at 2 s, 40 s at 5 s (default), 80 s at
|
case below. No single budget held both at 1 to 15 s: 3 to 6 let the false
|
||||||
10 s, 120 s at 15 s, 240 s at 30 s, 480 s at 60 s. Repeated hand-backs
|
alarm back at 2 s; 7 on 10 of 30 seeds at 2 s; 8 on 1 of 30 (seed 25) at
|
||||||
(2 x 2500 W, cheap-window charge, no usable forecast, cap 5000 W, house
|
2 s, and warned in only 1 to 3 of 7 quarters at 15 s; 9 and 10 miss quarters
|
||||||
5050 and 5100 W, 2 h; 20 runs at 2, 5 and 10 s, 8 at 15 s):
|
at 10 s. A slow cycle needs a small budget to warn, a fast one a large budget
|
||||||
|
to stay quiet. Per band, the largest budget that still warns: up to 3.5 s,
|
||||||
|
10 (9 and 10 had no false alarm at 1 and 2 s; above 10 was not swept); up to
|
||||||
|
11 s, 8 (9 misses quarters at 10 s); up to 16 s, 6 (7 leaves two quiet
|
||||||
|
quarters in a row at 15 s, 4 false-alarms at 12 s on 7 of 30 seeds); beyond,
|
||||||
|
4 (3 false-alarms at 20 and 30 s on 14 of 30, 5 leaves five quiet quarters at
|
||||||
|
20 s).
|
||||||
|
|
||||||
|
**What holds.** Quarter-edge case, 30 seeds each: 0 notifications at 1, 2,
|
||||||
|
3, 4, 5, 10, 12, 15, 20 and 30 s. Repeated hand-backs (2 x 2500 W,
|
||||||
|
cheap-window charge, no usable forecast, cap 5000 W, house 5050 and 5100 W,
|
||||||
|
constant and with noise, 8 runs per meter):
|
||||||
|
|
||||||
| meter | warnings in 2 h | quarters warned (of 7 after the first) | without the fix |
|
| meter | warnings in 2 h | quarters warned (of 7 after the first) | without the fix |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| 2 s | 51 to 57 | 7 | 54 to 56 |
|
| 1 s | 56 to 63 | 7 | 56 to 63 |
|
||||||
| 5 s | 19 to 25 | 7 | 28 |
|
| 2 s | 49 to 56 | 7 | 54 to 56 |
|
||||||
| 10 s | 7 to 11 | 7 | 14 |
|
| 5 s | 19 to 23 | 7 | 28 |
|
||||||
| 15 s | 1 to 3 | 1 to 3 | 14 |
|
| 10 s | 7 to 10 | 7 | 14 |
|
||||||
| 30 s | 0 | 0 | 7 and 11 |
|
| 15 s | 4 to 10 | 4 to 7, never two quiet in a row | 14 |
|
||||||
|
| 30 s | 3 to 7 | 3 to 5, up to two quiet in a row | 7 to 11 |
|
||||||
|
|
||||||
At 15 s it warns in some quarters, not all. At 30 s it does not warn in this
|
At 30 s no budget does better: 4 leaves two quiet quarters, 3 brings back
|
||||||
case. For 60 s the rig's cycle is a different regime (the house is not over
|
the false alarm. Single hand-back, the loop at its rail, first warning after
|
||||||
the cap there), so nothing is claimed. The cap held at 2 s and 5 s; at 10 s
|
the hand-back compared with no fix: 10 s at 1 s, 20 s at 2 s, 40 s at 5 s
|
||||||
and 15 s the meter's lag puts the highest quarter 20 to 55 W over 5000 W,
|
(default), 80 s at 10 s, 90 s at 15 s, 80 s at 20 s, 120 s at 30 s, 240 s at
|
||||||
the same with and without the fix. Quarter-edge case, 15 s meter: the highest
|
60 s. At 60 s the rig is a different regime (the house is not over the cap
|
||||||
quarter is 2327 W to 2680 W, the same with and without the fix.
|
in the case above), so nothing more is claimed there. The cap held at 1 to
|
||||||
|
10 s in the quarter-edge case and at 1 to 5 s in the case above; at slower
|
||||||
**Accepted limits (owner, 2026-10-20, with N=8).** The owner accepted N=8
|
meters the meter's lag puts quarters over it, the same with and without the
|
||||||
knowing that:
|
fix (commands are identical).
|
||||||
- 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.
|
**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
|
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
|
kick of `test_safety119.py` part 1 stays hidden (see above).
|
||||||
1, 2, 5, 10 and 15 s meters).
|
|
||||||
|
|
||||||
**Re-pinned.** test_safety84 (all six) and test_safety87 (`n1_crossing`,
|
**Re-pinned.** test_safety84 (all six) and test_safety87 (`n1_crossing`,
|
||||||
`n1_above`, `n1_charge_low`): only OUTPUT_CLAMP log rows differ; commands and
|
`n1_above`, `n1_charge_low`): only OUTPUT_CLAMP log rows differ; commands and
|
||||||
|
|||||||
@@ -300,10 +300,13 @@ REFUSED_PROGRESS_W = _ctl.PROBE_ANSWER_W / 2
|
|||||||
# 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: 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
|
# ROLLING_BUDGET cycles in any ROLLING_S seconds, fewer on a slower cycle:
|
||||||
# DECISIONS/SAFETY-119-bound-after-hand-back.md). Without a monotonic clock
|
# (cycle seconds up to, cycles), measured in
|
||||||
|
# DECISIONS/SAFETY-119-bound-after-hand-back.md. Without a monotonic clock
|
||||||
# the budget never refills.
|
# the budget never refills.
|
||||||
ROLLING_BOUND_CYCLES, ROLLING_S = 8, 900.0
|
ROLLING_S = 900.0
|
||||||
|
ROLLING_BUDGET = ((3.5, 10), (11.0, 8), (16.0, 6), (float("inf"), 4))
|
||||||
|
ROLLING_BOUND_CYCLES = 8 # the budget while the cycle length is unknown
|
||||||
|
|
||||||
|
|
||||||
def _refused_interval(sign: int) -> tuple:
|
def _refused_interval(sign: int) -> tuple:
|
||||||
@@ -1279,10 +1282,12 @@ 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.
|
# monotonic s of the bound cycles hidden in the last ROLLING_S; the
|
||||||
|
# last cycle's monotonic s and the cycle length.
|
||||||
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
|
||||||
@@ -5825,6 +5830,7 @@ 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]
|
||||||
@@ -5867,12 +5873,18 @@ class Controller:
|
|||||||
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. The settle is SHORT_CYCLES cycles and SHORT_SPAN_S
|
||||||
after the hand-back; losing the wire restarts it. Across all
|
after the hand-back; losing the wire restarts it. Across all
|
||||||
hand-backs the bound is hidden on at most ROLLING_BOUND_CYCLES cycles
|
hand-backs the bound is hidden on at most ROLLING_BUDGET[cycle
|
||||||
in any ROLLING_S, so a bound that stays is counted however often the
|
length] cycles in any ROLLING_S, so a bound that stays is counted
|
||||||
wire changes hands. DECISIONS/SAFETY-119-bound-after-hand-back.md."""
|
however often the wire changes hands.
|
||||||
|
DECISIONS/SAFETY-119-bound-after-hand-back.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
|
||||||
@@ -5889,7 +5901,10 @@ class Controller:
|
|||||||
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))
|
||||||
if len(kept) >= ROLLING_BOUND_CYCLES:
|
dt = self._settle_dt
|
||||||
|
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,)
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ 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. Round 3: across all
|
||||||
hand-backs the bound is hidden on at most ROLLING_BOUND_CYCLES cycles in any
|
hand-backs the bound is hidden on at most ROLLING_BUDGET[cycle length] cycles
|
||||||
|
(10 up to a 3.5 s cycle, 8 up to 11 s, 6 up to 16 s, 4 beyond) in any
|
||||||
ROLLING_S seconds, so a bound that stays warns when the guard and the charge
|
ROLLING_S seconds, so a bound that stays warns when the guard and the charge
|
||||||
keep taking turns. DECISIONS/SAFETY-119-bound-after-hand-back.md.
|
keep taking turns. DECISIONS/SAFETY-119-bound-after-hand-back.md.
|
||||||
|
|
||||||
@@ -23,10 +24,11 @@ keep taking turns. DECISIONS/SAFETY-119-bound-after-hand-back.md.
|
|||||||
hand-back (after the settle).
|
hand-back (after the settle).
|
||||||
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), 5, 10 and
|
4. Repeated hand-backs (N=2, the guard and the charge take turns), 2, 5, 10
|
||||||
15 s meters: it warns (every quarter at 5 s and 10 s); the quarter-edge
|
and 15 s meters: it warns (every quarter at 2, 5 and 10 s, every other
|
||||||
case stays silent at 10 s and 15 s too; a start or a blind cycle is not a
|
quarter at least at 15 s); the quarter-edge case stays silent at 2, 5, 10
|
||||||
hand-back; a slow meter delays by cycles, not seconds.
|
and 15 s, including the 2 s seed where a fixed budget of 8 warns; a start
|
||||||
|
or a blind cycle is not a hand-back; a slow meter delays by cycles.
|
||||||
5. The rule at its boundaries (`Controller._settling`): the settle, and the
|
5. The rule at its boundaries (`Controller._settling`): the settle, and the
|
||||||
rolling budget.
|
rolling budget.
|
||||||
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
|
||||||
@@ -250,7 +252,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 = 8
|
MB = 10 # the budget at a 1 s cycle
|
||||||
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
|
||||||
@@ -329,8 +331,8 @@ def per_quarter(c):
|
|||||||
for q in range(8)]
|
for q in range(8)]
|
||||||
|
|
||||||
|
|
||||||
_nv = {t: never(t) for t in (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 (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))
|
||||||
for _t in _nv:
|
for _t in _nv:
|
||||||
print(f" N=2, {_t:.0f} s meter, house 5050 W: notifications "
|
print(f" N=2, {_t:.0f} s meter, house 5050 W: notifications "
|
||||||
@@ -339,20 +341,23 @@ 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("⭐ 5 s meter: it warns in every quarter after the first",
|
check("⭐ 2 s and 5 s meters: it warns in every quarter after the first",
|
||||||
all(per_quarter(_nv[5.0][1])[q] >= 1 for q in range(1, 8)))
|
all(per_quarter(_nv[t][1])[q] >= 1 for q in range(1, 8)
|
||||||
|
for t in (2.0, 5.0)))
|
||||||
check("⭐ 10 s meter: it warns in every quarter after the first (round 2 "
|
check("⭐ 10 s meter: it warns in every quarter after the first (round 2 "
|
||||||
"gave none)",
|
"gave none)",
|
||||||
all(per_quarter(_nv[10.0][1])[q] >= 1 for q in range(1, 8)))
|
all(per_quarter(_nv[10.0][1])[q] >= 1 for q in range(1, 8)))
|
||||||
check("⭐ 15 s meter: it warns (round 2 gave none), in at least one quarter "
|
check("⭐ 15 s meter: it warns at least every other quarter after the first "
|
||||||
"of the seven after the first",
|
"(round 2 gave none)",
|
||||||
sum(1 for n in per_quarter(_nv[15.0][1])[1:] if n) >= 1)
|
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(
|
||||||
_nv[t][1].clamp_wd.bound < _nv_old[t][1].clamp_wd.bound
|
_nv[t][1].clamp_wd.bound < _nv_old[t][1].clamp_wd.bound
|
||||||
for t in _nv))
|
for t in _nv))
|
||||||
check("...every command is the same as without the fix, at 5, 10 and 15 s",
|
check("...every command is the same as without the fix, at 2, 5, 10 and "
|
||||||
|
"15 s",
|
||||||
all(commands(_nv[t][0]) == commands(_nv_old[t][0]) for t in _nv))
|
all(commands(_nv[t][0]) == commands(_nv_old[t][0]) for t in _nv))
|
||||||
check("...and at 5 s the cap held",
|
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
|
||||||
@@ -361,9 +366,12 @@ check("...and at 5 s the cap held",
|
|||||||
# 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)): # the brief's fixed set
|
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
|
||||||
@@ -371,8 +379,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 "
|
||||||
"no notification",
|
"and (2 s) 25: 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 "
|
||||||
@@ -380,9 +388,20 @@ 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)))
|
||||||
check("the budget is the one the sweep chose (8). Changing it means "
|
rig.TICK = 2.0
|
||||||
"re-running the sweep in DECISIONS/SAFETY-119-bound-after-hand-back.md",
|
_q25_8 = patched(app_main, "ROLLING_BUDGET", ((math.inf, 8),))(
|
||||||
app_main.ROLLING_BOUND_CYCLES == 8 and app_main.ROLLING_S == 900.0)
|
lambda: run(house(25), delay=2))
|
||||||
|
rig.TICK = 1.0
|
||||||
|
print(f" 2 s meter, seed 25, a fixed budget of 8: notifications "
|
||||||
|
f"{notes(_q25_8[1])}")
|
||||||
|
check("...2 s seed 25 is the sweep's known case: a fixed budget of 8 warns "
|
||||||
|
"there (why the budget follows the cycle length)",
|
||||||
|
len(notes(_q25_8[1])) >= 1)
|
||||||
|
check("the budget is the one the sweep chose. Changing it means re-running "
|
||||||
|
"the sweep in DECISIONS/SAFETY-119-bound-after-hand-back.md",
|
||||||
|
app_main.ROLLING_BUDGET == ((3.5, 10), (11.0, 8), (16.0, 6),
|
||||||
|
(math.inf, 4))
|
||||||
|
and app_main.ROLLING_BOUND_CYCLES == 8 and app_main.ROLLING_S == 900.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).
|
||||||
@@ -398,6 +417,7 @@ check("⭐ a start after a blind spell is not a hand-back: it warns at the "
|
|||||||
|
|
||||||
# The budget is cycles, not seconds (R4): at a 5 s meter the loop at its
|
# The budget is cycles, not seconds (R4): at a 5 s meter the loop at its
|
||||||
# rail after a hand-back warns 8 cycles (40 s) later, not 8 s.
|
# rail after a hand-back warns 8 cycles (40 s) later, not 8 s.
|
||||||
|
MB = 8 # the budget at a 5 s cycle
|
||||||
rig.TICK = 5.0
|
rig.TICK = 5.0
|
||||||
_sl = run(house(1, step=(600.0, -6000.0)), dur=900.0,
|
_sl = run(house(1, step=(600.0, -6000.0)), dur=900.0,
|
||||||
extra={"peak_cap_w": 20000})
|
extra={"peak_cap_w": 20000})
|
||||||
@@ -418,6 +438,7 @@ 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)
|
||||||
|
|
||||||
|
|
||||||
@@ -537,6 +558,25 @@ _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
|
||||||
|
# one counts, one hand-back per turn.
|
||||||
|
_bud = {}
|
||||||
|
for _tick in (1.0, 2.0, 3.0, 5.0, 10.0, 15.0, 20.0, 30.0, 60.0):
|
||||||
|
_c = _C()
|
||||||
|
_o, _ = turns(_c, _tick, 1, min(14, int(880.0 // (2 * _tick))))
|
||||||
|
_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()
|
||||||
|
_o, _t = turns(_c, 2.0, 3, 8)
|
||||||
|
_o2, _ = turns(_c, 121.0, 1, 3, first=_t + 119.0)
|
||||||
|
check("a gap over two minutes is not a cycle length: the 2 s budget (10) "
|
||||||
|
"stays",
|
||||||
|
[h for _t2, h in _o + _o2] == [True] * 10 + [False])
|
||||||
|
|
||||||
# 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.
|
||||||
_c = _C()
|
_c = _C()
|
||||||
@@ -576,8 +616,10 @@ check("...while the integrator bound on the next cycle is not counted",
|
|||||||
_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",
|
check("a stop clears it: the next start is not a hand-back, and the stop "
|
||||||
(_ctl._wire_lost, _ctl._handback) == (False, None))
|
"is not a cycle length",
|
||||||
|
(_ctl._wire_lost, _ctl._handback, _ctl._settle_t)
|
||||||
|
== (False, None, None))
|
||||||
|
|
||||||
# ============================================================================ #
|
# ============================================================================ #
|
||||||
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 "
|
||||||
|
|||||||
Reference in New Issue
Block a user