SAFETY-47: what a shadow run can show, and the watts on its status row
checks / unit (pull_request) Successful in 7m48s
checks / unit (pull_request) Successful in 7m48s
Not a leak and not a write-path defect. Shadow mode's safety is intact -
the substitution, the re-seed, the authorisation gate and the clamp are
untouched. What was wrong is what three merged documents SAY a shadow run
can show you, and a number on the status row that can be 6.61x what a live
controller would command.
SHADOW MODE CANNOT SURFACE A CLOSED-LOOP DEFECT, AND THE DOCUMENTS NOW SAY
SO. The battery is held at 0 W and the carried command is re-seeded to it,
so every measured input - the meter, the billed-quarter accumulator behind
it, the battery power, the SoC - describes a plant that did not respond. A
closed-loop defect is one whose stimulus IS that response. This is the same
property that makes shadow mode safe, so it is not a gap to close later:
the closed-loop answer comes from the closed-loop rig or from a live run.
SAFETY-10 §5 already conceded this for the grid-following loop. It was
never the loop leg's limitation; it is the general case.
THE "STRATEGIES ARE UNAFFECTED" EXEMPTION WAS TRUE UNTIL SAFETY-45 MERGED,
ONE COMMIT EARLIER. That card sizes the buy leg as min(asked, headroom)
with headroom = (900/remaining) x (ceiling - projected), and `projected`
reads the open quarter's accumulator - the meter - which is a function of
what the battery did earlier in that quarter. Measured on TEST-23 stage 3
at peak_cap_w 1500 / house 1200 / max_w 2000, one hour at 5 s:
SAFETY-45's limit cycle NOT visible. 28 onsets live vs 1; grid
-455.5..+3200.0 W vs flat at the 1200 W house;
guard engaged 410/720 cycles vs 0/720.
SAFETY-46's SoC churn NOT visible. The SoC moves 0.000000000 pp.
TARIFF-14's wrong hour VISIBLE, and it is the only one: which hour, at
what price, clearing what spread - identical on
720 of 720 charging cycles at a binding cap.
The exemption narrows to exactly what survives: WHICH hour a strategy picks
and WHY are unaffected; HOW HARD it trades is not. The peak guard's ceiling
and reasons are worth reading from a shadow run; its ENGAGEMENTS are not.
THE NUMBER IS CAVEATED, NOT RE-COMPUTED, AND THAT IS THE DECISION. The
arithmetic is not wrong - 900/remaining is correct for a meter genuinely
under the ceiling, and in shadow the meter genuinely is. It is the right
answer to a question nobody asked. Both surfaces now introduce the watts as
what would have been commanded ON THIS CYCLE, in every state, and carry
shadow.SIZED_FROM_METER whenever a measured billed quarter sized them.
Rejected, and recorded in the decision doc: re-computing it (the recorded
decision would no longer be the decision the shipped path reached, while the
arbiter, clamp, SAFETY-28 gate, log and MQTT all ran on the real one - a
second controller everywhere except the wire); recording nothing (blanks the
row exactly when arbitrage is trading, and the log's edge key is the watts);
and recording the unsized ask beside it (a second number, no warning, and at
the shipped 3500/2000 both figures read 2000 W).
"UPPER BOUND" AND NOT "SMALLER", AND THE DIFFERENCE WAS MEASURED. Charging
only adds offtake, so the shadow size is >= the live size with EQUALITY at
the start of every quarter. Pairwise over 3600 charging cycles in five
configurations: 0 below, 730 exactly equal. The first draft said "smaller",
which would have been false on every one of those 730.
AND THE PREDICATE IS NOT KEYED ON WHETHER THE TRADE WAS CAPPED. arbitrage's
own sentence says "SIZED BY THE CAPACITY-TARIFF GUARD" only when the
headroom came out below the ask - in a shadow run, the START of the quarter,
where the two runs agree to the float. By the end, 900/remaining has
multiplied the headroom past the ask, the trade is not capped, the sentence
is silent, and the record is at its most wrong. A caveat keyed on it would
be quietest exactly where it is needed loudest.
SAFETY-10 §6's COMMITTED EVIDENCE COULD NEVER HAVE FAILED, FOR TWO
INDEPENDENT REASONS, AND CLOSING EITHER ALONE CLOSES NOTHING. Its fixture is
peak_cap_w 15000 against a 350 W house, so the guard never engages and the
sizing is inert - the harness's own comment says so. AND every identity row
reads the FIRST charging sample, where at elapsed=0 the projection IS the
held level and the two runs agree to the float even at a cap that does bind.
Measured: widening the fixture alone leaves that comparison passing. That
second reason is a new shape for MUTATION.md's habit-4 list - an INDEX, not
a fixture - and it is recorded there as the twelfth instance. §6 is re-taken
at a binding cap across the whole run: 716 of 720 charging cycles differ,
headroom 300.0..54000.0 W in shadow against 299.9..307.8 W live, recorded
-2000.0 W against a live -302.4 W.
THE GRADER IS test_arbitrage.py PART 3d, beside SAFETY-45's own closed-loop
regression and built on the same s45_loop() harness, so the runs differ in
one argument. Three lines stand in for _write_unit()'s substitution and
_shadow_settle()'s record and re-seed, marked as a transcription;
test_clamp.py part 8 remains the check on the shipped placement. The section
carries its own precondition into every check: a mutation that stopped
either run trading made it raise on its first min(), kill the file and
report ZERO reds - FCST-06's shape, driven rather than imagined.
AND THE FIX'S OWN FIRST DRAFT CARRIED THE SAME DEFECT. Deleting the caveat
from the action-log line left all 29 files green, because the sentence was
an f-string inside _shadow_settle() and the only suite that drives it uses a
meter_source:off fixture where no arbitrage decision exists. It is
shadow.log_line() now, for the reason words() is a function.
14 mutations, no survivors; M13 and M14 are +0 B and were confirmed on disk
by sha256 with every run purged. M14's first form was aimed at the wrong
direction and correctly survived - min->max makes the shadow record larger,
which is still an upper bound. All 29 check files green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
This commit is contained in:
co-authored by
Claude Opus 5
parent
e6da478f87
commit
e3ccd9f6ea
+169
-12
@@ -19,6 +19,83 @@ learn nothing. Shadow mode is the third option: strategies, arbiter, clamps and
|
||||
guards all run, the decision is recorded with its reason, and NOTHING THE
|
||||
CONTROLLER DECIDES REACHES THE INVERTER.
|
||||
|
||||
⚠️ AND OF THOSE THREE, A SHADOW RUN CAN SHOW YOU EXACTLY ONE - SEE THE NEXT
|
||||
SECTION. This header used to name SAFETY-45's limit cycle as the example
|
||||
of what shadow mode surfaces. That was false, and SAFETY-47 measured it.
|
||||
|
||||
⚠️ WHAT A SHADOW RUN CAN AND CANNOT SHOW (SAFETY-47)
|
||||
=====================================================
|
||||
SHADOW MODE CANNOT SURFACE A CLOSED-LOOP DEFECT. Not badly, not partially -
|
||||
NOT AT ALL, and by the same construction that makes it safe. The actuator is
|
||||
held at WIRE_W and the carried command is re-seeded to WIRE_W, so every
|
||||
MEASURED input - the meter, the quarter accumulator behind it, the battery
|
||||
power, the state of charge - describes a plant that DID NOT RESPOND. A
|
||||
closed-loop defect is by definition one whose stimulus is the plant's response
|
||||
to the controller. No response, no stimulus, no defect to see.
|
||||
|
||||
That is not a gap to be closed later. Feeding the guards a simulated plant
|
||||
instead is what SAFETY-10 §4 refuses for the sign watchdogs, for a reason that
|
||||
applies to all of this: a controller reacting to a plant that does not exist
|
||||
drifts further from reality every tick, and three cycles in its numbers
|
||||
describe nothing. THE CLOSED-LOOP ANSWER COMES FROM A CLOSED-LOOP RIG
|
||||
(TEST-23 stage 3) OR FROM A LIVE RUN. There is no third place.
|
||||
|
||||
So, of the three defects above, measured on that rig at `peak_cap_w` 1500 /
|
||||
house 1200 / `max_w` 2000, one hour at a 5 s cycle:
|
||||
|
||||
* SAFETY-45's limit cycle NOT VISIBLE. Live: 28 charge onsets, the grid
|
||||
swinging -455.5 .. +3200.0 W, the guard engaged
|
||||
on 410 of 720 cycles. Shadow, same hour, same
|
||||
code: 1 onset, the grid flat at the 1200 W
|
||||
house, the guard engaged on 0 of 720. Every
|
||||
term of `wanted = batt_w + excess` is measured.
|
||||
* SAFETY-46's SoC churn NOT VISIBLE. The SoC moves 0.000000000 pp in a
|
||||
shadow run, because it is the pack's integral of
|
||||
what the pack was commanded and the pack was
|
||||
commanded 0 W. No SoC limit can be REACHED in a
|
||||
shadow run; the ones already crossed when it
|
||||
started are reported, and that is all.
|
||||
* TARIFF-14's wrong quarter VISIBLE, and it is the one. The price lookup's
|
||||
inputs are the CLOCK and the PRICE SERIES, and
|
||||
neither is a function of what the battery did.
|
||||
Measured: which hour the strategy picked, its
|
||||
price and the spread it cleared are identical
|
||||
live and shadow on 720 of 720 charging cycles,
|
||||
AT A BINDING CAP.
|
||||
|
||||
⚠️ THE EXEMPTION THAT USED TO BE HERE WAS TRUE UNTIL SAFETY-45 MERGED. This
|
||||
module, DOCS.md and DECISIONS/SAFETY-10-shadow-mode.md all said "the strategies
|
||||
are unaffected - they ask for a fixed power from inputs that do not depend on
|
||||
what the battery did". SAFETY-45 made the buy leg `min(asked, headroom)` with
|
||||
|
||||
headroom = (900 / remaining) x (ceiling - projected)
|
||||
|
||||
and `projected` reads the OPEN QUARTER'S ACCUMULATOR - the meter - which is a
|
||||
function of what the battery did earlier in that same quarter. The strategy
|
||||
became closed-loop the day that landed. What survives of the exemption, and
|
||||
only this:
|
||||
|
||||
WHICH hour a strategy picks, and WHY, are unaffected.
|
||||
HOW HARD it trades is not.
|
||||
|
||||
⚠️ AND THE SIZE A SHADOW RUN RECORDS IS THE RIGHT ANSWER TO A QUESTION NOBODY
|
||||
ASKED. `900/remaining` is correct arithmetic for a meter that is genuinely
|
||||
under the ceiling - and in shadow the meter genuinely IS under it, because the
|
||||
battery is not charging. So the number is not a bug; it is an answer computed
|
||||
for a plant that did not exist. Measured on the same rig, shipped code:
|
||||
|
||||
cap / house live commanded shadow recorded over
|
||||
1500 / 1200 -302.4 W -2000.0 W 6.61x
|
||||
3500 / 3000 -502.6 W -2000.0 W 3.98x
|
||||
3500 / 2000 -1502.6 W -2000.0 W 1.33x
|
||||
3500 / 1800 -1702.6 W -2000.0 W 1.17x
|
||||
15000 / 350 -2000.0 W -2000.0 W 1.00x
|
||||
|
||||
The last row is the fixture SAFETY-10's own evidence was taken on, which is why
|
||||
nothing reported this. `words()` carries the caveat rather than a re-computed
|
||||
number, and `DECISIONS/SAFETY-47-what-a-shadow-run-can-show.md` §3 records the
|
||||
three alternatives that were rejected and why.
|
||||
|
||||
⚠️ "REACHES NOTHING" IS NOT "WRITES NOTHING", AND THE DIFFERENCE IS THE WHOLE
|
||||
SAFETY ARGUMENT
|
||||
=======================================================================
|
||||
@@ -138,11 +215,18 @@ THE FAILSAFES, ONE BY ONE (the card asks for each to be decided and recorded)
|
||||
| | resolved intent against a meter that has |
|
||||
| | stopped moving, and both halves are as |
|
||||
| | true in shadow as out of it. |
|
||||
| SAFETY-07/24/25 peak guard | OBSERVES. Level-triggered, unlatched, and |
|
||||
| | it files its claim as usual so the |
|
||||
| | recorded decision is the real one. |
|
||||
| | SAFETY-45's limit cycle is visible from a |
|
||||
| | shadow run; that is the point. |
|
||||
| SAFETY-07/24/25 peak guard | OBSERVES, AND SEES ONLY THE HOUSE. It is |
|
||||
| | level-triggered and unlatched and it files|
|
||||
| | its claim as usual, so the recorded |
|
||||
| | decision is the real one FOR THIS METER - |
|
||||
| | but every term of `wanted = batt_w + |
|
||||
| | excess` is measured, and in shadow mode |
|
||||
| | the battery contributes nothing to either.|
|
||||
| | ⚠️ SAFETY-47: this row used to claim |
|
||||
| | SAFETY-45's limit cycle was visible from a|
|
||||
| | shadow run. It is not - 410/720 engaged |
|
||||
| | cycles live against 0/720 in shadow, on |
|
||||
| | the same hour and the same code. |
|
||||
| SAFETY-31 circulation | OBSERVES AND MAY LATCH, legitimately. Its |
|
||||
| SAFETY-42 delivery | evidence is MEASURED power, not a |
|
||||
| | command: packs pushing against each other |
|
||||
@@ -215,6 +299,29 @@ MODES = ("auto", "on", "off")
|
||||
#: commanding it so the inverter cannot be left latched on anything else.
|
||||
WIRE_W = 0.0
|
||||
|
||||
#: SAFETY-47. What the recorded watts mean when a MEASURED billed quarter sized
|
||||
#: them.
|
||||
#:
|
||||
#: ⚠️ "UPPER BOUND" AND NOT "SMALLER", AND THE DIFFERENCE WAS MEASURED RATHER
|
||||
#: THAN ARGUED. Charging only ADDS offtake, so a live meter's `projected` is
|
||||
#: never below a shadow meter's and the headroom is never above it: the shadow
|
||||
#: size is >= the live size, with EQUALITY at the start of every quarter, where
|
||||
#: `900/remaining` is 1.0 and neither plant has moved. Measured pairwise over
|
||||
#: 3600 charging cycles in five configurations - 0 cycles below, 730 exactly
|
||||
#: equal. A sentence reading "smaller" would have been false on every one of
|
||||
#: those 730.
|
||||
#:
|
||||
#: ⚠️ AND IT NAMES NO FACTOR. How far under the bound a live run lands is a
|
||||
#: property of the site - 6.61x at a 1500 W cap under a 1200 W house, 1.00x at
|
||||
#: a cap that does not bind - and a number invented for the row would be a
|
||||
#: second helping of the defect this constant exists to disclose.
|
||||
SIZED_FROM_METER = (
|
||||
"⚠️ AND ITS SIZE CAME FROM THE BILLED-QUARTER METER, WHICH HAS ONLY THE "
|
||||
"HOUSE IN IT WHILE THE BATTERY IS HELD AT 0 W. A live run sizes the same "
|
||||
"trade against a meter the battery is moving, so this is an UPPER BOUND "
|
||||
"and not a prediction; how far under it a live run lands is a closed-loop "
|
||||
"question and a shadow run cannot answer it")
|
||||
|
||||
|
||||
def mode(opts: dict) -> str:
|
||||
"""The configured `shadow_mode`, normalised to one of `MODES`.
|
||||
@@ -279,7 +386,7 @@ def why(opts: dict, *, control_started: bool) -> str:
|
||||
|
||||
|
||||
def words(opts: dict, *, control_started: bool, would_w: float | None,
|
||||
reason: str = "") -> str:
|
||||
sized_from_meter: bool, reason: str = "") -> str:
|
||||
"""The status page's `Shadow mode` row.
|
||||
|
||||
⚠️ THE ROW IS RENDERED IN EVERY STATE, INCLUDING `off`. A surface whose
|
||||
@@ -288,10 +395,35 @@ def words(opts: dict, *, control_started: bool, would_w: float | None,
|
||||
what gets graded, so there has to be text in both states.
|
||||
|
||||
`would_w` is the decision the path reached this cycle - what a live
|
||||
controller would have put on the wire - and `reason` is
|
||||
`Resolution.explain()`, the same sentence the `Why` row carries. Together
|
||||
they are the whole of "what would it have done, and why"; without them the
|
||||
row would say a shadow run happened and nothing about what it found.
|
||||
controller would have put on the wire GIVEN THIS METER AND A BATTERY AT
|
||||
0 W - and `reason` is `Resolution.explain()`, the same sentence the `Why`
|
||||
row carries. Together they are the whole of "what would it have done, and
|
||||
why"; without them the row would say a shadow run happened and nothing
|
||||
about what it found.
|
||||
|
||||
⚠️ `sized_from_meter` HAS NO DEFAULT, AND THAT IS SAFETY-47's WHOLE POINT.
|
||||
It is True when a MEASURED billed-quarter headroom sized the number being
|
||||
recorded - `arbitrage.sized_from_meter()` answers it, and it is the one
|
||||
place the question is asked. A default of False here would be a caller
|
||||
that forgot the caveat rendering a row that reads exactly like a caller
|
||||
that decided it did not apply, which is the shape of defect this card is
|
||||
about. TEST-14's rule with a new name on it.
|
||||
|
||||
⚠️ AND THE PHRASE IS "ON THIS CYCLE", UNCONDITIONALLY, NOT ONLY WHEN THE
|
||||
CAVEAT FIRES. Every number a shadow run records was reached from a plant
|
||||
that did not respond; "WOULD HAVE COMMANDED -2 000 W" reads as a forecast
|
||||
of a live run and no number here is one. The ⚠️ below is the STRONGER,
|
||||
conditional statement about the cycles where the answer can be multiples
|
||||
out - see this module's header for the measured table.
|
||||
|
||||
⚠️ AND IT IS NOT KEYED ON WHETHER THE TRADE WAS CAPPED. `arbitrage`'s own
|
||||
sentence says "SIZED BY THE CAPACITY-TARIFF GUARD" only when the headroom
|
||||
came out BELOW the ask - which in a shadow run is the START of the
|
||||
quarter, where the two runs still agree. By the end of the quarter
|
||||
`900/remaining` has multiplied the headroom past the ask, the trade is
|
||||
NOT capped, the sentence is silent and the recorded number is at its most
|
||||
wrong. A caveat keyed on that sentence would be quietest exactly where it
|
||||
is needed loudest. `test_arbitrage.py` part 3d-8 drives that.
|
||||
"""
|
||||
head = why(opts, control_started=control_started)
|
||||
if not engaged(opts, control_started=control_started):
|
||||
@@ -304,13 +436,38 @@ def words(opts: dict, *, control_started: bool, would_w: float | None,
|
||||
# half that says WHICH RUNG decided, which is the claim MUTATION.md's
|
||||
# masking rule says must survive.
|
||||
num = f"{float(would_w):+,.0f}".replace(",", " ")
|
||||
out = (f"{head}; WOULD HAVE COMMANDED {num} W, inverter held at "
|
||||
f"{WIRE_W:.0f} W")
|
||||
out = (f"{head}; ON THIS CYCLE WOULD HAVE COMMANDED {num} W, inverter "
|
||||
f"held at {WIRE_W:.0f} W")
|
||||
if sized_from_meter:
|
||||
out += f" {SIZED_FROM_METER}"
|
||||
if reason:
|
||||
out += f" - {reason}"
|
||||
return out
|
||||
|
||||
|
||||
def log_line(*, would_w: float, reason: str, sized_from_meter: bool) -> str:
|
||||
"""The action-log line for one change of shadow decision.
|
||||
|
||||
⚠️ IT IS A FUNCTION FOR THE REASON `words()` IS ONE, AND SAFETY-47 LEARNED
|
||||
IT THE EXPENSIVE WAY. This sentence used to be an f-string inside
|
||||
`Controller._shadow_settle()`. Deleting the caveat from it there left all
|
||||
29 files GREEN - the only check that could see the line drove a fixture
|
||||
with `meter_source: off`, where no arbitrage decision and therefore no
|
||||
caveat can ever be reached. A sentence assembled at a call site is a
|
||||
sentence no check can grade, and the page would have said one thing while
|
||||
the record a later analysis reads said another.
|
||||
|
||||
⚠️ AND IT IS THE SAME `SIZED_FROM_METER` CONSTANT THE ROW CARRIES, not a
|
||||
second wording of it. Two sentences meaning one thing is how the two
|
||||
surfaces start disagreeing about which cycles are caveated.
|
||||
"""
|
||||
out = (f"SHADOW: would have commanded {float(would_w):+.0f} W ON THIS "
|
||||
f"CYCLE - {reason}")
|
||||
if sized_from_meter:
|
||||
out += f" {SIZED_FROM_METER}"
|
||||
return out
|
||||
|
||||
|
||||
def banner(opts: dict, *, control_started: bool, latched: tuple = ()) -> tuple:
|
||||
"""`(level, text)` for the top of the status page, or `None` when live.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user