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
@@ -225,7 +225,12 @@ check("Leftover solar and Cheap hours: a give block exactly on the give run "
|
||||
# OPT-36: a larger evening (4.8 kW) so that B (Cheap hours) does not cover the
|
||||
# dear hours alone and A still buys from the grid; A starts at 30 %.
|
||||
c_n = S.make("dynamic", at(0, 18), soc=(30.0, 40.0), evening=4800.0)
|
||||
END_S, END_H = "03:00", 3.0 # the run's end, recomputed for this house
|
||||
# The run's end, read from the plan; for this house it is 02:30, as in 0.20.0.
|
||||
END = c_n.mode_runs.cheap.end
|
||||
END_H = (END - DAY0).total_seconds() / 3600.0
|
||||
END_S = S.brussels(END).strftime("%H:%M")
|
||||
check(f"the cheap run is 01:00 to {END_S} (02:30 for this house)",
|
||||
c_n.mode_runs.cheap.start == at(1, 0, 0) and END == at(2, 30, 0))
|
||||
nb = tab(c_n, at(0, 18))["days"][0]["batteries"]
|
||||
check("in the night before the cheap run both charge from the grid in it "
|
||||
f"(01:00 to {END_S}), then give at 17:00",
|
||||
@@ -249,11 +254,12 @@ def at_h(b, h):
|
||||
return dict((round(x, 3), v) for x, v in b["levels"]).get(h, float("nan"))
|
||||
|
||||
|
||||
check("the curve charges in a grid block: A (planned level 73 %) and B "
|
||||
"(Cheap hours, to the end of its run) are well above where they "
|
||||
LVL_A = c_n.plan_targets()[c_n.inverters[0].name]
|
||||
check(f"the curve charges in a grid block: A (planned level {LVL_A:.0f} %) "
|
||||
"and B (Cheap hours, to the end of its run) are well above where they "
|
||||
"were at 01:00, and stay put after the run",
|
||||
all(at_h(b, END_H) - at_h(b, 1.0) > 8.0
|
||||
and abs(at_h(b, 4.0) - at_h(b, END_H)) < 1e-6
|
||||
and abs(at_h(b, END_H + 0.5) - at_h(b, END_H)) < 1e-6
|
||||
for b in nb) and nb[0]["levels"][0][1] == 30.0)
|
||||
# Modes off: the verdict's legs.
|
||||
c_f = S.make("off", at(10, 7))
|
||||
@@ -828,14 +834,16 @@ def may_give(inv):
|
||||
|
||||
|
||||
def sweep(name, now0, soc, *, guard=None, until=48.0, sun=None,
|
||||
evening=None):
|
||||
evening=None, peak_w=None):
|
||||
"""Walk the real control cycle (`_mode_roles()`) every 15 minutes from
|
||||
`now0` to the end of tomorrow, each battery at the level the tab's curve
|
||||
has at that time, and return the tab and the role seen per battery and
|
||||
quarter. A GIVE role on a pack that control's discharge envelope closes
|
||||
is seen as "closed": the pack is not asked to give. The tab is built
|
||||
once, at `now0`."""
|
||||
once, at `now0`. `peak_w`: the site's peak limit (OPT-36, below)."""
|
||||
c = S.make(name, now0, soc=soc, evening=evening)
|
||||
if peak_w is not None:
|
||||
c.o = dict(c.o, peak_cap_w=peak_w)
|
||||
cut_out_at_floor(c)
|
||||
c.tariff_plan = S.quarters(now0, night_evening, days=3)
|
||||
c.tariff_at = now0
|
||||
@@ -856,6 +864,13 @@ def sweep(name, now0, soc, *, guard=None, until=48.0, sun=None,
|
||||
b = day["batteries"][i]
|
||||
inv.soc = level_at(b, h if h < 24 else h - 24) if b["levels"] \
|
||||
else inv.soc
|
||||
# OPT-36: the tab shows a level to 0.1 % (`dayview.LEVEL_DP`); a
|
||||
# battery whose curve shows its planned level (50.0 for 50.01) is
|
||||
# at it, or control charges it for the last 0.01 %.
|
||||
tg = (c.plan_targets() or {}).get(inv.name)
|
||||
if tg is not None and inv.soc is not None and round(
|
||||
inv.soc, dayview.LEVEL_DP) == round(tg, dayview.LEVEL_DP):
|
||||
inv.soc = max(inv.soc, tg)
|
||||
c.tariff_at = t
|
||||
tick = c._mode_roles(t, None)
|
||||
for inv in c.inverters:
|
||||
@@ -894,7 +909,12 @@ def tomorrow_match(out, seen, d0, role, kind):
|
||||
|
||||
|
||||
t22 = at(22, 0, 5)
|
||||
c1, o1, s1, d01 = sweep("dynamic", t22, (20.0, 20.0), evening=4800.0)
|
||||
# OPT-36: the plan counts B's charge under the peak limit, the tab's curve
|
||||
# charges at the batteries' ratings. Walked along the curve, B runs ahead of
|
||||
# the plan and A's level falls behind the tab's block. A limit that does not
|
||||
# bind (20 kW) makes the curve the charge control plans.
|
||||
c1, o1, s1, d01 = sweep("dynamic", t22, (20.0, 20.0), evening=4800.0,
|
||||
peak_w=20000.0)
|
||||
tm1 = o1["days"][1]["batteries"]
|
||||
print(" 22:00, both batteries at 20 %: tomorrow", [[(r["kind"], r["from"], r["to"]) for r in b["blocks"]] for b in tm1])
|
||||
check("F1, modes on: at 22:00 with both batteries at 20 % tomorrow's give "
|
||||
|
||||
Reference in New Issue
Block a user