SAFETY-85 review: grade discharge only, over the plant's settle time
Rule 2 graded each battery against last cycle's command and graded charges too. On plantsim's battery (10 s lag) at the 2 s meter it warned at every drain or charge start and 15 times an hour under the peak guard at N=2; a charge tapering near full warned 11 times in 50 min (SAFETY-42 section 7). Now a discharge is graded against the least it stood at over SHORT_CYCLES / SHORT_SPAN_S. DOCS lists `mode-bound` under Loop reason. test_safety85 part 7: peak guard with a dead battery, lagging plant, taper; part 3: the bound ends with maintenance. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -281,6 +281,11 @@ def pack_energy_of(inv):
|
||||
# ordinary load steps. At 2 s and slower this changes nothing.
|
||||
REFUSED_SPAN_S = (_ctl.SETTLE_CYCLES - 1) * _ctl.LOOP_TICK_S
|
||||
|
||||
# SAFETY-85 review: a battery another owner set is graded against the least it
|
||||
# was told over the same span as above (the plant's measured settle time), so
|
||||
# a healthy battery still settling on a new command is not short.
|
||||
SHORT_CYCLES, SHORT_SPAN_S = _ctl.SETTLE_CYCLES, REFUSED_SPAN_S
|
||||
|
||||
|
||||
def _refused_interval(sign: int) -> tuple:
|
||||
"""SAFETY-73: the (lo, hi) a refusing unit may be asked: 0 W that way."""
|
||||
@@ -1180,9 +1185,11 @@ class Controller:
|
||||
self.loop_clamped: tuple[bool, str, bool] = (False, "", False)
|
||||
# UX-08: the numbers of a loop-side over-rating, for the notification.
|
||||
self.loop_clamp_facts: tuple = ()
|
||||
# SAFETY-85: this cycle's loop Decision, and how many cycles in a row
|
||||
# a battery set by another owner gave less than asked.
|
||||
# SAFETY-85: this cycle's loop Decision, each battery's recent
|
||||
# standing commands, and how many cycles in a row a battery set by
|
||||
# another owner gave less than asked.
|
||||
self._loop_decision = None
|
||||
self._asked_hist: dict = {}
|
||||
self._short_run = 0
|
||||
self.stopping = False
|
||||
# SAFETY-61. time.monotonic() at the end of the last run_control() pass.
|
||||
@@ -5084,25 +5091,36 @@ class Controller:
|
||||
|
||||
def _loop_alarm_input(self, resolution) -> None:
|
||||
"""SAFETY-85. The loop's clamp counts only on a cycle the loop decided
|
||||
(a rating may cut it). A battery another owner set counts when it gave
|
||||
less than it was told for `saturation_cycles` cycles in a row.
|
||||
DECISIONS/SAFETY-85-no-false-not-keeping-up.md."""
|
||||
(a rating may cut it). A battery another owner set to discharge counts
|
||||
when it gave less than it was told for `saturation_cycles` cycles in a
|
||||
row. DECISIONS/SAFETY-85-no-false-not-keeping-up.md."""
|
||||
d = self._loop_decision
|
||||
if d is None:
|
||||
self._short_run = 0
|
||||
self._asked_hist = {}
|
||||
return
|
||||
unmet = self.allocation.unmet_w if self.allocation is not None else 0
|
||||
cut = (resolution.bound_by or "").split("(")[0]
|
||||
owns = (resolution.winner == "loop" and not d.held and not unmet
|
||||
and (resolution.target_w == d.target_w
|
||||
or cut in ("device", "supervised")))
|
||||
# Graded against what stands on the wire (0 W in shadow mode).
|
||||
# Graded against the least that stood on the wire (0 W in shadow
|
||||
# mode) over SHORT_CYCLES cycles and SHORT_SPAN_S seconds.
|
||||
now, asked = self._now_mono, {}
|
||||
for inv in self.inverters:
|
||||
h = self._asked_hist.setdefault(inv.name, [])
|
||||
h.append((now, inv.standing_w()))
|
||||
while len(h) > SHORT_CYCLES and (
|
||||
now is None or h[0][0] is None
|
||||
or now - h[0][0] > SHORT_SPAN_S):
|
||||
h.pop(0)
|
||||
asked[inv.name] = min(w for _t, w in h)
|
||||
others = [inv for inv in self.inverters if not owns
|
||||
or allocator.pin_w(resolution.unit_bounds.get(inv.name))
|
||||
is not None]
|
||||
short = [(inv, inv.standing_w(), inv.batt) for inv in others
|
||||
short = [(inv, asked[inv.name], inv.batt) for inv in others
|
||||
if inv.batt is not None
|
||||
and delivery_short(inv.standing_w(), inv.batt,
|
||||
and delivery_short(asked[inv.name], inv.batt,
|
||||
self.tuning.saturation_w)]
|
||||
self._short_run = self._short_run + 1 if short else 0
|
||||
if owns and self.loop_clamped[0]:
|
||||
|
||||
Reference in New Issue
Block a user