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 time steptstep:(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 time stepsseries: endogenous trajectories (Symbol => Vector{<:Real})noise: realised exogenous draws (Symbol => Vector{<:Real})
CausalDynamics.CDMPanel — Type
CDMPanelWide observational panel from $n$ independent DiscreteTimeCDM trajectories of length $T$.
Fields
n,T: dimensionsdata: column name → length-nvectorscolumn_order: stable column order for table conversionvariables: endogenous symbols from the CDMbaseline,timed,terminal: role partition used when building columnstemporal_spec: optionalTemporalDAGSpecfor downstream ID
CausalDynamics.panel_column_name — Function
panel_column_name(var, t) -> SymbolDefault time-indexed column name: concatenate the variable symbol and time index with no separator (:a, 2 → :a2).
CausalDynamics.check_time_resolution — Function
check_time_resolution(query, measured_at; warn=true) -> Vector{NamedTuple}Check whether a TemporalEffectQuery references variables at time indices that differ from where they were actually measured in the wide panel.
measured_at maps DAG node symbols to the time index of the wide column that holds the value (e.g. period-constant contact scores measured once at $t = 1$ but referenced at t_outcome = 4).
Returns a vector of issue records; emits @warn when warn=true.
CausalDynamics.trajectory_wide_row — Function
trajectory_wide_row(traj; baseline, timed, terminal, name_fn) -> Dict{Symbol, Float64}Flatten one CDMTrajectory into a wide named row.
CausalDynamics.simulate_panel — Function
simulate_panel(cdm, n, T; rng, intervention, baseline, timed, terminal, name_fn, temporal_spec)Simulate n independent trajectories and stack them as a wide CDMPanel.
For observational estimation (CausalTargeted sequential LMTP), leave intervention = nothing so treatments follow the natural mechanism.
Keyword arguments
baseline: symbols emitted once (value at $t = 1$), e.g.[:w]timed: symbols expanded asname_fn(v, t)for $t = 1:T$; default is all endogenous variables not listed inbaselineorterminalterminal: symbols emitted once from time index $T$ (bare name), e.g.[:y]name_fn:(var::Symbol, t::Int) -> Symbol(defaultpanel_column_name)temporal_spec: optional lag DAG carried for ID hand-off
CausalDynamics.RandomEffectSpec — Type
RandomEffectSpecSpecification for nested exogenous noise when simulating hierarchical panels.
Fields
cluster_col: column name for the cluster id in the panel (Float64codes)n_clusters: number of clustersσ_cluster: standard deviation of the cluster-level interceptσ_unit: standard deviation of the unit-level residual (added to named outcomes)outcome_vars: symbols that receive $U_j + U_{ij}$ (wide panel columns)
Cluster ids are stored as $1, 2, …$ encoded in Float64 to match CDMPanel column types.
CausalDynamics.HIERARCHY_ASSUMPTIONS — Constant
Assumption symbols for nested units (attach to certificates when relevant).
:nested_exogenous— shared cluster-level exogenous draws:no_interference_across_clusters— no cross-cluster causal pathways
CausalDynamics.assign_cluster_ids — Function
assign_cluster_ids(n, n_clusters; rng) -> Vector{Float64}Assign units to clusters as evenly as possible (ids $1…n_clusters$ as Float64).
CausalDynamics.draw_nested_effects — Function
draw_nested_effects(cluster_ids, spec; rng) -> NamedTupleDraw cluster intercepts and unit residuals aligned to cluster_ids.
Returns (u_cluster, u_unit, cluster_levels) where u_cluster[i] is the shared draw for unit $i$'s cluster.
CausalDynamics.add_nested_effects! — Function
add_nested_effects!(panel, cluster_ids, effects, spec) -> CDMPanelAdd $U_j + U_{ij}$ to spec.outcome_vars and store cluster_ids under spec.cluster_col. Mutates panel.data / column_order.
CausalDynamics.simulate_hierarchical_panel — Function
simulate_hierarchical_panel(cdm, n, T; hierarchy, rng, kwargs...) -> CDMPanelSimulate a CDMPanel then inject nested cluster/unit intercepts on hierarchy.outcome_vars, writing the cluster id column.
Other keywords are forwarded to simulate_panel.
CausalDynamics.simulate_hierarchical_intercept_ate — Function
simulate_hierarchical_intercept_ate(n; n_clusters, σ_cluster, β_a, β_w, σ_y, rng)
-> (columns, truth)Binary point-treatment DGP with a shared cluster intercept:
- assign $n$ units to
n_clustersclusters - $U_j \sim N(0, σ_cluster^2)$ per cluster; $ε_i \sim N(0, σ_y^2)$
- $W_i \sim N(0,1)$, $A_i \sim Bernoulli(logistic(0.5 W_i))$
- $Y_i = β_a A_i + β_w W_i + U_{c(i)} + ε_i$
Returns a column dict suitable for DataFrame (when loaded) and truth with ate = β_a, σ_cluster, and hierarchy assumption symbols.
This is a generative gate for nested $U$, not an LMM fit.
CausalDynamics.HierarchicalNestingSpec — Type
HierarchicalNestingSpecUnit-level template DAG plus a shared cluster-level node that points into selected unit variables. Used by unroll_hierarchical_dag.
Fields
unit_variables: symbols replicated once per unitunit_edges: directed edges within each unit copy(parent, child)cluster_variable: symbol for the shared cluster-level node (per cluster)affects: unit variables that receive an edge from the cluster node
CausalDynamics.HierarchicalUnrolling — Type
HierarchicalUnrollingResult of unroll_hierarchical_dag: a flat DAG over unit copies and cluster nodes, with index maps and hierarchy assumptions.
CausalDynamics.unroll_hierarchical_dag — Function
unroll_hierarchical_dag(spec, n_units; n_clusters=1, cluster_of_unit=nothing)
-> HierarchicalUnrollingExpand a unit template into n_units copies and n_clusters shared cluster nodes. Edges within each unit copy follow spec.unit_edges; each cluster node $j$ points to spec.affects variables of every unit assigned to cluster $j$.
When cluster_of_unit is omitted, units are assigned round-robin to clusters (ids $1:n_clusters$).
CausalDynamics.hierarchical_node — Function
hierarchical_node(unrolling, key) -> IntLook up a node id. Keys are (:cluster, j) or (:unit, i, var::Symbol).
CausalDynamics.hierarchical_node_names — Function
hierarchical_node_names(unrolling) -> Dict{Int, Symbol}Stable Symbol labels for Documenter / identify(...; node_names=): U_c1, W_u3, Y_u3, …
CausalDynamics.attach_hierarchy_assumptions — Function
attach_hierarchy_assumptions(result) -> IdentificationResultReturn a copy of result with HIERARCHY_ASSUMPTIONS unioned into assumptions (idempotent).
CausalDynamics.RelationalSchema — Type
Describe entity types, relation types, and attributes in a relational model.
CausalDynamics.RelationalSkeleton — Type
Provide the entity and relation instances for one relational situation.
CausalDynamics.RelationalParent — Type
Declare an attribute-level parent relation for a relational SCM.
CausalDynamics.relational_entities — Function
Return the entity identifiers present for entity_type.
CausalDynamics.relational_attributes — Function
Return the declared attributes for entity_type.
CausalDynamics.ground_relational_graph — Function
Ground relational parent rules to a directed graph with tuple node labels.
Relation instances follow the schema endpoint order (parent, child). The returned node_labels vector maps graph vertices to (entity_type, id, attribute) tuples, making the grounding auditable without introducing a new graph representation for downstream algorithms.
CausalDynamics.ObservationBridge — Type
ObservationBridge(mapping; measure=nothing, availability=nothing)Declare how latent (or endogenous) series become estimation columns.
Fields
mapping: whenmeasureisnothing, source series symbol → panel symbol; whenmeasureis set, optional rename of measure output keys → panel symbols (missing keys keep their measure names)measure: optional(state_nt, t) -> NamedTupleproducing observed values from the full endogenous state at timetavailability: optional map from panel (or source) symbol to the earliest decision time $t$ at which the quantity enters $ℋ_t$. A quantity may describe an earlier realisation and become available later.
CausalDynamics.available_at — Function
available_at(bridge, variable, decision_t) -> BoolReturn whether variable is in the decision information set at decision_t. When availability is unset, every mapped variable is treated as available.
CausalDynamics.information_set_at — Function
information_set_at(bridge, decision_t) -> Set{Symbol}Symbols available to a decision-maker at decision_t under this bridge.
CausalDynamics.policy_information_set — Function
policy_information_set(policy, t) -> Union{Nothing, Set{Symbol}}State symbols the policy may read at decision time t ($ℋ_t$). Returns nothing when the policy declares no information set (unrestricted), the declared static set, or — for an ObservationBridge — the latent variables whose observed counterparts are available at t.
CausalDynamics.identity_observation — Function
identity_observation(variables) -> ObservationBridgeBridge that exposes each symbol under its own name (no measurement map).
CausalDynamics.observe_series — Function
observe_series(series, bridge) -> Dict{Symbol, Vector{Float64}}Apply an ObservationBridge to a Dict of equal-length series.
CausalDynamics.observe_trajectory — Function
observe_trajectory(traj, bridge) -> Dict{Symbol, Vector{Float64}}Observe a CDMTrajectory endogenous series through bridge.
CausalDynamics.panel_from_trajectories — Function
panel_from_trajectories(trajs; baseline, timed, terminal, name_fn, bridge)Stack trajectories into a CDMPanel. Optional bridge is applied to each trajectory before wide flattening (latent → observed columns).
CausalDynamics.panel_from_latent_series — Function
panel_from_latent_series(unit_series; T, bridge, baseline, timed, terminal, name_fn)Build a panel from pre-inferred latent series (e.g. filter/smoother output).
Each element of unit_series is a Dict{Symbol, <:AbstractVector} of length T (or of equal length, with T taken from the first unit when T === nothing).
CausalDynamics.simulate_observed_panel — Function
simulate_observed_panel(cdm, n, T; bridge, kwargs...)Simulate natural trajectories, apply bridge, and stack as a CDMPanel.
CausalDynamics.ObservationMask — Type
ObservationMaskBinary response indicators aligned with named columns.
Fields
columns: column symbols (one per mask column)observed:n × length(columns)bit matrix;truemeansR = 1(observed)time_indexed: iftrue, columns are time steps of a timed process (e.g.:y1,:y2)
CausalDynamics.observation_mask — Function
observation_mask(data; columns, time_indexed=false) -> ObservationMaskBuild an ObservationMask from a column dictionary (or named vectors). Entries that are missing become R = 0; all other values are R = 1. Does not invent filled values.
CausalDynamics.n_units — Function
n_units(mask::ObservationMask) -> IntNumber of units (rows) in the mask.
CausalDynamics.miss_rates — Function
miss_rates(mask::ObservationMask) -> Dict{Symbol, Float64}Fraction of units with R = 0 for each column.
CausalDynamics.require_complete_values — Function
require_complete_values(x; context="values") -> xReturn x unchanged if it contains no missing. Otherwise throw ArgumentError naming context and directing the caller to an Observable missingness policy (CausalTargeted handle_missing_data or an explicit mask).
CausalDynamics.require_complete_matrix — Function
require_complete_matrix(X; context="matrix") -> XReject matrices whose eltype admits Missing and that contain any missing entry. Used by encode_to_panel so high-dim assays are completed (or rows dropped) under an Observable policy before encoding.
CausalDynamics.apply_observation_mask — Function
apply_observation_mask(values, observed) -> VectorReturn a copy of values with missing wherever observed is false. Lengths must match. Does not impute.
CausalDynamics.apply_missingness_mechanism — Function
apply_missingness_mechanism(data, spec; intercept, coefficients, rng)
-> (Dict{Symbol, Vector}, ObservationMask)Generative incompleteness for Dynamical × L2 oracles: start from complete columns and set spec.response entries to missing under
:mcar— Bernoulli with success probability $\mathrm{logistic}(\mathrm{intercept})$ of being missing:mar— $P(R=0 \mid W) = \mathrm{logistic}(\mathrm{intercept} + \beta'W)$:mnar— throws (not simulated without further assumptions)
coefficients defaults to 0.8 for each symbol in spec.conditioning_set. Returns a new column dictionary (response columns become Union{Missing,Float64}) and the corresponding ObservationMask.
apply_missingness_mechanism(panel::CDMPanel, spec; kwargs...)
-> (Dict{Symbol, Vector}, ObservationMask)Apply apply_missingness_mechanism to the wide columns of a complete CDMPanel.
CausalDynamics.simulate_incomplete_panel — Function
simulate_incomplete_panel(cdm, n, T; missingness, kwargs...)
-> (Dict{Symbol, Vector}, ObservationMask, CDMPanel)Simulate a complete CDMPanel, then apply apply_missingness_mechanism under missingness::MissingnessSpec. Returns (incomplete_columns, mask, complete_panel).
Extra keywords intercept, coefficients, and rng_missing are forwarded to the missingness mechanism; all other keywords go to simulate_panel.
CausalDynamics.RepresentationSpec — Type
RepresentationSpec(source, code_names, encode; role=:measurement)Declare how a high-dimensional input becomes low-dimensional codes for the causal graph and estimation APIs.
Fields
source: label for the high-dim input (documentation / certificate; not a DataFrame column requirement)code_names: symbols for the emitted code columns (lengthd)encode: callable on an $n × p$ matrix. Return an $n × d$ matrix, or a length-nvector of length-dvectors / NamedTuples whose keys matchcode_nameswhen NamedTuples are usedrole::measurement(codes observe a latent mediator) or:definitional(the mediator is the encoder output)
Identification and EIF/TMLE see only code_names, not CNN layers.
CausalDynamics.representation_certificate — Function
representation_certificate(spec) -> NamedTupleAuditable metadata for run records: role, source, code_names, and a stable hash of the encoder's type (not weights).
CausalDynamics.encode_to_panel — Function
encode_to_panel(X, spec; tabular=nothing) -> Dict{Symbol, Vector{Float64}}Apply spec.encode to the high-dim matrix X (n × p) and return a column dictionary with spec.code_names. Optional tabular supplies additional columns (e.g. :A, :W, :Y) of length n; they are copied unchanged.
Does not require DataFrames. For a DataFrame result, load DataFrames so the weakdep extension is available and call encode_to_panel(df, X, spec).
CausalDynamics.MechanismSpec — Type
MechanismSpec(node, parents; kind=:ode_residual, representation=nothing, output_dim=1)Declare a deep structural mechanism at node that may only depend on parents (must be a subset of the graph / CDM parent set).
Fields
node: endogenous variableparents: allowed causal parents (order used when packing NN inputs)kind::static(SCM $f_i$),:ode_residual(additive NN on $\dot u$), or:generative($X = f(\mathrm{pa}) + U$ with abduce/generate for L3)representation: optionalRepresentationSpeclinking Phase 1 codesoutput_dim: dimension of the node (1 for scalars; >1 for code/tensor vectors)model,parameters,states: filled by the Lux extension (nothinguntil attach)
CausalDynamics.MechanismLibrary — Type
MechanismLibrary(; allowed_parents, graph_hash=zero(UInt64))Collection of MechanismSpecs with parent-set validation against allowed_parents (typically from a DAG or ContinuousCDMSpec.parents).
CausalDynamics.mechanism_library_from_cdm — Function
mechanism_library_from_cdm(spec::ContinuousCDMSpec; graph_hash=...) -> MechanismLibraryBuild a library whose allowed parents match spec.parents.
CausalDynamics.register_mechanism! — Function
register_mechanism!(lib, spec) -> MechanismLibraryInsert spec after checking spec.parents ⊆ allowed_parents[spec.node].
CausalDynamics.mechanism_certificate — Function
mechanism_certificate(lib) -> NamedTupleAuditable metadata: nodes, kinds, parent maps, graph hash, whether Lux payloads are attached.
CausalDynamics.lux_mechanisms_available — Function
lux_mechanisms_available() -> BoolReturn true when the Lux mechanism extension is loaded.
CausalDynamics.attach_lux_mechanism! — Function
attach_lux_mechanism!(lib, node; parents, hidden, kind, rng, activation, output_dim)Attach a small Lux MLP at node. Requires the Lux weakdep extension.
CausalDynamics.build_ode_rhs — Function
build_ode_rhs(known_rhs!, spec, lib) -> FunctionCompose an in-place ODE RHS: known_rhs! then add Lux residuals for :ode_residual mechanisms, packing only declared parents. Requires Lux ext.
CausalDynamics.graphscm_with_mechanisms — Function
graphscm_with_mechanisms(g, equations, exogenous, lib; node_names) -> GraphSCMReplace selected equation entries with Lux :static / :generative mechanisms from lib. Requires Lux ext.
CausalDynamics.train_mechanisms! — Function
train_mechanisms!(lib; loss, maxiters, opt, kwargs...) -> NamedTupleThin Adam training loop over attached Lux parameters. Requires Lux ext.
Training designs must be complete: do not pass panels containing Missing into loss. Drop or impute under an Observable policy first (see require_complete_matrix / apply_missingness_mechanism).
CausalDynamics.pack_parent_vector — Function
pack_parent_vector(u, spec, parents) -> VectorGather state coordinates for parents from u using ContinuousCDMSpec indices.
CausalDynamics.abduce_noise — Function
abduce_noise(spec, x, pa) -> noiseInfer additive exogenous noise $U = X - f(\mathrm{pa})$ for a :generative mechanism. Requires Lux ext when spec.model is attached.
CausalDynamics.generate_from_noise — Function
generate_from_noise(spec, z, pa) -> xSample / reconstruct $X = f(\mathrm{pa}) + U$ for a :generative mechanism. Requires Lux ext.
CausalDynamics.mechanism_counterfactual — Function
mechanism_counterfactual(spec, x_factual, pa_factual, pa_cf) -> x_cfPearl L3 for one generative node: abduce $U$ from the factual, then generate under counterfactual parents (same $U$). Requires Lux ext.
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 time steps.
CausalDynamics.SeriesAssignment — Type
SeriesAssignment(values)Hold a time-indexed do(·) series (values[t] at time t).
CausalDynamics.TimedAssignment — Type
TimedAssignment(f)Hold a time-dependent do(·) rule f(t).
CausalDynamics.AbstractAvailability — Type
AbstractAvailabilityMarker for objects that can supply a time-indexed information set $ℋ_t$ to a Policy (for example an ObservationBridge).
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.
Optional information_set declares which symbols are available at decision time $ℋ_t$ (non-anticipation). It may be
- a collection of symbols: a static $ℋ$ — rules may only read those symbols from
state; or - an
ObservationBridge: a time-varying $ℋ_t$ derived from the bridge'smappingandavailability. At decision timetthe rule sees only the state variables whose observed counterpart isavailable_at(bridge, observed, t); unmapped (unobserved) variables are never visible. Availability is declared on the observation side and consumed here — it is not re-derived from the state.
Rules that need a symbol outside $ℋ_t$ fail with a field-access error rather than silently reading future or unobserved information.
CausalDynamics.policy — Function
policy(variable::Symbol, rule; information_set=nothing)Build a Policy assigning variable via rule(state, t).
policy(pairs::Pair{Symbol, <:Any}...; information_set=nothing)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 time steps.
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 replicateprovenance: optional semantic manifest for the analysis
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 time step). 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 time steps 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.
g_computation(spec, rhs!, u0_sampler, tspan, p; intervention, functional, n, rng, kwargs...)Monte Carlo interventional mean of a continuous CDM functional.
u0_sampler(rng) -> AbstractVector draws initial conditions. Returns a GComputationResult. Requires using OrdinaryDiffEq.
Relational grounding
RelationalSchema describes entity types, attributes, and binary relation types. RelationalSkeleton supplies the entities and relation instances present in one situation. A RelationalParent declares an attribute-level parent rule; ground_relational_graph expands those rules into an ordinary Graphs.jl directed graph with tuple labels. Identification and temporal unrolling remain downstream operations on that grounded graph. The prototype does not claim transportability to unseen skeletons without additional mechanism-invariance and confounding assumptions.
schema = RelationalSchema(
[:signal, :car],
Dict(:controls => (:signal, :car)),
Dict(:signal => [:walk], :car => [:brake]),
)
skeleton = RelationalSkeleton(
schema,
Dict(:signal => [:s1], :car => [:c1]),
Dict(:controls => [(:s1, :c1)]),
)
ground_relational_graph(
skeleton,
[RelationalParent(:signal, :walk, :car, :brake, :controls)],
)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.meanAuditable intervention and abstraction declarations
InterventionDescriptor records a declared intervention's target, replacement identifier, timing, scope, stochasticity, and cointerventions. It is metadata, not a solver-backed SDE surgery. Scalar DoIntervention values are recorded directly; policies and other functions require a caller-supplied stable identifier before they can be fingerprinted.
CDMProvenance also accepts optional estimand, identification, estimator, positivity, and sensitivity identifiers. These fields let an analysis certificate record the query and its evidential qualifications alongside the model semantics. They remain caller-supplied identifiers rather than hashes of arbitrary executable code.
CausalAbstractionSpec compares finite interventional laws after push-forward through the declared state map. exact means equality of the laws as mass maps (support order does not matter); accepted is controlled by law_mode and the declared tolerance.
Structural constraint declarations
StructuralConstraintSpec records an auditable claim about an invariance, feasibility condition, viability condition, or relation across embodiments. It names the variables or mechanisms to which the claim applies, the predicted consequences, and the identifiers of proposed tests. The corresponding constraint_certificate is deterministic metadata, not an empirical validation result.
These declarations deliberately remain outside the DAG and simulation semantics. They do not add a causal parent, graph node, attractor, or agent. Pair them with a MechanismLibrary, a CausalAbstractionSpec, and typed intervention descriptors when the analysis supplies those artefacts.
SetState is the typed form of DoIntervention on a GraphSCM and of a constant DoSequence on a DiscreteTimeCDM. ReplacePolicy is the auditable form of Policy when a rule is supplied. apply_intervention on a CDM bakes that surgery into initialise and step.
CausalDynamics.InterventionDescriptor — Type
InterventionDescriptor(target; replacement, replacement_id, interval, scope,
stochasticity, cointerventions=Symbol[])Auditable description of an intervention. The descriptor records semantics without serialising executable replacement functions. replacement_id is a required stable identifier for a functional replacement; scalar replacements receive a canonical literal identifier. Callers must supply versioned identifiers for functions.
CausalDynamics.intervention_descriptor — Function
Return an auditable descriptor for a static SCM intervention.
Return one descriptor for each assignment in a discrete-time do sequence.
Return one descriptor for each endogenous variable governed by a policy regime.
Return the audit descriptor for a typed state assignment.
CausalDynamics.canonical_intervention_descriptor — Function
canonical_intervention_descriptor(descriptor) -> StringReturn an unambiguous, length-prefixed encoding of a declared intervention. This encodes metadata only; it does not serialise or fingerprint arbitrary Julia functions. Use an explicit versioned replacement_id for functional policies.
Encode a composed audit bundle, including sequential repeats on one target.
CausalDynamics.compose_intervention_descriptors — Function
compose_intervention_descriptors(descriptors...) -> Vector{InterventionDescriptor}Validate a simultaneous declaration. Each target may occur at most once; this operation represents simultaneous declarations and does not model sequencing.
CausalDynamics.ComposedIntervention — Type
ComposedIntervention(descriptors; mode=:simultaneous)Audit container for a validated intervention composition. Executable algebra uses Simultaneous and Sequential; this type records the same mode for certificates. Simultaneous bundles still reject duplicate targets; sequential bundles may repeat a target. The vector-returning compose_intervention_descriptors function remains available.
CausalDynamics.compose_intervention_bundle — Function
Return a typed composition while retaining the legacy vector API.
CausalDynamics.SetState — Type
SetState(target, value; interval=:all, scope=:unit)Typed assignment of target to value on a stated interval. On a GraphSCM this is DoIntervention; on a DiscreteTimeCDM it is a constant DoSequence.
CausalDynamics.SetInitialCondition — Type
Typed initial-condition assignment for a continuous or discrete-time CDM.
CausalDynamics.ReplacePolicy — Type
ReplacePolicy(target, id; interval=:all, scope=:unit, rule=nothing)Successor policy instance with a stable replacement identifier. Supply rule to execute CDM surgery; the identifier is the auditable name of the instance.
CausalDynamics.ReplaceParameter — Type
Replace a named parameter with a successor identifier (metadata until bound).
CausalDynamics.ReplaceMechanism — Type
Replace a named mechanism on a stated interval (metadata until bound).
CausalDynamics.Simultaneous — Type
Simultaneous bundle of typed interventions; conflicting targets throw.
CausalDynamics.Sequential — Type
Sequential composition of typed interventions.
CausalDynamics.intervention_kind — Function
Return the semantic kind of a typed intervention (:state, :policy, …).
CausalDynamics.intervention_target — Function
Return the assignment target, or nothing for a composition.
CausalDynamics.intervention_interval — Function
Return the time interval on which the intervention applies.
CausalDynamics.canonical_intervention — Function
Return a stable canonical tuple encoding of a typed intervention.
CausalDynamics.intervention_fingerprint — Function
Return a stable SHA-256 identity for intervention semantics.
CausalDynamics.FiniteLaw — Type
A finite probability law with canonical support and probabilities.
CausalDynamics.pushforward — Function
Map a finite law through a deterministic state map.
CausalDynamics.law_equal — Function
Exact equality for finite laws with optional numerical tolerance.
CausalDynamics.law_distance — Function
Total-variation distance for finite laws, aligning atoms independently of order.
CausalDynamics.CDMProvenance — Type
CDMProvenance(; graph, mechanisms, observation, policy, exogenous,
intervention, scale_map="", numerical="", coupling="", estimand="",
identification="", estimator="", positivity="", sensitivity="")Versioned identifiers for the semantic components of one CDM analysis. Identifiers are supplied by the caller because executable Julia functions cannot be reliably fingerprinted from source alone.
CausalDynamics.provenance_dict — Function
Return canonical serialisable provenance metadata.
CausalDynamics.provenance_fingerprint — Function
Return a stable SHA-256 fingerprint for semantic provenance metadata.
CausalDynamics.certificate_schema_version — Function
Return the current certificate schema identifier.
CausalDynamics.CertificateEnvelope — Type
CertificateEnvelope(semantic; environment=NamedTuple())Wrap semantic analysis identity and optional runtime metadata in a versioned envelope. Semantic fields are kept separate from environment fields so a re-run on another Julia version does not silently change the identity of an estimand or intervention.
CausalDynamics.certificate_envelope — Function
Construct a certificate from provenance without conflating runtime metadata.
CausalDynamics.certificate_dict — Function
certificate_dict(result) -> Dict{Symbol, Any}Return a serialisable dictionary representation of a certificate.
CausalDynamics.CausalAbstractionSpec — Type
CausalAbstractionSpec(τ, ω; interventions, distance=law_distance, tolerance=0.0, law_mode=:exact)Compare finite interventional laws after push-forward through state map τ and intervention map ω. exact is mass-map equality; accepted uses law_mode and tolerance.
CausalDynamics.CausalAbstractionResult — Type
Result of validate_abstraction: exactness, acceptance, and per-intervention discrepancies.
CausalDynamics.validate_abstraction — Function
Compare micro and macro interventional laws under a CausalAbstractionSpec.
CausalDynamics.StructuralConstraintSpec — Type
StructuralConstraintSpec(id, targets; kind, claim, predictions = Symbol[],
test_ids = Symbol[], embodiments = Symbol[])Declare a structural claim that constrains a model's admissible states, transformations, or embodiments. kind must be one of :invariance, :feasibility, :viability, or :cross_embodiment.
The declaration records a claim, its named targets, and the predictions and tests through which a caller intends to assess it. It is metadata: it neither adds a causal parent or graph node, nor executes a test or numerical solver. Use it alongside a MechanismLibrary, CausalAbstractionSpec, and explicit intervention descriptors when those corresponding artefacts are available.
CausalDynamics.constraint_certificate — Function
constraint_certificate(spec) -> NamedTupleReturn a deterministic, serialisable record of a declared structural constraint. The fingerprint identifies the declaration's content; it is not evidence that the claim has been empirically confirmed.
Observational panels (estimation hand-off)
simulate_panel stacks n natural trajectories into a wide CDMPanel for sequential LMTP. Column roles:
- baseline — bare symbol, value at $t = 1$ (e.g.
:w) - timed —
panel_column_name(v, t)→:a1,:a2, … - terminal — bare symbol at occasion $T$ (e.g.
:y)
using DataFrames
panel = simulate_panel(
cdm, 200, 2;
baseline = [:w],
timed = [:a, :l],
terminal = [:y],
)
df = DataFrame(NamedTuple(panel)) # or DataFrame(panel) when CausalDynamicsDataFramesExt is loadedLeave intervention = nothing for observational estimation. Interventional Monte Carlo effects stay on g_computation; CausalTargeted consumes df via sequential_spec_from_identification / execute_estimand.
Latent → observed panels
When latents are inferred outside CausalDynamics (filter, smoother, EM), map them with ObservationBridge and panel_from_latent_series. No Kalman/particle code lives here—only the typed hand-off to wide panels.
bridge = ObservationBridge(
Dict(:y_hat => :y);
measure = (state, t) -> (y_hat = state.x,), # e.g. use latent x as assay
)
panel = panel_from_latent_series(
inferred_units; # Vector{<:Dict} of series
bridge = bridge,
timed = [:y],
terminal = Symbol[],
)