Skip to main content

fdia-graph

Load and generate ML-only dangerous FDIA localization datasets for power grids — PyTorch-ready, zero data plumbing.

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.

import fdia_graph as fg

ds = fg.load("ieee118", split="train")     # auto-downloads + caches the newest release
loader = ds.loader(batch_size=64)          # ready-to-train PyTorch DataLoader
for batch in loader:
    batch["node_x"], batch["edge_x"], batch["edge_index"], batch["y"], batch["family"], ...

Why this dataset

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.

Family Type Classical BDD
Aq stealthy load scaling: bounded per-bus rescale + AC re-solve — our contribution (cf. Boyaci Ao, Liu FDIA) evades (stealthy)
At temporal load surge, ramps up then back down (Haghshenas et al., IEEE ISGT 2023) evades (stealthy)
Al targeted masked-overload / load redistribution (Yuan, Li & Ren, IEEE T-SG 2011) evades (stealthy)
Ad random meter corruption caught
As meter scaling caught
Ar replay mostly caught

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.

Install

pip install fdia-graph              # loader (numpy + h5py)
pip install "fdia-graph[torch]"     # + PyTorch Dataset/DataLoader
pip install "fdia-graph[pyg]"       # + torch_geometric graph format
pip install "fdia-graph[generate]"  # + pandapower, to generate custom datasets from load profiles
pip install "fdia-graph[iso]"       # + gridstatus, to auto-download CAISO/ERCOT load profiles
pip install "fdia-graph[all]"       # everything

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.

Load

ds  = fg.load("ieee300", split="train")                 # 60/20/20 chronological split
val = fg.load("ieee300", split="val")
tst = fg.load("ieee300", split="test")

# family subsets and the unseen-attack generalization protocol
stealthy = fg.load("ieee118", split="test", families=["Aq", "At", "Al"])   # "Ao"/"ramp"/"LRA" still work as aliases
heldout  = fg.load("ieee118", split="train", heldout=True)   # As/Ar excluded from train (Boyaci et al. 2022)

Any framework you like

The .loader() streams records for training; these pull the whole split at once for analysis:

ds = fg.load("ieee14", split="test")
arrays = ds.to_numpy()      # dict of numpy arrays
tensors = ds.to_torch()     # dict of torch tensors  (float32 features, int64 labels)
tf_t   = ds.to_tf()         # dict of tf.Tensors      (needs tensorflow)
df     = ds.to_pandas()     # flat pandas DataFrame, one row per record

Dataset versioning

Datasets are GitHub releases, so your group version-controls them:

fg.load("ieee118")                      # newest release (default — everyone stays current)
fg.load("ieee118", release="v0.3.0")    # pin an exact version for a reproducible experiment

Generate a custom dataset (research knobs)

Turn any research knob and load the result by name — no data plumbing:

fg.generate("ieee118", name="high_intensity",
            per_family=5000,             # samples per attack family
            families=["Aq", "At", "Al"],
            attack_intensity=0.25,       # per-bus load-shift magnitude (± fraction)
            ramp_rate=0.003, ramp_len=80,
            n_benign=30000,
            redundancy={"pmu_frac": 0.3, "flow_frac": 0.95},
            split=(0.7, 0.15, 0.15),
            seed=7)

ds = fg.load("high_intensity", split="train")   # your custom dataset, ready to train

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.

N-1 line outages (topology shift)

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.

acc, rej = fg.line_outage_candidates(118, top_n=5)   # highest base-case flow, connectivity-screened
# rej explains every contingency that was skipped (islanding, non-convergent, ...)

from fdia_graph._core import FdiaGenerator
g = FdiaGenerator(118, seed=123, outage=acc[0]["line"])
states, ok = g.resolve_states(pool_X)                # re-solve the SAME load timestamps under this topology

fg.generate(118, name="ieee118_n1", states=states[ok], outage=acc[0]["line"])

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).

Own the whole pipeline: your own load profiles

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:

# 1. Get a load profile. Auto-download real system load at 5-minute resolution (the finest each ISO
#    publishes), or bring your own series.
S = fg.fetch_profile("nyiso", "2024-01-01", "2024-06-30")   # NYISO — no account, no extra deps
S = fg.fetch_profile("caiso", "2024-01-01", "2024-06-30")   # CAISO — needs fdia-graph[iso]
S = fg.fetch_profile("ercot", "2024-01-01", "2024-06-30")   # ERCOT — needs fdia-graph[iso]
S = fg.load_profile(my_load_array)                          # or a CSV / a numpy array of load values

# 2. Turn the profile into a pool of AC operating states, then generate attacks onto it.
states = fg.generate_states("ieee118", S)                   # [T, N, 4] via pandapower power flow
fg.generate(118, name="ieee118_nyiso_2024", states=states, per_family=5000)
ds = fg.load("ieee118_nyiso_2024", split="train")

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).

Schema

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.

Static graph (read once, shared by every record):

Field Shape Dtype Meaning
edge_index [2, E] int64 [from_bus; to_bus] for each branch (lines first, then transformers)
edge_reactance [E] float32 per-branch reactance (p.u.) — handy for physics-informed models

Per-record measurement graph (indexed 0 … T-1; access one via ds[i], or a whole split via ds.to_numpy()):

