19  Multi-Scale Causal Dynamics

Multi-Scale Causal Dynamics

Status: In Progress

v0.2

19.1 Why Multi-Scale Modelling?

Complex dynamical systems often operate across organisational scales. In biology that hierarchy may run molecular → cellular → tissue → organ → organism → population; analogous nestings appear in climate (local convection → regional circulation), cities (agents → neighbourhoods → metro regions), and engineered systems (components → modules → plant). Mechanisms at one scale produce emergent behaviours at another.

A complete causal dynamical model (CDM) must sometimes bridge those scales. A drug effect measured in cell culture (micro) may need to predict organism-level response (macro); a controller tuned on a subsystem may fail when coupled into a plant. Without a principled framework for cross-scale reasoning, we cannot say when causal conclusions at one scale transport to another.

Multi-scale modelling asks: when do causal structures at one level of description imply or constrain causal structures at another? The micro-scale captures finer-grained mechanisms; the macro-scale captures coarser patterns (often defined by aggregation or coarse-graining).

In the optional process vocabulary (Chapter 9), micro-level occasions contribute to macro-level descriptions. A macro variable may describe the state of an enduring object, such as a tissue or organ, only when an explicit continuity criterion and scale map justify that identity. A Whiteheadian account further treats the object as constituted by an ordered society of occasions, potentially including societies at lower scales. Cross-scale transport asks when the mechanism or estimand remains valid after aggregation; it does not follow merely from similarity of summaries.

Simondon’s language of individuation is useful here because it resists treating the macro individual as a pre-given container for micro events (Simondon 2020). In CDCS, however, its practical consequence is modest and explicit: a macro referent requires a declared scale map, constitution relation, and criterion of continuity before it can support a causal or interventional claim.

19.2 Hierarchical State-Space Models

We extend the state-space models of Chapter 12 to multiple nested scales. A hierarchical SSM couples latent states at different timescales and resolutions.

19.2.1 Slow/Fast Decomposition

A natural decomposition separates macro-level (slow) states \(X^{(M)}_t\) from micro-level (fast) states \(X^{(m)}_t\). The macro state evolves on a slower timescale and may depend on aggregated micro-level information; the micro state evolves faster and is modulated by the macro context.

19.2.2 Mathematical Formulation

Macro-level dynamics: \[ X^{(M)}_{t+1} = f^{(M)}\left(X^{(M)}_t, \bar{X}^{(m)}_t, \theta^{(M)}\right) + w^{(M)}_t \]

where \(\bar{X}^{(m)}_t\) denotes an aggregation of micro-level states (e.g., mean, sufficient statistic) that enters the macro dynamics.

Micro-level dynamics: \[ X^{(m)}_{t+1} = f^{(m)}\left(X^{(m)}_t, X^{(M)}_t, \theta^{(m)}\right) + w^{(m)}_t \]

The micro dynamics depend on the current macro state \(X^{(M)}_t\), encoding top-down modulation.

Observation model: \[ Y_t = h\left(X^{(M)}_t, X^{(m)}_t\right) + v_t \]

Observations may arise from either or both scales, depending on the measurement protocol.

19.2.3 Nested units are not scale maps

Repeated units within clusters are often called “multilevel”, but that sampling hierarchy is not by itself a map from micro states to a macro state. In CDCS, generative hierarchy means a shared cluster-level exogenous contribution alongside unit-level noise. Cluster-robust estimation belongs to CausalTargeted; partial pooling is an estimator choice in an application model. Neither supplies the state map \(\tau\) or intervention map \(\omega\) required for a cross-scale causal claim.

19.2.4 Relational structural dynamics

Hierarchical nesting is not the only way that a complex system changes its composition. A relational system may contain different numbers of entities and different relation instances in different situations. A traffic scene may gain a car or lose a pedestrian; an ecological community may contain different host and vector counts; a tissue may contain different interacting cell types. A fixed DAG over named variables does not by itself express this combinatorial variation.

A relational structural causal model separates a schema from a skeleton. The schema declares entity types, attributes, and admissible relations. The skeleton declares the entities and relation instances present in one situation. Attribute mechanisms can then use relational parents, such as the cross state of pedestrians whose path intersects a car. Grounding a relational model produces an ordinary causal graph for the declared situation, after which the existing identification and temporal machinery applies (Ejaz and Bareinboim 2026).

