#!/usr/bin/env python3 """Run a scenario through both our LP and the cracked store calculator. This lets us see, for any battery / PV / saldering / rate combination, what thuisbatterijgids.nl claims you'll save vs what the actual physics permit. Examples: # Reproduce the dynamic-mode tests: python scripts/compare_with_store.py --capacity 1.92 --power 0.8 --cost 700 --retail 0.25 python scripts/compare_with_store.py --capacity 5.12 --power 0.8 --cost 1339 --retail 0.25 python scripts/compare_with_store.py --capacity 5.12 --power 2.5 --cost 1339 --retail 0.25 # PV scenario, fixed retail, no saldering: python scripts/compare_with_store.py --pv-kwp 3.0 --fixed --retail 0.28 """ from __future__ import annotations import argparse from pluginbattery.sim import ( Battery, apply_nl_tariff, load_hourly, oracle_daily_schedule, simulate, synthesize_pv, ) from pluginbattery.store_calc import ( Scenario, StoreParams, payback_years, quote as store_quote, ) def main() -> None: p = argparse.ArgumentParser(description=__doc__) # Battery p.add_argument("--capacity", type=float, default=1.92, help="Battery capacity in kWh") p.add_argument("--power", type=float, default=0.8, help="Battery in/out power in kW") p.add_argument("--cost", type=float, default=700, help="Battery price (incl. VAT) in EUR") p.add_argument("--eta", type=float, default=0.88, help="LP round-trip efficiency") # Tariff p.add_argument("--retail", type=float, default=0.25, help="Average retail EUR/kWh") p.add_argument("--demand", type=float, default=4000.0, help="Annual demand kWh/yr") p.add_argument("--fixed", action="store_true", help="Fixed-rate mode (no time-of-day variation; kills arbitrage)") p.add_argument("--saldering", action="store_true", help="Full saldering on (export = retail). Default: no saldering.") # Solar p.add_argument("--pv-kwp", type=float, default=0.0, help="PV size in kWp; 0 = no PV") p.add_argument("--pv-yield", type=float, default=875.0, help="Annual PV yield in kWh/kWp (store uses 875 in their footer)") # Inflation p.add_argument("--inflation", type=float, default=0.03) args = p.parse_args() # ─── Honest LP ───────────────────────────────────────────────────── base = load_hourly("data/raw"); base = apply_nl_tariff(base) df = base.copy() # Demand scaling. annual_demand = base["demand_kwh"].sum() * (8766.0 / len(base)) df["demand_kwh"] = base["demand_kwh"] * (args.demand / annual_demand) # Retail price. if args.fixed: df["eur_per_kwh"] = args.retail epex_in_use = base["epex_eur_per_kwh"] # raw EPEX kept else: 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 epex_in_use = df["epex_eur_per_kwh"] # PV. if args.pv_kwp > 0: df = synthesize_pv(df, kwp=args.pv_kwp, target_kwh_per_kwp_per_year=args.pv_yield) # Saldering. if args.saldering: df["export_eur_per_kwh"] = df["eur_per_kwh"] else: df["export_eur_per_kwh"] = 0.0 # post-saldering default in this script bat = Battery( capacity_kwh=args.capacity, max_charge_kw=args.power, max_discharge_kw=args.power, round_trip_eff=args.eta, allows_export=False, ) out = simulate(df, bat, oracle_daily_schedule(df, bat)) lp_year1 = float(out["savings"].sum()) lp_payback = payback_years(lp_year1, args.cost, args.inflation) # ─── Store calculator (cracked) ──────────────────────────────────── avg_epex_used = float(epex_in_use.mean()) store_params = StoreParams(avg_epex_eur_per_kwh=avg_epex_used) scenario = Scenario( capacity_kwh=args.capacity, max_charge_kw=args.power, battery_cost_eur=args.cost, avg_retail_eur_per_kwh=args.retail, has_pv=args.pv_kwp > 0, has_saldering=args.saldering, dynamic_rate=not args.fixed, ) sq = store_quote(scenario, store_params, inflation=args.inflation) # ─── Report ───────────────────────────────────────────────────────── print(f"Scenario:") print(f" Battery : {args.capacity} kWh / {args.power} kW / €{args.cost:.0f}") print(f" Tariff : avg retail €{args.retail}, " f"{'FIXED' if args.fixed else 'DYNAMIC'} rate, " f"{'WITH' if args.saldering else 'NO'} saldering") print(f" PV : {args.pv_kwp} kWp" f"{' @ ' + str(args.pv_yield) + ' kWh/kWp/yr' if args.pv_kwp else ''}") print(f" Demand : {args.demand:.0f} kWh/yr") print(f" Inflation : {args.inflation*100:.1f}%/yr") print() print(f"{'':22s} {'year-1 €':>10s} {'payback':>9s}") print(f" {'Honest LP':20s} €{lp_year1:>8.2f} {lp_payback:>5.2f} yr") print(f" {'Store calculator':20s} €{sq['year1_total']:>8.2f} {sq['payback_years']:>5.2f} yr") if sq["dynamic_arbitrage"] > 0 or sq["pv_self_consumption"] > 0: print(f" ↳ arbitrage €{sq['dynamic_arbitrage']:>8.2f}") print(f" ↳ self-consume €{sq['pv_self_consumption']:>8.2f}") if sq["year1_total"] > 0: ratio = sq["year1_total"] / max(lp_year1, 1e-9) print() print(f" Store overstates savings by {ratio:.2f}× " f"(€{sq['year1_total'] - lp_year1:+.2f}/yr)") if __name__ == "__main__": main()