dalsi fix
This commit is contained in:
@@ -71,7 +71,7 @@ NEG_BUY_CHARGE_SHORTFALL_PENALTY_CZK_KWH = 100.0
|
||||
PRE_NEG_CHARGE_PENALTY_CZK_KWH = 400.0
|
||||
PRE_NEG_BATT_EXPORT_SHORTFALL_PENALTY_CZK_KWH = 80.0
|
||||
PRE_NEG_BATT_EXPORT_MIN_SELL_CZK_KWH = 1.0
|
||||
PLANNER_BUILD_TAG = "2026-06-06-ba81-gen-cutoff-exec-v1"
|
||||
PLANNER_BUILD_TAG = "2026-06-06-home01-late-replan-infeasible-v1"
|
||||
SOLVER_RELAX_STEPS: tuple[str, ...] = (
|
||||
"strict",
|
||||
"relaxed_expensive_import",
|
||||
@@ -79,6 +79,7 @@ SOLVER_RELAX_STEPS: tuple[str, ...] = (
|
||||
"relaxed_neg_prep_hold_only",
|
||||
"relaxed_neg_prep_window",
|
||||
"neg_sell_phases_fallback",
|
||||
"relaxed_pos_sell_ge_block",
|
||||
)
|
||||
# Ranní slabá FVE: neaplikovat pv_store ge_pv=0 (jinak curtail při sell < večerní peak).
|
||||
DAWN_LOW_PV_NO_CURTAIL_W = 1500
|
||||
@@ -149,6 +150,7 @@ def _solver_relax_chain(
|
||||
relaxed_neg_prep_hold_only: bool = False,
|
||||
relaxed_neg_prep_window: bool = False,
|
||||
neg_sell_phases_fallback: bool = False,
|
||||
relaxed_pos_sell_ge_block: bool = False,
|
||||
) -> list[str]:
|
||||
flags = {
|
||||
"relaxed_expensive_import": relaxed_expensive_import,
|
||||
@@ -156,6 +158,7 @@ def _solver_relax_chain(
|
||||
"relaxed_neg_prep_hold_only": relaxed_neg_prep_hold_only,
|
||||
"relaxed_neg_prep_window": relaxed_neg_prep_window,
|
||||
"neg_sell_phases_fallback": neg_sell_phases_fallback,
|
||||
"relaxed_pos_sell_ge_block": relaxed_pos_sell_ge_block,
|
||||
}
|
||||
chain = [SOLVER_RELAX_STEPS[0]]
|
||||
for step in SOLVER_RELAX_STEPS[1:]:
|
||||
@@ -2525,6 +2528,7 @@ def solve_dispatch(
|
||||
relaxed_neg_prep_hold_only: bool = False,
|
||||
relaxed_neg_prep_window: bool = False,
|
||||
neg_sell_phases_fallback: bool = False,
|
||||
relaxed_pos_sell_ge_block: bool = False,
|
||||
evening_push_ts_override: Optional[set[int]] = None,
|
||||
) -> tuple[list[DispatchResult], int, dict[str, Any]]:
|
||||
"""
|
||||
@@ -2534,6 +2538,7 @@ def solve_dispatch(
|
||||
relaxed_neg_buy_charge: druhý nouzový retry bez neg_buy charge shortfall.
|
||||
relaxed_neg_prep_hold_only: třetí retry — bez prep_soc_shortfall a prep hold binárek (evening push zůstává).
|
||||
relaxed_neg_prep_window: čtvrtý retry — vypne strict pre-neg bundle; future_neg_buy večerní export zůstává.
|
||||
relaxed_pos_sell_ge_block: poslední retry — neaplikovat ge=0 v pos_sell před buy<0 (zbylá Infeasible).
|
||||
"""
|
||||
T = len(slots)
|
||||
if T < 1:
|
||||
@@ -2543,6 +2548,7 @@ def solve_dispatch(
|
||||
or relaxed_neg_buy_charge
|
||||
or relaxed_neg_prep_window
|
||||
or neg_sell_phases_fallback
|
||||
or relaxed_pos_sell_ge_block
|
||||
)
|
||||
prep_hold_relaxed = relaxed_neg_prep_hold_only or relaxed_neg_prep_window
|
||||
EV = len(vehicles) # počet EV (typicky 2)
|
||||
@@ -3085,6 +3091,36 @@ def solve_dispatch(
|
||||
battery_export_defer_pv_ts = {
|
||||
t for t in range(T) if _battery_export_push_defer_to_pv(slots[t])
|
||||
}
|
||||
# Pozdní replan večer: SQL allow_charge může být false (drahý buy), ale večerní vývoz
|
||||
# k reserve před neg dnem vyžaduje souběžně grid import pro load (ne jen bd).
|
||||
if neg_evening_discharge_active or evening_push_ts:
|
||||
replan_day = _prague_calendar_date(slots[0])
|
||||
for t in range(T):
|
||||
if _prague_calendar_date(slots[t]) != replan_day:
|
||||
continue
|
||||
if float(slots[t].sell_price) < 0.0:
|
||||
continue
|
||||
if (
|
||||
t in evening_push_ts
|
||||
or t in neg_evening_push_ts
|
||||
or (
|
||||
_in_evening_push_hour_window(slots[t])
|
||||
and t in discharge_export_slots
|
||||
)
|
||||
):
|
||||
charge_slots.add(t)
|
||||
if neg_evening_discharge_active:
|
||||
for t in discharge_export_slots:
|
||||
if _prague_calendar_date(slots[t]) == replan_day:
|
||||
charge_slots.add(t)
|
||||
if relaxed_pos_sell_ge_block:
|
||||
# Poslední retry: SQL allow_charge / drahý import nesmí zablokovat fyzicky dosažitelný plán.
|
||||
charge_slots = set(range(T))
|
||||
discharge_export_slots = {
|
||||
t
|
||||
for t, s in enumerate(slots)
|
||||
if s.allow_discharge_export or float(s.sell_price) >= 0.0
|
||||
}
|
||||
else:
|
||||
battery_export_defer_pv_ts = set()
|
||||
pre_neg_buy_soc_ceiling_wh = _pre_neg_buy_soc_ceiling_wh(
|
||||
@@ -4174,6 +4210,7 @@ def solve_dispatch(
|
||||
and first_neg_buy_idx > 0
|
||||
and t in pos_sell_pre_neg_buy_ts
|
||||
and t not in pos_sell_pre_neg_buy_ge_exempt_ts
|
||||
and not relaxed_pos_sell_ge_block
|
||||
):
|
||||
prob += ge[t] == 0
|
||||
prob += ge_pv[t] == 0
|
||||
@@ -4551,6 +4588,38 @@ def solve_dispatch(
|
||||
neg_sell_phases_fallback=True,
|
||||
evening_push_ts_override=evening_push_ts_override,
|
||||
)
|
||||
if not relaxed_pos_sell_ge_block:
|
||||
logger.warning(
|
||||
"solve_dispatch still Infeasible, retry with relaxed_pos_sell_ge_block "
|
||||
"(no ge=0 on pos_sell before buy<0)"
|
||||
)
|
||||
battery_no_phases = SimpleNamespace(
|
||||
**{
|
||||
**vars(battery),
|
||||
"planner_neg_sell_prep_soc_percent": 100.0,
|
||||
}
|
||||
)
|
||||
return solve_dispatch(
|
||||
slots,
|
||||
battery_no_phases,
|
||||
heat_pump,
|
||||
grid,
|
||||
ev_sessions,
|
||||
vehicles,
|
||||
current_soc_wh,
|
||||
current_tuv_temp_c,
|
||||
tuv_delta_stats=tuv_delta_stats,
|
||||
operating_mode=operating_mode,
|
||||
charge_commitment_prev_w=charge_commitment_prev_w,
|
||||
planner_version=planner_version,
|
||||
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,
|
||||
relaxed_pos_sell_ge_block=True,
|
||||
evening_push_ts_override=evening_push_ts_override,
|
||||
)
|
||||
raise PlannerSolverError(
|
||||
pulp.LpStatus[status],
|
||||
relax_chain=_solver_relax_chain(
|
||||
@@ -4559,6 +4628,7 @@ def solve_dispatch(
|
||||
relaxed_neg_prep_hold_only=relaxed_neg_prep_hold_only,
|
||||
relaxed_neg_prep_window=relaxed_neg_prep_window,
|
||||
neg_sell_phases_fallback=neg_sell_phases_fallback,
|
||||
relaxed_pos_sell_ge_block=relaxed_pos_sell_ge_block,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -4910,12 +4980,14 @@ def solve_dispatch(
|
||||
"relaxed_neg_prep_hold_only": relaxed_neg_prep_hold_only,
|
||||
"relaxed_neg_prep_window": relaxed_neg_prep_window,
|
||||
"neg_sell_phases_fallback": neg_sell_phases_fallback,
|
||||
"relaxed_pos_sell_ge_block": relaxed_pos_sell_ge_block,
|
||||
"relax_chain": _solver_relax_chain(
|
||||
relaxed_expensive_import=relaxed_expensive_import,
|
||||
relaxed_neg_buy_charge=relaxed_neg_buy_charge,
|
||||
relaxed_neg_prep_hold_only=relaxed_neg_prep_hold_only,
|
||||
relaxed_neg_prep_window=relaxed_neg_prep_window,
|
||||
neg_sell_phases_fallback=neg_sell_phases_fallback,
|
||||
relaxed_pos_sell_ge_block=relaxed_pos_sell_ge_block,
|
||||
),
|
||||
"charge_acquisition_buy_czk_kwh": charge_acquisition_czk_kwh,
|
||||
"charge_acquisition_cutoff_at": (
|
||||
|
||||
@@ -3615,6 +3615,88 @@ class ChargeAcquisitionArbitrageTests(unittest.TestCase):
|
||||
self.assertFalse(snap_mid["inputs"].get("neg_sell_phases_fallback"))
|
||||
self.assertLess(results_mid[0].grid_setpoint_w, -1000)
|
||||
|
||||
def test_home01_late_replan_high_soc_realistic_masks(self) -> None:
|
||||
"""Pozdní replan večer (21:00): SoC ~74 %, SQL masky allow_charge=false večer — musí být Feasible."""
|
||||
prague = ZoneInfo("Europe/Prague")
|
||||
base = datetime(2026, 6, 6, 21, 0, tzinfo=prague).astimezone(timezone.utc)
|
||||
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),
|
||||
]
|
||||
slots: list[PlanningSlot] = []
|
||||
for i in range(96):
|
||||
local = (base + timedelta(minutes=15 * i)).astimezone(prague)
|
||||
if i < len(rows):
|
||||
buy, sell, load, pv_a, pv_b = rows[i]
|
||||
else:
|
||||
d, h, m = local.day, local.hour, local.minute
|
||||
hm = h + m / 60.0
|
||||
if d == 6:
|
||||
buy, sell, load, pv_a, pv_b = 4.5, 2.9, 800, 0, 0
|
||||
elif hm >= 5.75 and hm < 15:
|
||||
sell, buy = -0.3, 0.5
|
||||
pv_a, pv_b, load = 2000, 2500, 800
|
||||
elif 11 <= hm < 14:
|
||||
sell, buy = -0.8, -0.4
|
||||
pv_a, pv_b, load = 4000, 4500, 2000
|
||||
else:
|
||||
sell, buy = 2.5, 3.0
|
||||
pv_a, pv_b, load = 200, 200, 500
|
||||
pv_surplus = max(0, pv_a + pv_b - load)
|
||||
h = local.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)
|
||||
slots.append(
|
||||
PlanningSlot(
|
||||
interval_start=base + timedelta(minutes=15 * i),
|
||||
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,
|
||||
)
|
||||
)
|
||||
bat = _battery(uc_wh=64_000.0, arb_pct=20.0, terminal_soc_value_factor=0.9)
|
||||
bat.planner_neg_sell_prep_soc_percent = 80
|
||||
hp = SimpleNamespace(rated_heating_power_w=0, tuv_min_temp_c=45.0, tuv_target_temp_c=55.0)
|
||||
grid = SimpleNamespace(
|
||||
max_import_power_w=17_000,
|
||||
max_export_power_w=13_500,
|
||||
block_export_on_negative_sell=False,
|
||||
purchase_pricing_mode="spot",
|
||||
)
|
||||
vehicles = [
|
||||
SimpleNamespace(max_charge_power_w=0, battery_capacity_kwh=1.0, default_target_soc_pct=80.0),
|
||||
SimpleNamespace(max_charge_power_w=0, battery_capacity_kwh=1.0, default_target_soc_pct=80.0),
|
||||
]
|
||||
results, _ms, snap = solve_dispatch_two_pass(
|
||||
slots,
|
||||
bat,
|
||||
hp,
|
||||
grid,
|
||||
[None, None],
|
||||
vehicles,
|
||||
0.74 * bat.soc_max_wh,
|
||||
50.0,
|
||||
operating_mode="AUTO",
|
||||
)
|
||||
self.assertLess(results[0].grid_setpoint_w, -500)
|
||||
self.assertLess(results[0].battery_soc_target, 70.0)
|
||||
|
||||
def test_kv1_evening_push_profitable_vs_morning_zone_peak(self) -> None:
|
||||
"""v52: KV1 večer ≥ ranní max (5–11) − degrad; pod prahem ne."""
|
||||
prague = ZoneInfo("Europe/Prague")
|
||||
|
||||
@@ -5,6 +5,25 @@ Formát: **datum (ISO)** · stručný důvod · soubory · chování / ověřen
|
||||
|
||||
---
|
||||
|
||||
## 2026-06-06 — Pozdní replan večer: Infeasible při vysokém SoC (home-01)
|
||||
|
||||
**Problém:** Po přepnutí na AUTO a ručním replanem (~21:00, SoC **~74 %**, zítra `buy<0` + `sell<0`): všechny retry včetně `neg_sell_phases_fallback` → **`Solver: Infeasible`**. Aktivní zůstal starý plán z 17:00 (import místo večerního vývozu k **reserve ~20 %**).
|
||||
|
||||
**Příčina:** SQL maska `allow_charge=false` ve večerních slotech (drahý `buy`, `sell` < `buy`) + guard drahého importu vyžadoval baseload z baterie (`bd`), zatímco **v64 `future_neg_buy_discharge`** současně vynucoval večerní vývoz — LP bez rozšíření `charge_slots` neměl řešení.
|
||||
|
||||
**Oprava (tag `2026-06-06-home01-late-replan-infeasible-v1`):**
|
||||
- Při **`future_neg_buy_discharge`**: rozšířit `charge_slots` o večerní / exportní sloty dne replanu (grid smí krmit load během vývozu).
|
||||
- Nový poslední retry **`relaxed_pos_sell_ge_block`** (+ nouzové rozšíření masek) v `SOLVER_RELAX_STEPS`.
|
||||
|
||||
**Soubory:** `planning_engine.py`, `scripts/repro_home01_23840.py`, test `test_home01_late_replan_high_soc_realistic_masks`.
|
||||
|
||||
**Ověření:**
|
||||
- `PYTHONPATH=backend python3 scripts/repro_home01_23840.py` → `OK two_pass`
|
||||
- `pytest backend/tests/test_planning_dispatch_milp.py -k home01_late_replan`
|
||||
- Po deployi: ruční replan v AUTO → `planning_run.status=active`, večerní sloty `grid_setpoint_w < 0`.
|
||||
|
||||
---
|
||||
|
||||
## 2026-06-06 — BA81 GEN cut-off exekuce při sell<0 (Branch 4)
|
||||
|
||||
**Problém:** Audit BA81 6. 6. 2026 (07:45–08:30, `sell<0`): plán `grid_setpoint_w=0`, `deye_gen_cutoff_enabled=false`, ale **`actual_grid_export_wh` > 0** a **`flow_pv_to_grid_wh` > 0** (~0,8–1 kW). Reg **145** (`export_ban`) nestačí — mikroinvertory na GEN portu exportují, dokud reg **178** bits 0–1 ≠ cut-off ON.
|
||||
|
||||
269
scripts/repro_home01_23840.py
Normal file
269
scripts/repro_home01_23840.py
Normal file
@@ -0,0 +1,269 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user