261 lines
15 KiB
Markdown
261 lines
15 KiB
Markdown
# Planning / LP — changelog
|
||
|
||
Změny v plánovači (`planning_engine.py`, `R__063_fn_load_planning_slots_full.sql`) a souvisejících testech.
|
||
Formát: **datum (ISO)** · stručný důvod · soubory · chování / ověření.
|
||
|
||
---
|
||
|
||
## 2026-05-24 (k) — BA81: Infeasible při SoC = 100 % (telemetrie = soc_max)
|
||
|
||
**Problém:** Po v6 stále `Solver: Infeasible` při replanu, když `fn_planning_site_context` vrátí `soc_wh = soc_max_wh` (12 500).
|
||
|
||
**Příčina:** Při dlouhém `sell < 0` a vysoké FVE MILP potřebuje alespoň ~**650 Wh** rezervy pod `soc_max` pro modelování PV→baterie / export B. Na přesně 100 % SoC je model neřešitelný (reprodukce na datech runu 16184).
|
||
|
||
**Oprava:** tag **`2026-05-24-ba81-soc-headroom-v7`** — `_planner_soc_for_solver()` sníží vstupní SoC na `soc_max − max(650 Wh, 0,382×slot_nabíjení)`; v `solver_params.inputs.soc_headroom_applied_wh` je audit.
|
||
|
||
**Ověření:** `pytest …::NegativeSellPvChargeTests`; replan BA81 s telemetrií 100 % → tag v7, bez Infeasible.
|
||
|
||
---
|
||
|
||
## 2026-05-24 (j) — BA81: Solver Infeasible (plná baterie + pole B + GEN cut-off)
|
||
|
||
**Problém:** Po deployi večerních oprav u BA81 plánování padá na **`Solver: Infeasible`** (KV1 OK), typicky při **SoC ≈ 100 %** během dlouhého okna `sell < 0` (dnešní OTE).
|
||
|
||
**Příčiny (dvě vrstvy):**
|
||
|
||
1. **v5:** `ge_pv = 0` z pv_store při `pv_b > 0` → oprava `ge_pv ≤ pv_b`.
|
||
2. **v6 (skutečný blocker u BA81):** `deye_gen_microinverter_cutoff_enabled` společně s `sell < 0` vynucovalo **`ge == 0`** (podmínka `z_gen_cutoff is not None`). Při plné baterii nelze nabít ani exportovat přebytek pole B → Infeasible. BA81 má v kontextu `soc_wh = soc_max_wh = 12 500`.
|
||
|
||
**Oprava:** tag **`2026-05-24-ba81-gen-cutoff-v6`** — `ge == 0` jen při `block_export_on_negative_sell`; `ge_pv ≤ pv_b × (1 − z_gen_cutoff)`; v5 večerní push + pv_b cap zůstávají.
|
||
|
||
**Ověření:** `pytest backend/tests/test_planning_dispatch_milp.py::NegativeSellPvChargeTests`; MCP po deployi: `planner_build_tag = 2026-05-24-ba81-gen-cutoff-v6`.
|
||
|
||
---
|
||
|
||
## 2026-05-24 — Arbitráž: OTE místo hodin, export ve špičkách, FVE při sell<0
|
||
|
||
**Problém:** Plán ukazoval slabé nabíjení/vybíjení (KV1, BA81) přestože ekonomika (OTE) favorizovala opak. Ve špičkách MILP nevybíjel baterii naplno; noc BA81 držela SoC na rezervě bez exportu; záporný výkup neplnil FVE do baterie.
|
||
|
||
**Změny:**
|
||
|
||
| Oblast | Co | Proč |
|
||
|--------|-----|------|
|
||
| **R__063 — exportní maska** | Místo pevného vyloučení **00–04** na den prvního `sell<0`: slot vynechat z rozpočtu Wh jen pokud **existuje pozdější slot tentýž den** (před prvním `sell<0`) s `sell > sell_slot + degradace`. | Řídit se **OTE cenami**, ne hodinami. BA81 noc může exportovat; home-01 půlnoc se vynechá, pokud je lepší sell ráno. |
|
||
| **R__063 — fixní tarif** | Discharge kandidáti: `sell > buy + degradace` (ne jen `sell > degradace`). | U BA81/KV1 export jen když je výkup nad fixním nákupem. |
|
||
| **R__063 — PV vrstva A** | `allow_charge` z FVE při `sell < 0` **bez** filtru `future_sell_lookahead`; filtr „drž na večerní peak“ jen pro `sell ≥ 0`. | V záporném výkupním okně nabít z FVE (KV1 `block_export`). |
|
||
| **LP — export shortfall** | Penalizace nevyužitého exportu na **`ge_bat`**, ne na `ge`; pro **všechny** `allow_discharge_export` sloty s kladnou marží (`sell > acquisition` resp. `sell > buy + degrad` u fixed). | Dříve jen `high_sell_slot` (globální max lookahead) → většina večerních slotů bez tlaku na vývoz. |
|
||
| **LP — ge_bat push** | Min. ~8 kW export z baterie ve **všech** ekonomicky výhodných discharge slotech (ne jen večer/ráno seznam). | Plán má odpovídat „vylije co dá síť“ ve špičkách. |
|
||
| **LP — záporný sell + block_export** | `charge_slots` rozšířeny o sloty `sell<0` s PV přebytkem; měkká penalizace `pv_charge_shortfall` (`bc_pv` vs přebytek FVE). | Postupné nabíjení / curtail místo plné FVE do baterie. |
|
||
|
||
**Soubory:** `db/routines/R__063_fn_load_planning_slots_full.sql`, `backend/services/planning_engine.py`, `backend/tests/test_planning_charge_slot_selection.py`, `docs/04-modules/planning.md`.
|
||
|
||
**Neměněno (záměrně):**
|
||
|
||
- `reserve_soc_percent` u BA81 (**30 %**) — podlaha pro **prodej do sítě**; pod ní jen dům. Noc držela 30 % kvůli **zakázanému exportu v masce**, ne kvůli špatné rezervě.
|
||
- Ranní export 5–11 před `sell<0`, večerní peak ≥17, kotva SoC — beze změny.
|
||
|
||
**Ověření po deployi:**
|
||
|
||
1. Flyway repeatable `R__063` + restart backendu.
|
||
2. Rolling replan BA81 / KV1 / home-01.
|
||
3. MCP: noc BA81 — `allow_discharge_export=true` kde není lepší sell později; večer `abs(battery_setpoint_w)` řádově kW u slotů s `export_mode=BATTERY_SELL`.
|
||
4. `pytest backend/tests/test_planning_dispatch_milp.py backend/tests/test_planning_charge_slot_selection.py`
|
||
|
||
---
|
||
|
||
## 2026-05-24 (b) — Po deployi: export stále slabý (oprava #2)
|
||
|
||
**Problém:** Po prvním deployi MCP stále `max_discharge ~300 W`, KV1 `allow_charge=false` při `sell<0`, 0× `BATTERY_SELL` u BA81/KV1. home-01 částečně OK (backend běží).
|
||
|
||
**Příčiny z MCP:**
|
||
|
||
1. **Flyway `R__063` neaplikovaný** na DB → masky bez `allow_charge` u záporného výkupu (`ch_true=0` na celém runu KV1).
|
||
2. **Fixed marže:** `_slot_profitable_battery_export` používal `buy` v slotu (predikce 4,08 Kč) místo **`charge_acquisition`** (~3,09) → večerní export vypnutý i při `sell` 3,7.
|
||
3. **`ge_bat ≤ max_export × z_export`:** solver volil `z_export=0` → `ge_bat=0` navzdory push.
|
||
4. **Safety SoC floor** (~91 %) na ne-high-sell večerních slotech → téměř žádný export.
|
||
|
||
**Opravy:**
|
||
|
||
| Změna | Soubor |
|
||
|--------|--------|
|
||
| Explicitní `allow_charge` pro `sell<0` + `pv_surplus>0` | `R__063` |
|
||
| Marže exportu: vždy `sell > acquisition + degrad` | `planning_engine._slot_profitable_battery_export` |
|
||
| `ge_bat` push bez násobení `z_export`; `z_export ≥ ge_bat/max_export` | `solve_dispatch` |
|
||
| Safety export floor ne na `profitable_export_ts` | `solve_dispatch` |
|
||
| Tvrdé `bc_pv ≥ 0.9×pv_surplus` v `charge_slots` + `sell<0` | `solve_dispatch` |
|
||
| Penalizace shortfall 40 / 25 Kč/kWh | konstanty |
|
||
|
||
**Deploy checklist (povinné obojí):**
|
||
|
||
```bash
|
||
# 1) SQL masky
|
||
flyway migrate # nebo deploy skript s R__063
|
||
|
||
# 2) Backend
|
||
docker compose build ems-api && docker compose up -d ems-api
|
||
# rolling replan nebo počkat :15
|
||
```
|
||
|
||
**Ověření v MCP:**
|
||
|
||
```sql
|
||
-- musí být > 0 po novém runu KV1:
|
||
select count(*) from ems.planning_run pr,
|
||
jsonb_array_elements(pr.solver_params->'masks') m
|
||
where pr.site_id=4 and pr.status='active'
|
||
and (m->>'allow_charge')::boolean
|
||
and (select effective_sell_price from ems.planning_interval pi
|
||
where pi.run_id=pr.id and pi.interval_start=(m->>'slot')::timestamptz) < 0;
|
||
```
|
||
|
||
---
|
||
|
||
## 2026-05-24 (c) — BA81: fixní tarif bez grid nabíjení
|
||
|
||
**Problém:** Po deployi run **15810** — `max_chg ≈ 3275 W`, **`allow_grid_charge = 0`** na všech slotech. Noc 00–04 jen import pro dům (~100 W), žádné NT nabíjení ze sítě. HW limit BA81 je **6250 W** (`bms_max_charge_w`), ne 18 kW.
|
||
|
||
**Příčina:** V `R__063` vrstva **B (grid)** běžela jen pro `purchase_pricing_mode <> 'fixed'`. BA81 má **`fixed`** → masky povolily jen **PV vrstvu A** (Wh rozpočet rozdělený přes denní FVE sloty → postupné ~3 kW).
|
||
|
||
**Oprava:** Pro `fixed` + existuje arbitráž (`sell > buy + degrad`) → stejná AM/PM logika grid slotů jako u spotu, řazení podle času slotu (`slot_ord`), před `export_window_start`.
|
||
|
||
**Ověření po `flyway migrate` + replan:**
|
||
|
||
```sql
|
||
select count(*) filter (where (m->>'allow_grid_charge')::boolean) as grid_slots
|
||
from ems.planning_run pr, jsonb_array_elements(pr.solver_params->'masks') m
|
||
where pr.site_id = (select id from ems.site where code='BA81') and pr.status='active';
|
||
-- očekáváno > 0
|
||
|
||
select max(pi.battery_setpoint_w), max(pi.grid_setpoint_w) filter (where pi.grid_setpoint_w > 1000)
|
||
from ems.planning_interval pi
|
||
join ems.planning_run pr on pr.id = pi.run_id
|
||
where pr.site_id = (select id from ems.site where code='BA81') and pr.status='active';
|
||
-- battery/grid nabíjení řádově k 6250 W v NT slotech
|
||
```
|
||
|
||
---
|
||
|
||
## 2026-05-24 (d) — BA81: grid jen 1 slot (globální export okno)
|
||
|
||
**Problém:** Run **15820** — mírné zlepšení (1× ~4,5 kW grid+bat o půlnoci), ale **00:45–05:45 `allow_charge=false`**, max nabíjení pořád ~3,3 kW z FVE.
|
||
|
||
**Příčina:** `v_export_window_start` = **min přes celý horizont** (včerejší večerní sell 3,7 → čas ~22:15). Grid vrstva B řadí „před oknem“ vůči tomuto **jednomu** času → dnešní NT sloty (00–06) už jsou „po okně“ a nedostanou `allow_grid_charge`.
|
||
|
||
**Oprava:** Sloupec **`export_window_start_at` per kalendářní den** (Prague); grid AM/PM i `buy_min_next_n` používají `wk.interval_start < wk.export_window_start_at`.
|
||
|
||
**Deploy:** `flyway migrate` (R__063) + replan.
|
||
|
||
---
|
||
|
||
## 2026-05-24 (e) — BA81: FVE 13 kW → nabíjení jen ~3 kW (curtailment)
|
||
|
||
**Problém:** Run **15826** — `pv≈13 kW`, `battery_setpoint≈3,3 kW`, **`pv_a_curtailed≈9 kW`** (08:00–08:45). `allow_charge=true`, ale solver škrtí FVE místo plného nabíjení.
|
||
|
||
**Příčina:**
|
||
|
||
1. **`CURTAILMENT_PENALTY = 0,001 Kč/Wh`** vs degradace nabíjení → LP raději `ca` než `bc_pv`.
|
||
2. **`pv_charge_shortfall`** jen při `block_export_on_negative_sell` (KV1) — **BA81 má false** → žádný tlak na `bc_pv`.
|
||
3. SoC v plánu stagnuje ~52 % při záporném výkupu, zbytek jde do curtailment.
|
||
|
||
**Oprava (`planning_engine.py`):**
|
||
|
||
- `pv_charge_shortfall` pro **všechny** sloty `sell<0` + `allow_charge` + PV přebytek >500 W.
|
||
- Penalizace **50 Kč/kWh**.
|
||
- Tvrdé **`ca ≤ pv_a_forecast − bc_pv`** v okně záporného výkupu (nejdřív nabít, pak škrtit).
|
||
|
||
**Deploy:** restart **backend** (SQL beze změny) + replan.
|
||
|
||
---
|
||
|
||
## 2026-05-24 (f) — BA81: jen první slot sell<0 nabíjí 6 kW, další 1–2 kW
|
||
|
||
**Problém:** Run **15838** — 06:15 Prague ~6,1 kW, 06:30–07:30 ~1,4–2,2 kW, 07:45–08:45 **0 kW** + curtail ~9 kW, 09:00+ znovu ~3 kW. Uživatel: „jen u prvního slotu se zápornou cenou“.
|
||
|
||
**Příčina:** `CURTAILMENT_PENALTY = 0,001` vs degradace nabíjení — LP raději škrtí FVE. Oprava (e) pomohla jen prvnímu slotu (shortfall). Omezení `ca ≤ pv_a − bc_pv` bylo **špatně** (load-first: `pv_a_net` už závisí na `ca`). SoC v plánu stála ~51 % uprostřed okna, zbytek do curtailment.
|
||
|
||
**Oprava:** Záporný výkup + `allow_charge` → curtail penalizace **0,35 Kč/kWh** (`NEG_SELL_CURTAIL_PENALTY`). Shortfall nabíjení **80 Kč/kWh**. Odstraněno `ca ≤ pv_a − bc_pv`.
|
||
|
||
**Deploy:** jen **backend** restart + replan.
|
||
|
||
---
|
||
|
||
## 2026-05-24 (g) — BA81: plateau ~51 % SoC + curtail (run 15848/15849)
|
||
|
||
**Problém:** Po replanu stále 06:15 ~6 kW, 06:30–07:30 ~1–2 kW, **07:45–08:45 0 kW + curtail ~9 kW**, SoC plán ~51 %, pak znovu ~3 kW. `solver_params` bez `planner_build_tag` → nasazený backend pravděpodobně **bez** oprav (e)/(f).
|
||
|
||
**Příčiny (MCP + kód):**
|
||
|
||
1. **`charge_slots` v Pythonu** doplňoval `sell<0` jen při `block_export_on_negative_sell` (KV1). U BA81 (`false`) platily jen masky z DB → bez shortfall penalizace, i když R__063 nastaví `allow_charge` později.
|
||
2. **`safety_soc_target_wh`** z SQL roste jen k ~reserve + noční baseload (~50 % SoC v poledne). Jakmile `soc ≥ safety`, solver nemá motivaci dobít k `soc_max` v okně záporného výkupu (raději curtail / večerní export).
|
||
3. **`skip_pv_store_block`** u `pv_b` + fixní tarif: LP smí exportovat FVE při `sell<0` místo nabíjení (home-01 logika nepatří na BA81).
|
||
|
||
**Oprava (`planning_engine.py`):**
|
||
|
||
- `charge_slots` |= všechny sloty `sell<0` + PV přebytek > 500 W (jako R__063 ř. 787–791).
|
||
- V okně `sell<0` + `charge_slots`: safety deficit cílí na `max(safety_sql, 92 % soc_max)`.
|
||
- Fixní tarif: `ge_pv ≤ pv_b_forecast_w` při `sell<0`; `skip_pv_store` jen pro spot, ne fixed.
|
||
- Objective: odměna `bc_pv` při `sell<0` (`NEG_SELL_PV_CHARGE_REWARD`).
|
||
- `solver_params.planner_build_tag` = `2026-05-24-neg-sell-v2` (ověření deploye).
|
||
|
||
**Deploy:** `docker compose build ems-api && docker compose up -d ems-api` + rolling replan BA81.
|
||
|
||
**Ověření MCP:**
|
||
|
||
```sql
|
||
select pr.id, pr.solver_params->>'planner_build_tag' as tag,
|
||
max(pi.battery_setpoint_w) filter (where pi.effective_sell_price < 0) as max_neg_chg
|
||
from ems.planning_run pr
|
||
join ems.planning_interval pi on pi.run_id = pr.id
|
||
where pr.site_id = (select id from ems.site where code = 'BA81')
|
||
order by pr.id desc limit 1;
|
||
```
|
||
|
||
Očekáváno: `tag = 2026-05-24-neg-sell-v2`, v ranním okně `sell<0` více slotů s `battery_setpoint_w` ≥ 5000, SoC plán přes ~70 % směrem k 95 %.
|
||
|
||
---
|
||
|
||
## 2026-05-24 (i) — Večerní export BA81/KV1 + BA81 dobít na 100 %
|
||
|
||
**Problém:** Po v3 KV1 nabíjení OK, BA81 stále plateau ~94 % v neg. okně. **Večer žádný prodej** z baterie ani při sell ~3,7 Kč (BA81 i KV1).
|
||
|
||
**Příčiny:**
|
||
|
||
1. **`_slot_profitable_battery_export`:** u fixního tarifu porovnával `sell > acquisition + degrad` (BA81 acq ~3,61 → potřeba sell > ~3,91). Správně **`sell > buy + degrad`** jako v R__063.
|
||
2. **KV1 večer:** SQL večerní maska vyžadovala `sell > buy` (6,35 vs 3,7) → **`allow_discharge_export = false`**.
|
||
3. **LP:** `ge_bat >= export_push * z_export` — solver nechal **`z_export = 0`** (export „zdarma“ bez nutnosti).
|
||
|
||
**Oprava:** `planning_engine.py` tag **`2026-05-24-evening-export-v4`**; `R__063` večerní peak u fixed tarifu bez podmínky sell>buy. Měkký push `ge_bat`, odměna `z_export`, `neg_sell_soc_underfill`, večerní export floor = min_soc.
|
||
|
||
**Deploy:** `flyway migrate` (R__063) + rebuild `ems-api` + replan. MCP: `planner_build_tag = 2026-05-24-evening-export-v4`, večer `export_mode = BATTERY_SELL` nebo `grid_setpoint_w < -1000` v špičce.
|
||
|
||
---
|
||
|
||
## 2026-05-24 (h) — BA81: neg okno na plné soc_max (ne 92 %)
|
||
|
||
**Problém:** Po (g) plán lépe nabíjí v okně `sell<0`, ale SoC plán končí ~**92 %** a drží se do přechodu na kladný výkup; až pak dobíjí na 100 %.
|
||
|
||
**Příčina:** `NEG_SELL_CHARGE_SOC_FRAC_OF_MAX = 0.92` — umělý strop safety cíle v neg. okně.
|
||
|
||
**Oprava (`planning_engine.py`, tag **`2026-05-24-neg-sell-v3`**):
|
||
|
||
- Záporný výkup + PV: safety/shortfall cílí **`soc_max_wh`** (u BA81 100 %), ne 92 %.
|
||
- Po posledním `sell<0` tentýž den: **`post_neg_pv_topup`** — dobití z FVE na `soc_max` před exportem při kladném sell (ne ve high-sell špičce).
|
||
|
||
**Deploy:** rebuild `ems-api` + replan. MCP: `planner_build_tag = 2026-05-24-neg-sell-v3`, SoC v neg. okně ~100 % (resp. `planner_soc_max_wh`).
|
||
|
||
---
|
||
|
||
## Šablona pro další záznamy
|
||
|
||
```markdown
|
||
## YYYY-MM-DD — Krátký titul
|
||
|
||
**Problém:** …
|
||
|
||
**Změny:** …
|
||
|
||
**Soubory:** …
|
||
|
||
**Ověření:** …
|
||
```
|