Graph.DSep

d-separation on directed acyclic graphs: the Bayes-Ball characterization, ancestral closures, moralization bridges, and equivalences between the criteria.

Ordered­Local­SG 6 core · 15 supporting · 4 submodules This interface exports the ordered-local derivation rules, the active-walk surgery and joining lemmas used by the peel induction, and the theorem that d-separation yields an ordered-local semi-graphoid derivation.
Bayes­Ball 9 core · 6 supporting This file implements the Bayes Ball reachability computation for finite directed acyclic graphs. ★ bbReachable_minimal

Bayes Ball Reachability

This file implements the Bayes Ball reachability computation for finite directed acyclic graphs. Given source and conditioning sets, it tracks directed arrival states and returns the vertices reachable along walks that remain active under the conditioning information.

The core definitions are BBDir, BBState, bbZAncestors, one-step transition bbStep, initial frontier bbInit, reachable state set bbReachable, and vertex projection bbReachableVertices. The exposed invariants include source monotonicity (bbReachable_mono_source, bbReachableVertices_mono_source), minimality of the fixed point, and closure of reachable states under bbStep.

inductive BBDir reviewed
Causalean.Graph.DAG

The Bayes Ball arrival-direction type has exactly two cases: arrival at a vertex from one of its parents and arrival at a vertex from one of its children.

Definition (Lean source)
BBDir :
Type
fromParent :
fromChild :
deriving DecidableEq
Causalean.Graph.DAG.BBDir · Causalean/Graph/DSep/BayesBall.lean:54
abbrev BBState reviewed
Causalean.Graph.DAG

For a vertex population, a Bayes Ball state is a vertex together with an indication of whether the ball arrived from a parent or from a child.

Definition (Lean source)
V :
Type*
BBState V :
Type (max u_2 0)
V × BBDir
Causalean.Graph.DAG.BBState · Causalean/Graph/DSep/BayesBall.lean:68
def bbZAncestors reviewed
Causalean.Graph.DAG

For a finite directed acyclic graph on a vertex population and a conditioning set, the activated-collider set consists of every conditioned vertex and every ancestor of a conditioned vertex.

Definition (Lean source)
V :
Type u_1
shared
G :
DAG V
shared
Z :
bbZAncestors G Z :
G.ancestralSet Z
Causalean.Graph.DAG.bbZAncestors · Causalean/Graph/DSep/BayesBall.lean:74 · uses DAG
def bbStep reviewed
Causalean.Graph.DAG

For a finite directed acyclic graph on a vertex population, a conditioning set, and a current Bayes Ball state, writing the state as its vertex and arrival direction, the one-step successor set sends an arrival from a child at an unconditioned vertex to its parents and children, and sends such an arrival at a conditioned vertex nowhere; it sends an arrival from a parent at an unconditioned vertex to its children and, when that vertex is an ancestor of a conditioned vertex, also to its parents, while an arrival from a parent at a conditioned vertex goes to its parents.

Definition (Lean source)
V :
Type u_1
shared
G :
DAG V
shared
Z :
s :
bbStep G Z s :
(w, :
s (: dir))
match dir with | BBDir.fromChild
=> if w ∈ Z then ∅ else (G.parents w).map ⟨(·, BBDir.fromChild), fun _ _ h => by simpa using h⟩ ∪ (G.children w).map ⟨(·, BBDir.fromParent), fun _ _ h => by simpa using h⟩ | BBDir.fromParent => if w ∉ Z then (G.children w).map ⟨(·, BBDir.fromParent), fun _ _ h => by simpa using h⟩ ∪ if w ∈ G.bbZAncestors Z then (G.parents w).map ⟨(·, BBDir.fromChild), fun _ _ h => by simpa using h⟩ else ∅ else (G.parents w).map ⟨(·, BBDir.fromChild), fun _ _ h => by simpa using h⟩
Causalean.Graph.DAG.bbStep · Causalean/Graph/DSep/BayesBall.lean:82 · uses DAG , BBState
def bbReachAux reviewed
Causalean.Graph.DAG

Iterative Bayes Ball with a step budget. Given a conditioning set, the current frontier of ball states, the states visited so far, and a budget of expansion rounds, the fuel-bounded reachable set is obtained by returning the visited states when the budget is exhausted, and otherwise expanding the frontier by one Bayes Ball step, stopping with the visited states if no new state appears and recursing on the new states with one less round otherwise. It is the reachable set only when the budget suffices for the traversal to close, as arranged by bbReachable.

Definition (Lean source)
V :
Type u_1
shared
G :
DAG V
shared
Z :
frontier visited :
fuel :
ℕ
bbReachAux G Z frontier visited fuel :
match fuel with | 0
=> visited | fuel
+ 1
=> let newStates := frontier.biUnion (G.bbStep Z) \ visited if newStates = ∅ then visited else bbReachAux Z newStates (visited ∪ newStates) fuel
Causalean.Graph.DAG.bbReachAux · Causalean/Graph/DSep/BayesBall.lean:120 · uses DAG , BBState
def bbInit reviewed
Causalean.Graph.DAG

For a finite directed acyclic graph on a vertex population and a source set, the initial Bayes Ball frontier contains, for every source vertex, each child paired with arrival from a parent and each parent paired with arrival from a child.

Definition (Lean source)
V :
Type u_1
shared
G :
DAG V
shared
X :
bbInit G X :
X.biUnion (fun x => (G.children x).map ⟨(·, BBDir.fromParent), fun _ _ h => by simpa using h⟩ ∪ (G.parents x).map ⟨(·, BBDir.fromChild), fun _ _ h => by simpa using h⟩)
Causalean.Graph.DAG.bbInit · Causalean/Graph/DSep/BayesBall.lean:262 · uses DAG , BBState
def bbReachable reviewed
Causalean.Graph.DAG

For a finite directed acyclic graph on a vertex population, a conditioning set, and a source set, the Bayes Ball reachable-state set is the set returned by repeatedly applying the one-step transition to the initial frontier, with a limit of twice the number of graph vertices plus one rounds.

Definition (Lean source)
V :
Type u_1
shared
G :
DAG V
shared
Z X :
bbReachable G Z X :
bbReachAux G Z (G.bbInit X) (G.bbInit X) (2 * card V + 1)
Causalean.Graph.DAG.bbReachable · Causalean/Graph/DSep/BayesBall.lean:277 · uses DAG , BBState
def bbReachableVertices reviewed
Causalean.Graph.DAG

For a finite directed acyclic graph on a vertex population, a conditioning set, and a source set, the Bayes Ball reachable-vertex set is the set of vertices appearing in the reachable states, with their arrival directions discarded.

Definition (Lean source)
V :
Type u_1
shared
G :
DAG V
shared
Z X :
bbReachableVertices G Z X :
(G.bbReachable Z X).image fst
Causalean.Graph.DAG.bbReachableVertices · Causalean/Graph/DSep/BayesBall.lean:359 · uses DAG
theorem bbReachable_minimal reviewed
Causalean.Graph.DAG

bbReachable Z X is the least superset of bbInit X closed under bbStep Z: for a conditioning set Z and source set X, if a candidate set S of Bayes-Ball states contains the initial frontier bbInit X and S is closed under the Bayes-Ball step relation bbStep Z, then S contains every state reachable via bbReachable Z X.

