---
name: Unify PV correction source
status: draft
owner: cursor-agent
---

## Cíl

Uděláme **single source of truth** pro PV forecast používaný v plánování tak, aby:

- **solver** i **UI** četly *stejnou* PV řadu (žádné „dvě korekce dvěma cestami“),
- kanonický výpočet byl v **PostgreSQL**,
- výsledky byly auditovatelné (raw vs delta vs rolling-factor + decay).

## Zjištěný problém (dnes)

- Solver používá PV z DB + **multiplikativní rolling faktor** v Pythonu (`compute_correction_factor` + `apply_forecast_correction` v `backend/services/planning_engine.py`).
- UI (Planning tabulka) zobrazuje PV přes endpoint **delta-korekce** (`/forecast/pv-slots-corrected` → `ems.fn_forecast_pv_slots_range_corrected`), což může být jiné číslo než PV, se kterým solver počítal.
- Důsledek: v tabulce slotů nesedí výkonová bilance (UI ukáže např. 5.9 kW, ale plán implicitně pracuje s ~10.4 kW).

## Cílové chování (nová kanonická DB řada)

Kanonické PV pro plánování definujeme jako kombinaci obou korekcí:

1. **Delta-korekce (aditivní)** per PV array (odečíst `delta_profile[slot_of_day]`, clamp na 0)
2. Agregace do **PV-A / PV-B** podle `ems.asset_pv_array.controllable`
3. **Rolling faktor (multiplikativní)** z `ems.fn_pv_forecast_correction_factor(...)` aplikovaný na PV-A i PV-B
4. **Decay (lineární útlum)** faktoru podle offsetu slotu od `now` (stejná logika jako dnes v `apply_forecast_correction`)

Výstup této kanonické řady se musí propsat do:

- `ems.fn_load_planning_slots_full` (vstup pro solver),
- `ems.fn_plan_current_bundle` (výstup pro UI),
- a do uložených sloupců `planning_interval.pv_*_forecast_solver_w` (audit).

## Návrh DB API (kanonická funkce)

Přidat novou repeatable rutinu, např.:

- `db/routines/R__0xx_fn_forecast_pv_slots_range_canonical_ab.sql`

Funkce vrátí JSON pole slotů pro `[from, to)` s minimálně:

- `interval_start`
- `pv_a_forecast_raw_w`, `pv_b_forecast_raw_w`
- `pv_a_forecast_delta_w`, `pv_b_forecast_delta_w` (po delta-korekci)
- `rolling_factor` (globální faktor) + `rolling_effective_factor` (po decay pro slot)
- `pv_a_forecast_canonical_w`, `pv_b_forecast_canonical_w` (delta × rolling_effective_factor)

Poznámky:

- Delta profil už dnes existuje (`ems.fn_pv_forecast_delta_profile`) a `fn_forecast_pv_slots_range_corrected` už umí per-array delty; tu logiku zrecyklujeme.
- Rolling faktor už dnes existuje v DB (`ems.fn_pv_forecast_correction_factor`), jen se dnes aplikuje v Pythonu.
- Decay parametrizovat (např. `p_decay_slots int default 16`, `p_min_clamp numeric`, `p_max_clamp numeric`, `p_window_h numeric`).

## Změny solveru (Python)

V `backend/services/planning_engine.py`:

- Přepnout loader PV slotů na kanonickou DB řadu (A/B corrected).
- **Odstranit** aplikaci PV korekce v Pythonu (nebo ji dočasně nechat za feature flagem jen jako fallback při chybě DB funkce).
- Uložit do `planning_run` diagnostiku (např. `forecast_correction_factor` nahradit/rozšířit o `pv_forecast_method = 'canonical_db_delta+rolling'` + `rolling_factor`).

## Změny DB pro plánovací sloty a current bundle

- `db/routines/R__063_fn_load_planning_slots_full.sql`:
  - zdroj PV A/B musí být `pv_*_forecast_canonical_w` z nové funkce.
  - zachovat raw/solver sloupce pro audit a UI.
- `ems.fn_plan_current_bundle` (repeatable rutina ve `db/routines/`, dohledat a upravit):
  - pro intervaly z `planning_interval` vracet explicitně:
    - `pv_a_forecast_solver_w`, `pv_b_forecast_solver_w`, a `pv_forecast_total_w = pva+pvb` (aby UI nemuselo „domýšlet“ přes jiné endpointy),
  - pro sloty za horizontem (forecast extension) vracet `pv_forecast_total_w` jako **kanonický součet** (canonical A+B) z nové funkce.

## Změny UI

V `frontend/src/pages/Planning.tsx`:

- Pro tabulku slotů a graf použít **jen** PV z `/sites/{id}/plan/current`:
  - pro plánované sloty: `pv_a_forecast_solver_w + pv_b_forecast_solver_w` (ne `pv-slots-corrected`),
  - pro forecast-only sloty: `pv_forecast_total_w` (které už bude kanonické z DB).
- Endpoint `/forecast/pv-slots-corrected` ponechat pro stránku forecastu a diagnostiku, ale **ne** jako zdroj pro Planning tabulku.

## Ověření

- Pro konkrétní slot (např. `home-01`, `10:15` Prague) musí sedět:
  - UI PV (z `/plan/current`) == PV v solver vstupu == uložené `planning_interval.pv_*_forecast_solver_w`.
- Výkonová bilance v tabulce slotů: `PV - load - EV - HP = battery + export(+/- import)` bez „magické energie“.
- Doplnit regresní test: UI zobrazuje stejné PV jako `planning_interval` (alespoň na DTO úrovni / snapshot).

## Dokumentace

Aktualizovat:

- `docs/04-modules/forecast.md` (kde vzniká kanonické PV: delta + rolling factor + decay),
- `docs/04-modules/planning.md` (solver čte kanonický PV z DB; UI používá stejné sloupce z `/plan/current`),
- případně krátká poznámka do `docs/02-architecture.md` k „read-model = single point of truth“ pro plán.

