Graph.Markov­Equiv

Entry point for the formalization of the Verma–Pearl characterization of Markov equivalence (Verma & Pearl, *Equivalence and synthesis of causal models*, 1990): two directed acyclic graphs declare the same conditional-in

Markov­Equiv 1 core · 0 supporting Entry point for the formalization of the Verma–Pearl characterization of Markov equivalence (Verma & Pearl, *Equivalence and synthesis of causal models*, 1990): two directed acyclic graphs declare the same conditional-in ★ markovEquiv_iff_sameSkeleton_sameImmoralities

Markov equivalence of DAGs (Verma–Pearl) — umbrella

Entry point for the formalization of the Verma–Pearl characterization of Markov equivalence (Verma & Pearl, Equivalence and synthesis of causal models, 1990): two directed acyclic graphs declare the same conditional-independence constraints exactly when they have the same skeleton and the same v-structures. Import this file for the whole development; the headline result is stated here so it is not buried among the supporting files.

Main results

* MarkovEquiv (Defs.lean) — two DAGs declare the same d-separations. * markovEquiv_iff_sameSkeleton_sameImmoralities (this file) — the flagship. Two DAGs are Markov equivalent iff they have the same skeleton (SameSkeleton, undirected adjacency) and the same v-structures (SameImmoralities, colliders with non-adjacent parents). The constraint-based characterization underlying PC/FCI/GES and the CPDAG. * distMarkovEquiv_of_markovEquiv (Distributional.lean) — graph-level Markov equivalence implies distributional Markov equivalence (same distributions have the two DAGs as I-maps), via the global Markov bridge isGlobalIMap_dag_self. * DAG.dSep_iff_moralSep (Moralization.lean) — the Lauritzen moralization criterion: d-separation in a DAG is equivalent to graph separation in the moral graph of the ancestral closure.

Supporting machinery

Readoff.lean (easy direction — skeleton and v-structures are read off the d-separation relation), Transfer.lean (hard direction — same skeleton + v-structures transfer every d-separation), Distributional.lean (the I-map / faithfulness layer connecting the graph notion to distributions), and Moralization.lean (the ancestral moral graph criterion for d-separation). The development reuses the existing d-separation engine (Causalean.Graph.DSep) and the global Markov property (Causalean.SCM.Do.GlobalMarkov).

theorem markovEquiv_iff_sameSkeleton_sameImmoralities reviewed
Causalean

Verma–Pearl (1990). For two DAGs G₁, G₂ on the same vertex set, they are Markov equivalent — they declare exactly the same d-separations, hence impose the same conditional-independence constraints — if and only if they have the same skeleton and the same v-structures (immoralities).

Formal statement
G₁ G₂ :
DAG V
MarkovEquiv G₁ G₂ ↔ SameSkeleton G₁ G₂ ∧ SameImmoralities G₁ G₂
Proof (Lean source)
theorem markovEquiv_iff_sameSkeleton_sameImmoralities (G₁ G₂ : DAG V) : MarkovEquiv G₁ G₂ ↔ SameSkeleton G₁ G₂ ∧ SameImmoralities G₁ G₂ := ⟨sameSkeleton_sameImmoralities_of_markovEquiv, fun ⟨hskel, himm⟩ => markovEquiv_of_sameSkeleton_sameImmoralities hskel himm⟩
Causalean.markovEquiv_iff_sameSkeleton_sameImmoralities · Causalean/Graph/MarkovEquiv.lean:51 · uses DAG , MarkovEquiv , SameImmoralities , SameSkeleton
Defs 5 core · 5 supporting This file introduces the combinatorial vocabulary for the Verma–Pearl characterization of Markov equivalence. ★ MarkovEquiv★ refl

Markov equivalence of DAGs — basic definitions

This file introduces the combinatorial vocabulary for the Verma–Pearl characterization of Markov equivalence. Two directed acyclic graphs on the same vertices are Markov equivalent when they encode exactly the same d-separation statements, i.e. the same conditional-independence constraints. The Verma–Pearl theorem (proved in the umbrella file) says this happens precisely when the graphs share a skeleton and the same v-structures (immoralities).

The definitions here are:

* DAG.IsImmorality G a b c — there is a v-structure (immorality) a → b ← c whose two parents a, c are non-adjacent and distinct; * SameSkeleton G₁ G₂ — the two graphs have the same undirected adjacency; * SameImmoralities G₁ G₂ — the two graphs have the same v-structures; * MarkovEquiv G₁ G₂ — the two graphs declare the same d-separations.

All of these are decidable on a finite vertex type. The underlying undirected adjacency is the existing DAG.UAdj (a and b joined by an edge in either direction) and a collider a → b ← c is DAG.IsCollider.

def IsImmorality reviewed
Causalean.DAG

A v-structure (immorality) at b: directed edges a → b and c → b whose tails a and c are distinct and not joined by any edge. Immoralities are the colliders whose parents are non-adjacent; they are exactly the part of the collider structure that is visible to conditional independence.

Definition (Lean source)
def IsImmorality (a b c : V) : Prop := G.edge a b ∧ G.edge c b ∧ ¬ G.UAdj a c ∧ a ≠ c
Causalean.DAG.IsImmorality · Causalean/Graph/MarkovEquiv/Defs.lean:39 · uses DAG
def SameSkeleton reviewed
Causalean

Two DAGs have the same skeleton when their undirected adjacency relations agree: for every pair a, b, there is an edge (in either direction) between them in G₁ iff there is one in G₂.

Definition (Lean source)
def SameSkeleton (G₁ G₂ : DAG V) : Prop := ∀ a b, G₁.UAdj a b ↔ G₂.UAdj a b
Causalean.SameSkeleton · Causalean/Graph/MarkovEquiv/Defs.lean:52 · uses DAG
def SameImmoralities reviewed
Causalean

Two DAGs have the same v-structures when their immorality relations agree.

Definition (Lean source)
def SameImmoralities (G₁ G₂ : DAG V) : Prop := ∀ a b c, G₁.IsImmorality a b c ↔ G₂.IsImmorality a b c
Causalean.SameImmoralities · Causalean/Graph/MarkovEquiv/Defs.lean:57 · uses DAG
def MarkovEquiv reviewed
Causalean

Two DAGs are Markov equivalent when they entail exactly the same d-separations: for every triple of vertex sets X, Y, Z, X and Y are d-separated by Z in G₁ iff they are in G₂. Equivalently (via the global Markov property) the two graphs impose the same conditional-independence constraints on every distribution. Pairwise disjointness is already part of dSep, so it need not be repeated here.

Definition (Lean source)
def MarkovEquiv (G₁ G₂ : DAG V) : Prop := ∀ X Y Z : Finset V, G₁.dSep X Y Z ↔ G₂.dSep X Y Z
Causalean.MarkovEquiv · Causalean/Graph/MarkovEquiv/Defs.lean:61 · uses DAG
theorem refl reviewed
Causalean.MarkovEquiv

For any DAG G, G is Markov equivalent to itself.

Formal statement
G :
DAG V
Proof (Lean source)
@[refl] theorem MarkovEquiv.refl (G : DAG V) : MarkovEquiv G G := fun _ _ _ => Iff.rfl
5 supporting declarations (lemmas, instances)
Covered­Reversal 5 core · 20 supporting This file develops the covered-edge reversal route to the hard direction of Verma–Pearl, following Andersson, Madigan and Perlman (1997), *A characterization of Markov equivalence classes for acyclic digraphs*, Appendix ★ IsCoveredEdge★ markovEquiv_flipEdge

Covered-edge reversal (the Andersson–Madigan–Perlman route)

This file develops the covered-edge reversal route to the hard direction of Verma–Pearl, following Andersson, Madigan and Perlman (1997), A characterization of Markov equivalence classes for acyclic digraphs, Appendix B. The route proves the hard direction through covered-edge reversals rather than through moralization.

A directed edge a → b is covered when a and b have the same parents apart from the edge itself (pa(a) = pa(b) \ {a}); equivalently every other vertex is a parent of a iff it is a parent of b. The three pillars are:

* Reversibility (AMP Lemma 3.1): a covered edge can be reversed and the result is still an acyclic digraph with the same skeleton and the same immoralities (flipEdge_*). * Per-step invariance: reversing one covered edge preserves all d-separations, hence Markov equivalence (markovEquiv_flipEdge). This is the analytic core. * Decomposition (AMP Lemma 3.2): two ADGs with the same skeleton and same immoralities are connected by a finite sequence of single covered-edge reversals (exists_covered_reversed_edge + the assembly).

The flipped graph is materialised with DAG.ofAcyclic (AcyclicConstruct.lean).

def IsCoveredEdge reviewed
Causalean.DAG

A directed edge a → b is covered when a and b share all parents apart from the edge a → b itself: every vertex c ≠ a is a parent of a iff it is a parent of b. Then pa(a) = pa(b) \ {a}. Covered edges are exactly the reversible (unprotected) ones.

Definition (Lean source)
def IsCoveredEdge (a b : V) : Prop := G.edge a b ∧ ∀ c, c ≠ a → (G.edge c a ↔ G.edge c b)
def flipMinus reviewed
Causalean.DAG

The edge relation of G with the single edge a → b deleted.

Definition (Lean source)
def flipMinus (a b : V) : V → V → Prop := fun u w => G.edge u w ∧ ¬ (u = a ∧ w = b)
def flipRel reviewed
Causalean.DAG

The edge relation of G with the single edge a → b reversed to b → a.

Definition (Lean source)
def flipRel (a b : V) : V → V → Prop := fun u w => G.flipMinus a b u w ∨ (u = b ∧ w = a)
def flipEdge reviewed
Causalean.DAG

The directed acyclic graph obtained from G by reversing the covered edge a → b.

Definition (Lean source)
noncomputable def flipEdge {a b : V} (hcov : G.IsCoveredEdge a b) : DAG V := DAG.ofAcyclic (G.flipRel a b) (flipRel_acyclic hcov)
theorem markovEquiv_flipEdge reviewed
Causalean.DAG

The analytic core (AMP, per-step invariance). In a DAG G, if the edge a → b is covered — every other parent of b is also a parent of a, and vice versa, then the DAG obtained by reversing that edge to b → a is Markov equivalent to G: the two graphs license exactly the same d-separation statements. This is the single-edge kernel used by the covered-reversal proof of the Verma--Pearl hard direction.

