Fáze 2/3: rozšířený penalty audit + prototyp čistého jádra

Penalty audit (6 fixtures vč. evening_push a extreme_neg_buy):
- stejných 16/26 penalt mrtvých i na rozšířeném pokrytí
  (vč. EVENING_PUSH_Z_EXPORT_BONUS=2500 na evening-push dni)
- žádná penalta nezpůsobuje Infeasible 2026-05-01 (strukturální problém)
- Σpenalty 7978 Kč vs cashflow −614 Kč

clean_core_prototype.py: čistý ekonomický MILP (bez heuristických penalt) na
IDENTICKÝCH vstupech fixtures vs golden snapshoty současného plánovače:
- lepší na všech 5 řešitelných fixtures, celkem +266 Kč (+25 %) za horizonty
- extrémní den 2026-05-01: current INFEASIBLE → clean OK (−713 Kč zisk)
- férové: současné plány mají hp/ev setpointy 0, čistý dispatch srovnání

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dusan Vojacek
2026-06-11 14:02:17 +02:00
parent 9a2229641d
commit ec13c2ad6e
2 changed files with 126 additions and 11 deletions

View File

@@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""
Fáze 3 (teaser) prototyp čistého jádra plánovače nad golden fixtures.
Vezme STEJNÉ vstupy jako produkční solver (golden fixtures: forecast PV, baseline
load, efektivní ceny, battery/grid context) a vyřeší je ČISTÝM ekonomickým MILP
(scripts/harness/economics_report.solve_oracle): cash + degradace + terminal SoC,
tvrdá pravidla (block_export_on_negative_sell, curtail jen pole A, výkonové stropy),
ŽÁDNÉ heuristické penalty.
Porovná s výsledkem současného plánovače (golden snapshoty):
- cashflow current vs clean na identických vstupech (modelované Kč),
- feasibility (extrémní den 2026-05-01 je pro současný plánovač Infeasible).
Není to produkční náhrada (chybí EV deadline, TČ/TUV, provozní režimy) — je to
měření, kolik ekonomiky stojí heuristická vrstva. Spouštět z backend/:
python3 ../scripts/harness/clean_core_prototype.py
"""
from __future__ import annotations
import importlib.util
import json
import sys
from datetime import datetime
from pathlib import Path
HARNESS = Path(__file__).resolve().parent
BACKEND = HARNESS.parents[1] / "backend"
sys.path.insert(0, str(BACKEND))
_spec = importlib.util.spec_from_file_location("econ", HARNESS / "economics_report.py")
econ = importlib.util.module_from_spec(_spec)
sys.modules["econ"] = econ # dataclasses vyžadují modul v sys.modules
_spec.loader.exec_module(econ)
FIXTURES = sorted((BACKEND / "tests" / "golden" / "fixtures").glob("*.json"))
SNAPSHOTS = BACKEND / "tests" / "golden" / "snapshots"
def _fixture_to_inputs(fx: dict):
ctx = fx["context_json"]
b = ctx["battery"]
bat = econ.BatteryParams(
usable_wh=float(b["usable_capacity_wh"]),
min_soc_wh=float(b["min_soc_wh"]),
soc_max_wh=float(b.get("planner_soc_max_wh", b["soc_max_wh"])),
charge_eff=float(b["charge_efficiency"]),
discharge_eff=float(b["discharge_efficiency"]),
max_charge_w=float(b["max_charge_power_w"]),
max_discharge_w=float(b["max_discharge_power_w"]),
degradation_czk_kwh=float(b["degradation_cost_czk_kwh"]),
)
g = ctx["grid"]
grid = {
"max_import_w": float(g["max_import_power_w"]),
"max_export_w": float(g["max_export_power_w"]),
"block_export_on_negative_sell": bool(g.get("block_export_on_negative_sell") or False),
}
slots = []
for r in fx["slot_rows"]:
# forecast (W) → energie slotu (Wh): ×0.25 h
slots.append(
econ.DaySlot(
interval_start=datetime.fromisoformat(r["interval_start"]),
buy=float(r["buy_price"]),
sell=float(r["sell_price"]),
pv_a_wh=float(r["pv_a_forecast_w"] or 0) * 0.25,
pv_b_wh=float(r["pv_b_forecast_w"] or 0) * 0.25,
load_wh=float(r["load_baseline_w"] or 0) * 0.25,
grid_import_wh=0.0,
grid_export_wh=0.0,
soc_pct=None,
)
)
soc0 = float(ctx["soc_wh"])
return slots, bat, grid, soc0
def main() -> None:
header = (
f"{'fixture':<42} {'current':>9} {'clean':>9} {'Δ':>8} pozn."
)
print("# Clean core prototyp vs současný plánovač (modelovaný cashflow, Kč/horizont)")
print("# Δ < 0 = čisté jádro vydělá víc na stejných vstupech (bez SoC adjustu — terminal value v objective obou)")
print()
print(header)
print("-" * len(header))
total_cur = total_clean = 0.0
for path in FIXTURES:
fx = json.loads(path.read_text(encoding="utf-8"))
slots, bat, grid, soc0 = _fixture_to_inputs(fx)
avg_buy = sum(s.buy for s in slots[: 96]) / min(96, len(slots))
factor = float(fx["context_json"]["battery"].get("planner_terminal_soc_value_factor") or 1.0)
cash, soc_end = econ.solve_oracle(slots, bat, grid, soc0, avg_buy * factor)
# SoC-fér: ocenit koncový SoC stejně jako terminal value
clean_adj = cash - soc_end / 1000.0 * avg_buy * factor
snap = json.loads((SNAPSHOTS / path.name).read_text(encoding="utf-8"))
if "solver_error" in snap:
print(f"{path.stem:<42} {'INFEAS':>9} {clean_adj:>9.1f} {'':>8} current selhal, clean OK")
continue
cur_cash = snap["totals"]["cashflow_czk"]
cur_soc_end = snap["slots"][-1]["battery_soc_target"] / 100.0 * bat.usable_wh
cur_adj = cur_cash - cur_soc_end / 1000.0 * avg_buy * factor
d = clean_adj - cur_adj
total_cur += cur_adj
total_clean += clean_adj
print(f"{path.stem:<42} {cur_adj:>9.1f} {clean_adj:>9.1f} {d:>8.1f}")
print("-" * len(header))
print(f"{'CELKEM (bez infeasible)':<42} {total_cur:>9.1f} {total_clean:>9.1f} {total_clean - total_cur:>8.1f}")
if __name__ == "__main__":
main()

View File

@@ -1,35 +1,35 @@
# Δcashflow: záporné = plán bez penalty vydělá víc (modelované Kč za horizonty fixtures)
baseline: cashflow -440.4 Kč, penalty 2140.1 Kč
baseline: cashflow -613.6 Kč, penalty 7977.9 Kč; infeasible fixtures: ['home-01_2026-05-01_extreme_neg_buy']
konstanta hodnota Δcash Δpenalty Δsloty bind
-------------------------------------------------------------------------------------------------
CURTAILMENT_PENALTY 0.001 0.0 -134.4 0 NE (mrtvá?)
CURTAILMENT_PENALTY 0.001 0.0 -284.5 0 NE (mrtvá?)
EVENING_PUSH_Z_EXPORT_BONUS_CZK 2500.0 0.0 0.0 0 NE (mrtvá?)
LOAD_FIRST_INCENTIVE_CZK_KWH 0.05 0.0 0.0 0 NE (mrtvá?)
NEG_BUY_CHARGE_SHORTFALL_PENALTY_CZK_KWH 100.0 0.0 0.0 0 NE (mrtvá?)
NEG_EVENING_PREP_DISCHARGE_SHORTFALL_PENALTY_CZK_KWH 120.0 0.0 0.0 0 NE (mrtvá?)
NEG_EVENING_RESERVE_SOC_SLACK_PENALTY_CZK_PER_WH 55.0 1.0 -11.7 3 ano
NEG_EVENING_RESERVE_SOC_SLACK_PENALTY_CZK_PER_WH 55.0 2.2 -31.7 7 ano
NEG_SELL_BAT_DUMP_SHORTFALL_PENALTY_CZK_KWH 80.0 0.0 0.0 0 NE (mrtvá?)
NEG_SELL_CURTAIL_PENALTY_CZK_KWH 1.0 0.0 0.0 0 NE (mrtvá?)
NEG_SELL_POST_DETACH_BCPV_DISCOURAGE_CZK_KWH 250.0 0.0 0.0 0 NE (mrtvá?)
NEG_SELL_PREP_HOLD_BCPV_PENALTY_CZK_KWH 60.0 0.0 0.0 0 NE (mrtvá?)
NEG_SELL_PREP_SOC_SHORTFALL_PENALTY_CZK_PER_WH 0.85 0.0 0.0 0 NE (mrtvá?)
NEG_SELL_PV_B_VENT_PENALTY_CZK_KWH 4.0 7.5 -50.9 19 ano
NEG_SELL_PV_B_VENT_PENALTY_CZK_KWH 4.0 14.8 -113.8 34 ano
NEG_SELL_PV_CHARGE_REWARD_CZK_KWH 0.8 0.0 0.0 0 NE (mrtvá?)
NEG_SELL_SOC_UNDERFILL_PENALTY_CZK_PER_WH 0.35 1.3 -539.8 13 ano
NEG_SELL_SOC_UNDERFILL_PENALTY_CZK_PER_WH 0.35 10.0 -3903.3 40 ano
NIGHT_SELF_CONSUME_IMPORT_SURCHARGE_CZK_KWH 4.0 0.3 -0.5 10 ano
PEAK_EXPORT_SHORTFALL_PENALTY_CZK_KWH 80.0 33.1 -18.0 19 ano
POS_SELL_PRE_NEG_SOC_SHORTFALL_PENALTY_CZK_PER_WH 0.3 -20.6 481.2 13 ano
PEAK_EXPORT_SHORTFALL_PENALTY_CZK_KWH 80.0 33.1 -828.0 19 ano
POS_SELL_PRE_NEG_SOC_SHORTFALL_PENALTY_CZK_PER_WH 0.3 -21.8 649.1 29 ano
PRENEG_SELL_SOC_ANCHOR_SLACK_PENALTY_CZK_PER_WH 0.2 0.0 0.0 0 NE (mrtvá?)
PRE_NEG_BATT_EXPORT_SHORTFALL_PENALTY_CZK_KWH 80.0 -2.6 0.0 23 ano
PRE_NEG_BATT_EXPORT_SHORTFALL_PENALTY_CZK_KWH 80.0 4.0 -111.6 56 ano
PRE_NEG_BUY_EMPTY_EXPORT_SHORTFALL_PENALTY_CZK_KWH 80.0 0.0 0.0 0 NE (mrtvá?)
PRE_NEG_BUY_PV_CHARGE_PENALTY_CZK_KWH 250.0 1.3 -78.4 12 ano
PRE_NEG_BUY_SOC_CEILING_SLACK_PENALTY_CZK_PER_WH 0.25 20.3 -1480.0 33 ano
PRE_NEG_BUY_PV_CHARGE_PENALTY_CZK_KWH 250.0 7.1 -45.1 24 ano
PRE_NEG_BUY_SOC_CEILING_SLACK_PENALTY_CZK_PER_WH 0.25 25.5 -2149.7 56 ano
PRE_NEG_CHARGE_PENALTY_CZK_KWH 400.0 0.0 0.0 0 NE (mrtvá?)
PRE_NEG_PV_BCPV_DISCOURAGE_CZK_KWH 90.0 0.0 0.0 0 NE (mrtvá?)
PRE_NEG_PV_EXPORT_SHORTFALL_PENALTY_CZK_KWH 55.0 0.0 0.0 0 NE (mrtvá?)
PV_CHARGE_SHORTFALL_PENALTY_CZK_KWH 120.0 0.3 -473.9 9 ano
PV_CHARGE_SHORTFALL_PENALTY_CZK_KWH 120.0 0.3 -2182.6 9 ano
Mrtvé penalty (žádný vliv na 4 fixtures): 16
- CURTAILMENT_PENALTY