#!/usr/bin/env python3 """ Fáze 3 – vyhodnocení solver_v2 (čisté jádro) proti v1 na golden fixtures. Replay STEJNOU cestou jako golden gate (_load_site_context + _load_slots nad FixtureDB), ale přes services.planning.solver_v2.solve_dispatch_v2. Porovnání s golden snapshoty v1 (SoC-fér: koncový SoC obou oceněn terminal cenou v2). Spouštět z backend/: python3 ../scripts/harness/solver_v2_eval.py """ from __future__ import annotations import asyncio import importlib.util import json import sys from datetime import datetime 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 solver_v2 as v2 # noqa: E402 _spec = importlib.util.spec_from_file_location( "golden_replay", BACKEND / "tests" / "test_golden_replay.py" ) _golden = importlib.util.module_from_spec(_spec) sys.modules["golden_replay"] = _golden _spec.loader.exec_module(_golden) FIXTURES = sorted((BACKEND / "tests" / "golden" / "fixtures").glob("*.json")) SNAPSHOTS = BACKEND / "tests" / "golden" / "snapshots" def _replay_v2(fixture: dict): async def _run(): db = _golden._FixtureDB(fixture) meta = fixture["meta"] (battery, heat_pump, grid, vehicles, ev_sessions, soc_wh, tuv_temp, operating_mode, tuv_stats) = await pe._load_site_context(int(meta["site_id"]), db) slots = await pe._load_slots( int(meta["site_id"]), datetime.fromisoformat(meta["window_from"]), datetime.fromisoformat(meta["window_to"]), db, soc_wh=soc_wh, ) results, ms, snap = v2.solve_dispatch_v2( slots, battery, heat_pump, grid, ev_sessions, vehicles, soc_wh, tuv_temp, tuv_delta_stats=tuv_stats, operating_mode=operating_mode or "AUTO", ) return results, ms, snap, battery return asyncio.run(_run()) def main() -> None: header = f"{'fixture':<42} {'v1':>9} {'v2':>9} {'Δ':>8} {'v2 ms':>6} pozn." print("# solver_v2 vs v1 — modelovaný cashflow, SoC-fér (Kč/horizont; Δ<0 = v2 lepší)") print() print(header) print("-" * len(header)) tot1 = tot2 = 0.0 solved_both = 0 for path in FIXTURES: fixture = json.loads(path.read_text(encoding="utf-8")) try: results, ms, snap, battery = _replay_v2(fixture) except Exception as exc: print(f"{path.stem:<42} {'?':>9} {'CHYBA':>9} {'—':>8} {exc}") continue usable = float(battery.usable_capacity_wh) term = float(snap["inputs"]["terminal_czk_per_wh"]) v2_cash = sum(r.cashflow_czk for r in results) v2_soc_end = results[-1].battery_soc_target / 100.0 * usable v2_adj = v2_cash - v2_soc_end * term snap1 = json.loads((SNAPSHOTS / path.name).read_text(encoding="utf-8")) if "solver_error" in snap1: print(f"{path.stem:<42} {'INFEAS':>9} {v2_adj:>9.1f} {'—':>8} {ms:>6} v1 selhal, v2 OK") continue v1_cash = snap1["totals"]["cashflow_czk"] v1_soc_end = snap1["slots"][-1]["battery_soc_target"] / 100.0 * usable v1_adj = v1_cash - v1_soc_end * term d = v2_adj - v1_adj tot1 += v1_adj tot2 += v2_adj solved_both += 1 print(f"{path.stem:<42} {v1_adj:>9.1f} {v2_adj:>9.1f} {d:>8.1f} {ms:>6}") print("-" * len(header)) if solved_both: print(f"{'CELKEM (oba řešitelné)':<42} {tot1:>9.1f} {tot2:>9.1f} {tot2 - tot1:>8.1f}") if __name__ == "__main__": main()