Graph.MarkovEquiv.CoveredReversal
Reversing a covered edge of a directed acyclic graph does not change its d-separations.
FlipEdge 5 core · 6 supporting This file defines covered edges and constructs DAG.flipEdge by reversing one of them. ★ IsCoveredEdge★ flipEdge_sameImmoralities
Constructing a covered-edge reversal
This file defines covered edges and constructs DAG.flipEdge by reversing one of them. It proves
that the flipped relation is acyclic and that the resulting DAG has the same skeleton and
immoralities as the original graph. Active-walk transport across this construction is developed
in ActivePathTransport and PathSurgery; the resulting Markov-equivalence theorem is in
MarkovEquivalence.
For a finite directed acyclic graph on a vertex population and two vertices, the covered-edge condition holds exactly when there is a directed edge from the first vertex to the second and, for every other vertex, that vertex has an edge into the first if and only if it has an edge into the second.
Definition (Lean source)
For a finite directed acyclic graph on a vertex population and two vertices, the edge relation with one deletion holds for two queried vertices exactly when the original graph has an edge from the first queried vertex to the second and the queried edge is not the edge from the first specified vertex to the second.
Definition (Lean source)
For a finite directed acyclic graph on a vertex population and two vertices, the edge relation with one reversal holds for two queried vertices exactly when the original graph has the queried edge other than the edge from the first specified vertex to the second, or the queried edge is the reversed edge from the second specified vertex to the first.
Definition (Lean source)
For a finite vertex population, a directed acyclic graph, two vertices, and evidence that their directed edge is covered, the covered-edge reversal graph is the directed acyclic graph obtained by replacing the edge from the first vertex to the second with the edge from the second to the first.
Definition (Lean source)
For a finite directed acyclic graph and two vertices, if the directed edge between them is covered, then reversing that edge preserves every unshielded collider.
Formal statement
Proof (Lean source)
6 supporting declarations (lemmas, instances)
-
flipMinus_letheorem — A flipMinus-edge is in particular a G-edge.Proof (Lean source)
theorem flipMinus_le {a b u w : V} (h : G.flipMinus a b u w) : G.edge u w := h.1 -
topoOrder_lt_of_flipMinus_transGentheorem — A directed flipMinus-path strictly increases the topological order.hypothesesconclusionG.topoOrder u < G.topoOrder wProof (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) -
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.hypothesesconclusion¬ TransGen (G.flipRel a b) v vProof (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.hypothesesProof (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
ActivePathTransport 5 core · 26 supporting This file develops the local invariance facts used to transport active walks across DAG.flipEdge. ★ isActiveWalk_flip_of_not_mem
Active-walk transport ingredients for covered-edge reversal
This file develops the local invariance facts used to transport active walks across
DAG.flipEdge. It relates d-connection to active walks, proves edge-relation congruence lemmas,
tracks ancestry through a covered reversal, and defines swapPath together with the obstruction
and list-surgery operations used by PathSurgery. The final transport argument and
markovEquiv_flipEdge are in MarkovEquivalence.
For a DAG, edge endpoints, conditioning set, and walk, if the edge is covered, the walk is active, and the walk omits the reversed edge's tail, then that walk remains active after the covered reversal.
Formal statement
Proof (Lean source)
The walk p with each interior collider occurrence of a whose activation is lost in the flipped graph (a ∉ (flipEdge).bbZAncestors Z) replaced by b. Same length as p; endpoints are never touched (they are not interior).
Definition (Lean source)
The (l, m, r) configurations that the covered flip turns into an un-repairable block. Each is an a/b adjacency whose far arrowhead either makes a an inactive collider after the flip (clauses 1–2) or destroys the active collider at b (clauses 3–4). The two backtracks b — a — b and a — b — a are special cases (the first via clause 1 with r = b, the second via clause 4 with l = a, since a → b). swapPath cannot repair these locally; the assembly removes them by dropping/excising one vertex before swapping.
Definition (Lean source)
A list of values and an index determine the list of length one less whose entries before the index are unchanged and whose later entries are shifted forward by one.
A list of values and an index determine the list of length two less whose entries before the index are unchanged and whose later entries are shifted forward by two.
26 supporting declarations (lemmas, instances)
-
not_dSep_iff_hasActiveWalktheorem — Bridge: failure of d-separation is exactly an active walk. For pairwise-disjoint query sets, X and Y are *not* d-separated by Z iff there is an active walk from X to Y given Z. Assembled from bbReachableVertices_iff_activeWalk. Reduces the covered-flip invariance to a pure active-walk statement.hypothesesV :sharedType u_1H :DAG VX Y Z :Finset VhXY :Disjoint X YhXZ :Disjoint X ZhYZ :Disjoint Y Zconclusion¬ H.dSep X Y Z ↔ H.HasActiveWalk X Y ZProof (Lean source)
theorem not_dSep_iff_hasActiveWalk (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.HasActiveWalk X Y Z := by unfold DAG.dSep DAG.HasActiveWalk 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_activeWalk 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_activeWalk] 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) -
isActiveWalk_edge_congrtheorem — Graphs with the same directed edges have exactly the same active walks for every conditioning set.hypothesesconclusionG₁.IsActiveWalk Z p ↔ G₂.IsActiveWalk Z pProof (Lean source)
theorem isActiveWalk_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₁.IsActiveWalk Z p ↔ G₂.IsActiveWalk 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 -
hasActiveWalk_edge_congrtheorem — Equal directed-edge relations preserve existence of active walks.hypothesesconclusionG₁.HasActiveWalk X Y Z ↔ G₂.HasActiveWalk X Y ZProof (Lean source)
theorem hasActiveWalk_edge_congr {G₁ G₂ : DAG V} (he : ∀ u w : V, G₁.edge u w ↔ G₂.edge u w) (X Y Z : Finset V) : G₁.HasActiveWalk X Y Z ↔ G₂.HasActiveWalk X Y Z := by constructor · rintro ⟨p, hlen, hact, hhead, hlast⟩ exact ⟨p, hlen, (isActiveWalk_edge_congr he Z p).mp hact, hhead, hlast⟩ · rintro ⟨p, hlen, hact, hhead, hlast⟩ exact ⟨p, hlen, (isActiveWalk_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.hypothesesconclusionProof (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.hypothesesh :H.isAncestor u vconclusionH.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.hypothesesProof (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 ∈ (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⟩ -
bbZAncestors_flip_of_btheorem — The vertex b keeps its Bayes-ball ancestor-set membership after flipping a → b.hypothesesconclusionb ∈ (flipEdge hcov).bbZAncestors ZProof (Lean source)
theorem bbZAncestors_flip_of_b {a b : V} (hcov : G.IsCoveredEdge a b) (Z : Finset V) : b ∈ G.bbZAncestors Z → b ∈ (flipEdge hcov).bbZAncestors Z := bbZAncestors_flip_of_ne hcov Z hcov.ne.symm -
bbZAncestors_flip_of_losttheorem — If a reaches Z in G but not after the flip, that route used a → b, so b reaches Z; hence b is activated in the flipped graph.hypotheseshcov :G.IsCoveredEdge a bZ :Finset Vh1 :a ∈ G.bbZAncestors Zh2 :a ∉ (flipEdge hcov).bbZAncestors Zconclusionb ∈ (flipEdge hcov).bbZAncestors ZProof (Lean source)
theorem bbZAncestors_flip_of_lost {a b : V} (hcov : G.IsCoveredEdge a b) (Z : Finset V) (h1 : a ∈ G.bbZAncestors Z) (h2 : a ∉ (flipEdge hcov).bbZAncestors Z) : b ∈ (flipEdge hcov).bbZAncestors Z := by apply bbZAncestors_flip_of_b hcov Z simp only [bbZAncestors, ancestralSet, ancestorsSet, mem_union, mem_filter, Finset.mem_univ, true_and] at h1 ⊢ have hmemflip : ∀ {w : V}, (∃ z ∈ Z, (flipEdge hcov).isAncestor w z) → w ∈ (flipEdge hcov).bbZAncestors Z := by intro w hw simp only [bbZAncestors, ancestralSet, ancestorsSet, mem_union, mem_filter, Finset.mem_univ, true_and] exact Or.inr hw have haZ : a ∉ Z := fun h => h2 (hmemflip ⟨a, h, isAncestor.edge (by rw [flipEdge_edge]; exact Or.inr ⟨rfl, rfl⟩)⟩ |> fun _ => by simp only [bbZAncestors, ancestralSet, ancestorsSet, mem_union, mem_filter, Finset.mem_univ, true_and]; exact inl h) rcases h1 with hZ | ⟨z, hzZ, haz⟩ · exact absurd hZ haZ · rcases G.isAncestor_child haz with hedge | ⟨c, hac, hcz⟩ · by_cases hzb : z = b · exact inl (hzb ▸ hzZ) · exact absurd (hmemflip ⟨z, hzZ, isAncestor.edge (by rw [flipEdge_edge]; exact inl ⟨hedge, fun h => hzb h.2⟩)⟩) h2 · by_cases hcb : c = b · subst hcb; exact Or.inr ⟨z, hzZ, hcz⟩ · have hca : c ≠ a := fun h => G.irrefl a (h ▸ hac) have hacF : (flipEdge hcov).edge a c := by rw [flipEdge_edge]; exact inl ⟨hac, fun h => hcb h.2⟩ exact absurd (hmemflip ⟨z, hzZ, (flipEdge hcov).isAncestor_trans (isAncestor.edge hacF) (isAncestor_flip hcov hca hcz)⟩) h2 -
flipEdge_edge_iff_of_ne_atheorem — Away from a, the covered flip leaves directed edges unchanged.hypothesesProof (Lean source)
theorem flipEdge_edge_iff_of_ne_a {a b u w : V} (hcov : G.IsCoveredEdge a b) (hua : u ≠ a) (hwa : w ≠ a) : (flipEdge hcov).edge u w ↔ G.edge u w := by rw [flipEdge_edge] constructor · rintro (⟨he, _⟩ | ⟨_, hwa'⟩) · exact he · exact absurd hwa' hwa · intro he exact inl ⟨he, fun hdel => hua hdel.1⟩ -
swapPath_lengththeorem — For a directed acyclic graph, a covered edge, a conditioning set, and a walk, replacing affected collider occurrences along the walk preserves its length.hypotheses -
swapPath_get_eqtheorem — Each entry of swapPath is either the original vertex, or b at an interior collider occurrence of a whose activation is lost.Proof (Lean source)
theorem swapPath_get_eq {a b : V} (hcov : G.IsCoveredEdge a b) (Z : Finset V) (p : List V) (i : ℕ) (hi : i < (swapPath hcov Z p).length) (hi' : i < p.length) : (swapPath hcov Z p).get ⟨i, hi⟩ = p.get ⟨i, hi'⟩ ∨ ((swapPath hcov Z p).get ⟨i, hi⟩ = b ∧ p.get ⟨i, hi'⟩ = a ∧ 1 ≤ i ∧ ∃ (h2 : i + 1 < p.length), G.IsCollider (p.get ⟨i - 1, by omega⟩) (p.get ⟨i, hi'⟩) (p.get ⟨i + 1, h2⟩) ∧ a ∉ (flipEdge hcov).bbZAncestors Z) := by simp only [swapPath, List.get_ofFn, Fin.cast_mk] split_ifs with h hc · exact inr ⟨rfl, hc.1, h.1, h.2, hc.2.2, hc.2.1⟩ · exact inl rfl · exact inl rfl -
swapPath_get_b_oftheorem — When the swap predicate holds at i, the entry is b.hypotheseshcov :G.IsCoveredEdge a bZ :Finset Vp :List Vi :ℕhi' :i < p.lengthh1 :1 ≤ ih2 :i + 1 < p.lengthha :p.get ⟨i, hi'⟩ = ahanc :a ∉ (flipEdge hcov).bbZAncestors Zhcol :G.IsCollider (p.get ⟨i - 1, by omega⟩) (p.get ⟨i, hi'⟩) (p.get ⟨i + 1, h2⟩)Proof (Lean source)
theorem swapPath_get_b_of {a b : V} (hcov : G.IsCoveredEdge a b) (Z : Finset V) (p : List V) (i : ℕ) (hi : i < (swapPath hcov Z p).length) (hi' : i < p.length) (h1 : 1 ≤ i) (h2 : i + 1 < p.length) (ha : p.get ⟨i, hi'⟩ = a) (hanc : a ∉ (flipEdge hcov).bbZAncestors Z) (hcol : G.IsCollider (p.get ⟨i - 1, by omega⟩) (p.get ⟨i, hi'⟩) (p.get ⟨i + 1, h2⟩)) : (swapPath hcov Z p).get ⟨i, hi⟩ = b := by simp only [swapPath, List.get_ofFn, Fin.cast_mk] rw [dif_pos ⟨h1, h2⟩, if_pos ⟨ha, hanc, hcol⟩] -
flipEdge_edge_into_ifftheorem — An edge into a vertex m ∉ {a, b} is unaffected by the flip.hypothesesProof (Lean source)
theorem flipEdge_edge_into_iff {a b m : V} (hcov : G.IsCoveredEdge a b) (hma : m ≠ a) (hmb : m ≠ b) (u : V) : (flipEdge hcov).edge u m ↔ G.edge u m := by rw [flipEdge_edge] refine ⟨fun h => h.elim (·.1) (fun hh => absurd hh.2 hma), fun h => inl ⟨h, fun hh => hmb hh.2⟩⟩ -
bbZAncestors_of_edgetheorem — Ancestor-set membership moves backwards along a directed edge.hypothesesconclusionu ∈ G.bbZAncestors ZProof (Lean source)
theorem bbZAncestors_of_edge {Z : Finset V} {u w : V} (huw : G.edge u w) (hw : w ∈ G.bbZAncestors Z) : u ∈ G.bbZAncestors Z := by simp only [bbZAncestors, ancestralSet, ancestorsSet, mem_union, mem_filter, Finset.mem_univ, true_and] at hw ⊢ rcases hw with hwZ | ⟨z, hzZ, hwz⟩ · exact inr ⟨w, hwZ, isAncestor.edge huw⟩ · exact inr ⟨z, hzZ, G.isAncestor_trans (isAncestor.edge huw) hwz⟩ -
skipOne_lengththeorem — For a list of values and an index, skipping one entry reduces the length by one.Proof (Lean source)
@[simp] theorem skipOne_length (p : List V) (k : ℕ) : (skipOne p k).length = p.length - 1 := by simp [skipOne] -
skipTwo_lengththeorem — For a list of values and an index, skipping two entries reduces the length by two.Proof (Lean source)
@[simp] theorem skipTwo_length (p : List V) (k : ℕ) : (skipTwo p k).length = p.length - 2 := by simp [skipTwo] -
skipOne_head?theorem — If an index is positive and lies within a list of values, then skipping that entry preserves the first entry.hypothesesconclusion(skipOne p k).head? = p.head?Proof (Lean source)
theorem skipOne_head? {p : List V} {k : ℕ} (hk0 : 0 < k) (hk : k < p.length) : (skipOne p k).head? = p.head? := by rw [List.head?_eq_getElem?, List.head?_eq_getElem?] have hp0 : 0 < p.length := by omega have hq0 : 0 < (skipOne p k).length := by simp [skipOne_length]; omega rw [List.getElem?_eq_getElem hq0, List.getElem?_eq_getElem hp0] simp [skipOne, hk0] -
skipOne_getLast?theorem — If an index and its successor lie before the end of a list of values, then skipping that entry preserves the last entry.hypothesesProof (Lean source)
theorem skipOne_getLast? {p : List V} {k : ℕ} (hk : k + 1 < p.length) : (skipOne p k).getLast? = p.getLast? := by rw [getLast?_eq_getElem?, getLast?_eq_getElem?, skipOne_length] have hp0 : 0 < p.length := by omega rw [List.getElem?_eq_getElem (by rw [skipOne_length]; omega), List.getElem?_eq_getElem (by omega)] have hnot : ¬ (p.length - 1 - 1 < k) := by omega simp [skipOne, hnot, show p.length - 1 - 1 + 1 = p.length - 1 by omega] -
skipTwo_head?theorem — If an index is positive and its successor lies before the end of a list of values, then skipping the two entries beginning there preserves the first entry.hypothesesconclusion(skipTwo p k).head? = p.head?Proof (Lean source)
theorem skipTwo_head? {p : List V} {k : ℕ} (hk0 : 0 < k) (hk : k + 1 < p.length) : (skipTwo p k).head? = p.head? := by rw [List.head?_eq_getElem?, List.head?_eq_getElem?] have hp0 : 0 < p.length := by omega have hq0 : 0 < (skipTwo p k).length := by simp [skipTwo_length]; omega rw [List.getElem?_eq_getElem hq0, List.getElem?_eq_getElem hp0] simp [skipTwo, hk0] -
skipTwo_getLast?theorem — If a positive index and its successor lie before the end of a list of values and the entries immediately before and after the index agree, then skipping the two entries beginning at the index preserves the last entry.hypothesesk :ℕhk0 :0 < khk :k + 1 < p.lengthhsame :p.get ⟨k - 1, by omega⟩ = p.get ⟨k + 1, hk⟩Proof (Lean source)
theorem skipTwo_getLast? {p : List V} {k : ℕ} (hk0 : 0 < k) (hk : k + 1 < p.length) (hsame : p.get ⟨k - 1, by omega⟩ = p.get ⟨k + 1, hk⟩) : (skipTwo p k).getLast? = p.getLast? := by rw [getLast?_eq_getElem?, getLast?_eq_getElem?, skipTwo_length] have hp0 : 0 < p.length := by omega rw [List.getElem?_eq_getElem (by rw [skipTwo_length]; omega), List.getElem?_eq_getElem (by omega)] by_cases htail : k + 2 < p.length · have hnot : ¬ (p.length - 2 - 1 < k) := by omega simp [skipTwo, hnot, show p.length - 2 - 1 + 2 = p.length - 1 by omega] · have hk_last : k + 1 = p.length - 1 := by omega have hq_last : p.length - 2 - 1 = k - 1 := by omega have htrue : k - 1 < k := by omega simpa [skipTwo, hq_last, hk_last, htrue] using hsame
MarkovEquivalence 1 core · 0 supporting This file assembles the active-walk transport machinery into markovEquiv_flipEdge. ★ markovEquiv_flipEdge
Markov equivalence under one covered-edge reversal
This file assembles the active-walk transport machinery into markovEquiv_flipEdge. Obstructing
triples are removed by strictly shortening the walk, while unobstructed walks are handled by
swapPath. Applying the argument in both directions shows that reversing one covered edge
preserves every d-separation statement. The finite-chain decomposition that uses this result is
in Causalean.Graph.MarkovEquiv.Decompose.
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)
PathSurgery 1 core · 5 supporting This file proves that the list operations from ActivePathTransport preserve active walks under the required splice conditions. ★ isActiveWalk_swapPath_of_no_obstruct
Active-walk surgery for covered-edge reversal
This file proves that the list operations from ActivePathTransport preserve active walks under
the required splice conditions. It also shows that swapPath preserves endpoints and yields an
active walk when no obstructing triple occurs. MarkovEquivalence uses these results to remove
obstructions and transport an arbitrary active walk across a covered-edge reversal.
For a finite directed acyclic graph, the endpoints of a directed edge, a conditioning set, and a walk, if the directed edge is covered, the original walk is active, and none of its consecutive interior triples obstructs the reversal, then replacing the affected collider occurrences yields an active walk after the covered-edge reversal.
Formal statement
Proof (Lean source)
5 supporting declarations (lemmas, instances)
-
isActiveWalk_skipOnetheorem — Given a directed acyclic graph, conditioning set, walk, and index, if the walk is active, the index is positive, its successor lies inside the walk, the vertices on either side are adjacent, and the newly formed left and right triples satisfy the active-walk criterion, then removing the indexed vertex leaves an active walk.hypothesesk :ℕhact :G.IsActiveWalk Z phk0 :0 < khk :k + 1 < p.lengthhadj_splice :G.UAdj (p.get ⟨k - 1, by omega⟩) (p.get ⟨k + 1, hk⟩)hleft :∀ (hk2 : 2 ≤ k),if G.IsCollider (p.get ⟨k - 2, by omega⟩) (p.get ⟨k - 1, by omega⟩) (p.get ⟨k + 1, hk⟩) then p.get ⟨k- 1, by omega⟩ ∈ G.bbZAncestors Z else p.get ⟨k- 1, by omega⟩ ∉ Zhright :∀ (hkR : k + 2 < p.length),if G.IsCollider (p.get ⟨k - 1, by omega⟩) (p.get ⟨k + 1, by omega⟩) (p.get ⟨k + 2, hkR⟩) then p.get ⟨k+ 1, by omega⟩ ∈ G.bbZAncestors Z else p.get ⟨k+ 1, by omega⟩ ∉ ZconclusionG.IsActiveWalk Z (skipOne p k)Proof (Lean source)
theorem isActiveWalk_skipOne {Z : Finset V} {p : List V} {k : ℕ} (hact : G.IsActiveWalk Z p) (hk0 : 0 < k) (hk : k + 1 < p.length) (hadj_splice : G.UAdj (p.get ⟨k - 1, by omega⟩) (p.get ⟨k + 1, hk⟩)) (hleft : ∀ (hk2 : 2 ≤ k), if G.IsCollider (p.get ⟨k - 2, by omega⟩) (p.get ⟨k - 1, by omega⟩) (p.get ⟨k + 1, hk⟩) then p.get ⟨k - 1, by omega⟩ ∈ G.bbZAncestors Z else p.get ⟨k - 1, by omega⟩ ∉ Z) (hright : ∀ (hkR : k + 2 < p.length), if G.IsCollider (p.get ⟨k - 1, by omega⟩) (p.get ⟨k + 1, by omega⟩) (p.get ⟨k + 2, hkR⟩) then p.get ⟨k + 1, by omega⟩ ∈ G.bbZAncestors Z else p.get ⟨k + 1, by omega⟩ ∉ Z) : G.IsActiveWalk Z (skipOne p k) := by obtain ⟨hadj, htri⟩ := hact refine ⟨fun i hi => ?_, fun i hi => ?_⟩ · simp only [skipOne_length] at hi simp only [skipOne, List.get_ofFn] by_cases hi0 : i < k · by_cases hi1 : i + 1 < k · have h := hadj i (by omega) simpa [hi0, hi1] using h · have hik : i = k - 1 := by omega subst i have h0 : k - 1 < k := by omega have h1 : ¬ (k - 1 + 1 < k) := by omega simpa [h0, h1, show k - 1 + 1 + 1 = k + 1 by omega] using hadj_splice · have hi1 : ¬ (i + 1 < k) := by omega have h := hadj (i + 1) (by omega) simpa [hi0, hi1, Nat.add_assoc] using h · simp only [skipOne_length] at hi simp only [skipOne, List.get_ofFn] by_cases hi2 : i + 2 < k · have hi0 : i < k := by omega have hi1 : i + 1 < k := by omega have h := htri i (by omega) simpa [hi0, hi1, hi2] using h · by_cases hi1 : i + 1 < k · have hi0 : i < k := by omega have hik : i = k - 2 := by omega subst i have h0 : k - 2 < k := by omega have h1 : k - 2 + 1 < k := by omega have h2 : ¬ (k - 2 + 2 < k) := by omega have hmid : k - 1 < k := by omega simpa [h0, h1, h2, show k - 2 + 1 = k - 1 by omega, show k - 2 + 2 + 1 = k + 1 by omega, hmid] using hleft (by omega) · by_cases hi0 : i < k · have hik : i = k - 1 := by omega subst i have h0 : k - 1 < k := by omega have h1 : ¬ (k - 1 + 1 < k) := by omega have h2 : ¬ (k - 1 + 2 < k) := by omega have hrightNot : ¬ (k + 1 < k) := by omega simpa [h0, h1, h2, show k - 1 + 1 + 1 = k + 1 by omega, show k - 1 + 2 + 1 = k + 2 by omega, hrightNot] using hright (by omega) · have hi1' : ¬ (i + 1 < k) := by omega have hi2' : ¬ (i + 2 < k) := by omega have h := htri (i + 1) (by omega) simpa [hi0, hi1', hi2', Nat.add_assoc] using h -
isActiveWalk_skipTwotheorem — Given a directed acyclic graph, conditioning set, walk, and index, if the walk is active, the index is positive, its successor lies inside the walk, the vertices immediately before and after it agree, and the triple created by deleting two entries satisfies the active-walk criterion, then the shortened walk is active.hypothesesk :ℕhact :G.IsActiveWalk Z phk0 :0 < khk :k + 1 < p.lengthhsame :p.get ⟨k - 1, by omega⟩ = p.get ⟨k + 1, hk⟩hsplice :∀ (hk2 : 2 ≤ k) (hkR : k + 2 < p.length),if G.IsCollider (p.get ⟨k - 2, by omega⟩) (p.get ⟨k - 1, by omega⟩) (p.get ⟨k + 2, hkR⟩) then p.get ⟨k- 1, by omega⟩ ∈ G.bbZAncestors Z else p.get ⟨k- 1, by omega⟩ ∉ ZconclusionG.IsActiveWalk Z (skipTwo p k)Proof (Lean source)
theorem isActiveWalk_skipTwo {Z : Finset V} {p : List V} {k : ℕ} (hact : G.IsActiveWalk Z p) (hk0 : 0 < k) (hk : k + 1 < p.length) (hsame : p.get ⟨k - 1, by omega⟩ = p.get ⟨k + 1, hk⟩) (hsplice : ∀ (hk2 : 2 ≤ k) (hkR : k + 2 < p.length), if G.IsCollider (p.get ⟨k - 2, by omega⟩) (p.get ⟨k - 1, by omega⟩) (p.get ⟨k + 2, hkR⟩) then p.get ⟨k - 1, by omega⟩ ∈ G.bbZAncestors Z else p.get ⟨k - 1, by omega⟩ ∉ Z) : G.IsActiveWalk Z (skipTwo p k) := by obtain ⟨hadj, htri⟩ := hact refine ⟨fun i hi => ?_, fun i hi => ?_⟩ · simp only [skipTwo_length] at hi simp only [skipTwo, List.get_ofFn] by_cases hi0 : i < k · by_cases hi1 : i + 1 < k · have h := hadj i (by omega) simpa [hi0, hi1] using h · have hik : i = k - 1 := by omega subst i have h0 : k - 1 < k := by omega have h1 : ¬ (k - 1 + 1 < k) := by omega have h := hadj (k + 1) (by omega) rw [← hsame] at h simpa [h0, h1, Nat.add_assoc, show k - 1 + 3 = k + 2 by omega] using h · have hi1 : ¬ (i + 1 < k) := by omega have h := hadj (i + 2) (by omega) simpa [hi0, hi1, Nat.add_assoc] using h · simp only [skipTwo_length] at hi simp only [skipTwo, List.get_ofFn] by_cases hi2 : i + 2 < k · have hi0 : i < k := by omega have hi1 : i + 1 < k := by omega have h := htri i (by omega) simpa [hi0, hi1, hi2] using h · by_cases hi1 : i + 1 < k · have hi0 : i < k := by omega have hik : i = k - 2 := by omega subst i have h0 : k - 2 < k := by omega have h1 : k - 2 + 1 < k := by omega have h2 : ¬ (k - 2 + 2 < k) := by omega have hmid : k - 1 < k := by omega simpa [h0, h1, h2, show k - 2 + 1 = k - 1 by omega, show k - 2 + 2 + 2 = k + 2 by omega, hmid] using hsplice (by omega) (by omega) · by_cases hi0 : i < k · have hik : i = k - 1 := by omega subst i have h0 : k - 1 < k := by omega have h1 : ¬ (k - 1 + 1 < k) := by omega have h2 : ¬ (k - 1 + 2 < k) := by omega have h := htri (k + 1) (by omega) rw [← hsame] at h simpa [h0, h1, h2, Nat.add_assoc, show k - 1 + 3 = k + 2 by omega, show k - 1 + 4 = k + 3 by omega] using h · have hi1' : ¬ (i + 1 < k) := by omega have hi2' : ¬ (i + 2 < k) := by omega have h := htri (i + 2) (by omega) simpa [hi0, hi1', hi2', Nat.add_assoc] using h -
four_le_length_of_backtrack_endpoints_netheorem — If a list of values has distinct first and last entries, an index has a second successor in the list, and the entries at the index and second successor agree, then the list has at least four entries.hypothesesj :ℕhne :p.head? ≠ p.getLast?hj :j + 2 < p.lengthhback :p.get ⟨j, by omega⟩ = p.get ⟨j + 2, hj⟩conclusion4 ≤ p.lengthProof (Lean source)
theorem four_le_length_of_backtrack_endpoints_ne {p : List V} {j : ℕ} (hne : p.head? ≠ p.getLast?) (hj : j + 2 < p.length) (hback : p.get ⟨j, by omega⟩ = p.get ⟨j + 2, hj⟩) : 4 ≤ p.length := by by_contra hlt have hlen : p.length = 3 := by omega have hj0 : j = 0 := by omega apply hne rw [List.head?_eq_getElem?, getLast?_eq_getElem?] rw [List.getElem?_eq_getElem (by omega), List.getElem?_eq_getElem (by omega)] congr 1 simpa [hj0, hlen] using hback -
swapPath_head?theorem — swapPath never touches the first vertex, so it preserves the head.hypothesesconclusion(swapPath hcov Z p).head? = p.head?Proof (Lean source)
theorem swapPath_head? {a b : V} (hcov : G.IsCoveredEdge a b) (Z : Finset V) (p : List V) : (swapPath hcov Z p).head? = p.head? := by rw [List.head?_eq_getElem?, List.head?_eq_getElem?] rcases Nat.eq_zero_or_pos p.length with h | h · rw [List.getElem?_eq_none (by rw [swapPath_length]; omega), List.getElem?_eq_none (by omega)] · rw [List.getElem?_eq_getElem (by rw [swapPath_length]; exact h), List.getElem?_eq_getElem h] congr 1 rcases swapPath_get_eq hcov Z p 0 (by rw [swapPath_length]; exact h) h with hh | ⟨_, _, h1, _⟩ · simpa using hh · omega -
swapPath_getLast?theorem — swapPath never touches the last vertex, so it preserves the last element.hypothesesProof (Lean source)
theorem swapPath_getLast? {a b : V} (hcov : G.IsCoveredEdge a b) (Z : Finset V) (p : List V) : (swapPath hcov Z p).getLast? = p.getLast? := by rw [getLast?_eq_getElem?, getLast?_eq_getElem?, swapPath_length] rcases Nat.eq_zero_or_pos p.length with h | h · rw [List.getElem?_eq_none (by rw [swapPath_length]; omega), List.getElem?_eq_none (by omega)] · rw [List.getElem?_eq_getElem (by rw [swapPath_length]; omega), List.getElem?_eq_getElem (by omega)] congr 1 rcases swapPath_get_eq hcov Z p (p.length - 1) (by rw [swapPath_length]; omega) (by omega) with hh | ⟨_, _, _, h2, _⟩ · simpa using hh · omega