fix KV1/BA81 cyklovani
Some checks failed
CI and deploy / migration-check (push) Failing after 17s
CI and deploy / deploy (push) Has been skipped

This commit is contained in:
Dusan Vojacek
2026-05-06 12:50:05 +02:00
parent a5184ec42f
commit 64327af8e0
3 changed files with 202 additions and 7 deletions

View File

@@ -621,14 +621,30 @@ def solve_dispatch(
daytime_en = bool(getattr(battery, "planner_daytime_charge_target_enabled", True))
safety_pen_czk_per_wh: list[float] = []
safety_vars: list[Optional[pulp.LpVariable]] = []
safety_active: list[bool] = []
high_sell_slot: list[bool] = []
for t in range(T):
sft = slots[t].safety_soc_target_wh if daytime_en else None
# High-sell slot: typicky lokální maximum v SQL lookaheadu (future_sell_opportunity_czk_kwh).
# V těchto slotech safety floor nepoužijeme, aby se zachovala arbitráž na špičkách.
fso = slots[t].future_sell_opportunity_czk_kwh
hs = bool(fso is not None and float(slots[t].sell_price) >= float(fso) - 1e-6)
high_sell_slot.append(hs)
fb = float(slots[t].future_avoided_buy_czk_kwh or slots[t].buy_price)
fs = float(slots[t].future_sell_opportunity_czk_kwh or slots[t].sell_price)
bv = max(fb, fs) - float(degradation_cost_effective)
bv = max(0.0, min(5.0, bv))
safety_pen_czk_per_wh.append(bv / 1000.0 if sft is not None else 0.0)
if sft is not None:
# Safety deficit penalizujeme jen v PV surplus slotech, a ne ve high-sell špičce.
# Záměr: safety není obecná „nabij co nejdřív“ motivace; je to preference využít přebytek PV.
active = bool(
sft is not None
and bool(slots[t].is_daytime_pv_surplus_slot)
and not hs
)
safety_active.append(active)
safety_pen_czk_per_wh.append(bv / 1000.0 if active else 0.0)
if active:
safety_vars.append(
pulp.LpVariable(f"safety_def_{t}", 0, float(battery.usable_capacity_wh))
)
@@ -801,6 +817,17 @@ def solve_dispatch(
export_soc_floor_t = float(soc_panel_min[t])
else:
export_soc_floor_t = float(arb_base_wh)
# Safety export floor: v běžných (ne high-sell) slotech nevybít exportem energii potřebnou pro
# robustnost/noční baseload. Použije se pouze pokud je safety target v SQL vyplněný.
tgt_s = slots[t].safety_soc_target_wh if daytime_en else None
if tgt_s is not None and not high_sell_slot[t]:
export_soc_floor_t = max(
export_soc_floor_t,
min(
float(battery.soc_max_wh),
max(min_soc_wh, float(tgt_s)),
),
)
prob += soc[t] >= export_soc_floor_t - m_soc_bigm * (1 - z_export[t])
# EV limity a připojení
@@ -977,6 +1004,22 @@ def solve_dispatch(
}
)
tgt_s = st.safety_soc_target_wh if daytime_en else None
# Export floor pro debug snapshot (kopie logiky z constraintů výše).
if soc_panel_min[t] < min_soc_wh - 1e-3:
export_floor_wh = float(soc_panel_min[t])
export_floor_reason = "deep_relax"
else:
export_floor_wh = float(arb_base_wh)
export_floor_reason = "arb_base"
if tgt_s is not None and not high_sell_slot[t]:
export_floor_wh = max(
export_floor_wh,
min(
float(battery.soc_max_wh),
max(min_soc_wh, float(tgt_s)),
),
)
export_floor_reason = "safety_export_floor"
soc_bounds_snap.append(
{
"slot": st.interval_start.isoformat(),
@@ -984,6 +1027,9 @@ def solve_dispatch(
"arb_floor_wh": float(arb_floor_series[t]),
"soc_panel_min_wh": float(soc_panel_min[t]),
"safety_soc_target_wh": float(tgt_s) if tgt_s is not None else None,
"export_soc_floor_wh": float(export_floor_wh),
"export_floor_reason": export_floor_reason,
"high_sell_slot": bool(high_sell_slot[t]),
}
)
fb = float(st.future_avoided_buy_czk_kwh or st.buy_price)
@@ -1004,7 +1050,8 @@ def solve_dispatch(
st.future_sell_opportunity_czk_kwh or st.sell_price
),
"battery_value_czk_kwh": float(bv),
"safety_deficit_penalty_czk_per_wh": float(pen_wh),
"safety_deficit_penalty_czk_per_wh": float(pen_wh) if safety_active[t] else 0.0,
"safety_penalty_active": bool(safety_active[t]),
"safety_deficit_wh": sdv,
"commitment_shortfall_w": cshort,
"commitment_penalty_czk_kwh": float(commit_pen) if cshort is not None else None,
@@ -1436,7 +1483,9 @@ async def _load_previous_plan_charge_commitment_prev_w(
pva = int(r["pva"] or 0)
pvb = int(r["pvb"] or 0)
lb = int(r["lb"] or 0)
if bw > 500 and (pva + pvb) > lb and gw <= 0:
# Commitment má kotvit jen „nabíjení z PV přebytku“, ne situace kdy plán současně
# výrazně exportuje do sítě (typicky charge while exporting). To by stabilizovalo špatný cyklus.
if bw > 500 and (pva + pvb) > lb and gw <= 0 and gw >= -500:
out.append(float(bw))
else:
out.append(None)