MAINT-28: hold only where MAINT-24 moved the aim (banked peak above the cap); test_safety119 pins stay
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
92f0387415
commit
8640bfcbc1
@@ -6,13 +6,13 @@
|
||||
`peakguard.shaping_suspended()` pauses the charge while the forecast is over the aim or the guard is engaged. MAINT-24 moved the aim from 3450 W to the month aim (SAFETY-80), so the pause no longer lasts the whole peak: the guard gives, its projection drops under 5950 W and it idles ("heading for 5 6xx W, under this month's peak"), the pause ends, the floor charges A 150-460 W, the grid goes to 6.3-6.9 kW, the guard trips again. A flipped charge/give 468 times in 45 min (base 02eac3b 11). The quarters stayed at the aim; the cost was a battery working against itself and a grid that swung 5.4-6.9 kW.
|
||||
|
||||
## The change
|
||||
`peakguard.shaping_held(held, decision, forecast, aim, house)`: the old conjunction, and once it was true the pause stays until the house (`grid + battery power`, the load without our own charge) is `MAINT_RESUME_UNDER_W` = 300 W under the aim. A guard that is engaged or a forecast over the aim still pauses at once. House unread: the forecast stands in; neither read: no pause (SAFETY-63). `ctl_cycle.py` keeps the flag in `_maint_peak_hold`. The hold is for N=2 and up: at one inverter the old rule stays, because SAFETY-87's N=1 digests (`n1_charge_low`, a real peak at N=1) are pinned byte for byte.
|
||||
`peakguard.shaping_held(held, decision, forecast, aim, house)`: the old conjunction, and once it was true the pause stays until the house (`grid + battery power`, the load without our own charge) is `MAINT_RESUME_UNDER_W` = 300 W under the aim. A guard that is engaged or a forecast over the aim still pauses at once. House unread: the forecast stands in; neither read: no pause (SAFETY-63). `ctl_cycle.py` keeps the flag in `_maint_peak_hold`. The hold is for N=2 and up, and only where MAINT-24 moved the aim (a banked month peak above `peak_cap_w`, so the aim is above cap minus margin). Elsewhere the old rule stays, byte for byte: SAFETY-87's N=1 digests (`n1_charge_low`) and SAFETY-119's N=2 charge at 5050 W over a 5000 W cap with no banked peak are pinned, and 1b5f7c9 behaves as 02eac3b there (a chase that was not a MAINT-24 regression).
|
||||
|
||||
Why the house and not the forecast: while A gives, the forecast sits just under the aim by design, so no forecast margin is both safe and reachable. The house is the meter reading plus the batteries' power (`grid + site_batt_w`): the net demand the meter would see with no battery at work. Solar is already in it, and that is right: the aim is a limit on what the meter takes, so a sunny hour that lowers the net demand is real room, and a cloud that raises it brings the hold back at once. It is not the gross load, and it would be wrong to add PV back (that would hold the pause on room the meter really has). It stays put when A switches, except for the age of the battery reading (up to 10 s on the rig); the 300 W line is far larger than that lag moves it.
|
||||
|
||||
Why 300 W: the house wanders +-50 W; once the charge resumes it settles at (aim - house) / 2, so 300 W gives at least 150 W, three times the noise, and charging less than that is not worth a restart.
|
||||
|
||||
The flag lives only while the charge-only branch is taken: every cycle starts it at False, so a maintenance that ended, a mode change or a missing aim cannot carry a stale True into the next charge (test_maint28.py part 1b). The hold is for N=2 and up: at one inverter the old rule stays, because SAFETY-87's N=1 digests (`n1_charge_low`, a real peak at N=1) are pinned byte for byte.
|
||||
The flag lives only while the charge-only branch is taken: every cycle starts it at False, so a maintenance that ended, a mode change or a missing aim cannot carry a stale True into the next charge (test_maint28.py part 1b). The hold is for N=2 and up, and only where MAINT-24 moved the aim (a banked month peak above `peak_cap_w`, so the aim is above cap minus margin). Elsewhere the old rule stays, byte for byte: SAFETY-87's N=1 digests (`n1_charge_low`) and SAFETY-119's N=2 charge at 5050 W over a 5000 W cap with no banked peak are pinned, and 1b5f7c9 behaves as 02eac3b there (a chase that was not a MAINT-24 regression).
|
||||
|
||||
It only ever keeps the maintenance charge off. No cut-back is delayed or weakened: the guard's claim is filed as before. While the pause is held the loop works as at 02eac3b, so A also gives for the house (quarters then sit under the aim, A's energy goes to the load: 1.67 kWh in the 45 min below, 0.45 kWh on 1b5f7c9). A cap on that give would conflict with the guard's `lo` (`lo > hi` resolves to 0 W), so it is not tried here.
|
||||
|
||||
@@ -30,7 +30,7 @@ House 3000 -> 6500 W for 45 min -> 3000 W, +-50 W every 5 s, banked month peak 6
|
||||
Rig (MAINT-24 test report, F-1): 468 reversals in 45 min on 1b5f7c9, 11 on 02eac3b. Live shape (house 3000 W): no pause, A 1475 W, as MAINT-24.
|
||||
|
||||
## Tests and mutants
|
||||
`test_maint28.py`: part 1 the real peak, 1b the stale hold, 2 `shaping_held()` edges, 3 the unchanged cases (digests equal 1b5f7c9). On 1b5f7c9 itself (hold stubbed out) part 1 and 1b fail. Mutants, each red: M1 hold state not kept (`shaping_held(False, ...)`: the 1b5f7c9 behaviour); M2 resume line `-` to `+`; M3 house not passed; M4 the per-cycle reset removed; M5 the N=1 gate removed (digest `n1_peak`).
|
||||
`test_maint28.py`: part 1 the real peak, 1b the stale hold, 2 `shaping_held()` edges, 3 the unchanged cases (digests equal 1b5f7c9). On 1b5f7c9 itself (hold stubbed out) part 1 and 1b fail. Mutants, each red: M1 hold state not kept (`shaping_held(False, ...)`: the 1b5f7c9 behaviour); M2 resume line `-` to `+`; M3 house not passed; M4 the per-cycle reset removed; M5 the N=1 gate removed (digest `n1_peak`); M6 the aim gate removed (digest `n2_nobank`).
|
||||
|
||||
## Not changed
|
||||
MAINT-24's rate and its tests, N=1, the drain, modes off (digests of 02eac3b). The pause for a late or missing forecast.
|
||||
|
||||
@@ -567,11 +567,14 @@ class CycleMixin:
|
||||
float(self.o.get("peak_cap_w", 3500)),
|
||||
self.peak_margin_w, _reading)
|
||||
# MAINT-28: stays paused until the house is clear of the aim.
|
||||
# N=2 and up; one inverter keeps the old rule (SAFETY-87 pins).
|
||||
# Only where MAINT-24 moved the aim (a banked peak above the
|
||||
# cap) and at N=2 and up: elsewhere the old rule, byte for byte
|
||||
# (SAFETY-87 / SAFETY-119 pins).
|
||||
house = (None if self.grid is None or self.site_batt_w is None
|
||||
else float(self.grid) + float(self.site_batt_w))
|
||||
self._maint_peak_hold = peakguard.shaping_held(
|
||||
_peak_held and len(self.inverters) > 1,
|
||||
_peak_held and len(self.inverters) > 1
|
||||
and maint_aim > self.peak_aim_w(),
|
||||
self.peak_guard, self.peak_fc,
|
||||
maint_aim, house)
|
||||
if self._maint_peak_hold:
|
||||
|
||||
@@ -245,14 +245,16 @@ check("MAINT-24 live shape: A charges 1475 W and the pause never comes",
|
||||
d1 = sc.digest(scen(a=CH, n=1, fc=None, peak=None)[0])
|
||||
dd = sc.digest(scen(a=DRAIN_R, soc=(60.0, 30.0), peak=None, fc=None)[0])
|
||||
dm = sc.digest(scen(a=CH, variant="modes_off", peak=None, fc=None)[0])
|
||||
d2n = sc.digest(scen(a=CH, house_w=5000.0, peak=None, fc="grid")[0])
|
||||
d1p = sc.digest(scen(a=CH, n=1, fc=None, peak=None, house_w=5000.0)[0])
|
||||
AT_BASE = {
|
||||
"n2_nobank": "c164c9d4f221f7d685a0aa904a7878d4b287cf295d2e22281132140ba4a047db",
|
||||
"n1_peak": "8d266f39eb880f80fad8d236b2fb5100934cb92efd8ed2082e85eb3b71cd6c45",
|
||||
"n1": "44816d132984ecd64d5a2510223debe102f29decd1d5127ae2b255b45dbf8bd4",
|
||||
"drain": "8fd86d0816626ee5ae86bc973cb0121b1b5108e56c3b0fdd4b4be6c1f403f748",
|
||||
"off": "a007f962793611bce15b11f576a76aa52063adcfde82b2a2dc0b0c075b60e60b",
|
||||
}
|
||||
got = {"n1_peak": d1p, "n1": d1, "drain": dd, "off": dm}
|
||||
got = {"n2_nobank": d2n, "n1_peak": d1p, "n1": d1, "drain": dd, "off": dm}
|
||||
for k in AT_BASE:
|
||||
print(f" {k} {'same' if got[k] == AT_BASE[k] else 'DIFFERS'} "
|
||||
f"{got[k][:16]}")
|
||||
@@ -260,6 +262,9 @@ check("N=1 with A in maintenance charge and no forecast entity: equal to "
|
||||
"release/1.0 1b5f7c9", got["n1"] == AT_BASE["n1"])
|
||||
check("N=1 in a real peak (house 5000 W, a guard that steps in): equal "
|
||||
"to 1b5f7c9, the hold is for N=2 and up", got["n1_peak"] == AT_BASE["n1_peak"])
|
||||
check("N=2 in a real peak with no banked peak (house 5000 W over the aim "
|
||||
"3450 W): equal to 1b5f7c9, the hold is for a month aim above the cap",
|
||||
got["n2_nobank"] == AT_BASE["n2_nobank"])
|
||||
check("the SAFETY-78 drain beside a charger: equal to 1b5f7c9",
|
||||
got["drain"] == AT_BASE["drain"])
|
||||
check("battery modes off: equal to 1b5f7c9", got["off"] == AT_BASE["off"])
|
||||
|
||||
Reference in New Issue
Block a user