fix cyklovani
Some checks failed
CI and deploy / migration-check (push) Failing after 26s
CI and deploy / deploy (push) Has been skipped

This commit is contained in:
Dusan Vojacek
2026-05-15 17:47:20 +02:00
parent 30f16a14c2
commit d89d8b1e3a
6 changed files with 273 additions and 122 deletions

View File

@@ -57,8 +57,6 @@ declare
v_n_pm int;
v_chg_am_wh numeric;
v_chg_pm_wh numeric;
v_dis_am_wh numeric;
v_dis_pm_wh numeric;
v_reserve_wh numeric;
v_daytime_en boolean;
v_night_buf_pct numeric;
@@ -229,24 +227,18 @@ begin
v_per_slot_discharge_wh := v_max_discharge_w * v_discharge_eff * 0.25;
v_energy_to_fill := v_soc_max_wh - p_current_soc_wh;
v_exportable := v_soc_max_wh - v_min_soc_wh;
v_grid_target_wh := v_energy_to_fill * v_charge_buf;
v_grid_target_wh := greatest(v_energy_to_fill, 0) * v_charge_buf;
v_discharge_target_wh := v_exportable * v_discharge_buf;
-- Rozpočet na půl dne (Europe/Prague): 00:0012:00 vs 12:0024:00; chybějící segment dostane celý budget.
-- Nabíjecí rozpočet dál dělíme 50/50 (kvůli rozprostření v rámci dne), ale exportní vybíjení volíme globálně podle sell_price.
-- AM/PM rozpočet grid charging (Europe/Prague 0012 vs 1224).
-- Chybějící segment dostane celý budget.
select
coalesce(
count(*) filter (
where extract(hour from wk.interval_start at time zone 'Europe/Prague') < 12
),
0
)::int,
coalesce(
count(*) filter (
where extract(hour from wk.interval_start at time zone 'Europe/Prague') >= 12
),
0
)::int
coalesce(count(*) filter (
where extract(hour from wk.interval_start at time zone 'Europe/Prague') < 12
), 0)::int,
coalesce(count(*) filter (
where extract(hour from wk.interval_start at time zone 'Europe/Prague') >= 12
), 0)::int
into v_n_am, v_n_pm
from _ems_plan_slot_wk wk;
@@ -261,35 +253,65 @@ begin
v_chg_pm_wh := v_grid_target_wh - v_chg_am_wh;
end if;
-- charge mask (sloupce temp tabulky kvalifikujeme: RETURNS TABLE dělá PL proměnné stejných jmen)
-- charge mask: dvě nezávislé vrstvy
--
-- A) PV-surplus sloty (pv_surplus_w > 0): ranking dle sell_price ASC.
-- Nejlevnější PV-surplus sloty vybereme, dokud kumulativní
-- PV surplus nepokryje charge target (energy_to_fill × charge_buf).
-- Zbylé PV-surplus sloty mají allow_charge = false → PV jde do sítě.
-- Toto je hlavní mechanismus proti mikro-cyklování z PV:
-- v drahých slotech se PV prodává přímo, nabíjení jen v levných.
--
-- B) Non-PV sloty (pv_surplus_w <= 0): AM/PM budget, OTE-first.
-- Nejlevnější non-PV sloty (dle buy_price) s prioritou OTE cen
-- před predikovanými (is_predicted_price::int ASC). AM a PM mají
-- oddělený rozpočet (50/50), aby solver nekoncentroval veškeré
-- nabíjení/vybíjení do jediné půlky dne (double-cycle ochrana).
-- OTE-first: levné OTE sloty aktuálního dne nesmí být vytlačeny
-- levnějšími predikovanými cenami vzdálených dní (den 34 z 96h).
if v_charge_buf <= 0 then
update _ems_plan_slot_wk wk set allow_charge = true;
elsif v_energy_to_fill <= 0 then
-- Pokud rolling replan startuje s baterií plnou, nechceme zablokovat budoucí nabíjení po vybití.
-- Povolit alespoň nabíjení v PV surplus slotech, aby solver mohl vytvořit headroom a pak ho znovu zaplnit z FVE.
update _ems_plan_slot_wk wk set allow_charge = (wk.pv_surplus_w > 0);
update _ems_plan_slot_wk wk set allow_charge = true;
else
update _ems_plan_slot_wk wk set allow_charge = (wk.pv_surplus_w > 0);
update _ems_plan_slot_wk wk set allow_charge = false;
-- A) PV-surplus: cheapest sell_price first
v_cum := 0;
for r_slot in
select wk.slot_ord, wk.pv_surplus_w
from _ems_plan_slot_wk wk
where wk.pv_surplus_w > 0
order by wk.sell_price, wk.slot_ord
loop
exit when v_cum >= v_grid_target_wh;
update _ems_plan_slot_wk wk set allow_charge = true where wk.slot_ord = r_slot.slot_ord;
v_cum := v_cum + least(r_slot.pv_surplus_w, v_max_charge_w) * v_charge_eff * 0.25;
end loop;
-- B) Non-PV AM: OTE-first, then predicted, ordered by buy_price
v_cum := 0;
for r_slot in
select wk.slot_ord
from _ems_plan_slot_wk wk
where wk.pv_surplus_w <= 0
and extract(hour from wk.interval_start at time zone 'Europe/Prague') < 12
order by wk.buy_price, wk.slot_ord
order by wk.is_predicted_price::int, wk.buy_price, wk.slot_ord
loop
exit when v_cum >= v_chg_am_wh;
exit when v_per_slot_charge_wh <= 0;
update _ems_plan_slot_wk wk set allow_charge = true where wk.slot_ord = r_slot.slot_ord;
v_cum := v_cum + v_per_slot_charge_wh;
end loop;
-- B) Non-PV PM: OTE-first, then predicted, ordered by buy_price
v_cum := 0;
for r_slot in
select wk.slot_ord
from _ems_plan_slot_wk wk
where wk.pv_surplus_w <= 0
and extract(hour from wk.interval_start at time zone 'Europe/Prague') >= 12
order by wk.buy_price, wk.slot_ord
order by wk.is_predicted_price::int, wk.buy_price, wk.slot_ord
loop
exit when v_cum >= v_chg_pm_wh;
exit when v_per_slot_charge_wh <= 0;
@@ -409,7 +431,9 @@ $fn$;
comment on function ems.fn_load_planning_slots_full is
'15min sloty s cenami, forecastem, baseline a maskami proti mikro-cyklu (charge/discharge-export). '
'Masky charge/discharge-export se berou zvlášť pro 0012 a 1224 Europe/Prague (polovina budgetu na segment). '
'Charge mask: PV-surplus sloty rankované dle sell_price ASC nejlevnější pokrývají charge target, zbytek → PV do sítě; '
'non-PV sloty dle buy_price s AM/PM rozpočtem 50/50 a OTE-first prioritou (is_predicted_price::int ASC). '
'Discharge-export mask: nejdražší sell_price sloty globálně. '
'Strop SoC pro výpočet energie k dobití: coalesce(planner_max_soc_percent, max_soc_percent). '
'Denní safety vstupy: night_baseload_* (20:0006:00 Europe/Prague), safety_soc_target_wh (619), '
'lookahead max buy/sell pro měkké LP penalizace.';