OPT-09: the weights DO move on a wedged unit, and that narrows SAFETY-42
checks / unit (pull_request) Successful in 6m11s

B1, the blocking review finding. §5 of the decision said:

    "A wedged unit reads a healthy SoC, so no envelope closes and no weight
     moves - OPT-09 cannot see it at all."

The first clause is true and is about the ENVELOPE. The second clause is
FALSE, and it was load-bearing for the claim that OPT-09 and SAFETY-42 meet
without overlapping. A weight is a function of a pack's ENERGY HEADROOM, not
of what the pack delivers, so a wedged unit's weight moves whenever its
headroom differs from its sibling's - the ordinary case this card exists to
exploit. And delivery.delinquent() (app/delivery.py:484, threshold at :531)
names a unit only while its short_by() gap exceeds min_w = Tuning.saturation_w
= 500 W in ABSOLUTE WATTS. Shrinking the share shrinks the gap below the
threshold, and the alarm stops firing on a unit still delivering nothing.

So the seam, stated accurately: OPT-09 stops the ask AND narrows SAFETY-42's
detection window for a low-headroom unit. That is a DETECTION REGRESSION at
N=2. It is disclosed here, not fixed: making delinquent() relative to the ask
rather than absolute is SAFETY-42's code and a design change, filed as its own
card.

1. THE CORRECTION, in the three places that carried the claim

   * DECISIONS/OPT-09-soc-aware-weights.md §5 - rewritten. States the seam
     accurately, quotes the sentence it is replacing, carries the measured
     rows and the window arithmetic, and names the residual in the same voice
     as SAFETY-13's "2 of 272" and SAFETY-42's "the discriminator does not
     exist". Also drops "they meet and do not overlap" from the opening.
   * app/packshare.py - a new section in the module docstring, immediately
     after "weights are a comfort choice for stability and not for delivery",
     which is where the over-read starts.
   * CHANGELOG.md - a new paragraph in the SAFETY-42 entry, ending on the
     operator-facing sentence: a wedged inverter with less energy headroom
     than its sibling can now go unreported at ordinary house loads.

   test_packshare.py part 8's closing check and its "what this file cannot
   see" list repeated the same false clause; both now say the ENVELOPE cannot
   reach a wedged unit and point at part 8b for what the WEIGHTS do to it.

