Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
15 changes: 10 additions & 5 deletions docs/components/load.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,29 @@ This module provides functions and classes for generating load scenarios.

### `LoadScenarioGeneratorBase`

::: gridfm_datakit.perturbations.load_perturbation.LoadScenarioGeneratorBase
:::: gridfm_datakit.perturbations.load_perturbation.LoadScenarioGeneratorBase

### `LoadScenariosFromAggProfile`

::: gridfm_datakit.perturbations.load_perturbation.LoadScenariosFromAggProfile
:::: gridfm_datakit.perturbations.load_perturbation.LoadScenariosFromAggProfile


### `Powergraph`


::: gridfm_datakit.perturbations.load_perturbation.Powergraph
:::: gridfm_datakit.perturbations.load_perturbation.Powergraph


### `PrecomputedProfile`


:::: gridfm_datakit.perturbations.load_perturbation.PrecomputedProfile


### `load_scenarios_to_df`

::: gridfm_datakit.perturbations.load_perturbation.load_scenarios_to_df
:::: gridfm_datakit.perturbations.load_perturbation.load_scenarios_to_df

### `plot_load_scenarios_combined`

::: gridfm_datakit.perturbations.load_perturbation.plot_load_scenarios_combined
:::: gridfm_datakit.perturbations.load_perturbation.plot_load_scenarios_combined
5 changes: 3 additions & 2 deletions docs/manual/getting_started.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,12 @@ network:
network_dir: "scripts/grids" # if using source "file", this is the directory containing the network file (relative to the project root)

load:
generator: "agg_load_profile" # Name of the load generator; options: agg_load_profile, powergraph
generator: "agg_load_profile" # Name of the load generator; options: agg_load_profile, powergraph, precomputed_profile
agg_profile: "default" # Name of the aggregated load profile
scenarios: 10000 # Number of different load scenarios to generate
scenario_file: "load-scenarios-precomputed.csv" # Only used if generator is "precomputed_profile"
# WARNING: the following parameters are only used if generator is "agg_load_profile"
# if using generator "powergraph", these parameters are ignored
# if using generator "powergraph" or "precomputed_profile", these parameters are ignored
sigma: 0.2 # max local noise
change_reactive_power: true # If true, changes reactive power of loads. If False, keeps the ones from the case file
global_range: 0.4 # Range of the global scaling factor. used to set the lower bound of the scaling factor
Expand Down
49 changes: 40 additions & 9 deletions docs/manual/load_scenarios.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,20 @@ Load perturbations generate multiple load scenarios from an initial case file sc



The module provides two main perturbation strategies:
The module provides three main perturbation strategies:

### Comparison of Perturbation Strategies


<div class="center-table" markdown>

| Feature | `LoadScenariosFromAggProfile` | `Powergraph` |
|-----------------------------|-------------------------------|-------------------------------|
| **Global scaling** | ✅ Yes | ✅ Yes |
| **Local (per-load) scaling**| ✅ Yes (via noise) | ❌ No |
| **Reactive power perturbed**| ✅ Optional | ❌ No |
| **Interpolation** | ✅ Yes | ✅ Yes |
| **Use of real profile data**| ✅ Yes | ✅ Yes |
| Feature | `LoadScenariosFromAggProfile` | `Powergraph` | `PrecomputedProfile` |
|-----------------------------|-------------------------------|-------------------------------|-------------------------------|
| **Global scaling** | ✅ Yes | ✅ Yes | ❌ No |
| **Local (per-load) scaling**| ✅ Yes (via noise) | ❌ No | ❌ No |
| **Reactive power perturbed**| ✅ Optional | ❌ No | ✅ From file |
| **Interpolation** | ✅ Yes | ✅ Yes | ❌ No |
| **Use of real profile data**| ✅ Yes | ✅ Yes | ✅ From file |

</div>

Expand Down Expand Up @@ -85,7 +85,7 @@ Sample config parameters:

```yaml
load:
generator: "agg_load_profile" # Name of the load generator; options: agg_load_profile, powergraph
generator: "agg_load_profile" # Name of the load generator; options: agg_load_profile, powergraph, precomputed_profile
agg_profile: "default" # Name of the aggregated load profile
scenarios: 200 # Number of different load scenarios to generate
sigma: 0.2 # max local noise
Expand Down Expand Up @@ -129,6 +129,37 @@ load:
scenarios: 200 # Number of load scenarios to generate
```

