OPT-36 round 3: B's charge counts the cheap time left; OPT-34 nights on their own shapes
The run is sized at the batteries' ratings, not the peak limit, so a run too short to fill a Cheap hours battery is planned again later in the night. Counting that run alone made B's give grow through the night and A's level fall after A had bought (round 2: 55 -> 69 -> 41 -> 37 %, A to 56 %). dayplan._cheap_give_kwh() now also counts the later firm slots before the expensive hours that a run would still be worth charging in (price + the spread control asks at most the give run's dearest slot); the Controller passes the slots and the spread. test_opt34: back to the original house shapes, fixture and expected values. B is Off in the nights that test A's level (a Cheap hours B now covers these hours); B's checks run in new `_b` nights; the 03:00 check runs in a night from midnight; tail starts A at 60 %; late_fresh reads the kept 6.0. test_opt36: slot cases, a whole night on one figure (level never moves), F-1 with B counted (raised level reached and held). test_ux54: the run end is read from the plan and asserted (02:30); the F1/F2 sweep uses a peak limit that does not bind and reads a level shown as the target as at it. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
3a18475a5a
commit
e4b9dee699
@@ -231,14 +231,21 @@ def _room(p, to_pct):
|
||||
|
||||
|
||||
def _cheap_give_kwh(runs, now, house, packs, *, floor_pct, ceiling_pct,
|
||||
aim_w):
|
||||
aim_w, until=None, slots=None, spread=None):
|
||||
"""OPT-36: what the Cheap hours batteries in `packs` give in the
|
||||
expensive hours, in kWh on the house's side: (the level at the start of
|
||||
those hours - the floor) x capacity x `EFF`.
|
||||
|
||||
That level is the one its own cheap run reaches: the ceiling, or less when
|
||||
the run cannot get there. From now to the run's end (or the whole run
|
||||
ahead of it) the batteries together charge at their ratings, held to what
|
||||
That level is the one its cheap charging reaches: the ceiling, or less
|
||||
when the time left cannot get there. That time is the cheap run from now
|
||||
(or the whole run ahead of it) and, given the firm price `slots`, each
|
||||
later slot before the expensive hours (`until`, a give run) still worth
|
||||
charging in: its price plus the spread control asks (`spread`) at most
|
||||
the give run's dearest slot, `priceruns.plan()`'s test. The run is sized
|
||||
at the batteries' ratings (`priceruns.need_hours()`), not the peak limit,
|
||||
so a run too short to fill them is planned again in that time; counting
|
||||
the run alone let A's level fall in the night after A had bought. In
|
||||
that time the batteries together charge at their ratings, held to what
|
||||
the peak aim leaves above the house's use that hour (`aim_w`, None: no
|
||||
limit), shared by rating. No cheap run worth charging in: the level now.
|
||||
A battery that refuses a direction counts 0."""
|
||||
@@ -249,16 +256,26 @@ def _cheap_give_kwh(runs, now, house, packs, *, floor_pct, ceiling_pct,
|
||||
gain = {p.name: 0.0 for p in ps}
|
||||
rate = {p.name: abs(float(p.rating_w)) for p in ps}
|
||||
total = sum(rate.values())
|
||||
run = runs.cheap
|
||||
spans = [(max(now, run.start), run.end)]
|
||||
top = max((eur for s, _e, eur in slots or ()
|
||||
if until is not None and until.start <= s < until.end),
|
||||
default=None)
|
||||
if top is not None and spread is not None:
|
||||
spans += [(max(s, now, run.end), min(e, until.start))
|
||||
for s, e, eur in slots
|
||||
if eur + float(spread) <= top + 1e-9
|
||||
and e > run.end and s < until.start]
|
||||
if runs.charge_useful() and total > 0.0:
|
||||
for s, e, h in _hour_slices(max(now, runs.cheap.start),
|
||||
runs.cheap.end):
|
||||
w = total
|
||||
if aim_w is not None:
|
||||
w = min(w, max(0.0, float(aim_w)
|
||||
- max(house_w(h), base_w, 0.0)))
|
||||
hrs = (e - s).total_seconds() / 3600.0
|
||||
for p in ps:
|
||||
gain[p.name] += w * rate[p.name] / total * hrs / 1000.0
|
||||
for t0, t1 in spans:
|
||||
for s, e, h in _hour_slices(t0, t1):
|
||||
w = total
|
||||
if aim_w is not None:
|
||||
w = min(w, max(0.0, float(aim_w)
|
||||
- max(house_w(h), base_w, 0.0)))
|
||||
hrs = (e - s).total_seconds() / 3600.0
|
||||
for p in ps:
|
||||
gain[p.name] += w * rate[p.name] / total * hrs / 1000.0
|
||||
out = 0.0
|
||||
for p in ps:
|
||||
cap = float(p.capacity_kwh)
|
||||
@@ -289,11 +306,13 @@ def _solar_first_give_kwh(house, now, start, packs, *, floor_pct):
|
||||
|
||||
|
||||
def plan(forecast, house, runs, now, packs, *, trust_pct, floor_pct,
|
||||
ceiling_pct, aim_w=None):
|
||||
ceiling_pct, aim_w=None, slots=None, spread=None):
|
||||
"""The day plan, or None when there is none to make: no forecast, no
|
||||
house profile yet, no price plan, or no expensive run after the cheap
|
||||
run. `house` is `HouseProfile.estimator()`'s answer. `packs` are
|
||||
`modetrade.Pack`s."""
|
||||
`modetrade.Pack`s. `aim_w`, `slots` (`priceruns.firm_slots()`) and
|
||||
`spread` (the price gap control asks, EUR/kWh) size a Cheap hours
|
||||
battery's charge (`_cheap_give_kwh()`)."""
|
||||
if forecast is None or house is None or runs is None or not runs.on \
|
||||
or runs.cheap is None:
|
||||
return None
|
||||
@@ -346,7 +365,8 @@ def plan(forecast, house, runs, now, packs, *, trust_pct, floor_pct,
|
||||
# Leftover solar holds the rest. A Leftover solar battery's own stored energy counts through its
|
||||
# level: it buys from where it is to the level it needs.
|
||||
cheap = _cheap_give_kwh(runs, now, house, usable, floor_pct=floor_pct,
|
||||
ceiling_pct=ceiling_pct, aim_w=aim_w) \
|
||||
ceiling_pct=ceiling_pct, aim_w=aim_w,
|
||||
until=until, slots=slots, spread=spread) \
|
||||
if price else 0.0
|
||||
cheap = min(cheap, short)
|
||||
sfirst = _solar_first_give_kwh(house, now, until.start, usable,
|
||||
|
||||
@@ -2375,7 +2375,9 @@ class Controller:
|
||||
else self.house_profile.estimator(),
|
||||
priceruns.keep_started(self.mode_runs, runs, now), now, packs,
|
||||
trust_pct=self.solar_trust_pct, floor_pct=floor_pct,
|
||||
ceiling_pct=ceiling_pct, aim_w=self.peak_aim_w())
|
||||
ceiling_pct=ceiling_pct, aim_w=self.peak_aim_w(),
|
||||
slots=priceruns.firm_slots(plan.read.points, now),
|
||||
spread=required)
|
||||
self.day_plan = dp
|
||||
if dp is None:
|
||||
self._plan_targets = None
|
||||
|
||||
Reference in New Issue
Block a user