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

This commit is contained in:
Dusan Vojacek
2026-05-22 15:55:25 +02:00
parent c5525c729f
commit 9cf7708909
3 changed files with 77 additions and 12 deletions

View File

@@ -1168,14 +1168,21 @@ def solve_dispatch(
0.0,
float(s.pv_a_forecast_w) + float(s.pv_b_forecast_w) - load_neg,
)
# FVE→síť při záporném výkupu jen nucený vent (plná baterie); jinak bc_pv / load-first.
if not _pv_forced_vent_export_allowed(
t,
current_soc_wh=current_soc_wh,
battery=battery,
soc_headroom_wh=soc_headroom_wh,
pv_surplus_w=pv_surplus_neg_w,
):
# FVE→síť při záporném výkupu: u KV1 (block_export) jen bc/curtail A;
# u home-01 s polem B musí přebytek jít do sítě (ge_pv), jinak infeasible.
block_pv_export_neg_sell = bool(
getattr(grid, "block_export_on_negative_sell", False)
) or (
float(s.pv_b_forecast_w) <= 0
and not _pv_forced_vent_export_allowed(
t,
current_soc_wh=current_soc_wh,
battery=battery,
soc_headroom_wh=soc_headroom_wh,
pv_surplus_w=pv_surplus_neg_w,
)
)
if block_pv_export_neg_sell:
prob += ge_pv[t] == 0
# Tvrdý zákaz celého vývozu (GEN / fixní nákup bez pole B).
block_neg_sell_export = bool(
@@ -1306,7 +1313,6 @@ def solve_dispatch(
# Ekonomické guardy: ceny v objective nestačí proti maskám / terminal SoC.
ref_buy_horizon = min(float(s.buy_price) for s in slots)
min_spread = float(degradation_cost_effective)
hp_rated_w = float(heat_pump.rated_heating_power_w)
for t in range(T):
s = slots[t]
buy_t = float(s.buy_price)
@@ -1330,8 +1336,13 @@ def solve_dispatch(
and sell_t >= 0
)
pv_store_val = _pv_store_value_czk_kwh(s, min_spread)
skip_pv_store_block = (
float(s.pv_b_forecast_w) > 0
and not getattr(grid, "block_export_on_negative_sell", False)
)
if (
not allow_pre_neg_pv_export
and not skip_pv_store_block
and sell_t < pv_store_val
and not _pv_forced_vent_export_allowed(
t,
@@ -1348,11 +1359,13 @@ def solve_dispatch(
buy_t > charge_acquisition_czk_kwh + min_spread
)
if expensive_import_slot and t not in charge_slots:
prob += gi[t] <= ev_cap_t + hp_rated_w
# Síť jen pro EV + skutečný výkon TČ v tomto slotu (hp[t]), ne celý dům.
prob += gi[t] <= ev_cap_t + hp[t]
if om == "AUTO":
# Bazál + TČ z baterie/FVE; hp[t] může být 0 — nesmí se použít hp_rated (infeasible).
prob += (
bd[t] + pv_ld[t]
>= float(s.load_baseline_w) + hp_rated_w
>= float(s.load_baseline_w) + hp[t]
)
# Anti souběžný vývoz FVE + významný import (mikrocyklus).
if buy_t > sell_t + min_spread and pv_surplus_w > 0:

View File

@@ -1192,6 +1192,57 @@ class AutoPassiveSelfConsumptionTests(unittest.TestCase):
self.assertLessEqual(results[0].grid_setpoint_w, 0)
self.assertLess(results[0].battery_setpoint_w, -100)
def test_expensive_slot_uses_hp_variable_not_rated(self) -> None:
"""Regrese: bd+pv_ld >= load+hp[t], ne load+hp_rated (jinak Infeasible bez PV)."""
slots = [
PlanningSlot(
interval_start=datetime(2026, 5, 22, 20, 0, tzinfo=timezone.utc),
buy_price=3.0,
sell_price=2.0,
pv_a_forecast_w=0,
pv_b_forecast_w=0,
load_baseline_w=1961,
ev1_connected=False,
ev2_connected=False,
allow_charge=False,
allow_discharge_export=False,
charge_acquisition_buy_czk_kwh=0.52,
),
PlanningSlot(
interval_start=datetime(2026, 5, 22, 20, 15, tzinfo=timezone.utc),
buy_price=-5.0,
sell_price=2.0,
pv_a_forecast_w=0,
pv_b_forecast_w=0,
load_baseline_w=1961,
ev1_connected=False,
ev2_connected=False,
allow_charge=False,
allow_discharge_export=False,
charge_acquisition_buy_czk_kwh=0.52,
),
]
battery = _battery(uc_wh=64_000.0, min_pct=12.0, arb_pct=20.0)
battery.planner_terminal_soc_value_factor = 0.0
hp = SimpleNamespace(rated_heating_power_w=3500, tuv_min_temp_c=45.0, tuv_target_temp_c=55.0)
grid = SimpleNamespace(max_import_power_w=17_000, max_export_power_w=13_500)
vehicles = [
SimpleNamespace(max_charge_power_w=0, battery_capacity_kwh=1.0, default_target_soc_pct=80.0),
SimpleNamespace(max_charge_power_w=0, battery_capacity_kwh=1.0, default_target_soc_pct=80.0),
]
results, _, _ = solve_dispatch(
slots,
battery,
hp,
grid,
[None, None],
vehicles,
20_000.0,
50.0,
operating_mode="AUTO",
)
self.assertEqual(len(results), 2)
class AutoPassiveNoLoadFollowingDischargeTests(unittest.TestCase):
"""AUTO bez allow_discharge_export: žádný export do sítě (Deye PASSIVE)."""