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. Discrete panels use simulate_panel; inferred latents use ObservationBridge / panel_from_latent_series.
Continuous CDMs support the same Monte Carlo idea via ContinuousEffectFunctional (:terminal, :mean, :integral):
using OrdinaryDiffEq
spec = ContinuousCDMSpec([:x, :y])
# … define rhs! …
g_do = g_computation(
spec, rhs!, rng -> [1.0, 0.0], (0.0, 5.0), p;
intervention = do_pin(:x, 1.0),
functional = ContinuousEffectFunctional(:y; kind = :terminal),
n = 200,
)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.
Representation bridge (high-dim → codes)
Before SciML or estimation on imaging/spectral inputs, compress with RepresentationSpec / encode_to_panel so the DAG and nuisance models see low-dim codes, not raw tensors. Roles :measurement vs :definitional are recorded in representation_certificate. Example: examples/representation_bridge.jl.
Graph-constrained Lux mechanisms (Phase 2a)
Parent-constrained neural residuals attach via MechanismLibrary / attach_lux_mechanism! (requires Lux + ComponentArrays + Optimization; no Flux hard dependency). Compose with known physics using build_ode_rhs, then solve_cdm / do_pin. Thin training: train_mechanisms!. Examples: examples/mechanism_ude.jl, examples/mechanism_scm.jl.
Application-scale Quarto stress: docs/stress/deep_scm_stress.qmd (STRESS.md). Estimation on codes (mediation / LMTP) is the CausalTargeted sibling deep_scm_estimation_stress.qmd.
Generative L3 on codes (Phase 2b)
For :generative nodes, $X = f(\\mathrm{pa}) + U$. Use abduce_noise and mechanism_counterfactual for same-unit counterfactuals; see examples/mechanism_counterfactual.jl. Prefer Phase 1 codes (or other low-dim vectors) as nodes; raw images are not mechanism parents.
Deferred (explicitly not in core)
- UniversalDiffEq as a hard dependency (use as an optional advanced trainer below)
- Flux-in-core (Flux stays at MLJFlux nuisances / application encoders)
- Non-additive image DeepSCM (learned encoder abduction on tensors)
- Full MIRS / Twins cohort fixtures in-package (synthetic spectra in stress only)
UniversalDiffEq (optional advanced trainer)
Use CausalDynamics for adjustment / do semantics and Lux MechanismLibrary residuals when you want parent masks in-process. Use UniversalDiffEq.jl only when you need that package’s ecology-oriented UDE training APIs. Do not expect UDE training inside CausalDynamics core, and do not add UniversalDiffEq to CausalDynamics [deps].
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 - 0.4.x — representation bridge; Lux
MechanismLibrary(2a) and additive generative L3 (2b); Deep SCM stress notebooks