Fix SoC balance on battery export and improve evening push (v39).
SoC continuity now deducts only bd (ge_bat was double-counted via energy balance), which stopped the plan from draining ~2× faster than BMS during evening BATTERY_SELL. Also ships dynamic evening push budget + rolling hysteresis (v38), drops unused fn_soc_tracking_bundle, and adds tests/docs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
- **SQL-first:** horizont a sloty z DB funkcí (`fn_planning_horizon_end`, `fn_load_planning_slots_full`, …); viz **`CLAUDE.md`** → sekce *SQL-first a read-model*.
|
||||
- **Dynamický horizont (jen OTE):** konec plánu z **`ems.fn_planning_horizon_end(site_id, horizon_start)`** (výchozí strop **36 h**, minimum pro rolling **1 h** – obojí jako defaultní argumenty v SQL, úprava přes repeatable migraci). Pomocná `ems.fn_last_effective_ote` vrací konec posledního OTE intervalu. Rolling replan při `NULL` přeskočí; denní plán použije krátký (1 h) fallback v Pythonu. Sloty v solveru jsou bez predikovaných cen v rámci tohoto horizontu.
|
||||
- **Terminal SoC shadow price:** v objective je člen `−(avg_buy_prvních_24h × planner_terminal_soc_value_factor / 1000) × soc[T−1]` (Kč), kde faktor je **`ems.asset_battery.planner_terminal_soc_value_factor`** přes **`ems.fn_planning_site_context`** (default v DB **0.9**); viz sekci *Tuning pro malé baterie* níže. Účel: konec horizontu nemusí končit zbytečně vyprázdněnou baterií (receding horizon).
|
||||
- **SoC kontinuita a export z baterie:** `soc[t]` klesá při **`bd[t]` i `ge_bat[t]`** (vybíjení do domu i do sítě). Bez `ge_bat` v bilanci SoC LP „exportovalo“ bez vybití — arbitrážní dump v pozdních slotech místo ranního peaku.
|
||||
- **SoC kontinuita a export z baterie:** `soc[t]` klesá při **`bd[t]`** — výkon vybíjení na AC sběrnici z energetické bilance `pv + gi + bd = load + bc + ge`. Při exportu z baterie je v `bd` už započten i tok do sítě (`ge_bat` je součást `ge`); **`ge_bat` se v SoC znovu neodečítá** (dříve double-count → plán klesal ~2× rychleji než BMS ve večerním exportu). Tag `2026-05-28-evening-export-soc-balance-v39`.
|
||||
- **Masky `allow_charge` / `allow_discharge_export` (tenký anti-mikrocyklus):** generuje `ems.fn_load_planning_slots_full` (`R__063`). Ekonomiku primárně řídí LP podle efektivních cen; masky jen omezují počet slotů pro grid nabíjení / export baterie.
|
||||
- **PV-surplus (vrstva A):** ranking dle **`store_score DESC`** = `future_sell_opportunity − sell − max(0, buy−sell)`; jen sloty s `sell ≥ buy − degradation`. Kumulativní PV pokrývá `grid_target` (deficit SoC, nad `reserve_soc` bez násobení `charge_slot_buffer`). Zbytek → `allow_charge=false` (PV jen do sítě / `bc ≤ pv_surplus` v LP).
|
||||
- **Grid ze sítě (vrstva B, před FVE):** výchozí **AM/PM 50/50** z `grid_target × charge_slot_buffer` (do `soc_max`); **nevyčerpaný AM Wh přejde do PM** (`R__063`). **Spot:** výběr **nejlevnější `buy`** (den plánu → před exportním oknem → `buy ASC`); navíc všechny sloty s **`buy < 0`** → `allow_grid_charge`. Po výběru AM/PM běží **iterativní self-konzistentní filtr** (vyloučí drahé grid sloty, pokud `pv_charge_wh_ahead + neg_buy_wh_ahead >= 60 %` deficitu SoC; failsafe unlock). Debug: `grid_charge_suppressed_reason`. **Fixní tarif (BA81):** stejný AM/PM rozpočet, ale pořadí podle **`slot_ord`** (buy konstantní), jen pokud v horizontu existuje **`sell > buy + degradation`**; jinak jen PV vrstva A. Cap slotů: `ceil(budget/per_slot_wh) × charge_slot_buffer`. **`charge_acquisition`:** vážený `buy` u `allow_grid_charge` před 1. exportem; two-pass v `planning_engine.py`.
|
||||
@@ -85,24 +85,24 @@ Cíl zůstává **maximální ekonomický užitek v celém horizontu**: prodat (
|
||||
flowchart TD
|
||||
A[LP: globální optimum v horizontu] --> B{slot >= 17h a profitable export?}
|
||||
B -->|sell pod nocnim max - 0.05| C[ge_bat = 0: baterie ne pred spickou]
|
||||
B -->|sell v top pasme max - 0.05| D[evening_push kandidat]
|
||||
D --> E[Seradit sell desc, pridat sloty az do Wh rozpoctu]
|
||||
E --> F[ge_bat >= plny vykon na cap v kazdem push slotu]
|
||||
B -->|profitable + nocni okno| D[push: sell desc az do Wh rozpoctu]
|
||||
D --> F[ge_bat >= plny vykon na cap v kazdem push slotu]
|
||||
C --> G[Vysledek: energie zustane na nejdrazsi vecer]
|
||||
F --> G
|
||||
```
|
||||
|
||||
1. **SQL masky (R__063, vrstva 2)** — které večerní sloty *smí* export z baterie vůbec (`allow_discharge_export`): mimo jiné sloty v pásmu „denní večerní max − degrad“ (SQL), plus globální Wh rozpočet (vrstva 1).
|
||||
|
||||
2. **v27 — zákaz předčasného večerního vývozu** (`evening_early_export_penalty_ts` → tvrdé `ge_bat[t] = 0`):
|
||||
- jen v **nočním okně** (`_in_night_battery_export_window`) a **časově před** prvním slotem v `evening_push_ts`;
|
||||
- jen pokud `sell < max_sell_v_nočním_úseku − 0,05` (v30: max přes půlnoc, ne per kalendářní den);
|
||||
2. **v38 — zákaz předčasného / levného večerního vývozu** (`evening_early_export_penalty_ts` → tvrdé `ge_bat[t] = 0`):
|
||||
- v **nočním okně** pro profitable sloty **mimo** `evening_push_ts` (včetně slotů **po** prvním push — v27 je omezoval jen na čas před prvním push);
|
||||
- pokud `sell < max_sell_v_nočním_úseku − 0,05` (v30: max přes půlnoc);
|
||||
- **nezakazuje** přebytek FVE do sítě (`ge_pv`).
|
||||
|
||||
3. **v24 + v27 — plný výkon v top večerních slotech** (`evening_push_ts`):
|
||||
- kandidáti: profitable ∩ večer ∩ `sell ≥ max_večer − 0,05` (úzké pásmo u **absolutní** večerní špičky, ne široké „peak−degrad“ pro push);
|
||||
- řazení podle **`sell` sestupně**;
|
||||
- přidávat sloty, dokud `kumulované_Wh ≤` rozpočet (`discharge_slot_buffer`, SoC nad `min_soc`);
|
||||
3. **v38 — plný výkon v top večerních slotech** (`evening_push_ts`):
|
||||
- kandidáti: profitable ∩ noční okno ∩ `sell ≥ 0`;
|
||||
- push = nejdražší sloty **seřazené `sell` desc**, dokud `kumulované_Wh ≤ push_budget` (`min(available_soc, exportable_full × discharge_slot_buffer)`; `per_slot` ≈ max_discharge × účinnost × 0,25 h) — **počet slotů dynamický** (ne pevné top-3);
|
||||
- při vysokém SoC může být push slotů víc než 3 (např. 40+ kWh rozpočet → ~9–12 slotů podle `per_slot`);
|
||||
- **rolling hysteresis:** při `|Δ peak sell| < 0,5` Kč a `|Δ SoC| < 5 %` držet `evening_push_ts` z předchozího aktivního runu (`_rolling_evening_push_override`);
|
||||
- **v28 push fyzika:** cap `ge_bat ≈ min(export_cap, max_discharge − load)` a v push slotech BMS `load + ge_bat ≤ max_discharge` (ne `bd+ge_bat`, které dvojí započítávalo export); odpovídá Deye SELL — load z baterie, zbytek do sítě až po site cap;
|
||||
- **výsledek:** jeden nejdražší slot → export řádově kW; další drahé sloty **po** prvním push mohou exportovat dle ekonomiky LP.
|
||||
|
||||
@@ -116,7 +116,7 @@ flowchart TD
|
||||
| Měkká `peak_export_shortfall` → často ~50 % výkonu v mnoha slotech | Na `evening_push` slotech tvrdý push na cap; shortfall na push vypnutý |
|
||||
| `grid_setpoint = gi − ge` → Deye vidí ~0 W při velkém `ge_bat` | `_dispatch_grid_setpoint_w` z reálného exportu |
|
||||
|
||||
**Funkce:** `_evening_battery_export_push_indices`, `_evening_push_discharge_budget_wh`, `_evening_push_battery_export_w`, `_dispatch_grid_setpoint_w` v `planning_engine.py`. Tag: `2026-05-28-evening-peak-full-export-v28`.
|
||||
**Funkce:** `_evening_battery_export_push_indices`, `_evening_early_export_penalty_indices`, `_rolling_evening_push_override`, `_evening_push_discharge_budget_wh`, `_evening_push_battery_export_w`, `_dispatch_grid_setpoint_w` v `planning_engine.py`. Tag: `2026-05-28-evening-export-dynamic-v38`.
|
||||
|
||||
### Arbitráž baterie — účtování mezi sloty (povinné čtení)
|
||||
|
||||
@@ -301,9 +301,11 @@ if ev_session[e].target_deadline and ev_session[e].soc_at_connect_pct is not Non
|
||||
|
||||
### SoC kontinuita
|
||||
```python
|
||||
# battery_discharge = bd (W z baterie na AC sběrnici z bilance pv+gi+bd = load+bc+ge).
|
||||
# ge_bat je součást ge — v SoC znovu neodečítat (v39).
|
||||
soc[t] == soc[t-1]
|
||||
+ battery_charge[t] * charge_efficiency * interval_h
|
||||
- battery_discharge[t] / discharge_efficiency * interval_h
|
||||
+ (bc_pv[t] + bc_gi[t]) * charge_efficiency * interval_h
|
||||
- bd[t] / discharge_efficiency * interval_h
|
||||
|
||||
soc[0] == current_soc_wh # počáteční podmínka z telemetrie
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user