Discrete-time CDMs

Causal Dynamical Models advance named endogenous state over discrete occasions t = 1:T. Use DiscreteTimeCDM with simulate for observational or interventional trajectories, and counterfactual to reuse realised exogenous draws under an alternate DoSequence.

CausalDynamics.DiscreteTimeCDMType
DiscreteTimeCDM

Discrete-time Causal Dynamical Model with named endogenous variables.

Fields

  • variables: endogenous names (documentation / packing order)
  • initialise: (rng) -> NamedTuple of initial endogenous values at t = 1
  • sample_noise: (rng, state, t) -> NamedTuple of exogenous draws for occasion t
  • step: (state, t, noise, intervention) -> NamedTuple next endogenous state

The step function should use intervention_value for intervenable assignments. In simulate, initialise produces t = 1 (then do is applied); step is called for t = 2:T.

source
CausalDynamics.DoSequenceType
DoSequence

Time-indexed do(·) assignments. Each key is an endogenous variable symbol; each value is a typed AbstractDoAssignment (constructed automatically from a scalar, an AbstractVector indexed by t, or a function (t) -> value).

source
CausalDynamics.do_sequenceFunction
do_sequence(variable::Symbol, values)

Build a DoSequence fixing variable to values over time.

source
do_sequence(pairs::Pair{Symbol, <:Any}...)

Build a DoSequence from variable => assignment pairs.

source
CausalDynamics.PolicyType
Policy

State-dependent (soft) intervention. Each key is an endogenous variable symbol; each value is a Function rule (state, t) -> value evaluated against the current state before the update. Use for treatment strategies that react to the system, where DoSequence fixes a value independently of state.

source
CausalDynamics.policyFunction
policy(variable::Symbol, rule)

Build a Policy assigning variable via rule(state, t).

source
policy(pairs::Pair{Symbol, <:Any}...)

Build a Policy from variable => rule pairs, each rule(state, t).

source
CausalDynamics.intervention_valueFunction
intervention_value(intervention, variable, t, observational_value)
intervention_value(intervention, variable, t, observational_value, state)

Return the interventional assignment for variable at time t when present in intervention, otherwise observational_value.

Pass state (the current endogenous NamedTuple) to support Policy rules; DoSequence ignores it.

source
CausalDynamics.simulateFunction
simulate(cdm::DiscreteTimeCDM, T; rng=..., intervention=nothing)

Simulate a discrete-time CDM for T occasions.

Returns a CDMTrajectory. When intervention is a DoSequence, assignments are applied at t = 1 to the initial state and passed into step for t ≥ 2 (use intervention_value inside step).

Note: at t = 1, only fields named in the DoSequence are overwritten; child variables are not re-solved until step runs for t ≥ 2. Encode any t=1 downstream effects in initialise if needed.

source
CausalDynamics.counterfactualFunction
counterfactual(cdm, noise; intervention, initial=nothing)

Resimulate cdm under intervention using fixed exogenous draws noise (typically factual.noise from a prior simulate).

Arguments

  • noise: Dict{Symbol, Vector{<:Real}} of realised exogenous series (common length T)
  • intervention: DoSequence for the counterfactual world
  • initial: optional endogenous NamedTuple at t = 1 before do (default: cdm.initialise with a fixed seed — pass factual initials when they matter)
source
CausalDynamics.GComputationResultType
GComputationResult

Monte Carlo g-computation summary for an outcome under a fixed intervention.

Fields

  • mean: mean terminal outcome across replicate trajectories
  • std: standard deviation across replicates
  • n: number of replicates
  • samples: terminal outcome per replicate
source
CausalDynamics.g_computationFunction
g_computation(cdm, T, outcome; intervention, n=1000, rng=..., reduce=last)

Estimate E[outcome ∣ do(intervention)] by simulating n trajectories of length T.

Each replicate is summarised by reduce applied to the outcome series (default last, the terminal occasion). Contrast two calls to obtain an interventional effect, e.g. do_sequence(:a, 1.0) versus do_sequence(:a, 0.0).

Arguments

  • cdm: a DiscreteTimeCDM
  • T: number of occasions per replicate
  • outcome: endogenous variable symbol to summarise
  • intervention: DoSequence or Policy
  • n: replicate count
  • rng: random source (advanced across replicates)
  • reduce: series summary, e.g. last, Statistics.mean

Returns a GComputationResult.

source

Minimal example

using CausalDynamics
using Random

cdm = DiscreteTimeCDM(
    [:x, :y];
    initialise = (rng) -> (x = 1.0, y = 1.0),
    sample_noise = (rng, state, t) -> (u_x = randn(rng), u_y = 0.1 * randn(rng)),
    step = (state, t, noise, intervention) -> begin
        x = 0.5 * state.x + noise.u_x
        y = x + noise.u_y
        (x = x, y = y)
    end,
)

factual = simulate(cdm, 20; rng = Random.Xoshiro(1))
cf = counterfactual(
    cdm,
    factual.noise;
    intervention = do_sequence(:x, fill(0.0, 20)),
    initial = (x = 1.0, y = 1.0),
)

Inside step, call intervention_value for any intervenable assignment so the same structural map works under observation and do(·).

Soft interventions (policies)

A DoSequence fixes a value regardless of state. A Policy assigns via a rule (state, t) -> value, so treatment can react to the system. Pass state as the fifth argument to intervention_value:

step = (state, t, noise, intervention) -> begin
    a = intervention_value(intervention, :a, t, 0.5 * state.a + noise.u_a, state)
    (a = a, y = 2a + noise.u_y)
end

# Treat only when the previous action was non-positive
π = policy(:a, (state, t) -> state.a <= 0 ? 1.0 : 0.0)
traj = simulate(cdm, 20; intervention = π)

Performance notes

simulate preallocates one vector per variable (length T) from the element types returned by initialise and sample_noise, so the cost of a run is dominated by your step function.

  • Keep step type-stable. Return the same NamedTuple field names and element types on every call; a variable that is Int at t = 1 and Float64 later forces boxed storage. Prefer 0.0 over 0 in initialise.
  • Avoid allocation inside step. Build the returned NamedTuple directly rather than assembling intermediate arrays or dictionaries.
  • Reuse noise for counterfactuals. counterfactual copies the supplied noise once; it does not resample, so contrasting many interventions against one factual run is cheap.
  • Batch with g_computation. Replicates advance a single rng, so results are reproducible from one seed without allocating a generator per replicate.

Interventional means (g-computation)

g_computation estimates E[outcome ∣ do(·)] by Monte Carlo over replicate trajectories. Contrast two interventions for an effect:

treated = g_computation(cdm, 20, :y; intervention = do_sequence(:a, 1.0), n = 500)
control = g_computation(cdm, 20, :y; intervention = do_sequence(:a, 0.0), n = 500)
effect = treated.mean - control.mean