### `PrecomputedProfile`
Loads scenarios directly from a CSV or Excel file. Each row specifies the active and
reactive power for a given `(load_scenario, load)` pair. All pairs must be present.

Required columns:

- `load_scenario`: scenario index (0..n_scenarios-1)
- `load`: bus index (0..n_buses-1) using the network's continuous indexing
- `p_mw`: active power in MW
- `q_mvar`: reactive power in MVAr

Constraints:

- `load_scenario` and `load` must be integer-valued.
- All `(load_scenario, load)` pairs must be unique.
- The file must include exactly `n_buses * n_scenarios` rows.

Sample config parameters:

```yaml
load:
generator: "precomputed_profile"
scenario_file: "load-scenarios-precomputed.csv"
scenarios: 200
```

Example files:

- `scripts/config/case14_config_precomputed_profile.yaml`
- `scripts/precomputed_load_profiles/load-scenarios-precomputed-case14.csv`

## Aggregated load profiles

The following load profiles are available in the `gridfm-datakit/load_profiles` directory:
Expand Down
2 changes: 1 addition & 1 deletion docs/manual/outputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Load scenarios (per-element time series) produced by the selected load generator
Plot of the generated load scenarios.

#### `scenarios_{generator}.log`
Generator-specific notes (e.g., bounds for the global scaling factor when using `agg_load_profile`).
Generator-specific notes (e.g., bounds for the global scaling factor when using `agg_load_profile`, or source file path for `precomputed_profile`).

#### `n_scenarios.txt`
Metadata file containing the total number of scenarios (used for efficient partition management).
Expand Down
101 changes: 101 additions & 0 deletions gridfm_datakit/perturbations/load_perturbation.py
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,107 @@ 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
seed: int,
) -> 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)}"
)

# Validate integer-valued indices before casting
for col in ["load_scenario", "load"]:
numeric = pd.to_numeric(df[col], errors="coerce")
if numeric.isna().any():
raise ValueError(f"Column '{col}' must contain only numeric values.")
if not np.all(np.isclose(numeric, np.round(numeric))):
raise ValueError(f"Column '{col}' must contain integer-valued entries.")
df[col] = numeric.astype(int)

n_buses = int(np.asarray(net.buses).shape[0])

# Scenario index validation (0..n_scenarios-1)
min_scenario = int(df["load_scenario"].min())
max_scenario = int(df["load_scenario"].max())
if min_scenario < 0 or max_scenario >= n_scenarios:
raise ValueError(
"Scenario file contains out-of-range scenario indices in column 'load_scenario'. "
f"Expected 0..{n_scenarios - 1}, got min={min_scenario}, max={max_scenario}."
)

# Bus index validation (continuous indices 0..n_buses-1)
min_bus = int(df["load"].min())
max_bus = int(df["load"].max())
if min_bus < 0 or max_bus >= n_buses:
raise ValueError(
"Scenario file contains out-of-range bus indices in column 'load'. "
f"Expected 0..{n_buses - 1}, got min={min_bus}, max={max_bus}."
)

# uniqueness of (load_scenario, load) pairs
dup_mask = df.duplicated(subset=["load_scenario", "load"])
if dup_mask.any():
dup_count = int(dup_mask.sum())
raise ValueError(
f"Scenario file contains {dup_count} duplicate (load_scenario, load) pairs. "
"Each pair must be unique."
)

# check: require all scenario-bus pairs present
expected = n_buses * n_scenarios
actual = len(df)
if actual != expected:
raise ValueError(
f"Scenario file must contain exactly {expected} rows "
f"({n_buses} buses x {n_scenarios} scenarios); got {actual}."
)

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

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
14 changes: 14 additions & 0 deletions 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 @@ -196,6 +197,19 @@ def get_load_scenario_generator(args: NestedNamespace) -> LoadScenarioGeneratorB
)

return Powergraph(args.agg_profile)

if args.generator == "precomputed_profile":
unused_args = {
key: value
for key, value in args.flatten().items()
if key not in ["generator", "scenario_file", "scenarios"]
}
if unused_args:
warnings.warn(
f"The following arguments are not used by the precomputed_profile generator: {unused_args}",
UserWarning,
)
return PrecomputedProfile(args.scenario_file)