Formal statement
a b :
V
hcov :
G.IsCoveredEdge a b
Proof (Lean source)
theorem markovEquiv_flipEdge {a b : V} (hcov : G.IsCoveredEdge a b) : MarkovEquiv G (flipEdge hcov) := by intro X Y Z by_cases hXY : Disjoint X Y · by_cases hXZ : Disjoint X Z · by_cases hYZ : Disjoint Y Z · have hAP : G.HasActivePath X Y Z ↔ (flipEdge hcov).HasActivePath X Y Z := by constructor · exact hasActivePath_flipEdge_of_isCoveredEdge hcov X Y Z hXY · intro hp have hp₂ : (flipEdge (flipEdge_isCoveredEdge_back hcov)).HasActivePath X Y Z := @hasActivePath_flipEdge_of_isCoveredEdge V _ _ (flipEdge hcov) b a (flipEdge_isCoveredEdge_back hcov) X Y Z hXY hp exact (hasActivePath_edge_congr (flipEdge_flipEdge_edge hcov) X Y Z).mp hp₂ apply not_iff_not.mp calc ¬ G.dSep X Y Z ↔ G.HasActivePath X Y Z := not_dSep_iff_hasActivePath G X Y Z hXY hXZ hYZ _ ↔ (flipEdge hcov).HasActivePath X Y Z := hAP _ ↔ ¬ (flipEdge hcov).dSep X Y Z := (not_dSep_iff_hasActivePath (flipEdge hcov) X Y Z hXY hXZ hYZ).symm · exact iff_of_false (fun h => hYZ h.2.2.1) (fun h => hYZ h.2.2.1) · exact iff_of_false (fun h => hXZ h.2.1) (fun h => hXZ h.2.1) · exact iff_of_false (fun h => hXY h.1) (fun h => hXY h.1)
20 supporting declarations (lemmas, instances)
  • flipMinus_le theorem — A flipMinus-edge is in particular a G-edge.
    a b u w :
    V
    h :
    G.flipMinus a b u w
    G.edge u w
    Proof (Lean source)
    theorem flipMinus_le {a b u w : V} (h : G.flipMinus a b u w) : G.edge u w := h.1
  • topoOrder_lt_of_flipMinus_transGen theorem — A directed flipMinus-path strictly increases the topological order.
    a b u w :
    V
    h :
    TransGen (G.flipMinus a b) u w
    G.topoOrder u < G.topoOrder w
    Proof (Lean source)
    theorem topoOrder_lt_of_flipMinus_transGen {a b u w : V} (h : TransGen (G.flipMinus a b) u w) : G.topoOrder u < G.topoOrder w := by induction h with | single he => exact G.topoOrder_lt _ _ he.1 | tail _ he ih => exact lt_trans ih (G.topoOrder_lt _ _ he.1)
    Causalean.DAG.topoOrder_lt_of_flipMinus_transGen · Causalean/Graph/MarkovEquiv/CoveredReversal.lean:58
  • ne theorem — A covered edge is genuinely an edge a → b, so a ≠ b.
    a b :
    V
    h :
    G.IsCoveredEdge a b
    a ≠ b
    Proof (Lean source)
    theorem IsCoveredEdge.ne {a b : V} (h : G.IsCoveredEdge a b) : a ≠ b := by rintro rfl; exact G.irrefl _ h.1
  • flipRel_acyclic theorem — Reversing a covered edge keeps the graph acyclic. The transitive closure of the flipped relation is irreflexive. Key step (AMP Lemma 3.1): a directed G-path a ⇝ b of length ≥ 2 would end at a parent c ≠ a of b, hence (covered) a parent of a, closing a G-cycle; so no such detour exists and the single reversal introduces no cycle.
    a b :
    V
    hcov :
    G.IsCoveredEdge a b
    ∀ v, ¬ TransGen (G.flipRel a b) v v
    Proof (Lean source)
    theorem flipRel_acyclic {a b : V} (hcov : G.IsCoveredEdge a b) : ∀ v, ¬ TransGen (G.flipRel a b) v v := by -- last-step destructor for a transitive-closure path have transGen_last : ∀ {z : V}, TransGen (G.flipMinus a b) a z → G.flipMinus a b a z ∨ ∃ c, TransGen (G.flipMinus a b) a c ∧ G.flipMinus a b c z := by intro z h induction h with | single h => exact inl h | tail h1 h2 _ => exact inr ⟨_, h1, h2⟩ -- No `flipMinus`-detour from `a` to `b`: the last vertex `c` of such a path is a parent of -- `b` other than `a`, hence (covered) a parent of `a`, closing a `G`-cycle. have hnodetour : ¬ TransGen (G.flipMinus a b) a b := by intro h rcases transGen_last h with hab | ⟨c, hac, hcb⟩ · exact hab.2 ⟨rfl, rfl⟩ · have hca' : c ≠ a := fun hc => hcb.2 ⟨hc, rfl⟩ have hca : G.edge c a := (hcov.2 c hca').mpr hcb.1 have l1 : G.topoOrder a < G.topoOrder c := topoOrder_lt_of_flipMinus_transGen hac have l2 : G.topoOrder c < G.topoOrder a := G.topoOrder_lt _ _ hca omega -- Any flipped-walk either avoids `b → a` (so it is a `flipMinus`-walk) or it splits as -- `x ⇝ b` (before the first `b → a`) and `a ⇝ y` (after), both `flipMinus`-reachable. have hP : ∀ {x y}, TransGen (G.flipRel a b) x y → TransGen (G.flipMinus a b) x y ∨ (Relation.ReflTransGen (G.flipMinus a b) x b ∧ Relation.ReflTransGen (G.flipMinus a b) a y) := by intro x y h induction h with | single hxy => rcases hxy with h0 | ⟨hxb, hya⟩ · exact inl (Relation.TransGen.single h0) · subst hxb; subst hya exact inr ⟨Relation.ReflTransGen.refl, Relation.ReflTransGen.refl⟩ | @tail c y _ hcy ih => rcases hcy with h0 | ⟨hcb, hya⟩ · rcases ih with hl | ⟨hr1, hr2⟩ · exact inl (hl.tail h0) · exact inr ⟨hr1, hr2.tail h0⟩ · subst hcb; subst hya rcases ih with hl | ⟨hr1, _hr2⟩ · exact inr ⟨hl.to_reflTransGen, Relation.ReflTransGen.refl⟩ · exact inr ⟨hr1, Relation.ReflTransGen.refl⟩ intro v hv rcases hP hv with hl | ⟨hr1, hr2⟩ · exact absurd (topoOrder_lt_of_flipMinus_transGen hl) (lt_irrefl _) · have hab : Relation.ReflTransGen (G.flipMinus a b) a b := hr2.trans hr1 rcases Relation.reflTransGen_iff_eq_or_transGen.mp hab with heq | htr · exact hcov.ne heq.symm · exact hnodetour htr
  • flipEdge_edge theorem — In the graph obtained by reversing a covered edge, the edges are exactly the old edges except for deleting a → b and adding b → a.
    a b :
    V
    hcov :
    G.IsCoveredEdge a b
    u w :
    V
    (flipEdge hcov).edge u w ↔ (G.edge u w ∧ ¬ (u = a ∧ w = b)) ∨ (u = b ∧ w = a)
    Proof (Lean source)
    @[simp] theorem flipEdge_edge {a b : V} (hcov : G.IsCoveredEdge a b) (u w : V) : (flipEdge hcov).edge u w ↔ (G.edge u w ∧ ¬ (u = a ∧ w = b)) ∨ (u = b ∧ w = a) := by rfl
  • flipEdge_sameSkeleton theorem — Reversing a covered edge preserves the skeleton. The undirected adjacency is unchanged: only the orientation of the single edge a — b flips.
    a b :
    V
    hcov :
    G.IsCoveredEdge a b
    Proof (Lean source)
    theorem flipEdge_sameSkeleton {a b : V} (hcov : G.IsCoveredEdge a b) : SameSkeleton G (flipEdge hcov) := by intro u w simp only [DAG.UAdj, flipEdge_edge] constructor · rintro (h | h) · by_cases hab : u = a ∧ w = b · obtain ⟨rfl, rfl⟩ := hab; exact inr (inr ⟨rfl, rfl⟩) · exact inl (inl ⟨h, hab⟩) · by_cases hab : w = a ∧ u = b · obtain ⟨rfl, rfl⟩ := hab; exact inl (inr ⟨rfl, rfl⟩) · exact inr (inl ⟨h, hab⟩) · rintro ((⟨h, _⟩ | ⟨rfl, rfl⟩) | (⟨h, _⟩ | ⟨rfl, rfl⟩)) · exact inl h · exact inr hcov.1 · exact inr h · exact inl hcov.1
    Causalean.DAG.flipEdge_sameSkeleton · Causalean/Graph/MarkovEquiv/CoveredReversal.lean:134
  • flipEdge_sameImmoralities theorem — Reversing a covered edge preserves the immoralities. Because a and b share all other parents, no v-structure is created or destroyed by the single reversal.
    a b :
    V
    hcov :
    G.IsCoveredEdge a b
    Proof (Lean source)
    theorem flipEdge_sameImmoralities {a b : V} (hcov : G.IsCoveredEdge a b) : SameImmoralities G (flipEdge hcov) := by have hU : ∀ x y, G.UAdj x y ↔ (flipEdge hcov).UAdj x y := flipEdge_sameSkeleton hcov intro p q r constructor · rintro ⟨hpq, hrq, hnadj, hpr⟩ have hpq' : (flipEdge hcov).edge p q := by rw [flipEdge_edge]; left; refine ⟨hpq, ?_⟩ rintro ⟨hpa, hqb⟩ -- collider `a → b ← r`: covered forces `r → a`, contradicting non-adjacency rw [hpa] at hpr hnadj; rw [hqb] at hrq exact hnadj (inr ((hcov.2 r (Ne.symm hpr)).mpr hrq)) have hrq' : (flipEdge hcov).edge r q := by rw [flipEdge_edge]; left; refine ⟨hrq, ?_⟩ rintro ⟨hra, hqb⟩ -- collider `p → b ← a`: covered forces `p → a`, contradicting non-adjacency rw [hra] at hpr hnadj; rw [hqb] at hpq exact hnadj (inl ((hcov.2 p hpr).mpr hpq)) exact ⟨hpq', hrq', fun h => hnadj ((hU p r).mpr h), hpr⟩ · rintro ⟨hpq, hrq, hnadj, hpr⟩ rw [flipEdge_edge] at hpq hrq have hnadjG : ¬ G.UAdj p r := fun h => hnadj ((hU p r).mp h) have hpqG : G.edge p q := by rcases hpq with ⟨h, _⟩ | ⟨hpb, hqa⟩ · exact h · exfalso rcases hrq with ⟨hra, _⟩ | ⟨hrb, _⟩ · rw [hqa] at hra have hrne : r ≠ a := fun heq => G.irrefl _ (heq ▸ hra) apply hnadjG; rw [hpb]; exact inr ((hcov.2 r hrne).mp hra) · exact hpr (hpb.trans hrb.symm) have hrqG : G.edge r q := by rcases hrq with ⟨h, _⟩ | ⟨hrb, hqa⟩ · exact h · exfalso rcases hpq with ⟨hpa, _⟩ | ⟨hpb, _⟩ · rw [hqa] at hpa have hpne : p ≠ a := fun heq => G.irrefl _ (heq ▸ hpa) apply hnadjG; rw [hrb]; exact inl ((hcov.2 p hpne).mp hpa) · exact hpr (hpb.trans hrb.symm) exact ⟨hpqG, hrqG, hnadjG, hpr⟩
    Causalean.DAG.flipEdge_sameImmoralities · Causalean/Graph/MarkovEquiv/CoveredReversal.lean:154
  • not_dSep_iff_hasActivePath theorem — Bridge: failure of d-separation is exactly an active path. For pairwise-disjoint query sets, X and Y are *not* d-separated by Z iff there is an active path from X to Y given Z. Assembled from bbReachableVertices_iff_activePath. Reduces the covered-flip invariance to a pure active-path statement.
    H :
    DAG V
    X Y Z :
    hXY :
    hXZ :
    hYZ :
    ¬ H.dSep X Y Z ↔ H.HasActivePath X Y Z
    Proof (Lean source)
    theorem not_dSep_iff_hasActivePath (H : DAG V) (X Y Z : Finset V) (hXY : Disjoint X Y) (hXZ : Disjoint X Z) (hYZ : Disjoint Y Z) : ¬ H.dSep X Y Z ↔ H.HasActivePath X Y Z := by unfold DAG.dSep DAG.HasActivePath constructor · intro hnot have hReach : ¬ Disjoint (H.bbReachableVertices Z X) Y := by intro hReach exact hnot ⟨hXY, hXZ, hYZ, hReach⟩ rw [Finset.not_disjoint_iff] at hReach obtain ⟨v, hvR, hvY⟩ := hReach obtain ⟨x, hxX, p, hlen, hact, hhead, hlast⟩ := (H.bbReachableVertices_iff_activePath X Z v).mp hvR exact ⟨p, hlen, hact, by rw [hhead]; exact Finset.mem_image_of_mem _ hxX, by rw [hlast]; exact Finset.mem_image_of_mem _ hvY⟩ · rintro ⟨p, hlen, hact, hhead, hlast⟩ hsep obtain ⟨x, hxX, hx⟩ := Finset.mem_image.mp hhead obtain ⟨v, hvY, hv⟩ := Finset.mem_image.mp hlast have hvReach : v ∈ H.bbReachableVertices Z X := by rw [H.bbReachableVertices_iff_activePath] exact ⟨x, hxX, p, hlen, hact, by rw [hx], by rw [hv]⟩ exact (Finset.disjoint_left.mp hsep.2.2.2 hvReach) hvY
    Causalean.DAG.not_dSep_iff_hasActivePath · Causalean/Graph/MarkovEquiv/CoveredReversal.lean:198
  • isAncestor_edge_congr theorem — Graphs with the same directed edges have exactly the same ancestor relations.
    G₁ G₂ :
    DAG V
    he :
    ∀ u w : V, G₁.edge u w ↔ G₂.edge u w
    u v :
    V
    G₁.isAncestor u v ↔ G₂.isAncestor u v
    Proof (Lean source)
    theorem isAncestor_edge_congr {G₁ G₂ : DAG V} (he : ∀ u w : V, G₁.edge u w ↔ G₂.edge u w) {u v : V} : G₁.isAncestor u v ↔ G₂.isAncestor u v := by constructor · intro h induction h with | edge h => exact isAncestor.edge ((he _ _).mp h) | trans _ h ih => exact isAncestor.trans ih ((he _ _).mp h) · intro h induction h with | edge h => exact isAncestor.edge ((he _ _).mpr h) | trans _ h ih => exact isAncestor.trans ih ((he _ _).mpr h)
    Causalean.DAG.isAncestor_edge_congr · Causalean/Graph/MarkovEquiv/CoveredReversal.lean:225
  • bbZAncestors_edge_congr theorem — Graphs with the same directed edges have exactly the same sets of vertices that are ancestors of the conditioning set and can activate colliders.
    G₁ G₂ :
    DAG V
    he :
    ∀ u w : V, G₁.edge u w ↔ G₂.edge u w
    Z :
    v :
    V
    v ∈ G₁.bbZAncestors Z ↔ v ∈ G₂.bbZAncestors Z
    Proof (Lean source)
    theorem bbZAncestors_edge_congr {G₁ G₂ : DAG V} (he : ∀ u w : V, G₁.edge u w ↔ G₂.edge u w) (Z : Finset V) (v : V) : v ∈ G₁.bbZAncestors Z ↔ v ∈ G₂.bbZAncestors Z := by simp only [bbZAncestors, ancestralSet, ancestorsSet, mem_union, mem_filter, Finset.mem_univ, true_and] constructor · rintro (hv | ⟨w, hw, h⟩) · exact inl hv · exact inr ⟨w, hw, (isAncestor_edge_congr he).mp h⟩ · rintro (hv | ⟨w, hw, h⟩) · exact inl hv · exact inr ⟨w, hw, (isAncestor_edge_congr he).mpr h⟩
    Causalean.DAG.bbZAncestors_edge_congr · Causalean/Graph/MarkovEquiv/CoveredReversal.lean:239
  • uAdj_edge_congr theorem — Graphs with the same directed edges have exactly the same undirected adjacencies.
    G₁ G₂ :
    DAG V
    he :
    ∀ u w : V, G₁.edge u w ↔ G₂.edge u w
    u v :
    V
    G₁.UAdj u v ↔ G₂.UAdj u v
    Proof (Lean source)
    theorem uAdj_edge_congr {G₁ G₂ : DAG V} (he : ∀ u w : V, G₁.edge u w ↔ G₂.edge u w) {u v : V} : G₁.UAdj u v ↔ G₂.UAdj u v := by unfold UAdj exact or_congr (he u v) (he v u)
  • isCollider_edge_congr theorem — Graphs with the same directed edges have exactly the same collider triples.
    G₁ G₂ :
    DAG V
    he :
    ∀ u w : V, G₁.edge u w ↔ G₂.edge u w
    l m r :
    V
    G₁.IsCollider l m r ↔ G₂.IsCollider l m r
    Proof (Lean source)
    theorem isCollider_edge_congr {G₁ G₂ : DAG V} (he : ∀ u w : V, G₁.edge u w ↔ G₂.edge u w) {l m r : V} : G₁.IsCollider l m r ↔ G₂.IsCollider l m r := by unfold IsCollider exact and_congr (he l m) (he r m)
    Causalean.DAG.isCollider_edge_congr · Causalean/Graph/MarkovEquiv/CoveredReversal.lean:261
  • isActivePath_edge_congr theorem — Graphs with the same directed edges have exactly the same active paths for every conditioning set.
    G₁ G₂ :
    DAG V
    he :
    ∀ u w : V, G₁.edge u w ↔ G₂.edge u w
    Z :
    p :
    G₁.IsActivePath Z p ↔ G₂.IsActivePath Z p
    Proof (Lean source)
    theorem isActivePath_edge_congr {G₁ G₂ : DAG V} (he : ∀ u w : V, G₁.edge u w ↔ G₂.edge u w) (Z : Finset V) (p : List V) : G₁.IsActivePath Z p ↔ G₂.IsActivePath Z p := by constructor · intro h obtain ⟨hadj, htri⟩ := h refine ⟨fun i hi => (uAdj_edge_congr he).mp (hadj i hi), fun 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 hciff : G₁.IsCollider l m r ↔ G₂.IsCollider l m r := isCollider_edge_congr he have haiff : m ∈ G₁.bbZAncestors Z ↔ m ∈ G₂.bbZAncestors Z := bbZAncestors_edge_congr he Z m have hval := htri i hi change (if G₁.IsCollider l m r then m ∈ G₁.bbZAncestors Z else m ∉ Z) at hval change (if G₂.IsCollider l m r then m ∈ G₂.bbZAncestors Z else m ∉ Z) by_cases hC : G₁.IsCollider l m r · rw [if_pos (hciff.mp hC), ← haiff] rwa [if_pos hC] at hval · have hC₂ : ¬ G₂.IsCollider l m r := fun h => hC (hciff.mpr h) rw [if_neg hC₂] rwa [if_neg hC] at hval · intro h obtain ⟨hadj, htri⟩ := h refine ⟨fun i hi => (uAdj_edge_congr he).mpr (hadj i hi), fun 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 hciff : G₁.IsCollider l m r ↔ G₂.IsCollider l m r := isCollider_edge_congr he have haiff : m ∈ G₁.bbZAncestors Z ↔ m ∈ G₂.bbZAncestors Z := bbZAncestors_edge_congr he Z m have hval := htri i hi change (if G₂.IsCollider l m r then m ∈ G₂.bbZAncestors Z else m ∉ Z) at hval change (if G₁.IsCollider l m r then m ∈ G₁.bbZAncestors Z else m ∉ Z) by_cases hC : G₂.IsCollider l m r · rw [if_pos (hciff.mpr hC), haiff] rwa [if_pos hC] at hval · have hC₁ : ¬ G₁.IsCollider l m r := fun h => hC (hciff.mp h) rw [if_neg hC₁] rwa [if_neg hC] at hval
    Causalean.DAG.isActivePath_edge_congr · Causalean/Graph/MarkovEquiv/CoveredReversal.lean:268
  • hasActivePath_edge_congr theorem — Equal directed-edge relations preserve existence of active paths.
    G₁ G₂ :
    DAG V
    he :
    ∀ u w : V, G₁.edge u w ↔ G₂.edge u w
    X Y Z :
    G₁.HasActivePath X Y Z ↔ G₂.HasActivePath X Y Z
    Proof (Lean source)
    theorem hasActivePath_edge_congr {G₁ G₂ : DAG V} (he : ∀ u w : V, G₁.edge u w ↔ G₂.edge u w) (X Y Z : Finset V) : G₁.HasActivePath X Y Z ↔ G₂.HasActivePath X Y Z := by constructor · rintro ⟨p, hlen, hact, hhead, hlast⟩ exact ⟨p, hlen, (isActivePath_edge_congr he Z p).mp hact, hhead, hlast⟩ · rintro ⟨p, hlen, hact, hhead, hlast⟩ exact ⟨p, hlen, (isActivePath_edge_congr he Z p).mpr hact, hhead, hlast⟩
    Causalean.DAG.hasActivePath_edge_congr · Causalean/Graph/MarkovEquiv/CoveredReversal.lean:312
  • flipEdge_isCoveredEdge_back theorem — In the flipped graph, the reversed edge b → a is covered.
    a b :
    V
    hcov :
    G.IsCoveredEdge a b
    Proof (Lean source)
    theorem flipEdge_isCoveredEdge_back {a b : V} (hcov : G.IsCoveredEdge a b) : (flipEdge hcov).IsCoveredEdge b a := by constructor · rw [flipEdge_edge] exact inr ⟨rfl, rfl⟩ · intro c hcb by_cases hca : c = a · subst c rw [flipEdge_edge, flipEdge_edge] constructor · rintro (⟨_, hnot⟩ | ⟨hab, _⟩) · exact absurd ⟨rfl, rfl⟩ hnot · exact absurd hab hcov.ne · rintro (⟨haa, _⟩ | ⟨hab, _⟩) · exact absurd haa (G.irrefl a) · exact absurd hab hcov.ne · rw [flipEdge_edge, flipEdge_edge] constructor · rintro (⟨hcbG, _⟩ | ⟨hcb', _⟩) · left refine ⟨(hcov.2 c hca).mpr hcbG, ?_⟩ rintro ⟨hca', _⟩ exact hca hca' · exact absurd hcb' hcb · rintro (⟨hcaG, _⟩ | ⟨hcb', _⟩) · left refine ⟨(hcov.2 c hca).mp hcaG, ?_⟩ rintro ⟨hca', _⟩ exact hca hca' · exact absurd hcb' hcb
    Causalean.DAG.flipEdge_isCoveredEdge_back · Causalean/Graph/MarkovEquiv/CoveredReversal.lean:322
  • flipEdge_flipEdge_edge theorem — Flipping the reversed covered edge restores the original edge relation.
    a b :
    V
    hcov :
    G.IsCoveredEdge a b
    ∀ u w : V, (flipEdge (flipEdge_isCoveredEdge_back hcov)).edge u w ↔ G.edge u w
    Proof (Lean source)
    theorem flipEdge_flipEdge_edge {a b : V} (hcov : G.IsCoveredEdge a b) : ∀ u w : V, (flipEdge (flipEdge_isCoveredEdge_back hcov)).edge u w ↔ G.edge u w := by intro u w rw [flipEdge_edge, flipEdge_edge] constructor · rintro (h | ⟨hub, hwa⟩) · rcases h with ⟨h, hnot⟩ rcases h with ⟨hG, hnot_ab⟩ | ⟨hub, hwa⟩ · exact hG · exact absurd ⟨hub, hwa⟩ hnot · subst hub; subst hwa exact hcov.1 · intro hG by_cases hab : u = a ∧ w = b · obtain ⟨rfl, rfl⟩ := hab exact inr ⟨rfl, rfl⟩ · left refine ⟨Or.inl ⟨hG, hab⟩, ?_⟩ rintro ⟨hub, hwa⟩ subst hub; subst hwa exact G.asymm hcov.1 hG
    Causalean.DAG.flipEdge_flipEdge_edge · Causalean/Graph/MarkovEquiv/CoveredReversal.lean:354
  • isAncestor_last theorem — If one node is a strict ancestor of another in a directed acyclic graph, then it either has a direct edge to the latter or is a strict ancestor of a node that has a direct edge to the latter.
    H :
    DAG V
    u v :
    V
    h :
    H.isAncestor u v
    H.edge u v ∨ ∃ w, H.isAncestor u w ∧ H.edge w v
    Proof (Lean source)
    theorem isAncestor_last {H : DAG V} {u v : V} (h : H.isAncestor u v) : H.edge u v ∨ ∃ w, H.isAncestor u w ∧ H.edge w v := by induction h with | edge he => exact inl he | trans h₁ he _ => exact inr ⟨_, h₁, he⟩
  • flipEdge_edge_or_deleted theorem — Under a covered reversal of the edge from a to b, every directed edge of the original graph either remains an edge or is the deleted edge from a to b.
    a b u w :
    V
    hcov :
    G.IsCoveredEdge a b
    he :
    G.edge u w
    (flipEdge hcov).edge u w ∨ (u = a ∧ w = b)
    Proof (Lean source)
    theorem flipEdge_edge_or_deleted {a b u w : V} (hcov : G.IsCoveredEdge a b) (he : G.edge u w) : (flipEdge hcov).edge u w ∨ (u = a ∧ w = b) := by by_cases hab : u = a ∧ w = b · exact inr hab · exact inl (by rw [flipEdge_edge] exact inl ⟨he, hab⟩)
    Causalean.DAG.flipEdge_edge_or_deleted · Causalean/Graph/MarkovEquiv/CoveredReversal.lean:410
  • bbZAncestors_flip_of_ne theorem — If a vertex other than the reversed tail belongs to the Bayes-ball ancestor closure of a conditioning set before a covered reversal, it belongs to that closure after the reversal.
    a b v :
    V
    hcov :
    G.IsCoveredEdge a b
    Z :
    hva :
    v ≠ a
    v ∈ G.bbZAncestors Z → v ∈ (flipEdge hcov).bbZAncestors Z
    Proof (Lean source)
    theorem bbZAncestors_flip_of_ne {a b v : V} (hcov : G.IsCoveredEdge a b) (Z : Finset V) (hva : v ≠ a) : v ∈ G.bbZAncestors Z → v ∈ (flipEdge hcov).bbZAncestors Z := by simp only [bbZAncestors, ancestralSet, ancestorsSet, mem_union, mem_filter, Finset.mem_univ, true_and] rintro (hvZ | ⟨z, hzZ, hvz⟩) · exact inl hvZ · exact inr ⟨z, hzZ, isAncestor_flip hcov hva hvz⟩
    Causalean.DAG.bbZAncestors_flip_of_ne · Causalean/Graph/MarkovEquiv/CoveredReversal.lean:445
  • isActivePath_flip_of_not_mem theorem — A path that is active relative to a conditioning set and does not contain the tail of a covered reversed edge remains active relative to the same set after the reversal.
    a b :
    V
    hcov :
    G.IsCoveredEdge a b
    Z :
    p :
    hact :
    G.IsActivePath Z p
    hna :
    a ∉ p
    Proof (Lean source)
    theorem isActivePath_flip_of_not_mem {a b : V} (hcov : G.IsCoveredEdge a b) {Z : Finset V} {p : List V} (hact : G.IsActivePath Z p) (hna : a ∉ p) : (flipEdge hcov).IsActivePath Z p := by obtain ⟨hadj, htri⟩ := hact refine ⟨fun i hi => ?_, fun i hi => ?_⟩ · exact (flipEdge_sameSkeleton hcov _ _).mp (hadj 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 hla : l ≠ a := by intro h apply hna rw [← h] exact List.get_mem p ⟨i, by omega⟩ have hma : m ≠ a := by intro h apply hna rw [← h] exact List.get_mem p ⟨i + 1, by omega⟩ have hra : r ≠ a := by intro h apply hna rw [← h] exact List.get_mem p ⟨i + 2, hi⟩ have hciff : (flipEdge hcov).IsCollider l m r ↔ G.IsCollider l m r := flipEdge_isCollider_iff_of_ne_a hcov hla hma hra have hval := htri i hi change (if G.IsCollider l m r then m ∈ G.bbZAncestors Z else m ∉ Z) at hval change (if (flipEdge hcov).IsCollider l m r then m ∈ (flipEdge hcov).bbZAncestors Z else m ∉ Z) by_cases hC : G.IsCollider l m r · rw [if_pos ((hciff).mpr hC)] exact bbZAncestors_flip_of_ne hcov Z hma (by rwa [if_pos hC] at hval) · have hCF : ¬ (flipEdge hcov).IsCollider l m r := fun h => hC ((hciff).mp h) rw [if_neg hCF] rwa [if_neg hC] at hval
    Causalean.DAG.isActivePath_flip_of_not_mem · Causalean/Graph/MarkovEquiv/CoveredReversal.lean:526
Decompose 4 core · 7 supporting This file assembles the covered-edge route to the hard direction of Verma–Pearl: DAGs with the same skeleton and the same immoralities are Markov equivalent. ★ exists_covered_reversed_edge★ markovEquiv_of_sameSkeleton_sameImmoralities

Covered-edge decomposition (AMP Lemma 3.2) + Verma–Pearl hard direction

This file assembles the covered-edge route to the hard direction of Verma–Pearl: DAGs with the same skeleton and the same immoralities are Markov equivalent. Following Andersson–Madigan–Perlman (1997) Lemma 3.2, two such DAGs are connected by a finite chain of single covered-edge reversals; markovEquiv_flipEdge (CoveredReversal.lean) handles each step, and MarkovEquiv.trans chains them. The induction is on the number of oppositely oriented edges (edgeDiffCount).

def edgeDiff reviewed
Causalean

The directed edges of G₁ that are absent from G₂.

Definition (Lean source)
def edgeDiff (G₁ G₂ : DAG V) : Finset (V × V) := Finset.univ.filter (fun p => G₁.edge p.1 p.2 ∧ ¬ G₂.edge p.1 p.2)
def edgeDiffCount reviewed
Causalean

Number of directed edges of G₁ absent from G₂.

Definition (Lean source)
def edgeDiffCount (G₁ G₂ : DAG V) : ℕ := (edgeDiff G₁ G₂).card
theorem exists_covered_reversed_edge reviewed
Causalean

AMP Lemma 3.2 (existence). For DAGs G₁ and G₂ on the same vertex set, suppose G₁ and G₂ have the same skeleton (the same underlying undirected adjacency), the same immoralities (unshielded colliders), and there is a pair a₀, b₀ with an edge a₀ → b₀ in G₁ that appears reversed as b₀ → a₀ in G₂. Then there is a covered edge a → b in G₁ that is likewise reversed to b → a in G₂. The proof chooses a head of a reversed edge that is minimal in the first graph's topological order, then a tail into that head that is maximal among reversed tails; the skeleton and immorality hypotheses force this edge to have the same non-tail parents at both endpoints.

Formal statement
G₁ G₂ :
DAG V
hskel :
SameSkeleton G₁ G₂
himm :
a₀ b₀ :
V
h₀ :
G₁.edge a₀ b₀
h₀' :
G₂.edge b₀ a₀
∃ a b, G₁.edge a b ∧ G₂.edge b a ∧ G₁.IsCoveredEdge a b
Proof (Lean source)
theorem exists_covered_reversed_edge {G₁ G₂ : DAG V} (hskel : SameSkeleton G₁ G₂) (himm : SameImmoralities G₁ G₂) {a₀ b₀ : V} (h₀ : G₁.edge a₀ b₀) (h₀' : G₂.edge b₀ a₀) : ∃ a b, G₁.edge a b ∧ G₂.edge b a ∧ G₁.IsCoveredEdge a b := by let Heads : Finset V := Finset.univ.filter (fun y => ∃ x, G₁.edge x y ∧ G₂.edge y x) have hHead0 : b₀ ∈ Heads := by change b₀ ∈ Finset.univ.filter (fun y => ∃ x, G₁.edge x y ∧ G₂.edge y x) exact Finset.mem_filter.mpr ⟨Finset.mem_univ _, ⟨a₀, h₀, h₀'⟩⟩ have hHeads_ne : Heads.Nonempty := ⟨b₀, hHead0⟩ obtain ⟨b, hbHead, hbmin⟩ := Finset.exists_min_image Heads G₁.topoOrder hHeads_ne have hbWitness : ∃ x, G₁.edge x b ∧ G₂.edge b x := by have hbHead' : b ∈ Finset.univ.filter (fun y => ∃ x, G₁.edge x y ∧ G₂.edge y x) := by simpa [Heads] using hbHead exact (Finset.mem_filter.mp hbHead').2 obtain ⟨x, hxb₁, hbx₂⟩ := hbWitness let Tails : Finset V := Finset.univ.filter (fun x => G₁.edge x b ∧ G₂.edge b x) have hxTail : x ∈ Tails := by change x ∈ Finset.univ.filter (fun x => G₁.edge x b ∧ G₂.edge b x) exact Finset.mem_filter.mpr ⟨Finset.mem_univ _, hxb₁, hbx₂⟩ have hTails_ne : Tails.Nonempty := ⟨x, hxTail⟩ obtain ⟨a, haTail, hamax⟩ := Finset.exists_max_image Tails G₁.topoOrder hTails_ne have haRev : G₁.edge a b ∧ G₂.edge b a := by have haTail' : a ∈ Finset.univ.filter (fun x => G₁.edge x b ∧ G₂.edge b x) := by simpa [Tails] using haTail exact (Finset.mem_filter.mp haTail').2 refine ⟨a, b, haRev.1, haRev.2, haRev.1, ?_⟩ intro c hca_ne by_contra hiff by_cases hca : G₁.edge c a · by_cases hcb : G₁.edge c b · exact hiff ⟨fun _ => hcb, fun _ => hca⟩ · have hnotG1Ucb : ¬ G₁.UAdj c b := by intro hU rcases hU with hcb' | hbc · exact hcb hcb' · have hcbTop : G₁.topoOrder c < G₁.topoOrder b := lt_trans (G₁.topoOrder_lt c a hca) (G₁.topoOrder_lt a b haRev.1) exact (Nat.lt_irrefl _) (lt_trans hcbTop (G₁.topoOrder_lt b c hbc)) have hnotG2Ucb : ¬ G₂.UAdj c b := by intro hU exact hnotG1Ucb ((hskel c b).mpr hU) have hcb_ne : c ≠ b := by intro heq subst c exact (G₁.asymm haRev.1) hca have hnotG2ca : ¬ G₂.edge c a := by intro hca₂ have him₂ : G₂.IsImmorality c a b := ⟨hca₂, haRev.2, hnotG2Ucb, hcb_ne⟩ have him₁ : G₁.IsImmorality c a b := (himm c a b).mpr him₂ exact (G₁.asymm haRev.1) him₁.2.1 have hU2ca : G₂.UAdj c a := (hskel c a).mp (Or.inl hca) have hac₂ : G₂.edge a c := hU2ca.resolve_left hnotG2ca have haHead : a ∈ Heads := by change a ∈ Finset.univ.filter (fun y => ∃ x, G₁.edge x y ∧ G₂.edge y x) exact Finset.mem_filter.mpr ⟨Finset.mem_univ _, ⟨c, hca, hac₂⟩⟩ have hle : G₁.topoOrder b ≤ G₁.topoOrder a := hbmin a haHead exact (not_lt_of_ge hle) (G₁.topoOrder_lt a b haRev.1) · by_cases hcb : G₁.edge c b · have hac_ne : a ≠ c := fun heq => hca_ne heq.symm have hUac : G₁.UAdj a c := by by_contra hnotU have him₁ : G₁.IsImmorality a b c := ⟨haRev.1, hcb, hnotU, hac_ne⟩ have him₂ : G₂.IsImmorality a b c := (himm a b c).mp him₁ exact (G₂.asymm haRev.2) him₂.1 have hac : G₁.edge a c := hUac.resolve_right hca have hrevNext : G₂.edge b c ∨ G₂.edge c a := by by_cases hbc₂ : G₂.edge b c · exact Or.inl hbc₂ · by_cases hca₂ : G₂.edge c a · exact Or.inr hca₂ · have hU2cb : G₂.UAdj c b := (hskel c b).mp (Or.inl hcb) have hcb₂ : G₂.edge c b := hU2cb.resolve_right hbc₂ have hU2ac : G₂.UAdj a c := (hskel a c).mp (Or.inl hac) have hac₂ : G₂.edge a c := hU2ac.resolve_right hca₂ have hcycle : G₂.topoOrder b < G₂.topoOrder b := lt_trans (G₂.topoOrder_lt b a haRev.2) (lt_trans (G₂.topoOrder_lt a c hac₂) (G₂.topoOrder_lt c b hcb₂)) exact elim ((Nat.lt_irrefl _) hcycle) rcases hrevNext with hbc₂ | hca₂ · have hcTail : c ∈ Tails := by change c ∈ Finset.univ.filter (fun x => G₁.edge x b ∧ G₂.edge b x) exact Finset.mem_filter.mpr ⟨Finset.mem_univ _, hcb, hbc₂⟩ have hle : G₁.topoOrder c ≤ G₁.topoOrder a := hamax c hcTail exact (not_lt_of_ge hle) (G₁.topoOrder_lt a c hac) · have hcHead : c ∈ Heads := by change c ∈ Finset.univ.filter (fun y => ∃ x, G₁.edge x y ∧ G₂.edge y x) exact Finset.mem_filter.mpr ⟨Finset.mem_univ _, ⟨a, hac, hca₂⟩⟩ have hle : G₁.topoOrder b ≤ G₁.topoOrder c := hbmin c hcHead exact (not_lt_of_ge hle) (G₁.topoOrder_lt c b hcb) · exact hiff ⟨fun h => elim (hca h), fun h => elim (hcb h)⟩
theorem markovEquiv_of_sameSkeleton_sameImmoralities reviewed
Causalean

Verma–Pearl hard direction (covered-edge route). For DAGs G₁ and G₂ on the same vertex set, if G₁ and G₂ have the same skeleton and the same immoralities, then G₁ and G₂ are Markov equivalent: they license exactly the same d-separation statements — proven via AMP Lemma 3.2 (covered-edge reversals), independent of the moralization/ancestral kernel.

Formal statement
G₁ G₂ :
DAG V
hskel :
SameSkeleton G₁ G₂
himm :
MarkovEquiv G₁ G₂
Proof (Lean source)
theorem markovEquiv_of_sameSkeleton_sameImmoralities {G₁ G₂ : DAG V} (hskel : SameSkeleton G₁ G₂) (himm : SameImmoralities G₁ G₂) : MarkovEquiv G₁ G₂ := markovEquiv_covered_aux G₂ (edgeDiffCount G₁ G₂) G₁ hskel himm rfl
Causalean.markovEquiv_of_sameSkeleton_sameImmoralities · Causalean/Graph/MarkovEquiv/Decompose.lean:228 · uses DAG , MarkovEquiv , SameImmoralities , SameSkeleton
7 supporting declarations (lemmas, instances)
  • symm theorem — SameSkeleton is symmetric.
    G₁ G₂ :
    DAG V
    h :
    SameSkeleton G₁ G₂
    SameSkeleton G₂ G₁
    Proof (Lean source)
    theorem SameSkeleton.symm {G₁ G₂ : DAG V} (h : SameSkeleton G₁ G₂) : SameSkeleton G₂ G₁ := fun a b => (h a b).symm
  • trans theorem — SameSkeleton is transitive.
    G₁ G₂ G₃ :
    DAG V
    h₁ :
    SameSkeleton G₁ G₂
    h₂ :
    SameSkeleton G₂ G₃
    SameSkeleton G₁ G₃
    Proof (Lean source)
    theorem SameSkeleton.trans {G₁ G₂ G₃ : DAG V} (h₁ : SameSkeleton G₁ G₂) (h₂ : SameSkeleton G₂ G₃) : SameSkeleton G₁ G₃ := fun a b => (h₁ a b).trans (h₂ a b)
    Causalean.SameSkeleton.trans · Causalean/Graph/MarkovEquiv/Decompose.lean:27
  • symm theorem — SameImmoralities is symmetric.
    G₁ G₂ :
    DAG V
    h :
    Proof (Lean source)
    theorem SameImmoralities.symm {G₁ G₂ : DAG V} (h : SameImmoralities G₁ G₂) : SameImmoralities G₂ G₁ := fun a b c => (h a b c).symm
    Causalean.SameImmoralities.symm · Causalean/Graph/MarkovEquiv/Decompose.lean:31
  • trans theorem — SameImmoralities is transitive.
    G₁ G₂ G₃ :
    DAG V
    h₁ :
    h₂ :
    Proof (Lean source)
    theorem SameImmoralities.trans {G₁ G₂ G₃ : DAG V} (h₁ : SameImmoralities G₁ G₂) (h₂ : SameImmoralities G₂ G₃) : SameImmoralities G₁ G₃ := fun a b c => (h₁ a b c).trans (h₂ a b c)
    Causalean.SameImmoralities.trans · Causalean/Graph/MarkovEquiv/Decompose.lean:35
  • markovEquiv_of_same_edge theorem — Edge-congruence for Markov equivalence. DAGs with the same directed-edge relation are Markov equivalent (d-separation depends only on the edge relation).
    G₁ G₂ :
    DAG V
    he :
    ∀ u w, G₁.edge u w ↔ G₂.edge u w
    MarkovEquiv G₁ G₂
    Proof (Lean source)
    theorem markovEquiv_of_same_edge {G₁ G₂ : DAG V} (he : ∀ u w, G₁.edge u w ↔ G₂.edge u w) : MarkovEquiv G₁ G₂ := by intro X Y Z by_cases hXY : Disjoint X Y · by_cases hXZ : Disjoint X Z · by_cases hYZ : Disjoint Y Z · have h := DAG.hasActivePath_edge_congr he X Y Z rw [← DAG.not_dSep_iff_hasActivePath G₁ X Y Z hXY hXZ hYZ, ← DAG.not_dSep_iff_hasActivePath G₂ X Y Z hXY hXZ hYZ] at h exact not_iff_not.mp h · exact iff_of_false (fun h => hYZ h.2.2.1) (fun h => hYZ h.2.2.1) · exact iff_of_false (fun h => hXZ h.2.1) (fun h => hXZ h.2.1) · exact iff_of_false (fun h => hXY h.1) (fun h => hXY h.1)
    Causalean.markovEquiv_of_same_edge · Causalean/Graph/MarkovEquiv/Decompose.lean:40
  • same_edge_of_edgeDiff_empty theorem — With a one-way skeleton inclusion, an empty edge-difference forces equal edge relations.
    G₁ G₂ :
    DAG V
    hskel :
    ∀ u w
    if
    G₂.UAdj u w
    then
    G₁.UAdj u w
    h :
    edgeDiff G₁ G₂ = ∅
    ∀ u w, G₁.edge u w ↔ G₂.edge u w
    Proof (Lean source)
    theorem same_edge_of_edgeDiff_empty {G₁ G₂ : DAG V} (hskel : ∀ u w, G₂.UAdj u w → G₁.UAdj u w) (h : edgeDiff G₁ G₂ = ∅) : ∀ u w, G₁.edge u w ↔ G₂.edge u w := by have hsub : ∀ u w, G₁.edge u w → G₂.edge u w := by intro u w he by_contra hne have hmem : (u, w) ∈ edgeDiff G₁ G₂ := Finset.mem_filter.mpr ⟨Finset.mem_univ _, he, hne⟩ rw [h] at hmem; simp at hmem intro u w refine ⟨hsub u w, fun he2 => ?_⟩ rcases hskel u w (inl he2) with h1 | h1 · exact h1 · exact absurd (hsub w u h1) (G₂.asymm he2)
    Causalean.same_edge_of_edgeDiff_empty · Causalean/Graph/MarkovEquiv/Decompose.lean:63
  • edgeDiffCount_flipEdge_lt theorem — Flipping a covered, oppositely oriented edge strictly decreases the edge-difference count (it fixes exactly the pair (a,b) and changes nothing else).
    G₁ G₂ :
    DAG V
    a b :
    V
    hcov :
    G₁.IsCoveredEdge a b
    hba :
    G₂.edge b a
    edgeDiffCount (DAG.flipEdge hcov) G₂ < edgeDiffCount G₁ G₂
    Proof (Lean source)
    theorem edgeDiffCount_flipEdge_lt {G₁ G₂ : DAG V} {a b : V} (hcov : G₁.IsCoveredEdge a b) (hba : G₂.edge b a) : edgeDiffCount (DAG.flipEdge hcov) G₂ < edgeDiffCount G₁ G₂ := by have hsub : edgeDiff (DAG.flipEdge hcov) G₂ ⊆ edgeDiff G₁ G₂ := by intro p hp rw [edgeDiff, mem_filter] at hp ⊢ obtain ⟨_, hedge, hnot⟩ := hp rw [DAG.flipEdge_edge] at hedge rcases hedge with ⟨h1, _⟩ | ⟨hb, ha⟩ · exact ⟨Finset.mem_univ _, h1, hnot⟩ · exact absurd (by rw [hb, ha]; exact hba) hnot unfold edgeDiffCount apply Finset.card_lt_card rw [Finset.ssubset_iff_of_subset hsub] refine ⟨(a, b), ?_, ?_⟩ · rw [edgeDiff, mem_filter] exact ⟨Finset.mem_univ _, hcov.1, G₂.asymm hba⟩ · rw [edgeDiff, mem_filter] rintro ⟨_, hedge, _⟩ rw [DAG.flipEdge_edge] at hedge rcases hedge with ⟨_, hnotab⟩ | ⟨hb, _⟩ · exact hnotab ⟨rfl, rfl⟩ · exact hcov.ne hb
    Causalean.edgeDiffCount_flipEdge_lt · Causalean/Graph/MarkovEquiv/Decompose.lean:178
Distributional 5 core · 0 supporting The graph-level MarkovEquiv (same d-separations) is connected here to *distributions*. ★ IsGlobalIMap★ DistMarkovEquiv★ isGlobalIMap_dag_self★ distMarkovEquiv_of_markovEquiv

Markov equivalence — the distributional I-map layer

The graph-level MarkovEquiv (same d-separations) is connected here to distributions. A distribution μ is a global I-map of a DAG G when every d-separation of G is a conditional independence of μ; it is faithful to G when, conversely, every such conditional independence reflects an actual d-separation. The global Markov property (full_globalMarkov) says every structural causal model is a global I-map of its own DAG — this is the bridge from graphs to distributions, restated here as isGlobalIMap_dag_self.

The theorem in this file proves the easy direction:

* distMarkovEquiv_of_markovEquiv: graph-level Markov equivalence implies distributional Markov equivalence (the two DAGs are I-maps of exactly the same distributions). This is immediate from the definitions: the I-map condition is the same predicate when the d-separations agree.

The d-separation triples handled here are pairwise disjoint, matching the global Markov property; this is the standard setting for the I-map and faithfulness notions. The file states the I-map direction needed by the public Markov-equivalence API and leaves faithfulness-existence results outside this layer.

def IsGlobalIMap reviewed
Causalean.SCM

A measure μ on the random values of M is a global I-map of a DAG G (on the same node set) when every d-separation in G is a conditional independence under μ. The required pairwise disjointness is already part of d-separation.

Definition (Lean source)
def IsGlobalIMap (G : DAG (SWIGNode N)) (M : SCM N Ω) [StandardBorelSpace M.RandomValues] (μ : Measure M.RandomValues) [IsFiniteMeasure μ] : Prop := ∀ (X Y Z : Finset (SWIGNode N)) (hX : X ⊆ M.randomVars) (hY : Y ⊆ M.randomVars) (hZ : Z ⊆ M.randomVars), G.dSep X Y Z → FullCondIndep M X Y Z hX hY hZ μ
def IsFaithful reviewed
Causalean.SCM

A measure μ is faithful to a DAG G when every conditional independence of μ reflects a genuine d-separation of G (the converse of being an I-map): for pairwise-disjoint X, Y, Z, X ⟂ Y | Z under μ forces G to d-separate X and Y given Z. A measure that is both an I-map of and faithful to G has conditional independences exactly matching G's d-separations.

Definition (Lean source)
def IsFaithful (G : DAG (SWIGNode N)) (M : SCM N Ω) [StandardBorelSpace M.RandomValues] (μ : Measure M.RandomValues) [IsFiniteMeasure μ] : Prop := ∀ (X Y Z : Finset (SWIGNode N)) (hX : X ⊆ M.randomVars) (hY : Y ⊆ M.randomVars) (hZ : Z ⊆ M.randomVars), Disjoint X Y → Disjoint X Z → Disjoint Y Z → FullCondIndep M X Y Z hX hY hZ μ → G.dSep X Y Z
def DistMarkovEquiv reviewed
Causalean.SCM

Two DAGs are distributionally Markov equivalent (over value spaces Ω) when they are global I-maps of exactly the same distributions over every structural causal model on the same node set. The value-space family Ω is an explicit parameter since it is not determined by the graphs.

Definition (Lean source)
def DistMarkovEquiv (Ω : N → Type uΩ) [∀ n, MeasurableSpace (Ω n)] (G₁ G₂ : DAG (SWIGNode N)) : Prop := ∀ (M : SCM N Ω) [StandardBorelSpace M.RandomValues] (μ : Measure M.RandomValues) [IsFiniteMeasure μ], IsGlobalIMap G₁ M μ ↔ IsGlobalIMap G₂ M μ
theorem isGlobalIMap_dag_self reviewed
Causalean.SCM

The bridge, restated. For any structural causal model M and any point s of its fixed values, the joint distribution of M's random values under s is a global I-map of M's own DAG — this is exactly the global Markov property full_globalMarkov.

Formal statement
M :
SCM N Ω
StandardBorelSpace M.RandomValues
∀ n, Nonempty (swigΩ Ω n)
∀ s :
M.FixedValues, IsFiniteMeasure (M.jointKernel s)
s :
M.FixedValues
IsGlobalIMap M.dag M (M.jointKernel s)
Proof (Lean source)
theorem isGlobalIMap_dag_self (M : SCM N Ω) [StandardBorelSpace M.RandomValues] [∀ n, StandardBorelSpace (swigΩ Ω n)] [∀ n, Nonempty (swigΩ Ω n)] [∀ s : M.FixedValues, IsFiniteMeasure (M.jointKernel s)] (s : M.FixedValues) : IsGlobalIMap M.dag M (M.jointKernel s) := by intro X Y Z hX hY hZ hdsep exact full_globalMarkov M X Y Z hX hY hZ hdsep s
theorem distMarkovEquiv_of_markovEquiv reviewed
Causalean.SCM

Easy half. For two DAGs G₁, G₂ on the same node set, if they are Markov equivalent — they declare exactly the same d-separations, then they are distributionally Markov equivalent: a distribution is a global I-map of one exactly when it is a global I-map of the other.

Formal statement
G₁ G₂ :
h :
MarkovEquiv G₁ G₂
DistMarkovEquiv Ω G₁ G₂
Proof (Lean source)
theorem distMarkovEquiv_of_markovEquiv {G₁ G₂ : DAG (SWIGNode N)} (h : MarkovEquiv G₁ G₂) : DistMarkovEquiv Ω G₁ G₂ := by intro M _ μ _ constructor · intro himap X Y Z hX hY hZ hdsep exact himap X Y Z hX hY hZ ((h X Y Z).mpr hdsep) · intro himap X Y Z hX hY hZ hdsep exact himap X Y Z hX hY hZ ((h X Y Z).mp hdsep)
Moralization 5 core · 13 supporting This file develops the moral graph of a DAG and the classical *moralization criterion* for d-separation. ★ dSep_iff_moralSep

Markov equivalence — the moralization criterion

This file develops the moral graph of a DAG and the classical moralization criterion for d-separation. It proves that, for pairwise-disjoint source, target, and conditioning sets, d-separation is equivalent to separation in the moral graph of the ancestral set.

* DAG.MoralAdj G S u vu and v are adjacent in the moral graph restricted to a ground set S: they are skeleton-adjacent (UAdj) or "married", i.e. they have a common child inside S. * DAG.MoralConn G S Z u vu reaches v by a moral path inside S all of whose vertices avoid Z. * DAG.MoralSep G X Y Z — no x ∈ X is moral-connected to any y ∈ Y inside the ancestral set An(X ∪ Y ∪ Z) while avoiding Z. * DAG.dSep_iff_moralSepthe criterion: for pairwise-disjoint sets, d-separation is exactly moral separation in the ancestral set.

The file also proves that moral adjacency, moral steps, and moral connectivity are invariants of a graph's skeleton together with its v-structures, for a fixed ground set. The main Verma–Pearl hard direction used by the public umbrella theorem is assembled through the covered-edge route in Transfer.lean and Decompose.lean.

def MoralAdj reviewed
Causalean.DAG

Moral adjacency within a ground set S: distinct vertices u, v ∈ S that are either skeleton-adjacent or share a common child inside S ("married parents"). This is the undirected edge relation of the moral graph of G restricted to S.

Definition (Lean source)
def MoralAdj (S : Finset V) (u v : V) : Prop := u ≠ v ∧ u ∈ S ∧ v ∈ S ∧ (G.UAdj u v ∨ ∃ c ∈ S, G.edge u c ∧ G.edge v c)
def MoralStep reviewed
Causalean.DAG

A single moral step inside S that avoids the conditioning set Z (both endpoints outside Z).

Definition (Lean source)
def MoralStep (S Z : Finset V) (u v : V) : Prop := G.MoralAdj S u v ∧ u ∉ Z ∧ v ∉ Z
def MoralConn reviewed
Causalean.DAG

Moral connectivity: u reaches v by a (possibly empty) sequence of moral steps inside S, every vertex of which avoids Z.

Definition (Lean source)
def MoralConn (S Z : Finset V) (u v : V) : Prop := Relation.ReflTransGen (G.MoralStep S Z) u v
def MoralSep reviewed
Causalean.DAG

Moral separation: no vertex of X is moral-connected to a vertex of Y inside the ancestral set An(X ∪ Y ∪ Z) while avoiding Z.

Definition (Lean source)
def MoralSep (X Y Z : Finset V) : Prop := ∀ x ∈ X, ∀ y ∈ Y, ¬ G.MoralConn (G.ancestralSet (X ∪ Y ∪ Z)) Z x y
theorem dSep_iff_moralSep reviewed
Causalean.DAG

The moralization criterion. For pairwise-disjoint X, Y, Z, X and Y are d-separated by Z exactly when they are moral-separated: no moral path inside the ancestral set An(X ∪ Y ∪ Z) connects them while avoiding Z. (Lauritzen–Dawid–Larsen–Speed.)

Formal statement
X Y Z :
hXY :
hXZ :
hYZ :
G.dSep X Y Z ↔ G.MoralSep X Y Z
Proof (Lean source)
theorem dSep_iff_moralSep {X Y Z : Finset V} (hXY : Disjoint X Y) (hXZ : Disjoint X Z) (hYZ : Disjoint Y Z) : G.dSep X Y Z ↔ G.MoralSep X Y Z := by constructor · -- `dSepMoralSep` via Direction 2: a moral connection would build a d-connection. intro hdSep x hxX y hyY hconn have hxZ : x ∉ Z := Finset.disjoint_left.mp hXZ hxX obtain ⟨w, hwY, hwReach⟩ := G.dconn_of_moralConn hXY hconn (inl hxX) hxZ hyY exact (Finset.disjoint_left.mp hdSep.2.2.2 hwReach) hwY · -- `MoralSepdSep` via the contrapositive Direction 1. intro hsep by_contra hdSep exact G.not_moralSep_of_not_dSep hXY hXZ hYZ hdSep hsep
13 supporting declarations (lemmas, instances)
  • moralAdj_symm theorem — Moral adjacency is symmetric: an undirected moral edge from u to v is also one from v to u.
    S :
    u v :
    V
    h :
    G.MoralAdj S u v
    G.MoralAdj S v u
    Proof (Lean source)
    theorem moralAdj_symm {S : Finset V} {u v : V} (h : G.MoralAdj S u v) : G.MoralAdj S v u := by obtain ⟨hne, hu, hv, hdisj⟩ := h refine ⟨hne.symm, hv, hu, ?_⟩ rcases hdisj with hadj | ⟨c, hc, huc, hvc⟩ · exact inl (G.UAdj_symm hadj) · exact inr ⟨c, hc, hvc, huc⟩
  • moralConn_of_step theorem — A single moral step yields moral connectivity.
    S Z :
    u v :
    V
    h :
    G.MoralStep S Z u v
    G.MoralConn S Z u v
    Proof (Lean source)
    theorem moralConn_of_step {S Z : Finset V} {u v : V} (h : G.MoralStep S Z u v) : G.MoralConn S Z u v := Relation.ReflTransGen.single h
    Causalean.DAG.moralConn_of_step · Causalean/Graph/MarkovEquiv/Moralization.lean:75
  • moralConn_trans theorem — Moral connectivity is transitive.
    S Z :
    u v w :
    V
    h₁ :
    G.MoralConn S Z u v
    h₂ :
    G.MoralConn S Z v w
    G.MoralConn S Z u w
    Proof (Lean source)
    theorem moralConn_trans {S Z : Finset V} {u v w : V} (h₁ : G.MoralConn S Z u v) (h₂ : G.MoralConn S Z v w) : G.MoralConn S Z u w := Relation.ReflTransGen.trans h₁ h₂
  • moralStep_of_uAdj theorem — A skeleton edge between two non-Z vertices of S is a moral step.
    S Z :
    u v :
    V
    hne :
    u ≠ v
    hu :
    u ∈ S
    hv :
    v ∈ S
    hadj :
    G.UAdj u v
    huZ :
    u ∉ Z
    hvZ :
    v ∉ Z
    G.MoralStep S Z u v
    Proof (Lean source)
    theorem moralStep_of_uAdj {S Z : Finset V} {u v : V} (hne : u ≠ v) (hu : u ∈ S) (hv : v ∈ S) (hadj : G.UAdj u v) (huZ : u ∉ Z) (hvZ : v ∉ Z) : G.MoralStep S Z u v := ⟨⟨hne, hu, hv, inl hadj⟩, huZ, hvZ⟩
    Causalean.DAG.moralStep_of_uAdj · Causalean/Graph/MarkovEquiv/Moralization.lean:85
  • moralStep_of_married theorem — A married pair (common child c ∈ S) of distinct non-Z vertices of S is a moral step.
    S Z :
    u v c :
    V
    hne :
    u ≠ v
    hu :
    u ∈ S
    hv :
    v ∈ S
    hc :
    c ∈ S
    huc :
    G.edge u c
    hvc :
    G.edge v c
    huZ :
    u ∉ Z
    hvZ :
    v ∉ Z
    G.MoralStep S Z u v
    Proof (Lean source)
    theorem moralStep_of_married {S Z : Finset V} {u v c : V} (hne : u ≠ v) (hu : u ∈ S) (hv : v ∈ S) (hc : c ∈ S) (huc : G.edge u c) (hvc : G.edge v c) (huZ : u ∉ Z) (hvZ : v ∉ Z) : G.MoralStep S Z u v := ⟨⟨hne, hu, hv, inr ⟨c, hc, huc, hvc⟩⟩, huZ, hvZ⟩
    Causalean.DAG.moralStep_of_married · Causalean/Graph/MarkovEquiv/Moralization.lean:91
  • activePath_head_uAdj theorem — In an active path whose first two vertices are a and b, those vertices are adjacent in the underlying undirected graph.
    Z :
    a b :
    V
    r :
    h :
    G.IsActivePath Z (a :: b :: r)
    G.UAdj a b
    Proof (Lean source)
    theorem activePath_head_uAdj {Z : Finset V} {a b : V} {r : List V} (h : G.IsActivePath Z (a :: b :: r)) : G.UAdj a b := by have := h.1 0 (by simp) simpa using this
    Causalean.DAG.activePath_head_uAdj · Causalean/Graph/MarkovEquiv/Moralization.lean:103
  • activePath_head_triple theorem — In an active path whose first three vertices are a, b, and c, the middle vertex obeys the active-path condition: a collider belongs to the ancestral closure of the conditioning set, and a non-collider is not conditioned on.
    Z :
    a b c :
    V
    r :
    h :
    G.IsActivePath Z (a :: b :: c :: r)
    if G.IsCollider a b c then b ∈ G.bbZAncestors Z else b ∉ Z
    Proof (Lean source)
    theorem activePath_head_triple {Z : Finset V} {a b c : V} {r : List V} (h : G.IsActivePath Z (a :: b :: c :: r)) : if G.IsCollider a b c then b ∈ G.bbZAncestors Z else b ∉ Z := by have := h.2 0 (by simp) simpa using this
    Causalean.DAG.activePath_head_triple · Causalean/Graph/MarkovEquiv/Moralization.lean:110
  • activePath_drop2 theorem — Removing the first two vertices of an active path with at least three vertices leaves an active path.
    Z :
    a b c :
    V
    r :
    h :
    G.IsActivePath Z (a :: b :: c :: r)
    G.IsActivePath Z (c :: r)
    Proof (Lean source)
    theorem activePath_drop2 {Z : Finset V} {a b c : V} {r : List V} (h : G.IsActivePath Z (a :: b :: c :: r)) : G.IsActivePath Z (c :: r) := G.isActivePath_cons_tail (G.isActivePath_cons_tail h)
    Causalean.DAG.activePath_drop2 · Causalean/Graph/MarkovEquiv/Moralization.lean:119
  • moralConn_of_activePath theorem — An active path inside a ground set, with endpoints outside the conditioning set, induces a connection in the corresponding moral graph.
    S Z :
    ∀ (n : ℕ) {x y : V} {p : List V}
    if
    p.length ≤ n
    and
    G.IsActivePath Z p
    and
    p.length ≥ 2
    and
    (∀ v ∈ p, v ∈ S)
    and
    p.head? = some x
    and
    p.getLast? = some y
    and
    x ∉ Z
    and
    y ∉ Z
    then
    G.MoralConn S Z x y
    Proof (Lean source)
    theorem moralConn_of_activePath {S Z : Finset V} : ∀ (n : ℕ) {x y : V} {p : List V}, p.length ≤ n → G.IsActivePath Z p → p.length ≥ 2 → (∀ v ∈ p, v ∈ S) → p.head? = some x → p.getLast? = some y → x ∉ Z → y ∉ Z → G.MoralConn S Z x y := by intro n induction n with | zero => intro x y p hn _ hlen _ _ _ _ _; omega | succ n ih => intro x y p hn hp hlen hnodes hhead hlast hxZ hyZ -- `p = a :: b :: r` with `a = x`. obtain ⟨a, b, r, rfl⟩ : ∃ a b r, p = a :: b :: r := by match p, hlen with | a :: b :: r, _ => exact ⟨a, b, r, rfl⟩ have hax : a = x := by simpa using hhead subst hax clear hhead have haS : a ∈ S := hnodes a (by simp) have hbS : b ∈ S := hnodes b (by simp) have hadj_ab : G.UAdj a b := G.activePath_head_uAdj hp -- Branch on the remainder. match r, hp, hn, hnodes, hlast with | [], hp, hn, hnodes, hlast => -- `p = [a, b]`: single skeleton step `a — b`, with `b = y`. have hby : b = y := by simpa using hlast subst hby have hne : a ≠ b := by rintro rfl rcases hadj_ab with h | h <;> exact G.irrefl a h exact G.moralConn_of_step (G.moralStep_of_uAdj hne haS hbS hadj_ab hxZ hyZ) | c :: r', hp, hn, hnodes, hlast => have hcS : c ∈ S := hnodes c (by simp) have htri := G.activePath_head_triple hp by_cases hcoll : G.IsCollider a b c · -- Married step `a — c` across the collider apex `b`. rw [if_pos hcoll] at htri obtain ⟨hab_edge, hcb_edge⟩ := hcoll -- The married step (or reflexivity if `a = c`), then continue from `c`. have hstep : ∀ (hcZ : c ∉ Z), G.MoralConn S Z a c := by intro hcZ by_cases hne : a = c · subst hne; exact Relation.ReflTransGen.refl · exact G.moralConn_of_step (G.moralStep_of_married hne haS hcS hbS hab_edge hcb_edge hxZ hcZ) match r', hp, hn, hnodes, hlast with | [], hp, hn, hnodes, hlast => -- `c` is the last node, so `c = y`; the married step is the whole path. have hcy : c = y := by simpa using hlast subst hcy exact hstep hyZ | d :: r'', hp, hn, hnodes, hlast => -- triple `(b, c, d)`; `c` cannot be a collider (would clash with `b`). have htri2 := G.activePath_head_triple (G.isActivePath_cons_tail hp) have hncoll2 : ¬ G.IsCollider b c d := by rintro ⟨hbc, _⟩ exact G.asymm hcb_edge hbc rw [if_neg hncoll2] at htri2 have hcZ : c ∉ Z := htri2 have hrest : G.MoralConn S Z c y := by refine ih (p := c :: d :: r'') ?_ (G.activePath_drop2 hp) (by simp) (fun v hv => hnodes v (by simp [hv])) rfl (by simpa using hlast) hcZ hyZ simp only [List.length_cons] at hn ⊢; omega exact G.moralConn_trans (hstep hcZ) hrest · -- Skeleton step `a — b`, then recurse on `b :: c :: r'`. rw [if_neg hcoll] at htri have hbZ : b ∉ Z := htri have hne : a ≠ b := by rintro rfl rcases hadj_ab with h | h <;> exact G.irrefl a h have hstep : G.MoralConn S Z a b := G.moralConn_of_step (G.moralStep_of_uAdj hne haS hbS hadj_ab hxZ hbZ) have hrest : G.MoralConn S Z b y := by apply ih (p := b :: c :: r') (by simp at hn ⊢; omega) (G.isActivePath_cons_tail hp) (by simp) (fun v hv => hnodes v (by simp [hv])) rfl (by simpa using hlast) hbZ hyZ exact G.moralConn_trans hstep hrest
    Causalean.DAG.moralConn_of_activePath · Causalean/Graph/MarkovEquiv/Moralization.lean:129
  • ancestralSet_cases theorem — A vertex in the ancestral closure of a set either belongs to that set itself or is a strict ancestor of one of its elements.
    A :
    a :
    V
    h :
    a ∈ G.ancestralSet A
    a ∈ A ∨ ∃ w ∈ A, G.isAncestor a w
    Proof (Lean source)
    theorem ancestralSet_cases {A : Finset V} {a : V} (h : a ∈ G.ancestralSet A) : a ∈ A ∨ ∃ w ∈ A, G.isAncestor a w := by rcases Finset.mem_union.mp h with hA | hAnc · exact inl hA · simp only [ancestorsSet, mem_filter, Finset.mem_univ, true_and] at hAnc exact inr hAnc
    Causalean.DAG.ancestralSet_cases · Causalean/Graph/MarkovEquiv/Moralization.lean:331
  • moralAdj_congr theorem — Moral adjacency is a skeleton + v-structure invariant. Two DAGs with the same skeleton and the same v-structures induce the same moral adjacency on any ground set: a shielded pair is moral-adjacent via the shared skeleton edge, and an unshielded married pair is exactly the apex of a shared immorality.
    G₁ G₂ :
    DAG V
    hskel :
    SameSkeleton G₁ G₂
    himm :
    S :
    u v :
    V
    G₁.MoralAdj S u v ↔ G₂.MoralAdj S u v
    Proof (Lean source)
    theorem moralAdj_congr {G₁ G₂ : DAG V} (hskel : SameSkeleton G₁ G₂) (himm : SameImmoralities G₁ G₂) (S : Finset V) (u v : V) : G₁.MoralAdj S u v ↔ G₂.MoralAdj S u v := by unfold DAG.MoralAdj by_cases hne : u = v · simp [hne] refine and_congr_right (fun _ => ?_) refine and_congr_right (fun hu => ?_) refine and_congr_right (fun hv => ?_) by_cases hUA : G₁.UAdj u v · simp only [hUA, (hskel u v).mp hUA, true_or] · have hUA₂ : ¬ G₂.UAdj u v := fun h => hUA ((hskel u v).mpr h) simp only [hUA, hUA₂, false_or] refine exists_congr (fun c => ?_) refine and_congr_right (fun _ => ?_) -- common child `u → c ← v` (with `u,v` non-adjacent, distinct) is an immorality. constructor · rintro ⟨huc, hvc⟩ have him₁ : G₁.IsImmorality u c v := ⟨huc, hvc, hUA, hne⟩ have him₂ : G₂.IsImmorality u c v := (himm u c v).mp him₁ exact ⟨him₂.1, him₂.2.1⟩ · rintro ⟨huc, hvc⟩ have him₂ : G₂.IsImmorality u c v := ⟨huc, hvc, hUA₂, hne⟩ have him₁ : G₁.IsImmorality u c v := (himm u c v).mpr him₂ exact ⟨him₁.1, him₁.2.1⟩
  • moralStep_congr theorem — Moral steps agree across DAGs with the same skeleton and v-structures (same ground set, same conditioning set).
    G₁ G₂ :
    DAG V
    hskel :
    SameSkeleton G₁ G₂
    himm :
    S Z :
    u v :
    V
    G₁.MoralStep S Z u v ↔ G₂.MoralStep S Z u v
    Proof (Lean source)
    theorem moralStep_congr {G₁ G₂ : DAG V} (hskel : SameSkeleton G₁ G₂) (himm : SameImmoralities G₁ G₂) (S Z : Finset V) (u v : V) : G₁.MoralStep S Z u v ↔ G₂.MoralStep S Z u v := by unfold DAG.MoralStep rw [moralAdj_congr hskel himm]
  • moralConn_congr theorem — Moral connectivity agrees across DAGs with the same skeleton and v-structures, for a fixed ground set S. (The ancestral sets used by MoralSep still differ between the graphs; that reconciliation is moralSep_congr.)
    G₁ G₂ :
    DAG V
    hskel :
    SameSkeleton G₁ G₂
    himm :
    S Z :
    u v :
    V
    G₁.MoralConn S Z u v ↔ G₂.MoralConn S Z u v
    Proof (Lean source)
    theorem moralConn_congr {G₁ G₂ : DAG V} (hskel : SameSkeleton G₁ G₂) (himm : SameImmoralities G₁ G₂) (S Z : Finset V) (u v : V) : G₁.MoralConn S Z u v ↔ G₂.MoralConn S Z u v := by unfold DAG.MoralConn constructor <;> intro h <;> refine Relation.ReflTransGen.mono (fun a b hab => ?_) u v h · exact (moralStep_congr hskel himm S Z a b).mp hab · exact (moralStep_congr hskel himm S Z a b).mpr hab
Readoff 3 core · 10 supporting If two DAGs are Markov equivalent (declare the same d-separations) then they have the same skeleton and the same v-structures. ★ adjacent_iff_not_dSeparable★ immorality_iff_colliderSep★ sameSkeleton_sameImmoralities_of_markovEquiv

Markov equivalence — the easy direction (reading skeleton and v-structures off d-sep)

If two DAGs are Markov equivalent (declare the same d-separations) then they have the same skeleton and the same v-structures. The point is that both the skeleton and the v-structures are determined by the d-separation relation:

* Skeleton. Two distinct vertices are adjacent iff they cannot be d-separated by any set (adjacent_iff_not_dSeparable): an edge is an always-active path, and conversely two non-adjacent vertices are separated by the parents of the topologically later one. * V-structures. For an unshielded triple a — b — c (with a, c non-adjacent), the middle vertex b is a collider a → b ← c iff b lies in no separating set of a and c (immorality_iff_colliderSep) — the rule the PC algorithm uses to orient colliders.

Both characterizations are phrased purely in terms of dSep, so Markov equivalence transports them, giving sameSkeleton_of_markovEquiv and sameImmoralities_of_markovEquiv, hence the easy direction sameSkeleton_sameImmoralities_of_markovEquiv.

theorem adjacent_iff_not_dSeparable reviewed
Causalean.DAG

Skeleton read-off. For two distinct vertices a and b, a and b are adjacent (in either direction) exactly when no conditioning set d-separates a from b.

Formal statement
a b :
V
hne :
a ≠ b
G.UAdj a b ↔ ¬ ∃ Z : Finset V, G.dSep {a} {b} Z
Proof (Lean source)
theorem adjacent_iff_not_dSeparable {a b : V} (hne : a ≠ b) : G.UAdj a b ↔ ¬ ∃ Z : Finset V, G.dSep {a} {b} Z := by constructor · intro h ⟨Z, hZ⟩ exact G.not_dSeparable_of_uAdj h Z hZ · intro h by_contra hadj exact h (G.dSeparable_of_not_uAdj hne hadj)
Causalean.DAG.adjacent_iff_not_dSeparable · Causalean/Graph/MarkovEquiv/Readoff.lean:178 · uses DAG , UAdj , dSep
theorem immorality_iff_colliderSep reviewed
Causalean.DAG

Collider read-off. For vertices a, b, c forming an unshielded triple: a and b adjacent, c and b adjacent, and a, c non-adjacent and distinct, the middle vertex b is a collider a → b ← c — equivalently a → b ← c is a v-structure — exactly when b belongs to no conditioning set that d-separates a and c.

Formal statement
a b c :
V
hab :
G.UAdj a b
hcb :
G.UAdj c b
hac :
¬ G.UAdj a c
hne :
a ≠ c
G.IsImmorality a b c ↔ ∀ Z : Finset V, b ∈ Z → ¬ G.dSep {a} {c} Z
Proof (Lean source)
theorem immorality_iff_colliderSep {a b c : V} (hab : G.UAdj a b) (hcb : G.UAdj c b) (hac : ¬ G.UAdj a c) (hne : a ≠ c) : G.IsImmorality a b c ↔ ∀ Z : Finset V, b ∈ Z → ¬ G.dSep {a} {c} Z := by constructor · -- Forward. From the immorality, `(a, b, c)` is a collider; the length-3 path `[a, b, c]` -- is active given any `Z ∋ b` (the collider `b` is activated since `b ∈ Z ⊆ ancestralSet Z`), -- so `c` is reachable from `{a}`, contradicting d-separation. rintro ⟨heab, hecb, _, _⟩ Z hbZ hsep have hcoll : G.IsCollider a b c := ⟨heab, hecb⟩ have hcReach : c ∈ G.bbReachableVertices Z {a} := by rw [G.bbReachableVertices_iff_activePath] refine ⟨a, mem_singleton_self a, [a, b, c], by simp, ⟨?_, ?_⟩, rfl, rfl⟩ · -- Adjacency along `[a, b, c]`. intro i hi match i, hi with | 0, _ => exact hab | 1, _ => exact inr hecb · -- The single interior triple `(a, b, c)` is the collider `b`, activated by `b ∈ Z`. intro i hi have hi0 : i = 0 := by simp only [List.length_cons, List.length_nil] at hi; omega subst hi0 simp only [get] have hbAnc : b ∈ G.bbZAncestors Z := by unfold bbZAncestors; exact G.subset_ancestralSet Z hbZ rw [if_pos hcoll] exact hbAnc simp only [DAG.dSep] at hsep exact (Finset.disjoint_left.mp hsep.2.2.2 hcReach) (mem_singleton_self c) · -- Backward. Suppose `b` is in no separator of `a, c`. We must show `IsImmorality a b c`, -- i.e. `edge a b ∧ edge c b` (the remaining conjuncts are `hac`, `hne`). By contradiction: -- if `b` is not the collider `a → b ← c`, then `b` is a non-collider, hence a parent of -- the topologically-later of `a, c`; so `parents (later)` separates `a` from `c` *and* -- contains `b`, contradicting the hypothesis. intro H refine ⟨?_, ?_, hac, hne⟩ <;> · by_contra hcontra -- Reduce both subgoals to: `¬ (edge a b ∧ edge c b)`, i.e. `¬ IsCollider a b c`. have hnc : ¬ G.IsCollider a b c := by rintro ⟨h1, h2⟩ first | exact hcontra h1 | exact hcontra h2 -- Build a separator `Z ∋ b` with `dSep {a} {c} Z`, contradicting `H`. have htop_ne : G.topoOrder a ≠ G.topoOrder c := fun htop => hne (G.topoOrder_injective htop) rcases lt_or_gt_of_ne htop_ne with hlt | hlt · -- `a` no later than `c`: separator `parents c`; `b → c` makes `b ∈ parents c`. have heby : G.edge b c := G.edge_to_later_of_nonCollider (le_of_lt hlt) hab hcb hnc have hsep : G.dSep {a} {c} (G.parents c) := G.dSep_parents_of_topoOrder_lt hlt hac exact H (G.parents c) (G.mem_parents.mpr heby) hsep · -- `c` no later than `a`: separator `parents a`; `b → a` makes `b ∈ parents a`. have hac' : ¬ G.UAdj c a := fun h' => hac (G.UAdj_symm h') have hnc' : ¬ G.IsCollider c b a := by rintro ⟨h1, h2⟩; exact hnc ⟨h2, h1⟩ have heby : G.edge b a := G.edge_to_later_of_nonCollider (le_of_lt hlt) hcb hab hnc' have hsep : G.dSep {a} {c} (G.parents a) := G.dSep_symm _ _ _ (G.dSep_parents_of_topoOrder_lt hlt hac') exact H (G.parents a) (G.mem_parents.mpr heby) hsep
Causalean.DAG.immorality_iff_colliderSep · Causalean/Graph/MarkovEquiv/Readoff.lean:212 · uses DAG , IsImmorality , UAdj , dSep
theorem sameSkeleton_sameImmoralities_of_markovEquiv reviewed
Causalean

Easy direction of Verma–Pearl. For two DAGs G₁, G₂, if they are Markov equivalent then they share the same skeleton and the same v-structures (immoralities).

Formal statement
G₁ G₂ :
DAG V
h :
MarkovEquiv G₁ G₂
SameSkeleton G₁ G₂ ∧ SameImmoralities G₁ G₂
Proof (Lean source)
theorem sameSkeleton_sameImmoralities_of_markovEquiv {G₁ G₂ : DAG V} (h : MarkovEquiv G₁ G₂) : SameSkeleton G₁ G₂ ∧ SameImmoralities G₁ G₂ := ⟨sameSkeleton_of_markovEquiv h, sameImmoralities_of_markovEquiv h⟩
Causalean.sameSkeleton_sameImmoralities_of_markovEquiv · Causalean/Graph/MarkovEquiv/Readoff.lean:384 · uses DAG , MarkovEquiv , SameImmoralities , SameSkeleton
10 supporting declarations (lemmas, instances)
  • not_dSeparable_of_uAdj theorem — An edge makes its endpoints inseparable: if a and b are adjacent then no conditioning set d-separates them, because the single edge [a, b] is an active path regardless of the conditioning set.
    a b :
    V
    h :
    G.UAdj a b
    Z :
    ¬ G.dSep {a} {b} Z
    Proof (Lean source)
    theorem not_dSeparable_of_uAdj {a b : V} (h : G.UAdj a b) (Z : Finset V) : ¬ G.dSep {a} {b} Z := by intro hsep have hb : b ∈ G.bbReachableVertices Z {a} := by rw [G.bbReachableVertices_iff_activePath] refine ⟨a, mem_singleton_self a, [a, b], ?_, ⟨?_, ?_⟩, rfl, rfl⟩ · simp · intro i hi have hi0 : i = 0 := by simp only [List.length_cons, List.length_nil] at hi; omega subst hi0 exact h · intro i hi simp only [List.length_cons, List.length_nil] at hi omega simp only [DAG.dSep] at hsep exact (Finset.disjoint_left.mp hsep.2.2.2 hb) (mem_singleton_self b)
    Causalean.DAG.not_dSeparable_of_uAdj · Causalean/Graph/MarkovEquiv/Readoff.lean:37
  • dSep_parents_of_topoOrder_lt theorem — If one endpoint precedes a nonadjacent later endpoint topologically, conditioning on the later endpoint's parents d-separates the pair.
    x y :
    V
    hlt :
    G.topoOrder x < G.topoOrder y
    hxy :
    ¬ G.UAdj x y
    G.dSep {x} {y} (G.parents y)
    Proof (Lean source)
    theorem dSep_parents_of_topoOrder_lt {x y : V} (hlt : G.topoOrder x < G.topoOrder y) (hxy : ¬ G.UAdj x y) : G.dSep {x} {y} (G.parents y) := by have hne : x ≠ y := by intro h subst h exact (Nat.lt_irrefl _) hlt -- Suppose not: then `y` is reachable from `{x}` given `parents y`. Subst eliminates the -- bound names `x, y`, leaving the source `s₀` and target `t` introduced by the witness. rw [DAG.dSep] refine ⟨Finset.disjoint_singleton.mpr hne, Finset.disjoint_singleton_left.mpr (fun hmem => hxy (inl (G.mem_parents.mp hmem))), Finset.disjoint_singleton_left.mpr (fun hmem => G.irrefl y (G.mem_parents.mp hmem)), ?_⟩ rw [Finset.disjoint_left] intro t hv hvy rw [mem_singleton] at hvy subst hvy -- Extract an active path `p` from `s₀` to `t` given `Z := parents t`. rw [G.bbReachableVertices_iff_activePath] at hv obtain ⟨s₀, hx', p, hlen, hact, hhead, hlast⟩ := hv rw [mem_singleton] at hx' subst hx' -- The path has length ≥ 3: a length-2 path `[s₀, t]` would force `UAdj s₀ t`. have hp_ne : p ≠ [] := by intro hnil; rw [hnil] at hlen; simp at hlen have hlast_get : p.get ⟨p.length - 1, by omega⟩ = t := by have h := getLast?_eq_some_getLast hp_ne rw [hlast] at h have hy_eq : p.getLast hp_ne = t := Option.some_inj.mp h.symm rw [← hy_eq]; exact (List.getLast_eq_getElem hp_ne).symm have hhead_get : p.get ⟨0, by omega⟩ = s₀ := by have h := List.head?_eq_some_head hp_ne rw [hhead] at h have hx_eq : p.head hp_ne = s₀ := Option.some_inj.mp h.symm rw [← hx_eq]; simp [List.head_eq_getElem hp_ne] have hlen3 : p.length ≥ 3 := by by_contra hlt2 -- p.length = 2, so p = [s₀, t]; then UAdj s₀ t from the adjacency at index 0. have hp2 : p.length = 2 := by omega have hadj0 := hact.1 0 (by omega) have h1 : (⟨0 + 1, by omega⟩ : Fin p.length) = ⟨p.length - 1, by omega⟩ := Fin.ext (by simp; omega) rw [h1] at hadj0 rw [hlast_get] at hadj0 rw [hhead_get] at hadj0 exact hxy hadj0 -- Name the second-to-last vertex `m` and the triple `(m', m, t)`. have hm_idx : p.length - 2 + 1 < p.length := by omega have hadj_my := hact.1 (p.length - 2) hm_idx have hm1_eq : (⟨p.length - 2 + 1, hm_idx⟩ : Fin p.length) = ⟨p.length - 1, by omega⟩ := Fin.ext (by simp; omega) rw [hm1_eq, hlast_get] at hadj_my set m := p.get ⟨p.length - 2, by omega⟩ with hm_def -- `hadj_my : G.UAdj m t`. Split on the orientation of that last edge. rcases hadj_my with hmy | hym · -- Case `edge m t`: then `m ∈ parents t = Z`, but the active-path non-collider -- condition at the triple `(m', m, t)` forces `m ∉ Z`. have htri : p.length - 3 + 2 < p.length := by omega have hcoll := hact.2 (p.length - 3) htri simp only at hcoll have e1 : (⟨p.length - 3 + 1, by omega⟩ : Fin p.length) = ⟨p.length - 2, by omega⟩ := Fin.ext (by simp; omega) have e2 : (⟨p.length - 3 + 2, htri⟩ : Fin p.length) = ⟨p.length - 1, by omega⟩ := Fin.ext (by simp; omega) rw [e1, e2, hlast_get] at hcoll -- The middle vertex is `m`. It is not a collider: `edge m t` rules out `edge t m`. set l := p.get ⟨p.length - 3, by omega⟩ with hl_def have hnotColl : ¬ G.IsCollider l m t := by rintro ⟨_, hym⟩; exact G.asymm hmy hym rw [if_neg hnotColl] at hcoll -- But `m ∈ parents t` since `edge m t`. exact hcoll (G.mem_parents.mpr hmy) · -- Case `edge t m`: then `m` is a child of `t`, hence `topoOrder m > topoOrder t`. have htop_m : G.topoOrder t < G.topoOrder m := G.topoOrder_lt t m hym -- But `m` lies on the active path, hence in `ancestralSet ({s₀} ∪ {t} ∪ parents t)`. have hm_mem : m ∈ p := by rw [hm_def]; exact List.get_mem _ _ have hm_anc := G.activePath_nodes_are_ancestors (mem_singleton_self s₀) (mem_singleton_self t) hact hhead hlast m hm_mem -- Every member of `{s₀} ∪ {t} ∪ parents t` has `topoOrdertopoOrder t`. have hS_le : ∀ s ∈ ({s₀} ∪ {t} ∪ G.parents t : Finset V), G.topoOrder s ≤ G.topoOrder t := by intro s hs simp only [mem_union, mem_singleton] at hs rcases hs with (hsx | hsy) | hsp · subst hsx; exact le_of_lt hlt · subst hsy; exact le_refl _ · exact le_of_lt (G.topoOrder_lt s t (G.mem_parents.mp hsp)) -- `m ∈ ancestralSet S`: either `m ∈ S` or `m` is a strict ancestor of some `s ∈ S`. rcases Finset.mem_union.mp hm_anc with hmS | hmAnc · -- `m ∈ S` ⇒ `topoOrder m ≤ topoOrder t`, contradicting `topoOrder t < topoOrder m`. have := hS_le m hmS; omega · -- `m` strict ancestor of some `s ∈ S` ⇒ `topoOrder m < topoOrder s ≤ topoOrder t`. simp only [ancestorsSet, mem_filter, Finset.mem_univ, true_and] at hmAnc obtain ⟨s, hsS, hms⟩ := hmAnc have h1 := G.isAncestor_topoOrder_lt hms have h2 := hS_le s hsS omega
    Causalean.DAG.dSep_parents_of_topoOrder_lt · Causalean/Graph/MarkovEquiv/Readoff.lean:57
  • dSeparable_of_not_uAdj theorem — Two distinct non-adjacent vertices can always be d-separated: conditioning on the parents of the topologically later vertex blocks every path between them.
    a b :
    V
    hne :
    a ≠ b
    h :
    ¬ G.UAdj a b
    ∃ Z : Finset V, G.dSep {a} {b} Z
    Proof (Lean source)
    theorem dSeparable_of_not_uAdj {a b : V} (hne : a ≠ b) (h : ¬ G.UAdj a b) : ∃ Z : Finset V, G.dSep {a} {b} Z := by -- Pick the topologically later endpoint; condition on its parents. The helper only needs -- `topoOrder (earlier) < topoOrder (later)`, so `<` in either direction discharges the case. have htop_ne : G.topoOrder a ≠ G.topoOrder b := fun htop => hne (G.topoOrder_injective htop) rcases lt_or_gt_of_ne htop_ne with hlt | hlt · -- `a` no later than `b`: separate by `parents b`, already in the right orientation. exact ⟨G.parents b, G.dSep_parents_of_topoOrder_lt hlt h⟩ · -- `b` no later than `a`: separate by `parents a`, then flip with `dSep_symm`. have hba : ¬ G.UAdj b a := fun h' => h (G.UAdj_symm h') exact ⟨G.parents a, G.dSep_symm _ _ _ (G.dSep_parents_of_topoOrder_lt hlt hba)⟩
    Causalean.DAG.dSeparable_of_not_uAdj · Causalean/Graph/MarkovEquiv/Readoff.lean:164
  • edge_to_later_of_nonCollider theorem — On an unshielded two-edge triple, a non-collider middle vertex points into the topologically later endpoint.
    x y b :
    V
    hxy_le :
    G.topoOrder x ≤ G.topoOrder y
    hxb :
    G.UAdj x b
    hyb :
    G.UAdj y b
    hnc :
    ¬ G.IsCollider x b y
    G.edge b y
    Proof (Lean source)
    theorem edge_to_later_of_nonCollider {x y b : V} (hxy_le : G.topoOrder x ≤ G.topoOrder y) (hxb : G.UAdj x b) (hyb : G.UAdj y b) (hnc : ¬ G.IsCollider x b y) : G.edge b y := by have hby : G.UAdj b y := G.UAdj_symm hyb rcases G.nonCollider_has_outgoing hxb hby hnc with hbx | hby' · -- `edge b x`: then `topoOrder b < topoOrder x ≤ topoOrder y`. The adjacency `y — b` -- cannot be `edge y b` (that gives `topoOrder y < topoOrder b`), so it is `edge b y`. have htb_x : G.topoOrder b < G.topoOrder x := G.topoOrder_lt b x hbx rcases hyb with hyb_e | hby_e · exact absurd (G.topoOrder_lt y b hyb_e) (by omega) · exact hby_e · exact hby'
    Causalean.DAG.edge_to_later_of_nonCollider · Causalean/Graph/MarkovEquiv/Readoff.lean:190
  • not_edge_self theorem — No self-loops in a DAG.
    a :
    V
    ¬ G.edge a a
    Proof (Lean source)
    theorem not_edge_self (a : V) : ¬ G.edge a a := fun he => G.isAncestor_irrefl a (DAG.isAncestor.edge he)
    Causalean.DAG.not_edge_self · Causalean/Graph/MarkovEquiv/Readoff.lean:278
  • not_uAdj_self theorem — No self-adjacency in a DAG.
    a :
    V
    ¬ G.UAdj a a
    Proof (Lean source)
    theorem not_uAdj_self (a : V) : ¬ G.UAdj a a := fun h => h.elim (G.not_edge_self a) (G.not_edge_self a)
    Causalean.DAG.not_uAdj_self · Causalean/Graph/MarkovEquiv/Readoff.lean:282
  • dSeparable_disjoint_of_not_uAdj theorem — Disjoint skeleton read-off witness. Two distinct non-adjacent vertices are d-separated by a conditioning set disjoint from both endpoints (the parents of the topologically later one, which contain neither a nor b).
    a b :
    V
    hne :
    a ≠ b
    h :
    ¬ G.UAdj a b
    ∃ Z : Finset V, a ∉ Z ∧ b ∉ Z ∧ G.dSep {a} {b} Z
    Proof (Lean source)
    theorem dSeparable_disjoint_of_not_uAdj {a b : V} (hne : a ≠ b) (h : ¬ G.UAdj a b) : ∃ Z : Finset V, a ∉ Z ∧ b ∉ Z ∧ G.dSep {a} {b} Z := by have htop_ne : G.topoOrder a ≠ G.topoOrder b := fun htop => hne (G.topoOrder_injective htop) rcases lt_or_gt_of_ne htop_ne with hlt | hlt · refine ⟨G.parents b, fun hmem => h (inl (G.mem_parents.mp hmem)), fun hmem => G.not_edge_self b (G.mem_parents.mp hmem), G.dSep_parents_of_topoOrder_lt hlt h⟩ · have hba : ¬ G.UAdj b a := fun h' => h (G.UAdj_symm h') refine ⟨G.parents a, fun hmem => G.not_edge_self a (G.mem_parents.mp hmem), fun hmem => h (inr (G.mem_parents.mp hmem)), G.dSep_symm _ _ _ (G.dSep_parents_of_topoOrder_lt hlt hba)⟩
    Causalean.DAG.dSeparable_disjoint_of_not_uAdj · Causalean/Graph/MarkovEquiv/Readoff.lean:286
  • immorality_iff_colliderSep_disjoint theorem — Disjoint collider read-off. For an unshielded triple a — b — c, b is the collider a → b ← c iff every separator of a, c that contains b and excludes a, c fails to d-separate — equivalently, b lies in no such separator. (The endpoint-disjoint form used to transport immoralities across Markov-equivalent graphs.)
    a b c :
    V
    hab :
    G.UAdj a b
    hcb :
    G.UAdj c b
    hac :
    ¬ G.UAdj a c
    hne :
    a ≠ c
    G.IsImmorality a b c ↔ ∀ Z : Finset V, b ∈ Z → a ∉ Z → c ∉ Z → ¬ G.dSep {a} {c} Z
    Proof (Lean source)
    theorem immorality_iff_colliderSep_disjoint {a b c : V} (hab : G.UAdj a b) (hcb : G.UAdj c b) (hac : ¬ G.UAdj a c) (hne : a ≠ c) : G.IsImmorality a b c ↔ ∀ Z : Finset V, b ∈ Z → a ∉ Z → c ∉ Z → ¬ G.dSep {a} {c} Z := by constructor · intro him Z hbZ _ _ exact (G.immorality_iff_colliderSep hab hcb hac hne).mp him Z hbZ · intro H refine ⟨?_, ?_, hac, hne⟩ <;> · by_contra hcontra have hnc : ¬ G.IsCollider a b c := by rintro ⟨h1, h2⟩; first | exact hcontra h1 | exact hcontra h2 have htop_ne : G.topoOrder a ≠ G.topoOrder c := fun htop => hne (G.topoOrder_injective htop) rcases lt_or_gt_of_ne htop_ne with hlt | hlt · have heby : G.edge b c := G.edge_to_later_of_nonCollider (le_of_lt hlt) hab hcb hnc exact H (G.parents c) (G.mem_parents.mpr heby) (fun hmem => hac (inl (G.mem_parents.mp hmem))) (fun hmem => G.not_edge_self c (G.mem_parents.mp hmem)) (G.dSep_parents_of_topoOrder_lt hlt hac) · have hac' : ¬ G.UAdj c a := fun h' => hac (G.UAdj_symm h') have hnc' : ¬ G.IsCollider c b a := by rintro ⟨h1, h2⟩; exact hnc ⟨h2, h1⟩ have heby : G.edge b a := G.edge_to_later_of_nonCollider (le_of_lt hlt) hcb hab hnc' exact H (G.parents a) (G.mem_parents.mpr heby) (fun hmem => G.not_edge_self a (G.mem_parents.mp hmem)) (fun hmem => hac (inr (G.mem_parents.mp hmem))) (G.dSep_symm _ _ _ (G.dSep_parents_of_topoOrder_lt hlt hac'))
    Causalean.DAG.immorality_iff_colliderSep_disjoint · Causalean/Graph/MarkovEquiv/Readoff.lean:301
  • sameSkeleton_of_markovEquiv theorem — Markov equivalence ⇒ same skeleton.
    G₁ G₂ :
    DAG V
    h :
    MarkovEquiv G₁ G₂
    SameSkeleton G₁ G₂
    Proof (Lean source)
    theorem sameSkeleton_of_markovEquiv {G₁ G₂ : DAG V} (h : MarkovEquiv G₁ G₂) : SameSkeleton G₁ G₂ := by -- One-directional transfer: a disjoint separator of a non-adjacent pair transports. have key : ∀ {Ga Gb : DAG V}, MarkovEquiv Ga Gb → ∀ a b, a ≠ b → ¬ Ga.UAdj a b → ¬ Gb.UAdj a b := by intro Ga Gb hab a b hne hna hUAdj obtain ⟨Z, haZ, hbZ, hsep⟩ := Ga.dSeparable_disjoint_of_not_uAdj hne hna exact Gb.not_dSeparable_of_uAdj hUAdj Z ((hab {a} {b} Z).mp hsep) intro a b by_cases hne : a = b · subst hne; exact iff_of_false (G₁.not_uAdj_self a) (G₂.not_uAdj_self a) · constructor · intro h1; by_contra h2; exact key h.symm a b hne h2 h1 · intro h1; by_contra h2; exact key h a b hne h2 h1
    Causalean.sameSkeleton_of_markovEquiv · Causalean/Graph/MarkovEquiv/Readoff.lean:334
  • sameImmoralities_of_markovEquiv theorem — Markov equivalence ⇒ same v-structures.
    G₁ G₂ :
    DAG V
    h :
    MarkovEquiv G₁ G₂
    Proof (Lean source)
    theorem sameImmoralities_of_markovEquiv {G₁ G₂ : DAG V} (h : MarkovEquiv G₁ G₂) : SameImmoralities G₁ G₂ := by have hskel : SameSkeleton G₁ G₂ := sameSkeleton_of_markovEquiv h intro a b c -- Dispose of the cases where the triple is not unshielded; then use the collider read-off. by_cases hne : a = c · exact iff_of_false (fun him => him.2.2.2 hne) (fun him => him.2.2.2 hne) · by_cases hac : G₁.UAdj a c · have hac₂ : G₂.UAdj a c := (hskel a c).mp hac exact iff_of_false (fun him => him.2.2.1 hac) (fun him => him.2.2.1 hac₂) · have hac₂ : ¬ G₂.UAdj a c := fun h' => hac ((hskel a c).mpr h') by_cases hab : G₁.UAdj a b · by_cases hcb : G₁.UAdj c b · -- the unshielded triple case in both graphs have hab₂ : G₂.UAdj a b := (hskel a b).mp hab have hcb₂ : G₂.UAdj c b := (hskel c b).mp hcb rw [G₁.immorality_iff_colliderSep_disjoint hab hcb hac hne, G₂.immorality_iff_colliderSep_disjoint hab₂ hcb₂ hac₂ hne] refine forall_congr' (fun Z => ?_) refine imp_congr_right (fun _ => ?_) refine imp_congr_right (fun haZ => ?_) refine imp_congr_right (fun hcZ => ?_) exact not_congr (h {a} {c} Z) · -- c, b non-adjacent in both: both sides false (needs `edge c b`, hence `UAdj c b`) have hcb₂ : ¬ G₂.UAdj c b := fun h' => hcb ((hskel c b).mpr h') exact iff_of_false (fun him => hcb (inl him.2.1)) (fun him => hcb₂ (inl him.2.1)) · -- a, b non-adjacent in both: both sides false (needs `edge a b`, hence `UAdj a b`) have hab₂ : ¬ G₂.UAdj a b := fun h' => hab ((hskel a b).mpr h') exact iff_of_false (fun him => hab (inl him.1)) (fun him => hab₂ (inl him.1))
    Causalean.sameImmoralities_of_markovEquiv · Causalean/Graph/MarkovEquiv/Readoff.lean:351