Field Shape Dtype Meaning
node_x [N, 4] float32 node features [ |V| (p.u.), P_inj (MW), Q_inj (MVAr), θ (deg) ]
node_m [N, 4] float32 node availability mask (1 = that meter exists at that bus, else 0; masked entries are zeroed)
edge_x [E, 2] float32 branch-flow features [ P_from (MW), Q_from (MVAr) ]
edge_m [E, 2] float32 edge availability mask
y [N] float32 localization target — per-bus label, 1 = bus is attacked, 0 = clean
temporal_delta [N, 2] float32 (v0.3+) current-minus-previous-scan injection [ΔP_inj, ΔQ_inj] — the temporal feature for replay/ramp
family scalar int 0 benign · 1 Aq · 2 Ad · 3 As · 4 Ar · 5 At · 6 Al
stealthy scalar int 1 if the attack evades classical bad-data detection (Ao/ramp/LRA), else 0
split scalar int 0 train · 1 val · 2 test (60/20/20 chronological, sequence-boundary safe)
seq_id scalar int ramp-sequence id (≥0 groups the scans of one multi-timestep ramp); -1 otherwise
timestep scalar int source operating-point index (the benign snapshot the record was built from)
gap scalar int 1 if this is a physics non-convergence NA row (≈0% in the shipped data)

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.

Project structure

fdia-graph/
├── pyproject.toml              # package metadata, deps, optional extras ([torch], [pyg], [generate], [iso], [all])
├── README.md                   # this file
├── src/fdia_graph/
│   ├── __init__.py             # public API: load(), generate(), fetch_profile/load_profile/generate_states  ← start here
│   ├── registry.py             # dataset version control: name → GitHub release + file; latest vs pinned; cache dir
│   ├── download.py             # fetches release-asset .h5 shards (public, anonymous) → ~/.cache/fdia_graph
│   ├── dataset.py              # FdiaGraph: torch Dataset over one .h5 (lazy slicing, split/family filters, exporters)
│   ├── profiles.py             # front of the pipeline: ISO load download (CAISO/NYISO/ERCOT) → operating states
│   ├── generation.py           # generate(): tunable-knob dataset creation → new .h5, registered by name
│   └── _core.py                # generation engine: physics (Ybus/PTDF) + the 7 attack families
└── examples/
    ├── quickstart.py           # smallest load → train loop
    ├── train_gnn.py            # baseline GNN localizer
    ├── train_arma.py           # ARMA + physics-biased attention localizer (the strong model)
    ├── train_tgnn.py           # temporal-graph localizer
    ├── hpo_arma.py             # Optuna hyperparameter search (Boyaci-style space)
    └── reproduce_report.ipynb  # end-to-end notebook: generate → figures → train/test

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.

Evaluation protocol

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.

Citation

If you use this dataset, please cite the attack model and measurement-model sources:

  • Yuan, Li & Ren, Modeling load redistribution attacks in power systems, IEEE Trans. Smart Grid 2(2), 2011. (LRA attack)
  • Haghshenas, Hasnat & Naeini, A Temporal Graph Neural Network for Cyber Attack Detection and Localization in Smart Grids, IEEE ISGT 2023. (ramp attack)
  • Zaman & Lin, PING: Physics-Informed GNNs to Generalize FDIA Localization, NAPS 2025. (measurement model)
  • Boyaci et al., Joint Detection and Localization of Stealth FDIA, IEEE Trans. Smart Grid, 2022. (protocol)

License

The dataset (HDF5 shards and operating-point pools distributed through the GitHub releases) is licensed under Creative Commons Attribution 4.0 International (CC BY 4.0). The source code in src/ and examples/ is licensed under the MIT License. See the LICENSE file. The data is synthetic, generated from public IEEE test cases with simulated measurements and simulated attacks, and must not be used for operational decisions.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fdia_graph-0.5.0.tar.gz (60.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fdia_graph-0.5.0-py3-none-any.whl (59.0 kB view details)

Uploaded Python 3

File details

Details for the file fdia_graph-0.5.0.tar.gz.

File metadata

  • Download URL: fdia_graph-0.5.0.tar.gz
  • Upload date:
  • Size: 60.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.12

File hashes

Hashes for fdia_graph-0.5.0.tar.gz
Algorithm Hash digest
SHA256 e1a1f9e8f637399f83b370af33bc575289532817e44c397f65f7dbb011958f12
MD5 b1bb9a5c5d5283b12d00391f88be182d
BLAKE2b-256 9cce89b592a7d731048548e21a430c82857cba048970f7df0926798c127bf2b3

See more details on using hashes here.

File details

Details for the file fdia_graph-0.5.0-py3-none-any.whl.

File metadata

  • Download URL: fdia_graph-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 59.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.12

File hashes

Hashes for fdia_graph-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ba9f70897b928b39720779c2a046c550596ca38f580d9fb17147d0da0a742b9c
MD5 e7b792328e5f105a3b33cbfc330977d1
BLAKE2b-256 10de74cb8d51fa60aed3ed2b909e87c44919745acde8b4a3149c73ab805c42e1

See more details on using hashes here.

Release history Release notifications | RSS feed

0.10.0

2 files

0.9.0

2 files

0.8.0

2 files

0.7.10

2 files

0.7.9

2 files

0.7.8

2 files

0.7.7

2 files

0.6.0

2 files

This release

0.5.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page