SAFETY-125: a grid charge counts a late meter reading's seconds; the guard row says "over" when it is
A meter reading late near a quarter's end left seconds the projection did not count (SAFETY-99), and charge_headroom_w() read them as room: the cheap run (and price trading, and the N=2 maintenance charge) went back to full power in the quarter's last seconds and the quarter could land over a banked month peak or the cap. peakguard.charge_room_w() counts those seconds at the held reading for the charge only; the guard's floor is unchanged. The guard row said "idle, heading for 5 814 W, under this month's 5 413 W peak". Over the limit at the current rate the row and the month-peak line now say so, and that the grid charge is cut back. Version 0.19.2, CHANGELOG, DECISIONS. test_safety125.py new; the lag pins in test_price_run_fixes.py and test_opt20.py and the AST pin in test_arbitrage.py follow the change (see the pull request). Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
2932f3af39
commit
88ab8a50e3
@@ -963,7 +963,8 @@ def month_peak(c, now):
|
||||
ceiling = pg.ceiling_w or limit_w
|
||||
out["line"] = line_period(out["period_w"], ceiling, c.shadow,
|
||||
unmeasured=getattr(c, "peak_unmeasured_s", 0.0)
|
||||
> 0.0)
|
||||
> 0.0,
|
||||
cut=getattr(c, "room_charge", False))
|
||||
return out
|
||||
|
||||
|
||||
@@ -1367,13 +1368,19 @@ def line_peak_not_covered() -> str:
|
||||
return _t("peak_not_covered")
|
||||
|
||||
|
||||
def line_period(period_w, limit_w, shadow, unmeasured=False) -> str:
|
||||
def line_period(period_w, limit_w, shadow, unmeasured=False,
|
||||
cut=False) -> str:
|
||||
step = _w(shadow, "step_in", kw=_kw(limit_w))
|
||||
if unmeasured:
|
||||
# SAFETY-90: the add-on started during this period.
|
||||
return _t("period_unmeasured") + " " + step
|
||||
if period_w is None:
|
||||
return step
|
||||
if period_w > limit_w:
|
||||
# SAFETY-125: the figure is the current rate, and a grid charge is
|
||||
# what gets cut back.
|
||||
return _t("period_over", kw=_kw(period_w)) + " " + (
|
||||
_w(shadow, "charge_cut", kw=_kw(limit_w)) if cut else step)
|
||||
return _t("period", kw=_kw(period_w)) + " " + step
|
||||
|
||||
|
||||
|
||||
@@ -1217,6 +1217,8 @@ class Controller:
|
||||
# because `self.peak_guard` is a different cycle's decision by the time
|
||||
# the status page renders.
|
||||
self.arb_headroom_w = None
|
||||
# SAFETY-125: a grid charge sized to the quarter's room this cycle.
|
||||
self.room_charge = False
|
||||
# UX-13 P1: today's prices as the Home chart has seen them. Display
|
||||
# only; nothing reads it back into a decision.
|
||||
self.home_prices = homeview.DayPrices()
|
||||
@@ -3239,8 +3241,10 @@ class Controller:
|
||||
if any(c.kind == SET and c.actuator == ACTUATOR_SITE
|
||||
and c.priority > P_MAINT_SHAPE for c in claims):
|
||||
return 0.0
|
||||
room = peakguard.charge_headroom_w(self.peak_guard)
|
||||
return floor if room is None else min(floor, room)
|
||||
room = peakguard.charge_room_w(self.peak_guard, self._peak_proj)
|
||||
floor = floor if room is None else min(floor, room)
|
||||
self.room_charge = self.room_charge or floor > 0.0
|
||||
return floor
|
||||
|
||||
def _charge_ceiling(self, claims):
|
||||
"""SAFETY-84: the site `hi` for a cheap-window charge at N>=2, or None.
|
||||
@@ -4885,6 +4889,7 @@ class Controller:
|
||||
# decision presented as current.
|
||||
self.arb, self.arb_at = None, now
|
||||
self.arb_headroom_w = None
|
||||
self.room_charge = False
|
||||
# OPT-11 P3: with battery modes on, each battery's mode decides who
|
||||
# trades (`_mode_roles()`, `_mode_trade()`, below, which read the
|
||||
# same headroom), and this site-wide trade is not run.
|
||||
@@ -4895,7 +4900,9 @@ class Controller:
|
||||
# `arbitrage.sized_from_meter()` in `_shadow_settle()`, so the
|
||||
# caveat on the recorded watts can never be answered from a
|
||||
# different cycle's guard than the watts were.
|
||||
self.arb_headroom_w = peakguard.charge_headroom_w(self.peak_guard)
|
||||
# SAFETY-125: a late meter reading's seconds count, as the bill.
|
||||
self.arb_headroom_w = peakguard.charge_room_w(self.peak_guard,
|
||||
self._peak_proj)
|
||||
if site_trade:
|
||||
floor_pct, ceiling_pct = arbitrage.soc_bounds(self.o)
|
||||
charge_w, discharge_w = self.trade_power_w(floor_pct, ceiling_pct)
|
||||
@@ -4950,6 +4957,7 @@ class Controller:
|
||||
arb_claim = arbitrage.claim(self.arb)
|
||||
if arb_claim is not None:
|
||||
claims.append(arb_claim)
|
||||
self.room_charge = self.arb.action == arbitrage.CHARGE
|
||||
# Edge-logged, for the peak guard's reason: the action log holds 40
|
||||
# lines and a trade lasts a whole hour, so a line per cycle would
|
||||
# flush the context that explains it. The transition is what an
|
||||
@@ -5169,6 +5177,8 @@ class Controller:
|
||||
mode_trade, trade_was = False, self._mode_trade_was
|
||||
if self.modes_on:
|
||||
mode_trade = self._mode_trade(claims, now)
|
||||
self.room_charge = self.room_charge or (
|
||||
mode_trade and self.arb.action == arbitrage.CHARGE)
|
||||
self._mode_trade_was = mode_trade
|
||||
self._charge_track(claims, maint_results)
|
||||
_live96 = set(self.live_units())
|
||||
@@ -7062,6 +7072,15 @@ class Controller:
|
||||
"text": messages.row_peak_guard_on(
|
||||
_pg.floor_w or 0.0, _pg.ceiling_w,
|
||||
shadow=self.shadow)})
|
||||
elif _pg.projected_w is not None:
|
||||
# SAFETY-125: "under" only when it is; over the limit at this
|
||||
# rate, a grid charge is cut back to the quarter's room.
|
||||
out.append({"ok": True, "warn": False, "id": "peak_guard",
|
||||
"text": messages.row_peak_guard_idle(
|
||||
_pg.projected_w, _pg.ceiling_w,
|
||||
month_peak=(_pg.ceiling_from
|
||||
== peakguard.CEILING_MONTH_PEAK),
|
||||
cut=self.room_charge and not self.shadow)})
|
||||
else:
|
||||
out.append({"ok": True, "warn": False, "id": "peak_guard",
|
||||
"text": _pg.explain()})
|
||||
|
||||
@@ -1170,12 +1170,22 @@ def row_periods_measured(n) -> str:
|
||||
f"Gemeten kwartieren deze maand: {getoond}")
|
||||
|
||||
|
||||
def row_peak_guard_idle(projected_w, limit_w, month_peak: bool) -> str:
|
||||
"""The projection first and the limit second (SAFETY-38's ordered pair)."""
|
||||
def row_peak_guard_idle(projected_w, limit_w, month_peak: bool,
|
||||
cut: bool = False) -> str:
|
||||
"""The projection first and the limit second (SAFETY-38's ordered pair).
|
||||
SAFETY-125: over the limit at this rate it says so, and `cut` that a
|
||||
grid charge is cut back to the room left."""
|
||||
limit = (f"this month's {watts(limit_w)} peak" if month_peak
|
||||
else f"the {watts(limit_w)} peak limit")
|
||||
grens = (f"de maandpiek van {watts(limit_w)}" if month_peak
|
||||
else f"de piekgrens van {watts(limit_w)}")
|
||||
if float(projected_w) > float(limit_w):
|
||||
doing, doet = (("cutting the grid charge", "schroeft het laden van "
|
||||
"het net terug") if cut else ("idle", "in rust"))
|
||||
return Bi(f"Peak guard: {doing}, at this rate {watts(projected_w)}, "
|
||||
f"over {limit}",
|
||||
f"Piekbewaking: {doet}, aan dit tempo "
|
||||
f"{watts(projected_w)}, boven {grens}")
|
||||
return Bi(f"Peak guard: idle, heading for {watts(projected_w)}, under "
|
||||
f"{limit}",
|
||||
f"Piekbewaking: in rust, richting {watts(projected_w)}, onder "
|
||||
|
||||
@@ -1136,6 +1136,21 @@ def charge_headroom_w(decision) -> float | None:
|
||||
return max(0.0, -float(decision.wanted_w))
|
||||
|
||||
|
||||
def charge_room_w(decision, projection) -> float | None:
|
||||
"""SAFETY-125: `charge_headroom_w()` with the seconds since the last
|
||||
meter reading counted at that reading, so a late reading near a
|
||||
quarter's end is not read as room for a grid charge (SAFETY-99).
|
||||
DECISIONS/SAFETY-125-grid-charge-under-the-month-peak.md."""
|
||||
room = charge_headroom_w(decision)
|
||||
if not room or projection is None or projection.block_start is None:
|
||||
return room
|
||||
left = QUARTER_S - float(projection.elapsed_s)
|
||||
if left <= 0.0:
|
||||
return room
|
||||
late_ws = max(0.0, float(projection.held_w)) * uncovered_s(projection)
|
||||
return max(0.0, room - late_ws / left)
|
||||
|
||||
|
||||
def shaping_suspended(decision, peak_forecast_w, cap_w) -> bool:
|
||||
"""Must the maintenance charge-only shaping stand down this cycle?
|
||||
|
||||
|
||||
@@ -416,6 +416,9 @@
|
||||
"step_in_would": "The batteries would step in above {kw}.",
|
||||
"period": "This 15-minute period is heading for {kw}.",
|
||||
"period_unmeasured": "Part of this 15-minute period was not measured.",
|
||||
"period_over": "At this rate this 15-minute period reaches {kw}.",
|
||||
"charge_cut": "The grid charge is cut back to stay under {kw}.",
|
||||
"charge_cut_would": "The grid charge would be cut back to stay under {kw}.",
|
||||
"prices": {
|
||||
"now": "Price now {eur} per kWh, all in.",
|
||||
"today": "Today: lowest {lo} at {lo_at}, highest {hi} at {hi_at}.",
|
||||
|
||||
@@ -351,6 +351,9 @@
|
||||
"step_in_would": "De batterijen zouden bijspringen boven {kw}.",
|
||||
"period": "Dit kwartier gaat richting {kw}.",
|
||||
"period_unmeasured": "Een deel van dit kwartier is niet gemeten.",
|
||||
"period_over": "Aan dit tempo komt dit kwartier op {kw}.",
|
||||
"charge_cut": "Het laden van het net wordt teruggeschroefd om onder {kw} te blijven.",
|
||||
"charge_cut_would": "Het laden van het net zou teruggeschroefd worden om onder {kw} te blijven.",
|
||||
"prices": {"now": "Prijs nu {eur} per kWh, alles inbegrepen.", "today": "Vandaag: laagste {lo} om {lo_at}, hoogste {hi} om {hi_at}.", "none": "Geen prijzen voor vandaag."},
|
||||
"tomorrow": {"known": "De prijzen van morgen zijn binnen.", "unknown": "De prijzen van morgen komen meestal na 13:00."},
|
||||
"region": {"preset": "Heffingen en btw: {region}, cijfers van {year}.", "own": "Heffingen en btw: uw eigen bedrag."},
|
||||
|
||||
Reference in New Issue
Block a user