From ab80d13ecbe6fbf54edbc6600856d7a37a9cce8e Mon Sep 17 00:00:00 2001 From: Dusan Vojacek Date: Tue, 5 May 2026 12:13:07 +0200 Subject: [PATCH] dalsi fix forecat tuningu --- backend/services/planning_engine.py | 31 ++++++- ...__043_fn_pv_forecast_correction_factor.sql | 42 +++++----- ..._089_fn_forecast_pv_slots_range_raw_ab.sql | 83 +++++++++++++++++++ 3 files changed, 130 insertions(+), 26 deletions(-) create mode 100644 db/routines/R__089_fn_forecast_pv_slots_range_raw_ab.sql diff --git a/backend/services/planning_engine.py b/backend/services/planning_engine.py index 80c83ac..b386ce4 100644 --- a/backend/services/planning_engine.py +++ b/backend/services/planning_engine.py @@ -1158,8 +1158,8 @@ async def run_rolling_replan( ) slots = await _load_slots(site_id, replan_from, horizon_to, db, soc_wh=soc_wh) - # PV forecast korekce je kanonicky v DB (delta + rolling faktor + decay), - # viz ems.fn_forecast_pv_slots_range_canonical_ab a ems.fn_load_planning_slots_full. + # PV forecast korekce je kanonicky v DB (delta + rolling faktor + decay) a do LP vstupuje přes + # ems.fn_load_planning_slots_full. Pro audit/debug ale chceme ukládat i RAW (bez korekcí). correction_factor, correction_log = 1.0, { "window_start": None, "window_end": None, @@ -1169,6 +1169,31 @@ async def run_rolling_replan( "reason": "canonical_db", } + # RAW PV pro slot_inputs: přímý součet nejnovějších forecast_pv_interval per array/slot (bez delta/rolling). + raw_pv_rows = await db.fetchval( + "select ems.fn_forecast_pv_slots_range_raw_ab($1::int, $2::timestamptz, $3::timestamptz)", + site_id, + replan_from, + horizon_to, + ) + raw_pv = raw_pv_rows if isinstance(raw_pv_rows, list) else json.loads(raw_pv_rows) + raw_by_ts: dict[str, tuple[int, int]] = {} + if isinstance(raw_pv, list): + for r in raw_pv: + if not isinstance(r, dict): + continue + ts = r.get("interval_start") + if isinstance(ts, str): + raw_by_ts[ts] = ( + int(r.get("pv_a_forecast_raw_w") or 0), + int(r.get("pv_b_forecast_raw_w") or 0), + ) + slots_raw_pv: list[PlanningSlot] = [] + for s in slots: + key = s.interval_start.isoformat() + pva, pvb = raw_by_ts.get(key, (s.pv_a_forecast_w, s.pv_b_forecast_w)) + slots_raw_pv.append(replace(s, pv_a_forecast_w=pva, pv_b_forecast_w=pvb)) + commitment_prev = await _load_previous_plan_charge_commitment_prev_w(site_id, slots, db) results, duration_ms, solver_snapshot = solve_dispatch( @@ -1178,7 +1203,7 @@ async def run_rolling_replan( charge_commitment_prev_w=commitment_prev, ) - slot_inputs = _build_slot_inputs(slots, slots) + slot_inputs = _build_slot_inputs(slots_raw_pv, slots) run_id = await _save_planning_run( site_id, results, diff --git a/db/routines/R__043_fn_pv_forecast_correction_factor.sql b/db/routines/R__043_fn_pv_forecast_correction_factor.sql index 17720e4..e9250b4 100644 --- a/db/routines/R__043_fn_pv_forecast_correction_factor.sql +++ b/db/routines/R__043_fn_pv_forecast_correction_factor.sql @@ -25,30 +25,26 @@ begin and ti.measured_at >= p_window_start and ti.measured_at < p_window_end; - with pv_arrays as ( - select apa.id as pv_array_id - from ems.asset_pv_array apa - where apa.site_id = p_site_id - ), - latest_run as ( - select distinct on (fpr.pv_array_id) - fpr.pv_array_id, - fpr.id as run_id - from pv_arrays pa - join ems.forecast_pv_run fpr - on fpr.pv_array_id = pa.pv_array_id - and fpr.site_id = p_site_id - where fpr.status = 'ok' - and fpr.created_at <= p_window_start - order by fpr.pv_array_id, fpr.created_at desc - ) - select coalesce(sum(fpi.power_w) * 0.25 / 1000.0, 0) + -- Forecast pro korekční faktor bereme stejně jako pro plánování/UI: + -- nejnovější `ok` run per (interval_start, pv_array_id) v daném okně. + select coalesce(sum(u.power_w) * 0.25 / 1000.0, 0) into v_forecast - from ems.forecast_pv_interval fpi - join latest_run lr on lr.run_id = fpi.run_id - where fpi.interval_start >= p_window_start - and fpi.interval_start < p_window_end - and fpi.pv_array_id = lr.pv_array_id; + from ( + select distinct on (fpi.interval_start, fpr.pv_array_id) + fpi.power_w + from ems.forecast_pv_interval fpi + join ems.forecast_pv_run fpr + on fpr.id = fpi.run_id + and fpr.site_id = p_site_id + and fpr.pv_array_id = fpi.pv_array_id + and fpr.status = 'ok' + where fpi.interval_start >= p_window_start + and fpi.interval_start < p_window_end + and fpi.pv_array_id in ( + select apa.id from ems.asset_pv_array apa where apa.site_id = p_site_id + ) + order by fpi.interval_start, fpr.pv_array_id, fpr.created_at desc + ) u; if v_forecast < 0.1 or coalesce(v_actual, 0) < 0.05 then return jsonb_build_object( diff --git a/db/routines/R__089_fn_forecast_pv_slots_range_raw_ab.sql b/db/routines/R__089_fn_forecast_pv_slots_range_raw_ab.sql new file mode 100644 index 0000000..a8ac9c6 --- /dev/null +++ b/db/routines/R__089_fn_forecast_pv_slots_range_raw_ab.sql @@ -0,0 +1,83 @@ +-- ============================================================ +-- PV forecast sloty (15min) – RAW (bez korekcí), rozdělené na PV-A/PV-B +-- +-- Nejnovější `ok` forecast_pv_run per (interval_start, pv_array_id). +-- Slouží pro audit/debug v planning_interval.*_forecast_raw_w. +-- ============================================================ + +create or replace function ems.fn_forecast_pv_slots_range_raw_ab( + p_site_id int, + p_from timestamptz, + p_to timestamptz +) +returns jsonb +language sql +stable +set work_mem = '64MB' +as $fn$ + with bounds as ( + select + date_bin(interval '15 minutes', p_from, timestamptz '1970-01-01T00:00:00Z') as ts_from, + case + when p_to <= p_from then date_bin(interval '15 minutes', p_from, timestamptz '1970-01-01T00:00:00Z') + interval '15 minutes' + when p_to > p_from + interval '60 days' then date_bin(interval '15 minutes', p_from, timestamptz '1970-01-01T00:00:00Z') + interval '60 days' + else date_bin(interval '15 minutes', p_to, timestamptz '1970-01-01T00:00:00Z') + end as ts_to + ), + slot_spine as ( + select gs as interval_start + from bounds b, + generate_series( + b.ts_from, + (b.ts_to - interval '15 minutes')::timestamptz, + interval '15 minutes' + ) as gs + ), + fc_by_array as ( + select distinct on (fpi.interval_start, fpr.pv_array_id) + fpi.interval_start, + apa.controllable, + fpi.power_w::bigint as power_w + from bounds b + join ems.forecast_pv_interval fpi + on fpi.interval_start >= b.ts_from + and fpi.interval_start < b.ts_to + and fpi.pv_array_id in ( + select apa0.id from ems.asset_pv_array apa0 where apa0.site_id = p_site_id + ) + join ems.forecast_pv_run fpr + on fpr.id = fpi.run_id + and fpr.site_id = p_site_id + and fpr.pv_array_id = fpi.pv_array_id + and fpr.status = 'ok' + join ems.asset_pv_array apa + on apa.id = fpr.pv_array_id + and apa.site_id = p_site_id + order by fpi.interval_start, fpr.pv_array_id, fpr.created_at desc + ), + fc_ab as ( + select + s.interval_start, + coalesce(sum(case when f.controllable then f.power_w else 0 end), 0)::bigint as pv_a_forecast_raw_w, + coalesce(sum(case when not f.controllable then f.power_w else 0 end), 0)::bigint as pv_b_forecast_raw_w + from slot_spine s + left join fc_by_array f on f.interval_start = s.interval_start + group by s.interval_start + ) + select coalesce( + jsonb_agg( + jsonb_build_object( + 'interval_start', r.interval_start, + 'pv_a_forecast_raw_w', r.pv_a_forecast_raw_w, + 'pv_b_forecast_raw_w', r.pv_b_forecast_raw_w + ) + order by r.interval_start + ), + '[]'::jsonb + ) + from fc_ab r; +$fn$; + +comment on function ems.fn_forecast_pv_slots_range_raw_ab is + 'RAW PV forecast po 15 min (bez korekcí), rozdělený na PV-A/PV-B, jako nejnovější ok run per array a slot.'; +