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.DiscreteTimeCDM — Type
DiscreteTimeCDM

Discrete-time Causal Dynamical Model with named endogenous variables.

Fields

  • variables: endogenous names (documentation / packing order)
  • initialise: (rng) -> NamedTuple of initial endogenous values at t = 1
  • sample_noise: (rng, state, t) -> NamedTuple of exogenous draws for time step t
  • step: (state, t, noise, intervention) -> NamedTuple next 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.

source
CausalDynamics.CDMPanel — Type
CDMPanel

Wide observational panel from $n$ independent DiscreteTimeCDM trajectories of length $T$.

Fields

  • n, T: dimensions
  • data: column name → length-n vectors
  • column_order: stable column order for table conversion
  • variables: endogenous symbols from the CDM
  • baseline, timed, terminal: role partition used when building columns
  • temporal_spec: optional TemporalDAGSpec for downstream ID
source
CausalDynamics.panel_column_name — Function
panel_column_name(var, t) -> Symbol

Default time-indexed column name: concatenate the variable symbol and time index with no separator (:a, 2 → :a2).

source
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.

source
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 as name_fn(v, t) for $t = 1:T$; default is all endogenous variables not listed in baseline or terminal
  • terminal: symbols emitted once from time index $T$ (bare name), e.g. [:y]
  • name_fn: (var::Symbol, t::Int) -> Symbol (default panel_column_name)
  • temporal_spec: optional lag DAG carried for ID hand-off
source
CausalDynamics.RandomEffectSpec — Type
RandomEffectSpec

Specification for nested exogenous noise when simulating hierarchical panels.

Fields

  • cluster_col: column name for the cluster id in the panel (Float64 codes)
  • 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.

source
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
source
CausalDynamics.draw_nested_effects — Function
draw_nested_effects(cluster_ids, spec; rng) -> NamedTuple

Draw 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.

source
CausalDynamics.add_nested_effects! — Function
add_nested_effects!(panel, cluster_ids, effects, spec) -> CDMPanel

Add $U_j + U_{ij}$ to spec.outcome_vars and store cluster_ids under spec.cluster_col. Mutates panel.data / column_order.

source
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_clusters clusters
  • $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.

source
CausalDynamics.HierarchicalNestingSpec — Type
HierarchicalNestingSpec

Unit-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 unit
  • unit_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
source
CausalDynamics.unroll_hierarchical_dag — Function
unroll_hierarchical_dag(spec, n_units; n_clusters=1, cluster_of_unit=nothing)
    -> HierarchicalUnrolling

Expand 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$).

source
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.

source
CausalDynamics.ObservationBridge — Type
ObservationBridge(mapping; measure=nothing, availability=nothing)

Declare how latent (or endogenous) series become estimation columns.

Fields

  • mapping: when measure is nothing, source series symbol → panel symbol; when measure is set, optional rename of measure output keys → panel symbols (missing keys keep their measure names)
  • measure: optional (state_nt, t) -> NamedTuple producing observed values from the full endogenous state at time t
  • availability: 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.
source
CausalDynamics.available_at — Function
available_at(bridge, variable, decision_t) -> Bool

Return whether variable is in the decision information set at decision_t. When availability is unset, every mapped variable is treated as available.

source
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.

source
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).

source
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).

source
CausalDynamics.ObservationMask — Type
ObservationMask

Binary response indicators aligned with named columns.

Fields

  • columns: column symbols (one per mask column)
  • observed: n × length(columns) bit matrix; true means R = 1 (observed)
  • time_indexed: if true, columns are time steps of a timed process (e.g. :y1, :y2)
source
CausalDynamics.observation_mask — Function
observation_mask(data; columns, time_indexed=false) -> ObservationMask

Build 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.

source
CausalDynamics.require_complete_values — Function
require_complete_values(x; context="values") -> x

Return 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).

source
CausalDynamics.require_complete_matrix — Function
require_complete_matrix(X; context="matrix") -> X

Reject 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.

source
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.

source
apply_missingness_mechanism(panel::CDMPanel, spec; kwargs...)
    -> (Dict{Symbol, Vector}, ObservationMask)

Apply apply_missingness_mechanism to the wide columns of a complete CDMPanel.

source
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 (length d)
  • encode: callable on an $n × p$ matrix. Return an $n × d$ matrix, or a length-n vector of length-d vectors / NamedTuples whose keys match code_names when NamedTuples are used
  • role: :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.

