Branch 1: failed run journal + bisect Infeasible + granulární relaxace (bez vypnutí evening push)
This commit is contained in:
@@ -1,7 +1,21 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Bisect Infeasible na reálných slotech home-01 (MCP run 16674). PYTHONPATH=backend."""
|
||||
"""Bisect Infeasible na reálných slotech home-01 (fixture z MCP).
|
||||
|
||||
Export fixture z MCP (server user-postgres-ems, nástroj query):
|
||||
|
||||
python scripts/diagnose_home01_infeasible.py --print-export-sql --run-id 23784
|
||||
|
||||
# výstup SQL vlož do MCP query; JSON ulož např.:
|
||||
# scripts/home01_run23784_slots.json
|
||||
|
||||
Spuštění bisectu:
|
||||
|
||||
PYTHONPATH=backend python scripts/diagnose_home01_infeasible.py \\
|
||||
--fixture scripts/home01_run23784_slots.json --soc-wh 51840
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
@@ -10,13 +24,42 @@ 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
|
||||
from services.planning_engine import ( # noqa: E402
|
||||
PLANNER_BUILD_TAG,
|
||||
PlanningSlot,
|
||||
SOLVER_RELAX_STEPS,
|
||||
solve_dispatch,
|
||||
solve_dispatch_two_pass,
|
||||
)
|
||||
|
||||
# Export z MCP: planning_interval run_id=16674 + fn_planning_site_context(2)
|
||||
SLOTS_JSON = Path(__file__).with_name("home01_run16706_slots.json")
|
||||
if not SLOTS_JSON.exists():
|
||||
SLOTS_JSON = Path(__file__).with_name("home01_run16674_slots.json")
|
||||
SOC_WH = 37120.0
|
||||
DEFAULT_FIXTURE = Path(__file__).with_name("home01_run16674_slots.json")
|
||||
DEFAULT_SOC_WH = 37120.0
|
||||
|
||||
|
||||
def export_slots_sql(run_id: int) -> str:
|
||||
"""SQL pro MCP export slotů z fn_load_planning_slots_full pro daný run."""
|
||||
return f"""
|
||||
select json_agg(row order by row.interval_start)
|
||||
from (
|
||||
select
|
||||
s.interval_start,
|
||||
s.buy_price::float8 as buy,
|
||||
s.sell_price::float8 as sell,
|
||||
s.load_baseline_w as load,
|
||||
s.pv_a_forecast_w as pv_a,
|
||||
s.pv_b_forecast_w as pv_b,
|
||||
s.allow_charge,
|
||||
s.allow_discharge_export
|
||||
from ems.planning_run pr
|
||||
cross join lateral ems.fn_load_planning_slots_full(
|
||||
pr.site_id,
|
||||
coalesce(pr.replan_from, pr.horizon_start),
|
||||
pr.horizon_end,
|
||||
coalesce(pr.soc_at_replan_wh, 0)
|
||||
) s
|
||||
where pr.id = {run_id}
|
||||
) row;
|
||||
""".strip()
|
||||
|
||||
|
||||
def _ctx() -> tuple[SimpleNamespace, SimpleNamespace, SimpleNamespace, list]:
|
||||
@@ -39,12 +82,16 @@ def _ctx() -> tuple[SimpleNamespace, SimpleNamespace, SimpleNamespace, list]:
|
||||
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=10.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 = [
|
||||
@@ -54,12 +101,27 @@ def _ctx() -> tuple[SimpleNamespace, SimpleNamespace, SimpleNamespace, list]:
|
||||
return battery, hp, grid, vehicles
|
||||
|
||||
|
||||
def load_slots(*, permissive_masks: bool) -> list[PlanningSlot]:
|
||||
rows = json.loads(SLOTS_JSON.read_text())
|
||||
def load_slots(
|
||||
rows: list[dict],
|
||||
*,
|
||||
permissive_masks: bool,
|
||||
use_row_masks: bool,
|
||||
) -> list[PlanningSlot]:
|
||||
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"]))
|
||||
if permissive_masks:
|
||||
allow_charge = True
|
||||
allow_discharge_export = True
|
||||
elif use_row_masks and "allow_charge" in r:
|
||||
allow_charge = bool(r.get("allow_charge"))
|
||||
allow_discharge_export = bool(r.get("allow_discharge_export", True))
|
||||
else:
|
||||
allow_charge = float(r["buy"]) < 0 or (
|
||||
float(r["sell"]) < 0 and pv_surplus > 500
|
||||
)
|
||||
allow_discharge_export = float(r["sell"]) >= 0
|
||||
out.append(
|
||||
PlanningSlot(
|
||||
interval_start=ts,
|
||||
@@ -70,53 +132,156 @@ def load_slots(*, permissive_masks: bool) -> list[PlanningSlot]:
|
||||
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,
|
||||
allow_charge=allow_charge,
|
||||
allow_discharge_export=allow_discharge_export,
|
||||
)
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def try_solve(label: str, slots: list[PlanningSlot], **kwargs) -> str:
|
||||
def try_solve(
|
||||
label: str,
|
||||
slots: list[PlanningSlot],
|
||||
soc_wh: float,
|
||||
**kwargs,
|
||||
) -> tuple[str, dict | None]:
|
||||
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,
|
||||
_results, _ms, snap = 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,
|
||||
_results, _ms, snap = solve_dispatch(
|
||||
slots,
|
||||
battery,
|
||||
hp,
|
||||
grid,
|
||||
[None, None],
|
||||
vehicles,
|
||||
soc_wh,
|
||||
55.0,
|
||||
operating_mode="AUTO",
|
||||
**kwargs,
|
||||
)
|
||||
return f"OK {label}"
|
||||
inp = snap.get("inputs") or {}
|
||||
relax = inp.get("relax_chain") or []
|
||||
push = len(inp.get("evening_push_ts") or [])
|
||||
suppressed = inp.get("evening_push_hard_suppressed")
|
||||
return (
|
||||
f"OK {label} relax={relax[-1] if relax else 'strict'} "
|
||||
f"push_slots={push} hard_suppressed={suppressed}",
|
||||
inp,
|
||||
)
|
||||
except Exception as e:
|
||||
return f"FAIL {label}: {e}"
|
||||
return f"FAIL {label}: {e}", None
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if not SLOTS_JSON.exists():
|
||||
print(f"Chybí {SLOTS_JSON} — spusť export z MCP (run 16674).", file=sys.stderr)
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--fixture", type=Path, default=DEFAULT_FIXTURE)
|
||||
parser.add_argument("--soc-wh", type=float, default=DEFAULT_SOC_WH)
|
||||
parser.add_argument("--run-id", type=int, help="MCP run id pro --print-export-sql")
|
||||
parser.add_argument(
|
||||
"--print-export-sql",
|
||||
action="store_true",
|
||||
help="Vytiskne SQL pro export slotů z MCP",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.print_export_sql:
|
||||
run_id = args.run_id or 23784
|
||||
print(export_slots_sql(run_id))
|
||||
print(
|
||||
f"\n-- Ulož json_agg výsledek do {args.fixture.name} "
|
||||
f"(nebo jiné cesty přes --fixture).",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return
|
||||
|
||||
fixture = args.fixture
|
||||
if not fixture.exists():
|
||||
print(
|
||||
f"Chybí {fixture}. Spusť:\n"
|
||||
f" python {Path(__file__).name} --print-export-sql --run-id 23784",
|
||||
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)
|
||||
rows = json.loads(fixture.read_text())
|
||||
neg_buy = [r for r in rows if r["buy"] < 0]
|
||||
neg_sell = [r for r in rows if r["sell"] < 0]
|
||||
|
||||
cases = [
|
||||
print("tag", PLANNER_BUILD_TAG)
|
||||
print("fixture", fixture)
|
||||
print("slots", len(rows))
|
||||
print("soc_wh", args.soc_wh)
|
||||
print("neg_buy slots", len(neg_buy), "first", neg_buy[0]["interval_start"] if neg_buy else None)
|
||||
print("neg_sell slots", len(neg_sell), "first", neg_sell[0]["interval_start"] if neg_sell else None)
|
||||
print("relax steps", list(SOLVER_RELAX_STEPS))
|
||||
print()
|
||||
|
||||
cases: list[tuple[str, dict]] = [
|
||||
("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)),
|
||||
("realistic masks, 1-pass auto-retry", dict(permissive_masks=False, two_pass=False)),
|
||||
("realistic masks, 2-pass auto-retry", dict(permissive_masks=False, two_pass=True)),
|
||||
("realistic + row masks from fixture", dict(use_row_masks=True, two_pass=False)),
|
||||
("strict only (no auto retry)", dict(
|
||||
permissive_masks=False,
|
||||
relaxed_expensive_import=False,
|
||||
relaxed_neg_buy_charge=False,
|
||||
relaxed_neg_prep_hold_only=False,
|
||||
relaxed_neg_prep_window=False,
|
||||
neg_sell_phases_fallback=False,
|
||||
)),
|
||||
("+ relaxed_expensive_import", dict(
|
||||
permissive_masks=False,
|
||||
relaxed_expensive_import=True,
|
||||
)),
|
||||
("+ relaxed_neg_buy_charge", dict(
|
||||
permissive_masks=False,
|
||||
relaxed_expensive_import=True,
|
||||
relaxed_neg_buy_charge=True,
|
||||
)),
|
||||
("+ relaxed_neg_prep_hold_only (evening push kept)", dict(
|
||||
permissive_masks=False,
|
||||
relaxed_expensive_import=True,
|
||||
relaxed_neg_buy_charge=True,
|
||||
relaxed_neg_prep_hold_only=True,
|
||||
)),
|
||||
("+ relaxed_neg_prep_window (full prep relax)", dict(
|
||||
permissive_masks=False,
|
||||
relaxed_expensive_import=True,
|
||||
relaxed_neg_buy_charge=True,
|
||||
relaxed_neg_prep_hold_only=True,
|
||||
relaxed_neg_prep_window=True,
|
||||
)),
|
||||
("+ neg_sell_phases_fallback", dict(
|
||||
permissive_masks=False,
|
||||
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,
|
||||
)),
|
||||
]
|
||||
|
||||
for label, kw in cases:
|
||||
masks = kw.pop("permissive_masks")
|
||||
slots = load_slots(permissive_masks=masks)
|
||||
print(try_solve(label, slots, **kw))
|
||||
permissive = kw.pop("permissive_masks", False)
|
||||
use_row_masks = kw.pop("use_row_masks", False)
|
||||
slots = load_slots(rows, permissive_masks=permissive, use_row_masks=use_row_masks)
|
||||
msg, _inp = try_solve(label, slots, args.soc_wh, **kw)
|
||||
print(msg)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user