This distinction matters for CDCS. The schema is a reusable structural pattern; the skeleton is the realised composition; and the grounded graph is the situation on which a causal query is evaluated. Generalisation to an unseen skeleton therefore requires assumptions about mechanism invariance, aggregation, and unobserved confounding. Similarity of relational neighbourhoods is not itself a transport certificate. The package prototype below keeps that boundary explicit: it grounds to a Graphs.jl graph and does not create a second identification engine.

include(joinpath(dirname(Base.active_project()), "scripts", "ensure_packages.jl"))
@auto_using CausalDynamics Graphs

schema = RelationalSchema(
    [:signal, :car, :pedestrian],
    Dict(:controls => (:signal, :car), :path => (:pedestrian, :car)),
    Dict(:signal => [:walk], :car => [:brake], :pedestrian => [:cross]),
)
skeleton = RelationalSkeleton(
    schema,
    Dict(:signal => [:s1], :car => [:c1, :c2], :pedestrian => [:p1]),
    Dict(:controls => [(:s1, :c1)], :path => [(:p1, :c1)]),
)
grounded = ground_relational_graph(skeleton, [
    RelationalParent(:signal, :walk, :car, :brake, :controls),
    RelationalParent(:pedestrian, :cross, :car, :brake, :path),
])

println("Grounded variables: ", grounded.node_labels)
println("Grounded edges: ", collect(edges(grounded.graph)))
Grounded variables: [(:signal, :s1, :walk), (:car, :c1, :brake), (:car, :c2, :brake), (:pedestrian, :p1, :cross)]
Grounded edges: Graphs.SimpleGraphs.SimpleEdge{Int64}[Edge 1 => 2, Edge 4 => 2]

The following example separates these roles. It simulates shared cluster-level variation, then unrolls the unit template into a DAG on which identification is performed. The resulting certificate states the extra assumptions introduced by nested units.

book_root = let
    current = pwd()
    while !isfile(joinpath(current, "Project.toml")) && !isfile(joinpath(current, "_quarto.yml"))
        parent = dirname(current)
        parent == current && break
        current = parent
    end
    current
end
include(joinpath(book_root, "scripts", "ensure_packages.jl"))
@auto_using CausalDynamics Graphs Random

columns, truth = simulate_hierarchical_intercept_ate(
    80;
    n_clusters = 8,
    rng = MersenneTwister(15),
)

hierarchy_spec = HierarchicalNestingSpec(
    [:W, :A, :Y],
    [(:W, :A), (:W, :Y), (:A, :Y)];
    cluster_variable = :U,
    affects = [:Y],
)
unrolling = unroll_hierarchical_dag(hierarchy_spec, 4; n_clusters = 2)
a₁ = hierarchical_node(unrolling, (:unit, 1, :A))
y₁ = hierarchical_node(unrolling, (:unit, 1, :Y))
certificate = attach_hierarchy_assumptions(
    identify(unrolling.graph, TotalEffectQuery(a₁, y₁)),
)

println("Known ATE: ", truth.ate)
println("Cluster count: ", length(unique(columns[:cluster])))
println("Identification assumptions: ", certificate.assumptions)
Known ATE: 0.5
Cluster count: 8
Identification assumptions: [:no_unmeasured_confounding, :correct_graph, :nested_exogenous, :no_interference_across_clusters]

This supports a generative and identification claim about nested units. It does not imply that a cluster is the same individual as its units, nor that a random-intercept fit estimates a macro intervention.

19.2.5 Two-Scale Linear-Gaussian Example

The following Julia code illustrates the structure of a two-scale Gaussian Linear Dynamical System (GLDS). The macro level has slow dynamics; the micro level has fast dynamics modulated by the macro state.

# Two-scale GLDS: macro (slow) and micro (fast) dynamics
# Macro: x_M(t+1) = A_M * x_M(t) + B_M * x̄_m(t) + w_M
# Micro: x_m(t+1) = A_m * x_m(t) + B_m * x_M(t) + w_m
# Observation: y_t = H_M * x_M + H_m * x_m + v

