Part X: Sensor Fusion, World Models, and Spatial AI
Chapter 54: Graph Neural Networks for Sensor Networks

Message passing for fault localization

"An alarm tells me the network is sick. Message passing tells me which sensor is coughing and which ones just caught the draft."

A Diagnostically Inclined AI Agent

Prerequisites

This section builds directly on the graph representation of Section 54.1 (nodes, edges, the adjacency matrix \(\mathbf{A}\)) and the spatiotemporal predictors of Section 54.3, whose one-step-ahead forecasts we turn into residuals here. It assumes the residual and change-detection thinking of Chapter 12, the condition-monitoring context of Chapter 37, and the message-passing intuition that a variable's estimate should be refined by its neighbors, first met in the factor graphs of Chapter 11. The neural building blocks (a learnable linear layer, a nonlinearity) come from the Deep Learning Refresher in Appendix B.

The Big Picture

A network-wide anomaly detector answers one bit of a much richer question. It says "something is wrong" when a bearing overheats, a pipe cracks, or a phasor unit drifts. What an operator actually needs is the next word: where. Fault localization is the task of pointing at the specific node (or the small set of nodes) that is the source, and distinguishing it from the many neighbors that merely inherited the disturbance through the physics of the system. This is exactly where a graph model earns its keep. Because a fault propagates along edges (heat down a shaft, pressure down a main, congestion down a road), the pattern of who-deviates-and-by-how-much is a graph signal, and the mechanism that reads graph signals is message passing. This section shows how one round of message passing aggregates each node's own residual with its neighbors', and how stacking a few rounds lets a model separate the culprit from the collateral.

From "something is wrong" to "here is the source"

The what: fault localization outputs a per-node score, ideally a probability that node \(i\) is the origin of the current anomaly, rather than a single global flag. The why it is hard, and why classical per-sensor thresholds fail, is fault propagation. Consider a chilled-water loop: a fouled heat exchanger raises the temperature it reports, but within minutes the downstream pump, the return-line thermometer, and the setpoint controller all read abnormally too. A detector that flags every sensor exceeding its own band lights up half the plant and localizes nothing. The origin and the collateral look identical in the marginal statistics; they differ only in the relational pattern. The origin deviates first and most, and its deviation is unexplained by its neighbors, whereas a collateral node's deviation is exactly what its upstream neighbor predicts. Message passing is the tool that makes "explained by neighbors" computable.

The standard recipe has two stages. First, a model of normal behavior (the spatiotemporal GNN of Section 54.3, trained on healthy data only, leakage-safe per Chapter 37) produces a one-step prediction \(\hat{x}_i(t)\) for every node. The residual \(r_i(t) = x_i(t) - \hat{x}_i(t)\) is large exactly where reality departs from the learned normal. Second, a message-passing localizer takes the residual graph (residuals as node features on the same topology) and scores each node for being the source. Splitting the problem this way keeps the normal model reusable and lets the localizer specialize in the relational question.

The message-passing mechanism

The how is one equation applied in rounds. A message-passing layer updates every node's hidden vector \(\mathbf{h}_i\) by gathering transformed messages from its graph neighbors \(\mathcal{N}(i)\), pooling them with a permutation-invariant aggregator, and combining the pool with the node's own state:

\[ \mathbf{h}_i^{(\ell+1)} = \phi\!\left( \mathbf{h}_i^{(\ell)},\; \bigoplus_{j \in \mathcal{N}(i)} \psi\!\left(\mathbf{h}_i^{(\ell)}, \mathbf{h}_j^{(\ell)}, e_{ij}\right) \right). \]

