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
@@ -26,7 +26,9 @@ DECISIONS note and the dev report.
|
||||
solar + B Cheap hours, both 10 % at the start of the night, the night rate
|
||||
22:00-07:00, tomorrow 6 kWh, a 2.5 kW peak limit so the charge takes hours),
|
||||
sensors silent from 17:05, whole nights at 5 s per cycle (run in parallel
|
||||
processes, about 2.5 minutes each):
|
||||
processes, about 2.5 minutes each). OPT-36: B is Off in the nights that
|
||||
test A's level (a Cheap hours B now counts before A buys and would cover
|
||||
these hours); the `_b` nights keep it on for B's checks:
|
||||
a. silent from 17:05: both batteries end the night where a fresh figure
|
||||
ends them (the base left A at 57 % against 87 %), and the screens agree;
|
||||
b. restarts at 01:00 and 04:00 change nothing;
|
||||
@@ -267,8 +269,11 @@ PROFILE_LATE = ('{"periods": ['
|
||||
' "to": "22:00", "eur_per_kwh": 0.34}]}')
|
||||
CHOICE = {"a": (True, "leftover_solar", False),
|
||||
"b": (True, "cheap_hours", False)}
|
||||
# OPT-36: a Cheap hours B now gives first, and with these house shapes it would
|
||||
# cover the dear hours alone, so A would not charge. The nights that test A's
|
||||
# level have B Off (`b_off`, the default; the house is as written); `_b`
|
||||
# nights keep B on Cheap hours for B's own checks.
|
||||
LAST = at(3, 17, 5)
|
||||
MIDNIGHT = at(4, 0, 0)
|
||||
EVENING = [at(3, 16, 0) + timedelta(minutes=30 * i) for i in range(12)] \
|
||||
+ [at(3, 21, 55)]
|
||||
|
||||
@@ -291,12 +296,7 @@ def sensors(*, silent=True, fresh_from=None, fresh=(1.0, 5.0),
|
||||
when = now if fresh_until is None else min(now, fresh_until)
|
||||
return st(fresh[0] if ent == T_ENT else fresh[1], when)
|
||||
when = min(now, LAST) if silent else now
|
||||
# OPT-36: a fresh sensor reads the day it is on. After midnight "today"
|
||||
# is the 4th (6.0 kWh, the figure the 17:05 reading called tomorrow's),
|
||||
# not the evening's 0.4 kWh left of the 3rd; with that, fresh and
|
||||
# kept are the same figure all night (before, the plans only agreed
|
||||
# because A was capped at the ceiling in both).
|
||||
return st(0.4 if ent == T_ENT and now < MIDNIGHT else 6.0, when)
|
||||
return st(0.4 if ent == T_ENT else 6.0, when)
|
||||
return f
|
||||
|
||||
|
||||
@@ -305,12 +305,12 @@ def run_night(spec):
|
||||
hours, silent, fresh_from, fresh, restarts, soc, load_until_s, marks."""
|
||||
start = spec["start"]
|
||||
soc0 = spec.get("soc", {"a": 10.0, "b": 10.0})
|
||||
# OPT-36: Cheap hours B now covers part of the dear hours, so the house the
|
||||
# plan expects is 1.6 x larger than it was written for (700 W -> 1.1 kW):
|
||||
# A still has a shortfall to buy and the night charge is still long.
|
||||
house_w = 1.6 * spec.get("house_w", 700.0)
|
||||
house_w = spec.get("house_w", 700.0)
|
||||
store = rp._Store()
|
||||
for u, (use, mode, sell) in CHOICE.items():
|
||||
choice = dict(CHOICE)
|
||||
if spec.get("b_off", True):
|
||||
choice["b"] = (False, "cheap_hours", False)
|
||||
for u, (use, mode, sell) in choice.items():
|
||||
for k, val in (("pack_use", use), ("pack_mode", mode),
|
||||
("pack_sell", sell)):
|
||||
store.data[unit_key(f"inverter-{u}", k)] = val
|
||||
@@ -428,15 +428,24 @@ SCEN = {
|
||||
marks=[at(4, 4, 30)]),
|
||||
"silent": dict(start=NIGHT, hours=9,
|
||||
marks=[at(4, 1, 0), at(4, 4, 30), at(4, 6, 30)]),
|
||||
# OPT-36: B on Cheap hours, for B's checks
|
||||
"fresh_b": dict(start=NIGHT, hours=9, silent=False, b_off=False),
|
||||
"silent_b": dict(start=NIGHT, hours=9, b_off=False),
|
||||
# OPT-36: alone, A is full by 02:00 from 22:00; from midnight its charge
|
||||
# runs through 03:00
|
||||
"midnight": dict(start=at(4, 0, 0), hours=7),
|
||||
"restart": dict(start=NIGHT, hours=9,
|
||||
restarts=[at(4, 1, 0), at(4, 4, 0)],
|
||||
marks=[at(4, 1, 30), at(4, 4, 30)]),
|
||||
# c. the charge cannot start before 03:00 (the price dips at 04:00)
|
||||
"late": dict(start=at(4, 2, 30), hours=4.5, late=True, house_w=500.0,
|
||||
soc={"a": 40.0, "b": 70.0}, marks=[at(4, 3, 30)]),
|
||||
# OPT-36: the fresh sensors read the kept figure's 6.0 kWh for the 4th.
|
||||
# Alone, A reaches its level, so the runs must plan on the same sun (with
|
||||
# B charging too, A reached neither level by 07:00 and they matched).
|
||||
"late_fresh": dict(start=at(4, 2, 30), hours=4.5, late=True,
|
||||
house_w=500.0, soc={"a": 40.0, "b": 70.0},
|
||||
silent=False),
|
||||
fresh_from=at(4, 2, 30), fresh=(6.0, 6.0)),
|
||||
# d. a fresh, darker figure at 02:00 during the charge (a lighter house
|
||||
# use, so the level has room to rise)
|
||||
"dark": dict(start=at(4, 1, 30), hours=2.0, soc={"a": 45.0, "b": 45.0},
|
||||
@@ -455,8 +464,10 @@ SCEN = {
|
||||
house_w=500.0, fresh_from=at(4, 2, 40),
|
||||
fresh=(30.0, 30.0), marks=[at(4, 2, 44)]),
|
||||
# d. the last hour, after the kept figure ended at 06:00
|
||||
# (OPT-36: A at 60 %, not 70 %: alone it charges twice as fast and from
|
||||
# 70 % is full at 06:38, before the 06:40 mark)
|
||||
"tail": dict(start=at(4, 5, 40), hours=80 / 60.0,
|
||||
soc={"a": 70.0, "b": 70.0}, restarts=[at(4, 6, 30)],
|
||||
soc={"a": 60.0, "b": 70.0}, restarts=[at(4, 6, 30)],
|
||||
marks=[at(4, 5, 50), at(4, 6, 10), at(4, 6, 40)]),
|
||||
# a fresh, brighter figure at 03:10 lowers A's level below its SoC (A
|
||||
# stops, B goes on), then the sensors go quiet: the forecast ends at 06:10
|
||||
@@ -470,12 +481,12 @@ SCEN = {
|
||||
latch={"until": at(3, 7, 15).isoformat(),
|
||||
"born": at(2, 23, 0).isoformat(),
|
||||
"levels": {A: 80.0, B: 80.0}},
|
||||
marks=[at(3, 22, 20)]),
|
||||
marks=[at(3, 22, 20)], b_off=False),
|
||||
"stale_live": dict(start=at(3, 22, 0), hours=0.5, gone=True,
|
||||
latch={"until": at(4, 7, 15).isoformat(),
|
||||
"born": at(3, 22, 0).isoformat(),
|
||||
"levels": {A: 80.0, B: 80.0}},
|
||||
marks=[at(3, 22, 20)]),
|
||||
marks=[at(3, 22, 20)], b_off=False),
|
||||
# the same, B already full and a 1.5 kW limit: the run is sized at the
|
||||
# battery's 2.5 kW rating and ends at 06:33, before A is there, so the
|
||||
# next slice is planned after 06:00 and is only A's
|
||||
@@ -515,20 +526,20 @@ for k in R:
|
||||
fa, si, rr = R["fresh"], R["silent"], R["restart"]
|
||||
|
||||
# a. silent from 17:05
|
||||
# OPT-36: A's level is 95 % to ~05:00, then 78.4 % when B's cheap run is longer
|
||||
# (B's give 5.9 -> 7.1 kWh); A, already at 81.5 %, stops there.
|
||||
check("a fresh figure all night: A ends at 81.5 % (the plan's 78.4 % came "
|
||||
"after it had passed), B full",
|
||||
abs(fa["soc"][0] - 81.5) < 0.5 and fa["soc"][1] > 94.5)
|
||||
fb_, sb_ = R["fresh_b"], R["silent_b"]
|
||||
check("a fresh figure all night: A ends at its planned level (87 %), B "
|
||||
"(Cheap hours, on in the `_b` night) full",
|
||||
abs(fa["soc"][0] - 86.9) < 0.5 and fb_["soc"][1] > 94.5)
|
||||
check("sensors silent from 17:05: A ends where the fresh run ends it "
|
||||
f"({si['soc'][0]:.1f} against {fa['soc'][0]:.1f} %)",
|
||||
abs(si["soc"][0] - fa["soc"][0]) < 0.3)
|
||||
check("... and so does B (Cheap hours; the wide no-forecast gap does not "
|
||||
f"stop its slices): {si['soc'][1]:.1f} against {fa['soc'][1]:.1f} %",
|
||||
abs(si["soc"][1] - fa["soc"][1]) < 0.3)
|
||||
f"stop its slices): {sb_['soc'][1]:.1f} against {fb_['soc'][1]:.1f} %",
|
||||
abs(sb_["soc"][1] - fb_["soc"][1]) < 0.3)
|
||||
mn = R["midnight"]
|
||||
check("A charges through 03:00 without a break (no step at 03:00)",
|
||||
si["soc_at"]["03:30"][0] > si["soc_at"]["03:00"][0] + 4.0
|
||||
and si["soc_at"]["03:00"][0] > si["soc_at"]["02:55"][0] + 0.3)
|
||||
mn["soc_at"]["03:30"][0] > mn["soc_at"]["03:00"][0] + 4.0
|
||||
and mn["soc_at"]["03:00"][0] > mn["soc_at"]["02:55"][0] + 0.3)
|
||||
m1, m4 = si["marks"]["01:00"], si["marks"]["04:30"]
|
||||
check("01:00 and 04:30: the kept figure is in use, with a day plan, and the "
|
||||
"line says so (EN)",
|
||||
@@ -538,7 +549,7 @@ check("01:00 and 04:30: the kept figure is in use, with a day plan, and the "
|
||||
and m4["target_a"] is not None
|
||||
and near(m4["target_a"], fa["marks"]["04:30"]["target_a"], 1e-9))
|
||||
check("... and the level is the fresh run's (the kept figure counts as a "
|
||||
"fresh one)", near(m4["target_a"], 95.0, 0.2))
|
||||
"fresh one)", near(m4["target_a"], 86.87, 0.2))
|
||||
m6 = si["marks"]["06:30"]
|
||||
check("06:30: the kept figure has ended (the bell started) and the screen "
|
||||
"does not say 'kept' (the charge was done by then in this shape)",
|
||||
@@ -613,12 +624,12 @@ check("a fresh, brighter figure lowers A's level below its SoC during the "
|
||||
and lo["soc_at"]["03:30"][0] < lo["soc_at"]["03:20"][0] + 0.5)
|
||||
check("the sensors go quiet, the forecast ends (06:30): the latch holds the "
|
||||
"LOWERED level, so A does not charge back up to the old one "
|
||||
f"({lo['soc_at']['03:30'][0]:.1f} -> {lo['soc_at']['06:55'][0]:.1f} %), "
|
||||
"while B goes on",
|
||||
f"({lo['soc_at']['03:30'][0]:.1f} -> {lo['soc_at']['06:55'][0]:.1f} %)"
|
||||
" (OPT-36: B is Off here; B's slices with no forecast are the `_b` "
|
||||
"night's check above)",
|
||||
l2["fc_none"] and l2["target_a"] is not None
|
||||
and l2["target_a"] < l2["soc_a"] and l2["p_a"] > -200.0
|
||||
and lo["soc_at"]["06:55"][0] < lo["soc_at"]["03:30"][0] + 1.0
|
||||
and lo["soc"][1] > lo["soc_at"]["03:30"][1] + 15.0)
|
||||
and lo["soc_at"]["06:55"][0] < lo["soc_at"]["03:30"][0] + 1.0)
|
||||
st_, sl_ = R["stale"], R["stale_live"]
|
||||
check("F1: a latch from the night before (ended 07:15) loaded at a restart "
|
||||
"inside a cheap run, with no forecast: nothing grid-charges "
|
||||
@@ -661,19 +672,14 @@ md = fd["marks"]
|
||||
print(f" f1_dark: A level 01:55 {md['01:55']['target_a']} -> 02:15 "
|
||||
f"{md['02:15']['target_a']}; A {fd['soc_at']['06:55'][0]:.1f} % "
|
||||
"at 06:55")
|
||||
# OPT-36: the level (82.3 % at 02:15) falls to 64.2 % by 04:00 as B's cheap run
|
||||
# is made longer (B's expected give grows); A stops at its SoC of that moment
|
||||
# (72.8 %) and the latch holds 64.2 % after the figure ends.
|
||||
check("F-1 darker fresh figure at 02:00: the level rises, is held after "
|
||||
"05:00 with no forecast (never 'read 17:05'), and A charges while it is "
|
||||
"above its level",
|
||||
"05:00 with no forecast (never 'read 17:05'), and A charges to it",
|
||||
md["02:15"]["target_a"] is not None
|
||||
and md["01:55"]["target_a"] is not None
|
||||
and md["02:15"]["target_a"] > md["01:55"]["target_a"] + 3.0
|
||||
and all("17:05" not in md[h]["line"] for h in md if h >= "02:15")
|
||||
and all(md[h]["fc_none"] for h in md if "05:15" <= h < "07:00")
|
||||
and near(md["05:15"]["target_a"], 64.24, 0.2)
|
||||
and near(fd["soc_at"]["06:55"][0], 72.8, 0.5))
|
||||
and near(fd["soc_at"]["06:55"][0], md["02:15"]["target_a"], 1.5))
|
||||
|
||||
tb = R["tail_bfull"]
|
||||
check("B full: the run ends at 06:33 before A is at its level, so the next "
|
||||
|
||||
Reference in New Issue
Block a user