Skip to content

Plan: the [WU26] attack as the optimization it is, and one home for attack generation

Status: accepted; decisions D1 to D17 taken (section 9).

[WU26] defines its multi-snapshot attack (MS-FDIA) as an optimization: eq. (12) minimizes the number of tampered measurements subject to the AC measurement model (13)-(20), the operating limits (21)-(23) and a line-overload goal reached over several snapshots (24)-(25). The attack is the solution of that problem.

The generator does not solve it today. It builds local false states that satisfy (13)-(18) and (21)-(23), with targets drawn at random and no minimization, and it has no overload goal. The docs nevertheless cite [WU26] as the source of the stealthy families. This plan closes the gap: the stealthy families are generated by solving the paper's problem. It also moves every piece of attack generation into the engine's attack mixin, so a reader finds all of it in one place.

1. What the generator does today, equation by equation

Paths are as of main before step 2 moved the attack code into engine/attacks/.

[WU26] today where
(12) minimize the number of tampered measurements not solved. Targets are drawn at random and the attack area is the buses within hops branches of them, never the slack, grown over any zero-injection bus on its boundary and shrunk in reach until a fixed-voltage boundary exists (local_region); the tampered meters are the metered channels whose attack-vector entry moves by more than 1e-7 (_changed_meters), whatever that count is. engine/physics.py: local_region, engine/records.py: _stealthy_frame
(13)-(16) SCADA injections and flows consistent with the false state satisfied exactly: the false state is a local AC solution, and the attack vector is h(x_false) - h(x_true) formulas/network.py: local_ac_solve, engine/records.py: _attack_vector
(17)-(18) PMU voltage magnitude and angle satisfied by the same attack vector; an angle was emitted at every voltmeter bus, which D10 moves to the PMU buses only _attack_vector
(19)-(20) PMU branch current phasors not modeled: the meter plan has no current-phasor channels (modeled by the hybrid meter model of D10 since step 4) engine/core.py: _meter_plan
(21) voltage limits, (22)-(23) generator limits checked; a false state outside them is halved or rejected formulas/attacks.py: within_limits
(24)-(25) a line's flow driven to its limit over the window not modeled. Al steers a line the other way (a real overload reads lighter), and the IEEE cases carry no usable line ratings (D3) engine/attacks.py: lra_delta
the window t = κ ... κ+T At and Am hold one attack area and target set over an episode and solve each frame on its own timeline.py: _ramp_episode, _am_episode

The random draw and the missing objective were deliberate simplifications when the local false states were introduced (#98). They were never stated next to the citation. Step 1 of the sequence fixes the wording before any code changes.

2. The problem, in the package's terms

For one episode: an attack area A (a connected subnetwork), a target line l with limit S_max, snapshots t = κ ... κ+T, and the true states x_t and scans z_t from the timeline.

  • Variables: the false state x^a_t of each snapshot: |V| and θ of buses in A; every other bus keeps its true voltage, as today.
  • Attack vector: a_t = h(x^a_t) - h(x_t) on the metered channels, SCADA and PMU. Constraints (13)-(18) hold by construction, since they state that the tampered readings are the readings of the false state; (19)-(20) hold the same way once the meter plan has current-phasor channels (D4).
  • Objective (12): the number of devices tampered over the window. Following the paper's experiments section, a change smaller than the meter's noise does not count:

    minimize | { d : some channel m of device d has |a_{m,t}| > σ_{m,t} in some snapshot t } |

A device is what the paper counts: one SCADA terminal per bus, holding that bus's injection meters and the flow meters of the branches metered at that bus, and one PMU per bus, holding its voltage magnitude, angle and branch-current channels. A device counts once however many of its channels move; the number of channels moved is recorded next to it for analysis.

σ_{m,t} is the meter's rated accuracy for that channel in that snapshot, the accuracy-class standard deviation the package's measured calibration already uses (D7): a change within a meter's rated accuracy cannot be told from noise. It is expressed in the stored scan's units, as a_{m,t} is: |V| in per unit, P and Q in MW and MVAr, and θ in degrees (the accuracy class and the emitter's internal angle noise are in radians, converted with np.degrees). The emitter keeps drawing its per-scan jitter from its own rule (absolute for |V| and θ, relative plus POWER_NOISE_FLOOR_MW for P and Q), so the data is unchanged; the two are separate named functions. The current-phasor channels of D4 (the real and imaginary part of each branch current at a PMU bus, per unit on the base current) get the same kind of rule: noise relative to the current's magnitude at the PMU accuracy class of IEEE C37.118.1 (1% total vector error, taken as three standard deviations) plus a small floor, added to the same function, so the objective and the stealth bound are defined on every channel the attack can touch.

The paper writes ||Σ_t (z^a_t - h_A(x^a_t))||_0 and calls it "the number of nonzero entries in the attack vector"; its results count compromised devices over the window, which is the set above, grouped into devices as stated. Decision D1 confirms this reading. - Constraints: (21)-(23) on every snapshot, as today, and the goal (24)-(25): the flow magnitude on line l that the tampered measurements carry before noise, the noiseless reading h(x^a_t) of the false state, rides on the true flow and reaches S_max by κ+T (the schedule of D9). Stealth bound: the attack's own increment on every channel is at most its noise scale, |a_{m,t} - a_{m,t-1}| <= σ_{m,t}, the rated accuracy for At (D7) and [WU26]'s own noise for Am (D8), onset included: a_{m,κ-1} is the attack vector of the frame before the episode, zero when that frame is benign and that frame's own vector when an episode ends there (episodes may be adjacent, the placement being uniformly random). This is a rated-accuracy criterion: each step stays within what the meter's accuracy class allows, the standard an operator holds a meter to. It is not a guarantee against a temporal detector tuned to the per-scan jitter, which is smaller than the rated accuracy, and how the swing and delta features fare against it is measured, not assumed. The residual test sees noise only at any magnitude, since the tampered readings are those of an AC false state. The target line's flow is metered: the goal is defined on what the operator sees, so only lines whose flow is metered are eligible targets.

3. How it is solved

The paper does not name its solver; it reports 1.1 to 7.5 s per snapshot. The plan solves the problem exactly where it is small and by a bounded search where it is not, and records which.

The structure. Every tampered reading is a reading of the false state, so a channel can change only when a bus it depends on moves: the injections at the moved buses and their neighbours, the flows on branches touching them, and the PMU readings there. The candidates are therefore supports S, the buses of A whose voltage the false state moves, and a support's cost is the number of devices with a channel moved beyond its noise (σ_{m,t}), counted exactly once the false state is solved.

The false state of one support. Two goals, two solves, both with every voltage outside S held at its true value:

  • A load goal (At): the targeted loads take their new values and every other bus of S keeps its true injection. That is 2|S| equations in the 2|S| unknowns |V| and θ of S, today's local_ac_solve.
  • A flow goal (Am): only the attackable loads of S are free, as the attacker pretends them; every other bus of S keeps its true injection (a zero-injection bus stays at zero, a generator bus at its true dispatch), as in the load goal. The unknowns are |V| and θ of S, the equations are the fixed injections of S's non-load buses and the flow magnitude on line l at its target value, and the solve takes the smallest voltage change that meets them (least-norm Gauss-Newton, the false loads read off the result). A support can change the flow only when it holds an end bus of l and at least one attackable load (the only free injections). With fewer equations than unknowns a support has many feasible states, and the least-norm one need not cross the fewest noise thresholds, so for Am the optimum claimed is over supports each solved by this rule, not over every false state. Lowering the device count inside a support (reweighting the step toward channels already over noise) is a later refinement, with no claim until it is measured.

Both are checked against (21)-(23). The flow magnitude is S_l = sqrt(P_l^2 + Q_l^2) in MVA from the flow channels (MW and MVAr), compared with rate_a in MVA, or both divided by the base MVA in per unit.

The search.

  1. Candidates are connected supports that can reach the goal (the targeted load buses for a load goal, for a flow goal an end bus of l and at least one attackable load, since a support of generator and zero-injection buses alone has no free injection), under the area rules of today: never the slack, and a zero-injection bus on a support's boundary is taken in, since it cannot absorb the change.
  2. The only lower bound used for pruning is one that holds for every remaining support: a device counts only when the goal forces one of its channels above that channel's noise. For a flow goal the required change δ of the magnitude on l can split between P and Q, so at least one of them moves by δ/√2 and the metering device is forced only when δ/√2 exceeds the larger of the two σ. For a load goal a targeted bus's device is forced only when the designed step exceeds its injection channel's σ. When nothing is forced the bound is zero and nothing is pruned.
  3. On IEEE-14 and small areas every candidate is solved (exhaustive), and the cheapest feasible one is the optimum among the candidates whose solve converged. A solve that fails is not proof of infeasibility (Newton can stall where a solution exists), so each window records how many candidates failed, and the optimum is marked proven only when no candidate that could have beaten it failed. A test compares the result with a brute-force enumeration on the same terms.
  4. On IEEE-118 and 300 the search is best-first with a node budget and prunes only with the valid bound above. The file records per episode whether the enumeration finished (optimal over the area) or the budget ran out (the best found, no claim).

Checks on the solver. The brute-force test on IEEE-14 areas, and a continuous cross-check, iteratively reweighted ℓ1 on the local AC equations, that may never beat the search.

Rejected: a mixed-integer nonlinear program (binary per meter, big-M, nonconvex AC) handed to a general solver. It needs a heavy optional dependency, gives no better guarantee than exhaustive search on the small cases, and is slower on the large ones. It can serve as an offline validation if a reviewer asks.

The window. D1 counts the devices tampered over the whole window, so a candidate is one support held for the episode and solved at every snapshot: snapshot t meets its share of the goal (for Am, S_{l,t} = S_true_{l,t} + (t - κ)/T · (S_max - S_true_{l,κ+T}) (D9), for At the ramp's multiplier at t) and keeps the attack's increment on each channel under σ_{m,t}, onset included. Its cost is the union over the window of the devices with a channel moved beyond noise. The optimum claimed is over supports held for the window and no more: device cost is not monotone in the support, so nothing is claimed about supports that change between snapshots. When the budget runs out on 118 and 300, the fallback solves snapshot by snapshot from the previous support, and the file records which episodes used it.

4. What changes for each family

New generation is multi-snapshot only (D6). The attack engine is organised around the two multi-snapshot families, and both are generated by the minimizer (D2):

  • Am is the MS-FDIA of [WU26]: the overload goal (24)-(25) on a metered target line over the window, the fewest devices tampered, SCADA and PMU (current phasors included, D4). Its meters are the hybrid model of D10: angles at the PMUs only, and the PMUs' branch currents.
  • At, the slow ramp, keeps its goal (a load ramp: rise, hold, return) and is generated by the same minimizer: one support held for the episode, the fewest devices over the window, each snapshot reaching the ramp's load change under the stealth bound (At's alone, D11).
  • The single-snapshot families are deprecated for new generation: Aq and Al (stealthy, one frame) and Ad, As, Ar (in-place corruption, one frame). Asking the generator for one warns (DeprecationWarning) for one minor release and then is refused. Released files that hold them keep loading, with their family codes and labels unchanged, so every published result stays reproducible.

5. One home for attack generation

The engine's attack mixin is meant to hold how attacks are built. Today the logic is spread over five modules:

module attack logic in it today
engine/attacks.py (AttackMixin) the in-place corruptions; the Al target line and redistribution
engine/records.py every frame builder (_stealthy_frame, _resolve_frame, _lra_frame, _am_frame, _corrupt_frame), the false state and its limit check, the attack vector, the tamper set, the halving retries
engine/physics.py (PhysicsMixin) the attacker's area (local_region, zero-injection growth) and the local false-state solve (solve_local)
timeline.py each episode's attack design: ramp draws, target picking, the Am shape, its held redistribution and its peak check
formulas/attacks.py, formulas/network.py the pure formulas (limits, ramp profile, subnetwork, local AC solve)

Target layout:

  • engine/attacks/, one package whose modules compose the one AttackMixin: area.py (the attack area), false_state.py (the local solve, the limits, the attack vector, the tamper set), minimize.py (the ℓ0 search), stealthy.py (the Aq/At/Al/Am frames and episode designs), corrupt.py (Ad/As/Ar). A reader looking for how an attack is made opens this package.
  • engine/records.py keeps the benign frame and the replay memory, and asks the mixin for an attacked frame.
  • timeline.py keeps when and where: the schedule, the episode placement, the storage. What an episode's attack is comes from the mixin.
  • formulas/ keeps the pure formulas, per the kernel rule; the mixin calls them.

This move comes first and changes no number: the strict frozen suite stays bit-exact, so the minimizer lands in code already organised for it. Every name that moves stays importable from its old module for one minor release, as a one-line alias that emits a DeprecationWarning (private names included, per the compatibility rule), and every internal caller and test moves to the new path in the same change.

6. How we know it is right

  • The paper's IEEE-14 scenarios: overloads on lines 3-4 and 6-11, and on 1-2 and 4-5. [WU26] reports tampered SCADA devices at buses 3, 4, 5, 6 and 11 with PMUs at 4 and 6, and a largest change near 0.22 pu, for the first. Our loads and noise differ, so the comparison is of the device set and magnitude, reported side by side, not bit for bit.
  • Optimality: the brute-force test on IEEE-14 areas, and proven-versus-budget counts per system.
  • Stealth: per snapshot, the residual test at the noise level sees noise only; each meter's per-snapshot change sits under the bound.
  • The goal: the noiseless reported flow on the target line reaches S_max by the window's end, with the emitted (noisy) value recorded beside it.
  • Cost: tampered devices and channels per frame before and after (today's footprint is 3% to 45% of meters), and solve time per snapshot next to the paper's 1.1 to 7.5 s.

7. Data and documentation

The families change, so this needs a data release (v0.9.0): a timeline of At and Am with the hybrid meters of D10 (angles at the PMUs only, the current-phasor channels), the frozen references regenerated, and the guides' tables and the papers' numbers that use these families are rerun. Until the release, the documentation describes the families as they are.

8. Sequence

  1. Documentation matches the code (docs only): wherever the docs cite [WU26] for the stealthy families (README.md, docs/guides/generation.md, docs/reference/CONCEPTS_TO_CODE.md, docs/reference/GLOSSARY.md, the Am docstrings), say they satisfy the paper's constraints (13)-(18) and (21)-(23), not its objective (12) or goal (24)-(25).
  2. One home for attack generation (section 5): no behaviour change, bit-exact frozen suite, deprecated aliases at the old paths.
  3. The ℓ0 minimizer (section 3): the support search with the emitter's noise scale, its brute-force and reweighted-ℓ1 tests, behind a generator knob that is off by default.
  4. The [WU26] attack: PGLib-OPF ratings (D3), current-phasor meters (D4), the overload goal on a metered line and the window (D5), Am as the MS-FDIA and At on the minimizer (D2), the single-snapshot families deprecated for generation (D6). 4b. The hybrid meter model (D10): angles at the PMU buses only, the PMU branch-current channels in the emitter, the file, the loader and the attacks, and the estimators' optional eq. (3) pseudo-measurements; the v0.8.3 recipe unchanged.
  5. Data release v0.9.0 with the IEEE-14 scenario comparison, the guides rerun and the claims updated to what is then true.

9. Decisions

  • D1, the objective's reading: the number of devices (one SCADA terminal and one PMU per bus) with any channel tampered above its noise over the window, which is what the paper counts in its results; the channel count is recorded alongside (the literal norm of the summed attack vector would let opposite changes cancel).
  • D2, the minimizer's reach: every family the generator makes, At and Am (D6), so the area of every attack is the least a real attacker would need.
  • D3, S_max: (since D15 the alternative, am_attack={"rating_source": "pglib"}; the default rating is the pool's) the branch ratings (rate_a) of the PGLib-OPF v23.07 versions of IEEE-14, 118 and 300 (CC BY 4.0), in MVA, stored with the package and matched to our branches by their end buses; a branch's flow magnitude sqrt(P^2 + Q^2) from its MW and MVAr channels is compared with it directly. Pandapower's ratings cannot serve: every line and transformer of the three cases is rated 9,900 MVA, MATPOWER's placeholder for "no limit", and the base case loads the most loaded line to 1.5%, 4.5% and 8.7% of it. Matched by end buses, all branches pair up (20 of 20, 186 of 186, 411 of 411) and the base-case loadings are realistic (median 11%, 21% and 16%; most loaded 59%, 121% and 148%). A branch PGLib leaves at 9,900 MVA (7 on IEEE-300) is never a target, and a line whose true flow reaches its rating at any snapshot of a candidate window is not a target for that window, since the loads move along the timeline and such a goal would be met with no attack.
  • D4, PMU current phasors (19)-(20): added as branch current-phasor channels at PMU buses, a file-format change in the same release.
  • D5, the overload's meaning: (24)-(25) as written, on the noiseless reading of the false state: the flow the tampered measurements carry before noise and bias reaches the rating (the emitted reading adds noise, and its realized value is recorded next to the goal), so the operator sees the line at its limit. The operator's response, which the paper does not model, is out of scope and said so.
  • D6, multi-snapshot only: new generation makes At and Am; the single-snapshot families (Aq, Al, Ad, As, Ar) are deprecated for generation and remain loadable from released files.
  • D7, the noise scale of the bound and the objective: the meter's rated accuracy (the accuracy-class σ), not the per-scan jitter. The jitter σ, whose power floor is 0.001 MVAr on near-zero reactive flows, is far tighter than any real meter's resolution: under it almost no attack window met the between-snapshot bound.
  • D8, Am's noise scale: [WU26]'s own case-study noise, 0.03 pu on SCADA channels (P and Q injections and flows on the case base, in MW and MVAr as stored) and 0.01 pu on PMU channels (|V| in pu, the angle 0.01 rad in degrees), for the tamper count, since the paper excludes from its l0 count the changes smaller than its noise (narrowed by D11: at first also the scale of a between-snapshot stealth bound, which the paper does not have) (formulas.noise.paper_sigma). At keeps D7. Under the meters' rated accuracy (D7) no overload window was stealthy, since moving one line's flow moves the injections and flows around its ends by several times that change, beyond the rated accuracy of the small loads there.
  • D9, the goal schedule: S_{l,t} = S_true_{l,t} + (t - κ)/T (S_max - S_true_{l,κ+T}) on the noiseless reading, replacing the onset-anchored S_{l,κ} + (t - κ)/T (S_max - S_{l,κ}) and its floor at the true flow: the anchored form made the attack cancel the load's natural drift (a flow rising on its own had to be held back, one falling had to be propped up), spending tampering on the drift rather than the goal; the drift-free form still reaches the rating at κ+T (eq. 25).
  • D11, Am follows eqs. (12)-(25) exactly: [WU26]'s model has no between-snapshot increment constraint. Its noise enters only through the l0 count: a change smaller than the noise is not counted as tampering. Its IEEE-14 attack injects up to about 0.22 pu per device (its Fig. 4), about seven times its 0.03 pu noise, and is stealthy because every snapshot is AC-consistent, so the residual test sees noise. The between-snapshot stealth bound of D7 and D8 was this plan's invention and is what made Am infeasible on IEEE-14. Am therefore has no increment bound; its only use of noise is the l0 threshold (paper_sigma, 0.03 pu SCADA and 0.01 pu PMU, the current channels included). The rest of eqs. (12)-(25) is kept: (13)-(20) by construction, currents included; the limits (21)-(23); the goal (24)-(25) with the D9 schedule on the noiseless reading. At, which is not [WU26]'s attack, keeps its rated-accuracy bound: a sub-noise ramp is what At is. stealth_scale is At's alone. The comparison with the paper's case studies and the audit of the rest of the Am path are D14.
  • D12, meter_model is its own knob: a field of the meter plan (models.MeterSettings, passed as generate_timeline(redundancy=...) beside the coverage fractions, so the entry point takes no new parameter); new generation defaults to "hybrid"; the v0.8.3 recipe (the frozen test timeline, the legacy test recipe, the v0.8.3 build script and generate_stream's defaults) passes redundancy={"meter_model": "v083"} explicitly. FdiaGenerator's own default stays "v083", which nothing depends on. The frozen suite is bit-exact.
  • D13, prev_pmu_i: the previous emitted frame's PMU branch currents, a timeline field like prev_node_x and prev_edge_x (offered on a hybrid-meter file), so JacobianWeighting and any estimator that reads the previous frame can use the eq. (3) pseudo-measurements; the refusal is lifted.
  • D17, two lines at once, and [WU26]'s scenarios as a table: [WU26]'s case studies always overload two lines at once, so an overload episode drives n_lines lines, 2 by default (OverloadSettings.n_lines, 1 or 2: each added line multiplies the target sets an episode tries, and the paper uses no more). An episode draws its pairs from one random order of the eligible lines: each line in turn with the first other eligible line whose ends lie inside its attack area (the buses within hops of its ends), so one held support can reach both; it tries at most AM_LINE_TRIES pairs and otherwise stays benign and is counted (since the split-first generation it moves to another onset of its split instead). The file records n_lines, and episodes/am_* holds one row per target line: the branch, its rating, the goal at the window's end (am_target_mva) and the noiseless and emitted flows reached. The paper's scenarios are a table in engine/attacks/overload.py, in MATPOWER bus numbers mapped to our branches by their end buses (OverloadMixin.wu26_branch, wu26_buses):
system lines overloaded at once PMU buses source
IEEE-14 3-4 with 6-11; 1-2 with 4-5 1, 4, 6, 13 Section V-A
IEEE-118 84-85 with 99-100 76, 78, 80, 83, 89, 92, 94, 100, 105, 106, 110 Section V-B; the PMUs are the 11 buses drawn in red in Fig. 9 ("IEEE 118-bus system topology and PMU distribution", page 12 of the published PDF), read at 1,200 dpi against their bus labels; bus 94's label sits beside bus 95's bar, but the red bar is 94's

Fig. 9's red dashed region, the attacker's local network on IEEE-118, is legible: buses 74 to 112 and 118 (WU26_ATTACK_AREA). tests/test_wu_scenarios.py runs each scenario on the paper's PMUs with two-line goals and ratings 1.2 times each line's peak flow over a 10-snapshot window: on IEEE-14 both are feasible, reach both ratings, tamper every device of the paper's sets but one (SCADA 11 in lines 3-4 and 6-11: our 6-11 flow meter sits at its from end, bus 6, and the least-norm state leaves bus 11's injection within noise), with a largest per-device change bounded in the test (the paper reports 0.22 and 0.17 pu); on IEEE-118 (with FDIA_SLOW) the scenario is feasible and reaches both ratings. The S_max sensitivity study rerun on this code (two-line goals, the D16 bounds; experiment wu26.smax_sensitivity) is the table under D15. The episodes the two-line default builds are in the gen.am table under D15. - D16, the edge of the support is bounded: the injections of the buses on the support's edge move with the false voltages, and nothing bounded them: under D15 the heaviest lines' least-norm false states moved them by up to 5.7 pu on IEEE-14 and 45 pu on IEEE-118. Two bounds, both enforced in the flow solve by the active set that pins the generator limits (a component past its bound is pinned at it and the solve repeated), P and Q apart: (22)-(23) apply to every generator whose reported output the attack changes, the edge's included, and are checked by within_limits over the support and its edge, which is closer to the paper, whose (22)-(23) bound every generator, than our earlier interior-only reading; and a load bus the attack moves, in the support or on its edge (no generator, not the slack), shows an active change of at most load_cap times its true load, the load-plausibility rule of [YUA11] (tau 20% to 50%), ours and not the paper's, with tau = 0.5 by default (OverloadSettings.load_cap, in (0, 1], recorded in the file's attributes as load_cap). The measurements are with D15 below, under the D16 bounds and before them. - D15, the ratings default to the operating pool: S_max of each branch is rating_margin times its peak true apparent flow over the operating pool the timeline walks (static per line, like a thermal rating, computed once from the pool's noiseless from-end flows, the magnitude the goal reads), with rating_margin = 1.25 by default. Ben's advisor's guidance is that limits sit 20% to 30% above a line's flow. The sensitivity study on [WU26]'s IEEE-14 case-study metering (S_max = k times each goal line's peak flow over the window; experiment wu26.smax_sensitivity) puts the paper's reported scale of about 0.2 pu at the small k; the PGLib-OPF ratings sit several times above those lines' flows, and with D16's edge bounds no attack reaches them:

scenario (devices / channels / largest change, pu) k = 1.05 k = 1.1 k = 1.2 k = 1.3 k = 1.5 k = 2.0 k = pglib
lines 3-4 and 6-11 4 / 6 / 0.13 4 / 6 / 0.18 9 / 36 / 0.81 14 / 64 / 1.67 no attack no attack no attack
lines 1-2 and 4-5 6 / 13 / 0.14 8 / 22 / 0.23 10 / 43 / 0.37 17 / 69 / 0.50 no attack no attack no attack

The pool ratings work on every system of the ladder (no NoLineRatings); the PGLib-OPF ratings stay available as the robustness alternative (am_attack={"rating_source": "pglib"}, IEEE-14, 118 and 300 only) and are reported alongside in the papers. Both knobs live in models.OverloadSettings, passed as a dict through am_attack (no new top-level parameter), and are recorded in the file's attributes (rating_source, rating_margin) when the overload attack runs. Eligibility is unchanged: a line whose true flow reaches its rating at any snapshot of the window is not a target. The overload episodes each recipe builds (experiment generation.am_overload):

system recipe frames episodes fallen back devices (mean) channels (mean) largest change, pu (median / max episode) s per episode
IEEE-14 one line, before the D16 bounds 3000 25 0 5.3 15.0 0.18 / 5.74 15
IEEE-14 one line, D16 bounds 3000 25 0 3.9 9.9 0.14 / 0.82 30
IEEE-14 two lines, D16 bounds (default) 3000 25 0 6.8 19.3 0.17 / 1.59 39
IEEE-30 one line, before the D16 bounds 600 5 0 3.8 8.8 0.09 / 0.29 20
IEEE-118 one line, before the D16 bounds 2000 17 0 6.8 21.4 1.05 / 44.94 43
IEEE-118 one line, D16 bounds 2000 17 0 9.6 30.4 0.40 / 3.29 71
IEEE-118 two lines, D16 bounds (default) 2000 17 0 17.8 72.0 1.33 / 3.39 78
  • D14, the rest of the Am path against eqs. (12)-(25): generator injections are free, as [WU26, eqs. 13-14] let every injection measurement be tampered and (22)-(23) bound only the generator output: a generator bus of the support has free P and Q injection like an attackable load, inside its limits. The flow solve enforces the limits (21)-(23) with an active set: a generator driven outside its limits is pinned at the nearest limit, a bus voltage outside its limit is held at it, and the least-norm solve is repeated. A candidate support holds an end bus of each goal line and at least one free injection (an attackable load or a generator). A flow goal takes one or more lines (FlowGoal.more, overload_goal(window, line, *more)): one held support, each line's noiseless flow reaching its own rating by the window's end on the D9 schedule, the l0 count over the union. At D14 the generator's episodes drove one line (two by default since D17); the multi-line goal is what the paper's case studies use and is available as an option. S_max is the PGLib-OPF rating only: [WU26] does not state its line limits, so no assumed limit is compared with it. On the paper's IEEE-14 metering with the PGLib-OPF ratings, which sit several times above the scenario lines' true flows, every device of the paper's sets is in ours but ours are larger, with changes of several pu at the generators and loads around the lines, against the paper's 0.22 and 0.17 pu (its limits must sit much closer to the flows, which it does not state). The constraints this package adds that the paper does not state:
constraint we add ours, because
a zero-injection bus is held at zero physical: nothing is connected there, so no meter there can read anything else
the attacker's area is the buses within hops = 2 of the goal lines' ends, boundary voltages held true the local attacker of the package's stealthy families; the paper's attacker also knows only a local subnetwork but states no radius
one support held for the whole window the l0 count is over the window, and a support that changes between snapshots would count the union anyway; it keeps the search finite
the least-norm voltage change among a support's false states, with the limits enforced by an active set the paper leaves the choice among feasible false states to its solver; the smallest change is the one nearest the true state
at most AM_LINE_TRIES = 8 eligible lines tried per episode, each rated, metered and below its rating over the window a bounded generator; the paper studies chosen lines
generator limits widened to the range the pool ran each generator over, and a bus already outside its voltage limit may not be made worse the pools were built without nameplate enforcement, so the nameplate would refuse states the pool already holds
the goal is the apparent flow at the from end, where the flow is metered the reading the operator sees
a load bus the attack moves, in the support or on its edge, shows an active change of at most load_cap (tau, 0.5 by default) times its true load (D16) load plausibility: a load redistribution attack keeps each load within tau of its true value, tau 20% to 50% [YUA11]; without it the least-norm state moved edge injections by up to 45 pu
- D10, the meters of new generation: the hybrid SCADA and PMU plan of [WU26]. A SCADA voltmeter
reads |V| only and the angle is a PMU channel (the released files wrote an angle at every
voltmeter bus); every PMU reads the current phasor of each in-service branch at its bus (19)-(20),
the real and imaginary part at each end in per unit, with the PMU class of IEEE C37.118.1 (1%
total vector error as three standard deviations of the magnitude, plus a 1e-5 pu floor), one rule
for the emitter and the search (formulas.noise.current_sigma). The stealthy families write
h(x^a) - h(x) on the currents too, a tampered current counts in the PMU of its bus (D1); At bounds
its step by the class (D7), and Am counts it as tampered only beyond 0.01 pu, a tamper-count
threshold and not a step bound (D8, D11). The file stores
pmu_i with its benign twin, mask and tamper mask; a file without them loads unchanged.
meter_model defaults to "hybrid" and the v0.8.3 recipe pins "v083" through the meter plan,
redundancy={"meter_model": "v083"} (D12). The estimators may add [WU26, eq. (3)]'s pseudo voltage phasors at the far ends of the
PMU-metered branches (pmu_pseudo, off by default), weighted by their propagated variance.
Taking the angles away from the voltmeters raises the benign angle error, and the eq. (3)
pseudo-measurements win part of it back; the comparison is rerun into the results store with the
v0.9.0 data.