SciML integration (recipes)
CausalDynamics.jl does not hard-depend on SciML solvers. Discrete-time trajectories live in DiscreteTimeCDM; continuous mechanisms belong in OrdinaryDiffEq / UniversalDiffEq / application code. This page records composition patterns used in the CDCS book.
Pattern: identify → simulate → intervene
- Structural —
TemporalDAGSpec+unroll_temporal_dagor a hand-builtDiGraph - Dynamical —
DiscreteTimeCDM+simulate/counterfactual, or anODEProblem - Observable — TMLE / RxInfer on adjustment sets from step 1
Discrete-time CDM (package-native)
using CausalDynamics, Random
cdm = DiscreteTimeCDM(
[:z, :x, :y];
initialise = (rng) -> (z = 0.0, x = 0.0, y = 0.0),
sample_noise = (rng, state, t) -> (u_z = randn(rng), u_x = randn(rng), u_y = randn(rng)),
step = (state, t, noise, intervention) -> begin
z = 0.9 * state.z + noise.u_z
x = intervention_value(intervention, :x, t, 0.5 * state.x + 0.3 * z + noise.u_x)
y = 0.5 * x + 0.2 * z + noise.u_y
(z = z, x = x, y = y)
end,
)
traj = simulate(cdm, 100; rng = Random.Xoshiro(1))
cf = counterfactual(cdm, traj.noise; intervention = do_sequence(:x, fill(1.0, 100)))Executable recipe: examples/discrete_cdm.jl.
Estimation without leaving the package
For simulated systems, contrast interventional means directly with g_computation — no estimation dependency required:
treated = g_computation(cdm, 100, :y; intervention = do_sequence(:x, 1.0), n = 500)
control = g_computation(cdm, 100, :y; intervention = do_sequence(:x, 0.0), n = 500)
effect = treated.mean - control.meanState-dependent strategies use policy instead of a fixed sequence:
π = policy(:x, (state, t) -> state.z > 0 ? 1.0 : 0.0)
under_policy = g_computation(cdm, 100, :y; intervention = π, n = 500)For observational data (not simulation), use the TMLE or RxInfer bridges with adjustment sets from identification.
Bridge to ODEs (application code)
When the mechanism is continuous, keep causal structure in CausalDynamics and integration in SciML. Load OrdinaryDiffEq to activate CausalDynamicsSciMLExt:
using CausalDynamics, Graphs, OrdinaryDiffEq
g = DiGraph(3)
add_edge!(g, 1, 2) # Z → X
add_edge!(g, 1, 3) # Z → Y
add_edge!(g, 2, 3) # X → Y
adj = backdoor_adjustment_set(g, 2, 3)
spec = ContinuousCDMSpec([:Z, :X, :Y])
function cdm_dynamics!(du, u, p, t)
Z, X, Y = u
du[1] = p.λ_z * Z
du[2] = p.α * X + p.β * Z
du[3] = p.γ * Y + p.δ * X + p.ε * Z
end
p = (λ_z = -0.1, α = -0.2, β = 0.3, γ = -0.1, δ = 0.5, ε = 0.2)
sol = solve_cdm(spec, cdm_dynamics!, [1.0, 0.5, 0.2], (0.0, 10.0), p)
terminal_state(spec, sol) # NamedTuple(:Z, :X, :Y)
# Static do(·): SciML-native hard pin (IC + du=0 + DiscreteCallback)
do_x = do_pin(:X, 1.0)
sol_do = solve_cdm(spec, cdm_dynamics!, [1.0, 0.5, 0.2], (0.0, 10.0), p; intervention = do_x)Helpers: ContinuousCDMSpec, ode_problem_cdm, solve_cdm, terminal_state, state_series, interventional_rhs, intervention_callback, do_pin, do_ic, do_force, do_rhs.
Intervention types share AbstractCausalIntervention:
- discrete CDM:
AbstractIntervention(DoSequence,Policy) - continuous CDM:
AbstractContinuousIntervention(DoPin,DoInitialCondition, …) - static SCM:
DoIntervention
Hard pins use SciML DiscreteCallback to reassert the value after accepted steps (no in-RHS mutation of u). Soft force and RHS replacement modify du only. Optional parents on ContinuousCDMSpec record the continuous causal parent graph (continuous_cdm_graph). Executable recipe: examples/sciml_cdm_recipe.jl.
UniversalDiffEq
Use CausalDynamics for adjustment / do semantics; use UniversalDiffEq.jl to learn f from series. Do not expect UDE training inside CausalDynamics core.
ODE parent ranking across environments (infer_ode_parents; CausalKinetiX reference method [@pfister2019causalkinetix]) is documented in Methods adoption and bridges into ContinuousCDMSpec via ode_parent_ranking_to_continuous_spec.
Version note
- 0.2 —
DiscreteTimeCDM, time-indexed unrolling helpers - 0.3 —
CausalDynamicsSciMLExtweakdep (ContinuousCDMSpec,solve_cdm,do(·)RHS wrapper) - 0.3.x — unified
AbstractCausalIntervention; SciML-nativeDoPin(DiscreteCallback); IEE →TemporalDAGSpec