#!/usr/bin/env python3 """Bisect Infeasible na reálných slotech home-01 (MCP run 16674). PYTHONPATH=backend.""" from __future__ import annotations import json import sys from datetime import datetime, timezone from pathlib import Path from types import SimpleNamespace sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "backend")) from services.planning_engine import PlanningSlot, solve_dispatch, solve_dispatch_two_pass, PLANNER_BUILD_TAG # Export z MCP: planning_interval run_id=16674 + fn_planning_site_context(2) SLOTS_JSON = Path(__file__).with_name("home01_run16674_slots.json") SOC_WH = 37120.0 def _ctx() -> tuple[SimpleNamespace, SimpleNamespace, SimpleNamespace, list]: battery = SimpleNamespace( usable_capacity_wh=64000.0, min_soc_wh=6400.0, arb_floor_wh=12800.0, reserve_soc_wh=12800.0, soc_max_wh=64000.0, charge_efficiency=0.95, discharge_efficiency=0.95, degradation_cost_czk_kwh=0.15, max_charge_power_w=18000, max_discharge_power_w=18000, charge_slot_buffer=1.3, discharge_slot_buffer=1.5, planner_terminal_soc_value_factor=0.9, planner_discharge_floor_percent=5.0, planner_extreme_buy_threshold_czk_kwh=-2.0, planner_daytime_charge_target_enabled=True, planner_charge_commitment_penalty_czk_kwh=0.2, planner_night_baseload_buffer_percent=20, ) grid = SimpleNamespace( max_import_power_w=17000, max_export_power_w=13500, block_export_on_negative_sell=False, deye_gen_microinverter_cutoff_enabled=False, ) hp = SimpleNamespace(rated_heating_power_w=0, tuv_min_temp_c=45.0, tuv_target_temp_c=55.0) vehicles = [ SimpleNamespace(max_charge_power_w=11000, battery_capacity_kwh=75.0, default_target_soc_pct=80.0), SimpleNamespace(max_charge_power_w=7400, battery_capacity_kwh=52.0, default_target_soc_pct=90.0), ] return battery, hp, grid, vehicles def load_slots(*, permissive_masks: bool) -> list[PlanningSlot]: rows = json.loads(SLOTS_JSON.read_text()) out: list[PlanningSlot] = [] for r in rows: ts = datetime.fromisoformat(r["interval_start"].replace("Z", "+00:00")) pv_surplus = max(0, int(r["pv_a"]) + int(r["pv_b"]) - int(r["load"])) out.append( PlanningSlot( interval_start=ts, buy_price=float(r["buy"]), sell_price=float(r["sell"]), pv_a_forecast_w=int(r["pv_a"]), pv_b_forecast_w=int(r["pv_b"]), load_baseline_w=int(r["load"]), ev1_connected=False, ev2_connected=False, allow_charge=True if permissive_masks else (float(r["buy"]) < 0 or (float(r["sell"]) < 0 and pv_surplus > 500)), allow_discharge_export=permissive_masks, ) ) return out def try_solve(label: str, slots: list[PlanningSlot], **kwargs) -> str: battery, hp, grid, vehicles = _ctx() try: if kwargs.pop("two_pass", False): solve_dispatch_two_pass( slots, battery, hp, grid, [None, None], vehicles, SOC_WH, 55.0, operating_mode="AUTO", **kwargs, ) else: solve_dispatch( slots, battery, hp, grid, [None, None], vehicles, SOC_WH, 55.0, operating_mode="AUTO", **kwargs, ) return f"OK {label}" except Exception as e: return f"FAIL {label}: {e}" def main() -> None: if not SLOTS_JSON.exists(): print(f"Chybí {SLOTS_JSON} — spusť export z MCP (run 16674).", file=sys.stderr) sys.exit(1) print("tag", PLANNER_BUILD_TAG) print("slots", len(json.loads(SLOTS_JSON.read_text()))) neg_buy = [r for r in json.loads(SLOTS_JSON.read_text()) if r["buy"] < 0] print("neg_buy slots", len(neg_buy), "first", neg_buy[0]["interval_start"] if neg_buy else None) cases = [ ("permissive masks, 1-pass", dict(permissive_masks=True, two_pass=False)), ("permissive masks, 2-pass", dict(permissive_masks=True, two_pass=True)), ("realistic masks, 1-pass", dict(permissive_masks=False, two_pass=False)), ("realistic masks, 2-pass", dict(permissive_masks=False, two_pass=True)), ("realistic + relaxed_expensive", dict(permissive_masks=False, two_pass=False, relaxed_expensive_import=True)), ("realistic + both relaxed", dict(permissive_masks=False, two_pass=False, relaxed_expensive_import=True, relaxed_neg_buy_pressure=True)), ] for label, kw in cases: masks = kw.pop("permissive_masks") slots = load_slots(permissive_masks=masks) print(try_solve(label, slots, **kw)) if __name__ == "__main__": main()