Skip to main content

fdia-graph

Stealthy FDIA localization datasets for power grids, PyTorch-ready in one line. Eight IEEE systems (14 / 30 / 57 / 89 / 118 / 145 / 200 / 300 buses), 72,000 records each.

import fdia_graph as fg

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

New here?

Read To learn
docs/ROADMAP.md which file does what
docs/reference/DATA_DICTIONARY.md what every array means
docs/reference/CONCEPTS_TO_CODE.md paper equations → functions
docs/reference/EXAMPLES.md runnable baselines, streams, dataset stats
docs/se/ · docs/localization/ the two analysis modules, with real results

Install

pip install fdia-graph              # loader
pip install "fdia-graph[torch]"     # + PyTorch DataLoader
pip install "fdia-graph[pyg]"       # + torch_geometric
pip install "fdia-graph[se]"        # + state estimation / residual localization (torch + pandapower)
pip install "fdia-graph[generate]"  # + pandapower, to generate custom data

Data is pinned per SDK version and cached in ~/.cache/fdia_graph. Pin a data version with fg.load(..., release="v0.7.2"); pip install --upgrade fdia-graph moves it forward.

Load

fg.load("ieee300", split="train")                              # 60/20/20 chronological split
fg.load("ieee118", split="test", families=["Aq","At","Al"])    # family subset
fg.load("ieee118", units="pu")                                 # per-unit + radians (default is physical)
  • Whole split at once: ds.to_numpy() / .to_torch() / .to_pandas().
  • Custom data: fg.generate(system, name, per_family=..., attack_intensity=..., ...) then fg.load(name).
  • Continuous timeline for LSTM/TGN: fg.load_stream(system).

Details for all three in docs/reference/EXAMPLES.md.

State estimation

from fdia_graph.se import WLS, SubspacePrior   # pip install "fdia-graph[se]"

train, test = fg.load("ieee118", split="train"), fg.load("ieee118", split="test")
est = SubspacePrior(rank_frac=0.5, reweight="huber", c=2.5).fit(train)
xhat = est.estimate(test)          # [n, 2N-1] = [theta rad (non-slack) | V pu (all buses)]
print(est.score(test))             # per-family angle/voltage MAE vs the clean truth

WLS, AdaptiveWeighting, ResidualRemoval, SubspacePrior share one solver and differ only in state space and weights. Results and a walkthrough: docs/se/.

Localization

from fdia_graph.localization import SwingThreshold   # numpy only

loc = SwingThreshold(fa_target=0.01).fit(train)     # per-bus thresholds from benign records only
flag = loc.localize(test)                           # [n, N] bool: which buses are called attacked
print(loc.score(test))                              # per-family node-F1, strict accuracy, DR next to FA

SwingThreshold, DeltaThreshold, ResidualLocalizer share one calibration and metric protocol and differ only in the per-bus score. BusCNN and BusMLP are the papers' learned localizers on the 14-dim per-bus feature vector (needs [torch]):

from fdia_graph.localization import BusCNN

zs = dict(families=[0, 1, 2])                        # papers' zero-shot protocol: As/Ar unseen in train
cnn = BusCNN().fit(fg.load("ieee118", split="train", **zs), val=fg.load("ieee118", split="val", **zs))
print(cnn.score(fg.load("ieee118", split="test", families=[0, 1, 2, 3, 4]))["all"]["macro_f1"])

Results: docs/localization/.

Data

Each record is a sparse measurement graph with N buses (nodes) and E branches (edges). Read a shape as "values per item": [N,4] = 4 numbers per bus, [E,8] = an 8-dim vector per branch.

field shape columns meaning
node_x [N,4] |V|, P_inj, Q_inj, theta bus meters (node_m [N,4] is the mask)
edge_x [E,2] P_from, Q_from branch flows (edge_m [E,2] is the mask)
edge_index [2,E] from_bus; to_bus connectivity
edge_attr [E,8] r, x, b, g, gs, bs, tap, shift static line physics (ds.edge_attr; in PyG format Data.edge_phys)
y [N] 1 attacked, 0 clean. Which buses
family scalar 0 benign, 1 Aq, 2 Ad, 3 As, 4 Ar, 5 At, 6 Al. Which attack (fg.FAMILIES); one family per record by construction, possibly on several buses
temporal_delta, swing [N,2] ΔP, ΔQ temporal features
clean [N,4] same as node_x noiseless truth, all buses. SE target (v0.7.2+)
edge_clean [E,2] same as edge_x noiseless true flows, unmetered branches zeroed
edge_clean_full [E,2] same as edge_x noiseless true flows on every branch, metered or not (computed from clean through yf on load; equals edge_clean where a flow meter exists)
slack scalar index of the reference bus (ds.slack, Data.slack in PyG)
ybus [N,N] complex full nodal admittance matrix in node_x bus order (ds.ybus, static per shard)
yf, yt [E,N] complex from-end / to-end branch admittance matrices (ds.yf, ds.yt); V[from] * conj(Yf @ V) is the from-end flow

