Branch 3: charge-slot-budget v R__063 + odstranit v58 pro BA81/KV1 + fixed evening push
Some checks failed
CI and deploy / migration-check (push) Failing after 25s
CI and deploy / deploy (push) Has been skipped

This commit is contained in:
Dusan Vojacek
2026-06-06 22:32:48 +02:00
parent 09bca0a903
commit a7879f1141
7 changed files with 252 additions and 162 deletions

View File

@@ -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-future-neg-buy-evening-export-v64"
PLANNER_BUILD_TAG = "2026-06-06-charge-slot-budget-v1"
SOLVER_RELAX_STEPS: tuple[str, ...] = (
"strict",
"relaxed_expensive_import",
@@ -82,8 +82,6 @@ SOLVER_RELAX_STEPS: tuple[str, ...] = (
)
# Ranní slabá FVE: neaplikovat pv_store ge_pv=0 (jinak curtail při sell < večerní peak).
DAWN_LOW_PV_NO_CURTAIL_W = 1500
# BA81/KV1: PV→bat jen v těsné blízkosti nejnižšího sell v horizontu (≈ poledne), ne při ~3 Kč ráno.
FIXED_PV_CHARGE_ONLY_NEAR_MIN_SELL_CZK_KWH = 0.20
# Mimo evening_push: preferovat bd pro dům místo gi, když buy >> acq (účinná cena importu).
NIGHT_SELF_CONSUME_IMPORT_SURCHARGE_CZK_KWH = 4.0
# Po t_detach v prep: necpát PV do bat (měkké; tvrdý hold přes soc_target z rampy).
@@ -456,6 +454,13 @@ class PlanningSlot:
pv_charge_wh_ahead: float | None = None
neg_buy_wh_ahead: float | None = None
grid_charge_suppressed_reason: str | None = None
charge_target_wh: float | None = None
pre_window_wh: float | None = None
in_window_wh: float | None = None
charge_slot_wh: float | None = None
charge_cum_wh: float | None = None
charge_layer: str | None = None
charge_slot_reason: str | None = None
#: Pomocny atribut pro green_bonus v planning_interval (Kc/slot); lite default 0.
green_bonus_czk_per_slot: float = 0.0
@@ -1836,12 +1841,13 @@ def _slot_evening_push_profitable(
slots: list[PlanningSlot] | None = None,
first_neg_sell_idx: int | None = None,
kv1_evening_push: bool = False,
purchase_fixed: bool = False,
) -> bool:
"""
Push večerní špičky.
Spot / obecně: sell > acq+spread (zásoba z levného nabití).
KV1 (fixed + block_export, v52): sell ≥ max sell v pásmu 511 před 1. sell<0 spread
— neprodávat večer levněji než plánované ranní maximum; bez neg dne v horizontu sell ≥ 1 Kč.
Spot: sell > acq+spread (zásoba z levného nabití).
Fixní tarif (BA81/KV1): sell > buy+spread (stejně jako R__063 discharge maska).
KV1 (fixed + block_export, v52): navíc sell ≥ max sell v pásmu 511 před 1. sell<0 spread.
"""
sell_t = float(slot.sell_price)
if kv1_evening_push:
@@ -1852,6 +1858,10 @@ def _slot_evening_push_profitable(
if zone_peak is not None:
return sell_t >= float(zone_peak) - float(min_spread)
return True
if purchase_fixed:
buy_t = float(slot.buy_price)
if buy_t >= 0.0:
return sell_t > buy_t + float(min_spread)
return sell_t > float(charge_acquisition_czk_kwh) + float(min_spread)
@@ -1864,6 +1874,7 @@ def _evening_push_segment_candidates(
discharge_export_ok: set[int] | None = None,
first_neg_sell_idx: int | None = None,
kv1_evening_push: bool = False,
purchase_fixed: bool = False,
) -> list[int]:
"""Profitable sloty v nočním úseku — výběr pořadí a strop dělá rozpočet Wh (sell desc)."""
if not seg:
@@ -1881,6 +1892,7 @@ def _evening_push_segment_candidates(
slots=slots,
first_neg_sell_idx=first_neg_sell_idx,
kv1_evening_push=kv1_evening_push,
purchase_fixed=purchase_fixed,
):
continue
out.append(t)
@@ -2013,6 +2025,7 @@ def _evening_battery_export_push_indices(
evening_start_hour: int = 17,
first_neg_sell_idx: int | None = None,
kv1_evening_push: bool = False,
purchase_fixed: bool = False,
) -> list[int]:
"""
Večerní push (≥17h): plný ge_bat v nejdražších slotách (sell desc), rozpočet Wh
@@ -2049,6 +2062,7 @@ def _evening_battery_export_push_indices(
discharge_export_ok=discharge_export_ok,
first_neg_sell_idx=first_neg_sell_idx,
kv1_evening_push=kv1_evening_push,
purchase_fixed=purchase_fixed,
):
if t not in seen:
seen.add(t)
@@ -2078,6 +2092,7 @@ def _evening_push_peak_fallback_indices(
discharge_export_ok: set[int] | None,
first_neg_sell_idx: int | None,
kv1_evening_push: bool,
purchase_fixed: bool = False,
) -> set[int]:
"""Alespoň jeden večerní peak slot (sell desc), když rozpočet Wh nevybral žádný push."""
best_t: int | None = None
@@ -2094,6 +2109,7 @@ def _evening_push_peak_fallback_indices(
slots=slots,
first_neg_sell_idx=first_neg_sell_idx,
kv1_evening_push=kv1_evening_push,
purchase_fixed=purchase_fixed,
):
continue
sell_t = float(s.sell_price)
@@ -2944,6 +2960,7 @@ def solve_dispatch(
discharge_export_ok=discharge_export_slots,
first_neg_sell_idx=first_neg_sell_idx,
kv1_evening_push=kv1_evening_push_pre,
purchase_fixed=purchase_fixed_pre,
)
)
push_override_raw = _evening_push_override_for_solve(
@@ -2977,6 +2994,7 @@ def solve_dispatch(
discharge_export_ok=discharge_export_slots,
first_neg_sell_idx=first_neg_sell_idx,
kv1_evening_push=kv1_evening_push_pre,
purchase_fixed=purchase_fixed_pre,
)
# Tvrdý ge_bat push vypnout jen při neg_sell fallback (ne při prep relax — v64).
evening_push_hard_suppressed = bool(neg_sell_phases_fallback)
@@ -4216,34 +4234,8 @@ def solve_dispatch(
or fixed_pre_neg_pv_export
or fixed_block_pv_surplus_export
or fixed_mi_low_pv_surplus_export
or (
purchase_fixed_pre
and fixed_horizon_min_sell_pre is not None
and sell_t >= 0.0
and pv_surplus_w > NIGHT_EXPORT_PV_SUNRISE_SURPLUS_W
and sell_t
> fixed_horizon_min_sell_pre
+ FIXED_PV_CHARGE_ONLY_NEAR_MIN_SELL_CZK_KWH
)
)
fixed_sell_above_horizon_min = (
purchase_fixed_pre
and fixed_horizon_min_sell_pre is not None
and sell_t >= 0.0
and sell_t
> fixed_horizon_min_sell_pre + FIXED_PV_CHARGE_ONLY_NEAR_MIN_SELL_CZK_KWH
)
fixed_high_sell_no_pv_charge = (
fixed_sell_above_horizon_min
and pv_surplus_w > NIGHT_EXPORT_PV_SUNRISE_SURPLUS_W
)
fixed_grid_charge_unprofitable = (
purchase_fixed_pre
and buy_t >= 0.0
and fixed_sell_above_horizon_min
)
# Spot: mezi-slotová arbitráž — sell<buy ve slotu je normální (marže). Grid→bat jen
# když buy v tomto slotu odpovídá levnému nákupu (≤ charge_acquisition), ne 19:00 za 5,5 při acq 3,25.
# Spot: mezi-slotová arbitráž — grid→bat jen když buy ≤ charge_acquisition (v61).
spot_grid_charge_not_cheap_buy = (
not purchase_fixed_pre
and buy_t >= 0.0
@@ -4257,10 +4249,8 @@ def solve_dispatch(
and not fixed_pre_neg_pv_export
and int(s.pv_a_forecast_w) >= DAWN_LOW_PV_NO_CURTAIL_W
)
if fixed_grid_charge_unprofitable or spot_grid_charge_not_cheap_buy:
if spot_grid_charge_not_cheap_buy:
prob += bc_gi[t] == 0
if fixed_high_sell_no_pv_charge:
prob += bc_pv[t] == 0
if (
purchase_fixed_pre
and t in evening_push_ts
@@ -4773,6 +4763,25 @@ def solve_dispatch(
solver_snapshot: dict[str, Any] = {
"version": 1,
"planner_build_tag": PLANNER_BUILD_TAG,
"charge_slot_budget": {
"charge_target_wh": (
float(slots[0].charge_target_wh)
if slots[0].charge_target_wh is not None
else None
),
"pre_window_wh": (
float(slots[0].pre_window_wh)
if slots[0].pre_window_wh is not None
else None
),
"in_window_wh": (
float(slots[0].in_window_wh)
if slots[0].in_window_wh is not None
else None
),
"reliability_factor": 0.85,
"planner_build_tag": PLANNER_BUILD_TAG,
},
"inputs": {
"current_soc_wh": float(current_soc_wh),
"observed_soc_wh": float(observed_soc_wh),
@@ -4897,9 +4906,7 @@ def solve_dispatch(
and not push_override_eff
),
"fixed_horizon_min_sell_czk_kwh": fixed_horizon_min_sell_pre,
"fixed_pv_charge_near_min_sell_margin_czk_kwh": (
FIXED_PV_CHARGE_ONLY_NEAR_MIN_SELL_CZK_KWH if purchase_fixed_pre else None
),
"fixed_evening_push_sell_above_buy": bool(purchase_fixed_pre),
"charge_commitment_ignored_on_relaxed": bool(
commitment_for_solve is None and charge_commitment_prev_w is not None
),
@@ -5628,7 +5635,9 @@ async def _load_slots(
is_daytime_pv_surplus_slot,
charge_acquisition_buy_czk_kwh, charge_acquisition_cutoff_at,
min_buy_before_cutoff_czk_kwh, pv_charge_wh_ahead, neg_buy_wh_ahead,
grid_charge_suppressed_reason
grid_charge_suppressed_reason,
charge_target_wh, pre_window_wh, in_window_wh,
charge_slot_wh, charge_cum_wh, charge_layer, charge_slot_reason
from ems.fn_load_planning_slots_full(
$1::int, $2::timestamptz, $3::timestamptz, $4::numeric
)
@@ -5672,6 +5681,13 @@ async def _load_slots(
pv_charge_wh_ahead=_slot_float_nullable(d, "pv_charge_wh_ahead"),
neg_buy_wh_ahead=_slot_float_nullable(d, "neg_buy_wh_ahead"),
grid_charge_suppressed_reason=d.get("grid_charge_suppressed_reason"),
charge_target_wh=_slot_float_nullable(d, "charge_target_wh"),
pre_window_wh=_slot_float_nullable(d, "pre_window_wh"),
in_window_wh=_slot_float_nullable(d, "in_window_wh"),
charge_slot_wh=_slot_float_nullable(d, "charge_slot_wh"),
charge_cum_wh=_slot_float_nullable(d, "charge_cum_wh"),
charge_layer=d.get("charge_layer"),
charge_slot_reason=d.get("charge_slot_reason"),
)
)
if not out: