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=..., ...)thenfg.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. 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.
node_x [N,4] = [ |V|, P_inj, Q_inj, theta ] bus meters node_m [N,4] = mask
edge_x [E,2] = [ P_from, Q_from ] branch flow edge_m [E,2] = 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)
y [N] = attacked (1) / clean (0) localization target: WHICH buses
family = 0 benign, 1 Aq, 2 Ad, 3 As, 4 Ar, 5 At, 6 Al per record: WHICH attack (fg.FAMILIES)
swing [N,2], temporal_delta [N,2] temporal features
clean [N,4] = [ |V|, P_inj, Q_inj, theta ] noiseless truth, all buses (SE target, v0.7.2+)
edge_clean [E,2] = [ P_from, Q_from ] noiseless true flows (unmetered branches zeroed)
In format="pyg" the flows are Data.edge_attr (alias Data.edge_x) and the mask is edge_mask.
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 |
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.10.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| fdia_graph-0.10.1.tar.gz | 74.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| fdia_graph-0.10.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 156.5 kB
Release files / fdia_graph-0.10.1.tar.gz
| Download URL | fdia_graph-0.10.1.tar.gz |
|---|---|
| Size | 74.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
171344ba8cdadcdd567290c6f84dc590467f5892aac6d849b2ddc77fdfd0ad7d
|
|
BLAKE2b-256 checksum How to use checksums |
be1c08ec1e50feb1ec6d88ffcfc821030ca9c4e3210afa182b832d253b917e87
|
| 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 2, 2026.
Transparency logRelease files / fdia_graph-0.10.1-py3-none-any.whl
| Download URL | fdia_graph-0.10.1-py3-none-any.whl |
|---|---|
| Size | 81.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
27274189e5168c0a158776fffa4a554c85be27e23e7d9d2152b7d890fd2f5dcb
|
|
BLAKE2b-256 checksum How to use checksums |
db91eab91a31566a833c946dc54e868cf01493861d4fc995812a16879788cd7b
|
| 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 2, 2026.
Transparency log