Branch 3: charge-slot-budget v R__063 + odstranit v58 pro BA81/KV1 + fixed evening push
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-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 5–11 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 5–11 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:
|
||||
|
||||
Reference in New Issue
Block a user