Skip to content

Concepts to code

Paper ideas → the function that implements them. Pair with DATA_DICTIONARY.md. Paths are under src/fdia_graph/.

One record, start to finish

one record: the operating state gives the clean layer and the benign scan; Aq, At, Al and Am re-solve a local false state whose attack vector is added to the benign scan; Ad, As and Ar corrupt readings in place; every path ends in the record

Every attack is built in engine.attacks, the generator's AttackMixin: a local false state (engine.attacks.false_state) designed per episode by the family's designer (RampDesigner for At, OverloadDesigner for Am, engine.attacks.episodes) on a support the search or [WU26]'s row reduction chooses (SearchSupport, RrefSupport); engine.records.attack_frame adds the attack vector to the benign scan.

Modules

The paper's math lives in engine/. generation/ drives it in stages, timeline.py orchestrates them, profiles.py turns load series into operating points; se/ and localization/ analyze the timelines; the rest load and serve data. The table is generated from each module's docstring (tools/equation_map.py); DECISIONS.md and the "[WU26] equations to code" section of FORMULAS.md map the paper to the code.

module what it holds
fdia_graph fdia-graph — load & generate ML-only dangerous FDIA localization datasets (realistic measurement graphs).
_moved Names that moved to another module and stay importable from the old one for one minor release.
dataset FdiaGraph: a PyTorch-ready dataset over one HDF5 file, a timeline (0.18+, one row per frame in time order, kind="timeline") or a record shard (the v0.7.2 release).
dataset.base The loader's shared state and the constants its concerns read.
dataset.export A whole split at once: summary, and the arrays/tensors/frames of every kept record.
dataset.graph The static graph of a shard: connectivity and the per-branch and per-bus physics, as tensors.
dataset.physics The admittance matrices built from the static graph, and the clean power flow on every branch derived from the clean state through them.
dataset.records One record at a time: unit conversion, __getitem__ in dict/PyG form, collate, and a ready DataLoader.
dataset.sequence The timeline as sequences: sliding windows over a time-ordered view, and the episode table.
download Fetch built-in shards from the GitHub Release and cache them under ~/.cache/fdia_graph.
engine The generation engine — the math/physics/theory half of the SDK, behind fg.generate().
engine.attacks Every attack the generator builds, behind one mixin: AttackMixin, mixed into FdiaGenerator.
engine.attacks.area The attacker's area: the subnetwork a stealthy attack re-solves, and the boundary it holds true.
engine.attacks.episodes What one episode attacks: its design, drawn at onset by the family's designer.
engine.attacks.false_state The local false state of a stealthy attack: the attacker's area re-solved under false loads with the boundary held true, checked against the operating limits, and the attack vector it writes.
engine.attacks.minimize The cheapest support of an attack window [WU26 eq. 12].
engine.attacks.overload Am, the multi-snapshot overload attack of [WU26]: the reported flows of its target branches (two by default, as the paper's case studies overload two lines at once, or one; OverloadSettings.n_lines, [D17]) driven to their ratings over the window, with the fewest devices tampered.
engine.attacks.rref [WU26]'s own construction of the attack's support: row reduction of the transposed attack-area Jacobian with column exchanges.
engine.attacks.stealthy The stealthy ramp At: one scan at a time, a local false state added to the true scan.
engine.base Shared state contract for FdiaGenerator's mixins — attribute + cross-method annotations only.
engine.core Dataset generation engine (attack simulation + realistic measurement emission).
engine.measurement Measurement emission: turn a grid state into meter readings (the measurement function h(x)).
engine.physics A scan's load and generation, read from its stored state.
engine.pp_types The pandapower objects the engine reads, as types.
engine.records One scan of the timeline writer: a benign scan, or an attacked At scan built by engine.attacks.
errors Every error the package raises on purpose, in one place.
federated Federated training over K clients (utilities) that never pool their records [FED26].
federated.aggregate Averaging torch state dicts across clients with formulas.federated.fedavg [MCM17].
federated.localizer Federated per-bus localizers: the paper's BusMLP and BusCNN trained by FedAvg over K clients [MCM17], [FED26], with records never leaving a client.
federated.partition The split of a system's buses into K clients (utilities) for federated training [FED26].
federated.se Federated fitting of the state estimator's learned part [EST26], [FED26].
formulas The mathematics of the package as named, pure functions: numpy in, numpy out, no files, no dataset objects, no torch. Each function carries its equation and a source key from docs/reference/REFERENCES.md; docs/reference/FORMULAS.md is the catalogue.
formulas.attacks Attack shapes defined by the dataset [DAT26] that are pure functions of their parameters.
formulas.estimation Weighted least squares state estimation and its robust variants, one function per equation [SCH70], [HAN75], [HUB64], [EST26].
formulas.federated Statistics that combine across federated clients without moving their records [FED26].
formulas.linalg Numerical linear algebra the estimators lean on: a guarded inverse of a normal matrix, its condition number without an eigen-decomposition, and per-record normal matrices in sub-batches.
formulas.metrics Per-bus localization metrics from confusion counts, so they can be summed across clients or thresholds before any ratio is taken [KEC25].
formulas.network The AC network model: branch admittances, bus injections and branch flows [AE04, ch. 2], [MP19].
formulas.noise The meter error model: accuracy-class standard deviations split into a constant per-meter bias and a per-scan jitter [ASP14], our split.
formulas.projection Projections through the measurement Jacobian: the explained and unexplained parts of a measurement change, leverage, the weak directions, and the meter-to-bus aggregation of the Jacobian-informed features [JAC26], [HAN75].
formulas.temporal The temporal features of a scan [FED26]: the one-step injection change and its z-score against the bus's typical recent change, and the recent-change scale itself.
formulas.trust Trusted-meter selection against stealthy attacks [WU26]: the attack subspace a secured set leaves open, the cost of the cheapest attack in it, and the greedy selection that closes it.
generation generate(system, name, **knobs) — build a custom dataset and register it as name.
generation.design The design stage of a timeline: the overload Am episodes of a split, designed before it is walked.
generation.emit The emit stage of a timeline: every frame in time order, benign or under its episode's design.
generation.plan The plan stage of a timeline: where its splits are cut and where each split's episodes go.
generation.write The write stage of a timeline: its datasets, columns, episodes and attributes in the HDF5 file.
localization Per-bus FDIA localization on fdia-graph datasets, sklearn style.
localization.base Per-bus FDIA localization on fdia-graph datasets — the shared machinery behind every method class.
localization.learned Learned per-bus localizers: the graph-free encoders that gave the best localization numbers in the federated localization paper, on that paper's 14-dim per-bus feature vector.
localization.methods The localizer method classes. Each changes exactly one thing about LocalizerBase — the per-bus score — so a difference between two arms is a difference between detection signals, not between calibration or metrics code.
models Every data model of the package in one place.
models.assets How files are found: where a dataset comes from, where its bytes are fetched from, and one line of the N-1 candidate list.
models.base Bundle: a typed record that still IS the dict it used to be.
models.choices Every argument that takes one value from a fixed set, in one place.
models.config The settings every consumer accepts, one model each, checked when built (models.validation).
models.data What a user gets back: one record, a batch, a whole split, a summary, a stream, the episode table, and the per-record truth an estimator is scored against. Each is a Bundle, so it is still the dict it always was.
models.errors The named errors of conditions only the data reveals, beside ConfigError so a model can raise one (Validated.error). fdia_graph.errors re-exports every one.
models.federated What the federated layer passes around: how the buses split into clients.
models.fields The field groups the data bundles share.
models.frames What the generator passes around per scan: a measurement scan, the emitted frame with its labels, the run's attack knobs, the design of an At ramp and of an Am overload episode, and what the fewest-tamper search returns.
models.grid The static description of a system: the per-branch pi model and the admittance matrices built from it, which meters exist, the constant meter bias, and the N-1 contingency a generator was built with. Nothing here changes from one scan to the next.
models.inputs What the formulas and the parsers accept, one model each, checked when built (models.validation).
models.results The models of a results table: the metric registry, a Record (one measured value with the keys that say what it is) and a Provenance (which run made it), plus the parsers that read loose input (a nested score report, a filter value, a run's settings) by its type. Every check lives here, per the validation rule, so a store never holds an unknown metric, a non-finite value or a malformed key; fdia_graph.results builds these models and never checks its input itself.
models.scores The result tables: an estimator's error pair per record class, a localizer's metric rows per class, and the Jacobian feature outputs. Each is a Bundle, indexable by family name as before.
models.training What the learned localizers pass between their fitting pieces (a centralized fit and a federated client share them).
models.validation The one place inputs are checked (CONTRIBUTING.md, "an input is checked in one place").
profiles Load-profile ingestion + operating-state generation — the FRONT of the pipeline.
ratings Branch thermal ratings for the overload attack of [WU26] (eqs. 24-25), stored with the package.
registry Dataset registry: built-in (downloadable) datasets + locally generated ones.
results Measured results as data: typed records with their provenance, one tidy store, and the renderers that put the numbers into docs and figures, so no measured number is typed by hand.
results.docs Measured numbers in markdown come from the store, never from a keyboard.
results.render Turning records into what a reader sees: a formatted cell, a markdown table, a pivot of records by two keys, and the CSV sidecar a figure is drawn from. Nothing here measures or stores anything.
results.run How a harness records what it measured: open a Run, add values, and the run writes them to the store with its provenance when it closes.
results.store The results store: one tidy CSV per experiment plus one table of runs, in a folder kept in git.
se State estimation on fdia-graph datasets, sklearn style.
se.base State estimation on fdia-graph datasets — the shared machinery behind every method class.
se.jacobian Jacobian-informed features (Abdulin & Narimani): the measurement Jacobian as a physics transform of the scan-to-scan measurement change, not as raw model input.
se.methods The estimator method classes. Each changes exactly one thing about SEBase, so a difference between two arms is a difference between estimators rather than between implementations.
streams The deprecated stream entry points, to be removed in a future release: generate_stream writes a timeline file through fdia_graph.timeline and returns it as the stream dict, load_stream reads the v0.7.2 stream files, and windows slides over a stream dict. The timeline file is the dataset now: fg.generate writes it and fg.load(name, order="time") reads it, with ds.windows. load_stream and windows stay for reading the v0.7.x stream files, which a timeline loader does not open.
timeline One continuous attacked timeline per system, written as one HDF5 file.
trust Which meters to trust so that stealthy attacks stop being stealthy [WU26], sklearn style.
trust.base The shared machinery of the trusted-meter selectors: the measurement Jacobian at the benign mean (from a WLS estimator of fdia_graph.se), the attack-cost kernel, and the scoring that pins the secured meters and asks the residual test whether the attack shows.
trust.defense [WU26]'s trusted-PMU configuration as a Markov decision process (Sec. IV-D2, Fig. 1, Algorithm 1), on the overload attack of one window (docs/plans/WU_DEFENSE_PLAN.md, PR B).
trust.dqn The trusted-meter selection as a Markov decision process solved by a deep Q-network (the learned selection of [WU26]).
trust.secured A copy of a timeline with a set of meters secured: the attacker locked out of them, so on every frame their observed reading is the benign one, and the stored temporal features recomputed from the pinned observed frames (timeline.write_temporal_layers). The estimators and the localizers then read the copy like any timeline, which is how a trusted-meter selection is measured on state estimation and localization rather than on the residual test alone (TrustSelector.score). The copy is rewritten in blocks of frames, so a 72,000-frame timeline of any ladder system fits in bounded memory.
trust.solution1 [WU26]'s Solution 1: the trusted PMUs by row reduction of the measurement matrix (Sec. IV-D1, p. 657), on the trusted-PMU MDP of one overload window (trust.WuDefenseEnv; docs/plans/WU_DEFENSE_PLAN.md, PR C).
trust.wu26 [WU26]'s attack and defense as the paper states them (docs/wu26/README.md, "Faithful reproduction"): the meter plan, the multi-snapshot attack and the incremental trusted-PMU defense, on a pandapower case.
trust.wu26_defense [WU26]'s trusted-PMU defense on the faithful attack (trust.wu26): the MDP (Sec. IV-D2, Fig. 1, Algorithm 1) and Solution 1's row reduction (Sec. IV-D1, p. 657).
trust.wu_dqn [WU26]'s Solution 2: the trusted PMUs by a deep Q-network trained with Algorithm 1 (Sec. IV-D2, p. 658) on trusted-PMU MDPs (trust.WuDefenseEnv; docs/plans/WU_DEFENSE_PLAN.md, PR D).
engine/ file formula it implements
core.py FdiaGenerator: grid + noise setup (__init__), attack targeting; composes the measurement, physics and attack mixins
measurement.py emit_from_state (the measurement function h(x)), clean_flows_from_states, currents_from_states
physics.py true_load, scan_generation: a scan's load and generation from its stored state
attacks/ the false states, the fewest-tamper search, the At ramp and the overload Am

One column order everywhere: the operating-state pool, node_x, and clean are all [|V|, P_inj, Q_inj, theta]. Pools saved before 0.12 were P-first; generation.as_v_first detects and converts them on load.

State estimation

concept code
WLS x̂ = argmin (z−h(x))ᵀW(z−h(x)) se/base.py SEBase._w_solve (chord-Newton); h(x) is engine/measurement.emit_from_state
Robust reweighting (Huber), residual removal, subspace prior se/methods.py: one class per arm, each overrides one hook
Bad-data test r_i=(z_i−h_i)/σ_i, J=Σr_i² σ_i is the RMS of the benign residuals at the training truth (se/base.py SEBase.fit); residuals in se/base.py SEBase._nres. The stealthy flag marks the families that evade it by construction (Aq/At/Al/Am, schema.STEALTHY_FAMILIES)
Noise model FdiaGenerator.SD (accuracy class): reading = true + per-meter bias + per-scan jitter. The estimator does not read it; it calibrates σ_i from data
Meter model (hybrid SCADA and PMU, the plan's D10) MeasurementMixin.meter_masks (the angle at the PMU buses only), current_mask and _emit_currents (the PMU branch currents I_f = Y_f V, I_t = Y_t V, formulas.network.branch_currents, noise formulas.noise.current_sigma); chosen by generate_timeline(..., redundancy={"meter_model": ...}) through models.MeterSettings
PMU pseudo-measurements V_f = (I_n − y_nn V_n)/y_nf [WU26, eq. (3)] formulas.estimation.pmu_pseudo_links, pmu_pseudo_voltages (with the propagated variance), used by SEBase(pmu_pseudo=True) in _build_pseudo and _z_of

Walkthrough: ../guides/state_estimation.md. Results: ../se/README.md.

Attack families

family paper build code
At A_t slow ramp, 0.2 percent per frame, a local false state per frame engine/attacks/episodes.RampDesigner + ramp_step
Am A_m the overload attack of [WU26]: a line's reported flow driven to its rating over the window, the fewest devices tampered engine/attacks/episodes.OverloadDesigner + overload.overload_step
  • At and Am are local false states: the buses around the attack are re-solved with the boundary voltages held true, inside the case's voltage limits and the region's generator limits, and the attack vector h(x') − h(x) is added to the benign scan. The residual test flags them at the benign rate by construction. Both satisfy equations (13)-(23) of [WU26] (the SCADA measurements, the PMU voltage magnitudes, angles and branch currents, and the operating limits). Am also drives a line's reported flow to its rating, eqs. (24)-(25) (by default 1.25 times its peak pool flow, D15), on the support that tampers the fewest devices, eq. (12); At is held on that support when min_tamper=True (the default), else on the region within hops (docs/plans/WU_MSFDIA_PLAN.md). The single-snapshot families of older releases (Aq, Al, and the in-place Ad, As, Ar) are read-only: fdia-graph 0.20 generates them.
  • With min_tamper=True an At episode is held on the support that tampers the fewest devices over the episode, the objective of [WU26, eq. 12] (engine/attacks/minimize.MinimizeMixin.min_tamper, called by engine/attacks/episodes.RampDesigner).
  • The At ramp moves in per-frame steps under the noise floor, by design.

Temporal feature

field meaning built in
temporal_delta scan-to-scan [ΔP, ΔQ] of the observed frames generation.write.write_temporal_layers (formulas.temporal.temporal_delta)
swing temporal_delta as a z-score of the recent observed change generation.write.write_temporal_layers (formulas.temporal.swing_zscore, scale from recent_change_scale)

Both layers are written after the walk from the observed injections only, so a detector reads what an operator sees.

Any above-noise attack spikes the swing, so localization is per-bus and needs no graph. The slow ramp At stays inside the swing, so it is the open case.

The papers' per-bus feature vector and localizers

concept code
14-dim per-bus vector: [|V|,P,Q,θ] + meter mask + partial KCL residual + temporal_delta + swing localization/learned.py full14, kcl_residual
standardize all 14 channels on train (sd floored at 1e-3) LearnedLocalizer._fit_stats
1-D CNN across the bus axis (best localizer) BusCNN
per-bus MLP (lightweight arm) BusMLP
validation-best global threshold on a 0.05..0.95 grid LearnedLocalizer.tune_threshold

Metrics

metric definition code
node precision / recall / F1 micro over every (record, bus) pair localization/base.py LocalizerBase.score
per-sample macro-F1 F1 per record, averaged same
strict localization accuracy predicted attacked set equals the truth exactly same
DR with FA detection rate, always reported next to the benign false-alarm rate same
localization macro-F1 per-bus F1 averaged over the active buses, those attacked somewhere in the records scored (the papers' headline) LocalizerBase.score(...)["all"]["macro_f1"]; also EXAMPLES.md macro_f1