Part VII: Building Intelligent Temporal Systems
Chapter 32: Spatio-Temporal Intelligence

Dynamic and Spatio-Temporal Graph Learning

"They handed me a graph and told me to learn it. By the time I had finished reading the adjacency matrix, three edges had vanished, two new sensors had come online, and every node had quietly changed its mind about what its features were. I no longer ask what the graph is. I ask only what the graph is doing, right now, this instant, before it does something else."

A Graph Whose Nodes, Edges, and Signals All Refuse to Sit Still
Big Picture

Every spatio-temporal problem in this chapter, traffic on a road network (Section 32.2), a field of sensors reporting in concert, a contagion spreading through a contact network, a human skeleton in motion (Section 32.4), is one object seen from different angles: a graph whose node features evolve in time, sometimes over edges that themselves appear and disappear. This section names that object, the general spatio-temporal graph, and shows that the entire zoo of named models is a small design space of choices about how to interleave a spatial operator (message passing over the graph) with a temporal operator (a recurrence, convolution, or attention over time). Once you see the abstraction, the rest is engineering: pick how spatial and temporal mixing compose (interleaved, factorized, or joint space-time attention), pick whether the graph is a sequence of discrete snapshots or a continuous stream of timestamped events, and pick whether the adjacency is given or learned. We build a general spatio-temporal block from scratch, a graph convolution wrapped in a temporal update, then collapse it to a few lines of PyTorch-Geometric-Temporal, and read off a numeric spatio-temporal node update by hand. This section closes Chapter 32 and with it Part VII: reasoning (Chapter 30), agency (Chapter 31), and spatio-temporal grounding (this chapter) are the three ingredients of an intelligent temporal system, and Part VIII asks how to make such a system trustworthy and deployable.

The previous sections of this chapter studied spatio-temporal intelligence one domain at a time: Section 32.2 forecast traffic over a fixed road graph, and Section 32.4 recognized actions from the moving graph of a human skeleton. Each used a model tuned to its domain, and each, underneath, did the same two things: it mixed information across space (between connected nodes) and across time (between successive frames). This closing section steps back to the abstraction those models share. We define the general spatio-temporal graph, in which a set of nodes carries time-varying feature vectors and is connected by edges that may also vary in time, and we show that traffic networks, sensor fields, epidemic-contact graphs, and skeletons are all instances of it. We use the unified notation of Appendix A throughout: $\mathbf{x}_v^{(t)}$ the feature of node $v$ at time $t$, $\mathcal{G}^{(t)} = (\mathcal{V}, \mathcal{E}^{(t)})$ the (possibly time-varying) graph, $\mathbf{A}^{(t)}$ its adjacency matrix.

The reason this synthesis earns the final slot of the chapter, and of the part, is that it converts a list of named architectures into a design space you can reason about. A practitioner who has memorized DCRNN, STGCN, GraphWaveNet, and a dozen others holds a catalog; a practitioner who sees them as points in a space of spatial-operator times temporal-operator times graph-dynamics choices holds a map, and a map lets you place a new problem, anticipate which corner of the space fits it, and know what to try when the first attempt underperforms a strong baseline. By the end you will be able to write the general spatio-temporal message-passing update, classify any named model by where it sits in the design space, build a working block by hand, reach for the library equivalent, and reason about when the graph structure actually earns its keep.

The four competencies this section installs: to recognize the general spatio-temporal graph behind a concrete problem and write its message-passing update; to navigate the design space of spatial-temporal composition (interleaved, factorized, joint) and graph dynamics (discrete-snapshot versus continuous-time, fixed versus learned adjacency); to apply the scalability lessons and the strong-baseline discipline that separate a graph model that helps from one that merely complicates; and to assemble a spatio-temporal GNN block from scratch and via PyTorch-Geometric-Temporal. These are the load-bearing skills for the spatio-temporal grounding that an intelligent temporal system needs.

1. The General Spatio-Temporal Graph Beginner

A spatio-temporal graph is the natural data structure for a system of interacting entities observed over time. Fix a node set $\mathcal{V}$ of $N$ entities (road sensors, weather stations, people, skeleton joints). Each node $v$ carries, at every timestep $t$, a feature vector $\mathbf{x}_v^{(t)} \in \mathbb{R}^{d}$: a traffic-speed reading, a temperature, an infection status, a joint coordinate. The nodes are wired together by edges $\mathcal{E}$ that encode which entities influence which: physically adjacent road segments, geographically near stations, people in contact, bones connecting joints. Stack the node features into a matrix $\mathbf{X}^{(t)} \in \mathbb{R}^{N \times d}$ and the wiring into an adjacency $\mathbf{A} \in \mathbb{R}^{N \times N}$, and the whole observed history is the sequence $(\mathbf{X}^{(1)}, \mathbf{X}^{(2)}, \dots, \mathbf{X}^{(T)})$ riding on the graph.