Here \(\psi\) is a learnable message function (often a small MLP on the concatenation of the two endpoints and the edge feature \(e_{ij}\)), \(\bigoplus\) is the aggregator (sum, mean, or max, chosen so the result does not depend on neighbor ordering), and \(\phi\) is the update function. Initialize \(\mathbf{h}_i^{(0)} = r_i\) with the node's residual and this becomes a diffusion of anomaly evidence along physical edges. The design choices matter for localization specifically. Sum aggregation preserves degree information, so a node whose many neighbors all deviate accumulates a large signal, useful for spotting a hub-level fault. Max aggregation is directional-evidence friendly: it forwards the single strongest upstream deviation, which helps trace a fault back along a flow path. And because \(\psi\) sees both endpoints, the model can learn the crucial asymmetry: a message from an already-abnormal upstream neighbor should lower a node's own source-score (its deviation is explained), while an anomaly with no abnormal upstream cause should raise it.

Key Insight: receptive field is a diagnostic dial, not just a depth hyperparameter

Stacking \(L\) message-passing layers gives every node an \(L\)-hop receptive field: after \(L\) rounds, node \(i\)'s representation depends on all nodes within \(L\) edges. For localization this has a physical reading. Set \(L\) roughly to the number of hops a fault can travel within your prediction horizon. Too shallow, and the localizer cannot see far enough to tell "I deviated because my upstream did" from "I deviated on my own," so it over-blames collateral nodes. Too deep, and over-smoothing sets in: repeated averaging washes every node's representation toward the graph mean, and the sharp source-versus-collateral contrast you need for localization dissolves. A localizer with two or three layers usually beats a ten-layer one on this task, which is the opposite of the "deeper is better" reflex from vision. The right depth is the fault's physical reach, not the network's capacity.

Scoring the source, and reading the propagation

After \(L\) rounds, a shared readout head maps each node's final vector to a source-score \(s_i = \sigma(\mathbf{w}^\top \mathbf{h}_i^{(L)})\). Two supervision regimes are common. When labeled fault injections exist (a valve you can command shut, a documented historical failure with a known root node), train the localizer supervised with a per-node binary target and a class-weighted loss, since sources are rare relative to healthy and collateral nodes. When labels are scarce, run unsupervised: rank nodes by a message-passing-refined anomaly score, for example the residual reconstruction error after the network has had a chance to explain each node from its neighbors, so that only genuinely unexplained deviations keep a high score. Either way the output is a ranked shortlist, and the graph gives you the propagation story for free: follow the edges of high-score nodes backward along the flow direction and the path terminates at the origin. That directed trace is what connects this section to the root-cause and interpretability tooling of Chapter 67, and to the edge-attribution methods of Section 54.7.

Practical Example: locating a leak on a district water main

A water utility instruments a district network with pressure sensors at 90 junctions, wired into a graph whose edges are the pipes (directed downstream, weighted by inverse pipe length). A GNN forecaster trained on a leak-free month predicts each junction's next-hour pressure. One afternoon a pipe cracks near junction 41. The raw residuals are messy: junction 41 drops sharply, but so do junctions 39, 44, and 52 downstream, and a naive threshold flags all four plus three noisy sensors elsewhere. The two-layer message-passing localizer resolves it. Junction 41's abnormal pressure has no abnormal upstream neighbor to explain it, so its source-score stays high; junctions 44 and 52 each receive a strong message from an already-abnormal upstream node, and the learned update discounts their scores as explained-by-propagation. The localizer returns junction 41 as the top-ranked source with 44 and 52 as its downstream cone. The repair crew is dispatched to one junction, not seven, and the false alarms elsewhere in the district, which lacked any propagating structure, are suppressed because no coherent path supports them.

The code below implements the core loop in plain NumPy so the mechanism is visible: build a small directed graph, plant a fault at one node, propagate residuals, and score. We reference it to show that "message passing" is, at bottom, sparse matrix multiplies interleaved with a nonlinear update.

import numpy as np

# Directed line graph: 0 -> 1 -> 2 -> 3 -> 4 (flow downstream)
N = 5
A = np.zeros((N, N))
for i in range(N - 1):
    A[i + 1, i] = 1.0          # node i sends a message to its downstream node i+1

