#!/usr/bin/env python3 """Repro home-01 Infeasible @ 47360 Wh, replan 2026-06-06 21:00 Prague (run 23840).""" from __future__ import annotations import sys from datetime import datetime, timedelta, timezone from pathlib import Path from types import SimpleNamespace from zoneinfo import ZoneInfo sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "backend")) from services.planning_engine import ( # noqa: E402 PlanningSlot, solve_dispatch, solve_dispatch_two_pass, PlannerSolverError, ) # Compact slot data from MCP read-only export (buy, sell, load, pv_a, pv_b) SLOT_ROWS = [ (5.305729, 3.34, 2731, 0, 0), (5.162299, 3.23125, 2731, 0, 0), (4.866865, 3.00725, 2731, 0, 0), (4.662765, 2.8525, 2731, 0, 0), (5.18406, 3.24775, 1552, 0, 0), (4.878076, 3.01575, 1552, 0, 0), (4.749483, 2.91825, 1552, 0, 0), (4.460314, 2.699, 1552, 0, 0), (4.887308, 3.02275, 782, 0, 0), (4.883351, 3.01975, 782, 0, 0), (4.660787, 2.851, 782, 0, 0), (4.484384, 2.71725, 782, 0, 0), (4.756077, 2.92325, 453, 0, 0), (4.349197, 2.61475, 453, 0, 0), (4.32117, 2.5935, 453, 0, 0), (4.276657, 2.55975, 453, 0, 0), (4.22522, 2.52075, 437, 0, 0), (4.155318, 2.46775, 437, 0, 0), (3.975289, 2.33125, 437, 0, 0), (3.76954, 2.17525, 437, 0, 0), (4.063655, 2.39825, 439, 0, 0), (3.799215, 2.19775, 439, 0, 0), (3.759978, 2.168, 439, 0, 0), (3.48235, 1.9575, 439, 0, 0), (3.762616, 2.17, 462, 0, 0), (3.479382, 1.95525, 462, 0, 0), (3.344854, 1.85325, 462, 0, 0), (3.025021, 1.61075, 462, 0, 0), (3.165814, 1.7175, 477, 0, 0), (2.988092, 1.58275, 477, 0, 0), (2.8351, 1.46675, 477, 0, 0), (2.583849, 1.27625, 477, 0, 0), (2.673864, 1.3445, 464, 10, 9), (2.195433, 0.98175, 464, 58, 60), (1.809655, 0.68925, 464, 196, 99), (0.889722, -0.00825, 464, 75, 0), (0.778934, -0.09225, 508, 19, 0), (0.673422, -0.17225, 508, 277, 47), (0.651001, -0.18925, 508, 240, 0), (0.574175, -0.2475, 508, 296, 0), (0.565602, -0.254, 899, 314, 0), (0.505263, -0.29975, 899, 767, 0), (0.504933, -0.3, 899, 800, 0), (0.504658, -0.30025, 899, 1458, 372), (0.504658, -0.30025, 795, 1989, 1058), (0.503557, -0.30125, 795, 2254, 1942), (0.501905, -0.30275, 795, 2440, 2536), (0.448502, -0.35125, 795, 1778, 1945), (1.105087, -0.3325, 1847, 3741, 4015), (1.047004, -0.38525, 1847, 4028, 4369), (0.989196, -0.43775, 1847, 4843, 5119), (0.865047, -0.5505, 1847, 5175, 5564), (0.367571, -0.42475, 1487, 5443, 5934), (0.354633, -0.4365, 1487, 5597, 6235), (0.232961, -0.547, 1487, 5764, 6484), (0.183687, -0.59175, 1487, 5917, 6607), (0.150929, -0.6215, 2039, 6030, 6797), (0.103032, -0.665, 2039, 6100, 6837), (0.11129, -0.6575, 2039, 6243, 7027), (0.103857, -0.66425, 2039, 4918, 5820), (0.728235, -0.67475, 5445, 6048, 6988), (0.61785, -0.775, 5445, 5798, 5612), (0.61785, -0.775, 5445, 4634, 5683), (0.588671, -0.8015, 5445, 4653, 5848), (-0.141137, -0.88675, 6866, 3243, 4495), (-0.322268, -1.05125, 6866, 3476, 3873), (-0.60883, -1.3115, 6866, 2855, 3385), (-0.892363, -1.569, 6866, 5859, 5852), (-0.70903, -1.4025, 3625, 6375, 6266), (-0.747568, -1.4375, 3625, 4265, 4247), (-0.721692, -1.414, 3625, 5850, 5737), (-0.739861, -1.4305, 3625, 6502, 6411), (-0.893464, -1.57, 3856, 5448, 5535), (-0.615987, -1.318, 3856, 3694, 3496), (-0.381728, -1.10525, 3856, 5591, 5503), (-0.160957, -0.90475, 3856, 3403, 3478), (0.366249, -1.0035, 4480, 3905, 4076), (0.645102, -0.75025, 4480, 4815, 4332), (0.787695, -0.62075, 4480, 4106, 3720), (1.001859, -0.42625, 4480, 2989, 2882), (0.410514, -0.38575, 3747, 3001, 3059), (0.47713, -0.32525, 3747, 2817, 3173), (0.50163, -0.303, 3747, 2227, 2956), (0.493371, -0.3105, 3747, 2487, 3203), (0.493647, -0.31025, 1282, 1581, 1978), (1.613139, 0.54025, 1282, 690, 1064), (3.859225, 2.24325, 1282, 376, 880), (4.583631, 2.7925, 1282, 392, 687), (3.208019, 1.7495, 1898, 434, 656), (4.243355, 2.5345, 1898, 557, 726), (4.562529, 2.7765, 1898, 423, 609), (4.798282, 2.95525, 1898, 156, 269), (4.912267, 2.5595, 1993, 104, 178), (5.082735, 2.68875, 1993, 42, 43), (5.263424, 2.82575, 1993, 166, 185), (5.476756, 2.9875, 1993, 58, 60), (4.772234, 2.9355, 1513, 0, 0), (4.780807, 2.942, 1513, 0, 0), (4.895551, 3.029, 1513, 0, 0), (4.893573, 3.0275, 1513, 0, 0), ] SOC_WH = 47360.0 PRAGUE = ZoneInfo("Europe/Prague") BASE = datetime(2026, 6, 6, 21, 0, tzinfo=PRAGUE).astimezone(timezone.utc) def _build_slots(*, permissive: bool, evening_export: bool) -> list[PlanningSlot]: out: list[PlanningSlot] = [] for i, (buy, sell, load, pv_a, pv_b) in enumerate(SLOT_ROWS): ts = BASE + timedelta(minutes=15 * i) pv_surplus = max(0, pv_a + pv_b - load) if permissive: allow_charge = True allow_discharge_export = True elif evening_export: h = ts.astimezone(PRAGUE).hour allow_discharge_export = sell >= 0 and (h >= 17 or sell > buy + 0.15) allow_charge = buy < 0 or (sell < 0 and pv_surplus > 500) else: allow_discharge_export = sell >= 0 and sell > 2.5 allow_charge = buy < 0 or (sell < 0 and pv_surplus > 500) out.append( PlanningSlot( interval_start=ts, buy_price=buy, sell_price=sell, pv_a_forecast_w=pv_a, pv_b_forecast_w=pv_b, load_baseline_w=load, ev1_connected=False, ev2_connected=False, allow_charge=allow_charge, allow_discharge_export=allow_discharge_export, ) ) return out 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, planner_neg_sell_prep_soc_percent=80.0, planner_neg_sell_full_soc_tail_slots=4, planner_neg_sell_vent_min_sell_czk_kwh=-0.5, ) grid = SimpleNamespace( max_import_power_w=17000, max_export_power_w=13500, block_export_on_negative_sell=False, deye_gen_microinverter_cutoff_enabled=False, purchase_pricing_mode="spot", ) 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 _try(label: str, slots: list[PlanningSlot], **kwargs) -> None: battery, hp, grid, vehicles = _ctx() try: _r, _ms, snap = solve_dispatch( slots, battery, hp, grid, [None, None], vehicles, SOC_WH, 55.0, operating_mode="AUTO", **kwargs, ) inp = snap.get("inputs") or {} print( f"OK {label}: future_neg={inp.get('future_neg_buy_discharge')} " f"push_sup={inp.get('evening_push_hard_suppressed')} " f"eve={len(inp.get('evening_push_ts') or [])} " f"neg_eve={len(inp.get('neg_evening_push_slots') or [])}" ) except PlannerSolverError as e: print(f"FAIL {label}: {e}") def main() -> None: print(f"soc={SOC_WH} slots={len(SLOT_ROWS)} start={BASE.isoformat()}") for mask_label, permissive, evening in [ ("permissive", True, False), ("evening_peak_mask", False, True), ("tight_mask", False, False), ]: slots = _build_slots(permissive=permissive, evening_export=evening) print(f"\n--- masks: {mask_label} ---") _try("strict", slots) _try( "relaxed_prep_window", slots, relaxed_expensive_import=True, relaxed_neg_buy_charge=True, relaxed_neg_prep_hold_only=True, relaxed_neg_prep_window=True, ) bat_fb, hp, grid, vehicles = _ctx() bat_fb = SimpleNamespace(**{**vars(bat_fb), "planner_neg_sell_prep_soc_percent": 100.0}) try: _r, _ms, snap = solve_dispatch( slots, bat_fb, hp, grid, [None, None], vehicles, SOC_WH, 55.0, operating_mode="AUTO", relaxed_expensive_import=True, relaxed_neg_buy_charge=True, relaxed_neg_prep_hold_only=True, relaxed_neg_prep_window=True, neg_sell_phases_fallback=True, ) inp = snap.get("inputs") or {} print( f"OK neg_sell_phases_fallback: push_sup={inp.get('evening_push_hard_suppressed')}" ) except PlannerSolverError as e: print(f"FAIL neg_sell_phases_fallback: {e}") print("\n--- two_pass (production path) ---") slots = _build_slots(permissive=False, evening_export=True) battery, hp, grid, vehicles = _ctx() try: solve_dispatch_two_pass( slots, battery, hp, grid, [None, None], vehicles, SOC_WH, 55.0, operating_mode="AUTO", ) print("OK two_pass") except PlannerSolverError as e: print(f"FAIL two_pass: {e}") if __name__ == "__main__": main()