This chapter shows how to intervene in deterministic dynamical systems, modifying ODEs through the \(do(\cdot)\) operator and evaluating policies. This is part of Doing in the Dynamical stratum, how can we intervene/modify dynamic processes? In continuous-time causal models (Peters et al.’s causal kinetic models), an intervention replaces one or more coordinate mechanisms in the governing equations (hard point interventions, soft mechanism edits, or structural removals), then integrates the modified system forward (Peters et al. 2022).
17.2 Interventions in ODEs
17.2.1 The Intervention Operator
When we intervene \(do(A_t = a)\) in a deterministic system, we modify the structural equation for \(A_t\):
\[
A_t \coloneqq a \quad \text{(instead of } \pi(H_t, U^A_t) \text{)}
\]
This structural change propagates through the ODE system.
17.2.2 How Interventions Modify System Dynamics
Steps:
Set intervention: \(do(A_t = a)\) for all \(t\) in intervention period
Modify ODE: Replace \(A_t\) with constant \(a\) in the ODE
Solve ODE: Integrate the modified system forward
Observe effects: How does the system evolve under intervention?
17.2.3 Example: Vaccination in SIR Model
Consider an SIR model with vaccination intervention:
\[
\begin{aligned}
\frac{dS}{dt} &= -\beta S I - \nu \cdot do(V = 1) \\
\frac{dI}{dt} &= \beta S I - \gamma I \\
\frac{dR}{dt} &= \gamma I + \nu \cdot do(V = 1)
\end{aligned}
\]
Under intervention \(do(V = 1)\), vaccination rate \(\nu\) is applied, moving individuals directly from \(S\) to \(R\).
17.2.4 Implementation: Intervention in ODEs
We can implement interventions in ODEs using DifferentialEquations.jl. Here’s an example with the SIR model:
# Find project root and include ensure_packages.jlproject_root =let current =pwd()while !isfile(joinpath(current, "Project.toml")) && !isfile(joinpath(current, "_quarto.yml")) parent =dirname(current) parent == current &&break current = parentend currentendinclude(joinpath(dirname(Base.active_project()), "scripts", "book_bootstrap.jl"))@auto_using OrdinaryDiffEq CairoMakie# SIR model parametersβ =0.3# Transmission rateγ =0.1# Recovery rateν =0.05# Vaccination rate (when intervention is active)# Original SIR model (no intervention)functionsir_original!(du, u, p, t)"""SIR epidemic model: Susceptible → Infected → Recovered.""" S, I, R = u du[1] =-β * S * I # dS/dt du[2] = β * S * I - γ * I # dI/dt du[3] = γ * I # dR/dtend# SIR model under intervention do(V = 1) (vaccination active)functionsir_intervened!(du, u, p, t)"""SIR model with vaccination intervention: do(V = 1).""" S, I, R = u# Intervention: do(V = 1) means vaccination is active du[1] =-β * S * I - ν # dS/dt: vaccination moves S → R du[2] = β * S * I - γ * I # dI/dt: unchanged du[3] = γ * I + ν # dR/dt: vaccination adds to recoveredend# Initial conditionsu0 = [0.99, 0.01, 0.0] # 99% susceptible, 1% infected, 0% recoveredtspan = (0.0, 100.0)# Solve original systemprob_original =ODEProblem(sir_original!, u0, tspan)sol_original =solve(prob_original, Tsit5())# Solve intervened systemprob_intervened =ODEProblem(sir_intervened!, u0, tspan)sol_intervened =solve(prob_intervened, Tsit5())# Compare peak infectionpeak_original =maximum([u[2] for u in sol_original.u])peak_intervened =maximum([u[2] for u in sol_intervened.u])println("Peak infection rate:")println(" Original: ", round(peak_original, digits=3))println(" Intervened (do(V=1)): ", round(peak_intervened, digits=3))println(" Reduction: ", round((1- peak_intervened/peak_original) *100, digits=1), "%")
Figure 17.1: Intervention in SIR model: comparing original and intervened trajectories
17.2.5 CausalDynamics continuous CDM (do_pin)
The vaccination example above edits the RHS by hand. CausalDynamics provides the same continuous-CDM vocabulary on a named spec: hard pin (do_pin), initial-condition (do_ic), soft force (do_force), and RHS replacement (do_rhs), with SciML-native pin callbacks (Peters et al. 2022).
include(joinpath(dirname(Base.active_project()), "scripts", "book_bootstrap.jl"))@auto_using CausalDynamics OrdinaryDiffEq Graphsspec =ContinuousCDMSpec( [:prey, :predator]; parents =Dict(:prey => [:prey, :predator],:predator => [:prey, :predator], ),)functionlotka!(du, u, p, t) X, Y = u du[1] = p.r * X - p.α * X * Y du[2] = p.β * X * Y - p.δ * Yreturnnothingendp = (r =1.0, α =0.1, β =0.02, δ =0.5)sol =solve_cdm(spec, lotka!, [40.0, 9.0], (0.0, 8.0), p)sol_pin =solve_cdm( spec, lotka!, [40.0, 9.0], (0.0, 8.0), p; intervention =do_pin(:predator, 5.0),)g_cdm =continuous_cdm_graph(spec)(terminal_state(spec, sol).predator,terminal_state(spec, sol_pin).predator,ne(g_cdm),)
(7.0788894614717615, 5.0, 2)
Continuous CDM Lotka–Volterra: observational trajectory versus hard pin do_pin(:predator, 5). Predator stays flat under the pin; prey responds through the remaining mechanism.
17.2.6 Inferring ODE parents across environments
When the parent graph is unknown but several experimental environments are observed, infer_ode_parents ranks candidate parents by leave-one-environment stability of a linear-in-parameters mechanism for \dot Y (CausalKinetiX reference method (Pfister et al. 2019)). The ranking maps onto the same ContinuousCDMSpec parent field used above.
ODE parent ranking across environments. Left: multi-environment trajectories (true driver X_1 and spurious X_2). Centre: inclusion scores (higher is better). Right: recovered continuous-CDM parent graph X_1 \rightarrow Y.
17.3 Taxonomy of Continuous-Time Interventions
The interventions introduced above can be formalised into a taxonomy that distinguishes four fundamental types. Each type has distinct mathematical properties and appropriate domains of application.
17.3.1 Point Interventions
Point interventions are instantaneous interventions at a single time \(t^*\):
\[
X(t^*) = x^*
\]
Equivalently, they can be represented as Dirac delta forcing in the dynamics:
\[
\dot{X} = f(X) + c \cdot \delta(t - t^*)
\]
where \(\delta(t - t^*)\) is the Dirac delta distribution concentrated at \(t^*\). The constant \(c\) determines the magnitude of the instantaneous impulse.
Mathematical properties:
The intervention creates a discontinuity: the state jumps from its pre-intervention value to \(x^*\) (or receives an impulse)
The modified ODE is solved in two phases: integrate from initial conditions to \(t^*\), apply the intervention, then integrate forward from the new state
Point interventions apply when the intervention duration is negligible compared to the system’s characteristic timescales
Biological example: A bolus drug injection delivers a fixed dose instantaneously. The drug concentration jumps at the injection time, then decays according to the system’s natural dynamics (e.g., clearance).
17.3.2 Sustained Interventions
Sustained interventions hold a variable at a constant value over an interval \([t_1, t_2]\):
This is equivalent to clamping: the variable is removed from the dynamics during the intervention period. Downstream variables that depend on \(X\) receive the fixed value \(x^*\) instead of the natural evolution of \(X\).
Mathematical formulation: The modified ODE has boundary conditions \(X(t_1) = x^*\) and \(X(t_2) = x^*\), and \(X\) is treated as exogenous (not governed by \(\dot{X} = f(X)\)) during \([t_1, t_2]\). The system reduces to fewer state variables during the intervention.
Biological example: Continuous drug infusion maintains a constant drug concentration (or infusion rate) over time. The intervention sustains the treatment level rather than delivering it as a single pulse.
17.3.3 Stochastic Interventions
Stochastic interventions specify that the intervened variable is drawn from a distribution \(G\) rather than fixed at a value:
This generalises the standard \(do(X = x)\) to allow for uncertainty or heterogeneity in the intervention.
Connection to epidemiology: In shift interventions, the distribution of \(X\) is shifted (e.g., \(do(X \sim X + \delta)\)). Stochastic interventions formalise this by specifying a target distribution \(G\).
The interventional distribution under \(do(X \sim G)\) is the mixture of interventional distributions \(P(Y \mid do(X = x))\) weighted by \(G\).
Biological example: Heterogeneous treatment response, when the “same” treatment produces different effects across individuals, modelling \(do(X \sim G)\) captures the distribution of responses. For instance, a drug dose may vary around a target due to pharmacokinetic variability.
17.3.4 Dynamic Regimes
Dynamic regimes make the intervention depend on the current state (or history) of the system:
\[
do(X(t) = g(Y(t)))
\]
where \(g\) is a function of the observed state \(Y(t)\) (or more generally, the history \(H_t\)). The intervention is not fixed in advance but adapts as the system evolves.
Feedback control as a dynamic regime: A controller that adjusts the input \(u(t)\) based on the current state \(x(t)\) implements a dynamic regime: \(u(t) = \pi(x(t))\).
Connection to optimal control theory: Optimal control seeks the policy \(\pi^*\) that maximises (or minimises) an objective. The control law \(\pi^*\) defines a dynamic regime.
Mathematical formulation:
\[
\dot{X} = f(X) + u(X, t)
\]
where \(u\) is the control policy. The intervention \(u(X, t)\) depends on both state and time, making the system a closed-loop controlled system.
Biological example: Adaptive dosing protocols adjust drug dose based on measured biomarkers (e.g., therapeutic drug monitoring). The dose at time \(t\) depends on the patient’s current state rather than being fixed in advance.
17.3.5 Summary Table
Type
Mathematical form
When to use
Biological example
Point
\(X(t^*) = x^*\) or \(\dot{X} = f(X) + c \cdot \delta(t - t^*)\)
Methods: Dynamic programming, Pontryagin’s maximum principle.
17.4.3 Implementation: Policy Evaluation
We can evaluate policies by simulating the ODE under different treatment strategies:
# Find project root and include ensure_packages.jlproject_root =let current =pwd()while !isfile(joinpath(current, "Project.toml")) && !isfile(joinpath(current, "_quarto.yml")) parent =dirname(current) parent == current &&break current = parentend currentendinclude(joinpath(project_root, "scripts", "ensure_packages.jl"))@auto_using OrdinaryDiffEq CairoMakie# Example: Treatment policy in a simple disease model# State: X (disease severity), Action: A (treatment level 0 or 1)# Policy π: Treat if severity > thresholdβ =0.2# Disease progression rateα =0.3# Treatment effectiveness# Policy 1: No treatment (A = 0 always)functiondisease_no_treatment!(du, u, p, t) X = u[1] du[1] = β * X # Disease progressesend# Policy 2: Treat if X > 0.5 (threshold policy)functiondisease_threshold!(du, u, p, t) X = u[1] A = X >0.5 ? 1.0:0.0# Policy: treat if severity > 0.5 du[1] = β * X - α * A * X # Treatment reduces severityend# Policy 3: Always treat (A = 1 always)functiondisease_always_treat!(du, u, p, t) X = u[1] du[1] = β * X - α * X # Always apply treatmentend# Initial conditionu0 = [0.1] # Initial severity = 0.1tspan = (0.0, 20.0)# Solve under each policyprob1 =ODEProblem(disease_no_treatment!, u0, tspan)sol1 =solve(prob1, Tsit5())prob2 =ODEProblem(disease_threshold!, u0, tspan)sol2 =solve(prob2, Tsit5())prob3 =ODEProblem(disease_always_treat!, u0, tspan)sol3 =solve(prob3, Tsit5())# Evaluate expected outcomes (final severity)outcome1 = sol1.u[end][1]outcome2 = sol2.u[end][1]outcome3 = sol3.u[end][1]println("Expected final severity under each policy:")println(" Policy 1 (no treatment): ", round(outcome1, digits=3))println(" Policy 2 (threshold): ", round(outcome2, digits=3))println(" Policy 3 (always treat): ", round(outcome3, digits=3))println("\nBest policy: ", outcome3 < outcome2 < outcome1 ? "Policy 3 (always treat)": outcome2 < outcome1 ? "Policy 2 (threshold)":"Policy 1 (no treatment)")
Expected final severity under each policy:
Policy 1 (no treatment): 5.46
Policy 2 (threshold): 0.502
Policy 3 (always treat): 0.014
Best policy: Policy 3 (always treat)
Figure 17.2: Policy evaluation: comparing different treatment strategies
17.4.4 Optimal Control
For optimal control problems, we seek the policy that maximises (or minimises) an objective function. This typically requires specialised optimisation methods:
# Find project root and include ensure_packages.jlproject_root =let current =pwd()while !isfile(joinpath(current, "Project.toml")) && !isfile(joinpath(current, "_quarto.yml")) parent =dirname(current) parent == current &&break current = parentend currentendinclude(joinpath(project_root, "scripts", "ensure_packages.jl"))# Note: Full optimal control implementation requires:# - Optimization.jl for optimisation# - Dynamic programming or Pontryagin's maximum principle# - Cost function definition# Example framework (conceptual):# Objective: Minimise ∫[X(t)² + c*A(t)²] dt# - X(t)²: Penalty for high disease severity# - c*A(t)²: Cost of treatment (c is cost parameter)# Constraints: 0 ≤ A(t) ≤ 1 (treatment bounded)println("Optimal control framework:")println(" Objective: Minimise ∫[X(t)² + c*A(t)²] dt")println(" Subject to: dX/dt = β*X - α*A*X")println(" Constraints: 0 ≤ A(t) ≤ 1")println("\nMethods:")println(" - Dynamic programming (discrete time)")println(" - Pontryagin's maximum principle (continuous time)")println(" - Numerical optimisation (Optimization.jl)")println("\nFor full implementation, see Optimization.jl documentation")
Optimal control framework:
Objective: Minimise ∫[X(t)² + c*A(t)²] dt
Subject to: dX/dt = β*X - α*A*X
Constraints: 0 ≤ A(t) ≤ 1
Methods:
- Dynamic programming (discrete time)
- Pontryagin's maximum principle (continuous time)
- Numerical optimisation (Optimization.jl)
For full implementation, see Optimization.jl documentation
17.5 Stratum context
This chapter addresses Doing in the Dynamical stratum: how can we intervene/modify deterministic dynamic processes? Interventions in ODEs modify system dynamics, allowing us to evaluate policies and find optimal control strategies.
17.6 Key Takeaways
Interventions modify ODEs: \(do(\cdot)\) operator changes system dynamics
Policy evaluation: Simulate ODE under policy to evaluate outcomes
Optimal control: Find policies that maximise expected outcomes
Structural changes propagate: Interventions affect system evolution
Peters, Jonas, Stefan Bauer, and Niklas Pfister. 2022. “Causal Models for Dynamical Systems.” In Probabilistic and Causal Inference: The Works of Judea Pearl, edited by Hector Geffner, Rina Dechter, and Joseph Y. Halpern. Association for Computing Machinery. https://doi.org/10.1145/3501714.3501752.
Pfister, Niklas, Stefan Bauer, and Jonas Peters. 2019. “Learning Stable and Predictive Structures in Kinetic Systems.”Proceedings of the National Academy of Sciences 116 (51): 25405–11. https://doi.org/10.1073/pnas.1905688116.
Strogatz, Steven H. 2014. Nonlinear Dynamics and Chaos: With Applications to Physics, Biology, Chemistry, and Engineering. 2nd ed. Westview Press.