Default export_rate to 0; add tie-breaker so battery fills before exporting

- DEFAULTS['export_rate'] = 0 (terugleveringskosten can be re-enabled per
  scenario via the Advanced field — keeping the headline default simple).
- LP gets a microscopic per-hour penalty (1e-6 € · hour) on charging that
  prefers earlier hours, and on discharging that prefers later. Total
  effect on annual savings: < €0.001 (well below display rounding). But
  visually the battery now fills as soon as surplus arrives, instead of
  the LP arbitrarily mixing surplus hours with grid-arbitrage hours when
  both give the same daily total.
This commit is contained in:
Michiel Berger 2026-05-01 10:04:23 +02:00
parent e39f78fcc5
commit ab4f9712b5
3 changed files with 19 additions and 7 deletions

View file

@ -238,9 +238,20 @@ def oracle_daily_schedule(df: pd.DataFrame, battery: Battery) -> np.ndarray:
# Cost = sum(import_price * gI - export_price * gE). # Cost = sum(import_price * gI - export_price * gE).
# When import_price > export_price, the LP automatically picks # When import_price > export_price, the LP automatically picks
# min(gI, gE) = 0 because keeping both > 0 costs (imp - exp) > 0. # min(gI, gE) = 0 because keeping both > 0 costs (imp - exp) > 0.
#
# Tiny tie-breakers: when several surplus hours are equally good for
# charging (true whenever export_price is constant and we're capturing
# surplus), the simplex would otherwise pick an arbitrary subset.
# Adding a microscopic penalty proportional to hour-index makes early
# charging strictly cheaper than late charging by a fraction of a
# cent — so the battery visibly fills as soon as surplus appears.
# Symmetric for discharge: prefer late hours (when prices are
# typically highest anyway, but ties happen during evening plateaus).
EPS = 1e-6
hr = np.arange(m)
f = np.concatenate([ f = np.concatenate([
np.zeros(m), # c — already paid for via gI EPS * hr, # c: prefer earlier charging (smaller h ⇒ less cost)
np.zeros(m), # d — earnings reach us via reduced gI / increased gE EPS * (m - 1 - hr), # d: prefer later discharging
import_price, # pay for imports import_price, # pay for imports
-export_price, # earn export credit -export_price, # earn export credit
]) ])

View file

@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Honest battery payback — vs thuisbatterijgids.nl</title> <title>Honest battery payback — vs thuisbatterijgids.nl</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/uplot@1.6.31/dist/uPlot.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/uplot@1.6.31/dist/uPlot.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}?v=19"> <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}?v=20">
</head> </head>
<body> <body>
@ -194,6 +194,6 @@
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/uplot@1.6.31/dist/uPlot.iife.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/uplot@1.6.31/dist/uPlot.iife.min.js"></script>
<script src="{{ url_for('static', filename='app.js') }}?v=19"></script> <script src="{{ url_for('static', filename='app.js') }}?v=20"></script>
</body> </body>
</html> </html>

View file

@ -326,11 +326,12 @@ DEFAULTS = dict(
# demand_kwh / ANNUAL_DEMAND_BASE; ANNUAL_DEMAND_BASE annualises the # demand_kwh / ANNUAL_DEMAND_BASE; ANNUAL_DEMAND_BASE annualises the
# 8-month window's 1010 kWh → 1518 kWh-equivalent/yr, so set the # 8-month window's 1010 kWh → 1518 kWh-equivalent/yr, so set the
# default to 1518 to keep the per-hour P1 values unscaled (scale = 1.0). # default to 1518 to keep the per-hour P1 values unscaled (scale = 1.0).
# Dad's actual full-year net is 2351 kWh (from his complete CSV) — so # export_rate = 0 by default to keep the model simple. Dad's actual
# using 1518 effectively represents 'an 8-month-extrapolated 2025 year'. # situation has terugleveringskosten of €0.106/kWh; set the field
# explicitly to that value when modelling his real bill.
demand_kwh=1518.0, retail=0.25, pv_kwp=0.0, pv_yield=875.0, demand_kwh=1518.0, retail=0.25, pv_kwp=0.0, pv_yield=875.0,
fixed_rate=False, saldering=False, eta=0.88, inflation=0.03, fixed_rate=False, saldering=False, eta=0.88, inflation=0.03,
export_rate=-0.106, export_rate=0.0,
) )