Graph.MarkovEquiv
The Verma–Pearl characterization of Markov equivalence (Verma & Pearl, *Equivalence and synthesis of causal models*, 1990): two directed acyclic graphs on the same finite vertex set imply exactly the same d-separation st
MarkovEquiv 1 core · 0 supporting The Verma–Pearl characterization of Markov equivalence (Verma & Pearl, *Equivalence and synthesis of causal models*, 1990): two directed acyclic graphs on the same finite vertex set imply exactly the same d-separation st ★ markovEquiv_iff_sameSkeleton_sameImmoralities
Markov equivalence of DAGs (Verma–Pearl)
The Verma–Pearl characterization of Markov equivalence (Verma & Pearl, Equivalence and synthesis of causal models, 1990): two directed acyclic graphs on the same finite vertex set imply exactly the same d-separation statements, and hence the same conditional-independence constraints, if and only if they have the same skeleton (the same adjacent pairs) and the same v-structures (colliders a → b ← c with a and c non-adjacent). The forward direction reads the skeleton and v-structures off the d-separation relation; the converse shows that two such graphs are linked by reversals of covered edges, each of which preserves every d-separation. The development also proves the moralization criterion: for pairwise-disjoint sets, d-separation is the same as separation in the moral graph of the ancestral set.
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.
* 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), and Moralization.lean (the ancestral moral graph criterion for
d-separation). The development reuses the existing d-separation engine
(Causalean.Graph.DSep).
The higher-layer companion Causalean.SCM.Do.MarkovEquivDistributional connects
this graph notion to distributions and proves SCM.distMarkovEquiv_of_markovEquiv;
it is not imported by this file.
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
Proof (Lean source)
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
Causalean.Graph.MarkovEquiv) 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.
The first three relations have decidable instances on a finite vertex type. MarkovEquiv is
instead the semantic relation quantified over all finite vertex sets. 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.
For a finite directed acyclic graph on a vertex population and three ordered vertices, the v-structure, or immorality, condition holds exactly when the first and third vertices are distinct, each has a directed edge into the second, and no directed edge joins the first and third in either direction.
Definition (Lean source)
For a finite vertex population and two directed acyclic graphs on it, the same-skeleton condition holds exactly when, for every pair of vertices, a directed edge joins the pair in either direction in the first graph if and only if one does in the second graph.
Definition (Lean source)
For a finite vertex population and two directed acyclic graphs on it, the same-immoralities condition holds exactly when every ordered triple of vertices is an immorality in the first graph if and only if it is an immorality in the second graph.
Definition (Lean source)
For a finite vertex population and two directed acyclic graphs on it, Markov equivalence holds exactly when, for every three finite vertex sets, the first and second sets are d-separated by the third in the first graph if and only if they are d-separated in the second graph.
For any DAG G, G is Markov equivalent to itself.
Formal statement
Proof (Lean source)
5 supporting declarations (lemmas, instances)
-
instDecidableIsImmoralityinstance — For a finite vertex population with decidable equality, a directed acyclic graph on that population, and three ordered vertices, the decision procedure for the v-structure condition determines whether the first and third vertices are distinct non-adjacent parents of the second. -
instDecidableSameSkeletoninstance — For a finite vertex population with decidable equality and two directed acyclic graphs on that population, the decision procedure for the same-skeleton condition determines whether the graphs have identical undirected adjacencies for every pair of vertices.parametersV :sharedType u_1G₁ G₂ :DAG Vinstancegiven byby unfold SameSkeleton; infer_instance -
instDecidableSameImmoralitiesinstance — For a finite vertex population with decidable equality and two directed acyclic graphs on that population, the decision procedure for the same-immoralities condition determines whether the graphs have identical v-structures for every ordered triple of vertices.parametersV :sharedType u_1G₁ G₂ :DAG Vinstancegiven byby unfold SameImmoralities; infer_instance -
symmtheorem — Markov equivalence is symmetric.Proof (Lean source)
theorem MarkovEquiv.symm {G₁ G₂ : DAG V} (h : MarkovEquiv G₁ G₂) : MarkovEquiv G₂ G₁ := fun X Y Z => (h X Y Z).symm -
transtheorem — Markov equivalence is transitive.hypothesesconclusionMarkovEquiv G₁ G₃Proof (Lean source)
theorem MarkovEquiv.trans {G₁ G₂ G₃ : DAG V} (h₁ : MarkovEquiv G₁ G₂) (h₂ : MarkovEquiv G₂ G₃) : MarkovEquiv G₁ G₃ := fun X Y Z => (h₁ X Y Z).trans (h₂ X Y Z)
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 induction for the 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. The public helper
exists_covered_reversed_edge gives the key one-step progress result underlying
Andersson–Madigan–Perlman (1997), Lemma 3.2. A private strong induction on the number of
oppositely oriented edges (edgeDiffCount) combines each step with markovEquiv_flipEdge and
MarkovEquiv.trans to prove the semantic-equivalence theorem. This module does not expose a
finite reversal sequence or a finite-chain certificate.
For a finite vertex set with decidable equality and two directed acyclic graphs on that vertex set, the directed-edge difference is the finite set of all ordered pairs of vertices that form a directed edge in the first graph but not in the second graph.
For a finite vertex set with decidable equality and two directed acyclic graphs on that vertex set, the directed-edge difference count is the number of directed edges in the first graph that are absent from the second graph.
Key induction step underlying AMP Lemma 3.2. For DAGs G₁ and G₂ on the same vertex set, suppose G₁ and G₂ have the same skeleton, have the same immoralities, and G₁ contains a₀ → b₀ while G₂ contains b₀ → a₀. Then a covered G₁ edge that is reversed in G₂ exists. 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
Proof (Lean source)
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
Proof (Lean source)
7 supporting declarations (lemmas, instances)
-
symmtheorem — SameSkeleton is symmetric.Proof (Lean source)
theorem SameSkeleton.symm {G₁ G₂ : DAG V} (h : SameSkeleton G₁ G₂) : SameSkeleton G₂ G₁ := fun a b => (h a b).symm -
transtheorem — SameSkeleton is transitive.hypothesesconclusionSameSkeleton 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) -
symmtheorem — SameImmoralities is symmetric.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 -
transtheorem — SameImmoralities is transitive.hypothesesconclusionSameImmoralities G₁ G₃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) -
markovEquiv_of_same_edgetheorem — Edge-congruence for Markov equivalence. DAGs with the same directed-edge relation are Markov equivalent (d-separation depends only on the edge relation).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.hasActiveWalk_edge_congr he X Y Z rw [← DAG.not_dSep_iff_hasActiveWalk G₁ X Y Z hXY hXZ hYZ, ← DAG.not_dSep_iff_hasActiveWalk 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) -
same_edge_of_edgeDiff_emptytheorem — With a one-way skeleton inclusion, an empty edge-difference forces equal edge relations.hypothesesconclusionG₁.edge u w ↔ G₂.edge u wProof (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) -
edgeDiffCount_flipEdge_lttheorem — Flipping a covered, oppositely oriented edge strictly decreases the edge-difference count (it fixes exactly the pair (a,b) and changes nothing else).hypothesesconclusionedgeDiffCount (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
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 v — u 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 v — u 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_moralSep — the 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 Verma–Pearl equivalence theorem is assembled through
the covered-edge route in Transfer.lean and Decompose.lean.
For a finite directed acyclic graph on a vertex population, a ground set of vertices, and two vertices, moral adjacency holds exactly when the vertices are distinct members of the ground set and either a directed edge joins them in one direction or the other, or they have a common child in the ground set.
For a finite directed acyclic graph on a vertex population, a ground set, a conditioning set, and two vertices, a moral step holds exactly when the vertices are morally adjacent in the ground set and neither belongs to the conditioning set.
For a finite directed acyclic graph on a vertex population, a ground set, a conditioning set, and two vertices, moral connectivity holds exactly when the first vertex reaches the second by a possibly empty sequence of moral-adjacent steps within the ground set, each of whose endpoints lies outside the conditioning set.
For a finite directed acyclic graph on a vertex population, a first vertex set, a second vertex set, and a conditioning set, moral separation holds exactly when no member of the first set is morally connected to any member of the second within the ancestral closure of the union of all three sets, while avoiding the conditioning set.
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.
Formal statement
Proof (Lean source)
13 supporting declarations (lemmas, instances)
-
moralAdj_symmtheorem — Moral adjacency is symmetric: an undirected moral edge from u to v is also one from v to u.hypothesesconclusionG.MoralAdj S v uProof (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_steptheorem — A single moral step yields moral connectivity.hypothesesconclusionG.MoralConn S Z u vProof (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 -
moralConn_transtheorem — Moral connectivity is transitive.hypothesesconclusionG.MoralConn S Z u wProof (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_uAdjtheorem — A skeleton edge between two non-Z vertices of S is a moral step.hypothesesV :sharedType u_1S Z :Finset Vu v :Vhne :u ≠ vhu :u ∈ Shv :v ∈ Shadj :G.UAdj u vhuZ :u ∉ ZhvZ :v ∉ ZconclusionG.MoralStep S Z u vProof (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⟩ -
moralStep_of_marriedtheorem — A married pair (common child c ∈ S) of distinct non-Z vertices of S is a moral step.hypothesesV :sharedType u_1S Z :Finset Vu v c :Vhne :u ≠ vhu :u ∈ Shv :v ∈ Shc :c ∈ Shuc :G.edge u chvc :G.edge v chuZ :u ∉ ZhvZ :v ∉ ZconclusionG.MoralStep S Z u vProof (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⟩ -
activeWalk_head_uAdjtheorem — In an active walk whose first two vertices are a and b, those vertices are adjacent in the underlying undirected graph.hypothesesconclusionG.UAdj a bProof (Lean source)
theorem activeWalk_head_uAdj {Z : Finset V} {a b : V} {r : List V} (h : G.IsActiveWalk Z (a :: b :: r)) : G.UAdj a b := by have := h.1 0 (by simp) simpa using this -
activeWalk_head_tripletheorem — In an active walk whose first three vertices are a, b, and c, the middle vertex obeys the active-walk condition: a collider belongs to the ancestral closure of the conditioning set, and a non-collider is not conditioned on.hypothesesconclusionif G.IsCollider a b c then b ∈ G.bbZAncestors Z else b ∉ ZProof (Lean source)
theorem activeWalk_head_triple {Z : Finset V} {a b c : V} {r : List V} (h : G.IsActiveWalk 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 -
activeWalk_drop2theorem — Removing the first two vertices of an active walk with at least three vertices leaves an active walk.hypothesesconclusionG.IsActiveWalk Z (c :: r)Proof (Lean source)
theorem activeWalk_drop2 {Z : Finset V} {a b c : V} {r : List V} (h : G.IsActiveWalk Z (a :: b :: c :: r)) : G.IsActiveWalk Z (c :: r) := G.isActiveWalk_cons_tail (G.isActiveWalk_cons_tail h) -
moralConn_of_activeWalktheorem — An active walk inside a ground set, with endpoints outside the conditioning set, induces a connection in the corresponding moral graph.hypothesesV :sharedType u_1S Z :Finset Vn :ℕx y :Vp :List Vp.length ≤ nG.IsActiveWalk Z pp.length ≥ 2(∀ v ∈ p, v ∈ S)p.head? = some xp.getLast? = some yx ∉ Zy ∉ ZconclusionG.MoralConn S Z x yProof (Lean source)
theorem moralConn_of_activeWalk {S Z : Finset V} : ∀ (n : ℕ) {x y : V} {p : List V}, p.length ≤ n → G.IsActiveWalk 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.activeWalk_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.activeWalk_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.activeWalk_head_triple (G.isActiveWalk_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.activeWalk_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.isActiveWalk_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 -
ancestralSet_casestheorem — 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.hypothesesconclusiona ∈ A ∨ ∃ w ∈ A, G.isAncestor a wProof (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 -
moralAdj_congrtheorem — 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.hypothesesconclusionG₁.MoralAdj S u v ↔ G₂.MoralAdj S u vProof (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_congrtheorem — Moral steps agree across DAGs with the same skeleton and v-structures (same ground set, same conditioning set).hypothesesconclusionG₁.MoralStep S Z u v ↔ G₂.MoralStep S Z u vProof (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_congrtheorem — For two DAGs on a finite vertex population that have the same skeleton and the same v-structures, and for fixed ground and conditioning sets and two vertices, moral connectivity in one DAG holds exactly when it holds in the other.hypothesesconclusionG₁.MoralConn S Z u v ↔ G₂.MoralConn S Z u vProof (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 · 9 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 walk, 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.
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
Proof (Lean source)
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
Proof (Lean source)
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
Proof (Lean source)
9 supporting declarations (lemmas, instances)
-
not_dSeparable_of_uAdjtheorem — 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 walk regardless of the conditioning set.hypothesesconclusion¬ G.dSep {a} {b} ZProof (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_activeWalk] 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) -
dSep_parents_of_topoOrder_lttheorem — If one endpoint precedes a nonadjacent later endpoint topologically, conditioning on the later endpoint's parents d-separates the pair.hypothesesconclusionG.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 [Finset.mem_singleton] at hvy subst hvy -- Extract an active walk `p` from `s₀` to `t` given `Z := parents t`. rw [G.bbReachableVertices_iff_activeWalk] at hv obtain ⟨s₀, hx', p, hlen, hact, hhead, hlast⟩ := hv rw [Finset.mem_singleton] at hx' subst hx' -- The walk has length ≥ 3: a length-2 walk `[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-walk 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 walk, hence in `ancestralSet ({s₀} ∪ {t} ∪ parents t)`. have hm_mem : m ∈ p := by rw [hm_def]; exact List.get_mem _ _ have hm_anc := G.activeWalk_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 `topoOrder ≤ topoOrder 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, Finset.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 -
dSeparable_of_not_uAdjtheorem — Two distinct non-adjacent vertices can always be d-separated: conditioning on the parents of the topologically later vertex blocks every active walk between them.hypothesesconclusion∃ Z : Finset V, G.dSep {a} {b} ZProof (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)⟩ -
edge_to_later_of_nonCollidertheorem — On an unshielded two-edge triple, a non-collider middle vertex points into the topologically later endpoint.hypothesesV :sharedType u_1x y b :Vhxy_le :G.topoOrder x ≤ G.topoOrder yhxb :G.UAdj x bhyb :G.UAdj y bhnc :¬ G.IsCollider x b yconclusionG.edge b yProof (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' -
not_uAdj_selftheorem — No self-adjacency in a DAG.Proof (Lean source)
theorem not_uAdj_self (a : V) : ¬ G.UAdj a a := fun h => h.elim (G.irrefl a) (G.irrefl a) -
dSeparable_disjoint_of_not_uAdjtheorem — 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).hypothesesProof (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.irrefl 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.irrefl 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)⟩ -
immorality_iff_colliderSep_disjointtheorem — 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.)hypothesesV :sharedType u_1a b c :Vhab :G.UAdj a bhcb :G.UAdj c bhac :¬ G.UAdj a chne :a ≠ cconclusionG.IsImmorality a b c ↔ ∀ Z : Finset V, b ∈ Z → a ∉ Z → c ∉ Z → ¬ G.dSep {a} {c} ZProof (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.irrefl 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.irrefl 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')) -
sameSkeleton_of_markovEquivtheorem — Markov equivalence ⇒ same skeleton.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 -
sameImmoralities_of_markovEquivtheorem — Markov equivalence ⇒ same v-structures.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))