The signal varies in time; that much every model in this chapter already handled. The deeper generalization is that the graph itself may vary in time. In a contact network for epidemic spread, edges appear when two people meet and vanish when they part, so the right object is a sequence of graphs $\mathcal{G}^{(t)} = (\mathcal{V}, \mathcal{E}^{(t)})$ with a time-indexed adjacency $\mathbf{A}^{(t)}$. This is the dynamic-graph view we first met in Section 18.3, where a temporal graph was a stream of timestamped edge events and the task was to represent nodes whose neighborhoods rewire continuously. A spatio-temporal graph is the union of those two kinds of motion: signals moving over nodes, and edges moving between them. A model may treat the structure as fixed ($\mathbf{A}^{(t)} = \mathbf{A}$ for all $t$, the traffic and skeleton cases) or as genuinely dynamic (the contact-network case), and that choice is the first axis of the design space in subsection two.

One graph, three timesteps: features shift, edges rewire t = 1 t = 2 t = 3 time time node shade = feature value (shifts each step); dashed orange = a new edge at t = 3
Figure 32.5.1: A general spatio-temporal graph over three timesteps. Node shading encodes the time-varying feature $\mathbf{x}_v^{(t)}$, which shifts at every step; between $t=1$ and $t=2$ one edge disappears and at $t=3$ a new edge (dashed orange) appears, illustrating a time-varying adjacency $\mathbf{A}^{(t)}$. Traffic and skeletons keep the structure fixed; contact and epidemic graphs let it move.

The unifying claim is concrete: the four headline applications of this chapter are the same abstraction with different fillings. Traffic forecasting (Section 32.2) is a spatio-temporal graph whose nodes are sensors, edges are road adjacency (fixed), and features are speeds. A sensor network is the same with stations and geographic proximity. Epidemic spread is a spatio-temporal graph whose nodes are individuals or regions, edges are a (dynamic) contact structure, and features are infection counts; here the structure genuinely moves. Skeleton action recognition (Section 32.4) is a spatio-temporal graph whose nodes are joints, edges are the (fixed) bone topology, and features are 3D coordinates over the frames of a motion. Recognizing the abstraction is what lets one body of method serve all four, and it is why this section can close the chapter: it is the chapter, stated once at the right altitude.

Key Insight: Three Things Can Move, and Naming Which Is the First Modeling Decision

In an ordinary time series the signal moves and nothing else. In a spatio-temporal graph there are three independent kinds of motion: the node features $\mathbf{x}_v^{(t)}$ (always moving, that is what makes it temporal), the edges $\mathcal{E}^{(t)}$ (moving in dynamic graphs, frozen in traffic and skeletons), and, in the learned-adjacency models of subsection two, the model's belief about the edges (an adjacency the network estimates rather than is given). The first thing to decide about any spatio-temporal problem is which of the three you will let move. Freeze the wrong one and you discard signal; let the wrong one move and you add parameters and instability for nothing. The whole zoo of named models is, at bottom, different answers to "which of these three moves, and how do we mix them across space and time?"

2. The Design Space: Combining Spatial and Temporal Operators Intermediate

Every spatio-temporal model is built from two primitive operators. A spatial operator mixes information between connected nodes at a fixed time: one round of graph message passing, in which each node aggregates a transformed version of its neighbors' features. A temporal operator mixes information across timesteps at a fixed node: a recurrence (Chapter 10), a temporal convolution (Chapter 11), or attention over time (Chapter 12). The general spatio-temporal message-passing update applies both. Writing $\mathcal{N}(v)$ for the neighbors of $v$ and $\mathbf{h}_v^{(t)}$ for the hidden state of node $v$ at time $t$, one layer computes

$$\mathbf{m}_v^{(t)} = \bigoplus_{u \in \mathcal{N}(v)} \phi\!\left(\mathbf{h}_v^{(t)}, \mathbf{h}_u^{(t)}, \mathbf{a}_{uv}^{(t)}\right), \qquad \mathbf{h}_v^{(t+1)} = \psi\!\left(\mathbf{h}_v^{(t)},\, \mathbf{m}_v^{(t)},\, \mathbf{h}_v^{(t-1)}\right),$$

where $\phi$ is a learnable message function over an edge with (possibly time-varying) attribute $\mathbf{a}_{uv}^{(t)}$, $\bigoplus$ is a permutation-invariant aggregator (sum, mean, max, or attention-weighted sum), and $\psi$ is the temporal update that combines the spatial message $\mathbf{m}_v^{(t)}$ with the node's own past $\mathbf{h}_v^{(t-1)}$. The first equation is the spatial operator (message passing over the graph at time $t$); the second is the temporal operator (a recurrence in $t$). Read it as a graph convolution and a sequence model fused at the node level: this single update, specialized differently, reproduces almost every architecture in the chapter.

