Time¶
Time changes tensor fields before it changes topology.
The smallest temporal graph contract keeps one graph G and presents an
ordered sequence of node fields:
fixed topology G
|
+-------+-------+-------+
| | | |
X0 X1 X2 ...
| | |
v v v
Cell -> Cell -> Cell
| | |
H0 H1 H2
Each X_t has shape [N, F]. Each hidden state H_t has shape [N, H].
Node identity and row order stay stable across the sequence.
Batch lanes share one graph¶
A batch of windows adds leading axes; it does not copy topology:
values [B, L, N, H]
|
| node axis stays owned by Graph
v
[N, B * L * H]
|
| one destination-CSR sum
v
[N, B * L * H]
|
v
output [B, L, N, H]
Graph.sum accepts [..., N, H], moves N first, folds every independent
lane into feature width, runs the existing two-dimensional CSR operation once,
and restores the original axes. The graph and its device buffers remain shared.
Static scalar edge weights are also shared; their gradient sums over all lanes.
Graph.edge_values follows the same rule, so GINEConv can share one fixed
edge-feature tensor across batch and time lanes.
This is batching tensor fields over one graph. Batching different graphs, changing edge values per batch, and batched attention scores are separate contracts and remain unimplemented.
Recurrence is causal¶
One step is:
H_t may depend on the current and earlier snapshots, never a later one.
Unrolling ordinary tinygrad calls across an explicit Python sequence builds one
autograd graph through both space and time.
Training and evaluation splits must preserve that direction. A feature attached
to snapshot t must have been observable by t; putting future facts into an
earlier row is leakage even if the recurrent code is causal.
Windows make history explicit¶
StaticGraphTemporalSignal.batches() converts aligned snapshots into causal
sequence-to-one windows:
x [T, N, F] -- history L --> values [B, L, N, F]
y [T, N, Y] ----------------> target [B, N, Y]
values[b] = x[start:start + L]
target[b] = y[start + L - 1]
The last target aligns with the last input snapshot's declared label. For the
Chickenpox loader with lags=1, that label is the following week's value.
Splitting the signal before creating windows prevents any window from crossing
a split boundary.
The iterator keeps the final short batch. It creates each window batch from
L contiguous tensor slices and never duplicates topology.
Resolution belongs to the data¶
The cell sees order, not calendar meaning. Hourly, monthly, and quarterly snapshots use the same recurrence, but they are not interchangeable evidence. An application must retain the timestamp or interval represented by every position and must define how irregular gaps affect features and loss.
Missing state is not numeric zero. A future temporal data contract needs
explicit masks when observations may be absent. METR-LA is one concrete caller:
its reference protocol declares zero a missing-value sentinel, so
METRLA.observed exposes speed != 0 while retaining the raw reading tensor.
Fixed and changing graphs differ¶
Three cases should remain distinct:
The first case reuses one lowered graph and its device buffers. The second keeps edge identity but changes aligned values. The third changes connectivity and requires explicit rules for node identity, graph versions, hidden-state alignment, and cache lifetime.
Proving fixed-topology recurrence does not prove dynamic graphs.
Edge events precede dynamic snapshots¶
Some sources report changes as individual events rather than prebuilt graphs:
TemporalEdges owns only those aligned facts. It preserves direction,
duplicates, and equal timestamps. A strict causal prefix uses timestamp <
cutoff; the full stable node universe remains available so endpoint identity
never changes between prefixes.
from tinymesh import TemporalEdges
events = TemporalEdges(3, [0, 1, 0], [1, 0, 2], [10, 10, 20])
past = events.prefix(20)
assert past.source == (0, 1)
assert past.timestamp == (10, 10)
An event stream is source truth, not a sequence of Graph objects. Converting
it into first-contact, cumulative, active-window, directed, or undirected
topology requires task-specific semantics. CollegeMsg is the first public
caller: its dataset record retains original account IDs while its
TemporalEdges use compact endpoints.
A node-time mesh is a graph product¶
A fixed graph observed at ordered times has a conceptual joint domain:
The time-vertex framework writes its joint Laplacian as a Kronecker sum:
The default execution stays factorized: keep X[T,N,F], apply the spatial
operator over N, and advance causal state over T:
Topology and spatial transport remain proportional to the snapshots and sparse
support, O(T * (N + E) * H); learned feature projections add their ordinary
tensor cost. A changing edge field, delayed cross-time edge, or changing
topology needs its own aligned contract.
Some algorithms need the joint nodes to exchange messages directly. For a
bounded window, Graph.cartesian lowers that same product without constructing
a dense adjacency:
time = Graph(3, [0, 1], [1, 2])
space = Graph(2, [0, 1], [1, 0])
mesh = time.cartesian(space)
values = values.reshape(batch, time.nodes * space.nodes, features)
flat node (t, v) -> t * N + v
joint nodes T * N
joint edges E_T * N + T * E_G
time edge t -> u becomes (t, v) -> (u, v) for every v
space edge v -> w becomes (t, v) -> (t, w) for every t
Left-factor edges come first in COO order, each repeated across the right
factor's nodes; right-factor edges follow, each repeated across the left
factor's nodes. Callers can therefore align edge types and values without a
second topology map. For time.cartesian(space), temporal edges precede
spatial edges. This order belongs to that binary bracketing: regrouping three or
more factors preserves the directed edge multiset but can permute COO order, so
aligned edge values must be derived for the selected bracketing.
This explicit form costs O(TN + E_T N + T E_G) storage. Use it when a joint
message-passing rule needs it and keep long fixed-topology sequences
factorized. The Cartesian product is a lowering choice, not permission to
materialize an [TN,TN] matrix.
The fixed-graph signal¶
The first real dataset caller adds invariants that a tuple cannot carry:
StaticGraphTemporalSignal validates those axes, keeps topology in one owner,
and yields (x_t, y_t) in order or batched windows on request. Contiguous
temporal splits reuse the same graph and edge facts:
train, test = signal.split(0.8)
for x, y in train:
hidden = cell(x, train.graph, hidden)
values, target = next(train.batches(batch_size=32, history=8))
# values [B, L, N, F], target [B, N, Y]
The container does not claim more than its source. Chickenpox has ordered
weekly positions but no exact dates or missingness mask, so those are not
fabricated. METR-LA instead retains its exact timestamps and source-defined
missingness in a dataset-specific record; that evidence is not yet broad
enough to enlarge StaticGraphTemporalSignal. Read the
Chickenpox data record and
METR-LA data record for the concrete boundaries.
Where spatial mixing happens¶
The fixed-graph temporal components expose three architectural choices:
T-GCN graph-mix X_t, then combine node-local H_(t-1)
PeriodAttention
mix P same-shaped states; no graph assumption
A3T-GCN T-GCN each period from fixed H_0, then PeriodAttention
GConvGRU graph-mix X_t and H_(t-1) inside the gates
PeriodAttention is the composable temporal primitive: it can mix states from
any encoder without knowing whether they came from a graph, spectral filter, or
future multiscale operator. A3T-GCN mixes independent T-GCN period encodings
rather than carrying hidden state through them. GConvGRU lets a node's
remembered state affect neighboring nodes before the next update. Whether
either extra path helps is a model and data question, not a data-container
question. The graph cells consume the same explicit Graph, snapshots, and
hidden tensor.
The exact checked result lives in the T-GCN experiment. The controlled architectural comparison lives in the GConvGRU experiment. The first real batched forecast lives in the Chickenpox forecast.