SAFETY-48: what the lead cap actually does to a frozen integrator, and what the peak figure depends on
checks / unit (pull_request) Successful in 7m55s
checks / unit (pull_request) Successful in 7m55s
Four review findings, none of them in executable code. The control law, the tests and the instruments' arithmetic are untouched; every number below was re-measured against the committed 16-day capture before it was written down. R1 - app/control.py carried a justification the decision record and MUTATION.md habit 22 had already retracted, and it was false about the code it sits in. It said an unconditional placement "would also pull the integrator toward a command the SATURATION freeze is pinning", priced at 118 of 256 250 ticks. The 118 was measured against a different pair; on the pair the sentence names the two forms are bit-identical - 0 of 256 246 ticks, re-measured. And the narrow form that shipped DOES move a saturation-frozen integrator: it runs before the freeze block and is not gated on `frozen`. Of 79 160 frozen ticks the cap changes `i_w` on 1095, and on 679 of those it moves it further out on the side it was already on - putting back magnitude the freeze had just let fall, which the freeze's own rule forbids. Worst case 2424 W, at prev_w -3400 W, grid +4697 W, actual -4144 W, sat_count 2 -> 3: the freeze leaves the integrator at -176 W and the cap drags it to -2600 W. It never overrides a same-cycle refusal (the freeze clipped on 75 512 ticks, the slew bound on none of them), and that is stated too. It is also where SAFETY-13's settle values went: gating the cap on `not frozen` puts test_control.py part 5 back at -1190 W exactly, while part 4(b) does not come back - that one moved for the winding reason. R3 - the record read its own sensitivity table backwards. `fit_plant()` picks lag 7.5 s on battery-power RMS, which separates it from 2.5 s by 0.3 %, while the tool's own printed fidelity measure (export against the recorded 29.43 kWh) prefers 2.5 s by the whole 35 % gap. Swept properly: at every lag <= 5.0 s release/1.0's worst quarter is ALREADY 3434 W and this card changes it by exactly zero. The 3739 -> 3434 W headline exists at one plant. New section 3a carries the table; the export reduction now leads every summary and the peak is presented as lag-dependent. The form-A rejection is strengthened rather than weakened: form A is worse on the billed quarter at every lag where release/1.0 produces a peak at all, and at every top-N cut of the quarters. R2 - DOCS.md stated modelled watts to the owner as measurement. It now leads with the export reduction, says the figures are simulated, and says plainly that no capacity-tariff improvement is promised. R4 - tools/slew_sim.py --slew narrated "the OVER-ABSORPTION does not move, and at the slowest slews it is slightly WORSE" while printing a monotonically falling column. That sentence is TRUE on release/1.0 (reproduced: 958/1107/1133/1058/987/1029/1150/1143/1081) and false under this branch's law (777/734/663/563/553/507/435/358/321), so SAFETY-39 is not retracted: its record gets a forward reference, and the script now reads SLEW_LEAD_STEPS out of the law it imported and narrates from the column it just printed. Both branches of that narration were driven. R5 - three counts in the L table were decoration and are re-driven in the CI image on a `cp -a`, purged, `:ro`, one file at a time: L5 is 5+5 with the function names exchanged and 5+4 with the branch bodies exchanged (two mutations wearing one name, now listed separately), L6 is 6+3. The separable- state count is grid-dependent and three sweeps disagree, so all three are quoted with their grids and the load-bearing claim is WHERE they separate. R7 - "a large import answered too weakly" is not what the aggregate shows. Form A cuts import by 5.45 kWh and export by 5.18 kWh, and paired per tick delivers 8.56 kWh less offtake energy on the ticks release/1.0 was importing. The harm is a tail redistribution inside quarters that already net to offtake. The conclusion stands; the sentence is narrowed to it. Verified: the full suite in the CI image, purged tree, `:ro` mount, one file at a time, exactly the command in .gitea/workflows/checks.yml - 30/30, exit 0. 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
03ef61fa3b
commit
a6af332048
@@ -187,6 +187,23 @@ over-absorption. So the rig does not choose the number — **the constraint
|
|||||||
chooses it, and the rig says what the constraint does and does not buy.** That
|
chooses it, and the rig says what the constraint does and does not buy.** That
|
||||||
is the same relationship the 2026-08-22 table had to its own three columns.
|
is the same relationship the 2026-08-22 table had to its own three columns.
|
||||||
|
|
||||||
|
⚠️ **FORWARD REFERENCE, ADDED BY SAFETY-48 — THIS TABLE AND THIS SENTENCE ARE
|
||||||
|
TRUE OF release/1.0's LAW AND FALSE OF THE ONE THAT REPLACED IT.** Nothing above
|
||||||
|
is retracted: re-run on release/1.0 today the same rig prints the same column
|
||||||
|
(958 / 1107 / 1133 / 1058 / 987 / 1029 / 1150 / 1143 / 1081 W), flat,
|
||||||
|
non-monotone, 800 better than 556. Under SAFETY-48's lead cap the **same script
|
||||||
|
on the same plant** prints 777 / 734 / 663 / 563 / 553 / 507 / 435 / 358 / 321 W
|
||||||
|
— falling monotonically, so a slower slew is now *better* and the sentence above
|
||||||
|
inverts. The mechanism SAFETY-39 identified is exactly why: `slew_w` bounds the
|
||||||
|
OUTPUT while the integrator goes on accumulating `gain · error`, so slowing the
|
||||||
|
slew used to buy nothing; bound the integrator's lead and it starts to.
|
||||||
|
`tools/slew_sim.py --slew` now names which of the two laws it is running under
|
||||||
|
before it narrates the column, and
|
||||||
|
`DECISIONS/SAFETY-48-bounding-the-integrator-lead.md` §6 carries both tables and
|
||||||
|
the independent confirmation from `test_control.py`'s own plant model. ⚠️ **This
|
||||||
|
does NOT re-open the choice of 556**: the constraint picks the number, the rig
|
||||||
|
does not, and that is unchanged in both laws.
|
||||||
|
|
||||||
⚠️ **A single step is not evidence here.** The deadband, the 10 W quantisation
|
⚠️ **A single step is not evidence here.** The deadband, the 10 W quantisation
|
||||||
and the phase of the step against the tick move the result by hundreds of watts,
|
and the phase of the step against the tick move the result by hundreds of watts,
|
||||||
non-monotonically. The first cut of this derivation was a single step and ranked
|
non-monotonically. The first cut of this derivation was a single step and ranked
|
||||||
|
|||||||
@@ -19,15 +19,22 @@ does not deploy and does not bump the version.
|
|||||||
2. ⚠️ **THE FORM THE CARD ASKED FOR RAISES THE BILLED QUANTITY, AND THAT IS WHY
|
2. ⚠️ **THE FORM THE CARD ASKED FOR RAISES THE BILLED QUANTITY, AND THAT IS WHY
|
||||||
IT IS NOT WHAT SHIPPED.** Reusing the asymmetric freeze on a carried slew
|
IT IS NOT WHAT SHIPPED.** Reusing the asymmetric freeze on a carried slew
|
||||||
flag cuts export 13 % and moves the worst quarter-hour average **offtake**
|
flag cuts export 13 % and moves the worst quarter-hour average **offtake**
|
||||||
from 3739 W to **3935 W**, with the mean quarter +2.7 W. A frozen integrator
|
from 3739 W to **3935 W**, with the mean quarter +2.7 W — and on the billed
|
||||||
stops at `prev + gain·error`, which undershoots what the site needs by
|
quarter it is worse at **every plant lag where release/1.0 produces a peak
|
||||||
`1 − gain`, so a large import is answered too weakly for as long as the ramp
|
at all** (a tie at lag 0, worse at 2.5 / 5.0 / 7.5 / 10.0 s) and at **every
|
||||||
lasts. §3.
|
top-N cut of the quarters**, which makes this the least lag-sensitive result
|
||||||
|
in the card. The harm is a tail redistribution inside quarters that already
|
||||||
|
net to offtake, not a weaker answer to import in aggregate. §3, §3a.
|
||||||
3. **What ships bounds the LEAD at one slew step, only where the slew is what
|
3. **What ships bounds the LEAD at one slew step, only where the slew is what
|
||||||
stopped the command, and only in the leading direction.** Export 39.6 → 34.1
|
stopped the command, and only in the leading direction.** The robust result
|
||||||
kWh, wasted throughput 4.67 → 3.19 kWh, sign reversals 2992 → 2770, worst
|
is the **export reduction — 39.6 → 34.1 kWh, and it falls at every plant and
|
||||||
quarter-hour offtake 3739 → **3434 W** (it improves), median recovery 232 →
|
tuning tried** — with wasted throughput 4.67 → 3.19 kWh, sign reversals
|
||||||
199 s. §4.
|
2992 → 2770 and median recovery 232 → 199 s. ⚠️ **The billed quarter is not
|
||||||
|
something this card can claim.** It reads 3739 → **3434 W** at the fitted
|
||||||
|
plant lag of 7.5 s; at every lag ≤ 5.0 s — including the one that best
|
||||||
|
reproduces the recorded export — release/1.0 is *already* at 3434 W and this
|
||||||
|
card moves it by exactly zero. The lag is not identified by the data. §4,
|
||||||
|
§3a.
|
||||||
4. **Two committed numbers moved and neither is a silent edit.** SAFETY-13's
|
4. **Two committed numbers moved and neither is a silent edit.** SAFETY-13's
|
||||||
released command settles lower, because there is less wound integrator to
|
released command settles lower, because there is less wound integrator to
|
||||||
settle onto; and SAFETY-39's *"the slew does not own the over-absorption"* is
|
settle onto; and SAFETY-39's *"the slew does not own the over-absorption"* is
|
||||||
@@ -124,15 +131,82 @@ offtake, and form A raises it by **196 W** — and raises the MEAN quarter by
|
|||||||
2.7 W, so this is systematic and not one unlucky quarter. It does so in **five
|
2.7 W, so this is systematic and not one unlucky quarter. It does so in **five
|
||||||
of the six** plant and tuning variants in `--sensitivity`; the sixth is a tie.
|
of the six** plant and tuning variants in `--sensitivity`; the sixth is a tie.
|
||||||
|
|
||||||
The mechanism is not subtle once it is stated: a frozen integrator stops at
|
The per-cycle mechanism is that a frozen integrator stops at
|
||||||
`prev + gain·error(first cycle)`, which is `(1 − gain)` short of what the site
|
`prev + gain·error(first cycle)`, which is `(1 − gain)` short of what the site
|
||||||
needs — 40 % short at `gain: 0.6`. For an EXPORT that under-correction is free;
|
needs — 40 % short at `gain: 0.6`.
|
||||||
for an IMPORT it is offtake, for as long as the ramp lasts.
|
|
||||||
|
⚠️ **AND THE SENTENCE THAT USED TO FOLLOW THAT ONE WAS TOO BIG FOR THE DATA.**
|
||||||
|
It read *"for an IMPORT it is offtake, so a large import is answered too weakly
|
||||||
|
for as long as the ramp lasts"*. The aggregate does not show a weaker answer to
|
||||||
|
import: form A cuts total import by **5.45 kWh** and export by **5.18 kWh**
|
||||||
|
against release/1.0, and paired tick by tick it delivers **8.56 kWh LESS**
|
||||||
|
offtake energy on the ticks release/1.0 spent importing (lower on 38 229 such
|
||||||
|
ticks, higher on 19 669). What form A moves is the **TAIL, within quarters that
|
||||||
|
already net to offtake** — the worst quarter goes 3739 → 3935 W, the top five
|
||||||
|
3260 → 3300 W, the top fifty 1880.0 → 1882.7 W. The capacity tariff bills the
|
||||||
|
worst quarter and nothing else, so a redistribution that leaves the mean alone
|
||||||
|
and lifts the maximum is exactly the trade that is refused here. The conclusion
|
||||||
|
is unchanged; the causal sentence is now the one the data supports.
|
||||||
|
|
||||||
⚠️ **THE CARD SAID "do not invent a second rule", AND THE MEASUREMENT SAYS TO.**
|
⚠️ **THE CARD SAID "do not invent a second rule", AND THE MEASUREMENT SAYS TO.**
|
||||||
That instruction is recorded here rather than quietly dropped. What ships is a
|
That instruction is recorded here rather than quietly dropped. What ships is a
|
||||||
different rule; the paragraph above is why.
|
different rule; the paragraph above is why.
|
||||||
|
|
||||||
|
## 3a. ⚠️ The plant lag is NOT identified by this recording, and the billed quarter is the claim that depends on it
|
||||||
|
|
||||||
|
`fit_plant()` picks `lag 7.5 s` because it minimises **battery-power RMS**. That
|
||||||
|
objective barely distinguishes the candidates — 375.3 W at 7.5 s against 376.4 W
|
||||||
|
at 2.5 s, **0.3 %** — while the tool's *own* printed fidelity measure, replayed
|
||||||
|
export against the **29.43 kWh this install actually exported**, prefers 2.5 s by
|
||||||
|
the whole of the 35 % gap the header warns about. Two measures on the same data
|
||||||
|
disagree, and nothing here breaks the tie.
|
||||||
|
|
||||||
|
So the sweep has to be read as a sensitivity, not as a confirmation:
|
||||||
|
|
||||||
|
| plant lag | fit RMS W | replayed export kWh (release/1.0) | **worst 15-min offtake W:** release/1.0 | form A | SAFETY-48 | mean quarter ΔW |
|
||||||
|
|---:|---:|---:|---:|---:|---:|---:|
|
||||||
|
| 0.0 | 392.3 | 23.5 | **3434** | 3434 | **3434** | +0.53 |
|
||||||
|
| 2.5 | **376.4** | **28.9** ← nearest 29.43 | **3434** | ⚠️ 3732 | **3434** | −0.59 |
|
||||||
|
| 5.0 | 378.0 | 32.1 | **3434** | ⚠️ 3711 | **3434** | −1.02 |
|
||||||
|
| 7.5 | **375.3** ← chosen | 39.6 | 3739 | ⚠️ 3935 | **3434** | −2.39 |
|
||||||
|
| 10.0 | 384.2 | 75.4 | 3954 | ⚠️ 4326 | **3434** | −4.65 |
|
||||||
|
|
||||||
|
(The RMS column is `fit_plant()`'s own inner sweep pinned to one lag — best `tau`
|
||||||
|
and a re-fitted gain/offset at that lag. The export and quarter columns follow
|
||||||
|
`--sensitivity`'s convention instead: only the lag is substituted into the
|
||||||
|
global fit, everything else held. That is why the lag 5.0 and 10.0 export
|
||||||
|
figures are the ones already printed in §7b.)
|
||||||
|
|
||||||
|
⚠️ **AT EVERY LAG ≤ 5.0 s THE IMPROVEMENT IS NOT AN IMPROVEMENT — IT IS AN
|
||||||
|
ABSENCE.** release/1.0's worst quarter is *already* 3434 W there and SAFETY-48
|
||||||
|
changes it by **exactly zero**. §7b's paragraph used to read the `3434 → 3434`
|
||||||
|
rows as "the direction does not move"; that is the wrong reading. The direction
|
||||||
|
has nothing to move: at those plants release/1.0 never produces a quarter worse
|
||||||
|
than 3434 W in the first place, and the 3739 → 3434 W headline exists only at
|
||||||
|
the single longest lag `fit_plant()` happens to select. **The peak claim is a
|
||||||
|
property of the plant model, not of the card.** What the card can claim on the
|
||||||
|
billed quantity is that it does not make it worse, at any lag.
|
||||||
|
|
||||||
|
⚠️ **AND THE ONE RESULT HERE THAT IS NOT LAG-SENSITIVE IS THE FORM-A
|
||||||
|
REJECTION.** Form A's worst quarter is the worst of the three at **every lag
|
||||||
|
where release/1.0 produces a peak at all** — 3732, 3711, 3935, 4326 against
|
||||||
|
release/1.0's 3434, 3434, 3739, 3954 at lags 2.5 / 5.0 / 7.5 / 10.0, and a tie
|
||||||
|
at lag 0.0 where both sit at 3434. It is worse at every top-N cut of the
|
||||||
|
quarters at the fitted plant (top-1 3934.6 against 3738.7, top-5 3299.7 against
|
||||||
|
3260.5, top-10 2890.6 against 2871.0, top-50 1882.7 against 1880.0, top-100
|
||||||
|
1481.7 against 1479.0), and it raises the mean quarter. §3 understated this: the rejection does not rest on one unlucky
|
||||||
|
quarter at one plant, which is exactly the objection a lag-sensitive result
|
||||||
|
invites.
|
||||||
|
|
||||||
|
⚠️ **WHAT WOULD SETTLE IT IS NOT IN THIS RECORDING.** Both objectives above are
|
||||||
|
computed from the same four recorded entities, and they disagree; nothing else
|
||||||
|
in the capture breaks the tie, and this card did not go looking for a third
|
||||||
|
objective after seeing which lag each of the first two preferred. A driven step
|
||||||
|
response — the command timestamped where it is written rather than where Home
|
||||||
|
Assistant recorded it — is what identifies a lag. That is a rig measurement and
|
||||||
|
it is not this card. Until it exists, the export reduction is what this card
|
||||||
|
delivers and the peak figure is a plant-model artefact.
|
||||||
|
|
||||||
## 4. What ships
|
## 4. What ships
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@@ -154,13 +228,23 @@ Four properties, and each of them is a decision:
|
|||||||
thirty files until a check was written for it.** Applying it to every cycle
|
thirty files until a check was written for it.** Applying it to every cycle
|
||||||
instead — `i_w = max(target - cap, min(target + cap, i_w))` after
|
instead — `i_w = max(target - cap, min(target + cap, i_w))` after
|
||||||
`target = slewed`, −92 B — is **bit-identical on the shipped configuration**:
|
`target = slewed`, −92 B — is **bit-identical on the shipped configuration**:
|
||||||
0 of 256 246 ticks differ over the 16-day recording. The two are separable on
|
0 of 256 246 ticks differ over the 16-day recording. The two separate **only**
|
||||||
**2050 of 257 400** swept states, every one of them at `integrator_max_w`
|
where `integrator_max_w` is set ABOVE `max_w` — the configuration `control.py`
|
||||||
ABOVE `max_w` — the configuration `control.py` says is the only way its
|
says is the only way its `clamped` reason is reachable. There the integrator
|
||||||
`clamped` reason is reachable. There the integrator is *deliberately* allowed
|
is *deliberately* allowed above the rail, and an unconditional cap would drag
|
||||||
above the rail, and an unconditional cap would drag it back to the command
|
it back to the command every cycle and make the option inert.
|
||||||
every cycle and make the option inert. `test_telemetry.py` part 4 drives that
|
`test_telemetry.py` part 4 drives that configuration, and the unconditional
|
||||||
configuration, and the unconditional form is red on it.
|
form is red on it.
|
||||||
|
|
||||||
|
⚠️ **THE COUNT OF SEPARABLE STATES IS DECORATION AND THREE SWEEPS DISAGREE
|
||||||
|
ABOUT IT.** This card's grid gave **2050 of 257 400**; an independent review
|
||||||
|
measured **1220 of 246 064**; a third, smaller grid (four tunings × 7 `prev_w`
|
||||||
|
× 9 `grid_w` × 5 `actual_w` × 8 `i_w` including `None` × 4 `sat_count`) gives
|
||||||
|
**776 of 40 320** — and that third one is spelled out here precisely because
|
||||||
|
the first two are not, which is the whole reason they cannot be reconciled.
|
||||||
|
No count here should be read as exact without the grid beside it. All three
|
||||||
|
agree on the load-bearing part, which is **where**: every separable state sits
|
||||||
|
in the `integrator_max_w > max_w` tuning and nowhere else.
|
||||||
|
|
||||||
⚠️ **AND AN EARLIER DRAFT OF THIS BULLET GAVE THE WRONG REASON AND THE WRONG
|
⚠️ **AND AN EARLIER DRAFT OF THIS BULLET GAVE THE WRONG REASON AND THE WRONG
|
||||||
NUMBER.** It said the two forms differ on 118 ticks because an unconditional
|
NUMBER.** It said the two forms differ on 118 ticks because an unconditional
|
||||||
@@ -170,6 +254,21 @@ Four properties, and each of them is a decision:
|
|||||||
does not apply to the placement actually being compared, which sits before
|
does not apply to the placement actually being compared, which sits before
|
||||||
the freeze. The figures above are the ones that were measured on the pair the
|
the freeze. The figures above are the ones that were measured on the pair the
|
||||||
sentence names.
|
sentence names.
|
||||||
|
|
||||||
|
⚠️ **AND THE SATURATION STORY IS NOT MERELY INAPPLICABLE — IT IS BACKWARDS.
|
||||||
|
THE NARROW CAP THAT SHIPPED *DOES* MOVE A SATURATION-FROZEN INTEGRATOR.** It
|
||||||
|
sits before the freeze block and is not gated on `frozen`. Measured on the
|
||||||
|
same 16 days at the live shape: of **79 160** frozen ticks the cap changed
|
||||||
|
`i_w` on **1095**, and on **679** of those it moved it further out on the side
|
||||||
|
it was already on — putting back magnitude the freeze had just let fall, which
|
||||||
|
is the one direction the freeze's rule ("may fall, never rise") exists to
|
||||||
|
forbid. Worst case **2424 W**, at
|
||||||
|
`prev_w −3400 W, grid +4697 W, actual −4144 W, sat_count 2 → 3`: the freeze
|
||||||
|
leaves the integrator at **−176 W**, the slew binds, and the cap drags it to
|
||||||
|
**−2600 W**. It never overrides a *same-cycle* refusal — the freeze clipped
|
||||||
|
`moved` on 75 512 ticks and the slew bound on none of them — but the claim
|
||||||
|
that this placement leaves the frozen path alone is false, and §5 is where
|
||||||
|
that shows up in a committed number.
|
||||||
* **It is written one-sided, and that is a CLARITY choice, not a correctness
|
* **It is written one-sided, and that is a CLARITY choice, not a correctness
|
||||||
one.** ⚠️ **An earlier draft of this file and of the comment in `control.py`
|
one.** ⚠️ **An earlier draft of this file and of the comment in `control.py`
|
||||||
said the two-sided band `max(slewed - cap, min(slewed + cap, i_w))` was a bug
|
said the two-sided band `max(slewed - cap, min(slewed + cap, i_w))` was a bug
|
||||||
@@ -235,22 +334,35 @@ writer holding the pack at +700 W, the command pinned, then released — settles
|
|||||||
at **−800 W** where release/1.0 settled at −1190 W against an integrator of
|
at **−800 W** where release/1.0 settled at −1190 W against an integrator of
|
||||||
−1194 W; and its sibling run settles at **−1200 W** where release/1.0 settled at
|
−1194 W; and its sibling run settles at **−1200 W** where release/1.0 settled at
|
||||||
−2390 W. The property AC 4 asserts is unchanged: the released command stops at
|
−2390 W. The property AC 4 asserts is unchanged: the released command stops at
|
||||||
the frozen integrator and not at the rail. What changed is how far that
|
the frozen integrator and not at the rail.
|
||||||
integrator had been allowed to wind. Both literals in `test_control.py` were
|
|
||||||
re-baselined **with the reason beside them**, and the second is now asserted
|
⚠️ **AND THE TWO MOVED FOR TWO DIFFERENT REASONS, WHICH THE ONE-LINE EXPLANATION
|
||||||
against `i_w` rather than against a number.
|
"how far that integrator had been allowed to wind" COVERS ONLY HALF OF.** Gating
|
||||||
|
the cap on `not frozen` — one line, `if frozen: _lead_cap = float("inf")`, so
|
||||||
|
the cap becomes a no-op while frozen — puts **part 5 back at release/1.0's
|
||||||
|
−1190 W exactly**. That run moved because the cap **fires on frozen cycles**
|
||||||
|
(§4). **Part 4(b) does not come back**: it is −1200 W gated and −1200 W shipped,
|
||||||
|
against release/1.0's −2390 W, because *that* one moved for the winding reason.
|
||||||
|
Both literals in `test_control.py` were re-baselined **with the reason beside
|
||||||
|
them**, and the second is now asserted against `i_w` rather than against a
|
||||||
|
number.
|
||||||
|
|
||||||
**The alternative that avoids it was measured and rejected.** Gating the cap on
|
**The alternative that avoids it was measured and rejected.** Gating the cap on
|
||||||
`not frozen` leaves SAFETY-13 bit-identical and gives export 34.9 kWh (against
|
`not frozen` as above gives export **34.71 kWh** (against 34.07), worst quarter
|
||||||
34.07), worst quarter 3461 W (against 3434), wasted 3.5 kWh (against 3.19) — and
|
**3434 W** (the same as shipped), wasted **3.49 kWh** (against 3.19) — and a
|
||||||
a worst observed lead of **4966 W**, i.e. no bound at all on the quantity this
|
worst observed lead of **4961 W**, i.e. no bound at all on the quantity this
|
||||||
card exists to bound. A bound with an exception large enough to drive a truck
|
card exists to bound. A bound with an exception large enough to drive a truck
|
||||||
through is not a bound.
|
through is not a bound. ⚠️ These four figures replace an earlier set (34.9,
|
||||||
|
3461 W, 3.5 kWh, 4966 W) that does not reproduce on this tree with the gate
|
||||||
|
spelled as above; the gate spelling is written out here so the next reader can
|
||||||
|
re-run it rather than re-derive it.
|
||||||
|
|
||||||
**One quarter-hour in 1534 is 532 W worse.** The billed figure is a MAXIMUM, and
|
**One quarter-hour in 1534 is 532 W worse.** The billed figure is a MAXIMUM, and
|
||||||
the maximum improves (3739 → 3434 W); no quarter under SAFETY-48 reaches
|
at the fitted plant lag the maximum improves (3739 → 3434 W); no quarter under
|
||||||
release/1.0's own worst. The mean moves the right way. The tail is reported
|
SAFETY-48 reaches release/1.0's own worst. The mean moves the right way. The
|
||||||
because a maximum over 1534 quarters is decided by one of them.
|
tail is reported because a maximum over 1534 quarters is decided by one of them
|
||||||
|
— and §3a is the reason that improvement is not claimed as a benefit of the
|
||||||
|
card: at every shorter plant lag release/1.0's own maximum is already 3434 W.
|
||||||
|
|
||||||
## 6. The independent confirmation, which arrived as a failing check
|
## 6. The independent confirmation, which arrived as a failing check
|
||||||
|
|
||||||
@@ -280,6 +392,25 @@ slew owns a share of the over-absorption it did not own before. `gain` is still
|
|||||||
not touched here, for SAFETY-39's own reason: two live control-law constants
|
not touched here, for SAFETY-39's own reason: two live control-law constants
|
||||||
moving in one release makes the next measurement uninterpretable.
|
moving in one release makes the next measurement uninterpretable.
|
||||||
|
|
||||||
|
⚠️ **AND THE RIG SAYS THE SAME THING, WHICH MEANS TWO COMMITTED TRANSCRIPTS
|
||||||
|
CHANGED MEANING UNDER THIS CARD.** `tools/slew_sim.py --slew` runs the same
|
||||||
|
derivation on admin/goodwe's `plantsim.Plant`, a third model again. Worst-case
|
||||||
|
over-absorption over its 51-step family, `slew_w` from 1000 down to 250:
|
||||||
|
|
||||||
|
| `slew_w` | 1000 | 800 | 700 | 600 | 556 | 500 | 400 | 300 | 250 |
|
||||||
|
|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
|
||||||
|
| release/1.0 | 958 | 1107 | 1133 | 1058 | 987 | 1029 | 1150 | 1143 | 1081 |
|
||||||
|
| **this branch** | 777 | 734 | 663 | 563 | 553 | 507 | 435 | 358 | 321 |
|
||||||
|
|
||||||
|
release/1.0's row is flat and non-monotone and **800 really is better than 556**
|
||||||
|
on that column — SAFETY-39 measured it correctly and its §3 sentence is true of
|
||||||
|
the law it was written against. This branch's row falls monotonically. So
|
||||||
|
SAFETY-39's record is not retracted; it is given a **forward reference**, and
|
||||||
|
`slew_sim.py`'s own narration now reads `SLEW_LEAD_STEPS` out of the control law
|
||||||
|
it imported and says which of the two tables it is printing. MUTATION.md habit 8
|
||||||
|
— a committed transcript is a check, and a transcript whose prose contradicts
|
||||||
|
its own table is a failing one.
|
||||||
|
|
||||||
## 7. Verified by breaking it
|
## 7. Verified by breaking it
|
||||||
|
|
||||||
**The law.** `test_telemetry.py` part 4 and `test_control.py`'s SAFETY-39
|
**The law.** `test_telemetry.py` part 4 and `test_control.py`'s SAFETY-39
|
||||||
@@ -304,18 +435,32 @@ after and after the revert:
|
|||||||
| # | mutation to `app/control.py` | bytes | reds |
|
| # | mutation to `app/control.py` | bytes | reds |
|
||||||
|---|---|---:|---|
|
|---|---|---:|---|
|
||||||
| L1 | the whole SAFETY-48 block deleted | −195 B | 5 in `test_telemetry.py`, 4 in `test_control.py` |
|
| L1 | the whole SAFETY-48 block deleted | −195 B | 5 in `test_telemetry.py`, 4 in `test_control.py` |
|
||||||
| L2 | `SLEW_LEAD_STEPS = 0` — the velocity form | −0 B | 4 + 3 |
|
| L2 | `SLEW_LEAD_STEPS = 0` — the velocity form | +0 B | 4 + 3 |
|
||||||
| L3 | `SLEW_LEAD_STEPS = 12` — a cap above the rail | +1 B | 4 + 3 |
|
| L3 | `SLEW_LEAD_STEPS = 12` — a cap above the rail | +1 B | 4 + 3 |
|
||||||
| L4 | the cap applied unconditionally, outside the slew branch | −92 B | 1 — **and it was a SURVIVOR of all thirty files before that check existed** |
|
| L4 | the cap applied unconditionally, outside the slew branch | −92 B | 1 + 0 — **and it was a SURVIVOR of all thirty files before that check existed** |
|
||||||
| L5 | `min`/`max` swapped — the cap on the wrong side | +0 B | 2 + 5, one of them `test_control.py`'s own "frozen integrator never deadlocks" over 13 041 states |
|
| L5a | the `min`/`max` FUNCTION names exchanged | +0 B | 5 + 5, one of them `test_control.py`'s own "frozen integrator never deadlocks" over 13 041 states |
|
||||||
| L6 | the lead measured against `prev_w` rather than the command emitted | +0 B | 4 + 3 |
|
| L5b | the two BRANCH BODIES exchanged — the other reading of "the cap on the wrong side" | +0 B | 5 + 4, and it does **not** reach the deadlock sweep |
|
||||||
|
| L6 | the lead measured against `prev_w` rather than the command emitted | +0 B | 6 + 3 |
|
||||||
|
|
||||||
⚠️ **L5 AND L6 ARE BOTH ZERO BYTES** — `min`/`max` and `slewed`/`prev_w` are
|
⚠️ **L5 AND L6 ARE BOTH ZERO BYTES** — `min`/`max` and `slewed`/`prev_w` are
|
||||||
each the same length — so no fingerprint and no byte delta can see either, and
|
each the same length — so no fingerprint and no byte delta can see either, and
|
||||||
one of L5's reds is `test_control.py`'s own *"frozen integrator never
|
one of L5a's reds is `test_control.py`'s own *"frozen integrator never
|
||||||
deadlocks"* sweep over 13 041 states, written by SAFETY-13 for a different
|
deadlocks"* sweep over 13 041 states, written by SAFETY-13 for a different
|
||||||
reason.
|
reason.
|
||||||
|
|
||||||
|
⚠️ **THREE OF THESE COUNTS WERE WRONG AND ARE RE-MEASURED ABOVE, WHICH IS
|
||||||
|
MUTATION.md's REPORTING RULE 3 APPLIED TO THIS CARD'S OWN TABLE.** The earlier
|
||||||
|
row gave L5 as 2 + 5 and L6 as 4 + 3; driven again in the CI image on a `cp -a`
|
||||||
|
of this tree, purged, `:ro`, one file at a time, they are **5 + 5** and
|
||||||
|
**6 + 3**. ⚠️ **AND "L5" NAMED TWO DIFFERENT MUTATIONS.** "`min`/`max` swapped"
|
||||||
|
can mean the function names exchanged or the branch bodies exchanged; they are
|
||||||
|
both red, they are both the cap on the wrong side, and they give different
|
||||||
|
counts (5 + 5 against 5 + 4) and reach different checks. A row that writes one
|
||||||
|
number for a mutation a reader cannot re-derive from its own description is a
|
||||||
|
row that cannot be re-run, so both spellings are listed. The directions in the
|
||||||
|
original table were all correct; the counts were decoration, and decoration in
|
||||||
|
a mutation table reads as evidence.
|
||||||
|
|
||||||
## 7b. The transcript, quoted rather than summarised
|
## 7b. The transcript, quoted rather than summarised
|
||||||
|
|
||||||
`python3 tools/replay_recorded_loop.py --sensitivity`, on this branch, 58 s:
|
`python3 tools/replay_recorded_loop.py --sensitivity`, on this branch, 58 s:
|
||||||
@@ -362,7 +507,16 @@ SENSITIVITY - the ranking, not the watts
|
|||||||
```
|
```
|
||||||
|
|
||||||
⚠️ **`--sensitivity` IS THE RANKING AND NOT THE WATTS.** The export figure moves
|
⚠️ **`--sensitivity` IS THE RANKING AND NOT THE WATTS.** The export figure moves
|
||||||
by a factor of two across these plants; what does not move is the direction.
|
by a factor of two across these plants; what does not move is the direction —
|
||||||
|
**on export**. ⚠️ **THE `worst quarter W` COLUMN IS NOT THE SAME KIND OF
|
||||||
|
EVIDENCE AND THIS PARAGRAPH USED TO TREAT IT AS IF IT WERE.** Three of its six
|
||||||
|
rows read `3434 -> 3434`, and that is not "the direction holds": it is
|
||||||
|
release/1.0 *already sitting at 3434 W* on those plants, with nothing for the
|
||||||
|
card to improve. §3a sweeps the lag properly and finds the same thing at every
|
||||||
|
lag ≤ 5.0 s. The billed-quarter improvement in the A/B table above exists at one
|
||||||
|
plant, the one `fit_plant()` selects by an objective that separates it from its
|
||||||
|
neighbour by 0.3 %.
|
||||||
|
|
||||||
Two rows deserve naming rather than rounding: at `slew_w: 555.6` (the rail) and
|
Two rows deserve naming rather than rounding: at `slew_w: 555.6` (the rail) and
|
||||||
at `gain: 0.3` the mean quarter-hour delta is −0.17 W and **+0.16 W** — ties,
|
at `gain: 0.3` the mean quarter-hour delta is −0.17 W and **+0.16 W** — ties,
|
||||||
not improvements. Both are regimes where `|error| > slew_w / gain` is rarely
|
not improvements. Both are regimes where `|error| > slew_w / gain` is rarely
|
||||||
|
|||||||
@@ -385,10 +385,28 @@ saturation freeze is holding the command (`Loop reason` says
|
|||||||
Before this bound existed, the same 6 kW export drove the lead to 4600 W and the
|
Before this bound existed, the same 6 kW export drove the lead to 4600 W and the
|
||||||
integrator to its 5000 W rail, and the command then needed twelve cycles — about
|
integrator to its 5000 W rail, and the command then needed twelve cycles — about
|
||||||
a minute at this install's meter rate — to walk back down once the disturbance
|
a minute at this install's meter rate — to walk back down once the disturbance
|
||||||
cleared. Measured over 16 days of the reference install's own recorded
|
cleared.
|
||||||
disturbances, bounding it cut avoidable export by 5.5 kWh, cut wasted battery
|
|
||||||
throughput from 4.67 to 3.19 kWh, and **lowered** the worst quarter-hour average
|
⚠️ **What the bound buys — and how much of it is a model rather than a meter
|
||||||
offtake — the billed one — from 3739 W to 3434 W.
|
reading.** Sixteen days of the reference install's own recorded disturbances
|
||||||
|
were replayed through both laws (`tools/replay_recorded_loop.py`). The result
|
||||||
|
that survives every plant and tuning tried is the **export reduction: about an
|
||||||
|
eighth of the modelled export** — 5.5 kWh of 39.6 at the plant lag the replay
|
||||||
|
fits, 3.8 kWh of 28.9 at the shorter lag that best reproduces what this install
|
||||||
|
actually exported — with wasted battery throughput falling alongside it, 4.67 to
|
||||||
|
3.19 kWh at the fitted plant. ⚠️ **Those are simulated watt-hours.** The
|
||||||
|
replay's fitted inverter carries a ~375 W RMS residual and prints `+35 %
|
||||||
|
PESSIMISTIC` beside its own export figure on every run; the *direction* is what
|
||||||
|
it establishes, not the absolute kWh.
|
||||||
|
|
||||||
|
⚠️ **And it does NOT promise you a lower capacity-tariff peak.** The worst
|
||||||
|
quarter-hour average offtake — the billed quantity — improves from 3739 W to
|
||||||
|
3434 W **only at the longest plant lag the replay fits**. At every shorter lag,
|
||||||
|
including the one that best reproduces the recorded export, release/1.0 already
|
||||||
|
sits at 3434 W and this bound changes it by exactly zero. The plant lag is not
|
||||||
|
identified by the recording, so treat the peak as unchanged and the export as
|
||||||
|
the reason this exists. `DECISIONS/SAFETY-48-bounding-the-integrator-lead.md`
|
||||||
|
§3a has the table.
|
||||||
|
|
||||||
⚠️ **Two roster names that differ only in punctuation or case** — `inverter-b`
|
⚠️ **Two roster names that differ only in punctuation or case** — `inverter-b`
|
||||||
and `Inverter B` — are two names to the validator and one id once slugged. The
|
and `Inverter B` — are two names to the validator and one id once slugged. The
|
||||||
|
|||||||
@@ -216,36 +216,48 @@ _SAT_COUNT_CAP = SETTLE_CYCLES + 1 # 13
|
|||||||
# threshold LOWER, so the case fires more often, not less.
|
# threshold LOWER, so the case fires more often, not less.
|
||||||
#
|
#
|
||||||
# ⚠️ WHAT IS BOUNDED IS THE LEAD, NOT THE INTEGRATOR. Two forms were measured
|
# ⚠️ WHAT IS BOUNDED IS THE LEAD, NOT THE INTEGRATOR. Two forms were measured
|
||||||
# over 15.7 days of the live install's own recorded disturbances
|
# over 16.0 days of the live install's own recorded disturbances
|
||||||
# (`tools/replay_recorded_loop.py`, and DECISIONS/SAFETY-48 has the table):
|
# (`tools/replay_recorded_loop.py`, and DECISIONS/SAFETY-48 has the table):
|
||||||
#
|
#
|
||||||
# (a) reuse the asymmetric freeze on a carried slew flag - "may not wind
|
# (a) reuse the asymmetric freeze on a carried slew flag - "may not wind
|
||||||
# further in the direction it is already pushing", exactly as saturation
|
# further in the direction it is already pushing", exactly as saturation
|
||||||
# does. It cuts export 13 % AND RAISES THE BILLED QUANTITY: the worst
|
# does. It cuts export 13 % AND RAISES THE BILLED QUANTITY: the worst
|
||||||
# quarter-hour average OFFTAKE goes 3839 -> 3935 W and the mean quarter
|
# quarter-hour average OFFTAKE goes 3739 -> 3935 W and the mean quarter
|
||||||
# moves +2.7 W, in five of six plant and tuning variants. A frozen
|
# moves +2.7 W, in five of six plant and tuning variants, and on the
|
||||||
# integrator stops at `prev + gain * error(first cycle)`, which UNDER-
|
# billed quarter it is worse at every plant lag where release/1.0
|
||||||
# shoots what the site needs by (1 - gain), so a large import is answered
|
# produces a peak at all (a tie at lag 0, worse at 2.5/5/7.5/10 s)
|
||||||
# too weakly for as long as the ramp lasts. That is the wrong trade on a
|
# and at every top-N cut of the quarters.
|
||||||
# capacity tariff and the reason this is not that fix.
|
#
|
||||||
|
# ⚠️ AND THE MECHANISM IS A REDISTRIBUTION, NOT A WEAKER ANSWER. An
|
||||||
|
# earlier draft of this line said a frozen integrator undershoots by
|
||||||
|
# (1 - gain) "so a large import is answered too weakly". The aggregate
|
||||||
|
# does not show that: form A cuts total import by 5.45 kWh, and paired
|
||||||
|
# tick by tick against release/1.0 it delivers 8.56 kWh LESS offtake
|
||||||
|
# energy on the ticks release/1.0 was importing. What it moves is the
|
||||||
|
# TAIL - within quarters that already net to offtake, the worst of them
|
||||||
|
# gets worse - and the capacity tariff bills the worst quarter and
|
||||||
|
# nothing else. That is the wrong trade here and the reason this is not
|
||||||
|
# the fix that ships.
|
||||||
#
|
#
|
||||||
# (b) cap how far the integrator may LEAD the command, one slew step. Export
|
# (b) cap how far the integrator may LEAD the command, one slew step. Export
|
||||||
# 39.8 -> 34.1 kWh, wasted throughput 4.7 -> 3.2 kWh, sign reversals
|
# 39.6 -> 34.1 kWh, wasted throughput 4.7 -> 3.2 kWh, sign reversals
|
||||||
# 3050 -> 2806, worst quarter-hour offtake 3839 -> 3434 W (it IMPROVES),
|
# 2992 -> 2770, worst quarter-hour offtake 3739 -> 3434 W at the fitted
|
||||||
# median recovery 237 -> 214 s. It is what ships.
|
# plant lag (⚠️ AND AT NO SHORTER ONE: at every lag <= 5.0 s release/1.0
|
||||||
|
# is already at 3434 W and this changes it by zero - DECISIONS/SAFETY-48
|
||||||
|
# §3a), median recovery 232 -> 199 s. It is what ships.
|
||||||
#
|
#
|
||||||
# ⚠️ AND THE CAP IS NOT ZERO. `i_w = slewed` is full back-calculation, and this
|
# ⚠️ AND THE CAP IS NOT ZERO. `i_w = slewed` is full back-calculation, and this
|
||||||
# module's own docstring says what that costs: passing `i_w=None` "reduces this
|
# module's own docstring says what that costs: passing `i_w=None` "reduces this
|
||||||
# function to the exact velocity form it replaced". A zero cap makes `i_w`
|
# function to the exact velocity form it replaced". A zero cap makes `i_w`
|
||||||
# identically the command on every cycle, which is that same collapse written
|
# identically the command on every cycle, which is that same collapse written
|
||||||
# from the other end - it measures 32.1 kWh, better than (b) by 2 kWh, and it
|
# from the other end - it measures 32.0 kWh, better than (b) by 2 kWh, and it
|
||||||
# throws away the reason the accumulator was split out at all. One step is the
|
# throws away the reason the accumulator was split out at all. One step is the
|
||||||
# smallest cap that is not that.
|
# smallest cap that is not that.
|
||||||
#
|
#
|
||||||
# WHAT THE NUMBER MEANS: the command can cover a one-step lead in ONE cycle, so
|
# WHAT THE NUMBER MEANS: the command can cover a one-step lead in ONE cycle, so
|
||||||
# the worst-case unwind after a reversal is one cycle instead of
|
# the worst-case unwind after a reversal is one cycle instead of
|
||||||
# `integrator_max_w / slew_w` - 12.5 cycles at the live shape, ~60 s at the
|
# `integrator_max_w / slew_w` - 12.5 cycles at the live shape, ~60 s at the
|
||||||
# meter's measured 4.84 s tick.
|
# meter's measured 5.0 s tick.
|
||||||
#
|
#
|
||||||
# ⚠️ IT IS IN STEPS RATHER THAN IN WATTS FOR SAFETY-39's REASON, WHICH IS THE
|
# ⚠️ IT IS IN STEPS RATHER THAN IN WATTS FOR SAFETY-39's REASON, WHICH IS THE
|
||||||
# REASON THIS FILE KEEPS RE-LEARNING. A watt figure is a constant that outlives
|
# REASON THIS FILE KEEPS RE-LEARNING. A watt figure is a constant that outlives
|
||||||
@@ -570,16 +582,49 @@ def compute(
|
|||||||
if slewed != target:
|
if slewed != target:
|
||||||
reason = "slew-limited"
|
reason = "slew-limited"
|
||||||
# --- SAFETY-48: the step ceiling reaches the integrator too ---------
|
# --- SAFETY-48: the step ceiling reaches the integrator too ---------
|
||||||
# ⚠️ INSIDE THIS BRANCH AND NOWHERE ELSE. The bound is on the lead the
|
# ⚠️ INSIDE THIS BRANCH AND NOWHERE ELSE - AND THE REASON IS NOT THE
|
||||||
# SLEW created, so it is applied only where the slew is what stopped
|
# SATURATION FREEZE. An earlier draft of this comment said that applied
|
||||||
# the command. Applied unconditionally it would also pull the
|
# unconditionally the cap "would also pull the integrator toward a
|
||||||
# integrator toward a command the SATURATION freeze is pinning - and
|
# command the SATURATION freeze is pinning", and priced that at 118 of
|
||||||
# SAFETY-13's whole design is that the frozen integrator keeps standing
|
# 256 250 ticks. Both halves were invented, and MUTATION.md habit 22 is
|
||||||
# where it is, so that releasing the freeze buys the standing error
|
# what fell out of it. The 118 had been measured against a DIFFERENT
|
||||||
# back. Measured over the recording, the two forms differ on 118 of
|
# pair - a band applied after quantisation, at the very end of
|
||||||
# 256 250 ticks and agree on every reported figure to one decimal; the
|
# `compute()`. On the pair the sentence actually names - this same cap
|
||||||
# narrow one is written because it is the one whose scope matches its
|
# moved out of this branch to just after `target = slewed` - the two
|
||||||
# sentence.
|
# forms are BIT-IDENTICAL on the shipped configuration: 0 of 256 246
|
||||||
|
# ticks differ over the 16-day recording. They separate only where
|
||||||
|
# `integrator_max_w` is deliberately set ABOVE `max_w`, the one
|
||||||
|
# configuration this file says can reach its `clamped` reason; there
|
||||||
|
# the integrator is meant to run above the rail and an unconditional
|
||||||
|
# cap would drag it back to the command every cycle and make the option
|
||||||
|
# inert. THAT is why the cap stays in the branch, and
|
||||||
|
# `test_telemetry.py` part 4 drives it. (The separable-state COUNT is a
|
||||||
|
# property of whatever grid was swept - 2050 of 257 400 on this card's,
|
||||||
|
# 1220 of 246 064 on the review's, 776 of 40 320 on a third - so the
|
||||||
|
# claim to carry forward is WHERE they separate, not how many.)
|
||||||
|
#
|
||||||
|
# ⚠️ AND THE HALF A READER WILL OTHERWISE ASSUME AWAY: THIS CAP DOES
|
||||||
|
# MOVE A SATURATION-FROZEN INTEGRATOR. It runs BEFORE the freeze block
|
||||||
|
# below and is not gated on `frozen`. Measured over the same recording
|
||||||
|
# at the live shape: of 79 160 frozen ticks the cap changed `i_w` on
|
||||||
|
# 1095 of them, and on 679 of those it moved it FURTHER OUT on the side
|
||||||
|
# it was already on - putting back magnitude the freeze had just let
|
||||||
|
# fall, which is the one direction the freeze's own rule ("may fall,
|
||||||
|
# never rise") exists to forbid. Worst case 2424 W, and the worked tick
|
||||||
|
# is `prev_w -3400 W, grid +4697 W, actual -4144 W, sat_count 2 -> 3`:
|
||||||
|
# the freeze leaves the integrator at -176 W, the slew binds, and the
|
||||||
|
# cap drags it to -2600 W. It never overrides a SAME-CYCLE refusal -
|
||||||
|
# the freeze clipped `moved` on 75 512 ticks and the slew bound on none
|
||||||
|
# of them - but it does not leave the frozen path alone either.
|
||||||
|
#
|
||||||
|
# ⚠️ AND THAT IS WHERE SAFETY-13's SETTLE VALUES WENT. Gating this cap
|
||||||
|
# on `not frozen` puts `test_control.py` part 5 back at release/1.0's
|
||||||
|
# -1190 W exactly; part 4(b)'s -2390 -> -1200 W does NOT come back,
|
||||||
|
# because that one moved for the other reason - the winding BEFORE the
|
||||||
|
# freeze is bounded now. The gate is refused anyway: measured the same
|
||||||
|
# way it leaves a worst observed lead of 4961 W, i.e. no bound at all
|
||||||
|
# on the quantity this card exists to bound. DECISIONS/SAFETY-48 §5
|
||||||
|
# carries the rest of the price.
|
||||||
#
|
#
|
||||||
# ⚠️ WRITTEN ONE-SIDED, AND IT IS **EQUIVALENT** TO THE TWO-SIDED BAND
|
# ⚠️ WRITTEN ONE-SIDED, AND IT IS **EQUIVALENT** TO THE TWO-SIDED BAND
|
||||||
# `max(slewed - cap, min(slewed + cap, i_w))` - MEASURED, NOT REASONED.
|
# `max(slewed - cap, min(slewed + cap, i_w))` - MEASURED, NOT REASONED.
|
||||||
|
|||||||
@@ -51,6 +51,13 @@ of the tree, one file at a time, and say which tree. M7's count was written as
|
|||||||
was decoration, which is the half of MUTATION.md's reporting rule 3 that is
|
was decoration, which is the half of MUTATION.md's reporting rule 3 that is
|
||||||
easiest to let rot.
|
easiest to let rot.
|
||||||
|
|
||||||
|
⚠️ AND SAFETY-48 HAS ALREADY MOVED ONE OF THEM. M1 defeats FIVE checks on
|
||||||
|
TEL-10's own tree and FOUR on this one: this card replaced the "...the gap is
|
||||||
|
the slew limit and not an accident" check, because the gap it asserted is now
|
||||||
|
bounded and the assertion is written against the bound instead. The mutation
|
||||||
|
did not change; the suite did. Both counts are right, for different trees, and
|
||||||
|
neither is right without one.
|
||||||
|
|
||||||
⚠️ M9 IS ZERO BYTES AND CHANGES NO PAYLOAD. Every id, name, unit and topic is
|
⚠️ M9 IS ZERO BYTES AND CHANGES NO PAYLOAD. Every id, name, unit and topic is
|
||||||
identical; only the ORDER of the retained messages moves. No fingerprint and no
|
identical; only the ORDER of the retained messages moves. No fingerprint and no
|
||||||
byte delta can see it, which is why part 1 asserts the announcement as an
|
byte delta can see it, which is why part 1 asserts the announcement as an
|
||||||
@@ -88,16 +95,28 @@ SAFETY-48's OWN BATTERY, same discipline, against `app/control.py`:
|
|||||||
until part 4's
|
until part 4's
|
||||||
`integrator_max_w`
|
`integrator_max_w`
|
||||||
case existed
|
case existed
|
||||||
L5 `min`/`max` swapped - the cap on the wrong +0 B 2 here + 5 control
|
L5 `min`/`max` swapped - the cap on the wrong +0 B 5 here + 5 control
|
||||||
side
|
side (or 5 + 4; see below)
|
||||||
L6 the lead measured against `prev_w` rather than +0 B 4 here + 3 control
|
L6 the lead measured against `prev_w` rather than +0 B 6 here + 3 control
|
||||||
the command it emits
|
the command it emits
|
||||||
|
|
||||||
⚠️ L4, L5 AND L6 ARE ALL ZERO- OR NEGATIVE-BYTE EDITS THAT CHANGE NO PAYLOAD
|
⚠️ L4, L5 AND L6 ARE ALL ZERO- OR NEGATIVE-BYTE EDITS THAT CHANGE NO PAYLOAD
|
||||||
AND NO OPTION, AND L4 WAS BIT-IDENTICAL ON THE SHIPPED CONFIGURATION - 0 of
|
AND NO OPTION, AND L4 WAS BIT-IDENTICAL ON THE SHIPPED CONFIGURATION - 0 of
|
||||||
256 246 ticks over the 16-day recording. It is separable on 2050 of 257 400
|
256 246 ticks over the 16-day recording. It is separable only where
|
||||||
swept states, all at `integrator_max_w` above `max_w`, which is the stimulus
|
`integrator_max_w` is above `max_w`, which is the stimulus part 4 drives; the
|
||||||
part 4 drives.
|
COUNT of separable states is a property of whatever grid was swept and three
|
||||||
|
sweeps disagree about it, so DECISIONS/SAFETY-48 §4 carries all three rather
|
||||||
|
than one.
|
||||||
|
|
||||||
|
⚠️ AND L5 IS TWO MUTATIONS WEARING ONE NAME, WHICH IS WHY ITS COUNT NEEDS A
|
||||||
|
SPELLING BESIDE IT. "min/max swapped" can mean the FUNCTION names exchanged
|
||||||
|
(`max(i_w, slewed + cap)` / `min(i_w, slewed - cap)`) - 5 here + 5 in
|
||||||
|
test_control.py, one of the five being SAFETY-13's own "frozen integrator never
|
||||||
|
deadlocks" sweep over 13 041 states - or the whole BRANCH bodies exchanged
|
||||||
|
(`max(i_w, slewed - cap)` / `min(i_w, slewed + cap)`), which is 5 + 4 and does
|
||||||
|
NOT reach the deadlock sweep. Both are red, both are the cap on the wrong side,
|
||||||
|
and a table that writes one number for the pair is reporting a mutation nobody
|
||||||
|
can re-run. The first spelling is the one the row above quotes.
|
||||||
|
|
||||||
⚠️ MUTATING THE MODULE THIS FILE GRADES? PURGE `__pycache__` FIRST - or run
|
⚠️ MUTATING THE MODULE THIS FILE GRADES? PURGE `__pycache__` FIRST - or run
|
||||||
`tools/mutation_run.sh`, which does. `python3 -B` alone does NOT save you: it
|
`tools/mutation_run.sh`, which does. `python3 -B` alone does NOT save you: it
|
||||||
|
|||||||
@@ -220,6 +220,7 @@ def slew_table(ps, ct, tick_s, rail):
|
|||||||
f"faster than the")
|
f"faster than the")
|
||||||
print(f" plant settles. {rail:.0f} W / ({ct.PLANT_SETTLE_S:.0f} s / "
|
print(f" plant settles. {rail:.0f} W / ({ct.PLANT_SETTLE_S:.0f} s / "
|
||||||
f"{tick_s:.0f} s per tick) = {frac * rail:.0f} W per tick.\n")
|
f"{tick_s:.0f} s per tick) = {frac * rail:.0f} W per tick.\n")
|
||||||
|
overs = []
|
||||||
for slew in (1000, 800, 700, 600, int(frac * rail), 500, 400, 300, 250):
|
for slew in (1000, 800, 700, 600, int(frac * rail), 500, 400, 300, 250):
|
||||||
w = family(ps, ct, gain=0.6, slew_w=float(slew), rail_w=rail,
|
w = family(ps, ct, gain=0.6, slew_w=float(slew), rail_w=rail,
|
||||||
tick_s=tick_s)
|
tick_s=tick_s)
|
||||||
@@ -227,13 +228,52 @@ def slew_table(ps, ct, tick_s, rail):
|
|||||||
" <- the 2026-09-05 live pair" if slew == 1000 else (
|
" <- the 2026-09-05 live pair" if slew == 1000 else (
|
||||||
" <- the live interim" if slew == 400 else ""))
|
" <- the live interim" if slew == 400 else ""))
|
||||||
row(f"slew_w {slew}{tag}", w)
|
row(f"slew_w {slew}{tag}", w)
|
||||||
|
overs.append(w["over"])
|
||||||
print("\n READ IT THIS WAY. The >= 900 W step - the live symptom, which")
|
print("\n READ IT THIS WAY. The >= 900 W step - the live symptom, which")
|
||||||
print(" escalated 2 -> 1 -> 13 -> 43 per 30 min that morning - is gone from")
|
print(" escalated 2 -> 1 -> 13 -> 43 per 30 min that morning - is gone from")
|
||||||
print(" every run below 900 W of slew, and that is what the slew owns.")
|
print(" every run below 900 W of slew, and that is what the slew owns.")
|
||||||
print(" The OVER-ABSORPTION does not move, and at the slowest slews it is")
|
|
||||||
print(" slightly WORSE. The rig cannot pick the derived value out of that")
|
# ⚠️ THE NARRATION BELOW IS DERIVED FROM THE COLUMN JUST PRINTED, NOT
|
||||||
print(" column, and it is not asked to: the CONSTRAINT picks it, and this")
|
# WRITTEN ABOUT IT. SAFETY-39 wrote "the over-absorption does not move, and
|
||||||
print(" table is what says what the constraint does and does not buy.")
|
# at the slowest slews it is slightly WORSE" - true of release/1.0's law,
|
||||||
|
# which is what it was measured against, and FALSE under SAFETY-48's lead
|
||||||
|
# cap, where the same script on the same plant prints a monotonically
|
||||||
|
# FALLING column. A transcript whose prose contradicts its own table is a
|
||||||
|
# failing check (MUTATION.md habit 8), and a sentence that is right under
|
||||||
|
# one law and wrong under another has to be computed rather than typed. So:
|
||||||
|
# which law is imported is read off `control.py` itself, and whether the
|
||||||
|
# column falls is read off the column.
|
||||||
|
cap = getattr(ct, "SLEW_LEAD_STEPS", None)
|
||||||
|
falling = all(b < a for a, b in zip(overs, overs[1:]))
|
||||||
|
law = ("release/1.0's law - the integrator's lead over the command is "
|
||||||
|
"UNBOUNDED" if cap is None else
|
||||||
|
f"SAFETY-48's law - the integrator may lead the command by "
|
||||||
|
f"{cap} slew step(s)")
|
||||||
|
print(f"\n THE LAW THIS RAN AGAINST: {law}.")
|
||||||
|
if cap is None and not falling:
|
||||||
|
print(" The OVER-ABSORPTION does not move with the slew, and at the")
|
||||||
|
print(" slowest slews it is slightly WORSE - `slew_w` bounds the")
|
||||||
|
print(" OUTPUT while the integrator goes on winding, so a slower slew")
|
||||||
|
print(" leaves it winding for longer. SAFETY-48 bounds that lead and")
|
||||||
|
print(" INVERTS this column; see DECISIONS/SAFETY-48 §6.")
|
||||||
|
elif cap is not None and falling:
|
||||||
|
print(" The OVER-ABSORPTION FALLS MONOTONICALLY with the slew, which")
|
||||||
|
print(" is the OPPOSITE of what SAFETY-39 measured on release/1.0")
|
||||||
|
print(" (958/1107/1133/1058/987/1029/1150/1143/1081 W, flat and")
|
||||||
|
print(" non-monotone). Bounding the integrator's lead is what changed")
|
||||||
|
print(" it: the slew now owns a share of the over-absorption it did")
|
||||||
|
print(" not own before. DECISIONS/SAFETY-48 §6 has both tables and an")
|
||||||
|
print(" independent confirmation on test_control.py's own plant.")
|
||||||
|
else:
|
||||||
|
print(" ⚠️ NEITHER NARRATION FITS. The law above and the shape of the")
|
||||||
|
print(f" column ({'falling' if falling else 'not falling'}) are the")
|
||||||
|
print(" combination this script has no sentence for - which means the")
|
||||||
|
print(" measurement has moved and DECISIONS/SAFETY-39 §3 and")
|
||||||
|
print(" DECISIONS/SAFETY-48 §6 both need re-reading before this table")
|
||||||
|
print(" is quoted anywhere.")
|
||||||
|
print("\n EITHER WAY THE RIG DOES NOT PICK THE NUMBER, and it is not asked")
|
||||||
|
print(" to: the CONSTRAINT picks it, and this table is what says what the")
|
||||||
|
print(" constraint does and does not buy.")
|
||||||
|
|
||||||
|
|
||||||
def gain_table(ps, ct, tick_s, rail):
|
def gain_table(ps, ct, tick_s, rail):
|
||||||
|
|||||||
Reference in New Issue
Block a user