- REFUSED_SPAN_S: the 12 cycles of evidence must also span (SETTLE_CYCLES-1) * LOOP_TICK_S = 22 s. At a 1 s meter, lagged healthy readings refused both units 10-16 times an hour after load steps (site held 0 W). 2 s and slower ticks are unchanged. - REFUSES_ABOVE_CUT_OUT only for a refusal to give power: a full battery that stops taking charge is always above its cut-out level. - test_delivery: its wedged fixture (0.2 s cycles) no longer reaches a refusal; the release/1.0 checks are restored and a span check added. - test_refusal_units: lagged-reading and full-battery plants, two new checks, U14/U15 in the mutation table. Stale "> 500 W" wording fixed. DOCS/DECISIONS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
105 lines
5.7 KiB
Markdown
105 lines
5.7 KiB
Markdown
# SAFETY-73 - a battery that refuses, with two or more inverters
|
|
|
|
**Decided 2026-09-27 by the owner, on the phase A plan (option R). All four
|
|
new questions answered yes. SAFETY-52's four decisions carry over.**
|
|
|
|
## The defect
|
|
|
|
SAFETY-52 held a refusing battery at 0 W at one inverter only. At N>1
|
|
(`main.py` passed `refused=None`) nothing closed a unit that gives ~0 W while
|
|
its state of charge reads above its configured cut-out, so the allocator kept
|
|
handing it its weighted half. Measured on the real `Controller`, two units,
|
|
house 3000 W, A cut out:
|
|
|
|
| register | law | B gives | grid | A's gate trips / h |
|
|
|---|---|---|---|---|
|
|
| flat -4 W | old | 533 W | +2471 W | 46 (each stops the whole site) |
|
|
| flat -4 W | new | 2000 W (its rating) | +1004 W | 1 |
|
|
| jitter | old | 1910 W | +1094 W | 0 |
|
|
| jitter | new | 2000 W | +1004 W | 0 |
|
|
|
|
House 1200 W, which B alone carries: old +671 W (flat) and +284 W (jitter)
|
|
standing import, new -16 W. Both cut out: old keeps asking two empty batteries
|
|
(+1910 W each on the jitter shape), new holds the site at 0 W.
|
|
|
|
## What was decided
|
|
|
|
1. **A refusing unit is closed in that direction** by its own P_SAFETY unit
|
|
claim, source `refused`. It engages when the unit gives under
|
|
`REFUSED_BELOW_W` (100 W) for `SETTLE_CYCLES` (12) cycles, spanning at least
|
|
`REFUSED_SPAN_S` (22 s), in one direction while asked more than `PROBE_W`
|
|
(200 W), or when its stale-reading gate
|
|
trips on a held reading under 100 W. The allocator then hands its share to
|
|
the others within their own limits (owner decision 1). It is a hard unit
|
|
limit that always intersects, so OPT-11's P_MODE layer fits inside it.
|
|
2. **It binds every strategy's ask on that unit** (decision 2): peak guard,
|
|
arbitrage and maintenance are placed around it like the loop.
|
|
3. **A refusal to give power above the configured cut-out** logs INFO plus one WARNING ending
|
|
in `REFUSES_ABOVE_CUT_OUT` (decision 3). SAFETY-42/43 no longer report such a
|
|
unit: its share is 0 W, and `account()` treats it as parked.
|
|
4. **All units refusing the same way** pass that sign to `compute()`, so the
|
|
site holds 0 W with SAFETY-52's law (decision 4). Probes go one unit at a
|
|
time.
|
|
5. **Probe:** every 15 min for 34 s the claim becomes a pin at +-200 W and the
|
|
others carry the rest. The pin stands only while the resolved site target
|
|
asks at least 200 W its way; otherwise it would make a sibling take the
|
|
opposite direction, so it falls back to the closed limit for that cycle.
|
|
6. **Exits:** a reading of 100 W or more in the held direction (probe or not);
|
|
100 W or more taken the other way for 12 cycles (charge at the floor; one
|
|
cycle of the loop overshooting through zero is not enough); stop or start.
|
|
On every exit the site target and integrator restart from the watts on the
|
|
wire, or the reopened unit is handed a parked remainder at once.
|
|
|
|
**Why 200 W and not `saturation_w`.** A unit's share is often small: a 400 W
|
|
night load split two ways asks 200 W of each. With 500 W the empty battery
|
|
kept a phantom share that SAFETY-43's relative arm then reported as not
|
|
delivering, the ERROR decision 3 replaces. Shares of 200 W or less that give
|
|
under 100 W are "too small to call" in `account()`: the sibling covers the
|
|
meter and nothing is asked of the empty battery that matters.
|
|
|
|
**Why one direction throughout.** On TEST-19's flapping family a command that
|
|
swaps sign each cycle reads ~0 through the plant's lag: 42 engagements in 21
|
|
no-fault runs without the rule, 0 with it.
|
|
|
|
**N=1 is unchanged.** Every SAFETY-73 path is gated on `len(inverters) > 1`.
|
|
`test_refusal_units.py` part 6 runs SAFETY-52's whole suite with every
|
|
SAFETY-73 method made to raise.
|
|
|
|
## Evidence
|
|
|
|
- `test_refusal_units.py`: cases a, b and c on both register shapes against
|
|
the old behaviour, probes and their spacing, one-at-a-time probing, the
|
|
recovery reseed, each exit, the small-ask and small-site-target cases, the
|
|
WARNING, the status row and shadow mode. Mutations U1-U13 in its docstring.
|
|
- **No false engagements.** TEST-19's no-fault campaign (admin/goodwe
|
|
`sim/delivery_window.py` families, 2640 runs at N=2 and 2640 at N=1, every
|
|
unit executing its share) graded with the engage clause: 0 engagements, the
|
|
longest qualifying streak 5 cycles against 12. The grader is
|
|
`goodwe-session/scratch_s73/s73_nofault.py`.
|
|
- **Recovery.** Case a, A recovering into a house that fell below the parked
|
|
target: worst grid +0 W with the reseed. Phase A measured -1059 W without the
|
|
reseed and the site hold.
|
|
|
|
## Review changes
|
|
|
|
- **The evidence must span 22 s, not only 12 cycles.** At a 1 s meter the 12
|
|
cycles were 11 s, shorter than a healthy reading's lag (dead time plus a
|
|
10-17 s publish). The reviewer's harness (real `Controller`, both units with
|
|
6 s dead time and a 13-17 s publish, load steps 300/3000 W every 2 min) then
|
|
refused both healthy units 10-16 times an hour, and each time the site held
|
|
0 W: +50-85 Wh import an hour and a +4.6 kW grid peak against +3.7 kW without
|
|
the refusal. At a 2 s tick there were none. `REFUSED_SPAN_S` is
|
|
`(SETTLE_CYCLES - 1) * LOOP_TICK_S`, so 2 s and slower ticks are unchanged.
|
|
- **The cut-out WARNING is for a refusal to give power only.** A full battery
|
|
that stops taking charge is always above its cut-out level, and the WARNING
|
|
told the installer to check a setting that has nothing to do with it.
|
|
|
|
## Not covered
|
|
|
|
- A unit that gives a steady part of its ask (100 W or more) is a limit, as at
|
|
N=1. SAFETY-42/43 still report it.
|
|
- `pack_permissions` stays SoC-based, so the peak guard may size a shave with a
|
|
refusing battery counted. The shortfall shows as `unmet_w`.
|
|
- A wedged unit (HW-01) reading ~0 W is now shown as a refusal, not reported as
|
|
not delivering. Its share still moves, which is the gain SAFETY-42 measured.
|