fdia-graph 0.5.0__tar.gz

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,51 @@
1
+ Creative Commons Attribution 4.0 International (CC BY 4.0)
2
+
3
+ The fdia-graph datasets (the HDF5 shards, operating-point pools, and derived
4
+ data distributed through this repository's releases) are licensed under the
5
+ Creative Commons Attribution 4.0 International License.
6
+
7
+ You are free to:
8
+
9
+ Share — copy and redistribute the material in any medium or format
10
+ Adapt — remix, transform, and build upon the material for any purpose,
11
+ even commercially.
12
+
13
+ Under the following terms:
14
+
15
+ Attribution — You must give appropriate credit, provide a link to the
16
+ license, and indicate if changes were made. You may do so in any reasonable
17
+ manner, but not in any way that suggests the licensor endorses you or your
18
+ use.
19
+
20
+ Full license text: https://creativecommons.org/licenses/by/4.0/legalcode
21
+
22
+ Disclaimer: this is synthetic data generated from public IEEE test cases
23
+ (pandapower case14/case118/case300) with simulated measurements and simulated
24
+ attack injections. It does not describe any real power system, real telemetry,
25
+ or real operating conditions, and it must not be used for operational
26
+ decisions.
27
+
28
+ The source code in this repository (src/, examples/) is licensed under the MIT
29
+ License:
30
+
31
+ MIT License
32
+
33
+ Copyright (c) 2026 Benjamin Myers
34
+
35
+ Permission is hereby granted, free of charge, to any person obtaining a copy
36
+ of this software and associated documentation files (the "Software"), to deal
37
+ in the Software without restriction, including without limitation the rights
38
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
39
+ copies of the Software, and to permit persons to whom the Software is
40
+ furnished to do so, subject to the following conditions:
41
+
42
+ The above copyright notice and this permission notice shall be included in all
43
+ copies or substantial portions of the Software.
44
+
45
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
46
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
47
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
48
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
49
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
50
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
51
+ SOFTWARE.
@@ -0,0 +1,249 @@
1
+ Metadata-Version: 2.4
2
+ Name: fdia-graph
3
+ Version: 0.5.0
4
+ Summary: Load and generate ML-only dangerous FDIA localization datasets (realistic measurement graphs) for power grids, PyTorch-ready.
5
+ Author-email: Benjamin Myers <benjamin.myers.465@my.csun.edu>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/myersben9/fdia-graph
8
+ Project-URL: Repository, https://github.com/myersben9/fdia-graph
9
+ Keywords: power-systems,false-data-injection,FDIA,state-estimation,graph-neural-network,smart-grid,dataset
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: numpy>=1.21
18
+ Requires-Dist: h5py>=3.6
19
+ Requires-Dist: requests>=2.25
20
+ Requires-Dist: tqdm>=4.60
21
+ Provides-Extra: torch
22
+ Requires-Dist: torch>=1.12; extra == "torch"
23
+ Provides-Extra: pyg
24
+ Requires-Dist: torch>=1.12; extra == "pyg"
25
+ Requires-Dist: torch_geometric>=2.2; extra == "pyg"
26
+ Provides-Extra: generate
27
+ Requires-Dist: pandapower>=2.10; extra == "generate"
28
+ Requires-Dist: pandas>=1.3; extra == "generate"
29
+ Provides-Extra: iso
30
+ Requires-Dist: gridstatus>=0.25; extra == "iso"
31
+ Requires-Dist: pandas>=1.3; extra == "iso"
32
+ Provides-Extra: all
33
+ Requires-Dist: torch>=1.12; extra == "all"
34
+ Requires-Dist: torch_geometric>=2.2; extra == "all"
35
+ Requires-Dist: pandapower>=2.10; extra == "all"
36
+ Requires-Dist: pandas>=1.3; extra == "all"
37
+ Requires-Dist: gridstatus>=0.25; extra == "all"
38
+ Dynamic: license-file
39
+
40
+ # fdia-graph
41
+
42
+ **Load and generate ML-only *dangerous* FDIA localization datasets for power grids — PyTorch-ready, zero data plumbing.**
43
+
44
+ `fdia-graph` gives power-systems / ML researchers one-line access to a benchmark of **false-data-injection attacks that evade every classical detector but are localizable only by a model** — on realistic sparse SCADA/PMU measurement graphs for IEEE-14 / 118 / 300. Install it, call `load(...)`, and train.
45
+
46
+ ```python
47
+ import fdia_graph as fg
48
+
49
+ ds = fg.load("ieee118", split="train") # auto-downloads + caches the newest release
50
+ loader = ds.loader(batch_size=64) # ready-to-train PyTorch DataLoader
51
+ for batch in loader:
52
+ batch["node_x"], batch["edge_x"], batch["edge_index"], batch["y"], batch["family"], ...
53
+ ```
54
+
55
+ ---
56
+
57
+ ## Why this dataset
58
+
59
+ Most FDIA benchmarks contain attacks a bad-data detector (BDD) catches, so "ML beats BDD" is unsurprising. This dataset is built around the opposite: **three stealthy families that provably evade classical detection** — and are genuinely dangerous — alongside three detectable families as a contrast set.
60
+
61
+ | Family | Type | Classical BDD |
62
+ |--------|------|---------------|
63
+ | `Aq` | stealthy load scaling: bounded per-bus rescale + AC re-solve — **our contribution** (cf. Boyaci `Ao`, Liu FDIA) | **evades (stealthy)** |
64
+ | `At` | temporal load surge, ramps up then back down (Haghshenas et al., IEEE ISGT 2023) | **evades (stealthy)** |
65
+ | `Al` | targeted masked-overload / load redistribution (Yuan, Li & Ren, IEEE T-SG 2011) | **evades (stealthy)** |
66
+ | `Ad` | random meter corruption | caught |
67
+ | `As` | meter scaling | caught |
68
+ | `Ar` | replay | mostly caught |
69
+
70
+ Each record is a **PING-style measurement graph** (branch flows as edge features, metered injections + |V| + sparse PMU angles as node features, with availability masks) — what a real EMS actually sees (redundancy ≈ 2–3), not the full-injection idealization.
71
+
72
+ ## Install
73
+
74
+ ```bash
75
+ pip install fdia-graph # loader (numpy + h5py)
76
+ pip install "fdia-graph[torch]" # + PyTorch Dataset/DataLoader
77
+ pip install "fdia-graph[pyg]" # + torch_geometric graph format
78
+ pip install "fdia-graph[generate]" # + pandapower, to generate custom datasets from load profiles
79
+ pip install "fdia-graph[iso]" # + gridstatus, to auto-download CAISO/ERCOT load profiles
80
+ pip install "fdia-graph[all]" # everything
81
+ ```
82
+
83
+ Everything is delivered from this package and its GitHub releases — datasets, operating-point pools, and (with `[generate]`) the whole simulation pipeline. There is no separate data drop to sync.
84
+
85
+ ## Load
86
+
87
+ ```python
88
+ ds = fg.load("ieee300", split="train") # 60/20/20 chronological split
89
+ val = fg.load("ieee300", split="val")
90
+ tst = fg.load("ieee300", split="test")
91
+
92
+ # family subsets and the unseen-attack generalization protocol
93
+ stealthy = fg.load("ieee118", split="test", families=["Aq", "At", "Al"]) # "Ao"/"ramp"/"LRA" still work as aliases
94
+ heldout = fg.load("ieee118", split="train", heldout=True) # As/Ar excluded from train (Boyaci et al. 2022)
95
+ ```
96
+
97
+ ### Any framework you like
98
+
99
+ The `.loader()` streams records for training; these pull the whole split at once for analysis:
100
+
101
+ ```python
102
+ ds = fg.load("ieee14", split="test")
103
+ arrays = ds.to_numpy() # dict of numpy arrays
104
+ tensors = ds.to_torch() # dict of torch tensors (float32 features, int64 labels)
105
+ tf_t = ds.to_tf() # dict of tf.Tensors (needs tensorflow)
106
+ df = ds.to_pandas() # flat pandas DataFrame, one row per record
107
+ ```
108
+
109
+ ### Dataset versioning
110
+
111
+ Datasets are GitHub **releases**, so your group version-controls them:
112
+
113
+ ```python
114
+ fg.load("ieee118") # newest release (default — everyone stays current)
115
+ fg.load("ieee118", release="v0.3.0") # pin an exact version for a reproducible experiment
116
+ ```
117
+
118
+ ## Generate a custom dataset (research knobs)
119
+
120
+ Turn any research knob and load the result by name — no data plumbing:
121
+
122
+ ```python
123
+ fg.generate("ieee118", name="high_intensity",
124
+ per_family=5000, # samples per attack family
125
+ families=["Aq", "At", "Al"],
126
+ attack_intensity=0.25, # per-bus load-shift magnitude (± fraction)
127
+ ramp_rate=0.003, ramp_len=80,
128
+ n_benign=30000,
129
+ redundancy={"pmu_frac": 0.3, "flow_frac": 0.95},
130
+ split=(0.7, 0.15, 0.15),
131
+ seed=7)
132
+
133
+ ds = fg.load("high_intensity", split="train") # your custom dataset, ready to train
134
+ ```
135
+
136
+ Generation ships with compact operating-point **pools** (a few MB/system), so you never need the raw simulation data. Benign records are emitted *exactly* from the stored operating state (0-error AC flows); only attacks re-solve a power flow.
137
+
138
+ ### N-1 line outages (topology shift)
139
+
140
+ A switching event moves the operating manifold, so anything fitted on the intact network (a subspace prior, a learned estimator) is extrapolating afterwards. `outage=` builds a shard under a post-contingency topology: the line is taken out of service before Ybus, PTDF, the base operating point and every emitted measurement are derived, so the physics is genuinely different rather than masked.
141
+
142
+ ```python
143
+ acc, rej = fg.line_outage_candidates(118, top_n=5) # highest base-case flow, connectivity-screened
144
+ # rej explains every contingency that was skipped (islanding, non-convergent, ...)
145
+
146
+ from fdia_graph._core import FdiaGenerator
147
+ g = FdiaGenerator(118, seed=123, outage=acc[0]["line"])
148
+ states, ok = g.resolve_states(pool_X) # re-solve the SAME load timestamps under this topology
149
+
150
+ fg.generate(118, name="ieee118_n1", states=states[ok], outage=acc[0]["line"])
151
+ ```
152
+
153
+ One shard per topology: each file's `graph/` group describes its own network (`edge_status` carries the single zero), so the loader is unchanged and the static graph can never be wrong for a record. Feed each scenario a pool re-solved under that topology — a stored intact-network state emitted through a post-contingency Ybus would satisfy no power flow at all. Keep the load timestamps identical across scenarios, or topology and load level are confounded. Shard attrs record `topology`, `outage_line`, `outage_base_flow_mw` and the per-family `solve_yield` (attempts/accepted, so dropped hard cases stay visible).
154
+
155
+ ## Own the whole pipeline: your own load profiles
156
+
157
+ The pools above are pre-built, but you can build your own from real grid load — swap in data from a different ISO or a different time period and regenerate everything. The front of the pipeline is two functions:
158
+
159
+ ```python
160
+ # 1. Get a load profile. Auto-download real system load at 5-minute resolution (the finest each ISO
161
+ # publishes), or bring your own series.
162
+ S = fg.fetch_profile("nyiso", "2024-01-01", "2024-06-30") # NYISO — no account, no extra deps
163
+ S = fg.fetch_profile("caiso", "2024-01-01", "2024-06-30") # CAISO — needs fdia-graph[iso]
164
+ S = fg.fetch_profile("ercot", "2024-01-01", "2024-06-30") # ERCOT — needs fdia-graph[iso]
165
+ S = fg.load_profile(my_load_array) # or a CSV / a numpy array of load values
166
+
167
+ # 2. Turn the profile into a pool of AC operating states, then generate attacks onto it.
168
+ states = fg.generate_states("ieee118", S) # [T, N, 4] via pandapower power flow
169
+ fg.generate(118, name="ieee118_nyiso_2024", states=states, per_family=5000)
170
+ ds = fg.load("ieee118_nyiso_2024", split="train")
171
+ ```
172
+
173
+ `fetch_profile` returns a normalized per-timestep scaling vector; `generate_states` scales each bus's load by `clip(1 + k·S_t + noise)`, solves the power flow, and records the clean, bad-data-consistent state. Switching load source or time window is a one-line change, so the same grid can be re-generated under many demand regimes. Requires `fdia-graph[generate]` (`[iso]` too for CAISO/ERCOT).
174
+
175
+ ## Schema
176
+
177
+ One HDF5 file per system (`ml_only_ieee{14,118,300}.h5`), with `N` = buses and `E` = branches (lines + transformers). The **static graph** is stored once; everything else is **per record** (`T` records total). A record is one realistic measurement snapshot — benign or attacked.
178
+
179
+ **Static graph** (read once, shared by every record):
180
+
181
+ | Field | Shape | Dtype | Meaning |
182
+ |-------|-------|-------|---------|
183
+ | `edge_index` | `[2, E]` | int64 | `[from_bus; to_bus]` for each branch (lines first, then transformers) |
184
+ | `edge_reactance` | `[E]` | float32 | per-branch reactance (p.u.) — handy for physics-informed models |
185
+
186
+ **Per-record measurement graph** (indexed `0 … T-1`; access one via `ds[i]`, or a whole split via `ds.to_numpy()`):
187
+
188
+ | Field | Shape | Dtype | Meaning |
189
+ |-------|-------|-------|---------|
190
+ | `node_x` | `[N, 4]` | float32 | node features `[ \|V\| (p.u.), P_inj (MW), Q_inj (MVAr), θ (deg) ]` |
191
+ | `node_m` | `[N, 4]` | float32 | node **availability mask** (1 = that meter exists at that bus, else 0; masked entries are zeroed) |
192
+ | `edge_x` | `[E, 2]` | float32 | branch-flow features `[ P_from (MW), Q_from (MVAr) ]` |
193
+ | `edge_m` | `[E, 2]` | float32 | edge availability mask |
194
+ | `y` | `[N]` | float32 | **localization target** — per-bus label, `1` = bus is attacked, `0` = clean |
195
+ | `temporal_delta` | `[N, 2]` | float32 | *(v0.3+)* current-minus-previous-scan injection `[ΔP_inj, ΔQ_inj]` — the temporal feature for replay/ramp |
196
+ | `family` | scalar | int | `0` benign · `1` Aq · `2` Ad · `3` As · `4` Ar · `5` At · `6` Al |
197
+ | `stealthy` | scalar | int | `1` if the attack evades classical bad-data detection (Ao/ramp/LRA), else `0` |
198
+ | `split` | scalar | int | `0` train · `1` val · `2` test (60/20/20 chronological, sequence-boundary safe) |
199
+ | `seq_id` | scalar | int | ramp-sequence id (`≥0` groups the scans of one multi-timestep ramp); `-1` otherwise |
200
+ | `timestep` | scalar | int | source operating-point index (the benign snapshot the record was built from) |
201
+ | `gap` | scalar | int | `1` if this is a physics non-convergence NA row (`≈0%` in the shipped data) |
202
+
203
+ Sparsity is real: `node_m`/`edge_m` encode a redundancy of ≈ 2–3 (a realistic EMS regime), so a model must consume the masks — not every bus is metered, and PMU angles (`θ`) are sparse.
204
+
205
+ ## Project structure
206
+
207
+ ```
208
+ fdia-graph/
209
+ ├── pyproject.toml # package metadata, deps, optional extras ([torch], [pyg], [generate], [iso], [all])
210
+ ├── README.md # this file
211
+ ├── src/fdia_graph/
212
+ │ ├── __init__.py # public API: load(), generate(), fetch_profile/load_profile/generate_states ← start here
213
+ │ ├── registry.py # dataset version control: name → GitHub release + file; latest vs pinned; cache dir
214
+ │ ├── download.py # fetches release-asset .h5 shards (public, anonymous) → ~/.cache/fdia_graph
215
+ │ ├── dataset.py # FdiaGraph: torch Dataset over one .h5 (lazy slicing, split/family filters, exporters)
216
+ │ ├── profiles.py # front of the pipeline: ISO load download (CAISO/NYISO/ERCOT) → operating states
217
+ │ ├── generation.py # generate(): tunable-knob dataset creation → new .h5, registered by name
218
+ │ └── _core.py # generation engine: physics (Ybus/PTDF) + the 7 attack families
219
+ └── examples/
220
+ ├── quickstart.py # smallest load → train loop
221
+ ├── train_gnn.py # baseline GNN localizer
222
+ ├── train_arma.py # ARMA + physics-biased attention localizer (the strong model)
223
+ ├── train_tgnn.py # temporal-graph localizer
224
+ ├── hpo_arma.py # Optuna hyperparameter search (Boyaci-style space)
225
+ └── reproduce_report.ipynb # end-to-end notebook: generate → figures → train/test
226
+ ```
227
+
228
+ Read order to understand the codebase: `__init__.py` (the two entry points) → `registry.py`/`download.py` (how a name becomes a local `.h5`) → `dataset.py` (how that `.h5` becomes tensors) → `generate.py`/`_core.py` (how new datasets are made). Every module is commented line-by-line.
229
+
230
+ ## Evaluation protocol
231
+
232
+ 60/20/20 **chronological** split cut on sequence boundaries (ramp sequences never straddle a split — a random shuffle would leak them). Equal count per attack family. **Report per-attack-type node-F1**, not accuracy — the stealthy families (`Ao`/`ramp`/`LRA`) are the hard ones and accuracy hides them.
233
+
234
+ ## Citation
235
+
236
+ If you use this dataset, please cite the attack model and measurement-model sources:
237
+
238
+ - Yuan, Li & Ren, *Modeling load redistribution attacks in power systems*, IEEE Trans. Smart Grid 2(2), 2011. *(LRA attack)*
239
+ - Haghshenas, Hasnat & Naeini, *A Temporal Graph Neural Network for Cyber Attack Detection and Localization in Smart Grids*, IEEE ISGT 2023. *(ramp attack)*
240
+ - Zaman & Lin, *PING: Physics-Informed GNNs to Generalize FDIA Localization*, NAPS 2025. *(measurement model)*
241
+ - Boyaci et al., *Joint Detection and Localization of Stealth FDIA*, IEEE Trans. Smart Grid, 2022. *(protocol)*
242
+
243
+ ## License
244
+
245
+ The dataset (HDF5 shards and operating-point pools distributed through the GitHub releases) is
246
+ licensed under Creative Commons Attribution 4.0 International (CC BY 4.0). The source code in `src/`
247
+ and `examples/` is licensed under the MIT License. See the `LICENSE` file. The data is synthetic,
248
+ generated from public IEEE test cases with simulated measurements and simulated attacks, and must
249
+ not be used for operational decisions.
@@ -0,0 +1,210 @@
1
+ # fdia-graph
2
+
3
+ **Load and generate ML-only *dangerous* FDIA localization datasets for power grids — PyTorch-ready, zero data plumbing.**
4
+
5
+ `fdia-graph` gives power-systems / ML researchers one-line access to a benchmark of **false-data-injection attacks that evade every classical detector but are localizable only by a model** — on realistic sparse SCADA/PMU measurement graphs for IEEE-14 / 118 / 300. Install it, call `load(...)`, and train.
6
+
7
+ ```python
8
+ import fdia_graph as fg
9
+
10
+ ds = fg.load("ieee118", split="train") # auto-downloads + caches the newest release
11
+ loader = ds.loader(batch_size=64) # ready-to-train PyTorch DataLoader
12
+ for batch in loader:
13
+ batch["node_x"], batch["edge_x"], batch["edge_index"], batch["y"], batch["family"], ...
14
+ ```
15
+
16
+ ---
17
+
18
+ ## Why this dataset
19
+
20
+ Most FDIA benchmarks contain attacks a bad-data detector (BDD) catches, so "ML beats BDD" is unsurprising. This dataset is built around the opposite: **three stealthy families that provably evade classical detection** — and are genuinely dangerous — alongside three detectable families as a contrast set.
21
+
22
+ | Family | Type | Classical BDD |
23
+ |--------|------|---------------|
24
+ | `Aq` | stealthy load scaling: bounded per-bus rescale + AC re-solve — **our contribution** (cf. Boyaci `Ao`, Liu FDIA) | **evades (stealthy)** |
25
+ | `At` | temporal load surge, ramps up then back down (Haghshenas et al., IEEE ISGT 2023) | **evades (stealthy)** |
26
+ | `Al` | targeted masked-overload / load redistribution (Yuan, Li & Ren, IEEE T-SG 2011) | **evades (stealthy)** |
27
+ | `Ad` | random meter corruption | caught |
28
+ | `As` | meter scaling | caught |
29
+ | `Ar` | replay | mostly caught |
30
+
31
+ Each record is a **PING-style measurement graph** (branch flows as edge features, metered injections + |V| + sparse PMU angles as node features, with availability masks) — what a real EMS actually sees (redundancy ≈ 2–3), not the full-injection idealization.
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ pip install fdia-graph # loader (numpy + h5py)
37
+ pip install "fdia-graph[torch]" # + PyTorch Dataset/DataLoader
38
+ pip install "fdia-graph[pyg]" # + torch_geometric graph format
39
+ pip install "fdia-graph[generate]" # + pandapower, to generate custom datasets from load profiles
40
+ pip install "fdia-graph[iso]" # + gridstatus, to auto-download CAISO/ERCOT load profiles
41
+ pip install "fdia-graph[all]" # everything
42
+ ```
43
+
44
+ Everything is delivered from this package and its GitHub releases — datasets, operating-point pools, and (with `[generate]`) the whole simulation pipeline. There is no separate data drop to sync.
45
+
46
+ ## Load
47
+
48
+ ```python
49
+ ds = fg.load("ieee300", split="train") # 60/20/20 chronological split
50
+ val = fg.load("ieee300", split="val")
51
+ tst = fg.load("ieee300", split="test")
52
+
53
+ # family subsets and the unseen-attack generalization protocol
54
+ stealthy = fg.load("ieee118", split="test", families=["Aq", "At", "Al"]) # "Ao"/"ramp"/"LRA" still work as aliases
55
+ heldout = fg.load("ieee118", split="train", heldout=True) # As/Ar excluded from train (Boyaci et al. 2022)
56
+ ```
57
+
58
+ ### Any framework you like
59
+
60
+ The `.loader()` streams records for training; these pull the whole split at once for analysis:
61
+
62
+ ```python
63
+ ds = fg.load("ieee14", split="test")
64
+ arrays = ds.to_numpy() # dict of numpy arrays
65
+ tensors = ds.to_torch() # dict of torch tensors (float32 features, int64 labels)
66
+ tf_t = ds.to_tf() # dict of tf.Tensors (needs tensorflow)
67
+ df = ds.to_pandas() # flat pandas DataFrame, one row per record
68
+ ```
69
+
70
+ ### Dataset versioning
71
+
72
+ Datasets are GitHub **releases**, so your group version-controls them:
73
+
74
+ ```python
75
+ fg.load("ieee118") # newest release (default — everyone stays current)
76
+ fg.load("ieee118", release="v0.3.0") # pin an exact version for a reproducible experiment
77
+ ```
78
+
79
+ ## Generate a custom dataset (research knobs)
80
+
81
+ Turn any research knob and load the result by name — no data plumbing:
82
+
83
+ ```python
84
+ fg.generate("ieee118", name="high_intensity",
85
+ per_family=5000, # samples per attack family
86
+ families=["Aq", "At", "Al"],
87
+ attack_intensity=0.25, # per-bus load-shift magnitude (± fraction)
88
+ ramp_rate=0.003, ramp_len=80,
89
+ n_benign=30000,
90
+ redundancy={"pmu_frac": 0.3, "flow_frac": 0.95},
91
+ split=(0.7, 0.15, 0.15),
92
+ seed=7)
93
+
94
+ ds = fg.load("high_intensity", split="train") # your custom dataset, ready to train
95
+ ```
96
+
97
+ Generation ships with compact operating-point **pools** (a few MB/system), so you never need the raw simulation data. Benign records are emitted *exactly* from the stored operating state (0-error AC flows); only attacks re-solve a power flow.
98
+
99
+ ### N-1 line outages (topology shift)
100
+
101
+ A switching event moves the operating manifold, so anything fitted on the intact network (a subspace prior, a learned estimator) is extrapolating afterwards. `outage=` builds a shard under a post-contingency topology: the line is taken out of service before Ybus, PTDF, the base operating point and every emitted measurement are derived, so the physics is genuinely different rather than masked.
102
+
103
+ ```python
104
+ acc, rej = fg.line_outage_candidates(118, top_n=5) # highest base-case flow, connectivity-screened
105
+ # rej explains every contingency that was skipped (islanding, non-convergent, ...)
106
+
107
+ from fdia_graph._core import FdiaGenerator
108
+ g = FdiaGenerator(118, seed=123, outage=acc[0]["line"])
109
+ states, ok = g.resolve_states(pool_X) # re-solve the SAME load timestamps under this topology
110
+
111
+ fg.generate(118, name="ieee118_n1", states=states[ok], outage=acc[0]["line"])
112
+ ```
113
+
114
+ One shard per topology: each file's `graph/` group describes its own network (`edge_status` carries the single zero), so the loader is unchanged and the static graph can never be wrong for a record. Feed each scenario a pool re-solved under that topology — a stored intact-network state emitted through a post-contingency Ybus would satisfy no power flow at all. Keep the load timestamps identical across scenarios, or topology and load level are confounded. Shard attrs record `topology`, `outage_line`, `outage_base_flow_mw` and the per-family `solve_yield` (attempts/accepted, so dropped hard cases stay visible).
115
+
116
+ ## Own the whole pipeline: your own load profiles
117
+
118
+ The pools above are pre-built, but you can build your own from real grid load — swap in data from a different ISO or a different time period and regenerate everything. The front of the pipeline is two functions:
119
+
120
+ ```python
121
+ # 1. Get a load profile. Auto-download real system load at 5-minute resolution (the finest each ISO
122
+ # publishes), or bring your own series.
123
+ S = fg.fetch_profile("nyiso", "2024-01-01", "2024-06-30") # NYISO — no account, no extra deps
124
+ S = fg.fetch_profile("caiso", "2024-01-01", "2024-06-30") # CAISO — needs fdia-graph[iso]
125
+ S = fg.fetch_profile("ercot", "2024-01-01", "2024-06-30") # ERCOT — needs fdia-graph[iso]
126
+ S = fg.load_profile(my_load_array) # or a CSV / a numpy array of load values
127
+
128
+ # 2. Turn the profile into a pool of AC operating states, then generate attacks onto it.
129
+ states = fg.generate_states("ieee118", S) # [T, N, 4] via pandapower power flow
130
+ fg.generate(118, name="ieee118_nyiso_2024", states=states, per_family=5000)
131
+ ds = fg.load("ieee118_nyiso_2024", split="train")
132
+ ```
133
+
134
+ `fetch_profile` returns a normalized per-timestep scaling vector; `generate_states` scales each bus's load by `clip(1 + k·S_t + noise)`, solves the power flow, and records the clean, bad-data-consistent state. Switching load source or time window is a one-line change, so the same grid can be re-generated under many demand regimes. Requires `fdia-graph[generate]` (`[iso]` too for CAISO/ERCOT).
135
+
136
+ ## Schema
137
+
138
+ One HDF5 file per system (`ml_only_ieee{14,118,300}.h5`), with `N` = buses and `E` = branches (lines + transformers). The **static graph** is stored once; everything else is **per record** (`T` records total). A record is one realistic measurement snapshot — benign or attacked.
139
+
140
+ **Static graph** (read once, shared by every record):
141
+
142
+ | Field | Shape | Dtype | Meaning |
143
+ |-------|-------|-------|---------|
144
+ | `edge_index` | `[2, E]` | int64 | `[from_bus; to_bus]` for each branch (lines first, then transformers) |
145
+ | `edge_reactance` | `[E]` | float32 | per-branch reactance (p.u.) — handy for physics-informed models |
146
+
147
+ **Per-record measurement graph** (indexed `0 … T-1`; access one via `ds[i]`, or a whole split via `ds.to_numpy()`):
148
+
149
+ | Field | Shape | Dtype | Meaning |
150
+ |-------|-------|-------|---------|
151
+ | `node_x` | `[N, 4]` | float32 | node features `[ \|V\| (p.u.), P_inj (MW), Q_inj (MVAr), θ (deg) ]` |
152
+ | `node_m` | `[N, 4]` | float32 | node **availability mask** (1 = that meter exists at that bus, else 0; masked entries are zeroed) |
153
+ | `edge_x` | `[E, 2]` | float32 | branch-flow features `[ P_from (MW), Q_from (MVAr) ]` |
154
+ | `edge_m` | `[E, 2]` | float32 | edge availability mask |
155
+ | `y` | `[N]` | float32 | **localization target** — per-bus label, `1` = bus is attacked, `0` = clean |
156
+ | `temporal_delta` | `[N, 2]` | float32 | *(v0.3+)* current-minus-previous-scan injection `[ΔP_inj, ΔQ_inj]` — the temporal feature for replay/ramp |
157
+ | `family` | scalar | int | `0` benign · `1` Aq · `2` Ad · `3` As · `4` Ar · `5` At · `6` Al |
158
+ | `stealthy` | scalar | int | `1` if the attack evades classical bad-data detection (Ao/ramp/LRA), else `0` |
159
+ | `split` | scalar | int | `0` train · `1` val · `2` test (60/20/20 chronological, sequence-boundary safe) |
160
+ | `seq_id` | scalar | int | ramp-sequence id (`≥0` groups the scans of one multi-timestep ramp); `-1` otherwise |
161
+ | `timestep` | scalar | int | source operating-point index (the benign snapshot the record was built from) |
162
+ | `gap` | scalar | int | `1` if this is a physics non-convergence NA row (`≈0%` in the shipped data) |
163
+
164
+ Sparsity is real: `node_m`/`edge_m` encode a redundancy of ≈ 2–3 (a realistic EMS regime), so a model must consume the masks — not every bus is metered, and PMU angles (`θ`) are sparse.
165
+
166
+ ## Project structure
167
+
168
+ ```
169
+ fdia-graph/
170
+ ├── pyproject.toml # package metadata, deps, optional extras ([torch], [pyg], [generate], [iso], [all])
171
+ ├── README.md # this file
172
+ ├── src/fdia_graph/
173
+ │ ├── __init__.py # public API: load(), generate(), fetch_profile/load_profile/generate_states ← start here
174
+ │ ├── registry.py # dataset version control: name → GitHub release + file; latest vs pinned; cache dir
175
+ │ ├── download.py # fetches release-asset .h5 shards (public, anonymous) → ~/.cache/fdia_graph
176
+ │ ├── dataset.py # FdiaGraph: torch Dataset over one .h5 (lazy slicing, split/family filters, exporters)
177
+ │ ├── profiles.py # front of the pipeline: ISO load download (CAISO/NYISO/ERCOT) → operating states
178
+ │ ├── generation.py # generate(): tunable-knob dataset creation → new .h5, registered by name
179
+ │ └── _core.py # generation engine: physics (Ybus/PTDF) + the 7 attack families
180
+ └── examples/
181
+ ├── quickstart.py # smallest load → train loop
182
+ ├── train_gnn.py # baseline GNN localizer
183
+ ├── train_arma.py # ARMA + physics-biased attention localizer (the strong model)
184
+ ├── train_tgnn.py # temporal-graph localizer
185
+ ├── hpo_arma.py # Optuna hyperparameter search (Boyaci-style space)
186
+ └── reproduce_report.ipynb # end-to-end notebook: generate → figures → train/test
187
+ ```
188
+
189
+ Read order to understand the codebase: `__init__.py` (the two entry points) → `registry.py`/`download.py` (how a name becomes a local `.h5`) → `dataset.py` (how that `.h5` becomes tensors) → `generate.py`/`_core.py` (how new datasets are made). Every module is commented line-by-line.
190
+
191
+ ## Evaluation protocol
192
+
193
+ 60/20/20 **chronological** split cut on sequence boundaries (ramp sequences never straddle a split — a random shuffle would leak them). Equal count per attack family. **Report per-attack-type node-F1**, not accuracy — the stealthy families (`Ao`/`ramp`/`LRA`) are the hard ones and accuracy hides them.
194
+
195
+ ## Citation
196
+
197
+ If you use this dataset, please cite the attack model and measurement-model sources:
198
+
199
+ - Yuan, Li & Ren, *Modeling load redistribution attacks in power systems*, IEEE Trans. Smart Grid 2(2), 2011. *(LRA attack)*
200
+ - Haghshenas, Hasnat & Naeini, *A Temporal Graph Neural Network for Cyber Attack Detection and Localization in Smart Grids*, IEEE ISGT 2023. *(ramp attack)*
201
+ - Zaman & Lin, *PING: Physics-Informed GNNs to Generalize FDIA Localization*, NAPS 2025. *(measurement model)*
202
+ - Boyaci et al., *Joint Detection and Localization of Stealth FDIA*, IEEE Trans. Smart Grid, 2022. *(protocol)*
203
+
204
+ ## License
205
+
206
+ The dataset (HDF5 shards and operating-point pools distributed through the GitHub releases) is
207
+ licensed under Creative Commons Attribution 4.0 International (CC BY 4.0). The source code in `src/`
208
+ and `examples/` is licensed under the MIT License. See the `LICENSE` file. The data is synthetic,
209
+ generated from public IEEE test cases with simulated measurements and simulated attacks, and must
210
+ not be used for operational decisions.
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "fdia-graph"
7
+ version = "0.5.0"
8
+ description = "Load and generate ML-only dangerous FDIA localization datasets (realistic measurement graphs) for power grids, PyTorch-ready."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Benjamin Myers", email = "benjamin.myers.465@my.csun.edu" }]
13
+ keywords = ["power-systems", "false-data-injection", "FDIA", "state-estimation", "graph-neural-network", "smart-grid", "dataset"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Intended Audience :: Science/Research",
18
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
19
+ ]
20
+ dependencies = [
21
+ "numpy>=1.21",
22
+ "h5py>=3.6",
23
+ "requests>=2.25",
24
+ "tqdm>=4.60",
25
+ ]
26
+
27
+ [project.optional-dependencies]
28
+ torch = ["torch>=1.12"]
29
+ pyg = ["torch>=1.12", "torch_geometric>=2.2"]
30
+ generate = ["pandapower>=2.10", "pandas>=1.3"] # dataset generation + load-profile ingestion (needs a power-flow engine)
31
+ iso = ["gridstatus>=0.25", "pandas>=1.3"] # auto-download CAISO/ERCOT load profiles (NYISO works without it)
32
+ all = ["torch>=1.12", "torch_geometric>=2.2", "pandapower>=2.10", "pandas>=1.3", "gridstatus>=0.25"]
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/myersben9/fdia-graph"
36
+ Repository = "https://github.com/myersben9/fdia-graph"
37
+
38
+ [tool.setuptools.packages.find]
39
+ where = ["src"]
40
+
41
+ [tool.setuptools.package-data]
42
+ fdia_graph = ["profiles/*.npz"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+