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

@@ -42,8 +42,9 @@ NT/VT podle hodin Europe/Prague (--buy-nt-kwh, VT = NT + --buy-vt-surcharge-kwh)
nebo od raw OTE spotu: --buy-spot-add-fixed-kwh / --buy-spot-asym-pct; u všech režimů
lze přičíst --buy-distribution-kwh a --buy-other-fees-kwh a výslednou cenu násobit
--buy-vat-multiplier. Model explicitně zakazuje současný import+export a současné
nabíjení+vybíjení v jednom slotu. Mikroinvertory / GEN nejsou; zelený bonus není
v účelové funkci. Výsledek = screening, ne nabídka.
nabíjení+vybíjení v jednom slotu. Dlouhé běhy MILP lze řídit přes
--solver-time-limit-sec a průběžný tisk přes --progress-every-days. Mikroinvertory /
GEN nejsou; zelený bonus není v účelové funkci. Výsledek = screening, ne nabídka.
"""
from __future__ import annotations
@@ -55,6 +56,7 @@ import sys
from dataclasses import dataclass
from datetime import date, datetime, timedelta
from pathlib import Path
from time import perf_counter
from typing import Iterable, Sequence, Mapping
try:
@@ -386,6 +388,7 @@ def solve_one_day(
p_batt_w: float,
site: SiteLimits,
soc_start_wh: float,
solver_time_limit_sec: float,
) -> tuple[float, float, float, float]:
"""
Vrátí (cash_kc, soc_end_wh, curtailed_wh, discharged_wh_sum).
@@ -434,7 +437,10 @@ def solve_one_day(
prob += pulp.lpSum(obj)
solver = pulp.PULP_CBC_CMD(msg=False, timeLimit=60)
solver_kwargs: dict[str, object] = {"msg": False}
if solver_time_limit_sec > 0:
solver_kwargs["timeLimit"] = solver_time_limit_sec
solver = pulp.PULP_CBC_CMD(**solver_kwargs)
prob.solve(solver)
if prob.status != pulp.LpStatusOptimal:
raise RuntimeError(f"LP status {pulp.LpStatus[prob.status]}")
@@ -447,7 +453,7 @@ def solve_one_day(
def simulate_year(
days: Iterable[date],
days: Sequence[date],
px_day: dict[date, list[float]],
usable_kwh: float,
site: SiteLimits,
@@ -459,6 +465,8 @@ def simulate_year(
load_kw: float,
shape: Sequence[float],
monthly_ed_kwh: Mapping[int, float] | None,
solver_time_limit_sec: float,
progress_every_days: int,
) -> dict[str, float]:
e_wh = usable_kwh * 1000.0
p_batt = batt_power_cap_w(usable_kwh, site)
@@ -467,10 +475,30 @@ def simulate_year(
curt_total = 0.0
dis_total = 0.0
soc_state = 0.5 * (site.soc_min_frac + site.soc_max_frac) * e_wh
run_days = [d for d in days if d in px_day]
total_days = len(run_days)
started = perf_counter()
n_days = 0
for d in days:
if d not in px_day:
continue
if progress_every_days > 0:
limit_msg = (
f"{solver_time_limit_sec:g} s/den"
if solver_time_limit_sec > 0
else "bez limitu / den"
)
print(
f"[{usable_kwh:.1f} kWh] start: {total_days} dnů, CBC limit {limit_msg}",
flush=True,
)
for idx, d in enumerate(run_days, start=1):
if progress_every_days > 0 and (
idx == 1 or idx % progress_every_days == 0 or idx == total_days
):
elapsed_sec = perf_counter() - started
print(
f"[{usable_kwh:.1f} kWh] den {idx}/{total_days}: {d.isoformat()} "
f"(elapsed {elapsed_sec:.1f} s)",
flush=True,
)
raw = px_day[d]
p_sell = [effective_sell_kc_kwh(x, sell_margin_fixed, sell_margin_pct) for x in raw]
p_buy = build_buy_prices_96(raw, buy_cfg)
@@ -479,12 +507,25 @@ def simulate_year(
else:
pv_wh = daily_pv_wh(d, summer_kwh, winter_kwh, shape)
cash, soc_state, curt, dis = solve_one_day(
pv_wh, load_wh, p_sell, p_buy, e_wh, p_batt, site, soc_state
pv_wh,
load_wh,
p_sell,
p_buy,
e_wh,
p_batt,
site,
soc_state,
solver_time_limit_sec,
)
cash_total += cash
curt_total += curt
dis_total += dis
n_days += 1
if progress_every_days > 0:
print(
f"[{usable_kwh:.1f} kWh] hotovo za {perf_counter() - started:.1f} s",
flush=True,
)
feq = (dis_total / e_wh / n_days) if n_days and e_wh > 0 else 0.0
return {
"cash_kc": cash_total,
@@ -585,6 +626,18 @@ def main() -> None:
ap.add_argument("--max-import-w", type=float, default=17_000.0)
ap.add_argument("--inv-batt-max-w", type=float, default=12_000.0)
ap.add_argument("--c-rate", type=float, default=0.5)
ap.add_argument(
"--solver-time-limit-sec",
type=float,
default=60.0,
help="CBC time limit na jeden den; 0 = bez limitu",
)
ap.add_argument(
"--progress-every-days",
type=int,
default=1,
help="Po kolika dnech vytisknout průběh; 0 = tichý režim",
)
ap.add_argument("--capex-per-kwh", type=float, default=0.0, help="CAPEX za 1 kWh rozšíření; vypíše jednoduchou návratnost vs. nejmenší baterie")
args = ap.parse_args()
@@ -599,6 +652,10 @@ def main() -> None:
ap.error("Zvol jen jeden režim základu nákupu: flat, NT/VT, --buy-spot-add-fixed-kwh nebo --buy-spot-asym-pct")
if args.buy_vat_multiplier <= 0:
ap.error("--buy-vat-multiplier musí být > 0")
if args.solver_time_limit_sec < 0:
ap.error("--solver-time-limit-sec musí být >= 0")
if args.progress_every_days < 0:
ap.error("--progress-every-days musí být >= 0")
for hour_arg, hour_value in (("nt-from-hour", args.nt_from_hour), ("nt-to-hour", args.nt_to_hour)):
if not (0 <= hour_value <= 23):
ap.error(f"--{hour_arg} musí být v rozsahu 0..23")
@@ -683,6 +740,8 @@ def main() -> None:
args.load_kw,
shape,
monthly_ed,
args.solver_time_limit_sec,
args.progress_every_days,
)
results.append((kwh, r))
@@ -725,6 +784,12 @@ def main() -> None:
)
print(f" Load (konstanta) {args.load_kw} kW")
print(f" Limity: export {args.max_export_w} W, import {args.max_import_w} W, P_batt = min({args.c_rate}*E_kWh, {args.inv_batt_max_w} W)")
limit_msg = (
f"{args.solver_time_limit_sec:g} s/den"
if args.solver_time_limit_sec > 0
else "bez limitu / den"
)
print(f" Solver: CBC, limit {limit_msg}, progress every {args.progress_every_days} dnů")
print()
print(f"{'kWh':>8} {'P_batt_kW':>10} {'cash_kc/rok':>14} {'Δ vs min':>12} {'curt_MWh/y':>12} {'Feq/den':>8}")