LP first zjednoduseni
Some checks failed
CI and deploy / migration-check (push) Failing after 12s
CI and deploy / deploy (push) Has been skipped

This commit is contained in:
Dusan Vojacek
2026-05-21 15:41:26 +02:00
parent 649c9e9510
commit c9149babd3
7 changed files with 419 additions and 117 deletions

View File

@@ -104,24 +104,20 @@ Pro **home-01** při nabíjení 11:0014:00 za ~0,70,9 Kč a výprodeji 19:
---
## 6. Implementace (2026-05) a backlog
## 6. Implementace (LP-first přestavba, 2026-05)
### Hotovo (jednoduchá varianta před solve)
### Hotovo
1. **`ems.fn_load_planning_slots_full`** (`R__063`): sloupce
`charge_acquisition_buy_czk_kwh`, `charge_acquisition_cutoff_at`.
2. **Vážený průměr `buy`** v `allow_charge` slotech **před** prvním `allow_discharge_export` (`cutoff`):
`Σ(buy × per_slot_charge_wh) / Σ(per_slot_charge_wh)` — bez `future_sell` z odpolední FVE (jinak acquisition nafukovala večerní export).
3. **`solve_dispatch`:** v exportních slotech (`allow_discharge_export`) přičíst k objective
`+ ge_bat[t] × charge_acquisition × INTERVAL_H/1000` (náklad uložené energie), ponechat `ge×sell`. Snapshot v `solver_params.inputs`.
4. **FVE opportunity:** varianta **B** — lookahead `future_sell_opportunity`, ne jen `sell[t]` v odhadu PV Wh.
1. **`ems.fn_load_planning_slots_full`** (`R__063`): grid **B** = nejlevnější sloty v AM/PM do Wh rozpočtu (bez `buy≤min+band` a lookahead gate na grid); **A** = PV jen pokud `sell ≥ future_sell_lookahead degrad`. `charge_acquisition` z `allow_grid_charge` před 1. exportem.
2. **`solve_dispatch` (AUTO):** objective `gi×buy ge_pv×sell ge_bat×sell + ge_bat×acquisition` (export bat. jen v `allow_discharge_export`). Odstraněn cross-slot guard `ge_pv ≥ surplus` / `bc=0` dle `export_refill_net`.
3. **Guard FVE:** `ge_pv=0` jen při `sell < charge_acquisition degrad` (ne `sell < buy` ve stejném slotu).
4. **`solve_dispatch_two_pass`:** pass 1 → vážený `buy` z `bc`+`gi` v `allow_charge` → pass 2; volá `run_daily_plan` / `run_rolling_replan` v AUTO. Snapshot: `acquisition_pass1_czk_kwh`, `acquisition_pass2_czk_kwh`, `two_pass_enabled`.
5. **Regrese:** `Home01RegressionTests` v `backend/tests/test_planning_dispatch_milp.py`; masky v `test_planning_charge_slot_selection.py`.
### Zbývá / vylepšit
### Co dál neřešit ad-hoc
1. **Iterace po solve:** přepočítat acquisition z plánovaných `bc`+`gi` / `pv` místo odhadu z masek — viz [`docs/05-todo.md`](../05-todo.md).
2. **Objective:** explicitní `ge_bat × (sell acquisition degrad)` vs současné `ge×sell` + `+ge_bat×acquisition` (ekvivalentní jen pokud `ge_pv` v exportním slotu ≈ 0).
3. **Masky:** více charge slotů ∝ `energy_to_fill / per_slot_wh` — viz [`planning.md`](planning.md).
4. **Bilance / guardy:** zpřesnit, aby večerní export nebyl vázaný na falešný `gi×buy` v tomže slotu.
- Další Python `if sell < buy` guardy — ekonomiku drží LP + acquisition + masky rozpočtu slotů.
- Multi-period inventory model (větší projekt) — mimo tuto vlnu.
---
@@ -148,4 +144,4 @@ Očekávání: SoC před večerem **7090 %** po levném pásmu; večer **expo
---
*Poslední aktualizace: 2026-05 — charge_acquisition před 1. exportem + LP `ge_bat` náklad; iterace po solve v TODO.*
*Poslední aktualizace: 2026-05 — LP-first přestavba (masky B/A, two-pass acquisition, explicitní ge_pv/ge_bat objective). Po deployi: `solver_params.inputs.two_pass_enabled` na novém `planning_run`.*

View File

