SAFETY-119 round 3: rolling 15-minute budget for the hidden integrator bound
At most 8 hidden bound cycles in any 900 s across all hand-backs, replacing the per-run budget. Chosen from a sweep (N 3 to 10, 1 to 15 s meters, both cases); DECISIONS has the table. Tests: never case at 5, 10, 15 s, quarter edge at 2, 10, 15 s, the rolling budget at its boundaries. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
85542e443b
commit
103db2fcda
@@ -17,34 +17,51 @@ until the plant's settle has passed (`SHORT_CYCLES` cycles and
|
||||
counts as before. An over-rating (`clamped`, a bug) on a cycle the loop
|
||||
decided always counts. No command changes, and no text was added or changed.
|
||||
|
||||
**One window per run of hand-backs (round 2).** A run is hand-backs with the
|
||||
wire away less than a settle (12 cycles and 22 s) between them. It ends when
|
||||
the loop holds the wire, or the wire is away, a whole settle in a row. Each
|
||||
hand-back restarts the settle, but a run hides the bound for at most 11 bound
|
||||
cycles and 22 s in all. Round 1 restarted the window at every hand-back, so
|
||||
a guard and a charge taking turns hid the bound for good (0 warnings in 2 h
|
||||
where 28 were raised before). Now a bound that stays warns after at most 11
|
||||
of its cycles, however often the wire changes hands.
|
||||
**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
|
||||
most 8 cycles in any 15 minutes (`ROLLING_BOUND_CYCLES`, `ROLLING_S`). A cycle
|
||||
over the budget counts. Round 1 restarted the settle at
|
||||
every hand-back, so a guard and a charge taking turns hid the bound for good
|
||||
(0 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
|
||||
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
|
||||
per 15 minutes tells them apart.
|
||||
|
||||
**Delay.** The first counted bound cycle, from the hand-back (worst case):
|
||||
**Why 8.** Measured over 2 h (`goodwe-addon-checks:1`), budgets 3 to 10, the
|
||||
quarter-edge case (house 800 W, cap 2500 W, 1 to 15 s meters) and the case
|
||||
below. 3, 4, 5 and 6 let the false alarm back (at 2 s, for each). 7 let it
|
||||
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.
|
||||
9 and 10: none, but the case below then misses quarters at 10 s. So 8 is the
|
||||
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.
|
||||
|
||||
| meter | one hand-back | repeated hand-backs |
|
||||
|---|---|---|
|
||||
| 2 s | 22 s | 264 s |
|
||||
| 5 s (default) | 55 s | 660 s |
|
||||
| 60 s | 660 s | 7920 s |
|
||||
**What holds with 8.** Single hand-back, the loop at its rail: the first
|
||||
counted bound cycle is the 9th, so 8 cycles after the hand-back (measured,
|
||||
real Controller): 8 s at 1 s, 16 s at 2 s, 40 s at 5 s (default), 80 s at
|
||||
10 s, 120 s at 15 s, 240 s at 30 s, 480 s at 60 s. Repeated hand-backs
|
||||
(2 x 2500 W, cheap-window charge, no usable forecast, cap 5000 W, house
|
||||
5050 and 5100 W, 2 h, 4 loads each):
|
||||
|
||||
One hand-back: 11 cycles, never less than 22 s. Repeated: the worst case is
|
||||
one bound cycle per turn with the wire away 11 cycles between (22 at 1 s),
|
||||
so 11 turns. It grows with the meter; it stays under a quarter (900 s) up
|
||||
to a 5 s meter.
|
||||
Measured: N=2, 5 s meter, cheap-window charge, no usable forecast, house 50 W
|
||||
over the 5000 W rating: 14 warnings in 2 h (two in each quarter after the first), 0 with
|
||||
round 1, 28 without any rule. The cap held.
|
||||
| meter | warnings in 2 h | quarters warned (of 7 after the first) | without the fix |
|
||||
|---|---|---|---|
|
||||
| 2 s | 51 to 57 | 7 | 54 to 56 |
|
||||
| 5 s | 19 to 23 | 7 | 28 |
|
||||
| 10 s | 7 to 10 | 7 | 14 |
|
||||
| 15 s | 1 to 3 | 1 to 3 | 14 |
|
||||
| 30 s | 0 | 0 | 7 and 11 |
|
||||
|
||||
At 15 s it warns in some quarters, not all. At 30 s it does not warn in this
|
||||
case. For 60 s the rig's cycle is a different regime (the house is not over
|
||||
the cap there), so nothing is claimed. The cap held at 2 s and 5 s; at 10 s
|
||||
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.
|
||||
|
||||
**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).
|
||||
kick of `test_safety119.py` part 1 stays hidden (0 warnings in 2 h at
|
||||
1, 2, 5, 10 and 15 s meters).
|
||||
|
||||
**Re-pinned.** test_safety84 (all six) and test_safety87 (`n1_crossing`,
|
||||
`n1_above`, `n1_charge_low`): only OUTPUT_CLAMP log rows differ; commands and
|
||||
|
||||
@@ -299,10 +299,10 @@ REFUSED_PROGRESS_W = _ctl.PROBE_ANSWER_W / 2
|
||||
# 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.
|
||||
SHORT_CYCLES, SHORT_SPAN_S = _ctl.SETTLE_CYCLES, REFUSED_SPAN_S
|
||||
# SAFETY-119: a run of hand-backs (the wire away less than a settle between
|
||||
# them) may hide the loop's bound for at most this many cycles and
|
||||
# SHORT_SPAN_S, in all.
|
||||
RUN_BOUND_CYCLES = SHORT_CYCLES - 1
|
||||
# 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).
|
||||
ROLLING_BOUND_CYCLES, ROLLING_S = 8, 900.0
|
||||
|
||||
|
||||
def _refused_interval(sign: int) -> tuple:
|
||||
@@ -1276,13 +1276,12 @@ class Controller:
|
||||
self._loop_decision = None
|
||||
self._asked_hist: dict = {}
|
||||
self._short_run = 0
|
||||
# SAFETY-119: did another owner set the wire last cycle; the open run
|
||||
# (monotonic s and count of the loop's cycles in a row, bound cycles
|
||||
# hidden, monotonic s of the first); the wire's cycles away in a row
|
||||
# (first s, count).
|
||||
# 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
|
||||
# monotonic s of the bound cycles hidden in the last ROLLING_S.
|
||||
self._wire_lost = False
|
||||
self._handback = None
|
||||
self._away = None
|
||||
self._hidden = ()
|
||||
self.stopping = False
|
||||
# SAFETY-61. time.monotonic() at the end of the last run_control() pass.
|
||||
self.loop_pass_mono = None
|
||||
@@ -5825,7 +5824,6 @@ class Controller:
|
||||
self._short_run = 0
|
||||
self._asked_hist = {}
|
||||
self._wire_lost, self._handback = False, None
|
||||
self._away = None
|
||||
return
|
||||
unmet = self.allocation.unmet_w if self.allocation is not None else 0
|
||||
cut = (resolution.bound_by or "").split("(")[0]
|
||||
@@ -5864,41 +5862,35 @@ class Controller:
|
||||
|
||||
def _settling(self, owns, now) -> bool:
|
||||
"""SAFETY-119: does the loop's bound on this cycle count? Not within
|
||||
a run of hand-backs from another owner: its integrator is re-seeded
|
||||
from that owner's command and kicked by a meter that does not show it
|
||||
yet. A run opens at a hand-back and ends when the loop held the wire,
|
||||
or the wire was away, a whole settle in a row (SHORT_CYCLES cycles
|
||||
and SHORT_SPAN_S). Each hand-back restarts the settle, but the run
|
||||
hides the bound for RUN_BOUND_CYCLES cycles and SHORT_SPAN_S in all,
|
||||
so a bound that stays is counted after that, however often the wire
|
||||
changes hands. DECISIONS/SAFETY-119-bound-after-hand-back.md."""
|
||||
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
|
||||
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
|
||||
hand-backs the bound is hidden on at most ROLLING_BOUND_CYCLES cycles
|
||||
in any ROLLING_S, so a bound that stays is counted however often the
|
||||
wire changes hands. DECISIONS/SAFETY-119-bound-after-hand-back.md."""
|
||||
def aged(t0, span):
|
||||
return now is None or t0 is None or now - t0 >= span
|
||||
|
||||
if not owns:
|
||||
self._wire_lost = True
|
||||
t0, k = self._away or (now, 0)
|
||||
self._away = (t0, k + 1)
|
||||
if k + 1 >= SHORT_CYCLES and aged(t0, SHORT_SPAN_S):
|
||||
self._handback = None
|
||||
self._wire_lost, self._handback = True, None
|
||||
return False
|
||||
self._away = None
|
||||
if self._wire_lost:
|
||||
self._wire_lost = False
|
||||
self._handback = (now, 0) + (self._handback or (0,) * 4)[2:]
|
||||
self._wire_lost, self._handback = False, (now, 0)
|
||||
if self._handback is None:
|
||||
return False
|
||||
tc, n, hid, tb = self._handback
|
||||
tc, n = self._handback
|
||||
n += 1
|
||||
self._handback = (tc, n, hid, tb)
|
||||
self._handback = (tc, n)
|
||||
if aged(tc, SHORT_SPAN_S) and n >= SHORT_CYCLES:
|
||||
self._handback = None
|
||||
return False
|
||||
if self.loop_clamped[0] and not self.loop_clamped[2]:
|
||||
if hid >= RUN_BOUND_CYCLES and aged(tb, SHORT_SPAN_S):
|
||||
kept = tuple(t for t in self._hidden if not aged(t, ROLLING_S))
|
||||
if len(kept) >= ROLLING_BOUND_CYCLES:
|
||||
self._hidden = kept
|
||||
return False
|
||||
tb = tb if hid else now
|
||||
self._handback = (tc, n, hid + 1, tb)
|
||||
self._hidden = kept + (now,)
|
||||
return True
|
||||
|
||||
async def check_output_clamp(self) -> None:
|
||||
|
||||
+148
-100
@@ -9,10 +9,10 @@ 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
|
||||
`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
|
||||
SHORT_SPAN_S) has passed since the hand-back. Round 2: one such window per
|
||||
run of hand-backs, hiding at most RUN_BOUND_CYCLES bound cycles and
|
||||
SHORT_SPAN_S in all, so a bound that stays warns when the guard and the
|
||||
charge keep taking turns. DECISIONS/SAFETY-119-bound-after-hand-back.md.
|
||||
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
|
||||
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.
|
||||
|
||||
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
|
||||
@@ -23,10 +23,12 @@ charge keep taking turns. DECISIONS/SAFETY-119-bound-after-hand-back.md.
|
||||
hand-back (after the settle).
|
||||
3. With a usable forecast: the same commands and the same alarms as without
|
||||
the fix.
|
||||
4. Repeated hand-backs (N=2, 5 s meter, the guard and the charge take turns):
|
||||
it warns at least once per quarter; a start or a blind cycle is not a
|
||||
4. Repeated hand-backs (N=2, the guard and the charge take turns), 5, 10 and
|
||||
15 s meters: it warns (every quarter at 5 s and 10 s); the quarter-edge
|
||||
case stays silent at 10 s and 15 s too; a start or a blind cycle is not a
|
||||
hand-back; a slow meter delays by cycles, not seconds.
|
||||
5. The rule at its boundaries (`Controller._settling`).
|
||||
5. The rule at its boundaries (`Controller._settling`): the settle, and the
|
||||
rolling budget.
|
||||
6. test_safety84's and test_safety87's re-pinned digests: the same commands
|
||||
and claims; only OUTPUT_CLAMP log rows differ.
|
||||
|
||||
@@ -248,14 +250,14 @@ _first = notes(_hb[1])[:1]
|
||||
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"end {_hb[0][-1][1]}")
|
||||
check("⭐ the loop at its rail after a hand-back still warns, once the "
|
||||
"settle has passed (12 cycles, 22 s): delay, not loss",
|
||||
_first and 600.0 + app_main.SHORT_SPAN_S <= _first[0] <= 640.0
|
||||
MB = 8
|
||||
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)",
|
||||
_first and 600.0 + MB <= _first[0] <= 600.0 + MB + 4.0
|
||||
and _hb[0][-1][1] == -2500.0
|
||||
and _hb[1].clamp_wd.bound > 100)
|
||||
check("...without the fix it warned at once (non-vacuous)",
|
||||
notes(_hb_old[1])[:1] and notes(_hb_old[1])[0] < 600.0
|
||||
+ app_main.SHORT_SPAN_S)
|
||||
notes(_hb_old[1])[:1] and notes(_hb_old[1])[0] < 600.0 + MB)
|
||||
# The same charge with no forecast; at 1795 s, under the guard's step-in at
|
||||
# the quarter's end, the house goes to 4000 W, over the 2500 W rating.
|
||||
_up = run(house(1, step=(1795.0, 4000.0)), dur=1900.0)
|
||||
@@ -309,32 +311,79 @@ def round1(self, owns, now):
|
||||
return True
|
||||
|
||||
|
||||
# N=2 (2 x 2500 W), a 5 s meter, a cheap-window charge, no usable forecast,
|
||||
# the house 50 W 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.
|
||||
rig.TICK = 5.0
|
||||
_nv = run(house(1, step=(300.0, 5050.0)), n=2)
|
||||
_nv_old = no_fix(lambda: run(house(1, step=(300.0, 5050.0)), n=2))
|
||||
_nv_r1 = patched(Controller, "_settling", round1)(
|
||||
lambda: run(house(1, step=(300.0, 5050.0)), n=2))
|
||||
rig.TICK = 1.0
|
||||
_per_q = [sum(1 for t in notes(_nv[1]) if int(t // QUARTER) == q)
|
||||
# N=2 (2 x 2500 W), a cheap-window charge, no usable forecast, the house 50 W
|
||||
# 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
|
||||
# 15 s meters.
|
||||
def never(tick, hw=5050.0, fix=True):
|
||||
rig.TICK = tick
|
||||
try:
|
||||
f = lambda: run(house(1, step=(300.0, hw)), n=2)
|
||||
return f() if fix else no_fix(f)
|
||||
finally:
|
||||
rig.TICK = 1.0
|
||||
|
||||
|
||||
def per_quarter(c):
|
||||
return [sum(1 for t in notes(c) if int(t // QUARTER) == q)
|
||||
for q in range(8)]
|
||||
print(f" N=2, 5 s meter, house 5050 W: notifications {len(notes(_nv[1]))} "
|
||||
f"(per quarter {_per_q}); round 1's rule {len(notes(_nv_r1[1]))}; "
|
||||
f"without the fix {len(notes(_nv_old[1]))}; highest quarter "
|
||||
f"{max(quarters(_nv[1], 5.0)):.0f} W")
|
||||
check("⭐ it warns at least once in every quarter after the first warning "
|
||||
"(the guard and the charge keep taking turns)",
|
||||
len(notes(_nv[1])) >= 7 and all(_per_q[q] >= 1 for q in range(1, 8)))
|
||||
|
||||
|
||||
_nv = {t: never(t) for t in (5.0, 10.0, 15.0)}
|
||||
_nv_old = {t: never(t, fix=False) for t in (5.0, 10.0, 15.0)}
|
||||
_nv_r1 = patched(Controller, "_settling", round1)(lambda: never(5.0))
|
||||
for _t in _nv:
|
||||
print(f" N=2, {_t:.0f} s meter, house 5050 W: notifications "
|
||||
f"{len(notes(_nv[_t][1]))} (per quarter "
|
||||
f"{per_quarter(_nv[_t][1])}), without the fix "
|
||||
f"{len(notes(_nv_old[_t][1]))}, highest quarter "
|
||||
f"{max(quarters(_nv[_t][1], _t)):.0f} W")
|
||||
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",
|
||||
all(per_quarter(_nv[5.0][1])[q] >= 1 for q in range(1, 8)))
|
||||
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 (round 2 gave none), in at least one quarter "
|
||||
"of the seven after the first",
|
||||
sum(1 for n in per_quarter(_nv[15.0][1])[1:] if n) >= 1)
|
||||
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",
|
||||
notes(_nv_r1[1]) == []
|
||||
and _nv[1].clamp_wd.bound < _nv_old[1].clamp_wd.bound)
|
||||
check("...the cap held and every command is the same as without the fix",
|
||||
len(quarters(_nv[1], 5.0)) == 8
|
||||
and max(quarters(_nv[1], 5.0)) < 5000.0
|
||||
and commands(_nv[0]) == commands(_nv_old[0]))
|
||||
notes(_nv_r1[1]) == [] and all(
|
||||
_nv[t][1].clamp_wd.bound < _nv_old[t][1].clamp_wd.bound
|
||||
for t in _nv))
|
||||
check("...every command is the same as without the fix, at 5, 10 and 15 s",
|
||||
all(commands(_nv[t][0]) == commands(_nv_old[t][0]) for t in _nv))
|
||||
check("...and at 5 s the cap held",
|
||||
len(quarters(_nv[5.0][1], 5.0)) == 8
|
||||
and max(quarters(_nv[5.0][1], 5.0)) < 5000.0)
|
||||
|
||||
# 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
|
||||
rig.TICK = _t
|
||||
for _seed, _dl in _seeds:
|
||||
_qe[_t, _seed] = (run(house(_seed), delay=_dl),
|
||||
no_fix(lambda: run(house(_seed), delay=_dl)))
|
||||
rig.TICK = 1.0
|
||||
print(f" {_t:.0f} s meter, quarter edge: notifications "
|
||||
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",
|
||||
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)",
|
||||
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()))
|
||||
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)
|
||||
|
||||
# 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).
|
||||
@@ -348,8 +397,8 @@ 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
|
||||
and notes(_bl[1]) == notes(_bl_old[1]))
|
||||
|
||||
# The settle is SHORT_CYCLES cycles, not only SHORT_SPAN_S (R4): at a 5 s
|
||||
# meter the loop at its rail after a hand-back warns 11 cycles (55 s) later.
|
||||
# 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.
|
||||
rig.TICK = 5.0
|
||||
_sl = run(house(1, step=(600.0, -6000.0)), dur=900.0,
|
||||
extra={"peak_cap_w": 20000})
|
||||
@@ -357,9 +406,9 @@ rig.TICK = 1.0
|
||||
_sl1 = [t for t in notes(_sl[1]) if t >= 600.0][:1]
|
||||
print(f" 5 s meter, hand-back to a loop at its rail at 600 s: first "
|
||||
f"notification {_sl1}")
|
||||
check("⭐ at a 5 s meter the settle is 11 cycles: the first warning comes "
|
||||
"55 s after the hand-back, not 22 s",
|
||||
_sl1 and 600.0 + 11 * 5.0 <= _sl1[0] <= 600.0 + 11 * 5.0 + 10.0)
|
||||
check(f"⭐ at a 5 s meter the budget is cycles, not seconds: the first "
|
||||
f"warning comes {MB} cycles ({MB * 5} s) after the hand-back",
|
||||
_sl1 and 600.0 + MB * 5.0 <= _sl1[0] <= 600.0 + MB * 5.0 + 10.0)
|
||||
|
||||
# ============================================================================ #
|
||||
print("\n5. the rule at its boundaries")
|
||||
@@ -369,7 +418,7 @@ class _C:
|
||||
_settling = Controller._settling
|
||||
|
||||
def __init__(self):
|
||||
self._wire_lost, self._handback, self._away = False, None, None
|
||||
self._wire_lost, self._handback, self._hidden = False, None, ()
|
||||
self.loop_clamped = (False, "", False)
|
||||
|
||||
|
||||
@@ -383,6 +432,7 @@ def seq(c, steps):
|
||||
|
||||
|
||||
N, SPAN = app_main.SHORT_CYCLES, app_main.SHORT_SPAN_S
|
||||
M, ROLL = 8, 900.0 # the chosen budget, written out on purpose
|
||||
_c = _C()
|
||||
check("from the start, owned every cycle: never settling (no hand-back)",
|
||||
seq(_c, [(True, float(i)) for i in range(30)]) == [False] * 30)
|
||||
@@ -393,35 +443,34 @@ check(f"after another owner: settling for {N - 1} owned cycles, then not "
|
||||
_out == [False] + [True] * (N - 1) + [False] * 3)
|
||||
_c = _C()
|
||||
_out = seq(_c, [(False, 0.0)] + [(True, 0.5 * i) for i in range(1, 60)])
|
||||
_end = _out.index(False, 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",
|
||||
_end == 1 + int(SPAN / 0.5) and all(_out[1:_end]))
|
||||
_c = _C()
|
||||
_out = seq(_c, [(False, 0.0), (True, 2.0), (True, 4.0), (False, 6.0),
|
||||
(True, 8.0), (True, 10.0)])
|
||||
check("losing the wire again restarts the settle (but not the run)",
|
||||
check("losing the wire again restarts the settle",
|
||||
_out == [False, True, True, False, True, True]
|
||||
and _c._handback == (8.0, 2, 0, 0))
|
||||
and _c._handback == (8.0, 2))
|
||||
_c = _C()
|
||||
check("no monotonic clock: the cycle count alone decides",
|
||||
seq(_c, [(False, None)] + [(True, None)] * (N + 1))
|
||||
== [False] + [True] * (N - 1) + [False] * 2)
|
||||
|
||||
# One run of hand-backs hides RUN_BOUND_CYCLES bound cycles and SHORT_SPAN_S,
|
||||
# however often the wire changes hands. T = the loop's bound that cycle.
|
||||
M = app_main.RUN_BOUND_CYCLES
|
||||
# The rolling budget: the loop's bound is hidden on at most M cycles in any
|
||||
# ROLL seconds, across all hand-backs. T = the loop's bound that cycle.
|
||||
assert M < N - 1, "the budget is meant to run out inside one settle"
|
||||
|
||||
|
||||
def turns(c, tick, owned, lost, periods, first=0.0):
|
||||
"""A hand-back, then `periods` x (`owned` bound cycles, `lost` cycles
|
||||
away). Returns (time, hidden) of each owned bound cycle."""
|
||||
def turns(c, tick, lost, periods, first=0.0, bound=True):
|
||||
"""A hand-back, then `periods` x (1 owned cycle with the loop's bound,
|
||||
`lost` cycles away). Returns [(time, hidden)] of the owned cycles."""
|
||||
t, out = first, []
|
||||
for _ in range(lost):
|
||||
c._settling(False, t)
|
||||
t += tick
|
||||
for _ in range(periods):
|
||||
for _ in range(owned):
|
||||
c.loop_clamped = (True, "", False)
|
||||
c.loop_clamped = (bound, "", False)
|
||||
out.append((t, c._settling(True, t)))
|
||||
t += tick
|
||||
for _ in range(lost):
|
||||
@@ -431,69 +480,68 @@ def turns(c, tick, owned, lost, periods, first=0.0):
|
||||
|
||||
|
||||
_c = _C()
|
||||
_o, _ = turns(_c, 5.0, 1, 3, 20)
|
||||
_hid = [h for _t, h in _o]
|
||||
check(f"at 5 s, one bound cycle per turn: {M} are hidden, the {M + 1}th and "
|
||||
"every later one counts",
|
||||
_hid == [True] * M + [False] * (20 - M))
|
||||
_o = seq(_c, [(False, 0.0)] + [(True, 5.0 * i, True) for i in range(1, N)])
|
||||
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()
|
||||
_o, _ = turns(_c, 1.0, 2, 1, 12)
|
||||
_first, _o0 = first(t for t, h in _o if not h), nth(_o, 0)[0]
|
||||
check(f"at 1 s the span holds too: not counted before {SPAN:.0f} s after "
|
||||
f"the first hidden one (counted at {_first:.0f} s)",
|
||||
present(_first, _o0) and _o0 + SPAN <= _first <= _o0 + SPAN + 3.0
|
||||
and sum(h for _t, h in _o) > M)
|
||||
_o, _ = turns(_c, 5.0, 3, 60)
|
||||
_hid = [t for t, h in _o if h]
|
||||
check(f"across hand-backs (one bound cycle per turn, 5 s): the first {M} "
|
||||
"are hidden, then every one counts",
|
||||
[h for _t, h in _o[:M + 5]] == [True] * M + [False] * 5)
|
||||
check(f"...never more than {M} hidden in any {ROLL:.0f} s, and they come "
|
||||
f"back as the oldest ages out (exactly {M} in the first {ROLL:.0f} s "
|
||||
"after the first)",
|
||||
all(sum(1 for x in _hid if t <= x < t + ROLL) <= M for t in _hid)
|
||||
and sum(1 for x in _hid if x < _hid[0] + ROLL) == M
|
||||
and any(x >= _hid[0] + ROLL for x in _hid))
|
||||
_c = _C()
|
||||
_t = 0.0
|
||||
for _ in range(20): # an over-rating: always counts
|
||||
for _ in range(3):
|
||||
_c._settling(False, _t)
|
||||
_t += 5.0
|
||||
_c.loop_clamped = (True, "", True)
|
||||
_c._settling(True, _t)
|
||||
_t += 5.0
|
||||
for _ in range(3):
|
||||
_c._settling(False, _t)
|
||||
_t += 5.0
|
||||
_c._hidden = (0.0,) * M
|
||||
_c._wire_lost, _c._handback = False, (ROLL - 30.0, 0)
|
||||
_c.loop_clamped = (True, "", False)
|
||||
check("an over-rating does not use up what the run may hide",
|
||||
_c._settling(True, _t) is True)
|
||||
_a = _c._settling(True, ROLL - 1.0)
|
||||
_c.loop_clamped = (True, "", False)
|
||||
_b = _c._settling(True, ROLL)
|
||||
check(f"the budget refills {ROLL:.0f} s after the oldest hidden cycle: "
|
||||
f"counted at {ROLL - 1:.0f} s, hidden again at {ROLL:.0f} s",
|
||||
_a is False and _b is True)
|
||||
_c = _C()
|
||||
_o1, _t = turns(_c, 2.0, 1, 3, 20)
|
||||
_o1, _t = turns(_c, 2.0, 3, 20)
|
||||
for _ in range(N): # the wire away a whole settle
|
||||
_c._settling(False, _t)
|
||||
_t += 2.0
|
||||
_o2 = []
|
||||
for _ in range(20):
|
||||
_c.loop_clamped = (True, "", False)
|
||||
_o2.append(_c._settling(True, _t))
|
||||
_c._settling(False, _t + 2.0)
|
||||
_t += 4.0
|
||||
check("a settle with the wire away ends the run: the next hand-back hides "
|
||||
f"{M} again",
|
||||
[h for _t2, h in _o1] == [True] * M + [False] * (20 - M)
|
||||
and _o2 == [True] * M + [False] * (20 - M))
|
||||
_o2, _ = turns(_c, 2.0, 3, 5, first=_t)
|
||||
check("the budget is not per hand-back or per run: after a whole settle "
|
||||
"with the wire away, a new hand-back within the same "
|
||||
f"{ROLL:.0f} s hides nothing more",
|
||||
[h for _t2, h in _o1[:M]] == [True] * M
|
||||
and not any(h for _t2, h in _o2))
|
||||
_c = _C()
|
||||
_o1, _t = turns(_c, 2.0, 1, 3, 20)
|
||||
for _ in range(N - 1 - 3): # 3 already: N - 1 away
|
||||
_c._settling(False, _t)
|
||||
_t += 2.0
|
||||
_c.loop_clamped = (True, "", False)
|
||||
check("...but not a shorter time away: the same run, nothing more hidden",
|
||||
_c._settling(True, _t) is False)
|
||||
_c = _C()
|
||||
_o1, _t = turns(_c, 2.0, 1, 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(False, _t + 40.0)
|
||||
_c.loop_clamped = (True, "", False)
|
||||
check("the loop holding the wire a whole settle ends the run too: the next "
|
||||
"hand-back hides again",
|
||||
_c._settling(True, _t + 42.0) is True)
|
||||
check("the loop holding the wire a whole settle ends the window: the next "
|
||||
"hand-back hides nothing more while the budget is spent",
|
||||
_c._settling(True, _t + 42.0) is False)
|
||||
_c = _C()
|
||||
_c._wire_lost = True
|
||||
_t = 0.0
|
||||
for _ in range(M + 4): # an over-rating: always counts
|
||||
_c.loop_clamped = (True, "", True)
|
||||
_c._settling(True, _t)
|
||||
_c._wire_lost = True
|
||||
_t += 5.0
|
||||
_c._settling(False, _t)
|
||||
_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)
|
||||
|
||||
# An over-rating (a command over `max_w`) is a bug, never settled away.
|
||||
_ctl = _hb[1]
|
||||
_ctl.loop_clamped = (True, "x", True)
|
||||
_ctl._wire_lost, _ctl._handback = True, None
|
||||
_ctl._wire_lost, _ctl._handback, _ctl._hidden = True, None, ()
|
||||
|
||||
|
||||
class _Res:
|
||||
|
||||
Reference in New Issue
Block a user