SAFETY-119: no "not keeping up" when the loop takes the wire back #203
@@ -17,21 +17,42 @@ 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
|
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.
|
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.
|
||||||
|
|
||||||
|
**Delay.** The first counted bound cycle, from the hand-back (worst case):
|
||||||
|
|
||||||
|
| 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 |
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
**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.
|
||||||
A loop still at its rail after a hand-back warns 11 cycles later (at least
|
The start and a restart after a stop are not hand-backs. The quarter-edge
|
||||||
22 s): 22 s at a 2 s meter, 55 s at 5 s, 11 min at a 60 s cycle. The start
|
kick of `test_safety119.py` part 1 stays hidden (0 warnings in 2 h).
|
||||||
and a restart after a stop are not hand-backs.
|
|
||||||
|
|
||||||
**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
|
||||||
claims are the same (`test_safety119.py` part 5).
|
claims are the same (`test_safety119.py` part 6).
|
||||||
|
|
||||||
**Open.** Each hand-back restarts the window, so a loop that loses the wire
|
**Not hidden.** A battery that gives less than another owner told it still
|
||||||
again within 11 cycles is never counted. Seen in review: N=2 (2 x 2500 W), a
|
alarms through SAFETY-85 rule 2. "Always counts" for an over-rating holds on
|
||||||
5 s meter, a cheap-window charge with no usable forecast, a house 50-100 W
|
cycles the loop decided; on others SAFETY-85 rule 1 already dropped it.
|
||||||
over the 5000 W rating. The guard and the charge took the wire back at least
|
|
||||||
every 11 cycles. 0 notifications in 2 h, 28 without this rule. The cap held.
|
|
||||||
From 200 W over the rating it warns at least once a quarter.
|
|
||||||
|
|
||||||
**Not changed.** The kick itself is control behaviour: for a few seconds at
|
**Not changed.** The kick itself is control behaviour: for a few seconds at
|
||||||
a quarter's edge the loop asks up to its rail. In `test_safety119.py` part 1
|
a quarter's edge the loop asks up to its rail. In `test_safety119.py` part 1
|
||||||
|
|||||||
@@ -299,6 +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
|
# 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: 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
|
||||||
|
|
||||||
|
|
||||||
def _refused_interval(sign: int) -> tuple:
|
def _refused_interval(sign: int) -> tuple:
|
||||||
@@ -1272,10 +1276,13 @@ class Controller:
|
|||||||
self._loop_decision = None
|
self._loop_decision = None
|
||||||
self._asked_hist: dict = {}
|
self._asked_hist: dict = {}
|
||||||
self._short_run = 0
|
self._short_run = 0
|
||||||
# SAFETY-119: did another owner set the wire last cycle, and since
|
# SAFETY-119: did another owner set the wire last cycle; the open run
|
||||||
# when (monotonic s, owned cycles) has the loop had it back.
|
# (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).
|
||||||
self._wire_lost = False
|
self._wire_lost = False
|
||||||
self._handback = None
|
self._handback = None
|
||||||
|
self._away = 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
|
||||||
@@ -5818,6 +5825,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._away = None
|
||||||
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]
|
||||||
@@ -5855,25 +5863,42 @@ class Controller:
|
|||||||
self.loop_clamp_facts = ()
|
self.loop_clamp_facts = ()
|
||||||
|
|
||||||
def _settling(self, owns, now) -> bool:
|
def _settling(self, owns, now) -> bool:
|
||||||
"""SAFETY-119: is the loop within the plant's settle (SHORT_CYCLES
|
"""SAFETY-119: does the loop's bound on this cycle count? Not within
|
||||||
cycles and SHORT_SPAN_S) of taking the wire back from another owner?
|
a run of hand-backs from another owner: its integrator is re-seeded
|
||||||
Its integrator is re-seeded from that owner's command and kicked by a
|
from that owner's command and kicked by a meter that does not show it
|
||||||
meter that does not show it yet, so its bound there is not counted.
|
yet. A run opens at a hand-back and ends when the loop held the wire,
|
||||||
DECISIONS/SAFETY-119-bound-after-hand-back.md."""
|
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."""
|
||||||
|
def aged(t0, span):
|
||||||
|
return now is None or t0 is None or now - t0 >= span
|
||||||
|
|
||||||
if not owns:
|
if not owns:
|
||||||
self._wire_lost, self._handback = True, None
|
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
|
||||||
return False
|
return False
|
||||||
|
self._away = None
|
||||||
if self._wire_lost:
|
if self._wire_lost:
|
||||||
self._wire_lost, self._handback = False, (now, 0)
|
self._wire_lost = False
|
||||||
|
self._handback = (now, 0) + (self._handback or (0,) * 4)[2:]
|
||||||
if self._handback is None:
|
if self._handback is None:
|
||||||
return False
|
return False
|
||||||
t0, n = self._handback
|
tc, n, hid, tb = self._handback
|
||||||
n += 1
|
n += 1
|
||||||
self._handback = (t0, n)
|
self._handback = (tc, n, hid, tb)
|
||||||
if n >= SHORT_CYCLES and (now is None or t0 is None
|
if aged(tc, SHORT_SPAN_S) and n >= SHORT_CYCLES:
|
||||||
or now - t0 >= SHORT_SPAN_S):
|
|
||||||
self._handback = None
|
self._handback = None
|
||||||
return False
|
return False
|
||||||
|
if self.loop_clamped[0] and not self.loop_clamped[2]:
|
||||||
|
if hid >= RUN_BOUND_CYCLES and aged(tb, SHORT_SPAN_S):
|
||||||
|
return False
|
||||||
|
tb = tb if hid else now
|
||||||
|
self._handback = (tc, n, hid + 1, tb)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def check_output_clamp(self) -> None:
|
async def check_output_clamp(self) -> None:
|
||||||
|
|||||||
@@ -9,8 +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
|
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.
|
SHORT_SPAN_S) has passed since the hand-back. Round 2: one such window per
|
||||||
DECISIONS/SAFETY-119-bound-after-hand-back.md.
|
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.
|
||||||
|
|
||||||
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
|
||||||
@@ -18,11 +20,14 @@ DECISIONS/SAFETY-119-bound-after-hand-back.md.
|
|||||||
the same as without the fix; without the fix it warns.
|
the same as without the fix; without the fix it warns.
|
||||||
2. A genuine integrator bound still warns: the loop in charge with the house
|
2. A genuine integrator bound still warns: the loop in charge with the house
|
||||||
over the rating (at once, as before), and the loop at its rail after a
|
over the rating (at once, as before), and the loop at its rail after a
|
||||||
hand-back (after the settle, not lost).
|
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. The rule at its boundaries (`Controller._settling`).
|
4. Repeated hand-backs (N=2, 5 s meter, the guard and the charge take turns):
|
||||||
5. test_safety84's and test_safety87's re-pinned digests: the same commands
|
it warns at least once per quarter; 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`).
|
||||||
|
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.
|
||||||
|
|
||||||
Mutations (MUTATION.md, purged copies) are listed in the pull request.
|
Mutations (MUTATION.md, purged copies) are listed in the pull request.
|
||||||
@@ -126,11 +131,12 @@ def house(seed, base=800.0, step=None):
|
|||||||
return load
|
return load
|
||||||
|
|
||||||
|
|
||||||
def run(load, *, dur=7200.0, delay=1, extra=None, cheap=True, cut=None,
|
def run(load, *, n=1, dur=7200.0, delay=1, extra=None, cheap=True, cut=None,
|
||||||
charging=True):
|
charging=True, blind=0.0):
|
||||||
"""One N=1 run; `charging=False`: no maintenance, the loop in charge."""
|
"""One run of `n` inverters of 2500 W; `charging=False`: no maintenance,
|
||||||
|
the loop in charge; `blind`: no meter reading until that second."""
|
||||||
extra = dict(extra or {})
|
extra = dict(extra or {})
|
||||||
s84.OPT = dict(OPT0, peak_cap_w=int(CAP), peak_margin_w=50,
|
s84.OPT = dict(OPT0, peak_cap_w=int(CAP * n), peak_margin_w=50,
|
||||||
maintenance_charge_w=2000)
|
maintenance_charge_w=2000)
|
||||||
if not charging:
|
if not charging:
|
||||||
extra.update(maintenance_enabled=False, peak_cap_w=20000)
|
extra.update(maintenance_enabled=False, peak_cap_w=20000)
|
||||||
@@ -138,10 +144,16 @@ def run(load, *, dur=7200.0, delay=1, extra=None, cheap=True, cut=None,
|
|||||||
s84.OPT[k] = extra.pop(k)
|
s84.OPT[k] = extra.pop(k)
|
||||||
DELAY[0] = delay
|
DELAY[0] = delay
|
||||||
try:
|
try:
|
||||||
return s84.run(1, load=load, cheap=cheap,
|
feed = s84.feed(cheap)
|
||||||
store=(s79.store_charging(1) if charging
|
|
||||||
|
def before(c, t):
|
||||||
|
if t >= blind: # before that: no meter reading
|
||||||
|
return feed(c, t)
|
||||||
|
return s84.run(n, load=load, cheap=cheap,
|
||||||
|
store=(s79.store_charging(n) if charging
|
||||||
else rig._Store()),
|
else rig._Store()),
|
||||||
soc=lambda u, t: 60.0, dur=dur, cut=cut,
|
soc=lambda u, t: 60.0, dur=dur, cut=cut,
|
||||||
|
before=before if blind else None,
|
||||||
extra=dict({"max_w": 2500, "slew_w": 0}, **extra))
|
extra=dict({"max_w": 2500, "slew_w": 0}, **extra))
|
||||||
finally:
|
finally:
|
||||||
s84.OPT = OPT0
|
s84.OPT = OPT0
|
||||||
@@ -166,12 +178,13 @@ def notes(c):
|
|||||||
if i == messages.ID_LOOP_BOUND]
|
if i == messages.ID_LOOP_BOUND]
|
||||||
|
|
||||||
|
|
||||||
def quarters(c):
|
def quarters(c, tick=1.0):
|
||||||
"""Each whole quarter's true average import (W)."""
|
"""Each whole quarter's true average import (W)."""
|
||||||
q = {}
|
q = {}
|
||||||
for t, g in c.hass.grid_true:
|
for t, g in c.hass.grid_true:
|
||||||
q.setdefault(int(t // QUARTER), []).append(g)
|
q.setdefault(int(t // QUARTER), []).append(g)
|
||||||
return [sum(v) / len(v) for v in q.values() if len(v) == QUARTER]
|
return [sum(v) / len(v) for v in q.values()
|
||||||
|
if len(v) == QUARTER / tick]
|
||||||
|
|
||||||
|
|
||||||
def commands(rows):
|
def commands(rows):
|
||||||
@@ -272,18 +285,100 @@ check("...the forecast was used (the charge is shaped under -2000 W)",
|
|||||||
-2000.0 < min(r[1] for r in _fc[0][:300]) < 0.0)
|
-2000.0 < min(r[1] for r in _fc[0][:300]) < 0.0)
|
||||||
|
|
||||||
# ============================================================================ #
|
# ============================================================================ #
|
||||||
print("\n4. the rule at its boundaries")
|
print("\n4. repeated hand-backs: the guard and the charge take turns")
|
||||||
|
|
||||||
|
|
||||||
|
def round1(self, owns, now):
|
||||||
|
"""Round 1's rule, kept here to show what it hid: every hand-back
|
||||||
|
restarts the window, so a loss within the settle never lets it end."""
|
||||||
|
d = self.__dict__
|
||||||
|
if not owns:
|
||||||
|
d["_r1"], d["_r1_n"] = True, None
|
||||||
|
return False
|
||||||
|
if d.get("_r1"):
|
||||||
|
d["_r1"], d["_r1_n"] = False, (now, 0)
|
||||||
|
if d.get("_r1_n") is None:
|
||||||
|
return False
|
||||||
|
t0, n = d["_r1_n"]
|
||||||
|
d["_r1_n"] = (t0, n + 1)
|
||||||
|
if n + 1 >= app_main.SHORT_CYCLES and (
|
||||||
|
now is None or t0 is None or now - t0 >= app_main.SHORT_SPAN_S):
|
||||||
|
d["_r1_n"] = None
|
||||||
|
return False
|
||||||
|
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)
|
||||||
|
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)))
|
||||||
|
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]))
|
||||||
|
|
||||||
|
# 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).
|
||||||
|
_bl = run(house(1, base=4000.0), dur=120.0, charging=False, blind=20.0)
|
||||||
|
_bl_old = no_fix(lambda: run(house(1, base=4000.0), dur=120.0,
|
||||||
|
charging=False, blind=20.0))
|
||||||
|
print(f" the loop not running until 20 s, then at its rail: "
|
||||||
|
f"{notes(_bl[1])}, without the fix {notes(_bl_old[1])}")
|
||||||
|
check("⭐ a start after a blind spell is not a hand-back: it warns at the "
|
||||||
|
"same cycle as without the fix",
|
||||||
|
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.
|
||||||
|
rig.TICK = 5.0
|
||||||
|
_sl = run(house(1, step=(600.0, -6000.0)), dur=900.0,
|
||||||
|
extra={"peak_cap_w": 20000})
|
||||||
|
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)
|
||||||
|
|
||||||
|
# ============================================================================ #
|
||||||
|
print("\n5. the rule at its boundaries")
|
||||||
|
|
||||||
|
|
||||||
class _C:
|
class _C:
|
||||||
_settling = Controller._settling
|
_settling = Controller._settling
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._wire_lost, self._handback = False, None
|
self._wire_lost, self._handback, self._away = False, None, None
|
||||||
|
self.loop_clamped = (False, "", False)
|
||||||
|
|
||||||
|
|
||||||
def seq(c, steps):
|
def seq(c, steps):
|
||||||
return [c._settling(o, t) for o, t in steps]
|
"""(owned, time[, the loop's bound that cycle]) -> does it count not."""
|
||||||
|
out = []
|
||||||
|
for st in steps:
|
||||||
|
c.loop_clamped = (len(st) > 2 and st[2], "", False)
|
||||||
|
out.append(c._settling(st[0], st[1]))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
N, SPAN = app_main.SHORT_CYCLES, app_main.SHORT_SPAN_S
|
N, SPAN = app_main.SHORT_CYCLES, app_main.SHORT_SPAN_S
|
||||||
@@ -303,14 +398,97 @@ check(f"at a 0.5 s meter the span decides: settling until {SPAN:.0f} s",
|
|||||||
_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)])
|
||||||
check("losing the wire again restarts the window",
|
check("losing the wire again restarts the settle (but not the run)",
|
||||||
_out == [False, True, True, False, True, True]
|
_out == [False, True, True, False, True, True]
|
||||||
and _c._handback == (8.0, 2))
|
and _c._handback == (8.0, 2, 0, 0))
|
||||||
_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)] * (N + 1))
|
||||||
== [False] + [True] * (N - 1) + [False] * 2)
|
== [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
|
||||||
|
|
||||||
|
|
||||||
|
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."""
|
||||||
|
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)
|
||||||
|
out.append((t, c._settling(True, t)))
|
||||||
|
t += tick
|
||||||
|
for _ in range(lost):
|
||||||
|
c._settling(False, t)
|
||||||
|
t += tick
|
||||||
|
return out, t
|
||||||
|
|
||||||
|
|
||||||
|
_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))
|
||||||
|
_c = _C()
|
||||||
|
_o, _ = turns(_c, 1.0, 2, 1, 12)
|
||||||
|
_first = next(t for t, h in _o if not h)
|
||||||
|
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)",
|
||||||
|
_o[0][0] + SPAN <= _first <= _o[0][0] + SPAN + 3.0
|
||||||
|
and sum(h for _t, h in _o) > M)
|
||||||
|
_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.loop_clamped = (True, "", False)
|
||||||
|
check("an over-rating does not use up what the run may hide",
|
||||||
|
_c._settling(True, _t) is True)
|
||||||
|
_c = _C()
|
||||||
|
_o1, _t = turns(_c, 2.0, 1, 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))
|
||||||
|
_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)
|
||||||
|
_ = [_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)
|
||||||
|
|
||||||
# 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]
|
||||||
_ctl.loop_clamped = (True, "x", True)
|
_ctl.loop_clamped = (True, "x", True)
|
||||||
@@ -342,7 +520,7 @@ check("a stop clears it: the next start is not a hand-back",
|
|||||||
(_ctl._wire_lost, _ctl._handback) == (False, None))
|
(_ctl._wire_lost, _ctl._handback) == (False, None))
|
||||||
|
|
||||||
# ============================================================================ #
|
# ============================================================================ #
|
||||||
print("\n5. the pinned digests of test_safety84 and test_safety87: only the "
|
print("\n6. the pinned digests of test_safety84 and test_safety87: only the "
|
||||||
"log changes")
|
"log changes")
|
||||||
# Those harnesses run at the 5 s tick with a battery that follows at once.
|
# Those harnesses run at the 5 s tick with a battery that follows at once.
|
||||||
rig._Hass.power, rig.TICK, s79._feed = _real_power, 5.0, _real_feed
|
rig._Hass.power, rig.TICK, s79._feed = _real_power, 5.0, _real_feed
|
||||||
|
|||||||
@@ -540,8 +540,9 @@ _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, crossing and above the reserve, in a "
|
"log) equals b8ddfd7's (the log as re-pinned by SAFETY-119), under, "
|
||||||
"charge and a HOLD, and under a banked peak",
|
"crossing and above the reserve, in a charge and a HOLD, and under a "
|
||||||
|
"banked peak",
|
||||||
_now == PINNED)
|
_now == PINNED)
|
||||||
|
|
||||||
# ============================================================================ #
|
# ============================================================================ #
|
||||||
|
|||||||
Reference in New Issue
Block a user