source
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).

source
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 variable
  • parents: 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: optional RepresentationSpec linking Phase 1 codes
  • output_dim: dimension of the node (1 for scalars; >1 for code/tensor vectors)
  • model, parameters, states: filled by the Lux extension (nothing until attach)
source
CausalDynamics.build_ode_rhs — Function
build_ode_rhs(known_rhs!, spec, lib) -> Function

Compose an in-place ODE RHS: known_rhs! then add Lux residuals for :ode_residual mechanisms, packing only declared parents. Requires Lux ext.

source
CausalDynamics.graphscm_with_mechanisms — Function
graphscm_with_mechanisms(g, equations, exogenous, lib; node_names) -> GraphSCM

Replace selected equation entries with Lux :static / :generative mechanisms from lib. Requires Lux ext.

source
CausalDynamics.abduce_noise — Function
abduce_noise(spec, x, pa) -> noise

Infer additive exogenous noise $U = X - f(\mathrm{pa})$ for a :generative mechanism. Requires Lux ext when spec.model is attached.

source
CausalDynamics.mechanism_counterfactual — Function
mechanism_counterfactual(spec, x_factual, pa_factual, pa_cf) -> x_cf

Pearl L3 for one generative node: abduce $U$ from the factual, then generate under counterfactual parents (same $U$). Requires Lux ext.

source
CausalDynamics.DoSequence — Type
DoSequence

Time-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).

source
CausalDynamics.do_sequence — Function
do_sequence(variable::Symbol, values)

Build a DoSequence fixing variable to values over time.

source
do_sequence(pairs::Pair{Symbol, <:Any}...)

Build a DoSequence from variable => assignment pairs.

source
CausalDynamics.Policy — Type
Policy

State-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's mapping and availability. At decision time t the rule sees only the state variables whose observed counterpart is available_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.

source
CausalDynamics.policy — Function
policy(variable::Symbol, rule; information_set=nothing)

Build a Policy assigning variable via rule(state, t).

source
policy(pairs::Pair{Symbol, <:Any}...; information_set=nothing)

Build a Policy from variable => rule pairs, each rule(state, t).

source
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.

source
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.

source
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 length T)
  • intervention: DoSequence for the counterfactual world
  • initial: optional endogenous NamedTuple at t = 1 before do (default: cdm.initialise with a fixed seed — pass factual initials when they matter)
source
CausalDynamics.GComputationResult — Type
GComputationResult

Monte Carlo g-computation summary for an outcome under a fixed intervention.

Fields

  • mean: mean terminal outcome across replicate trajectories
  • std: standard deviation across replicates
  • n: number of replicates
  • samples: terminal outcome per replicate
  • provenance: optional semantic manifest for the analysis
source
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: a DiscreteTimeCDM
  • T: number of time steps per replicate
  • outcome: endogenous variable symbol to summarise
  • intervention: DoSequence or Policy
  • n: replicate count
  • rng: random source (advanced across replicates)
  • reduce: series summary, e.g. last, Statistics.mean

Returns a GComputationResult.

source
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.

source

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 step type-stable. Return the same NamedTuple field names and element types on every call; a variable that is Int at t = 1 and Float64 later forces boxed storage. Prefer 0.0 over 0 in initialise.
  • Avoid allocation inside step. Build the returned NamedTuple directly rather than assembling intermediate arrays or dictionaries.
  • Reuse noise for counterfactuals. counterfactual copies 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 single rng, 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.mean

Auditable 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.

source
CausalDynamics.intervention_descriptor — Function

Return an auditable descriptor for a static SCM intervention.

source

Return one descriptor for each assignment in a discrete-time do sequence.

source

Return one descriptor for each endogenous variable governed by a policy regime.

source

Return the audit descriptor for a typed state assignment.

source
CausalDynamics.canonical_intervention_descriptor — Function
canonical_intervention_descriptor(descriptor) -> String

Return 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.

source

Encode a composed audit bundle, including sequential repeats on one target.

source
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.

source
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.

source
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.

source
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.

source
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.

source
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.

source
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.

source
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.

source
CausalDynamics.constraint_certificate — Function
constraint_certificate(spec) -> NamedTuple

Return 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.

source

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 loaded

Leave 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[],
)