# Residuals: a fault originates at node 1 and propagates, decaying, downstream.
r = np.array([0.0, 1.0, 0.7, 0.5, 0.3])

def message_pass(h, A, W_self, W_msg):
    agg = A @ h                       # sum of upstream neighbor states (max-aggregation variant omitted)
    # Source-evidence update: own residual minus what an abnormal upstream explains.
    return np.tanh(h @ W_self - agg @ W_msg)

np.random.seed(0)
W_self = np.array([[1.6]])            # amplify a node's own unexplained deviation
W_msg  = np.array([[1.1]])            # subtract deviation already explained upstream
h = r.reshape(-1, 1)
for _ in range(2):                    # two hops: physical reach of the fault
    h = message_pass(h, A, W_self, W_msg)

source_score = h.ravel()
print("source scores:", np.round(source_score, 3))
print("localized origin -> node", int(np.argmax(source_score)))
A minimal two-hop message-passing localizer in NumPy. Node 1 carries a deviation with no abnormal upstream cause, so the self-versus-neighbor update keeps its score highest; downstream nodes 2 to 4 are discounted as explained-by-propagation, and the argmax returns the true origin.

Library Shortcut: PyTorch Geometric's MessagePassing base class

The hand-rolled loop above hides the parts that get painful at scale: sparse neighbor gathering, per-edge message functions, batched multi-relation graphs, and gradient flow through the aggregator. PyTorch Geometric's MessagePassing base class reduces a custom layer to overriding message() and update(); its propagate() call handles the scatter-gather, edge indexing, and choice of aggregator (aggr="sum" | "mean" | "max") for you. A localizer that would take roughly 120 lines of careful sparse-tensor bookkeeping in raw PyTorch collapses to about 15 lines, and it runs on the real METR-LA or a utility's SCADA graph without you touching an index. Reach for the from-scratch version only to build the intuition, or when a target device forbids the dependency (the TinyML constraints of Chapter 61).

Research Frontier: graph anomaly localization at grid scale

Current work pushes message-passing localization in three directions. GDN (Graph Deviation Network, Deng and Hooi, AAAI 2021) learns the sensor graph and localizes multivariate anomalies by per-node deviation from an attention-based neighbor forecast, and remains a strong baseline on the SWaT and WADI water-treatment testbeds. FuSAGNet and related fused reconstruction-plus-forecast localizers tighten the source-versus-collateral separation. On power systems, spatiotemporal GNN localizers now diagnose line faults from PMU streams within cycles. The open problems are honest uncertainty on the localization (which node with what confidence, tying back to the conformal methods of Chapter 18) and localization under a shifting graph, where the topology itself changes as breakers trip or valves close mid-fault.

Exercise

Take the NumPy example and branch the graph: make node 2 feed two downstream children, 3 and 4, and plant the fault at node 2 instead. (a) Run two hops and confirm the origin is still top-ranked. (b) Now increase to five hops and observe over-smoothing: report how many nodes end up within 10% of the maximum score, and explain why deeper hurts localization here. (c) Switch the aggregator from sum (\(\mathbf{A} \mathbf{h}\)) to max by replacing the matrix product with a per-node maximum over incoming neighbors, and describe one fault topology where max localizes better than sum.

Self-Check

  1. Why does a per-sensor threshold detector "light up half the plant" during a propagating fault, and what relational quantity does message passing add that a marginal threshold cannot?
  2. Explain the trade-off in choosing the number of message-passing layers \(L\) for localization, and why the best \(L\) is often much smaller than for a deep vision network.
  3. In the two-stage recipe, why is the normal-behavior model trained on healthy data only, and what would go wrong if fault windows leaked into its training set?

What's Next

In Section 54.5, we confront the practical wall that a real grid, a national road network, or a utility with hundreds of thousands of nodes puts in front of message passing: full-graph training no longer fits in memory. We cover neighbor sampling, graph partitioning, and the linear-time tricks that let the mechanism you just built run on networks far too large to load at once.