Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions gridfm_datakit/perturbations/load_perturbation.py
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,96 @@ def __call__(

return load_profiles

class PrecomputedProfile(LoadScenarioGeneratorBase):
"""Reads precomputed bus-demand scenarios and returns them as (n_buses, n_scenarios, 2).

CSV/XLSX columns:
- load_scenario : int scenario index (0..S-1)
- load : int BUS INDEX (0..n_buses-1) in *continuous indexing* used by Network
(i.e., after mapping to 0..n_buses-1)
- p_mw, q_mvar : floats
"""

def __init__(self, scenario_file: str):
self.scenario_file = scenario_file

def _read(self) -> pd.DataFrame:
p = self.scenario_file
if p.lower().endswith((".xlsx", ".xls")):
return pd.read_excel(p)
return pd.read_csv(p)

def __call__(
self,
net, # type: Network
n_scenarios: int,
scenario_log: str,
max_iter: int, # unused, kept for interface compatibility
) -> np.ndarray:
df = self._read()

required = {"load_scenario", "load", "p_mw", "q_mvar"}
missing = required - set(df.columns)
if missing:
raise ValueError(
f"Scenario file must contain columns {sorted(required)}; missing {sorted(missing)}. "
f"Got {list(df.columns)}"
)

df["load_scenario"] = df["load_scenario"].astype(int)
df["load"] = df["load"].astype(int)

n_buses = int(np.asarray(net.buses).shape[0]) # or len(net.Pd)
if n_buses == 0:
raise ValueError("Network has zero buses.")

file_scenarios = int(df["load_scenario"].max()) + 1
if n_scenarios > file_scenarios:
raise ValueError(
f"Requested n_scenarios={n_scenarios}, but file provides only {file_scenarios}."
)

# bus index validation (continuous indices 0..n_buses-1)
if int(df["load"].min()) < 0:
raise ValueError("Scenario file contains negative bus indices in column 'load'.")
max_bus = int(df["load"].max())
if max_bus >= n_buses:
raise ValueError(
f"Scenario file references bus index {max_bus}, but network has {n_buses} buses."
)

df = df[df["load_scenario"].between(0, n_scenarios - 1)]

# Optional strict coverage check (comment out if you allow missing => 0)
expected = n_buses * n_scenarios
actual = df[["load_scenario", "load"]].drop_duplicates().shape[0]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why drop duplicates?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll modify it to ensure strict checks

if actual != expected:
raise ValueError(
f"Scenario file does not fully specify all (scenario, bus) pairs: "
f"expected {expected}, got {actual}. "
f"If you want unspecified buses to default to 0, remove this check."
)

# Allocate output: (n_buses, n_scenarios, 2)
out = np.zeros((n_buses, n_scenarios, 2), dtype=float)

# If duplicates exist, keep last
df = df.sort_index().drop_duplicates(subset=["load_scenario", "load"], keep="last")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldnt we enforce no duplicate from the start?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, will update with checks to ensure right bus numbers, right load_scenario numbers, no duplicates, etc


s = df["load_scenario"].to_numpy(dtype=int)
b = df["load"].to_numpy(dtype=int)
out[b, s, 0] = df["p_mw"].to_numpy(dtype=float)
out[b, s, 1] = df["q_mvar"].to_numpy(dtype=float)

if scenario_log:
with open(scenario_log, "a") as f:
f.write(
f"precomputed_profile: scenarios={n_scenarios}, buses={n_buses}, "
f"path={self.scenario_file}\n"
)

return out


if __name__ == "__main__":
"""
Expand Down
7 changes: 6 additions & 1 deletion gridfm_datakit/utils/param_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
LoadScenarioGeneratorBase,
LoadScenariosFromAggProfile,
Powergraph,
PrecomputedProfile,
)
from typing import Dict, Any
import warnings
Expand Down Expand Up @@ -173,6 +174,7 @@ def get_load_scenario_generator(args: NestedNamespace) -> LoadScenarioGeneratorB
Note:
Currently supports 'agg_load_profile' and 'powergraph' generator types.
"""
print("args.generator: ", args.generator)
if args.generator == "agg_load_profile":
return LoadScenariosFromAggProfile(
args.agg_profile,
Expand All @@ -194,8 +196,11 @@ def get_load_scenario_generator(args: NestedNamespace) -> LoadScenarioGeneratorB
f"The following arguments are not used by the powergraph generator: {unused_args}",
UserWarning,
)


return Powergraph(args.agg_profile)
if args.generator == "precomputed_profile":
print("precomputed_profile being used")
return PrecomputedProfile(args.scenario_file)


def initialize_topology_generator(
Expand Down
Loading