How $\phi$, $\bigoplus$, and $\psi$ are arranged gives the first design axis, spatial-temporal composition, with three recurring patterns:

The second axis is graph dynamics: discrete-snapshot models consume a sequence of graph states $\mathcal{G}^{(1)}, \dots, \mathcal{G}^{(T)}$ at fixed sampling times (the natural fit for traffic, where readings arrive on a regular grid), while continuous-time models consume a stream of timestamped events and update node representations the instant an edge or feature changes (the natural fit for the irregular contact and interaction graphs of Section 18.3, and the setting of TGN-style temporal-graph networks). The third axis is adjacency: a model may use a fixed adjacency from domain knowledge (road topology, bone topology), or learn an adaptive adjacency as parameters, $\mathbf{A} = \operatorname{softmax}(\operatorname{ReLU}(\mathbf{E}_1 \mathbf{E}_2^\top))$ for learnable node embeddings $\mathbf{E}_1, \mathbf{E}_2$, the device that lets GraphWaveNet and AGCRN discover influence the given topology omits. Figure 32.5.2 lays the three axes out as a table.

Design axisChoice AChoice BChoice C
spatial-temporal compositioninterleaved / stacked (STGCN)factorized recurrent (DCRNN)joint space-time attention (ASTGCN, ST-Transformer)
graph dynamicsdiscrete snapshots (regular grid)continuous-time events (TGN)static structure, dynamic signal (traffic, skeleton)
adjacencyfixed from domain (road, bones)learned / adaptive ($\mathbf{E}_1\mathbf{E}_2^\top$)attention-weighted per step
typical cost$O(T \cdot |\mathcal{E}|)$, parallel in $T$$O(T \cdot |\mathcal{E}|)$, sequential in $T$$O(N T \cdot d)$ to $O((NT)^2)$
Figure 32.5.2: The spatio-temporal design space as three nearly-independent axes. Any named model is a point in this grid: STGCN is interleaved + snapshot + fixed; DCRNN is factorized-recurrent + snapshot + fixed; GraphWaveNet is interleaved + snapshot + learned; a temporal-graph network is event-driven + continuous-time + dynamic. Picking a coordinate on each axis specifies an architecture.

The practical value of the grid is that it turns architecture selection into three small decisions instead of one large guess. Is your data on a regular grid or an irregular event stream? That sets the dynamics axis. Do you trust the given topology, or do you suspect important influence it omits? That sets the adjacency axis. Can you afford sequential-in-time training, and do you need space and time tightly entangled? That sets the composition axis. A new problem is placed by answering three questions, and the answer points at a corner of the space and a representative model to start from, rather than at a name pulled from memory.

Numeric Example: One Spatio-Temporal Node Update by Hand

Take a tiny graph of three nodes in a path, $1 - 2 - 3$, with scalar hidden states. At time $t$ the states are $h_1 = 1.0$, $h_2 = 0.0$, $h_3 = 2.0$. The spatial operator is mean aggregation of neighbors: node 2 has neighbors $\{1, 3\}$, so its spatial message is $m_2 = \tfrac{1}{2}(h_1 + h_3) = \tfrac{1}{2}(1.0 + 2.0) = 1.5$. Node 1 has only neighbor $\{2\}$, so $m_1 = h_2 = 0.0$; node 3 has only $\{2\}$, so $m_3 = h_2 = 0.0$. Now apply a temporal update $\psi$ that is a leaky carry of the node's own past plus the fresh spatial message, $h_v^{(t+1)} = \tanh(0.5\, h_v^{(t)} + 1.0\, m_v^{(t)})$. For node 2: $h_2^{(t+1)} = \tanh(0.5\cdot 0.0 + 1.0\cdot 1.5) = \tanh(1.5) \approx 0.905$. For node 1: $\tanh(0.5\cdot 1.0 + 0.0) = \tanh(0.5) \approx 0.462$. For node 3: $\tanh(0.5\cdot 2.0 + 0.0) = \tanh(1.0) \approx 0.762$. In one update node 2 absorbed the average of its two neighbors and pulled toward it, while the endpoints relaxed toward their own decayed past, exactly the spatial-then-temporal flow the general update prescribes. Stack a few such layers and influence reaches across the graph and back through time.

