Replace LP with greedy self-consumption — match real battery firmware
The LP was producing visually unintuitive schedules: when several surplus
hours had identical cost benefit (true with constant export_rate), the
solver picked an arbitrary subset, which the user couldn't read as 'this
is the battery doing its job'. Worse, the daily-LP variant drained to 0
every midnight because it placed zero value on next-day SoC.
Replaced oracle_daily_schedule() with a one-pass greedy dispatcher that
matches what every plug-in battery in the catalog (Marstek, Zendure,
EcoFlow, HomeWizard) actually does in 'self-consumption mode':
for each hour:
if exporting at meter: charge as fast as power+capacity allows
if importing at meter: discharge to cover net demand
Trade-offs:
- Greedy doesn't do grid arbitrage (charge cheap → discharge expensive
without a surplus source). The LP would; real plug-in firmware
doesn't, so greedy is more honest about what dad's battery would do.
- Charts now show 'fill in the morning, overflow at midday' which is
what users expect to see.
- Updated tests: dropped LP-arbitrage assertion, added a greedy-fills-
from-surplus-then-overflows test that locks the new behaviour in.
This commit is contained in:
parent
ab4f9712b5
commit
b4d895224b
2 changed files with 76 additions and 5 deletions
|
|
@ -46,7 +46,10 @@ def test_plugin_never_exports():
|
|||
assert (out["grid_kwh_with_battery"] >= -1e-9).all()
|
||||
|
||||
|
||||
def test_oracle_arbitrages_clear_spread():
|
||||
def test_greedy_does_not_grid_arbitrage_without_pv():
|
||||
"""Greedy dispatch is self-consumption only — it won't import cheap and
|
||||
discharge expensive without a surplus source. (LP would; greedy by
|
||||
design doesn't, matching real plug-in firmware.)"""
|
||||
prices = [0.05] * 12 + [0.50] * 12
|
||||
demands = [1.0] * 24
|
||||
df = make_df(prices, demands)
|
||||
|
|
@ -54,9 +57,28 @@ def test_oracle_arbitrages_clear_spread():
|
|||
round_trip_eff=0.9, allows_export=False)
|
||||
schedule = oracle_daily_schedule(df, bat)
|
||||
out = simulate(df, bat, schedule)
|
||||
assert out["charge_kwh"].iloc[:12].sum() > 0
|
||||
assert out["charge_kwh"].sum() == 0
|
||||
assert out["discharge_kwh"].sum() == 0
|
||||
|
||||
|
||||
def test_greedy_fills_from_surplus_then_overflows():
|
||||
"""With PV surplus, greedy fills the battery as fast as power allows
|
||||
until capacity is reached, then lets the rest export."""
|
||||
prices = [0.20] * 24
|
||||
demand = [0.1] * 24
|
||||
pv = [0.0] * 6 + [3.0] * 6 + [0.0] * 12 # 6 sunny hours, 3 kWh/h surplus
|
||||
df = make_df(prices, demand)
|
||||
df["pv_kwh"] = pv
|
||||
bat = Battery(capacity_kwh=2.0, max_charge_kw=0.8, max_discharge_kw=0.8,
|
||||
round_trip_eff=1.0, allows_export=False)
|
||||
schedule = oracle_daily_schedule(df, bat)
|
||||
out = simulate(df, bat, schedule)
|
||||
# Charging happens during the first surplus hours, capped at 0.8 kW
|
||||
assert out["charge_kwh"].iloc[6:9].sum() == pytest.approx(2.0, abs=1e-6)
|
||||
# Once full, no more charging even though surplus continues
|
||||
assert out["charge_kwh"].iloc[9:12].sum() == 0
|
||||
# Battery discharges into evening demand
|
||||
assert out["discharge_kwh"].iloc[12:].sum() > 0
|
||||
assert out["savings"].sum() > 0
|
||||
|
||||
|
||||
def test_apply_nl_tariff_matches_user_formula():
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue