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_tof 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'slocal_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 forAmthe 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.
- 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.
- 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. - 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.
- 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):
Amis 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:
AqandAl(stealthy, one frame) andAd,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 oneAttackMixin: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(theAq/At/Al/Amframes and episode designs),corrupt.py(Ad/As/Ar). A reader looking for how an attack is made opens this package.engine/records.pykeeps the benign frame and the replay memory, and asks the mixin for an attacked frame.timeline.pykeeps 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_maxby 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¶
- 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, theAmdocstrings), say they satisfy the paper's constraints (13)-(18) and (21)-(23), not its objective (12) or goal (24)-(25). - One home for attack generation (section 5): no behaviour change, bit-exact frozen suite, deprecated aliases at the old paths.
- 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.
- The [WU26] attack: PGLib-OPF ratings (D3), current-phasor meters (D4), the overload goal on
a metered line and the window (D5),
Amas the MS-FDIA andAton 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. - 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,
AtandAm(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 magnitudesqrt(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
AtandAm; 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).Atkeeps 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-anchoredS_{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,
Amfollows 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 madeAminfeasible on IEEE-14.Amtherefore 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 whatAtis.stealth_scaleisAt's alone. The comparison with the paper's case studies and the audit of the rest of theAmpath are D14. - D12,
meter_modelis its own knob: a field of the meter plan (models.MeterSettings, passed asgenerate_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 andgenerate_stream's defaults) passesredundancy={"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 likeprev_node_xandprev_edge_x(offered on a hybrid-meter file), soJacobianWeightingand 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_lineslines, 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 withinhopsof its ends), so one held support can reach both; it tries at mostAM_LINE_TRIESpairs and otherwise stays benign and is counted (since the split-first generation it moves to another onset of its split instead). The file recordsn_lines, andepisodes/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 inengine/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
Ampath 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. |