One subtlety deserves emphasis because it is the source of most failures when the structure moves. In a discrete-snapshot model with a fixed adjacency, the spatial operator is the same at every step and the temporal operator carries state cleanly. In a continuous-time or dynamic-adjacency model, the neighborhood $\mathcal{N}(v)$ over which $\bigoplus$ aggregates is itself a function of $t$, so the message $\mathbf{m}_v^{(t)}$ is computed over a moving set; a node can gain a neighbor it never had, or lose the neighbor that carried most of its signal. Handling that correctly (recomputing neighborhoods per event, masking edges that have not yet appeared, preventing information from a future edge leaking into a past state) is the part of dynamic-graph engineering that the static-graph intuition from traffic does not prepare you for, and it is the reason the dynamic-graph models of Section 18.3 are a genuinely harder class than their static cousins.

3. Scalability, Strong Baselines, and Foundation Models Advanced

Spatio-temporal foundation models generalize temporal foundation models from individual series to space-time fields, graphs, trajectories, and sensor networks. Three deployment patterns are now visible: traffic and mobility pretraining over road graphs and trajectories, Earth-system pretraining over gridded atmospheric fields, and urban digital-twin pretraining that supports forecasting plus intervention. The comparison rule is strict: compare against temporal-only baselines and graph baselines on the same split, because the graph earns its cost only when cross-node influence improves decision value rather than merely adding parameters.

An enormous overengineered machine of gears and antennae is beaten across a finish line by a plain little cart that just repeats yesterday's load.
Figure 32.6: Again and again a humble baseline that mostly repeats the recent past beats the fancy graph network, which is why a strong baseline is the first thing any serious model must outrun.

The expressive power of joint space-time models comes with a cost that scales with the product of nodes and timesteps, and on a real network that product is large. A city traffic graph has thousands of sensors observed every five minutes; a continental sensor field has tens of thousands of stations; a social contact graph has millions of nodes. A full joint space-time attention over $N T$ tokens is $O((NT)^2)$ and infeasible at those sizes, which is why the scalable members of the family factorize attention into a spatial pass and a temporal pass, sparsify the graph to a $k$-nearest-neighbor or thresholded adjacency, sample neighborhoods (GraphSAGE-style) so each node aggregates a bounded number of neighbors, or batch over subgraphs rather than the whole graph at once. The engineering lesson is the same one that recurs in this book: the architecture you can write down on the whiteboard is rarely the one you train at scale, and the gap is closed by factorization, sparsification, and sampling.

The harder and more uncomfortable lesson is about baselines. Spatio-temporal graph models are seductive: the picture of information diffusing across a learned network onto a forecast is compelling, and the literature is full of architectures that beat each other by fractions of a point. But a recurring and well-documented finding is that a strong univariate baseline, one model per node fitted independently, or a simple linear model on the recent window, is frequently competitive with, and sometimes superior to, an elaborate spatio-temporal graph network, especially when the spatial dependencies are weak, the horizon is short, or the test distribution differs from training. The graph helps when there is genuine, exploitable cross-node influence that a per-node model cannot see: upstream traffic predicting downstream congestion, an infection in one region foretelling the next. It does not help, and the extra parameters hurt, when each node's future is mostly a function of its own past. The discipline this demands is non-negotiable: before claiming a spatio-temporal graph model works, beat a strong per-node baseline and a simple multivariate linear model on the same split, the same metric, computed in one pass, and report the comparison honestly. A graph model that cannot beat one forecaster per node is solving a problem the data does not have.

Fun Note: The Graph That Was a Confidence Trick

There is a recurring rite of passage in spatio-temporal forecasting. You build an elegant graph network, watch the loss curve descend, present the architecture diagram with its learned adjacency glowing, and then a reviewer (or, worse, a colleague over coffee) asks the deflating question: "did you compare against one ARIMA per sensor?" Often the answer, found later and quietly, is that the humble bank of per-node models was within noise of the graph, sometimes ahead of it. The graph was not wrong; it was unnecessary, which on a busy week is the same thing. The cure is not cynicism about graphs, it is running the boring baseline first, every time, so that when the graph does win you know it won something real.

The frontier of 2024 to 2026 is the arrival of spatio-temporal foundation models: large models pretrained across many networks and domains, then applied zero-shot or with light fine-tuning to a new graph. Where the temporal foundation models of Chapter 15 (TimesFM, Moirai, Chronos, MOMENT) pretrain on vast collections of univariate and multivariate series, the spatio-temporal analogues add the graph: models such as UniST and the GPD line of work pretrain spatio-temporal prediction across cities and sensor types so that a previously unseen network can be forecast with little or no local training data, and prompt-based and graph-foundation efforts (OpenCity, and the broader "graph foundation model" agenda surveyed in 2024 to 2025) push toward a single backbone that transfers across both the temporal and the relational structure. The open questions are exactly the ones a careful reader will already suspect: how to tokenize a graph whose node set and topology differ from anything seen in pretraining, how to transfer across genuinely different relational structures rather than merely different signals on a similar structure, and whether the strong-baseline lesson of the previous paragraph survives at foundation-model scale or is finally overturned by transfer. These are live, and they are where a reader entering the field in 2026 can contribute.

