# backend/services/planning/constants.py # # EMS plánovač – konstanty (Fáze 1 dekompozice, čistý přesun z planning_engine.py). # POZOR: ekonomické penalty/váhy jsou kandidáti na přesun do DB ve Fázi 2 # (CLAUDE.md pravidlo 16: žádný skrytý faktor v Pythonu). from zoneinfo import ZoneInfo # ============================================================ # Konstanty # ============================================================ # Když DB vrátí NULL (skoro žádná OTE data), denní plán použije krátký fallback (soulad s min hodinami ve fn_planning_horizon_end). _DAILY_FALLBACK_HORIZON_HOURS = 1.0 # Shadow cena zbytkové energie na konci horizontu: - (avg_buy * FACTOR / 1000) * soc[T-1] (Kč; soc v Wh). INTERVAL_H = 0.25 # 15 minut v hodinách CURTAILMENT_PENALTY = 0.001 # Kč/Wh – malá penalizace za omezení FVE pole A SOLVER_TIME_LIMIT = 10 # sekund # MILP: významný export ge (W) ⇒ koncové soc[t] ≥ podlaha; mimo arbitrážní relax je to arb_base_wh # (rezerva z DB). Při relaxaci spodku před extrémně záporným buy je podlaha soc_panel_min[t] # (planner floor), jinak by šlo jen do zátěže a nešlo by „vypustit do sítě“ před levným nákupem. GE_MIN_EXPORT_W = 1.0 # Dvouprůchodové solve: stop když acquisition z pass1 vs pass2 se liší méně než (Kč/kWh). ACQUISITION_TWO_PASS_EPS_KWH = 0.05 # Load-first (Deye): PV nejdřív pokryje load+EV+TČ; bc_pv/ge_pv jen z pv_sp (přebytek). LOAD_FIRST_INCENTIVE_CZK_KWH = 0.05 # Dokud je kotva pro hluboký dump (první sell < 0 v horizontu, jinak první extrémní buy) dál než # tento počet 15min slotů, držíme plánovací spodek na rezervě (arb_base_wh) místo planner floor — # priorita: beze „ztráty na prodeji“ (sell >= 0) držet buffer, hluboký vývoz až těsně před záporným prodejem. DEFAULT_PLANNER_DISCHARGE_RELAX_PREWINDOW_SLOTS = 8 # Měkká kotva: chceme být u planner floor už v posledním slotu před prvním sell < 0. # Penalizace je v Kč/Wh (např. 0.20 = 200 Kč/kWh). Musí být dost velká, aby přebila # bezpečnostní SoC buffer + terminal shadow cenu a solver skutečně „dovylil“ před sell<0. PRENEG_SELL_SOC_ANCHOR_SLACK_PENALTY_CZK_PER_WH = 0.20 PEAK_EXPORT_SHORTFALL_PENALTY_CZK_KWH = 80.0 # Měkký tlak: v okně sell<0 + block_export využít PV přebytek do baterie (ne curtail). PV_CHARGE_SHORTFALL_PENALTY_CZK_KWH = 120.0 # Curtailment při sell<0 + allow_charge: nesmí být téměř zdarma oproti nabíjení (BA81). NEG_SELL_CURTAIL_PENALTY_CZK_KWH = 1.0 # Odměna v objective za FVE→baterie při sell<0 (doplňuje shortfall; BA81 fixed tarif). NEG_SELL_PV_CHARGE_REWARD_CZK_KWH = 0.8 # Měkký tlak: v okně sell<0 dobít na soc_max (ne zastavit na ~94 % kvůli curtail). NEG_SELL_SOC_UNDERFILL_PENALTY_CZK_PER_WH = 0.35 # Jen ventil nekontrolovatelného pole B při plné baterii a sell<0 (spot); ne celý PV přebytek. NEG_SELL_PV_B_VENT_PENALTY_CZK_KWH = 4.0 # Fáze sell<0 (v32): ASAP na prep_soc %, tail rampa na soc_max. NEG_SELL_PREP_SOC_SHORTFALL_PENALTY_CZK_PER_WH = 0.85 NEG_SELL_PREP_HOLD_BCPV_PENALTY_CZK_KWH = 60.0 # Výboj baterie při sell<0 jen těsně před extrémně záporným buy (round-trip arbitráž). EXTREME_BUY_DUMP_PREWINDOW_SLOTS = 12 NEG_SELL_BAT_DUMP_SHORTFALL_PENALTY_CZK_KWH = 80.0 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-home01-strict-late-replan-v5" SOLVER_RELAX_STEPS: tuple[str, ...] = ( "strict", "relaxed_expensive_import", "relaxed_neg_buy_charge", "relaxed_neg_prep_hold_only", "relaxed_neg_prep_window", "neg_sell_phases_fallback", "relaxed_pos_sell_ge_block", "relaxed_solver_masks", ) # 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). NEG_SELL_POST_DETACH_BCPV_DISCOURAGE_CZK_KWH = 250.0 # Večer před neg dnem: výboj do sítě (měkký shortfall na ge_bat). NEG_EVENING_PREP_DISCHARGE_SHORTFALL_PENALTY_CZK_KWH = 120.0 # Kotva: SoC na konci večera D−1 a těsně před 1. sell<0 ráno D ≤ reserve_soc. NEG_EVENING_RESERVE_SOC_MAX_SLACK_WH = 400.0 NEG_EVENING_RESERVE_SOC_SLACK_PENALTY_CZK_PER_WH = 55.0 # Terminal SoC shadow price: effective_factor = base × (1 − w_neg); w_neg roste s blízkostí a záporností buy<0. TERMINAL_NEG_BUY_WEIGHT_HORIZON_SLOTS = int(36 / INTERVAL_H) TERMINAL_NEG_BUY_MAGNITUDE_REF_CZK = 1.0 TERMINAL_NEG_BUY_MAGNITUDE_FLOOR = 0.25 TERMINAL_NEG_BUY_WEIGHT_CAP = 0.95 # Před prvním sell<0: export FVE jen pokud predikce v sell<0 okně pokryje dobítí na prep cíl. PRE_NEG_PV_EXPORT_FORECAST_MARGIN = 1.15 PRE_NEG_PV_EXPORT_MIN_NEEDED_WH = 2500.0 PRE_NEG_PV_EXPORT_SHORTFALL_PENALTY_CZK_KWH = 55.0 PRE_NEG_PV_BCPV_DISCOURAGE_CZK_KWH = 90.0 POS_SELL_PRE_NEG_SOC_SHORTFALL_PENALTY_CZK_PER_WH = 0.30 PRE_NEG_BUY_SOC_CEILING_SLACK_PENALTY_CZK_PER_WH = 0.25 PRE_NEG_BUY_EMPTY_EXPORT_SHORTFALL_PENALTY_CZK_KWH = 80.0 EVENING_PEAK_SELL_EPS_CZK_KWH = 0.05 # Rolling replan: držet evening_push_ts při malé změně peak sell / SoC. EVENING_PUSH_HYSTERESIS_SELL_PEAK_DELTA_CZK_KWH = 0.5 EVENING_PUSH_HYSTERESIS_SOC_PCT = 5.0 # Noční výprodej baterie: večer (≥17h) + ráno do východu FVE (0–5h Prague), jedna špička přes půlnoc. NIGHT_EXPORT_EVENING_START_HOUR = 17 NIGHT_EXPORT_MORNING_END_HOUR = 5 NIGHT_EXPORT_PV_SUNRISE_SURPLUS_W = 500.0 # Převáží terminal SoC shadow price při krátkém večerním horizontu (home-01). EVENING_PUSH_Z_EXPORT_BONUS_CZK = 2500.0 # buy<0: preferovat import před PV A→bat (měkké; tvrdé bc_pv=0 láme bilanci s polem B). PRE_NEG_BUY_PV_CHARGE_PENALTY_CZK_KWH = 250.0 CORRECTION_WINDOW_H = 1 # hodina zpět pro výpočet korekčního faktoru CORRECTION_MIN_CLAMP = 0.5 # spodní limit korekčního faktoru CORRECTION_MAX_CLAMP = 1.5 # horní limit korekčního faktoru # Útlum korekce: čím dál od aktuálního času, tím méně korigujeme forecast CORRECTION_DECAY_SLOTS = 16 # po 16 slotech (4h) klesne korekce na 0 # Dynamická ekonomická podlaha (MILP w_arb): lookahead FVE energie v dalších slotech ARB_LOOKAHEAD_SLOTS = 32 # 8 h při INTERVAL_H=0.25 ARB_FLOOR_E_REF_FRAC = 0.5 # má scale Wh = tato frakce usable_capacity (0..1) _PRAGUE_TZ = ZoneInfo("Europe/Prague") # --- Konstanty původně roztroušené mezi funkcemi planning_engine.py (Fáze 1) --- MORNING_PRENEG_START_HOUR = 5 MORNING_PRENEG_END_HOUR = 11 # --- EV anti-fragmentace (Fix B, solver_v2) --- # IEC 61851 min. nabíjecí proud (A) na fázi. 3f wallbox NEumí jet 1f trickle pod # 6 A na všech fázích → fyzikální dolní mez dávky je 6 A × phases × napětí. EV_MIN_CHARGE_CURRENT_A = 6.0 # Síťové napětí fáze (V) pro odhad 3f power floor (3f wallbox: 6 A × 3 × 230 ≈ 4140 W). EV_PHASE_VOLTAGE_V = 230.0 # Práh, od kolika fází považujeme wallbox za vícefázový (≥ tato hodnota → power floor # z fází; jinak držíme min_power_w z DB). 3 = jen čistě 3f wallbox dostane 3f floor. EV_MULTIPHASE_FLOOR_MIN_PHASES = 3