gridfm-datakit 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ """gridfm-datakit - A library for generating synthetic power grid data."""
2
+
3
+ from gridfm_datakit.generate import (
4
+ generate_power_flow_data,
5
+ generate_power_flow_data_distributed,
6
+ )
7
+
8
+ __version__ = "0.1.0"
9
+
10
+ __all__ = ["generate_power_flow_data", "generate_power_flow_data_distributed"]
gridfm_datakit/cli.py ADDED
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env python3
2
+ """Command-line interface for generating power flow data."""
3
+
4
+ import argparse
5
+ from gridfm_datakit.generate import (
6
+ generate_power_flow_data_distributed,
7
+ )
8
+
9
+
10
+ def main():
11
+ """Command-line interface for the data generation script."""
12
+ parser = argparse.ArgumentParser(
13
+ description="Generate power flow data for grid analysis",
14
+ )
15
+
16
+ parser.add_argument(
17
+ "config",
18
+ type=str,
19
+ help="Path to config file",
20
+ )
21
+
22
+ args = parser.parse_args()
23
+ file_paths = generate_power_flow_data_distributed(args.config)
24
+
25
+ print("\nData generation complete.")
26
+ print("Generated files:")
27
+ for key, path in file_paths.items():
28
+ print(f"- {key}: {path}")
29
+
30
+
31
+ if __name__ == "__main__":
32
+ main()
@@ -0,0 +1,449 @@
1
+ """Main data generation module for gridfm_datakit."""
2
+
3
+ import numpy as np
4
+ import os
5
+ from gridfm_datakit.save import (
6
+ save_edge_params,
7
+ save_bus_params,
8
+ save_branch_idx_removed,
9
+ save_node_edge_data,
10
+ )
11
+ from gridfm_datakit.process.process_network import (
12
+ network_preprocessing,
13
+ process_scenario,
14
+ process_scenario_contingency,
15
+ process_scenario_chunk,
16
+ )
17
+ from gridfm_datakit.utils.stats import plot_stats, Stats
18
+ from gridfm_datakit.utils.param_handler import (
19
+ NestedNamespace,
20
+ get_load_scenario_generator,
21
+ initialize_generator,
22
+ )
23
+ from gridfm_datakit.network import (
24
+ load_net_from_pp,
25
+ load_net_from_file,
26
+ load_net_from_pglib,
27
+ )
28
+ from gridfm_datakit.perturbations.load_perturbation import (
29
+ load_scenarios_to_df,
30
+ plot_load_scenarios_combined,
31
+ )
32
+ from pandapower.auxiliary import pandapowerNet
33
+ import gc
34
+ from datetime import datetime
35
+ from tqdm import tqdm
36
+ from multiprocessing import Pool, Manager
37
+ import shutil
38
+ from gridfm_datakit.utils.utils import write_ram_usage_distributed, Tee
39
+ import yaml
40
+ from typing import List, Tuple, Any, Dict, Optional, Union
41
+ import sys
42
+
43
+
44
+ def _setup_environment(
45
+ config: Union[str, Dict, NestedNamespace],
46
+ ) -> Tuple[NestedNamespace, str, Dict[str, str]]:
47
+ """Setup the environment for data generation.
48
+
49
+ Args:
50
+ config: Configuration can be provided in three ways:
51
+ 1. Path to a YAML config file (str)
52
+ 2. Configuration dictionary (Dict)
53
+ 3. NestedNamespace object (NestedNamespace)
54
+
55
+ Returns:
56
+ Tuple of (args, base_path, file_paths)
57
+ """
58
+ # Load config from file if a path is provided
59
+ if isinstance(config, str):
60
+ with open(config, "r") as f:
61
+ config = yaml.safe_load(f)
62
+
63
+ # Convert dict to NestedNamespace if needed
64
+ if isinstance(config, dict):
65
+ args = NestedNamespace(**config)
66
+ else:
67
+ args = config
68
+
69
+ # Setup output directory
70
+ base_path = os.path.join(args.settings.data_dir, args.network.name, "raw")
71
+ if os.path.exists(base_path) and args.settings.overwrite:
72
+ shutil.rmtree(base_path)
73
+ os.makedirs(base_path, exist_ok=True)
74
+
75
+ # Setup file paths
76
+ file_paths = {
77
+ "tqdm_log": os.path.join(base_path, "tqdm.log"),
78
+ "error_log": os.path.join(base_path, "error.log"),
79
+ "args_log": os.path.join(base_path, "args.log"),
80
+ "node_data": os.path.join(base_path, "pf_node.csv"),
81
+ "edge_data": os.path.join(base_path, "pf_edge.csv"),
82
+ "branch_indices": os.path.join(base_path, "branch_idx_removed.csv"),
83
+ "edge_params": os.path.join(base_path, "edge_params.csv"),
84
+ "bus_params": os.path.join(base_path, "bus_params.csv"),
85
+ "scenarios": os.path.join(base_path, f"scenarios_{args.load.generator}.csv"),
86
+ "scenarios_plot": os.path.join(
87
+ base_path,
88
+ f"scenarios_{args.load.generator}.html",
89
+ ),
90
+ "scenarios_log": os.path.join(
91
+ base_path,
92
+ f"scenarios_{args.load.generator}.log",
93
+ ),
94
+ }
95
+
96
+ # Initialize logs
97
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
98
+ for log_file in [
99
+ file_paths["tqdm_log"],
100
+ file_paths["error_log"],
101
+ file_paths["scenarios_log"],
102
+ file_paths["args_log"],
103
+ ]:
104
+ with open(log_file, "a") as f:
105
+ f.write(f"\nNew generation started at {timestamp}\n")
106
+ if log_file == file_paths["args_log"]:
107
+ yaml.dump(config if isinstance(config, dict) else vars(config), f)
108
+
109
+ return args, base_path, file_paths
110
+
111
+
112
+ def _prepare_network_and_scenarios(
113
+ args: NestedNamespace,
114
+ file_paths: Dict[str, str],
115
+ ) -> Tuple[pandapowerNet, Any]:
116
+ """Prepare the network and generate load scenarios.
117
+
118
+ Args:
119
+ args: Configuration object
120
+ file_paths: Dictionary of file paths
121
+
122
+ Returns:
123
+ Tuple of (network, scenarios)
124
+ """
125
+ # Load network
126
+ if args.network.source == "pandapower":
127
+ net = load_net_from_pp(args.network.name)
128
+ elif args.network.source == "pglib":
129
+ net = load_net_from_pglib(args.network.name)
130
+ elif args.network.source == "file":
131
+ net = load_net_from_file(
132
+ os.path.join(args.network.network_dir, args.network.name) + ".m",
133
+ )
134
+ else:
135
+ raise ValueError("Invalid grid source!")
136
+
137
+ network_preprocessing(net)
138
+ assert (net.sgen["scaling"] == 1).all(), "Scaling factor >1 not supported yet!"
139
+
140
+ # Generate load scenarios
141
+ load_scenario_generator = get_load_scenario_generator(args.load)
142
+ scenarios = load_scenario_generator(
143
+ net,
144
+ args.load.scenarios,
145
+ file_paths["scenarios_log"],
146
+ )
147
+ scenarios_df = load_scenarios_to_df(scenarios)
148
+ scenarios_df.to_csv(file_paths["scenarios"], index=False)
149
+ plot_load_scenarios_combined(scenarios_df, file_paths["scenarios_plot"])
150
+ save_edge_params(net, file_paths["edge_params"])
151
+ save_bus_params(net, file_paths["bus_params"])
152
+
153
+ return net, scenarios
154
+
155
+
156
+ def _save_generated_data(
157
+ net: pandapowerNet,
158
+ csv_data: List,
159
+ adjacency_lists: List,
160
+ branch_idx_removed: List,
161
+ global_stats: Optional[Stats],
162
+ file_paths: Dict[str, str],
163
+ base_path: str,
164
+ args: NestedNamespace,
165
+ ) -> None:
166
+ """Save the generated data to files.
167
+
168
+ Args:
169
+ net: Pandapower network
170
+ csv_data: List of CSV data
171
+ adjacency_lists: List of adjacency lists
172
+ branch_idx_removed: List of removed branch indices
173
+ global_stats: Optional statistics object
174
+ file_paths: Dictionary of file paths
175
+ base_path: Base output directory
176
+ args: Configuration object
177
+ """
178
+ if len(adjacency_lists) > 0:
179
+ save_node_edge_data(
180
+ net,
181
+ file_paths["node_data"],
182
+ file_paths["edge_data"],
183
+ csv_data,
184
+ adjacency_lists,
185
+ mode=args.settings.mode,
186
+ )
187
+ save_branch_idx_removed(branch_idx_removed, file_paths["branch_indices"])
188
+ if not args.settings.no_stats and global_stats:
189
+ global_stats.save(base_path)
190
+ plot_stats(base_path)
191
+
192
+
193
+ def generate_power_flow_data(
194
+ config: Union[str, Dict, NestedNamespace],
195
+ ) -> Dict[str, str]:
196
+ """Generate power flow data based on the provided configuration using sequential processing.
197
+
198
+ Args:
199
+ config: Configuration can be provided in three ways:
200
+ 1. Path to a YAML config file (str)
201
+ 2. Configuration dictionary (Dict)
202
+ 3. NestedNamespace object (NestedNamespace)
203
+ The config must include settings, network, load, and topology_perturbation configurations.
204
+
205
+ Returns:
206
+ Dictionary containing paths to generated files:
207
+ {
208
+ 'node_data': path to node data CSV,
209
+ 'edge_data': path to edge data CSV,
210
+ 'branch_indices': path to branch indices CSV,
211
+ 'edge_params': path to edge parameters CSV,
212
+ 'bus_params': path to bus parameters CSV,
213
+ 'scenarios': path to scenarios CSV,
214
+ 'scenarios_plot': path to scenarios plot HTML,
215
+ 'scenarios_log': path to scenarios log
216
+ }
217
+
218
+ Note:
219
+ The function creates several output files in the specified data directory:
220
+
221
+ - tqdm.log: Progress tracking
222
+ - error.log: Error messages
223
+ - args.log: Configuration parameters
224
+ - pf_node.csv: Node data
225
+ - pf_edge.csv: Edge data
226
+ - branch_idx_removed.csv: Removed branch indices
227
+ - edge_params.csv: Edge parameters
228
+ - bus_params.csv: Bus parameters
229
+ - scenarios_{generator}.csv: Load scenarios
230
+ - scenarios_{generator}.html: Scenario plots
231
+ - scenarios_{generator}.log: Scenario generation log
232
+ """
233
+ # Setup environment
234
+ args, base_path, file_paths = _setup_environment(config)
235
+
236
+ # Prepare network and scenarios
237
+ net, scenarios = _prepare_network_and_scenarios(args, file_paths)
238
+
239
+ # Initialize topology generator and data structures
240
+ generator = initialize_generator(args.topology_perturbation, net)
241
+ csv_data = []
242
+ adjacency_lists = []
243
+ branch_idx_removed = []
244
+ global_stats = Stats() if not args.settings.no_stats else None
245
+
246
+ # Process scenarios sequentially
247
+ with open(file_paths["tqdm_log"], "a") as f:
248
+ with tqdm(
249
+ total=args.load.scenarios,
250
+ desc="Processing scenarios",
251
+ file=Tee(sys.stdout, f),
252
+ miniters=5,
253
+ ) as pbar:
254
+ for scenario_index in range(args.load.scenarios):
255
+ # Process the scenario
256
+ if args.settings.mode == "pf":
257
+ csv_data, adjacency_lists, branch_idx_removed, global_stats = (
258
+ process_scenario(
259
+ net,
260
+ scenarios,
261
+ scenario_index,
262
+ generator,
263
+ args.settings.no_stats,
264
+ csv_data,
265
+ adjacency_lists,
266
+ branch_idx_removed,
267
+ global_stats,
268
+ file_paths["error_log"],
269
+ )
270
+ )
271
+ elif args.settings.mode == "contingency":
272
+ csv_data, adjacency_lists, branch_idx_removed, global_stats = (
273
+ process_scenario_contingency(
274
+ net,
275
+ scenarios,
276
+ scenario_index,
277
+ generator,
278
+ args.settings.no_stats,
279
+ csv_data,
280
+ adjacency_lists,
281
+ branch_idx_removed,
282
+ global_stats,
283
+ file_paths["error_log"],
284
+ )
285
+ )
286
+
287
+ pbar.update(1)
288
+
289
+ # Save final data
290
+ _save_generated_data(
291
+ net,
292
+ csv_data,
293
+ adjacency_lists,
294
+ branch_idx_removed,
295
+ global_stats,
296
+ file_paths,
297
+ base_path,
298
+ args,
299
+ )
300
+ return file_paths
301
+
302
+
303
+ def generate_power_flow_data_distributed(
304
+ config: Union[str, Dict, NestedNamespace],
305
+ ) -> Dict[str, str]:
306
+ """Generate power flow data based on the provided configuration using distributed processing.
307
+
308
+
309
+ Args:
310
+ config: Configuration can be provided in three ways:
311
+ 1. Path to a YAML config file (str)
312
+ 2. Configuration dictionary (Dict)
313
+ 3. NestedNamespace object (NestedNamespace)
314
+ The config must include settings, network, load, and topology_perturbation configurations.
315
+
316
+ Returns:
317
+ Dictionary containing paths to generated files:
318
+ {
319
+ 'node_data': path to node data CSV,
320
+ 'edge_data': path to edge data CSV,
321
+ 'branch_indices': path to branch indices CSV,
322
+ 'edge_params': path to edge parameters CSV,
323
+ 'bus_params': path to bus parameters CSV,
324
+ 'scenarios': path to scenarios CSV,
325
+ 'scenarios_plot': path to scenarios plot HTML,
326
+ 'scenarios_log': path to scenarios log
327
+ }
328
+
329
+ Note:
330
+ The function creates several output files in the specified data directory:
331
+
332
+ - tqdm.log: Progress tracking
333
+ - error.log: Error messages
334
+ - args.log: Configuration parameters
335
+ - pf_node.csv: Node data
336
+ - pf_edge.csv: Edge data
337
+ - branch_idx_removed.csv: Removed branch indices
338
+ - edge_params.csv: Edge parameters
339
+ - bus_params.csv: Bus parameters
340
+ - scenarios_{generator}.csv: Load scenarios
341
+ - scenarios_{generator}.html: Scenario plots
342
+ - scenarios_{generator}.log: Scenario generation log
343
+ """
344
+ # Setup environment
345
+ args, base_path, file_paths = _setup_environment(config)
346
+
347
+ # Prepare network and scenarios
348
+ net, scenarios = _prepare_network_and_scenarios(args, file_paths)
349
+
350
+ # Initialize topology generator
351
+ generator = initialize_generator(args.topology_perturbation, net)
352
+
353
+ # Setup multiprocessing
354
+ manager = Manager()
355
+ progress_queue = manager.Queue()
356
+
357
+ # Process scenarios in chunks
358
+ large_chunks = np.array_split(
359
+ range(args.load.scenarios),
360
+ np.ceil(args.load.scenarios / args.settings.large_chunk_size).astype(int),
361
+ )
362
+
363
+ with open(file_paths["tqdm_log"], "a") as f:
364
+ with tqdm(
365
+ total=args.load.scenarios,
366
+ desc="Processing scenarios",
367
+ file=Tee(sys.stdout, f),
368
+ miniters=5,
369
+ ) as pbar:
370
+ for large_chunk_index, large_chunk in enumerate(large_chunks):
371
+ write_ram_usage_distributed(f)
372
+ chunk_size = len(large_chunk)
373
+ scenario_chunks = np.array_split(
374
+ large_chunk,
375
+ args.settings.num_processes,
376
+ )
377
+
378
+ tasks = [
379
+ (
380
+ args.settings.mode,
381
+ chunk[0],
382
+ chunk[-1] + 1,
383
+ scenarios,
384
+ net,
385
+ progress_queue,
386
+ generator,
387
+ args.settings.no_stats,
388
+ file_paths["error_log"],
389
+ )
390
+ for chunk in scenario_chunks
391
+ ]
392
+
393
+ # Run parallel processing
394
+ with Pool(processes=args.settings.num_processes) as pool:
395
+ results = [
396
+ pool.apply_async(process_scenario_chunk, task) for task in tasks
397
+ ]
398
+
399
+ # Update progress
400
+ completed = 0
401
+ while completed < chunk_size:
402
+ progress_queue.get()
403
+ pbar.update(1)
404
+ completed += 1
405
+
406
+ # Gather results
407
+ csv_data = []
408
+ adjacency_lists = []
409
+ branch_idx_removed = []
410
+ global_stats = Stats() if not args.settings.no_stats else None
411
+
412
+ for result in results:
413
+ (
414
+ e,
415
+ traceback,
416
+ local_csv_data,
417
+ local_adjacency_lists,
418
+ local_branch_idx_removed,
419
+ local_stats,
420
+ ) = result.get()
421
+ if isinstance(e, Exception):
422
+ print(f"Error in process_scenario_chunk: {e}")
423
+ print(traceback)
424
+ sys.exit(1)
425
+ csv_data.extend(local_csv_data)
426
+ adjacency_lists.extend(local_adjacency_lists)
427
+ branch_idx_removed.extend(local_branch_idx_removed)
428
+ if not args.settings.no_stats and local_stats:
429
+ global_stats.merge(local_stats)
430
+
431
+ pool.close()
432
+ pool.join()
433
+
434
+ # Save processed data
435
+ _save_generated_data(
436
+ net,
437
+ csv_data,
438
+ adjacency_lists,
439
+ branch_idx_removed,
440
+ global_stats,
441
+ file_paths,
442
+ base_path,
443
+ args,
444
+ )
445
+
446
+ del csv_data, adjacency_lists, global_stats
447
+ gc.collect()
448
+
449
+ return file_paths
File without changes