project_root = let
    current = pwd()
    while !isfile(joinpath(current, "Project.toml")) && !isfile(joinpath(current, "_quarto.yml"))
        parent = dirname(current)
        parent == current && break
        current = parent
    end
    current
end
include(joinpath(project_root, "scripts", "book_bootstrap.jl"))
@auto_using LinearAlgebra Distributions Random Statistics

# Dimensions
n_M = 2   # macro state dimension
n_m = 3   # micro state dimension
n_y = 2   # observation dimension

# Macro dynamics (slow): decay + coupling from micro mean
α_M = 0.95  # slow decay
A_M = α_M * I(n_M)
B_M = 0.1 * ones(n_M)  # coupling coefficients from scalar micro mean to macro

# Micro dynamics (fast): decay + modulation by macro
α_m = 0.7   # faster decay
A_m = α_m * I(n_m)
B_m = 0.2 * ones(n_m, n_M)  # top-down modulation

# Observation: both scales contribute
H_M = [1.0 0.0; 0.0 1.0]
H_m = [0.5 0.5 0.0; 0.0 0.5 0.5]

# Noise covariances
σ_w_M = 0.1
σ_w_m = 0.2
σ_v = 0.15
Q_M = (σ_w_M^2) * I(n_M)
Q_m = (σ_w_m^2) * I(n_m)
R = (σ_v^2) * I(n_y)

# Simulate T steps
T = 100
rng = Random.MersenneTwister(42)
x_M = zeros(n_M, T+1)
x_m = zeros(n_m, T+1)
y = zeros(n_y, T)

x_M[:, 1] = rand(rng, MvNormal(zeros(n_M), I(n_M)))
x_m[:, 1] = rand(rng, MvNormal(zeros(n_m), I(n_m)))

for t in 1:T
    x̄_m = mean(x_m[:, t])
    x_M[:, t+1] = A_M * x_M[:, t] + B_M .* x̄_m + rand(rng, MvNormal(zeros(n_M), Q_M))
    x_m[:, t+1] = A_m * x_m[:, t] + B_m * x_M[:, t] + rand(rng, MvNormal(zeros(n_m), Q_m))
    y[:, t] = H_M * x_M[:, t] + H_m * x_m[:, t] + rand(rng, MvNormal(zeros(n_y), R))
end

# Structure summary
println("Two-scale GLDS: macro dim=$n_M, micro dim=$n_m, obs dim=$n_y")
println("Macro decay α_M=$α_M (slow), micro decay α_m=$α_m (fast)")
Two-scale GLDS: macro dim=2, micro dim=3, obs dim=2
Macro decay α_M=0.95 (slow), micro decay α_m=0.7 (fast)

Two-scale GLDS: macro (slow) and micro (fast) latent states over time

19.3 Coarse-Graining and Emergence

When does a micro-level causal structure imply a macro-level causal structure? This question connects to renormalisation group ideas in physics: as we coarse-grain, what information is lost and what effective theory remains?

19.3.1 Coarse-Graining Operators

A state coarse-graining map \(\tau: \mathcal{X}_m \to \mathcal{X}_M\) projects micro-level states to macro-level states. It must be accompanied by an intervention map \(\omega:\mathcal I_m\to\mathcal I_M\): a macro intervention is generally not determined by aggregation alone. Common state maps include:

  • Aggregation: Sum or count over micro units (e.g., total cell count)
  • Averaging: \(\bar{X}^{(M)} = \frac{1}{N}\sum_{i=1}^N X^{(m)}_i\)
  • Projection: Linear or nonlinear projection onto a lower-dimensional subspace
  • Sufficient statistics: Moments, order parameters, or other statistics that capture macro-relevant information

19.3.2 Causal Consistency

For exact causal abstraction, interventionally induced distributions—not the syntactic \(do(\cdot)\) operator—must commute (Rubenstein et al. 2017):

