diff --git a/docs/components/load.md b/docs/components/load.md index 0d9675ac7..20aec7980 100644 --- a/docs/components/load.md +++ b/docs/components/load.md @@ -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 diff --git a/docs/manual/getting_started.md b/docs/manual/getting_started.md index 57db8d10d..c2003c5a3 100644 --- a/docs/manual/getting_started.md +++ b/docs/manual/getting_started.md @@ -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 diff --git a/docs/manual/load_scenarios.md b/docs/manual/load_scenarios.md index ac0a974ec..95c4a3b8a 100644 --- a/docs/manual/load_scenarios.md +++ b/docs/manual/load_scenarios.md @@ -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
-| 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 |
@@ -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 @@ -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: diff --git a/docs/manual/outputs.md b/docs/manual/outputs.md index ec8a38fe3..635cef9c0 100644 --- a/docs/manual/outputs.md +++ b/docs/manual/outputs.md @@ -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). diff --git a/gridfm_datakit/perturbations/load_perturbation.py b/gridfm_datakit/perturbations/load_perturbation.py index 0d2b2e335..f8e5c12be 100644 --- a/gridfm_datakit/perturbations/load_perturbation.py +++ b/gridfm_datakit/perturbations/load_perturbation.py @@ -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__": """ diff --git a/gridfm_datakit/utils/param_handler.py b/gridfm_datakit/utils/param_handler.py index cd1fca724..bfc5bf7c7 100644 --- a/gridfm_datakit/utils/param_handler.py +++ b/gridfm_datakit/utils/param_handler.py @@ -3,6 +3,7 @@ LoadScenarioGeneratorBase, LoadScenariosFromAggProfile, Powergraph, + PrecomputedProfile, ) from typing import Dict, Any import warnings @@ -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( diff --git a/scripts/config/case14_config_precomputed_profile.yaml b/scripts/config/case14_config_precomputed_profile.yaml new file mode 100644 index 000000000..6045321cc --- /dev/null +++ b/scripts/config/case14_config_precomputed_profile.yaml @@ -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. diff --git a/scripts/precomputed_load_profiles/load-scenarios-precomputed-case14.csv b/scripts/precomputed_load_profiles/load-scenarios-precomputed-case14.csv new file mode 100644 index 000000000..f50d152f9 --- /dev/null +++ b/scripts/precomputed_load_profiles/load-scenarios-precomputed-case14.csv @@ -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 \ No newline at end of file