Files
ems/scripts/harness/penalty_audit.py
Dusan Vojacek 9a2229641d Fáze 2.1: 4 zastaralé testy → expectedFailure; +2 fixtures vč. Infeasible reproduceru
Analýza (agent + ručně): všechny 4 failující testy vynucují heuristické chování
před retry-chain v5; současné chování je ekonomicky správné nebo jde o korektní
fallback. Scénáře zachovány s @unittest.expectedFailure + zdůvodněním —
přepsat na ekonomické asserty ve Fázi 3. Suite: 120 passed, 4 xfailed.

Nové golden fixtures home-01: 2026-05-01 extreme_neg_buy (buy −13.26;
ZACHYCENO: solver Infeasible po celém relax řetězci — zmrazeno jako golden
failure snapshot), 2026-05-25 evening_push. Golden replay i penalty audit
umí solver_error výsledky (penalta měnící feasibility se zviditelní).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 13:56:12 +02:00

162 lines
5.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Fáze 2 penalty audit: změř přínos každé heuristické penalty v objective.
Pro každou ekonomickou konstantu (penalty/reward/bonus/surcharge) ji vynuluje
ve VŠECH modulech, kde je importovaná (fasáda: planning_engine, planning.heuristics,
planning.constants), přehraje golden fixtures (tests/golden/fixtures/) přes
solve_dispatch_two_pass a porovná s baseline:
- Δcashflow : změna reálných peněz plánu ( = plán vydělá víc bez penalty),
- Δslotů : kolik slotů změnilo battery/grid setpoint (chování),
- bind : jestli penalta vůbec něco dělá (Δ=0 na všech fixtures = mrtvá).
POZOR na interpretaci: penalty mění modelované peníze za robustnost vůči chybě
forecastu. Záporná Δcashflow ⇒ penalta v modelu stojí peníze — to samo o sobě
neznamená „smazat“; mrtvé penalty (bind=NE) ale smazat lze bezpečně.
Druhý krok auditu = economics_report nad reálnými dny.
Spouštět z backend/: python3 ../scripts/harness/penalty_audit.py [--only NÁZEV]
"""
from __future__ import annotations
import argparse
import importlib.util
import json
import re
import sys
from pathlib import Path
BACKEND = Path(__file__).resolve().parents[2] / "backend"
sys.path.insert(0, str(BACKEND))
from services import planning_engine as pe # noqa: E402
from services.planning import constants as C # noqa: E402
from services.planning import heuristics as H # noqa: E402
# replay funkce z golden testu (bez duplikace logiky)
_spec = importlib.util.spec_from_file_location(
"golden_replay", BACKEND / "tests" / "test_golden_replay.py"
)
_golden = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_golden)
FIXTURES = sorted((BACKEND / "tests" / "golden" / "fixtures").glob("*.json"))
# Ekonomické konstanty k auditu: penalty/reward/bonus/surcharge/incentive/discourage
AUDIT_PATTERN = re.compile(
r"(PENALTY|SHORTFALL|DISCOURAGE|REWARD|SURCHARGE|INCENTIVE|BONUS)", re.I
)
MODULES = (pe, C, H)
def _audit_names() -> list[str]:
names = []
for n in dir(C):
if n.startswith("_") or not n.isupper():
continue
if AUDIT_PATTERN.search(n) and isinstance(getattr(C, n), (int, float)):
names.append(n)
return sorted(names)
def _set_const(name: str, value: float) -> dict:
saved = {}
for mod in MODULES:
if hasattr(mod, name):
saved[mod.__name__] = getattr(mod, name)
setattr(mod, name, value)
return saved
def _restore_const(name: str, saved: dict) -> None:
for mod in MODULES:
if mod.__name__ in saved:
setattr(mod, name, saved[mod.__name__])
def _replay_all() -> dict[str, dict]:
out = {}
for path in FIXTURES:
fixture = json.loads(path.read_text(encoding="utf-8"))
out[path.stem] = _golden._replay_fixture(fixture)
return out
def _diff(base: dict, new: dict) -> tuple[float, float, int, list[str]]:
"""(Δcashflow, Δpenalty, změněné sloty, změny feasibility) napříč fixtures."""
d_cash = d_pen = 0.0
changed = 0
feas: list[str] = []
for key, b in base.items():
n = new[key]
b_err = "solver_error" in b
n_err = "solver_error" in n
if b_err or n_err:
if b_err and not n_err:
feas.append(f"{key}: INFEASIBLE → OK!")
changed += 1
elif n_err and not b_err:
feas.append(f"{key}: OK → INFEASIBLE")
changed += 1
continue
d_cash += n["totals"]["cashflow_czk"] - b["totals"]["cashflow_czk"]
d_pen += n["totals"]["penalty_czk"] - b["totals"]["penalty_czk"]
for rb, rn in zip(b["slots"], n["slots"]):
if (
rb["battery_setpoint_w"] != rn["battery_setpoint_w"]
or rb["grid_setpoint_w"] != rn["grid_setpoint_w"]
or rb["pv_a_curtailed_w"] != rn["pv_a_curtailed_w"]
):
changed += 1
return d_cash, d_pen, changed, feas
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--only", default=None, help="audit jen jedné konstanty")
args = ap.parse_args()
names = [args.only] if args.only else _audit_names()
print(f"# Penalty audit — {len(names)} konstant × {len(FIXTURES)} fixtures")
print("# Δcashflow: záporné = plán bez penalty vydělá víc (modelované Kč za horizonty fixtures)")
print()
baseline = _replay_all()
base_cash = sum(r["totals"]["cashflow_czk"] for r in baseline.values() if "totals" in r)
base_pen = sum(r["totals"]["penalty_czk"] for r in baseline.values() if "totals" in r)
infeas = [k for k, r in baseline.items() if "solver_error" in r]
print(f"baseline: cashflow {base_cash:.1f} Kč, penalty {base_pen:.1f} Kč; infeasible fixtures: {infeas}\n")
header = f"{'konstanta':<55} {'hodnota':>9} {'Δcash':>8} {'Δpenalty':>9} {'Δsloty':>6} bind"
print(header)
print("-" * len(header))
rows = []
for name in names:
value = getattr(C, name)
saved = _set_const(name, 0.0)
try:
result = _replay_all()
except Exception as exc: # infeasible apod. — informace sama o sobě
print(f"{name:<55} {value:>9} {'ERROR':>8} {str(exc)[:40]}")
_restore_const(name, saved)
continue
_restore_const(name, saved)
d_cash, d_pen, changed, feas = _diff(baseline, result)
bind = "NE (mrtvá?)" if changed == 0 and abs(d_cash) < 0.05 else "ano"
if feas:
bind = " | ".join(feas)
rows.append((name, value, d_cash, d_pen, changed, bind))
print(f"{name:<55} {value:>9} {d_cash:>8.1f} {d_pen:>9.1f} {changed:>6} {bind}")
dead = [r[0] for r in rows if r[5].startswith("NE")]
print(f"\nMrtvé penalty (žádný vliv na 4 fixtures): {len(dead)}")
for n in dead:
print(f" - {n}")
if __name__ == "__main__":
main()