|
3 | 3 | from datetime import datetime |
4 | 4 |
|
5 | 5 | import geopandas as gpd |
| 6 | +import numpy as np |
6 | 7 | import pandas as pd |
7 | 8 |
|
8 | 9 | from edisgo import EDisGo |
@@ -350,6 +351,66 @@ def prepare_edisgo_for_14a(edisgo, *, shapefile_path, output_dir, cache_dir=None |
350 | 351 | edisgo.set_time_series_reactive_power_control() |
351 | 352 |
|
352 | 353 |
|
| 354 | +def fix_hp_peak_loads(edisgo, seed=None): |
| 355 | + """ |
| 356 | + Enforce minimum HP p_set constraints and ensure adequate representation |
| 357 | + in higher power bands. Timeseries are scaled proportionally when p_set changes. |
| 358 | +
|
| 359 | + Rules: |
| 360 | + 1. p_set >= 0.003 MW for every heat pump. |
| 361 | + 2. At least 5 % of HPs have p_set in [0.01, 0.02] MW. |
| 362 | + 3. At least 5 % of HPs have p_set in [0.02, 0.03] MW. |
| 363 | + 4. At least 5 % of HPs have p_set in [0.04, 0.05] MW. |
| 364 | + """ |
| 365 | + rng = np.random.default_rng(seed) |
| 366 | + loads_df = edisgo.topology.loads_df |
| 367 | + hp_idx = loads_df[loads_df["type"] == "heat_pump"].index.tolist() |
| 368 | + n_hp = len(hp_idx) |
| 369 | + if n_hp == 0: |
| 370 | + return |
| 371 | + |
| 372 | + lap = edisgo.timeseries.loads_active_power |
| 373 | + |
| 374 | + def _rescale(hp, new_p): |
| 375 | + old_p = loads_df.at[hp, "p_set"] |
| 376 | + if old_p > 0 and hp in lap.columns: |
| 377 | + lap[hp] = lap[hp] * (new_p / old_p) |
| 378 | + loads_df.at[hp, "p_set"] = new_p |
| 379 | + |
| 380 | + # Rule 1: floor all HPs at 0.003 MW |
| 381 | + for hp in hp_idx: |
| 382 | + if loads_df.at[hp, "p_set"] < 0.003: |
| 383 | + _rescale(hp, 0.003) |
| 384 | + |
| 385 | + # Rules 2–4: ensure at least 5 % of HPs fall in each target band |
| 386 | + target_bands = [(0.01, 0.02), (0.02, 0.03), (0.04, 0.05)] |
| 387 | + min_count = max(1, int(np.ceil(n_hp * 0.05))) |
| 388 | + |
| 389 | + assigned = set() |
| 390 | + for lo, hi in target_bands: |
| 391 | + already = [hp for hp in hp_idx if lo <= loads_df.at[hp, "p_set"] <= hi] |
| 392 | + assigned |= set(already) |
| 393 | + need = min_count - len(already) |
| 394 | + if need <= 0: |
| 395 | + continue |
| 396 | + # prefer HPs not yet assigned to any target band; fall back to all others |
| 397 | + candidates = [hp for hp in hp_idx if hp not in assigned] |
| 398 | + if len(candidates) < need: |
| 399 | + candidates = [hp for hp in hp_idx if hp not in set(already)] |
| 400 | + chosen = rng.choice(candidates, size=min(need, len(candidates)), replace=False).tolist() |
| 401 | + for hp in chosen: |
| 402 | + _rescale(hp, float(rng.uniform(lo, hi))) |
| 403 | + assigned.add(hp) |
| 404 | + |
| 405 | + # Keep heat_demand_df consistent with updated timeseries |
| 406 | + hp_in_lap = [h for h in hp_idx if h in lap.columns] |
| 407 | + if not edisgo.heat_pump.cop_df.empty and hp_in_lap: |
| 408 | + edisgo.heat_pump.heat_demand_df = ( |
| 409 | + lap[hp_in_lap] * edisgo.heat_pump.cop_df[hp_in_lap] |
| 410 | + ) |
| 411 | + print(f"[fix_hp_peak_loads] Adjusted {n_hp} heat pumps.") |
| 412 | + |
| 413 | + |
353 | 414 | def get_monthly_snapshot_ranges(year=2025, test=False): |
354 | 415 | """Return list of (month_label, start_idx, end_idx) for each month of year. |
355 | 416 |
|
@@ -400,8 +461,10 @@ def main(output_dir, snapshot_range, seed=42): |
400 | 461 | seed=seed, |
401 | 462 | ) |
402 | 463 |
|
| 464 | + fix_hp_peak_loads(edisgo, seed=seed) |
| 465 | + |
403 | 466 | edisgo = run_optimization_14a(edisgo) |
404 | | - edisgo.analyze() |
| 467 | + #edisgo.analyze() |
405 | 468 |
|
406 | 469 | # ────────────────────────── Slack diagnosis ────────────────────────────── |
407 | 470 | slacks = edisgo.opf_results.grid_slacks_t |
|
0 commit comments