@@ -11,11 +11,12 @@
- **Terminal SoC shadow price:** v objective je člen `(avg_buy_prvních_24h × planner_terminal_soc_value_factor / 1000) × soc[T1]` (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).
- **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, buysell)`; 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):** spot, **AM/PM rozpočet Wh 50/50** z `grid_target`. Výběr: nejdřív **kalendářní den plánu** (`p_from` Prague), pak sloty před **výkupním oknem daného dne** (`sell > min(buy téhož dne)+degrad` — ne globální min zítra). Lookahead VT→NT jen před tímto oknem. **`charge_acquisition`:** vážený `buy` jen u `allow_grid_charge` (ne FVE vrstva A).
- **LP (AUTO):** FVE export při `sell < buy` jen pokud `(sell min_buy_v_charge)(future_sell acquisition) + degrad` — jinak PV do baterie na večerní peak. Viz [`planning-arbitrage-accounting.md`](planning-arbitrage-accounting.md) §2.2.
- **Grid ze sítě (vrstva B, před FVE):** spot, **AM/PM rozpočet Wh 50/50** z `grid_target`. Výběr: **nejlevnější `buy`** v pásmu (kalendářní den plánu → před výkupním oknem dne → `buy ASC`), bez pásma `min+0,40` a bez lookahead gate na grid B. **`charge_acquisition`:** vážený `buy` jen u `allow_grid_charge` před 1. exportem; po solve **dvouprůchodově** přepočet z `bc`+`gi` (`solve_dispatch_two_pass` v `planning_engine.py`).
- **PV vrstva A:** jen pokud `sell ≥ future_sell_opportunity degradation` (držet FVE na večerní peak, ne „nabíjet z FVE“ při nízkém sell).
- **LP (AUTO):** objective explicitně `ge_pv×sell ge_bat×sell + ge_bat×acquisition` v exportních slotech; **bez** cross-slot vynucení `ge_pv ≥ surplus`. Guard FVE: `ge_pv=0` jen pokud `sell < charge_acquisition degrad` (ne `sell < buy` ve slotu). Viz [`planning-arbitrage-accounting.md`](planning-arbitrage-accounting.md).
- **`ref_buy_min` (brána exportu):** `min(buy_price)` horizontu — jen „existuje levný nákup?“, **ne** průměrná cena nabití přes hodiny. Export sloty: `sell > ref_buy_min + degradation` (spot). Viz [`planning-arbitrage-accounting.md`](planning-arbitrage-accounting.md).
- Pokud `energy_to_fill <= 0` nebo `charge_slot_buffer = 0`: všechny sloty povoleny.
- **LP ekonomické guardy** (`solve_dispatch`, AUTO): pokud `sell < buy degradation``ge_pv=0` (výjimka: plná baterie, přebytek **pv_b**). Pokud `buy > min(buy)+degradation``gi` jen na load+EV+TČ. Viz `planning_engine.py` sekce po slot pre-selection.
- **LP ekonomické guardy** (`solve_dispatch`, AUTO): `ge_pv=0` pokud `sell < charge_acquisition degradation` (výjimka: plná baterie, přebytek **pv_b**). Pokud `buy > min(buy)+degradation` mimo charge masku`gi` jen na load+EV+TČ. Viz `planning_engine.py` po slot pre-selection.
- **Denní safety charge (měkké LP, ne maska):** `fn_load_planning_slots_full` (**V077+**) vrací navíc odhad nočního baseload Wh (20:0006:00 Europe/Prague), buffer % z `asset_battery.planner_night_baseload_buffer_percent`, lookahead `future_*_czk_kwh`, volitelný `safety_soc_target_wh` (619) a flag `is_daytime_pv_surplus_slot`.\n+\n+ V solveru (`planning_engine.solve_dispatch()`):\n+ - `safety_soc_target_wh` se používá primárně jako **ochrana exportu z baterie**: v běžných slotech (mimo highsell špičky) se při aktivním exportu vynutí `soc[t] ≥ max(arb_base_wh, safety_soc_target_wh)`.\n+ - safety deficit penalizace v objective běží jen v `is_daytime_pv_surplus_slot` (a ne v highsell špičce), aby solver neměl motivaci dělat obecné „nabij co nejdřív“ chování.\n+ Tvrdé `allow_charge` se kvůli tomu nemění.
- **Rolling charge commitment:** při `run_rolling_replan` se z aktivního plánu načtou sloty, kde dříve platilo `battery_setpoint_w > 500`, `pv_a+pv_b > load_baseline`, `grid_setpoint_w ≤ 0` a současně **není výrazný export** (`grid_setpoint_w ≥ 500`). To je záměr: commitment má kotvit „nabíjení z PV přebytku“, ne „charge while exporting“. Měkká penalizace proti snížení `bc[t]` oproti předchozímu plánu je řízená `planner_charge_commitment_penalty_czk_kwh` na `asset_battery`. Implementace: `_load_previous_plan_charge_commitment_prev_w`, volitelný argument `charge_commitment_prev_w` u `solve_dispatch()`.
- **Debug snapshot:** každý běh ukládá JSON do `ems.planning_run.solver_params` (sekce `version`, `inputs`, `masks`, `soc_bounds`, `objective_terms`, `chosen_slots`) přes `fn_planning_run_commit` (`p_run_meta->'solver_params'`). Read-model: **`select ems.fn_planning_run_debug(<run_id>);`** (`R__087_fn_planning_run_debug.sql`).

View File

@@ -26,8 +26,8 @@ Shrnutí otevřených bodů z `docs/06-open-questions.md`, checklistů v modulec
| Popis | Kde | Kdo |
|-------|-----|-----|
| **`charge_acquisition` po solve:** druhé kolo — vážený průměr z plánovaných `bc`+`gi` a PV nabíjení místo odhadu z masek před solve; případně jedna iterace solve. Cutoff před 1. exportem už je v SQL. | `R__063_fn_load_planning_slots_full.sql`, `planning_engine.py`; [`planning-arbitrage-accounting.md`](04-modules/planning-arbitrage-accounting.md) §6 | programátor |
| **Více levných charge slotů** v masce (N ∝ `energy_to_fill / per_slot_wh`, ne jen cap 6). | `R__063` | programátor |
| ~~**`charge_acquisition` po solve (two-pass):**~~ hotovo — `solve_dispatch_two_pass` v `planning_engine.py` (AUTO daily/rolling). | `planning_engine.py`, [`planning-arbitrage-accounting.md`](04-modules/planning-arbitrage-accounting.md) §6 | |
| ~~**Grid maska B (nejlevnější sloty):**~~ hotovo — `buy ASC` v AM/PM do Wh rozpočtu; cap z `ceil(budget/per_slot_wh)`. | `R__063` | |
---