zasadni uprava LP planneru
This commit is contained in:
@@ -127,6 +127,8 @@ Marže se konfigurují v `site_market_config`:
|
||||
|
||||
Denní ekonomika v DB (`ems.fn_economics_daily_for_window`, repeatable `R__068_fn_economics_daily_month.sql`) musí používat stejnou kombinaci jako `fn_effective_buy_price` (komentář ve funkci).
|
||||
|
||||
**Plánování:** efektivní `buy_price` per 15min slot už nese skok **VT→NT** (distribuce v `fn_effective_buy_price`). Maska grid nabíjení v `fn_load_planning_slots_full` navíc vyžaduje `buy ≤ min(buy v příštích 4 slotech) + ε`, aby se neplánoval import v posledním VT slotu před levným NT — viz `docs/04-modules/planning.md`.
|
||||
|
||||
### Screening skript pro dimenzování baterie
|
||||
|
||||
Analytický skript `scripts/analysis/battery_sizing_screen.py` umí pro nákup v režimu spot simulovat dva užitečné screening režimy bez vazby na konkrétní `site_market_config`:
|
||||
|
||||
@@ -9,10 +9,12 @@
|
||||
- **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).
|
||||
- **Masky `allow_charge` / `allow_discharge_export` (anti-mikrocyklování):** generuje `ems.fn_load_planning_slots_full`. Dvě nezávislé vrstvy pro nabíjení:
|
||||
- **PV-surplus sloty** (`pv_surplus_w > 0`): ranking dle `sell_price ASC`. Nejlevnější PV-surplus sloty se vybírají, dokud kumulativní PV surplus × η_charge nepokryje `energy_to_fill × charge_slot_buffer`. Zbylé PV-surplus sloty mají `allow_charge=false` → PV jde rovnou do sítě. V drahých slotech se PV prodává, v levných nabíjí baterie.
|
||||
- **Non-PV sloty** (`pv_surplus_w <= 0`): AM/PM rozpočet 50/50, řazení dle `is_predicted_price::int ASC, buy_price ASC` — **jen pokud** `site_market_config.purchase_pricing_mode <> 'fixed'`. U **fixního nákupu** (KV1) se vrstva B **nepoužívá**: `buy` je v každém slotu stejný, grid nabíjení by byl čistá ztráta cyklu; nabíjení jen z **PV přebytku** (vrstva A).
|
||||
- Pokud `energy_to_fill <= 0` (baterie plná) nebo `charge_slot_buffer = 0`: všechny sloty povoleny.
|
||||
- **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).
|
||||
- **Non-PV grid (vrstva B):** jen **spot** nákup (`purchase_pricing_mode <> 'fixed'`), `buy ≤ min(buy horizontu) + degradation`, **lookahead** `buy ≤ min(buy v příštích 4 slotech) + 0,05 Kč` (VT→NT), max **6 slotů** AM a PM; AM/PM rozpočet 50/50 z `grid_target`. **KV1/fixed:** vrstva B vypnutá.
|
||||
- **`ref_buy` pro export baterie:** `min(buy_price)` celého horizontu (ne jen z `allow_charge`). Export sloty: `sell > ref_buy + degradation` (spot) / `sell > degradation` (fixed).
|
||||
- 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.
|
||||
- **Denní safety charge (měkké LP, ne maska):** `fn_load_planning_slots_full` (**V077+**) vrací navíc odhad nočního baseload Wh (20:00–06:00 Europe/Prague), buffer % z `asset_battery.planner_night_baseload_buffer_percent`, lookahead `future_*_czk_kwh`, volitelný `safety_soc_target_wh` (6–19) 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 high‑sell š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 high‑sell š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`).
|
||||
@@ -30,7 +32,7 @@
|
||||
- měkký cíl na konci 24h přes `_soc_security_profile` + tvrdé dvouúrovňové pravidlo výše.
|
||||
- **Dynamická ekonomická podlaha (fáze 2):**
|
||||
- `_dynamic_arb_floor_wh_series`: podle součtu FVE výkonu v dalších ~8 h (`ARB_LOOKAHEAD_SLOTS`) se `arb_floor_wh[t]` posouvá mezi `min_soc_wh` a rezervou z DB – silné očekávané slunce ji sníží (ráno / po obloze); vynutit konstantní chování lze `battery.disable_dynamic_arb_floor=True` jen pro testy / ladění.
|
||||
- **Výběr exportních slotů (`allow_discharge_export`):** `ems.fn_load_planning_slots_full` označí jen sloty, kde smí solver **úmyslně** vybíjet baterii do sítě (SELL). Výběr je **globálně** podle `sell_price desc` (ne AM/PM 50/50). **Spot nákup:** `sell_price > ref_buy + degradation_cost_czk_kwh` (`ref_buy` = min `buy` mezi `allow_charge`, arbitráž mezi sloty). **Fixní nákup** (`purchase_pricing_mode = fixed`): `sell_price > degradation_cost_czk_kwh` (prodej na spotu, bez porovnání s fixním 6,35 Kč). V `solve_dispatch` (AUTO) je export rozdělen: **`ge_pv`** (kanál FVE) a **`ge_bat`** (baterie do sítě, jen v `allow_discharge_export`, vázáno na `z_export` a SoC podlahu); platí `ge = ge_pv + ge_bat` a `ge_bat ≥ ge − (pv_a + pv_b)` — baterie nesmí „přestrojit“ FVE. Mimo exportní sloty: **`ge_bat = 0`**, **`bd`** smí pokrýt vlastní spotřebu; **`bc`** smí nabíjet jen z **PV přebytku** i bez grid-charge masky (plná baterie + přebytek pole B jinak nejde do sítě). **`deye_physical_mode`** = PASSIVE kromě CHARGE/SELL.
|
||||
- **Výběr exportních slotů (`allow_discharge_export`):** `ems.fn_load_planning_slots_full` označí jen sloty, kde smí solver **úmyslně** vybíjet baterii do sítě (SELL). Výběr je **globálně** podle `sell_price desc` (ne AM/PM 50/50). **Spot nákup:** `sell_price > ref_buy + degradation_cost_czk_kwh` (`ref_buy` = **min `buy` horizontu**). **Fixní nákup** (`purchase_pricing_mode = fixed`): `sell_price > degradation_cost_czk_kwh` (prodej na spotu, bez porovnání s fixním 6,35 Kč). V `solve_dispatch` (AUTO) je export rozdělen: **`ge_pv`** (kanál FVE) a **`ge_bat`** (baterie do sítě, jen v `allow_discharge_export`, vázáno na `z_export` a SoC podlahu); platí `ge = ge_pv + ge_bat` a `ge_bat ≥ ge − (pv_a + pv_b)` — baterie nesmí „přestrojit“ FVE. Mimo exportní sloty: **`ge_bat = 0`**, **`bd`** smí pokrýt vlastní spotřebu; **`bc`** smí nabíjet jen z **PV přebytku** i bez grid-charge masky (plná baterie + přebytek pole B jinak nejde do sítě). **`deye_physical_mode`** = PASSIVE kromě CHARGE/SELL.
|
||||
- **Záporná nákupní cena:**
|
||||
- horní mez `grid_import` zahrnuje `load_baseline_w` + nabíjení/EV/TČ (bez nekonečného importu).
|
||||
- **Záporná prodejní cena → tvrdý zákaz vývozu (`ge = 0`)** (`planning_engine.solve_dispatch`): platí ve slotu kde `sell_price < 0`, pokud lokality zapne některou z opcí —
|
||||
@@ -56,8 +58,8 @@ where allow_charge is true
|
||||
order by interval_start;
|
||||
```
|
||||
|
||||
- PV-surplus sloty: `allow_charge=true` jen pro nejlevnější (dle `sell_price`), dokud se nepokryje charge target.
|
||||
- Non-PV sloty: AM/PM budget, OTE sloty mají přednost před predikovanými (ORDER BY `is_predicted_price::int, buy_price`).
|
||||
- PV-surplus: `allow_charge=true` pro nejvyšší `store_score`, dokud se nepokryje `grid_target`.
|
||||
- Non-PV: levný `buy`, lookahead 4 sloty, cap 6/segment; OTE před predikovanými.
|
||||
- Pokud `current_soc_wh` odpovídá plné baterii (`soc_max_wh`), jsou povoleny všechny sloty.
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user