Add timeline inspector — print hour-by-hour LP schedule for any week
This commit is contained in:
parent
1804606ff6
commit
2f05a40d40
1 changed files with 134 additions and 0 deletions
134
scripts/timeline_inspect.py
Normal file
134
scripts/timeline_inspect.py
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Print the hour-by-hour LP schedule for a battery over a chosen 7 days.
|
||||||
|
|
||||||
|
Same LP we use everywhere else — the "shortcut" is just the per-day LP
|
||||||
|
optimum aggregated to a yearly number. This script shows the disaggregated
|
||||||
|
trajectory so you can read the actual decisions.
|
||||||
|
|
||||||
|
Default scenario matches the web app's defaults (dad's P1, retail €0.25,
|
||||||
|
terugleveringskosten −€0.106). Pick a different week with --start.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
uv run python scripts/timeline_inspect.py
|
||||||
|
uv run python scripts/timeline_inspect.py --start 2024-06-17 --days 7
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import datetime as dt
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from pluginbattery.sim import (
|
||||||
|
Battery, apply_nl_tariff, load_hourly, oracle_daily_schedule, simulate,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
p = argparse.ArgumentParser(description=__doc__)
|
||||||
|
p.add_argument("--start", type=lambda s: dt.date.fromisoformat(s),
|
||||||
|
default=dt.date(2024, 6, 17),
|
||||||
|
help="First UTC day to print (yyyy-mm-dd). Default: a sunny mid-June week.")
|
||||||
|
p.add_argument("--days", type=int, default=7)
|
||||||
|
p.add_argument("--retail", type=float, default=0.25)
|
||||||
|
p.add_argument("--export-rate", type=float, default=-0.106)
|
||||||
|
p.add_argument("--demand-kwh", type=float, default=2350.0)
|
||||||
|
|
||||||
|
# Zendure SolarFlow 800 Plus
|
||||||
|
p.add_argument("--cap", type=float, default=1.92)
|
||||||
|
p.add_argument("--power", type=float, default=0.8)
|
||||||
|
p.add_argument("--eta", type=float, default=0.88)
|
||||||
|
p.add_argument("--cost", type=float, default=479.0)
|
||||||
|
|
||||||
|
args = p.parse_args()
|
||||||
|
|
||||||
|
# ─── Build the same df the web app sees ───
|
||||||
|
base = apply_nl_tariff(load_hourly("data/raw"))
|
||||||
|
df = base.copy()
|
||||||
|
annual = base["demand_kwh"].sum() * 8766 / len(base)
|
||||||
|
df["demand_kwh"] = base["demand_kwh"] * (args.demand_kwh / annual)
|
||||||
|
scale = args.retail / base["eur_per_kwh"].mean()
|
||||||
|
df["eur_per_kwh"] = base["eur_per_kwh"] * scale
|
||||||
|
df["epex_eur_per_kwh"] = base["epex_eur_per_kwh"] * scale
|
||||||
|
df["export_eur_per_kwh"] = float(args.export_rate)
|
||||||
|
|
||||||
|
bat = Battery(args.cap, args.power, args.power, args.eta, allows_export=False)
|
||||||
|
schedule = oracle_daily_schedule(df, bat)
|
||||||
|
out = simulate(df, bat, schedule)
|
||||||
|
|
||||||
|
# Pre-compute import / export split for clarity.
|
||||||
|
g = out["grid_kwh_with_battery"]
|
||||||
|
out = out.assign(
|
||||||
|
import_kwh=np.maximum(0.0, g),
|
||||||
|
export_kwh=np.maximum(0.0, -g),
|
||||||
|
no_bat_grid=out["demand_kwh"], # equals the original net since pv=0
|
||||||
|
cost_no_battery_eur=np.where(out["demand_kwh"] > 0,
|
||||||
|
out["demand_kwh"] * out["eur_per_kwh"],
|
||||||
|
out["demand_kwh"] * out["export_eur_per_kwh"]),
|
||||||
|
cost_with_battery_eur=np.where(g > 0, g * out["eur_per_kwh"],
|
||||||
|
g * out["export_eur_per_kwh"]),
|
||||||
|
)
|
||||||
|
out["savings_eur"] = out["cost_no_battery_eur"] - out["cost_with_battery_eur"]
|
||||||
|
|
||||||
|
# ─── Slice the chosen window ───
|
||||||
|
start = pd.Timestamp(args.start, tz="UTC")
|
||||||
|
end = start + pd.Timedelta(days=args.days)
|
||||||
|
win = out.loc[(out.index >= start) & (out.index < end)].copy()
|
||||||
|
if win.empty:
|
||||||
|
raise SystemExit(f"No data in range {start} .. {end}; window is "
|
||||||
|
f"{out.index[0].date()} .. {out.index[-1].date()}")
|
||||||
|
|
||||||
|
# ─── Print per-day blocks ───
|
||||||
|
print(f"Battery: {args.cap} kWh / {args.power} kW / €{args.cost:.0f} ; "
|
||||||
|
f"η_rt = {args.eta}; retail €{args.retail}; export €{args.export_rate}")
|
||||||
|
print(f"Window: {start.date()} → {end.date()-pd.Timedelta(days=1)} ({len(win)} hours)\n")
|
||||||
|
|
||||||
|
for date, day in win.groupby(win.index.date):
|
||||||
|
print(f"── {date} ────────────────────────────────────────────────────────────────")
|
||||||
|
print(f" {'hr':>2s} {'price':>5s} {'net (no bat)':>13s} "
|
||||||
|
f"{'charge':>7s} {'disch':>7s} {'SoC':>5s} {'grid':>7s} {'sav':>6s}")
|
||||||
|
for ts, r in day.iterrows():
|
||||||
|
net_no = r["demand_kwh"]
|
||||||
|
mark = "↓ surplus" if net_no < -0.01 else "↑ import" if net_no > 0.01 else "·"
|
||||||
|
grid = r["grid_kwh_with_battery"]
|
||||||
|
grid_str = f"{grid:+6.2f}"
|
||||||
|
print(f" {ts.hour:02d} €{r['eur_per_kwh']:.3f} "
|
||||||
|
f"{net_no:+8.2f} {mark:5s} "
|
||||||
|
f"{r['charge_kwh']:>6.2f} {r['discharge_kwh']:>6.2f} "
|
||||||
|
f"{r['soc_kwh']:>4.2f} {grid_str:>7s} €{r['savings_eur']:+5.2f}")
|
||||||
|
d_imp_no = max(0.0, day['demand_kwh'].clip(lower=0).sum())
|
||||||
|
d_exp_no = -day['demand_kwh'].clip(upper=0).sum()
|
||||||
|
d_imp_yes = day['import_kwh'].sum()
|
||||||
|
d_exp_yes = day['export_kwh'].sum()
|
||||||
|
d_sav = day['savings_eur'].sum()
|
||||||
|
d_charge = day['charge_kwh'].sum()
|
||||||
|
d_discharge = day['discharge_kwh'].sum()
|
||||||
|
print(f" day total imports: {d_imp_no:5.1f} → {d_imp_yes:5.1f} "
|
||||||
|
f"exports: {d_exp_no:5.1f} → {d_exp_yes:5.1f} "
|
||||||
|
f"battery in/out: {d_charge:5.2f}/{d_discharge:5.2f} kWh "
|
||||||
|
f"SAVED €{d_sav:+.2f}\n")
|
||||||
|
|
||||||
|
# ─── Window aggregate vs annual rate ───
|
||||||
|
week_savings = win["savings_eur"].sum()
|
||||||
|
week_charge = win["charge_kwh"].sum()
|
||||||
|
week_disch = win["discharge_kwh"].sum()
|
||||||
|
week_surplus = (-win["demand_kwh"].clip(upper=0)).sum()
|
||||||
|
week_imports = win["demand_kwh"].clip(lower=0).sum()
|
||||||
|
|
||||||
|
annual_savings = out["savings_eur"].sum()
|
||||||
|
annualised = week_savings * 365.25 / args.days
|
||||||
|
|
||||||
|
print(f"=== {args.days}-day totals ===")
|
||||||
|
print(f" imports without bat : {week_imports:6.1f} kWh")
|
||||||
|
print(f" surplus without bat : {week_surplus:6.1f} kWh")
|
||||||
|
print(f" battery throughput : charge {week_charge:.1f} / discharge {week_disch:.1f} kWh "
|
||||||
|
f"({week_disch/args.cap:.1f} cycles)")
|
||||||
|
print(f" savings this {args.days:>2d} days: €{week_savings:6.2f}")
|
||||||
|
print(f" annualised → 365 days : €{annualised:6.2f}")
|
||||||
|
print(f" full-year LP savings : €{annual_savings:6.2f} "
|
||||||
|
f"(this week is {week_savings/annual_savings*100:.1f}% of annual)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue