oprava nevyberu maximalnich sell slotu (sahal i na zitejsi vecer)
This commit is contained in:
@@ -71,7 +71,9 @@ 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-05-30-post-push-night-battery-v47"
|
||||
PLANNER_BUILD_TAG = "2026-05-31-evening-push-budget-primary-night-v49"
|
||||
# Ranní slabá FVE: neaplikovat pv_store ge_pv=0 (jinak curtail při sell < večerní peak).
|
||||
DAWN_LOW_PV_NO_CURTAIL_W = 1500
|
||||
# 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).
|
||||
@@ -1636,19 +1638,27 @@ def _evening_peak_export_indices(
|
||||
return sorted(out)
|
||||
|
||||
|
||||
def _planner_discharge_floor_wh(battery: Any) -> float:
|
||||
"""Provozní podlaha vývoje: reserve_soc (domluva), ne jen min_soc."""
|
||||
return max(
|
||||
float(getattr(battery, "min_soc_wh", 0.0)),
|
||||
float(getattr(battery, "reserve_soc_wh", 0.0)),
|
||||
)
|
||||
|
||||
|
||||
def _evening_push_discharge_budget_wh(
|
||||
*,
|
||||
current_soc_wh: float,
|
||||
min_soc_wh: float,
|
||||
discharge_floor_wh: float,
|
||||
soc_max_wh: float,
|
||||
discharge_slot_buffer: float,
|
||||
) -> float:
|
||||
"""
|
||||
Rozpočet Wh pro tvrdý večerní push — stejný princip jako R__063 (discharge_slot_buffer).
|
||||
Tvrdý push nesmí překročit energii nad min_soc na začátku horizontu (jinak Infeasible).
|
||||
Podlaha = reserve_soc (typ. 20 %), ne min_soc (10 %).
|
||||
"""
|
||||
exportable_full_wh = max(0.0, float(soc_max_wh) - float(min_soc_wh))
|
||||
available_wh = max(0.0, float(current_soc_wh) - float(min_soc_wh))
|
||||
exportable_full_wh = max(0.0, float(soc_max_wh) - float(discharge_floor_wh))
|
||||
available_wh = max(0.0, float(current_soc_wh) - float(discharge_floor_wh))
|
||||
buf = float(discharge_slot_buffer)
|
||||
if buf <= 0.0:
|
||||
return available_wh
|
||||
@@ -1745,6 +1755,36 @@ def _evening_push_calendar_segments(
|
||||
return [sorted(v) for v in by_date.values() if v]
|
||||
|
||||
|
||||
def _primary_night_export_segment_indices(slots: list[PlanningSlot]) -> set[int]:
|
||||
"""
|
||||
První noční epizoda v horizontu (17h → půlnoc → do východu FVE), která platí pro
|
||||
rozpočet Wh z aktuální SoC. Další večery v horizontu (po dni FVE / nabíjení) se
|
||||
plánují až vlastním rolling replanem — nesdílí dnešní baterii.
|
||||
"""
|
||||
segs = _night_export_window_segments(slots)
|
||||
if not segs:
|
||||
return set()
|
||||
for seg in segs:
|
||||
if 0 in seg:
|
||||
return set(seg)
|
||||
return set(segs[0])
|
||||
|
||||
|
||||
def _evening_push_soc_budget_calendar_segments(
|
||||
slots: list[PlanningSlot],
|
||||
discharge_export_ok: set[int] | None = None,
|
||||
) -> list[list[int]]:
|
||||
"""Kalendářní večery jen v primární noční epizodě — vhodné pro push_budget z current_soc."""
|
||||
primary = _primary_night_export_segment_indices(slots)
|
||||
if not primary:
|
||||
return []
|
||||
return [
|
||||
seg
|
||||
for seg in _evening_push_calendar_segments(slots, discharge_export_ok)
|
||||
if seg and all(t in primary for t in seg)
|
||||
]
|
||||
|
||||
|
||||
def _night_self_consume_discourage_import_indices(
|
||||
slots: list[PlanningSlot],
|
||||
*,
|
||||
@@ -1786,7 +1826,9 @@ def _evening_battery_export_push_indices(
|
||||
) -> list[int]:
|
||||
"""
|
||||
Večerní push (≥17h): plný ge_bat v nejdražších slotách (sell desc), rozpočet Wh
|
||||
**per kalendářní večer** — druhý den v horizontu nedostane nulový push (v42 bug).
|
||||
z aktuální SoC jen pro **primární noční epizodu** (dnešní večer → ráno).
|
||||
Zítřejší večer v horizontu se nekrade polovinou budgetu (v43 split) — nabije se
|
||||
přes den / neg okno; push přidá zítřejší rolling replan.
|
||||
per_slot_discharge_wh: min(BMS, export cap) × účinnost × 0,25 h.
|
||||
"""
|
||||
_ = evening_start_hour # kompatibilita volání
|
||||
@@ -1794,41 +1836,45 @@ def _evening_battery_export_push_indices(
|
||||
return []
|
||||
push_budget_wh = _evening_push_discharge_budget_wh(
|
||||
current_soc_wh=current_soc_wh,
|
||||
min_soc_wh=min_soc_wh,
|
||||
discharge_floor_wh=min_soc_wh,
|
||||
soc_max_wh=soc_max_wh,
|
||||
discharge_slot_buffer=discharge_slot_buffer,
|
||||
)
|
||||
if push_budget_wh < per_slot_discharge_wh * 0.5:
|
||||
return []
|
||||
evening_segments = _evening_push_calendar_segments(
|
||||
evening_segments = _evening_push_soc_budget_calendar_segments(
|
||||
slots,
|
||||
discharge_export_ok=discharge_export_ok,
|
||||
)
|
||||
if not evening_segments:
|
||||
return []
|
||||
seg_budget_wh = push_budget_wh / float(len(evening_segments))
|
||||
out: list[int] = []
|
||||
candidates: list[int] = []
|
||||
seen: set[int] = set()
|
||||
for seg in evening_segments:
|
||||
candidates = _evening_push_segment_candidates(
|
||||
for t in _evening_push_segment_candidates(
|
||||
slots,
|
||||
seg,
|
||||
charge_acquisition_czk_kwh=charge_acquisition_czk_kwh,
|
||||
min_spread=degrad_czk_kwh,
|
||||
discharge_export_ok=discharge_export_ok,
|
||||
)
|
||||
if not candidates:
|
||||
continue
|
||||
ranked = sorted(
|
||||
candidates,
|
||||
key=lambda i: (float(slots[i].sell_price), -i),
|
||||
reverse=True,
|
||||
)
|
||||
remaining_wh = float(seg_budget_wh)
|
||||
for t in ranked:
|
||||
if remaining_wh + 1e-6 < per_slot_discharge_wh:
|
||||
break
|
||||
out.append(t)
|
||||
remaining_wh -= per_slot_discharge_wh
|
||||
):
|
||||
if t not in seen:
|
||||
seen.add(t)
|
||||
candidates.append(t)
|
||||
if not candidates:
|
||||
return []
|
||||
ranked = sorted(
|
||||
candidates,
|
||||
key=lambda i: (float(slots[i].sell_price), -i),
|
||||
reverse=True,
|
||||
)
|
||||
remaining_wh = float(push_budget_wh)
|
||||
out: list[int] = []
|
||||
for t in ranked:
|
||||
if remaining_wh + 1e-6 < per_slot_discharge_wh:
|
||||
break
|
||||
out.append(t)
|
||||
remaining_wh -= per_slot_discharge_wh
|
||||
return sorted(out)
|
||||
|
||||
|
||||
@@ -2532,13 +2578,14 @@ def solve_dispatch(
|
||||
export_cap_push_w * float(battery.discharge_efficiency) * INTERVAL_H,
|
||||
)
|
||||
discharge_buf_pre = float(getattr(battery, "discharge_slot_buffer", 0) or 0)
|
||||
discharge_floor_wh = _planner_discharge_floor_wh(battery)
|
||||
computed_evening_push_ts = set(
|
||||
_evening_battery_export_push_indices(
|
||||
slots,
|
||||
charge_acquisition_czk_kwh=charge_acquisition_czk_kwh,
|
||||
degrad_czk_kwh=float(degradation_cost_effective),
|
||||
current_soc_wh=float(current_soc_wh),
|
||||
min_soc_wh=float(min_soc_wh),
|
||||
min_soc_wh=float(discharge_floor_wh),
|
||||
soc_max_wh=float(battery.soc_max_wh),
|
||||
per_slot_discharge_wh=per_slot_push_wh_pre,
|
||||
discharge_slot_buffer=discharge_buf_pre,
|
||||
@@ -3181,11 +3228,13 @@ def solve_dispatch(
|
||||
if om == "AUTO":
|
||||
profitable_export_ts = profitable_export_ts_pre
|
||||
export_push_w = _battery_export_cap_w(battery, grid)
|
||||
discharge_floor_wh = _planner_discharge_floor_wh(battery)
|
||||
for t_peak in morning_pre_neg_export_ts:
|
||||
if t_peak in profitable_export_ts:
|
||||
if _battery_export_push_defer_to_pv(slots[t_peak]):
|
||||
continue
|
||||
prob += ge_bat[t_peak] >= export_push_w * z_export[t_peak]
|
||||
prob += soc[t_peak] >= float(discharge_floor_wh)
|
||||
for t_pnd in pre_neg_buy_discharge_ts:
|
||||
if _battery_export_push_defer_to_pv(slots[t_pnd]):
|
||||
continue
|
||||
@@ -3206,6 +3255,7 @@ def solve_dispatch(
|
||||
if push_floor_w >= GE_MIN_EXPORT_W:
|
||||
prob += z_export[t_peak] == 1
|
||||
prob += ge_bat[t_peak] >= push_floor_w
|
||||
prob += soc[t_peak] >= float(discharge_floor_wh)
|
||||
# Ostatní profitable sloty: měkká shortfall penalizace (ne večerní push).
|
||||
if (
|
||||
last_pos_sell_pre_neg_buy is not None
|
||||
@@ -3730,6 +3780,10 @@ def solve_dispatch(
|
||||
and not skip_pv_store_block
|
||||
and not fixed_pv_b_export_cap
|
||||
and sell_t < pv_store_val
|
||||
and not (
|
||||
sell_t >= 0.0
|
||||
and int(s.pv_a_forecast_w) < DAWN_LOW_PV_NO_CURTAIL_W
|
||||
)
|
||||
and not _pv_forced_vent_export_allowed(
|
||||
t,
|
||||
current_soc_wh=current_soc_wh,
|
||||
@@ -4865,6 +4919,15 @@ async def _rolling_evening_push_override(
|
||||
if not isinstance(prev_iso, list) or not prev_iso:
|
||||
return None
|
||||
prev_push = _evening_push_ts_from_iso(slots, [str(x) for x in prev_iso])
|
||||
if not prev_push:
|
||||
return None
|
||||
budget_eligible = {
|
||||
t
|
||||
for seg in _evening_push_soc_budget_calendar_segments(slots, None)
|
||||
for t in seg
|
||||
}
|
||||
if budget_eligible:
|
||||
prev_push = {t for t in prev_push if t in budget_eligible}
|
||||
if not prev_push:
|
||||
return None
|
||||
prev_peak = inputs.get("evening_push_peak_sell_czk_kwh")
|
||||
|
||||
Reference in New Issue
Block a user