From 3f69c174afa3a3fcfc7dda22a3ce70bd5e1a9951 Mon Sep 17 00:00:00 2001 From: admin Date: Tue, 29 Sep 2026 22:50:46 +0200 Subject: [PATCH] SAFETY-83: grade the refile at its boundary, and N=2 on its own A floor exactly at the drain rate stays at P_PEAK (kills `lo >= rate`), and part 4's N=2 check no longer shares its failure list with N=1. Co-Authored-By: Claude Opus 5.5 --- goodwe_controller/test_safety83.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/goodwe_controller/test_safety83.py b/goodwe_controller/test_safety83.py index da2ec0a..25ce45a 100644 --- a/goodwe_controller/test_safety83.py +++ b/goodwe_controller/test_safety83.py @@ -25,6 +25,7 @@ import logging import os import sys import tempfile +import types HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HERE) @@ -34,9 +35,9 @@ logging.disable(logging.WARNING) import opt11_identity as rig # noqa: E402 import safety78_identity as s78 # noqa: E402 import safety79_identity as s79 # noqa: E402 -from app.arbiter import P_MAINTENANCE, P_PEAK # noqa: E402 +from app.arbiter import Claim, P_MAINTENANCE, P_PEAK # noqa: E402 from app.main import Controller # noqa: E402 -from app.maintenance import CHARGE, DRAIN # noqa: E402 +from app.maintenance import CHARGE, DRAIN, MaintResult # noqa: E402 fails = [] TOL = 1.0 # watts over the aim a banked quarter may show @@ -201,18 +202,33 @@ for k, rows in _id.items(): and (not rate or r[1] == rate[0])) if not ok: _bad.append((k, r[0], r[1], r[6], pk)) +_bad1 = [b for b in _bad if b[0].startswith("n1")] +_bad2 = [b for b in _bad if b[0].startswith("n2")] _more = {k: sum(map(s78.wanted_more, rows)) for k, rows in _id.items()} print(f" cycles with the guard above the drain: {_more}; bad {_bad[:3]}") check("⭐ at N=1, on every cycle where the guard asks for more than the drain " "the peak claim is refiled at P_MAINTENANCE and the target is its floor; " "on every other cycle the peak claim stays at P_PEAK and the target is " "the drain rate", - not _bad and all(_more[k] > 50 for k in _id if k.startswith("n1"))) + not _bad1 and all(_more[k] > 50 for k in _id if k.startswith("n1"))) check("⭐ at N=2 the peak claim is never refiled (the SAFETY-78 floor does " "that job), and test_safety78.py part 5 holds N=2 to the digests of " - "release/1.0 35cdffe", not _bad) + "release/1.0 35cdffe", not _bad2) +# The rule itself, at the boundary: a floor AT the drain rate changes nothing. +_one = types.SimpleNamespace(inverters=[None]) +_edge = {} +for lo in (1999.5, 2000.0, 2000.5): + cl = [Claim.set("maintenance", P_MAINTENANCE, 2000.0, DRAIN), + Claim.limit("peak", P_PEAK, lo=lo, reason="quarter peak")] + Controller._peak_over_drain(_one, cl, [MaintResult(DRAIN, True, 2000.0, + False, [])]) + _edge[lo] = cl[1].priority +check("at the boundary: a floor at or below the 2000 W drain stays at P_PEAK, " + "a floor 0.5 W above it is refiled at P_MAINTENANCE", + _edge == {1999.5: P_PEAK, 2000.0: P_PEAK, 2000.5: P_MAINTENANCE}) + def store_hold(): st = rig._Store()