Research Frontier: Spatio-Temporal Foundation Models (2024 to 2026)

Three threads define the current frontier. Pretrain-and-transfer across networks: UniST (Yuan et al., 2024) and prompt-based spatio-temporal models pretrain on many cities and grids and forecast an unseen city with little local data, lifting the foundation-model recipe of Chapter 15 from series to spatio-temporal fields. Graph foundation models: OpenCity (2024) and the graph-foundation-model agenda surveyed across 2024 to 2025 aim for a single backbone that transfers across both topology and signal, tackling the hard problem of a node set and adjacency that change between pretraining and deployment. Continuous-time and event-driven dynamic graphs: temporal-graph-network successors and neural-ODE-on-graph models (building on the Neural CDE thread of Chapter 13) push past the discrete-snapshot assumption toward graphs that update the instant an event arrives, the regime epidemic and interaction data actually live in. The practitioner's read for 2026: the snapshot-plus-fixed-graph models of this chapter are mature and strong, and the open ground is transfer across networks and faithful continuous-time dynamics.

4. Synthesis of Part VII: The Ingredients of an Intelligent Temporal System Advanced

This section closes not only the chapter but Part VII, and it is worth gathering what the part built. Part VII asked what it takes to move from a model that predicts a temporal signal to a system that acts intelligently in a temporal world, and it answered with three ingredients, one per chapter.

Chapter 30 supplied reasoning: the ability to go beyond correlation in time to causal and counterfactual structure, to ask not merely what will happen but what would happen under an intervention, and to chain temporal facts into inferences. A system that cannot reason about cause cannot plan a deliberate action, only react to a forecast. Chapter 31 supplied agency: the loop of perceiving a temporal environment, maintaining memory and belief over time, deciding, acting, and learning from the consequence, the integration of the sequential-decision-making machinery of Part VI into a persistent agent that operates over an extended horizon. This chapter, Chapter 32, supplied spatio-temporal grounding: the ability to represent and predict a world that is not a single stream but a network of interacting, evolving entities, a world with structure in space as well as in time. Figure 32.5.3 draws the three as the legs of one system.

IntelligentTemporal System Reasoning & CausalityChapter 30 Temporal AgentsChapter 31 Spatio-Temporalgrounding (Ch 32) Part VIIITrustworthy & Deployed three ingredients combine into one system, which Part VIII makes safe and operable
Figure 32.5.3: The synthesis of Part VII. Reasoning (Chapter 30), agency (Chapter 31), and spatio-temporal grounding (Chapter 32) are the three ingredients of an intelligent temporal system; combine them and you have a system that understands cause, acts over a horizon, and is grounded in a structured, evolving world. Part VIII turns that capability into something safe, fair, and operable.

The three ingredients are not a checklist but a composition. A grid-operations agent (the sensor-and-IoT thread that runs through Chapter 8, Chapter 20, and this chapter) reasons causally about how a fault propagates, acts over a horizon to reroute load, and is grounded in the spatio-temporal graph of the network it manages. Take away the reasoning and it reacts blindly; take away the agency and it forecasts without ever acting; take away the spatio-temporal grounding and it treats a structured network as a bag of independent series and misses every cross-node effect. Part VII's claim is that intelligence in a temporal world needs all three, and that they reinforce one another. The chapter's place in the book is to be the spatial leg of that tripod, the part that insists the world has structure and that the structure carries signal.

Looking Back: Part VII Complete, and the Turn to Part VIII

With this section Chapter 32 closes, and with it Part VII, Building Intelligent Temporal Systems. Across three chapters the part assembled the ingredients of temporal intelligence: Chapter 30 gave the system the ability to reason about cause and counterfactual in time; Chapter 31 turned forecasting and decision-making into a persistent agent that perceives, remembers, decides, and acts over a horizon; and Chapter 32 grounded the agent in a spatio-temporal world of interacting, evolving entities, from traffic networks (Section 32.2) through skeletons (Section 32.4) to the general dynamic graph of this section. A system that reasons, acts, and is spatially grounded is, by the working definition of this book, an intelligent temporal system. But capability is not yet trustworthiness, and a system that is intelligent is not yet one you would deploy. Part VIII, Trustworthy and Deployed Temporal AI, takes up exactly that gap: Chapter 33 on interpretability, robustness, fairness, and privacy, asking whether we can understand, stress, and trust these systems; and Chapter 34 on deployment and MLOps, asking how to operate them reliably, monitor them under drift, and keep them working long after the paper is written. The arc of the book turns here from building intelligent temporal systems to making them safe, fair, and operable.

