OPT-11 P1: per-battery mode limits, on/off, solar order
A battery's mode is a soft unit limit (source `mode`, P_MODE 35) kept
apart from the hard limits in pre_resolve(). The allocator applies it
only while the site target is decided at or below 35 (new
Resolution.deciding_priority), so the peak floor releases it and a mode
`hi` never meets the site `lo`. At N=1 resolve() applies the same rule
to the site target after stage 2. A mode range without 0 W is dropped
as a software fault, so REFUSE_SIGN stays unreachable from a mode.
compute() gets the loop bound (sum over live units of hard limits with
modes applied), so the loop never asks for watts no battery may give
and the saturation freeze never parks on them. While charging, Leftover
solar weighs 0 so Solar first fills first. Store keys pack_use and
pack_mode per battery, set by two new /action values; Off stops and
skips maintenance with a log line. Minimal Batteries cards (EN/NL).
Off by default (battery_modes_enabled); off, commands are byte-identical
to a5dddad at N=1 and N=2 (test_modes.py part 1).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
python3 tools/alloc_sim.py --splits # AC 2: split-invariance at the meter
|
||||
python3 tools/alloc_sim.py --sweep # AC 3: gain to 1.0 at every split
|
||||
python3 tools/alloc_sim.py --all
|
||||
python3 tools/alloc_sim.py --spill # OPT-11: surplus solar, by mode
|
||||
|
||||
Exit 0 only if every split-invariance run HOLDS; 1 if one FAILS; 2 if no rig.
|
||||
|
||||
@@ -245,12 +246,98 @@ def sweep(sitesim, control, watchdog, cycles, load_w):
|
||||
return worst
|
||||
|
||||
|
||||
def spill(sitesim, control, arbiter, allocator, packmode, gain=0.6,
|
||||
cycles=30, load_w=1000.0, pv_w=9000.0):
|
||||
"""OPT-11 P1: surplus solar spills from Solar first to Leftover solar.
|
||||
|
||||
The rig's two plants and the add-on's own `compute()`, `resolve()`,
|
||||
`loop_bound()` and `packmode.charge_weights()`, with the claims main.py
|
||||
files for this state: each unit's rating, each unit's mode, the loop, and
|
||||
OPT-09's `pack` limit (`lo = 0`) once battery A is full. A is Solar first
|
||||
and B Leftover solar. Three phases of `cycles` each: A has room; A is full;
|
||||
then B is switched off. The loop is fed the site total (DEV-02).
|
||||
Returns (rows, verdicts).
|
||||
"""
|
||||
from plantsim import INVERTER_A, INVERTER_B, Inverter, Site
|
||||
names = ("inverter-a", "inverter-b")
|
||||
specs = [("A", INVERTER_A), ("B", INVERTER_B)]
|
||||
invs = [Inverter.from_spec(n, dict(sp, **sitesim.INSTANT))
|
||||
for n, sp in specs]
|
||||
site = Site(invs, load=load_w, pv=pv_w)
|
||||
tun = control.Tuning(gain=gain, max_w=sum(sp["rating_w"] for _n, sp in specs),
|
||||
slew_w=sitesim.EXPERIMENT_SLEW_W)
|
||||
C = arbiter.Claim
|
||||
prev, i_w, sat = 0.0, None, 0
|
||||
rows = []
|
||||
phases = (("A has room", False, True), ("A full", True, True),
|
||||
("A full, B off", True, False))
|
||||
for label, a_full, b_on in phases:
|
||||
modes = {names[0]: packmode.PackMode(True, packmode.SOLAR_FIRST),
|
||||
names[1]: packmode.PackMode(b_on, packmode.LEFTOVER_SOLAR)}
|
||||
for _c in range(cycles):
|
||||
claims = []
|
||||
for u, (_n, sp) in zip(names, specs):
|
||||
r = sp["rating_w"]
|
||||
claims.append(C.limit("supervised", arbiter.P_SAFETY, -r, r,
|
||||
actuator=u))
|
||||
lo, hi = packmode.mode_range(modes[u], r)
|
||||
claims.append(C.limit("mode", arbiter.P_MODE, lo, hi,
|
||||
actuator=u))
|
||||
if a_full:
|
||||
claims.append(C.limit("pack", arbiter.P_SAFETY, lo=0.0,
|
||||
actuator=names[0]))
|
||||
bound = allocator.loop_bound(
|
||||
{u: arbiter.pre_resolve([c for c in claims if c.actuator == u])
|
||||
for u in names})
|
||||
d = control.compute(prev, site.grid, site.battery, tun, sat, i_w,
|
||||
bound=bound)
|
||||
prev, i_w, sat = d.target_w, d.i_w, d.sat_count
|
||||
claims.append(C.set("loop", arbiter.P_LOOP, d.target_w))
|
||||
res = arbiter.resolve(
|
||||
claims, units=names,
|
||||
weights=lambda t: packmode.charge_weights(None, modes, t))
|
||||
for inv, u in zip(invs, names):
|
||||
inv.command(float(res.shares[u]))
|
||||
site.settle(sitesim.CYCLE_S, dt=0.05)
|
||||
rows.append(dict(phase=label, target=d.target_w, grid=site.grid,
|
||||
a=res.shares[names[0]], b=res.shares[names[1]],
|
||||
unmet=res.allocation.unmet_w, frozen=d.frozen,
|
||||
reason=d.reason, bound=bound))
|
||||
return rows
|
||||
|
||||
|
||||
def spill_report(rows, cycles):
|
||||
print("=== OPT-11 P1: surplus solar, A Solar first, B Leftover solar ===")
|
||||
ok = True
|
||||
for label in dict.fromkeys(r["phase"] for r in rows):
|
||||
ph = [r for r in rows if r["phase"] == label]
|
||||
last = ph[-1]
|
||||
frozen = sum(r["frozen"] for r in ph)
|
||||
print(f" {label:15} A {last['a']:+6.0f} W B {last['b']:+6.0f} W "
|
||||
f"grid {last['grid']:+7.0f} W loop bound "
|
||||
f"[{last['bound'][0]:.0f}, {last['bound'][1]:.0f}] "
|
||||
f"unmet {last['unmet']} W frozen cycles {frozen} "
|
||||
f"({last['reason']})")
|
||||
ok &= frozen == 0 and all(r["a"] <= 0 and r["b"] <= 0 for r in ph)
|
||||
if label == "A has room":
|
||||
ok &= last["a"] == -5000 and last["b"] < 0
|
||||
elif label == "A full":
|
||||
ok &= last["a"] == 0 and last["b"] == -5000
|
||||
else:
|
||||
ok &= last["a"] == 0 and last["b"] == 0 and last["unmet"] == 0
|
||||
print(f" -> spill {'HOLDS' if ok else 'FAILS'}: A fills first, the rest "
|
||||
f"spills to B, a full A and an Off B send the surplus to the grid, "
|
||||
f"and the loop never freezes on watts no battery may take")
|
||||
return ok
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
ap.add_argument("--table", action="store_true")
|
||||
ap.add_argument("--splits", action="store_true")
|
||||
ap.add_argument("--sweep", action="store_true")
|
||||
ap.add_argument("--all", action="store_true")
|
||||
ap.add_argument("--spill", action="store_true")
|
||||
ap.add_argument("--cycles", type=int, default=40)
|
||||
ap.add_argument("--load", type=float, default=2000.0)
|
||||
ap.add_argument("--gain", type=float, default=1.0)
|
||||
@@ -261,7 +348,7 @@ def main():
|
||||
"or one inverter's power (the PRE-DEV-02 sensing, "
|
||||
"kept because the comparison is the evidence)")
|
||||
a = ap.parse_args()
|
||||
if not (a.table or a.splits or a.sweep):
|
||||
if not (a.table or a.splits or a.sweep or a.spill):
|
||||
a.all = True
|
||||
|
||||
sim = find_sim()
|
||||
@@ -288,6 +375,11 @@ def main():
|
||||
print()
|
||||
if a.sweep or a.all:
|
||||
sweep(sitesim, control, watchdog, a.cycles, a.load)
|
||||
print()
|
||||
if a.spill or a.all:
|
||||
packmode = sitesim.load_addon("app.packmode", "alloc_sim")
|
||||
held &= spill_report(spill(sitesim, control, arbiter, allocator,
|
||||
packmode), 30)
|
||||
return 0 if held else 1
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user