diff --git a/db/routines/R__078_fn_pv_forecast_delta_profile.sql b/db/routines/R__078_fn_pv_forecast_delta_profile.sql index f75fc58..25be8d5 100644 --- a/db/routines/R__078_fn_pv_forecast_delta_profile.sql +++ b/db/routines/R__078_fn_pv_forecast_delta_profile.sql @@ -64,19 +64,101 @@ as $fn$ (extract(hour from (b.interval_start at time zone tz.tz_name))::int * 60) + extract(minute from (b.interval_start at time zone tz.tz_name))::int ) / 15 as slot_of_day, + (b.interval_start at time zone tz.tz_name)::date as day_local, extract(epoch from (now() - b.interval_start)) / 86400.0 as age_days from best b cross join tz where b.rn = 1 - group by b.interval_start, slot_of_day, tz.tz_name + group by b.interval_start, slot_of_day, day_local, tz.tz_name + ), + -- Denní „clear-ish“ skóre: preferujeme dny s hladkou křivkou výroby a vysokou denní energií + -- relativně k ostatním dnům v okně (mraky dělají vysokofrekvenční šum na 15min, který není dobrý anchor pro slot bias). + day_energy as ( + select + s.day_local, + sum(s.actual_total_w)::numeric / 4000.0 as energy_kwh + from slots s + group by s.day_local + ), + ref as ( + select percentile_cont(0.5) within group (order by de.energy_kwh) as med_kwh + from day_energy de + ), + slot_steps as ( + select + s.*, + lag(s.actual_total_w) over (partition by s.day_local order by s.interval_start) as prev_actual_w + from slots s + where s.slot_of_day between 20 and 80 + and s.actual_total_w > (select threshold_w from bounds) + ), + day_jump as ( + select + ss.day_local, + percentile_cont(0.5) within group (order by abs(ss.actual_total_w - ss.prev_actual_w)) as med_jump_w + from slot_steps ss + where ss.prev_actual_w is not null + group by ss.day_local + ), + day_med as ( + select + s.day_local, + percentile_cont(0.5) within group (order by s.actual_total_w) as p50_actual_w + from slots s + where s.actual_total_w > (select threshold_w from bounds) + group by s.day_local + ), + day_stats as ( + select + de.day_local, + de.energy_kwh, + dj.med_jump_w, + dm.p50_actual_w, + case + when (select med_kwh from ref) is null or (select med_kwh from ref) <= 0 then 0.5 + else greatest( + 0.0, + least( + 1.0, + (de.energy_kwh - (select med_kwh from ref) * 0.55) + / nullif((select med_kwh from ref) * 0.35, 0) + ) + ) + end as w_energy, + case + when dj.med_jump_w is null or dm.p50_actual_w is null then 0.35 + else greatest( + 0.0, + least( + 1.0, + 1.0 + - ( + dj.med_jump_w + / nullif(greatest(300.0, dm.p50_actual_w * 0.25), 0) + ) + ) + ) + end as w_smooth + from day_energy de + left join day_jump dj on dj.day_local = de.day_local + left join day_med dm on dm.day_local = de.day_local ), filtered as ( select s.slot_of_day, (s.forecast_total_w - s.actual_total_w) as error_w, - exp(-s.age_days / nullif((select half_life_days from bounds), 0)) as w + exp(-s.age_days / nullif((select half_life_days from bounds), 0)) + * ( + 0.05 + + 0.95 + * greatest( + 0.0, + least(1.0, coalesce(ds.w_energy, 0.35) * coalesce(ds.w_smooth, 0.35)) + ) + ) as w from slots s cross join bounds b + left join day_stats ds on ds.day_local = s.day_local where s.slot_of_day between 0 and 95 and (s.actual_total_w > b.threshold_w or s.forecast_total_w > b.threshold_w) ), @@ -119,4 +201,4 @@ as $fn$ $fn$; comment on function ems.fn_pv_forecast_delta_profile(int, timestamptz, timestamptz, numeric, int) is - 'Aditivní delta profil chyby PV forecastu po 15min slotu dne (96 slotů). Zdroj: forecast_accuracy, vážení exp(-age/half_life_days). Vrací JSON {deltas:[{slot_of_day, delta_w, sample_count}], ...}. Interní minimální cutoff dat (2026-04-06 Europe/Prague) brání učení z nekonzistentní historie před kompletním plněním actual.'; + 'Aditivní delta profil chyby PV forecastu po 15min slotu dne (96 slotů). Zdroj: forecast_accuracy, vážení exp(-age/half_life_days) * day_weight (preferuje „clear-ish“ dny: vyšší denní energie vs median v okně + nižší median skoků výkonu mezi 15min v daylight bandu). Vrací JSON {deltas:[{slot_of_day, delta_w, sample_count}], ...}. Interní minimální cutoff dat (2026-04-11 Europe/Prague) brání učení z nekonzistentní historie před kompletním plněním actual.'; diff --git a/docs/05-todo.md b/docs/05-todo.md index 1114572..58b4f78 100644 --- a/docs/05-todo.md +++ b/docs/05-todo.md @@ -18,7 +18,7 @@ Shrnutí otevřených bodů z `docs/06-open-questions.md`, checklistů v modulec | **Telemetry – výroba FVE:** Registry 672/673/667 jsou **signed** W; `pv_power_w` = max(0,pv1)+max(0,pv2)+max(0,gen) (dashboard); sloupce pv1/pv2/gen ukládají signed pro audit. | | **Ekonomika baterie:** snížení `reserve_soc_percent` na 10 % a `degradation_cost_czk_kwh` na 0.1500 (migrace `V026__battery_economics_tuning.sql`), úpravy objective pro ekonomicky konzistentnější nabíjení/vybíjení. | | **Planning UI operátor akce:** trvale viditelné akce import/forecast/init plan, volba data OTE (dnes/zítra), zobrazení `pv_scarcity_factor` ve stavu plánu. | -| **PV delta profil – cutoff historie:** po analýze `ems.forecast_accuracy` pro `home-01` je minimální spolehlivý začátek učení **2026-04-06 (Europe/Prague)** (UTC `2026-04-05T22:00:00Z`); cutoff je zafixovaný v `db/routines/R__078_fn_pv_forecast_delta_profile.sql` (ignoruje starší data i při širším `p_data_from`). | +| **PV delta profil – cutoff historie:** po analýze `ems.forecast_accuracy` pro `home-01` je minimální spolehlivý začátek učení **2026-04-11 (Europe/Prague)** (UTC `2026-04-10T22:00:00Z`); cutoff je zafixovaný v `db/routines/R__078_fn_pv_forecast_delta_profile.sql` (ignoruje starší data i při širším `p_data_from`). | ---