OPT-09: the weights parameter nothing ever passed, and what it cannot do
checks / unit (pull_request) Successful in 6m8s

`allocator.plan()` has taken a `weights` argument since ALLOC-01 and NOTHING
had ever passed one - main.py called `resolve()` without it and the arbiter
defaulted it to 1.0 per unit - so the site target was split 50/50 regardless
of what either pack could do. Two things now feed it, and they are different
kinds of thing:

  * a `pack` UNIT claim at P_SAFETY that CLOSES a unit's envelope at its own
    `bms_soc_floor` or at 100 % - a PERMISSION, obeyed absolutely, so an empty
    pack is not asked to discharge and its share goes to the sibling;
  * SoC-aware weights by ENERGY headroom in kWh - a PREFERENCE, which
    `_fill()`'s last-resort pass will override the moment the sibling saturates.

Measured (test_packshare.py part 8): at a load one pack cannot cover, the
weighted arm and the shipped arm command the IDENTICAL watts into an empty pack
and deliver the IDENTICAL watts. Only the envelope stops the ask, and what it
buys is not delivery - it is a reported `unmet_w` where the record used to read
impeccable. Weights are a comfort choice for STABILITY and NOT for DELIVERY.

Also: an optional per-entry `capacity_kwh` (schema change, all-or-nothing across
the roster, rule 9 refuses a partial one); per-pack arbitrage sizing, with the
sell side's no-op stated rather than hidden; and a 0 W trade refused by name,
because a 0 W arbitrage claim is a park order that outranks the loop.

SAFETY-42 and this card meet at one 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 its end-to-end fixture moved as a result.
Its finding is unchanged and is now stated on the two quantities its rejected
discriminator actually reads.

Not filed at all at a roster of one: at N=1 every source routes to the site, so
the interval would bound the SITE TARGET, and SAFETY-25's decided Option A
stands untouched on the live install.

