fix KV1/BA81 cyklovani
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user