Graph.MarkovEquiv
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 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).
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 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.
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)
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)
Two DAGs have the same v-structures when their immorality relations agree.
Definition (Lean source)
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)
For any DAG G, G is Markov equivalent to itself.
Formal statement
Proof (Lean source)
5 supporting declarations (lemmas, instances)
-
instDecidableIsImmoralityinstance — Whether a proposed v-structure in a finite DAG is decidable.instance (a b c : V) : Decidable (G.IsImmorality a b c) := by unfold IsImmorality; infer_instance -
instDecidableSameSkeletoninstance — Whether two finite DAGs have the same skeleton is decidable. -
instDecidableSameImmoralitiesinstance — Whether two finite DAGs have the same v-structures is decidable.instance (G₁ G₂ : DAG V) : Decidable (SameImmoralities G₁ G₂) := by 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)
CoveredReversal 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).
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)
The edge relation of G with the single edge a → b deleted.
Definition (Lean source)
The edge relation of G with the single edge a → b reversed to b → a.
Definition (Lean source)
The directed acyclic graph obtained from G by reversing the covered edge a → b.
Definition (Lean source)
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
Proof (Lean source)
20 supporting declarations (lemmas, instances)
-
flipMinus_letheorem — A flipMinus-edge is in particular a G-edge.hypothesesa b u w :Vh :G.flipMinus a b u wconclusionG.edge u wProof (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_transGentheorem — A directed flipMinus-path strictly increases the topological order.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) -
netheorem — A covered edge is genuinely an edge a → b, so a ≠ b.hypothesesa b :Vh :G.IsCoveredEdge a bconclusiona ≠ bProof (Lean source)
theorem IsCoveredEdge.ne {a b : V} (h : G.IsCoveredEdge a b) : a ≠ b := by rintro rfl; exact G.irrefl _ h.1 -
flipRel_acyclictheorem — 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.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_edgetheorem — 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.hypothesesa b :Vhcov :G.IsCoveredEdge a bu w :VProof (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_sameSkeletontheorem — Reversing a covered edge preserves the skeleton. The undirected adjacency is unchanged: only the orientation of the single edge a — b flips.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 -
flipEdge_sameImmoralitiestheorem — 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.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⟩ -
not_dSep_iff_hasActivePaththeorem — 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.hypothesesconclusion¬ H.dSep X Y Z ↔ H.HasActivePath X Y ZProof (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 -
isAncestor_edge_congrtheorem — Graphs with the same directed edges have exactly the same ancestor relations.hypothesesconclusionG₁.isAncestor u v ↔ G₂.isAncestor u vProof (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) -
bbZAncestors_edge_congrtheorem — Graphs with the same directed edges have exactly the same sets of vertices that are ancestors of the conditioning set and can activate colliders.hypothesesconclusionv ∈ G₁.bbZAncestors Z ↔ v ∈ G₂.bbZAncestors ZProof (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⟩ -
uAdj_edge_congrtheorem — Graphs with the same directed edges have exactly the same undirected adjacencies.hypothesesconclusionG₁.UAdj u v ↔ G₂.UAdj u vProof (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_congrtheorem — Graphs with the same directed edges have exactly the same collider triples.hypothesesconclusionG₁.IsCollider l m r ↔ G₂.IsCollider l m rProof (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) -
isActivePath_edge_congrtheorem — Graphs with the same directed edges have exactly the same active paths for every conditioning set.hypothesesconclusionG₁.IsActivePath Z p ↔ G₂.IsActivePath Z pProof (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 -
hasActivePath_edge_congrtheorem — Equal directed-edge relations preserve existence of active paths.hypothesesconclusionG₁.HasActivePath X Y Z ↔ G₂.HasActivePath X Y ZProof (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⟩ -
flipEdge_isCoveredEdge_backtheorem — In the flipped graph, the reversed edge b → a is covered.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 -
flipEdge_flipEdge_edgetheorem — Flipping the reversed covered edge restores the original edge relation.hypothesesa b :Vhcov :G.IsCoveredEdge a bconclusionProof (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 -
isAncestor_lasttheorem — 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.hypothesesconclusionH.edge u v ∨ ∃ w, H.isAncestor u w ∧ H.edge w vProof (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_deletedtheorem — 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.hypothesesa b u w :Vhcov :G.IsCoveredEdge a bhe :G.edge u wProof (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⟩) -
bbZAncestors_flip_of_netheorem — 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.hypothesesconclusionv ∈ G.bbZAncestors Z → v ∈ (flipEdge hcov).bbZAncestors ZProof (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⟩ -
isActivePath_flip_of_not_memtheorem — 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.hypothesesconclusion(flipEdge hcov).IsActivePath Z pProof (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
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).
The directed edges of G₁ that are absent from G₂.
Number of directed edges of G₁ absent from G₂.
Definition (Lean source)
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
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.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) -
same_edge_of_edgeDiff_emptytheorem — With a one-way skeleton inclusion, an empty edge-difference forces equal edge relations.hypothesesconclusion∀ u w, G₁.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
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.
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)
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)
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)
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
Proof (Lean source)
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
Proof (Lean source)
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 public umbrella theorem is assembled through
the covered-edge route in Transfer.lean and Decompose.lean.
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.
A single moral step inside S that avoids the conditioning set Z (both endpoints outside Z).
Moral connectivity: u reaches v by a (possibly empty) sequence of moral steps inside S, every vertex of which avoids Z.
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.
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
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.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_steptheorem — A single moral step yields moral connectivity.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 -
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.hypothesesconclusionG.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.hypothesesS 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⟩ -
activePath_head_uAdjtheorem — In an active path whose first two vertices are a and b, those vertices are adjacent in the underlying undirected graph.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 -
activePath_head_tripletheorem — 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.hypothesesconclusionif G.IsCollider a b c then b ∈ G.bbZAncestors Z else b ∉ ZProof (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 -
activePath_drop2theorem — Removing the first two vertices of an active path with at least three vertices leaves an active path.hypothesesconclusionG.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) -
moralConn_of_activePaththeorem — An active path inside a ground set, with endpoints outside the conditioning set, induces a connection in the corresponding moral graph.hypothesesS Z :Finset Vconclusion∀ (n : ℕ) {x y : V} {p : List V}ifp.length ≤ nandG.IsActivePath Z pandp.length ≥ 2and(∀ v ∈ p, v ∈ S)andp.head? = some xandp.getLast? = some yandx ∉ Zandy ∉ ZthenG.MoralConn S Z x yProof (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 -
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.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 -
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 — 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.)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 · 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.
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)
10 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 path regardless of the conditioning set.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) -
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.hypothesesx y :Vhlt :G.topoOrder x < G.topoOrder yhxy :¬ G.UAdj x yconclusionG.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 `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, 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 path between them.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)⟩ -
edge_to_later_of_nonCollidertheorem — On an unshielded two-edge triple, a non-collider middle vertex points into the topologically later endpoint.hypothesesx 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_edge_selftheorem — No self-loops in a DAG.hypothesesa :Vconclusion¬ G.edge a aProof (Lean source)
theorem not_edge_self (a : V) : ¬ G.edge a a := fun he => G.isAncestor_irrefl a (DAG.isAncestor.edge he) -
not_uAdj_selftheorem — No self-adjacency in a DAG.hypothesesa :Vconclusion¬ G.UAdj a aProof (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) -
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).hypothesesa b :Vhne :a ≠ bh :¬ G.UAdj a bconclusion∃ Z : Finset V, a ∉ Z ∧ b ∉ Z ∧ G.dSep {a} {b} ZProof (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)⟩ -
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.)hypothesesa 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.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')) -
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))