OPT-09: the weights parameter nothing ever passed, and what it cannot do
`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:
co-authored by
Claude Opus 5
parent
3d2999337d
commit
f8e501393f
@@ -2576,9 +2576,42 @@ check("...and so does the persistent notification, which outlives the page and "
|
||||
# observe exactly {device, supervised, loop} at every SoC, because the shipped
|
||||
# roster is one inverter and this claim cannot be filed there at all. See
|
||||
# DECISIONS/SAFETY-30-per-unit-sign.md.
|
||||
# ⚠️ OPT-09 ADDED THE ELEVENTH, AND IT IS THE ONE THIS TRIPWIRE WAS BUILT FOR
|
||||
# THE SHAPE OF, SO IT NEEDS THE LONGEST PARAGRAPH. `pack` is a
|
||||
# `Claim.limit(... P_SAFETY ...)` on ONE INVERTER'S actuator, filed only where it
|
||||
# BINDS and only at a roster of TWO OR MORE, carrying that pack's own state of
|
||||
# charge against that pack's own `bms_soc_floor`: `hi = 0` for a battery at or
|
||||
# under its cut-out, `lo = 0` for one at 100 %.
|
||||
#
|
||||
# IT IS CONDITIONED ON A STATE OF CHARGE, WHICH IS EXACTLY WHAT THIS SET WATCHES
|
||||
# FOR, so the argument has to be made rather than asserted. SAFETY-25's deferred
|
||||
# Option B was `Claim.limit("reserve", P_LOOP, hi=0.0)` under a NEW
|
||||
# `loop_soc_floor` option: an EMS PREFERENCE, chosen by an operator, stopping the
|
||||
# grid-following loop at a depth somebody picked, ON THE SITE. Four things
|
||||
# separate this from it, and all four are graded elsewhere in this repository:
|
||||
#
|
||||
# 1. THE NUMBER IS NOT AN OPTION. It is `bms_soc_floor` - measured off
|
||||
# register 0x0560 and required of every roster entry since SAFETY-32. No
|
||||
# new option is added and `peak_soc_floor` is deliberately NOT consulted
|
||||
# (test_packshare.py part 2 grades that a pack at 15 % with a 10 % cut-out
|
||||
# is NOT closed, though a 20 % EMS floor would have closed it).
|
||||
# 2. IT IS A UNIT CLAIM, SO IT CANNOT BOUND THE SITE TARGET. It changes WHICH
|
||||
# INVERTER carries the target, never how big the target is.
|
||||
# 3. IT IS NOT FILED AT ALL AT A ROSTER OF ONE, which is why the sweeps below
|
||||
# still observe exactly {device, supervised, loop} at every state of charge
|
||||
# from 0 to 100 % on the shipped install. Option A stands untouched there:
|
||||
# ordinary self-consumption still discharges to the BMS cut-out.
|
||||
# 4. AND THE MEMO ITSELF NAMES THIS AS THE REVISIT: "under ALLOC-01 the
|
||||
# reserve becomes a PER-INVERTER INTERVAL AT THE ALLOCATOR rather than a
|
||||
# ninth claim here". Stage 1 is how an interval reaches the allocator, and
|
||||
# stage 1's input is claims - so a unit-scoped claim is the shape that
|
||||
# sentence asks for.
|
||||
#
|
||||
# If you are here because you added a TWELFTH, that argument is what this line
|
||||
# wants from you, not an edit.
|
||||
CLAIM_SOURCES = {"device", "supervised", "failsafe", "safety", "peak",
|
||||
"arbitrage", "maintenance", "loop", "circulation",
|
||||
"sign_site"}
|
||||
"sign_site", "pack"}
|
||||
# The two appends that file a claim BUILT ELSEWHERE in the method. Named here so
|
||||
# that a THIRD such indirection reads as an unrecognised append rather than as a
|
||||
# silent gap in the scan.
|
||||
@@ -2619,10 +2652,11 @@ check("every `claims.append(...)` in main.py's cycle is one this scan can READ "
|
||||
"- a claim filed through a shape it cannot parse has to fail here rather "
|
||||
"than be counted as nothing, which is how a scan like this goes quietly "
|
||||
"vacuous", len(_appends) >= 10 and None not in _filed)
|
||||
check("...and the sources that cycle can file are EXACTLY these nine. THIS is "
|
||||
"what makes part 10 a tripwire: replay_loop() hand-builds the claim list, "
|
||||
"so it cannot see a claim main.py adds - and a loop-side SoC reserve is a "
|
||||
"NINTH source. Filing one takes this line red on the line it is written",
|
||||
check("...and the sources that cycle can file are EXACTLY these ELEVEN. THIS "
|
||||
"is what makes part 10 a tripwire: replay_loop() hand-builds the claim "
|
||||
"list, so it cannot see a claim main.py adds - and a loop-side SoC "
|
||||
"reserve is a new source. Filing one takes this line red on the line it "
|
||||
"is written, and OPT-09's `pack` had to argue its way past it above",
|
||||
set(_filed) == CLAIM_SOURCES)
|
||||
|
||||
|
||||
@@ -2749,8 +2783,8 @@ check("...and replay_loop() folding main.py's TWO P_SAFETY rating limits into "
|
||||
# structurally impossible. The structure is `CLAIM_SCOPE`, so `CLAIM_SCOPE` is
|
||||
# pinned to the AST-scanned set rather than trusted to agree with it.
|
||||
check("every source main.py can file is CLASSIFIED site or per-inverter, and "
|
||||
"nothing else is. A TENTH source now goes red twice - once here for "
|
||||
"having no classification, once above for being a tenth - and it takes an "
|
||||
"nothing else is. A TWELFTH source now goes red twice - once here for "
|
||||
"having no classification, once above for being a twelfth - and it takes an "
|
||||
"argument about what the claim is ABOUT to make either line green",
|
||||
set(CLAIM_SCOPE) == CLAIM_SOURCES)
|
||||
print(" site: " + ", ".join(sorted(s for s, v in CLAIM_SCOPE.items()
|
||||
@@ -2758,8 +2792,8 @@ print(" site: " + ", ".join(sorted(s for s, v in CLAIM_SCOPE.items()
|
||||
+ " | per-inverter: "
|
||||
+ ", ".join(sorted(s for s, v in CLAIM_SCOPE.items() if v == SCOPE_UNIT)))
|
||||
check("...and the split is the one ARB-03 argued, with SAFETY-31's and "
|
||||
"SAFETY-30's additions: the six about the GRID CONNECTION are site "
|
||||
"claims and the four about a pack, a device or one write are "
|
||||
"SAFETY-30's and OPT-09's additions: the six about the GRID CONNECTION "
|
||||
"are site claims and the five about a pack, a device or one write are "
|
||||
"per-inverter. `circulation` is a site claim because its quantity is "
|
||||
"defined over the roster and is identically zero for any one unit; "
|
||||
"`sign_site` is one because the number that tripped it is the SITE "
|
||||
@@ -2769,7 +2803,7 @@ check("...and the split is the one ARB-03 argued, with SAFETY-31's and "
|
||||
{s for s, v in CLAIM_SCOPE.items() if v == SCOPE_SITE}
|
||||
== {"loop", "peak", "arbitrage", "safety", "circulation", "sign_site"}
|
||||
and {s for s, v in CLAIM_SCOPE.items() if v == SCOPE_UNIT}
|
||||
== {"device", "supervised", "failsafe", "maintenance"})
|
||||
== {"device", "supervised", "failsafe", "maintenance", "pack"})
|
||||
|
||||
# The shipped roster is ONE inverter, so §6.2's identity holds and every claim a
|
||||
# real cycle files is a site claim. Observed on the claims main.py's own resolve
|
||||
@@ -2853,8 +2887,9 @@ _restore_b()
|
||||
print(" with a second inverter on the roster, main.py routes: "
|
||||
+ ", ".join(f"{s}->{a}" for s, a in sorted(_routed.items())))
|
||||
check("⚠️ AND THIS IS THE BREAKING HALF. Give the same Controller a second "
|
||||
"inverter and its OWN routing method splits the ten: `device`, "
|
||||
"`supervised`, `failsafe` and `maintenance` bind the unit this controller "
|
||||
"inverter and its OWN routing method splits the eleven: `device`, "
|
||||
"`supervised`, `failsafe`, `maintenance` and OPT-09's `pack` bind the "
|
||||
"unit this controller "
|
||||
"drives, `loop`, `peak`, `arbitrage`, `safety`, SAFETY-31's `circulation` "
|
||||
"and SAFETY-30's `sign_site` stay on the grid connection. Swap any one of "
|
||||
"them - route `peak` to the unit and the site keeps no floor; route "
|
||||
@@ -2865,6 +2900,7 @@ check("⚠️ AND THIS IS THE BREAKING HALF. Give the same Controller a second "
|
||||
"rest of the roster goes on pushing the same way - and this line goes red",
|
||||
_routed == {"device": "inverter-a", "supervised": "inverter-a",
|
||||
"failsafe": "inverter-a", "maintenance": "inverter-a",
|
||||
"pack": "inverter-a",
|
||||
"loop": ACTUATOR_SITE, "peak": ACTUATOR_SITE,
|
||||
"arbitrage": ACTUATOR_SITE, "safety": ACTUATOR_SITE,
|
||||
"circulation": ACTUATOR_SITE, "sign_site": ACTUATOR_SITE})
|
||||
@@ -2948,16 +2984,16 @@ _unit_sources_unrouted = [
|
||||
if CLAIM_SCOPE.get(r[0]) == SCOPE_UNIT and "," not in r[1]]
|
||||
print(f" {len(_direct)} direct appends, each read at its own line: "
|
||||
+ ", ".join(f"main.py:{ln} {s} -> {a}" for s, a, ln in _routing))
|
||||
check("all TEN of `Controller.cycle()`'s DIRECT appends route through "
|
||||
check("all ELEVEN of `Controller.cycle()`'s DIRECT appends route through "
|
||||
"`self.actuator_of(<string literal>)`, so this scan can read the routing "
|
||||
"of EVERY one of them. An append that computed its actuator, or omitted "
|
||||
"the argument, or hand-wrote a string, fails HERE rather than being "
|
||||
"silently skipped - which is how a scan like this goes quietly vacuous, "
|
||||
"and 10f is in this file because that already happened once",
|
||||
len(_direct) == 10
|
||||
len(_direct) == 11
|
||||
and all(a is not None and a.startswith("self.actuator_of('")
|
||||
for _s, a, _ln in _routing))
|
||||
check("⚠️ AND AT EVERY ONE OF THE TEN THE LITERAL IT ROUTES THROUGH IS THE "
|
||||
check("⚠️ AND AT EVERY ONE OF THE ELEVEN THE LITERAL IT ROUTES THROUGH IS THE "
|
||||
"SOURCE THE CLAIM IS FILED UNDER. THIS is the line a mis-typed "
|
||||
"`actuator_of(...)` at a call site goes red on - all nine of them, on "
|
||||
"the line the typo is written, at every roster, INCLUDING the six "
|
||||
@@ -2971,14 +3007,15 @@ check("⚠️ AND EVERY PER-INVERTER APPEND NAMES THE INVERTER IT IS ABOUT. "
|
||||
"site-scoped sources and is the whole defect for a per-inverter one: a "
|
||||
"`device` limit built inside the roster loop and routed without `inv` "
|
||||
"files inverter B's rating on inverter A, where it binds the wrong "
|
||||
"share. Five of the ten appends are SCOPE_UNIT - `device`, "
|
||||
"`supervised`, `failsafe` and maintenance's two - and all five pass one. "
|
||||
"share. Six of the eleven appends are SCOPE_UNIT - `device`, "
|
||||
"`supervised`, `failsafe`, maintenance's two and OPT-09's `pack` - and "
|
||||
"all six pass one. "
|
||||
"SAFETY-30's `sign_site` is deliberately NOT one of them: it is filed on "
|
||||
"the site, beside `circulation`, because the number that tripped it is "
|
||||
"the site target",
|
||||
_unit_sources_unrouted == []
|
||||
and len([r for r in _routing
|
||||
if CLAIM_SCOPE.get(r[0]) == SCOPE_UNIT]) == 5)
|
||||
if CLAIM_SCOPE.get(r[0]) == SCOPE_UNIT]) == 6)
|
||||
check("...and the only appends this pin does NOT reach are the two that file a "
|
||||
"claim BUILT ELSEWHERE - `peak_claim` and `arb_claim` - which have no "
|
||||
"`actuator=` at the call site to mis-type. Their routing is pinned where "
|
||||
@@ -2987,7 +3024,7 @@ check("...and the only appends this pin does NOT reach are the two that file a "
|
||||
"arbitrage.claim() by test_arbitrage.py",
|
||||
sorted(_indirect) == sorted(CLAIM_BUILDERS)
|
||||
and len(_direct) + len(_indirect) == len(_appends))
|
||||
check("...and between them the eleven appends account for exactly the nine "
|
||||
check("...and between them the twelve appends account for exactly the eleven "
|
||||
"sources 10f pins, so the routing pin and the source set are one scan "
|
||||
"and neither can drift from the other",
|
||||
{s for s, _a, _ln in _routing} | set(CLAIM_BUILDERS.values())
|
||||
@@ -3027,10 +3064,19 @@ check("⚠️ AND THE RATING LIMITS ARE FILED ONCE PER INVERTER, ON ITS OWN "
|
||||
"connection",
|
||||
sorted((c.source, c.actuator) for c in _cl2)
|
||||
== [("device", "inverter-a"), ("device", "inverter-b"),
|
||||
("loop", ACTUATOR_SITE), ("supervised", "inverter-a"),
|
||||
("loop", ACTUATOR_SITE),
|
||||
# ⚠️ OPT-09, AND IT IS OBSERVED HERE RATHER THAN ONLY PINNED
|
||||
# STATICALLY. This fixture leaves both packs at 100 % - the last value
|
||||
# of the SoC sweep above - so each one's `pack` claim closes its `lo`
|
||||
# at 0 W: a full battery is not asked to charge. It is on ITS OWN
|
||||
# actuator, which is the half a `pack` limit filed on the site would
|
||||
# fail, and it appears ONLY here: the one-inverter sweep above files
|
||||
# no `pack` claim at any state of charge, which is AC 4.
|
||||
("pack", "inverter-a"), ("pack", "inverter-b"),
|
||||
("supervised", "inverter-a"),
|
||||
("supervised", "inverter-b")])
|
||||
check("...and naming those three is not an accident of this fixture but the "
|
||||
"reason the static pin exists - SEVEN of the ten sources need a state "
|
||||
"reason the static pin exists - SEVEN of the eleven sources need a state "
|
||||
"no check in this file reaches. SAFETY-31's `circulation` is one of the "
|
||||
"seven: it is filed only while the site is MEASURED to be circulating, "
|
||||
"which no fixture here is. SAFETY-30's `sign_site` is another, and it "
|
||||
@@ -3079,16 +3125,21 @@ check("...and so is each unit's `device` bound, which is the other number the "
|
||||
"inverter's reported range bound nothing at all",
|
||||
_dev2 == {"inverter-a": (-LIVE_MAX_W, LIVE_MAX_W),
|
||||
"inverter-b": (-B_RATING_HYPOTHESIS, B_RATING_HYPOTHESIS)})
|
||||
_n2_plans = {p.unit: (p.lo, p.hi) for p in (_n2_alloc.plans if _n2_alloc else ())}
|
||||
print(f" the envelopes the ARBITER RESOLVED: {_n2_plans}")
|
||||
check("...and the bounds the ARBITER RESOLVED for inverter B - not just the "
|
||||
"claims filed at it - are B's own, because a claim filed correctly and "
|
||||
"then resolved against the wrong envelope would be the same defect one "
|
||||
"layer down",
|
||||
"layer down. ⚠️ THE `hi` IS B'S OWN RATING AND THE `lo` IS 0 W, and the "
|
||||
"second half is OPT-09 rather than a defect: both packs in this fixture "
|
||||
"sit at 100 % - the last value of the SoC sweep above - so each one's "
|
||||
"`pack` claim closes its charge end. The two units' envelopes are still "
|
||||
"DIFFERENT NUMBERS, which is what this check exists to observe: a bound "
|
||||
"copied from the first unit would read +5000 on B",
|
||||
_n2_alloc is not None
|
||||
and next((p for p in _n2_alloc.plans if p.unit == "inverter-b"), None)
|
||||
is not None
|
||||
and (lambda p: p.lo == -B_RATING_HYPOTHESIS
|
||||
and p.hi == B_RATING_HYPOTHESIS)(
|
||||
next(p for p in _n2_alloc.plans if p.unit == "inverter-b")))
|
||||
and _n2_plans.get("inverter-b") == (0.0, B_RATING_HYPOTHESIS)
|
||||
and _n2_plans.get("inverter-a") == (0.0, LIVE_MAX_W)
|
||||
and _n2_plans["inverter-a"] != _n2_plans["inverter-b"])
|
||||
|
||||
|
||||
# --- 10h. ALLOC-01: WHAT MAIN.PY DOES WITH AN ALLOCATION IT CANNOT DELIVER ---
|
||||
|
||||
Reference in New Issue
Block a user