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

  1. Structural — TemporalDAGSpec + unroll_temporal_dag or a hand-built DiGraph
  2. Dynamical — DiscreteTimeCDM + simulate / counterfactual, or an ODEProblem
  3. 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.mean

State-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:

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 — CausalDynamicsSciMLExt weakdep (ContinuousCDMSpec, solve_cdm, do(·) RHS wrapper)
  • 0.3.x — unified AbstractCausalIntervention; SciML-native DoPin (DiscreteCallback); IEE → TemporalDAGSpec
  • 0.4.x — representation bridge; Lux MechanismLibrary (2a) and additive generative L3 (2b); Deep SCM stress notebooks