5. Worked Example: A Spatio-Temporal GNN Block, From Scratch and via PyG-Temporal Advanced

We now make the general update of subsection two executable. The plan is the from-scratch-then-library pattern of this book: build a spatio-temporal block by hand as a graph convolution wrapped in a temporal recurrence (the factorized-recurrent corner of the design space, DCRNN-style), train it on a small dynamic-graph forecasting task, then rebuild the same model in PyTorch-Geometric-Temporal and read off the line-count reduction. Code 32.5.1 is the from-scratch block: a normalized graph convolution for the spatial operator and a GRU-style gate for the temporal operator, both in plain PyTorch tensor ops.

import torch
import torch.nn as nn

torch.manual_seed(0)

def normalized_adjacency(A):
    """Symmetric-normalized adjacency with self-loops: D^-1/2 (A + I) D^-1/2."""
    A_hat = A + torch.eye(A.size(0))           # add self-loops so a node sees itself
    deg = A_hat.sum(dim=1)                      # node degrees
    d_inv_sqrt = torch.diag(deg.pow(-0.5))     # D^-1/2
    return d_inv_sqrt @ A_hat @ d_inv_sqrt     # the spatial diffusion operator

class STGNNBlock(nn.Module):
    """Spatio-temporal block: graph conv (space) inside a GRU-style update (time)."""
    def __init__(self, d_in, d_hidden):
        super().__init__()
        # Graph-conv weight feeds BOTH the candidate state and the two gates.
        self.W_in = nn.Linear(d_in, d_hidden)          # input projection
        self.W_g  = nn.Linear(d_hidden, d_hidden)      # graph-conv on hidden (spatial)
        self.W_z  = nn.Linear(2 * d_hidden, d_hidden)  # update gate
        self.W_r  = nn.Linear(2 * d_hidden, d_hidden)  # reset gate
        self.W_h  = nn.Linear(2 * d_hidden, d_hidden)  # candidate state

    def graph_conv(self, A_norm, x):
        # Spatial operator: aggregate neighbors via the normalized adjacency.
        return A_norm @ self.W_g(x)                     # (N, d_hidden)

    def forward(self, A_norm, x_t, h_prev):
        # x_t: (N, d_in) features this step; h_prev: (N, d_hidden) previous state.
        u = torch.tanh(self.W_in(x_t))                  # project input features
        gx = self.graph_conv(A_norm, h_prev)            # spatial mixing of past state
        cat = torch.cat([u, gx], dim=-1)                # fuse input and spatial message
        z = torch.sigmoid(self.W_z(cat))                # update gate
        r = torch.sigmoid(self.W_r(cat))                # reset gate
        cand = torch.tanh(self.W_h(torch.cat([u, r * gx], dim=-1)))
        h_t = (1 - z) * h_prev + z * cand               # temporal update (GRU form)
        return h_t

class STForecaster(nn.Module):
    """Run the block across time, then read out a one-step-ahead forecast per node."""
    def __init__(self, d_in, d_hidden):
        super().__init__()
        self.block = STGNNBlock(d_in, d_hidden)
        self.readout = nn.Linear(d_hidden, 1)           # per-node scalar forecast
        self.d_hidden = d_hidden

    def forward(self, A_norm, X):                       # X: (T, N, d_in)
        N = X.size(1)
        h = torch.zeros(N, self.d_hidden)               # initial node states
        for t in range(X.size(0)):                      # the temporal loop
            h = self.block(A_norm, X[t], h)             # space + time, one step
        return self.readout(h).squeeze(-1)              # (N,) forecast for next step
Code 32.5.1: The spatio-temporal block from scratch. graph_conv is the spatial operator (neighbor aggregation through the normalized adjacency); the GRU-style gates are the temporal operator, mixing the spatial message with the node's own past, exactly the $\psi$ of subsection two. The forecaster runs the block across all $T$ steps and reads out a one-step forecast per node.

Code 32.5.2 builds a tiny synthetic dynamic-graph task, a diffusion process on a small graph where each node's next value depends on its neighbors' current values, and trains the from-scratch forecaster on it. The point is not the dataset but that the block learns: the loss falls because the graph convolution lets each node exploit its neighbors, which a per-node model could not.

# Small fixed graph: a 6-node ring, edges between consecutive nodes.
N, d_in, T = 6, 1, 12
A = torch.zeros(N, N)
for i in range(N):
    A[i, (i + 1) % N] = 1.0
    A[i, (i - 1) % N] = 1.0
A_norm = normalized_adjacency(A)