def initialize_topology_generator(
Expand Down
51 changes: 51 additions & 0 deletions scripts/config/case14_config_precomputed_profile.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
network:
name: "case14_ieee" # Name of the power grid network (without extension)
source: "pglib" # Data source for the grid; options: pglib, file
# WARNING: the following parameter is only used if source is "file"
network_dir: "scripts/grids" # if using source "file", this is the directory containing the network file (relative to the project root)

load:
generator: "precomputed_profile" # Name of the load generator; options: agg_load_profile, powergraph, precomputed_profile
agg_profile: "default" # Name of the aggregated load profile
scenarios: 2 # Number of different load scenarios to generate. #if using precomputed_profile, this should match the number of scenarios in the csv file
# WARNING: the following parameters are only used if generator is "agg_load_profile"
# if using generator "powergraph" or "precomputed_profile", these parameters are ignored
sigma: 0.0 # max local noise
change_reactive_power: false # If true, changes reactive power of loads. If False, keeps the ones from the case file
global_range: 0.4 # Range of the global scaling factor. used to set the lower bound of the scaling factor
max_scaling_factor: 4.0 # Max upper bound of the global scaling factor
step_size: 0.1 # Step size when finding the upper bound of the global scaling factor
start_scaling_factor: 1.0 # Initial value of the global scaling factor
# WARNING: the following parameters are only used if generator is "precomputed_profile"
# if using generator "agg_load_profile" or "powergraph", these parameters are ignored
scenario_file: "scripts/precomputed_load_profiles/load-scenarios-precomputed-case14.csv" # precomputed scenarios (cols: load_scenario, load, p_mw, q_mvar)

topology_perturbation:
type: "random" # Type of topology generator; options: n_minus_k, random, none
# WARNING: the following parameters are only used if type is not "none"
k: 1 # Maximum number of components to drop in each perturbation
n_topology_variants: 2 # Number of unique perturbed topologies per scenario
elements: [branch, gen] # elements to perturb. options: branch, gen

generation_perturbation:
type: "cost_permutation" # Type of generation perturbation; options: cost_permutation, cost_perturbation, none
# WARNING: the following parameter is only used if type is "cost_permutation"
sigma: 1.0 # Size of range used for sampling scaling factor

admittance_perturbation:
type: "random_perturbation" # Type of admittance perturbation; options: random_perturbation, none
# WARNING: the following parameter is only used if type is "random_perturbation"
sigma: 0.2 # Size of range used for sampling scaling factor

settings:
num_processes: 16 # Number of parallel processes to use
data_dir: "./data_out" # Directory to save generated data relative to the project root
large_chunk_size: 1000 # Number of load scenarios processed before saving
overwrite: true # If true, overwrites existing files, if false, appends to files
mode: "pf" # Mode of the script; options: pf, opf. pf: power flow data where one or more operating limits – the inequality constraints defined in OPF, e.g., voltage magnitude or branch limits – may be violated. opf: generates datapoints for training OPF solvers, with cost-optimal dispatches that satisfy all operating limits (OPF-feasible)
include_dc_res: true # If true, also stores the results of dc power flow or dc optimal power flow
enable_solver_logs: true # If true, write OPF/PF logs to {data_dir}/solver_log; PF fast and DCPF fast do not log.
pf_fast: true # Whether to use fast PF solver by default (compute_ac_pf from powermodels.jl); if false, uses Ipopt-based PF. Some networks (typically large ones e.g. case10000_goc) do not work with pf_fast: true. pf_fast is faster and more accurate than the Ipopt-based PF.
dcpf_fast: true # Whether to use fast DCPF solver by default (compute_dc_pf from PowerModels.jl)
max_iter: 200 # Max iterations for Ipopt-based solvers
seed: null # Seed for random number generation. If null, a random seed is generated (RECOMMENDED). To get the same data across runs, set the seed and note that ALL OTHER PARAMETERS IN THE CONFIG FILE MUST BE THE SAME.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
load_scenario,load,p_mw,q_mvar
0,0,0,0
0,1,21.7,12.7
0,2,94.2,19.0
0,3,47.8,-3.9
0,4,7.6,1.6
0,5,11.2,7.5
0,6,0,0
0,7,0,0
0,8,29.5,16.6
0,9,9.0,5.8
0,10,3.5,1.8
0,11,6.1,1.6
0,12,13.5,5.8
0,13,14.9,5.0
1,0,0,0
1,1,22.7,13.2
1,2,94.2,19.0
1,3,47.8,-3.9
1,4,7.6,1.6
1,5,11.2,7.5
1,6,0,0
1,7,0,0
1,8,29.5,16.6
1,9,9.0,5.8
1,10,3.5,1.8
1,11,6.1,1.6
1,12,13.5,5.8
1,13,14.9,5.0
Loading