16 KiB
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-25 (l) — Plán 25. 5.: BA81 neg. výkup bez exportu, KV1 ranní curtail
Problém (MCP plán run 16346–16350, tag v7): KV1 06–08 h masivní curtail FVE (plná baterie, ge_pv=0 z pv_store). BA81 při sell<0 export ~10 kW místo nabíjení. Večer slabý export u KV1/home-01 (spot: sell < buy).
Oprava (tag 2026-05-25-neg-sell-no-export-fixed-v8):
- Fixní tarif (BA81): při
sell < 0tvrděge = 0(jako KV1 s block_export) — přebytek jen baterie/curtail. fixed_pv_b_export_capjen přisell ≥ 0(po neg. okně export B).- KV1:
skip_pv_store_blockpři kladnémsell+ PV přebytek — méně curtailu před neg. oknem.
Deploy: služba v compose je backend, ne ems-api. Ověření:
docker compose -f /opt/ems-deploy/docker-compose.yml exec backend grep PLANNER_BUILD_TAG /app/services/planning_engine.py
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):
- v5:
ge_pv = 0z pv_store připv_b > 0→ opravage_pv ≤ pv_b. - v6 (skutečný blocker u BA81):
deye_gen_microinverter_cutoff_enabledspolečně ssell < 0vynucovaloge == 0(podmínkaz_gen_cutoff is not None). Při plné baterii nelze nabít ani exportovat přebytek pole B → Infeasible. BA81 má v kontextusoc_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_percentu 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:
- Flyway repeatable
R__063+ restart backendu. - Rolling replan BA81 / KV1 / home-01.
- MCP: noc BA81 —
allow_discharge_export=truekde není lepší sell později; večerabs(battery_setpoint_w)řádově kW u slotů sexport_mode=BATTERY_SELL. 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:
- Flyway
R__063neaplikovaný na DB → masky bezallow_chargeu záporného výkupu (ch_true=0na celém runu KV1). - Fixed marže:
_slot_profitable_battery_exportpoužívalbuyv slotu (predikce 4,08 Kč) místocharge_acquisition(~3,09) → večerní export vypnutý i přisell3,7. ge_bat ≤ max_export × z_export: solver volilz_export=0→ge_bat=0navzdory push.- 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í):
# 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:
-- 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:
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:
CURTAILMENT_PENALTY = 0,001 Kč/Whvs degradace nabíjení → LP radějicanežbc_pv.pv_charge_shortfalljen přiblock_export_on_negative_sell(KV1) — BA81 má false → žádný tlak nabc_pv.- SoC v plánu stagnuje ~52 % při záporném výkupu, zbytek jde do curtailment.
Oprava (planning_engine.py):
pv_charge_shortfallpro všechny slotysell<0+allow_charge+ PV přebytek >500 W.- Penalizace 50 Kč/kWh.
- Tvrdé
ca ≤ pv_a_forecast − bc_pvv 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):
charge_slotsv Pythonu doplňovalsell<0jen přiblock_export_on_negative_sell(KV1). U BA81 (false) platily jen masky z DB → bez shortfall penalizace, i když R__063 nastavíallow_chargepozději.safety_soc_target_whz SQL roste jen k ~reserve + noční baseload (~50 % SoC v poledne). Jakmilesoc ≥ safety, solver nemá motivaci dobít ksoc_maxv okně záporného výkupu (raději curtail / večerní export).skip_pv_store_blockupv_b+ fixní tarif: LP smí exportovat FVE přisell<0místo nabíjení (home-01 logika nepatří na BA81).
Oprava (planning_engine.py):
charge_slots|= všechny slotysell<0+ PV přebytek > 500 W (jako R__063 ř. 787–791).- V okně
sell<0+charge_slots: safety deficit cílí namax(safety_sql, 92 % soc_max). - Fixní tarif:
ge_pv ≤ pv_b_forecast_wpřisell<0;skip_pv_storejen pro spot, ne fixed. - Objective: odměna
bc_pvpřisell<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:
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:
_slot_profitable_battery_export: u fixního tarifu porovnávalsell > acquisition + degrad(BA81 acq ~3,61 → potřeba sell > ~3,91). Správněsell > buy + degradjako v R__063.- KV1 večer: SQL večerní maska vyžadovala
sell > buy(6,35 vs 3,7) →allow_discharge_export = false. - LP:
ge_bat >= export_push * z_export— solver nechalz_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<0tentýž den:post_neg_pv_topup— dobití z FVE nasoc_maxpř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
## YYYY-MM-DD — Krátký titul
**Problém:** …
**Změny:** …
**Soubory:** …
**Ověření:** …