# Synthetic data: a signal that diffuses along the ring over time.
def make_sequence():
    X = torch.zeros(T, N, d_in)
    X[0, :, 0] = torch.randn(N)                         # random initial signal
    for t in range(1, T):
        X[t, :, 0] = 0.5 * (A_norm @ X[t - 1, :, :]).squeeze(-1) + 0.1 * torch.randn(N)
    target = (A_norm @ X[-1, :, :]).squeeze(-1)         # next-step value to predict
    return X, target

model = STForecaster(d_in, d_hidden=16)
opt = torch.optim.Adam(model.parameters(), lr=0.02)
for epoch in range(300):
    X, target = make_sequence()
    pred = model(A_norm, X)
    loss = ((pred - target) ** 2).mean()                # per-node MSE
    opt.zero_grad(); loss.backward(); opt.step()
    if epoch % 100 == 0:
        print(f"epoch {epoch:3d}  loss {loss.item():.4f}")
print(f"final loss {loss.item():.4f}")
Code 32.5.2: Training the from-scratch block on a synthetic ring-graph diffusion task. The target is the next-step value, which genuinely depends on neighbors, so the spatial operator earns its keep; the falling loss confirms the block learns the cross-node structure.
epoch   0  loss 0.9143
epoch 100  loss 0.0418
epoch 200  loss 0.0231
final loss 0.0187
Output 32.5.2: The training loss falls from near 0.91 to under 0.02 over 300 epochs, evidence that the from-scratch spatio-temporal block learns the neighbor-dependent diffusion the task encodes.

Now the library equivalent. PyTorch-Geometric-Temporal ships the same factorized-recurrent architecture, A3TGCN and the closely related GConvGRU, as a single module: the normalized graph convolution, the gating, the self-loops, and the per-step recurrence are all internal. Code 32.5.3 rebuilds the same factorized-recurrent design (a graph-conv GRU) as Code 32.5.1 in a handful of lines.

import torch
from torch_geometric_temporal.nn.recurrent import GConvGRU

# edge_index is the (2, E) COO form of the same ring graph as Code 32.5.2.
edge_index = torch.tensor(
    [[i for i in range(N)] + [(i + 1) % N for i in range(N)],
     [(i + 1) % N for i in range(N)] + [i for i in range(N)]], dtype=torch.long)

class PyGTForecaster(torch.nn.Module):
    def __init__(self, d_in, d_hidden):
        super().__init__()
        self.recurrent = GConvGRU(d_in, d_hidden, K=1)   # graph-conv GRU, the whole block
        self.readout = torch.nn.Linear(d_hidden, 1)

    def forward(self, X, edge_index):                    # X: (T, N, d_in)
        h = None
        for t in range(X.size(0)):
            h = self.recurrent(X[t], edge_index, H=h)    # space + time in one call
        return self.readout(h).squeeze(-1)

model = PyGTForecaster(d_in=1, d_hidden=16)              # same task, ~5 lines of model code
Code 32.5.3: The PyTorch-Geometric-Temporal equivalent. The roughly 45 lines of hand-written block, normalized adjacency, graph convolution, three GRU gates, and the recurrence in Code 32.5.1, collapse to a single GConvGRU layer plus a linear readout, about 5 lines of model code. The library handles the adjacency normalization, the Chebyshev graph convolution (order K), the gating, and the per-step recurrence internally; only the temporal loop and readout remain visible.
Library Shortcut: PyTorch-Geometric-Temporal in a Handful of Lines

The from-scratch spatio-temporal block of Code 32.5.1 ran about 45 lines: the normalized-adjacency helper, the graph convolution, three gates, the candidate-state algebra, and the temporal loop. PyTorch-Geometric-Temporal collapses the entire block to a single recurrent layer, GConvGRU(d_in, d_hidden, K) (or A3TGCN, DCRNN, TGCN for other corners of the design space), about a 5-line model, a roughly ninefold reduction. The library handles the symmetric adjacency normalization, the Chebyshev graph convolution of order K, the GRU gating, the self-loops, and the masking for dynamic graphs internally, and it ships dataset loaders, temporal-signal splitters, and the discrete-snapshot and dynamic-graph iterators of Section 18.3. The "Right Tool" rule for spatio-temporal forecasting: prototype the block by hand once to understand it, then build on PyG-Temporal, sktime, or GluonTS so you spend your effort on the design-space choices of subsection two, not on re-deriving normalized graph convolution.

Read the three code blocks together. Code 32.5.1 built the general spatio-temporal update by hand, a spatial graph convolution wrapped in a temporal GRU, the factorized-recurrent corner of subsection two's design space. Code 32.5.2 trained it and watched the loss fall on a task whose target genuinely depends on neighbors, the regime where the graph helps. Code 32.5.3 rebuilt the same factorized-recurrent design (a graph-conv GRU) in five lines of PyTorch-Geometric-Temporal. The pedagogical payoff is the one this book repeats: the named spatio-temporal models are not black boxes but specializations of one update you can write yourself, and once you understand the update the library is a productivity tool, not a mystery.

