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.AbstractCDM — Type
AbstractCDMAbstract type for Causal Dynamical Models (time-indexed structural models).
CausalDynamics.DiscreteTimeCDM — Type
DiscreteTimeCDMDiscrete-time Causal Dynamical Model with named endogenous variables.
Fields
variables: endogenous names (documentation / packing order)initialise:(rng) -> NamedTupleof initial endogenous values att = 1sample_noise:(rng, state, t) -> NamedTupleof exogenous draws for occasiontstep:(state, t, noise, intervention) -> NamedTuplenext 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.
CausalDynamics.CDMTrajectory — Type
CDMTrajectoryResult of simulating a DiscreteTimeCDM.
Fields
T: number of occasionsseries: endogenous trajectories (Symbol => Vector{<:Real})noise: realised exogenous draws (Symbol => Vector{<:Real})
CausalDynamics.AbstractIntervention — Type
AbstractInterventionDiscrete-time interventions for simulate / counterfactual: DoSequence and Policy.
CausalDynamics.DoSequence — Type
DoSequenceTime-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).
CausalDynamics.do_sequence — Function
do_sequence(variable::Symbol, values)Build a DoSequence fixing variable to values over time.
do_sequence(pairs::Pair{Symbol, <:Any}...)Build a DoSequence from variable => assignment pairs.
CausalDynamics.AbstractDoAssignment — Type
AbstractDoAssignmentTyped payload for a single variable in a DoSequence: constant, time series, or t -> value function.
CausalDynamics.ConstantAssignment — Type
ConstantAssignment(value)Hold a constant do(·) value for all occasions.
CausalDynamics.SeriesAssignment — Type
SeriesAssignment(values)Hold a time-indexed do(·) series (values[t] at occasion t).
CausalDynamics.TimedAssignment — Type
TimedAssignment(f)Hold a time-dependent do(·) rule f(t).
CausalDynamics.Policy — Type
PolicyState-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.
CausalDynamics.policy — Function
policy(variable::Symbol, rule)Build a Policy assigning variable via rule(state, t).
policy(pairs::Pair{Symbol, <:Any}...)Build a Policy from variable => rule pairs, each rule(state, t).
CausalDynamics.intervention_value — Function
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.
CausalDynamics.simulate — Function
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.
CausalDynamics.counterfactual — Function
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 lengthT)intervention:DoSequencefor the counterfactual worldinitial: optional endogenousNamedTupleatt = 1beforedo(default:cdm.initialisewith a fixed seed — pass factual initials when they matter)
CausalDynamics.GComputationResult — Type
GComputationResultMonte Carlo g-computation summary for an outcome under a fixed intervention.
Fields
mean: mean terminal outcome across replicate trajectoriesstd: standard deviation across replicatesn: number of replicatessamples: terminal outcome per replicate
CausalDynamics.g_computation — Function
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: aDiscreteTimeCDMT: number of occasions per replicateoutcome: endogenous variable symbol to summariseintervention:DoSequenceorPolicyn: replicate countrng: random source (advanced across replicates)reduce: series summary, e.g.last,Statistics.mean
Returns a GComputationResult.
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
steptype-stable. Return the sameNamedTuplefield names and element types on every call; a variable that isIntatt = 1andFloat64later forces boxed storage. Prefer0.0over0ininitialise. - Avoid allocation inside
step. Build the returnedNamedTupledirectly rather than assembling intermediate arrays or dictionaries. - Reuse noise for counterfactuals.
counterfactualcopies 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 singlerng, 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