\[ \tau_{\#}P_m^{\,i}=P_M^{\,\omega(i)} \qquad \text{for every declared } i\in\mathcal I_m, \]

where \(\tau_{\#}\) denotes push-forward. For approximate abstraction, report

\[ \epsilon(\tau,\omega)=\sup_{i\in\mathcal I_m}D\!\left(\tau_{\#}P_m^{\,i},P_M^{\,\omega(i)}\right). \]

The choice of \(D\) (for example total variation or a decision-relevant loss), intervention domain and tolerance are part of the causal claim. Different micro-realisations with the same mean can have different macro consequences.

abstraction_root = let
    current = pwd()
    while !isfile(joinpath(current, "Project.toml")) && !isfile(joinpath(current, "_quarto.yml"))
        parent = dirname(current)
        parent == current && break
        current = parent
    end
    current
end
include(joinpath(abstraction_root, "scripts", "ensure_packages.jl"))
@auto_using CausalDynamics

# The package checks a declared state map τ and intervention map ω on supplied
# finite interventional laws. It does not discover those maps.
τ = x -> x == 0 ? :low : :high
ω = i -> i
abstraction = CausalAbstractionSpec(τ, ω; interventions = [:dose], law_mode = :exact)

micro_laws = Dict(:dose => FiniteLaw([0, 1], [0.25, 0.75]))
macro_laws = Dict(:dose => FiniteLaw([:low, :high], [0.25, 0.75]))
abstraction_check = validate_abstraction(abstraction, micro_laws, macro_laws)

@assert abstraction_check.exact
@assert abstraction_check.accepted
@assert abstraction_check.discrepancy == 0.0

The same contract should be evaluated on a failure case before a macro claim is reported: it is a test of a declared abstraction, not a procedure for discovering one.

19.3.3 Information Loss and Effective Theories

Coarse-graining entails information loss. The macro model is an effective theory, valid for certain questions at the macro scale, but not a complete description. The renormalisation group teaches us that effective theories can be causally consistent for a range of scales and questions, even when the full micro structure is unknown.

19.4 Cross-Scale Transportability

Transportability asks: when can causal conclusions from one domain be transferred to another? (See Chapter 8 on transportability.) Cross-scale transportability extends this to scales: when can causal conclusions at the micro scale be transported to the macro scale (or vice versa)?

19.4.1 Cross-Scale Selection Diagrams

We extend Pearl’s selection diagrams to include scale variables \(S\) that indicate which scale we observe or intervene at. Edges from \(S\) to a variable indicate that the mechanism for that variable differs across scales. For example, \(S \to Y\) means the observation process differs (we might observe \(Y^{(m)}\) in cells vs \(Y^{(M)}\) in organisms).

19.4.2 Formal Conditions for Cross-Scale Identifiability

A causal effect \(P(Y^{(M)} \mid \text{do}(A^{(M)}))\) is cross-scale identifiable from micro-level data and assumptions when:

  1. The effect is supported by a declared state map \(\tau\), intervention map \(\omega\) and micro intervention domain
  2. Exchangeability and cross-scale confounding assumptions are stated and defended, rather than treated as testable from observations alone
  3. The observation/intervention protocols at each scale are encoded in the graph

19.4.3 Example: Drug Effect from Cells to Organisms

A drug effect measured in cell culture (\(A^{(m)} \to Y^{(m)}\)) may or may not transport to organism-level outcome (\(A^{(M)} \to Y^{(M)}\)). Transportability requires:

  • The cellular mechanism is invariant (or its variation is modelled)
  • The coarse-graining from cellular response to organism response is known or identifiable
  • No scale-specific confounders (e.g., organism-level factors that affect outcome but not cell-level measurements)

19.5 Practical Implementation

19.5.1 Building a Two-Scale SSM Structure

The following code defines a struct representing the two-scale SSM and shows how to organise parameters for simulation.

# Struct for two-scale SSM (conceptual structure)
struct TwoScaleSSM
    n_M::Int
    n_m::Int
    n_y::Int
    f_M  # macro dynamics function
    f_m  # micro dynamics function
    h    # observation function
    θ_M  # macro parameters
    θ_m  # micro parameters
end

# Example: linear two-scale with aggregation
function macro_dynamics(x_M, x̄_m, θ)
    A_M, B_M, Q_M = θ
    A_M * x_M + B_M * x̄_m
end

function micro_dynamics(x_m, x_M, θ)
    A_m, B_m, Q_m = θ
    A_m * x_m + B_m * x_M
end

function observation(x_M, x_m, θ)
    H_M, H_m, R = θ
    H_M * x_M + H_m * x_m
end

# Parameter setup
n_M, n_m, n_y = 2, 3, 2
θ_M = (0.95 * I(2), 0.1 * ones(2, 3), 0.01 * I(2))
θ_m = (0.7 * I(3), 0.2 * ones(3, 2), 0.04 * I(3))
θ_h = (I(2), [0.5 0.5 0.0; 0.0 0.5 0.5], 0.02 * I(2))

model = TwoScaleSSM(n_M, n_m, n_y, macro_dynamics, micro_dynamics, observation, θ_M, θ_m)
println("Two-scale SSM: macro=$n_M, micro=$n_m, obs=$n_y")
Two-scale SSM: macro=2, micro=3, obs=2

19.5.2 Coarse-Graining a Micro-Level DAG to Macro-Level

Coarse-graining a causal graph aggregates micro nodes into macro nodes while preserving an edge when the corresponding macro-level influence exists.

# Coarse-graining: map micro DAG to macro DAG
# Micro: A1→B1, A2→B2, A1→A2 (3 nodes each in two groups)
# Macro: A→B with A = {A1,A2}, B = {B1,B2}

project_root = let
    current = pwd()
    while !isfile(joinpath(current, "Project.toml")) && !isfile(joinpath(current, "_quarto.yml"))
        parent = dirname(current)
        parent == current && break
        current = parent
    end
    current
end
include(joinpath(project_root, "scripts", "ensure_packages.jl"))
@auto_using Graphs

# Micro-level DAG
g_micro = SimpleDiGraph(6)
# Group 1: A1(1), B1(2); Group 2: A2(3), B2(4); cross-group A1(1)→A2(3)
add_edge!(g_micro, 1, 2)  # A1 → B1
add_edge!(g_micro, 3, 4)  # A2 → B2
add_edge!(g_micro, 1, 3)  # A1 → A2

# Partition: {1,3} → macro A, {2,4} → macro B
partition = [[1, 3], [2, 4]]

# Macro graph: edge A→B exists if any micro edge from group 1 to group 2
g_macro = SimpleDiGraph(2)
has_edge_1_to_2 = any(has_edge(g_micro, i, j) for i in partition[1], j in partition[2])
if has_edge_1_to_2
    add_edge!(g_macro, 1, 2)
end

println("Micro graph: ", ne(g_micro), " edges")
println("Macro graph: ", ne(g_macro), " edges (A→B)")
Micro graph: 3 edges
Macro graph: 1 edges (A→B)

19.5.3 Checking cross-scale transportability

A directed path is not a transportability criterion. In particular, a scale indicator in a selection diagram is not automatically an ordinary covariate to adjust for. The required argument combines the declared \((\tau, \omega)\) abstraction contract, the intervention domain, and the invariances or selection mechanisms that connect the source and target scales. The transportability calculus and selection-diagram conditions are developed in Chapter 8; the CausalAbstractionSpec check above evaluates only the supplied finite interventional laws. It does not discover a scale map or establish that an organism-level intervention is represented by a cell-level one.

19.6 Connection to the Structural, Dynamical, and Observable Strata

Multi-scale modelling touches all three layers at each scale:

19.6.1 Structural Layer

Multi-scale DAGs with cross-scale edges encode which macro variables influence which micro variables (top-down) and how micro variables aggregate to influence macro variables (bottom-up). Coarse-graining corresponds to moving from a finer structural description to a coarser one, while preserving causal semantics when the coarse-graining map commutes with intervention.

19.6.2 Dynamical Layer

Hierarchical SSMs provide the dynamical counterpart: scale-specific transition functions \(f^{(M)}\) and \(f^{(m)}\) with cross-scale coupling. The macro dynamics \(f^{(M)}\) depend on an aggregation of micro states; the micro dynamics \(f^{(m)}\) depend on the macro state. Timescale separation (slow/fast) reflects different rates of change at different scales.

19.6.3 Observable Stratum

Measurements at different scales correspond to different observation functions \(h\). Cell-level assays observe \(Y^{(m)}\); organism-level outcomes observe \(Y^{(M)}\). The observation model \(Y_t = h(X^{(M)}_t, X^{(m)}_t) + v_t\) may mix both scales when measurements aggregate (e.g., tissue biopsy averaging over cells).

Multi-scale modelling then adds a further question: when we coarse-grain or transport causal claims, how do these layers align across scales?