SAFETY-119: no "not keeping up" when the loop takes the wire back #203
@@ -5,23 +5,33 @@
|
||||
**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
|
||||
owner's command, and `compute()` adds `gain * error` from a meter that does
|
||||
not show that command yet. On the TEST-23 rig (SAFETY-118 I-2) the guard's
|
||||
last-seconds step went from -2000 W to +429 W. The next cycle's ask crossed
|
||||
the +2500 W rail and beat the guard's floor, so SAFETY-85 rule 1 counted it
|
||||
as the loop's own bound and raised `goodwe_integrator_bound`. The battery gave
|
||||
what it was told; the cap held.
|
||||
not show that command yet. On the TEST-23 rig (SAFETY-118 I-2) the guard
|
||||
stepped from -2000 W to +429 W with 5 s left. A cycle later the loop's ask
|
||||
crossed the +2500 W rail and beat the guard's floor, so SAFETY-85 rule 1
|
||||
counted it as the loop's own bound and raised `goodwe_integrator_bound`. The
|
||||
battery gave what it was told, and the cap held.
|
||||
|
||||
**Decision.** On the cycles after a hand-back, the loop's integrator bound is
|
||||
not counted until the plant's settle has passed (`SHORT_CYCLES` cycles and
|
||||
`SHORT_SPAN_S`, the same window SAFETY-85 grades a battery on). After that it
|
||||
**Decision.** After a hand-back, the loop's integrator bound is not counted
|
||||
until the plant's settle has passed (`SHORT_CYCLES` cycles and
|
||||
`SHORT_SPAN_S`, the window SAFETY-85 grades a battery on). After that it
|
||||
counts as before. An over-rating (`clamped`, a bug) always counts. No command
|
||||
changes, and no text was added or changed.
|
||||
|
||||
**Kept.** The loop in charge at its rail warns on the same cycle as before. A
|
||||
loop still at its rail after the window warns then (about 22 s later): delay,
|
||||
not loss. The start and a restart after a stop are not hand-backs.
|
||||
**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 once the window has passed:
|
||||
22 s at a 2 s meter, 55 s at 5 s. Delay, not loss. The start and a restart
|
||||
after a stop are not hand-backs.
|
||||
|
||||
**Re-pinned.** test_safety84 (all six) and test_safety87 (`n1_crossing`,
|
||||
`n1_above`, `n1_charge_low`): only OUTPUT_CLAMP log rows differ; commands and
|
||||
claims are the same (`test_safety119.py` part 5).
|
||||
|
||||
**Open.** If another owner and the loop swapped the wire every few cycles,
|
||||
each swap restarts the window and the loop's bound would not be counted.
|
||||
A loop at its rail beats a floor or ceiling on its own side, so it keeps the
|
||||
wire; a swap needs a claim that keeps outranking it.
|
||||
|
||||
**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
|
||||
that is more discharge than the guard asked at a quarter's end, and about
|
||||
1700 Ws more charge (2 W on that quarter's average) at the next one's start.
|
||||
that is more discharge than the guard asked at a quarter's end, and a few
|
||||
seconds of up to 500 W more charge at the next quarter's start.
|
||||
|
||||
@@ -22,6 +22,8 @@ DECISIONS/SAFETY-119-bound-after-hand-back.md.
|
||||
3. With a usable forecast: the same commands and the same alarms as without
|
||||
the fix.
|
||||
4. The rule at its boundaries (`Controller._settling`).
|
||||
5. test_safety84's and test_safety87's re-pinned digests: the same commands
|
||||
and claims; only OUTPUT_CLAMP log rows differ.
|
||||
|
||||
Mutations (MUTATION.md, purged copies) are listed in the pull request.
|
||||
"""
|
||||
@@ -81,7 +83,8 @@ def _power(self, u, lag=1.0):
|
||||
return float(round(a))
|
||||
|
||||
|
||||
_real_number = rig._Hass.number
|
||||
_real_number, _real_power = rig._Hass.number, rig._Hass.power
|
||||
_real_feed = s79._feed
|
||||
|
||||
|
||||
async def _number(self, e, invert=False):
|
||||
@@ -338,6 +341,43 @@ _ctl._loop_alarm_input(_Res())
|
||||
check("a stop clears it: the next start is not a hand-back",
|
||||
(_ctl._wire_lost, _ctl._handback) == (False, None))
|
||||
|
||||
# ============================================================================ #
|
||||
print("\n5. the pinned digests of test_safety84 and test_safety87: only the "
|
||||
"log changes")
|
||||
# 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.number = _real_number
|
||||
import safety87_identity as s87 # noqa: E402
|
||||
|
||||
RUNS = dict({f"s84 {k}": (lambda v=v: s84.run(**v))
|
||||
for k, v in s84.SCENARIOS.items()},
|
||||
**{f"s87 {k}": (lambda v=v: s87_run(v))
|
||||
for k, v in s87.SCENARIOS.items()})
|
||||
|
||||
|
||||
def s87_run(v):
|
||||
v = dict(v)
|
||||
return s84.run(1, dur=s87.DUR, store=s87.store(v.pop("store", None)), **v)
|
||||
|
||||
|
||||
_moved, _gone = [], 0
|
||||
for k, fn in RUNS.items():
|
||||
new, old = fn()[0], no_fix(fn)[0]
|
||||
same = all(a[:-1] == b[:-1] and a[-1][:4] == b[-1][:4]
|
||||
for a, b in zip(new, old)) and len(new) == len(old)
|
||||
logs = {e for r in old for e in [r[-1][5]] if e} - {
|
||||
e for r in new for e in [r[-1][5]] if e}
|
||||
_gone += len(logs)
|
||||
if not same or any("OUTPUT_CLAMP episode" not in e for e in logs):
|
||||
_moved.append(k)
|
||||
print(f" {k}: commands and claims "
|
||||
f"{'the same' if same else 'DIFFER'}, log rows differ {len(logs)}, "
|
||||
f"digest {s84.digest(new)[:16]}")
|
||||
check("⭐ in all twelve, every command, loop field, claim, guard decision "
|
||||
"and phase is the same as without the fix; the only log rows that "
|
||||
"differ are OUTPUT_CLAMP rows (gone, or a lower count)",
|
||||
not _moved and _gone > 0)
|
||||
|
||||
print()
|
||||
if fails:
|
||||
print(f"FAILED: {len(fails)} check(s)")
|
||||
|
||||
@@ -472,19 +472,22 @@ print("\n8. the N=1 and N=2 digests")
|
||||
# charge pin stands. n2_charge and _banked change only the loop's reason and
|
||||
# claim on 2 cycles; n2_charge_dear changes commands on 98 cycles, where A's
|
||||
# pin lifts (t=1800) and B no longer jumps 1390 W, and after the surplus step.
|
||||
# SAFETY-119 re-pinned all six: only the log. OUTPUT_CLAMP rows from the
|
||||
# loop's bound just after the guard hands back are gone; every command and
|
||||
# claim is the same (test_safety119.py part 5).
|
||||
PINNED = {
|
||||
"n1_charge":
|
||||
"0ed25c856456b97acb07a39299a8cb7b1a762c4d0005b88a0823c03057a80be5",
|
||||
"19781f90b9751b10994dedeaafb38e42d48ad2551640792e601336b7f9b5bb4f",
|
||||
"n1_charge_dear":
|
||||
"c25340b56060d4581f7204e200b07e7c8618711be2baca3b1667b7ebeb600dc3",
|
||||
"eeeb0fa19a6fdc04d44a0838bb2bb911f506a936b0f93d8a6f4d87159053ec74",
|
||||
"n1_charge_banked":
|
||||
"2619acdd06b04e56c000b168d64e1d044346c6ba3f929c94258d6ad8189ec609",
|
||||
"a51f125a8c1c98fdbb009fce72b6781d20d384bdf0f5ce7bbfe367d86e5477db",
|
||||
"n2_charge":
|
||||
"b16d692128f14bcce3d87b142d290e0d6b13724289680e44a05215c9eaa24a7c",
|
||||
"e37e98390baa5dad9128991fabf8b170894395fe026d8a33c8a51de7ed0789cc",
|
||||
"n2_charge_dear":
|
||||
"00a197d89a5dd1e8b3ae6a5d1af66329a4dc4850476569c5931c125fa4d9eaee",
|
||||
"7727372de423f62d77b4426a7226eae31a30f3bb89a3cf70272cc7c1823d7263",
|
||||
"n2_charge_banked":
|
||||
"a6973faea916e4c88ca1770047f94478346eeb1efa4889b154fad0747f91267f",
|
||||
"c77e26006c026b953e9c3dcc9a02f3993589f885cb52c7cbceac6bd59b600551",
|
||||
}
|
||||
_now = s84.digests()
|
||||
for k in PINNED:
|
||||
@@ -492,7 +495,8 @@ for k in PINNED:
|
||||
f"{_now[k][:16]}")
|
||||
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 "
|
||||
"claim), and N=2 outside a cheap window matches its SAFETY-85 pin",
|
||||
"claim; the log as re-pinned by SAFETY-119), and N=2 outside a cheap "
|
||||
"window matches its SAFETY-85 pin",
|
||||
all(_now[k] == PINNED[k] for k in ("n1_charge", "n1_charge_dear",
|
||||
"n1_charge_banked",
|
||||
"n2_charge_dear")))
|
||||
|
||||
@@ -518,17 +518,21 @@ check("...a stopped controller still says so: the peak alarm does not take "
|
||||
|
||||
# ============================================================================ #
|
||||
print("\n9. N=1 is byte-identical to release/1.0 b8ddfd7")
|
||||
# SAFETY-119 re-pinned n1_crossing, n1_above and n1_charge_low: only the
|
||||
# log. The loop's bound just after a hand-back is not counted, so OUTPUT_CLAMP
|
||||
# rows are gone or count fewer; every command and claim is the same
|
||||
# (test_safety119.py part 5).
|
||||
PINNED = { # tools/safety87_identity.py on b8ddfd7
|
||||
"n1_under":
|
||||
"d2395ef8d74fb23607ad864d5a4bf62c03044bbd9367e873675d7ad10e8c169f",
|
||||
"n1_crossing":
|
||||
"97f7321017d9d34ee17a40dfd4abf73d915f25b9faef086a483d2b5cd124b198",
|
||||
"20f8e542c1ee3eb3a58c8fd6ba3b3e5b6b1486800cd63b675abc68bc222a172b",
|
||||
"n1_above":
|
||||
"45726a285aa84f40542d56482652397c3c1e212ba4fe2009f6cc52f537ccad52",
|
||||
"3a79f1394b2b49fa88f3c054da1d9da450153ad5f0d2a2c2f960d02a7abe2208",
|
||||
"n1_under_banked":
|
||||
"a2876c7cc873bc4c974d2b4bd98e23bba2d007b84545587d2fe0b713b0a6249b",
|
||||
"n1_charge_low":
|
||||
"91336e3c0314985a3b00f314c79b5ffb5a740d218de3ad44f88a4db6e4235b32",
|
||||
"2e1ee6db0d4f9f18836c4cbbe406fddd496656b54b97f329da513a1591f0ce6c",
|
||||
"n1_hold":
|
||||
"0a1df347754c69166b20d04c15ee878bbcefb02abb824b9b452334f68f03e46d",
|
||||
}
|
||||
@@ -536,7 +540,7 @@ _now = s87.digests()
|
||||
for k in PINNED:
|
||||
print(f" {k} {'same' if _now[k] == PINNED[k] else 'DIFFERS'}")
|
||||
check("⭐ every N=1 digest (claims, commands, the guard's decision and the "
|
||||
"log) equals b8ddfd7's, under, crossing and above the reserve, in a "
|
||||
"log) equals b8ddfd7's (the log as re-pinned by SAFETY-119), under, crossing and above the reserve, in a "
|
||||
"charge and a HOLD, and under a banked peak",
|
||||
_now == PINNED)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user