Time-indexed graphs

Discrete-time CDMs often share a time-invariant lag structure. Unroll that structure to a static DAG over times t = 0:T under a TimeUnrolledGraph, then apply standard identification on the causal projection. Node multiplicity follows temporal_support and graph_kind, not ontological_character. Single-node supports (FromOnsetSupport, GlobalSupport, …) keep one reused node that may be assigned at an explicit onset.

CausalDynamics.GlobalSupport — Type

No finite window under the model’s declared clock, when that property is stated separately from time invariance. Do not use this type to mean “support unspecified”: leave support unspecified or declare it explicitly. Time invariance of a value or mechanism is a distinct declaration, not an alias of global support.

source
CausalDynamics.ReferentSpec — Type
ReferentSpec(id; identity_criterion=nothing, ontological_character=:unspecified)

Declare what a variable concerns. Prefer placing identity_criterion and optional ontological_character here so measurements of the same referent share one declaration. identity_criterion is required only when continuity of the referent matters to the argument. An unspecified ontology is not a defective causal model.

source
CausalDynamics.LaggedEdge — Type
LaggedEdge(parent, child, lag; relation_kind=:causal_influence)

Directed edge from parent at child_time - lag to child.

For a single-node child, child_time is its onset; for a single-node parent, the edge is emitted to every active time of the child.

source
CausalDynamics.TemporalNodeSpec — Type
TemporalNodeSpec(name; temporal_support=PointwiseSupport(),
    value_representation=:unspecified, referent=nothing, causal_role=nothing)

Describe one variable in a TemporalDAGSpec by three orthogonal declarations:

  • temporal_support: a TemporalSupport (or the symbol shorthands accepted by parse_temporal_support). It alone decides how many nodes the variable contributes when unrolled. A from-onset attribute must name its onset: FromOnsetSupport(t₀).
  • value_representation: what a node's value is (:state, :event, :attribute, :trajectory, :interval_summary, …).
  • referent: an optional ReferentSpec naming what the variable is about, together with any identity_criterion and ontological_character. referent_id = :sheep is shorthand for referent = ReferentSpec(:sheep). Identity and ontology are declared on the referent, never on the node.

causal_role is optional descriptive metadata (:assigned, :mediator, …) read by downstream planners; it does not alter the graph.

Derived read-only properties: onset_time (from the support; 0 for pointwise and global supports), referent_id, identity_criterion, ontological_character.

source
CausalDynamics.TemporalDAGSpec — Type
TemporalDAGSpec(; entity, nodes, edges, graph_kind=TimeUnrolledGraph())

Time-invariant temporal graph specification over TemporalNodeSpecs.

entity is an optional display label. Every node must have a unique name. graph_kind defaults to TimeUnrolledGraph; process and semantic graphs are not automatically valid for d-separation or adjustment.

source
CausalDynamics.unroll_temporal_dag — Function
unroll_temporal_dag(spec, T)

Unroll spec over times t = 0:T under TimeUnrolledGraph.

Single-node supports (FromOnsetSupport, GlobalSupport, …) contribute one node. A pointwise-to-single-node edge is an assignment into persistence: with child onset a, lag ℓ connects parent[a - ℓ] to the reused child node.

source
CausalDynamics.temporal_node — Function
temporal_node(unrolling, variable, t)

Return the node for variable at time t. Single-node supports resolve to their reused node once active at t.

source
CausalDynamics.temporal_edge_role — Function
temporal_edge_role(unrolling, parent, child) -> Symbol

Return the semantic role of an edge in a temporal unrolling.

If the edge was declared with a relation_kind other than :causal_influence (:constitutive_persistence, :constitutive_dependence, :participation, :measurement, …), that declared kind is returned. Otherwise the role is a construction-pattern label for a causal-influence edge, describing only the supports of its endpoints:

  • :onset_assignment — pointwise parent assigns into a single-node child
  • :recurrent_influence — single-node parent influences a pointwise child
  • :pointwise_influence — pointwise to pointwise
  • :causal_influence — single-node to single-node

Support patterns never make an edge constitutive: an onset assignment is an ordinary causal edge unless declared otherwise. Derived roles do not alter the unrolled topology used for display.

source
CausalDynamics.causal_projection — Function
causal_projection(unrolling) -> NamedTuple