Formal statement
V :
Type u_1
shared
G :
DAG V
shared
Z X :
S :
hinit :
G.bbInit X ⊆ S
hstep :
∀ s ∈ S, G.bbStep Z s ⊆ S
G.bbReachable Z X ⊆ S
Proof (Lean source)
theorem bbReachable_minimal (Z X : Finset V) (S : Finset (BBState V)) (hinit : G.bbInit X ⊆ S) (hstep : ∀ s ∈ S, G.bbStep Z s ⊆ S) : G.bbReachable Z X ⊆ S := by unfold bbReachable refine bbReachAux_closed G Z _ hinit hinit ?_ intro x hx rw [Finset.mem_biUnion] at hx obtain ⟨s, hs, hxs⟩ := hx exact hstep s hs hxs
6 supporting declarations (lemmas, instances)
  • instDecidableEqBBDir instance
    deriving DecidableEq
    Causalean.Graph.DAG.instDecidableEqBBDir · Causalean/Graph/DSep/BayesBall.lean:60
  • instFintypeBBDir instance — The finite enumeration of Bayes Ball arrival directions lists the two arrival directions and certifies that every arrival direction is listed.
    instFintypeBBDir :
    clause 1
    elems := {BBDir.fromParent, BBDir.fromChild}
    clause 2
    complete := fun b => by cases b <;> decide
    Causalean.Graph.DAG.instFintypeBBDir · Causalean/Graph/DSep/BayesBall.lean:62
  • bbReachable_mono_source theorem — In a finite directed acyclic graph, for a conditioning set and two source sets, containment of the smaller source set implies that every state reachable from it is reachable from the larger source set.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    Z :
    X X' :
    hXX' :
    X' ⊆ X
    G.bbReachable Z X' ⊆ G.bbReachable Z X
    Proof (Lean source)
    theorem bbReachable_mono_source {Z : Finset V} {X X' : Finset V} (hXX' : X' ⊆ X) : G.bbReachable Z X' ⊆ G.bbReachable Z X := by simp only [bbReachable] exact bbReachAux_mono_combined G Z (refl _) (Finset.biUnion_subset_biUnion_of_subset_left _ hXX') _
    Causalean.Graph.DAG.bbReachable_mono_source · Causalean/Graph/DSep/BayesBall.lean:370
  • bbReachableVertices_mono_source theorem — In a finite directed acyclic graph, for a conditioning set and two source sets, containment of the smaller source set implies that every vertex reachable from it is reachable from the larger source set.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    Z :
    X X' :
    hXX' :
    X' ⊆ X
    G.bbReachableVertices Z X' ⊆ G.bbReachableVertices Z X
    Proof (Lean source)
    theorem bbReachableVertices_mono_source {Z : Finset V} {X X' : Finset V} (hXX' : X' ⊆ X) : G.bbReachableVertices Z X' ⊆ G.bbReachableVertices Z X := Finset.image_subset_image (G.bbReachable_mono_source hXX')
    Causalean.Graph.DAG.bbReachableVertices_mono_source · Causalean/Graph/DSep/BayesBall.lean:382
  • bbReachable_init_subset theorem — In a finite directed acyclic graph, a source set and conditioning set have their initial Bayes Ball frontier contained in the reachable-state set.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    Z X :
    G.bbInit X ⊆ G.bbReachable Z X
    Proof (Lean source)
    theorem bbReachable_init_subset (Z X : Finset V) : G.bbInit X ⊆ G.bbReachable Z X := by unfold bbReachable exact visited_sub_bbReachAux G Z (G.bbInit X) (G.bbInit X) _
    Causalean.Graph.DAG.bbReachable_init_subset · Causalean/Graph/DSep/BayesBall.lean:394
  • bbReachable_bbStep_subset theorem — In a finite directed acyclic graph, a source set, conditioning set, and state with the state already reachable have every one-step successor reachable as well.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    Z X :
    s :
    hs :
    s ∈ G.bbReachable Z X
    G.bbStep Z s ⊆ G.bbReachable Z X
    Proof (Lean source)
    theorem bbReachable_bbStep_subset (Z X : Finset V) {s : BBState V} (hs : s ∈ G.bbReachable Z X) : G.bbStep Z s ⊆ G.bbReachable Z X := by -- Apply `bbReachAux_closed_of_invariant` with the initial setup -- `frontier = visited = bbInit X` and the trivial invariant -- (no element is outside the frontier). The fuel `2 * card V + 1` -- is enough since the state space has size `2 * card V`. unfold bbReachable at hs ⊢ apply bbReachAux_closed_of_invariant G Z (G.bbInit X) (G.bbInit X) (2 * card V + 1) (refl _) · intro s hsv hsf; exact absurd hsv hsf · -- fuel + visited.card = 2|V| + 1 + visited.card ≥ 2|V| + 1 have h := zero_le (G.bbInit X).card omega · exact hs
    Causalean.Graph.DAG.bbReachable_bbStep_subset · Causalean/Graph/DSep/BayesBall.lean:418
Active­Path 6 core · 14 supporting This file defines active walks in directed acyclic graphs relative to a conditioning set. ★ IsActiveWalk★ bbReachableVertices_iff_activeWalk

Active Walks

This file defines active walks in directed acyclic graphs relative to a conditioning set. Unlike the textbook simple paths in Basic Concepts.tex, these finite vertex sequences may repeat vertices. It records undirected adjacency (UAdj), collider triples (IsCollider), walk activity (IsActiveWalk), and active-walk existence between sets (HasActiveWalk).

The main walk lemmas prove reversal symmetry (isActiveWalk_reverse, hasActiveWalk_symm), directed-walk activity helpers, suffix extraction for source-to-conditioning transfer, and the Bayes Ball correctness theorem bbReachableVertices_iff_activeWalk, which identifies computed reachability with existence of an active walk.

def UAdj reviewed
Causalean.Graph.DAG

For a finite directed acyclic graph on a vertex population and two vertices, undirected adjacency holds exactly when a directed edge joins the two vertices in either direction.

Definition (Lean source)
V :
Type u_1
shared
G :
DAG V
shared
u v :
V
UAdj G u v :
Prop
G.edge u v ∨ G.edge v u
Causalean.Graph.DAG.UAdj · Causalean/Graph/DSep/ActivePath.lean:62 · uses DAG
def IsCollider reviewed
Causalean.Graph.DAG

For a finite directed acyclic graph on a vertex population and three ordered vertices, the collider condition holds exactly when both outer vertices have directed edges into the middle vertex.

Definition (Lean source)
V :
Type u_1
shared
G :
DAG V
shared
l m r :
V
IsCollider G l m r :
Prop
clause 1
G.edge l m
clause 2
G.edge r m
Causalean.Graph.DAG.IsCollider · Causalean/Graph/DSep/ActivePath.lean:74 · uses DAG
def IsActiveWalk reviewed
Causalean.Graph.DAG

For a finite directed acyclic graph on a vertex population, a conditioning set, and a finite vertex sequence, the active-walk condition holds exactly when each consecutive pair is joined by a directed edge in one direction or the other, and at every consecutive triple, its middle vertex is a collider that is either conditioned on or has a conditioned descendant, or else is a non-collider outside the conditioning set.

Definition (Lean source)
V :
Type u_1
shared
G :
DAG V
shared
Z :
p :
IsActiveWalk G Z p :
Prop
clause 1
i :
ℕ
hi :
i + 1 < p.length
G.UAdj (p.get ⟨i, by omega⟩) (p.get ⟨i + 1, hi⟩)
clause 2
i :
ℕ
hi :
i + 2 < p.length
l :
p.get ⟨i, by omega⟩
m :
p.get ⟨i + 1, by omega⟩
r :
p.get ⟨i + 2, hi⟩
if G.IsCollider l m r then m ∈ G.bbZAncestors Z else m ∉ Z
Causalean.Graph.DAG.IsActiveWalk · Causalean/Graph/DSep/ActivePath.lean:89 · uses DAG
def HasActiveWalk reviewed
Causalean.Graph.DAG

For a finite directed acyclic graph on a vertex population, a source set, a target set, and a conditioning set, the active-walk existence condition holds exactly when there is a vertex sequence with at least two vertices that is active relative to the conditioning set, whose first vertex belongs to the source set, and whose last vertex belongs to the target set.

Definition (Lean source)
V :
Type u_1
shared
G :
DAG V
shared
X Y Z :
HasActiveWalk G X Y Z :
Prop
∃ (p : List V),
clause 1
p.length ≥ 2
clause 2
G.IsActiveWalk Z p
clause 3
p.head? ∈ (X.image some)
clause 4
p.getLast? ∈ (Y.image some)
Causalean.Graph.DAG.HasActiveWalk · Causalean/Graph/DSep/ActivePath.lean:115 · uses DAG
def lastIdxLt reviewed
Causalean.Graph.DAG

For a nonnegative integer bound and a predicate on nonnegative integers whose truth can be checked at every index, the optional last qualifying index is the largest index strictly below the bound that satisfies the predicate, if such an index exists, and is absent otherwise.

Definition (Lean source)
n :
ℕ
P :
ℕ → Prop
lastIdxLt n P :
Option ℕ
if h : ((range n).filter P).Nonempty then some (((range n).filter P).max' h) else none
Causalean.Graph.DAG.lastIdxLt · Causalean/Graph/DSep/ActivePath.lean:240
theorem bbReachableVertices_iff_activeWalk reviewed
Causalean.Graph.DAG

Bayes Ball correctness. For a source vertex set X and a conditioning vertex set Z and a vertex v, v lies in the breadth-first-search reachable set bbReachableVertices Z X if and only if there is an active walk, given Z, from some vertex of X to v containing at least two vertices.

Formal statement
V :
Type u_1
shared
G :
DAG V
shared
X Z :
v :
V
v ∈ G.bbReachableVertices Z X
↔ ∃ (x : V), x ∈ X ∧ ∃ (p : List V), p.length ≥ 2 ∧ G.IsActiveWalk Z p ∧ p.head? = some x ∧ p.getLast? = some v
Proof (Lean source)
theorem bbReachableVertices_iff_activeWalk (X Z : Finset V) (v : V) : v ∈ G.bbReachableVertices Z X ↔ ∃ (x : V), x ∈ X ∧ ∃ (p : List V), p.length ≥ 2 ∧ G.IsActiveWalk Z p ∧ p.head? = some x ∧ p.getLast? = some v := by constructor · intro hv rw [bbReachableVertices] at hv obtain ⟨s, hs, hsv⟩ := Finset.mem_image.mp hv rcases s with ⟨w, d⟩ change w = v at hsv subst w obtain ⟨x, hxX, u, r, _hdir, hact, hlast⟩ := G.bbReachable_state_has_reverse_activeWalk X Z hs refine ⟨x, hxX, (v :: u :: r).reverse, ?_, ?_, ?_, ?_⟩ · simp · exact G.isActiveWalk_reverse hact · rwa [List.head?_reverse] · rw [getLast?_reverse] rfl · rintro ⟨x, hxX, p, hlen, hact, hhead, hlast⟩ cases p with | nil => simp at hlen | cons a p' => cases p' with | nil => simp at hlen | cons b r => simp only [List.head?_cons] at hhead injection hhead with ha subst a exact G.bbReachableVertices_of_activeWalk_cons hxX hact hlast
Causalean.Graph.DAG.bbReachableVertices_iff_activeWalk · Causalean/Graph/DSep/ActivePath.lean:750 · uses DAG , IsActiveWalk , bbReachableVertices
14 supporting declarations (lemmas, instances)
  • decUAdj instance — For a finite vertex population with decidable equality, a directed acyclic graph on that population, and two vertices, the decision procedure for undirected adjacency determines whether a directed edge joins the vertices in either direction.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    u v :
    V
    decUAdj G u v :
    Decidable (G.UAdj u v)
    inferInstanceAs (Decidable (_ ∨ _))
    Causalean.Graph.DAG.decUAdj · Causalean/Graph/DSep/ActivePath.lean:67
  • decIsCollider instance — For a finite vertex population with decidable equality, a directed acyclic graph on that population, and three ordered vertices, the decision procedure for the collider condition determines whether each outer vertex has a directed edge into the middle vertex.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    l m r :
    V
    decIsCollider G l m r :
    Decidable (G.IsCollider l m r)
    inferInstanceAs (Decidable (_ ∧ _))
    Causalean.Graph.DAG.decIsCollider · Causalean/Graph/DSep/ActivePath.lean:82
  • UAdj_symm theorem — In a finite directed acyclic graph, adjacency of two vertices implies that the vertices are adjacent in the opposite order.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    u v :
    V
    h :
    G.UAdj u v
    G.UAdj v u
    Proof (Lean source)
    theorem UAdj_symm {u v : V} (h : G.UAdj u v) : G.UAdj v u := Or.comm.mp h
    Causalean.Graph.DAG.UAdj_symm · Causalean/Graph/DSep/ActivePath.lean:131
  • isActiveWalk_reverse theorem — In a finite directed acyclic graph, a walk and conditioning set with an active-walk certificate yield an active reversed walk.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    Z :
    p :
    h :
    G.IsActiveWalk Z p
    G.IsActiveWalk Z p.reverse
    Proof (Lean source)
    theorem isActiveWalk_reverse {Z : Finset V} {p : List V} (h : G.IsActiveWalk Z p) : G.IsActiveWalk Z p.reverse := by obtain ⟨hadj, hcoll⟩ := h set n := p.length refine ⟨fun i hi => ?_, fun i hi => ?_⟩ · -- Adjacency: p.reverse[i] and p.reverse[i+1] are UAdj have hn : i + 1 < n := by rw [List.length_reverse] at hi; exact hi rw [get_rev, get_rev] -- p[n-1-i] and p[n-1-(i+1)] = p[n-2-i] are UAdj -- From hadj: p[n-2-i] and p[n-2-i+1] = p[n-1-i] are UAdj have hj : n - 1 - (i + 1) + 1 < n := by omega have := hadj (n - 1 - (i + 1)) hj have heq1 : n - 1 - (i + 1) + 1 = n - 1 - i := by omega rw [show (⟨n - 1 - (i + 1) + 1, hj⟩ : Fin p.length) = ⟨n - 1 - i, by omega⟩ from Fin.ext heq1] at this exact G.UAdj_symm this · -- Collider condition: triple (p.reverse[i], p.reverse[i+1], p.reverse[i+2]) have hn : i + 2 < n := by rw [List.length_reverse] at hi; exact hi rw [get_rev, get_rev, get_rev] -- The triple (i, i+1, i+2) in the reverse corresponds to -- (n-1-i, n-2-i, n-3-i) in the original, which is the reversed triple -- (n-3-i, n-2-i, n-1-i). Use hcoll at index (n-3-i) with And.comm. have hj : p.length - 3 - i + 2 < p.length := by omega have horig := hcoll (p.length - 3 - i) hj simp only [IsCollider] at horig ⊢ -- The indices in horig are (n-3-i, n-3-i+1, n-3-i+2) = (n-3-i, n-2-i, n-1-i) -- The goal indices are (n-1-i, n-2-i, n-3-i) via get_rev -- These are the same vertices but the outer pair is swapped -- First, show the Fin values match: n-3-i+k and n-1-(i+2-k) are the same have heq0 : (⟨p.length - 3 - i, by omega⟩ : Fin p.length) = ⟨p.length - 1 - (i + 2), by omega⟩ := Fin.ext (by simp; omega) have heq1 : (⟨p.length - 3 - i + 1, by omega⟩ : Fin p.length) = ⟨p.length - 1 - (i + 1), by omega⟩ := Fin.ext (by simp; omega) have heq2 : (⟨p.length - 3 - i + 2, hj⟩ : Fin p.length) = ⟨p.length - 1 - i, by omega⟩ := Fin.ext (by simp; omega) simp only [heq0, heq1, heq2] at horig -- horig and goal differ only by And.comm in the if-condition. -- Introduce abbreviations for the three vertices to avoid Fin issues. set a := p.get ⟨p.length - 1 - (i + 2), by omega⟩ set b := p.get ⟨p.length - 1 - (i + 1), by omega⟩ set c := p.get ⟨p.length - 1 - i, by omega⟩ -- horig : if (G.edge a b ∧ G.edge c b) then b ∈ ... else b ∉ ... -- goal : if (G.edge c b ∧ G.edge a b) then b ∈ ... else b ∉ ... -- Split on the condition; And.comm relates the two. by_cases hab : G.edge a b ∧ G.edge c b · simp only [hab] at horig simp only [hab.symm, horig] · simp only [hab, ite_false] at horig have : ¬(G.edge c b ∧ G.edge a b) := fun h => hab h.symm simp only [this, ite_false] exact horig
    Causalean.Graph.DAG.isActiveWalk_reverse · Causalean/Graph/DSep/ActivePath.lean:142
  • isActiveWalk_of_reversed_directed theorem — In a finite directed acyclic graph, a walk and conditioning set whose edges all point backward and whose interior vertices avoid the conditioning set form an active walk.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    Z :
    p :
    hdir :
    ∀ (i : ℕ) (hi : i + 1 < p.length), G.edge (p.get ⟨i + 1, hi⟩) (p.get ⟨i, by omega⟩)
    hZ :
    ∀ (i : ℕ) (hi : i + 2 < p.length), p.get ⟨i + 1, by omega⟩ ∉ Z
    G.IsActiveWalk Z p
    Proof (Lean source)
    theorem isActiveWalk_of_reversed_directed {Z : Finset V} {p : List V} (hdir : ∀ (i : ℕ) (hi : i + 1 < p.length), G.edge (p.get ⟨i + 1, hi⟩) (p.get ⟨i, by omega⟩)) (hZ : ∀ (i : ℕ) (hi : i + 2 < p.length), p.get ⟨i + 1, by omega⟩ ∉ Z) : G.IsActiveWalk Z p := by refine ⟨fun i hi => ?_, fun i hi => ?_⟩ · -- Adjacency: hdir says G.edge p[i+1] p[i], so p[i] and p[i+1] are UAdj exact inr (hdir i hi) · -- Collider condition. Let l = p[i], m = p[i+1], r = p[i+2]. -- hdir at index i: G.edge m l (so NOT edge l m, by asymm) -- hdir at index i+1: G.edge r m -- IsCollider l m r requires G.edge l m ∧ G.edge r m; first conjunct fails. simp only have hml : G.edge (p.get ⟨i + 1, by omega⟩) (p.get ⟨i, by omega⟩) := hdir i (by omega) have hlm_false : ¬ G.edge (p.get ⟨i, by omega⟩) (p.get ⟨i + 1, by omega⟩) := G.asymm hml have hnotcoll : ¬ G.IsCollider (p.get ⟨i, by omega⟩) (p.get ⟨i + 1, by omega⟩) (p.get ⟨i + 2, hi⟩) := by intro ⟨h1, _⟩; exact hlm_false h1 rw [if_neg hnotcoll] -- Need p[i+1] ∉ Z, which follows from hZ exact hZ i hi
    Causalean.Graph.DAG.isActiveWalk_of_reversed_directed · Causalean/Graph/DSep/ActivePath.lean:201
  • lastIdxLt_eq_some theorem — For a bound and decidable predicate, if an index is returned as the final qualifying one, then it is below the bound, satisfies the predicate, and no larger index below the bound satisfies the predicate.
    n :
    ℕ
    P :
    ℕ → Prop
    i :
    ℕ
    h :
    lastIdxLt n P = some i
    conclusion 1
    i < n
    conclusion 2
    P i
    conclusion 3
    j :
    i < j
    j < n
    ¬ P j
    Proof (Lean source)
    theorem lastIdxLt_eq_some {n : ℕ} {P : ℕ → Prop} [DecidablePred P] {i : ℕ} (h : lastIdxLt n P = some i) : i < n ∧ P i ∧ ∀ j, i < j → j < n → ¬ P j := by unfold lastIdxLt at h split_ifs at h with hne · injection h with heq subst heq have hi_mem : ((range n).filter P).max' hne ∈ (range n).filter P := Finset.max'_mem _ hne rw [mem_filter, Finset.mem_range] at hi_mem refine ⟨hi_mem.1, hi_mem.2, ?_⟩ intro j hj hjn hPj have hjmem : j ∈ (range n).filter P := by rw [mem_filter, Finset.mem_range]; exact ⟨hjn, hPj⟩ have := Finset.le_max' _ j hjmem omega
    Causalean.Graph.DAG.lastIdxLt_eq_some · Causalean/Graph/DSep/ActivePath.lean:251
  • lastIdxLt_eq_none theorem — For a bound and decidable predicate, if no final qualifying index exists, then no index below the bound satisfies the predicate.
    n :
    ℕ
    P :
    ℕ → Prop
    h :
    i :
    i < n
    ¬ P i
    Proof (Lean source)
    theorem lastIdxLt_eq_none {n : ℕ} {P : ℕ → Prop} [DecidablePred P] (h : lastIdxLt n P = none) : ∀ i, i < n → ¬ P i := by intro i hi hPi have hmem : i ∈ (range n).filter P := by rw [mem_filter, Finset.mem_range]; exact ⟨hi, hPi⟩ have hne : ((range n).filter P).Nonempty := ⟨i, hmem⟩ simp [lastIdxLt, hne] at h
    Causalean.Graph.DAG.lastIdxLt_eq_none · Causalean/Graph/DSep/ActivePath.lean:271
  • take_suffix_at_last_S theorem — Given a directed acyclic graph, endpoint and conditioning sets, a walk, and its stated endpoints, if the first endpoint belongs to the source set, the walk has at least two vertices, is active under the combined conditioning set, begins at the stated source, and ends at the stated target, then some suffix remains active from the source-or-secondary-conditioning set to the same target and has no strictly interior vertex in the secondary conditioning set.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    X Z S :
    p :
    x w :
    V
    hxX :
    x ∈ X
    hlen :
    p.length ≥ 2
    hact :
    G.IsActiveWalk (Z ∪ S) p
    hhead :
    p.head? = some x
    hlast :
    p.getLast? = some w
    ∃ (x' : V) (q : List V),
    x' ∈ X ∪ S
    q.length ≥ 2
    G.IsActiveWalk (Z ∪ S) q
    q.head? = some x'
    q.getLast? = some w
    conclusion 1
    k :
    ℕ
    _hk1 :
    0 < k
    hk2 :
    k + 1 < q.length
    q.get ⟨k, by omega⟩ ∉ S
    Proof (Lean source)
    theorem take_suffix_at_last_S {X Z S : Finset V} {p : List V} {x w : V} (hxX : x ∈ X) (hlen : p.length ≥ 2) (hact : G.IsActiveWalk (Z ∪ S) p) (hhead : p.head? = some x) (hlast : p.getLast? = some w) : ∃ (x' : V) (q : List V), x' ∈ X ∪ S ∧ q.length ≥ 2 ∧ G.IsActiveWalk (Z ∪ S) q ∧ q.head? = some x' ∧ q.getLast? = some w ∧ (∀ (k : ℕ) (_hk1 : 0 < k) (hk2 : k + 1 < q.length), q.get ⟨k, by omega⟩ ∉ S) := by -- Interior-S-index set: i : Fin p.length with i+1 < p.length and p.get i ∈ S. let IS : Finset (Fin p.length) := Finset.univ.filter (fun i => i.val + 1 < p.length ∧ p.get i ∈ S) by_cases hIS : IS.Nonempty · -- Cut at the maximum. let i₀ : Fin p.length := IS.max' hIS have hi₀_mem : i₀ ∈ IS := Finset.max'_mem _ hIS have hi₀_filt : i₀.val + 1 < p.length ∧ p.get i₀ ∈ S := by have := hi₀_mem simp only [IS, mem_filter, Finset.mem_univ, true_and] at this exact this let q : List V := p.drop i₀.val refine ⟨p.get i₀, q, ?_, ?_, ?_, ?_, ?_, ?_⟩ · exact mem_union_right _ hi₀_filt.2 · show q.length ≥ 2 simp only [q, List.length_drop] omega · -- G.IsActiveWalk (Z ∪ S) q — shift indices obtain ⟨hadj, hcoll⟩ := hact refine ⟨fun i hi => ?_, fun i hi => ?_⟩ · have hq_len : q.length = p.length - i₀.val := by simp only [q, List.length_drop] have hi' : i₀.val + i + 1 < p.length := by rw [hq_len] at hi; omega have hq_i : q.get ⟨i, by omega⟩ = p.get ⟨i₀.val + i, by omega⟩ := by simp [q, List.getElem_drop] have hq_i1 : q.get ⟨i + 1, hi⟩ = p.get ⟨i₀.val + (i + 1), by omega⟩ := by simp [q, List.getElem_drop] rw [hq_i, hq_i1] have hadj' := hadj (i₀.val + i) (by omega) exact hadj' · have hq_len : q.length = p.length - i₀.val := by simp only [q, List.length_drop] have hi' : i₀.val + i + 2 < p.length := by rw [hq_len] at hi; omega have hq_i : q.get ⟨i, by omega⟩ = p.get ⟨i₀.val + i, by omega⟩ := by simp [q, List.getElem_drop] have hq_i1 : q.get ⟨i + 1, by omega⟩ = p.get ⟨i₀.val + (i + 1), by omega⟩ := by simp [q, List.getElem_drop] have hq_i2 : q.get ⟨i + 2, hi⟩ = p.get ⟨i₀.val + (i + 2), by omega⟩ := by simp [q, List.getElem_drop] rw [hq_i, hq_i1, hq_i2] have hcoll' := hcoll (i₀.val + i) (by omega) exact hcoll' · -- q.head? = some (p.get i₀) have hq_ne : q ≠ [] := by simp only [q, ne_eq, List.drop_eq_nil_iff] omega have : q.head? = some (q.head hq_ne) := List.head?_eq_some_head hq_ne rw [this] congr 1 simp only [q] rw [List.head_drop] rfl · -- q.getLast? = p.getLast? = some w have hq_ne : q ≠ [] := by simp only [q, ne_eq, List.drop_eq_nil_iff] omega have hp_ne : p ≠ [] := by intro hnil; rw [hnil] at hlen; simp at hlen rw [getLast?_eq_some_getLast hq_ne] rw [getLast?_eq_some_getLast hp_ne] at hlast simp only [q] rw [List.getLast_drop] exact hlast · -- strict interior q has no S-vertex intro k hk1 hk2 hkS have hq_len : q.length = p.length - i₀.val := by simp only [q, List.length_drop] have hq_k : q.get ⟨k, by omega⟩ = p.get ⟨i₀.val + k, by rw [hq_len] at hk2; omega⟩ := by simp [q, List.getElem_drop] rw [hq_k] at hkS have hidx_lt : i₀.val + k < p.length := by rw [hq_len] at hk2; omega have hmem_IS : (⟨i₀.val + k, hidx_lt⟩ : Fin p.length) ∈ IS := by simp only [IS, mem_filter, Finset.mem_univ, true_and] refine ⟨?_, hkS⟩ rw [hq_len] at hk2; omega have hle := Finset.le_max' _ _ hmem_IS change i₀.val + k ≤ i₀.val at hle omega · -- No interior S-vertex. Take q := p. refine ⟨x, p, mem_union_left _ hxX, hlen, hact, hhead, hlast, ?_⟩ intro k hk1 hk2 hkS apply hIS refine ⟨⟨k, by omega⟩, ?_⟩ simp only [IS, mem_filter, Finset.mem_univ, true_and] exact ⟨hk2, hkS⟩
    Causalean.Graph.DAG.take_suffix_at_last_S · Causalean/Graph/DSep/ActivePath.lean:284
  • isActiveWalk_Z_of_no_S_only_collider theorem — In a finite directed acyclic graph, a walk and two conditioning sets that is active under their union and whose interior colliders are activated by the first set alone is active under the first set.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    Z S :
    q :
    hact :
    G.IsActiveWalk (Z ∪ S) q
    hNoSOnly :
    ∀ (i : ℕ) (hi : i + 2 < q.length)
    if
    G.IsCollider (q.get ⟨i, by omega⟩) (q.get ⟨i + 1, by omega⟩) (q.get ⟨i + 2, hi⟩)
    then
    q.get ⟨i + 1, by omega⟩ ∈ G.bbZAncestors Z
    G.IsActiveWalk Z q
    Proof (Lean source)
    theorem isActiveWalk_Z_of_no_S_only_collider {Z S : Finset V} {q : List V} (hact : G.IsActiveWalk (Z ∪ S) q) (hNoSOnly : ∀ (i : ℕ) (hi : i + 2 < q.length), G.IsCollider (q.get ⟨i, by omega⟩) (q.get ⟨i + 1, by omega⟩) (q.get ⟨i + 2, hi⟩) → q.get ⟨i + 1, by omega⟩ ∈ G.bbZAncestors Z) : G.IsActiveWalk Z q := by obtain ⟨hadj, hcoll⟩ := hact refine ⟨hadj, fun i hi => ?_⟩ -- Let m := q.get ⟨i+1, _⟩. Two cases: collider or not. simp only by_cases hC : G.IsCollider (q.get ⟨i, by omega⟩) (q.get ⟨i + 1, by omega⟩) (q.get ⟨i + 2, hi⟩) · -- Collider: use hNoSOnly rw [if_pos hC] exact hNoSOnly i hi hC · -- Non-collider: use the (Z ∪ S)-activity to conclude m ∉ Z ∪ S, hence m ∉ Z. rw [if_neg hC] have := hcoll i hi simp only [if_neg hC] at this -- this : q.get ⟨i + 1, _⟩ ∉ Z ∪ S intro hZ exact this (mem_union_left _ hZ)
    Causalean.Graph.DAG.isActiveWalk_Z_of_no_S_only_collider · Causalean/Graph/DSep/ActivePath.lean:388
  • hasActiveWalk_symm theorem — In a finite directed acyclic graph, two endpoint sets and a conditioning set satisfy active-walk existence in one direction only if it also holds in the opposite direction.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    X Y Z :
    G.HasActiveWalk X Y Z
    G.HasActiveWalk Y X Z
    Proof (Lean source)
    theorem hasActiveWalk_symm (X Y Z : Finset V) : G.HasActiveWalk X Y Z → G.HasActiveWalk Y X Z := by rintro ⟨p, hlen, hact, hhead, hlast⟩ refine ⟨p.reverse, ?_, G.isActiveWalk_reverse hact, ?_, ?_⟩ · simp only [List.length_reverse]; exact hlen · rwa [List.head?_reverse] · rwa [getLast?_reverse]
    Causalean.Graph.DAG.hasActiveWalk_symm · Causalean/Graph/DSep/ActivePath.lean:415
  • isActiveWalk_cons_of_active_triple theorem — In a finite directed acyclic graph, a conditioning set, three leading vertices, and a remaining suffix, adjacency of the new first pair, activity of the new first triple, and activity of the original walk ensure that prepending the new vertex preserves activity.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    Z :
    z w u :
    V
    r :
    hadj :
    G.UAdj z w
    htri :
    if G.IsCollider z w u then w ∈ G.bbZAncestors Z else w ∉ Z
    hact :
    G.IsActiveWalk Z (w :: u :: r)
    G.IsActiveWalk Z (z :: w :: u :: r)
    Proof (Lean source)
    theorem isActiveWalk_cons_of_active_triple {Z : Finset V} {z w u : V} {r : List V} (hadj : G.UAdj z w) (htri : if G.IsCollider z w u then w ∈ G.bbZAncestors Z else w ∉ Z) (hact : G.IsActiveWalk Z (w :: u :: r)) : G.IsActiveWalk Z (z :: w :: u :: r) := by obtain ⟨hadj_old, hcoll_old⟩ := hact refine ⟨fun i hi => ?_, fun i hi => ?_⟩ · cases i with | zero => simpa using hadj | succ i => have h := hadj_old i (by simpa [Nat.add_assoc] using hi) simpa using h · cases i with | zero => simpa using htri | succ i => have h := hcoll_old i (by simpa [Nat.add_assoc] using hi) simpa [Nat.add_assoc] using h
    Causalean.Graph.DAG.isActiveWalk_cons_of_active_triple · Causalean/Graph/DSep/ActivePath.lean:559
  • active_triple_swap_outer theorem — In a finite directed acyclic graph, a conditioning set and three vertices with an active triple retain the active-triple condition when the outer vertices are exchanged.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    Z :
    u w z :
    V
    h :
    if G.IsCollider u w z then w ∈ G.bbZAncestors Z else w ∉ Z
    if G.IsCollider z w u then w ∈ G.bbZAncestors Z else w ∉ Z
    Proof (Lean source)
    theorem active_triple_swap_outer {Z : Finset V} {u w z : V} (h : if G.IsCollider u w z then w ∈ G.bbZAncestors Z else w ∉ Z) : if G.IsCollider z w u then w ∈ G.bbZAncestors Z else w ∉ Z := by simp only [IsCollider] at h ⊢ by_cases hC : G.edge u w ∧ G.edge z w · have hC' : G.edge z w ∧ G.edge u w := hC.symm simpa [hC'] using h · have hC' : ¬(G.edge z w ∧ G.edge u w) := fun h' => hC h'.symm simpa [hC, hC'] using h
    Causalean.Graph.DAG.active_triple_swap_outer · Causalean/Graph/DSep/ActivePath.lean:586
  • isActiveWalk_pair theorem — In a finite directed acyclic graph, a conditioning set and two vertices whose vertices are adjacent form an active two-vertex walk.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    Z :
    a b :
    V
    h :
    G.UAdj a b
    G.IsActiveWalk Z [a, b]
    Proof (Lean source)
    theorem isActiveWalk_pair {Z : Finset V} {a b : V} (h : G.UAdj a b) : G.IsActiveWalk Z [a, b] := by refine ⟨fun i hi => ?_, fun i hi => ?_⟩ · match i, hi with | 0, _ => simpa using h · have hlt : i + 2 < 2 := by simpa using hi omega
    Causalean.Graph.DAG.isActiveWalk_pair · Causalean/Graph/DSep/ActivePath.lean:607
  • isActiveWalk_cons_tail theorem — In a finite directed acyclic graph, a conditioning set, two leading vertices, and a suffix with an active full walk retain an active walk after the first vertex is removed.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    Z :
    u w :
    V
    r :
    h :
    G.IsActiveWalk Z (u :: w :: r)
    G.IsActiveWalk Z (w :: r)
    Proof (Lean source)
    theorem isActiveWalk_cons_tail {Z : Finset V} {u w : V} {r : List V} (h : G.IsActiveWalk Z (u :: w :: r)) : G.IsActiveWalk Z (w :: r) := by obtain ⟨hadj, hcoll⟩ := h refine ⟨fun i hi => ?_, fun i hi => ?_⟩ · have h' := hadj (i + 1) (by simpa [Nat.add_assoc] using Nat.succ_lt_succ hi) simpa using h' · have h' := hcoll (i + 1) (by simpa [Nat.add_assoc] using Nat.succ_lt_succ hi) simpa [Nat.add_assoc] using h'
    Causalean.Graph.DAG.isActiveWalk_cons_tail · Causalean/Graph/DSep/ActivePath.lean:683
Separation 4 core · 11 supporting This file defines d-separation in a finite directed acyclic graph as pairwise disjointness of the query sets together with absence of Bayes Ball reachability from the source set to the target set given a conditioning set ★ dSep_source_to_cond★ dSep_of_edge_subgraph★ dSep_symm

d-Separation

This file defines d-separation in a finite directed acyclic graph as pairwise disjointness of the query sets together with absence of Bayes Ball reachability from the source set to the target set given a conditioning set. It proves structural properties used by the global Markov and identification layers: monotonicity in source and target sets, the union rule for collider-activation ancestors (bbZAncestors_union_eq), directed-path extraction avoiding a conditioning set, source-to-conditioning transfer (dSep_source_to_cond), transfer to edge subgraphs (dSep_of_edge_subgraph), and symmetry.

def dSep reviewed
Causalean.Graph.DAG

For a finite directed acyclic graph, a source set, a target set, and a conditioning set, the d-separation relation holds exactly when the source and target sets are disjoint, the source and conditioning sets are disjoint, the target and conditioning sets are disjoint, and no target vertex is Bayes-Ball reachable from the source set after conditioning on the conditioning set.

Definition (Lean source)
V :
Type u_1
shared
G :
DAG V
shared
X Y Z :
dSep G X Y Z :
Prop
clause 1
clause 2
clause 3
clause 4
Disjoint (G.bbReachableVertices Z X) Y
Causalean.Graph.DAG.dSep · Causalean/Graph/DSep/Separation.lean:55 · uses DAG
theorem dSep_source_to_cond reviewed
Causalean.Graph.DAG

Source-to-conditioning transfer for d-separation. If X and S are disjoint and X ∪ S and Y are d-separated by Z, then X and Y remain d-separated once S is moved into the conditioning set: G.dSep X Y (Z ∪ S). Equivalently: bbReachableVertices (Z ∪ S) X is a subset of bbReachableVertices Z (X ∪ S).

Formal statement
V :
Type u_1
shared
G :
DAG V
shared
X Y Z S :
hXS :
h :
G.dSep (X ∪ S) Y Z
G.dSep X Y (Z ∪ S)
Proof (Lean source)
theorem dSep_source_to_cond {X Y Z S : Finset V} (hXS : Disjoint X S) (h : G.dSep (X ∪ S) Y Z) : G.dSep X Y (Z ∪ S) := by rcases h with ⟨hXUSY, hXUSZ, hYZ, hReach⟩ refine ⟨?_, ?_, ?_, ?_⟩ · exact Disjoint.mono_left (subset_union_left (s₁ := X) (s₂ := S)) hXUSY · rw [Finset.disjoint_left] intro v hvX hvZS rcases Finset.mem_union.mp hvZS with hvZ | hvS · exact Finset.disjoint_left.mp hXUSZ (mem_union_left S hvX) hvZ · exact Finset.disjoint_left.mp hXS hvX hvS · rw [Finset.disjoint_left] intro v hvY hvZS rcases Finset.mem_union.mp hvZS with hvZ | hvS · exact Finset.disjoint_left.mp hYZ hvY hvZ · exact Finset.disjoint_left.mp hXUSY (mem_union_right X hvS) hvY · refine Disjoint.mono_left ?_ hReach intro w hw rw [G.bbReachableVertices_iff_activeWalk] at hw ⊢ obtain ⟨x, hxX, p, hlen, hact, hhead, hlast⟩ := hw obtain ⟨x', p', hx'XS, hlen', hact', hhead', hlast'⟩ := G.activeWalk_transfer_cond_to_source (X := X) (S := S) hxX hlen hact hhead hlast exact ⟨x', hx'XS, p', hlen', hact', hhead', hlast'⟩
Causalean.Graph.DAG.dSep_source_to_cond · Causalean/Graph/DSep/Separation.lean:679 · uses DAG , dSep
theorem dSep_of_edge_subgraph reviewed
Causalean.Graph.DAG

d-separation transfers from a supergraph to a subgraph. For DAGs G and G' on the same vertex set, if every edge of G' is also an edge of G — G' is obtained from G by removing edges — and X and Y are d-separated by Z in G, then X and Y are also d-separated by Z in G'.

Formal statement
V :
Type u_1
shared
G :
DAG V
shared
X Y Z :
G' :
DAG V
hEdge :
∀ u v : V
if
G'.edge u v
then
G.edge u v
h :
G.dSep X Y Z
G'.dSep X Y Z
Proof (Lean source)
theorem dSep_of_edge_subgraph {X Y Z : Finset V} (G' : DAG V) (hEdge : ∀ u v : V, G'.edge u v → G.edge u v) (h : G.dSep X Y Z) : G'.dSep X Y Z := by rcases h with ⟨hXY, hXZ, hYZ, hReach⟩ refine ⟨hXY, hXZ, hYZ, ?_⟩ refine Disjoint.mono_left ?_ hReach intro v hv rw [G'.bbReachableVertices_iff_activeWalk] at hv rw [G.bbReachableVertices_iff_activeWalk] obtain ⟨x, hxX, p, hlen, hact, hhead, hlast⟩ := hv exact ⟨x, hxX, p, hlen, G.isActiveWalk_mono_edge G' hEdge hact, hhead, hlast⟩
Causalean.Graph.DAG.dSep_of_edge_subgraph · Causalean/Graph/DSep/Separation.lean:798 · uses DAG , dSep
theorem dSep_symm reviewed
Causalean.Graph.DAG

d-separation is symmetric. For three finite vertex sets X, Y, Z, if X is d-separated from Y given Z then Y is d-separated from X given Z.

Formal statement
V :
Type u_1
shared
G :
DAG V
shared
X Y Z :
h :
G.dSep X Y Z
G.dSep Y X Z
Proof (Lean source)
theorem dSep_symm (X Y Z : Finset V) (h : G.dSep X Y Z) : G.dSep Y X Z := by rcases h with ⟨hXY, hXZ, hYZ, hReach⟩ refine ⟨hXY.symm, hYZ, hXZ, ?_⟩ rw [Finset.disjoint_left] at hReach ⊢ intro v hv hX -- v ∈ bbReachableVertices Z Y and v ∈ X -- By BFS correctness, there is an active walk from some y ∈ Y to v rw [bbReachableVertices_iff_activeWalk] at hv obtain ⟨y, hy, p, hlen, hact, hhead, hlast⟩ := hv -- Reverse the walk: active walk from v to y have hact' := G.isActiveWalk_reverse hact -- y is reachable from v ∈ X via the reversed active walk, so y ∈ bbReachableVertices Z X have hyReach : y ∈ G.bbReachableVertices Z X := by rw [bbReachableVertices_iff_activeWalk] exact ⟨v, hX, p.reverse, by simp only [List.length_reverse]; exact hlen, hact', by rwa [List.head?_reverse], by rwa [getLast?_reverse]⟩ -- But h says nothing in bbReachableVertices Z X is in Y, contradicting y ∈ Y exact hReach hyReach hy
Causalean.Graph.DAG.dSep_symm · Causalean/Graph/DSep/Separation.lean:829 · uses DAG , dSep
11 supporting declarations (lemmas, instances)
  • decDSep instance — For a finite vertex population with decidable equality, a directed acyclic graph on that population, and a source set, target set, and conditioning set, the decision procedure for d-separation determines whether the source and target sets are d-separated conditional on the conditioning set.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    X Y Z :
    decDSep G X Y Z :
    Decidable (G.dSep X Y Z)
    by unfold dSep infer_instance
    Causalean.Graph.DAG.decDSep · Causalean/Graph/DSep/Separation.lean:68
  • dSep_subset_left theorem — In a finite directed acyclic graph, for two nested source sets, a target set, and a conditioning set, containment of the smaller source set and d-separation of the larger source set imply d-separation of the smaller source set.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    X X' Y Z :
    hXX' :
    X' ⊆ X
    h :
    G.dSep X Y Z
    G.dSep X' Y Z
    Proof (Lean source)
    theorem dSep_subset_left {X X' Y Z : Finset V} (hXX' : X' ⊆ X) (h : G.dSep X Y Z) : G.dSep X' Y Z := by rcases h with ⟨hXY, hXZ, hYZ, hReach⟩ exact ⟨Disjoint.mono_left hXX' hXY, Disjoint.mono_left hXX' hXZ, hYZ, Disjoint.mono_left (G.bbReachableVertices_mono_source hXX') hReach⟩
    Causalean.Graph.DAG.dSep_subset_left · Causalean/Graph/DSep/Separation.lean:81
  • dSep_subset_right theorem — In a finite directed acyclic graph, for a source set, two nested target sets, and a conditioning set, containment of the smaller target set and d-separation of the larger target set imply d-separation of the smaller target set.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    X Y Y' Z :
    hYY' :
    Y' ⊆ Y
    h :
    G.dSep X Y Z
    G.dSep X Y' Z
    Proof (Lean source)
    theorem dSep_subset_right {X Y Y' Z : Finset V} (hYY' : Y' ⊆ Y) (h : G.dSep X Y Z) : G.dSep X Y' Z := by rcases h with ⟨hXY, hXZ, hYZ, hReach⟩ exact ⟨Disjoint.mono_right hYY' hXY, hXZ, Disjoint.mono_left hYY' hYZ, Disjoint.mono_right hYY' hReach⟩
    Causalean.Graph.DAG.dSep_subset_right · Causalean/Graph/DSep/Separation.lean:91
  • bbZAncestors_union_eq theorem — In a finite directed acyclic graph, the two conditioning sets have an activated-collider set for their union equal to the union of their separate activated-collider sets.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    Z S :
    G.bbZAncestors (Z ∪ S) = G.bbZAncestors Z ∪ G.bbZAncestors S
    Proof (Lean source)
    theorem bbZAncestors_union_eq (Z S : Finset V) : G.bbZAncestors (Z ∪ S) = G.bbZAncestors Z ∪ G.bbZAncestors S := by ext v simp only [bbZAncestors, ancestralSet, ancestorsSet, mem_union, mem_filter, Finset.mem_univ, true_and] constructor · rintro (hZS | ⟨w, hwZS, haw⟩) · rcases hZS with hZ | hS · exact inl (inl hZ) · exact inr (inl hS) · rcases hwZS with hwZ | hwS · exact inl (inr ⟨w, hwZ, haw⟩) · exact inr (inr ⟨w, hwS, haw⟩) · rintro ((hZ | ⟨w, hwZ, haw⟩) | (hS | ⟨w, hwS, haw⟩)) · exact inl (inl hZ) · exact inr ⟨w, inl hwZ, haw⟩ · exact inl (inr hS) · exact inr ⟨w, inr hwS, haw⟩
    Causalean.Graph.DAG.bbZAncestors_union_eq · Causalean/Graph/DSep/Separation.lean:101
  • exists_directedPath_avoiding theorem — In a finite directed acyclic graph, if one vertex is a proper ancestor of another and the ancestor lies outside a conditioning set's ancestral closure, then a nontrivial directed walk between them avoids the conditioning set.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    u v :
    V
    huv :
    G.isAncestor u v
    Z :
    huZ :
    u ∉ G.ancestralSet Z
    ∃ (p : List V),
    p.length ≥ 2
    p.head? = some u
    p.getLast? = some v
    conclusion 1
    i :
    ℕ
    hi :
    i + 1 < p.length
    G.edge (p.get ⟨i, by omega⟩) (p.get ⟨i + 1, hi⟩)
    conclusion 2
    x ∈ p :
    x ∉ Z
    Proof (Lean source)
    theorem exists_directedPath_avoiding {u v : V} (huv : G.isAncestor u v) {Z : Finset V} (huZ : u ∉ G.ancestralSet Z) : ∃ (p : List V), p.length ≥ 2 ∧ p.head? = some u ∧ p.getLast? = some v ∧ (∀ (i : ℕ) (hi : i + 1 < p.length), G.edge (p.get ⟨i, by omega⟩) (p.get ⟨i + 1, hi⟩)) ∧ (∀ x ∈ p, x ∉ Z) := by induction huv with | edge he => rename_i u v refine ⟨[u, v], ?_, rfl, rfl, ?_, ?_⟩ · simp · intro i hi -- p.length = 2, so i + 1 < 2 means i = 0 match i, hi with | 0, _ => exact he · intro x hx simp only [List.mem_cons, List.not_mem_nil, or_false] at hx rcases hx with hxu | hxv · -- u ∉ Z because Z ⊆ ancestralSet Z subst hxu intro hxZ exact huZ (mem_union_left _ hxZ) · -- v ∉ Z: else u ∈ ancestorsSet Z ⊆ ancestralSet Z subst hxv intro hvZ apply huZ apply mem_union_right simp only [ancestorsSet, mem_filter, Finset.mem_univ, true_and] exact ⟨_, hvZ, isAncestor.edge he⟩ | trans h₁ he' ih => rename_i u w v obtain ⟨p, hlen, hhead, hlast, hedge, hZ⟩ := ih -- p is a u→w path. Build p ++ [v]. refine ⟨p ++ [v], ?_, ?_, ?_, ?_, ?_⟩ · -- length ≥ 2 rw [List.length_append, List.length_singleton]; omega · -- head? of p ++ [v] = head? of p = some u (p nonempty) have hne : p ≠ [] := by intro hp; rw [hp] at hlen; simp at hlen rw [List.head?_append_of_ne_nil _ hne] exact hhead · -- getLast? of p ++ [v] = some v simp [getLast?_append] · -- edge condition intro i hi have hlen' : (p ++ [v]).length = p.length + 1 := by rw [List.length_append, List.length_singleton] have hi' : i + 1 < p.length + 1 := by rw [← hlen']; exact hi -- Convert .get to [i] change G.edge ((p ++ [v])[i]) ((p ++ [v])[i + 1]) by_cases hlt : i + 1 < p.length · -- both indices in p-part have hi0 : i < p.length := by omega have e1 : (p ++ [v])[i]'(by omega) = p[i]'hi0 := by rw [List.getElem_append]; simp [hi0] have e2 : (p ++ [v])[i + 1]'(by omega) = p[i + 1]'hlt := by rw [List.getElem_append]; simp [hlt] rw [e1, e2] have := hedge i hlt -- convert .get to [i] for hedge simpa [List.get_eq_getElem] using this · -- seam: i + 1 = p.length push_neg at hlt have heq : i + 1 = p.length := by omega have hplen_pos : 0 < p.length := by omega have hi0 : i < p.length := by omega have hne : p ≠ [] := List.ne_nil_of_length_pos hplen_pos -- p[p.length - 1] = w (from hlast) have hlast_w : p[p.length - 1]'(by omega) = w := by have h1 : p.getLast? = some (p.getLast hne) := getLast?_eq_some_getLast hne rw [hlast] at h1 have : p.getLast hne = w := by exact (Option.some_inj.mp h1.symm) rw [← this] exact (List.getLast_eq_getElem hne).symm have hi_eq : i = p.length - 1 := by omega have e1 : (p ++ [v])[i]'(by omega) = p[i]'hi0 := by rw [List.getElem_append]; simp [hi0] have e2 : (p ++ [v])[i + 1]'(by omega) = v := by rw [List.getElem_append] simp [heq] have e3 : p[i]'hi0 = w := by have : p[i]'hi0 = p[p.length - 1]'(by omega) := by congr 1 rw [this, hlast_w] rw [e1, e2, e3] exact he' · -- avoidance intro x hx rw [List.mem_append] at hx rcases hx with hxp | hxv · exact hZ x hxp · rw [List.mem_singleton] at hxv subst hxv intro hvZ apply huZ apply mem_union_right simp only [ancestorsSet, mem_filter, Finset.mem_univ, true_and] refine ⟨x, hvZ, ?_⟩ exact G.isAncestor_trans h₁ (isAncestor.edge he')
    Causalean.Graph.DAG.exists_directedPath_avoiding · Causalean/Graph/DSep/Separation.lean:127
  • activeWalk_transfer_cond_to_source theorem — In a finite directed acyclic graph, consider source and conditioning sets, a walk, and its endpoints. If the first endpoint belongs to the source set, the walk is nontrivial, is active under the enlarged conditioning set, and has the stated endpoints, then there is an active walk under the original conditioning set whose source may also come from the newly conditioned vertices.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    X Z S :
    p :
    x w :
    V
    hxX :
    x ∈ X
    hlen :
    p.length ≥ 2
    hact :
    G.IsActiveWalk (Z ∪ S) p
    hhead :
    p.head? = some x
    hlast :
    p.getLast? = some w
    ∃ (x' : V) (p' : List V),
    conclusion 1
    x' ∈ X ∪ S
    conclusion 2
    p'.length ≥ 2
    conclusion 3
    G.IsActiveWalk Z p'
    conclusion 4
    p'.head? = some x'
    conclusion 5
    p'.getLast? = some w
    Proof (Lean source)
    theorem activeWalk_transfer_cond_to_source {X Z S : Finset V} {p : List V} {x w : V} (hxX : x ∈ X) (hlen : p.length ≥ 2) (hact : G.IsActiveWalk (Z ∪ S) p) (hhead : p.head? = some x) (hlast : p.getLast? = some w) : ∃ (x' : V) (p' : List V), x' ∈ X ∪ S ∧ p'.length ≥ 2 ∧ G.IsActiveWalk Z p' ∧ p'.head? = some x' ∧ p'.getLast? = some w := by classical -- Step A: Take suffix at last S-vertex. Strict interior of q has no S-vertex. obtain ⟨x₁, q, hx₁XS, hqlen, hqact, hqhead, hqlast, hqInterior⟩ := G.take_suffix_at_last_S hxX hlen hact hhead hlast -- Case split on existence of an S-only-activated collider in q's strict interior. by_cases hExists : ∃ (i : ℕ) (hi : i + 2 < q.length), G.IsCollider (q.get ⟨i, by omega⟩) (q.get ⟨i + 1, by omega⟩) (q.get ⟨i + 2, hi⟩) ∧ q.get ⟨i + 1, by omega⟩ ∉ G.bbZAncestors Z · -- Case B: reroute via directed ancestor path. -- Pick the LARGEST strict-interior index j_star of q at which there is an -- "S-only activated" collider (activated in bbZAncestors (Z ∪ S) via -- bbZAncestors S but NOT already in bbZAncestors Z). Use Finset.max'. let IColl : Finset ℕ := (range q.length).filter (fun i => ∃ (hi : i + 2 < q.length), G.IsCollider (q.get ⟨i, by omega⟩) (q.get ⟨i + 1, by omega⟩) (q.get ⟨i + 2, hi⟩) ∧ q.get ⟨i + 1, by omega⟩ ∉ G.bbZAncestors Z) have hIColl_ne : IColl.Nonempty := by obtain ⟨i, hi, hC, hNotZ⟩ := hExists refine ⟨i, ?_⟩ simp only [IColl, mem_filter, Finset.mem_range] exact ⟨by omega, hi, hC, hNotZ⟩ let j_star : ℕ := IColl.max' hIColl_ne have hj_star_mem : j_star ∈ IColl := Finset.max'_mem _ hIColl_ne have hj_star_spec : j_star < q.length ∧ ∃ (hj : j_star + 2 < q.length), G.IsCollider (q.get ⟨j_star, by omega⟩) (q.get ⟨j_star + 1, by omega⟩) (q.get ⟨j_star + 2, hj⟩) ∧ q.get ⟨j_star + 1, by omega⟩ ∉ G.bbZAncestors Z := by have := hj_star_mem simp only [IColl, mem_filter, Finset.mem_range] at this exact this obtain ⟨_hj_lt_len, hj_lt, hj_coll, hm_notZ⟩ := hj_star_spec set m_star : V := q.get ⟨j_star + 1, by omega⟩ with hm_star_def -- m_star is a strict-interior vertex, so m_star ∉ S (from hqInterior). have hm_notS : m_star ∉ S := hqInterior (j_star + 1) (by omega) (by omega) -- m_star ∈ bbZAncestors (Z ∪ S) comes from the (Z ∪ S)-activity at the collider. have hm_coll_ZUS : m_star ∈ G.bbZAncestors (Z ∪ S) := by obtain ⟨_hqadj, hqcoll⟩ := hqact have hval := hqcoll j_star hj_lt simp only at hval rw [if_pos hj_coll] at hval exact hval -- Since m_star ∉ bbZAncestors Z, by bbZAncestors_union_eq, m_star ∈ bbZAncestors S. have hm_S_anc : m_star ∈ G.bbZAncestors S := by have hsplit : m_star ∈ G.bbZAncestors Z ∪ G.bbZAncestors S := by rw [← G.bbZAncestors_union_eq]; exact hm_coll_ZUS rw [mem_union] at hsplit rcases hsplit with hZ | hS · exact absurd hZ hm_notZ · exact hS -- Unfold bbZAncestors S = S ∪ ancestorsSet S. Since m_star ∉ S, -- m_star is a proper ancestor of some s ∈ S. have hm_ancset_S : ∃ s ∈ S, G.isAncestor m_star s := by have hmem : m_star ∈ G.ancestralSet S := hm_S_anc simp only [ancestralSet, mem_union] at hmem rcases hmem with h | h · exact absurd h hm_notS · simp only [ancestorsSet, mem_filter, Finset.mem_univ, true_and] at h exact h obtain ⟨s, hsS, hmAncs⟩ := hm_ancset_S -- m_star ∉ ancestralSet Z = bbZAncestors Z. have hm_notAncZ : m_star ∉ G.ancestralSet Z := hm_notZ -- Get a directed path m_star → ... → s avoiding Z. obtain ⟨r, hr_len, hr_head, hr_last, hr_edges, hr_avoid⟩ := G.exists_directedPath_avoiding hmAncs hm_notAncZ -- Build q' := r.reverse ++ q.drop (j_star + 2). let qTail : List V := q.drop (j_star + 2) let qPrime : List V := r.reverse ++ qTail have hr_ne : r ≠ [] := by intro h; rw [h] at hr_len; simp at hr_len have hrRev_ne : r.reverse ≠ [] := by simp [hr_ne] have hqTail_ne : qTail ≠ [] := by simp only [qTail, ne_eq, List.drop_eq_nil_iff]; omega -- Head of qPrime = s (head of r.reverse = last of r = s). have hqPrime_head : qPrime.head? = some s := by simp only [qPrime, List.head?_append_of_ne_nil _ hrRev_ne, List.head?_reverse] exact hr_last -- Last of qPrime = last of qTail = w. have hqTail_last : qTail.getLast? = some w := by have hq_ne : q ≠ [] := by intro h; rw [h] at hqlen; simp at hqlen rw [getLast?_eq_some_getLast hqTail_ne] rw [getLast?_eq_some_getLast hq_ne] at hqlast simp only [qTail] rw [List.getLast_drop] exact hqlast have hqPrime_last : qPrime.getLast? = some w := by simp only [qPrime, getLast?_append, hqTail_last] rfl -- Length ≥ 2. have hqPrime_len : qPrime.length ≥ 2 := by have hrl : r.reverse.length ≥ 2 := by rw [List.length_reverse]; exact hr_len have htl : qTail.length ≥ 1 := List.length_pos_iff.mpr hqTail_ne simp only [qPrime, List.length_append] omega -- Head s ∈ X ∪ S. have hs_XS : s ∈ X ∪ S := mem_union_right _ hsS refine ⟨s, qPrime, hs_XS, hqPrime_len, ?_, hqPrime_head, hqPrime_last⟩ -- The remaining goal: G.IsActiveWalk Z qPrime. -- Structure (deferred — indices are finicky): -- (A) inside r.reverse (strict interior): edges are reversed-directed, -- so non-colliders; vertices avoid Z by hr_avoid. -- (B) seam at r.reverse boundary: last vertex of r.reverse = r.head = m_star. -- First vertex of qTail = q.get ⟨j_star + 2, _⟩. -- Adjacency at the seam: q-side is G.UAdj (m_star, q[j_star+2]) from hqact. -- Collider triple at index r.length - 2 (inside r.reverse, ending at m_star): -- m_star = r.head; predecessor in r.reverse = r[1]; successor is -- q[j_star+2]. Asymmetry rules out collider (r.head → r[1] means the -- edge points OUT of m_star; no way to have edge q[j_star+2] → m_star -- AND edge r[1] → m_star both as required, since the former goes out). -- Actually the non-collider guard needs m_star ∉ Z — from hm_notAncZ. -- Collider triple at index r.length - 1 (m_star, q[j_star+2], q[j_star+3]): -- adjacency m_star-q[j_star+2] (UAdj from hqact). For collider status use -- maximality of j_star to show q[j_star+2] (the m of this triple) isn't -- an S-only activated collider in the ORIGINAL q — if it is a collider, -- it must already lie in bbZAncestors Z; otherwise it must ∉ Z ∪ S. -- (C) inside qTail proper (indices ≥ r.length): translated triples from q at -- indices ≥ j_star + 2. By maximality of j_star, any collider there is -- in bbZAncestors Z already; non-colliders avoid Z ∪ S, hence avoid Z. -- ACTIVE-WALK VERIFICATION for qPrime = r.reverse ++ q.drop (j_star + 2). -- Index layout: qPrime[k] = r[R-1-k] for k < R := r.length; qPrime[k] = -- q[j_star + 2 + (k - R)] for k ≥ R. Cases on (i vs R). obtain ⟨hqadj, hqcoll⟩ := hqact have hrrev_len : r.reverse.length = r.length := List.length_reverse have hqTail_len_eq : qTail.length = q.length - (j_star + 2) := by simp only [qTail, List.length_drop] have hqP_len_eq : qPrime.length = r.length + qTail.length := by simp only [qPrime, List.length_append, hrrev_len] have hm_notZ_simple : m_star ∉ Z := fun h => hm_notZ (mem_union_left _ h) -- r.get ⟨0, _⟩ = m_star have hr_get_0 : r.get ⟨0, by omega⟩ = m_star := by have h1 : r.head? = some (r.head hr_ne) := List.head?_eq_some_head hr_ne rw [hr_head] at h1 have hhead_eq : r.head hr_ne = m_star := Option.some_inj.mp h1.symm rw [← hhead_eq] rw [List.get_eq_getElem, List.getElem_zero] -- Maximality of j_star: any j' > j_star with a collider has middle ∈ bbZAncestors Z have hmax : ∀ (j' : ℕ) (hj' : j' + 2 < q.length) (_hlt : j_star < j') (_hcol : G.IsCollider (q.get ⟨j', by omega⟩) (q.get ⟨j' + 1, by omega⟩) (q.get ⟨j' + 2, hj'⟩)), q.get ⟨j' + 1, by omega⟩ ∈ G.bbZAncestors Z := by intro j' hj' hlt hcol by_contra hnotZ have hmem : j' ∈ IColl := by simp only [IColl, mem_filter, Finset.mem_range] exact ⟨by omega, hj', hcol, hnotZ⟩ have hle := Finset.le_max' _ _ hmem change j' ≤ j_star at hle omega -- qPrime on left part (k < r.length): qPrime[k] = r[r.length - 1 - k] have hqP_L : ∀ (k : ℕ) (hkR : k < r.length), qPrime.get ⟨k, by rw [hqP_len_eq]; omega⟩ = r.get ⟨r.length - 1 - k, by omega⟩ := by intro k hkR have hk_rev : k < r.reverse.length := by rw [hrrev_len]; exact hkR simp only [qPrime, List.get_eq_getElem, List.getElem_append_left (h := hk_rev), List.getElem_reverse] -- qPrime on right part (r.length ≤ k): qPrime[k] = q[j_star + 2 + (k - r.length)] have hqP_R : ∀ (k : ℕ) (hkL : r.length ≤ k) (hk : k < qPrime.length), qPrime.get ⟨k, hk⟩ = q.get ⟨j_star + 2 + (k - r.length), by rw [hqP_len_eq, hqTail_len_eq] at hk; omega⟩ := by intro k hkL hk have hk_rev : r.reverse.length ≤ k := by rw [hrrev_len]; exact hkL simp only [qPrime, List.get_eq_getElem, List.getElem_append_right hk_rev, qTail, List.getElem_drop, hrrev_len] -- Main split: adjacency and collider condition refine ⟨?_, ?_⟩ · -- Adjacency: G.UAdj qPrime[i] qPrime[i+1] intro i hi rw [hqP_len_eq] at hi by_cases hA1 : i + 1 < r.length · -- A1: both in r.reverse have hiR : i < r.length := by omega rw [hqP_L i hiR, hqP_L (i + 1) hA1] -- hr_edges at index r.length - 2 - i: G.edge r[r.length-2-i] r[r.length-1-i] have hedge_idx : r.length - 2 - i + 1 < r.length := by omega have hedge := hr_edges (r.length - 2 - i) hedge_idx have heq : r.length - 2 - i + 1 = r.length - 1 - i := by omega rw [show (⟨r.length - 2 - i + 1, hedge_idx⟩ : Fin r.length) = ⟨r.length - 1 - i, by omega⟩ from Fin.ext heq] at hedge have heq2 : r.length - 1 - (i + 1) = r.length - 2 - i := by omega rw [show (⟨r.length - 1 - (i + 1), by omega⟩ : Fin r.length) = ⟨r.length - 2 - i, by omega⟩ from Fin.ext heq2] exact inr hedge · push_neg at hA1 -- r.length ≤ i + 1 by_cases hA2 : i + 1 = r.length · -- A2: seam: i = r.length - 1 have hi_eq : i = r.length - 1 := by omega subst hi_eq have hiR : r.length - 1 < r.length := by omega rw [hqP_L (r.length - 1) hiR] have hi1L : r.length ≤ r.length - 1 + 1 := by omega have hi1_lt : r.length - 1 + 1 < qPrime.length := by -- … truncated; follow the source link for the rest …
    Causalean.Graph.DAG.activeWalk_transfer_cond_to_source · Causalean/Graph/DSep/Separation.lean:234
  • isAncestor_mono_edge theorem — For two finite directed acyclic graphs, if every edge of the first is an edge of the second, then an ancestor relation between two vertices in the first also holds in the second.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    G' :
    DAG V
    hEdge :
    ∀ u v : V
    if
    G'.edge u v
    then
    G.edge u v
    u v :
    V
    h :
    G'.isAncestor u v
    G.isAncestor u v
    Proof (Lean source)
    theorem isAncestor_mono_edge (G' : DAG V) (hEdge : ∀ u v : V, G'.edge u v → G.edge u v) {u v : V} (h : G'.isAncestor u v) : G.isAncestor u v := by induction h with | edge he => exact isAncestor.edge (hEdge _ _ he) | trans h₁ he ih => exact isAncestor.trans ih (hEdge _ _ he)
    Causalean.Graph.DAG.isAncestor_mono_edge · Causalean/Graph/DSep/Separation.lean:717
  • bbZAncestors_mono_edge theorem — For two finite directed acyclic graphs, if every edge of the first is an edge of the second, then for any conditioning set, every collider activated in the first graph is activated in the second.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    G' :
    DAG V
    hEdge :
    ∀ u v : V
    if
    G'.edge u v
    then
    G.edge u v
    Z :
    G'.bbZAncestors Z ⊆ G.bbZAncestors Z
    Proof (Lean source)
    theorem bbZAncestors_mono_edge (G' : DAG V) (hEdge : ∀ u v : V, G'.edge u v → G.edge u v) (Z : Finset V) : G'.bbZAncestors Z ⊆ G.bbZAncestors Z := by intro v hv simp only [bbZAncestors, ancestralSet, ancestorsSet, mem_union, mem_filter, Finset.mem_univ, true_and] at hv ⊢ rcases hv with hvZ | ⟨w, hwZ, hvw⟩ · exact inl hvZ · exact inr ⟨w, hwZ, G.isAncestor_mono_edge G' hEdge hvw⟩
    Causalean.Graph.DAG.bbZAncestors_mono_edge · Causalean/Graph/DSep/Separation.lean:727
  • uAdj_mono_edge theorem — For two finite directed acyclic graphs, if every edge of the first is an edge of the second, then two vertices adjacent in the first graph are adjacent in the second graph.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    G' :
    DAG V
    hEdge :
    ∀ u v : V
    if
    G'.edge u v
    then
    G.edge u v
    u v :
    V
    h :
    G'.UAdj u v
    G.UAdj u v
    Proof (Lean source)
    theorem uAdj_mono_edge (G' : DAG V) (hEdge : ∀ u v : V, G'.edge u v → G.edge u v) {u v : V} (h : G'.UAdj u v) : G.UAdj u v := by rcases h with huv | hvu · exact inl (hEdge _ _ huv) · exact inr (hEdge _ _ hvu)
    Causalean.Graph.DAG.uAdj_mono_edge · Causalean/Graph/DSep/Separation.lean:741
  • isCollider_of_supergraph theorem — For two finite directed acyclic graphs, if every edge of the first is an edge of the second, then three vertices adjacent in consecutive pairs in the first graph that form a collider in the second also form a collider in the first.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    G' :
    DAG V
    hEdge :
    ∀ u v : V
    if
    G'.edge u v
    then
    G.edge u v
    l m r :
    V
    hadj_lm :
    G'.UAdj l m
    hadj_mr :
    G'.UAdj m r
    hcoll :
    G.IsCollider l m r
    G'.IsCollider l m r
    Proof (Lean source)
    theorem isCollider_of_supergraph (G' : DAG V) (hEdge : ∀ u v : V, G'.edge u v → G.edge u v) {l m r : V} (hadj_lm : G'.UAdj l m) (hadj_mr : G'.UAdj m r) (hcoll : G.IsCollider l m r) : G'.IsCollider l m r := by unfold IsCollider at hcoll ⊢ obtain ⟨hlm, hrm⟩ := hcoll constructor · rcases hadj_lm with hlm' | hml' · exact hlm' · exact absurd (hEdge _ _ hml') (G.asymm hlm) · rcases hadj_mr with hmr' | hrm' · exact absurd (hEdge _ _ hmr') (G.asymm hrm) · exact hrm'
    Causalean.Graph.DAG.isCollider_of_supergraph · Causalean/Graph/DSep/Separation.lean:751
  • isActiveWalk_mono_edge theorem — For two finite directed acyclic graphs, if every edge of the first is an edge of the second, then a walk and conditioning set active in the first graph remain active in the second graph.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    G' :
    DAG V
    hEdge :
    ∀ u v : V
    if
    G'.edge u v
    then
    G.edge u v
    Z :
    p :
    h :
    G'.IsActiveWalk Z p
    G.IsActiveWalk Z p
    Proof (Lean source)
    theorem isActiveWalk_mono_edge (G' : DAG V) (hEdge : ∀ u v : V, G'.edge u v → G.edge u v) {Z : Finset V} {p : List V} (h : G'.IsActiveWalk Z p) : G.IsActiveWalk Z p := by obtain ⟨hadj, hcoll⟩ := h refine ⟨fun i hi => G.uAdj_mono_edge G' hEdge (hadj i hi), fun i hi => ?_⟩ simp only by_cases hC : G.IsCollider (p.get ⟨i, by omega⟩) (p.get ⟨i + 1, by omega⟩) (p.get ⟨i + 2, hi⟩) · rw [if_pos hC] have hC' : G'.IsCollider (p.get ⟨i, by omega⟩) (p.get ⟨i + 1, by omega⟩) (p.get ⟨i + 2, hi⟩) := G.isCollider_of_supergraph G' hEdge (hadj i (by omega)) (hadj (i + 1) (by omega)) hC have hval := hcoll i hi simp only at hval rw [if_pos hC'] at hval exact G.bbZAncestors_mono_edge G' hEdge Z hval · rw [if_neg hC] have hnotC' : ¬ G'.IsCollider (p.get ⟨i, by omega⟩) (p.get ⟨i + 1, by omega⟩) (p.get ⟨i + 2, hi⟩) := by intro hC' exact hC ⟨hEdge _ _ hC'.1, hEdge _ _ hC'.2⟩ have hval := hcoll i hi simp only at hval rwa [if_neg hnotC'] at hval
    Causalean.Graph.DAG.isActiveWalk_mono_edge · Causalean/Graph/DSep/Separation.lean:769
Ancestral 2 core · 11 supporting This file proves that active walks relevant to a d-separation query can be restricted to the ancestral set of the source, target, and conditioning vertices. ★ activeWalk_nodes_are_ancestors★ ancestralSet_inter_subset_ancestralSet_of_dSep

Ancestral Reduction for d-Separation

This file proves that active walks relevant to a d-separation query can be restricted to the ancestral set of the source, target, and conditioning vertices. The resulting finset lemmas distinguish the trivial reachable-target endpoint inclusion from the stronger statement about all nodes on a witnessing active walk.

theorem activeWalk_nodes_are_ancestors reviewed
Causalean.Graph.DAG

Active-walk nodes lie in the ancestral set. For finite vertex sets X, Y, Z, consider a walk p that is active given Z running from a node x in X to a node y in Y. Then every vertex on p lies in the ancestral set of X ∪ Y ∪ Z. This is the main classical lemma used to justify ancestral reduction of d-separation.

Formal statement
V :
Type u_1
shared
G :
DAG V
shared
X Y Z :
x y :
V
p :
hxX :
x ∈ X
hyY :
y ∈ Y
hp :
G.IsActiveWalk Z p
hhead :
p.head? = some x
hlast :
p.getLast? = some y
v ∈ p :
v ∈ G.ancestralSet (X ∪ Y ∪ Z)
Proof (Lean source)
theorem activeWalk_nodes_are_ancestors {X Y Z : Finset V} {x y : V} {p : List V} (hxX : x ∈ X) (hyY : y ∈ Y) (hp : G.IsActiveWalk Z p) (hhead : p.head? = some x) (hlast : p.getLast? = some y) : ∀ v ∈ p, v ∈ G.ancestralSet (X ∪ Y ∪ Z) := by intro v hv -- Convert `v ∈ p` to an index witness, then invoke `walk_forward_witness`. rw [List.mem_iff_get] at hv obtain ⟨i, hieq⟩ := hv rw [← hieq] exact G.walk_forward_witness hxX hyY hp hhead hlast i.val i.isLt
Causalean.Graph.DAG.activeWalk_nodes_are_ancestors · Causalean/Graph/DSep/Ancestral.lean:328 · uses DAG , IsActiveWalk , ancestralSet
theorem ancestralSet_inter_subset_ancestralSet_of_dSep reviewed
Causalean.Graph.DAG

Ancestral intersection from reachability separation. If X and Y are disjoint and no vertex of Y is Bayes-Ball-reachable from X given Z, then any vertex lying in both the ancestral closure of X and the ancestral closure of Y also lies in the ancestral closure of Z.

Formal statement
V :
Type u_1
shared
G :
DAG V
shared
X Y Z :
hXY :
hReach :
Disjoint (G.bbReachableVertices Z X) Y
G.ancestralSet X ∩ G.ancestralSet Y ⊆ G.ancestralSet Z
Proof (Lean source)
theorem ancestralSet_inter_subset_ancestralSet_of_dSep {X Y Z : Finset V} (hXY : Disjoint X Y) (hReach : Disjoint (G.bbReachableVertices Z X) Y) : G.ancestralSet X ∩ G.ancestralSet Y ⊆ G.ancestralSet Z := by intro u hu rw [Finset.mem_inter] at hu obtain ⟨huX, huY⟩ := hu -- Proceed by contradiction. by_contra huZ -- Unfold membership in ancestralSet for each side. have huX' : u ∈ X ∨ ∃ x ∈ X, G.isAncestor u x := by simp only [ancestralSet, mem_union, ancestorsSet, mem_filter, Finset.mem_univ, true_and] at huX rcases huX with h | ⟨x, hxX, hax⟩ · exact inl h · exact inr ⟨x, hxX, hax⟩ have huY' : u ∈ Y ∨ ∃ y ∈ Y, G.isAncestor u y := by simp only [ancestralSet, mem_union, ancestorsSet, mem_filter, Finset.mem_univ, true_and] at huY rcases huY with h | ⟨y, hyY, hay⟩ · exact inl h · exact inr ⟨y, hyY, hay⟩ -- Convert dSep to: no active walk from X to Y given Z. have hNoPath : ∀ (x y : V), x ∈ X → y ∈ Y → ¬ ∃ (p : List V), p.length ≥ 2 ∧ G.IsActiveWalk Z p ∧ p.head? = some x ∧ p.getLast? = some y := by intro x y hxX hyY ⟨p, hlen, hact, hhead, hlast⟩ -- y ∈ bbReachableVertices Z X, but dSep says it's disjoint from Y. have hyR : y ∈ G.bbReachableVertices Z X := by rw [G.bbReachableVertices_iff_activeWalk] exact ⟨x, hxX, p, hlen, hact, hhead, hlast⟩ exact (Finset.disjoint_left.mp hReach) hyR hyY -- Build the active walk. Three cases on (huX', huY'). rcases huX' with hxIs | ⟨x, hxX, hax⟩ · -- u ∈ X. Use directed path u → ... → y for some y ∈ Y. rcases huY' with hyIs | ⟨y, hyY, hay⟩ · -- u ∈ X ∩ Y, contradicting disjointness. exact absurd hyIs ((Finset.disjoint_left.mp hXY) hxIs) · obtain ⟨p, hplen, hphead, hplast, hpact⟩ := G.exists_activeWalk_of_ancestor_avoiding hay huZ exact hNoPath u y hxIs hyY ⟨p, hplen, hpact, hphead, hplast⟩ · -- u is a strict ancestor of x ∈ X. obtain ⟨xp, hxp_len, hxp_head, hxp_last, hxp_edge, hxp_Z⟩ := G.exists_directedPath_avoiding hax huZ rcases huY' with hyIs | ⟨y, hyY, hay⟩ · -- u ∈ Y. The directed path xp (u → ... → x) is itself an active walk -- from u to x; reverse it via the public `isActiveWalk_reverse`. have hxp_act_fwd : G.IsActiveWalk Z xp := G.isActiveWalk_of_directed hxp_edge (fun i hi => hxp_Z _ (List.get_mem _ _)) have hxp_act : G.IsActiveWalk Z xp.reverse := G.isActiveWalk_reverse hxp_act_fwd have hxp_rev_len : xp.reverse.length ≥ 2 := by rw [List.length_reverse]; exact hxp_len have hxp_rev_head : xp.reverse.head? = some x := by rw [List.head?_reverse]; exact hxp_last have hxp_rev_last : xp.reverse.getLast? = some u := by rw [getLast?_reverse]; exact hxp_head exact hNoPath x u hxX hyIs ⟨xp.reverse, hxp_rev_len, hxp_act, hxp_rev_head, hxp_rev_last⟩ · -- u is a strict ancestor of both x ∈ X and y ∈ Y. Build the fork. obtain ⟨yp, hyp_len, hyp_head, hyp_last, hyp_edge, hyp_Z⟩ := G.exists_directedPath_avoiding hay huZ obtain ⟨hp_len, hp_head, hp_last, hp_act⟩ := G.fork_isActiveWalk hxp_len hxp_head hxp_last hxp_edge hxp_Z hyp_len hyp_head hyp_last hyp_edge hyp_Z exact hNoPath x y hxX hyY ⟨_, hp_len, hp_act, hp_head, hp_last⟩
Causalean.Graph.DAG.ancestralSet_inter_subset_ancestralSet_of_dSep · Causalean/Graph/DSep/Ancestral.lean:754 · uses DAG , ancestralSet , bbReachableVertices
11 supporting declarations (lemmas, instances)
  • subset_ancestralSet lemma — In a finite directed acyclic graph, every member of a vertex set belongs to that set's ancestral closure.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    S :
    S ⊆ G.ancestralSet S
    Proof (Lean source)
    lemma subset_ancestralSet (S : Finset V) : S ⊆ G.ancestralSet S := by intro v hv exact mem_union_left _ hv
    Causalean.Graph.DAG.subset_ancestralSet · Causalean/Graph/DSep/Ancestral.lean:59
  • ancestralSet_mono lemma — In a finite directed acyclic graph, if one vertex set is contained in another, then its ancestral closure is contained in the other's.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    S T :
    h :
    S ⊆ T
    G.ancestralSet S ⊆ G.ancestralSet T
    Proof (Lean source)
    lemma ancestralSet_mono {S T : Finset V} (h : S ⊆ T) : G.ancestralSet S ⊆ G.ancestralSet T := by intro v hv rcases Finset.mem_union.mp hv with hvS | hvA · exact mem_union_left _ (h hvS) · simp only [ancestorsSet, mem_filter, Finset.mem_univ, true_and] at hvA obtain ⟨w, hwS, haw⟩ := hvA exact mem_union_right _ (by simp only [ancestorsSet, mem_filter, Finset.mem_univ, true_and] exact ⟨w, h hwS, haw⟩)
    Causalean.Graph.DAG.ancestralSet_mono · Causalean/Graph/DSep/Ancestral.lean:65
  • mem_ancestralSet_of_isAncestor lemma — In a finite directed acyclic graph, if a vertex belongs to a target set and another vertex is its ancestor, then that ancestor belongs to the target set's ancestral closure.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    u w :
    V
    S :
    hwS :
    w ∈ S
    h :
    G.isAncestor u w
    u ∈ G.ancestralSet S
    Proof (Lean source)
    lemma mem_ancestralSet_of_isAncestor {u w : V} {S : Finset V} (hwS : w ∈ S) (h : G.isAncestor u w) : u ∈ G.ancestralSet S := by apply mem_union_right simp only [ancestorsSet, mem_filter, Finset.mem_univ, true_and] exact ⟨w, hwS, h⟩
    Causalean.Graph.DAG.mem_ancestralSet_of_isAncestor · Causalean/Graph/DSep/Ancestral.lean:78
  • bbZAncestors_subset_ancestralSet_of_subset lemma — In a finite directed acyclic graph, containment of one conditioning set in another implies that every collider activated by the first set belongs to the second set's ancestral closure.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    Z S :
    hZS :
    Z ⊆ S
    G.bbZAncestors Z ⊆ G.ancestralSet S
    Proof (Lean source)
    lemma bbZAncestors_subset_ancestralSet_of_subset {Z S : Finset V} (hZS : Z ⊆ S) : G.bbZAncestors Z ⊆ G.ancestralSet S := by unfold bbZAncestors exact G.ancestralSet_mono hZS
    Causalean.Graph.DAG.bbZAncestors_subset_ancestralSet_of_subset · Causalean/Graph/DSep/Ancestral.lean:88
  • nonCollider_has_outgoing lemma — In a finite directed acyclic graph, three vertices whose consecutive pairs are adjacent but whose middle vertex is not a collider have an outgoing edge from the middle vertex to an outer vertex.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    l m r :
    V
    hadj_lm :
    G.UAdj l m
    hadj_mr :
    G.UAdj m r
    hnc :
    ¬ G.IsCollider l m r
    G.edge m l ∨ G.edge m r
    Proof (Lean source)
    lemma nonCollider_has_outgoing {l m r : V} (hadj_lm : G.UAdj l m) (hadj_mr : G.UAdj m r) (hnc : ¬ G.IsCollider l m r) : G.edge m l ∨ G.edge m r := by -- IsCollider l m r := G.edge l m ∧ G.edge r m -- ¬ collider means ¬ edge l m ∨ ¬ edge r m. -- Combined with UAdj giving edge ∨ reverse, deduce one outgoing. unfold IsCollider at hnc push_neg at hnc rcases hadj_lm with hlm | hml · -- edge l m: then ¬ edge r m (else collider). So edge m r (by UAdj m r). have hnr : ¬ G.edge r m := hnc hlm rcases hadj_mr with hmr | hrm · exact inr hmr · exact absurd hrm hnr · -- edge m l: outgoing on the left. exact inl hml
    Causalean.Graph.DAG.nonCollider_has_outgoing · Causalean/Graph/DSep/Ancestral.lean:101
  • mem_ancestralSet_of_edge_to_mem lemma — In a finite directed acyclic graph, if an edge joins two vertices and its target belongs to a set's ancestral closure, then its source also belongs to that ancestral closure.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    u v :
    V
    S :
    huv :
    G.edge u v
    hv :
    v ∈ G.ancestralSet S
    u ∈ G.ancestralSet S
    Proof (Lean source)
    lemma mem_ancestralSet_of_edge_to_mem {u v : V} {S : Finset V} (huv : G.edge u v) (hv : v ∈ G.ancestralSet S) : u ∈ G.ancestralSet S := by rcases Finset.mem_union.mp hv with hvS | hvA · exact G.mem_ancestralSet_of_isAncestor hvS (isAncestor.edge huv) · apply mem_union_right simp only [ancestorsSet, mem_filter, Finset.mem_univ, true_and] at hvA ⊢ obtain ⟨w, hwS, hvw⟩ := hvA exact ⟨w, hwS, G.isAncestor_trans (isAncestor.edge huv) hvw⟩
    Causalean.Graph.DAG.mem_ancestralSet_of_edge_to_mem · Causalean/Graph/DSep/Ancestral.lean:122
  • subset_ancestralSet_of_subset theorem — In a finite directed acyclic graph, containment of one vertex set in another ensures that the smaller set lies in the larger set's ancestral closure.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    Y S :
    hYS :
    Y ⊆ S
    Y ⊆ G.ancestralSet S
    Proof (Lean source)
    theorem subset_ancestralSet_of_subset {Y S : Finset V} (hYS : Y ⊆ S) : Y ⊆ G.ancestralSet S := by intro v hvY apply G.subset_ancestralSet exact hYS hvY
    Causalean.Graph.DAG.subset_ancestralSet_of_subset · Causalean/Graph/DSep/Ancestral.lean:352
  • activeWalk_witness_subset_ancestralSet theorem — In a finite directed acyclic graph, source, target, and conditioning sets with a selected target, if the target is Bayes-Ball reachable from the source and belongs to the target set, then there is a witnessing active walk whose every vertex lies in the query's ancestral closure.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    X Y Z :
    v :
    V
    hvR :
    v ∈ G.bbReachableVertices Z X
    hvY :
    v ∈ Y
    ∃ (p : List V) (x : V),
    x ∈ X
    p.length ≥ 2
    G.IsActiveWalk Z p
    p.head? = some x
    p.getLast? = some v
    conclusion 1
    w ∈ p :
    w ∈ G.ancestralSet (X ∪ Y ∪ Z)
    Proof (Lean source)
    theorem activeWalk_witness_subset_ancestralSet {X Y Z : Finset V} {v : V} (hvR : v ∈ G.bbReachableVertices Z X) (hvY : v ∈ Y) : ∃ (p : List V) (x : V), x ∈ X ∧ p.length ≥ 2 ∧ G.IsActiveWalk Z p ∧ p.head? = some x ∧ p.getLast? = some v ∧ ∀ w ∈ p, w ∈ G.ancestralSet (X ∪ Y ∪ Z) := by -- Pull an active walk witness from `bbReachableVertices_iff_activeWalk`, -- then apply `activeWalk_nodes_are_ancestors`. obtain ⟨x, hxX, p, hlen, hact, hhead, hlast⟩ := (G.bbReachableVertices_iff_activeWalk X Z v).mp hvR exact ⟨p, x, hxX, hlen, hact, hhead, hlast, G.activeWalk_nodes_are_ancestors hxX hvY hact hhead hlast⟩
    Causalean.Graph.DAG.activeWalk_witness_subset_ancestralSet · Causalean/Graph/DSep/Ancestral.lean:367
  • isActiveWalk_of_directed theorem — In a finite directed acyclic graph, a walk and conditioning set whose edges all point forward and whose interior vertices avoid the conditioning set form an active walk.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    Z :
    p :
    hdir :
    ∀ (i : ℕ) (hi : i + 1 < p.length), G.edge (p.get ⟨i, by omega⟩) (p.get ⟨i + 1, hi⟩)
    hZ :
    ∀ (i : ℕ) (hi : i + 2 < p.length), p.get ⟨i + 1, by omega⟩ ∉ Z
    G.IsActiveWalk Z p
    Proof (Lean source)
    theorem isActiveWalk_of_directed {Z : Finset V} {p : List V} (hdir : ∀ (i : ℕ) (hi : i + 1 < p.length), G.edge (p.get ⟨i, by omega⟩) (p.get ⟨i + 1, hi⟩)) (hZ : ∀ (i : ℕ) (hi : i + 2 < p.length), p.get ⟨i + 1, by omega⟩ ∉ Z) : G.IsActiveWalk Z p := by refine ⟨fun i hi => ?_, fun i hi => ?_⟩ · -- Adjacency: G.edge p[i] p[i+1] gives UAdj. exact inl (hdir i hi) · -- Non-collider: edge from p[i+1] to p[i+2], so ¬ edge p[i+2] p[i+1]. simp only have hmr : G.edge (p.get ⟨i + 1, by omega⟩) (p.get ⟨i + 2, hi⟩) := hdir (i + 1) hi have hrm_false : ¬ G.edge (p.get ⟨i + 2, hi⟩) (p.get ⟨i + 1, by omega⟩) := G.asymm hmr have hnotcoll : ¬ G.IsCollider (p.get ⟨i, by omega⟩) (p.get ⟨i + 1, by omega⟩) (p.get ⟨i + 2, hi⟩) := by intro ⟨_, h2⟩; exact hrm_false h2 rw [if_neg hnotcoll] exact hZ i hi
    Causalean.Graph.DAG.isActiveWalk_of_directed · Causalean/Graph/DSep/Ancestral.lean:393
  • exists_activeWalk_of_ancestor_avoiding theorem — In a finite directed acyclic graph, two vertices and a conditioning set such that the first is an ancestor of the second and lies outside the conditioning set's ancestral closure admit an active walk from the ancestor to the descendant.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    Z :
    u v :
    V
    huv :
    G.isAncestor u v
    huZ :
    u ∉ G.ancestralSet Z
    ∃ (p : List V),
    conclusion 1
    p.length ≥ 2
    conclusion 2
    p.head? = some u
    conclusion 3
    p.getLast? = some v
    conclusion 4
    G.IsActiveWalk Z p
    Proof (Lean source)
    theorem exists_activeWalk_of_ancestor_avoiding {Z : Finset V} {u v : V} (huv : G.isAncestor u v) (huZ : u ∉ G.ancestralSet Z) : ∃ (p : List V), p.length ≥ 2 ∧ p.head? = some u ∧ p.getLast? = some v ∧ G.IsActiveWalk Z p := by obtain ⟨p, hlen, hhead, hlast, hedge, hZavoid⟩ := G.exists_directedPath_avoiding huv huZ exact ⟨p, hlen, hhead, hlast, G.isActiveWalk_of_directed hedge (fun i hi => hZavoid _ (List.get_mem _ _))⟩
    Causalean.Graph.DAG.exists_activeWalk_of_ancestor_avoiding · Causalean/Graph/DSep/Ancestral.lean:421
  • fork_isActiveWalk theorem — In a finite directed acyclic graph, a conditioning set, fork vertex, endpoints, and two walks, if the first walk is a directed path from the fork to the first endpoint and avoids conditioning and the second does likewise, then reversing the first and joining it to the second gives an active walk between the endpoints.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    Z :
    u x y :
    V
    xp yp :
    hxp_len :
    xp.length ≥ 2
    hxp_head :
    xp.head? = some u
    hxp_last :
    xp.getLast? = some x
    hxp_edge :
    ∀ (i : ℕ) (hi : i + 1 < xp.length), G.edge (xp.get ⟨i, by omega⟩) (xp.get ⟨i + 1, hi⟩)
    hxp_Z :
    ∀ z ∈ xp, z ∉ Z
    hyp_len :
    yp.length ≥ 2
    hyp_head :
    yp.head? = some u
    hyp_last :
    yp.getLast? = some y
    hyp_edge :
    ∀ (i : ℕ) (hi : i + 1 < yp.length), G.edge (yp.get ⟨i, by omega⟩) (yp.get ⟨i + 1, hi⟩)
    hyp_Z :
    ∀ z ∈ yp, z ∉ Z
    p :
    xp.reverse ++ yp.tail
    p.length ≥ 2
    p.head? = some x
    p.getLast? = some y
    G.IsActiveWalk Z p
    Proof (Lean source)
    theorem fork_isActiveWalk {Z : Finset V} {u x y : V} {xp yp : List V} (hxp_len : xp.length ≥ 2) (hxp_head : xp.head? = some u) (hxp_last : xp.getLast? = some x) (hxp_edge : ∀ (i : ℕ) (hi : i + 1 < xp.length), G.edge (xp.get ⟨i, by omega⟩) (xp.get ⟨i + 1, hi⟩)) (hxp_Z : ∀ z ∈ xp, z ∉ Z) (hyp_len : yp.length ≥ 2) (hyp_head : yp.head? = some u) (hyp_last : yp.getLast? = some y) (hyp_edge : ∀ (i : ℕ) (hi : i + 1 < yp.length), G.edge (yp.get ⟨i, by omega⟩) (yp.get ⟨i + 1, hi⟩)) (hyp_Z : ∀ z ∈ yp, z ∉ Z) : let p := xp.reverse ++ yp.tail p.length ≥ 2 ∧ p.head? = some x ∧ p.getLast? = some y ∧ G.IsActiveWalk Z p := by -- yp.head = u, yp.tail = yp without u have hyp_ne : yp ≠ [] := by intro h; rw [h] at hyp_len; simp at hyp_len have hxp_ne : xp ≠ [] := by intro h; rw [h] at hxp_len; simp at hxp_len have hxpRev_ne : xp.reverse ≠ [] := by simp [hxp_ne] -- yp.head hyp_ne = u have hyp_head_eq : yp.head hyp_ne = u := by have h := List.head?_eq_some_head hyp_ne rw [hyp_head] at h exact (Option.some_inj.mp h.symm) -- xp.getLast hxp_ne = x have hxp_last_eq : xp.getLast hxp_ne = x := by have h := getLast?_eq_some_getLast hxp_ne rw [hxp_last] at h exact (Option.some_inj.mp h.symm) -- xp.head hxp_ne = u have hxp_head_eq : xp.head hxp_ne = u := by have h := List.head?_eq_some_head hxp_ne rw [hxp_head] at h exact (Option.some_inj.mp h.symm) -- yp.getLast hyp_ne = y have hyp_last_eq : yp.getLast hyp_ne = y := by have h := getLast?_eq_some_getLast hyp_ne rw [hyp_last] at h exact (Option.some_inj.mp h.symm) -- yp = u :: yp.tail have hyp_decomp : yp = u :: yp.tail := by conv_lhs => rw [← List.cons_head_tail hyp_ne] rw [hyp_head_eq] -- yp.tail length = yp.length - 1 have hyp_tail_len : yp.tail.length = yp.length - 1 := List.length_tail -- yp.tail nonempty since yp.length ≥ 2 have hyp_tail_ne : yp.tail ≠ [] := by rw [← List.length_pos_iff, hyp_tail_len]; omega set p := xp.reverse ++ yp.tail with hp_def have hp_len_eq : p.length = xp.length + yp.length - 1 := by simp only [hp_def, List.length_append, List.length_reverse, hyp_tail_len] omega have hp_len : p.length ≥ 2 := by rw [hp_len_eq]; omega -- head p = head xp.reverse = last xp = x have hp_head : p.head? = some x := by simp only [hp_def, List.head?_append_of_ne_nil _ hxpRev_ne, List.head?_reverse] rw [hxp_last] -- last p = last yp.tail = last yp = y have hp_last : p.getLast? = some y := by have hlast_tail : yp.tail.getLast? = some y := by -- yp = [u] ++ yp.tail, so getLast? = yp.tail.getLast? when yp.tail ≠ []. have heq : yp = [u] ++ yp.tail := by simpa using hyp_decomp have := getLast?_append_of_ne_nil [u] hyp_tail_ne rw [← heq] at this rw [← this]; exact hyp_last simp only [hp_def, getLast?_append, hlast_tail] rfl refine ⟨hp_len, hp_head, hp_last, ?_⟩ -- Now show `IsActiveWalk Z p`. -- Layout: p[k] = xp.reverse[k] = xp[xp.length - 1 - k] for k < xp.length. -- p[k] = yp.tail[k - xp.length] = yp[k - xp.length + 1] for k ≥ xp.length. -- The seam is at k = xp.length - 1 (= u, since xp.reverse[xp.length-1] = xp[0] = u), -- and k = xp.length is yp.tail[0] = yp[1]. set R := xp.length with hR_def have hxpRev_len : xp.reverse.length = R := List.length_reverse have hp_len_eq2 : p.length = R + yp.tail.length := by simp only [hp_def, List.length_append, hxpRev_len] have hp_L : ∀ (k : ℕ) (hkR : k < R), p.get ⟨k, by rw [hp_len_eq2]; omega⟩ = xp.get ⟨R - 1 - k, by omega⟩ := by intro k hkR have hk_rev : k < xp.reverse.length := by rw [hxpRev_len]; exact hkR simp only [hp_def, List.get_eq_getElem, List.getElem_append_left (h := hk_rev), List.getElem_reverse] change xp[xp.length - 1 - k] = xp[R - 1 - k] congr 1 have hp_R : ∀ (k : ℕ) (hkL : R ≤ k) (hk : k < p.length), p.get ⟨k, hk⟩ = yp.get ⟨k - R + 1, by rw [hp_len_eq2, hyp_tail_len] at hk; omega⟩ := by intro k hkL hk have hk_rev : xp.reverse.length ≤ k := by rw [hxpRev_len]; exact hkL have htail_idx_lt : k - xp.reverse.length < yp.tail.length := by rw [hxpRev_len, hyp_tail_len] rw [hp_len_eq2, hyp_tail_len] at hk; omega have htail_yp_idx_lt : k - R + 1 < yp.length := by rw [hp_len_eq2, hyp_tail_len] at hk; omega -- yp.tail[j] = yp[j+1] via cons decomposition have hyp_tail_get : ∀ (j : ℕ) (hj1 : j < yp.tail.length) (hj2 : j + 1 < yp.length), yp.tail[j]'hj1 = yp[j + 1]'hj2 := by intro j hj1 hj2 have : yp[j + 1] = (u :: yp.tail)[j + 1]'(by rw [← hyp_decomp]; exact hj2) := by congr 1 rw [this] simp [List.getElem_cons_succ] -- Compute p.get have hpget : p.get ⟨k, hk⟩ = yp.tail[k - xp.reverse.length]'htail_idx_lt := by simp only [hp_def, List.get_eq_getElem, List.getElem_append_right hk_rev] rw [hpget] have hidx_eq : k - xp.reverse.length = k - R := by rw [hxpRev_len] rw [show yp.tail[k - xp.reverse.length]'htail_idx_lt = yp.tail[k - R]'(by rw [← hidx_eq]; exact htail_idx_lt) from by congr 1] rw [hyp_tail_get (k - R) (by rw [hyp_tail_len]; omega) htail_yp_idx_lt] simp [List.get_eq_getElem] -- xp[0] = u have hxp_get_0 : xp.get ⟨0, by omega⟩ = u := by rw [List.get_eq_getElem, List.getElem_zero] exact hxp_head_eq -- u ∉ Z (since u ∈ xp via head) have hu_notZ : u ∉ Z := by apply hxp_Z rw [← hxp_head_eq] exact List.head_mem hxp_ne refine ⟨?_, ?_⟩ · -- Adjacency: G.UAdj p[i] p[i+1] intro i hi rw [hp_len_eq2] at hi by_cases hA1 : i + 1 < R · -- A1: both in xp.reverse. Reversed-directed edge. have hiR : i < R := by omega rw [hp_L i hiR, hp_L (i + 1) hA1] have hedge_idx : R - 2 - i + 1 < R := by omega have hedge := hxp_edge (R - 2 - i) hedge_idx have heq : R - 2 - i + 1 = R - 1 - i := by omega rw [show (⟨R - 2 - i + 1, hedge_idx⟩ : Fin xp.length) = ⟨R - 1 - i, by omega⟩ from Fin.ext heq] at hedge have heq2 : R - 1 - (i + 1) = R - 2 - i := by omega rw [show (⟨R - 1 - (i + 1), by omega⟩ : Fin xp.length) = ⟨R - 2 - i, by omega⟩ from Fin.ext heq2] exact Or.inr hedge · push_neg at hA1 by_cases hA2 : i + 1 = R · -- A2: seam i = R - 1. p[R-1] = xp[0] = u. p[R] = yp.tail[0] = yp[1]. have hi_eq : i = R - 1 := by omega subst hi_eq have hiR : R - 1 < R := by omega rw [hp_L (R - 1) hiR] have hi1L : R ≤ R - 1 + 1 := by omega have hi1_lt : R - 1 + 1 < p.length := by rw [hp_len_eq2]; exact hi rw [hp_R (R - 1 + 1) hi1L hi1_lt] have h_xidx : R - 1 - (R - 1) = 0 := by omega rw [show (⟨R - 1 - (R - 1), by omega⟩ : Fin xp.length) = ⟨0, by omega⟩ from Fin.ext h_xidx] rw [hxp_get_0] have h_yidx : R - 1 + 1 - R + 1 = 1 := by omega rw [show (⟨R - 1 + 1 - R + 1, by rw [hp_len_eq2, hyp_tail_len] at hi1_lt; omega⟩ : Fin yp.length) = ⟨1, by omega⟩ from Fin.ext h_yidx] -- Goal: G.UAdj u yp[1]. Use hyp_edge at 0: G.edge yp[0] yp[1] = G.edge u yp[1]. have hedge0 := hyp_edge 0 (by omega) have hyp0 : yp.get ⟨0, by omega⟩ = u := by rw [List.get_eq_getElem, List.getElem_zero] exact hyp_head_eq rw [hyp0] at hedge0 exact inl hedge0 · -- A3: both in yp.tail. i ≥ R. have hiL : R ≤ i := by omega have hi1L : R ≤ i + 1 := by omega have hi_lt : i < p.length := by rw [hp_len_eq2]; omega have hi1_lt : i + 1 < p.length := by rw [hp_len_eq2]; exact hi rw [hp_R i hiL hi_lt, hp_R (i + 1) hi1L hi1_lt] -- Goal: G.UAdj yp[i-R+1] yp[(i+1)-R+1]. Use hyp_edge at i - R + 1. have hidx : (i - R + 1) + 1 < yp.length := by rw [hyp_tail_len] at hi; omega have hedge := hyp_edge (i - R + 1) hidx have heq : (i - R + 1) + 1 = (i + 1) - R + 1 := by omega rw [show (⟨(i - R + 1) + 1, hidx⟩ : Fin yp.length) = ⟨(i + 1) - R + 1, by omega⟩ from Fin.ext heq] at hedge exact inl hedge · -- Collider condition intro i hi rw [hp_len_eq2] at hi by_cases hC1 : i + 2 < R · -- C1: all three in xp.reverse. Reversed-directed: middle is non-collider. have hiR : i < R := by omega have hi1R : i + 1 < R := by omega rw [hp_L i hiR, hp_L (i + 1) hi1R, hp_L (i + 2) hC1] -- middle: xp[R - 1 - (i+1)] = xp[R - 2 - i]. Edge from middle to left. have h_midx : R - 1 - (i + 1) = R - 2 - i := by omega rw [show (⟨R - 1 - (i + 1), by omega⟩ : Fin xp.length) = ⟨R - 2 - i, by omega⟩ from Fin.ext h_midx] have hedge_idx : R - 2 - i + 1 < R := by omega have hedge := hxp_edge (R - 2 - i) hedge_idx have heq : R - 2 - i + 1 = R - 1 - i := by omega rw [show (⟨R - 2 - i + 1, hedge_idx⟩ : Fin xp.length) = ⟨R - 1 - i, by omega⟩ from Fin.ext heq] at hedge have hnotColl : ¬ G.IsCollider (xp.get ⟨R - 1 - i, by omega⟩) (xp.get ⟨R - 2 - i, by omega⟩) (xp.get ⟨R - 1 - (i + 2), by omega⟩) := by intro ⟨hLM, _⟩ exact G.asymm hedge hLM simp only [hnotColl, if_false] apply hxp_Z exact List.get_mem _ _ · push_neg at hC1 by_cases hC2 : i + 2 = R · -- C2: i = R - 2. Triple straddles seam: left and middle in xp.reverse, right at seam. have hi_eq : i = R - 2 := by omega subst hi_eq have hiR : R - 2 < R := by omega have hi1R : R - 2 + 1 < R := by omega have hi2L : R ≤ R - 2 + 2 := by omega have hi2_lt : R - 2 + 2 < p.length := by rw [hp_len_eq2]; exact hi rw [hp_L (R - 2) hiR, hp_L (R - 2 + 1) hi1R, hp_R (R - 2 + 2) hi2L hi2_lt] -- Middle: xp[R - 1 - (R - 2 + 1)] = xp[0] = u have h_midx : R - 1 - (R - 2 + 1) = 0 := by omega rw [show (⟨R - 1 - (R - 2 + 1), by omega⟩ : Fin xp.length) = ⟨0, by omega⟩ from Fin.ext h_midx] rw [hxp_get_0] -- Non-collider: edge from u to xp[1] (hxp_edge at 0), so ¬ edge xp[1] u. have hedge := hxp_edge 0 (by omega) rw [hxp_get_0] at hedge have h_lidx : R - 1 - (R - 2) = 1 := by omega rw [show (⟨R - 1 - (R - 2), by omega⟩ : Fin xp.length) = ⟨1, by omega⟩ from Fin.ext h_lidx] have hnotColl : ¬ G.IsCollider (xp.get ⟨1, by omega⟩) u (yp.get ⟨R - 2 + 2 - R + 1, by rw [hp_len_eq2, hyp_tail_len] at hi2_lt; omega⟩) := by intro ⟨hLM, _⟩ exact G.asymm hedge hLM simp only [hnotColl, if_false] exact hu_notZ · by_cases hC3 : i + 1 = R · -- C3: seam at middle. i = R - 1. -- Triple: xp.reverse[R-1] = u, yp.tail[0] = yp[1], yp.tail[1] = yp[2]. -- … truncated; follow the source link for the rest …
    Causalean.Graph.DAG.fork_isActiveWalk · Causalean/Graph/DSep/Ancestral.lean:439
Backdoor­Bridges 1 core · 1 supporting This file contains graph-level lemmas for backdoor identification arguments. ★ dSep_union_roots_right

Backdoor Graph Bridges

This file contains graph-level lemmas for backdoor identification arguments. The main theorem DAG.dSep_union_roots_right shows that adding root vertices disjoint from the query variables to the conditioning set preserves d-separation. The proof uses active-walk semantics: a conditioned root can only appear as a non-collider fork on an interior walk, so it blocks rather than opens walks.

theorem dSep_union_roots_right reviewed
Causalean.Graph.DAG

Adding root nodes (no incoming edges) to the conditioning set preserves d-separation. In a DAG G, suppose X and Y are d-separated by Z, every vertex of R has no incoming edge in G, i.e. R consists of root vertices, and R is disjoint from X and from Y. Then X and Y remain d-separated once the root vertices R are added to the conditioning set: G.dSep X Y (Z ∪ R).

Formal statement
V :
Type u_1
shared
G :
DAG V
shared
X Y Z R :
hXY_sep :
G.dSep X Y Z
hRoots :
∀ r ∈ R, ∀ u, ¬ G.edge u r
hRX :
hRY :
G.dSep X Y (Z ∪ R)
Proof (Lean source)
theorem dSep_union_roots_right {X Y Z R : Finset V} (hXY_sep : G.dSep X Y Z) (hRoots : ∀ r ∈ R, ∀ u, ¬ G.edge u r) (hRX : Disjoint R X) (hRY : Disjoint R Y) : G.dSep X Y (Z ∪ R) := by rcases hXY_sep with ⟨hXY, hXZ, hYZ, hReach⟩ refine ⟨hXY, ?_, ?_, ?_⟩ · rw [Finset.disjoint_left] intro v hvX hvZR rcases Finset.mem_union.mp hvZR with hvZ | hvR · exact Finset.disjoint_left.mp hXZ hvX hvZ · exact Finset.disjoint_left.mp hRX hvR hvX · rw [Finset.disjoint_left] intro v hvY hvZR rcases Finset.mem_union.mp hvZR with hvZ | hvR · exact Finset.disjoint_left.mp hYZ hvY hvZ · exact Finset.disjoint_left.mp hRY hvR hvY rw [Finset.disjoint_left] at hReach ⊢ intro v hv_ZR hvY rw [G.bbReachableVertices_iff_activeWalk] at hv_ZR obtain ⟨x, hxX, p, hlen, hact, hhead, hlast⟩ := hv_ZR obtain ⟨hadj, hcoll⟩ := hact -- Helper: the interior vertex condition, parametrized by the triple's left index. -- For a triple (i, i+1, i+2), middle `p[i+1]` is not a collider and not in Z ∪ R. -- If `p[i+1] ∈ R`, the "not in Z ∪ R" fails, contradiction. have hNoR_interior : ∀ (i : ℕ) (hi : i + 2 < p.length), p.get ⟨i + 1, by omega⟩ ∉ R := by intro i hi hmem have hc := hcoll i hi set l := p.get ⟨i, by omega⟩ set m := p.get ⟨i + 1, by omega⟩ set r := p.get ⟨i + 2, hi⟩ have hnoInL : ¬ G.edge l m := hRoots _ hmem l have hnc : ¬ G.IsCollider l m r := fun h => hnoInL h.1 simp only [hnc, if_false] at hc exact hc (mem_union_right _ hmem) -- Rebuild active walk given Z. have hact_Z : G.IsActiveWalk Z p := by refine ⟨hadj, ?_⟩ intro i hi have hc := hcoll i hi set l := p.get ⟨i, by omega⟩ set m := p.get ⟨i + 1, by omega⟩ set r := p.get ⟨i + 2, hi⟩ have hm_notR : m ∉ R := hNoR_interior i hi by_cases hColl : G.IsCollider l m r · -- Collider case. simp only [hColl, if_true] at hc simp only [hColl, if_true] -- `hc : m ∈ bbZAncestors (Z ∪ R)`. Want `m ∈ bbZAncestors Z`. -- bbZAncestors = ancestralSet = S ∪ ancestorsSet S simp only [bbZAncestors, ancestralSet] at hc ⊢ rcases Finset.mem_union.mp hc with hZR | hmAnc · rcases Finset.mem_union.mp hZR with hZ | hR · exact mem_union_left _ hZ · exact absurd hR hm_notR · simp only [ancestorsSet, mem_filter, Finset.mem_univ, true_and] at hmAnc obtain ⟨w, hwZR, hma⟩ := hmAnc rcases Finset.mem_union.mp hwZR with hwZ | hwR · refine mem_union_right _ ?_ simp only [ancestorsSet, mem_filter, Finset.mem_univ, true_and] exact ⟨w, hwZ, hma⟩ · exact absurd hma (G.not_isAncestor_of_root' (hRoots w hwR) m) · -- Non-collider case. simp only [hColl, if_false] at hc simp only [hColl, if_false] exact fun hmZ => hc (mem_union_left _ hmZ) -- Close: active walk given Z witnesses bbReachable Z X, contradicting hXY_sep. have hvReachZ : v ∈ G.bbReachableVertices Z X := by rw [G.bbReachableVertices_iff_activeWalk] exact ⟨x, hxX, p, hlen, hact_Z, hhead, hlast⟩ exact hReach hvReachZ hvY
Causalean.Graph.DAG.dSep_union_roots_right · Causalean/Graph/DSep/BackdoorBridges.lean:55 · uses DAG , dSep
1 supporting declaration (lemmas, instances)
  • not_isAncestor_of_root' lemma — In a finite directed acyclic graph, if a vertex has no incoming edges, then a selected vertex is not its proper ancestor.
    V :
    Type u_1
    shared
    G :
    DAG V
    shared
    r :
    V
    hr :
    ∀ u, ¬ G.edge u r
    u :
    V
    ¬ G.isAncestor u r
    Proof (Lean source)
    lemma not_isAncestor_of_root' {r : V} (hr : ∀ u, ¬ G.edge u r) (u : V) : ¬ G.isAncestor u r := by intro h cases h with | edge he => exact hr _ he | trans _ he => exact hr _ he
    Causalean.Graph.DAG.not_isAncestor_of_root' · Causalean/Graph/DSep/BackdoorBridges.lean:42
Induce­Transport 1 core · 3 supporting This file collects graph-only bridges that compare d-separation in an induced SWIG with d-separation in the ambient SWIG. ★ dSep_union_fixed_of_induce_dSep

d-Separation Transport for Induced SWIGs

This file collects graph-only bridges that compare d-separation in an induced SWIG with d-separation in the ambient SWIG. The main bridge is tailored to ancestral induced graphs: active walks between observed vertices cannot use vertices outside the ancestral support, except for fixed nodes, which are handled by placing all fixed nodes in the conditioning set.

The support lemmas prove idempotence of ancestral closure, preservation of ancestor paths and collider activations inside an ancestral induced SWIG, and non-membership of fixed nodes on relevant observed-endpoint active walks. The main theorem SWIGGraph.dSep_union_fixed_of_induce_dSep lifts d-separation from (G.induce R).dag to the ambient G.dag after adjoining all fixed nodes to the conditioning set.

theorem dSep_union_fixed_of_induce_dSep reviewed
Causalean.Graph.SWIGGraph

D-separation in an ancestral induced SWIG lifts to the ambient SWIG once fixed intervention nodes are included in the conditioning set. Fix a SWIG G and a node set R that is closed under G's ancestral relation, with X, Y, and Z each contained in the observed nodes of R. If X and Y are d-separated by Z in the graph induced on R, then X and Y are d-separated by Z together with G's fixed intervention nodes, in the ambient graph.

Formal statement
N :
Type u_1
shared
G :
shared
R X Y Z :
hX :
X ⊆ R ∩ G.observed
hY :
Y ⊆ R ∩ G.observed
hZ :
Z ⊆ R ∩ G.observed
hR :
G.dag.ancestralSet R ⊆ R
h :
(G.induce R).dag.dSep X Y Z
G.dag.dSep X Y (Z ∪ G.fixed)
Proof (Lean source)
theorem dSep_union_fixed_of_induce_dSep (R X Y Z : Finset (SWIGNode N)) (hX : X ⊆ R ∩ G.observed) (hY : Y ⊆ R ∩ G.observed) (hZ : Z ⊆ R ∩ G.observed) (hR : G.dag.ancestralSet R ⊆ R) (h : (G.induce R).dag.dSep X Y Z) : G.dag.dSep X Y (Z ∪ G.fixed) := by rcases h with ⟨hXY, hXZ, hYZ, hReach⟩ refine ⟨hXY, ?_, ?_, ?_⟩ · rw [Finset.disjoint_left] intro x hxX hxZf rcases Finset.mem_union.mp hxZf with hxZ | hxF · exact Finset.disjoint_left.mp hXZ hxX hxZ · exact G.not_mem_fixed_of_mem_observed ((Finset.mem_inter.mp (hX hxX)).2) hxF · rw [Finset.disjoint_left] intro y hyY hyZf rcases Finset.mem_union.mp hyZf with hyZ | hyF · exact Finset.disjoint_left.mp hYZ hyY hyZ · exact G.not_mem_fixed_of_mem_observed ((Finset.mem_inter.mp (hY hyY)).2) hyF rw [Finset.disjoint_left] intro y hyReach hyY rw [G.dag.bbReachableVertices_iff_activeWalk] at hyReach obtain ⟨x, hxX, p, hlen, hact, hhead, hlast⟩ := hyReach have hnotFixed : ∀ v ∈ p, v ∉ G.fixed := by intro v hv exact G.activeWalk_node_not_fixed hX hY hxX hyY hact hhead hlast hv have hnodeR : ∀ v ∈ p, v ∈ R := by intro v hv have hvAnc : v ∈ G.dag.ancestralSet (X ∪ Y ∪ (Z ∪ G.fixed)) := G.dag.activeWalk_nodes_are_ancestors hxX hyY hact hhead hlast v hv exact G.mem_R_of_ancestor_union_not_fixed hX hY hZ hR hvAnc (hnotFixed v hv) let active : Finset (SWIGNode N) := (G.fixed.filter (fun s => iotaMap s ∈ R ∩ G.observed)) ∪ (R ∩ G.observed) ∪ (G.unobserved.filter (fun u => ∃ z ∈ R ∩ G.observed, G.dag.edge u z)) have hactive_of_incident : ∀ {v : SWIGNode N}, v ∈ p → (∃ w ∈ R, G.dag.edge v w ∨ G.dag.edge w v) → v ∈ active := by intro v hv hinc exact G.mem_induce_active_of_mem_R_of_not_fixed_of_incident (hnodeR v hv) (hnotFixed v hv) hinc have hIndEdge : ∀ {u v : SWIGNode N}, G.dag.edge u v → u ∈ active → v ∈ active → (G.induce R).dag.edge u v := by intro u v huv hu hv exact G.induced_edge_of_edge_of_active huv hu hv have hactInd : (G.induce R).dag.IsActiveWalk Z p := by refine ⟨?_, ?_⟩ · intro i hi let u := p.get ⟨i, by omega⟩ let v := p.get ⟨i + 1, hi⟩ have huMem : u ∈ p := List.get_mem _ _ have hvMem : v ∈ p := List.get_mem _ _ rcases hact.1 i hi with huv | hvu · have huAct : u ∈ active := hactive_of_incident huMem ⟨v, hnodeR v hvMem, Or.inl huv⟩ have hvAct : v ∈ active := hactive_of_incident hvMem ⟨u, hnodeR u huMem, Or.inr huv⟩ exact Or.inl (hIndEdge huv huAct hvAct) · have huAct : u ∈ active := hactive_of_incident huMem ⟨v, hnodeR v hvMem, Or.inr hvu⟩ have hvAct : v ∈ active := hactive_of_incident hvMem ⟨u, hnodeR u huMem, Or.inl hvu⟩ exact Or.inr (hIndEdge hvu hvAct huAct) · intro i hi let l := p.get ⟨i, by omega⟩ let m := p.get ⟨i + 1, by omega⟩ let r := p.get ⟨i + 2, hi⟩ have hlMem : l ∈ p := List.get_mem _ _ have hmMem : m ∈ p := List.get_mem _ _ have hrMem : r ∈ p := List.get_mem _ _ have hambClause := hact.2 i hi by_cases hcollInd : (G.induce R).dag.IsCollider l m r · rw [if_pos hcollInd] have hlmAmb : G.dag.edge l m := by change (G.inducedDag active).edge l m ∧ (G.inducedDag active).edge r m at hcollInd exact ((G.inducedDag_edge_iff active l m).mp hcollInd.1).1 have hrmAmb : G.dag.edge r m := by change (G.inducedDag active).edge l m ∧ (G.inducedDag active).edge r m at hcollInd exact ((G.inducedDag_edge_iff active r m).mp hcollInd.2).1 have hcollAmb : G.dag.IsCollider l m r := ⟨hlmAmb, hrmAmb⟩ rw [if_pos hcollAmb] at hambClause have hmpar : G.dag.parents m ≠ ∅ := by intro hempty have : l ∈ G.dag.parents m := G.dag.mem_parents.mpr hlmAmb simp [hempty] at this exact G.induced_bbZAncestors_of_ambient_union_fixed hZ hR hmpar hambClause · rw [if_neg hcollInd] intro hmZ by_cases hcollAmb : G.dag.IsCollider l m r · have hlAct : l ∈ active := hactive_of_incident hlMem ⟨m, hnodeR m hmMem, Or.inl hcollAmb.1⟩ have hmAct : m ∈ active := hactive_of_incident hmMem ⟨l, hnodeR l hlMem, Or.inr hcollAmb.1⟩ have hrAct : r ∈ active := hactive_of_incident hrMem ⟨m, hnodeR m hmMem, Or.inl hcollAmb.2⟩ have hcollInd' : (G.induce R).dag.IsCollider l m r := ⟨hIndEdge hcollAmb.1 hlAct hmAct, hIndEdge hcollAmb.2 hrAct hmAct⟩ exact hcollInd hcollInd' · rw [if_neg hcollAmb] at hambClause exact hambClause (mem_union_left _ hmZ) have hyReachInd : y ∈ (G.induce R).dag.bbReachableVertices Z X := by rw [(G.induce R).dag.bbReachableVertices_iff_activeWalk] exact ⟨x, hxX, p, hlen, hactInd, hhead, hlast⟩ exact Finset.disjoint_left.mp hReach hyReachInd hyY
3 supporting declarations (lemmas, instances)
  • ancestralSet_idem theorem — In a finite directed acyclic graph, taking the ancestral closure of a vertex set and then closing it again leaves that closure unchanged.
    V :
    G :
    DAG V
    S :
    G.ancestralSet (G.ancestralSet S) = G.ancestralSet S
    Proof (Lean source)
    theorem ancestralSet_idem {V : Type*} [DecidableEq V] [Fintype V] (G : DAG V) (S : Finset V) : G.ancestralSet (G.ancestralSet S) = G.ancestralSet S := by apply Finset.Subset.antisymm · intro u hu rw [ancestralSet, mem_union] at hu rcases hu with hu | hu · exact hu · rw [ancestorsSet, mem_filter] at hu obtain ⟨_, v, hv, huv⟩ := hu rw [ancestralSet, mem_union] at hv rcases hv with hvS | hvAnc · exact G.mem_ancestralSet_of_isAncestor hvS huv · rw [ancestorsSet, mem_filter] at hvAnc obtain ⟨_, w, hwS, hvw⟩ := hvAnc exact G.mem_ancestralSet_of_isAncestor hwS (G.isAncestor_trans huv hvw) · exact G.subset_ancestralSet (G.ancestralSet S)
    Causalean.Graph.DAG.ancestralSet_idem · Causalean/Graph/DSep/InduceTransport.lean:37
  • not_mem_fixed_of_mem_observed lemma — In a finite single-world intervention graph, an observed vertex does not belong to the fixed intervention set.
    N :
    Type u_1
    shared
    G :
    shared
    v :
    hv :
    v ∈ G.observed
    v ∉ G.fixed
    Proof (Lean source)
    lemma not_mem_fixed_of_mem_observed {v : SWIGNode N} (hv : v ∈ G.observed) : v ∉ G.fixed := by intro hfix obtain ⟨n, hn⟩ := G.fixed_is_fixed v hfix obtain ⟨m, hm⟩ := G.observed_is_random v hv rw [hn] at hm cases hm
    Causalean.Graph.SWIGGraph.not_mem_fixed_of_mem_observed · Causalean/Graph/DSep/InduceTransport.lean:63
  • activeWalk_node_not_fixed lemma — In a finite single-world intervention graph, consider [support, endpoint, and conditioning sets, observed endpoints, a walk, and a selected walk vertex] (hyp:R,X,Y,Z,x,y,p,v). If the endpoint sets lie in the observed support, the endpoints belong to their sets, the walk is active after conditioning on both the given and fixed vertices, it has the stated endpoints, and the selected vertex lies on it, then that vertex is not fixed.
    N :
    Type u_1
    shared
    G :
    shared
    R X Y Z :
    x y v :
    p :
    hX :
    X ⊆ R ∩ G.observed
    hY :
    Y ⊆ R ∩ G.observed
    hxX :
    x ∈ X
    hyY :
    y ∈ Y
    hact :
    G.dag.IsActiveWalk (Z ∪ G.fixed) p
    hhead :
    p.head? = some x
    hlast :
    p.getLast? = some y
    hv :
    v ∈ p
    v ∉ G.fixed
    Proof (Lean source)
    lemma activeWalk_node_not_fixed {R X Y Z : Finset (SWIGNode N)} {x y v : SWIGNode N} {p : List (SWIGNode N)} (hX : X ⊆ R ∩ G.observed) (hY : Y ⊆ R ∩ G.observed) (hxX : x ∈ X) (hyY : y ∈ Y) (hact : G.dag.IsActiveWalk (Z ∪ G.fixed) p) (hhead : p.head? = some x) (hlast : p.getLast? = some y) (hv : v ∈ p) : v ∉ G.fixed := by rw [List.mem_iff_get] at hv obtain ⟨i, rfl⟩ := hv by_cases hi0 : i.val = 0 · have hpne : p ≠ [] := by intro hp simp [hp] at hhead have hhead_get : p.get ⟨0, by omega⟩ = x := by have h := List.head?_eq_some_head hpne rw [h] at hhead have hx_eq : p.head hpne = x := Option.some_inj.mp hhead rw [← hx_eq] simp [List.head_eq_getElem hpne] have hidx : i = ⟨0, by omega⟩ := Fin.ext hi0 rw [hidx, hhead_get] exact G.not_mem_fixed_of_mem_observed (Finset.mem_inter.mp (hX hxX)).2 · by_cases hilast : i.val = p.length - 1 · have hpne : p ≠ [] := by intro hp simp [hp] at hlast have hlast_get : p.get ⟨p.length - 1, by omega⟩ = y := by have h := getLast?_eq_some_getLast hpne rw [h] at hlast have hy_eq : p.getLast hpne = y := Option.some_inj.mp hlast rw [← hy_eq] exact (List.getLast_eq_getElem hpne).symm have hidx : i = ⟨p.length - 1, by omega⟩ := Fin.ext hilast rw [hidx, hlast_get] exact G.not_mem_fixed_of_mem_observed (Finset.mem_inter.mp (hY hyY)).2 · have htri : (i.val - 1) + 2 < p.length := by omega have hclause := hact.2 (i.val - 1) htri have hrew₁ : i.val - 1 + 1 = i.val := by omega simp only [hrew₁] at hclause set l := p.get ⟨i.val - 1, by omega⟩ with hl set m := p.get ⟨i.val, i.isLt⟩ with hm set r := p.get ⟨i.val - 1 + 2, htri⟩ with hr by_cases hcoll : G.dag.IsCollider l m r · rw [if_pos hcoll] at hclause intro hmfix have hpar : l ∈ G.dag.parents m := G.dag.mem_parents.mpr hcoll.1 have hroot := G.fixed_are_roots m hmfix simp [hroot] at hpar · rw [if_neg hcoll] at hclause intro hmfix exact hclause (mem_union_right _ hmfix)
    Causalean.Graph.SWIGGraph.activeWalk_node_not_fixed · Causalean/Graph/DSep/InduceTransport.lean:251