Practical Example: Forecasting Load Across a Sensor Network

Who: A grid-operations analytics team at a regional utility, the sensor-and-IoT thread that runs through Chapter 8 and Chapter 20, forecasting electrical load at every substation in a network of a few hundred substations wired by the physical grid topology.

Situation: Load at each substation was already forecast by a per-node model, but the team suspected that load shifts propagate across the network (a transferred industrial load shows up downstream minutes later) and that a spatio-temporal graph model could exploit that propagation.

Problem: They needed to know whether the grid topology actually carried exploitable forecasting signal, or whether each substation's future was essentially its own past, in which case the graph model would add cost for nothing.

Dilemma: Build the elaborate spatio-temporal graph network and risk the confidence trick of the fun note (an impressive model that does not beat one forecaster per substation), or stay with the per-node baseline and risk leaving real cross-node signal on the table.

Decision: They ran the strong-baseline discipline of subsection three first: a bank of per-node models and a multivariate linear model, evaluated on the same split and metric in one pass, as the bar the graph model had to clear.

How: They built a factorized-recurrent block (the GConvGRU of Code 32.5.3) on the physical grid adjacency, then added a learned adaptive-adjacency term (the $\mathbf{E}_1\mathbf{E}_2^\top$ device of subsection two) to let the model discover influence the wiring diagram omitted, and compared all three on a rolling-origin evaluation.

Result: The graph model beat the per-node baseline at the short horizons where neighbor propagation mattered, by a margin that was real and survived the honest comparison, and the learned adjacency surfaced a few cross-region influences the physical topology did not encode; at long horizons the margin shrank toward the baseline, exactly as the scalability-and-baseline discussion predicts.

Lesson: The graph earns its keep only where genuine cross-node influence exists; prove that with a strong baseline before deploying the graph, and let a learned adjacency find the influence the given topology missed.

That closes the worked example, the chapter, and Part VII. We have named the general spatio-temporal graph, mapped the design space that organizes its models, stated the scalability and strong-baseline lessons that keep practice honest, surveyed the foundation-model frontier, synthesized the three ingredients of an intelligent temporal system, and built a spatio-temporal block by hand and by library. The exercises below ask you to extend each of these. Beyond them lies Part VIII, where the intelligent temporal systems of Part VII are made trustworthy and deployable.

Exercises

Exercise 32.5.1 (Conceptual): Placing Models in the Design Space

For each of the following, state its coordinate on all three axes of subsection two (spatial-temporal composition; graph dynamics; adjacency): (a) STGCN, (b) DCRNN, (c) GraphWaveNet, (d) a temporal-graph network for an evolving social-interaction stream. Then describe a problem for which the continuous-time + learned-adjacency corner is the right choice, and explain why the discrete-snapshot + fixed-adjacency corner would lose signal on it. Finally, identify which of the "three things that can move" (features, edges, the model's belief about edges) each of (a) to (d) lets move.

Exercise 32.5.2 (Implementation): Add a Learned Adjacency and Beat a Baseline

Starting from the from-scratch STForecaster of Code 32.5.1, (a) add a learned adaptive adjacency $\mathbf{A}_{\text{learned}} = \operatorname{softmax}(\operatorname{ReLU}(\mathbf{E}_1 \mathbf{E}_2^\top))$ with learnable node embeddings $\mathbf{E}_1, \mathbf{E}_2$, and use it alongside the fixed ring adjacency. (b) Implement the strong baseline of subsection three: one independent per-node linear forecaster on the recent window. (c) On the synthetic task of Code 32.5.2, compare the graph model (fixed adjacency), the graph model (fixed + learned adjacency), and the per-node baseline on the same held-out sequences, the same metric, computed in one pass. Report which wins and by how much, and relate the outcome to whether the task has genuine cross-node dependence. (d) Now weaken the cross-node coupling (replace the diffusion coefficient 0.5 with 0.05) and rerun: does the graph still beat the baseline?

Exercise 32.5.3 (Open-ended): The Synthesis as a System

Sketch the design of an intelligent temporal system for one concrete domain (grid operations, epidemic response, or autonomous-fleet coordination) that explicitly uses all three Part VII ingredients: reasoning and causality (Chapter 30), agency (Chapter 31), and spatio-temporal grounding (this chapter). Specify, for your domain, what the spatio-temporal graph is (nodes, edges, features, and whether the structure is fixed or dynamic), where the causal reasoning enters, and what the agent's action loop is. Then anticipate Part VIII: name one way the system could be unfair, opaque, or brittle, and one operational failure that would arise after deployment, and sketch how you would detect each.