⚠️ INVERTER B DOES NOT EXIST. Its 4600 W / 20.48 kWh / 10 % are FROM DATASHEET
AND SERIAL, and `sim/plantsim.py`'s INVERTER_B is inverter A copied
(`measured=False`), so every two-inverter figure here is arithmetic or two
copies of A. No hardware was touched.

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 20:36:19 +02:00
co-authored by Claude Opus 5
parent 464d398051
commit 83df37963e
20 changed files with 2313 additions and 81 deletions
+104 -36
View File
@@ -475,13 +475,42 @@ def _power_domain(rows):
_pw, _pf = _power_domain(_sw), _power_domain(_sf)
check(f"THE TWO ARMS ARE BYTE-IDENTICAL IN THE POWER DOMAIN on all "
f"{len(_pw)} cycles - grid, site target, every share, every measured "
f"watt. No threshold on any of those numbers, at any N and any X, can "
f"separate a wedged unit from a pack at its floor, and the card's own "
f"proposed discriminator reads only those numbers",
len(_pw) == len(_pf) and _pw == _pf)
check("...and the ONLY thing that differs is the state of charge",
# ⚠️ OPT-09 CHANGED WHAT THIS PAIR OF ARMS LOOKS LIKE, AND THE CARD'S FINDING
# SURVIVES IT INTACT. UNTIL OPT-09 the two traces were byte-identical in every
# power number - the cold pack was ASKED for the same +2000 W the wedged one was
# asked for, and both produced 0 W - and that identity was this file's headline.
# OPT-09 closes a pack's `hi` at 0 W once its own state of charge is at or under
# its own `bms_soc_floor`, so the cold arm's B is now commanded 0 W and the two
# arms' SHARES differ. ⚠️ THAT IS A CHANGE IN WHAT THE SITE ASKS FOR, NOT IN
# WHAT ANY DETECTOR CAN SEE, and the card's argument is about the latter: the
# proposed discriminator reads SATURATION-PERSISTENCE and |GRID ERROR|, and on
# BOTH arms the loop is saturation-frozen on every cycle while ~1000 W stands at
# the meter. No (N, X) separates them, exactly as measured on the rig - so the
# claim is now stated on the two quantities the predicate actually reads rather
# than on the whole trace, which is the stronger form of the same sentence.
_ERR_W = 900.0
_frozen = [[r["reason"] for r in rows] for rows in (_sw, _sf)]
_settled = [[r for r in rows if abs(r["grid"]) > _ERR_W] for rows in (_sw, _sf)]
print(f" the two quantities the card's predicate reads, last 8 cycles: "
f"wedged grid {[round(r['grid']) for r in _sw[-8:]]}, "
f"cold grid {[round(r['grid']) for r in _sf[-8:]]}")
check(f"THE TWO ARMS ARE INSEPARABLE IN THE TWO QUANTITIES THE CARD'S PROPOSED "
f"DISCRIMINATOR READS. On both, the loop reports the SAME reason on the "
f"last {len(_sw[-8:])} cycles and |grid error| stands over "
f"{_ERR_W:.0f} W on every one of them. No threshold on saturation "
f"persistence and no threshold on grid error, at any N and any X, can "
f"separate a wedged unit from a pack at its floor",
[r["reason"] for r in _sw[-8:]] == [r["reason"] for r in _sf[-8:]]
and len(_settled[0]) >= 8 and len(_settled[1]) >= 8)
check("...and the SHARES now differ, which is OPT-09 and not a weakening of the "
"line above: the cold pack is commanded 0 W because its own SoC closed "
"its own envelope, while the wedged pack - whose SoC is healthy - is "
"still asked for its full share and still delivers nothing. The two "
"cards meet here from opposite sides: OPT-09 stops the ASK, this file "
"grades the ALARM, and neither can do the other's half",
_pw != _pf
and _sf[-1]["shares"][1] == 0.0 and _sw[-1]["shares"][1] == 2000.0)
check("...and the state of charge is still what tells them apart",
[r["soc"] for r in _sw] != [r["soc"] for r in _sf])
print(f" both arms, last cycle: grid {_sw[-1]['grid']:.0f} W, shares "
f"{tuple(round(v) for v in _sw[-1]['shares'])}, measured "
@@ -490,27 +519,48 @@ print(f" SoC: wedged arm {_sw[-1]['soc']} cold arm {_sf[-1]['soc']}")
check("AC 1: the WEDGED unit is NAMED, and only it",
_sw[-1]["named"] == ("inverter-b",))
check("AC 2: the COLD PACK AT ITS FLOOR IS NOT - on the identical trace",
check("AC 2: the COLD PACK AT ITS FLOOR IS NOT",
_sf[-1]["named"] == ())
print(f" {_cw.inverters[1].delivery_wd.reason}")
# ⚠️ THE VACUOUS-PASS GUARD, AND IT IS NOT DECORATION. MUTATION.md's third
# failure mode is a check whose data cannot reach the failure it names: "the
# cold arm named nobody" reads exactly the same against a fixture where nothing
# was short, nothing was asked, or the loop never ran. So the cold arm is
# asserted to have REACHED the branch and been turned away by the account, not
# to have missed it.
# was short, nothing was asked, or the loop never ran.
#
# ⚠️ AND OPT-09 MOVED WHICH OF THOSE IS TRUE, WHICH IS WORTH MORE THAN THE
# CHECK IT REPLACES. Until OPT-09 the cold pack WAS asked for its full share,
# was short of it, and was turned away by `account()` alone - the account was the
# only thing standing between an empty battery and an alarm. It is not any more:
# the pack's own envelope is closed, so it is never asked, `short_by()` answers
# None on a zero share, and AC 2 is delivered STRUCTURALLY - by the site not
# asking - rather than by a discriminator applied after the fact.
#
# BOTH HALVES ARE ASSERTED, because the second is the one that survives OPT-09
# being reverted or a pack whose numbers are absent: `account()` STILL turns this
# pack away when the ask IS present, and that is asked of it directly here on
# this arm's own measured SoC and configured cut-out. Parts 2 and 3 grade the
# predicate over its whole input space; this line pins it to THIS fixture's
# numbers so the end-to-end arm cannot pass for a reason unrelated to the pack.
_cold_b = _cf.inverters[1]
check("...and the cold arm REACHED that decision rather than falling short of "
"it: B was asked for a discharge share, was short of it by more than "
"saturation_w, and was turned away by `account()` alone",
float(_cold_b.standing_w()) > 0
and short_by(_cold_b.standing_w(), _cold_b.batt) > _cf.tuning.saturation_w
and account(live=True, parked=False, soc=_cold_b.soc,
bms_floor=_cold_b.bms_soc_floor) is not None)
check("...and the WEDGED arm's B differs from it in exactly that one place",
check("...and the cold arm reaches AC 2 the way OPT-09 makes it reach it: B's "
"own state of charge closed B's own envelope, so B is commanded 0 W and "
"`short_by()` has no discharge share to be short of. The ask never "
"arrives, which is a better answer than an alarm suppressed after it does",
float(_cold_b.standing_w()) == 0.0
and short_by(_cold_b.standing_w(), _cold_b.batt) is None)
check("...and `account()` STILL turns this pack away if the ask does arrive - "
"asked directly, on this arm's own measured SoC and its own configured "
"cut-out. That is the half that must not depend on OPT-09: an unreadable "
"SoC or an unstated floor is a case OPT-09 deliberately does NOT close on, "
"and the account is what keeps those quiet",
account(live=True, parked=False, soc=_cold_b.soc,
bms_floor=_cold_b.bms_soc_floor) is not None)
check("...and the WEDGED arm's B differs from it in exactly that one place - "
"which is why it is still asked for its share, and still named",
account(live=True, parked=False, soc=_cw.inverters[1].soc,
bms_floor=_cw.inverters[1].bms_soc_floor) is None)
bms_floor=_cw.inverters[1].bms_soc_floor) is None
and float(_cw.inverters[1].standing_w()) > 0)
# ⚠️ WHICH FLOOR, AND THIS IS A WIRING CHECK THAT THE PURE ONES ABOVE CANNOT
# MAKE. `peakguard.pack_floor_pct()` conjoins a pack's own cut-out with the
@@ -694,11 +744,17 @@ check("...and each arm is a config validate.py rule 8 accepts: its "
"peak_soc_floor is above EVERY unit's own cut-out",
all(float(c.o.get("peak_soc_floor")) > float(i.bms_soc_floor)
for c in (_c_deep, _c_shal) for i in c.inverters))
check("...and the two arms are IDENTICAL IN THE POWER DOMAIN on every cycle - "
"grid, site target, every share, every measured watt. The configured "
"cut-out is the only thing that differs, which is part 4's claim made "
"again at two cut-outs that are not the shipped one",
_power_domain(_r_deep) == _power_domain(_r_shal))
check("...and the two arms are INSEPARABLE IN THE QUANTITIES THE CARD'S "
"PROPOSED DISCRIMINATOR READS - the same loop reason and over 900 W of "
"grid error on every one of the last eight cycles - which is part 4's "
"claim made again at two cut-outs that are not the shipped one. ⚠️ THEIR "
"SHARES DIFFER, because OPT-09 closes the DEEP arm's envelope (15 % "
"against a 25 % cut-out) and leaves the SHALLOW arm's open (8 % against "
"5 %); that is the site declining to ask, not a detector telling them "
"apart, and it is why the pin below still needs the pack's OWN number",
[r["reason"] for r in _r_deep[-8:]] == [r["reason"] for r in _r_shal[-8:]]
and all(abs(r["grid"]) > 900.0
for rows in (_r_deep, _r_shal) for r in rows[-8:]))
check(f"a pack at {_DEEP_SOC:.0f} % against its OWN {_FLOOR_DEEP} % cut-out is "
f"cold, and is NOT named - even though it is far above the "
@@ -706,11 +762,14 @@ check(f"a pack at {_DEEP_SOC:.0f} % against its OWN {_FLOOR_DEEP} % cut-out is "
f"against, which would have called an empty pack a fault",
_r_deep[-1]["named"] == () and float(_bd.bms_soc_floor) == _FLOOR_DEEP
and _bd.soc == _DEEP_SOC)
check("...and it REACHED that decision: it was asked for a discharge share, "
"was short of it by more than saturation_w, and was turned away by "
"`account()` alone",
float(_bd.standing_w()) > 0
and short_by(_bd.standing_w(), _bd.batt) > _c_deep.tuning.saturation_w
check("...and it reaches that decision the way OPT-09 makes it reach it - the "
"pack's own 25 % cut-out closed its own envelope, so it is commanded 0 W "
"and has no discharge share to be short of - while `account()`, asked "
"directly on the same two numbers, STILL turns it away. Both halves, for "
"the reason part 4 gives: the second is what survives OPT-09 being "
"reverted, and it is what covers the absences OPT-09 will not close on",
float(_bd.standing_w()) == 0.0
and short_by(_bd.standing_w(), _bd.batt) is None
and account(live=True, parked=False, soc=_bd.soc,
bms_floor=_bd.bms_soc_floor) is not None)
check(f"a pack at {_SHAL_SOC:.0f} % against its OWN {_FLOOR_SHALLOW} % cut-out "
@@ -801,12 +860,21 @@ check("the saturation freeze: FIRES - and that is the point. It is the one "
print(f" saturation counter {_cw.sat_count} (freezes at "
f"{_cw.tuning.saturation_cycles}); site commanded "
f"{_cw.target:.0f} W against {_cw.site_batt_w:.0f} W measured")
check("...and it fires IDENTICALLY on the benign arm, on the identical trace, "
"at the same counter. A saturation-shaped alarm cannot tell these two "
"apart at any threshold and any duration",
_cf.sat_count == _cw.sat_count
and _cf.site_batt_w == _cw.site_batt_w
and _cf.target == _cw.target)
print(f" ...and on the benign arm: counter {_cf.sat_count}, site "
f"commanded {_cf.target:.0f} W against {_cf.site_batt_w:.0f} W measured")
check("...and it fires ON THE BENIGN ARM TOO, from the same site numbers. The "
"site is commanded the same watts against the same measured watts and is "
"frozen on both, many times over the configured cycle count. ⚠️ THE "
"COUNTERS DIFFER BY ONE CYCLE SINCE OPT-09, and that rescues nothing: the "
"cold arm reaches its steady state one cycle sooner because its healthy "
"sibling gets the whole share immediately instead of half of it. A "
"duration threshold that both arms exceed by an order of magnitude "
"cannot be set between them, which is the claim - and the two settled "
"states are identical to the watt",
_cf.site_batt_w == _cw.site_batt_w
and _cf.target == _cw.target
and min(_cf.sat_count, _cw.sat_count) >= _cw.tuning.saturation_cycles
and abs(_cf.sat_count - _cw.sat_count) <= 1)
print()
print(" ...and a unit the ALLOCATOR HAS ALREADY DROPPED is not reported twice")