In PyG format (format="pyg" and fg.pyg_stream) the flows are Data.edge_attr (alias Data.edge_x), the mask is edge_mask, and the static line physics are Data.edge_phys. Full reference: docs/reference/DATA_DICTIONARY.md.

Attacks

Three stealthy families that evade classical bad-data detection (BDD), plus three detectable ones as a contrast set.

family attack classical BDD
Aq stealthy load rescale + AC re-solve evades
At slow temporal load ramp evades
Al targeted load redistribution (hides overloads) evades
Ad / As / Ar meter corruption / scaling / replay caught

BDD statistic per family: the three stealthy families sit below the alarm line with benign, the three tampering families sit far above it

Bad-data statistic J relative to the alarm threshold, per family (Aq is labeled A_o here). Green families are indistinguishable from benign; red ones trip the alarm.

  • Every per-bus change stays in a plausibility band: 2% noise floor to 20% cap.
  • Meter error follows an accuracy-class model (per-meter bias plus per-scan jitter).
  • Report per-family node-F1 with the false-alarm rate, not accuracy: clean buses dominate.
  • A lightweight per-bus MLP reaches ~0.92 localization macro-F1 (see the examples page).

Citation

Cite the attack- and measurement-model sources:

  • Yuan, Li & Ren, Modeling load redistribution attacks in power systems, IEEE T-SG 2(2), 2011. (LRA)
  • Haghshenas, Hasnat & Naeini, A Temporal GNN for Cyber Attack Detection and Localization in Smart Grids, IEEE ISGT 2023. (ramp)
  • Zaman & Lin, PING: Physics-Informed GNNs to Generalize FDIA Localization, NAPS 2025. (measurement model)
  • Asprou, Kyriakides & Albu, Variable Weights in a WLS State Estimator, IEEE T-IM 63, 2014. (meter noise)
  • Boyaci et al., Joint Detection and Localization of Stealth FDIA, IEEE T-SG, 2022. (protocol)

License

Data under CC BY 4.0, code under MIT (see LICENSE). Synthetic, from public IEEE cases. Not for operational decisions.

Release files for fdia-graph 0.15.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for fdia-graph 0.15.0
File Size Uploaded
fdia_graph-0.15.0.tar.gz 94.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for fdia-graph 0.15.0
File Interpreter ABI Platform
fdia_graph-0.15.0-py3-none-any.whl Python 3 none any Details

Total release size: 192.7 kB

Release files / fdia_graph-0.15.0.tar.gz

Download URL fdia_graph-0.15.0.tar.gz
Size 94.6 kB
Tags Source
SHA-256 checksum
How to use checksums
efed215432989e17fccdf3e839280bba22c79a3891050beb4e72b5c648217182
BLAKE2b-256 checksum
How to use checksums
5ff11765bf8e44a3d82a8c41067eaeb1026baeda3a0b4551af60f2ad697f6c30
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 8, 2026.

Transparency log

Release files / fdia_graph-0.15.0-py3-none-any.whl

Download URL fdia_graph-0.15.0-py3-none-any.whl
Size 98.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c30b220fdf2224d4f79877024ac27c5c36192cd6fd57a9111900c2abc3ab52d7
BLAKE2b-256 checksum
How to use checksums
119019bfef754ac1853e7c9cdef26aecc27d2554c48502389e431acf00667f5a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 8, 2026.

Transparency log

Release history Release notifications | RSS feed

0.20.0

2 release files

0.19.0

2 release files

0.18.0

2 release files

0.17.0

2 release files

0.16.0

2 release files

This release

0.15.0 This release

2 release files

0.9.0

2 release files

0.8.0

2 release files

0.7.10

2 release files

0.7.9

2 release files

0.7.8

2 release files

0.7.7

2 release files

0.6.0

2 release files

0.5.0

2 release 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