17 KiB
Plánování: rozpočet nabíjecích slotů (Wh × ceny × forecast)
Stav: návrh k implementaci (2026-06) — zatím neimplementováno v produkčním kódu.
Účel: nahradit tvrdé prahy typu sell > min_sell + 0,20 → bc_pv = 0 (v58) a binární pre-neg „cushion“ (v33) jednotným energetickým rozpočtem ve fn_load_planning_slots_full, který pokryje fixní tarify (BA81, KV1), spot (home-01) i zkracující se okna sell < 0 (zima).
Související:
| Dokument | Vztah |
|---|---|
planning.md |
LP, masky, současné v58–v62 |
planning-arbitrage-accounting.md |
proč ne sell < buy v jednom slotu |
planning-neg-sell-strategy.md |
rampa, tail, pole B — cílové SoC v okně |
R__063_fn_load_planning_slots_full.sql |
místo implementace masek |
planning_engine.py |
LP jen respektuje masky + objective |
Changelog (plánované): docs/planning-changelog.md — sekce Plánováno: charge-slot-budget.
1. Problém, který řešíme
1.1 Fixní tarif (BA81, KV1) — slunečný den ~60 % SoC
Symptom: přes den nabíjení ze slunce jen na ~60–66 %, zbytek FVE jde do sítě při výkupu 2–3 Kč/kWh; nabíjení jen v 2–4 slotech u minima výkupu (~1,45 Kč).
Příčina (současný kód):
R__063už počítáv_energy_to_filla vrstvu A (PV) s kumulací Wh, ale u fixního tarifu řadí PV vrstvu podlestore_score(future sell − sell).planning_enginev58: tvrděbc_pv = 0(a často ibc_gi = 0) kdyžsell > min(sell≥0) + 0,20— LP nesmí nabíjet, i když SQL nastavíallow_charge = true.
→ Rozpor mezi maskou a LP; ekonomicky „správný“ export v 10:00 blokuje doplnění baterie, i když večerní arbitráž to nevyžaduje.
1.2 home-01 — velká baterie, kratší / slabší okno sell < 0
Symptom: ráno export FVE před sell < 0 i při výkupu ~2–3 Kč, zatímco v záporném okně by energie mohla být potřeba víc (krátké okno, slabší zimní FVE).
Příčina (současný kód):
- v33:
_pre_neg_pv_export_forecast_cushion_ok— binární rozhodnutí: pokud forecast PV v celém denním okněsell < 0pokryje deficit dosoc_target[first_neg]× 1,15 → všechny pre-neg sloty s přebytkem → export (bc_pv = 0, pushge_pv). - Neptá se: kolik Wh musím nabít před oknem, když uvnitř okna forecast nestačí.
- v44: na neg den žádný grid před 1.
sell < 0— při nedostatku FVE v okně chybí páka levného NT před oknem.
1.3 Společná chyba modelu
| Špatně | Správně |
|---|---|
Prah sell > min + 0,20 |
Kolik Wh chybí do cíle a které sloty je nejlevněji doplní |
| Binární cushion OK / fail | pre_window_wh = max(0, deficit − in_window_wh) |
store_score u fixního buy |
U fixního tarifu řadit sell ASC (výkup = příležitostní cena uložení) |
| LP přepisuje SQL masky (v58) | LP jen bc_* kde allow_charge; export kde maska nepřidělila charge budget |
2. Principy návrhu
- SQL-first: výběr nabíjecích slotů =
ems.fn_load_planning_slots_full(rozšířeníR__063), ne nové tvrdé větve vsolve_dispatchkromě stávajících bezpečnostních guardů (load-first, večerní push, neg fáze). - Energetický rozpočet (Wh), ne Kč prah: ceny řadí prioritu slotů; zastavení kumulace je
cum_wh ≥ target_wh(±charge_slot_buffer). - Forecast v každém slotu:
pv_surplus_w= max(0, pv_a + pv_b − load_baseline − …) už ve work tabulce; nabíjecí příspěvek slotu =min(pv_surplus_w, max_charge_w) × charge_eff × 0,25(+ volitelně gridper_slot_charge_wh). - Oddělení nákupního a výprodejního okna — viz
planning-arbitrage-accounting.md;charge_acquisitionz vybraných slotů, nemin(buy)celého horizontu. - Jeden algoritmus, více režimů: stejná kostra pro spot i fixed; liší se řazení (buy vs sell), cíl SoC a výjimky (neg den, block_export).
3. Slovník
| Symbol / pole | Význam |
|---|---|
energy_to_fill_wh |
soc_max_wh − current_soc_wh (základní deficit do plné baterie) |
charge_target_wh |
Cíl pro výběr slotů — může být < soc_max (rezerva neg okna, safety, večerní export) |
in_window_wh |
Odhad energie do baterie uvnitř speciálního okna (např. všechny sloty sell < 0 téhož pražského dne), z forecastu A+B (+ volitelně grid v okně) |
pre_window_wh |
max(0, charge_target_wh − in_window_wh × reliability_factor) — kolik Wh je třeba doplnit před oknem |
slot_charge_wh[t] |
Wh, které lze v t reálně natočit (PV surplus cap + výkon baterie) |
allow_charge |
SQL maska: LP smí bc_pv / bc_gi |
allow_grid_charge |
podmnožina: smí bc_gi |
charge_slot_reason |
debug: grid_layer_b, pv_layer_a, pre_neg_fill, neg_window, buy_negative, … |
4. Jádro algoritmu (společné)
Pro každý běh plánovače (site, horizont, current_soc_wh):
Krok 1 — Cíl energie
charge_target_wh := min(
soc_max_wh − current_soc_wh,
optional_cap_from_neg_strategy, -- viz §6
optional_safety_soc_target_wh
)
- Výchozí: doplnit do
soc_max_wh(scharge_slot_bufferzasset_batteryjako dnes). - U home-01 s neg dnem: cíl na vstupu do okna =
soc_need[first_neg_sell]z rampy (v35/v36), ne fixních 80 %. - Rezerva pro okno
sell < 0: stávající logikav_pv_layer_cap_wh -= neg_window_pv_surplus_whvR__063zůstane jako sníženícharge_target_whpřed oknem, ne jako samostatný binární export.
Krok 2 — Dodávka uvnitř speciálního okna (volitelná větev)
Pro každý pražský den s alespoň jedním sell < 0:
in_window_wh := sum over slots t in neg_window(day):
min(pv_surplus_w[t], max_charge_w) * charge_eff * 0.25
+ (optional) grid_wh if allow_grid in neg window (v45)
reliability_factor ∈ (0, 1] — např. 0,85 zimní / krátké okno, 1,0 letní dlouhé okno; nebo odvozené z počtu slotů sell < 0 (≤ 4 sloty → nižší faktor).
Krok 3 — Deficit před oknem
pre_window_wh := max(0, charge_target_at_neg_entry − in_window_wh * reliability_factor)
Kde charge_target_at_neg_entry = SoC potřeba na first_neg_sell (z rampy / soc_need), minus current_soc (pozorované), přepočteno na Wh.
Krok 4 — Fronta slotů (řazení + kumulace)
Kandidáti = sloty s slot_charge_wh > 0 nebo (pro grid vrstvu) sloty bez PV, kde smí síť.
Řazení (priorita):
| Režim | Primární klíč | Sekundární |
|---|---|---|
Spot (purchase_pricing_mode ≠ fixed) |
buy_price ASC |
den plánu, před exportním oknem, slot_ord |
| Fixní tarif | sell_price ASC |
stejné geografické priority jako dnes v R__063 |
Kumulace:
cum := 0
for slot in candidates ordered:
if cum >= budget_wh: break
allow_charge[slot] := true
if grid_layer: allow_grid_charge[slot] := true
cum += slot_charge_wh[slot] -- u grid vrstvy min(cum increment, per_slot_charge_wh)
Budgety (vrstvy, po sobě):
- Pre-neg / před oknem —
budget = pre_window_wh(jen slotyt < first_neg_selltéhož dne). - Grid B —
budget = grid_target_wh(AM/PM 50/50 jako dnes); spot: nejlevnějšíbuy; fixed: nejlevnějšísellu slotů splňujících marži. - PV A — zbytek —
budget = max(0, charge_target_wh − grid_filled_wh − pre_neg_filled_wh).
Po výběru: sloty mimo vybranou frontu nemusí mít allow_charge — LP pak může exportovat FVE, pokud objective dává smysl (bez v58 zákazu).
Krok 5 — Export před oknem (home-01)
Nahradit v33 binární cushion:
export_allowed_pre_neg[t] :=
t in pre_neg_calendar_window
AND pv_surplus_w[t] > threshold
AND NOT allow_charge[t] -- přebytek po naplnění pre_window_wh
AND (optional) sell[t] >= sell_export_floor[t] -- ne dump pod ranním pásmem (R__063 morning zone)
V Pythonu: pre_neg_pv_export_ts = sloty s exportem jen pokud nejsou v charge frontě; případně měkká penalizace místo bc_pv = 0 na celé pásmo.
5. Fixní tarif (BA81, KV1)
5.1 Změny oproti dnešku
| Dnes (v58–v59) | Plán |
|---|---|
bc_pv = 0 if sell > min + 0,20 |
Zrušit v planning_engine.py |
PV vrstva A: store_score DESC |
Fixed: sell_price ASC + kumulace pv_surplus Wh |
| Grid: min sell sloty (v59) | Zachovat, sladit s jednotnou frontou (grid = vrstva B) |
Večer push: bc_* = 0 |
Zachovat (exportní okno) |
5.2 Ekonomická interpretace
- Buy je konstantní → rozhoduje výkup v slotu (opportunity cost uložení do baterie).
- Nabíjet v pořadí nejnižší sell, dokud
cum < charge_target_wh. - Export v slotu s vyšším sell je OK až poté, co je rozpočet Wh vyčerpán v levnějších slotech (včetně pozdějších levných sellů v pořadí času — viz pořadí: nejdřív globálně nejlevnější sell v rámci dne / AM-PM, viz níže).
5.3 AM/PM a pořadí v čase
Zachovat stávající AM/PM rozpočet grid_target_wh (50/50, přeliv AM→PM). Uvnitř segmentu:
- fixed grid:
sell ASC+ filtrsell ≤ min(sell≥0) + degrad + ε(jako v59) nebo čistě kumulace Wh bez ε, pokud Wh budget stačí — rozhodnutí při implementaci: preferovat Wh kumulaci; ε jen jako failsafe proti grid nabíjení za 6 Kč v noci (KV1).
5.4 Večerní špička a profitable export
Beze změny principu: allow_discharge_export + evening_push_ts; v push slotech allow_charge = false.
6. Spot — home-01 a negativní výkup
6.1 Běžný spot (bez neg dne v horizontu)
Stejná kostra jako §4:
- Grid vrstva:
buy ASC(stávající spot loop + self-konzistentní filtrpv_charge_wh_ahead). - PV vrstva:
store_score DESCnebo hybrid — po grid; budget = zbytekcharge_target_wh.
6.2 Den s sell < 0 (neg strategie)
Integrace s planning-neg-sell-strategy.md:
| Komponenta | Role v charge-slot budget |
|---|---|
soc_need[t] rampa (v35/v36) |
charge_target_at_neg_entry |
in_window_wh z A+B v neg sloty |
sníží pre_window_wh |
pre_window_wh > 0 |
nabíjecí fronta před first_neg_sell (PV + případně grid) |
| Tail / T / curtail | beze změny v LP (fáze neg okna) |
v44 neg_day_no_grid_before_neg_sell |
Změkčit: povolit grid v N nejlevnějších buy slotech před oknem, pokud pre_window_wh > in_window_wh × factor a buy < 0 nebo buy ≤ ref_buy_am + degrad |
6.3 Nahrazení v33 cushion
| v33 (dnes) | charge-slot budget |
|---|---|
cushion_ok → export vše pre-neg |
pre_window_wh malé → více allow_charge pre-neg |
cushion_fail → nabíjet |
pre_window_wh velké → fronta nabíjení |
bc_pv = 0 v celém pre_neg_pv_export_ts |
Jen sloty, kde není allow_charge a export je ekonomicky výhodný |
Zima / krátké okno: málo slotů sell < 0 → malé in_window_wh → velké pre_window_wh → více nabíjení před oknem, i při sell 2–3 Kč, pokud jsou to nejlevnější dostupné sloty (ne plošný export).
6.4 Velká baterie (64 kWh)
charge_target_whmůže být desítky kWh — fronta musí počítat skutečnéslot_charge_wh, ne jen cap 6 slotů / segment, pokud budget vyžaduje více (rozšířitgrid_charge_cap_*odvozeně odceil(budget / per_slot_charge_wh)— částečně už je).charge_acquisition= vážený buy ve vybranýchallow_grid_chargeslotech (stávající two-pass v Pythonu).
7. Role LP po změně masek
solve_dispatch nesmí přepisovat energetický výběr prahy typu v58.
| Oblast | LP chování |
|---|---|
| Nabíjení | bc_pv, bc_gi jen kde allow_charge / allow_grid_charge |
| Export FVE | objective −ge_pv×sell; bez bc_pv=0 jen kvůli sell |
| Export bat | allow_discharge_export, charge_acquisition, večerní push |
| Guard | ge_pv=0 if sell < charge_acquisition − degrad (spot) — měkká hranice hodnoty uložené energie |
| Spot grid | v61: bc_gi=0 if buy > charge_acquisition + degrad |
| Neg fáze | rampa, tail, T — beze změny |
8. Návrh rozšíření fn_load_planning_slots_full
Nové / rozšířené sloupce ve výstupu (nebo JSON v meta — preferováno sloupce pro debug):
| Sloupec | Typ | Popis |
|---|---|---|
charge_budget_wh |
numeric | celkový cíl pro tento běh |
charge_slot_wh |
numeric | odhad Wh v daném slotu |
charge_cum_wh |
numeric | kumulativa po řazení (audit) |
charge_layer |
text | pre_neg / grid_am / grid_pm / pv_a / … |
pre_window_wh |
numeric | jen informativní per běh (nebo per den v solver_params) |
V planning_run.solver_params (commit meta):
{
"charge_slot_budget": {
"charge_target_wh": 42000,
"pre_window_wh_by_day": {"2026-06-02": 18000},
"in_window_wh_by_day": {"2026-06-02": 12000},
"reliability_factor": 0.85,
"planner_build_tag": "…-charge-slot-budget-v1"
}
}
9. Co zrušit / neimplementovat znovu
| Položka | Akce |
|---|---|
v58 fixed_high_sell_no_pv_charge |
odstranit po nasazení budget |
v58 FIXED_PV_CHARGE_ONLY_NEAR_MIN_SELL_CZK_KWH |
odstranit (konstanta) |
v59 fixed_grid_charge_unprofitable část sell > min + 0,20 |
nahradit: grid jen ve vybrané frontě; případně ponechat sell < buy guard pro grid |
v60 sell < buy → bc_gi=0 (spot) |
neobnovovat (záměrně zrušeno v61) |
| v33 binární cushion jako hlavní páka | nahradit §4 krok 2–3; cushion ponechat jako audit / solver_params.inputs.pre_neg_cushion_legacy_ok |
10. Fáze implementace (doporučené pořadí)
- Dokumentace + testy scénářů (tento soubor, pytest fixtures s umělými sloty).
R__063: fixed PV vrstvasell ASC+ Wh kumulace; sloupce debug; bez změny Pythonu → ověřit, že v58 stále blokuje (regrese).- Python: odstranit v58/v59
bc_pv/bc_gifixed větve; spoléhat na masky. R__063+ Python: pre-negpre_window_whpro spot; zúžitpre_neg_pv_export_ts.- v44 změkčení: grid před neg jen když
pre_window_wh> práh. - Dokumentace + changelog tag
charge-slot-budget-v1; MCP ověření home-01 / KV1 / BA81.
11. Ověření
11.1 Automatické testy (pytest)
| Scénář | Očekávání |
|---|---|
| Fixed, slunečný den, nízký ranní SoC | allow_charge v mnoha PV slotech; max SoC v plánu → blízko soc_max |
| Fixed, min sell odpoledne | dřívější sloty s vyšším sell bez allow_charge, pokud budget vyčerpán v levnějších |
| Spot, krátké neg okno (2–4 sloty), slabá FVE | pre_window_wh > 0; nabíjení před first_neg_sell; ne plošný pre-neg export |
| Spot, dlouhé neg okno, silná FVE | po naplnění pre_window_wh může export pre-neg v drahých sell |
| home-01 velká baterie | kumulace ≥ desítky kWh přes více slotů |
11.2 SQL / MCP
select interval_start, allow_charge, allow_grid_charge,
charge_layer, charge_slot_wh, charge_cum_wh,
sell_price, pv_surplus_w
from ems.fn_load_planning_slots_full(<site_id>, <from>, <to>, <soc_wh>)
where allow_charge
order by interval_start;
Po deployi: aktivní planning_run.solver_params->'charge_slot_budget'; u home-01 pre_neg_pv_export_slots ⊆ sloty bez allow_charge.
11.3 Regrese
- Večerní push (v57), spot v61, KV1 noc v62 — nesmí rozpadnout.
- Neg rampa v35/v36 — SoC v okně
sell < 0stejné cíle, mění se jen příprava před oknem.
12. Otevřené body (rozhodnutí před kódem)
reliability_factor: fixní 0,85 vs funkce počtu neg slotů?- Fixed řazení: globální
sell ASCpřes den vs AM/PM segmenty (doporučení: AM/PM budget +sell ASCv segmentu). - Večer před neg dnem: zda
charge_target_whzahrnuje večerní výboj D−1 (neg_evening_before_neg) — ano, přessoc_need, ne duplicitní budget. - Multi-day horizont: každý pražský den vlastní
pre_window_wh(jako v36 bundle) — ano. - UI: badge „charge budget“ ve frontendu — volitelné, až budou sloupce v API.
13. Shrnutí pro produkt
Jednou větou: místo prahu „sell nad minimum + 20 haléřů“ spočítáme, kolik Wh chybí do cíle, kolik jich dodá záporné výkupní okno z forecastu, a zbytek nabijeme v nejlevnějších slotech (buy nebo sell podle tarifu) s ohledem na PV přebytek a spotřebu — u home-01 tím nahradíme plošný ranní export před sell < 0, u KV1/BA81 doplníme baterii ve slunečný den nad ~60 %.