Return the subgraph of causal-influence edges together with binding non-causal constraints (constitution, participation, measurement, …) that remain relevant for intervention admissibility.

Membership is decided only by the declared relation_kind of each edge. Support patterns (pointwise → single-node onset assignments, and so on) are construction facts and never demote a declared causal edge to a constraint: a diagnosis that assigns a subsequently maintained treatment is a causal parent of that treatment. To record a constitutive relation, declare it with LaggedEdge(...; relation_kind = :constitutive_persistence) or :constitutive_dependence.

source
CausalDynamics.validate_intervention_semantics — Function
validate_intervention_semantics(nodes, intervention; constraints, kwargs...)

Combined P0 gate: interval-summary scalar $do$ refusal and feasibility constraints. Call from simulate/identify paths that carry a temporal spec. Justifications are InterventionJustification records (macro_intervention_justification, feasibility_justification).

source
CausalDynamics.assert_interval_summary_do! — Function
assert_interval_summary_do!(nodes, intervention; justification=nothing)

Refuse a scalar $do(·)$ on an :interval_summary target unless justification is an InterventionJustification of a macro-intervention kind (:trajectory_generator, :admissible_trajectory_distribution, or :certificate_note) that names that target. A bare Boolean records nothing and is not accepted.

source
CausalDynamics.InterventionJustification — Type
InterventionJustification(kind, targets, note; source=nothing)

Recorded justification that licenses an otherwise-refused intervention. It is an artefact for the certificate, not a switch: it names what is claimed (kind), for which targets, and why (note), so the claim can be audited and travels with the analysis.

Accepted kinds:

  • macro-interventions on :interval_summary targets — :trajectory_generator, :admissible_trajectory_distribution, :certificate_note;
  • :feasibility constraints — :physical_justification.

note must be non-empty. source may point to a document, protocol, or certificate identifier.

source
CausalDynamics.ObservationSemantics — Type
ObservationSemantics(; realisation, sampling_interval, aggregation_window, measurement, availability)

Separate when a quantity is realised, sampled, summarised, measured, and available to a decision-maker.

source
CausalDynamics.parse_temporal_support — Function
parse_temporal_support(support; onset=nothing) -> TemporalSupport

Accept a typed support or a symbol shorthand (:point, :pointwise, :global).

:interval is refused: use IntervalSupport with an explicit window. :from_onset is refused unless an explicit onset is supplied: the onset must follow the mechanism (an attribute generated by a lag-one assignment from t = 0 has onset 1; one present at baseline has onset 0), so a symbol alone does not name it. Prefer FromOnsetSupport(t₀).

source

Single-node attributes from onset

Typed support decides node count. A pasture assignment that persists from onset uses FromOnsetSupport (and usually value_representation = :attribute). Identity and ontology, when claimed, live on a shared ReferentSpec and never change node count.

using CausalDynamics, Graphs

sheep = ReferentSpec(:sheep; identity_criterion = :administrative_identifier)
spec = TemporalDAGSpec(
    entity = :sheep,
    nodes = [
        TemporalNodeSpec(:diagnosis; value_representation = :state, referent = sheep),
        TemporalNodeSpec(
            :pasture;
            temporal_support = FromOnsetSupport(1),
            value_representation = :attribute,
            causal_role = :assigned,
            referent = sheep,
        ),
        TemporalNodeSpec(:weight; value_representation = :state, referent = sheep),
    ],
    edges = [
        (:diagnosis, :pasture, 1),
        (:pasture, :weight, 0),
        (:weight, :weight, 1),
    ],
)
u = unroll_temporal_dag(spec, 2)
nv(u.graph), temporal_node_label(u, single_node(u, :pasture))
(7, "pasture")

Panel mapping follows support: single-node variables keep their bare column symbol; pointwise variables use panel_column_name. Prefer declaring temporal_support on the DAG rather than a downstream unit_level override. The symbol shorthand :from_onset is refused without an onset; write FromOnsetSupport(t₀).

The unrolled graph retains edge provenance. Use temporal_edge_role for one edge and temporal_edge_records for an auditable inventory. For edges declared with a relation_kind other than :causal_influence the role is that declared kind. For causal-influence edges the role is a construction-pattern label only: :onset_assignment (pointwise → single-node), :recurrent_influence (single-node → pointwise), :pointwise_influence (pointwise → pointwise), or :causal_influence (single-node → single-node). Support patterns never make an edge constitutive: a diagnosis assigning a subsequently maintained treatment is a causal parent of that treatment.