2. THE GRADED CHECK - test_packshare.py part 8b, 6 named checks

   The reviewer's probe, reproduced: real compute(), real plan(), real
   packshare.weights(), real delivery.account(), real delivery.delinquent();
   the only model is the plant (A obeys within one cycle inside its rating,
   B accepts its share and delivers 0 W). Unit B is WEDGED at 15 % against
   its own 10 % floor, so envelope() does not close and account() explains
   nothing - a SAFETY-42 true positive by that card's own definition, which
   a PRECONDITION check asserts before anything is concluded from it.

       load 1500 W, B wedged at 15 %, equal weights  named on 199/200 cycles,
         site target +3000 W, shares a+1500, b+1500, grid +0 W, named=[b]
       load 1500 W, B wedged at 15 %, OPT-09 weights named on   0/200 cycles,
         site target +1630 W, shares a+1505, b+125,  grid -5 W, named=(nobody)
       load 6000 W, B wedged at 15 %, equal weights  named on 199/200 cycles
       load 6000 W, B wedged at 15 %, OPT-09 weights named on 195/200 cycles

       suppression window: equal weights, site target < 1000 W; OPT-09
       weights, site target < 6500 W of a 9600 W site

   THE CHECKS ASSERT THE CURRENT, UNFLATTERING BEHAVIOUR ON PURPOSE - that
   the weighted arm names NOBODY where the equal arm names the wedged unit.
   A check asserting the alarm still fires would fail, and papering over it
   would be worse than the original defect. The 6000 W row is there so the
   part is not pinning "the weighted arm never names anybody": past the
   healthy pack's own rating, _fill()'s last-resort pass hands the wedged
   unit 3000 W and both arms name it. The window is real and it has an edge.

   MUTATION - packshare.weights() keeps its shape and forces every weight to
   1.0 (M11 in part 11's table, which now has 11 rows). RUN on a purged tree
   with python3 -B, not reasoned about:

       load 1500 W, B wedged at 15 %, equal weights  named on 199/200 cycles,
         site target +3000 W, shares a+1500, b+1500, grid +0 W, named=[b]
       load 1500 W, B wedged at 15 %, OPT-09 weights named on 199/200 cycles,
         site target +3000 W, shares a+1500, b+1500, grid +0 W, named=[b]
       load 6000 W, B wedged at 15 %, OPT-09 weights named on 199/200 cycles,
         site target +4000 W, shares a+2000, b+2000, grid +4000 W, named=[b]
       suppression window: equal weights < 1000 W; OPT-09 weights < 1000 W

     FAIL  AND WITH OPT-09's WEIGHTS IT IS NAMED ON NONE OF THE 200 [...]
     FAIL  ...and the arithmetic behind it, stated as a window [...]
     FAIL  AND IT IS NOT THAT THE WEIGHTED ARM NEVER NAMES ANYBODY [...]

   test_packshare.py exits 1. Restored: exits 0.

3. THREE PROC-01 PROSE DEFECTS

   * DECISIONS/OPT-09-soc-aware-weights.md:3 said "Base: release/1.0 @
     464d398". The branch was rebased; git merge-base HEAD 3368231 is
     3368231. Corrected, and it says why the old number was there.
   * Same file:13 cited main.py:2272 for "resolution = resolve(...)".
     git show 3368231:goodwe_controller/app/main.py | grep -n says 2422.
     Corrected. The sibling citation arbiter.py:535 is right and is left.
   * DOCS.md:174 said "Since 0.7.x the split is driven by two separate
     things", which tells an operator the fix already shipped. config.yaml
     is 0.8.0 and the live install is 0.7.0, where the split is 50/50 - this
     card's own premise. Now "from 0.8.0 onwards", with an explicit warning
     that 0.7.0 and earlier are a fixed 50/50.

NOT TOUCHED, deliberately: delinquent()'s absolute threshold (SAFETY-42's
code, its own card), and app/main.py:2699's double-logged scope refusal (N4,
filed separately).

INVERTER B DOES NOT EXIST (expected ~2026-09-12). Every two-unit figure above
is TWO COPIES OF A through a plant modelled in test_packshare.py; B's 4600 W /
20.48 kWh / 10 % floor are from the datasheet and the serial. Nothing here was
measured on hardware, and the added disclaimers say so in each place.

