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:
115
scripts/harness/clean_core_prototype.py
Normal file
115
scripts/harness/clean_core_prototype.py
Normal 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()
|
||||||
@@ -1,35 +1,35 @@
|
|||||||
# Δcashflow: záporné = plán bez penalty vydělá víc (modelované Kč za horizonty fixtures)
|
# Δ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
|
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á?)
|
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á?)
|
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_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_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_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_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_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_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_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_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
|
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
|
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 -20.6 481.2 13 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á?)
|
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_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_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 20.3 -1480.0 33 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_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_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á?)
|
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
|
Mrtvé penalty (žádný vliv na 4 fixtures): 16
|
||||||
- CURTAILMENT_PENALTY
|
- CURTAILMENT_PENALTY
|
||||||
|
|||||||
Reference in New Issue
Block a user