causal_projection keeps every edge declared :causal_influence and records every other declared kind (:constitutive_persistence, :constitutive_dependence, :participation, :measurement, …) as a constraint. Constitution therefore has to be declared; it is never inferred from node multiplicity.

proj = causal_projection(u)
ne(proj.graph), length(proj.constraints)
(5, 0)
records = temporal_edge_records(u)
filter(record -> record.role === :onset_assignment, records)
1-element Vector{NamedTuple{(:parent, :child, :role)}}:
 (parent = (:diagnosis, 0), child = (:pasture, nothing), role = :onset_assignment)

Plotting an unrolling

With DAGMakie.jl loaded, DAGMakie.dagplot_temporal places times left→right and variables as rows. Markers follow value_representation: only :interval_summary uses a rounded rectangle under the package convention. From-onset attributes and pointwise states remain circles unless you pass an explicit node_marker. Shape does not encode enduring identity. The CausalDynamics extension adds a TemporalUnrolling method automatically.

using CausalDynamics, DAGMakie, CairoMakie

spec = TemporalDAGSpec(
    nodes = [
        TemporalNodeSpec(:x; value_representation = :state),
        TemporalNodeSpec(:y; value_representation = :state),
    ],
    edges = [(:x, :x, 1), (:y, :y, 1), (:x, :y, 1)],
)
u = unroll_temporal_dag(spec, 3)

fig, ax, p = dagplot_temporal(u;
    figure_size = (520, 260),
    fit_node_size_to_labels = false,
    node_size = 28,
)
fig
Example block output
using CausalDynamics, DAGMakie, CairoMakie

spec = TemporalDAGSpec(
    entity = :sheep,
    nodes = [
        TemporalNodeSpec(:diagnosis; value_representation = :state),
        TemporalNodeSpec(
            :pasture;
            temporal_support = FromOnsetSupport(1),
            value_representation = :attribute,
            causal_role = :assigned,
        ),
        TemporalNodeSpec(:weight; value_representation = :state),
    ],
    edges = [
        (:diagnosis, :pasture, 1),
        (:pasture, :weight, 0),
        (:weight, :weight, 1),
    ],
)
u = unroll_temporal_dag(spec, 2)
fig, ax, p = dagplot_temporal(u;
    figure_size = (560, 280),
    fit_node_size_to_labels = false,
    node_size = 26,
)
fig
Example block output

Confounded treatment (book Ch. 28)

using CausalDynamics

spec = TemporalDAGSpec(
    nodes = [TemporalNodeSpec(v; value_representation = :state) for v in [:x, :y, :a, :c]],
    edges = [
        (:c, :c, 1), (:a, :c, 1), (:c, :a, 0),  # confounder dynamics + confounding
        (:x, :x, 1), (:a, :x, 1), (:c, :x, 1),  # state evolution
        (:x, :y, 0),                             # measurement
    ],
)
u = unroll_temporal_dag(spec, 10)

# Effect of A_{t-1} on X_t: adjust for C_{t-1}
adj = temporal_backdoor_adjustment_nodes(u, :a, 2, :x, 2)
# Set containing (:c, 1)

See also Utilities, Discrete-time CDMs, and the CDCS book Ch. 28.

Baseline assignment into a from-onset attribute

A point-supported diagnosis can assign a value that persists thereafter. With FromOnsetSupport(1), a lag-one edge from diagnosis connects diagnosis[0] to the single pasture node:

spec = TemporalDAGSpec(
    entity = :sheep,
    nodes = [
        TemporalNodeSpec(:diagnosis; value_representation = :state),
        TemporalNodeSpec(
            :pasture;
            temporal_support = FromOnsetSupport(1),
            value_representation = :attribute,
        ),
        TemporalNodeSpec(:weight; value_representation = :state),
    ],
    edges = [
        (:diagnosis, :pasture, 1),
        (:pasture, :weight, 0),
    ],
)
u = unroll_temporal_dag(spec, 4)
temporal_node(u, :diagnosis, 0)
single_node(u, :pasture)