Verified with the real harness per .gitea/workflows/checks.yml
(goodwe-addon-checks:1): COLLECTED 24 files, ALLGREEN. Harness positive
control run first - test_validate.py deliberately broken, harness reported
"FAILED: test_validate.py" and exited 1; restored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
This commit is contained in:
glenn schrooyen
2026-09-06 22:55:20 +02:00
co-authored by Claude Opus 5
parent f8e501393f
commit 8d83ef0dfb
5 changed files with 303 additions and 19 deletions
+29
View File
@@ -267,6 +267,35 @@ the conclusion is unchanged and is now stated on the predicate's own inputs.
hazard from opposite sides: **OPT-09 stops the site ASKING a pack whose SoC says
it cannot deliver; SAFETY-42 alarms when a pack that said it could does not.**
⚠️ **AND THIS CARD NARROWS SAFETY-42's DETECTION WINDOW. IT IS A DETECTION
REGRESSION AT TWO INVERTERS AND IT IS DISCLOSED RATHER THAN FIXED.** An earlier
draft of this entry and of `DECISIONS/OPT-09-soc-aware-weights.md` §5 said a
wedged unit's weight does not move, so OPT-09 could not see the case at all. The
ENVELOPE half is true - a wedged box reads a healthy SoC and nothing closes. The
WEIGHT half was **false**: a weight is a function of a pack's ENERGY HEADROOM,
not of what the pack delivers, so a wedged unit's weight moves whenever its
headroom differs from its sibling's. And `delivery.delinquent()` names a unit
only while its shortfall exceeds `saturation_w` - **500 W in absolute watts** -
so shrinking the share shrinks the gap below the threshold and the alarm stops
firing. Measured end to end with the real loop, the real allocator and the real
detector, on a unit wedged at a healthy 15 % against its own 10 % floor (a
SAFETY-42 true positive by that card's own definition): at a 1500 W load the
healthy pack can carry alone, equal weights name it on **199 of 200** cycles and
these weights name it on **none of the 200** - shares 1505/125, gap 125 W, and
125 W is under the threshold. The suppression window widens from a site target
below 1000 W to a site target below `saturation_w * (w_A + w_B) / w_B`, which on
that fixture is **6500 W of a 9600 W site**; above it the healthy pack hits its
own rating, the allocator's last-resort pass hands the wedged unit a big share
again and the alarm returns. So in the quiet regime - where `unmet_w` is 0, the
meter is satisfied and no action-log line fires - **a wedged inverter with less
energy headroom than its sibling can now go unreported at ordinary house
loads.** `test_packshare.py` part 8b grades it and pins the CURRENT behaviour, so
the day it changes a named check moves. Closing it means making `delinquent()`
relative to the ask rather than absolute, which is SAFETY-42's code and a design
change; that is filed as its own card and is deliberately not done here.
⚠️ Every two-unit figure above is **two copies of inverter A** through a modelled
plant - inverter B does not exist yet - and B's ratings are datasheet and serial.
⚠️ **AN INVERTER THAT STOPS DELIVERING ITS SHARE IS NOW REPORTED BY NAME
([[SAFETY-42]]) - AND THIS ADDS AN OPTION, SO IT IS A SCHEMA CHANGE.**
`delivery_hold_s: 185` joins `options:` and `schema:` (`int(30,900)`). The
@@ -1,6 +1,8 @@
# OPT-09 - the weights parameter, and what it is honestly for
**Status:** implemented. **Branch:** `OPT-09`. **Base:** `release/1.0` @ `464d398`.
**Status:** implemented. **Branch:** `OPT-09`. **Base:** `release/1.0` @ `3368231`
(`git merge-base HEAD 3368231`; the branch was rebased and this line used to name
the pre-rebase base, `464d398`).
## 0. What was actually wrong
@@ -9,7 +11,7 @@ consumes it in two passes, and **nothing had ever passed one**. Confirmed on the
base commit:
arbiter.py:535 def resolve(claims, *, units=None, weights=None, live=None, ...)
main.py:2272 resolve(claims, units=self.units, live=self.live_units(), ...)
main.py:2422 resolve(claims, units=self.units, live=self.live_units(), ...)
So `weights` was None on every tick of every install, `plan()` turned that into
1.0 per unit, and the site target was split **50/50 regardless of what either
@@ -147,16 +149,67 @@ deferred option, and the BMS is what stops it.
## 5. SAFETY-42 - the same hazard from the opposite side
They meet and do not overlap, and the seam is **the state of charge**:
The seam is **the state of charge** - and they overlap in one direction, which
is what the rest of this section is about:
* **OPT-09 stops the site ASKING** a pack whose own SoC says it cannot deliver.
* **SAFETY-42 alarms** when a pack that said it could does not.
A wedged unit reads a healthy SoC, so no envelope closes and no weight moves -
OPT-09 cannot see it at all. A pack at its floor is `account()`'s explained case,
so SAFETY-42 stays silent - and after this card that ask does not arrive in the
first place, which removes the benign arm from the detector's input rather than
teaching it a new discriminator.
A wedged unit reads a healthy SoC, so **no envelope closes** - that half is
absolute, and it is why this card cannot stop the ask on a wedged box. A pack at
its floor is `account()`'s explained case, so SAFETY-42 stays silent - and after
this card that ask does not arrive in the first place, which removes the benign
arm from the detector's input rather than teaching it a new discriminator.
⚠️ **BUT THE WEIGHT DOES MOVE, AND IT NARROWS SAFETY-42's DETECTION WINDOW. THIS
IS A DETECTION REGRESSION AT N=2 AND IT IS NOT FIXED HERE.** An earlier draft of
this section said "no envelope closes and no weight moves - OPT-09 cannot see it
at all". The second clause was **false**, and it was load-bearing for the claim
that the two cards do not overlap. A weight is a function of the pack's ENERGY
HEADROOM, not of what the pack delivers, so a wedged unit's weight moves whenever
its headroom differs from its sibling's - which is the ordinary case this card
exists to exploit. And `delivery.delinquent()` (`app/delivery.py:484`)
names a unit only while its `short_by()` gap exceeds `min_w =
Tuning.saturation_w` - the comparison is `app/delivery.py:531`, and it is
**500 W in absolute watts**. Shrinking a unit's share shrinks its gap. Shrink the
share far enough and the gap drops below 500 W while the unit is still delivering
nothing, and the alarm stops firing.
Measured on the closed loop - real `compute()`, real `plan()`, real
`delinquent()`, modelled plant, B wedged at 15 % against its own 10 % floor so
nothing closes and `account()` explains nothing, i.e. a SAFETY-42 **true
positive** by that card's own definition:
load 1500 W equal weights (shipped) named on 199/200 cycles
shares {a:1500, b:1500} grid +0 W named=['inverter-b']
load 1500 W OPT-09 weights named on 0/200 cycles
shares {a:1505, b: 125} grid -5 W named=(nobody)
⚠️ That plant is modelled in `test_packshare.py`, and **inverter B does not
exist** (expected ~2026-09-12): it is two copies of A driven with different
numbers, and B's 4600 W / 20.48 kWh / 10 % floor are from the datasheet and the
serial. The claim is about the arithmetic of the split and the threshold, which
needs no hardware, and not about a GW5000S-BP.
At a load the healthy pack can cover - the regime where nothing else tells the
operator anything, `unmet_w` is 0, the meter is satisfied and no action-log line
fires - a wedged inverter goes from named on essentially every cycle to **never
named**. The suppression window widens from `site target < 2 * min_w` = 1000 W to
`site target < min_w * (w_A + w_B) / w_B`, which on that fixture is **6500 W** of
a 9600 W site; above it - once the healthy pack is at its own rating and
`_fill()`'s last-resort pass hands the wedged unit a big share again - the alarm
returns. `test_packshare.py` part 8b grades all of it and **pins the current,
unflattering behaviour**, so that the day it changes a named check moves.
So the seam, stated accurately: **OPT-09 stops the ask AND narrows SAFETY-42's
detection window for a low-headroom unit.** The two cards do not merely meet from
opposite sides; this one takes something away from the other one. Closing the gap
means making `delinquent()` relative to the ask rather than absolute, which is
SAFETY-42's code and a design change - it is filed as its own card, deliberately
not done here, and this paragraph is its input. Until then this is a **disclosed
residual**, in the same voice as SAFETY-13's "2 of 272" and SAFETY-42's own "the
discriminator does not exist": at two inverters, a wedged unit with materially
less energy headroom than its sibling can go unreported at ordinary house loads.
⚠️ **And OPT-09 supplies the mechanism SAFETY-42 said was missing, without
connecting it.** That module's rejection of re-allocation rests on *"the only
+6 -3
View File
@@ -234,9 +234,12 @@ single-inverter entity back in that field. **Before downgrading, write down the
#### How the site target is divided between the packs (OPT-09)
Two packs do not get half each. Since 0.7.x the split is driven by two separate
things, and it is worth knowing which is which, because only one of them is a
rule:
Two packs do not get half each **from 0.8.0 onwards**. ⚠️ **On 0.7.0 and every
version before it the split is a fixed 50/50 regardless of what either pack can
do** — that is the defect this section describes the fix for, so if the add-on
you are running reports 0.7.0, none of what follows is happening yet. From 0.8.0
the split is driven by two separate things, and it is worth knowing which is
which, because only one of them is a rule:
* **The envelope — a permission.** A pack whose own state of charge is at or
under its own `bms_soc_floor` is **not asked to discharge**; a pack at 100 %
+31
View File
@@ -57,6 +57,37 @@ deliver DOES NOT SHIFT TO ITS SIBLING by any mechanism in this add-on.
never rises. That module DETECTS it; this one cannot fix it, and saying so
is worth more than a weighting scheme that looks like it might.
⚠️ AND IT IS WORSE THAN "CANNOT FIX IT": THESE WEIGHTS NARROW SAFETY-42's
DETECTION WINDOW, WHICH IS A DETECTION REGRESSION AT N=2
=======================================================================
This module used to say - and `DECISIONS/OPT-09-soc-aware-weights.md` §5 said
with it - that a wedged unit's weight does not move, so OPT-09 cannot see the
case at all. THE ENVELOPE HALF IS TRUE: a wedged box reads a healthy SoC and
nothing here closes. THE WEIGHT HALF WAS FALSE. `weights()` is a function of a
pack's ENERGY HEADROOM, not of what the pack delivers, so a wedged unit's weight
moves whenever its headroom differs from its sibling's - the ordinary case this
module exists to exploit.
`delivery.delinquent()` names a unit only while its `short_by()` gap exceeds
`min_w` (`Tuning.saturation_w`, 500 W) in ABSOLUTE WATTS. Shrinking a unit's
share shrinks its gap. Measured end to end (`test_packshare.py` part 8b: real
`compute()`, real `plan()`, real `delinquent()`, B wedged at 15 % against its
own 10 % floor, so `envelope()` does not close and `account()` explains
nothing): at a 1500 W load the healthy pack can carry alone, equal weights name
the wedged unit on 199 of 200 cycles and these weights name it on NONE. The
suppression window widens from `site target < 2 * min_w` = 1000 W to
`site target < min_w * (w_A + w_B) / w_B` = 6500 W on that fixture - and it has
an upper edge: past the healthy pack's own rating, `_fill()`'s last-resort pass
hands the wedged unit a big share again and the alarm returns.
⚠️ SO THE SEAM IS NOT "OPT-09 CANNOT SEE IT". IT IS: OPT-09 STOPS THE ASK
**AND** NARROWS SAFETY-42's DETECTION WINDOW FOR A LOW-HEADROOM UNIT. That
is a disclosed residual, not a fixed thing. Closing it means making
`delinquent()` relative to the ask rather than absolute - SAFETY-42's code
and a design change, filed as its own card and deliberately NOT done here.
Do not "fix" part 8b by making it assert the alarm still fires; it does not,
and a green check that says otherwise would be worse than the gap.
WHY ENERGY AND NOT POWER
========================
The two packs at this site are asymmetric IN OPPOSITE DIRECTIONS. From the
+176 -8
View File
@@ -41,6 +41,7 @@ from app.arbiter import (
ACTUATOR_SITE, CLAIM_SCOPE, P_LOOP, P_SAFETY, SCOPE_UNIT, Claim, resolve,
)
from app.control import Tuning, compute
from app.delivery import UnitDelivery, account, delinquent
from app.peakguard import discharge_permitted
fails = []
@@ -599,14 +600,168 @@ check("...so: WEIGHTS ARE A COMFORT CHOICE FOR STABILITY AND NOT FOR DELIVERY, "
weights={A: 1.0, Bee: 0.0}).shares == {A: 2000, Bee: 1000})
check("...and SAFETY-42 is where the case this card CANNOT reach is handled. A "
"WEDGED unit - one that accepts its share and does not produce it - has a "
"healthy state of charge, so no envelope closes and no weight moves; that "
"module detects it, latches and notifies, and deliberately does not act. "
"OPT-09 stops the site ASKING a pack whose own SoC says it cannot "
"deliver; SAFETY-42 alarms when a pack that said it could does not. Same "
"hazard, opposite sides, and the seam is the state of charge",
"healthy state of charge, so NO ENVELOPE CLOSES; that module detects it, "
"latches and notifies, and deliberately does not act. OPT-09 stops the "
"site ASKING a pack whose own SoC says it cannot deliver; SAFETY-42 "
"alarms when a pack that said it could does not. ⚠️ THE WEIGHT, HOWEVER, "
"DOES MOVE - this check used to say it did not - and part 8b grades what "
"that costs the alarm",
not packshare.envelope(PE(Bee, 55.0, B_KWH)).binding)
# ==============================================================================
print()
print("8b. ⚠️ AND WHAT THE WEIGHTS DO TO SAFETY-42's DETECTION WINDOW, WHICH IS "
"A REGRESSION AND IS PINNED HERE AS ONE")
# ⚠️ READ THIS BEFORE READING THE CHECKS: THEY ASSERT THE UNFLATTERING ANSWER ON
# PURPOSE. An earlier draft of §5 of the decision said a wedged unit's weight
# does not move, so OPT-09 "cannot see it at all". The first clause is about the
# ENVELOPE and is true; the second is FALSE. A wedged unit's weight moves
# whenever its energy headroom differs from its sibling's - it is a pack fact,
# not a delivery fact - and `delivery.delinquent()` names a unit only while its
# `short_by()` gap exceeds `min_w` (`Tuning.saturation_w`, 500 W) IN ABSOLUTE
# WATTS. Shrinking a unit's share shrinks its gap. Shrink it below 500 W and the
# alarm stops firing on a unit that is still delivering nothing.
#
# So the seam is not "OPT-09 cannot see it". It is: OPT-09 stops the ask AND
# narrows SAFETY-42's detection window on the same unit. At equal weights the
# window is `site target < 2 * min_w` = 1000 W; at OPT-09's weights it is
# `site target < min_w * (w_A + w_B) / w_B`, which on this fixture is 6500 W.
#
# ⚠️ THIS IS A DETECTION REGRESSION AT N=2 AND IT IS NOT FIXED HERE. Making
# `delinquent()` relative to the ask rather than absolute is SAFETY-42's code and
# a design change; it is filed as its own card. What this part does is GRADE THE
# CURRENT BEHAVIOUR so that the day it changes, a named check moves.
#
# ⚠️ AND THE FIXTURE IS A SAFETY-42 TRUE POSITIVE BY THAT CARD'S OWN DEFINITION,
# checked below before anything is concluded from it: B is WEDGED - it accepts
# its share and produces nothing - at a HEALTHY 15 % against its own 10 % floor,
# so `envelope()` does not close and `account()` explains nothing. This is
# exactly the case §5 says the envelope cannot reach, which is why it is the
# case where the weights' side effect matters.
#
# ⚠️ TWO COPIES OF A. Inverter B does not exist; the plant below is modelled in
# this file the way part 8's is, and B's ratings are datasheet.
WEDGED_A_SOC, WEDGED_B_SOC = 90.0, 15.0 # both healthy, both above their floor
MIN_W = Tuning().saturation_w # what `delinquent()` is handed
def wedged_loop(load_w, weighted, cycles=200):
"""The closed loop with unit B WEDGED at a healthy SoC. Real everything.
Real `compute()`, real `plan()`, real `packshare.weights()`, real
`delivery.account()` and real `delivery.delinquent()`. The only model is the
plant: A obeys within one cycle inside its rating, B accepts whatever it is
told and delivers 0 W - which is the wedged box, and is what makes this a
SAFETY-42 case rather than an OPT-09 one.
`weighted=False` is equal weights - what shipped. Envelopes are left OPEN in
both arms deliberately: neither pack's SoC closes one here, so filing them
would change nothing and would blur which half of the card is being graded.
Returns `(cycles on which delinquent() named somebody, last cycle)`.
"""
packs = [PE(A, WEDGED_A_SOC, A_KWH), PE(Bee, WEDGED_B_SOC, B_KWH)]
tuning = Tuning(gain=1.0, slew_w=1000.0, max_w=A_W + B_W)
prev, i_w, sat = 0.0, None, 0
power = [0.0, 0.0]
grid = load_w
named, last = 0, None
for _c in range(cycles):
d = compute(prev, grid, sum(power), tuning, sat, i_w)
prev, i_w, sat = d.target_w, d.i_w, d.sat_count
w = packshare.weights(packs, d.target_w) if weighted else None
r = plan(d.target_w, {A: _B(-A_W, A_W), Bee: _B(-B_W, B_W)}, weights=w)
assert r.ok, r.refused
# THE MODELLED PLANT: A within one cycle inside its rating; B WEDGED.
power = [max(-A_W, min(A_W, float(r.shares[A]))), 0.0]
grid = load_w - sum(power)
who = delinquent(
[UnitDelivery(p.name, r.shares[p.name], m,
account(live=True, parked=False, soc=p.soc,
bms_floor=p.bms_floor))
for p, m in zip(packs, power)], MIN_W)
named += bool(who)
last = (d.target_w, dict(r.shares), grid, who)
return named, last
_WEDGED_PACK = PE(Bee, WEDGED_B_SOC, B_KWH)
check("PRECONDITION: the wedged unit is a SAFETY-42 TRUE POSITIVE and NOT an "
"OPT-09 case - at 15 % against its own 10 % floor no envelope closes and "
"`account()` explains nothing, so anything the weights do to the alarm "
"here is done to a fault the operator is entitled to be told about",
not packshare.envelope(_WEDGED_PACK).binding
and account(live=True, parked=False, soc=WEDGED_B_SOC,
bms_floor=FLOOR) is None)
_wedge = {}
for _load in (1500.0, 6000.0):
for _weighted in (False, True):
_n, _t = wedged_loop(_load, _weighted)
_wedge[(_load, _weighted)] = (_n, _t)
print(f" load {_load:.0f} W, B wedged at 15 %, "
f"{'OPT-09 weights' if _weighted else 'equal weights ':<14} "
f"named on {_n:3d}/200 cycles, site target {_t[0]:+.0f} W, shares "
+ ", ".join(f"{u}{v:+d}" for u, v in sorted(_t[1].items()))
+ f", grid {_t[2]:+.0f} W, named="
+ (str(sorted(_t[3])) if _t[3] else "(nobody)"))
check("AT EQUAL WEIGHTS - WHAT SHIPPED - the wedged unit is named on every "
"cycle but the first, at a load its sibling can carry on its own. The "
"loop winds the site target to twice the load, the 50/50 split stands "
"+1500 W on a box producing nothing, and 1500 W > 500 W is what "
"`delinquent()` reads",
_wedge[(1500.0, False)][0] == 199
and sorted(_wedge[(1500.0, False)][1][3]) == [Bee]
and _wedge[(1500.0, False)][1][1] == {A: 1500, Bee: 1500})
check("⚠️ AND WITH OPT-09's WEIGHTS IT IS NAMED ON NONE OF THE 200. Same wedged "
"box, same healthy SoC, same load, same detector: the split moves to "
"1505/125, the gap on the wedged unit falls to 125 W, and 125 W < 500 W "
"so `delinquent()` returns {}. THIS CHECK PINS A REGRESSION - it asserts "
"what the code DOES, not what anyone wants, and if it ever goes red "
"because the alarm fired, read the card that made it fire before "
"'fixing' this line",
_wedge[(1500.0, True)][0] == 0
and _wedge[(1500.0, True)][1][3] == {}
and _wedge[(1500.0, True)][1][1] == {A: 1505, Bee: 125})
check("...and this is the regime where NOTHING ELSE TELLS THE OPERATOR "
"ANYTHING: the meter is satisfied (grid within 5 W of zero), `unmet_w` is "
"0 because both units accepted their shares, no envelope closed and no "
"action-log line fires. The site is quietly running one inverter and the "
"one instrument built to say so has gone quiet with it",
abs(_wedge[(1500.0, True)][1][2]) <= 5.0
and abs(_wedge[(1500.0, False)][1][2]) <= 5.0)
_w = packshare.weights([PE(A, WEDGED_A_SOC, A_KWH), _WEDGED_PACK], 1000.0)
_window_equal = MIN_W * 2.0
_window_opt09 = MIN_W * sum(_w.values()) / _w[Bee]
print(f" suppression window on this fixture: equal weights, site target "
f"< {_window_equal:.0f} W; OPT-09 weights, site target "
f"< {_window_opt09:.0f} W of a {A_W + B_W:.0f} W site")
check("...and the arithmetic behind it, stated as a window rather than as one "
"fixture: `delinquent()` reads an ABSOLUTE gap, so a unit weighted to a "
"fraction f of the site target is invisible while `f * target < min_w`. "
"Equal weights suppress below 1000 W; these weights suppress below "
"6500 W of a 9600 W site. That is the regression, and it is a property "
"of the threshold being absolute rather than of any number chosen here",
abs(_window_equal - 1000.0) < 1.0 and abs(_window_opt09 - 6500.0) < 1.0)
check("⚠️ AND IT IS NOT THAT THE WEIGHTED ARM NEVER NAMES ANYBODY - which is "
"what a check that only ever ran the quiet load would be pinning. Push "
"the load past the healthy pack's OWN RATING and `_fill()`'s last-resort "
"pass hands the wedged unit 3000 W anyway, the gap goes back over 500 W, "
"and BOTH arms name it. The window is real and it has an upper edge; the "
"hole is the quiet regime below that edge",
_wedge[(6000.0, True)][0] == 195
and sorted(_wedge[(6000.0, True)][1][3]) == [Bee]
and _wedge[(6000.0, False)][0] == 199
and sorted(_wedge[(6000.0, False)][1][3]) == [Bee])
# ==============================================================================
print()
print("9. PER-PACK ARBITRAGE SIZING")
@@ -814,8 +969,12 @@ print(" policy; `arbitrage_soc_ceiling` is a trade preference and using")
print(" it here would be SAFETY-25's deferred option in the charge")
print(" direction. Everything between is left to the weights, which are")
print(" a preference and are honest about being one.")
print(" * A WEDGED UNIT IS NOT REACHABLE FROM HERE AT ALL. Its SoC reads")
print(" healthy, so no envelope closes. SAFETY-42 owns it.")
print(" * A WEDGED UNIT IS NOT REACHABLE BY THE ENVELOPE. Its SoC reads")
print(" healthy, so nothing closes, and SAFETY-42 owns the detection.")
print(" ⚠️ BUT IT IS REACHABLE BY THE WEIGHTS, WHICH MOVE ON ENERGY")
print(" HEADROOM AND NOT ON DELIVERY - part 8b grades what that does")
print(" to SAFETY-42's window, and the answer is a REGRESSION that is")
print(" pinned rather than fixed.")
# ==============================================================================
print()
@@ -856,6 +1015,15 @@ MUT = [
"test_packshare 1's PRECONDITION. ⚠️ IT WAS A TRACEBACK BEFORE THAT LINE "
"EXISTED - the first draft of this file had no precondition and the "
"mutation took it down with a TypeError three lines into part 1"),
("M11 weights(): keep the shape, force every weight to 1.0 <- THE PROOF "
"THAT PART 8b CAN GO RED",
"test_packshare 8b, three named checks: the wedged unit goes back to being "
"named on 199/200 at 1500 W, the split reverts to 1500/1500 and the "
"window collapses from 6500 W to 1000 W. ⚠️ RUN, NOT REASONED ABOUT - a "
"check that pins an UNFLATTERING behaviour has to be shown capable of "
"failing, or it is indistinguishable from a check that pins nothing. "
"(It also takes parts 1, 3, 7 and 8 down, which is M3's territory; 8b is "
"the part that names the SAFETY-42 interaction)"),
]
for _n, _w in MUT:
print(f" caught {_n}")
@@ -866,7 +1034,7 @@ check("every mutation in the table above was RUN and every one of them is "
"invisible to every behavioural fixture in this repository, because they "
"all build their own claim list and call `resolve()` themselves. That is "
"why the call site is pinned statically as well",
len(MUT) == 10)
len(MUT) == 11)
print()
if fails: