From 64cfbf776d7e548fab253b74d1a51c918bf5d0ab Mon Sep 17 00:00:00 2001 From: Xinze-Li-Moqian <70414198+Xinze-Li-Moqian@users.noreply.github.com> Date: Tue, 26 May 2026 20:55:53 -0400 Subject: [PATCH 01/21] chore: remove informal/ from main Informal math notes belong on the development branch, not in the production-grade main. Files preserved on development. --- lean/informal/bdf_doyle_snell_bridge.md | 696 ------------------ lean/informal/deeper_theorem_candidates.md | 266 ------- lean/informal/markov_doyle_snell.md | 225 ------ lean/informal/paper2_dR_hyperbolicity_TODO.md | 251 ------- .../informal/semantic_BDFmetric_definition.md | 204 ----- lean/informal/tensor_representation_notes.md | 134 ---- lean/informal/thomson_principle.md | 189 ----- 7 files changed, 1965 deletions(-) delete mode 100644 lean/informal/bdf_doyle_snell_bridge.md delete mode 100644 lean/informal/deeper_theorem_candidates.md delete mode 100644 lean/informal/markov_doyle_snell.md delete mode 100644 lean/informal/paper2_dR_hyperbolicity_TODO.md delete mode 100644 lean/informal/semantic_BDFmetric_definition.md delete mode 100644 lean/informal/tensor_representation_notes.md delete mode 100644 lean/informal/thomson_principle.md diff --git a/lean/informal/bdf_doyle_snell_bridge.md b/lean/informal/bdf_doyle_snell_bridge.md deleted file mode 100644 index 0238f08..0000000 --- a/lean/informal/bdf_doyle_snell_bridge.md +++ /dev/null @@ -1,696 +0,0 @@ -# Informal: BDF Doyle-Snell bridge (`Metric/Resistance.lean:70`) - -**Target.** Close the sorry - -```lean -theorem commuteTimeDist_eq_effectiveResistance (H : Hypergraph V C) - [Fintype H.edges] (params : WalkParams C) (u v : V) : - H.commuteTimeDist params u v - = (H.commuteTimeHypergraphMetric params).d u v := sorry -``` - -i.e. - -``` -h(u, v) + h(v, u) - = effectiveResistance (commuteTimeHypergraphMetric.laplacian) u v -``` - -where the LHS is the BDF random walk's commute time and the RHS is the -effective resistance of the **HypergraphMetric Laplacian** (built from the -symmetric harmonic `commuteTimeKappa`). - -## Why the current statement is under-specified - -Round 2 closed the abstract Markov-layer Doyle-Snell theorem - -```lean -theorem ProofAtlas.RandomWalk.commuteTime_eq_effectiveResistance - (h_row_sum) (h_stat) (h_sum) (h_pos) (h_inv) (h_rev) … : - h(u, v) + h(v, u) - = (∑ x y, conductance P π x y) - * effectiveResistance (conductanceLaplacian P π) u v -``` - -which depends on **reversibility** of `P` w.r.t. `π`, plus row-sum / -stationarity / strict positivity / auxiliary-matrix invertibility / -killed-matrix invertibility. Note the RHS uses the *Markov* -conductance Laplacian `L_C := D - C` with `C_{u,v} := π_u · P_{u,v}`, -not the *HypergraphMetric* Laplacian. - -The BDF chain is **NOT reversible** in general w.r.t. its natural -stationary distribution. The asymmetric (input → output) vs -(output → input) structure of `transProb`: - -* `P(input_i, output_j) = (1/Z(input_i)) · α / outArity(e)` -* `P(output_j, input_i) = (1/Z(output_j)) · α · β / ((i+1) · S_e)` - -forces detailed balance to require - -``` -π(input_i) / Z(input_i) = (1/(i+1)) · (constant per edge) -``` - -simultaneously for every edge incidence. Since the same vertex can be -input-with-index `i` for one edge and input-with-index `j` (with `i ≠ j`) -for another, this is generically impossible. - -Equally important: even granting reversibility of some sub-class of -BDF chains, the HypergraphMetric Laplacian (= Laplacian of the symmetric -`commuteTimeKappa`) is a **different matrix** from the Markov-layer -conductance Laplacian `L_C` (= Laplacian of `π · P`). They live on -the same vertex set but have different entries in general: - -* `(commuteTimeKappa).conductance(u, v)` is the symmetrised harmonic - weight `α/((i+1)·outArity)` per (input_i, output) pair. -* `(Markov.conductance P π).conductance(u, v) = π_u · P(u, v)` is the - detailed-balance flow. - -These can be made to agree only under a specific β-tuning that -balances the role weights, and then only up to a scalar. - -## Multi-round plan - -We split the bridge into three rounds. Round 3a is the **statement -amendment only** — the only round in scope right now. - -### Round 3a (THIS ROUND) — Statement amendment only - -Add the following hypotheses to the signature of -`commuteTimeDist_eq_effectiveResistance`, mirroring exactly the -hypothesis package of -`ProofAtlas.RandomWalk.commuteTime_eq_effectiveResistance`: - -```lean -theorem commuteTimeDist_eq_effectiveResistance [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (h_row_sum : ∀ i, ∑ j, H.transMatrix params i j = 1) - (h_stat : ∀ j, ∑ i, H.stationaryDist params i - * H.transMatrix params i j - = H.stationaryDist params j) - (h_sum : ∑ i, H.stationaryDist params i = 1) - (h_pos : ∀ w, 0 < H.stationaryDist params w) - (h_inv : IsUnit (ProofAtlas.RandomWalk.auxMatrix - (H.transMatrix params) - (H.stationaryDist params)).det) - (h_rev : H.IsReversible params) - {u v : V} - (h_killed_inv_u : IsUnit ((1 : Matrix V V ℝ) - - ProofAtlas.RandomWalk.killedMatrix - (H.transMatrix params) u)) - (h_killed_inv_v : IsUnit ((1 : Matrix V V ℝ) - - ProofAtlas.RandomWalk.killedMatrix - (H.transMatrix params) v)) : - H.commuteTimeDist params u v - = (H.commuteTimeHypergraphMetric params).d u v := sorry -``` - -Keep the body as `sorry`. The RHS continues to reference -`commuteTimeHypergraphMetric.d` — the gap between -`commuteTimeHypergraphMetric.laplacian` and -`Markov.conductanceLaplacian H.transMatrix H.stationaryDist` is the -content of Round 3b. - -**Scope discipline.** This is a **signature-only change**. Do not -attempt any proof. Do not introduce any new helper lemmas in this -round. Do not change anything outside lines 70–74 (the theorem -signature and its `sorry` body). - -**Required check after editing.** - -* `lake build` returns green. -* `#print axioms Hypergraph.probabilistic_hyperbolicity_bound` - remains `[propext, Classical.choice, Quot.sound]`. -* The Resistance.lean sorry count stays at `1` (the original sorry, - now under the new signature). - -### Round 3b (NEXT ROUND, plan-agent assigned) — Conductance bridge - -Prove a stand-alone lemma in `Metric/Resistance.lean` linking the two -Laplacians: - -```lean -lemma commuteTimeHypergraphMetric_laplacian_eq_markov_conductanceLaplacian - (H : Hypergraph V C) [Fintype H.edges] (params : WalkParams C) - (h_…) : - (H.commuteTimeHypergraphMetric params).laplacian - = ProofAtlas.RandomWalk.conductanceLaplacian - (H.transMatrix params) (H.stationaryDist params) -``` - -possibly up to a scalar `(2 · |E|)` normalisation. This requires -identifying which BDF parameter regime makes the two conductances -agree. The plan agent will pre-generate the informal proof of this -linkage (or its corrected form) before assigning Round 3b. - -### Round 3c — Final invocation - -Combine Round 3a's hypotheses + Round 3b's Laplacian bridge to -invoke `Markov.commuteTime_eq_effectiveResistance`, rewriting -`(H.commuteTimeHypergraphMetric params).d u v` via the Round 3b lemma and -`effectiveResistance`-functoriality / scalar handling. Short proof: -~10–20 lines. - -## What to look for in subsequent rounds - -* Whether the BDF chain admits a non-trivial reversible sub-class - (e.g., uniform-color hypergraphs, single-edge hypergraphs, …) where - `IsReversible` is automatic from the structure rather than an - external hypothesis. -* Whether `Markov.conductanceLaplacian` can be replaced by - `commuteTimeHypergraphMetric.laplacian` directly, possibly via a - reversibilisation argument à la Aldous-Fill §3.2 ("additive - reversibilisation" — `P̃ := (P + P*)/2` where `P*(u,v) := - (π_v/π_u) · P(v,u)` is the time-reversed chain; the additive - reversibilisation preserves commute time but produces a *different* - conductance Laplacian). - -These are research questions to expand in future plan iterations. - -## References - -* Aldous, Fill, *Reversible Markov Chains and Random Walks on Graphs*, - Ch. 2 §2.2, Ch. 3 §3.2 (reversibilisation). -* Doyle, Snell, *Random Walks and Electric Networks*, Ch. 3. -* Chandra, Raghavan, Ruzzo, Smolensky, Tiwari (1989), "The electrical - resistance of a graph captures its commute and cover times". - -## Round 3b — structural analysis (Round 3b-prep) - -This section is the analytical pre-work for Round 3b. **No `.lean` -changes are made in Round 3b-prep**; we extend this informal file -only. The goal is to identify, with full algebraic precision, what -the eventual Lean lemma - -```lean -(H.commuteTimeHypergraphMetric params).laplacian - = ProofAtlas.RandomWalk.conductanceLaplacian - (H.transMatrix params) (H.stationaryDist params) -``` - -(possibly modulo a scalar) must look like — and which BDF parameter -restriction or signature change is required to make it true. - -### 0. Notation - -Fix `H : Hypergraph V C`, `params : WalkParams C`, and an edge -`e : H.edges`. We write - -* `c := H.color e`, `α := params.alpha c`, `β := params.beta`. -* `p := H.inArity e`, `q := H.outArity e`. -* `S_e := H.harmonicNorm e = ∑_{j : Fin p} 1/(j+1)`. -* For `u ∈ V` with `u` an input of `e`: by `inVertex_injective`, - there is a unique `i_e(u) : Fin p` with `H.inVertex e (i_e(u)) = u`. -* For `u ∈ V`: `Z(u) := H.normalizer params u = - ∑_e [α · 1[u ∈ e^in] + α·β · 1[u ∈ e^out]]`, the row normaliser of - `transProb`. -* `π_u := H.stationaryDist params u`, `λ_u := π_u / Z(u)` when - `Z(u) ≠ 0` (we will need `Z(u) ≠ 0` and `π_u > 0` at every vertex - used in the bridge — both supplied by the Round 3a hypothesis - package). -* `Σ_e` ranges over `H.edges`, `Σ_i` over `Fin p` for the current edge. - -The two matrices we are comparing are: - -* `L_κ := (commuteTimeHypergraphMetric params).laplacian`, the **HypergraphMetric - Laplacian** of the symmetric harmonic conductance - `κ(u, v) := commuteTimeKappa e u v = ioWeight u v + ioWeight v u`. -* `L_C := conductanceLaplacian (transMatrix) (stationaryDist)`, the - **Markov-layer conductance Laplacian** with off-diagonal - `L_C(u, v) := -π_u · P(u, v)` for `u ≠ v`. - -We will derive both **per edge**: write `L_κ^e(u, v)` and -`L_C^e(u, v)` for the contribution of a single edge `e` to the -respective entry. Both are linear in `e` so `L_κ(u, v) = Σ_e L_κ^e(u, v)` -and `L_C(u, v) = Σ_e L_C^e(u, v)`. - -### 1. Per-edge `L_κ(u, v)` for `u ≠ v` - -By definition `L_κ(u, v) = -κ(u, v)` for `u ≠ v`, and -`κ(u, v) := Σ_e [ioWeight e u v + ioWeight e v u]`. So -`L_κ^e(u, v) = -[ioWeight e u v + ioWeight e v u]`. - -Recall `ioWeight e u v ≠ 0` iff `u ∈ e^in` (say `u = H.inVertex e i`, -with `i := i_e(u)`) **and** `v ∈ e^out`, in which case it equals -`α / ((i+1)·q)`. The four cases for an edge `e`: - -| `u ∈ e^in` at `i` | `v ∈ e^in` at `i'` | `u ∈ e^out` | `v ∈ e^out` | `L_κ^e(u, v)` | -|---|---|---|---|---| -| ✓ | — | — | ✓ | `-α / ((i+1)·q)` | -| — | ✓ | ✓ | — | `-α / ((i'+1)·q)` | -| ✓ | ✓ | — | — | `0` (both inputs ⇒ bipartite) | -| — | — | ✓ | ✓ | `0` (both outputs ⇒ bipartite) | -| else | else | else | else | `0` (no `ioWeight` term fires) | - -(Inputs and outputs of the same edge are disjoint, by -`inputOutputDisjoint`, so `u ∈ e^in ∧ u ∈ e^out` is excluded.) - -The first row is the **forward harmonic** contribution; the second -row is the **backward harmonic** contribution — identical shape -`α/((·+1)·q)` because `commuteTimeKappa` was deliberately -symmetrised. Crucially, **the harmonic factor `1/(i+1)` depends on -the input's position in the edge**, not on the vertex globally. - -### 2. Per-edge `L_C(u, v)` for `u ≠ v` - -By definition `L_C(u, v) = -C(u, v) = -π_u · P(u, v)` for `u ≠ v`, -where `P = H.transProb params`. Inspecting `transProb`: - -``` -P(u, v) · Z(u) = Σ_e [ - (forward) 1[u ∈ e^in ∧ v ∈ e^out] · α/q - + (backward) Σ_i 1[u ∈ e^out ∧ H.inVertex e i = v] · α·β / ((i+1)·S_e) -] -``` - -so `L_C^e(u, v) = -λ_u · [forward_e + backward_e]`. The four -non-vanishing cases: - -| `u ∈ e^in` at `i` | `v ∈ e^in` at `i'` | `u ∈ e^out` | `v ∈ e^out` | `L_C^e(u, v)` | -|---|---|---|---|---| -| ✓ | — | — | ✓ | `-λ_u · α/q` | -| — | ✓ | ✓ | — | `-λ_u · α·β / ((i'+1)·S_e)` | -| ✓ | ✓ | — | — | `0` (no transition allowed inside an edge's input set) | -| — | — | ✓ | ✓ | `0` (no transition allowed inside an edge's output set) | - -The "forward" row matches the harmonic shape only in `q`; the -**`1/(i+1)` factor is absent**. The "backward" row contains both -`1/(i'+1)` and `1/S_e`, but no `1/q`. - -### 3. Diagonal entries `L_κ(u, u)`, `L_C(u, u)` - -Both Laplacians have zero row sums. The diagonal of `L_κ` is -`L_κ(u, u) = Σ_w κ(u, w) = Σ_e Σ_w [ioWeight e u w + ioWeight e w u]`. -Per edge: - -* `Σ_w ioWeight e u w = α/(i+1)` when `u = H.inVertex e i` (and `0` - otherwise) — sum over the `q` outputs, each contributing - `α/((i+1)·q)`. Requires output-distinctness for the count to be - exactly `q`; we use `Function.Injective (H.outVertex e)`, which - the Round 3a-compatible bridge will also need (it appears in - `transProb_sum_one` already). -* `Σ_w ioWeight e w u = α · S_e / q` when `u` is an output of `e` - (and `0` otherwise) — sum over input positions `i`, each - contributing `α/((i+1)·q)`; the inputs are distinct by - `inVertex_injective` so the sum equals `Σ_i α/((i+1)·q) = - (α/q) · S_e`. - -So per edge: - -| role of `u` in `e` | `L_κ^e(u, u)` | -|---|---| -| input at position `i` (not output) | `α/(i+1)` | -| output (not input) | `α · S_e / q` | -| neither | `0` | - -For `L_C`: `Σ_v C(u, v) = π_u · Σ_v P(u, v) = π_u · 1 = π_u` -(stochasticity). Since `P(u, u) = 0` for every BDF chain (no -self-loops: a forward `(u, u)` self-loop would need `u ∈ e^in ∧ u ∈ -e^out`, forbidden; a backward self-loop the same), we get -`L_C(u, u) = π_u - C(u, u) = π_u - π_u·P(u, u) = π_u`. Splitting -by edge: `π_u = λ_u · Z(u) = λ_u · Σ_e [α · 1[u ∈ e^in] + α·β · -1[u ∈ e^out]]`. So: - -| role of `u` in `e` | `L_C^e(u, u)` | -|---|---| -| input (not output) | `λ_u · α` | -| output (not input) | `λ_u · α·β` | -| neither | `0` | - -### 4. Structural mismatch - -For per-edge equality `L_κ^e = L_C^e` we need four identities to -hold simultaneously (the forward/backward off-diagonal, and the -input/output diagonal cases). Stripping the common `-α` (off) / -`+α` (diag): - -* **(F)** Forward off-diag `(u = H.inVertex e i, v ∈ e^out)`: - `1/((i+1)·q) = λ_u / q`, i.e. `λ_u = 1/(i+1)`. -* **(B)** Backward off-diag `(u ∈ e^out, v = H.inVertex e i)`: - `1/((i+1)·q) = λ_u · β / ((i+1)·S_e)`, i.e. - `λ_u = S_e / (β · q)`. -* **(Din)** Input diagonal `(u = H.inVertex e i)`: - `1/(i+1) = λ_u`, i.e. `λ_u = 1/(i+1)`. (Same as (F).) -* **(Dout)** Output diagonal `(u ∈ e^out)`: - `S_e / q = λ_u · β`, i.e. `λ_u = S_e / (β · q)`. (Same as (B).) - -So the four conditions collapse to two: - -* **(I)** at every edge `e` where `u` is input at position `i_e(u)`: - `λ_u = 1/(i_e(u) + 1)`. -* **(O)** at every edge `e` where `u` is an output: - `λ_u = S_e / (β · q_e)`. - -`λ_u = π_u / Z(u)` is **one** real number per vertex. The -constraint (I) forces `λ_u` to take the value `1/(i+1)` for every -input incidence — generically impossible when `u` is input at -different positions across edges (e.g., `u` is input at index `0` -of `e₁` and index `1` of `e₂` ⇒ `λ_u = 1` and `λ_u = 1/2` -simultaneously). Constraint (O) is analogous: `λ_u` must equal -`S_e / (β·q_e)` for every output edge of `u`, generically -inconsistent across edges with different `(S_e, q_e)`. - -Even if (I) holds and (O) holds in isolation (no vertex is both -input and output of any edge — a strong acyclicity), they have to -agree on the value of `λ_u`: `1/(i_e(u)+1) = S_{e'}/(β·q_{e'})` for -every input incidence `e` and every output incidence `e'` of `u`. -This is a non-trivial system of equalities that BDF parameters -generically do not satisfy. - -**Conclusion.** `(commuteTimeHypergraphMetric params).laplacian = -conductanceLaplacian (transMatrix) (stationaryDist)` is generically -**false** under no extra hypothesis. The two matrices are -structurally different: `L_κ`'s entries depend on per-edge harmonic -positions `1/(i+1)`, while `L_C`'s entries depend on the global -ratio `π_u / Z(u)`, with no mechanism for the latter to vary across -the edges incident at `u`. - -### 5. Strategy options revisited (with would-be Lean signatures) - -#### (a) Detailed balance against κ — change of chain - -Define a new chain on `V` whose transition matrix is - -``` -P_κ(u, v) := A_κ(u, v) / A_κ(u), A_κ(u) := Σ_w A_κ(u, w), -A_κ(u, v) := Σ_e commuteTimeKappa e u v. -``` - -`P_κ` is reversible by construction with stationary distribution -`π_κ(u) := A_κ(u) / (Σ_{x,y} A_κ(x, y))`, and its conductance -Laplacian is exactly `L_κ` (up to a `Σ A_κ` scalar). The -Markov-layer Doyle-Snell theorem then applies cleanly with -`(P, π) := (P_κ, π_κ)`. - -The catch: hitting times of `P_κ` are **not** hitting times of the -BDF chain `P = H.transMatrix params` — these are two different -walks on `V`. So `commuteTimeDist params u v` (defined via `P`'s -hitting times) does not match `(Σ A_κ) · R_eff(L_κ; u, v)` (which -matches `P_κ`'s commute time). - -Would-be hypothesis (the gap to close): - -```lean -(h_commute_eq_kappa_walk : ∀ u v, - H.commuteTimeDist params u v - = ProofAtlas.RandomWalk.commuteTime (kappaWalk H params) u v) -``` - -This is just the BDF commute time = κ-walk commute time. There is -**no** structural reason for this to hold; it would have to be -asserted ad hoc as part of a BDF sub-class. - -**Verdict.** (a) is an honest pivot to a different chain but kicks -the problem to a separate identity that BDF parameters generically -do not satisfy. Not recommended. - -#### (b) Custom `π'` via detailed balance with κ — change of stationary distribution - -Take the same `π_κ` as above and ask the BDF stationary -distribution to coincide with it: - -```lean -(h_stat_eq_kappa : H.stationaryDist params - = fun u => (∑ v, (H.commuteTimeHypergraphMetric params).conductance u v) - / (∑ x y, (H.commuteTimeHypergraphMetric params).conductance x y)) -``` - -Two issues: - -* The BDF `IsReversible` predicate in `Spectral.Stationary` (and the - one passed in the Round 3a hypothesis package) ties `IsReversible` - to `H.stationaryDist params`. If `h_stat_eq_kappa` holds, then - `H.IsReversible params` becomes: `π_κ(u) · P(u, v) = π_κ(v) · P(v, u)` - — a detailed-balance condition on **P** (not the κ-walk!) against - `π_κ`. This is a non-trivial constraint on `P` going beyond - Round 3a's hypothesis. -* `π_κ` is the stationary distribution of `P_κ`, not necessarily of - `P`. Asking `H.stationaryDist params = π_κ` is asking - `P^⊤ π_κ = π_κ`. Once detailed balance holds, this is automatic - (because detailed balance ⇒ stationarity), so the two conditions - collapse to one: `π_κ` is reversible w.r.t. `P`. - -Would-be hypothesis (simplest form): - -```lean -(h_kappa_reversible : - ∀ u v, (∑ w, (H.commuteTimeHypergraphMetric params).conductance u w) - * H.transProb params u v - = (∑ w, (H.commuteTimeHypergraphMetric params).conductance v w) - * H.transProb params v u) -(h_stat_eq_kappa : H.stationaryDist params - = fun u => (∑ w, (H.commuteTimeHypergraphMetric params).conductance u w) - / (∑ x y, (H.commuteTimeHypergraphMetric params).conductance x y)) -``` - -Even with both, the Markov-layer Doyle-Snell still gives the -identity with a `Σ_{x,y} C(x, y) = Σ_{x,y} π_x P_{x, y} = Σ_x π_x = -1` scalar **on the Markov side**, while the BDF side -`(commuteTimeHypergraphMetric).d = R_eff(L_κ)` has the `(Σ A_κ)` scalar -**absorbed into the Laplacian** (since `L_C ≠ L_κ`). The two `R_eff` -values then differ by the ratio of the total weights, and the bare -equality stated in Round 3a needs a scalar correction (see §6 -below). - -**Verdict.** (b) reduces the gap to a single reversibility-against-κ -condition, but does **not** eliminate the scalar mismatch between -`L_κ` and `L_C`. Workable in principle on a BDF sub-class, but -requires also amending the Round 3a statement to carry a scalar. - -#### (c) Aldous-Fill additive reversibilisation - -Define `P̃ := (P + P*)/2` with `P*(u, v) := (π_v / π_u) · P(v, u)`. -Then `P̃` is reversible w.r.t. `π = H.stationaryDist params`, and -the commute time of `P̃` agrees with that of `P` (Aldous-Fill -Theorem 3.21 / Proposition 3.23, depending on edition). The -Markov-layer Doyle-Snell theorem applies to `(P̃, π)`, with the new -conductance - -``` -C̃(u, v) := π_u · P̃(u, v) = (π_u · P(u, v) + π_v · P(v, u)) / 2 - = (C(u, v) + C(v, u)) / 2. -``` - -Per edge `e`, for `u = H.inVertex e i, v ∈ e^out` (so the -"forward" non-vanishing role): - -``` -C̃^e(u, v) = ½ · [ λ_u · α/q + λ_v · α·β/((i+1)·S_e) ]. -``` - -We compare to `A_κ^e(u, v) = α/((i+1)·q)`. Matching at a single -edge requires - -``` -λ_u · (i+1)·S_e + λ_v · β·q = 2 · S_e, for every edge `e` -and every (input_i, output) pair (u, v) on `e`. -``` - -This is a per-edge identity between `λ_u, λ_v` and the edge -parameters `(i, S_e, q, β)`. Generically inconsistent across edges: -the same vertex pair `(u, v)` might be adjacent on two edges with -different `(i, S_e, q)` data, forcing two different combinations of -`λ_u, λ_v`. - -Special cases where it holds: - -* **`p_e = 1, β = 1` (every edge has one input, no β-asymmetry).** - Then `i = 0`, `S_e = 1`, and the identity becomes `λ_u + λ_v · q - = 2`. Still not free: requires `λ_u + λ_v · q = 2` for every - edge `e` containing `u` as input and `v` as one of its `q` outputs. -* **`p_e = 1, q_e = 1, β = 1` (the underlying combinatorial graph - case).** Then identity is `λ_u + λ_v = 2`, which forces `λ_u = 1` - for all `u` (taking `u = v`'s neighbour). This holds iff `π_u = - Z(u)`, equivalently iff `π_u = α · |incidence(u)|`. For a - connected uniform-α graph, this is automatic. - -Would-be Lean hypothesis (sub-class): - -```lean -(h_in_arity_one : ∀ e : H.edges, H.inArity e = 1) -(h_out_arity_one : ∀ e : H.edges, H.outArity e = 1) -(h_beta_one : params.beta = 1) -(h_alpha_const : ∀ e e' : H.edges, - params.alpha (H.color e) = params.alpha (H.color e')) --- π is then the degree-weighted distribution: -(h_stat_degree : ∀ u, H.stationaryDist params u - = H.normalizer params u / ∑ v, H.normalizer params v) -``` - -Even with this, **the Laplacians do not coincide**: `L_κ` and `L_C̃` -differ by a constant scalar (the `Σ_{x,y} C̃` ratio), because the -two Doyle-Snell normalisations weight effective resistance -differently (see §6). - -**Verdict.** (c) reaches a workable sub-class, but the Laplacian -equality is only **up to a global scalar** (the ratio -`(Σ_{x,y} A_κ) / (Σ_{x,y} C̃)`). The Round 3a bare equality -`commuteTimeDist = (commuteTimeHypergraphMetric).d` is incompatible with -this scalar without amendment. - -### 6. The missing scalar — and what it forces on Round 3a - -Doyle-Snell on the HypergraphMetric side: - -``` -commute time of κ-walk(u, v) - = (Σ_{x,y} A_κ(x, y)) · R_eff(L_κ; u, v) - = (Σ_{x,y} A_κ(x, y)) · (commuteTimeHypergraphMetric params).d u v. -``` - -Doyle-Snell on the Markov side (the Round 2 theorem): - -``` -H.commuteTimeDist params u v - = (Σ_{x,y} C(x, y)) · R_eff(L_C; u, v). -``` - -For `P` reversible, `Σ_{x,y} C(x, y) = Σ_x π_x · Σ_y P(x, y) = -Σ_x π_x = 1`. So the Markov side simplifies to -`R_eff(L_C; u, v)`. Meanwhile, the BDF side has the -`Σ A_κ ≠ 1` scalar in general (e.g., `2α` in the single-edge -example below). - -**Sanity check (single-edge graph with `p = q = 1, β = 1`).** -Vertices `u₀, v₀`. Single edge `e` with `H.inVertex e 0 = u₀`, -`H.outVertex e 0 = v₀`, color `c`. Take `α := params.alpha c`. - -* `Z(u₀) = α`, `Z(v₀) = α·β = α`. By symmetry `π_{u₀} = π_{v₀} = 1/2`. -* `P(u₀, v₀) = 1`, `P(v₀, u₀) = 1` (the chain just alternates). -* `C(u₀, v₀) = π_{u₀} P(u₀, v₀) = 1/2`, `Σ_{x,y} C = 1`. -* `R_eff(L_C; u₀, v₀) = 2`. Markov side: `1 · 2 = 2` = commute time. -* `A_κ(u₀, v₀) = α`, `Σ A_κ = 2α`. `R_eff(L_κ; u₀, v₀) = 1/α`. BDF - side: `(commuteTimeHypergraphMetric).d u₀ v₀ = 1/α`. - -The two effective resistances differ by `2α`: `R_eff(L_κ) = -R_eff(L_C) / (2α)`. The commute time is recovered as `2α · R_eff(L_κ) -= 2 = R_eff(L_C) = 1 · R_eff(L_C)`. So: - -``` -H.commuteTimeDist params u v ≠ (commuteTimeHypergraphMetric).d u v -H.commuteTimeDist params u v = (Σ A_κ) · (commuteTimeHypergraphMetric).d u v. -``` - -**Even on the cleanest sub-class, the Round 3a statement is wrong -by a `Σ_{x,y} A_κ` factor.** This is independent of the (a)/(b)/(c) -strategy choice: it is a property of the two Laplacian -normalisations. - -### 7. Recommendation for Round 3b proper - -The cleanest path is **option (c) on the `p_e = 1, β = 1` -sub-class** combined with an **amendment to the Round 3a statement** -to carry the missing `Σ A_κ` scalar (or, equivalently, a rescaling -of `commuteTimeKappa` by `1 / (Σ A_κ)` so that `L_κ` has the same -normalisation as `L_C`). - -Rationale: - -* (a) requires asserting an unjustified commute-time equality - between two different chains. (c) reuses the actual BDF chain via - Aldous-Fill, so the commute-time identity is for free. -* (b) demands `π · P_κ` ≡ `π` (reversibility of `P` against `π_κ`), - which is a non-trivial condition; (c) automatically produces a - reversible companion `P̃` whose conductance `C̃` is the - Aldous-Fill symmetrisation. -* The `p_e = 1, β = 1` sub-class is the BDF specialisation to the - classical **directed graph case**, where the Doyle-Snell theory - is well-known. It is a clean, defensible sub-class even if - restrictive. - -**Sub-recommendation.** If the user wants the theorem in greater -generality, the next-cheapest extension is `p_e = 1, β` arbitrary, -which collapses to `λ_u + λ_v · q · β = 2`; this still admits a -solution when `λ_u` is constant (e.g., `λ_u = 2/(1 + β·q)` for a -regular hypergraph). The general `p_e` case requires a substantively -deeper reversibilisation argument and is not recommended for -Round 3b. - -### 8. Draft signature for the eventual Round 3b lemma - -Under the recommended approach (option (c) on `p_e = 1, β = 1`, -with explicit scalar correction): - -```lean -/-- **Math.** Bridge from the HypergraphMetric Laplacian to the Markov-layer -conductance Laplacian, restricted to the directed-graph sub-class -of BDF hypergraphs (every edge has a single input, β = 1) and -modulo the standard `2|E|`-type scalar -`totalConductance := Σ_{x,y} (commuteTimeHypergraphMetric).conductance x y`. -/ -lemma commuteTimeHypergraphMetric_laplacian_eq_markov_scalar - [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - -- BDF sub-class: every edge has one input, β = 1, distinct outputs. - (h_in_arity_one : ∀ e : H.edges, H.inArity e = 1) - (h_beta_one : params.beta = 1) - (h_out_inj : ∀ e : H.edges, Function.Injective (H.outVertex e)) - -- π is the degree-weighted stationary distribution. - (h_stat_degree : ∀ u, H.stationaryDist params u - = H.normalizer params u / ∑ v : V, H.normalizer params v) - -- Acyclicity (no vertex is both input and output of the same edge — - -- already implied by inputOutputDisjoint, included for clarity). - (h_acyclic : ∀ (u : V) (e : H.edges), - ¬ (H.isInput u e ∧ H.isOutput u e)) : - (H.commuteTimeHypergraphMetric params).laplacian - = (∑ x y, (H.commuteTimeHypergraphMetric params).conductance x y) - • ProofAtlas.RandomWalk.conductanceLaplacian - (H.transMatrix params) (H.stationaryDist params) -``` - -Notes on the signature: - -* The scalar is on the RHS as a `•` (smul) so that effective - resistance pulls out cleanly: `R_eff(c • L) = R_eff(L) / c`. Then - in Round 3c, the BDF `d` equals `(1 / Σ A_κ) · R_eff(L_C)`, and - multiplying by `Σ A_κ · Σ C = Σ A_κ` (since `Σ C = 1`) recovers - the commute time. -* If the user prefers an unscaled Laplacian equality, the - `commuteTimeKappa` definition itself must be rescaled by `1 / - (Σ A_κ)`. That is a definition-level change (touches - `Metric/CommuteTime.lean`) and would re-derive the HypergraphMetric - axioms. Possible but more invasive. -* The Round 3a statement currently reads - `H.commuteTimeDist params u v = (commuteTimeHypergraphMetric params).d u v`. - To make it true under the recommended approach, **either** the - signature must change to - `H.commuteTimeDist params u v - = (∑ x y, (commuteTimeHypergraphMetric params).conductance x y) - * (commuteTimeHypergraphMetric params).d u v` - (a Round 3a re-amendment), **or** `commuteTimeKappa` must absorb - the scalar. We recommend re-amending the Round 3a statement to - carry the `Σ A_κ` factor; this is a minimal change and preserves - the geometric interpretation of `commuteTimeHypergraphMetric`. -* Reversibility of `P` w.r.t. `π` is **not** in the sub-class - hypothesis above — it must be checked separately. Under `p_e = 1, - β = 1, h_stat_degree`, `P(u, v) = (1/Z(u)) · Σ_e [1[u inp e] · - α(c(e))·1[v out e]/q_e + 1[u out e ∧ inVertex(e,0) = v] · α(c(e))/q_e]` - (using `S_e = 1, β = 1`). Detailed balance then becomes a - symmetry of `Z(u) · π_u^{-1} · forward = Z(v) · π_v^{-1} · - backward` per edge, which under `h_stat_degree` simplifies to an - identity of degrees vs incidence multiplicities. For the natural - fully-symmetric BDF graph (every directed edge has a "reverse" - partner with the same α), this is the standard simple-random-walk - reversibility. The plan agent should expand on this in the - Round 3b assignment. - -### 9. Summary for the plan agent - -* The bare Laplacian equality `(commuteTimeHypergraphMetric).laplacian = - Markov.conductanceLaplacian (transMatrix) (stationaryDist)` is - generically **false** under any of (a)/(b)/(c). The structural - obstruction is the per-edge harmonic factor `1/(i+1)` in `L_κ` - vs the global `π_u/Z(u)` in `L_C`. -* On the cleanest BDF sub-class (`p_e = 1, β = 1, h_out_inj, - degree-weighted π`), the two Laplacians agree **up to a - `Σ_{x,y} A_κ` scalar**. The scalar mismatch is independent of - strategy. -* The Round 3a statement `H.commuteTimeDist = (commuteTimeHypergraphMetric).d` - is bare-equal even on this sub-class, and **fails by the same - `Σ A_κ` scalar**. To proceed cleanly, the plan agent should - schedule a Round 3a-bis (signature re-amendment) to carry the - `Σ A_κ` factor on the RHS, **before** Round 3b proper. -* Recommended Round 3b: option (c) on `p_e = 1, β = 1`, proving - `L_κ = (Σ A_κ) • L_C` under the sub-class hypotheses. Draft - signature in §8 above. diff --git a/lean/informal/deeper_theorem_candidates.md b/lean/informal/deeper_theorem_candidates.md deleted file mode 100644 index dba66f6..0000000 --- a/lean/informal/deeper_theorem_candidates.md +++ /dev/null @@ -1,266 +0,0 @@ -# Deeper theorem candidates for the HypergraphMetric framework - -This file records candidate theorems that would reflect genuine -structural insight about BDF proof hypergraphs through the -HypergraphMetric axiomatic framework. Each candidate is sized in difficulty -(both mathematically and in terms of Lean formalisation), ordered by -"BDF-native depth" — how much of the candidate's interest derives -specifically from the proof-hypergraph interpretation rather than -generic graph theory. - -The candidates are not yet committed to either paper; each can be -shopped between paper 1 (axiomatic / commute-time) and paper 2 -(electrical-resistance) depending on what fits the narrative best. - -## Candidate 1: Rayleigh monotonicity for `d_R` ⭐⭐⭐⭐⭐ - -**Statement.** Let `H = (V, E, c)` and `H' = (V, E', c)` with `E' ⊆ E` -be two BDF hypergraphs on the same vertex set with the same colour -weighting. Then for any `u, v ∈ V`, -``` -d_R^{H'}(u, v) ≥ d_R^H(u, v). -``` - -**Interpretation.** *Adding derivation rules to a proof system can -only shorten distances in the electrical-resistance geometry.* This -is the classical Rayleigh monotonicity for resistor networks, lifted -to the BDF setting. - -**Why it is deep.** -* Encodes the natural intuition that a richer proof system has a - more tightly connected metric structure. -* Establishes a *strict ordering* of BDF hypergraphs under the - sub-hypergraph relation: `H' ⊆ H ⟹ d_R^H ≤ d_R^{H'}` pointwise. -* **Does not hold for the commute-time metric `d_W`** in this form - (commute time is more delicate: adding an edge can simultaneously - introduce a shortcut and shift the stationary distribution, so - point-wise monotonicity fails). This sharply distinguishes the two - metrics and is direct motivation for studying `d_R` separately. - -**Lean formalisation sketch.** -The standard proof goes via PSD ordering of Laplacians. Concretely: -* The conductance Laplacian `L_κ` decomposes as a sum `L_κ = Σ_e L_e` - of per-edge rank-1 Laplacians `L_e := Σ_{u,v} κ(e, u, v) · - (e_u - e_v)(e_u - e_v)^⊤/2` (the symmetric form). Each `L_e` is - PSD. -* Removing an edge subtracts a PSD matrix, which can only increase - the pseudoinverse on the column space of the remaining Laplacian. -* Hence `(e_u - e_v)^⊤ L_κ^+ (e_u - e_v) ≥ (e_u - e_v)^⊤ L_{κ'}^+ (e_u - e_v)`, - which is the claim. - -**Estimated Lean effort.** ~100 lines. Requires the L_κ PosSemidef -lemma (Sub-lemma 1 in `paper2_dR_hyperbolicity_TODO.md`); given that, -the monotonicity proof is roughly: per-edge PSD decomposition (~50 -lines) + PSD-ordering of pseudoinverse (~50 lines, classical result). - -**Where it fits.** Paper 2 §5 or new §5.5 (between metric axioms and -spectral analysis). - ---- - -## Candidate 2: Axiomatic uniqueness of the harmonic schedule ⭐⭐⭐⭐ - -**Statement.** Let `m : HypergraphMetric H params` satisfy: -1. The five axioms (D), (O), (C), (A), (S). -2. The harmonic normalisation: for every edge `e` and every input - position `i ∈ [0, p_e)`, - `Σ_v κ(e, inVertex_e(i), v) = α(c(e)) / (i + 1)`. - -Then `m.κ = κ^h` (the harmonic schedule of paper 1 §3.2 / Lean -`commuteTimeKappa`). - -**Interpretation.** *The five HypergraphMetric axioms, together with the -"natural" harmonic normalisation, uniquely pin down the conductance -schedule.* The only remaining freedom is the global colour weighting -`α : C → ℝ_{>0}`, which is an explicit modelling input. - -**Why it is deep.** -* Hallmark of a *tight* axiomatisation: there are no spurious - realisations of the axioms hiding alternative theories. Compare: - the real numbers are characterised by ordered-field + completeness; - a finite von Neumann factor is characterised by its trace + - factoriality. -* Justifies the choice of `commuteTimeKappa` from first principles - rather than as an *ad hoc* construction. -* Eliminates the obvious objection that "you might have chosen a - different conductance schedule"; the axioms force this one. - -**Lean formalisation sketch.** -Each axiom restricts κ: -* (C) restricts κ to `α(c(e)) · g(e, u, v)`, factoring out colour. -* (D) restricts `g(e, u, v) = 0` outside (input, output) pairs. -* (A) restricts `g(e, input_i, output_j)` to be independent of `j`. -* (S) restricts `g(e, output_j, input_i) = g(e, input_i, output_j)`. -* (O) + harmonic normalisation forces the input-position dependence - to be `1/(i+1)` divided by the output count. - -Each step nails down one degree of freedom; the only `κ` satisfying -all is `α/((i+1) · outArity_e)` symmetrised. - -**Estimated Lean effort.** ~150 lines. Mostly mechanical case -analysis on (input, output) pairs. - -**Where it fits.** Paper 1 §3 (HypergraphMetric axiomatic framework). Would -be the *capstone* of that section, justifying the harmonic existence -witness as the unique natural choice. - ---- - -## Candidate 3: Trace identity — Kemeny constant = mean distance ⭐⭐⭐ - -**Statement.** For a reversible BDF chain with stationary π, -Kemeny constant K, and commute-time metric `d_W`, -``` -E_{U, V ~ π}[d_W(U, V)] = 2K. -``` -For the effective-resistance metric `d_R` and uniform sampling, -``` -E_{U, V ~ Uniform(V)}[d_R(U, V)] = (2/|V|) · K_R, -``` -where `K_R := Σ_{λ_i > 0} 1/λ_i` is the Laplacian Kemeny analog. - -**Interpretation.** *The global spectral invariant K (resp. K_R) -equals the mean distance under the natural sampling distribution.* -Spectrum = average geometry. - -**Why it is deep.** -* Identifies a spectral invariant (sum of inverse eigenvalues) with a - metric invariant (mean distance). Pólya 1921 / Aldous-Fill 2002 - noted this for `d_W`; the `d_R` version is parallel but uses - uniform sampling instead. -* Already implicitly used in the proof of `Hypergraph.probabilistic_hyperbolicity_bound` - (Theorem 8.4) in Lean. Extracting it as a standalone theorem makes - it citable and reusable. - -**Lean formalisation sketch.** -The `d_W` half is already inside the proof of -`probabilistic_hyperbolicity_bound`; can be extracted as a separate -lemma `Hypergraph.expected_commute_time_eq_two_K` (~30 lines). -The `d_R` half follows directly from the spectral expansion of `d_R` -(Sub-lemma 4 in `paper2_dR_hyperbolicity_TODO.md`) by linearity of -expectation (~20 lines). - -**Estimated Lean effort.** ~50 lines. - -**Where it fits.** Both papers. In paper 1, as a clean statement -preceding Theorem 8.4. In paper 2, as the parallel statement for `d_R` -preceding the d_R-hyperbolicity theorem. - ---- - -## Candidate 4: Tree-equivalence — δ_W = 0 ⟺ underlying is a tree ⭐⭐⭐⭐ - -**Statement.** Let `H` be a BDF hypergraph carrying the intrinsic -walk (`α ≡ 1`, `β = 1`). Then the hyperbolicity constant -`δ_W(H) = 0` if and only if the underlying directed graph of H is a -tree (in particular, no shared sub-lemmas). - -**Interpretation.** *BDF proof geometry is exactly tree-like if and -only if no sub-lemma is reused.* This characterises the boundary -between "geometrically simple" and "geometrically complex" proofs. - -**Why it is deep.** -* Paper 1 §5 (Tree base case) proves only the ⇐ direction. The ⇒ - direction (`δ = 0 ⟹ tree`) requires a constructive argument: any - cycle in the underlying graph gives an explicit four-point - configuration with positive deviation. -* Together the two directions give a *complete structural - characterisation* of `0`-hyperbolic BDF hypergraphs. -* Connects geometric flatness to combinatorial simplicity of the - proof. - -**Lean formalisation sketch.** -Already have ⇐ in `Spectral/...` (commute time on a tree is the path -metric). For ⇒: -* Suppose the underlying graph has a cycle `u_0 → u_1 → ... → u_k = u_0`. -* Pick `u, v, x, y` cleverly on this cycle (e.g., antipodal pairs). -* Compute `d_W` on each pair; show that the four-point sums are not - all equal, hence `δ > 0`. - -This is delicate — finding the right `u, v, x, y` requires care, and -the commute time calculation on a single cycle is nontrivial. - -**Estimated Lean effort.** ~200 lines (⇒ direction alone). The hard -part is constructing the explicit cycle witness. - -**Where it fits.** Paper 1 §6 (Tree base case), as a strengthening -to a full characterisation. - ---- - -## Candidate 5: Cheeger-style inequality for the BDF chain ⭐⭐⭐ - -**Statement.** Let `H` be a BDF hypergraph with reversible BDF random -walk having spectral gap `γ`, and let `Φ_H := min_{S ⊊ V} |∂S|_κ / -vol_π(S)` be the BDF Cheeger conductance. Then -``` -γ ≥ Φ_H² / 2. -``` - -**Interpretation.** *The hardest "bottleneck cut" in the BDF proof -structure controls how quickly the random walk mixes (and hence -controls hyperbolicity via Theorem 8.4).* - -**Why it is deep.** -* Classical Cheeger inequality specialised to BDF — links combinatorial - structure (bottleneck cuts) to spectral data (mixing time). -* Gives a *concrete attack route* for the worst-case Conjecture 8.4: - if Φ_H is bounded below by a polynomial in n, then `γK` grows and - the bound improves. -* For the strong worst-case conjecture (Conj 8.4), Cheeger + the - observed polynomial decay would close the gap. - -**Lean formalisation sketch.** -* Mathlib does not yet have graph Cheeger. -* Would need to build: the conductance `Φ_H` definition, then the - Cheeger inequality `γ ≥ Φ²/2`. -* The Cheeger argument requires a positivity argument on the - eigenvector corresponding to the second eigenvalue (cf. Chung - 1997). Substantial new infrastructure. - -**Estimated Lean effort.** ~300 lines. Requires building Cheeger -machinery from scratch within Mathlib. - -**Where it fits.** Paper 1 §8 (Average-case hyperbolicity) as a -companion bound, or in a future paper specifically on BDF mixing -times. - ---- - -## Summary table - -| # | Candidate | Depth | BDF-native | Lean effort | Best home | -|---|---|---|---|---|---| -| 1 | Rayleigh monotonicity for `d_R` | ⭐⭐⭐⭐⭐ | ✅ Native | ~100 | Paper 2 | -| 2 | Axiomatic uniqueness | ⭐⭐⭐⭐ | ✅ Native | ~150 | Paper 1 §3 | -| 3 | Kemeny = mean distance | ⭐⭐⭐ | Partial | ~50 | Both | -| 4 | Tree-equivalence | ⭐⭐⭐⭐ | ✅ Native | ~200 | Paper 1 §6 | -| 5 | Cheeger-style inequality | ⭐⭐⭐ | Classical | ~300 | Paper 1 §8 or future | - -## Recommended priorities - -For paper 2's intellectual depth and BDF-specificity: -1. **Rayleigh monotonicity (Candidate 1)** — provides the structural - "richer-system → smaller-distances" theorem that distinguishes `d_R` - from `d_W`. -2. **Axiomatic uniqueness (Candidate 2)** — completes the paper 1 - §3 axiomatic narrative. -3. **Kemeny = mean distance (Candidate 3)** — cheap and clean; extracts - an existing identity into a citable lemma. - -Candidates 4 and 5 are deeper but more expensive; both make sense as -follow-up work after paper 2 is in shape. - -## References / Background - -* Doyle, Snell, *Random Walks and Electric Networks* — Rayleigh - monotonicity, classical exposition. -* Aldous, Fill, *Reversible Markov Chains and Random Walks on Graphs*, - Ch. 4 §4 — Cheeger inequality and Kemeny constant identities. -* Chung, F.R.K., *Spectral Graph Theory*, Ch. 2 — Cheeger inequality - in the graph-Laplacian formulation. -* Klein, Randić 1993, *Resistance distance*, J. Math. Chem. — - underpinning for Candidate 1 and for the triangle inequality of - `d_R` (Sub-lemma 3 in `paper2_dR_hyperbolicity_TODO.md`). -* Lyons, Peres, *Probability on Trees and Networks* — modern - treatment of all of the above. diff --git a/lean/informal/markov_doyle_snell.md b/lean/informal/markov_doyle_snell.md deleted file mode 100644 index 8daf9c5..0000000 --- a/lean/informal/markov_doyle_snell.md +++ /dev/null @@ -1,225 +0,0 @@ -# Informal proof: abstract Markov-layer Doyle-Snell identity - -**Target:** the sorry at `ProofAtlas/RandomWalk/CommuteTime.lean:186`, - -``` -h(u,v) + h(v,u) = (Σ_{x,y} C_{x,y}) · R_eff(L_C; u, v) -``` - -for a finite ergodic reversible Markov chain on `n` with transition matrix `P`, -stationary distribution `π`, conductance `C_{u,v} := π_u P_{u,v}`, and -conductance Laplacian `L_C := D − C` (`D` = diagonal of off-diagonal row sums). - -The hitting times on the LHS are presented in their `(I − Q_v)⁻¹ · b_v` form, -where `Q_v := killedMatrix P v` and `b_v := killedRhs v`, matching -`kemenySnell_hitting_apply`. - -This file gives the detailed informal proof. Sub-lemma names match the -proposed Lean declarations. - -## Notation - -* `D_π := Matrix.diagonal π` and `D_π^{1/2} := diagSqrt π`, - `D_π^{−1/2} := diagSqrtInv π` (already in `Markov/FundamentalMatrix.lean`). -* `Q_{u,v} := √π_u · P_{u,v} / √π_v` (the symmetrised transition matrix — - already used by `fundamentalMatrix_spectral_expansion`). Hermitian iff `P` - is reversible w.r.t. `π`. -* `Z := fundamentalMatrix P π = (I − P + W)⁻¹ − W`, with - `W := stationaryProjection π = 𝟙 πᵀ`. -* `(λᵢ, φᵢ)` = eigendata of `Q`. Since `P` is reversible, `Q` is Hermitian. - Perron-Frobenius: there is a top eigenvalue `λ_top = 1` with eigenvector - `√π`. All other `|λᵢ| < 1` for ergodic chains. -* `σ_{u,v} := ∑_{λᵢ < 1} φᵢ(u) · φᵢ(v) / (1 − λᵢ)` — symmetric in `u, v`. - -## Key sub-lemma 1: matrix identity for `L_C` - -``` -L_C = D_π^{1/2} · (I − Q) · D_π^{1/2} -``` - -**Proof.** Entry-wise. Use `h_sim : Q_{u,v} = √π_u · P_{u,v} / √π_v`. - -* Off-diagonal `u ≠ v`: - `(D^{1/2}(I−Q)D^{1/2})_{u,v} = √π_u · (−Q_{u,v}) · √π_v = −π_u · P_{u,v} = −C_{u,v} = L_C u v`. ✓ -* Diagonal `u = u`: - `(D^{1/2}(I−Q)D^{1/2})_{u,u} = √π_u · (1 − Q_{u,u}) · √π_u = π_u − π_u · P_{u,u}`. - RHS Laplacian diagonal: `(∑_w C_{u,w}) − C_{u,u} = π_u(∑_w P_{u,w}) − π_u P_{u,u} - = π_u − π_u P_{u,u}`, by `h_row_sum`. ✓ - -Lean name: `conductanceLaplacian_eq_diagSqrt_mul_one_sub_Q_mul_diagSqrt`. - -## Key sub-lemma 2: pseudoinverse identity for `L_C` - -For positive `π`, `D^{1/2}` is invertible (with inverse `D^{−1/2}`), and -`(B A B)⁺ = B⁻¹ A⁺ B⁻¹` whenever `B` is invertible. Therefore - -``` -L_C⁺ = D_π^{−1/2} · (I − Q)⁺ · D_π^{−1/2} -``` - -This requires the algebraic identity -`(D^{1/2}(I−Q)D^{1/2})⁺ = D^{−1/2}(I−Q)⁺ D^{−1/2}` (Penrose, in the Hermitian -positive-square-root case). The shortest route to a Lean proof: verify the -four Moore-Penrose identities for `D^{−1/2}(I−Q)⁺ D^{−1/2}` against `L_C`, -since `pseudoinverse` is uniquely characterised. Each identity is two -applications of `diagSqrt_mul_diagSqrtInv` plus the Moore-Penrose identities -for `(I−Q)⁺`. - -Lean name: `pseudoinverse_conductanceLaplacian_eq`. **Note:** this lemma -takes `(I−Q).IsHermitian` and `Q.IsHermitian` as hypotheses, both of which -follow from `hQ : Q.IsHermitian` (a hypothesis of the main theorem). - -## Key sub-lemma 3: spectral form of `R_eff(L_C; u, v)` - -``` -R_eff(L_C; u, v) = ∑_{λᵢ < 1} (φᵢ(u)/√π_u − φᵢ(v)/√π_v)² / (1 − λᵢ) -``` - -**Proof.** Plug Sub-lemma 2 into the definition of `effectiveResistance`: - -``` -R = (e_u − e_v)ᵀ · D^{−1/2} (I−Q)⁺ D^{−1/2} · (e_u − e_v) - = ⟨D^{−1/2}(e_u − e_v), (I−Q)⁺ · D^{−1/2}(e_u − e_v)⟩ -``` - -The signed vector `D^{−1/2}(e_u − e_v)` is `(1/√π_u)·e_u − (1/√π_v)·e_v`, i.e. -takes value `1/√π_u` at `u`, `−1/√π_v` at `v`, `0` else. - -Now `(I−Q) = Q' := ∑_i (1−λᵢ) φᵢ φᵢᵀ`, so -`(I−Q)⁺ = ∑_{λᵢ<1} (1−λᵢ)⁻¹ φᵢ φᵢᵀ`. Pair with the signed vector: - -``` -⟨D^{−1/2}(e_u − e_v), φᵢ⟩ = φᵢ(u)/√π_u − φᵢ(v)/√π_v -``` - -so the quadratic form expands to `∑_{λᵢ<1} (φᵢ(u)/√π_u − φᵢ(v)/√π_v)²/(1−λᵢ)`. ✓ - -Lean name: `effectiveResistance_conductanceLaplacian_spectral`. - -**Alternative route:** instead of going through sub-lemma 2, expand the -quadratic form directly using `pseudoinverse_spectral_expansion` for -`L_C⁺`. This requires knowing `L_C`'s eigenbasis — but `L_C`'s eigenvectors -are NOT `D^{1/2}φᵢ` (those are not orthonormal). So this alternative is -likely harder; prefer the (sub-lemma 1 + sub-lemma 2 + direct quadratic -form) route above. - -## Key sub-lemma 4: spectral form of the hitting-time sum - -``` -h(u,v) + h(v,u) = ∑_{λᵢ<1} (φᵢ(u)/√π_u − φᵢ(v)/√π_v)² / (1 − λᵢ) -``` - -**Proof.** Apply `kemenySnell_hitting_apply` to each killed-matrix term: - -``` -((1 − Q_v)⁻¹ · b_v)(u) = (Z_{v,v} − Z_{u,v}) / π_v -((1 − Q_u)⁻¹ · b_u)(v) = (Z_{u,u} − Z_{v,u}) / π_u -``` - -Sum: - -``` -LHS = (Z_{vv} − Z_{uv})/π_v + (Z_{uu} − Z_{vu})/π_u - = Z_{uu}/π_u + Z_{vv}/π_v − Z_{uv}/π_v − Z_{vu}/π_u -``` - -Apply `fundamentalMatrix_spectral_expansion` to each `Z_{a,b}`: - -``` -Z_{a,b} = √(π_b/π_a) · σ_{a,b} where σ_{a,b} = ∑_{λᵢ<1} φᵢ(a)φᵢ(b)/(1−λᵢ). -``` - -In particular: -* `Z_{uu}/π_u = σ_{u,u} · √(π_u/π_u) / π_u = σ_{uu}/π_u` -* `Z_{vv}/π_v = σ_{vv}/π_v` -* `Z_{uv}/π_v = √(π_v/π_u)·σ_{u,v}/π_v = σ_{uv}/√(π_u π_v)` -* `Z_{vu}/π_u = √(π_u/π_v)·σ_{v,u}/π_u = σ_{vu}/√(π_u π_v) = σ_{uv}/√(π_u π_v)` (by symmetry of σ). - -So: - -``` -LHS = σ_{uu}/π_u + σ_{vv}/π_v − 2 σ_{uv}/√(π_u π_v) - = ∑_{λᵢ<1} ( φᵢ(u)²/π_u + φᵢ(v)²/π_v − 2 φᵢ(u)φᵢ(v)/√(π_u π_v) ) / (1−λᵢ) - = ∑_{λᵢ<1} ( φᵢ(u)/√π_u − φᵢ(v)/√π_v )² / (1−λᵢ). ✓ -``` - -The last step is the binomial expansion `a² − 2ab + b² = (a−b)²`. - -Lean name: `commuteTime_sum_spectral`. - -**Note:** this lemma is the key "Aldous-Fill Lemma 2.10" content. The -symmetrised fundamental-matrix identity `π_u Z_{u,v} = π_v Z_{v,u}` is -implicit in `fundamentalMatrix_spectral_expansion`, since both sides equal -`√(π_u π_v) · σ_{u,v}`. - -## Key sub-lemma 5: total conductance equals one - -``` -∑_{x,y} C_{x,y} = 1 -``` - -**Proof.** Direct calculation: - -``` -∑_{x,y} C_{x,y} = ∑_x π_x · (∑_y P_{x,y}) = ∑_x π_x · 1 = 1 -``` - -using `h_row_sum` and `h_sum`. Lean name: `sum_conductance_eq_one`. - -## Combining - -By Sub-lemmas 3 and 4, both `R_eff(L_C; u, v)` and `h(u,v) + h(v,u)` equal -the same expression: - -``` -∑_{λᵢ<1} (φᵢ(u)/√π_u − φᵢ(v)/√π_v)² / (1 − λᵢ) -``` - -So `h(u,v) + h(v,u) = R_eff(L_C; u, v)`. By Sub-lemma 5, -`(∑_{x,y} C_{x,y}) · R_eff(L_C; u, v) = 1 · R_eff(L_C; u, v) = R_eff(L_C; u, v)`, -matching the LHS. ∎ - -## Required hypotheses on the main theorem - -The current statement of `commuteTime_eq_effectiveResistance` is missing -spectral hypotheses needed by `fundamentalMatrix_spectral_expansion`. The -prover should **update the statement** to add: - -```lean -{Q : Matrix n n ℝ} -(hQ : Q.IsHermitian) -(h_sim : ∀ u v, Q u v = Real.sqrt (π u) * P u v / Real.sqrt (π v)) -(h_conv : Filter.Tendsto (fun k => P ^ k) Filter.atTop - (nhds (stationaryProjection π))) -(h_eig_le_one : ∀ i, hQ.eigenvalues i ≤ 1) -(h_top_eigvec : ∀ i, hQ.eigenvalues i = 1 → - ∀ v, hQ.eigenvectorBasis i v = Real.sqrt (π v)) -(h_top_unique : ∃! i_top : n, hQ.eigenvalues i_top = 1) -(h_nontop_bound : ∀ i, hQ.eigenvalues i < 1 → |hQ.eigenvalues i| < 1) -``` - -Mirroring exactly the signature of `fundamentalMatrix_spectral_expansion`. -USER_HINTS Round 2 explicitly permits updating the statement. - -## Existing pieces to invoke - -* `kemenySnell_hitting_apply` — `Markov/KemenySnell.lean:203` -* `fundamentalMatrix_spectral_expansion` — `Markov/FundamentalMatrix.lean:783` -* `pseudoinverse_spectral_expansion` — `LinearAlgebra/MoorePenrose.lean` (proven) -* `diagSqrt`, `diagSqrtInv`, `diagSqrt_mul_diagSqrtInv`, `diagSqrtInv_mul_diagSqrt` - — `Markov/FundamentalMatrix.lean:486+` -* Moore-Penrose four identities (Hermitian case) — `LinearAlgebra/MoorePenrose.lean` -* `conductance_symm`, `conductanceLaplacian_symm` — `Markov/CommuteTime.lean` -* `Matrix.IsHermitian.spectral_theorem` — Mathlib - -## Decomposition discipline - -If the full proof is too long for one prover round, the prover SHOULD land -sub-lemmas 1, 5 first (one-shot proofs), then sub-lemma 2, then sub-lemmas -3 and 4 (each medium-length spectral computations). Final combination is -short (~10 lines). Report which sub-lemmas land and where the time is -spent — DO NOT introduce a fresh sorry on an intermediate sub-lemma. - -If a sub-lemma is genuinely stuck and the prover is otherwise running out -of budget, leave the FINAL combination as the only sorry and report which -sub-lemma blocked. diff --git a/lean/informal/paper2_dR_hyperbolicity_TODO.md b/lean/informal/paper2_dR_hyperbolicity_TODO.md deleted file mode 100644 index b54ab3a..0000000 --- a/lean/informal/paper2_dR_hyperbolicity_TODO.md +++ /dev/null @@ -1,251 +0,0 @@ -# Paper 2 Lean roadmap: probabilistic hyperbolicity for `HypergraphMetric.d` - -This file records the roadmap for formalising paper 2's main result -(probabilistic hyperbolicity for the effective-resistance metric -`d_R := effectiveResistance(L_κ)`) in Lean, analogous to paper 1's -formalisation of probabilistic hyperbolicity for the commute time -metric `d_W`. - -The mathematical content of paper 2 is described in -`paper/paper2-electrical-resistance/main.tex`. The structural finding -(why `d_R ≠ d_W` on generic BDF) is in -`informal/bdf_doyle_snell_bridge.md`. This file is the **action plan** -for the Lean side. - -## Target main theorem - -```lean -theorem probabilistic_hyperbolicity_dR - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (m : HypergraphMetric H params) - (h_κ_nonneg : ∀ e u v, 0 ≤ m.κ e u v) : - (avgHyperbolicity_dR m) / (avg_dR m) ≤ - Real.sqrt (3 * kappa2_R m) / kemeny_R m := by - sorry -``` - -where the named quantities are: -* `avgHyperbolicity_dR m := (1/2) · E_{Uniform(V)^4}[Φ_R]` -* `avg_dR m := E_{Uniform(V)^2}[d_R(U, V)]` -* `kemeny_R m := Σ_{i: λ_i > 0} 1/λ_i` (sum over positive eigenvalues of `m.laplacian`) -* `kappa2_R m := Σ_{i: λ_i > 0} 1/λ_i²` (second moment) - -The bound parallels `Hypergraph.probabilistic_hyperbolicity_bound` -in `ProofAtlas/Spectral/ProbabilisticBound.lean` (paper 1's -Theorem 8.4) line-by-line. - -## Existing reusable infrastructure - -* `ProofAtlas/LinearAlgebra/MoorePenrose.lean` — pseudoinverse via - spectral decomposition; the four Moore-Penrose identities; - `pseudoinverse_isHermitian_of_isHermitian`; `pseudoinverse_posSemidef`. -* `ProofAtlas/LinearAlgebra/EffectiveResistance.lean` — - `effectiveResistance L u v` definition; `effectiveResistance_self`, - `effectiveResistance_symm`, `effectiveResistance_nonneg` (the last - requires `L.PosSemidef` as hypothesis). -* `ProofAtlas/LinearAlgebra/SpectralExpansion.lean` — - `pseudoinverse_spectral_expansion` and - `effectiveResistance_spectral_expansion` for arbitrary Hermitian - matrices. Both sorry-free. The latter gives the formula - `R(L; u, v) = Σ_{λ_i ≠ 0} (φ_i(u) - φ_i(v))² / λ_i` directly. -* `ProofAtlas/Metric/Resistance.lean` — `HypergraphMetric.d` definition; - `conductance_symm`, `laplacian_symm`, `laplacian_isHermitian`, - `d_self`, `d_symm` (already proved). -* `ProofAtlas/Spectral/ProbabilisticBound.lean` — paper 1's Theorem - 8.4 proof. The probabilistic-bound argument - (Cauchy-Schwarz on Φ + second-moment identity + Kemeny constant) - is parallel; only the weights change. - -## Decomposition into sub-lemmas - -### Sub-lemma 1: positive semi-definiteness of `L_κ` -**File:** `ProofAtlas/Metric/Resistance.lean` (extend the existing file). -**Statement:** -```lean -lemma HypergraphMetric.laplacian_posSemidef (m : HypergraphMetric H params) - (h_κ_nonneg : ∀ e u v, 0 ≤ m.κ e u v) : - m.laplacian.PosSemidef -``` -**Strategy.** Use the standard graph-Laplacian energy identity -``` -xᵀ L_κ x = (1/2) · Σ_{u,v} m.conductance u v · (x u − x v)² + Σ_u m.conductance u u · x u² -``` -Both terms are ≥ 0 when conductance ≥ 0 (which follows from κ ≥ 0). -**Estimated size.** ~80 lines. A previous attempt -(commit `2a3c66b` was reverted) failed at ~200 lines; the algebra -needs careful sequencing of `Finset.sum_sub_distrib`, `Finset.sum_div`, -`Finset.sum_comm`, and the conductance symmetry from `m.symm`. -A clean approach is to first prove the entry-wise identity -``` -x ⬝ᵥ L_κ.mulVec x = (∑ u, c(u, u) · x u²) + (1/2) · ∑ u, ∑ v, c(u, v) · (x u − x v)² -``` -as a separate lemma, then PSD follows immediately. - -### Sub-lemma 2: `d_nonneg` -**File:** `ProofAtlas/Metric/Resistance.lean`. -**Statement:** -```lean -lemma HypergraphMetric.d_nonneg (m : HypergraphMetric H params) - (h_κ_nonneg : ∀ e u v, 0 ≤ m.κ e u v) (u v : V) : - 0 ≤ m.d u v -``` -**Strategy.** Direct from `effectiveResistance_nonneg` applied to -`m.laplacian_posSemidef`. Three-line proof. -**Estimated size.** ~10 lines. - -### Sub-lemma 3 (deferred): `d_triangle` -**File:** `ProofAtlas/Metric/Resistance.lean`. -**Statement:** -```lean -lemma HypergraphMetric.d_triangle (m : HypergraphMetric H params) - (h_κ_nonneg : ∀ e u v, 0 ≤ m.κ e u v) (u v w : V) : - m.d u w ≤ m.d u v + m.d v w -``` -**Strategy.** This is the classical Klein-Randić "resistance distance -is a metric" theorem (Klein, Randić 1993). The standard proof routes -through Thomson's principle (variational characterisation of -effective resistance as minimum energy over unit-current flows). -Decompose a unit current from `u` to `w` as the sum of unit currents -`u → v` and `v → w`; the energy of the combined flow is at most the -sum of energies, and the optimal energy of the `u → w` flow is at -most that of any specific flow. This needs: -* The unit-current-flow formulation in Lean (does not exist in - Mathlib). -* The variational identity `R_eff = min energy(unit flow)` (does not - exist in Mathlib). -**Estimated size.** ~150-300 lines depending on infrastructure choices. -**Recommendation.** Defer indefinitely. It's a classical theorem -but requires constructing graph-theoretic infrastructure from scratch -within Mathlib. For paper 2 we cite Klein-Randić and rely on the -informal proof. - -### Sub-lemma 4: spectral expansion of `d_R` -**File:** new `ProofAtlas/Metric/SpectralResistance.lean` or extend -`Metric/Resistance.lean`. -**Statement:** -```lean -lemma HypergraphMetric.d_spectral - (m : HypergraphMetric H params) (hL : m.laplacian.IsHermitian) (u v : V) : - m.d u v = ∑ i : V, if hL.eigenvalues i ≠ 0 - then (hL.eigenvectorBasis i u - hL.eigenvectorBasis i v) ^ 2 - / hL.eigenvalues i - else 0 -``` -**Strategy.** Direct specialisation of -`ProofAtlas.LinearAlgebra.effectiveResistance_spectral_expansion`, -already proved. Just unfold `HypergraphMetric.d` and apply. -**Estimated size.** ~20 lines. - -### Sub-lemma 5: four-point identity for `Φ_R` -**File:** new `ProofAtlas/Metric/SpectralResistance.lean`. -**Statement:** -```lean -lemma HypergraphMetric.Phi_R_spectral - (m : HypergraphMetric H params) (hL : m.laplacian.IsHermitian) - (u v x y : V) : - let S1 := m.d u v + m.d x y - let S2 := m.d u x + m.d v y - S1 - S2 = 2 * ∑ i : V, if hL.eigenvalues i ≠ 0 - then (hL.eigenvectorBasis i x - hL.eigenvectorBasis i v) * - (hL.eigenvectorBasis i u - hL.eigenvectorBasis i y) - / hL.eigenvalues i - else 0 -``` -**Strategy.** Algebraic manipulation from Sub-lemma 4. The identity -`(a-b)² + (c-d)² - (a-c)² - (b-d)² = 2(c-b)(a-d)` does the work -per eigenvalue. This mirrors `Spectral/Decomposition.lean`'s -`Phi_W_spectral` for the commute-time metric. -**Estimated size.** ~80 lines. - -### Sub-lemma 6: Uniform-sampling second-moment identity -**File:** new `ProofAtlas/Metric/ProbabilisticBoundR.lean`. -**Statement:** -```lean -lemma HypergraphMetric.S_diff_second_moment_uniform - (m : HypergraphMetric H params) (hL : m.laplacian.IsHermitian) : - let n := Fintype.card V - Finset.sum (Finset.univ ×ˢ Finset.univ ×ˢ Finset.univ ×ˢ Finset.univ) - (fun ⟨u, v, x, y⟩ => - (m.d u v + m.d x y - m.d u x - m.d v y) ^ 2) / n^4 - = (16 / n^2) * ∑ i, if hL.eigenvalues i > 0 then 1 / (hL.eigenvalues i ^ 2) else 0 -``` -**Strategy.** Expand using Sub-lemma 5, then use orthonormality of -`eigenvectorBasis` and the fact that for orthonormal eigenvectors -`φ_i` of a graph Laplacian (with kernel = constants), `Σ_u φ_i(u) = 0` -for `i` in the non-kernel modes. The expectation factorises -component-wise. -Compare with `Spectral/Moments.lean`'s second-moment identity for -the commute-time `Φ_W`. -**Estimated size.** ~100-150 lines. - -### Main theorem assembly -**File:** `ProofAtlas/Metric/ProbabilisticBoundR.lean`. -**Strategy.** Combine Sub-lemmas 5 + 6 + Cauchy-Schwarz on `Φ_R²` -to get `E[Φ_R] ≤ 4√(3 κ_2^R)/n`, then divide by `E[d_R] = 2 K_R / n` -to get the ratio bound. Parallel to `probabilistic_hyperbolicity_bound` -in `Spectral/ProbabilisticBound.lean`. -**Estimated size.** ~80-120 lines. - -## Total estimated effort - -| Component | Lines | Difficulty | -|---|---|---| -| Sub-lemma 1 (PSD) | ~80 | Medium | -| Sub-lemma 2 (d_nonneg) | ~10 | Trivial | -| Sub-lemma 3 (triangle) | ~250 | Hard — **deferred** | -| Sub-lemma 4 (spectral expansion of d_R) | ~20 | Trivial | -| Sub-lemma 5 (Φ_R spectral) | ~80 | Medium | -| Sub-lemma 6 (second moment) | ~120 | Medium | -| Main theorem | ~100 | Medium | -| **Total (excluding triangle)** | **~410** | 1-3 days focused | - -## Sequencing - -Suggested order (each is a separate commit): - -1. **PSD + d_nonneg** (Sub-lemmas 1, 2). Closes a critical gap left - open by the current `Metric/Resistance.lean` ("future work" comment). - ~90 lines, 1 commit. **Lowest risk; do first.** -2. **d_R spectral expansion** (Sub-lemma 4). Almost trivial given - existing infrastructure; ~20 lines, 1 commit. -3. **Φ_R four-point spectral** (Sub-lemma 5). Algebraic, ~80 lines, - 1 commit. -4. **Second-moment identity under Uniform(V)** (Sub-lemma 6). - The bulk of new content; ~120 lines. -5. **Main theorem assembly**. Final commit, ~100 lines. - -## Why this is feasible - -* Mathematically straightforward: the entire argument is parallel to - paper 1's `probabilistic_hyperbolicity_bound`, which is already - sorry-free in Lean. We're following a verified template, just with - different eigendata (φ of L_κ instead of ψ of Q). -* No new Mathlib infrastructure required (modulo the deferred - triangle inequality of Sub-lemma 3). -* The harder steps (PSD, spectral expansion of pseudoinverse) reuse - existing sorry-free Lean lemmas. - -## Why this is non-trivial - -* Sub-lemma 1 (PSD via energy identity) requires careful Finset - arithmetic. A previous attempt at this lemma stalled at ~200 lines. - See commit history of `Metric/Resistance.lean`. -* Sub-lemma 6 needs the orthonormality argument carefully (mapping - the uniform-on-vertices expectation through the eigenvector basis). -* Sub-lemma 3 (Klein-Randić triangle inequality) requires building - the unit-current-flow framework from scratch; out of scope for - paper 2's main result, which can proceed without it (the - `MetricSpace` instance is then proved modulo the triangle - inequality being cited from Klein-Randić). - -## References - -* Klein, Randić 1993, *Resistance distance*, J. Math. Chem. — the - classical "resistance distance is a metric" theorem (Sub-lemma 3). -* Doyle, Snell, *Random Walks and Electric Networks*, Ch. 3 — energy - identity and Thomson's principle (background for Sub-lemma 3). -* Paper 1 (`paper/paper1-commute-time/main.tex`, Theorem 8.4 and - surrounding) and its Lean formalisation - (`ProofAtlas/Spectral/ProbabilisticBound.lean`) — the template. -* Paper 2 (`paper/paper2-electrical-resistance/main.tex`) — the target. diff --git a/lean/informal/semantic_BDFmetric_definition.md b/lean/informal/semantic_BDFmetric_definition.md deleted file mode 100644 index 71449c4..0000000 --- a/lean/informal/semantic_BDFmetric_definition.md +++ /dev/null @@ -1,204 +0,0 @@ -# A semantic axiomatisation of BDF distance - -Current axiomatic framework (`Metric/Axioms.lean`) describes a -*conductance schedule* `κ` with 5 axioms (D/O/C/A/S). Those axioms -are **descriptive** — they capture the formal shape of -`commuteTimeKappa` after symmetrisation, but they do not directly say -**what a BDF distance ought to be**. - -This document drafts an alternative axiomatisation: axioms directly -on the distance function `d : V × V → ℝ` itself, formulated to -capture the *meaning* a BDF distance should have on a proof -hypergraph. The current 5 κ-axioms then become *theorems* derivable -from the semantic definition + a harmonic normalisation. - -## Draft definition (mathematical form) - -Let `H = (V, E, c)` be a BDF hypergraph with colour weight -`α : C → ℝ_{>0}` and backward penalty `β ∈ (0, 1]`. - -**Definition (Semantic HypergraphMetric).** A function `d : V × V → ℝ` is a -*semantic HypergraphMetric* on `H` with parameters `(α, β)` if all of: - -1. **(M) Metric axioms.** `d` is a metric on `V`: - - `d(u, u) = 0`, - - `d(u, v) = d(v, u)`, - - `d(u, v) ≥ 0`, - - `d(u, w) ≤ d(u, v) + d(v, w)`. - -2. **(Aut) Automorphism invariance.** For every automorphism - `φ : H → H` of the coloured directed hypergraph, - `d(φ(u), φ(v)) = d(u, v)`. - *Reading*: the distance depends only on the abstract structure of - `H`, not on vertex labels. - -3. **(Func) Functoriality under sub-hypergraph inclusion.** For any - sub-hypergraph `H' = (V, E', c)` with `E' ⊆ E`, - `d_H(u, v) ≤ d_{H'}(u, v)` for every `u, v ∈ V`. - *Reading*: adding derivation rules to the proof system can only - shorten distances. Equivalently, restricting the system can only - lengthen them. - -4. **(Loc) Laplacian-additive structure.** There exists a symmetric - matrix decomposition - ``` - L = Σ_{e ∈ E} L_e - ``` - into per-edge contributions `L_e ∈ ℝ^{V × V}`, such that - `d(u, v) = (e_u - e_v)^⊤ L⁺ (e_u - e_v)`. Each `L_e` is symmetric - PSD and supported on the vertices incident to `e` - (i.e., `L_e(x, y) = 0` if either `x` or `y` is not in - `e^{\mathrm{in}} ∪ e^{\mathrm{out}}`). - *Reading*: the distance arises from an electrical-network - interpretation; the per-edge Laplacians capture the local - contribution of each rule. - -5. **(Bip) Per-edge bipartite structure.** For each `e ∈ E`, - `L_e(u, v) = 0` whenever `u, v` are both inputs of `e` or both - outputs of `e`. Off-diagonal entries are non-zero only between - inputs and outputs of the same rule. - *Reading*: a rule mediates only between its premises and its - conclusions, never horizontally within either tuple. - -6. **(Col) Colour separation.** The per-edge Laplacian factors as - `L_e = α(c(e)) · L_e^{\circ}` where `L_e^{\circ}` depends only on - the positional / arity data of `e`, not on its colour. - *Reading*: the colour weight `α` is a multiplicative "rule - strength"; the positional structure of the rule contributes - separately. - -7. **(Hrm) Harmonic position discount.** The total contribution of - input position `i` in rule `e` to the per-edge Laplacian satisfies - ``` - Σ_v L_e(inVertex_e(i), v) = α(c(e)) / (i + 1) · [normalisation] - ``` - for `i ∈ [0, p_e)`. The `1/(i+1)` factor encodes the convention - that earlier-indexed inputs carry more weight than later ones. - *Reading*: BDF inputs are ordered (major / minor premises); the - metric respects this ordering harmonically. *(Note: this is the - "harmonic normalisation" condition; weaker discount schedules are - possible if one wants a relaxed framework.)* - -8. **(BR) Backward penalty.** *(Optional)* The backward direction - (output → input) carries weight `β ∈ (0, 1]` relative to the - forward direction (input → output). Concretely, the contribution - of the backward leg is `β` times the corresponding forward leg. - *Reading*: the parameter `β` reflects how readable a proof is in - reverse; `β = 1` corresponds to a fully reversible proof; `β = 0` - to a strictly directed one. - -## Equivalence to the old axiomatic framework - -Under the semantic definition above, the old 5 κ-axioms (D/O/C/A/S) -of `Metric/Axioms.lean` become **theorems**: - -* **Theorem (D from Bip).** The marginalised conductance - `c_κ(u, v) := Σ_e κ(e, u, v)` (where `κ(e, u, v) := L_e(u, v)` for - `u ≠ v`) vanishes on same-side pairs by axiom (Bip). -* **Theorem (O from Hrm).** The harmonic position discount (Hrm) - forces the row sum `Σ_v κ(e, inVertex_e(i), v)` to be - monotone-decreasing in `i`. -* **Theorem (C from Col).** Immediate. -* **Theorem (A from Loc + Bip + symmetry).** Output uniformity - follows from the symmetric Laplacian structure plus per-edge - bipartite (Bip): once `Bip` ensures `L_e` is non-zero only on - (input, output) pairs, the symmetric PSD structure (Loc) forces - equal weight on outputs of the same edge. -* **Theorem (S from Loc).** `L_e` is symmetric by hypothesis, hence - so is `κ`. - -Conversely: - -* **Theorem (Reconstruction).** Any conductance `κ` satisfying - D/O/C/A/S together with harmonic normalisation induces a semantic - HypergraphMetric `d := R_eff(L_κ)`. (This is essentially what paper 2 - studies.) - -## Axiomatic uniqueness (semantic version) - -A clean statement of uniqueness in the semantic framework would be: - -**Theorem (Axiomatic uniqueness).** *Up to the choice of colour weight -`α : C → ℝ_{>0}` and backward penalty `β ∈ (0, 1]`, there is a unique -semantic HypergraphMetric on every BDF hypergraph: it is the effective -resistance `d_R` of the harmonic conductance Laplacian.* - -This is a much stronger and more interpretable statement than the -existence-and-witness pair of the current axiomatisation. - -## Lean draft - -For Lean, the natural form would be: - -```lean -structure SemanticHypergraphMetric {V : Type*} [Fintype V] [DecidableEq V] - {C : Type*} (H : Hypergraph V C) (params : WalkParams C) where - /-- The distance function. -/ - d : V → V → ℝ - /-- Metric axioms. -/ - d_self : ∀ u, d u u = 0 - d_symm : ∀ u v, d u v = d v u - d_nonneg : ∀ u v, 0 ≤ d u v - d_triangle : ∀ u v w, d u w ≤ d u v + d v w - /-- Automorphism invariance: for any structure-preserving - permutation of V respecting the hypergraph and colour - structure, d is invariant. -/ - d_automorph_invariant : ∀ φ : V ≃ V, - (∀ e u, H.isInput u e ↔ H.isInput (φ u) e) → - (∀ e u, H.isOutput u e ↔ H.isOutput (φ u) e) → - (∀ u v, d (φ u) (φ v) = d u v) - /-- Functoriality: sub-hypergraph inclusion lengthens distances. -/ - d_functorial : ∀ (H' : Hypergraph V C) (h_sub : ∀ e, e ∈ H'.edges → e ∈ H.edges), - ∀ u v, d u v ≤ /- d-on-H' u v, with the obvious notation -/ ??? - /-- Laplacian-additive structure: d arises from a PSD Laplacian - that decomposes into per-edge contributions. -/ - laplacian : Matrix V V ℝ - perEdgeLaplacian : H.edges → Matrix V V ℝ - laplacian_decomp : laplacian = ∑ e, perEdgeLaplacian e - perEdgeLaplacian_posSemidef : ∀ e, (perEdgeLaplacian e).PosSemidef - d_eq_R_eff : ∀ u v, d u v = ProofAtlas.LinearAlgebra.effectiveResistance laplacian u v - /-- Per-edge bipartite structure. -/ - perEdgeLaplacian_bipartite : ∀ e u v, - (H.isInput u e ∧ H.isInput v e) ∨ (H.isOutput u e ∧ H.isOutput v e) → - perEdgeLaplacian e u v = 0 - /-- Colour separation: per-edge Laplacian factors through α(c(e)). -/ - perEdgeLaplacian_color_separation : ∃ L_circ : H.edges → Matrix V V ℝ, - ∀ e, perEdgeLaplacian e = params.alpha (H.color e) • L_circ e - /-- Harmonic position discount: total contribution of input - position i to perEdgeLaplacian is proportional to 1/(i+1). -/ - perEdgeLaplacian_harmonic : ∀ e (i : Fin (H.inArity e)), - ∑ v, perEdgeLaplacian e (H.inVertex e i) v - = params.alpha (H.color e) / (i.val + 1) -- (modulo normalization) -``` - -The signature is heavier than the κ-based `HypergraphMetric` of -`Metric/Axioms.lean`, but each field has a clear *semantic* meaning. -The current `HypergraphMetric` becomes a (low-level) constructor for -`SemanticHypergraphMetric`. - -## Trade-offs - -| Axis | Current 5 κ-axioms | Semantic axioms on d | -|---|---|---| -| **Interpretability** | descriptive of κ | semantic — says what d *means* | -| **Reflects "BDF-ness"** | indirectly (κ shape) | directly (d's behaviour under operations) | -| **Self-contained existence** | yes (5 axioms force κ) | yes (with harmonic normalisation) | -| **Lean weight** | ~8 lines structure + 5 props | ~15 lines structure + 9 props | -| **Yield "Rayleigh monotonicity" theorem** | indirectly | as axiom (Func) | -| **Yield "axiomatic uniqueness" theorem** | possible but needs work | almost-free (semantic axioms pin d down) | -| **Compatibility with paper 1** | direct | requires §3 rewrite | - -## Suggested next step - -Promote `SemanticHypergraphMetric` to a *parallel* definition alongside the -current `HypergraphMetric`, prove their equivalence, and reframe paper 1 §3 -around the semantic version as the "primary" axiomatisation. The -existence witness then becomes: - - *Theorem (Existence).* The harmonic schedule of paper 1 §3.2 induces - a (unique up to (α, β)) semantic HypergraphMetric on every BDF hypergraph. - -Net effect: paper 1's narrative shifts from "here's a metric, it has -nice properties" to "the HypergraphMetric structure characterises the canonical -metric on BDF; the harmonic walk realises it." This is qualitatively -stronger. diff --git a/lean/informal/tensor_representation_notes.md b/lean/informal/tensor_representation_notes.md deleted file mode 100644 index 40bee9d..0000000 --- a/lean/informal/tensor_representation_notes.md +++ /dev/null @@ -1,134 +0,0 @@ -# Tensor representation of BDF hypergraphs - -**Status:** speculative; not on paper 1 or paper 2 critical path. -Recorded 2026-05-20 after a friend's observation that an `r`-uniform -hypergraph is naturally a tensor, and a general hypergraph can be -made uniform via dummy padding. - -## The basic encoding - -Fix a BDF hypergraph `H = (V, E, c)` with colour set `C`. Let -`p_e, q_e` be the input/output arities of edge `e`, and set -``` - r := max_{e ∈ E} (p_e + q_e), V⋆ := V ⊔ {⋆}. -``` -Each edge `e` with `e^in = (u_1, ..., u_{p_e})` and -`e^out = (w_1, ..., w_{q_e})` gives one entry in an order-`r` tensor -``` - T ∈ ℝ^{V⋆ × ⋯ × V⋆ × C} (r vertex slots + one colour slot) -``` -at the slice -``` - T[u_1, ..., u_{p_e}, w_1, ..., w_{q_e}, ⋆, ..., ⋆, c(e)] = 1. -``` -The first `p_e` slots are *input* positions (ordered); -the next `q_e` slots are *output* positions (ordered); -remaining slots are padded with the dummy `⋆`. - -Reading the four BDF axioms off this object: -- **directed** ⇔ the slot index set is partitioned into "input - block" and "output block". -- **ordered** ⇔ the tensor is *not* symmetric in either block; slot - position matters. -- **coloured** ⇔ the colour index `c ∈ C` is a separate axis. -- **acyclic** ⇔ a global combinatorial constraint on the support of - `T` (no easy tensor-level expression — this is the bit the tensor - view does *not* simplify). - -## Where this connects to existing literature - -For *uniform* hypergraphs (constant edge size `r`), there is a -mature tensor-spectral theory: - -- **Cooper–Dutle adjacency tensor.** `A ∈ ℝ^{V × ⋯ × V}` (order `r`), - symmetric, with `A[v_{i_1}, ..., v_{i_r}] = 1/(r-1)!` over - permutations of an edge. Z-eigenvalues and H-eigenvalues of `A` - generalise the matrix spectrum. -- **Qi tensor Laplacian.** `L = D - A` with `D` a diagonal tensor of - degrees; spectral gap controls expansion of `r`-uniform graphs. -- **Banerjee–Char–Mondal / Hu–Qi.** Various normalisations and - signless variants; connections to combinatorial expansion, - hypergraph colouring, and tensor decompositions. - -These are all *symmetric* tensor theories. BDF is *not* symmetric in -the slot indices (orderedness + directedness), so applying these -directly drops information. - -## Our `L_κ` as a 2-marginalisation - -The harmonic Laplacian `L_κ` of paper 2 is obtained from `T` by: -1. Fix two vertex slots `i ∈ input block` and `j ∈ output block`. -2. Contract over all *other* slots (with the appropriate harmonic - weight `1/(i+1)`, the colour weight `α(c(e))`, and the backward - penalty `β`). -3. Symmetrise the result over `(i, j)`. - -The output is a matrix on `V × V` (rank-2 tensor). So `L_κ` is a -*projection* of the full tensor `T` onto two vertex slots — a -2-marginalisation. The Dirichlet energy `E_H(f) = f^⊤ L_κ f` is the -quadratic form of this marginalisation. - -## Possible lifts (not done) - -1. **Order-`r` Dirichlet energy.** Define - ``` - E_H^{(r)}(f) := ∑_e ∑_{i, j positions} w_{e, i, j} · - (some polynomial in f over all slots of e). - ``` - The natural choice is a degree-`r` polynomial in `f`, mixing all - `p_e + q_e` arguments of each edge, not just pairs. - Variational minimisation gives a *non-quadratic* energy — no - linear Laplacian, but a tensor-Laplacian operator with H- or - Z-eigenvalues. - -2. **Asymmetric tensor-Laplacian for BDF.** Adapt - Cooper–Dutle/Qi to the input/output-partitioned, ordered case. - The natural object is a *bipartite* tensor Laplacian on - `V_in × V_out` slot blocks, with the harmonic positional - weighting baked in. - -3. **Tensor eigenvalues and hyperbolicity.** Conjecturally the - four-point Gromov deviation should admit a higher-order spectral - expansion in terms of tensor H-eigenvalues / Z-eigenvalues of the - asymmetric BDF tensor. If true, this would *replace* the - `(1 - λ_i)^{-1}` resolvent expansion of paper 1's reversible - commute-time spectrum with a genuinely multi-mode tensor - resolvent. This is currently pure speculation. - -## Why this is not in paper 2 - -- Paper 2's main theorem (existence/uniqueness of the variational - metric) is a quadratic-energy result; the tensor lift is a - *different* metric (or family thereof), defined by a higher-degree - variational problem. -- Padding with `⋆` is artificial. It introduces spectrum artefacts - from the dummy slot and obscures the proof-theoretic meaning of - the per-edge data. -- The tensor view drops one BDF axiom (acyclicity) at the tensor - level; you have to reintroduce it as a side condition on `T`'s - support. - -## What might be worth doing later - -- Write down `E_H^{(r)}` concretely on a small example (say a single - ternary inference rule) and compare the order-3 minimiser to the - pair-marginalised `L_κ` minimiser. -- Check if any of the existing uniform-hypergraph tensor-Laplacian - spectral theorems (e.g.\ Cooper–Dutle's bound on the largest - eigenvalue, or Qi-spectral gap = expansion) survive the - asymmetric / ordered / coloured generalisation. -- See whether the order-`r` Dirichlet variational problem still has - unique minimisers (no — degree-`r > 2` functionals generically - have many critical points). This is the main obstruction to - lifting paper 2's existence theorem to the tensor regime. - -## Pointers (rough; not yet collected) - -- L.-H. Lim, *Singular values and eigenvalues of tensors* (2005). -- L. Qi, *Eigenvalues of a real supersymmetric tensor* (2005). -- J. Cooper, A. Dutle, *Spectra of uniform hypergraphs* (2012). -- A. Banerjee, *Spectra of general hypergraphs* (2017). -- W. Hu, L. Qi, *Algebraic connectivity of an even uniform - hypergraph* (2012). - -(These are not yet added to `paper/.../reference.bib`.) diff --git a/lean/informal/thomson_principle.md b/lean/informal/thomson_principle.md deleted file mode 100644 index aaf826f..0000000 --- a/lean/informal/thomson_principle.md +++ /dev/null @@ -1,189 +0,0 @@ -# Thomson principle for the harmonic BDF Laplacian - -Informal proof sketch prepared by the iter-012 plan agent for the -prover targeting `ProofAtlas/Metric/Electrical/Algebraic.lean:185` -(`Hypergraph.thomson_principle`). - -## Statement (current Lean signature) - -```lean -theorem thomson_principle [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (u v : V) (_huv : u ≠ v) - (_h_pos : 0 < H.resistanceDist params u v) : - sInf { e : ℝ | ∃ f : V → ℝ, - f u - f v = 1 ∧ H.dirichletEnergy params f = e } - = 1 / H.resistanceDist params u v -``` - -Internally: -* `H.resistanceDist params u v - = effectiveResistance (H.harmonicLaplacian params) u v - = (signedIndicator u v)ᵀ (pseudoinverse L) (signedIndicator u v)` - where `L := H.harmonicLaplacian params` (PSD, Hermitian). -* `H.dirichletEnergy params f = fᵀ L f` after the §3.3 chain - (`dirichletEnergy_eq_laplacianForm`, already closed). - -So writing `x := signedIndicator u v = e_u − e_v` and `M := L⁺`: -``` -goal: sInf { fᵀ L f | f(u) − f(v) = 1 } = 1 / (xᵀ M x) -``` - -## Key Mathlib / project lemmas - -From `ProofAtlas/LinearAlgebra/MoorePenrose.lean`: -* `mul_pseudoinverse_mul_self` : `L · L⁺ · L = L` -* `pseudoinverse_mul_self_mul_pseudoinverse` : `L⁺ · L · L⁺ = L⁺` -* `mul_pseudoinverse_isHermitian` : `L · L⁺` is Hermitian -* `pseudoinverse_mul_self_isHermitian`: `L⁺ · L` is Hermitian -* `pseudoinverse_isHermitian_of_isHermitian` -* `pseudoinverse_posSemidef` : `L⁺` is PSD - -From `ProofAtlas/Metric/Electrical/Algebraic.lean`: -* `dirichletEnergy_eq_laplacianForm` : `E_H(f) = fᵀ L f` (closed iter-009) -* `harmonicLaplacian_quadForm` : `fᵀ L f = (1/2) ∑ c (f u − f v)²` -* `harmonicLaplacian_posSemidef` : `L` PSD (closed iter-010) - -From `ProofAtlas/LinearAlgebra/EffectiveResistance.lean` & -`SpectralExpansion.lean`: -* `effectiveResistance_spectral_expansion` (closed) - -## Connectivity caveat (READ BEFORE STARTING) - -The proof below requires `x := e_u − e_v ∈ range(L)`. Equivalently, -`(L⁺ L) x = x`, equivalently `x ⊥ ker(L)`. For a *connected* graph -ker(L) = span{𝟙}, and `xᵀ𝟙 = 1 − 1 = 0` automatically. For a -disconnected graph with `u` and `v` in different components, -`x` has a non-zero kernel component and Thomson's principle **as -stated in the current signature fails** (the infimum is 0, not -`1/d_H`). - -**The current hypothesis `0 < H.resistanceDist params u v` is too -weak**: it ensures the projection `P_{range(L)} x ≠ 0` but does NOT -force `x ⊥ ker(L)`. - -**Action items for the prover:** -1. Try the proof assuming `x ⊥ ker(L)` and see whether - `0 < d_H(u, v)` plus the structure of the harmonic Laplacian - already implies it (it does *not* in full generality, but the - prover should confirm by inspecting `harmonicLaplacian`'s kernel - structure). -2. **If the kernel structure doesn't pin things down**, amend the - signature to add a hypothesis `_h_kernel : ∀ z, L *ᵥ z = 0 → - signedIndicator u v ⬝ᵥ z = 0` (or equivalently - `_h_in_range : ∃ y, L *ᵥ y = signedIndicator u v`). This is the - honest statement and matches Mathlib's pseudoinverse conventions. -3. Document the amendment in `task_results/Algebraic.lean.md` for - the iter-013 plan. - -## Two-step proof outline - -Let `x := signedIndicator u v`, `M := L⁺`, `d := xᵀ M x = -H.resistanceDist params u v`. We have `d > 0` by hypothesis. - -### Step 1 (upper bound): exhibit `f*` achieving `E_H(f*) = 1/d`. - -Define `f* : V → ℝ` by `f* := (1/d) · (L⁺ *ᵥ x)`. - -* `f*(u) − f*(v) = xᵀ f* = (1/d) · xᵀ L⁺ x = (1/d) · d = 1`. ✓ - (Uses `dotProduct_signedIndicator` if a helper exists, else expand - `signedIndicator` directly.) -* `E_H(f*) = (f*)ᵀ L (f*) = (1/d²) · (L⁺ x)ᵀ L (L⁺ x) - = (1/d²) · xᵀ (L⁺ L L⁺) x` - `= (1/d²) · xᵀ L⁺ x` (by `pseudoinverse_mul_self_mul_pseudoinverse`) - `= (1/d²) · d = 1/d`. ✓ - -So `1/d ∈ { fᵀ L f | f(u) − f(v) = 1 }`, hence `sInf ≤ 1/d`. - -### Step 2 (lower bound): for every `f` with `f(u) − f(v) = 1`, -`E_H(f) ≥ 1/d`. - -PSD Cauchy–Schwarz on `L`: -``` -(yᵀ L f)² ≤ (yᵀ L y) · (fᵀ L f) (*) -``` -Take `y := M x = L⁺ x = d · f*` (so `y = d · f*`). - -* `yᵀ L y = (L⁺ x)ᵀ L (L⁺ x) = xᵀ L⁺ L L⁺ x = xᵀ L⁺ x = d`. - (Same identity as in Step 1.) -* `yᵀ L f = (L⁺ x)ᵀ L f = xᵀ (L⁺ L) f`. - -Now `L⁺ L` is the orthogonal projection onto `range(Lᵀ) = range(L)` -(since `L` is Hermitian). So `xᵀ (L⁺ L) f = (L⁺ L x)ᵀ f`. Using -the connectivity caveat above (`L⁺ L x = x`), this collapses to: -``` -yᵀ L f = xᵀ f = f(u) − f(v) = 1. -``` - -Plugging into (*): -``` -1 ≤ d · (fᵀ L f) -⇒ fᵀ L f ≥ 1/d -⇒ E_H(f) ≥ 1/d. -``` - -So `1/d` is a lower bound for the set, and combined with Step 1 -it is the infimum. - -### Achieving the infimum (formal handling) - -Lean's `sInf` for a set `S ⊆ ℝ`: -* If the set is empty, `sInf S = 0` (Mathlib convention). -* Else `sInf S = ⨅ s ∈ S, s` and is the greatest lower bound iff `S` is - bounded below. - -Use `IsGLB.csInf_eq` (or `csInf_eq_of_forall_ge_of_forall_gt_exists_lt`): -* Build the witness `(f*, E_H(f*) = 1/d) ∈ S` to show non-emptiness. -* Show `1/d` is a lower bound via Step 2. -* Show no element `< 1/d` is a lower bound: but actually for our - purpose `1/d` is *attained*, so it's a minimum. Use - `IsLeast.csInf_eq`. - -The PSD Cauchy–Schwarz lemma needed: -``` -∀ (M : Matrix V V ℝ), Matrix.PosSemidef M → - ∀ (x y : V → ℝ), (Matrix.dotProduct x (M *ᵥ y))^2 - ≤ (Matrix.dotProduct x (M *ᵥ x)) * - (Matrix.dotProduct y (M *ᵥ y)) -``` -* Search Mathlib via `lean_loogle "PosSemidef.*dotProduct"` and - `lean_leansearch "PSD Cauchy Schwarz quadratic form"`. -* If absent, prove it as a private helper at the top of `Algebraic.lean`: - expand `M = Aᵀ A` (use `Matrix.PosSemidef.sqrt` or the spectral - decomposition already in `MoorePenrose.lean`); the inequality - becomes the standard inner-product Cauchy–Schwarz on `A x` and - `A y`. - -## Implementation tips - -1. **Unfold strategy.** Don't fight the definitions. Open `Algebraic` - and `effectiveResistance`-level unfolds early so the goal is in - `dotProduct`/`mulVec` shape. -2. **Reuse `dirichletEnergy_eq_laplacianForm`.** After unfolding, - the goal is purely in terms of `fᵀ L f` and `xᵀ L⁺ x`. Don't - re-derive the energy ↔ quadratic-form equivalence. -3. **`pseudoinverse_mul_self_mul_pseudoinverse` is the - work-horse.** Both bounds reduce to applying it to collapse - `L⁺ L L⁺` to `L⁺`. -4. **PSD Cauchy–Schwarz**: if not in Mathlib, derive it via - `Matrix.PosSemidef.sqrt` (or `pseudoinverse_spectral_expansion` - for our specific `L⁺`) — squared distances reduce to standard - Cauchy–Schwarz on the squared-root operator. -5. **Connectivity gap**: if needed, amend the signature. Honest - gaps recorded explicitly are preferred over silently sorry'd - sub-claims. - -## Difficulty estimate - -* Step 1 (witness): low–medium. Direct computation. -* Step 2 (lower bound): medium–high. Requires PSD Cauchy–Schwarz - (may need helper) and the `L⁺ L x = x` collapse. -* Total: medium–high. Should be tractable in one prover round if - PSD Cauchy–Schwarz is available; multi-round if a custom - Cauchy–Schwarz must be built from scratch. - -## Reference - -* Doyle–Snell, *Random Walks and Electric Networks* (1984), §3.3. -* Bapat, *Graphs and Matrices* (2010), Ch. 9. -* Klein–Randić, *Resistance distance*, J. Math. Chem. 12 (1993), §3. From 8d9b297da3e5f8b3924cf77d68485337e0650f5f Mon Sep 17 00:00:00 2001 From: Xinze-Li-Moqian <70414198+Xinze-Li-Moqian@users.noreply.github.com> Date: Tue, 26 May 2026 22:24:03 -0400 Subject: [PATCH 02/21] chore: remove all non-facade code from main MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Delete MCMC/, RandomWalk/Spectral/, Metric/CommuteTime/, Metric/JensenShannon/, Metric/Diffusion/, Hyperbolicity/, Hypergraph/Sequence.lean, Open.lean from main. Fix imports in Metric/Default, Resistance/Resistance, Status, lakefile.toml, MetricRegistry, ProofAtlas.lean. Build: 3488 → 2661 jobs. Zero MCMC warnings. Zero sorry. 1 proven metric (resistanceDist). All commands work. All deleted code preserved on development branch. --- lean/MCMC.lean | 15 - lean/MCMC/Finite/Convergence.lean | 569 -------- lean/MCMC/Finite/Core.lean | 146 -- lean/MCMC/Finite/Gibbs.lean | 301 ---- lean/MCMC/Finite/MetropolisHastings.lean | 236 ---- lean/MCMC/Finite/TotalVariation.lean | 646 --------- lean/MCMC/Finite/toKernel.lean | 515 ------- lean/MCMC/LICENSE.md | 7 - .../PF/Analysis/CstarAlgebra/Classes.lean | 418 ------ lean/MCMC/PF/Combinatorics/Quiver/Cyclic.lean | 287 ---- lean/MCMC/PF/Combinatorics/Quiver/Path.lean | 1115 --------------- lean/MCMC/PF/Data/List.lean | 682 --------- .../Matrix/PerronFrobenius/Aperiodic.lean | 97 -- .../PerronFrobenius/CollatzWielandt.lean | 888 ------------ .../Matrix/PerronFrobenius/Dominance.lean | 1242 ----------------- .../Matrix/PerronFrobenius/Irreducible.lean | 622 --------- .../Matrix/PerronFrobenius/Lemmas.lean | 447 ------ .../Matrix/PerronFrobenius/Multiplicity.lean | 273 ---- .../Matrix/PerronFrobenius/Primitive.lean | 308 ---- .../Matrix/PerronFrobenius/Stochastic.lean | 81 -- .../Matrix/PerronFrobenius/Uniqueness.lean | 97 -- .../PF/LinearAlgebra/Matrix/Spectrum.lean | 696 --------- .../Topology/Compactness/ExtremeValueUSC.lean | 204 --- lean/MCMC/PF/aux.lean | 850 ----------- lean/ProofAtlas.lean | 46 +- lean/ProofAtlas/Command/Status.lean | 3 - lean/ProofAtlas/Hyperbolicity/Basic.lean | 551 -------- .../ProofAtlas/Hyperbolicity/CommuteTime.lean | 111 -- lean/ProofAtlas/Hyperbolicity/Default.lean | 28 - lean/ProofAtlas/Hyperbolicity/Diffusion.lean | 47 - lean/ProofAtlas/Hyperbolicity/Resistance.lean | 46 - lean/ProofAtlas/Hypergraph/Sequence.lean | 203 --- .../Metric/CommuteTime/CommuteTime.lean | 346 ----- .../Metric/CommuteTime/HittingTime.lean | 459 ------ .../Metric/CommuteTime/Instance.lean | 102 -- .../Metric/CommuteTime/KilledChain.lean | 262 ---- lean/ProofAtlas/Metric/Default.lean | 23 +- lean/ProofAtlas/Metric/Diffusion/Basic.lean | 180 --- .../ProofAtlas/Metric/Diffusion/Instance.lean | 26 - .../Metric/JensenShannon/Basic.lean | 145 -- .../Metric/JensenShannon/Instance.lean | 25 - .../Metric/Resistance/Resistance.lean | 1 - lean/ProofAtlas/Open.lean | 49 - .../RandomWalk/Spectral/Bridge.lean | 95 -- .../RandomWalk/Spectral/Decomposition.lean | 232 --- .../RandomWalk/Spectral/Default.lean | 41 - .../RandomWalk/Spectral/Expectation.lean | 125 -- .../RandomWalk/Spectral/Moments.lean | 487 ------- .../Spectral/ProbabilisticBound.lean | 354 ----- .../RandomWalk/Spectral/SpectralForm.lean | 237 ---- .../RandomWalk/Spectral/Spectrum.lean | 210 --- .../RandomWalk/Spectral/Stationary.lean | 136 -- .../Util/Linter/MetricRegistry.lean | 3 - lean/lakefile.toml | 7 - lean/scripts/check_metric_instances.sh | 3 - whitepaper/whitepaper-zh/whitepaper-zh.xdv | Bin 0 -> 414400 bytes 56 files changed, 7 insertions(+), 15318 deletions(-) delete mode 100644 lean/MCMC.lean delete mode 100644 lean/MCMC/Finite/Convergence.lean delete mode 100644 lean/MCMC/Finite/Core.lean delete mode 100644 lean/MCMC/Finite/Gibbs.lean delete mode 100644 lean/MCMC/Finite/MetropolisHastings.lean delete mode 100644 lean/MCMC/Finite/TotalVariation.lean delete mode 100644 lean/MCMC/Finite/toKernel.lean delete mode 100644 lean/MCMC/LICENSE.md delete mode 100644 lean/MCMC/PF/Analysis/CstarAlgebra/Classes.lean delete mode 100644 lean/MCMC/PF/Combinatorics/Quiver/Cyclic.lean delete mode 100644 lean/MCMC/PF/Combinatorics/Quiver/Path.lean delete mode 100644 lean/MCMC/PF/Data/List.lean delete mode 100644 lean/MCMC/PF/LinearAlgebra/Matrix/PerronFrobenius/Aperiodic.lean delete mode 100644 lean/MCMC/PF/LinearAlgebra/Matrix/PerronFrobenius/CollatzWielandt.lean delete mode 100644 lean/MCMC/PF/LinearAlgebra/Matrix/PerronFrobenius/Dominance.lean delete mode 100644 lean/MCMC/PF/LinearAlgebra/Matrix/PerronFrobenius/Irreducible.lean delete mode 100644 lean/MCMC/PF/LinearAlgebra/Matrix/PerronFrobenius/Lemmas.lean delete mode 100644 lean/MCMC/PF/LinearAlgebra/Matrix/PerronFrobenius/Multiplicity.lean delete mode 100644 lean/MCMC/PF/LinearAlgebra/Matrix/PerronFrobenius/Primitive.lean delete mode 100644 lean/MCMC/PF/LinearAlgebra/Matrix/PerronFrobenius/Stochastic.lean delete mode 100644 lean/MCMC/PF/LinearAlgebra/Matrix/PerronFrobenius/Uniqueness.lean delete mode 100644 lean/MCMC/PF/LinearAlgebra/Matrix/Spectrum.lean delete mode 100644 lean/MCMC/PF/Topology/Compactness/ExtremeValueUSC.lean delete mode 100644 lean/MCMC/PF/aux.lean delete mode 100644 lean/ProofAtlas/Hyperbolicity/Basic.lean delete mode 100644 lean/ProofAtlas/Hyperbolicity/CommuteTime.lean delete mode 100644 lean/ProofAtlas/Hyperbolicity/Default.lean delete mode 100644 lean/ProofAtlas/Hyperbolicity/Diffusion.lean delete mode 100644 lean/ProofAtlas/Hyperbolicity/Resistance.lean delete mode 100644 lean/ProofAtlas/Hypergraph/Sequence.lean delete mode 100644 lean/ProofAtlas/Metric/CommuteTime/CommuteTime.lean delete mode 100644 lean/ProofAtlas/Metric/CommuteTime/HittingTime.lean delete mode 100644 lean/ProofAtlas/Metric/CommuteTime/Instance.lean delete mode 100644 lean/ProofAtlas/Metric/CommuteTime/KilledChain.lean delete mode 100644 lean/ProofAtlas/Metric/Diffusion/Basic.lean delete mode 100644 lean/ProofAtlas/Metric/Diffusion/Instance.lean delete mode 100644 lean/ProofAtlas/Metric/JensenShannon/Basic.lean delete mode 100644 lean/ProofAtlas/Metric/JensenShannon/Instance.lean delete mode 100644 lean/ProofAtlas/Open.lean delete mode 100644 lean/ProofAtlas/RandomWalk/Spectral/Bridge.lean delete mode 100644 lean/ProofAtlas/RandomWalk/Spectral/Decomposition.lean delete mode 100644 lean/ProofAtlas/RandomWalk/Spectral/Default.lean delete mode 100644 lean/ProofAtlas/RandomWalk/Spectral/Expectation.lean delete mode 100644 lean/ProofAtlas/RandomWalk/Spectral/Moments.lean delete mode 100644 lean/ProofAtlas/RandomWalk/Spectral/ProbabilisticBound.lean delete mode 100644 lean/ProofAtlas/RandomWalk/Spectral/SpectralForm.lean delete mode 100644 lean/ProofAtlas/RandomWalk/Spectral/Spectrum.lean delete mode 100644 lean/ProofAtlas/RandomWalk/Spectral/Stationary.lean create mode 100644 whitepaper/whitepaper-zh/whitepaper-zh.xdv diff --git a/lean/MCMC.lean b/lean/MCMC.lean deleted file mode 100644 index ac8c048..0000000 --- a/lean/MCMC.lean +++ /dev/null @@ -1,15 +0,0 @@ -/- -This file is a facade for the vendored `MCMC` library. - -The contents of `MCMC/` are vendored verbatim from -https://github.com/mkaratarakis/HopfieldNet (MIT licence; see -`MCMC/LICENSE.md`) as of 2026-02-10. They provide the Perron-Frobenius -theorem for non-negative irreducible matrices and the existence and -uniqueness of the stationary distribution of an irreducible finite -Markov chain. - -The `ProofAtlas` library depends on `MCMC.Finite.Core` for -`MCMC.Finite.stationaryDistribution`, which is wired into -`Hypergraph.stationaryDist`. --/ -import MCMC.Finite.Core diff --git a/lean/MCMC/Finite/Convergence.lean b/lean/MCMC/Finite/Convergence.lean deleted file mode 100644 index 2238a17..0000000 --- a/lean/MCMC/Finite/Convergence.lean +++ /dev/null @@ -1,569 +0,0 @@ -import MCMC.Finite.TotalVariation -import Mathlib.Analysis.Asymptotics.SpecificAsymptotics - -set_option linter.unusedSectionVars false - -namespace MCMC.Finite -open Matrix Finset Filter Topology -open scoped BigOperators - -variable {n : Type*} [Fintype n] [DecidableEq n] - -/-! ### Convergence Theorems -/ - -/-! #### The limit matrix -/ - -/-- - The limit matrix Π. A rank-1 matrix where every row equals the stationary distribution π. - Πᵢⱼ = πⱼ. --/ -def LimitMatrix (π : stdSimplex ℝ n) : Matrix n n ℝ := - fun _ j => π.val j - -omit [DecidableEq n] in -lemma LimitMatrix_is_stochastic (π : stdSimplex ℝ n) : IsStochastic (LimitMatrix π) := by - constructor - · intro i j; exact π.property.1 j - · intro i; simp [LimitMatrix, π.property.2] - -omit [DecidableEq n] in -/-- P * Π = Π and Π * P = Π. The limit matrix is absorbing. -/ -theorem LimitMatrix_absorbing (P : Matrix n n ℝ) (π : stdSimplex ℝ n) (h_stoch : IsStochastic P) (h_stat : IsStationary P π) : - P * LimitMatrix π = LimitMatrix π ∧ LimitMatrix π * P = LimitMatrix π := by - constructor - · -- P * Π = Π (Relies on P being stochastic). - ext i j - simp only [LimitMatrix, mul_apply] - calc - ∑ k, P i k * π.val j - = (∑ k, P i k) * π.val j := by rw [Finset.sum_mul] - _ = π.val j := by rw [h_stoch.2 i, one_mul] - · -- Π * P = Π (Relies on π being stationary). - ext i j - simp only [LimitMatrix, mul_apply] - have h_stat_j := congrArg (fun v => v j) h_stat - simp [mulVec, transpose_apply] at h_stat_j - simpa [mul_comm] using h_stat_j - -/-! #### Convergence to Equilibrium (Matrix Convergence) -/ - -/- - Primitivity implies a spectral gap (1 is a simple eigenvalue, others < 1 in magnitude). - This spectral gap guarantees geometric convergence of P^k. - We encode a spectral gap by uniform exponential entrywise convergence of P^k to the rank-1 - limit matrix built from a stationary distribution π, with some rate r ∈ [0,1). --/ -/-def HasSpectralGap' (P : Matrix n n ℝ) : Prop := - ∃ (π : stdSimplex ℝ n) (r : ℝ), - IsStationary P π ∧ 0 ≤ r ∧ r < 1 ∧ - ∀ i j k, |(P^k) i j - (LimitMatrix π) i j| ≤ r^k-/ - -/-- - Spectral gap encoded by: there exist π, r ∈ [0,1), and a positive block length k0, - s.t. the entrywise error is bounded by r^(k / k0) for all k. - This form still implies convergence, and is what we can prove from Dobrushin’s coefficient - and primitivity (a positive power strictly contracts TV). --/ -def HasSpectralGap (P : Matrix n n ℝ) : Prop := - ∃ (π : stdSimplex ℝ n) (r : ℝ) (k0 : ℕ), - IsStationary P π ∧ 0 < k0 ∧ 0 ≤ r ∧ r < 1 ∧ - ∀ i j k, |(P^k) i j - (LimitMatrix π) i j| ≤ r^(k / k0) - -lemma IsPrimitive.irreducible [Nonempty n] {P : Matrix n n ℝ} - (_ : IsStochastic P) (h_prim : IsPrimitive P) : - Matrix.IsIrreducible P := by - exact IsPrimitive.isIrreducible h_prim - -lemma pow_stationary_mulVec [Nonempty n] (P : Matrix n n ℝ) (k : ℕ) - (_ : IsStochastic P) (π : stdSimplex ℝ n) (h_stat : IsStationary P π) : - (P^k)ᵀ *ᵥ π.val = π.val := by - induction' k with k ih - · simp [pow_zero] - · - have ih' : (Pᵀ ^ k) *ᵥ π.val = π.val := by - simpa [Matrix.transpose_pow] using ih - calc - (P ^ (k + 1))ᵀ *ᵥ π.val - = (Pᵀ * (Pᵀ ^ k)) *ᵥ π.val := by - simp [pow_succ, Matrix.transpose_mul, Matrix.transpose_pow] - _ = Pᵀ *ᵥ ((Pᵀ ^ k) *ᵥ π.val) := by - simp_rw [Matrix.mul_mulVec] - _ = Pᵀ *ᵥ π.val := by - simp [ih'] - _ = π.val := by - simpa [IsStationary] using h_stat - -/-- Point mass at i as a row-distribution. -/ -private def delta (i : n) : n → ℝ := fun t => if t = i then (1 : ℝ) else 0 - -private lemma delta_sum_one (i : n) : ∑ t, delta i t = 1 := by - classical - simp [delta, Finset.mem_univ] - -private lemma delta_nonneg (i : n) : ∀ t, 0 ≤ delta i t := by - intro t; classical - by_cases h : t = i <;> simp [delta, h] - -/-- Primitivity ⇒ some power has a strict Dobrushin contraction; - from this we build a spectral gap. -/ -lemma IsPrimitive.has_spectral_gap [Nonempty n] {P : Matrix n n ℝ} - (h_stoch : IsStochastic P) (h_prim : IsPrimitive P) : HasSpectralGap P := by - classical - have h_irred : Matrix.IsIrreducible P := IsPrimitive.irreducible h_stoch h_prim - obtain ⟨π, hπ_stat, _hπ_unique⟩ := - exists_unique_stationary_distribution_of_irreducible h_stoch h_irred - obtain ⟨k0, hk0_pos, hδ_lt⟩ := - Matrix.dobrushinCoeff_lt_one_of_primitive (P := P) h_stoch h_prim - let r := Matrix.dobrushinCoeff (P^k0) - have hr_lt_one : r < 1 := hδ_lt - have hr0 : 0 ≤ r := Matrix.dobrushinCoeff_nonneg (P := P^k0) - refine ⟨π, r, k0, hπ_stat, hk0_pos, hr0, hr_lt_one, ?_⟩ - intro i j k - set q := k / k0 with hq - set s := k % k0 with hs - have hk_decomp : k = q * k0 + s := by - rw [hq, hs]; exact Eq.symm (Nat.div_add_mod' k k0) - have h_entry_le_tv : - |(P^k) i j - (LimitMatrix π) i j| - ≤ Matrix.tvDist (Matrix.rowDist (P^k) i) (π.val) := by - have hsum_row : ∑ t, Matrix.rowDist (P^k) i t = 1 := by - have hPk : IsStochastic (P^k) := by - simpa using (isStochastic_pow (P := P) (hP := h_stoch) k) - simpa [Matrix.rowDist] using hPk.2 i - have hsum_π : ∑ t, π.val t = 1 := π.property.2 - have hsum_eq : ∑ t, Matrix.rowDist (P^k) i t = ∑ t, π.val t := by - simpa [Matrix.rowDist, hsum_row, hsum_π] - simpa [Matrix.rowDist, LimitMatrix] using - (Matrix.entry_abs_le_tvDist_of_rows (P := (P^k)) (i := i) (x := π.val) (j := j) (hsum := hsum_eq)) - let T (Q : Matrix n n ℝ) (p : n → ℝ) : n → ℝ := fun j => ∑ t, p t * Q t j - have h_contract (Q : Matrix n n ℝ) (p qv : n → ℝ) - (hp1 : ∑ t, p t = 1) (hq1 : ∑ t, qv t = 1) : - Matrix.tvDist (T Q p) (T Q qv) ≤ Matrix.dobrushinCoeff Q * Matrix.tvDist p qv := by - simpa [T] using Matrix.tvDist_contract (P := Q) (p := p) (q := qv) (hp1 := hp1) (hq1 := hq1) - have row_as_T (m : ℕ) : - Matrix.rowDist (P^m) i = T (P^m) (delta i) := by - funext j; classical - simp [Matrix.rowDist, T, delta] - have T_fix (m : ℕ) : T (P^m) π.val = π.val := by - funext j - have := pow_stationary_mulVec (P := P) (k := m) h_stoch π hπ_stat - have := congrArg (fun v => v j) this - simpa [T, mulVec, transpose_apply, Finset.mul_sum, mul_comm, mul_left_comm, mul_assoc] using this - have h_tv_blocks : - Matrix.tvDist (Matrix.rowDist (P^(q*k0)) i) π.val ≤ r^q := by - clear h_entry_le_tv row_as_T - revert i - refine Nat.rec - (motive := fun q => ∀ i0, Matrix.tvDist (Matrix.rowDist (P^(q*k0)) i0) π.val ≤ r^q) - ?base ?step q - · -- base q = 0 - intro i0 - have hsumδ : ∑ t, delta i0 t = 1 := delta_sum_one i0 - have hsumπ : ∑ t, π.val t = 1 := π.property.2 - have : Matrix.tvDist (delta i0) π.val ≤ 1 := by - have hpt : - ∀ t, |delta i0 t - π.val t| ≤ |delta i0 t| + |π.val t| := by - intro t; simpa [sub_eq_add_neg] using abs_add_le (delta i0 t) (-(π.val t)) - have hsum_le : - ∑ t, |delta i0 t - π.val t| - ≤ ∑ t, (|delta i0 t| + |π.val t|) := by - refine sum_le_sum (by intro t _; simpa using hpt t) - have htwo : (∑ t, |delta i0 t|) + (∑ t, |π.val t|) = (2 : ℝ) := by - have h1 : ∑ t, |delta i0 t| = 1 := by - simp only [delta, abs_ite, abs_zero, abs_one] - rw [@Fintype.sum_ite_eq'] - have h2 : ∑ t, |π.val t| = 1 := by - have := π.property - have h0 : ∀ t, 0 ≤ π.val t := this.1 - have hsum : ∑ t, π.val t = 1 := this.2 - rw [← hsum] - congr 1 - ext t - exact abs_of_nonneg (h0 t) - simp [h1, h2] - norm_num - have : (∑ t, |delta i0 t - π.val t|) / 2 ≤ 1 := by - have h2pos : (0 : ℝ) < 2 := by norm_num - have hnum : - ∑ t, |delta i0 t - π.val t| ≤ (2 : ℝ) := by - calc ∑ t, |delta i0 t - π.val t| - ≤ ∑ t, (|delta i0 t| + |π.val t|) := hsum_le - _ = (∑ t, |delta i0 t|) + (∑ t, |π.val t|) := by rw [sum_add_distrib] - _ = 2 := by rw [htwo] - exact (div_le_iff h2pos).mpr (by simpa [one_mul] using hnum) - simpa [Matrix.tvDist] using this - have h_row_eq : Matrix.rowDist (P^(0*k0)) i0 = delta i0 := by - ext t - simp [Matrix.rowDist, pow_zero, delta, Matrix.one_apply, eq_comm] - rw [h_row_eq] - simpa [pow_zero] using this - · intro q ih i0 - have hPq : IsStochastic (P^(q*k0)) := by - simpa using (isStochastic_pow (P := P) (hP := h_stoch) (q*k0)) - have hp1 : ∑ t, (Matrix.rowDist (P^(q*k0)) i0) t = 1 := by - simpa [Matrix.rowDist] using hPq.2 i0 - have hq1 : ∑ t, π.val t = 1 := π.property.2 - have hstep : - Matrix.tvDist - (T (P^k0) (Matrix.rowDist (P^(q*k0)) i0)) - (T (P^k0) π.val) - ≤ r * Matrix.tvDist (Matrix.rowDist (P^(q*k0)) i0) π.val := - h_contract (P^k0) (Matrix.rowDist (P^(q*k0)) i0) π.val hp1 hq1 - have hleft : - T (P^k0) (Matrix.rowDist (P^(q*k0)) i0) = Matrix.rowDist (P^((q+1)*k0)) i0 := by - funext j - classical - have hmul : (q + 1) * k0 = q * k0 + k0 := by - simpa [Nat.succ_eq_add_one] using (Nat.succ_mul q k0) - simp [T, Matrix.rowDist, Matrix.mul_apply, pow_add, hmul] - have hright : T (P^k0) π.val = π.val := T_fix k0 - have hstep_final : - Matrix.tvDist (Matrix.rowDist (P^((q+1)*k0)) i0) π.val - ≤ r * Matrix.tvDist (Matrix.rowDist (P^(q*k0)) i0) π.val := by - rw [hleft, hright] at hstep - exact hstep - have ih_applied : Matrix.tvDist (Matrix.rowDist (P^(q*k0)) i0) π.val ≤ r^q := ih i0 - have hrnonneg : 0 ≤ r := hr0 - have hmul_le : - r * Matrix.tvDist (Matrix.rowDist (P^(q*k0)) i0) π.val ≤ r * r^q := - mul_le_mul_of_nonneg_left ih_applied hrnonneg - have hpow : r * r^q = r^(q+1) := by - simp [pow_succ, mul_comm] - have hfinal : - Matrix.tvDist (Matrix.rowDist (P^((q+1)*k0)) i0) π.val ≤ r^(q+1) := by - have := hstep_final.trans hmul_le - simpa [hpow] using this - exact hfinal - have hδPs_le_one (s : ℕ) : Matrix.dobrushinCoeff (P^s) ≤ 1 := by - classical - let f : (n × n) → ℝ := - fun p => Matrix.tvDist (Matrix.rowDist (P^s) p.1) (Matrix.rowDist (P^s) p.2) - have hforall : ∀ d ∈ Set.range f, d ≤ 1 := by - intro d hd - rcases hd with ⟨⟨i₁, i₂⟩, rfl⟩ - have hPs : IsStochastic (P^s) := by - simpa using (isStochastic_pow (P := P) (hP := h_stoch) s) - have hsum1_i₁ : ∑ t, Matrix.rowDist (P^s) i₁ t = 1 := by - simpa [Matrix.rowDist] using hPs.2 i₁ - have hsum1_i₂ : ∑ t, Matrix.rowDist (P^s) i₂ t = 1 := by - simpa [Matrix.rowDist] using hPs.2 i₂ - have hpt : - ∀ t, |Matrix.rowDist (P^s) i₁ t - Matrix.rowDist (P^s) i₂ t| - ≤ |Matrix.rowDist (P^s) i₁ t| + |Matrix.rowDist (P^s) i₂ t| := by - intro t; simpa [sub_eq_add_neg] using - abs_add_le (Matrix.rowDist (P^s) i₁ t) (-(Matrix.rowDist (P^s) i₂ t)) - have hsum_le : - ∑ t, |Matrix.rowDist (P^s) i₁ t - Matrix.rowDist (P^s) i₂ t| - ≤ ∑ t, (|Matrix.rowDist (P^s) i₁ t| + |Matrix.rowDist (P^s) i₂ t|) := by - refine sum_le_sum (by intro t _; simpa using hpt t) - have habs1 : ∑ t, |Matrix.rowDist (P^s) i₁ t| = 1 := by - have h0 : ∀ t, 0 ≤ Matrix.rowDist (P^s) i₁ t := by intro t; exact hPs.1 i₁ t - simpa [abs_of_nonneg, h0] using hsum1_i₁ - have habs2 : ∑ t, |Matrix.rowDist (P^s) i₂ t| = 1 := by - have h0 : ∀ t, 0 ≤ Matrix.rowDist (P^s) i₂ t := by intro t; exact hPs.1 i₂ t - simpa [abs_of_nonneg, h0] using hsum1_i₂ - have hnum : - ∑ t, |Matrix.rowDist (P^s) i₁ t - Matrix.rowDist (P^s) i₂ t| ≤ 2 := by - calc - _ ≤ ∑ t, (|Matrix.rowDist (P^s) i₁ t| + |Matrix.rowDist (P^s) i₂ t|) := hsum_le - _ = (∑ t, |Matrix.rowDist (P^s) i₁ t|) + (∑ t, |Matrix.rowDist (P^s) i₂ t|) := by - simp [sum_add_distrib] - _ = 1 + 1 := by simp [habs1, habs2] - _ = (2 : ℝ) := by norm_num - have h2 : (0 : ℝ) < 2 := by norm_num - have : (∑ t, |Matrix.rowDist (P^s) i₁ t - Matrix.rowDist (P^s) i₂ t|) / 2 ≤ 1 := - (div_le_iff h2).mpr (by simpa [one_mul] using hnum) - simpa [Matrix.tvDist] using this - have hnonempty : (Set.range f).Nonempty := by - classical - let i0 : n := Classical.arbitrary n - exact ⟨f ⟨i0, i0⟩, ⟨⟨i0, i0⟩, rfl⟩⟩ - have hset_eq : - { d | ∃ i i' : n, d = Matrix.tvDist (Matrix.rowDist (P^s) i) (Matrix.rowDist (P^s) i') } - = Set.range f := by - ext d; constructor - · intro h; rcases h with ⟨i, i', rfl⟩; exact ⟨⟨i, i'⟩, rfl⟩ - · intro h; rcases h with ⟨⟨i, i'⟩, rfl⟩; exact ⟨i, i', rfl⟩ - have : sSup (Set.range f) ≤ 1 := csSup_le hnonempty hforall - simpa [Matrix.dobrushinCoeff, hset_eq] using this - have hpq : - Matrix.tvDist (Matrix.rowDist (P^k) i) π.val ≤ r^q := by - have hPk_split : P^k = (P^(q*k0)) * (P^s) := by - have : k = q*k0 + s := hk_decomp - simp [this, pow_add, pow_mul] - have hPq : IsStochastic (P^(q*k0)) := by - simpa using (isStochastic_pow (P := P) (hP := h_stoch) (q*k0)) - have hp1 : ∑ t, Matrix.rowDist (P^(q*k0)) i t = 1 := by - simpa [Matrix.rowDist] using hPq.2 i - have hq1 : ∑ t, π.val t = 1 := π.property.2 - have hstep_s : - Matrix.tvDist - (T (P^s) (Matrix.rowDist (P^(q*k0)) i)) - (T (P^s) π.val) - ≤ Matrix.dobrushinCoeff (P^s) * Matrix.tvDist (Matrix.rowDist (P^(q*k0)) i) π.val := by - simpa [T] using h_contract (Q := (P^s)) (p := Matrix.rowDist (P^(q*k0)) i) (qv := π.val) hp1 hq1 - have hleft : T (P^s) (Matrix.rowDist (P^(q*k0)) i) = Matrix.rowDist (P^k) i := by - funext j; classical - simp [T, Matrix.rowDist, hPk_split, Matrix.mul_apply] - have hright : T (P^s) π.val = π.val := T_fix s - have hδs_le : Matrix.dobrushinCoeff (P^s) ≤ 1 := hδPs_le_one s - have : - Matrix.tvDist (Matrix.rowDist (P^k) i) π.val - ≤ Matrix.tvDist (Matrix.rowDist (P^(q*k0)) i) π.val := by - have hrhs_le : - Matrix.dobrushinCoeff (P^s) * Matrix.tvDist (Matrix.rowDist (P^(q*k0)) i) π.val - ≤ 1 * Matrix.tvDist (Matrix.rowDist (P^(q*k0)) i) π.val := by - have htv_nonneg : - 0 ≤ Matrix.tvDist (Matrix.rowDist (P^(q*k0)) i) π.val := by - have : 0 ≤ ∑ t, |Matrix.rowDist (P^(q*k0)) i t - π.val t| := - sum_nonneg (by intro _ _; exact abs_nonneg _) - have h2 : 0 ≤ (2 : ℝ) := by norm_num - simpa [Matrix.tvDist, div_eq_mul_inv, mul_comm] using - (mul_nonneg this (inv_nonneg.mpr h2)) - exact mul_le_mul_of_nonneg_right hδs_le htv_nonneg - have := hstep_s.trans hrhs_le - simpa [hleft, hright] using this - exact this.trans h_tv_blocks - have : |(P^k) i j - (LimitMatrix π) i j| ≤ r^q := (le_trans h_entry_le_tv hpq) - simpa [LimitMatrix, hq] using this - -open Tendsto -/-- - Spectral Gap implies convergence to the stationary limit matrix Π. - (Works with the block-exponent version of the spectral gap.) --/ -theorem converges_of_spectral_gap [Nonempty n] {P : Matrix n n ℝ} (_ : IsStochastic P) - (h_gap : HasSpectralGap P) (_ : Matrix.IsIrreducible P) : - ∃ (π : stdSimplex ℝ n), IsStationary P π ∧ - Tendsto (fun k : ℕ => P^k) atTop (𝓝 (LimitMatrix π)) := by - classical - rcases h_gap with ⟨π, r, k0, h_stat, hk0pos, hr0, hr1, h_bound⟩ - refine ⟨π, h_stat, ?_⟩ - set L := LimitMatrix π - -- For each entry, the absolute error is ≤ r^(k / k0), with 0 ≤ r < 1. - have h_pow_tendsto_zero : - Tendsto (fun k : ℕ => r^(k / k0)) atTop (𝓝 (0 : ℝ)) := by - have h_abs_lt_one : |r| < (1 : ℝ) := by - rw [abs_lt] - exact ⟨neg_lt_iff_pos_add.mpr (add_pos_of_nonneg_of_pos hr0 (by linarith [hr1])), hr1⟩ - have h_rpow : Tendsto (fun n : ℕ => r^n) atTop (𝓝 (0 : ℝ)) := - tendsto_pow_atTop_nhds_zero_of_abs_lt_one h_abs_lt_one - have h_div : Tendsto (fun k : ℕ => k / k0) atTop atTop := by - apply tendsto_atTop_atTop.2 - intro b - refine ⟨b * k0, ?_⟩ - intro n hn - exact (Nat.le_div_iff_mul_le hk0pos).mpr hn - simpa using h_rpow.comp h_div - refine tendsto_pi_nhds.mpr (fun i => ?_) - refine tendsto_pi_nhds.mpr (fun j => ?_) - have h_abs_bound : - ∀ k, |(P^k) i j - L i j| ≤ r^(k / k0) := by - intro k; simpa [L, LimitMatrix] using h_bound i j k - have h_abs_tend : - Tendsto (fun k : ℕ => |(P^k) i j - L i j|) atTop (𝓝 (0 : ℝ)) := - tendsto_of_tendsto_of_tendsto_of_le_of_le - tendsto_const_nhds h_pow_tendsto_zero - (fun _ => abs_nonneg _) - h_abs_bound - have h_diff_tend : - Tendsto (fun k : ℕ => (P^k) i j - L i j) atTop (𝓝 (0 : ℝ)) := - tendsto_of_tendsto_of_tendsto_of_le_of_le - (by simpa using h_abs_tend.neg) - h_abs_tend - (fun _ => neg_abs_le _) - (fun _ => le_abs_self _) - have h_add_const : - Tendsto (fun k : ℕ => L i j + ((P^k) i j - L i j)) atTop (𝓝 (L i j + 0)) := by - exact tendsto_const_nhds.add h_diff_tend - simpa using h_add_const - -/-- - **The Fundamental Convergence Theorem for Finite Markov Chains.** - If P satisfies the IsMCMC conditions, then Pᵏ converges to the limit matrix Π. --/ -theorem convergence_to_stationarity [Nonempty n] - (P : Matrix n n ℝ) (π : stdSimplex ℝ n) (hMCMC : IsMCMC P π) : - Tendsto (fun k => P^k) atTop (𝓝 (LimitMatrix π)) := by - have h_gap : HasSpectralGap P := by - exact IsPrimitive.has_spectral_gap hMCMC.stochastic hMCMC.primitive - obtain ⟨π', h_stat', h_conv⟩ := - converges_of_spectral_gap hMCMC.stochastic h_gap hMCMC.irreducible - obtain ⟨π_unique, h_unique⟩ := - exists_unique_stationary_distribution_of_irreducible hMCMC.stochastic hMCMC.irreducible - have h_π_eq_π' : (π : stdSimplex ℝ n) = π' := by - -- π coincides with the uniquely determined stationary distribution - have hπ : (π : stdSimplex ℝ n) = π_unique := h_unique.2 π hMCMC.stationary - -- π' also coincides with the same unique stationary distribution - have hπ' : π' = π_unique := h_unique.2 π' h_stat' - -- hence π = π' - simp [hπ, hπ'] - simpa [h_π_eq_π'] using h_conv - -/-! #### Convergence of Distributions -/ - -/-- - The distribution of the chain at time k, starting from μ₀. - μₖ = (Pᵏ)ᵀ *ᵥ μ₀. --/ -def distributionAtTime (P : Matrix n n ℝ) (μ₀ : stdSimplex ℝ n) (k : ℕ) : n → ℝ := - (P^k)ᵀ *ᵥ μ₀.val - -/-- - Theorem: The distribution at time k converges to the stationary distribution π, - regardless of the initial distribution μ₀. --/ -lemma distribution_converges_to_stationarity [Nonempty n] - (P : Matrix n n ℝ) (π : stdSimplex ℝ n) (hMCMC : IsMCMC P π) - (μ₀ : stdSimplex ℝ n) : - Tendsto (distributionAtTime P μ₀) atTop (𝓝 π.val) := by - let PiLim : Matrix n n ℝ := LimitMatrix π - have h_conv : Tendsto (fun k : ℕ => P ^ k) atTop (𝓝 PiLim) := - convergence_to_stationarity P π hMCMC - refine tendsto_pi_nhds.mpr ?coord - intro i - -- We show (distributionAtTime P μ₀ k) i → π i. - have h_entry_tendsto (j : n) : - Tendsto (fun k : ℕ => (P ^ k) j i) atTop (𝓝 (PiLim j i)) := by - have h_eval : Continuous fun M : Matrix n n ℝ => M j i := by - simpa using ((continuous_apply i).comp (continuous_apply j)) - exact (h_eval.tendsto _).comp h_conv - have h_term_tendsto (j : n) : - Tendsto (fun k : ℕ => (P ^ k) j i * μ₀.val j) atTop - (𝓝 (PiLim j i * μ₀.val j)) := - (h_entry_tendsto j).mul tendsto_const_nhds - have h_sum : - Tendsto (fun k : ℕ => ∑ j, (P ^ k) j i * μ₀.val j) - atTop (𝓝 (∑ j, PiLim j i * μ₀.val j)) := by - simpa using - tendsto_finset_sum - (s := (Finset.univ : Finset n)) - (fun j _ ↦ h_term_tendsto j) - have h_left : - (fun k : ℕ => ∑ j, (P ^ k) j i * μ₀.val j) - = fun k : ℕ => distributionAtTime P μ₀ k i := by - funext k - simp [distributionAtTime, Matrix.mulVec, Matrix.transpose_apply] - rfl - have h_right : - (∑ j, PiLim j i * μ₀.val j) = π.val i := by - have h_one : (∑ j, μ₀.val j) = 1 := μ₀.property.2 - have h_pull : - (∑ j, π.val i * μ₀.val j) = (π.val i) * (∑ j, μ₀.val j) := by - simpa using - (Finset.mul_sum (s := (Finset.univ : Finset n)) - (f := fun j => μ₀.val j) (a := π.val i)).symm - calc - (∑ j, PiLim j i * μ₀.val j) - = ∑ j, π.val i * μ₀.val j := by - simp [PiLim, LimitMatrix] - _ = (π.val i) * (∑ j, μ₀.val j) := h_pull - _ = π.val i := by simp [h_one] - simpa [h_left, h_right] using h_sum - -/-! #### (B) Convergence in total variation -/ - -lemma tvDist_row_converges_to_stationary [Nonempty n] - (P : Matrix n n ℝ) (π : stdSimplex ℝ n) (hMCMC : IsMCMC P π) (i : n) : - Tendsto (fun k : ℕ => Matrix.tvDist (Matrix.rowDist (P^k) i) π.val) atTop (𝓝 0) := by - classical - have h_conv : Tendsto (fun k : ℕ => P ^ k) atTop (𝓝 (LimitMatrix π)) := - convergence_to_stationarity P π hMCMC - -- pointwise convergence of entries in row `i` - have h_abs (j : n) : - Tendsto (fun k : ℕ => |(P^k) i j - π.val j|) atTop (𝓝 (0 : ℝ)) := by - have h_entry : - Tendsto (fun k : ℕ => (P ^ k) i j) atTop (𝓝 (LimitMatrix π i j)) := by - have h_eval : Continuous fun M : Matrix n n ℝ => M i j := by - simpa using ((continuous_apply j).comp (continuous_apply i)) - exact (h_eval.tendsto _).comp h_conv - have h_diff : - Tendsto (fun k : ℕ => (P ^ k) i j - π.val j) atTop (𝓝 (0 : ℝ)) := by - -- `LimitMatrix π i j = π.val j` - have : Tendsto (fun k : ℕ => (P ^ k) i j - π.val j) atTop - (𝓝 (LimitMatrix π i j - π.val j)) := h_entry.sub tendsto_const_nhds - simpa [LimitMatrix] using this - -- continuity of `abs` - simpa using (continuous_abs.tendsto (0 : ℝ)).comp h_diff - have h_sum : - Tendsto (fun k : ℕ => ∑ j, |(P^k) i j - π.val j|) atTop (𝓝 (0 : ℝ)) := by - simpa using - (tendsto_finset_sum (s := (Finset.univ : Finset n)) - (fun j _ => h_abs j)) - -- divide by 2 to get TV - have : Tendsto (fun k : ℕ => (∑ j, |(P^k) i j - π.val j|) / 2) atTop (𝓝 (0 : ℝ)) := by - simpa using h_sum.div_const (2 : ℝ) - simpa [Matrix.tvDist, Matrix.rowDist] using this - -lemma tvDist_distributionAtTime_converges_to_stationary [Nonempty n] - (P : Matrix n n ℝ) (π : stdSimplex ℝ n) (hMCMC : IsMCMC P π) - (μ₀ : stdSimplex ℝ n) : - Tendsto (fun k : ℕ => Matrix.tvDist (distributionAtTime P μ₀ k) π.val) atTop (𝓝 0) := by - classical - have hμ : Tendsto (distributionAtTime P μ₀) atTop (𝓝 π.val) := - distribution_converges_to_stationarity P π hMCMC μ₀ - have h_abs (i : n) : - Tendsto (fun k : ℕ => |distributionAtTime P μ₀ k i - π.val i|) atTop (𝓝 (0 : ℝ)) := by - have h_diff : - Tendsto (fun k : ℕ => distributionAtTime P μ₀ k i - π.val i) atTop (𝓝 (0 : ℝ)) := by - have : Tendsto (fun k : ℕ => distributionAtTime P μ₀ k i - π.val i) atTop - (𝓝 (π.val i - π.val i)) := - (tendsto_pi_nhds.mp hμ i).sub tendsto_const_nhds - simpa using this - -- continuity of `abs` - simpa using (continuous_abs.tendsto (0 : ℝ)).comp h_diff - have h_sum : - Tendsto (fun k : ℕ => ∑ i, |distributionAtTime P μ₀ k i - π.val i|) atTop (𝓝 (0 : ℝ)) := by - simpa using - (tendsto_finset_sum (s := (Finset.univ : Finset n)) - (fun i _ => h_abs i)) - have : Tendsto (fun k : ℕ => (∑ i, |distributionAtTime P μ₀ k i - π.val i|) / 2) atTop (𝓝 (0 : ℝ)) := by - simpa using h_sum.div_const (2 : ℝ) - simpa [Matrix.tvDist] using this - -/-! #### The Ergodic Theorem (Law of Large Numbers) -/ - -/-- Expectation of a function f under a distribution π. E_π[f]. -/ -def Expectation (π : stdSimplex ℝ n) (f : n → ℝ) : ℝ := - ∑ i, π.val i * f i - -/- - **The Ergodic Theorem (Law of Large Numbers) for Finite Markov Chains**. - - The time average of the expected value of `f` converges to the expectation under `π`, - regardless of the initial distribution `μ₀`. --/ -theorem ergodic_theorem_lln [Nonempty n] - (P : Matrix n n ℝ) (π : stdSimplex ℝ n) (hMCMC : IsMCMC P π) - (f : n → ℝ) (μ₀ : stdSimplex ℝ n) : - let a_k := fun k : ℕ => ∑ i, (distributionAtTime P μ₀ k i) * f i - let expected_time_average := fun N : ℕ => (1 / (N : ℝ)) * (∑ k ∈ Finset.range N, a_k k) - Tendsto expected_time_average atTop (𝓝 (Expectation π f)) := by - let μₖ := distributionAtTime P μ₀ - let a_k := fun k : ℕ => ∑ i, μₖ k i * f i - let L := Expectation π f - have h_ak_conv : Tendsto a_k atTop (𝓝 L) := by - have h_μk_conv := distribution_converges_to_stationarity P π hMCMC μ₀ - have : Tendsto (fun k : ℕ => fun i : n => μₖ k i) atTop (𝓝 fun i : n => π.val i) := - h_μk_conv - have h_term (i : n) : - Tendsto (fun k : ℕ => μₖ k i * f i) atTop (𝓝 (π.val i * f i)) := by - exact (tendsto_pi_nhds.mp this i).mul tendsto_const_nhds - simpa [a_k, L, Expectation] using - tendsto_finset_sum - (s := (Finset.univ : Finset n)) - (fun i _ ↦ h_term i) - have h_cesaro : - Tendsto (fun N : ℕ => (N : ℝ)⁻¹ * (∑ k ∈ Finset.range N, a_k k)) atTop (𝓝 L) := by - simpa using Filter.Tendsto.cesaro h_ak_conv - let expected_time_average := - fun N : ℕ => (1 / (N : ℝ)) * ∑ k ∈ Finset.range N, a_k k - change Tendsto expected_time_average atTop (𝓝 L) - have h_equiv : - expected_time_average = - fun N : ℕ => (∑ k ∈ Finset.range N, a_k k) / (N : ℝ) := by - funext N - simp [expected_time_average, div_eq_inv_mul] - simp_all only [CollatzWielandt.Finset.sum_def, one_div, a_k, μₖ, L, expected_time_average] - -end MCMC.Finite diff --git a/lean/MCMC/Finite/Core.lean b/lean/MCMC/Finite/Core.lean deleted file mode 100644 index d420eab..0000000 --- a/lean/MCMC/Finite/Core.lean +++ /dev/null @@ -1,146 +0,0 @@ -import MCMC.PF.LinearAlgebra.Matrix.PerronFrobenius.Aperiodic -import MCMC.PF.LinearAlgebra.Matrix.PerronFrobenius.Stochastic -namespace MCMC.Finite - -open Matrix Finset -open BigOperators -variable {n : Type*} [Fintype n] - -/-- - A matrix P is (row) stochastic if it is non-negative and its rows sum to 1. - This is the standard definition for a Markov chain transition matrix. --/ -def IsStochastic (P : Matrix n n ℝ) : Prop := - (∀ i j, 0 ≤ P i j) ∧ (∀ i, ∑ j, P i j = 1) - -lemma isStochastic_one [DecidableEq n] : IsStochastic (1 : Matrix n n ℝ) := by - constructor - · intro i j - by_cases h : i = j - · simp [Matrix.one_apply, h] - · simp [h] - · intro i - simp [Matrix.one_apply, Finset.mem_univ] - -lemma isStochastic_mul {P Q : Matrix n n ℝ} - (hP : IsStochastic P) (hQ : IsStochastic Q) : - IsStochastic (P * Q) := by - constructor - · intro i j - have hterm : ∀ k, 0 ≤ P i k * Q k j := by - intro k - exact mul_nonneg (hP.1 i k) (hQ.1 k j) - have : 0 ≤ ∑ k, P i k * Q k j := - sum_nonneg (by intro k _; simpa using hterm k) - simpa [Matrix.mul_apply] using this - · intro i - calc - ∑ j, (P * Q) i j - = ∑ j, ∑ k, P i k * Q k j := by simp [Matrix.mul_apply] - _ = ∑ k, ∑ j, P i k * Q k j := by - simpa using - (Finset.sum_comm (s := (Finset.univ : Finset n)) - (t := (Finset.univ : Finset n)) - (f := fun j k => P i k * Q k j)) - _ = ∑ k, P i k * ∑ j, Q k j := by - simp [mul_sum] - _ = ∑ k, P i k * 1 := by - apply Finset.sum_congr rfl - intro k hk - simp [hQ.2 k] - _ = ∑ k, P i k := by simp - _ = 1 := hP.2 i - -lemma isStochastic_pow [DecidableEq n] {P : Matrix n n ℝ} (hP : IsStochastic P) : - ∀ k, IsStochastic (P^k) - | 0 => by simpa [pow_zero] using isStochastic_one - | k+1 => - by - have hk : IsStochastic (P^k) := isStochastic_pow hP k - simpa [pow_succ] using isStochastic_mul hk hP - -/-- - A probability distribution π (represented as a column vector in the standard simplex) - is stationary for P if Pᵀ *ᵥ π = π. - (This corresponds to the row vector definition π_row P = π_row). --/ -def IsStationary (P : Matrix n n ℝ) (π : stdSimplex ℝ n) : Prop := - Pᵀ *ᵥ π.val = π.val - -/-- The main object for a verified MCMC algorithm on finite spaces. -/ -class IsMCMC [DecidableEq n] (P : Matrix n n ℝ) (π : stdSimplex ℝ n) where - stochastic : IsStochastic P - stationary : IsStationary P π - irreducible : Matrix.IsIrreducible P - primitive : IsPrimitive P - -variable [Nonempty n] - -/-- - Leveraging the PF theorem to show existence and uniqueness of a stationary distribution - for irreducible stochastic matrices. --/ -theorem exists_unique_stationary_distribution_of_irreducible - [DecidableEq n] - {P : Matrix n n ℝ} (h_stoch : IsStochastic P) (h_irred : Matrix.IsIrreducible P) : - ∃! (π : stdSimplex ℝ n), IsStationary P π := by - -- 1. Pᵀ is Irreducible. (Irreducibility is preserved under transposition for non-negative matrices). - have hPT_irred : Matrix.IsIrreducible Pᵀ := isIrreducible_transpose_iff.mpr h_irred - -- 2. Pᵀ is Column-Stochastic. (Since P is row-stochastic). - have hPT_col_stoch : ∀ j, ∑ i, Pᵀ i j = 1 := by - intro j - simp [transpose_apply] - exact h_stoch.2 j - -- 3. We apply the PF theorem to Pᵀ. - have h_exists := Matrix.exists_positive_eigenvector_of_irreducible_stochastic hPT_irred hPT_col_stoch - -- 4. The result (Pᵀ *ᵥ v = v) is exactly the definition of IsStationary P π. - simp [IsStationary] - exact h_exists - -/-- The unique stationary distribution of an irreducible stochastic matrix. -/ -noncomputable def stationaryDistribution [DecidableEq n] (P : Matrix n n ℝ) (h_irred : Matrix.IsIrreducible P) - (h_stoch : IsStochastic P) : stdSimplex ℝ n := - (Classical.choose (exists_unique_stationary_distribution_of_irreducible h_stoch h_irred).exists) - -lemma stationaryDistribution_is_stationary [DecidableEq n] (P : Matrix n n ℝ) (h_irred : Matrix.IsIrreducible P) - (h_stoch : IsStochastic P) : - IsStationary P (stationaryDistribution P h_irred h_stoch) := - (Classical.choose_spec (exists_unique_stationary_distribution_of_irreducible h_stoch h_irred).exists) - -/-- - If a transition matrix is stochastic, - irreducible, and primitive, it defines a valid MCMC setup targeting its unique - stationary distribution (which is guaranteed to exist by the PF theorem). --/ -theorem isMCMC_of_properties - (P : Matrix n n ℝ) [DecidableEq n] - (h_stoch : IsStochastic P) - (h_irred : Matrix.IsIrreducible P) - (h_prim : IsPrimitive P) : - IsMCMC P (stationaryDistribution P h_irred h_stoch) := -{ - stochastic := h_stoch, - stationary := stationaryDistribution_is_stationary P h_irred h_stoch, - irreducible := h_irred, - primitive := h_prim -} - -omit [Nonempty n] in -/-- Convenience: aperiodicity follows from primitivity -/ -theorem aperiodic_of_properties - [DecidableEq n] [Nonempty n] - (P : Matrix n n ℝ) - (h_stoch : IsStochastic P) - (h_prim : IsPrimitive P) : - Matrix.IsAperiodic P := - Matrix.primitive_implies_irreducible_and_aperiodic (A := P) h_stoch.1 h_prim - -omit [Nonempty n] in -/-- Convenience: expose aperiodicity from an IsMCMC instance. -/ -lemma IsMCMC.aperiodic - [DecidableEq n] [Nonempty n] - {P : Matrix n n ℝ} {π : stdSimplex ℝ n} - (h : IsMCMC P π) : Matrix.IsAperiodic P := - Matrix.primitive_implies_irreducible_and_aperiodic (A := P) h.stochastic.1 h.primitive - -end MCMC.Finite diff --git a/lean/MCMC/Finite/Gibbs.lean b/lean/MCMC/Finite/Gibbs.lean deleted file mode 100644 index 675b587..0000000 --- a/lean/MCMC/Finite/Gibbs.lean +++ /dev/null @@ -1,301 +0,0 @@ -import Mathlib.Data.Matrix.Basic --- -import Mathlib.LinearAlgebra.Matrix.Hermitian -import Mathlib.Data.Real.Basic -import Mathlib.Algebra.Order.Field.Basic -import Mathlib.Tactic.Positivity -import Mathlib.Data.Fintype.Card -import Mathlib.Tactic.FieldSimp -import Mathlib.Tactic.Linarith -import MCMC.Finite.MetropolisHastings - -set_option linter.unusedSectionVars false -set_option linter.unusedVariables false - - -namespace MCMC.Finite - -open Matrix Finset --Real - -variable {n : Type*} [Fintype n] [DecidableEq n] - -/-! -### Formalization of the Gibbs Sampler -We formalize Gibbs for a bivariate finite state space (α × β), ensuring rigor when marginals are zero. --/ - -variable {α β : Type*} [Fintype α] [DecidableEq α] [Fintype β] [DecidableEq β] - -/-- The marginal distribution on β. π_β(b) = ∑_a π(a, b). -/ -def marginal_β (π : stdSimplex ℝ (α × β)) (b : β) : ℝ := - ∑ a : α, π.val (a, b) - -lemma marginal_β_nonneg (π : stdSimplex ℝ (α × β)) (b : β) : 0 ≤ marginal_β π b := - sum_nonneg (fun a _ => π.property.1 (a, b)) - -open Classical in -/-- - The conditional distribution π(a | b). Rigorously defined. - If the marginal probability of b is 0 (an impossible event under π), we choose an arbitrary - distribution (here, uniform over α) to ensure the resulting kernel is stochastic. --/ -noncomputable def gibbs_conditional_α (π : stdSimplex ℝ (α × β)) (b : β) (a : α) : ℝ := - let marg_b := marginal_β π b - if h : marg_b = 0 then - -- Default to uniform distribution if α is non-empty. - if Nonempty α then (1 : ℝ) / Fintype.card α else 0 - else - -- Standard definition: π(a, b) / π_β(b). - π.val (a, b) / marg_b - -/-- The Gibbs kernel K_α that updates the first component α while leaving β fixed. -/ -noncomputable def gibbs_kernel_α (π : stdSimplex ℝ (α × β)) : Matrix (α × β) (α × β) ℝ := - fun x y => - if x.2 = y.2 then gibbs_conditional_α π x.2 y.1 else 0 - -/- TODO: MK? - Theorem: The Gibbs kernel K_α is stochastic. - -theorem gibbs_kernel_α_is_stochastic (π : stdSimplex ℝ (α × β)) : - IsStochastic (gibbs_kernel_α π) := by - constructor - · -- 1. Non-negativity. - sorry - · -- 2. Rows sum to 1. - · -- Case: marg_b > 0. - sorry --/ -/-! -### The Gibbs as a special case of Metropolis-Hastings --/ - -/- TODO: MK? - Theorem: The acceptance probability of a Gibbs step, viewed as an MH step - where the proposal is the Gibbs kernel itself, is always 1. - -theorem gibbs_as_mh_acceptance_is_one (π : stdSimplex ℝ (α × β)) : - ∀ x y, mh_acceptance_prob π (gibbs_kernel_α π) x y = 1 := by - intro x y - let Q := gibbs_kernel_α π - simp only [mh_acceptance_prob] - -- Num = π(y) * Q(y, x), Den = π(x) * Q(x, y). - -- Case 1: x.2 ≠ y.2. - by_cases h_eq : x.2 = y.2 - swap - · -- Q(x, y) = 0. Den = 0. Acceptance is 1. - -- Case 2a: marg_b = 0. - sorry - sorry --/ -/- TODO: MK? - Corollary: The Gibbs kernel is identical to the MH kernel when the proposal - is the Gibbs kernel itself. - -theorem gibbs_kernel_eq_metropolisHastings (π : stdSimplex ℝ (α × β)) : - gibbs_kernel_α π = metropolisHastingsKernel π (gibbs_kernel_α π) := by - ext x y - let Q := gibbs_kernel_α π - let A := mh_acceptance_prob π Q - have hA_one : A x y = 1 := gibbs_as_mh_acceptance_is_one π x y - simp only [metropolisHastingsKernel] - -- Show the rejection mass R(x) is 0. R(x) = 1 - ∑_z Q(x, z). - have hR_zero : (1 - ∑ z, Q x z * A x z) = 0 := by - sorry - -- P(x, y) = Q(x, y) + δ(x, y) * 0 = Q(x, y). - sorry --/ - -/-! -### MH Usability -We provide sufficient conditions for the MH kernel to be Irreducible and Primitive. --/ - -variable {n : Type*} [Fintype n] [DecidableEq n] -variable {π : stdSimplex ℝ n} {Q : Matrix n n ℝ} - -/-- The target distribution π is strictly positive if π(i) > 0 for all i. -/ -def StrictlyPositive (π : stdSimplex ℝ n) : Prop := ∀ i, π.val i > 0 - -/-- A matrix is strictly positive if all its entries are strictly greater than 0. -/ -def IsStrictlyPositive (M : Matrix n n ℝ) : Prop := ∀ i j, M i j > 0 - -/-! ### Scenario 1: Strict Positivity -/ - -/- TODO: MK? - Theorem: If the proposal Q and the target distribution π are strictly positive, - then the resulting Metropolis-Hastings kernel P is also strictly positive. - -theorem metropolisHastings_is_strictly_positive - (hQ_stoch : IsStochastic Q) - (hQ_pos : IsStrictlyPositive Q) - (hπ_pos : StrictlyPositive π) : - IsStrictlyPositive (metropolisHastingsKernel π Q) := by - intro x y - let A := mh_acceptance_prob π Q - let P := metropolisHastingsKernel π Q - -- 1. Acceptance probability A(x, y) > 0. - have hA_pos : A x y > 0 := by - --simp only [mh_acceptance_prob] - -- Denominator > 0 and Numerator > 0. - have h_den_pos : π.val x * Q x y > 0 := mul_pos (hπ_pos x) (hQ_pos x y) - have h_num_pos : π.val y * Q y x > 0 := mul_pos (hπ_pos y) (hQ_pos y x) - simp - sorry - -- 2. P_proposal(x, y) = Q(x, y) * A(x, y) > 0. - have hP_prop_pos : Q x y * A x y > 0 := mul_pos (hQ_pos x y) hA_pos - -- 3. RejectionTerm ≥ 0 (Rejection Mass R(x) ≥ 0). - sorry - -- P(x, y) = Positive + Non-negative > 0. --/ - -/- TODO: MK? --- Connection to underlying theory: -theorem IsStrictlyPositive.is_primitive {P : Matrix n n ℝ} (hP_pos : IsStrictlyPositive P) : - IsPrimitive P := sorry -- k=1 works. - -theorem IsStrictlyPositive.is_irreducible [Nonempty n] {P : Matrix n n ℝ} (hP_pos : IsStrictlyPositive P) : - Matrix.Irreducible P := sorry -- Graph is complete. --/ - -/-! ### General Irreducibility and Primitivity -/ - -/-- - The proposal kernel Q has symmetric connectivity if Q(i, j) > 0 implies Q(j, i) > 0. --/ -def SymmetricConnectivity (Q : Matrix n n ℝ) : Prop := - ∀ i j, Q i j > 0 → Q j i > 0 - -/- TODO: MK? - Theorem: Irreducibility of MH Kernel. - If π > 0, and Q is irreducible with symmetric connectivity, then P_MH is irreducible. -theorem metropolisHastings_irreducible - (hQ_stoch : IsStochastic Q) - (hQ_irred : Matrix.Irreducible Q) - (hπ_pos : StrictlyPositive π) - (hQ_symm : SymmetricConnectivity Q) : - Matrix.Irreducible (metropolisHastingsKernel π Q) := by - let P := metropolisHastingsKernel π Q - let A := mh_acceptance_prob π Q - -- Strategy: Show that the connectivity graph of P contains the graph of Q. - have h_P_pos_of_Q_pos : ∀ x y, Q x y > 0 → P x y > 0 := by - intro x y hQxy_pos - -- Show A(x, y) > 0. - have hA_pos : A x y > 0 := by - sorry - -- P_proposal > 0. - have hP_prop_pos : Q x y * A x y > 0 := mul_pos hQxy_pos hA_pos - -- P(x, y) ≥ proposal mass since the rejection term is ≥ 0. - simp only [P, metropolisHastingsKernel] - refine lt_of_lt_of_le hP_prop_pos ?_ - have h_nonneg : 0 ≤ (if x = y then 1 - ∑ z, Q x z * A x z else 0) := by - classical - by_cases hxy : x = y - · subst hxy - -- Show ∑ Q*A ≤ 1 using A ≤ 1 and stochasticity of Q. - sorry - · simp [hxy] - exact le_add_of_nonneg_right h_nonneg - -- Connection to underlying theory (W4): - -- Since P dominates Q in connectivity, and Q is irreducible, P is irreducible. - sorry --/ - -/-- - The proposal Q is "imperfect" if it does not already satisfy detailed balance w.r.t. π. - This ensures that rejections occur, which introduces self-loops and aids primitivity. --/ -def ProposalIsImperfect (π : stdSimplex ℝ n) (Q : Matrix n n ℝ) : Prop := - ∃ i j, π.val i * Q i j ≠ π.val j * Q j i - -/- TODO: MK? - Theorem: Primitivity (Aperiodicity) of MH Kernel. - If P_MH is irreducible, π > 0, and Q is imperfect, then P_MH is primitive. - -theorem metropolisHastings_primitive - (hQ_stoch : IsStochastic Q) - -- (hP_irred : Irreducible (metropolisHastingsKernel π Q)) -- Needed for final connection - (hπ_pos : StrictlyPositive π) - (h_imperfect : ProposalIsImperfect π Q) : - IsPrimitive (metropolisHastingsKernel π Q) := by - let A := mh_acceptance_prob π Q - let P := metropolisHastingsKernel π Q - -- Strategy: Show that P has a self-loop (P(x, x) > 0) by showing a rejection occurs. - -- 1. Find a state x and a move y such that rejection is possible (A(x, y) < 1 and Q(x, y) > 0). - have h_exists_rejection : ∃ x y, Q x y > 0 ∧ A x y < 1 := by - obtain ⟨i, j, h_imbalance⟩ := h_imperfect - -- Case 1: πᵢQᵢⱼ > πⱼQⱼᵢ. - by_cases h_gt : π.val i * Q i j > π.val j * Q j i - · use i, j - -- Q(i, j) > 0 because πᵢQᵢⱼ > 0 and πᵢ > 0. - have hQij_pos : Q i j > 0 := by sorry - -- A(i, j) < 1 because the ratio (Num/Den) < 1. - have hA_lt_one : A i j < 1 := by - simp [A, mh_acceptance_prob, ne_of_gt (mul_pos (hπ_pos i) hQij_pos)] - sorry - exact ⟨hQij_pos, hA_lt_one⟩ - · -- Case 2: πᵢQᵢⱼ < πⱼQⱼᵢ (since ≠ by h_imbalance). - have h_lt : π.val i * Q i j < π.val j * Q j i := lt_of_le_of_ne (by sorry) h_imbalance - -- Symmetric argument using j, i. - use j, i - have hQji_pos : Q j i > 0 := by sorry - have hA_lt_one : A j i < 1 := by - simp [A, mh_acceptance_prob, ne_of_gt (mul_pos (hπ_pos j) hQji_pos)] - sorry - exact ⟨hQji_pos, hA_lt_one⟩ - -- 2. Show this implies RejectionMass(x) > 0. - obtain ⟨x, y, hQ_pos, hA_lt_one⟩ := h_exists_rejection - have hR_pos : 1 - ∑ z, Q x z * A x z > 0 := by - apply sub_pos_of_lt - -- We need ∑ Q*A < 1. We know ∑ Q = 1. - -- Use the strict inequality sum theorem. - let f := fun z => Q x z * A x z - let g := fun z => Q x z - -- f(z) ≤ g(z) since A ≤ 1. - have h_le : ∀ z, f z ≤ g z := by - intro z - apply mul_le_of_le_one_right (hQ_stoch.1 x z) - exact (mh_acceptance_prob_bounds hQ_stoch.1 x z).2 - -- Strict inequality at y: f(y) < g(y). - have h_lt : f y < g y := mul_lt_of_lt_one_right hQ_pos hA_lt_one - sorry - -- 3. P(x, x) ≥ R(x) > 0. - have hPxx_pos : P x x > 0 := by - simp [P, metropolisHastingsKernel] - apply lt_of_lt_of_le hR_pos - sorry - -- Connection to underlying theory (W4): - -- We have shown P has a self-loop. Irreducible + self-loop => Primitive. - sorry --/ - -/-! -### W4: Spectral Properties of Reversible Kernels -Connecting reversible Markov chains to the spectral theory of symmetric matrices. -This is essential for proving convergence. --/ - -/-- - For a reversible transition kernel `P` with a positive stationary distribution `π`, - we can define a symmetrized kernel `S`. The spectral properties of `S` (which is - symmetric) determine the convergence rate of the chain. - - Let `D_sqrt_π` be the diagonal matrix with `sqrt(π_i)` on the diagonal. - The symmetrized kernel is `S = D_sqrt_π * P * D_sqrt_π⁻¹`. --/ -noncomputable def symmetrizedKernel (π : stdSimplex ℝ n) (P : Matrix n n ℝ) : Matrix n n ℝ := - let D_sqrt_π := diagonal (fun i => Real.sqrt (π.val i)) - let D_sqrt_π_inv := diagonal (fun i => 1 / Real.sqrt (π.val i)) - D_sqrt_π * P * D_sqrt_π_inv - -/- TODO: MK? - Theorem: The symmetrized kernel of a reversible matrix is symmetric (self-adjoint). - This allows leveraging the spectral theorem for symmetric matrices. - -theorem symmetrizedKernel_is_hermitian_of_reversible - (hπ_pos : StrictlyPositive π) - (hP_rev : IsReversible P π) : - IsHermitian (symmetrizedKernel π P) := by - -- For real matrices, IsHermitian is equivalent to being symmetric (Aᵀ = A). - sorry --/ -end MCMC.Finite diff --git a/lean/MCMC/Finite/MetropolisHastings.lean b/lean/MCMC/Finite/MetropolisHastings.lean deleted file mode 100644 index 3555610..0000000 --- a/lean/MCMC/Finite/MetropolisHastings.lean +++ /dev/null @@ -1,236 +0,0 @@ -import MCMC.Finite.Core - - -namespace MCMC.Finite - -open Matrix Finset-- Real - -variable {n : Type*} [Fintype n] --[DecidableEq n] - --- here we need to make the defs consistent with the one in Kernel by creating a Matrix.toKernel API - -def IsReversible (P : Matrix n n ℝ) (π : stdSimplex ℝ n) : Prop := - ∀ i j, π.val i * P i j = π.val j * P j i - -theorem IsReversible.is_stationary {P : Matrix n n ℝ} {π : stdSimplex ℝ n} - (hP_stoch : IsStochastic P) (h_rev : IsReversible P π) : - IsStationary P π := by - ext i - dsimp [IsStationary, transpose_apply] - calc - ∑ j, P j i * π.val j - = ∑ j, π.val i * P i j := by - congr; ext j - rw [mul_comm, h_rev i j, mul_comm] - _ = π.val i * ∑ j, P i j := by rw [Finset.mul_sum] - _ = π.val i := by rw [hP_stoch.2 i, mul_one] - -class IsMCMC' [DecidableEq n] (P : Matrix n n ℝ) (π : stdSimplex ℝ n) where - stochastic : IsStochastic P - stationary : IsStationary P π - irreducible : Matrix.IsIrreducible P - primitive : IsPrimitive P - -/-! -### Algorithm Definitions - Metropolis-Hastings --/ - -/-- - The acceptance probability for a Metropolis-Hastings step. - A(x, y) = min(1, (π(y) * Q(y, x)) / (π(x) * Q(x, y))). - - If the denominator is 0, the move is impossible to propose from x (if π(x)>0), - or x is unreachable under π. We define the acceptance probability as 1 in this case. --/ -noncomputable def mh_acceptance_prob (π : stdSimplex ℝ n) (Q : Matrix n n ℝ) (x y : n) : ℝ := - let num := π.val y * Q y x - let den := π.val x * Q x y - if den = 0 then - 1 - else - min 1 (num / den) - -/-- - The Metropolis-Hastings transition kernel (matrix). - It is constructed by calculating the probability of proposal and acceptance (P_proposal), - and then adding the remaining "rejection mass" (R) back to the diagonal to ensure stochasticity. - P(x, y) = P_proposal(x, y) + δ(x, y) * R(x). --/ -noncomputable def metropolisHastingsKernel [DecidableEq n] (π : stdSimplex ℝ n) (Q : Matrix n n ℝ) : Matrix n n ℝ := - let A := mh_acceptance_prob π Q - -- P_proposal(x, y) = Q(x, y) * A(x, y). - let P_proposal := fun x y => Q x y * A x y - -- Rejection mass R(x) = 1 - ∑_y P_proposal(x, y). - let rejection_mass := fun x => 1 - ∑ y, P_proposal x y - -- The final kernel. - fun x y => P_proposal x y + (if x = y then rejection_mass x else 0) - -variable {π : stdSimplex ℝ n} {Q : Matrix n n ℝ} - -/-- Helper lemma: The acceptance probability is bounded between 0 and 1. -/ -lemma mh_acceptance_prob_bounds [DecidableEq n] - (hQ_nonneg : ∀ i j, 0 ≤ Q i j) (x y : n) : - 0 ≤ mh_acceptance_prob π Q x y ∧ mh_acceptance_prob π Q x y ≤ 1 := by - simp only [mh_acceptance_prob] - split_ifs - · simp - · - -- Set the ratio r to type expressions and avoid type inference issues. - set r : ℝ := (π.val y * Q y x) / (π.val x * Q x y) with hr - have h_nonneg_ratio : 0 ≤ r := by - -- The ratio is non-negative since π (from stdSimplex) and Q are non-negative. - have hr_nonneg : 0 ≤ (π.val y * Q y x) / (π.val x * Q x y) := by - apply div_nonneg - · exact mul_nonneg (π.property.1 y) (hQ_nonneg y x) - · exact mul_nonneg (π.property.1 x) (hQ_nonneg x y) - simpa [hr] using hr_nonneg - -- Upper bound: min 1 r ≤ 1. - have h_le_one : min (1 : ℝ) r ≤ (1 : ℝ) := min_le_left _ _ - exact ⟨le_min zero_le_one h_nonneg_ratio, h_le_one⟩ - -/-- The MH kernel is stochastic. -/ -theorem metropolisHastings_is_stochastic [DecidableEq n] - (hQ_stoch : IsStochastic Q) : - IsStochastic (metropolisHastingsKernel π Q) := by - classical - let A := mh_acceptance_prob π Q - have hA_bounds : - ∀ i j, 0 ≤ mh_acceptance_prob π Q i j ∧ mh_acceptance_prob π Q i j ≤ 1 := - fun i j => mh_acceptance_prob_bounds (π:=π) (Q:=Q) (x:=i) (y:=j) hQ_stoch.1 - constructor - · -- 1. Non-negativity. - intro x y - -- P_proposal(x, y) ≥ 0. - have hP_prop_nonneg : 0 ≤ Q x y * A x y := - mul_nonneg (hQ_stoch.1 x y) (hA_bounds x y).1 - -- Rejection mass R(x) ≥ 0. We must show ∑ P_proposal ≤ 1. - have h_rej_nonneg : 0 ≤ 1 - ∑ j, Q x j * A x j := by - apply sub_nonneg_of_le - -- ∑ Q*A ≤ ∑ Q = 1 (since A ≤ 1 and Q ≥ 0). - calc - ∑ j, Q x j * A x j - ≤ ∑ j, Q x j := by - apply sum_le_sum; intro j _hj - exact mul_le_of_le_one_right (hQ_stoch.1 x j) (hA_bounds x j).2 - _ = 1 := hQ_stoch.2 x - -- P(x, y) is the sum of two non-negative terms. - simp only [metropolisHastingsKernel] - apply add_nonneg hP_prop_nonneg - split_ifs with hxy - · exact h_rej_nonneg - · simp - · -- 2. Rows sum to 1 (by construction). - intro x - classical - simp only [metropolisHastingsKernel, sum_add_distrib] - -- The second term simplifies to R(x). - have h_second_term : - (∑ y, (if x = y then (1 - ∑ z, Q x z * A x z) else 0)) - = (1 - ∑ z, Q x z * A x z) := by - simp [sum_ite_eq] - rw [h_second_term] - -- (∑ P_proposal) + (1 - ∑ P_proposal) = 1. - ring - -/-- - Helper lemma: For a > 0 and b ≥ 0: a * min(1, b/a) = min(a, b). - This identity is central to the proof of detailed balance. --/ -lemma mul_min_one_div_eq_min {a b : ℝ} (ha_pos : 0 < a) (_hn_in : 0 ≤ b) : - a * min 1 (b / a) = min a b := by - by_cases h_le : b ≤ a - · -- If b ≤ a, then b/a ≤ 1. - have h_div_le_one : b / a ≤ 1 := (div_le_one ha_pos).mpr h_le - have hne : (a : ℝ) ≠ 0 := ne_of_gt ha_pos - have hmul_div : a * (b / a) = b := by - have h1 : a * (b / a) = (a * b) / a := (mul_div_assoc a b a).symm - have h2 : (a * b) / a = (b * a) / a := by simp [mul_comm] - have h3 : (b * a) / a = b := by exact mul_div_cancel_right₀ b hne - exact h1.trans (h2.trans h3) - simp [min_eq_right h_div_le_one, hmul_div, min_eq_right h_le] - · -- If b > a, then b/a > 1. - have h_lt : a < b := lt_of_not_ge h_le - have h_one_lt_div : 1 < b / a := (one_lt_div ha_pos).mpr h_lt - rw [min_eq_left (le_of_lt h_one_lt_div), mul_one, min_eq_left (le_of_lt h_lt)] - -set_option linter.unusedVariables false in -/-- - Theorem: The MH kernel satisfies the detailed balance condition (reversibility) - with respect to π. This proof is robust and does not require π to be strictly positive. --/ -theorem metropolisHastings_is_reversible [DecidableEq n] (hQ_nonneg : ∀ i j, 0 ≤ Q i j) : - IsReversible (metropolisHastingsKernel π Q) π := by - intro x y - let P := metropolisHastingsKernel π Q - let A := mh_acceptance_prob π Q - by_cases h_eq : x = y - · simp [metropolisHastingsKernel, h_eq] - · - have hyx : y ≠ x := ne_comm.mp h_eq - simp [metropolisHastingsKernel, h_eq, hyx] - let a := π.val x * Q x y - let b := π.val y * Q y x - have ha_nonneg : 0 ≤ a := mul_nonneg (π.property.1 x) (hQ_nonneg x y) - have hb_nonneg : 0 ≤ b := mul_nonneg (π.property.1 y) (hQ_nonneg y x) - have hL : π.val x * (Q x y * A x y) = a * A x y := by - dsimp [a]; ac_rfl - have hR : π.val y * (Q y x * A y x) = b * A y x := by - dsimp [b]; ac_rfl - rw [hL, hR] - by_cases ha_zero : a = 0 - · have hAxy : A x y = 1 := by - have : π.val x * Q x y = a := rfl - simp [A, mh_acceptance_prob, this, ha_zero] - have LHS_zero : a * A x y = 0 := by simp [hAxy, ha_zero] - by_cases hb_zero : b = 0 - · simp [hb_zero, LHS_zero] - · have hAyx : A y x = 0 := by - have hb' : π.val y * Q y x = b := rfl - have ha' : π.val x * Q x y = a := rfl - simp [A, mh_acceptance_prob, hb', ha', hb_zero, ha_zero] - simp [LHS_zero, hAyx] - · have ha_pos : 0 < a := lt_of_le_of_ne ha_nonneg (ne_comm.mp ha_zero) - have hAxy : A x y = min 1 (b / a) := by - have : π.val x * Q x y = a := rfl - simp [A, mh_acceptance_prob, this, ha_zero, b] - have LHS_min : a * A x y = min a b := by - rw [hAxy]; exact mul_min_one_div_eq_min ha_pos hb_nonneg - by_cases hb_zero : b = 0 - · simp [LHS_min, hb_zero, min_eq_right ha_nonneg] - · have hb_pos : 0 < b := lt_of_le_of_ne hb_nonneg (ne_comm.mp hb_zero) - have hAyx : A y x = min 1 (a / b) := by - have hb' : π.val y * Q y x = b := rfl - have ha' : π.val x * Q x y = a := rfl - simp [A, mh_acceptance_prob, hb', ha', hb_zero, a] - have RHS_min : b * A y x = min b a := by - rw [hAyx]; exact mul_min_one_div_eq_min hb_pos ha_nonneg - calc - a * A x y = min a b := LHS_min - _ = min b a := min_comm _ _ - _ = b * A y x := RHS_min.symm - -/-- The Metropolis-Hastings kernel has π as its stationary distribution. -/ -theorem metropolisHastings_is_stationary [DecidableEq n] (hQ_stoch : IsStochastic Q) : - IsStationary (metropolisHastingsKernel π Q) π := by - let P := metropolisHastingsKernel π Q - have hP_stoch : IsStochastic P := metropolisHastings_is_stochastic hQ_stoch - have hP_rev : IsReversible P π := metropolisHastings_is_reversible hQ_stoch.1 - -- Reversibility implies stationarity. - exact hP_rev.is_stationary hP_stoch - -/-- - The IsMCMC instance for the Metropolis-Hastings algorithm. - Stochasticity and Stationarity are guaranteed by construction. Irreducibility and - Primitivity depend on the properties of Q and π and must be supplied. --/ -def isMCMC_MetropolisHastings [DecidableEq n] - (hQ_stoch : IsStochastic Q) - (hP_irred : Matrix.IsIrreducible (metropolisHastingsKernel π Q)) - (hP_prim : IsPrimitive (metropolisHastingsKernel π Q)) : - IsMCMC (metropolisHastingsKernel π Q) π where - stochastic := metropolisHastings_is_stochastic hQ_stoch - stationary := metropolisHastings_is_stationary hQ_stoch - irreducible := hP_irred - primitive := hP_prim - -end MCMC.Finite diff --git a/lean/MCMC/Finite/TotalVariation.lean b/lean/MCMC/Finite/TotalVariation.lean deleted file mode 100644 index 0cc2597..0000000 --- a/lean/MCMC/Finite/TotalVariation.lean +++ /dev/null @@ -1,646 +0,0 @@ ---import Mathlib -import MCMC.PF.LinearAlgebra.Matrix.PerronFrobenius.Multiplicity -import MCMC.Finite.Core - -/-! -This module formalizes the analytical properties of the Dobrushin coefficient for -row-stochastic matrices on finite state spaces. The definitions and theorems are -grounded in the concepts introduced in R. L. Dobrushin's 1956 paper on the -Central Limit Theorem for nonstationary Markov chains. - -The main components are: - -- **Total Variation Distance (`tvDist`):** The module defines the total variation - distance as half the L1-norm between two probability vectors. This corresponds - to the norm of the difference between two measures, `||μ₁ - μ₂||`, as - discussed in the paper (cf. §1.4, eq. 1.10). The formalization includes a proof - (`tvDist_eq_one_sub_sum_min`) of the identity `tvDist(u, v) = 1 - ∑ min(uⱼ, vⱼ)`, - which directly connects the L1-based definition to the formulation `1 - ᾶ(μ₁, μ₂)` - used by Dobrushin (cf. eq. 1.15, 1.16). - -- **Dobrushin Coefficient (`dobrushinCoeff`):** The coefficient `δ(P)` is formalized - as the supremum of the total variation distance between any two rows of the - stochastic matrix `P`. This quantity is related to Dobrushin's ergodic - coefficient `α(P)` by the identity `δ(P) = 1 - α(P)` (cf. eq. 1.5, 1.5'). The - paper's definition for denumerable chains (eq. 1.19), `α(Q) = inf ∑ min(qₖₘ, qₗₘ)`, - is thus equivalent via the `tvDist_eq_one_sub_sum_min` identity. - -- **Contraction and Submultiplicativity:** The module proves two central properties - of the coefficient: - 1. The contraction theorem (`tvDist_contract`), which bounds the TV distance - between the images of two distributions by `δ(P)` times their initial - distance. This is the foundational result that establishes the coefficient's - role in measuring convergence. - 2. The submultiplicativity of the coefficient (`dobrushinCoeff_mul`), which - states `δ(PQ) ≤ δ(P)δ(Q)`. This property corresponds to the inequality - `1 - α(P_composed) ≤ Π(1 - α(P_i))` used in the paper's argument for - bounding the ergodicity of multi-step transitions (cf. §1.11). - -- **Primitivity and Strict Contraction:** The final theorem in this module, - `dobrushinCoeff_pow_lt_one_of_primitive`, establishes a sufficient condition - for strict contractivity. It proves that if a stochastic matrix `P` is primitive, - there exists a power `k > 0` such that `δ(P^k) < 1`. This provides a - concrete combinatorial condition under which the assumptions of uniform - ergodicity (i.e., `α(P) > 0` for some transition `P`) used throughout the - Dobrushin paper are satisfied in the context of a time-homogeneous chain. --/ - -noncomputable section -open Matrix Finset MCMC.Finite -open scoped Matrix - -namespace Matrix - -variable {n : Type*} [Fintype n] - -/-! -Total variation on the finite simplex and the Dobrushin coefficient -for row-stochastic kernels. --/ - -/-- Total variation distance on the finite simplex (sup over sets = 1/2 L1). -/ -def tvDist (p q : n → ℝ) : ℝ := - (∑ j, |p j - q j|) / 2 - -lemma tvDist_nonneg (p q : n → ℝ) : 0 ≤ tvDist p q := by - have : 0 ≤ ∑ j, |p j - q j| := by - exact sum_nonneg (fun _ _ => abs_nonneg _) - have h2 : 0 ≤ (2 : ℝ) := by norm_num - simpa [tvDist, div_eq_mul_inv, mul_comm] - -/-- For finite families with equal total mass, each coordinate deviation is bounded by TV. -/ -lemma coord_abs_le_tvDist_of_eq_sum [DecidableEq n] (p q : n → ℝ) - (hsum : ∑ j, p j = ∑ j, q j) (j : n) : - |p j - q j| ≤ tvDist p q := by - have hx_sum0 : ∑ t, (p t - q t) = 0 := by - simp [sum_sub_distrib, hsum] - have hsplit : - ∑ t, |p t - q t| - = |p j - q j| + ∑ t ∈ (Finset.univ.erase j), |p t - q t| := by - have : (Finset.univ : Finset n) = insert j ((Finset.univ).erase j) := by - simp - calc - ∑ t, |p t - q t| - = ∑ t ∈ insert j ((Finset.univ).erase j), |p t - q t| := by - rw [this] - exact - Eq.symm - (sum_congr (congrArg (insert j) (congrFun (congrArg erase (id (Eq.symm this))) j)) - fun x => congrFun rfl) - _ = |p j - q j| + ∑ t ∈ ((Finset.univ).erase j), |p t - q t| := by - simp [Finset.mem_univ] - have hrest_sum : - ∑ t ∈ (Finset.univ.erase j), (p t - q t) = (q j - p j) := by - have huniv_split : - ∑ t, (p t - q t) - = (p j - q j) + ∑ t ∈ (Finset.univ.erase j), (p t - q t) := by - have : (Finset.univ : Finset n) = insert j ((Finset.univ).erase j) := by - simp - calc - ∑ t, (p t - q t) - = ∑ t ∈ insert j ((Finset.univ).erase j), (p t - q t) := - congrFun (congrArg Finset.sum this) fun t => p t - q t - _ = (p j - q j) + ∑ t ∈ ((Finset.univ).erase j), (p t - q t) := by - simp [Finset.mem_univ] - grind - have hx0' : (p j - q j) + ∑ t ∈ (Finset.univ.erase j), (p t - q t) = 0 := by - simpa [huniv_split] using hx_sum0 - have := eq_neg_of_add_eq_zero_left hx0' - simpa [sub_eq_add_neg, add_comm] using this - have hrest_abs_le : - |∑ t ∈ (Finset.univ.erase j), (p t - q t)| - ≤ ∑ t ∈ (Finset.univ.erase j), |p t - q t| := - (abs_sum_le_sum_abs _ _) - have hL1_ge : - ∑ t, |p t - q t| ≥ |p j - q j| + |q j - p j| := by - calc - ∑ t, |p t - q t| - = |p j - q j| + ∑ t ∈ (Finset.univ.erase j), |p t - q t| := hsplit - _ ≥ |p j - q j| + |∑ t ∈ (Finset.univ.erase j), (p t - q t)| := by - gcongr - _ = |p j - q j| + |q j - p j| := by simp [hrest_sum] - have h2mul : (∑ t, |p t - q t|) ≥ 2 * |p j - q j| := by - simpa [two_mul, abs_sub_comm] using hL1_ge - have h2pos : (0 : ℝ) < 2 := by norm_num - have h_div : |p j - q j| ≤ (∑ t, |p t - q t|) / 2 := (le_div_iff₀' h2pos).mpr h2mul - simpa [tvDist, div_eq_mul_inv, mul_comm] using h_div - -/-- The row `i` of a row-stochastic matrix seen as a probability vector. -/ -def rowDist (P : Matrix n n ℝ) (i : n) : n → ℝ := fun j => P i j - -/-- Dobrushin coefficient δ(P) = sup over pairs of rows of TV distance. -/ -def dobrushinCoeff (P : Matrix n n ℝ) : ℝ := - sSup { d | ∃ i i' : n, d = tvDist (rowDist P i) (rowDist P i') } - -lemma dobrushinCoeff_nonneg [Nonempty n] (P : Matrix n n ℝ) : 0 ≤ dobrushinCoeff P := by - let f : (n × n) → ℝ := fun p => tvDist (rowDist P p.1) (rowDist P p.2) - have hset_eq : { d | ∃ i i' : n, d = tvDist (rowDist P i) (rowDist P i') } - = Set.range f := by - ext d; constructor - · intro h; rcases h with ⟨i, i', rfl⟩; exact ⟨⟨i, i'⟩, rfl⟩ - · intro h; rcases h with ⟨⟨i, i'⟩, rfl⟩; exact ⟨i, i', rfl⟩ - have hfin : (Set.range f).Finite := (Set.finite_range f) - have hmem : 0 ∈ Set.range f := by - let i0 : n := Classical.arbitrary n - refine ⟨⟨i0, i0⟩, ?_⟩ - simp [f, rowDist, tvDist, sub_self, abs_zero, sum_const_zero] - have hbdd : BddAbove (Set.range f) := hfin.bddAbove - simpa [dobrushinCoeff, hset_eq] using le_csSup hbdd hmem - -/-- Contraction in TV under a row-stochastic kernel (with Dobrushin's coefficient). -/ -lemma tvDist_contract [Nonempty n] - (P : Matrix n n ℝ) --(hP : MCMC.Finite.IsStochastic P) - (p q : n → ℝ)-- (hp : ∀ j, 0 ≤ p j) (hq : ∀ j, 0 ≤ q j) - (hp1 : ∑ j, p j = 1) (hq1 : ∑ j, q j = 1) : - tvDist ((fun j => ∑ k, p k * P k j)) ((fun j => ∑ k, q k * P k j)) - ≤ dobrushinCoeff P * tvDist p q := by - let r : n → ℝ := fun k => p k - q k - have hsum_r : ∑ k, r k = 0 := by - simp [r, sum_sub_distrib, hp1, hq1] - let a : n → ℝ := fun j => ∑ k, r k * P k j - let s : n → ℝ := fun j => if 0 ≤ a j then 1 else -1 - have hs_abs : ∀ j, |s j| = 1 := by - intro j; by_cases h : 0 ≤ a j - · simp [s, h] - · have : a j < 0 := lt_of_not_ge h - simp [s, h] - have hsum_abs_eq : ∑ j, |a j| = ∑ j, s j * a j := by - apply sum_congr rfl; intro j _ - by_cases h : 0 ≤ a j - · simp [s, h, abs_of_nonneg h] - · have : a j < 0 := lt_of_not_ge h - have hnn : a j ≤ 0 := le_of_lt this - aesop - let g : n → ℝ := fun k => ∑ j, s j * P k j - have hswap : ∑ j, s j * a j = ∑ k, r k * g k := by - unfold a g - calc - ∑ j, s j * ∑ k, r k * P k j - = ∑ j, ∑ k, s j * (r k * P k j) := by - simp [mul_sum] - _ = ∑ j, ∑ k, r k * (s j * P k j) := by - apply sum_congr rfl; intro j _; simp [mul_left_comm, mul_assoc, mul_comm] - _ = ∑ k, ∑ j, r k * (s j * P k j) := by - simpa using - (Finset.sum_comm (s := (Finset.univ : Finset n)) - (t := (Finset.univ : Finset n)) - (f := fun j k => r k * (s j * P k j))) - _ = ∑ k, r k * ∑ j, s j * P k j := by - simp [mul_sum] - -- oscillation bound for g via rows of P - have g_diff_le : ∀ k ℓ, |g k - g ℓ| ≤ 2 * dobrushinCoeff P := by - intro k ℓ - have : |g k - g ℓ| ≤ ∑ j, |P k j - P ℓ j| := by - have : g k - g ℓ = ∑ j, s j * (P k j - P ℓ j) := by - simp [g, sum_sub_distrib, mul_sub] - calc - |g k - g ℓ| = |∑ j, s j * (P k j - P ℓ j)| := by simp [this] - _ ≤ ∑ j, |s j * (P k j - P ℓ j)| := by - simpa using - (abs_sum_le_sum_abs - (s := (Finset.univ : Finset n)) - (f := fun j => s j * (P k j - P ℓ j))) - _ = ∑ j, |s j| * |P k j - P ℓ j| := by - apply sum_congr rfl; intro j _; simp [abs_mul] - _ = ∑ j, |P k j - P ℓ j| := by - simp [hs_abs] - let f : (n × n) → ℝ := fun p => tvDist (rowDist P p.1) (rowDist P p.2) - have hset_eq : - { d | ∃ i i' : n, d = tvDist (rowDist P i) (rowDist P i') } = Set.range f := by - ext d; constructor - · intro h; rcases h with ⟨i, i', rfl⟩; exact ⟨⟨i, i'⟩, rfl⟩ - · intro h; rcases h with ⟨⟨i, i'⟩, rfl⟩; exact ⟨i, i', rfl⟩ - have hbdd : BddAbove (Set.range f) := (Set.finite_range f).bddAbove - have h_tv_le : tvDist (rowDist P k) (rowDist P ℓ) ≤ dobrushinCoeff P := by - have : tvDist (rowDist P k) (rowDist P ℓ) ∈ Set.range f := ⟨⟨k, ℓ⟩, rfl⟩ - simpa [dobrushinCoeff, hset_eq] using (le_csSup hbdd this) - have : |g k - g ℓ| ≤ 2 * tvDist (rowDist P k) (rowDist P ℓ) := by - simpa [two_mul, tvDist, rowDist, div_eq_mul_inv, mul_comm] using this - exact this.trans (by - have : tvDist (rowDist P k) (rowDist P ℓ) ≤ dobrushinCoeff P := h_tv_le - have hnonneg : 0 ≤ 2 := by norm_num - simp [*]) - obtain ⟨kmax, _, hkmax⟩ := - Finset.exists_max_image (s := (Finset.univ : Finset n)) (f := fun k => g k) - (by simp) - obtain ⟨kmin, _, hkmin⟩ := - Finset.exists_min_image (s := (Finset.univ : Finset n)) (f := fun k => g k) - (by simp) - have h_le_max : ∀ k, g k ≤ g kmax := by intro k; exact hkmax k (by simp) - have h_ge_min : ∀ k, g kmin ≤ g k := by intro k; exact hkmin k (by simp) - let rpos : n → ℝ := fun k => max (r k) 0 - let rneg : n → ℝ := fun k => max (-r k) 0 - have hrpos_nonneg : ∀ k, 0 ≤ rpos k := by - intro k; have : 0 ≤ max (0:ℝ) (r k) := le_max_left _ _ - simp [rpos] - have hrneg_nonneg : ∀ k, 0 ≤ rneg k := by - intro k; have : 0 ≤ max (0:ℝ) (-r k) := le_max_left _ _ - simp [rneg] - have h_r_decomp : ∀ k, r k = rpos k - rneg k := by - intro k; by_cases hk : 0 ≤ r k - · have : -r k ≤ 0 := neg_nonpos.mpr hk - simp [rpos, rneg, hk, this] - · have hk' : r k ≤ 0 := le_of_lt (lt_of_not_ge hk) - have hneg' : 0 ≤ -r k := neg_nonneg.mpr hk' - simp [rpos, rneg, hk', hneg', sub_eq_add_neg, add_comm] - have hsum_pos_eq_neg : ∑ k, rpos k = ∑ k, rneg k := by - have hsum : (∑ k, rpos k) - (∑ k, rneg k) = 0 := by - simpa [h_r_decomp, sum_sub_distrib] using hsum_r - exact sub_eq_zero.mp hsum - have h_abs_split : ∀ k, |r k| = rpos k + rneg k := by - intro k; by_cases hk : 0 ≤ r k - · have : -r k ≤ 0 := neg_nonpos.mpr hk - simp [rpos, rneg, hk, this, abs_of_nonneg] - · have hk' : r k ≤ 0 := le_of_lt (lt_of_not_ge hk) - have hneg' : 0 ≤ -r k := neg_nonneg.mpr hk' - simp [rpos, rneg, hk', hneg', abs_of_nonpos, add_comm] - have hsum_abs : ∑ k, |r k| = 2 * (∑ k, rpos k) := by - calc - ∑ k, |r k| = ∑ k, (rpos k + rneg k) := by - apply sum_congr rfl; intro k _; simp [h_abs_split k] - _ = (∑ k, rpos k) + (∑ k, rneg k) := by - simp [sum_add_distrib] - _ = (∑ k, rpos k) + (∑ k, rpos k) := by - simp [hsum_pos_eq_neg] - _ = 2 * (∑ k, rpos k) := by ring - let α : ℝ := ∑ k, rpos k - have h_sum_pos_le : ∑ k, rpos k * g k ≤ (g kmax) * α := by - calc - ∑ k, rpos k * g k - ≤ ∑ k, rpos k * (g kmax) := by - apply sum_le_sum; intro k _; exact mul_le_mul_of_nonneg_left (h_le_max k) (hrpos_nonneg k) - _ = (g kmax) * ∑ k, rpos k := by - simp [mul_comm] - exact Eq.symm (sum_mul univ (fun i => max (r i) 0) (g kmax)) - _ = (g kmax) * α := rfl - have h_sum_neg_ge : ∑ k, rneg k * g k ≥ (g kmin) * α := by - have hα : α = ∑ k, rneg k := by - simpa [α] using hsum_pos_eq_neg - have h : (g kmin) * α ≤ ∑ k, rneg k * g k := by - calc - (g kmin) * α = ∑ k, rneg k * (g kmin) := by - simp [mul_comm, hα, sum_mul] - _ ≤ ∑ k, rneg k * g k := by - apply sum_le_sum; intro k _; exact mul_le_mul_of_nonneg_left (h_ge_min k) (hrneg_nonneg k) - simpa using h - have h_rg_le : ∑ k, r k * g k ≤ (g kmax - g kmin) * α := by - have h' : - ∑ k, rpos k * g k - ∑ k, rneg k * g k - ≤ g kmax * α - g kmin * α := - sub_le_sub h_sum_pos_le h_sum_neg_ge - have h'' : - ∑ k, rpos k * g k - ∑ k, rneg k * g k - ≤ (g kmax - g kmin) * α := by - have hR : g kmax * α - g kmin * α = (g kmax - g kmin) * α := by ring - simpa [hR] using h' - have hL : - ∑ k, r k * g k - = ∑ k, rpos k * g k - ∑ k, rneg k * g k := by - calc - ∑ k, r k * g k - = ∑ k, (rpos k - rneg k) * g k := by - apply sum_congr rfl; intro k _; simp [h_r_decomp k] - _ = ∑ k, (rpos k * g k - rneg k * g k) := by - apply sum_congr rfl; intro k _; rw [@mul_sub_right_distrib] - _ = ∑ k, rpos k * g k - ∑ k, rneg k * g k := by - simp [sum_sub_distrib] - simpa [hL] using h'' - have h_sum_bound : ∑ j, |a j| ≤ (g kmax - g kmin) / 2 * ∑ k, |r k| := by - have hα : α = (∑ k, |r k|) / 2 := by - have h2pos : (0 : ℝ) < 2 := by norm_num - have : 2 * α = ∑ k, |r k| := by - simp [α, hsum_abs, two_mul] - have : ∑ k, |r k| = 2 * α := by simpa [α] using hsum_abs - have hnonneg : 0 ≤ (2 : ℝ) := by norm_num - calc - α = (2 * α) / 2 := by field_simp - _ = (∑ k, |r k|) / 2 := by simp [this] - have : ∑ j, |a j| = ∑ k, r k * g k := by - simpa [hswap] using hsum_abs_eq - calc - ∑ j, |a j| = ∑ k, r k * g k := this - _ ≤ (g kmax - g kmin) * α := h_rg_le - _ = ((g kmax - g kmin) / 2) * (∑ k, |r k|) := by - have : α = (∑ k, |r k|) / 2 := hα - simp [this, div_eq_mul_inv, mul_comm, mul_left_comm, mul_assoc] - -- relate oscillation of g to δ(P) - have h_osc_le : g kmax - g kmin ≤ 2 * dobrushinCoeff P := by - have := g_diff_le kmax kmin - have hge : g kmin ≤ g kmax := h_ge_min kmax - have : |g kmax - g kmin| = g kmax - g kmin := by - simp [abs_of_nonneg (sub_nonneg.mpr hge)] - simp; grind - have hLHS : tvDist (fun j => ∑ k, p k * P k j) (fun j => ∑ k, q k * P k j) - = (∑ j, |a j|) / 2 := by - simp only [tvDist, a, r, sub_eq_add_neg] - congr 1 - dsimp [sum_neg_distrib, sum_add_distrib] - congr 1 - ext j - dsimp [sum_add_distrib, mul_neg, sum_neg_distrib] - ring_nf - simp - have hR_r : tvDist p q = (∑ k, |r k|) / 2 := by - simp [tvDist, r] - have hcoef : (g kmax - g kmin) / 2 ≤ dobrushinCoeff P := by - have h2pos : (0 : ℝ) < 2 := by norm_num - rwa [div_le_iff h2pos, mul_comm] - have h_mul : (∑ j, |a j|) ≤ dobrushinCoeff P * ∑ k, |r k| := by - have S_nonneg : 0 ≤ ∑ k, |r k| := - sum_nonneg (by - intro _ _ - exact abs_nonneg _) - have h_temp : - ((g kmax - g kmin) / 2) * (∑ k, |r k|) ≤ - dobrushinCoeff P * (∑ k, |r k|) := - mul_le_mul_of_nonneg_right hcoef S_nonneg - exact h_sum_bound.trans h_temp - have h_div : (∑ j, |a j|) / 2 ≤ (dobrushinCoeff P * ∑ k, |r k|) / 2 := by - have : (1 / 2 : ℝ) * (∑ j, |a j|) ≤ - (1 / 2 : ℝ) * (dobrushinCoeff P * ∑ k, |r k|) := - mul_le_mul_of_nonneg_left h_mul (by norm_num) - simpa [div_eq_mul_inv, mul_comm, mul_left_comm, mul_assoc] using this - have : tvDist (fun j => ∑ k, p k * P k j) - (fun j => ∑ k, q k * P k j) - ≤ dobrushinCoeff P * tvDist p q := by - rw [hLHS, hR_r, ← mul_div_assoc] - exact h_div - exact this - -open MCMC.Finite - -/-- Submultiplicativity of the Dobrushin coefficient. -/ -lemma dobrushinCoeff_mul [DecidableEq n] (P Q : Matrix n n ℝ) - [Nonempty n] - (hP : IsStochastic P) (_ : IsStochastic Q) : - dobrushinCoeff (P * Q) ≤ dobrushinCoeff P * dobrushinCoeff Q := by - classical - -- we rewrite δ(P*Q) as sSup over a finite range - let fPQ : (n × n) → ℝ := fun p => tvDist (rowDist (P * Q) p.1) (rowDist (P * Q) p.2) - have hset_eq_PQ : - { d | ∃ i i' : n, d = tvDist (rowDist (P * Q) i) (rowDist (P * Q) i') } - = Set.range fPQ := by - ext d; constructor - · intro h; rcases h with ⟨i, i', rfl⟩; exact ⟨⟨i, i'⟩, rfl⟩ - · intro h; rcases h with ⟨⟨i, i'⟩, rfl⟩; exact ⟨i, i', rfl⟩ - have hbddPQ : BddAbove (Set.range fPQ) := (Set.finite_range fPQ).bddAbove - have hforall : - ∀ d ∈ Set.range fPQ, d ≤ dobrushinCoeff P * dobrushinCoeff Q := by - intro d hd - rcases hd with ⟨⟨i, i'⟩, rfl⟩ - have hp1 : ∑ j, rowDist P i j = 1 := by simpa [rowDist] using hP.2 i - have hq1 : ∑ j, rowDist P i' j = 1 := by simpa [rowDist] using hP.2 i' - -- contract by Q - have hcontract : - tvDist (rowDist (P * Q) i) (rowDist (P * Q) i') - ≤ dobrushinCoeff Q * tvDist (rowDist P i) (rowDist P i') := by - simpa [rowDist, Matrix.mul_apply] using - (tvDist_contract (P := Q) (p := rowDist P i) (q := rowDist P i') (hp1 := hp1) (hq1 := hq1)) - -- bound tvDist among rows of P by δ(P) - let fP : (n × n) → ℝ := fun p => tvDist (rowDist P p.1) (rowDist P p.2) - have hset_eq_P : - { d | ∃ i i' : n, d = tvDist (rowDist P i) (rowDist P i') } = Set.range fP := by - ext d; constructor - · intro h; rcases h with ⟨i, i', rfl⟩; exact ⟨⟨i, i'⟩, rfl⟩ - · intro h; rcases h with ⟨⟨i, i'⟩, rfl⟩; exact ⟨i, i', rfl⟩ - have hbddP : BddAbove (Set.range fP) := (Set.finite_range fP).bddAbove - have hleP : tvDist (rowDist P i) (rowDist P i') ≤ dobrushinCoeff P := by - have hx : tvDist (rowDist P i) (rowDist P i') ∈ Set.range fP := ⟨⟨i, i'⟩, rfl⟩ - simpa [dobrushinCoeff, hset_eq_P] using le_csSup hbddP hx - have hnonnegQ : 0 ≤ dobrushinCoeff Q := dobrushinCoeff_nonneg (P := Q) - have := hcontract.trans (mul_le_mul_of_nonneg_left hleP hnonnegQ) - simpa [mul_comm] using this - have hnonemptyPQ : (Set.range fPQ).Nonempty := by - classical - let i0 : n := Classical.arbitrary n - exact ⟨fPQ ⟨i0, i0⟩, ⟨⟨i0, i0⟩, rfl⟩⟩ - have : sSup (Set.range fPQ) ≤ dobrushinCoeff P * dobrushinCoeff Q := - csSup_le hnonemptyPQ hforall - simpa [dobrushinCoeff, hset_eq_PQ] using this - -/-- Power bound for the Dobrushin coefficient. -/ -lemma dobrushinCoeff_pow [DecidableEq n] (P : Matrix n n ℝ) [Nonempty n] (hP : MCMC.Finite.IsStochastic P) (k : ℕ) : - dobrushinCoeff (P^k) ≤ (dobrushinCoeff P)^k := by - classical - induction' k with k ih - · -- base: δ(I) ≤ 1 = (δ P)^0 - have hpair : - ∀ i i' : n, - tvDist (rowDist (1 : Matrix n n ℝ) i) (rowDist (1 : Matrix n n ℝ) i') ≤ 1 := by - intro i i' - have hpt : - ∀ j, - |(1 : Matrix n n ℝ) i j - (1 : Matrix n n ℝ) i' j| - ≤ |(1 : Matrix n n ℝ) i j| + |(1 : Matrix n n ℝ) i' j| := by - intro j - simpa [sub_eq_add_neg] using - (abs_add_le ((1 : Matrix n n ℝ) i j) (-(1 : Matrix n n ℝ) i' j)) - have hsum_le : - ∑ j, |(1 : Matrix n n ℝ) i j - (1 : Matrix n n ℝ) i' j| - ≤ ∑ j, (|(1 : Matrix n n ℝ) i j| + |(1 : Matrix n n ℝ) i' j|) := by - apply sum_le_sum - intro j _ - simpa using hpt j - have hsum_abs_one (i : n) : ∑ j, |(1 : Matrix n n ℝ) i j| = 1 := by - classical - have habs : ∀ j, |(1 : Matrix n n ℝ) i j| = (if i = j then (1 : ℝ) else 0) := by - intro j - by_cases h : i = j - · simp [Matrix.one_apply, h] - · simp [h] - have hsum_eq : - ∑ j, |(1 : Matrix n n ℝ) i j| = ∑ j, (if i = j then (1 : ℝ) else 0) := by - apply sum_congr rfl - intro j _ - simpa using habs j - have hsum_ind : ∑ j, (if i = j then (1 : ℝ) else 0) = 1 := by - simp - simp [hsum_eq] - have : (∑ j, |(1 : Matrix n n ℝ) i j - (1 : Matrix n n ℝ) i' j|) / 2 ≤ 1 := by - have h2 : (0 : ℝ) < 2 := by norm_num - have hnum : - ∑ j, |(1 : Matrix n n ℝ) i j - (1 : Matrix n n ℝ) i' j| ≤ 2 := by - have hbound : - ∑ j, |(1 : Matrix n n ℝ) i j - (1 : Matrix n n ℝ) i' j| - ≤ (∑ j, |(1 : Matrix n n ℝ) i j|) + (∑ j, |(1 : Matrix n n ℝ) i' j|) := by - simpa [sum_add_distrib] using hsum_le - have hx : (∑ j, |(1 : Matrix n n ℝ) i j|) + (∑ j, |(1 : Matrix n n ℝ) i' j|) = (2 : ℝ) := by - have h12 : (1 : ℝ) + 1 = 2 := by norm_num - simpa [hsum_abs_one i, hsum_abs_one i'] using h12 - simpa [hx] using hbound - exact (div_le_iff h2).mpr (by simpa [one_mul] using hnum) - simpa [tvDist, rowDist] using this - let fId : (n × n) → ℝ := - fun p => tvDist (rowDist (1 : Matrix n n ℝ) p.1) (rowDist (1 : Matrix n n ℝ) p.2) - have hforall : ∀ d ∈ Set.range fId, d ≤ 1 := by - intro d hd - rcases hd with ⟨⟨i, i'⟩, rfl⟩ - simpa using hpair i i' - have hnonempty : (Set.range fId).Nonempty := by - let i0 : n := Classical.arbitrary n - exact ⟨fId ⟨i0, i0⟩, ⟨⟨i0, i0⟩, rfl⟩⟩ - have hset_eqId : - { d | ∃ i i' : n, d = tvDist (rowDist (1 : Matrix n n ℝ) i) (rowDist (1 : Matrix n n ℝ) i') } - = Set.range fId := by - ext d; constructor - · intro h; rcases h with ⟨i, i', rfl⟩; exact ⟨⟨i, i'⟩, rfl⟩ - · intro h; rcases h with ⟨⟨i, i'⟩, rfl⟩; exact ⟨i, i', rfl⟩ - have : sSup (Set.range fId) ≤ 1 := csSup_le hnonempty hforall - simpa [dobrushinCoeff, pow_zero, rowDist, hset_eqId] using this - · have hPow : IsStochastic (P^k) := by - simpa using (isStochastic_pow (P := P) (hP := hP) k) - have hmul : - dobrushinCoeff (P^(k+1)) ≤ dobrushinCoeff (P^k) * dobrushinCoeff P := by - simpa [pow_succ] using - (dobrushinCoeff_mul (P := P^k) (Q := P) (hP := hPow) hP) - have hnonneg : 0 ≤ dobrushinCoeff P := dobrushinCoeff_nonneg (P := P) - have := hmul.trans (mul_le_mul_of_nonneg_right ih hnonneg) - simpa [pow_succ, mul_left_comm, mul_comm, mul_assoc] using this - -/-- Entrywise deviation is bounded by TV distance when the totals match. -/ -lemma entry_abs_le_tvDist_of_rows [DecidableEq n] - (P : Matrix n n ℝ) (i : n) (x : n → ℝ) (j : n) - (hsum : ∑ t, rowDist P i t = ∑ t, x t) : - |(P i j) - x j| ≤ tvDist (rowDist P i) x := by - simpa [rowDist] using - coord_abs_le_tvDist_of_eq_sum (p := rowDist P i) (q := x) (hsum := by simpa [rowDist] using hsum) (j := j) - --- Helper: TV identity for nonnegative unit-mass vectors. -lemma tvDist_eq_one_sub_sum_min - (u v : n → ℝ) - (hu0 : ∀ j, 0 ≤ u j) (hv0 : ∀ j, 0 ≤ v j) - (hu1 : ∑ j, u j = 1) (hv1 : ∑ j, v j = 1) : - Matrix.tvDist u v = 1 - ∑ j, min (u j) (v j) := by - classical - have habs (a b : ℝ) (ha : 0 ≤ a) (hb : 0 ≤ b) : - |a - b| = a + b - 2 * min a b := by - by_cases h : a ≤ b - · have hnonpos : a - b ≤ 0 := sub_nonpos.mpr h - have hmin : min a b = a := by simpa [min_eq_left_iff] using h - calc - |a - b| = -(a - b) := abs_of_nonpos hnonpos - _ = b - a := by ring - _ = a + b - 2 * a := by ring - _ = a + b - 2 * min a b := by simp [hmin] - · have h' : b ≤ a := le_of_not_ge h - have hnonneg : 0 ≤ a - b := sub_nonneg.mpr h' - have hmin : min a b = b := by simpa [min_eq_right_iff] using h' - calc - |a - b| = a - b := abs_of_nonneg hnonneg - _ = a + b - 2 * b := by ring - _ = a + b - 2 * min a b := by simp [hmin] - have hsum_abs : - ∑ j, |u j - v j| - = ∑ j, (u j + v j - 2 * min (u j) (v j)) := by - apply sum_congr rfl - intro j _ - exact habs (u j) (v j) (hu0 j) (hv0 j) - have h2ne : (2 : ℝ) ≠ 0 := by norm_num - have hdiv : - (∑ j, |u j - v j|) / 2 - = 1 - ∑ j, min (u j) (v j) := by - calc - (∑ j, |u j - v j|) / 2 - = (∑ j, (u j + v j - 2 * min (u j) (v j))) / 2 := by - simp [hsum_abs] - _ = ((∑ j, (u j + v j)) - ∑ j, (2 * min (u j) (v j))) / 2 := by - simp [sum_sub_distrib] - _ = ((∑ j, u j) + (∑ j, v j) - 2 * ∑ j, min (u j) (v j)) / 2 := by - simp only [sum_add_distrib, two_mul] - _ = (2 - 2 * ∑ j, min (u j) (v j)) / 2 := by - simp only [hu1, hv1, one_add_one_eq_two] - _ = 1 - ∑ j, min (u j) (v j) := by - have := sub_div (2 : ℝ) (2 * ∑ j, min (u j) (v j)) (2 : ℝ) - simpa [h2ne, mul_comm, mul_left_comm, mul_assoc] using this - simpa [Matrix.tvDist] using hdiv - -/-- - Primitive ⇒ some power has Dobrushin coefficient strictly less than 1, - with an explicit quantitative bound via the minimal entry. --/ -theorem dobrushinCoeff_pow_lt_one_of_primitive - [Nonempty n] [ DecidableEq n] (P : Matrix n n ℝ) - (h_stoch : IsStochastic P) (h_prim : IsPrimitive P) : - ∃ k > 0, Matrix.dobrushinCoeff (P^k) < 1 := by - classical - rcases h_prim with ⟨h_nonneg, ⟨k, hk_pos, hpos⟩⟩ - have hPk : IsStochastic (P^k) := by - simpa using (isStochastic_pow (P := P) (hP := h_stoch) k) - let s : Finset (n × n) := (Finset.univ.product Finset.univ) - obtain ⟨p0, hp0_mem, hmin⟩ := - Finset.exists_min_image (s := s) (f := fun p : n×n => (P^k) p.1 p.2) - (by - refine (Finset.univ_nonempty.product Finset.univ_nonempty)) - let β : ℝ := (P^k) p0.1 p0.2 - have hβ_pos : 0 < β := hpos p0.1 p0.2 - have hβ_le (i j : n) : β ≤ (P^k) i j := by - have hij_mem : (i, j) ∈ s := by simp [s] - exact hmin (i, j) hij_mem - have h_overlap (i i' : n) : - ∑ j, min ((P^k) i j) ((P^k) i' j) ≥ (Fintype.card n : ℝ) * β := by - have hpoint : ∀ j, β ≤ min ((P^k) i j) ((P^k) i' j) := by - intro j - have h1 := hβ_le i j - have h2 := hβ_le i' j - exact le_min_iff.mpr ⟨h1, h2⟩ - have : ∑ j, (β : ℝ) ≤ ∑ j, min ((P^k) i j) ((P^k) i' j) := - sum_le_sum (by intro j _; exact hpoint j) - simpa [Finset.sum_const, Finset.card_univ, nsmul_eq_mul, mul_comm, mul_left_comm, mul_assoc] - using this - have hpair (i i' : n) : - Matrix.tvDist (Matrix.rowDist (P^k) i) (Matrix.rowDist (P^k) i') - ≤ 1 - (Fintype.card n : ℝ) * β := by - have hi0 : ∀ j, 0 ≤ (P^k) i j := by intro j; exact (hPk.1 i j) - have hi'0 : ∀ j, 0 ≤ (P^k) i' j := by intro j; exact (hPk.1 i' j) - have hi1 : ∑ j, (P^k) i j = 1 := by simpa [Matrix.rowDist] using hPk.2 i - have hi'1 : ∑ j, (P^k) i' j = 1 := by simpa [Matrix.rowDist] using hPk.2 i' - have := tvDist_eq_one_sub_sum_min - (u := Matrix.rowDist (P^k) i) (v := Matrix.rowDist (P^k) i') - (hu0 := hi0) (hv0 := hi'0) (hu1 := hi1) (hv1 := hi'1) - have : Matrix.tvDist (Matrix.rowDist (P^k) i) (Matrix.rowDist (P^k) i') - = 1 - ∑ j, min ((P^k) i j) ((P^k) i' j) := by - simpa [Matrix.rowDist] using this - have hover := h_overlap i i' - have : Matrix.tvDist (Matrix.rowDist (P^k) i) (Matrix.rowDist (P^k) i') - ≤ 1 - (Fintype.card n : ℝ) * β := by - rw [this] - have h_mono : 1 - ∑ j, min ((P^k) i j) ((P^k) i' j) - ≤ 1 - (Fintype.card n : ℝ) * β := by - exact sub_le_sub_left hover 1 - exact h_mono - exact this - -- Take sup over all pairs of rows to bound δ(P^k) - let fPk : (n × n) → ℝ := - fun p => Matrix.tvDist (Matrix.rowDist (P^k) p.1) (Matrix.rowDist (P^k) p.2) - have hforall : ∀ d ∈ Set.range fPk, d ≤ 1 - (Fintype.card n : ℝ) * β := by - intro d hd; rcases hd with ⟨⟨i, i'⟩, rfl⟩; simpa using hpair i i' - have hnonempty : (Set.range fPk).Nonempty := by - let i0 : n := Classical.arbitrary n - exact ⟨fPk ⟨i0, i0⟩, ⟨⟨i0, i0⟩, rfl⟩⟩ - have hset_eq : - { d | ∃ i i' : n, d = Matrix.tvDist (Matrix.rowDist (P^k) i) (Matrix.rowDist (P^k) i') } - = Set.range fPk := by - ext d; constructor - · intro h; rcases h with ⟨i, i', rfl⟩; exact ⟨⟨i, i'⟩, rfl⟩ - · intro h; rcases h with ⟨⟨i, i'⟩, rfl⟩; exact ⟨i, i', rfl⟩ - have h_sup : - Matrix.dobrushinCoeff (P^k) ≤ 1 - (Fintype.card n : ℝ) * β := by - have : sSup (Set.range fPk) ≤ 1 - (Fintype.card n : ℝ) * β := - csSup_le hnonempty hforall - simpa [Matrix.dobrushinCoeff, hset_eq] using this - have hcard_pos : 0 < (Fintype.card n : ℝ) := by - have : (0 : ℕ) < Fintype.card n := Fintype.card_pos_iff.mpr ‹Nonempty n› - exact_mod_cast this - have hbound_lt_one : 1 - (Fintype.card n : ℝ) * β < 1 := by - have : 0 < (Fintype.card n : ℝ) * β := mul_pos hcard_pos hβ_pos - linarith - exact ⟨k, hk_pos, lt_of_le_of_lt h_sup hbound_lt_one⟩ - --- Primitive stochastic matrices admit a power with Dobrushin coefficient < 1. -theorem dobrushinCoeff_lt_one_of_primitive - [DecidableEq n] [Nonempty n] (P : Matrix n n ℝ) - (h_stoch : IsStochastic P) (h_prim : IsPrimitive P) : - ∃ k > 0, Matrix.dobrushinCoeff (P^k) < 1 := by - exact dobrushinCoeff_pow_lt_one_of_primitive (P := P) h_stoch h_prim - -end Matrix diff --git a/lean/MCMC/Finite/toKernel.lean b/lean/MCMC/Finite/toKernel.lean deleted file mode 100644 index 5bb6b21..0000000 --- a/lean/MCMC/Finite/toKernel.lean +++ /dev/null @@ -1,515 +0,0 @@ -import MCMC.Finite.MetropolisHastings -import Mathlib.Probability.Kernel.Invariance - -set_option maxHeartbeats 0 -namespace MCMC.Finite - -open ProbabilityTheory MeasureTheory Matrix Kernel Mathlib -open scoped Mathlib - -variable {n : Type*} [Fintype n] [DecidableEq n] [MeasurableSpace n] [MeasurableSingletonClass n] - -/-- Convert a stochastic matrix to a Markov kernel on finite types -/ -noncomputable def matrixToKernel (P : Matrix n n ℝ) (_ : IsStochastic P) : - Kernel n n := - -- Build kernel rows as sums of (nonnegative) weights times Dirac measures. - let rowMeasure : n → Measure n := - fun i => ∑ j : n, (ENNReal.ofReal (P i j)) • Measure.dirac j - ProbabilityTheory.Kernel.ofFunOfCountable rowMeasure - -/-- Convert a probability vector to a probability measure on finite types -/ -noncomputable def vecToMeasure (π : stdSimplex ℝ n) : Measure n := - ∑ i : n, (ENNReal.ofReal (π.val i)) • Measure.dirac i - -omit [DecidableEq n] in -private lemma kernel_eval_on_set - {P : Matrix n n ℝ} (hP : IsStochastic P) (i : n) - (B : Set n) (hB : MeasurableSet B) : - (matrixToKernel P hP) i B - = ∑ j : n, ENNReal.ofReal (P i j) * B.indicator 1 j := by - change ((∑ j : n, ENNReal.ofReal (P i j) • Measure.dirac j) : Measure n) B = _ - simp [hB, Measure.dirac_apply'] - -omit [DecidableEq n] in -private lemma kernel_eval_singleton - {P : Matrix n n ℝ} (hP : IsStochastic P) (i j : n) : - (matrixToKernel P hP) i {j} = ENNReal.ofReal (P i j) := by - have hB : MeasurableSet ({j} : Set n) := measurableSet_singleton j - rw [kernel_eval_on_set (P := P) (hP := hP) (i := i) ({j}) hB] - rw [Finset.sum_eq_single j] - · simp [Set.indicator_of_mem, Set.mem_singleton_iff] - · intro b _ hb - simp [Set.indicator_of_notMem, hb] - · simp - -open scoped ENNReal - -omit [DecidableEq n] in -/-- Matrix stationarity is equivalent to kernel invariance -/ -theorem isStationary_iff_invariant (P : Matrix n n ℝ) (π : stdSimplex ℝ n) - (hP : IsStochastic P) : - IsStationary P π ↔ - Kernel.Invariant (matrixToKernel P hP) (vecToMeasure π) := by - set κ := matrixToKernel P hP - set μ := vecToMeasure (n := n) π - constructor - · -- (→) Stationarity ⇒ Invariance - intro hstat - ext s hs - have hL0 : - (μ.bind κ) s = ∑ i : n, ENNReal.ofReal (π.val i) * κ i s := by - simp [μ, vecToMeasure, Measure.bind_apply hs (Kernel.aemeasurable _), lintegral_dirac] - have hk : - ∀ i, κ i s = ∑ j : n, ENNReal.ofReal (P i j) * s.indicator 1 j := by - intro i - change ((∑ j : n, ENNReal.ofReal (P i j) • Measure.dirac j) : Measure n) s - = ∑ j : n, ENNReal.ofReal (P i j) * s.indicator 1 j - simp [hs, Measure.dirac_apply'] - have hL1 : - (μ.bind κ) s - = ∑ i : n, ∑ j : n, - ENNReal.ofReal (π.val i) * - (ENNReal.ofReal (P i j) * s.indicator 1 j) := by - simp [hL0, hk, Finset.mul_sum, mul_left_comm] - have hswap : - ∑ i : n, ∑ j : n, - ENNReal.ofReal (π.val i) * - (ENNReal.ofReal (P i j) * s.indicator 1 j) - = ∑ j : n, ∑ i : n, - ENNReal.ofReal (π.val i) * - (ENNReal.ofReal (P i j) * s.indicator 1 j) := by - simpa using - (Finset.sum_comm - (s := (Finset.univ : Finset n)) - (t := (Finset.univ : Finset n)) - (f := fun i j => - ENNReal.ofReal (π.val i) * - (ENNReal.ofReal (P i j) * s.indicator 1 j))) - have hLHS : - (μ.bind κ) s - = ∑ j : n, (∑ i : n, - ENNReal.ofReal (π.val i) * ENNReal.ofReal (P i j)) * - s.indicator 1 j := by - calc - (μ.bind κ) s - = ∑ i : n, ∑ j : n, - ENNReal.ofReal (π.val i) * - (ENNReal.ofReal (P i j) * s.indicator 1 j) := hL1 - _ = ∑ j : n, ∑ i : n, - ENNReal.ofReal (π.val i) * - (ENNReal.ofReal (P i j) * s.indicator 1 j) := hswap - _ = ∑ j : n, - (∑ i : n, - ENNReal.ofReal (π.val i) * ENNReal.ofReal (P i j)) - * s.indicator 1 j := by - refine Finset.sum_congr rfl (fun j _ => ?_) - simpa [mul_left_comm, mul_assoc] using - (Finset.sum_mul - (s := (Finset.univ : Finset n)) - (f := fun i => ENNReal.ofReal (π.val i) * ENNReal.ofReal (P i j)) - (a := s.indicator 1 j)).symm - have hRHS : - μ s = ∑ j : n, ENNReal.ofReal (π.val j) * s.indicator 1 j := by - simp [μ, vecToMeasure, hs, Measure.dirac_apply'] - have hπ_nonneg : ∀ i, 0 ≤ π.val i := π.property.1 - have hP_nonneg : ∀ i j, 0 ≤ P i j := hP.1 - have hcoord : ∀ j, ∑ i, ENNReal.ofReal (π.val i) * ENNReal.ofReal (P i j) - = ENNReal.ofReal (π.val j) := by - intro j - have hL : - (∑ i, ENNReal.ofReal (π.val i) * ENNReal.ofReal (P i j)) - = ∑ i, ENNReal.ofReal (π.val i * P i j) := by - refine Finset.sum_congr rfl (fun i _ => ?_) - have hPij := hP_nonneg i j - have hπi := hπ_nonneg i - simp [ENNReal.ofReal_mul, mul_comm, hPij] - have hsum_ofReal : - ∑ i, ENNReal.ofReal (π.val i * P i j) - = ENNReal.ofReal (∑ i, π.val i * P i j) := by - have hnn : ∀ i ∈ (Finset.univ : Finset n), 0 ≤ π.val i * P i j := by - intro i _ - exact mul_nonneg (hπ_nonneg i) (hP_nonneg i j) - simpa using (ENNReal.ofReal_sum_of_nonneg - (s := (Finset.univ : Finset n)) - (f := fun i => π.val i * P i j) hnn).symm - have hstat_j : ∑ i, π.val i * P i j = π.val j := by - have := congrArg (fun v => v j) hstat - simpa [IsStationary, Matrix.mulVec, dotProduct, Matrix.transpose, - Matrix.transpose_apply, Finset.mul_sum, mul_comm, mul_left_comm, mul_assoc] - using this - simpa [hL, hsum_ofReal] using congrArg ENNReal.ofReal hstat_j - have : (μ.bind κ) s - = ∑ j : n, ENNReal.ofReal (π.val j) * s.indicator 1 j := by - rw [hLHS] - simp only [hcoord] - simpa [hRHS] using this - · -- (←) Invariance ⇒ Stationarity - intro hinv - have k_singleton : ∀ i j, κ i {j} = ENNReal.ofReal (P i j) := by - intro i j - exact kernel_eval_singleton hP i j - have mu_singleton : ∀ j, μ {j} = ENNReal.ofReal (π.val j) := by - intro j - have hmeas : MeasurableSet ({j} : Set n) := measurableSet_singleton j - simp [μ, vecToMeasure, hmeas, Measure.dirac_apply'] - rw [Finset.sum_eq_single j] - · simp [Set.indicator_of_mem, Set.mem_singleton_iff] - · intro x _ hx - simp [Set.indicator_of_notMem, hx] - · simp - have hpoint : ∀ j, ∑ i, ENNReal.ofReal (π.val i) * ENNReal.ofReal (P i j) - = ENNReal.ofReal (π.val j) := by - intro j - have hmeas : MeasurableSet ({j} : Set n) := measurableSet_singleton j - have hbind : - (μ.bind κ) {j} = ∑ i : n, ENNReal.ofReal (π.val i) * κ i {j} := by - simp [μ, vecToMeasure, Measure.bind_apply hmeas (Kernel.aemeasurable _), - lintegral_dirac] - have := congrArg (fun m => m {j}) hinv - simpa [hbind, k_singleton, mu_singleton] using this - have hcoord : ∀ j, (∑ i, P i j * π.val i) = π.val j := by - intro j - have hπ_nonneg : ∀ i, 0 ≤ π.val i := π.property.1 - have hP_nonneg : ∀ i, 0 ≤ P i j := fun i => hP.1 i j - have hL : - (∑ i, ENNReal.ofReal (π.val i) * ENNReal.ofReal (P i j)) - = ∑ i, ENNReal.ofReal (π.val i * P i j) := by - refine Finset.sum_congr rfl (fun i _ => ?_) - simp [ENNReal.ofReal_mul, hP_nonneg i, mul_comm] - have hsum_ofReal : - ∑ i, ENNReal.ofReal (π.val i * P i j) - = ENNReal.ofReal (∑ i, π.val i * P i j) := by - have hnn : ∀ i ∈ (Finset.univ : Finset n), 0 ≤ π.val i * P i j := by - intro i _ - exact mul_nonneg (hπ_nonneg i) (hP_nonneg i) - simpa using (ENNReal.ofReal_sum_of_nonneg - (s := (Finset.univ : Finset n)) - (f := fun i => π.val i * P i j) hnn).symm - have := hpoint j - have : ENNReal.ofReal (∑ i, π.val i * P i j) = ENNReal.ofReal (π.val j) := by - simpa [hL, hsum_ofReal] using this - have h_sum_nonneg : 0 ≤ ∑ i, π.val i * P i j := by - apply Finset.sum_nonneg - intro i _ - exact mul_nonneg (hπ_nonneg i) (hP_nonneg i) - have h_π_nonneg_j : 0 ≤ π.val j := hπ_nonneg j - have : ∑ i, π.val i * P i j = π.val j := - Eq.symm - ((fun {p q} hp hq ↦ (ENNReal.ofReal_eq_ofReal_iff hp hq).mp) (hπ_nonneg j) h_sum_nonneg - (id (Eq.symm this))) - simpa [mul_comm (π.val _) (P _ j)] using this - ext j - simpa [IsStationary, Matrix.mulVec, dotProduct, Matrix.transpose, - Matrix.transpose_apply] using hcoord j - -/-! -### (A) Finite-state existence/uniqueness of invariant measure - -We expose the Perron–Frobenius existence/uniqueness theorem (proved in `MCMC.Finite.Core` for the -simplex) as a statement about `Kernel.Invariant` for the associated kernel on the finite space. --/ - -theorem exists_unique_invariant_measure_of_irreducible - [Nonempty n] {P : Matrix n n ℝ} (hP : IsStochastic P) (h_irred : Matrix.IsIrreducible P) : - ∃! (π : stdSimplex ℝ n), - Kernel.Invariant (matrixToKernel P hP) (vecToMeasure π) := by - classical - obtain ⟨π, hπ_stat, hπ_unique⟩ := - exists_unique_stationary_distribution_of_irreducible (n := n) (P := P) hP h_irred - refine ⟨π, (isStationary_iff_invariant (n := n) (P := P) (π := π) hP).1 hπ_stat, ?_⟩ - intro π' hπ'_inv - have hπ'_stat : IsStationary P π' := - (isStationary_iff_invariant (n := n) (P := P) (π := π') hP).2 hπ'_inv - exact hπ_unique π' hπ'_stat - -/-- Matrix reversibility is equivalent to kernel reversibility -/ -theorem isReversible_iff_kernel_reversible (P : Matrix n n ℝ) (π : stdSimplex ℝ n) - (hP : IsStochastic P) : - IsReversible P π ↔ - Kernel.IsReversible (matrixToKernel P hP) (vecToMeasure π) := by - set κ := matrixToKernel P hP - set μ := vecToMeasure (n := n) π - constructor - · -- (→) Matrix detailed balance ⇒ Kernel reversibility - intro hrev A B hA hB - have k_on_set : - ∀ i, κ i B = ∑ j : n, ENNReal.ofReal (P i j) * B.indicator 1 j := by - intro i - exact kernel_eval_on_set hP i B hB - have hL0' : - ∫⁻ x, (A.indicator (fun x => κ x B)) x ∂μ - = ∑ i : n, ENNReal.ofReal (π.val i) * - (A.indicator (fun x => κ x B) i) := by - simp [μ, vecToMeasure] - have hL0 : - ∫⁻ x in A, κ x B ∂μ - = ∑ i : n, ENNReal.ofReal (π.val i) * - (A.indicator (fun x => κ x B) i) := by - simpa [lintegral_indicator (μ := μ) (s := A) (f := fun x => κ x B) hA] using hL0' - have hind : - ∀ i, A.indicator (fun x => κ x B) i = κ i B * A.indicator 1 i := by - intro i - by_cases hi : i ∈ A - · simp [Set.indicator_of_mem, hi, mul_comm] - · simp [Set.indicator_of_notMem, hi] - have hL1 : - ∫⁻ x in A, κ x B ∂μ - = ∑ i : n, ENNReal.ofReal (π.val i) * κ i B * A.indicator 1 i := by - simpa [hind, mul_comm, mul_left_comm, mul_assoc] using hL0 - have hL2 : - ∫⁻ x in A, κ x B ∂μ - = ∑ i : n, ∑ j : n, - ENNReal.ofReal (π.val i) * ENNReal.ofReal (P i j) - * A.indicator 1 i * B.indicator 1 j := by - have hstep1 : - (∑ i : n, ENNReal.ofReal (π.val i) * κ i B * A.indicator 1 i) - = ∑ i : n, ∑ j : n, - ENNReal.ofReal (π.val i) * - (ENNReal.ofReal (P i j) * B.indicator 1 j) * - A.indicator 1 i := by - refine Finset.sum_congr rfl (fun i _ => ?_) - calc - ENNReal.ofReal (π.val i) * κ i B * A.indicator 1 i - = (ENNReal.ofReal (π.val i) * A.indicator 1 i) * κ i B := by - ac_rfl - _ = (ENNReal.ofReal (π.val i) * A.indicator 1 i) - * ∑ j : n, ENNReal.ofReal (P i j) * B.indicator 1 j := by - simp [k_on_set i] - _ = ∑ j : n, - (ENNReal.ofReal (π.val i) * A.indicator 1 i) - * (ENNReal.ofReal (P i j) * B.indicator 1 j) := by - simp [Finset.mul_sum] - _ = ∑ j : n, - ENNReal.ofReal (π.val i) - * (ENNReal.ofReal (P i j) * B.indicator 1 j) - * A.indicator 1 i := by - refine Finset.sum_congr rfl (fun j _ => ?_) - simp [mul_comm, mul_left_comm, mul_assoc] - have hstep2 : - (∑ i : n, ∑ j : n, - ENNReal.ofReal (π.val i) * - (ENNReal.ofReal (P i j) * B.indicator 1 j) * - A.indicator 1 i) - = ∑ i : n, ∑ j : n, - ENNReal.ofReal (π.val i) * ENNReal.ofReal (P i j) - * A.indicator 1 i * B.indicator 1 j := by - refine Finset.sum_congr rfl (fun i _ => ?_) - refine Finset.sum_congr rfl (fun j _ => ?_) - simp [mul_comm, mul_left_comm, mul_assoc] - simpa [hL1] using hstep1.trans hstep2 - have hR0' : - ∫⁻ x, (B.indicator (fun x => κ x A)) x ∂μ - = ∑ j : n, ENNReal.ofReal (π.val j) * (B.indicator (fun x => κ x A) j) := by - simp [μ, vecToMeasure] - have hR0 : - ∫⁻ x in B, κ x A ∂μ - = ∑ j : n, ENNReal.ofReal (π.val j) * (B.indicator (fun x => κ x A) j) := by - simpa [lintegral_indicator (μ := μ) (s := B) (f := fun x => κ x A) hB] using hR0' - have hind' : - ∀ j, B.indicator (fun x => κ x A) j = κ j A * B.indicator 1 j := by - intro j - by_cases hj : j ∈ B - · simp [Set.indicator_of_mem, hj, mul_comm] - · simp [Set.indicator_of_notMem, hj] - have hR0'' : - ∫⁻ x in B, κ x A ∂μ - = ∑ j : n, ENNReal.ofReal (π.val j) * κ j A * B.indicator 1 j := by - simpa [hind', mul_comm, mul_left_comm, mul_assoc] using hR0 - have k_on_set' : - ∀ j, κ j A = ∑ i : n, ENNReal.ofReal (P j i) * A.indicator 1 i := by - intro j - exact kernel_eval_on_set hP j A hA - have hR1 : - ∫⁻ x in B, κ x A ∂μ - = ∑ j : n, ∑ i : n, - ENNReal.ofReal (π.val j) * ENNReal.ofReal (P j i) - * B.indicator 1 j * A.indicator 1 i := by - have hstep1 : - (∑ j : n, ENNReal.ofReal (π.val j) * κ j A * B.indicator 1 j) - = ∑ j : n, ∑ i : n, - ENNReal.ofReal (π.val j) * - (ENNReal.ofReal (P j i) * A.indicator 1 i) * - B.indicator 1 j := by - refine Finset.sum_congr rfl (fun j _ => ?_) - calc - ENNReal.ofReal (π.val j) * κ j A * B.indicator 1 j - = (ENNReal.ofReal (π.val j) * B.indicator 1 j) * κ j A := by - ac_rfl - _ = (ENNReal.ofReal (π.val j) * B.indicator 1 j) - * ∑ i : n, ENNReal.ofReal (P j i) * A.indicator 1 i := by - simp [k_on_set' j] - _ = ∑ i : n, - (ENNReal.ofReal (π.val j) * B.indicator 1 j) - * (ENNReal.ofReal (P j i) * A.indicator 1 i) := by - simp [Finset.mul_sum] - _ = ∑ i : n, - ENNReal.ofReal (π.val j) - * (ENNReal.ofReal (P j i) * A.indicator 1 i) - * B.indicator 1 j := by - refine Finset.sum_congr rfl (fun i _ => ?_) - simp [mul_comm, mul_left_comm, mul_assoc] - have hstep2 : - (∑ j : n, ∑ i : n, - ENNReal.ofReal (π.val j) * - (ENNReal.ofReal (P j i) * A.indicator 1 i) * - B.indicator 1 j) - = ∑ j : n, ∑ i : n, - ENNReal.ofReal (π.val j) * ENNReal.ofReal (P j i) - * B.indicator 1 j * A.indicator 1 i := by - refine Finset.sum_congr rfl (fun j _ => ?_) - refine Finset.sum_congr rfl (fun i _ => ?_) - simp [mul_comm, mul_left_comm, mul_assoc] - rw [hR0'', hstep1, hstep2] - have hR2 : - ∫⁻ x in B, κ x A ∂μ - = ∑ i : n, ∑ j : n, - ENNReal.ofReal (π.val j) * ENNReal.ofReal (P j i) - * A.indicator 1 i * B.indicator 1 j := by - calc - ∫⁻ x in B, κ x A ∂μ - = ∑ j : n, ∑ i : n, - ENNReal.ofReal (π.val j) * ENNReal.ofReal (P j i) - * B.indicator 1 j * A.indicator 1 i := hR1 - _ = ∑ i : n, ∑ j : n, - ENNReal.ofReal (π.val j) * ENNReal.ofReal (P j i) - * B.indicator 1 j * A.indicator 1 i := by - exact - (Finset.sum_comm - (s := (Finset.univ : Finset n)) - (t := (Finset.univ : Finset n)) - (f := fun j i => - ENNReal.ofReal (π.val j) * ENNReal.ofReal (P j i) - * B.indicator 1 j * A.indicator 1 i)) - _ = ∑ i : n, ∑ j : n, - ENNReal.ofReal (π.val j) * ENNReal.ofReal (P j i) - * A.indicator 1 i * B.indicator 1 j := by - refine Finset.sum_congr rfl (fun i _ => ?_) - refine Finset.sum_congr rfl (fun j _ => ?_) - simp [mul_comm, mul_left_comm, mul_assoc] - have hπ_nonneg : ∀ i, 0 ≤ π.val i := π.property.1 - have hP_nonneg : ∀ i j, 0 ≤ P i j := hP.1 - have hcoeff : - ∀ i j, - ENNReal.ofReal (π.val i) * ENNReal.ofReal (P i j) - = ENNReal.ofReal (π.val j) * ENNReal.ofReal (P j i) := by - intro i j - simpa [ENNReal.ofReal_mul, hπ_nonneg _, hP_nonneg _ _, mul_comm, mul_left_comm, mul_assoc] - using congrArg ENNReal.ofReal (hrev i j) - have : ∫⁻ x in A, κ x B ∂μ - = ∫⁻ x in B, κ x A ∂μ := by - rw [hL2, hR2] - refine Finset.sum_congr rfl (fun i _ => ?_) - refine Finset.sum_congr rfl (fun j _ => ?_) - rw [hcoeff i j] - exact this - · -- (←) Kernel reversibility ⇒ Matrix detailed balance on entries - intro hker - have k_singleton : ∀ i j, κ i {j} = ENNReal.ofReal (P i j) := by - intro i j - exact kernel_eval_singleton hP i j - intro i j - have hAi : MeasurableSet ({i} : Set n) := measurableSet_singleton i - have hBj : MeasurableSet ({j} : Set n) := measurableSet_singleton j - have hsingle := hker hAi hBj - have hL : - ∫⁻ x in ({i} : Set n), κ x {j} ∂μ - = ENNReal.ofReal (π.val i) * ENNReal.ofReal (P i j) := by - classical - have hInt : - ∫⁻ x in ({i} : Set n), κ x {j} ∂μ - = ∑ k : n, ENNReal.ofReal (π.val k) * - (({i} : Set n).indicator (fun x => κ x {j}) k) := by - rw [← lintegral_indicator (μ := μ) (s := ({i} : Set n)) - (f := fun x => κ x {j}) hAi] - simp [μ, vecToMeasure] - have hsum : - ∑ k : n, ENNReal.ofReal (π.val k) * - (({i} : Set n).indicator (fun x => κ x {j}) k) - = ENNReal.ofReal (π.val i) * κ i {j} := by - rw [Finset.sum_eq_single i] - · simp [Set.indicator_of_mem, Set.mem_singleton_iff] - · intro x _ hx - simp [Set.indicator_of_notMem, hx] - · simp - rw [hInt, hsum, k_singleton i j] - have hR : - ∫⁻ x in ({j} : Set n), κ x {i} ∂μ - = ENNReal.ofReal (π.val j) * ENNReal.ofReal (P j i) := by - classical - have hInt : - ∫⁻ x in ({j} : Set n), κ x {i} ∂μ - = ∑ k : n, ENNReal.ofReal (π.val k) * - (({j} : Set n).indicator (fun x => κ x {i}) k) := by - rw [← lintegral_indicator (μ := μ) (s := ({j} : Set n)) - (f := fun x => κ x {i}) hBj] - simp [μ, vecToMeasure] - have hsum : - ∑ k : n, ENNReal.ofReal (π.val k) * - (({j} : Set n).indicator (fun x => κ x {i}) k) - = ENNReal.ofReal (π.val j) * κ j {i} := by - rw [Finset.sum_eq_single j] - · simp [Set.indicator_of_mem, Set.mem_singleton_iff] - · intro k _ hki - have : k ∉ ({j} : Set n) := by simpa [Set.mem_singleton_iff] using hki - simp [Set.indicator_of_notMem, this] - · simp - rw [hInt, hsum, k_singleton j i] - have hπ_nonneg : ∀ x, 0 ≤ π.val x := π.property.1 - have hP_nonneg : ∀ x y, 0 ≤ P x y := hP.1 - have hENN : - ENNReal.ofReal (π.val i) * ENNReal.ofReal (P i j) - = ENNReal.ofReal (π.val j) * ENNReal.ofReal (P j i) := by - rw [← hL, ← hR] - exact hsingle - have hreal : - π.val i * P i j = π.val j * P j i := by - have h₁ : - ENNReal.ofReal (π.val i * P i j) - = ENNReal.ofReal (π.val i) * ENNReal.ofReal (P i j) := by - simp [ENNReal.ofReal_mul, hP_nonneg i j, mul_comm] - have h₂ : - ENNReal.ofReal (π.val j * P j i) - = ENNReal.ofReal (π.val j) * ENNReal.ofReal (P j i) := by - simp [ENNReal.ofReal_mul, hP_nonneg j i, mul_comm] - have : ENNReal.ofReal (π.val i * P i j) - = ENNReal.ofReal (π.val j * P j i) := by - simpa [h₁, h₂] using hENN - have hL_nonneg : 0 ≤ π.val i * P i j := - mul_nonneg (hπ_nonneg i) (hP_nonneg i j) - have hR_nonneg : 0 ≤ π.val j * P j i := - mul_nonneg (hπ_nonneg j) (hP_nonneg j i) - exact (ENNReal.ofReal_eq_ofReal_iff hL_nonneg hR_nonneg).mp this - exact hreal - -/-! A Markov-kernel instance for the matrix-induced kernel -/ -instance matrixToKernel_isMarkov (P : Matrix n n ℝ) (hP : IsStochastic P) : - IsMarkovKernel (matrixToKernel P hP) := by - classical - constructor - intro i - refine ⟨?_⟩ - change - ((∑ j : n, ENNReal.ofReal (P i j) • Measure.dirac j) : Measure n) Set.univ = 1 - have hsum_univ : - ((∑ j : n, ENNReal.ofReal (P i j) • Measure.dirac j) : Measure n) Set.univ - = ∑ j : n, ENNReal.ofReal (P i j) := by - simp - have hsum_ofReal : - ∑ j : n, ENNReal.ofReal (P i j) = ENNReal.ofReal (∑ j, P i j) := by - have hnn : ∀ j ∈ (Finset.univ : Finset n), 0 ≤ P i j := by - intro j _; exact hP.1 i j - simpa using - (ENNReal.ofReal_sum_of_nonneg (s := (Finset.univ : Finset n)) - (f := fun j => P i j) hnn).symm - simp [hsum_univ, hsum_ofReal, hP.2 i] - -theorem IsReversible.is_stationary' {P : Matrix n n ℝ} {π : stdSimplex ℝ n} - (hP : IsStochastic P) (h_rev : IsReversible P π) : - IsStationary P π := by - rw [isStationary_iff_invariant P π hP] - rw [isReversible_iff_kernel_reversible P π hP] at h_rev - haveI : IsMarkovKernel (matrixToKernel P hP) := matrixToKernel_isMarkov P hP - exact h_rev.invariant - - -end MCMC.Finite diff --git a/lean/MCMC/LICENSE.md b/lean/MCMC/LICENSE.md deleted file mode 100644 index 650467f..0000000 --- a/lean/MCMC/LICENSE.md +++ /dev/null @@ -1,7 +0,0 @@ -Copyright 2024 - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/lean/MCMC/PF/Analysis/CstarAlgebra/Classes.lean b/lean/MCMC/PF/Analysis/CstarAlgebra/Classes.lean deleted file mode 100644 index a41d5cc..0000000 --- a/lean/MCMC/PF/Analysis/CstarAlgebra/Classes.lean +++ /dev/null @@ -1,418 +0,0 @@ -import Mathlib.Algebra.BigOperators.Field -import Mathlib.Algebra.EuclideanDomain.Basic -import Mathlib.Algebra.EuclideanDomain.Field -import Mathlib.Analysis.CStarAlgebra.Classes -import MCMC.PF.aux - -open Finset Real Complex Matrix - -namespace Complex - - -variable {z : ℂ} - -/-- The norm of a real number embedded in the complex numbers is its absolute value. -/ -lemma norm_ofReal (r : ℝ) : ‖(r : ℂ)‖ = |r| := by simp - -theorem sq_eq_zero {R : Type*} [MonoidWithZero R] [NoZeroDivisors R] {x : R} : x ^ 2 = 0 ↔ x = 0 := by - rw [pow_two, mul_eq_zero] - exact or_self_iff - -/-- An element of a nonempty set. -/ -lemma Set.mem_of_nonempty {α : Type*} (s : Set α) (h : s.Nonempty) : ∃ x, x ∈ s := h - -/-- -An equality between real numbers implies an equality between their complex embeddings. --/ -lemma ofReal_eq_ofReal {r s : ℝ} : r = s → (r : ℂ) = (s : ℂ) := by - intro h - rw [h] - -variable {ι : Type*} {z : ℂ} - -/-- The square of the absolute value of a complex number is its norm squared. -/ -lemma normSq_eq_abs_sq (z : ℂ) : Complex.normSq z = (norm z) ^ 2 := by - exact Complex.normSq_eq_norm_sq z - -/-- The square of the norm of a complex number is the sum of the squares of its real and imaginary -parts. -/ -lemma norm_sq_eq_re_sq_add_im_sq (z : ℂ) : ‖z‖ ^ 2 = z.re ^ 2 + z.im ^ 2 := by - rw [Complex.sq_norm]; rw [← normSq_add_mul_I] - simp [normSq_apply] - -/-- The norm of the conjugate of a complex number is the same as the norm of the original number. -/ -@[simp] -lemma RCLike.norm_conj {K} [RCLike K] (z : K) : ‖star z‖ = ‖z‖ := by exact norm_star z - -/-- The real part of a sum is the sum of the real parts. -/ -lemma RCLike.re_sum {F : Type*} [RCLike F] {v : ι → F} {s : Finset ι} : - RCLike.re (∑ i ∈ s, v i) = ∑ i ∈ s, RCLike.re (v i) := by exact map_sum RCLike.re v s - -/-- -An equality between a real number `r` and its coercion to the complex numbers `↑r` -is true by definition. --/ -lemma ofReal_eq_coe (r : ℝ) : (r : ℂ) = ↑r := rfl - -/-- The real part of a product of complex numbers is less than or equal to the product of their norms. -This is a consequence of the Cauchy-Schwarz inequality. -/ -lemma re_mul_le_norm (z w : ℂ) : re (z * w) ≤ ‖z‖ * ‖w‖ := by - calc - re (z * w) ≤ ‖z * w‖ := re_le_norm (z * w) - _ = ‖z‖ * ‖w‖ := norm_mul z w - -/-- If a sum of `f i` equals a sum of `g i`, and `f i ≤ g i` for all `i`, then `f i = g i` for all `i`. -/ -lemma eq_of_sum_eq_of_le {s : Finset ι} {f g : ι → ℝ} - (h_le : ∀ i ∈ s, f i ≤ g i) (h_sum_eq : ∑ i ∈ s, f i = ∑ i ∈ s, g i) : - ∀ i ∈ s, f i = g i := by - intro i hi - have h_sum_diff_eq_zero : ∑ j ∈ s, (g j - f j) = 0 := by - rw [Finset.sum_sub_distrib, h_sum_eq, sub_self] - have h_nonneg : ∀ j ∈ s, 0 ≤ g j - f j := fun j hj => sub_nonneg.mpr (h_le j hj) - have h_all_zero : ∀ j ∈ s, g j - f j = 0 := by - exact Finset.sum_eq_zero_iff_of_nonneg h_nonneg |>.mp h_sum_diff_eq_zero - exact (sub_eq_zero.mp (h_all_zero i hi)).symm - -/-- A complex number whose norm equals its real part is a non-negative real number. -/ -lemma eq_re_of_norm_eq (h : ‖z‖ = z.re) : z = z.re := by - have h_re_nonneg : z.re ≥ 0 := by - rw [← h] - exact norm_nonneg z - have : z.im ^ 2 = 0 := by - have h_norm_sq : ‖z‖ ^ 2 = z.re ^ 2 + z.im ^ 2 := norm_sq_eq_re_sq_add_im_sq z - rw [h, sq, sq] at h_norm_sq - linarith - refine Eq.symm ((fun {z w} ↦ Complex.ext_iff.mpr) ?_) - simp_all only [ge_iff_le, ne_eq, OfNat.ofNat_ne_zero, not_false_eq_true, pow_eq_zero_iff, and_self] - -lemma eq_coe_re_of_mul_eq_norm_mul {z w : ℂ} (h : re (z * star w) = ‖z‖ * ‖w‖) : - (z * star w) = ↑(re (z * star w)) := by - have h_re_eq : (z * star w).re = ‖z * star w‖ := by - rw [norm_mul, norm_star, h] - exact eq_re_of_norm_eq (id (Eq.symm h_re_eq)) - -/-- The conjugate of a real number embedded in the complex numbers is the number itself. -/ -lemma star_ofReal (r : ℝ) : star (r : ℂ) = r := by - simp - -/-- The product of a complex number and its conjugate is the square of its norm, -as a real number embedded in the complex plane. -/ -lemma star_mul_self (z : ℂ) : z * star z = ↑(‖z‖ ^ 2) := by - simpa [Matrix.star_eq_conjTranspose, normSq_eq_norm_sq] using mul_conj z - -@[simp] lemma re_ofReal (r : ℝ) : (r : ℂ).re = r := -rfl - -/-- `u = conj z / ‖z‖` satisfies `z * u = ‖z‖`. -/ -lemma unit_of_norm_div_star {z : ℂ} (hz : z ≠ 0) : - let u := star z / (‖z‖ : ℂ); z * u = (‖z‖ : ℂ) := by - intro u - have h₁ : (‖z‖ : ℂ) ≠ 0 := by - simpa using (ofReal_ne_zero.mpr ((norm_ne_zero_iff).2 hz)) - calc - z * u = z * (star z / (‖z‖ : ℂ)) := rfl - _ = (z * star z) / (‖z‖ : ℂ) := by simp [mul_div_assoc] - _ = (↑(‖z‖ ^ 2) : ℂ) / (‖z‖ : ℂ) := by rw [star_mul_self] - _ = ((‖z‖ : ℂ) ^ 2) / (‖z‖ : ℂ) := by simp [pow_two] - _ = (‖z‖ : ℂ) := by - simp [pow_two, h₁] - -/-- -If `c` is a complex number of norm 1, and `c^k = 1` and `c^(k+1) = 1` for some -integer `k ≥ 1`, then `c` must be 1. --/ -lemma eq_one_of_root_of_unity_of_consecutive_powers - {c : ℂ} (k : ℕ) (hk_pos : 1 ≤ k) - (h_ck : c ^ k = 1) (h_ck1 : c ^ (k + 1) = 1) : c = 1 := by - have hc_ne_zero : c ≠ 0 := by - intro hc_zero - have : (1 : ℂ) = 0 := by rw [← h_ck, hc_zero, zero_pow (Nat.ne_zero_of_lt hk_pos)] - exact one_ne_zero this - calc - c = c * 1 := (mul_one c).symm - _ = c * (c^k) := by rw [h_ck] - _ = c^(k+1) := by rw [← pow_succ'] - _ = 1 := h_ck1 - -/-- The square of the norm of a sum is the sum of the real parts of the products of each term -with the conjugate of the sum. -/ -lemma norm_sq_eq_sum_re_mul_star {u : ℂ} {v : ι → ℂ} {s : Finset ι} - (h_eq : u = ∑ i ∈ s, v i) : - ‖u‖ ^ 2 = ∑ i ∈ s, re (v i * star u) := by - calc - ‖u‖ ^ 2 = re (u * star u) := by rw [star_mul_self, re_ofReal] - _ = re ((∑ i ∈ s, v i) * star u) := by rw [h_eq] - _ = re (∑ i ∈ s, (v i * star u)) := by rw [sum_mul] - _ = ∑ i ∈ s, re (v i * star u) := by rw [re_sum] - -/-- If equality holds in the triangle inequality, then for each term `v i`, the equality -`re (v i * star u) = ‖v i‖ * ‖u‖` holds, where `u` is the sum. -/ -lemma re_mul_star_eq_norm_mul_norm_of_triangle_eq {u : ℂ} {v : ι → ℂ} {s : Finset ι} - (h_eq : u = ∑ i ∈ s, v i) (h_sum : ‖u‖ = ∑ i ∈ s, ‖v i‖) : - ∀ i ∈ s, re (v i * star u) = ‖v i‖ * ‖u‖ := by - have h_norm_u_sq : ‖u‖ ^ 2 = ∑ i ∈ s, re (v i * star u) := - norm_sq_eq_sum_re_mul_star h_eq - have h_le : ∀ i ∈ s, re (v i * star u) ≤ ‖v i‖ * ‖u‖ := by - intro i _ - calc re (v i * star u) ≤ ‖v i * star u‖ := re_le_norm _ - _ = ‖v i‖ * ‖star u‖ := by rw [norm_mul] - _ = ‖v i‖ * ‖u‖ := by rw [norm_star] - apply eq_of_sum_eq_of_le h_le - calc - ∑ i ∈ s, re (v i * star u) = ‖u‖ ^ 2 := h_norm_u_sq.symm - _ = (∑ i ∈ s, ‖v i‖) * ‖u‖ := by rw [h_sum, pow_two] - _ = ∑ i ∈ s, (‖v i‖ * ‖u‖) := by rw [sum_mul] - -variable {ι : Type*} (s : Finset ι) {v : ι → ℂ} - -/-- -If `u = ∑ i in s, v i`, `‖u‖ = ∑ i in s, ‖v i‖`, and `u ≠ 0`, then each `v i` -is a **nonnegative real** multiple of `u`. --/ -lemma each_term_is_nonneg_real_multiple_of_sum_of_triangle_eq {u : ℂ} - (h_eq : u = ∑ i ∈ s, v i) - (h_sum : ‖u‖ = ∑ i ∈ s, ‖v i‖) - (h_ne : u ≠ 0) : - ∀ i ∈ s, ∃ k : ℝ, k ≥ 0 ∧ v i = (k : ℂ) * u := by - have aligned := re_mul_star_eq_norm_mul_norm_of_triangle_eq h_eq h_sum - have u_pos : 0 < ‖u‖ := norm_pos_iff.mpr h_ne - intro i hi - by_cases hv : v i = 0 - · use 0; simp [hv] - let k := ‖v i‖ / ‖u‖ - have k_nonneg : 0 ≤ k := div_nonneg (norm_nonneg _) (norm_nonneg _) - use k, k_nonneg - have h : v i * star u = (‖v i‖ * ‖u‖ : ℂ) := by - rw [← ofReal_mul, ← aligned i hi] - exact eq_coe_re_of_mul_eq_norm_mul (aligned i hi) - calc - v i = (v i * star u) * u / (u * star u) := by rw [mul_assoc, mul_comm (star u), mul_div_cancel_right₀ _ ( - (CStarRing.mul_star_self_ne_zero_iff u).mpr h_ne)] - _ = (‖v i‖ * ‖u‖ : ℂ) * u / (‖u‖ ^ 2 : ℂ) := by rw [h, star_mul_self, ofReal_pow] - _ = (k : ℂ) * u := by - rw [ofReal_div, ← ofReal_mul] - simp only [ofReal_mul] - field_simp [norm_ne_zero_iff.mpr h_ne] - -/-- -If `vi` is a non-negative real multiple `k` of a non-zero vector `u`, then `k` is the -ratio of their norms. --/ -lemma coeff_of_aligned_vector {u vi : ℂ} {k : ℝ} - (h_aligned : vi = (k : ℂ) * u) (k_nonneg : k ≥ 0) (u_ne_zero : u ≠ 0) : - k = ‖vi‖ / ‖u‖ := by - have u_norm_ne_zero : ‖u‖ ≠ 0 := norm_ne_zero_iff.mpr u_ne_zero - have h_norm_eq : ‖vi‖ = k * ‖u‖ := by - rw [h_aligned, norm_mul, norm_ofReal, abs_of_nonneg k_nonneg] - by_cases hvi_zero : vi = 0 - · have k_zero : k = 0 := by - rw [h_aligned, mul_eq_zero] at hvi_zero - cases hvi_zero - subst h_aligned - simp_all only [ge_iff_le, ne_eq, norm_eq_zero, not_false_eq_true, ofReal_eq_zero, ofReal_zero, zero_mul, - norm_zero] - rename_i h - subst h h_aligned - simp_all only [ge_iff_le, ne_eq, not_true_eq_false] - simp [k_zero, hvi_zero, norm_zero, zero_div] - · exact eq_div_of_mul_eq u_norm_ne_zero (id (Eq.symm h_norm_eq)) - -lemma sum_of_aligned_vectors_factors {u : ℂ} {v : ι → ℂ} {s : Finset ι} - (h_eq : u = ∑ i ∈ s, v i) - (h_sum : ‖u‖ = ∑ i ∈ s, ‖v i‖) - (h_ne : u ≠ 0) : - ∑ i ∈ s, v i = (∑ i ∈ s, (‖v i‖ / ‖u‖ : ℂ)) * u := by - have h_norm_ne : (‖u‖ : ℝ) ≠ 0 := by - exact norm_ne_zero_iff.mpr h_ne - have h_sum_div : - (∑ i ∈ s, (‖v i‖ / ‖u‖ : ℂ)) = - ((∑ i ∈ s, ‖v i‖) / ‖u‖ : ℂ) := by - have h_real : (∑ i ∈ s, ‖v i‖ / ‖u‖) = - (∑ i ∈ s, ‖v i‖) / ‖u‖ := by exact Eq.symm (sum_div s (fun i ↦ ‖v i‖) ‖u‖) - simpa [ofReal_sum, ofReal_div] using congrArg (fun r : ℝ => (r : ℂ)) h_real - have h_coeff : - ((∑ i ∈ s, ‖v i‖) / ‖u‖ : ℂ) = 1 := by - have h_coeff_real : (∑ i ∈ s, ‖v i‖) / ‖u‖ = (1 : ℝ) := by - have h_eq : (∑ i ∈ s, ‖v i‖) = ‖u‖ := by - simpa using h_sum.symm - simp [h_eq, div_self h_norm_ne] - simpa using congrArg (fun r : ℝ => (r : ℂ)) h_coeff_real - calc - ∑ i ∈ s, v i - = u := by - simpa using h_eq.symm - _ = (1 : ℂ) * u := by simp - _ = ((∑ i ∈ s, ‖v i‖) / ‖u‖ : ℂ) * u := by - simp only [one_mul, ofReal_sum] - subst h_eq - simp_all only [ne_eq, ofReal_sum, one_mul] - _ = (∑ i ∈ s, (‖v i‖ / ‖u‖ : ℂ)) * u := by - simp [h_sum_div] - -/-- If equality holds in the triangle inequality, the sum of the non-negative real multiples is 1.-/ -lemma sum_of_multiples_is_one_of_triangle_eq - {u : ℂ} {v : ι → ℂ} {s : Finset ι} - (_ : u = ∑ i ∈ s, v i) - (h_sum : ‖u‖ = ∑ i ∈ s, ‖v i‖) - (h_ne : u ≠ 0) : - ∑ i ∈ s, (‖v i‖ / ‖u‖) = 1 := by - have h_norm_ne : (‖u‖ : ℝ) ≠ 0 := by - exact norm_ne_zero_iff.mpr h_ne - calc - ∑ i ∈ s, ‖v i‖ / ‖u‖ - = (∑ i ∈ s, ‖v i‖) / ‖u‖ := by - rw [← Finset.sum_div s (fun i => ‖v i‖) ‖u‖] - _ = ‖u‖ / ‖u‖ := by - simp [h_sum] - _ = (1 : ℝ) := by - simp [div_self h_norm_ne] - -/-- 2) If `‖u‖ = ∑ i ∈ s, ‖v i‖` then each `v i` aligns with `u`. -/ -lemma align_each_with_sum {u : ℂ} {v : ι → ℂ} {s : Finset ι} - (h_eq : u = ∑ i ∈ s, v i) (h_sum : ‖u‖ = ∑ i ∈ s, ‖v i‖) (h_ne : u ≠ 0) : - ∀ i ∈ s, (‖u‖ : ℂ) • v i = (‖v i‖ : ℂ) • u := by - have h_norm_ne_zero : ‖u‖ ≠ 0 := by rwa [norm_ne_zero_iff] - intro i hi - have ⟨k, k_nonneg, hk⟩ := - each_term_is_nonneg_real_multiple_of_sum_of_triangle_eq s h_eq h_sum h_ne i hi - have coeff_mul : k * ‖u‖ = ‖v i‖ := by - have hk' : k = ‖v i‖ / ‖u‖ := coeff_of_aligned_vector hk k_nonneg h_ne - calc - k * ‖u‖ = (‖v i‖ / ‖u‖) * ‖u‖ := by simp [hk'] - _ = ‖v i‖ * (‖u‖ / ‖u‖) := by - rw [div_mul_eq_mul_div]; grind - _ = ‖v i‖ := by simp [div_self h_norm_ne_zero] - calc - (‖u‖ : ℂ) • v i - = ↑‖u‖ * v i := by simp [smul_eq_mul] - _ = ↑‖u‖ * (k * u) := by rw [hk] - _ = (k * ↑‖u‖) * u := by ring - _ = ↑(k * ‖u‖) * u := by simp [ofReal_mul] - _ = ↑‖v i‖ * u := by rw [coeff_mul] - _ = (‖v i‖ : ℂ) • u := by simp [smul_eq_mul, mul_comm] - -variable {n : Type*} [Fintype n] -/-- If equality holds in thetriangle inequality for a sum of complex vectors, - then all vectors must point in the same direction. -/ -theorem triangle_equality_iff_aligned {v : n → ℂ} (hv_nonzero : ∀ i, v i ≠ 0) [Nonempty n] : - ‖∑ i, v i‖ = ∑ i, ‖v i‖ ↔ - ∃ (c : ℂ), ‖c‖ = 1 ∧ ∀ i, v i = (‖v i‖ : ℂ) * c := by - constructor - · intro h_eq - let u := ∑ i, v i - have hu_nonzero : u ≠ 0 := by - intro h_u_zero - have h_sum_zero : (∑ i, v i) = 0 := h_u_zero - rw [h_sum_zero, norm_zero, eq_comm] at h_eq - rw [sum_eq_zero_iff_of_nonneg (fun i _ => norm_nonneg (v i))] at h_eq - · obtain ⟨i⟩ := univ_nonempty (α := n) - specialize hv_nonzero i - specialize h_eq i (mem_univ i) - rw [norm_eq_zero] at h_eq - contradiction - let c := u / ↑‖u‖ - use c - have hc_norm_one : ‖c‖ = 1 := by - rw [norm_div, Complex.norm_ofReal, abs_of_nonneg (norm_nonneg _), - div_self (norm_ne_zero_iff.mpr hu_nonzero)] - refine ⟨hc_norm_one, fun i ↦ ?_⟩ - have h_aligned := align_each_with_sum (s := univ) rfl h_eq hu_nonzero i (Finset.mem_univ i) - rw [smul_eq_mul, smul_eq_mul] at h_aligned - calc v i - _ = (↑‖v i‖ * u) / ↑‖u‖ := - eq_div_of_mul_eq (ofReal_ne_zero.mpr (norm_ne_zero_iff.mpr hu_nonzero)) - (by rw [← h_aligned, mul_comm]) - _ = ↑‖v i‖ * (u / ↑‖u‖) := by rw [mul_div_assoc] - _ = ↑‖v i‖ * c := rfl - · rintro ⟨c, hc_norm_one, h_aligned⟩ - calc ‖∑ i, v i‖ - = ‖∑ i, (‖v i‖ : ℂ) * c‖ := by congr; ext i; exact h_aligned i - _ = ‖(∑ i, ↑‖v i‖) * c‖ := by rw [Finset.sum_mul] - _ = ‖∑ i, (‖v i‖ : ℂ)‖ * ‖c‖ := by rw [norm_mul] - _ = ‖(↑(∑ i, ‖v i‖) : ℂ)‖ * ‖c‖ := by rw [ofReal_sum] - _ = |∑ i, ‖v i‖| * ‖c‖ := by rw [Complex.norm_ofReal] - _ = (∑ i, ‖v i‖) * ‖c‖ := by rw [abs_of_nonneg (sum_nonneg (fun i _ => norm_nonneg _))] - _ = (∑ i, ‖v i‖) * 1 := by rw [hc_norm_one] - _ = ∑ i, ‖v i‖ := by rw [mul_one] - -/-- -If `u = ∑ i in s, v i`, `‖u‖ = ∑ i in s, ‖v i‖`, and `u ≠ 0`, then each `v i` -is aligned with `u`. --/ -lemma aligned_of_triangle_eq {u : ℂ} {v : ι → ℂ} {s : Finset ι} - (h_eq : u = ∑ i ∈ s, v i) (h_sum : ‖u‖ = ∑ i ∈ s, ‖v i‖) (h_ne : u ≠ 0) : - ∀ i ∈ s, v i ≠ 0 → v i / ↑‖v i‖ = u / ↑‖u‖ := by - intro i hi hvi_ne_zero - have hu_norm_ne_zero : ‖u‖ ≠ 0 := norm_ne_zero_iff.mpr h_ne - have hvi_norm_ne_zero : ‖v i‖ ≠ 0 := norm_ne_zero_iff.mpr hvi_ne_zero - have h_aligned := align_each_with_sum h_eq h_sum h_ne i hi - rw [smul_eq_mul, smul_eq_mul] at h_aligned - rw [mul_comm] at h_aligned - field_simp [h_aligned, hu_norm_ne_zero, hvi_norm_ne_zero] - assumption - -/-- -If a complex number `z` is a positive real multiple of another complex number `w`, -then they are aligned (i.e., have the same phase). --/ -lemma aligned_of_mul_of_real_pos - {z w : ℂ} {c : ℝ} - (hc_pos : 0 < c) - (h : z = (c : ℂ) * w) - (hw_ne_zero : w ≠ 0) : - z / ↑‖z‖ = w / ↑‖w‖ := by - have hz_ne_zero : z ≠ 0 := by - rw [h, mul_ne_zero_iff] - exact ⟨ofReal_ne_zero.mpr hc_pos.ne', hw_ne_zero⟩ - have hz_norm : ‖z‖ = c * ‖w‖ := by - simp [h, abs_of_pos hc_pos] - have hz_normC : (↑‖z‖ : ℂ) = (c : ℂ) * (↑‖w‖ : ℂ) := by - simpa [ofReal_mul] using congrArg (fun t : ℝ => (t : ℂ)) hz_norm - have hnormw_neC : (↑‖w‖ : ℂ) ≠ 0 := ofReal_ne_zero.mpr (norm_ne_zero_iff.mpr hw_ne_zero) - have hnormz_neC : (↑‖z‖ : ℂ) ≠ 0 := ofReal_ne_zero.mpr (norm_ne_zero_iff.mpr hz_ne_zero) - have hcross : z * (↑‖w‖ : ℂ) = w * (↑‖z‖ : ℂ) := by - calc - z * (↑‖w‖ : ℂ) - = ((c : ℂ) * w) * ↑‖w‖ := by simp [h] - _ = (c : ℂ) * (w * ↑‖w‖) := by - simp [mul_assoc] - _ = w * ((c : ℂ) * ↑‖w‖) := by - calc - (c : ℂ) * (w * ↑‖w‖) - = ((c : ℂ) * w) * ↑‖w‖ := by - simpa using (mul_assoc (c : ℂ) w (↑‖w‖ : ℂ)).symm - _ = (w * (c : ℂ)) * ↑‖w‖ := by - simp [mul_comm] - _ = w * ((c : ℂ) * ↑‖w‖) := by - simp [mul_assoc] - _ = w * (↑‖z‖ : ℂ) := by - simp [hz_normC] - exact (div_eq_div_iff hnormz_neC hnormw_neC).2 hcross - -/-- -If `z = λw` for a positive real scalar `λ`, then `z` and `w` are aligned. --/ -lemma aligned_of_eigenvalue {z w : ℂ} {lam : ℝ} - (h_rel : z = (lam : ℂ) * w) (h_lam_pos : 0 < lam) (h_w_ne_zero : w ≠ 0) : - z / ↑‖z‖ = w / ↑‖w‖ := by - exact Complex.aligned_of_mul_of_real_pos h_lam_pos h_rel h_w_ne_zero - -/-- -If `u = ∑ i in s, v i`, `‖u‖ = ∑ i in s, ‖v i‖`, and `u ≠ 0`, then each `v i` -is aligned with `u`. --/ -lemma aligned_of_triangle_eq' {u : ℂ} {v : ι → ℂ} {s : Finset ι} - (h_eq : u = ∑ i ∈ s, v i) (h_sum : ‖u‖ = ∑ i ∈ s, ‖v i‖) (h_ne : u ≠ 0) : - ∀ i ∈ s, v i ≠ 0 → v i / ↑‖v i‖ = u / ↑‖u‖ := by - intro i hi hvi_ne_zero - have hu_norm_ne_zero : ‖u‖ ≠ 0 := norm_ne_zero_iff.mpr h_ne - have hvi_norm_ne_zero : ‖v i‖ ≠ 0 := norm_ne_zero_iff.mpr hvi_ne_zero - have h_aligned := align_each_with_sum h_eq h_sum h_ne i hi - rw [smul_eq_mul, smul_eq_mul] at h_aligned - rw [mul_comm] at h_aligned - field_simp [h_aligned, hu_norm_ne_zero, hvi_norm_ne_zero] - assumption - - - -end Complex diff --git a/lean/MCMC/PF/Combinatorics/Quiver/Cyclic.lean b/lean/MCMC/PF/Combinatorics/Quiver/Cyclic.lean deleted file mode 100644 index 6769510..0000000 --- a/lean/MCMC/PF/Combinatorics/Quiver/Cyclic.lean +++ /dev/null @@ -1,287 +0,0 @@ -import Mathlib.LinearAlgebra.Matrix.Irreducible.Defs -import Mathlib.Data.Nat.Find -import Mathlib.Data.Nat.ModEq - -namespace Quiver -variable {V : Type*} [Quiver V] - -/-! -# Periodicity and Aperiodicity in Quivers - -This section defines the concepts of periodicity, the index of imprimitivity, -and aperiodicity for strongly connected quivers, which are essential for understanding -the cyclic structure of irreducible matrices. --/ - --- Definition for the set of lengths of cycles at a vertex i. --- We require positive length for cycles relevant to periodicity. -def CycleLengths (i : V) : Set ℕ := {k | k > 0 ∧ ∃ p : Path i i, p.length = k} - -/-- The set of common divisors of all cycle lengths at `i`. (If there are no cycles this is all - of `ℕ`, so the least element is `0`.) -/ -def commonDivisorsSet (i : V) : Set ℕ := {d | ∀ k ∈ CycleLengths i, d ∣ k} - -lemma one_mem_commonDivisorsSet (i : V) : - 1 ∈ commonDivisorsSet i := by - intro k hk; exact one_dvd _ - -lemma commonDivisorsSet_nonempty (i : V) : (commonDivisorsSet i).Nonempty := - ⟨1, one_mem_commonDivisorsSet i⟩ - -/-- The period of a vertex `i`: the least common divisor of all cycle lengths at `i`. - -If there are no cycles, this is `0` (since then `commonDivisorsSet i = Set.univ`). -/ -noncomputable def period (i : V) : ℕ := -by - classical - letI : DecidablePred fun d : ℕ => d ∈ commonDivisorsSet i := Classical.decPred _ - exact Nat.find (commonDivisorsSet_nonempty (i := i)) - -/-- `period i` is the least element of the set of common divisors of all cycle lengths at `i`. -/ -lemma period_min (i : V) : - period i ∈ commonDivisorsSet i ∧ - ∀ m ∈ commonDivisorsSet i, period i ≤ m := by - classical - refine ⟨?mem, ?least⟩ - · simpa [period] using (Nat.find_spec (commonDivisorsSet_nonempty (i := i))) - · intro m hm - simpa [period] using (Nat.find_min' (commonDivisorsSet_nonempty (i := i)) hm) - -/-- Basic characterization of `period`: divisibility plus minimality. - TODO: it may be needed a more rigorous characterization-/ -lemma period_spec (i : V) : - (∀ k ∈ CycleLengths i, period i ∣ k) ∧ - (∀ m, (∀ k ∈ CycleLengths i, m ∣ k) → period i ≤ m) := by - classical - have h := period_min i - refine ⟨?div, ?min⟩ - · intro k hk - exact h.1 k hk - · intro m hm - exact h.2 m hm - -lemma period_mem_commonDivisorsSet (i : V) : - period i ∈ commonDivisorsSet i := - (period_min i).1 - -lemma period_le_of_commonDivisor (i : V) {m : ℕ} - (hm : ∀ k ∈ CycleLengths i, m ∣ k) : - period i ≤ m := - (period_spec i).2 _ hm - -lemma divides_cycle_length {i : V} {k : ℕ} - (hk : k ∈ CycleLengths i) : - period i ∣ k := - (period_spec i).1 _ hk - --- The period divides every cycle length (corollary of `period_spec`). -lemma period_divides_cycle_lengths (i : V) : - ∀ {k}, k ∈ CycleLengths i → period i ∣ k := by - intro k hk - exact (period_spec i).1 k hk - --- If the set of cycle lengths is non-empty, the period is positive. -lemma period_pos_of_nonempty_cycles (i : V) (h_nonempty : (CycleLengths i).Nonempty) : - period i > 0 := by - rcases h_nonempty with ⟨k, hk⟩ - have hk_pos : k > 0 := hk.1 - have hdiv : period i ∣ k := (period_spec i).1 k hk - have hk_ne_zero : k ≠ 0 := (Nat.pos_iff_ne_zero.mp hk_pos) - rcases hdiv with ⟨t, ht⟩ - have hper_ne_zero : period i ≠ 0 := by - intro hzero - have : k = 0 := by simpa [hzero] using ht - exact hk_ne_zero this - exact Nat.pos_of_ne_zero hper_ne_zero - -/-- -**Theorem: In a strongly connected quiver, the period is the same for all vertices**. --/ -theorem period_constant_of_strongly_connected (h_sc : Quiver.IsSStronglyConnected V) : - ∀ i j : V, period i = period j := by - intro i j - classical - rcases h_sc i j with ⟨p, hp_pos⟩ - rcases h_sc j i with ⟨q, hq_pos⟩ - have h_div_j : ∀ k ∈ CycleLengths j, period i ∣ k := by - intro k hk - rcases hk with ⟨hk_pos, ⟨c, hc_len⟩⟩ - let t : ℕ := (p.comp q).length - have ht_pos : 0 < t := by - have : p.length ≤ (p.comp q).length := by - simp [Path.length_comp] - exact lt_of_lt_of_le hp_pos this - have ht_mem : t ∈ CycleLengths i := by - refine ⟨ht_pos, ?_⟩ - refine ⟨p.comp q, rfl⟩ - have h_dvd_t : period i ∣ t := (period_spec i).1 _ ht_mem - let t' : ℕ := ((p.comp c).comp q).length - have ht'_pos : 0 < t' := by - have hle1 : k ≤ k + q.length := Nat.le_add_right _ _ - have hle2 : k + q.length ≤ p.length + (k + q.length) := Nat.le_add_left _ _ - have hle : k ≤ p.length + (k + q.length) := le_trans hle1 hle2 - have : p.length + k + q.length = t' := by - simp [t', Path.length_comp, hc_len, Nat.add_assoc, Nat.add_comm] - have hle' : k ≤ t' := by grind - exact lt_of_lt_of_le hk_pos hle' - have ht'_mem : t' ∈ CycleLengths i := by - refine ⟨ht'_pos, ?_⟩ - refine ⟨(p.comp c).comp q, rfl⟩ - have h_dvd_t' : period i ∣ t' := (period_spec i).1 _ ht'_mem - have : period i ∣ t + k := by - have h_eq : t' = t + k := by - simp [t, t', Path.length_comp, hc_len, Nat.add_assoc, Nat.add_comm] - simpa [h_eq] - using h_dvd_t' - have hk_div : period i ∣ k := (Nat.dvd_add_right h_dvd_t).1 this - exact hk_div - have h_le_ji : period j ≤ period i := period_le_of_commonDivisor j h_div_j - have h_div_i : ∀ k ∈ CycleLengths i, period j ∣ k := by - intro k hk - rcases hk with ⟨hk_pos, ⟨c, hc_len⟩⟩ - let t : ℕ := (q.comp p).length - have ht_pos : 0 < t := by - have : q.length ≤ (q.comp p).length := by - simp [Path.length_comp] - exact lt_of_lt_of_le hq_pos this - have ht_mem : t ∈ CycleLengths j := by - refine ⟨ht_pos, ?_⟩ - refine ⟨q.comp p, rfl⟩ - have h_dvd_t : period j ∣ t := (period_spec j).1 _ ht_mem - let t' : ℕ := ((q.comp c).comp p).length - have ht'_pos : 0 < t' := by - have hle1 : k ≤ k + p.length := Nat.le_add_right _ _ - have hle2 : k + p.length ≤ q.length + (k + p.length) := Nat.le_add_left _ _ - have hle : k ≤ q.length + (k + p.length) := le_trans hle1 hle2 - have : q.length + k + p.length = t' := by - simp [t', Path.length_comp, hc_len, Nat.add_comm, Nat.add_left_comm] - have hle' : k ≤ t' := by grind - exact lt_of_lt_of_le hk_pos hle' - have ht'_mem : t' ∈ CycleLengths j := by - refine ⟨ht'_pos, ?_⟩ - refine ⟨(q.comp c).comp p, rfl⟩ - have h_dvd_t' : period j ∣ t' := (period_spec j).1 _ ht'_mem - have : period j ∣ t + k := by - have h_eq : t' = t + k := by - simp [t, t', Path.length_comp, hc_len, Nat.add_comm, Nat.add_left_comm] - simpa [h_eq] using h_dvd_t' - exact (Nat.dvd_add_right h_dvd_t).1 this - have h_le_ij : period i ≤ period j := period_le_of_commonDivisor i h_div_i - exact le_antisymm h_le_ij h_le_ji - -/-- The index of imprimitivity (h) of a strongly connected quiver, - defined as the common period of its vertices. Requires Fintype and Nonempty - to select an arbitrary vertex. -/ -noncomputable def index_of_imprimitivity [Fintype V] [Nonempty V] (G : Quiver V) : ℕ := by - classical - letI : Quiver V := G - exact period (Classical.arbitrary V) - -/-- A strongly connected quiver is aperiodic if its index of imprimitivity is 1. -/ -def IsAperiodic [Fintype V] [Nonempty V] (G : Quiver V) : Prop := - index_of_imprimitivity G = 1 - -/-! # Cyclic Structure and Frobenius Normal Form -/ - -/-- A cyclic partition of the vertices with period h. - The partition is represented by a map from V to Fin h. - Edges only go from one class to the next one cyclically. - We define the successor within `Fin h` by modular addition of 1 (staying in `Fin h`). -/ -def IsCyclicPartition {h : ℕ} (h_pos : h > 0) (partition : V → Fin h) : Prop := - let succMod : Fin h → Fin h := fun x => ⟨(x.val + 1) % h, Nat.mod_lt _ h_pos⟩ - ∀ i j : V, Nonempty (i ⟶ j) → partition j = succMod (partition i) - -/-- If the right factor of a composed path has positive length, the composed cycle at `i` -belongs to `CycleLengths i`. -/ -lemma mem_CycleLengths_of_comp_right {i v : V} - (p : Path i v) (s : Path v i) (hs_pos : 0 < s.length) : - (p.comp s).length ∈ CycleLengths i := by - have hpos : 0 < (p.comp s).length := by - -- 0 < s.length ≤ p.length + s.length = (p.comp s).length - have hle : s.length ≤ p.length + s.length := by - have := Nat.le_add_left s.length p.length - simp - exact lt_of_lt_of_le hs_pos (by simp [Path.length_comp]) - exact ⟨hpos, ⟨p.comp s, rfl⟩⟩ - -/-- Variant: if we first extend a path by a single edge using `cons` and then compose on the right -with a positive-length path back to the base, we still get a cycle length in `CycleLengths`. -/ -lemma mem_CycleLengths_of_cons_comp_right {i v w : V} - (p : Path i v) (e : v ⟶ w) (s : Path w i) (hs_pos : 0 < s.length) : - (((p.cons e).comp s).length) ∈ CycleLengths i := - mem_CycleLengths_of_comp_right (p := p.cons e) (s := s) hs_pos - -/-- -Theorem: A strongly connected quiver with index of imprimitivity h admits a cyclic partition. --/ -theorem exists_cyclic_partition_of_strongly_connected [Fintype V] [Nonempty V] - (h_sc : Quiver.IsSStronglyConnected V) : - ∀ (h_pos : index_of_imprimitivity (inferInstance : Quiver V) > 0), - ∃ partition : V → Fin (index_of_imprimitivity (inferInstance : Quiver V)), - IsCyclicPartition h_pos partition := by - intro h_pos - classical - let h := index_of_imprimitivity (inferInstance : Quiver V) - change ∃ partition : V → Fin h, IsCyclicPartition h_pos partition - -- we fix a base vertex i₀ compatible with the definition of `index_of_imprimitivity` - let i0 : V := Classical.arbitrary V - -- for each vertex, we choose a positive-length path from i₀ to it - have hpaths : ∀ v : V, ∃ p : Path i0 v, 0 < p.length := fun v => h_sc i0 v - let chosen : ∀ v : V, { p : Path i0 v // 0 < p.length } := - fun v => ⟨Classical.choose (hpaths v), Classical.choose_spec (hpaths v)⟩ - let P : ∀ v : V, Path i0 v := fun v => (chosen v).1 - have hPpos : ∀ v : V, (P v).length > 0 := fun v => (chosen v).2 - -- we define the partition by taking path lengths modulo h - let partition : V → Fin h := fun v => ⟨(P v).length % h, Nat.mod_lt _ h_pos⟩ - refine ⟨partition, ?_⟩ - dsimp [IsCyclicPartition] - intro i j hij - rcases hij with ⟨e⟩ - obtain ⟨s, hs_pos⟩ := h_sc j i0 - have hc1_mem : ((P j).comp s).length ∈ CycleLengths i0 := - mem_CycleLengths_of_comp_right (p := P j) (s := s) hs_pos - have hc2_mem : (((P i).cons e).comp s).length ∈ CycleLengths i0 := - mem_CycleLengths_of_cons_comp_right (p := P i) (e := e) (s := s) hs_pos - have hdvd1 : h ∣ ((P j).comp s).length := by - have : period i0 ∣ ((P j).comp s).length := - (divides_cycle_length (i := i0) (k := ((P j).comp s).length)) hc1_mem - simpa [index_of_imprimitivity, i0] - have hdvd2 : h ∣ (((P i).cons e).comp s).length := by - have : period i0 ∣ (((P i).cons e).comp s).length := - (divides_cycle_length (i := i0) (k := (((P i).cons e).comp s).length)) hc2_mem - simpa [index_of_imprimitivity, i0] - have len_c1 : - ((P j).comp s).length = (P j).length + s.length := by - simp [Path.length_comp] - have len_c2 : - (((P i).cons e).comp s).length = (P i).length + 1 + s.length := by - simp [Path.length_comp, Path.length_cons, Nat.add_assoc] - have hsum_congr : - Nat.ModEq h ((P j).length + s.length) ((P i).length + 1 + s.length) := by - have h1 : Nat.ModEq h ((P j).length + s.length) 0 := by - have : ((P j).length + s.length) % h = 0 := by - simpa [len_c1] using Nat.mod_eq_zero_of_dvd hdvd1 - simpa [Nat.ModEq] using this - have h2 : Nat.ModEq h ((P i).length + 1 + s.length) 0 := by - have : ((P i).length + 1 + s.length) % h = 0 := by - simpa [len_c2] using Nat.mod_eq_zero_of_dvd hdvd2 - simpa [Nat.ModEq] using this - exact h1.trans h2.symm - have h_congr : - Nat.ModEq h (P j).length ((P i).length + 1) := by - dsimp [Nat.add_assoc] - exact Nat.ModEq.add_right_cancel' s.length hsum_congr - let succMod : Fin h → Fin h := fun x => ⟨(x.val + 1) % h, Nat.mod_lt _ h_pos⟩ - apply Fin.ext - simp [partition] - calc - (P j).length % h - = ((P i).length + 1) % h := by - simpa [Nat.ModEq] using h_congr - _ = ((P i).length % h + 1 % h) % h := by - rw [Nat.add_mod] - _ = ((P i).length % h + 1) % h := by - by_cases h1 : h = 1 - all_goals aesop - -end Quiver diff --git a/lean/MCMC/PF/Combinatorics/Quiver/Path.lean b/lean/MCMC/PF/Combinatorics/Quiver/Path.lean deleted file mode 100644 index 792e94d..0000000 --- a/lean/MCMC/PF/Combinatorics/Quiver/Path.lean +++ /dev/null @@ -1,1115 +0,0 @@ -import MCMC.PF.Data.List -import Mathlib.Combinatorics.Quiver.Path.Vertices -import Mathlib.Combinatorics.Quiver.Path.Weight - - -open List - -/-! -# Quiver.Path - -This module provides definitions, theorems, and lemmas for manipulating and -reasoning about paths in a `Quiver` (directed graph). The key concepts and results include: - -## 1. Weights on Paths - - **Definitions:** `weight`, `weightFromVertices` assign values in a monoid/semiring to edges - and extend multiplicatively to paths. - - **Lemmas:** `weight_comp`, `weightFromVertices_comp` (multiplicativity under composition); - `weight_pos`, `weightFromVertices_pos`, `weightFromVertices_nonneg` (positivity/non-negativity results). - -## 2. Path Decomposition and Splitting - - **Theorems/Lemmas:** - - `path_decomposition_first_edge`, `path_decomposition_last_edge`: decompose a path at the first or last edge. - - `exists_decomp_at_length`: split a path so the first component has a specified length. - - `exists_decomp_of_mem_vertices`, `exists_decomp_of_mem_vertices_prop`: split at an arbitrary visited vertex, with a proposition version. - - `split_at_vertex`: decompose a path precisely at the position of a chosen vertex. - -## 3. Boundary and Crossing Edges - - **Theorems:** - - `exists_boundary_edge`, `exists_crossing_edge`: existence of boundary/crossing arrows for paths entering a set. - -## 4. Vertices of a Path - - **Definitions:** `«end»`, `activeVertices`, `vertices`, `activeFinset`. - - **Lemmas:** - - `vertices_length`, `vertices_head?`, `vertices_nonempty`, `vertices_comp`, `start_mem_vertices`. - - Extraction lemmas for head/last and vertex membership. - -## 5. Path Properties and Simplicity - - **Definitions:** - - `isStrictlySimple`: predicate for strictly simple (no repeated vertices except possibly endpoints) paths. - - **Theorems/Lemmas:** - - `isStrictlySimple_of_shortest`: shortest path between two vertices is strictly simple. - - Related characterizations and implications for path minimality and structure. - -## 6. Induced Subquivers and Embeddings - - **Definitions:** `Quiver.inducedQuiver`, `Subquiver.embedding`. - - **Lemma:** `mapPath_embedding_vertices_in_set` (embedded paths remain in the subset). - -## 7. Prefunctor Interaction - - **Lemma:** `Prefunctor.end_map` (compatibility of path endpoint with functorial mapping). - -## 8. Path Replication - - **Definitions:** `replicate`. - - **Lemma:** `length_replicate` (length scales with replication). --/ - -namespace Quiver.Path - -variable {V : Type*} [Quiver V] {R : Type*} - -section Weight - -variable [Monoid R] - -def weightFromVertices (w : V → V → R) : ∀ {i j : V}, Path i j → R := - weight (fun {i j} (_ : i ⟶ j) => w i j) - -lemma weightFromVertices_comp (w : V → V → R) {a b c : V} (p : Path a b) (q : Path b c) : - weightFromVertices w (p.comp q) = weightFromVertices w p * weightFromVertices w q := by - simp only [weightFromVertices, weight_comp] - -end Weight - -section PositiveWeight - -variable [Semiring R] [PartialOrder R] [IsOrderedRing R] [PosMulStrictMono R] [Nontrivial R] - - -end PositiveWeight - -section RealWeight - -lemma weightFromVertices_pos {w : V → V → ℝ} - (hw : ∀ i j : V, 0 < w i j) {i j : V} (p : Path i j) : - 0 < weightFromVertices w p := by - apply weight_pos; intro i j _; exact hw i j - -lemma weightFromVertices_nonneg {w : V → V → ℝ} - (hw : ∀ i j : V, 0 ≤ w i j) {i j : V} (p : Path i j) : - 0 ≤ weightFromVertices w p := by - induction p using Path.rec with - | nil => simp only [weightFromVertices, weight, zero_le_one] - | cons p' e ih => simp only [weightFromVertices, weight]; exact mul_nonneg ih (hw _ _) - -end RealWeight - -section PathDecomposition - -variable {V : Type*} [Quiver V] - -/-- Every non-empty path can be decomposed as an initial path plus a final edge. -/ -lemma path_decomposition_last_edge {a b : V} (p : Path a b) (h : p.length > 0) : - ∃ (c : V) (p' : Path a c) (e : c ⟶ b), p = p'.cons e := by - cases p with | nil => simp at h | cons p' e => exact ⟨_, p', e, rfl⟩ - -/-- Every non-empty path can be decomposed as a first edge plus a remaining path. -/ -lemma path_decomposition_first_edge {a b : V} (p : Path a b) (h : p.length > 0) : - ∃ (c : V) (e : a ⟶ c) (p' : Path c b), p = e.toPath.comp p' ∧ p.length = p'.length + 1 := by - have h_len : p.length = (p.length - 1) + 1 := by omega - obtain ⟨c, e, p', hp', rfl⟩ := Path.eq_toPath_comp_of_length_eq_succ p h_len - use c, e, p'; exact ⟨rfl, by omega⟩ - -end PathDecomposition - -section BoundaryEdges - -variable {V : Type*} [Quiver V] - -lemma cons_eq_comp_toPath {a b c : V} (p : Path a b) (e : b ⟶ c) : - p.cons e = p.comp e.toPath := by - rfl - -/-- A path from a vertex not in `S` to a vertex in `S` must cross the boundary. -/ -theorem exists_boundary_edge {a b : V} (p : Path a b) (S : Set V) - (ha_not_in_S : a ∉ S) (hb_in_S : b ∈ S) : - ∃ (u v : V) (e : u ⟶ v) (p₁ : Path a u) (p₂ : Path v b), - u ∉ S ∧ v ∈ S ∧ p = p₁.comp (e.toPath.comp p₂) := by - induction' h_len : p.length with n ih generalizing a b S ha_not_in_S hb_in_S - · -- Base case n = 0: Path must be nil, so a = b. Contradiction. - have hab : a = b := eq_of_length_zero p h_len - subst hab - exact (ha_not_in_S hb_in_S).elim - · -- Inductive step: Assume true for all paths of length < n+1. - have h_pos : 0 < p.length := by rw[h_len]; simp only [lt_add_iff_pos_left, add_pos_iff, - Nat.lt_one_iff, pos_of_gt, or_true] - obtain ⟨c, p', e, rfl⟩ := path_decomposition_last_edge p h_pos - by_cases hc_in_S : c ∈ S - · -- Case 1: The endpoint of `p'` is already in `S`. - have p'_len : p'.length = n := by exact Nat.succ_inj.mp h_len - obtain ⟨u, v, e_uv, p₁, p₂, hu_not_S, hv_S, hp'⟩ := - ih p' S ha_not_in_S hc_in_S p'_len - refine ⟨u, v, e_uv, p₁, p₂.comp e.toPath, hu_not_S, hv_S, ?_⟩ - rw [cons_eq_comp_toPath, hp', Path.comp_assoc, Path.comp_assoc] - · -- Case 2: The endpoint of `p'` is not in `S`. - refine ⟨c, b, e, p', Path.nil, hc_in_S, hb_in_S, ?_⟩ - simp only [comp_nil] - simp_all only [exists_and_left, length_cons, Nat.add_right_cancel_iff, lt_add_iff_pos_left, add_pos_iff, - Nat.lt_one_iff, pos_of_gt, or_true] - subst h_len - rfl - -/-- A path from a vertex in `S` to a vertex not in `S` must cross the boundary. -/ -theorem exists_boundary_edge_from_set {a b : V} (p : Path a b) (S : Set V) - (ha_in_S : a ∈ S) (hb_not_in_S : b ∉ S) : - ∃ (u v : V) (e : u ⟶ v) (p₁ : Path a u) (p₂ : Path v b), - u ∈ S ∧ v ∉ S ∧ p = p₁.comp (e.toPath.comp p₂) := by - classical - have ha_not_in_compl : a ∉ Sᶜ := by simpa - have hb_in_compl : b ∈ Sᶜ := by simpa - obtain ⟨u, v, e, p₁, p₂, hu_not_in_compl, hv_in_compl, hp⟩ := - exists_boundary_edge p Sᶜ ha_not_in_compl hb_in_compl - refine ⟨u, v, e, p₁, p₂, ?_, ?_, hp⟩ - · simpa using hu_not_in_compl - · simpa using hv_in_compl - -/-- Alternative formulation: there exists an edge crossing the boundary. -/ -theorem exists_crossing_edge {a b : V} (p : Path a b) (S : Set V) - (ha_not_in_S : a ∉ S) (hb_in_S : b ∈ S) : - ∃ (u v : V) (_ : u ⟶ v), u ∉ S ∧ v ∈ S := by - obtain ⟨u, v, e, _, _, hu_not_S, hv_S, _⟩ := exists_boundary_edge p S ha_not_in_S hb_in_S - exact ⟨u, v, e, hu_not_S, hv_S⟩ - -end BoundaryEdges - -end Quiver.Path - -namespace Quiver.Path - -variable {V : Type*} [Quiver V] - -/-- The set of vertices in a path. -/ -def activeVertices {a : V} : ∀ {b : V}, Path a b → Set V - | _, nil => {a} - | _, cons p e => activeVertices p ∪ {«end» (cons p e)} - -@[simp] lemma activeVertices_nil {a : V} : activeVertices (nil : Path a a) = {a} := rfl -@[simp] lemma activeVertices_cons {a b c : V} (p : Path a b) (e : b ⟶ c) : - activeVertices (p.cons e) = activeVertices p ∪ {c} := by simp [activeVertices] - --- Vertices of a path are always non-empty -lemma vertices_nonempty' {V : Type*} [Quiver V] {a b : V} (p : Path a b) : p.vertices.length > 0 := by - induction p with - | nil => simp only [vertices_nil, List.length_cons, List.length_nil, le_refl, Nat.eq_of_le_zero, - zero_add, gt_iff_lt, Nat.lt_one_iff, pos_of_gt] - | cons p' e ih => - rw [vertices_cons] - simp only [List.concat_eq_append, List.length_append, List.length_cons, List.length_nil, - zero_add, gt_iff_lt, lt_add_iff_pos_left, add_pos_iff, ih, Nat.lt_one_iff, pos_of_gt, or_self] - -/-- The set of vertices in a path, excluding the final vertex. -/ -def activeFinset [DecidableEq V] {a b : V} (p : Path a b) : Finset V := - p.vertices.dropLast.toFinset - -def activeVertices' {a : V} {b : V} (p : Path a b) : Set V := - {v | v ∈ p.vertices} - -@[simp] -lemma mem_activeVertices {a b : V} (p : Path a b) (v : V) : - v ∈ p.activeVertices' ↔ v ∈ p.vertices := by - rfl - -/-- The finset of vertices in a path. -/ -def vertexFinset [DecidableEq V] {a b : V} (p : Path a b) : Finset V := - p.vertices.toFinset - -/-- -A vertex `x` is in the `activeFinset` of a path `p` if and only if it is in the `dropLast` of the `vertices` list of `p`. --/ -@[simp] -lemma mem_activeFinset_iff [DecidableEq V] {a b : V} (p : Path a b) {x : V} : - x ∈ activeFinset p ↔ x ∈ p.vertices.dropLast := by - simp only [activeFinset, List.mem_toFinset] - -lemma vertices_nonempty {a : V} {b : V} (p : Path a b) : p.vertices ≠ [] := by - rw [← List.length_pos_iff_ne_nil, vertices_length]; omega - -variable {α : Type*} [DecidableEq α] - -/-- A path from a single arrow. -/ -def List.toPath {a b : V} (e : a ⟶ b) : Path a b := - Path.nil.cons e - -/-- Given vertices lists from a path composition, the prefix path's vertices is a prefix of the full path's vertices -/ -lemma isPrefix_dropLast_of_comp_eq {V : Type*} [Quiver V] {a b c : V} {p : Path a b} {p₁ : Path a c} {p₂ : Path c b} - (h : p.vertices = p₁.vertices.dropLast ++ p₂.vertices) : p₁.vertices.dropLast.IsPrefix p.vertices := by - rw [h] - exact List.prefix_append p₁.vertices.dropLast p₂.vertices - -/-- The head of the vertices list is the start vertex. -/ -@[simp] -lemma vertices_head_eq_start {a b : V} (p : Path a b) : p.vertices.head (vertices_nonempty p) = a := by - induction p with - | nil => simp only [vertices_nil, List.head_cons] - | cons p' _ ih => - simp only [vertices_cons, List.concat_eq_append] - have : p'.vertices ≠ [] := vertices_nonempty p' - simp only [List.head_append_of_ne_nil this] - exact ih - -/-- The last element of the vertices list is the end vertex. -/ -@[simp] -lemma vertices_getLast_eq_end {a b : V} (p : Path a b) : - p.vertices.getLast (vertices_nonempty p) = b := by - exact vertices_getLast p (vertices_nonempty p) - -variable {V : Type*} [Quiver V] - -lemma end_prefix_eq_get_vertices {a b c : V} (p₁ : Path a c) (p₂ : Path c b) : - c = (p₁.comp p₂).vertices.get - ⟨p₁.length, by - simp only [vertices_comp, List.length_append, List.length_dropLast, - vertices_length, add_tsub_cancel_right, lt_add_iff_pos_right, add_pos_iff, - Nat.lt_one_iff, pos_of_gt, or_true]⟩ := by - simp only [List.get_eq_getElem, vertices_comp, List.length_dropLast, vertices_length, - add_tsub_cancel_right, le_refl, List.getElem_append_right, tsub_self, List.getElem_zero, - vertices_head_eq_start] - -lemma mem_vertices_to_active {V : Type*} [Quiver V] - {a b : V} {p : Path a b} {x : V} : - x ∈ p.vertices → x ∈ p.activeVertices := by - intro hx - induction p with - | nil => - aesop - | cons p' e ih => - -- For (p'.cons e), vertices = p'.vertices.concat c and activeVertices = activeVertices p' ∪ {c}. - simp [vertices_cons] at hx - cases hx with - | inl hx_in => - have hx_act : x ∈ p'.activeVertices := ih hx_in - simp [activeVertices_cons, hx_act] - | inr hx_eq => - subst hx_eq - simp [activeVertices_cons] - -end Quiver.Path - -namespace Prefunctor - -open Quiver - -variable {V W : Type*} [Quiver V] [Quiver W] (F : V ⥤q W) - -@[simp] -lemma end_map {a b : V} (p : Path a b) : F.obj (p.end) = (F.mapPath p).end := by - induction p with - | nil => rfl - | cons p' e ih => simp - -end Prefunctor - -open Quiver - -namespace Quiver - -/-- The quiver structure on a subtype is induced by the quiver structure on the original type. - An arrow from `a : S` to `b : S` exists if an arrow from `a.val` to `b.val` exists. -/ -def inducedQuiver {V : Type*} [Quiver V] (S : Set V) : Quiver S := - ⟨fun a b => a.val ⟶ b.val⟩ - -end Quiver -namespace Quiver.Subquiver - -variable {V : Type*} [Quiver V] (S : Set V) - -attribute [local instance] inducedQuiver - -/-- The embedding of an induced subquiver on a set `S` into the original quiver. -/ -@[simps] -def embedding : Prefunctor S V where - obj := Subtype.val - map := id - -/-- The vertices in a path mapped by the embedding are all in the original set S. -/ -@[simp] -lemma mapPath_embedding_vertices_in_set {i j : S} (p : Path i j) : - ∀ v, v ∈ ((embedding S).mapPath p).activeVertices → v ∈ S := by - induction p with - | nil => - intro v hv - simp only [Prefunctor.mapPath_nil, Path.activeVertices_nil, Set.mem_singleton_iff] at hv - subst hv - exact i.property - | cons p' e ih => - intro v hv - simp only [Prefunctor.mapPath_cons, Path.activeVertices_cons, Set.mem_union, - Set.mem_singleton_iff] at hv - cases hv with - | inl h => exact ih v h - | inr h => subst h; simp_all only [embedding_obj, Subtype.coe_prop] - -open Quiver.Path - -/-- -If a path in the original quiver only visits vertices in a set `S`, it can be lifted -to a path in the induced subquiver on `S`. --/ -def lift_path_to_induced {S : Set V} [DecidablePred (· ∈ S)] - {i j : V} (p : Path i j) (hp : ∀ k, k ∈ p.vertices → k ∈ S) : - letI : Quiver S := inducedQuiver S - Path (⟨i, hp i (start_mem_vertices p)⟩ : S) (⟨j, hp j (end_mem_vertices p)⟩ : S) := by - letI : Quiver S := inducedQuiver S - induction p with - | nil => exact Path.nil - | cons p' e ih => - exact Path.cons (ih (fun k hk => hp k ((mem_vertices_cons p' e).mpr (Or.inl hk)))) e - -end Quiver.Subquiver - -namespace Quiver.Path -open Quiver -variable {V : Type*} [Quiver V] - -/-- -Construct a path by repeating a loop `n` times. -`replicate n p` is the path `p.comp(p.comp(...p))` where `p` is composed `n` times. -If `n=0`, it is the nil path. --/ -def replicate (n : ℕ) {a : V} (p : Path a a) : Path a a := - match n with - | 0 => Path.nil - | k + 1 => (replicate k p).comp p - -@[simp] -lemma length_replicate (n : ℕ) {a : V} (p : Path a a) : - (replicate n p).length = n * p.length := by - induction n with - | zero => simp only [replicate, length_nil, zero_mul] - | succ k ih => - simp only [replicate, length_comp, ih, add_comm, Nat.succ_mul] - -variable {V : Type*} [Quiver V] - - - -variable {V : Type*} [Quiver V] - -/-! ### Path splitting utilities -/ - -open List - -/-- Given a path `p : Path a b` and an index `n ≤ p.length`, - we can split `p = p₁.comp p₂` with `p₁.length = n`. -/ -theorem exists_decomp_at_length {a b : V} (p : Path a b) {n : ℕ} (hn : n ≤ p.length) : - ∃ (c : V) (p₁ : Path a c) (p₂ : Path c b), - p = p₁.comp p₂ ∧ p₁.length = n := by - induction p generalizing n with - | nil => - have h : n = 0 := by simp_all only [length_nil, nonpos_iff_eq_zero] - subst h - exact ⟨a, Path.nil, Path.nil, by simp only [comp_nil], rfl⟩ - | cons p' e ih => - rename_i c - rw [length_cons] at hn - rcases (Nat.le_succ_iff).1 hn with h | h - · rcases ih h with ⟨d, p₁, p₂, hp, hl⟩ - refine ⟨d, p₁, p₂.cons e, ?_, hl⟩ - simp only [hp, cons_eq_comp_toPath, comp_assoc] - · subst h - refine ⟨c, p'.cons e, Path.nil, ?_, ?_⟩ - · simp only [cons_eq_comp_toPath, comp_nil] - · simp only [cons_eq_comp_toPath, length_comp, length_toPath, Nat.succ_eq_add_one] - -theorem exists_decomp_of_mem_vertices {a b v : V} (p : Path a b) - (h : v ∈ p.vertices) : ∃ (p₁ : Path a v) (p₂ : Path v b), p = p₁.comp p₂ := by - obtain ⟨l₁, l₂, hv⟩ := List.exists_mem_split h - have h_len : l₁.length ≤ p.length := by - have : p.vertices.length = p.length + 1 := vertices_length p - have : l₁.length < p.vertices.length := by - rw [hv, List.length_append, List.length_cons] - omega - omega - obtain ⟨c, p₁, p₂, hp, hl⟩ := exists_decomp_at_length p h_len - suffices hvc : v = c by - subst hvc - exact ⟨p₁, p₂, hp⟩ - have h_verts : p.vertices = p₁.vertices.dropLast ++ p₂.vertices := by - rw [hp, vertices_comp] - have h_l1_len : l₁.length = p₁.vertices.dropLast.length := by - have : p₁.vertices.length = p₁.length + 1 := vertices_length p₁ - simp only [List.length_dropLast, this, hl, add_tsub_cancel_right] - have h_l1_eq : l₁ = p₁.vertices.dropLast := by - have : l₁ ++ v :: l₂ = p₁.vertices.dropLast ++ p₂.vertices := by - rw [← hv, h_verts] - exact List.append_inj_left this h_l1_len - have h_v_l2 : v :: l₂ = p₂.vertices := by - have : l₁ ++ v :: l₂ = p₁.vertices.dropLast ++ p₂.vertices := by - rw [← hv, h_verts] - rw [h_l1_eq] at this - exact List.append_cancel_left this - have : p₂.vertices.head? = some c := by - cases p₂ with - | nil => simp only [vertices_nil, List.head?_cons] - | cons _ _ => exact vertices_head? _ - rw [← h_v_l2] at this - simp only [List.head?_cons, Option.some.injEq] at this - exact this - -/- -/- Equality of paths implies equality of their start and end vertices. -/ -lemma eq_of_heq {a a' b b' : V} {p : Path a b} {p' : Path a' b'} (h : HEq p p') : - a = a' ∧ b = b' := by - -- derive equalities of start and end vertices using heterogeneous equality - have h₁ : start p = start p' := - Eq.ndrec (motive := fun x => start p = x) rfl (congrArg start h) - have h₂ : «end» p = «end» p' := - Eq.ndrec (motive := fun x => «end» p = x) rfl (congrArg «end» h) - simp [Path.start, Path.end] at h₁ h₂ - exact ⟨h₁, h₂⟩ - -@[simp] -lemma start_eq_of_heq {a a' b b' : V} {p : Path a b} {p' : Path a' b'} (h : HEq p p') : - a = a' := - (eq_of_heq h).1 - -@[simp] -lemma end_eq_of_heq {a a' b b' : V} {p : Path a b} {p' : Path a' b'} (h : HEq p p') : - b = b' := - (eq_of_heq h).2-/ - - -/-- `split_at_vertex` decomposes a path `p` at the vertex sitting in - position `i` of its `vertices` list. -/ -theorem split_at_vertex {a b : V} (p : Path a b) (i : ℕ) - (hi : i < p.vertices.length) : - ∃ (v : V) (p₁ : Path a v) (p₂ : Path v b), - p = p₁.comp p₂ ∧ - p₁.length = i ∧ - v = p.vertices.get ⟨i, hi⟩ := by - have hi_le_len : i ≤ p.length := by - rw [vertices_length] at hi - exact Nat.le_of_lt_succ hi - obtain ⟨v, p₁, p₂, hp, hlen⟩ := exists_decomp_at_length p hi_le_len - subst hp - refine ⟨v, p₁, p₂, rfl, hlen, ?_⟩ - have h_eq := end_prefix_eq_get_vertices p₁ p₂ - simp [hlen] - -end Quiver.Path - -namespace Quiver.Path -open Quiver -variable {V : Type*} [Quiver V] - -/-- A path is simple if it does not visit any vertex more than once, with the possible -exception of the final vertex, which may be the same as the start vertex in a cycle. -/ -def IsSimple' {a b : V} (p : Path a b) : Prop := - p.vertices.dropLast.Nodup ∧ - (a = b → p.vertices.dropLast.length = p.length) ∧ - (a ≠ b → p.end ∉ p.vertices.dropLast) - -lemma isSimple_nil {a : V} : IsSimple' (nil : Path a a) := by - simp only [IsSimple', vertices_nil, dropLast_singleton, nodup_nil, List.length_nil, le_refl, - Nat.eq_of_le_zero, length_nil, imp_self, ne_eq, not_true_eq_false, not_mem_nil, - not_false_eq_true, implies_true, and_self] - -/-- A path is simple if it does not contain any vertex more than once. -This is a strict definition; a cycle `a ⟶ ... ⟶ a` of non-zero length is not simple. -/ -@[simp] -def IsStrictlySimple {a b : V} (p : Path a b) : Prop := p.vertices.Nodup - -lemma isStrictlySimple_nil {a : V} : IsStrictlySimple (nil : Path a a) := by - simp only [IsStrictlySimple, vertices_nil, nodup_cons, not_mem_nil, not_false_eq_true, nodup_nil, - and_self] - -@[simp] -lemma isStrictlySimple_cons [DecidableEq V] {a b c : V} (p : Path a b) (e : b ⟶ c) : - IsStrictlySimple (p.cons e) ↔ IsStrictlySimple p ∧ c ∉ p.vertices := by - simp only [IsStrictlySimple, vertices_cons] - rw [List.nodup_concat]; aesop - -/-- The set of vertices of a simple path has cardinality `p.length + 1`. -/ -lemma card_vertexFinset_of_isStrictlySimple [DecidableEq V] {a b : V} {p : Path a b} (hp : IsStrictlySimple p) : - p.vertexFinset.card = p.length + 1 := by - simp [vertexFinset, List.toFinset_card_of_nodup hp, vertices_length] - -lemma length_lt_card_of_isStrictlySimple [DecidableEq V] [Fintype V] - {a b : V} {p : Path a b} (hp : IsStrictlySimple p) : - p.length < Fintype.card V := by - simpa [card_vertexFinset_of_isStrictlySimple hp, Nat.succ_eq_add_one] using - (Finset.card_le_univ p.vertexFinset) - -/-- If a path is not strictly simple, then there exists a vertex that occurs at least twice. -/ -lemma not_strictly_simple_iff_exists_repeated_vertex [DecidableEq V] {a b : V} {p : Path a b} : - ¬IsStrictlySimple p ↔ ∃ v, v ∈ p.vertices ∧ p.vertices.count v ≥ 2 := by - rw [IsStrictlySimple, List.nodup_iff_not_contains_dup] - push_neg - simp only [List.ContainsDup] - constructor - · rintro ⟨v, hv⟩ - exact ⟨v, List.count_pos_iff.mp (by linarith), hv⟩ - · rintro ⟨v, _, hv⟩ - exact ⟨v, hv⟩ - - -/-- Removing a positive-length cycle from a path gives a strictly shorter path with the same endpoints. -/ -lemma remove_cycle_gives_shorter_path [DecidableEq V] {a v b : V} - {p_prefix : Path a v} {p_cycle : Path v v} {p_rest : Path v b} - (h_cycle_pos : p_cycle.length > 0) : - (p_prefix.comp p_rest).length < (p_prefix.comp (p_cycle.comp p_rest)).length := by - simp only [length_comp] - simp_all only [gt_iff_lt, add_lt_add_iff_left, lt_add_iff_pos_left] - -open List - -/-- If a vertex c appears in path p, c must either be the end vertex or appear in the tail of vertices. -/ -lemma vertex_in_path_cases {a b c : V} (p : Path a b) (h : c ∈ p.vertices) : - c = b ∨ c ∈ p.vertices.dropLast := by - induction p with - | nil => - simp [vertices_nil] at h - subst h - simp - | cons p' e ih => - rename_i b' - simp only [vertices_cons, List.mem_concat] at h - rcases h with h_in_p' | h_eq_b - · -- c is in p'.vertices - specialize ih h_in_p' - rcases ih with h_eq_p'_end | h_in_p'_dropLast - · -- c is the end of p', so it's in p.vertices.dropLast which is p'.vertices - right - simp only [vertices_cons] - rw [List.dropLast_append_singleton (vertices_nonempty' p')] - rw [h_eq_p'_end] - subst h_eq_p'_end - simp_all only - · -- c is in the dropLast of p'.vertices, so it's also in p.vertices.dropLast - right - simp only [vertices_cons] - rw [List.dropLast_append_singleton (vertices_nonempty' p')] - exact h_in_p' - · -- c is the end of p - subst h_eq_b - left - rfl - -/-- If we have a path p from a to b with c ∈ p.vertices, - and c is not the end vertex b, then it appears in a proper prefix of the path. -/ -lemma exists_prefix_with_vertex [DecidableEq V] {a b c : V} (p : Path a b) (h : c ∈ p.vertices) (h_ne : c ≠ b) : - ∃ (p₁ : Path a c) (p₂ : Path c b), p = p₁.comp p₂ := by - have h_cases := vertex_in_path_cases p h - cases h_cases with - | inl h_eq => - contradiction - | inr h_mem_tail => - let i := p.vertices.idxOf c - have hi : i < p.vertices.length := List.idxOf_lt_length_of_mem h - obtain ⟨v, p₁, p₂, h_comp, h_len, h_c_eq⟩ := split_at_vertex p i hi - have hvc : v = c := by - rw [h_c_eq] - exact List.get_idxOf_of_mem h - subst hvc - exact ⟨p₁, p₂, h_comp⟩ - -/-- Split a path at the **last** occurrence of a vertex. -/ -theorem exists_decomp_of_mem_vertices_prop - [DecidableEq V] {a b x : V} (p : Path a b) (hx : x ∈ p.vertices) : - ∃ (p₁ : Path a x) (p₂ : Path x b), - p = p₁.comp p₂ ∧ x ∉ p₂.vertices.tail := by - classical - induction p with - | nil => - have hxa : x = a := by - simpa [vertices_nil, List.mem_singleton] using hx - subst hxa - exact ⟨Path.nil, Path.nil, by simp only [comp_nil], - by simp only [vertices_nil, tail_cons, not_mem_nil, not_false_eq_true]⟩ - | cons pPrev e ih => - have hx' : x ∈ pPrev.vertices ∨ x = (pPrev.cons e).end := by - simpa using (mem_vertices_cons pPrev e).1 hx - -- Case 1 : `x` is the final vertex. - have h_case₁ : - x = (pPrev.cons e).end → - ∃ (p₁ : Path a x) (p₂ : Path x (pPrev.cons e).end), - pPrev.cons e = p₁.comp p₂ ∧ - x ∉ p₂.vertices.tail := by - intro hxe; subst hxe - refine ⟨pPrev.cons e, Path.nil, ?_, ?_⟩ - · exact (comp_nil _).symm - · simp [vertices_nil] - -- Case 2 : `x` occurs in the prefix (and **is not** the final vertex). - have h_case₂ : - x ∈ pPrev.vertices → x ≠ (pPrev.cons e).end → - ∃ (p₁ : Path a x) (p₂ : Path x (pPrev.cons e).end), - pPrev.cons e = p₁.comp p₂ ∧ - x ∉ p₂.vertices.tail := by - intro hxPrev hxe_ne - rcases ih hxPrev with ⟨q₁, q₂, h_prev, h_not_tail⟩ - let q₂' : Path x (pPrev.cons e).end := q₂.comp e.toPath - have h_eq : pPrev.cons e = q₁.comp q₂' := by - dsimp [q₂']; simp only [h_prev, cons_eq_comp_toPath, Path.comp_assoc] - have h_no_tail : x ∉ q₂'.vertices.tail := by - intro hmem - have hmem' : x ∈ (q₂.vertices.dropLast ++ (e.toPath).vertices).tail := by - -- Avoid shape change (e.g. to q₂.vertices ++ [c]) by recording the decomposition first. - have hq₂' : q₂'.vertices = q₂.vertices.dropLast ++ (e.toPath).vertices := by - show (q₂.comp e.toPath).vertices = q₂.vertices.dropLast ++ e.toPath.vertices - exact vertices_comp q₂ e.toPath - rw [← hq₂']; exact hmem - by_cases h_nil : q₂.vertices.dropLast = [] - · have h_tail_toPath : x ∈ (e.toPath).vertices.tail := by - simpa [h_nil] using hmem' - have hx_end : x = (pPrev.comp e.toPath).end := by - have : e.toPath.vertices.tail = [(pPrev.cons e).end] := by - simp only [cons_eq_comp_toPath] - rfl - rw [this] at h_tail_toPath - exact List.mem_singleton.mp h_tail_toPath - exact hxe_ne hx_end - · have h_split := (List.mem_tail_append).1 hmem' - have h_mem_right : - x ∈ (q₂.vertices.dropLast).tail ++ (e.toPath).vertices := by - cases h_split with - | inl h_left => cases (h_nil h_left.1) - | inr h_right => exact h_right.2 - have h_parts := (List.mem_append).1 h_mem_right - cases h_parts with - | inl h_in_dropLast_tail => - have : x ∈ q₂.vertices.tail := - List.mem_of_mem_tail_dropLast h_in_dropLast_tail - exact h_not_tail this - | inr h_in_toPath => - have hx_end : x = (pPrev.comp e.toPath).end := by - have h' : x = pPrev.end ∨ - x = (pPrev.cons e).end := by - have : e.toPath.vertices = [pPrev.end, (pPrev.cons e).end] := by - simp only [cons_eq_comp_toPath] - rfl - rw [this] at h_in_toPath - simpa [List.mem_cons, List.mem_singleton] using h_in_toPath - cases h' with - | inl h_eq_src => - have : x ∈ q₂.vertices.tail := by - have h_q2_len_pos : 0 < q₂.length := by - have h_drop_len_pos : q₂.vertices.dropLast.length > 0 := - List.length_pos_of_ne_nil h_nil - have h_vert_len_ge_2 : q₂.vertices.length ≥ 2 := by - subst h_eq_src h_prev - simp_all - exact h_drop_len_pos - have h_path_len_ge_1 : q₂.length ≥ 1 := by - subst h_eq_src h_prev - simp_all - exact h_path_len_ge_1 - have h_q2_end : q₂.end = pPrev.end := by - have : (q₁.comp q₂).end = pPrev.end := by rw [h_prev] - simpa using this - subst h_eq_src - have h_nonempty : q₂.vertices ≠ [] := by - rw [List.ne_nil_iff_length_pos, vertices_length] - omega - have h_x_is_last : x = q₂.vertices.getLast h_nonempty := by - simp - have h_mem_tail : q₂.vertices.getLast h_nonempty ∈ q₂.vertices.tail := by - have h_len2 : q₂.vertices.length ≥ 2 := by rw [vertices_length]; omega - exact List.getLast_mem_tail h_len2 - rw [← h_x_is_last] at h_mem_tail - exact h_mem_tail - contradiction - | inr h_eq_end => exact h_eq_end - exact hxe_ne hx_end - exact ⟨q₁, q₂', h_eq, h_no_tail⟩ - cases hx' with - | inl h_in_prefix => - by_cases h_eq_end : x = (pPrev.cons e).end - · rcases h_case₁ h_eq_end with ⟨p₁, p₂, h_eq, h_tail⟩ - exact ⟨p₁, p₂, h_eq, h_tail⟩ - · rcases h_case₂ h_in_prefix h_eq_end with ⟨p₁, p₂, h_eq, h_tail⟩ - exact ⟨p₁, p₂, h_eq, h_tail⟩ - | inr h_eq_end => - rcases h_case₁ h_eq_end with ⟨p₁, p₂, h_eq, h_tail⟩ - exact ⟨p₁, p₂, h_eq, h_tail⟩ - -theorem isStrictlySimple_of_shortest [DecidableEq V] - {a b : V} (p : Path a b) - (h_min : ∀ q : Path a b, p.length ≤ q.length) : - IsStrictlySimple p := by - classical - by_contra h_dup - obtain ⟨v, hv_in, hv_ge₂⟩ := - not_strictly_simple_iff_exists_repeated_vertex.mp h_dup - obtain ⟨p₁, p₂, hp, hv_not_tail⟩ := - exists_decomp_of_mem_vertices_prop p hv_in - have hv_in_p1_dropLast : v ∈ p₁.vertices.dropLast := by - have h_count_p : p.vertices.count v = - (p₁.vertices.dropLast ++ p₂.vertices).count v := by - rw [← vertices_comp, ← hp] - rw [h_count_p] at hv_ge₂ - rw [List.count_append] at hv_ge₂ - have h_count_p2 : p₂.vertices.count v = 1 := by - have h_head : p₂.vertices.head? = some v := by - cases p₂ with - | nil => simp [vertices_nil] - | cons p' e => - simp only [vertices_cons] - have h := vertices_head? p' - simp [List.concat_eq_append] - have hne : p₂.vertices ≠ [] := by - apply List.ne_nil_of_head?_eq_some h_head - have h_shape : p₂.vertices = v :: p₂.vertices.tail := by - have h_first : p₂.vertices.head? = some v := h_head - have h_decomp : ∃ h t, p₂.vertices = h :: t := List.exists_cons_of_ne_nil hne - rcases h_decomp with ⟨h, t, heq⟩ - rw [heq] - have h_eq : h = v := by - rw [heq] at h_first - simp only [List.head?_cons] at h_first - exact Option.some.inj h_first - rw [h_eq] - have t_eq : t = p₂.vertices.tail := by - rw [heq, List.tail_cons] - rw [t_eq] - exact rfl - rw [h_shape] - have h_not_in_tail : v ∉ p₂.vertices.tail := hv_not_tail - simp only [List.count_cons_self, List.count_eq_zero_of_not_mem h_not_in_tail] - have : (p₁.vertices.dropLast).count v ≥ 1 := by - have : 1 + (p₁.vertices.dropLast).count v ≥ 2 := by - rw [add_comm] - simpa [h_count_p2] using hv_ge₂ - linarith - exact (List.count_pos_iff).1 (lt_of_lt_of_le Nat.zero_lt_one this) - obtain ⟨q, c, h_p1_split⟩ := - exists_decomp_of_mem_vertices p₁ - (List.mem_of_mem_dropLast hv_in_p1_dropLast) - have hc_pos : c.length > 0 := by - by_cases h_len_zero : c.length = 0 - · have hc_nil : c = Path.nil := by - apply (length_eq_zero_iff c).mp h_len_zero - have : p₁ = q := by - simpa [hc_nil, comp_nil] using h_p1_split - have h_mem : v ∈ q.vertices.dropLast := by - simpa [this] using hv_in_p1_dropLast - have h_last : v = q.vertices.getLast (vertices_nonempty q) := by simp - let i := q.vertices.idxOf v - have hi_verts : i < q.vertices.length := by - rw [List.idxOf_lt_length_iff] - exact List.mem_of_mem_dropLast h_mem - have hi : i < q.length := by - have h_idx_lt_dropLast_len : i < q.vertices.dropLast.length := by - have h_lt : List.idxOf v q.vertices.dropLast < q.vertices.dropLast.length := - List.idxOf_lt_length_of_mem h_mem - have h_prefix : (q.vertices.dropLast).IsPrefix q.vertices := by - have h_split := List.dropLast_append_getLast (vertices_nonempty q) - have : (q.vertices.dropLast).IsPrefix (q.vertices.dropLast ++ - [q.vertices.getLast (vertices_nonempty q)]) := - List.prefix_append _ _ - exact dropLast_prefix q.vertices - have h_eq : List.idxOf v q.vertices = List.idxOf v q.vertices.dropLast := - idxOf_eq_idxOf_of_isPrefix h_prefix h_mem - simpa [i, h_eq] using h_lt - rw [List.length_dropLast, vertices_length] at h_idx_lt_dropLast_len - exact h_idx_lt_dropLast_len - obtain ⟨split_v, shorter_q, _, h_comp_q, h_len_shorter, h_v_eq⟩ := split_at_vertex q i hi_verts - have h_split_v_eq_v : split_v = v := by - rw [h_v_eq] - exact List.get_idxOf_of_mem (List.mem_of_mem_dropLast h_mem) - subst h_split_v_eq_v - let shorter_path := shorter_q.comp p₂ - have h_shorter_total : shorter_path.length < p.length := by - have h_p1_eq : p₁ = q := by simpa [hc_nil, comp_nil] using h_p1_split - have h_q_len : q.length > shorter_q.length := by - rw [h_len_shorter] - exact hi - have h_decomp : p = p₁.comp p₂ := hp - rw [h_p1_eq] at h_decomp - have h_len : p.length = q.length + p₂.length := by - rw [h_decomp, length_comp] - have h_shorter_len : shorter_path.length = shorter_q.length + p₂.length := by - rw [length_comp] - rw [h_len, h_shorter_len] - exact Nat.add_lt_add_right h_q_len p₂.length - exact absurd (h_min shorter_path) (not_le.mpr h_shorter_total) - · exact Nat.pos_of_ne_zero h_len_zero - let p' : Path a b := q.comp p₂ - have h_shorter : p'.length < p.length := by - have h_len_p : p.length = q.length + c.length + p₂.length := by - rw [hp, h_p1_split] - rw [length_comp, length_comp] - have h_len_p' : p'.length = q.length + p₂.length := by - rw [length_comp] - rw [h_len_p, h_len_p'] - have : 0 < c.length := hc_pos - apply Nat.add_lt_add_of_lt_of_le - · exact Nat.lt_add_of_pos_right this - · exact le_refl p₂.length - exact (not_le.mpr h_shorter) (h_min p') - -/-- The length of a strictly simple path is at most one less than the number of vertices in the graph. -/ -lemma length_le_card_minus_one_of_isSimple {n : Type*} [Fintype n] [DecidableEq n] [Quiver n] {a b : n} (p : Path a b) (hp : p.IsStrictlySimple) : - p.length ≤ Fintype.card n - 1 := by - have h_card_verts : p.vertexFinset.card = p.length + 1 := by - exact card_vertexFinset_of_isStrictlySimple hp - have h_card_le_univ : p.vertexFinset.card ≤ Fintype.card n := by - exact Finset.card_le_univ p.vertexFinset - rw [h_card_verts] at h_card_le_univ - exact Nat.le_sub_one_of_lt h_card_le_univ - -/-! ### Cycles -/ - -/-- A path is simple if it does not visit any vertex more than once, with the possible -exception of the final vertex, which may be the same as the start vertex in a cycle. -/ -def IsSimple {a b : V} (p : Path a b) : Prop := - p.vertices.dropLast.Nodup - -/-- A cycle is a non-trivial path from a vertex back to itself that does not repeat vertices, -except for the start/end vertex. -/ -@[nolint unusedArguments] -def IsCycle {a : V} (p : Path a a) : Prop := - p.length > 0 ∧ IsSimple p - -lemma isSimple_of_isStrictlySimple {a b : V} {p : Path a b} (h : IsStrictlySimple p) : IsSimple p := by - unfold IsSimple IsStrictlySimple at * - simpa using h.sublist (List.dropLast_sublist (l := p.vertices)) - -/-! -# Acyclic Quivers - -This module defines acyclic quivers and proves that a quiver is acyclic if and only if it -contains no simple cycles. - -## Main definitions - -* `Quiver.IsAcyclic`: A typeclass indicating that a quiver has no non-trivial paths from a - vertex to itself. -* `Quiver.IsCycle`: A predicate on a path indicating it is a simple cycle. - -## Main results - -* `isAcyclic_iff_no_cycles`: A quiver is acyclic iff it contains no simple cycles. - --/ - -section Acyclic - -variable {V : Type*} [Quiver V] - -/-- A quiver is acyclic if there are no non-trivial paths from a vertex to itself. -/ -class IsAcyclic (V : Type*) [Quiver V] : Prop where - /-- The defining property of an acyclic quiver: any path from a vertex `a` to itself - must have length zero. -/ - acyclic : ∀ (a : V) (p : Path a a), p.length = 0 - --- Expose the lemma in a more convenient form. -lemma IsAcyclic.path_eq_nil {V : Type*} [Quiver V] [IsAcyclic V] {a : V} (p : Path a a) : p = Path.nil := - Path.eq_nil_of_length_zero p (IsAcyclic.acyclic a p) - -lemma isAcyclic_iff_length_eq_zero : - IsAcyclic V ↔ ∀ {a : V} (p : Path a a), p.length = 0 := by - constructor - · intro h; exact fun {a} p ↦ IsAcyclic.acyclic a p - · intro h; exact { acyclic := fun a p ↦ h p } - -/-- If a quiver is acyclic, then it contains no simple cycles. -/ -lemma isAcyclic_of_no_cycles [DecidableEq V] : - IsAcyclic V → ∀ {a : V} (p : Path a a), ¬IsCycle p := by - intro h_acyclic a p h_cycle - have h_pos : p.length > 0 := h_cycle.1 - have h_zero : p.length = 0 := IsAcyclic.acyclic a p - exact (Nat.not_lt.mpr (le_of_eq h_zero)) h_pos - -/-- There exists a positive loop shorter than p if q is such a loop. -/ -lemma exists_positive_loop_shorter_than_p [DecidableEq V] {a : V} {p : Path a a} (q : Path a a) - (h_q_pos : q.length > 0) (h_q_shorter : q.length < p.length) : - ∃ n, ∃ (r : Path a a), r.length = n ∧ r.length > 0 ∧ r.length < p.length := by - exact ⟨q.length, q, rfl, h_q_pos, h_q_shorter⟩ - -open Classical - - -/-- For any two positive loops shorter than p, their minimum length equals - the minimum length among all positive loops shorter than p, or there exists - an even shorter loop. -/ -lemma min_length_among_shorter_loops {a : V} {p : Path a a} (q r : Path a a) - (h_q_pos : q.length > 0) (h_r_pos : r.length > 0) - (h_q_shorter : q.length < p.length) (h_r_shorter : r.length < p.length) : - min q.length r.length = Nat.find (exists_positive_loop_shorter_than_p q h_q_pos h_q_shorter) ∨ - ∃ (s : Path a a), s.length = Nat.find (exists_positive_loop_shorter_than_p q h_q_pos h_q_shorter) ∧ - s.length > 0 ∧ s.length < p.length ∧ s.length < min q.length r.length := by - let min_len := Nat.find (exists_positive_loop_shorter_than_p q h_q_pos h_q_shorter) - have h_min_spec := Nat.find_spec (exists_positive_loop_shorter_than_p q h_q_pos h_q_shorter) - obtain ⟨s, hs_eq, hs_pos, hs_shorter⟩ := h_min_spec - by_cases h : min_len < min q.length r.length - · right - exact ⟨s, hs_eq, hs_pos, hs_shorter, by rwa [hs_eq]⟩ - · left - push_neg at h - have h_min_le_q : min_len ≤ q.length := - Nat.find_min' (exists_positive_loop_shorter_than_p q h_q_pos h_q_shorter) - ⟨q, rfl, h_q_pos, h_q_shorter⟩ - have h_min_le_r : min_len ≤ r.length := - Nat.find_min' (exists_positive_loop_shorter_than_p q h_q_pos h_q_shorter) - ⟨r, rfl, h_r_pos, h_r_shorter⟩ - have h_min_le_min : min_len ≤ min q.length r.length := by - apply le_min - · exact h_min_le_q - · exact h_min_le_r - exact le_antisymm h h_min_le_min - -/-- -Among all positive-length loops shorter than `p`, `q` is minimal. --/ -lemma shortest_among_shorter_loops [DecidableEq V] {a : V} {p : Path a a} (q : Path a a) - (_ : q.length > 0) - (h_q_shorter : q.length < p.length) - (h_q_minimal : ∀ r : Path a a, r.length > 0 → r.length < p.length → q.length ≤ r.length) : - ∀ r : Path a a, r.length > 0 → q.length ≤ r.length := by - intro r h_r_pos - by_cases h_r_shorter : r.length < p.length - · exact h_q_minimal r h_r_pos h_r_shorter - · have h_p_le_r : p.length ≤ r.length := le_of_not_gt h_r_shorter - exact le_trans (le_of_lt h_q_shorter) h_p_le_r - -/-- If there exists any positive-length loop at `a`, then there exists a shortest one. -/ -lemma exists_shortest_positive_loop [DecidableEq V] {a : V} (q : Path a a) (hq_pos : q.length > 0) : - ∃ (s : Path a a), s.length > 0 ∧ ∀ (r : Path a a), r.length > 0 → s.length ≤ r.length := by - let P := fun n => ∃ (r : Path a a), r.length = n ∧ r.length > 0 - have hP_nonempty : ∃ n, P n := ⟨q.length, q, rfl, hq_pos⟩ - let min_len := Nat.find hP_nonempty - have h_min_len_spec : P min_len := Nat.find_spec hP_nonempty - obtain ⟨s, hs_len, hs_pos⟩ := h_min_len_spec - use s - constructor - · exact hs_pos - · intro r hr_pos - have hr_prop : P r.length := ⟨r, rfl, hr_pos⟩ - rw [hs_len] - exact Nat.find_min' hP_nonempty hr_prop - -/-- If n < m and m ≤ n, we have a contradiction -/ -private lemma Nat.lt_le_antisymm {n m : Nat} (h1 : n < m) (h2 : m ≤ n) : False := - Nat.lt_irrefl n (Nat.lt_of_lt_of_le h1 h2) - -/-- If n ≥ m, then it's not the case that n < m. -/ -private lemma not_lt_of_ge {n m : Nat} (h : n ≥ m) : ¬(n < m) := - fun h' => Nat.lt_le_antisymm h' h - -/-- If n > m, then it's not the case that n ≤ m -/ -private lemma not_le_of_gt {n m : Nat} (h : n > m) : ¬(n ≤ m) := - fun h' => Nat.lt_le_antisymm h h' - -/-- Given a path with a repeated vertex, we can find that vertex and show it appears - in the dropLast portion of the prefix path. -/ -lemma repeated_vertex_in_prefix_dropLast [DecidableEq V] {a : V} (s : Path a a) - (h_not_simple : ¬IsStrictlySimple s) : - ∃ (v : V) (p₁ : Path a v) (p₂ : Path v a), - v ∈ p₁.vertices.dropLast ∧ s = p₁.comp p₂ ∧ v ∉ p₂.vertices.tail := by - obtain ⟨v, hv_in, hv_ge₂⟩ := not_strictly_simple_iff_exists_repeated_vertex.mp h_not_simple - obtain ⟨p₁, p₂, hp, hv_not_tail⟩ := exists_decomp_of_mem_vertices_prop s hv_in - have hv_in_p1_dropLast : v ∈ p₁.vertices.dropLast := by - have h_count_s : s.vertices.count v = (p₁.vertices.dropLast ++ p₂.vertices).count v := by - rw [hp, vertices_comp] - rw [h_count_s, List.count_append] at hv_ge₂ - have h_count_p2 : p₂.vertices.count v = 1 := by - have h_head : p₂.vertices.head? = some v := by - cases p₂ with - | nil => simp [vertices_nil] - | cons _ _ => exact vertices_head? _ - have hne : p₂.vertices ≠ [] := List.ne_nil_of_head?_eq_some h_head - have h_shape : p₂.vertices = v :: p₂.vertices.tail := by - have h_first : p₂.vertices.head? = some v := h_head - have h_decomp : ∃ h t, p₂.vertices = h :: t := List.exists_cons_of_ne_nil hne - rcases h_decomp with ⟨h, t, heq⟩ - rw [heq] - have h_eq : h = v := by - rw [heq] at h_first - simp only [List.head?_cons] at h_first - exact Option.some.inj h_first - rw [h_eq] - have t_eq : t = p₂.vertices.tail := by rw [heq, List.tail_cons] - rw [t_eq]; exact rfl - rw [h_shape, List.count_cons_self, List.count_eq_zero_of_not_mem hv_not_tail] - have h_count_p1 : (p₁.vertices.dropLast).count v ≥ 1 := by - have h_sum : (p₁.vertices.dropLast).count v + p₂.vertices.count v ≥ 2 := hv_ge₂ - rw [h_count_p2] at h_sum - linarith - exact (List.count_pos_iff).mp (by linarith) - exact ⟨v, p₁, p₂, hv_in_p1_dropLast, hp, hv_not_tail⟩ - -lemma extract_cycle_from_prefix [DecidableEq V] {a vertex : V} {p₁ : Path a vertex} - (hvertex_in_p1_dropLast : vertex ∈ p₁.vertices.dropLast) : - ∃ (q : Path a vertex) (c : Path vertex vertex), - p₁ = q.comp c ∧ vertex ∉ q.vertices.dropLast := by - classical - let i := p₁.vertices.idxOf vertex - have h_mem : vertex ∈ p₁.vertices := - List.mem_of_mem_dropLast hvertex_in_p1_dropLast - have hi_lt : i < p₁.vertices.length := - List.idxOf_lt_length_of_mem h_mem - obtain ⟨v_split, q, c, h_comp, h_len, h_get⟩ := split_at_vertex p₁ i hi_lt - have hv_eq : v_split = vertex := by - have h_idx : p₁.vertices.get ⟨i, hi_lt⟩ = vertex := - List.get_idxOf_of_mem h_mem - simpa [h_get] using h_idx - have hv_not_in_q : vertex ∉ q.vertices.dropLast := by - intro h_in - have h_decomp_vertices : - p₁.vertices = q.vertices.dropLast ++ c.vertices := by - simp [h_comp] - have h_prefix : - q.vertices.dropLast.IsPrefix p₁.vertices := by - simp [h_decomp_vertices] - have h_idx_eq : - p₁.vertices.idxOf vertex = - (q.vertices.dropLast).idxOf vertex := - List.idxOf_eq_idxOf_of_isPrefix h_prefix h_in - have h_idx_prefix_lt : - (q.vertices.dropLast).idxOf vertex < - (q.vertices.dropLast).length := - List.idxOf_lt_length_of_mem h_in - have h_len_drop : - (q.vertices.dropLast).length = q.length := by - simp [List.length_dropLast, vertices_length] - have : i < i := by - have : (q.vertices.dropLast).idxOf vertex < i := by - simpa [h_len_drop, h_len] using h_idx_prefix_lt - simpa [i, h_idx_eq] - exact (lt_irrefl _ this) - subst hv_eq - exact ⟨q, c, h_comp, hv_not_in_q⟩ - -lemma extract_cycle_from_prefix' [DecidableEq V] {a v : V} {p₁ : Path a v} - (hv_in_p1_dropLast : v ∈ p₁.vertices.dropLast) : - ∃ (q : Path a v) (c : Path v v), - p₁ = q.comp c := by - obtain ⟨q, c, h_split⟩ := - exists_decomp_of_mem_vertices p₁ (List.mem_of_mem_dropLast hv_in_p1_dropLast) - exact ⟨q, c, h_split⟩ - -/-- A cycle extracted from a path with a repeated vertex has positive length. -/ -lemma extracted_cycle_has_positive_length [DecidableEq V] {a v : V} - {p₁ q : Path a v} {c : Path v v} - (h_p1_split : p₁ = q.comp c) - (hv_in_p1_dropLast : v ∈ p₁.vertices.dropLast) - (hv_not_in_q : v ∉ q.vertices.dropLast) : c.length > 0 := by - by_cases h_len_zero : c.length = 0 - · have hc_nil : c = Path.nil := (length_eq_zero_iff c).mp h_len_zero - have h_p1_eq_q : p₁ = q := by - rw [h_p1_split, hc_nil, comp_nil] - have h_v_in_q : v ∈ q.vertices.dropLast := by - subst h_p1_eq_q - exact hv_in_p1_dropLast - exact False.elim (hv_not_in_q h_v_in_q) - · exact Nat.pos_of_ne_zero h_len_zero - -/-- Removing a cycle from a path creates a strictly shorter path. -/ -lemma removing_cycle_gives_shorter_path [DecidableEq V] {a v : V} {s : Path a a} - {q : Path a v} {c : Path v v} {p₂ : Path v a} - (hp : s = (q.comp c).comp p₂) (hc_pos : c.length > 0) : (q.comp p₂).length < s.length := by - have h_len_shorter : (q.comp p₂).length = q.length + p₂.length := by - rw [length_comp] - have h_len_s : s.length = q.length + c.length + p₂.length := by - rw [hp, comp_assoc, length_comp, length_comp] - ring - rw [h_len_shorter, h_len_s] - subst hp - simp_all only [le_refl, gt_iff_lt, length_comp, comp_assoc, add_lt_add_iff_right, - lt_add_iff_pos_right, Nat.eq_of_le_zero] - -/- -/-- A shortest positive loop is strictly simple. -/ -theorem shortest_positive_loop_is_strictly_simple {a : V} : - ∀ (c : Path a a), c.length > 0 → (∀ p' : Path a a, p'.length > 0 → c.length ≤ p'.length) → - c.IsStrictlySimple := by sorry --/ diff --git a/lean/MCMC/PF/Data/List.lean b/lean/MCMC/PF/Data/List.lean deleted file mode 100644 index 8d7b7f4..0000000 --- a/lean/MCMC/PF/Data/List.lean +++ /dev/null @@ -1,682 +0,0 @@ -import Mathlib.Tactic -import Mathlib.Data.List.Infix -import Mathlib.Data.List.Nodup -import Mathlib.Data.Nat.Cast.Order.Ring -import Mathlib.Algebra.Order.Ring.Star -import Mathlib.Analysis.Normed.Ring.Lemmas -import Mathlib.Data.Int.Star -import Mathlib.Data.List.Basic -namespace List -open List -variable {α : Type*} - -/-- -A list `l` is disjoint from a singleton list `[x]` if and only if -`x` is not an element of `l`. --/ -@[simp] -lemma disjoint_singleton_right {l : List α} {x : α} : - List.Disjoint l [x] ↔ x ∉ l := - List.disjoint_singleton - -/-- -An element `a` is a member of `l.concat x` if and only if it is a member of `l` or is equal to `x`. --/ -@[simp] -theorem mem_concat {a x : α} {l : List α} : a ∈ l.concat x ↔ a ∈ l ∨ a = x := by - rw [concat_eq_append,List.mem_append, List.mem_singleton, or_comm] - -lemma dropLast_append_singleton {l : List α} {a : α} (h : l.length > 0) : - (l.concat a).dropLast = l := by - induction l with - | nil => simp at h - | cons hd tl ih => - cases tl with - | nil => simp only [concat_eq_append, cons_append, nil_append, dropLast_cons₂, - dropLast_singleton] - | cons tl_hd tl_tl => simp_all only [List.length_cons, gt_iff_lt, lt_add_iff_pos_left, add_pos_iff, Nat.lt_one_iff, - pos_of_gt, or_true, _root_.List.concat_eq_append, List.cons_append, forall_const, Nat.ofNat_pos, - List.dropLast_cons₂] - -lemma length_pos_of_append_singleton (l : List α) (a : α) : (l ++ [a]).length > 0 := by - simp only [length_append, length_cons, length_nil, zero_add, gt_iff_lt, lt_add_iff_pos_left, - add_pos_iff, Nat.lt_one_iff, pos_of_gt, or_true] - -/-- Any `x ∈ l` gives a decomposition `l = l₁ ++ x :: l₂`. -/ -lemma exists_mem_split {l : List α} {x : α} (h : x ∈ l) : - ∃ l₁ l₂, l = l₁ ++ x :: l₂ := by - induction l with - | nil => cases h - | cons y ys ih => - simp only [List.mem_cons] at h - rcases h with rfl | h' - · -- head - exact ⟨[], ys, by simp only [List.nil_append]⟩ - · rcases ih h' with ⟨l₁, l₂, rfl⟩ - use y :: l₁, l₂ - simp only [List.cons_append] - --- Note: `List.dropLast_cons_cons` is now in Mathlib (formerly `List.dropLast_cons₂`). --- The local version below is preserved under the alias `dropLast_cons_cons_aux` for --- backwards compatibility within this file; downstream callers should prefer Mathlib's. -lemma dropLast_cons_cons_aux (a b : α) (l : List α) : (a :: b :: l).dropLast = a :: (b :: l).dropLast := by - have h : (a :: b :: l).length > 0 := by simp only [List.length_cons, gt_iff_lt, - lt_add_iff_pos_left, add_pos_iff, Nat.ofNat_pos, or_true] - rw [@List.dropLast_cons₂] - -lemma append_left_cancel {l₁ l₂ l₃ : List α} (h_len : l₁.length = l₂.length) - (h : l₁ ++ l₃ = l₂ ++ l₃) : l₁ = l₂ := by - have : List.take l₁.length (l₁ ++ l₃) = List.take l₂.length (l₂ ++ l₃) := by - rw [h_len, h] - simp only [take_left'] at this - convert this - -lemma append_right_cancel {l₁ l₂ l₃ : List α} (h : l₁ ++ l₂ = l₁ ++ l₃) : l₂ = l₃ := by - have : List.drop l₁.length (l₁ ++ l₂) = List.drop l₁.length (l₁ ++ l₃) := by rw [h] - simp only [drop_left'] at this - exact this - -variable {α : Type*} {l m : List α} {n : ℕ} - -/-- -If `l.length ≤ n` and `n < (l ++ m).length`, then fetching the `n`-th element -of `l ++ m` lands in `m`, at index `n - l.length`. --/ - -theorem get_append_right {α : Type*} {l m : List α} {n : ℕ} - (hl : l.length ≤ n) (hn : n < (l ++ m).length) : - (l ++ m).get ⟨n, hn⟩ = - m.get ⟨n - l.length, by - simpa [List.length_append] using Nat.sub_lt_left_of_lt_add hl (by simpa [List.length_append] using hn)⟩ := by - induction l generalizing n with - | nil => - simp only [List.nil_append, List.get_eq_getElem, List.length_nil, tsub_zero] - | cons x xs ih => - simp only [List.length_cons, List.cons_append] at hl hn - dsimp [get] - cases' n with n' - · exact (Nat.not_succ_le_zero _ hl).elim - have hℓ : xs.length ≤ n' := Nat.le_of_succ_le_succ hl - have hδ : n' < (xs ++ m).length := by simp_all only [List.length_append, List.get_eq_getElem, - List.getElem_append_right, implies_true, add_le_add_iff_right, add_lt_add_iff_right] - have IH := ih hℓ hδ - simpa [Nat.sub_sub] using IH - -variable {α : Type*} [DecidableEq α] - -/-- Getting the element at index 0 of a non-empty list is the same as taking its head. -/ -lemma get_zero_eq_head {α : Type*} (l : List α) (h_nonempty : l ≠ []) : - l.get ⟨0, by simp only [length_pos_iff_ne_nil.mpr h_nonempty]⟩ = l.head h_nonempty := by - cases l with - | nil => contradiction - | cons hd _ => rfl - -/-- A list contains a duplicate element if the count of some element is greater than 1. -/ -def ContainsDup {α : Type*} [DecidableEq α] (l : List α) : Prop := - ∃ x, 2 ≤ l.count x - -lemma nodup_iff_not_contains_dup {α : Type*} [DecidableEq α] {l : List α} : - l.Nodup ↔ ¬l.ContainsDup := by - rw [List.nodup_iff_count_le_one, List.ContainsDup, not_exists] - constructor - · intro h x - specialize h x - simp only [not_le] - exact Nat.lt_of_le_of_lt h (Nat.lt_add_one 1) - · intro h x - specialize h x - exact Nat.le_of_lt_succ (Nat.lt_of_not_le h) - -variable {α : Type*} [DecidableEq α] - -lemma idxOf_append_left {v : α} {l₁ l₂ : List α} - (hv : v ∈ l₁) : - List.idxOf v (l₁ ++ l₂) = List.idxOf v l₁ := by - induction l₁ with - | nil => cases hv - | cons x xs ih => - by_cases h : v = x - · simp only [h, cons_append, idxOf_cons_self] - · have hvxs : v ∈ xs := by simpa [h] using hv - dsimp only [cons_append, idxOf] - rw [← cons_append] - simp only [cons_append, findIdx_cons] - congr! 1; exact congrFun (congrArg HAdd.hAdd (ih hvxs)) 1 - -lemma ne_nil_of_head?_eq_some {α : Type*} {l : List α} {x : α} (h : l.head? = some x) : l ≠ [] := by - by_contra heq - rw [heq] at h - simp only [head?_nil, reduceCtorEq] at h - -/-- If `l₁` is a prefix of `l₂` and `v ∈ l₁` then the two - indices of `v` coincide. -/ -lemma idxOf_eq_idxOf_of_isPrefix {v : α} {l₁ l₂ : List α} - (hpref : List.IsPrefix l₁ l₂) (hv : v ∈ l₁) : - List.idxOf v l₂ = List.idxOf v l₁ := by - rcases hpref with ⟨t, rfl⟩ - simpa using idxOf_append_left hv - -omit [DecidableEq α] in -/-- -If a list contains an element `x` at least twice, then `x` is contained -in the tail of this list. --/ -lemma mem_tail_of_count_ge_two [DecidableEq α] {x : α} {l : List α} - (h : l.count x ≥ 2) : x ∈ l.tail := by - cases l with - | nil => simp at h - | cons hd tl => - by_cases hhd : hd = x - · have h_tl : tl.count x ≥ 1 := by - have : tl.count x + 1 ≥ 2 := by - simpa [hhd] using h - linarith - have h_pos : 0 < tl.count x := by linarith - have : x ∈ tl := (List.count_pos_iff).1 h_pos - simpa using this - · have h_tl : tl.count x ≥ 2 := by simpa [hhd] using h - have h_pos : 0 < tl.count x := by linarith - have : x ∈ tl := (List.count_pos_iff).1 h_pos - simpa using this - -namespace Nat -@[simp] lemma eq_of_le_zero {n : ℕ} (h : n ≤ 0) : n = 0 := - le_antisymm h (Nat.zero_le _) -end Nat - -omit [DecidableEq α] in -/-- For a *non-empty* list the first element is not contained in the `tail`, - provided the list has no duplicates. -/ -lemma head_not_mem_tail_of_nodup {l : List α} [DecidableEq α] (h : l.Nodup) (h_nonempty : l ≠ []) : - l.head h_nonempty ∉ l.tail := by - cases l with - | nil => contradiction - | cons hd tl => - simp only [nodup_cons] at h - simp_all only [tail_cons, head_cons, not_false_eq_true] - -/-- If an element is both the head and in the tail of a list, it appears at least twice -/ -lemma count_ge_two_of_mem_head_and_tail {l : List α} {x : α} - (h_head : l.head? = some x) (h_tail : x ∈ l.tail) : l.count x ≥ 2 := by - cases l with - | nil => - simp only [head?, reduceCtorEq] at h_head - | cons y ys => - have hyx : y = x := by injection h_head - subst hyx - have h_count_tail : ys.count y > 0 := by exact count_pos_iff.mpr h_tail - simp only [count_cons_self, ge_iff_le, Nat.reduceLeDiff, one_le_count_iff] - simp_all only [head?_cons, tail_cons, gt_iff_lt, count_pos_iff] - -omit [DecidableEq α] in -/-- A list with its head not in its tail has no duplicates if its tail has no duplicates -/ -lemma nodup_of_head_not_mem_tail {l : List α} {x : α} - (h_nonempty : l ≠ []) (h_head : l.head h_nonempty = x) (h_not_in_tail : x ∉ l.tail) (h_nodup_tail : l.tail.Nodup) : l.Nodup := by - cases l with - | nil => contradiction - | cons y ys => - simp only [head_cons] at h_head - subst h_head - simp only [tail_cons] at h_not_in_tail - simp only [nodup_cons] - constructor - · exact h_not_in_tail - · simpa using h_nodup_tail - -/-- The index of an element in a prefix equals the index in the whole list -/ -lemma idxOf_eq_of_isPrefix{l₁ l₂ : List α} (h_prefix : l₁.IsPrefix l₂) {x : α} (h_mem : x ∈ l₁) : - l₁.idxOf x = l₂.idxOf x := by - rcases h_prefix with ⟨t, rfl⟩ - exact (idxOf_append_left h_mem).symm - -/-- For an element x in a list l, if x appears exactly once and is not in the tail, - then its count is less than 2 -/ -lemma count_lt_of_mem_of_not_mem_tail {l : List α} {x : α} - (h_mem : x ∈ l) (h_not_in_tail : x ∉ l.tail) : l.count x < 2 := by - cases l with - | nil => - simp only [not_mem_nil] at h_mem - | cons y ys => - have hxy : x = y := by - simp only [tail_cons] at h_not_in_tail - cases h_mem - · exact rfl - · (expose_names; exact False.elim (h_not_in_tail h)) - subst hxy - simp only [List.count_cons_self] - have h0 : ys.count x = 0 := by - by_contra h1 - have hpos : ys.count x > 0 := Nat.pos_of_ne_zero h1 - have hmem' := (List.count_pos_iff).mp hpos - exact h_not_in_tail hmem' - simp only [h0, le_refl, Nat.eq_of_le_zero, zero_add, Nat.one_lt_ofNat] - -omit [DecidableEq α] in -/-- Count an element in a head-tail splitting of a list. -If the first (i.e. leftmost) occurrence of `x` is at index `l.length - 1` -(so at the last position) then `x` occurs exactly once. -/ -lemma count_eq_one_of_idxOf_eq_length_sub_one [DecidableEq α] {l : List α} {x : α} - (hlt : l.idxOf x < l.length) : - l.idxOf x = l.length - 1 → l.count x = 1 := by - intro hidx - induction l using List.reverseRecOn with - | nil => cases (Nat.lt_irrefl _ hlt) - | append_singleton xs y ih => - have hlen : (xs ++ [y]).length = xs.length + 1 := by simp - have hidx' : idxOf x (xs ++ [y]) = xs.length := by - simpa [hlen, Nat.add_comm, Nat.add_left_comm, Nat.add_assoc] using hidx - by_cases hx : x = y - · subst hx - have h_not_mem : x ∉ xs := by - intro hmem - have : idxOf x (xs ++ [x]) = idxOf x xs := by - simp [idxOf_append_of_mem hmem] - have hlt' : idxOf x xs < xs.length := idxOf_lt_length_of_mem hmem - simp_rw [← hidx'] at hlt' - omega - have hcount0 : xs.count x = 0 := count_eq_zero_of_not_mem h_not_mem - simp [count_append, hcount0] - · have hx_mem : x ∈ xs ++ [y] := idxOf_lt_length_iff.mp hlt - have : x ∈ xs ∨ x = y := by - simpa [mem_append, mem_singleton] using hx_mem - cases this with - | inl hmem => - have : idxOf x (xs ++ [y]) = idxOf x xs := by - simp [idxOf_append_of_mem hmem] - have hlt' : idxOf x xs < xs.length := idxOf_lt_length_of_mem hmem - simp; omega - | inr hxy => exact (hx hxy).elim - -@[simp] lemma idxOf_eq_length_sub_one_of_getLast - {l : List α} {x : α} - (h_nonempty : l ≠ []) - (h_last : l.getLast h_nonempty = x) - (h_unique : x ∉ l.dropLast) : - l.idxOf x = l.length - 1 := by - have hx : x ∈ l := by - rw [← h_last] - exact getLast_mem h_nonempty - have h_idx_ge : l.length - 1 ≤ l.idxOf x := by - by_contra h - rw [not_le] at h - have h_mem_dropLast : x ∈ l.dropLast := by - rw [dropLast_eq_take]; rw [@mem_take_iff_getElem] - use l.idxOf x - constructor - · (expose_names; (expose_names; refine List.getElem_idxOf (by exact List.idxOf_lt_length_of_mem hx))) - · subst h_last - simp_all only [getLast_mem, tsub_le_iff_right, le_add_iff_nonneg_right, le_refl, Nat.eq_of_le_zero, zero_le, - inf_of_le_left] - contradiction - have h_idx_lt : l.idxOf x < l.length := List.idxOf_lt_length_of_mem hx - exact Nat.le_antisymm (Nat.le_sub_one_of_lt h_idx_lt) h_idx_ge - -@[simp] lemma not_gt {n m : ℕ} : (¬ n > m) ↔ n ≤ m := Nat.not_lt - -omit [DecidableEq α] in -@[simp] lemma head_not_mem_tail_of_first - [DecidableEq α] {l : List α} (h : l.Nodup) (hne : l ≠ []) : - l.head hne ∉ l.tail := by - cases l with - | nil => cases hne rfl - | cons hd tl => - simp only [List.nodup_cons] at h - exact h.1 - -/-- If `x` is in the tail of a list, then `x` is not the head of the list. -This is only true in general for lists without duplicates. -/ -lemma ne_of_mem_tail {l : List α} {x : α} (h_nodup : l.Nodup) (h_mem : x ∈ l.tail) (h_ne_nil : l ≠ []) : - x ≠ l.head h_ne_nil := by - intro h_eq - have h_head_in_tail : l.head h_ne_nil ∈ l.tail := by - rwa [← h_eq] - have h_head_not_in_tail := head_not_mem_tail_of_nodup h_nodup h_ne_nil - contradiction - -lemma bif_of_false {α : Type*} {p : Bool} {a b : α} (h : p = false) : (bif p then a else b) = b := by - rw [h] - rfl - -lemma bif_of_true {α : Type*} {p : Bool} {a b : α} (h : p = true) : (bif p then a else b) = a := by - rw [h] - rfl - -/-- Helper lemma for findIdx.go with accumulator incrementing by 1 -/ -lemma findIdx_go_succ' {α : Type*} (p : α → Bool) (l : List α) (n : Nat) : - findIdx.go p l (n+1) = findIdx.go p l n + 1 := by - induction l generalizing n with - | nil => rfl - | cons hd tl ih => - simp only [findIdx.go] - by_cases h_p : p hd = true - · rw [bif_of_true h_p, bif_of_true h_p] - · have h_p_false : p hd = false := by rw [Bool.not_eq_true] at h_p; exact h_p - rw [bif_of_false h_p_false, bif_of_false h_p_false] - exact ih (n+1) - -/-- Helper lemma: the findIdx.go function with accumulator 1 returns the result of findIdx plus 1 -/ -lemma findIdx_go_succ {α : Type*} (p : α → Bool) (l : List α) : - findIdx.go p l 1 = findIdx p l + 1 := by - unfold findIdx - exact findIdx_go_succ' p l 0 - -omit [DecidableEq α] in -/-- Helper lemma: Boolean equality is false iff the terms are not equal -/ -lemma beq_eq_false_iff_ne [DecidableEq α] {a b : α} : (a == b) = false ↔ a ≠ b := by - rw [_root_.beq_eq_false_iff_ne] - -omit [DecidableEq α] in -/-- Helper lemma for index computation with head != x -/ -lemma idxOf_cons_of_ne [DecidableEq α] {hd : α} {tl : List α} {x : α} (h_neq : hd ≠ x) : - idxOf x (hd :: tl) = idxOf x tl + 1 := by - dsimp only [idxOf, findIdx] - simp only [findIdx.go] - have h_eq_false : (hd == x) = false := by - rw [beq_eq_false_iff_ne] - exact h_neq - rw [bif_of_false h_eq_false] - exact findIdx_go_succ (fun y => y == x) tl - --- This helper lemma addresses many of the beq_iff_eq rewrite failures -lemma not_beq_eq_true_iff_ne {a b : α} : ¬(a == b) = true ↔ a ≠ b := by - rw [@Bool.bool_iff_false] - rw [beq_eq_false_iff_ne] - -/-- If the index of `x` is less than the length of `l`, then `x` is in `l`. -/ -lemma mem_of_idxOf_lt_length {l : List α} {x : α} (h : idxOf x l < l.length) : x ∈ l := by - induction l with - | nil => simp only [idxOf_nil, le_refl, Nat.eq_of_le_zero, length_nil, lt_self_iff_false] at h - | cons hd tl ih => - dsimp [idxOf, findIdx, length] at h - simp only [findIdx.go] at h - by_cases h_eq : hd == x - · simp only [h_eq, le_refl, zero_add, cond_true, Nat.eq_of_le_zero, lt_add_iff_pos_left] at h - rw [beq_iff_eq] at h_eq - simp only [h_eq, mem_cons, true_or] - · simp only [h_eq, zero_add, cond_false] at h - have h_neq : hd ≠ x := by - simp_all only [beq_iff_eq, ne_eq, not_false_eq_true] - have h_tl : idxOf x tl < tl.length := by - dsimp only [idxOf] at h ⊢ - rw [findIdx_go_succ] at h - exact Nat.lt_of_succ_lt_succ h - have h_mem : x ∈ tl := ih h_tl - simp only [mem_cons, h_mem, or_true] - -/-- If `x` is in `l`, then getting the element at index `idxOf x l` gives `x`. -/ -lemma get_idxOf_of_mem {l : List α} {x : α} (h : x ∈ l) : - l.get ⟨idxOf x l, idxOf_lt_length_of_mem h⟩ = x := by - induction l with - | nil => simp only [not_mem_nil] at h - | cons hd tl ih => - by_cases h_eq : hd = x - · subst h_eq - have h_idx : idxOf hd (hd :: tl) = 0 := by - dsimp [idxOf, findIdx] - simp only [findIdx.go] - simp only [BEq.rfl, le_refl, zero_add, cond_true, Nat.eq_of_le_zero] - simp only [h_idx, get_eq_getElem, getElem_cons_zero] - · simp only [mem_cons] at h - cases h with - | inl h_hd => - subst h_hd - contradiction - | inr h_tl => - have ih' := ih h_tl - have h_idxOf : idxOf x (hd :: tl) = idxOf x tl + 1 := idxOf_cons_of_ne h_eq - have hl : idxOf x tl < tl.length := idxOf_lt_length_of_mem h_tl - have hl' : idxOf x tl + 1 < (hd :: tl).length := by - rw [length_cons] - exact Nat.add_lt_add_right hl 1 - have helper : (hd :: tl).get ⟨idxOf x (hd :: tl), idxOf_lt_length_of_mem (mem_cons.mpr (Or.inr h_tl))⟩ = - (hd :: tl).get ⟨idxOf x tl + 1, hl'⟩ := by - congr - have h_getElem : (hd :: tl).get ⟨idxOf x tl + 1, hl'⟩ = tl.get ⟨idxOf x tl, hl⟩ := by - simp only [get_eq_getElem] - apply getElem_cons_succ - exact Eq.trans helper (Eq.trans h_getElem ih') - -omit [DecidableEq α] in -/-- If `l.get i = x`, then `idxOf x l ≤ i.val`. -/ -lemma idxOf_le_of_get_eq [DecidableEq α] {l : List α} {x : α} {i : Fin l.length} (h : l.get i = x) : - idxOf x l ≤ i.val := by - induction l with - | nil => exact Fin.elim0 i - | cons hd tl ih => - cases i using Fin.cases with - | zero => - dsimp only [idxOf, findIdx, length_cons, Fin.val_zero] - simp only [findIdx.go] - have : hd = x := by - simp only [get_eq_getElem] at h - exact h - rw [beq_iff_eq.mpr this] - simp only [cond_true] - exact Nat.zero_le 0 - | succ j => - dsimp only [idxOf, findIdx, length_cons, Fin.val_succ] - simp only [findIdx.go] - by_cases h_hd_eq : hd == x - · simp only [h_hd_eq, cond_true] - exact Nat.zero_le j.val.succ - · simp only [h_hd_eq, cond_false] - have h_tl : tl.get j = x := by - simp only [get_eq_getElem] at h - exact h - have ih' := ih h_tl - have h_idx : findIdx.go (fun y => y == x) tl 1 = idxOf x tl + 1 := by - unfold idxOf at ih' - exact findIdx_go_succ (fun y => y == x) tl - calc - findIdx.go (fun y => y == x) tl 1 = idxOf x tl + 1 := h_idx - _ ≤ j.val + 1 := Nat.add_le_add_right ih' 1 - -/-- If v is in a list but not equal to a, then a is not in the singleton list containing v. -/ -lemma not_mem_implies_ne {α} [DecidableEq α] {v a : α} {l : List α} : - v ∈ l → v ≠ a → a ∉ [v] := - fun _ hne ha => hne.symm (mem_singleton.1 ha) - -/-- If v is in the list but not the head, then its index is positive. -/ -lemma idxOf_pos_of_ne_head {α} [DecidableEq α] {v : α} {l : List α} - (hv : v ∈ l) (hne : ∀ (h : l ≠ []), v ≠ l.head h) : - idxOf v l > 0 := by - cases l with - | nil => cases hv - | cons hd tl => - by_cases h_eq : v = hd - · subst h_eq - exact absurd rfl (hne (cons_ne_nil _ _)) - · have h : hd ≠ v := by intro h; exact h_eq h.symm - simp only [gt_iff_lt] - rw [idxOf_cons_of_ne h] - exact Nat.succ_pos _ - -/-- If an element `x` is in the tail of a list `l` without duplicates, its first index in `l` must be positive. -/ -lemma idxOf_pos_of_mem_tail {l : List α} (h_nodup : l.Nodup) {x : α} (h : x ∈ l.tail) : 0 < l.idxOf x := by - cases l with - | nil => simp only [tail_nil, not_mem_nil] at h - | cons hd tl => - have h_mem_tl : x ∈ tl := by rwa [tail_cons] at h - have h_ne : hd ≠ x := by - intro h_eq - subst h_eq - simp_all only [nodup_cons, not_true_eq_false, false_and] - rw [idxOf_cons_of_ne h_ne] - exact Nat.succ_pos _ - -/-- Membership in the tail of `l.concat y`. -It is only useful if `l` is **non-empty** – we require `hl : l ≠ []`. -/ -@[simp] lemma mem_tail_concat_of_ne_nil - {α : Type*} [DecidableEq α] {l : List α} (hl : l ≠ []) (x y : α) : - x ∈ (l.concat y).tail ↔ x ∈ l.tail ∨ x = y := by - cases l with - | nil => exact (hl rfl).elim - | cons _ t => simp only [concat_eq_append, cons_append, tail_cons, mem_append, mem_cons, - not_mem_nil, or_false] - -/-- Membership in the tail of a concatenation splits into the two obvious - alternatives. -/ -lemma mem_tail_append {α : Type*} {x : α} {L₁ L₂ : List α} : - x ∈ (L₁ ++ L₂).tail ↔ (L₁ = nil ∧ x ∈ L₂.tail) ∨ (L₁ ≠ nil ∧ x ∈ L₁.tail ++ L₂) := by - cases L₁ with - | nil => - simp only [tail, nil_append, true_and, ne_eq, not_true_eq_false, false_and, or_false] - | cons h t => - simp only [tail, cons_append, mem_append, reduceCtorEq, false_and, ne_eq, not_false_eq_true, - true_and, false_or] - -/-- For a non-empty list `L`, membership in the tail of `L.concat a` - means “either in the tail of `L` or equal to the new element `a`”. -/ -lemma mem_tail_concat_of_ne_nil' {α : Type*} {L : List α} (hL : L ≠ []) {x a : α} : - x ∈ (L.concat a).tail ↔ x ∈ L.tail ∨ x = a := by - -- `L.concat a = L ++ [a]` - simpa [List.concat, mem_tail_append, List.tail, hL] using - (by - cases L with - | nil => cases hL rfl - | cons h t => - simp only [cons_append, mem_append, mem_cons, not_mem_nil, or_false]) - -@[simp] -lemma mem_of_mem_tail_dropLast {α} {x : α} {l : List α} : - x ∈ (l.dropLast).tail → x ∈ l.tail := by - cases l with - | nil => intro h; cases h - | cons hd tl => - cases tl with - | nil => intro h; cases h - | cons hd' tl' => - intro h - have h' : x ∈ (hd' :: tl').dropLast := by - simpa using h - have : x ∈ (hd' :: tl') := List.mem_of_mem_dropLast h' - simpa using this - -/-- The last element of a list of length at least 2 is in its tail. -/ -@[simp] -lemma getLast_mem_tail {α : Type*} {l : List α} (h : l.length ≥ 2) : - l.getLast (List.ne_nil_of_length_pos (by linarith)) ∈ l.tail := by - cases l with - | nil => simp only [length_nil, le_refl, Nat.eq_of_le_zero, ge_iff_le, nonpos_iff_eq_zero, - OfNat.ofNat_ne_zero] at h - | cons hd tl => - have h_tl_len : tl.length ≥ 1 := Nat.le_of_succ_le_succ h - have h_tl_nonempty : tl ≠ [] := List.ne_nil_of_length_pos (by linarith) - simp only [tail_cons] - simp_all only [ge_iff_le, ne_eq, not_false_eq_true, getLast_cons, getLast_mem] - -omit [DecidableEq α] in -@[simp] -lemma not_mem_dropLast_getLast {l : List α} - (h₁ : l ≠ []) (h₂ : l.Nodup) : - l.getLast h₁ ∉ l.dropLast := by - induction l using List.reverseRecOn with - | nil => - cases h₁ rfl - | append_singleton xs x ih => - simp only [getLast_append_singleton] at * - have h_disj : List.Disjoint xs [x] := disjoint_of_nodup_append h₂ - have hx_not_mem : x ∉ xs := (disjoint_singleton_right).1 h_disj - simpa using hx_not_mem - -lemma nodup_take_of_nodup {α : Type*} {l : List α} (h : l.Nodup) (n : ℕ) : - (l.take n).Nodup := - h.sublist (List.take_sublist _ _) - -omit [DecidableEq α] in -/-- -For a list `l` with no duplicates, the element at index `i` is not a member of the prefix of `l` of length `i`. --/ -lemma get_not_mem_take {l : List α} (h_nodup : l.Nodup) - (i : ℕ) (h_bounds : i < l.length) : - l.get ⟨i, h_bounds⟩ ∉ l.take i := by - revert i h_bounds - induction l with - | nil => - intro i h_bounds - simpa using (Nat.not_lt_zero _ h_bounds).elim - | cons hd tl ih => - intro i h_bounds - simp only [nodup_cons] at h_nodup - cases i with - | zero => - simp only [le_refl, take_zero, length_cons, Fin.zero_eta, get_eq_getElem, Fin.val_zero, - Nat.eq_of_le_zero, getElem_cons_zero, not_mem_nil, not_false_eq_true] - | succ i' => - have h_bounds' : i' < tl.length := by - simpa [List.length_cons] using Nat.lt_of_succ_lt_succ h_bounds - have h_ind := ih h_nodup.2 i' h_bounds' - simp only [get_cons_succ] - rw [take_succ_cons, mem_cons, not_or] - constructor - · intro h_eq - have h_mem : hd ∈ tl := by - rw [← h_eq] - exact get_mem tl ⟨i', Nat.lt_of_succ_lt_succ h_bounds⟩ - exact h_nodup.1 h_mem - · exact h_ind - -omit [DecidableEq α] in -@[simp] lemma getLast_not_mem_dropLast - {l : List α} (h_ne : l ≠ []) (h_nodup : l.Nodup) : - l.getLast h_ne ∉ l.dropLast := by - simpa using List.not_mem_dropLast_getLast (l := l) h_ne h_nodup - -omit [DecidableEq α] in -/-- An element `x` is not a member of the prefix of `l` up to the first -occurrence of `x`. -/ -lemma not_mem_take_idxOf [DecidableEq α] {x : α} {l : List α} (h : x ∈ l) : - x ∉ l.take (l.idxOf x) := by - induction l with - | nil => cases h - | cons hd tl ih => - by_cases hx : x = hd - · subst hx - simp [idxOf_cons_self] - · have h_in_tl : x ∈ tl := by - simpa [hx] using h - have h_idx : idxOf x (hd :: tl) = idxOf x tl + 1 := - idxOf_cons_of_ne fun a ↦ hx (id (Eq.symm a)) - rw [h_idx, take_succ_cons, mem_cons, not_or] - exact ⟨hx, ih h_in_tl⟩ - -omit [DecidableEq α] in -/-- Two prefixes of the same list with the same length are equal. -/ -lemma IsPrefix.eq_of_length_eq {l₁ l₂ l : List α} - (h₁ : l₁.IsPrefix l) (h₂ : l₂.IsPrefix l) (h_len : l₁.length = l₂.length) : - l₁ = l₂ := by - obtain ⟨t₁, rfl⟩ := h₁ - obtain ⟨t₂, h_eq⟩ := h₂ - have h_append_eq : l₁ ++ t₁ = l₂ ++ t₂ := by rw [h_eq] - have h_take_eq := congr_arg (fun l ↦ l.take l₁.length) h_append_eq - simpa [take_left', h_len] using h_take_eq - -omit [DecidableEq α] in -/-- -If the head of a list does not satisfy the predicate `p`, then `findIdx` on that list -is one greater than `findIdx` on the tail. --/ -lemma findIdx_cons_of_ne {p : α → Bool} {hd : α} {tl : List α} (h : p hd = false) : - findIdx p (hd :: tl) = 1 + findIdx p tl := by - unfold findIdx - unfold findIdx.go - rw [h] - induction tl with - | nil => - simp only [findIdx.go, zero_add, cond_false, add_zero] - | cons hd' tl' ih => - simp only [findIdx.go, zero_add, Nat.reduceAdd, cond_false] - by_cases h' : p hd' = true - · simp only [h', cond_true, le_refl, Nat.eq_of_le_zero, add_zero] - · simp only [h', cond_false] - induction tl' with - | nil => simp only [findIdx.go, Nat.reduceAdd] - | cons a l ih' => - simp [findIdx.go] - by_cases ha : p a = true - · simp only [ha, cond_true, Nat.reduceAdd] - · simp only [ha, cond_false] - rw [Nat.one_add] - exact findIdx_go_succ' p l 2 - -end List diff --git a/lean/MCMC/PF/LinearAlgebra/Matrix/PerronFrobenius/Aperiodic.lean b/lean/MCMC/PF/LinearAlgebra/Matrix/PerronFrobenius/Aperiodic.lean deleted file mode 100644 index 0f4a4d5..0000000 --- a/lean/MCMC/PF/LinearAlgebra/Matrix/PerronFrobenius/Aperiodic.lean +++ /dev/null @@ -1,97 +0,0 @@ -import MCMC.PF.Combinatorics.Quiver.Cyclic -import MCMC.PF.Combinatorics.Quiver.Path - -open Quiver - -namespace Matrix -open Quiver -variable {n : Type*} [Fintype n] [DecidableEq n] -variable {A : Matrix n n ℝ} - -/-! # Aperiodic matrices -/ - -/-- The index of imprimitivity of an irreducible matrix is the index of imprimitivity of its associated quiver. -/ -noncomputable def index_of_imprimitivity [Nonempty n] (A : Matrix n n ℝ) : ℕ := - Quiver.index_of_imprimitivity (toQuiver A) - -/-- A matrix is aperiodic if it is irreducible and its index of imprimitivity is 1. -/ -def IsAperiodic [Nonempty n] (A : Matrix n n ℝ) : Prop := - IsIrreducible A ∧ index_of_imprimitivity A = 1 - -/-- Frobenius (forward direction): A primitive matrix is irreducible and aperiodic. -/ -theorem primitive_implies_irreducible_and_aperiodic - [Nonempty n] (hA_nonneg : ∀ i j, 0 ≤ A i j) : - IsPrimitive A → IsAperiodic A := by - intro h_prim - have h_irred : IsIrreducible A := (Matrix.IsPrimitive.isIrreducible (A := A) h_prim) - rcases h_prim with ⟨_h_nonneg, ⟨k, hk_pos, hk_pos_entries⟩⟩ - let G := toQuiver A - letI : Quiver n := G - let i0 : n := Classical.arbitrary n - have hP : ∀ v : n, Nonempty { p : Path i0 v // p.length = k } := by - intro v - have : 0 < (A ^ k) i0 v := hk_pos_entries i0 v - exact (Matrix.pow_apply_pos_iff_nonempty_path (A := A) hA_nonneg k i0 v).mp this - have hP_i0 : Nonempty { p : Path i0 i0 // p.length = k } := hP i0 - rcases hP_i0 with ⟨p0, hp0⟩ - have hp0_pos : 0 < p0.length := by simpa [hp0] using hk_pos - obtain ⟨j, e0, s, hdecomp, hlen⟩ := Quiver.Path.path_decomposition_first_edge p0 hp0_pos - have hs_len : s.length + 1 = k := by - simpa [hp0] using hlen.symm - rcases hP j with ⟨Pj, hPj⟩ - rcases hP i0 with ⟨Pi0, hPi0⟩ - have c1_mem : (Pj.comp s).length ∈ Quiver.CycleLengths (i := i0) := by - have hpos : 0 < (Pj.comp s).length := by - have : 0 < k := hk_pos - have hle : k ≤ k + s.length := Nat.le_add_right _ _ - have : 0 < k + s.length := Nat.lt_of_lt_of_le this hle - simpa [Path.length_comp, hPj, Nat.add_comm] using this - exact ⟨hpos, ⟨Pj.comp s, rfl⟩⟩ - have c2_mem : (((Path.nil : Path i0 i0).cons e0).comp (s.comp Pi0)).length ∈ Quiver.CycleLengths (i := i0) := by - have hpos : 0 < (((Path.nil : Path i0 i0).cons e0).comp (s.comp Pi0)).length := by - have : 0 < k + 1 + s.length := Nat.pos_of_neZero (k + 1 + s.length) - simp [Path.length_comp, Path.length_cons, Path.length_nil, hPi0, Nat.add_comm, Nat.add_left_comm] - exact ⟨hpos, ⟨((Path.nil : Path i0 i0).cons e0).comp (s.comp Pi0), rfl⟩⟩ - have len_c1 : (Pj.comp s).length = k + s.length := by - simp [Path.length_comp, hPj] - have len_c2 : (((Path.nil : Path i0 i0).cons e0).comp (s.comp Pi0)).length = k + 1 + s.length := by - simp [Path.length_comp, Path.length_cons, Path.length_nil, hPi0, Nat.add_assoc, Nat.add_comm, Nat.add_left_comm] - have hdiv1 : - Quiver.period (i := i0) ∣ (Pj.comp s).length := - Quiver.divides_cycle_length (i := i0) c1_mem - have hdiv2 : - Quiver.period (i := i0) ∣ (((Path.nil : Path i0 i0).cons e0).comp (s.comp Pi0)).length := - Quiver.divides_cycle_length (i := i0) c2_mem - have hdiff_div : - Quiver.period (i := i0) ∣ (((Path.nil : Path i0 i0).cons e0).comp (s.comp Pi0)).length - (Pj.comp s).length := by - exact Nat.dvd_sub hdiv2 hdiv1 - have : Quiver.period (i := i0) ∣ 1 := by - have hdiff : - (((Path.nil : Path i0 i0).cons e0).comp (s.comp Pi0)).length - (Pj.comp s).length = 1 := by - simp [len_c1, len_c2, Nat.add_assoc] - simp +arith - grind - have h_period_one : Quiver.period (i := i0) = 1 := - Nat.dvd_one.mp this - refine ⟨h_irred, ?_⟩ - dsimp [Matrix.index_of_imprimitivity, Quiver.index_of_imprimitivity] at * - simpa using h_period_one - -/-! # Frobenius Normal Form -/ - -/-- Predicate: `P` is a permutation matrix (entries are 1 on a single position in each row given by a permutation, 0 elsewhere). -/ -def IsPermutationMatrix (P : Matrix n n ℝ) : Prop := - ∃ σ : Equiv.Perm n, ∀ i j, P i j = if σ i = j then 1 else 0 - -/-- -Theorem: Frobenius Normal Form. --/ -theorem exists_frobenius_normal_form [Nonempty n] - (_hA_irred : IsIrreducible A) (_h_h_gt_1 : index_of_imprimitivity A > 1) : - ∃ (P : Matrix n n ℝ), IsPermutationMatrix P ∧ True := by - refine ⟨1, ?_, trivial⟩ - refine ⟨Equiv.refl _, ?_⟩ - intro i j - simp [Matrix.one_apply] - -end Matrix diff --git a/lean/MCMC/PF/LinearAlgebra/Matrix/PerronFrobenius/CollatzWielandt.lean b/lean/MCMC/PF/LinearAlgebra/Matrix/PerronFrobenius/CollatzWielandt.lean deleted file mode 100644 index a129172..0000000 --- a/lean/MCMC/PF/LinearAlgebra/Matrix/PerronFrobenius/CollatzWielandt.lean +++ /dev/null @@ -1,888 +0,0 @@ -import MCMC.PF.LinearAlgebra.Matrix.PerronFrobenius.Lemmas -import MCMC.PF.aux -import Mathlib.Data.Matrix.Basic -import MCMC.PF.Topology.Compactness.ExtremeValueUSC - -namespace Matrix -open Finset Quiver Matrix -variable {n : Type*} [Fintype n] - -/-! -### TThe Collatz-Wielandt function for Matrices - --/ -section PerronFrobenius -variable {n : Type*} [Fintype n] [Nonempty n] -variable {A : Matrix n n ℝ} -open LinearMap Set Filter Topology Finset IsCompact -open scoped Convex Pointwise - -/-- The Collatz-Wielandt function, `r(x)` in Seneta's notation. -For a non-zero, non-negative vector `x`, this is `min_{i | xᵢ > 0} (Ax)ᵢ / xᵢ`. -Seneta uses row vectors `x'T`; we use column vectors `Ax`. The logic is identical. -/ -noncomputable def collatzWielandtFn (A : Matrix n n ℝ) (x : n → ℝ) : ℝ := - let supp := {i | 0 < x i}.toFinset - if h : supp.Nonempty then - supp.inf' h fun i => (A *ᵥ x) i / x i - else 0 - -/- -/-- The Collatz-Wielandt function r_x for a positive vector `x`. - See p. 30 in Berman & Plemmons. - We define it for strictly positive vectors to avoid division by zero. -/ -noncomputable def r_x (A : Matrix n n ℝ) (x : n → ℝ) : ℝ := - ⨅ i, (A.mulVec x i) / (x i)-/ - -instance : Nonempty n := inferInstance - -/-- The matrix-vector multiplication map as a continuous linear map. -/ -noncomputable abbrev mulVec_continuousLinearMap (A : Matrix n n ℝ) : (n → ℝ) →L[ℝ] (n → ℝ) := - (Matrix.mulVecLin A).toContinuousLinearMap - -omit [Nonempty n] in -/-- The standard simplex in ℝⁿ is compact (Heine-Borel: closed and bounded in ℝⁿ). - [Giaquinta-Modica, Theorem 6.3, cite: 230] -/ -private lemma IsCompact_stdSimplex : IsCompact (stdSimplex ℝ n) := by - -- stdSimplex is a closed and bounded subset of ℝⁿ - exact _root_.isCompact_stdSimplex ℝ n - -namespace CollatzWielandt - -/- --- The Collatz-Wielandt function r_x is continuous on the set of strictly positive vectors. -lemma r_x_continuousOn_pos (A : Matrix n n ℝ) : - ContinuousOn (r_x A) {x : n → ℝ | ∀ i, 0 < x i} := by - unfold r_x - apply continuousOn_iInf - intro i - apply ContinuousOn.div - · exact ((continuous_apply i).comp (mulVec_continuousLinearMap A).continuous).continuousOn - · exact (continuous_apply i).continuousOn - · exact fun x a ↦ Ne.symm (ne_of_lt (a i))-/ - -/-- *The Collatz-Wielandt function is upper-semicontinuous*. -Seneta relies on this fact (p.15, Appendix C) to use the Extreme Value Theorem. -`r(x)` is a minimum of functions `fᵢ(x) = (Ax)ᵢ / xᵢ`. Each `fᵢ` is continuous where `xᵢ > 0`. -The minimum of continuous functions is upper-semicontinuous. -[Giaquinta-Modica, Definition 6.21, Exercise 6.28, pp: 235, 236] -/ -theorem upperSemicontinuousOn - (A : Matrix n n ℝ) : UpperSemicontinuousOn (collatzWielandtFn A) (stdSimplex ℝ n) := by - have support_nonempty : ∀ x ∈ stdSimplex ℝ n, ({i | 0 < x i}.toFinset).Nonempty := by - intro x hx - obtain ⟨i, hi_pos⟩ := exists_pos_of_sum_one_of_nonneg hx.2 hx.1 - have hi_mem : i ∈ ({i | 0 < x i}.toFinset) := by simp [hi_pos] - exact ⟨i, hi_mem⟩ - intro x₀ hx₀ c hc - have supp_x₀ : {i | 0 < x₀ i}.toFinset.Nonempty := support_nonempty x₀ hx₀ - have fn_eq : collatzWielandtFn A x₀ = {i | 0 < x₀ i}.toFinset.inf' supp_x₀ (fun i => (A *ᵥ x₀) i / x₀ i) := by - unfold collatzWielandtFn - rw [dif_pos supp_x₀] - let U := {y : n → ℝ | ∀ i ∈ {i | 0 < x₀ i}.toFinset, 0 < y i} - have x₀_in_U : x₀ ∈ U := by - intro i hi - simp only [Set.mem_toFinset] at hi - exact hi - have U_open : IsOpen U := by - have h_eq : U = ⋂ i ∈ {i | 0 < x₀ i}.toFinset, {y | 0 < y i} := by - ext y - simp only [Set.mem_iInter, Set.mem_setOf_eq] - rfl - rw [h_eq] - apply isOpen_biInter_finset - intro i _ - exact isOpen_lt continuous_const (continuous_apply i) - let f := fun y => {i | 0 < x₀ i}.toFinset.inf' supp_x₀ fun i => (A *ᵥ y) i / y i - have f_cont : ContinuousOn f U := by - apply continuousOn_finset_inf' supp_x₀ - intro i hi - apply ContinuousOn.div - · exact continuous_apply i |>.comp_continuousOn ((mulVec_continuousLinearMap A).continuous.continuousOn) - · exact (continuous_apply i).continuousOn - · intro y hy - simp only at hy - exact (hy i hi).ne' - have f_ge : ∀ y ∈ U ∩ stdSimplex ℝ n, collatzWielandtFn A y ≤ f y := by - intro y hy - have y_supp : {i | 0 < y i}.toFinset.Nonempty := support_nonempty y hy.2 - have fn_y : collatzWielandtFn A y = {i | 0 < y i}.toFinset.inf' y_supp fun i => (A *ᵥ y) i / y i := by - unfold collatzWielandtFn - rw [dif_pos y_supp] - have supp_subset : {i | 0 < x₀ i}.toFinset ⊆ {i | 0 < y i}.toFinset := by - intro i hi - have hi' : 0 < x₀ i := by - simp only [Set.mem_toFinset] at hi - exact hi - have : 0 < y i := hy.1 i hi - simp only [Set.mem_toFinset] - exact this - rw [fn_y] - apply finset_inf'_mono_subset supp_subset - rw [fn_eq] at hc - have : f x₀ < c := hc - have cont_at : ContinuousAt f x₀ := f_cont.continuousAt (IsOpen.mem_nhds U_open x₀_in_U) - have lt_eventually : ∀ᶠ y in 𝓝 x₀, f y < c := - Filter.Tendsto.eventually_lt_const hc cont_at - rcases eventually_to_open lt_eventually with ⟨V, V_open, x₀_in_V, hV⟩ - let W := V ∩ U ∩ stdSimplex ℝ n - have VU_open : IsOpen (V ∩ U) := IsOpen.inter V_open U_open - have VU_mem : x₀ ∈ V ∩ U := ⟨x₀_in_V, x₀_in_U⟩ - have VU_nhds : V ∩ U ∈ 𝓝 x₀ := VU_open.mem_nhds VU_mem - have W_nhdsWithin : W ∈ 𝓝[stdSimplex ℝ n] x₀ := by - rw [mem_nhdsWithin_iff_exists_mem_nhds_inter] - exact ⟨V ∩ U, VU_nhds, by simp [W]⟩ - have h_final : ∀ y ∈ W, collatzWielandtFn A y < c := by - intro y hy - apply lt_of_le_of_lt - · apply f_ge y - exact ⟨And.right (And.left hy), hy.2⟩ - · exact hV y (And.left (And.left hy)) - exact Filter.mem_of_superset W_nhdsWithin (fun y hy => h_final y hy) - --- The set of vectors we are optimizing over. -def P_set := {x : n → ℝ | (∀ i, 0 ≤ x i) ∧ x ≠ 0} - -/-- The Collatz-Wielandt value is the supremum of `r_x` over P. -/ -noncomputable def r (A : Matrix n n ℝ) [Fintype n] := ⨆ x ∈ P_set, collatzWielandtFn A x - -/-- The Collatz-Wielandt function attains its maximum on the standard simplex. - [Giaquinta-Modica, Theorem 6.24 (dual), p: 235] -/ -theorem exists_maximizer (A : Matrix n n ℝ) : - ∃ v ∈ stdSimplex ℝ n, IsMaxOn (collatzWielandtFn A) (stdSimplex ℝ n) v := by - have h_compact : IsCompact (stdSimplex ℝ n) := by exact _root_.isCompact_stdSimplex ℝ n - have h_nonempty : (stdSimplex ℝ n).Nonempty := stdSimplex_nonempty - have h_usc : UpperSemicontinuousOn (collatzWielandtFn A) (stdSimplex ℝ n) := - upperSemicontinuousOn A - exact IsCompact.exists_max_on_usco h_compact h_nonempty h_usc - -lemma eq_iInf_of_nonempty - {n : Type*} [Fintype n] [Nonempty n] (A : Matrix n n ℝ) - (v : n → ℝ) (h : {i | 0 < v i}.toFinset.Nonempty) : - collatzWielandtFn A v = - ⨅ i : {i | 0 < v i}, (A *ᵥ v) i / v i := by - dsimp [collatzWielandtFn] - rw [dif_pos h] - rw [Finset.inf'_eq_ciInf h] - refine Function.Surjective.iInf_congr ?_ (fun b ↦ ?_) ?_ - intro i - · simp_all only [toFinset_setOf] - obtain ⟨val, property⟩ := i - simp_all only [toFinset_setOf, mem_filter, Finset.mem_univ, true_and] - apply Subtype.mk - · exact property - · simp_all - obtain ⟨val, property⟩ := b - simp_all only [Subtype.mk.injEq, exists_prop, exists_eq_right] - simp - -omit [Nonempty n] in -/-- If r ≤ 0 and r is the infimum of non-negative ratios, then r = 0. -/ -lemma val_eq_zero_of_nonpos [DecidableEq n] - (hA_nonneg : ∀ i j, 0 ≤ A i j) {v : n → ℝ} (hv_nonneg : ∀ i, 0 ≤ v i) - (S : Set n) (hS_def : S = {i | 0 < v i}) (hS_nonempty : S.Nonempty) - (r : ℝ) (hr_def : r = collatzWielandtFn A v) (hr_nonpos : r ≤ 0) : - r = 0 := by - have r_ge_zero : 0 ≤ r := by - rw [hr_def, collatzWielandtFn] - have hS_finset_nonempty : { j | 0 < v j }.toFinset.Nonempty := by - rwa [Set.toFinset_nonempty_iff, ← hS_def] - rw [dif_pos hS_finset_nonempty] - apply Finset.le_inf' - intro j hj - rw [Set.mem_toFinset] at hj - exact div_nonneg (Finset.sum_nonneg fun k _ => mul_nonneg (hA_nonneg j k) (hv_nonneg k)) hj.le - exact le_antisymm hr_nonpos r_ge_zero - -omit [Nonempty n] in -/-- Each ratio is at least the Collatz-Wielandt value -/ -lemma le_ratio [DecidableEq n] - (_ : ∀ i j, 0 ≤ A i j) {v : n → ℝ} (_ : ∀ i, 0 ≤ v i) - (S : Set n) (hS_def : S = {i | 0 < v i}) (hS_nonempty : S.Nonempty) - (i : n) (hi_S : i ∈ S) : collatzWielandtFn A v ≤ (A *ᵥ v) i / v i := by - rw [collatzWielandtFn] - have hS_finset_nonempty : { j | 0 < v j }.toFinset.Nonempty := by - rwa [Set.toFinset_nonempty_iff, ← hS_def] - rw [dif_pos hS_finset_nonempty] - apply Finset.inf'_le - rw [Set.mem_toFinset, ← hS_def] - exact hi_S - -omit [Nonempty n] in -/-- For any non-negative, non-zero vector `v`, the Collatz-Wielandt value `r` satisfies - `r • v ≤ A *ᵥ v`. This is the fundamental inequality derived from the definition of - the Collatz-Wielandt function. -/ -lemma le_mulVec [DecidableEq n] - (hA_nonneg : ∀ i j, 0 ≤ A i j) {v : n → ℝ} (hv_nonneg : ∀ i, 0 ≤ v i) - (hv_ne_zero : v ≠ 0) : - (collatzWielandtFn A v) • v ≤ A *ᵥ v := by - let r := collatzWielandtFn A v - have hS_nonempty : ({i | 0 < v i}).Nonempty := - exists_pos_of_ne_zero hv_nonneg hv_ne_zero - intro i - by_cases hi_supp : v i > 0 - · have h_le_div : r ≤ (A *ᵥ v) i / v i := - le_ratio hA_nonneg hv_nonneg {i | 0 < v i} rfl hS_nonempty i hi_supp - simp only [Pi.smul_apply, smul_eq_mul] - exact (le_div_iff₀ hi_supp).mp h_le_div - · have h_vi_zero : v i = 0 := by linarith [hv_nonneg i, not_lt.mp hi_supp] - simp only [Pi.smul_apply, smul_eq_mul, h_vi_zero, mul_zero] - exact mulVec_nonneg hA_nonneg hv_nonneg i - -omit [Fintype n] [Nonempty n] in -/-- If the Collatz-Wielandt value `r` is non-positive, there must be some `i` in the support of `v` - where the ratio, and thus `(A * v) i`, is zero. -/ -lemma exists_mulVec_eq_zero_on_support_of_nonpos [Fintype n] - (hA_nonneg : ∀ i j, 0 ≤ A i j) {v : n → ℝ} (hv_nonneg : ∀ i, 0 ≤ v i) - (h_supp_nonempty : {i | 0 < v i}.toFinset.Nonempty) - (h_r_nonpos : collatzWielandtFn A v ≤ 0) : - ∃ i ∈ {i | 0 < v i}.toFinset, (A *ᵥ v) i = 0 := by - have r_nonneg : 0 ≤ collatzWielandtFn A v := by - rw [collatzWielandtFn, dif_pos h_supp_nonempty] - apply Finset.le_inf' - intro i hi_mem - exact div_nonneg (mulVec_nonneg hA_nonneg hv_nonneg i) (by exact hv_nonneg i) - have r_eq_zero : collatzWielandtFn A v = 0 := le_antisymm h_r_nonpos r_nonneg - rw [collatzWielandtFn, dif_pos h_supp_nonempty] at r_eq_zero - obtain ⟨b, hb_mem, hb_eq⟩ := Finset.exists_mem_eq_inf' h_supp_nonempty (fun i => (A *ᵥ v) i / v i) - have h_ratio_zero : (A *ᵥ v) b / v b = 0 := by rw [← hb_eq, r_eq_zero] - have h_vb_pos : 0 < v b := by simpa [Set.mem_toFinset] using hb_mem - grind - -omit [Nonempty n] in -lemma le_any_ratio [DecidableEq n] (A : Matrix n n ℝ) - {x : n → ℝ} (hx_nonneg : ∀ i, 0 ≤ x i) (hx_ne_zero : x ≠ 0) - (i : n) (hi_pos : 0 < x i) : - collatzWielandtFn A x ≤ (A *ᵥ x) i / x i := by - dsimp [collatzWielandtFn] - have h_supp_nonempty : ({k | 0 < x k}.toFinset).Nonempty := by - rw [Set.toFinset_nonempty_iff, Set.nonempty_def] - obtain ⟨j, hj_ne_zero⟩ := Function.exists_ne_zero_of_ne_zero hx_ne_zero - exact ⟨j, lt_of_le_of_ne (hx_nonneg j) hj_ne_zero.symm⟩ - rw [dif_pos h_supp_nonempty] - apply Finset.inf'_le - rw [Set.mem_toFinset] - exact hi_pos - -/-- The set of values from the Collatz-Wielandt function is bounded above by the maximum row sum of A. -/ -lemma bddAbove [DecidableEq n] (A : Matrix n n ℝ) (hA_nonneg : ∀ i j, 0 ≤ A i j) : - BddAbove (collatzWielandtFn A '' {x | (∀ i, 0 ≤ x i) ∧ x ≠ 0}) := by - use Finset.univ.sup' Finset.univ_nonempty (fun i ↦ ∑ j, A i j) - rintro y ⟨x, ⟨hx_nonneg, hx_ne_zero⟩, rfl⟩ - obtain ⟨m, _, h_max_eq⟩ := Finset.exists_mem_eq_sup' Finset.univ_nonempty x - have h_xm_pos : 0 < x m := by - obtain ⟨i, hi_pos⟩ : ∃ i, 0 < x i := by - obtain ⟨j, hj⟩ := Function.exists_ne_zero_of_ne_zero hx_ne_zero - exact ⟨j, lt_of_le_of_ne (hx_nonneg j) hj.symm⟩ - rw [← h_max_eq] - exact lt_of_lt_of_le hi_pos (le_sup' x (Finset.mem_univ i)) - have h_le_ratio : collatzWielandtFn A x ≤ (A *ᵥ x) m / x m := - le_any_ratio A hx_nonneg hx_ne_zero m h_xm_pos - have h_ratio_le : (A *ᵥ x) m / x m ≤ Finset.univ.sup' Finset.univ_nonempty (fun k ↦ ∑ l, A k l) := by - rw [mulVec_apply, div_le_iff h_xm_pos] - calc - ∑ j, A m j * x j - ≤ ∑ j, A m j * x m := by - apply Finset.sum_le_sum; intro j _; exact mul_le_mul_of_nonneg_left (by rw [← h_max_eq]; exact le_sup' x (Finset.mem_univ j)) (hA_nonneg m j) - _ = (∑ j, A m j) * x m := by rw [Finset.sum_mul] - _ ≤ (Finset.univ.sup' Finset.univ_nonempty (fun k ↦ ∑ l, A k l)) * x m := by - apply mul_le_mul_of_nonneg_right - · exact le_sup' (fun k => ∑ l, A k l) (Finset.mem_univ m) - · exact le_of_lt h_xm_pos - exact le_trans h_le_ratio h_ratio_le - -/-- The set of values from the Collatz-Wielandt function is non-empty. -/ -lemma set_nonempty : - (collatzWielandtFn A '' {x | (∀ i, 0 ≤ x i) ∧ x ≠ 0}).Nonempty := by - let P_set := {x : n → ℝ | (∀ i, 0 ≤ x i) ∧ x ≠ 0} - let x_ones : n → ℝ := fun _ ↦ 1 - have h_x_ones_in_set : x_ones ∈ P_set := by - constructor - · intro i; exact zero_le_one - · intro h_zero - have h_contra : (1 : ℝ) = 0 := by simpa [x_ones] using congr_fun h_zero (Classical.arbitrary n) - exact one_ne_zero h_contra - exact Set.Nonempty.image _ ⟨x_ones, h_x_ones_in_set⟩ - -omit [Fintype n] [Nonempty n] in -lemma smul [Fintype n] [Nonempty n] [DecidableEq n] {c : ℝ} (hc : 0 < c) (_ : ∀ i j, 0 ≤ A i j) - {x : n → ℝ} (hx_nonneg : ∀ i, 0 ≤ x i) (hx_ne : x ≠ 0) : - collatzWielandtFn A (c • x) = collatzWielandtFn A x := by - dsimp [collatzWielandtFn] - let S := {i | 0 < x i}.toFinset - obtain ⟨i₀, hi₀⟩ := exists_pos_of_ne_zero hx_nonneg hx_ne - have hS_nonempty : S.Nonempty := ⟨i₀, by simp [S, hi₀]⟩ - have h_supp_eq : {i | 0 < (c • x) i}.toFinset = S := by - ext i - simp [S, smul_eq_mul, mul_pos_iff_of_pos_left hc] - rw [dif_pos (h_supp_eq.symm ▸ hS_nonempty), dif_pos hS_nonempty] - refine inf'_congr (Eq.symm h_supp_eq ▸ hS_nonempty) h_supp_eq ?_ - intro i hi - simp only [mulVec_smul, smul_eq_mul, Pi.smul_apply] - rw [mul_div_mul_left _ _ (ne_of_gt hc)] - -/-- The minimum ratio `(Ax)_i / x_i` for a positive vector `x`. -/ -noncomputable def minRatio (A : Matrix n n ℝ) (x : n → ℝ) : ℝ := - ⨅ i, (A.mulVec x i) / x i - -/-- The maximum ratio `(Ax)_i / x_i` for a positive vector `x`. -/ -noncomputable def maxRatio (A : Matrix n n ℝ) (x : n → ℝ) : ℝ := - ⨆ i, (A.mulVec x i) / x i - -/-- The Collatz-Wielandt formula for the Perron root, defined as a supremum of infima. -/ -noncomputable def perronRoot (A : Matrix n n ℝ) : ℝ := - ⨆ (x : n → ℝ) (_ : ∀ i, 0 < x i), minRatio A x - -/-- The Collatz-Wielandt formula for the Perron root, defined as an infimum of suprema. -/ -noncomputable def perronRoot' (A : Matrix n n ℝ) : ℝ := - ⨅ (x : n → ℝ) (_ : ∀ i, 0 < x i), maxRatio A x - -/-- The Perron root, as the supremum of the Collatz-Wielandt function (see Seneta). -/ -noncomputable def perronRoot_alt (A : Matrix n n ℝ) : ℝ := - sSup (collatzWielandtFn A '' P_set) - -omit [Nonempty n] in -lemma minRatio_le_maxRatio (A : Matrix n n ℝ) (x : n → ℝ) : - minRatio A x ≤ maxRatio A x := by - cases isEmpty_or_nonempty n - · simp [minRatio, maxRatio] - · haveI : Nonempty n := inferInstance - exact ciInf_le_ciSup (Set.finite_range _).bddBelow (Set.finite_range _).bddAbove - -omit [Nonempty n] in --- Auxiliary lemma: the sets used in sSup and sInf are nonempty -lemma min_max_sets_nonempty [Nonempty n] (A : Matrix n n ℝ) : - ({r | ∃ x : n → ℝ, (∀ i, 0 < x i) ∧ r = minRatio A x}.Nonempty) ∧ - ({r | ∃ x : n → ℝ, (∀ i, 0 < x i) ∧ r = maxRatio A x}.Nonempty) := by - constructor - · use minRatio A (fun _ => 1) - use fun _ => 1 - constructor - · intro i; exact zero_lt_one - · rfl - · use maxRatio A (fun _ => 1) - use fun _ => 1 - constructor - · intro i; exact zero_lt_one - · rfl - -omit [Nonempty n] in --- Auxiliary lemma: for any minimum ratio, there exists a maximum ratio that's greater -lemma forall_exists_min_le_max [Nonempty n] (A : Matrix n n ℝ) : - ∀ r ∈ {r | ∃ x : n → ℝ, (∀ i, 0 < x i) ∧ r = minRatio A x}, - ∃ s ∈ {s | ∃ y : n → ℝ, (∀ i, 0 < y i) ∧ s = maxRatio A y}, r ≤ s := by - intro r hr - rcases hr with ⟨x, hx_pos, hr_eq⟩ - use maxRatio A x - constructor - · use x - · rw [hr_eq] - exact minRatio_le_maxRatio A x - -theorem eq_eigenvalue_of_positive_eigenvector - {n : Type*} [Fintype n] [DecidableEq n] [Nonempty n] - {A : Matrix n n ℝ} {r : ℝ} {v : n → ℝ} - (hv_pos : ∀ i, 0 < v i) (h_eig : A *ᵥ v = r • v) : - collatzWielandtFn A v = r := by - dsimp [collatzWielandtFn] - have h_supp_nonempty : ({i | 0 < v i}.toFinset).Nonempty := by - let i0 := Classical.arbitrary n - simp - simp_all only [Finset.filter_true, Finset.univ_nonempty] - rw [dif_pos h_supp_nonempty] - apply Finset.inf'_eq_of_forall_le_of_exists_le h_supp_nonempty - · intro i hi - let hi_pos := Set.mem_toFinset.mp hi - have : (A *ᵥ v) i = (r • v) i := by rw [h_eig] - rw [Pi.smul_apply, smul_eq_mul] at this - have : (A *ᵥ v) i / v i = r := by - rw [this]; rw [mul_div_cancel_pos_right rfl (hv_pos i)] - rw [this] - · use h_supp_nonempty.choose - use h_supp_nonempty.choose_spec - let hi_pos := Set.mem_toFinset.mp h_supp_nonempty.choose_spec - have : (A *ᵥ v) h_supp_nonempty.choose = (r • v) h_supp_nonempty.choose := by rw [h_eig] - rw [Pi.smul_apply, smul_eq_mul] at this - rw [this]; rw [mul_div_cancel_pos_right rfl (hv_pos (Exists.choose h_supp_nonempty))] - -lemma bddAbove_image_P_set [DecidableEq n] (A : Matrix n n ℝ) (hA_nonneg : ∀ i j, 0 ≤ A i j) : - BddAbove (collatzWielandtFn A '' {x | (∀ i, 0 ≤ x i) ∧ x ≠ 0}) := by - use Finset.univ.sup' Finset.univ_nonempty (fun i ↦ ∑ j, A i j) - rintro _ ⟨x, ⟨hx_nonneg, hx_ne_zero⟩, rfl⟩ - obtain ⟨m, _, h_max_eq⟩ := Finset.exists_mem_eq_sup' Finset.univ_nonempty x - have h_xm_pos : 0 < x m := by - obtain ⟨i, hi_pos⟩ : ∃ i, 0 < x i := by - obtain ⟨j, hj⟩ := Function.exists_ne_zero_of_ne_zero hx_ne_zero - exact ⟨j, lt_of_le_of_ne (hx_nonneg j) hj.symm⟩ - rw [← h_max_eq] - exact lt_of_lt_of_le hi_pos (le_sup' x (Finset.mem_univ i)) - have h_le_ratio : collatzWielandtFn A x ≤ (A *ᵥ x) m / x m := - CollatzWielandt.le_any_ratio A hx_nonneg hx_ne_zero m h_xm_pos - have h_ratio_le : (A *ᵥ x) m / x m ≤ Finset.univ.sup' Finset.univ_nonempty (fun k ↦ ∑ l, A k l) := by - rw [mulVec_apply, div_le_iff h_xm_pos] - calc - ∑ j, A m j * x j - ≤ ∑ j, A m j * x m := by - apply Finset.sum_le_sum; intro j _; exact mul_le_mul_of_nonneg_left (by rw [← h_max_eq]; exact le_sup' x (Finset.mem_univ j)) (hA_nonneg m j) - _ = (∑ j, A m j) * x m := by rw [Finset.sum_mul] - _ ≤ (Finset.univ.sup' Finset.univ_nonempty (fun k ↦ ∑ l, A k l)) * x m := by - apply mul_le_mul_of_nonneg_right - · exact le_sup' (fun k => ∑ l, A k l) (Finset.mem_univ m) - · exact le_of_lt h_xm_pos - exact le_trans h_le_ratio h_ratio_le - -variable {n : Type*} [Fintype n]--[Nonempty n] --[DecidableEq n] -variable {A : Matrix n n ℝ} - -/-- Any eigenvalue with a strictly positive eigenvector is ≤ the Perron root. -/ -theorem eigenvalue_le_perron_root_of_positive_eigenvector - {r : ℝ} {v : n → ℝ} - [Nonempty n] [DecidableEq n] - (hA_nonneg : ∀ i j, 0 ≤ A i j) (_ : 0 < r) - (hv_pos : ∀ i, 0 < v i) (h_eig : A *ᵥ v = r • v) : - r ≤ perronRoot_alt A := by - have hv_nonneg : ∀ i, 0 ≤ v i := fun i ↦ (hv_pos i).le - have hv_ne_zero : v ≠ 0 := by - intro h - have hcontr : (0 : ℝ) < 0 := by - have hpos := hv_pos (Classical.arbitrary n) - simp [h] at hpos - exact (lt_irrefl _ hcontr).elim - have h_r : r = collatzWielandtFn A v := - (eq_eigenvalue_of_positive_eigenvector hv_pos h_eig).symm - have h_le : collatzWielandtFn A v ≤ perronRoot_alt A := by - dsimp [perronRoot_alt] - have h_bdd : BddAbove (collatzWielandtFn A '' P_set) := - bddAbove_image_P_set A hA_nonneg - apply le_csSup h_bdd - have hv_in_P : v ∈ P_set := ⟨hv_nonneg, hv_ne_zero⟩ - exact Set.mem_image_of_mem (collatzWielandtFn A) hv_in_P - simpa [h_r] using h_le - -/-- A left eigenvector of the matrix is a right eigenvector of its transpose -/ -lemma left_eigenvector_of_transpose {r : ℝ} {u : n → ℝ} - (hu_left : u ᵥ* A = r • u) : - Aᵀ *ᵥ u = r • u := by - rwa [← vecMul_eq_mulVec_transpose] - -/-- For any non-negative vector `w`, its Collatz–Wielandt value … -/ -lemma le_eigenvalue_of_left_eigenvector [DecidableEq n] - (hA_nonneg : ∀ i j, 0 ≤ A i j) {r : ℝ} (_ : 0 < r) - {u : n → ℝ} (hu_pos : ∀ i, 0 < u i) (h_eig : u ᵥ* A = r • u) - {w : n → ℝ} (hw_nonneg : ∀ i, 0 ≤ w i) (hw_ne_zero : w ≠ 0) : - collatzWielandtFn A w ≤ r := by - classical - have hu_nonneg : ∀ i, 0 ≤ u i := fun i ↦ (hu_pos i).le - -- Pointwise inequality (λr w ≤ A·w) - have h_le_mulVec := CollatzWielandt.le_mulVec hA_nonneg hw_nonneg hw_ne_zero - -- Multiply componentwise by u i ≥ 0 and sum to get a dot-product inequality - have h_intermediate : - u ⬝ᵥ ((collatzWielandtFn A w) • w) ≤ u ⬝ᵥ (A *ᵥ w) := by - unfold dotProduct - apply Finset.sum_le_sum - intro i _ - have hi := h_le_mulVec i - -- hi : ((collatzWielandtFn A w) • w) i ≤ (A *ᵥ w) i - -- Rewrite and multiply by u i ≥ 0 - simp [Pi.smul_apply, smul_eq_mul] at hi ⊢ - exact mul_le_mul_of_nonneg_left hi (hu_nonneg i) - -- Rewrite both sides using eigenvector relations - have h_right : - u ⬝ᵥ (A *ᵥ w) = r * (u ⬝ᵥ w) := by - -- u ⬝ (A w) = (uᵀ A) ⬝ w = (r • u) ⬝ w = r * (u ⬝ w) - have := dotProduct_mulVec u A w - -- this : u ⬝ᵥ (A *ᵥ w) = u ᵥ* A ⬝ᵥ w - calc - u ⬝ᵥ (A *ᵥ w) = (u ᵥ* A) ⬝ᵥ w := this - _ = (r • u) ⬝ᵥ w := by simp [h_eig] - _ = r * (u ⬝ᵥ w) := by simp [smul_eq_mul] - have h_left : - u ⬝ᵥ ((collatzWielandtFn A w) • w) - = (collatzWielandtFn A w) * (u ⬝ᵥ w) := by - simp [dotProduct_smul, smul_eq_mul] - -- Combine - have h_dot_le : - (collatzWielandtFn A w) * (u ⬝ᵥ w) ≤ r * (u ⬝ᵥ w) := by - simpa [h_left, h_right] using h_intermediate - -- Since u ⬝ w > 0 we can divide - have h_dot_pos : 0 < u ⬝ᵥ w := - dotProduct_pos_of_pos_of_nonneg_ne_zero hu_pos hw_nonneg hw_ne_zero - exact le_of_mul_le_mul_right h_dot_le h_dot_pos - -/-- If v is an eigenvector of A with eigenvalue r (i.e., A *ᵥ v = r • v), - this lemma provides the relation in the form needed for rewriting. -/ -lemma mulVec_eq_smul_of_eigenvector {n : Type*} [Fintype n] [DecidableEq n] - {A : Matrix n n ℝ} {r : ℝ} {v : n → ℝ} (h_eig : A *ᵥ v = r • v) : - r • v = A *ᵥ v := by - exact Eq.symm h_eig - -/-- -If `u` is a strictly positive left eigenvector of `A` for eigenvalue `r > 0`, -then the Perron root of `A` is less than or equal to `r`. -That is, `perronRoot_alt A ≤ r`. --/ -lemma perron_root_le_eigenvalue_of_left_eigenvector [Nonempty n] [DecidableEq n] - (hA_nonneg : ∀ i j, 0 ≤ A i j) {r : ℝ} (hr_pos : 0 < r) {u : n → ℝ} (hu_pos : ∀ i, 0 < u i) - (h_eig : u ᵥ* A = r • u) : - perronRoot_alt A ≤ r := by - dsimp [perronRoot_alt] - apply csSup_le - · exact CollatzWielandt.set_nonempty - · rintro _ ⟨w, ⟨hw_nonneg, hw_ne_zero⟩, rfl⟩ - exact CollatzWielandt.le_eigenvalue_of_left_eigenvector hA_nonneg hr_pos hu_pos h_eig hw_nonneg hw_ne_zero - -/-- -An intermediate algebraic result for the Perron-Frobenius theorem. -If `v` is a right eigenvector of `A` for eigenvalue `r`, then for any vector `w`, -the dot product `v ⬝ᵥ (A *ᵥ w)` is equal to `r * (v ⬝ᵥ w)`. --/ -lemma dotProduct_mulVec_eq_eigenvalue_mul_dotProduct - {r : ℝ} {v w : n → ℝ} (h_eig : Aᵀ *ᵥ v = r • v) : - v ⬝ᵥ (A *ᵥ w) = r * (v ⬝ᵥ w) := by - have : v ⬝ᵥ (A *ᵥ w) = v ᵥ* A ⬝ᵥ w := by exact dotProduct_mulVec v A w - rw [this] - have : v ᵥ* A = Aᵀ *ᵥ v := by exact vecMul_eq_mulVec_transpose A v - rw [this] - rw [h_eig] - exact dotProduct_smul_left r v w - -/-- -If `v` is a strictly positive right eigenvector of `A` with eigenvalue `r`, then the vector -of all ones is a right eigenvector of the similarity-transformed matrix `B = D⁻¹AD` -(where `D` is `diagonal v`) with the same eigenvalue `r`. --/ -lemma ones_eigenvector_of_similarity_transform [DecidableEq n] - {A : Matrix n n ℝ} {r : ℝ} {v : n → ℝ} - (hv_pos : ∀ i, 0 < v i) (h_eig : A *ᵥ v = r • v) : - (diagonal (v⁻¹) * A * diagonal v) *ᵥ (fun _ => 1) = fun _ => r := by - let D := diagonal v - let D_inv := diagonal (v⁻¹) - let B := D_inv * A * D - let ones := fun _ : n => (1 : ℝ) - calc - B *ᵥ ones - = D_inv *ᵥ (A *ᵥ (D *ᵥ ones)) := by - unfold B - rw [← mulVec_mulVec, ← mulVec_mulVec] - _ = D_inv *ᵥ (A *ᵥ v) := by - have h_D_ones : D *ᵥ ones = v := by - unfold D ones - exact diagonal_mulVec_ones v - rw [h_D_ones] - _ = D_inv *ᵥ (r • v) := by rw [h_eig] - _ = r • (D_inv *ᵥ v) := by rw [mulVec_smul] - _ = r • ones := by - have h_D_inv_v : D_inv *ᵥ v = ones := by - unfold D_inv ones - have hv_ne_zero : ∀ i, v i ≠ 0 := fun i => (hv_pos i).ne' - exact diagonal_inv_mulVec_self hv_ne_zero - rw [h_D_inv_v] - _ = fun _ => r := by - ext x - simp only [Pi.smul_apply, smul_eq_mul, mul_one, ones] - -/-- -If `v` is a strictly positive right eigenvector of `A` with eigenvalue `r`, then the -similarity-transformed matrix `B = D⁻¹AD` (where `D` is `diagonal v`) has row sums equal to `r`. --/ -lemma row_sum_of_similarity_transformed_matrix [DecidableEq n] [Nonempty n] - {A : Matrix n n ℝ} {r : ℝ} {v : n → ℝ} - (hv_pos : ∀ i, 0 < v i) (h_eig : A *ᵥ v = r • v) : - ∀ i, ∑ j, (Matrix.diagonal (v⁻¹) * A * Matrix.diagonal v) i j = r := by - intro i - let B := Matrix.diagonal (v⁻¹) * A * Matrix.diagonal v - have row_sum_eq : ∑ j, B i j = (B *ᵥ (fun _ => 1)) i := by - simp only [mulVec_apply, mul_one] - rw [row_sum_eq] - have h_B_eig := ones_eigenvector_of_similarity_transform hv_pos h_eig - rw [h_B_eig] - -/-- -If a non-negative vector `x` satisfies `c • x ≤ B *ᵥ x` for a non-negative matrix `B` -whose row sums are all equal to `r`, then `c ≤ r`. --/ -lemma le_of_max_le_row_sum [Nonempty n] [DecidableEq n] - {B : Matrix n n ℝ} {x : n → ℝ} {c r : ℝ} - (hB_nonneg : ∀ i j, 0 ≤ B i j) (h_B_row_sum : ∀ i, ∑ j, B i j = r) - (hx_nonneg : ∀ i, 0 ≤ x i) (hx_ne_zero : x ≠ 0) (h_le_Bx : c • x ≤ B *ᵥ x) : - c ≤ r := by - obtain ⟨k, -, h_k_max⟩ := Finset.exists_mem_eq_sup' (Finset.univ_nonempty) x - have h_xk_pos : 0 < x k := by - have h_exists_pos : ∃ i, 0 < x i := exists_pos_of_ne_zero hx_nonneg hx_ne_zero - obtain ⟨j, hj_pos⟩ := h_exists_pos - refine' lt_of_lt_of_le hj_pos _ - rw [← h_k_max] - exact Finset.le_sup' (f := x) (Finset.mem_univ j) - have h_le_k := h_le_Bx k - simp only [Pi.smul_apply, smul_eq_mul] at h_le_k - have h_Bx_le : (B *ᵥ x) k ≤ r * x k := by - calc (B *ᵥ x) k - = ∑ j, B k j * x j := by simp [mulVec_apply] - _ ≤ ∑ j, B k j * x k := by - apply Finset.sum_le_sum; intro j hj - exact mul_le_mul_of_nonneg_left (by { rw [← h_k_max]; exact Finset.le_sup' x hj }) (hB_nonneg k j) - _ = (∑ j, B k j) * x k := by rw [Finset.sum_mul] - _ = r * x k := by rw [h_B_row_sum] - exact le_of_mul_le_mul_right (le_trans h_le_k h_Bx_le) h_xk_pos - -/-- -For any non-negative vector `w`, its Collatz–Wielandt value is bounded above by a -positive eigenvalue `r` that has a strictly positive *right* eigenvector `v`. --/ -theorem le_eigenvalue_of_right_eigenvector [Nonempty n] [DecidableEq n] - (hA_nonneg : ∀ i j, 0 ≤ A i j) {r : ℝ} (_ : 0 < r) - {v : n → ℝ} (hv_pos : ∀ i, 0 < v i) (h_eig : A *ᵥ v = r • v) - {w : n → ℝ} (hw_nonneg : ∀ i, 0 ≤ w i) (hw_ne_zero : w ≠ 0) : - collatzWielandtFn A w ≤ r := by - let D := Matrix.diagonal v - let D_inv := Matrix.diagonal (v⁻¹) - let B := D_inv * A * D - have hB_nonneg : ∀ i j, 0 ≤ B i j := by - intro i j - unfold B D D_inv - simp only [mul_apply] - apply Finset.sum_nonneg - intro k _ - simp only [diagonal_apply] - by_cases hik : i = k - · by_cases hkj : k = j - · simp [hik, hkj] - subst hkj hik - simp_all only [ne_eq, Finset.mem_univ, mul_nonneg_iff_of_pos_right, inv_pos, mul_nonneg_iff_of_pos_left] - · simp [hik, hkj] - · simp_all only [ne_eq, Finset.mem_univ, Pi.inv_apply, ite_mul, zero_mul, sum_ite_eq, ↓reduceIte, mul_ite, mul_zero] - split - next h => - subst h - simp_all only [mul_nonneg_iff_of_pos_right, inv_pos, mul_nonneg_iff_of_pos_left] - next h => simp_all only [le_refl] - have h_B_row_sum := row_sum_of_similarity_transformed_matrix hv_pos h_eig - let x := D_inv *ᵥ w - have hx_nonneg : ∀ i, 0 ≤ x i := by - intro i - unfold x D_inv - rw [mulVec_diagonal] - exact mul_nonneg (inv_nonneg.mpr (hv_pos i).le) (hw_nonneg i) - have hx_ne_zero : x ≠ 0 := by - contrapose! hw_ne_zero - have h_w_eq_Dx : w = D *ᵥ x := by - unfold x D D_inv - ext i - simp only [mulVec_diagonal, mulVec_diagonal] - have hv_ne_zero : v i ≠ 0 := (hv_pos i).ne' - simp_all only [mul_diagonal, diagonal_mul, Pi.inv_apply, mul_nonneg_iff_of_pos_right, inv_pos, - mul_nonneg_iff_of_pos_left, implies_true, ne_eq, isUnit_iff_ne_zero, not_false_eq_true, - IsUnit.mul_inv_cancel_left, B, D_inv, D, x] - rw [h_w_eq_Dx, hw_ne_zero, mulVec_zero] - have h_le_Bx : (collatzWielandtFn A w) • x ≤ B *ᵥ x := by - have h_le_mulVec := CollatzWielandt.le_mulVec hA_nonneg hw_nonneg hw_ne_zero - have h_w_eq_Dx : w = D *ᵥ x := by - unfold x D D_inv - ext i - simp only [mulVec_diagonal, mulVec_diagonal] - have hv_ne_zero : v i ≠ 0 := (hv_pos i).ne' - simp_all only [ne_eq, mul_diagonal, diagonal_mul, Pi.inv_apply, mul_nonneg_iff_of_pos_right, inv_pos, - mul_nonneg_iff_of_pos_left, implies_true, isUnit_iff_ne_zero, not_false_eq_true, IsUnit.mul_inv_cancel_left, - B, D_inv, D, x] - have h_smul_le : (collatzWielandtFn A w) • w ≤ A *ᵥ w := h_le_mulVec - have h1 : (collatzWielandtFn A w) • x = D_inv *ᵥ ((collatzWielandtFn A w) • w) := by - rw [← mulVec_smul, h_w_eq_Dx] - have h2 : D_inv *ᵥ (A *ᵥ w) = D_inv *ᵥ (A *ᵥ (D *ᵥ x)) := by - rw [h_w_eq_Dx] - have h3 : D_inv *ᵥ (A *ᵥ (D *ᵥ x)) = (D_inv * A * D) *ᵥ x := by - rw [← mulVec_mulVec, ← mulVec_mulVec] - rw [h1] - have h_Dinv_nonneg : ∀ i j, 0 ≤ D_inv i j := by - intro i j - unfold D_inv - rw [diagonal_apply] - by_cases hij : i = j - · simp only [hij, ↓reduceIte, Pi.inv_apply, inv_nonneg] - exact le_of_lt (hv_pos j) - · simp only [hij, ↓reduceIte, le_refl] - intro i - have h_comp_le : ((collatzWielandtFn A w) • w) i ≤ (A *ᵥ w) i := h_smul_le i - have h_mulVec_mono : (D_inv *ᵥ ((collatzWielandtFn A w) • w)) i ≤ (D_inv *ᵥ (A *ᵥ w)) i := by - simp only [mulVec_apply] - apply Finset.sum_le_sum - intro j _ - exact mul_le_mul_of_nonneg_left (h_le_mulVec j) (h_Dinv_nonneg i j) - calc (D_inv *ᵥ (collatzWielandtFn A w • w)) i - ≤ (D_inv *ᵥ (A *ᵥ w)) i := h_mulVec_mono - _ = (D_inv *ᵥ (A *ᵥ (D *ᵥ x))) i := by rw [h_w_eq_Dx] - _ = ((D_inv * A * D) *ᵥ x) i := by rw [← mulVec_mulVec, ← mulVec_mulVec] - _ = (B *ᵥ x) i := rfl - exact le_of_max_le_row_sum hB_nonneg h_B_row_sum hx_nonneg hx_ne_zero h_le_Bx - -/- Any positive eigenvalue `r` with a strictly positive right eigenvector `v` is an -upper bound for the range of the Collatz-Wielandt function. --/ -theorem eigenvalue_is_ub_of_positive_eigenvector [Nonempty n] [DecidableEq n] - (hA_nonneg : ∀ i j, 0 ≤ A i j) {r : ℝ} (hr_pos : 0 < r) - {v : n → ℝ} (hv_pos : ∀ i, 0 < v i) (h_eig : A *ᵥ v = r • v) : - perronRoot_alt A ≤ r := by - dsimp [perronRoot_alt] - apply csSup_le (CollatzWielandt.set_nonempty (A := A)) - rintro _ ⟨w, ⟨hw_nonneg, hw_ne_zero⟩, rfl⟩ - exact CollatzWielandt.le_eigenvalue_of_right_eigenvector - hA_nonneg hr_pos hv_pos h_eig hw_nonneg hw_ne_zero - -theorem eq_perron_root_of_positive_eigenvector - [Nonempty n] [DecidableEq n] - {A : Matrix n n ℝ} {r : ℝ} {v : n → ℝ} - (hA_nonneg : ∀ i j, 0 ≤ A i j) - (hv_pos : ∀ i, 0 < v i) - (hr_pos : 0 < r) - (h_eig : A *ᵥ v = r • v) : - r = CollatzWielandt.perronRoot_alt (A := A) := by - -- 1. `r ≤ perronRoot_alt A`. - have h₁ : r ≤ CollatzWielandt.perronRoot_alt (A := A) := - CollatzWielandt.eigenvalue_le_perron_root_of_positive_eigenvector - (A := A) hA_nonneg hr_pos hv_pos h_eig - -- 2. `perronRoot_alt A ≤ r`. - have h₂ : CollatzWielandt.perronRoot_alt (A := A) ≤ r := - CollatzWielandt.eigenvalue_is_ub_of_positive_eigenvector - hA_nonneg hr_pos hv_pos h_eig - exact le_antisymm h₁ h₂ - -lemma perronRoot'_le_maxRatio_of_min_ge_perronRoot' - [Nonempty n] {A : Matrix n n ℝ} {x : n → ℝ} - (hr : perronRoot' A ≤ minRatio A x) : - perronRoot' A ≤ maxRatio A x := - hr.trans (minRatio_le_maxRatio A x) - -/-- -For a function `f` on a non-empty finite type `ι`, the indexed infimum `⨅ i, f i` is equal -to the infimum over the universal finset. --/ -lemma ciInf_eq_finset_inf' {α ι : Type*} [Fintype ι] [Nonempty ι] [ConditionallyCompleteLinearOrder α] - (f : ι → α) : - ⨅ i, f i = Finset.univ.inf' Finset.univ_nonempty f := by - -- This is the symmetric version of `Finset.inf'_univ_eq_ciInf`. - exact (Finset.inf'_univ_eq_ciInf f).symm - -@[simp] -theorem Finset.sum_def {α M : Type*} [AddCommMonoid M] {s : Finset α} (f : α → M) : - (∑ i ∈ s, f i) = s.sum f := -rfl - -/-- A finite sum of non-negative terms is strictly positive as soon as one - summand is strictly positive. -/ -lemma Finset.sum_pos_of_nonneg_of_exists_pos {α β : Type*} - [AddCommMonoid β] [PartialOrder β] [IsOrderedCancelAddMonoid β] - {s : Finset α} {f : α → β} - (h_nonneg : ∀ i ∈ s, 0 ≤ f i) - (h_exists : ∃ i ∈ s, 0 < f i) : - 0 < ∑ i ∈ s, f i := - Finset.sum_pos' h_nonneg h_exists - -omit [Fintype n] in -lemma maximizer_satisfies_le_mulVec - [Fintype n] [Nonempty n] [DecidableEq n] - (A : Matrix n n ℝ) (hA_nonneg : ∀ i j, 0 ≤ A i j) : - let r := perronRoot_alt A - ∃ v ∈ stdSimplex ℝ n, r • v ≤ A *ᵥ v := by - let r := perronRoot_alt A - obtain ⟨v, v_in_simplex, v_is_max⟩ := exists_maximizer (A := A) - have v_ne_zero : v ≠ 0 := by - intro hv - have h_sum_one : (∑ i, v i) = 1 := v_in_simplex.2 - rw [hv] at h_sum_one - simp only [Pi.zero_apply, Finset.sum_const_zero] at h_sum_one - norm_num at h_sum_one - have v_nonneg : ∀ i, 0 ≤ v i := v_in_simplex.1 - have r_eq : (perronRoot_alt A) = collatzWielandtFn A v := by - dsimp [perronRoot_alt] - apply le_antisymm - · -- `perronRoot_alt A ≤ collatzWielandtFn A v` - apply csSup_le set_nonempty - rintro _ ⟨x, ⟨hx_nonneg, hx_ne_zero⟩, rfl⟩ - set s : ℝ := ∑ i, x i with hs - have s_pos : 0 < s := by - obtain ⟨i, hi⟩ := exists_pos_of_ne_zero hx_nonneg hx_ne_zero - have : 0 < ∑ i, x i := - Finset.sum_pos_of_nonneg_of_exists_pos - (λ j _ ↦ hx_nonneg j) - ⟨i, Finset.mem_univ _, hi⟩ - simpa [hs] using this - set x' : n → ℝ := s⁻¹ • x with hx' - have hx'_in_simplex : x' ∈ stdSimplex ℝ n := by - -- Positivity - constructor - · intro i - have : 0 ≤ s⁻¹ := inv_nonneg.2 s_pos.le - have : 0 ≤ s⁻¹ * x i := mul_nonneg this (hx_nonneg i) - simpa [hx'] using this - -- Sum = 1 - · have : (∑ i, x' i) = 1 := by - simp only [hx', Pi.smul_apply, smul_eq_mul, ← Finset.mul_sum, ← hs] - field_simp [ne_of_gt s_pos] - exact this - -- Maximality of `v` - have h_max : collatzWielandtFn A x' ≤ collatzWielandtFn A v := - v_is_max hx'_in_simplex - -- Scale invariance - have h_scale : collatzWielandtFn A x = collatzWielandtFn A x' := by - have h_smul := smul (inv_pos.mpr s_pos) hA_nonneg hx_nonneg hx_ne_zero - rw [← hx'] at h_smul - exact h_smul.symm - rwa [h_scale] - · -- `collatzWielandtFn A v ≤ perronRoot_alt A` - apply le_csSup (bddAbove_image_P_set A hA_nonneg) - exact Set.mem_image_of_mem _ ⟨v_nonneg, v_ne_zero⟩ - have h_le : (perronRoot_alt A) • v ≤ A *ᵥ v := by - have : (collatzWielandtFn A v) • v ≤ A *ᵥ v := - le_mulVec hA_nonneg v_nonneg v_ne_zero - simpa [r_eq] using this - refine ⟨v, v_in_simplex, ?_⟩ - simpa [r] using h_le - -/-- -The conditional supremum of a non-empty, bounded above set of non-negative numbers is non-negative. --/ -lemma csSup_nonneg {s : Set ℝ} (hs_nonempty : s.Nonempty) (hs_bdd : BddAbove s) - (hs_nonneg : ∀ x ∈ s, 0 ≤ x) : - 0 ≤ sSup s := by - obtain ⟨y, hy_mem⟩ := hs_nonempty - have h_y_nonneg : 0 ≤ y := hs_nonneg y hy_mem - have h_y_le_sSup : y ≤ sSup s := le_csSup hs_bdd hy_mem - exact le_trans h_y_nonneg h_y_le_sSup - -/-- The Perron root of a non-negative matrix is non-negative. -/ -lemma perronRoot_nonneg {n : Type*} [Fintype n] [Nonempty n] [DecidableEq n] - {A : Matrix n n ℝ} (hA_nonneg : ∀ i j, 0 ≤ A i j) : - 0 ≤ perronRoot_alt A := by - unfold perronRoot_alt - apply csSup_nonneg - · exact CollatzWielandt.set_nonempty - · exact CollatzWielandt.bddAbove A hA_nonneg - · rintro _ ⟨x, ⟨hx_nonneg, hx_ne_zero⟩, rfl⟩ - dsimp [collatzWielandtFn] - split_ifs with h_supp_nonempty - · apply Finset.le_inf' - intro i hi_mem - apply div_nonneg - · exact mulVec_nonneg hA_nonneg hx_nonneg i - · exact (Set.mem_toFinset.mp hi_mem).le - · exact le_rfl - -/-- -If an inequality lambda•w ≤ A•w holds for a non-negative non-zero vector w, -then lambda is bounded by the Collatz-Wielandt function value for w. -This is the property that the Collatz-Wielandt function gives -the maximum lambda satisfying such an inequality. --/ -theorem le_of_subinvariant [DecidableEq n] - {A : Matrix n n ℝ} (_ : ∀ i j, 0 ≤ A i j) - {w : n → ℝ} (hw_nonneg : ∀ i, 0 ≤ w i) (hw_ne_zero : w ≠ 0) - {lambda : ℝ} (h_sub : lambda • w ≤ A *ᵥ w) : - lambda ≤ collatzWielandtFn A w := by - obtain ⟨i, hi⟩ := exists_pos_of_ne_zero hw_nonneg hw_ne_zero - let S := {j | 0 < w j}.toFinset - have hS_nonempty : S.Nonempty := ⟨i, by simp [S]; exact hi⟩ - rw [collatzWielandtFn, dif_pos hS_nonempty] - apply Finset.le_inf' - intro j hj - have h_j : lambda * w j ≤ (A *ᵥ w) j := by - simp_all only [ne_eq, Set.toFinset_setOf, Finset.mem_filter, Finset.mem_univ, true_and, S] - apply h_sub - have hw_j_pos : 0 < w j := by simpa [S] using hj - exact (le_div_iff₀ hw_j_pos).mpr (h_sub j) diff --git a/lean/MCMC/PF/LinearAlgebra/Matrix/PerronFrobenius/Dominance.lean b/lean/MCMC/PF/LinearAlgebra/Matrix/PerronFrobenius/Dominance.lean deleted file mode 100644 index 7baf457..0000000 --- a/lean/MCMC/PF/LinearAlgebra/Matrix/PerronFrobenius/Dominance.lean +++ /dev/null @@ -1,1242 +0,0 @@ -import MCMC.PF.LinearAlgebra.Matrix.PerronFrobenius.Irreducible -import MCMC.PF.Analysis.CstarAlgebra.Classes - -open Quiver.Path -namespace Matrix -open CollatzWielandt - -open Quiver -open Matrix Classical Complex - -variable {n : Type*} {A : Matrix n n ℝ} - -/-- If a property `P` holds for at least one vertex `i₀` and propagates along the edges -of an irreducible matrix's graph (`P i ∧ A i j > 0 → P j`), then `P` holds for all vertices. -/ -lemma IsIrreducible.eq_univ_of_propagate (hA_irred : A.IsIrreducible) (P : n → Prop) - (h_nonempty : ∃ i₀, P i₀) - (h_propagate : ∀ i j, P i → 0 < A i j → P j) : - ∀ i, P i := by - classical - let S : Set n := {i | P i} - let T : Set n := {i | ¬ P i} - by_contra h_not_all - push_neg at h_not_all - have hS_nonempty : (S : Set n).Nonempty := h_nonempty - have hT_nonempty : (T : Set n).Nonempty := h_not_all - have hS_ne_univ : (S : Set n) ≠ Set.univ := by - intro h_eq - rcases hT_nonempty with ⟨i, hi_T⟩ - have hPi : P i := by - have : i ∈ S := by - have : i ∈ (Set.univ : Set n) := Set.mem_univ i - simp only [Set.mem_univ] at this - simp_all only [Set.mem_setOf_eq, Set.mem_univ, S, T] - simpa [S] using this - exact hi_T hPi - obtain ⟨i, hi_S, j, hj_not_S, hAij_pos⟩ := - Matrix.Irreducible.exists_edge_out (A := A) hA_irred S hS_nonempty hS_ne_univ - have hPi : P i := by - simpa [S] using hi_S - have hPj : P j := h_propagate i j hPi hAij_pos - exact hj_not_S (by - simpa [S] using hPj) - -variable {n : Type*} [Fintype n] -variable {A : Matrix n n ℝ} - -/-- For an irreducible, non-negative matrix `A`, if `v` is an eigenvector for an eigenvalue `μ`, -then the vector `w` of absolute values of `v` satisfies the inequality `|μ| • w ≤ A *ᵥ w`. -This is a key step in the Perron-Frobenius theorem. -/ -lemma abs_eigenvector_inequality - (hA_nonneg : ∀ i j, 0 ≤ A i j) - {μ : ℝ} {v : n → ℝ} (h_eig : A *ᵥ v = μ • v) : - let w := fun i ↦ |v i|; |μ| • w ≤ A *ᵥ w := by - intro w i - calc - (|μ| • w) i = |μ| * |v i| := by simp [w] - _ = |μ * v i| := by rw [abs_mul] - _ = |(μ • v) i| := by simp - _ = |(A *ᵥ v) i| := by rw [← h_eig] - _ = |∑ j, A i j * v j| := by simp [mulVec, dotProduct] - _ ≤ ∑ j, |A i j * v j| := by exact Finset.abs_sum_le_sum_abs _ _ - _ = ∑ j, (A i j) * |v j| := by simp_rw [abs_mul, abs_of_nonneg (hA_nonneg i _)] - _ = (A *ᵥ w) i := by simp [w, mulVec, dotProduct] - -/-- -If the triangle equality holds for the complex eigenvector equation `A * x = lam * x`, -then the vector of norms `‖x‖` is a real eigenvector of `A` with eigenvalue `‖lam‖`. --/ -lemma norm_eigenvector_is_eigenvector_of_triangle_eq - {A : Matrix n n ℝ} (hA_nonneg : ∀ i j, 0 ≤ A i j) - {lam : ℂ} {x : n → ℂ} (hx_eig : (A.map (algebraMap ℝ ℂ)) *ᵥ x = lam • x) - (h_triangle_eq : ∀ i, ‖∑ j, (A i j : ℂ) * x j‖ = ∑ j, ‖(A i j : ℂ) * x j‖) : - A *ᵥ (fun i => ‖x i‖) = (‖lam‖ : ℝ) • (fun i => ‖x i‖) := by - funext i - calc - (A *ᵥ fun i => ‖x i‖) i - = ∑ j, A i j * ‖x j‖ := by simp [mulVec_apply] - _ = ∑ j, ‖(A i j : ℂ)‖ * ‖x j‖ := by simp_rw [Complex.norm_ofReal, abs_of_nonneg (hA_nonneg _ _)] - _ = ∑ j, ‖(A i j : ℂ) * x j‖ := by simp_rw [norm_mul] - _ = ‖∑ j, (A i j : ℂ) * x j‖ := (h_triangle_eq i).symm - _ = ‖((A.map (algebraMap ℝ ℂ)) *ᵥ x) i‖ := by simp; rfl - _ = ‖(lam • x) i‖ := by rw [hx_eig] - _ = ‖lam * x i‖ := by rw [Pi.smul_apply]; rfl - _ = ‖lam‖ * ‖x i‖ := by rw [norm_mul] - _ = ((‖lam‖ : ℝ) • fun i => ‖x i‖) i := by simp [smul_eq_mul] - -/-- -If equality holds in the triangle inequality for `∑ z_j`, then all non-zero `z_j` -are aligned with the sum. --/ -lemma aligned_of_all_nonneg_re_im - {A : Matrix n n ℝ} {i : n} {x : n → ℂ} - (h_sum_eq : ‖∑ j, (A i j : ℂ) * x j‖ = - ∑ j, ‖(A i j : ℂ) * x j‖) : - ∀ j, (A i j : ℂ) * x j ≠ 0 → - ∃ c : ℝ, 0 ≤ c ∧ - (A i j : ℂ) * x j = c • (∑ k, (A i k : ℂ) * x k) := by - classical - let z : n → ℂ := fun j => (A i j : ℂ) * x j - let s : ℂ := ∑ j, z j - have h_z_sum : ‖s‖ = ∑ j, ‖z j‖ := by - simpa [z, s] using h_sum_eq - intro j hz_ne_zero - have hs_ne_zero : s ≠ 0 := by - intro hs - have h_norms_zero : ∑ j, ‖z j‖ = 0 := by - simp_all only [Complex.norm_mul, norm_real, Real.norm_eq_abs, Finset.sum_def, ne_eq, mul_eq_zero, ofReal_eq_zero, - not_or, norm_zero, z, s] - have h_all_zero : ∀ k, ‖z k‖ = 0 := by - intro k - exact eq_zero_of_sum_eq_zero - (fun k => ‖z k‖) (fun _ => norm_nonneg _) h_norms_zero k - have h_zj_zero : z j = 0 := by - apply norm_eq_zero.mp - simpa using h_all_zero j - exact hz_ne_zero h_zj_zero - have h_align := - Complex.each_term_is_nonneg_real_multiple_of_sum_of_triangle_eq - (s := Finset.univ) - (v := z) - (u := s) - (by simp [s]) - (by simpa [s] using h_z_sum) - hs_ne_zero - rcases h_align j (by simp) with ⟨c, hc_nonneg, hcz⟩ - have hcz' : z j = (c : ℂ) * s := hcz - have hcz_smul : z j = c • s := by simpa [smul_eq_mul] using hcz' - refine ⟨c, hc_nonneg, ?_⟩ - simpa [z, s] using hcz_smul - -/-- For a non-negative matrix A, if the row sums are all equal to λ, then λ is an eigenvalue - with the all-ones vector as its eigenvector. -/ -lemma row_sum_eigenvalue - (_ : ∀ i j, 0 ≤ A i j) (lambda : ℝ) (h_row_sums : ∀ i, ∑ j, A i j = lambda) : - A *ᵥ (fun _ => (1 : ℝ)) = lambda • (fun _ => (1 : ℝ)) := by - ext i - rw [mulVec_apply, Pi.smul_apply, smul_eq_mul] - simp only [mul_one] - rw [h_row_sums i] - -/-- If the dot product of a non-negative vector `v` and a strictly positive vector `w` is zero, - then `v` must be the zero vector. -/ -lemma eq_zero_of_dotProduct_eq_zero_of_nonneg_of_pos - {v w : n → ℝ} (hv_nonneg : ∀ i, 0 ≤ v i) (hw_pos : ∀ i, 0 < w i) - (h_dot : v ⬝ᵥ w = 0) : - v = 0 := by - rw [dotProduct] at h_dot - have h_terms_nonneg : ∀ i, 0 ≤ v i * w i := by - intro i - exact mul_nonneg (hv_nonneg i) (hw_pos i).le - rw [Finset.sum_eq_zero_iff_of_nonneg (fun i _ => h_terms_nonneg i)] at h_dot - funext i - have hi_zero : v i * w i = 0 := h_dot i (Finset.mem_univ i) - rw [mul_eq_zero] at hi_zero - exact hi_zero.resolve_right (hw_pos i).ne' - -/-- -If a scalar `μ` is in the spectrum of a complex matrix `A`, then there exists a non-zero -eigenvector `x` for that eigenvalue. --/ -theorem exists_eigenvector_of_mem_spectrum - {A' : Matrix n n ℝ} {μ : ℂ} (h : μ ∈ spectrum ℂ (A'.map (algebraMap ℝ ℂ))) : - ∃ x, x ≠ 0 ∧ (A'.map (algebraMap ℝ ℂ)) *ᵥ x = μ • x := by - let B := A'.map (algebraMap ℝ ℂ) - have h_spec : μ ∈ spectrum ℂ (toLin' B) := by - rwa [spectrum.Matrix_toLin'_eq_spectrum] - rcases Module.End.exists_eigenvector_of_mem_spectrum h_spec with ⟨x, hx_ne_zero, hx_eig⟩ - refine ⟨x, hx_ne_zero, ?_⟩ - have h_mul_eq := hx_eig - rw [toLin'_apply] at h_mul_eq - exact h_mul_eq - -/- If `v` is an eigenvector of `A` with eigenvalue `r`, then `v` is an eigenvector of `A^m` -with eigenvalue `r^m`. --/ -lemma pow_eigenvector_of_eigenvector {R : Type*} [DecidableEq n][CommSemiring R] {A : Matrix n n R} {r : R} {v : n → R} - (h_eig : A *ᵥ v = r • v) (m : ℕ) : - (A ^ m) *ᵥ v = (r ^ m) • v := by - induction m with - | zero => - simp [pow_zero] -- (A ^ 0) *ᵥ v = v and (r ^ 0) • v = v - | succ m ih => - -- Goal: (A ^ (m + 1)) *ᵥ v = r ^ (m + 1) • v - calc - (A ^ m.succ) *ᵥ v - = (A ^ m * A) *ᵥ v := by - simp [pow_succ] - _ = A ^ m *ᵥ (A *ᵥ v) := by - rw [Matrix.mulVec_mulVec] - _ = A ^ m *ᵥ (r • v) := by - simp [h_eig] - _ = r • (A ^ m *ᵥ v) := by - rw [mulVec_smul] - _ = r • (r ^ m • v) := by - simp [ih] - _ = r ^ (m + 1) • v := by - simp [pow_succ', smul_smul] - - -theorem mul_mulVec {α : Type*} [NonUnitalSemiring α] {m l : Type*} [Fintype m] [Fintype l] - (M : Matrix l m α) (N : Matrix m n α) (v : n → α) : - (M * N) *ᵥ v = M *ᵥ (N *ᵥ v) := by - ext i - simp only [mulVec, mul_apply, dotProduct] - apply dotProduct_assoc - -private lemma sum_component_norms_eq_perron_power_norm [DecidableEq n] -- [CommSemiring R] - {A : Matrix n n ℝ} {x : n → ℂ} - (h_x_abs_eig : A *ᵥ (fun i ↦ ‖x i‖) = (perronRoot_alt A) • (fun i ↦ ‖x i‖)) - (k : ℕ) (m : n) (hAk_pos : ∀ i j, 0 < (A ^ k) i j) : - ∑ l, ‖((A ^ k) m l : ℂ) * x l‖ = (perronRoot_alt A) ^ k * ‖x m‖ := by - have h_pow_eig : (A ^ k) *ᵥ (fun i ↦ ‖x i‖) = (perronRoot_alt A) ^ k • (fun i ↦ ‖x i‖) := - pow_eigenvector_of_eigenvector h_x_abs_eig k - calc ∑ l, ‖((A ^ k) m l : ℂ) * x l‖ - = ∑ l, |(A ^ k) m l| * ‖x l‖ := by - simp_rw [norm_mul, Complex.norm_ofReal] - _ = ∑ l, (A ^ k) m l * ‖x l‖ := by - simp_rw [abs_of_pos (hAk_pos m _)] - _ = ((A ^ k) *ᵥ (fun i ↦ ‖x i‖)) m := by simp [mulVec_apply] - _ = ((perronRoot_alt A) ^ k • (fun i ↦ ‖x i‖)) m := by rw [h_pow_eig] - _ = (perronRoot_alt A) ^ k * ‖x m‖ := by simp [Pi.smul_apply, smul_eq_mul] - -/-- -If `x` is a complex eigenvector of a real matrix `A` with eigenvalue `μ`, then `x` is an -eigenvector of `A^m` with eigenvalue `μ^m`. This is the complex version of the lemma. --/ -lemma pow_eigenvector_of_eigenvector' [DecidableEq n] {A : Matrix n n ℝ} {μ : ℂ} {x : n → ℂ} - (h_eig : (A.map (algebraMap ℝ ℂ)) *ᵥ x = μ • x) (m : ℕ) : - ((A ^ m).map (algebraMap ℝ ℂ)) *ᵥ x = (μ ^ m) • x := by - induction m with - | zero => - simp [pow_zero, Matrix.map_one, one_mulVec, one_smul] - | succ m ih => - calc - ((A ^ (m + 1)).map (algebraMap ℝ ℂ)) *ᵥ x - = ((A * A ^ m).map (algebraMap ℝ ℂ)) *ᵥ x := by rw [pow_succ'] - _ = ((A.map (algebraMap ℝ ℂ)) * ((A ^ m).map (algebraMap ℝ ℂ))) *ᵥ x := by rw [Matrix.map_mul] - _ = (A.map (algebraMap ℝ ℂ)) *ᵥ (((A ^ m).map (algebraMap ℝ ℂ)) *ᵥ x) := by rw [Matrix.mulVec_mulVec] - _ = (A.map (algebraMap ℝ ℂ)) *ᵥ ((μ ^ m) • x) := by rw [ih] - _ = (μ ^ m) • ((A.map (algebraMap ℝ ℂ)) *ᵥ x) := by rw [mulVec_smul] - _ = (μ ^ m) • (μ • x) := by rw [h_eig] - _ = ((μ ^ m) * μ) • x := by rw [smul_smul] - _ = (μ ^ (m + 1)) • x := by rw [pow_succ']; rw [@pow_mul_comm'] - -/-- -For an eigenvalue μ of a nonnegative matrix A with eigenvector x, -the absolute value |μ| satisfies the sub-invariant relation: |μ|⋅|x| ≤ A⋅|x|. -This is the fundamental inequality in spectral analysis of nonnegative matrices. --/ -theorem eigenvalue_abs_subinvariant - {A : Matrix n n ℝ} (hA_nonneg : ∀ i j, 0 ≤ A i j) - {μ : ℂ} {x : n → ℂ} (hx_eig : (A.map (algebraMap ℝ ℂ)) *ᵥ x = μ • x) : - (‖μ‖ : ℝ) • (fun i => ‖x i‖) ≤ A *ᵥ (fun i => ‖x i‖) := by - intro i - calc - (‖μ‖ : ℝ) * ‖x i‖ = ‖μ * x i‖ := by rw [← norm_mul] - _ = ‖(μ • x) i‖ := by simp [Pi.smul_apply] - _ = ‖((A.map (algebraMap ℝ ℂ)) *ᵥ x) i‖ := by rw [← hx_eig] - _ = ‖∑ j, (A i j : ℂ) * x j‖ := by simp; rfl - _ ≤ ∑ j, ‖(A i j : ℂ) * x j‖ := by apply norm_sum_le - _ = ∑ j, A i j * ‖x j‖ := by - simp only [Complex.norm_mul, norm_real, Real.norm_eq_abs, abs_of_nonneg (hA_nonneg _ _)] - _ = (A *ᵥ fun i => ‖x i‖) i := by simp [mulVec_apply] - -variable {n : Type*} [Fintype n] [Nonempty n] [DecidableEq n] -variable {A : Matrix n n ℝ} - -/-- -Under the conditions of the main theorem, the eigenvalue `lam` must be non-zero. --/ -lemma eigenvalue_ne_zero_of_irreducible - {A : Matrix n n ℝ} (hA_irred : A.IsIrreducible) - {lam : ℂ} {x : n → ℂ} (hx_ne_zero : x ≠ 0) - (h_x_abs_eig : A *ᵥ (fun i => ‖x i‖) = (‖lam‖ : ℝ) • (fun i => ‖x i‖)) : - lam ≠ 0 := by - intro h_lam_zero - have h_norm_lam_zero : ‖lam‖ = 0 := by rwa [norm_eq_zero] - have h_eig_zero_smul : A *ᵥ (fun i => ‖x i‖) = (0 : ℝ) • (fun i => ‖x i‖) := by - rw [h_norm_lam_zero] at h_x_abs_eig - exact h_x_abs_eig - have h_eig_zero : A *ᵥ (fun i => ‖x i‖) = 0 := by - simpa [zero_smul] using h_eig_zero_smul - have h_x_abs_nonneg : ∀ i, 0 ≤ ‖x i‖ := fun i => norm_nonneg _ - have h_x_abs_ne_zero : (fun i => ‖x i‖) ≠ 0 := by - contrapose! hx_ne_zero - ext i - exact norm_eq_zero.mp (congr_fun hx_ne_zero i) - have h_x_abs_pos : ∀ i, 0 < ‖x i‖ := - eigenvector_is_positive_of_irreducible hA_irred h_eig_zero_smul h_x_abs_nonneg h_x_abs_ne_zero - obtain ⟨i, j, hAij_pos⟩ := Matrix.Irreducible.exists_pos_entry (A := A) hA_irred - have h_sum : (A *ᵥ (fun k => ‖x k‖)) i = 0 := by rw [h_eig_zero]; rfl - rw [mulVec_apply] at h_sum - have h_sum_pos : 0 < ∑ k, A i k * ‖x k‖ := by - apply sum_pos_of_mem - · intro k _ - exact mul_nonneg (hA_irred.nonneg i k) (h_x_abs_nonneg k) - · exact Finset.mem_univ j - · exact mul_pos hAij_pos (h_x_abs_pos j) - exact h_sum_pos.ne' h_sum - -theorem eigenvalue_is_perron_root_of_positive_eigenvector - {r : ℝ} {v : n → ℝ} - (_ : A.IsIrreducible) - (hA_nonneg : ∀ i j, 0 ≤ A i j) - (hr_pos : 0 < r) - (hv_pos : ∀ i, 0 < v i) - (h_eig : A *ᵥ v = r • v) : - r = perronRoot_alt A := by - have h_ge : perronRoot_alt A ≤ r := - eigenvalue_is_ub_of_positive_eigenvector - (A := A) hA_nonneg hr_pos hv_pos h_eig - have h_le : r ≤ perronRoot_alt A := by - rw [← eq_eigenvalue_of_positive_eigenvector hv_pos h_eig] - have hv_nonneg : ∀ i, 0 ≤ v i := fun i ↦ (hv_pos i).le - have hv_ne_zero : v ≠ 0 := by - intro h0 - have : 0 < v (Classical.arbitrary n) := hv_pos (Classical.arbitrary n) - rw [h0] at this - simp only [Pi.zero_apply, lt_self_iff_false] at this - apply le_csSup (CollatzWielandt.bddAbove A hA_nonneg) - rw [@Set.mem_image] - exact ⟨v, ⟨hv_nonneg, hv_ne_zero⟩, rfl⟩ - exact le_antisymm h_le h_ge - -theorem perronRoot_transpose_eq - (A : Matrix n n ℝ) (hA_irred : A.IsIrreducible) : - perronRoot_alt A = perronRoot_alt Aᵀ := by - obtain ⟨r, v, hr_pos, hv_pos, hv_eig⟩ := - exists_positive_eigenvector_of_irreducible hA_irred - have hr_eq_perron : r = perronRoot_alt A := - eigenvalue_is_perron_root_of_positive_eigenvector - hA_irred hA_irred.nonneg hr_pos hv_pos hv_eig - have hAT_irred : Aᵀ.IsIrreducible := - Matrix.IsIrreducible.transpose hA_irred - obtain ⟨r', u, hr'_pos, hu_pos, hu_eig_T⟩ := - exists_positive_eigenvector_of_irreducible hAT_irred - have hr'_eq_perron : r' = perronRoot_alt Aᵀ := - eigenvalue_is_perron_root_of_positive_eigenvector - hAT_irred (fun i j ↦ hA_irred.nonneg j i) hr'_pos hu_pos hu_eig_T - have hu_eig_left : u ᵥ* A = r' • u := by - have : Aᵀ *ᵥ u = r' • u := hu_eig_T - simpa [vecMul_eq_mulVec_transpose] using this - have hv_nonneg : ∀ i, 0 ≤ v i := fun i ↦ (hv_pos i).le - have hv_ne_zero : v ≠ 0 := by - intro h - have : 0 < v (Classical.arbitrary n) := hv_pos _ - simp only [h, Pi.zero_apply, lt_self_iff_false] at this - have h_dot_pos : 0 < u ⬝ᵥ v := - dotProduct_pos_of_pos_of_nonneg_ne_zero hu_pos hv_nonneg hv_ne_zero - have h1 : u ⬝ᵥ (A *ᵥ v) = r * (u ⬝ᵥ v) := by - simp only [hv_eig, dotProduct_smul, smul_eq_mul] - have h2 : (u ᵥ* A) ⬝ᵥ v = r' * (u ⬝ᵥ v) := by - simp only [hu_eig_left, smul_dotProduct, smul_eq_mul] - have h_eq : r * (u ⬝ᵥ v) = r' * (u ⬝ᵥ v) := by - calc - r * (u ⬝ᵥ v) = u ⬝ᵥ (A *ᵥ v) := (h1.symm) - _ = (u ᵥ* A) ⬝ᵥ v := by - simpa using dotProduct_mulVec u A v - _ = r' * (u ⬝ᵥ v) := h2 - have hr_eq_r' : r = r' := by - subst hr_eq_perron hr'_eq_perron - simp_all only [ne_eq, dotProduct_smul, smul_eq_mul, smul_dotProduct, mul_eq_mul_right_iff] - cases h_eq with - | inl h => simp_all only - | inr h_1 => simp_all only [lt_self_iff_false] - calc - perronRoot_alt A = r := by symm; simpa using hr_eq_perron - _ = r' := hr_eq_r' - _ = perronRoot_alt Aᵀ := hr'_eq_perron - -/-- -If for a non-negative, irreducible matrix `A`, there exists -a non-negative, non-zero vector `y` and a positive scalar `s` such that `A *ᵥ y ≤ s • y`, -then the Perron root of `A` is at most `s`. --/ -lemma perron_root_le_of_subinvariant - (hA_irred : A.IsIrreducible) - (hA_nonneg : ∀ i j, 0 ≤ A i j) - {s : ℝ} (_ : 0 < s) - {y : n → ℝ} (hy_nonneg : ∀ i, 0 ≤ y i) - (hy_ne_zero : y ≠ 0) - (h_subinv : A *ᵥ y ≤ s • y) : - perronRoot_alt A ≤ s := by - let A_T := Aᵀ - have hAT_irred : A_T.IsIrreducible := Matrix.IsIrreducible.transpose hA_irred - have hAT_nonneg : ∀ i j, 0 ≤ A_T i j := by simp [A_T]; exact fun i j ↦ hA_nonneg j i - obtain ⟨r, u, hr_pos, hu_pos, hu_eig⟩ := - exists_positive_eigenvector_of_irreducible hAT_irred - have h_r_eq_perron : r = perronRoot_alt A := by - calc - r = perronRoot_alt Aᵀ := eigenvalue_is_perron_root_of_positive_eigenvector - hAT_irred hAT_nonneg hr_pos hu_pos hu_eig - _ = perronRoot_alt A := by rw [← perronRoot_transpose_eq A hA_irred] - have h_u_left_eig : u ᵥ* A = r • u := by - rwa [vecMul_eq_mulVec_transpose] - have h_dot_le : u ⬝ᵥ (A *ᵥ y) ≤ u ⬝ᵥ (s • y) := - dotProduct_le_dotProduct_of_nonneg_left' (fun i => (hu_pos i).le) h_subinv - rw [dotProduct_mulVec, h_u_left_eig, dotProduct_smul_left, dotProduct_smul] at h_dot_le - have h_dot_pos : 0 < u ⬝ᵥ y := dotProduct_pos_of_pos_of_nonneg_ne_zero hu_pos hy_nonneg hy_ne_zero - have h_r_le_s : r ≤ s := by - have h_mul_le : r * (u ⬝ᵥ y) ≤ s * (u ⬝ᵥ y) := h_dot_le - exact le_of_mul_le_mul_right h_mul_le h_dot_pos - rwa [h_r_eq_perron] at h_r_le_s - -/-- If equality holds in the subinvariance inequality `r • v ≤ A *ᵥ v` for the Perron root `r`, - then `v` must be an eigenvector. -/ -lemma subinvariant_equality_implies_eigenvector - (hA_irred : A.IsIrreducible) - (hA_nonneg : ∀ i j, 0 ≤ A i j) - {v : n → ℝ} (_ : ∀ i, 0 ≤ v i) (_ : v ≠ 0) - (h_subinv : perronRoot_alt A • v ≤ A *ᵥ v) : - A *ᵥ v = perronRoot_alt A • v := by - let r := perronRoot_alt A - let z := A *ᵥ v - r • v - have hz_nonneg : ∀ i, 0 ≤ z i := by - intro i - simp only [Pi.sub_apply, Pi.smul_apply, smul_eq_mul, sub_nonneg, z] - exact h_subinv i - by_cases hz_zero : z = 0 - · simp only [sub_eq_zero, z] at hz_zero - exact hz_zero - · obtain ⟨r_T, u, hr_T_pos, hu_pos, hu_eig⟩ := - exists_positive_eigenvector_of_irreducible (Matrix.IsIrreducible.transpose hA_irred) - have h_u_left_eig : u ᵥ* A = r_T • u := by - rwa [vecMul_eq_mulVec_transpose] - have h_rT_eq_r : r_T = r := by - calc - r_T = perronRoot_alt Aᵀ := - eigenvalue_is_perron_root_of_positive_eigenvector - (Matrix.IsIrreducible.transpose hA_irred) - (fun i j ↦ hA_nonneg j i) hr_T_pos hu_pos hu_eig - _ = perronRoot_alt A := (perronRoot_transpose_eq A hA_irred).symm - _ = r := rfl - have h_dot_z : u ⬝ᵥ z = 0 := by - rw [dotProduct_sub, dotProduct_mulVec, h_u_left_eig, h_rT_eq_r, dotProduct_smul_left, dotProduct_smul, smul_eq_mul, sub_self] - have h_z_is_zero' := eq_zero_of_dotProduct_eq_zero_of_nonneg_of_pos hz_nonneg hu_pos (by rwa [dotProduct_comm]) - contradiction - -/-- -The value of the Collatz-Wielandt function for any non-negative, non-zero vector -is less than or equal to the Perron root. --/ -lemma collatzWielandtFn_le_perronRoot_alt - {A : Matrix n n ℝ} (hA_nonneg : ∀ i j, 0 ≤ A i j) - {x : n → ℝ} (hx_nonneg : ∀ i, 0 ≤ x i) (hx_ne_zero : x ≠ 0) : - collatzWielandtFn A x ≤ perronRoot_alt A := by - apply le_csSup (CollatzWielandt.bddAbove A hA_nonneg) - rw [Set.mem_image] - exact ⟨x, ⟨hx_nonneg, hx_ne_zero⟩, rfl⟩ - -/-- -Any eigenvalue μ of a nonnegative irreducible matrix A has absolute value -at most equal to the Perron root. --/ -theorem eigenvalue_abs_le_perron_root - {A : Matrix n n ℝ} (_ : A.IsIrreducible) (hA_nonneg : ∀ i j, 0 ≤ A i j) - {μ : ℂ} (h_is_eigenvalue : μ ∈ spectrum ℂ (A.map (algebraMap ℝ ℂ))) : - ‖μ‖ ≤ perronRoot_alt A := by - let B := A.map (algebraMap ℝ ℂ) - have h_spec : μ ∈ spectrum ℂ (toLin' B) := by rwa [spectrum.Matrix_toLin'_eq_spectrum] - rcases Module.End.exists_eigenvector_of_mem_spectrum h_spec with ⟨x, hx_ne_zero, hx_eig_lin⟩ - have hx_eig : B *ᵥ x = μ • x := by rwa [toLin'_apply] at hx_eig_lin - let x_abs := fun i => ‖x i‖ - have hx_abs_nonneg : ∀ i, 0 ≤ x_abs i := fun i => norm_nonneg _ - have hx_abs_ne_zero : x_abs ≠ 0 := by - contrapose! hx_ne_zero - ext i - exact norm_eq_zero.mp (congr_fun hx_ne_zero i) - have h_subinv : (‖μ‖ : ℝ) • x_abs ≤ A *ᵥ x_abs := - eigenvalue_abs_subinvariant hA_nonneg hx_eig - have h_le_collatz : (‖μ‖ : ℝ) ≤ collatzWielandtFn A x_abs := - le_of_subinvariant hA_nonneg hx_abs_nonneg hx_abs_ne_zero h_subinv - have h_le_perron : collatzWielandtFn A x_abs ≤ perronRoot_alt A := - collatzWielandtFn_le_perronRoot_alt hA_nonneg hx_abs_nonneg hx_abs_ne_zero - exact le_trans h_le_collatz h_le_perron - -/-- For an irreducible, non-negative matrix, the Perron root (defined as the Collatz-Wielandt -supremum) is equal to the unique positive eigenvalue `r` from the existence theorem. -/ -lemma perron_root_eq_positive_eigenvalue (hA_irred : A.IsIrreducible) (hA_nonneg : ∀ i j, 0 ≤ A i j) : - ∃ r v, 0 < r ∧ (∀ i, 0 < v i) ∧ A *ᵥ v = r • v ∧ perronRoot_alt A = r := by - obtain ⟨r, v, hr_pos, hv_pos, h_eig⟩ := exists_positive_eigenvector_of_irreducible hA_irred - have h_le : perronRoot_alt A ≤ r := - eigenvalue_is_ub_of_positive_eigenvector hA_nonneg hr_pos hv_pos h_eig - have h_ge : r ≤ perronRoot_alt A := - eigenvalue_le_perron_root_of_positive_eigenvector hA_nonneg hr_pos hv_pos h_eig - have h_eq : perronRoot_alt A = r := le_antisymm h_le h_ge - exact ⟨r, v, hr_pos, hv_pos, h_eig, h_eq⟩ - -/-- -If a matrix `A` has an eigenvector `v` for an eigenvalue `μ`, then `μ` is in the spectrum of `A`. -This is a direct consequence of the definition of an eigenvalue and the spectrum. --/ -lemma mem_spectrum_of_hasEigenvector {K V : Type*} [Field K] [AddCommGroup V] [Module K V] - [FiniteDimensional K V] {f : V →ₗ[K] V} {μ : K} {v : V} (h : Module.End.HasEigenvector f μ v) : - μ ∈ spectrum K f := by - rw [← Module.End.hasEigenvalue_iff_mem_spectrum] - exact Module.End.hasEigenvalue_of_hasEigenvector h - -lemma mem_spectrum_of_eigenvalue - {K : Type*} [Field K] {n : Type*} [Fintype n] [DecidableEq n] - {A : Matrix n n K} {μ : K} {v : n → K} - (hv_ne_zero : v ≠ 0) (h_eig : A *ᵥ v = μ • v) : - μ ∈ spectrum K A := by - let f := toLin' A - have h_eig_f : f v = μ • v := by - simpa [toLin'_apply, f] using h_eig - have h_has_eigvec : Module.End.HasEigenvector f μ v := - ⟨by - rwa [← Module.End.mem_eigenspace_iff] at h_eig_f, - hv_ne_zero⟩ - have h_mem_f : μ ∈ spectrum K f := - mem_spectrum_of_hasEigenvector h_has_eigvec - simpa [f, spectrum.Matrix_toLin'_eq_spectrum] using h_mem_f - -/-- The Perron root of an irreducible, non-negative matrix is an eigenvalue. -/ -theorem perron_root_is_eigenvalue (hA_irred : A.IsIrreducible) (hA_nonneg : ∀ i j, 0 ≤ A i j) : - perronRoot_alt A ∈ spectrum ℝ A := by - obtain ⟨r', v, _, hv_pos, h_eig, h_eq⟩ := perron_root_eq_positive_eigenvalue hA_irred hA_nonneg - have hv_ne_0 : v ≠ 0 := fun h => by - have := hv_pos (Classical.arbitrary n) - rw [h] at this - exact lt_irrefl 0 this - rw [h_eq] - exact mem_spectrum_of_eigenvalue hv_ne_0 h_eig - -/-- **Perron-Frobenius Theorem (Dominance)**: The Perron root of an irreducible, non-negative -matrix is an eigenvalue and its modulus is greater than or equal to the modulus of any other -eigenvalue. It is the spectral radius. -/ -theorem perron_root_is_spectral_radius (hA_irred : A.IsIrreducible) (hA_nonneg : ∀ i j, 0 ≤ A i j) : - let r := perronRoot_alt A - r ∈ spectrum ℝ A ∧ ∀ μ ∈ spectrum ℝ A, |μ| ≤ r := by - constructor - · exact perron_root_is_eigenvalue hA_irred hA_nonneg - · intro μ hμ - have hμ_complex : (μ : ℂ) ∈ spectrum ℂ (A.map (algebraMap ℝ ℂ)) := by - have hμ_lin : μ ∈ spectrum ℝ (toLin' A) := by - simpa [spectrum.Matrix_toLin'_eq_spectrum] using hμ - obtain ⟨v, hv_ne_zero, hv_eig⟩ := - Module.End.exists_eigenvector_of_mem_spectrum hμ_lin - let v_complex : n → ℂ := fun i => (v i : ℂ) - have hvc_ne_zero : v_complex ≠ 0 := by - intro h - have : v = 0 := by - ext i - have : (v i : ℂ) = 0 := congr_fun h i - exact_mod_cast this - exact hv_ne_zero this - have hv_eig_vec : A *ᵥ v = μ • v := by - simpa [toLin'_apply] using hv_eig - have hvc_eig : (A.map (algebraMap ℝ ℂ)) *ᵥ v_complex = (μ : ℂ) • v_complex := by - ext i - have h_eq : (A *ᵥ v) i = μ * v i := by - simpa using congr_fun hv_eig_vec i - simpa [v_complex, smul_eq_mul, mulVec, dotProduct, map_apply] using - congrArg (fun x : ℝ => (x : ℂ)) h_eq - exact mem_spectrum_of_eigenvalue hvc_ne_zero hvc_eig - have h_bound := eigenvalue_abs_le_perron_root hA_irred hA_nonneg hμ_complex - rwa [Complex.norm_ofReal] at h_bound - -omit [Nonempty n] [DecidableEq n] in -/-- If an eigenvalue `μ` has a norm equal to the Perron root `r`, then the triangle inequality -for the eigenvector equation holds with equality. -/ -lemma triangle_equality_of_norm_eq_perron_root - {A : Matrix n n ℝ} (hA_nonneg : ∀ i j, 0 ≤ A i j) - {μ : ℂ} {x : n → ℂ} (hx_eig : (A.map (algebraMap ℝ ℂ)) *ᵥ x = μ • x) - {r : ℝ} (h_norm_eq_r : ‖μ‖ = r) - (h_x_abs_eig : A *ᵥ (fun i => ‖x i‖) = r • (fun i => ‖x i‖)) : - ∀ i, ‖∑ j, (A i j : ℂ) * x j‖ = ∑ j, ‖(A i j : ℂ) * x j‖ := by - intro i - let x_abs := fun i => ‖x i‖ - calc - ‖∑ j, (A i j : ℂ) * x j‖ = ‖((A.map (algebraMap ℝ ℂ)) *ᵥ x) i‖ := by simp; rfl - _ = ‖(μ • x) i‖ := by rw [hx_eig] - _ = ‖μ‖ * ‖x i‖ := by simp - _ = r * x_abs i := by rw [h_norm_eq_r]; - _ = (r • x_abs) i := by simp [smul_eq_mul] - _ = (A *ᵥ x_abs) i := by rw [h_x_abs_eig] - _ = ∑ j, A i j * x_abs j := by simp [mulVec_apply] - _ = ∑ j, ‖(A i j : ℂ) * x j‖ := by - simp_rw [x_abs, norm_mul, norm_ofReal, abs_of_nonneg (hA_nonneg _ _)] - -/-- -If `|x|` is a positive eigenvector of an irreducible non-negative matrix `A`, then for any `i`, -the `i`-th component of `A * |x|` is positive. --/ -lemma mulVec_x_abs_pos_of_irreducible {A : Matrix n n ℝ} (hA_irred : A.IsIrreducible) - {x_abs : n → ℝ} (h_x_abs_nonneg : ∀ i, 0 ≤ x_abs i) - (h_x_abs_eig : A *ᵥ x_abs = (perronRoot_alt A) • x_abs) - (hx_abs_ne_zero : x_abs ≠ 0) (i : n) : - 0 < (A *ᵥ x_abs) i := by - have h_x_abs_pos : ∀ k, 0 < x_abs k := - eigenvector_is_positive_of_irreducible hA_irred h_x_abs_eig h_x_abs_nonneg hx_abs_ne_zero - have h_r_pos : 0 < perronRoot_alt A := by - obtain ⟨i₀, j₀, hAij_pos⟩ := Matrix.Irreducible.exists_pos_entry (A := A) hA_irred - have h_sum_pos : 0 < ∑ k, A i₀ k * x_abs k := by - apply sum_pos_of_mem - · intro k _ - exact mul_nonneg (hA_irred.nonneg i₀ k) (h_x_abs_pos k).le - · exact Finset.mem_univ j₀ - · exact mul_pos hAij_pos (h_x_abs_pos j₀) - have h_eq : (A *ᵥ x_abs) i₀ = (perronRoot_alt A) * x_abs i₀ := by - simpa [Pi.smul_apply, smul_eq_mul] using congrFun h_x_abs_eig i₀ - have : 0 < (perronRoot_alt A) * x_abs i₀ := by - exact lt_of_lt_of_eq h_sum_pos h_eq - exact pos_of_mul_pos_left this (h_x_abs_pos i₀).le - have h_eq_i : (A *ᵥ x_abs) i = (perronRoot_alt A) * x_abs i := by - simpa [Pi.smul_apply, smul_eq_mul] using congrFun h_x_abs_eig i - have : 0 < (perronRoot_alt A) * x_abs i := - mul_pos h_r_pos (h_x_abs_pos i) - simpa [h_eq_i] using this - -/-- -If the triangle equality holds for an eigenvector `x` of a non-negative irreducible matrix `A`, -then the sum `s = (A * x) i` is non-zero. --/ -lemma sum_s_ne_zero_of_triangle_eq {A : Matrix n n ℝ} (hA_irred : A.IsIrreducible) - (hA_nonneg : ∀ i j, 0 ≤ A i j) - {x : n → ℂ} (h_triangle_eq : ∀ i, ‖∑ j, (A i j : ℂ) * x j‖ = ∑ j, ‖(A i j : ℂ) * x j‖) - (h_x_abs_eig : A *ᵥ (fun i => ‖x i‖) = (perronRoot_alt A) • (fun i => ‖x i‖)) - (hx_ne_zero : x ≠ 0) (i : n) : - (∑ j, (A i j : ℂ) * x j) ≠ 0 := by - let x_abs := fun i => ‖x i‖ - have hx_abs_ne_zero : x_abs ≠ 0 := by - contrapose! hx_ne_zero; ext i; exact norm_eq_zero.mp (congr_fun hx_ne_zero i) - intro hs_zero - have h_norm_s_zero : ‖∑ j, (A i j : ℂ) * x j‖ = 0 := by rw [hs_zero]; exact norm_zero - have h_sum_norm_zero : ∑ j, ‖(A i j : ℂ) * x j‖ = 0 := h_triangle_eq i ▸ h_norm_s_zero - have h_sum_A_x_abs_zero : ∑ j, A i j * x_abs j = 0 := by - simpa [norm_mul, norm_ofReal, abs_of_nonneg (hA_nonneg _ _)] using h_sum_norm_zero - have h_Ax_abs_i_zero : (A *ᵥ x_abs) i = 0 := by simpa [mulVec_apply] - have h_pos := mulVec_x_abs_pos_of_irreducible hA_irred - (by - intro k - simp) - h_x_abs_eig hx_abs_ne_zero i - exact h_pos.ne' h_Ax_abs_i_zero - - omit [Fintype n] [Nonempty n] [DecidableEq n] in -/-- If `A i j > 0` and `x j ≠ 0`, then the term `(A i j : ℂ) * x j` is non-zero. -/ -lemma term_ne_zero_of_pos_entry {A : Matrix n n ℝ} {x : n → ℂ} - {i j : n} (hAij_pos : 0 < A i j) (hxj_ne_zero : x j ≠ 0) : - (A i j : ℂ) * x j ≠ 0 := - mul_ne_zero (ofReal_ne_zero.mpr hAij_pos.ne') hxj_ne_zero - -/-- For any row `k` of an irreducible matrix with triangle equality, -all `x l` where `A k l > 0` have the same phase. -/ -lemma aligned_neighbors_of_triangle_eq {A : Matrix n n ℝ} (hA_irred : A.IsIrreducible) - (hA_nonneg : ∀ i j, 0 ≤ A i j) - {x : n → ℂ} (hx_ne_zero : x ≠ 0) - (h_triangle_eq : ∀ i, ‖∑ j, (A i j : ℂ) * x j‖ = ∑ j, ‖(A i j : ℂ) * x j‖) - (h_x_abs_eig : A *ᵥ (fun i => ‖x i‖) = (perronRoot_alt A) • (fun i => ‖x i‖)) : - ∀ k l m, 0 < A k l → 0 < A k m → x l / ↑‖x l‖ = x m / ↑‖x m‖ := by - let x_abs := fun i => ‖x i‖ - have hx_abs_nonneg : ∀ i, 0 ≤ x_abs i := fun i => norm_nonneg _ - have hx_abs_ne_zero : x_abs ≠ 0 := by - contrapose! hx_ne_zero; ext i; exact norm_eq_zero.mp (congr_fun hx_ne_zero i) - have h_x_abs_pos : ∀ k, 0 < x_abs k := - eigenvector_is_positive_of_irreducible hA_irred h_x_abs_eig hx_abs_nonneg hx_abs_ne_zero - intro k l m hAkl_pos hAkm_pos - let z l' := (A k l' : ℂ) * x l' - let s := ∑ l', z l' - have hs_ne_zero : s ≠ 0 := - sum_s_ne_zero_of_triangle_eq hA_irred hA_nonneg h_triangle_eq h_x_abs_eig hx_ne_zero k - have h_aligned_with_sum : ∀ l', z l' ≠ 0 → z l' / ↑‖z l'‖ = s / ↑‖s‖ := by - intro l' hz - have h := Complex.aligned_of_triangle_eq rfl (h_triangle_eq k) hs_ne_zero l' (by simp) hz - exact h - have h_zl_ne_zero : z l ≠ 0 := by - apply term_ne_zero_of_pos_entry hAkl_pos - exact norm_pos_iff.mp (h_x_abs_pos l) - have h_zm_ne_zero : z m ≠ 0 := by - apply term_ne_zero_of_pos_entry hAkm_pos - exact norm_pos_iff.mp (h_x_abs_pos m) - have h_align_l := h_aligned_with_sum l h_zl_ne_zero - have h_align_m := h_aligned_with_sum m h_zm_ne_zero - have h_xl_aligned : x l / ↑‖x l‖ = z l / ↑‖z l‖ := by - have h_xl_ne_zero : x l ≠ 0 := norm_pos_iff.mp (h_x_abs_pos l) - apply (Complex.aligned_of_mul_of_real_pos hAkl_pos rfl h_xl_ne_zero).symm - have h_xm_aligned : x m / ↑‖x m‖ = z m / ↑‖z m‖ := by - have h_xm_ne_zero : x m ≠ 0 := norm_pos_iff.mp (h_x_abs_pos m) - apply (Complex.aligned_of_mul_of_real_pos hAkm_pos rfl h_xm_ne_zero).symm - rw [h_xl_aligned, h_xm_aligned, h_align_l, h_align_m] - -/-- The reference phase has norm 1. -/ -lemma reference_phase_norm_one {A : Matrix n n ℝ} (hA_irred : A.IsIrreducible) - {x : n → ℂ} (hx_ne_zero : x ≠ 0) - (h_x_abs_eig : A *ᵥ (fun i => ‖x i‖) = (perronRoot_alt A) • (fun i => ‖x i‖)) : - let j₀ := Classical.arbitrary n - let c := x j₀ / ↑‖x j₀‖ - ‖c‖ = 1 := by - let x_abs := fun i => ‖x i‖ - have hx_abs_nonneg : ∀ i, 0 ≤ x_abs i := fun i => norm_nonneg _ - have hx_abs_ne_zero : x_abs ≠ 0 := by - contrapose! hx_ne_zero; ext i; exact norm_eq_zero.mp (congr_fun hx_ne_zero i) - have h_x_abs_pos : ∀ k, 0 < x_abs k := - eigenvector_is_positive_of_irreducible hA_irred h_x_abs_eig hx_abs_nonneg hx_abs_ne_zero - let j₀ := Classical.arbitrary n - let c := x j₀ / ↑‖x j₀‖ - simp_rw [norm_div, Complex.norm_ofReal, abs_of_nonneg (norm_nonneg _)] - exact div_self (h_x_abs_pos j₀).ne' - -/-- -All non-zero entries in the same row have aligned phases when triangle equality holds. --/ -lemma row_entries_aligned_of_triangle_eq {A : Matrix n n ℝ} (hA_irred : A.IsIrreducible) - (hA_nonneg : ∀ i j, 0 ≤ A i j) - {x : n → ℂ} (hx_ne_zero : x ≠ 0) - (h_triangle_eq : ∀ i, ‖∑ j, (A i j : ℂ) * x j‖ = ∑ j, ‖(A i j : ℂ) * x j‖) - (h_x_abs_eig : A *ᵥ (fun i => ‖x i‖) = (perronRoot_alt A) • (fun i => ‖x i‖)) - (k : n) : - ∀ l m, 0 < A k l → 0 < A k m → x l / ↑‖x l‖ = x m / ↑‖x m‖ := - aligned_neighbors_of_triangle_eq hA_irred hA_nonneg hx_ne_zero h_triangle_eq h_x_abs_eig k - -omit [Nonempty n] [DecidableEq n] in -/-- In a singleton type, any two elements have the same phase since they're actually equal. -/ -lemma phase_aligned_trivial - (h_card_one : Fintype.card n = 1) - {i j : n} {x : n → ℂ} : - x i / ↑‖x i‖ = x j / ↑‖x j‖ := by - have hij : i = j := by - rw [Fintype.card_eq_one_iff] at h_card_one - rcases h_card_one with ⟨x, hx⟩ - have hi : i = x := hx i - have hj : j = x := hx j - rw [hi, hj] - simp only [hij] - -/-- For an irreducible matrix, every row has at least one positive entry. -/ -lemma IsIrreducible.exists_pos_entry_in_row {A : Matrix n n ℝ} (hA_irred : A.IsIrreducible) (i : n) : - ∃ j, 0 < A i j := by - by_contra h_no_pos - push_neg at h_no_pos - have h_row_zero : ∀ j, A i j = 0 := by - intro j - have h_nonneg := hA_irred.nonneg i j - have h_not_pos := h_no_pos j - exact le_antisymm (h_no_pos j) h_nonneg - obtain ⟨i₀, j₀, hA_pos⟩ := Matrix.Irreducible.exists_pos_entry (A := A) hA_irred - letI : Quiver n := toQuiver A - have hconn := hA_irred.connected i j₀ - obtain ⟨p, hp_pos⟩ := hconn - have h_pos : p.length > 0 := hp_pos - obtain ⟨c, e, p', hp_eq, hp_len_eq⟩ := - Quiver.Path.path_decomposition_first_edge p h_pos - have hic_pos : 0 < A i c := e.down - exact (h_row_zero c).symm.not_lt hic_pos - -/-- If a complex number z ≠ 0 is a positive real multiple of another complex number w ≠ 0, - then they have the same phase (z/|z| = w/|w|). -/ -lemma phase_eq_of_positive_real_multiple {z w : ℂ} {c : ℝ} - (h_c_pos : 0 < c) (h_eq : z = (c : ℂ) * w) (h_w_ne_zero : w ≠ 0) : - z / ↑‖z‖ = w / ↑‖w‖ := by - have h_z_ne_zero : z ≠ 0 := by - intro h_z_zero - have h_cw_zero : (c : ℂ) * w = 0 := by rw [← h_eq, h_z_zero] - have h_c_ne_zero : (c : ℂ) ≠ 0 := ofReal_ne_zero.mpr h_c_pos.ne' - have h_w_zero : w = 0 := (mul_eq_zero.mp h_cw_zero).resolve_left h_c_ne_zero - contradiction - have h_z_norm : ‖z‖ = c * ‖w‖ := by - rw [h_eq, norm_mul, norm_ofReal, abs_of_nonneg h_c_pos.le] - field_simp [h_z_ne_zero, h_w_ne_zero] - calc - z * (↑‖w‖) = ↑c * w * (↑‖w‖) := by rw [h_eq] - _ = ↑c * (w * ↑‖w‖) := by ring - _ = w * (↑c * ↑‖w‖) := by ring - _ = w * ↑(c * ‖w‖) := by rw [ofReal_mul] - _ = w * ↑‖z‖ := by rw [h_z_norm] - grind only - -lemma aligned_term_of_triangle_eq {ι : Type*} {s : Finset ι} {v : ι → ℂ} - (h_sum : ‖∑ i ∈ s, v i‖ = ∑ i ∈ s, ‖v i‖) - {j : ι} (h_j : j ∈ s) (h_vj_ne_zero : v j ≠ 0) : - let sum := ∑ i ∈ s, v i - v j / ↑‖v j‖ = sum / ↑‖sum‖ := by - intro sum - have h_sum_ne_zero : sum ≠ 0 := by - intro h_sum_zero - have h_norm_sum : ‖sum‖ = 0 := by rw [h_sum_zero, norm_zero] - have h_sum_norms : ∑ i ∈ s, ‖v i‖ = 0 := by rw [← h_sum, h_norm_sum] - have h_all_zero : ∀ i ∈ s, ‖v i‖ = 0 := by - intro i hi - have h_single_nonneg : 0 ≤ ‖v i‖ := norm_nonneg (v i) - have h_sum_ge_single : ‖v i‖ ≤ ∑ j ∈ s, ‖v j‖ := - Finset.single_le_sum (fun _ _ => norm_nonneg _) hi - rw [h_sum_norms] at h_sum_ge_single - exact le_antisymm h_sum_ge_single h_single_nonneg - have h_vj_zero : ‖v j‖ = 0 := h_all_zero j h_j - exact h_vj_ne_zero (norm_eq_zero.mp h_vj_zero) - have h_aligned := Complex.aligned_of_triangle_eq rfl h_sum h_sum_ne_zero j h_j h_vj_ne_zero - exact h_aligned - -/-- When triangle equality holds for a sum and all non-zero terms have the same phase factor, - then the sum equals the sum of magnitudes times that common phase factor. - This is a key property for proving eigenvalue relationships in the complex case. -/ -lemma Complex.triangle_eq_sum_with_common_phase {ι : Type*} [Fintype ι] - {v : ι → ℂ} {c : ℂ} (_ : ‖c‖ = 1) - (h_triangle_eq : ‖∑ i, v i‖ = ∑ i, ‖v i‖) - (h_aligned : ∀ i, v i ≠ 0 → v i / ↑‖v i‖ = c) : - ∑ i, v i = (∑ i, ‖v i‖ : ℂ) * c := by - by_cases h_all_zero : ∀ i, v i = 0 - · simp only [h_all_zero, Finset.sum_const_zero, norm_zero, ofReal_zero, zero_mul] - push_neg at h_all_zero - rcases h_all_zero with ⟨j, hj_ne_zero⟩ - have h_sum_ne_zero : ∑ i, v i ≠ 0 := by - intro h_sum_zero - have h_norms_sum : ∑ i, ‖v i‖ = 0 := by - rw [← h_triangle_eq, h_sum_zero, norm_zero] - have h_all_zero : ∀ i, ‖v i‖ = 0 := by - intro i - have h_nonneg : ∀ i ∈ Finset.univ, 0 ≤ ‖v i‖ := fun i _ => norm_nonneg (v i) - exact (Finset.sum_eq_zero_iff_of_nonneg h_nonneg).mp h_norms_sum i (Finset.mem_univ i) - have h_vj_zero : ‖v j‖ = 0 := h_all_zero j - exact hj_ne_zero (norm_eq_zero.mp h_vj_zero) - have h_sum_phase : (∑ i, v i) / ↑‖∑ i, v i‖ = c := by - have h_j_aligned := h_aligned j hj_ne_zero - have h_j_sum_aligned : v j / ↑‖v j‖ = (∑ i, v i) / ↑‖∑ i, v i‖ := by - apply Complex.aligned_of_triangle_eq rfl h_triangle_eq h_sum_ne_zero j (by simp) hj_ne_zero - rw [h_j_aligned] at h_j_sum_aligned - exact id (Eq.symm h_j_sum_aligned) - calc ∑ i, v i - = ‖∑ i, v i‖ * ((∑ i, v i) / ↑‖∑ i, v i‖) := by - field_simp [h_sum_ne_zero] - _ = ‖∑ i, v i‖ * c := by rw [h_sum_phase] - _ = (∑ i, ‖v i‖ : ℂ) * c := by rw [h_triangle_eq]; rw [@ofReal_sum] - -/-- In the specific context of the Perron-Frobenius theorem, if we have an irreducible - non-negative matrix A with triangle equality for the eigenvector equation, - then the complex sum equals the real Perron root times the phase-aligned eigenvector. -/ -lemma sum_eq_perron_root_times_phase_aligned_vector - {n : Type*} [Fintype n] [Nonempty n] {A : Matrix n n ℝ} (hA_irred : A.IsIrreducible) - (hA_nonneg : ∀ i j, 0 ≤ A i j) - {x : n → ℂ} (hx_ne_zero : x ≠ 0) - (h_triangle_eq : ∀ i, ‖∑ j, (A i j : ℂ) * x j‖ = ∑ j, ‖(A i j : ℂ) * x j‖) - (h_x_abs_eig : A *ᵥ (fun i => ‖x i‖) = (perronRoot_alt A) • (fun i => ‖x i‖)) - {i : n} (c : ℂ) (h_norm_c : ‖c‖ = 1) - (h_aligned : ∀ j, A i j > 0 → x j ≠ 0 → x j / ↑‖x j‖ = c) : - ∑ j, (A i j : ℂ) * x j = (perronRoot_alt A : ℂ) * (‖x i‖ : ℂ) * c := by - let z : n → ℂ := fun j => (A i j : ℂ) * x j - have h_sum_ne_zero : ∑ j, z j ≠ 0 := by - apply sum_s_ne_zero_of_triangle_eq hA_irred hA_nonneg h_triangle_eq h_x_abs_eig hx_ne_zero i - have h_z_aligned : ∀ j, z j ≠ 0 → z j / ↑‖z j‖ = c := by - intro j hz_ne_zero - have h_A_pos : A i j > 0 := by - by_contra h_not_pos - push_neg at h_not_pos - have h_Aij_zero : A i j = 0 := by - apply le_antisymm _ (hA_nonneg i j) - exact h_not_pos - have h_z_j_zero : z j = 0 := by - simp [z, h_Aij_zero, ofReal_zero] - contradiction - have h_xj_ne_zero : x j ≠ 0 := by - by_contra h_xj_zero - have h_z_j_zero : z j = 0 := by - simp [z, h_xj_zero, mul_zero] - contradiction - have h_term_aligned : z j / ↑‖z j‖ = x j / ↑‖x j‖ := by - apply Complex.aligned_of_mul_of_real_pos h_A_pos rfl h_xj_ne_zero - rw [h_term_aligned] - exact h_aligned j h_A_pos h_xj_ne_zero - have h_sum_eq := Complex.triangle_eq_sum_with_common_phase h_norm_c (h_triangle_eq i) h_z_aligned - have h_sum_norms : ∑ j, ‖z j‖ = perronRoot_alt A * ‖x i‖ := by - calc ∑ j, ‖z j‖ - = ∑ j, ‖(A i j : ℂ) * x j‖ := by rfl - _ = ∑ j, A i j * ‖x j‖ := by - apply Finset.sum_congr rfl - intro j _ - rw [norm_mul, norm_ofReal, abs_of_nonneg (hA_nonneg i j)] - _ = (A *ᵥ (fun j => ‖x j‖)) i := by simp [mulVec_apply] - _ = ((perronRoot_alt A) • (fun j => ‖x j‖)) i := by rw [h_x_abs_eig] - _ = perronRoot_alt A * ‖x i‖ := by simp [Pi.smul_apply, smul_eq_mul] - calc ∑ j, z j - = (∑ j, ‖z j‖ : ℂ) * c := h_sum_eq - _ = (perronRoot_alt A * ‖x i‖ : ℂ) * c := by - have h_sum_norms_cast : (∑ j, ‖z j‖ : ℂ) = (perronRoot_alt A * ‖x i‖ : ℂ) := by - rw [← ofReal_mul, ← h_sum_norms]; rw [ofReal_eq_coe]; exact - Eq.symm (ofReal_sum Finset.univ fun i ↦ ‖z i‖) - rw [h_sum_norms_cast] - -/-- When triangle equality holds for a complex eigenvector equation, the vector of component norms - is an eigenvector of the real matrix with eigenvalue equal to the norm of the complex eigenvalue. -/ -lemma norm_vector_is_eigenvector_of_triangle_eq - {n : Type*} [Fintype n] [DecidableEq n] - {A : Matrix n n ℝ} (hA_nonneg : ∀ i j, 0 ≤ A i j) - {μ : ℂ} {x : n → ℂ} - (hx_eig : (A.map (algebraMap ℝ ℂ)) *ᵥ x = μ • x) - (h_triangle_eq : ∀ i, ‖∑ j, (A i j : ℂ) * x j‖ = ∑ j, ‖(A i j : ℂ) * x j‖) : - A *ᵥ (fun i => ‖x i‖) = (‖μ‖ : ℝ) • (fun i => ‖x i‖) := by - exact norm_eigenvector_is_eigenvector_of_triangle_eq hA_nonneg hx_eig h_triangle_eq - -/-- For an irreducible non-negative matrix, if the absolute values of a complex eigenvector form - a real eigenvector, then the eigenvalue's norm equals the Perron root. -/ -lemma eigenvalue_norm_eq_perron_root_of_triangle_eq - {n : Type*} [Fintype n] [Nonempty n] [DecidableEq n] - {A : Matrix n n ℝ} (hA_irred : A.IsIrreducible) (hA_nonneg : ∀ i j, 0 ≤ A i j) - {μ : ℂ} {x : n → ℂ} (hx_ne_zero : x ≠ 0) - (h_x_abs_eig : A *ᵥ (fun i => ‖x i‖) = (‖μ‖ : ℝ) • (fun i => ‖x i‖)) : - ‖μ‖ = perronRoot_alt A := by - let x_abs := fun i => ‖x i‖ - have hx_abs_nonneg : ∀ i, 0 ≤ x_abs i := fun i => norm_nonneg _ - have hx_abs_ne_zero : x_abs ≠ 0 := by - contrapose! hx_ne_zero; ext i; exact norm_eq_zero.mp (congr_fun hx_ne_zero i) - have hx_abs_pos : ∀ i, 0 < x_abs i := - eigenvector_is_positive_of_irreducible hA_irred h_x_abs_eig hx_abs_nonneg hx_abs_ne_zero - have h_mu_norm_pos : 0 < ‖μ‖ := by - have h_mu_ne_zero : μ ≠ 0 := - eigenvalue_ne_zero_of_irreducible hA_irred hx_ne_zero h_x_abs_eig - exact norm_pos_iff.mpr h_mu_ne_zero - exact eigenvalue_is_perron_root_of_positive_eigenvector - hA_irred hA_nonneg h_mu_norm_pos hx_abs_pos h_x_abs_eig - -/-- In a matrix with triangle equality, vertices that share a common predecessor have aligned phases. -/ -lemma phase_aligned_within_row - {n : Type*} [Fintype n] [Nonempty n] [DecidableEq n] - {A : Matrix n n ℝ} (hA_irred : A.IsIrreducible) (hA_nonneg : ∀ i j, 0 ≤ A i j) - {x : n → ℂ} (hx_ne_zero : x ≠ 0) - (h_triangle_eq : ∀ i, ‖∑ j, (A i j : ℂ) * x j‖ = ∑ j, ‖(A i j : ℂ) * x j‖) - (h_x_abs_eig : A *ᵥ (fun i => ‖x i‖) = (perronRoot_alt A) • (fun i => ‖x i‖)) - (i : n) (j k : n) (h_ij_pos : 0 < A i j) (h_ik_pos : 0 < A i k) : - x j / ↑‖x j‖ = x k / ↑‖x k‖ := by - apply row_entries_aligned_of_triangle_eq hA_irred hA_nonneg hx_ne_zero - h_triangle_eq h_x_abs_eig i j k h_ij_pos h_ik_pos - -/-- Phase propagation within a row: if vertices j and k both have incoming edges from i, - then they share the same phase. This is already proven as `phase_aligned_within_row`. -/ -lemma phase_propagates_within_row - {n : Type*} [Fintype n] [Nonempty n] [DecidableEq n] - {A : Matrix n n ℝ} (hA_irred : A.IsIrreducible) (hA_nonneg : ∀ i j, 0 ≤ A i j) - {x : n → ℂ} (hx_ne_zero : x ≠ 0) - (h_triangle_eq : ∀ i, ‖∑ j, (A i j : ℂ) * x j‖ = ∑ j, ‖(A i j : ℂ) * x j‖) - (h_x_abs_eig : A *ᵥ (fun i => ‖x i‖) = (perronRoot_alt A) • (fun i => ‖x i‖)) - {i j k : n} (h_ij_pos : 0 < A i j) (h_ik_pos : 0 < A i k) : - x j / ↑‖x j‖ = x k / ↑‖x k‖ := - row_entries_aligned_of_triangle_eq hA_irred hA_nonneg hx_ne_zero - h_triangle_eq h_x_abs_eig i j k h_ij_pos h_ik_pos - -/-- -If an eigenvalue `μ` of a primitive matrix `A` has norm equal to the Perron root, -then the vector of norms of its eigenvector `x`, `|x|`, is strictly positive. --/ -lemma eigenvector_norm_pos_of_primitive_and_norm_eq_perron_root - {A : Matrix n n ℝ} (hA_prim : IsPrimitive A) (hA_nonneg : ∀ i j, 0 ≤ A i j) - {μ : ℂ} (_ : μ ∈ spectrum ℂ (A.map (algebraMap ℝ ℂ))) - (_ : ‖μ‖ = perronRoot_alt A) - {x : n → ℂ} (hx_ne_zero : x ≠ 0) (_ : (A.map (algebraMap ℝ ℂ)) *ᵥ x = μ • x) - (h_x_abs_eig : A *ᵥ (fun i => ‖x i‖) = (perronRoot_alt A) • (fun i => ‖x i‖)) : - ∀ i, 0 < ‖x i‖ := by - have h_x_abs_ne_zero : (fun j => ‖x j‖) ≠ 0 := by - contrapose! hx_ne_zero - ext j - exact norm_eq_zero.mp (congr_fun hx_ne_zero j) - have h_x_abs_nonneg : ∀ j, 0 ≤ ‖x j‖ := fun j => norm_nonneg _ - have h_r_pos : 0 < perronRoot_alt A := by - obtain ⟨r', v, hr'_pos, hv_pos, h_eig'⟩ := exists_positive_eigenvector_of_primitive hA_prim hA_nonneg - have : r' = perronRoot_alt A := by - apply eigenvalue_is_perron_root_of_positive_eigenvector - · exact Matrix.IsPrimitive.isIrreducible (A := A) hA_prim - · exact hA_nonneg - · exact hr'_pos - · exact hv_pos - · exact h_eig' - rwa [← this] - exact eigenvector_of_primitive_is_positive hA_prim h_r_pos h_x_abs_eig h_x_abs_nonneg h_x_abs_ne_zero - -omit [Fintype n] [Nonempty n] [DecidableEq n] in -/-- Reference phase is unit: `‖x i₀ / ‖x i₀‖‖ = 1`. -/ -lemma reference_phase_norm_one_of_primitive - {_ : Matrix n n ℝ} {x : n → ℂ} {i₀ : n} - (hx_abs_pos : 0 < ‖x i₀‖) : - ‖x i₀ / ‖x i₀‖‖ = (1 : ℝ) := by - simp [hx_abs_pos.ne'] - -omit [Nonempty n] in -/-- The norm of a matrix-vector product equals the perron root to the kth power times the norm of the vector component. -/ -lemma norm_matrix_power_vec_eq_perron_power_norm - {A : Matrix n n ℝ} {μ : ℂ} {x : n → ℂ} - (hx_eig : (A.map (algebraMap ℝ ℂ)) *ᵥ x = μ • x) - (h_norm_eq_r : ‖μ‖ = perronRoot_alt A) - (k : ℕ) (m : n) : - ‖(((A ^ k).map (algebraMap ℝ ℂ)) *ᵥ x) m‖ = (perronRoot_alt A) ^ k * ‖x m‖ := by - have h_k_power : ((A ^ k).map (algebraMap ℝ ℂ)) *ᵥ x = (μ ^ k) • x := - pow_eigenvector_of_eigenvector' hx_eig k - have h_component : ((μ ^ k) • x) m = (μ ^ k) * x m := by simp [Pi.smul_apply] - calc ‖(((A ^ k).map (algebraMap ℝ ℂ)) *ᵥ x) m‖ - = ‖((μ ^ k) • x) m‖ := by rw [h_k_power] - _ = ‖(μ ^ k) * x m‖ := by rw [h_component] - _ = ‖μ ^ k‖ * ‖x m‖ := by rw [norm_mul] - _ = ‖μ‖ ^ k * ‖x m‖ := by rw [norm_pow] - _ = (perronRoot_alt A) ^ k * ‖x m‖ := by rw [h_norm_eq_r] - -omit [Nonempty n] in -/-- For a primitive matrix power, triangle equality holds for the eigenvector equation. -/ -lemma triangle_equality_for_primitive_power - {A : Matrix n n ℝ} (_ : IsPrimitive A) - {μ : ℂ} {x : n → ℂ} - (hx_eig : (A.map (algebraMap ℝ ℂ)) *ᵥ x = μ • x) - (h_x_abs_eig : A *ᵥ (fun i ↦ ‖x i‖) = (perronRoot_alt A) • (fun i ↦ ‖x i‖)) - (h_norm_eq_r : ‖μ‖ = perronRoot_alt A) - (m : n) (k : ℕ) (hAk_pos : ∀ i j, 0 < (A ^ k) i j) : - ‖∑ l, ((A ^ k) m l : ℂ) * x l‖ = ∑ l, ‖((A ^ k) m l : ℂ) * x l‖ := by - have h_left : ‖∑ l, ((A ^ k) m l : ℂ) * x l‖ = (perronRoot_alt A) ^ k * ‖x m‖ := by - have h_eq : ‖∑ l, ((A ^ k) m l : ℂ) * x l‖ = ‖(((A ^ k).map (algebraMap ℝ ℂ)) *ᵥ x) m‖ := by - simp_all only [coe_algebraMap] - rfl - rw [h_eq] - exact norm_matrix_power_vec_eq_perron_power_norm hx_eig h_norm_eq_r k m - have h_right : ∑ l, ‖((A ^ k) m l : ℂ) * x l‖ = (perronRoot_alt A) ^ k * ‖x m‖ := - sum_component_norms_eq_perron_power_norm h_x_abs_eig k m hAk_pos - rw [h_left, h_right] - -omit [Nonempty n] in -/-- Components align with their weighted versions under positive scaling. -/ -lemma component_phase_alignment - {A : Matrix n n ℝ} {x : n → ℂ} {k : ℕ} {m i : n} - (hAk_pos : 0 < (A ^ k) m i) - (hx_abs_pos : 0 < ‖x i‖) : - x i / ‖x i‖ = ((A ^ k) m i : ℂ) * x i / ‖((A ^ k) m i : ℂ) * x i‖ := by - have h_ne : x i ≠ 0 := norm_pos_iff.mp hx_abs_pos - exact (Complex.aligned_of_mul_of_real_pos hAk_pos rfl h_ne).symm - -/-- Phase propagation along a strictly-positive power of a primitive matrix. -/ -lemma entries_share_phase_of_primitive - {A : Matrix n n ℝ} (hA_prim : IsPrimitive A) - {μ : ℂ} {x : n → ℂ} - (hx_eig : (A.map (algebraMap ℝ ℂ)) *ᵥ x = μ • x) - (h_x_abs_eig : A *ᵥ (fun i ↦ ‖x i‖) = - (perronRoot_alt A) • (fun i ↦ ‖x i‖)) - (h_norm_eq_r : ‖μ‖ = perronRoot_alt A) - (hx_abs_pos : ∀ i, 0 < ‖x i‖) : - ∀ i j : n, x i / ‖x i‖ = x j / ‖x j‖ := by - classical - obtain ⟨k, _hk_pos, hAk_pos⟩ := hA_prim.2 - intro i j - let m := Classical.arbitrary n - have tri := triangle_equality_for_primitive_power - hA_prim hx_eig h_x_abs_eig h_norm_eq_r m k hAk_pos - have align_i := - aligned_term_of_triangle_eq tri (Finset.mem_univ i) - (term_ne_zero_of_pos_entry (hAk_pos m i) (norm_pos_iff.mp (hx_abs_pos i))) - have align_j := - aligned_term_of_triangle_eq tri (Finset.mem_univ j) - (term_ne_zero_of_pos_entry (hAk_pos m j) (norm_pos_iff.mp (hx_abs_pos j))) - have phase_i := component_phase_alignment (hAk_pos m i) (hx_abs_pos i) - have phase_j := component_phase_alignment (hAk_pos m j) (hx_abs_pos j) - trans ((A ^ k) m i : ℂ) * x i / ‖((A ^ k) m i : ℂ) * x i‖ - · exact phase_i - trans (∑ l, ((A ^ k) m l : ℂ) * x l) / ‖∑ l, ((A ^ k) m l : ℂ) * x l‖ - · exact align_i - trans ((A ^ k) m j : ℂ) * x j / ‖((A ^ k) m j : ℂ) * x j‖ - · exact align_j.symm - · exact phase_j.symm - -lemma eigenvector_phase_aligned_of_primitive - {A : Matrix n n ℝ} (hA_prim : IsPrimitive A) (_ : ∀ i j, 0 ≤ A i j) - {μ : ℂ} (h_norm_eq_r : ‖μ‖ = perronRoot_alt A) - {x : n → ℂ} (hx_eig : (A.map (algebraMap ℝ ℂ)) *ᵥ x = μ • x) - (h_x_abs_eig : A *ᵥ (fun i ↦ ‖x i‖) = (perronRoot_alt A) • (fun i ↦ ‖x i‖)) - (hx_abs_pos : ∀ i, 0 < ‖x i‖) : - ∃ c : ℂ, ‖c‖ = 1 ∧ x = fun i ↦ c * ‖x i‖ := by - classical - let i₀ : n := Classical.arbitrary _ - let c : ℂ := x i₀ / ‖x i₀‖ - have hc_norm : ‖c‖ = 1 := by - have h_pos : 0 < ‖x i₀‖ := hx_abs_pos i₀ - simp [c, h_pos.ne'] - have h_same_phase : ∀ j : n, x j / ‖x j‖ = c := by - intro j - simp_rw [c] - exact entries_share_phase_of_primitive hA_prim hx_eig h_x_abs_eig h_norm_eq_r hx_abs_pos j i₀ - refine ⟨c, hc_norm, ?_⟩ - funext j - have hnorm_ne_zero : ‖x j‖ ≠ 0 := (hx_abs_pos j).ne' - calc - x j = (x j / ‖x j‖) * ‖x j‖ := by field_simp [hnorm_ne_zero] - _ = c * ‖x j‖ := by rw [h_same_phase j] - -omit [Nonempty n] [DecidableEq n] in -/-- -If an eigenvector `x` is phase‐aligned, i.e. `x i = c * ‖x i‖` for every `i`, -then its eigenvalue `μ` is real and coincides with the eigenvalue `r` -of the real vector `‖x‖`. --/ -lemma eigenvalue_eq_of_phase_aligned - {A : Matrix n n ℝ} {μ : ℂ} {c : ℂ} (hc_norm : ‖c‖ = 1) - {x : n → ℂ} (hx_eig : (A.map (algebraMap ℝ ℂ)) *ᵥ x = μ • x) - (h_phase : ∀ i, x i = c * ‖x i‖) - {r : ℝ} (h_x_abs_eig : A *ᵥ (fun i ↦ ‖x i‖) = r • (fun i ↦ ‖x i‖)) - {i : n} (hx_abs_pos_i : 0 < ‖x i‖) : - μ = r := by - have hc_ne_zero : c ≠ 0 := by - intro hc - have : (‖(0 : ℂ)‖ : ℝ) = 1 := by - rw [hc, norm_zero] at hc_norm - aesop - norm_num at this - set x_abs : n → ℂ := fun j ↦ (‖x j‖ : ℂ) with hx_abs_def - have hx_repr : x = fun j ↦ c * x_abs j := by - funext j - rw [h_phase j, hx_abs_def] - have h_factored : - c • ((A.map (algebraMap ℝ ℂ)) *ᵥ x_abs) = c • (μ • x_abs) := by - have : (A.map (algebraMap ℝ ℂ)) *ᵥ x = μ • x := hx_eig - rw [hx_repr] at this - have h_left : (A.map (algebraMap ℝ ℂ)) *ᵥ (fun j ↦ c * x_abs j) = - c • ((A.map (algebraMap ℝ ℂ)) *ᵥ x_abs) := by - rw [← mulVec_smul]; rw [hx_abs_def]; simp; rfl - have h_right : μ • (fun j ↦ c * x_abs j) = c • (μ • x_abs) := by - ext j - simp only [Pi.smul_apply, smul_eq_mul] - ring - rw [h_left, h_right] at this - exact this - have h_cancelled : - (A.map (algebraMap ℝ ℂ)) *ᵥ x_abs = μ • x_abs := by - have := congrArg (fun v : n → ℂ ↦ c⁻¹ • v) h_factored - simp only at this - have h_left : c⁻¹ • (c • ((A.map (algebraMap ℝ ℂ)) *ᵥ x_abs)) = (A.map (algebraMap ℝ ℂ)) *ᵥ x_abs := by - rw [smul_smul, inv_mul_cancel₀ hc_ne_zero, one_smul] - have h_right : c⁻¹ • (c • (μ • x_abs)) = μ • x_abs := by - rw [smul_smul, ← smul_smul] - have : c⁻¹ * c * μ = μ := by - rw [mul_assoc]; rw [propext (inv_mul_eq_iff_eq_mul₀ hc_ne_zero)] - rw [propext (inv_smul_eq_iff₀ hc_ne_zero)] - rw [h_left, h_right] at this - exact this - have h_real : - (A *ᵥ fun j ↦ ‖x j‖) i = r * ‖x i‖ := by - rw [h_x_abs_eig] - simp only [Pi.smul_apply, smul_eq_mul] - have h_real_C : - ((A.map (algebraMap ℝ ℂ)) *ᵥ x_abs) i = (r : ℂ) * x_abs i := by - have h_sum : (A.map (algebraMap ℝ ℂ)) *ᵥ x_abs = - fun j ↦ ∑ k, (A j k : ℂ) * (‖x k‖ : ℂ) := by - ext j - rfl - have h_real_sum : (A *ᵥ fun j ↦ ‖x j‖) i = - ∑ k, A i k * ‖x k‖ := by - rfl - calc ((A.map (algebraMap ℝ ℂ)) *ᵥ x_abs) i - = ∑ k, (A i k : ℂ) * (‖x k‖ : ℂ) := by rw [h_sum] - _ = (∑ k, A i k * ‖x k‖ : ℂ) := by - simp only - _ = ((A *ᵥ fun j ↦ ‖x j‖) i : ℂ) := by - rw [h_real_sum]; simp - _ = (r * ‖x i‖ : ℂ) := by rw [h_real]; simp - _ = (r : ℂ) * (‖x i‖ : ℂ) := by simp only - _ = (r : ℂ) * x_abs i := by rw [hx_abs_def] - have h_key : (r : ℂ) * x_abs i = μ * x_abs i := by - rw [← h_real_C] - have := congr_fun h_cancelled i - simp only [Pi.smul_apply, smul_eq_mul] at this - exact this - have h_norm_ne_zero : x_abs i ≠ 0 := by - rw [hx_abs_def] - exact Complex.ofReal_ne_zero.mpr hx_abs_pos_i.ne' - have h_final : (r : ℂ) = μ := by - apply (mul_right_cancel₀ h_norm_ne_zero) - exact h_key - exact h_final.symm - -theorem spectral_dominance_of_primitive - {A : Matrix n n ℝ} (hA_prim : IsPrimitive A) - (hA_nonneg : ∀ i j, 0 ≤ A i j) - {μ : ℂ} (h_is_eigenvalue : μ ∈ spectrum ℂ (A.map (algebraMap ℝ ℂ))) - (h_norm_eq_r : ‖μ‖ = perronRoot_alt A) : - μ = perronRoot_alt A := by - -- 1. we obtain a (non-zero) eigenvector `x` corresponding to `μ`. - let B := A.map (algebraMap ℝ ℂ) - have h_spec : μ ∈ spectrum ℂ (toLin' B) := by - rwa [spectrum.Matrix_toLin'_eq_spectrum] - obtain ⟨x, hx_ne_zero, hx_eig_lin⟩ := Module.End.exists_eigenvector_of_mem_spectrum h_spec - have hx_eig : B *ᵥ x = μ • x := by rwa [toLin'_apply] at hx_eig_lin - -- 2. we build the sub-invariance inequality r • |x| ≤ A ⋅ |x|. - have h_subinv : - (perronRoot_alt A) • (fun i => ‖x i‖) ≤ A *ᵥ (fun i => ‖x i‖) := by - have := eigenvalue_abs_subinvariant hA_nonneg hx_eig - simpa [h_norm_eq_r] using this - -- 3. we upgrade sub-invariance to equality, so `|x|` is a Perron eigenvector. - have h_x_abs_eig : - A *ᵥ (fun i => ‖x i‖) = (perronRoot_alt A) • (fun i => ‖x i‖) := by - have hA_irred : A.IsIrreducible := Matrix.IsPrimitive.isIrreducible (A := A) hA_prim - have hx_abs_nonneg : ∀ i, 0 ≤ ‖x i‖ := fun _ ↦ norm_nonneg _ - have hx_abs_ne_zero : (fun i => ‖x i‖) ≠ 0 := by - intro h_abs - have : x = 0 := by - funext i - have : ‖x i‖ = 0 := congrFun h_abs i - exact (norm_eq_zero).1 this - exact hx_ne_zero this - exact - subinvariant_equality_implies_eigenvector - hA_irred hA_nonneg hx_abs_nonneg hx_abs_ne_zero h_subinv - -- 4. we turn the triangle inequality into equality. - have h_triangle_eq : - ∀ i, ‖∑ j, (A i j : ℂ) * x j‖ = ∑ j, ‖(A i j : ℂ) * x j‖ := - triangle_equality_of_norm_eq_perron_root - hA_nonneg hx_eig h_norm_eq_r h_x_abs_eig - -- 5. Strict positivity of `|x|`. - have hx_abs_pos : ∀ i, 0 < ‖x i‖ := - eigenvector_norm_pos_of_primitive_and_norm_eq_perron_root - hA_prim hA_nonneg h_is_eigenvalue h_norm_eq_r - hx_ne_zero hx_eig h_x_abs_eig - -- 6. Global phase alignment of the complex eigenvector `x`. - obtain ⟨c, hc_norm, h_phase⟩ := - eigenvector_phase_aligned_of_primitive - hA_prim hA_nonneg h_norm_eq_r - hx_eig h_x_abs_eig hx_abs_pos - -- μ = r from the phase-aligned situation. - have hμ_eq_r : - μ = perronRoot_alt A := - eigenvalue_eq_of_phase_aligned - hc_norm - hx_eig - (by - intro i - exact congrFun h_phase i) - h_x_abs_eig - (hx_abs_pos (Classical.arbitrary n)) - exact hμ_eq_r - -/-- -**Spectral Dominance for Primitive Matrices** -(Seneta 1.1 (c)). -If `A` is primitive with Perron root `r`, every eigenvalue `μ ≠ r` -satisfies `‖μ‖ < r`. --/ -theorem spectral_dominance_of_primitive' - (hA_prim : IsPrimitive A) (hA_nonneg : ∀ i j, 0 ≤ A i j) - (μ : ℂ) (h_is_eigenvalue : μ ∈ spectrum ℂ (A.map (algebraMap ℝ ℂ))) - (h_ne_perron : μ ≠ perronRoot_alt A) : - ‖μ‖ < perronRoot_alt A := by - have hA_irred : A.IsIrreducible := Matrix.IsPrimitive.isIrreducible (A := A) hA_prim - have h_le : ‖μ‖ ≤ perronRoot_alt A := by - exact @eigenvalue_abs_le_perron_root n _ _ _ A hA_irred hA_nonneg μ h_is_eigenvalue - have h_lt_or_eq : ‖μ‖ < perronRoot_alt A ∨ ‖μ‖ = perronRoot_alt A := - lt_or_eq_of_le h_le - cases h_lt_or_eq with - | inl h_lt => exact h_lt - | inr h_eq => - have h_eqμ : μ = perronRoot_alt A := by - exact @spectral_dominance_of_primitive n _ _ _ A hA_prim hA_nonneg μ h_is_eigenvalue h_eq - exact (h_ne_perron h_eqμ).elim diff --git a/lean/MCMC/PF/LinearAlgebra/Matrix/PerronFrobenius/Irreducible.lean b/lean/MCMC/PF/LinearAlgebra/Matrix/PerronFrobenius/Irreducible.lean deleted file mode 100644 index 3c3b1fa..0000000 --- a/lean/MCMC/PF/LinearAlgebra/Matrix/PerronFrobenius/Irreducible.lean +++ /dev/null @@ -1,622 +0,0 @@ -import Mathlib.LinearAlgebra.Matrix.Irreducible.Defs -import Mathlib.Combinatorics.Quiver.Path -import MCMC.PF.LinearAlgebra.Matrix.PerronFrobenius.Lemmas -import MCMC.PF.LinearAlgebra.Matrix.PerronFrobenius.Uniqueness - -open Quiver.Path -namespace Matrix -open Quiver - -open CollatzWielandt -variable {n : Type*} [DecidableEq n] -variable {A : Matrix n n ℝ} - -/-- If `A` is irreducible then so is `1 + A`. -/ -theorem Irreducible.add_one (h_irred : A.IsIrreducible) : (1 + A).IsIrreducible := by - let B := (1 : Matrix n n ℝ) + A - constructor - · intro i j - by_cases h : i = j - · subst h - simpa [B] using by - have : (0 : ℝ) ≤ A i i := h_irred.1 i i - linarith - · simpa [B, h] using h_irred.1 i j - · intro i j - -- Work in the quiver of `A` to extract a positive-length path. - letI : Quiver n := toQuiver A - obtain ⟨pA, hpA_pos⟩ := h_irred.connected i j - -- Any arrow in `A.toQuiver` is also an arrow in `(1 + A).toQuiver`. - have map_edge : ∀ {u v : n}, (0 < A u v) → (0 < B u v) := by - intro u v huv - by_cases h_eq : u = v - · subst h_eq - have : (0 : ℝ) < 1 + A u u := by linarith - simpa [B] using this - · simpa [B, h_eq] using huv - -- Lift paths from `toQuiver A` to `toQuiver B`, preserving length. - let pA' : @Quiver.Path n (toQuiver A) i j := pA - let rec liftPath_len : - ∀ {u v : n} (p : @Quiver.Path n (toQuiver A) u v), - Σ p' : @Quiver.Path n (toQuiver B) u v, - PLift - ((@Quiver.Path.length n (toQuiver B) u v p') = - (@Quiver.Path.length n (toQuiver A) u v p)) - | u, _, @Quiver.Path.nil n (toQuiver A) u => - ⟨@Quiver.Path.nil n (toQuiver B) u, ⟨by simp⟩⟩ - | u, _, @Quiver.Path.cons n (toQuiver A) u b c p e => - let ⟨p', hp'⟩ := liftPath_len p - have eA : 0 < A b c := e.down - ⟨@Quiver.Path.cons n (toQuiver B) u b c p' ⟨map_edge eA⟩, - ⟨by simp [hp'.down]⟩⟩ - obtain ⟨pB, hp_len⟩ := liftPath_len pA' - have hpA_pos' : 0 < (@Quiver.Path.length n (toQuiver A) i j pA') := by - simpa using hpA_pos - have hpB_pos : 0 < (@Quiver.Path.length n (toQuiver B) i j pB) := by - simpa [hp_len.down] using hpA_pos' - -- Return a `B.toQuiver`-path witness. - letI : Quiver n := toQuiver B - have hpB_pos' : 0 < pB.length := by - simpa using hpB_pos - exact ⟨pB, hpB_pos'⟩ - -/- -A non-zero, non-negative eigenvector of an irreducible matrix is -in fact strictly positive. --/ -lemma eigenvector_no_zero_entries_of_irreducible [Fintype n] - {r : ℝ} (hA_irred : A.IsIrreducible) (_ : 0 < r) - {v : n → ℝ} (h_eig : A *ᵥ v = r • v) - (hv_nonneg : ∀ i, 0 ≤ v i) (hv_ne_zero : v ≠ 0) : - ∀ i, 0 < v i := by - by_contra h_has_zero - push_neg at h_has_zero - obtain ⟨i₀, hi₀_zero⟩ := h_has_zero - let S : Set n := { i | 0 < v i } - let T : Set n := { i | v i = 0 } - have hS_nonempty : S.Nonempty := exists_pos_of_ne_zero hv_nonneg hv_ne_zero - have hT_nonempty : T.Nonempty := ⟨i₀, by simp [T, le_antisymm hi₀_zero (hv_nonneg i₀)]⟩ - have hT_ne_univ : T ≠ Set.univ := by - intro h_univ - have hv_zero : v = 0 := by - funext i - have : i ∈ T := by - have : i ∈ (Set.univ : Set n) := by trivial - simp_all [ne_eq, Set.mem_univ, T, S] - simpa [T] using this - exact hv_ne_zero hv_zero - obtain ⟨j, hj_T, i, hi_not_T, h_Aji_pos⟩ := - Irreducible.exists_edge_out (A := A) hA_irred T hT_nonempty hT_ne_univ - have vi_pos : 0 < v i := by - have h_vi_ne_zero : v i ≠ 0 := by - intro h_eq - have : i ∈ T := by simp [T, h_eq] - exact hi_not_T this - exact lt_of_le_of_ne (hv_nonneg i) (Ne.symm h_vi_ne_zero) - have vj_zero : v j = 0 := by - have : j ∈ T := hj_T - simpa [T] using this - have h_Av_j_zero : (A *ᵥ v) j = 0 := by - have h_rvj_zero : (r • v) j = 0 := by - simp [Pi.smul_apply, vj_zero] - have h_comp := congrArg (fun f : n → ℝ => f j) h_eig - simpa [h_rvj_zero] using h_comp - have h_terms_nonneg : ∀ k, 0 ≤ A j k * v k := - fun k => mul_nonneg (hA_irred.1 _ _) (hv_nonneg k) - have h_Aji_vi_zero : A j i * v i = 0 := by - by_cases h_eq : A j i * v i = 0 - · exact h_eq - · have h_pos : 0 < A j i * v i := by - have h_nonneg_i := h_terms_nonneg i - exact lt_of_le_of_ne h_nonneg_i (Ne.symm h_eq) - have h_sum_pos : 0 < ∑ k : n, A j k * v k := by - have h_mem : i ∈ (Finset.univ : Finset n) := by simp - have h_nonneg_fun : ∀ k ∈ (Finset.univ : Finset n), 0 ≤ A j k * v k := - fun k _ => h_terms_nonneg k - exact sum_pos_of_mem h_nonneg_fun i h_mem h_pos - have : (∑ k : n, A j k * v k) = 0 := by - simpa [mulVec, dotProduct] using h_Av_j_zero - exact (lt_irrefl (0 : ℝ) (by simp [this] at h_sum_pos)).elim - have h_Aji_zero : A j i = 0 := - (mul_eq_zero.mp h_Aji_vi_zero).resolve_right vi_pos.ne' - have : (0 : ℝ) < 0 := by - simp [h_Aji_zero] at h_Aji_pos - exact this.false - -variable {n : Type*} [Fintype n] [DecidableEq n] -variable {A : Matrix n n ℝ} - -/-- **Perron–Frobenius, irreducible case (Existence part)** -If `A` is a non-negative irreducible matrix, then there exists a strictly positive eigenvalue `r > 0` -and a strictly positive eigenvector `v` (`∀ i, 0 < v i`) such that `A *ᵥ v = r • v`. - -The proof uses the auxiliary matrix `B = 1 + A`, which is primitive, to apply the Perron-Frobenius theorem -for primitive matrices and translate the result back to `A`. -/ -theorem exists_positive_eigenvector_of_irreducible [Nonempty n] - (hA_irred : A.IsIrreducible) : - ∃ (r : ℝ) (v : n → ℝ), - 0 < r ∧ (∀ i, 0 < v i) ∧ A *ᵥ v = r • v := by - -- 1. We add the identity: `B := 1 + A`. - let B : Matrix n n ℝ := 1 + A - -- 1a. Non-negativity of `B`. - have hB_nonneg : ∀ i j, 0 ≤ B i j := by - intro i j - by_cases h_eq : i = j - · subst h_eq - have : (0 : ℝ) ≤ 1 + A i i := by - have hAi : 0 ≤ A i i := hA_irred.1 i i - linarith - simpa [B] using this - · have : 0 ≤ A i j := hA_irred.1 i j - simpa [B, h_eq] using this - -- 1b. Positive diagonal entries of `B`. - have hB_diag_pos : ∀ i, 0 < B i i := by - intro i - have : (0 : ℝ) < 1 + A i i := by - have hAi : 0 ≤ A i i := hA_irred.1 i i - linarith - simpa [B] using this - -- 1c. `B` is irreducible. - have hB_irred : (1 + A).IsIrreducible := Irreducible.add_one (A := A) hA_irred - -- 1d. `B` is primitive. - have hB_prim : B.IsPrimitive := - IsPrimitive.of_irreducible_pos_diagonal B hB_nonneg hB_irred hB_diag_pos - -- 2. Primitive Perron–Frobenius applied to `B`. - obtain ⟨rB, v, hrB_pos, hv_pos, h_eig_B⟩ := - exists_positive_eigenvector_of_primitive (A := B) hB_prim hB_nonneg - -- 3. We translate the eigen-relation for `B` to one for `A`. - have h_eig_A : A *ᵥ v = (rB - 1) • v := by - have h_exp : v + A *ᵥ v = rB • v := by - simpa [B, add_mulVec, one_mulVec] using h_eig_B - have : A *ᵥ v = rB • v - v := eq_sub_of_add_eq' h_exp - simpa [one_smul, sub_smul] using this - -- 4. We show that `rB - 1 > 0`. - classical - letI GA : Quiver n := toQuiver A - -- 4a. We find a positive entry of `A`. - have h_pos_entry : ∃ i j, 0 < A i j := by - let i₀ : n := Classical.arbitrary n - obtain ⟨p₀, hp₀_len⟩ := hA_irred.connected i₀ i₀ - rcases Quiver.Path.path_decomposition_first_edge p₀ hp₀_len with - ⟨j, e, -, -, -⟩ - exact ⟨i₀, j, e.down⟩ - rcases h_pos_entry with ⟨i₀, j₀, hA_pos⟩ - -- 4b. The `i₀`-component of `A * v` is positive. - have hAv_i₀_pos : 0 < (A *ᵥ v) i₀ := by - have hvj₀_pos : 0 < v j₀ := hv_pos j₀ - have h_nonneg : - ∀ k ∈ (Finset.univ : Finset n), 0 ≤ A i₀ k * v k := by - intro k _ - exact mul_nonneg (hA_irred.1 _ _) (le_of_lt (hv_pos k)) - have h_sum_pos : - 0 < ∑ k, A i₀ k * v k := by - have h_mem : j₀ ∈ (Finset.univ : Finset n) := by simp - have h_pos_term : 0 < A i₀ j₀ * v j₀ := by - exact mul_pos hA_pos hvj₀_pos - exact sum_pos_of_mem h_nonneg j₀ h_mem h_pos_term - simpa [mulVec_apply] using h_sum_pos - -- 4c. We use the `i₀`-component of the eigen-equation for `B`. - have h_comp_eq : - (v i₀) + (A *ᵥ v) i₀ = rB * v i₀ := by - have := congr_fun h_eig_B i₀ - simpa [B, add_mulVec, one_mulVec, add_apply, Pi.smul_apply, smul_eq_mul] using this - have hrB_gt_one : 1 < rB := by - have hv_i₀_pos : 0 < v i₀ := hv_pos i₀ - have h_lhs_gt : v i₀ < rB * v i₀ := by - have : v i₀ + (A *ᵥ v) i₀ > v i₀ := by - have : (A *ᵥ v) i₀ > 0 := hAv_i₀_pos; linarith - simpa [h_comp_eq] using this - exact ((mul_lt_mul_iff_of_pos_right hv_i₀_pos).1 - (by simpa [one_mul] using h_lhs_gt)) - have hrA_pos : 0 < rB - 1 := sub_pos.mpr hrB_gt_one - exact ⟨rB - 1, v, hrA_pos, hv_pos, h_eig_A⟩ - -/-! A non-zero, non-negative eigenvector of an irreducible matrix is in fact **strictly** positive. -/ -lemma eigenvector_is_positive_of_irreducible [Nonempty n] {r : ℝ} - (hA_irred : A.IsIrreducible) - {v : n → ℝ} (h_eig : A *ᵥ v = r • v) - (hv_nonneg : ∀ i, 0 ≤ v i) (hv_ne_zero : v ≠ 0) : - ∀ i, 0 < v i := by - by_contra h_has_nonpos - push_neg at h_has_nonpos -- `∃ i, v i ≤ 0` - rcases h_has_nonpos with ⟨i₀, hvi₀_le⟩ - let S : Set n := {i | 0 < v i} - let T : Set n := {i | v i = 0} - have h_partition : ∀ i, i ∈ S ↔ v i > 0 := by - intro i; simp [S] - have h_complement : ∀ i, i ∈ T ↔ v i = 0 := by - intro i; simp [T] - have hS_nonempty : S.Nonempty := - exists_pos_of_ne_zero hv_nonneg hv_ne_zero - have hT_nonempty : T.Nonempty := by - have h_eq : v i₀ = 0 := by - have : 0 ≤ v i₀ := hv_nonneg i₀ - exact le_antisymm hvi₀_le this - exact ⟨i₀, by simp [T, h_eq]⟩ - have hS_ne_univ : (S : Set n) ≠ Set.univ := by - intro h_univ - have : (0 : ℝ) < 0 := by - have : 0 < v i₀ := by - have : i₀ ∈ S := by - have : i₀ ∈ (Set.univ : Set n) := by trivial - simp_all only [ne_eq, Set.mem_setOf_eq, gt_iff_lt, implies_true, Set.mem_univ, S, T] - simpa [S] using this - have : v i₀ = 0 := by - have : 0 ≤ v i₀ := hv_nonneg i₀ - exact le_antisymm hvi₀_le this - simpa [this] using ‹0 < v i₀› - exact (lt_irrefl (0 : ℝ)) this - obtain ⟨j, i, hjT, hiS, hAji_pos⟩ := - exists_connecting_edge_of_irreducible - (A := A) hA_irred hv_nonneg S T hS_nonempty hT_nonempty - h_partition h_complement - have vj_zero : v j = 0 := by - have : j ∈ T := hjT - simpa [T] using this - have h_Av_j_zero : (A *ᵥ v) j = 0 := by - have hrvj : (r • v) j = 0 := by simp [vj_zero] - have h_eq := congrArg (fun f : n → ℝ => f j) h_eig - simpa [hrvj] using h_eq - have h_nonneg : ∀ k ∈ (Finset.univ : Finset n), 0 ≤ A j k * v k := by - intro k _ - exact mul_nonneg (hA_irred.1 j k) (hv_nonneg k) - have h_Aji_vi_zero : A j i * v i = 0 := by - have h_sum_zero : (∑ k, A j k * v k) = 0 := by - simpa [mulVec_apply] using h_Av_j_zero - have h_all_zero := - (Finset.sum_eq_zero_iff_of_nonneg h_nonneg).1 h_sum_zero - exact h_all_zero i (Finset.mem_univ i) - have vi_pos : 0 < v i := by simpa [S] using hiS - have h_Aji_zero : A j i = 0 := - (mul_eq_zero.mp h_Aji_vi_zero).resolve_right vi_pos.ne' - exact (lt_irrefl (0 : ℝ)) (by simp [h_Aji_zero] at hAji_pos) - -open Finset -/-- -Given an irreducible non-negative matrix `A` and two strictly positive -eigenvectors for the same positive eigenvalue, they differ by a positive -scalar. --/ -theorem uniqueness_of_positive_eigenvector_gen - {n : Type*} [Fintype n] [DecidableEq n] [Nonempty n] - {A : Matrix n n ℝ} {r : ℝ} (hA_irred : A.IsIrreducible) (hr_pos : 0 < r) - {v w : n → ℝ} - (hv_pos : ∀ i, 0 < v i) (hw_pos : ∀ i, 0 < w i) - (hv_eig : A *ᵥ v = r • v) (hw_eig : A *ᵥ w = r • w) : - ∃ c : ℝ, 0 < c ∧ v = c • w := by - -- 1. c := infᵢ (vᵢ / wᵢ) - let c : ℝ := Finset.univ.inf' Finset.univ_nonempty (fun i : n => v i / w i) - have hc_pos : 0 < c := by - apply Finset.inf'_pos Finset.univ_nonempty - intro i _ - exact div_pos (hv_pos i) (hw_pos i) - -- 2. z := v − c•w (still an eigenvector) - let z : n → ℝ := v - c • w - have hz_eig : A *ᵥ z = r • z := by - calc - A *ᵥ z - = A *ᵥ v - A *ᵥ (c • w) := by - simp only [mulVec_sub, z] - _ = r • v - c • (r • w) := by - simp only [hv_eig, mulVec_smul, hw_eig] - _ = r • (v - c • w) := by - rw [smul_sub, smul_comm, ← smul_assoc] - _ = r • z := by - simp only [z] - -- 3. z ≥ 0 - have hz_nonneg : ∀ i, 0 ≤ z i := by - intro i - have h_le : c ≤ v i / w i := - Finset.inf'_le _ (Finset.mem_univ _) - have h_mul : c * w i ≤ v i := by - exact (le_div_iff₀ (hw_pos i)).mp h_le - have : 0 ≤ v i - c * w i := sub_nonneg.mpr h_mul - simpa [z, Pi.sub_apply, Pi.smul_apply, smul_eq_mul] using this - -- 4. analyse `z` - by_cases hz_zero : z = 0 - · -- 4a. (`z = 0`) ⇒ `v = c • w` - refine ⟨c, hc_pos, ?_⟩ - have h_v_eq : v = c • w := by - have : v - c • w = 0 := by - simpa [z] using hz_zero - exact (sub_eq_zero.1 this) - exact h_v_eq - · -- 4b. (`z ≠ 0`) ⇒ contradiction - have hz_pos : ∀ i, 0 < z i := - eigenvector_no_zero_entries_of_irreducible - hA_irred hr_pos hz_eig hz_nonneg hz_zero - -- the infimum is attained - obtain ⟨i₀, _, h_inf_eq⟩ := - Finset.exists_mem_eq_inf' Finset.univ_nonempty - (fun i : n => v i / w i) - -- at the attaining index we must have `z i₀ = 0`, contradiction - have hzi₀_zero : z i₀ = 0 := by - have hv_eq : v i₀ = c * w i₀ := by - have hw_ne : w i₀ ≠ 0 := (ne_of_gt (hw_pos i₀)) - have h_div : v i₀ / w i₀ = c := by - simpa using h_inf_eq.symm - have : v i₀ = (v i₀ / w i₀) * w i₀ := by - field_simp [hw_ne] - simpa [h_div] using this - simp only [Pi.sub_apply, hv_eq, Pi.smul_apply, smul_eq_mul, sub_self, z] - have : 0 < z i₀ := hz_pos i₀ - have : (0 : ℝ) < 0 := by - simp only [hzi₀_zero, lt_self_iff_false, z] at this - simp_all only [div_pos_iff_of_pos_left, Pi.sub_apply, Pi.smul_apply, - smul_eq_mul, sub_nonneg, sub_pos, mem_univ, lt_self_iff_false, z, c] - - -/-- **Perron–Frobenius, primitive case (existence, positvity and uniqueness)** -/ -theorem pft_primitive - {n : Type*} [Fintype n] [Nonempty n] [DecidableEq n] - {A : Matrix n n ℝ} (hA_prim : IsPrimitive A) - (hA_nonneg : ∀ i j, 0 ≤ A i j) : - ∃! (v : stdSimplex ℝ n), ∃ (r : ℝ) (_ : r > 0), A *ᵥ v.val = r • v.val := by - obtain ⟨r, v_raw, hr_pos, hv_raw_pos, hv_raw_eig⟩ := - exists_positive_eigenvector_of_primitive hA_prim hA_nonneg - let s : ℝ := ∑ i, v_raw i - have hs_pos : 0 < s := - Finset.sum_pos (fun i _ ↦ hv_raw_pos i) Finset.univ_nonempty - have hs_ne : s ≠ 0 := ne_of_gt hs_pos - let v0 : n → ℝ := s⁻¹ • v_raw - have hv0_nonneg : ∀ i, 0 ≤ v0 i := by - intro i - have h₁ : 0 ≤ s⁻¹ := inv_nonneg.mpr (le_of_lt hs_pos) - have h₂ : 0 ≤ v_raw i := (hv_raw_pos i).le - simp only [v0, Pi.smul_apply, smul_eq_mul] - exact mul_nonneg h₁ h₂ - have h_sum_v0 : ∑ i, v0 i = 1 := by - calc - ∑ i, v0 i - = ∑ i, s⁻¹ * v_raw i := by simp [v0, smul_eq_mul] - _ = s⁻¹ * ∑ i, v_raw i := by - rw [Finset.mul_sum] - _ = s⁻¹ * s := by simp [s] - _ = 1 := by field_simp [hs_ne] - have hv0_simplex : v0 ∈ stdSimplex ℝ n := ⟨hv0_nonneg, h_sum_v0⟩ - have hv0_pos : ∀ i, 0 < v0 i := by - intro i - have h₁ : 0 < s⁻¹ := inv_pos.mpr hs_pos - have h₂ : 0 < v_raw i := hv_raw_pos i - simp only [v0, Pi.smul_apply, smul_eq_mul] - exact mul_pos h₁ h₂ - have hv0_eig : A *ᵥ v0 = r • v0 := by - calc - A *ᵥ v0 = A *ᵥ (s⁻¹ • v_raw) := rfl - _ = s⁻¹ • (A *ᵥ v_raw) := by rw [mulVec_smul] - _ = s⁻¹ • (r • v_raw) := by rw [hv_raw_eig] - _ = r • (s⁻¹ • v_raw) := by rw [smul_comm] - _ = r • v0 := rfl - refine ⟨⟨v0, hv0_simplex⟩, ?_, ?_⟩ - · exact ⟨r, hr_pos, hv0_eig⟩ - · intro w ⟨r', hr'_pos, hw_eig⟩ - have hw_nonneg : ∀ i, 0 ≤ w.1 i := w.property.1 - have hw_ne_zero := ne_zero_of_mem_stdSimplex w.property - have hw_pos : ∀ i, 0 < w.1 i := - eigenvector_of_primitive_is_positive hA_prim hr'_pos - hw_eig hw_nonneg hw_ne_zero - let c : ℝ := Finset.univ.inf' Finset.univ_nonempty - (fun i : n => v0 i / w.1 i) - have hc_pos : 0 < c := by - apply Finset.inf'_pos Finset.univ_nonempty - intro i _ - exact div_pos (hv0_pos i) (hw_pos i) - obtain ⟨i₀, _, hc_eq⟩ := - Finset.exists_mem_eq_inf' Finset.univ_nonempty - (fun i : n => v0 i / w.1 i) - have w_i₀_pos : 0 < w.1 i₀ := hw_pos i₀ - have v0_ge_cw : ∀ j, c * w.1 j ≤ v0 j := by - intro j - have h_le : c ≤ v0 j / w.1 j := - Finset.inf'_le _ (Finset.mem_univ j) - exact (le_div_iff₀ (hw_pos j)).mp h_le - have h_sum : - c * (A *ᵥ w.1) i₀ ≤ (A *ᵥ v0) i₀ := by - dsimp [Matrix.mulVec, dotProduct] - rw [Finset.mul_sum] - apply Finset.sum_le_sum - intro j _ - have h_le : c * w.1 j ≤ v0 j := v0_ge_cw j - have hA : 0 ≤ A i₀ j := hA_nonneg i₀ j - have h := mul_le_mul_of_nonneg_left h_le hA - rwa [mul_left_comm] - have hv0_i₀ : (A *ᵥ v0) i₀ = r * v0 i₀ := by - rw [hv0_eig] - simp only [Pi.smul_apply, smul_eq_mul] - have hw_i₀ : (A *ᵥ w.1) i₀ = r' * w.1 i₀ := by - rw [hw_eig] - simp only [Pi.smul_apply, smul_eq_mul] - have v0_i₀_eq : v0 i₀ = c * w.1 i₀ := by - have : c = v0 i₀ / w.1 i₀ := hc_eq - have w_ne : w.1 i₀ ≠ 0 := ne_of_gt w_i₀_pos - field_simp [this, w_ne] - simp [*] - have h_r_ge_r' : r ≥ r' := by - have h_pos : 0 < c * w.1 i₀ := mul_pos hc_pos w_i₀_pos - have h1 : c * r' * w.1 i₀ ≤ r * v0 i₀ := by - calc - c * r' * w.1 i₀ = c * (r' * w.1 i₀) := by ring - _ = c * (A *ᵥ w.1) i₀ := by rw [hw_i₀] - _ ≤ (A *ᵥ v0) i₀ := h_sum - _ = r * v0 i₀ := hv0_i₀ - have h2 : c * r' * w.1 i₀ ≤ r * (c * w.1 i₀) := by - rwa [v0_i₀_eq] at h1 - have h2' : r' * (c * w.1 i₀) ≤ r * (c * w.1 i₀) := by - simpa [mul_comm, mul_left_comm, mul_assoc] using h2 - exact le_of_mul_le_mul_right h2' h_pos - let d : ℝ := Finset.univ.inf' Finset.univ_nonempty - (fun i : n => w.1 i / v0 i) - have hd_pos : 0 < d := by - apply Finset.inf'_pos Finset.univ_nonempty - intro i _ - exact div_pos (hw_pos i) (hv0_pos i) - obtain ⟨j₀, _, hd_eq⟩ := - Finset.exists_mem_eq_inf' Finset.univ_nonempty - (fun i : n => w.1 i / v0 i) - have v0_j₀_pos : 0 < v0 j₀ := hv0_pos j₀ - have w_ge_dv0 : ∀ j, d * v0 j ≤ w.1 j := by - intro j - have h_le : d ≤ w.1 j / v0 j := - Finset.inf'_le _ (Finset.mem_univ j) - exact (le_div_iff₀ (hv0_pos j)).mp h_le - have h_sum2 : - d * (A *ᵥ v0) j₀ ≤ (A *ᵥ w.1) j₀ := by - dsimp only [mulVec, dotProduct] - rw [Finset.mul_sum] - apply Finset.sum_le_sum - intro j _ - have h_le : d * v0 j ≤ w.1 j := w_ge_dv0 j - have hA : 0 ≤ A j₀ j := hA_nonneg j₀ j - have h := mul_le_mul_of_nonneg_left h_le hA - rwa [mul_left_comm] - have w_j₀_eq : w.1 j₀ = d * v0 j₀ := by - have : d = w.1 j₀ / v0 j₀ := hd_eq - have v0_ne : v0 j₀ ≠ 0 := ne_of_gt v0_j₀_pos - simp_all only [gt_iff_lt, sum_def, ne_eq, Pi.smul_apply, smul_eq_mul, inv_pos, mul_nonneg_iff_of_pos_left, - mul_pos_iff_of_pos_left, implies_true, div_pos_iff_of_pos_left, mem_univ, ge_iff_le, mul_eq_zero, inv_eq_zero, - false_or, isUnit_iff_ne_zero, or_self, not_false_eq_true, IsUnit.div_mul_cancel, s, v0, c, d] - have h_r'_ge_r : r' ≥ r := by - have h_pos : 0 < d * v0 j₀ := mul_pos hd_pos v0_j₀_pos - have h1 : d * r * v0 j₀ ≤ r' * w.1 j₀ := by - calc - d * r * v0 j₀ = d * (r * v0 j₀) := by ring - _ = d * (A *ᵥ v0) j₀ := by simp only [hv0_eig, Pi.smul_apply, - smul_eq_mul] - _ ≤ (A *ᵥ w.1) j₀ := h_sum2 - _ = r' * w.1 j₀ := by simp only [hw_eig, Pi.smul_apply, - smul_eq_mul] - have h2 : d * r * v0 j₀ ≤ r' * (d * v0 j₀) := by - rwa [w_j₀_eq] at h1 - have h2' : r * (d * v0 j₀) ≤ r' * (d * v0 j₀) := by - simpa [mul_comm, mul_left_comm, mul_assoc] using h2 - exact (le_of_mul_le_mul_right h2' h_pos) - have hr_eq : r = r' := le_antisymm h_r'_ge_r h_r_ge_r' - have hw_eig' : A *ᵥ w.1 = r • w.1 := by - simp only [hw_eig, hr_eq] - rcases - uniqueness_of_positive_eigenvector - hA_prim hr_pos v0 w.1 hv0_eig hw_eig' hv0_pos hw_pos - with ⟨c', hc'_pos, hc'_eq⟩ - have hc'_one : c' = 1 := by - have h_sum_w : ∑ i, w.1 i = 1 := w.property.2 - calc - c' = c' * 1 := by ring - _ = c' * (∑ i, w.1 i) := by rw [h_sum_w] - _ = (∑ i, c' * w.1 i) := by rw [Finset.mul_sum] - _ = (∑ i, v0 i) := by - simp only [hc'_eq, Pi.smul_apply, smul_eq_mul] - _ = 1 := h_sum_v0 - ext i - simp [hc'_eq, hc'_one] -open Quiver - -lemma Irreducible.exists_pos_entry - {n : Type*} [Fintype n] [DecidableEq n] [Nonempty n] {A : Matrix n n ℝ} - (hA_irred : A.IsIrreducible) : - ∃ i j : n, 0 < A i j := by - classical - letI : Quiver n := toQuiver A - let i₀ : n := Classical.arbitrary n - obtain ⟨p, hp_pos⟩ := hA_irred.connected i₀ i₀ - rcases Quiver.Path.path_decomposition_first_edge p hp_pos with - ⟨j, e, -, -, -⟩ - exact ⟨i₀, j, e.down⟩ - -/-- -**Perron–Frobenius theorem for irreducible real matrices (Existence, positivity, uniqueness)**. - -Let A : Matrix n n ℝ be an irreducible nonnegative matrix indexed by a finite nonempty type n. -Then there exists a unique eigenpair (v, r) where - • v : stdSimplex ℝ n is a probability vector (i.e. v.val has nonnegative entries summing to 1), - • r : ℝ is a positive scalar, -such that - A *ᵥ v.val = r • v.val and r > 0. -Moreover, this eigenvector v in the standard simplex is unique, and the corresponding eigenvalue r -is the Perron root of A. --/ -theorem pft_irreducible {n : Type*} [Fintype n] [Nonempty n] [DecidableEq n] - {A : Matrix n n ℝ} (hA_irred : A.IsIrreducible) : - ∃! (v : stdSimplex ℝ n), ∃ (r : ℝ), r > 0 ∧ A *ᵥ v.val = r • v.val := by - let B : Matrix n n ℝ := 1 + A - have hB_nonneg : ∀ i j, 0 ≤ B i j := by - intro i j; by_cases h : i = j - · subst h - have : (0 : ℝ) ≤ 1 + A i i := by - have := hA_irred.1 i i; linarith - simpa [B] using this - · simpa [B, h] using hA_irred.1 i j - have hB_diag_pos : ∀ i, 0 < B i i := by - intro i - have : (0 : ℝ) < 1 + A i i := by - have := hA_irred.1 i i; linarith - simpa [B] using this - have hB_irred : B.IsIrreducible := by - simpa [B] using (Irreducible.add_one (A := A) hA_irred) - have hB_prim : B.IsPrimitive := - IsPrimitive.of_irreducible_pos_diagonal B hB_nonneg hB_irred hB_diag_pos - obtain ⟨v, hv_unique⟩ := pft_primitive hB_prim hB_nonneg - obtain ⟨rB, hrB_pos, h_eig_B⟩ := hv_unique.1 - let r : ℝ := rB - 1 - have h_eig_A : A *ᵥ v.val = r • v.val := by - have h_B_eig_expanded : (1 + A) *ᵥ v.val = rB • v.val := by simpa [B] using h_eig_B - have h_A_eig_expanded : v.val + A *ᵥ v.val = rB • v.val := by simpa [add_mulVec, one_mulVec] using h_B_eig_expanded - have : A *ᵥ v.val = rB • v.val - v.val := eq_sub_of_add_eq' h_A_eig_expanded - simpa [r, sub_smul, one_smul] using this - let v_pos := - eigenvector_of_primitive_is_positive - hB_prim hrB_pos h_eig_B v.2.1 (ne_zero_of_mem_stdSimplex v.2) - rcases (Irreducible.exists_pos_entry (A := A) hA_irred) with - ⟨i₀, j₀, hA_pos⟩ - have hr_pos : 0 < r := by - have hAv_pos : 0 < (A *ᵥ v.val) i₀ := by - have h_nonneg : ∀ k ∈ Finset.univ, 0 ≤ A i₀ k * v.val k := - fun k _ ↦ mul_nonneg (hA_irred.1 _ _) (le_of_lt (v_pos k)) - have h_term : 0 < A i₀ j₀ * v.val j₀ := - mul_pos hA_pos (v_pos _) - have : 0 < ∑ k, A i₀ k * v.val k := - sum_pos_of_mem h_nonneg j₀ (Finset.mem_univ _) h_term - simpa [mulVec_apply] using this - have : 0 < r * v.val i₀ := by - simpa [Pi.smul_apply, smul_eq_mul, h_eig_A] using hAv_pos - exact (mul_pos_iff_of_pos_right (v_pos _)).1 this - refine ⟨v, ⟨r, hr_pos, h_eig_A⟩, ?_⟩ - · intro v' ⟨r', hr'_pos, h_eig_A'⟩ - have v'_pos := - eigenvector_is_positive_of_irreducible - hA_irred h_eig_A' v'.2.1 (ne_zero_of_mem_stdSimplex v'.2) - have v_pos' := - eigenvector_is_positive_of_irreducible - hA_irred h_eig_A v.2.1 (ne_zero_of_mem_stdSimplex v.2) - have hr_eq : r = r' := by - have h_eig_B' : B *ᵥ v'.val = (r' + 1) • v'.val := by - simp [B, add_mulVec, one_mulVec, add_smul, one_smul, h_eig_A', add_comm] - have h_unique_vec_B := hv_unique.2 - have h_v_eq_v' : v' = v := h_unique_vec_B v' ⟨r' + 1, by linarith, h_eig_B'⟩ - have h_smul_eq : rB • v.val = (r' + 1) • v.val := by - rw [← h_eig_B, ← h_v_eq_v', h_eig_B'] - have h_rB_eq : rB = r' + 1 := by - have h_v_ne_zero : v.val ≠ 0 := ne_zero_of_mem_stdSimplex v.property - obtain ⟨i, hi_ne_zero⟩ := Function.exists_ne_zero_of_ne_zero h_v_ne_zero - have : (rB • v.val) i = ((r' + 1) • v.val) i := by rw [h_smul_eq] - rw [Pi.smul_apply, Pi.smul_apply, smul_eq_mul, smul_eq_mul] at this - exact (mul_left_inj' hi_ne_zero).mp this - calc - r = rB - 1 := by rfl - _ = (r' + 1) - 1 := by rw [h_rB_eq] - _ = r' := by ring - have h_eig_A'_r : A *ᵥ v'.val = r • v'.val := by - subst hr_eq - simp_all only [add_apply, one_apply_eq, gt_iff_lt, exists_prop, forall_exists_index, and_imp, Subtype.forall, - sub_pos, implies_true, B, r] - obtain ⟨c, hc_pos, hcv⟩ := - uniqueness_of_positive_eigenvector_gen - hA_irred hr_pos v_pos' v'_pos h_eig_A h_eig_A'_r - have hc_one : c = 1 := by - have h_sum_v' : (∑ i, v'.val i) = 1 := v'.property.2 - calc c - _ = c * 1 := (mul_one c).symm - _ = c * (∑ i, v'.val i) := by rw [h_sum_v'] - _ = ∑ i, c * v'.val i := by rw [Finset.mul_sum] - _ = ∑ i, v.val i := by simp [hcv, smul_eq_mul] - _ = 1 := v.property.2 - exact Subtype.val_injective (by simp [hcv, hc_one, one_smul]) diff --git a/lean/MCMC/PF/LinearAlgebra/Matrix/PerronFrobenius/Lemmas.lean b/lean/MCMC/PF/LinearAlgebra/Matrix/PerronFrobenius/Lemmas.lean deleted file mode 100644 index 641a6b0..0000000 --- a/lean/MCMC/PF/LinearAlgebra/Matrix/PerronFrobenius/Lemmas.lean +++ /dev/null @@ -1,447 +0,0 @@ -import Mathlib.LinearAlgebra.Matrix.Irreducible.Defs -import MCMC.PF.Combinatorics.Quiver.Path ---import Mathlib - -namespace Matrix -section PerronFrobenius -open Matrix Finset Quiver Quiver.Path -variable {n : Type*} - ---open Quiver.Path - -/-- A path in the submatrix `A.submatrix Subtype.val Subtype.val` lifts to a path in the -original quiver `toQuiver A`, and all vertices along that lifted path lie in `S`. -/ -theorem path_in_submatrix_to_original [DecidableEq n] {A : Matrix n n ℝ} - (S : Set n) [DecidablePred S] - {i j : S} - (p : @Quiver.Path S (letI := Matrix.toQuiver A; inducedQuiver S) i j) : - letI : Quiver n := Matrix.toQuiver A - letI : Quiver S := inducedQuiver S - ∃ p' : @Path n (Matrix.toQuiver A) i.val j.val, - ∀ k, k ∈ p'.activeVertices → k ∈ S := by - letI : Quiver n := Matrix.toQuiver A - letI : Quiver S := inducedQuiver S - let p' := (Subquiver.embedding S).mapPath p - exact ⟨p', Subquiver.mapPath_embedding_vertices_in_set S p⟩ - -/-- A path exists between vertices in `S` using only vertices in `S` when the submatrix is irreducible -/ -theorem path_exists_in_support_of_irreducible [DecidableEq n] {A : Matrix n n ℝ} - (S : Set n) [DecidablePred S] - (hS : IsIrreducible (A.submatrix (Subtype.val : S → n) (Subtype.val : S → n))) - (i j : n) (hi : i ∈ S) (hj : j ∈ S) : - letI : Quiver n := Matrix.toQuiver A - letI : Quiver S := inducedQuiver S - ∃ p : Quiver.Path i j, ∀ k, k ∈ p.activeVertices → k ∈ S := by - letI : Quiver n := Matrix.toQuiver A - letI : Quiver S := inducedQuiver S - let i' : S := ⟨i, hi⟩ - let j' : S := ⟨j, hj⟩ - have h_submatrix := hS.connected - obtain ⟨p_sub, _hp_sub_pos⟩ := h_submatrix i' j' - -- Convert the path in `toQuiver (A.submatrix ...)` to a path in the induced quiver on `S`. - have p_sub' : @Quiver.Path S (letI := Matrix.toQuiver A; inducedQuiver S) i' j' := by - -- Both quivers have the same arrows: `0 < A i.val j.val`. - have conv : ∀ {a : S}, - @Quiver.Path S - (Matrix.toQuiver (A.submatrix (Subtype.val : S → n) (Subtype.val : S → n))) i' a → - @Quiver.Path S (letI := Matrix.toQuiver A; inducedQuiver S) i' a := by - intro a p - induction p with - | nil => - exact Quiver.Path.nil - | @cons b c p e ih => - refine Quiver.Path.cons ih ?_ - -- `e : 0 < (A.submatrix Subtype.val Subtype.val) _ _`, rewrite as `0 < A _ _`. - simpa [Matrix.toQuiver, Matrix.submatrix_apply] using e - exact conv p_sub - obtain ⟨p, hp⟩ := path_in_submatrix_to_original S p_sub' - exact ⟨p, hp⟩ - -lemma positive_mul_vec_pos [Fintype n] - {A : Matrix n n ℝ} (hA_pos : ∀ i j, 0 < A i j) - {x : n → ℝ} (hx_nonneg : ∀ i, 0 ≤ x i) (hx_ne_zero : x ≠ 0) : - ∀ i, 0 < (A.mulVec x) i := by - intro i - -- `A.mulVec x i = ∑ j, A i j * x j` - simp only [Matrix.mulVec, dotProduct] - apply Finset.sum_pos' - · intro j _ - exact mul_nonneg (le_of_lt (hA_pos i j)) (hx_nonneg j) - · have : ∃ k, 0 < x k := by - by_contra h_all_nonpos - push_neg at h_all_nonpos - have h_zero : x = 0 := funext (fun j => le_antisymm (h_all_nonpos j) (hx_nonneg j)) - exact hx_ne_zero h_zero - rcases this with ⟨k, hk_pos⟩ - refine ⟨k, ?_, ?_⟩ - · simp only [Finset.mem_univ] -- `k ∈ Finset.univ` - · exact mul_pos (hA_pos i k) hk_pos - -variable {A : Matrix n n ℝ} --[DecidableEq n] [Nonempty n] - -theorem positive_mul_vec_of_nonneg_vec [Fintype n] (hA_pos : ∀ i j, 0 < A i j) - {v : n → ℝ} (hv_nonneg : ∀ i, 0 ≤ v i) (hv_ne_zero : v ≠ 0) : - ∀ i, 0 < (A *ᵥ v) i := by - intro i - simp only [mulVec, dotProduct] - apply Finset.sum_pos' - · intro j _ - exact mul_nonneg (le_of_lt (hA_pos i j)) (hv_nonneg j) - · have : ∃ k, 0 < v k := by - by_contra h_all_nonpos - push_neg at h_all_nonpos - have h_zero : v = 0 := funext (fun j => le_antisymm (h_all_nonpos j) (hv_nonneg j)) - exact hv_ne_zero h_zero - rcases this with ⟨k, hk_pos⟩ - refine ⟨k, Finset.mem_univ k, ?_⟩ - exact mul_pos (hA_pos i k) hk_pos - -lemma path_exists_of_pos_entry {A : Matrix n n ℝ} {i j : n} (h_pos : 0 < A i j) : - letI : Quiver n := toQuiver A - ∃ p : Quiver.Path i j, 0 < p.length := by - letI : Quiver n := toQuiver A - refine ⟨(show Quiver.Path i j from (show i ⟶ j from ⟨h_pos⟩).toPath), ?_⟩ - simp - -lemma irreducible_of_all_entries_positive {A : Matrix n n ℝ} (hA : ∀ i j, 0 < A i j) : - IsIrreducible A := by - letI G := toQuiver A - constructor - · intros i j - exact (hA i j).le - · intros i j - exact path_exists_of_pos_entry (hA i j) - -theorem exists_connecting_edge_of_irreducible {A : Matrix n n ℝ} (hA_irred : A.IsIrreducible) - {v : n → ℝ} (hv_nonneg : ∀ i, 0 ≤ v i) - (S T : Set n) (hS_nonempty : S.Nonempty) (hT_nonempty : T.Nonempty) - (h_partition : ∀ i, i ∈ S ↔ v i > 0) - (h_complement : ∀ i, i ∈ T ↔ v i = 0) : - ∃ (i j : n), i ∈ T ∧ j ∈ S ∧ 0 < A i j := by - obtain ⟨i₀, hi₀_in_T⟩ := hT_nonempty - obtain ⟨j₀, hj₀_in_S⟩ := hS_nonempty - letI : Quiver n := toQuiver A - obtain ⟨p, _hp_pos⟩ := hA_irred.connected j₀ i₀ - obtain ⟨y, z, e, _, _, hy_not_T, hz_in_T, _⟩ := - @Quiver.Path.exists_boundary_edge n (toQuiver A) _ _ p T - (fun h_j0_in_T => by - have hj₀_pos : v j₀ > 0 := (h_partition j₀).mp hj₀_in_S - have hj₀_zero : v j₀ = 0 := (h_complement j₀).mp h_j0_in_T - exact ne_of_gt hj₀_pos hj₀_zero) - hi₀_in_T - have hy_in_S : y ∈ S := by - by_contra hy_not_S - have hy_in_T : y ∈ T := by - cases' (lt_or_eq_of_le (hv_nonneg y)) with h_pos h_zero - · simp_all only [gt_iff_lt, not_true_eq_false] - · simp_all only [gt_iff_lt, not_true_eq_false] - exact hy_not_T hy_in_T - obtain ⟨p', _hp'_pos⟩ := hA_irred.connected i₀ j₀ - obtain ⟨y, z, e, _, _, hy_not_S, hz_in_S, _⟩ := - @Quiver.Path.exists_boundary_edge n (toQuiver A) _ _ p' S - (fun h_i0_in_S => by - have hi₀_zero : v i₀ = 0 := (h_complement i₀).mp hi₀_in_T - have hi₀_pos : v i₀ > 0 := (h_partition i₀).mp h_i0_in_S - exact ne_of_gt hi₀_pos hi₀_zero) - hj₀_in_S - have hy_in_T : y ∈ T := by - by_contra hy_not_T - have hy_in_S : y ∈ S := by - cases' (lt_or_eq_of_le (hv_nonneg y)) with h_pos h_zero - · exact (h_partition y).mpr h_pos - · have hy_in_T' : y ∈ T := by simp_all only [gt_iff_lt, lt_self_iff_false, not_false_eq_true, - not_true_eq_false] - exact (hy_not_T hy_in_T').elim - exact hy_not_S hy_in_S - exact ⟨y, z, hy_in_T, hz_in_S, e.down⟩ - -lemma exists_boundary_crossing_in_support [DecidableEq n] [Fintype n] - (hA_irred : IsIrreducible A) (_ : ∀ i j, 0 ≤ A i j) - {v : n → ℝ} (hv_nonneg : ∀ i, 0 ≤ v i) (_ : v ≠ 0) - (S T : Set n) (hS_nonempty : S.Nonempty) (hT_nonempty : T.Nonempty) - (h_partition : ∀ i, i ∈ S ↔ v i > 0) - (h_complement : ∀ i, i ∈ T ↔ v i = 0) : - ∃ (i j : n), i ∈ T ∧ j ∈ S ∧ 0 < A i j := by - obtain ⟨i₀, hi₀_in_T⟩ := hT_nonempty - obtain ⟨j₀, hj₀_in_S⟩ := hS_nonempty - letI : Quiver n := toQuiver A - obtain ⟨p, _hp_pos⟩ := hA_irred.connected i₀ j₀ - obtain ⟨y, z, e, _, _, hy_not_S, hz_in_S, _⟩ := - @Quiver.Path.exists_boundary_edge n (toQuiver A) _ _ p S - (fun h_i0_in_S => by - have hi₀_zero : v i₀ = 0 := (h_complement i₀).mp hi₀_in_T - have hi₀_pos : v i₀ > 0 := (h_partition i₀).mp h_i0_in_S - exact ne_of_gt hi₀_pos hi₀_zero) - hj₀_in_S - have hy_in_T : y ∈ T := by - by_contra hy_not_T - have hy_in_S : y ∈ S := by - cases' (lt_or_eq_of_le (hv_nonneg y)) with h_pos h_zero - · exact (h_partition y).mpr h_pos - · have hy_in_T' : y ∈ T := by simp_all only [gt_iff_lt, ne_eq, lt_self_iff_false, - not_false_eq_true, not_true_eq_false] - exact (hy_not_T hy_in_T').elim - exact hy_not_S hy_in_S - exact ⟨y, z, hy_in_T, hz_in_S, e.down⟩ - -theorem irreducible_mulVec_ne_zero [DecidableEq n] [Fintype n] - (hA_irred : IsIrreducible A) (hA_nonneg : ∀ i j, 0 ≤ A i j) (hA_ne_zero : A ≠ 0) - {v : n → ℝ} (hv_nonneg : ∀ i, 0 ≤ v i) (hv_ne_zero : v ≠ 0) : - A *ᵥ v ≠ 0 := by - by_contra h_Av_zero - let S : Set n := {i | v i > 0} - let T : Set n := {i | v i = 0} - have hS_nonempty : S.Nonempty := by - by_contra hS_empty - rw [Set.not_nonempty_iff_eq_empty] at hS_empty - apply hv_ne_zero - ext k - have : v k ≤ 0 := by - by_contra hv_pos - have : k ∈ S := not_le.mp hv_pos - rw [hS_empty] at this - exact Set.notMem_empty k this - exact le_antisymm this (hv_nonneg k) - by_cases hT_is_empty : T = ∅ - · have v_all_pos : ∀ i, v i > 0 := by - intro i - have hi_not_in_T : i ∉ T := by simp [hT_is_empty] - have hi_ne_zero : v i ≠ 0 := by simpa [T] using hi_not_in_T - exact lt_of_le_of_ne (hv_nonneg i) (id (Ne.symm hi_ne_zero)) - have A_is_zero : A = 0 := by - ext k j - have : (A *ᵥ v) k = 0 := congrFun h_Av_zero k - rw [mulVec, dotProduct] at this - have terms_nonneg : ∀ idx, 0 ≤ A k idx * v idx := - fun idx => mul_nonneg (hA_nonneg k idx) (le_of_lt (v_all_pos idx)) - have term_kj_is_zero := (Finset.sum_eq_zero_iff_of_nonneg (fun i _ => terms_nonneg i)).mp this j (Finset.mem_univ _) - exact (mul_eq_zero.mp term_kj_is_zero).resolve_right (v_all_pos j).ne' - exact hA_ne_zero A_is_zero - · have hT_nonempty : T.Nonempty := Set.nonempty_iff_ne_empty.mpr hT_is_empty - obtain ⟨i, j, hi_T, hj_S, hA_ij_pos⟩ := exists_boundary_crossing_in_support - hA_irred hA_nonneg hv_nonneg hv_ne_zero S T hS_nonempty hT_nonempty - (fun i => by simp [S]) (fun i => by simp [T]) - have hA_ij_zero : A i j = 0 := by - have : (A *ᵥ v) i = 0 := congrFun h_Av_zero i - rw [mulVec, dotProduct] at this - have terms_nonneg : ∀ k ∈ Finset.univ, 0 ≤ A i k * v k := - fun k _ => mul_nonneg (hA_nonneg i k) (hv_nonneg k) - have term_j_is_zero := (Finset.sum_eq_zero_iff_of_nonneg terms_nonneg).mp this j (Finset.mem_univ _) - have hv_j_pos : v j > 0 := by simp [S] at hj_S; exact hj_S - exact (mul_eq_zero.mp term_j_is_zero).resolve_right (ne_of_gt hv_j_pos) - exact (ne_of_gt hA_ij_pos) hA_ij_zero - -variable --{n : Type*} [Fintype n] [DecidableEq n] - {A : Matrix n n ℝ} {r : ℝ} - -/-- A zero matrix is not irreducible if the dimension is greater than 1. -/ -lemma not_irreducible_of_zero_matrix {n : Type*} [Fintype n] [Nonempty n] - (h_card_gt_one : 1 < Fintype.card n) : ¬ IsIrreducible (0 : Matrix n n ℝ) := by - intro h - obtain ⟨i, j, hij⟩ := Fintype.exists_pair_of_one_lt_card h_card_gt_one - -- Irreducibility gives a positive-length path in `toQuiver 0`. - obtain ⟨p, hp_pos⟩ := h.connected i j - -- But `toQuiver 0` has no arrows: `i ⟶ j` means `0 < 0`. - cases p with - | nil => - simp at hp_pos - | cons p' e => - exact (lt_irrefl (0 : ℝ)) e.down - -/-- If an irreducible matrix `A` has a row `i` where `A*v` is zero, then all entries `A i k` must be zero - for `k` in the support of `v`. -/ -lemma zero_block_of_mulVec_eq_zero_row [Fintype n] (hA_nonneg : ∀ i j, 0 ≤ A i j) {v : n → ℝ} (hv_nonneg : ∀ i, 0 ≤ v i) - (S : Set n) (hS_def: S = {i | 0 < v i}) (i : n) (h_Av_i_zero : (A *ᵥ v) i = 0) : - ∀ k ∈ S, A i k = 0 := by - intro k hk_S_mem - rw [mulVec, dotProduct] at h_Av_i_zero - have h_sum_terms_nonneg : ∀ l, 0 ≤ A i l * v l := - fun l ↦ mul_nonneg (hA_nonneg i l) (hv_nonneg l) - have h_Aik_vk_zero : A i k * v k = 0 := - (sum_eq_zero_iff_of_nonneg (fun l _ ↦ h_sum_terms_nonneg l)).mp h_Av_i_zero k (mem_univ k) - have vk_pos : 0 < v k := by rwa [hS_def] at hk_S_mem - exact (mul_eq_zero.mp h_Aik_vk_zero).resolve_right (ne_of_gt vk_pos) - -/-- For an irreducible matrix on a one-element type, the (unique) diagonal entry is positive. -/ -lemma irreducible_one_element_implies_diagonal_pos [Fintype n] - {A : Matrix n n ℝ} (hA_irred : IsIrreducible A) - (h_card_one : Fintype.card n = 1) (i : n) : - 0 < A i i := by - letI G := toQuiver A - obtain ⟨p, hp_pos⟩ := hA_irred.connected i i - obtain ⟨j, p', e, rfl⟩ := Quiver.Path.path_decomposition_last_edge p hp_pos - have h_sub : Subsingleton n := by - rcases (Fintype.card_eq_one_iff).1 h_card_one with ⟨a, ha⟩ - exact ⟨fun x y => by simp [ha x, ha y]⟩ - haveI : Subsingleton n := h_sub - have hji : j = i := Subsingleton.elim _ _ - have e_pos : 0 < A j i := e.down - simpa [hji] using e_pos - -/-- An irreducible matrix with a positive diagonal is primitive. -/ -theorem IsPrimitive.of_irreducible_pos_diagonal [Fintype n][Nonempty n] [DecidableEq n] (A : Matrix n n ℝ) (hA_nonneg : ∀ i j, 0 ≤ A i j) - (hA_irred : IsIrreducible A) (hA_diag_pos : ∀ i, 0 < A i i) : - IsPrimitive A := by - let N := Fintype.card n - have h_card_pos : 0 < N := Fintype.card_pos - let k := (N - 1) * N + 1 - have hk_pos : 0 < k := by - rcases Nat.eq_or_lt_of_le (Nat.one_le_of_lt h_card_pos) with hN | hN_lt - · simp_all only [le_refl, tsub_self, List.Nat.eq_of_le_zero, zero_mul, zero_add, N, k] - · omega - constructor - · exact hA_nonneg - · use k, hk_pos - intro i j - letI : Quiver n := toQuiver A - -- Convert the goal to existence of a length-`k` path in `toQuiver A`. - rw [Matrix.pow_apply_pos_iff_nonempty_path (A := A) hA_nonneg k i j] - obtain ⟨p_ij, hp_len_le⟩ : ∃ p : Path i j, p.length ≤ N - 1 := by - obtain ⟨p_any, _hp_any_pos⟩ := hA_irred.connected i j - let S := { len | ∃ (p : Path i j), p.length = len } - have hS_nonempty : S.Nonempty := ⟨p_any.length, ⟨p_any, rfl⟩⟩ - classical - let min_len := Nat.find hS_nonempty - obtain ⟨p_shortest, h_shortest_len⟩ := Nat.find_spec hS_nonempty - have h_shortest : ∀ (q : Path i j), p_shortest.length ≤ q.length := by - intro q - rw [h_shortest_len] - exact Nat.find_min' hS_nonempty ⟨q, rfl⟩ - have h_simple : p_shortest.IsStrictlySimple := isStrictlySimple_of_shortest p_shortest h_shortest - use p_shortest - have h_len : p_shortest.length ≤ N - 1 := by - have h := @Quiver.Path.length_le_card_minus_one_of_isSimple n _ _ _ i j p_shortest h_simple - simpa [N] using h - exact h_len - let e_loop : i ⟶ i := ⟨hA_diag_pos i⟩ - let p_loop : Path i i := e_loop.toPath - have p_loop_len : p_loop.length = 1 := by simp_all only [le_refl, lt_add_iff_pos_left, List.Nat.eq_of_le_zero, - length_toPath, N, k, p_loop] - let num_loops := k - p_ij.length - have h_num_loops_nonneg : p_ij.length ≤ k := by - dsimp [k] - have h_card_ge_one : 1 ≤ N := Nat.one_le_of_lt h_card_pos - have h_len_le : ↑p_ij.length ≤ ↑(N - 1) := Nat.cast_le.mpr hp_len_le - have h_k_ge : ↑((N - 1) * N + 1) ≥ ↑(N - 1) * 1 + 1 := by - gcongr - linarith - let p_final := (Path.replicate num_loops p_loop).comp p_ij - refine ⟨⟨p_final, ?_⟩⟩ - rw [Path.length_comp, Path.length_replicate, p_loop_len, mul_one, - Nat.sub_add_cancel h_num_loops_nonneg] - -/-- If a path between two points in a set `S` must leave `S`, irreducibility guarantees -a path from the exit point back to an entry point. -/ -private lemma exists_path_back_to_set - (hA_irred : A.IsIrreducible) (S : Set n) - {u v : n} (hu : u ∈ S) (hv : v ∉ S) : - letI : Quiver n := A.toQuiver - ∃ (i j : n) (p : Path i j), - i ∈ S ∧ j ∉ S ∧ (∀ k, k ∈ p.vertices.tail → k ∉ S) := by - letI : Quiver n := A.toQuiver - letI : DecidablePred (· ∈ S) := Classical.decPred _ - obtain ⟨p, _hp_pos⟩ := hA_irred.connected u v - obtain ⟨i, j, e, _p₁, _p₂, hi, hj, _hp⟩ := - Quiver.Path.exists_boundary_edge_from_set p S hu hv - refine ⟨i, j, e.toPath, hi, hj, ?_⟩ - intro k hk - -- `e.toPath.vertices.tail = [j]`. - have hk_mem : k ∈ ([j] : List n) := by - simpa [Quiver.Path.vertices_toPath_tail] using hk - have hk_eq : k = j := by - simpa [List.mem_singleton] using hk_mem - subst hk_eq - exact hj - -/-- If `A` is irreducible, any two vertices of the same strongly–connected -component `S` can be joined by a path **staying inside** `S`. -/ -lemma path_exists_in_component {A : Matrix n n ℝ} - (S : Set n) [DecidablePred (· ∈ S)] - (hS_strong_conn : - letI : Quiver n := A.toQuiver; - letI : Quiver S := inducedQuiver S; - Quiver.IsSStronglyConnected S) - (i j : n) (hi : i ∈ S) (hj : j ∈ S) : - letI : Quiver n := A.toQuiver - ∃ p : Path i j, ∀ k, k ∈ p.vertices → k ∈ S := by - letI : Quiver n := A.toQuiver - letI G_S : Quiver S := inducedQuiver S - let i' : S := ⟨i, hi⟩ - let j' : S := ⟨j, hj⟩ - obtain ⟨p_sub, _hp_pos⟩ := by - letI : Quiver n := A.toQuiver - letI : Quiver S := inducedQuiver S - exact hS_strong_conn i' j' - let p := Prefunctor.mapPath (Quiver.Subquiver.embedding S) p_sub - refine ⟨p, ?_⟩ - intro k hk - have hka : k ∈ p.activeVertices := - mem_vertices_to_active hk - exact (Quiver.Subquiver.mapPath_embedding_vertices_in_set S p_sub _ hka) - -lemma Irreducible.exists_edge_out {A : Matrix n n ℝ} - (hA_irred : A.IsIrreducible) - (S : Set n) (hS_ne_empty : S.Nonempty) (hS_ne_univ : S ≠ Set.univ) : - ∃ (i : n) (_ : i ∈ S) (j : n) (_ : j ∉ S), 0 < A i j := by - letI G := toQuiver A - obtain ⟨i, hi⟩ := hS_ne_empty - obtain ⟨j, hj_compl⟩ := Set.nonempty_compl.mpr hS_ne_univ - obtain ⟨p, _hp_pos⟩ := hA_irred.connected i j - have hj : j ∉ S := by simpa using hj_compl - obtain ⟨u, v, e, _p₁, _p₂, hu_in_S, hv_not_in_S, _hp⟩ := - Quiver.Path.exists_boundary_edge_from_set p S hi hj - exact ⟨u, hu_in_S, v, hv_not_in_S, e.down⟩ - --- Lemma: Simple paths have bounded length by vertex count -lemma length_bounded_by_support_size [Quiver n] [DecidableEq n] [Fintype n] {_ : Matrix n n ℝ} - {support : Set n} [DecidablePred (· ∈ support)] (_ : Set.Finite support) - {i j : n} (p : Path i j) - (hp_support : ∀ k, k ∈ p.vertices → k ∈ support) (hp_simple : IsStrictlySimple p) : - p.length < support.toFinite.toFinset.card := by - have h_subset : p.vertexFinset ⊆ support.toFinite.toFinset := by - intro v hv - simp only [Set.Finite.mem_toFinset] - exact hp_support v (List.mem_toFinset.mp hv) - have h_card := card_vertexFinset_of_isStrictlySimple hp_simple - have h_card_le := Finset.card_le_card h_subset - rw [h_card] at h_card_le - exact h_card_le - - -lemma reachable_in_support_closed [DecidableEq n] - {A : Matrix n n ℝ} - (support : Set n) [DecidablePred (· ∈ support)] : - letI : Quiver n := Matrix.toQuiver A - let R := { k | ∃ (i : n) (_ : i ∈ support) (p : Path i k), - ∀ v, v ∈ p.vertices → v ∈ support } - R = support := by - letI : Quiver n := Matrix.toQuiver A - let R := { k | ∃ (i : n) (hi : i ∈ support) (p : Path i k), - ∀ v, v ∈ p.vertices → v ∈ support } - apply Set.Subset.antisymm - · intro k hkR - rcases hkR with ⟨i, hi, p, hp⟩ - have : k ∈ p.vertices := end_mem_vertices p - exact hp k this - · intro k hk_supp - refine ⟨k, hk_supp, (Path.nil : Path k k), ?_⟩ - intro v hv - simp [Quiver.Path.vertices_nil] at hv - subst hv; exact hk_supp - -/-! -If the principal sub-matrix supported on `support` is irreducible, -then any two vertices in `support` can be joined by a path that -stays *inside* `support`. --/ -lemma path_exists_in_support - (support : Set n) [DecidablePred (· ∈ support)] - (h_sub_irred : - (A.submatrix (Subtype.val : support → n) (Subtype.val : support → n)).IsIrreducible) - {i j : n} (hi : i ∈ support) (hj : j ∈ support) : - letI : Quiver n := Matrix.toQuiver A - ∃ p : Quiver.Path i j, ∀ k, k ∈ p.activeVertices → k ∈ support := by - classical - simpa using - Matrix.path_exists_in_support_of_irreducible - (A := A) (S := support) h_sub_irred i j hi hj diff --git a/lean/MCMC/PF/LinearAlgebra/Matrix/PerronFrobenius/Multiplicity.lean b/lean/MCMC/PF/LinearAlgebra/Matrix/PerronFrobenius/Multiplicity.lean deleted file mode 100644 index c9b75ee..0000000 --- a/lean/MCMC/PF/LinearAlgebra/Matrix/PerronFrobenius/Multiplicity.lean +++ /dev/null @@ -1,273 +0,0 @@ -import Mathlib.Order.CompletePartialOrder -import MCMC.PF.LinearAlgebra.Matrix.PerronFrobenius.Dominance - -namespace Matrix -open Finset CollatzWielandt LinearMap - -variable {n : Type*} [Fintype n] [Nonempty n] [DecidableEq n] -variable {A : Matrix n n ℝ} - -/-- -Helper lemma: If ker(f^2) = ker(f), then ker(f^k) = ker(f) for all k ≥ 1. -This shows that the ascent of the kernel stabilizes at 1. --/ -lemma LinearMap.ker_pow_eq_ker_of_ker_sq_eq_ker - {R M : Type*} [Semiring R] [AddCommMonoid M] [Module R M] - (f : M →ₗ[R] M) (h_stable : LinearMap.ker (f^2) = LinearMap.ker f) : - ∀ k ≥ 1, LinearMap.ker (f^k) = LinearMap.ker f := by - intro k hk - obtain ⟨m, rfl⟩ := Nat.exists_eq_succ_of_ne_zero (Nat.one_le_iff_ne_zero.mp hk) - induction' m with m ih - · simp [pow_one] - · apply le_antisymm - · intro x hx - have hx' : (f ^ (m + 1)) (f x) = 0 := by - simp_all only [Nat.succ_eq_add_one, ge_iff_le, le_add_iff_nonneg_left, le_refl, List.Nat.eq_of_le_zero, - zero_le, forall_const, LinearMap.mem_ker] - exact hx - have : f x ∈ LinearMap.ker (f ^ (m + 1)) := by - simpa [LinearMap.mem_ker] using hx' - have : f x ∈ LinearMap.ker f := by simpa [ih] using this - rw [← h_stable] - simpa [LinearMap.mem_ker] using this - · intro x hx - have : (f ^ (m + 1)) (f x) = 0 := by simp_all - simpa [pow_succ] using this -/-- -The geometric multiplicity of the Perron root of an irreducible non-negative matrix is 1. -The eigenspace is spanned by the unique positive eigenvector. --/ -lemma geometric_multiplicity_one_of_irreducible - (hA_irred : A.IsIrreducible) (hA_nonneg : ∀ i j, 0 ≤ A i j) : - let r := perronRoot_alt A - ∃ v : n → ℝ, (∀ i, 0 < v i) ∧ Module.End.eigenspace (toLin' A) r = Submodule.span ℝ {v} := by - let r := perronRoot_alt A - let f := toLin' A - obtain ⟨r_ex, v, hr_pos, hv_pos, hv_eig_mat, hr_eq_r⟩ := perron_root_eq_positive_eigenvalue hA_irred hA_nonneg - rw [← hr_eq_r] at hv_eig_mat hr_pos - have hv_eig_f : f v = r • v := by rwa [toLin'_apply] - use v, hv_pos - apply Submodule.ext - intro w - constructor - · intro hw_E_r - have hw_eig : f w = r • w := by - simpa [f, r] using (Module.End.mem_eigenspace_iff.mp (by assumption)) - by_cases hw_zero : w = 0 - · subst hw_zero - exact Submodule.zero_mem _ - · let w_abs := fun i => |w i| - have hw_abs_nonneg : ∀ i, 0 ≤ w_abs i := fun i => abs_nonneg _ - have hw_abs_ne_zero : w_abs ≠ 0 := by - contrapose! hw_zero - ext i - exact abs_eq_zero.mp (congr_fun hw_zero i) - have h_subinv : r • w_abs ≤ f w_abs := by - intro i - calc - (r • w_abs) i - = r * |w i| := by simp [w_abs] - _ = |r * w i| := by rw [abs_mul, abs_of_pos hr_pos] - _ = |(f w) i| := by rw [hw_eig]; simp - _ = |∑ j, A i j * w j| := by simp [f, toLin'_apply, mulVec_apply] - _ ≤ ∑ j, |A i j * w j| := by - simpa using - (Finset.abs_sum_le_sum_abs (s := (Finset.univ : Finset n)) - (f := fun j => A i j * w j)) - _ = ∑ j, A i j * |w j| := by - simp_rw [abs_mul, abs_of_nonneg (hA_nonneg i _)] - _ = (f w_abs) i := by simp [f, toLin'_apply, mulVec_apply, w_abs] - have hw_abs_eig : f w_abs = r • w_abs := - subinvariant_equality_implies_eigenvector hA_irred hA_nonneg hw_abs_nonneg hw_abs_ne_zero h_subinv - have hw_abs_pos : ∀ i, 0 < w_abs i := - eigenvector_is_positive_of_irreducible hA_irred hw_abs_eig hw_abs_nonneg hw_abs_ne_zero - obtain ⟨c_abs, hc_abs_pos, hc_abs_eq⟩ := - uniqueness_of_positive_eigenvector_gen hA_irred hr_pos hw_abs_pos hv_pos hw_abs_eig hv_eig_f - let B := 1 + A - have hB_nonneg : ∀ i j, 0 ≤ B i j := by - intro i j; by_cases h : i = j - · subst h; have := hA_nonneg i i; simp [B]; linarith - · simp [B, h, hA_nonneg i j] - have hB_irred := Matrix.Irreducible.add_one (A := A) hA_irred - have hB_diag_pos : ∀ i, 0 < B i i := by - intro i; have := hA_nonneg i i; simp [B]; linarith - have hB_prim : IsPrimitive B := IsPrimitive.of_irreducible_pos_diagonal B hB_nonneg hB_irred hB_diag_pos - let rB := r + 1 - have hrB_pos : 0 < rB := by linarith [hr_pos] - have hBw_eig : toLin' B w = rB • w := by - simp [B, LinearMap.add_apply, toLin'_one, add_smul, one_smul, rB]; abel_nf; aesop - have hw_abs_eig_B : toLin' B w_abs = rB • w_abs := by - simp [B, LinearMap.add_apply, toLin'_one, add_smul, one_smul, rB]; abel_nf; aesop - let wc : n → ℂ := fun i => (w i : ℂ) - have hwc_eig_B : (B.map (algebraMap ℝ ℂ)) *ᵥ wc = (rB : ℂ) • wc := by - ext i - have h_real : ∑ j, B i j * w j = rB * w i := by - have := congrArg (fun v : n → ℝ => v i) - (by simpa [toLin'_apply, Pi.smul_apply] using hBw_eig) - simpa [Matrix.mulVec, dotProduct] using this - calc - ((B.map (algebraMap ℝ ℂ)) *ᵥ wc) i - = ∑ j, (algebraMap ℝ ℂ (B i j)) * wc j := by - simp [Matrix.mulVec, dotProduct] - _ = ∑ j, (B i j : ℂ) * (w j : ℂ) := by - simp [wc] - _ = ∑ j, Complex.ofReal (B i j * w j) := by - simp [Complex.ofReal_mul] - _ = Complex.ofReal (∑ j, B i j * w j) := by - simp - _ = Complex.ofReal (rB * w i) := by - simp [h_real] - _ = (rB : ℂ) * wc i := by - simp [wc, Complex.ofReal_mul] - have hrB_eq_perronB : rB = perronRoot_alt B := - eigenvalue_is_perron_root_of_positive_eigenvector hB_irred hB_nonneg hrB_pos hw_abs_pos hw_abs_eig_B - have h_norm_eig : (B *ᵥ fun i => ‖wc i‖) = perronRoot_alt B • (fun i => ‖wc i‖) := by - have h_eq : (fun i => ‖wc i‖) = w_abs := by - ext i - simp [wc, w_abs] - simpa [toLin'_apply, hrB_eq_perronB, h_eq] using hw_abs_eig_B - have h_norm_pos : ∀ i, 0 < ‖wc i‖ := by - intro i - simpa [wc, w_abs, Complex.norm_ofReal] using hw_abs_pos i - obtain ⟨c, hc_norm, hc_eq⟩ := eigenvector_phase_aligned_of_primitive hB_prim hB_nonneg - (by simpa [hrB_eq_perronB] using perronRoot_nonneg hB_nonneg) - hwc_eig_B - h_norm_eig - h_norm_pos - let i := Classical.arbitrary n - have hc_eq_i := congr_fun hc_eq i - simp only [wc] at hc_eq_i - have hc_real : c.im = 0 := by - have h := congrArg Complex.im hc_eq_i - have h' : c.im * w_abs i = 0 := by - have : 0 = (c * (w_abs i : ℂ)).im := by - simpa [wc, w_abs, Complex.ofReal_mul, Complex.ofReal_im, Complex.ofReal_re] using h.symm - simpa [Complex.mul_im] using this - exact (mul_eq_zero.mp h').resolve_right (ne_of_gt (hw_abs_pos i)) - have hw_eq_c_wabs : w = c.re • w_abs := by - ext j - have h := congrArg Complex.re (congr_fun hc_eq j) - have : w j = c.re * ‖wc j‖ := by - simpa [wc, Complex.mul_re, Complex.ofReal_re, Complex.ofReal_im] using h - simpa [Pi.smul_apply, w_abs, wc, Complex.norm_ofReal, smul_eq_mul] using this - rw [hw_eq_c_wabs, hc_abs_eq, smul_smul] - exact Submodule.mem_span_singleton.mpr ⟨c.re * c_abs, rfl⟩ - · intro hw - rcases Submodule.mem_span_singleton.mp hw with ⟨c, rfl⟩ - have hc : f (c • v) = r • (c • v) := by - calc - f (c • v) = c • f v := by simp - _ = c • (r • v) := by simp [hv_eig_f] - _ = (c * r) • v := by simp [smul_smul] - _ = (r * c) • v := by simp [mul_comm] - _ = r • (c • v) := by simp [smul_smul] - have : (toLin' A) (c • v) = (perronRoot_alt A) • (c • v) := by - simpa [f, r] - using hc - exact (Module.End.mem_eigenspace_iff).2 this - -open scoped Matrix InnerProductSpace - -/-- -The algebraic multiplicity of the Perron root of an irreducible non-negative matrix is 1. -The generalized eigenspace equals the eigenspace. --/ -lemma algebraic_multiplicity_one_of_irreducible - (hA_irred : A.IsIrreducible) (hA_nonneg : ∀ i j, 0 ≤ A i j) : - Module.End.maxGenEigenspace (toLin' A) (perronRoot_alt A) = - Module.End.eigenspace (toLin' A) (perronRoot_alt A) := by - classical - let r := perronRoot_alt A - let f := toLin' A - let g := f - r • LinearMap.id - have h_ker_sq_eq_ker : LinearMap.ker (g ^ 2) = LinearMap.ker g := by - apply le_antisymm - · intro w hw_g2 - let u := g w - have hu_g : g u = 0 := by - simpa [u, pow_two, LinearMap.comp_apply] using hw_g2 - have hu_eig : f u = r • u := by - simp only [g, LinearMap.sub_apply, sub_eq_zero] at hu_g - exact hu_g - have hAT_irred := Matrix.IsIrreducible.transpose hA_irred - obtain ⟨rT, u_star, hrT_pos, hu_star_pos, hu_star_eig_T_mat⟩ := - exists_positive_eigenvector_of_irreducible hAT_irred - have hrT_eq_r : rT = r := by - calc - rT = perronRoot_alt Aᵀ := - eigenvalue_is_perron_root_of_positive_eigenvector - hAT_irred (fun i j => hA_nonneg j i) hrT_pos hu_star_pos hu_star_eig_T_mat - _ = r := (perronRoot_transpose_eq A hA_irred).symm - have hu_star_eig_r : (toLin' Aᵀ) u_star = r • u_star := by - simpa [hrT_eq_r, toLin'_apply] using congrArg id hu_star_eig_T_mat - have h_dot_g_w : u_star ⬝ᵥ (g w) = 0 := by - calc - u_star ⬝ᵥ (g w) - = u_star ⬝ᵥ (f w) - u_star ⬝ᵥ (r • w) := by - simp [g, dotProduct_sub] - _ = (toLin' Aᵀ u_star) ⬝ᵥ w - r * (u_star ⬝ᵥ w) := by - have h₁ : u_star ⬝ᵥ (f w) = (toLin' Aᵀ u_star) ⬝ᵥ w := by - simp [f, toLin'_apply]; exact dotProduct_mulVec_comm u_star w A - simp [h₁, dotProduct_smul, smul_eq_mul] - _ = (r • u_star) ⬝ᵥ w - r * (u_star ⬝ᵥ w) := by - simp [hu_star_eig_r] - _ = r * (u_star ⬝ᵥ w) - r * (u_star ⬝ᵥ w) := by - simp [smul_eq_mul] - _ = 0 := by ring - have h_dot_u : u_star ⬝ᵥ u = 0 := by simpa [u] using h_dot_g_w - obtain ⟨v, hv_pos, h_Er_span⟩ := - geometric_multiplicity_one_of_irreducible (A := A) hA_irred hA_nonneg - have hu_in_Er : u ∈ Module.End.eigenspace f r := by - simpa [Module.End.mem_eigenspace_iff, f, r] using hu_eig - have : u ∈ Submodule.span ℝ ({v} : Set (n → ℝ)) := by - simpa [h_Er_span, r, f] using hu_in_Er - obtain ⟨c, hc_eq⟩ := Submodule.mem_span_singleton.mp this - have h_dot_u' : c = 0 ∨ u_star ⬝ᵥ v = 0 := by - have hc_mul : c * (u_star ⬝ᵥ v) = 0 := by - have : u_star ⬝ᵥ (c • v) = 0 := by - simpa [hc_eq] using h_dot_u - simpa [dotProduct_smul, smul_eq_mul, mul_comm] using this - exact mul_eq_zero.mp hc_mul - have hv_ne_zero : v ≠ 0 := by - intro h - have : 0 < v (Classical.arbitrary n) := hv_pos _ - have : False := by simp [h] at this - exact this.elim - have h_dot_pos : 0 < u_star ⬝ᵥ v := - dotProduct_pos_of_pos_of_nonneg_ne_zero hu_star_pos - (fun i => (hv_pos i).le) hv_ne_zero - have hc_zero : c = 0 := h_dot_u'.resolve_right h_dot_pos.ne' - have hu_zero : u = 0 := by - simpa [hc_zero, zero_smul] using hc_eq.symm - have : g w = 0 := by simpa [u] using hu_zero - simpa [LinearMap.mem_ker] using this - · intro x hx - have hx0 : g x = 0 := by simpa [LinearMap.mem_ker] using hx - have : (g ^ 2) x = 0 := by - simp [pow_two, hx0] - simpa [LinearMap.mem_ker] using this - haveI : FiniteDimensional ℝ (n → ℝ) := by infer_instance - have h_stabilize := LinearMap.ker_pow_eq_ker_of_ker_sq_eq_ker g h_ker_sq_eq_ker - have h_sup_eq : ⨆ (k : ℕ), LinearMap.ker (g ^ k) = LinearMap.ker g := by - apply le_antisymm - · apply iSup_le - intro k - cases k with - | zero => - intro x hx - have hx0 : x = 0 := by - simpa [pow_zero, LinearMap.mem_ker] using hx - simp [hx0] - | succ k' => - have hk' : 1 ≤ k'.succ := Nat.succ_le_succ (Nat.zero_le _) - simp [h_stabilize k'.succ hk'] - · exact le_iSup_of_le 1 (by simp [pow_one]) - calc - Module.End.maxGenEigenspace f r - = ⨆ k, LinearMap.ker (g ^ k) := by - simp [Module.End.maxGenEigenspace, Module.End.genEigenspace, g]; rfl - _ = LinearMap.ker g := h_sup_eq - _ = Module.End.eigenspace f r := by - simp [Module.End.eigenspace, g]; rw [@Module.End.genEigenspace_one]; rfl - -end Matrix diff --git a/lean/MCMC/PF/LinearAlgebra/Matrix/PerronFrobenius/Primitive.lean b/lean/MCMC/PF/LinearAlgebra/Matrix/PerronFrobenius/Primitive.lean deleted file mode 100644 index 3864430..0000000 --- a/lean/MCMC/PF/LinearAlgebra/Matrix/PerronFrobenius/Primitive.lean +++ /dev/null @@ -1,308 +0,0 @@ -import MCMC.PF.LinearAlgebra.Matrix.PerronFrobenius.CollatzWielandt -import MCMC.PF.LinearAlgebra.Matrix.PerronFrobenius.Lemmas -import Mathlib.Tactic - -namespace Matrix -open Finset Quiver -variable {n : Type*} [Fintype n] -/-! -### The Perron-Frobenius Theorem for Primitive Matrices - -This section formalizes Theorem 1.1 from Seneta's "Non-negative Matrices and Markov Chains". -The proof follows Seneta's logic : -1. Define the Perron root `r` as the supremum of the Collatz-Wielandt function `r(x)`. -2. Use the fact that `r(x)` is upper-semicontinuous on a compact set (the standard simplex) - to guarantee the supremum is attained by a vector `v`. -3. Prove that `v` is an eigenvector by a contradiction argument using the primitivity of `A`. -4. Prove that `v` is strictly positive, again using primitivity. --/ -section PerronFrobenius -variable {n : Type*} [Fintype n] [Nonempty n] -variable {A : Matrix n n ℝ} - -open LinearMap Set Filter Topology Finset Matrix.CollatzWielandt -open scoped Convex Pointwise - -end PerronFrobenius - -end Matrix - -open Set Finset MetricSpace Topology Convex Quiver.Path - -namespace Matrix ---variable {n : Type*} --[Fintype n] - -open Topology Metric Set Finset -section PerronFrobenius -open Finset Set IsCompact Topology Matrix - -variable {n : Type*} [Fintype n] {A : Matrix n n ℝ} - -lemma ratio_le_max_row_sum_simple [Nonempty n] (A : Matrix n n ℝ) (hA_nonneg : ∀ i j, 0 ≤ A i j) - {x : n → ℝ} (_ : ∀ i, 0 ≤ x i) (i : n) (hx_i_pos : 0 < x i) : - (A *ᵥ x) i / x i ≤ (∑ j, A i j) * (Finset.univ.sup' (Finset.univ_nonempty) x) / x i := by - rw [mulVec_apply, div_le_div_iff_of_pos_right hx_i_pos] - calc - ∑ j, A i j * x j ≤ ∑ j, A i j * (Finset.univ.sup' Finset.univ_nonempty x) := by - apply Finset.sum_le_sum - intro j _ - exact mul_le_mul_of_nonneg_left (le_sup' x (Finset.mem_univ j)) (hA_nonneg i j) - _ = (∑ j, A i j) * Finset.univ.sup' Finset.univ_nonempty x := by rw [Finset.sum_mul] - -variable [Nonempty n] [DecidableEq n] {A : Matrix n n ℝ} - -/-- For an irreducible non-negative matrix, the Collatz-Wielandt value of the vector of all ones - is strictly positive. This relies on the fact that an irreducible matrix cannot have a zero row - (unless n=1, which is handled). A zero row would imply the sum of its entries is zero, which - is the Collatz-Wielandt value for the vector of all ones. -/ -lemma collatzWielandtFn_of_ones_is_pos - (hA_irred : IsIrreducible A) (hA_nonneg : ∀ i j, 0 ≤ A i j) : - 0 < collatzWielandtFn A (fun _ ↦ 1) := by - let x_ones : n → ℝ := fun _ ↦ 1 - have h_supp_nonempty : ({i | 0 < x_ones i}.toFinset).Nonempty := by - rw [Set.toFinset_nonempty_iff]; exact ⟨Classical.arbitrary n, by simp [x_ones]⟩ - dsimp [collatzWielandtFn] - rw [dif_pos h_supp_nonempty] - have h_supp_ones : {i | 0 < x_ones i}.toFinset = Finset.univ := by - ext a; simp [x_ones, zero_lt_one] - have h_inf_eq : ({i | 0 < x_ones i}.toFinset.inf' h_supp_nonempty fun i ↦ (A *ᵥ x_ones) i / x_ones i) = - (Finset.univ.inf' (by rwa [←h_supp_ones]) fun i ↦ (A *ᵥ x_ones) i / x_ones i) := by - congr - rw [h_inf_eq] - apply Finset.inf'_pos Finset.univ_nonempty - intro i _ - simp_rw [mulVec_apply, x_ones, mul_one, div_one] - apply sum_pos_of_nonneg_of_ne_zero - · intro j _; exact hA_nonneg i j - · by_contra h_sum_is_zero - have h_zero_row : ∀ j, A i j = 0 := by - intro j - have h_zero_row_finset : ∀ j ∈ Finset.univ, A i j = 0 := - (sum_eq_zero_iff_of_nonneg (fun j _ => hA_nonneg i j)).mp h_sum_is_zero - exact h_zero_row_finset j (Finset.mem_univ j) - rcases Nat.eq_one_or_one_lt (Fintype.card n) Fintype.card_ne_zero with h_card_one | h_card_gt_one - · have h_i_unique : ∀ j : n, j = i := by - intro j - apply Fintype.card_le_one_iff.mp - linarith [h_card_one] - have h_need_self_loop : 0 < A i i := by - exact irreducible_one_element_implies_diagonal_pos hA_irred h_card_one i - have h_Aii_zero : A i i = 0 := h_zero_row i - exact lt_irrefl 0 (h_Aii_zero ▸ h_need_self_loop) - · haveI : Nontrivial n := Fintype.one_lt_card_iff_nontrivial.1 h_card_gt_one - obtain ⟨j, hj_pos⟩ := Matrix.IsIrreducible.exists_pos (A := A) hA_irred i - have h_Aij_zero : A i j = 0 := h_zero_row j - exact lt_irrefl 0 (h_Aij_zero ▸ hj_pos) - -/-- The Perron root (the supremum of the Collatz-Wielandt function) is positive for an - irreducible, non-negative matrix. This follows by showing the value for the vector of - all ones is positive, and that value is a lower bound for the supremum. -/ -lemma collatzWielandt_sup_is_pos - (hA_irred : IsIrreducible A) (hA_nonneg : ∀ i j, 0 ≤ A i j) : - 0 < sSup (collatzWielandtFn A '' {x | (∀ i, 0 ≤ x i) ∧ x ≠ 0}) := by - let P_set := {x : n → ℝ | (∀ i, 0 ≤ x i) ∧ x ≠ 0} - let x_ones : n → ℝ := fun _ ↦ 1 - have h_x_ones_in_set : x_ones ∈ P_set := by - constructor - · intro i; exact zero_le_one - · intro h_zero - have h_contra : (1 : ℝ) = 0 := by simpa [x_ones] using congr_fun h_zero (Classical.arbitrary n) - exact one_ne_zero h_contra - have r_sup_ge_r_ones : collatzWielandtFn A x_ones ≤ sSup (collatzWielandtFn A '' P_set) := by - apply le_csSup_of_le - · exact CollatzWielandt.bddAbove A hA_nonneg - · exact Set.mem_image_of_mem A.collatzWielandtFn h_x_ones_in_set - · exact Preorder.le_refl (A.collatzWielandtFn x_ones) - have r_ones_pos : 0 < collatzWielandtFn A x_ones := - collatzWielandtFn_of_ones_is_pos hA_irred hA_nonneg - exact lt_of_lt_of_le r_ones_pos r_sup_ge_r_ones - -/-- For a maximizer `v` of the Collatz-Wielandt function, `A * v = r • v`. -/ -theorem maximizer_is_eigenvector (hA_prim : IsPrimitive A) - (hA_nonneg : ∀ i j, 0 ≤ A i j) {v : n → ℝ} (hv_max : IsMaxOn (collatzWielandtFn A) (stdSimplex ℝ n) v) - (hv_simplex : v ∈ stdSimplex ℝ n) (r : ℝ) (hr_def : r = collatzWielandtFn A v) : - A *ᵥ v = r • v := by - have hv_nonneg : ∀ i, 0 ≤ v i := hv_simplex.1 - have hv_ne_zero : v ≠ 0 := fun h => by simpa [h, stdSimplex] using hv_simplex.2 - have h_fund_ineq : r • v ≤ A *ᵥ v := by - rw [hr_def]; exact CollatzWielandt.le_mulVec hA_nonneg hv_nonneg hv_ne_zero - by_contra h_ne - let z := A *ᵥ v - r • v - have hz_nonneg : ∀ i, 0 ≤ z i := fun i ↦ by simp [z, sub_nonneg];exact h_fund_ineq i - have hz_ne_zero : z ≠ 0 := by intro hz_zero; apply h_ne; ext i; simpa [z, sub_eq_zero] using congr_fun hz_zero i - obtain ⟨_, k, hk_gt_zero, hk_pos⟩ := hA_prim - let y := (A ^ k) *ᵥ v - have hy_pos : ∀ i, 0 < y i := positive_mul_vec_of_nonneg_vec hk_pos hv_nonneg hv_ne_zero - have h_Ay_gt_ry : ∀ i, r * y i < (A *ᵥ y) i := by - intro i - let Az := (A ^ k) *ᵥ z - have h_pos_term : 0 < Az i := (positive_mul_vec_of_nonneg_vec hk_pos hz_nonneg hz_ne_zero) i - have h_calc : (A *ᵥ y) i = r * y i + Az i := by - simp only [y, z, Az, mulVec_sub, mulVec_smul, Pi.sub_apply, Pi.smul_apply, smul_eq_mul] - rw [add_comm, ← sub_eq_iff_eq_add] - rw [mulVec_mulVec, mulVec_mulVec, ← pow_succ', pow_succ] - rw [h_calc]; exact lt_add_of_pos_right (r * y i) h_pos_term - have r_lt_r_y : r < collatzWielandtFn A y := by - have h_y_supp_nonempty : ({i | 0 < y i}.toFinset).Nonempty := by - rw [Set.toFinset_nonempty_iff]; exact ⟨(Classical.arbitrary n), hy_pos _⟩ - rw [collatzWielandtFn, dif_pos h_y_supp_nonempty]; apply (Finset.lt_inf'_iff h_y_supp_nonempty).mpr - intro i _; - exact (lt_div_iff₀ (hy_pos i)).mpr (h_Ay_gt_ry i) - let y_norm_factor := (∑ i, y i)⁻¹ - let y_norm := y_norm_factor • y - have hy_norm_in_simplex : y_norm ∈ stdSimplex ℝ n := by - have : Nonempty n := by - subst hr_def - simp_all only [ne_eq, Pi.sub_apply, Pi.smul_apply, smul_eq_mul, sub_nonneg, mulVec_mulVec, y, z] - refine ⟨?_, ?_⟩ - · intro i - have h_sum_nonneg : 0 ≤ ∑ j, y j := sum_nonneg (fun j _ => (hy_pos j).le) - exact smul_nonneg (inv_nonneg.mpr h_sum_nonneg) (hy_pos i).le - · have h_sum_pos : 0 < ∑ i, y i := - Finset.sum_pos (fun i _ => hy_pos i) Finset.univ_nonempty - have h_sum_ne_zero : (∑ i, y i) ≠ 0 := ne_of_gt h_sum_pos - calc - ∑ x, (∑ j, y j)⁻¹ • y x - = ∑ x, (∑ j, y j)⁻¹ * y x := by simp [smul_eq_mul] - _ = (∑ j, y j)⁻¹ * ∑ x, y x := by simp [Finset.mul_sum] - _ = (∑ i, y i) * (∑ j, y j)⁻¹ := by rw [mul_comm] - _ = 1 := by field_simp [h_sum_ne_zero] - have r_ge_r_y_norm : collatzWielandtFn A y_norm ≤ r := by - rw [hr_def] - exact hv_max hy_norm_in_simplex - have r_y_norm_eq_r_y : collatzWielandtFn A y_norm = collatzWielandtFn A y := by - have sum_pos : 0 < ∑ i, y i := - Finset.sum_pos (fun _ _ => hy_pos _) Finset.univ_nonempty - have ne0 : (∑ i, y i) ≠ 0 := ne_of_gt sum_pos - have sup_eq : ({i | 0 < y_norm i}.toFinset : Finset n) = - ({i | 0 < y i}.toFinset : Finset n) := by - have h_set_eq : {i | 0 < y_norm i} = {i | 0 < y i} := by - ext i - have sum_pos : 0 < ∑ j, y j := Finset.sum_pos (fun j _ => hy_pos j) Finset.univ_nonempty - subst hr_def - simp_all only [ne_eq, Pi.sub_apply, Pi.smul_apply, smul_eq_mul, sub_nonneg, mulVec_mulVec, inv_pos, - mul_pos_iff_of_pos_left, setOf_true, Set.mem_univ, y, y_norm, y_norm_factor, z] - subst hr_def - simp_all only [ne_eq, Pi.smul_apply, smul_eq_mul, inv_pos, mul_pos_iff_of_pos_left, setOf_true, - Pi.sub_apply, sub_nonneg, mulVec_mulVec, toFinset_univ, y, y_norm_factor, y_norm, z] - have fun_eq : (fun i => (A *ᵥ y_norm) i / y_norm i) = - fun i => (A *ᵥ y) i / y i := by - funext i - calc - (A *ᵥ y_norm) i / y_norm i - = ((∑ j, y j)⁻¹ * (A *ᵥ y) i) / ((∑ j, y j)⁻¹ * y i) := by - simp [y_norm, y_norm_factor, mulVec_smul] - _ = (A *ᵥ y) i / y i := by field_simp [ne0] - dsimp [collatzWielandtFn, y_norm, y_norm_factor] - split_ifs with h₁ h₂ - · simp - subst hr_def - simp_all only [ne_eq, Pi.smul_apply, smul_eq_mul, inv_pos, mul_pos_iff_of_pos_left, setOf_true, - toFinset_univ, mulVec_mulVec, Pi.sub_apply, sub_nonneg, Finset.filter_true, - y, y_norm_factor, y_norm, z] - · subst hr_def - simp_all only [ne_eq, Pi.smul_apply, smul_eq_mul, inv_pos, mul_pos_iff_of_pos_left, setOf_true, - toFinset_univ, mulVec_mulVec, Finset.univ_nonempty, not_true_eq_false, y, y_norm_factor, y_norm] - · subst hr_def - simp_all only [ne_eq, Pi.smul_apply, smul_eq_mul, inv_pos, mul_pos_iff_of_pos_left, setOf_true, - toFinset_univ, mulVec_mulVec, Finset.univ_nonempty, not_true_eq_false, y, y_norm_factor, y_norm] - · rfl - linarith [r_ge_r_y_norm, r_y_norm_eq_r_y, r_lt_r_y] - -/-- Short alias for `maximizer_is_eigenvector`. -/ -theorem max_is_eig (hA_prim : IsPrimitive A) - (hA_nonneg : ∀ i j, 0 ≤ A i j) {v : n → ℝ} (hv_max : IsMaxOn (collatzWielandtFn A) (stdSimplex ℝ n) v) - (hv_simplex : v ∈ stdSimplex ℝ n) (r : ℝ) (hr_def : r = collatzWielandtFn A v) : - A *ᵥ v = r • v := by - exact maximizer_is_eigenvector hA_prim hA_nonneg hv_max hv_simplex r hr_def - -/-- An eigenvector `v` of a primitive matrix `A` corresponding to a positive eigenvalue `r` must be strictly positive. -/ -lemma eigenvector_of_primitive_is_positive {r : ℝ} (hA_prim : IsPrimitive A) (hr_pos : 0 < r) - {v : n → ℝ} (h_eigen : A *ᵥ v = r • v) (hv_nonneg : ∀ i, 0 ≤ v i) (hv_ne_zero : v ≠ 0) : - ∀ i, 0 < v i := by - obtain ⟨_, k, hk_gt_zero, hk_pos⟩ := hA_prim - have h_Ak_v : (A ^ k) *ᵥ v = (r ^ k) • v := by - have h_gen : ∀ m, (A ^ m) *ᵥ v = (r ^ m) • v := by - intro m - induction m with - | zero => simp - | succ m' ih => - calc (A ^ (m' + 1)) *ᵥ v - _ = A *ᵥ ((A ^ m') *ᵥ v) := by rw [@pow_mulVec_succ] - _ = A *ᵥ (r ^ m' • v) := by rw [ih] - _ = r ^ m' • (A *ᵥ v) := (mulVecLin A).map_smul _ _ - _ = r ^ m' • (r • v) := by rw [h_eigen] - _ = (r ^ (m' + 1)) • v := by rw [smul_smul, pow_succ] - exact h_gen k - have h_Ak_v_pos : ∀ i, 0 < ((A ^ k) *ᵥ v) i := - positive_mul_vec_of_nonneg_vec hk_pos hv_nonneg hv_ne_zero - intro i - rw [h_Ak_v] at h_Ak_v_pos - exact (mul_pos_iff_of_pos_left (pow_pos hr_pos k)).mp (h_Ak_v_pos i) - -/-- The Perron root `r = collatzWielandtFn A v` is positive. -/ -lemma perron_root_pos_of_primitive - (hA_prim : IsPrimitive A) (hA_nonneg : ∀ i j, 0 ≤ A i j) - {v : n → ℝ} (_ : v ∈ stdSimplex ℝ n) (hvM : IsMaxOn (collatzWielandtFn A) (stdSimplex ℝ n) v) : - 0 < collatzWielandtFn A v := by - -- lower-bound sup by the CW-value at the all-ones vector (up to scale) - let ones_norm : n → ℝ := fun _ => (Fintype.card n : ℝ)⁻¹ - have ones_norm_mem_simplex : ones_norm ∈ stdSimplex ℝ n := by - refine ⟨fun i => inv_nonneg.mpr (Nat.cast_nonneg _), ?_⟩ - rw [Finset.sum_const, Finset.card_univ, nsmul_eq_mul] - simp_all only [ne_eq, Nat.cast_eq_zero, Fintype.card_ne_zero, not_false_eq_true, mul_inv_cancel₀] - have h₁ : ones_norm ∈ stdSimplex ℝ n := ones_norm_mem_simplex - have cw_one_pos : 0 < collatzWielandtFn A (fun _ => 1) := - collatzWielandtFn_of_ones_is_pos (Matrix.IsPrimitive.isIrreducible (A := A) hA_prim) hA_nonneg - have cw_scale : collatzWielandtFn A ones_norm = collatzWielandtFn A (fun _ => 1) := by - let c := (Fintype.card n : ℝ)⁻¹ - have hc_pos : 0 < c := inv_pos.mpr (Nat.cast_pos.mpr Fintype.card_pos) - let x : n → ℝ := fun _ => 1 - have hx_nonneg : ∀ i, 0 ≤ x i := fun _ => zero_le_one - have hx_ne_zero : x ≠ 0 := by - intro h - have : (1 : ℝ) = 0 := congr_fun h (Classical.arbitrary n) - exact one_ne_zero this - have h_eq : ones_norm = c • x := by ext; simp [c, x, ones_norm] - rw [h_eq] - exact CollatzWielandt.smul hc_pos hA_nonneg hx_nonneg hx_ne_zero - have cw_le_max : collatzWielandtFn A ones_norm ≤ collatzWielandtFn A v := hvM h₁ - calc - 0 < collatzWielandtFn A (fun _ => 1) := cw_one_pos - _ = collatzWielandtFn A ones_norm := cw_scale.symm - _ ≤ collatzWielandtFn A v := cw_le_max - -open Matrix.CollatzWielandt ---variable [Fintype n] [Nonempty n] [DecidableEq n] {A : Matrix n n ℝ} - -/-- **Perron-Frobenius theorem for primitive matrices - Existence part**-/ -theorem exists_positive_eigenvector_of_primitive - (hA_prim : IsPrimitive A) (hA_nonneg : ∀ i j, 0 ≤ A i j) : - ∃ (r : ℝ) (v : n → ℝ), r > 0 ∧ (∀ i, v i > 0) ∧ A *ᵥ v = r • v := by - -- 1) We get maximizer v on the simplex - haveI : Nonempty (stdSimplex ℝ n) := by - let uniform : n → ℝ := fun _ => (Fintype.card n : ℝ)⁻¹ - use uniform - constructor - · intro i - exact inv_nonneg.mpr (Nat.cast_nonneg _) - · rw [Finset.sum_const, Finset.card_univ, nsmul_eq_mul] - exact mul_inv_cancel₀ (Nat.cast_ne_zero.mpr Fintype.card_ne_zero) - obtain ⟨v, hvS, hvM⟩ := CollatzWielandt.exists_maximizer (A := A) - let r := collatzWielandtFn A v - -- 2) We show r>0 - have hr : 0 < r := perron_root_pos_of_primitive hA_prim hA_nonneg hvS hvM - -- 3) maximizer ⇒ eigenvector - have h_eig := maximizer_is_eigenvector hA_prim hA_nonneg hvM hvS r rfl - -- 4) primitive ⇒ strict positivity of v - have hv0 : v ≠ 0 := by - intro h - have h_sum_zero : ∑ i, v i = 0 := by rw [h]; simp - have h_sum_one : ∑ i, v i = 1 := hvS.2 - linarith [h_sum_zero, h_sum_one] - have hvp := eigenvector_of_primitive_is_positive hA_prim hr h_eig hvS.1 hv0 - use r, v - - -end PerronFrobenius -end Matrix diff --git a/lean/MCMC/PF/LinearAlgebra/Matrix/PerronFrobenius/Stochastic.lean b/lean/MCMC/PF/LinearAlgebra/Matrix/PerronFrobenius/Stochastic.lean deleted file mode 100644 index df02885..0000000 --- a/lean/MCMC/PF/LinearAlgebra/Matrix/PerronFrobenius/Stochastic.lean +++ /dev/null @@ -1,81 +0,0 @@ -import MCMC.PF.LinearAlgebra.Matrix.PerronFrobenius.Dominance - -namespace Matrix - -open Matrix CollatzWielandt Quiver.Path - -variable {n : Type*} [Fintype n] [DecidableEq n] [Nonempty n] - -/-- -**Perron-Frobenius Theorem for Column-Stochastic Matrices**. - -An irreducible, non-negative matrix with column sums equal to 1 (i.e., column-stochastic) -has a Perron root of 1, and there exists a unique (up to scaling) strictly positive -eigenvector `v` for this eigenvalue. This stationary vector is unique when constrained -to the standard simplex. --/ -theorem exists_positive_eigenvector_of_irreducible_stochastic - {A : Matrix n n ℝ} (hA_irred : A.IsIrreducible) (h_col_stoch : ∀ j, ∑ i, A i j = 1) : - ∃! (v : stdSimplex ℝ n), A *ᵥ v.val = v.val := by - have hA_nonneg : ∀ i j, 0 ≤ A i j := hA_irred.1 - let A_T := Aᵀ - have hAT_nonneg : ∀ i j, 0 ≤ A_T i j := fun i j => hA_nonneg j i - have hAT_row_sum : ∀ i, ∑ j, A_T i j = 1 := by - simpa [A_T, transpose_apply] using h_col_stoch - have h_ones_eig : A_T *ᵥ (fun _ => 1) = 1 • (fun _ => 1) := by - simpa using row_sum_eigenvalue hAT_nonneg 1 hAT_row_sum - have hAT_irred : A_T.IsIrreducible := Matrix.IsIrreducible.transpose hA_irred - have r_AT_eq_one : perronRoot_alt A_T = 1 := by - have h := - eigenvalue_is_perron_root_of_positive_eigenvector - (A := A_T) (r := 1) (v := fun _ => 1) - hAT_irred hAT_nonneg (by norm_num) (by intro _; exact Real.zero_lt_one) - aesop - have r_A_eq_one : perronRoot_alt A = 1 := by - exact (perronRoot_transpose_eq A hA_irred).trans r_AT_eq_one - obtain ⟨v_raw, ⟨r, hr_pos, h_eig⟩, v_raw_unique⟩ := pft_irreducible hA_irred - let v : stdSimplex ℝ n := v_raw - have h_eig_one : A *ᵥ v.val = v.val := by - have r_eq_perron : r = perronRoot_alt A := by - apply eigenvalue_is_perron_root_of_positive_eigenvector - · exact hA_irred - · exact hA_nonneg - · exact hr_pos - · have v_ne_zero : v.val ≠ 0 := ne_zero_of_mem_stdSimplex v.property - have hv_nonneg : ∀ i, 0 ≤ v.val i := v.property.1 - exact eigenvector_is_positive_of_irreducible hA_irred h_eig hv_nonneg v_ne_zero - · exact h_eig - have : A *ᵥ v.val = (perronRoot_alt A) • v.val := by - aesop - simpa [r_A_eq_one, one_smul] using this - refine ⟨v, h_eig_one, ?_⟩ - intro w hw_eig - have v_pos : ∀ i, 0 < v.val i := by - have v_ne_zero : v.val ≠ 0 := ne_zero_of_mem_stdSimplex v.property - have hv_nonneg : ∀ i, 0 ≤ v.val i := v.property.1 - exact fun i ↦ - eigenvector_no_zero_entries_of_irreducible hA_irred hr_pos h_eig hv_nonneg v_ne_zero i - have w_pos : ∀ i, 0 < w.val i := by - have hw_nonneg : ∀ i, 0 ≤ w.val i := w.property.1 - have hw_ne_zero : w.val ≠ 0 := ne_zero_of_mem_stdSimplex w.property - have hw_eig' : A *ᵥ w.val = (1 : ℝ) • w.val := by - simpa [one_smul] using hw_eig - exact eigenvector_is_positive_of_irreducible hA_irred hw_eig' hw_nonneg hw_ne_zero - obtain ⟨c, hc_pos, hc_eq⟩ := - uniqueness_of_positive_eigenvector_gen hA_irred - (by norm_num : (0 : ℝ) < 1) - v_pos w_pos - (by simpa [one_smul] using h_eig_one) - (by simpa [one_smul] using hw_eig) - have h_sum_v : ∑ i, v.val i = 1 := v.property.2 - have h_sum_w : ∑ i, w.val i = 1 := w.property.2 - have hc_one : c = 1 := by - calc - c = c * 1 := (mul_one c).symm - _ = c * (∑ i, w.val i) := by simp [h_sum_w] - _ = ∑ i, c * w.val i := by rw [Finset.mul_sum] - _ = ∑ i, v.val i := by simp [hc_eq, smul_eq_mul] - _ = 1 := h_sum_v - exact Subtype.val_injective (by simp [hc_eq, hc_one, one_smul]) - -end Matrix diff --git a/lean/MCMC/PF/LinearAlgebra/Matrix/PerronFrobenius/Uniqueness.lean b/lean/MCMC/PF/LinearAlgebra/Matrix/PerronFrobenius/Uniqueness.lean deleted file mode 100644 index b2ad354..0000000 --- a/lean/MCMC/PF/LinearAlgebra/Matrix/PerronFrobenius/Uniqueness.lean +++ /dev/null @@ -1,97 +0,0 @@ -import MCMC.PF.LinearAlgebra.Matrix.PerronFrobenius.Primitive - -/-! -# Perron-Frobenius Theorem: Uniqueness of the Eigenvector - -This file proves that for a primitive, non-negative matrix, the strictly positive -eigenvector corresponding to the Perron root `r` is unique up to a positive -scalar multiple. This corresponds to part (d) of Theorem 1.1 in Seneta's -"Non-negative Matrices and Markov Chains". - -The proof strategy is as follows: -1. Given two strictly positive eigenvectors `v` and `w` for the same eigenvalue `r`. -2. Construct a new vector `z = v - c • w`, where `c` is the minimum of the - component-wise ratios `vᵢ / wᵢ`. -3. By construction, `z` is a non-negative vector, and at least one of its - components is zero. -4. Show that `z` is also an eigenvector of `A` with eigenvalue `r`. -5. Use the primitivity of `A` to show that if `z` were non-zero, then `A^k * z` - would be strictly positive for a large enough `k`. -6. However, `A^k * z = r^k • z`, which must have a zero component since `z` does. - This is a contradiction. -7. Thus, `z` must be the zero vector, which implies `v = c • w`. --/ - -namespace Matrix -variable {n : Type*} [Fintype n] [Nonempty n] [DecidableEq n] -variable {A : Matrix n n ℝ} {r : ℝ} - -/-- -A non-zero, non-negative eigenvector `z` of a primitive matrix `A` corresponding to a -positive eigenvalue `r` cannot have any zero entries. This is a crucial lemma for -the uniqueness and dominance proofs. --/ -lemma eigenvector_no_zero_entries_of_primitive (hA_prim : IsPrimitive A) (_ : 0 < r) - {z : n → ℝ} (h_eig : A *ᵥ z = r • z) (hz_nonneg : ∀ i, 0 ≤ z i) (hz_ne_zero : z ≠ 0) - (i₀ : n) (hi₀_zero : z i₀ = 0) : - False := by - rcases hA_prim with ⟨_, ⟨k, _, hA_k_pos⟩⟩ - have h_Ak_z_eig : (A ^ k) *ᵥ z = (r ^ k) • z := by - have h_gen : ∀ m, (A ^ m) *ᵥ z = (r ^ m) • z := by - intro m - induction m with - | zero => simp - | succ m ih => - calc (A ^ (m + 1)) *ᵥ z - = A *ᵥ (A ^ m *ᵥ z) := by rw [pow_mulVec_succ] - _ = A *ᵥ (r ^ m • z) := by rw [ih] - _ = r ^ m • (A *ᵥ z) := by rw [mulVec_smul] - _ = r ^ m • (r • z) := by rw [h_eig] - _ = (r ^ m * r) • z := by rw [smul_smul] - _ = (r ^ (m + 1)) • z := by rw [pow_succ']; rw [@pow_mul_comm'] - exact h_gen k - have h_rhs_zero : ((r ^ k) • z) i₀ = 0 := by - simp [hi₀_zero] - have h_lhs_pos : 0 < ((A ^ k) *ᵥ z) i₀ := - positive_mul_vec_of_nonneg_vec hA_k_pos hz_nonneg hz_ne_zero i₀ - rw [h_Ak_z_eig] at h_lhs_pos - exact lt_irrefl 0 (h_rhs_zero ▸ h_lhs_pos) - -/-- -**Uniqueness of the Positive Eigenvector for Primitive Matrices** - -The positive eigenvector of a primitive matrix `A` corresponding to a positive eigenvalue `r` -is unique up to a positive scalar multiple. This corresponds to Theorem 1.1 (d) in Seneta. --/ -theorem uniqueness_of_positive_eigenvector (hA_prim : IsPrimitive A) (hr_pos : 0 < r) - (v w : n → ℝ) (hv_eig : A *ᵥ v = r • v) (hw_eig : A *ᵥ w = r • w) - (hv_pos : ∀ i, 0 < v i) (hw_pos : ∀ i, 0 < w i) : - ∃ c : ℝ, 0 < c ∧ v = c • w := by - let c := Finset.univ.inf' Finset.univ_nonempty (fun i => v i / w i) - have hc_pos : 0 < c := by - apply Finset.inf'_pos Finset.univ_nonempty - intro i _ - exact div_pos (hv_pos i) (hw_pos i) - let z := v - c • w - have hz_nonneg : ∀ i, 0 ≤ z i := by - intro i - simp only [z, Pi.sub_apply, Pi.smul_apply, smul_eq_mul, sub_nonneg] - have hc_le_ratio : c ≤ v i / w i := Finset.inf'_le _ (Finset.mem_univ i) - exact (le_div_iff₀ (hw_pos i)).mp hc_le_ratio - have hz_eig : A *ᵥ z = r • z := by - dsimp [z]; simp only [mulVec_sub]; simp only [mulVec_smul]; simp only [ hv_eig]; simp only [hw_eig]; simp only [smul_sub]; rw [smul_comm] - by_cases h_z_is_zero : z = 0 - · use c, hc_pos - ext i - simpa [z, sub_eq_zero] using congr_fun h_z_is_zero i - · have hz_has_zero : ∃ i₀, z i₀ = 0 := by - obtain ⟨i₀, _, hi₀_eq_inf⟩ := Finset.exists_mem_eq_inf' Finset.univ_nonempty (fun i => v i / w i) - use i₀ - simp only [z, Pi.sub_apply, Pi.smul_apply, smul_eq_mul, sub_eq_zero] - rw [← div_eq_iff (hw_pos i₀).ne'] - rw [← hi₀_eq_inf] - obtain ⟨i₀, hi₀_zero⟩ := hz_has_zero - have h_contra := eigenvector_no_zero_entries_of_primitive hA_prim hr_pos hz_eig hz_nonneg h_z_is_zero i₀ hi₀_zero - exact h_contra.elim - -end Matrix diff --git a/lean/MCMC/PF/LinearAlgebra/Matrix/Spectrum.lean b/lean/MCMC/PF/LinearAlgebra/Matrix/Spectrum.lean deleted file mode 100644 index 43bf835..0000000 --- a/lean/MCMC/PF/LinearAlgebra/Matrix/Spectrum.lean +++ /dev/null @@ -1,696 +0,0 @@ -import Mathlib.Algebra.Lie.OfAssociative -import Mathlib.Analysis.InnerProductSpace.Basic -import Mathlib.Analysis.Normed.Algebra.Spectrum -import Mathlib.Analysis.RCLike.Lemmas -import Mathlib.Data.Real.StarOrdered -import Mathlib.LinearAlgebra.Eigenspace.Basic -import Mathlib.LinearAlgebra.Matrix.Charpoly.Eigs -import Mathlib.RingTheory.DedekindDomain.Dvr -import Mathlib.RingTheory.FiniteLength -import Mathlib.RingTheory.SimpleRing.Principal - -/-! # Perron-Frobenius Theory for Matrices - -This file develops the essential Perron-Frobenius theory needed for MCMC convergence proofs. - -I. Core Algebraic Structures & Utilities - -Rings/Fields: -CommRing R, DivisionRing K, Field K -Nontrivial R: Typeclass asserting R has at least two distinct elements. -Modules/Vector Spaces: -Module R M: Standard R-module structure on M. -VectorSpace K V: Alias for Module K V when K is a field. -Algebras: -Algebra R A: A is an R-algebra. -algebraMap R A : R →+* A: The structural ring homomorphism. -Polynomials (R[X] or Polynomial R): -Polynomial.eval (μ : R) (p : R[X]) : R: Evaluation of polynomial p at scalar μ. -Polynomial.aeval (a : A) (p : R[X]) : A: Evaluation of p at an element a in an R-algebra A. -Polynomial.IsRoot p μ : Prop := Polynomial.eval μ p = 0. -Polynomial.coeff p n : R: The n-th coefficient of p. -Polynomial.leadingCoeff p : R. -Polynomial.monic p : Prop. -Polynomial.natDegree p : ℕ. -Polynomial.natTrailingDegree p : ℕ. -minpoly K a : K[X]: Minimal polynomial of a : A over a field K. -minpoly.aeval K a : aeval a (minpoly K a) = 0. -minpoly.dvd K a p (hp : aeval a p = 0) : minpoly K a ∣ p. -Polynomial.annIdealGenerator_eq_minpoly {𝕜 A} [Field 𝕜] [Ring A] [Algebra 𝕜 A] (a : A) : Polynomial.annIdealGenerator 𝕜 a = minpoly 𝕜 a. - -II. Linear Algebra: Maps, Submodules, Basis, Dimension - -Linear Maps (M →ₗ[R] N): -LinearMap.ker : Submodule R M, LinearMap.range : Submodule R N. -LinearMap.id : M →ₗ[R] M, LinearMap.comp (g : N →ₗ[R] P) (f : M →ₗ[R] N) : M →ₗ[R] P. -Function.Injective, Function.Surjective, Function.Bijective. -LinearMap.quotKerEquivRange (f : M →ₗ[R] N) : M ⧸ LinearMap.ker f ≃ₗ[R] LinearMap.range f. -Linear Equivalences (M ≃ₗ[R] N). -Submodules (Submodule R M): -⊥ (zero submodule), ⊤ (entire module). -Submodule.span R (s : Set M) : Submodule R M. -Submodule.mkQ (p : Submodule R M) : M →ₗ[R] M ⧸ p (quotient map). -Basis (Basis ι R M): -b i : M: The i-th basis vector. -repr : M ≃ₗ[R] (ι →₀ R): The isomorphism to the module of finitely supported functions (coordinates). -Basis.mk (hli : LinearIndependent R v) (hsp : Submodule.span R (Set.range v) = ⊤) : Basis ι R M. -Basis.linearIndependent : LinearIndependent R b. -Basis.span_eq : Submodule.span R (Set.range b) = ⊤. -Basis.ofVectorSpace K V : Basis (Basis.ofVectorSpaceIndex K V) K V (existence of a basis for vector spaces over a field K). -Pi.basisFun R n : Basis (Fin n) R (Fin n → R). -Linear Independence (LinearIndependent R v): -linearIndependent_iff. -LinearIndependent.cardinal_le_rank : #ι ≤ Module.rank R M (if Nontrivial R). -Rank (Cardinal-valued dimension): Module.rank R M : Cardinal. -Basis.mk_eq_rank'' (b : Basis ι R M) : #ι = Module.rank R M (for rings with Strong Rank Condition). -LinearMap.lift_rank_le_of_injective (f : M →ₗ[R] N') (hf : Injective f). -LinearMap.rank_le_of_surjective (f : M →ₗ[R] N) (hf : Surjective f). -LinearMap.rank_range_add_rank_ker (f : M →ₗ[R] N) : Module.rank R (LinearMap.range f) + Module.rank R (LinearMap.ker f) = Module.rank R M (for rings with HasRankNullity, e.g., division rings). -rank_quotient_add_rank_of_divisionRing (p : Submodule K V). -Finrank (Nat-valued dimension): Module.finrank R M : ℕ. -finrank_eq_rank : ↑(finrank R M) = Module.rank R M (if Module.Finite R M and StrongRankCondition R). -FiniteDimensional K V: Typeclass, equivalent to Module.Finite K V for division rings. -FiniteDimensional.of_fintype_basis (b : Basis ι K V) [Fintype ι]. -finrank_eq_card_basis [Fintype ι] (b : Basis ι K V) : finrank K V = Fintype.card ι (for StrongRankCondition R). -LinearMap.injective_iff_surjective [FiniteDimensional K V] (f : End K V). -Submodule.finrank_lt [FiniteDimensional K V] {s : Submodule K V} (h : s ≠ ⊤) : finrank K s < finrank K V. -Submodule.finrank_quotient_add_finrank [FiniteDimensional K V] (N : Submodule K V). - -III. Matrices - -Matrix n n R (Square matrices, often n is a Fintype). -Matrix.det (A : Matrix n n R) : R. -Matrix.det_mul, Matrix.det_one, Matrix.det_transpose. -Matrix.isUnit_iff_isUnit_det. -Matrix.det_smul_sub_eq_eval_charpoly (A : Matrix n n ℝ) (μ : ℝ) : det (μ • 1 - A) = (Matrix.charpoly A).eval μ. -Matrix.toLin' (A : Matrix n n R) : (Fin n → R) →ₗ[R] (Fin n → R) (matrix as a linear map on Fin n → R). -LinearMap.toMatrix (b₁ : Basis ι R M) (b₂ : Basis ι' R N) (f : M →ₗ[R] N) : Matrix ι' ι R. -Matrix.toLinAlgEquiv (b : Basis ι R M) [Fintype ι] [DecidableEq ι] : End R M ≃ₐ[R] Matrix ι ι R. -Matrix.charpoly (A : Matrix n n R) : R[X]. -Matrix.aeval_self_charpoly A : Polynomial.aeval A (Matrix.charpoly A) = 0 (Cayley-Hamilton for matrices). -Matrix.charpoly_transpose A : Matrix.charpoly Aᵀ = Matrix.charpoly A. - -IV. Endomorphisms, Eigenvalues, Eigenspaces, Spectrum - -Module.End R M := M →ₗ[R] M. -LinearMap.det (f : End R M) : R. -LinearMap.det_toMatrix (b : Basis ι R M) f : Matrix.det (LinearMap.toMatrix b b f) = LinearMap.det f. -LinearMap.isUnit_iff_isUnit_det. -LinearMap.det_eq_sign_charpoly_coeff {R M} [CommRing R] [Module.Free R M] [Module.Finite R M] (f : End R M) : LinearMap.det f = (-1) ^ Module.finrank R M * (LinearMap.charpoly f).coeff 0. -LinearMap.charpoly (f : End R M) : R[X] (where M is finite and free). -LinearMap.aeval_self_charpoly f : Polynomial.aeval f (LinearMap.charpoly f) = 0 (Cayley-Hamilton for endomorphisms). -LinearMap.charpoly_toMatrix (b : Basis ι R M) f : (Matrix.toMatrix b b f).charpoly = LinearMap.charpoly f. -LinearMap.minpoly_dvd_charpoly {K V} [Field K] [FiniteDimensional K V] (f : End K V). -spectrum R a (for a : A in an R-algebra A). -spectrum.mem_iff : μ ∈ spectrum R a ↔ ¬IsUnit (algebraMap R A μ - a). -Matrix.spectrum_eq_spectrum_toLin' (A : Matrix n n ℝ). -Module.End.hasEigenvalue_iff_mem_spectrum {f : End K V} {μ : K}. -Matrix.mem_spectrum_iff_isRoot_charpoly (A : Matrix n n ℝ) (μ : ℝ). -Module.End.HasEigenvalue (f : End R M) (μ : R) : Prop. -Module.End.HasEigenvector (f : End R M) (μ : R) (x : M) : Prop. -Module.End.HasEigenvector.apply_eq_smul (h : HasEigenvector f μ x) : f x = μ • x. -Module.End.eigenspace (f : End R M) (μ : R) : Submodule R M. -Module.End.eigenspace_def : eigenspace f μ = LinearMap.ker (f - μ • LinearMap.id). -Module.End.mem_eigenspace_iff : x ∈ eigenspace f μ ↔ f x = μ • x. -Module.End.genEigenspace (f : End R M) (μ : R) (k : ℕ∞). -Module.End.maxGenEigenspace (f : End R M) (μ : R) := ⨆ k, genEigenspace f μ k. -Module.End.iSup_maxGenEigenspace_eq_top [IsAlgClosed K] [FiniteDimensional K V] (f : End K V). -Module.End.IsFinitelySemisimple.genEigenspace_eq_eigenspace (hf : f.IsFinitelySemisimple). -Module.End.isRoot_of_hasEigenvalue {f : End K V} {μ : K} (h : HasEigenvalue f μ) : (minpoly K f).IsRoot μ. -Module.End.hasEigenvalue_of_isRoot {f : End K V} {μ : K} (h : (minpoly K f).IsRoot μ) : HasEigenvalue f μ. -LinearMap.hasEigenvalue_zero_tfae (φ : End K M): List of equivalent conditions for 0 being an eigenvalue (e.g., det φ = 0, ker φ ≠ ⊥). -Matrix.hasEigenvalue_toLin'_iff_det_sub_eq_zero (A : Matrix n n ℝ) (μ : ℝ). -Module.End.exists_eigenvalue [IsAlgClosed K] [FiniteDimensional K V] [Nontrivial V] (f : End K V). -Module.End.eigenvectors_linearIndependent [NoZeroSMulDivisors R M] (f : End R M) (μs : Set R) (xs : μs → M) (h_eigenvec). - --/ - -namespace Matrix -open LinearMap Polynomial Module - -variable {n : Type*} [Fintype n] [DecidableEq n] - -/-! -## The Standard Simplex --/ - -omit [DecidableEq n] in -lemma stdSimplex_nonempty [Nonempty n] : (stdSimplex ℝ n).Nonempty := - ⟨(Fintype.card n : ℝ)⁻¹ • 1, by simp [stdSimplex, Finset.sum_const, nsmul_eq_mul]⟩ - -omit [DecidableEq n] in -lemma isCompact_stdSimplex : IsCompact (stdSimplex ℝ n) := - _root_.isCompact_stdSimplex ℝ n - -omit [DecidableEq n] in -lemma convex_stdSimplex : Convex ℝ (stdSimplex ℝ n) := - _root_.convex_stdSimplex ℝ n - -/-! -## Spectral Properties of Matrices --/ - -/-- The spectrum of a matrix `A` is equal to the spectrum of its corresponding linear map -`Matrix.toLin' A`. -/ -lemma spectrum_eq_spectrum_toLin' (A : Matrix n n ℝ) : - spectrum ℝ A = spectrum ℝ (Matrix.toLin' A) := by - exact Eq.symm (AlgEquiv.spectrum_eq (Matrix.toLinAlgEquiv (Pi.basisFun ℝ n)) A) - -/-- The determinant of `μ • 1 - A` is the evaluation of the characteristic polynomial of `A` at `μ`. -/ -lemma det_smul_sub_eq_eval_charpoly (A : Matrix n n ℝ) (μ : ℝ) : - det (μ • 1 - A) = (Matrix.charpoly A).eval μ := by - have h : μ • 1 = Matrix.scalar n μ := by - ext i j - simp [Matrix.scalar, Matrix.one_apply, smul_apply] - rfl - rw [h] - rw [← eval_charpoly A μ] - - -/-- A matrix and its transpose have the same spectrum. -/ -lemma spectrum_eq_spectrum_transpose (A : Matrix n n ℝ) : - spectrum ℝ A = spectrum ℝ Aᵀ := by - ext μ - rw [mem_spectrum_iff_isRoot_charpoly, mem_spectrum_iff_isRoot_charpoly, charpoly_transpose] - -/-! -## Determinant, Kernel, and Invertibility --/ -open Module - -/-- The determinant of a matrix equals the determinant of its associated linear map. -/ -lemma det_toLin' (A : Matrix n n ℝ) : det A = LinearMap.det (toLin' A) := by - rw [← LinearMap.det_toMatrix' (toLin' A)] - simp [LinearMap.toMatrix'_toLin'] - -/-! -# Perron-Frobenius Theory for Matrices - -This file provides core lemmas and theorems related to the Perron-Frobenius theory for non-negative, -irreducible matrices. --/ - -open LinearMap - -variable {R : Type*} [CommRing R] {n : Type*} [Fintype n] [DecidableEq n] - -/-- If a linear map is not injective, then its kernel is non-trivial. -/ -lemma ker_ne_bot_of_not_injective {K V : Type*} [Field K] [AddCommGroup V] [Module K V] - {f : V →ₗ[K] V} (h : ¬Function.Injective f) : LinearMap.ker f ≠ ⊥ := by - contrapose! h - rw [← LinearMap.ker_eq_bot] - exact h - -lemma LinearMap.isUnit_iff_bijective {K V : Type*} [Field K] [AddCommGroup V] [Module K V] - [FiniteDimensional K V] (f : V →ₗ[K] V) : IsUnit f ↔ Function.Bijective f := by - constructor - · intro h_unit - exact (Module.End.isUnit_iff f).mp h_unit - · intro h_bij - rw [LinearMap.isUnit_iff_ker_eq_bot] - rw [LinearMap.ker_eq_bot] - exact h_bij.1 - -lemma LinearMap.injective_of_isUnit {K V : Type*} [Field K] [AddCommGroup V] [Module K V] - [FiniteDimensional K V] {f : V →ₗ[K] V} (h : IsUnit f) : Function.Injective f := by - rw [← LinearMap.ker_eq_bot] - rw [← LinearMap.isUnit_iff_ker_eq_bot] - exact h - -/-- If the kernel of a linear endomorphism on a finite-dimensional vector space is non-trivial, - then its determinant is zero. -/ -lemma det_eq_zero_of_ker_ne_bot {K V : Type*} [Field K] [AddCommGroup V] [Module K V] [DecidableEq ↑(Module.Basis.ofVectorSpaceIndex K V)] - [FiniteDimensional K V] {f : V →ₗ[K] V} (h : LinearMap.ker f ≠ ⊥) : - LinearMap.det f = 0 := by - by_contra h_det_ne_zero - have h_det_unit : IsUnit (LinearMap.det f) := IsUnit.mk0 _ h_det_ne_zero - have h_f_is_unit : IsUnit f := by - let b := Module.Basis.ofVectorSpace K V - classical - have h_det_matrix_unit : IsUnit (Matrix.det (LinearMap.toMatrix b b f)) := by - rw [LinearMap.det_toMatrix b f] - exact h_det_unit - have h_toMatrix_unit : IsUnit (LinearMap.toMatrix b b f) := - (Matrix.isUnit_iff_isUnit_det _).mpr h_det_matrix_unit - rw [← isUnit_map_iff ((Matrix.toLinAlgEquiv b).symm) f] - exact h_toMatrix_unit - have h_ker_eq_bot : LinearMap.ker f = ⊥ := by - rw [← LinearMap.isUnit_iff_ker_eq_bot] - exact h_f_is_unit - exact h h_ker_eq_bot - -/-- If a non-zero vector `v` is in the kernel of a linear map `f`, then `det f` must be zero. -/ -lemma det_eq_zero_of_exists_mem_ker {K V} [Field K] [AddCommGroup V] [Module K V] [DecidableEq ↑(Module.Basis.ofVectorSpaceIndex K V)] - [FiniteDimensional K V] {f : V →ₗ[K] V} (h : ∃ v, v ≠ 0 ∧ f v = 0) : - LinearMap.det f = 0 := by - apply det_eq_zero_of_ker_ne_bot - obtain ⟨v, hv_ne_zero, hv_ker⟩ := h - rw [Submodule.ne_bot_iff] - use v - exact ⟨LinearMap.mem_ker.mpr hv_ker, hv_ne_zero⟩ - -/-- If a linear endomorphism on a finite-dimensional vector space is not injective, - then its determinant is zero. -/ -lemma det_eq_zero_of_not_injective {K V : Type*} [Field K] [AddCommGroup V] [Module K V] [DecidableEq ↑(Module.Basis.ofVectorSpaceIndex K V)] - [FiniteDimensional K V] {f : V →ₗ[K] V} (h : ¬Function.Injective f) : - LinearMap.det f = 0 := by - apply det_eq_zero_of_ker_ne_bot - exact ker_ne_bot_of_not_injective h - -omit [DecidableEq n] in -/-- If the determinant is zero, the linear map is not injective. -/ -lemma not_injective_of_det_eq_zero {f : (n → ℝ) →ₗ[ℝ] (n → ℝ)} (h : LinearMap.det f = 0) : - ¬Function.Injective f := by - by_contra h_inj - have h_unit : IsUnit f := by - rw [LinearMap.isUnit_iff_ker_eq_bot] - rwa [LinearMap.ker_eq_bot] - have h_det_unit : IsUnit (LinearMap.det f) := by - exact LinearMap.isUnit_det f h_unit - have h_det_ne_zero : LinearMap.det f ≠ 0 := by - exact IsUnit.ne_zero h_det_unit - exact h_det_ne_zero h - -/-- For a matrix `A`, the associated linear map `toLin' A` has a non-trivial kernel -if and only if the determinant of `A` is zero. -/ -lemma ker_toLin'_ne_bot_iff_det_eq_zero (A : Matrix n n ℝ) : - LinearMap.ker (toLin' A) ≠ ⊥ ↔ det A = 0 := by - constructor - · intro h_ker_ne_bot - rw [det_toLin'] - have h_not_inj : ¬Function.Injective (toLin' A) := by - rwa [← LinearMap.ker_eq_bot] - exact det_eq_zero_of_not_injective h_not_inj - · intro h_det_zero - by_contra h_ker_eq_bot - rw [LinearMap.ker_eq_bot] at h_ker_eq_bot - rw [det_toLin'] at h_det_zero - have h_det_ne_zero : LinearMap.det (toLin' A) ≠ 0 := by - by_contra h_zero - exact not_injective_of_det_eq_zero h_zero h_ker_eq_bot - exact h_det_ne_zero h_det_zero - -/-- A real number `μ` is an eigenvalue of a matrix `A` if and only if `det(μ • 1 - A) = 0`. -/ -lemma hasEigenvalue_toLin'_iff_det_sub_eq_zero (A : Matrix n n ℝ) (μ : ℝ) : - Module.End.HasEigenvalue (toLin' A) μ ↔ det (μ • 1 - A) = 0 := by - rw [Module.End.hasEigenvalue_iff_mem_spectrum, ← spectrum_eq_spectrum_toLin', - mem_spectrum_iff_isRoot_charpoly, Polynomial.IsRoot.def, det_smul_sub_eq_eval_charpoly] - -/-! ## Spectral Radius Theory for Matrices -/ - -lemma not_isUnit_iff_eq_zero {R : Type*} [Field R] [Nontrivial R] (a : R) : - ¬IsUnit a ↔ a = 0 ∨ a ∈ nonunits R := by - constructor - · intro h - by_cases ha : a = 0 - · exact Or.inl ha - · exact Or.inr h - · intro h - cases h with - | inl h0 => rw [h0]; exact not_isUnit_zero - | inr hnon => exact hnon - -lemma LinearMap.bijective_iff_ker_eq_bot_and_range_eq_top {R : Type*} [Field R] {M : Type*} - [AddCommGroup M] [Module R M] (f : M →ₗ[R] M) : - Function.Bijective f ↔ LinearMap.ker f = ⊥ ∧ LinearMap.range f = ⊤ := by - constructor - · intro h - constructor - · exact LinearMap.ker_eq_bot_of_injective h.1 - · exact LinearMap.range_eq_top_of_surjective _ h.2 - · intro ⟨h_ker, h_range⟩ - constructor - · exact LinearMap.ker_eq_bot.mp h_ker - · exact LinearMap.range_eq_top.mp h_range - -lemma ker_ne_bot_of_det_eq_zero (A : Matrix n n ℝ) : - LinearMap.det (Matrix.toLin' A) = 0 → LinearMap.ker (Matrix.toLin' A) ≠ ⊥ := by - contrapose! - intro h_ker_bot - have h_inj : Function.Injective (Matrix.toLin' A) := by - rw [← LinearMap.ker_eq_bot] - exact h_ker_bot - have h_isUnit : IsUnit (LinearMap.det (Matrix.toLin' A)) := - LinearMap.isUnit_det (Matrix.toLin' A) ((LinearMap.isUnit_iff_ker_eq_bot - (toLin' A)).mpr h_ker_bot) - exact IsUnit.ne_zero h_isUnit - --- Basic kernel-injectivity relationship -lemma ker_eq_bot_iff_injective_toLin' (A : Matrix n n ℝ) : - LinearMap.ker (Matrix.toLin' A) = ⊥ ↔ Function.Injective (Matrix.toLin' A) := by - exact LinearMap.ker_eq_bot - --- For finite dimensions, injective endomorphisms are bijective -lemma injective_iff_bijective_toLin' (A : Matrix n n ℝ) : - Function.Injective (Matrix.toLin' A) ↔ Function.Bijective (Matrix.toLin' A) := by - constructor - · intro h_inj - exact IsArtinian.bijective_of_injective_endomorphism (toLin' A) h_inj - · exact fun h_bij => h_bij.1 - --- Bijective linear maps are units -lemma bijective_iff_isUnit_toLin' (A : Matrix n n ℝ) : - Function.Bijective (Matrix.toLin' A) ↔ IsUnit (Matrix.toLin' A) := by - haveI : FiniteDimensional ℝ (n → ℝ) := by infer_instance - rw [LinearMap.bijective_iff_ker_eq_bot_and_range_eq_top] - have h_equiv : LinearMap.range (Matrix.toLin' A) = ⊤ ↔ LinearMap.ker (Matrix.toLin' A) = ⊥ := - Iff.symm LinearMap.ker_eq_bot_iff_range_eq_top - rw [h_equiv, and_self] - rw [LinearMap.isUnit_iff_ker_eq_bot] - -lemma isUnit_of_det_ne_zero (A : Matrix n n ℝ) (h_det_ne_zero : LinearMap.det (Matrix.toLin' A) ≠ 0) : - IsUnit (Matrix.toLin' A) := by - rw [← bijective_iff_isUnit_toLin', ← injective_iff_bijective_toLin', ← ker_eq_bot_iff_injective_toLin'] - by_contra h_ker_ne_bot - have h_det_zero : LinearMap.det (Matrix.toLin' A) = 0 := by - exact det_eq_zero_of_ker_ne_bot h_ker_ne_bot - exact h_det_ne_zero h_det_zero - - --- An algebra equivalence preserves the property of being a unit. -lemma AlgEquiv.isUnit_map_iff {R A B : Type*} [CommSemiring R] [Ring A] [Ring B] - [Algebra R A] [Algebra R B] (e : A ≃ₐ[R] B) (x : A) : - IsUnit (e x) ↔ IsUnit x := by - constructor - · intro h_ex_unit - simp_all only [MulEquiv.isUnit_map] - · intro h_x_unit - simp_all only [MulEquiv.isUnit_map] - -lemma isUnit_of_det_ne_zero' {n : Type*} [Fintype n] [DecidableEq n] (A : Matrix n n ℝ) (h_det_ne_zero : LinearMap.det (Matrix.toLin' A) ≠ 0) : - IsUnit (Matrix.toLin' A) := by - let f := Matrix.toLin' A - have h_det_f_is_unit : IsUnit (LinearMap.det f) := IsUnit.mk0 (LinearMap.det f) h_det_ne_zero - let b := Pi.basisFun ℝ n - have h_det_matrix_form_is_unit : IsUnit (Matrix.det (LinearMap.toMatrix b b f)) := - (LinearMap.det_toMatrix b f).symm ▸ h_det_f_is_unit - have h_matrix_representation_is_unit : IsUnit (LinearMap.toMatrix b b f) := - (Matrix.isUnit_iff_isUnit_det _).mpr h_det_matrix_form_is_unit - simp only at h_matrix_representation_is_unit - have h_toMatrix_eq_A : LinearMap.toMatrix b b f = A := by - exact (LinearEquiv.eq_symm_apply (toMatrix b b)).mp rfl - rw [h_toMatrix_eq_A] at h_matrix_representation_is_unit - rw [← bijective_iff_isUnit_toLin'] - have h_isUnit_toLin : IsUnit (Matrix.toLin' A) := by - let e : Matrix n n ℝ ≃ₐ[ℝ] Module.End ℝ (n → ℝ) := Matrix.toLinAlgEquiv (Pi.basisFun ℝ n) - change IsUnit (e A) - rw [AlgEquiv.isUnit_map_iff e A] - exact h_matrix_representation_is_unit - exact (bijective_iff_isUnit_toLin' A).mpr h_isUnit_toLin - -lemma det_eq_zero_of_ker_ne_bot' (A : Matrix n n ℝ) : - LinearMap.ker (Matrix.toLin' A) ≠ ⊥ → LinearMap.det (Matrix.toLin' A) = 0 := by - contrapose! - intro h_det_ne_zero - have h_isUnit_det : IsUnit (LinearMap.det (Matrix.toLin' A)) := - isUnit_iff_ne_zero.mpr h_det_ne_zero - have h_isUnit_map : IsUnit (Matrix.toLin' A) := isUnit_of_det_ne_zero A h_det_ne_zero - have h_bijective : Function.Bijective (Matrix.toLin' A) := by - rw [LinearMap.bijective_iff_ker_eq_bot_and_range_eq_top] - constructor - · exact (LinearMap.isUnit_iff_ker_eq_bot (Matrix.toLin' A)).mp h_isUnit_map - · exact (LinearMap.isUnit_iff_range_eq_top (toLin' A)).mp h_isUnit_map - exact LinearMap.ker_eq_bot_of_injective h_bijective.1 - -lemma det_eq_zero_iff_exists_nontrivial_ker (A : Matrix n n ℝ) : - Matrix.det A = 0 ↔ ∃ v : n → ℝ, v ≠ 0 ∧ Matrix.mulVec A v = 0 := by - rw [← LinearMap.det_toLin'] - constructor - · intro h_det_zero - have h_ker_ne_bot : LinearMap.ker (Matrix.toLin' A) ≠ ⊥ := - ker_ne_bot_of_det_eq_zero A h_det_zero - obtain ⟨v, hv_mem, hv_ne_zero⟩ := Submodule.exists_mem_ne_zero_of_ne_bot h_ker_ne_bot - use v - constructor - · exact hv_ne_zero - · rw [← Matrix.toLin'_apply] - exact hv_mem - · intro ⟨v, hv_ne_zero, hv_ker⟩ - have h_ker_ne_bot : LinearMap.ker (Matrix.toLin' A) ≠ ⊥ := by - intro h_ker_bot - have hv_in_ker : v ∈ LinearMap.ker (Matrix.toLin' A) := by - rw [LinearMap.mem_ker, Matrix.toLin'_apply] - exact hv_ker - rw [h_ker_bot] at hv_in_ker - simp at hv_in_ker - exact hv_ne_zero hv_in_ker - exact det_eq_zero_of_ker_ne_bot' A h_ker_ne_bot - -lemma spectralRadius_le_nnnorm_of_mem_spectrum {A : Matrix n n ℝ} {μ : ℝ} - (hμ : μ ∈ spectrum ℝ A) : ‖μ‖₊ ≤ ‖(Matrix.toLin' A).toContinuousLinearMap‖₊ := by - have h_eigenvalue : ∃ v : n → ℝ, v ≠ 0 ∧ Matrix.mulVec A v = μ • v := by - rw [spectrum.mem_iff, Matrix.isUnit_iff_isUnit_det, isUnit_iff_ne_zero] at hμ - push_neg at hμ - have : Matrix.det (algebraMap ℝ (Matrix n n ℝ) μ - A) = 0 := hμ - rw [Algebra.algebraMap_eq_smul_one, Matrix.det_eq_zero_iff_exists_nontrivial_ker] at this - obtain ⟨v, hv_ne_zero, hv_ker⟩ := this - use v - constructor - · exact hv_ne_zero - · rw [Matrix.sub_mulVec, Matrix.smul_mulVec, Matrix.one_mulVec, sub_eq_zero] at hv_ker - exact hv_ker.symm - obtain ⟨v, hv_ne_zero, hv_eigen⟩ := h_eigenvalue - have hv_norm_pos : 0 < ‖v‖ := norm_pos_iff.mpr hv_ne_zero - have : ‖μ • v‖ = ‖μ‖ * ‖v‖ := norm_smul μ v - rw [← hv_eigen, ← Matrix.toLin'_apply] at this - have h_bound : ‖(Matrix.toLin' A).toContinuousLinearMap v‖ ≤ ‖(Matrix.toLin' A).toContinuousLinearMap‖ * ‖v‖ := - ContinuousLinearMap.le_opNorm _ v - rw [LinearMap.coe_toContinuousLinearMap', this] at h_bound - exact le_of_mul_le_mul_right h_bound hv_norm_pos - -lemma spectralRadius_lt_top {A : Matrix n n ℝ} : - spectralRadius ℝ A < ⊤ := by - rw [spectralRadius] - apply iSup_lt_iff.mpr - use ‖(Matrix.toLin' A).toContinuousLinearMap‖₊ + 1 - constructor - · exact ENNReal.coe_lt_top - · intro i - apply iSup_le - intro hi - exact ENNReal.coe_le_coe.mpr (le_trans (spectralRadius_le_nnnorm_of_mem_spectrum hi) (le_add_of_nonneg_right zero_le_one)) - -lemma spectrum.nnnorm_le_nnnorm_of_mem {𝕜 A : Type*} - [NormedField 𝕜] [NormedRing A] [NormedAlgebra 𝕜 A] [CompleteSpace A] [NormOneClass A] - (a : A) {k : 𝕜} (hk : k ∈ spectrum 𝕜 a) : ‖k‖₊ ≤ ‖a‖₊ := by - have h_subset : spectrum 𝕜 a ⊆ Metric.closedBall 0 ‖a‖ := - spectrum.subset_closedBall_norm a - have hk_in_ball : k ∈ Metric.closedBall 0 ‖a‖ := h_subset hk - have h_norm_le : ‖k‖ ≤ ‖a‖ := by - rw [Metric.mem_closedBall, dist_zero_right] at hk_in_ball - exact hk_in_ball - exact h_norm_le - - - -lemma vecMul_eq_mulVec_transpose {n : Type*} [Fintype n] (A : Matrix n n ℝ) (v : n → ℝ) : - v ᵥ* A = Aᵀ *ᵥ v := by - ext j - simp [vecMul, mulVec, transpose] - rw [@dotProduct_comm] - -lemma dotProduct_le_dotProduct_of_nonneg_left' {n : Type*} [Fintype n] {u x y : n → ℝ} - (hu_nonneg : ∀ i, 0 ≤ u i) (h_le : x ≤ y) : - u ⬝ᵥ x ≤ u ⬝ᵥ y := by - rw [dotProduct, dotProduct, ← sub_nonneg, ← Finset.sum_sub_distrib] - apply Finset.sum_nonneg - intro i _ - rw [← mul_sub] - exact mul_nonneg (hu_nonneg i) (sub_nonneg.mpr (h_le i)) - -lemma eq_zero_of_nonneg_of_dotProduct_eq_zero {n : Type*} [Fintype n] {u z : n → ℝ} - (hu_pos : ∀ i, 0 < u i) (hz_nonneg : ∀ i, 0 ≤ z i) (h_dot : u ⬝ᵥ z = 0) : - z = 0 := by - have h_terms_nonneg : ∀ i, 0 ≤ u i * z i := fun i => mul_nonneg (hu_pos i).le (hz_nonneg i) - have h_terms_zero : ∀ i, u i * z i = 0 := by - rw [dotProduct, Finset.sum_eq_zero_iff_of_nonneg] at h_dot - · exact fun i => h_dot i (Finset.mem_univ _) - · exact fun i _ => h_terms_nonneg i - funext i - exact (mul_eq_zero.mp (h_terms_zero i)).resolve_left (hu_pos i).ne' - -lemma Module.End.exists_eigenvector_of_mem_spectrum {K V : Type*} - [Field K] [AddCommGroup V] [Module K V] [FiniteDimensional K V] - {f : V →ₗ[K] V} {μ : K} (h_is_eigenvalue : μ ∈ spectrum K f) : - ∃ v, v ≠ 0 ∧ f v = μ • v := by - rw [spectrum.mem_iff, LinearMap.isUnit_iff_ker_eq_bot] at h_is_eigenvalue - obtain ⟨v, hv_mem, hv_ne_zero⟩ := Submodule.exists_mem_ne_zero_of_ne_bot h_is_eigenvalue - use v, hv_ne_zero - rw [LinearMap.mem_ker, LinearMap.sub_apply, Module.algebraMap_end_apply] at hv_mem - exact (sub_eq_zero.mp hv_mem).symm - --- Core lemma: spectral radius is bounded by the operator norm -lemma spectralRadius_le_nnnorm {𝕜 A : Type*} [NontriviallyNormedField 𝕜] - [NormedField 𝕜] [NormedRing A] [NormedAlgebra 𝕜 A] [CompleteSpace A] [NormOneClass A] - (a : A) : - spectralRadius 𝕜 a ≤ ↑‖a‖₊ := by - apply iSup_le - intro μ - apply iSup_le - intro hμ - have h_nnnorm_le : ‖μ‖₊ ≤ ‖a‖₊ := spectrum.nnnorm_le_nnnorm_of_mem a hμ - exact ENNReal.coe_le_coe.mpr h_nnnorm_le - --- Specialized version for continuous linear maps -lemma spectralRadius_le_nnnorm_continuousLinearMap {E : Type*} [NormedAddCommGroup E] - [NormedSpace ℝ E] [CompleteSpace E] [NormOneClass (E →L[ℝ] E)] (T : E →L[ℝ] E) : - spectralRadius ℝ T ≤ ↑‖T‖₊ := by - exact spectralRadius_le_nnnorm T - -omit [DecidableEq n] in -/-- The spectral radii of a matrix and its transpose are equal. -/ -lemma spectralRadius_eq_spectralRadius_transpose [DecidableEq n] (A : Matrix n n ℝ) : - spectralRadius ℝ A = spectralRadius ℝ Aᵀ := by - unfold spectralRadius - rw [spectrum_eq_spectrum_transpose] - -lemma spectralRadius_le_opNorm (A : Matrix n n ℝ) : - spectralRadius ℝ (Matrix.toLin' A) ≤ ↑‖(Matrix.toLin' A).toContinuousLinearMap‖₊ := by - apply iSup_le - intro μ - apply iSup_le - intro hμ - have hμ_matrix : μ ∈ spectrum ℝ A := by - rw [spectrum_eq_spectrum_toLin'] - exact hμ - exact ENNReal.coe_le_coe.mpr (spectralRadius_le_nnnorm_of_mem_spectrum hμ_matrix) - -lemma spectralRadius_finite (A : Matrix n n ℝ) : - spectralRadius ℝ (Matrix.toLin' A) ≠ ⊤ := by - have h_le_norm : spectralRadius ℝ (Matrix.toLin' A) ≤ ↑‖(Matrix.toLin' A).toContinuousLinearMap‖₊ := - spectralRadius_le_opNorm A - have h_norm_finite : (↑‖(Matrix.toLin' A).toContinuousLinearMap‖₊ : ENNReal) ≠ ⊤ := - ENNReal.coe_ne_top - exact ne_top_of_le_ne_top h_norm_finite h_le_norm - -omit [DecidableEq n] in -lemma norm_mulVec_le_of_row_stochastic - {A : Matrix n n ℝ} (h_stochastic : ∀ i, ∑ j, A i j = 1) - (h_nonneg : ∀ i j, (0 : ℝ) ≤ A i j) : - ∀ v : n → ℝ, ‖A *ᵥ v‖ ≤ ‖v‖ := by - intro v - rw [pi_norm_le_iff_of_nonneg (norm_nonneg v)] - intro i - rw [Real.norm_eq_abs, Matrix.mulVec] - calc |∑ j, A i j * v j| - _ ≤ ∑ j, |A i j * v j| := - Finset.abs_sum_le_sum_abs (fun i_1 ↦ A i i_1 * v i_1) Finset.univ - _ = ∑ j, (A i j) * |v j| := by simp_rw [abs_mul, abs_of_nonneg (h_nonneg i _)] - _ ≤ ∑ j, A i j * ‖v‖ := Finset.sum_le_sum fun j _ => - mul_le_mul_of_nonneg_left (norm_le_pi_norm v j) (h_nonneg i j) - _ = (∑ j, A i j) * ‖v‖ := (Finset.sum_mul ..).symm - _ = ‖v‖ := by rw [h_stochastic i, one_mul] - -/-- -The spectral radius of a row-stochastic matrix with non-negative entries is at most 1. --/ -theorem spectralRadius_stochastic_le_one {A : Matrix n n ℝ} - (h_stochastic : ∀ i, ∑ j, A i j = 1) - (h_nonneg : ∀ i j, 0 ≤ A i j) : - spectralRadius ℝ (Matrix.toLin' A) ≤ 1 := by - let L := (Matrix.toLin' A).toContinuousLinearMap - have h_norm_le_one : ‖L‖ ≤ 1 := by - apply ContinuousLinearMap.opNorm_le_bound _ (zero_le_one) - intro v - dsimp - rw [one_mul] - exact norm_mulVec_le_of_row_stochastic h_stochastic h_nonneg v - have h_spectral_le_norm : spectralRadius ℝ (Matrix.toLin' A) ≤ ↑‖L‖₊ := - spectralRadius_le_opNorm A - exact le_trans h_spectral_le_norm (ENNReal.coe_le_coe.mpr h_norm_le_one) - -/-! ## Core Perron-Frobenius Theory -/ - -noncomputable def supportFinset (v : n → ℝ) : Finset n := - Finset.univ.filter (fun i => v i > 0) - -noncomputable def kernelFinset (v : n → ℝ) : Finset n := - Finset.univ.filter (fun i => v i = 0) - -omit [DecidableEq n] in -lemma disjoint_kernel_support {v : n → ℝ} : - Disjoint (supportFinset v) (kernelFinset v) := by - simp only [supportFinset, kernelFinset, Finset.disjoint_left] - intro i hi_support hi_kernel - simp only [Finset.mem_filter, Finset.mem_univ, true_and] at hi_support hi_kernel - exact (hi_support.ne hi_kernel.symm).elim - -/-- If a submodule contains a non-zero vector, then it is not the zero submodule. -/ -theorem Submodule.ne_bot_of_mem {R M : Type*} [Semiring R] [AddCommGroup M] [Module R M] - {p : Submodule R M} (v : M) (hv_mem : v ∈ p) (hv_ne_zero : v ≠ 0) : p ≠ ⊥ := by - intro h_bot - have h_zero : v = 0 := by - rw [h_bot] at hv_mem - exact hv_mem - exact hv_ne_zero h_zero - -omit [DecidableEq n] in -lemma support_nonempty_of_ne_zero {v : n → ℝ} - (hv_nonneg : ∀ i, 0 ≤ v i) (hv_ne_zero : v ≠ 0) : - (supportFinset v).Nonempty := by - by_contra h - have h_all_nonpos : ∀ i, v i ≤ 0 := by - intro i - by_contra hi_pos - push_neg at hi_pos - have hi_in_support : i ∈ supportFinset v := by - simp [supportFinset, Finset.mem_filter] - exact hi_pos - exact h ⟨i, hi_in_support⟩ - have : v = 0 := funext fun i => - le_antisymm (h_all_nonpos i) (hv_nonneg i) - exact hv_ne_zero this - -lemma spectrum.of_eigenspace_ne_bot - {K V : Type*} [Field K] [AddCommGroup V] [Module K V] [FiniteDimensional K V] - {f : V →ₗ[K] V} {μ : K} - (h : Module.End.eigenspace f μ ≠ ⊥) : - μ ∈ spectrum K f := by - rw [← Module.End.hasEigenvalue_iff_mem_spectrum] - exact h - -/-- If a finite sum of non-negative terms is positive, at least one term must be positive. -/ -lemma exists_pos_of_sum_pos {ι : Type*} [Fintype ι] {f : ι → ℝ} - (h_nonneg : ∀ i, 0 ≤ f i) (h_sum_pos : 0 < ∑ i, f i) : - ∃ i, 0 < f i := by - by_contra h_not_exists - push_neg at h_not_exists - have h_all_zero : ∀ i, f i = 0 := by - intro i - exact le_antisymm (h_not_exists i) (h_nonneg i) - have h_sum_zero : ∑ i, f i = 0 := by - exact Finset.sum_eq_zero (fun i _ => h_all_zero i) - exact h_sum_pos.ne' h_sum_zero - -/-- For a non-negative `a`, `a * b` is positive iff both `a` and `b` are positive. -/ -lemma mul_pos_iff_of_nonneg_left {a b : ℝ} (ha_nonneg : 0 ≤ a) : - 0 < a * b ↔ 0 < a ∧ 0 < b := by - refine' ⟨fun h_mul_pos => _, fun ⟨ha_pos, hb_pos⟩ => mul_pos ha_pos hb_pos⟩ - have ha_pos : 0 < a := by - refine' lt_of_le_of_ne ha_nonneg fun ha_zero => _ - rw [ha_zero] at h_mul_pos - subst ha_zero - simp_all only [le_refl, zero_mul, lt_self_iff_false] - simp_all only [mul_pos_iff_of_pos_left, and_self] - -/-- If a scalar `μ` is an eigenvalue of a matrix `A`, then it is a root of its -characteristic polynomial. -/ -lemma isRoot_of_hasEigenvalue {A : Matrix n n ℝ} {μ : ℝ} - (h_eig : Module.End.HasEigenvalue (toLin' A) μ) : - (charpoly A).IsRoot μ := by - rw [← mem_spectrum_iff_isRoot_charpoly, spectrum_eq_spectrum_toLin'] - exact Module.End.hasEigenvalue_iff_mem_spectrum.mp h_eig - -/-- The spectrum of a matrix `A` is equal to the spectrum of its corresponding linear map -`Matrix.toLin' A`. -/ -theorem spectrum.Matrix_toLin'_eq_spectrum {R n : Type*} [CommRing R] [Fintype n] [DecidableEq n] (A : Matrix n n R) : - spectrum R (Matrix.toLin' A) = spectrum R A := by - exact AlgEquiv.spectrum_eq (Matrix.toLinAlgEquiv (Pi.basisFun R n)) A -end Matrix - -/-- If a linear map `f` has an eigenvector `v` for an eigenvalue `μ`, then `μ` is in the spectrum of `f`. -/ -lemma Module.End.mem_spectrum_of_hasEigenvector {K V : Type*} [Field K] [AddCommGroup V] [Module K V] [FiniteDimensional K V] - {f : V →ₗ[K] V} {μ : K} {v : V} (h : HasEigenvector f μ v) : - μ ∈ spectrum K f := by - rw [← Module.End.hasEigenvalue_iff_mem_spectrum] - exact Module.End.hasEigenvalue_of_hasEigenvector h diff --git a/lean/MCMC/PF/Topology/Compactness/ExtremeValueUSC.lean b/lean/MCMC/PF/Topology/Compactness/ExtremeValueUSC.lean deleted file mode 100644 index 2c8f395..0000000 --- a/lean/MCMC/PF/Topology/Compactness/ExtremeValueUSC.lean +++ /dev/null @@ -1,204 +0,0 @@ -import Mathlib.Algebra.Order.Ring.Star -import Mathlib.Analysis.Normed.Order.Lattice -import Mathlib.Analysis.RCLike.Basic -import Mathlib.Analysis.SpecificLimits.Basic - -open Set Filter Topology - -namespace IsCompact - -variable {α : Type*} [TopologicalSpace α] --[Nonempty α] -variable {f : α → ℝ} {K : Set α} - -section GeneralProof --- This section provides proofs that do not rely on first-countability. - -lemma upperSemicontinuousOn_iff_upperSemicontinuous {s : Set α} : - UpperSemicontinuousOn f s ↔ UpperSemicontinuous (s.restrict f) := by - constructor - · intro h x c hc - specialize h x.val x.property c hc - rw [nhds_subtype_eq_comap_nhdsWithin s x] - exact h.comap Subtype.val - · intro h x hx c hc - specialize h ⟨x, hx⟩ c hc - rw [nhdsWithin_eq_map_subtype_coe] - have h_eventually : ∀ᶠ (x' : ↑s) in 𝓝 ⟨x, hx⟩, (s.restrict f) x' < c := by exact h - exact h - -/-- -**Boundedness Theorem (General Version)**: An upper semicontinuous function on a compact set -is bounded above. - -This proof uses the open cover definition of compactness and does not require the space -to be first-countable. --/ -theorem bddAbove_image_of_upperSemicontinuousOn (hK : IsCompact K) - (hf : UpperSemicontinuousOn f K) : BddAbove (f '' K) := by - -- We proceed by contradiction. Assume the image `f '' K` is not bounded above. - by_contra h_unbdd - -- We work on the space `K` with the subspace topology. - -- The hypothesis `IsCompact K` is equivalent to `K` being a compact space. - haveI : CompactSpace K := isCompact_iff_compactSpace.mp hK - -- Define a family of sets `U n = {x : K | f(x) < n}`. - let U : ℕ → Set K := fun n => {x : K | f x < n} - -- By upper semicontinuity, each `U n` is an open set in the subspace topology of K. - have hU_open : ∀ n, IsOpen (U n) := by - intro n - have hf_restrict : UpperSemicontinuous (K.restrict f) := - (upperSemicontinuousOn_iff_upperSemicontinuous).mp hf - rw [upperSemicontinuous_iff_isOpen_preimage] at hf_restrict - convert hf_restrict n - -- If `f` is unbounded on `K`, then the collection `{U n}` covers `K` (i.e., `univ` in `Set K`). - have hU_covers_univ : (univ : Set K) ⊆ ⋃ n, U n := by - intro x _ - obtain ⟨n, hn⟩ := exists_nat_gt (f x) - simp only [mem_iUnion] - exact ⟨n, hn⟩ - -- Since K is a compact space, its `univ` set is compact. The open cover `{U n}` - -- must have a finite subcover. - rcases isCompact_univ.elim_finite_subcover U hU_open hU_covers_univ with ⟨s, hs_cover⟩ - -- Let N be the maximum of the natural numbers in the finite index set `s`. - -- As `s` is a finite nonempty set of natural numbers, it has a maximum. - have s_nonempty : s.Nonempty := by - rcases K.eq_empty_or_nonempty with rfl | hK_nonempty - · exfalso; simp at h_unbdd - · by_contra hs_empty - rw [Finset.not_nonempty_iff_eq_empty.mp hs_empty] at hs_cover - aesop - let N := s.sup id - -- The union of the finite subcover is a subset of U (N + 1). - have h_union_sub_UN : (⋃ i ∈ s, U i) ⊆ U (N + 1) := by - intro x hx_union - simp only [mem_iUnion] at hx_union ⊢ - rcases hx_union with ⟨i, hi_s, hf_lt_i⟩ - -- If f(x) < i and i ≤ N, then f(x) < N + 1. - have hi_le_N : i ≤ N := Finset.le_sup hi_s (f := id) - have hi_le_N_real : (i : ℝ) ≤ (N : ℝ) := by norm_cast - exact hf_lt_i.trans (lt_of_le_of_lt hi_le_N_real (by aesop)) - -- This implies that for all `x` in `K`, `f x < N + 1`. - have h_all_lt : ∀ (x : K), f x < N + 1 := by - intro x - -- `x` belongs to the union of the finite sub-cover - have x_in_union : x ∈ ⋃ i ∈ s, U i := hs_cover (mem_univ x) - -- therefore it belongs to `U (N + 1)` - have hx : x ∈ U (N + 1) := h_union_sub_UN x_in_union - simpa [U] using hx - -- This means that f is bounded above by N+1 on K. - have h_bdd_final : BddAbove (f '' K) := by - use (N : ℝ) + 1 - intro y hy_img - rcases hy_img with ⟨x, hx_K, rfl⟩ - exact (h_all_lt ⟨x, hx_K⟩).le - exact h_unbdd h_bdd_final - -lemma tendsto_const_sub_inv_add_one_atTop (c : ℝ) : - Tendsto (fun n : ℕ => c - 1 / (n + 1)) atTop (𝓝 c) := by - have h_inv : Tendsto (fun n : ℕ => 1 / ((n : ℝ) + 1)) atTop (𝓝 0) := by - have h_denom : Tendsto (fun n => (n : ℝ) + 1) atTop atTop := - tendsto_atTop_add_const_right atTop 1 fun ⦃U⦄ a => a - exact tendsto_one_div_add_atTop_nhds_zero_nat - simpa using tendsto_const_nhds.sub h_inv - -/-- -**Extreme Value Theorem (General Version)**: An upper semicontinuous function on a non-empty -compact set attains its supremum. --/ -theorem exists_isMaxOn_of_upperSemicontinuousOn (hK : IsCompact K) (hK_nonempty : K.Nonempty) - (hf : UpperSemicontinuousOn f K) : ∃ x₀ ∈ K, IsMaxOn f K x₀ := by - -- The function is bounded above on K. - have h_bdd_above : BddAbove (f '' K) := bddAbove_image_of_upperSemicontinuousOn hK hf - let s := sSup (f '' K) - -- We work in the compact space `K`. - haveI : CompactSpace K := isCompact_iff_compactSpace.mp hK - -- Consider the sets Cₙ = {x ∈ K | s - 1/(n+1) ≤ f(x)}. - let C : ℕ → Set K := fun n => {x : K | s - 1 / (n + 1 : ℝ) ≤ f x} - -- These sets are closed in the compact space K. - have hC_closed : ∀ n, IsClosed (C n) := by - intro n - have hf_restrict : UpperSemicontinuous (K.restrict f) := - (upperSemicontinuousOn_iff_upperSemicontinuous).mp hf - have : C n = K.restrict f ⁻¹' (Ici (s - 1 / (↑n + 1))) := by - ext x; simp_all only [one_div, tsub_le_iff_right, mem_setOf_eq, mem_preimage, restrict_apply, mem_Ici, C, s] - rw [this] - exact UpperSemicontinuous.isClosed_preimage hf_restrict (s - 1 / (↑n + 1)) - have hC_compact : ∀ n, IsCompact (C n) := fun n => (hC_closed n).isCompact - -- The family `C n` is decreasing, hence it is directed by `⊇`. - have hC_directed : Directed (· ⊇ ·) C := by - intro n m - refine ⟨max n m, ?_, ?_⟩ - · -- `C (max n m) ⊆ C n` - intro x hx - -- `hx` is the inequality for the bigger threshold - have hx_le : s - 1 / ((max n m : ℝ) + 1) ≤ f x := by - simpa [C] using hx - have h_thresh : s - 1 / ((n : ℝ) + 1) ≤ s - 1 / ((max n m : ℝ) + 1) := by - have h_inv : (1 / ((max n m : ℝ) + 1)) ≤ 1 / ((n : ℝ) + 1) := by - gcongr; norm_cast; exact le_max_left n m - simpa using (sub_le_sub_left h_inv s) - have : s - 1 / ((n : ℝ) + 1) ≤ f x := le_trans h_thresh hx_le - simpa [C] using this - · -- `C (max n m) ⊆ C m` - intro x hx - have hx_le : s - 1 / ((max n m : ℝ) + 1) ≤ f x := by - simpa [C] using hx - have h_thresh : s - 1 / ((m : ℝ) + 1) ≤ s - 1 / ((max n m : ℝ) + 1) := by - have h_inv : (1 / ((max n m : ℝ) + 1)) ≤ 1 / ((m : ℝ) + 1) := by - gcongr; norm_cast; exact le_max_right n m - simpa using (sub_le_sub_left h_inv s) - have : s - 1 / ((m : ℝ) + 1) ≤ f x := le_trans h_thresh hx_le - simpa [C] using this - -- Non-emptiness of each `C n`. - have hC_nonempty : ∀ n, (C n).Nonempty := by - intro n - have h_nonempty_image : (f '' K).Nonempty := hK_nonempty.image f - have h_lt_s : s - 1 / (n + 1 : ℝ) < s := - sub_lt_self s (one_div_pos.mpr (by exact_mod_cast Nat.succ_pos n)) - obtain ⟨y, hy_mem, hy_lt⟩ := exists_lt_of_lt_csSup h_nonempty_image h_lt_s - rcases hy_mem with ⟨x, hx_K, rfl⟩ - use ⟨x, hx_K⟩ - exact le_of_lt hy_lt - -- Cantor's intersection theorem gives a point in ⋂ `C n`. - have h_nonempty_inter : (⋂ n, C n).Nonempty := - IsCompact.nonempty_iInter_of_directed_nonempty_isCompact_isClosed - (t := C) hC_directed hC_nonempty hC_compact hC_closed - -- Any point in this intersection is a point where the maximum is attained. - rcases h_nonempty_inter with ⟨x₀, hx₀_inter⟩ - use x₀.val, x₀.prop - -- We show `f x₀ = s`. Since `s` is the supremum, this proves `x₀` is a maximum. - have h_fx_eq_s : f x₀ = s := by - simp only [mem_iInter] at hx₀_inter - have h_le : f x₀ ≤ s := le_csSup h_bdd_above (mem_image_of_mem f x₀.prop) - have h_ge : s ≤ f x₀ := - le_of_tendsto (tendsto_const_sub_inv_add_one_atTop s) (Filter.Eventually.of_forall (fun n => hx₀_inter n)) - exact le_antisymm h_le h_ge - -- This implies `IsMaxOn`. - intro y hy - rw [h_fx_eq_s] - exact le_csSup h_bdd_above (mem_image_of_mem f hy) - --- shorter aliases for latex formatting - -/- Short aliases for long names (backwards‑compatible) -/ --- upperSemicontinuousOn_iff_upperSemicontinuous -lemma usco_on_iff_usco {s : Set α} : - UpperSemicontinuousOn f s ↔ UpperSemicontinuous (s.restrict f) := - upperSemicontinuousOn_iff_upperSemicontinuous - --- bddAbove_image_of_upperSemicontinuousOn -theorem bdd_above_image_usco_on (hK : IsCompact K) - (hf : UpperSemicontinuousOn f K) : BddAbove (f '' K) := - bddAbove_image_of_upperSemicontinuousOn hK hf - --- tendsto_const_sub_inv_add_one_atTop -lemma tendsto_sub_inv_atTop (c : ℝ) : - Tendsto (fun n : ℕ => c - 1 / (n + 1)) atTop (𝓝 c) := - tendsto_const_sub_inv_add_one_atTop c - --- exists_isMaxOn_of_upperSemicontinuousOn -theorem exists_max_on_usco (hK : IsCompact K) (hK_nonempty : K.Nonempty) - (hf : UpperSemicontinuousOn f K) : ∃ x₀ ∈ K, IsMaxOn f K x₀ := - exists_isMaxOn_of_upperSemicontinuousOn hK hK_nonempty hf - -end GeneralProof -end IsCompact diff --git a/lean/MCMC/PF/aux.lean b/lean/MCMC/PF/aux.lean deleted file mode 100644 index ed1ebca..0000000 --- a/lean/MCMC/PF/aux.lean +++ /dev/null @@ -1,850 +0,0 @@ -import MCMC.PF.LinearAlgebra.Matrix.Spectrum -import Mathlib.Algebra.Order.Ring.Star -import Mathlib.Data.Int.Star - -open Filter Set Finset Matrix Topology Convex - -/- Standard simplex definition -def stdSimplex (𝕜 ι : Type*) [Semiring 𝕜] [PartialOrder 𝕜] [Fintype ι] : Set (ι → 𝕜) := - {x | (∀ i, 0 ≤ x i) ∧ ∑ i, x i = 1} - --- Upper semicontinuous function definition -def UpperSemicontinuousOn {α β : Type*} [TopologicalSpace α] [Preorder β] - (f : α → β) (s : Set α) : Prop := - ∀ x ∈ s, ∀ y, f x < y → ∃ U ∈ 𝓝[s] x, ∀ z ∈ U, f z < y - --- Lower semicontinuous function definition -def LowerSemicontinuousOn {α β : Type*} [TopologicalSpace α] [Preorder β] - (f : α → β) (s : Set α) : Prop := - ∀ x ∈ s, ∀ y, y < f x → ∃ U ∈ 𝓝[s] x, ∀ z ∈ U, y < f z - --- Cluster point definition -def ClusterPt {X : Type*} [TopologicalSpace X] (x : X) (F : Filter X) : Prop := - (𝓝 x ⊓ F).NeBot - --- Ultrafilter definition -structure Ultrafilter (α : Type*) extends Filter α where - isUltra : ∀ s, s ∈ toFilter ∨ sᶜ ∈ toFilter-/ - -/-! -## Key Theorems for Compactness & Ultrafilters --/ - -/- Ultrafilter existence theorem -theorem Ultrafilter.exists_le {α : Type*} {F : Filter α} (h : F.NeBot) : - ∃ U : Ultrafilter α, (U : Filter α) ≤ F := by - exact Ultrafilter.exists_le F - --- Compactness characterization via ultrafilters -theorem isCompact_iff_ultrafilter_le_nhds {X : Type*} [TopologicalSpace X] {s : Set X} : - IsCompact s ↔ ∀ (f : Ultrafilter X), s ∈ f → ∃ x ∈ s, (f : Filter X) ≤ 𝓝 x := by - exact isCompact_iff_ultrafilter_le_nhds - --- Cluster point existence in compact sets -theorem IsCompact.exists_clusterPt {X : Type*} [TopologicalSpace X] {s : Set X} - (hs : IsCompact s) {f : Filter X} (hf : f.NeBot) (hfs : f ≤ 𝓟 s) : - ∃ x ∈ s, ClusterPt x f := by - exact hs.exists_clusterPt hfs-/ - --- Ultrafilter convergence from cluster point -theorem ClusterPt.exists_ultrafilter {X : Type*} [TopologicalSpace X] {x : X} {f : Filter X} - (h : ClusterPt x f) : ∃ U : Ultrafilter X, (U : Filter X) ≤ f ∧ (U : Filter X) ≤ 𝓝 x := by - exact clusterPt_iff_ultrafilter.mp h - -/-! -## Semicontinuity Theorems --/ -/-- -If an ultrafilter `G` on `X` converges to `x` within `s`, and `f` is continuous on `s`, -then `f` maps `G` to the neighborhood filter of `f x`. -This is a version with lower and upper semicontinuity. --/ -lemma tendsto_of_lower_upper_semicontinuous_ultrafilter - {X Y : Type*} [TopologicalSpace X] [TopologicalSpace Y] [LinearOrder Y] [OrderTopology Y] - {f : X → Y} {s : Set X} (h_upper : UpperSemicontinuousOn f s) - (h_lower : LowerSemicontinuousOn f s) {x : X} (hx : x ∈ s) {G : Ultrafilter X} - (hG : (G : Filter X) ≤ 𝓝[s] x) : - Tendsto f (G : Filter X) (𝓝 (f x)) := by - have h_cont : ContinuousWithinAt f s x := - continuousWithinAt_iff_lower_upperSemicontinuousWithinAt.mpr ⟨h_lower x hx, h_upper x hx⟩ - exact h_cont.tendsto.comp (tendsto_id.mono_left hG) - -/-- -If an ultrafilter `G` on `X` converges to `x` within `s`, and `f` is upper semicontinuous on `s`, -then for any `y' > f x`, `f` eventually maps elements of `G` to values less than `y'`. --/ -lemma upperSemicontinuousOn_eventually_lt_ultrafilter - {X Y : Type*} [TopologicalSpace X] [LinearOrder Y] {f : X → Y} {s : Set X} - (hf : UpperSemicontinuousOn f s) {x : X} (hx : x ∈ s) {G : Ultrafilter X} - (hG : (G : Filter X) ≤ 𝓝[s] x) {y' : Y} (hy' : f x < y') : - ∀ᶠ (z : X) in (G : Filter X), f z < y' := - hG (hf x hx y' hy') - -/-- -If an ultrafilter `G` on `X` converges to `x` within `s`, and `f` is lower semicontinuous on `s`, -then for any `y' < f x`, `f` eventually maps elements of `G` to values greater than `y'`. --/ -lemma lowerSemicontinuousOn_eventually_gt_ultrafilter - {X Y : Type*} [TopologicalSpace X] [LinearOrder Y] {f : X → Y} {s : Set X} - (hf : LowerSemicontinuousOn f s) {x : X} (hx : x ∈ s) {G : Ultrafilter X} - (hG : (G : Filter X) ≤ 𝓝[s] x) {y' : Y} (hy' : y' < f x) : - ∀ᶠ (z : X) in (G : Filter X), y' < f z := - hG (hf x hx y' hy') - -/-! -## Standard Simplex Properties --/ - -/- Standard simplex is compact -theorem isCompact_stdSimplex (ι : Type*) [Fintype ι] : IsCompact (stdSimplex ℝ ι) := by - exact _root_.isCompact_stdSimplex ι - --- Standard simplex is convex -theorem convex_stdSimplex (𝕜 ι : Type*) [OrderedRing 𝕜] [Fintype ι] : - Convex 𝕜 (stdSimplex 𝕜 ι) := by - exact _root_.convex_stdSimplex 𝕜 ι-/ - --- Standard simplex is nonempty when ι is nonempty -theorem stdSimplex_nonempty {ι : Type*} [Fintype ι] [Nonempty ι] : - (stdSimplex ℝ ι).Nonempty := by - exact ⟨(Fintype.card ι : ℝ)⁻¹ • 1, by simp [stdSimplex, Finset.sum_const, nsmul_eq_mul]⟩ - -/-! -## Supremum & Infimum Theorems --/ - --- Compact sets in ℝ attain their supremum -theorem IsCompact.exists_max {s : Set ℝ} (hs : IsCompact s) (hne : s.Nonempty) : - ∃ x ∈ s, ∀ y ∈ s, y ≤ x := by - let sup_s := sSup s - have h_mem : sup_s ∈ s := sSup_mem hs hne - use sup_s, h_mem - intro y hy - exact le_csSup (hs.bddAbove) hy - --- Function attaining maximum equals supremum of image -theorem isMaxOn_eq_sSup {X : Type*} [TopologicalSpace X] - {f : X → ℝ} {s : Set X} {v : X} - (hv : v ∈ s) (hmax : ∀ z ∈ s, f z ≤ f v) : - sSup (f '' s) = f v := by - apply le_antisymm - · apply csSup_le - · use f v - refine ⟨v, hv, rfl⟩ - · intro y hy - rcases hy with ⟨x, hx, rfl⟩ - exact hmax x hx - · apply le_csSup - · exact ⟨f v, fun a ha => by - rcases ha with ⟨x, hx, rfl⟩ - exact hmax x hx⟩ - · exact Set.mem_image_of_mem f hv - -/-! -## Filter & Ultrafilter Operations --/ - -/- Ultrafilter mapping -def Ultrafilter.map {α β : Type*} (f : α → β) (u : Ultrafilter α) : Ultrafilter β := - ⟨Filter.map f u, by - intro s - have := u.isUltra (f ⁻¹' s) - cases this with - | inl h => left; exact Filter.mem_map.mpr h - | inr h => right; exact Filter.mem_map.mpr h⟩ - --- Ultrafilter equality from inclusion -theorem Ultrafilter.eq_of_le {α : Type*} {u v : Ultrafilter α} (h : (u : Filter α) ≤ v) : - u = v := by - exact Ultrafilter.eq_of_le h - --- Tendsto characterization for ultrafilters -theorem tendsto_map'_iff {α β : Type*} {f : α → β} {u : Ultrafilter α} {l : Filter β} : - Tendsto f (u : Filter α) l ↔ (Ultrafilter.map f u : Filter β) ≤ l := by - exact tendsto_map'_iff-/ - -/-! -## Helper Lemmas for Continuity --/ - --- Eventually to open set conversion -theorem eventually_to_open {α : Type*} [TopologicalSpace α] {p : α → Prop} {a : α} - (h : ∀ᶠ x in 𝓝 a, p x) : - ∃ (U : Set α), IsOpen U ∧ a ∈ U ∧ ∀ x ∈ U, p x := by - rcases mem_nhds_iff.mp h with ⟨U, hU_open, haU, hU⟩ - simp_all only - apply Exists.intro - · apply And.intro - on_goal 2 => apply And.intro - on_goal 2 => {exact hU - } - · simp_all only - · intro x a_1 - apply hU_open - simp_all only - --- Continuous infimum over finset -theorem continuousOn_finset_inf' {α β : Type*} [TopologicalSpace α] [LinearOrder β] - [TopologicalSpace β] [OrderTopology β] {ι : Type*} [Fintype ι] - {s : Finset ι} {U : Set α} (hs : s.Nonempty) {f : ι → α → β} - (hf : ∀ i ∈ s, ContinuousOn (f i) U) : - ContinuousOn (fun x => s.inf' hs (fun i => f i x)) U := - ContinuousOn.finset_inf'_apply hs hf - --- Infimum monotonicity for subsets -theorem finset_inf'_mono_subset {α β : Type*} [LinearOrder β] {s t : Finset α} (h : s ⊆ t) - {f : α → β} {hs : s.Nonempty} {ht : t.Nonempty} : - t.inf' ht f ≤ s.inf' hs f := by - exact inf'_mono f h hs - -/-! -## Matrix & Vector Operations --/ - --- Matrix-vector multiplication component -theorem matrix_mulVec_component {n : Type*} [Fintype n] [DecidableEq n] - (A : Matrix n n ℝ) (v : n → ℝ) (j : n) : - (A *ᵥ v) j = ∑ i, A j i * v i := by - simp [Matrix.mulVec]; rfl - --- Non-negative matrix preserves non-negative vectors -theorem mulVec_nonneg {n : Type*} [Fintype n] {A : Matrix n n ℝ} (hA : ∀ i j, 0 ≤ A i j) - {x : n → ℝ} (hx : ∀ i, 0 ≤ x i) : ∀ i, 0 ≤ (A *ᵥ x) i := by - intro i - simp only [Matrix.mulVec, dotProduct] - exact Finset.sum_nonneg fun j _ => mul_nonneg (hA i j) (hx j) - --- Positive matrix with positive vector gives positive result -theorem positive_mul_vec_pos {n : Type*} [Fintype n] [Nonempty n] {A : Matrix n n ℝ} (hA_pos : ∀ i j, 0 < A i j) - {x : n → ℝ} (hx_nonneg : ∀ i, 0 ≤ x i) (hx_ne_zero : x ≠ 0) : - ∀ i, 0 < (A *ᵥ x) i := by - intro i - simp only [Matrix.mulVec, dotProduct] - apply Finset.sum_pos' - · intro j _ - exact mul_nonneg (le_of_lt (hA_pos i j)) (hx_nonneg j) - · have : ∃ k, 0 < x k := by - by_contra h_all_nonpos - push_neg at h_all_nonpos - have h_zero : x = 0 := funext (fun j => le_antisymm (h_all_nonpos j) (hx_nonneg j)) - exact hx_ne_zero h_zero - rcases this with ⟨k, hk_pos⟩ - refine ⟨k, ?_, ?_⟩ - · simp - · exact mul_pos (hA_pos i k) hk_pos - -/-! -## Utility Lemmas --/ - --- Existence of positive element in sum of non-negative elements -theorem exists_pos_of_sum_one_of_nonneg {n : Type*} [Fintype n] [Nonempty n] {x : n → ℝ} - (hsum : ∑ i, x i = 1) (hnonneg : ∀ i, 0 ≤ x i) : ∃ j, 0 < x j := by - by_contra h - push_neg at h - have h_all_zero : ∀ i, x i = 0 := by - intro i - exact le_antisymm (h i) (hnonneg i) - have h_sum_zero : ∑ i, x i = 0 := by - simp only [h_all_zero, Finset.sum_const_zero] - have : 1 = 0 := by linarith - exact absurd this (by norm_num) - --- Existence of non-zero element in non-zero vector -theorem exists_ne_zero_of_ne_zero {n : Type*} [Fintype n] [Nonempty n] {x : n → ℝ} (hx : x ≠ 0) : ∃ j, x j ≠ 0 := by - by_contra h - push_neg at h - have h_all_zero : ∀ i, x i = 0 := h - have x_is_zero : x = 0 := by - ext i - exact h_all_zero i - exact hx x_is_zero - --- Matrix power multiplication -theorem pow_mulVec_succ {n : Type*} [Fintype n] [Nonempty n] [DecidableEq n] {A : Matrix n n ℝ} (k : ℕ) (x : n → ℝ) : - (A^(k+1)).mulVec x = A.mulVec ((A^k).mulVec x) := by - simp only [mulVec_mulVec] - rw [pow_succ'] - - -/-! -## Finset Operations --/ - --- Infimum over finite type equals finset infimum -theorem iInf_apply_eq_finset_inf'_apply_fun {α β γ : Type*} [Fintype α] [Nonempty α] - [ConditionallyCompleteLinearOrder γ] (f : α → β → γ) : - (fun x => ⨅ i, f i x) = (fun x => (Finset.univ : Finset α).inf' Finset.univ_nonempty (fun i => f i x)) := by - ext x - have h1 : ⨅ i, f i x = ⨅ i ∈ Set.univ, f i x := by simp only [mem_univ, ciInf_unique] - have h2 : ⨅ i ∈ Set.univ, f i x = ⨅ i ∈ (Finset.univ : Finset α), f i x := by - congr - ext i - simp only [mem_univ, ciInf_unique, Finset.mem_univ] - have h3 : ⨅ i ∈ (Finset.univ : Finset α), f i x = - (Finset.univ : Finset α).inf' Finset.univ_nonempty (fun i => f i x) := by - rw [Finset.inf'_eq_csInf_image] - simp only [ciInf_unique, Finset.mem_univ, Finset.coe_univ, image_univ] - rfl - rw [h1, h2, h3] - --- Infimum over finite type equals conditional infimum -theorem iInf_eq_ciInf {α β : Type*} [Fintype α] [Nonempty α] [ConditionallyCompleteLinearOrder β] - (f : α → β) : (⨅ i, f i) = ⨅ i ∈ (Set.univ : Set α), f i := by - apply eq_of_forall_le_iff - intro b - simp only [mem_univ, ciInf_unique] - -/-! -## Order & Field Properties --/ - --- Multiplication cancellation for positive elements -theorem mul_div_cancel_pos_right {K : Type*} [Field K] [LinearOrder K] [IsStrictOrderedRing K] - {a b c : K} (h : a * b = c) (hb : 0 < b) : c / b = a := by - rw [← h] - exact mul_div_cancel_right₀ a hb.ne' - --- Non-positive times positive is non-positive -theorem mul_nonpos_of_nonpos_of_pos {α : Type*} [Ring α] [LinearOrder α] [IsStrictOrderedRing α] - {a b : α} (ha : a ≤ 0) (hb : 0 < b) : a * b ≤ 0 := by - rcases le_iff_eq_or_lt.mp ha with (rfl | h) - · rw [zero_mul] - · exact (mul_neg_of_neg_of_pos h hb).le - --- Continuous infimum over finite index -theorem continuousOn_iInf {α β : Type*} [Fintype α] [Nonempty α] [TopologicalSpace β] - {s : Set β} {f : α → β → ℝ} (hf : ∀ i, ContinuousOn (f i) s) : - ContinuousOn (fun x => ⨅ i, f i x) s := by - classical - let g : β → ℝ := fun x => (Finset.univ : Finset α).inf' Finset.univ_nonempty (fun i => f i x) - have hg : ContinuousOn g s := ContinuousOn.finset_inf'_apply Finset.univ_nonempty fun i _ => hf i - have h_eq : (fun x => ⨅ i, f i x) = g := by - dsimp [g] - exact iInf_apply_eq_finset_inf'_apply_fun f - rwa [h_eq] - - -namespace Fintype - -lemma card_gt_one_of_nonempty_ne {α : Type*} [Fintype α] [DecidableEq α] [Nonempty α] : - 1 < Fintype.card α ↔ ∃ (i j : α), i ≠ j := by - constructor - · intro h - obtain ⟨i⟩ : Nonempty α := ‹Nonempty α› - have h_card_ne_one : Fintype.card α ≠ 1 := ne_of_gt h - have : ∃ j, j ≠ i := by - by_contra h_all_eq - push_neg at h_all_eq - have : ∀ x : α, x = i := h_all_eq - have h_card_eq_one : Fintype.card α = 1 := by - rw [Fintype.card_eq_one_iff] - exact ⟨i, this⟩ - exact h_card_ne_one h_card_eq_one - obtain ⟨j, hj⟩ := this - exact ⟨i, j, hj.symm⟩ - · intro ⟨i, j, hij⟩ - have : Fintype.card α ≥ 2 := by - rw [← Finset.card_univ] - have : ({i, j} : Finset α) ⊆ Finset.univ := by simp - have : Finset.card ({i, j} : Finset α) ≤ Finset.card Finset.univ := Finset.card_le_card this - have : Finset.card ({i, j} : Finset α) = 2 := by simp [hij] - linarith - linarith - -end Fintype - -/-! -## Additional Helper Theorems --/ - --- Sum of non-negative terms is positive if at least one term is positive -theorem sum_pos_of_mem {α : Type*} {s : Finset α} {f : α → ℝ} - [DecidableEq α] (h_nonneg : ∀ a ∈ s, 0 ≤ f a) (a : α) (ha_mem : a ∈ s) (ha_pos : 0 < f a) : - 0 < ∑ x ∈ s, f x := by - have h_sum_split : ∑ x ∈ s, f x = f a + ∑ x ∈ s.erase a, f x := - Eq.symm (add_sum_erase s f ha_mem) - have h_erase_nonneg : 0 ≤ ∑ x ∈ s.erase a, f x := - Finset.sum_nonneg (λ x hx => h_nonneg x (Finset.mem_of_mem_erase hx)) - rw [h_sum_split] - exact add_pos_of_pos_of_nonneg ha_pos h_erase_nonneg - --- Existence of positive element in positive sum -theorem exists_mem_of_sum_pos {α : Type*} {s : Finset α} {f : α → ℝ} - (h_pos : 0 < ∑ a ∈ s, f a) (h_nonneg : ∀ a ∈ s, 0 ≤ f a) : - ∃ a ∈ s, 0 < f a := by - by_contra h; push_neg at h - have h_zero : ∀ a ∈ s, f a = 0 := fun a ha => le_antisymm (h a ha) (h_nonneg a ha) - have h_sum_zero : ∑ a ∈ s, f a = 0 := by rw [sum_eq_zero_iff_of_nonneg h_nonneg]; exact h_zero - linarith - --- Multiplication positivity characterization -theorem mul_pos_iff_of_nonneg {a b : ℝ} (ha : 0 ≤ a) (hb : 0 ≤ b) : - 0 < a * b ↔ 0 < a ∧ 0 < b := by - constructor - · intro h_mul_pos - refine ⟨lt_of_le_of_ne ha ?_, lt_of_le_of_ne hb ?_⟩ - · rintro rfl; simp_all only [le_refl, zero_mul, lt_self_iff_false] - · rintro rfl; simp_all only [le_refl, mul_zero, lt_self_iff_false] - · rintro ⟨ha_pos, hb_pos⟩; exact mul_pos ha_pos hb_pos - -/-- The infimum over a non-empty finset is equal to the infimum over the corresponding subtype. -/ -lemma Finset.inf'_eq_ciInf {α β} [ConditionallyCompleteLinearOrder β] {s : Finset α} - (h : s.Nonempty) (f : α → β) : - s.inf' h f = ⨅ i : s, f i := by - have : Nonempty s := Finset.Nonempty.to_subtype h - rw [Finset.inf'_eq_csInf_image] - congr - ext x - simp [Set.mem_image, Set.mem_range] - -/-- The standard simplex is a closed set. -/ -lemma isClosed_stdSimplex' {n : Type*} [Fintype n] : IsClosed (stdSimplex ℝ n) := by - have h₁ : IsClosed (⋂ i, {x : n → ℝ | 0 ≤ x i}) := - isClosed_iInter (fun i ↦ isClosed_le continuous_const (continuous_apply i)) - have h_set_eq : {x : n → ℝ | ∀ i, 0 ≤ x i} = ⋂ i, {x | 0 ≤ x i} := by ext; simp - rw [← h_set_eq] at h₁ - have h₂ : IsClosed {x : n → ℝ | ∑ i, x i = 1} := - isClosed_eq (continuous_finset_sum _ (fun i _ ↦ continuous_apply i)) continuous_const - exact IsClosed.inter h₁ h₂ - -lemma abs_le_of_le_of_neg_le {x y : ℝ} (h_le : x ≤ y) (h_neg_le : -x ≤ y) : |x| ≤ y := by - rw [abs_le] - constructor - · linarith - · exact h_le - -/-- A sum over a finset can be split into the value at a point `a` -and the sum over the rest of the finset. -/ -lemma sum_add_sum_erase {n M : Type*} [AddCommMonoid M] [DecidableEq n] {s : Finset n} {f : n → M} - (a : n) (ha : a ∈ s) : - f a + ∑ i ∈ s.erase a, f i = ∑ i ∈ s, f i := by - rw [add_sum_erase s f ha] - -/-- A finset `s` is disjoint from its right complement. -/ -@[simp] -lemma Finset.disjoint_compl_right {n : Type*} [Fintype n] [DecidableEq n] {s : Finset n} : - Disjoint s (univ \ s) := by - rw [@Finset.disjoint_iff_inter_eq_empty] - rw [@inter_sdiff_self] - -/-- The standard simplex is bounded. -/ -lemma bounded_stdSimplex' {n : Type*} [Fintype n] [DecidableEq n] : Bornology.IsBounded (stdSimplex ℝ n) := by - rw [Metric.isBounded_iff_subset_closedBall 0] - use 1 - intro v hv - rw [mem_closedBall_zero_iff, pi_norm_le_iff_of_nonneg zero_le_one] - intro i - rw [Real.norm_eq_abs] - have h_le_one : v i ≤ 1 := by - have h_sum_others_nonneg : 0 ≤ ∑ j ∈ univ.erase i, v j := - sum_nonneg fun j _ => hv.1 j - have h_split : ∑ j ∈ univ, v j = v i + ∑ j ∈ univ.erase i, v j := by - rw [add_sum_erase _ _ (mem_univ i)] - linarith [hv.2, h_split, h_sum_others_nonneg] - exact abs_le_of_le_of_neg_le h_le_one (by linarith [hv.1 i]) - -variable {n : Type*} - -/-- For a vector on the standard simplex, if the sum of a subset of its components is 1, - then the components outside that subset must be zero. -/ -lemma mem_supp_of_sum_eq_one [Fintype n] [DecidableEq n] {v : n → ℝ} (hv : v ∈ stdSimplex ℝ n) (S : Finset n) - (h_sum : ∑ i ∈ S, v i = 1) : - ∀ i, v i ≠ 0 → i ∈ S := by - intro i hi_ne_zero - by_contra hi_not_in_S - have h_sum_all : ∑ j, v j = 1 := hv.2 - have h_sum_split : ∑ j, v j = (∑ j ∈ S, v j) + (∑ j ∈ Sᶜ, v j) := by - rw [Finset.sum_add_sum_compl S v] - rw [← h_sum, h_sum_split] at h_sum_all - have h_sum_compl_zero : ∑ j ∈ Sᶜ, v j = 0 := by linarith - have h_nonneg : ∀ j ∈ Sᶜ, 0 ≤ v j := fun j _ ↦ hv.1 j - have h_v_compl_zero : ∀ j ∈ Sᶜ, v j = 0 := - (sum_eq_zero_iff_of_nonneg h_nonneg).mp h_sum_compl_zero - specialize h_v_compl_zero i (mem_compl.mpr hi_not_in_S) - exact hi_ne_zero h_v_compl_zero - -/-- A non-negative, non-zero vector must have a positive component. -/ -lemma exists_pos_of_ne_zero [Fintype n] [DecidableEq n] {v : n → ℝ} (h_nonneg : ∀ i, 0 ≤ v i) (h_ne_zero : v ≠ 0) : - ∃ i, 0 < v i := by - by_contra h_all_nonpos - apply h_ne_zero - ext i - exact le_antisymm (by simp_all) (h_nonneg i) - -/-- A set is nonempty if and only if its finite conversion is nonempty. -/ -lemma Set.toFinset_nonempty_iff {α : Type*} [Fintype α] [DecidableEq α] (s : Set α) [Finite s] [Fintype s] : - s.toFinset.Nonempty ↔ s.Nonempty := by - constructor - · intro h - obtain ⟨x, hx⟩ := h - exact ⟨x, Set.mem_toFinset.mp hx⟩ - · intro h - obtain ⟨x, hx⟩ := h - exact ⟨x, Set.mem_toFinset.mpr hx⟩ - -/-- Division inequality: a / b ≤ c ↔ a ≤ c * b when b > 0. -/ -lemma div_le_iff {a b c : ℝ} (hb : 0 < b) : a / b ≤ c ↔ a ≤ c * b := by - rw [@le_iff_le_iff_lt_iff_lt] - exact lt_div_iff₀ hb - -/-- For real numbers, if `0 < b`, then `a ≤ c * b ↔ a / b ≤ c`. -/ -lemma le_div_iff {a b c : ℝ} (hb : 0 < b) : a ≤ c * b ↔ a / b ≤ c := by - rw [←div_le_iff hb] - -/-- The ratio (A *ᵥ v) i / v i is nonnegative when A has nonnegative entries and v is nonnegative -/ -lemma ratio_nonneg [Fintype n] (A : Matrix n n ℝ) (hA_nonneg : ∀ i j, 0 ≤ A i j) {v : n → ℝ} - (hv_nonneg : ∀ i, 0 ≤ v i) (i : n) (hv_pos : 0 < v i) : 0 ≤ (A *ᵥ v) i / v i := - div_nonneg (Finset.sum_nonneg fun j _ => mul_nonneg (hA_nonneg i j) (hv_nonneg j)) hv_pos.le - -lemma Finset.inf'_pos {α : Type*} {s : Finset α} (hs : s.Nonempty) - {f : α → ℝ} (h_pos : ∀ a ∈ s, 0 < f a) : - 0 < s.inf' hs f := by - obtain ⟨b, hb_mem, h_fb_is_inf⟩ := s.exists_mem_eq_inf' hs f - have h_fb_pos : 0 < f b := h_pos b hb_mem - rw [h_fb_is_inf] - exact h_fb_pos - -lemma lt_not_le {α : Type*} [PartialOrder α] (x y : α) : x < y → ¬ (x ≥ y) := by - intro h_lt h_ge - exact not_le_of_gt h_lt h_ge - -section ConditionallyCompleteLinearOrder - -variable {α : Type*} [ConditionallyCompleteLinearOrder α] -/-- If y is an upper bound of a set s, and x is in s, then x ≤ y -/ -lemma le_of_mem_upperBounds {s : Set α} {x : α} {y : α} (hy : y ∈ upperBounds s) (hx : x ∈ s) : x ≤ y := by - exact hy hx - -lemma bddAbove_iff_exists_upperBound {s : Set α} : BddAbove s ↔ ∃ b, ∀ x ∈ s, x ≤ b := by exact - bddAbove_def - ---lemma le_sSup_of_mem {s : Set α} {x : α} (hx : x ∈ s) : x ≤ sSup s := by --- exact le_sSup_iff.mpr fun b a ↦ a hx - -end ConditionallyCompleteLinearOrder - -/-- -The definition of the `i`-th component of a matrix-vector product. -This is standard in Mathlib and often available via `simp`. --/ -lemma mulVec_apply {n : Type*} [Fintype n] {A : Matrix n n ℝ} {v : n → ℝ} (i : n) : - (A *ᵥ v) i = ∑ j, A i j * v j := -rfl - -/-- -An element of a set is less than or equal to the supremum of that set, -provided the set is non-empty and bounded above. --/ -lemma le_sSup_of_mem {s : Set ℝ} (_ : s.Nonempty) (hs_bdd : BddAbove s) {y : ℝ} (hy : y ∈ s) : - y ≤ sSup s := -le_csSup hs_bdd hy - -/-- A sum of non-negative terms is strictly positive if and only if the sum is not zero. - This is a direct consequence of the sum being non-negative. -/ -lemma sum_pos_of_nonneg_of_ne_zero {α : Type*} {s : Finset α} {f : α → ℝ} - (h_nonneg : ∀ a ∈ s, 0 ≤ f a) (h_ne_zero : ∑ x ∈ s, f x ≠ 0) : - 0 < ∑ x ∈ s, f x := by - have h_sum_nonneg : 0 ≤ ∑ x ∈ s, f x := Finset.sum_nonneg h_nonneg - exact lt_of_le_of_ne h_sum_nonneg h_ne_zero.symm - --- Missing lemma: bound each component by the supremum -lemma le_sup'_of_mem {α β : Type*} [SemilatticeSup α] {s : Finset β} (hs : s.Nonempty) - (f : β → α) {b : β} (hb : b ∈ s) : f b ≤ s.sup' hs f := by - exact le_sup' f hb - --- Missing lemma: supremum is at least any component -lemma sup'_le_sup'_of_le {α β : Type*} [SemilatticeSup α] {s t : Finset β} - (hs : s.Nonempty) (ht : t.Nonempty) (f : β → α) (h : s ⊆ t) : - s.sup' hs f ≤ t.sup' ht f := by - exact sup'_mono f h hs - --- A non-zero function must be non-zero at some point. -lemma Function.exists_ne_zero_of_ne_zero {α β} [Zero β] {f : α → β} (h : f ≠ (fun _ => 0)) : ∃ i, f i ≠ 0 := by - by_contra hf - push_neg at hf - apply h - ext x - exact hf x - -/-- If the ratio (A *ᵥ v) i / v i = 0 and v i > 0, then (A *ᵥ v) i = 0. -/ -lemma mulVec_eq_zero_of_ratio_zero [Fintype n] (A : Matrix n n ℝ) {v : n → ℝ} (i : n) (hv_pos : 0 < v i) - (h_ratio_zero : (A *ᵥ v) i / v i = 0) : - (A *ᵥ v) i = 0 := by - rw [div_eq_zero_iff] at h_ratio_zero - exact h_ratio_zero.resolve_right (ne_of_gt hv_pos) - -lemma mul_vec_mul_vec - {n : Type*} [Fintype n] [Nonempty n] (A B : Matrix n n ℝ) (v : n → ℝ) : - (A * B) *ᵥ v = A *ᵥ (B *ᵥ v) := by - ext i - simp only [mulVec, dotProduct, mul_apply] - simp_rw [Finset.mul_sum] - rw [Finset.sum_comm] - simp_rw [Finset.sum_mul] - rw [Finset.sum_comm] - rw [Finset.sum_comm] - simp [mul_assoc] - -/-- If `A *ᵥ v` is zero on the support `S` of `v`, then for any `i ∈ S`, `A i k` must be zero -for all `k` where `v` is positive (i.e., `k ∈ S`). -/ -lemma zero_block_of_mulVec_eq_zero [Fintype n] (A : Matrix n n ℝ) (hA_nonneg : ∀ i j, 0 ≤ A i j) - {v : n → ℝ} (hv_nonneg : ∀ i, 0 ≤ v i) (S : Set n) (hS_def : S = {i | 0 < v i}) - (h_Av_zero : ∀ i ∈ S, (A *ᵥ v) i = 0) : - ∀ i ∈ S, ∀ k ∈ S, A i k = 0 := by - intro i hi_S k hk_S - have h_sum_Aiv_eq_zero : (A *ᵥ v) i = 0 := h_Av_zero i hi_S - rw [mulVec, dotProduct] at h_sum_Aiv_eq_zero - have h_sum_terms_nonneg : ∀ l, 0 ≤ A i l * v l := - fun l ↦ mul_nonneg (hA_nonneg i l) (hv_nonneg l) - have h_Aik_vk_zero : A i k * v k = 0 := - (sum_eq_zero_iff_of_nonneg (fun l _ ↦ h_sum_terms_nonneg l)).mp h_sum_Aiv_eq_zero k (mem_univ k) - rw [hS_def] at hk_S - exact (mul_eq_zero.mp h_Aik_vk_zero).resolve_right (ne_of_gt hk_S) - -/-- For any natural number `n > 0`, it is either equal to 1 or greater than 1. - This is a helper for reasoning about the cardinality of a Fintype. -/ -lemma Nat.eq_one_or_one_lt (n : ℕ) (hn : n ≠ 0) : n = 1 ∨ 1 < n := by - rcases n with _ | n - · contradiction - rcases n with _ | n - · exact Or.inl rfl - · exact Or.inr (Nat.succ_lt_succ (Nat.succ_pos _)) - -/-- For a finite type, the infimum over the type is attained at some element. -/ -lemma exists_eq_iInf {α : Type*} [Fintype α] [Nonempty α] (f : α → ℝ) : ∃ i, f i = ⨅ j, f j := - exists_eq_ciInf_of_finite - -/-- Functions computing pointwise infima are equal when using `iInf` vs `Finset.inf'`. -/ -lemma Finset.iInf_apply_eq_finset_inf'_apply_fun {α β γ : Type*} - [Fintype α] [Nonempty α] [ConditionallyCompleteLinearOrder γ] - (f : α → β → γ) : - (fun x ↦ ⨅ i, f i x) = (fun x ↦ (Finset.univ : Finset α).inf' Finset.univ_nonempty (fun i ↦ f i x)) := by - ext x - have h1 : ⨅ i, f i x = ⨅ i ∈ Set.univ, f i x := by - simp only [Set.mem_univ, ciInf_unique] - have h2 : ⨅ i ∈ Set.univ, f i x = ⨅ i ∈ (Finset.univ : Finset α), f i x := by - congr - ext i - simp only [Set.mem_univ, ciInf_unique, mem_univ] - have h3 : ⨅ i ∈ (Finset.univ : Finset α), f i x = - (Finset.univ : Finset α).inf' Finset.univ_nonempty (fun i ↦ f i x) := by - rw [Finset.inf'_eq_csInf_image] - simp only [mem_univ, ciInf_unique, Finset.mem_univ, Finset.coe_univ, image_univ] - rfl - rw [h1, h2, h3] - -/-- For a finite index type, the point-wise (finite) infimum of a family of - continuous functions is continuous. -/ -lemma continuousOn_iInf' {α β : Type*} - [Fintype α] [Nonempty α] - [TopologicalSpace β] - {s : Set β} {f : α → β → ℝ} - (hf : ∀ i, ContinuousOn (f i) s) : - ContinuousOn (fun x ↦ ⨅ i, f i x) s := by - classical - let g : β → ℝ := - fun x ↦ (Finset.univ : Finset α).inf' Finset.univ_nonempty (fun i ↦ f i x) - have hg : ContinuousOn g s := by - exact ContinuousOn.finset_inf'_apply Finset.univ_nonempty fun i a ↦ hf i - have h_eq : (fun x ↦ ⨅ i, f i x) = g := by - dsimp [g] - exact Finset.iInf_apply_eq_finset_inf'_apply_fun f - rwa [h_eq] - -/-- An element of the image of a set is less than or equal to the supremum of that set. -/ -lemma le_csSup_of_mem {α : Type*} {f : α → ℝ} {s : Set α} (hs_bdd : BddAbove (f '' s)) {y : α} (hy : y ∈ s) : - f y ≤ sSup (f '' s) := -le_csSup hs_bdd (Set.mem_image_of_mem f hy) - -lemma div_lt_iff {a b c : ℝ} (hc : 0 < c) : b / c < a ↔ b < a * c := - lt_iff_lt_of_le_iff_le (by exact le_div_iff₀ hc) - - ---lemma lt_div_iff (hc : 0 < c) : a < b / c ↔ a * c < b := --- lt_iff_lt_of_le_iff_le (div_le_iff hc) - -lemma smul_sum (α : Type*) [Fintype α] (r : ℝ) (f : α → ℝ) : - r • (∑ i, f i) = ∑ i, r • f i := by - simp only [smul_eq_mul, Finset.mul_sum] - -lemma ones_norm_mem_simplex [Fintype n] [Nonempty n] : - (fun _ => (Fintype.card n : ℝ)⁻¹) ∈ stdSimplex ℝ n := by - dsimp [stdSimplex]; constructor - · intro i; apply inv_nonneg.2; norm_cast; exact Nat.cast_nonneg _ - · simp [Finset.sum_const, Finset.card_univ]; - -/-- -If a value `y` is a lower bound for a function `f` over a non-empty finset `s` and is -also attained by `f` for some element in `s`, then `y` is the infimum of `f` over `s`. --/ -lemma Finset.inf'_eq_of_forall_le_of_exists_le {α β} [LinearOrder β] - {s : Finset α} (hs : s.Nonempty) (f : α → β) (y : β) - (h_le : ∀ i ∈ s, y ≤ f i) (h_exists : ∃ i ∈ s, f i = y) : - s.inf' hs f = y := by - apply le_antisymm - · obtain ⟨i, hi_mem, hi_eq⟩ := h_exists - rw [← hi_eq] - exact inf'_le f hi_mem - · exact (le_inf'_iff hs f).mpr h_le - -/-- -If a vector `x` lies in the standard simplex, then it cannot be the zero vector. -Indeed, the coordinates of a simplex‐vector sum to `1`, whereas the coordinates of -the zero vector sum to `0`. --/ -lemma ne_zero_of_mem_stdSimplex - {n : Type*} [Fintype n] [Nonempty n] {x : n → ℝ} - (hx : x ∈ stdSimplex ℝ n) : - x ≠ 0 := by - intro h_zero - have h_sum_zero : (∑ i, x i) = 0 := by - subst h_zero - simp_all only [Pi.zero_apply, Finset.sum_const_zero] - have h_sum_one : (∑ i, x i) = 1 := hx.2 - linarith - -lemma Real.le_sSup {s : Set ℝ} {y : ℝ} (h_mem : y ∈ s) (h_bdd : BddAbove s) : - y ≤ sSup s := - le_csSup h_bdd h_mem - -/-- The supremum of the image of `s` under `f` equals the indexed supremum over the subtype. -/ -lemma csSup_image' {α β : Type*} [ConditionallyCompleteLattice α] - {f : β → α} {s : Set β} (hs : s.Nonempty) (hb : BddAbove (f '' s)) : - sSup (f '' s) = ⨆ i : s, f i := by - have h₁ : IsLUB (f '' s) (sSup (f '' s)) := isLUB_csSup (hs.image _) hb - have h₂ := isLUB_ciSup_set (f := f) (s := s) hb hs - exact h₁.unique h₂ - -lemma iSup_eq_sSup {α β : Type*} [ConditionallyCompleteLattice α] - (f : β → α) (s : Set β) : - (⨆ i : s, f i) = sSup (f '' s) := by - classical - -- `sSup_image'` gives `sSup (f '' s) = ⨆ i : s, f i` - simpa using (sSup_image' (f := f) (s := s)).symm - -namespace Matrix - -/-- The dot product of two strictly positive vectors is positive. -/ -lemma dotProduct_pos_of_pos_of_pos {n : Type*} [Fintype n] [Nonempty n] - {u v : n → ℝ} (hu_pos : ∀ i, 0 < u i) (hv_pos : ∀ i, 0 < v i) : - 0 < u ⬝ᵥ v := by - simp [dotProduct] - apply Finset.sum_pos - · intro i _ - exact mul_pos (hu_pos i) (hv_pos i) - · apply Finset.univ_nonempty - -/-- The dot product of a positive vector with a non-negative, non-zero vector is positive. -/ -lemma dotProduct_pos_of_pos_of_nonneg_ne_zero {n : Type*} [Fintype n] [DecidableEq n] - {u v : n → ℝ} (hu_pos : ∀ i, 0 < u i) (hv_nonneg : ∀ i, 0 ≤ v i) (hv_ne_zero : v ≠ 0) : - 0 < u ⬝ᵥ v := by - simp [dotProduct] - have h_exists_pos : ∃ i, 0 < v i := by - by_contra h - push_neg at h - have h_all_zero : ∀ i, v i = 0 := fun i => - le_antisymm (h i) (hv_nonneg i) - have h_zero : v = 0 := funext h_all_zero - contradiction - have h_nonneg : ∀ i ∈ Finset.univ, 0 ≤ u i * v i := - fun i _ => mul_nonneg (le_of_lt (hu_pos i)) (hv_nonneg i) - rcases h_exists_pos with ⟨i, hi⟩ - have hi_mem : i ∈ Finset.univ := Finset.mem_univ i - have h_pos : 0 < u i * v i := mul_pos (hu_pos i) hi - exact sum_pos_of_mem h_nonneg i hi_mem h_pos - -/-- Dot‐product is linear in the first argument. -/ -lemma dotProduct_smul_left {n : Type*} [Fintype n] - (c : ℝ) (v w : n → ℝ) : - (c • v) ⬝ᵥ w = c * (v ⬝ᵥ w) := by - unfold dotProduct - simp [smul_eq_mul, Finset.mul_sum, mul_comm, mul_left_comm] - -/-- The dot product is linear in the right argument. -/ -lemma dotProduct_smul_right {n : Type*} [Fintype n] - (c : ℝ) (v w : n → ℝ) : - v ⬝ᵥ (c • w) = c * (v ⬝ᵥ w) := by - simp [dotProduct, smul_eq_mul, Finset.mul_sum, mul_left_comm] - -/-- -If `u` is a non-negative vector and `v ≤ w` component-wise, then `u ⬝ᵥ v ≤ u ⬝ᵥ w`. -This is because the dot product is a sum of products, and multiplying by non-negative -numbers preserves the inequality. --/ -lemma dotProduct_le_dotProduct_of_nonneg {n : Type*} [Fintype n] {u v w : n → ℝ} - (hu_nonneg : ∀ i, 0 ≤ u i) (h_le : v ≤ w) : - u ⬝ᵥ v ≤ u ⬝ᵥ w := by - simp_rw [dotProduct, Pi.le_def] at h_le ⊢ - apply Finset.sum_le_sum - intro i _ - exact mul_le_mul_of_nonneg_left (h_le i) (hu_nonneg i) - -/-- -The dot product is "associative" with matrix-vector multiplication, in the sense -that `v ⬝ᵥ (A *ᵥ w) = (Aᵀ *ᵥ v) ⬝ᵥ w`. This is a consequence of the definition of -the matrix transpose and dot product. --/ -lemma dotProduct_mulVec_assoc {n : Type*} [Fintype n] [DecidableEq n] - (A : Matrix n n ℝ) (v w : n → ℝ) : - v ⬝ᵥ (A *ᵥ w) = (Aᵀ *ᵥ v) ⬝ᵥ w := by - simp only [dotProduct, mulVec, transpose_apply, Finset.mul_sum, Finset.sum_mul] - rw [Finset.sum_comm] - simp [mul_comm, mul_left_comm] - --- Matrix-vector multiplication component -theorem matrix_mulVec_component {n : Type*} [Fintype n] [DecidableEq n] - (A : Matrix n n ℝ) (v : n → ℝ) (j : n) : - (A *ᵥ v) j = ∑ i, A j i * v i := by - simp [Matrix.mulVec]; rfl - -/-- -The dot product `v ⬝ᵥ (A *ᵥ w)` can be rewritten by moving the matrix `A` -to the other argument, where it becomes its transpose `Aᵀ`. --/ -lemma transpose_mulVec {n : Type*} [Fintype n] (A : Matrix n n ℝ) (v w : n → ℝ) : - v ⬝ᵥ (A *ᵥ w) = (Aᵀ *ᵥ v) ⬝ᵥ w := by - classical - simp only [dotProduct, mulVec_apply, transpose_apply, - Finset.mul_sum, Finset.sum_mul]; - rw [Finset.sum_comm] - simp [mul_comm, mul_left_comm] - -/-- -Commutativity property for dot product with matrix-vector multiplication. -For vectors `u`, `v` and matrix `A`: `u ⬝ᵥ (A *ᵥ v) = (A *ᵥ u) ⬝ᵥ v`. -This follows from the fact that `u ⬝ᵥ (A *ᵥ v) = u ᵥ* A ⬝ᵥ v = (Aᵀ *ᵥ u) ⬝ᵥ v`. --/ -lemma dotProduct_mulVec_comm {n : Type*} [Fintype n] (u v : n → ℝ) (A : Matrix n n ℝ) : - u ⬝ᵥ (A *ᵥ v) = (Aᵀ *ᵥ u) ⬝ᵥ v := by - rw [dotProduct_mulVec, vecMul_eq_mulVec_transpose] - --- This could be a general lemma in the Matrix API -lemma diagonal_mulVec_ones [DecidableEq n][Fintype n] (d : n → ℝ) : - diagonal d *ᵥ (fun _ => 1) = d := by - ext i; simp [mulVec_diagonal] - --- This could also be a general lemma -lemma diagonal_inv_mulVec_self [DecidableEq n][Fintype n] {d : n → ℝ} (hd : ∀ i, d i ≠ 0) : - diagonal (d⁻¹) *ᵥ d = fun _ => 1 := by - ext i - simp [mulVec_diagonal] - simp_all only [ne_eq, isUnit_iff_ne_zero, not_false_eq_true, IsUnit.inv_mul_cancel] - -end Matrix - -variable {α ι : Type*} {f : ι → α} {s : Set ι} -open Set --- Indexed supremum equals the supremum of the image -theorem iSup_eq_sSup_image [ConditionallyCompleteLattice α] : - (⨆ x : s, f x) = sSup (f '' s) := by - simp [iSup, image_eq_range] - -lemma eq_zero_of_sum_eq_zero {ι : Type*} [Fintype ι] - (f : ι → ℝ) (hf : ∀ i, 0 ≤ f i) (hsum : ∑ j, f j = 0) (i : ι) : f i = 0 := by - by_contra hne0 - have hne : ¬ 0 = f i := mt Eq.symm hne0 - have hgt : 0 < f i := lt_iff_le_and_ne.mpr ⟨hf i, hne⟩ - have hsum_pos : 0 < ∑ j, f j := - Finset.sum_pos' (fun j _ => hf j) ⟨i, Finset.mem_univ i, hgt⟩ - simpa [hsum] using ne_of_gt hsum_pos diff --git a/lean/ProofAtlas.lean b/lean/ProofAtlas.lean index f92fec7..bc6f401 100644 --- a/lean/ProofAtlas.lean +++ b/lean/ProofAtlas.lean @@ -17,29 +17,17 @@ import ProofAtlas.Hypergraph.TransitionMatrix -- Linear algebra import ProofAtlas.LinearAlgebra.Default --- Random walk theory +-- Random walk theory (abstract Markov chain, no MCMC) import ProofAtlas.RandomWalk.FundamentalMatrix import ProofAtlas.RandomWalk.KemenySnell import ProofAtlas.RandomWalk.CommuteTime -import ProofAtlas.RandomWalk.Spectral.Default --- Metric: all instances (development branch — sorry allowed) +-- Metric: resistance distance (sorry-free, MCMC-free) import ProofAtlas.Metric.Default import ProofAtlas.Metric.Resistance.Instance -import ProofAtlas.Metric.CommuteTime.Instance -import ProofAtlas.Metric.JensenShannon.Basic -import ProofAtlas.Metric.JensenShannon.Instance -import ProofAtlas.Metric.Diffusion.Basic -import ProofAtlas.Metric.Diffusion.Instance import ProofAtlas.Metric.Hausdorff import ProofAtlas.Metric.GromovHausdorff --- Hyperbolicity + sequences (sorry'd, development only) -import ProofAtlas.Hypergraph.Sequence -import ProofAtlas.Hyperbolicity.Default -import ProofAtlas.Hyperbolicity.Diffusion -import ProofAtlas.Open - -- Mapping: Expr → Hypergraph import ProofAtlas.Mapping.ExprColor import ProofAtlas.Mapping.Expr @@ -58,33 +46,7 @@ import ProofAtlas.Command.Default import ProofAtlas.Demos.Paper /-! -# `ProofAtlas` library facade - -Top-level entry point for the `ProofAtlas` library. Importing -`ProofAtlas` gives you the complete, sorry-free chain from -`#atlas.*` commands through the compute pipeline to the verified -mathematical layer. - -**Sorry-free guarantee.** Every theorem reachable through this -facade compiles without `sorry`. Work-in-progress theoretical -results (JensenShannon, Diffusion, Hyperbolicity proofs, -`Hypergraph.Sequence`) live on the `development` branch and are -merged into `main` as their proofs are completed. See ADR 0007. - -## What's included - -* `Hypergraph.*` — BDF hypergraph structure and random walk -* `Metric.Resistance.Instance` — resistance distance metric (proven) -* `Metric.CommuteTime.Instance` — commute time metric (proven) -* `Pipeline.*` — compute layer (Float, end-to-end) -* `Command.*` — `#atlas.graph`, `#atlas.resistance`, `#atlas.delta`, etc. -* `Mapping.*` — `Lean.Expr → Hypergraph` extraction - -## What's NOT included (on `development` branch) +# `ProofAtlas` library facade (main branch) -* `Metric.JensenShannon.*` — sorry'd instance -* `Metric.Diffusion.*` — sorry'd instance -* `Hyperbolicity.Basic` — 4 sorry'd proofs -* `Hypergraph.Sequence` — sorry'd -* `Open.lean` — intentional open conjecture +Sorry-free, MCMC-free. See ADR 0007. -/ diff --git a/lean/ProofAtlas/Command/Status.lean b/lean/ProofAtlas/Command/Status.lean index 89613a7..a13c504 100644 --- a/lean/ProofAtlas/Command/Status.lean +++ b/lean/ProofAtlas/Command/Status.lean @@ -5,9 +5,6 @@ Authors: MathNetwork -/ import ProofAtlas.Command.Common import ProofAtlas.Metric.Resistance.Instance -import ProofAtlas.Metric.CommuteTime.Instance -import ProofAtlas.Metric.JensenShannon.Instance -import ProofAtlas.Metric.Diffusion.Instance /-! # `#atlas.status` — `IsHypergraphMetric` registry lookup diff --git a/lean/ProofAtlas/Hyperbolicity/Basic.lean b/lean/ProofAtlas/Hyperbolicity/Basic.lean deleted file mode 100644 index 7f250db..0000000 --- a/lean/ProofAtlas/Hyperbolicity/Basic.lean +++ /dev/null @@ -1,551 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Util.Linter.MathTag -import Mathlib.Topology.MetricSpace.Pseudo.Defs -import Mathlib.Topology.MetricSpace.GromovHausdorff -import Mathlib.Analysis.SpecialFunctions.Exp -import Mathlib.Data.Set.Card - -/-! -# Gromov δ-hyperbolicity (abstract) - -Abstract δ-hyperbolicity of a (pseudo-)metric space, formulated via -Gromov's *four-point condition*. The definition is intentionally -generic — it has no dependence on the BDF setting and applies to any -pseudo-metric space. - -The same notion can be expressed equivalently via Gromov products -`(x | y)_w := (d(x, w) + d(y, w) − d(x, y)) / 2`, via thin -triangles in geodesic metric spaces, or via isometric tree -approximations. We take the four-point form as primary because it -makes sense in discrete / non-geodesic settings (such as -`(V, d_H)` for a BDF hypergraph) without auxiliary path-space -infrastructure. - -## Main definitions - -* `IsGromovHyperbolic X δ` — `X` satisfies Gromov's four-point - condition with constant `δ`. -* `GromovDeviation w x y z` — the four-point deviation - $\max_i S_i - \mathrm{med}_i S_i$ of the three two-point sums. - -## Main properties - -* `IsGromovHyperbolic.mono` — monotone in `δ`. -* `gromovDeviation_le_iff` — equivalent reformulation in terms of - `GromovDeviation`. - -## References - -* Gromov, *Hyperbolic groups*, Essays in Group Theory (1987). -* Bridson–Haefliger, *Metric Spaces of Non-Positive Curvature* (1999), - Ch. III.H. --/ - -namespace ProofAtlas - -variable {X : Type*} [PseudoMetricSpace X] - -/-- **Math.** *Four-point Gromov deviation* of an ordered 4-tuple -under a pseudo-distance function `d : V → V → ℝ`: the gap between -the maximum and the median of the three two-point sums -``` - S₁ := d(u, v) + d(x, y), S₂ := d(u, x) + d(v, y), - S₃ := d(u, y) + d(v, x). -``` -Computed via `2·max + min − (S₁ + S₂ + S₃) = max − median`. - -Note: this is the "raw" function form, taking any `d : V → V → ℝ`. -For a `PseudoMetricSpace`, instantiate with `d := dist`. -/ --- doc:start fourPointDeviation -def fourPointDeviation {V : Type*} (d : V → V → ℝ) (u v x y : V) : ℝ := - let S₁ := d u v + d x y - let S₂ := d u x + d v y - let S₃ := d u y + d v x - let M := max S₁ (max S₂ S₃) - let m := min S₁ (min S₂ S₃) - 2 * M + m - (S₁ + S₂ + S₃) --- doc:end - -/-- **Math.** For any three reals `a, b, c`, the quantity -`max − median = 2·max + min − (a+b+c)` has squared value bounded by -the sum of the three squared pairwise differences. -/ -lemma fourPointDeviation_aux_sq_le_sum_sq (a b c : ℝ) : - (2 * max a (max b c) + min a (min b c) - (a + b + c))^2 - ≤ (a - b)^2 + (b - c)^2 + (a - c)^2 := by - rcases le_total a b with hab | hab <;> - rcases le_total b c with hbc | hbc <;> - rcases le_total a c with hac | hac <;> - simp_all <;> - nlinarith [sq_nonneg (a-b), sq_nonneg (b-c), sq_nonneg (a-c)] - -/-- **Math.** Nonnegativity of `max − median = 2·max + min − sum` -for any three reals — the four-point Gromov deviation `Φ` is -nonneg per tuple. -/ -lemma fourPointDeviation_aux_nonneg (a b c : ℝ) : - 0 ≤ 2 * max a (max b c) + min a (min b c) - (a + b + c) := by - rcases le_total a b with hab | hab <;> - rcases le_total b c with hbc | hbc <;> - rcases le_total a c with hac | hac <;> - simp_all <;> - linarith - -/-- **Math.** *Gromov δ-hyperbolicity* (four-point condition): for -every quadruple `(w, x, y, z) ∈ X⁴`, -``` - d(w, y) + d(x, z) ≤ max(d(w, x) + d(y, z), d(w, z) + d(x, y)) + 2δ. -``` -By symmetry of the role of `(w, y)` vs `(x, z)` after relabelling, -the four-point condition forces the same inequality for all three -pairings; equivalently `gromovDeviation w x y z ≤ 2δ`. -/ -def IsGromovHyperbolic (X : Type*) [PseudoMetricSpace X] (δ : ℝ) : Prop := - ∀ w x y z : X, - dist w y + dist x z ≤ - max (dist w x + dist y z) (dist w z + dist x y) + 2 * δ - -/-- **Math.** δ-hyperbolicity is monotone in `δ`: tightening the -constant only strengthens the condition. -/ -lemma IsGromovHyperbolic.mono {δ δ' : ℝ} (hδ : δ ≤ δ') - (h : IsGromovHyperbolic X δ) : - IsGromovHyperbolic X δ' := by - intro w x y z - calc dist w y + dist x z - ≤ max (dist w x + dist y z) (dist w z + dist x y) + 2 * δ := h w x y z - _ ≤ max (dist w x + dist y z) (dist w z + dist x y) + 2 * δ' := by - gcongr - -/-- **Math.** Any pseudo-metric space is `δ`-hyperbolic for -sufficiently large `δ` (trivially): take `δ ≥ diam X`. (Useful as a -sanity check that the predicate is not vacuous.) -/ -lemma IsGromovHyperbolic.of_bounded_dist - (M : ℝ) (hM : ∀ a b : X, dist a b ≤ M) : - IsGromovHyperbolic X M := by - intro w x y z - have h1 : dist w y ≤ M := hM _ _ - have h2 : dist x z ≤ M := hM _ _ - have h3 : (0 : ℝ) ≤ max (dist w x + dist y z) (dist w z + dist x y) := by - refine le_max_of_le_left ?_ - positivity - linarith - -/-! ## Gromov products and the equivalent characterisation -/ - -/-- **Math.** *Gromov product* of `x` and `y` based at `w`: -`(x | y)_w := (d(x, w) + d(y, w) − d(x, y)) / 2`. Measures how far -`x` and `y` "go together" starting from `w`; bounded below by `0` -(triangle inequality) and above by `min(d(x, w), d(y, w))`. -/ -noncomputable def gromovProduct (w x y : X) : ℝ := - (dist x w + dist y w - dist x y) / 2 - -/-- **Math.** Symmetry of the Gromov product in its two arguments. -/ -lemma gromovProduct_comm (w x y : X) : - gromovProduct w x y = gromovProduct w y x := by - unfold gromovProduct - rw [dist_comm x y, add_comm (dist x w) (dist y w)] - -/-- **Math.** Self-product equals base distance: -`(x | x)_w = d(x, w)`. -/ -lemma gromovProduct_self (w x : X) : - gromovProduct w x x = dist x w := by - unfold gromovProduct - rw [dist_self, sub_zero] - ring - -/-- **Math.** The Gromov product is nonnegative (triangle inequality). -/ -lemma gromovProduct_nonneg (w x y : X) : - 0 ≤ gromovProduct w x y := by - unfold gromovProduct - have := dist_triangle x w y - linarith [dist_comm w y] - -/-- **Math.** *Gromov product characterisation of δ-hyperbolicity.* -A pseudo-metric space is `δ`-hyperbolic iff for every base point `w` -and every triple `x, y, z`, -``` - (x | z)_w ≥ min ((x | y)_w, (y | z)_w) − δ. -``` -This is the standard alternative formulation; both express the same -4-point inequality after algebraic rearrangement. -/ -theorem isGromovHyperbolic_iff_gromovProduct {δ : ℝ} : - IsGromovHyperbolic X δ ↔ - ∀ w x y z : X, - min (gromovProduct w x y) (gromovProduct w y z) - δ - ≤ gromovProduct w x z := by - unfold IsGromovHyperbolic gromovProduct - refine forall_congr' fun w => forall_congr' fun x => forall_congr' fun y => - forall_congr' fun z => ?_ - set a := dist w x - set b := dist w y - set c := dist w z - set p := dist x y - set q := dist y z - set r := dist x z - rw [dist_comm x w, dist_comm y w, dist_comm z w] - change b + r ≤ max (a + q) (c + p) + 2 * δ ↔ - min ((a + b - p) / 2) ((b + c - q) / 2) - δ ≤ (a + c - r) / 2 - constructor - · intro h - rcases le_total ((a + b - p) / 2) ((b + c - q) / 2) with hmin | hmin - · rw [min_eq_left hmin] - have hmax : max (a + q) (c + p) = c + p := max_eq_right (by linarith) - rw [hmax] at h - linarith - · rw [min_eq_right hmin] - have hmax : max (a + q) (c + p) = a + q := max_eq_left (by linarith) - rw [hmax] at h - linarith - · intro h - rcases le_total (a + q) (c + p) with hcase | hcase - · rw [max_eq_right hcase] - have hmin : (a + b - p) / 2 ≤ (b + c - q) / 2 := by linarith - rw [min_eq_left hmin] at h - linarith - · rw [max_eq_left hcase] - have hmin : (b + c - q) / 2 ≤ (a + b - p) / 2 := by linarith - rw [min_eq_right hmin] at h - linarith - -/-! ## Abstract consequences of δ-hyperbolicity - -The four theorems below are abstract corollaries of -`IsGromovHyperbolic X δ` — none of them reference any BDF data. They -are the substantive payoff of the abstract layer: any concrete -metric that verifies `IsGromovHyperbolic` inherits all four -consequences automatically. -/ - -/-- **Math.** *Discrete slim-triangles.* In a δ-hyperbolic space, -given three "vertices" `u, v, w` and a fourth point `p` that lies -approximately on a `u`–`v` geodesic (in the 4-point sense -`d(u, p) + d(p, v) ≤ d(u, v) + 2ε`), the point `p` is within -distance `δ + ε` of the union of the `u`–`w` and `v`–`w` shoulders. -Formal version: a lower bound on `d(p, w)` in terms of the smaller -of the two shoulder distances. -/ -theorem IsGromovHyperbolic.slim_triangles - {δ ε : ℝ} (h : IsGromovHyperbolic X δ) - (u v w p : X) (h_p_on_uv : dist u p + dist p v ≤ dist u v + 2 * ε) : - dist p w - ≥ min (dist u w - dist u p) (dist v w - dist v p) - δ - ε := by - have hδ : 0 ≤ δ := by - have h0 := h u u u u - simp at h0 - linarith - have hε : 0 ≤ ε := by - have := dist_triangle u p v - linarith - have h1 : dist u w ≤ dist u p + dist p w := dist_triangle u p w - have h2 : dist v w ≤ dist v p + dist p w := dist_triangle v p w - have h3 : dist u w - dist u p ≤ dist p w := by linarith - have h4 : dist v w - dist v p ≤ dist p w := by linarith - have hmin : min (dist u w - dist u p) (dist v w - dist v p) ≤ dist p w := - le_trans (min_le_left _ _) h3 - linarith - -/-- **Math.** *Exponential ball-growth bound.* A δ-hyperbolic space -with **bounded local geometry** (the unit ball at every point has -size `≤ K`) has at most exponentially growing balls: there exist -`C, λ` such that `|B(x, R)| ≤ C · exp(λ · R)` for every centre `x` -and radius `R`. Stated for `Fintype X` to give finite cardinalities. -/ -theorem IsGromovHyperbolic.ball_growth_bound - {δ : ℝ} (_h : IsGromovHyperbolic X δ) - {K : ℕ} (_h_local : ∀ x : X, (Metric.ball x 1 : Set X).ncard ≤ K) : - ∃ C lam : ℝ, ∀ (x : X) (R : ℝ), - ((Metric.ball x R : Set X).ncard : ℝ) ≤ C * Real.exp (lam * R) := by - sorry - -/-- **Math.** *δ-hyperbolicity is closed under Gromov–Hausdorff -limits.* If a sequence `X_n` of (nonempty compact) metric spaces is -uniformly δ-hyperbolic and tends to `X` in the GH topology, then -`X` is also δ-hyperbolic. The 4-point inequality is a closed -condition because the Gromov–Hausdorff topology refines pointwise -convergence of distances on Hausdorff-close ε-nets. -/ -theorem IsGromovHyperbolic.stable_under_GH_limit - {δ : ℝ} - {X_n : ℕ → GromovHausdorff.GHSpace} {Y : GromovHausdorff.GHSpace} - (_h_each : ∀ n, IsGromovHyperbolic (X_n n).Rep δ) - (_h_tendsto : Filter.Tendsto X_n Filter.atTop (nhds Y)) : - IsGromovHyperbolic Y.Rep δ := by - sorry - -end ProofAtlas - -/-! ## Average four-point deviation bound (Cauchy–Schwarz) - -The Markov-style bound `Φ ≤ 2δ` is too crude for asymptotic -applications. The non-trivial bound is the **Cauchy–Schwarz** -form: the `μ⁴`-weighted mean of `Φ` is controlled by the -`μ⁴`-weighted second moment of one pair-sum difference (and by -the iid position-permutation symmetry, the three pair-sum -differences have equal moments). The bound is parameterised by an -abstract value `M = 𝔼_{μ⁴}[(d(u, v) + d(x, y) − d(u, x) − d(v, y))²]`; -concrete applications plug in their own evaluation of `M`. - -* **Spectral** (paper 1 / paper 2): `M = 16 · κ₂` via spectral - expansion. Gives the sharp Theorem 7.1 bound. -* **Variance fallback**: `M ≤ 4 · Var_{μ²}[d]` (always true, - generally not sharp; the slack is `8 · Var_μ[E_v[d(u, v)]]`). - -This file states the abstract bound only. Concrete corollaries -live alongside each metric (paper 1 = `RandomWalk/Spectral/ -ProbabilisticBound.lean`; paper 2 = `Metric/Resistance/Spectral.lean`). -/ - -namespace ProofAtlas - -/-- **Eng.** Swap of positions 3 and 4 in a 4-tuple. Used in the iid -symmetry argument for the weighted average four-point deviation bound: -under this swap, `μ⁴` is invariant (commutativity of multiplication) -and `(S₁ − S₂)²` maps to `(S₁ − S₃)²`. -/ -private def fpdSwap34 (V : Type*) : V × V × V × V ≃ V × V × V × V where - toFun p := (p.1, p.2.1, p.2.2.2, p.2.2.1) - invFun p := (p.1, p.2.1, p.2.2.2, p.2.2.1) - left_inv _ := rfl - right_inv _ := rfl - -/-- **Eng.** Swap of positions 1 and 3 in a 4-tuple. Used in the iid -symmetry argument: `μ⁴` is invariant, and `(S₁ − S₂)²` maps to -`(S₂ − S₃)²` (up to sign, which the square removes). -/ -private def fpdSwap13 (V : Type*) : V × V × V × V ≃ V × V × V × V where - toFun p := (p.2.2.1, p.2.1, p.1, p.2.2.2) - invFun p := (p.2.2.1, p.2.1, p.1, p.2.2.2) - left_inv _ := rfl - right_inv _ := rfl - -/-- **Math.** *Cauchy–Schwarz bound on weighted average four-point -deviation.* For any finite type `V` with pseudo-metric `d` and any -probability measure `μ : V → ℝ` (non-negative, sums to `1`): -``` - 𝔼_{μ⁴}[Φ] ≤ √(3 · 𝔼_{μ⁴}[(d(u, v) + d(x, y) − d(u, x) − d(v, y))²]). -``` -By iid position-permutation symmetry under `μ⁴`, the three pair-sum -differences `S₁ − S₂`, `S₂ − S₃`, `S₁ − S₃` have equal second -moments, so the right-hand side is `3 · M` for any one of them. - -Proof sketch (mirrors paper 1 Theorem 7.1 internal structure): - -1. *Per-tuple bound* via `fourPointDeviation_aux_sq_le_sum_sq`: - `Φ² ≤ (S₁ − S₂)² + (S₂ − S₃)² + (S₁ − S₃)²` pointwise. -2. *Symmetry*: under iid `μ⁴`, the three `𝔼[(Sᵢ − Sⱼ)²]` are equal - (relabel the four iid samples). -3. *Take expectation*: `𝔼[Φ²] ≤ 3 · 𝔼[(S₁ − S₂)²] = 3 M`. -4. *Cauchy–Schwarz*: `(𝔼[Φ])² ≤ (∑ μ⁴) · 𝔼[Φ²] = 1 · 3 M = 3 M`. -5. *Square root*: `𝔼[Φ] ≤ √(3 M)`. -/ -theorem weighted_avg_fourPointDeviation_le_sqrt_pairSumDiff_sq - {V : Type*} [Fintype V] [PseudoMetricSpace V] - (μ : V → ℝ) (hμ_nonneg : ∀ v, 0 ≤ μ v) - (hμ_sum : ∑ v : V, μ v = 1) : - (∑ p : V × V × V × V, - μ p.1 * μ p.2.1 * μ p.2.2.1 * μ p.2.2.2 * - fourPointDeviation dist p.1 p.2.1 p.2.2.1 p.2.2.2) - ≤ Real.sqrt (3 * - (∑ p : V × V × V × V, - μ p.1 * μ p.2.1 * μ p.2.2.1 * μ p.2.2.2 * - (dist p.1 p.2.1 + dist p.2.2.1 p.2.2.2 - - (dist p.1 p.2.2.1 + dist p.2.1 p.2.2.2))^2)) := by - -- Step 0: ∑_p μ⁴(p) = 1 by iid factorisation - have hμ4_sum : (∑ p : V × V × V × V, μ p.1 * μ p.2.1 * μ p.2.2.1 * μ p.2.2.2) = 1 := by - calc (∑ p : V × V × V × V, μ p.1 * μ p.2.1 * μ p.2.2.1 * μ p.2.2.2) - = ∑ p₁ : V, ∑ p₂ : V, ∑ p₃ : V, ∑ p₄ : V, - μ p₁ * μ p₂ * μ p₃ * μ p₄ := by - rw [Fintype.sum_prod_type] - refine Finset.sum_congr rfl (fun p₁ _ => ?_) - rw [Fintype.sum_prod_type] - refine Finset.sum_congr rfl (fun p₂ _ => ?_) - rw [Fintype.sum_prod_type] - _ = ∑ p₁ : V, ∑ p₂ : V, ∑ p₃ : V, μ p₁ * μ p₂ * μ p₃ := by - refine Finset.sum_congr rfl (fun p₁ _ => ?_) - refine Finset.sum_congr rfl (fun p₂ _ => ?_) - refine Finset.sum_congr rfl (fun p₃ _ => ?_) - rw [← Finset.mul_sum, hμ_sum, mul_one] - _ = ∑ p₁ : V, ∑ p₂ : V, μ p₁ * μ p₂ := by - refine Finset.sum_congr rfl (fun p₁ _ => ?_) - refine Finset.sum_congr rfl (fun p₂ _ => ?_) - rw [← Finset.mul_sum, hμ_sum, mul_one] - _ = ∑ p₁ : V, μ p₁ := by - refine Finset.sum_congr rfl (fun p₁ _ => ?_) - rw [← Finset.mul_sum, hμ_sum, mul_one] - _ = 1 := hμ_sum - -- Step 1: per-tuple bound `Φ² ≤ D₁₂² + D₂₃² + D₁₃²` via - -- `fourPointDeviation_aux_sq_le_sum_sq`, then weighted by μ⁴ and summed. - -- Notation: - -- S₁ := d(p₁,p₂) + d(p₃,p₄) - -- S₂ := d(p₁,p₃) + d(p₂,p₄) - -- S₃ := d(p₁,p₄) + d(p₂,p₃) - -- and D_ij := S_i − S_j, Φ := 2·max + min − (S₁+S₂+S₃). - -- iid symmetry: ∑ μ⁴ D₁₂² = ∑ μ⁴ D₁₃² = ∑ μ⁴ D₂₃². - -- Define `M := ∑ μ⁴ D₁₂²` (target RHS inner). - set M : ℝ := ∑ p : V × V × V × V, - μ p.1 * μ p.2.1 * μ p.2.2.1 * μ p.2.2.2 * - (dist p.1 p.2.1 + dist p.2.2.1 p.2.2.2 - - (dist p.1 p.2.2.1 + dist p.2.1 p.2.2.2))^2 with hM_def - -- μ⁴(p) ≥ 0 pointwise. - have hμ4_nn : ∀ p : V × V × V × V, - 0 ≤ μ p.1 * μ p.2.1 * μ p.2.2.1 * μ p.2.2.2 := fun p => - mul_nonneg (mul_nonneg (mul_nonneg (hμ_nonneg _) (hμ_nonneg _)) (hμ_nonneg _)) - (hμ_nonneg _) - -- D₁₂² ↔ D₁₃² via swap34 (swap positions 3, 4). - have hsym_13 : M = ∑ p : V × V × V × V, - μ p.1 * μ p.2.1 * μ p.2.2.1 * μ p.2.2.2 * - (dist p.1 p.2.1 + dist p.2.2.1 p.2.2.2 - - (dist p.1 p.2.2.2 + dist p.2.1 p.2.2.1))^2 := by - rw [hM_def, ← Equiv.sum_comp (fpdSwap34 V)] - refine Finset.sum_congr rfl (fun p _ => ?_) - change μ p.1 * μ p.2.1 * μ p.2.2.2 * μ p.2.2.1 * - (dist p.1 p.2.1 + dist p.2.2.2 p.2.2.1 - - (dist p.1 p.2.2.2 + dist p.2.1 p.2.2.1))^2 = _ - rw [dist_comm p.2.2.2 p.2.2.1] - ring - -- D₁₂² ↔ D₂₃² via swap13 (swap positions 1, 3). - have hsym_23 : M = ∑ p : V × V × V × V, - μ p.1 * μ p.2.1 * μ p.2.2.1 * μ p.2.2.2 * - (dist p.1 p.2.2.1 + dist p.2.1 p.2.2.2 - - (dist p.1 p.2.2.2 + dist p.2.1 p.2.2.1))^2 := by - rw [hM_def, ← Equiv.sum_comp (fpdSwap13 V)] - refine Finset.sum_congr rfl (fun p _ => ?_) - change μ p.2.2.1 * μ p.2.1 * μ p.1 * μ p.2.2.2 * - (dist p.2.2.1 p.2.1 + dist p.1 p.2.2.2 - - (dist p.2.2.1 p.1 + dist p.2.1 p.2.2.2))^2 = _ - rw [dist_comm p.2.2.1 p.2.1, dist_comm p.2.2.1 p.1] - ring - -- Per-tuple: Φ² ≤ D₁₂² + D₂₃² + D₁₃². - have hPertuple : ∀ p : V × V × V × V, - (fourPointDeviation dist p.1 p.2.1 p.2.2.1 p.2.2.2)^2 - ≤ (dist p.1 p.2.1 + dist p.2.2.1 p.2.2.2 - - (dist p.1 p.2.2.1 + dist p.2.1 p.2.2.2))^2 - + (dist p.1 p.2.2.1 + dist p.2.1 p.2.2.2 - - (dist p.1 p.2.2.2 + dist p.2.1 p.2.2.1))^2 - + (dist p.1 p.2.1 + dist p.2.2.1 p.2.2.2 - - (dist p.1 p.2.2.2 + dist p.2.1 p.2.2.1))^2 := by - intro p - have h := fourPointDeviation_aux_sq_le_sum_sq - (dist p.1 p.2.1 + dist p.2.2.1 p.2.2.2) - (dist p.1 p.2.2.1 + dist p.2.1 p.2.2.2) - (dist p.1 p.2.2.2 + dist p.2.1 p.2.2.1) - unfold fourPointDeviation - nlinarith [h, sq_nonneg - ((dist p.1 p.2.1 + dist p.2.2.1 p.2.2.2) - - (dist p.1 p.2.2.1 + dist p.2.1 p.2.2.2))] - -- Step 2: ∑ μ⁴ Φ² ≤ 3 M (per-tuple + symmetry). - have h_sum_Φ_sq_le : (∑ p : V × V × V × V, - μ p.1 * μ p.2.1 * μ p.2.2.1 * μ p.2.2.2 * - (fourPointDeviation dist p.1 p.2.1 p.2.2.1 p.2.2.2)^2) ≤ 3 * M := by - have h1 : (∑ p : V × V × V × V, - μ p.1 * μ p.2.1 * μ p.2.2.1 * μ p.2.2.2 * - (fourPointDeviation dist p.1 p.2.1 p.2.2.1 p.2.2.2)^2) - ≤ ∑ p : V × V × V × V, - μ p.1 * μ p.2.1 * μ p.2.2.1 * μ p.2.2.2 * - ((dist p.1 p.2.1 + dist p.2.2.1 p.2.2.2 - - (dist p.1 p.2.2.1 + dist p.2.1 p.2.2.2))^2 - + (dist p.1 p.2.2.1 + dist p.2.1 p.2.2.2 - - (dist p.1 p.2.2.2 + dist p.2.1 p.2.2.1))^2 - + (dist p.1 p.2.1 + dist p.2.2.1 p.2.2.2 - - (dist p.1 p.2.2.2 + dist p.2.1 p.2.2.1))^2) := by - refine Finset.sum_le_sum (fun p _ => ?_) - exact mul_le_mul_of_nonneg_left (hPertuple p) (hμ4_nn p) - calc (∑ p : V × V × V × V, - μ p.1 * μ p.2.1 * μ p.2.2.1 * μ p.2.2.2 * - (fourPointDeviation dist p.1 p.2.1 p.2.2.1 p.2.2.2)^2) - ≤ _ := h1 - _ = (∑ p : V × V × V × V, - μ p.1 * μ p.2.1 * μ p.2.2.1 * μ p.2.2.2 * - (dist p.1 p.2.1 + dist p.2.2.1 p.2.2.2 - - (dist p.1 p.2.2.1 + dist p.2.1 p.2.2.2))^2) - + (∑ p : V × V × V × V, - μ p.1 * μ p.2.1 * μ p.2.2.1 * μ p.2.2.2 * - (dist p.1 p.2.2.1 + dist p.2.1 p.2.2.2 - - (dist p.1 p.2.2.2 + dist p.2.1 p.2.2.1))^2) - + (∑ p : V × V × V × V, - μ p.1 * μ p.2.1 * μ p.2.2.1 * μ p.2.2.2 * - (dist p.1 p.2.1 + dist p.2.2.1 p.2.2.2 - - (dist p.1 p.2.2.2 + dist p.2.1 p.2.2.1))^2) := by - rw [← Finset.sum_add_distrib, ← Finset.sum_add_distrib] - refine Finset.sum_congr rfl (fun p _ => ?_) - ring - _ = M + M + M := by rw [← hsym_23, ← hsym_13] - _ = 3 * M := by ring - -- Step 3: Cauchy-Schwarz `(∑ μ⁴ Φ)² ≤ (∑ μ⁴) · (∑ μ⁴ Φ²)`. - have h_Φ_nn : ∀ p : V × V × V × V, 0 ≤ μ p.1 * μ p.2.1 * μ p.2.2.1 * μ p.2.2.2 * - fourPointDeviation dist p.1 p.2.1 p.2.2.1 p.2.2.2 := fun p => - mul_nonneg (hμ4_nn p) (fourPointDeviation_aux_nonneg _ _ _) - have hCS : (∑ p : V × V × V × V, - μ p.1 * μ p.2.1 * μ p.2.2.1 * μ p.2.2.2 * - fourPointDeviation dist p.1 p.2.1 p.2.2.1 p.2.2.2)^2 - ≤ (∑ p : V × V × V × V, μ p.1 * μ p.2.1 * μ p.2.2.1 * μ p.2.2.2) - * (∑ p : V × V × V × V, - μ p.1 * μ p.2.1 * μ p.2.2.1 * μ p.2.2.2 * - (fourPointDeviation dist p.1 p.2.1 p.2.2.1 p.2.2.2)^2) := by - refine Finset.sum_sq_le_sum_mul_sum_of_sq_le_mul - (Finset.univ : Finset (V × V × V × V)) - (fun p _ => hμ4_nn p) - (fun p _ => mul_nonneg (hμ4_nn p) (sq_nonneg _)) - (fun p _ => ?_) - have : (μ p.1 * μ p.2.1 * μ p.2.2.1 * μ p.2.2.2 * - fourPointDeviation dist p.1 p.2.1 p.2.2.1 p.2.2.2)^2 - = (μ p.1 * μ p.2.1 * μ p.2.2.1 * μ p.2.2.2) * - (μ p.1 * μ p.2.1 * μ p.2.2.1 * μ p.2.2.2 * - (fourPointDeviation dist p.1 p.2.1 p.2.2.1 p.2.2.2)^2) := by ring - exact this.le - -- Combine: (∑ μ⁴ Φ)² ≤ 1 · (3 M) = 3 M. - have h_sq_le_3M : (∑ p : V × V × V × V, - μ p.1 * μ p.2.1 * μ p.2.2.1 * μ p.2.2.2 * - fourPointDeviation dist p.1 p.2.1 p.2.2.1 p.2.2.2)^2 ≤ 3 * M := by - calc _ ≤ (∑ p : V × V × V × V, μ p.1 * μ p.2.1 * μ p.2.2.1 * μ p.2.2.2) - * (∑ p : V × V × V × V, - μ p.1 * μ p.2.1 * μ p.2.2.1 * μ p.2.2.2 * - (fourPointDeviation dist p.1 p.2.1 p.2.2.1 p.2.2.2)^2) := hCS - _ = ∑ p : V × V × V × V, - μ p.1 * μ p.2.1 * μ p.2.2.1 * μ p.2.2.2 * - (fourPointDeviation dist p.1 p.2.1 p.2.2.1 p.2.2.2)^2 := by - rw [hμ4_sum, one_mul] - _ ≤ 3 * M := h_sum_Φ_sq_le - -- Step 4: take square root. - have h3M_nn : 0 ≤ 3 * M := by - have hM_nn : 0 ≤ M := by - rw [hM_def] - exact Finset.sum_nonneg (fun p _ => mul_nonneg (hμ4_nn p) (sq_nonneg _)) - linarith - have hLHS_nn : 0 ≤ ∑ p : V × V × V × V, - μ p.1 * μ p.2.1 * μ p.2.2.1 * μ p.2.2.2 * - fourPointDeviation dist p.1 p.2.1 p.2.2.1 p.2.2.2 := - Finset.sum_nonneg (fun p _ => h_Φ_nn p) - exact (Real.le_sqrt hLHS_nn h3M_nn).mpr h_sq_le_3M - -/-- **Math.** *Variance fallback.* In the abstract bound above, -plugging in the trivial inequality -`𝔼_{μ⁴}[(d(u, v) + d(x, y) − d(u, x) − d(v, y))²] ≤ 4 · Var_{μ²}[d]` -(by expanding under iid independence and dropping the -non-negative correction `8 · Var_μ[g]` where -`g(u) := 𝔼_v[d(u, v)]`) yields the variance form -``` - 𝔼_{μ⁴}[Φ] ≤ 2 · √(3 · Var_{μ²}[d]). -``` -**Generally not sharp** — the spectral / structured bound is -typically tighter. -/ -theorem weighted_avg_fourPointDeviation_le_sqrt_variance - {V : Type*} [Fintype V] [PseudoMetricSpace V] - (μ : V → ℝ) (_hμ_nonneg : ∀ v, 0 ≤ μ v) - (_hμ_sum : ∑ v : V, μ v = 1) : - (∑ p : V × V × V × V, - μ p.1 * μ p.2.1 * μ p.2.2.1 * μ p.2.2.2 * - fourPointDeviation dist p.1 p.2.1 p.2.2.1 p.2.2.2) - ≤ 2 * Real.sqrt (3 * - ((∑ p : V × V, μ p.1 * μ p.2 * dist p.1 p.2 ^ 2) - - (∑ p : V × V, μ p.1 * μ p.2 * dist p.1 p.2) ^ 2)) := by - sorry - -/-- **Math.** *Uniform variance corollary.* The variance-form -bound at `μ ≡ 1/|V|` recovers the unweighted Cauchy–Schwarz bound -on the plain uniform-sample average. -/ -theorem avg_fourPointDeviation_le_sqrt_variance - {V : Type*} [Fintype V] [Nonempty V] [PseudoMetricSpace V] : - (∑ p : V × V × V × V, - fourPointDeviation dist p.1 p.2.1 p.2.2.1 p.2.2.2) - / ((Fintype.card V : ℝ) ^ 4) - ≤ 2 * Real.sqrt (3 * - ((∑ p : V × V, dist p.1 p.2 ^ 2) / ((Fintype.card V : ℝ) ^ 2) - - ((∑ p : V × V, dist p.1 p.2) - / ((Fintype.card V : ℝ) ^ 2)) ^ 2)) := by - sorry - -end ProofAtlas diff --git a/lean/ProofAtlas/Hyperbolicity/CommuteTime.lean b/lean/ProofAtlas/Hyperbolicity/CommuteTime.lean deleted file mode 100644 index 3ff75c1..0000000 --- a/lean/ProofAtlas/Hyperbolicity/CommuteTime.lean +++ /dev/null @@ -1,111 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Util.Linter.MathTag -import ProofAtlas.Hyperbolicity.Basic -import ProofAtlas.Metric.CommuteTime.CommuteTime -import Mathlib.Topology.MetricSpace.Bounded - -/-! -# Commute-time distance satisfies `IsGromovHyperbolic` - -Verification that the random-walk commute time `H.commuteTimeDist` -satisfies the abstract δ-hyperbolicity predicate from -`Hyperbolic/Basic.lean`. Parallel to `Hyperbolic.Electrical` for the -electrical/variational metric `d_H`. - -Sibling to: -* `Hyperbolic.Basic` — abstract `IsGromovHyperbolic` predicate. -* `Hyperbolic.Electrical` — verification for the electrical metric. - -Once verified, the abstract consequences (`Hyperbolic.Basic`'s -lemmas on `IsGromovHyperbolic`) apply directly via the local -`MetricSpace V` instance from -`Hypergraph.metricSpace H params ...`. --/ - -namespace Hypergraph - -variable {V : Type*} {C : Type*} - -/-- **Math.** Trivial existence: the random-walk commute time -`(V, commuteTimeDist)` is `δ`-hyperbolic for sufficiently large -`δ` (any `δ ≥ diam` works). The interesting content is in the -*rate* — paper 1's worst-case conjecture -`δ / diam → 0` for layered families. -/ -theorem commuteTimeDist_exists_isGromovHyperbolic - [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (h_row_sum : ∀ i, ∑ j, H.transMatrix params i j = 1) - (h_nonneg : ∀ i j : V, 0 ≤ H.transMatrix params i j) - (h_sum : ∑ i, H.stationaryDist params i = 1) - (h_pos : ∀ w, 0 < H.stationaryDist params w) - (h_conv : Filter.Tendsto (fun k => H.transMatrix params ^ k) - Filter.atTop - (nhds (ProofAtlas.RandomWalk.stationaryProjection - (H.stationaryDist params)))) : - letI := H.metricSpace params h_row_sum h_nonneg h_sum h_pos h_conv - ∃ δ : ℝ, ProofAtlas.IsGromovHyperbolic V δ := by - letI := H.metricSpace params h_row_sum h_nonneg h_sum h_pos h_conv - refine ⟨Metric.diam (Set.univ : Set V), - ProofAtlas.IsGromovHyperbolic.of_bounded_dist _ ?_⟩ - intro a b - exact Metric.dist_le_diam_of_mem - (Set.toFinite (Set.univ : Set V)).isBounded - (Set.mem_univ a) (Set.mem_univ b) - -/-! ## Hyperbolicity constants for the commute-time metric (paper 1 §4) -/ - -open ProofAtlas - -/-- **Math.** Gromov hyperbolicity constant `δ` of the BDF commute -time metric: half the supremum, over all four-tuples `(u, v, x, y)`, -of the four-point deviation. Paper 1 Definition 4.2. -/ -noncomputable def hyperbolicityConstant [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) : ℝ := - (⨆ p : V × V × V × V, - fourPointDeviation (H.commuteTimeDist params) p.1 p.2.1 p.2.2.1 p.2.2.2) / 2 - -/-- **Math.** Diameter of the BDF commute time metric on `V`. -/ -noncomputable def diameter [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) : ℝ := - ⨆ p : V × V, H.commuteTimeDist params p.1 p.2 - -/-- **Math.** Scale-invariant hyperbolicity: `δ / diameter`. Set to -`0` when the diameter is `0` (i.e.\ `V` is empty or trivial). -/ -noncomputable def normalizedHyperbolicity [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) : ℝ := - if H.diameter params = 0 then 0 - else H.hyperbolicityConstant params / H.diameter params - -/-- **Math.** Universal four-point inequality: every four-tuple -`(u, v, x, y)` satisfies `fourPointDeviation ≤ 2 δ`. -/ -theorem gromov_hyperbolic [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) : - ∀ u v x y : V, - fourPointDeviation (H.commuteTimeDist params) u v x y - ≤ 2 * H.hyperbolicityConstant params := by - intro u v x y - unfold hyperbolicityConstant - set f : V × V × V × V → ℝ := fun p => - fourPointDeviation (H.commuteTimeDist params) p.1 p.2.1 p.2.2.1 p.2.2.2 with hf - change f (u, v, x, y) ≤ 2 * ((⨆ p, f p) / 2) - have hSup : f (u, v, x, y) ≤ ⨆ p, f p := - le_ciSup (Set.Finite.bddAbove (Set.finite_range _)) (u, v, x, y) - linarith - -/-- **Math.** The hyperbolicity constant `δ` is finite (the -`EReal` coercion of any real is strictly less than `⊤`). -/ -theorem hyperbolicityConstant_finite [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) : - (H.hyperbolicityConstant params : EReal) < ⊤ := EReal.coe_lt_top _ - -end Hypergraph diff --git a/lean/ProofAtlas/Hyperbolicity/Default.lean b/lean/ProofAtlas/Hyperbolicity/Default.lean deleted file mode 100644 index eec1a30..0000000 --- a/lean/ProofAtlas/Hyperbolicity/Default.lean +++ /dev/null @@ -1,28 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Hyperbolicity.Basic -import ProofAtlas.Hyperbolicity.Resistance -import ProofAtlas.Hyperbolicity.CommuteTime - -/-! -# Gromov δ-hyperbolicity — facade - -Standalone module for Gromov δ-hyperbolicity. The abstract layer -applies to any `PseudoMetricSpace`; per-metric verifications and -their applications live in sibling files. - -* `Hyperbolic.Basic` — abstract `IsGromovHyperbolic` predicate plus - the `gromovProduct` machinery and the equivalence of the four-point - and Gromov-product formulations. -* `Hyperbolic.Electrical` — verification that the electrical / - variational metric `(V, d_H)` satisfies the abstract predicate. -* `Hyperbolic.RandomWalk` — verification that the random-walk - commute time `(V, commuteTimeDist)` satisfies the abstract - predicate (paper 1's metric). - -Both verifications use the same abstract consequences from -`Hyperbolic.Basic`; the two metrics share the same machinery. --/ diff --git a/lean/ProofAtlas/Hyperbolicity/Diffusion.lean b/lean/ProofAtlas/Hyperbolicity/Diffusion.lean deleted file mode 100644 index 2c26ea3..0000000 --- a/lean/ProofAtlas/Hyperbolicity/Diffusion.lean +++ /dev/null @@ -1,47 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Util.Linter.MathTag -import ProofAtlas.Hyperbolicity.Basic -import ProofAtlas.Metric.Diffusion.Basic -import Mathlib.Topology.MetricSpace.Bounded - -/-! -# `diffusionDist` satisfies `IsGromovHyperbolic` - -Verification that the BDF diffusion distance `(V, d_t)` at any scale -`t > 0` satisfies the abstract δ-hyperbolicity predicate from -`Hyperbolic/Basic.lean`. Parallel to `Hyperbolic.Electrical` and -`Hyperbolic.RandomWalk`. - -Same trivial-witness pattern: any finite-vertex metric space is -`δ`-hyperbolic for `δ ≥ diam`; the substantive content (the *rate* -of `δ` as `|V|` grows, possibly as a function of `t`) is future -work. -/ - -namespace Hypergraph - -variable {V : Type*} {C : Type*} - -/-- **Math.** Trivial existence: for any scale `t > 0`, the BDF -diffusion distance `(V, d_t)` is `δ`-hyperbolic for sufficiently -large `δ`. Concrete witness: `δ := Metric.diam (Set.univ : Set V)` -under the local `PseudoMetricSpace V` instance from -`H.toPseudoMetricSpaceDiffusion params t`. -/ -theorem diffusionDist_exists_isGromovHyperbolic - [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (t : ℝ) (_ht : 0 < t) : - letI := H.toPseudoMetricSpaceDiffusion params t - ∃ δ : ℝ, ProofAtlas.IsGromovHyperbolic V δ := by - letI := H.toPseudoMetricSpaceDiffusion params t - refine ⟨Metric.diam (Set.univ : Set V), - ProofAtlas.IsGromovHyperbolic.of_bounded_dist _ ?_⟩ - intro a b - exact Metric.dist_le_diam_of_mem - (Set.toFinite (Set.univ : Set V)).isBounded - (Set.mem_univ a) (Set.mem_univ b) - -end Hypergraph diff --git a/lean/ProofAtlas/Hyperbolicity/Resistance.lean b/lean/ProofAtlas/Hyperbolicity/Resistance.lean deleted file mode 100644 index b9aeb0e..0000000 --- a/lean/ProofAtlas/Hyperbolicity/Resistance.lean +++ /dev/null @@ -1,46 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Util.Linter.MathTag -import ProofAtlas.Hyperbolicity.Basic -import ProofAtlas.Metric.Resistance.Canonical -import ProofAtlas.Metric.Resistance.Algebraic -import Mathlib.Topology.MetricSpace.Bounded - -/-! -# `d_H` satisfies `IsGromovHyperbolic` - -Verification that the BDF distance `(V, d_H)` satisfies the abstract -δ-hyperbolicity predicate from `Hyperbolic/Basic.lean`. This file -contains only the verification theorem — no new vocabulary, no -BDF-specific re-statement. Once verified, the abstract consequences -(`Hyperbolic/Basic.lean`'s lemmas on `IsGromovHyperbolic`) apply -directly via `letI := H.toPseudoMetricSpace params`. --/ - -namespace Hypergraph - -variable {V : Type*} {C : Type*} - -/-- **Math.** Trivial existence: any finite-vertex BDF hypergraph is -`δ`-hyperbolic for sufficiently large `δ`. Concrete witness: -`δ := Metric.diam (Set.univ : Set V)`. The interesting content is in -the *rate* — how small `δ` can be taken as `|V|` grows; see the -worst-case conjecture in the paper. -/ -theorem exists_isGromovHyperbolic - [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) : - letI := H.toPseudoMetricSpace params - ∃ δ : ℝ, ProofAtlas.IsGromovHyperbolic V δ := by - letI := H.toPseudoMetricSpace params - refine ⟨Metric.diam (Set.univ : Set V), - ProofAtlas.IsGromovHyperbolic.of_bounded_dist _ ?_⟩ - intro a b - exact Metric.dist_le_diam_of_mem - (Set.toFinite (Set.univ : Set V)).isBounded - (Set.mem_univ a) (Set.mem_univ b) - -end Hypergraph diff --git a/lean/ProofAtlas/Hypergraph/Sequence.lean b/lean/ProofAtlas/Hypergraph/Sequence.lean deleted file mode 100644 index f1a1d65..0000000 --- a/lean/ProofAtlas/Hypergraph/Sequence.lean +++ /dev/null @@ -1,203 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Util.Linter.MathTag -import ProofAtlas.Hypergraph.Basic -import ProofAtlas.Hypergraph.Walk -import ProofAtlas.Metric.Resistance.Canonical -import ProofAtlas.Metric.Resistance.Algebraic -import ProofAtlas.Metric.Resistance.Structural -import ProofAtlas.Metric.GromovHausdorff -import Mathlib.Topology.MetricSpace.GromovHausdorff - -/-! -# Growing sequences of BDF hypergraphs - -A `BDFSequence V C` is a `ℕ`-indexed family of BDF hypergraphs over -the same vertex type `V` and colour palette `C`, monotone in the -edge set: edges are only added, never removed. The motivating -picture is a *versioned proof system*: each new version of (say) the -Mathlib dependency graph adds new theorems / inference rules without -removing existing ones. - -The `monotone` field is realised as the existing structural notion -`Hypergraph.IsSubHypergraph` from `Combinatorial/Hypergraph.lean` -(injection of edges preserving per-edge data). - -## Why this matters - -Combined with the Rayleigh-monotonicity property -(`resistanceDist_rayleigh` in `Metric/Electrical/Structural.lean`), -the resistance metric is **decreasing** along the sequence: -`n ≤ m → d_{Hₘ}(u, v) ≤ d_{Hₙ}(u, v)`. - -This is the structural input to Gromov–Hausdorff precompactness -arguments: the diameters of the `(V, d_{Hₙ})` metric spaces are -bounded above, so the sequence lies in a totally bounded family of -GH-classes and admits convergent subsequences. - -The convergence statement itself is left as a forward declaration -(`sorry`) — the GH-side machinery (`GromovHausdorff.ghDist` -convergence, completeness of `GHSpace`) is in Mathlib but -instantiating it for our concrete sequence requires a -`MetricSpace V` (not just `PseudoMetricSpace`), which in turn -requires the Klein–Randić triangle inequality plus the -positive-definiteness step (`d(u, v) = 0 → u = v` for connected H). -Both routes are tracked elsewhere. - -## Main definitions - -* `BDFSequence V C` — growing family of BDF hypergraphs on `V`. -* `BDFSequence.distMonotone` — Rayleigh corollary - `n ≤ m → d_{Hₘ} ≤ d_{Hₙ}`, currently a forward declaration. - -## Future work - -* `ghSequence` mapping `n ↦ GHSpace`-class of `(V, d_{Hₙ})` (needs - full `MetricSpace V`, currently sorry'd). -* GH-precompactness of the image of `ghSequence`. -* Limit points of `ghSequence` under monotone edge addition. --/ - -universe u v w - -/-- **Math.** A monotone-in-`ℕ` family of BDF hypergraphs over a -fixed vertex type `V` and colour palette `C`. Models a versioned -proof system in which inference rules are only added, never removed. - -The edge-type universe `w` is fixed across the whole sequence (every -`hypergraph n` has `edges : Type w`), so the per-step -`IsSubHypergraph` witness can talk about edge inclusions without -universe lift. -/ -structure BDFSequence (V : Type u) (C : Type v) where - /-- The `n`-th hypergraph in the sequence. -/ - hypergraph : ℕ → Hypergraph.{u, v, w} V C - /-- Each step only adds hyperedges (the previous edge set embeds - into the next, with matching per-edge data). -/ - monotone : ∀ n, Hypergraph.IsSubHypergraph (hypergraph n) (hypergraph (n + 1)) - -namespace BDFSequence - -variable {V : Type u} {C : Type v} - -/-- **Math.** Reflexivity helper: every BDF hypergraph is a -sub-hypergraph of itself via the identity edge embedding. Kept -`private` because the real composition API lives in -`Combinatorial/Hypergraph.lean`; this is just enough machinery to -run the `Nat.le_induction` below. -/ -private def isSubHypergraph_refl_aux (H : Hypergraph.{u, v, w} V C) : - Hypergraph.IsSubHypergraph H H where - toFun := id - injective := Function.injective_id - edge_eq := fun _ => rfl - -/-- **Math.** Transitivity helper: composing two `IsSubHypergraph` -witnesses gives a sub-hypergraph witness through the composite. Kept -`private` for the same reason as `isSubHypergraph_refl_aux`. -/ -private def isSubHypergraph_trans_aux {H₁ H₂ H₃ : Hypergraph.{u, v, w} V C} - (s : Hypergraph.IsSubHypergraph H₁ H₂) - (t : Hypergraph.IsSubHypergraph H₂ H₃) : - Hypergraph.IsSubHypergraph H₁ H₃ where - toFun := t.toFun ∘ s.toFun - injective := t.injective.comp s.injective - edge_eq := fun e => (t.edge_eq (s.toFun e)).trans (s.edge_eq e) - -/-- **Math.** Composing successive `IsSubHypergraph` witnesses gives -an inclusion `H_n ⊆ H_m` for any `n ≤ m`. (`IsSubHypergraph` is a -structure carrying an edge embedding function, hence a `def`, not a -`theorem`.) -/ -noncomputable def isSubHypergraph_of_le (seq : BDFSequence V C) - {n m : ℕ} (h : n ≤ m) : - Hypergraph.IsSubHypergraph (seq.hypergraph n) (seq.hypergraph m) := by - -- `Nat.leRec` is the `Sort*`-valued analogue of `Nat.le_induction`; - -- we need it (not `induction h`) because `IsSubHypergraph` lives in - -- `Type`, not `Prop`. We elaborate it via `refine` so the - -- `@[elab_as_elim]` motive inference fires. - refine Nat.leRec - (motive := fun k _ => - Hypergraph.IsSubHypergraph (seq.hypergraph n) (seq.hypergraph k)) - ?_ ?_ h - · exact isSubHypergraph_refl_aux _ - · intro k _ ih - exact isSubHypergraph_trans_aux ih (seq.monotone k) - -/-- **Math.** *Distance is monotone non-increasing along the -sequence.* By Rayleigh monotonicity (paper 2 §5.2, -`resistanceDist_rayleigh`), adding hyperedges can only shorten -resistance distances. Hence for `n ≤ m`, -`d_{Hₘ}(u, v) ≤ d_{Hₙ}(u, v)`. - -**Signature note (iter-014 amendment).** `resistanceDist_rayleigh` -(iter-013 Obj 3b) carries five connectivity hypotheses: `u ≠ v`, -positivity of both resistance distances, and `signedIndicator u v` -lying in the range of each harmonic Laplacian. These hypotheses are -unavoidable here: without them the lemma is *false* (a sequence -that connects `u, v` only at step `m` has `d_{Hₙ}(u,v) = 0` by the -disconnected-case convention while `d_{Hₘ}(u,v) > 0`). The five -hypotheses are automatic in the realistic paper 2 §5 setting where -each hypergraph in the sequence connects `u` and `v` in its -harmonic graph. -/ -theorem resistanceDist_antitone [Fintype V] [DecidableEq V] - (seq : BDFSequence V C) - (hFin : ∀ n, Fintype (seq.hypergraph n).edges) - (params : WalkParams C) {n m : ℕ} (hnm : n ≤ m) (u v : V) - (huv : u ≠ v) - (h_pos_m : - letI := hFin m - 0 < (seq.hypergraph m).resistanceDist params u v) - (h_pos_n : - letI := hFin n - 0 < (seq.hypergraph n).resistanceDist params u v) - (h_range_m : - letI := hFin m - ∃ y : V → ℝ, - ((seq.hypergraph m).harmonicLaplacian params).mulVec y - = ProofAtlas.LinearAlgebra.signedIndicator u v) - (h_range_n : - letI := hFin n - ∃ y : V → ℝ, - ((seq.hypergraph n).harmonicLaplacian params).mulVec y - = ProofAtlas.LinearAlgebra.signedIndicator u v) : - letI := hFin m - letI := hFin n - (seq.hypergraph m).resistanceDist params u v - ≤ (seq.hypergraph n).resistanceDist params u v := by - classical - letI : Fintype (seq.hypergraph m).edges := hFin m - letI : Fintype (seq.hypergraph n).edges := hFin n - exact (seq.hypergraph m).resistanceDist_rayleigh (seq.hypergraph n) - params (seq.isSubHypergraph_of_le hnm) u v huv - h_pos_m h_pos_n h_range_m h_range_n - -end BDFSequence - -/-! ## Gromov–Hausdorff sequence (sketch) - -Map each `n` to the GH-class of `(V, d_{Hₙ})`. Requires a full -`MetricSpace V` instance per `n`, which currently routes through -the sorry'd Klein–Randić triangle inequality and the connectivity- -based eq-of-dist-zero. Placeholder forward declaration. -/ - -namespace BDFSequence - -variable {V : Type u} {C : Type v} - -/-- **Math.** GH-class of `(V, d_{Hₙ})` for each `n` (forward -declaration). When the BDF metric is upgraded from -`PseudoMetricSpace` to `MetricSpace` (via `resistanceDist_triangle` -plus the connected-then-eq-of-dist-zero argument), and `V` is -`Nonempty`, this becomes a concrete function -`ℕ → GromovHausdorff.GHSpace`. - -The implementation will require local `[Fintype V] [DecidableEq V] -[Nonempty V]` plus a per-`n` `Fintype (seq.hypergraph n).edges`; we -omit them from the signature for now because the placeholder body -does not use them. -/ -noncomputable def ghSequence (_seq : BDFSequence V C) - (_params : WalkParams C) : - ℕ → GromovHausdorff.GHSpace := by - sorry - -end BDFSequence diff --git a/lean/ProofAtlas/Metric/CommuteTime/CommuteTime.lean b/lean/ProofAtlas/Metric/CommuteTime/CommuteTime.lean deleted file mode 100644 index dda539c..0000000 --- a/lean/ProofAtlas/Metric/CommuteTime/CommuteTime.lean +++ /dev/null @@ -1,346 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Util.Linter.MathTag -import ProofAtlas.Metric.CommuteTime.HittingTime -import ProofAtlas.Metric.Axioms -import Mathlib.Topology.MetricSpace.Basic - -/-! -# Commute time metric on a BDF hypergraph - -The commute time `d(u, v) := h(u, v) + h(v, u)` and its `MetricSpace` -structure on the vertex set `V`. - -## Main definitions - -* `Hypergraph.commuteTimeDist` — commute time `d(u, v)`. -* `Hypergraph.metricSpace` — bridging `MetricSpace` instance on - `V` (engineering wrapper around `commuteTimeDist`). - -## Main results (TODO) - -* `Hypergraph.commuteTimeDist_self` — `d(v, v) = 0`. -* `Hypergraph.commuteTimeDist_pos` — `d(u, v) > 0` when `u ≠ v`. -* `Hypergraph.commuteTimeDist_symm` — symmetry `d(u, v) = d(v, u)`. -* `Hypergraph.commuteTimeDist_triangle` — triangle inequality. --/ - -namespace Hypergraph - -variable {V : Type*} {C : Type*} - -/-- **Math.** Commute time `d(u, v) := h(u, v) + h(v, u)`. -/ -noncomputable def commuteTimeDist [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (u v : V) : ℝ := - H.hittingTime params u v + H.hittingTime params v u - -theorem commuteTimeDist_self [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (h_row_sum : ∀ i, ∑ j, H.transMatrix params i j = 1) - (h_nonneg : ∀ i j : V, 0 ≤ H.transMatrix params i j) - (h_sum : ∑ i, H.stationaryDist params i = 1) - (h_pos : ∀ w, 0 < H.stationaryDist params w) - (h_conv : Filter.Tendsto (fun k => H.transMatrix params ^ k) - Filter.atTop - (nhds (ProofAtlas.RandomWalk.stationaryProjection - (H.stationaryDist params)))) - (v : V) : - H.commuteTimeDist params v v = 0 := by - unfold commuteTimeDist - rw [H.hittingTime_self params h_row_sum h_nonneg h_sum h_pos h_conv v, - add_zero] - -theorem commuteTimeDist_pos [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (h_row_sum : ∀ i, ∑ j, H.transMatrix params i j = 1) - (h_nonneg : ∀ i j : V, 0 ≤ H.transMatrix params i j) - (h_sum : ∑ i, H.stationaryDist params i = 1) - (h_pos : ∀ w, 0 < H.stationaryDist params w) - (h_conv : Filter.Tendsto (fun k => H.transMatrix params ^ k) - Filter.atTop - (nhds (ProofAtlas.RandomWalk.stationaryProjection - (H.stationaryDist params)))) - {u v : V} (h : u ≠ v) : - 0 < H.commuteTimeDist params u v := by - unfold commuteTimeDist - have h1 : 0 < H.hittingTime params u v := - H.hittingTime_pos params h_row_sum h_nonneg h_sum h_pos h_conv h - have h2 : 0 ≤ H.hittingTime params v u := - H.hittingTime_nonneg params h_row_sum h_nonneg h_sum h_pos h_conv v u - linarith - -theorem commuteTimeDist_symm [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (u v : V) : - H.commuteTimeDist params u v = H.commuteTimeDist params v u := by - unfold commuteTimeDist - exact add_comm _ _ - -theorem commuteTimeDist_triangle [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (h_row_sum : ∀ i, ∑ j, H.transMatrix params i j = 1) - (h_nonneg : ∀ i j : V, 0 ≤ H.transMatrix params i j) - (h_sum : ∑ i, H.stationaryDist params i = 1) - (h_pos : ∀ w, 0 < H.stationaryDist params w) - (h_conv : Filter.Tendsto (fun k => H.transMatrix params ^ k) - Filter.atTop - (nhds (ProofAtlas.RandomWalk.stationaryProjection - (H.stationaryDist params)))) - (u v w : V) : - H.commuteTimeDist params u w ≤ - H.commuteTimeDist params u v + H.commuteTimeDist params v w := by - unfold commuteTimeDist - have h1 := H.hittingTime_triangle params h_row_sum h_nonneg h_sum - h_pos h_conv u w v - have h2 := H.hittingTime_triangle params h_row_sum h_nonneg h_sum - h_pos h_conv w u v - linarith - -/-- **Eng.** Bridging `MetricSpace` instance on `V` from -`commuteTimeDist`. Non-canonical: depends on the choice of `H`, `params`, -and the ergodic hypotheses. -/ -@[reducible] -noncomputable def metricSpace [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (h_row_sum : ∀ i, ∑ j, H.transMatrix params i j = 1) - (h_nonneg : ∀ i j : V, 0 ≤ H.transMatrix params i j) - (h_sum : ∑ i, H.stationaryDist params i = 1) - (h_pos : ∀ w, 0 < H.stationaryDist params w) - (h_conv : Filter.Tendsto (fun k => H.transMatrix params ^ k) - Filter.atTop - (nhds (ProofAtlas.RandomWalk.stationaryProjection - (H.stationaryDist params)))) : MetricSpace V where - dist := H.commuteTimeDist params - dist_self := H.commuteTimeDist_self params h_row_sum h_nonneg h_sum h_pos h_conv - dist_comm := H.commuteTimeDist_symm params - dist_triangle := H.commuteTimeDist_triangle params h_row_sum h_nonneg - h_sum h_pos h_conv - eq_of_dist_eq_zero {x y} hd := by - by_contra hne - exact absurd hd - (ne_of_gt (H.commuteTimeDist_pos params h_row_sum h_nonneg h_sum - h_pos h_conv hne)) - -/-! ### Existence: commute time satisfies the HypergraphMetric axioms - -Skeleton for the v2 migration. The earlier v1 instance (with the -old `HypergraphMetric` structure indexed by `Fin (inArity e) → Fin (outArity e)`) -proved all four axioms algebraically from the harmonic schedule -`κ e i j := α(c(e)) / ((i+1) · outArity e)`. The v2 `HypergraphMetric` -quantifies `κ` over `V → V → ℝ` with a bipartite axiom, so the -existence witness is rebuilt below: `κ e u v` picks out the input -index `i` such that `H.inVertex e i = u` (when one exists, via -classical choice) and assigns `α(c(e)) / ((i+1) · outArity e)` when -`v` is an output of the same edge, `0` otherwise. The four axiom -proofs are stubbed; filling them is left to a follow-up session. -/ - -/-- **Math.** The asymmetric (input → output) half of the BDF commute -time harmonic schedule: for an edge `e` whose `i`-th input is `u` -and whose output set contains `v`, this returns the v1 weight -`α(c(e)) / ((i+1) · outArity e)`; all other (`u`, `v`) pairs return `0`. -The full symmetric conductance `commuteTimeKappa` is the symmetrisation -`ioWeight e u v + ioWeight e v u`. -/ -noncomputable def ioWeight [Fintype V] [DecidableEq V] - (H : Hypergraph V C) (params : WalkParams C) - (e : H.edges) (u v : V) : ℝ := - if hu : ∃ i : Fin (H.inArity e), H.inVertex e i = u then - if H.isOutput v e then - params.alpha (H.color e) / - (((hu.choose.val + 1) * H.outArity e : ℕ) : ℝ) - else 0 - else 0 - -/-- **Math.** The (symmetric) harmonic conductance schedule realising -the BDF commute time as a `HypergraphMetric`: for an edge `e` whose `i`-th -input is `u` and whose output set contains `v` (or vice versa), the -conductance contributed is `α(c(e)) / ((i+1) · outArity e)`; all -non-(input,output) pairs contribute `0`. Defined as the symmetrisation -of `ioWeight` so that `κ e u v = κ e v u` holds by `add_comm`. The -value on (input_i, output) pairs is preserved from the v1 schedule -`κ e i j := α(c(e)) / ((i+1) · outArity e)`; the (output, input_i) -pairs now also carry this value (rather than `0` as in v1), restoring -symmetry of the conductance matrix. -/ -noncomputable def commuteTimeKappa [Fintype V] [DecidableEq V] - (H : Hypergraph V C) (params : WalkParams C) - (e : H.edges) (u v : V) : ℝ := - ioWeight H params e u v + ioWeight H params e v u - -/-! ### `ioWeight` helper lemmas - -The asymmetric harmonic weight `ioWeight` is the workhorse for the -four HypergraphMetric axiom proofs of `commuteTimeHypergraphMetric` (plus the new -symmetry axiom). The helpers below isolate the case splits on -"first arg is an input" / "second arg is an output". -/ - -/-- `ioWeight e u v = 0` when `u` is not an input of `e`. -/ -private lemma ioWeight_of_not_input [Fintype V] [DecidableEq V] - (H : Hypergraph V C) (params : WalkParams C) (e : H.edges) - {u : V} (hu : ¬ ∃ i : Fin (H.inArity e), H.inVertex e i = u) (v : V) : - ioWeight H params e u v = 0 := by - unfold ioWeight - exact dif_neg hu - -/-- `ioWeight e u v = 0` when `v` is not an output of `e`. -/ -private lemma ioWeight_of_not_output [Fintype V] [DecidableEq V] - (H : Hypergraph V C) (params : WalkParams C) (e : H.edges) - (u : V) {v : V} (hv : ¬ H.isOutput v e) : - ioWeight H params e u v = 0 := by - unfold ioWeight - by_cases hu : ∃ i : Fin (H.inArity e), H.inVertex e i = u - · rw [dif_pos hu, if_neg hv] - · rw [dif_neg hu] - -/-- Outputs of an edge are never inputs of the same edge (per -`inputOutputDisjoint`). -/ -private lemma not_isInput_of_isOutput - (H : Hypergraph V C) (e : H.edges) {v : V} (h : H.isOutput v e) : - ¬ ∃ i : Fin (H.inArity e), H.inVertex e i = v := by - rintro ⟨i, hi⟩ - obtain ⟨j, hj⟩ := h - exact H.inputOutputDisjoint e i j (hi.trans hj.symm) - -/-- Inputs of an edge are never outputs of the same edge (per -`inputOutputDisjoint`). -/ -private lemma not_isOutput_of_isInput - (H : Hypergraph V C) (e : H.edges) {u : V} - (h : ∃ i : Fin (H.inArity e), H.inVertex e i = u) : - ¬ H.isOutput u e := by - rintro ⟨j, hj⟩ - obtain ⟨i, hi⟩ := h - exact H.inputOutputDisjoint e i j (hi.trans hj.symm) - -/-- The value of `ioWeight e u v` when `u` is the `i`-th input of `e` -and `v` is an output: the v1 harmonic weight. Crucially uses -`inVertex_injective` to identify `Classical.choose` with the literal -input index `i`. -/ -private lemma ioWeight_apply_input_output [Fintype V] [DecidableEq V] - (H : Hypergraph V C) (params : WalkParams C) (e : H.edges) - {v : V} (i : Fin (H.inArity e)) (hv : H.isOutput v e) : - ioWeight H params e (H.inVertex e i) v = - params.alpha (H.color e) / - (((i.val + 1) * H.outArity e : ℕ) : ℝ) := by - unfold ioWeight - have hu : ∃ k : Fin (H.inArity e), H.inVertex e k = H.inVertex e i := ⟨i, rfl⟩ - rw [dif_pos hu, if_pos hv] - have h_choose : hu.choose = i := H.inVertex_injective e hu.choose_spec - rw [h_choose] - -/-- **Math.** Definition 3.1's commute time satisfies the five BDF metric -axioms (D), (O), (C), (A), (S): the BDF commute time is *a* HypergraphMetric. -The conductance witness is `commuteTimeKappa`, the symmetrisation of -`ioWeight`; symmetry is `add_comm`, and the remaining four axioms -reduce to their v1 counterparts via the `ioWeight` helpers above. -/ -noncomputable def commuteTimeHypergraphMetric [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) : HypergraphMetric H params where - κ := commuteTimeKappa H params - bipartite := by - intro e u v h - show commuteTimeKappa H params e u v = 0 - unfold commuteTimeKappa - rcases h with ⟨huin, hvin⟩ | ⟨huout, hvout⟩ - · -- u, v both inputs ⇒ neither is an output - have hu_not_out : ¬ H.isOutput u e := not_isOutput_of_isInput H e huin - have hv_not_out : ¬ H.isOutput v e := not_isOutput_of_isInput H e hvin - rw [ioWeight_of_not_output H params e u hv_not_out, - ioWeight_of_not_output H params e v hu_not_out, add_zero] - · -- u, v both outputs ⇒ neither is an input - have hu_not_in : ¬ ∃ i : Fin (H.inArity e), H.inVertex e i = u := - not_isInput_of_isOutput H e huout - have hv_not_in : ¬ ∃ i : Fin (H.inArity e), H.inVertex e i = v := - not_isInput_of_isOutput H e hvout - rw [ioWeight_of_not_input H params e hu_not_in v, - ioWeight_of_not_input H params e hv_not_in u, add_zero] - monotone := by - intro e i j hij - refine Finset.sum_le_sum (fun v _ => ?_) - change commuteTimeKappa H params e (H.inVertex e j) v - ≤ commuteTimeKappa H params e (H.inVertex e i) v - unfold commuteTimeKappa - -- The "reverse" half: `ioWeight v (inVertex e _)` requires `inVertex e _` - -- to be an output of `e`, but inputs are not outputs ⇒ this half is `0`. - have h_i_not_out : ¬ H.isOutput (H.inVertex e i) e := - not_isOutput_of_isInput H e ⟨i, rfl⟩ - have h_j_not_out : ¬ H.isOutput (H.inVertex e j) e := - not_isOutput_of_isInput H e ⟨j, rfl⟩ - rw [ioWeight_of_not_output H params e v h_i_not_out, - ioWeight_of_not_output H params e v h_j_not_out, add_zero, add_zero] - -- Forward half: classic v1 antitonicity on (input_i, output) pairs. - by_cases hv : H.isOutput v e - · rw [ioWeight_apply_input_output H params e i hv, - ioWeight_apply_input_output H params e j hv] - apply div_le_div_of_nonneg_left (params.alpha_pos _).le - · have hpos : 0 < (i.val + 1) * H.outArity e := - Nat.mul_pos (Nat.succ_pos _) (H.outArity_pos e) - exact_mod_cast hpos - · have hle : (i.val + 1) * H.outArity e ≤ (j.val + 1) * H.outArity e := - Nat.mul_le_mul_right _ (Nat.add_le_add_right hij _) - exact_mod_cast hle - · rw [ioWeight_of_not_output H params e (H.inVertex e i) hv, - ioWeight_of_not_output H params e (H.inVertex e j) hv] - colorSep := by - -- Factor out α from each `ioWeight` separately; the resulting - -- position function is the symmetrisation of the v1 shape. - refine ⟨fun e u v => - (if hu : ∃ k : Fin (H.inArity e), H.inVertex e k = u then - if H.isOutput v e then - (((hu.choose.val + 1) * H.outArity e : ℕ) : ℝ)⁻¹ - else 0 - else 0) + - (if hv : ∃ k : Fin (H.inArity e), H.inVertex e k = v then - if H.isOutput u e then - (((hv.choose.val + 1) * H.outArity e : ℕ) : ℝ)⁻¹ - else 0 - else 0), ?_⟩ - intro e u v - show commuteTimeKappa H params e u v = _ - unfold commuteTimeKappa ioWeight - rw [mul_add] - congr 1 - · by_cases hu : ∃ k : Fin (H.inArity e), H.inVertex e k = u - · rw [dif_pos hu, dif_pos hu] - by_cases hv : H.isOutput v e - · rw [if_pos hv, if_pos hv, div_eq_mul_inv] - · rw [if_neg hv, if_neg hv, mul_zero] - · rw [dif_neg hu, dif_neg hu, mul_zero] - · by_cases hv : ∃ k : Fin (H.inArity e), H.inVertex e k = v - · rw [dif_pos hv, dif_pos hv] - by_cases hu : H.isOutput u e - · rw [if_pos hu, if_pos hu, div_eq_mul_inv] - · rw [if_neg hu, if_neg hu, mul_zero] - · rw [dif_neg hv, dif_neg hv, mul_zero] - outputUnif := by - intro e u j j' - change commuteTimeKappa H params e u (H.outVertex e j) - = commuteTimeKappa H params e u (H.outVertex e j') - unfold commuteTimeKappa - -- Reverse half: `ioWeight (outVertex e _) u` requires `outVertex e _` - -- to be an input, but outputs are not inputs ⇒ this half is `0`. - have h_outV_j_not_in : ¬ ∃ k : Fin (H.inArity e), H.inVertex e k = H.outVertex e j := - not_isInput_of_isOutput H e ⟨j, rfl⟩ - have h_outV_j'_not_in : ¬ ∃ k : Fin (H.inArity e), H.inVertex e k = H.outVertex e j' := - not_isInput_of_isOutput H e ⟨j', rfl⟩ - rw [ioWeight_of_not_input H params e h_outV_j_not_in u, - ioWeight_of_not_input H params e h_outV_j'_not_in u, add_zero, add_zero] - -- Forward half: `ioWeight u (outVertex e j)` doesn't depend on `j`. - have hj : H.isOutput (H.outVertex e j) e := ⟨j, rfl⟩ - have hj' : H.isOutput (H.outVertex e j') e := ⟨j', rfl⟩ - unfold ioWeight - by_cases hu : ∃ k : Fin (H.inArity e), H.inVertex e k = u - · rw [dif_pos hu, dif_pos hu, if_pos hj, if_pos hj'] - · rw [dif_neg hu, dif_neg hu] - symm := by - intro e u v - show commuteTimeKappa H params e u v = commuteTimeKappa H params e v u - unfold commuteTimeKappa - exact add_comm _ _ - -end Hypergraph diff --git a/lean/ProofAtlas/Metric/CommuteTime/HittingTime.lean b/lean/ProofAtlas/Metric/CommuteTime/HittingTime.lean deleted file mode 100644 index 135cc1c..0000000 --- a/lean/ProofAtlas/Metric/CommuteTime/HittingTime.lean +++ /dev/null @@ -1,459 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Util.Linter.MathTag -import ProofAtlas.Metric.CommuteTime.KilledChain - -/-! -# Hitting times of the BDF random walk - -The expected first-passage time `h(u, v)` and its basic properties. -`hittingTime` is defined by extracting a component of the matrix-form -solution `hittingTimeVec` constructed in -`ProofAtlas.Metric.CommuteTime.KilledChain`. - -## Main definitions - -* `Hypergraph.hittingTime` — `h(u, v) := (hittingTimeVec v) u`. - -## Main results (TODO) - -* `Hypergraph.hittingTime_self` — `h(v, v) = 0`. -* `Hypergraph.hittingTime_pos` — `h(u, v) > 0` when `u ≠ v`. -* `Hypergraph.hittingTime_triangle` — triangle inequality - `h(u, v) ≤ h(u, w) + h(w, v)`. --/ - -namespace Hypergraph - -variable {V : Type*} {C : Type*} - -/-- **Math.** Expected first-passage time from `u` to `v`, with -`h(v, v) := 0`. The defining equation is engineering: `h(u, v)` is -the `u`-component of the solution to `(I - Q_v) x = b_v`, where `Q_v` -is the transition matrix with row `v` zeroed out and `b_v` is `0` at -`v` and `1` elsewhere. -/ -noncomputable def hittingTime [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (u v : V) : ℝ := - hittingTimeVec H params v u - -theorem hittingTime_self [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (h_row_sum : ∀ i, ∑ j, H.transMatrix params i j = 1) - (h_nonneg : ∀ i j : V, 0 ≤ H.transMatrix params i j) - (h_sum : ∑ i, H.stationaryDist params i = 1) - (h_pos : ∀ w, 0 < H.stationaryDist params w) - (h_conv : Filter.Tendsto (fun k => H.transMatrix params ^ k) - Filter.atTop - (nhds (ProofAtlas.RandomWalk.stationaryProjection - (H.stationaryDist params)))) - (v : V) : - H.hittingTime params v v = 0 := by - unfold hittingTime - exact H.hittingTimeVec_self params h_row_sum h_nonneg h_sum h_pos h_conv v - -/-- **Math.** Positivity: `h(u, v) > 0` when `u ≠ v`. - -Proof: extend the Neumann-series argument from `hittingTime_nonneg`. -For `N ≥ 1`, peel off the `i = 0` term via `Finset.sum_range_succ'`: -`∑_{i < N} Q^i = Q^0 + ∑_{i < N-1} Q^(i+1) = 1 + (nonneg)`. Applied -to `b` at component `u`, this gives -`((S_N · b) u) ≥ (1.mulVec b) u = b u = 1` since `u ≠ v` forces -`b u = 1`. Pass to the limit via `ge_of_tendsto`. -/ -theorem hittingTime_pos [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (h_row_sum : ∀ i, ∑ j, H.transMatrix params i j = 1) - (h_nonneg : ∀ i j : V, 0 ≤ H.transMatrix params i j) - (h_sum : ∑ i, H.stationaryDist params i = 1) - (h_pos : ∀ w, 0 < H.stationaryDist params w) - (h_conv : Filter.Tendsto (fun k => H.transMatrix params ^ k) - Filter.atTop - (nhds (ProofAtlas.RandomWalk.stationaryProjection - (H.stationaryDist params)))) - {u v : V} (h : u ≠ v) : - 0 < H.hittingTime params u v := by - change 0 < ((1 - H.killedTransMatrix params v : Matrix V V ℝ)⁻¹.mulVec - (fun i => if i = v then (0 : ℝ) else 1)) u - set Q : Matrix V V ℝ := H.killedTransMatrix params v with hQ_def - set A : Matrix V V ℝ := 1 - Q with hA_def - set b : V → ℝ := fun i => if i = v then (0 : ℝ) else 1 with hb_def - change 0 < (A⁻¹.mulVec b) u - have hb_u : b u = 1 := by - change (if u = v then (0 : ℝ) else 1) = 1 - rw [if_neg h] - have h_Q_eq : Q = ProofAtlas.RandomWalk.killedMatrix (H.transMatrix params) v := by - change H.killedTransMatrix params v = - ProofAtlas.RandomWalk.killedMatrix (H.transMatrix params) v - unfold killedTransMatrix ProofAtlas.RandomWalk.killedMatrix Hypergraph.transMatrix - rfl - have h_S_tendsto : - Filter.Tendsto (fun N : ℕ => ∑ i ∈ Finset.range N, Q ^ i) - Filter.atTop (nhds A⁻¹) := by - change Filter.Tendsto _ Filter.atTop (nhds (1 - Q)⁻¹) - rw [h_Q_eq] - exact ProofAtlas.RandomWalk.killedMatrix_neumann_partial_sum_tendsto - h_row_sum h_nonneg h_sum h_pos h_conv v - have h_entry : ∀ j : V, - Filter.Tendsto (fun N : ℕ => (∑ i ∈ Finset.range N, Q ^ i) u j) - Filter.atTop (nhds (A⁻¹ u j)) := by - intro j - have hu : Filter.Tendsto (fun N : ℕ => (∑ i ∈ Finset.range N, Q ^ i) u) - Filter.atTop (nhds (A⁻¹ u)) := - ((continuous_apply u).tendsto _).comp h_S_tendsto - exact ((continuous_apply j).tendsto _).comp hu - have h_term : ∀ j : V, - Filter.Tendsto (fun N : ℕ => (∑ i ∈ Finset.range N, Q ^ i) u j * b j) - Filter.atTop (nhds (A⁻¹ u j * b j)) := fun j => - (h_entry j).mul_const (b j) - have h_apply_tendsto : - Filter.Tendsto (fun N : ℕ => ((∑ i ∈ Finset.range N, Q ^ i).mulVec b) u) - Filter.atTop (nhds ((A⁻¹.mulVec b) u)) := by - have h_sum := tendsto_finsetSum (Finset.univ : Finset V) - (fun j (_ : j ∈ Finset.univ) => h_term j) - simpa only [Matrix.mulVec, dotProduct] using h_sum - have h_Q_nonneg : ∀ i j, 0 ≤ Q i j := by - intros i j - change 0 ≤ H.killedTransMatrix params v i j - unfold killedTransMatrix - simp only [Matrix.of_apply] - by_cases hi : i = v - · simp [hi] - · simp only [if_neg hi]; exact h_nonneg i j - have h_b_nonneg : ∀ j, 0 ≤ b j := by - intro j - change 0 ≤ (if j = v then (0 : ℝ) else 1) - by_cases hj : j = v - · simp [hj] - · simp [hj] - -- For N = N' + 1, (S_N · b) u ≥ 1. - have h_S_ge_one : ∀ N' : ℕ, - 1 ≤ ((∑ i ∈ Finset.range (N' + 1), Q ^ i).mulVec b) u := by - intro N' - have h_split : (∑ i ∈ Finset.range (N' + 1), Q ^ i) - = (∑ i ∈ Finset.range N', Q ^ (i + 1)) + Q ^ 0 := by - exact Finset.sum_range_succ' (fun i => Q ^ i) N' - rw [h_split, Matrix.add_mulVec, Pi.add_apply, pow_zero, Matrix.one_mulVec, hb_u] - have h_rest_nonneg : - 0 ≤ ((∑ i ∈ Finset.range N', Q ^ (i + 1)).mulVec b) u := by - change 0 ≤ ∑ j, (∑ i ∈ Finset.range N', Q ^ (i + 1)) u j * b j - refine Finset.sum_nonneg (fun j _ => ?_) - refine mul_nonneg ?_ (h_b_nonneg j) - rw [Matrix.sum_apply] - exact Finset.sum_nonneg (fun i _ => - ProofAtlas.RandomWalk.pow_nonneg_entries h_Q_nonneg (i + 1) u j) - linarith - have h_eventually : - ∀ᶠ N in Filter.atTop, 1 ≤ ((∑ i ∈ Finset.range N, Q ^ i).mulVec b) u := by - rw [Filter.eventually_atTop] - refine ⟨1, fun N hN => ?_⟩ - obtain ⟨N', rfl⟩ : ∃ N', N = N' + 1 := - ⟨N - 1, by omega⟩ - exact h_S_ge_one N' - have h_ge_one : 1 ≤ (A⁻¹.mulVec b) u := - ge_of_tendsto h_apply_tendsto h_eventually - linarith - -/-- **Math.** Nonnegativity: `h(u, v) ≥ 0`. - -Proof: write `h(u, v) = ((1 - Q_v)⁻¹ · b_v)(u)` as the limit of the -Neumann partial sums `S_N := ∑_{i < N} Q_v ^ i` applied to `b_v` -(`killedMatrix_neumann_partial_sum_tendsto`). At each component `u`, -`(S_N · b_v)(u) = ∑_j (∑_{i < N} (Q_v ^ i) u j) · b_v(j) ≥ 0` -because `Q_v` is entrywise nonnegative (`pow_nonneg_entries`) and -`b_v ≥ 0`. Limit preserves the inequality (`ge_of_tendsto'`). -/ -theorem hittingTime_nonneg [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (h_row_sum : ∀ i, ∑ j, H.transMatrix params i j = 1) - (h_nonneg : ∀ i j : V, 0 ≤ H.transMatrix params i j) - (h_sum : ∑ i, H.stationaryDist params i = 1) - (h_pos : ∀ w, 0 < H.stationaryDist params w) - (h_conv : Filter.Tendsto (fun k => H.transMatrix params ^ k) - Filter.atTop - (nhds (ProofAtlas.RandomWalk.stationaryProjection - (H.stationaryDist params)))) - (u v : V) : - 0 ≤ H.hittingTime params u v := by - change 0 ≤ ((1 - H.killedTransMatrix params v : Matrix V V ℝ)⁻¹.mulVec - (fun i => if i = v then (0 : ℝ) else 1)) u - set Q : Matrix V V ℝ := H.killedTransMatrix params v with hQ_def - set A : Matrix V V ℝ := 1 - Q with hA_def - set b : V → ℝ := fun i => if i = v then (0 : ℝ) else 1 with hb_def - change 0 ≤ (A⁻¹.mulVec b) u - have h_Q_eq : Q = ProofAtlas.RandomWalk.killedMatrix (H.transMatrix params) v := by - change H.killedTransMatrix params v = - ProofAtlas.RandomWalk.killedMatrix (H.transMatrix params) v - unfold killedTransMatrix ProofAtlas.RandomWalk.killedMatrix Hypergraph.transMatrix - rfl - have h_S_tendsto : - Filter.Tendsto (fun N : ℕ => ∑ i ∈ Finset.range N, Q ^ i) - Filter.atTop (nhds A⁻¹) := by - change Filter.Tendsto _ Filter.atTop (nhds (1 - Q)⁻¹) - rw [h_Q_eq] - exact ProofAtlas.RandomWalk.killedMatrix_neumann_partial_sum_tendsto - h_row_sum h_nonneg h_sum h_pos h_conv v - have h_entry : ∀ j : V, - Filter.Tendsto (fun N : ℕ => (∑ i ∈ Finset.range N, Q ^ i) u j) - Filter.atTop (nhds (A⁻¹ u j)) := by - intro j - have hu : Filter.Tendsto (fun N : ℕ => (∑ i ∈ Finset.range N, Q ^ i) u) - Filter.atTop (nhds (A⁻¹ u)) := - ((continuous_apply u).tendsto _).comp h_S_tendsto - exact ((continuous_apply j).tendsto _).comp hu - have h_term : ∀ j : V, - Filter.Tendsto (fun N : ℕ => (∑ i ∈ Finset.range N, Q ^ i) u j * b j) - Filter.atTop (nhds (A⁻¹ u j * b j)) := fun j => - (h_entry j).mul_const (b j) - have h_apply_tendsto : - Filter.Tendsto (fun N : ℕ => ((∑ i ∈ Finset.range N, Q ^ i).mulVec b) u) - Filter.atTop (nhds ((A⁻¹.mulVec b) u)) := by - have h_sum := tendsto_finsetSum (Finset.univ : Finset V) - (fun j (_ : j ∈ Finset.univ) => h_term j) - simpa only [Matrix.mulVec, dotProduct] using h_sum - have h_Q_nonneg : ∀ i j, 0 ≤ Q i j := by - intros i j - change 0 ≤ H.killedTransMatrix params v i j - unfold killedTransMatrix - simp only [Matrix.of_apply] - by_cases hi : i = v - · simp [hi] - · simp only [if_neg hi]; exact h_nonneg i j - have h_b_nonneg : ∀ j, 0 ≤ b j := by - intro j - change 0 ≤ (if j = v then (0 : ℝ) else 1) - by_cases hj : j = v - · simp [hj] - · simp [hj] - have h_S_nonneg : ∀ N : ℕ, 0 ≤ ((∑ i ∈ Finset.range N, Q ^ i).mulVec b) u := by - intro N - change 0 ≤ ∑ j, (∑ i ∈ Finset.range N, Q ^ i) u j * b j - refine Finset.sum_nonneg (fun j _ => ?_) - refine mul_nonneg ?_ (h_b_nonneg j) - rw [Matrix.sum_apply] - exact Finset.sum_nonneg (fun i _ => - ProofAtlas.RandomWalk.pow_nonneg_entries h_Q_nonneg i u j) - exact ge_of_tendsto' h_apply_tendsto h_S_nonneg - -/-- **Math.** Triangle inequality: `h(u, v) ≤ h(u, w) + h(w, v)`. - -Proof outline (when `u, v, w` are pairwise distinct; trivial cases -handled separately): - -* Set `f u := h(u, v)`, `g_w u := h(u, w)`, `h_wv := h(w, v)`, - `g u := g_w u + h_wv`, `d := g - f`. -* `d v = h(v, w) + h(w, v) ≥ 0` (both terms nonneg). -* `d w = 0` (using `h(w, w) = 0`). -* For `u ∉ {v, w}`, the one-step recurrence - `f u = 1 + ∑_j P(u, j) f j` and `g_w u = 1 + ∑_j P(u, j) g_w j` - combine (using `∑_j P(u, j) = 1`) to give - `d u = ∑_j P(u, j) d j`, i.e. `d` is `P`-harmonic on `V \ {v, w}`. -* Truncate `d̃ := d · 1_{V\{v,w}}`. The matrix equation - `(I - Q_{v,w}) d̃ = b̃` holds, where `Q_{v,w}` is `P` with rows - `v, w` zeroed and `b̃(u) = P(u, v) · d v · 1_{u∉{v,w}}` is entrywise - nonneg. Since `(I - Q_{v,w})⁻¹` is entrywise nonneg - (`killedMatrixPair_inv_entry_nonneg`), `d̃ ≥ 0` entrywise, hence - `d ≥ 0` on `V \ {v, w}`. -/ -theorem hittingTime_triangle [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (h_row_sum : ∀ i, ∑ j, H.transMatrix params i j = 1) - (h_nonneg : ∀ i j : V, 0 ≤ H.transMatrix params i j) - (h_sum : ∑ i, H.stationaryDist params i = 1) - (h_pos : ∀ w, 0 < H.stationaryDist params w) - (h_conv : Filter.Tendsto (fun k => H.transMatrix params ^ k) - Filter.atTop - (nhds (ProofAtlas.RandomWalk.stationaryProjection - (H.stationaryDist params)))) - (u v w : V) : - H.hittingTime params u v ≤ - H.hittingTime params u w + H.hittingTime params w v := by - -- Trivial cases - by_cases huv : u = v - · subst huv - rw [hittingTime_self H params h_row_sum h_nonneg h_sum h_pos h_conv] - exact add_nonneg - (hittingTime_nonneg H params h_row_sum h_nonneg h_sum h_pos h_conv u w) - (hittingTime_nonneg H params h_row_sum h_nonneg h_sum h_pos h_conv w u) - by_cases huw : u = w - · subst huw - rw [hittingTime_self H params h_row_sum h_nonneg h_sum h_pos h_conv, zero_add] - by_cases hvw : v = w - · subst hvw - rw [hittingTime_self H params h_row_sum h_nonneg h_sum h_pos h_conv, add_zero] - -- Main case: u, v, w pairwise distinct. - -- Abbreviations (no `set` to avoid def-eq surprises around `g`). - let P : Matrix V V ℝ := H.transMatrix params - let f : V → ℝ := H.hittingTimeVec params v - let gw : V → ℝ := H.hittingTimeVec params w - let hwv : ℝ := f w - let g : V → ℝ := fun x => gw x + hwv - let d : V → ℝ := fun x => g x - f x - -- d v ≥ 0 - have hd_v : 0 ≤ d v := by - have hfv : f v = 0 := - H.hittingTimeVec_self params h_row_sum h_nonneg h_sum h_pos h_conv v - have h_gw_v_nn : 0 ≤ gw v := - H.hittingTime_nonneg params h_row_sum h_nonneg h_sum h_pos h_conv v w - have h_hwv_nn : 0 ≤ hwv := - H.hittingTime_nonneg params h_row_sum h_nonneg h_sum h_pos h_conv w v - change 0 ≤ (gw v + hwv) - f v - rw [hfv]; linarith - -- d w = 0 - have hd_w : d w = 0 := by - have hgww : gw w = 0 := - H.hittingTimeVec_self params h_row_sum h_nonneg h_sum h_pos h_conv w - change (gw w + hwv) - f w = 0 - change (gw w + f w) - f w = 0 - rw [hgww, zero_add, sub_self] - -- d is P-harmonic on V \ {v, w}: d u = ∑_j P(u, j) d j - have hd_harmonic : ∀ x : V, x ≠ v → x ≠ w → d x = ∑ j, P x j * d j := by - intro x hxv hxw - have hf_rec : f x = 1 + ∑ j, P x j * f j := - H.hittingTimeVec_recurrence params h_row_sum h_nonneg - h_sum h_pos h_conv hxv - have hg_w_rec : gw x = 1 + ∑ j, P x j * gw j := - H.hittingTimeVec_recurrence params h_row_sum h_nonneg - h_sum h_pos h_conv hxw - have hg_x : g x = 1 + ∑ j, P x j * g j := by - change gw x + hwv = 1 + ∑ j, P x j * (gw j + hwv) - rw [hg_w_rec] - have h_dist : ∑ j, P x j * (gw j + hwv) - = (∑ j, P x j * gw j) + (∑ j, P x j) * hwv := by - rw [Finset.sum_mul] - rw [← Finset.sum_add_distrib] - refine Finset.sum_congr rfl (fun j _ => ?_); ring - rw [h_dist, h_row_sum x]; ring - change g x - f x = ∑ j, P x j * d j - rw [hg_x, hf_rec] - have h_dist_d : ∑ j, P x j * d j = ∑ j, P x j * g j - ∑ j, P x j * f j := by - rw [← Finset.sum_sub_distrib] - refine Finset.sum_congr rfl (fun j _ => ?_) - change P x j * (g j - f j) = P x j * g j - P x j * f j; ring - rw [h_dist_d]; ring - -- Set up matrix equation (I - Qpair) dtilde = btilde. - let Qp : Matrix V V ℝ := ProofAtlas.RandomWalk.killedMatrixPair P v w - let Ap : Matrix V V ℝ := 1 - Qp - let dtilde : V → ℝ := fun x => if x = v ∨ x = w then 0 else d x - let btilde : V → ℝ := fun x => if x = v ∨ x = w then 0 else P x v * d v - -- (Ap.mulVec dtilde) x = btilde x for all x - have h_eq : Ap.mulVec dtilde = btilde := by - funext x - change ((1 - Qp).mulVec dtilde) x = btilde x - rw [Matrix.sub_mulVec, Pi.sub_apply, Matrix.one_mulVec] - by_cases hxv : x = v - · -- Row x = v: Qp row v zero, dtilde v = 0, btilde v = 0. - have h1 : dtilde x = 0 := by - change (if x = v ∨ x = w then (0 : ℝ) else d x) = 0 - rw [if_pos (Or.inl hxv)] - have h2 : (Qp.mulVec dtilde) x = 0 := by - rw [hxv] - change ∑ j, Qp v j * dtilde j = 0 - apply Finset.sum_eq_zero - intros j _ - have hQp_vj : Qp v j = 0 := - ProofAtlas.RandomWalk.killedMatrixPair_apply_self_left v w j - rw [hQp_vj, zero_mul] - have h3 : btilde x = 0 := by - change (if x = v ∨ x = w then (0 : ℝ) else P x v * d v) = 0 - rw [if_pos (Or.inl hxv)] - rw [h1, h2, h3, sub_zero] - · by_cases hxw : x = w - · have h1 : dtilde x = 0 := by - change (if x = v ∨ x = w then (0 : ℝ) else d x) = 0 - rw [if_pos (Or.inr hxw)] - have h2 : (Qp.mulVec dtilde) x = 0 := by - rw [hxw] - change ∑ j, Qp w j * dtilde j = 0 - apply Finset.sum_eq_zero - intros j _ - have hQp_wj : Qp w j = 0 := - ProofAtlas.RandomWalk.killedMatrixPair_apply_self_right v w j - rw [hQp_wj, zero_mul] - have h3 : btilde x = 0 := by - change (if x = v ∨ x = w then (0 : ℝ) else P x v * d v) = 0 - rw [if_pos (Or.inr hxw)] - rw [h1, h2, h3, sub_zero] - · -- x ∉ {v, w} - have hdt_x : dtilde x = d x := by - change (if x = v ∨ x = w then (0 : ℝ) else d x) = d x - rw [if_neg (not_or.mpr ⟨hxv, hxw⟩)] - have hbt_x : btilde x = P x v * d v := by - change (if x = v ∨ x = w then (0 : ℝ) else P x v * d v) = P x v * d v - rw [if_neg (not_or.mpr ⟨hxv, hxw⟩)] - rw [hdt_x, hbt_x] - -- (Qp.mulVec dtilde) x = ∑_{j ∉ {v, w}} P(x, j) * d(j) - have h_Qp_mulVec : - (Qp.mulVec dtilde) x = ∑ j ∈ Finset.univ \ {v, w}, P x j * d j := by - change ∑ j, Qp x j * dtilde j = _ - have h_subset : ({v, w} : Finset V) ⊆ Finset.univ := Finset.subset_univ _ - rw [← Finset.sum_sdiff h_subset] - have h_v_w : ∑ j ∈ ({v, w} : Finset V), Qp x j * dtilde j = 0 := by - rw [Finset.sum_insert (by simp [hvw]), Finset.sum_singleton] - have hdt_v : dtilde v = 0 := by - change (if v = v ∨ v = w then (0 : ℝ) else d v) = 0; simp - have hdt_w : dtilde w = 0 := by - change (if w = v ∨ w = w then (0 : ℝ) else d w) = 0; simp - rw [hdt_v, hdt_w, mul_zero, mul_zero, add_zero] - rw [h_v_w, add_zero] - refine Finset.sum_congr rfl (fun j hj => ?_) - have hj_mem : j ∉ ({v, w} : Finset V) := (Finset.mem_sdiff.mp hj).2 - have hjv : j ≠ v := fun h => hj_mem (by simp [h]) - have hjw : j ≠ w := fun h => hj_mem (by simp [h]) - have hQp_apply : Qp x j = P x j := - ProofAtlas.RandomWalk.killedMatrixPair_apply_of_ne hxv hxw j - have hdt_j : dtilde j = d j := by - change (if j = v ∨ j = w then (0 : ℝ) else d j) = d j - rw [if_neg (not_or.mpr ⟨hjv, hjw⟩)] - rw [hQp_apply, hdt_j] - rw [h_Qp_mulVec] - have h_harm := hd_harmonic x hxv hxw - have h_subset : ({v, w} : Finset V) ⊆ Finset.univ := Finset.subset_univ _ - have h_split : (∑ j, P x j * d j) - = (∑ j ∈ ({v, w} : Finset V), P x j * d j) - + ∑ j ∈ Finset.univ \ {v, w}, P x j * d j := by - rw [← Finset.sum_sdiff h_subset]; ring - rw [h_harm, h_split] - rw [Finset.sum_insert (by simp [hvw]), Finset.sum_singleton, - hd_w, mul_zero, add_zero] - ring - -- dtilde = Ap⁻¹.mulVec btilde - have hAp_unit : IsUnit Ap := - ProofAtlas.RandomWalk.killedMatrixPair_one_sub_isUnit - h_row_sum h_nonneg h_sum h_pos h_conv v w - have hAp_det : IsUnit Ap.det := (Matrix.isUnit_iff_isUnit_det Ap).mp hAp_unit - have hdt_eq : dtilde = Ap⁻¹.mulVec btilde := by - have : Ap⁻¹.mulVec (Ap.mulVec dtilde) = Ap⁻¹.mulVec btilde := by rw [h_eq] - rw [Matrix.mulVec_mulVec, Matrix.nonsing_inv_mul _ hAp_det, - Matrix.one_mulVec] at this - exact this - have hbt_nonneg : ∀ x, 0 ≤ btilde x := by - intro x - change 0 ≤ (if x = v ∨ x = w then (0 : ℝ) else P x v * d v) - by_cases hx : x = v ∨ x = w - · rw [if_pos hx] - · rw [if_neg hx]; exact mul_nonneg (h_nonneg x v) hd_v - have hdt_nonneg : ∀ x, 0 ≤ dtilde x := by - intro x - rw [hdt_eq] - change 0 ≤ ∑ j, Ap⁻¹ x j * btilde j - refine Finset.sum_nonneg (fun j _ => ?_) - refine mul_nonneg ?_ (hbt_nonneg j) - exact ProofAtlas.RandomWalk.killedMatrixPair_inv_entry_nonneg - h_row_sum h_nonneg h_sum h_pos h_conv v w x j - -- For u ∉ {v, w}, dtilde u = d u ≥ 0. - have hdt_u : dtilde u = d u := by - change (if u = v ∨ u = w then (0 : ℝ) else d u) = d u - rw [if_neg (not_or.mpr ⟨huv, huw⟩)] - have hd_u : 0 ≤ d u := by rw [← hdt_u]; exact hdt_nonneg u - change H.hittingTime params u v ≤ H.hittingTime params u w + H.hittingTime params w v - have h_arith : d u = H.hittingTime params u w + H.hittingTime params w v - - H.hittingTime params u v := by - change (gw u + hwv) - f u = _; rfl - linarith - -end Hypergraph diff --git a/lean/ProofAtlas/Metric/CommuteTime/Instance.lean b/lean/ProofAtlas/Metric/CommuteTime/Instance.lean deleted file mode 100644 index 9128246..0000000 --- a/lean/ProofAtlas/Metric/CommuteTime/Instance.lean +++ /dev/null @@ -1,102 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Metric.Axioms -import ProofAtlas.Metric.CommuteTime.CommuteTime - -/-! -# `commuteTimeDist` satisfies `IsHypergraphMetric` - -Verification of the 7-check quality certificate for the BDF commute -time distance. All 7 fields close here by assembling the existing -metric-axiom lemmas (`commuteTimeDist_self/_symm/_triangle`, -`hittingTime_nonneg`); the heavier ergodic preconditions are exposed -as a single `hErgodic` instance parameter, mirroring the -`hConn`/`hRange` pattern used by `resistanceDist_isHypergraphMetric`. - -The three faithfulness witnesses (`hWit_direction/_ordering/_coloring`) -are abstract existence statements over `commuteTimeDist` itself. -Their truth follows from the BDF harmonic schedule's -`α · (i+1)⁻¹ · (outArity)⁻¹` shape — discharging them by a concrete -scaling argument is a follow-up. --/ - -namespace Hypergraph - -/-- **Math.** The BDF commute time distance is a *good BDF metric*. - -Hypotheses, mirroring `resistanceDist_isHypergraphMetric`: - -* `hErgodic` — for every hypergraph and parameter choice, the BDF - random walk has row-stochastic transition matrix, nonnegative - entries, summable strictly-positive stationary distribution, and - converges to its stationary projection. These five conditions are - the standing hypothesis of every `commuteTime*` lemma in - `CommuteTime.lean`; bundling them keeps this signature manageable. -* `hWit_direction / _ordering / _coloring` — sensitivity existence - witnesses. -/ -theorem commuteTimeDist_isHypergraphMetric {V C : Type*} [Fintype V] [DecidableEq V] - [hFin : ∀ H : Hypergraph V C, Fintype H.edges] - (hErgodic : ∀ (H : Hypergraph V C) (params : WalkParams C), - (∀ i, ∑ j, H.transMatrix params i j = 1) ∧ - (∀ i j : V, 0 ≤ H.transMatrix params i j) ∧ - (∑ i, H.stationaryDist params i = 1) ∧ - (∀ w, 0 < H.stationaryDist params w) ∧ - Filter.Tendsto (fun k => H.transMatrix params ^ k) - Filter.atTop - (nhds (ProofAtlas.RandomWalk.stationaryProjection - (H.stationaryDist params)))) - (hWit_direction : - ∃ (H H' : Hypergraph V C) (params : WalkParams C) (u v : V), - H.commuteTimeDist params u v ≠ H'.commuteTimeDist params u v) - (hWit_ordering : - ∃ (H H' : Hypergraph V C) (params : WalkParams C) (u v : V), - H.commuteTimeDist params u v ≠ H'.commuteTimeDist params u v) - (hWit_coloring : - ∃ (H : Hypergraph V C) (params params' : WalkParams C) (u v : V), - H.commuteTimeDist params u v ≠ H.commuteTimeDist params' u v) : - IsHypergraphMetric (fun (H : Hypergraph V C) params u v => - H.commuteTimeDist params u v) := by - refine - { d_self := ?_ - d_symm := ?_ - d_triangle := ?_ - d_nonneg := ?_ - sensitive_to_direction := ?_ - sensitive_to_ordering := ?_ - sensitive_to_coloring := ?_ } - · -- d_self - intro H _ params u - letI : Fintype H.edges := hFin H - obtain ⟨h_row, h_nn, h_sum, h_pos, h_conv⟩ := hErgodic H params - convert H.commuteTimeDist_self params h_row h_nn h_sum h_pos h_conv u - · -- d_symm - intro H _ params u v - letI : Fintype H.edges := hFin H - convert H.commuteTimeDist_symm params u v - · -- d_triangle - intro H _ params u v w - letI : Fintype H.edges := hFin H - obtain ⟨h_row, h_nn, h_sum, h_pos, h_conv⟩ := hErgodic H params - convert H.commuteTimeDist_triangle params h_row h_nn h_sum h_pos h_conv u v w - · -- d_nonneg: d(u,v) = h(u,v) + h(v,u), both summands ≥ 0. - intro H _ params u v - letI : Fintype H.edges := hFin H - obtain ⟨h_row, h_nn, h_sum, h_pos, h_conv⟩ := hErgodic H params - have h1 := H.hittingTime_nonneg params h_row h_nn h_sum h_pos h_conv u v - have h2 := H.hittingTime_nonneg params h_row h_nn h_sum h_pos h_conv v u - change 0 ≤ H.hittingTime params u v + H.hittingTime params v u - linarith - · -- sensitive_to_direction - obtain ⟨H, H', params, u, v, hne⟩ := hWit_direction - exact ⟨H, H', hFin H, hFin H', params, u, v, hne⟩ - · -- sensitive_to_ordering - obtain ⟨H, H', params, u, v, hne⟩ := hWit_ordering - exact ⟨H, H', hFin H, hFin H', params, u, v, hne⟩ - · -- sensitive_to_coloring - obtain ⟨H, params, params', u, v, hne⟩ := hWit_coloring - exact ⟨H, hFin H, params, params', u, v, hne⟩ - -end Hypergraph diff --git a/lean/ProofAtlas/Metric/CommuteTime/KilledChain.lean b/lean/ProofAtlas/Metric/CommuteTime/KilledChain.lean deleted file mode 100644 index 836619f..0000000 --- a/lean/ProofAtlas/Metric/CommuteTime/KilledChain.lean +++ /dev/null @@ -1,262 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Util.Linter.MathTag -import ProofAtlas.Hypergraph.TransitionMatrix -import ProofAtlas.RandomWalk.Spectral.Stationary -import ProofAtlas.RandomWalk.KemenySnell -import Mathlib.LinearAlgebra.Matrix.NonsingularInverse -import Mathlib.Analysis.Matrix.Normed -import Mathlib.Analysis.SpecificLimits.Normed -import Mathlib.Algebra.Ring.GeomSum - -/-! -# Killed transition matrix and matrix-form hitting time - -**Eng.** Linear-algebra plumbing for expressing the BDF hitting -time `h(u, v)` as the `u`-component of `(I - Q_v)⁻¹ · b_v`, where -`Q_v` is the transition matrix with row `v` zeroed out (so that `v` -becomes absorbing) and `b_v` is `0` at `v` and `1` elsewhere. The -paper defines `h(u, v)` abstractly as an expected first-passage -time; this module encodes the equivalent matrix-form definition -that the Lean library uses to make `hittingTime` computable on -finite vertex sets. - -## Main definitions - -* `Hypergraph.killedTransMatrix` — transition matrix with row - indexed by `v` zeroed out. -* `Hypergraph.hittingTimeVec` — vector of expected hitting times - to `v`, defined via the matrix inverse `(I - Q)⁻¹ · b`. --/ - -namespace Hypergraph - -variable {V : Type*} {C : Type*} - -/-- **Eng.** Transition matrix with the row indexed by `v` zeroed -out, so that `v` becomes an absorbing state. -/ -noncomputable def killedTransMatrix [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (v : V) : Matrix V V ℝ := - Matrix.of fun i j => - if i = v then 0 else H.transProb params i j - -/-- **Eng.** Vector of expected hitting times to `v`, defined as the -solution of the linear system `(I - Q) x = b`, where `Q` is the -killed transition matrix and `b` is `0` at `v` and `1` elsewhere. -/ -noncomputable def hittingTimeVec [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (v : V) : V → ℝ := - let Q := killedTransMatrix H params v - let A := 1 - Q - let b : V → ℝ := fun i => if i = v then 0 else 1 - A⁻¹.mulVec b - -/-- **Eng.** The killed transition matrix is sub-stochastic: each -row sums to at most `1`. For the absorbing row `v` the row is -identically zero. For rows `i ≠ v` the row equals `transProb i ·`, -which sums to `1` by `transProb_sum_one` when `i` is incident to -some hyperedge (`H.normalizer params i ≠ 0`); the row is also `0` -on vertices not incident to any hyperedge. - -Takes the global hypotheses required by `transProb_sum_one`. -/ -theorem killed_sub_stochastic [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (v : V) - (h_out_inj : ∀ e, Function.Injective (H.outVertex e)) - (h_acyclic : ∀ i (e : H.edges), ¬(H.isInput i e ∧ H.isOutput i e)) : - ∀ i, ∑ j, H.killedTransMatrix params v i j ≤ 1 := by - intro i - unfold killedTransMatrix - simp only [Matrix.of_apply] - by_cases hi : i = v - · subst hi - simp - · simp only [if_neg hi] - by_cases hZi : H.normalizer params i = 0 - · -- isolated row: transProb i j = 0 for all j - have : ∀ j, H.transProb params i j = 0 := by - intro j - unfold transProb - rw [if_pos hZi] - simp [this] - · exact (H.transProb_sum_one params i hZi h_out_inj (h_acyclic i)).le - -open Matrix.Norms.Operator in -/-- **Eng.** Some power `Q_v ^ (k+1)` has L∞ operator norm strictly -less than `1`. Equivalent to spectral radius `< 1` on the -finite-dimensional space `Matrix V V ℝ`. - -Mathematical content: `Q_v` is sub-stochastic -(`killed_sub_stochastic`) with row `v` zeroed out (i.e. `v` is -absorbing). Together with ergodicity of `H` (via `h_conv`) this -gives a strict drop in row sums of `Q_v ^ k` for `k` large enough. -Routes through `ProofAtlas.RandomWalk.killedMatrix_pow_norm_lt_one`. -/ -theorem killed_pow_norm_lt_one [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (h_row_sum : ∀ i, ∑ j, H.transMatrix params i j = 1) - (h_nonneg : ∀ i j : V, 0 ≤ H.transMatrix params i j) - (h_sum : ∑ i, H.stationaryDist params i = 1) - (h_pos : ∀ w, 0 < H.stationaryDist params w) - (h_conv : Filter.Tendsto (fun k => H.transMatrix params ^ k) - Filter.atTop - (nhds (ProofAtlas.RandomWalk.stationaryProjection - (H.stationaryDist params)))) - (v : V) : - ∃ k : ℕ, ‖(H.killedTransMatrix params v) ^ (k + 1)‖ < 1 := by - have h_eq : H.killedTransMatrix params v = - ProofAtlas.RandomWalk.killedMatrix (H.transMatrix params) v := by - unfold killedTransMatrix ProofAtlas.RandomWalk.killedMatrix Hypergraph.transMatrix - rfl - rw [h_eq] - exact ProofAtlas.RandomWalk.killedMatrix_pow_norm_lt_one - h_row_sum h_nonneg h_sum h_pos h_conv v - -open Matrix.Norms.Operator in -/-- **Eng.** `1 - Q_v` is invertible, where `Q_v` is the killed -transition matrix. Underwrites well-definedness of `hittingTimeVec` -and every closed-form matrix expression for `h(u, v)`. - -Proof: pick `k+1 ≥ 1` with `‖Q_v ^ (k+1)‖ < 1` (`killed_pow_norm_lt_one`); -then `1 - Q_v ^ (k+1)` is a unit by `isUnit_one_sub_of_norm_lt_one`. -The geometric factorization `(1 - Q_v) * ∑_{i=0}^{k} Q_v^i = 1 - Q_v^(k+1)` -(`mul_neg_geom_sum`) shows `(1 - Q_v)` divides a unit on the left; -since `Matrix V V ℝ` is Dedekind-finite (ℝ is stably finite), -`isUnit_of_mul_isUnit_left` concludes that `1 - Q_v` is itself a unit. - -(Note: a non-sorry derivation from the ergodic hypotheses `h_conv` is -available via `ProofAtlas.RandomWalk.killedMatrix_one_sub_isUnit`. The -Spectral-layer call sites use that directly, so this BDF wrapper is -only consumed by `hittingTimeVec_self`, which is the `h(v, v) = 0` -identity living off the main theorem chain.) -/ -theorem one_sub_killed_invertible [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (h_row_sum : ∀ i, ∑ j, H.transMatrix params i j = 1) - (h_nonneg : ∀ i j : V, 0 ≤ H.transMatrix params i j) - (h_sum : ∑ i, H.stationaryDist params i = 1) - (h_pos : ∀ w, 0 < H.stationaryDist params w) - (h_conv : Filter.Tendsto (fun k => H.transMatrix params ^ k) - Filter.atTop - (nhds (ProofAtlas.RandomWalk.stationaryProjection - (H.stationaryDist params)))) - (v : V) : - IsUnit (1 - H.killedTransMatrix params v : Matrix V V ℝ) := by - have h_eq : H.killedTransMatrix params v = - ProofAtlas.RandomWalk.killedMatrix (H.transMatrix params) v := by - unfold killedTransMatrix ProofAtlas.RandomWalk.killedMatrix Hypergraph.transMatrix - rfl - rw [h_eq] - exact ProofAtlas.RandomWalk.killedMatrix_one_sub_isUnit - h_row_sum h_nonneg h_sum h_pos h_conv v - -/-- **Eng.** The `v`-th component of `hittingTimeVec H params v` is `0`. - -Proof: let `Q = killedTransMatrix H params v`, `A = 1 - Q`, -`b i = if i = v then 0 else 1`, and `x = A⁻¹.mulVec b`. From -`one_sub_killed_invertible`, `A * A⁻¹ = 1`, so `A.mulVec x = b`. -The `v`-th row of `Q` is zero by definition of `killedTransMatrix`, -so `(A.mulVec x) v = (1.mulVec x) v - (Q.mulVec x) v = x v - 0 = x v`. -Combining with `b v = 0` gives `x v = 0`. -/ -theorem hittingTimeVec_self [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (h_row_sum : ∀ i, ∑ j, H.transMatrix params i j = 1) - (h_nonneg : ∀ i j : V, 0 ≤ H.transMatrix params i j) - (h_sum : ∑ i, H.stationaryDist params i = 1) - (h_pos : ∀ w, 0 < H.stationaryDist params w) - (h_conv : Filter.Tendsto (fun k => H.transMatrix params ^ k) - Filter.atTop - (nhds (ProofAtlas.RandomWalk.stationaryProjection - (H.stationaryDist params)))) - (v : V) : - H.hittingTimeVec params v v = 0 := by - change ((1 - H.killedTransMatrix params v)⁻¹.mulVec - (fun i => if i = v then (0 : ℝ) else 1)) v = 0 - set Q : Matrix V V ℝ := H.killedTransMatrix params v with hQ_def - set A : Matrix V V ℝ := 1 - Q with hA_def - set b : V → ℝ := fun i => if i = v then (0 : ℝ) else 1 with hb_def - have hA_unit : IsUnit A := - one_sub_killed_invertible H params h_row_sum h_nonneg h_sum h_pos h_conv v - have hA_det : IsUnit A.det := (Matrix.isUnit_iff_isUnit_det A).mp hA_unit - have key : A.mulVec (A⁻¹.mulVec b) = b := by - rw [Matrix.mulVec_mulVec, Matrix.mul_nonsing_inv _ hA_det, Matrix.one_mulVec] - have hQ_row : ∀ j, Q v j = 0 := by - intro j - change H.killedTransMatrix params v v j = 0 - unfold killedTransMatrix - simp - have h_diag : ∀ X : V → ℝ, (A.mulVec X) v = X v := by - intro X - rw [hA_def, Matrix.sub_mulVec, Pi.sub_apply, Matrix.one_mulVec] - have hQX : (Q.mulVec X) v = 0 := by - change ∑ j, Q v j * X j = 0 - apply Finset.sum_eq_zero - intros j _ - rw [hQ_row j, zero_mul] - rw [hQX, sub_zero] - have hxv : (A⁻¹.mulVec b) v = b v := by - rw [← h_diag (A⁻¹.mulVec b), key] - rw [hxv, hb_def] - simp - -/-- **Math.** One-step recurrence for `hittingTimeVec` at any `u ≠ v`: -`(hittingTimeVec v) u = 1 + ∑_j P(u, j) · (hittingTimeVec v) j`. - -This is the Lean translation of the standard first-step decomposition -`h(u, v) = 1 + ∑_j P(u, j) h(j, v)`, and is the algebraic source of -the triangle inequality for hitting times. -/ -theorem hittingTimeVec_recurrence [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (h_row_sum : ∀ i, ∑ j, H.transMatrix params i j = 1) - (h_nonneg : ∀ i j : V, 0 ≤ H.transMatrix params i j) - (h_sum : ∑ i, H.stationaryDist params i = 1) - (h_pos : ∀ w, 0 < H.stationaryDist params w) - (h_conv : Filter.Tendsto (fun k => H.transMatrix params ^ k) - Filter.atTop - (nhds (ProofAtlas.RandomWalk.stationaryProjection - (H.stationaryDist params)))) - {u v : V} (huv : u ≠ v) : - H.hittingTimeVec params v u = 1 + ∑ j, H.transMatrix params u j * - H.hittingTimeVec params v j := by - set P : Matrix V V ℝ := H.transMatrix params with hP_def - set Q : Matrix V V ℝ := H.killedTransMatrix params v with hQ_def - set A : Matrix V V ℝ := 1 - Q with hA_def - set b : V → ℝ := fun i => if i = v then (0 : ℝ) else 1 with hb_def - set x : V → ℝ := H.hittingTimeVec params v with hx_def - have hx_eq : x = A⁻¹.mulVec b := rfl - have hA_unit : IsUnit A := - one_sub_killed_invertible H params h_row_sum h_nonneg h_sum h_pos h_conv v - have hA_det : IsUnit A.det := (Matrix.isUnit_iff_isUnit_det A).mp hA_unit - have key : A.mulVec x = b := by - rw [hx_eq, Matrix.mulVec_mulVec, Matrix.mul_nonsing_inv _ hA_det, - Matrix.one_mulVec] - -- Row u of A · x = b: x u - (Q.mulVec x) u = 1 (since u ≠ v) - have h_b_u : b u = 1 := by - change (if u = v then (0 : ℝ) else 1) = 1 - rw [if_neg huv] - have h_row_u : (A.mulVec x) u = 1 := by rw [key]; exact h_b_u - have h_row_split : x u - (Q.mulVec x) u = 1 := by - have := h_row_u - rw [hA_def, Matrix.sub_mulVec, Pi.sub_apply, Matrix.one_mulVec] at this - exact this - -- Q.mulVec x at u = ∑_j P(u, j) x(j), since Q(u, ·) = P(u, ·) for u ≠ v - have h_Qx : (Q.mulVec x) u = ∑ j, P u j * x j := by - change ∑ j, Q u j * x j = ∑ j, P u j * x j - refine Finset.sum_congr rfl (fun j _ => ?_) - have hQ_apply : Q u j = P u j := by - change H.killedTransMatrix params v u j = P u j - unfold killedTransMatrix - simp only [Matrix.of_apply] - rw [if_neg huv] - rfl - rw [hQ_apply] - rw [h_Qx] at h_row_split - linarith - -end Hypergraph diff --git a/lean/ProofAtlas/Metric/Default.lean b/lean/ProofAtlas/Metric/Default.lean index dc87b4a..fba8ebe 100644 --- a/lean/ProofAtlas/Metric/Default.lean +++ b/lean/ProofAtlas/Metric/Default.lean @@ -4,10 +4,7 @@ Released under Apache 2.0 license as described in the file LICENSE. Authors: MathNetwork -/ import ProofAtlas.Util.Linter.MetricRegistry -import ProofAtlas.Metric.CommuteTime.KilledChain -import ProofAtlas.Metric.CommuteTime.HittingTime import ProofAtlas.Metric.Axioms -import ProofAtlas.Metric.CommuteTime.CommuteTime import ProofAtlas.Metric.Resistance.Canonical import ProofAtlas.Metric.Resistance.HarmonicLaplacian import ProofAtlas.Metric.Resistance.Algebraic @@ -16,26 +13,12 @@ import ProofAtlas.Metric.Resistance.Structural import ProofAtlas.Metric.Resistance.Resistance import ProofAtlas.Metric.Resistance.Spectral import ProofAtlas.Metric.Resistance.Instance -import ProofAtlas.Metric.CommuteTime.Instance -import ProofAtlas.Metric.JensenShannon.Instance -import ProofAtlas.Metric.Diffusion.Instance /-! -# BDF commute time metric — facade +# Metric — facade (main branch) -Aggregates the metric-level content. Each submodule is self-contained: - -* `Metric.KilledChain` — matrix-form hitting-time machinery - (`killedTransMatrix`, `hittingTimeVec`). -* `Metric.HittingTime` — expected first-passage times. -* `Metric.Axioms` — axiomatic characterisation `HypergraphMetric` (paper §3 - axioms D, O, C, A). -* `Metric.CommuteTime` — commute time metric `d`, `MetricSpace` - instance, and the existence witness `commuteTimeHypergraphMetric`. -* `Metric.FourPointDeviation` — Gromov hyperbolicity framework. -* `Metric.Resistance` — bridges `HypergraphMetric` to `LinearAlgebra.EffectiveResistance` - (defines `HypergraphMetric.d` as effective resistance; sorry'd bridging - theorems pending v2 migration). +Resistance distance metric and its `IsHypergraphMetric` instance. +CommuteTime, JensenShannon, Diffusion on `development` branch. -/ #guard_metric_registry diff --git a/lean/ProofAtlas/Metric/Diffusion/Basic.lean b/lean/ProofAtlas/Metric/Diffusion/Basic.lean deleted file mode 100644 index 1a65c46..0000000 --- a/lean/ProofAtlas/Metric/Diffusion/Basic.lean +++ /dev/null @@ -1,180 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Util.Linter.MathTag -import ProofAtlas.Metric.Resistance.HarmonicLaplacian -import ProofAtlas.LinearAlgebra.EffectiveResistance -import Mathlib.Analysis.Normed.Algebra.MatrixExponential -import Mathlib.Analysis.InnerProductSpace.PiL2 -import Mathlib.Topology.MetricSpace.Pseudo.Defs - -/-! -# Diffusion distance on a BDF hypergraph - -A one-parameter family of pseudo-metrics on the vertex set, -parameterised by a *scale* `t > 0`. Same harmonic Laplacian `L_κ` -as the electrical resistance distance, different spectral weighting: - - `d_t(u, v)² := ∑_w (K_t(w, u) − K_t(w, v))²` - -where `K_t := exp(−t · L_κ)` is the heat kernel matrix. - -In spectral form: if `{(λᵢ, φᵢ)}` is the eigendata of `L_κ`, then - - `d_t(u, v)² = ∑_{λᵢ > 0} e^{−2 t λᵢ} · (φᵢ(u) − φᵢ(v))²`. - -**Interpretation.** "At scale `t`, how different do `u` and `v` -look to a diffusion process that has been running for time `t`." -Small `t` resolves local structure (early diffusion has not mixed); -large `t` resolves global structure (mixing reaches further). The -family interpolates between vertex equality at `t = 0⁺` and the -resistance metric at `t → ∞` (modulo rescaling). - -## Main definitions - -* `diffusionDist params t u v` — diffusion distance at scale `t`. -* `diffusionDist_self`, `diffusionDist_nonneg` — trivial axioms. -* `diffusionDist_symm`, `diffusionDist_triangle` — sorry'd. -* `toPseudoMetricSpaceDiffusion` — bundled `PseudoMetricSpace V` - for each scale `t`. - -## Future work - -* `diffusionDist_monotone_in_t` — larger `t` ⇒ shorter distances. -* `diffusionDist_tendsto_resistance` — `t → ∞` limit recovers `d_H`. --/ - -namespace Hypergraph - -variable {V : Type*} {C : Type*} - -open scoped Matrix - -/-- **Math.** *Heat kernel* at scale `t`: -`K_t := exp(−t · L_κ) : Matrix V V ℝ`. -/ -noncomputable def heatKernel [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (t : ℝ) : Matrix V V ℝ := - NormedSpace.exp (-t • H.harmonicLaplacian params) - -/-- **Math.** *Diffusion distance at scale `t`*. For `t > 0`, -`d_t(u, v)² := ∑_w (K_t(w, u) − K_t(w, v))²`. Small `t` resolves -local structure; large `t` global structure. Built on the same -harmonic Laplacian as `resistanceDist`. -/ -noncomputable def diffusionDist [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (t : ℝ) (u v : V) : ℝ := - Real.sqrt (∑ i : V, - (H.heatKernel params t i u - H.heatKernel params t i v)^2) - -/-- **Math.** Metric axiom: `d_t(u, v) ≥ 0`. Direct from -`Real.sqrt_nonneg`. -/ -lemma diffusionDist_nonneg [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (t : ℝ) (u v : V) : - 0 ≤ H.diffusionDist params t u v := - Real.sqrt_nonneg _ - -/-- **Math.** Metric axiom: `d_t(u, u) = 0`. Each summand is -`(x − x)² = 0`, so the inner sum is `0` and `√0 = 0`. -/ -lemma diffusionDist_self [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (t : ℝ) (u : V) : - H.diffusionDist params t u u = 0 := by - unfold diffusionDist - have : (∑ i : V, - (H.heatKernel params t i u - H.heatKernel params t i u)^2) = 0 := by - refine Finset.sum_eq_zero (fun i _ => ?_) - rw [sub_self]; ring - rw [this, Real.sqrt_zero] - -/-- **Math.** Metric axiom: `d_t(u, v) = d_t(v, u)`. The squared -differences in the sum are symmetric under `(u, v) ↦ (v, u)`. -/ -lemma diffusionDist_symm [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (t : ℝ) (u v : V) : - H.diffusionDist params t u v = H.diffusionDist params t v u := by - unfold diffusionDist - congr 1 - refine Finset.sum_congr rfl (fun i _ => ?_) - ring - -/-- **Eng.** Heat-kernel column vector as an element of -`EuclideanSpace ℝ V`. The diffusion distance is the `EuclideanSpace` -distance between these vectors. -/ -noncomputable def heatColumn [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (t : ℝ) (u : V) : EuclideanSpace ℝ V := - (EuclideanSpace.equiv V ℝ).symm (fun i => H.heatKernel params t i u) - -/-- **Math.** Bridge: the diffusion distance equals the -`EuclideanSpace ℝ V` distance between the heat-kernel column vectors -`i ↦ K_t(i, u)` and `i ↦ K_t(i, v)`. -/ -lemma diffusionDist_eq_euclidean_dist [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (t : ℝ) (u v : V) : - H.diffusionDist params t u v - = dist (H.heatColumn params t u) (H.heatColumn params t v) := by - unfold diffusionDist heatColumn - rw [EuclideanSpace.dist_eq] - congr 1 - refine Finset.sum_congr rfl (fun i _ => ?_) - simp [Real.dist_eq, sq_abs] - -/-- **Math.** Metric axiom (triangle inequality): `d_t(u, w) ≤ -d_t(u, v) + d_t(v, w)`. Follows from the `ℓ²` triangle inequality -applied to the heat-kernel column-difference vectors. -/ -lemma diffusionDist_triangle [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (t : ℝ) (u v w : V) : - H.diffusionDist params t u w - ≤ H.diffusionDist params t u v + H.diffusionDist params t v w := by - rw [H.diffusionDist_eq_euclidean_dist params t u w, - H.diffusionDist_eq_euclidean_dist params t u v, - H.diffusionDist_eq_euclidean_dist params t v w] - exact dist_triangle _ _ _ - -/-- **Math.** *Monotonicity in scale.* For `0 < s ≤ t`, the -diffusion distance shrinks: `d_t(u, v) ≤ d_s(u, v)`. Larger `t` -means more smoothing, which contracts pairwise differences. -/ -theorem diffusionDist_monotone_in_t [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) {s t : ℝ} (_hs : 0 < s) (_hst : s ≤ t) - (u v : V) : - H.diffusionDist params t u v ≤ H.diffusionDist params s u v := by - sorry - -/-- **Math.** *Connection to resistance distance (informal).* As -`t → ∞`, after appropriate rescaling, the diffusion distance -recovers the resistance distance. The exact rescaling involves the -spectral weights `e^{−2 t λᵢ}` which decay differently per mode; -the classical formulation is that the *time integral* -`∫₀^∞ d_t(u, v)² dt` equals (up to a constant depending on the -non-zero spectrum) `d_H(u, v)²`. Stated here as an existential -forward declaration; the precise form depends on chosen -normalisation. -/ -theorem diffusionDist_tendsto_resistance [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (u v : V) : - ∃ (rescale : ℝ → ℝ), - Filter.Tendsto - (fun t : ℝ => rescale t * (H.diffusionDist params t u v)^2) - Filter.atTop - (nhds ((ProofAtlas.LinearAlgebra.effectiveResistance - (H.harmonicLaplacian params) u v)^2)) := by - sorry - -/-- **Math.** Bundled pseudometric structure on `V` from the -diffusion distance at scale `t`. -/ -@[reducible] -noncomputable def toPseudoMetricSpaceDiffusion [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (t : ℝ) : PseudoMetricSpace V where - dist u v := H.diffusionDist params t u v - dist_self u := H.diffusionDist_self params t u - dist_comm := H.diffusionDist_symm params t - dist_triangle := H.diffusionDist_triangle params t - -end Hypergraph diff --git a/lean/ProofAtlas/Metric/Diffusion/Instance.lean b/lean/ProofAtlas/Metric/Diffusion/Instance.lean deleted file mode 100644 index 3257716..0000000 --- a/lean/ProofAtlas/Metric/Diffusion/Instance.lean +++ /dev/null @@ -1,26 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Metric.Axioms -import ProofAtlas.Metric.Diffusion.Basic - -/-! -# `diffusionDist` satisfies `IsHypergraphMetric` - -The required quality certificate for the BDF diffusion distance at a -fixed scale `t`. Proof body deferred. --/ - -namespace Hypergraph - -/-- **Math.** The BDF diffusion distance (at a fixed scale `t`) is a -*good BDF metric*. -/ -theorem diffusionDist_isHypergraphMetric {V C : Type*} [Fintype V] [DecidableEq V] - [∀ H : Hypergraph V C, Fintype H.edges] (t : ℝ) : - IsHypergraphMetric (fun (H : Hypergraph V C) params u v => - H.diffusionDist params t u v) := by - sorry - -end Hypergraph diff --git a/lean/ProofAtlas/Metric/JensenShannon/Basic.lean b/lean/ProofAtlas/Metric/JensenShannon/Basic.lean deleted file mode 100644 index 973f618..0000000 --- a/lean/ProofAtlas/Metric/JensenShannon/Basic.lean +++ /dev/null @@ -1,145 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Util.Linter.MathTag -import ProofAtlas.Metric.Resistance.HarmonicLaplacian -import Mathlib.Analysis.SpecialFunctions.Log.Basic -import Mathlib.Analysis.SpecialFunctions.Pow.Real -import Mathlib.Topology.MetricSpace.Pseudo.Defs - -/-! -# Jensen–Shannon distance on a BDF hypergraph - -A third metric on the vertex set of a BDF hypergraph, parallel to -the electrical `resistanceDist` (paper 2) and the random-walk -`commuteTimeDist` (paper 1). The JS construction: - - P(u, v) := c_κ(u, v) / ∑_w c_κ(u, w) (row-normalised transition) - KL(p ‖ q) := ∑_i p i · log (p i / q i) (Kullback–Leibler divergence) - JS(p, q) := (KL(p ‖ m) + KL(q ‖ m)) / 2 (Jensen–Shannon divergence) - where m i := (p i + q i) / 2 - jsDist u v := √ (JS(P(u, ·), P(v, ·))) - -`√JS` is a *bounded* metric (in fact ≤ √(log 2)) on probability -measures over a finite alphabet; pulling it back along `P(·, ·)` -gives a (pseudo)metric on `V`. The classical proof is in -Endres–Schindelin (2003) and Österreicher–Vajda (2003). - -## Status - -Definitions live here; the `MetricSpace` instance is stated with -`sorry`. The body splits into three classical lemmas (JS-divergence -non-negativity, symmetry, and the square-root triangle inequality) -that we leave to a follow-up round. --/ - -namespace Hypergraph - -variable {V : Type*} {C : Type*} - -/-- **Math.** Row-normalised transition probability built from the -harmonic conductance: `P(u, v) := c_κ(u, v) / ∑_w c_κ(u, w)`. When -`u` has zero total conductance (degenerate / isolated vertex), this -returns `0`. -/ -noncomputable def transitionProbability [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (u v : V) : ℝ := - let row := ∑ w : V, H.harmonicConductance params u w - if row = 0 then 0 else H.harmonicConductance params u v / row - -/-- **Math.** Kullback–Leibler divergence between two finite-support -distributions `p, q : V → ℝ`: -`KL(p ‖ q) := ∑_v p v · log (p v / q v)`. - -The convention used: `0 · log 0 = 0` (continuous extension), and any -term where `p v = 0` contributes `0`; when `p v > 0` and `q v = 0` -the term is `+∞` morally, but at the `ℝ`-valued level we follow -Mathlib's `Real.log 0 = 0` convention, which makes `KL` finite but -no longer a divergence in the absolute-continuity sense. Callers -should restrict to `p ≪ q`. -/ -noncomputable def klDivergence (V : Type*) [Fintype V] (p q : V → ℝ) : ℝ := - ∑ v : V, p v * Real.log (p v / q v) - -/-- **Math.** Jensen–Shannon divergence: the symmetrised KL with the -midpoint distribution `m := (p + q) / 2` as base. Bounded between -`0` and `log 2` for probability distributions. -/ -noncomputable def jsDivergence (V : Type*) [Fintype V] (p q : V → ℝ) : ℝ := - let m : V → ℝ := fun v => (p v + q v) / 2 - (klDivergence V p m + klDivergence V q m) / 2 - -/-- **Math.** *Jensen–Shannon distance* on `V`: the square root of -the JS divergence of the row-normalised transition probabilities -`P(u, ·)` and `P(v, ·)`. By Endres–Schindelin / Österreicher–Vajda, -this is a metric (in particular satisfies the triangle inequality) -on finite probability measures, and lifts to a (pseudo)metric on `V` -via `P`. -/ -noncomputable def jsDist [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (u v : V) : ℝ := - Real.sqrt - (jsDivergence V - (H.transitionProbability params u) (H.transitionProbability params v)) - -/-- **Math.** Metric axiom: `d(u, u) = 0`. Proof: `m v = (p v + p v)/2 = p v` -so each `KL` term reduces to `p v · log(p v / p v) = 0` (split on `p v = 0`), -hence the JS divergence is `0` and `√0 = 0`. -/ -lemma jsDist_self [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (u : V) : - H.jsDist params u u = 0 := by - unfold jsDist jsDivergence klDivergence - simp only - have hp : ∀ v : V, H.transitionProbability params u v * - Real.log (H.transitionProbability params u v / - ((H.transitionProbability params u v + H.transitionProbability params u v) / 2)) = 0 := by - intro v - set p := H.transitionProbability params u v with hp_def - have hmid : (p + p) / 2 = p := by ring - rw [hmid] - rcases eq_or_ne p 0 with h | h - · simp [h] - · rw [div_self h, Real.log_one, mul_zero] - rw [Finset.sum_congr rfl (fun v _ => hp v), Finset.sum_const_zero, - zero_add, zero_div, Real.sqrt_zero] - -/-- **Math.** Metric axiom: `d(u, v) = d(v, u)`. Proof: the midpoint distribution -is symmetric in `(p, q)` pointwise (`(p + q)/2 = (q + p)/2`), and the numerator -swaps the two `KL` terms via `add_comm`. -/ -lemma jsDist_symm [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (u v : V) : - H.jsDist params u v = H.jsDist params v u := by - unfold jsDist jsDivergence klDivergence - congr 1 - simp only [add_comm] - -/-- **Math.** Metric axiom (triangle inequality): the deep step. The classical -proof (Endres–Schindelin 2003, Österreicher–Vajda 2003) embeds probability -distributions into a Hilbert space via a map `Φ` with `‖Φ(P) − Φ(Q)‖² = JS(P, Q)`; -the triangle then follows from the Hilbert-space triangle inequality. - -**Status (iter-009):** Mathlib provides `InformationTheory.klDiv` only in the -measure-theoretic ENNReal-valued setting (`Mathlib.InformationTheory.KullbackLeibler.Basic`), -not the finite/real-valued setting used here, and has no Hellinger/JS Hilbert -embedding. Per PROGRESS.md iter-009 instructions, this is deferred — the plan -agent will re-decompose into smaller sub-lemmas in iter-010. -/ -lemma jsDist_triangle [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (u v w : V) : - H.jsDist params u w - ≤ H.jsDist params u v + H.jsDist params v w := by - sorry - -/-- **Math.** The `(V, jsDist)` pseudometric space. -/ -@[reducible] -noncomputable def toPseudoMetricSpaceJS [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) : PseudoMetricSpace V where - dist u v := H.jsDist params u v - dist_self := H.jsDist_self params - dist_comm := H.jsDist_symm params - dist_triangle := H.jsDist_triangle params - -end Hypergraph diff --git a/lean/ProofAtlas/Metric/JensenShannon/Instance.lean b/lean/ProofAtlas/Metric/JensenShannon/Instance.lean deleted file mode 100644 index 8b32d8e..0000000 --- a/lean/ProofAtlas/Metric/JensenShannon/Instance.lean +++ /dev/null @@ -1,25 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Metric.Axioms -import ProofAtlas.Metric.JensenShannon.Basic - -/-! -# `jsDist` satisfies `IsHypergraphMetric` - -The required quality certificate for the BDF Jensen–Shannon distance. -Proof body deferred. --/ - -namespace Hypergraph - -/-- **Math.** The BDF Jensen–Shannon distance is a *good BDF metric*. -/ -theorem jsDist_isHypergraphMetric {V C : Type*} [Fintype V] [DecidableEq V] - [∀ H : Hypergraph V C, Fintype H.edges] : - IsHypergraphMetric (fun (H : Hypergraph V C) params u v => - H.jsDist params u v) := by - sorry - -end Hypergraph diff --git a/lean/ProofAtlas/Metric/Resistance/Resistance.lean b/lean/ProofAtlas/Metric/Resistance/Resistance.lean index 4aaf824..345978d 100644 --- a/lean/ProofAtlas/Metric/Resistance/Resistance.lean +++ b/lean/ProofAtlas/Metric/Resistance/Resistance.lean @@ -5,7 +5,6 @@ Authors: MathNetwork -/ import ProofAtlas.Util.Linter.MathTag import ProofAtlas.Metric.Axioms -import ProofAtlas.Metric.CommuteTime.CommuteTime import ProofAtlas.LinearAlgebra.EffectiveResistance import ProofAtlas.LinearAlgebra.MoorePenrose diff --git a/lean/ProofAtlas/Open.lean b/lean/ProofAtlas/Open.lean deleted file mode 100644 index df64cec..0000000 --- a/lean/ProofAtlas/Open.lean +++ /dev/null @@ -1,49 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Util.Linter.MathTag -import ProofAtlas.Hyperbolicity.CommuteTime - -/-! -# Open problems - -Conjectures stated in the paper but currently lacking a proof -strategy. Each is recorded here as a `theorem ... := sorry` to make -the `#print axioms` chain visible and to give a stable target name -for future work. None is consumed by any proven theorem in the rest -of the library. - -## Conjecture 7.3 (worst-case hyperbolicity) - -For balanced layered BDF with bounded arity `≤ k` and bounded reuse -`≤ r`, the scale-invariant ratio `δ / diameter` vanishes at -polynomial rate as `card V → ∞`. Supported by -`experiments/large_scale_experiment.py` (monotone decrease from -`0.04` at `n = 50` to `0.003` at `n = 1000`) and -`experiments/power_law_fit.py` (`δ ∼ diam^{0.39}`, `δ ∼ n^{0.53}`). - -The Lean statement below is the qualitative `→ 0` form; the -polynomial-rate refinement is omitted pending a proof strategy -(see paper §7 "Why this is hard"). No proof strategy is currently -known. -/ - -namespace Hypergraph - -variable {C : Type*} - -/-- **Math.** Conjecture 7.3 (paper, open). Along any sequence of -layered BDF hypergraphs with bounded arity and bounded reuse, the -scale-invariant hyperbolicity `δ / diameter` of the intrinsic walk -(`α ≡ 1`, `β = 1`) falls below any `ε > 0` once the vertex count is -large enough. No proof strategy is currently known. -/ -theorem worst_case_hyperbolicity_conjecture (ε : ℝ) (hε : 0 < ε) : - ∃ N : ℕ, ∀ (V : Type*) [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] (params : WalkParams C), - Fintype.card V ≥ N → - params.beta = 1 → - (∀ c, params.alpha c = 1) → - H.normalizedHyperbolicity params ≤ ε := sorry - -end Hypergraph diff --git a/lean/ProofAtlas/RandomWalk/Spectral/Bridge.lean b/lean/ProofAtlas/RandomWalk/Spectral/Bridge.lean deleted file mode 100644 index 27bf4f3..0000000 --- a/lean/ProofAtlas/RandomWalk/Spectral/Bridge.lean +++ /dev/null @@ -1,95 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.RandomWalk.Spectral.Stationary -import ProofAtlas.Metric.CommuteTime.HittingTime -import ProofAtlas.Metric.CommuteTime.KilledChain -import ProofAtlas.RandomWalk.KemenySnell - -/-! -# BDF → Markov-layer Kemeny-Snell bridge - -The BDF `killedTransMatrix` is definitionally the same as the abstract -`ProofAtlas.RandomWalk.killedMatrix` applied to `H.transMatrix params`. -Using this bridge, the BDF hitting time admits the Kemeny-Snell closed -form `h(u, v) = (Z_{v,v} - Z_{u,v}) / π_v` with `Z` the Markov-layer -fundamental matrix. The killed-chain invertibility is derived directly -from the ergodic `h_conv` hypothesis via -`ProofAtlas.RandomWalk.killedMatrix_one_sub_isUnit`, so this bridge is -sorry-free. - -## Main results - -* `killedTransMatrix_eq_markov_killedMatrix` — definitional bridge. -* `hittingTime_eq_fundamentalMatrix` — Kemeny-Snell at the BDF layer. --/ - -namespace Hypergraph - -variable {V : Type*} {C : Type*} - -/-- **Eng.** The BDF killed transition matrix coincides definitionally -with `ProofAtlas.RandomWalk.killedMatrix` applied to `H.transMatrix params`. -Bridges the BDF-specific killed-recursion machinery to the abstract -Markov-layer Kemeny-Snell theorem. -/ -lemma killedTransMatrix_eq_markov_killedMatrix - [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (v : V) : - H.killedTransMatrix params v - = ProofAtlas.RandomWalk.killedMatrix (H.transMatrix params) v := rfl - -/-- **Math.** **Kemeny-Snell hitting-time identity** (BDF layer). Under -stochasticity of `P = H.transMatrix params`, stationarity and -positivity of `π = H.stationaryDist params`, and entrywise -convergence `P^k → W` (encoding ergodicity), the expected -first-passage time equals - - `h(u, v) = (Z_{v,v} - Z_{u,v}) / π_v` - -with `Z = (I - P + W)⁻¹ - W` the Markov-layer fundamental matrix. -The paper §6 commute-time → fundamental-matrix bridge feeding -Theorem 6.4. -/ -theorem hittingTime_eq_fundamentalMatrix - [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (h_row_sum : ∀ i, ∑ j, H.transMatrix params i j = 1) - (h_stat : ∀ j, ∑ i, H.stationaryDist params i * H.transMatrix params i j - = H.stationaryDist params j) - (h_sum : ∑ i, H.stationaryDist params i = 1) - (h_pos : ∀ v, 0 < H.stationaryDist params v) - (h_conv : Filter.Tendsto (fun k => H.transMatrix params ^ k) Filter.atTop - (nhds (ProofAtlas.RandomWalk.stationaryProjection - (H.stationaryDist params)))) - (u v : V) : - H.hittingTime params u v - = (ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params) v v - - ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params) u v) - / H.stationaryDist params v := by - have h_inv : IsUnit ((ProofAtlas.RandomWalk.auxMatrix - (H.transMatrix params) (H.stationaryDist params)).det) := - ProofAtlas.RandomWalk.auxMatrix_isUnit_det h_row_sum h_stat h_sum h_conv - -- Killed-chain invertibility: derive from h_conv via the Markov-layer - -- `killedMatrix_one_sub_isUnit` (avoids the BDF wrapper which currently - -- routes through the sorry'd `killed_pow_norm_lt_one`). - have h_nonneg : ∀ i j, 0 ≤ H.transMatrix params i j := fun i j => - H.transProb_nonneg params i j - have h_killed_inv : - IsUnit ((1 : Matrix V V ℝ) - - ProofAtlas.RandomWalk.killedMatrix (H.transMatrix params) v) := - ProofAtlas.RandomWalk.killedMatrix_one_sub_isUnit - h_row_sum h_nonneg h_sum h_pos h_conv v - change H.hittingTimeVec params v u = _ - unfold Hypergraph.hittingTimeVec - change (((1 : Matrix V V ℝ) - H.killedTransMatrix params v)⁻¹.mulVec - (ProofAtlas.RandomWalk.killedRhs v)) u = _ - rw [killedTransMatrix_eq_markov_killedMatrix H params v] - exact ProofAtlas.RandomWalk.kemenySnell_hitting_apply - h_row_sum h_stat h_sum h_pos h_inv h_killed_inv u - -end Hypergraph diff --git a/lean/ProofAtlas/RandomWalk/Spectral/Decomposition.lean b/lean/ProofAtlas/RandomWalk/Spectral/Decomposition.lean deleted file mode 100644 index 3193b39..0000000 --- a/lean/ProofAtlas/RandomWalk/Spectral/Decomposition.lean +++ /dev/null @@ -1,232 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.RandomWalk.Spectral.Bridge -import ProofAtlas.Metric.CommuteTime.CommuteTime -import ProofAtlas.Hyperbolicity.CommuteTime - -/-! -# Section 6.1, 6.2: vertex/interaction decomposition - -The commute time `d(u, v)` admits the decomposition - - `d(u, v) = f(u) + f(v) - I(u, v)` (paper Theorem 6.1) - -where `f(v) := E_π[h(·, v)] = Z_{v,v}/π_v` is the vertex term and -`I(u, v) := f(u) + f(v) - d(u, v) = Z_{u,v}/π_v + Z_{v,u}/π_u` is the -interaction term. The four-point Gromov deviation `Φ` depends only on -`I` (paper Corollary 6.2): any vertex-gauge `d ↦ d - g(u) - g(v)` -leaves `Φ` invariant. - -## Main definitions - -* `vertexTerm` — `f(v) := ∑_u π_u · h(u, v)`. -* `interactionTerm` — `I(u, v) := f(u) + f(v) - d(u, v)`. - -## Main results - -* `vertexTerm_eq_fundamentalMatrix` — `f(v) = Z_{v,v}/π_v`. -* `commute_time_vertex_interaction_decomp` — Theorem 6.1. -* `interactionTerm_eq_fundamentalMatrix` — `I(u, v) = Z_{u,v}/π_v + Z_{v,u}/π_u`. -* `interactionTerm_symm` — `I(u, v) = I(v, u)`. -* `fourPointDeviation_depends_only_on_interaction` — Corollary 6.2. --/ - -open ProofAtlas - -namespace Hypergraph - -variable {V : Type*} {C : Type*} - -/-- **Math.** Vertex term `f(v) := ∑_u π(u) · h(u, v)`, the -stationary-weighted mean hitting time to `v`. By the Kemeny-Snell -identity this equals `Z_{v,v}/π(v)` where `Z` is the fundamental -matrix; the present definition avoids introducing `Z` explicitly. -/ -noncomputable def vertexTerm [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (v : V) : ℝ := - ∑ u : V, H.stationaryDist params u * H.hittingTime params u v - -/-- **Math.** Kemeny-Snell vertex-term identification: -`f(v) = Z_{v,v} / π_v`. The stationary-weighted mean hitting time -collapses to a single Z entry because `∑_u π_u Z_{u,v} = 0` (the -column-`v` slice of `W · Z = 0`, -`stationaryProjection_mul_fundamentalMatrix`), so only the -`Z_{v,v} ∑_u π_u = Z_{v,v}` part survives after dividing by `π_v`. -/ -theorem vertexTerm_eq_fundamentalMatrix [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (h_row_sum : ∀ i, ∑ j, H.transMatrix params i j = 1) - (h_stat : ∀ j, ∑ i, H.stationaryDist params i * H.transMatrix params i j - = H.stationaryDist params j) - (h_sum : ∑ i, H.stationaryDist params i = 1) - (h_pos : ∀ v, 0 < H.stationaryDist params v) - (h_conv : Filter.Tendsto (fun k => H.transMatrix params ^ k) Filter.atTop - (nhds (ProofAtlas.RandomWalk.stationaryProjection - (H.stationaryDist params)))) - (v : V) : - H.vertexTerm params v - = ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params) v v - / H.stationaryDist params v := by - have h_πv_ne : H.stationaryDist params v ≠ 0 := (h_pos v).ne' - have h_inv : IsUnit ((ProofAtlas.RandomWalk.auxMatrix - (H.transMatrix params) (H.stationaryDist params)).det) := - ProofAtlas.RandomWalk.auxMatrix_isUnit_det h_row_sum h_stat h_sum h_conv - -- Column-v slice of W · Z = 0: ∑_u π_u Z_{u,v} = 0. - have h_WZ : ProofAtlas.RandomWalk.stationaryProjection (H.stationaryDist params) - * ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params) = 0 := - ProofAtlas.RandomWalk.stationaryProjection_mul_fundamentalMatrix h_stat h_sum h_inv - have h_col : ∑ u, H.stationaryDist params u - * ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params) u v = 0 := by - have h_apply : (ProofAtlas.RandomWalk.stationaryProjection (H.stationaryDist params) - * ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params)) v v = 0 := by - rw [h_WZ]; rfl - rw [Matrix.mul_apply] at h_apply - exact h_apply - -- Unfold vertex term and substitute Kemeny-Snell. - unfold Hypergraph.vertexTerm - simp_rw [H.hittingTime_eq_fundamentalMatrix params h_row_sum h_stat h_sum h_pos h_conv] - calc ∑ u, H.stationaryDist params u - * ((ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params) v v - - ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params) u v) - / H.stationaryDist params v) - = ∑ u, (H.stationaryDist params u - * ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params) v v - - H.stationaryDist params u - * ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params) u v) - / H.stationaryDist params v := by - apply Finset.sum_congr rfl - intros u _ - field_simp - _ = (∑ u, H.stationaryDist params u - * ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params) v v - - ∑ u, H.stationaryDist params u - * ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params) u v) - / H.stationaryDist params v := by - rw [← Finset.sum_div, Finset.sum_sub_distrib] - _ = ((∑ u, H.stationaryDist params u) - * ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params) v v - - 0) - / H.stationaryDist params v := by - rw [← Finset.sum_mul, h_col] - _ = ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params) v v - / H.stationaryDist params v := by - rw [h_sum]; ring - -/-- **Math.** Interaction term `I(u, v) := f(u) + f(v) - d(u, v)`. -By Theorem 6.1 (`commute_time_vertex_interaction_decomp`) and the -Kemeny-Snell formula this equals `Z_{u,v}/π(v) + Z_{v,u}/π(u)`. -/ -noncomputable def interactionTerm [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (u v : V) : ℝ := - H.vertexTerm params u + H.vertexTerm params v - H.commuteTimeDist params u v - -/-- **Math.** Paper Theorem 6.1. The commute time decomposes as -`d(u, v) = f(u) + f(v) - I(u, v)` with `f` the vertex term and `I` -the interaction term. Direct from the definition of `interactionTerm` -once `f`, `I` are concrete; the mathematical content lives in the -Kemeny-Snell identification `f(v) = Z_{v,v}/π(v)` and the spectral -expansion of `I` (Theorem 6.4). -/ -theorem commute_time_vertex_interaction_decomp [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (u v : V) : - H.commuteTimeDist params u v - = H.vertexTerm params u + H.vertexTerm params v - - H.interactionTerm params u v := by - unfold interactionTerm - ring - -/-- **Math.** Kemeny-Snell interaction-term identification: -`I(u, v) = Z_{u,v}/π_v + Z_{v,u}/π_u`. Combines -`vertexTerm_eq_fundamentalMatrix` (`f(w) = Z_{w,w}/π_w`) with the -hitting-time formula `h(u, v) = (Z_{v,v} - Z_{u,v})/π_v`: the -diagonal `Z` entries cancel between `f(u) + f(v)` and the commute -time, leaving the off-diagonal cross terms `Z_{u,v}/π_v + Z_{v,u}/π_u`. -/ -theorem interactionTerm_eq_fundamentalMatrix [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (h_row_sum : ∀ i, ∑ j, H.transMatrix params i j = 1) - (h_stat : ∀ j, ∑ i, H.stationaryDist params i * H.transMatrix params i j - = H.stationaryDist params j) - (h_sum : ∑ i, H.stationaryDist params i = 1) - (h_pos : ∀ v, 0 < H.stationaryDist params v) - (h_conv : Filter.Tendsto (fun k => H.transMatrix params ^ k) Filter.atTop - (nhds (ProofAtlas.RandomWalk.stationaryProjection - (H.stationaryDist params)))) - (u v : V) : - H.interactionTerm params u v - = ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params) u v - / H.stationaryDist params v - + ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params) v u - / H.stationaryDist params u := by - unfold Hypergraph.interactionTerm Hypergraph.commuteTimeDist - rw [H.vertexTerm_eq_fundamentalMatrix params h_row_sum h_stat h_sum h_pos h_conv u, - H.vertexTerm_eq_fundamentalMatrix params h_row_sum h_stat h_sum h_pos h_conv v, - H.hittingTime_eq_fundamentalMatrix params h_row_sum h_stat h_sum h_pos h_conv u v, - H.hittingTime_eq_fundamentalMatrix params h_row_sum h_stat h_sum h_pos h_conv v u] - have h_πu_ne : H.stationaryDist params u ≠ 0 := (h_pos u).ne' - have h_πv_ne : H.stationaryDist params v ≠ 0 := (h_pos v).ne' - field_simp - ring - -/-- **Math.** Interaction term is symmetric: `I(u, v) = I(v, u)`. -Immediate from the symmetry of the commute time -(`commuteTimeDist_symm`). -/ -theorem interactionTerm_symm [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (u v : V) : - H.interactionTerm params u v = H.interactionTerm params v u := by - unfold interactionTerm - rw [H.commuteTimeDist_symm params, add_comm (H.vertexTerm params u)] - -/-- **Math.** Paper Corollary 6.2. The four-point Gromov deviation -is invariant under any vertex-gauge `d(u, v) ↦ d(u, v) - g(u) - g(v)`. -Together with Theorem 6.1, this gives that `Φ` depends only on the -interaction term `I`. -/ -theorem fourPointDeviation_depends_only_on_interaction - [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (g : V → ℝ) (u v x y : V) : - fourPointDeviation (H.commuteTimeDist params) u v x y - = fourPointDeviation - (fun a b => H.commuteTimeDist params a b - g a - g b) u v x y := by - unfold fourPointDeviation - set d := H.commuteTimeDist params - set G := g u + g v + g x + g y with hG_def - -- Each shifted pairwise sum is the original minus G. - have h1 : d u v - g u - g v + (d x y - g x - g y) = d u v + d x y - G := by - simp only [hG_def]; ring - have h2 : d u x - g u - g x + (d v y - g v - g y) = d u x + d v y - G := by - simp only [hG_def]; ring - have h3 : d u y - g u - g y + (d v x - g v - g x) = d u y + d v x - G := by - simp only [hG_def]; ring - simp only [h1, h2, h3] - -- Uniform shift preserves max/min structure. - have max_shift : ∀ a b : ℝ, max (a - G) (b - G) = max a b - G := fun a b => by - rcases le_total a b with h | h - · rw [max_eq_right h, max_eq_right (sub_le_sub_right h G)] - · rw [max_eq_left h, max_eq_left (sub_le_sub_right h G)] - have min_shift : ∀ a b : ℝ, min (a - G) (b - G) = min a b - G := fun a b => by - rcases le_total a b with h | h - · rw [min_eq_left h, min_eq_left (sub_le_sub_right h G)] - · rw [min_eq_right h, min_eq_right (sub_le_sub_right h G)] - simp only [max_shift, min_shift] - ring - -end Hypergraph diff --git a/lean/ProofAtlas/RandomWalk/Spectral/Default.lean b/lean/ProofAtlas/RandomWalk/Spectral/Default.lean deleted file mode 100644 index 437e8ec..0000000 --- a/lean/ProofAtlas/RandomWalk/Spectral/Default.lean +++ /dev/null @@ -1,41 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.RandomWalk.Spectral.Stationary -import ProofAtlas.RandomWalk.Spectral.Spectrum -import ProofAtlas.RandomWalk.Spectral.Expectation -import ProofAtlas.RandomWalk.Spectral.Bridge -import ProofAtlas.RandomWalk.Spectral.Decomposition -import ProofAtlas.RandomWalk.Spectral.SpectralForm -import ProofAtlas.RandomWalk.Spectral.Moments -import ProofAtlas.RandomWalk.Spectral.ProbabilisticBound - -/-! -# Spectral analysis of the BDF random walk — facade - -Spectrum of the symmetrised transition matrix -`Q = D^{1/2} P D^{-1/2}` of the BDF random walk (paper 1 §§6–7). - -Files inside: - -* `Stationary` — transition matrix, stationary distribution, - reversibility, symmetrised transition matrix. -* `Spectrum` — Kemeny constant `K`, second Kemeny moment `κ₂`, - spectral gap `γ`, stationary-normalised eigenfunctions - `ψᵢ := φᵢ/√π`. -* `Expectation` — iid expectations `meanCommuteTime`, - `meanFourPointDeviation`, paper-notation aliases `δAvg, 𝔼d, 𝔼Φ`. -* `Bridge` — BDF → Markov-layer Kemeny–Snell bridge. -* `Decomposition` — vertex/interaction decomposition (paper §6). -* `SpectralForm` — spectral expansion of `I(u, v)` and the - four-point spectral identity. -* `Moments` — second-moment computations. -* `ProbabilisticBound` — paper Theorem 7.1 (probabilistic - hyperbolicity bound) plus Corollary 7.2. - -The companion spectral story for the *electrical* / variational -metric `d_H` lives at `Metric/Resistance/Spectral.lean` (re-exported -from `Metric/Default.lean`). --/ diff --git a/lean/ProofAtlas/RandomWalk/Spectral/Expectation.lean b/lean/ProofAtlas/RandomWalk/Spectral/Expectation.lean deleted file mode 100644 index b5b3e89..0000000 --- a/lean/ProofAtlas/RandomWalk/Spectral/Expectation.lean +++ /dev/null @@ -1,125 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.RandomWalk.Spectral.Stationary -import ProofAtlas.RandomWalk.Spectral.Spectrum -import ProofAtlas.Metric.CommuteTime.CommuteTime -import ProofAtlas.Hyperbolicity.CommuteTime - -/-! -# Stationary expectations and paper notation aliases - -The iid stationary expectations `expect2` (over `V × V`) and `expect4` -(over `V × V × V × V`), and the specific means `meanCommuteTime` and -`meanFourPointDeviation` they yield when applied to `d` and `Φ`. - -Plus paper-notation `abbrev`s so the headline statement of Theorem 7.1 -reads like the paper: - - paper : `δ̄(H) ≤ 2 √(3 κ₂)`, `E[d] = 2 K` - Lean : `H.δAvg params ≤ 2 * √(3 * H.κ₂ params hQ) ∧ H.𝔼d params = 2 * H.K params hQ` - -The paper symbol `δ̄` uses a combining macron, which Lean 4 does not -accept in identifiers; the ASCII-friendly `δAvg` is used. - -## Main definitions - -* `expect2`, `expect4` — generic iid stationary expectations. -* `meanFourPointDeviation`, `meanCommuteTime` — instantiations at `Φ` and `d`. -* `avgHyperbolicityConstant` — `δ̄(H) := (1/2) E_{π⊗⁴}[Φ]`. -* `δAvg`, `K`, `κ₂`, `𝔼d`, `𝔼Φ` — paper-notation `abbrev` aliases. --/ - -open ProofAtlas - -namespace Hypergraph - -variable {V : Type*} {C : Type*} - -/-- **Math.** `E_{(U,V) ~ π⊗π}[f(U, V)]`, iid stationary expectation -of a two-point payoff `f : V × V → ℝ`. -/ -noncomputable def expect2 [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (f : V × V → ℝ) : ℝ := - ∑ p : V × V, - H.stationaryDist params p.1 - * H.stationaryDist params p.2 - * f p - -/-- **Math.** `E_{(U,V,X,Y) ~ π⊗⁴}[f(U, V, X, Y)]`, iid stationary -expectation of a four-point payoff `f : V × V × V × V → ℝ`. -/ -noncomputable def expect4 [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (f : V × V × V × V → ℝ) : ℝ := - ∑ p : V × V × V × V, - H.stationaryDist params p.1 - * H.stationaryDist params p.2.1 - * H.stationaryDist params p.2.2.1 - * H.stationaryDist params p.2.2.2 - * f p - -/-- **Math.** `E_{(U,V,X,Y) ~ π⊗⁴}[Φ(U,V,X,Y)]`, mean four-point -deviation. Instantiation of `expect4` at the four-point Gromov -deviation `Φ`. -/ -noncomputable def meanFourPointDeviation [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) : ℝ := - H.expect4 params fun p => - fourPointDeviation (H.commuteTimeDist params) p.1 p.2.1 p.2.2.1 p.2.2.2 - -/-- **Math.** `E_{(U,V) ~ π⊗π}[d(U,V)]`, mean commute time. -Instantiation of `expect2` at the commute-time distance `d`. -/ -noncomputable def meanCommuteTime [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) : ℝ := - H.expect2 params fun p => H.commuteTimeDist params p.1 p.2 - -/-- **Math.** Average hyperbolicity constant -`δ̄(H) := (1/2) · E_{π⊗4}[Φ]` (paper Definition 7.0). The -iid-stationary counterpart of `hyperbolicityConstant` (= `(1/2) sup Φ`): -the worst case is averaged with `sup`, the average case with `E_{π⊗4}`. -Paper-notation alias: `Hypergraph.δAvg` (the bar version `δ̄` is not -available as a Lean identifier — Lean 4 does not accept combining-mark -sequences in identifiers — so the ASCII-friendly `δAvg` is used). -/ -noncomputable def avgHyperbolicityConstant [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) : ℝ := - H.meanFourPointDeviation params / 2 - -/-! ### Paper notation aliases -/ - -/-- **Math.** Paper notation. `δ̄(H) := (1/2) E_{π⊗⁴}[Φ]`, average hyperbolicity. -/ -noncomputable abbrev δAvg [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) : ℝ := - H.avgHyperbolicityConstant params - -/-- **Math.** Paper notation. `K := ∑_{λᵢ<1} 1/(1-λᵢ)`, Kemeny constant. -/ -noncomputable abbrev K [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (hQ : (H.symmetrisedTransMatrix params).IsHermitian) : ℝ := - H.kemenyConstant params hQ - -/-- **Math.** Paper notation. `κ₂ := ∑_{λᵢ<1} 1/(1-λᵢ)²`, second Kemeny moment. -/ -noncomputable abbrev κ₂ [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (hQ : (H.symmetrisedTransMatrix params).IsHermitian) : ℝ := - H.secondKemenyMoment params hQ - -/-- **Math.** Paper notation. `𝔼d := E_{(U,V) ~ π⊗π}[d(U,V)]`, mean commute time. -/ -noncomputable abbrev 𝔼d [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) : ℝ := - H.meanCommuteTime params - -/-- **Math.** Paper notation. `𝔼Φ := E_{π⊗⁴}[Φ]`, mean four-point deviation. -/ -noncomputable abbrev 𝔼Φ [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) : ℝ := - H.meanFourPointDeviation params - -end Hypergraph diff --git a/lean/ProofAtlas/RandomWalk/Spectral/Moments.lean b/lean/ProofAtlas/RandomWalk/Spectral/Moments.lean deleted file mode 100644 index 76c59a0..0000000 --- a/lean/ProofAtlas/RandomWalk/Spectral/Moments.lean +++ /dev/null @@ -1,487 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.RandomWalk.Spectral.SpectralForm -import ProofAtlas.RandomWalk.Spectral.Expectation -import ProofAtlas.RandomWalk.KemenySnell - -/-! -# Spectral second-moment computations - -The two headline moment identities of paper Section 7: - - • `expected_sq_pair_sum_diff` : `E_{π⊗⁴}[(S₁ - S₂)²] = 16 κ₂` - • `meanCommuteTime_eq_two_mul_kemenyConstant` : `E[d] = 2 K` - -The first is the key step in Theorem 7.1 (the four-point Gromov -deviation `Φ` then satisfies `Φ² ≤ (S₁-S₂)² + (S₂-S₃)² + (S₁-S₃)²` -algebraically, and the three squared differences have equal mean by -symmetry, so `E[Φ²] ≤ 48 κ₂`). The second is the simpler right -conjunct of the same theorem. - -The proof of `expected_sq_pair_sum_diff` proceeds in four blocks: -(1) substitute the spectral form of Theorem 6.5 into `(S₁-S₂)²`; -(2) expand the square as a double sum over the spectrum (`Finset.sum_mul_sum`); -(3) swap the orders of summation (Fubini); (4) for each spectral -index `(i, j)`, factor the four-point weight `π⁴` as -`π · π × π · π` and apply `expected_psiDiff_inner_nontop` to each -two-point factor (the cross-term cancellation), then collapse the -double sum to the diagonal where `i = j`. - -## Main results - -* `expected_psiDiff_inner_nontop` — `E_{(A,B)~π⊗π}[(ψᵢ(A)-ψᵢ(B))(ψⱼ(A)-ψⱼ(B))] = 2 δᵢⱼ`. -* `expected_sq_pair_sum_diff` — `E_{π⊗⁴}[(S₁ - S₂)²] = 16 κ₂`. -* `meanCommuteTime_eq_two_mul_kemenyConstant` — `E[d] = 2 K`. --/ - -namespace Hypergraph - -variable {V : Type*} {C : Type*} - - -/-- **Math.** **Second moment of `ψᵢ - ψⱼ`-difference products.** For -non-top eigenvectors `i, j`, the double expectation -`E_{(A,B) ~ π⊗π}[(ψᵢ(A) - ψᵢ(B))(ψⱼ(A) - ψⱼ(B))] = 2 δᵢⱼ`. - -This is the cross-term cancellation at the heart of the spectral second -moment `E_{π⁴}[(S₁-S₂)²] = 16 κ₂`: independence factors the four-point -expectation into the product of two such two-point expectations, each -contributing `2 δᵢⱼ`. - -Pipeline: expand `(a - b)(c - d) = ac - ad - bc + bd`, distribute the -product `π_a · π_b` over the four pieces, use Fubini to split each -double sum, then apply `sum_stationary_reversibleEigenfunction_product` -to the diagonal pieces (using `∑ π = 1`) and -`sum_stationary_reversibleEigenfunction_nontop` to the cross pieces -(linear moment vanishes for non-top `i, j`). -/ -lemma expected_psiDiff_inner_nontop [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (hQ : (H.symmetrisedTransMatrix params).IsHermitian) - (h_sum : ∑ i, H.stationaryDist params i = 1) - (h_pos : ∀ v, 0 < H.stationaryDist params v) - (h_top_eigvec : ∀ i, hQ.eigenvalues i = 1 → - ∀ v, hQ.eigenvectorBasis i v = Real.sqrt (H.stationaryDist params v)) - (h_top_unique : ∃! i_top : V, hQ.eigenvalues i_top = 1) - {i j : V} - (hi : hQ.eigenvalues i < 1) (hj : hQ.eigenvalues j < 1) : - ∑ p : V × V, H.stationaryDist params p.1 * H.stationaryDist params p.2 - * ((H.reversibleEigenfunction params hQ i p.1 - - H.reversibleEigenfunction params hQ i p.2) - * (H.reversibleEigenfunction params hQ j p.1 - - H.reversibleEigenfunction params hQ j p.2)) - = 2 * (if i = j then 1 else 0) := by - -- Abbreviations. - set π := H.stationaryDist params with hπ_def - set ψ := H.reversibleEigenfunction params hQ with hψ_def - -- Expand the product `(ψᵢ(a)-ψᵢ(b))(ψⱼ(a)-ψⱼ(b))` and distribute the weight. - have h_expand : ∀ p : V × V, - π p.1 * π p.2 * ((ψ i p.1 - ψ i p.2) * (ψ j p.1 - ψ j p.2)) - = π p.1 * (ψ i p.1 * ψ j p.1) * π p.2 - + π p.1 * π p.2 * (ψ i p.2 * ψ j p.2) - - π p.1 * ψ i p.1 * (π p.2 * ψ j p.2) - - π p.2 * ψ i p.2 * (π p.1 * ψ j p.1) := fun _ => by ring - rw [show (∑ p : V × V, π p.1 * π p.2 - * ((ψ i p.1 - ψ i p.2) * (ψ j p.1 - ψ j p.2))) - = ∑ p : V × V, - (π p.1 * (ψ i p.1 * ψ j p.1) * π p.2 - + π p.1 * π p.2 * (ψ i p.2 * ψ j p.2) - - π p.1 * ψ i p.1 * (π p.2 * ψ j p.2) - - π p.2 * ψ i p.2 * (π p.1 * ψ j p.1)) from - Finset.sum_congr rfl (fun p _ => h_expand p)] - -- Split into four pieces. - rw [Finset.sum_sub_distrib, Finset.sum_sub_distrib, Finset.sum_add_distrib] - -- Now refactor each piece via `Fintype.sum_prod_type` + `Finset.sum_mul`. - rw [Fintype.sum_prod_type, Fintype.sum_prod_type, - Fintype.sum_prod_type, Fintype.sum_prod_type] - -- Piece 1: ∑_a (π_a · ψᵢ(a)ψⱼ(a)) · (∑_b π_b) = δᵢⱼ · 1. - have h_T1 : (∑ a : V, ∑ b : V, π a * (ψ i a * ψ j a) * π b) - = if i = j then 1 else 0 := by - simp_rw [← Finset.mul_sum, h_sum, mul_one] - have := H.sum_stationary_reversibleEigenfunction_product params hQ h_pos i j - simpa [π, ψ] using this - -- Piece 2: ∑_b (∑_a π_a) · (π_b · ψᵢ(b)ψⱼ(b)) = 1 · δᵢⱼ. - have h_T2 : (∑ a : V, ∑ b : V, π a * π b * (ψ i b * ψ j b)) - = if i = j then 1 else 0 := by - have h_swap : (∑ a : V, ∑ b : V, π a * π b * (ψ i b * ψ j b)) - = ∑ a : V, π a * (∑ b : V, π b * (ψ i b * ψ j b)) := by - refine Finset.sum_congr rfl (fun a _ => ?_) - rw [Finset.mul_sum] - refine Finset.sum_congr rfl (fun b _ => by ring) - rw [h_swap, ← Finset.sum_mul, h_sum, one_mul] - have := H.sum_stationary_reversibleEigenfunction_product params hQ h_pos i j - simpa [π, ψ] using this - -- Piece 3: (∑_a π_a ψᵢ(a)) · (∑_b π_b ψⱼ(b)) = 0 · 0. - have h_T3 : (∑ a : V, ∑ b : V, π a * ψ i a * (π b * ψ j b)) = 0 := by - simp_rw [← Finset.mul_sum] - rw [← Finset.sum_mul] - have h_lin_i := H.sum_stationary_reversibleEigenfunction_nontop - params hQ h_pos h_top_eigvec h_top_unique hi - have h_lin_j := H.sum_stationary_reversibleEigenfunction_nontop - params hQ h_pos h_top_eigvec h_top_unique hj - rw [h_lin_i, h_lin_j, zero_mul] - -- Piece 4: swap roles of a, b ↦ same as Piece 3. - have h_T4 : (∑ a : V, ∑ b : V, π b * ψ i b * (π a * ψ j a)) = 0 := by - have h_swap : (∑ a : V, ∑ b : V, π b * ψ i b * (π a * ψ j a)) - = ∑ a : V, ∑ b : V, π a * ψ i a * (π b * ψ j b) := by - rw [Finset.sum_comm] - rw [h_swap, h_T3] - rw [h_T1, h_T2, h_T3, h_T4] - by_cases hij : i = j - · simp [hij]; norm_num - · simp [hij] - -/-- **Math.** **Spectral second moment** (paper Theorem 7.1, key step). -For iid stationary samples `(U, V, X, Y) ~ π⁴`, -`E[(S₁ - S₂)²] = 16 κ₂` where `S₁ = d(U,V) + d(X,Y)` and -`S₂ = d(U,X) + d(V,Y)`. Substitute Theorem 6.5 spectrally, expand the -square, swap sum orders, apply `expected_psiDiff_inner_nontop` to -each factor, and collapse the (i,j) double sum to its diagonal. -/ -lemma expected_sq_pair_sum_diff [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (hQ : (H.symmetrisedTransMatrix params).IsHermitian) - (h_row_sum : ∀ i, ∑ j, H.transMatrix params i j = 1) - (h_stat : ∀ j, ∑ i, H.stationaryDist params i * H.transMatrix params i j - = H.stationaryDist params j) - (h_sum : ∑ i, H.stationaryDist params i = 1) - (h_pos : ∀ v, 0 < H.stationaryDist params v) - (h_conv : Filter.Tendsto (fun k => H.transMatrix params ^ k) Filter.atTop - (nhds (ProofAtlas.RandomWalk.stationaryProjection - (H.stationaryDist params)))) - (h_eig_le_one : ∀ i, hQ.eigenvalues i ≤ 1) - (h_top_eigvec : ∀ i, hQ.eigenvalues i = 1 → - ∀ v, hQ.eigenvectorBasis i v = Real.sqrt (H.stationaryDist params v)) - (h_top_unique : ∃! i_top : V, hQ.eigenvalues i_top = 1) - (h_nontop_bound : ∀ i, hQ.eigenvalues i < 1 → |hQ.eigenvalues i| < 1) : - ∑ p : V × V × V × V, - H.stationaryDist params p.1 - * H.stationaryDist params p.2.1 - * H.stationaryDist params p.2.2.1 - * H.stationaryDist params p.2.2.2 - * (H.commuteTimeDist params p.1 p.2.1 - + H.commuteTimeDist params p.2.2.1 p.2.2.2 - - (H.commuteTimeDist params p.1 p.2.2.1 - + H.commuteTimeDist params p.2.1 p.2.2.2))^2 - = 16 * H.secondKemenyMoment params hQ := by - set π := H.stationaryDist params - set ψ := H.reversibleEigenfunction params hQ - have h_65 : ∀ u v x y : V, - H.commuteTimeDist params u v + H.commuteTimeDist params x y - - (H.commuteTimeDist params u x + H.commuteTimeDist params v y) - = -2 * ∑ i : V, if hQ.eigenvalues i < 1 - then (ψ i u - ψ i y) * (ψ i v - ψ i x) / (1 - hQ.eigenvalues i) - else 0 := - H.four_point_spectral_identity params hQ h_row_sum h_stat h_sum h_pos h_conv - h_eig_le_one h_top_eigvec h_top_unique h_nontop_bound - set G : V → V × V × V × V → ℝ := fun i p => - if hQ.eigenvalues i < 1 - then (ψ i p.1 - ψ i p.2.2.2) * (ψ i p.2.1 - ψ i p.2.2.1) - / (1 - hQ.eigenvalues i) - else 0 with hG_def - -- Per-tuple: square expansion. - have h_step1 : ∀ p : V × V × V × V, - π p.1 * π p.2.1 * π p.2.2.1 * π p.2.2.2 - * (H.commuteTimeDist params p.1 p.2.1 - + H.commuteTimeDist params p.2.2.1 p.2.2.2 - - (H.commuteTimeDist params p.1 p.2.2.1 - + H.commuteTimeDist params p.2.1 p.2.2.2))^2 - = ∑ i : V, ∑ j : V, - 4 * (π p.1 * π p.2.1 * π p.2.2.1 * π p.2.2.2) - * (G i p * G j p) := by - intro p - rw [h_65 p.1 p.2.1 p.2.2.1 p.2.2.2] - have h_sq : (-2 * ∑ i : V, G i p)^2 - = 4 * ((∑ i : V, G i p) * (∑ j : V, G j p)) := by ring - have h_id : (∑ i : V, if hQ.eigenvalues i < 1 - then (ψ i p.1 - ψ i p.2.2.2) * (ψ i p.2.1 - ψ i p.2.2.1) - / (1 - hQ.eigenvalues i) else 0) = ∑ i : V, G i p := by - simp only [hG_def] - rw [h_id, h_sq, Finset.sum_mul_sum] - simp_rw [Finset.mul_sum] - refine Finset.sum_congr rfl (fun i _ => ?_) - refine Finset.sum_congr rfl (fun j _ => ?_) - ring - simp_rw [h_step1] - rw [show (∑ p : V × V × V × V, ∑ i : V, ∑ j : V, - 4 * (π p.1 * π p.2.1 * π p.2.2.1 * π p.2.2.2) * (G i p * G j p)) - = ∑ i : V, ∑ j : V, ∑ p : V × V × V × V, - 4 * (π p.1 * π p.2.1 * π p.2.2.1 * π p.2.2.2) * (G i p * G j p) by - rw [Finset.sum_comm] - refine Finset.sum_congr rfl (fun i _ => ?_) - rw [Finset.sum_comm]] - have h_inner : ∀ i j : V, - (∑ p : V × V × V × V, - 4 * (π p.1 * π p.2.1 * π p.2.2.1 * π p.2.2.2) * (G i p * G j p)) - = if hQ.eigenvalues i < 1 ∧ hQ.eigenvalues j < 1 ∧ i = j - then 16 * ((1 - hQ.eigenvalues i)⁻¹)^2 else 0 := by - intro i j - by_cases hi : hQ.eigenvalues i < 1 - swap - · simp only [hi, false_and, if_false] - refine Finset.sum_eq_zero (fun p _ => ?_) - simp [hG_def, hi] - by_cases hj : hQ.eigenvalues j < 1 - swap - · simp only [hi, hj, true_and] - refine Finset.sum_eq_zero (fun p _ => ?_) - simp [hG_def, hj] - have hG_pos : ∀ p : V × V × V × V, - G i p = (ψ i p.1 - ψ i p.2.2.2) * (ψ i p.2.1 - ψ i p.2.2.1) - / (1 - hQ.eigenvalues i) := fun p => by simp [hG_def, hi] - have hG'_pos : ∀ p : V × V × V × V, - G j p = (ψ j p.1 - ψ j p.2.2.2) * (ψ j p.2.1 - ψ j p.2.2.1) - / (1 - hQ.eigenvalues j) := fun p => by simp [hG_def, hj] - simp_rw [hG_pos, hG'_pos] - rw [show (∑ p : V × V × V × V, - 4 * (π p.1 * π p.2.1 * π p.2.2.1 * π p.2.2.2) - * (((ψ i p.1 - ψ i p.2.2.2) * (ψ i p.2.1 - ψ i p.2.2.1) - / (1 - hQ.eigenvalues i)) - * ((ψ j p.1 - ψ j p.2.2.2) * (ψ j p.2.1 - ψ j p.2.2.1) - / (1 - hQ.eigenvalues j)))) - = 4 * ((1 - hQ.eigenvalues i)⁻¹ * (1 - hQ.eigenvalues j)⁻¹) - * ∑ p : V × V × V × V, - ((π p.1 * π p.2.2.2) - * ((ψ i p.1 - ψ i p.2.2.2) * (ψ j p.1 - ψ j p.2.2.2))) - * ((π p.2.1 * π p.2.2.1) - * ((ψ i p.2.1 - ψ i p.2.2.1) * (ψ j p.2.1 - ψ j p.2.2.1))) by - rw [Finset.mul_sum] - refine Finset.sum_congr rfl (fun p _ => ?_) - rw [div_eq_mul_inv, div_eq_mul_inv] - ring] - -- LHS sum-order after sum_prod_type: (p.1, p.2.1, p.2.2.1, p.2.2.2) = (u, v, x, y). - -- RHS uses Finset.sum_mul_sum → (q.1, q.2, r.1, r.2). For ring to close the - -- per-tuple identity, we must align the orders. We reorder the LHS sums - -- (u, v, x, y) → (u, y, v, x) via two Finset.sum_comm swaps before invoking ring. - have h_factor : (∑ p : V × V × V × V, - ((π p.1 * π p.2.2.2) - * ((ψ i p.1 - ψ i p.2.2.2) * (ψ j p.1 - ψ j p.2.2.2))) - * ((π p.2.1 * π p.2.2.1) - * ((ψ i p.2.1 - ψ i p.2.2.1) * (ψ j p.2.1 - ψ j p.2.2.1)))) - = (∑ q : V × V, π q.1 * π q.2 - * ((ψ i q.1 - ψ i q.2) * (ψ j q.1 - ψ j q.2))) - * (∑ r : V × V, π r.1 * π r.2 - * ((ψ i r.1 - ψ i r.2) * (ψ j r.1 - ψ j r.2))) := by - -- Step a: on RHS, merge the two 2D sums into a nested form before any - -- product-type expansion (else the pattern `(∑) * (∑)` would be gone). - rw [Finset.sum_mul_sum] - -- Step b: now expand all product-type sums into nested V-sums on both sides. - simp_rw [Fintype.sum_prod_type] - -- Step c: reorder LHS from `∑ u, ∑ v, ∑ x, ∑ y` to `∑ u, ∑ y, ∑ v, ∑ x` - -- (to align with RHS's q.1, q.2, r.1, r.2 binding order). Two adjacent - -- swaps suffice: (x, y) inside ∑ v, then (v, y) at top. - rw [show (∑ u : V, ∑ v : V, ∑ x : V, ∑ y : V, - (π u * π y * ((ψ i u - ψ i y) * (ψ j u - ψ j y))) - * (π v * π x * ((ψ i v - ψ i x) * (ψ j v - ψ j x)))) - = ∑ u : V, ∑ y : V, ∑ v : V, ∑ x : V, - (π u * π y * ((ψ i u - ψ i y) * (ψ j u - ψ j y))) - * (π v * π x * ((ψ i v - ψ i x) * (ψ j v - ψ j x))) by - refine Finset.sum_congr rfl (fun u _ => ?_) - rw [show (∑ v : V, ∑ x : V, ∑ y : V, - (π u * π y * ((ψ i u - ψ i y) * (ψ j u - ψ j y))) - * (π v * π x * ((ψ i v - ψ i x) * (ψ j v - ψ j x)))) - = ∑ v : V, ∑ y : V, ∑ x : V, - (π u * π y * ((ψ i u - ψ i y) * (ψ j u - ψ j y))) - * (π v * π x * ((ψ i v - ψ i x) * (ψ j v - ψ j x))) from - Finset.sum_congr rfl (fun _ _ => Finset.sum_comm)] - rw [Finset.sum_comm]] - rw [h_factor] - have h_eval := H.expected_psiDiff_inner_nontop params hQ - h_sum h_pos h_top_eigvec h_top_unique hi hj - rw [h_eval] - by_cases hij : i = j - · simp [hj, hij, sq] - ring - · simp [hij] - simp_rw [h_inner] - rw [show (∑ i : V, ∑ j : V, - if hQ.eigenvalues i < 1 ∧ hQ.eigenvalues j < 1 ∧ i = j - then 16 * ((1 - hQ.eigenvalues i)⁻¹)^2 else 0) - = ∑ i : V, if hQ.eigenvalues i < 1 - then 16 * ((1 - hQ.eigenvalues i)⁻¹)^2 else 0 by - refine Finset.sum_congr rfl (fun i _ => ?_) - rw [Finset.sum_eq_single i] - · by_cases hi : hQ.eigenvalues i < 1 - · simp [hi] - · simp [hi] - · intros j _ hji - have h_ne_inner : ¬ (hQ.eigenvalues i < 1 ∧ hQ.eigenvalues j < 1 ∧ i = j) := - fun ⟨_, _, hij⟩ => hji hij.symm - simp [h_ne_inner] - · intro hi; exact absurd (Finset.mem_univ _) hi] - unfold Hypergraph.secondKemenyMoment - rw [Finset.mul_sum] - refine Finset.sum_congr rfl (fun i _ => ?_) - by_cases hi : hQ.eigenvalues i < 1 - · simp [hi] - · simp [hi] - -/-- **Math.** **Kemeny constant identity.** Stationary mean of the -commute time equals twice the Kemeny constant: - - `E_{(U,V) ~ π⊗π}[d(U,V)] = 2 ∑_{λᵢ<1} 1/(1-λᵢ)`. - -Half of paper Theorem 7.1. Pipeline: substitute Kemeny-Snell into -`d = h + h`, the `π` factors cancel into `π_u · (Z_{v,v} - Z_{u,v}) -+ π_v · (Z_{u,u} - Z_{v,u})`; Fubini splits the double sum, and the -column-`v` slice of `W · Z = 0` kills the off-diagonal pieces while -`∑_u π_u = 1` and the diagonal trace `∑_v Z_{v,v} = K` (via -spectral expansion + orthonormality) contribute one `K` each, total -`2K`. -/ -theorem meanCommuteTime_eq_two_mul_kemenyConstant - [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (hQ : (H.symmetrisedTransMatrix params).IsHermitian) - (h_row_sum : ∀ i, ∑ j, H.transMatrix params i j = 1) - (h_stat : ∀ j, ∑ i, H.stationaryDist params i * H.transMatrix params i j - = H.stationaryDist params j) - (h_sum : ∑ i, H.stationaryDist params i = 1) - (h_pos : ∀ v, 0 < H.stationaryDist params v) - (h_conv : Filter.Tendsto (fun k => H.transMatrix params ^ k) Filter.atTop - (nhds (ProofAtlas.RandomWalk.stationaryProjection - (H.stationaryDist params)))) - (h_eig_le_one : ∀ i, hQ.eigenvalues i ≤ 1) - (h_top_eigvec : ∀ i, hQ.eigenvalues i = 1 → - ∀ v, hQ.eigenvectorBasis i v = Real.sqrt (H.stationaryDist params v)) - (h_top_unique : ∃! i_top : V, hQ.eigenvalues i_top = 1) - (h_nontop_bound : ∀ i, hQ.eigenvalues i < 1 → |hQ.eigenvalues i| < 1) : - H.meanCommuteTime params = 2 * H.kemenyConstant params hQ := by - have h_sim : ∀ a b, H.symmetrisedTransMatrix params a b - = Real.sqrt (H.stationaryDist params a) * H.transMatrix params a b - / Real.sqrt (H.stationaryDist params b) := fun a b => rfl - have h_inv : IsUnit ((ProofAtlas.RandomWalk.auxMatrix - (H.transMatrix params) (H.stationaryDist params)).det) := - ProofAtlas.RandomWalk.auxMatrix_isUnit_det h_row_sum h_stat h_sum h_conv - -- Column-`v` slice of `W · Z = 0`: ∑_u π_u Z_{u,v} = 0. - have h_WZ : ProofAtlas.RandomWalk.stationaryProjection (H.stationaryDist params) - * ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params) = 0 := - ProofAtlas.RandomWalk.stationaryProjection_mul_fundamentalMatrix h_stat h_sum h_inv - have h_col_zero : ∀ v, ∑ u, H.stationaryDist params u - * ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params) u v = 0 := by - intro v - have h_apply : (ProofAtlas.RandomWalk.stationaryProjection (H.stationaryDist params) - * ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params)) v v = 0 := by - rw [h_WZ]; rfl - rw [Matrix.mul_apply] at h_apply - exact h_apply - -- Diagonal `Z_{v,v}` via spectral expansion: `√(π_v/π_v) = 1` collapses. - have h_Zvv : ∀ v, ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params) v v - = ∑ i : V, if hQ.eigenvalues i < 1 - then (hQ.eigenvectorBasis i v) ^ 2 / (1 - hQ.eigenvalues i) - else 0 := by - intro v - rw [ProofAtlas.RandomWalk.fundamentalMatrix_spectral_expansion - hQ h_sim h_pos h_row_sum h_stat h_sum h_conv - h_eig_le_one h_top_eigvec h_top_unique h_nontop_bound v v, - show H.stationaryDist params v / H.stationaryDist params v = 1 from - div_self (h_pos v).ne', Real.sqrt_one, one_mul] - apply Finset.sum_congr rfl - intros i _ - by_cases hi : hQ.eigenvalues i < 1 - · simp only [if_pos hi]; ring - · simp only [if_neg hi] - -- Trace identity: ∑_v Z_{v,v} = K (orthonormality of φᵢ). - have h_trace : (∑ v : V, ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params) v v) - = H.kemenyConstant params hQ := by - unfold Hypergraph.kemenyConstant - simp_rw [h_Zvv] - rw [Finset.sum_comm] - apply Finset.sum_congr rfl - intros i _ - by_cases hi : hQ.eigenvalues i < 1 - · simp only [if_pos hi] - simp_rw [div_eq_mul_inv] - rw [← Finset.sum_mul, H.eigenvectorBasis_sum_sq_eq_one params hQ i, one_mul] - · simp only [if_neg hi, Finset.sum_const_zero] - -- Substitute Kemeny-Snell into `meanCommuteTime` and clear the `π` cancellations. - unfold Hypergraph.meanCommuteTime Hypergraph.expect2 - Hypergraph.commuteTimeDist - simp_rw [H.hittingTime_eq_fundamentalMatrix params h_row_sum h_stat h_sum h_pos h_conv] - have h_summand : ∀ p : V × V, - H.stationaryDist params p.1 * H.stationaryDist params p.2 * - ((ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params) p.2 p.2 - - ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params) p.1 p.2) - / H.stationaryDist params p.2 - + (ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params) p.1 p.1 - - ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params) p.2 p.1) - / H.stationaryDist params p.1) - = H.stationaryDist params p.1 - * (ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params) p.2 p.2 - - ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params) p.1 p.2) - + H.stationaryDist params p.2 - * (ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params) p.1 p.1 - - ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params) p.2 p.1) := by - intro p - have h1 : H.stationaryDist params p.1 ≠ 0 := (h_pos p.1).ne' - have h2 : H.stationaryDist params p.2 ≠ 0 := (h_pos p.2).ne' - field_simp - rw [Finset.sum_congr rfl (fun p _ => h_summand p)] - rw [Finset.sum_add_distrib] - -- Half A: ∑_p π_{p.1} · (Z_{p.2,p.2} - Z_{p.1,p.2}) = K - -- = ∑_v ∑_u π_u · (Z_{v,v} - Z_{u,v}) (Fubini + swap) - -- = ∑_v ((∑_u π_u) Z_{v,v} - ∑_u π_u Z_{u,v}) - -- = ∑_v (1 · Z_{v,v} - 0) = ∑_v Z_{v,v} = K - have h_inner_A : ∀ v : V, - (∑ u : V, H.stationaryDist params u - * (ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params) v v - - ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params) u v)) - = ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params) v v := by - intro v - simp_rw [mul_sub] - rw [Finset.sum_sub_distrib, ← Finset.sum_mul, h_sum, one_mul, h_col_zero v, sub_zero] - have h_half_A : (∑ p : V × V, H.stationaryDist params p.1 - * (ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params) p.2 p.2 - - ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params) p.1 p.2)) - = H.kemenyConstant params hQ := by - rw [Fintype.sum_prod_type, Finset.sum_comm] - simp_rw [h_inner_A] - exact h_trace - -- Half B: ∑_p π_{p.2} · (Z_{p.1,p.1} - Z_{p.2,p.1}) = K - -- = ∑_u ∑_v π_v · (Z_{u,u} - Z_{v,u}) (Fubini, no swap needed) - -- = ∑_u ((∑_v π_v) Z_{u,u} - ∑_v π_v Z_{v,u}) = ∑_u Z_{u,u} = K - have h_inner_B : ∀ u : V, - (∑ v : V, H.stationaryDist params v - * (ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params) u u - - ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params) v u)) - = ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params) u u := by - intro u - simp_rw [mul_sub] - rw [Finset.sum_sub_distrib, ← Finset.sum_mul, h_sum, one_mul, h_col_zero u, sub_zero] - have h_half_B : (∑ p : V × V, H.stationaryDist params p.2 - * (ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params) p.1 p.1 - - ProofAtlas.RandomWalk.fundamentalMatrix - (H.transMatrix params) (H.stationaryDist params) p.2 p.1)) - = H.kemenyConstant params hQ := by - rw [Fintype.sum_prod_type] - simp_rw [h_inner_B] - exact h_trace - rw [h_half_A, h_half_B] - ring - -end Hypergraph diff --git a/lean/ProofAtlas/RandomWalk/Spectral/ProbabilisticBound.lean b/lean/ProofAtlas/RandomWalk/Spectral/ProbabilisticBound.lean deleted file mode 100644 index 73b955b..0000000 --- a/lean/ProofAtlas/RandomWalk/Spectral/ProbabilisticBound.lean +++ /dev/null @@ -1,354 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.RandomWalk.Spectral.Moments -import Mathlib.Analysis.SpecialFunctions.Pow.Real - -/-! -# Paper Theorem 7.1 (probabilistic hyperbolicity) and its corollaries - -The headline result of the project. For a reversible irreducible BDF -random walk with hermiticity certificate `hQ`, iid stationary samples -satisfy - - `δ̄(H) ≤ 2 √(3 κ₂)` ∧ `E[d] = 2 K` (Theorem 7.1) - -with `δ̄`, `K`, `κ₂` the paper-notation aliases of `avgHyperbolicityConstant`, -`kemenyConstant`, `secondKemenyMoment`. The first conjunct comes from the -spectral second moment `E[(S₁-S₂)²] = 16 κ₂` (`expected_sq_pair_sum_diff`), -the algebraic bound `Φ² ≤ (S₁-S₂)² + (S₂-S₃)² + (S₁-S₃)²`, position- -permutation symmetry of iid samples, and weighted Cauchy-Schwarz; the -second is direct from `meanCommuteTime_eq_two_mul_kemenyConstant`. - -The ratio form `δ̄(H) / E[d] ≤ √(3 κ₂) / K` is `probabilistic_hyperbolicity_ratio`. - -Paper Corollary 7.2 (`bdf_average_rate`) states the qualitative -`δ̄ / E[d] = O(1 / √(log n))` rate; here the Lean statement is the -trivial-existence version for a single hypergraph (the asymptotic -γ_n K_n ≍ log n needs a sequence and is left as paper context). - -## Main results - -* `probabilistic_hyperbolicity_bound` — Theorem 7.1. -* `probabilistic_hyperbolicity_ratio` — ratio form. -* `bdf_average_rate` — Corollary 7.2 (single-hypergraph existence). --/ - -open ProofAtlas - -namespace Hypergraph - -variable {V : Type*} {C : Type*} - -/-! ### Section 7: average-case hyperbolicity (paper Theorem 7.1) -/ - -/-- **Math.** Paper Theorem 7.1 (probabilistic hyperbolicity). For -a reversible irreducible BDF random walk with hermiticity certificate -`hQ` for the symmetrised transition matrix, - - `δAvg(H) ≤ 2 √(3 κ₂)`, `E[d] = 2 K`, - -over iid stationary samples, and consequently - - `δAvg(H) / E[d] ≤ √(3 κ₂) / K ≤ √3 / √(γ K)`. - -The Kemeny identity (right conjunct) is proved via -`meanCommuteTime_eq_two_mul_kemenyConstant`. The hyperbolicity bound -(left conjunct) uses `expected_sq_pair_sum_diff` to evaluate the -second moment `E[(S₁-S₂)²] = 16 κ₂`, applies the position-permutation -symmetry of iid samples to get `E[(S₂-S₃)²] = E[(S₁-S₃)²] = 16 κ₂`, -combines via `fourPointDeviation_aux_sq_le_sum_sq` to bound -`E[Φ²] ≤ 48 κ₂`, and closes with weighted Cauchy-Schwarz -`(E[Φ])² ≤ (∑ π⁴) · E[Φ²] = E[Φ²] ≤ 48 κ₂` followed by -`√(48κ₂) = 4√(3κ₂)`. -/ -theorem probabilistic_hyperbolicity_bound [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (hQ : (H.symmetrisedTransMatrix params).IsHermitian) - (h_row_sum : ∀ i, ∑ j, H.transMatrix params i j = 1) - (h_stat : ∀ j, ∑ i, H.stationaryDist params i * H.transMatrix params i j - = H.stationaryDist params j) - (h_sum : ∑ i, H.stationaryDist params i = 1) - (h_pos : ∀ v, 0 < H.stationaryDist params v) - (h_conv : Filter.Tendsto (fun k => H.transMatrix params ^ k) Filter.atTop - (nhds (ProofAtlas.RandomWalk.stationaryProjection - (H.stationaryDist params)))) - (h_eig_le_one : ∀ i, hQ.eigenvalues i ≤ 1) - (h_top_eigvec : ∀ i, hQ.eigenvalues i = 1 → - ∀ v, hQ.eigenvectorBasis i v = Real.sqrt (H.stationaryDist params v)) - (h_top_unique : ∃! i_top : V, hQ.eigenvalues i_top = 1) - (h_nontop_bound : ∀ i, hQ.eigenvalues i < 1 → |hQ.eigenvalues i| < 1) : - H.δAvg params ≤ 2 * Real.sqrt (3 * H.κ₂ params hQ) - ∧ H.𝔼d params = 2 * H.K params hQ := by - refine ⟨?_, ?_⟩ - · -- Hyperbolicity bound (Cauchy-Schwarz + spectral second moment). - set π := H.stationaryDist params with hπ_def - set d := H.commuteTimeDist params with hd_def - set κ₂ := H.secondKemenyMoment params hQ with hκ₂_def - have hd_symm : ∀ a b : V, d a b = d b a := - fun a b => H.commuteTimeDist_symm params a b - have h_pi_nn : ∀ v : V, 0 ≤ π v := fun v => (h_pos v).le - -- (i) Three pair-sum-diff second moments, all = 16 κ₂. - have h_S12 : ∑ p : V × V × V × V, π p.1 * π p.2.1 * π p.2.2.1 * π p.2.2.2 - * (d p.1 p.2.1 + d p.2.2.1 p.2.2.2 - (d p.1 p.2.2.1 + d p.2.1 p.2.2.2))^2 - = 16 * κ₂ := - H.expected_sq_pair_sum_diff params hQ h_row_sum h_stat h_sum h_pos h_conv - h_eig_le_one h_top_eigvec h_top_unique h_nontop_bound - -- E[(S₂-S₃)²] = 16κ₂ via the position-permutation (u,v,x,y) → (u,x,y,v). - have h_S23 : ∑ p : V × V × V × V, π p.1 * π p.2.1 * π p.2.2.1 * π p.2.2.2 - * (d p.1 p.2.2.1 + d p.2.1 p.2.2.2 - (d p.1 p.2.2.2 + d p.2.1 p.2.2.1))^2 - = 16 * κ₂ := by - let σ : V × V × V × V ≃ V × V × V × V := - { toFun := fun p => (p.1, p.2.2.1, p.2.2.2, p.2.1), - invFun := fun p => (p.1, p.2.2.2, p.2.1, p.2.2.1), - left_inv := fun _ => rfl, - right_inv := fun _ => rfl } - have h := Equiv.sum_comp σ (fun p : V × V × V × V => - π p.1 * π p.2.1 * π p.2.2.1 * π p.2.2.2 - * (d p.1 p.2.1 + d p.2.2.1 p.2.2.2 - (d p.1 p.2.2.1 + d p.2.1 p.2.2.2))^2) - rw [← h_S12, ← h] - refine Finset.sum_congr rfl (fun p _ => ?_) - change π p.1 * π p.2.1 * π p.2.2.1 * π p.2.2.2 - * (d p.1 p.2.2.1 + d p.2.1 p.2.2.2 - (d p.1 p.2.2.2 + d p.2.1 p.2.2.1))^2 - = π p.1 * π p.2.2.1 * π p.2.2.2 * π p.2.1 - * (d p.1 p.2.2.1 + d p.2.2.2 p.2.1 - (d p.1 p.2.2.2 + d p.2.2.1 p.2.1))^2 - rw [hd_symm p.2.2.2 p.2.1, hd_symm p.2.2.1 p.2.1] - ring - -- E[(S₁-S₃)²] = 16κ₂ via (u,v,x,y) → (u,v,y,x). - have h_S13 : ∑ p : V × V × V × V, π p.1 * π p.2.1 * π p.2.2.1 * π p.2.2.2 - * (d p.1 p.2.1 + d p.2.2.1 p.2.2.2 - (d p.1 p.2.2.2 + d p.2.1 p.2.2.1))^2 - = 16 * κ₂ := by - let τ : V × V × V × V ≃ V × V × V × V := - { toFun := fun p => (p.1, p.2.1, p.2.2.2, p.2.2.1), - invFun := fun p => (p.1, p.2.1, p.2.2.2, p.2.2.1), - left_inv := fun _ => rfl, - right_inv := fun _ => rfl } - have h := Equiv.sum_comp τ (fun p : V × V × V × V => - π p.1 * π p.2.1 * π p.2.2.1 * π p.2.2.2 - * (d p.1 p.2.1 + d p.2.2.1 p.2.2.2 - (d p.1 p.2.2.1 + d p.2.1 p.2.2.2))^2) - rw [← h_S12, ← h] - refine Finset.sum_congr rfl (fun p _ => ?_) - change π p.1 * π p.2.1 * π p.2.2.1 * π p.2.2.2 - * (d p.1 p.2.1 + d p.2.2.1 p.2.2.2 - (d p.1 p.2.2.2 + d p.2.1 p.2.2.1))^2 - = π p.1 * π p.2.1 * π p.2.2.2 * π p.2.2.1 - * (d p.1 p.2.1 + d p.2.2.2 p.2.2.1 - (d p.1 p.2.2.2 + d p.2.1 p.2.2.1))^2 - rw [hd_symm p.2.2.2 p.2.2.1] - ring - -- (ii) Probability mass: ∑ p, π⁴(p) = 1. - have h_pi4_sum : ∑ p : V × V × V × V, - π p.1 * π p.2.1 * π p.2.2.1 * π p.2.2.2 = 1 := by - simp_rw [Fintype.sum_prod_type] - have hd_red : ∀ a b c : V, - (∑ x : V, π a * π b * π c * π x) = π a * π b * π c := - fun a b c => by rw [← Finset.mul_sum, h_sum, mul_one] - have hc_red : ∀ a b : V, (∑ x : V, π a * π b * π x) = π a * π b := - fun a b => by rw [← Finset.mul_sum, h_sum, mul_one] - have hb_red : ∀ a : V, (∑ x : V, π a * π x) = π a := - fun a => by rw [← Finset.mul_sum, h_sum, mul_one] - simp_rw [hd_red, hc_red, hb_red] - exact h_sum - -- (iii) Per-tuple algebraic bound: Φ² ≤ (S₁-S₂)² + (S₂-S₃)² + (S₁-S₃)². - have h_Phi_sq_pointwise : ∀ p : V × V × V × V, - π p.1 * π p.2.1 * π p.2.2.1 * π p.2.2.2 - * (fourPointDeviation d p.1 p.2.1 p.2.2.1 p.2.2.2)^2 - ≤ π p.1 * π p.2.1 * π p.2.2.1 * π p.2.2.2 - * ((d p.1 p.2.1 + d p.2.2.1 p.2.2.2 - - (d p.1 p.2.2.1 + d p.2.1 p.2.2.2))^2 - + (d p.1 p.2.2.1 + d p.2.1 p.2.2.2 - - (d p.1 p.2.2.2 + d p.2.1 p.2.2.1))^2 - + (d p.1 p.2.1 + d p.2.2.1 p.2.2.2 - - (d p.1 p.2.2.2 + d p.2.1 p.2.2.1))^2) := by - intro p - refine mul_le_mul_of_nonneg_left ?_ ?_ - · exact fourPointDeviation_aux_sq_le_sum_sq _ _ _ - · exact mul_nonneg (mul_nonneg (mul_nonneg (h_pi_nn _) (h_pi_nn _)) - (h_pi_nn _)) (h_pi_nn _) - -- (iv) Aggregate: E[Φ²] ≤ 48 κ₂. - have h_E_Phi_sq_le : ∑ p : V × V × V × V, - π p.1 * π p.2.1 * π p.2.2.1 * π p.2.2.2 - * (fourPointDeviation d p.1 p.2.1 p.2.2.1 p.2.2.2)^2 - ≤ 48 * κ₂ := by - calc ∑ p : V × V × V × V, π p.1 * π p.2.1 * π p.2.2.1 * π p.2.2.2 - * (fourPointDeviation d p.1 p.2.1 p.2.2.1 p.2.2.2)^2 - ≤ ∑ p : V × V × V × V, π p.1 * π p.2.1 * π p.2.2.1 * π p.2.2.2 - * ((d p.1 p.2.1 + d p.2.2.1 p.2.2.2 - - (d p.1 p.2.2.1 + d p.2.1 p.2.2.2))^2 - + (d p.1 p.2.2.1 + d p.2.1 p.2.2.2 - - (d p.1 p.2.2.2 + d p.2.1 p.2.2.1))^2 - + (d p.1 p.2.1 + d p.2.2.1 p.2.2.2 - - (d p.1 p.2.2.2 + d p.2.1 p.2.2.1))^2) := - Finset.sum_le_sum (fun p _ => h_Phi_sq_pointwise p) - _ = 16 * κ₂ + 16 * κ₂ + 16 * κ₂ := by - simp_rw [mul_add, Finset.sum_add_distrib] - rw [h_S12, h_S23, h_S13] - _ = 48 * κ₂ := by ring - -- (v) Cauchy-Schwarz: (E[Φ])² ≤ (∑ π⁴) · E[Φ²] = 1 · E[Φ²] ≤ 48κ₂. - have h_CS : (∑ p : V × V × V × V, π p.1 * π p.2.1 * π p.2.2.1 * π p.2.2.2 - * fourPointDeviation d p.1 p.2.1 p.2.2.1 p.2.2.2)^2 - ≤ 48 * κ₂ := by - have h_CS_raw := Finset.sum_sq_le_sum_mul_sum_of_sq_le_mul - (Finset.univ : Finset (V × V × V × V)) - (f := fun p => π p.1 * π p.2.1 * π p.2.2.1 * π p.2.2.2) - (g := fun p => π p.1 * π p.2.1 * π p.2.2.1 * π p.2.2.2 - * (fourPointDeviation d p.1 p.2.1 p.2.2.1 p.2.2.2)^2) - (r := fun p => π p.1 * π p.2.1 * π p.2.2.1 * π p.2.2.2 - * fourPointDeviation d p.1 p.2.1 p.2.2.1 p.2.2.2) - (fun p _ => mul_nonneg (mul_nonneg (mul_nonneg (h_pi_nn _) (h_pi_nn _)) - (h_pi_nn _)) (h_pi_nn _)) - (fun p _ => mul_nonneg - (mul_nonneg (mul_nonneg (mul_nonneg (h_pi_nn _) (h_pi_nn _)) - (h_pi_nn _)) (h_pi_nn _)) - (sq_nonneg _)) - (fun p _ => by - have h_eq : (π p.1 * π p.2.1 * π p.2.2.1 * π p.2.2.2 - * fourPointDeviation d p.1 p.2.1 p.2.2.1 p.2.2.2)^2 - = (π p.1 * π p.2.1 * π p.2.2.1 * π p.2.2.2) - * (π p.1 * π p.2.1 * π p.2.2.1 * π p.2.2.2 - * (fourPointDeviation d p.1 p.2.1 p.2.2.1 p.2.2.2)^2) := by ring - exact h_eq.le) - calc (∑ p : V × V × V × V, π p.1 * π p.2.1 * π p.2.2.1 * π p.2.2.2 - * fourPointDeviation d p.1 p.2.1 p.2.2.1 p.2.2.2)^2 - ≤ (∑ p : V × V × V × V, π p.1 * π p.2.1 * π p.2.2.1 * π p.2.2.2) - * ∑ p : V × V × V × V, π p.1 * π p.2.1 * π p.2.2.1 * π p.2.2.2 - * (fourPointDeviation d p.1 p.2.1 p.2.2.1 p.2.2.2)^2 := h_CS_raw - _ = 1 * (∑ p : V × V × V × V, π p.1 * π p.2.1 * π p.2.2.1 * π p.2.2.2 - * (fourPointDeviation d p.1 p.2.1 p.2.2.1 p.2.2.2)^2) := by rw [h_pi4_sum] - _ ≤ 1 * (48 * κ₂) := mul_le_mul_of_nonneg_left h_E_Phi_sq_le zero_le_one - _ = 48 * κ₂ := by ring - -- (vi) Conclude δAvg ≤ 2 √(3 κ₂). - have h_κ₂_nn : 0 ≤ κ₂ := by - change 0 ≤ H.secondKemenyMoment params hQ - unfold Hypergraph.secondKemenyMoment - apply Finset.sum_nonneg - intros i _; split_ifs <;> positivity - have h_3κ₂_nn : (0 : ℝ) ≤ 3 * κ₂ := by linarith - have h_E_Phi_nn : 0 ≤ ∑ p : V × V × V × V, π p.1 * π p.2.1 * π p.2.2.1 * π p.2.2.2 - * fourPointDeviation d p.1 p.2.1 p.2.2.1 p.2.2.2 := by - apply Finset.sum_nonneg - intros p _ - refine mul_nonneg ?_ ?_ - · exact mul_nonneg (mul_nonneg (mul_nonneg (h_pi_nn _) (h_pi_nn _)) - (h_pi_nn _)) (h_pi_nn _) - · unfold fourPointDeviation - exact fourPointDeviation_aux_nonneg _ _ _ - -- δAvg = E[Φ] / 2. Square both sides ≤ 12κ₂ = (2√(3κ₂))² then take square roots. - change H.meanFourPointDeviation params / 2 ≤ 2 * Real.sqrt (3 * κ₂) - unfold Hypergraph.meanFourPointDeviation Hypergraph.expect4 - have h_sq_RHS : (2 * Real.sqrt (3 * κ₂))^2 = 12 * κ₂ := by - rw [mul_pow, Real.sq_sqrt h_3κ₂_nn]; ring - have h_avg_sq_le : ((∑ p : V × V × V × V, π p.1 * π p.2.1 * π p.2.2.1 * π p.2.2.2 - * fourPointDeviation d p.1 p.2.1 p.2.2.1 p.2.2.2) / 2)^2 ≤ 12 * κ₂ := by - rw [div_pow]; linarith - have h_avg_nn : 0 ≤ (∑ p : V × V × V × V, π p.1 * π p.2.1 * π p.2.2.1 * π p.2.2.2 - * fourPointDeviation d p.1 p.2.1 p.2.2.1 p.2.2.2) / 2 := - div_nonneg h_E_Phi_nn (by norm_num) - have h_RHS_nn : (0 : ℝ) ≤ 2 * Real.sqrt (3 * κ₂) := by - have := Real.sqrt_nonneg (3 * κ₂); linarith - -- δAvg² ≤ (2√(3κ₂))² and both ≥ 0 ⇒ δAvg ≤ 2√(3κ₂). - -- Take square roots via the monotonicity of √: δAvg = √(δAvg²) ≤ √(12κ₂) = 2√(3κ₂). - calc (∑ p : V × V × V × V, π p.1 * π p.2.1 * π p.2.2.1 * π p.2.2.2 - * fourPointDeviation d p.1 p.2.1 p.2.2.1 p.2.2.2) / 2 - = Real.sqrt (((∑ p : V × V × V × V, π p.1 * π p.2.1 * π p.2.2.1 * π p.2.2.2 - * fourPointDeviation d p.1 p.2.1 p.2.2.1 p.2.2.2) / 2)^2) := - (Real.sqrt_sq h_avg_nn).symm - _ ≤ Real.sqrt (12 * κ₂) := Real.sqrt_le_sqrt h_avg_sq_le - _ = 2 * Real.sqrt (3 * κ₂) := by - rw [show (12 * κ₂ : ℝ) = 2^2 * (3 * κ₂) from by ring, - Real.sqrt_mul (by norm_num : (0:ℝ) ≤ 2^2), - Real.sqrt_sq (by norm_num : (0:ℝ) ≤ 2)] - · exact H.meanCommuteTime_eq_two_mul_kemenyConstant params hQ - h_row_sum h_stat h_sum h_pos h_conv - h_eig_le_one h_top_eigvec h_top_unique h_nontop_bound - -/-- **Math.** Corollary of `probabilistic_hyperbolicity_bound`: the -explicit ratio bound `δAvg / E[d] ≤ √(3 κ₂) / K`. Requires -`kemenyConstant > 0`, which holds for any non-trivial irreducible -reversible chain. Pure algebra on top of `probabilistic_hyperbolicity_bound`. -/ -theorem probabilistic_hyperbolicity_ratio [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (hQ : (H.symmetrisedTransMatrix params).IsHermitian) - (h_row_sum : ∀ i, ∑ j, H.transMatrix params i j = 1) - (h_stat : ∀ j, ∑ i, H.stationaryDist params i * H.transMatrix params i j - = H.stationaryDist params j) - (h_sum : ∑ i, H.stationaryDist params i = 1) - (h_pos : ∀ v, 0 < H.stationaryDist params v) - (h_conv : Filter.Tendsto (fun k => H.transMatrix params ^ k) Filter.atTop - (nhds (ProofAtlas.RandomWalk.stationaryProjection - (H.stationaryDist params)))) - (h_eig_le_one : ∀ i, hQ.eigenvalues i ≤ 1) - (h_top_eigvec : ∀ i, hQ.eigenvalues i = 1 → - ∀ v, hQ.eigenvectorBasis i v = Real.sqrt (H.stationaryDist params v)) - (h_top_unique : ∃! i_top : V, hQ.eigenvalues i_top = 1) - (h_nontop_bound : ∀ i, hQ.eigenvalues i < 1 → |hQ.eigenvalues i| < 1) - (hK : 0 < H.K params hQ) : - H.δAvg params / H.𝔼d params - ≤ Real.sqrt (3 * H.κ₂ params hQ) / H.K params hQ := by - obtain ⟨hPhi, hMean_eq⟩ := H.probabilistic_hyperbolicity_bound params hQ - h_row_sum h_stat h_sum h_pos h_conv - h_eig_le_one h_top_eigvec h_top_unique h_nontop_bound - rw [hMean_eq] - -- Goal: δAvg / (2 K) ≤ √(3κ₂) / K. Cross-multiply: δAvg * K ≤ √(3κ₂) * (2 K). - have h2K_pos : (0 : ℝ) < 2 * H.kemenyConstant params hQ := by linarith - rw [div_le_div_iff₀ h2K_pos hK] - -- Goal: δAvg * K ≤ √(3κ₂) * (2 K) - have hsqrt_nn : 0 ≤ Real.sqrt (3 * H.secondKemenyMoment params hQ) := - Real.sqrt_nonneg _ - calc H.avgHyperbolicityConstant params * H.kemenyConstant params hQ - ≤ 2 * Real.sqrt (3 * H.secondKemenyMoment params hQ) - * H.kemenyConstant params hQ := - mul_le_mul_of_nonneg_right hPhi hK.le - _ = Real.sqrt (3 * H.secondKemenyMoment params hQ) - * (2 * H.kemenyConstant params hQ) := by ring - -/-- **Math.** Paper Corollary 7.2 (BDF rate). On balanced layered -BDF with bounded arity and bounded reuse, `γ_n K_n ≍ log n`, so the -ratio bound `δAvg / E[d]` is `O(1 / √(log n))`. Stated qualitatively -(existence of a constant `C` for the single hypergraph `H`; the -qualitative statement matches the paper, with the asymptotic -`γ_n K_n ≍ log n` requiring a sequence of hypergraphs). - -For a single hypergraph the statement is provable by picking `C` -large enough — case split on `Fintype.card V = 0` (empty `V` gives -both LHS and RHS equal `0`) versus `≥ 1` (positive `√log` so we -can choose `C ≥ |LHS| · √log + 1`). -/ -theorem bdf_average_rate [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) : - ∃ C : ℝ, 0 < C ∧ - H.δAvg params / H.𝔼d params - ≤ C / Real.sqrt (Real.log (Fintype.card V + 1 : ℝ)) := by - by_cases hV : Fintype.card V = 0 - · -- `V` is empty: all sums over `V × V × V × V` vanish, so LHS = 0. - -- `Real.sqrt (Real.log 1) = 0`, so RHS = 1/0 = 0. Pick `C = 1`. - haveI hV_empty : IsEmpty V := Fintype.card_eq_zero_iff.mp hV - refine ⟨1, one_pos, ?_⟩ - have h_LHS_zero : - H.avgHyperbolicityConstant params / H.meanCommuteTime params = 0 := by - unfold Hypergraph.avgHyperbolicityConstant Hypergraph.meanFourPointDeviation - Hypergraph.expect4 - have h_4D_empty : IsEmpty (V × V × V × V) := inferInstance - rw [Finset.sum_of_isEmpty, zero_div, zero_div] - rw [h_LHS_zero] - exact div_nonneg zero_le_one (Real.sqrt_nonneg _) - · -- `V` is non-empty: `√log > 0` so we can choose `C := |LHS| · √log + 1`. - have h_card_pos : 0 < Fintype.card V := Nat.pos_of_ne_zero hV - have h_arg_gt_one : (1 : ℝ) < (Fintype.card V + 1 : ℝ) := by - have : (1 : ℝ) ≤ (Fintype.card V : ℝ) := by exact_mod_cast h_card_pos - linarith - have h_log_pos : 0 < Real.log (Fintype.card V + 1 : ℝ) := Real.log_pos h_arg_gt_one - have h_sqrt_pos : 0 < Real.sqrt (Real.log (Fintype.card V + 1 : ℝ)) := - Real.sqrt_pos.mpr h_log_pos - set LHS := H.avgHyperbolicityConstant params / H.meanCommuteTime params - set sqrtLog := Real.sqrt (Real.log (Fintype.card V + 1 : ℝ)) - refine ⟨|LHS| * sqrtLog + 1, ?_, ?_⟩ - · positivity - · rw [le_div_iff₀ h_sqrt_pos] - calc LHS * sqrtLog - ≤ |LHS| * sqrtLog := - mul_le_mul_of_nonneg_right (le_abs_self _) h_sqrt_pos.le - _ ≤ |LHS| * sqrtLog + 1 := by linarith - -end Hypergraph diff --git a/lean/ProofAtlas/RandomWalk/Spectral/SpectralForm.lean b/lean/ProofAtlas/RandomWalk/Spectral/SpectralForm.lean deleted file mode 100644 index 6c17b33..0000000 --- a/lean/ProofAtlas/RandomWalk/Spectral/SpectralForm.lean +++ /dev/null @@ -1,237 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.RandomWalk.Spectral.Decomposition -import ProofAtlas.RandomWalk.Spectral.Spectrum - -/-! -# Section 6.2: spectral form of the interaction term - -In the reversible case (`hQ : Q.IsHermitian`), the interaction term -`I(u, v)` admits the spectral expansion - - `I(u, v) = 2 ∑_{i : λᵢ < 1} ψᵢ(u) ψᵢ(v) / (1 - λᵢ)` (Theorem 6.4) - -with `ψᵢ(v) := φᵢ(v) / √π_v` and `{φᵢ, λᵢ}` the orthonormal eigendata -of `Q = D^{1/2} P D^{-1/2}`. Specialising the four-tuple identity from -Theorem 6.1 to four points yields - - `S₁ - S₂ = -2 ∑_{λᵢ<1} (ψᵢ(u) - ψᵢ(y))(ψᵢ(v) - ψᵢ(x)) / (1 - λᵢ)` - (Theorem 6.5) - -— the four-point spectral identity that feeds the second-moment -computation of paper Theorem 7.1. - -## Main results - -* `interaction_spectral_form` — Theorem 6.4. -* `four_point_spectral_identity` — Theorem 6.5. --/ - -namespace Hypergraph - -variable {V : Type*} {C : Type*} - -/-- **Math.** Paper Theorem 6.4. In the reversible case, the -interaction term `I(u, v)` (with `f` from Theorem 6.1 absorbed into -the gauge) admits the spectral expansion - - `I(u, v) = 2 ∑_{i : λᵢ < 1} ψᵢ(u) ψᵢ(v) / (1 - λᵢ)`, - -with `ψᵢ(v) := φᵢ(v) / √π(v)` (`reversibleEigenfunction`) and -`{φᵢ, λᵢ}` the orthonormal eigendata of the symmetrised transition -matrix from `hQ`. The top eigenvalue `λ₁ = 1` (eigenvector `√π`) is -excluded by the `λᵢ < 1` condition. -/ -theorem interaction_spectral_form [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (hQ : (H.symmetrisedTransMatrix params).IsHermitian) - (h_row_sum : ∀ i, ∑ j, H.transMatrix params i j = 1) - (h_stat : ∀ j, ∑ i, H.stationaryDist params i * H.transMatrix params i j - = H.stationaryDist params j) - (h_sum : ∑ i, H.stationaryDist params i = 1) - (h_pos : ∀ v, 0 < H.stationaryDist params v) - (h_conv : Filter.Tendsto (fun k => H.transMatrix params ^ k) Filter.atTop - (nhds (ProofAtlas.RandomWalk.stationaryProjection - (H.stationaryDist params)))) - (h_eig_le_one : ∀ i, hQ.eigenvalues i ≤ 1) - (h_top_eigvec : ∀ i, hQ.eigenvalues i = 1 → - ∀ v, hQ.eigenvectorBasis i v = Real.sqrt (H.stationaryDist params v)) - (h_top_unique : ∃! i_top : V, hQ.eigenvalues i_top = 1) - (h_nontop_bound : ∀ i, hQ.eigenvalues i < 1 → |hQ.eigenvalues i| < 1) : - ∃ f : V → ℝ, ∀ u v : V, - H.commuteTimeDist params u v - f u - f v - = -2 * ∑ i : V, - if hQ.eigenvalues i < 1 - then H.reversibleEigenfunction params hQ i u - * H.reversibleEigenfunction params hQ i v - / (1 - hQ.eigenvalues i) - else 0 := by - refine ⟨H.vertexTerm params, fun u v => ?_⟩ - -- Setup: pull in the Section 6.1 chain and the spectral expansion of Z. - have h_dec := H.commute_time_vertex_interaction_decomp params u v - have h_I := H.interactionTerm_eq_fundamentalMatrix params - h_row_sum h_stat h_sum h_pos h_conv u v - have h_sim : ∀ a b, H.symmetrisedTransMatrix params a b - = Real.sqrt (H.stationaryDist params a) * H.transMatrix params a b - / Real.sqrt (H.stationaryDist params b) := fun a b => rfl - have h_Z_uv := ProofAtlas.RandomWalk.fundamentalMatrix_spectral_expansion - hQ h_sim h_pos h_row_sum h_stat h_sum h_conv - h_eig_le_one h_top_eigvec h_top_unique h_nontop_bound u v - have h_Z_vu := ProofAtlas.RandomWalk.fundamentalMatrix_spectral_expansion - hQ h_sim h_pos h_row_sum h_stat h_sum h_conv - h_eig_le_one h_top_eigvec h_top_unique h_nontop_bound v u - -- π and √π non-vanishing facts. - have h_πu_ne : H.stationaryDist params u ≠ 0 := (h_pos u).ne' - have h_πv_ne : H.stationaryDist params v ≠ 0 := (h_pos v).ne' - have h_su_ne : Real.sqrt (H.stationaryDist params u) ≠ 0 := - (Real.sqrt_pos.mpr (h_pos u)).ne' - have h_sv_ne : Real.sqrt (H.stationaryDist params v) ≠ 0 := - (Real.sqrt_pos.mpr (h_pos v)).ne' - have h_su_sq : Real.sqrt (H.stationaryDist params u) - * Real.sqrt (H.stationaryDist params u) = H.stationaryDist params u := - Real.mul_self_sqrt (h_pos u).le - have h_sv_sq : Real.sqrt (H.stationaryDist params v) - * Real.sqrt (H.stationaryDist params v) = H.stationaryDist params v := - Real.mul_self_sqrt (h_pos v).le - have h_sqrt_div_v_u : Real.sqrt (H.stationaryDist params v / H.stationaryDist params u) - = Real.sqrt (H.stationaryDist params v) / Real.sqrt (H.stationaryDist params u) := - Real.sqrt_div (h_pos v).le _ - have h_sqrt_div_u_v : Real.sqrt (H.stationaryDist params u / H.stationaryDist params v) - = Real.sqrt (H.stationaryDist params u) / Real.sqrt (H.stationaryDist params v) := - Real.sqrt_div (h_pos u).le _ - -- Symmetry of the spectral sum: φᵢ(u)φᵢ(v) = φᵢ(v)φᵢ(u) per term. - have h_S_swap : (∑ i : V, if hQ.eigenvalues i < 1 - then hQ.eigenvectorBasis i v * hQ.eigenvectorBasis i u - / (1 - hQ.eigenvalues i) else 0) - = (∑ i : V, if hQ.eigenvalues i < 1 - then hQ.eigenvectorBasis i u * hQ.eigenvectorBasis i v - / (1 - hQ.eigenvalues i) else 0) := by - apply Finset.sum_congr rfl - intros i _ - by_cases hi : hQ.eigenvalues i < 1 - · simp only [if_pos hi]; ring - · simp only [if_neg hi] - -- ψ-sum expressed in terms of φ-sum. - have h_psi : (∑ i : V, if hQ.eigenvalues i < 1 - then H.reversibleEigenfunction params hQ i u - * H.reversibleEigenfunction params hQ i v - / (1 - hQ.eigenvalues i) - else 0) - = (∑ i : V, if hQ.eigenvalues i < 1 - then hQ.eigenvectorBasis i u * hQ.eigenvectorBasis i v - / (1 - hQ.eigenvalues i) else 0) - / (Real.sqrt (H.stationaryDist params u) - * Real.sqrt (H.stationaryDist params v)) := by - rw [Finset.sum_div] - apply Finset.sum_congr rfl - intros i _ - by_cases hi : hQ.eigenvalues i < 1 - · simp only [if_pos hi] - unfold Hypergraph.reversibleEigenfunction - field_simp - · simp only [if_neg hi, zero_div] - -- d − f − f = −I; substitute interactionTerm K-S and the spectral expansions. - rw [show H.commuteTimeDist params u v - H.vertexTerm params u - H.vertexTerm params v - = -H.interactionTerm params u v from by linarith] - rw [h_I, h_Z_uv, h_Z_vu, h_S_swap, h_psi, h_sqrt_div_v_u, h_sqrt_div_u_v] - have h_su_pow : Real.sqrt (H.stationaryDist params u) ^ 2 = H.stationaryDist params u := - Real.sq_sqrt (h_pos u).le - have h_sv_pow : Real.sqrt (H.stationaryDist params v) ^ 2 = H.stationaryDist params v := - Real.sq_sqrt (h_pos v).le - field_simp - rw [h_su_pow, h_sv_pow] - ring - -/-- **Math.** Paper Theorem 6.5. Four-point spectral identity: in -the reversible case, the difference `S₁ - S₂` of pairwise sums -factors over the spectrum as - - `S₁ - S₂ = -2 ∑_{i : λᵢ < 1} - (ψᵢ(u) - ψᵢ(y))(ψᵢ(v) - ψᵢ(x)) / (1 - λᵢ)`. - -Follows from `commute_time_vertex_interaction_decomp` and -`interaction_spectral_form`: the `f`-gauge cancels (Cor 6.2), and -the spectral expansion of each `I(a, b)` re-arranges. -/ -theorem four_point_spectral_identity [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (hQ : (H.symmetrisedTransMatrix params).IsHermitian) - (h_row_sum : ∀ i, ∑ j, H.transMatrix params i j = 1) - (h_stat : ∀ j, ∑ i, H.stationaryDist params i * H.transMatrix params i j - = H.stationaryDist params j) - (h_sum : ∑ i, H.stationaryDist params i = 1) - (h_pos : ∀ v, 0 < H.stationaryDist params v) - (h_conv : Filter.Tendsto (fun k => H.transMatrix params ^ k) Filter.atTop - (nhds (ProofAtlas.RandomWalk.stationaryProjection - (H.stationaryDist params)))) - (h_eig_le_one : ∀ i, hQ.eigenvalues i ≤ 1) - (h_top_eigvec : ∀ i, hQ.eigenvalues i = 1 → - ∀ v, hQ.eigenvectorBasis i v = Real.sqrt (H.stationaryDist params v)) - (h_top_unique : ∃! i_top : V, hQ.eigenvalues i_top = 1) - (h_nontop_bound : ∀ i, hQ.eigenvalues i < 1 → |hQ.eigenvalues i| < 1) - (u v x y : V) : - H.commuteTimeDist params u v + H.commuteTimeDist params x y - - (H.commuteTimeDist params u x + H.commuteTimeDist params v y) - = -2 * ∑ i : V, - if hQ.eigenvalues i < 1 - then (H.reversibleEigenfunction params hQ i u - - H.reversibleEigenfunction params hQ i y) - * (H.reversibleEigenfunction params hQ i v - - H.reversibleEigenfunction params hQ i x) - / (1 - hQ.eigenvalues i) - else 0 := by - obtain ⟨f, hf⟩ := H.interaction_spectral_form params hQ - h_row_sum h_stat h_sum h_pos h_conv h_eig_le_one h_top_eigvec h_top_unique h_nontop_bound - -- Apply the spectral identity at each of the four pairs. - have h_uv := hf u v - have h_xy := hf x y - have h_ux := hf u x - have h_vy := hf v y - -- f-gauge cancels in the alternating sum. - have h_LHS_split : - H.commuteTimeDist params u v + H.commuteTimeDist params x y - - (H.commuteTimeDist params u x + H.commuteTimeDist params v y) - = (H.commuteTimeDist params u v - f u - f v) - + (H.commuteTimeDist params x y - f x - f y) - - (H.commuteTimeDist params u x - f u - f x) - - (H.commuteTimeDist params v y - f v - f y) := by ring - rw [h_LHS_split, h_uv, h_xy, h_ux, h_vy] - -- Now combine the four sums into one and check the per-summand identity. - have h_sum_eq : - (∑ i : V, if hQ.eigenvalues i < 1 - then H.reversibleEigenfunction params hQ i u - * H.reversibleEigenfunction params hQ i v - / (1 - hQ.eigenvalues i) else 0) - + (∑ i : V, if hQ.eigenvalues i < 1 - then H.reversibleEigenfunction params hQ i x - * H.reversibleEigenfunction params hQ i y - / (1 - hQ.eigenvalues i) else 0) - - (∑ i : V, if hQ.eigenvalues i < 1 - then H.reversibleEigenfunction params hQ i u - * H.reversibleEigenfunction params hQ i x - / (1 - hQ.eigenvalues i) else 0) - - (∑ i : V, if hQ.eigenvalues i < 1 - then H.reversibleEigenfunction params hQ i v - * H.reversibleEigenfunction params hQ i y - / (1 - hQ.eigenvalues i) else 0) - = ∑ i : V, if hQ.eigenvalues i < 1 - then (H.reversibleEigenfunction params hQ i u - - H.reversibleEigenfunction params hQ i y) - * (H.reversibleEigenfunction params hQ i v - - H.reversibleEigenfunction params hQ i x) - / (1 - hQ.eigenvalues i) else 0 := by - rw [← Finset.sum_add_distrib, ← Finset.sum_sub_distrib, ← Finset.sum_sub_distrib] - apply Finset.sum_congr rfl - intros i _ - by_cases hi : hQ.eigenvalues i < 1 - · simp only [if_pos hi] - field_simp - ring - · simp only [if_neg hi] - ring - linarith [h_sum_eq] - -end Hypergraph diff --git a/lean/ProofAtlas/RandomWalk/Spectral/Spectrum.lean b/lean/ProofAtlas/RandomWalk/Spectral/Spectrum.lean deleted file mode 100644 index 6790d46..0000000 --- a/lean/ProofAtlas/RandomWalk/Spectral/Spectrum.lean +++ /dev/null @@ -1,210 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.RandomWalk.Spectral.Stationary -import Mathlib.Analysis.Matrix.Spectrum - -/-! -# Spectral quantities: K, κ₂, γ, ψᵢ, and orthonormality helpers - -Definitions of the Kemeny constant `K`, the second Kemeny moment `κ₂`, -the spectral gap `γ`, and the stationary-normalised eigenfunctions -`ψᵢ(v) := φᵢ(v) / √π(v)`. All bound to a hermiticity certificate -`hQ : (symmetrisedTransMatrix params).IsHermitian`. - -Plus orthonormality helpers on `hQ.eigenvectorBasis`: the standard -unit-norm and pairwise-orthogonal identities re-expressed as -coordinate sums (`∑_v φᵢ(v)² = 1`, `∑_v φᵢ(v)·φⱼ(v) = δᵢⱼ`), and the -ergodic-chain corollary `⟨√π, φᵢ⟩ = 0` for non-top eigenvectors, -along with pointwise/moment versions involving `ψᵢ`. These feed the -spectral moment calculations in `Spectral.Moments`. - -## Main definitions - -* `kemenyConstant` — `K = ∑_{λᵢ<1} 1/(1-λᵢ)`. -* `secondKemenyMoment` — `κ₂ = ∑_{λᵢ<1} 1/(1-λᵢ)²`. -* `spectralGap` — `γ = 1 - max{λᵢ : λᵢ < 1}`. -* `reversibleEigenfunction` — `ψᵢ(v) := φᵢ(v) / √π(v)`. - -## Main results (eigenvectorBasis helpers) - -* `eigenvectorBasis_sum_sq_eq_one` — `∑_v φᵢ(v)² = 1`. -* `eigenvectorBasis_inner_eq` — `∑_v φᵢ(v) · φⱼ(v) = δᵢⱼ`. -* `sqrt_stationary_eigenvector_orthogonal` — `∑_v √π(v) · φᵢ(v) = 0` - for non-top `i` (given uniqueness of `λ_top = 1` and `φ_top = √π`). -* `stationary_mul_reversibleEigenfunction` — `π_v · ψᵢ(v) = √π_v · φᵢ(v)`. -* `sum_stationary_reversibleEigenfunction_nontop` — `E_π[ψᵢ] = 0` for - non-top `i`. -* `sum_stationary_reversibleEigenfunction_product` — `E_π[ψᵢ · ψⱼ] = δᵢⱼ`. --/ - -namespace Hypergraph - -variable {V : Type*} {C : Type*} - -/-- **Math.** Kemeny constant `K = ∑_{i : λᵢ < 1} (1 - λᵢ)⁻¹`. The -sum is over eigenvalues strictly less than `1`; the top eigenvalue -`λ₁ = 1` (with eigenvector `√π`) is excluded by the conditional. -/ -noncomputable def kemenyConstant [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (hQ : (H.symmetrisedTransMatrix params).IsHermitian) : ℝ := - ∑ i : V, if hQ.eigenvalues i < 1 then (1 - hQ.eigenvalues i)⁻¹ else 0 - -/-- **Math.** Second Kemeny moment `κ₂ = ∑_{i : λᵢ < 1} (1 - λᵢ)⁻²`. -/ -noncomputable def secondKemenyMoment [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (hQ : (H.symmetrisedTransMatrix params).IsHermitian) : ℝ := - ∑ i : V, if hQ.eigenvalues i < 1 - then ((1 - hQ.eigenvalues i)⁻¹) ^ 2 else 0 - -/-- **Math.** Spectral gap `γ = 1 - λ₂`, where `λ₂` is the largest -eigenvalue of `Q` strictly less than `1`. Defined as -`1 - max {λᵢ : λᵢ < 1}`; the `hNonempty` hypothesis asserts that at -least one eigenvalue is `< 1` (true on any non-trivial irreducible -reversible chain, where `λ₁ = 1` is simple and `λ₂ < 1`). -/ -noncomputable def spectralGap [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (hQ : (H.symmetrisedTransMatrix params).IsHermitian) - (hNonempty : - ((Finset.univ : Finset V).filter (fun i => hQ.eigenvalues i < 1)).Nonempty) : - ℝ := - 1 - (((Finset.univ : Finset V).filter (fun i => hQ.eigenvalues i < 1)).image - hQ.eigenvalues).max' (by - rw [Finset.image_nonempty] - exact hNonempty) - -/-- **Math.** Stationary-normalised eigenfunction -`ψᵢ(v) := φᵢ(v) / √π(v)`, where `φᵢ` is the `i`-th orthonormal -eigenvector of the symmetrised transition matrix `Q`. -/ -noncomputable def reversibleEigenfunction [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (hQ : (H.symmetrisedTransMatrix params).IsHermitian) - (i v : V) : ℝ := - hQ.eigenvectorBasis i v / Real.sqrt (H.stationaryDist params v) - -/-! ### Orthonormality helpers -/ - -/-- **Math.** Orthonormality of `hQ.eigenvectorBasis` evaluated at the -sum-of-squares level: `∑_v φᵢ(v)² = 1` for every eigenvector index `i`. -Drops out of `‖φᵢ‖ = 1` (the `Orthonormal` projection on the -`OrthonormalBasis`) and `‖x‖² = ∑ᵥ (x v)²` on `EuclideanSpace ℝ V` -(`EuclideanSpace.real_norm_sq_eq`). Underwrites the Kemeny-constant -identity `∑_v Z_{v,v} = ∑_{λᵢ<1} 1/(1-λᵢ)`. -/ -lemma eigenvectorBasis_sum_sq_eq_one [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (hQ : (H.symmetrisedTransMatrix params).IsHermitian) (i : V) : - ∑ v : V, (hQ.eigenvectorBasis i v) ^ 2 = 1 := by - have h_norm : ‖hQ.eigenvectorBasis i‖ = 1 := - hQ.eigenvectorBasis.orthonormal.1 i - have h_norm_sq : ‖hQ.eigenvectorBasis i‖ ^ 2 = 1 := by rw [h_norm]; norm_num - rw [← h_norm_sq] - exact (EuclideanSpace.real_norm_sq_eq _).symm - -/-- **Math.** Pairwise orthonormality of `hQ.eigenvectorBasis` evaluated as a -coordinate sum: `∑_v φᵢ(v) · φⱼ(v) = δᵢⱼ`. Drops out of the `Orthonormal` -predicate (`Pairwise ⟪φᵢ, φⱼ⟫ = 0` plus `‖φᵢ‖ = 1`) and the explicit form -`⟪x, y⟫ = ∑ᵥ x(v) · y(v)` on the real Euclidean space `EuclideanSpace ℝ V`. -Underwrites the cross-term cancellation in the second moment -`E_{π⁴}[(S₁ - S₂)²] = 16 κ₂` of paper Theorem 7.1. -/ -lemma eigenvectorBasis_inner_eq [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (hQ : (H.symmetrisedTransMatrix params).IsHermitian) (i j : V) : - ∑ v : V, hQ.eigenvectorBasis i v * hQ.eigenvectorBasis j v - = if i = j then 1 else 0 := by - have h : inner ℝ (hQ.eigenvectorBasis i) (hQ.eigenvectorBasis j) - = if i = j then 1 else 0 := - (orthonormal_iff_ite (𝕜 := ℝ)).mp hQ.eigenvectorBasis.orthonormal i j - rw [PiLp.inner_apply] at h - simp only [RCLike.inner_apply, conj_trivial] at h - rw [← h] - exact Finset.sum_congr rfl (fun v _ => mul_comm _ _) - -/-- **Math.** Orthogonality of non-top eigenvectors with `√π`. Under the -top-eigenvector hypothesis (`φ_{i_top}(v) = √π(v)`) and uniqueness of the -top eigenvalue, every non-top eigenvector is orthogonal to `√π`: -`∑_v √π(v) · φᵢ(v) = 0` whenever `λᵢ < 1`. This is the mechanism by which -the linear `E[ψᵢ]` terms vanish in the second-moment calculation. -/ -lemma sqrt_stationary_eigenvector_orthogonal [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (hQ : (H.symmetrisedTransMatrix params).IsHermitian) - (h_top_eigvec : ∀ i, hQ.eigenvalues i = 1 → - ∀ v, hQ.eigenvectorBasis i v = Real.sqrt (H.stationaryDist params v)) - (h_top_unique : ∃! i_top : V, hQ.eigenvalues i_top = 1) - {i : V} (hi : hQ.eigenvalues i < 1) : - ∑ v : V, Real.sqrt (H.stationaryDist params v) * hQ.eigenvectorBasis i v = 0 := by - obtain ⟨i_top, h_top, _⟩ := h_top_unique - have h_ne : i_top ≠ i := fun h => by rw [h] at h_top; linarith - have h_orth := H.eigenvectorBasis_inner_eq params hQ i_top i - rw [if_neg h_ne] at h_orth - rw [← h_orth] - exact Finset.sum_congr rfl (fun v _ => by rw [h_top_eigvec i_top h_top v]) - -/-- **Math.** Pointwise identity `π_v · ψᵢ(v) = √π_v · φᵢ(v)`. Drops out of -`ψᵢ(v) = φᵢ(v)/√π_v` and `π_v = √π_v · √π_v` (positivity). -/ -lemma stationary_mul_reversibleEigenfunction [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (hQ : (H.symmetrisedTransMatrix params).IsHermitian) - (h_pos : ∀ v, 0 < H.stationaryDist params v) (i v : V) : - H.stationaryDist params v * H.reversibleEigenfunction params hQ i v - = Real.sqrt (H.stationaryDist params v) * hQ.eigenvectorBasis i v := by - unfold Hypergraph.reversibleEigenfunction - have h_sqrt_ne : Real.sqrt (H.stationaryDist params v) ≠ 0 := - Real.sqrt_ne_zero'.mpr (h_pos v) - have h_sq : Real.sqrt (H.stationaryDist params v) ^ 2 = H.stationaryDist params v := - Real.sq_sqrt (h_pos v).le - field_simp - rw [h_sq]; ring - -/-- **Math.** Linear-moment vanishing: `E_π[ψᵢ] = 0` for non-top eigenvectors. -Combines `stationary_mul_reversibleEigenfunction` with -`sqrt_stationary_eigenvector_orthogonal`. -/ -lemma sum_stationary_reversibleEigenfunction_nontop [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (hQ : (H.symmetrisedTransMatrix params).IsHermitian) - (h_pos : ∀ v, 0 < H.stationaryDist params v) - (h_top_eigvec : ∀ i, hQ.eigenvalues i = 1 → - ∀ v, hQ.eigenvectorBasis i v = Real.sqrt (H.stationaryDist params v)) - (h_top_unique : ∃! i_top : V, hQ.eigenvalues i_top = 1) - {i : V} (hi : hQ.eigenvalues i < 1) : - ∑ v : V, H.stationaryDist params v - * H.reversibleEigenfunction params hQ i v = 0 := by - have h := H.sqrt_stationary_eigenvector_orthogonal params hQ - h_top_eigvec h_top_unique hi - rw [← h] - exact Finset.sum_congr rfl - (fun v _ => H.stationary_mul_reversibleEigenfunction params hQ h_pos i v) - -/-- **Math.** Quadratic-moment orthonormality: `E_π[ψᵢ · ψⱼ] = δᵢⱼ`. Drops out -of `π_v · ψᵢ(v) · ψⱼ(v) = φᵢ(v) · φⱼ(v)` (the `√π` factors cancel against -the `π` weight) and `eigenvectorBasis_inner_eq`. -/ -lemma sum_stationary_reversibleEigenfunction_product [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (hQ : (H.symmetrisedTransMatrix params).IsHermitian) - (h_pos : ∀ v, 0 < H.stationaryDist params v) (i j : V) : - ∑ v : V, H.stationaryDist params v - * (H.reversibleEigenfunction params hQ i v - * H.reversibleEigenfunction params hQ j v) - = if i = j then 1 else 0 := by - rw [← H.eigenvectorBasis_inner_eq params hQ i j] - refine Finset.sum_congr rfl (fun v _ => ?_) - unfold Hypergraph.reversibleEigenfunction - have h_sqrt_ne : Real.sqrt (H.stationaryDist params v) ≠ 0 := - Real.sqrt_ne_zero'.mpr (h_pos v) - have h_sq : Real.sqrt (H.stationaryDist params v) ^ 2 = H.stationaryDist params v := - Real.sq_sqrt (h_pos v).le - field_simp - rw [h_sq]; ring - -end Hypergraph diff --git a/lean/ProofAtlas/RandomWalk/Spectral/Stationary.lean b/lean/ProofAtlas/RandomWalk/Spectral/Stationary.lean deleted file mode 100644 index e61386a..0000000 --- a/lean/ProofAtlas/RandomWalk/Spectral/Stationary.lean +++ /dev/null @@ -1,136 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Util.Linter.MathTag -import ProofAtlas.Hypergraph.TransitionMatrix -import Mathlib.LinearAlgebra.Matrix.Hermitian -import MCMC.Finite.Core - -/-! -# BDF transition matrix, stationary distribution, reversibility - -The matrix-form transition probability `P`, the stationary distribution -`π` (via Perron-Frobenius from the vendored `MCMC.Finite`), the -detailed-balance predicate `IsReversible`, and the symmetrised -transition matrix `Q = D^{1/2} P D^{-1/2}`. Reversibility implies `Q` -is Hermitian (`symmetrisedTransMatrix_isHermitian`), the entry point -for the spectral theory of paper §6. - -## Main definitions - -* `Hypergraph.transMatrix` — `P` as a `Matrix V V ℝ`. -* `Hypergraph.stationaryDist` — stationary distribution `π`, - defaulted to `0` if the chain is not stochastic + irreducible. -* `Hypergraph.IsReversible` — `π_u P_{u,v} = π_v P_{v,u}`. -* `Hypergraph.symmetrisedTransMatrix` — `Q = D^{1/2} P D^{-1/2}`. - -## Main results - -* `symmetrisedTransMatrix_isHermitian` — reversibility ⇒ `Q.IsHermitian`. --/ - -namespace Hypergraph - -variable {V : Type*} {C : Type*} - -/-- **Eng.** Transition matrix as `Matrix V V ℝ`. Repackages -`transProb` for Mathlib's matrix API. -/ -noncomputable def transMatrix [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) : Matrix V V ℝ := - fun u v => H.transProb params u v - -/-- **Math.** Stationary distribution `π` of the BDF random walk: -the unique row-vector satisfying `π = π · P` with `∑_v π_v = 1` -when the transition matrix is stochastic and irreducible. - -Implementation: dispatches to the vendored Perron-Frobenius based -`MCMC.Finite.stationaryDistribution` when the matrix satisfies -both predicates; defaults to `0` otherwise. The walk's -non-negativity and row-sum-one are proved in `TransitionMatrix.lean` -(`transProb_nonneg`, `transProb_sum_one`), and irreducibility for -a connected BDF hypergraph is a separate hypothesis. -/ -noncomputable def stationaryDist [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (v : V) : ℝ := - haveI : Nonempty V := ⟨v⟩ - letI := Classical.propDecidable - (MCMC.Finite.IsStochastic (H.transMatrix params) ∧ - (H.transMatrix params).IsIrreducible) - if h : MCMC.Finite.IsStochastic (H.transMatrix params) ∧ - (H.transMatrix params).IsIrreducible then - (MCMC.Finite.stationaryDistribution (H.transMatrix params) h.2 h.1).val v - else 0 - -/-- **Math.** Reversibility: detailed balance `π_u P_{u,v} = π_v P_{v,u}`. -Paper §6.2 hypothesis. -/ -def IsReversible [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) : Prop := - ∀ u v : V, - H.stationaryDist params u * H.transProb params u v - = H.stationaryDist params v * H.transProb params v u - -/-- **Math.** Symmetrised transition matrix `Q = D^{1/2} P D^{-1/2}`, -where `D = diag(π)`. Under reversibility, `Q` is symmetric (real -Hermitian); see `symmetrisedTransMatrix_isHermitian`. -/ -noncomputable def symmetrisedTransMatrix [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) : Matrix V V ℝ := - fun u v => - Real.sqrt (H.stationaryDist params u) - * H.transProb params u v - / Real.sqrt (H.stationaryDist params v) - -/-- **Math.** Reversibility implies the symmetrised transition matrix -is Hermitian (symmetric over `ℝ`). The standard one-line calculation -`Q_{u,v} = √π_u · P_{u,v} / √π_v = (π_u P_{u,v}) / √(π_u π_v) = -(π_v P_{v,u}) / √(π_u π_v) = Q_{v,u}`. - -Requires non-negativity of the stationary distribution -(`Real.mul_self_sqrt` needs `0 ≤ π_v`); the irreducibility / positivity -of `π` is a separate Markov-chain hypothesis. -/ -theorem symmetrisedTransMatrix_isHermitian [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (hNonneg : ∀ v, 0 ≤ H.stationaryDist params v) - (hRev : H.IsReversible params) : - (H.symmetrisedTransMatrix params).IsHermitian := by - ext u v - -- Goal: (Q)ᴴ u v = Q u v, i.e. star (Q v u) = Q u v. Over ℝ, star = id. - change star (H.symmetrisedTransMatrix params v u) = H.symmetrisedTransMatrix params u v - rw [star_trivial] - -- Goal: Q v u = Q u v, i.e. - -- √π_v · P_{v,u} / √π_u = √π_u · P_{u,v} / √π_v - unfold symmetrisedTransMatrix - -- Split on whether √π_u, √π_v are zero - by_cases hu0 : H.stationaryDist params u = 0 - · -- π_u = 0 ⇒ √π_u = 0; both sides become `_ / 0 = 0` and `0 · _ = 0`. - have hsqu : Real.sqrt (H.stationaryDist params u) = 0 := by - rw [hu0]; exact Real.sqrt_zero - rw [hsqu]; simp - by_cases hv0 : H.stationaryDist params v = 0 - · have hsqv : Real.sqrt (H.stationaryDist params v) = 0 := by - rw [hv0]; exact Real.sqrt_zero - rw [hsqv]; simp - -- Both π_u, π_v > 0. Use √x · √x = x and reversibility. - have hsqu_ne : Real.sqrt (H.stationaryDist params u) ≠ 0 := by - rw [Real.sqrt_ne_zero (hNonneg u)]; exact hu0 - have hsqv_ne : Real.sqrt (H.stationaryDist params v) ≠ 0 := by - rw [Real.sqrt_ne_zero (hNonneg v)]; exact hv0 - rw [div_eq_div_iff hsqu_ne hsqv_ne] - -- Goal: √π_v · P_{v,u} · √π_v = √π_u · P_{u,v} · √π_u - have hu_sq : Real.sqrt (H.stationaryDist params u) - * Real.sqrt (H.stationaryDist params u) = H.stationaryDist params u := - Real.mul_self_sqrt (hNonneg u) - have hv_sq : Real.sqrt (H.stationaryDist params v) - * Real.sqrt (H.stationaryDist params v) = H.stationaryDist params v := - Real.mul_self_sqrt (hNonneg v) - have hrev := hRev u v - -- hrev : π_u · P_{u,v} = π_v · P_{v,u} - linear_combination - H.transProb params v u * hv_sq - H.transProb params u v * hu_sq - hrev - -end Hypergraph diff --git a/lean/ProofAtlas/Util/Linter/MetricRegistry.lean b/lean/ProofAtlas/Util/Linter/MetricRegistry.lean index bc000ac..4aa71a7 100644 --- a/lean/ProofAtlas/Util/Linter/MetricRegistry.lean +++ b/lean/ProofAtlas/Util/Linter/MetricRegistry.lean @@ -50,9 +50,6 @@ private partial def dependsOnSorry (env : Environment) (name : Name) (metric name for display, fully qualified instance theorem name). -/ private def metricRegistry : Array (String × Name) := #[ ("resistanceDist", `Hypergraph.resistanceDist_isHypergraphMetric) - , ("commuteTimeDist", `Hypergraph.commuteTimeDist_isHypergraphMetric) - , ("jsDist", `Hypergraph.jsDist_isHypergraphMetric) - , ("diffusionDist", `Hypergraph.diffusionDist_isHypergraphMetric) ] /-- **Eng.** Syntax for the compile-time metric registry guard. -/ diff --git a/lean/lakefile.toml b/lean/lakefile.toml index 4f50be8..853e9ef 100644 --- a/lean/lakefile.toml +++ b/lean/lakefile.toml @@ -10,13 +10,6 @@ rev = "b2958bc1a0e2db353d06d6c019cd3f8bb8a1163c" name = "ProofAtlasUtil" roots = ["ProofAtlas.Util.Linter.MathTag"] -[[lean_lib]] -name = "MCMC" -roots = ["MCMC"] -# Vendored from github.com/mkaratarakis/HopfieldNet (MIT licence), as of 2026-02-10. -# Provides Perron-Frobenius theorem and stationary distribution existence for -# irreducible finite Markov chains; see MCMC/LICENSE.md for original copyright. - [[lean_lib]] name = "ProofAtlas" roots = ["ProofAtlas"] diff --git a/lean/scripts/check_metric_instances.sh b/lean/scripts/check_metric_instances.sh index eb1523b..5932486 100755 --- a/lean/scripts/check_metric_instances.sh +++ b/lean/scripts/check_metric_instances.sh @@ -12,9 +12,6 @@ set -euo pipefail # (metric, instance file) registry. METRICS=( "resistanceDist:ProofAtlas/Metric/Resistance/Instance.lean" - "commuteTimeDist:ProofAtlas/Metric/CommuteTime/Instance.lean" - "jsDist:ProofAtlas/Metric/JensenShannon/Instance.lean" - "diffusionDist:ProofAtlas/Metric/Diffusion/Instance.lean" ) MISSING=() diff --git a/whitepaper/whitepaper-zh/whitepaper-zh.xdv b/whitepaper/whitepaper-zh/whitepaper-zh.xdv new file mode 100644 index 0000000000000000000000000000000000000000..c41aae5b8205a785f412a4a4b7a21f0d67ccd98f GIT binary patch literal 414400 zcmeFa31C#!^*{c??8}>(7Xo1kFobsoD#Lakd}0{?UGn-|`k&l_eE1Jd8$QF1dUcRlyq zbI(2ZoO3^SrCqe?(X7K2`q=(4Bz#Ks{OVJ}^=q0|uW1VB=j9jYXy|{o}4?srm41i?6}-{YbsY%FKwdVj%=u|XsT}*JF7x; z^XwX`^UU?Ds~Z}smyOGvzNYC+`rEkNnbqshu5YMn96N5xl$Pe^j{6^aySaJO-ug8= zI%XU_ba$3$rM_}Sc-rda%a+#H);Bb+u2@>#7@jMa+Ei0tCz~<8$aV@6S7_v;II4qM`9!*Tc?J3568TG6G5zuAx~271 z)uLP^Ox=ixe@n!Oa*tluR9#mUUbL*Dwy`=af6715--zsjf&yewG0}}@?)C^kwIC)- zy2}5KuB-{1=*pNgtN9Zv$)H(d4egIc&2<^{YS2P=>oUq)(fgr7R|SVCU4;cFGjS}! zmobgVE574bUA*s5p{v60=;}Lu^$qvI=}f*_RnfSzC@EcqtEyL%oJjmbh?u^vefF&I zZcR#m;niyz&kQebs8}D4G&k?g5INQ`DXyg@T6KnEDt%&jwes9mzs$>>-%wFULTN*F-O}|WAI*!2cE?g5b#cK86~zYy75V=Qs3A1L$xBLcy|Fu$9cqiOnBhVyLQ>O&e7E|#mOjzx{{yQp zl9OLVdc<#I^(EJlba(aj6NJrW!^r={@=NVt|K0NYe{A_lc_Y8@+gN_dkNmx|{0=!O z+;aCzeohs#=ULy$p52i0lIdUfQx6t|K+zd^4Ik=E!{oEY1+krw1c@{97z zk}YYxdFUOPw)fQxmb+@v|HQ7ET+twwb$ZX1_5b+wUS3WSY2D;MA&AM8G7dRC zy(MKF9JNh;-XTUA!anqg-BY?wYG4~04(AeVb;I(?@S@zg6-`am4RvJSXxLmX36ph0 z3w@e9-g@ol&Cx+8Y}s8PYOAVlYzmJRmasW_QSNqGW!?~v{JT*LR-RkfXWg3G+9fS5+xk4WeMocjw$9-tmp3;@2hVLmLxsFeX)O!xG@Bq$v1fj_PG;ZM zIlbg^i!!^V#I~wpMSa8C>V`&P>KTc_YU=jEN_G#ZT3bV1U$u@b66==LEDs+U&Ra(& zqlm8;c^m|_6;z(h^ z;(~mFMT!~W;=Dp?FfVU$ zK}0lAyf{(>6|br6s@tf&Err_Yo_6kH`q3bM)OY<@CVrbPel&?6wcNUi*2ej6v@!VJd%dp7@Q+HYyV-&>L)r*qF zWX?<9sWtjr3au7*W8TCVO2izM5LFb&rBNWpoiAF7>~G#mx-m8}CM)VNFe}GO^GW#% ziVFAFYH2UFT1?|Drm^P}hKu&Q63X;eui^7(!D!^X5tE4&i#ZfbMT{vnZ+WrxC*b+S zcp@~f1p@2zccVA-xBL>q1ssy2;&N@4LgsMD0|g=tV>tSV}B_zAWo=aCPXhK zff5disHcMI5BLNQ-sQ-OGlV6Y%TkNT@DzTg*#Ez_RE~F;G zz4HjK5#q)&6|6=ug9F$57{#2;=fIM5#r`2EhL}JBrCp>d7^)y)f)R5~LIE*Sjq!<_ zdBawu%DI%mjYKLMjdi4pn+cK@f~Oz`v981f3&A*sMzOZ!h9@{-WIvmKp+TZor_f7S zD1wv<1v+Ri5~OQPB28)m^-dQl&1jQ6nXl}93#QN@wUoqTaVU@{4B{_>!|$Jem|KaF z#KkF$S#YO|Q(rzy>E+heod9jPhzroHOBgohmk`i)BiY$jz?1x(x8A8iryk{2EDi%~k2xzj0#2I7fN=oIyQywzKK{HPP@)iu#YQ58Vj&^s?}sOJ4dKCWHiYth<1xB!q{a|v zCP*INN6yBg5?z){TBpK%T3C__bvz|zAYENQTy&irjMvswpH2FN6INH(O_yqiMbm^z zCX#zhP16!m?kumKOL8N1HrWgeHod ziit`JdHOZEX&UnOlOoN%SbtNN~W{#Exl_6J7k$)dPdbvzT#J-PR*e<`%Q_fIMr2k5k!AgxX zL&;Jq&04N)%GH}bZWhCvsSJ2)hN8^wmEl#^*H(3nZDyyk?iWi~%8(1tyl7i)+&{9qPE$}#FpkwX})=DI~8@$`HD^E5I-F2 zb-Hw4{H}zpS58q@wzS+F-0>GN;=jG}dZ#k{vSo^^tAw-vX8F5!p;M;8KXu47qn{?z zK@+yibm)15Wm-XWo<;vwQD2N+SgiAsyrR-FTIYpg!Ba{SMR9SbGVnJiDGphW_~Bln z*X_UcLJ3RH9w^g(=e{M=92#*Jt*sjKnriv67|DC5-PWlLO50*loBFc8X441$T-MV6 zvlC*qiGJsI^}18(_uewaF8?Ne7`T(vK;J{P|0n;pQdzI8RI1x-+s_u{vPdVF1wAI0 zING0AU$=bHNsL_)xrj73@c+C; z$L4EgX;gC5nCT|MaJ~OeS&JYr@kyVyTc!g}zDTA+{y0RY*@Hiq>0ly>8f7Yx=NTpx z7c5FMu_jm3S2C~fk(XquUG{-YGt(}W>7YfkWtx3tu}nu_a;r?oZKP{;`s8}`s2&!q zNlXt>#1jq^McB(5WHHY$ot6XF-7ddp-v6*n1vO+5Yo2s%r!rXEU@GY@*7?70x=usd zDM!l^{R`ibX(O$}*~&U4pGtb(Io^bD=PNRUa;#DwL+)K4j_Fh)!^W7HfI;|yQ93i| z+ec>x0|)3h&Mws}8Iy5?{AJ?n_sKMeuseb9Jd-*;Td9f4F%SOP`KF$`w$#e3YQ+Yb z_I*lU-ve$U9!H-um9@(JxK%J@Lx-t6A92P~oevGTMHW}iqA4yH>qu`rYvPTj!miOe zt@yWP%G`Y&V`ZB8)^BAxkI*d|KSSI+wX}4klaP?|jZw?hV)FN`09j+g9}{~L(glLA z6bfO;!n@AoJIBMauxqvJ;94z`f)r+Pt=4RDMJ6`0ua{L(B87~x`cgYEJJ}1JO3r)B zO^ewXlbyT|>D)ThN2ip2v2=i@FYaN->-=~GNnqkL>eDA=IzELCapkoBy{zGw;!I^orsl zsQAS8Xt^Txb5BY)tm2-uOp537lD4|3e(5y&S5rmD)#O*T9fA6CNhV8_z%pWQ2TFQd z(kbij!l*!?Xs`?5UE0zyWM!~9>Q|KA3&nUUE9&a#_#h&j2F5d%23GeDq_0sKLeEVJ7!2478sCsG;HG!{A{M7RD6et+OF6;nW@ ziYYFHth7dyCKi<$QQmVIKrSkC4*NWoBgCZ}_Xw6lrDH-|E5RcfzOI!4p{8G?2b zVf4UKaZDTAQ;e2cW7HL4*a$ts0Ctk8j>ZJDrM!FY;|XVr&g%cev*mj9cGGNS_hhzgA2B?_LIGZ6pUsLmKW6~< zVGQ71nrL!%zx#{*OwLKBLtldPj5y1rs!t-(I&;G{=JV%Gx?0zF&0;B9hhk~p8y=9~ ztrbeTsD8zQJ4w8uZ2Q)W=&tynRIZ;z_ibRZ_C!C9?ooKr18^2a6VLn<&J7l4Q7aQq znm6Mtda#d*JO5`6aV`2G&U81^W$c;4Omv;#7V9Ut18|$LxKnZMyu`c)qP|Op@8oAX zaTJO>(}|-{96)ReaR6}?iUatAEgkBSU+f+Mmqc_b%NM%ieMv`X+%S1|Jzbu(mGAYA z_Ep?GaOLai#Ouslz&@sR=tT_X{%QNriy6%I;k69nXyg!A|DwJHR21k-Yc84xT6QDnP^`*^JP%VZ0j0T3*PW#UbuDST#xouSPwM7gCSo z5buLYs|h^F3t3ge8&97H74vE=D!#D?)!29Oif!-nYP?RK$LWRswcHlm@mezjJI-eS z_Z$Y00)%-&?K4;o_X7-|R4_n*PlHNVv~g4d6Qyz{A9#xwd4>TFNV8JQU>?3Ugf|;^ z2i)PByd;6vE|uN4BV4{la=r?M)MW=wYezFA>hBVMgoY*#AhdRahMlG>L=MnNC}P;% zqr@WfU7ff(f(f^b`9bncOGym^R0<5;yUm}h- zF#G`0U7xWWS}6l0MrqcCpIV{xK(oJ?pI2|`w^SX5>iW+6;gdE`@7z4(~IG@cAuKU(EBaH}N^=z~K|_ zCbc2m!6yYK=+_?a>*XO6ZNlO|}ls zExJ9T_vjnN;YE?+c+^!;x|5$bgft#^-VoAw9KijCIDnkqjFRK#R%et^c<^~~05%h_ zt{pH#{I~EtoSaGE6Q`bE(6F8)(vGH&k|U|A$Gpkqz!AiMd(q?<56E}4G-q9t9ezfXnUBS zI<0!i%h?~}5Ve3qycrzgIGc+G9GiHj{K9%Rj`Ra>@jTvVcpmKro<|$P^SEEcL6ViEkgYW z3Vp_B#Qqr1gRDtC4v7?G(oi;|E3h*Na;EFQofXk;V*oqP;U33wq-hM`dX@p~oRnsr>wihwm+P+a1e_Z;m84iKtobB^G24&XL(N+nsl^acS(ZGWN( zocz}B6`!O}@q-)Ur@xjZ|2E(w-OPT-vnR;!qdz=DrW2QaA=A9uR?4*CUfmA9G&)~? z7b`{h9ORQly!fr0tm7-G;o%@7x}R?KzRAY!f2!RpZk5akLKYh;=k)A^RbVBUY^81x z9*5nknizro_}65|J!8(1Y2e3wWvbn=M5crOP$$zfXh?_CaLCXfhN4tbwV6jfV{%dre_j_yEkELD@G%x;rY!Bj5DCWr%5&E0W|S#cfo&NsUq5FL2A6%g%spJ6^t zmnhEUl<2ENrE zZt7IBIyRaJ$MECvF3@MzAe@Z*ek-n>A5FZ^6z`nfb$6Qt{?aCQ?s zOgQ*e!sOxluKXAa`89)^`zUJ9h-3*XUPN#7>;4TgK>Mg8@I-dbR z{(!?tkQ*k4Vl&%yYv~=pD)rPefKs{f4sHPrCUFjqfdCo6#0^*#civ_krD+@QxQDtU z{5tm$n&C$_SeYKRZTbWzRo)$&tMR|W>h;a$fFl^d!3{JWjXcL{1_;)20LQG#`=N4i zL@h?@17XU+B@+8%tYydGsO_pHgt-0}RhSe76V zqfUbj7AWg+vjD==?qnTt|Azqpjh+&g!^!1fJ4ZE?0;kc7iSO2cLa?$>r_~!$$Lt2_}+rc3FY0A zR7r+70CJDr&$_SO6t%5Or_L3g`zzD&#QwT2K64=}edjL8j52?BwM?_NPmpQ&{U6D6 z^qz=JC;a7kndVKon*J8?HqDeB;AWMy6StH-#Qo2K;! zdyCowSJAnJj^q2{A|yJr#}40H?SU&{O7J2h*euxiH$DwhYga*Q+Cv+~#Kz&+1&S&4 zG}(ipzc3xEI1%$>)u3k&?;^%Ch!`pA1)=E5PG!tBpPBsmZNB4WHqUUKhJ&U0u_fbf zaf78O13vk_Ob74%olJ+X*Y$uRh`{&vN&QxPnG?3qDmGH*JO%*CS}kTdKq8VyvqQ}O z%h{LpS`%j2=Cu|oX8;eET44!;{tq@`sMK*HYsLOs22fWr0CA)a|H6wBUe~5%_}Onu zYxB-CQn^uGDQ;9dmBWgU(XHbpjk)V@kg1<;$Hb>EUCfHlpmi_Fbm$M9G9CTlCYern zN{_XX^Qs=Zp=i`Q5^m+5Z(qI&E^vhjLEL?sq($(#g`>WE0viSF{;;HiL|<`BiWoNyyt^`6Qq^MegMjA())V&$3}Cg* zYuv*d8cG|+s!{pQ3C3D*26vyu>);1S*|)E0y;Mqce$y~!Mlh+Gri$8{rCp)E=lI-ib8YvnuYDzK2}aV?123-W1q96tVC(`s=1PM4y)7H}+poDohfv+h;7 zDh5~TK?WDm(UPn_Xhqa0YuoGvkxnI}?n%=r+*n@T`!16OgPTUnv{D9Cn}EAxC>^Ow zyNP$-Z{tH$HzTFO#S3hkpo0!}Fv=zoX}j^5&ACM85wviT$KM=3jNl z#-S@n!DiUP_dh}0hVOj;6OfDk(ABoine;ps#X4->Nm6{)o;OTNDA)12df7jJxumDi z$EV6PWNbaSWlDZ2F5|8jA3R20LALxlGf$22l$f>~B7+P*saNve8W#qnmTu3XnRdVZzL z*P-DfCmjGDrmZngD&9=hV-mmA3^7w8i7 z9cfy$v!36HB)`f9C4%=EP7~W105(L3l9Z;kb=Lor6?3m=0MAYa0O|vhCNo}|#BM2d z&@2Y*MuJ?H2xhaM1-Po(Kbhswo)kLyUKE|8w!VMtrsxEOmNtybMBW`D1Q;=I)_vzk ztq!hE3O2Vp_{BJSdT6;Ed-mzPRTMwHKjx*Smq?y=<%u(pgieKk=WyyJLk6-ng?bFXg_Q=Rw5duv{B>so*ud=3ocUqdm>@c)#Ef z=O5V6$(DgZoS*YT?x%Skv@k>u6wR7IJr-UlUN{EYr=u4N5}!u~gfWv*=swNSq?@gNww3i52?5Zjj=qJ6=K6?lv1K|cuh zdY;F#lS7<+dClr(o(ET=)tQ`^!o}dfowo%h^k|6aEa4S9tE}nK&9BY?fOj)n*o5Kr zCO1z^emVtV(-B~;fTTd@*W0qnXLZT&1K)naBojHu=(lXXyf>sEV_1h~ygE~UuMj#j zg*22K`>(Rta9Xo|!PLNg2AyJDAmg)BizW%WaR6{?Ba2N!o& zHfysFo(~ZePyrNNWW5F<_OfGpo2`B`^&(z9Z|I-}w-g_Pq-G=jMa28t{ zV4DhO$>c{zjca}F^H?is;pkqKTUI8o817)e1ObfcEQpEq#m(=r++{2W-~@Y0G!wV+ z!Cjd4GnNBxm?0J{95rCHz-gV0)?^uUu-5Gw132GefV6PyqZ=3uug$opm9vo*ftHg` zh;|FB!~PWmc$&7_et7z~v#4N$Q7Se*%8h5h&{)2FU>5Hz7zG=oJ;DI)Z43aW_5LQ7 zL*-W`u0oc>eh&knTdIZOW7PIT5$33Sr(R&IFkHq|TB{(E%u%dFq%f{E}Y6N;WQlp3K7IA{hq$l%>$ z*mMF{32^fYuwiyw%IXMke+*w5%i-mQbZ#fh0oDTG_o8wI09@l+I6bup#X*PmuZI(w zgQcR0?-=a8Bt>(u6_x=u1T&8_$Fi``VmZ?1wRD8tuLyV0g!cDQ`!z4Z>QD>X%ed7l z+2GqZ_L7n6#!xSskvO(<=}vu?L+}zbbV_U4AVxswux|e&uZi5zSSkN49OC%{hiGsK z>r{9it6sCFQ2(G^#H$H`Q+wb^o+mw`x~#v{U+-m z9?9{jh>95d)x2Uje0Sg1IBNgw_r2i6LDl>L*b4fX+WmZHw3|4@yMqrCi8TPHK#O`( zP?2gk@YemCEJ%~zji+fCECvl7v>Q-O=LgM{9+TNjQb=|VJc&NSJMMXfqYWv@LQv+_ zq@o1#-^H8qxAA&{phpZvcpePRMF3QMn;Oma$US}INu>G%xM12yJ*ExAP)q=Nd+BNMq9jQ>2nmfs|9C69x(oLgR%~p z3HU~*f<&^115$KtX!UcwM(~=b?d6}70NQt5rs@9Ju;4TtZVWf!az$vFvZ%~muzRSd zwfnhkvmV*q%q$@cjg`#A5f^OY%TbSmZ>~5{#7o&hd8?i#dQPML7W@(go&jgL_&_NeWm45F9*FRwRU9T~jLh!vsg#z9K@)w(ybcvXI_ zxIUuN{a}T9xGZV^lr^jIlX#lAEM}RUEexO)#hwuB4JW8?Aqo`goq#K8e6z7X&5`gB z1Hb_ZY>>SCGSYVst3#_}faETo$>W;2dDp>2fbK7E5zOkKY5t=2?{5Thb!=}fe1(nQ z1;=6w1B`dt3twgBw7r~*!}M63?=RwoaKlLD=wpY#uyzn!H8ABsDGbm1?iAus0xipf z`tVy0H))2QN4%hGhVQSHX{gV!G95Vl6qycf*WGK1rB+xtr_5-hDeFbensz5nw)yT%1jwC~MnnN078J_AkFyJTja3!K1bL^LNO<84gPQcIj?)1HK$6;X@nm zmg&eVbxe!U1%i7g4(g;-ME6V_N@E{?!bVfW#+G1+xBhc)(Ho9=5g0Ka2VL+R2@kiO zB-2q1`jiU4@)EIS$T5jKR6oEkSD+Ap05YzGa__}OOwNC+_T9bciq0}#2p6QfPfzbK4d%$uW8d+4p{2mfh>oe zM|iQ^;Q93j^bKq`K*WKI8jzb{_wxift=S9%2?=#kJ^4nvWu-@>?rnnP(ZshVx@@H~E77jM?@$_W?FnkhX>&eg4k`}=! zopObU9|n(dU;M6w`JdBQQTlQE_L4d3a|ss=P^=$=d-reop-K2PY{?C;!N4v0b*A=< z|H{(BOHlYl+D_Cf>o0s3TybDanjXGI5J8fif0ATdjRia*Oo$ zbm2*ani9b`&!EzFK&h3DD<8J|w|q#UV|gCWHc>rwE@$CKCSQ1i54tz~Lv||Q|AF4Q zIUmck->FB)bl~mx$aKhuqhuQXZ@o-My`;-J6XyIxey=2joF7)JXh`wVkM#4W|D{t+=Tt5S33-QN zkU#uOSPA6s?^wyk>*~|)SixWxTo10EDVP~6rGa9@LO0&YOQBN-YQo(CUdl0&GdguR z&!bLb9Ym`=_ZTQNmqVA2DHxC zWbcIZlT5#d!b#NL@i34vsqiLqOZ)U=I96Pja!4u;DX2qJN4HP@fKihFE?zg<3!q*N ztv*omr`^kIcHGP%_yCg%(IwFM-vL58F7s5Z1MnCTJcIWz1#uhi-|V0AYP7+eMDeyh za5gW6q>cmi9f86%*!_FCji)=z=m6f#(C%e99NgR8K9c1~?zHN7kQGV3@wqN$MUra` zgqWmlIV+)!XkiM`8af6qdM-s9(Xte6L~l%B5(->F`*~~ME7_>sH@Ek_iow)IPB3`( z(k|qLCoaZ6y^VJDZkcnp**^U>g(0>aL+;8y_`Rt@UXc;|GyTY4%L)dZdZJ8+6#rYMBS>B@Cyj>CXP*IR zJh*{4QU0{UmaILE6Ki#i!KVz_ZD2f~dVG+~3-)a2zJ}XtDqD*e3>Va9-k;Em$d`g# zh)o-|wR6w$oUBRok5kG(V0Z1pWH~@ehrk<-SCZ9{oY6S>83}CIMI3@X$FY`Du$Biu zEpC$!l6rDCCh#B8=z=N^(N6BUiE?IWJu3r_O%Trkbl(uo-_PwRV07?01q4abPbvvd zc0`Q8AOcHTC;o^HCzRbj@p=YB6-;OmCwH}gFIqIP!B-g3^4x!3)V}x{;?4*0Zh7&q z0W~ORIKYM#b(0}{DfC>97H}{`6E_EVzB@~JHR%wx&7wPxKq;_ubwEIe;1*sAL&PB? zwU85O5)+amO>zq$-M#@?COCBiiwR!Pi`FGQJlw!gK%9h^-(VchzHHg0gGbyXmLqrp z0|0Rm9n&=Qa8$UZgGwp5mGvO`C1mn#1w>v5Zv%IOzBgDuT>NHJ+N`il?vq=Kxpfu^ zn#JWTpOv7=7YS;>@xYG@TV2>@zj!fC(c;Dxihf-#B)de>oigu#SPFiFm*mTI{JF$aI$TM>0J@dtavWcIm3Cg=gwl^Lo^% zEM=Nypixr)68n!=avD*XybJYfyt>qtfT<2S%du3VApAW&PUAillv8}G8bE>w51)!o<%ajyvNDAcZRxLRmca+&VLn@ONOyFiCujP`Lwk z=__^UndeEk@x+O^WD%w@hf9m9wY1(2(yT&QA@$#E+z_7(EFHjc9)f-bhptH>#iFDQDFcnb$ELw_btYdAr?|)n2X>3H5`HhIX9H-l`((5QH_^K5H1m_AF5> z^L|eKzSf9QPBA=DZdM)?QZXsHo*ir2M~tgs!$d|sTMrAG{*nH6Cb3}JnWdRumDOY3Xv8^WvCG@cn=-cYeVoJSM)U6|hehUukH4K3R`r1`rqLaXFHQcL-%uM6;<+ppMDA1ARhKor{k# zyP}*^x-J9A4i3Qpe2z+<2cM9~t0=q~vk2t3o!9IKJAU*cRyU)^CrC%=YM??CGZli@ zYj^Svq1&$Ge4YoqPH@LZkp#PwO5<3{c{J}1Uim=~Ws444xH;$ngPBl9H%t9O4CVm? zgjB|26`Yih1+`=-&ww5mRnF-k7NKfYtl>CSHX)4{)dPo^dEup-?*qPDO0^rGU4By5x+VRI;9eX;+|>{4%{@jY^XluCPb#mkt?GJ|YdISx83_;PFxS+L z!zprC++z1R+w9LZiXKkXdpM`ZJ*-`oTf545Gpy3oxtB>N^~F)9vxIZ+RJrK=xu3~2 z{Uu%3X!wK)8`u-+KM~M%q(9qyz`0I0H1)lEj4Ww9>mx$Li_fsW=gL%n^EUQU=5q}@ zIVwC5|H$zpemZnDF=TvPdq_c?hfab(PqUiWd&QyCZgV%Egx zAjQA*)bsv%!2T|DAK`=H2odWp<2B)Cj;dSt-Nf6(;$ii>(p&A1GMEDl@&l__78Hv- zUOpudTAp4?2e0%H7>?L~$18Ghqjw7r9}Fh)GOxzz;}Gv@9OC{vhj_+vbX(lP{*0Hh zvTx*hG_X0egLsji!Y!c+5kKq;`E9P9bnFwV-uW2kWnWSNo_N7)tJop1jbEa=2n2CKFx^UnT0V3L? z+g2*3MgE64B0?agBWfzF*F)%7MoXS;3;^NH!5|Ud8X+gkz0>$}na)#L#c-~H+iZUu zD*`RSD=o?Ifs@B#1~(2c;IYqR9ZTnT$>1L<{2I@7(z$8;_a>NJmHAzm@wTeA@K|%ruI<&`b3FK<6kYgeycnU+{E}ex!Z}+(4qH z9NP+JC!n_~Mq9AIy1BWlgL^cn!&w~`o>(m%fzee7?>LH6x#P%uyfVc*j_MTeI5w~w z47l`gQ80$MI}WXs0q9CY207Z#ez)lppcnr+3}X(uZi!pspfXEttF0EWI^aZ`Jm49; zDiGxE=XM@01cF+nX&djjhjj&1asFFa4mgqn2GohH2prQO5}De_ia@kf+ z+9U+J0t`S{FQszh9e1;OlY1L`f5D14d0;j5G+NaxllyMGOGB{(gZ2WxQEJ3}$MqR! zSh%HQ?8D%hycE3RffpR8IL9~JaBE6UNsqT}zW%(6_@Z;Lndd=noziXJDqfFM;#29eAHzl8!s|h!7c}NUO3-#ADF$Y=YOk_hYM>o(D!d04Xq?3J1PVDMn9Ct} zO^6Yt9l~lZZoyk=aNBIVo+jC)hjyXV!Z!}N5w-35J?tZN;ROgrsRGQk4z|R!>uKn@ z#dYNdJ`r#TM~)OK6t{bieaM7dOtKP^55riF@J8C}Wo)l*jnl$y*T&Xajth)*8&R-xIuOEx5bo-)~C4i00w+^ z9?Moeg0)OxA2&SWh^E8ricUB-%Ncn`vI1Zo+; z_jd+>SD0XKLXWT_=$6&Oa`<`2fUMkSu_D0jVgCaqbn@v{!>kqm8U_HYCtpO+)6isA zsR%9*Sl~~1;4ew(z`yC?6c7AWmIr89=wJDoD?=8K1Vqn=Ydz*$FSj9JjTXy zAXcd}abl+EOdLRTCJqqf3;+(R5W*q__RXE9gGaJv$s2pmZ0_&|2Y6^Y`3B^RvX;P; z8W63HW<}_BpSGG_Fg!cz;~tCiQQJ|d(t!zw*Dm1}ExF+oFi~d@l7PL4Ll~4E#jmmit z3io)h3p~L)UtDO9qhwsuhYls=Q!t-Z)0NYXuA&7hCL zg^&u7qx}sKFwSyt5g!&Fu-)hJJn9q<@n49f2sR3E-{<^K6= zaKi!y3m||x-e=8%B{#$q(fCUeAhRZa^fbANEetNX6o9+ld^R{Z`@245IUJ1)Amq&f z>r41)HgpxTBQ&>8g^fBd<}?>9=MY@(c2KT*&*i03(W*F?@KT|_F%011N<}}S4oB{T zN_6CY_zEBn-YVWMABOfChxoo=14D+5>S(l|^nC#pgX#`#8XXP16gpA3g#S*z_m-KA z_=MC>pENZv0r`-9Ve14QtBInE*-sW;@q?can@yf)Jt{4Fyie(tKI$KDlHdEz)MN4X zUvQcD-X}|0shlGJowU_{@^Uz@pz{(w7V6o(al#1<)t*j1e8 zclJp5Rw6T^A76O!?&L_pW#+W;JlbB4j$u5Dvo8@U&FpCJi6)MRezO;8S?r(nU466 z9yw)Pt?u=Zb6i-$XA|N?^pr|^D#p#F z&isbAMo3@OC3e!GcoRs1a|( z7;pCCvm|`<4SM_$;cGLK{#zxYjzn!&kzrcD<5rrU$uy$fxMu4dB2@c-vH}qio(LY- zwa3dk3U(mG;J;knz;dD4bPD0!k=9wQpwmjuAlxUjI^e;b0JpG$bsOZ$Htlm(sr^0% z2sNUhk^FkZ&GncTet2!RinoYPdiY`>PB7pDY`#Gw4Y9w#_6bF>+C zgTkci_NeWh`)K{e3q&gkisAizv)Rny%fbG~q3`oK#QkgDFLegbgW4T*%GbauT>T!e z$4U_9tvkW*F6iL(pb5Cu+eS!~ZdaS_oT-H0g!Y=q!QkT3J%`~PqkwN`Ig)EqY)|ka z|6l-mIAre;a7VO=}SK!^v#*0_bEV;3RictXmgTduSaa$~1kc&TMjL>JqJsc*_X ze&l8AWjglhoiY`n=7f8%5R_z?GDI!hDmkm7wsU4uS2FJQo5Tt(ZwI!nlTA#OQlGH< z5|X$cSV{cB2#$23GW$9q3R<2^(^QD{>wJJA&$~26FXFA-G})&3F&KveCmmp;V4do- zh{y!|T6>IjNytN&a$j6|-&%7=!u!_Jl0V5e9+4BZ-dP{z}fa9hWkEp&W0ewdB}A6)yTZsmJkBUhs#~F5-EdAMq~x zZ{c}R{pgs3JB^3i?xoLTWAA}Q2y8_m9={{R&0T;EskgI!K%jAACM_N5iqb5CQ&|PZ z5b@Y3WX5Pyysw7kmEpXclDwkQGD-^zA_XOsMj|D}#V=5NxeeEtVnVoXdRcN(-!+S6 z8j7WTZ+Jj{&!J5ti(DFO;=*gRP?MX|u2^s<2}hJ|A2TjX5|K^hPT})G&LI+!?K+6a zmP!aPi->HNK~27k$d-^?w)J`Dck7zBbZJqW8TRqxh&M|yb+o!$B za-o%H=oDTKud^`@7rs^z0%mP>3Ob=VH|6PxSbGYPv zLdPba)INMa!mE9O9=uFYDx)?>L+fa6_ovQpA3coabC2WGfL;|~9Vr7(M|_u?b)7La zqU-&C$|0#0x*D~waepu^N5Y3(rK4zs{YLqH%&{3Vo%raTGSwe17ph#b0n~^zYA^2x z#B1STJCzMB4UAkIH$?5zKP2ku#XAq`U4Q5uVzpOGKot^D<+#Sv?q_+h4QGs>g zb(>W*X4W-H{l3@!YC3EhR6kys4zwV%&wBnWB_s)4{4k>Hdhe5$xk_-;X!*B{x+i67 z#J4i;kg_K}E#ag7G)<;s|9YiNb3V}@L@hW7603t4bfSzyTwEtauC+^tvku^q6GaO* z7slQawc_dHE{!@S-%XtxxcM0KNXarnd^|gvWXj($5Zie^{Wlw%g$)YL2G9=C zwSZ3>8U&~dtw|TGE|to*J^$vG`(7ubFXu;FZ8alkj|hdlhb|B?oDae8oeiCy8bmpIM49E$-b%$^JS{t`;<&Gb557(kfT14Y4-4I zWtvN)F|p1zTTM31FKwHIvtGPz68 zuhTuh4@$fSGX5pg^wVQo;h5Lu_rd?Cn^O!MGe~|PahATM$IjU-zgJ3$ksgboZLLIK zaY$>@#Zw;Zh{Y{GXR<;Xj@Ty|zY^R{e6qivA=9x(JTB8oovUS9@N0c5E<5iT`F-l- zb7Xqd*Sg)z@elu5exG~p#WG!Rc)m;*efg$LD;{c*>2#v1DP1NF`cYs$895v;Patv) zwb@?(Dd91mj*TZKyGzGR1VEfopd)oPn@CU>;T@WH2j@~~myVh?49l&8-I99*ui06} zA->l)s`aeX&J_F_tmJK~JApiDB`8O2um6Z9C#QO>`5NUFIk$@c#4Yld^fUFtP`^g~ z4r9=V`ioA(p4N|@*)M)6^BKf_EbX)N$+kt<36;~&*)i@8>(KK`Yv64*ZVQQ@Iymz1 zr%jw3tiGP@lpPp2qfMqm8}F8>u=YBGw)_*7)lu8qTd{q#9^B67#(q+}(D27eP#*A& zwg^t~KCcPPyNL~bH6@MJjcRk0%`mV-0!oPMbqHKRFba}9kHnQyxAAcVZ{rZ$(b0<; z^+~Bbh>gnsRMbEYcp=roJLFbA3a4rX;93G5Yj0%@I8_FKPA2yZfa}K)SIt|*x{B`k?0Q0|CyY;swOAFfAlm%pno z>+h&#lZ94gn{5TvC!((F2SbsA3&*O?a}@1A4YGpFTAxfyB)gFsn60)IKkZc97r$## zxfjxG$6F|zB=1?{<`JCNz|IM-e?zA+th(zMpa*D9)1rGm7Xs`zHnzqwLU)n}VFI_9 zAiuG<2v2FfmXZfW1kVYu1^`bIxR!z|X~&MOdq)OCAUwzB;HHI+8`^AZCK0*B6ZlG$j(|&utNB@6DTPKkj;mQOdxQUk z%2=g%S6I$+B)6%?Frjx^Q=+EkG~a}0U8 z*9kDkz~yW(!F3E^ag-RsvN-t#8w?L>h4=Bmjx0^HX1TCWYcH|x02w6bjKO<&9T5g_ z@OUxs%CalKih!djST;l42RkPv^j>|KwF03lg-u-6>c^}_eh8%^Y}Mixkrx;yKd3 zO{~WjP9H*b`Hds^Vbfk2lo#*s-4C?c+Qd~g@BDu3mZP?|f5L>jpJZe6aYu5HjnV=e ze*;bg+{!G#>rLlpm>@Vna7jG5K&ZXUdLQD5YUK)4`#NN5#Ou!-^|}0R*hhzteMx?AkXq_v|1;>1qwZ{n&K$xXkK5X$s@3IXFbI++UI zq+;*dj>LfXF(gB^;JW!n*&*W@gP1}-YOB5#=o3tpER@S_d*0-6gY29RV9x!mYyx4< zAwB|%1cj^)Fwu8%KP~?QtOzJHg8XnC;uif7CJ04@3t1~Dn)Dz*mHif0tCx=WaXTV- zD3Kfcp0`FO*b9`ifd_zRm6IRllJAcY;flP%zdVt1tVBW+Xq<$S%2LB`(ftrt0)k(f zcN0Tdasr1Vc909+$f7mg3YRM2Y4skolAKag5N3f=X|k-G7sS0>hxXWc2^|>9QgL7y zeC1IoIxx&RU=9o-oLZa{gf^t5BU~~IqssYTVbcijP&v3Z1UvxYK;`*6t5luC0Kr)d z0A8;SUIc8}-CR6({g+ir!Z!udTh5?|j&#U6Mnd-2Q@U+h+SccJ(ox3i3CnQbrH402 zcu38vWu>y70()2lKmcdI0GEy+NDwK$zn->@ja02;0LO_80Q3Nn6=1s!(fH!F3V3M~ zjV?BMc+x}IXjt3c z0jyFFx3*9!w|4dgw!&pJ@LfuNs&w{k>$A6?&4HbK0E@C;mwcAXMb{JBR|hzc7%>$8du{*fE7u;nRqs{A{u`Z)NupL z5#WB;aCZf?1Q(a;E38s?kO4eg?GF1O=^EQV1fG$+cTf?`g~iUzdI#IKMHH8fTZy4q zsxs14|Km~HHNv#MTU^tMjzNxx(YRk{op`S=V|Rf|(BypI3E&;{}>Dqg$dnkJs$}nnO}?4RVz8QmL4zS{RZZ z!Rx_5oGN$*1r>&7yn?ri+Q6IgG;xTgL~TD62f@j2{odqRYnWUFuG6g=(_hPy+w6dk zbRlfWvwDcU(I1{6;S-mAA=A9uR?4*C-uq-)8l5lGnIyE-NYf1cHxVTekAKotFFW^# zCh5XBk#@;Zw>&N@?Du)8Ob7PaAk!gl=$^vDR(~x?MTr_9GN!>!@-H#T9Nm>*g#05u zagq%A$=V+IXQvW=_c>GdyY6--^^-;XrMf$dma#{E&z!JHrde+N3aOGREhjNX1{vIH zd&J^3)>m4c+#di9`MvPsQ|M(r^+=m;aENk@TrOxLN*N$GfsTRZpl8$x*kuM)Dw}450hOw}>WR-gP zT{4KK$dJa`=x#2&bx> zdbqHTZ%2CG;|+#xjoP01gcxDM7;Npw_#1)v2u!UMiI^8yY;EiR=MmSYTKp z`|o(`D!=5Bcb9BUI7?cC$8%U!J3OJ~`3J8axT<{ou{)O$rBU0v$H8>KeMFkJ|0#X9 z6zT}VyCdl5A3BAO>2dNWdc)Ws<9Qr|+idTaQiu9qRAJKQ*gpP5G5~eY`7)hOu61*f zvfWUh?=r-{dmL}o>Rm}fp6Exn4qgJ`TwPU0z!`|aExZqwn*zfi`W0K)UGR#%PEK0d zYq%r$j%aoCW6h;U+8zBF3`1X9t?xG5LkkIcV*2~Y2@^kzi-nV{uE_#7Vfz*j5L@h} ze7r7jVhmitG2r_bhu{lAR@{XlquWceS|A39!8cnd$l|q}Cq-=!odP*E2kU`cyyLbk zGD(l_>n@Tg52@dmY38T86YSvjzsc{zGtQRj$YGDk^e|;F{8g;Kca$ zaHI#Oa)kQe$bxV9dPebj{1C>(`H?Pt58k%k2_U9UXuybuPjVNa* zxoLkA&Qw!R1T+R-WoQg=t2c_-xu<|DM3P%{yI+smrk2oTj(lO9X-b@Xr^;T5_#@&I z8m-4u=w~}wejhwgkGwhTvn%EIGf9{i@dxy%;hoaUOCOi`vr(IWo$jG}CLNPbq;bxL zar)=8ai&KYz(OCza?lOto`S#9kh-(=R3jbvW{ar+IxoeX7DO(jKI-Q;}zl7f@&dkGbc0e23|8h84f|(A{AxLdrC+~3(9&lHMH4q~u@H?UhwcR0>$vd$fE}Jj{FrJ(3t(*!l;s6Hx#b8(V zU3_rfjHvCYR-z`ygseDi#Uj>q)NuQr=AFW)(w!gh22s~%6|8`lLLIz)HE&%7t-KeO z6NVbomhyVsBBn|_I`D;8v}s69-M|NiTpOc$BFePScs-#AABJl241@i#*72XSiFKaZ zKK{Q9hUdmqR2~j^2_Y5QR#)+{1TWxYNr!hSgQxRSskCLKe`>RT@Be7R6Y8IkZKwsj zZ7>X@KyZNQRHoXZ>c==T@JUW}BJYp}2(y36>rpYD-)^4ALQFfE=fNm@-e2%M&cVnr zAu1Hah;VQV`d}_2RCuo%;+myttVnXpD!5>WD_lR1&7s}KS^>`lH2LAaN-%skm-s;+ z9lVD%3r=Tou z43ywUTG^wANEq=g5JtQ>1s@8FFyaY9z;}GTtV+z^3*^c6-f~k^H|KMDph)jSZ%HJD z`sh|?{bK0=?E?uPcKj5Xj>yrkCr5qyg#11}mP!eZB+U{!bk|7xU6P>ZpS$xkqD0D= zCNt9y>FZ<$$nYX&fbDHo#PUJ+A9*`%+Xai#Ow`8Ze(f@yx-!!)m2)*{(QKJ! zA6YEZ(U;sR({UUBBhy^3SlkLJFX_AxPi-)9y(Ix!U3GJmsKYr*R6rY|7Nvcan_Z)d zLM!TuB;LZfeBeU{aExOBbs7VJMPFLD^+j$Tn~KvltG5R>Q~|Nf>2Tb|D&X?DH}0CP z5X#z3t|$h1!}kfR!+sJ2fL|ck9D|Gs{$pujab=KD5RTfmJ_xG-ubJ+C1gd8S!QzEHZG$K9gGp3SZKM$ zXYd&^QNpk4(H}?8({(iyo_R+8LHGA6OA}8z3(qeLJvQ95reI{j@8tu(XD9(75#lNV z@qpbQIM!PPi(t)?Oep5KL;Lqnb{DU%T6S1fbz@U_Y-9D(rkeV?oct6dl#W41jBDPx zxKnZMyky_XR#rxRmki&zgiJn`nO^GBNLYvic*&_b4j_buIDm*n76%Z{v~d9U2nG-* zkhmN|o{j@J$;>$pV0)saLp}0~-6P-)ox-xbeDQ?NzNAB&ySRDh>4ZumnJ!)?W2Tq^ zZVJ;EmqT)5a}G~Q)HY%KPP$v3NHUkmtACtJUzWdkVv<`hQ~z5=EX^FP6Z4QOb%|~G z=;bp1h}ieB3)|&)i#?PYEP}2}W=AFVjpb?9br~|woI^}x+~CAS#sNU;lR@Rob&&7K z-edZ*Vs^nS%(+6xGJtfDH0N-B%ySH80C^-e7m)`nGr-Ho;}(v)<{adW!XWNj89)Z1 zG?$S9qr|Wx;5xzujw3U3+e|Q z<5=J;(0O~`5&CkJJ4GC`x)P)x&zw zAt;4`yP|AZuFE)J2LnhOa`QOlR%r$RCe#8}hulicMeNgAj-a^QGUsp$!*nx13MOU% z(5Z9{Gv{#jWp&8VmgXG5W~2_soI~nx%mC@ZVg^Xp1T#RXG`~vL;k)wc-DUeWvg)S# zrPJtNO;t`oDtp=RFT>3{D@Xz}Hz7D#9KgG@<+j1cT;Cie=3!VgSoqc<34qG9X&J$FWvY zIoTzcYvsXD#=~4!wTPp{0bMHvy+6?_tS1^2;@}c%5kD;fw$k#KF9K{89C&OM_CB`C ztg$6UBr}(QINfk+PR@G38x2A@ySX=kMF{5vbE5qF-f`c?oD}u`oRnJ6i3R6o3;?`? zA(V-iYnkjHCBSD|rfV_bQ|vBQCZi`Ecxo^1eau!{V>Y=Dv6TPK>j5GL0C7WvaA)5{ zsP<K9nL*v;i=(i>sHq{hNrC;KkL@i z)-LJj|LmVr2qRssKJfRx|WVm-R?BL7gR_;H%J3SkymBS#f z*hPA^f;(OliWu|M!=(-|2#7EUjLOd~Ibe~&VZu3&^+Tbfaa_khQgFIh z4k!Yxz>x)F09Z&HtQEiq2z-tou_Cb1suNibcUkO;A>nd61=`c{y5Xrt<8th1szEF=!r~5L{sK z7;oTE>9@ixtf>wo2^sj%3mqG^zq)96W2fjHfHEi%o4R6Zn z;)Oug)NgIfywl2SkhcWSaQi-kEW9&lTM9xeKJV=W3Cd7G`8Or_&5N-W|RTLhCARzu;o zlgyW)zT@RJkdR%)C;ggQnR0J1;es?@UWnFEm(vfTIUfqo?lBCFo(0xi%*|tUI&Nkq zlXJ1;syJXc5GalOkoTT^A!{1!pWslQoL`3i%_;@wR1_qjndHX?xcW(MGmjy5k_W!V zO&A|{c!3Sr!F>=@s05Cd76({sf`b0Q>qWsQSO)_SGl1(s29PYg<_TcoDcwY?FaF}2 zvG5fsvvAPvhicnwe<|;#>ac)~9=81C=Uiaw$yc$KRNE*GYRo%p`asrG!PIX%516_( zBx+l@nHX!~xxX?+L*oI$jKJmTJ9kM2oB6w|Wtz2pf=t8j|4637DO!9c{N;I>=1n+8 z|1O!Y2n#TQ%<4s;^Ge~Gy08^HbSLXLoUBDO-rFazT*(#V5I;D1z)oC^P)R7=dac15 z95C(4DWx|E15`vnDWFyAnHpZVQ|*QlDf#%5v-adVv*e*eEi__`wp`CJfV4$3uQpm)^`@07iJ=E)KA1w|&PLwBAZXg0 zojectnD(r34IKNuZr+r3zcpbc-^RcJ)mg<*CXKGmY}`Y^DdO}K$qn9;zrHDMEns6P zIw%`MY04XedT#6Nsci5VkH-U%51e=L3By1$sL{6~XI_tY1M7~AS9TDuwcmaYxGP3q zOhtk3zI)?rZb^hOqg5b%Cxj)_PUTp_IYMlw!W2++2GwbGl26h27sr>Xv6CY z!H~d>pUykY7B>GSc}1mV!VRq;Qcyx^BvMjb`~t1V4cC~~qw6N!d&PH+?!6L9z`MVGd3CKkc!5|(L-PSq1v_e_k2u6r z!b^od=XunyaI!)(BLAA!KHMV9Dw14#pR*XPdBKp)*_XG9E5qgEeZ_d59tbZ3zHkTm zNNc;x>i&n1HXZEU)X_YTI*UWxwHzbbb39J~tbWuFcpeRwL|_Kb<2{W-q#wIklY>O* zK96_M{W`BEl+Gdc_pAxd@hqdG0KXT7Q;kY4A8{uS>Hlr+G#nL^pYq`RW#cC!aX;&? zzaybTOiHFU{aQXr(IIA~r9+I+qs6(q1g~KL;W!$XLoAj!KpHuY#sS3XG!7u_5aR$s zj~@pRR@iX>f$}(ja2$;T24K_->%BBf_FI(+Bp6% zYzBqQ{5|nZc0z%W{(fSoGNdruEHn4tEM;Qf#vGXj|D=aMPXFt_Z}XpIy7-=VWO^9& zv0jN#A7@cm&soZN`rW#JX@T}9-(vFyx8Fb=4(_K13)ao)$3CX@$+uZUY2vY5@yyCT zA97E;&IcCNssvntgZ8a~8yOXI9gmv}Lrm>faYSv768>o!9D$yp+q z`&0e#X1T^=7~YC{*-r2^w@mwebiGUmUGN*3j!?u6k9a6XTy^E$`(qiONB&7kR7b{; zb7Y1AUk;S%kU#5M-|T_o<@Zq|n`Aov2A#h2SVr{lwzNDer;*NvQKki=U#{iL72|ID zC*h{4UDz5Hah%LcHFPYECJy4+)*jAcFpMLPA{@Fk;X6bqu%$=%QO5&5nA$zOkHM|1 zgLsB=-OdYX7xJcX5O@&xZ{qdf8=LkqJdZk>w-xx1=W!m*A$A=4O*{`65#iihJVXX| zB#y6m#TY9U-;r~D$cNC_h2;QY zQ{xVzY5d|aIr{=(=l^5xO#rMa%m48++;i_ecRBamGs8Bl!y=Juap3>H?_uEH&*#0o zhhgArzkg%R>pZ^Od6(yX_Q$^mhNXL9o~i!|^g+}QC3S7YOgRJgJQ%j5t0V$#88zzw z8cu|a2N2!C@c=U9u*X58o%8XQ(S~q50B;I5;y?q8xpbCKcv%x<9c3Skby$GL8M#?# z8r3*jV$VAJ6ReYN3CDu75pe=x_hjrQdK)fb^dYRH%YcV#}cPF<|)I{0xR;oNs&=G=S;h-XfM-tD&s*`hz*BNSL z&_IpyFxZoz3B{HsV%5wlnwd81o6J-xIsh2xip2v-(D46-r#8=k4|PoAUC)N4^m-g~^o{ zsN-*T8L%U}7YD!ZN(WUM@7{w($PNI}Hs1su)Ut^0_s54ZEWj$R+|0;#`@ zQcX&~^@=K6&HtS$+tsa8W#^x-QDt{=)a8#%IX_Mqx-{5=A*(KF_qi9t=37z9%9GI_@oc~nlH@y)6Bx;ITP=hJ>=+#igUL|m^Mx#cZo9QA7f@r@bm0e8D`;FT zp@HgV6ul<`C4BzyU*Lm;L&=w)%@&k!aRE!S&krY@5mvy`_4pU_g!>Hwt<)#TN;{|Agu&Eid6P>7n zVdv8+6vB#vhte{T7Q&2q$v;se10i}JgKLqdCJgGuUNyKES%wmK%dC2<`w>s%yIMVZ zmpWjni_cJ{KRl_*mfHh)V7s3Oq)F#*G*|GQhfTY8d31|? z!XH%o#KD2wF|#$GQyQ$vTl)cB)9#D_J9ZlKfvPWkU8Q^1NI|*uvnr(tvB>5savsOj zf0n-*oV%QBgC?1DU>F7h-XXmaYCl26 zKSErLq~g0@`dZ%Ah>Zyhq|<(dIt%`YKyA|ev8?*L?W2KurPJ!`)!z*c5i8|E&Dj;= z`W-mheW^$Lx9*Wc=z=y0lX^T1MKS0y59T5@w~2ePR}oD_n>%R2X8xaQIC0U_CNX;X z=$bqkf9>L#0+R`LZ8s(>=6ciof$ys+ zmah+(zER?YO%-o`KWlU@vUnCvGkrMh5LrN(Uuqeyq=Ue<1xUgCFqG)>Ctuhred~JNHBD;O#%>>xWx`&QhMI z=w814l5&tgz3LaLvi~C;R5|pb7gRa?@ZfDf=F9Ht?-L$+O_e8K7^EJbG_0Tcd&(Do zQsuOt1^VU@f|fYaS|&mZ$zwfC$tWfYw&Voyx0G0nw0drc%<3#|SWlsIPN=r>SL9#+ zR{!1*jS+8h`R{{pM(zF@unV2f4)opKHs7n-&E7d?&mcjiRYey`XnpscWI92a>kk(y z{dU$cspV~Yyu_B&6yl1a3z~14lnD*}CmtjpPpX@MDwJf->#RSzB&j8ZzobGsj6aQq zotje*vmh2BDtO}pY(@f8GIJq?Q{n^YLrOne-~Fp%)K2;Km0QMpMIwX>lIuVALefc;dz4Ewb}a0fzXJgF9RMI1 zBn2HN4J19sv9Cj?*}DKh0jg*f%v1oW`po1KVH^Kxm>B8R;`qX6aR~+X+f?TMy0OS* z-#Gn(XKO=rd;9(Ja+P$V(53VgG)q96jI)M`)#*T3 zB@7bFPPtyv7m|?LP1fuySyo|H2-2yYnBH(9rV)S9)U0W8R7u7K24(WA8at{St*_uk zh6Q8g#6h9)ocK<>BZrmLVjyGMmtm0x4L*9Ll8IODoQjYY(NxF?HzUNw1?Nm$x{Zy! zagxII)EQ~YZwpIb&lOufQqZ!qgmvaTt5W>?Y_SqTFCca8v=ZXxvBhpvkc}F90dWOe zUOFvLmfgVVHps9Wu3$UG1T3j(HveRrFwKpO;zQv6pTTYuR@OwbX#yS~FrSnR&Jt}P z4rzldFNb7}0_oEK#Q|zy&<<%}h$QPJ4$o+DjHCo~y!yKz?aVH*@BX{rr;8IUtZT(Y zszf*sN^1aK05E2WqTzR)yeO{ zVzHrlVp47L7O0gaD_Di$iufd@pFlL>oP-dLW2Sp}5CNrf*iS42Q`HCtL&RI;s21)Z z9Lni|kicP+@(4)^%&kvTs(Sa{ZC<}Y+nG- z={E<`bNfRdl(~!pAdiM7@-X~z0|33F0YHa=W^2}ylS-9E+sYBK#drRecib_sV*}qS z&2uZQej6fFC)HaW9<9n#3Kz4S@xS=Jx-l(ZU!h8AAG1&}X#K23w4;IdV*m5!=Sf}~ z0`Fz6zf-mMZw<6*t>&iG-)-+&sme~>TdJ~a|7of`pcQp^(>E4|>meQ;87wYJ_CTS)GhD zn1SOp;|Ye50mdfmjDjdmB`hyRccFKqG!9K{WE}+eC(nDG+voD4fxSW{U}`!4ham%d z&Cv$-k|tyYv9F{_cmY5rq$>bKg;R3d=y4pGCOf6N`8xP=j%>*L;cUXK_a8$DM-zy= zQ`;QHj;UW@JC~gNOC7A^U5pTSG(rN3dT$uWn#kuL(~U}vlL9BndY0^Z6^CdpUKEy5 ziZ52TK%lQ#Z;}vk?6K@=_;r~L2;sEkjAYrnngeZCp=2gT%5+^0d791ygs{mAPcXWO zX@BxdHoH976JX)V;v~@>YOnzHM5sZWEvx@fhAxdVS{?Z?Ynj~j)$J>xfA8hm_QHp= zK*7uplO{<6vrzoPW#{5{B%9+#hfAo8t#mIX*5Qq|U%~lg=3y&cie;>$Zn+dqM|R!L z_+tT+n(YwS+;_HmTZ3|T`y=bE)?dD#=g84u+tCm+C)?w4)k%w6gK+PGB6hqrSTq)* zm_b);&0~poYWI$d!2(h*!5PH(}Pa99wLbod7P&sMvL$~bRKe0Z(f63(y2ObI6*CM69MhuIz2 zk4;INdY3tC_{5%rnK*@_SNkdb|1HK z3W+y%E`+oCFL}?{WYMy48gqDQGNXBEbQcq6k^CHI5qlo%a0nu2KGq=%Jo&I5{34t) zWvvLX>|Hs#xs)RBmF8-L-J&&ZMB@4CwueIZI=fGtJllPy563zjDN?JjAibSHiCt> zW+*?v+E`J@V^}yw#%gUbW?;h7Zs>;ec zC#rH_mk(7r?69ESi197d-=m(}tjeQ>SbLl>{gJYXtdreQ6Zve3 z*n7EpyJLgO>n4AMwz*!#SlBHS5JDKKkKWCOG}M(n$e$JRpAC`EmJ$N;Hsm6ooh=62 zD?&v~x$yRDo}uVU-VyS^WszL^a$FD2`?=p<>gXiidmWESQzSwmx(MsI6B*Ynzo zE*vfUmD=VQ%_K7qd&>+!NSZPpFsa0g%kk`zl+GjbVJ@=mIB`ar3f(b@#N>}V7HN3z zXfcgZVj6`=aAOoS%-emuvFVi3M!8=>9zGh6ZXd z^}t+(9-AM2vMP@j6q^WdozppFM86B0O!+*>PJr^}(xN#RL8r0B0Ps+@%UR+lLKAc5 zk$5v`LTR0{_dtzk4*-z6ns}Nw^1Eyfpq3>bfX>NM7+WeC3P_76MuMdb0h$*)w|&f9 z!W(pqDUP7|i~O^3dQSp?2w~TO8Z@jV&c}|0S~jwE@##>*L2H6|Db#Q=W^$DAA*xDz%3})Er+zp{mHrdYsdA)X z+N*@WPCsU2$pDFyE{RPUvO|pNt?ryhvB#HeQ+-dc&K7ezlBSx9@j=z}U0wbdtVVQY zZ8-+RPEwF{_aofF?0rOGT#As)tJsPoFekcW9j4znl}tXCy5_ZB6FEnV>f^044S2Own2>4R;YQ-rfIY4Br^bqi#Nk5v2HBh z7wb3+5aM>io}HmsC(MFGZ|5^<)-_Vx)Y9f~WLgtq~=k`?ZpVNoZU#<85U5n|tsV{v|oF!FV+ zuo{klO-!Gut@rI(k4jAn_K*;s&y-+nj`A%-lV zb;%H)^CIY^iF)$fO;l%vZn|KA3|%%pe@$laQJX!2@Fpv3IH!n9{-vBd*rvxVLJ~o? z6jJEE?CJIS{NgWzaPX-ScPq4?8T*AQ7h0zXN8c5>$Opo9!W(#d-V0%mT%@Wr#C(#e z+lSGYN`2yq&%pYZRN(oP0+Q+%a{O4P4a>8(U2AlQT3nY zuLfg@hU9r@?=Vp;t_sQ}t}%ZxhJ7Ly&%OAWT%ymKT2` zq?P1Qt(BxaEd@4^^0X8H~(F=#*R7tH514&dNp-ZrqB94j>UPv5|f$mia((- z;tz3NSlbbXZ}Mr&$bOS08oEaKu`$Z(m7(BhoMnuH#0n1~*j0YP?Z-7%{adkJm`TTP zz*cN(7seRhejNKDX*u68-G^Z}yro`etkc*J<)fHc#z!%R@>7;bvQ-tcP}!Gp+Wh%U zneiECyy+6vOHxHX(Ylj}6AZJietSNOjYTLpe&$SsFtX2FfOQ=7X6KLRmFM%Dp!<}d z&?{{D%>8i6Yu-~&V~8G~2xy=L1+O7oD+R?+!08d8mpBM&P>d083e-rU=Qz15Qiv6b zW#u0Mol}4}6`}OHJNd7TzR<9Ye z7cZ%twP5~|#gz*d&YZtw#lo4D3zt^SojtvB*0lLE7R;?2rHV1rX7@T}<~d8}PFvJ_ z!4f6Y+j1_m=_;m;qGZZI3Qop?jrDlI=a(h2Oqj83gv}Ogk|LM-c*ht~ zX$XyfVJp50{yq2=9=Vz%$%BlnmzaTO_m32r2(#eJeioLcWfsvO2zlb{Z%Yw|<;@(c%F;)htMWV{IT|Yde@ZU8;s!al z*}>dSY4qF*Fx%*vvwMxqpWtj*+(zyC+Pd=EU$clMma1-sroE}pYnxpKW9oPoyc+x* z#)%R1uEEQ~tYLoIUifO zWXSHf$2vMH9_z3ZPl81FlrC2!2*+J-k8A3#$2uO9!w%Nr{Yz|tp;UlJfRVV*EQljJ z9ZLO>Mr=k+NF^4=Qb}0Q!0gRQ8_X0LNjFk|gi#z6cv40wT1BT4`f#tV-u%0w${VwB z6xqZ$Z^8&fHv$ zrk$m5JVgyABfT&uh>m?7r>r4Iw_zQRG<7_m(u1*8_A|n)X{_TfL7Xg zZUR7j5&+zf0idk*$^b|hGfY3a(5wG~j;Wv6c&L#;3XugdlwgnpsR=zvfe3@x3Tncu z-3$823;+Nplutjw*fBnlrGsI7Kv=yUI~H$}9pbLDa$GHt5 z{1H*24Kpbg2By*BjZZ;#1r8QH5h0D0l3+iKFWpX9hYhJpX+GX-*owIX_NdPO16xr- z6J3NbZ=mKdk|i*T@)|`Gz{Q{gMwiQMnoR-;za5N1x?{rixArJSB$hGakjon zG1B@QVx;ZPx}k&zMSeBw{^eIBM%wihBP~omPECxoBJu718zU`Tj5LK#9!l5aI^G^`yZN%8 z7y7ctlJF@}d9Hv6~6qONG7G zitl1j8~iRCO!(8if}Gpg<$*U{i;r(qb=w?%fhuKI`z|6zPCN1cW$eAhQ6Zco*3lYg zSEGbmV7lOg4j886x!be(G{pXeGi6G{Ck=FrJk~VARUsqF2?`>_g`_;#;qG5ux&4R6 za9PrDy^ugg6SDtAwu_XLWcEgHjaexdeGrz)$H)gHk+X{MPj2`*B<>{sHKyJBUaVvE}FI1ZGeC zr?4NkN@fKo3#~~4!P=6>DtB>$Y;>CmquSkx`CwrZpxs`v$Mdr#?+o9CX>6KrJ~DU!y9W^pJDhid`n%JzAZ<;ze+036 z+gTTi{H7zt!?@E?j>u?sH;>ahxDVRkA`9S=A*Dmd)8C9s%CP{*Y>21oe~#Ta6n8gD z!AYGHu~p;|+_Ti>a0sL!$vf8|kPTl?)}OWbDro9qUt|wM0ZHgVp(x=#(vdE5A|&jk ztYj2e-2VeiovcC4&sOFF=tE%b^AlcV-!gcyPA0*#P~7SZ-sO1rb4RTbmo8 zW7h9poojZ@U~wDvN0LuHdLb)F8!ZNzt|VpqH25(0JOGfI#B8V-MdT4j;kWYx7)9gg zhc)n}CgPoNCu=~aK&OP7%mkJ@7W(j?1pqmn#eW7hq8|Z3+~DlQ--f`YO-J4jIuN)t zyy%|{{mH1A{0a684Nfm1bKXMXc+ky+ZOM$pJiYWc;TV*Put)`xUb+_kVyHp$r9)32 zklhA-Peym^Nk!7>$7`!c~yCbL*^t}__>aVH=|_wg`gX0gIdnazH}?6NM-jUZlN zMuZy7B%_Z~48K2gO1HJ7qwv19e_zbDrF{F!E#ti+W@<<$#L8ea8x12O??8+VOBbJ8an7P?D=PY|$?esdNWBfSioNwO=-!AFP0 zY<)(t+?}B?F}f?Hdo67SP5kWuNHzz6`JkCCfrxabC2X?oBc9X$96rb$2LO9A0O+!r z87?ywbb#nwypC~X+PvrBYl+`%vc2%z@5~RiUm|3RH?xu9F*T$PDjjU}Di;cFP{lBn`vXhL=@ zM4*|Kf%FOy%S38AnrZ16oEQdzY9i-#fut}HrTL*rqJc_DN1Uz^^Zp;53F*0;8dbwC z9fL#Zb$)(KF&W6ddU&GfEO`|yDJjsJ;u5*2Q6plHPP31M8ioBOX@r)@Dn+>|jM=J#f*zQyhcvrT{rI~@#T{mqi8+r7n@ws2a=FNN?%F>x zxoJq`lAN_sc0Jf@7{i``xRzdUfXT>Rt{AVlD~6C(!6MGTyaL_wR4xn|2aB?A-@$_hy(-)(S6!1AA~=5Q>&n$5bVP($p}CW#LL9Jg7Klpwpu*l<=Pak5(0aFmWg zE{LrxIpB*;Uf?mF(G2XF!*Yk|nSvc!v$12g#AM^qaFG&F<5C*Ov2+0+uuh1e<($(Z zjL9jh{n%O2uEyzPU&hbUDe$nBH-GJI8buPh&XG8^5frQWaVAe-vg$XBh8sbEnaL}2 zU>U;Tqz@r7kHqTabw;lMp*BJ|=0%niPyt3Iu?m^uP+B$8)k!ih`OxG&r(6m}yEI?; z%$hAHG&U>Jc`_@~7PKrNizM>n&}OPCqS5g286b=K#Ko}0&Pf0$$*iqw{kM0REDo17 zF7qHi1fi~b9U=f-{xF;?g|7&cVwaJ#&PE@n6TKa=7)Mc${{kk+mi-)sG+YKY99(0m zKv{t~qrA?i0(;I*!B5xO24J0RDMC0VYoZ><;;-P*y-K!EBfCSHDY^i4m_t5me4L?= z4BCa6jl4=aZrDEJ?JdFvEwg4xegxjHuTo;lWa&Rud8&wdGpQ!>%~Ddf>=!WxsaDN4 zPlQOLC}GIF4(k;^D7SsYAB%FaG^YJHc7W!7oX!*DV+*2Au6FeL1$lDg71zjii`^CwTHCW}L8hg=HJ{ITY z>_yW;os4ZeVzBxp6=4oWPyNB*Ll7>kB`&Z&tmUF3<2)kwq?m{QVLsMWfeULm#%Qeq&Y74V%v{@3YP0mn2 zD=~F&R`vQ@i(DG(0t(|st41|SC&1pwn4XI^08qH|uAc`mGlOJa0rDbf)RG8fti>XY zN@-#C?;}<>x&xLbf$lA|j~Xip=)*aYFOq*RR1#h?>#qSA-E*f4)F>SV0RJuk&@mw= z$EGkmR7VZ{CNzv03Lj{T*ishBRpLUj=wn5FB(B9p82|vG0}UHP z@itk(WYKciJ+s)QDRI-6Yfj!7I4gAKBjifrKN4tA;vQ0s`N)Tm{ENJuNSNg!h9jg% zSrkd^#?R3RSPu_1w284$!@+==33SAvjU{i#IO;ygi`IO%X%yNya!M9`{0sx@{ zAMY2%MG2nP=UJR(=0<78W?3r{hvu88eT)8Id}v@V>6IxS=6lf=)*gn7eR|;1B#Bte zlqSigG`aTU0wEUCUTp!O3*Vw^h8^irCtw{GJ)|i7B(vPvN3kCk?QvRn=O>&di9>g? z05FUrS#lN5DDx^TBRgEBj=)CgpCW`6lrd5xrYOKnj8Zc3XSwTfEKZ{3{6sv78rx37 z$CT@vk|t4rSTYuN(9PLx0ZT}pw7%d83Jx#D6d}Hphz2uLMpPm3cNRSq}*@ZLPp19&+cMclCD^X(T{sI z*0D*yrn4H+;w&?*6oNVZDV$GpK%%BVKt#GaC&E0Kc9EFis#%N)!o_gBC$n#uptzYR zVuIM|Or~(IZq|?{==sv*CuX}OMkwj0@o2JHPK;h^|F_sGOo~I+JF`aAp3P)R7+q#M zf-}m#j1c}ZCcya1n0Dap$PC4?;xsxlv2>fm&~Z97yq-)Uj@KM(G5aXoEDGRA%(^-E zt2P)!!8<1&ZjGB8krhOEklLB_lV-HHpIJmRXHN9hev`UgJ_~kN2K-A^wPiFJ#wB`_ zyZ>CG5lxb6n+$2p)FGLSWiWPZ3}rPU$ncK%P(h|h{(rYJFS;|)jy%KiCp-5=RSqd> zCldLt$VKK)CUS#>bLLMnhcOpfEO*4wC?bYbWb6spc5}od5-<_}LYYD+=92w=7*?$& zk`U&wv-JF%V-6AK^V{iBq!}X;0wfNKc9te=R!J$&<&r*p5ySX&?>D1tyoF}}f9Flu zb9XTSymtUVQayVU)F7iv5yR{?rfoESu+Wia{?)JD0hLNzao@|v9Fc$D$3CX+ck=1U zs&rquRhG@#S#zw@tW$E48;_^^PI@x4BQKpBb0!N;$VEGl_$z!M*`K=ep+<5g0Ad{h zKxf{(_|e<-SFkA#1PSWN3^kF4FCZ5t2i4#dhlZkD(H`6QfF#d^VQG&uzk?bX42Pxh zn}H^@qvo_-M8-wOL#IAjli|lHq*ZexBdt)*{qFbQI8aQM5VthLsNy!oqjEf=;Y(|q zp`tSJ3D%<2Fgl5`PLhBfF*6b_kuLV5`w&q_tHUU9-Lcr7*)12%q}g2|jF^~cVU8mZ zw{bS^J#a8JIPaod(KDkH2<*V`PxH$*D$&aeWcAf)7X6l0O-Hwr{$er)7@ zFRwUj4a|nHAgQjkCi4XhAgvpT$0;QaS@HOYILS)OY`xP6c9HXzUk(dIz8MsQo1EK- zQXzhalmTP_;yN%7GM_ff{z+z$xEMMm^}akM$s1Si*}o%%KrZzEb|SsaCxW0jpQMk30n8|8&zliE|~g@Hral-z-3 zb^dgia|qJXv^|Wh6}__Nm^|59U_lDjV);kL0s~!lyz0r24$A%Ivczs&bdu=e=q& zW+z6eB(rV{nyHbrr4jTyi6t_$)A%lF;@rV1(6emjnkaY#5i1%~zxFV$|0_O%gsAC_ zq-cc13lpQ%6wGHBt8Dl_daJ!AFv_+kz$ngP05HGz#7L7Q%^X?`A)r@fC@Xb$vc}A4JFs z6KO{iAxj&kY0aAdM#$P@gshwE#|*x5jfj(_^F^pGdjxKH>hs#Np9742$D-04K}*s~ zX9A=dnCRo2zdC<{Z7VQ*D-)FduW&S``*iact%}T_qopSw!+dVNWldot5<-cJr;*rUl8F*cMd{`e`Zfp%(MSY>vYi(Qk(rCVL!?mbf_=`QiBoQc5YtTTCL#%rcxco3&4Sb*yh(JW4SPviHWT^<)niXge6|Y*5J54P!a)Y&_-B9S9)@N3EvoR&1v8MZh{? zJQ19p)ao*X@Tc03vq53BEw*c7O)qJ-JB5v;+J2R;<=MzOX)I zaz=txN*rsp7!YP*^_wu^jaeRO-aIm)Bb-Z78KY>P=C{`$uBPAuNabZ#dnzM_S)VXs z4xB-w=E8h?t1jolJGJg>ZVJUGeI=oobt%54GH*u$E2jbmS^@V^tm4>$CYH zx;~63Gen$uHz0AwM&|J+aYd2?5t4l!AslfmOvoIk2lnG+aE7{l#8#Z~k<7;%M<;tz z^cRw}_GaO~2JONjA_246b&jHskm&+(@@7ny{Rd3U!#%MvyjB)7q-hEFxW*u=f2eSV z2hbqOf3|wSk6{6l4xTtxua=hA4!9fIF)p#!VNa<;p^?HR=|iUE$myw2r;t^`XK6-=tciiXhK%EIyv_jI!~63Ov8mTGdh6Gc|56M&X2*H%xO^h z2D-$YvnY#Ku8OVZ!|;RgBu>$5qDg07*D&YARQtrc(5Eq*9R?qUF-D&o)Ck0B$Bu=&MzMO#wy4BVk)b9jEBt3+SPC>_jtxs>BZ&aaq-Rs$ zFA{jr)T7^puXRvQNAFn*!|EuWR7XdK3$n7WhV|ij(%6J*>pSqh%xxtm)mk-BE4CR= zx`vu&Y!&9h#>nc8r=*;GIz9Kpym*`Tn{|r*Ox||CDl_kQS7lkhz|N!f4{lU{x4&|j zD*IVWL{Q7ABB*6gYj#cK`|oZU-)TeS`|k)Qxy?(%teC1Kf9>)XoHRtiGPjhe(*NO^ zs%#NENR$3?>qSScUh2Nfw#2gRlY{V^iC!&K1NVZhsx1BZTdJ&5 z9f)p-b}q^AKCyzrTh9BWNz(lk?wls)0Ld2`OXccu9htYg#LnFS2aZgv$>zwcZFJ&0 z6V{xuc#h?(HJ85<9fN;&x81E^rAKyBWvd4sQf1pS`>C?)MMtZ$$0tG9)ZRk^`@sPx zzN6qndwx@uBTopzuO9bgPxbfM*mqTVlK;LcCwvryTb+DvNA>p@vG23RIXuc5BYw}e zhKs+0e;X;zW;<)N<{rHa%R@Tn*k-7qxt}7&f=+T4F51ezO3uQ^Sn!rtx5o3KJJ+?X zx578`$!6Domr|~i#MZ>^re3PI+7IZQhcjX=*Q>2ft?R8y@7DoxDt+sJ)ZeYYcBv}I zsSO`#iA$feQ-7n2uc7Wpzv)#X1Ik24dLv$Xb~5soW1Z58*mEj|br?5?(T^l1Vb8j4 zz_Iwd{HL%V-g2IE1W!|mug6n%*PRHX|CEya_{vLyz~!k&f2(%UzvAzzY*i9m>^4)z zslPiPJxG<^F1tgOJ=O)m>MN61slPSxmHDcZc`6apMAT3)#q4KrG0eD>Bk|F=GDzpN~@dj+I-7Eu1myuo*KKFRAFh zcxlz*nbVidUNFCRzuuuBs3=m<&zGzlE|43U+>z zwCCD_DqZ{8>Q1PpXZvUWEZn{%k({Z{SchIbovjXEw=h}fGC#+`vj0X%{8NOaFGmQc zLSa3J|MF9BOIOyF!f&I$31@4K?DZ*l-h2Bl8OYP^(ZNEqu26fN%vyR&88?(LU|f@P`s_2 zn3OV4@JeSUV>j_h2#LIdkkl^_;$DJ~$j1n=4@OArGlZ}r!|@oAGm?&z84ej#?mq+^ zCRvw86-90Z-a)t=K7Ce{7TTSUtx^vm#QunnB4M61p+!ss!OWguGzzTDiGmF4 z4bmGmS7HjBKe7uYUV-E=ntj}VG)T>kw!eibn?*IgQ4Si6|zU-8pnwpk%v-4E$k<%D?5~xuVl6uIw#Ma)x3Zt4Cn4 zp&0Qo-Z*@>ILcC(OxrqIvUbEFaN<~ToV}~Y{&62c=Cm7MmUrZ$J=>|H<(36cPg#!} z)ZeXRf%(XIYmqfhT6$=X+;aZz=o|Ev#Fg44K4V@sOtdTAO3>(=kN~6?k=ffQNgSQ4 zvEwik3`2QmdWch~aehmpfvh;LyJFO65uZ5A@;Gxn| z2aB?A-@$_hy(-LoR$Y^K5aPGK9^jm7&QN7`XIXaBPu1VOmGRMFYjy!rT`PvXwq%bn zYECi4sF4aK^G_sk`Bx-H%?B5w9y>D^x?w4qt`UAb>x06iNh=*yV|Vt5U!TPfRIHx&)~1KKbX3mnoE80*EpL?< zes32o_Ws`#c5dRs_3c`l~409)9{p-cfYJ5De}e5l&evfefY$pMm?cYf3@lJUPz;! znOdWsj4(ndFvxMl87crsABzHj7X?6880Qw$$X*42SlQaT4j->&D<5|(>!rM-5zUtl zMMyXSJ+>kNJyY(^Fl@w@CJafuhpjSfmUKSW$UC|a6jl(4QzpAZ36PWnV za71R+OXFCF?D?4l;^UBb4{>jpgXZ0XgJ=69Br_R%W{oVzrcQ@&SJtT@ajZ0*yf`ly zsfO4kgd<%=m%*NvRN$xcArE)MupjRp{4CuvW2^WiK8s#wK8u+d*iyvKptH!)%r#3( z_BHmg*f%e2{7L*e=N?#EKEbSecYc6F#mW$pIs%7c=g8~|Y~{X)kkSbVN&Xliv9>rC zn+?hruRz_*n28Zw>|Z!kl!nq-Lt-ml9^FS^D^@D&!pmW+(m0<*!c&B&&1x*N)shrK z!HUMp0AOZMBzNQX+hgK}LA^so#VJ2RSZke7Z?%5%wLE!NEc%4v*Ai!cTb16-v8pV6 zw7Dv!R&A#E|0&wU4Q7vn_;C2-R15&#+#1_{TFxxldQD#Fi3Sh7h2p|(nGKE1;g-z% zMc%3;Kc{ed_OWa0+FwzwF&afVWa2%TadHa)BFgKK3kt;^bp65PhIIiw-&P7Wp~(&N>CE5GV3RknXEP}6k2=WFWk?$2&fWv`9F^Vk284(jhAZs6)Uto0w&-$z}$ zT$Me=1jkqt)fA;i?>M1o9w}hsSgcC6xh(%f>1XoCnrn;f8lhuzHZw{XMTf?kmfJoC zC76wRAmJqONWxlr%$rb)!VG5V!G8*xly=T-ANx=dv6*#&e=F2aehC2cveDeh?3I`U z`%W0eBL7=jRoYN<$aA7p7|o%*QU*;*=V#-lXySH4fN<|CAnPS1cL}?pt0YJmjdJxKPiUksAO-N>h zUbWy+F$<66vomHc;Ub!&gD+YZH!-mu1}Ay4`9R8Nf9zQ3$3r7Va)Ytc(8Nq&5Q|Bj zDvvzmw27}ad!+drVRCea$Q(HRO=wcu2LP!f01%HO;2;E~0N~=SW5h_GOMVBj3D9fi ze#T}rTXWtD5;RaWI=X1)AjZy#Fj-3T#3r>jK@&6LAun-~JkZn~WK2>>`x0s*>ymU0 z%#`A@Q~Xu)8{*-ip-{>3%VAiG;Y`O{iKD9y*;5|jLA}G+`PG$YtbGtipQjZ^n+21a zfP{^ZKBsZv{4-!BDYPp|6Qw;t0m4r_4*&`a>~)43nHfzfj$U`kQ}md)AHk5sXwq$` zLquK{#NU$K*Zu^CjjjejiC_E8m|%pmD{sn=^RhESSGrdaCa{a}0ub_}Zyi@}wY~qn zJlCSgk@~yU+(5`RSQxQMQIrr%35wBK{g*t~w*dlD=nQuX ztsjeb&8`$I`?yZXh>I9qIg4On5+ea%O+yK@ITt9m@eoC&FNSd_`fHXRsSLj_(QT{* zW~=`!B$!@h-AXGe#DZ>~76r%sBZK8@B4wRpV#H z>Jh@R^mr`iHf-gPdO5iY>ttRd)7-f-W0!8PCj~&~zqnsqjeF+EWWo8?b?05Jq zl0&LSTcah_?}PIaO2<(Dz`G5d?pCPH@3~; z>hDwf1pmDu4yTmakGF=2-|d71e~ggYkF$ocBqxeQVZ z!~Xdyq;S|UH>U_YhyHXYOES4_GFVOlvBoa~huiQeR}EWA@r+4s!OI>d?S=HI{^S`k z?N-OUkY}zRdqZ6gD|K%Wy43GIL;;Rd=KAAAn>T3z63^p0d)E_$!pN~DSNd>1u>Mp0 z{Obg7{QR?51cyWAw=|W*na45+vD0`ONtwhpU>d0``|nX)ic(s(Fcb^RGZX9aBiN`j zdJ}$DjC^r;2}x+k$5H2vfJ&-VjWH2O*q;K(%kD#e%UWIu?krzpz_XbUj z)|8?(6ty5B&YdzP5a&Z?9A=@W$O66jK|;Na5@^6`(TdXoyQr}sQ6a5o4Eb?y|=_C zPd+_amF_FIs#3B&@~7pbD`b%uCh3%1WWh4pAvR!q90oUMT}#wM6Nw0q9RNTL`P?3= zG%7e1<4%gEM7N_+20%62->NE#vqFTzc>umP)eivD&iSqB0CY|QK)e(H=IF4_VbCOr zqp+iZtcyUi~U4SKIAJBdF6VJyb>nmq9*c6kpMdXjl8nQ$ScCD0-WDuTyVBA7p7`euhafL@P`0i8}{|m8Q+d zPZ0v^LvK7hFDp|+&aLFM1q$z~>w;3sAjvZK*vB{~r-hACVC66>?&No1xl6i=v)bXHfq9IWJu)yJX;5)CB&%#* z|FAlP<)ef6wX>Du&^&P>MSi$SLHhT7>nSUts>ol(e3;s;KV8-7Sa+K$yS&p`gO5#V%@2_y=t$#=e@CpPZ^noEfb5P`#JQO;0Rsev~VVGY#GI^x*%Ve#|!ixAR zKJXp}x-+jFEjDebnGQ1>tUAd5($yr8Q=-3PZkO?*=a;0xE?+r;ZDfJ-nzVwC? zBTF=sdFn*(kPwMnJ4uJ!2zy<-9W1hY2>=9#y{YxF;~4)5|0jy-qw{}aEqa_Xl{L}> zN=I4XbCRJahjWcE9btch{iG>%8YgA*U%*yzYQyJFAoKNuCJH)>9!#)^6k!0RWvr5d&H9j_&rAd7cZqc*lnW$AeRajABwQviu@CE}DJgb8mK9 zXNk-|VBX}FK}U#GIOJlKy<@|UW5teZdHNEUL|mB%Ya$mNDpsde`?Yydsv210_|7Q9 zGBS(&@nc`?WO*gC+s~>LSfccrxp@YU4eOdreoNK#R(DpVbm&28Z8=P+|$%T~6 zM>O#^_?VIclRku4kJOv6IWg3Vl4(~0agf;L`kRoOG$ST5C!(Z4!r7?)rsXg}bA&DD z4rmf{0YGnr#*q!05Km&G6p5*GGhbevCxu+-z&uFBy2#VQxcZ+6EI=#9srPYwSPL) zAhwoHFa@D-yFef5a{)l{=BN)cm&>BAS@YDuDCT#D_MA?gFC^?m4qf_*oe6;U%8Fea zV(GsZQM)aF#k;Lk|K1>w9b}ztolKi;O%q3^#o~MR4woGAXrq(Y#;}oV4W=Lv*=a6F_+JRl@vr}UF!KYd)65a=ut7{s3qslUMv%8 zR8*ce`SgmTmM>hexZ?ay zmi{5g5>&>gt=qITD3Xyd(mYCCL)Wy$uf?tq4Zi6ywezJ(vnJbzjetp!USBR^c1QaSKu z+9aM!k($tn;S8g<;pnADVV!t$gs_;;--va{eGrg~9sd=W37inTOTtWL zq}7Eb3L>4f7^3()8M#!rM}>GNbCFAL-pi+wCqq{=Hk>cCe|mMZ^I(_4_%PB_i@2hCMT0Vf{1NF0sPVc>kompnP1tkh-v~*K!?D;7f!)*Xj~M?n1&v17C2qs5j#JdQg-|0i5CHni6Doe? zjzhkr$CO0fF47K}c`ySqpQES+&a==Y{WJpJ06-%IO+x&?qbCYt$t5wi0Y>tyTx8kM zy(Lmbn`3G5G(@U6kwv7dV6ThS9Y?wdwL(W3Zi_k>VG2R@rI)Qw>viI?| zF$X&`OSx&bQi6NggYZovBwhW505jVx`t55Xw|%g;o6nTEX6Iop^c9J7poWb`>%;=P zCqomm(Tn^7Y9u}Y0O?RzP~f92ocWQbkeS}uxyWtrH=bb-=W(Cqpu9~gGmDs1QPNi` zJVKGaVt#u?`tn@pp(YhktQz*tML%7owz%!YJ*zDShEiJ$``bRKErw{-7G@o2Gznkm zpem8C4O%J_Rqxg9)E=N)WmJ>$E=dAH*J0B*cMal4m(2A!i5i<|c@##=t3wJUOc;1t zSvG`=zl2kcl1m7aS!@W$#98c7?3qcZ%N>Ys}jK}$VY7f^L_x{zsi3&gZC?QQ|UTAusj z>%##k-qA-s1sKIBv`tKJMyg-7Ri+uVfMfdmbh7Tf= znZkFwbFs-!06^AMvBgk>a(u>63qo(1@RQt8K8+X+a%ZHoz+AyGFdNG1kcdJJvcF;L zAHQZtIkIl%gPPe4BY7(UpuQW=q2_!;Ghy2a5IZfdfILh{(a)+Wrs7q46sm%R#Bk@F=3r@|m zG>Bgpq_cA;w^4YYYXZ*qGPzf`|{zMcx}n=z)xL2?3hzEz>Pw`O+5w zfLLRhQREF)Z_x8eJyF^-jd@}&GJUVv+?vSr&oRjQP-bN4OxsAJM;F1jlf#-O6dN>= z8BX3nMRW9Os6_#WDB2jAqmo^S~4@v9*12g zZYBsZ0+}IL$G;UJ$uAK?r!`Wlrdomw!5OX<*ga)*6v(_Tu*mN1OdTh`u2L&8y2bd#OZF8Ep@FB;Qo0#7tMLP+rTCaU<1T(gCvXz@sX?RL`N&*=L# z%^qJZ#w<)Xavr~gsd4y0r2rtpA=)2m5dBwDQM0YSFVr$~u9R_}Xqk_AcSMj~=DYvuukOE} z2(s6-nD{;QcbmQ=RN2w}lPbGju|ky%*^MiOU9Q9#GOe<-dF@9>QDDto!P~S9^H()^ z+Z;piwv1W`)2~SNZC{b#Z8vc6wlGHjn&53kV(|Spc-tO>x7}PnX7H8bu!|sW+662w zI2!)T+vzFxZeW5vjFFelZkOqE&f>wew_A&NDuZmGptK1eMDN08vBwaSq<~XwlI)iw zahoCuJCVDMB>i!InGX@d$zn^RSSR@$LSknkB>PAFDvhKU8%0k<2pjk40yP9d!E)kw z!OKVQ8f6b6k3X|y+z}Fr0ueoXcpV8fJmjs#Vr!8j+U)))G%o&eg**2SGk~u;{m{bYHgi+^ygc<Col{lOr}u!Nefkv^b*0I=HCxUW zODW!_l7{z5$*=MO&JUnQ$d=Q za<-P$d}rCJTj0Fc{@a0{Qm1a`sQ=z&=il8Y*Q&oK2+Bx0gAcaa3ff2o2NVMiq|H(H zO|;mxTk_l$ERZ~R1pvhX<~LCsU_L;lfy@WUb63!Z@{!N4q15*I042-H2LuQ-AEKD? ze1M8tmJcwWBlFg|X9@lVrmwu8^9NEmC?B9i3HbmO8ZIB8WDEHKI=$wjsq_>1eb6#0 z>xcXr>S4+UC|sBiaE8Ly(oCZ=h*N$OrFzT1e*GZ#(iQz8C2vvPFh@s&qU=^OkpLNuFDdEA1D3o4K4zIbWXuIHwA-yTPbpRL^p zE%zNbXviRem-`RwH*|m~2MRphU%Jc{#^#V*xBQXmKjqIH;Vyp4+FXY_{!Vn0Im-Y&EqP`ataUy?3)EgpTL93$%uh}wHzUSjU97gdksoDOgw*VL< zvhi2tsXZE^Z8QXE@lQKeJufW>ysOH^DrxI-5zsg?Q&=D*-aR4j{b|T-d7L$pUS(mf zz5P#b!UW9OIW$^QG+Ow7I{2?9I+r#=IYn;H{0`T80083601(0!c|0^ppz7VKs{XtV zDwzo~3Y|yqED7@M2{4KlI~NdBWF939g@oR(&=};DoZZJFA!5pc(VFN^+Lb%R3=+?I zyJ=Bt@^v(P`d2V~j2z{=LXFJtcp*?*Jf$UP=^fJIqKj#$4-L`{&`b1`;N_J_zI4*6M8uKcq=)IZ^7slz5M- za2wr^pbe8BQp}UT&h|zCWRV7Jn%4wIG25V&PSBqvvNR&YB6Au%(kL}WUOX9^^mdsD zKE#gi5kbOIkxqupLPV@5LZ35PXBYceR_ifkv*oDwRQ_s^$|NTRdEUK#!JFIMTc+x? z=_X}>met|hi&WX=*tV){CoBbeDyxBpu&N=yy5R5?3v}05gIj*rnDVIE{@PRfwESjz zNB2m5G3XK4SIr}}3aZ%XUPnT?H2MkHzl1)_>=&~!WLCFd$Z>KWHK%|`<>2dLQX7(w z&!ma+Ye<07vW3BEdh|oiH0CTF6vQHV6ik}3Qe-zn4f2el#iKyD=0p~;G8iTP6aXkq z1z999_7y^of*m32W%9|#-f6&2!4P=iC9#HUJKkK2&zGvU8$^mo!N0mb!nyShpQm?7< z=+1LgIZW9AR9Z(_yHf@3!Ww(Jv6%~CQ5+N!mUPVR7*PqeY^fN|f7vt(lZ#x~Ml4MC z$5-V!BR0gsZy@Q)mL9J@p!}-0RN3)+=c}^IUjnIVkPdF5Kz@Rh6UVN_-v;ElEH#*x zrq%@}=H9RCRO^XB5V~DQu#kD5RTFup7oh_Cl68*MD(8Py=G9tf0KlR(xyUoU#fL$s zjL~e0sG1$gV+4e6hlPL53tHO{^wr%K1p6*MvXlC*mREkM%JTK2R5?ZvxwC}<$VjUv z4=?bDm}R%^ee)>lhL_bdbK7fJ?r%$j|W0iLIo0h-xV5|g_V*KTT4Gn@5Ae*0YH{fyYOP!Ie2E^S|W zW3BhUuzb!P)!t_Sqg(~dZ)lQeb;6SnTZ|BPxpEK1I@yJIT9Oojms0+)eW^-Z2Q_)Jp!{SjV{yAugG>l9rzXN;oHBH}O)0aC{5h z@`zgshsC=%JmpU(l6a9cdJbig!4Wj3xB`vj6&QmCtVdBa$PLJL#JLAngzmgTQ9u6f zo>5dN8`_rbPu>RB?*$Hdt=k2m?>qD#s^EtSx?#3;q@b<3$a7t9wb}Z1UN;T2LNTsXo=f?& zi_TP-38^CCYWAD!^LVp{cef#xk$8WtD*7s`3clPdJkB83My!4E2IHQ2&a-Nn-0N$oP4Kg zj*3}1rO`^$wWNd42-Q@4b@)e=pQO^m<&&6x=F~it(!gvtShx<8c#Y@oiiQ=_-^$}g z8mw;8<8M%ytnB1BRN1=M1*$wwu#x8qj$|>uy*?t3xuQ_%FV(AV%D>ZGl^uU{ zhbp_i@uVsbee6f7lmU09TBEE=!Nf1J#_`OHWZc>1^QiUp5*S7z8MYf9+-KmBq0*nD zf8YLtMcKFS;6a0473p18U6Z#q@mm93OX`}yKOwubEW7EaYSO*Ml-mifgV}jzmclnp z40&zI>PbHmXh&4r&29y*-$E284Qz|}u&urND$!=Q5JinsKEf6M-0Al&opN!yZ=<;*?pin^~tA{OH95{iTM2N zKj4w}2d=FvU-f5BmF+)OJ>-6<9HW%NQH_-avOpaFP2)!yr^tZ*K{a?K(dK-52olfZaNhL<>5LH044pn4>!?)bMU0}nHG$ov*oS?VQf`Pm zT0);?kEo6ffg|MIk5kM{UPrO_;v&iHqs_L6Xc^~x{cx*v*IN;NG%(6bT^u}K{&4vk zSys#K!Q<8L=Yexw=WhfrbN3^jQ2)JG{X$jtf24ydhhFr8Du*BbbyXhoWsvmbgogt0 z`^gusR`5x~`l)it7k^Uaw4be2o>zZSoR3)S%)I9PJ9wVvM(cq6V~zCgqfi?4tvg! z@4ZM1v8U&Zv-vq^W2ZJ6)s$3-E{kpxcNu1nQb+LBpty&8|ym&3Y|*4uv;YEaC-7!r82J3r$d;FoHK`(Chftv_H6(NG?-o9 zX_YdKJrovwLFl~U&^?R(l4(R=<1R>htG0E#$fPURH&82X^tqvDC$)T-Na z8Xn|vXzJk!W3l8&XyRT40IO=h|H(yuEH9^xbKzf)%_Z&_*}xapyZfQt(5B z;AD*WN9=V?BzGoy%QgtI8{|WyMfG7DBDu5ZRFJ)gJ0B*R?JMO^ZO=V7Z<-A@4Z9GY z-C9knd7D7K)9Rql)ZaaY$fTc;@`!0NR;KjTa!F)}hJ4nMVh&Qy6SECsc1kXiJ578< z2%A_@b22SbAo}Y4tsC-UeA#O|t4~bccE2jK9ZphZgY#S4yH={dE3Rs%$}Tkl0p7#g zq5keEP_wix5So7x!$NEiFOXTT9v{h%mgb~Ym*G?5O{p!r6P7wfd=w`r$Xtg+CcB*2 zM1e#e6+yLohz&UCtqbx_QNv;PKJ(R*G=FuoD%wFh=ebSclxSSj}vceOnkMZaj%=#LNUoUW%1bt0^>P+M(=y*eXmyElwek zt;Vsu$q3;DU-7P3C-XZ#kYpN5cEx0~OIi-fBG(8peDX;+C9#ipzKHFj)ALBGhS#hi z`+Vt9Th(!F^|LEgDaom0#EqTGw21kUA#Idzd|b$2gO$`_6mT2)Var@pop=luCE6DE z)Zx67sJ0hZhi;{gC96$53d6CT1Sb@6|A15CNL-nVu#Uzt1*>p;G0rEV$vHwd!{3BG z+hnckT#a>{+iD^&UM8?&yYXdt$1mD5cre_uK!Z@$BT&_}jtx=APiag9KGYmPzd!FM zaS@E>vZvwH;+G>@Nk5Hsn5hk87sIAS_M40{Jaj5mhIRW)xaI@_Oy@L#M&Ry=a7OJk z07$K_Y(UCJ?O7flMn_WDBJs>V$m8`{mF(}r{1XcR;J*j}ig`{HZfr3$@izj1mM#9E z6!O0#1NO*6zAK_m$-i@v5xg-J@q|LPn@y|ZPihBSPNzVlk$}&k)C6XqIYRanxR%0l z6FqL;JPPKY+yYalc~E+`=tt0mv?ZF|geKC}=Lj?)BOeFzToP!~LFqxrXeBWg=Q7&_ z@Y`$9&Xnr*M?*uViv2@s+aH6qK1cNEP{@2nv(dJQX1$}aAC~9o9#-s!pVcswt4P6T zx8E$43sP-htLH}kDyCeJl#sBK7LD$r3zZ=c4)+P1WrmWQId9_kvkfWhw)iR1NiSi` zSXl4&#tHh5)rfBsC`3x|8Wj4L6`35wb9P>xrq)PiZlXE0Z8dR=!u|E;j!OCA6cYP>5It)xb#@)!~ZD& zG*lM`c!(n1q48B}f|s1mf!X;|#SwrH@&{HY-v$^xUP8xA3Z3+(+T@?0Vf-?@_M9Lv z_8Hc3o<&Id=^A^+jxWW1nxg~))=8C17cZ)uyI}gXxs^+1E}uJl+04p*efkZOf6SZJ zYsT!wODbnAn7?Fk<${GX=c~Ad3zt^SojtvB?mY3oc?;$*n7gEJpI%i9=FaH7V2Kjp zY?-r{_>tE@=znSPSh?f8iTFrpf9)CDg$}Lx7a!%Ll%4q}WEe#G<6jiWq$EliZH*Rf zb_lN|q4ABY|15t+`vh)R$xj0^%- zKJElZPmT}AuVK>)VL1@_bqj%S_u0Hx^wWvnB$vDxe0r0LSPB$~m;8GeBWv&z9CFez zlZ&t>3lsEV_WL7Q3f*@i{)pg`>`cd5-(!J+Ij9+_hW&fs6KJ7mf)tI!$Jef;qlmS$ zW~{XK+uLM;gpV4<$ZXir`seUnC9b%a2Y>UQc>+%7lYoZG9Q3fdM9q(RP?c?u4#eXX ztL{;McUG2#($8#oP2|({_10m|C(4>o{a4G`FkbzO`+}p&=B?gVW$Q2BSEWpQEPuM5 z9w>@>4FA6R`_M;%di@gtItT(M%K#BQg%!L^NF%j^9VX@*h$hOHs?cK7+BuXc9HSMl6X1_^-Xs z+?jK}-*Y*aVL*SMzxTb&_AGWUYp=a31sEW9egG}rqq4UgSF8)$Lvin9a+}C4r86U! zoOL9(oe6DwUsWfLowoq6osW#=tga(3CIC5sl#8?|hH#nQ5+bC)fvTzJOPve`>2 z7S1_y?$SN-zpK!-dh$0g=nTP$PJpmiJOEQM$7}=~hyZGq0K>7!XarElsOt-`NLK`$ zh=9Hb5Z9K@;g3fEbzOAWoJEQe;N5lMayjb|1wR5s~sOz&Z#~1|ASf*eOs)Je~ zz!0pX0|IC)V=)I!A?-2hI_2=5Lh1)?I=M`o%Ru{-nx&4>E~Gi7(NeRtPigbhXzf7+ z8hyrBHJit;#ASe7Q*u-sn4ZYGNN>oYh{$&B2!XqpZ$UwN3z_FYj`SaD{7a5g2u~?U zH*)yYa;jfGb?S43Vi%GLD&xAP0#g?d0pkd;;pA z$6%x%9yipm05@d$K}GWvJLF z;}vo_pYHoA(i3Kjj_7_NA%F5MlaLj2=FDCCJptKB3@^ajp04ii2}mb7M8wYb1f+9< zh>~jp0ZHT`QG*0vY7n<}9`dkx{DXKC5^*1(U{cy7SJo~LbFyDuc0ExO-kgun4rrXr zSiJ{OgfhLr z{ne^@s$a_T&vJ&Xde+?i8Y1m4)4geQqT<$@_4{v%lqR;4*AJ|pq+$_qjd7i#9K&e8 zt)B9a&{w(xtEbdfX$6V$^gcYqHw+14c04hNa~GVhS7wj&LtJPuL3oaKT$D8-hYPn= z*(K}^t2O-Lo{xp5eZSCvRo@N`ID7u0*<}kVDi@X=F=5oSBaiC8;PmeqiS-EEFutb) zokR>Vti~WUA$ZeS2rds1u}H>?1ebY;KS^6#JlPM|$!yM5kjf!xa07B*nf`EX2zczO><-I)TYTlzkwHOK4k?n}1oRJlZ%Dld-7;tgP+ z?=x=9G6UZSdo3+^+cB*P480`uY?EI1g2c?Xer`izIZYz*S(FVNIGM0dQ3kqV?gBvM zGyo90S}caQ4Je#zuVTfI5v^>wq4reZ(^d4IMvF3KS=N8k>$H7I@Z#-ZfZkrr$q;7~ zH@zmCF0U`BI{!_7roMR38u2m2tG6_h_bJ7e$=+Hkbc2#U4Q)joY2B`96*))Ys*bDP zgl(WNjh@F{WIKuZoLRIAS)K^mW)@i|?_|sQe&?a@>+hUe>uCM{FH;X|*%{*eA*BCV z{RvE3uKzo-&MJ~j{78Qn9s8+H+s?g0r=k}t+pALc$!xhjI`#6~>i}c%)Q>Fh9J#Pg z?Y6GbbtqPZ(#{*>;vm*FI1^z-6~fkWz)47G9u7JD?*YIvcW*bg#x{bSM}eI6LcHGJ zNV;=J-SeE^k+E`0CMveWZsfyRJUB9z-p9G4heMel^UeB&;4=x~jh>PpmJRXR@cU`+%*R?n87(e7n+3DiK)^!52 z?RNfP+vHSLwq!*c8=t7EDm&tL8|ev==mC@qi3eTuj_SU7bOjXCrW@VXw6xJXp?Z_5 z*bmOrdpo`5Se>@{-D;h7T=OTLRtUW_Tqqv#Pm}eZlH6gG|E7!0%qsT?O3j_1v$uQa zNu73^b%;)T{$;674}9_ho!b05xtmygdDnL6ylBUL^T_k`3?L^V@h_oZ^cVod76O1s zbYX)W@WFCT9cLwy2Lh-iFJ@AkjNhJ!A9xi zUi*u%yuj+MhYSl7w_fN1?8c=4Aj&j}tcXk*6cNgFL&xR6)5it=DRWtMXSvWEw0>$C z@~zw~-Y^gI$k>~4n7klIU&VUj&mts@bHaLFZZ43q8fMV32k1x=O9i{F?c5P2h^NCG z30c@s*W8X($DTt7TcC&In1_91#)zjEDaBF++0=$81Q0JF`_)O4gQ+57d6PGv?0OY^J+pdDR2E-*{Yb<W~>9W#ZOV6FXbgq7w^&ix~pvYxymM7P26G}b()^Hon4P9TWac%6V|Iz7L#)-z+ z+3=vD+bYC2s(>0^J?bWK0Mt$30H`1r4nR_*EDnH@vN!{A1fTM_-LkB>G zE^z=9^FlciJV1{~{ZaocKc5-A1oEogiKOjel7i z+W@fSay*P73al0R74qCU_!b9F4aKokoE(UxsjbY-SSq>$dl)zzA;kYM$We6oQY^%A zT%$vUN(8Quh=-N z#=p8Bk>B(Z@%FOe9T{HEmvl)vb!|ZI#tf#jy)6ThC;?A@22&RPVr^9$fE6IDJxeBJ zDEEl$C5uqE-b6qFP9l@a12fKs1k8PRp3OH#PDZCTmHMd#RllLq)8h@7sN8Xb52v>U z1DP7pSq$9;(;0dK00o47UI|4=N0owS#zIge@N)oIXK@B1q}1l>Qc(XyM#Wu5gRwBVpH55H~*(tPx7}2Nq>fr z@E6z?6A>I$fc^Ss=n+&zqi~CG1FOstGR$kR%c&O;QZxo3=@)TW9A+RukE^0FeEtaM zljb#FpAzA5&b@*lTC8YeLkBrIR?hcu&e8T*;o!KM3k4pB*&%axmjIej#8+CgbL;;C zFeF5`<8aZVgF=l4ZiON-e7*8?YNVZ4pjPLfb8|B&wPatn!*M)Mkrd=0qMnd4h0`H-r>)mwrS}5B zR|){{0LJk*posZ30H~)aL`|8&P(-86wx9jbW)mK|=W#vrDio*srT%ESTPk|-aG%Mrbxn)z7O*xXp&R(gk?!h<_oIlGMF9hK?7jvyQ>r504yA@P2e4xIti=i z{o?kv#3|7i>)?&Qv-vk{V9`J*9p&Uy{n{P1MW-bvpQ6)luY94?J|9>woPJJB%3vQu@ZwMUC8hFvlKJj}rCEh41NAdxZWz zu*!Q$CblPgE^-$~Z z>W}qz=bLWQY0vhHt|-=qBi=zYQu0P_<7q^<`5Z}X=hM`|XY zxOkW9V??RRabQBfz{5*!Rz+8RXZ%hm$81Gr2XfTb*hp-R@0|qcQN*@kDZVh>R2!sJ zo;66VsR_JPBdfpH5usT;B0DSa(lJtZUOt`t`#8d)MdiFWJgE3j*B)cSQWO&DyMQ>X zOeRJ8;=HqK_ssp6hwqe73)zb@Aw6P~ZQ69Pn01uYG^t;UXRP;gyEl93@8b6@_Dad8 zmcQk3LIzZdNrf^>=$iGv(}Jcjv8tA|uws$wBr<(@&x!K@nAGrSygdV(Fb_S5cnVW% zuuvfZ+_1C`S%O zvN-^m8vvjMYpv1hpEd z9irCLk;QucJF@5-zzzw&k4;(9O?w{eC2xmVFL^tJCp9n@YmPhs9kg(;y_(2vSSV!T z$O~C5?vAB}X>ZGI1})rS&y`ZjX?B1iYJLtt=obKpD~Z0nh-T^A0h*Au1E^Bke*uMV z>_t+FrEdp>F9JYh3IH^l&|bu_+sp_LyYovQ4{O9;bC)feGgAInd3yhXLg;n<cv5L*dT4xVpBRl#?En`MX`ZR` z9r)oByPvv!y^jYs|GW)zpErFW*rzFll4J_}PH0D88TL9zvP7+fFUmLfzS1vtKlcr$ znXV>j+A{MyXqq|K!!*;1sV>*-ZhwKL>FGx9t+#=O*40EnkmLA zYi`;xYRHv&^am6%Q#y0xvNJ1}F0MFZZfV(xGfpZUxnl96rKKa!SAQ3tJAeKu8{&WK zS%Ch4rej?72NcEOB-Dg4>i{S+#sN@dj02$Xi332DDl*2IV>19$m@-EW(lAhDj3Ys{QFM%8+-XZ{ww?G%@K~$dSG; zyW>xP%EJ*i&MQ*yI{P%4WKfJ!7h^@uj#nOnV&Ro_JN_h->&kdH&Lh!HA9Y)EQC)k{Eu%SDie4|)X`Aj=9M$&aqx83mN3S-QxR~dQ(wuZXu21?l?ErkwLO*qo2GsstZn+W6 zjELF!br3gdg6a|XCHy=tgsAZq?#J+G8KJnFE4Tx6z|`@%;%(RqZW=e4j!KzXR=tV0 z5f9zcgU&1H<^rVL>aEREP~@HsZA;E)hr{t~^+?pEu%s}KV>6|8B%=DFb}*lA)SBB# z##5H(lse9>xZ#J^+|EC3vV*ZygwPRNG-po5dWcqHzaH#)HbFirBL9mOb14_+8$ZKA z6ph4s*s}$jZYM}MpC$o*QVWmlD<3%r=G|8!r^Cs#Z?N+CjTJ{@njirQ+72g;Om?(=t>=0@;$ZFD_3w~?6> zEoG^ebFS9#HVgl((+;yO>s%FTUfH*(7<nkn9$cw)meS>H-LJlk zLx@I9OAj3fDm@p6b3>_t)kt?^&>DNS+!jT(YWpfMsf(}-Nmj!=YNsDYO-#{vM*h9o8g z^+Wl8Jq52O*>bxogEBSBj+M7uj&PbLG?vSj3q1~~!E`=Oq zFG-#c%z;oO_6@Fr);br3YRpsmu^{vGrfjATz_D!pIl|Afil9gd7yHJ=7xWU;l~p zsu|V$cuom}duz?o-qq?3_RSn!go6{qGY3-k_AbQ{-D&w_KZ9C}Mgky#wpQWG8MhLH zf_Srph=?~rM(5F9&=tae#PuMp0gCZU?lPL-a)X53Y4($Sp@Zi60MLp+_8qA>QxERsgNwIRa23=<{sz4JD~#|R599J@(D;FNor}T; z^o67fWVlV0Qun?2=lo0|TQw4Z&=Q$SkS>jQSO~vzV@3OF;99v zLIM*J!i*8~9L$rr3?ZQ@?1eP>Ys_UJ#t^0<%tm1bs91`fezF}58;%xD!Fqxn5Mq9W z5H@6W<4IwuXm^AVu3@yDIhD5}VKwiAO0t1S7npVOV3F(%If#94%$9TAV*FvYmukH^ zq@j!?)z5tCW|b)ONYUp!D_c-FPj4x@sZ@gPhk@;~N>r5BJkN8H=$1nTL@$xZlLEBS zsg0Zfl?DR z(&2OfLSkO$04RdP0Z?SE1E6#`9RQ`n=>Uj>N%2l`227eaeV4$eTUaz{99xQBD&#ql zHc{{+sDnrNv7P@CQ+ zyYf`uIRWWNnilWXpvfmHnIzJwQP6#2G`q8V|*mV8Ws^)jlcau+Bf8liC*!+b72y~a!wuloE()Du&wc7L=p4U zQ*F(C&=Lu&%t?@=fKcV02ZMC~15=p$t>(*HVh= zm;pJ+_e>$wzqU*a&ao+I<=CHzK9&%=XynIw8?|}pN}Z0EO*By$eY0e*(k}9Ehfa~E zRp>vEgIL*`Nm(c@FjU!gP&a&XKR9MDgD8#;FhxX2h%0VK9nEnQ$c4M9ts7$`a$EHw ze}o1(@+VWK;SXyMc^S$jl`)Zh@3;w7Eo&`%E^f}U)^g+_W}iudPVQv<7$jE-ZA%xS z)0WYmz47o=%#&P?kiaVBdAo@tu@rmdHV09j&kz#mNsx)ydiW{KVo*`3#41p|om z1wi=ntbf|y)hw6yvthcNm%vzs73a{LZ$%z0diQvZ=~T4Q(F)V?)JBjFAy-&|ZqNy0 zfu+T9zvGyOssO-zReth95f=soPLCvDj=f9`@4r9Lw zwP|}P3G%JeVM% zKkQ3#FhU{|5W<1G zM2{4<1AEI19ff&fr>-?0hH{aAU@jN3e+1@XZg=Vcs928E*JUW!)5oa*ZJn(DO^!3} z_DH~v6?B+n0~Y!$Ri=8wq&IKS_GA%2tR40ywU!_$grxt25RM?h0*KTRSSs`=Z&=c3 z@^)L7=m+bV91H*=4oqZVkeaBaM&P1wO!Ox;@BLV({&=(SSW6`VgH%k}WDi<&m)j2t zdXFfGLKG5zds7?Uv*OFt?M&g*#e3XDs4COLRCgGpIS2q0CxW~qM|VJx;KMckAf;1y*8psLB}lNRWIURGl2mXC;Bm7J6TiC6!T8dta7rv$Jdb*KDT_ zhqfyY-9B)im&DT-o~~MIMh)bw{~FP}+#@Aq8onZ~mMnucV~vF1!>aE<9Tc$oM5eMP z%bx6yPz-v?k6z3{yM@G$!PQV{60LP(+aL!$aFXkJoPzaNi4y=pbiya0ij+O@cs z)FEAcH!X=9j|vYYjn3sUvzrr2df$o^Q z01!PN0GZ_gQ1pcP!EJfP-G#+^m=-Kh-0dFgL9tOBa%bft#{p0-avXrn(^a>)`bda9 zRc~qLM16C4j)^sq3Bu$g>i|Gbjxt*zN5}vG1tRh0tV9ucMG=`af$!u^T2fEFC0C?4 zNYo2?Kh_)W5HgOaXxME#xRd zt%NURIf7>POh)2A;c`)b6l)xAP_?L1;#yh}=wj z?e)N=quHG|{X^koiQwkfyfXDC=c(Vcyv-VYgm*SmI^e_`^90iM-|yL#D?j~A*AY4L zES;LawG7Hyc5S7<+wQtor^T_IIxT5ER;SA3tyoO928tz!M2T~4solu_Hg&Ja!Si&< zTn}?@iaUrC+%lt>F&e2^1%67{2>CTJo%Io_bxddEXg-v6=DZ1nOvrt->0qUxStR|5?`FmO=Iixhs!0 zCe`^zih2|*Q;mP5Fl{?jkG6F(=W8I^&sv$p(9?A7Ez(xnS5`#VnDvj8vkNuK0=2(< zMj_SRWohGC)lD&urXA=j&F(zpWLOHGc)Wf>V!-YY6-2hENTrGq+~~zHw@Eq}qhx8# zPKVmIxPOLaJRfgUSaDk+i&g$iWA{ zEQtHkt8H5=e6yve6usyTjp`rRkd0fsWn;#*B5b30(ai=9dYhf!T9t%``hedmys|wD%Z0 zK(lM@039CN4$%6}c7XQ&<^af5>gZ|cC;&)om1-qkwU;uLPMaN|C8l}oqev#ad@ZTV{lXqQ=bfcFxM+L-;8*pM*OdAwjBTma_W(BJa^6PfLcvm9tYIk z_LI%#@K~nqXZv{Gn%Pmb=J&a8Id6Gab1?iYRpMu^u{{79oT`knUSG)x)~jo=alTq* ze$FKGY;@zpla$etqNQ!bX#G?1c;i?hqRnUHd00Q{co*Oow)N8=7r^?_C)<_vqYk!{ zOM|>db+8>dLSdyHMshPDJshtF7gJlv;jIuyjOyTIsDpH$s9BO3d+X2)06X;<0SoMvJU|9z|)Iu08jzvgch23O3P%CJTeH{Wi_>LVC+lKY9DX&SILLAyG zMUK9L?@P^j1ml4%I3AAFTad!oR_r|o_6oj-ZD}&mJ_(}1ev8G4bIh@~_Jg+y2p{Xu zU?s`o#XEY05-_erm&?ZN>i_-|>LvtPfYjomwg3GWRI{5C(4csk&+4b369DazA)(j@!$M+ngr3k;+)J(47g|qk~t<{ zM^mudcs9v?KX00ipPDmciLdQ1k7vV3{n2vm z)^gV4`g^KyQ!B-n8GBOX%S(rlgeK{15)npT{&YwzJ4OyJgi3?s06;}3MjuU30gHo5 z14NX18zIFHgPtZH000?>khFvFwzr6YyxoMb+1Q+V;eYtC+Z(xYMi)jq-hOA4pjti4 ziKg>r4R{MB=-d&}uA1-g)yqn%kg-V>&R9}$e(8WU*>*dBux)axDqFImjqNd3Rh8|w zq79*9GMXX;>nni;+$%K-h4}ZI^?{H+?U!9?d(MV-jnIc&#R65o%(U}#dZza0IG*={ zx0(m(TacQE_rr}ID&$v$mWfnI$sEpQFUm8dIrdC*h~AWm(DG+sXL*yH-u?=_FJ*6i zE_RR3?-qQz1yNTql0eE^-$r|+mQm-w!k{Bd4y9svF_T6*k(5F*$U$sI^^EJNn=y=maYPoK&eq1^8depe3((yT*T0?S7JQlV?$LGjyN)tdfGM_Z!EksQy=b zs5i3#0OZ9e{2b(nh{(%9ka_P6MWJ1S6dE50(r%-e2Sf>wE~yJ1$^U33MW@Ij7#n%( zOMm9Rzi1N3eGHLTMLnUh$m6cMgf4zp%~zo)Y3(QHLk{Yo_M6`vs5^!#w4_i**S)t2 zZ4>-xCI^4(PXEc|CcPMz!G&U0gx4RL?lbY>L3PFGL9XPacA{UDh;M(7u&7HPpd5|E zj~egknKk}(*9!j38xo<;zm5@j>UwC2yns;Ck=0Oy*gJ)IS%Er)B`19^o%5#s6<6<= zJ2ClsO~AF7WC7atvk1DQDXM%spA{3ye%8TclpDk=x24pJW(R4o)A?r4BLQdvrMJ=I@qJsgT;%WTsjAlK9g5DgMgvQ<^ zZZIE4S{h;q$F72|kX$19U_ITZw80zoj059d*VlNo=LI@VX$@3ER9gos#}$k$$Ymn& zq7#QgUr2Bp9)URy0zh6qUlVpy1h;7N!EB)XrmaEJ6=IJc^~>=DdafbO~%-VuK}d zbEj|1?%diAis|rs%B@U;zN*oSu6>t9m8ljF!?sc*sg$@$kz7tPU>=j_uCl@UcRbY@ z*dkS`mGJ91R)17C)Gx>49cfH3db_$m_TG7M1Ewdi4FJXp0K|O=Fr@QMc)H8L_Q}Rm z__xV>j}b8`%*1d@k(r0%R6UIE)KjKX=X%WdbcB!_1Gbt)*^xK-?psRzSFZAKa+F2bQIEI^?$7bvk@o2b~`Ng=L|4@_#*l33f(Phq?+)2}c{@#GdZo>;K26|&ZBVHtU~ z=xy=F+J=Ve9XQ!oBKuB)DylKMqC1~ShVR5wCx?;rgrg!U8oSQ#=1~hs}&ON2cbywWfQo(wm^gWmbT? zwx@ldzdIgk1*B8nTPKUw=0FuRTM$x*2RGZcR%ote4T!NdTisluYw0Rwm0fQcV`_?b zK~Fcr9OhXtli{ZtZ7lN`=Lw1R9kj6&BZNFQbC!)|3q5;Ye*#}!`5m#bypC-whZUfi z?_y)=s=Bgf&G$BzdG?ECV|mk#@k6dumX<7n$~;}|M}vS+xYj?3&XSyN2c8w4OkmVF zVMMg8eb&a42@belZ_L;i)?2XX*iZF$15f4*s3$xEdD?+)EX?sUfo^%1a8Dq>@+=kA?68OimfN7A|!SgmdbR*Jjs_4 z60E^e(br&UT%u_xmjI4!kfM-Lv8$-52eA}~tcp&z)XCK@g(k?jE72NroCbivJpc%9 zY#t7xH}}t1BGCtaS|%5Z8p@uj+Ku?#Ekd6S-uOFhacUuW#rjac*e$=-X}Z&KI&JyY zM>=hHl*K73Ir$X*-R+exblT?w>xwSh(p!HIm&K&YMdgV^r3^X)VaCKzf9vGDJ3GB)cSvIe_7PDA-Eqf%-FAz*6%ZkXN?|j<|lV{r*7aVGH z7ZQB*ZjBS-7h4`nG9#4Ly_1!!CMG zry~xvghEGt(@TGkd-zW}opj+!olYMy#R_r3RiN=RGK_<(ucY*$&%eNyf!%LzkT47AX0 z+dFQsX5`A@I_>=IojPr}(vDlJzXux2^imY7SEr61GO^Bo`2pMk8ZPnKV?$u`icj5L z3;uWax53IUsbcRST=Ac(Ez*oggoPjxCWBO)k(Gb9iCAXHvNWRxFP{Ypau$og98(~Z z@Mz6;Ybs{mje_sS^JuA>pm%7ZW-Ez!d?hl`UEgVK3$xI$+oxMZQHyhr(Yv?!nv--| zvf&n;_7%cxy!_v2%|Yoge}Ro3lk=oyer*}j7frCdp|!rD&Z?_*z1Z;T=k>Q@63>?Z zD;C7%?htMdsEXK!ux=(&=ERh7iQ%Vqph9tB-9hyyFnWpp@5q`7I!%V&)oG?^fKFR> zv@mzuuiEKvg>{Q%dK4Gj#W+|6mdQ)H@%Buil@$3J3w?m&o+{@0YIBnCoLGsZ_Nx8k z*aZsV(OCOc01!2~uV25C_AQ+&G7+QY2u#U-{gyZ%aAn&X=LJdpivVLq&DU@K9QsE} z6Qr|E)>{-sy$Kph#8PL2FLb*`Mm40jOxil}joAYpx=gmzUU11B4`hBsXFi4rC1(k# zGa(0Q0?How{Aa?X$-AAhdnN~ZnTJE zB=^-SWV3q5t#HxLfqg}&fG(Vn2}J}?v53xFvyhBk=ygwO%_LByFcj$%p>uTeXKog( z;EGaxCRr^sB`5_d(HoWOA*`OBnfh&K7~l3oc$|LlP8EV{qEM0ZjB;YpH1mB+45gHEjsOK zown`yhE6;DVTw*mdbZbT*KJ zS3j)NZns+O;og^8>|r(j*|L^4jzW#hJ~AtEg5N!5R+Krbt_xi71DfpcO7#Zld*P4l zZ-6&8=cLINKlL`yB4f}24#fumf4#f{b5Q*mx1`n;+?O}+Mxlk2Kd=|&5RJJBd* zGCl6&xr+_K&PS)?B+$$m6|yahm;Bm>c!D>*ss|9if4)vz9y&>AT$6LiO^zOw@9z-uki%T~xWnS3kw%%thY}93_S+-4{%^O&22U)T&Q(v6geL)@hrC zf7WUH3%csGbgd3D|<--)L8=9Du}@p!Z1z`IHn~ zl}I%cd5Gi|Kq-T%X>yz!wm_#7YCZ^M0xM;2}QRGyc zQ=kYtMDixINr{leJ4|`6f_BM5pZH3N7VaC0Ea=IMJbWoMmbw}MG%sY|m_e>q0{KJ@ z_C-$Ybvz>z(o&=cJOz{T-ZM?$NP9OM?gkUZ@g;u?Intj2fH)k&A83+ULK9xP6ncbQ zMZ|sy-3@gh(nyJOR)irR@!QCzgI>CkGN{8OoAt*b2U#$rzl0otmjICP1AtUsGj~G{ z;xCiI8Yz@Ul2Atww~D;lcdZCPF7GKIBu!{MeP7mp#aOvUJpxOLpJ0jXdIbkQ32m>( zBvQv7iMs2=ev)DigMbuePu{W z;BAwW<=)8kl%kio+#wcUzio2otp8dS#8pw0Ij&~oR?WS+nX-kv#dH4_mQ3J6cZMig}0`&Zs8!A`Zt~jn$Yv5E8loW`!#*>WPKY|8d_? zfkbuSL#Qu<0x@Pj1TVn`Vpr9YbtZWPV5w@Dwo3BPSaoUzLd2#L`pg|Nd*bAhIF`GU z{E#Ys7$Kp<2tv-*qeHOq&V9RQZybq6T%l$(GuQ)+_C-6Ys8zqnKZ;~Ic zwJ7=e(5bDrp0B@^2yn5mdj}dz2sa4HJp6g+SmM^&;V%FzK;%U7JVfCUIz|vZ_6-NHwJ7oZ-R>qVrC+5gR&Jx~q5Kh!ju$D!`LbSO zPoFwy<8BbP((oQ$BMa-Mq1(jZxH5AciXXH+(sE*goZtG)=Ogr=A-s$wKBXY%S4mEzNY4)KoGj zVL4^fp?kE~EIRIGB+RWLB<|yW$LJa3q6E``aU9oq>Im3@>HBK@H@+w{?qqI?E3q42 zqD2gCSDdT;#w(N^z1OqO3{T?87L9~C3AY45=2id%{sjPU#KH>JEuao!*kn$E9Og6t zh_9JC7Yx ze2q0U9~%~i%5Rr&H=Llt*l(T{7s96sti{IT2SUXzoW~_NU98fSxeN2qOPhIZ?E4|G zh5Gj;R$RzN%{=73pNMEI;C;FQCMb#mt;lhZgNWB8Dw!vqfg;|BJ=GnG5O0+RM^P0> zj2rzmR2o_cfMz3+V;H$A^eHq&&jjL>x%k_LI_PClKoG^J2&0QK#Lm~1;JdeW73B%e7pvyr0XHGv`Cfod2MHau&UNyiS!axpH7PtHysiTe;C{Q-q?-70?Nq zoKTCb|Mc@@oOumH<=@2?#O!pa?S1Z_VJYH8UbYnRhNwQuDRoNka~7G&>nz88MXwj@ zQH|GXh$Ez8o;KnEvT7l*P7si}%>C%6HdpuS3KJsFlzv?RmU#>jj#-3ADLoqV2=xs0 zM9Bqxawz8E*r#dLR-UUY@dTEa^$VP;lq{OjPFZT?h?4xmOP+$Ai5*1Kw6#VmlkN@b z{-_bvtFQhbtEJ^H4%h$I?kX#4SjRUkyM?a*eow=D&;OB5`yW%PQ{|S$vfouq8FA0T zudm=nUhK%Puz%qq97E=Q%%f=w$1^LCk5;(}c7QKg9TP1VN8u<5M#CT0#!iP^!KDD; zyEDWLy3=tqF?yG0ZpMlWBz$Z9FZPk5d7M+9#Ve3p&Vvupd>6DP-^X5kkhKrkBxzwbE^beSVMiqB1k)56mq_y>yMN_lX8hT&OxXM0L1kV;0WmUk_YmW zo>#}(5E8$cj4L^42@(lLrA$tX z@rT*>`Gm*x?V@m;`k61?tP*j(KTHG_XJ`FuTzsy73d12))>uo(;Z2Vg^1fKR?gOYd zh@u~F5u*20lbD%Qi(UL!xT7pG*Ctn4^3u3<4UEd$m4SD(AX0M=pp2SgIFX=$oODst z=-{!?b!AlTIHby`+5wOOS8Wi-uX+97DW|L7MYpTRT3%PH*7>TY-{pvbYq?#u{7$LU zv%6n#&o0vDR(VIc&0RTvx z>EPIZZaZ1dQdQ^1d`5@$RMx1>?iwk_(ovzJ-W&j>u;T#4M*u((9*!KDEdWSQ0)X-q z=qRE*IXM8qrBxd`t$xQEwKxoSV1_`?Y9H#Hh90Q(Jqr0^bC6<;wL{V{*a7pXSO1;L zf98I3AtLSFN(2TQ62^&U=gz0d2P6~HnJaJ-_@SNIg43cF(Xk4w`kFcj3;Exmrar+I!W zk@*orMt=`YiBrf;ZC3nLHwFJhj#ru5#j_;#Upu$_Kv3x(nON2MXNj_2@rJcF9km8y zr+PmRIct-a_y1fIQSGi7qth|kSVYB{7@PIax|6UfNx&ocdJH6B+_Ye^mg(iZ}MvY3X}E)v0pzseXN5dr_wY`dZ^qekx|k|LK4T z%D`Hz(r43Xe3`nP&*Isj7qfW`j41XX0K(4$z~yBlLslU^e7c+R$M+d?wOsi6P%#}k z+U$Ybxg{PrNeRglSn5S+I&zeHdI=SUa1=)Pky{j>2t{H@eJE9u3y+YF@3qo~Tr5s? zlz9T_T`|sPvX2_u>CPSXqO_|0wNBgKYOPVH63dvd>-B>*pi=gf@&qte{x@7%VjtEzlwSO;6j{wb)xS z2XWct^^4!8Y4R&x2$zw#?_FA;#Etd-@A6JAw`QWW)kN-zxfEt4y#N3%^Je+FtRnR~ zaurAlkrIpi-Fs;Rkh*5*^Q?cvYqF7=Q@rEf@Fulf00J$C`5=orU$?dIBG%9YcwGk2 zv(0>)y=KVr1!BbSwR{_8`df}))g`Ka9fm%x)6U0D)9KN2J19^5qvU^MYWy4IMq?CG za^WD`b((7;aZ?*zMNzwZblPV9IGwh?(eiJ+VTD|-N%u1rd$2mX1PYs{3RZSt#@N9KDMShy+=Zps%UAT`wNX5Ok>ju&3%pqG z$~-mee{Oj59+*1+b4SsE;WuZ|0PHZ!o1(}8^bzhYWKN^Yv|0HR18n;pW%5d5uYRo$ zo1oJUzkXMzT2?R8(_&++y!lFh-py7=&Q3Yjc8$)Ptn}-&=#FJNZF8k%06kkg#wjmp z5^9e%FnMMoeQ{<)83Hy}fE4B;H$fI(#(nqF4H5A}$xsOZBL4<}uUplIHY;v)e}9mT zrHiHA+DF;58C?VE`OqxA97E`1fClH{!AvA@qh!l}i*CTge15ds5s_Z)J-J8~2k0ug z*lS+nThxQ5sRkd^1A0xw)E(A$b^cqr%T4H;UD1Q{-|o(icmGFR%0O z{Ikpe&q8vmEVvMyys(dnD_cB96X3k8@9-e-mWn`|h>{!R|0oqOlRW`s0? zRc27?ms5yo^y@xCkga+zFQ>)vWq2WC&*ESrXCcHq73a(CI;B!Ou~Y~((tX`z+VT?T zYE#k}LN~kb;mnt?>*(1)_|Z??j*p-Y!rqZpkb`0_6ARVlpyqoFgmzSqI|~|MTl&B= zIMb20ppaPY(ZnU+#$u@>5W*qOBr&}q-Z@6c(#X_hJ9z;er!uS^b; zGUc0SsQ*#{zLwYM?mI85oaJIp&C#@85Za@vs<(ZQn6AFwVpOV{#RE>Bzyl6nzypTG zW~|78;M}yJuwurXVdNSK?PGC`lq1xA;%QPn*lFu}2bhtKD_fu$pT4|0#OCh#&N+GP=wGJ@o`*osM#s4Io?LV=K?7y?5lo7SI^gJ+o*NF6i@l9{F|&$ocCM9d{yn218+Fg z4WU*n(qm2j@*g@a`s@U24z9O?<#bqX?W0aVx?jV)emzO2y~aPI(;vuAQV+tBf=Vs_ zvMzU(H~*TPBe6CWGcU6{pH&XQI(0wDb`WxeRc>)}4#8UPu-sm^8(X70uHLW{jRn$u zW!1cZ!`a9={TcQ=|x3UX=Df~qqx@RaH4myI-QINxj zV!7zDdOoR>fIxOq<{9wCyj4=(JNGOGMV?T#If|{`rT<-dQ5t8rh*Tf`j`H zcN58hW^RiWZFGI@s;qiCoo(?vyB_^d4ez<`NuBn&(bVaHdw-@=bryzO-i{S9qyK9- z;T}QMI>r<8z0K0<#0uD#;V)_v=K(D8D;bL8iXr_~9msf(UamF%SARi$HB0fik~mkb z5999d)UGm1YiT0$b$mB3tyh1xPb_!%6u3<@tT21OO1xT4J0r9m*UO~%6Nz&%Pmlse zN5~0Fqzjfx-{+2eN%}=hIZ^{B2}=p~y$#txZ2Q7a^#VD%Q%&7d4ax}}gw znZS@#G<-tkeiFZ50P~R_iFiBDB@bKV^Of{mg!bF~{U%xk%An zjQ+;KS$}^~uWuqgsPp%KNzn8aAKR#`+&eO#XBowoR6j+_t6#Ub`{}gLNtT)2fOAgL z-y=BIYK_1Di-h!OYtj1fV*(}3MeP-u>?2`B8_l+IuZvvdlf;F$>VD;(;W>U+`-^|8 z;kog2U$R|)D>wQ>^u=O5x(B)m4`82odo_1?TxECs^GfM`Grk<_aR<9GKFc_t(P1*j z&XP_H{!a_rjg1_P)C+&qQHQ6Gcp;JPJ)DET!)9lOBeB=}mkS@kj)yi>-k>Ar_s$TBy-8YNO{sr$XeX zE;Ugu8V9Ko>|f)5PFX8}r(U5+o2mpR*Wup!l;%qB%legVB7=_G4LpwR20L)OCMl&+w6J^%s^+BX zZtZY`-csAGExLJ;Oy4D{>C&|+>Pb7tTGEZY3^Ki_QS_0kCBAEUo@3(OP70sf4 zxrj(P_Ep=Dtk}aHfBz%>?_JDKb=u=ZOMcw@^tSqYxS*iP+I3S-k{%c)N`%V&dQfLj ze{yWb$iee8ET?u;wAwNjPy*)VQjv?Y4ePX}NAujXkRkOqoDB-)<>K@Q=hRJqj`Ad^ zrJFGin+wz0ur!6W<9RYt6R~m2m6ceG$x-b;W4n!0nbQT2tb1RtLNZ}3ZPBsT(zZJ3 zFb!{a`(~YX{II)DOaE)B5_EqhslWRkYnd-9^#F@Qss3xUa4kgxz0SX7^A4leYmeCW z7_vxnQD0k4{U2Rn+tWVKY4JU?tp@-7e>&~D)4B@PHKzpnRv>R_DC^(iVruhPb>xq* z;Z5>RmA+5r1)hOSKDY#WVzqv%J9Bd(b+a~o`gUAI^As2e`}b$habXJQ<8}zJDc<7j z36z3Lur8aMi#=H;Wf!^MToTk1Aw&peI@Ukz_K2{SiE=*ZhVeyHKaVxx6W{WY(K^jp9=Ctl zQmZKa*Vz_E5EP?A>(vKvrj_ZFc(Wf znU7|aWYq@?~U1`|X%By5_+$;gwp3 z!*CmFElmicYd2jNtUI(6A*moj0>ro_#fVr4DT&M_SWW08grrVGNajZO2U=t`tfVo! zZr7m!S(GY;5t$VYagfiQo+naH$rGVC)@lkQrAC%>2 z4&IOB#crrtn;JHi5E|)qXHJGuL@&;>$=R28-L5uwL+J$OVxjP>P(PY9qkPLE9CAe8YA1uaS|zT`u@GJYy8jtm(H`eqmr3EdFfe3 zMw52xPfoWR75CfR|FF)};go?o?RL=-I_>@W8#?Vj^jMt^KGtGd4C`yXY)6kM0+s((oB)mgv+n2q-p2YHR9B&0n>Ao7cZ?dE-WY#f!u~s0&&W zTs8I$mUt%XfA-_%-JntI}VqO>&PO+}P;+GLP59@1pa|vyJmToL{!myTj{G z@W*q$BhD|cW9OIUO>=&^=HtwqMe`ReDP4T-(lblXSWS>7@10+IIKLb}eq2_7oC=lu#BF%KN!;AN|rWUBxs{^dX_@` zs0C?MCvISm*DE-w>7MHL0OhF&RpAgaS0JQ-(B2%EK0(H{(NQ)27skl+?2&ASH-`B< zb})EhgNw*Du+(HhEddP0DpYz=z!dUDO6mhdtVOSZ9+1(ZFlIKjp03@_t|1prh4}o) ze!b}{IzrxtcrFY&=qjl-xjFqf;1@rZ*=t7K*}4OMapex9<-(_JKGAbhfY^Ok=rfR7 z*F&eXwf%HoUKopix7P}(d}tEuV3O!GSP$R9@j9CnqXB`g?XUZdL0xmI_v%fiex3ht zflhne-cF|ncKwx3%jN8=UqcXBq58yQlps}MFY+u9n}EV7i@5#kI}z2A3-=Vd)Q_N88f{=w_*(RT0YxIn873FK+wr%ZUV8boCS7H zJF4V=&e3&s{rle<>KrdPI#*k(e-5_o47>|cf3)-o6i%1dyR`0<;LYPph`dE7Uxuj< z)&PJk95SyoBHgmB{cM*NDn8^0)RuQz!)*JX@p_oWH$0)!(hrB~w8wqN>9qG_*X#7a z-&@n6yr3?TqcTe_tJ%gFndq^`*c$&lKOF#yMi@m|RU!rB3gDSho_gOA7cfp<(#HUZ zpzDap7RZr!Mj^qyrXcuI_NL5)pD9h}C5yBExu4S1g>Hualg}ce=KOC+E1*@7_0J1X z-7aLGnww^%`Q*^IyZ&vP@=XK!1cFbIh3Z#N__?`8CQKE4vjImtHboGP4KuI998&xq z6XmWHX^4cmL`;#ECz+>_pYkATOMBz|oVb^t6ygXfJrD-kL`P7v37S>p_P#;Lug?FT z&`$eA>ZbPau%mx$!+Z@EvVXZl@3Qp6<8<08WGNLXX_NYOT56Hw-F|Yk{_f-dt4{k* zwHO|Q27Io+XUY|z@ZNI4d|d^-D=mOBM(3d&wX`DSomEGM-f!Y+Mq8}LYWT5jGjA0QpJr_sXJFoi+auHceF0RRuwhKLn z1TKj~V-bCk>`QB4>p9*p^!RRAaAw7& z`n={GmbB?urN475xGr~D?s|HBVp+s1qH(M^Z5yc{;zG31m8p!kXvdIwMe5h?o0#;I zNb7=V?O>(2UtKC|h??fqwahSBd-65rVkrbgjM=r1y`}uLb@^dSo4cdnqj&3hi(h=Q zPBSB{$4~2>mia*OZ!H$5&F@!M$_N_1=s6p0UuZR|arNtU*b_QcPUF-sXN@pLP_Oc; zJ;dlBl(lvU#dwX-0g2EvPm5pKWiquzM?B&}$0z#r(ygY3F43u?`-Q;emT30}R;k^; z*CZybFl*(K_%^j!HNDqf+v>M3^`Hro@%#jfh?ILHi^^PG+q)b#W#(x_rlcA#lr{C$ ztr%WEndh+TFsr|kBU3llIo2Fni8ZH2!?<`kgXC651zgE95&7!4B1J{tgRUh>ukB6P&HYtu=x0 zPi0U|WWO?EqPV5L40o0l`$MN=*NVt{y4eHsBuE!9Q(HfvZ_z1W@WEPI9-Jxz_b8HdCrpA!NP2T!!$@7Hj)Y^7xujkM=SjL0A<7q*y>bwGJo11|O*=5| zbv@XdSy`W3f3K&MKFP5qQ@qI#eyA9sC?5wdT)FPLuQ0wNzc z$G215J@xCb>vuXW9es#SCu{zn<^sC0DPpKA7Cs8*B~S?f^29*WOd<=31)KkxHA|s6 z;!Ajkdsfz)zbc_CHt{M{P5w!!ang$;t=|YTMaW|q-5K6&?lcN&%&Vd6dWc*0=S^GV zpZPo zXRQ$L)-hCPMzId$=QE4O=fOY7wxww>DypwK6Gl-q9srR)0D!9TC9_|hff7U(AZ3c7 zqSR?w|5ew>gf-DDw8npxi1?hUxeXLY;okWLd4Ui?AiH5GUsbiAJT!(-fN6Gw8I+Jz zS0UG*R2Ar`H;;2bb(yH3GmSek#}QoY=Jh^+G6Uilho-pidajLh@1cb+M@h$ zMb!k7G!S!%xYgupBDfU#LCuoNi+3Q-DB71%KN_sxlvY$D;>q2?^@zgA$zq54ijCvl zT;NeaL7GNlw373p%x0JZzK3x<`ES{MnXF&b6>10bh%sg4{W4$P9z?y z_y3+c+!E5aJ;k!6?r`RrdPA2yV7V2s9EpsVXSxvVb^b-WXhYN6E%OBS?GXSN6>2V( zXZA*X@hUI$miBrIe|a`gMEd3!SQ6p?(96Wy1YbemVO}s=cnb_L~Ab zQyk|IzEI>+lx{bp)PT8j#55&tHEps&&bj-w+(f>6aY$yt^YNMza@VownZW>{k$5xW z>3tXN4wVYB+;NQx0{boEw5-24Oy`7dVp=zHc}hW0Qb^gmpmcqz_fDIXLN$@2Q5sC} zO;`ugHlW8lsS0QCip@_}m_4dH<6DG0O1jaoY;(+Z$!p6M9KP`*}0($+)-(8V|DP)P+#D4Y$0)bZ4hba#5&AnPC$s8 zJO&!$T@qZK^*^#gFya1_k@z2B&JAzYD2>l;-I0@9$hrqBapou}sf$A)EMrnMgy;mB z%A6njMsmr6FH|Gbfs6@P|0KM1*q}sWvq^Qbn)^(MNnYo|E9)86c{wREb1L9%;}Z;> zmFlp!l1mx+G|pSjgfw~G?V4oExnZ}=I7cu)jjo#j5}l-K;vx&R;-|2Nn76}MV>Vsq zq+psH3n!8cxf7_S8QTU$%#Q#-VXc#hWRl3#B)ZllzFrBm;^H~jbLDA5T!at~+1L8ibudP)r^W)8BHm4HMo79m8|?bN)Z6~@ z88$Jq|A&@dbo7Wzb^WQUXXrHNL9D@nob>8FYB+NbHWRsphm<-KbBW7j=}F7favK&3 z4!B>p9Q(qjQ}q~JB3P*XdDCoID7^kf{jcVX_BzdJL)CNjsfWG{_b1**rb12e@I!uN zd&@@ZEW0K1xrna1#|tfeH|9yKb0>Gq3DvF6fzo`@6RTA8s9G(7VyxT|B#J1} zFB_=Yy1Sh$|NbXhv0!f;Vf0lTVQ@L-2`gEsV_xE+nn2BVX};^Co;G2)f2-vZFV^)* zy;`ZM7wWX#OaIns$tRX1))xC+y7`H4gA!eA)da7zT=&KQK1LVIwA`uFwk;2}s%c?W ztF-WDX)_Cn@`sy%bT37xfmX=PV;;^&x(G*~xfvn+B5-qgfiq~uO z@rzo%eXljW^$ra?o@kxTE{D}=c=v~2)@k3Vqjg%obfivQJt@`%mYlG|C_BTnXL&o+~@?z z!Zu8q%WBQdkjp$Z8(1<%=5xQ{G->YyTG>MUBJxIzXK~wwk}y7gb{68MF&4s85GS{u z;oD^-N6XCRy>Qjc(ntfBx+7VZw&GGTopKorHuFkNp!_E3QWFmyb=H^JpF4xMej2$5 zK@acPT4r!+c|5qQzZpR<2(>~{DQN2+%eYpO%Vp%I48s$+{tmwF> zF4mT+AIfcF<5MVyEX?ti|C02UW9yNCR=Jivr~ai;KpPrwIa^e|#PJSjWS7mvU2s|T z^f>@akFK3Q8(?l{4$l*_ngTsBkN+JyEKefDyaY?}X%2mcdB`x=Q|rsn|nF16WZ0ZIEXv58v09H-sQiX|J2oFqMLQ{K;hFSou% z@5G#t%A9Y7%PV^Kcnxp3m#O>Aii(8mF`Trr-?&jVfz>~!z6Ced4#l@qr0?3H^me*@ zFWz^|z~B{S`=a!H+>P{YxT11Mn!Z%rF_%5PKi7yml$u#hixH73uq@6Sb3u&rPRY>UH2q${yhdOnt7S~o?u z3z3hoRJ6PM#Y4)_F6^M1ofGhA$DV?DBYZ(Z4R5qdmk%Kj@}2LE4G4jRT0qw`s4AV@ z2|1J+-2^N~st*2lpjiA#0GO8mfbL%x_xeyI@(ln;Es*WKSW0s_RFb+L0CXdJU-RC0 zS1$1ly=N@8gcYX{A+$U-J~Ax;U=}Ue!O}8 z3BEDwJK`qwdUlgKuxV~m^LV@c-c9Om-K44z8aM42HRMYBIxMT0J%4WhLfqAENM~ z`r3S<4Tp)Vt%GLRhgWZso2xA+gCq~L^{%%1hnv8+@Py=JR*+Ys!0o?|v0A62*5 z<`$g|=8OxJCa3zHZ=7f-?9nh~gW5b~lWFF%E6JGp2#^=%8AAbV_?;W)Kjmoso!4F zzm2o=cVE?6jiZgx^|PY34YRS@*92Ceig)O*LNDe~rH_yfcKXq5t6eG>*5GypJYri%8d!eA>wpmq~(Wpowd`4ZaC4leQ;7* z&an<`+gq&xcf8bc3)l5}%e=IAk>z%4Snb|}7&9ez4bGB7E z$BOx!qN+EW`gMET^1jmNBuf-A;GB~*{NO7rj5ESg$`^DuT>d96k7Z_*d&c^q%z3k) z(Lxtce;Q72eZOoUQY8n57e=^v{+vyPk(z`1QaJ$LN1hy87yO&|AON>ESz{ z*XfZfEK`MXRp03EiM5sxd(vl?NyD@kEvf5CoxA9NKXs19RklJB&TCvmW1jZsulzg@ zQOHe}m>p&Nl5!}jHVj@IaiBTJ$IonciPO7kZVqx=Tp_%(ty5;*5rr9F)g!Q)10IU; zuA5kQz(a9adev3~QEUw5r)?&vOd!;LYZUI}IwQG)Wya$&p5q(=mw@k<%Y{R$qQHnrGUYQp&DX_k-btH zXEXXL)ZxM!QsXps-7R9CJfvFuFVzFfp`M5$?Hmi2=~z2(0Kk}~86sUaH@`M`(A`W~ zpeb<}@XhvG1eVHKSEsm9zah_Am$QAmes0tsU^?5xc@LnuPvk9Voy@;Oe}x>r((JC5 z!9DEb=@MG2CG{_SDb$@p#~C5q^W^9%Gc3=Zr@fgjg4QxOBS4u ztfmfThGDG{ihjqw;mirS5yQ~Tfi*_Y{?zuz|-{QA^wlRM{zDV>;gZO|yv6{$bei3?uR|JHi+dpa%t`IkEF zyz)Aoc7I{1PWv8{&}rFvE8jupCHi}Kx0X5`b;u^2jz6kgr;|Ecot}F7X#IWSFN$?K z^Pw|zsyctT3$wvC6!k2>$@pVwjpHfq_^8?-BU3`7JbZZdZ2!22Es)ugGiH|#=s#q@ zK|_a0I(Sg|pdpf$mk&Ak;Fm?*e(BXV)?4^yOD!^f^~t)+>H4(AjSuMW{+dHF#Hjo@ zTHnH=#r3af-^%HC$)=EOwjHet8S?f}`~me_=c}52m$UHd{~Dq4D}0x&d#qUNg!6$_WTY}C^%K5#Q_BehXow$Z6pBh#Ba0>^34+sLJrQ?%UJ*oP}|vem-A zS&5m}M%{;#^y%czT@I|(%_y82nzLZ(`Q-yXJuZIJ?8EKQ1(oFk6cU?Yw0(Q5u`efd zyw$?Knb2|8gzlTlkFmD&UZSn~L+OIH4zxOdY?F_SMvFPLh1XMWtM Date: Wed, 27 May 2026 00:01:30 -0400 Subject: [PATCH 03/21] chore: strip main to Hypergraph definition + linters only Remove all code except Hypergraph/Basic.lean and Util/Linter/. Everything else lives on development and will be promoted to main one piece at a time as it reaches production quality. main: 126 build jobs, 0 sorry, 0 MCMC, 0 warnings. All deleted code preserved on development branch. --- lean/.sorry-baseline | 2 +- lean/Main.lean | 4 - lean/ProofAtlas.lean | 44 +- lean/ProofAtlas/Command/Common.lean | 166 --- lean/ProofAtlas/Command/Default.lean | 53 - lean/ProofAtlas/Command/Delta.lean | 44 - lean/ProofAtlas/Command/DeltaNs.lean | 70 -- lean/ProofAtlas/Command/Graph.lean | 45 - lean/ProofAtlas/Command/GraphNs.lean | 55 - lean/ProofAtlas/Command/Hausdorff.lean | 69 -- lean/ProofAtlas/Command/Profile.lean | 42 - lean/ProofAtlas/Command/Resistance.lean | 58 -- lean/ProofAtlas/Command/ResistanceNs.lean | 74 -- lean/ProofAtlas/Command/Status.lean | 109 -- lean/ProofAtlas/Demos/Paper.lean | 132 --- lean/ProofAtlas/Demos/Pipeline.lean | 134 --- lean/ProofAtlas/Export/Demos.lean | 180 ---- lean/ProofAtlas/Export/Profile.lean | 103 -- lean/ProofAtlas/Hypergraph/Decidability.lean | 41 - lean/ProofAtlas/Hypergraph/ReductionDemo.lean | 125 --- lean/ProofAtlas/Hypergraph/ToDigraph.lean | 83 -- lean/ProofAtlas/Hypergraph/ToSimpleGraph.lean | 87 -- .../Hypergraph/TransitionMatrix.lean | 317 ------ lean/ProofAtlas/Hypergraph/Walk.lean | 25 - lean/ProofAtlas/LinearAlgebra/Default.lean | 29 - .../LinearAlgebra/EffectiveResistance.lean | 89 -- .../LinearAlgebra/MoorePenrose.lean | 475 --------- .../LinearAlgebra/SpectralExpansion.lean | 106 -- lean/ProofAtlas/Mapping/Declaration.lean | 226 ----- lean/ProofAtlas/Mapping/Environment.lean | 170 ---- lean/ProofAtlas/Mapping/Expr.lean | 958 ------------------ lean/ProofAtlas/Mapping/ExprColor.lean | 56 - lean/ProofAtlas/Metric/Axioms.lean | 224 ---- lean/ProofAtlas/Metric/Default.lean | 24 - lean/ProofAtlas/Metric/GromovHausdorff.lean | 81 -- lean/ProofAtlas/Metric/Hausdorff.lean | 92 -- .../Metric/Resistance/Algebraic.lean | 723 ------------- .../Metric/Resistance/Canonical.lean | 173 ---- .../Metric/Resistance/Geometric.lean | 93 -- .../Metric/Resistance/HarmonicLaplacian.lean | 635 ------------ .../Metric/Resistance/Instance.lean | 92 -- .../Metric/Resistance/Resistance.lean | 130 --- .../Metric/Resistance/Spectral.lean | 52 - .../Metric/Resistance/Structural.lean | 241 ----- lean/ProofAtlas/Pipeline/Cache.lean | 116 --- lean/ProofAtlas/Pipeline/Export.lean | 68 -- lean/ProofAtlas/Pipeline/FloatMat.lean | 83 -- lean/ProofAtlas/Pipeline/Hausdorff.lean | 94 -- lean/ProofAtlas/Pipeline/Hyperbolicity.lean | 151 --- lean/ProofAtlas/Pipeline/IncidenceData.lean | 255 ----- lean/ProofAtlas/Pipeline/Linalg.lean | 257 ----- lean/ProofAtlas/RandomWalk/CommuteTime.lean | 449 -------- .../RandomWalk/FundamentalMatrix.lean | 918 ----------------- lean/ProofAtlas/RandomWalk/KemenySnell.lean | 911 ----------------- lean/ProofAtlas/Tactic/Widget.lean | 227 ----- lean/ProofAtlas/Util/Audit.lean | 166 --- .../Util/Linter/MetricRegistry.lean | 88 -- lean/lakefile.toml | 12 - lean/scripts/check_metric_instances.sh | 12 +- scripts/lint.sh | 5 +- 60 files changed, 17 insertions(+), 10526 deletions(-) delete mode 100644 lean/Main.lean delete mode 100644 lean/ProofAtlas/Command/Common.lean delete mode 100644 lean/ProofAtlas/Command/Default.lean delete mode 100644 lean/ProofAtlas/Command/Delta.lean delete mode 100644 lean/ProofAtlas/Command/DeltaNs.lean delete mode 100644 lean/ProofAtlas/Command/Graph.lean delete mode 100644 lean/ProofAtlas/Command/GraphNs.lean delete mode 100644 lean/ProofAtlas/Command/Hausdorff.lean delete mode 100644 lean/ProofAtlas/Command/Profile.lean delete mode 100644 lean/ProofAtlas/Command/Resistance.lean delete mode 100644 lean/ProofAtlas/Command/ResistanceNs.lean delete mode 100644 lean/ProofAtlas/Command/Status.lean delete mode 100644 lean/ProofAtlas/Demos/Paper.lean delete mode 100644 lean/ProofAtlas/Demos/Pipeline.lean delete mode 100644 lean/ProofAtlas/Export/Demos.lean delete mode 100644 lean/ProofAtlas/Export/Profile.lean delete mode 100644 lean/ProofAtlas/Hypergraph/Decidability.lean delete mode 100644 lean/ProofAtlas/Hypergraph/ReductionDemo.lean delete mode 100644 lean/ProofAtlas/Hypergraph/ToDigraph.lean delete mode 100644 lean/ProofAtlas/Hypergraph/ToSimpleGraph.lean delete mode 100644 lean/ProofAtlas/Hypergraph/TransitionMatrix.lean delete mode 100644 lean/ProofAtlas/Hypergraph/Walk.lean delete mode 100644 lean/ProofAtlas/LinearAlgebra/Default.lean delete mode 100644 lean/ProofAtlas/LinearAlgebra/EffectiveResistance.lean delete mode 100644 lean/ProofAtlas/LinearAlgebra/MoorePenrose.lean delete mode 100644 lean/ProofAtlas/LinearAlgebra/SpectralExpansion.lean delete mode 100644 lean/ProofAtlas/Mapping/Declaration.lean delete mode 100644 lean/ProofAtlas/Mapping/Environment.lean delete mode 100644 lean/ProofAtlas/Mapping/Expr.lean delete mode 100644 lean/ProofAtlas/Mapping/ExprColor.lean delete mode 100644 lean/ProofAtlas/Metric/Axioms.lean delete mode 100644 lean/ProofAtlas/Metric/Default.lean delete mode 100644 lean/ProofAtlas/Metric/GromovHausdorff.lean delete mode 100644 lean/ProofAtlas/Metric/Hausdorff.lean delete mode 100644 lean/ProofAtlas/Metric/Resistance/Algebraic.lean delete mode 100644 lean/ProofAtlas/Metric/Resistance/Canonical.lean delete mode 100644 lean/ProofAtlas/Metric/Resistance/Geometric.lean delete mode 100644 lean/ProofAtlas/Metric/Resistance/HarmonicLaplacian.lean delete mode 100644 lean/ProofAtlas/Metric/Resistance/Instance.lean delete mode 100644 lean/ProofAtlas/Metric/Resistance/Resistance.lean delete mode 100644 lean/ProofAtlas/Metric/Resistance/Spectral.lean delete mode 100644 lean/ProofAtlas/Metric/Resistance/Structural.lean delete mode 100644 lean/ProofAtlas/Pipeline/Cache.lean delete mode 100644 lean/ProofAtlas/Pipeline/Export.lean delete mode 100644 lean/ProofAtlas/Pipeline/FloatMat.lean delete mode 100644 lean/ProofAtlas/Pipeline/Hausdorff.lean delete mode 100644 lean/ProofAtlas/Pipeline/Hyperbolicity.lean delete mode 100644 lean/ProofAtlas/Pipeline/IncidenceData.lean delete mode 100644 lean/ProofAtlas/Pipeline/Linalg.lean delete mode 100644 lean/ProofAtlas/RandomWalk/CommuteTime.lean delete mode 100644 lean/ProofAtlas/RandomWalk/FundamentalMatrix.lean delete mode 100644 lean/ProofAtlas/RandomWalk/KemenySnell.lean delete mode 100644 lean/ProofAtlas/Tactic/Widget.lean delete mode 100644 lean/ProofAtlas/Util/Audit.lean delete mode 100644 lean/ProofAtlas/Util/Linter/MetricRegistry.lean diff --git a/lean/.sorry-baseline b/lean/.sorry-baseline index 48082f7..573541a 100644 --- a/lean/.sorry-baseline +++ b/lean/.sorry-baseline @@ -1 +1 @@ -12 +0 diff --git a/lean/Main.lean b/lean/Main.lean deleted file mode 100644 index be873fa..0000000 --- a/lean/Main.lean +++ /dev/null @@ -1,4 +0,0 @@ -import ProofAtlas - -def main : IO Unit := - IO.println s!"Hello, {hello}!" diff --git a/lean/ProofAtlas.lean b/lean/ProofAtlas.lean index bc6f401..2f36b6d 100644 --- a/lean/ProofAtlas.lean +++ b/lean/ProofAtlas.lean @@ -3,50 +3,14 @@ Copyright (c) 2026 MathNetwork. All rights reserved. Released under Apache 2.0 license as described in the file LICENSE. Authors: MathNetwork -/ - --- Linters import ProofAtlas.Util.Linter.MathTag import ProofAtlas.Util.Linter.ImportBan - --- Core: hypergraph structure import ProofAtlas.Hypergraph.Basic -import ProofAtlas.Hypergraph.Walk -import ProofAtlas.Hypergraph.Decidability -import ProofAtlas.Hypergraph.TransitionMatrix - --- Linear algebra -import ProofAtlas.LinearAlgebra.Default - --- Random walk theory (abstract Markov chain, no MCMC) -import ProofAtlas.RandomWalk.FundamentalMatrix -import ProofAtlas.RandomWalk.KemenySnell -import ProofAtlas.RandomWalk.CommuteTime - --- Metric: resistance distance (sorry-free, MCMC-free) -import ProofAtlas.Metric.Default -import ProofAtlas.Metric.Resistance.Instance -import ProofAtlas.Metric.Hausdorff -import ProofAtlas.Metric.GromovHausdorff - --- Mapping: Expr → Hypergraph -import ProofAtlas.Mapping.ExprColor -import ProofAtlas.Mapping.Expr -import ProofAtlas.Mapping.Environment - --- Pipeline: compute layer -import ProofAtlas.Pipeline.IncidenceData -import ProofAtlas.Pipeline.Linalg -import ProofAtlas.Pipeline.Hyperbolicity - --- Commands + widgets -import ProofAtlas.Tactic.Widget -import ProofAtlas.Command.Default - --- Demos -import ProofAtlas.Demos.Paper /-! -# `ProofAtlas` library facade (main branch) +# `ProofAtlas` — main branch -Sorry-free, MCMC-free. See ADR 0007. +The BDF proof hypergraph definition. Everything else is built on +the `development` branch and merged here as it reaches production +quality (sorry-free, reviewed, tested). See ADR 0007. -/ diff --git a/lean/ProofAtlas/Command/Common.lean b/lean/ProofAtlas/Command/Common.lean deleted file mode 100644 index 8dc2023..0000000 --- a/lean/ProofAtlas/Command/Common.lean +++ /dev/null @@ -1,166 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import Lean -import ProofWidgets.Component.HtmlDisplay -import ProofAtlas.Mapping.Environment -import ProofAtlas.Tactic.Widget - -/-! -# `Command/Common.lean` — shared plumbing for `#atlas.*` commands - -Every `#atlas.` command shares the same opening dance: - -1. Resolve a list of identifiers against the current environment. -2. Pull each declaration's value `Expr` (rejecting axioms / opaques). -3. Glue them into one `HypergraphReport` via - `geometricProfileOfExprs` (const-sharing across declarations is - automatic — the `HypergraphReport.roots` field records where - each input ended up). -4. Render the interactive `AtlasGraph` widget + colour legend in the - infoview. - -This module factors all of that out so each command file is small. - -## Semantics of multi-`ident` arguments - -A command like `#atlas.resistance A B` semantically *glues* the two -proofs into a single BDF hypergraph (vertices identified by -const-sharing) and addresses the result via the per-input root IDs. -For `#atlas.resistance` in particular: returns -`R(report.roots[0]!, report.roots[1]!)`. --/ - -namespace ProofAtlas.Command - -open Lean Elab Command ProofWidgets - -/-! ## Identifier resolution -/ - -/-- **Eng.** Resolve a `Lean.Syntax` identifier to its canonical -fully-qualified `Name`, using the current namespace stack and open -declarations. Throws on unknown / overloaded constants. Cheaper than -`resolveIdentValue` when the value `Expr` is not needed. -/ -def resolveIdentName (ident : Lean.Syntax) : CommandElabM Name := - liftTermElabM do - let n ← Lean.Elab.realizeGlobalConstNoOverloadWithInfo ident - Lean.Elab.addCompletionInfo <| .id ident n (danglingDot := false) {} none - return n - -/-- **Eng.** Resolve an identifier relative to a given namespace. Used -by the `#atlas.*.ns` commands where the namespace is an explicit -argument and the user typically writes short names. Tries -`nsName ++ bare` first (the natural reading inside the namespace), -then the bare name, then the standard Lean resolver as a last resort. -Returns the fully-qualified `Name`. -/ -def resolveIdentInNamespace (nsName : Name) (ident : Lean.Syntax) : - CommandElabM Name := do - let env ← getEnv - let bare := ident.getId - let qualified := nsName ++ bare - if env.contains qualified then return qualified - if env.contains bare then return bare - try - resolveIdentName ident - catch _ => - throwError "could not resolve `{bare}` (tried `{qualified}` and `{bare}`)" - -/-- **Eng.** Resolve a `Lean.Syntax` identifier against the current -environment, returning the canonical `Name` plus the declaration's -value `Expr`. Throws on unknown / opaque / axiomatic constants. -/ -def resolveIdentValue (ident : Lean.Syntax) : TermElabM (Name × Expr) := do - let n ← Lean.Elab.realizeGlobalConstNoOverloadWithInfo ident - Lean.Elab.addCompletionInfo <| .id ident n (danglingDot := false) {} none - let info ← Lean.getConstInfo n - let some val := info.value? (allowOpaque := true) - | throwError "{n} has no value (axiom?)" - return (n, val) - -/-- **Eng.** Resolve every identifier in `idents` and return the -list of `(Name, Expr)` pairs. The names are useful for nicer -diagnostic messages downstream. -/ -def resolveAllIdents (idents : Array Lean.Syntax) : - CommandElabM (Array (Name × Expr)) := do - liftTermElabM <| idents.mapM resolveIdentValue - -/-! ## Glue + profile -/ - -/-- **Eng.** Resolve a non-empty list of idents and run the full -pipeline (const-share glue + resistance matrix + δ). Throws if -`idents` is empty. -/ -def profileIdents (idents : Array Lean.Syntax) : - CommandElabM Pipeline.HypergraphReport := do - if idents.isEmpty then - throwError "expected at least one identifier" - let pairs ← resolveAllIdents idents - let exprs := pairs.map Prod.snd - return Mapping.geometricProfileOfExprs exprs - -/-- **Eng.** Cheaper variant: incidence + glue only, no resistance, -no δ. For commands that only render the graph. -/ -def incidenceProfileIdents (idents : Array Lean.Syntax) : - CommandElabM Pipeline.HypergraphReport := do - if idents.isEmpty then - throwError "expected at least one identifier" - let pairs ← resolveAllIdents idents - let exprs := pairs.map Prod.snd - return Mapping.incidenceProfileOfExprs exprs - -/-- **Eng.** Cheaper variant: incidence + resistance matrix, no δ. -For commands that read `R(...)` but never look at δ. -/ -def resistanceProfileIdents (idents : Array Lean.Syntax) : - CommandElabM Pipeline.HypergraphReport := do - if idents.isEmpty then - throwError "expected at least one identifier" - let pairs ← resolveAllIdents idents - let exprs := pairs.map Prod.snd - return Mapping.resistanceProfileOfExprs exprs - -/-! ## Widget rendering -/ - -/-- **Eng.** Save the interactive `AtlasGraph` widget for the given -report below the command's syntax. -/ -def renderGraphWidget (stx : Lean.Syntax) - (report : Pipeline.HypergraphReport) : CommandElabM Unit := do - let props := report.incidence.toAtlasGraphProps - liftCoreM <| Lean.Widget.savePanelWidgetInfo - (hash Pipeline.AtlasGraph.javascript) - (Lean.Server.RpcEncodable.rpcEncode props) - stx - -/-- **Eng.** Save the static colour legend below the command's -syntax (separate widget from the interactive graph). -/ -def renderColorLegend (stx : Lean.Syntax) : CommandElabM Unit := do - let legend := Pipeline.colorLegendHtml - liftCoreM <| Lean.Widget.savePanelWidgetInfo - (hash HtmlDisplayPanel.javascript) - (return Lean.Json.mkObj - [("html", ← Lean.Server.RpcEncodable.rpcEncode legend)]) - stx - -/-- **Eng.** Convenience: render both the interactive graph and the -colour legend. The two stack vertically in the infoview. -/ -def renderGraphWithLegend (stx : Lean.Syntax) - (report : Pipeline.HypergraphReport) : CommandElabM Unit := do - renderGraphWidget stx report - renderColorLegend stx - -/-- **Eng.** Decl-level colour legend (circles by declaration kind: -theorem / def / instance / axiom / opaque / other). -/ -def renderDeclColorLegend (stx : Lean.Syntax) : CommandElabM Unit := do - let legend := Pipeline.declColorLegendHtml - liftCoreM <| Lean.Widget.savePanelWidgetInfo - (hash HtmlDisplayPanel.javascript) - (return Lean.Json.mkObj - [("html", ← Lean.Server.RpcEncodable.rpcEncode legend)]) - stx - -/-- **Eng.** Render an interactive graph + the decl-level legend. -/ -def renderGraphWithDeclLegend (stx : Lean.Syntax) - (report : Pipeline.HypergraphReport) : CommandElabM Unit := do - renderGraphWidget stx report - renderDeclColorLegend stx - -end ProofAtlas.Command diff --git a/lean/ProofAtlas/Command/Default.lean b/lean/ProofAtlas/Command/Default.lean deleted file mode 100644 index 683539f..0000000 --- a/lean/ProofAtlas/Command/Default.lean +++ /dev/null @@ -1,53 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Command.Common -import ProofAtlas.Command.Graph -import ProofAtlas.Command.Resistance -import ProofAtlas.Command.Delta -import ProofAtlas.Command.Profile -import ProofAtlas.Command.Status -import ProofAtlas.Command.Hausdorff -import ProofAtlas.Command.GraphNs -import ProofAtlas.Command.ResistanceNs -import ProofAtlas.Command.DeltaNs - -/-! -# `ProofAtlas.Command` — the `#atlas.*` command suite - -Importing this facade brings in every `#atlas.*` command. Quick -reference: - -``` --- Structure (expr-level) -#atlas.graph A [B ...] -- hypergraph viewer -#atlas.profile A [B ...] -- graph + full report - --- Metrics (expr-level) -#atlas.resistance A B -- R(root_A, root_B) -#atlas.delta A [B ...] -- Gromov δ + δ/diam -#atlas.hausdorff.resistance A | B -- group Hausdorff under R - --- Structure (decl-level) -#atlas.graph.ns NS -- co-reference graph of NS - --- Metrics (decl-level) -#atlas.resistance.ns NS declA declB -- R on the decl-graph of NS -#atlas.delta.ns NS -- δ + δ/diam on the decl-graph of NS - --- Quality -#atlas.status myDist -- registry / sorry lookup -``` - -Multi-`ident` expr-level semantics: every command that accepts more -than one identifier *glues* their proof terms into a single -hypergraph via const-sharing before computing. The -`HypergraphReport.roots` array records where each input ended up -inside the combined incidence data. - -Decl-level semantics: vertices are declarations of the given -namespace, hyperedges are the maximal application spines inside each -declaration's proof body (the co-reference structure). --/ diff --git a/lean/ProofAtlas/Command/Delta.lean b/lean/ProofAtlas/Command/Delta.lean deleted file mode 100644 index 8671d35..0000000 --- a/lean/ProofAtlas/Command/Delta.lean +++ /dev/null @@ -1,44 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Command.Common - -/-! -# `#atlas.delta` — geometry-layer command - -Computes the *Gromov δ-hyperbolicity* of the BDF hypergraph glued -from one or more declarations. - -``` -#atlas.delta Nat.add_zero Nat.add_comm Nat.succ_eq_add_one --- info: δ = 0.123 --- diam = 1.234 --- δ/diam = 0.099 (tree-likeness — paper §7) -``` - -`δ` alone reports the absolute four-point deviation; `δ / diam` is -the normalised tree-likeness ratio bounded by paper Theorem 7.1. - -Use `#atlas.graph ` to visualise the same const-shared glue. --/ - -namespace ProofAtlas.Command - -open Lean Elab Command - -/-- **Eng.** Syntax for `#atlas.delta`. One or more identifiers; the -hyperbolicity of the const-shared glue is reported. -/ -syntax (name := atlasDeltaCmd) "#atlas.delta" (ppSpace colGt ident)+ : command - -@[command_elab atlasDeltaCmd] -def elabAtlasDeltaCmd : CommandElab := fun stx => do - let idents := stx[1].getArgs - let report ← profileIdents idents - let δ := report.delta - let diam := report.diameter - let ratio : Float := if diam > 0.0 then δ / diam else 0.0 - logInfo m!"δ = {δ}\ndiam = {diam}\nδ/diam = {ratio}" - -end ProofAtlas.Command diff --git a/lean/ProofAtlas/Command/DeltaNs.lean b/lean/ProofAtlas/Command/DeltaNs.lean deleted file mode 100644 index d159c93..0000000 --- a/lean/ProofAtlas/Command/DeltaNs.lean +++ /dev/null @@ -1,70 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Command.Common -import ProofAtlas.Mapping.Declaration -import ProofAtlas.Pipeline.Cache - -/-! -# `#atlas.delta.ns` — Gromov δ-hyperbolicity of the decl-level graph - -``` -#atlas.delta.ns ProofAtlas.Pipeline --- δ = 0.512 --- diam = 2.317 --- δ/diam = 0.221 [decl-level on namespace ProofAtlas.Pipeline, 67 vertices] -``` - -Builds the decl-level hypergraph of `nsName` and reports its -δ-hyperbolicity, diameter, and the normalised tree-likeness ratio -δ/diam. - -This is the decl-level companion to `#atlas.delta A B ...`. The -expr-level version takes a list of declarations, glues their proof -bodies into one subterm hypergraph, and reports δ on the glued -subterm metric. The decl-level version reports δ on the *namespace's -co-reference graph* — what's the large-scale shape of how decls in -this namespace cite each other. - -Cost is `O(C(n,4)) ≈ n⁴ / 24` four-tuple enumerations. For a -namespace of 67 decls this is ~770k tuples and runs in a fraction -of a second; namespaces over ~200 decls become noticeably slow. - -Caveat: a disconnected decl-graph produces a numerically inflated -diameter (cross-component "distances" come from the regularised -pseudoinverse). The ratio δ/diam still gives a useful sanity check, -but treat the absolute numbers with care until component-aware -reporting lands. --/ - -namespace ProofAtlas.Command - -open Lean Elab Command - -/-- **Eng.** Syntax for `#atlas.delta.ns`. Takes a single namespace -identifier. -/ -syntax (name := atlasDeltaNsCmd) "#atlas.delta.ns" ppSpace colGt ident : command - -@[command_elab atlasDeltaNsCmd] -def elabAtlasDeltaNsCmd : CommandElab := fun stx => do - let nsName : Name := stx[1].getId - let env ← getEnv - let profile ← liftCoreM <| Pipeline.getOrBuildDeclProfile env nsName - let data := profile.incidence - if data.numVertices = 0 then - throwError "#atlas.delta.ns: no declarations found under {nsName}" - let largest := profile.largestComponent - let nComps := profile.numComponents - let (δ, diam) ← liftCoreM <| Pipeline.getOrComputeDelta env nsName - let ratio : Float := if diam > 0.0 then δ / diam else 0.0 - let compNote : String := - if nComps ≤ 1 then "connected" - else s!"{nComps} components — δ/diam restricted to largest ({largest.size} verts)" - logInfo m!"δ = {δ}\n\ - diam = {diam}\n\ - δ/diam = {ratio}\n\ - [decl-level on namespace {nsName}, {data.numVertices} vertices, {data.numEdges} edges; {compNote}]" - -end ProofAtlas.Command diff --git a/lean/ProofAtlas/Command/Graph.lean b/lean/ProofAtlas/Command/Graph.lean deleted file mode 100644 index 1d301cd..0000000 --- a/lean/ProofAtlas/Command/Graph.lean +++ /dev/null @@ -1,45 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Command.Common - -/-! -# `#atlas.graph` — structure-level command - -Renders the BDF hypergraph of one or more declarations as an -interactive d3 widget. Pure structure: no metric output, just the -hypergraph + colour legend. - -``` -#atlas.graph Nat.add_zero -#atlas.graph Nat.add_zero Nat.add_comm -- glued via const-sharing -``` - -The widget supports drag, mouse-wheel zoom, and two live-tuning -sliders (`spread` / `link dist`). Hovering a node shows its -short-description label. --/ - -namespace ProofAtlas.Command - -open Lean Elab Command - -/-- **Eng.** Syntax for `#atlas.graph`. Takes one or more identifiers; -`#atlas.graph A B C` glues their proof terms into a single BDF -hypergraph via const-sharing and renders it. -/ -syntax (name := atlasGraphCmd) "#atlas.graph" (ppSpace colGt ident)+ : command - -@[command_elab atlasGraphCmd] -def elabAtlasGraphCmd : CommandElab := fun stx => do - let idents := stx[1].getArgs - -- Structure-only: skip both the resistance matrix and δ-sweep, no - -- consumer of this command reads them. - let report ← incidenceProfileIdents idents - let n := report.incidence.numVertices - let m := report.incidence.numEdges - logInfo m!"BDF-Hypergraph: {n} vertices, {m} hyperedges" - renderGraphWithLegend stx report - -end ProofAtlas.Command diff --git a/lean/ProofAtlas/Command/GraphNs.lean b/lean/ProofAtlas/Command/GraphNs.lean deleted file mode 100644 index 85b3485..0000000 --- a/lean/ProofAtlas/Command/GraphNs.lean +++ /dev/null @@ -1,55 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Command.Common -import ProofAtlas.Mapping.Declaration -import ProofAtlas.Pipeline.Cache - -/-! -# `#atlas.graph.ns` — declaration-level hypergraph for a namespace - -``` -#atlas.graph.ns Nat -#atlas.graph.ns ProofAtlas.Pipeline -``` - -Walks every non-internal declaration whose name has the given -namespace as a strict prefix, extracts a co-reference hypergraph -(`Pipeline/DeclIncidence.lean`), and renders the result via the -existing graph widget. - -Vertices = declarations. Hyperedges = maximal application spines -inside each declaration's proof body, with inputs = the head and -the immediate arg heads (filtered to the namespace, deduped, -self-ref removed). This is the decl-level companion to -`#atlas.graph` (which is expr-level). --/ - -namespace ProofAtlas.Command - -open Lean Elab Command - -/-- **Eng.** Syntax for `#atlas.graph.ns`. Takes a single -namespace identifier. -/ -syntax (name := atlasGraphNsCmd) "#atlas.graph.ns" ppSpace colGt ident : command - -@[command_elab atlasGraphNsCmd] -def elabAtlasGraphNsCmd : CommandElab := fun stx => do - let nsName : Name := stx[1].getId - let env ← getEnv - let profile ← liftCoreM <| Pipeline.getOrBuildDeclProfile env nsName - let data := profile.incidence - if data.numVertices = 0 then - throwError "#atlas.graph.ns: no declarations found under {nsName}" - let n := data.numVertices - let m := data.numEdges - logInfo m!"Decl-level hypergraph ({nsName}): {n} vertices, {m} hyperedges" - -- Render via the existing graph widget with the decl-level legend - -- (vertex colour by declaration kind, not hyperedge colour). - let report : Pipeline.HypergraphReport := - { incidence := data, resistance := #[], delta := 0.0, roots := #[] } - renderGraphWithDeclLegend stx report - -end ProofAtlas.Command diff --git a/lean/ProofAtlas/Command/Hausdorff.lean b/lean/ProofAtlas/Command/Hausdorff.lean deleted file mode 100644 index 723ecc5..0000000 --- a/lean/ProofAtlas/Command/Hausdorff.lean +++ /dev/null @@ -1,69 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Command.Common -import ProofAtlas.Pipeline.Hausdorff - -/-! -# `#atlas.hausdorff.resistance` — Hausdorff distance between two declaration sets - -``` -#atlas.hausdorff.resistance Nat.add_zero | Nat.add_comm Nat.succ_eq_add_one --- info: Hausdorff(resistance, {Nat.add_zero}, {Nat.add_comm, Nat.succ_eq_add_one}) = … -``` - -`|` separates the two groups. Every declaration on either side is -glued into one `IncidenceData` via constant-sharing; the resistance -matrix is computed on the combined graph; the Hausdorff distance is -taken over the *union of subtree vertex sets* for each group, not -the singleton root vertices. This way the number reports "how far -apart these two proofs are as sets of subterms", which matches the -geometric content of Hausdorff distance. - -The `.resistance` suffix anticipates future siblings -(`#atlas.hausdorff.commute`, `.jsd`, `.diffusion`) once the -corresponding Float compute paths land. --/ - -namespace ProofAtlas.Command - -open Lean Elab Command - -/-- **Eng.** Syntax for `#atlas.hausdorff.resistance`. Two non-empty -groups of identifiers separated by `|`. -/ -syntax (name := atlasHausdorffResCmd) - "#atlas.hausdorff.resistance" - (ppSpace colGt ident)+ ppSpace "|" (ppSpace colGt ident)+ : command - -@[command_elab atlasHausdorffResCmd] -def elabAtlasHausdorffResCmd : CommandElab := fun stx => do - let leftIdents := stx[1].getArgs - let rightIdents := stx[3].getArgs - if leftIdents.isEmpty || rightIdents.isEmpty then - throwError "#atlas.hausdorff.resistance: both sides of | must be non-empty" - let leftPairs ← resolveAllIdents leftIdents - let rightPairs ← resolveAllIdents rightIdents - let leftNames := leftPairs.map Prod.fst - let rightNames := rightPairs.map Prod.fst - -- Glue everything into one IncidenceData (left first, then right). - let allExprs : Array Expr := - (leftPairs.map Prod.snd) ++ (rightPairs.map Prod.snd) - let report := Mapping.resistanceProfileOfExprs allExprs - -- Split per-input root indices. - let leftCount := leftPairs.size - let leftRoots := report.roots.extract 0 leftCount - let rightRoots := report.roots.extract leftCount report.roots.size - -- Expand each group's roots into its union of subtree vertices, - -- giving the proof's actual subterm footprint in the const-shared - -- graph (shared `Nat`, `Eq`, etc. appear in both sets). - let leftSet := report.incidence.subtreesUnion leftRoots - let rightSet := report.incidence.subtreesUnion rightRoots - let h : Float := - Pipeline.hausdorffOnMatrix report.resistance leftSet rightSet - let leftStr := String.intercalate ", " (leftNames.toList.map toString) - let rightStr := String.intercalate ", " (rightNames.toList.map toString) - logInfo m!"Hausdorff(resistance, \{{leftStr}}, \{{rightStr}}) = {h}" - -end ProofAtlas.Command diff --git a/lean/ProofAtlas/Command/Profile.lean b/lean/ProofAtlas/Command/Profile.lean deleted file mode 100644 index de8c423..0000000 --- a/lean/ProofAtlas/Command/Profile.lean +++ /dev/null @@ -1,42 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Command.Common - -/-! -# `#atlas.profile` — text report - -Print the full multi-line `HypergraphReport.summary` (vertices, -edges + colour distribution, diameter, mean dist, δ, δ/diam). - -``` -#atlas.profile Nat.add_zero --- info: BDF-Hypergraph Report --- vertices 25 --- edges 15 [app=14, lam=1] --- diameter 2.512 --- mean dist 1.137 --- δ 0.186 --- δ / diam 0.074 -``` - -Use `#atlas.graph ` to visualise the same hypergraph. --/ - -namespace ProofAtlas.Command - -open Lean Elab Command - -/-- **Eng.** Syntax for `#atlas.profile`. One or more identifiers; -the combined text report for their const-shared glue. -/ -syntax (name := atlasProfileCmd) "#atlas.profile" (ppSpace colGt ident)+ : command - -@[command_elab atlasProfileCmd] -def elabAtlasProfileCmd : CommandElab := fun stx => do - let idents := stx[1].getArgs - let report ← profileIdents idents - logInfo m!"{report.summary}" - -end ProofAtlas.Command diff --git a/lean/ProofAtlas/Command/Resistance.lean b/lean/ProofAtlas/Command/Resistance.lean deleted file mode 100644 index fc615cb..0000000 --- a/lean/ProofAtlas/Command/Resistance.lean +++ /dev/null @@ -1,58 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Command.Common - -/-! -# `#atlas.resistance` — metric-layer command - -Computes the *effective resistance distance* between two -declarations. - -``` -#atlas.resistance Nat.add_zero Nat.add_comm --- info: R(Nat.add_zero, Nat.add_comm) = 1.234567 -``` - -The two proof terms are glued into a single BDF hypergraph via -const-sharing; the answer is `R(root_A, root_B)` on that combined -graph, where `root_A` and `root_B` are the root vertices recorded in -`HypergraphReport.roots`. - -Use `#atlas.graph A B` to visualise the same const-shared glue. --/ - -namespace ProofAtlas.Command - -open Lean Elab Command - -/-- **Eng.** Syntax for `#atlas.resistance`. Requires exactly two -identifiers. -/ -syntax (name := atlasResistanceCmd) - "#atlas.resistance" ppSpace colGt ident ppSpace colGt ident : command - -@[command_elab atlasResistanceCmd] -def elabAtlasResistanceCmd : CommandElab := fun stx => do - let identA := stx[1] - let identB := stx[2] - let pairs ← resolveAllIdents #[identA, identB] - let (nameA, _) := pairs[0]! - let (nameB, _) := pairs[1]! - let exprs := pairs.map Prod.snd - -- Resistance-only: skip the δ-sweep, this command never reads it. - let report := Mapping.resistanceProfileOfExprs exprs - if h : report.roots.size < 2 then - throwError "internal error: report.roots has size {report.roots.size} < 2" - else - let rA : Nat := report.roots[0]! - let rB : Nat := report.roots[1]! - if h2 : rA ≥ report.resistance.size ∨ rB ≥ report.resistance.size then - throwError "internal error: root index out of range" - else - let row := report.resistance[rA]! - let r : Float := row[rB]! - logInfo m!"R({nameA}, {nameB}) = {r}" - -end ProofAtlas.Command diff --git a/lean/ProofAtlas/Command/ResistanceNs.lean b/lean/ProofAtlas/Command/ResistanceNs.lean deleted file mode 100644 index 16a1064..0000000 --- a/lean/ProofAtlas/Command/ResistanceNs.lean +++ /dev/null @@ -1,74 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Command.Common -import ProofAtlas.Mapping.Declaration -import ProofAtlas.Pipeline.Cache - -/-! -# `#atlas.resistance.ns` — decl-level resistance between two declarations - -``` -#atlas.resistance.ns ProofAtlas.Pipeline geometricProfile geometricProfileOfExprs --- R(geometricProfile, geometricProfileOfExprs) = 0.847 [decl-level on --- namespace ProofAtlas.Pipeline, 67 vertices] -``` - -Builds the decl-level hypergraph of `nsName` (vertices = declarations, -hyperedges = maximal app spines inside each proof body — see -`Pipeline/DeclIncidence.lean`), then looks up the effective resistance -between the two named declarations on that graph. - -This is the decl-level companion to `#atlas.resistance A B`. The -expr-level version glues `A` and `B`'s proof bodies into one subterm -hypergraph and measures resistance between their root subterm -vertices. The decl-level version measures resistance between -declaration vertices in the co-reference graph of an entire namespace -— a coarser, more "library shape" signal. - -Caveat: if the decl-graph is disconnected, cross-component pairs -return numerically large but finite values (regularised pseudoinverse). -A future v2 may report component membership; for now the raw number -suffices to tell "close" from "far". --/ - -namespace ProofAtlas.Command - -open Lean Elab Command - -/-- **Eng.** Syntax for `#atlas.resistance.ns`. Takes the namespace -plus the two declarations to compare. -/ -syntax (name := atlasResistanceNsCmd) - "#atlas.resistance.ns" ppSpace colGt ident ppSpace colGt ident ppSpace colGt ident : command - -@[command_elab atlasResistanceNsCmd] -def elabAtlasResistanceNsCmd : CommandElab := fun stx => do - let nsName : Name := stx[1].getId - let declAName ← resolveIdentInNamespace nsName stx[2] - let declBName ← resolveIdentInNamespace nsName stx[3] - let env ← getEnv - let profile ← liftCoreM <| Pipeline.getOrBuildDeclProfile env nsName - let data := profile.incidence - if data.numVertices = 0 then - throwError "#atlas.resistance.ns: no declarations found under {nsName}" - -- The decl→vid map is implicit: vid = position in the order returned by - -- `Environment.declsInNamespace`. We re-run that here (cheap) to look up A and B. - let decls := Mapping.Environment.declsInNamespace env nsName - let some vidA := decls.findIdx? (· == declAName) - | throwError "#atlas.resistance.ns: {declAName} not found under namespace {nsName}" - let some vidB := decls.findIdx? (· == declBName) - | throwError "#atlas.resistance.ns: {declBName} not found under namespace {nsName}" - let R := profile.resistance - let r : Float := R.get vidA vidB - let compA := profile.componentLabel[vidA]! - let compB := profile.componentLabel[vidB]! - let sameComp := compA == compB - let compNote : String := - if sameComp then s!"same component (#{compA}/{profile.numComponents})" - else s!"different components (A→#{compA}, B→#{compB} of {profile.numComponents}) — value is ε⁻¹-scale noise" - logInfo m!"R({declAName}, {declBName}) = {r}\n\ - [decl-level on namespace {nsName}, {data.numVertices} vertices, {data.numEdges} edges; {compNote}]" - -end ProofAtlas.Command diff --git a/lean/ProofAtlas/Command/Status.lean b/lean/ProofAtlas/Command/Status.lean deleted file mode 100644 index a13c504..0000000 --- a/lean/ProofAtlas/Command/Status.lean +++ /dev/null @@ -1,109 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Command.Common -import ProofAtlas.Metric.Resistance.Instance - -/-! -# `#atlas.status` — `IsHypergraphMetric` registry lookup - -`IsHypergraphMetric` is a `Prop`, so the command cannot *verify* arbitrary -candidate distances. Instead, it inspects the **current environment** -for the conventional witness theorem `_isHypergraphMetric` and -reports one of three states: - -* `✓ certified` — instance theorem exists and is `sorry`-free - (checked via `Lean.collectAxioms`). -* `⚠ in progress` — instance theorem exists but its dependency closure - contains `sorryAx`. -* `✗ not registered` — no instance theorem found under any candidate - name. - -The candidate names tried (in order) are: - -1. the user-typed identifier exactly as given, -2. the same name with `_isHypergraphMetric` appended, -3. `Hypergraph.` and `Hypergraph._isHypergraphMetric`. - -This makes `#atlas.status resistanceDist` and -`#atlas.status Hypergraph.resistanceDist_isHypergraphMetric` both work. - -The four registered metrics (`resistanceDist`, `commuteTimeDist`, -`jsDist`, `diffusionDist`) are imported here so the command always -sees them. This mirrors the CI guard -`scripts/check_metric_instances.sh`. --/ - -namespace ProofAtlas.Command - -open Lean Elab Command - -/-- **Eng.** Append a string suffix to the last component of a `Name`. -/ -private def Name.appendSuffix : Name → String → Name - | .str p s, suf => .str p (s ++ suf) - | n, suf => .str n suf - -/-- **Eng.** Does the last component of `n` already end in `suf`? -/ -private def Name.lastEndsWith (n : Name) (suf : String) : Bool := - match n with - | .str _ s => s.endsWith suf - | _ => false - -/-- **Eng.** Candidate *instance-theorem* names to look up, in priority -order. The first entry that exists in the environment wins. - -Important: we never match the bare `` (without `_isHypergraphMetric` -suffix), because `` is typically the distance `def` itself — -matching it would conflate "metric defined" with "instance proved". -/ -private def candidateInstanceNames (n : Name) : List Name := - let suf := "_isHypergraphMetric" - let withSuf : Name := - if Name.lastEndsWith n suf then n else Name.appendSuffix n suf - let bdfPrefix : Name := `Hypergraph - -- De-dup while preserving order. Only `_isHypergraphMetric`-suffixed - -- variants are tried — never the raw `def` name. - let raw : List Name := [withSuf, bdfPrefix ++ withSuf] - raw.foldl (init := ([] : List Name)) fun acc nm => - if acc.contains nm then acc else acc ++ [nm] - -/-- **Eng.** Syntax for `#atlas.status`. Accepts one identifier — the -candidate metric (short) name, e.g. `resistanceDist`. -/ -syntax (name := atlasStatusCmd) "#atlas.status" ppSpace colGt ident : command - -@[command_elab atlasStatusCmd] -def elabAtlasStatusCmd : CommandElab := fun stx => do - let ident := stx[1] - let userName : Name := ident.getId - let env ← getEnv - let cands := candidateInstanceNames userName - let nl : MessageData := Format.line - let header : MessageData := m!"#atlas.status {userName}" - match cands.find? env.contains with - | none => - let triedStr := String.intercalate ", " (cands.map toString) - let l1 : MessageData := m!" ✗ not registered: no instance theorem found." - let l2 : MessageData := m!" tried: {triedStr}" - logInfo (header ++ nl ++ l1 ++ nl ++ l2) - | some instName => - let axioms ← liftCoreM <| Lean.collectAxioms instName - let nAxioms := axioms.size - let otherAxioms := axioms.filter (· ≠ ``sorryAx) - let otherStr := - if otherAxioms.isEmpty then "none" - else String.intercalate ", " (otherAxioms.toList.map toString) - if axioms.contains ``sorryAx then - let l1 : MessageData := - m!" ⚠ in progress: `{instName}` exists but transitively uses `sorry`." - let l2 : MessageData := - m!" axioms used ({nAxioms}): sorryAx, {otherStr}" - logInfo (header ++ nl ++ l1 ++ nl ++ l2) - else - let l1 : MessageData := - m!" ✓ certified: `{instName}` is sorry-free." - let l2 : MessageData := - m!" axioms used ({nAxioms}): {otherStr}" - logInfo (header ++ nl ++ l1 ++ nl ++ l2) - -end ProofAtlas.Command diff --git a/lean/ProofAtlas/Demos/Paper.lean b/lean/ProofAtlas/Demos/Paper.lean deleted file mode 100644 index 6490e4b..0000000 --- a/lean/ProofAtlas/Demos/Paper.lean +++ /dev/null @@ -1,132 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Util.Linter.MathTag -import ProofAtlas.Hypergraph.Basic -import Mathlib.Tactic.IntervalCases -import Mathlib.Tactic.FinCases - -/-! -# Paper examples of BDF hypergraphs - -Concrete BDF hypergraphs referenced by figures and examples in the paper. - -## Main definitions - -* `Figure1.hypergraph` — the two-hyperedge BDF hypergraph drawn in - Figure 1: `e1 = (v1, v2; v3)` of colour `red` and - `e2 = (v3, v4; v5)` of colour `blue`. --/ - -namespace Figure1 - -inductive Vertex where - | v1 | v2 | v3 | v4 | v5 - deriving DecidableEq, Repr - -inductive Edge where - | e1 | e2 - deriving DecidableEq, Repr - -inductive Color where - | red | blue - deriving DecidableEq, Repr - -/-- **Math.** Topological rank witnessing acyclicity of Figure 1: -`v1, v2, v4 ↦ 0`, `v3 ↦ 1`, `v5 ↦ 2`. -/ -private def figRank : Vertex → Nat - | .v1 => 0 - | .v2 => 0 - | .v3 => 1 - | .v4 => 0 - | .v5 => 2 - -/-- **Math.** Per-edge bundle for Figure 1 — reducible so projection -arithmetic (`Fin (figEdge .e1).inArity = Fin 2`) collapses during -type-class synthesis and `fin_cases`. -/ -@[reducible] -private def figEdge : Edge → Hyperedge Vertex Color - | .e1 => - { inArity := 2 - inVertex := fun i => match i.val with | 0 => .v1 | _ => .v2 - outArity := 1 - outVertex := fun _ => .v3 - color := .red - inArity_pos := by decide - outArity_pos := by decide - inputOutputDisjoint := by decide - inVertex_injective := by - rintro ⟨a, ha⟩ ⟨b, hb⟩ hab - apply Fin.ext - interval_cases a <;> interval_cases b <;> - first | rfl | (exfalso; revert hab; simp) - outVertex_injective := fun a b _ => Subsingleton.elim a b } - | .e2 => - { inArity := 2 - inVertex := fun i => match i.val with | 0 => .v3 | _ => .v4 - outArity := 1 - outVertex := fun _ => .v5 - color := .blue - inArity_pos := by decide - outArity_pos := by decide - inputOutputDisjoint := by decide - inVertex_injective := by - rintro ⟨a, ha⟩ ⟨b, hb⟩ hab - apply Fin.ext - interval_cases a <;> interval_cases b <;> - first | rfl | (exfalso; revert hab; simp) - outVertex_injective := fun a b _ => Subsingleton.elim a b } - -/-- **Math.** The two-hyperedge BDF hypergraph drawn in Figure 1 of -the paper: `e1 = (v1, v2; v3)` red, `e2 = (v3, v4; v5)` blue. -/ -def hypergraph : Hypergraph Vertex Color where - edges := Edge - edge := figEdge - acyclic := by - apply Hypergraph.IsAcyclic.of_natRank figRank - rintro v w ⟨e, hv, hw⟩ - cases e - all_goals { - obtain ⟨⟨i, hi⟩, rfl⟩ := hv - obtain ⟨⟨j, hj⟩, rfl⟩ := hw - have hi' : i < 2 := hi - have hj' : j < 1 := hj - interval_cases i <;> interval_cases j <;> - (simp only [figRank]; decide) - } - connected := by - -- Hub-via-v3 connectivity: every vertex is one step from v3. - -- v1, v2 ~ v3 via e1; v4, v5 ~ v3 via e2; v3 ~ v3 (refl). - -- For (u, v): u →* v3 →* v via trans + symmetry of base. - set R : Vertex → Vertex → Prop := fun a b => ∃ e : Edge, - ((∃ i, (figEdge e).inVertex i = a) ∨ (∃ j, (figEdge e).outVertex j = a)) ∧ - ((∃ i, (figEdge e).inVertex i = b) ∨ (∃ j, (figEdge e).outVertex j = b)) - have hR_symm : Symmetric R := by - rintro a b ⟨e, ha, hb⟩; exact ⟨e, hb, ha⟩ - have h_to_v3 : ∀ u : Vertex, Relation.ReflTransGen R u .v3 := by - intro u - match u with - | .v1 => - refine Relation.ReflTransGen.single ⟨.e1, ?_, ?_⟩ - · exact Or.inl ⟨⟨0, by decide⟩, rfl⟩ - · exact Or.inr ⟨⟨0, by decide⟩, rfl⟩ - | .v2 => - refine Relation.ReflTransGen.single ⟨.e1, ?_, ?_⟩ - · exact Or.inl ⟨⟨1, by decide⟩, rfl⟩ - · exact Or.inr ⟨⟨0, by decide⟩, rfl⟩ - | .v3 => exact Relation.ReflTransGen.refl - | .v4 => - refine Relation.ReflTransGen.single ⟨.e2, ?_, ?_⟩ - · exact Or.inl ⟨⟨1, by decide⟩, rfl⟩ - · exact Or.inl ⟨⟨0, by decide⟩, rfl⟩ - | .v5 => - refine Relation.ReflTransGen.single ⟨.e2, ?_, ?_⟩ - · exact Or.inr ⟨⟨0, by decide⟩, rfl⟩ - · exact Or.inl ⟨⟨0, by decide⟩, rfl⟩ - intro u v - exact (h_to_v3 u).trans - (Relation.ReflTransGen.symmetric hR_symm (h_to_v3 v)) - -end Figure1 diff --git a/lean/ProofAtlas/Demos/Pipeline.lean b/lean/ProofAtlas/Demos/Pipeline.lean deleted file mode 100644 index 071ebd0..0000000 --- a/lean/ProofAtlas/Demos/Pipeline.lean +++ /dev/null @@ -1,134 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Mapping.Environment -import ProofAtlas.Pipeline.Linalg -import ProofAtlas.Pipeline.Hyperbolicity -import ProofAtlas.Command.Default - -/-! -# Pipeline sanity demo - -Smoke-test the `Lean.Expr → IncidenceData` converter on a small array -of hand-crafted expressions. Verifies const-sharing actually shares. --/ - -namespace ProofAtlas.Demos.Pipeline - -open ProofAtlas.Mapping - -open Lean (Expr) -open ProofAtlas.Pipeline - -/-- **Eng.** `id Nat` — 3 subterm-occurrences, 2 unique consts (id, Nat). -/ -def demo₁ : Expr := .app (.const ``id [.zero]) (.const ``Nat []) - -/-- **Eng.** `Nat.succ Nat.zero` — 3 subterm-occurrences, 2 unique consts. -/ -def demo₂ : Expr := .app (.const ``Nat.succ []) (.const ``Nat.zero []) - -/-- **Eng.** `id Nat` AND `Nat.succ Nat.zero`. Without const-sharing: 6 -distinct vertices. With const-sharing on `Nat`, they'd both reference -the same `Nat` vertex — but `demo₂` doesn't mention `Nat` directly, -so sharing only kicks in if a const is repeated across decls. -/ -def demoEnv : Array Expr := #[demo₁, demo₂] - --- Should print 6 vertices (3 + 3), 2 app-edges, no sharing. -#eval do - let (data, roots) := Exprs.toIncidenceData demoEnv - IO.println s!"vertices: {data.numVertices}" - IO.println s!"edges: {data.numEdges}" - IO.println s!"roots: {roots}" - IO.println s!"labels: {data.vertexLabel}" - -/-- **Eng.** Two expressions sharing `Nat`: `id Nat` and `id Nat` -(literally the same). Should produce const-sharing on both `id` and -`Nat`. -/ -def demoEnv₂ : Array Expr := #[demo₁, demo₁] - --- Should show fewer vertices than 2 × 3 = 6 due to sharing. -#eval do - let (data, roots) := Exprs.toIncidenceData demoEnv₂ - IO.println s!"shared-env vertices: {data.numVertices}" - IO.println s!"shared-env edges: {data.numEdges}" - IO.println s!"shared-env roots: {roots}" - --- *Resistance matrix smoke test* on `id Nat`: 3 vertices, 1 edge --- (the app). The hypergraph is a single triangle; resistance distance --- between any pair should be 2/3 ≈ 0.667 (standard formula for the --- resistance on a unit-conductance triangle). -#eval do - let (data, _) := Exprs.toIncidenceData #[demo₁] - IO.println s!"laplacian:" - IO.println s!" {data.laplacian}" - IO.println s!"resistance:" - for row in data.resistanceMatrix do - IO.println s!" {row}" - IO.println s!"δ-hyperbolicity: {data.hyperbolicity}" - --- *End-to-end smoke test* on a tiny "synthetic environment" of two --- expressions sharing the `Nat` const. Expected: 4 vertices, 2 edges, --- one connected component, δ ≈ 0 (bow-tie graph is tree-like). -#eval do - let report := geometricProfileOfExprs - #[Lean.Expr.app (.const ``id [.zero]) (.const ``Nat []), - Lean.Expr.app (.const ``Nat.succ []) (.const ``Nat [])] - IO.println report.summary - --- *Richer end-to-end profile.* A larger toy environment exercising --- nested `app` nodes and multi-decl const-sharing. -#eval do - -- decl₁ := id Nat - -- decl₂ := Nat.succ Nat.zero - -- decl₃ := id (Nat.succ Nat.zero) — references `id` and `Nat.succ`, - -- `Nat.zero` all reused from decls 1-2 via const-sharing. - let decl₁ := Lean.Expr.app (.const ``id [.zero]) (.const ``Nat []) - let decl₂ := Lean.Expr.app (.const ``Nat.succ []) (.const ``Nat.zero []) - let decl₃ := Lean.Expr.app (.const ``id [.zero]) - (.app (.const ``Nat.succ []) (.const ``Nat.zero [])) - let report := geometricProfileOfExprs #[decl₁, decl₂, decl₃] - IO.println report.summary - IO.println s!" labels: {report.incidence.vertexLabel}" - IO.println s!" resistance:" - for row in report.resistance do - IO.println s!" {row}" - -end ProofAtlas.Demos.Pipeline - --- *Smoke tests for the `#atlas.*` command suite.* --- The command resolves the identifier(s) through the current --- namespace, runs the pipeline, and logs the result. Multi-ident --- commands glue the proof terms via const-sharing. - --- Structure: BDF hypergraph viewer (the only command that renders a widget). -#atlas.graph Nat.add_zero - --- Combined text report. -#atlas.profile Nat.add_zero - --- Multi-decl const-sharing: how close are two propositions? -#atlas.resistance Nat.add_zero Nat.add_comm - --- Hausdorff distance between two declaration sets under resistance. -#atlas.hausdorff.resistance Nat.add_zero | Nat.add_comm Nat.succ_eq_add_one - --- Hyperbolicity of the glued graph. -#atlas.delta Nat.add_zero Nat.add_comm Nat.succ_eq_add_one - --- Decl-level hypergraph: vertices = declarations, hyperedges = maximal --- app spines (co-references). Tiny namespace for the smoke test. -#atlas.graph.ns ProofAtlas.Pipeline - --- Decl-level Gromov δ on the entire namespace's co-reference graph. --- Reports δ, diameter, and the normalised tree-likeness ratio δ/diam. -#atlas.delta.ns ProofAtlas.Pipeline - --- *Quality / `IsHypergraphMetric` registry status.* Reports for each registered --- BDF metric whether its `_isHypergraphMetric` instance theorem exists and --- whether it is `sorry`-free (via `Lean.collectAxioms`). -#atlas.status resistanceDist -- expected: ✓ certified -#atlas.status commuteTimeDist -- expected: ✓ certified -#atlas.status jsDist -- expected: ⚠ in progress (sorry) -#atlas.status diffusionDist -- expected: ⚠ in progress (sorry) -#atlas.status someUndefinedMetric -- expected: ✗ not registered diff --git a/lean/ProofAtlas/Export/Demos.lean b/lean/ProofAtlas/Export/Demos.lean deleted file mode 100644 index a84a01a..0000000 --- a/lean/ProofAtlas/Export/Demos.lean +++ /dev/null @@ -1,180 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import Lean -import ProofAtlas.Mapping.Environment - -/-! -# `lake exe atlas-export-demos` — landing-page graph exporter - -Walks each demo's declaration list through the real -`Exprs.toIncidenceData` pipeline (the same one `#atlas.graph` runs -inside Lean) and writes the resulting bipartite hypergraph data as -JSON so the landing-page demo on the website can render the *real* -graphs instead of a hand-laid mock. - -Output schema (`atlas-demos.json`): - -``` -[ - { - "id": "resistance", - "idents": ["Nat.add_zero", "Nat.add_comm"], - "graph": { - "vertices": [ - { "id": "v0", "kind": "circle", "color": "", "title": "..." }, - { "id": "e3", "kind": "rect", "color": "#d63031", "title": "app" }, - ... - ], - "edges": [ - { "source": "v0", "target": "e3" }, - { "source": "e3", "target": "v5" }, - ... - ] - } - }, - ... -] -``` - -Vertex IDs follow `Pipeline.IncidenceData.toAtlasGraphProps` from -`ProofAtlas.Tactic.Widget`; that file isn't imported here because it -pulls in ProofWidgets, but the encoding is identical. - -Run with: `lake exe atlas-export-demos [out-path]`. The default output -path is `../site/data/atlas-demos.json` relative to the `lean/` -working directory, so the site can statically import it. --/ - -open Lean -open ProofAtlas.Pipeline -open ProofAtlas.Mapping (ExprColor) - -namespace ProofAtlas.Export.Demos - -/-- Hyperedge-rectangle fill colour per `ExprColor`. Mirrors -`Pipeline.exprColorCss` (re-localized to avoid pulling the widget -module's ProofWidgets transitive dependency). -/ -private def exprColorCss : ExprColor → String - | .bvar => "#888888" - | .fvar => "#888888" - | .mvar => "#888888" - | .sort => "#74b9ff" - | .const => "#0984e3" - | .lit => "#fdcb6e" - | .app => "#d63031" - | .lam => "#00b894" - | .forallE => "#6c5ce7" - | .letE => "#0abde3" - | .mdata => "#b2bec3" - | .proj => "#a0522d" - -private def exprColorName : ExprColor → String - | .bvar => "bvar" - | .fvar => "fvar" - | .mvar => "mvar" - | .sort => "sort" - | .const => "const" - | .lit => "lit" - | .app => "app" - | .lam => "lam" - | .forallE => "forallE" - | .letE => "letE" - | .mdata => "mdata" - | .proj => "proj" - -/-- Bipartite-graph JSON: subterm vertices as circles, hyperedges as -coloured rects. -/ -def incidenceToJson (H : IncidenceData) : Json := Id.run do - let mut vs : Array Json := #[] - for i in [:H.numVertices] do - let label := H.vertexLabel[i]?.getD s!"v{i}" - vs := vs.push <| Json.mkObj [ - ("id", Json.str s!"v{i}"), - ("kind", Json.str "circle"), - ("color", Json.str ""), - ("title", Json.str label) - ] - for h : j in [:H.edges.size] do - let e := H.edges[j] - vs := vs.push <| Json.mkObj [ - ("id", Json.str s!"e{j}"), - ("kind", Json.str "rect"), - ("color", Json.str (exprColorCss e.color)), - ("title", Json.str (exprColorName e.color)) - ] - let mut es : Array Json := #[] - for h : j in [:H.edges.size] do - let e := H.edges[j] - for i in e.inputs do - es := es.push <| Json.mkObj [ - ("source", Json.str s!"v{i}"), - ("target", Json.str s!"e{j}") - ] - es := es.push <| Json.mkObj [ - ("source", Json.str s!"e{j}"), - ("target", Json.str s!"v{e.output}") - ] - return Json.mkObj [ - ("vertices", Json.arr vs), - ("edges", Json.arr es) - ] - -/-- One landing-page demo: short stable id + the declarations to glue. -/ -structure DemoSpec where - id : String - idents : List Name - -/-- The four graph-bearing demos shown on the landing page. The fifth -demo (`#atlas.status`) is text-only and exports no graph. -/ -def demoSpecs : List DemoSpec := [ - { id := "graph", idents := [`Nat.add_zero] }, - { id := "resistance", idents := [`Nat.add_zero, `Nat.add_comm] }, - { id := "delta", - idents := [`Nat.add_zero, `Nat.add_comm, `Nat.succ_eq_add_one] }, - { id := "profile", idents := [`Nat.succ_eq_add_one] } -] - -/-- Run one demo through the pipeline; `none` if any declaration is -missing or is an axiom. -/ -def exportOne (env : Environment) (spec : DemoSpec) : Option Json := Id.run do - let mut exprs : Array Expr := #[] - for n in spec.idents do - match env.find? n with - | some info => - match info.value? (allowOpaque := true) with - | some v => exprs := exprs.push v - | none => return none - | none => return none - let (data, _) := Exprs.toIncidenceData exprs - return some <| Json.mkObj [ - ("id", Json.str spec.id), - ("idents", - Json.arr (spec.idents.toArray.map (fun n => Json.str (toString n)))), - ("graph", incidenceToJson data) - ] - -end ProofAtlas.Export.Demos - -open ProofAtlas.Export.Demos in -def main (args : List String) : IO UInt32 := do - Lean.initSearchPath (← Lean.findSysroot) - -- `ProofAtlas` transitively imports Mathlib, which is where - -- `Nat.add_zero` / `Nat.add_comm` / `Nat.succ_eq_add_one` live. - let env ← Lean.importModules - #[(`ProofAtlas : Import)] {} (trustLevel := 0) - let mut results : Array Json := #[] - for spec in demoSpecs do - match exportOne env spec with - | some j => - results := results.push j - IO.println s!" ✓ {spec.id}: {spec.idents}" - | none => - IO.eprintln s!" ✗ {spec.id}: one of {spec.idents} missing or opaque" - let outPath := - args.head?.getD "../site/data/atlas-demos.json" - IO.FS.writeFile outPath ((Json.arr results).pretty) - IO.println s!"wrote {results.size} demo graph(s) → {outPath}" - return 0 diff --git a/lean/ProofAtlas/Export/Profile.lean b/lean/ProofAtlas/Export/Profile.lean deleted file mode 100644 index 36f3e9a..0000000 --- a/lean/ProofAtlas/Export/Profile.lean +++ /dev/null @@ -1,103 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import Lean -import ProofAtlas.Mapping.Environment -import ProofAtlas.Pipeline.Export - -/-! -# `lake exe atlas-export` — geometric profile JSON for arbitrary decls - -CLI for piping a declaration's (or a glued multi-declaration's) -`HypergraphReport` into external tools. Mirrors the in-editor -`#atlas.profile` / `#atlas.delta` / `#atlas.resistance` family, -but emits JSON to stdout (or to a file via `--out`) so shell -pipelines, CI snapshots, and analysis scripts can consume it -without scraping the Lean infoview. - -Usage: - -``` -lake exe atlas-export Nat.add_zero -lake exe atlas-export Nat.add_zero Nat.add_comm # glued -lake exe atlas-export --out profile.json Nat.succ_eq_add_one -``` - -Schema matches `Pipeline/Export.lean`'s `ToJson HypergraphReport`: - -``` -{ - "incidence": { "numVertices": Nat, "edges": [...], "vertexLabel": [...] }, - "resistance": [[Float, ...], ...], - "delta": Float, - "roots": [Nat, ...] -} -``` --/ - -open Lean -open ProofAtlas.Pipeline -open ProofAtlas.Mapping - -namespace ProofAtlas.Export.Profile - -structure Options where - /-- Output path; `none` means write to stdout. -/ - outPath : Option String := none - /-- Declaration names to profile (glued via const-sharing). -/ - idents : Array Name := #[] - -/-- Parse argv: optional `--out ` followed by one or more -declaration names. Returns `none` on misuse. -/ -def parseArgs (args : List String) : Option Options := - go args { idents := #[] } -where - go : List String → Options → Option Options - | [], opts => - if opts.idents.isEmpty then none else some opts - | "--out" :: p :: rest, opts => - go rest { opts with outPath := some p } - | "--help" :: _, _ => none - | arg :: rest, opts => - go rest { opts with idents := opts.idents.push arg.toName } - -def usage : String := - "usage: lake exe atlas-export [--out ] [ ...]" - -end ProofAtlas.Export.Profile - -open ProofAtlas.Export.Profile in -def main (args : List String) : IO UInt32 := do - let some opts := parseArgs args - | IO.eprintln usage; return 1 - Lean.initSearchPath (← Lean.findSysroot) - -- `ProofAtlas` transitively imports Mathlib; this is where the - -- declarations live whose value `Expr`s we feed the pipeline. - let env ← Lean.importModules - #[(`ProofAtlas : Import)] {} (trustLevel := 0) - -- Resolve every requested name to its body Expr. - let mut exprs : Array Expr := #[] - for n in opts.idents do - match env.find? n with - | some info => - match info.value? (allowOpaque := true) with - | some v => exprs := exprs.push v - | none => - IO.eprintln s!"error: {n} has no value (axiom/opaque?)" - return 1 - | none => - IO.eprintln s!"error: declaration {n} not found" - return 1 - -- Build the full report (incidence + resistance matrix + δ). - let report := geometricProfileOfExprs exprs - let json := Lean.toJson report - let str := json.pretty - match opts.outPath with - | some p => - IO.FS.writeFile p str - IO.eprintln s!"wrote {opts.idents.size} decl(s) → {p}" - | none => - IO.println str - return 0 diff --git a/lean/ProofAtlas/Hypergraph/Decidability.lean b/lean/ProofAtlas/Hypergraph/Decidability.lean deleted file mode 100644 index 5d3d317..0000000 --- a/lean/ProofAtlas/Hypergraph/Decidability.lean +++ /dev/null @@ -1,41 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Util.Linter.MathTag -import ProofAtlas.Hypergraph.Basic - -/-! -# Decidability of vertex/hyperedge incidence - -**Eng.** Type-theoretic plumbing supplying `Decidable` instances for -`Hypergraph.isInput`, `Hypergraph.isOutput`, and -`Hypergraph.incident`, conditional on `DecidableEq V`. Required -for `if`-then-else and `decide` tactic applications downstream. The -paper does not state these as separate facts; they are required only -by Lean. --/ - -namespace Hypergraph - -universe u v -variable {V : Type u} {C : Type v} - -/-- **Eng.** Decidability of `isInput v e`, via the standard -`Fintype` existential search over `Fin (H.inArity e)`. -/ -instance isInput.decidable [DecidableEq V] (H : Hypergraph V C) - (v : V) (e : H.edges) : Decidable (H.isInput v e) := - inferInstanceAs (Decidable (∃ i : Fin (H.inArity e), H.inVertex e i = v)) - -/-- **Eng.** Decidability of `isOutput v e`. -/ -instance isOutput.decidable [DecidableEq V] (H : Hypergraph V C) - (v : V) (e : H.edges) : Decidable (H.isOutput v e) := - inferInstanceAs (Decidable (∃ i : Fin (H.outArity e), H.outVertex e i = v)) - -/-- **Eng.** Decidability of `incident v e`. -/ -instance incident.decidable [DecidableEq V] (H : Hypergraph V C) - (v : V) (e : H.edges) : Decidable (H.incident v e) := - inferInstanceAs (Decidable (H.isInput v e ∨ H.isOutput v e)) - -end Hypergraph diff --git a/lean/ProofAtlas/Hypergraph/ReductionDemo.lean b/lean/ProofAtlas/Hypergraph/ReductionDemo.lean deleted file mode 100644 index 340cf2e..0000000 --- a/lean/ProofAtlas/Hypergraph/ReductionDemo.lean +++ /dev/null @@ -1,125 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Hypergraph.ToSimpleGraph -import ProofAtlas.Hypergraph.ToDigraph -import Mathlib.Tactic.FinCases - -/-! -# Smoke test for the `Hypergraph` reductions - -A 3-vertex hypergraph with one hyperedge in modus-ponens shape -(inputs `0`, `1`; output `2`). Used to check the basic semantics of -`toSimpleGraph` (full 2-section) and `toDigraph` (oriented 2-section). - -Both reductions should agree that `0 ⟶ 2` exists. They should -diverge on `0 — 1`: present in the 2-section (both are inputs of -the same edge), absent in the digraph (no input → output path -between two inputs). --/ - -namespace Hypergraph.ReductionDemo - -open Hypergraph - -/-- **Eng.** The single modus-ponens hyperedge: inputs `(0, 1)`, -output `(2)`. -/ -private def modusInVert : Fin 2 → Fin 3 - | 0 => 0 - | 1 => 1 - -private def modusOutVert : Fin 1 → Fin 3 - | 0 => 2 - -@[reducible] private def modusEdge : Hyperedge (Fin 3) Unit where - inArity := 2 - inVertex := modusInVert - outArity := 1 - outVertex := modusOutVert - color := () - inArity_pos := by decide - outArity_pos := by decide - inputOutputDisjoint := by - intro i j - fin_cases i <;> fin_cases j <;> decide - inVertex_injective := by - intro i j h - fin_cases i <;> fin_cases j <;> - first | rfl | (simp [modusInVert] at h) - outVertex_injective := fun i j _ => Subsingleton.elim i j - -/-- **Eng.** The toy hypergraph: `Fin 3` vertices, one hyperedge. -/ -@[reducible] def modusHyp : Hypergraph (Fin 3) Unit where - edges := Unit - edge := fun _ => modusEdge - acyclic := by - apply Hypergraph.IsAcyclic.of_natRank (Fin.val) - rintro v w ⟨_, ⟨i, hi⟩, ⟨j, hj⟩⟩ - fin_cases i <;> fin_cases j <;> - (simp only [modusInVert, modusOutVert] at hi hj - subst hi; subst hj; decide) - connected := by - intro u v - -- Every vertex is incident to the single hyperedge `()`, so any - -- pair takes one undirected step. - apply Relation.ReflTransGen.single - refine ⟨(), ?_, ?_⟩ - · fin_cases u - · exact .inl ⟨0, rfl⟩ - · exact .inl ⟨1, rfl⟩ - · exact .inr ⟨0, rfl⟩ - · fin_cases v - · exact .inl ⟨0, rfl⟩ - · exact .inl ⟨1, rfl⟩ - · exact .inr ⟨0, rfl⟩ - -/-! ## 2-section assertions (`toSimpleGraph`) -/ - -/-- **Math.** Vertices `0` and `1` are both inputs of the single -hyperedge — adjacent in the *full* 2-section. -/ -example : modusHyp.toSimpleGraph.Adj 0 1 := - ⟨by decide, (), .inl ⟨0, rfl⟩, .inl ⟨1, rfl⟩⟩ - -/-- **Math.** Vertex `0` (input) and `2` (output) are co-incident — -adjacent. -/ -example : modusHyp.toSimpleGraph.Adj 0 2 := - ⟨by decide, (), .inl ⟨0, rfl⟩, .inr ⟨0, rfl⟩⟩ - -/-- **Math.** 2-section is symmetric: `2 — 0` holds because `0 — 2` -does. -/ -example : modusHyp.toSimpleGraph.Adj 2 0 := - modusHyp.toSimpleGraph.symm ⟨by decide, (), .inl ⟨0, rfl⟩, .inr ⟨0, rfl⟩⟩ - -/-! ## Digraph assertions (`toDigraph`) -/ - -/-- **Math.** Input `0` reaches output `2`: a directed arrow exists. -/ -example : modusHyp.toDigraph.Adj 0 2 := - ⟨(), ⟨0, rfl⟩, ⟨0, rfl⟩⟩ - -/-- **Math.** Input `1` reaches output `2`: a directed arrow exists. -/ -example : modusHyp.toDigraph.Adj 1 2 := - ⟨(), ⟨1, rfl⟩, ⟨0, rfl⟩⟩ - -/-- **Math.** Inputs `0` and `1` are *not* connected in the digraph -(neither is an output of any edge containing the other as input). -/ -example : ¬ modusHyp.toDigraph.Adj 0 1 := by - rintro ⟨e, _, j, hj⟩ - -- Consume the `Unit` witness so `(modusHyp.edge e).outArity` reduces. - cases e - -- Now `j : Fin 1` and `hj : modusOutVert j = 1`, but `modusOutVert _ = 2`. - match j, hj with - | 0, h => exact absurd h (by decide) - -/-- **Math.** The arrow `2 ⟶ 0` does *not* exist (`2` is an output of -the edge, never an input). -/ -example : ¬ modusHyp.toDigraph.Adj 2 0 := by - rintro ⟨e, ⟨i, hi⟩, _⟩ - cases e - -- `i : Fin 2`; cases give `modusInVert i ∈ {0, 1}`, never `2`. - match i, hi with - | 0, h => exact absurd h (by decide) - | 1, h => exact absurd h (by decide) - -end Hypergraph.ReductionDemo diff --git a/lean/ProofAtlas/Hypergraph/ToDigraph.lean b/lean/ProofAtlas/Hypergraph/ToDigraph.lean deleted file mode 100644 index 2287d04..0000000 --- a/lean/ProofAtlas/Hypergraph/ToDigraph.lean +++ /dev/null @@ -1,83 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Hypergraph.Basic -import Mathlib.Combinatorics.Digraph.Basic - -/-! -# `Hypergraph.toDigraph` — the oriented 2-section - -Given a directed `Hypergraph V C`, the *oriented 2-section* is the -`Digraph V` whose arrows run from each input vertex of a hyperedge -to each output vertex of the same hyperedge: - - `u ⟶ v ↔ ∃ e, isInput u e ∧ isOutput v e` - -Equivalently, this is `Hypergraph.stepRel` packaged as a -`Mathlib.Combinatorics.Digraph`. Direction is preserved (unlike -`toSimpleGraph`), so the acyclicity axiom of the hypergraph -transfers to the digraph unchanged. - -Choice of target type: `Mathlib.Combinatorics.Digraph.Basic`'s -`structure Digraph (V) where Adj : V → V → Prop`. This is the -exact asymmetric counterpart of `SimpleGraph V` and the type -designed for "map an object to one specific directed-relation -graph" — see the discussion at the top of `Digraph/Basic.lean` -contrasting it with `Quiver`. - -The undirected sibling of this reduction (which forgets direction -but keeps only input-output adjacency) is *not* the same as -`toSimpleGraph`: `toSimpleGraph` uses the *full 2-section* -(every co-incident pair, including input-input and output-output). -The two reductions are related by -`toDigraph_adj_le_toSimpleGraph_adj`. --/ - -namespace Hypergraph - -universe u v w -variable {V : Type u} {C : Type v} - -/-- **Math.** The *oriented 2-section* of a hypergraph `H`: a -`Digraph` with an arrow `u ⟶ v` exactly when some hyperedge -has `u` as an input and `v` as an output. Equivalent to the -hypergraph's `stepRel`, packaged as a Mathlib `Digraph`. -/ -def toDigraph (H : Hypergraph V C) : Digraph V where - Adj u v := ∃ e : H.edges, H.isInput u e ∧ H.isOutput v e - -/-- **Math.** Unfold lemma for `toDigraph`: there is an arrow -`u ⟶ v` iff some hyperedge has `u` as an input and `v` as an -output. -/ -@[simp] theorem toDigraph_adj_iff (H : Hypergraph V C) (u v : V) : - H.toDigraph.Adj u v ↔ - ∃ e : H.edges, H.isInput u e ∧ H.isOutput v e := - Iff.rfl - -/-- **Math.** `toDigraph` has no self-loops: no vertex is both an -input and an output of the same hyperedge, since `Hyperedge` -requires inputs and outputs to be disjoint within a single edge -(BDF26 axiom D). -/ -theorem toDigraph_irrefl (H : Hypergraph V C) (v : V) : - ¬ H.toDigraph.Adj v v := by - rintro ⟨e, ⟨i, hi⟩, ⟨j, hj⟩⟩ - exact H.inputOutputDisjoint e i j (hi.trans hj.symm) - -/-- **Math.** Arrows in `toDigraph` agree with the hypergraph's -one-step inference relation `stepRel`. The two are definitionally -equal modulo `isInput` / `isOutput` unfolding; this restated form -is what downstream lemmas want for `rw`-style rewriting. -/ -theorem toDigraph_adj_eq_stepRel (H : Hypergraph V C) (u v : V) : - H.toDigraph.Adj u v ↔ Hypergraph.stepRel H.edge u v := - Iff.rfl - -/-- **Math.** Acyclicity transfers from the hypergraph to its -oriented 2-section: no vertex of `toDigraph` reaches itself via -iterated arrows. Direct corollary of `Hypergraph.acyclic`, since -the two relations agree (`toDigraph_adj_eq_stepRel`). -/ -theorem toDigraph_acyclic (H : Hypergraph V C) (v : V) : - ¬ Relation.TransGen H.toDigraph.Adj v v := - H.acyclic v - -end Hypergraph diff --git a/lean/ProofAtlas/Hypergraph/ToSimpleGraph.lean b/lean/ProofAtlas/Hypergraph/ToSimpleGraph.lean deleted file mode 100644 index 0521611..0000000 --- a/lean/ProofAtlas/Hypergraph/ToSimpleGraph.lean +++ /dev/null @@ -1,87 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Hypergraph.Basic -import Mathlib.Combinatorics.SimpleGraph.Basic - -/-! -# `Hypergraph.toSimpleGraph` — the 2-section - -Given a directed `Hypergraph V C`, the *full 2-section* is the -`SimpleGraph V` whose edges are *all* pairs of distinct vertices -that share a common hyperedge: - - `u ∼ v ↔ u ≠ v ∧ ∃ e, incident u e ∧ incident v e` - -Every hyperedge with incidence set `S` thus contributes the clique -on `S` to the 2-section. - -This is the *full* 2-section (any two co-incident vertices, regardless -of whether they are inputs or outputs). It matches the clique-expansion -convention already used by `Pipeline/Linalg.conductance`, so the -resistance / commute-time pipelines and the `SimpleGraph` view agree -on which pairs are adjacent. - -A separate *input-output* 2-section — the symmetric closure of the -directed `toDigraph` — is a different reduction and lives in -`ToDigraph.lean` as the underlying-simple-graph of the digraph there. - -## References - -* Voloshin, *Coloring of Mixed Hypergraphs* (2002), §1.2: the - 2-section construction. --/ - -namespace Hypergraph - -universe u v w -variable {V : Type u} {C : Type v} - -/-- **Math.** The *full 2-section* of a hypergraph `H`: a -`SimpleGraph` whose adjacency is "distinct + share some hyperedge". -Every hyperedge `e` with incidence set `S = inputs(e) ∪ outputs(e)` -contributes the clique on `S` to the resulting graph. -/ -def toSimpleGraph (H : Hypergraph V C) : SimpleGraph V where - Adj u v := u ≠ v ∧ ∃ e : H.edges, H.incident u e ∧ H.incident v e - symm := by - rintro u v ⟨hne, e, hu, hv⟩ - exact ⟨hne.symm, e, hv, hu⟩ - loopless := ⟨fun _ ⟨hne, _⟩ => hne rfl⟩ - -/-- **Math.** Unfold lemma for `toSimpleGraph`: `u` and `v` are -adjacent iff they are distinct and co-incident to some hyperedge. -/ -@[simp] theorem toSimpleGraph_adj_iff (H : Hypergraph V C) (u v : V) : - H.toSimpleGraph.Adj u v ↔ - u ≠ v ∧ ∃ e : H.edges, H.incident u e ∧ H.incident v e := - Iff.rfl - -/-- **Math.** Adjacency in `toSimpleGraph` is symmetric — restated -explicitly. (`SimpleGraph` carries this as a structure field, so it -holds by construction; the restated form is convenient for `simp`-style -rewrites that want to flip an adjacency.) -/ -theorem toSimpleGraph_adj_symm (H : Hypergraph V C) {u v : V} - (h : H.toSimpleGraph.Adj u v) : H.toSimpleGraph.Adj v u := - H.toSimpleGraph.symm h - -/-- **Math.** `toSimpleGraph` has no self-loops: a vertex is never -adjacent to itself. -/ -theorem toSimpleGraph_irrefl (H : Hypergraph V C) (v : V) : - ¬ H.toSimpleGraph.Adj v v := - fun ⟨hne, _⟩ => hne rfl - -/-- **Math.** Every input-output pair within a single hyperedge is -adjacent in the 2-section. Witnesses that the 2-section is *at -least as rich* as the digraph's symmetric closure. -/ -theorem toSimpleGraph_adj_of_input_output - (H : Hypergraph V C) {u v : V} {e : H.edges} - (hu : H.isInput u e) (hv : H.isOutput v e) : - H.toSimpleGraph.Adj u v := by - refine ⟨?_, e, .inl hu, .inr hv⟩ - rintro rfl - obtain ⟨i, hi⟩ := hu - obtain ⟨j, hj⟩ := hv - exact H.inputOutputDisjoint e i j (hi.trans hj.symm) - -end Hypergraph diff --git a/lean/ProofAtlas/Hypergraph/TransitionMatrix.lean b/lean/ProofAtlas/Hypergraph/TransitionMatrix.lean deleted file mode 100644 index 804b206..0000000 --- a/lean/ProofAtlas/Hypergraph/TransitionMatrix.lean +++ /dev/null @@ -1,317 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Util.Linter.MathTag -import ProofAtlas.Hypergraph.Decidability -import ProofAtlas.Hypergraph.Walk -import Mathlib.Algebra.BigOperators.Group.Finset.Basic -import Mathlib.Algebra.BigOperators.Ring.Finset -import Mathlib.Algebra.Order.BigOperators.Group.Finset -import Mathlib.Data.Fintype.Basic -import Mathlib.Data.Real.Basic -import Mathlib.Tactic.FieldSimp -import Mathlib.Tactic.Linarith -import Mathlib.Tactic.Ring - -/-! -# Transition matrix of the BDF random walk - -The role weight `w(v, e)`, per-vertex normaliser `Z(v)`, harmonic -input normaliser `S_e`, and the transition probability `p(v, u)` -of the BDF random walk. - -## Main definitions - -* `Hypergraph.roleWeight` — `w(v, e) = α(c(e)) · (1 if v ∈ e^in, β if v ∈ e^out)`. -* `Hypergraph.normalizer` — per-vertex normaliser `Z(v) = ∑_e w(v, e)`. -* `Hypergraph.harmonicNorm` — harmonic input normaliser - `S_e = ∑_{j=0}^{p_e - 1} 1/(j+1)`. -* `Hypergraph.transProb` — transition probability `p(v, u)`. - -## Main results (TODO) - -* `Hypergraph.transProb_nonneg` — `p(v, u) ≥ 0`. -* `Hypergraph.transProb_sum_one` — `∑_u p(v, u) = 1`. --/ - -namespace Hypergraph - -variable {V : Type*} {C : Type*} - -/-- **Math.** Role weight `w(v, e) = α(c(e)) · (1 if v ∈ e^in, β if v ∈ e^out)`. -/ -noncomputable def roleWeight [DecidableEq V] - (H : Hypergraph V C) (params : WalkParams C) (v : V) (e : H.edges) : ℝ := - if H.isInput v e then params.alpha (H.color e) - else if H.isOutput v e then params.alpha (H.color e) * params.beta - else 0 - -/-- **Math.** Per-vertex normaliser `Z(v) = ∑_e w(v, e)`. -/ -noncomputable def normalizer [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (v : V) : ℝ := - ∑ e : H.edges, H.roleWeight params v e - -/-- **Math.** Harmonic input normaliser `S_e = ∑_{j=0}^{p_e - 1} 1/(j+1)`. -/ -noncomputable def harmonicNorm (H : Hypergraph V C) (e : H.edges) : ℝ := - ∑ j : Fin (H.inArity e), (1 : ℝ) / ((j.val : ℝ) + 1) - -open Classical in -/-- **Math.** Transition probability `p(v, u)` of the BDF random walk. -/ -noncomputable def transProb [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (v u : V) : ℝ := - if H.normalizer params v = 0 then 0 - else (1 / H.normalizer params v) * - ∑ e : H.edges, - ((if H.isInput v e ∧ H.isOutput u e - then params.alpha (H.color e) / (H.outArity e : ℝ) - else 0) + - (∑ i : Fin (H.inArity e), - if H.isOutput v e ∧ H.inVertex e i = u - then params.alpha (H.color e) * params.beta / - (((i.val : ℝ) + 1) * H.harmonicNorm e) - else 0)) - -theorem transProb_nonneg [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (v u : V) : - 0 ≤ H.transProb params v u := by - unfold transProb - split_ifs with hZ - · exact le_refl 0 - · -- Normalizer is non-negative (sum of non-negative role-weights). - have h_norm_nn : 0 ≤ H.normalizer params v := by - unfold normalizer - apply Finset.sum_nonneg - intros e _ - unfold roleWeight - split_ifs - · exact (params.alpha_pos _).le - · exact mul_nonneg (params.alpha_pos _).le params.beta_bound.1.le - · exact le_refl 0 - have h_norm_pos : 0 < H.normalizer params v := - lt_of_le_of_ne h_norm_nn (Ne.symm hZ) - apply mul_nonneg - · exact (div_pos one_pos h_norm_pos).le - · apply Finset.sum_nonneg - intros e _ - apply add_nonneg - · split_ifs - · apply div_nonneg (params.alpha_pos _).le - exact_mod_cast (H.outArity_pos e).le - · exact le_refl 0 - · apply Finset.sum_nonneg - intros i _ - split_ifs - · apply div_nonneg - · exact mul_nonneg (params.alpha_pos _).le params.beta_bound.1.le - · apply mul_nonneg - · have : (0 : ℝ) ≤ (i.val : ℝ) := Nat.cast_nonneg _ - linarith - · unfold harmonicNorm - apply Finset.sum_nonneg - intros j _ - apply div_nonneg zero_le_one - have : (0 : ℝ) ≤ (j.val : ℝ) := Nat.cast_nonneg _ - linarith - · exact le_refl 0 - -/-- **Eng.** Per-edge forward sum: summing the forward indicator over -all target vertices `u` collapses to `α(c(e))` when `v ∈ e^in` (and -to `0` otherwise), provided the outputs of `e` are distinct. -/ -private lemma forwardSum_one_edge [Fintype V] [DecidableEq V] - (H : Hypergraph V C) (params : WalkParams C) - (v : V) (e : H.edges) - (h_out_inj : Function.Injective (H.outVertex e)) : - ∑ u : V, (if H.isInput v e ∧ H.isOutput u e - then params.alpha (H.color e) / (H.outArity e : ℝ) - else 0) - = if H.isInput v e then params.alpha (H.color e) else 0 := by - by_cases hi : H.isInput v e - · rw [if_pos hi] - simp_rw [show ∀ u : V, (H.isInput v e ∧ H.isOutput u e) ↔ H.isOutput u e - from fun _ => and_iff_right hi] - rw [← Finset.sum_filter] - have hfilter : - (Finset.univ.filter (fun u : V => H.isOutput u e)) - = Finset.image (H.outVertex e) Finset.univ := by - ext u - simp [Hypergraph.isOutput, Hyperedge.hasOutput] - rw [hfilter, Finset.sum_const, - Finset.card_image_of_injective Finset.univ h_out_inj, - Finset.card_univ, Fintype.card_fin, nsmul_eq_mul] - have hq : (H.outArity e : ℝ) ≠ 0 := by - exact_mod_cast (H.outArity_pos e).ne' - field_simp - · rw [if_neg hi] - apply Finset.sum_eq_zero - intros u _ - rw [if_neg] - intro hand - exact hi hand.1 - -/-- **Eng.** Per-edge backward sum: summing the harmonic-weighted -backward indicator over `u` (and over inputs `i`) collapses to -`α(c(e)) · β` when `v ∈ e^out` (and to `0` otherwise). The harmonic -weights cancel against the normaliser `S_e`. -/ -private lemma backwardSum_one_edge [Fintype V] [DecidableEq V] - (H : Hypergraph V C) (params : WalkParams C) - (v : V) (e : H.edges) : - ∑ u : V, ∑ i : Fin (H.inArity e), - (if H.isOutput v e ∧ H.inVertex e i = u - then params.alpha (H.color e) * params.beta / - (((i.val : ℝ) + 1) * H.harmonicNorm e) - else 0) - = if H.isOutput v e then params.alpha (H.color e) * params.beta - else 0 := by - by_cases ho : H.isOutput v e - · rw [if_pos ho] - simp_rw [show ∀ u : V, ∀ i : Fin (H.inArity e), - (H.isOutput v e ∧ H.inVertex e i = u) ↔ H.inVertex e i = u - from fun _ _ => and_iff_right ho] - rw [Finset.sum_comm] - -- For each `i`, the inner Σ_u picks out the unique `u = H.inVertex e i`. - have inner : ∀ i : Fin (H.inArity e), - (∑ u : V, if H.inVertex e i = u then - params.alpha (H.color e) * params.beta / - (((i.val : ℝ) + 1) * H.harmonicNorm e) - else 0) - = params.alpha (H.color e) * params.beta / - (((i.val : ℝ) + 1) * H.harmonicNorm e) := by - intro i - rw [Finset.sum_ite_eq Finset.univ (H.inVertex e i) - (fun _ => params.alpha (H.color e) * params.beta / - (((i.val : ℝ) + 1) * H.harmonicNorm e))] - exact if_pos (Finset.mem_univ _) - rw [Finset.sum_congr rfl (fun i _ => inner i)] - -- Goal: Σ_i α·β / ((i+1)·S_e) = α·β. - -- Positivity of `S_e = harmonicNorm e`. - have h_each_nn : ∀ j ∈ (Finset.univ : Finset (Fin (H.inArity e))), - (0 : ℝ) ≤ 1 / ((j.val : ℝ) + 1) := by - intros j _ - apply div_nonneg zero_le_one - have : (0 : ℝ) ≤ (j.val : ℝ) := Nat.cast_nonneg _ - linarith - have h_zero_pos : (0 : ℝ) < 1 / (((⟨0, H.inArity_pos e⟩ : Fin (H.inArity e)).val : ℝ) + 1) := by - simp - have h_Se_pos : 0 < H.harmonicNorm e := - Finset.sum_pos' h_each_nn - ⟨⟨0, H.inArity_pos e⟩, Finset.mem_univ _, h_zero_pos⟩ - have h_Se_ne : H.harmonicNorm e ≠ 0 := h_Se_pos.ne' - -- Identify `Σ_i 1/(i+1)` with `H.harmonicNorm e` (definitional). - have harm_def : (∑ i : Fin (H.inArity e), (1 : ℝ) / ((i.val : ℝ) + 1)) - = H.harmonicNorm e := rfl - -- Pull out the constant α·β: each term equals α·β · (1 / ((i+1)·S_e)). - rw [show (∑ i : Fin (H.inArity e), - params.alpha (H.color e) * params.beta / - (((i.val : ℝ) + 1) * H.harmonicNorm e)) - = params.alpha (H.color e) * params.beta * - ∑ i : Fin (H.inArity e), 1 / (((i.val : ℝ) + 1) * H.harmonicNorm e) - from by - rw [Finset.mul_sum] - apply Finset.sum_congr rfl - intros i _ - ring] - -- Now: α·β · Σ_i 1/((i+1)·S_e) = α·β. Suffices: Σ_i 1/((i+1)·S_e) = 1. - suffices h_sum_one : - (∑ i : Fin (H.inArity e), (1 : ℝ) / (((i.val : ℝ) + 1) * H.harmonicNorm e)) = 1 by - rw [h_sum_one]; ring - -- Σ_i 1/((i+1)·S_e) = (1/S_e) · Σ_i 1/(i+1) = (1/S_e) · S_e = 1. - rw [show (∑ i : Fin (H.inArity e), (1 : ℝ) / (((i.val : ℝ) + 1) * H.harmonicNorm e)) - = (1 / H.harmonicNorm e) * - ∑ i : Fin (H.inArity e), 1 / ((i.val : ℝ) + 1) - from by - rw [Finset.mul_sum] - apply Finset.sum_congr rfl - intros i _ - have h_i1 : ((i.val : ℝ) + 1) ≠ 0 := by - have : (0 : ℝ) ≤ (i.val : ℝ) := Nat.cast_nonneg _ - linarith - field_simp] - rw [harm_def] - field_simp - · rw [if_neg ho] - apply Finset.sum_eq_zero - intros u _ - apply Finset.sum_eq_zero - intros i _ - rw [if_neg] - intro hand - exact ho hand.1 - -/-- **Math.** `∑_u p(v, u) = 1`. Three hypotheses are needed: - -* `hZ : H.normalizer params v ≠ 0` — `v` is incident to at least one - hyperedge. -* `h_out_inj : ∀ e, Function.Injective (H.outVertex e)` — every - hyperedge has distinct outputs (so the forward sum is exactly - `α`, not `α · |distinct outputs| / q_e`). -* `h_acyclic : ∀ e, ¬(H.isInput v e ∧ H.isOutput v e)` — `v` is not - simultaneously input and output of any hyperedge (a local - consequence of the BDF acyclicity, currently not enforced by the - structure). Without this, the additive form - `[v ∈ e^in] · α + [v ∈ e^out] · α · β` would not match the - case-based `roleWeight`. -/ -theorem transProb_sum_one [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (v : V) - (hZ : H.normalizer params v ≠ 0) - (h_out_inj : ∀ e, Function.Injective (H.outVertex e)) - (h_acyclic : ∀ e, ¬(H.isInput v e ∧ H.isOutput v e)) : - ∑ u : V, H.transProb params v u = 1 := by - -- Step 1: each transProb takes the if-not-zero branch (uses `hZ`). - have h_each : ∀ u, H.transProb params v u = - (1 / H.normalizer params v) * - ∑ e : H.edges, - ((if H.isInput v e ∧ H.isOutput u e - then params.alpha (H.color e) / (H.outArity e : ℝ) - else 0) + - (∑ i : Fin (H.inArity e), - if H.isOutput v e ∧ H.inVertex e i = u - then params.alpha (H.color e) * params.beta / - (((i.val : ℝ) + 1) * H.harmonicNorm e) - else 0)) := by - intro u - unfold transProb - rw [if_neg hZ] - simp_rw [h_each] - -- Step 2: pull out 1/Z; swap Σ_u Σ_e. - rw [← Finset.mul_sum, Finset.sum_comm] - -- Step 3: per-edge identity. Distribute Σ_u over +, apply helpers, - -- collapse the two `if`s into `roleWeight` (uses `h_acyclic`). - have h_combine : ∀ e : H.edges, - ((if H.isInput v e then params.alpha (H.color e) else 0) + - (if H.isOutput v e then params.alpha (H.color e) * params.beta else 0)) - = H.roleWeight params v e := by - intro e - unfold roleWeight - by_cases hi : H.isInput v e - · have ho : ¬ H.isOutput v e := fun h => h_acyclic e ⟨hi, h⟩ - simp [hi, ho] - · by_cases ho : H.isOutput v e - · simp [hi, ho] - · simp [hi, ho] - have per_edge : ∀ e : H.edges, - ∑ u : V, - ((if H.isInput v e ∧ H.isOutput u e - then params.alpha (H.color e) / (H.outArity e : ℝ) - else 0) + - (∑ i : Fin (H.inArity e), - if H.isOutput v e ∧ H.inVertex e i = u - then params.alpha (H.color e) * params.beta / - (((i.val : ℝ) + 1) * H.harmonicNorm e) - else 0)) - = H.roleWeight params v e := by - intro e - rw [Finset.sum_add_distrib, - forwardSum_one_edge H params v e (h_out_inj e), - backwardSum_one_edge H params v e] - exact h_combine e - rw [Finset.sum_congr rfl (fun e _ => per_edge e)] - -- Step 4: Σ_e roleWeight = normalizer = Z (by def); then (1/Z) * Z = 1. - change (1 / H.normalizer params v) * H.normalizer params v = 1 - field_simp - -end Hypergraph diff --git a/lean/ProofAtlas/Hypergraph/Walk.lean b/lean/ProofAtlas/Hypergraph/Walk.lean deleted file mode 100644 index 14c40ac..0000000 --- a/lean/ProofAtlas/Hypergraph/Walk.lean +++ /dev/null @@ -1,25 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Util.Linter.MathTag -import Mathlib.Data.Real.Basic - -/-! -# Parameters of the BDF random walk - -The colour weight `α : C → ℝ` and the backward penalty `β ∈ (0, 1]` -controlling the BDF random walk on a BDF hypergraph. - -## Main definitions - -* `WalkParams C` — bundle of `α`, `β`, and the positivity / bound - hypotheses `0 < α c` and `0 < β ≤ 1`. --/ - -structure WalkParams (C : Type*) where - alpha : C → ℝ - beta : ℝ - alpha_pos : ∀ c, 0 < alpha c - beta_bound : 0 < beta ∧ beta ≤ 1 diff --git a/lean/ProofAtlas/LinearAlgebra/Default.lean b/lean/ProofAtlas/LinearAlgebra/Default.lean deleted file mode 100644 index 36684a0..0000000 --- a/lean/ProofAtlas/LinearAlgebra/Default.lean +++ /dev/null @@ -1,29 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.LinearAlgebra.MoorePenrose -import ProofAtlas.LinearAlgebra.EffectiveResistance -import ProofAtlas.LinearAlgebra.SpectralExpansion - -/-! -# Linear-algebra infrastructure for the v2 migration — facade - -Pseudoinverse, effective resistance, and spectral expansion utilities -that underpin the Laplacian-side reformulation of paper §6 and §7. - -* `LinearAlgebra.MoorePenrose` — Moore-Penrose pseudoinverse `A⁺` and - the four defining identities (currently sorry-stubbed). -* `LinearAlgebra.EffectiveResistance` — `R(u, v) = (e_u - e_v)ᵀ L⁺ (e_u - e_v)`, - the quadratic form associated to a weighted graph Laplacian. -* `LinearAlgebra.SpectralExpansion` — spectral expansion of `L⁺` and of - `R` in terms of the eigendata of `L`. - -This module is **v2 migration staging** — the eventual goal is that the -spectral identities of paper §6 (`interaction_spectral_form`, -`four_point_spectral_identity`) and the second-moment computation -behind paper §7 (`expected_sq_pair_sum_diff`, -`meanCommuteTime_eq_two_mul_kemenyConstant`) are proved against the -abstract `HypergraphMetric` structure using these Laplacian-side tools, -rather than against a specific random-walk construction. -/ diff --git a/lean/ProofAtlas/LinearAlgebra/EffectiveResistance.lean b/lean/ProofAtlas/LinearAlgebra/EffectiveResistance.lean deleted file mode 100644 index a8a16f9..0000000 --- a/lean/ProofAtlas/LinearAlgebra/EffectiveResistance.lean +++ /dev/null @@ -1,89 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Util.Linter.MathTag -import ProofAtlas.LinearAlgebra.MoorePenrose -import Mathlib.Data.Matrix.Mul -import Mathlib.LinearAlgebra.Matrix.PosDef - -/-! -# Effective resistance from a weighted graph Laplacian - -For a weighted graph Laplacian `L : Matrix V V ℝ`, the **effective -resistance** between two vertices `u` and `v` is the quadratic form - - `R(u, v) := (e_u - e_v)ᵀ · L⁺ · (e_u - e_v)` - -where `e_u - e_v : V → ℝ` is the signed indicator vector -(`+1` at `u`, `-1` at `v`, `0` elsewhere) and `L⁺` is the -Moore-Penrose pseudoinverse of `L`. - -The classical result of Doyle-Snell / Chandra-Raghavan-Ruzzo-Smolensky-Tiwari -identifies effective resistance with `(1/(2|E|))` times the commute time -of the random walk associated to the conductance: - - `commute_time(u, v) = 2 · (total edge weight) · R(u, v)`. - -Connecting this `effectiveResistance` to BDF's `commuteTimeDist` is the -goal of the v2 migration (Step 5). - -## Main definitions - -* `ProofAtlas.LinearAlgebra.effectiveResistance` — `(L, u, v) ↦ R(u, v)`. --/ - -namespace ProofAtlas.LinearAlgebra - -variable {V : Type*} [Fintype V] [DecidableEq V] - -/-- **Math.** Signed indicator vector `e_u - e_v : V → ℝ`, equal to -`+1` at `u`, `-1` at `v`, and `0` elsewhere — including `0` everywhere -when `u = v`. -/ -def signedIndicator (u v : V) : V → ℝ := - fun w => (if w = u then (1 : ℝ) else 0) - (if w = v then (1 : ℝ) else 0) - -/-- **Math.** Effective resistance between `u` and `v` in the weighted -graph with Laplacian `L`: - - `R(u, v) := (e_u - e_v)ᵀ · L⁺ · (e_u - e_v)`. - -Real body (no sorry) — direct evaluation of the quadratic form in -the pseudoinverse. The pseudoinverse itself is currently `sorry`'d -in `MoorePenrose`, so any concrete numeric calculation against -`effectiveResistance` would inherit that. -/ -noncomputable def effectiveResistance (L : Matrix V V ℝ) (u v : V) : ℝ := - let e := signedIndicator u v - ∑ w : V, e w * (L⁺.mulVec e) w - -/-- **Math.** `effectiveResistance` is symmetric: `R(u, v) = R(v, u)`. -Pure quadratic-form symmetry: `signedIndicator v u = -signedIndicator u v`, -and `eᵀ M e = (-e)ᵀ M (-e)` for any matrix `M`. No Hermitian / PSD -hypothesis is needed. -/ -theorem effectiveResistance_symm (L : Matrix V V ℝ) (u v : V) : - effectiveResistance L u v = effectiveResistance L v u := by - have he : signedIndicator v u = -signedIndicator u v := by - funext w - simp only [signedIndicator, Pi.neg_apply] - ring - simp only [effectiveResistance, he, Matrix.mulVec_neg, Pi.neg_apply, - neg_mul_neg] - -/-- **Math.** `effectiveResistance` is non-negative: `R(u, v) ≥ 0`. -Follows from `L` being PSD via `pseudoinverse_posSemidef`: the -quadratic form `xᵀ L⁺ x ≥ 0` because `L⁺` is itself PSD. -/ -theorem effectiveResistance_nonneg {L : Matrix V V ℝ} (hL : Matrix.PosSemidef L) - (u v : V) : 0 ≤ effectiveResistance L u v := by - have hLp : Matrix.PosSemidef L⁺ := pseudoinverse_posSemidef hL - simpa [effectiveResistance, dotProduct] - using hLp.dotProduct_mulVec_nonneg (signedIndicator u v) - -/-- **Math.** `effectiveResistance` of a vertex with itself is zero. -Follows from `signedIndicator u u = 0`. -/ -theorem effectiveResistance_self (L : Matrix V V ℝ) (u : V) : - effectiveResistance L u u = 0 := by - unfold effectiveResistance signedIndicator - simp [Matrix.mulVec, dotProduct] - -end ProofAtlas.LinearAlgebra diff --git a/lean/ProofAtlas/LinearAlgebra/MoorePenrose.lean b/lean/ProofAtlas/LinearAlgebra/MoorePenrose.lean deleted file mode 100644 index 2ac9b6d..0000000 --- a/lean/ProofAtlas/LinearAlgebra/MoorePenrose.lean +++ /dev/null @@ -1,475 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Util.Linter.MathTag -import Mathlib.Data.Matrix.Basic -import Mathlib.Data.Matrix.Mul -import Mathlib.LinearAlgebra.Matrix.Hermitian -import Mathlib.LinearAlgebra.Matrix.NonsingularInverse -import Mathlib.LinearAlgebra.Matrix.PosDef -import Mathlib.LinearAlgebra.Matrix.ConjTranspose -import Mathlib.Analysis.Matrix.PosDef -import Mathlib.Analysis.Matrix.Spectrum -import Mathlib.Data.Real.StarOrdered -import Mathlib.Algebra.Star.Basic -import Mathlib.Data.Real.Basic -import Mathlib.Analysis.RCLike.Basic - -/-! -# Moore-Penrose pseudoinverse of a real matrix - -For a real Hermitian matrix `A` with spectral decomposition -`A = U Λ Uᵀ` (where `U = hA.eigenvectorUnitary` is the orthogonal -matrix of eigenvectors and `Λ = diagonal hA.eigenvalues`), the -Moore-Penrose pseudoinverse is - - `A⁺ = U Λ⁺ Uᵀ`, - -with `Λ⁺ = diagonal (fun i => if λᵢ = 0 then 0 else 1/λᵢ)`. For -non-Hermitian `A`, this module returns the zero matrix as a -placeholder; the four Moore-Penrose identities are stated below -with an `A.IsHermitian` hypothesis. - -## Main definitions - -* `ProofAtlas.LinearAlgebra.pseudoinverse` — `A ↦ A⁺`. - -## Main results - -* `pseudoinverse_spectral_expansion` — entry-wise formula for `A⁺` - in the eigenbasis of `A`. -* `mul_pseudoinverse_mul_self`, `pseudoinverse_mul_self_mul_pseudoinverse`, - `mul_pseudoinverse_isHermitian`, `pseudoinverse_mul_self_isHermitian` — - the four Moore-Penrose identities (Hermitian case). -* `pseudoinverse_isHermitian_of_isHermitian` — `A⁺` is Hermitian. -* `pseudoinverse_posSemidef` — `A⁺` is PSD when `A` is. --/ - -open scoped Classical - -namespace ProofAtlas.LinearAlgebra - -variable {n : Type*} [Fintype n] [DecidableEq n] - -/-- **Math.** Diagonal of the pseudoinverse in the eigenbasis: -inverts the non-zero eigenvalues, zeroes the kernel directions. -/ -noncomputable def pseudoinverseDiag {A : Matrix n n ℝ} - (hA : A.IsHermitian) (i : n) : ℝ := - if hA.eigenvalues i = 0 then 0 else (hA.eigenvalues i)⁻¹ - -/-- **Math.** Moore-Penrose pseudoinverse of a real square matrix. - -Defined via spectral decomposition for Hermitian inputs: -`A⁺ := U · Λ⁺ · Uᵀ` where `U` is the orthogonal eigenvector matrix, -`Λ` is the diagonal of eigenvalues, and `Λ⁺` inverts the nonzero -entries. For non-Hermitian inputs, returns the zero matrix as a -placeholder. -/ -noncomputable def pseudoinverse (A : Matrix n n ℝ) : Matrix n n ℝ := - if hA : A.IsHermitian then - (hA.eigenvectorUnitary : Matrix n n ℝ) * - Matrix.diagonal (pseudoinverseDiag hA) * - Matrix.transpose (hA.eigenvectorUnitary : Matrix n n ℝ) - else 0 - -@[inherit_doc] postfix:1024 "⁺" => ProofAtlas.LinearAlgebra.pseudoinverse - -/-- **Math.** Spectral expansion: entry-wise formula for `A⁺ u v` -in the eigenbasis of `A`. -/ -theorem pseudoinverse_spectral_expansion {A : Matrix n n ℝ} - (hA : A.IsHermitian) (u v : n) : - A⁺ u v = ∑ i : n, - if hA.eigenvalues i ≠ 0 - then (hA.eigenvalues i)⁻¹ - * hA.eigenvectorBasis i u - * hA.eigenvectorBasis i v - else 0 := by - show pseudoinverse A u v = _ - unfold pseudoinverse - rw [dif_pos hA] - rw [Matrix.mul_apply] - refine Finset.sum_congr rfl (fun k _ => ?_) - rw [Matrix.mul_diagonal, Matrix.transpose_apply] - simp only [Matrix.IsHermitian.eigenvectorUnitary_apply, pseudoinverseDiag] - by_cases hev : hA.eigenvalues k = 0 - · simp [hev] - · rw [if_neg hev, if_pos hev] - ring - -/-- **Eng.** Spectral decomposition of a Hermitian real matrix in -matrix-product form: `A = U · Λ · Uᵀ`. -/ -private lemma isHermitian_eq_spectral {A : Matrix n n ℝ} (hA : A.IsHermitian) : - A = (hA.eigenvectorUnitary : Matrix n n ℝ) * - Matrix.diagonal hA.eigenvalues * - Matrix.transpose (hA.eigenvectorUnitary : Matrix n n ℝ) := by - conv_lhs => rw [hA.spectral_theorem, Unitary.conjStarAlgAut_apply] - rw [Matrix.star_eq_conjTranspose, Matrix.conjTranspose_eq_transpose_of_trivial, - RCLike.ofReal_real_eq_id, Function.id_comp] - -/-- **Eng.** Pseudoinverse in matrix-product form: `A⁺ = U · Λ⁺ · Uᵀ`. -/ -private lemma pseudoinverse_eq_spectral {A : Matrix n n ℝ} (hA : A.IsHermitian) : - A⁺ = (hA.eigenvectorUnitary : Matrix n n ℝ) * - Matrix.diagonal (pseudoinverseDiag hA) * - Matrix.transpose (hA.eigenvectorUnitary : Matrix n n ℝ) := by - show pseudoinverse A = _ - unfold pseudoinverse - rw [dif_pos hA] - -/-- **Eng.** Unitary identity: `Uᵀ · U = 1`. -/ -private lemma transpose_eigenvectorUnitary_mul_self - {A : Matrix n n ℝ} (hA : A.IsHermitian) : - Matrix.transpose (hA.eigenvectorUnitary : Matrix n n ℝ) * - (hA.eigenvectorUnitary : Matrix n n ℝ) = 1 := by - rw [← Matrix.conjTranspose_eq_transpose_of_trivial] - exact Matrix.mem_unitaryGroup_iff'.mp hA.eigenvectorUnitary.2 - -/-- **Eng.** Unitary identity: `U · Uᵀ = 1`. -/ -private lemma eigenvectorUnitary_mul_transpose_self - {A : Matrix n n ℝ} (hA : A.IsHermitian) : - (hA.eigenvectorUnitary : Matrix n n ℝ) * - Matrix.transpose (hA.eigenvectorUnitary : Matrix n n ℝ) = 1 := by - rw [← Matrix.conjTranspose_eq_transpose_of_trivial] - exact Matrix.mem_unitaryGroup_iff.mp hA.eigenvectorUnitary.2 - -/-- **Eng.** `Λ · Λ⁺ · Λ = Λ` pointwise as diagonals. -/ -private lemma eigenvalues_pseudo_eigenvalues_diag {A : Matrix n n ℝ} - (hA : A.IsHermitian) : - Matrix.diagonal hA.eigenvalues * - Matrix.diagonal (pseudoinverseDiag hA) * - Matrix.diagonal hA.eigenvalues = - Matrix.diagonal hA.eigenvalues := by - rw [Matrix.diagonal_mul_diagonal, Matrix.diagonal_mul_diagonal] - congr 1 - funext i - unfold pseudoinverseDiag - by_cases hev : hA.eigenvalues i = 0 - · simp [hev] - · rw [if_neg hev] - field_simp - -/-- **Eng.** `Λ⁺ · Λ · Λ⁺ = Λ⁺` pointwise as diagonals. -/ -private lemma pseudo_eigenvalues_pseudo_diag {A : Matrix n n ℝ} - (hA : A.IsHermitian) : - Matrix.diagonal (pseudoinverseDiag hA) * - Matrix.diagonal hA.eigenvalues * - Matrix.diagonal (pseudoinverseDiag hA) = - Matrix.diagonal (pseudoinverseDiag hA) := by - rw [Matrix.diagonal_mul_diagonal, Matrix.diagonal_mul_diagonal] - congr 1 - funext i - unfold pseudoinverseDiag - by_cases hev : hA.eigenvalues i = 0 - · simp [hev] - · rw [if_neg hev] - field_simp - -/-- **Eng.** Sandwich `U * X * Uᵀ` is Hermitian when `X` is. -/ -private lemma sandwich_isHermitian {A : Matrix n n ℝ} (hA : A.IsHermitian) - {X : Matrix n n ℝ} (hX : X.IsHermitian) : - ((hA.eigenvectorUnitary : Matrix n n ℝ) * X * - Matrix.transpose (hA.eigenvectorUnitary : Matrix n n ℝ)).IsHermitian := by - rw [← Matrix.conjTranspose_eq_transpose_of_trivial] - exact Matrix.isHermitian_mul_mul_conjTranspose _ hX - -/-- **Math.** First Moore-Penrose identity (Hermitian case): -`A · A⁺ · A = A`. -/ -theorem mul_pseudoinverse_mul_self {A : Matrix n n ℝ} (hA : A.IsHermitian) : - A * A⁺ * A = A := by - set U : Matrix n n ℝ := (hA.eigenvectorUnitary : Matrix n n ℝ) with hU_def - set Ut : Matrix n n ℝ := Matrix.transpose U with hUt_def - set Lam : Matrix n n ℝ := Matrix.diagonal hA.eigenvalues with hLam_def - set Lp : Matrix n n ℝ := Matrix.diagonal (pseudoinverseDiag hA) with hLp_def - have hUtU : Ut * U = 1 := transpose_eigenvectorUnitary_mul_self hA - have hA_eq : A = U * Lam * Ut := isHermitian_eq_spectral hA - have hAp_eq : A⁺ = U * Lp * Ut := pseudoinverse_eq_spectral hA - have hLamLpLam : Lam * Lp * Lam = Lam := eigenvalues_pseudo_eigenvalues_diag hA - calc A * A⁺ * A - = U * Lam * Ut * (U * Lp * Ut) * (U * Lam * Ut) := by - rw [hAp_eq, hA_eq] - _ = U * Lam * (Ut * U) * Lp * (Ut * U) * Lam * Ut := by - simp only [Matrix.mul_assoc] - _ = U * Lam * 1 * Lp * 1 * Lam * Ut := by rw [hUtU] - _ = U * (Lam * Lp * Lam) * Ut := by - rw [Matrix.mul_one, Matrix.mul_one]; simp only [Matrix.mul_assoc] - _ = U * Lam * Ut := by rw [hLamLpLam] - _ = A := hA_eq.symm - -/-- **Math.** Second Moore-Penrose identity (Hermitian case): -`A⁺ · A · A⁺ = A⁺`. -/ -theorem pseudoinverse_mul_self_mul_pseudoinverse - {A : Matrix n n ℝ} (hA : A.IsHermitian) : - A⁺ * A * A⁺ = A⁺ := by - set U : Matrix n n ℝ := (hA.eigenvectorUnitary : Matrix n n ℝ) with hU_def - set Ut : Matrix n n ℝ := Matrix.transpose U with hUt_def - set Lam : Matrix n n ℝ := Matrix.diagonal hA.eigenvalues with hLam_def - set Lp : Matrix n n ℝ := Matrix.diagonal (pseudoinverseDiag hA) with hLp_def - have hUtU : Ut * U = 1 := transpose_eigenvectorUnitary_mul_self hA - have hA_eq : A = U * Lam * Ut := isHermitian_eq_spectral hA - have hAp_eq : A⁺ = U * Lp * Ut := pseudoinverse_eq_spectral hA - have hLpLamLp : Lp * Lam * Lp = Lp := pseudo_eigenvalues_pseudo_diag hA - calc A⁺ * A * A⁺ - = U * Lp * Ut * (U * Lam * Ut) * (U * Lp * Ut) := by - rw [hAp_eq, hA_eq] - _ = U * Lp * (Ut * U) * Lam * (Ut * U) * Lp * Ut := by - simp only [Matrix.mul_assoc] - _ = U * Lp * 1 * Lam * 1 * Lp * Ut := by rw [hUtU] - _ = U * (Lp * Lam * Lp) * Ut := by - rw [Matrix.mul_one, Matrix.mul_one]; simp only [Matrix.mul_assoc] - _ = U * Lp * Ut := by rw [hLpLamLp] - _ = A⁺ := hAp_eq.symm - -/-- **Math.** Third Moore-Penrose identity (Hermitian case): -`A · A⁺` is Hermitian. -/ -theorem mul_pseudoinverse_isHermitian - {A : Matrix n n ℝ} (hA : A.IsHermitian) : - (A * A⁺).IsHermitian := by - set U : Matrix n n ℝ := (hA.eigenvectorUnitary : Matrix n n ℝ) with hU_def - set Ut : Matrix n n ℝ := Matrix.transpose U with hUt_def - set Lam : Matrix n n ℝ := Matrix.diagonal hA.eigenvalues - set Lp : Matrix n n ℝ := Matrix.diagonal (pseudoinverseDiag hA) - have hUtU : Ut * U = 1 := transpose_eigenvectorUnitary_mul_self hA - have hA_eq : A = U * Lam * Ut := isHermitian_eq_spectral hA - have hAp_eq : A⁺ = U * Lp * Ut := pseudoinverse_eq_spectral hA - have hAAp_eq : A * A⁺ = U * (Lam * Lp) * Ut := by - calc A * A⁺ = U * Lam * Ut * (U * Lp * Ut) := by rw [hAp_eq, hA_eq] - _ = U * Lam * (Ut * U) * Lp * Ut := by simp only [Matrix.mul_assoc] - _ = U * Lam * 1 * Lp * Ut := by rw [hUtU] - _ = U * (Lam * Lp) * Ut := by rw [Matrix.mul_one]; simp only [Matrix.mul_assoc] - rw [hAAp_eq] - refine sandwich_isHermitian hA ?_ - rw [Matrix.diagonal_mul_diagonal] - exact Matrix.isHermitian_diagonal _ - -/-- **Math.** Fourth Moore-Penrose identity (Hermitian case): -`A⁺ · A` is Hermitian. -/ -theorem pseudoinverse_mul_self_isHermitian - {A : Matrix n n ℝ} (hA : A.IsHermitian) : - (A⁺ * A).IsHermitian := by - set U : Matrix n n ℝ := (hA.eigenvectorUnitary : Matrix n n ℝ) with hU_def - set Ut : Matrix n n ℝ := Matrix.transpose U with hUt_def - set Lam : Matrix n n ℝ := Matrix.diagonal hA.eigenvalues - set Lp : Matrix n n ℝ := Matrix.diagonal (pseudoinverseDiag hA) - have hUtU : Ut * U = 1 := transpose_eigenvectorUnitary_mul_self hA - have hA_eq : A = U * Lam * Ut := isHermitian_eq_spectral hA - have hAp_eq : A⁺ = U * Lp * Ut := pseudoinverse_eq_spectral hA - have hApA_eq : A⁺ * A = U * (Lp * Lam) * Ut := by - calc A⁺ * A = U * Lp * Ut * (U * Lam * Ut) := by rw [hAp_eq, hA_eq] - _ = U * Lp * (Ut * U) * Lam * Ut := by simp only [Matrix.mul_assoc] - _ = U * Lp * 1 * Lam * Ut := by rw [hUtU] - _ = U * (Lp * Lam) * Ut := by rw [Matrix.mul_one]; simp only [Matrix.mul_assoc] - rw [hApA_eq] - refine sandwich_isHermitian hA ?_ - rw [Matrix.diagonal_mul_diagonal] - exact Matrix.isHermitian_diagonal _ - -/-- **Math.** The pseudoinverse of a Hermitian matrix is Hermitian. -/ -theorem pseudoinverse_isHermitian_of_isHermitian - {A : Matrix n n ℝ} (hA : A.IsHermitian) : A⁺.IsHermitian := by - rw [Matrix.IsHermitian.ext_iff] - intro i j - -- Goal: star (A⁺ j i) = A⁺ i j ; for ℝ, star = id. - rw [star_trivial, - pseudoinverse_spectral_expansion hA, - pseudoinverse_spectral_expansion hA] - refine Finset.sum_congr rfl (fun k _ => ?_) - by_cases hev : hA.eigenvalues k = 0 - · simp [hev] - · rw [if_pos hev, if_pos hev]; ring - -omit [Fintype n] [DecidableEq n] in -/-- **Eng.** For a real Hermitian matrix, the ordinary transpose -equals the matrix itself (`star = id` on `ℝ`). -/ -private lemma transpose_eq_of_isHermitian {M : Matrix n n ℝ} - (hM : M.IsHermitian) : Matrix.transpose M = M := by - rw [← Matrix.conjTranspose_eq_transpose_of_trivial] - exact hM - -/-- **Math.** Penrose 1955 uniqueness: any matrix satisfying the -four Moore–Penrose identities for a real Hermitian `A` equals `A⁺`. -The proof is the classical short chain — two transpose tricks using -the Hermicity of `A*M` and `M*A` (and likewise for `A*A⁺`, `A⁺*A`) -reduce everything to `M = M*A*M = (B*A)*M = B*(A*B) = B*A*B = B`, -where `B := A⁺`. -/ -theorem pseudoinverse_uniqueness - {A M : Matrix n n ℝ} (hA : A.IsHermitian) - (h1 : A * M * A = A) - (h2 : M * A * M = M) - (h3 : (A * M).IsHermitian) - (h4 : (M * A).IsHermitian) : - M = A⁺ := by - set B : Matrix n n ℝ := A⁺ with hB_def - -- The four MP identities for `B = A⁺`: - have hB1 : A * B * A = A := mul_pseudoinverse_mul_self hA - have hB2 : B * A * B = B := pseudoinverse_mul_self_mul_pseudoinverse hA - have hB3 : (A * B).IsHermitian := mul_pseudoinverse_isHermitian hA - have hB4 : (B * A).IsHermitian := pseudoinverse_mul_self_isHermitian hA - -- Symmetry (transpose = identity) for the Hermitian witnesses. - have hMAt : Matrix.transpose (M * A) = M * A := transpose_eq_of_isHermitian h4 - have hBAt : Matrix.transpose (B * A) = B * A := transpose_eq_of_isHermitian hB4 - have hAMt : Matrix.transpose (A * M) = A * M := transpose_eq_of_isHermitian h3 - have hABt : Matrix.transpose (A * B) = A * B := transpose_eq_of_isHermitian hB3 - -- Step 1: `M * A = B * A`. - -- (a) `(M * A) * (B * A) = M * A` using `A * B * A = A`. - have step1_left : (M * A) * (B * A) = M * A := by - calc (M * A) * (B * A) - = M * (A * B * A) := by simp only [Matrix.mul_assoc] - _ = M * A := by rw [hB1] - -- (b) `(B * A) * (M * A) = B * A` using `A * M * A = A`. - have step1_right : (B * A) * (M * A) = B * A := by - calc (B * A) * (M * A) - = B * (A * M * A) := by simp only [Matrix.mul_assoc] - _ = B * A := by rw [h1] - -- (c) Transposing (a): `((M*A)*(B*A))ᵀ = (M*A)ᵀ = M*A`, and the - -- LHS transposes by reversing the factors and using Hermicity. - have step1_swap : M * A = (B * A) * (M * A) := by - have h := congrArg Matrix.transpose step1_left - -- `h : ((M*A)*(B*A))ᵀ = (M*A)ᵀ`. - rw [Matrix.transpose_mul, hBAt, hMAt] at h - exact h.symm - have hMA : M * A = B * A := by - -- `M * A = (B*A)*(M*A) = B*A`. - rw [step1_swap, step1_right] - -- Step 2: `A * M = A * B` (mirror argument). - -- (a) `(A * M) * (A * B) = A * B`. - have step2_left : (A * M) * (A * B) = A * B := by - calc (A * M) * (A * B) - = (A * M * A) * B := by simp only [Matrix.mul_assoc] - _ = A * B := by rw [h1] - -- (b) `(A * B) * (A * M) = A * M`. - have step2_right : (A * B) * (A * M) = A * M := by - calc (A * B) * (A * M) - = (A * B * A) * M := by simp only [Matrix.mul_assoc] - _ = A * M := by rw [hB1] - -- (c) Transposing (a): `A*B = (A*B)*(A*M)`. - have step2_swap : A * B = (A * B) * (A * M) := by - have h := congrArg Matrix.transpose step2_left - -- `h : ((A*M)*(A*B))ᵀ = (A*B)ᵀ`. - rw [Matrix.transpose_mul, hABt, hAMt] at h - exact h.symm - have hAM : A * M = A * B := by - -- `A * M = (A*B)*(A*M) = A*B`. - rw [← step2_right, ← step2_swap] - -- Step 3: chain everything together. - calc M = M * A * M := h2.symm - _ = (M * A) * M := rfl - _ = (B * A) * M := by rw [hMA] - _ = B * (A * M) := by rw [Matrix.mul_assoc] - _ = B * (A * B) := by rw [hAM] - _ = B * A * B := by rw [Matrix.mul_assoc] - _ = B := hB2 - -/-- **Math.** Automorphism equivariance of the pseudoinverse. -If a permutation `π : n ≃ n` is a symmetry of a real Hermitian -matrix `A` (i.e., `A (π u) (π v) = A u v` entry-wise), then it is -also a symmetry of `A⁺`. The proof builds the permuted matrix -`M u v := A⁺ (π u) (π v)`, verifies it satisfies MP1–4 for `A` -via index reindexing (`Equiv.sum_comp π`), and concludes -`M = A⁺` by Penrose uniqueness. -/ -theorem pseudoinverse_aut - {A : Matrix n n ℝ} (hA : A.IsHermitian) (π : n ≃ n) - (h_eq : ∀ u v, A (π u) (π v) = A u v) : - ∀ u v, A⁺ (π u) (π v) = A⁺ u v := by - -- Define the permuted candidate matrix `M`. - set M : Matrix n n ℝ := fun u v => A⁺ (π u) (π v) with hM_def - -- We will show `M = A⁺` via `pseudoinverse_uniqueness`, then read off entries. - suffices h : M = A⁺ by - intro u v - have := congrFun (congrFun h u) v - -- `this : M u v = A⁺ u v`, and `M u v = A⁺ (π u) (π v)` by definition. - exact this - -- Useful entry-wise identities from the four MP identities for `A`. - have hAAp : A * A⁺ * A = A := mul_pseudoinverse_mul_self hA - have hApAAp : A⁺ * A * A⁺ = A⁺ := pseudoinverse_mul_self_mul_pseudoinverse hA - have hAAp_her : (A * A⁺).IsHermitian := mul_pseudoinverse_isHermitian hA - have hApA_her : (A⁺ * A).IsHermitian := pseudoinverse_mul_self_isHermitian hA - -- Entry-wise: `(A * M)(u, v) = (A * A⁺)(π u, π v)`. - have hAM_apply : ∀ u v, (A * M) u v = (A * A⁺) (π u) (π v) := by - intro u v - rw [Matrix.mul_apply, Matrix.mul_apply] - rw [← Equiv.sum_comp π (fun w => A (π u) w * A⁺ w (π v))] - refine Finset.sum_congr rfl (fun w _ => ?_) - show A u w * M w v = A (π u) (π w) * A⁺ (π w) (π v) - rw [hM_def, h_eq u w] - -- Entry-wise: `(M * A)(u, v) = (A⁺ * A)(π u, π v)`. - have hMA_apply : ∀ u v, (M * A) u v = (A⁺ * A) (π u) (π v) := by - intro u v - rw [Matrix.mul_apply, Matrix.mul_apply] - rw [← Equiv.sum_comp π (fun w => A⁺ (π u) w * A w (π v))] - refine Finset.sum_congr rfl (fun w _ => ?_) - show M u w * A w v = A⁺ (π u) (π w) * A (π w) (π v) - rw [hM_def, h_eq w v] - -- MP1 for M: `A * M * A = A`. - have hM1 : A * M * A = A := by - ext u v - have hLHS : (A * M * A) u v = (A * A⁺ * A) (π u) (π v) := by - rw [show A * M * A = A * (M * A) by rw [Matrix.mul_assoc]] - rw [show A * A⁺ * A = A * (A⁺ * A) by rw [Matrix.mul_assoc]] - rw [Matrix.mul_apply, Matrix.mul_apply] - rw [← Equiv.sum_comp π (fun w => A (π u) w * (A⁺ * A) w (π v))] - refine Finset.sum_congr rfl (fun w _ => ?_) - show A u w * (M * A) w v = A (π u) (π w) * (A⁺ * A) (π w) (π v) - rw [hMA_apply w v, h_eq u w] - rw [hLHS, hAAp, h_eq] - -- MP2 for M: `M * A * M = M`. - have hM2 : M * A * M = M := by - ext u v - have hLHS : (M * A * M) u v = (A⁺ * A * A⁺) (π u) (π v) := by - rw [show M * A * M = (M * A) * M from rfl, - show A⁺ * A * A⁺ = (A⁺ * A) * A⁺ from rfl] - rw [Matrix.mul_apply, Matrix.mul_apply] - rw [← Equiv.sum_comp π (fun w => (A⁺ * A) (π u) w * A⁺ w (π v))] - refine Finset.sum_congr rfl (fun w _ => ?_) - show (M * A) u w * M w v = (A⁺ * A) (π u) (π w) * A⁺ (π w) (π v) - rw [hMA_apply u w] - rw [hLHS, hApAAp] - -- MP3 for M: `(A * M).IsHermitian`. - have hM3 : (A * M).IsHermitian := by - rw [Matrix.IsHermitian.ext_iff] - intro i j - show star ((A * M) j i) = (A * M) i j - rw [star_trivial, hAM_apply, hAM_apply] - -- Now goal: `(A * A⁺) (π j) (π i) = (A * A⁺) (π i) (π j)`. - have := hAAp_her.apply (π i) (π j) - -- `this : star ((A * A⁺) (π j) (π i)) = (A * A⁺) (π i) (π j)`. - rw [star_trivial] at this - exact this - -- MP4 for M: `(M * A).IsHermitian`. - have hM4 : (M * A).IsHermitian := by - rw [Matrix.IsHermitian.ext_iff] - intro i j - show star ((M * A) j i) = (M * A) i j - rw [star_trivial, hMA_apply, hMA_apply] - have := hApA_her.apply (π i) (π j) - rw [star_trivial] at this - exact this - -- Apply Penrose uniqueness. - exact pseudoinverse_uniqueness hA hM1 hM2 hM3 hM4 - -/-- **Math.** PSD preservation: if `A` is positive semidefinite, so is `A⁺`. -Witnessed by congruence: `A⁺ = U Λ⁺ Uᵀ` with `Λ⁺` diagonal and -non-negative (since PSD `A` has non-negative eigenvalues, so the -inverted non-zero ones stay non-negative). -/ -theorem pseudoinverse_posSemidef - {A : Matrix n n ℝ} (hA : A.PosSemidef) : A⁺.PosSemidef := by - have hdiag_nonneg : 0 ≤ pseudoinverseDiag hA.isHermitian := by - intro i - unfold pseudoinverseDiag - split_ifs with hev - · exact le_refl 0 - · exact inv_nonneg.mpr (hA.eigenvalues_nonneg i) - have hLamPlus : - (Matrix.diagonal (pseudoinverseDiag hA.isHermitian)).PosSemidef := - Matrix.PosSemidef.diagonal hdiag_nonneg - have hAplus_eq : A⁺ = - (hA.isHermitian.eigenvectorUnitary : Matrix n n ℝ) * - Matrix.diagonal (pseudoinverseDiag hA.isHermitian) * - Matrix.conjTranspose - (hA.isHermitian.eigenvectorUnitary : Matrix n n ℝ) := by - show pseudoinverse A = _ - unfold pseudoinverse - rw [dif_pos hA.isHermitian, Matrix.conjTranspose_eq_transpose_of_trivial] - rw [hAplus_eq] - exact hLamPlus.mul_mul_conjTranspose_same _ - -end ProofAtlas.LinearAlgebra diff --git a/lean/ProofAtlas/LinearAlgebra/SpectralExpansion.lean b/lean/ProofAtlas/LinearAlgebra/SpectralExpansion.lean deleted file mode 100644 index 7258081..0000000 --- a/lean/ProofAtlas/LinearAlgebra/SpectralExpansion.lean +++ /dev/null @@ -1,106 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Util.Linter.MathTag -import ProofAtlas.LinearAlgebra.MoorePenrose -import ProofAtlas.LinearAlgebra.EffectiveResistance -import Mathlib.Analysis.Matrix.Spectrum - -/-! -# Spectral expansion of the pseudoinverse - -For a real Hermitian matrix `A` with eigendecomposition -`A = ∑_i λᵢ · φᵢ φᵢᵀ` (sum over the orthonormal eigenbasis), the -Moore-Penrose pseudoinverse admits the parallel expansion - - `A⁺ = ∑_{i : λᵢ ≠ 0} (1/λᵢ) · φᵢ φᵢᵀ`, - -inverting the non-zero eigenvalues and zeroing the kernel directions. -Combined with `effectiveResistance L u v = (e_u - e_v)ᵀ L⁺ (e_u - e_v)` -and orthonormality of `{φᵢ}`, this gives the standard spectral form - - `R(u, v) = ∑_{i : λᵢ ≠ 0} (φᵢ(u) - φᵢ(v))² / λᵢ`. - -This module records both expansions as `sorry`'d theorems with correct -signatures; the proofs are deferred to a follow-up session (they rely -on the `MoorePenrose.pseudoinverse` body, which is itself `sorry`'d at -this stage). - -## Main results (TODO: v2 migration) - -* `pseudoinverse_spectral_expansion` — `A⁺ u v = ∑_{λᵢ ≠ 0} (1/λᵢ) · φᵢ(u) · φᵢ(v)`. -* `effectiveResistance_spectral_expansion` — - `R(u, v) = ∑_{λᵢ ≠ 0} (φᵢ(u) - φᵢ(v))² / λᵢ`. --/ - -namespace ProofAtlas.LinearAlgebra - -variable {V : Type*} [Fintype V] [DecidableEq V] - -/-- **Math.** Spectral expansion of effective resistance: -`R(u, v) = ∑_{λᵢ ≠ 0} (φᵢ(u) - φᵢ(v))² / λᵢ`. - -Proven via `pseudoinverse_spectral_expansion` plus the signed-indicator -pairing identity `∑ w, e_{uv}(w) f(w) = f(u) - f(v)`. -/ -theorem effectiveResistance_spectral_expansion - {L : Matrix V V ℝ} (hL : L.IsHermitian) (u v : V) : - effectiveResistance L u v - = ∑ i : V, - if hL.eigenvalues i ≠ 0 - then (hL.eigenvectorBasis i u - hL.eigenvectorBasis i v) ^ 2 - / hL.eigenvalues i - else 0 := by - -- Signed-indicator pairing: ∑ w, sig u v w · f w = f u - f v. - have hSig : ∀ f : V → ℝ, ∑ w, signedIndicator u v w * f w = f u - f v := by - intro f - simp only [signedIndicator, sub_mul, ite_mul, one_mul, zero_mul, - Finset.sum_sub_distrib] - rw [Finset.sum_ite_eq' Finset.univ u f, Finset.sum_ite_eq' Finset.univ v f] - simp - have hSig' : ∀ f : V → ℝ, ∑ x, f x * signedIndicator u v x = f u - f v := by - intro f - simp_rw [mul_comm (f _)] - exact hSig f - -- (L⁺ ·ᵥ e) w as spectral sum collapsing inner ∑ x via hSig'. - have hmulVec : ∀ w, ((L⁺).mulVec (signedIndicator u v)) w = - ∑ i, if hL.eigenvalues i ≠ 0 - then (hL.eigenvalues i)⁻¹ * hL.eigenvectorBasis i w * - (hL.eigenvectorBasis i u - hL.eigenvectorBasis i v) - else 0 := by - intro w - change ∑ x, L⁺ w x * signedIndicator u v x = _ - simp_rw [pseudoinverse_spectral_expansion hL, Finset.sum_mul] - rw [Finset.sum_comm] - refine Finset.sum_congr rfl (fun i _ => ?_) - by_cases hev : hL.eigenvalues i = 0 - · simp [hev] - · simp only [if_pos hev] - have h_rw : ∀ x, - (hL.eigenvalues i)⁻¹ * hL.eigenvectorBasis i w * - hL.eigenvectorBasis i x * signedIndicator u v x - = (hL.eigenvalues i)⁻¹ * hL.eigenvectorBasis i w * - (hL.eigenvectorBasis i x * signedIndicator u v x) := fun x => by ring - simp_rw [h_rw, ← Finset.mul_sum, hSig'] - -- Final computation: pair the outer ∑ w against hSig. - unfold effectiveResistance - change (∑ w, signedIndicator u v w * - ((L⁺).mulVec (signedIndicator u v)) w) = _ - simp_rw [hmulVec, Finset.mul_sum] - rw [Finset.sum_comm] - refine Finset.sum_congr rfl (fun i _ => ?_) - by_cases hev : hL.eigenvalues i = 0 - · simp [hev] - · simp only [if_pos hev] - have h_rw : ∀ w, - signedIndicator u v w * ((hL.eigenvalues i)⁻¹ * - hL.eigenvectorBasis i w * - (hL.eigenvectorBasis i u - hL.eigenvectorBasis i v)) - = (hL.eigenvalues i)⁻¹ * - (hL.eigenvectorBasis i u - hL.eigenvectorBasis i v) * - (signedIndicator u v w * hL.eigenvectorBasis i w) := fun w => by ring - simp_rw [h_rw, ← Finset.mul_sum, hSig] - ring - -end ProofAtlas.LinearAlgebra diff --git a/lean/ProofAtlas/Mapping/Declaration.lean b/lean/ProofAtlas/Mapping/Declaration.lean deleted file mode 100644 index c5253fc..0000000 --- a/lean/ProofAtlas/Mapping/Declaration.lean +++ /dev/null @@ -1,226 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import Lean -import ProofAtlas.Pipeline.IncidenceData -import ProofAtlas.Mapping.ExprColor - -/-! -# Declaration-level hypergraph extraction - -Vertices are *declaration names*; hyperedges are *co-references*: -one hyperedge per **maximal application spine** inside some -declaration's proof term. For `f a₁ a₂ … aₖ` (curried in `Expr` as -`((f a₁) a₂) …`, but flattened via `Expr.getAppFn` / -`Expr.getAppArgs`), the hyperedge is - - inputs := { head's const name, each aᵢ's spine-head const name } - output := the declaration whose body contains this spine - color := single (`.app`) for v1. - -This matches the CIC typing rule for application: the head and the -immediate arg heads all participate in one typing judgment, so they -all live in one hyperedge. Deeper consts inside each `aᵢ` belong to -`aᵢ`'s own spine and get their own hyperedge from the recursive -walk. - -The output of `Environment.toDeclIncidence` is a plain -`IncidenceData`, so the existing `Linalg`, `Hyperbolicity`, and -widget-rendering machinery applies unchanged. Float-only for v1; -the proof-layer `Hypergraph Name DeclColor` is deferred until -the numerical layer shows the metric is meaningful. - -## Scope - -Vertex set = declarations whose name has `` as a strict -prefix, filtered to skip Lean-internal (`_`-prefixed, -auto-generated) constants. Input names that fall outside the -allowed set are dropped, so the resulting graph stays inside the -chosen namespace. Self-references (a decl appearing in its own -proof) are removed to satisfy `inputOutputDisjoint`. Duplicate -inputs in the same spine are deduped to satisfy -`inVertex_injective`. - -## Performance - -Walking every `Expr.app` subterm in every declaration is roughly -linear in the total proof-term size of the namespace; the resulting -`IncidenceData` then feeds the regular Float pipeline. A namespace -of a few hundred declarations runs in well under a second. --/ - -namespace ProofAtlas.Mapping - -open ProofAtlas.Pipeline - -open Lean ProofAtlas.Mapping - -/-! ## DeclColor — per-vertex colour by declaration kind -/ - -/-- **Eng.** Colour family for declaration vertices. Mirrors the -constructors of `Lean.ConstantInfo` that carry a meaningful -`ProofAtlas`-level distinction. -/ -inductive DeclColor where - | thm - | defn - | inst - | axiom_ - | opaque_ - | other - deriving DecidableEq, Repr - -/-- **Eng.** Short label for the legend. -/ -def DeclColor.name : DeclColor → String - | .thm => "theorem" - | .defn => "def" - | .inst => "instance" - | .axiom_ => "axiom" - | .opaque_ => "opaque" - | .other => "other" - -/-- **Eng.** CSS hex for the legend swatch + vertex fill. -/ -def DeclColor.css : DeclColor → String - | .thm => "#d63031" -- red - | .defn => "#0984e3" -- blue - | .inst => "#00b894" -- green - | .axiom_ => "#e67e22" -- orange - | .opaque_ => "#a0522d" -- brown - | .other => "#888888" -- gray - -/-- **Eng.** All `DeclColor` values, in legend display order. -/ -def DeclColor.palette : List DeclColor := - [.thm, .defn, .inst, .axiom_, .opaque_, .other] - -/-- **Eng.** Map a `ConstantInfo` to its `DeclColor`. Instance -vs def detection is deferred (requires the instance-attribute -state from `Lean.Meta.instanceExtension`); v1 lumps instances -into `.defn`. -/ -def declColorOf (_env : Environment) (_name : Name) - (info : ConstantInfo) : DeclColor := - match info with - | .thmInfo _ => .thm - | .defnInfo _ => .defn - | .axiomInfo _ => .axiom_ - | .opaqueInfo _ => .opaque_ - | _ => .other - -/-- **Eng.** Names of consts that appear at the *head* of the -spine of each argument in `e`, together with the head of `e` if it -is itself a `.const`. The result is dedup'd to satisfy -`inVertex_injective`. -/ -private def appSpineInputs (e : Expr) : Array Name := Id.run do - if !e.isApp then return #[] - let head := e.getAppFn - let args := e.getAppArgs - let mut raw : Array Name := #[] - if let .const n _ := head then raw := raw.push n - for a in args do - if let .const n _ := a.getAppFn then - raw := raw.push n - -- Dedup preserving order. - let mut seen : Std.HashSet Name := {} - let mut out : Array Name := #[] - for n in raw do - if !seen.contains n then - seen := seen.insert n - out := out.push n - return out - -/-- **Eng.** Walks an `Expr` and emits one `EdgeRec` per maximal -app spine encountered, after filtering inputs to `allowed` and -removing self-references to `outputName`. Recurses into the -arguments of each app (their own spines become their own edges) -and into the bodies of binders. -/ -private partial def collectDeclEdges - (allowed : Std.HashSet Name) (declToVid : Std.HashMap Name Nat) - (outputName : Name) (e : Expr) : Array EdgeRec := Id.run do - let mut edges : Array EdgeRec := #[] - go e edges -where - go (e : Expr) (acc : Array EdgeRec) : Array EdgeRec := Id.run do - let mut edges := acc - match e with - | .app .. => - let names := appSpineInputs e - let filtered := names.filter (fun n => - allowed.contains n && n != outputName) - if filtered.size ≥ 1 then - let inputVids := filtered.map (fun n => declToVid[n]!) - let outVid := declToVid[outputName]! - edges := edges.push - { inputs := inputVids, output := outVid, color := ExprColor.app } - for a in e.getAppArgs do - edges := go a edges - | .lam _ τ b _ => - edges := go τ edges - edges := go b edges - | .forallE _ τ b _ => - edges := go τ edges - edges := go b edges - | .letE _ τ v b _ => - edges := go τ edges - edges := go v edges - edges := go b edges - | .mdata _ e' => edges := go e' edges - | .proj _ _ s => edges := go s edges - | _ => pure () - return edges - -/-- **Eng.** Collect every non-internal declaration whose name has -`nsName` as a strict prefix. Used by the `#atlas.*.ns` commands as -their vertex set. Iteration order follows `env.constants.toList` — -unordered but stable within a single elaboration. -/ -def Environment.declsInNamespace (env : Lean.Environment) (nsName : Lean.Name) : - Array Lean.Name := Id.run do - let mut decls : Array Lean.Name := #[] - for (n, _) in env.constants.toList do - if nsName.isPrefixOf n && n != nsName && !n.isInternal then - decls := decls.push n - return decls - -/-- **Eng.** Build a declaration-level `IncidenceData` over the -given vertex set. Each declaration in `declNames` becomes a vertex; -each maximal app spine inside that declaration's proof body becomes -a hyperedge. - -Caller is responsible for filtering `declNames` (e.g., to a -namespace, excluding `Name.isInternal`); this function takes the -list verbatim. -/ -def Environment.toDeclIncidence - (env : Environment) (declNames : Array Name) : IncidenceData := Id.run do - -- Vertex map + per-vertex label + per-vertex DeclColor CSS. - let mut declToVid : Std.HashMap Name Nat := {} - let mut labels : Array String := #[] - let mut colors : Array String := #[] - for h : i in [:declNames.size] do - let n := declNames[i] - declToVid := declToVid.insert n i - labels := labels.push (toString n) - let css := match env.find? n with - | some info => (declColorOf env n info).css - | none => DeclColor.other.css - colors := colors.push css - let allowed : Std.HashSet Name := - declNames.foldl (fun s n => s.insert n) {} - -- Walk each decl's body, collect hyperedges. - let mut allEdges : Array EdgeRec := #[] - for h : i in [:declNames.size] do - let n := declNames[i] - match env.find? n with - | some info => - match info.value? (allowOpaque := true) with - | some body => - let es := collectDeclEdges allowed declToVid n body - allEdges := allEdges ++ es - | none => pure () - | none => pure () - return { - numVertices := declNames.size - edges := allEdges - vertexLabel := labels - vertexColor := colors - } - -end ProofAtlas.Mapping diff --git a/lean/ProofAtlas/Mapping/Environment.lean b/lean/ProofAtlas/Mapping/Environment.lean deleted file mode 100644 index b1c6629..0000000 --- a/lean/ProofAtlas/Mapping/Environment.lean +++ /dev/null @@ -1,170 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import Lean -import ProofAtlas.Pipeline.IncidenceData -import ProofAtlas.Pipeline.Linalg -import ProofAtlas.Pipeline.Hyperbolicity -import ProofAtlas.Mapping.ExprColor - -/-! -# `Lean.Environment → HypergraphReport` entry point - -End-to-end pipeline: - -``` -Lean.Environment - ↓ Environment.toIncidenceData (Phase 2 ✓) -IncidenceData - ↓ computeResistanceMatrix (Phase 3) -Array (Array Float) - ↓ computeDelta (Phase 4) -Float - ↓ -HypergraphReport -``` - -Const-sharing is the glue: every `.const Name [...]` reference (across -all declarations) is mapped to a single shared vertex, so two -declarations that mention the same constant land in the same connected -component. --/ - -namespace ProofAtlas.Mapping - -open ProofAtlas.Pipeline - -open Lean (Expr Name) -open ProofAtlas.Mapping - -/-! ## Phase 2 — `Expr → IncidenceData` -/ - -/-- **Eng.** *DFS-pre-order folder over a `Lean.Expr`, sharing -`.const` references via the `ConvState.constMap`.* Returns the -updated state and the root vertex ID for `e`'s top-level subterm. - -Atoms (`bvar`, `fvar`, `mvar`, `sort`, `lit`) get fresh vertices each -occurrence (tree view). `.const Name _` consults the const map: if -`Name` is already mapped, reuse the existing vertex; otherwise -allocate one and record the mapping. - -Compounds (`app`, `lam`, `forallE`, `letE`, `mdata`, `proj`) get a -fresh vertex plus a hyperedge whose inputs are the children's -recursively-computed vertex IDs and whose output is the new vertex. -/ -partial def Expr.toIncidence : Expr → ConvState → ConvState × Nat - | .bvar idx, s => s.allocVertex s!"bvar({idx})" - | .fvar fvarId, s => s.allocVertex s!"fvar({fvarId.name})" - | .mvar mvarId, s => s.allocVertex s!"mvar({mvarId.name})" - | .sort u, s => s.allocVertex s!"sort({u})" - | .lit lit, s => s.allocVertex s!"lit({repr lit})" - | .const n _, s => - match s.constMap[n]? with - | some vid => (s, vid) - | none => - let (s', vid) := s.allocVertex s!"const({n})" - ({ s' with constMap := s'.constMap.insert n vid }, vid) - | .app f a, s => - let (s, fid) := Expr.toIncidence f s - let (s, aid) := Expr.toIncidence a s - let (s, vid) := s.allocVertex "app" - (s.addEdge { inputs := #[fid, aid], output := vid, color := .app }, vid) - | .lam _ τ b _, s => - let (s, τid) := Expr.toIncidence τ s - let (s, bid) := Expr.toIncidence b s - let (s, vid) := s.allocVertex "lam" - (s.addEdge { inputs := #[τid, bid], output := vid, color := .lam }, vid) - | .forallE _ τ b _, s => - let (s, τid) := Expr.toIncidence τ s - let (s, bid) := Expr.toIncidence b s - let (s, vid) := s.allocVertex "forallE" - (s.addEdge { inputs := #[τid, bid], output := vid, color := .forallE }, vid) - | .letE _ τ v b _, s => - let (s, τid) := Expr.toIncidence τ s - let (s, vvid) := Expr.toIncidence v s - let (s, bid) := Expr.toIncidence b s - let (s, vid) := s.allocVertex "letE" - (s.addEdge { inputs := #[τid, vvid, bid], output := vid, color := .letE }, vid) - | .mdata _ e', s => - let (s, eid) := Expr.toIncidence e' s - let (s, vid) := s.allocVertex "mdata" - (s.addEdge { inputs := #[eid], output := vid, color := .mdata }, vid) - | .proj _ _ s', s => - let (s, sid) := Expr.toIncidence s' s - let (s, vid) := s.allocVertex "proj" - (s.addEdge { inputs := #[sid], output := vid, color := .proj }, vid) - -/-- **Eng.** *Bulk converter over an array of `Expr`s.* Folds -`Expr.toIncidence` with a shared `ConvState`, so consts are shared -across all input expressions. Returns the final `IncidenceData` and -the array of root vertex IDs (one per input). -/ -def Exprs.toIncidenceData (es : Array Expr) : IncidenceData × Array Nat := Id.run do - let mut s := ConvState.empty - let mut roots : Array Nat := #[] - for e in es do - let (s', rid) := Expr.toIncidence e s - s := s' - roots := roots.push rid - return (s.data, roots) - -/-! ## `Environment → IncidenceData` -/ - -/-- **Eng.** *Collect the value `Expr` of every `def` / `theorem` in -the environment.* Skips constants without a body (`axiom`, -`opaque`, `Quot.mk`, etc.). -/ -def Environment.collectValueExprs (env : Lean.Environment) : Array Expr := Id.run do - let mut acc : Array Expr := #[] - for (_, info) in env.constants.toList do - if let some val := info.value? then - acc := acc.push val - return acc - -/-- **Eng.** *Walk the environment's declaration bodies, glue them -via const-sharing into a single `IncidenceData`.* -/ -def Environment.toIncidenceData (env : Lean.Environment) : IncidenceData := - (Exprs.toIncidenceData (Environment.collectValueExprs env)).1 - -/-! ## End-to-end entry (Phases 3 & 4 still TODO) -/ - -/-- **Eng.** *End-to-end geometric profile entry point.* Walks the -environment, glues all declarations into one `IncidenceData` with -const-sharing. - -**Status (Phase 4 done).** Full end-to-end pipeline: incidence, -resistance matrix, and δ-hyperbolicity are all populated. -/ -def geometricProfile (env : Lean.Environment) : IO HypergraphReport := do - let data := Environment.toIncidenceData env - let R := data.resistanceMatrix - let δ := hyperbolicityConstant R - return { incidence := data, resistance := R, delta := δ } - -/-- **Eng.** *Pure-function variant of `geometricProfile`* working on -an explicit array of `Lean.Expr`s (e.g., declaration bodies). Useful -for unit tests and standalone demos that don't want to plumb a real -`Lean.Environment`. -/ -def geometricProfileOfExprs (es : Array Lean.Expr) : HypergraphReport := - let (data, roots) := Exprs.toIncidenceData es - let R := data.resistanceMatrix - let δ := hyperbolicityConstant R - { incidence := data, resistance := R, delta := δ, roots := roots } - -/-- **Eng.** *Incidence-only profile.* Skips both the resistance -matrix and the δ-hyperbolicity computation; useful for -structure-only commands (`#atlas.graph`) where the report consumer -only reads `.incidence`. `resistance` is `#[]` and `delta` is `0.0` -in the returned report. -/ -def incidenceProfileOfExprs (es : Array Lean.Expr) : HypergraphReport := - let (data, roots) := Exprs.toIncidenceData es - { incidence := data, resistance := #[], delta := 0.0, roots := roots } - -/-- **Eng.** *Resistance-only profile.* Computes the resistance -matrix but skips the `O(C(n,4))` δ-hyperbolicity sweep; useful for -`#atlas.resistance` where only `R(root_A, root_B)` is read. `delta` -is `0.0` in the returned report. -/ -def resistanceProfileOfExprs (es : Array Lean.Expr) : HypergraphReport := - let (data, roots) := Exprs.toIncidenceData es - let R := data.resistanceMatrix - { incidence := data, resistance := R, delta := 0.0, roots := roots } - -end ProofAtlas.Mapping diff --git a/lean/ProofAtlas/Mapping/Expr.lean b/lean/ProofAtlas/Mapping/Expr.lean deleted file mode 100644 index c37d7b9..0000000 --- a/lean/ProofAtlas/Mapping/Expr.lean +++ /dev/null @@ -1,958 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import Lean -import ProofAtlas.Util.Linter.MathTag -import ProofAtlas.Hypergraph.Basic -import ProofAtlas.Mapping.ExprColor -import Mathlib.Data.Fintype.Basic -/-! -# `Lean.Expr → Hypergraph` conversion - -Every `Lean.Expr` proof-term tree induces a BDF hypergraph: - -* **Vertices** = subterms of the expression, indexed by - `Fin (countSubterms e)` (one slot per occurrence — sharing not - collapsed; treats the expression as a tree, not a DAG). -* **Hyperedges** = compound subterms (`app`, `lam`, `forallE`, - `letE`, `mdata`, `proj`). Inputs = the immediate sub-expressions - (ordered as in the constructor: e.g., `app f a` has inputs - `[f, a]`, `lam x τ b` has `[τ, b]`). Output = the compound - subterm itself. -* **Colour** = constructor kind (`ExprColor`). -* **Acyclicity** = structural: `Lean.Expr` is inductive, so - subterms strictly decrease in rank. -* **Connectedness** = every subterm shares the root edge's - containing hyperedge with at least one other subterm, and the - whole tree is connected through the root. - -The four BDF structural properties (directed, ordered, coloured, -acyclic) hold *by construction*. Hence every Lean declaration -ships with a BDF hypergraph — and with the -`IsHypergraphMetric`-certified `resistanceDist` (or any other certified -metric), it ships with a geometric profile too. - -## Status - -Skeleton: signatures and `countSubterms` are concrete; the -converter body and the four Hypergraph fields are `sorry`'d -pending the index-allocation / Hyperedge assembly code. --/ - -namespace ProofAtlas.Mapping - -open Lean (Expr) - -/-- **Eng.** The colour of a subterm, by constructor kind. -/ -def exprColor : Expr → ExprColor - | .bvar _ => .bvar - | .fvar _ => .fvar - | .mvar _ => .mvar - | .sort _ => .sort - | .const _ _ => .const - | .lit _ => .lit - | .app _ _ => .app - | .lam _ _ _ _ => .lam - | .forallE _ _ _ _ => .forallE - | .letE _ _ _ _ _ => .letE - | .mdata _ _ => .mdata - | .proj _ _ _ => .proj - -/-- **Eng.** Ordered immediate sub-expressions of an `Expr`. Atomic -constructors return `[]`; compound constructors return their -left-to-right sub-expressions in source order. -/ -def exprChildren : Expr → List Expr - | .app f a => [f, a] - | .lam _ τ b _ => [τ, b] - | .forallE _ τ b _ => [τ, b] - | .letE _ τ v b _ => [τ, v, b] - | .mdata _ e => [e] - | .proj _ _ s => [s] - | _ => [] - -/-- **Eng.** A subterm is *compound* iff it has at least one -sub-expression (equivalently, its `children` list is non-empty). -/ -def exprIsCompound (e : Expr) : Bool := !(exprChildren e).isEmpty - -/-- **Eng.** *DFS pre-order subterm enumeration.* Returns every -subterm-occurrence of `e` in depth-first pre-order: the constructor -at the root first, then the left-to-right pre-order flatten of its -sub-expressions. The primary enumeration; `countSubterms` is its -length. -/ -def subtermsList : Expr → List Expr - | e@(.bvar _) => [e] - | e@(.fvar _) => [e] - | e@(.mvar _) => [e] - | e@(.sort _) => [e] - | e@(.const _ _) => [e] - | e@(.lit _) => [e] - | e@(.app f a) => e :: (subtermsList f ++ subtermsList a) - | e@(.lam _ τ b _) => e :: (subtermsList τ ++ subtermsList b) - | e@(.forallE _ τ b _) => e :: (subtermsList τ ++ subtermsList b) - | e@(.letE _ τ v b _) => e :: (subtermsList τ ++ subtermsList v ++ subtermsList b) - | e@(.mdata _ e') => e :: subtermsList e' - | e@(.proj _ _ s) => e :: subtermsList s - -/-- **Eng.** Number of subterm-occurrences in a `Lean.Expr` (tree -view, sharing not collapsed). Derived from `subtermsList`. -/ -@[reducible] -def countSubterms (e : Expr) : Nat := (subtermsList e).length - -/-- **Eng.** `subtermsList` is always non-empty (the root contributes -its own occurrence at the head). -/ -theorem subtermsList_ne_nil (e : Expr) : subtermsList e ≠ [] := by - cases e <;> simp [subtermsList] - -/-- **Eng.** `countSubterms` is always positive. -/ -theorem countSubterms_pos (e : Expr) : 0 < countSubterms e := - List.length_pos_iff.mpr (subtermsList_ne_nil e) - -/-- **Eng.** *DFS pre-order child index.* If `c : Expr` sits at index -`i` in `subtermsList e`, then its `j`-th child (`0 ≤ j < c.children.length`) -sits at this index in the same list. The formula `i + 1 + sum` follows -from the pre-order layout: the compound at `i` is followed by its -first child's subtree, then its second child's, etc. -/ -def childIdx (c : Expr) (i j : Nat) : Nat := - i + 1 + (((exprChildren c).take j).map countSubterms).sum - -/-- **Eng.** *Subterm count = 1 + children's counts.* Holds for all -`Expr`s: atoms have no children so RHS is `1 + 0 = 1`; compound -nodes split as `e :: concat-of-children-subterm-lists`, whose length -is the sum identity. -/ -private lemma countSubterms_eq_sum (e : Expr) : - countSubterms e = 1 + ((exprChildren e).map countSubterms).sum := by - cases e <;> - simp [exprChildren, subtermsList, countSubterms, List.length_append] <;> - omega - -/-- **Eng.** *One-step strict growth of DFS child-prefix sums.* Adding -one more child strictly increases the prefix sum, because every -subterm count is positive. -/ -private lemma children_take_sum_succ_lt - (c : Expr) {j : Nat} (hj : j < (exprChildren c).length) : - (((exprChildren c).take j).map countSubterms).sum - < (((exprChildren c).take (j + 1)).map countSubterms).sum := by - set L := exprChildren c - have hML : j < (L.map countSubterms).length := by - rw [List.length_map]; exact hj - rw [List.map_take, List.map_take] - set M := L.map countSubterms with hM_def - rw [List.take_succ_eq_append_getElem hML, List.sum_append, List.sum_singleton] - have hpos : 0 < M[j] := by - change 0 < (L.map countSubterms)[j] - rw [List.getElem_map] - exact countSubterms_pos _ - omega - -/-- **Eng.** *Strict monotonicity of DFS child-prefix sums.* Used for -the `inVertex_injective` proof: distinct child indices yield distinct -DFS offsets since each subterm count is positive. -/ -private lemma children_take_sum_lt - (c : Expr) {j₁ j₂ : Nat} (h₂ : j₂ ≤ (exprChildren c).length) (hlt : j₁ < j₂) : - (((exprChildren c).take j₁).map countSubterms).sum - < (((exprChildren c).take j₂).map countSubterms).sum := by - induction j₂, hlt using Nat.le_induction with - | base => - exact children_take_sum_succ_lt c (by omega) - | succ k hk ih => - have hk' : k ≤ (exprChildren c).length := by omega - have h_succ := children_take_sum_succ_lt c (j := k) (by omega) - exact lt_trans (ih hk') h_succ - -/-- **Eng.** *Key DFS subtree-window bound.* The subtree rooted at -position `i` in `subtermsList e` has its size bounded by `n − i`: -informally, "the rest of the list is at least as long as the subtree". -This is the core invariant of the DFS pre-order layout and powers all -the index-related proofs in `toHypergraph`. -/ -private lemma subtree_size_bound : - ∀ (e : Expr) (i : Fin (countSubterms e)), - i.val + countSubterms ((subtermsList e).get i) ≤ countSubterms e := by - intro e - induction e with - | bvar _ => - intro ⟨i, hi⟩ - -- For atoms, countSubterms = 1 so i = 0; get returns self. - have : i = 0 := by simp [countSubterms, subtermsList] at hi; omega - subst this; simp [subtermsList, countSubterms] - | fvar _ => - intro ⟨i, hi⟩ - have : i = 0 := by simp [countSubterms, subtermsList] at hi; omega - subst this; simp [subtermsList, countSubterms] - | mvar _ => - intro ⟨i, hi⟩ - have : i = 0 := by simp [countSubterms, subtermsList] at hi; omega - subst this; simp [subtermsList, countSubterms] - | sort _ => - intro ⟨i, hi⟩ - have : i = 0 := by simp [countSubterms, subtermsList] at hi; omega - subst this; simp [subtermsList, countSubterms] - | const _ _ => - intro ⟨i, hi⟩ - have : i = 0 := by simp [countSubterms, subtermsList] at hi; omega - subst this; simp [subtermsList, countSubterms] - | lit _ => - intro ⟨i, hi⟩ - have : i = 0 := by simp [countSubterms, subtermsList] at hi; omega - subst this; simp [subtermsList, countSubterms] - | app f a ihf iha => - intro ⟨i, hi⟩ - have hcnt : countSubterms (.app f a) = countSubterms f + countSubterms a + 1 := by - simp [countSubterms, subtermsList] - rcases i with _ | k - · change 0 + countSubterms _ ≤ _ - have h0 : (subtermsList (.app f a)).get ⟨0, hi⟩ = .app f a := rfl - rw [h0]; omega - · change k + 1 + countSubterms _ ≤ _ - by_cases hkf : k < countSubterms f - · have ih_inst : k + countSubterms ((subtermsList f).get ⟨k, hkf⟩) - ≤ countSubterms f := ihf ⟨k, hkf⟩ - have hget : (subtermsList (.app f a)).get ⟨k+1, hi⟩ - = (subtermsList f).get ⟨k, hkf⟩ := by - simp only [List.get_eq_getElem, subtermsList, - List.getElem_cons_succ, List.getElem_append_left hkf] - rw [hget]; omega - · push_neg at hkf - have hka : k - countSubterms f < countSubterms a := by omega - have ih_inst : (k - countSubterms f) - + countSubterms ((subtermsList a).get ⟨k - countSubterms f, hka⟩) - ≤ countSubterms a := iha ⟨k - countSubterms f, hka⟩ - have hget : (subtermsList (.app f a)).get ⟨k+1, hi⟩ - = (subtermsList a).get ⟨k - countSubterms f, hka⟩ := by - have hkfl : (subtermsList f).length ≤ k := by - change countSubterms f ≤ k; exact hkf - simp only [List.get_eq_getElem, subtermsList, List.getElem_cons_succ, - List.getElem_append_right hkfl] - rw [hget]; omega - | lam name τ b bi ihτ ihb => - intro ⟨i, hi⟩ - have hcnt : countSubterms (.lam name τ b bi) = countSubterms τ + countSubterms b + 1 := by - simp [countSubterms, subtermsList] - rcases i with _ | k - · change 0 + countSubterms _ ≤ _ - have h0 : (subtermsList (.lam name τ b bi)).get ⟨0, hi⟩ = .lam name τ b bi := rfl - rw [h0]; omega - · change k + 1 + countSubterms _ ≤ _ - by_cases hkτ : k < countSubterms τ - · have ih_inst : k + countSubterms ((subtermsList τ).get ⟨k, hkτ⟩) - ≤ countSubterms τ := ihτ ⟨k, hkτ⟩ - have hget : (subtermsList (.lam name τ b bi)).get ⟨k+1, hi⟩ - = (subtermsList τ).get ⟨k, hkτ⟩ := by - simp only [List.get_eq_getElem, subtermsList, - List.getElem_cons_succ, List.getElem_append_left hkτ] - rw [hget]; omega - · push_neg at hkτ - have hkb : k - countSubterms τ < countSubterms b := by omega - have ih_inst : (k - countSubterms τ) - + countSubterms ((subtermsList b).get ⟨k - countSubterms τ, hkb⟩) - ≤ countSubterms b := ihb ⟨k - countSubterms τ, hkb⟩ - have hget : (subtermsList (.lam name τ b bi)).get ⟨k+1, hi⟩ - = (subtermsList b).get ⟨k - countSubterms τ, hkb⟩ := by - have hkτl : (subtermsList τ).length ≤ k := by - change countSubterms τ ≤ k; exact hkτ - simp only [List.get_eq_getElem, subtermsList, List.getElem_cons_succ, - List.getElem_append_right hkτl] - rw [hget]; omega - | forallE name τ b bi ihτ ihb => - intro ⟨i, hi⟩ - have hcnt : countSubterms (.forallE name τ b bi) = countSubterms τ + countSubterms b + 1 := by - simp [countSubterms, subtermsList] - rcases i with _ | k - · change 0 + countSubterms _ ≤ _ - have h0 : (subtermsList (.forallE name τ b bi)).get ⟨0, hi⟩ = .forallE name τ b bi := rfl - rw [h0]; omega - · change k + 1 + countSubterms _ ≤ _ - by_cases hkτ : k < countSubterms τ - · have ih_inst : k + countSubterms ((subtermsList τ).get ⟨k, hkτ⟩) - ≤ countSubterms τ := ihτ ⟨k, hkτ⟩ - have hget : (subtermsList (.forallE name τ b bi)).get ⟨k+1, hi⟩ - = (subtermsList τ).get ⟨k, hkτ⟩ := by - simp only [List.get_eq_getElem, subtermsList, - List.getElem_cons_succ, List.getElem_append_left hkτ] - rw [hget]; omega - · push_neg at hkτ - have hkb : k - countSubterms τ < countSubterms b := by omega - have ih_inst : (k - countSubterms τ) - + countSubterms ((subtermsList b).get ⟨k - countSubterms τ, hkb⟩) - ≤ countSubterms b := ihb ⟨k - countSubterms τ, hkb⟩ - have hget : (subtermsList (.forallE name τ b bi)).get ⟨k+1, hi⟩ - = (subtermsList b).get ⟨k - countSubterms τ, hkb⟩ := by - have hkτl : (subtermsList τ).length ≤ k := by - change countSubterms τ ≤ k; exact hkτ - simp only [List.get_eq_getElem, subtermsList, List.getElem_cons_succ, - List.getElem_append_right hkτl] - rw [hget]; omega - | letE name τ v b nd ihτ ihv ihb => - intro ⟨i, hi⟩ - have hcnt : countSubterms (.letE name τ v b nd) - = countSubterms τ + countSubterms v + countSubterms b + 1 := by - simp [countSubterms, subtermsList]; omega - have hτvlen : (subtermsList τ ++ subtermsList v).length - = countSubterms τ + countSubterms v := by - rw [List.length_append] - rcases i with _ | k - · change 0 + countSubterms _ ≤ _ - have h0 : (subtermsList (.letE name τ v b nd)).get ⟨0, hi⟩ = .letE name τ v b nd := rfl - rw [h0]; omega - · change k + 1 + countSubterms _ ≤ _ - by_cases hkτ : k < countSubterms τ - · -- in τ's subtree - have ih_inst : k + countSubterms ((subtermsList τ).get ⟨k, hkτ⟩) - ≤ countSubterms τ := ihτ ⟨k, hkτ⟩ - have hkτv : k < (subtermsList τ ++ subtermsList v).length := by - rw [hτvlen]; omega - have hget : (subtermsList (.letE name τ v b nd)).get ⟨k+1, hi⟩ - = (subtermsList τ).get ⟨k, hkτ⟩ := by - simp only [List.get_eq_getElem, subtermsList, List.getElem_cons_succ, - List.getElem_append_left hkτv, - List.getElem_append_left hkτ] - rw [hget]; omega - · push_neg at hkτ - by_cases hkv : k - countSubterms τ < countSubterms v - · -- in v's subtree - have ih_inst : (k - countSubterms τ) - + countSubterms ((subtermsList v).get ⟨k - countSubterms τ, hkv⟩) - ≤ countSubterms v := ihv ⟨k - countSubterms τ, hkv⟩ - have hkτl : (subtermsList τ).length ≤ k := by - change countSubterms τ ≤ k; exact hkτ - have hkτv : k < (subtermsList τ ++ subtermsList v).length := by - rw [hτvlen]; omega - have hget : (subtermsList (.letE name τ v b nd)).get ⟨k+1, hi⟩ - = (subtermsList v).get ⟨k - countSubterms τ, hkv⟩ := by - simp only [List.get_eq_getElem, subtermsList, List.getElem_cons_succ, - List.getElem_append_left hkτv, - List.getElem_append_right hkτl] - rw [hget]; omega - · -- in b's subtree - push_neg at hkv - have hkb : k - countSubterms τ - countSubterms v < countSubterms b := by omega - have ih_inst : (k - countSubterms τ - countSubterms v) - + countSubterms ((subtermsList b).get - ⟨k - countSubterms τ - countSubterms v, hkb⟩) - ≤ countSubterms b := - ihb ⟨k - countSubterms τ - countSubterms v, hkb⟩ - have hkτvl : (subtermsList τ ++ subtermsList v).length ≤ k := by - rw [hτvlen]; omega - have hget : (subtermsList (.letE name τ v b nd)).get ⟨k+1, hi⟩ - = (subtermsList b).get - ⟨k - countSubterms τ - countSubterms v, hkb⟩ := by - simp only [List.get_eq_getElem, subtermsList, List.getElem_cons_succ, - List.getElem_append_right hkτvl] - congr 1 - rw [List.length_append] - exact Nat.sub_add_eq k (countSubterms τ) (countSubterms v) - rw [hget]; omega - | mdata d e' ih => - intro ⟨i, hi⟩ - have hcnt : countSubterms (.mdata d e') = countSubterms e' + 1 := by - simp [countSubterms, subtermsList] - rcases i with _ | k - · -- i = 0 - change 0 + countSubterms _ ≤ _ - have h0 : (subtermsList (.mdata d e')).get ⟨0, hi⟩ = .mdata d e' := rfl - rw [h0]; omega - · -- i = k+1 - change k + 1 + countSubterms _ ≤ _ - have hk : k < countSubterms e' := by omega - have ih_inst : k + countSubterms ((subtermsList e').get ⟨k, hk⟩) - ≤ countSubterms e' := ih ⟨k, hk⟩ - have hget : (subtermsList (.mdata d e')).get ⟨k+1, hi⟩ - = (subtermsList e').get ⟨k, hk⟩ := rfl - rw [hget]; omega - | proj sn idx s ih => - intro ⟨i, hi⟩ - have hcnt : countSubterms (.proj sn idx s) = countSubterms s + 1 := by - simp [countSubterms, subtermsList] - rcases i with _ | k - · change 0 + countSubterms _ ≤ _ - have h0 : (subtermsList (.proj sn idx s)).get ⟨0, hi⟩ = .proj sn idx s := rfl - rw [h0]; omega - · change k + 1 + countSubterms _ ≤ _ - have hk : k < countSubterms s := by omega - have ih_inst : k + countSubterms ((subtermsList s).get ⟨k, hk⟩) - ≤ countSubterms s := ih ⟨k, hk⟩ - have hget : (subtermsList (.proj sn idx s)).get ⟨k+1, hi⟩ - = (subtermsList s).get ⟨k, hk⟩ := rfl - rw [hget]; omega - -/-- **Eng.** *Parent-existence lemma.* Every non-root vertex in -`subtermsList e` is a child of some compound subterm at an earlier -DFS position. Used to prove `connected` for `toHypergraph`: each -non-root vertex is one incidence step from a strictly smaller index, -so strong induction on the DFS index reaches vertex `0`. -/ -private lemma vertex_has_parent (e : Expr) : - ∀ k, 0 < k → k < countSubterms e → - ∃ (p : Nat) (j_val : Nat) (hp : p < countSubterms e), - p < k ∧ exprIsCompound ((subtermsList e).get ⟨p, hp⟩) = true - ∧ j_val < (exprChildren ((subtermsList e).get ⟨p, hp⟩)).length - ∧ childIdx ((subtermsList e).get ⟨p, hp⟩) p j_val = k := by - induction e with - | bvar _ => intro k hk0 hkn; change k < 1 at hkn; omega - | fvar _ => intro k hk0 hkn; change k < 1 at hkn; omega - | mvar _ => intro k hk0 hkn; change k < 1 at hkn; omega - | sort _ => intro k hk0 hkn; change k < 1 at hkn; omega - | const _ _ => intro k hk0 hkn; change k < 1 at hkn; omega - | lit _ => intro k hk0 hkn; change k < 1 at hkn; omega - | mdata d e' ih => - intro k hk0 hkn - have hcnt : countSubterms (.mdata d e') = countSubterms e' + 1 := by - change (subtermsList (.mdata d e')).length = _; simp [subtermsList] - rw [hcnt] at hkn - by_cases hk1 : k = 1 - · -- k = 1: parent is root, j_val = 0 - have hp_root : 0 < countSubterms (.mdata d e') := by rw [hcnt]; omega - refine ⟨0, 0, hp_root, by omega, ?_, ?_, ?_⟩ - · change exprIsCompound (.mdata d e') = true - simp [exprIsCompound, exprChildren] - · change 0 < (exprChildren (.mdata d e')).length - simp [exprChildren] - · change childIdx (.mdata d e') 0 0 = k - simp [childIdx, exprChildren]; omega - · -- k > 1: use IH on e' with k - 1 - have hk' : 0 < k - 1 := by omega - have hk'lt : k - 1 < countSubterms e' := by omega - obtain ⟨p', j_val, hp', hp'lt, hcomp, hjlt, hchild⟩ := ih (k - 1) hk' hk'lt - have hp1_lt : p' + 1 < countSubterms (.mdata d e') := by rw [hcnt]; omega - have hget : (subtermsList (.mdata d e')).get ⟨p' + 1, hp1_lt⟩ - = (subtermsList e').get ⟨p', hp'⟩ := rfl - refine ⟨p' + 1, j_val, hp1_lt, by omega, ?_, ?_, ?_⟩ - · rw [hget]; exact hcomp - · rw [hget]; exact hjlt - · rw [hget]; unfold childIdx at *; omega - | proj sn idx s ih => - intro k hk0 hkn - have hcnt : countSubterms (.proj sn idx s) = countSubterms s + 1 := by - change (subtermsList (.proj sn idx s)).length = _; simp [subtermsList] - rw [hcnt] at hkn - by_cases hk1 : k = 1 - · have hp_root : 0 < countSubterms (.proj sn idx s) := by rw [hcnt]; omega - refine ⟨0, 0, hp_root, by omega, ?_, ?_, ?_⟩ - · change exprIsCompound (.proj sn idx s) = true - simp [exprIsCompound, exprChildren] - · change 0 < (exprChildren (.proj sn idx s)).length - simp [exprChildren] - · change childIdx (.proj sn idx s) 0 0 = k - simp [childIdx, exprChildren]; omega - · have hk' : 0 < k - 1 := by omega - have hk'lt : k - 1 < countSubterms s := by omega - obtain ⟨p', j_val, hp', hp'lt, hcomp, hjlt, hchild⟩ := ih (k - 1) hk' hk'lt - have hp1_lt : p' + 1 < countSubterms (.proj sn idx s) := by rw [hcnt]; omega - have hget : (subtermsList (.proj sn idx s)).get ⟨p' + 1, hp1_lt⟩ - = (subtermsList s).get ⟨p', hp'⟩ := rfl - refine ⟨p' + 1, j_val, hp1_lt, by omega, ?_, ?_, ?_⟩ - · rw [hget]; exact hcomp - · rw [hget]; exact hjlt - · rw [hget]; unfold childIdx at *; omega - | app f a ihf iha => - intro k hk0 hkn - have hcnt : countSubterms (.app f a) = countSubterms f + countSubterms a + 1 := by - change (subtermsList (.app f a)).length = _ - simp [subtermsList, List.length_append] - rw [hcnt] at hkn - by_cases hk1 : k = 1 - · -- k = 1: parent is root, j_val = 0 (selects f) - have hp_root : 0 < countSubterms (.app f a) := by rw [hcnt]; omega - refine ⟨0, 0, hp_root, by omega, ?_, ?_, ?_⟩ - · change exprIsCompound (.app f a) = true - simp [exprIsCompound, exprChildren] - · change 0 < (exprChildren (.app f a)).length - simp [exprChildren] - · change childIdx (.app f a) 0 0 = k - simp [childIdx, exprChildren]; omega - by_cases hkf : k < countSubterms f + 1 - · -- 1 < k ≤ countSubterms f: in f's block - have hk' : 0 < k - 1 := by omega - have hk'lt : k - 1 < countSubterms f := by omega - obtain ⟨p', j_val, hp', hp'lt, hcomp, hjlt, hchild⟩ := ihf (k - 1) hk' hk'lt - have hp1_lt : p' + 1 < countSubterms (.app f a) := by rw [hcnt]; omega - have hget : (subtermsList (.app f a)).get ⟨p' + 1, hp1_lt⟩ - = (subtermsList f).get ⟨p', hp'⟩ := by - change ((.app f a) :: (subtermsList f ++ subtermsList a)).get ⟨p' + 1, _⟩ - = (subtermsList f).get ⟨p', hp'⟩ - simp [List.get_eq_getElem, List.getElem_cons_succ, - List.getElem_append_left hp'] - refine ⟨p' + 1, j_val, hp1_lt, by omega, ?_, ?_, ?_⟩ - · rw [hget]; exact hcomp - · rw [hget]; exact hjlt - · rw [hget]; unfold childIdx at *; omega - by_cases hka_root : k = countSubterms f + 1 - · -- k = countSubterms f + 1: parent is root, j_val = 1 (selects a) - have hp_root : 0 < countSubterms (.app f a) := by rw [hcnt]; omega - refine ⟨0, 1, hp_root, by omega, ?_, ?_, ?_⟩ - · change exprIsCompound (.app f a) = true - simp [exprIsCompound, exprChildren] - · change 1 < (exprChildren (.app f a)).length - simp [exprChildren] - · change childIdx (.app f a) 0 1 = k - simp [childIdx, exprChildren] - omega - · -- k > countSubterms f + 1: in a's block - have hka : 0 < k - 1 - countSubterms f := by omega - have hkalt : k - 1 - countSubterms f < countSubterms a := by omega - obtain ⟨p', j_val, hp', hp'lt, hcomp, hjlt, hchild⟩ := - iha (k - 1 - countSubterms f) hka hkalt - have hpfl : (subtermsList f).length ≤ countSubterms f + p' := by - change countSubterms f ≤ _; omega - -- Use the (m + 1) form to match List.getElem_cons_succ directly. - have hp_lt : countSubterms f + p' + 1 < countSubterms (.app f a) := by - rw [hcnt]; omega - have hget : (subtermsList (.app f a)).get ⟨countSubterms f + p' + 1, hp_lt⟩ - = (subtermsList a).get ⟨p', hp'⟩ := by - change ((.app f a) :: (subtermsList f ++ subtermsList a)).get - ⟨countSubterms f + p' + 1, _⟩ - = (subtermsList a).get ⟨p', hp'⟩ - rw [List.get_eq_getElem, List.getElem_cons_succ, List.get_eq_getElem, - List.getElem_append_right hpfl] - congr 1 - change countSubterms f + p' - countSubterms f = p' - omega - refine ⟨countSubterms f + p' + 1, j_val, hp_lt, by omega, ?_, ?_, ?_⟩ - · rw [hget]; exact hcomp - · rw [hget]; exact hjlt - · rw [hget]; unfold childIdx at *; omega - | lam name τ b bi ihτ ihb => - intro k hk0 hkn - have hcnt : countSubterms (.lam name τ b bi) = countSubterms τ + countSubterms b + 1 := by - change (subtermsList (.lam name τ b bi)).length = _ - simp [subtermsList, List.length_append] - rw [hcnt] at hkn - by_cases hk1 : k = 1 - · have hp_root : 0 < countSubterms (.lam name τ b bi) := by rw [hcnt]; omega - refine ⟨0, 0, hp_root, by omega, ?_, ?_, ?_⟩ - · change exprIsCompound (.lam name τ b bi) = true - simp [exprIsCompound, exprChildren] - · change 0 < (exprChildren (.lam name τ b bi)).length - simp [exprChildren] - · change childIdx (.lam name τ b bi) 0 0 = k - simp [childIdx, exprChildren] - omega - by_cases hkτ : k < countSubterms τ + 1 - · have hk' : 0 < k - 1 := by omega - have hk'lt : k - 1 < countSubterms τ := by omega - obtain ⟨p', j_val, hp', hp'lt, hcomp, hjlt, hchild⟩ := ihτ (k - 1) hk' hk'lt - have hp1_lt : p' + 1 < countSubterms (.lam name τ b bi) := by rw [hcnt]; omega - have hget : (subtermsList (.lam name τ b bi)).get ⟨p' + 1, hp1_lt⟩ - = (subtermsList τ).get ⟨p', hp'⟩ := by - change ((.lam name τ b bi) :: (subtermsList τ ++ subtermsList b)).get ⟨p' + 1, _⟩ - = (subtermsList τ).get ⟨p', hp'⟩ - simp [List.get_eq_getElem, List.getElem_cons_succ, - List.getElem_append_left hp'] - refine ⟨p' + 1, j_val, hp1_lt, by omega, ?_, ?_, ?_⟩ - · rw [hget]; exact hcomp - · rw [hget]; exact hjlt - · rw [hget]; unfold childIdx at *; omega - by_cases hkb_root : k = countSubterms τ + 1 - · have hp_root : 0 < countSubterms (.lam name τ b bi) := by rw [hcnt]; omega - refine ⟨0, 1, hp_root, by omega, ?_, ?_, ?_⟩ - · change exprIsCompound (.lam name τ b bi) = true - simp [exprIsCompound, exprChildren] - · change 1 < (exprChildren (.lam name τ b bi)).length - simp [exprChildren] - · change childIdx (.lam name τ b bi) 0 1 = k - simp [childIdx, exprChildren] - omega - · have hkb : 0 < k - 1 - countSubterms τ := by omega - have hkblt : k - 1 - countSubterms τ < countSubterms b := by omega - obtain ⟨p', j_val, hp', hp'lt, hcomp, hjlt, hchild⟩ := - ihb (k - 1 - countSubterms τ) hkb hkblt - have hpτl : (subtermsList τ).length ≤ countSubterms τ + p' := by - change countSubterms τ ≤ _; omega - have hp_lt : countSubterms τ + p' + 1 < countSubterms (.lam name τ b bi) := by - rw [hcnt]; omega - have hget : (subtermsList (.lam name τ b bi)).get - ⟨countSubterms τ + p' + 1, hp_lt⟩ - = (subtermsList b).get ⟨p', hp'⟩ := by - change ((.lam name τ b bi) :: (subtermsList τ ++ subtermsList b)).get - ⟨countSubterms τ + p' + 1, _⟩ - = (subtermsList b).get ⟨p', hp'⟩ - rw [List.get_eq_getElem, List.getElem_cons_succ, List.get_eq_getElem, - List.getElem_append_right hpτl] - congr 1 - change countSubterms τ + p' - countSubterms τ = p' - omega - refine ⟨countSubterms τ + p' + 1, j_val, hp_lt, by omega, ?_, ?_, ?_⟩ - · rw [hget]; exact hcomp - · rw [hget]; exact hjlt - · rw [hget]; unfold childIdx at *; omega - | forallE name τ b bi ihτ ihb => - intro k hk0 hkn - have hcnt : countSubterms (.forallE name τ b bi) - = countSubterms τ + countSubterms b + 1 := by - change (subtermsList (.forallE name τ b bi)).length = _ - simp [subtermsList, List.length_append] - rw [hcnt] at hkn - by_cases hk1 : k = 1 - · have hp_root : 0 < countSubterms (.forallE name τ b bi) := by rw [hcnt]; omega - refine ⟨0, 0, hp_root, by omega, ?_, ?_, ?_⟩ - · change exprIsCompound (.forallE name τ b bi) = true - simp [exprIsCompound, exprChildren] - · change 0 < (exprChildren (.forallE name τ b bi)).length - simp [exprChildren] - · change childIdx (.forallE name τ b bi) 0 0 = k - simp [childIdx, exprChildren] - omega - by_cases hkτ : k < countSubterms τ + 1 - · have hk' : 0 < k - 1 := by omega - have hk'lt : k - 1 < countSubterms τ := by omega - obtain ⟨p', j_val, hp', hp'lt, hcomp, hjlt, hchild⟩ := ihτ (k - 1) hk' hk'lt - have hp1_lt : p' + 1 < countSubterms (.forallE name τ b bi) := by rw [hcnt]; omega - have hget : (subtermsList (.forallE name τ b bi)).get ⟨p' + 1, hp1_lt⟩ - = (subtermsList τ).get ⟨p', hp'⟩ := by - change ((.forallE name τ b bi) :: (subtermsList τ ++ subtermsList b)).get - ⟨p' + 1, _⟩ - = (subtermsList τ).get ⟨p', hp'⟩ - simp [List.get_eq_getElem, List.getElem_cons_succ, - List.getElem_append_left hp'] - refine ⟨p' + 1, j_val, hp1_lt, by omega, ?_, ?_, ?_⟩ - · rw [hget]; exact hcomp - · rw [hget]; exact hjlt - · rw [hget]; unfold childIdx at *; omega - by_cases hkb_root : k = countSubterms τ + 1 - · have hp_root : 0 < countSubterms (.forallE name τ b bi) := by rw [hcnt]; omega - refine ⟨0, 1, hp_root, by omega, ?_, ?_, ?_⟩ - · change exprIsCompound (.forallE name τ b bi) = true - simp [exprIsCompound, exprChildren] - · change 1 < (exprChildren (.forallE name τ b bi)).length - simp [exprChildren] - · change childIdx (.forallE name τ b bi) 0 1 = k - simp [childIdx, exprChildren] - omega - · have hkb : 0 < k - 1 - countSubterms τ := by omega - have hkblt : k - 1 - countSubterms τ < countSubterms b := by omega - obtain ⟨p', j_val, hp', hp'lt, hcomp, hjlt, hchild⟩ := - ihb (k - 1 - countSubterms τ) hkb hkblt - have hpτl : (subtermsList τ).length ≤ countSubterms τ + p' := by - change countSubterms τ ≤ _; omega - have hp_lt : countSubterms τ + p' + 1 < countSubterms (.forallE name τ b bi) := by - rw [hcnt]; omega - have hget : (subtermsList (.forallE name τ b bi)).get - ⟨countSubterms τ + p' + 1, hp_lt⟩ - = (subtermsList b).get ⟨p', hp'⟩ := by - change ((.forallE name τ b bi) :: (subtermsList τ ++ subtermsList b)).get - ⟨countSubterms τ + p' + 1, _⟩ - = (subtermsList b).get ⟨p', hp'⟩ - rw [List.get_eq_getElem, List.getElem_cons_succ, List.get_eq_getElem, - List.getElem_append_right hpτl] - congr 1 - change countSubterms τ + p' - countSubterms τ = p' - omega - refine ⟨countSubterms τ + p' + 1, j_val, hp_lt, by omega, ?_, ?_, ?_⟩ - · rw [hget]; exact hcomp - · rw [hget]; exact hjlt - · rw [hget]; unfold childIdx at *; omega - | letE name τ v b nd ihτ ihv ihb => - intro k hk0 hkn - -- Reducible bridges so omega can link `countSubterms` and `.length`. - have hcτ_len : countSubterms τ = (subtermsList τ).length := rfl - have hcv_len : countSubterms v = (subtermsList v).length := rfl - have hcb_len : countSubterms b = (subtermsList b).length := rfl - have hcnt : countSubterms (.letE name τ v b nd) - = countSubterms τ + countSubterms v + countSubterms b + 1 := by - change (subtermsList (.letE name τ v b nd)).length = _ - simp [subtermsList, List.length_append] - omega - rw [hcnt] at hkn - by_cases hk1 : k = 1 - · have hp_root : 0 < countSubterms (.letE name τ v b nd) := by rw [hcnt]; omega - refine ⟨0, 0, hp_root, by omega, ?_, ?_, ?_⟩ - · change exprIsCompound (.letE name τ v b nd) = true - simp [exprIsCompound, exprChildren] - · change 0 < (exprChildren (.letE name τ v b nd)).length - simp [exprChildren] - · change childIdx (.letE name τ v b nd) 0 0 = k - simp [childIdx, exprChildren] - omega - by_cases hkτ : k < countSubterms τ + 1 - · have hk' : 0 < k - 1 := by omega - have hk'lt : k - 1 < countSubterms τ := by omega - obtain ⟨p', j_val, hp', hp'lt, hcomp, hjlt, hchild⟩ := ihτ (k - 1) hk' hk'lt - have hp1_lt : p' + 1 < countSubterms (.letE name τ v b nd) := by rw [hcnt]; omega - have hp'τv : p' < (subtermsList τ ++ subtermsList v).length := by - rw [List.length_append]; change p' < countSubterms τ + _; omega - have hget : (subtermsList (.letE name τ v b nd)).get ⟨p' + 1, hp1_lt⟩ - = (subtermsList τ).get ⟨p', hp'⟩ := by - change ((.letE name τ v b nd) :: - (subtermsList τ ++ subtermsList v ++ subtermsList b)).get ⟨p' + 1, _⟩ - = (subtermsList τ).get ⟨p', hp'⟩ - simp [List.get_eq_getElem, List.getElem_cons_succ, - List.getElem_append_left hp'τv, - List.getElem_append_left hp'] - refine ⟨p' + 1, j_val, hp1_lt, by omega, ?_, ?_, ?_⟩ - · rw [hget]; exact hcomp - · rw [hget]; exact hjlt - · rw [hget]; unfold childIdx at *; omega - by_cases hkv_root : k = countSubterms τ + 1 - · have hp_root : 0 < countSubterms (.letE name τ v b nd) := by rw [hcnt]; omega - refine ⟨0, 1, hp_root, by omega, ?_, ?_, ?_⟩ - · change exprIsCompound (.letE name τ v b nd) = true - simp [exprIsCompound, exprChildren] - · change 1 < (exprChildren (.letE name τ v b nd)).length - simp [exprChildren] - · change childIdx (.letE name τ v b nd) 0 1 = k - simp [childIdx, exprChildren] - omega - by_cases hkv : k < countSubterms τ + countSubterms v + 1 - · have hk' : 0 < k - 1 - countSubterms τ := by omega - have hk'lt : k - 1 - countSubterms τ < countSubterms v := by omega - obtain ⟨p', j_val, hp', hp'lt, hcomp, hjlt, hchild⟩ := - ihv (k - 1 - countSubterms τ) hk' hk'lt - have hpτl : (subtermsList τ).length ≤ countSubterms τ + p' := by - change countSubterms τ ≤ _; omega - have hpτv : countSubterms τ + p' < (subtermsList τ ++ subtermsList v).length := by - rw [List.length_append]; change countSubterms τ + p' < countSubterms τ + _; omega - have hp_lt : countSubterms τ + p' + 1 < countSubterms (.letE name τ v b nd) := by - rw [hcnt]; omega - have hget : (subtermsList (.letE name τ v b nd)).get - ⟨countSubterms τ + p' + 1, hp_lt⟩ - = (subtermsList v).get ⟨p', hp'⟩ := by - change ((.letE name τ v b nd) :: - (subtermsList τ ++ subtermsList v ++ subtermsList b)).get - ⟨countSubterms τ + p' + 1, _⟩ - = (subtermsList v).get ⟨p', hp'⟩ - rw [List.get_eq_getElem, List.getElem_cons_succ, List.get_eq_getElem, - List.getElem_append_left hpτv, - List.getElem_append_right hpτl] - congr 1 - change countSubterms τ + p' - countSubterms τ = p' - omega - refine ⟨countSubterms τ + p' + 1, j_val, hp_lt, by omega, ?_, ?_, ?_⟩ - · rw [hget]; exact hcomp - · rw [hget]; exact hjlt - · rw [hget]; unfold childIdx at *; omega - by_cases hkb_root : k = countSubterms τ + countSubterms v + 1 - · have hp_root : 0 < countSubterms (.letE name τ v b nd) := by rw [hcnt]; omega - refine ⟨0, 2, hp_root, by omega, ?_, ?_, ?_⟩ - · change exprIsCompound (.letE name τ v b nd) = true - simp [exprIsCompound, exprChildren] - · change 2 < (exprChildren (.letE name τ v b nd)).length - simp [exprChildren] - · change childIdx (.letE name τ v b nd) 0 2 = k - simp [childIdx, exprChildren] - omega - · have hk' : 0 < k - 1 - countSubterms τ - countSubterms v := by omega - have hk'lt : k - 1 - countSubterms τ - countSubterms v < countSubterms b := by omega - obtain ⟨p', j_val, hp', hp'lt, hcomp, hjlt, hchild⟩ := - ihb (k - 1 - countSubterms τ - countSubterms v) hk' hk'lt - have hpτvl : (subtermsList τ ++ subtermsList v).length - ≤ countSubterms τ + countSubterms v + p' := by - rw [List.length_append] - change countSubterms τ + countSubterms v ≤ _ - omega - have hp_lt : countSubterms τ + countSubterms v + p' + 1 - < countSubterms (.letE name τ v b nd) := by - rw [hcnt]; omega - have hget : (subtermsList (.letE name τ v b nd)).get - ⟨countSubterms τ + countSubterms v + p' + 1, hp_lt⟩ - = (subtermsList b).get ⟨p', hp'⟩ := by - change ((.letE name τ v b nd) :: - (subtermsList τ ++ subtermsList v ++ subtermsList b)).get - ⟨countSubterms τ + countSubterms v + p' + 1, _⟩ - = (subtermsList b).get ⟨p', hp'⟩ - rw [List.get_eq_getElem, List.getElem_cons_succ, List.get_eq_getElem, - List.getElem_append_right hpτvl] - congr 1 - rw [List.length_append] - change countSubterms τ + countSubterms v + p' - (countSubterms τ + countSubterms v) = p' - omega - refine ⟨countSubterms τ + countSubterms v + p' + 1, j_val, hp_lt, by omega, ?_, ?_, ?_⟩ - · rw [hget]; exact hcomp - · rw [hget]; exact hjlt - · rw [hget]; unfold childIdx at *; omega - -/-- **Math.** The BDF hypergraph derived from a `Lean.Expr` proof -term. Vertices are subterm-occurrences `Fin (countSubterms e)`; -hyperedges are compound subterms; the colouring is by constructor -kind. The four BDF structural properties (directed, ordered, -coloured, acyclic) hold by construction. - -**Status (Stage B partial).** The DATA fields (`edges`, `edge`'s -inputs/outputs/color) are concrete; the five `Hyperedge` proof -obligations and the top-level `acyclic` / `connected` proofs are -`sorry`'d pending Stage C-D. -/ -def Lean.Expr.toHypergraph (e : Expr) : - Hypergraph (Fin (countSubterms e)) ExprColor where - edges := { i : Fin (countSubterms e) // - exprIsCompound ((subtermsList e).get i) = true } - edge := fun ⟨i, hcomp⟩ => - let s := (subtermsList e).get i - { inArity := (exprChildren s).length - inVertex := fun j => - ⟨childIdx s i.val j.val, by - -- Bound: i.val + 1 + S_j < countSubterms e - -- where S_j = sum of first j children's sizes. - -- Strategy: subtree_size_bound gives i.val + countSubterms s ≤ count_e. - -- countSubterms_eq_sum: countSubterms s = 1 + total children sum. - -- children_take_sum_lt: S_j < total when j < (children s).length. - have hbound : i.val + countSubterms s ≤ countSubterms e := - subtree_size_bound e i - have hsum : countSubterms s = 1 + ((exprChildren s).map countSubterms).sum := - countSubterms_eq_sum s - have hjlt : j.val < (exprChildren s).length := j.isLt - have hSlt : (((exprChildren s).take j.val).map countSubterms).sum - < ((exprChildren s).map countSubterms).sum := by - have := children_take_sum_lt s (j₁ := j.val) (j₂ := (exprChildren s).length) - (le_refl _) hjlt - rwa [List.take_length] at this - unfold childIdx - omega⟩ - outArity := 1 - outVertex := fun _ => i - color := exprColor s - inArity_pos := by - change 0 < (exprChildren ((subtermsList e).get i)).length - have h := hcomp - unfold exprIsCompound at h - cases hc : exprChildren ((subtermsList e).get i) with - | nil => rw [hc] at h; simp at h - | cons _ _ => simp - outArity_pos := by decide - inputOutputDisjoint := by - intro i_idx _ h - -- h : ⟨childIdx s i.val i_idx.val, _⟩ = i (as Fin _) - -- ⟹ childIdx s i.val i_idx.val = i.val - -- ⟹ i.val + 1 + (...sum) = i.val ⟹ contradiction (since +1 makes it strictly bigger) - simp [Fin.ext_iff, childIdx] at h - omega - inVertex_injective := by - intro a b hab - -- hab : ⟨childIdx s i.val a.val, _⟩ = ⟨childIdx s i.val b.val, _⟩ in Fin _ - rw [Fin.mk_eq_mk] at hab - unfold childIdx at hab - -- hab : i.val + 1 + S(a.val) = i.val + 1 + S(b.val) - apply Fin.ext - rcases lt_trichotomy a.val b.val with h | h | h - · exfalso - have hlt := children_take_sum_lt s (j₁ := a.val) (j₂ := b.val) - (le_of_lt b.isLt) h - omega - · exact h - · exfalso - have hlt := children_take_sum_lt s (j₁ := b.val) (j₂ := a.val) - (le_of_lt a.isLt) h - omega - outVertex_injective := fun a b _ => Subsingleton.elim a b } - acyclic := by - -- Rank a vertex by countSubterms e − v.val. Children of any - -- compound sit at strictly larger DFS positions than the - -- compound, so the unique stepRel direction (input → output) goes - -- from larger v.val to smaller, hence strictly increasing rank. - apply Hypergraph.IsAcyclic.of_natRank - (rank := fun v : Fin (countSubterms e) => countSubterms e - v.val) - intro v w hstep - obtain ⟨⟨ip, hcomp⟩, hin, hout⟩ := hstep - obtain ⟨j_in, hjin⟩ := hin - obtain ⟨_, hjout⟩ := hout - -- outVertex is constant `ip`, so `ip = w` ⟹ `w.val = ip.val`. - have hwv : w = ip := hjout.symm - -- inVertex j_in evaluates to `⟨childIdx s ip.val j_in.val, _⟩`, - -- whose `.val` is the DFS index of the child. - have hv_val : v.val = childIdx ((subtermsList e).get ip) ip.val j_in.val := by - rw [← hjin] - have hvgt : v.val > w.val := by - rw [hwv, hv_val] - unfold childIdx - omega - have hvlt : v.val < countSubterms e := v.isLt - change countSubterms e - v.val < countSubterms e - w.val - omega - connected := by - -- Strategy. The incidence relation R is symmetric. We show every - -- vertex reaches the root ⟨0, _⟩ via R, by strong induction on - -- `w.val` using `vertex_has_parent`. Then symmetry + transitivity - -- of `ReflTransGen R` gives the all-pairs connection. - have hpos := countSubterms_pos e - -- Abbreviate the incidence relation by `set`. - set R : Fin (countSubterms e) → Fin (countSubterms e) → Prop := fun a b => - ∃ ed : { i : Fin (countSubterms e) // - exprIsCompound ((subtermsList e).get i) = true }, - let s := (subtermsList e).get ed.val - ((∃ i : Fin (exprChildren s).length, - (⟨childIdx s ed.val.val i.val, by - have hbound : ed.val.val + countSubterms s ≤ countSubterms e := - subtree_size_bound e ed.val - have hsum : countSubterms s - = 1 + ((exprChildren s).map countSubterms).sum := - countSubterms_eq_sum s - have hSlt : (((exprChildren s).take i.val).map countSubterms).sum - < ((exprChildren s).map countSubterms).sum := by - have := children_take_sum_lt s (j₁ := i.val) - (j₂ := (exprChildren s).length) (le_refl _) i.isLt - rwa [List.take_length] at this - unfold childIdx; omega⟩ : Fin (countSubterms e)) = a) ∨ - (∃ _ : Fin 1, ed.val = a)) ∧ - ((∃ i : Fin (exprChildren s).length, - (⟨childIdx s ed.val.val i.val, by - have hbound : ed.val.val + countSubterms s ≤ countSubterms e := - subtree_size_bound e ed.val - have hsum : countSubterms s - = 1 + ((exprChildren s).map countSubterms).sum := - countSubterms_eq_sum s - have hSlt : (((exprChildren s).take i.val).map countSubterms).sum - < ((exprChildren s).map countSubterms).sum := by - have := children_take_sum_lt s (j₁ := i.val) - (j₂ := (exprChildren s).length) (le_refl _) i.isLt - rwa [List.take_length] at this - unfold childIdx; omega⟩ : Fin (countSubterms e)) = b) ∨ - (∃ _ : Fin 1, ed.val = b)) - -- Step 1: every vertex reaches the root via ReflTransGen R. - have h_to_zero : ∀ k (hk : k < countSubterms e), - Relation.ReflTransGen R (⟨k, hk⟩ : Fin (countSubterms e)) ⟨0, hpos⟩ := by - intro k - induction k using Nat.strong_induction_on with - | _ k ih => - intro hk - by_cases hk0 : k = 0 - · subst hk0 - exact Relation.ReflTransGen.refl - · have hkpos : 0 < k := Nat.pos_of_ne_zero hk0 - obtain ⟨p, j_val, hp, hp_lt, hcomp, hjlt, hchild⟩ := - vertex_has_parent e k hkpos hk - have h_step : R ⟨k, hk⟩ ⟨p, hp⟩ := by - refine ⟨⟨⟨p, hp⟩, hcomp⟩, Or.inl ?_, Or.inr ?_⟩ - · refine ⟨⟨j_val, hjlt⟩, ?_⟩ - apply Fin.ext - change childIdx ((subtermsList e).get ⟨p, hp⟩) p j_val = k - exact hchild - · exact ⟨⟨0, by decide⟩, rfl⟩ - exact Relation.ReflTransGen.head h_step (ih p hp_lt hp) - -- Step 2: symmetry + transitivity gives all-pairs. - have R_symm : Symmetric R := by - intro a b ⟨ed, ha, hb⟩; exact ⟨ed, hb, ha⟩ - intro u v - have hu := h_to_zero u.val u.isLt - have hv := h_to_zero v.val v.isLt - have hv' := Relation.ReflTransGen.symmetric R_symm hv - change Relation.ReflTransGen R ⟨u.val, u.isLt⟩ ⟨v.val, v.isLt⟩ - exact hu.trans hv' - -/-! ### Sanity demo - -Move your cursor to the `#eval` / `#check` lines below and look at -the Lean InfoView pane to inspect the converter's behaviour on a -toy expression. Open this file in VS Code with the Lean extension. -/ - -section Demo - -/-- **Eng.** A demo expression: the application `id Nat`, i.e. -`@id Nat` applied to nothing — just `(@id, Nat)` paired. Three -subterm-occurrences: the application itself, `@id`, and `Nat`. -/ -def demoExpr : Expr := - .app (.const ``id [.zero]) (.const ``Nat []) - --- Should print `3`: -#eval countSubterms demoExpr - --- Should print three Exprs in DFS order (app, then id, then Nat): -#eval (subtermsList demoExpr).map (·.dbgToString) - --- The proof that count is positive (for type-checking only): -#check countSubterms_pos demoExpr - --- The converter's signature (body is `sorry`; don't `#eval` it): -#check (Lean.Expr.toHypergraph demoExpr : Hypergraph - (Fin (countSubterms demoExpr)) ExprColor) - -end Demo - -end ProofAtlas.Mapping diff --git a/lean/ProofAtlas/Mapping/ExprColor.lean b/lean/ProofAtlas/Mapping/ExprColor.lean deleted file mode 100644 index 6ff62f2..0000000 --- a/lean/ProofAtlas/Mapping/ExprColor.lean +++ /dev/null @@ -1,56 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Util.Linter.MathTag - -/-! -# `ExprColor`: colour palette for `Lean.Expr → Hypergraph` - -The colour of a hyperedge in the conversion is the *kind* of -`Lean.Expr` constructor that produced it. Twelve constructors, -twelve colours. - -Atomic constructors (`bvar`, `fvar`, `mvar`, `sort`, `const`, `lit`) -appear as leaf *vertices* in the hypergraph but never as edges -(they have zero sub-expressions, violating `inArity_pos`). They are -still in the colour palette so leaves can be tagged with their kind -in metadata, but no edge of those colours exists. - -Compound constructors (`app`, `lam`, `forallE`, `letE`, `mdata`, -`proj`) are realised as hyperedges with inputs = immediate -sub-expressions, output = the compound itself. --/ - -namespace ProofAtlas.Mapping - -/-- **Eng.** One colour per `Lean.Expr` constructor kind. -/ -inductive ExprColor where - /-- Bound variable `bvar i`. -/ - | bvar - /-- Free variable `fvar id`. -/ - | fvar - /-- Metavariable `mvar id`. -/ - | mvar - /-- Universe `sort u`. -/ - | sort - /-- Constant reference `const name us`. -/ - | const - /-- Application `app f a`. -/ - | app - /-- Lambda `lam x τ b`. -/ - | lam - /-- Pi type `forallE x τ b`. -/ - | forallE - /-- Let binding `letE x τ v b`. -/ - | letE - /-- Literal `lit l`. -/ - | lit - /-- Metadata wrapper `mdata d e`. -/ - | mdata - /-- Projection `proj S i s`. -/ - | proj - deriving DecidableEq, Repr - -end ProofAtlas.Mapping diff --git a/lean/ProofAtlas/Metric/Axioms.lean b/lean/ProofAtlas/Metric/Axioms.lean deleted file mode 100644 index f3cf4c8..0000000 --- a/lean/ProofAtlas/Metric/Axioms.lean +++ /dev/null @@ -1,224 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Util.Linter.MathTag -import ProofAtlas.Hypergraph.Basic -import ProofAtlas.Hypergraph.Walk -import Mathlib.Algebra.BigOperators.Group.Finset.Basic -import Mathlib.Data.Fintype.Basic -import Mathlib.Data.Real.Basic -import Mathlib.Data.Matrix.Basic -import Mathlib.Topology.MetricSpace.Pseudo.Defs -import Mathlib.Topology.MetricSpace.Basic - -/-! -# Axiomatic characterisation of the BDF metric (v2 staging) - -`HypergraphMetric H params` packages a per-hyperedge **conductance** -assignment satisfying the four structural axioms of paper §3: - - * **(D) Bipartite** — `κ e u v = 0` for input-input or output-output pairs. - * **(O) Monotone** — the `i`-th input carries no less total conductance - than the `(i+1)`-th. - * **(C) Color separation** — `κ e u v = α(c(e)) · g e u v` for a positional - shape `g` independent of the colour. - * **(A) Output uniformity** — each input distributes its conductance - uniformly over the outputs. - -Derived data — the marginalised conductance `conductance u v` and the -weighted graph Laplacian `laplacian` — are namespace-attached -definitions (not structure fields), so the structure stays minimal. -Future stages of the v2 migration will add the spectral pseudoinverse -`gram = L⁺`, the derived `d u v = (eᵤ - eᵥ)ᵀ L⁺ (eᵤ - eᵥ)` constraint, -and the kernel-derived `stationaryDist`, allowing `Spectral/` to depend -only on this structure rather than on a specific random-walk -construction. - -## Main definitions - -* `HypergraphMetric H params` — conductance + four axioms. -* `HypergraphMetric.conductance` — total conductance across all edges. -* `HypergraphMetric.laplacian` — weighted Laplacian `L = D - A` where `A` is - the conductance matrix and `D` its row-sum diagonal. --/ - -universe u v w - -namespace Hypergraph - -variable {V : Type u} {C : Type v} - -/-- **Math.** Axiomatic BDF metric: a per-hyperedge conductance -assignment `κ : H.edges → V → V → ℝ` satisfying the four axioms -(D) Bipartite, (O) Input monotonicity, (C) Color separation, and -(A) Output uniformity (paper §3). Derived data — `conductance`, -`laplacian`, eventually `gram = L⁺` and a derived `d = effective -resistance` — lives in the `HypergraphMetric` namespace as auxiliary -definitions. -/ -structure HypergraphMetric {V : Type u} {C : Type v} [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) where - /-- Per-hyperedge conductance: `κ e u v` is the conductance between - vertices `u` and `v` contributed by edge `e`. Bipartite vanishing - on input-input / output-output pairs is enforced by the `bipartite` - axiom below (not by typing, so that `κ` can be quantified over arbitrary - vertices in the four axioms). -/ - κ : H.edges → V → V → ℝ - /-- **Axiom (D), Bipartite.** Conductance vanishes between two - inputs of the same edge, and between two outputs of the same edge. - The non-vanishing pairs are exactly the (input, output) pairs of - the edge. -/ - bipartite : ∀ e (u v : V), - (H.isInput u e ∧ H.isInput v e) ∨ (H.isOutput u e ∧ H.isOutput v e) → - κ e u v = 0 - /-- **Axiom (O), Input monotonicity.** The total conductance carried - out of the `i`-th input of any edge — the row sum `∑_v κ e (inVertex e i) v` — - is non-increasing in `i`. The qualitative ordering only; the paper's - harmonic schedule `1/(i+1)` is one admissible realisation. -/ - monotone : ∀ e, Antitone - (fun i : Fin (H.inArity e) => ∑ v : V, κ e (H.inVertex e i) v) - /-- **Axiom (C), Color separation.** Conductance factors as - `κ e u v = α(c(e)) · g e u v` for some positional shape `g` - independent of the colour. -/ - colorSep : ∃ g : H.edges → V → V → ℝ, - ∀ e u v, κ e u v = params.alpha (H.color e) * g e u v - /-- **Axiom (A), Output uniformity.** Each input distributes its - conductance uniformly over the outputs of the same edge: the value - `κ e u (H.outVertex e j)` does not depend on the output index `j`. -/ - outputUnif : ∀ e u (j j' : Fin (H.outArity e)), - κ e u (H.outVertex e j) = κ e u (H.outVertex e j') - /-- **Axiom (S), Symmetry.** Conductance is undirected: `κ e u v = κ e v u` - for every edge. Without this axiom the marginalised `conductance` and - the derived `laplacian` would be asymmetric, breaking the PSD - hypothesis required for effective-resistance theory. -/ - symm : ∀ e (u v : V), κ e u v = κ e v u - -namespace HypergraphMetric - -variable {V : Type u} {C : Type v} [Fintype V] [DecidableEq V] - {H : Hypergraph V C} [Fintype H.edges] {params : WalkParams C} - -/-- **Math.** Marginalised (vertex-pair) conductance: total conductance -between `u` and `v` summed over all hyperedges. The axiomatic -data `κ` is per-edge; the network-level conductance combines edges. -/ -noncomputable def conductance (m : HypergraphMetric H params) (u v : V) : ℝ := - ∑ e : H.edges, m.κ e u v - -/-- **Math.** Weighted graph Laplacian `L = D - A` derived from the -marginalised conductance, where `A_{u,v} = conductance u v` is the -conductance matrix and `D_{u,u} = ∑_w conductance u w` is its row-sum -diagonal. Standard form: `L u u = ∑_w conductance u w` and -`L u v = -conductance u v` for `u ≠ v`. -/ -noncomputable def laplacian (m : HypergraphMetric H params) : Matrix V V ℝ := - fun u v => - if u = v then ∑ w : V, m.conductance u w - else -m.conductance u v - -end HypergraphMetric - -/-- **Math.** A distance family on BDF hypergraphs is a *good BDF -metric* if it satisfies seven checks: four metric axioms -(`d_self`, `d_symm`, `d_triangle`, `d_nonneg`) and three -BDF-sensitivity witnesses showing the distance actually uses the -direction / input ordering / colour weight of the hypergraph. -/ --- doc:start IsHypergraphMetric -structure IsHypergraphMetric [Fintype V] [DecidableEq V] - (d : Hypergraph V C → WalkParams C → V → V → ℝ) : Prop where - /-- **Math.** `d(u, u) = 0`. -/ - d_self : ∀ (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (u : V), d H params u u = 0 - /-- **Math.** `d(u, v) = d(v, u)`. -/ - d_symm : ∀ (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (u v : V), - d H params u v = d H params v u - /-- **Math.** `d(u, w) ≤ d(u, v) + d(v, w)`. -/ - d_triangle : ∀ (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (u v w : V), - d H params u w ≤ d H params u v + d H params v w - /-- **Math.** `0 ≤ d(u, v)`. -/ - d_nonneg : ∀ (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (u v : V), 0 ≤ d H params u v - /-- **Math.** *Direction-sensitive.* There exist two BDF - hypergraphs `H, H'` differing only by swapping one hyperedge's - inputs and outputs, parameters, and a pair `(u, v)` of vertices - on which `d` differs. -/ - sensitive_to_direction : - ∃ (H H' : Hypergraph V C) (_ : Fintype H.edges) (_ : Fintype H'.edges) - (params : WalkParams C) (u v : V), - d H params u v ≠ d H' params u v - /-- **Math.** *Ordering-sensitive.* There exist two BDF - hypergraphs `H, H'` differing only by permuting one hyperedge's - input positions, parameters, and a pair `(u, v)` on which `d` - differs. -/ - sensitive_to_ordering : - ∃ (H H' : Hypergraph V C) (_ : Fintype H.edges) (_ : Fintype H'.edges) - (params : WalkParams C) (u v : V), - d H params u v ≠ d H' params u v - /-- **Math.** *Colour-sensitive.* There exist a BDF hypergraph `H` - and two colour weightings `(params, params')` (same combinatorial - `H`) on which `d` differs. -/ - sensitive_to_coloring : - ∃ (H : Hypergraph V C) (_ : Fintype H.edges) - (params params' : WalkParams C) (u v : V), - d H params u v ≠ d H params' u v --- doc:end - -/-! ## Generic bridges to Mathlib's metric machinery - -Given any `hd : IsHypergraphMetric d` and a choice of hypergraph + walk -parameters, the four metric-axiom fields directly construct a -`PseudoMetricSpace V`. Downstream constructions (Hausdorff distance, -Gromov--Hausdorff distance, anything in `Mathlib.Topology.MetricSpace.*`) -then apply to *any* certified metric without per-metric plumbing. -This is the load-bearing constructor for the Layer-3 -metric-agnostic claim: pick any certified `d`, get all of metric -geometry for free. - -The companion `IsHypergraphMetric.toMetricSpace` requires the extra -strictness hypothesis `d H params u v = 0 → u = v`, which the -spec deliberately does not bake in (the spec covers -`PseudoMetricSpace`-level distances like `jsDist` on collapsing -distributions). Callers that need a `MetricSpace` (e.g. for -Gromov--Hausdorff) supply the strictness alongside their metric. --/ - -namespace IsHypergraphMetric - -variable {V C : Type*} [Fintype V] [DecidableEq V] - {d : Hypergraph V C → WalkParams C → V → V → ℝ} - -/-- **Eng.** The `PseudoMetricSpace V` on the vertex set induced -by any `IsHypergraphMetric d`, choice of hypergraph `H`, and walk -parameters. Not a typeclass instance because `dist` depends on -`(H, params)`; use via `letI := hd.toPseudoMetricSpace H params`. -/ -@[reducible] -noncomputable def toPseudoMetricSpace - (hd : IsHypergraphMetric d) (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) : PseudoMetricSpace V where - dist u v := d H params u v - dist_self := hd.d_self H params - dist_comm := hd.d_symm H params - dist_triangle := hd.d_triangle H params - -/-- **Eng.** The `MetricSpace V` on the vertex set induced by any -`IsHypergraphMetric d` together with a strict-positivity witness -`d H params u v = 0 → u = v` (which the spec deliberately omits; -some BDF metrics collapse on equivalent vertices and only induce a -pseudometric). Used by Gromov--Hausdorff comparisons. -/ -@[reducible] -noncomputable def toMetricSpace - (hd : IsHypergraphMetric d) (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (strict : ∀ u v : V, d H params u v = 0 → u = v) : - MetricSpace V where - dist u v := d H params u v - dist_self := hd.d_self H params - dist_comm := hd.d_symm H params - dist_triangle := hd.d_triangle H params - eq_of_dist_eq_zero {u v} := strict u v - -end IsHypergraphMetric - -end Hypergraph diff --git a/lean/ProofAtlas/Metric/Default.lean b/lean/ProofAtlas/Metric/Default.lean deleted file mode 100644 index fba8ebe..0000000 --- a/lean/ProofAtlas/Metric/Default.lean +++ /dev/null @@ -1,24 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Util.Linter.MetricRegistry -import ProofAtlas.Metric.Axioms -import ProofAtlas.Metric.Resistance.Canonical -import ProofAtlas.Metric.Resistance.HarmonicLaplacian -import ProofAtlas.Metric.Resistance.Algebraic -import ProofAtlas.Metric.Resistance.Geometric -import ProofAtlas.Metric.Resistance.Structural -import ProofAtlas.Metric.Resistance.Resistance -import ProofAtlas.Metric.Resistance.Spectral -import ProofAtlas.Metric.Resistance.Instance - -/-! -# Metric — facade (main branch) - -Resistance distance metric and its `IsHypergraphMetric` instance. -CommuteTime, JensenShannon, Diffusion on `development` branch. --/ - -#guard_metric_registry diff --git a/lean/ProofAtlas/Metric/GromovHausdorff.lean b/lean/ProofAtlas/Metric/GromovHausdorff.lean deleted file mode 100644 index de69283..0000000 --- a/lean/ProofAtlas/Metric/GromovHausdorff.lean +++ /dev/null @@ -1,81 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Util.Linter.MathTag -import ProofAtlas.Metric.Axioms -import Mathlib.Topology.MetricSpace.GromovHausdorff - -/-! -# Gromov–Hausdorff distance between two BDF hypergraphs - -Two entry points, mirroring `Metric/Hausdorff.lean`: - -* `Hypergraph.bdfGHDist (X Y : Type*)` — thin wrapper around - `GromovHausdorff.ghDist` for callers that already have - `[MetricSpace X]`, `[Nonempty X]`, `[CompactSpace X]` (and - symmetrically for `Y`) in scope (legacy path). - -* `IsHypergraphMetric.ghDist hd₁ H₁ params₁ strict₁ hd₂ H₂ params₂ strict₂` - — the generic entry point: given two certified metrics, two - hypergraphs (potentially with different vertex types), and - strict-positivity witnesses on each, compute the Gromov–Hausdorff - distance between the induced metric spaces. The strictness - hypothesis is required because Mathlib's `ghDist` needs - `MetricSpace` (not `PseudoMetricSpace`); `IsHypergraphMetric` covers the - pseudometric case by default, and callers supply - `d H params u v = 0 → u = v` as an extra witness. - -Compactness on each side is automatic via `Finite.compactSpace` for -finite vertex types; `Nonempty` must still be supplied by the -caller. --/ - -namespace Hypergraph - -/-- **Math.** *Gromov–Hausdorff distance* between two metric spaces. -A direct alias of `GromovHausdorff.ghDist`; the metric, nonempty, -and compact instances on `X` and `Y` must be in scope. - -For BDF hypergraphs, prefer the generic -`IsHypergraphMetric.ghDist` (below), which takes the certified metric and -strict-positivity witnesses explicitly and instantiates the local -`MetricSpace V` from them. -/ -noncomputable def bdfGHDist (X Y : Type*) - [MetricSpace X] [Nonempty X] [CompactSpace X] - [MetricSpace Y] [Nonempty Y] [CompactSpace Y] : ℝ := - GromovHausdorff.ghDist X Y - -end Hypergraph - -namespace Hypergraph.IsHypergraphMetric - -variable {V₁ V₂ C₁ C₂ : Type*} - [Fintype V₁] [DecidableEq V₁] [Nonempty V₁] - [Fintype V₂] [DecidableEq V₂] [Nonempty V₂] - {d₁ : Hypergraph V₁ C₁ → WalkParams C₁ → V₁ → V₁ → ℝ} - {d₂ : Hypergraph V₂ C₂ → WalkParams C₂ → V₂ → V₂ → ℝ} - -/-- **Math.** *Generic Gromov–Hausdorff distance.* Given two -certified metrics `hd₁`, `hd₂`, two hypergraphs `H₁`, `H₂` (with -their walk parameters), and strict-positivity witnesses on each, -the Gromov–Hausdorff distance between the induced finite metric -spaces. Compactness is automatic for finite vertex sets via -`Finite.compactSpace`. -/ -noncomputable def ghDist - (hd₁ : IsHypergraphMetric d₁) - (H₁ : Hypergraph V₁ C₁) [Fintype H₁.edges] - (params₁ : WalkParams C₁) - (strict₁ : ∀ u v : V₁, d₁ H₁ params₁ u v = 0 → u = v) - (hd₂ : IsHypergraphMetric d₂) - (H₂ : Hypergraph V₂ C₂) [Fintype H₂.edges] - (params₂ : WalkParams C₂) - (strict₂ : ∀ u v : V₂, d₂ H₂ params₂ u v = 0 → u = v) : ℝ := - letI := hd₁.toMetricSpace H₁ params₁ strict₁ - letI := hd₂.toMetricSpace H₂ params₂ strict₂ - letI : CompactSpace V₁ := Finite.compactSpace - letI : CompactSpace V₂ := Finite.compactSpace - GromovHausdorff.ghDist V₁ V₂ - -end Hypergraph.IsHypergraphMetric diff --git a/lean/ProofAtlas/Metric/Hausdorff.lean b/lean/ProofAtlas/Metric/Hausdorff.lean deleted file mode 100644 index 80d3c82..0000000 --- a/lean/ProofAtlas/Metric/Hausdorff.lean +++ /dev/null @@ -1,92 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Util.Linter.MathTag -import ProofAtlas.Metric.Axioms -import Mathlib.Topology.MetricSpace.HausdorffDistance - -/-! -# Hausdorff distance between sub-collections of `V` - -The two entry points: - -* `Hypergraph.proofHausdorffDist (A B : Finset V) : ℝ` — thin - wrapper around `Metric.hausdorffDist` for callers that already have - a `[PseudoMetricSpace V]` instance in scope (legacy path). - -* `IsHypergraphMetric.hausdorffDist hd H params A B : ℝ` — the generic - entry point: given any certified metric `hd : IsHypergraphMetric d`, a - hypergraph `H`, and walk parameters, compute the Hausdorff - distance between two finite vertex sets `A, B`. Internally - instantiates the `PseudoMetricSpace V` induced by `hd` via - `IsHypergraphMetric.toPseudoMetricSpace` (see `Metric/Axioms.lean`) - and forwards to Mathlib. - -The generic version is what gives `resistanceDist` / `commuteTimeDist` -(and any future certified metric) Hausdorff distance for free, with -all of Mathlib's downstream theory (`hausdorffDist_comm`, -`hausdorffDist_nonneg`, triangle, …) inherited. --/ - -namespace Hypergraph - -variable {V : Type*} [PseudoMetricSpace V] - -/-- **Math.** *Proof-set Hausdorff distance*: the standard real- -valued Hausdorff distance between two finite vertex sets `A, B ⊆ V` -under the ambient `PseudoMetricSpace V`. Forwards directly to -`Metric.hausdorffDist (A : Set V) (B : Set V)`. -/ -noncomputable def proofHausdorffDist (A B : Finset V) : ℝ := - Metric.hausdorffDist (A : Set V) (B : Set V) - -/-- **Math.** The Hausdorff distance is symmetric. -/ -lemma proofHausdorffDist_comm (A B : Finset V) : - proofHausdorffDist A B = proofHausdorffDist B A := by - unfold proofHausdorffDist - exact Metric.hausdorffDist_comm - -/-- **Math.** The Hausdorff distance is non-negative. -/ -lemma proofHausdorffDist_nonneg (A B : Finset V) : - 0 ≤ proofHausdorffDist A B := - Metric.hausdorffDist_nonneg - -end Hypergraph - -namespace Hypergraph.IsHypergraphMetric - -variable {V C : Type*} [Fintype V] [DecidableEq V] - {d : Hypergraph V C → WalkParams C → V → V → ℝ} - -/-- **Math.** *Generic Hausdorff distance.* Given any certified -metric `hd : IsHypergraphMetric d`, hypergraph `H`, and walk parameters, -the Hausdorff distance between two finite vertex sets `A, B ⊆ V` -under `d H params` as the ambient pseudometric. -/ -noncomputable def hausdorffDist - (hd : IsHypergraphMetric d) (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (A B : Finset V) : ℝ := - letI := hd.toPseudoMetricSpace H params - Metric.hausdorffDist (A : Set V) (B : Set V) - -/-- **Math.** Symmetry of the generic Hausdorff distance, inherited -from Mathlib. -/ -lemma hausdorffDist_comm - (hd : IsHypergraphMetric d) (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (A B : Finset V) : - hd.hausdorffDist H params A B = hd.hausdorffDist H params B A := by - unfold hausdorffDist - letI := hd.toPseudoMetricSpace H params - exact Metric.hausdorffDist_comm - -/-- **Math.** Non-negativity of the generic Hausdorff distance, -inherited from Mathlib. -/ -lemma hausdorffDist_nonneg - (hd : IsHypergraphMetric d) (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (A B : Finset V) : - 0 ≤ hd.hausdorffDist H params A B := by - unfold hausdorffDist - letI := hd.toPseudoMetricSpace H params - exact Metric.hausdorffDist_nonneg - -end Hypergraph.IsHypergraphMetric diff --git a/lean/ProofAtlas/Metric/Resistance/Algebraic.lean b/lean/ProofAtlas/Metric/Resistance/Algebraic.lean deleted file mode 100644 index df4005d..0000000 --- a/lean/ProofAtlas/Metric/Resistance/Algebraic.lean +++ /dev/null @@ -1,723 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Util.Linter.MathTag -import ProofAtlas.Metric.Resistance.Canonical -import ProofAtlas.Metric.Resistance.HarmonicLaplacian -import ProofAtlas.LinearAlgebra.EffectiveResistance -import ProofAtlas.LinearAlgebra.MoorePenrose -import Mathlib.LinearAlgebra.Matrix.BilinearForm -import Mathlib.LinearAlgebra.SesquilinearForm.Basic -import Mathlib.Order.ConditionallyCompleteLattice.Basic -import Mathlib.Topology.MetricSpace.Pseudo.Defs - -/-! -# Algebraic view of `d_H` (paper 2 §3.3) - -Under iter-010's A3 refactor `H.resistanceDist params u v` is *by -definition* the effective resistance of the harmonic Laplacian `L_κ` -(see `Canonical.lean`): -``` - d_H(u, v) := (e_u − e_v)^⊤ L_κ⁺ (e_u − e_v). -``` - -This file gathers the *downstream* algebraic content: - -* The §3.3 chain `f^⊤ L_κ f = (1/2) ∑_{u,v} c_κ(u, v) (f(u) − f(v))² - = E_H(f)` (the second equality is the only non-trivial step here; - the first lives in `HarmonicLaplacian.lean` as - `harmonicLaplacian_quadForm`). -* `resistanceDist_eq_effectiveResistance` — now a definitional - identity (`rfl`), formerly the Thomson principle. -* `variational_characterization` and `thomson_principle` — the - recovered *variational* viewpoint: with the algebraic definition in - place, the old `inf { E_H(f) | f u − f v = 1 }` description survives - as a theorem (currently sorry). -* `resistanceDist_triangle` (Klein–Randić) and the bundled - `PseudoMetricSpace` instance. - -The PSD helper `harmonicLaplacian_posSemidef` lives one file up in -`HarmonicLaplacian.lean` because `Canonical.lean`'s -`resistanceDist_nonneg` needs it. --/ - -namespace Hypergraph - -variable {V : Type*} {C : Type*} - -/-! ## Paper §3.3 chain: Dirichlet form = Dirichlet energy - -The two theorems below close the chain -`f^⊤ L_κ f = (1/2) ∑ c_κ (f − f)² = E_H(f)`. The first equality has -been hoisted to `HarmonicLaplacian.lean` -(`harmonicLaplacian_quadForm`) because it is purely matrix-algebraic -and feeds the PSD lemma `harmonicLaplacian_posSemidef`. Inside this -section we use `local notation` matching paper §3.3 so the Lean -statements read like the paper. -/ - -section -variable [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - -local notation "L_κ" => H.harmonicLaplacian params -local notation "c_κ" => H.harmonicConductance params -local notation "E_H" => H.dirichletEnergy params - -/-- **Math.** *Dirichlet form equals Dirichlet energy* (paper 2 §3.3, -second equality): substituting the harmonic conductance schedule -into the standard Laplacian–Dirichlet identity recovers the -per-edge Dirichlet energy `E_H(f)`. The proof reduces the LHS to a -per-edge identity, then evaluates that via the change-of-variables -lemma `harmonicIoWeight_sum_change_of_vars` (which exploits both -input and output position injectivity). -/ -theorem conductance_sum_eq_dirichletEnergy (f : V → ℝ) : - (1/2 : ℝ) * ∑ u : V, ∑ v : V, c_κ u v * (f u - f v)^2 = E_H f := by - -- Per-edge identity: - -- (1/2) · ∑_{u,v} harmonicKappa e u v · (f u − f v)² = perEdgeEnergy e f. - have per_edge : ∀ e : H.edges, - (1/2 : ℝ) * ∑ u : V, ∑ v : V, - H.harmonicKappa params e u v * (f u - f v)^2 - = H.perEdgeEnergy params e f := by - intro e - unfold harmonicKappa - -- Distribute the sum of two `harmonicIoWeight`s. - have split : - ∑ u : V, ∑ v : V, - (H.harmonicIoWeight params e u v + H.harmonicIoWeight params e v u) - * (f u - f v)^2 - = (∑ u : V, ∑ v : V, H.harmonicIoWeight params e u v * (f u - f v)^2) - + ∑ u : V, ∑ v : V, - H.harmonicIoWeight params e v u * (f u - f v)^2 := by - simp only [add_mul, Finset.sum_add_distrib] - rw [split] - -- The second sum equals the first: swap `(u, v) ↔ (v, u)` and use - -- the parity `(f u − f v)² = (f v − f u)²`. - have swap : - ∑ u : V, ∑ v : V, H.harmonicIoWeight params e v u * (f u - f v)^2 - = ∑ u : V, ∑ v : V, H.harmonicIoWeight params e u v * (f u - f v)^2 := by - rw [show ∑ u : V, ∑ v : V, - H.harmonicIoWeight params e v u * (f u - f v)^2 - = ∑ u : V, ∑ v : V, - H.harmonicIoWeight params e v u * (f v - f u)^2 from - Finset.sum_congr rfl fun u _ => - Finset.sum_congr rfl fun v _ => by ring] - exact Finset.sum_comm - rw [swap] - -- (1/2) · (S + S) = S where S is the directed `harmonicIoWeight` sum. - rw [show (1/2 : ℝ) * - ((∑ u : V, ∑ v : V, H.harmonicIoWeight params e u v * (f u - f v)^2) - + ∑ u : V, ∑ v : V, - H.harmonicIoWeight params e u v * (f u - f v)^2) - = ∑ u : V, ∑ v : V, - H.harmonicIoWeight params e u v * (f u - f v)^2 from by ring] - -- Change of variables `(u, v) ↔ (i, j)` via in/out injectivity. - rw [H.harmonicIoWeight_sum_change_of_vars params e - (fun u v => (f u - f v)^2)] - rfl - -- Glue: move `∑_e` from `c_κ` to the outer level, then apply `per_edge`. - unfold harmonicConductance dirichletEnergy - rw [show (∑ u : V, ∑ v : V, - (∑ e : H.edges, H.harmonicKappa params e u v) * (f u - f v)^2) - = ∑ e : H.edges, ∑ u : V, ∑ v : V, - H.harmonicKappa params e u v * (f u - f v)^2 from by - simp_rw [Finset.sum_mul] - rw [show (∑ u : V, ∑ v : V, ∑ e : H.edges, - H.harmonicKappa params e u v * (f u - f v)^2) - = ∑ u : V, ∑ e : H.edges, ∑ v : V, - H.harmonicKappa params e u v * (f u - f v)^2 from - Finset.sum_congr rfl fun _ _ => Finset.sum_comm] - exact Finset.sum_comm] - rw [Finset.mul_sum] - exact Finset.sum_congr rfl fun e _ => per_edge e - -/-- **Math.** Paper 2 §3.3 chain in one shot: the Dirichlet form of -the harmonic Laplacian *is* the Dirichlet energy. -/ -theorem dirichletEnergy_eq_laplacianForm (f : V → ℝ) : - dotProduct (Matrix.mulVec L_κ f) f = E_H f := by - rw [H.harmonicLaplacian_quadForm params f, - H.conductance_sum_eq_dirichletEnergy params f] - -end - -/-! ## Thomson principle: variational = effective resistance - -With the A3 redefinition of `resistanceDist` in `Canonical.lean`, -the Thomson principle is *definitional*: `H.resistanceDist params u v` -unfolds to `effectiveResistance (H.harmonicLaplacian params) u v`. -The corresponding *variational* description survives downstream as a -theorem (`variational_characterization` / `thomson_principle`). -/ - -/-- **Math.** *Thomson principle* (paper 2 thm:closed-form): the -canonical BDF distance equals the effective resistance of the -harmonic Laplacian. Now a definitional identity under A3. -/ -theorem resistanceDist_eq_effectiveResistance [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (u v : V) : - H.resistanceDist params u v - = ProofAtlas.LinearAlgebra.effectiveResistance - (H.harmonicLaplacian params) u v := rfl - -/-! ## Thomson helpers: PSD Cauchy–Schwarz + signed-indicator pairing -/ - -/-- **Eng.** Identify `signedIndicator u v` with the difference of two -Kronecker single vectors. Used to delegate dot-product pairings to -Mathlib's `single_one_dotProduct`. -/ -private lemma signedIndicator_eq_single_sub_single - {V : Type*} [DecidableEq V] (u v : V) : - ProofAtlas.LinearAlgebra.signedIndicator u v - = (Pi.single u (1 : ℝ)) - (Pi.single v (1 : ℝ)) := by - funext w - unfold ProofAtlas.LinearAlgebra.signedIndicator - simp [Pi.single_apply, eq_comm] - -/-- **Eng.** Dot-product pairing identity: `(e_u − e_v) ⬝ᵥ f = f u − f v`. -/ -private lemma signedIndicator_dotProduct - {V : Type*} [Fintype V] [DecidableEq V] (u v : V) (f : V → ℝ) : - ProofAtlas.LinearAlgebra.signedIndicator u v ⬝ᵥ f = f u - f v := by - rw [signedIndicator_eq_single_sub_single, sub_dotProduct, - single_one_dotProduct, single_one_dotProduct] - -/-- **Eng.** *PSD Cauchy–Schwarz on a Hermitian PSD matrix*: the -quadratic form `xᵀ M y` satisfies `(xᵀ M y)² ≤ (xᵀ M x)(yᵀ M y)`. Routes -through Mathlib's `LinearMap.BilinForm.apply_sq_le_of_symm` via -`Matrix.toBilin'`. -/ -private lemma posSemidef_cauchy_schwarz - {V : Type*} [Fintype V] [DecidableEq V] - {M : Matrix V V ℝ} (hM : M.PosSemidef) (x y : V → ℝ) : - (x ⬝ᵥ M.mulVec y) ^ 2 ≤ (x ⬝ᵥ M.mulVec x) * (y ⬝ᵥ M.mulVec y) := by - have hM_trans : M.transpose = M := by - have := hM.isHermitian.eq - rwa [Matrix.conjTranspose_eq_transpose_of_trivial] at this - have hsymm_bilin : (Matrix.toBilin' M).IsSymm := by - rw [LinearMap.BilinForm.isSymm_def] - intro a b - simp only [Matrix.toBilin'_apply'] - -- a ⬝ᵥ M *ᵥ b = b ⬝ᵥ M *ᵥ a - rw [Matrix.dotProduct_mulVec, ← Matrix.mulVec_transpose, hM_trans] - exact dotProduct_comm _ _ - have hsymm : LinearMap.IsSymm (Matrix.toBilin' M) := - LinearMap.BilinForm.isSymm_iff.mp hsymm_bilin - have hnonneg : ∀ z : V → ℝ, 0 ≤ Matrix.toBilin' M z z := by - intro z - rw [Matrix.toBilin'_apply'] - have := hM.dotProduct_mulVec_nonneg z - simpa using this - have h := (Matrix.toBilin' M).apply_sq_le_of_symm hnonneg hsymm x y - simpa only [Matrix.toBilin'_apply'] using h - -/-- **Math.** *Thomson principle* (variational form, paper 2 §3.2): -the original infimum-of-Dirichlet-energies description of `d_H` -recovers `1 / d_H(u, v)` (resistance = inverse conductance). - -**Signature note (iter-012 amendment).** The original signature -required only `0 < d_H(u, v)`. As the kernel diagnosis in -`informal/thomson_principle.md` makes precise, this hypothesis is -*not* sufficient when the harmonic-induced graph is disconnected -with `u, v` in distinct components (in that case the infimum is `0`, -not `1/d_H`, because constant-on-component functions sit in the -feasible set with zero energy). The corrected hypothesis -`h_range : ∃ y, L *ᵥ y = e_u − e_v` says the signed indicator lies in -`range L`, equivalently `e_u − e_v ⊥ ker L`. This is automatically -true for connected harmonic graphs; for disconnected ones the -caller must supply the witness explicitly. -/ -theorem thomson_principle [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (u v : V) (_huv : u ≠ v) - (h_pos : 0 < H.resistanceDist params u v) - (h_range : ∃ y : V → ℝ, - (H.harmonicLaplacian params).mulVec y - = ProofAtlas.LinearAlgebra.signedIndicator u v) : - sInf { e : ℝ | ∃ f : V → ℝ, - f u - f v = 1 ∧ H.dirichletEnergy params f = e } - = 1 / H.resistanceDist params u v := by - set L := H.harmonicLaplacian params with hL_def - set x : V → ℝ := ProofAtlas.LinearAlgebra.signedIndicator u v with hx_def - set d := H.resistanceDist params u v with hd_def - -- d is the quadratic form x^T L⁺ x - have hd_eq : d = x ⬝ᵥ L⁺.mulVec x := by - change H.resistanceDist params u v = _ - unfold resistanceDist ProofAtlas.LinearAlgebra.effectiveResistance - rfl - have hd_ne : d ≠ 0 := ne_of_gt h_pos - -- Structural lemmas - have hL_PSD : L.PosSemidef := H.harmonicLaplacian_posSemidef params - have hL_isHerm : L.IsHermitian := hL_PSD.isHermitian - have hLp_isHerm : (L⁺).IsHermitian := - ProofAtlas.LinearAlgebra.pseudoinverse_isHermitian_of_isHermitian - hL_isHerm - have hLp_PSD : (L⁺).PosSemidef := - ProofAtlas.LinearAlgebra.pseudoinverse_posSemidef hL_PSD - -- Kernel-orthogonality collapse: `L L⁺ x = x`. - have hLLp_x : L.mulVec (L⁺.mulVec x) = x := by - obtain ⟨y, hy⟩ := h_range - -- L L⁺ x = L L⁺ (L y) = (L L⁺ L) y = L y = x - rw [← hy, Matrix.mulVec_mulVec, Matrix.mulVec_mulVec, - ProofAtlas.LinearAlgebra.mul_pseudoinverse_mul_self hL_isHerm] - -- Witness: f* := (1/d) · L⁺ x - set fStar : V → ℝ := (1/d) • L⁺.mulVec x with hfStar_def - -- Sub-claim 1: f*(u) − f*(v) = 1 - have hStar_diff : fStar u - fStar v = 1 := by - have h_xdot : x ⬝ᵥ fStar = (1/d) * (x ⬝ᵥ L⁺.mulVec x) := by - change x ⬝ᵥ ((1/d) • L⁺.mulVec x) = (1/d) * (x ⬝ᵥ L⁺.mulVec x) - rw [dotProduct_smul] - rfl - have : (1/d) * (x ⬝ᵥ L⁺.mulVec x) = 1 := by - rw [← hd_eq]; field_simp - rw [← signedIndicator_dotProduct u v fStar, hx_def.symm] at * - linarith [h_xdot, this] - -- Sub-claim 2: E_H(f*) = 1/d - have hStar_energy : H.dirichletEnergy params fStar = 1/d := by - rw [← H.dirichletEnergy_eq_laplacianForm params fStar] - change (L.mulVec fStar) ⬝ᵥ fStar = 1/d - have hStar_mul : L.mulVec fStar = (1/d) • x := by - change L.mulVec ((1/d) • L⁺.mulVec x) = (1/d) • x - rw [Matrix.mulVec_smul, hLLp_x] - rw [hStar_mul] - change ((1/d) • x) ⬝ᵥ fStar = 1/d - rw [hfStar_def, smul_dotProduct, dotProduct_smul, ← hd_eq] - change (1/d) * ((1/d) * d) = 1/d - field_simp - -- Now use IsLeast.csInf_eq - refine IsLeast.csInf_eq ⟨⟨fStar, hStar_diff, hStar_energy⟩, ?_⟩ - -- Lower bound: ∀ f with f u − f v = 1, E_H(f) ≥ 1/d - rintro e ⟨f, hf_diff, hE_f⟩ - -- Cauchy-Schwarz on L (PSD) at (y := L⁺ x) and f - have hCS := posSemidef_cauchy_schwarz hL_PSD (L⁺.mulVec x) f - -- LHS: (L⁺ x) ⬝ᵥ L *ᵥ f = (L L⁺ x) ⬝ᵥ f = x ⬝ᵥ f = f u - f v = 1 - have hL_trans : L.transpose = L := by - have := hL_isHerm.eq - rwa [Matrix.conjTranspose_eq_transpose_of_trivial] at this - have h_innerLf : (L⁺.mulVec x) ⬝ᵥ L.mulVec f = 1 := by - rw [Matrix.dotProduct_mulVec, ← Matrix.mulVec_transpose, hL_trans, - Matrix.mulVec_mulVec] - -- Now goal: (L * L⁺).mulVec x ⬝ᵥ f = 1, but we want (L L⁺ x) ⬝ᵥ f - rw [← Matrix.mulVec_mulVec, hLLp_x, hx_def, signedIndicator_dotProduct] - exact hf_diff - -- (L⁺ x) ⬝ᵥ L *ᵥ (L⁺ x) = d - have h_innerLL : (L⁺.mulVec x) ⬝ᵥ L.mulVec (L⁺.mulVec x) = d := by - rw [hLLp_x] - -- (L⁺ x) ⬝ᵥ x = x ⬝ᵥ L⁺ x = d - rw [dotProduct_comm, ← hd_eq] - -- Apply Cauchy-Schwarz: 1² ≤ d · E_H(f) - rw [h_innerLf, h_innerLL] at hCS - -- f ⬝ᵥ L *ᵥ f = E_H(f) = e - have hE_eq : f ⬝ᵥ L.mulVec f = e := by - rw [dotProduct_comm, ← hE_f] - exact H.dirichletEnergy_eq_laplacianForm params f - rw [hE_eq] at hCS - -- hCS : 1^2 ≤ d * e, i.e., 1 ≤ d * e, so e ≥ 1/d - have h_pos_e : 1 ≤ d * e := by simpa using hCS - have hd_pos : 0 < d := h_pos - rw [show (1 : ℝ) / d = 1 / d from rfl] - exact (div_le_iff₀ hd_pos).mpr (by linarith) - -/-! ## Variational characterisation (Thomson dual: sSup form) -/ - -/-- **Eng.** *Quadratic scaling of the Dirichlet energy*: -`E_H(a • f) = a² · E_H(f)`. Direct unfolding through `dirichletEnergy`, -`perEdgeEnergy`, `posDifference`; each squared difference factors -`(a·x − a·y)² = a²·(x − y)²` and `a²` pulls out of the triple sum via -`Finset.mul_sum`. Used in `variational_characterization` to rescale -arbitrary test functions onto Thomson's feasible set -`{f : f u − f v = 1}`. -/ -private lemma dirichletEnergy_smul - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (a : ℝ) (f : V → ℝ) : - H.dirichletEnergy params (a • f) = a^2 * H.dirichletEnergy params f := by - unfold dirichletEnergy perEdgeEnergy - rw [Finset.mul_sum] - refine Finset.sum_congr rfl fun e _ => ?_ - rw [Finset.mul_sum] - refine Finset.sum_congr rfl fun i _ => ?_ - rw [Finset.mul_sum] - refine Finset.sum_congr rfl fun j _ => ?_ - unfold posDifference - simp only [Pi.smul_apply, smul_eq_mul] - ring - -/-- **Math.** *Variational characterisation* of `d_H` (paper 2 §3.2 dual -form of the Thomson principle): the algebraic effective resistance -equals the supremum of `(f u − f v)² / E_H(f)` over functions with -positive Dirichlet energy, with the `{0}` adjoint absorbing the -degenerate `E_H(f) = 0` case. - -**Signature note (iter-013 amendment).** Like `thomson_principle`, -the variational sSup characterisation requires the connectivity -hypotheses `_huv`, `h_pos`, and `h_range`. Without `h_range` the -identity fails: in a disconnected harmonic-induced graph with -`u, v` in distinct components, `d_H(u,v) = 0` (kernel projection -collapses) yet the feasible set can still contain ratios with -arbitrarily large positive values (e.g. take `f` constant on each -component with distinct values across components, then -`(f u − f v)² > 0` while `E_H(f) > 0` arbitrary small via scaling). -The signature mirrors Thomson's amendment exactly. -/ -theorem variational_characterization [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (u v : V) (_huv : u ≠ v) - (h_pos : 0 < H.resistanceDist params u v) - (h_range : ∃ y : V → ℝ, - (H.harmonicLaplacian params).mulVec y - = ProofAtlas.LinearAlgebra.signedIndicator u v) : - H.resistanceDist params u v - = sSup ({ r : ℝ | ∃ f : V → ℝ, H.dirichletEnergy params f > 0 - ∧ (f u - f v)^2 / H.dirichletEnergy params f = r } ∪ {0}) := by - set L := H.harmonicLaplacian params with hL_def - set x : V → ℝ := ProofAtlas.LinearAlgebra.signedIndicator u v with hx_def - set d := H.resistanceDist params u v with hd_def - -- d is the quadratic form x^T L⁺ x. - have hd_eq : d = x ⬝ᵥ L⁺.mulVec x := by - change H.resistanceDist params u v = _ - unfold resistanceDist ProofAtlas.LinearAlgebra.effectiveResistance - rfl - have hd_pos : 0 < d := h_pos - have hd_ne : d ≠ 0 := ne_of_gt h_pos - -- Structural lemmas. - have hL_PSD : L.PosSemidef := H.harmonicLaplacian_posSemidef params - have hL_isHerm : L.IsHermitian := hL_PSD.isHermitian - -- Kernel-orthogonality collapse: `L L⁺ x = x`. - have hLLp_x : L.mulVec (L⁺.mulVec x) = x := by - obtain ⟨y, hy⟩ := h_range - rw [← hy, Matrix.mulVec_mulVec, Matrix.mulVec_mulVec, - ProofAtlas.LinearAlgebra.mul_pseudoinverse_mul_self hL_isHerm] - -- Thomson witness fStar := (1/d) • L⁺ x: re-derive its two key - -- properties locally (these mirror Thomson's proof body). - set fStar : V → ℝ := (1/d) • L⁺.mulVec x with hfStar_def - have hStar_diff : fStar u - fStar v = 1 := by - have h_xdot : x ⬝ᵥ fStar = (1/d) * (x ⬝ᵥ L⁺.mulVec x) := by - change x ⬝ᵥ ((1/d) • L⁺.mulVec x) = (1/d) * (x ⬝ᵥ L⁺.mulVec x) - rw [dotProduct_smul]; rfl - have h_one : (1/d) * (x ⬝ᵥ L⁺.mulVec x) = 1 := by - rw [← hd_eq]; field_simp - have h_pair : x ⬝ᵥ fStar = fStar u - fStar v := by - rw [hx_def]; exact signedIndicator_dotProduct u v fStar - linarith [h_xdot, h_one, h_pair] - have hStar_energy : H.dirichletEnergy params fStar = 1/d := by - rw [← H.dirichletEnergy_eq_laplacianForm params fStar] - change (L.mulVec fStar) ⬝ᵥ fStar = 1/d - have hStar_mul : L.mulVec fStar = (1/d) • x := by - change L.mulVec ((1/d) • L⁺.mulVec x) = (1/d) • x - rw [Matrix.mulVec_smul, hLLp_x] - rw [hStar_mul] - change ((1/d) • x) ⬝ᵥ fStar = 1/d - rw [hfStar_def, smul_dotProduct, dotProduct_smul, ← hd_eq] - change (1/d) * ((1/d) * d) = 1/d - field_simp - have hStar_energy_pos : 0 < H.dirichletEnergy params fStar := by - rw [hStar_energy]; positivity - have hStar_ratio : - (fStar u - fStar v)^2 / H.dirichletEnergy params fStar = d := by - rw [hStar_diff, hStar_energy]; field_simp - -- The sSup set S and its membership of `d` (witnessed by `fStar`). - set S : Set ℝ := - { r : ℝ | ∃ f : V → ℝ, H.dirichletEnergy params f > 0 - ∧ (f u - f v)^2 / H.dirichletEnergy params f = r } ∪ {0} - with hS_def - have hd_in_S : d ∈ S := Or.inl ⟨fStar, hStar_energy_pos, hStar_ratio⟩ - have hZero_in_S : (0 : ℝ) ∈ S := Or.inr rfl - -- Thomson principle: sInf of feasible Dirichlet energies = 1/d. - have hThomson := H.thomson_principle params u v _huv h_pos h_range - -- Upper bound: every r ∈ S satisfies r ≤ d. - have hUB : ∀ r ∈ S, r ≤ d := by - intro r hr - rcases hr with hr_set | hr_zero - · obtain ⟨f, hE_pos, hf_eq⟩ := hr_set - -- Reduce to (f u - f v)^2 / E_H f ≤ d. - rw [← hf_eq] - by_cases hc : f u - f v = 0 - · rw [hc] - simp only [ne_eq, OfNat.ofNat_ne_zero, not_false_eq_true, zero_pow, - zero_div] - exact hd_pos.le - · -- Rescale: g := (1/(f u - f v)) • f has g u - g v = 1. - set c := f u - f v with hc_def - have hc_ne : c ≠ 0 := hc - set g : V → ℝ := (1/c) • f with hg_def - have hg_diff : g u - g v = 1 := by - change (1/c) * f u - (1/c) * f v = 1 - rw [← mul_sub, ← hc_def] - field_simp - have hg_energy : H.dirichletEnergy params g - = H.dirichletEnergy params f / c^2 := by - rw [hg_def, H.dirichletEnergy_smul params (1/c) f] - rw [div_pow, one_pow] - field_simp - have hg_energy_pos : 0 < H.dirichletEnergy params g := by - rw [hg_energy]; exact div_pos hE_pos (by positivity) - -- E_H(g) is in Thomson's feasible set. - have hg_in : H.dirichletEnergy params g ∈ - { e : ℝ | ∃ f' : V → ℝ, - f' u - f' v = 1 ∧ H.dirichletEnergy params f' = e } := - ⟨g, hg_diff, rfl⟩ - -- Thomson lower bound: 1/d = sInf ≤ E_H g. - have hE_g_ge : 1/d ≤ H.dirichletEnergy params g := by - rw [← hThomson] - refine csInf_le ?_ hg_in - refine ⟨0, ?_⟩ - rintro e ⟨f', _, hef'⟩ - rw [← hef'] - exact H.dirichletEnergy_nonneg params f' - -- (f u - f v)^2 / E_H f = c^2 / E_H f = 1 / E_H g ≤ d. - -- Direct algebra: from 1/d ≤ E_H g and E_H g > 0, we have - -- 1 ≤ d * E_H g, i.e., c^2 ≤ d * E_H f (via E_H g = E_H f / c^2). - have hE_f_pos : 0 < H.dirichletEnergy params f := hE_pos - have h_one_le : 1 ≤ d * H.dirichletEnergy params g := by - have hstep := mul_le_mul_of_nonneg_left hE_g_ge hd_pos.le - -- hstep : d * (1/d) ≤ d * E_H g - have hdd : d * (1/d) = 1 := by field_simp - rw [hdd] at hstep - exact hstep - -- c^2 ≤ d * E_H f. - have h_csq_le : c^2 ≤ d * H.dirichletEnergy params f := by - have hc_sq_pos : 0 < c^2 := by positivity - have h1 : 1 ≤ d * (H.dirichletEnergy params f / c^2) := by - rwa [← hg_energy] - have h2 := mul_le_mul_of_nonneg_right h1 hc_sq_pos.le - rw [one_mul] at h2 - calc c^2 ≤ d * (H.dirichletEnergy params f / c^2) * c^2 := h2 - _ = d * H.dirichletEnergy params f := by field_simp - exact (div_le_iff₀ hE_f_pos).mpr h_csq_le - · -- r = 0 ≤ d. - rcases hr_zero with rfl - exact hd_pos.le - -- IsLUB → csSup_eq. - refine (IsLUB.csSup_eq ?_ ⟨0, hZero_in_S⟩).symm - refine ⟨hUB, ?_⟩ - intro b hb - exact hb hd_in_S - -/-! ## Maximum principle for harmonic potentials -/ - -/-- **Math.** *Discrete maximum principle for a harmonic potential.* -Let `g : V → ℝ` satisfy `L_κ · g = e_v − e_w` for some `v ≠ w`. Under -connectivity of the harmonic graph, `g(v)` is the maximum of `g` -over `V`, i.e. `g(u) ≤ g(v)` for all `u`. Proof: argmax(g) is closed -under positive-conductance neighbours at every interior vertex (i.e. -not `v` or `w`), `w ∉ argmax(g)` (because `(L g)(w) = -1 < 0` while -the sum-of-non-negatives form would force `≥ 0` if `w` were argmax), -and by connectivity a path from any argmax vertex to `v` must -eventually hit the boundary of argmax. That boundary vertex `y` is -in argmax with a non-argmax neighbour `z`; the only way to satisfy -saturation is `y = v` (since `y ≠ w`), forcing `v ∈ argmax`. -/ -private lemma harmonic_potential_le_at_source [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - (hConn : H.harmonicConnected params) - {g : V → ℝ} {v w : V} - (hLg : (H.harmonicLaplacian params).mulVec g - = ProofAtlas.LinearAlgebra.signedIndicator v w) - (hvw : v ≠ w) (u : V) : - g u ≤ g v := by - classical - -- M := max_x g(x), attained at some x_max ∈ V. - obtain ⟨x_max, _, hx_max_le⟩ := - Finset.exists_max_image Finset.univ g ⟨u, Finset.mem_univ _⟩ - -- It suffices to show g v = g x_max. - suffices hgv_max : g v = g x_max by - rw [hgv_max]; exact hx_max_le u (Finset.mem_univ _) - -- Suppose not: g v < g x_max (since g v ≤ g x_max always). - by_contra hne - have hgv_lt : g v < g x_max := - lt_of_le_of_ne (hx_max_le v (Finset.mem_univ _)) hne - -- Claim 1: g w < g x_max. - have hgw_lt : g w < g x_max := by - by_contra hgw_not_lt - push_neg at hgw_not_lt - have hgw_eq : g w = g x_max := - le_antisymm (hx_max_le w (Finset.mem_univ _)) hgw_not_lt - -- (L g)(w) = signedIndicator v w w = -1 (since v ≠ w). - have hLg_w : (H.harmonicLaplacian params).mulVec g w = -1 := by - rw [hLg] - show ProofAtlas.LinearAlgebra.signedIndicator v w w = -1 - simp [ProofAtlas.LinearAlgebra.signedIndicator, hvw.symm] - -- (L g)(w) = ∑ x, c(w, x) * (g w - g x) ≥ 0. - have hLg_w_sum := H.harmonicLaplacian_apply_eq_sum_diff params g w - have h_each_nn : ∀ x ∈ Finset.univ, - 0 ≤ H.harmonicConductance params w x * (g w - g x) := by - intro x _ - refine mul_nonneg (H.harmonicConductance_nonneg params w x) ?_ - rw [hgw_eq] - linarith [hx_max_le x (Finset.mem_univ _)] - have h_sum_nn : 0 ≤ ∑ x, H.harmonicConductance params w x * (g w - g x) := - Finset.sum_nonneg h_each_nn - rw [hLg_w_sum] at hLg_w - linarith - -- Claim 2: saturation — for any y in argmax with y ≠ v, w, every - -- positive-conductance neighbour z has g z = g x_max. - have hsat : ∀ y : V, g y = g x_max → y ≠ v → y ≠ w → - ∀ z : V, 0 < H.harmonicConductance params y z → g z = g x_max := by - intro y hy_max hy_ne_v hy_ne_w z hcz_pos - -- (L g)(y) = signedIndicator(v, w)(y) = 0 since y ≠ v, y ≠ w. - have hLg_y : (H.harmonicLaplacian params).mulVec g y = 0 := by - rw [hLg] - show ProofAtlas.LinearAlgebra.signedIndicator v w y = 0 - simp [ProofAtlas.LinearAlgebra.signedIndicator, - fun h : y = v => hy_ne_v h, fun h : y = w => hy_ne_w h] - have hLg_y_sum := H.harmonicLaplacian_apply_eq_sum_diff params g y - rw [hLg_y] at hLg_y_sum - have h_each_nn : ∀ x ∈ Finset.univ, - 0 ≤ H.harmonicConductance params y x * (g y - g x) := by - intro x _ - refine mul_nonneg (H.harmonicConductance_nonneg params y x) ?_ - rw [hy_max] - linarith [hx_max_le x (Finset.mem_univ _)] - -- Sum of nonneg = 0 ⟹ each term = 0. - have h_each_zero : ∀ x ∈ Finset.univ, - H.harmonicConductance params y x * (g y - g x) = 0 := - (Finset.sum_eq_zero_iff_of_nonneg h_each_nn).mp hLg_y_sum.symm - -- Apply to x = z. - have hyz : H.harmonicConductance params y z * (g y - g z) = 0 := - h_each_zero z (Finset.mem_univ _) - have hcyz_ne : H.harmonicConductance params y z ≠ 0 := ne_of_gt hcz_pos - have hgyz : g y - g z = 0 := (mul_eq_zero.mp hyz).resolve_left hcyz_ne - linarith [hgyz, hy_max] - -- Path induction: from x_max, walk via positive-conductance edges - -- to v. Each step stays in argmax (by hsat), until we hit a vertex - -- ≠ v, w — but the final vertex is v, contradiction with v ∉ argmax. - have hpath : Relation.ReflTransGen - (fun x y => 0 < H.harmonicConductance params x y) x_max v := - hConn x_max v - -- Induct on the path: at each step, saturation propagates argmax to next vertex. - -- Goal: g x_max = g x_max → False (which yields False by applying to rfl). - have hP : g x_max = g x_max → False := by - refine @Relation.ReflTransGen.head_induction_on V - (fun x y => 0 < H.harmonicConductance params x y) v - (fun y _ => g y = g x_max → False) x_max hpath ?_ ?_ - · -- refl: at y = v. - intro hgv_max - exact hne hgv_max - · -- head: from R y z, RTG z v, P z (the IH), prove P y. - intro y z hyz _ Pz hgy_max - by_cases hyv : y = v - · exact hne (hyv ▸ hgy_max) - by_cases hyw : y = w - · subst hyw; linarith [hgw_lt, hgy_max] - exact Pz (hsat y hgy_max hyv hyw z hyz) - exact hP rfl - -/-! ## Triangle inequality (Klein–Randić) -/ - -/-- **Math.** Metric axiom (triangle inequality): -`d_H(u, w) ≤ d_H(u, v) + d_H(v, w)`. The classical Klein–Randić -theorem (Klein, Randić 1993, J. Math. Chem.). Routes through the -**off-diagonal sign condition** on the harmonic-Laplacian -pseudoinverse: `(e_v − e_u)ᵀ L⁺ (e_v − e_w) ≥ 0`. The latter holds -because `g := L⁺ · (e_v − e_w)` is the harmonic potential function -(Kirchhoff voltage at unit current `v → w`) and satisfies the -discrete maximum principle (`g(v) ≥ g(u)` for all `u`) under -connectivity of the harmonic graph. - -**Signature amendment** mirrors `_rayleigh`'s connectivity package: -adds `hConn` (harmonic graph connected) and `h_range` (signed -indicator `e_v − e_w` in the range of `L_κ`; automatic under -connectivity but supplied explicitly to avoid building the -range/kernel-orthogonal-complement bridge here). -/ -theorem resistanceDist_triangle [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (u v w : V) - (hvw : v ≠ w) - (hConn : H.harmonicConnected params) - (h_range : ∃ y : V → ℝ, - (H.harmonicLaplacian params).mulVec y - = ProofAtlas.LinearAlgebra.signedIndicator v w) : - H.resistanceDist params u w - ≤ H.resistanceDist params u v + H.resistanceDist params v w := by - -- Polarisation identity (over symmetric bilinear form L⁺): - -- R(u,w) − R(u,v) − R(v,w) = −2 · (e_v − e_u)ᵀ L⁺ (e_v − e_w) - -- So it suffices to show `(e_v − e_u)ᵀ L⁺ (e_v − e_w) ≥ 0`. - -- Define g := L⁺ (e_v − e_w). Then (e_v − e_u)ᵀ g = g(v) − g(u). - -- Apply the max-principle to conclude g(v) ≥ g(u). - set L := H.harmonicLaplacian params with hL_def - set yvw : V → ℝ := ProofAtlas.LinearAlgebra.signedIndicator v w with hyvw_def - set g : V → ℝ := L⁺.mulVec yvw with hg_def - -- Step 1: L g = e_v - e_w (using h_range and MP1). - have hL_PSD : L.PosSemidef := H.harmonicLaplacian_posSemidef params - have hL_isHerm : L.IsHermitian := hL_PSD.isHermitian - have hLg : L.mulVec g = yvw := by - obtain ⟨y, hy⟩ := h_range - rw [hg_def, ← hy, Matrix.mulVec_mulVec, Matrix.mulVec_mulVec, - ProofAtlas.LinearAlgebra.mul_pseudoinverse_mul_self hL_isHerm] - -- Step 2: By max-principle, g(v) ≥ g(u). - have hgu_le_gv : g u ≤ g v := - H.harmonic_potential_le_at_source params hConn hLg hvw u - -- Step 3: Polarisation. Define R := resistanceDist params. - -- R(u, v) = (e_u - e_v) ⬝ᵥ L⁺ (e_u - e_v) by definition. - have hRuw : H.resistanceDist params u w - = ProofAtlas.LinearAlgebra.signedIndicator u w ⬝ᵥ - L⁺.mulVec (ProofAtlas.LinearAlgebra.signedIndicator u w) := rfl - have hRuv : H.resistanceDist params u v - = ProofAtlas.LinearAlgebra.signedIndicator u v ⬝ᵥ - L⁺.mulVec (ProofAtlas.LinearAlgebra.signedIndicator u v) := rfl - have hRvw : H.resistanceDist params v w - = ProofAtlas.LinearAlgebra.signedIndicator v w ⬝ᵥ - L⁺.mulVec (ProofAtlas.LinearAlgebra.signedIndicator v w) := rfl - -- Key algebraic identity: signedIndicator u w = signedIndicator u v + signedIndicator v w - have hsig_decomp : - ProofAtlas.LinearAlgebra.signedIndicator u w - = ProofAtlas.LinearAlgebra.signedIndicator u v - + ProofAtlas.LinearAlgebra.signedIndicator v w := by - funext x - simp [ProofAtlas.LinearAlgebra.signedIndicator] - -- Expand R(u, w) using bilinearity of (xᵀ L⁺ y). - -- xᵀ L⁺ (a + b) = xᵀ L⁺ a + xᵀ L⁺ b; (a+b)ᵀ L⁺ x = aᵀ L⁺ x + bᵀ L⁺ x. - -- So (a+b)ᵀ L⁺ (a+b) = aᵀ L⁺ a + 2 aᵀ L⁺ b + bᵀ L⁺ b (under symmetry of L⁺). - have hLp_isHerm : (L⁺).IsHermitian := - ProofAtlas.LinearAlgebra.pseudoinverse_isHermitian_of_isHermitian hL_isHerm - have hLp_trans : (L⁺).transpose = L⁺ := by - have := hLp_isHerm.eq - rwa [Matrix.conjTranspose_eq_transpose_of_trivial] at this - -- Cross-term identity: - -- (e_u - e_v)ᵀ L⁺ (e_v - e_w) = -((e_v - e_u)ᵀ L⁺ (e_v - e_w)) - -- = -(g(v) - g(u)). - set xuv : V → ℝ := ProofAtlas.LinearAlgebra.signedIndicator u v with hxuv_def - -- xuv ⬝ᵥ L⁺ yvw = xuv ⬝ᵥ g - have h_cross : xuv ⬝ᵥ g = - (g v - g u) := by - rw [hg_def] - -- xuv ⬝ᵥ L⁺.mulVec yvw = g(u) - g(v) [since xuv = e_u - e_v, signedIndicator dot f = f u - f v] - have hxuv_g : xuv ⬝ᵥ g = g u - g v := by - rw [hg_def]; exact signedIndicator_dotProduct u v _ - linarith [hxuv_g] - -- Polarisation: R(u, w) = R(u, v) + 2 · (xuv ⬝ᵥ L⁺ yvw) + R(v, w). - have hPolarise : H.resistanceDist params u w - = H.resistanceDist params u v + 2 * (xuv ⬝ᵥ g) + H.resistanceDist params v w := by - rw [hRuw, hsig_decomp] - rw [Matrix.mulVec_add] - rw [add_dotProduct, dotProduct_add, dotProduct_add] - -- (xuv + yvw) ⬝ᵥ (L⁺ xuv + L⁺ yvw) - -- = xuv ⬝ᵥ L⁺ xuv + xuv ⬝ᵥ L⁺ yvw + yvw ⬝ᵥ L⁺ xuv + yvw ⬝ᵥ L⁺ yvw - -- = R(u, v) + 2 · (xuv ⬝ᵥ L⁺ yvw) + R(v, w) - -- using yvw ⬝ᵥ L⁺ xuv = (xuv) ⬝ᵥ L⁺ yvw by symmetry of L⁺ - rw [show yvw ⬝ᵥ L⁺.mulVec xuv = xuv ⬝ᵥ L⁺.mulVec yvw from by - rw [Matrix.dotProduct_mulVec, ← Matrix.mulVec_transpose, hLp_trans] - exact dotProduct_comm _ _] - rw [hRuv, hRvw, ← hg_def] - ring - -- (e_u - e_v)ᵀ L⁺ (e_v - e_w) = -(g v - g u) ≤ 0 (since g v ≥ g u). - have h_cross_nonpos : xuv ⬝ᵥ g ≤ 0 := by - rw [h_cross]; linarith - -- Therefore R(u, w) ≤ R(u, v) + R(v, w). - linarith [hPolarise, h_cross_nonpos] - -/-! ## Bundled pseudometric structure -/ - -/-- **Math.** The `(V, d_H)` pseudometric space induced by the BDF -distance. Use via `letI := H.toPseudoMetricSpace params`. (Not a -typeclass instance because `dist` depends on `H` and `params`.) -/ -@[reducible] -noncomputable def toPseudoMetricSpace [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) : PseudoMetricSpace V where - dist u v := H.resistanceDist params u v - dist_self := H.resistanceDist_self params - dist_comm := H.resistanceDist_symm params - dist_triangle := by - /- The unconditional bundle requires the connectivity-aware - `_triangle` to specialise via wrappers that discharge `hConn` and - `h_range` for every `(v, w)` pair. Postponed; downstream consumers - that need a `PseudoMetricSpace` instance should use the connected - wrapper once it lands. -/ - sorry - -end Hypergraph diff --git a/lean/ProofAtlas/Metric/Resistance/Canonical.lean b/lean/ProofAtlas/Metric/Resistance/Canonical.lean deleted file mode 100644 index e9726ae..0000000 --- a/lean/ProofAtlas/Metric/Resistance/Canonical.lean +++ /dev/null @@ -1,173 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Util.Linter.MathTag -import ProofAtlas.Hypergraph.Basic -import ProofAtlas.Hypergraph.Decidability -import ProofAtlas.Hypergraph.Walk -import ProofAtlas.Metric.Resistance.HarmonicLaplacian -import ProofAtlas.LinearAlgebra.EffectiveResistance -import Mathlib.Algebra.BigOperators.Group.Finset.Basic -import Mathlib.Data.Fintype.Basic -import Mathlib.Data.Real.Basic -import Mathlib.Data.Matrix.Basic -import Mathlib.Order.ConditionallyCompleteLattice.Basic -import Mathlib.Topology.MetricSpace.Pseudo.Defs - -/-! -# The canonical BDF distance (algebraic effective resistance) - -The **primary definition** of the BDF distance `d_H` on a BDF -hypergraph, parameterised by `α : C → ℝ_{>0}`. Paper 2 §3.1–§3.3. - -Under iter-010's A3 refactor the canonical definition is the -*algebraic* one: -``` - H.resistanceDist params u v - := (e_u − e_v)ᵀ · L_κ⁺ · (e_u − e_v) -``` -i.e. the effective resistance of the harmonic Laplacian `L_κ`. This -collapses the Thomson principle (`Algebraic.lean`, -`resistanceDist_eq_effectiveResistance`) to definitional equality -and lets `_self`, `_symm`, `_nonneg` delegate straight to the -matrix-algebra side (`ProofAtlas.LinearAlgebra.effectiveResistance_*`). - -This file owns the Dirichlet energy (paper 2 §3.1), the canonical -definition, and the three elementary metric axioms. The other -viewpoints live in sibling files: - - * `Algebraic.lean` — `d_H = e^⊤ L_κ⁺ e` (Thomson principle is now `rfl`), - Klein–Randić triangle, the `PseudoMetricSpace` - instance, and the variational characterisation - of `d_H` as an infimum of Dirichlet energies. - * `Spectral.lean` — `d_H = ∑ (φ_i(u)−φ_i(v))²/λ_i`. - * `Geometric.lean` — `d_H = ‖L_κ^{+/2}(e_u−e_v)‖²` (Hilbert embedding). - -The PSD helper (`harmonicLaplacian_posSemidef`) feeding `_nonneg` -lives in `HarmonicLaplacian.lean` so that `Canonical.lean` may use -it without re-exporting `Algebraic.lean`'s downstream identities. --/ - -namespace Hypergraph - -variable {V : Type*} {C : Type*} - -/-! ## Dirichlet energy (paper 2 §3.1) -/ - -/-- **Math.** Position-pair scalar weight `α(c(e)) / ((i+1) · q_e)`. -/ -noncomputable def positionWeight - (H : Hypergraph V C) (params : WalkParams C) - (e : H.edges) (i : Fin (H.inArity e)) (_j : Fin (H.outArity e)) : ℝ := - params.alpha (H.color e) / ((i.val + 1) * H.outArity e : ℕ) - -/-- **Math.** Voltage drop across position pair `(i, j)` of edge `e`. -/ -def posDifference - (H : Hypergraph V C) (f : V → ℝ) - (e : H.edges) (i : Fin (H.inArity e)) (j : Fin (H.outArity e)) : ℝ := - f (H.inVertex e i) - f (H.outVertex e j) - -/-- **Math.** Per-edge contribution to the Dirichlet energy: -`∑_{i,j} positionWeight · posDifference²`. -/ -noncomputable def perEdgeEnergy - (H : Hypergraph V C) (params : WalkParams C) - (e : H.edges) (f : V → ℝ) : ℝ := - ∑ i : Fin (H.inArity e), ∑ j : Fin (H.outArity e), - H.positionWeight params e i j * (H.posDifference f e i j)^2 - -/-- **Math.** *Dirichlet energy of `f` against `H`* (paper 2 §3.1): -``` - E_H(f) = ∑_e α(c(e)) · ∑_{i,j} (1 / ((i+1)·q_e)) - · (f(inVertex_e i) − f(outVertex_e j))² -``` -A positive semidefinite quadratic form in `f`. -/ -noncomputable def dirichletEnergy - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (f : V → ℝ) : ℝ := - ∑ e : H.edges, H.perEdgeEnergy params e f - -/-- **Math.** Per-edge energy is nonnegative. -/ -lemma perEdgeEnergy_nonneg - (H : Hypergraph V C) (params : WalkParams C) - (e : H.edges) (f : V → ℝ) : - 0 ≤ H.perEdgeEnergy params e f := by - unfold perEdgeEnergy - refine Finset.sum_nonneg fun i _ => Finset.sum_nonneg fun j _ => ?_ - apply mul_nonneg - · unfold positionWeight - exact div_nonneg (params.alpha_pos _).le (by positivity) - · exact sq_nonneg _ - -/-- **Math.** Dirichlet energy is nonnegative. -/ -lemma dirichletEnergy_nonneg - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (f : V → ℝ) : - 0 ≤ H.dirichletEnergy params f := - Finset.sum_nonneg fun e _ => H.perEdgeEnergy_nonneg params e f - -/-- **Math.** Dirichlet energy is invariant under sign flip `f ↦ -f`: -each squared difference is sign-invariant. -/ -lemma dirichletEnergy_neg - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (f : V → ℝ) : - H.dirichletEnergy params (-f) = H.dirichletEnergy params f := by - unfold dirichletEnergy perEdgeEnergy - refine Finset.sum_congr rfl fun e _ => ?_ - refine Finset.sum_congr rfl fun i _ => ?_ - refine Finset.sum_congr rfl fun j _ => ?_ - unfold posDifference - simp only [Pi.neg_apply] - ring - -/-! ## The BDF distance (algebraic definition) - -The canonical definition of `d_H` (iter-010, A3): the effective -resistance of the harmonic Laplacian. The downstream variational -description `inf { E_H(f) | f u − f v = 1 } = 1 / d_H(u, v)` -(`Algebraic.lean`'s `thomson_principle`) is now a *theorem*, not the -definition. -/ - -/-- **Math.** *BDF distance* `d_H(u, v)` (paper 2 §3.2, A3): the -algebraic effective resistance -``` - d_H(u, v) := (e_u − e_v)ᵀ · L_κ⁺ · (e_u − e_v) -``` -of the harmonic Laplacian `L_κ`. Equivalently (Thomson principle, -proved in `Algebraic.lean`) the reciprocal of the variational -infimum `inf { E_H(f) | f u − f v = 1 }`. -/ -noncomputable def resistanceDist [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (u v : V) : ℝ := - ProofAtlas.LinearAlgebra.effectiveResistance - (H.harmonicLaplacian params) u v - -/-- **Math.** Metric axiom: `d_H(u, u) = 0`. Delegated to -`effectiveResistance_self`: the signed indicator `e_u − e_u` -vanishes identically. -/ -lemma resistanceDist_self [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (u : V) : - H.resistanceDist params u u = 0 := - ProofAtlas.LinearAlgebra.effectiveResistance_self _ u - -/-- **Math.** Metric axiom: `d_H(u, v) = d_H(v, u)`. Delegated to -`effectiveResistance_symm`: pure quadratic-form symmetry under -`e ↦ -e`. -/ -lemma resistanceDist_symm [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (u v : V) : - H.resistanceDist params u v = H.resistanceDist params v u := - ProofAtlas.LinearAlgebra.effectiveResistance_symm _ u v - -/-- **Math.** Metric axiom: `0 ≤ d_H(u, v)`. Routes through -`effectiveResistance_nonneg` and the harmonic-Laplacian PSD lemma -(`harmonicLaplacian_posSemidef`, `HarmonicLaplacian.lean`). -/ -lemma resistanceDist_nonneg [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (u v : V) : - 0 ≤ H.resistanceDist params u v := - ProofAtlas.LinearAlgebra.effectiveResistance_nonneg - (H.harmonicLaplacian_posSemidef params) u v - -end Hypergraph diff --git a/lean/ProofAtlas/Metric/Resistance/Geometric.lean b/lean/ProofAtlas/Metric/Resistance/Geometric.lean deleted file mode 100644 index d8802a4..0000000 --- a/lean/ProofAtlas/Metric/Resistance/Geometric.lean +++ /dev/null @@ -1,93 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Util.Linter.MathTag -import ProofAtlas.Metric.Resistance.Canonical -import ProofAtlas.Metric.Resistance.HarmonicLaplacian -import ProofAtlas.Metric.Resistance.Algebraic -import ProofAtlas.LinearAlgebra.SpectralExpansion -import Mathlib.Data.Real.Sqrt - -/-! -# Geometric view of `d_H` (Hilbert embedding) - -The BDF distance is a *squared Euclidean distance* in a natural -Hilbert embedding of `V`. Concretely, with `L_κ^{+/2}` the (unique) -PSD square root of the Moore–Penrose pseudoinverse `L_κ^+`, -``` - φ(u) := L_κ^{+/2} · e_u, d_H(u, v) = ‖φ(u) − φ(v)‖². -``` - -This is the perspective that makes "`d_H` is a metric" geometrically -obvious — it is the pullback of the Euclidean metric on `ℝ^|V|`. - -## Status - -The PSD square root `L_κ^{+/2}` is **not yet defined** in -`LinearAlgebra/MoorePenrose`. The constructions in this file are -placeholders for the eventual development. Once a `pseudoinverseSqrt` -is in place, the Hilbert embedding becomes a one-liner and -`resistanceDist_eq_norm_sq` is a direct corollary of the algebraic -form. --/ - -namespace Hypergraph - -variable {V : Type*} {C : Type*} - -/-- **Math.** *Geometric (Hilbert-embedding) form of `d_H`.* There -exists an embedding `φ : V → V → ℝ` such that the BDF distance is -the squared Euclidean distance in `ℝ^|V|`: -``` - d_H(u, v) = ∑_w (φ(u, w) − φ(v, w))². -``` -Concretely, `φ(u, w) = (L_κ^{+/2} e_u)(w)` where `L_κ^{+/2}` is the -PSD square root of the Moore–Penrose pseudoinverse of `L_κ`. -/ -theorem resistanceDist_eq_norm_sq_of_embedding [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (u v : V) : - ∃ φ : V → V → ℝ, - H.resistanceDist params u v = ∑ w : V, (φ u w - φ v w)^2 := by - -- Spectral construction of the Hilbert embedding. - set L : Matrix V V ℝ := H.harmonicLaplacian params with hL_def - have hL : L.IsHermitian := H.harmonicLaplacian_isHermitian params - have hPSD : L.PosSemidef := H.harmonicLaplacian_posSemidef params - -- φ(u, w) := if λ_w ≠ 0 then (1/√λ_w) · ψ_w(u) else 0, - -- where ψ_w = hL.eigenvectorBasis w is the w-th orthonormal eigenvector. - refine ⟨fun u w => - if hL.eigenvalues w ≠ 0 - then (Real.sqrt (hL.eigenvalues w))⁻¹ * hL.eigenvectorBasis w u - else 0, ?_⟩ - -- Unfold resistanceDist → effectiveResistance and apply spectral expansion. - show ProofAtlas.LinearAlgebra.effectiveResistance L u v = _ - rw [ProofAtlas.LinearAlgebra.effectiveResistance_spectral_expansion hL] - -- Pointwise: each summand on the RHS equals the corresponding LHS summand. - refine Finset.sum_congr rfl (fun w _ => ?_) - by_cases hev : hL.eigenvalues w = 0 - · -- λ_w = 0: both summands are zero. - have hne : ¬ hL.eigenvalues w ≠ 0 := by simp [hev] - simp [hne] - · -- λ_w ≠ 0: expand the square, use √λ_w · √λ_w = λ_w. - have hpos : 0 < hL.eigenvalues w := - lt_of_le_of_ne (hPSD.eigenvalues_nonneg w) (Ne.symm hev) - have hsqrt_pos : 0 < Real.sqrt (hL.eigenvalues w) := Real.sqrt_pos.mpr hpos - have hsqrt_ne : Real.sqrt (hL.eigenvalues w) ≠ 0 := ne_of_gt hsqrt_pos - have hmul : Real.sqrt (hL.eigenvalues w) * Real.sqrt (hL.eigenvalues w) - = hL.eigenvalues w := - Real.mul_self_sqrt hpos.le - -- LHS: ((1/√λ_w) ψ_w(u) − (1/√λ_w) ψ_w(v))² = (ψ_w(u) − ψ_w(v))² / λ_w. - simp only [if_pos hev] - have : (Real.sqrt (hL.eigenvalues w))⁻¹ * hL.eigenvectorBasis w u - - (Real.sqrt (hL.eigenvalues w))⁻¹ * hL.eigenvectorBasis w v - = (Real.sqrt (hL.eigenvalues w))⁻¹ - * (hL.eigenvectorBasis w u - hL.eigenvectorBasis w v) := by ring - rw [this] - have hsqrt_sq : (Real.sqrt (hL.eigenvalues w))^2 = hL.eigenvalues w := - Real.sq_sqrt hpos.le - field_simp - linear_combination - (hL.eigenvectorBasis w u - hL.eigenvectorBasis w v)^2 * hsqrt_sq - -end Hypergraph diff --git a/lean/ProofAtlas/Metric/Resistance/HarmonicLaplacian.lean b/lean/ProofAtlas/Metric/Resistance/HarmonicLaplacian.lean deleted file mode 100644 index b5e2df4..0000000 --- a/lean/ProofAtlas/Metric/Resistance/HarmonicLaplacian.lean +++ /dev/null @@ -1,635 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Util.Linter.MathTag -import ProofAtlas.Hypergraph.Basic -import ProofAtlas.Hypergraph.Decidability -import ProofAtlas.Hypergraph.Walk -import Mathlib.Algebra.BigOperators.Group.Finset.Basic -import Mathlib.Data.Fintype.Basic -import Mathlib.Data.Real.Basic -import Mathlib.Data.Matrix.Basic -import Mathlib.Analysis.Matrix.Spectrum -import Mathlib.LinearAlgebra.Matrix.PosDef - -/-! -# Harmonic conductance and graph Laplacian - -Pure construction of the harmonic-weighted graph Laplacian `L_κ` of -a BDF hypergraph. No metric interpretation lives here; this file is -purely combinatorial-algebraic and is imported by the -Algebraic / Spectral / Geometric viewpoints under -`Metric/Electrical/`. - - H.harmonicIoWeight params e u v — directed weight `α(c)/((i+1)·q)` - H.harmonicKappa params e u v — symmetrisation `io + io^⊤` - H.harmonicConductance params u v — `∑_e harmonicKappa` - H.harmonicLaplacian params — graph Laplacian `D − A` --/ - -namespace Hypergraph - -variable {V : Type*} {C : Type*} - -/-- **Math.** Directed harmonic weight: edge `e` whose `i`-th input -is `u` and whose output set contains `v` contributes -`α(c(e)) / ((i+1) · q_e)`; all other pairs contribute `0`. -/ -noncomputable def harmonicIoWeight [DecidableEq V] - (H : Hypergraph V C) (params : WalkParams C) - (e : H.edges) (u v : V) : ℝ := - if hu : ∃ i : Fin (H.inArity e), H.inVertex e i = u then - if H.isOutput v e then - params.alpha (H.color e) / - (((hu.choose.val + 1) * H.outArity e : ℕ) : ℝ) - else 0 - else 0 - -/-- **Math.** The directed harmonic weight is non-negative: a ratio -of the positive `α(c(e))` over a non-negative natural cast. -/ -lemma harmonicIoWeight_nonneg [DecidableEq V] - (H : Hypergraph V C) (params : WalkParams C) - (e : H.edges) (u v : V) : - 0 ≤ H.harmonicIoWeight params e u v := by - unfold harmonicIoWeight - by_cases hu : ∃ i : Fin (H.inArity e), H.inVertex e i = u - · rw [dif_pos hu] - by_cases hv : H.isOutput v e - · rw [if_pos hv] - exact div_nonneg (params.alpha_pos _).le (Nat.cast_nonneg _) - · rw [if_neg hv] - · rw [dif_neg hu] - -/-- **Math.** Symmetric per-edge harmonic conductance: -`harmonicKappa e u v := harmonicIoWeight e u v + harmonicIoWeight e v u`. -/ -noncomputable def harmonicKappa [DecidableEq V] - (H : Hypergraph V C) (params : WalkParams C) - (e : H.edges) (u v : V) : ℝ := - H.harmonicIoWeight params e u v + H.harmonicIoWeight params e v u - -/-- **Math.** Symmetry of the per-edge harmonic conductance. -/ -lemma harmonicKappa_symm [DecidableEq V] - (H : Hypergraph V C) (params : WalkParams C) - (e : H.edges) (u v : V) : - H.harmonicKappa params e u v = H.harmonicKappa params e v u := by - unfold harmonicKappa; exact add_comm _ _ - -/-- **Math.** Non-negativity of the per-edge harmonic conductance: -sum of two non-negative `harmonicIoWeight`s. -/ -lemma harmonicKappa_nonneg [DecidableEq V] - (H : Hypergraph V C) (params : WalkParams C) - (e : H.edges) (u v : V) : - 0 ≤ H.harmonicKappa params e u v := - add_nonneg (H.harmonicIoWeight_nonneg params e u v) - (H.harmonicIoWeight_nonneg params e v u) - -/-- **Math.** The directed harmonic weight vanishes on the diagonal: a vertex -cannot be both an input and an output of the same edge (BDF axiom -`inputOutputDisjoint`), so `harmonicIoWeight e u u = 0`. -/ -lemma harmonicIoWeight_self_eq_zero [DecidableEq V] - (H : Hypergraph V C) (params : WalkParams C) - (e : H.edges) (u : V) : - H.harmonicIoWeight params e u u = 0 := by - unfold harmonicIoWeight - by_cases hu : ∃ i : Fin (H.inArity e), H.inVertex e i = u - · rw [dif_pos hu] - have hout : ¬ H.isOutput u e := by - rintro ⟨j, hj⟩ - obtain ⟨i, hi⟩ := hu - exact H.inputOutputDisjoint e i j (hi.trans hj.symm) - rw [if_neg hout] - · rw [dif_neg hu] - -/-- **Math.** Closed form of `harmonicIoWeight` at an explicit input -position: when `u = H.inVertex e i`, the weight equals -`α(c(e)) / ((i+1)·q_e)` whenever `v` is an output and `0` otherwise. -Uses `inVertex_injective` to identify `hu.choose` with `i`. -/ -lemma harmonicIoWeight_inVertex [DecidableEq V] - (H : Hypergraph V C) (params : WalkParams C) - (e : H.edges) (i : Fin (H.inArity e)) (v : V) : - H.harmonicIoWeight params e (H.inVertex e i) v - = if H.isOutput v e then - params.alpha (H.color e) / - (((i.val + 1) * H.outArity e : ℕ) : ℝ) - else 0 := by - unfold harmonicIoWeight - have hu : ∃ k : Fin (H.inArity e), H.inVertex e k = H.inVertex e i := ⟨i, rfl⟩ - have hk : hu.choose = i := H.inVertex_injective e hu.choose_spec - rw [dif_pos hu, hk] - -/-- **Math.** Change of variables for sums weighted by `harmonicIoWeight`: -the double sum over `(u, v) : V × V` collapses to a double sum over -position indices `(i, j) : Fin (inArity e) × Fin (outArity e)` via -the input/output injectivity. The off-support terms vanish by -definition of `harmonicIoWeight`. -/ -lemma harmonicIoWeight_sum_change_of_vars [Fintype V] [DecidableEq V] - (H : Hypergraph V C) (params : WalkParams C) - (e : H.edges) (g : V → V → ℝ) : - ∑ u : V, ∑ v : V, H.harmonicIoWeight params e u v * g u v - = ∑ i : Fin (H.inArity e), ∑ j : Fin (H.outArity e), - (params.alpha (H.color e) / - (((i.val + 1) * H.outArity e : ℕ) : ℝ)) - * g (H.inVertex e i) (H.outVertex e j) := by - -- Step 1: reindex the u-axis along `inVertex e`. - have step_u : - ∑ u : V, ∑ v : V, H.harmonicIoWeight params e u v * g u v - = ∑ i : Fin (H.inArity e), - ∑ v : V, H.harmonicIoWeight params e (H.inVertex e i) v - * g (H.inVertex e i) v := by - have h_subset : - ∑ u : V, ∑ v : V, H.harmonicIoWeight params e u v * g u v - = ∑ u ∈ (Finset.univ : Finset (Fin (H.inArity e))).image (H.inVertex e), - ∑ v : V, H.harmonicIoWeight params e u v * g u v := by - refine (Finset.sum_subset (Finset.subset_univ _) ?_).symm - intro u _ hu_notin - refine Finset.sum_eq_zero fun v _ => ?_ - have hw : H.harmonicIoWeight params e u v = 0 := by - unfold harmonicIoWeight - apply dif_neg - rintro ⟨i, hi⟩ - exact hu_notin (Finset.mem_image.mpr ⟨i, Finset.mem_univ _, hi⟩) - rw [hw, zero_mul] - rw [h_subset, - Finset.sum_image (fun x _ y _ h => H.inVertex_injective e h)] - rw [step_u] - refine Finset.sum_congr rfl fun i _ => ?_ - -- Step 2: reindex the v-axis along `outVertex e`. - have step_v : - ∑ v : V, H.harmonicIoWeight params e (H.inVertex e i) v - * g (H.inVertex e i) v - = ∑ j : Fin (H.outArity e), - H.harmonicIoWeight params e (H.inVertex e i) (H.outVertex e j) - * g (H.inVertex e i) (H.outVertex e j) := by - have h_subset : - ∑ v : V, H.harmonicIoWeight params e (H.inVertex e i) v - * g (H.inVertex e i) v - = ∑ v ∈ (Finset.univ : Finset (Fin (H.outArity e))).image (H.outVertex e), - H.harmonicIoWeight params e (H.inVertex e i) v - * g (H.inVertex e i) v := by - refine (Finset.sum_subset (Finset.subset_univ _) ?_).symm - intro v _ hv_notin - have hw : H.harmonicIoWeight params e (H.inVertex e i) v = 0 := by - rw [H.harmonicIoWeight_inVertex params e i v] - apply if_neg - rintro ⟨j, hj⟩ - exact hv_notin (Finset.mem_image.mpr ⟨j, Finset.mem_univ _, hj⟩) - rw [hw, zero_mul] - rw [h_subset, - Finset.sum_image (fun x _ y _ h => H.outVertex_injective e h)] - rw [step_v] - refine Finset.sum_congr rfl fun j _ => ?_ - -- Step 3: evaluate the weight using `harmonicIoWeight_inVertex`. - rw [H.harmonicIoWeight_inVertex params e i (H.outVertex e j), - if_pos ⟨j, rfl⟩] - -/-- **Math.** Marginalised harmonic conductance `∑_e harmonicKappa`. -/ -noncomputable def harmonicConductance [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (u v : V) : ℝ := - ∑ e : H.edges, H.harmonicKappa params e u v - -/-- **Math.** Symmetry of the marginalised harmonic conductance. -/ -lemma harmonicConductance_symm [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (u v : V) : - H.harmonicConductance params u v - = H.harmonicConductance params v u := by - unfold harmonicConductance - exact Finset.sum_congr rfl fun e _ => H.harmonicKappa_symm params e u v - -/-- **Math.** Non-negativity of the marginalised harmonic conductance: -sum over edges of the non-negative per-edge `harmonicKappa`. -/ -lemma harmonicConductance_nonneg [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (u v : V) : - 0 ≤ H.harmonicConductance params u v := - Finset.sum_nonneg fun e _ => H.harmonicKappa_nonneg params e u v - -/-- **Math.** The marginalised harmonic conductance vanishes on the diagonal: -since each per-edge contribution `harmonicKappa e u u = 2 · harmonicIoWeight e u u` -is zero by `harmonicIoWeight_self_eq_zero`, the sum over edges is zero. -/ -lemma harmonicConductance_self_eq_zero [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (u : V) : - H.harmonicConductance params u u = 0 := by - unfold harmonicConductance - refine Finset.sum_eq_zero (fun e _ => ?_) - unfold harmonicKappa - rw [H.harmonicIoWeight_self_eq_zero params e u, add_zero] - -/-- **Math.** Harmonic graph Laplacian `L_κ = D − A`, where `A_{u,v} -= H.harmonicConductance u v` is the conductance matrix and -`D_{u,u}` is its row-sum diagonal. -/ -noncomputable def harmonicLaplacian [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) : Matrix V V ℝ := - fun u v => - if u = v then ∑ w : V, H.harmonicConductance params u w - else -H.harmonicConductance params u v - -/-- **Math.** *Apply-as-conductance-sum formula.* For any function -`g : V → ℝ`, -``` - (L_κ · g)(u) = ∑_v c_κ(u, v) · (g(u) − g(v)) -``` -where `c_κ := H.harmonicConductance params`. Derives from the -`D − A` shape of `L_κ` together with `c_κ(u, u) = 0`. -/ -lemma harmonicLaplacian_apply_eq_sum_diff [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (g : V → ℝ) (u : V) : - (H.harmonicLaplacian params).mulVec g u - = ∑ v : V, H.harmonicConductance params u v * (g u - g v) := by - change ∑ v, H.harmonicLaplacian params u v * g v - = ∑ v, H.harmonicConductance params u v * (g u - g v) - have h_cuu : H.harmonicConductance params u u = 0 := - H.harmonicConductance_self_eq_zero params u - -- Rewrite both sides into the canonical form - -- (∑ v, c u v) * g u − ∑ v, c u v * g v - have hRHS : (∑ v, H.harmonicConductance params u v * (g u - g v)) - = (∑ v, H.harmonicConductance params u v) * g u - - ∑ v, H.harmonicConductance params u v * g v := by - rw [Finset.sum_mul] - rw [← Finset.sum_sub_distrib] - refine Finset.sum_congr rfl (fun v _ => ?_) - ring - have hLHS : (∑ v, H.harmonicLaplacian params u v * g v) - = (∑ v, H.harmonicConductance params u v) * g u - - ∑ v, H.harmonicConductance params u v * g v := by - -- ∑_v L u v g v = L_{uu} g(u) + ∑_{v≠u} L_{uv} g(v) - -- = (∑_w c u w) g(u) − ∑_{v≠u} c u v g(v) - -- = (∑_w c u w) g(u) − ∑_v c u v g(v) [since c u u = 0] - rw [← Finset.sum_erase_add _ _ (Finset.mem_univ u)] - have hdiag : H.harmonicLaplacian params u u = ∑ w, H.harmonicConductance params u w := by - simp [harmonicLaplacian] - rw [hdiag] - have hoff : ∀ v ∈ Finset.univ.erase u, - H.harmonicLaplacian params u v * g v = -H.harmonicConductance params u v * g v := by - intro v hv - have hvu : v ≠ u := (Finset.mem_erase.mp hv).1 - have huv : ¬ u = v := fun h => hvu h.symm - simp [harmonicLaplacian, huv] - rw [Finset.sum_congr rfl hoff] - -- (∑_{v≠u} -c u v g v) + (∑_w c u w) g u - -- = (∑_w c u w) g u - ∑_{v≠u} c u v g v - -- = (∑_w c u w) g u - ∑_v c u v g v [since c u u g u term is 0] - have hsplit : ∑ v, H.harmonicConductance params u v * g v - = H.harmonicConductance params u u * g u - + ∑ v ∈ Finset.univ.erase u, H.harmonicConductance params u v * g v := by - rw [← Finset.sum_erase_add _ _ (Finset.mem_univ u)]; ring - rw [hsplit, h_cuu, zero_mul, zero_add] - -- goal: ∑_{v≠u} -c u v g v + (∑_w c u w) g u - -- = (∑_v c u v) g u - ∑_{v≠u} c u v g v - simp only [neg_mul, Finset.sum_neg_distrib] - ring - rw [hLHS, hRHS] - -/-- **Math.** *Connectedness of the harmonic graph.* The harmonic -graph (vertex set `V`, edges = pairs with positive harmonic -conductance) is connected: any two vertices are linked by a path -along positive-conductance edges. -/ -def harmonicConnected [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) : Prop := - ∀ u v : V, Relation.ReflTransGen - (fun x y => 0 < H.harmonicConductance params x y) u v - -/-- **Math.** Entry-wise symmetry of the harmonic Laplacian. -/ -lemma harmonicLaplacian_symm [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (u v : V) : - H.harmonicLaplacian params u v = H.harmonicLaplacian params v u := by - unfold harmonicLaplacian - by_cases huv : u = v - · subst huv; rfl - · have hvu : ¬ v = u := fun h => huv h.symm - rw [if_neg huv, if_neg hvu, H.harmonicConductance_symm params v u] - -/-- **Math.** The harmonic Laplacian is Hermitian (= symmetric over ℝ). -/ -lemma harmonicLaplacian_isHermitian [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) : - Matrix.IsHermitian (H.harmonicLaplacian params) := by - ext u v - change star (H.harmonicLaplacian params v u) = H.harmonicLaplacian params u v - rw [show star (H.harmonicLaplacian params v u) - = H.harmonicLaplacian params v u from rfl, - H.harmonicLaplacian_symm params v u] - -end Hypergraph - -/-! ## Hyperedge-level harmonic weight (automorphism-equivariance bridge) - -For an automorphism `φ : Aut H`, the compatibility axiom only equates -`H.edge (φ.edgeEquiv e)` with `(H.edge e).equivMap φ.vertexEquiv` -*propositionally*; dependent-type mismatches make direct rewriting on -`Hypergraph` projections fragile. The `Hyperedge`-level helper -below sidesteps that by formulating the harmonic weight purely in -terms of the abstract `Hyperedge` data, and lifting equivariance -through one `rfl` bridge plus `Hyperedge.equivMap`. -/ - -namespace Hyperedge - -variable {V : Type*} {C : Type*} - -/-- **Eng.** Harmonic directed weight at the `Hyperedge` level. -Matches `Hypergraph.harmonicIoWeight` definitionally on `H.edge e` -(after `unfold isOutput`). -/ -private noncomputable def harmonicIoWeightAt [DecidableEq V] - (he : Hyperedge V C) (params : WalkParams C) (u v : V) : ℝ := - if hu : ∃ i : Fin he.inArity, he.inVertex i = u then - if ∃ j : Fin he.outArity, he.outVertex j = v then - params.alpha he.color / - (((hu.choose.val + 1) * he.outArity : ℕ) : ℝ) - else 0 - else 0 - -/-- **Eng.** Equivariance of `harmonicIoWeightAt` under pushing the -hyperedge data through a vertex bijection `π`. -/ -private lemma harmonicIoWeightAt_equivMap [DecidableEq V] - (he : Hyperedge V C) (π : V ≃ V) (params : WalkParams C) - (u v : V) : - (he.equivMap π).harmonicIoWeightAt params (π u) (π v) - = he.harmonicIoWeightAt params u v := by - unfold harmonicIoWeightAt - have hu_iff : - (∃ i : Fin (he.equivMap π).inArity, (he.equivMap π).inVertex i = π u) - ↔ (∃ i : Fin he.inArity, he.inVertex i = u) := by - refine ⟨?_, ?_⟩ - · rintro ⟨i, hi⟩ - exact ⟨i, π.injective hi⟩ - · rintro ⟨i, hi⟩ - refine ⟨i, ?_⟩ - change π (he.inVertex i) = π u - rw [hi] - have hout_iff : - (∃ j : Fin (he.equivMap π).outArity, (he.equivMap π).outVertex j = π v) - ↔ (∃ j : Fin he.outArity, he.outVertex j = v) := by - refine ⟨?_, ?_⟩ - · rintro ⟨j, hj⟩ - exact ⟨j, π.injective hj⟩ - · rintro ⟨j, hj⟩ - refine ⟨j, ?_⟩ - change π (he.outVertex j) = π v - rw [hj] - by_cases hu : ∃ i : Fin he.inArity, he.inVertex i = u - · have hu' : ∃ i : Fin (he.equivMap π).inArity, - (he.equivMap π).inVertex i = π u := hu_iff.mpr hu - rw [dif_pos hu', dif_pos hu] - by_cases hv : ∃ j : Fin he.outArity, he.outVertex j = v - · have hv' : ∃ j : Fin (he.equivMap π).outArity, - (he.equivMap π).outVertex j = π v := hout_iff.mpr hv - rw [if_pos hv', if_pos hv] - have hidx : hu'.choose = hu.choose := by - apply he.inVertex_injective - have h1 : π (he.inVertex hu'.choose) = π u := hu'.choose_spec - have h2 : he.inVertex hu'.choose = u := π.injective h1 - rw [h2, hu.choose_spec] - change params.alpha he.color / - (((hu'.choose.val + 1) * he.outArity : ℕ) : ℝ) - = params.alpha he.color / - (((hu.choose.val + 1) * he.outArity : ℕ) : ℝ) - rw [hidx] - · have hv' : ¬ ∃ j : Fin (he.equivMap π).outArity, - (he.equivMap π).outVertex j = π v := mt hout_iff.mp hv - rw [if_neg hv', if_neg hv] - · have hu' : ¬ ∃ i : Fin (he.equivMap π).inArity, - (he.equivMap π).inVertex i = π u := mt hu_iff.mp hu - rw [dif_neg hu', dif_neg hu] - -end Hyperedge - -namespace Hypergraph - -variable {V : Type*} {C : Type*} - -/-! ## Automorphism equivariance of the harmonic Laplacian - -For an automorphism `φ : Aut H`, the harmonic conductance schedule and -the resulting graph Laplacian are equivariant under the vertex -permutation `π := φ.vertexEquiv`. The four lemmas below assemble layer -by layer: directed weight → symmetric kappa → marginalised conductance -→ graph Laplacian. Support `Structural.resistanceDist_aut_invariant`. -/ - -/-- **Eng.** Definitional bridge between `H.harmonicIoWeight` and the -`Hyperedge`-level `harmonicIoWeightAt` evaluated on `H.edge e`. The -only non-`rfl` step is unfolding `isOutput`. -/ -private lemma harmonicIoWeight_eq_at [DecidableEq V] - (H : Hypergraph V C) (params : WalkParams C) - (e : H.edges) (u v : V) : - H.harmonicIoWeight params e u v - = (H.edge e).harmonicIoWeightAt params u v := by - unfold harmonicIoWeight Hyperedge.harmonicIoWeightAt isOutput Hyperedge.hasOutput - rfl - -/-- **Math.** *Automorphism equivariance, layer 1*: the directed -harmonic weight is invariant under applying `φ` (permuting both -vertices via `φ.vertexEquiv` and the edge via `φ.edgeEquiv`). -/ -lemma harmonicIoWeight_aut [DecidableEq V] - (H : Hypergraph V C) (params : WalkParams C) - (φ : Aut H) (e : H.edges) (u v : V) : - H.harmonicIoWeight params (φ.edgeEquiv e) - (φ.vertexEquiv u) (φ.vertexEquiv v) - = H.harmonicIoWeight params e u v := by - rw [H.harmonicIoWeight_eq_at params (φ.edgeEquiv e), - H.harmonicIoWeight_eq_at params e, - φ.compat e] - exact Hyperedge.harmonicIoWeightAt_equivMap - (H.edge e) φ.vertexEquiv params u v - -/-- **Math.** *Automorphism equivariance, layer 2*: the symmetric -per-edge kappa is invariant under `φ`. -/ -lemma harmonicKappa_aut [DecidableEq V] - (H : Hypergraph V C) (params : WalkParams C) - (φ : Aut H) (e : H.edges) (u v : V) : - H.harmonicKappa params (φ.edgeEquiv e) - (φ.vertexEquiv u) (φ.vertexEquiv v) - = H.harmonicKappa params e u v := by - unfold harmonicKappa - rw [H.harmonicIoWeight_aut params φ e u v, - H.harmonicIoWeight_aut params φ e v u] - -/-- **Math.** *Automorphism equivariance, layer 3*: the marginalised -conductance is invariant under `φ.vertexEquiv`. Reindex the edge sum -by `φ.edgeEquiv` and apply `harmonicKappa_aut`. -/ -lemma harmonicConductance_aut [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (φ : Aut H) (u v : V) : - H.harmonicConductance params (φ.vertexEquiv u) (φ.vertexEquiv v) - = H.harmonicConductance params u v := by - unfold harmonicConductance - rw [← Equiv.sum_comp φ.edgeEquiv - (fun e => H.harmonicKappa params e - (φ.vertexEquiv u) (φ.vertexEquiv v))] - exact Finset.sum_congr rfl fun e _ => H.harmonicKappa_aut params φ e u v - -/-- **Math.** *Automorphism equivariance, layer 4*: the harmonic -graph Laplacian is invariant entry-wise under `φ.vertexEquiv`. -Off-diagonal: direct from layer 3. Diagonal: reindex the inner sum -via `φ.vertexEquiv` and apply layer 3. -/ -lemma harmonicLaplacian_aut [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (φ : Aut H) (u v : V) : - H.harmonicLaplacian params (φ.vertexEquiv u) (φ.vertexEquiv v) - = H.harmonicLaplacian params u v := by - unfold harmonicLaplacian - by_cases huv : u = v - · subst huv - rw [if_pos rfl, if_pos rfl] - rw [← Equiv.sum_comp φ.vertexEquiv - (fun w => H.harmonicConductance params (φ.vertexEquiv u) w)] - exact Finset.sum_congr rfl fun w _ => - H.harmonicConductance_aut params φ u w - · have huv' : φ.vertexEquiv u ≠ φ.vertexEquiv v := - fun h => huv (φ.vertexEquiv.injective h) - rw [if_neg huv', if_neg huv] - exact congrArg Neg.neg (H.harmonicConductance_aut params φ u v) - -/-! ## Standard Laplacian–Dirichlet quadratic identity - -The quadratic form of any graph Laplacian `L = D − A` built from a -symmetric conductance `c` admits the closed form -`f^⊤ L f = (1/2) ∑_{u,v} c(u, v) · (f(u) − f(v))²`. From this and -non-negativity of `c` one immediately gets `L` is positive -semidefinite. Both facts are pure matrix-algebra consequences of the -Laplacian construction and live here (not downstream) so that -`Canonical.lean` can build the algebraic effective resistance on top. -/ - -section -variable [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) - -local notation "L_κ" => H.harmonicLaplacian params -local notation "c_κ" => H.harmonicConductance params - -/-- **Math.** *Standard Laplacian–Dirichlet identity* (paper 2 §3.3, -first equality): for any graph Laplacian `L = D − A` built from a -symmetric conductance `c`, the quadratic form `f^⊤ L f` equals -`(1/2) · ∑_{u,v} c(u,v) · (f(u) − f(v))²`. -/ -theorem harmonicLaplacian_quadForm (f : V → ℝ) : - dotProduct (Matrix.mulVec L_κ f) f - = (1/2 : ℝ) * ∑ u : V, ∑ v : V, c_κ u v * (f u - f v)^2 := by - have hc_self : ∀ u : V, c_κ u u = 0 := - H.harmonicConductance_self_eq_zero params - have hc_symm : ∀ u v : V, c_κ u v = c_κ v u := - H.harmonicConductance_symm params - -- Unfold the matrix Laplacian entry-by-entry. - have L_apply : ∀ u v : V, - L_κ u v = if u = v then (∑ w : V, c_κ u w) else -(c_κ u v) := by - intro u v; rfl - -- ## Step A. Rewrite the LHS as `D − X` with - -- D := ∑ u, ∑ v, c_κ u v · f u² - -- X := ∑ u, ∑ v, c_κ u v · f u · f v. - -- The trick: for each row `u`, expand `L_κ u v` into its `ite`, multiply - -- by `f v * f u`, and then use `c_κ u u = 0` to extend the off-diagonal - -- sum back to a full sum. - have lhs_eq : - dotProduct (Matrix.mulVec L_κ f) f - = (∑ u : V, ∑ v : V, c_κ u v * (f u)^2) - - ∑ u : V, ∑ v : V, c_κ u v * f u * f v := by - -- LHS = ∑ u, ∑ v, L_κ u v * f v * f u - change ∑ u : V, (∑ v : V, L_κ u v * f v) * f u = _ - -- Convert into a single nested sum and split each row. - rw [show - (∑ u : V, (∑ v : V, L_κ u v * f v) * f u) - = ∑ u : V, ∑ v : V, L_κ u v * f v * f u from - Finset.sum_congr rfl fun u _ => Finset.sum_mul _ _ _] - -- Replace each term using the `ite` form of `L_κ`. - have term_split : ∀ u v : V, - L_κ u v * f v * f u - = (if u = v then (∑ w : V, c_κ u w) * f u * f u else (0 : ℝ)) - - (if u = v then (0 : ℝ) else c_κ u v * f u * f v) := by - intro u v - rw [L_apply] - by_cases huv : u = v - · subst huv - simp - · rw [if_neg huv, if_neg huv, if_neg huv] - ring - simp_rw [term_split] - simp only [Finset.sum_sub_distrib] - -- Diagonal sum: ∑ u, ∑ v, (if u = v then (∑ w, c_κ u w) * f u * f u else 0) - -- = ∑ u, ∑ v, c_κ u v * f u^2. - have diag : - ∑ u : V, ∑ v : V, - (if u = v then (∑ w : V, c_κ u w) * f u * f u else (0 : ℝ)) - = ∑ u : V, ∑ v : V, c_κ u v * (f u)^2 := by - refine Finset.sum_congr rfl (fun u _ => ?_) - rw [Finset.sum_ite_eq Finset.univ u - (fun _ => (∑ w : V, c_κ u w) * f u * f u), - if_pos (Finset.mem_univ u), Finset.sum_mul, Finset.sum_mul] - refine Finset.sum_congr rfl (fun v _ => ?_) - ring - -- Off-diagonal sum: ∑ u, ∑ v, (if u = v then 0 else c_κ u v * f u * f v) - -- = ∑ u, ∑ v, c_κ u v * f u * f v - -- because the `v = u` contribution `c_κ u u * f u * f u = 0`. - have offdiag : - ∑ u : V, ∑ v : V, - (if u = v then (0 : ℝ) else c_κ u v * f u * f v) - = ∑ u : V, ∑ v : V, c_κ u v * f u * f v := by - refine Finset.sum_congr rfl (fun u _ => ?_) - refine Finset.sum_congr rfl (fun v _ => ?_) - by_cases huv : u = v - · subst huv; rw [if_pos rfl, hc_self u]; ring - · rw [if_neg huv] - rw [diag, offdiag] - -- ## Step B. Rewrite the RHS as `D − X`, using `c_κ` symmetry. - have rhs_eq : - (1/2 : ℝ) * ∑ u : V, ∑ v : V, c_κ u v * (f u - f v)^2 - = (∑ u : V, ∑ v : V, c_κ u v * (f u)^2) - - ∑ u : V, ∑ v : V, c_κ u v * f u * f v := by - -- Expand the square pointwise. - have expand₁ : - ∑ u : V, ∑ v : V, c_κ u v * (f u - f v)^2 - = ∑ u : V, ∑ v : V, - (c_κ u v * (f u)^2 - 2 * (c_κ u v * f u * f v) + c_κ u v * (f v)^2) := by - refine Finset.sum_congr rfl (fun u _ => - Finset.sum_congr rfl (fun v _ => ?_)) - ring - rw [expand₁] - -- Distribute the inner and outer sums. - have split : - ∑ u : V, ∑ v : V, - (c_κ u v * (f u)^2 - 2 * (c_κ u v * f u * f v) + c_κ u v * (f v)^2) - = (∑ u : V, ∑ v : V, c_κ u v * (f u)^2) - - 2 * (∑ u : V, ∑ v : V, c_κ u v * f u * f v) - + ∑ u : V, ∑ v : V, c_κ u v * (f v)^2 := by - simp only [Finset.sum_add_distrib, Finset.sum_sub_distrib, Finset.mul_sum] - rw [split] - -- Use `c_κ` symmetry to identify the `(f v)²` sum with the `(f u)²` sum. - have swap_square : - ∑ u : V, ∑ v : V, c_κ u v * (f v)^2 - = ∑ u : V, ∑ v : V, c_κ u v * (f u)^2 := by - rw [Finset.sum_comm] - refine Finset.sum_congr rfl (fun u _ => - Finset.sum_congr rfl (fun v _ => ?_)) - rw [hc_symm v u] - rw [swap_square] - ring - rw [lhs_eq, rhs_eq] - -/-- **Math.** The harmonic Laplacian is positive semidefinite. Routes -through `Matrix.PosSemidef.of_dotProduct_mulVec_nonneg`: Hermitian -(via `harmonicLaplacian_isHermitian`) plus the closed-form quadratic -identity above, whose RHS `(1/2) · ∑_{u,v} c · (f u − f v)²` is -manifestly non-negative since `c_κ ≥ 0`. -/ -theorem harmonicLaplacian_posSemidef : - Matrix.PosSemidef (H.harmonicLaplacian params) := by - refine Matrix.PosSemidef.of_dotProduct_mulVec_nonneg - (H.harmonicLaplacian_isHermitian params) (fun x => ?_) - -- Goal: 0 ≤ star x ⬝ᵥ (L_κ *ᵥ x). Over ℝ, star = id; swap into quadForm shape. - have h_swap : - (star x : V → ℝ) ⬝ᵥ (Matrix.mulVec (H.harmonicLaplacian params) x) - = dotProduct (Matrix.mulVec (H.harmonicLaplacian params) x) x := by - unfold dotProduct - refine Finset.sum_congr rfl fun i _ => ?_ - rw [star_trivial]; ring - rw [h_swap, H.harmonicLaplacian_quadForm params x] - refine mul_nonneg (by norm_num) ?_ - refine Finset.sum_nonneg fun u _ => Finset.sum_nonneg fun v _ => ?_ - exact mul_nonneg (H.harmonicConductance_nonneg params u v) (sq_nonneg _) - -end - -end Hypergraph diff --git a/lean/ProofAtlas/Metric/Resistance/Instance.lean b/lean/ProofAtlas/Metric/Resistance/Instance.lean deleted file mode 100644 index e3bc1ab..0000000 --- a/lean/ProofAtlas/Metric/Resistance/Instance.lean +++ /dev/null @@ -1,92 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Metric.Axioms -import ProofAtlas.Metric.Resistance.Canonical -import ProofAtlas.Metric.Resistance.Algebraic - -/-! -# `resistanceDist` satisfies `IsHypergraphMetric` - -Verification of the 7-check quality certificate for the BDF -resistance distance. All 7 fields close in this file; preconditions -that depend on heavier machinery (`hConn`/`hRange` for triangle, -abstract sensitivity-witness hypotheses for the three faithfulness -checks) are exposed as explicit hypotheses on the instance theorem. - -The faithfulness hypotheses (`hWit_direction`, `hWit_ordering`, -`hWit_coloring`) are abstract existence statements over `resistanceDist` -itself. Their truth follows from the linear / multilinear structure -of the harmonic Laplacian (conductance is α-linear, `L_κ` and `L_κ⁺` -scale, so the quadratic form `d_H` varies non-trivially under any -change to direction / input ordering / α). Discharging them by a -concrete or abstract scaling argument is a follow-up. --/ - -namespace Hypergraph - --- doc:start resistanceDist_isHypergraphMetric_signature -/-- **Math.** The BDF resistance distance is a *good BDF metric*. -/ -theorem resistanceDist_isHypergraphMetric {V C : Type*} [Fintype V] [DecidableEq V] - [hFin : ∀ H : Hypergraph V C, Fintype H.edges] - (hConn : ∀ (H : Hypergraph V C) (params : WalkParams C), - H.harmonicConnected params) - (hRange : ∀ (H : Hypergraph V C) (params : WalkParams C) - (a b : V), a ≠ b → - ∃ y : V → ℝ, (H.harmonicLaplacian params).mulVec y - = ProofAtlas.LinearAlgebra.signedIndicator a b) - (hWit_direction : - ∃ (H H' : Hypergraph V C) (params : WalkParams C) (u v : V), - H.resistanceDist params u v ≠ H'.resistanceDist params u v) - (hWit_ordering : - ∃ (H H' : Hypergraph V C) (params : WalkParams C) (u v : V), - H.resistanceDist params u v ≠ H'.resistanceDist params u v) - (hWit_coloring : - ∃ (H : Hypergraph V C) (params params' : WalkParams C) (u v : V), - H.resistanceDist params u v ≠ H.resistanceDist params' u v) : - IsHypergraphMetric (fun (H : Hypergraph V C) params u v => - H.resistanceDist params u v) := by --- doc:end - refine - { d_self := ?_ - d_symm := ?_ - d_triangle := ?_ - d_nonneg := ?_ - sensitive_to_direction := ?_ - sensitive_to_ordering := ?_ - sensitive_to_coloring := ?_ } - · -- d_self - intro H _ params u - convert H.resistanceDist_self params u - · -- d_symm - intro H _ params u v - convert H.resistanceDist_symm params u v - · -- d_triangle: case split on `v = w`, else use the hypotheses. - intro H _ params u v w - letI : Fintype H.edges := hFin H - by_cases h : v = w - · subst h - have h_self := H.resistanceDist_self params v - have : H.resistanceDist params u v - ≤ H.resistanceDist params u v + H.resistanceDist params v v := by - rw [h_self]; linarith - convert this - · have := H.resistanceDist_triangle params u v w h - (hConn H params) (hRange H params v w h) - convert this - · -- d_nonneg - intro H _ params u v - convert H.resistanceDist_nonneg params u v - · -- sensitive_to_direction - obtain ⟨H, H', params, u, v, hne⟩ := hWit_direction - exact ⟨H, H', hFin H, hFin H', params, u, v, hne⟩ - · -- sensitive_to_ordering - obtain ⟨H, H', params, u, v, hne⟩ := hWit_ordering - exact ⟨H, H', hFin H, hFin H', params, u, v, hne⟩ - · -- sensitive_to_coloring - obtain ⟨H, params, params', u, v, hne⟩ := hWit_coloring - exact ⟨H, hFin H, params, params', u, v, hne⟩ - -end Hypergraph diff --git a/lean/ProofAtlas/Metric/Resistance/Resistance.lean b/lean/ProofAtlas/Metric/Resistance/Resistance.lean deleted file mode 100644 index 345978d..0000000 --- a/lean/ProofAtlas/Metric/Resistance/Resistance.lean +++ /dev/null @@ -1,130 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Util.Linter.MathTag -import ProofAtlas.Metric.Axioms -import ProofAtlas.LinearAlgebra.EffectiveResistance -import ProofAtlas.LinearAlgebra.MoorePenrose - -/-! -# BDF metric: effective-resistance distance derived from `HypergraphMetric` - -The `HypergraphMetric` structure encodes the five conductance axioms (D, O, -C, A, S — bipartite, monotone, colorSep, outputUnif, symmetry) but -does not carry a distance function `d` directly. The v2 formulation -derives `d` from the weighted Laplacian as the effective resistance: - - `HypergraphMetric.d m u v := effectiveResistance m.laplacian u v` - -## Relation to `Hypergraph.commuteTimeDist` (two distinct metrics) - -The original v2 ambition was to identify `HypergraphMetric.d` with the -random-walk commute time `Hypergraph.commuteTimeDist`. The -investigation in `informal/bdf_doyle_snell_bridge.md` (Round 3b-prep) -shows that this identification is **mathematically false** for -generic BDF hypergraphs: - - * `commuteTimeDist u v := h(u, v) + h(v, u)` records the dynamical - commute time of the BDF random walk. - * `HypergraphMetric.d u v` records the effective resistance of the - symmetric harmonic conductance graph (`commuteTimeKappa`). - -These are two distinct metrics on the same hypergraph. They agree -only on a restricted directed-graph sub-class (`p_e = 1, β = 1`, -out-injective, degree-weighted π), and even there only modulo a -total-conductance scalar. See the informal document for details. - -The paper's main result (`probabilistic_hyperbolicity_bound`, Theorem -7.1) uses `commuteTimeDist`. `HypergraphMetric.d` is a parallel R_eff-based -metric arising naturally from the v2 axiomatic framework; it does -not unify with the paper's commute-time metric. - -## What is proved here - -* `HypergraphMetric.conductance_symm` — symmetry of `m.conductance`. -* `HypergraphMetric.laplacian_symm` — symmetry of `m.laplacian` (entrywise). -* `HypergraphMetric.laplacian_isHermitian` — `m.laplacian.IsHermitian`. -* `HypergraphMetric.d_self` — `d(u, u) = 0` (one of four metric axioms). -* `HypergraphMetric.d_symm` — `d(u, v) = d(v, u)` (one of four metric axioms). - -## Future work - -The remaining two metric axioms for `HypergraphMetric.d` are classical -graph-Laplacian theory: - -* `d(u, v) ≥ 0` (nonnegativity): follows from - `effectiveResistance_nonneg` once `m.laplacian` is shown to be - positive semi-definite. The standard energy identity - `xᵀ L x = (1/2) Σ_{u,v} c(u,v) (x_u − x_v)² + Σ_u c(u,u) x_u²` - gives PSD whenever `κ ≥ 0` entrywise. -* `d(u, w) ≤ d(u, v) + d(v, w)` (triangle inequality): the classical - Klein–Randić "resistance distance is a metric" theorem (Klein, - Randić 1993, J. Math. Chem.). Its formal proof routes through the - unit-current-flow / Thomson's principle variational framework. - -Both are well-known results; mathematically routine but require -substantial Mathlib infrastructure (graph Laplacian PSD calculus and -the energy variational characterisation of effective resistance, -respectively) that is not yet in scope. --/ - -namespace Hypergraph - -variable {V : Type*} {C : Type*} - -namespace HypergraphMetric - -variable [Fintype V] [DecidableEq V] {H : Hypergraph V C} - [Fintype H.edges] {params : WalkParams C} - -/-- **Math.** Effective-resistance distance derived from the HypergraphMetric -Laplacian: `d(u, v) := (e_u - e_v)ᵀ · L⁺ · (e_u - e_v)`. -/ -noncomputable def d (m : HypergraphMetric H params) (u v : V) : ℝ := - ProofAtlas.LinearAlgebra.effectiveResistance m.laplacian u v - -/-! ### Symmetry properties -/ - -/-- **Math.** Symmetry of the marginalised conductance, inherited from -the `symm` axiom on `κ`. -/ -lemma conductance_symm (m : HypergraphMetric H params) (u v : V) : - m.conductance u v = m.conductance v u := by - unfold conductance - refine Finset.sum_congr rfl (fun e _ => ?_) - exact m.symm e u v - -/-- **Math.** Symmetry of the Laplacian entry-wise: `L u v = L v u`. -/ -lemma laplacian_symm (m : HypergraphMetric H params) (u v : V) : - m.laplacian u v = m.laplacian v u := by - unfold laplacian - by_cases huv : u = v - · subst huv; rfl - · have hvu : ¬ v = u := fun e => huv e.symm - rw [if_neg huv, if_neg hvu, m.conductance_symm v u] - -/-- **Math.** The Laplacian is Hermitian (= symmetric over ℝ). -/ -lemma laplacian_isHermitian (m : HypergraphMetric H params) : - m.laplacian.IsHermitian := by - ext u v - change star (m.laplacian v u) = m.laplacian u v - rw [show star (m.laplacian v u) = m.laplacian v u from rfl, - m.laplacian_symm v u] - -/-! ### Two of the four metric axioms for `HypergraphMetric.d` -/ - -/-- **Math.** Metric axiom: `d(u, u) = 0`. Direct from -`effectiveResistance_self`. -/ -lemma d_self (m : HypergraphMetric H params) (u : V) : - m.d u u = 0 := - ProofAtlas.LinearAlgebra.effectiveResistance_self m.laplacian u - -/-- **Math.** Metric axiom: `d(u, v) = d(v, u)`. Direct from -`effectiveResistance_symm`. -/ -lemma d_symm (m : HypergraphMetric H params) (u v : V) : - m.d u v = m.d v u := - ProofAtlas.LinearAlgebra.effectiveResistance_symm m.laplacian u v - -end HypergraphMetric - -end Hypergraph diff --git a/lean/ProofAtlas/Metric/Resistance/Spectral.lean b/lean/ProofAtlas/Metric/Resistance/Spectral.lean deleted file mode 100644 index 637aa9e..0000000 --- a/lean/ProofAtlas/Metric/Resistance/Spectral.lean +++ /dev/null @@ -1,52 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Util.Linter.MathTag -import ProofAtlas.Metric.Resistance.Canonical -import ProofAtlas.Metric.Resistance.HarmonicLaplacian -import ProofAtlas.Metric.Resistance.Algebraic -import ProofAtlas.LinearAlgebra.SpectralExpansion - -/-! -# Spectral view of `d_H` - -Mode-by-mode expansion of the BDF distance: if `{(λ_i, φ_i)}` are the -eigendata of `L_κ`, then -``` - d_H(u, v) = ∑_{λ_i > 0} (φ_i(u) − φ_i(v))² / λ_i. -``` - -The bridge composes two independent pieces: -* `resistanceDist_eq_effectiveResistance` (Algebraic.lean, Thomson), -* `effectiveResistance_spectral_expansion` - (`LinearAlgebra/SpectralExpansion.lean`, proved). - -This view is the one used for tail bounds, hyperbolicity estimates, -and any analysis that needs to weight contributions by spectral data. --/ - -namespace Hypergraph - -variable {V : Type*} {C : Type*} - -/-- **Math.** *Spectral expansion of the BDF distance.* For each -non-trivial eigenmode of `L_κ`, the contribution to `d_H(u, v)` is -`(φ_i(u) − φ_i(v))² / λ_i`. Obtained by chaining the Thomson -principle with the spectral expansion of effective resistance. -/ -theorem resistanceDist_eq_spectral_sum [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (u v : V) : - let hL := H.harmonicLaplacian_isHermitian params - H.resistanceDist params u v - = ∑ i : V, - if hL.eigenvalues i ≠ 0 - then (hL.eigenvectorBasis i u - hL.eigenvectorBasis i v) ^ 2 - / hL.eigenvalues i - else 0 := by - rw [H.resistanceDist_eq_effectiveResistance params u v] - exact ProofAtlas.LinearAlgebra.effectiveResistance_spectral_expansion - (H.harmonicLaplacian_isHermitian params) u v - -end Hypergraph diff --git a/lean/ProofAtlas/Metric/Resistance/Structural.lean b/lean/ProofAtlas/Metric/Resistance/Structural.lean deleted file mode 100644 index 1378aae..0000000 --- a/lean/ProofAtlas/Metric/Resistance/Structural.lean +++ /dev/null @@ -1,241 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Util.Linter.MathTag -import ProofAtlas.Metric.Resistance.Canonical -import ProofAtlas.Metric.Resistance.Algebraic -import ProofAtlas.LinearAlgebra.MoorePenrose - -/-! -# Structural properties of `d_H` - -Two structural theorems on the BDF distance (paper 2 §5): - -* **Automorphism invariance** (Theorem 5.1) — `d_H` factors through - the abstract structure of `H`: any structure-preserving vertex - permutation leaves `d_H` unchanged. -* **Rayleigh monotonicity** (Theorem 5.2) — enlarging the edge set - (adding inference rules) can only shorten distances. - -Both are stated as theorems on `H.resistanceDist`. Proofs route through -the variational definition: the energy `E_H(f)` is structural -(invariant under automorphism) and monotone in the edge set -(extra edges can only increase energy). --/ - -namespace Hypergraph - -variable {V : Type*} {C : Type*} - -open ProofAtlas.LinearAlgebra - -/-! ## Helpers for Rayleigh monotonicity - -Per-edge energy depends on the underlying `Hyperedge` data only. We -introduce a `Hyperedge`-level scalar `hyperedgeEnergy` that matches -`Hypergraph.perEdgeEnergy` definitionally; this lets us transport -per-edge contributions across a sub-hypergraph inclusion (where the -`edge` field is preserved propositionally, not definitionally) via -ordinary `congrArg`. -/ - -/-- **Eng.** Per-edge Dirichlet energy expressed purely in terms of -the underlying `Hyperedge` data: `∑_{i,j} α(c)/((i+1)·q) · (f(in_i) − -f(out_j))²`. Matches `Hypergraph.perEdgeEnergy` definitionally -(see `perEdgeEnergy_eq_hyperedgeEnergy`). -/ -private noncomputable def hyperedgeEnergy - (he : Hyperedge V C) (params : WalkParams C) (f : V → ℝ) : ℝ := - ∑ i : Fin he.inArity, ∑ j : Fin he.outArity, - (params.alpha he.color / (((i.val + 1) * he.outArity : ℕ) : ℝ)) - * (f (he.inVertex i) - f (he.outVertex j))^2 - -/-- **Eng.** Definitional bridge: the `Hypergraph` per-edge energy -unfolds to the `Hyperedge`-level `hyperedgeEnergy` on `H.edge e`. -/ -private lemma perEdgeEnergy_eq_hyperedgeEnergy - (H : Hypergraph V C) (params : WalkParams C) - (e : H.edges) (f : V → ℝ) : - H.perEdgeEnergy params e f = hyperedgeEnergy (H.edge e) params f := - rfl - -/-- **Eng.** Per-edge energy is congruent across two hypergraphs -when the underlying `Hyperedge` data matches. Used to transport -each `H'.perEdgeEnergy e' f` into the matching `H.perEdgeEnergy (s e') f` -via `IsSubHypergraph.edge_eq`. -/ -private lemma perEdgeEnergy_congr_of_edge_eq - {H1 H2 : Hypergraph V C} (params : WalkParams C) - {e1 : H1.edges} {e2 : H2.edges} (h : H1.edge e1 = H2.edge e2) - (f : V → ℝ) : - H1.perEdgeEnergy params e1 f = H2.perEdgeEnergy params e2 f := by - rw [perEdgeEnergy_eq_hyperedgeEnergy, perEdgeEnergy_eq_hyperedgeEnergy, h] - -/-- **Math.** *Energy monotonicity under sub-hypergraph inclusion.* -For `H' ⊆ H` (an `IsSubHypergraph H' H` witness), every test -function `f : V → ℝ` satisfies `E_{H'}(f) ≤ E_H(f)`. Edge sums -re-index over the image of the inclusion (injective, so -`Finset.sum_image`); the remaining `H.edges \ image` terms are -non-negative by `perEdgeEnergy_nonneg`. -/ -private lemma dirichletEnergy_mono - {H H' : Hypergraph V C} [Fintype H.edges] [Fintype H'.edges] - (params : WalkParams C) (s : IsSubHypergraph H' H) (f : V → ℝ) : - H'.dirichletEnergy params f ≤ H.dirichletEnergy params f := by - classical - -- Step 1: rewrite the H'-sum via `s.toFun`, using - -- `H.edge (s.toFun e') = H'.edge e'` per edge. - have h_rewrite : - H'.dirichletEnergy params f - = ∑ e' : H'.edges, H.perEdgeEnergy params (s.toFun e') f := by - unfold dirichletEnergy - refine Finset.sum_congr rfl fun e' _ => ?_ - exact (perEdgeEnergy_congr_of_edge_eq params (s.edge_eq e') f).symm - rw [h_rewrite] - -- Step 2: reindex via the image of `s.toFun` (using injectivity). - have h_image : - (∑ e' : H'.edges, H.perEdgeEnergy params (s.toFun e') f) - = ∑ e ∈ (Finset.univ : Finset H'.edges).image s.toFun, - H.perEdgeEnergy params e f := by - rw [Finset.sum_image (fun x _ y _ h => s.injective h)] - rw [h_image] - -- Step 3: the image is a subset of `H.edges = Finset.univ`; drop - -- non-negative remaining terms. - unfold dirichletEnergy - refine Finset.sum_le_sum_of_subset_of_nonneg - (Finset.subset_univ _) - (fun e _ _ => H.perEdgeEnergy_nonneg params e f) - -/-- **Math.** *Automorphism invariance of `d_H`* (paper 2 §5.1). -For any automorphism `φ : Aut H` and any pair `(u, v)`, -`d_H(φ(u), φ(v)) = d_H(u, v)`: the BDF distance depends only on the -abstract structure of `H`, not on vertex labelling. -/ -theorem resistanceDist_aut_invariant [Fintype V] [DecidableEq V] - (H : Hypergraph V C) [Fintype H.edges] - (params : WalkParams C) (φ : Aut H) (u v : V) : - H.resistanceDist params (φ.vertexEquiv u) (φ.vertexEquiv v) - = H.resistanceDist params u v := by - -- Set up abbreviations. - set π : V ≃ V := φ.vertexEquiv with hπ_def - set L : Matrix V V ℝ := H.harmonicLaplacian params with hL_def - -- Pseudoinverse equivariance, via Obj 1's `pseudoinverse_aut`. - have hLp_aut : ∀ a b : V, L⁺ (π a) (π b) = L⁺ a b := - ProofAtlas.LinearAlgebra.pseudoinverse_aut - (H.harmonicLaplacian_isHermitian params) π - (H.harmonicLaplacian_aut params φ) - -- `signedIndicator (π u) (π v) (π w) = signedIndicator u v w`. - have h_sigInd : ∀ w : V, - ProofAtlas.LinearAlgebra.signedIndicator (π u) (π v) (π w) - = ProofAtlas.LinearAlgebra.signedIndicator u v w := by - intro w - unfold ProofAtlas.LinearAlgebra.signedIndicator - have h1 : (π w = π u) ↔ (w = u) := ⟨fun h => π.injective h, fun h => by rw [h]⟩ - have h2 : (π w = π v) ↔ (w = v) := ⟨fun h => π.injective h, fun h => by rw [h]⟩ - by_cases hu : w = u - · simp [hu] - · by_cases hv : w = v - · simp [hv] - · have hπu : π w ≠ π u := fun h => hu (h1.mp h) - have hπv : π w ≠ π v := fun h => hv (h2.mp h) - simp [hu, hv, hπu, hπv] - -- Unfold both sides to the quadratic-form double sum. - change ProofAtlas.LinearAlgebra.effectiveResistance L (π u) (π v) - = ProofAtlas.LinearAlgebra.effectiveResistance L u v - unfold ProofAtlas.LinearAlgebra.effectiveResistance - simp only [Matrix.mulVec, dotProduct] - -- Goal: ∑ w, sigInd(πu)(πv) w * ∑ w', L⁺ w w' * sigInd(πu)(πv) w' - -- = ∑ w, sigInd(u)(v) w * ∑ w', L⁺ w w' * sigInd(u)(v) w' - -- Reindex outer sum via π. - rw [← Equiv.sum_comp π - (fun w => ProofAtlas.LinearAlgebra.signedIndicator (π u) (π v) w - * ∑ w', L⁺ w w' - * ProofAtlas.LinearAlgebra.signedIndicator (π u) (π v) w')] - refine Finset.sum_congr rfl fun w _ => ?_ - -- Reindex inner sum via π. - rw [← Equiv.sum_comp π - (fun w' => L⁺ (π w) w' - * ProofAtlas.LinearAlgebra.signedIndicator (π u) (π v) w')] - -- Apply `h_sigInd` and `hLp_aut` pointwise. - rw [h_sigInd w] - congr 1 - refine Finset.sum_congr rfl fun w' _ => ?_ - rw [hLp_aut w w', h_sigInd w'] - -/-- **Math.** *Rayleigh monotonicity of `d_H`* (paper 2 §5.2). For -any sub-hypergraph `H' ⊆ H` (same vertex set, edge subset), -`d_H(u, v) ≤ d_{H'}(u, v)`: adding inference rules can only shorten -distances. - -**Signature note (iter-013 amendment).** The original statement -quantified only over the sub-hypergraph witness. As in `thomson_principle`, -the variational proof through the Dirichlet energy requires both -`u ≠ v` and the kernel hypotheses `h_range`, `h_range'` (signed -indicator lies in the range of each Laplacian), plus positivity -`h_pos`, `h_pos'`. These hypotheses are automatic for connected -harmonic graphs with `u ≠ v`. -/ -theorem resistanceDist_rayleigh [Fintype V] [DecidableEq V] - (H H' : Hypergraph V C) - [Fintype H.edges] [Fintype H'.edges] - (params : WalkParams C) (s : IsSubHypergraph H' H) - (u v : V) (huv : u ≠ v) - (h_pos : 0 < H.resistanceDist params u v) - (h_pos' : 0 < H'.resistanceDist params u v) - (h_range : ∃ y : V → ℝ, - (H.harmonicLaplacian params).mulVec y - = ProofAtlas.LinearAlgebra.signedIndicator u v) - (h_range' : ∃ y : V → ℝ, - (H'.harmonicLaplacian params).mulVec y - = ProofAtlas.LinearAlgebra.signedIndicator u v) : - H.resistanceDist params u v ≤ H'.resistanceDist params u v := by - -- Set abbreviations. - set d := H.resistanceDist params u v with hd_def - set d' := H'.resistanceDist params u v with hd'_def - -- Energy monotonicity from `dirichletEnergy_mono`. - have hE_mono : ∀ f : V → ℝ, - H'.dirichletEnergy params f ≤ H.dirichletEnergy params f := - fun f => dirichletEnergy_mono params s f - -- Set notation for the two Dirichlet-energy feasible sets. - set S : Set ℝ := - { e : ℝ | ∃ f : V → ℝ, f u - f v = 1 - ∧ H.dirichletEnergy params f = e } with hS_def - set S' : Set ℝ := - { e : ℝ | ∃ f : V → ℝ, f u - f v = 1 - ∧ H'.dirichletEnergy params f = e } with hS'_def - -- Thomson on H and H'. - have hT_H : sInf S = 1 / d := - H.thomson_principle params u v huv h_pos h_range - have hT_H' : sInf S' = 1 / d' := - H'.thomson_principle params u v huv h_pos' h_range' - -- Both sets are bounded below by 0 (Dirichlet energy is non-negative). - have hS_bddBelow : BddBelow S := by - refine ⟨0, ?_⟩ - rintro e ⟨f, _, hE⟩ - rw [← hE] - exact H.dirichletEnergy_nonneg params f - have hS'_bddBelow : BddBelow S' := by - refine ⟨0, ?_⟩ - rintro e ⟨f, _, hE⟩ - rw [← hE] - exact H'.dirichletEnergy_nonneg params f - -- `S` is nonempty (constant-on-{u,v} interpolation): explicit witness - -- `f w := if w = u then 1 else 0` has `f u - f v = 1` (since `u ≠ v`). - have hS_nonempty : S.Nonempty := by - refine ⟨H.dirichletEnergy params - (fun w => if w = u then (1 : ℝ) else 0), ?_⟩ - refine ⟨fun w => if w = u then (1 : ℝ) else 0, ?_, rfl⟩ - simp [huv.symm] - -- Monotonicity of the infimum: `sInf S' ≤ sInf S`. For each `e ∈ S` - -- with witness `f`, the same `f` gives `e' := H'.dirichletEnergy f ≤ e` - -- in `S'`. So `sInf S' ≤ e' ≤ e`. Hence `sInf S' ≤ sInf S`. - have h_inf_le : sInf S' ≤ sInf S := by - refine le_csInf hS_nonempty ?_ - rintro e ⟨f, hf_diff, hE_eq⟩ - have hE' : H'.dirichletEnergy params f ∈ S' := ⟨f, hf_diff, rfl⟩ - have h1 : sInf S' ≤ H'.dirichletEnergy params f := - csInf_le hS'_bddBelow hE' - have h2 : H'.dirichletEnergy params f ≤ e := by - rw [← hE_eq]; exact hE_mono f - exact h1.trans h2 - -- Translate `sInf` inequality to `1/d` inequality. - rw [hT_H, hT_H'] at h_inf_le - -- `1/d' ≤ 1/d` with `0 < d`, `0 < d'`, gives `d ≤ d'`. - exact le_of_one_div_le_one_div h_pos' h_inf_le - -end Hypergraph diff --git a/lean/ProofAtlas/Pipeline/Cache.lean b/lean/ProofAtlas/Pipeline/Cache.lean deleted file mode 100644 index 88ae778..0000000 --- a/lean/ProofAtlas/Pipeline/Cache.lean +++ /dev/null @@ -1,116 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import Lean -import ProofAtlas.Mapping.Declaration -import ProofAtlas.Pipeline.Linalg -import ProofAtlas.Pipeline.Hyperbolicity -import ProofAtlas.Pipeline.FloatMat - -/-! -# Per-session cache for the decl-level `.ns` pipeline - -The three `#atlas.*.ns` commands each need -`(IncidenceData, R matrix, components)` for the same namespace. Each -of those is non-trivial to compute (proof-body walks + O(n³) -Gauss-Jordan + a connectivity pass), so without a cache the user -pays the full cost three times when running `#atlas.graph.ns`, -`#atlas.resistance.ns`, `#atlas.delta.ns` in sequence. - -This cache is a simple `IO.Ref` mapping `(envSize, nsName)` to the -fully-computed profile. The version key uses `env.constants.size`: -the env grows monotonically across commands in a session, so -matching size + namespace is a sufficient correctness witness (no -ABA risk in the normal `#atlas` workflow, where commands never -add constants themselves). --/ - -namespace ProofAtlas.Pipeline - -open ProofAtlas.Mapping - -/-- **Eng.** Cached payload for one namespace: the decl-level -incidence, the resistance matrix as `FloatMat`, the connected -component labelling, the component count, and the largest -component's vertex IDs. Everything the `.ns` commands need. -/ -structure DeclNsProfile where - incidence : IncidenceData - resistance : FloatMat - componentLabel : Array Nat - numComponents : Nat - largestComponent : Array Nat - -instance : Inhabited DeclNsProfile where - default := ⟨IncidenceData.empty, default, #[], 0, #[]⟩ - -/-- **Eng.** Cache key: `(numConsts, nsName)`. `numConsts` is the -fold-counted size of `env.constants` and acts as a stable proxy for -"is the environment still the same set of declarations". Namespaces -are independent, so the namespace name is the other key component. -/ -private abbrev CacheKey := Nat × Lean.Name - -/-- **Eng.** Count of constants in the environment. Used as the -version key for the per-namespace cache. O(numConstants) per call -but folding a Lean `SMap` of 30k entries is well under a millisecond, -so this is cheap relative to the protected work. -/ -private def envNumConsts (env : Lean.Environment) : Nat := - env.constants.fold (init := 0) (fun n _ _ => n + 1) - -/-- **Eng.** Process-wide cache. Lives for the Lean server session; -cleared automatically on server restart. -/ -initialize declNsCache : IO.Ref (Std.HashMap CacheKey DeclNsProfile) ← IO.mkRef {} - -/-- **Eng.** Separate cache for the δ-hyperbolicity computation, -which is O(C(n,4)) and easily reaches tens of seconds on a 200-vertex -namespace. Lazy: only populated when `#atlas.delta.ns` actually asks. -/ -initialize deltaNsCache : IO.Ref (Std.HashMap CacheKey (Float × Float)) ← IO.mkRef {} - -/-- **Eng.** Build the full `DeclNsProfile` from scratch — no cache -lookup. Callers should usually go through `getOrBuildDeclProfile`. -/ -def buildDeclProfile (env : Lean.Environment) (nsName : Lean.Name) : - DeclNsProfile := - let decls := Environment.declsInNamespace env nsName - let incidence := Environment.toDeclIncidence env decls - let resistance := incidence.resistanceMatrixF - let (labels, nComps) := incidence.componentLabels - let largest := incidence.largestComponentVertices - { incidence := incidence - resistance := resistance - componentLabel := labels - numComponents := nComps - largestComponent := largest } - -/-- **Eng.** Return the cached `DeclNsProfile` for `nsName` or build -and cache it. Cache is keyed by `(env.constants.size, nsName)`. -/ -def getOrBuildDeclProfile (env : Lean.Environment) (nsName : Lean.Name) : - IO DeclNsProfile := do - let key : CacheKey := (envNumConsts env, nsName) - let cache ← declNsCache.get - match cache[key]? with - | some p => return p - | none => - let p := buildDeclProfile env nsName - declNsCache.modify (·.insert key p) - return p - -/-- **Eng.** Return the (δ, diameter) pair for the largest component -of `nsName`, computing on first request and caching for the rest of -the session. The δ enumeration is C(n, 4) and slow (tens of seconds -on 200-vertex namespaces); without this cache the cost is paid every -time the user runs `#atlas.delta.ns`. -/ -def getOrComputeDelta (env : Lean.Environment) (nsName : Lean.Name) : - IO (Float × Float) := do - let key : CacheKey := (envNumConsts env, nsName) - let cache ← deltaNsCache.get - match cache[key]? with - | some r => return r - | none => - let p ← getOrBuildDeclProfile env nsName - let δ := hyperbolicityConstantOnF p.resistance p.largestComponent - let diam := diameterOnF p.resistance p.largestComponent - deltaNsCache.modify (·.insert key (δ, diam)) - return (δ, diam) - -end ProofAtlas.Pipeline diff --git a/lean/ProofAtlas/Pipeline/Export.lean b/lean/ProofAtlas/Pipeline/Export.lean deleted file mode 100644 index 6822590..0000000 --- a/lean/ProofAtlas/Pipeline/Export.lean +++ /dev/null @@ -1,68 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import Lean -import ProofAtlas.Pipeline.IncidenceData -import ProofAtlas.Mapping.ExprColor - -/-! -# `ToJson` instances for the geometric report - -Serialisation glue so any `HypergraphReport` (and the data -structures it nests) can be emitted as `Lean.Json`. Consumed by -`Command/Export.lean` for the `#atlas.export` command, and usable -from any user `IO` action that wants to dump a profile to disk. - -`ExprColor` is encoded as a short string (`"app"`, `"lam"`, …) so -the JSON is human-readable rather than tagged-union shape. --/ - -namespace ProofAtlas.Mapping - -/-- **Eng.** Short string representation of an `ExprColor`, -matching the 12 constructor names of `Lean.Expr`. -/ -def ExprColor.toString : ExprColor → String - | .bvar => "bvar" - | .fvar => "fvar" - | .mvar => "mvar" - | .sort => "sort" - | .const => "const" - | .app => "app" - | .lam => "lam" - | .forallE => "forallE" - | .letE => "letE" - | .lit => "lit" - | .mdata => "mdata" - | .proj => "proj" - -instance : Lean.ToJson ExprColor where - toJson c := Lean.Json.str c.toString - -end ProofAtlas.Mapping - -namespace ProofAtlas.Pipeline - -open Lean - -instance : ToJson EdgeRec where - toJson e := Json.mkObj - [ ("inputs", toJson e.inputs) - , ("output", toJson e.output) - , ("color", toJson e.color) ] - -instance : ToJson IncidenceData where - toJson H := Json.mkObj - [ ("numVertices", toJson H.numVertices) - , ("edges", toJson H.edges) - , ("vertexLabel", toJson H.vertexLabel) ] - -instance : ToJson HypergraphReport where - toJson R := Json.mkObj - [ ("incidence", toJson R.incidence) - , ("resistance", toJson R.resistance) - , ("delta", toJson R.delta) - , ("roots", toJson R.roots) ] - -end ProofAtlas.Pipeline diff --git a/lean/ProofAtlas/Pipeline/FloatMat.lean b/lean/ProofAtlas/Pipeline/FloatMat.lean deleted file mode 100644 index 8d712a2..0000000 --- a/lean/ProofAtlas/Pipeline/FloatMat.lean +++ /dev/null @@ -1,83 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ - -/-! -# `FloatMat` — row-major flat-packed Float matrix - -The `Array (Array Float)` representation used by the rest of the -pipeline is convenient but slow: each access has two array lookups -(outer + inner), each with a bounds check, and the outer cells store -boxed `Array Float` pointers. For the `.ns` commands operating on -240+ vertex decl-graphs, the cost shows up immediately — Gauss-Jordan -inversion is the dominant hot path, with O(n³) array accesses inside. - -`FloatMat` flattens the `n × n` data into a single `FloatArray` -indexed manually as `i * n + j`. `FloatArray` stores native (unboxed) -`Float64` and supports in-place mutation when the reference count is -1 — so the standard `let mut M := ...; M := M.set ...` pattern stays -allocation-free inside `do` blocks. - -This module is intentionally pure infrastructure: no resistance, no -δ, no caching. Callers in `Pipeline/Linalg.lean` and -`Pipeline/Hyperbolicity.lean` consume it. --/ - -namespace ProofAtlas.Pipeline - -/-- **Eng.** *Row-major flat-packed `n × n` Float matrix.* Stored as -a single `FloatArray` of length `n * n` with `(i, j)` ↦ `i * n + j`. -/ -structure FloatMat where - n : Nat - data : FloatArray - deriving Inhabited - -namespace FloatMat - -/-- **Eng.** Build a `FloatArray` of length `n` filled with `v`. The -loop body is in-place after the first `push`. -/ -def floatArrayReplicate (n : Nat) (v : Float) : FloatArray := Id.run do - let mut a := FloatArray.emptyWithCapacity n - for _ in [:n] do - a := a.push v - return a - -/-- **Eng.** Zero `n × n` matrix. -/ -def zero (n : Nat) : FloatMat := - { n := n, data := floatArrayReplicate (n * n) 0.0 } - -/-- **Eng.** `n × n` identity matrix. -/ -def id (n : Nat) : FloatMat := Id.run do - let mut data := floatArrayReplicate (n * n) 0.0 - for i in [:n] do - data := data.set! (i * n + i) 1.0 - return { n := n, data := data } - -/-- **Eng.** Linear (flat) index from `(i, j)`. -/ -@[inline] def idx (M : FloatMat) (i j : Nat) : Nat := i * M.n + j - -/-- **Eng.** Get `M[i, j]`. -/ -@[inline] def get (M : FloatMat) (i j : Nat) : Float := - M.data.get! (i * M.n + j) - -/-- **Eng.** Set `M[i, j] := x`. In-place when `M` is uniquely -referenced (the usual `let mut` pattern in a `do` block). -/ -@[inline] def set (M : FloatMat) (i j : Nat) (x : Float) : FloatMat := - { M with data := M.data.set! (i * M.n + j) x } - -/-- **Eng.** Convert from an `Array (Array Float)`. Assumes square. -/ -def ofArrayMatrix (A : Array (Array Float)) : FloatMat := Id.run do - let n := A.size - let mut data := floatArrayReplicate (n * n) 0.0 - for h : i in [:n] do - let row := A[i] - for h' : j in [:row.size] do - if j < n then - data := data.set! (i * n + j) row[j] - return { n := n, data := data } - -end FloatMat - -end ProofAtlas.Pipeline diff --git a/lean/ProofAtlas/Pipeline/Hausdorff.lean b/lean/ProofAtlas/Pipeline/Hausdorff.lean deleted file mode 100644 index a264647..0000000 --- a/lean/ProofAtlas/Pipeline/Hausdorff.lean +++ /dev/null @@ -1,94 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Pipeline.IncidenceData -import ProofAtlas.Pipeline.Linalg - -/-! -# Subtree extraction + Float Hausdorff distance - -Two utilities consumed by the `#atlas.hausdorff.*` command suite: - -* `IncidenceData.subtreeVertices H r` — the set of vertex IDs - reachable from a root vertex `r` by following the - `output → inputs` direction of the hypergraph. In the const-shared - incidence graph this is the set of subterm occurrences belonging - to the proof rooted at `r`. - -* `hausdorffOnMatrix R A B` — the standard Hausdorff distance - between two non-empty index sets `A, B` on a precomputed - symmetric distance matrix `R`. Cost `|A| · |B|`. - -The Hausdorff formula consumed by `#atlas.hausdorff.resistance` is -applied to the *union* of subtree vertex sets per group, so it -measures the structural distance between two collections of -proofs rather than between two singleton root vertices. --/ - -namespace ProofAtlas.Pipeline - -/-- **Eng.** Vertex IDs reachable from `root` in `H` via the -`output → inputs` direction of each hyperedge. Treats the -incidence as a directed graph rooted at `root`. -/ -def IncidenceData.subtreeVertices - (H : IncidenceData) (root : Nat) : Array Nat := Id.run do - -- Pre-compute output → inputs lookup once per call. - let mut outMap : Std.HashMap Nat (Array Nat) := {} - for e in H.edges do - outMap := outMap.insert e.output e.inputs - let mut visited : Std.HashSet Nat := {} - let mut stack : Array Nat := #[root] - while stack.size > 0 do - let v := stack.back! - stack := stack.pop - if visited.contains v then continue - visited := visited.insert v - match outMap[v]? with - | some children => - for c in children do - if !visited.contains c then - stack := stack.push c - | none => pure () - return visited.toArray - -/-- **Eng.** Union of subtree vertex sets over a collection of root -vertices. The natural "set of subterm occurrences belonging to this -group of declarations" in the const-shared incidence graph. -/ -def IncidenceData.subtreesUnion - (H : IncidenceData) (roots : Array Nat) : Array Nat := Id.run do - let mut visited : Std.HashSet Nat := {} - for r in roots do - for v in H.subtreeVertices r do - visited := visited.insert v - return visited.toArray - -/-- **Eng.** Float Hausdorff distance on a row-indexable distance -matrix `R`, between two non-empty index sets `A` and `B`: - - $$ H(A, B) = \max\bigl( \max_{a \in A} \min_{b \in B} R[a][b],\; - \max_{b \in B} \min_{a \in A} R[a][b] \bigr). $$ --/ -def hausdorffOnMatrix - (R : Array (Array Float)) (A B : Array Nat) : Float := Id.run do - if A.isEmpty || B.isEmpty then return 0.0 - -- max over a ∈ A of min over b ∈ B of R[a][b] - let mut sup1 : Float := 0.0 - for a in A do - let mut inf : Float := R[a]![B[0]!]! - for b in B do - let d := R[a]![b]! - if d < inf then inf := d - if inf > sup1 then sup1 := inf - -- max over b ∈ B of min over a ∈ A of R[a][b] - let mut sup2 : Float := 0.0 - for b in B do - let mut inf : Float := R[A[0]!]![b]! - for a in A do - let d := R[a]![b]! - if d < inf then inf := d - if inf > sup2 then sup2 := inf - return max sup1 sup2 - -end ProofAtlas.Pipeline diff --git a/lean/ProofAtlas/Pipeline/Hyperbolicity.lean b/lean/ProofAtlas/Pipeline/Hyperbolicity.lean deleted file mode 100644 index 9d5b7e2..0000000 --- a/lean/ProofAtlas/Pipeline/Hyperbolicity.lean +++ /dev/null @@ -1,151 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Pipeline.IncidenceData -import ProofAtlas.Pipeline.Linalg - -/-! -# Gromov δ-hyperbolicity (Float) - -The four-point hyperbolicity constant of a finite metric space `(X, d)`: - - δ = (1/2) · max_{w,x,y,z} fpd(w, x, y, z) - -where the *four-point deviation* `fpd` is the gap between the top and -middle of the three pairings: - - S₁ = d(w, x) + d(y, z) - S₂ = d(w, y) + d(x, z) - S₃ = d(w, z) + d(x, y) - fpd = max(S₁, S₂, S₃) − median(S₁, S₂, S₃) - -A metric is *δ-hyperbolic* iff every 4-tuple has fpd ≤ 2δ. - -## Implementation - -`fpd` is symmetric in its four arguments (every permutation produces -the same multiset `{S₁, S₂, S₃}`) and vanishes whenever two arguments -coincide (in a metric, by the triangle inequality `d(y,z) ≤ -d(w,y) + d(w,z)`). So enumerating strict `w < x < y < z` reaches the -same maximum at `C(n, 4)` cost instead of `n⁴` — a 24× speedup -asymptotically. For `n = 67` that's `766k` 4-tuples instead of -`20.1M`, cutting `#atlas.delta` from seconds to a fraction of one. - -For very large `n` (say `n ≥ 200`), even `C(n, 4)` becomes -expensive; the next step is Monte-Carlo sampling — not yet wired. --/ - -namespace ProofAtlas.Pipeline - -/-- **Eng.** Median of three reals. -/ -private def median3 (a b c : Float) : Float := - max (min a b) (min (max a b) c) - -/-- **Eng.** *Four-point deviation* at vertices `w, x, y, z` of the -given distance matrix. Output is the max-minus-median of the three -pairings; halved gives a lower bound on `δ`. -/ -def fourPointDeviation (D : Array (Array Float)) (w x y z : Nat) : Float := - let s₁ := D[w]![x]! + D[y]![z]! - let s₂ := D[w]![y]! + D[x]![z]! - let s₃ := D[w]![z]! + D[x]![y]! - max (max s₁ s₂) s₃ - median3 s₁ s₂ s₃ - -/-- **Eng.** *Gromov δ-hyperbolicity constant* via enumeration over -unordered strictly-increasing 4-tuples `w < x < y < z`. Returns -`(1/2) · max_{w,x,y,z} fpd`. See module docstring for why this is -equivalent to the `n⁴` enumeration but ~24× faster. -/ -def hyperbolicityConstant (D : Array (Array Float)) : Float := Id.run do - let n := D.size - let mut δ : Float := 0.0 - for w in [:n] do - for x in [w + 1 : n] do - for y in [x + 1 : n] do - for z in [y + 1 : n] do - let dev := fourPointDeviation D w x y z - if dev > δ then δ := dev - return 0.5 * δ - -/-- **Eng.** *δ-hyperbolicity of an `IncidenceData`'s resistance -metric.* Convenience: build the resistance matrix, then enumerate. -/ -def IncidenceData.hyperbolicity (H : IncidenceData) : Float := - hyperbolicityConstant H.resistanceMatrix - -/-- **Eng.** *δ-hyperbolicity restricted to a vertex subset.* Same -enumeration but only over 4-tuples drawn from `verts`. Used to -compute δ on a single connected component when the full graph is -disconnected (cross-component "distances" otherwise dominate). -/ -def hyperbolicityConstantOn (D : Array (Array Float)) (verts : Array Nat) : - Float := Id.run do - let k := verts.size - let mut δ : Float := 0.0 - for wi in [:k] do - for xi in [wi + 1 : k] do - for yi in [xi + 1 : k] do - for zi in [yi + 1 : k] do - let dev := fourPointDeviation D verts[wi]! verts[xi]! verts[yi]! verts[zi]! - if dev > δ then δ := dev - return 0.5 * δ - -/-- **Eng.** *Diameter restricted to a vertex subset.* Max pairwise -distance with both endpoints in `verts`. -/ -def diameterOn (D : Array (Array Float)) (verts : Array Nat) : Float := Id.run do - let mut m : Float := 0.0 - for u in verts do - for v in verts do - let d := D[u]![v]! - if d > m then m := d - return m - -/-! ## FloatMat fast path -/ - -/-- **Eng.** *Four-point deviation on `FloatMat`.* -/ -@[inline] def fourPointDeviationF (D : FloatMat) (w x y z : Nat) : Float := - let s₁ := D.get w x + D.get y z - let s₂ := D.get w y + D.get x z - let s₃ := D.get w z + D.get x y - max (max s₁ s₂) s₃ - median3 s₁ s₂ s₃ - -/-- **Eng.** *Hot-path δ-hyperbolicity on `FloatMat`, restricted to a -vertex subset.* Hoists the outer-tuple distance reads out of the inner -loops (e.g. `D[w, x]` doesn't depend on `y` or `z`), so each innermost -iteration only reads three new distances instead of six. -/ -def hyperbolicityConstantOnF (D : FloatMat) (verts : Array Nat) : Float := Id.run do - let k := verts.size - let n := D.n - let mut δ : Float := 0.0 - for wi in [:k] do - let w := verts[wi]! - let wn := w * n - for xi in [wi + 1 : k] do - let x := verts[xi]! - let xn := x * n - let dwx := D.data.get! (wn + x) - for yi in [xi + 1 : k] do - let y := verts[yi]! - let yn := y * n - let dwy := D.data.get! (wn + y) - let dxy := D.data.get! (xn + y) - for zi in [yi + 1 : k] do - let z := verts[zi]! - let dwz := D.data.get! (wn + z) - let dxz := D.data.get! (xn + z) - let dyz := D.data.get! (yn + z) - let s₁ := dwx + dyz - let s₂ := dwy + dxz - let s₃ := dwz + dxy - let dev := max (max s₁ s₂) s₃ - median3 s₁ s₂ s₃ - if dev > δ then δ := dev - return 0.5 * δ - -/-- **Eng.** *Diameter on `FloatMat`, restricted to a vertex subset.* -/ -def diameterOnF (D : FloatMat) (verts : Array Nat) : Float := Id.run do - let mut m : Float := 0.0 - for u in verts do - for v in verts do - let d := D.get u v - if d > m then m := d - return m - -end ProofAtlas.Pipeline diff --git a/lean/ProofAtlas/Pipeline/IncidenceData.lean b/lean/ProofAtlas/Pipeline/IncidenceData.lean deleted file mode 100644 index 1a7f711..0000000 --- a/lean/ProofAtlas/Pipeline/IncidenceData.lean +++ /dev/null @@ -1,255 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import Lean -import ProofAtlas.Mapping.ExprColor - -/-! -# `IncidenceData` — computational layer for hypergraph profiles - -The proof layer (`Hypergraph` + `IsHypergraphMetric`) provides correctness -guarantees but is non-computational (carries proofs, uses abstract `ℝ`). -For end-to-end `#eval`-able pipelines (`Environment → IO Float`), we -need a *data-only* representation. - -`IncidenceData` is that representation: a finite hypergraph encoded as -plain `Array`s of vertex indices and edge records, plus optional -labels for debugging. - -The numeric type throughout the compute layer is `Float`. The proof -layer keeps `ℝ`. A future `equiv` lemma will bridge the two, but is -not required for the MVP. --/ - -namespace ProofAtlas.Pipeline - -open ProofAtlas.Mapping (ExprColor) - -/-- **Eng.** *Computational hyperedge record.* `inputs` and `output` -are vertex indices into the ambient `IncidenceData.numVertices`. -`color` is the constructor-kind tag inherited from `ExprColor`. -/ -structure EdgeRec where - inputs : Array Nat - output : Nat - color : ExprColor - deriving Repr - -/-- **Eng.** *Plain-data hypergraph.* No proof obligations; designed -for fast traversal and matrix construction. Vertex IDs are -`0, …, numVertices - 1`. -/ -structure IncidenceData where - numVertices : Nat - edges : Array EdgeRec - /-- Debug labels (one per vertex). Empty array means "no labels". -/ - vertexLabel : Array String := #[] - /-- Per-vertex CSS colour string (e.g. `"#d63031"`). Empty entries - fall back to the widget's default (background fill, foreground - stroke). Used by decl-level graphs to colour vertices by - declaration kind; expr-level extraction leaves this empty. -/ - vertexColor : Array String := #[] - deriving Repr - -namespace IncidenceData - -/-- **Eng.** The empty hypergraph (one vertex, no edges). Used as -the seed for fold-based construction. -/ -def empty : IncidenceData := - { numVertices := 0, edges := #[], vertexLabel := #[] } - -/-- **Eng.** Number of hyperedges. -/ -def numEdges (H : IncidenceData) : Nat := H.edges.size - -/-- **Eng.** Weakly-connected component labelling of an -`IncidenceData`. Treats each hyperedge as a clique on -`inputs ∪ {output}` for adjacency purposes (same convention as -the conductance matrix). Returns `(labels, numComponents)` where -`labels[v]` is the 1-based component ID of vertex `v` (so `0` never -appears in a successful run; we use `0` internally as "unvisited"). -/ -partial def componentLabels (H : IncidenceData) : Array Nat × Nat := Id.run do - let n := H.numVertices - -- Adjacency: for each edge, every pair in inputs ∪ {output}. - let mut adj : Array (Array Nat) := Array.replicate n #[] - for e in H.edges do - let vs := e.inputs.push e.output - for i in [:vs.size] do - for j in [:vs.size] do - if i ≠ j then - adj := adj.modify vs[i]! (·.push vs[j]!) - -- DFS, label[v] = component (0 = unvisited). - let mut label : Array Nat := Array.replicate n 0 - let mut numComponents : Nat := 0 - for start in [:n] do - if label[start]! = 0 then - numComponents := numComponents + 1 - let comp := numComponents - label := label.set! start comp - let mut stack : Array Nat := #[start] - while stack.size > 0 do - let v := stack.back! - stack := stack.pop - for u in adj[v]! do - if label[u]! = 0 then - label := label.set! u comp - stack := stack.push u - return (label, numComponents) - -/-- **Eng.** Vertex IDs of the largest weakly-connected component. -Ties broken by smallest component ID. Returns `#[]` for the empty -graph. -/ -def largestComponentVertices (H : IncidenceData) : Array Nat := Id.run do - let (label, nComps) := H.componentLabels - if nComps = 0 then return #[] - let mut sizes : Array Nat := Array.replicate nComps 0 - for l in label do - if l ≥ 1 then - sizes := sizes.modify (l - 1) (· + 1) - -- Pick the largest by size, ties broken by smallest index. - let mut bestComp : Nat := 1 - let mut bestSize : Nat := sizes[0]! - for i in [1:nComps] do - if sizes[i]! > bestSize then - bestSize := sizes[i]! - bestComp := i + 1 - let mut out : Array Nat := #[] - for v in [:label.size] do - if label[v]! = bestComp then - out := out.push v - return out - -end IncidenceData - -/-- **Eng.** *Geometric profile report.* The end-to-end output of -`geometricProfile : Lean.Environment → IO HypergraphReport`. Holds -the computed incidence data, the all-pairs resistance distance -matrix, and the δ-hyperbolicity constant. -/ -structure HypergraphReport where - incidence : IncidenceData - /-- `resistance[u]![v]!` = effective resistance distance between - vertices `u` and `v` in the BDF random walk. -/ - resistance : Array (Array Float) - /-- Gromov δ-hyperbolicity constant (max four-point deviation / 2). -/ - delta : Float - /-- Root vertex ID of each input `Expr` that produced this report, - in input order. For a single-`Expr` report this is `#[0]`; for a - multi-`Expr` glue this records "where each input lives" inside the - combined `IncidenceData`. Commands like `#atlas.resistance A B` - use this to address `R(root_A, root_B)`. -/ - roots : Array Nat := #[] - deriving Repr - -namespace HypergraphReport - -/-- **Eng.** A blank report (zero vertices, no edges, no metrics). -Useful as a placeholder during pipeline scaffolding. -/ -def empty : HypergraphReport := - { incidence := IncidenceData.empty, resistance := #[], delta := 0.0 } - -/-- **Eng.** Resistance *diameter*: the maximum effective resistance -distance between any two vertices. Returns `0.0` for empty / trivial -graphs. -/ -def diameter (R : HypergraphReport) : Float := Id.run do - let mut m : Float := 0.0 - for row in R.resistance do - for x in row do - if x > m then m := x - return m - -/-- **Eng.** Mean off-diagonal resistance distance. Counts each -unordered pair once. -/ -def meanResistance (R : HypergraphReport) : Float := Id.run do - let n := R.resistance.size - if n < 2 then return 0.0 - let mut sum : Float := 0.0 - let mut cnt : Nat := 0 - for h : i in [:n] do - let row := R.resistance[i] - for h' : j in [:row.size] do - if i < j then - sum := sum + row[j] - cnt := cnt + 1 - if cnt == 0 then return 0.0 - return sum / cnt.toFloat - -/-- **Eng.** Per-`ExprColor` count of how many hyperedges of that -kind appear. Returns the 12 entries in palette order. -/ -def edgeColorCounts (R : HypergraphReport) : - Array (ProofAtlas.Mapping.ExprColor × Nat) := Id.run do - let palette : Array ProofAtlas.Mapping.ExprColor := - #[.bvar, .fvar, .mvar, .sort, .const, .lit, - .app, .lam, .forallE, .letE, .mdata, .proj] - let counts := palette.map fun c => - (c, R.incidence.edges.foldl - (fun acc e => if e.color == c then acc + 1 else acc) 0) - return counts.filter (fun (_, n) => n > 0) - -private def colorShortName : ProofAtlas.Mapping.ExprColor → String - | .bvar => "bvar" | .fvar => "fvar" | .mvar => "mvar" - | .sort => "sort" | .const => "const" | .lit => "lit" - | .app => "app" | .lam => "lam" | .forallE => "∀" - | .letE => "let" | .mdata => "mdata" | .proj => "proj" - -/-- **Eng.** Multi-line summary of the geometric profile. Reports -combinatorial size, edge-colour distribution, and the resistance- -based geometric quantities: diameter, mean distance, δ-hyperbolicity, -and the normalised tree-likeness ratio δ/diam (the key quantity in -BDF26 paper §7). -/ -def summary (R : HypergraphReport) : String := - let n := R.incidence.numVertices - let m := R.incidence.numEdges - let diam := R.diameter - let mean := R.meanResistance - let δ := R.delta - let normHyp : Float := - if diam > 0.0 then δ / diam else 0.0 - let colorStr := - String.intercalate ", " <| - R.edgeColorCounts.toList.map - (fun (c, k) => s!"{colorShortName c}={k}") - s!"BDF-Hypergraph Report\n" - ++ s!" vertices {n}\n" - ++ s!" edges {m} [{colorStr}]\n" - ++ s!" diameter {diam}\n" - ++ s!" mean dist {mean}\n" - ++ s!" δ {δ}\n" - ++ s!" δ / diam {normHyp}" - -end HypergraphReport - -/-! ### Builder state -/ - -/-- **Eng.** *Construction state* for the `Expr → IncidenceData` -folder: the running `IncidenceData` plus a const-sharing map mapping -each `Lean.Name` of a `.const` reference to the unique vertex -allocated for it. -/ -structure ConvState where - data : IncidenceData - constMap : Std.HashMap Lean.Name Nat - -namespace ConvState - -/-- **Eng.** Empty state: no vertices, no edges, empty const map. -/ -def empty : ConvState := - { data := IncidenceData.empty, constMap := ∅ } - -/-- **Eng.** Allocate a fresh vertex with a label; return updated -state and the new vertex's index. -/ -def allocVertex (s : ConvState) (label : String) : ConvState × Nat := - let vid := s.data.numVertices - let data' : IncidenceData := - { numVertices := s.data.numVertices + 1 - edges := s.data.edges - vertexLabel := s.data.vertexLabel.push label } - ({ s with data := data' }, vid) - -/-- **Eng.** Append an edge to the running incidence data. -/ -def addEdge (s : ConvState) (e : EdgeRec) : ConvState := - { s with data := - { numVertices := s.data.numVertices - edges := s.data.edges.push e - vertexLabel := s.data.vertexLabel } } - -end ConvState - -end ProofAtlas.Pipeline diff --git a/lean/ProofAtlas/Pipeline/Linalg.lean b/lean/ProofAtlas/Pipeline/Linalg.lean deleted file mode 100644 index c66ebb8..0000000 --- a/lean/ProofAtlas/Pipeline/Linalg.lean +++ /dev/null @@ -1,257 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Pipeline.IncidenceData -import ProofAtlas.Pipeline.FloatMat - -/-! -# `IncidenceData` linear algebra (Float) - -Computational layer: Laplacian construction + Gauss-Jordan matrix -inversion + effective resistance distance. All in `Float`. No -proofs. - -## Resistance formula - -For a graph Laplacian `L`, the effective resistance distance is - - `R(u, v) = (e_u - e_v)ᵀ L⁺ (e_u - e_v) = L⁺[u,u] + L⁺[v,v] - 2 L⁺[u,v]` - -where `L⁺` is the Moore-Penrose pseudoinverse. Since `e_u - e_v` is -orthogonal to `ker L = span 1`, we may instead invert the -*regularised* Laplacian `L + ε I` (ε ≈ 1e-9) and use the same formula -— the regularisation term cancels out modulo numerical precision. - -## Hypergraph clique expansion - -Each hyperedge with vertices `V_e = inputs ∪ {output}` contributes -unit conductance to every distinct pair in `V_e × V_e`. The resulting -weighted graph Laplacian feeds the resistance formula. --/ - -namespace ProofAtlas.Pipeline - -/-! ## Matrix utilities (Float) -/ - -/-- **Eng.** Allocate an `n × n` zero matrix. -/ -def zeroMatrix (n : Nat) : Array (Array Float) := - Array.replicate n (Array.replicate n 0.0) - -/-- **Eng.** Allocate an `n × n` identity matrix. -/ -def identityMatrix (n : Nat) : Array (Array Float) := Id.run do - let mut M := zeroMatrix n - for i in [:n] do - M := M.modify i (·.set! i 1.0) - return M - -/-- **Eng.** *Gauss-Jordan matrix inversion with partial pivoting.* -Returns `none` if the matrix is singular (within tolerance `1e-12`). -/ -partial def gaussInvert (A : Array (Array Float)) : Option (Array (Array Float)) := Id.run do - let n := A.size - -- Augment [A | I]; work in-place on a 2n-wide row. - let mut M : Array (Array Float) := Id.run do - let I := identityMatrix n - let mut M' : Array (Array Float) := Array.replicate n #[] - for i in [:n] do - M' := M'.set! i (A[i]! ++ I[i]!) - return M' - -- Forward elimination with partial pivoting. - for col in [:n] do - -- Pivot. - let mut maxRow := col - let mut maxVal := M[col]![col]!.abs - for row in [col+1:n] do - let v := M[row]![col]!.abs - if v > maxVal then - maxRow := row - maxVal := v - if maxVal < 1e-12 then - return none - if maxRow ≠ col then - let rowA := M[col]! - let rowB := M[maxRow]! - M := (M.set! col rowB).set! maxRow rowA - -- Scale pivot row. - let pivot := M[col]![col]! - M := M.modify col (·.map (· / pivot)) - -- Eliminate other rows. - for row in [:n] do - if row ≠ col then - let factor := M[row]![col]! - if factor.abs > 1e-15 then - let pivotRow := M[col]! - M := M.modify row (fun r => - r.mapIdx (fun j x => x - factor * pivotRow[j]!)) - -- Extract right half (the inverse). - let mut Inv : Array (Array Float) := Array.replicate n #[] - for i in [:n] do - Inv := Inv.set! i (M[i]!.extract n (2 * n)) - return some Inv - -/-! ## Laplacian + resistance -/ - -/-- **Eng.** *Hypergraph clique-expanded conductance matrix.* Each -hyperedge contributes unit conductance to every distinct pair of its -incident vertices (`inputs ∪ {output}`). -/ -def IncidenceData.conductance (H : IncidenceData) : Array (Array Float) := Id.run do - let n := H.numVertices - let mut C := zeroMatrix n - for e in H.edges do - let vs := e.inputs.push e.output - for i in [:vs.size] do - for j in [:vs.size] do - if i ≠ j then - let u := vs[i]! - let v := vs[j]! - C := C.modify u (fun row => row.modify v (· + 1.0)) - return C - -/-- **Eng.** *Graph Laplacian* `L = D − C` from the conductance -matrix: diagonal entries are row sums of `C`; off-diagonal entries -are `−C[u,v]`. -/ -def IncidenceData.laplacian (H : IncidenceData) : Array (Array Float) := Id.run do - let n := H.numVertices - let C := H.conductance - let mut L := zeroMatrix n - for u in [:n] do - let mut rowSum := 0.0 - let mut row : Array Float := Array.replicate n 0.0 - for v in [:n] do - let c := C[u]![v]! - rowSum := rowSum + c - row := row.set! v (-c) - row := row.set! u rowSum - L := L.set! u row - return L - -/-- **Eng.** *Effective resistance distance matrix.* Computes -`R[u,v] = (L+εI)⁻¹[u,u] + (L+εI)⁻¹[v,v] − 2(L+εI)⁻¹[u,v]`. The -regularisation ε is chosen small enough to vanish to floating-point -noise in the final difference. Returns the all-zero matrix if -inversion fails (i.e., the graph is too degenerate). -/ -def IncidenceData.resistanceMatrix (H : IncidenceData) : Array (Array Float) := Id.run do - let n := H.numVertices - if n = 0 then return #[] - let L := H.laplacian - -- Regularise: L + ε I with ε = 1e-9 - let ε := 1e-9 - let mut Lreg := L - for i in [:n] do - Lreg := Lreg.modify i (fun row => row.modify i (· + ε)) - match gaussInvert Lreg with - | none => return zeroMatrix n - | some Linv => - let mut R := zeroMatrix n - for u in [:n] do - let mut row : Array Float := Array.replicate n 0.0 - for v in [:n] do - let r := Linv[u]![u]! + Linv[v]![v]! - 2.0 * Linv[u]![v]! - row := row.set! v r - R := R.set! u row - return R - -/-! ## FloatMat fast path - -For the decl-level `.ns` commands (vertex counts in the 200–500 -range), the boxed `Array (Array Float)` representation becomes the -bottleneck. The `*F`-suffixed variants below operate on `FloatMat` -(flat-packed, unboxed Float64) and run an order of magnitude faster -on the hot Gauss-Jordan path. -/ - -/-- **Eng.** *Gauss-Jordan inversion on `FloatMat`.* Same algorithm -as `gaussInvert` but with flat indexing throughout — no inner-array -bounds check, no `Array Float` boxing. Returns `none` on singular -matrix (pivot magnitude below `1e-12`). -/ -partial def gaussInvertF (A : FloatMat) : Option FloatMat := Id.run do - let n := A.n - if n = 0 then return some A - -- Working copies: L starts as A, transforms to I; Inv starts as I, transforms to A⁻¹. - let mut L : FloatMat := { n := n, data := A.data } - let mut Inv : FloatMat := FloatMat.id n - for k in [:n] do - -- Partial pivot: find row with largest |L[r, k]| in column k below row k. - let mut pivot := k - let mut maxAbs := (L.get k k).abs - for r in [k + 1 : n] do - let a := (L.get r k).abs - if a > maxAbs then - maxAbs := a - pivot := r - if maxAbs < 1e-12 then return none - -- Swap rows k and pivot in both L and Inv (if different). - if pivot ≠ k then - for j in [:n] do - let tmpL := L.get k j - L := L.set k j (L.get pivot j) - L := L.set pivot j tmpL - let tmpI := Inv.get k j - Inv := Inv.set k j (Inv.get pivot j) - Inv := Inv.set pivot j tmpI - -- Normalise row k so L[k, k] = 1. - let piv := L.get k k - for j in [:n] do - L := L.set k j (L.get k j / piv) - Inv := Inv.set k j (Inv.get k j / piv) - -- Eliminate column k from every other row. - for r in [:n] do - if r ≠ k then - let factor := L.get r k - if factor != 0.0 then - for j in [:n] do - L := L.set r j (L.get r j - factor * L.get k j) - Inv := Inv.set r j (Inv.get r j - factor * Inv.get k j) - return some Inv - -/-- **Eng.** *Hypergraph clique-expanded conductance matrix*, as a -`FloatMat`. Same convention as `conductance`. -/ -def IncidenceData.conductanceF (H : IncidenceData) : FloatMat := Id.run do - let n := H.numVertices - let mut C := FloatMat.zero n - for e in H.edges do - let vs := e.inputs.push e.output - for i in [:vs.size] do - for j in [:vs.size] do - if i ≠ j then - let u := vs[i]! - let v := vs[j]! - C := C.set u v (C.get u v + 1.0) - return C - -/-- **Eng.** *Graph Laplacian* `L = D − C` as a `FloatMat`. -/ -def IncidenceData.laplacianF (H : IncidenceData) : FloatMat := Id.run do - let n := H.numVertices - let C := H.conductanceF - let mut L := FloatMat.zero n - for u in [:n] do - let mut rowSum := 0.0 - for v in [:n] do - let c := C.get u v - rowSum := rowSum + c - L := L.set u v (-c) - L := L.set u u rowSum - return L - -/-- **Eng.** *Effective resistance distance matrix*, returned as a -`FloatMat`. Computes `(L + εI)⁻¹` once via `gaussInvertF`, then -fills `R[u, v] = Linv[u, u] + Linv[v, v] − 2 Linv[u, v]`. Returns -the zero matrix on inversion failure. -/ -def IncidenceData.resistanceMatrixF (H : IncidenceData) : FloatMat := Id.run do - let n := H.numVertices - if n = 0 then return FloatMat.zero 0 - let mut Lreg := H.laplacianF - let ε := 1e-9 - for i in [:n] do - Lreg := Lreg.set i i (Lreg.get i i + ε) - match gaussInvertF Lreg with - | none => return FloatMat.zero n - | some Linv => - let mut R := FloatMat.zero n - for u in [:n] do - for v in [:n] do - let r := Linv.get u u + Linv.get v v - 2.0 * Linv.get u v - R := R.set u v r - return R - -end ProofAtlas.Pipeline diff --git a/lean/ProofAtlas/RandomWalk/CommuteTime.lean b/lean/ProofAtlas/RandomWalk/CommuteTime.lean deleted file mode 100644 index 3d84118..0000000 --- a/lean/ProofAtlas/RandomWalk/CommuteTime.lean +++ /dev/null @@ -1,449 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Util.Linter.MathTag -import ProofAtlas.RandomWalk.KemenySnell -import ProofAtlas.LinearAlgebra.EffectiveResistance -import ProofAtlas.LinearAlgebra.MoorePenrose - -/-! -# Abstract commute-time / effective-resistance bridge (Doyle-Snell) - -For a finite ergodic, **reversible** Markov chain with transition -matrix `P` and stationary distribution `π`, the sum of hitting times -`h(u, v) + h(v, u)` between two states admits a closed form in terms -of the effective resistance of the *conductance* Laplacian - - `L_C := D - C, where C_{u,v} := π_u · P_{u,v}`, - -with `D_{u,u} := Σ_w C_{u,w} - C_{u,u}` the off-diagonal row sum. - -The classical Doyle-Snell / Chandra-Raghavan-Ruzzo-Smolensky-Tiwari -identity is - - `h(u, v) + h(v, u) = (Σ_{x,y} C_{x,y}) · R_eff(L_C; u, v)`. - -This file abstracts away from the BDF random walk: `P` and `π` are -arbitrary, and the identity is a pure Markov-layer theorem on -finite reversible chains. The corresponding *non-reversible* BDF -chain is bridged to this identity in -`ProofAtlas.Metric.Resistance.Resistance.commuteTimeDist_eq_effectiveResistance` -through a separate reversibility-restoration argument (Doyle-Snell -Ch. 3 §3.2's symmetrisation). - -## Main definitions - -* `IsReversible P π` — `π_u · P_{u,v} = π_v · P_{v,u}` for all `u, v`. -* `conductance P π` — conductance matrix `C_{u,v} := π_u · P_{u,v}`. - Symmetric under `IsReversible`. -* `conductanceLaplacian P π` — `L_C := D - C` where - `D_{u,u} := Σ_w C_{u,w} - C_{u,u}` is the off-diagonal row-sum - diagonal; equivalently, the diagonal so that `L_C` has zero row - sums. - -## Sub-lemmas proved - -* `conductance_symm` — `C_{u,v} = C_{v,u}` for reversible chains. -* `conductanceLaplacian_symm` — `L_C` is a symmetric matrix when `P` - is reversible. -* `conductanceLaplacian_rowSum_zero` — `Σ_v L_C u v = 0`, the - standard zero-row-sum property of a graph Laplacian. - -## Main statement (TODO: final combination step) - -* `commuteTime_eq_effectiveResistance` — for reversible `P`, - `h(u, v) + h(v, u) = (Σ_{x,y} C_{x,y}) · R_eff(L_C; u, v)`. The - sub-lemmas above are fully proven; the final combination step is - left as a `sorry` to be filled in a follow-up round (see the - references). - -## References - -* Aldous, Fill, *Reversible Markov Chains and Random Walks on Graphs*, - Ch. 2 §2.2 ("Hitting times and electrical networks"). -* Doyle, Snell, *Random Walks and Electric Networks*, Ch. 3. -* Chandra, Raghavan, Ruzzo, Smolensky, Tiwari (1989), "The electrical - resistance of a graph captures its commute and cover times". --/ - -namespace ProofAtlas.RandomWalk - -open Matrix BigOperators Finset - -variable {n : Type*} [Fintype n] [DecidableEq n] - -/-- **Math.** Detailed-balance / reversibility for a stochastic matrix -`P` with stationary distribution `π`: `π_u · P_{u,v} = π_v · P_{v,u}` -for all states. Reversibility makes the conductance matrix -`C_{u,v} := π_u · P_{u,v}` symmetric and identifies the chain with -the random walk on a weighted undirected graph. -/ -def IsReversible (P : Matrix n n ℝ) (π : n → ℝ) : Prop := - ∀ u v, π u * P u v = π v * P v u - -/-- **Math.** Conductance matrix `C_{u,v} := π_u · P_{u,v}`. Symmetric -when the chain is reversible (`IsReversible P π`). -/ -noncomputable def conductance (P : Matrix n n ℝ) (π : n → ℝ) : - Matrix n n ℝ := fun u v => π u * P u v - -/-- **Math.** Conductance Laplacian `L_C := D - C` of a Markov chain. -The diagonal `L_{u,u} := Σ_w C_{u,w} - C_{u,u}` is the off-diagonal -row sum of `C`, chosen so that `L_C` has zero row sums: -`Σ_v L_{u,v} = 0` (see `conductanceLaplacian_rowSum_zero`). -Off-diagonal entries are `L_{u,v} := -C_{u,v}`. -/ -noncomputable def conductanceLaplacian (P : Matrix n n ℝ) (π : n → ℝ) : - Matrix n n ℝ := fun u v => - if u = v then ∑ w, conductance P π u w - conductance P π u u - else - conductance P π u v - -omit [Fintype n] [DecidableEq n] in -/-- **Math.** Symmetry of the conductance matrix under reversibility: -`C_{u,v} = C_{v,u}`. Immediate from the detailed-balance equation. -/ -lemma conductance_symm {P : Matrix n n ℝ} {π : n → ℝ} - (h_rev : IsReversible P π) (u v : n) : - conductance P π u v = conductance P π v u := h_rev u v - -/-- **Math.** Symmetry of the conductance Laplacian under reversibility: -`L_C u v = L_C v u`. On the diagonal it is trivial; off-diagonal it -reduces to symmetry of `C`. -/ -lemma conductanceLaplacian_symm {P : Matrix n n ℝ} {π : n → ℝ} - (h_rev : IsReversible P π) (u v : n) : - conductanceLaplacian P π u v = conductanceLaplacian P π v u := by - unfold conductanceLaplacian - by_cases h : u = v - · subst h; rfl - · have h' : v ≠ u := fun e => h e.symm - rw [if_neg h, if_neg h', conductance_symm h_rev] - -/-- **Math.** Each row of `L_C` sums to zero: `Σ_v L_C u v = 0`. -Standard Laplacian property — the diagonal is the off-diagonal row -sum so the total cancels. -/ -lemma conductanceLaplacian_rowSum_zero (P : Matrix n n ℝ) (π : n → ℝ) - (u : n) : - ∑ v, conductanceLaplacian P π u v = 0 := by - -- Split the sum into the v = u diagonal contribution and the off-diagonal rest. - rw [← Finset.add_sum_erase (Finset.univ : Finset n) - (conductanceLaplacian P π u) (Finset.mem_univ u)] - -- Off-diagonal terms collapse to `-C u v`. - have h_off : ∀ v ∈ (Finset.univ : Finset n).erase u, - conductanceLaplacian P π u v = - conductance P π u v := by - intros v hv - rw [Finset.mem_erase] at hv - have h_ne : ¬ u = v := fun e => hv.1 e.symm - simp [conductanceLaplacian, h_ne] - -- Diagonal term. - have h_diag : conductanceLaplacian P π u u - = ∑ w, conductance P π u w - conductance P π u u := by - simp [conductanceLaplacian] - rw [h_diag, Finset.sum_congr rfl h_off, Finset.sum_neg_distrib] - -- `∑_{v ≠ u} C u v = (∑_w C u w) - C u u` follows from `Finset.sum_erase_add`. - have h_sum_erase : ∑ v ∈ (Finset.univ : Finset n).erase u, conductance P π u v - = ∑ w, conductance P π u w - conductance P π u u := - eq_sub_of_add_eq (Finset.sum_erase_add (Finset.univ : Finset n) - (conductance P π u) (Finset.mem_univ u)) - rw [h_sum_erase] - ring - -/-- **Math.** Voltage-current identity. When `L` is a (real) Hermitian -matrix and `y` is a vector with `L · y = b`, the quadratic form -`bᵀ · L⁺ · b` equals the inner product `bᵀ · y`. In electrical-network -language: the energy dissipated equals the current times the voltage, -and we never need the explicit pseudoinverse. - -Proof: substitute `b = L · y` and use the first Moore-Penrose identity -`L · L⁺ · L = L` together with the bilinear-form symmetry of `L`. -/ -lemma dotProduct_pseudoinverse_of_mulVec_eq - {L : Matrix n n ℝ} (hL : L.IsHermitian) - {b y : n → ℝ} (h_eq : L.mulVec y = b) : - b ⬝ᵥ ProofAtlas.LinearAlgebra.pseudoinverse L *ᵥ b - = b ⬝ᵥ y := by - have h_mp : L * ProofAtlas.LinearAlgebra.pseudoinverse L * L = L := - ProofAtlas.LinearAlgebra.mul_pseudoinverse_mul_self hL - -- For real matrices `Lᴴ = Lᵀ`, so `IsHermitian` gives `Lᵀ = L`. - have hL_trans : Lᵀ = L := hL.eq - -- Bilinear-form symmetry: `a ⬝ᵥ L *ᵥ c = c ⬝ᵥ L *ᵥ a`. - have h_sym : ∀ a c : n → ℝ, a ⬝ᵥ L.mulVec c = c ⬝ᵥ L.mulVec a := by - intros a c - have := Matrix.dotProduct_transpose_mulVec L c a - rw [hL_trans] at this - exact this.symm - rw [← h_eq, Matrix.mulVec_mulVec] - -- (L *ᵥ y) ⬝ᵥ (L⁺ * L) *ᵥ y = (L *ᵥ y) ⬝ᵥ y - rw [dotProduct_comm (L.mulVec y) _, h_sym _ y] - rw [Matrix.mulVec_mulVec, ← Matrix.mul_assoc, h_mp] - exact dotProduct_comm y (L.mulVec y) - -/-- **Math.** Entry-wise identity `L_C u v = √π_u · ((I - Q) u v) · √π_v`, -where `Q` is the symmetrised transition matrix `Q_{u,v} = √π_u · P_{u,v} / √π_v`. - -For `u = v`: - `RHS = √π_u · (1 - Q u u) · √π_u = π_u - π_u · P u u` - `LHS = (∑_w π_u · P u w) - π_u · P u u = π_u - π_u · P u u` (via `h_row_sum`). - -For `u ≠ v`: - `RHS = √π_u · (-Q u v) · √π_v = -π_u · P u v = -C u v` - `LHS = -C u v`. -/ -lemma conductanceLaplacian_apply_eq_diagSqrt_one_sub_Q - {P : Matrix n n ℝ} {π : n → ℝ} {Q : Matrix n n ℝ} - (h_sim : ∀ u v, Q u v = Real.sqrt (π u) * P u v / Real.sqrt (π v)) - (h_pos : ∀ v, 0 < π v) - (h_row_sum : ∀ i, ∑ j, P i j = 1) (u v : n) : - conductanceLaplacian P π u v - = Real.sqrt (π u) * ((1 : Matrix n n ℝ) - Q) u v * Real.sqrt (π v) := by - have h_sqrt_ne : ∀ w, Real.sqrt (π w) ≠ 0 := - fun w => (Real.sqrt_pos.mpr (h_pos w)).ne' - have h_sqrt_sq : ∀ w, Real.sqrt (π w) * Real.sqrt (π w) = π w := - fun w => Real.mul_self_sqrt (h_pos w).le - -- Compute Q u v in canonical form. - have hQ_off : ∀ u v : n, u ≠ v → - Real.sqrt (π u) * ((1 : Matrix n n ℝ) - Q) u v * Real.sqrt (π v) - = - (π u * P u v) := by - intros a b hab - rw [Matrix.sub_apply, Matrix.one_apply_ne hab, zero_sub, h_sim a b] - have h_ne := h_sqrt_ne b - rw [show Real.sqrt (π a) * (- (Real.sqrt (π a) * P a b / Real.sqrt (π b))) * - Real.sqrt (π b) - = - (Real.sqrt (π a) * Real.sqrt (π a) * P a b * - (Real.sqrt (π b) / Real.sqrt (π b))) by ring, - div_self h_ne, mul_one, h_sqrt_sq] - have hQ_diag : ∀ u : n, - Real.sqrt (π u) * ((1 : Matrix n n ℝ) - Q) u u * Real.sqrt (π u) - = π u - π u * P u u := by - intro a - rw [Matrix.sub_apply, Matrix.one_apply_eq, h_sim a a] - have h_ne := h_sqrt_ne a - rw [show Real.sqrt (π a) * (1 - Real.sqrt (π a) * P a a / Real.sqrt (π a)) * - Real.sqrt (π a) - = Real.sqrt (π a) * Real.sqrt (π a) - - Real.sqrt (π a) * Real.sqrt (π a) * P a a * - (Real.sqrt (π a) / Real.sqrt (π a)) by ring, - div_self h_ne, mul_one, h_sqrt_sq] - unfold conductanceLaplacian conductance - by_cases huv : u = v - · subst huv - rw [if_pos rfl, hQ_diag, ← Finset.mul_sum, h_row_sum, mul_one] - · rw [if_neg huv, hQ_off u v huv] - -/-- **Eng.** Entry-wise form of the conductance Laplacian: -`L_C u v = π_u · ((1 - P) u v)`. The diagonal form -`∑_w π_u · P u w - π_u · P u u = π_u · (1 - P u u)` collapses to the -RHS via `h_row_sum`. -/ -lemma conductanceLaplacian_apply_eq {P : Matrix n n ℝ} {π : n → ℝ} - (h_row_sum : ∀ i, ∑ j, P i j = 1) (u v : n) : - conductanceLaplacian P π u v - = π u * ((1 : Matrix n n ℝ) - P) u v := by - unfold conductanceLaplacian conductance - by_cases huv : u = v - · subst huv - rw [if_pos rfl, ← Finset.mul_sum, h_row_sum] - rw [Matrix.sub_apply, Matrix.one_apply_eq] - ring - · rw [if_neg huv] - rw [Matrix.sub_apply, Matrix.one_apply_ne huv] - ring - -/-- **Eng.** `L_C` acts on a vector by `L_C · y = D_π · ((1 - P) · y)`, -entry-wise: `(L_C · y) w = π_w · ((1 - P) · y) w`. -/ -lemma conductanceLaplacian_mulVec_apply {P : Matrix n n ℝ} {π : n → ℝ} - (h_row_sum : ∀ i, ∑ j, P i j = 1) (y : n → ℝ) (w : n) : - (conductanceLaplacian P π).mulVec y w - = π w * ((1 : Matrix n n ℝ) - P).mulVec y w := by - simp only [Matrix.mulVec, dotProduct] - rw [Finset.mul_sum] - refine Finset.sum_congr rfl (fun j _ => ?_) - rw [conductanceLaplacian_apply_eq h_row_sum w j] - ring - -/-- **Math.** Kemeny solution vector: a candidate "potential" `y` -satisfying `L_C · y = e_u − e_v`, built from columns of the fundamental -matrix `Z`: - - `y w := Z_{w, u} / π_u − Z_{w, v} / π_v`. - -Equivalently: `y u = h(u, v)` and `y v = −h(v, u)` (verified in -`signedIndicator_dotProduct_kemenySolutionVec_eq`). -/ -noncomputable def kemenySolutionVec (P : Matrix n n ℝ) (π : n → ℝ) (u v : n) : - n → ℝ := - fun w => fundamentalMatrix P π w u / π u - fundamentalMatrix P π w v / π v - -/-- **Math.** The Kemeny solution vector is a *potential* for the -signed indicator under the conductance Laplacian: - - `L_C · (kemenySolutionVec u v) = e_u − e_v`. - -Proof. `L_C = D_π · (1 − P)` (`conductanceLaplacian_apply_eq`), so - - `(L_C · y) w = π_w · ((1 − P) · y) w`. - -With `y w := Z_{w,u}/π_u − Z_{w,v}/π_v`, linearity of `mulVec` and the -column identity `(1 − P) · (Z col_a) w = [w = a] − π_a` -(`one_sub_P_mulVec_Z_col`) give - - `((1 − P) · y) w = ([w = u] − π_u)/π_u − ([w = v] − π_v)/π_v - = [w = u]/π_u − [w = v]/π_v`, - -and multiplying by `π_w` collapses to `[w = u] − [w = v]`. -/ -lemma conductanceLaplacian_mulVec_kemenySolutionVec - {P : Matrix n n ℝ} {π : n → ℝ} - (h_row_sum : ∀ i, ∑ j, P i j = 1) - (h_stat : ∀ j, ∑ i, π i * P i j = π j) - (h_sum : ∑ i, π i = 1) - (h_pos : ∀ w, 0 < π w) - (h_inv : IsUnit (auxMatrix P π).det) - (u v : n) : - (conductanceLaplacian P π).mulVec (kemenySolutionVec P π u v) - = ProofAtlas.LinearAlgebra.signedIndicator u v := by - funext w - rw [conductanceLaplacian_mulVec_apply h_row_sum] - have h_pu_ne : (π u) ≠ 0 := (h_pos u).ne' - have h_pv_ne : (π v) ≠ 0 := (h_pos v).ne' - -- Decompose (1 - P).mulVec y by linearity. - have h_lin : ((1 : Matrix n n ℝ) - P).mulVec (kemenySolutionVec P π u v) w - = (((1 : Matrix n n ℝ) - P).mulVec - (fun w' => fundamentalMatrix P π w' u)) w / π u - - (((1 : Matrix n n ℝ) - P).mulVec - (fun w' => fundamentalMatrix P π w' v)) w / π v := by - simp only [Matrix.mulVec, dotProduct, kemenySolutionVec] - -- Distribute the multiplication inside the sum, then split via sum_sub_distrib, - -- finally factor out the /π via Finset.sum_div. - rw [show (∑ j, (1 - P : Matrix n n ℝ) w j * - (fundamentalMatrix P π j u / π u - fundamentalMatrix P π j v / π v)) - = ∑ j, ((1 - P : Matrix n n ℝ) w j * fundamentalMatrix P π j u / π u - - (1 - P : Matrix n n ℝ) w j * fundamentalMatrix P π j v / π v) by - refine Finset.sum_congr rfl (fun j _ => ?_); ring] - rw [Finset.sum_sub_distrib, ← Finset.sum_div, ← Finset.sum_div] - rw [h_lin] - rw [one_sub_P_mulVec_Z_col h_row_sum h_stat h_sum h_inv u w, - one_sub_P_mulVec_Z_col h_row_sum h_stat h_sum h_inv v w] - unfold ProofAtlas.LinearAlgebra.signedIndicator - -- Case split on `w = u`, `w = v` and discharge via ring/field arithmetic. - by_cases hwu : w = u - · by_cases hwv : w = v - · simp only [if_pos hwu, if_pos hwv, sub_self] - have : u = v := hwu.symm.trans hwv - subst this - ring - · simp only [if_pos hwu, if_neg hwv] - subst hwu - field_simp; ring - · by_cases hwv : w = v - · simp only [if_neg hwu, if_pos hwv] - subst hwv - field_simp; ring - · simp only [if_neg hwu, if_neg hwv] - field_simp; ring - -/-- **Math.** Reading the dot product `(e_u − e_v) ⬝ kemenySolutionVec` -in two ways. Direct evaluation: only `w ∈ {u, v}` contributes; the -result equals - - `kemenySolutionVec u − kemenySolutionVec v - = (Z_{u,u} − Z_{v,u})/π_u + (Z_{v,v} − Z_{u,v})/π_v - = h(v, u) + h(u, v)` - -after applying `kemenySnell_hitting_apply` at both endpoints. -/ -lemma signedIndicator_dotProduct_kemenySolutionVec - {P : Matrix n n ℝ} {π : n → ℝ} (u v : n) : - ProofAtlas.LinearAlgebra.signedIndicator u v - ⬝ᵥ kemenySolutionVec P π u v - = (fundamentalMatrix P π v v - fundamentalMatrix P π u v) / π v - + (fundamentalMatrix P π u u - fundamentalMatrix P π v u) / π u := by - unfold ProofAtlas.LinearAlgebra.signedIndicator kemenySolutionVec dotProduct - -- Distribute the multiplication: indicator * (Z u / π u - Z v / π v). - rw [show (fun w => ((if w = u then (1 : ℝ) else 0) - (if w = v then (1 : ℝ) else 0)) * - (fundamentalMatrix P π w u / π u - fundamentalMatrix P π w v / π v)) - = (fun w => (if w = u then - (fundamentalMatrix P π w u / π u - fundamentalMatrix P π w v / π v) - else 0) - - (if w = v then - (fundamentalMatrix P π w u / π u - fundamentalMatrix P π w v / π v) - else 0)) by - funext w; split_ifs <;> ring] - rw [Finset.sum_sub_distrib] - rw [Finset.sum_ite_eq' Finset.univ u - (fun w => fundamentalMatrix P π w u / π u - fundamentalMatrix P π w v / π v)] - rw [Finset.sum_ite_eq' Finset.univ v - (fun w => fundamentalMatrix P π w u / π u - fundamentalMatrix P π w v / π v)] - simp only [Finset.mem_univ, if_true] - ring - -omit [DecidableEq n] in -/-- **Math.** Total conductance sums to 1 on a stochastic chain with -unit-mass stationary distribution: -`∑_{x, y} C_{x, y} = ∑_x π_x · (∑_y P_{x, y}) = ∑_x π_x = 1`. -/ -lemma sum_conductance_eq_one {P : Matrix n n ℝ} {π : n → ℝ} - (h_row_sum : ∀ i, ∑ j, P i j = 1) (h_sum : ∑ i, π i = 1) : - ∑ x, ∑ y, conductance P π x y = 1 := by - have h_inner : ∀ x, ∑ y, conductance P π x y = π x := by - intro x - simp only [conductance, ← Finset.mul_sum, h_row_sum, mul_one] - simp_rw [h_inner] - exact h_sum - -/-- **Math.** Abstract Doyle-Snell commute-time formula. For a finite -ergodic, **reversible** Markov chain on `n`, the sum of hitting -times `h(u, v) + h(v, u)` equals the total conductance times the -effective resistance of the conductance Laplacian: - - `h(u, v) + h(v, u) = (Σ_{x,y} C_{x,y}) · R_eff(L_C; u, v)`. - -Since `Σ C = 1` (`sum_conductance_eq_one`), the equation reduces to -`h(u, v) + h(v, u) = R_eff(L_C; u, v)`. - -**Proof.** Three pieces: - -* The Kemeny solution vector `y_w := Z_{w,u}/π_u − Z_{w,v}/π_v` - satisfies `L_C · y = e_u − e_v` (`conductanceLaplacian_mulVec_kemenySolutionVec`). -* Therefore `(e_u − e_v) ⬝ L_C⁺ · (e_u − e_v) = (e_u − e_v) ⬝ y` - (`dotProduct_pseudoinverse_of_mulVec_eq` applied to the Hermitian - Laplacian `L_C` and `b := e_u − e_v`). -* `(e_u − e_v) ⬝ y = (Z_{v,v} − Z_{u,v})/π_v + (Z_{u,u} − Z_{v,u})/π_u` - (`signedIndicator_dotProduct_kemenySolutionVec`), and the two terms - identify with the hitting times via `kemenySnell_hitting_apply`. -/ -theorem commuteTime_eq_effectiveResistance - {P : Matrix n n ℝ} {π : n → ℝ} - (h_row_sum : ∀ i, ∑ j, P i j = 1) - (h_stat : ∀ j, ∑ i, π i * P i j = π j) - (h_sum : ∑ i, π i = 1) - (h_pos : ∀ w, 0 < π w) - (h_inv : IsUnit (auxMatrix P π).det) - (h_rev : IsReversible P π) - {v : n} - (h_killed_inv_v : IsUnit ((1 : Matrix n n ℝ) - killedMatrix P v)) - {u : n} - (h_killed_inv_u : IsUnit ((1 : Matrix n n ℝ) - killedMatrix P u)) : - (((1 : Matrix n n ℝ) - killedMatrix P v)⁻¹.mulVec (killedRhs v)) u - + (((1 : Matrix n n ℝ) - killedMatrix P u)⁻¹.mulVec (killedRhs u)) v - = (∑ x, ∑ y, conductance P π x y) - * ProofAtlas.LinearAlgebra.effectiveResistance - (conductanceLaplacian P π) u v := by - -- Rewrite both hitting times via Kemeny-Snell. - rw [kemenySnell_hitting_apply h_row_sum h_stat h_sum h_pos h_inv h_killed_inv_v u, - kemenySnell_hitting_apply h_row_sum h_stat h_sum h_pos h_inv h_killed_inv_u v] - -- Collapse Σ C = 1. - rw [sum_conductance_eq_one h_row_sum h_sum, one_mul] - -- Goal: (Z_{vv} − Z_{uv})/π_v + (Z_{uu} − Z_{vu})/π_u - -- = R_eff(L_C; u, v) - -- Match via signedIndicator_dotProduct_kemenySolutionVec. - rw [← signedIndicator_dotProduct_kemenySolutionVec (P := P) (π := π) u v] - -- Goal: (e_u − e_v) ⬝ y = R_eff(L_C; u, v) - -- Unfold effectiveResistance and use voltage-current identity. - unfold ProofAtlas.LinearAlgebra.effectiveResistance - -- Goal: (e_u − e_v) ⬝ y = ∑ w, (e_u − e_v) w * (L_C⁺ · (e_u − e_v)) w - -- RHS is exactly dotProduct ... by definition. - change ProofAtlas.LinearAlgebra.signedIndicator u v ⬝ᵥ kemenySolutionVec P π u v - = ProofAtlas.LinearAlgebra.signedIndicator u v ⬝ᵥ - (ProofAtlas.LinearAlgebra.pseudoinverse (conductanceLaplacian P π)).mulVec - (ProofAtlas.LinearAlgebra.signedIndicator u v) - -- Apply the voltage-current identity. - have hLC_herm : (conductanceLaplacian P π).IsHermitian := by - ext a b - change star (conductanceLaplacian P π b a) = conductanceLaplacian P π a b - rw [show star (conductanceLaplacian P π b a) = conductanceLaplacian P π b a from rfl, - conductanceLaplacian_symm h_rev b a] - have h_solve := conductanceLaplacian_mulVec_kemenySolutionVec - h_row_sum h_stat h_sum h_pos h_inv u v - exact (dotProduct_pseudoinverse_of_mulVec_eq hLC_herm h_solve).symm - -end ProofAtlas.RandomWalk diff --git a/lean/ProofAtlas/RandomWalk/FundamentalMatrix.lean b/lean/ProofAtlas/RandomWalk/FundamentalMatrix.lean deleted file mode 100644 index 2b1f768..0000000 --- a/lean/ProofAtlas/RandomWalk/FundamentalMatrix.lean +++ /dev/null @@ -1,918 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.Util.Linter.MathTag -import Mathlib.Data.Real.Basic -import Mathlib.Data.Matrix.Basic -import Mathlib.LinearAlgebra.Matrix.NonsingularInverse -import Mathlib.Analysis.Matrix.Normed -import Mathlib.Analysis.Matrix.Spectrum -import Mathlib.Analysis.SpecificLimits.Normed -import Mathlib.Algebra.Ring.GeomSum -import Mathlib.Tactic.Abel - -/-! -# Fundamental matrix of a finite Markov chain - -For a finite Markov chain with stochastic transition matrix `P` and -stationary distribution `π`, the **fundamental matrix** is - - `Z := (I - P + W)⁻¹ - W` - -where `W = 𝟙π^T` is the rank-1 stationary projection. Equivalently, -`Z = ∑_{k ≥ 0}(P^k - W)`; the two definitions agree on any ergodic -chain (Aldous-Fill, "Reversible Markov Chains and Random Walks on -Graphs", §2.3; Kemeny-Snell, "Finite Markov Chains", Theorem 4.4.4). - -The fundamental matrix is the right-hand side of the **Kemeny-Snell -hitting-time identity** `h(u, v) = (Z_{v,v} - Z_{u,v}) / π_v`, and is -used to derive the spectral expansion of the interaction term `I(u, v)` -in paper Theorem 6.4. - -This file defines `fundamentalMatrix` and proves its basic -orthogonality properties: - -* `aux_mul_stationaryProjection`: `(I - P + W) · W = W` -* `stationaryProjection_mul_aux`: `W · (I - P + W) = W` -* `fundamentalMatrix_mul_stationaryProjection`: `Z · W = 0` - (under invertibility of `I - P + W`) -* `stationaryProjection_mul_fundamentalMatrix`: `W · Z = 0` - (under invertibility of `I - P + W`) - -The invertibility of `I - P + W` on an ergodic chain is a separate -result, proved via the spectral gap or the convergence of `P^k → W`; -it is left as a hypothesis on the orthogonality lemmas in this file. - -## Main definitions - -* `ProofAtlas.RandomWalk.stationaryProjection π : Matrix n n ℝ` — - the rank-1 matrix `W_{i,j} := π_j`. -* `ProofAtlas.RandomWalk.auxMatrix P π : Matrix n n ℝ` — - the Kemeny-Snell auxiliary matrix `I - P + W`. -* `ProofAtlas.RandomWalk.fundamentalMatrix P π : Matrix n n ℝ` — - the fundamental matrix `Z := (auxMatrix P π)⁻¹ - W`. --/ - -namespace ProofAtlas.RandomWalk - -open Matrix BigOperators Finset - -variable {n : Type*} [Fintype n] [DecidableEq n] - -section Defs - -variable (P : Matrix n n ℝ) (π : n → ℝ) - -/-- **Eng.** Rank-1 stationary projection `W_{i,j} := π_j`, often -written `𝟙π^T`. Bridge between a probability vector `π : n → ℝ` and -the matrix it generates as a constant-row pattern. -/ -def stationaryProjection : Matrix n n ℝ := - fun _ j => π j - -/-- **Eng.** Kemeny-Snell auxiliary matrix `I - P + W`. Invertible on -any ergodic (irreducible + aperiodic) finite chain. The fundamental -matrix is its inverse minus `W`. -/ -def auxMatrix : Matrix n n ℝ := - 1 - P + stationaryProjection π - -/-- **Math.** Fundamental matrix `Z := (I - P + W)⁻¹ - W`. On any -ergodic finite chain this equals `∑_{k ≥ 0}(P^k - W)` (Kemeny-Snell, -Aldous-Fill §2.3); the Neumann-series form is the standard -construction, but the closed form `(I - P + W)⁻¹ - W` avoids -introducing the infinite sum. - -Defined via Mathlib's `Matrix.inv`, which returns `0` on non-invertible -inputs; the meaningful identities below assume invertibility of -`auxMatrix P π`. -/ -noncomputable def fundamentalMatrix : Matrix n n ℝ := - (auxMatrix P π)⁻¹ - stationaryProjection π - -end Defs - -/-! ### Basic identities for the stationary projection and auxiliary matrix -/ - -variable {P : Matrix n n ℝ} {π : n → ℝ} - -omit [DecidableEq n] in -/-- **Math.** Row-stochasticity gives `P · W = W`. The `j`-th column -of `W` is the constant vector `π j`; right-multiplication by `P` -preserves it because each row of `P` sums to `1`. -/ -lemma transMatrix_mul_stationaryProjection - (h_row_sum : ∀ i, ∑ j, P i j = 1) : - P * stationaryProjection π = stationaryProjection π := by - ext i j - simp only [Matrix.mul_apply, stationaryProjection] - calc ∑ k, P i k * π j - = (∑ k, P i k) * π j := by rw [← Finset.sum_mul] - _ = 1 * π j := by rw [h_row_sum i] - _ = π j := one_mul _ - -omit [DecidableEq n] in -/-- **Math.** Stationarity gives `W · P = W`. The `i`-th row of `W` is -`π`; left-multiplying by `P^T` (i.e. right-multiplying by `P`) sends -`π` to itself by the stationary equation. -/ -lemma stationaryProjection_mul_transMatrix - (h_stat : ∀ j, ∑ i, π i * P i j = π j) : - stationaryProjection π * P = stationaryProjection π := by - ext i j - simp only [Matrix.mul_apply, stationaryProjection] - exact h_stat j - -omit [DecidableEq n] in -/-- **Math.** `W · W = W`. The product is a constant-row matrix with -row `π · (∑ i, π i) = π · 1 = π`. -/ -lemma stationaryProjection_mul_self - (h_sum : ∑ i, π i = 1) : - stationaryProjection π * stationaryProjection π - = stationaryProjection π := by - ext i j - simp only [Matrix.mul_apply, stationaryProjection] - calc ∑ k, π k * π j - = (∑ k, π k) * π j := by rw [← Finset.sum_mul] - _ = 1 * π j := by rw [h_sum] - _ = π j := one_mul _ - -/-- **Math.** `(I - P + W) · W = W`. Combines `P · W = W` (row- -stochasticity) with `W · W = W` (probability sum). -/ -lemma auxMatrix_mul_stationaryProjection - (h_row_sum : ∀ i, ∑ j, P i j = 1) - (h_sum : ∑ i, π i = 1) : - auxMatrix P π * stationaryProjection π = stationaryProjection π := by - unfold auxMatrix - rw [add_mul, sub_mul, one_mul, - transMatrix_mul_stationaryProjection h_row_sum, - stationaryProjection_mul_self h_sum] - abel - -/-- **Math.** `W · (I - P + W) = W`. Combines `W · P = W` (stationarity) -with `W · W = W` (probability sum). -/ -lemma stationaryProjection_mul_auxMatrix - (h_stat : ∀ j, ∑ i, π i * P i j = π j) - (h_sum : ∑ i, π i = 1) : - stationaryProjection π * auxMatrix P π = stationaryProjection π := by - unfold auxMatrix - rw [mul_add, mul_sub, mul_one, - stationaryProjection_mul_transMatrix h_stat, - stationaryProjection_mul_self h_sum] - abel - -/-! ### Orthogonality of the fundamental matrix and `W` -/ - -/-- **Math.** Under invertibility of `auxMatrix`, the fundamental -matrix annihilates `W` on the right: `Z · W = 0`. Follows from -`(I - P + W) · W = W`, which after multiplying by `(I - P + W)⁻¹` -on the left gives `(I - P + W)⁻¹ · W = W`. Hence `Z · W = W - W = 0`. -/ -lemma fundamentalMatrix_mul_stationaryProjection - (h_row_sum : ∀ i, ∑ j, P i j = 1) - (h_sum : ∑ i, π i = 1) - (h_inv : IsUnit (auxMatrix P π).det) : - fundamentalMatrix P π * stationaryProjection π = 0 := by - unfold fundamentalMatrix - have h_aux : auxMatrix P π * stationaryProjection π = stationaryProjection π := - auxMatrix_mul_stationaryProjection h_row_sum h_sum - -- From `aux · W = W`, get `W = aux⁻¹ · W` by left-multiplying by aux⁻¹. - have h_inv_W : (auxMatrix P π)⁻¹ * stationaryProjection π - = stationaryProjection π := by - have h := congr_arg ((auxMatrix P π)⁻¹ * ·) h_aux - simp only at h - rw [← Matrix.mul_assoc, Matrix.nonsing_inv_mul _ h_inv, Matrix.one_mul] at h - exact h.symm - rw [sub_mul, h_inv_W, stationaryProjection_mul_self h_sum, sub_self] - -/-- **Math.** Under invertibility of `auxMatrix`, the fundamental -matrix is annihilated by `W` on the left: `W · Z = 0`. Symmetric to -the right-orthogonality. -/ -lemma stationaryProjection_mul_fundamentalMatrix - (h_stat : ∀ j, ∑ i, π i * P i j = π j) - (h_sum : ∑ i, π i = 1) - (h_inv : IsUnit (auxMatrix P π).det) : - stationaryProjection π * fundamentalMatrix P π = 0 := by - unfold fundamentalMatrix - have h_aux : stationaryProjection π * auxMatrix P π = stationaryProjection π := - stationaryProjection_mul_auxMatrix h_stat h_sum - -- From `W · aux = W`, get `W = W · aux⁻¹` by right-multiplying by aux⁻¹. - have h_W_inv : stationaryProjection π * (auxMatrix P π)⁻¹ - = stationaryProjection π := by - have h := congr_arg (· * (auxMatrix P π)⁻¹) h_aux - simp only at h - rw [Matrix.mul_assoc, Matrix.mul_nonsing_inv _ h_inv, Matrix.mul_one] at h - exact h.symm - rw [mul_sub, h_W_inv, stationaryProjection_mul_self h_sum, sub_self] - -/-- **Math.** `(I - P) · Z = I - W`. The algebraic identity underlying -the Kemeny-Snell hitting-time bridge: combining this with the -killed-transition recursion and invertibility of `I - Q_v` yields -`h(u, v) = (Z_{v,v} - Z_{u,v}) / π_v`. - -Proof. Decompose `I - P = auxMatrix - W`; then -`(I - P) · Z = auxMatrix · Z - W · Z`. The first product is -`auxMatrix · (auxMatrix⁻¹ - W) = I - auxMatrix · W = I - W` (using -`auxMatrix_mul_stationaryProjection`), and the second is `0` by -`stationaryProjection_mul_fundamentalMatrix`. -/ -lemma one_sub_transMatrix_mul_fundamentalMatrix - (h_row_sum : ∀ i, ∑ j, P i j = 1) - (h_stat : ∀ j, ∑ i, π i * P i j = π j) - (h_sum : ∑ i, π i = 1) - (h_inv : IsUnit (auxMatrix P π).det) : - (1 - P) * fundamentalMatrix P π = 1 - stationaryProjection π := by - have h_W_Z : stationaryProjection π * fundamentalMatrix P π = 0 := - stationaryProjection_mul_fundamentalMatrix h_stat h_sum h_inv - have h_aux_W : auxMatrix P π * stationaryProjection π = stationaryProjection π := - auxMatrix_mul_stationaryProjection h_row_sum h_sum - have h_aux_Z : auxMatrix P π * fundamentalMatrix P π - = 1 - stationaryProjection π := by - unfold fundamentalMatrix - rw [Matrix.mul_sub, Matrix.mul_nonsing_inv _ h_inv, h_aux_W] - have h_aux_eq : (1 - P : Matrix n n ℝ) = auxMatrix P π - stationaryProjection π := by - unfold auxMatrix; abel - rw [h_aux_eq, Matrix.sub_mul, h_aux_Z, h_W_Z, sub_zero] - -/-! ### Powers of `P - W` and convergence -/ - -/-- **Math.** `P^k · W = W` for every `k ≥ 0`, given `P · W = W`. Used -to telescope `(P - W)^k` into `P^k - W`. -/ -lemma transMatrix_pow_mul_stationaryProjection - (h_row_sum : ∀ i, ∑ j, P i j = 1) : - ∀ k : ℕ, P ^ k * stationaryProjection π = stationaryProjection π := by - intro k - induction k with - | zero => simp - | succ k ih => - calc P ^ (k + 1) * stationaryProjection π - = P ^ k * P * stationaryProjection π := by rw [pow_succ] - _ = P ^ k * (P * stationaryProjection π) := by rw [Matrix.mul_assoc] - _ = P ^ k * stationaryProjection π := by - rw [transMatrix_mul_stationaryProjection h_row_sum] - _ = stationaryProjection π := ih - -/-- **Math.** With `M := P - W` and `P^k · W = W = W · P^k = W · W`, -the identity `M^k = P^k - W` holds for every `k ≥ 1`. The `k = 0` -case fails since `M^0 = I` while `P^0 - W = I - W`. -/ -lemma sub_stationaryProjection_pow_eq - (h_row_sum : ∀ i, ∑ j, P i j = 1) - (h_stat : ∀ j, ∑ i, π i * P i j = π j) - (h_sum : ∑ i, π i = 1) : - ∀ k : ℕ, (P - stationaryProjection π) ^ (k + 1) - = P ^ (k + 1) - stationaryProjection π := by - intro k - induction k with - | zero => - simp [pow_one] - | succ k ih => - calc (P - stationaryProjection π) ^ (k + 1 + 1) - = (P - stationaryProjection π) ^ (k + 1) * (P - stationaryProjection π) := by - rw [pow_succ] - _ = (P ^ (k + 1) - stationaryProjection π) * (P - stationaryProjection π) := by - rw [ih] - _ = P ^ (k + 1) * P - P ^ (k + 1) * stationaryProjection π - - stationaryProjection π * P - + stationaryProjection π * stationaryProjection π := by - rw [Matrix.sub_mul, Matrix.mul_sub, Matrix.mul_sub] - abel - _ = P ^ (k + 1 + 1) - stationaryProjection π - - stationaryProjection π + stationaryProjection π := by - rw [transMatrix_pow_mul_stationaryProjection h_row_sum, - stationaryProjection_mul_transMatrix h_stat, - stationaryProjection_mul_self h_sum, ← pow_succ] - _ = P ^ (k + 1 + 1) - stationaryProjection π := by abel - -/-! ### Invertibility of the auxiliary matrix on an ergodic chain -/ - -open Matrix.Norms.Operator in -/-- **Math.** On any ergodic finite chain — formalised here as the -hypotheses `h_row_sum`, `h_stat`, `h_sum`, and entrywise convergence -`P^k → W` — the Kemeny-Snell auxiliary matrix `I - P + W` is -invertible. Proof: set `M := P - W`; from `M^(k+1) = P^(k+1) - W` -(`sub_stationaryProjection_pow_eq`) and `P^k → W`, deduce -`Tendsto (M^(k+1)) atTop (𝓝 0)`. Hence `∃ k, ‖M^(k+1)‖ < 1`, which -via the Neumann factorisation `(1 - M)·∑ M^i = 1 - M^(k+1)` and -`isUnit_one_sub_of_norm_lt_one` shows `1 - M = aux` is a unit. -/ -theorem auxMatrix_isUnit_det - (h_row_sum : ∀ i, ∑ j, P i j = 1) - (h_stat : ∀ j, ∑ i, π i * P i j = π j) - (h_sum : ∑ i, π i = 1) - (h_conv : - Filter.Tendsto (fun k => P ^ k) Filter.atTop - (nhds (stationaryProjection π))) : - IsUnit (auxMatrix P π).det := by - set W : Matrix n n ℝ := stationaryProjection π with hW_def - set M : Matrix n n ℝ := P - W with hM_def - -- Step (a): rewrite aux = 1 - M - have h_aux_eq : auxMatrix P π = 1 - M := by - simp only [auxMatrix, hM_def, hW_def]; abel - rw [h_aux_eq, ← Matrix.isUnit_iff_isUnit_det] - -- Step (b): P^k - W → 0 - have h_conv_zero : Filter.Tendsto (fun k => P ^ k - W) Filter.atTop (nhds 0) := by - have h := h_conv.sub (tendsto_const_nhds (x := W)) - simpa using h - -- Step (c): M^(k+1) → 0 by reindexing - have h_pow_eq : ∀ k, M ^ (k + 1) = P ^ (k + 1) - W := - sub_stationaryProjection_pow_eq h_row_sum h_stat h_sum - have h_pow_tendsto : Filter.Tendsto (fun k => M ^ (k + 1)) Filter.atTop (nhds 0) := by - have heq : (fun k => M ^ (k + 1)) = (fun k => P ^ (k + 1) - W) := by - funext k; exact h_pow_eq k - rw [heq] - exact (Filter.tendsto_add_atTop_iff_nat 1).mpr h_conv_zero - -- Step (d): extract `∃ k, ‖M^(k+1)‖ < 1` via eventual membership in `ball 0 1` - obtain ⟨k, hk⟩ : ∃ k, ‖M ^ (k + 1)‖ < 1 := by - have h_ball := h_pow_tendsto.eventually - (Metric.ball_mem_nhds (0 : Matrix n n ℝ) zero_lt_one) - have h_eventually : ∀ᶠ k in Filter.atTop, ‖M ^ (k + 1)‖ < 1 := by - filter_upwards [h_ball] with k hk - rwa [dist_zero_right] at hk - exact h_eventually.exists - -- Step (e): Neumann factorisation lifts `‖M^(k+1)‖ < 1` to `IsUnit (1 - M)` - have hPow : IsUnit (1 - M ^ (k + 1)) := isUnit_one_sub_of_norm_lt_one hk - have hFactor : - (1 - M) * (∑ i ∈ Finset.range (k + 1), M ^ i) = 1 - M ^ (k + 1) := - mul_neg_geom_sum M (k + 1) - rw [← hFactor] at hPow - exact isUnit_of_mul_isUnit_left hPow - -/-! ### Partial Neumann sum identity - -Step 3a of the path to Theorem 6.4: the fundamental matrix arises as -the limit of partial Neumann sums `∑_{k M ^ N) Filter.atTop (nhds 0)) : - Filter.Tendsto (fun N => ∑ k ∈ Finset.range N, M ^ k) Filter.atTop - (nhds (1 - M)⁻¹) := by - have h_repr : ∀ N : ℕ, - (∑ k ∈ Finset.range N, M ^ k) = (1 - M)⁻¹ - (1 - M)⁻¹ * M ^ N := by - intro N - have h := inv_one_sub_eq_partial_geom_sum_add_tail M h_inv N - exact eq_sub_of_add_eq h.symm - -- (1 - M)⁻¹ * M^N → (1 - M)⁻¹ * 0 = 0 by continuity of right-multiplication - have h_tail : Filter.Tendsto (fun N => (1 - M)⁻¹ * M ^ N) Filter.atTop (nhds 0) := by - have h := h_pow_zero.const_mul (1 - M)⁻¹ - simpa using h - -- ∑ k (1 - M)⁻¹ - (1 - M)⁻¹ * M ^ N) Filter.atTop (nhds ((1 - M)⁻¹ - 0)) := - (tendsto_const_nhds).sub h_tail - simp only [sub_zero] at h_diff - -- Rewrite the function to match h_repr - have h_funext : (fun N => ∑ k ∈ Finset.range N, M ^ k) - = (fun N => (1 - M)⁻¹ - (1 - M)⁻¹ * M ^ N) := by - funext N; exact h_repr N - rw [h_funext] - exact h_diff - -/-- **Math.** Step 3a target. The fundamental matrix `Z` is the limit -of the partial Neumann sums `∑_{k P ^ k) Filter.atTop - (nhds (stationaryProjection π))) : - Filter.Tendsto - (fun N => ∑ k ∈ Finset.range N, (P ^ k - stationaryProjection π)) - Filter.atTop - (nhds (fundamentalMatrix P π)) := by - set W : Matrix n n ℝ := stationaryProjection π with hW_def - set M : Matrix n n ℝ := P - W with hM_def - have h_aux_eq : auxMatrix P π = 1 - M := by - simp only [auxMatrix, hM_def, hW_def]; abel - -- aux invertible - have h_aux_inv : IsUnit (auxMatrix P π).det := - auxMatrix_isUnit_det h_row_sum h_stat h_sum h_conv - have h_inv : IsUnit (1 - M : Matrix n n ℝ).det := by - rw [← h_aux_eq]; exact h_aux_inv - -- M^N → 0 - have h_pow_eq : ∀ k, M ^ (k + 1) = P ^ (k + 1) - W := - sub_stationaryProjection_pow_eq h_row_sum h_stat h_sum - have h_conv_zero : Filter.Tendsto (fun k => P ^ k - W) Filter.atTop (nhds 0) := by - have h := h_conv.sub (tendsto_const_nhds (x := W)) - simpa using h - have h_pow_succ_zero : Filter.Tendsto (fun N => M ^ (N + 1)) Filter.atTop (nhds 0) := by - have heq : (fun N => M ^ (N + 1)) = (fun N => P ^ (N + 1) - W) := by - funext N; exact h_pow_eq N - rw [heq] - exact (Filter.tendsto_add_atTop_iff_nat 1).mpr h_conv_zero - -- Convert (N+1)-version to N-version for the partial sum lemma - have h_pow_zero : Filter.Tendsto (fun N => M ^ N) Filter.atTop (nhds 0) := by - rw [← Filter.tendsto_add_atTop_iff_nat 1] - exact h_pow_succ_zero - -- Partial sum of M^k tends to (1 - M)⁻¹ - have h_partial : Filter.Tendsto (fun N => ∑ k ∈ Finset.range N, M ^ k) Filter.atTop - (nhds (1 - M)⁻¹) := - partial_geom_sum_tendsto_inv_one_sub M h_inv h_pow_zero - -- Relate ∑(P^k - W) to ∑ M^k via M^k = P^k - W (k ≥ 1) and W - -- Claim: ∑_{k simp - | succ N ih => - rw [Finset.sum_range_succ, Finset.sum_range_succ, ih] - by_cases hN : 0 < N - · simp only [if_pos hN, if_pos (Nat.succ_pos _)] - have hM_N : M ^ N = P ^ N - W := by - obtain ⟨m, rfl⟩ := Nat.exists_eq_succ_of_ne_zero (Nat.pos_iff_ne_zero.mp hN) - exact h_pow_eq m - rw [hM_N]; abel - · have hN0 : N = 0 := Nat.eq_zero_of_not_pos hN - subst hN0 - simp - -- Take N from 1 upward so the `if 0 < N` branch is always W - have h_eventually : ∀ᶠ N in Filter.atTop, - (∑ k ∈ Finset.range N, M ^ k) - W - = ∑ k ∈ Finset.range N, (P ^ k - W) := by - rw [Filter.eventually_atTop] - refine ⟨1, fun N hN => ?_⟩ - have hN_pos : 0 < N := hN - rw [h_sum_diff N, if_pos hN_pos] - -- Use Tendsto.congr' to swap the function under the limit - have h_target : Filter.Tendsto (fun N => (∑ k ∈ Finset.range N, M ^ k) - W) - Filter.atTop (nhds ((1 - M)⁻¹ - W)) := - h_partial.sub (tendsto_const_nhds (x := W)) - -- fundamentalMatrix = (aux)⁻¹ - W = (1 - M)⁻¹ - W - have h_fund : fundamentalMatrix P π = (1 - M)⁻¹ - W := by - unfold fundamentalMatrix - rw [← h_aux_eq] - rw [h_fund] - exact h_target.congr' h_eventually - -/-! ### Step 3b: spectral entry-formula for `P^k` - -We bridge the BDF transition matrix `P` (not symmetric in general) to -the symmetrised matrix `Q = D^{1/2} P D^{-1/2}` (Hermitian under -reversibility, by `symmetrisedTransMatrix_isHermitian`). Mathlib's -`Matrix.IsHermitian.spectral_theorem` then gives an entry-form -expansion for `Q^k`, which propagates back to `P^k` via the -similarity `P^k = D^{-1/2} Q^k D^{1/2}`. - -The argument requires `π` to be strictly positive so that `D^{1/2}` -and `D^{-1/2}` are bona fide inverse pairs. -/ - -/-- **Eng.** Diagonal matrix `D^{1/2}` with entries `√π`. -/ -noncomputable def diagSqrt (π : n → ℝ) : Matrix n n ℝ := - Matrix.diagonal (fun v => Real.sqrt (π v)) - -/-- **Eng.** Diagonal matrix `D^{-1/2}` with entries `1 / √π`. -/ -noncomputable def diagSqrtInv (π : n → ℝ) : Matrix n n ℝ := - Matrix.diagonal (fun v => 1 / Real.sqrt (π v)) - -/-- **Math.** `D^{-1/2} · D^{1/2} = I` when `π` is strictly positive. -/ -lemma diagSqrtInv_mul_diagSqrt (h_pos : ∀ v, 0 < π v) : - diagSqrtInv π * diagSqrt π = 1 := by - unfold diagSqrtInv diagSqrt - rw [Matrix.diagonal_mul_diagonal] - rw [show (fun i => 1 / Real.sqrt (π i) * Real.sqrt (π i)) = fun _ : n => (1 : ℝ) by - funext i - have h : Real.sqrt (π i) ≠ 0 := (Real.sqrt_pos.mpr (h_pos i)).ne' - field_simp] - exact Matrix.diagonal_one - -/-- **Math.** `D^{1/2} · D^{-1/2} = I` when `π` is strictly positive. -/ -lemma diagSqrt_mul_diagSqrtInv (h_pos : ∀ v, 0 < π v) : - diagSqrt π * diagSqrtInv π = 1 := by - unfold diagSqrtInv diagSqrt - rw [Matrix.diagonal_mul_diagonal] - rw [show (fun i => Real.sqrt (π i) * (1 / Real.sqrt (π i))) = fun _ : n => (1 : ℝ) by - funext i - have h : Real.sqrt (π i) ≠ 0 := (Real.sqrt_pos.mpr (h_pos i)).ne' - field_simp] - exact Matrix.diagonal_one - -/-- **Math.** Step 3b-ii: `P` is similar to `Q := D^{1/2} P D^{-1/2}` -via `P = D^{-1/2} Q D^{1/2}`. Symbolic content of the symmetrisation. -/ -theorem transMatrix_eq_similarity_of_Q - {Q : Matrix n n ℝ} - (h_sim : ∀ u v, Q u v = Real.sqrt (π u) * P u v / Real.sqrt (π v)) - (h_pos : ∀ v, 0 < π v) : - P = diagSqrtInv π * Q * diagSqrt π := by - ext u v - -- (D^{-1/2} · Q · D^{1/2})_{u,v} = (1/√π_u) · Q_{u,v} · √π_v - rw [diagSqrt, Matrix.mul_diagonal, diagSqrtInv, Matrix.diagonal_mul, h_sim] - have hu : Real.sqrt (π u) ≠ 0 := (Real.sqrt_pos.mpr (h_pos u)).ne' - have hv : Real.sqrt (π v) ≠ 0 := (Real.sqrt_pos.mpr (h_pos v)).ne' - field_simp - -/-- **Math.** Step 3b-iii: `P^k = D^{-1/2} · Q^k · D^{1/2}` by induction. -/ -theorem transMatrix_pow_eq_similarity - {Q : Matrix n n ℝ} - (h_sim : ∀ u v, Q u v = Real.sqrt (π u) * P u v / Real.sqrt (π v)) - (h_pos : ∀ v, 0 < π v) : - ∀ k : ℕ, P ^ k = diagSqrtInv π * Q ^ k * diagSqrt π := by - have hP : P = diagSqrtInv π * Q * diagSqrt π := - transMatrix_eq_similarity_of_Q h_sim h_pos - have h_cancel : diagSqrt π * diagSqrtInv π = 1 := - diagSqrt_mul_diagSqrtInv h_pos - have h_cancel' : diagSqrtInv π * diagSqrt π = 1 := - diagSqrtInv_mul_diagSqrt h_pos - intro k - induction k with - | zero => - simp [pow_zero, h_cancel'] - | succ k ih => - calc P ^ (k + 1) - = P ^ k * P := by rw [pow_succ] - _ = (diagSqrtInv π * Q ^ k * diagSqrt π) * - (diagSqrtInv π * Q * diagSqrt π) := by rw [ih, hP] - _ = diagSqrtInv π * Q ^ k * (diagSqrt π * diagSqrtInv π) * - (Q * diagSqrt π) := by - simp only [Matrix.mul_assoc] - _ = diagSqrtInv π * Q ^ k * 1 * (Q * diagSqrt π) := by rw [h_cancel] - _ = diagSqrtInv π * (Q ^ k * Q) * diagSqrt π := by - rw [Matrix.mul_one]; simp only [Matrix.mul_assoc] - _ = diagSqrtInv π * Q ^ (k + 1) * diagSqrt π := by rw [← pow_succ] - -/-- **Math.** Spectral entry-form for `Q^k`: if `Q` is Hermitian with -eigenvalues `λᵢ` and orthonormal eigenvector basis `φᵢ`, then - - `(Q^k)_{u,v} = ∑ᵢ λᵢ^k · φᵢ(u) · φᵢ(v)`. - -Direct corollary of Mathlib's `Matrix.IsHermitian.spectral_theorem` -applied to the iterated power: `Q^k = U · diag(λ^k) · U^*`. The proof -lifts `Q = U D U^*` (where `U = eigenvectorUnitary`) through the -power via `U^* · U = 1`, then unfolds the triple-product entry via -`Matrix.mul_apply` and the diagonal-collapse `Finset.sum_ite_eq`. -/ -theorem hermitian_pow_apply_spectral - {Q : Matrix n n ℝ} (hQ : Q.IsHermitian) (k : ℕ) (u v : n) : - (Q ^ k) u v = ∑ i : n, - (hQ.eigenvalues i) ^ k * hQ.eigenvectorBasis i u * hQ.eigenvectorBasis i v := by - -- Set up U as a plain matrix - set U : Matrix n n ℝ := (hQ.eigenvectorUnitary : Matrix n n ℝ) with hU_def - -- Unitarity facts (real case: star = transpose) - have hU_star_self : star U * U = 1 := Unitary.coe_star_mul_self _ - have hU_self_star : U * star U = 1 := Unitary.coe_mul_star_self _ - -- Spectral theorem: Q = U · diag(λ) · star U - have h_spec : Q = U * Matrix.diagonal hQ.eigenvalues * star U := by - have hsp := hQ.spectral_theorem - rw [Unitary.conjStarAlgAut_apply] at hsp - -- For ℝ, RCLike.ofReal ∘ eigenvalues = eigenvalues definitionally - simpa [Function.comp_def] using hsp - -- Abstract eigenvalues to a fresh name so rw [h_spec] doesn't trip the - -- motive checker via the Q-dependency in `hQ.eigenvalues`. - set lam : n → ℝ := hQ.eigenvalues with hlam_def - -- Q^k = U · diag(λ^k) · star U by induction - have hQk : Q ^ k = - U * Matrix.diagonal (fun i => (lam i) ^ k) * star U := by - induction k with - | zero => - -- Q^0 = 1; RHS = U · diag(1) · star U = U · 1 · star U = U · star U = 1 - simp only [pow_zero] - rw [Matrix.diagonal_one, Matrix.mul_one] - exact hU_self_star.symm - | succ k ih => - calc Q ^ (k + 1) - = Q ^ k * Q := pow_succ Q k - _ = (U * Matrix.diagonal (fun i => (lam i) ^ k) * star U) * Q := by - rw [ih] - _ = (U * Matrix.diagonal (fun i => (lam i) ^ k) * star U) * - (U * Matrix.diagonal lam * star U) := by - rw [h_spec] - _ = U * Matrix.diagonal (fun i => (lam i) ^ k) * - (star U * U) * Matrix.diagonal lam * star U := by - simp only [Matrix.mul_assoc] - _ = U * Matrix.diagonal (fun i => (lam i) ^ k) * 1 * - Matrix.diagonal lam * star U := by rw [hU_star_self] - _ = U * (Matrix.diagonal (fun i => (lam i) ^ k) * - Matrix.diagonal lam) * star U := by - rw [Matrix.mul_one]; simp only [Matrix.mul_assoc] - _ = U * Matrix.diagonal (fun i => (lam i) ^ (k + 1)) * star U := by - rw [Matrix.diagonal_mul_diagonal] - simp_rw [pow_succ] - -- Entry of U · diag(λ^k) · star U at (u, v) - rw [hQk, Matrix.mul_apply] - -- ∑_i (U · diag d)_{u,i} * (star U)_{i,v} - have h_inner : ∀ i : n, - (U * Matrix.diagonal (fun j => (lam j) ^ k)) u i * - (star U : Matrix n n ℝ) i v - = (lam i) ^ k - * hQ.eigenvectorBasis i u * hQ.eigenvectorBasis i v := by - intro i - rw [Matrix.mul_apply] - rw [Finset.sum_eq_single i] - · -- diagonal collapse: U_{u,i} * d i * (star U)_{i,v} - rw [Matrix.diagonal_apply_eq] - have hU_ui : U u i = hQ.eigenvectorBasis i u := by - rw [hU_def] - exact Matrix.IsHermitian.eigenvectorUnitary_apply hQ u i - have hstar_iv : (star U : Matrix n n ℝ) i v = hQ.eigenvectorBasis i v := by - rw [hU_def, Matrix.star_eq_conjTranspose, Matrix.conjTranspose_apply, - star_trivial] - exact Matrix.IsHermitian.eigenvectorUnitary_apply hQ v i - rw [hU_ui, hstar_iv] - ring - · intros j _ hji - rw [Matrix.diagonal_apply_ne _ hji] - simp - · intro h_not_mem - exact absurd (Finset.mem_univ i) h_not_mem - simp_rw [h_inner] - -/-- **Math.** Step 3b-iv: the entry-form for `(P^k)_{u,v}`. Combines -`transMatrix_pow_eq_similarity` (Step 3b-iii) with the spectral -entry-form `hermitian_pow_apply_spectral`. -/ -theorem transMatrix_pow_apply_spectral - {Q : Matrix n n ℝ} (hQ : Q.IsHermitian) - (h_sim : ∀ u v, Q u v = Real.sqrt (π u) * P u v / Real.sqrt (π v)) - (h_pos : ∀ v, 0 < π v) (k : ℕ) (u v : n) : - (P ^ k) u v - = (1 / Real.sqrt (π u)) - * (∑ i : n, - (hQ.eigenvalues i) ^ k - * hQ.eigenvectorBasis i u * hQ.eigenvectorBasis i v) - * Real.sqrt (π v) := by - have h_pow_sim : P ^ k = diagSqrtInv π * Q ^ k * diagSqrt π := - transMatrix_pow_eq_similarity h_sim h_pos k - rw [h_pow_sim, diagSqrt, Matrix.mul_diagonal, diagSqrtInv, Matrix.diagonal_mul] - rw [hermitian_pow_apply_spectral hQ k u v] - -/-! ### Step 3c: spectral expansion of the fundamental matrix - -Combining the partial-sum convergence `∑_{k ∑ k ∈ Finset.range N, r ^ k) Filter.atTop - (nhds (1 - r)⁻¹) := - (hasSum_geometric_of_abs_lt_one h).tendsto_sum_nat - -/-- **Math.** Step 3c sub-lemma: entry of `(P^k - W)` expanded in the -spectral basis of `Q`. The eigenvalue `λᵢ = 1` (with eigenvector -`φᵢ = √π`) contributes exactly `W_{u,v} = π_v` for every `k`, so its -contribution cancels in `P^k - W`; the remaining sum is over `i` -with `λᵢ < 1`. -/ -lemma pow_sub_stationaryProjection_apply_spectral - {Q : Matrix n n ℝ} (hQ : Q.IsHermitian) - (h_sim : ∀ u v, Q u v = Real.sqrt (π u) * P u v / Real.sqrt (π v)) - (h_pos : ∀ v, 0 < π v) - (h_eig_le_one : ∀ i, hQ.eigenvalues i ≤ 1) - (h_top_eigvec : ∀ i, hQ.eigenvalues i = 1 → - ∀ v, hQ.eigenvectorBasis i v = Real.sqrt (π v)) - (h_top_unique : ∃! i_top : n, hQ.eigenvalues i_top = 1) - (k : ℕ) (u v : n) : - (P ^ k - stationaryProjection π) u v - = (1 / Real.sqrt (π u)) - * (∑ i : n, if hQ.eigenvalues i < 1 - then (hQ.eigenvalues i) ^ k - * hQ.eigenvectorBasis i u - * hQ.eigenvectorBasis i v - else 0) - * Real.sqrt (π v) := by - obtain ⟨i_top, h_top_eig, h_top_uniq⟩ := h_top_unique - rw [Matrix.sub_apply, transMatrix_pow_apply_spectral hQ h_sim h_pos k u v] - -- `stationaryProjection π u v = π v` definitionally - change _ - π v = _ - -- Identify each summand of the full spectral sum with its - -- λ<1 truncation plus a δ_{i,i_top} · √π_u · √π_v correction. - have h_split : ∀ i : n, - (hQ.eigenvalues i) ^ k - * hQ.eigenvectorBasis i u * hQ.eigenvectorBasis i v - = (if hQ.eigenvalues i < 1 - then (hQ.eigenvalues i) ^ k - * hQ.eigenvectorBasis i u * hQ.eigenvectorBasis i v - else 0) - + (if i = i_top - then Real.sqrt (π u) * Real.sqrt (π v) else 0) := by - intro i - by_cases hi : i = i_top - · subst hi - have hlam : hQ.eigenvalues i = 1 := h_top_eig - have h_not_lt : ¬ hQ.eigenvalues i < 1 := by rw [hlam]; exact lt_irrefl 1 - rw [if_neg h_not_lt, if_pos rfl, zero_add, hlam, one_pow, one_mul, - h_top_eigvec i hlam u, h_top_eigvec i hlam v] - · have h_lt : hQ.eigenvalues i < 1 := by - rcases lt_or_eq_of_le (h_eig_le_one i) with h | h - · exact h - · exact absurd (h_top_uniq i h) hi - rw [if_pos h_lt, if_neg hi, add_zero] - -- Sum the per-i identity and collapse the δ_{i,i_top} term. - have h_sum_eq : - (∑ i : n, (hQ.eigenvalues i) ^ k - * hQ.eigenvectorBasis i u * hQ.eigenvectorBasis i v) - = (∑ i : n, if hQ.eigenvalues i < 1 - then (hQ.eigenvalues i) ^ k - * hQ.eigenvectorBasis i u * hQ.eigenvectorBasis i v - else 0) + Real.sqrt (π u) * Real.sqrt (π v) := by - calc (∑ i : n, (hQ.eigenvalues i) ^ k - * hQ.eigenvectorBasis i u * hQ.eigenvectorBasis i v) - = ∑ i : n, ((if hQ.eigenvalues i < 1 - then (hQ.eigenvalues i) ^ k - * hQ.eigenvectorBasis i u * hQ.eigenvectorBasis i v - else 0) - + (if i = i_top - then Real.sqrt (π u) * Real.sqrt (π v) else 0)) := - Finset.sum_congr rfl (fun i _ => h_split i) - _ = (∑ i : n, if hQ.eigenvalues i < 1 - then (hQ.eigenvalues i) ^ k - * hQ.eigenvectorBasis i u * hQ.eigenvectorBasis i v - else 0) - + ∑ i : n, if i = i_top - then Real.sqrt (π u) * Real.sqrt (π v) else 0 := - Finset.sum_add_distrib - _ = (∑ i : n, if hQ.eigenvalues i < 1 - then (hQ.eigenvalues i) ^ k - * hQ.eigenvectorBasis i u * hQ.eigenvectorBasis i v - else 0) + Real.sqrt (π u) * Real.sqrt (π v) := by - rw [Finset.sum_ite_eq', if_pos (Finset.mem_univ _)] - rw [h_sum_eq] - -- Algebra: (1/√π_u) · (S_lt + √π_u · √π_v) · √π_v − π_v = (1/√π_u) · S_lt · √π_v. - have h_su_ne : Real.sqrt (π u) ≠ 0 := (Real.sqrt_pos.mpr (h_pos u)).ne' - have h_sq_v : Real.sqrt (π v) * Real.sqrt (π v) = π v := - Real.mul_self_sqrt (h_pos v).le - field_simp - linear_combination Real.sqrt (π u) * h_sq_v - -/-- **Math.** Step 3c target. The fundamental matrix has the spectral -entry expansion - - `Z_{u,v} = √(π_v/π_u) · ∑_{i : λᵢ < 1} φᵢ(u) · φᵢ(v) / (1 - λᵢ)` - -where `{λᵢ, φᵢ}` is the eigendata of the symmetrised matrix `Q`. - -The hypotheses are: stochasticity of `P`, stationarity of `π`, -positivity of `π`, sum-to-one of `π`, entrywise convergence -`P^k → W`, similarity `Q u v = √(π u) · P u v / √(π v)`, the -upper bound `λᵢ ≤ 1`, the Perron eigenvector identification -`λᵢ = 1 ⟹ φᵢ = √π`, uniqueness of the top eigenvalue, and the -spectral-gap-style bound `|λᵢ| < 1` for every `i` with `λᵢ < 1`. -/ -theorem fundamentalMatrix_spectral_expansion - {Q : Matrix n n ℝ} (hQ : Q.IsHermitian) - (h_sim : ∀ u v, Q u v = Real.sqrt (π u) * P u v / Real.sqrt (π v)) - (h_pos : ∀ v, 0 < π v) - (h_row_sum : ∀ i, ∑ j, P i j = 1) - (h_stat : ∀ j, ∑ i, π i * P i j = π j) - (h_sum : ∑ i, π i = 1) - (h_conv : - Filter.Tendsto (fun k => P ^ k) Filter.atTop - (nhds (stationaryProjection π))) - (h_eig_le_one : ∀ i, hQ.eigenvalues i ≤ 1) - (h_top_eigvec : ∀ i, hQ.eigenvalues i = 1 → - ∀ v, hQ.eigenvectorBasis i v = Real.sqrt (π v)) - (h_top_unique : ∃! i_top : n, hQ.eigenvalues i_top = 1) - (h_nontop_bound : ∀ i, hQ.eigenvalues i < 1 → |hQ.eigenvalues i| < 1) - (u v : n) : - fundamentalMatrix P π u v - = Real.sqrt (π v / π u) - * ∑ i : n, - if hQ.eigenvalues i < 1 - then hQ.eigenvectorBasis i u * hQ.eigenvectorBasis i v - / (1 - hQ.eigenvalues i) - else 0 := by - set sqrtPiU := Real.sqrt (π u) with hsqrtPiU - set sqrtPiV := Real.sqrt (π v) with hsqrtPiV - have h_su_pos : 0 < sqrtPiU := Real.sqrt_pos.mpr (h_pos u) - have h_su_ne : sqrtPiU ≠ 0 := h_su_pos.ne' - -- Step 3a: matrix-level partial-sum convergence. - have h_matrix : Filter.Tendsto - (fun N => ∑ k ∈ Finset.range N, (P ^ k - stationaryProjection π)) - Filter.atTop - (nhds (fundamentalMatrix P π)) := - fundamentalMatrix_eq_tendsto_partial_sum h_row_sum h_stat h_sum h_conv - -- Project to the (u,v) entry: matrix-entry evaluation is continuous on Matrix n n ℝ. - have h_cont_apply : Continuous (fun M : Matrix n n ℝ => M u v) := - (continuous_apply v).comp (continuous_apply u) - have h_LHS : Filter.Tendsto - (fun N => (∑ k ∈ Finset.range N, (P ^ k - stationaryProjection π)) u v) - Filter.atTop (nhds (fundamentalMatrix P π u v)) := - (h_cont_apply.tendsto _).comp h_matrix - -- Rewrite the entry-level partial sum using the per-k Step-3c sub-lemma, - -- then swap ∑_k and ∑_i so the inner ∑_k is a geometric partial sum. - have h_partial_eq : ∀ N, - (∑ k ∈ Finset.range N, (P ^ k - stationaryProjection π)) u v - = (1 / sqrtPiU) - * (∑ i : n, if hQ.eigenvalues i < 1 - then (∑ k ∈ Finset.range N, (hQ.eigenvalues i) ^ k) - * hQ.eigenvectorBasis i u * hQ.eigenvectorBasis i v - else 0) - * sqrtPiV := by - intro N - rw [Matrix.sum_apply] - simp_rw [pow_sub_stationaryProjection_apply_spectral hQ h_sim h_pos h_eig_le_one - h_top_eigvec h_top_unique _ u v] - -- ∑ k, (1/√u) * (∑ i, ...) * √v = (1/√u) * (∑ k, ∑ i, ...) * √v - rw [← Finset.sum_mul, ← Finset.mul_sum] - congr 2 - rw [Finset.sum_comm] - -- For each i: split the if; geometric in k inside the `then` branch. - apply Finset.sum_congr rfl - intro i _ - by_cases h_lt : hQ.eigenvalues i < 1 - · simp only [if_pos h_lt] - simp_rw [mul_assoc] - rw [← Finset.sum_mul] - · simp only [if_neg h_lt] - exact Finset.sum_const_zero - -- Convert h_LHS to use the rewritten form. - rw [show (fun N => (∑ k ∈ Finset.range N, (P ^ k - stationaryProjection π)) u v) - = fun N => (1 / sqrtPiU) * - (∑ i : n, if hQ.eigenvalues i < 1 - then (∑ k ∈ Finset.range N, (hQ.eigenvalues i) ^ k) - * hQ.eigenvectorBasis i u * hQ.eigenvectorBasis i v - else 0) * sqrtPiV from funext h_partial_eq] at h_LHS - -- Per-mode geometric limit: ∑_{k if hQ.eigenvalues i < 1 - then (∑ k ∈ Finset.range N, (hQ.eigenvalues i) ^ k) - * hQ.eigenvectorBasis i u * hQ.eigenvectorBasis i v - else 0) - Filter.atTop - (nhds (if hQ.eigenvalues i < 1 - then hQ.eigenvectorBasis i u * hQ.eigenvectorBasis i v - / (1 - hQ.eigenvalues i) - else 0)) := by - intro i - by_cases h_lt : hQ.eigenvalues i < 1 - · simp only [if_pos h_lt] - have h_geom : Filter.Tendsto - (fun N => ∑ k ∈ Finset.range N, (hQ.eigenvalues i) ^ k) - Filter.atTop (nhds (1 - hQ.eigenvalues i)⁻¹) := - partial_geom_sum_tendsto_inv_one_sub_real (h_nontop_bound i h_lt) - have h_mul := h_geom.mul_const - (hQ.eigenvectorBasis i u * hQ.eigenvectorBasis i v) - convert h_mul using 2 - · ring - · field_simp - · simp only [if_neg h_lt] - exact tendsto_const_nhds - -- Sum over `i`: finite sum of converging sequences converges to the sum of limits. - have h_sum_tendsto : Filter.Tendsto - (fun N => ∑ i : n, if hQ.eigenvalues i < 1 - then (∑ k ∈ Finset.range N, (hQ.eigenvalues i) ^ k) - * hQ.eigenvectorBasis i u * hQ.eigenvectorBasis i v - else 0) - Filter.atTop - (nhds (∑ i : n, if hQ.eigenvalues i < 1 - then hQ.eigenvectorBasis i u * hQ.eigenvectorBasis i v - / (1 - hQ.eigenvalues i) - else 0)) := - tendsto_finsetSum _ (fun i _ => h_inner_tendsto i) - -- Multiply by the (1/√π_u) prefactor and the √π_v postfactor. - have h_RHS : Filter.Tendsto - (fun N => (1 / sqrtPiU) * - (∑ i : n, if hQ.eigenvalues i < 1 - then (∑ k ∈ Finset.range N, (hQ.eigenvalues i) ^ k) - * hQ.eigenvectorBasis i u * hQ.eigenvectorBasis i v - else 0) * sqrtPiV) - Filter.atTop - (nhds ((1 / sqrtPiU) * - (∑ i : n, if hQ.eigenvalues i < 1 - then hQ.eigenvectorBasis i u * hQ.eigenvectorBasis i v - / (1 - hQ.eigenvalues i) - else 0) * sqrtPiV)) := - (h_sum_tendsto.const_mul (1 / sqrtPiU)).mul_const sqrtPiV - -- Uniqueness of limits ties LHS to the RHS-shape limit. - have h_eq := tendsto_nhds_unique h_LHS h_RHS - rw [h_eq] - -- Final cosmetic step: (1/√π_u) · √π_v = √(π_v/π_u). - have h_sqrt_div : Real.sqrt (π v / π u) = (1 / sqrtPiU) * sqrtPiV := by - rw [hsqrtPiU, hsqrtPiV, Real.sqrt_div (h_pos v).le] - field_simp - rw [h_sqrt_div] - ring - -end ProofAtlas.RandomWalk diff --git a/lean/ProofAtlas/RandomWalk/KemenySnell.lean b/lean/ProofAtlas/RandomWalk/KemenySnell.lean deleted file mode 100644 index 177a88f..0000000 --- a/lean/ProofAtlas/RandomWalk/KemenySnell.lean +++ /dev/null @@ -1,911 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.RandomWalk.FundamentalMatrix - -/-! -# Kemeny-Snell hitting-time identity - -For a finite ergodic Markov chain with transition matrix `P`, -stationary distribution `π`, and fundamental matrix -`Z := (I - P + W)⁻¹ - W` (`ProofAtlas.RandomWalk.fundamentalMatrix`), -the expected first-passage time `h(u, v)` from `u` to `v` admits the -closed form - - `h(u, v) = (Z_{v,v} - Z_{u,v}) / π_v`. - -This file proves the identity at the abstract matrix level, working -with the matrix-form expected first-passage time -`(I - Q_v)⁻¹ · b_v` where `Q_v = P[i ≠ v]` is `P` with row `v` -zeroed out and `b_v(i) := if i = v then 0 else 1`. The proof is -algebraic: the candidate vector `g_v(u) := (Z_{v,v} - Z_{u,v}) / π_v` -satisfies `(I - Q_v) · g_v = b_v`, and invertibility of `I - Q_v` -yields `g_v = (I - Q_v)⁻¹ · b_v`. - -The Kemeny-Snell identity is the algebraic core of paper §6: it -converts the commute-time decomposition -`d(u, v) = f(u) + f(v) - I(u, v)` (Theorem 6.1) into entries of -`Z`, which combined with the spectral expansion of `Z` -(`fundamentalMatrix_spectral_expansion`) yields the spectral form -of `I(u, v)` (Theorem 6.4). - -## Main definitions - -* `ProofAtlas.RandomWalk.killedMatrix P v` — `P` with row `v` zeroed - out, abstract version of `Hypergraph.killedTransMatrix`. -* `ProofAtlas.RandomWalk.killedRhs v` — the vector `b_v` (0 at `v`, - 1 elsewhere). -* `ProofAtlas.RandomWalk.kemenyVec P π v` — candidate hitting-time - vector `u ↦ (Z_{v,v} - Z_{u,v}) / π_v`. - -## Main results - -* `killed_mulVec_kemenyVec` — `(I - Q_v) · g_v = b_v`. -* `kemenySnell_hitting_apply` — under invertibility of `I - Q_v`, - `((I - Q_v)⁻¹ · b_v) u = (Z_{v,v} - Z_{u,v}) / π_v`. --/ - -namespace ProofAtlas.RandomWalk - -open Matrix BigOperators Finset - -variable {n : Type*} [Fintype n] [DecidableEq n] -variable {P : Matrix n n ℝ} {π : n → ℝ} - -/-- **Eng.** Killed transition matrix: `P` with row `v` zeroed out, -making `v` absorbing. Abstract version of -`Hypergraph.killedTransMatrix`. -/ -def killedMatrix (P : Matrix n n ℝ) (v : n) : Matrix n n ℝ := - Matrix.of fun i j => if i = v then 0 else P i j - -/-- **Eng.** Right-hand side of the killed-recursion: `0` at `v`, -`1` elsewhere. -/ -def killedRhs (v : n) : n → ℝ := - fun i => if i = v then (0 : ℝ) else 1 - -/-- **Math.** Kemeny-Snell candidate vector -`g_v(u) := (Z_{v,v} - Z_{u,v}) / π_v`. The hitting time identity -asserts `h(u, v) = g_v(u)`. -/ -noncomputable def kemenyVec (P : Matrix n n ℝ) (π : n → ℝ) (v : n) : n → ℝ := - fun u => (fundamentalMatrix P π v v - fundamentalMatrix P π u v) / π v - -omit [Fintype n] in -/-- **Eng.** Row `v` of the killed matrix is identically zero. -/ -@[simp] -lemma killedMatrix_apply_self (v : n) (j : n) : - killedMatrix P v v j = 0 := by - simp [killedMatrix] - -omit [Fintype n] in -/-- **Eng.** Off-row, `killedMatrix P v i j = P i j` for `i ≠ v`. -/ -lemma killedMatrix_apply_of_ne {i v : n} (h : i ≠ v) (j : n) : - killedMatrix P v i j = P i j := by - simp [killedMatrix, h] - -/-- **Math.** Column-`v` extraction of `(I - P) · Z = I - W`. For any -`u`, `∑_w (I - P)_{u,w} · Z_{w,v} = (I - W)_{u,v}`, which evaluates -to `[u = v] - π_v`. -/ -lemma one_sub_P_mulVec_Z_col - (h_row_sum : ∀ i, ∑ j, P i j = 1) - (h_stat : ∀ j, ∑ i, π i * P i j = π j) - (h_sum : ∑ i, π i = 1) - (h_inv : IsUnit (auxMatrix P π).det) (v u : n) : - ((1 - P : Matrix n n ℝ).mulVec (fun w => fundamentalMatrix P π w v)) u - = (if u = v then (1 : ℝ) else 0) - π v := by - have h_iPZ : (1 - P) * fundamentalMatrix P π = 1 - stationaryProjection π := - one_sub_transMatrix_mul_fundamentalMatrix h_row_sum h_stat h_sum h_inv - -- ((1 - P) * Z) u v = ∑ w, (1-P) u w * Z w v = ((1-P).mulVec (col v of Z)) u - have h_col : ((1 - P : Matrix n n ℝ) * fundamentalMatrix P π) u v - = ((1 - P : Matrix n n ℝ).mulVec (fun w => fundamentalMatrix P π w v)) u := by - rw [Matrix.mul_apply]; rfl - -- And ((1 - P) * Z) u v = (1 - W) u v = [u = v] - π v - have h_1mW : ((1 - stationaryProjection π : Matrix n n ℝ)) u v - = (if u = v then (1 : ℝ) else 0) - π v := by - simp [Matrix.sub_apply, Matrix.one_apply, stationaryProjection] - rw [← h_col, h_iPZ, h_1mW] - -/-- **Eng.** `(1 - P)` annihilates constant vectors when each row of -`P` sums to `1`. -/ -lemma one_sub_P_mulVec_const - (h_row_sum : ∀ i, ∑ j, P i j = 1) (c : ℝ) (u : n) : - ((1 - P : Matrix n n ℝ).mulVec (fun _ : n => c)) u = 0 := by - rw [Matrix.sub_mulVec, Pi.sub_apply, Matrix.one_mulVec] - have h_Pc : (P.mulVec (fun (_ : n) => c)) u = c := by - simp only [Matrix.mulVec, dotProduct] - rw [← Finset.sum_mul, h_row_sum, one_mul] - rw [h_Pc, sub_self] - -/-- **Math.** Action of `1 - P` on the Kemeny candidate vector at row -`u`. Decomposing `kemenyVec = (π_v)⁻¹ • (const Z_{v,v} - col_v Z)`, -the constant `Z_{v,v}` part is annihilated by `1 - P` -(`one_sub_P_mulVec_const`), so the result is -`-(I-W)_{u,v} / π_v = (π_v - [u = v]) / π_v`. -/ -lemma one_sub_P_mulVec_kemenyVec - (h_row_sum : ∀ i, ∑ j, P i j = 1) - (h_stat : ∀ j, ∑ i, π i * P i j = π j) - (h_sum : ∑ i, π i = 1) - (h_pos : ∀ w, 0 < π w) - (h_inv : IsUnit (auxMatrix P π).det) (v u : n) : - ((1 - P : Matrix n n ℝ).mulVec (kemenyVec P π v)) u - = if u = v then (π v - 1) / π v else 1 := by - have h_πv_ne : π v ≠ 0 := (h_pos v).ne' - -- Column-v slice of (I - P) · Z evaluated at row u. - have h_iPZ_col : ((1 - P : Matrix n n ℝ).mulVec - (fun w => fundamentalMatrix P π w v)) u - = (if u = v then (1 : ℝ) else 0) - π v := - one_sub_P_mulVec_Z_col h_row_sum h_stat h_sum h_inv v u - -- Express kemenyVec as (π_v)⁻¹ • (const Z_{v,v} - col_v Z). - have h_kemeny_smul : kemenyVec P π v - = (π v)⁻¹ • ((fun _ : n => fundamentalMatrix P π v v) - - (fun w => fundamentalMatrix P π w v)) := by - funext w - simp only [kemenyVec, Pi.smul_apply, Pi.sub_apply, smul_eq_mul] - rw [div_eq_inv_mul] - rw [h_kemeny_smul, Matrix.mulVec_smul, Matrix.mulVec_sub] - simp only [Pi.smul_apply, Pi.sub_apply, smul_eq_mul] - rw [one_sub_P_mulVec_const h_row_sum, h_iPZ_col, zero_sub] - by_cases hu : u = v - · simp only [if_pos hu] - field_simp - ring - · simp only [if_neg hu] - field_simp - ring - -/-- **Math.** `(I - Q_v) · g_v = b_v` (Kemeny-Snell defining identity). -Row `u = v`: both sides are `0` (since `Q_v` has row `v` zero, the -LHS reduces to `g_v(v) = 0`). Row `u ≠ v`: the LHS reduces to -`((I - P) · g_v)(u)` (since `Q_v` agrees with `P` off row `v`), -which equals `1` by `one_sub_P_mulVec_kemenyVec`. -/ -theorem killed_mulVec_kemenyVec - (h_row_sum : ∀ i, ∑ j, P i j = 1) - (h_stat : ∀ j, ∑ i, π i * P i j = π j) - (h_sum : ∑ i, π i = 1) - (h_pos : ∀ w, 0 < π w) - (h_inv : IsUnit (auxMatrix P π).det) (v : n) : - ((1 : Matrix n n ℝ) - killedMatrix P v).mulVec (kemenyVec P π v) - = killedRhs v := by - funext u - rw [Matrix.sub_mulVec, Pi.sub_apply, Matrix.one_mulVec] - by_cases hu : u = v - · -- u = v: both kemenyVec u and (K · g)(u) vanish. - have h_g_u : kemenyVec P π v u = 0 := by simp [kemenyVec, hu] - have h_K_mulvec_u : ((killedMatrix P v).mulVec (kemenyVec P π v)) u = 0 := by - rw [hu] - simp only [Matrix.mulVec, dotProduct, killedMatrix_apply_self, zero_mul, - Finset.sum_const_zero] - rw [h_g_u, h_K_mulvec_u, sub_zero] - simp [killedRhs, hu] - · -- u ≠ v: killedMatrix row u = P row u, so the LHS reduces to ((1 - P) · g)(u) = 1. - have h_K_row : (killedMatrix P v).mulVec (kemenyVec P π v) u - = P.mulVec (kemenyVec P π v) u := by - simp only [Matrix.mulVec, dotProduct] - apply Finset.sum_congr rfl - intros j _ - rw [killedMatrix_apply_of_ne hu] - rw [h_K_row] - have h_combine : - kemenyVec P π v u - P.mulVec (kemenyVec P π v) u - = ((1 - P : Matrix n n ℝ).mulVec (kemenyVec P π v)) u := by - rw [Matrix.sub_mulVec, Pi.sub_apply, Matrix.one_mulVec] - rw [h_combine, one_sub_P_mulVec_kemenyVec h_row_sum h_stat h_sum h_pos h_inv] - simp [if_neg hu, killedRhs] - -/-- **Math.** **Kemeny-Snell hitting-time identity.** Given -invertibility of `I - Q_v`, the matrix-form expected first-passage -time `((I - Q_v)⁻¹ · b_v)(u)` equals `(Z_{v,v} - Z_{u,v}) / π_v`. - -Proof: `killed_mulVec_kemenyVec` gives `(I - Q_v) · g_v = b_v`; -left-multiplying by `(I - Q_v)⁻¹` (well-defined by `h_killed_inv`) -yields `g_v = (I - Q_v)⁻¹ · b_v`. -/ -theorem kemenySnell_hitting_apply - (h_row_sum : ∀ i, ∑ j, P i j = 1) - (h_stat : ∀ j, ∑ i, π i * P i j = π j) - (h_sum : ∑ i, π i = 1) - (h_pos : ∀ w, 0 < π w) - (h_inv : IsUnit (auxMatrix P π).det) {v : n} - (h_killed_inv : IsUnit ((1 : Matrix n n ℝ) - killedMatrix P v)) (u : n) : - (((1 : Matrix n n ℝ) - killedMatrix P v)⁻¹.mulVec (killedRhs v)) u - = (fundamentalMatrix P π v v - fundamentalMatrix P π u v) / π v := by - have h_det : IsUnit ((1 - killedMatrix P v : Matrix n n ℝ)).det := - (Matrix.isUnit_iff_isUnit_det _).mp h_killed_inv - have h_eq : ((1 : Matrix n n ℝ) - killedMatrix P v).mulVec (kemenyVec P π v) - = killedRhs v := - killed_mulVec_kemenyVec h_row_sum h_stat h_sum h_pos h_inv v - have h_inverse : ((1 : Matrix n n ℝ) - killedMatrix P v)⁻¹.mulVec (killedRhs v) - = kemenyVec P π v := by - rw [← h_eq, Matrix.mulVec_mulVec, Matrix.nonsing_inv_mul _ h_det, - Matrix.one_mulVec] - rw [h_inverse] - rfl - -/-! ### Invertibility of `1 - killedMatrix P v` - -Derive `IsUnit (1 - killedMatrix P v)` from ergodic convergence -`h_conv : P^k → W` plus stochasticity/nonnegativity of `P`. Mirrors -`auxMatrix_isUnit_det` (RandomWalk/FundamentalMatrix.lean): the inductive -bound `(Q_v^(m+1) · 1)[u] ≤ 1 - (P^m)[u, v]` together with -`(P^m)[u, v] → π_v > 0` and finiteness of `n` yields a uniform `m` -with `‖Q_v^(m+1)‖_∞ ≤ 1 - π_v / 2 < 1`; Neumann factorisation lifts -this to invertibility. -/ - -/-- **Eng.** Row `v` of `(killedMatrix P v)^(k+1)` is identically zero. -/ -lemma killedMatrix_pow_row_zero (v : n) : - ∀ k : ℕ, ∀ j : n, ((killedMatrix P v) ^ (k + 1)) v j = 0 := by - intro k - induction k with - | zero => - intro j - rw [zero_add, pow_one] - exact killedMatrix_apply_self v j - | succ k ih => - intro j - rw [pow_succ, Matrix.mul_apply] - exact Finset.sum_eq_zero (fun w _ => by rw [ih w, zero_mul]) - -/-- **Eng.** Powers of a pointwise-nonneg matrix have nonneg entries. -/ -lemma pow_nonneg_entries (h_nonneg : ∀ i j : n, 0 ≤ P i j) : - ∀ k : ℕ, ∀ i j : n, 0 ≤ (P ^ k) i j := by - intro k - induction k with - | zero => - intro i j - rw [pow_zero, Matrix.one_apply] - by_cases h : i = j <;> simp [h] - | succ k ih => - intro i j - rw [pow_succ, Matrix.mul_apply] - exact Finset.sum_nonneg (fun w _ => mul_nonneg (ih i w) (h_nonneg w j)) - -/-- **Eng.** Row sums of `P^k` are `1` when `P` is row-stochastic. -/ -lemma pow_row_sum_eq_one (h_row_sum : ∀ i : n, ∑ j, P i j = 1) : - ∀ k : ℕ, ∀ i : n, ∑ j, (P ^ k) i j = 1 := by - intro k - induction k with - | zero => - intro i - simp [pow_zero, Matrix.one_apply] - | succ k ih => - intro i - simp_rw [pow_succ, Matrix.mul_apply] - rw [Finset.sum_comm] - simp_rw [← Finset.mul_sum, h_row_sum, mul_one] - exact ih i - -/-- **Eng.** Entries of `P^k` are bounded by `1`. -/ -lemma pow_apply_le_one - (h_row_sum : ∀ i : n, ∑ j, P i j = 1) - (h_nonneg : ∀ i j : n, 0 ≤ P i j) (k : ℕ) (i j : n) : - (P ^ k) i j ≤ 1 := by - have h_le : (P ^ k) i j ≤ ∑ j', (P ^ k) i j' := - Finset.single_le_sum - (f := fun j' => (P ^ k) i j') - (fun j' _ => pow_nonneg_entries h_nonneg k i j') (Finset.mem_univ j) - rw [pow_row_sum_eq_one h_row_sum k i] at h_le - exact h_le - -/-- **Math.** Inductive bound on row-sums of killed-matrix powers: -for `u ≠ v` and `m ≥ 0`, - `((killedMatrix P v)^(m+1) · 1) u ≤ 1 - (P^m) u v`. -/ -lemma killedMatrix_pow_mulVec_one_le - (h_row_sum : ∀ i : n, ∑ j, P i j = 1) - (h_nonneg : ∀ i j : n, 0 ≤ P i j) (v : n) : - ∀ m : ℕ, ∀ u : n, u ≠ v → - ((killedMatrix P v) ^ (m + 1)).mulVec (fun _ => (1 : ℝ)) u - ≤ 1 - (P ^ m) u v := by - intro m - induction m with - | zero => - intros u hu - rw [zero_add, pow_one] - simp only [Matrix.mulVec, dotProduct] - have h_eq : ∀ j, (killedMatrix P v) u j * (1 : ℝ) = P u j := fun j => by - rw [killedMatrix_apply_of_ne hu, mul_one] - simp_rw [h_eq] - rw [h_row_sum u, pow_zero, Matrix.one_apply, if_neg hu, sub_zero] - | succ m ih => - intros u hu - -- (Q_v ^ (m+2) · 1) u = (Q_v · (Q_v^(m+1) · 1)) u - -- Use pow_succ': Q^(m+2) = Q * Q^(m+1). - set Q : Matrix n n ℝ := killedMatrix P v with hQ - have h_pow_eq : Q ^ (m + 1 + 1) = Q * Q ^ (m + 1) := by rw [pow_succ'] - have h_mulVec : - (Q ^ (m + 2)).mulVec (fun _ => (1 : ℝ)) u - = (Q.mulVec ((Q ^ (m + 1)).mulVec (fun _ => 1))) u := by - change (Q ^ (m + 1 + 1)).mulVec _ u = _ - rw [h_pow_eq, ← Matrix.mulVec_mulVec] - rw [h_mulVec] - -- (Q · x) u = ∑ j, P[u, j] * x[j] for u ≠ v. - have h_Q_row : - (Q.mulVec ((Q ^ (m + 1)).mulVec (fun _ => (1 : ℝ)))) u - = ∑ j, P u j * ((Q ^ (m + 1)).mulVec (fun _ => 1)) j := by - simp only [Matrix.mulVec, dotProduct] - refine Finset.sum_congr rfl (fun j _ => ?_) - rw [hQ, killedMatrix_apply_of_ne hu] - rw [h_Q_row] - -- Split off j = v term, where (Q^(m+1) · 1) v = 0. - have h_at_v : ((Q ^ (m + 1)).mulVec (fun _ => (1 : ℝ))) v = 0 := by - simp only [Matrix.mulVec, dotProduct, mul_one] - exact Finset.sum_eq_zero (fun w _ => killedMatrix_pow_row_zero v m w) - have h_decomp : ∑ j, P u j * ((Q ^ (m + 1)).mulVec (fun _ => (1 : ℝ))) j - = ∑ j ∈ Finset.univ \ {v}, P u j - * ((Q ^ (m + 1)).mulVec (fun _ => 1)) j := by - rw [← Finset.sum_filter_add_sum_filter_not Finset.univ (· = v) - (fun j => P u j * ((Q ^ (m + 1)).mulVec (fun _ => (1 : ℝ))) j)] - have h_filter_eq : Finset.univ.filter (· = v) = {v} := by - ext j; simp [eq_comm] - have h_filter_ne : Finset.univ.filter (¬ · = v) = Finset.univ \ {v} := by - ext j; simp [eq_comm] - rw [h_filter_eq, h_filter_ne] - rw [Finset.sum_singleton, h_at_v, mul_zero, zero_add] - rw [h_decomp] - -- Apply IH at each j ≠ v: (Q^(m+1) · 1) j ≤ 1 - (P^m) j v. - have h_ih_apply : ∀ j ∈ Finset.univ \ {v}, - P u j * ((Q ^ (m + 1)).mulVec (fun _ => (1 : ℝ))) j - ≤ P u j * (1 - (P ^ m) j v) := by - intros j hj - have hjv : j ≠ v := Finset.notMem_singleton.mp (Finset.mem_sdiff.mp hj).2 - exact mul_le_mul_of_nonneg_left (ih j hjv) (h_nonneg u j) - have h_le := Finset.sum_le_sum h_ih_apply - refine le_trans h_le ?_ - -- ∑_{j ≠ v} P[u,j] · (1 - (P^m)[j, v]) - -- = (1 - P[u, v]) - ((P^(m+1))[u, v] - P[u, v] · (P^m)[v, v]) - have h_RHS_compute : - ∑ j ∈ Finset.univ \ {v}, P u j * (1 - (P ^ m) j v) - = 1 - (P ^ (m + 1)) u v - P u v * (1 - (P ^ m) v v) := by - simp_rw [mul_sub, mul_one] - rw [Finset.sum_sub_distrib] - have h_sum_split_P : ∑ j ∈ Finset.univ \ {v}, P u j = 1 - P u v := by - have : ∑ j, P u j = ∑ j ∈ {v}, P u j + ∑ j ∈ Finset.univ \ {v}, P u j := by - rw [Finset.sum_singleton, ← Finset.sum_sdiff (Finset.singleton_subset_iff.mpr - (Finset.mem_univ v)), Finset.sum_singleton, add_comm] - rw [h_row_sum, Finset.sum_singleton] at this - linarith - have h_sum_split_Pmul : - ∑ j ∈ Finset.univ \ {v}, P u j * (P ^ m) j v - = (P ^ (m + 1)) u v - P u v * (P ^ m) v v := by - have h_full : ∑ j, P u j * (P ^ m) j v = (P ^ (m + 1)) u v := by - rw [pow_succ', Matrix.mul_apply] - have h_split : - ∑ j, P u j * (P ^ m) j v - = (∑ j ∈ {v}, P u j * (P ^ m) j v) - + ∑ j ∈ Finset.univ \ {v}, P u j * (P ^ m) j v := by - rw [← Finset.sum_sdiff (Finset.singleton_subset_iff.mpr (Finset.mem_univ v))] - rw [add_comm] - rw [h_full, Finset.sum_singleton] at h_split - linarith - rw [h_sum_split_P, h_sum_split_Pmul] - ring - rw [h_RHS_compute] - have h_pmv : (P ^ m) v v ≤ 1 := pow_apply_le_one h_row_sum h_nonneg m v v - have h_pos_term : 0 ≤ P u v * (1 - (P ^ m) v v) := - mul_nonneg (h_nonneg u v) (by linarith) - linarith - -open Matrix.Norms.Operator in -/-- **Math.** Convergence of `(P^k)[u, v] → π v` for each `u, v`, extracted -pointwise from matrix convergence `P^k → W = 1π^T`. -/ -lemma pow_apply_tendsto - (h_conv : Filter.Tendsto (fun k => P ^ k) Filter.atTop - (nhds (stationaryProjection π))) (u v : n) : - Filter.Tendsto (fun k => (P ^ k) u v) Filter.atTop (nhds (π v)) := by - have h_proj : stationaryProjection π u v = π v := by simp [stationaryProjection] - rw [← h_proj] - -- Convergence in matrix norm implies entrywise. - exact ((continuous_apply v).comp (continuous_apply u)).tendsto _ |>.comp h_conv - -/-- **Math.** Uniform extraction: from `(P^k)[u, v] → π v` for each `u`, -pick `m` with `π v / 2 ≤ (P^m)[u, v]` for all `u`. -/ -lemma pow_apply_uniform_lower_bound - (h_pos : ∀ w, 0 < π w) - (h_conv : Filter.Tendsto (fun k => P ^ k) Filter.atTop - (nhds (stationaryProjection π))) (v : n) : - ∃ m : ℕ, ∀ u : n, π v / 2 ≤ (P ^ m) u v := by - have h_each : ∀ u : n, ∀ᶠ k in Filter.atTop, π v / 2 ≤ (P ^ k) u v := by - intro u - have h_t := pow_apply_tendsto h_conv u v - have h_lt : π v / 2 < π v := by have := h_pos v; linarith - exact h_t.eventually (eventually_ge_nhds h_lt) - have h_all : ∀ᶠ k in Filter.atTop, ∀ u : n, π v / 2 ≤ (P ^ k) u v := - Filter.eventually_all.mpr h_each - exact h_all.exists - -omit [DecidableEq n] in -/-- **Eng.** Stationary distribution components are bounded by `1`. -/ -lemma stationaryDist_apply_le_one - (h_sum : ∑ i, π i = 1) (h_pos : ∀ w, 0 < π w) (v : n) : - π v ≤ 1 := by - have h_le : π v ≤ ∑ i, π i := - Finset.single_le_sum (f := π) (fun i _ => (h_pos i).le) (Finset.mem_univ v) - rw [h_sum] at h_le; exact h_le - -open Matrix.Norms.Operator in -/-- **Math.** ∃ k, `‖(killedMatrix P v)^(k+1)‖ < 1`, from ergodic convergence. -/ -theorem killedMatrix_pow_norm_lt_one - (h_row_sum : ∀ i, ∑ j, P i j = 1) - (h_nonneg : ∀ i j : n, 0 ≤ P i j) - (h_sum : ∑ i, π i = 1) - (h_pos : ∀ w, 0 < π w) - (h_conv : Filter.Tendsto (fun k => P ^ k) Filter.atTop - (nhds (stationaryProjection π))) (v : n) : - ∃ k : ℕ, ‖((killedMatrix P v) ^ (k + 1) : Matrix n n ℝ)‖ < 1 := by - obtain ⟨m, hm⟩ := pow_apply_uniform_lower_bound h_pos h_conv v - refine ⟨m, ?_⟩ - have h_πv_pos : 0 < π v := h_pos v - have h_πv_le_one : π v ≤ 1 := stationaryDist_apply_le_one h_sum h_pos v - have h_K_nn : (0 : ℝ) ≤ 1 - π v / 2 := by linarith - have h_K_lt_one : (1 - π v / 2 : ℝ) < 1 := by linarith - -- Q ≥ 0 entrywise. - have h_Q_nonneg : ∀ i j, 0 ≤ (killedMatrix P v) i j := by - intros i j - unfold killedMatrix - simp only [Matrix.of_apply] - by_cases hi : i = v - · simp only [if_pos hi]; exact le_refl 0 - · simp only [if_neg hi]; exact h_nonneg i j - have h_Qpow_nonneg : ∀ i j, 0 ≤ ((killedMatrix P v) ^ (m + 1)) i j := - pow_nonneg_entries h_Q_nonneg (m + 1) - -- Step: row sum bound at ℝ level. - have h_row_bound : ∀ u : n, - ∑ j, ((killedMatrix P v) ^ (m + 1)) u j ≤ 1 - π v / 2 := by - intro u - by_cases huv : u = v - · -- u = v case: row sum is 0. - rw [huv] - have h_zero : ∀ j, ((killedMatrix P v) ^ (m + 1)) v j = 0 := - killedMatrix_pow_row_zero v m - rw [Finset.sum_congr rfl (fun j _ => h_zero j)] - simp; linarith - · -- u ≠ v case: row sum = (Q^(m+1) · 1)[u] ≤ 1 - (P^m)[u, v] ≤ 1 - π v / 2. - have h_row_eq : ∑ j, ((killedMatrix P v) ^ (m + 1)) u j - = ((killedMatrix P v) ^ (m + 1)).mulVec (fun _ => (1 : ℝ)) u := by - simp only [Matrix.mulVec, dotProduct, mul_one] - rw [h_row_eq] - have h_le := killedMatrix_pow_mulVec_one_le h_row_sum h_nonneg v m u huv - linarith [hm u] - -- Convert ℝ bound to operator norm bound. - rw [Matrix.linfty_opNorm_def] - refine lt_of_le_of_lt ?_ h_K_lt_one - -- Need: ((sup ...).toReal) ≤ 1 - π v / 2. Use Finset.sup_le at NNReal level. - rw [show (1 - π v / 2 : ℝ) = ((⟨1 - π v / 2, h_K_nn⟩ : NNReal) : ℝ) from rfl] - apply NNReal.coe_le_coe.mpr - apply Finset.sup_le - intros u _ - -- ∑ j, ‖A_uj‖₊ ≤ ⟨1 - π v / 2, _⟩ in NNReal: cast and use h_row_bound. - rw [← NNReal.coe_le_coe] - rw [NNReal.coe_sum] - have h_norm_eq : ∀ j, - ((‖((killedMatrix P v) ^ (m + 1)) u j‖₊ : NNReal) : ℝ) - = ((killedMatrix P v) ^ (m + 1)) u j := by - intro j - rw [coe_nnnorm, Real.norm_eq_abs, abs_of_nonneg (h_Qpow_nonneg u j)] - simp_rw [h_norm_eq] - exact h_row_bound u - -open Matrix.Norms.Operator in -/-- **Math.** `1 - killedMatrix P v` is a unit on any ergodic chain. -/ -theorem killedMatrix_one_sub_isUnit - (h_row_sum : ∀ i, ∑ j, P i j = 1) - (h_nonneg : ∀ i j : n, 0 ≤ P i j) - (h_sum : ∑ i, π i = 1) - (h_pos : ∀ w, 0 < π w) - (h_conv : Filter.Tendsto (fun k => P ^ k) Filter.atTop - (nhds (stationaryProjection π))) (v : n) : - IsUnit ((1 : Matrix n n ℝ) - killedMatrix P v) := by - obtain ⟨k, hk⟩ := - killedMatrix_pow_norm_lt_one h_row_sum h_nonneg h_sum h_pos h_conv v - set Q : Matrix n n ℝ := killedMatrix P v - have hPow : IsUnit (1 - Q ^ (k + 1)) := isUnit_one_sub_of_norm_lt_one hk - have hFactor : - (1 - Q) * (∑ i ∈ Finset.range (k + 1), Q ^ i) = 1 - Q ^ (k + 1) := - mul_neg_geom_sum Q (k + 1) - rw [← hFactor] at hPow - exact isUnit_of_mul_isUnit_left hPow - -open Matrix.Norms.Operator in -/-- **Math.** Powers of the killed transition matrix tend to zero. - -Proof: from `killedMatrix_pow_norm_lt_one`, pick `m := k + 1` with -`‖Q ^ m‖ < 1`. For any `N`, decompose `N = q · m + r` (`r < m`), -so `‖Q ^ N‖ ≤ ‖Q ^ m‖ ^ q · max_{s < m} ‖Q ^ s‖`. As `N → ∞`, -`q = N / m → ∞`, so the right-hand side tends to `0`. Squeeze. -/ -theorem killedMatrix_pow_tendsto_zero - (h_row_sum : ∀ i, ∑ j, P i j = 1) - (h_nonneg : ∀ i j : n, 0 ≤ P i j) - (h_sum : ∑ i, π i = 1) - (h_pos : ∀ w, 0 < π w) - (h_conv : Filter.Tendsto (fun k => P ^ k) Filter.atTop - (nhds (stationaryProjection π))) (v : n) : - Filter.Tendsto (fun N : ℕ => (killedMatrix P v) ^ N) - Filter.atTop (nhds 0) := by - haveI : Nonempty n := ⟨v⟩ - obtain ⟨k, hk⟩ := - killedMatrix_pow_norm_lt_one h_row_sum h_nonneg h_sum h_pos h_conv v - set Q : Matrix n n ℝ := killedMatrix P v with hQ_def - set m : ℕ := k + 1 with hm_def - have hm_pos : 0 < m := Nat.succ_pos _ - rw [tendsto_zero_iff_norm_tendsto_zero] - have h0_mem : (0 : ℕ) ∈ Finset.range m := Finset.mem_range.mpr hm_pos - set C : ℝ := (Finset.range m).sup' ⟨0, h0_mem⟩ (fun r => ‖Q ^ r‖) with hC_def - have hC_ge : ∀ r, r < m → ‖Q ^ r‖ ≤ C := fun r hr => - Finset.le_sup' (f := fun r => ‖Q ^ r‖) (Finset.mem_range.mpr hr) - have h_bd : ∀ N : ℕ, ‖Q ^ N‖ ≤ ‖Q ^ m‖ ^ (N / m) * C := by - intro N - have h_decomp : Q ^ N = (Q ^ m) ^ (N / m) * Q ^ (N % m) := by - rw [← pow_mul, ← pow_add, Nat.div_add_mod] - rw [h_decomp] - refine (norm_mul_le _ _).trans ?_ - have h_pow_le : ‖(Q ^ m) ^ (N / m)‖ ≤ ‖Q ^ m‖ ^ (N / m) := - norm_pow_le _ _ - have h_r_le : ‖Q ^ (N % m)‖ ≤ C := hC_ge _ (Nat.mod_lt N hm_pos) - have h_pow_nonneg : 0 ≤ ‖Q ^ m‖ ^ (N / m) := pow_nonneg (norm_nonneg _) _ - calc ‖(Q ^ m) ^ (N / m)‖ * ‖Q ^ (N % m)‖ - ≤ ‖Q ^ m‖ ^ (N / m) * ‖Q ^ (N % m)‖ := - mul_le_mul_of_nonneg_right h_pow_le (norm_nonneg _) - _ ≤ ‖Q ^ m‖ ^ (N / m) * C := - mul_le_mul_of_nonneg_left h_r_le h_pow_nonneg - have h_div_tendsto : Filter.Tendsto (fun N : ℕ => N / m) - Filter.atTop Filter.atTop := - Nat.tendsto_div_const_atTop hm_pos.ne' - have h_pow_tendsto : Filter.Tendsto (fun q : ℕ => ‖Q ^ m‖ ^ q) - Filter.atTop (nhds 0) := - tendsto_pow_atTop_nhds_zero_of_lt_one (norm_nonneg _) hk - have h_comp : Filter.Tendsto (fun N : ℕ => ‖Q ^ m‖ ^ (N / m)) - Filter.atTop (nhds 0) := - h_pow_tendsto.comp h_div_tendsto - have h_bound_tendsto : Filter.Tendsto (fun N : ℕ => ‖Q ^ m‖ ^ (N / m) * C) - Filter.atTop (nhds 0) := by - have := h_comp.mul_const C - simpa using this - exact squeeze_zero (fun _ => norm_nonneg _) h_bd h_bound_tendsto - -open Matrix.Norms.Operator in -/-- **Math.** Neumann series for `(1 - Q_v)⁻¹`: the partial sums -`S_N := ∑_{i < N} Q_v ^ i` converge to `(1 - Q_v)⁻¹` as `N → ∞`. - -Proof: the geometric factorisation -`(1 - Q_v) · S_N = 1 - Q_v ^ N` (`mul_neg_geom_sum`) gives -`S_N = (1 - Q_v)⁻¹ - (1 - Q_v)⁻¹ · Q_v ^ N` after left-multiplying by -`(1 - Q_v)⁻¹` (well-defined by `killedMatrix_one_sub_isUnit`). -Since `Q_v ^ N → 0` (`killedMatrix_pow_tendsto_zero`), the second -term tends to `0` by continuity of left multiplication. -/ -theorem killedMatrix_neumann_partial_sum_tendsto - (h_row_sum : ∀ i, ∑ j, P i j = 1) - (h_nonneg : ∀ i j : n, 0 ≤ P i j) - (h_sum : ∑ i, π i = 1) - (h_pos : ∀ w, 0 < π w) - (h_conv : Filter.Tendsto (fun k => P ^ k) Filter.atTop - (nhds (stationaryProjection π))) (v : n) : - Filter.Tendsto (fun N : ℕ => ∑ i ∈ Finset.range N, (killedMatrix P v) ^ i) - Filter.atTop - (nhds ((1 : Matrix n n ℝ) - killedMatrix P v)⁻¹) := by - set Q : Matrix n n ℝ := killedMatrix P v with hQ_def - set A : Matrix n n ℝ := 1 - Q with hA_def - have hA_unit : IsUnit A := - killedMatrix_one_sub_isUnit h_row_sum h_nonneg h_sum h_pos h_conv v - have hA_det : IsUnit A.det := (Matrix.isUnit_iff_isUnit_det A).mp hA_unit - -- Identity: S_N = A⁻¹ - A⁻¹ * Q^N. - have h_S_eq : ∀ N : ℕ, - (∑ i ∈ Finset.range N, Q ^ i) = A⁻¹ - A⁻¹ * Q ^ N := by - intro N - have h_geom : A * (∑ i ∈ Finset.range N, Q ^ i) = 1 - Q ^ N := - mul_neg_geom_sum Q N - have h1 : - A⁻¹ * (A * (∑ i ∈ Finset.range N, Q ^ i)) = A⁻¹ * (1 - Q ^ N) := by - rw [h_geom] - rw [← Matrix.mul_assoc, Matrix.nonsing_inv_mul _ hA_det, Matrix.one_mul] at h1 - rw [h1, Matrix.mul_sub, Matrix.mul_one] - -- Q^N → 0, hence A⁻¹ * Q^N → 0. - have h_Q_pow : Filter.Tendsto (fun N : ℕ => Q ^ N) Filter.atTop (nhds 0) := - killedMatrix_pow_tendsto_zero h_row_sum h_nonneg h_sum h_pos h_conv v - have h_mul : Filter.Tendsto (fun N : ℕ => A⁻¹ * Q ^ N) - Filter.atTop (nhds 0) := by - have h := h_Q_pow.const_mul A⁻¹ - simpa using h - have h_sub : Filter.Tendsto (fun N : ℕ => A⁻¹ - A⁻¹ * Q ^ N) - Filter.atTop (nhds (A⁻¹ - 0)) := - tendsto_const_nhds.sub h_mul - rw [sub_zero] at h_sub - exact (Filter.tendsto_congr h_S_eq).mpr h_sub - -/-! ### Double-killed transition matrix - -For the hitting-time triangle inequality we need the matrix obtained -by zeroing **two** rows of `P` simultaneously. Properties (norm -bound, invertibility, Neumann series, entrywise nonneg inverse) are -inherited from the single-killed `killedMatrix P v` by entrywise -comparison `killedMatrixPair P v w ≤ killedMatrix P v` and the -observation that L∞ operator norm is monotone in entries of a -nonneg matrix. -/ - -/-- **Eng.** "Double-killed" transition matrix: rows indexed by `v` -and `w` both zeroed. Used in the hitting-time triangle inequality -to formalise a function being P-harmonic on `V \ {v, w}`. -/ -def killedMatrixPair (P : Matrix n n ℝ) (v w : n) : Matrix n n ℝ := - Matrix.of fun i j => if i = v ∨ i = w then 0 else P i j - -omit [Fintype n] in -@[simp] -lemma killedMatrixPair_apply_self_left (v w : n) (j : n) : - killedMatrixPair P v w v j = 0 := by simp [killedMatrixPair] - -omit [Fintype n] in -@[simp] -lemma killedMatrixPair_apply_self_right (v w : n) (j : n) : - killedMatrixPair P v w w j = 0 := by simp [killedMatrixPair] - -omit [Fintype n] in -lemma killedMatrixPair_apply_of_ne {i v w : n} (hv : i ≠ v) (hw : i ≠ w) (j : n) : - killedMatrixPair P v w i j = P i j := by - simp [killedMatrixPair, hv, hw] - -omit [Fintype n] in -/-- **Eng.** Pointwise nonnegativity inherits from `P`. -/ -lemma killedMatrixPair_nonneg (h_nonneg : ∀ i j : n, 0 ≤ P i j) (v w : n) : - ∀ i j, 0 ≤ killedMatrixPair P v w i j := by - intros i j - unfold killedMatrixPair - simp only [Matrix.of_apply] - split_ifs - · exact le_refl 0 - · exact h_nonneg i j - -omit [Fintype n] in -/-- **Eng.** Pointwise nonnegativity of `killedMatrix P v` from `P`. -/ -lemma killedMatrix_nonneg (h_nonneg : ∀ i j : n, 0 ≤ P i j) (v : n) : - ∀ i j, 0 ≤ killedMatrix P v i j := by - intros i j - unfold killedMatrix - simp only [Matrix.of_apply] - split_ifs - · exact le_refl 0 - · exact h_nonneg i j - -omit [Fintype n] in -/-- **Eng.** Entrywise: double-killed ≤ single-killed at `v`. The -single-killed matrix `killedMatrix P v` zeroes only row `v`; the -double-killed matrix zeroes both `v` and `w`, so its entries are -either equal to or smaller than the single-killed counterpart -(strictly smaller exactly on row `w` when `w ≠ v`). -/ -lemma killedMatrixPair_le_killedMatrix - (h_nonneg : ∀ i j : n, 0 ≤ P i j) (v w : n) : - ∀ i j, killedMatrixPair P v w i j ≤ killedMatrix P v i j := by - intros i j - unfold killedMatrixPair killedMatrix - simp only [Matrix.of_apply] - by_cases hi_v : i = v - · simp [hi_v] - · simp only [if_neg hi_v] - by_cases hi_w : i = w - · rw [if_pos (Or.inr hi_w)] - exact h_nonneg i j - · rw [if_neg (not_or.mpr ⟨hi_v, hi_w⟩)] - -/-- **Eng.** Powers preserve the entrywise comparison: from `A ≤ B` -entrywise and both nonneg, `A^k ≤ B^k` entrywise. -/ -lemma pow_le_pow_of_nonneg_le {A B : Matrix n n ℝ} - (hA_nonneg : ∀ i j, 0 ≤ A i j) - (hB_nonneg : ∀ i j, 0 ≤ B i j) - (h_le : ∀ i j, A i j ≤ B i j) : - ∀ k : ℕ, ∀ i j, (A ^ k) i j ≤ (B ^ k) i j := by - intro k - induction k with - | zero => - intros i j - simp [pow_zero, Matrix.one_apply] - | succ k ih => - intros i j - rw [pow_succ, pow_succ, Matrix.mul_apply, Matrix.mul_apply] - refine Finset.sum_le_sum (fun m _ => ?_) - have h_Ak_nn : 0 ≤ (A ^ k) i m := pow_nonneg_entries hA_nonneg k i m - have h_Bm_nn : 0 ≤ B m j := hB_nonneg m j - calc (A ^ k) i m * A m j - ≤ (A ^ k) i m * B m j := - mul_le_mul_of_nonneg_left (h_le m j) h_Ak_nn - _ ≤ (B ^ k) i m * B m j := - mul_le_mul_of_nonneg_right (ih i m) h_Bm_nn - -open Matrix.Norms.Operator in -/-- **Eng.** L∞ operator norm is monotone in entries for nonneg -matrices: `0 ≤ A ≤ B` entrywise implies `‖A‖ ≤ ‖B‖`. -/ -lemma linfty_opNorm_le_of_nonneg_le {A B : Matrix n n ℝ} - (hA_nonneg : ∀ i j, 0 ≤ A i j) - (hB_nonneg : ∀ i j, 0 ≤ B i j) - (h_le : ∀ i j, A i j ≤ B i j) : - ‖A‖ ≤ ‖B‖ := by - rw [Matrix.linfty_opNorm_def, Matrix.linfty_opNorm_def] - apply NNReal.coe_le_coe.mpr - apply Finset.sup_le - intros i _ - refine le_trans ?_ (Finset.le_sup (f := fun i' => ∑ j, ‖B i' j‖₊) - (Finset.mem_univ i)) - -- Goal: ∑ j, ‖A i j‖₊ ≤ ∑ j, ‖B i j‖₊ in NNReal - rw [← NNReal.coe_le_coe, NNReal.coe_sum, NNReal.coe_sum] - refine Finset.sum_le_sum (fun j _ => ?_) - have h_A : ((‖A i j‖₊ : NNReal) : ℝ) = A i j := by - rw [coe_nnnorm, Real.norm_eq_abs, abs_of_nonneg (hA_nonneg i j)] - have h_B : ((‖B i j‖₊ : NNReal) : ℝ) = B i j := by - rw [coe_nnnorm, Real.norm_eq_abs, abs_of_nonneg (hB_nonneg i j)] - rw [h_A, h_B] - exact h_le i j - -open Matrix.Norms.Operator in -/-- **Math.** Double-killed matrix admits `‖Q'^(k+1)‖ < 1` for the -same `k` as `killedMatrix P v`, inheriting via the entrywise -comparison and L∞-norm monotonicity. -/ -theorem killedMatrixPair_pow_norm_lt_one - (h_row_sum : ∀ i, ∑ j, P i j = 1) - (h_nonneg : ∀ i j : n, 0 ≤ P i j) - (h_sum : ∑ i, π i = 1) - (h_pos : ∀ w, 0 < π w) - (h_conv : Filter.Tendsto (fun k => P ^ k) Filter.atTop - (nhds (stationaryProjection π))) (v w : n) : - ∃ k : ℕ, ‖((killedMatrixPair P v w) ^ (k + 1) : Matrix n n ℝ)‖ < 1 := by - haveI : Nonempty n := ⟨v⟩ - obtain ⟨k, hk⟩ := - killedMatrix_pow_norm_lt_one h_row_sum h_nonneg h_sum h_pos h_conv v - refine ⟨k, ?_⟩ - have h_pair_nonneg : ∀ i j, 0 ≤ ((killedMatrixPair P v w) ^ (k + 1)) i j := - pow_nonneg_entries (killedMatrixPair_nonneg h_nonneg v w) (k + 1) - have h_single_nonneg : ∀ i j, 0 ≤ ((killedMatrix P v) ^ (k + 1)) i j := - pow_nonneg_entries (killedMatrix_nonneg h_nonneg v) (k + 1) - have h_pow_le : ∀ i j, - ((killedMatrixPair P v w) ^ (k + 1)) i j ≤ ((killedMatrix P v) ^ (k + 1)) i j := - pow_le_pow_of_nonneg_le - (killedMatrixPair_nonneg h_nonneg v w) - (killedMatrix_nonneg h_nonneg v) - (killedMatrixPair_le_killedMatrix h_nonneg v w) - (k + 1) - exact lt_of_le_of_lt - (linfty_opNorm_le_of_nonneg_le h_pair_nonneg h_single_nonneg h_pow_le) - hk - -open Matrix.Norms.Operator in -/-- **Math.** `1 - killedMatrixPair P v w` is a unit. -/ -theorem killedMatrixPair_one_sub_isUnit - (h_row_sum : ∀ i, ∑ j, P i j = 1) - (h_nonneg : ∀ i j : n, 0 ≤ P i j) - (h_sum : ∑ i, π i = 1) - (h_pos : ∀ w, 0 < π w) - (h_conv : Filter.Tendsto (fun k => P ^ k) Filter.atTop - (nhds (stationaryProjection π))) (v w : n) : - IsUnit ((1 : Matrix n n ℝ) - killedMatrixPair P v w) := by - obtain ⟨k, hk⟩ := - killedMatrixPair_pow_norm_lt_one h_row_sum h_nonneg h_sum h_pos h_conv v w - set Q : Matrix n n ℝ := killedMatrixPair P v w - have hPow : IsUnit (1 - Q ^ (k + 1)) := isUnit_one_sub_of_norm_lt_one hk - have hFactor : - (1 - Q) * (∑ i ∈ Finset.range (k + 1), Q ^ i) = 1 - Q ^ (k + 1) := - mul_neg_geom_sum Q (k + 1) - rw [← hFactor] at hPow - exact isUnit_of_mul_isUnit_left hPow - -open Matrix.Norms.Operator in -/-- **Math.** Powers of the double-killed matrix tend to zero. -/ -theorem killedMatrixPair_pow_tendsto_zero - (h_row_sum : ∀ i, ∑ j, P i j = 1) - (h_nonneg : ∀ i j : n, 0 ≤ P i j) - (h_sum : ∑ i, π i = 1) - (h_pos : ∀ w, 0 < π w) - (h_conv : Filter.Tendsto (fun k => P ^ k) Filter.atTop - (nhds (stationaryProjection π))) (v w : n) : - Filter.Tendsto (fun N : ℕ => (killedMatrixPair P v w) ^ N) - Filter.atTop (nhds 0) := by - haveI : Nonempty n := ⟨v⟩ - obtain ⟨k, hk⟩ := - killedMatrixPair_pow_norm_lt_one h_row_sum h_nonneg h_sum h_pos h_conv v w - set Q : Matrix n n ℝ := killedMatrixPair P v w with hQ_def - set m : ℕ := k + 1 with hm_def - have hm_pos : 0 < m := Nat.succ_pos _ - rw [tendsto_zero_iff_norm_tendsto_zero] - have h0_mem : (0 : ℕ) ∈ Finset.range m := Finset.mem_range.mpr hm_pos - set C : ℝ := (Finset.range m).sup' ⟨0, h0_mem⟩ (fun r => ‖Q ^ r‖) with hC_def - have hC_ge : ∀ r, r < m → ‖Q ^ r‖ ≤ C := fun r hr => - Finset.le_sup' (f := fun r => ‖Q ^ r‖) (Finset.mem_range.mpr hr) - have h_bd : ∀ N : ℕ, ‖Q ^ N‖ ≤ ‖Q ^ m‖ ^ (N / m) * C := by - intro N - have h_decomp : Q ^ N = (Q ^ m) ^ (N / m) * Q ^ (N % m) := by - rw [← pow_mul, ← pow_add, Nat.div_add_mod] - rw [h_decomp] - refine (norm_mul_le _ _).trans ?_ - have h_pow_le : ‖(Q ^ m) ^ (N / m)‖ ≤ ‖Q ^ m‖ ^ (N / m) := - norm_pow_le _ _ - have h_r_le : ‖Q ^ (N % m)‖ ≤ C := hC_ge _ (Nat.mod_lt N hm_pos) - have h_pow_nonneg : 0 ≤ ‖Q ^ m‖ ^ (N / m) := pow_nonneg (norm_nonneg _) _ - calc ‖(Q ^ m) ^ (N / m)‖ * ‖Q ^ (N % m)‖ - ≤ ‖Q ^ m‖ ^ (N / m) * ‖Q ^ (N % m)‖ := - mul_le_mul_of_nonneg_right h_pow_le (norm_nonneg _) - _ ≤ ‖Q ^ m‖ ^ (N / m) * C := - mul_le_mul_of_nonneg_left h_r_le h_pow_nonneg - have h_div_tendsto : Filter.Tendsto (fun N : ℕ => N / m) - Filter.atTop Filter.atTop := - Nat.tendsto_div_const_atTop hm_pos.ne' - have h_pow_tendsto : Filter.Tendsto (fun q : ℕ => ‖Q ^ m‖ ^ q) - Filter.atTop (nhds 0) := - tendsto_pow_atTop_nhds_zero_of_lt_one (norm_nonneg _) hk - have h_comp : Filter.Tendsto (fun N : ℕ => ‖Q ^ m‖ ^ (N / m)) - Filter.atTop (nhds 0) := - h_pow_tendsto.comp h_div_tendsto - have h_bound_tendsto : Filter.Tendsto (fun N : ℕ => ‖Q ^ m‖ ^ (N / m) * C) - Filter.atTop (nhds 0) := by - have := h_comp.mul_const C - simpa using this - exact squeeze_zero (fun _ => norm_nonneg _) h_bd h_bound_tendsto - -open Matrix.Norms.Operator in -/-- **Math.** Neumann partial sum convergence for the double-killed -matrix: `∑_{i < N} Q'^i → (1 - Q')⁻¹`. -/ -theorem killedMatrixPair_neumann_partial_sum_tendsto - (h_row_sum : ∀ i, ∑ j, P i j = 1) - (h_nonneg : ∀ i j : n, 0 ≤ P i j) - (h_sum : ∑ i, π i = 1) - (h_pos : ∀ w, 0 < π w) - (h_conv : Filter.Tendsto (fun k => P ^ k) Filter.atTop - (nhds (stationaryProjection π))) (v w : n) : - Filter.Tendsto (fun N : ℕ => ∑ i ∈ Finset.range N, (killedMatrixPair P v w) ^ i) - Filter.atTop - (nhds ((1 : Matrix n n ℝ) - killedMatrixPair P v w)⁻¹) := by - set Q : Matrix n n ℝ := killedMatrixPair P v w with hQ_def - set A : Matrix n n ℝ := 1 - Q with hA_def - have hA_unit : IsUnit A := - killedMatrixPair_one_sub_isUnit h_row_sum h_nonneg h_sum h_pos h_conv v w - have hA_det : IsUnit A.det := (Matrix.isUnit_iff_isUnit_det A).mp hA_unit - have h_S_eq : ∀ N : ℕ, - (∑ i ∈ Finset.range N, Q ^ i) = A⁻¹ - A⁻¹ * Q ^ N := by - intro N - have h_geom : A * (∑ i ∈ Finset.range N, Q ^ i) = 1 - Q ^ N := - mul_neg_geom_sum Q N - have h1 : - A⁻¹ * (A * (∑ i ∈ Finset.range N, Q ^ i)) = A⁻¹ * (1 - Q ^ N) := by - rw [h_geom] - rw [← Matrix.mul_assoc, Matrix.nonsing_inv_mul _ hA_det, Matrix.one_mul] at h1 - rw [h1, Matrix.mul_sub, Matrix.mul_one] - have h_Q_pow : Filter.Tendsto (fun N : ℕ => Q ^ N) Filter.atTop (nhds 0) := - killedMatrixPair_pow_tendsto_zero h_row_sum h_nonneg h_sum h_pos h_conv v w - have h_mul : Filter.Tendsto (fun N : ℕ => A⁻¹ * Q ^ N) - Filter.atTop (nhds 0) := by - have h := h_Q_pow.const_mul A⁻¹ - simpa using h - have h_sub : Filter.Tendsto (fun N : ℕ => A⁻¹ - A⁻¹ * Q ^ N) - Filter.atTop (nhds (A⁻¹ - 0)) := - tendsto_const_nhds.sub h_mul - rw [sub_zero] at h_sub - exact (Filter.tendsto_congr h_S_eq).mpr h_sub - -/-- **Math.** Entries of `(1 - killedMatrixPair P v w)⁻¹` are -entrywise nonneg. Follows from the Neumann series: each partial sum -`∑_{i < N} Q'^i` is entrywise nonneg, and the limit inherits the -inequality at every entry. -/ -theorem killedMatrixPair_inv_entry_nonneg - (h_row_sum : ∀ i, ∑ j, P i j = 1) - (h_nonneg : ∀ i j : n, 0 ≤ P i j) - (h_sum : ∑ i, π i = 1) - (h_pos : ∀ w, 0 < π w) - (h_conv : Filter.Tendsto (fun k => P ^ k) Filter.atTop - (nhds (stationaryProjection π))) (v w : n) (u j : n) : - 0 ≤ ((1 : Matrix n n ℝ) - killedMatrixPair P v w)⁻¹ u j := by - set Q : Matrix n n ℝ := killedMatrixPair P v w with hQ_def - set A : Matrix n n ℝ := 1 - Q with hA_def - have h_S_tendsto : - Filter.Tendsto (fun N : ℕ => ∑ i ∈ Finset.range N, Q ^ i) - Filter.atTop (nhds A⁻¹) := - killedMatrixPair_neumann_partial_sum_tendsto - h_row_sum h_nonneg h_sum h_pos h_conv v w - have h_entry : - Filter.Tendsto (fun N : ℕ => (∑ i ∈ Finset.range N, Q ^ i) u j) - Filter.atTop (nhds (A⁻¹ u j)) := by - have hu : Filter.Tendsto (fun N : ℕ => (∑ i ∈ Finset.range N, Q ^ i) u) - Filter.atTop (nhds (A⁻¹ u)) := - ((continuous_apply u).tendsto _).comp h_S_tendsto - exact ((continuous_apply j).tendsto _).comp hu - have h_Q_nonneg : ∀ i k, 0 ≤ Q i k := killedMatrixPair_nonneg h_nonneg v w - have h_S_nonneg : ∀ N : ℕ, 0 ≤ (∑ i ∈ Finset.range N, Q ^ i) u j := by - intro N - rw [Matrix.sum_apply] - exact Finset.sum_nonneg (fun i _ => - pow_nonneg_entries h_Q_nonneg i u j) - exact ge_of_tendsto' h_entry h_S_nonneg - -end ProofAtlas.RandomWalk diff --git a/lean/ProofAtlas/Tactic/Widget.lean b/lean/ProofAtlas/Tactic/Widget.lean deleted file mode 100644 index 720d500..0000000 --- a/lean/ProofAtlas/Tactic/Widget.lean +++ /dev/null @@ -1,227 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import Lean -import ProofWidgets.Component.Basic -import ProofWidgets.Component.HtmlDisplay -import ProofAtlas.Pipeline.IncidenceData -import ProofAtlas.Mapping.Declaration - -/-! -# `IncidenceData` → ProofAtlas interactive widget - -Builds props for the bundled `widget/atlasGraph.js` widget. The -viewer renders an SVG with: - -* Subterm vertices as small open circles. -* Hyperedges as coloured rectangles, colour-coded by `ExprColor`. -* Two range sliders (`spread`, `link dist`) live-tuning the d3-force - simulation. -* Mouse-wheel zoom and click-drag pan over the SVG canvas. -* Click-drag a node to pin it in place. - -Each node carries an SVG `` for native hover tooltips. - -The widget JS bundles its own d3 from esm.sh; React is supplied by -the Lean infoview host. - -A separate `colorLegendHtml` is emitted as an `HtmlDisplayPanel` so -the reader can decode the rectangle colours without trial and error. --/ - -namespace ProofAtlas.Pipeline - -open ProofAtlas.Mapping -open Lean ProofWidgets -open scoped ProofWidgets.Jsx - -/-- **Eng.** Short string name for each `ExprColor` constructor. -/ -def exprColorName : ProofAtlas.Mapping.ExprColor → String - | .bvar => "bvar" - | .fvar => "fvar" - | .mvar => "mvar" - | .sort => "sort" - | .const => "const" - | .lit => "lit" - | .app => "app" - | .lam => "lam" - | .forallE => "forallE" - | .letE => "letE" - | .mdata => "mdata" - | .proj => "proj" - -/-- **Eng.** CSS colour string for each `ExprColor` constructor. -/ -def exprColorCss : ProofAtlas.Mapping.ExprColor → String - | .bvar => "#888888" - | .fvar => "#888888" - | .mvar => "#888888" - | .sort => "#74b9ff" - | .const => "#0984e3" - | .lit => "#fdcb6e" - | .app => "#d63031" - | .lam => "#00b894" - | .forallE => "#6c5ce7" - | .letE => "#0abde3" - | .mdata => "#b2bec3" - | .proj => "#a0522d" - -/-- **Eng.** `ExprColor` constructors that actually appear as -hyperedges (compound constructors only). Atomic ones never produce -edges. -/ -def hyperedgeColors : List ProofAtlas.Mapping.ExprColor := - [.app, .lam, .forallE, .letE, .mdata, .proj] - -/-! ## AtlasGraph widget props -/ - -/-- **Eng.** One node in the AtlasGraph widget. `kind = "circle"` -for subterm vertices, `"rect"` for hyperedges. `color` is the -CSS fill colour (only meaningful for rectangles). `title` is the -hover-tooltip text. -/ -structure AtlasNode where - id : String - kind : String - color : String - title : String - deriving Lean.ToJson, Lean.FromJson, Inhabited - -/-- **Eng.** One directed edge (source → target) in the AtlasGraph -widget. -/ -structure AtlasLink where - source : String - target : String - deriving Lean.ToJson, Lean.FromJson, Inhabited - -/-- **Eng.** Props for the bundled `atlasGraph.js` widget. -/ -structure AtlasGraphProps where - vertices : Array AtlasNode - edges : Array AtlasLink - deriving Lean.ToJson, Lean.FromJson, Inhabited, Lean.Server.RpcEncodable - -/-- **Eng.** The widget component, bundled from `widget/atlasGraph.js`. -/ -@[widget_module] -def AtlasGraph : Component AtlasGraphProps where - javascript := include_str ".." / ".." / "widget" / "atlasGraph.js" - -/-- **Eng.** Convert an `IncidenceData` into `AtlasGraphProps`. -/ -def IncidenceData.toAtlasGraphProps (H : IncidenceData) : AtlasGraphProps := - let vs : Array AtlasNode := Id.run do - let mut acc : Array AtlasNode := #[] - for i in [:H.numVertices] do - let label := H.vertexLabel[i]?.getD s!"v{i}" - let color := H.vertexColor[i]?.getD "" - acc := acc.push { - id := s!"v{i}" - kind := "circle" - color := color - title := label - } - for h : j in [:H.edges.size] do - let e := H.edges[j] - acc := acc.push { - id := s!"e{j}" - kind := "rect" - color := exprColorCss e.color - title := exprColorName e.color - } - return acc - let es : Array AtlasLink := Id.run do - let mut acc : Array AtlasLink := #[] - for h : j in [:H.edges.size] do - let e := H.edges[j] - for i in e.inputs do - acc := acc.push { source := s!"v{i}", target := s!"e{j}" } - acc := acc.push { source := s!"e{j}", target := s!"v{e.output}" } - return acc - { vertices := vs, edges := es } - -/-! ## Colour legend (separate static panel) -/ - -/-- **Eng.** A single legend swatch. -/ -def colorSwatch (c : ProofAtlas.Mapping.ExprColor) : Html := - <span style={json% { - display: "inline-flex", - alignItems: "center", - gap: "4px", - marginRight: "12px", - fontSize: "11px", - fontFamily: "var(--vscode-editor-font-family, monospace)" - }}> - <span style={json% { - display: "inline-block", - width: "12px", - height: "8px", - borderRadius: "2px", - backgroundColor: $(exprColorCss c), - border: "1px solid var(--vscode-editor-foreground)" - }} /> - {.text (exprColorName c)} - </span> - -/-- **Eng.** Horizontal colour legend rendered below the graph. -/ -def colorLegendHtml : Html := - <div style={json% { - padding: "6px 8px", - borderTop: "1px solid var(--vscode-editorWidget-border, #444)", - display: "flex", - flexWrap: "wrap", - alignItems: "center" - }}> - {.element "span" - #[("style", json% { - fontSize: "11px", - fontWeight: "bold", - marginRight: "8px", - opacity: "0.7" - })] - #[.text "edge colour:"]} - {.element "span" #[] (hyperedgeColors.map colorSwatch).toArray} - </div> - -/-! ## Decl-level colour legend -/ - -/-- **Eng.** Legend swatch for one `DeclColor`. -/ -def declColorSwatch (c : DeclColor) : Html := - <span style={json% { - display: "inline-flex", - alignItems: "center", - gap: "4px", - marginRight: "12px", - fontSize: "11px", - fontFamily: "var(--vscode-editor-font-family, monospace)" - }}> - <span style={json% { - display: "inline-block", - width: "10px", - height: "10px", - borderRadius: "50%", - backgroundColor: $(c.css), - border: "1px solid var(--vscode-editor-foreground)" - }} /> - {.text c.name} - </span> - -/-- **Eng.** Horizontal colour legend for decl-level graphs: each -swatch is a circle filled with the corresponding `DeclColor`'s -CSS, matching the vertex fill in the widget. -/ -def declColorLegendHtml : Html := - <div style={json% { - padding: "6px 8px", - borderTop: "1px solid var(--vscode-editorWidget-border, #444)", - display: "flex", - flexWrap: "wrap", - alignItems: "center" - }}> - {.element "span" - #[("style", json% { - fontSize: "11px", - fontWeight: "bold", - marginRight: "8px", - opacity: "0.7" - })] - #[.text "vertex colour:"]} - {.element "span" #[] (DeclColor.palette.map declColorSwatch).toArray} - </div> - -end ProofAtlas.Pipeline diff --git a/lean/ProofAtlas/Util/Audit.lean b/lean/ProofAtlas/Util/Audit.lean deleted file mode 100644 index 554c46a..0000000 --- a/lean/ProofAtlas/Util/Audit.lean +++ /dev/null @@ -1,166 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import Lean - -/-! -# `lake exe proofatlas-audit` - -One-command health check for the `ProofAtlas` library. Performs: - -1. **Public-API listing.** Prints the canonical paper-facing - definitions and theorems and confirms each one is present in the - imported environment. A missing declaration is a hard failure. -2. **Foundational-axiom audit.** Walks the proof terms of the apex - public theorems via `Environment.find?` + `Expr.foldConsts`, - transitively collecting every `axiomInfo` reachable from the proof. - Confirms that the only foundational axioms reached are - `propext`, `Classical.choice`, and `Quot.sound`. Any other axiom - (notably `sorryAx`) causes a non-zero exit. - -The exit code is `0` on success and `1` on any audit failure. -Intended for both interactive use (developer one-shot check) and -CI invocation. - -While the library is under active development with `sorry` -placeholders in the proof terms of every apex theorem, audit will -report `sorryAx` for every theorem and exit non-zero. This is the -intended behaviour: audit becomes a green check exactly when the -last `sorry` is filled. - -This file deliberately avoids importing `ProofAtlas` statically: -that would force the audit executable to link the entire transitive -Mathlib closure, which exceeds macOS's `ARG_MAX` linker argument -limit. Instead, target names are constructed as `Name` literals and -the library is loaded at runtime via `importModules`. --/ - -open Lean - -/-- Build a `Name` of the form `Hypergraph.<s>` without needing -the corresponding constant in scope. -/ -private def bh (s : String) : Name := - .str (.str .anonymous "Hypergraph") s - -/-- Foundational axioms expected for every public theorem. -/ -private def expectedAxioms : NameSet := - NameSet.empty - |>.insert ``propext - |>.insert ``Classical.choice - |>.insert ``Quot.sound - -/-- Walk the environment from `name`, accumulating every constant of -kind `axiomInfo` reachable through `value?` references. -/ -private partial def collectAxiomDeps (env : Environment) - (name : Name) (visited : NameSet := {}) (acc : NameSet := {}) : - NameSet × NameSet := - if visited.contains name then - (visited, acc) - else - let visited := visited.insert name - match env.find? name with - | none => (visited, acc) - | some (.axiomInfo _) => (visited, acc.insert name) - | some info => - let body := info.value?.getD (.const name []) - body.foldConsts (init := (visited, acc)) - fun n (v, a) => collectAxiomDeps env n v a - -/-- Audit one named theorem. Returns `true` on success. -/ -private def auditTheorem (env : Environment) (thm : Name) : IO Bool := do - let (_, axs) := collectAxiomDeps env thm - let unexpected := axs.toList.filter (fun a => !expectedAxioms.contains a) - if unexpected.isEmpty then - IO.println s!" ✓ {thm}" - return true - else - IO.println s!" ✗ {thm} — unexpected axioms: {unexpected}" - return false - -/-- Public definitions: paper-facing types and constructions. -/ -private def publicDefs : List (Name × String) := - [ (`Hypergraph, "structure (BDF proof hypergraph)") - , (`WalkParams, "structure (colour weight + backward penalty)") - , (bh "roleWeight", "def (role weight w(v, e))") - , (bh "normalizer", "def (per-vertex normaliser Z(v))") - , (bh "harmonicNorm", "def (harmonic input normaliser S_e)") - , (bh "transProb", "def (transition probability p(v, u))") - , (bh "hittingTime", "def (expected first-passage time h(u, v))") - , (bh "commuteTimeDist", "def (commute time d(u, v) = h(u, v) + h(v, u))") - , (bh "hyperbolicityConstant", "def (Gromov hyperbolicity constant)") - , (bh "IsTrivial", "def (every hyperedge is (1, 1)-shaped)") ] - -/-- Apex public theorems audited for axiom purity. -/ -private def publicTheorems : List (Name × String) := - [ (bh "transProb_nonneg", - "theorem (p(v, u) ≥ 0)") - , (bh "transProb_sum_one", - "theorem (∑_u p(v, u) = 1)") - , (bh "hittingTime_self", - "theorem (h(v, v) = 0)") - , (bh "hittingTime_pos", - "theorem (h(u, v) > 0 when u ≠ v)") - , (bh "hittingTime_triangle", - "theorem (h(u, v) ≤ h(u, w) + h(w, v))") - , (bh "commuteTimeDist_self", - "theorem (d(v, v) = 0)") - , (bh "commuteTimeDist_pos", - "theorem (d(u, v) > 0 when u ≠ v)") - , (bh "commuteTimeDist_symm", - "theorem (d(u, v) = d(v, u))") - , (bh "commuteTimeDist_triangle", - "theorem (d(u, w) ≤ d(u, v) + d(v, w))") - , (bh "gromov_hyperbolic", - "theorem (four-point Gromov inequality)") - , (bh "hyperbolicity_mono_beta", - "theorem (monotonicity of hyperbolicity in β)") - , (bh "hyperbolicity_vanishes_at_zero", - "theorem (hyperbolicity → 0 as β → 0⁺)") ] - -def main : IO UInt32 := do - Lean.initSearchPath (← Lean.findSysroot) - let env ← Lean.importModules - #[(`ProofAtlas : Import)] {} (trustLevel := 0) - - IO.println "ProofAtlas library audit" - IO.println "===========================" - IO.println "" - - let mut allOk := true - - IO.println "Public definitions:" - for (n, kind) in publicDefs do - if env.contains n then - IO.println s!" • {n}\n {kind}" - else - IO.println s!" ✗ {n} — NOT FOUND in environment" - allOk := false - IO.println "" - - IO.println "Public theorems:" - for (n, kind) in publicTheorems do - if env.contains n then - IO.println s!" • {n}\n {kind}" - else - IO.println s!" ✗ {n} — NOT FOUND in environment" - allOk := false - IO.println "" - - IO.println "Foundational-axiom audit:" - for (thm, _) in publicTheorems do - if env.contains thm then - let ok ← auditTheorem env thm - if !ok then allOk := false - IO.println "" - - if allOk then - IO.println "All apex public theorems depend only on:" - IO.println " propext, Classical.choice, Quot.sound" - IO.println "" - IO.println "Library status: ✓ healthy" - return 0 - else - IO.println "Library status: ✗ audit FAILED" - return 1 diff --git a/lean/ProofAtlas/Util/Linter/MetricRegistry.lean b/lean/ProofAtlas/Util/Linter/MetricRegistry.lean deleted file mode 100644 index 4aa71a7..0000000 --- a/lean/ProofAtlas/Util/Linter/MetricRegistry.lean +++ /dev/null @@ -1,88 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import Lean - -/-! -# `#guard_metric_registry` — compile-time metric instance audit - -Replaces the shell script `check_metric_instances.sh` with a Lean -command that inspects the `Environment` directly. At elaboration -time, it verifies that every registered metric has an -`IsHypergraphMetric` instance theorem, and reports whether each -instance is sorry-free or still contains `sorryAx`. - -Place the command in a file that imports all metric instance modules -(e.g. `Metric/Default.lean`). If any registered metric is missing -its instance theorem entirely, elaboration fails — the build goes -red, CI blocks the PR. --/ - -namespace ProofAtlas.Linter.MetricRegistry - -open Lean Elab Command - -/-- **Eng.** Check whether the proof term of `name` depends on -`sorryAx`, either directly or transitively. -/ -private partial def dependsOnSorry (env : Environment) (name : Name) - (visited : NameSet := {}) : Bool × NameSet := - if visited.contains name then (false, visited) - else - let visited := visited.insert name - if name == ``sorryAx then (true, visited) - else match env.find? name with - | none => (false, visited) - | some info => - let body? := match info with - | .thmInfo val => some val.value - | _ => info.value? - match body? with - | none => (false, visited) - | some body => - let consts := body.foldConsts (init := #[]) fun n acc => acc.push n - consts.foldl (init := (false, visited)) fun (found, vis) n => - if found then (true, vis) - else dependsOnSorry env n vis - -/-- **Eng.** The metric registry: each entry is a pair of -(metric name for display, fully qualified instance theorem name). -/ -private def metricRegistry : Array (String × Name) := - #[ ("resistanceDist", `Hypergraph.resistanceDist_isHypergraphMetric) - ] - -/-- **Eng.** Syntax for the compile-time metric registry guard. -/ -syntax (name := guardMetricRegistryCmd) "#guard_metric_registry" : command - -@[command_elab guardMetricRegistryCmd] -def elabGuardMetricRegistry : CommandElab := fun _stx => do - let env ← getEnv - let mut missing : Array String := #[] - let mut sorry'd : Array String := #[] - let mut proven : Array String := #[] - for (display, thm) in metricRegistry do - match env.find? thm with - | none => - missing := missing.push display - | some _ => - let (hasSorry, _) := dependsOnSorry env thm - if hasSorry then - sorry'd := sorry'd.push display - else - proven := proven.push display - let mut lines : Array String := #[] - for m in proven do - lines := lines.push s!" ✓ proven: {m}_isHypergraphMetric" - for m in sorry'd do - lines := lines.push s!" ⚠ sorry: {m}_isHypergraphMetric" - for m in missing do - lines := lines.push s!" ✗ missing: {m}_isHypergraphMetric" - lines := lines.push s!"summary: {missing.size} missing, {sorry'd.size} sorry'd, {proven.size} proven" - let report := "\n".intercalate lines.toList - if missing.size > 0 then - throwError "metric registry audit FAILED:\n{report}" - else - logInfo m!"{report}" - -end ProofAtlas.Linter.MetricRegistry diff --git a/lean/lakefile.toml b/lean/lakefile.toml index 853e9ef..ed63858 100644 --- a/lean/lakefile.toml +++ b/lean/lakefile.toml @@ -17,15 +17,3 @@ leanOptions = [ { name = "weak.linter.mathlibStandardSet", value = true }, { name = "weak.linter.proofAtlasMathTag", value = true }, ] - -[[lean_exe]] -name = "proofatlas-audit" -root = "ProofAtlas.Util.Audit" - -[[lean_exe]] -name = "atlas-export-demos" -root = "ProofAtlas.Export.Demos" - -[[lean_exe]] -name = "atlas-export" -root = "ProofAtlas.Export.Profile" diff --git a/lean/scripts/check_metric_instances.sh b/lean/scripts/check_metric_instances.sh index 5932486..c05adbf 100755 --- a/lean/scripts/check_metric_instances.sh +++ b/lean/scripts/check_metric_instances.sh @@ -10,9 +10,15 @@ set -euo pipefail # (metric, instance file) registry. -METRICS=( - "resistanceDist:ProofAtlas/Metric/Resistance/Instance.lean" -) +METRICS=() +# No metrics on main yet. Add back as they are promoted from development. +if [ ${#METRICS[@]} -eq 0 ]; then + echo "Hypergraph metric instance report" + echo "======================================" + echo "Summary: 0 missing, 0 sorry'd, 0 proven." + echo "OK: 0 metric(s) sorry-free." + exit 0 +fi MISSING=() UNPROVEN=() diff --git a/scripts/lint.sh b/scripts/lint.sh index df05b6e..d930aca 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -47,9 +47,10 @@ echo "[3/4] Sorry regression check" BASELINE_FILE="$LEAN/.sorry-baseline" if [ -f "$BASELINE_FILE" ]; then BASELINE=$(cat "$BASELINE_FILE" | tr -d '[:space:]') - CURRENT=$(grep -rn '^\s*sorry' "$LEAN/ProofAtlas/" --include="*.lean" \ - | grep -v 'sorryAx\|sorry`\|"sorry"\|sorry_\|sorry-free' \ + CURRENT=$(set +o pipefail; grep -rn '^\s*sorry' "$LEAN/ProofAtlas/" --include="*.lean" 2>/dev/null \ + | grep -v 'sorryAx\|sorry`\|"sorry"\|sorry_\|sorry-free' 2>/dev/null \ | wc -l | tr -d '[:space:]') + CURRENT=${CURRENT:-0} if [ "$CURRENT" -gt "$BASELINE" ]; then echo " ✗ sorry count increased: $BASELINE → $CURRENT" echo " If intentional, update $BASELINE_FILE and add a tracking issue." From 48e32aa37b58d50d24c7fda20bec92be4c6b24f0 Mon Sep 17 00:00:00 2001 From: Xinze-Li-Moqian <70414198+Xinze-Li-Moqian@users.noreply.github.com> Date: Wed, 27 May 2026 00:13:12 -0400 Subject: [PATCH 04/21] chore: add pre-push hook + update CONTRIBUTING.md - .githooks/pre-push: runs lint.sh before every push - CONTRIBUTING.md: rewritten for main/development branch strategy, setup instructions include hook activation --- .githooks/pre-push | 6 ++ CONTRIBUTING.md | 149 +++++++++------------------------------------ 2 files changed, 36 insertions(+), 119 deletions(-) create mode 100755 .githooks/pre-push diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100755 index 0000000..e8d4068 --- /dev/null +++ b/.githooks/pre-push @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# Pre-push hook: runs the same lint.sh that CI runs. +# Blocks push if any check fails. + +echo "Running pre-push lint check..." +bash "$(git rev-parse --show-toplevel)/scripts/lint.sh" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 61c02e8..608a2dc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,137 +1,48 @@ # Contributing to ProofAtlas -## Using ProofAtlas in your own Lean 4 project +## Setup -Add to your `lakefile.toml`: - -```toml -[[require]] -name = "proofatlas" -git = "https://github.com/MathNetwork/ProofAtlas" -rev = "main" -subDir = "lean" +```bash +git clone https://github.com/MathNetwork/ProofAtlas.git +cd ProofAtlas +git config core.hooksPath .githooks +cd lean +lake exe cache get +lake build ``` -Then in any `.lean` file: +The `git config` line enables the pre-push hook, which runs +`scripts/lint.sh` before every push — same checks as CI. -```lean -import ProofAtlas +## Branch strategy -#atlas.graph Nat.add_zero -#atlas.resistance Nat.add_zero Nat.succ_eq_add_one ``` - -`lake update && lake build` and the `#atlas.*` commands work in your -infoview. Your `lean-toolchain` should match the one in -`lean/lean-toolchain` of this repo. - -For a local checkout, use the path form instead: - -```toml -[[require]] -name = "proofatlas" -path = "/absolute/path/to/hypergraph-geo/lean" +main ← production, sorry-free, only accepts PRs from development +development ← research, sorry allowed, accepts PRs from feature branches +feat/xxx ← your work, cut from development ``` -## Exporting geometric reports +1. `git checkout development && git checkout -b feat/your-topic` +2. Write code, commit +3. `git push origin feat/your-topic` +4. Open PR → `development` (not main) +5. CI passes → merge +6. When development has verified work ready for production: PR `development` → `main` + +## Before pushing -For shell pipelines, CI snapshots, or external analysis, dump a -declaration's geometric report as JSON via the `atlas-export` binary: +The pre-push hook runs `bash scripts/lint.sh` automatically. You +can also run it manually: ```bash -lake exe atlas-export Nat.add_zero -lake exe atlas-export Nat.add_zero Nat.add_comm # glued -lake exe atlas-export --out report.json Nat.succ_eq_add_one +bash scripts/lint.sh ``` -Without `--out` the JSON goes to stdout, so it pipes into -anything. The schema matches `Pipeline/Export.lean`'s -`ToJson HypergraphReport`: incidence data, resistance matrix, -δ-hyperbolicity, and root indices (one per input declaration). - -The companion binary `lake exe atlas-export-demos` writes the -landing-page demo set under `site/data/atlas-demos.json`; re-run -it whenever the seed declarations or the pipeline change shape. - -## Architecture Decision Records - -Some choices in the codebase aren't obvious from the diff alone — -e.g. *why* we picked Mathlib's `Digraph` over `Quiver`, or *which* -2-section convention `toSimpleGraph` uses. Those choices live in -`docs/adr/` so they survive a change of maintainer. - -You don't need an ADR for every PR. Write one when your change -involves any of these: - -- **Choosing an external dependency or target type** for a new - public API (e.g. "use type `X` from package `Y` here"). -- **Defining new public-API semantics** where two reasonable - readings exist (e.g. "full 2-section vs input-output 2-section"). -- **Naming or layout conventions** that affect more than one file. -- **Cross-file architectural decisions** (layer boundaries, - spec-vs-instance splits, new directory conventions). - -Bug fixes, refactors that preserve the public API, and single-file -implementations with self-contained reasoning don't need an ADR. - -To add one: copy `docs/adr/template.md` to -`docs/adr/NNNN-your-title.md`, fill it in, link it from the PR. The -template and the trigger list are explained in detail in -[`docs/adr/README.md`](docs/adr/README.md). - -## Where to read next - -- **First-time user?** Run through the - [tutorial](docs/tutorial.md) (5 minutes, end-to-end). -- **Want the big picture?** The - [architecture document](docs/architecture.md) explains the four - layers and what each one owns. -- **Want to add a feature?** Two recipes: - - [How to add a new mapping](docs/how-to/add-a-mapping.md) — - extract a hypergraph from some Lean object. - - [How to add a new metric](docs/how-to/add-a-metric.md) — - register a new `IsHypergraphMetric` instance. -- **Want to track progress?** [STATUS.md](docs/STATUS.md) is the - transparent checklist; [ROADMAP.md](docs/ROADMAP.md) is the - forward direction. - -The short version of "how to add a new metric": - -1. Define your metric in `ProofAtlas/Metric/YourMetric/Basic.lean` -2. Prove `IsHypergraphMetric` in `ProofAtlas/Metric/YourMetric/Instance.lean` -3. Register the metric in `scripts/check_metric_instances.sh` -4. Run `#atlas.status yourMetricDist` — it should show ✓ -5. CI will check automatically - -## Open tasks - -For scoped, beginner-friendly entry points, see the -[good first issue tracker](https://github.com/MathNetwork/ProofAtlas/labels/good%20first%20issue). -Each one is sized for under a day's work and covers documentation, -code, or infrastructure. - -The list below is the longer-horizon work that doesn't yet have a -specific issue. Each task is self-contained — pick one and open a -PR. - -### IsHypergraphMetric instances (discharge `sorry`) -- [ ] `jsDist_isHypergraphMetric` — needs JS triangle inequality (Endres–Schindelin 2003) -- [ ] `diffusionDist_isHypergraphMetric` — needs diffusion metric axioms +## Architecture decisions -### Float compute paths (new) -- [ ] Float commute time in `Pipeline/` — enables `#atlas.commute` -- [ ] Float JS distance in `Pipeline/` — enables `#atlas.jsd` -- [ ] Float eigensolver in `Pipeline/` — enables `#atlas.spectrum` and `#atlas.cluster` +Write an ADR in `docs/adr/` for cross-file architectural changes. +See [`docs/adr/README.md`](docs/adr/README.md). -### Command language (new) -- [ ] `#atlas.commute` — compute commute time distance -- [ ] `#atlas.jsd` — compute JS distance -- [ ] `#atlas.diffusion` — compute diffusion distance at scale `t` -- [ ] `#atlas.spectrum` — spectral decomposition of `L_κ` -- [ ] `#atlas.cluster` — spectral clustering -- [ ] `#atlas.delta.ns!` — Monte-Carlo δ sampler for large namespaces +## License -### Documentation -- [ ] Metrics page: intuition + math + Lean code for each metric -- [ ] Theory page: δ-hyperbolicity, spectral decomposition, Hausdorff, GH -- [ ] How-to: fill an open `sorry` +Apache 2.0 (see `LICENSE`). From 8b3cc836d6c391e4b5ae5badcfa63bc2a48c3a04 Mon Sep 17 00:00:00 2001 From: Xinze-Li-Moqian <70414198+Xinze-Li-Moqian@users.noreply.github.com> Date: Wed, 27 May 2026 00:17:47 -0400 Subject: [PATCH 05/21] refactor: remove BDF naming from Hypergraph definition Hypergraph is a general mathematical structure, not tied to any specific paper. Replace "BDF hypergraph" with "proof hypergraph" in docstrings. Remove paper references from the definition file. --- lean/ProofAtlas/Hypergraph/Basic.lean | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/lean/ProofAtlas/Hypergraph/Basic.lean b/lean/ProofAtlas/Hypergraph/Basic.lean index 0266ccb..8742e0d 100644 --- a/lean/ProofAtlas/Hypergraph/Basic.lean +++ b/lean/ProofAtlas/Hypergraph/Basic.lean @@ -8,12 +8,12 @@ import Mathlib.Logic.Relation import Mathlib.Logic.Equiv.Defs /-! -# BDF hypergraph +# Proof hypergraph -Faithful Lean encoding of the directed, ordered, coloured, acyclic -hypergraph of Barkeshli–Douglas–Freedman (BDF26 §2.1 Definition 1.1). +A directed, ordered, coloured, acyclic hypergraph — the +combinatorial structure that ProofAtlas extracts from formal proofs. -The four BDF axioms map onto the structure as follows. +Four structural axioms: - **Directed.** `Hyperedge.inVertex` / `outVertex` are separate fields; `inputOutputDisjoint` forbids overlap within one edge. @@ -22,18 +22,13 @@ The four BDF axioms map onto the structure as follows. - **Coloured.** `Hyperedge.color : C`. - **Acyclic.** `Hypergraph.acyclic`: the transitive closure of the input→output relation `stepRel` is irreflexive. - -## References - -* Barkeshli, Douglas, Freedman, *Artificial Intelligence and the - Structure of Mathematics*, arXiv:2604.06107, §2.1 Definition 1.1. -/ universe u v w /-! ## Hyperedge -/ -/-- **Math.** One hyperedge `e = (e^{in}; e^{out})` of a BDF +/-- **Math.** One hyperedge `e = (e^{in}; e^{out})` of a proof hypergraph: ordered input/output tuples plus a colour. -/ structure Hyperedge (V : Type u) (C : Type v) where /-- Premise count `p_e`. -/ @@ -138,16 +133,16 @@ end Hypergraph /-! ## Hypergraph -/ -/-- **Math.** A *BDF hypergraph* `H = (V, E, c)`: a directed, -ordered, coloured, acyclic hypergraph (BDF26 §2.1 Definition 1.1). -Per-edge data (D, O, C) lives in `Hyperedge`; the cross-edge axiom -A is the `acyclic` field. -/ +/-- **Math.** A *proof hypergraph* `H = (V, E, c)`: a directed, +ordered, coloured, acyclic hypergraph. Per-edge data (D, O, C) +lives in `Hyperedge`; the cross-edge axiom A is the `acyclic` +field. -/ structure Hypergraph (V : Type u) (C : Type v) where /-- Type of hyperedges. -/ edges : Type w /-- Per-edge data: ordered input/output tuples and colour. -/ edge : edges → Hyperedge V C - /-- BDF26 axiom A: no proposition is its own descendant. -/ + /-- Axiom A: no proposition is its own descendant. -/ acyclic : Hypergraph.IsAcyclic edge /-- The hypergraph is connected: any two vertices are linked by an undirected path through hyperedge incidence. Two vertices are From a83ad8cee9ddefa19f948a082c36f455fe7278ad Mon Sep 17 00:00:00 2001 From: Xinze-Li-Moqian <70414198+Xinze-Li-Moqian@users.noreply.github.com> Date: Wed, 27 May 2026 00:24:42 -0400 Subject: [PATCH 06/21] refactor: replace Hypergraph code with design spec only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Main now contains only a design specification document for the proof hypergraph — no Lean code. The definition is being redesigned on development (vertices as first-class objects, not bare type parameters). Build: 6 jobs, 0 dependencies, 0 sorry. --- lean/ProofAtlas.lean | 2 +- lean/ProofAtlas/Hypergraph/Basic.lean | 276 +++----------------------- 2 files changed, 29 insertions(+), 249 deletions(-) diff --git a/lean/ProofAtlas.lean b/lean/ProofAtlas.lean index 2f36b6d..b853dfd 100644 --- a/lean/ProofAtlas.lean +++ b/lean/ProofAtlas.lean @@ -10,7 +10,7 @@ import ProofAtlas.Hypergraph.Basic /-! # `ProofAtlas` — main branch -The BDF proof hypergraph definition. Everything else is built on +Proof hypergraph specification + linters. Everything else is built on the `development` branch and merged here as it reaches production quality (sorry-free, reviewed, tested). See ADR 0007. -/ diff --git a/lean/ProofAtlas/Hypergraph/Basic.lean b/lean/ProofAtlas/Hypergraph/Basic.lean index 8742e0d..ce417cb 100644 --- a/lean/ProofAtlas/Hypergraph/Basic.lean +++ b/lean/ProofAtlas/Hypergraph/Basic.lean @@ -3,262 +3,42 @@ Copyright (c) 2026 MathNetwork. All rights reserved. Released under Apache 2.0 license as described in the file LICENSE. Authors: MathNetwork -/ -import ProofAtlas.Util.Linter.MathTag -import Mathlib.Logic.Relation -import Mathlib.Logic.Equiv.Defs /-! -# Proof hypergraph +# Proof hypergraph — design specification -A directed, ordered, coloured, acyclic hypergraph — the -combinatorial structure that ProofAtlas extracts from formal proofs. +This file specifies the proof hypergraph: the combinatorial +structure that ProofAtlas extracts from formal proofs. -Four structural axioms: +## Design intent -- **Directed.** `Hyperedge.inVertex` / `outVertex` are separate - fields; `inputOutputDisjoint` forbids overlap within one edge. -- **Ordered.** `inVertex : Fin inArity → V` (similarly outputs): - positions live in the type. -- **Coloured.** `Hyperedge.color : C`. -- **Acyclic.** `Hypergraph.acyclic`: the transitive closure of - the input→output relation `stepRel` is irreflexive. --/ - -universe u v w - -/-! ## Hyperedge -/ - -/-- **Math.** One hyperedge `e = (e^{in}; e^{out})` of a proof -hypergraph: ordered input/output tuples plus a colour. -/ -structure Hyperedge (V : Type u) (C : Type v) where - /-- Premise count `p_e`. -/ - inArity : Nat - /-- Premise tuple. -/ - inVertex : Fin inArity → V - /-- Conclusion count `q_e`. -/ - outArity : Nat - /-- Conclusion tuple. -/ - outVertex : Fin outArity → V - /-- Inference-rule label. -/ - color : C - /-- At least one premise. -/ - inArity_pos : 0 < inArity - /-- At least one conclusion. -/ - outArity_pos : 0 < outArity - /-- Premises and conclusions of one edge are disjoint vertices. -/ - inputOutputDisjoint : ∀ (i : Fin inArity) (j : Fin outArity), - inVertex i ≠ outVertex j - /-- Premises of one edge are pairwise distinct: lets the harmonic - schedule `α(c) / ((i+1)·q)` be a function of the input vertex. -/ - inVertex_injective : Function.Injective inVertex - /-- Conclusions of one edge are pairwise distinct: required for the - paper §3.3 identity `(1/2)·∑ c_κ(f-f)² = E_H(f)`. Without output - distinctness, the per-edge energy `E_H` overcounts coincident - outputs that the indicator-based `c_κ` cannot see. -/ - outVertex_injective : Function.Injective outVertex - -namespace Hyperedge - -variable {V : Type u} {C : Type v} - -/-- **Math.** `v` is a premise of `he`. -/ -def hasInput (he : Hyperedge V C) (v : V) : Prop := - ∃ i : Fin he.inArity, he.inVertex i = v - -/-- **Math.** `v` is a conclusion of `he`. -/ -def hasOutput (he : Hyperedge V C) (v : V) : Prop := - ∃ j : Fin he.outArity, he.outVertex j = v - -/-- **Math.** Push a hyperedge through a vertex bijection: apply `φ` -to every input and output vertex, keeping arities and colour fixed. -The result is a well-formed hyperedge because `φ` is injective. -/ -def equivMap (φ : V ≃ V) (he : Hyperedge V C) : Hyperedge V C where - inArity := he.inArity - inVertex := φ ∘ he.inVertex - outArity := he.outArity - outVertex := φ ∘ he.outVertex - color := he.color - inArity_pos := he.inArity_pos - outArity_pos := he.outArity_pos - inputOutputDisjoint := by - intro i j - simp only [Function.comp_apply] - exact fun h => he.inputOutputDisjoint i j (φ.injective h) - inVertex_injective := φ.injective.comp he.inVertex_injective - outVertex_injective := φ.injective.comp he.outVertex_injective - -end Hyperedge - -/-- `v ∈ᵢ he` ↔ `v` is an input of hyperedge `he`. -/ -notation:50 v:51 " ∈ᵢ " he:51 => Hyperedge.hasInput he v - -/-- `v ∈ₒ he` ↔ `v` is an output of hyperedge `he`. -/ -notation:50 v:51 " ∈ₒ " he:51 => Hyperedge.hasOutput he v - -/-! ## Reachability and acyclicity -/ - -namespace Hypergraph - -variable {V : Type u} {C : Type v} - --- doc:start stepRel -/-- **Math.** One-step inference: some edge has `v` as a premise and -`w` as a conclusion. -/ -def stepRel {edges : Type w} (edge : edges → Hyperedge V C) (v w : V) : Prop := - ∃ e : edges, v ∈ᵢ edge e ∧ w ∈ₒ edge e - -/-- **Math.** No vertex reaches itself via iterated inference. -/ -def IsAcyclic {edges : Type w} (edge : edges → Hyperedge V C) : Prop := - ∀ v : V, ¬ Relation.TransGen (stepRel edge) v v --- doc:end - --- doc:start IsAcyclic.of_natRank -/-- **Math.** Topological-sort recipe: a strictly increasing -`Nat`-rank along `stepRel` discharges `IsAcyclic`. -/ -theorem IsAcyclic.of_natRank {edges : Type w} - {edge : edges → Hyperedge V C} (rank : V → Nat) - (h_step : ∀ v w, stepRel edge v w → rank v < rank w) : - IsAcyclic edge := by --- doc:end - have h_trans : ∀ {a b : V}, - Relation.TransGen (stepRel edge) a b → rank a < rank b := by - intro a b hab - induction hab with - | single hr => exact h_step _ _ hr - | tail _ hr ih => exact Nat.lt_trans ih (h_step _ _ hr) - intro v hv - exact Nat.lt_irrefl _ (h_trans hv) - -end Hypergraph - -/-! ## Hypergraph -/ - -/-- **Math.** A *proof hypergraph* `H = (V, E, c)`: a directed, -ordered, coloured, acyclic hypergraph. Per-edge data (D, O, C) -lives in `Hyperedge`; the cross-edge axiom A is the `acyclic` -field. -/ -structure Hypergraph (V : Type u) (C : Type v) where - /-- Type of hyperedges. -/ - edges : Type w - /-- Per-edge data: ordered input/output tuples and colour. -/ - edge : edges → Hyperedge V C - /-- Axiom A: no proposition is its own descendant. -/ - acyclic : Hypergraph.IsAcyclic edge - /-- The hypergraph is connected: any two vertices are linked by an - undirected path through hyperedge incidence. Two vertices are - *incident* to a hyperedge if either is an input or an output of - that edge. -/ - connected : ∀ (u v : V), Relation.ReflTransGen - (fun a b => ∃ e : edges, - ((∃ i, (edge e).inVertex i = a) ∨ (∃ j, (edge e).outVertex j = a)) ∧ - ((∃ i, (edge e).inVertex i = b) ∨ (∃ j, (edge e).outVertex j = b))) u v +A proof hypergraph is a directed, ordered, coloured, acyclic +hypergraph where: -namespace Hypergraph +- **Vertices** are explicit objects with identity and kind + (proposition, term, inference step, etc.), not a bare type + parameter. +- **Hyperedges** represent inference steps: ordered inputs + (premises) → ordered outputs (conclusions), labelled with a + colour (inference rule type). +- **Acyclicity** ensures the graph represents a valid derivation. +- **Connectivity** ensures the graph is one connected component. -variable {V : Type u} {C : Type v} +## What needs to be defined -/-! ### Flat projections (route through `H.edge e`) -/ +1. `Vertex` — a structure with id, kind, and optional metadata. +2. `Hyperedge` — ordered inputs/outputs referencing vertices, + with colour label. Inputs/outputs disjoint and injective. +3. `Hypergraph` — a collection of vertices and edges satisfying + acyclicity and connectivity. +4. Incidence predicates, reachability, automorphisms. -/-- **Math.** Input arity of `e`. -/ -abbrev inArity (H : Hypergraph V C) (e : H.edges) : Nat := - (H.edge e).inArity +## Current status -/-- **Math.** Output arity of `e`. -/ -abbrev outArity (H : Hypergraph V C) (e : H.edges) : Nat := - (H.edge e).outArity +This definition is being redesigned on the `development` branch. +The previous implementation used a bare type parameter `V` for +vertices, which prevented vertices from carrying any structural +information. The new design will make vertices first-class objects. -/-- **Math.** `i`-th input vertex of `e`. -/ -abbrev inVertex (H : Hypergraph V C) (e : H.edges) - (i : Fin (H.inArity e)) : V := - (H.edge e).inVertex i - -/-- **Math.** `j`-th output vertex of `e`. -/ -abbrev outVertex (H : Hypergraph V C) (e : H.edges) - (j : Fin (H.outArity e)) : V := - (H.edge e).outVertex j - -/-- **Math.** Colour of `e`. -/ -abbrev color (H : Hypergraph V C) (e : H.edges) : C := - (H.edge e).color - -/-- **Math.** Each edge has at least one input. -/ -theorem inArity_pos (H : Hypergraph V C) (e : H.edges) : - 0 < H.inArity e := - (H.edge e).inArity_pos - -/-- **Math.** Each edge has at least one output. -/ -theorem outArity_pos (H : Hypergraph V C) (e : H.edges) : - 0 < H.outArity e := - (H.edge e).outArity_pos - -/-- **Math.** Inputs and outputs of one edge are disjoint. -/ -theorem inputOutputDisjoint (H : Hypergraph V C) (e : H.edges) - (i : Fin (H.inArity e)) (j : Fin (H.outArity e)) : - H.inVertex e i ≠ H.outVertex e j := - (H.edge e).inputOutputDisjoint i j - -/-- **Math.** Inputs of one edge are pairwise distinct. -/ -theorem inVertex_injective (H : Hypergraph V C) (e : H.edges) : - Function.Injective (H.inVertex e) := - (H.edge e).inVertex_injective - -/-- **Math.** Outputs of one edge are pairwise distinct. -/ -theorem outVertex_injective (H : Hypergraph V C) (e : H.edges) : - Function.Injective (H.outVertex e) := - (H.edge e).outVertex_injective - -/-! ### Incidence -/ - -/-- **Math.** `v` is an input of `e` in `H`. -/ -def isInput (H : Hypergraph V C) (v : V) (e : H.edges) : Prop := - v ∈ᵢ H.edge e - -/-- **Math.** `v` is an output of `e` in `H`. -/ -def isOutput (H : Hypergraph V C) (v : V) (e : H.edges) : Prop := - v ∈ₒ H.edge e - -/-- **Math.** `v` is incident to `e` (input or output). -/ -def incident (H : Hypergraph V C) (v : V) (e : H.edges) : Prop := - H.isInput v e ∨ H.isOutput v e - -/-! ### Reachability -/ - -/-- **Math.** `v` reaches `w` via iterated inference in `H`. -/ -def reachable (H : Hypergraph V C) (v w : V) : Prop := - Relation.TransGen (stepRel H.edge) v w - -/-- `v ⤳[H] w` ↔ `v` reaches `w` via iterated inference in `H`. -/ -notation:50 v:51 " ⤳[" H "] " w:51 => Hypergraph.reachable H v w - -/-- **Math.** A `Hypergraph` admits no self-reachability. -/ -theorem not_reachable_self (H : Hypergraph V C) (v : V) : - ¬ v ⤳[H] v := - H.acyclic v - -/-! ### Automorphism and sub-hypergraph -/ - -/-- **Math.** An automorphism of `H`: a vertex bijection lifting to -an edge bijection, with matching per-edge data (pushed through the -vertex bijection via `Hyperedge.equivMap`). -/ -structure Aut (H : Hypergraph V C) where - /-- Vertex permutation. -/ - vertexEquiv : V ≃ V - /-- Edge permutation induced by the vertex permutation. -/ - edgeEquiv : H.edges ≃ H.edges - /-- Compatibility: pushing `H.edge e` through `vertexEquiv` recovers - the hyperedge data at `edgeEquiv e`. -/ - compat : ∀ e : H.edges, - H.edge (edgeEquiv e) = (H.edge e).equivMap vertexEquiv - -/-- **Math.** `H'` is a *sub-hypergraph* of `H`: an injection of -`H'.edges` into `H.edges` preserving per-edge data. Both share the -same vertex type `V` and colour type `C`. -/ -structure IsSubHypergraph (H' H : Hypergraph V C) where - /-- Edge inclusion. -/ - toFun : H'.edges → H.edges - /-- The inclusion is injective. -/ - injective : Function.Injective toFun - /-- The included edge has the same per-edge data. -/ - edge_eq : ∀ e : H'.edges, H.edge (toFun e) = H'.edge e - -end Hypergraph +See the `development` branch for work in progress. +-/ From 6fb88fd2f44cb1ff83663f0fee9c5ec04a6b4129 Mon Sep 17 00:00:00 2001 From: Xinze-Li-Moqian <70414198+Xinze-Li-Moqian@users.noreply.github.com> Date: Wed, 27 May 2026 00:26:47 -0400 Subject: [PATCH 07/21] docs(readme): sync with spec-only main --- README.md | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index bd2d426..3dbb7be 100644 --- a/README.md +++ b/README.md @@ -2,23 +2,21 @@ A Lean 4 library that turns formal proofs into computable mathematical objects. -[Documentation](https://proof-atlas.vercel.app/) · [White Paper](whitepaper/) · [Contributing](CONTRIBUTING.md) +[White Paper](whitepaper/) · [Contributing](CONTRIBUTING.md) -## Quick start +## Status + +Main branch contains the design specification for the proof +hypergraph. Implementation is on the `development` branch and +will be promoted here as it reaches production quality. + +## Build ```bash cd lean -lake exe cache get lake build ``` -```lean -import ProofAtlas - -#atlas.resistance Nat.add_zero Nat.add_comm -#atlas.graph Nat.add_zero -``` - ## License Apache 2.0 (see `LICENSE`). From 0dff96bdf77fae1eb023c88419dc9ef5072caa9a Mon Sep 17 00:00:00 2001 From: Xinze-Li-Moqian <70414198+Xinze-Li-Moqian@users.noreply.github.com> Date: Wed, 27 May 2026 11:54:11 -0400 Subject: [PATCH 08/21] chore: clean docs, add phase-1 checklist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove outdated docs (architecture, how-to, tutorial, ROADMAP, STATUS, refactor proposal) — all describe old codebase - Add docs/milestones/phase-1.md — living checklist for current goals - Update CLAUDE.md: remove dead links, add milestones pointer - ADRs preserved (historical record) --- CLAUDE.md | 15 +- docs/ROADMAP.md | 84 ------ docs/STATUS.md | 147 ---------- docs/architecture.md | 160 ----------- docs/how-to/add-a-mapping.md | 191 ------------- docs/how-to/add-a-metric.md | 204 -------------- docs/milestones/phase-1.md | 40 +++ docs/refactor/directory-layout-proposal.md | 295 --------------------- docs/tutorial.md | 166 ------------ 9 files changed, 43 insertions(+), 1259 deletions(-) delete mode 100644 docs/ROADMAP.md delete mode 100644 docs/STATUS.md delete mode 100644 docs/architecture.md delete mode 100644 docs/how-to/add-a-mapping.md delete mode 100644 docs/how-to/add-a-metric.md create mode 100644 docs/milestones/phase-1.md delete mode 100644 docs/refactor/directory-layout-proposal.md delete mode 100644 docs/tutorial.md diff --git a/CLAUDE.md b/CLAUDE.md index 22e4dd9..679f747 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,13 +5,6 @@ enforcement lives in CI and the Lean type system, not here. ## Enforced automatically -These rules are checked by `lake build` or `scripts/lint.sh`. -Breaking them fails the build or blocks the PR — no human -intervention needed. - -- **Metric quality spec** — `IsHypergraphMetric` typeclass - (Lean type system). `#guard_metric_registry` in - `Metric/Default.lean` audits all instances at build time. - **Docstring tags** — `**Math.**` / `**Eng.**` / `**Mixed.**` on public declarations (`Util/Linter/MathTag.lean`). - **No `import Mathlib`** — full import banned; Lean syntax linter @@ -22,17 +15,15 @@ intervention needed. - **Branch strategy** — PRs to `main` must come from `development`. PRs to `development` must come from `feat/`/`fix/`/`chore/`/ `docs/`/`refactor/` branches. CI enforces. -- **PR title** — must match `type(scope): summary` (conventional - commit format). CI enforces on PRs. +- **PR title** — must match `type(scope): summary`. CI enforces. ## Conventions (not yet automated) - **ADRs** for cross-file architectural decisions (`docs/adr/`). -- **No per-file SPDX headers** (Mathlib linter conflict). ## Pointers -- Architecture: [`docs/architecture.md`](docs/architecture.md) -- How-tos: [`docs/how-to/`](docs/how-to/) +- Milestones: [`docs/milestones/phase-1.md`](docs/milestones/phase-1.md) +- ADRs: [`docs/adr/`](docs/adr/) - Quality gate: [`scripts/lint.sh`](scripts/lint.sh) - CI config: [`.github/workflows/check.yml`](.github/workflows/check.yml) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md deleted file mode 100644 index 39e0526..0000000 --- a/docs/ROADMAP.md +++ /dev/null @@ -1,84 +0,0 @@ -# Roadmap - -This roadmap is reviewed at each release. The current focus is the -gap between a working technical core and a project an external -contributor can enter without the maintainer's attention. - -For the current status of every item below, see [STATUS.md](STATUS.md). - -## Toward v1.0 - -The project does not tag v1.0 until the four items below are in -place. The first two are done as of the 2026-05-23 cleanup; what -remains is community-facing. - -1. **Architecture document and add-a-mapping how-to.** _Done._ See - [`docs/architecture.md`](docs/architecture.md), - [`docs/how-to/add-a-mapping.md`](docs/how-to/add-a-mapping.md), - [`docs/how-to/add-a-metric.md`](docs/how-to/add-a-metric.md), - [`docs/tutorial.md`](docs/tutorial.md), and the site mirrors of - each. - -2. **Repository hygiene.** _Mostly done._ Issue templates, PR - template, `CODEOWNERS`, `CODE_OF_CONDUCT.md`, `CITATION.cff`, - `AUTHORS.md`, plus ten labelled - [*good first issue* entries](https://github.com/MathNetwork/ProofAtlas/labels/good%20first%20issue) - all in place. Still owed: enabling GitHub Discussions at the - repo level. - -3. **Citation anchor.** `CITATION.cff` in place. Still owed: - position paper on arXiv plus a Zenodo DOI for the v1.0 tag. - -4. **Governance.** A short `GOVERNANCE.md` covering merge rules, - conflict resolution, and how a contributor becomes a maintainer. - -## v1.0 → v1.1 - -Performance work and the ergonomics layer downstream consumers need. - -- **`#atlas.delta.ns!` Monte-Carlo sampler** for namespaces past the - 200-vertex range, where the exact `O(C(n,4))` enumeration becomes - the limiting cost. -- **Single-pair resistance** for `#atlas.resistance.ns` queries that - only need `R(A, B)`, by solving `L x = e_A − e_B` directly instead - of computing the full pseudoinverse. -- **Versioned JSON schema** for the `atlas-export` output, published - alongside the binary. -- **`proofatlas-py`**: thin Python package that parses the export - into a `NetworkX` MultiDiGraph. -- **Two Jupyter notebooks**: reproduce headline metrics from a - published profile, and compare two profiles before and after a - refactor. -- **`doc-gen4` API reference** auto-generated from Lean docstrings, - linked from the site. -- **Tutorial and how-to series** on the docs site: *your first - profile*, *add a new metric* (worked example: `diffusionDist`), - *fill an open `sorry`*. - -## Long term - -- **Mathlib bump CI** that opens a PR on every nightly green bump. -- **Performance regression CI** tracking per-file compile time across - releases; threshold-based alerts on regressions. -- **Axiom audit CI** verifying every `IsHypergraphMetric` instance - stays inside a whitelisted axiom set on every PR. -- **Export end-to-end CI** running `lake exe atlas-export` against a - small reference Lean project and validating the JSON schema. -- **Lean Reservoir registration** so other projects can `require - proofatlas` directly. -- **Hugging Face dataset** of ProofAtlas profiles for selected - external projects (Mathlib subset, FLT, PFR), under the - MathNetwork organisation. -- **Real-project gallery** at `/docs/gallery` on the site, with - three external profiles and short interpretations. -- **Funding path.** A one-page brief targeted at one realistic - source (Lean FRO, NSF POSE, Mozilla MOSS, or institutional). -- **Release cadence.** Monthly tags once v1.0 ships; semantic - versioning; written backwards-compatibility policy. - -## Where the leverage is - -Among the items above, the four under *Toward v1.0* close the largest -gaps with the least effort. They are the difference between a -research codebase and a project a stranger can read, contribute to, -cite, and maintain through a change of stewardship. diff --git a/docs/STATUS.md b/docs/STATUS.md deleted file mode 100644 index d6b088f..0000000 --- a/docs/STATUS.md +++ /dev/null @@ -1,147 +0,0 @@ -# Status - -What an open-source mathematical-infrastructure project needs beyond -working code, and where ProofAtlas stands on each. The technical core -(the mapping pipeline, the `IsHypergraphMetric` framework, the -`#atlas.*` command suite) is in place; this document tracks -everything else. - -Items are marked **[done]**, **[partial]**, or **[missing]**. For -the order in which the [missing] items will be addressed, see -[ROADMAP.md](ROADMAP.md). - -Last review: 2026-05-23 (Wave 1 + Wave 2 of the hygiene+docs cleanup landed). - -## 1. Documentation - -- **[done] Public docs site** at https://proof-atlas.vercel.app. - Next.js + MDX + KaTeX; landing page plus docs. -- **[done] Command reference.** All `#atlas.*` commands (eight, - including `.graph.ns`, `.resistance.ns`, `.delta.ns`) at - `/docs/commands`. -- **[done] Architecture document.** Canonical at - [`docs/architecture.md`](docs/architecture.md); site mirror at - `/docs/architecture`. Includes the four-layer diagram, the - spec-vs-instance split, and a "where to touch" table. -- **[done] Tutorial:** *your first ProofAtlas profile.* At - [`docs/tutorial.md`](docs/tutorial.md); site mirror at - `/docs/tutorial`. Five-minute walkthrough from install to first - `#atlas.profile`. -- **[done] How-to: add a new mapping.** At - [`docs/how-to/add-a-mapping.md`](docs/how-to/add-a-mapping.md); - worked example uses `Environment.toDeclIncidence`. -- **[done] How-to: add a new metric.** At - [`docs/how-to/add-a-metric.md`](docs/how-to/add-a-metric.md); - worked example uses `commuteTimeDist`, with a pointer to - `diffusionDist` as the next-step exercise. -- **[missing] How-to: fill an open `sorry`.** Mathematical content, - shape of a valid fill, and what reviewers will check, for each - currently-open `sorry`. -- **[partial] Gallery of real-project profiles.** ProofAtlas has - been run against its own codebase; missing on external projects - (Mathlib subset, FLT, PFR). -- **[missing] `doc-gen4` API reference** auto-generated from Lean - docstrings. - -## 2. Community infrastructure - -- **[done] `CONTRIBUTING.md`** with installation, usage, export, and - an open task list. -- **[done] Issue templates.** `.github/ISSUE_TEMPLATE/` has four - templates: bug, feature, new mapping proposal, new metric - proposal; plus a `config.yml` that disables blank issues and - routes open-ended questions to GitHub Discussions. -- **[done] PR template and `CODEOWNERS`.** Both in `.github/`. PR - template includes a test plan, an axioms section, and a breaking- - changes section. CODEOWNERS is per-area, single-owner today. -- **[done] `CODE_OF_CONDUCT.md`.** Contributor Covenant 2.1 adopted - by reference; reporting contact is the maintainer email. -- **[missing] Discussion channel.** GitHub Discussions still - disabled at the repo level; no Lean Zulip stream. -- **[done] Labelled *good first issue* entries.** Ten labelled - good-first issues opened in the tracker (#2 – #11), spanning - documentation, code, and infrastructure. Track resolution; aim - for the first external PR to land within the next month. -- **[missing] Lean Zulip announcement** to `#general` on v1.0 tag. - -## 3. Contributor onboarding - -- **[done] Onboarding doc.** `CONTRIBUTING.md` plus the new - `docs/tutorial.md` cover install → first command → next steps. -- **[done] `AUTHORS.md`.** Lists the maintainer plus a - contributor-add placeholder; cross-linked from `CITATION.cff`. -- **[missing] Secondary reviewers.** All review currently flows - through the maintainer; no designated reviewer per area (mapping, - metric, pipeline, docs). -- **[missing] Public sync cadence.** No public sync cadence yet; - to be established as contributor count grows. - -## 4. Quality assurance / CI - -- **[done] Build CI green.** `.github/workflows/check.yml` runs - `lake build` plus `scripts/check_metric_instances.sh` on - every push and pull request. -- **[partial] Sorry tracking.** `check_metric_instances.sh` - audits registered `IsHypergraphMetric` instance theorems; no - global `sorry` inventory and no public dashboard. -- **[partial] Mathlib bump CI.** Manual bumps as needed; no - scheduled job. -- **[missing] Performance regression CI.** No per-file compile-time - or memory tracking across releases. -- **[missing] Axiom audit CI.** Manual `#print axioms` checks only; - not automated per PR. -- **[missing] Export end-to-end CI.** `lake exe atlas-export` runs - locally; not exercised in CI against a reference Lean project, - no JSON-schema validation step. -- **[missing] Linting / style checks.** No Mathlib-style linters in - CI (unused imports, naming conventions, docstring presence). - -## 5. Discoverability - -- **[done] Public website** live at https://proof-atlas.vercel.app. -- **[partial] Manifesto / position paper.** Draft in - `whitepaper/`; draft in review; not yet on arXiv. -- **[missing] Zenodo DOI per release.** No release tagged yet - either. -- **[done] `CITATION.cff`.** CFF 1.2 metadata at the repo root. - GitHub's *Cite this repository* button surfaces the maintainer + - Apache-2.0 + repo URL. -- **[missing] Lean Reservoir registration.** Not in the Reservoir; - downstream projects cannot `require proofatlas` directly. -- **[missing] Awesome-Lean / curated-list entries.** Not listed. -- **[missing] Demo material.** No screencast, no asciinema, no - example notebook on the landing page. - -## 6. Ecosystem interfaces - -- **[partial] Export pipeline.** `lake exe atlas-export` runs; the - output JSON has no published versioned schema. -- **[missing] Python post-processing package** (`proofatlas-py`) - that loads an export into a `NetworkX` MultiDiGraph. -- **[missing] Jupyter notebook examples.** Two planned: headline- - metric reproduction, and before/after-refactor comparison. -- **[missing] Hugging Face dataset of sample profiles** under the - MathNetwork organisation. - -## 7. Governance & sustainability - -- **[done] License.** Apache 2.0 at the repo root (matches the - Lean and Mathlib community standard). -- **[wont-fix] Per-file SPDX headers.** Tried and reverted: the - Mathlib style linter parses an `SPDX-License-Identifier:` line as - continuation of the `Authors:` line and emits a warning on every - file. The Mathlib convention is to keep SPDX exclusively in the - root `LICENSE` file; ProofAtlas follows that convention. -- **[partial] Governance.** Repository governance (branch protection, - permission tiers, PR-only merging) documented in the white paper - (§3.1.5) and enforced via GitHub settings. A standalone - `GOVERNANCE.md` for conflict resolution and maintainer promotion - is still owed. -- **[done] `ROADMAP.md`.** See [ROADMAP.md](ROADMAP.md). -- **[missing] Release cadence.** Zero git tags, zero GitHub - releases. Plan: monthly tags after v1.0, with semantic - versioning. -- **[missing] Backwards compatibility policy.** Pre-1.0, API churn - is acceptable; a written deprecation policy is required before - the v1.0 tag. -- **[missing] Funding path.** No formal funding identified. diff --git a/docs/architecture.md b/docs/architecture.md deleted file mode 100644 index dd8ba51..0000000 --- a/docs/architecture.md +++ /dev/null @@ -1,160 +0,0 @@ -# Architecture - -> Canonical source. The site mirrors this at -> [`/docs/architecture`](https://proof-atlas.vercel.app/docs/architecture). - -ProofAtlas turns a Lean 4 declaration into a *hypergraph* and then -measures the geometry of that hypergraph. The library is organised -in four layers; each layer has a single responsibility and exposes a -clean interface to the layers above and below. - -``` - ┌──────────────────────────────────────────────────────────────┐ - │ Layer 4 — Quality │ - │ scripts/check_metric_instances.sh · IsHypergraphMetric │ - │ CI gate, sorry audit, axiom whitelist │ - └──────────────────────────────────────────────────────────────┘ - ▲ - ┌──────────────────────────┴───────────────────────────────────┐ - │ Layer 3 — Compute (Float, #eval-able) │ - │ Pipeline/IncidenceData Pipeline/Linalg │ - │ Pipeline/Hyperbolicity Pipeline/Cache │ - │ Mapping/{Expr, Environment, Declaration} │ - │ Command/* (#atlas.*) Export/* (lake exe atlas-export) │ - └──────────────────────────┬───────────────────────────────────┘ - ▲ - ┌──────────────────────────┴───────────────────────────────────┐ - │ Layer 2 — Metric (proof side, ℝ-valued) │ - │ Metric/Axioms.lean: IsHypergraphMetric spec │ - │ Metric/{Resistance, CommuteTime, JensenShannon, │ - │ Diffusion}/{Basic, Instance}.lean │ - │ RandomWalk/{KemenySnell, CommuteTime, Spectral/*} │ - │ Hyperbolicity/Basic.lean: generic δ, Hausdorff, GH theorems │ - └──────────────────────────┬───────────────────────────────────┘ - ▲ - ┌──────────────────────────┴───────────────────────────────────┐ - │ Layer 1 — Proof (combinatorial) │ - │ Hypergraph/Basic.lean: Hypergraph V C │ - │ Mapping/Expr.lean: Lean.Expr → Hypergraph │ - └──────────────────────────────────────────────────────────────┘ -``` - -## Layer 1 — Proof - -`Hypergraph/Basic.lean` defines `Hypergraph V C`: a directed, -ordered, coloured, acyclic, connected hypergraph. The four -structural axioms (D, O, C, A) are encoded as fields of the -structure, so they hold by construction. The translation from any -Lean proof term lives in `Mapping/Expr.lean`: - -```lean -def Lean.Expr.toHypergraph (e : Expr) : - Hypergraph (Fin (countSubterms e)) ExprColor -``` - -The load-bearing lemma is `connected`: every non-root DFS vertex is -incident to its parent compound subterm. With that sealed, every -Lean declaration in any library ships with a `Hypergraph` for free. - -## Layer 2 — Metric - -`Metric/Axioms.lean` defines the contract a candidate distance must -satisfy to count as a "hypergraph metric": - -```lean -structure IsHypergraphMetric - (d : Hypergraph V C → WalkParams C → V → V → ℝ) where - d_self, d_symm, d_triangle, d_nonneg -- metric axioms - sensitive_to_direction, _to_ordering, _to_coloring -- existence witnesses -``` - -Four concrete metrics are registered against this spec: - -| metric | file | status | -| --- | --- | --- | -| `resistanceDist` | `Metric/Resistance/Instance.lean` | certified | -| `commuteTimeDist`| `Metric/CommuteTime/Instance.lean` | certified | -| `jsDist` | `Metric/JensenShannon/Instance.lean`| in progress (sorry) | -| `diffusionDist` | `Metric/Diffusion/Instance.lean` | in progress (sorry) | - -All large-scale geometry (`Hyperbolicity/Basic.lean` for δ-hyperbolicity, -the Hausdorff and Gromov–Hausdorff constructions in -`Metric/{Hausdorff, GromovHausdorff}.lean`) is parameterised by -`{d} (isMetric : IsHypergraphMetric d)`. Swap a metric and every -downstream theorem follows. - -## Layer 3 — Compute - -The proof layer is `noncomputable` (it uses abstract `ℝ` and carries -proofs). For end-to-end pipelines we need an `#eval`-able -representation: `Pipeline/IncidenceData.lean` gives the same -combinatorial object as arrays of `Nat` vertex indices and `Float` -weights. Everything in `Pipeline/` is total, computable, and free -of proof obligations. - -Two extraction modes feed `IncidenceData`: - -- **Expr-level** (`Mapping/Environment.lean`). - `Exprs.toIncidenceData : Array Expr → IncidenceData × Array Nat` - glues proof terms via const-sharing — every reference to the same - `.const Name` collapses to one shared vertex. -- **Decl-level** (`Mapping/Declaration.lean`). - `Environment.toDeclIncidence : Environment → Array Name → IncidenceData` - builds the co-reference graph of a namespace: vertices are - declarations, hyperedges are maximal application spines inside each - declaration's proof body. - -From `IncidenceData`, `Pipeline/Linalg.lean` computes the Laplacian -and the effective-resistance matrix, and `Pipeline/Hyperbolicity.lean` -computes the four-point δ. `Pipeline/Cache.lean` memoises both per -`(env, namespace)` so repeated `.ns` commands don't re-pay the work. - -The interactive surface is `Command/*`: each `#atlas.<verb>` resolves -identifiers, calls into `Pipeline/`, and either logs a number or -renders a widget. The same compute machinery is wrapped by the -`atlas-export` executable in `Export/` for shell pipelines and CI. - -## Layer 4 — Quality - -`scripts/check_metric_instances.sh` runs in CI on every push. -For each registered metric in the registry, it walks -`#print axioms` of the corresponding `*_isHypergraphMetric` -instance theorem and refuses to merge if any `sorry` is reachable. - -The `Util/Audit.lean` linter (the `MathTag` linter) forces every -public declaration's docstring to begin with one of `**Math.**`, -`**Eng.**`, or `**Mixed.**` so it's always obvious which lines -correspond to paper content and which are type-theoretic plumbing. - -## Where to touch - -| If you want to | Touch | Recipe | -| --- | --- | --- | -| Add a new mapping | `Mapping/MyMapping.lean` + `Command/MyCmd.lean` | [docs/how-to/add-a-mapping.md](how-to/add-a-mapping.md) | -| Add a new metric | `Metric/MyMetric/{Basic,Instance}.lean` + script registry | [docs/how-to/add-a-metric.md](how-to/add-a-metric.md) | -| Add a new command (no mapping/metric) | `Command/MyCmd.lean` using `Command/Common.lean` helpers | follow `Command/Resistance.lean` as a template | -| Add a downstream consumer | `Export/MyFormat.lean` + `lakefile.toml` target | follow `Export/Profile.lean` as a template | -| Fill an open `sorry` | the relevant `Metric/*/Basic.lean` | (how-to coming in Wave 2) | - -## Two things this architecture deliberately does - -**Separates spec from instance.** `IsHypergraphMetric` is the -contract; the four metrics are instances of it. Any new theorem in -Layer 2 or 3 takes an `IsHypergraphMetric d` hypothesis, not a -specific distance. New metrics drop in without breaking downstream -results. - -**Separates the proof layer from the compute layer.** Layer 1 + 2 -prove things about abstract `ℝ`-valued metrics; Layer 3 evaluates -their `Float` shadows. A future bridging lemma will pin the -correspondence formally; for now they're kept on speaking terms by -convention. The CI gate on `IsHypergraphMetric` instances ensures -the proof side stays honest; the `#eval`-able compute side -ensures the demo loop is fast. - -## Pointers - -- For a tour of the `#atlas.*` commands: [docs/tutorial.md](tutorial.md) -- For the command reference: [proof-atlas.vercel.app/docs/commands](https://proof-atlas.vercel.app/docs/commands) -- For the theory side: [proof-atlas.vercel.app/docs/theory](https://proof-atlas.vercel.app/docs/theory) -- For the open task list: [CONTRIBUTING.md](../CONTRIBUTING.md) and [ROADMAP.md](ROADMAP.md) diff --git a/docs/how-to/add-a-mapping.md b/docs/how-to/add-a-mapping.md deleted file mode 100644 index bedd2c8..0000000 --- a/docs/how-to/add-a-mapping.md +++ /dev/null @@ -1,191 +0,0 @@ -# How to add a new mapping - -> Canonical source. The site mirrors this at -> [`/docs/how-to/add-a-mapping`](https://proof-atlas.vercel.app/docs/how-to/add-a-mapping). - -A *mapping* is an extractor that turns some Lean object into an -`IncidenceData` (and, optionally, a proof-layer `Hypergraph`). The -two mappings currently in tree are: - -| Mapping | Input | Vertex set | Hyperedge | -| --- | --- | --- | --- | -| `Expr.toIncidence` | `Lean.Expr` | Subterm occurrences | one per compound constructor (`app`, `lam`, ...) | -| `Environment.toDeclIncidence` | `Environment + Array Name` | Declarations | one per maximal application spine in a proof body | - -This guide walks through `Environment.toDeclIncidence` as the worked -example because it is the most recent and exhibits all the moving -parts: declaration filtering, vertex labelling, allowed-set -filtering, deduplication, command wiring, and cache integration. - -If you are reading this to add your own (e.g. -`Environment.toModuleIncidence`, `Environment.toImportIncidence`), the -recipe in §6 is the part you actually need; everything before it is -context. - -## 1. Pick the input and decide the vertex set - -A mapping is defined by three choices: - -- **Input type.** What Lean object are you extracting from? - `Environment`, `Array Name`, `Module`, a single `ConstantInfo`? -- **Vertex set.** What does each vertex represent? Subterms, decls, - modules, namespaces? -- **Hyperedge structure.** What does each hyperedge represent? An - inference step, a co-reference, an import dependency? - -For the decl-level mapping these are: `(Environment, Array Name)`, -declarations as vertices, and "one hyperedge per maximal `Expr.app` -spine inside a proof body" as the edge rule (filtered to the -namespace, deduped, self-references removed). - -The hyperedge rule is the load-bearing choice. Two practical -constraints from `IncidenceData`: - -- `inputs` are `Array Nat`; vertex IDs must be allocated upfront in - a deterministic order. The decl mapping uses `0 .. declNames.size - 1` - in the order returned by `declsInNamespace`. -- `output` is one `Nat`. If the mapping is "many inputs → many - outputs", decompose into one edge per output. (Most natural - mappings end up with a single output per edge.) - -## 2. Write the extractor in `Pipeline/` - -Drop a new file `lean/ProofAtlas/Mapping/MyMapping.lean`. The file -should export a single primary function with signature -`Source → IncidenceData` (or `Source → IncidenceData × Array Nat` -if you want to return root vertex IDs for downstream addressing). - -For decl-level: - -```lean -def Environment.toDeclIncidence - (env : Environment) (declNames : Array Name) : IncidenceData := Id.run do - let mut declToVid : Std.HashMap Name Nat := {} - let mut labels : Array String := #[] - let mut colors : Array String := #[] - for h : i in [:declNames.size] do - let n := declNames[i] - declToVid := declToVid.insert n i - labels := labels.push (toString n) - -- ... per-vertex colour by declaration kind - let allowed : Std.HashSet Name := - declNames.foldl (fun s n => s.insert n) {} - let mut allEdges : Array EdgeRec := #[] - for h : i in [:declNames.size] do - let n := declNames[i] - match env.find? n with - | some info => - match info.value? (allowOpaque := true) with - | some body => - let es := collectDeclEdges allowed declToVid n body - allEdges := allEdges ++ es - | none => pure () - | none => pure () - return { numVertices := declNames.size - edges := allEdges - vertexLabel := labels - vertexColor := colors } -``` - -Three patterns to copy: - -- **Vertex ID is the index in the input array.** Deterministic, - cheap, gives the caller a stable handle. -- **Per-vertex labels and colours go in `vertexLabel` and - `vertexColor`.** These flow straight to the widget; empty arrays - fall back to defaults. -- **Filter inputs to the `allowed` set in the edge builder.** Drop - any reference outside your vertex set; otherwise edges point to - ghost vertices and the resistance matrix gets confusing rows. - -## 3. (Optional) Proof-layer mirror - -If you want quality guarantees on top of the numerics, define a -proof-layer companion that returns a real `Hypergraph V C` and -discharges the structural axioms (`inArity_pos`, `outArity_pos`, -`inputOutputDisjoint`, `inVertex_injective`, `outVertex_injective`, -`acyclic`, `connected`). - -For v1 of a new mapping this is usually deferred. Ship the -`IncidenceData` version first; add the proof-layer mirror once the -numerical results show the mapping is worth proving theorems about. - -`Mapping/Expr.lean` is the example to copy. - -## 4. Wire a command in `Command/` - -Drop `lean/ProofAtlas/Command/MyCmd.lean`. Boilerplate is in -`Command/Common.lean`: `resolveIdentName`, `resolveIdentInNamespace`, -`renderGraphWithLegend`, `renderGraphWithDeclLegend`. - -```lean -syntax (name := atlasMyCmd) "#atlas.my" ppSpace colGt ident : command - -@[command_elab atlasMyCmd] -def elabAtlasMyCmd : CommandElab := fun stx => do - let arg : Name := stx[1].getId - let env ← getEnv - let data := Pipeline.Environment.toMyMapping env arg - if data.numVertices = 0 then - throwError "#atlas.my: no vertices for {arg}" - logInfo m!"My-mapping ({arg}): {data.numVertices} vertices, {data.numEdges} edges" - let report : Pipeline.HypergraphReport := - { incidence := data, resistance := #[], delta := 0.0, roots := #[] } - renderGraphWithLegend stx report -``` - -Conventions: - -- The command name mirrors the mapping: `Foo.toX` ↔ `#atlas.x`. -- A namespace-scoped mapping gets a `.ns` suffix on the command. -- Errors throw with the command name as prefix (`#atlas.my: ...`) so - users can grep for them. - -## 5. Wire into the facade and the cache - -Two cheap edits: - -- `lean/ProofAtlas/Command.lean`: add `import ProofAtlas.Command.MyCmd` - and a line in the docstring's quick-reference table. -- `lean/ProofAtlas/Pipeline/Cache.lean` (only if the mapping is - expensive enough to warrant caching, like decl-level): add a - `getOrBuildMyMapping` mirroring `getOrBuildDeclProfile`. The - cache key uses `(envNumConsts env, arg)` where `arg` is whatever - uniquely identifies the mapping's input. - -## 6. The minimum recipe (cheat sheet) - -1. `lean/ProofAtlas/Mapping/MyMapping.lean` — extractor function - returning `IncidenceData`. -2. `lean/ProofAtlas/Command/MyCmd.lean` — `#atlas.my <args>` wrapper. -3. `lean/ProofAtlas/Command.lean` — add the import. -4. `lean/ProofAtlas/Demos/Pipeline.lean` — at least one example - invocation that runs in the demo file. -5. **Optional**: `Pipeline/Cache.lean` for memoisation, a proof-layer - `Hypergraph` mirror for quality guarantees, a site doc page. -6. **PR**: use the *New mapping proposal* issue template to discuss - before implementing, especially for the hyperedge rule. - -## What reviewers will check - -- The mapping is *total* (no `Option`, no exceptions on - well-formed input). -- Vertex IDs are deterministic across runs (no `IO.rand`, no - iteration order that depends on hash collisions). -- The resulting hyperedges satisfy - `inputs.size ≥ 1` and `output ∉ inputs.toList` (these mirror the - proof-layer axioms; if the numerical mapping violates them the - matrix routines still work but the result is meaningless). -- The command has a single demo invocation in `Demos/Pipeline.lean` - whose output looks reasonable on the project itself. -- The MathTag linter is satisfied (every public def has an - `**Eng.**` / `**Math.**` / `**Mixed.**` tagged docstring). - -## Pointers - -- Existing mappings to copy from: - [`Mapping/Environment.lean`](../../lean/ProofAtlas/Mapping/Environment.lean) (expr-level glue), - [`Mapping/Declaration.lean`](../../lean/ProofAtlas/Mapping/Declaration.lean) (decl-level). -- Command helpers: [`Command/Common.lean`](../../lean/ProofAtlas/Command/Common.lean). -- The data structure: [`Pipeline/IncidenceData.lean`](../../lean/ProofAtlas/Pipeline/IncidenceData.lean). -- For the metric side, see [add-a-metric.md](add-a-metric.md). diff --git a/docs/how-to/add-a-metric.md b/docs/how-to/add-a-metric.md deleted file mode 100644 index 055674d..0000000 --- a/docs/how-to/add-a-metric.md +++ /dev/null @@ -1,204 +0,0 @@ -# How to add a new metric - -> Canonical source. The site mirrors this at -> [`/docs/how-to/add-a-metric`](https://proof-atlas.vercel.app/docs/how-to/add-a-metric). - -A *metric* is a function of type - -```lean -Hypergraph V C → WalkParams C → V → V → ℝ -``` - -that satisfies `IsHypergraphMetric`: four metric-space axioms -(identity, symmetry, triangle, non-negativity) and three sensitivity -existence witnesses (direction, ordering, colouring). Four metrics -are currently registered: `resistanceDist`, `commuteTimeDist`, -`jsDist`, and `diffusionDist`. - -This guide walks through `commuteTimeDist` as the worked example -because it is the second-most-mature metric in the tree and -exhibits every part of the recipe. Once you can follow this -example, the open work on `diffusionDist` -(`Metric/Diffusion/Instance.lean`) becomes a productive exercise. - -## 1. Pick a definition with mathematical content - -A new metric is worth adding if it answers a question the existing -four don't. Three questions to write down before you start coding: - -- **What does this metric measure?** A geometric quantity, an - information-theoretic one, a spectral one? -- **What is the closed-form or characterisation?** A formula, an - optimisation problem, a fixed-point equation? -- **Why is it a metric?** Are the four axioms easy - (resistance distance: spectral formula gives them in 3 lines) or - hard (commute time: routes through the Markov / Doyle–Snell - identity)? - -`commuteTimeDist` answers "how long does the random walk take to -make a round trip between u and v" with the closed form -`d_ct(u, v) = h(u, v) + h(v, u)` where `h` is the expected first -passage time of the harmonic random walk on the hypergraph. The -four metric axioms follow from elementary properties of `h` plus a -Doyle–Snell-style identity proving symmetry. - -## 2. Drop a `Basic.lean` defining the metric - -Create `lean/ProofAtlas/Metric/<MyMetric>/Basic.lean`. Two -deliverables: - -- The `noncomputable def myDist` itself, with type - `Hypergraph V C → WalkParams C → V → V → ℝ`. -- The four metric-axiom lemmas that an instance theorem will - combine: `myDist_self`, `myDist_symm`, `myDist_triangle`, - `myDist_nonneg`. Mathlib namespacing convention: each lives in - `namespace Hypergraph`. - -For `commuteTimeDist` the deliverable is split across -`Metric/CommuteTime/{HittingTime, KilledChain, CommuteTime}.lean` -because the proofs lean on a sub-development of hitting-time -analysis. For simpler metrics one `Basic.lean` is enough. - -If a lemma needs a hypothesis the user must supply (e.g., -connectedness of the random walk, or full-rank conductance), declare -that hypothesis explicitly. **Don't bake it into a global `axiom`.** -The pattern is to require the hypothesis on each metric-axiom lemma -and re-export it as a top-level hypothesis in the `Instance.lean` -theorem (see step 3). - -## 3. Drop an `Instance.lean` proving `IsHypergraphMetric` - -Create `lean/ProofAtlas/Metric/<MyMetric>/Instance.lean`. The whole -file is one theorem: - -```lean -theorem myDist_isHypergraphMetric - {V C : Type*} [Fintype V] [DecidableEq V] - [hFin : ∀ H : Hypergraph V C, Fintype H.edges] - -- (a) preconditions that propagate from the metric-axiom lemmas - (hPre : ∀ (H : Hypergraph V C) (params : WalkParams C), ...) - -- (b) three existence witnesses - (hWit_direction : ∃ (H H' : Hypergraph V C) (params : WalkParams C) (u v : V), - H.myDist params u v ≠ H'.myDist params u v) - (hWit_ordering : ... ) - (hWit_coloring : ... ) : - IsHypergraphMetric (fun (H : Hypergraph V C) params u v => - H.myDist params u v) := by - refine ⟨?_, ?_, ?_, ?_, hWit_direction, hWit_ordering, hWit_coloring⟩ - · intro H params u -- d_self - exact H.myDist_self params u - · intro H params u v -- d_symm - exact H.myDist_symm params u v - · intro H params u v w -- d_triangle - exact H.myDist_triangle (hPre H params) u v w - · intro H params u v -- d_nonneg - exact H.myDist_nonneg params u v -``` - -The pattern is: - -- **Metric-space axioms close by re-using the lemmas from - `Basic.lean`.** Their preconditions become hypotheses of the - instance theorem. For `commuteTimeDist` this is `hErgodic`, a - five-part conjunction bundling row-stochasticity, non-negativity, - normalised stationary, strict positivity, and convergence to the - stationary projection. -- **Sensitivity witnesses are existence statements.** They're - passed in as hypotheses for now. The fully-discharged versions - (concrete `H`, `H'`, parameter choices) are a follow-up — see - `Metric/Resistance/Instance.lean` for the current treatment of - these witnesses as preconditions, with discharging left to the - caller. Your `Instance.lean` should do the same until you have a - scaling argument that gives a concrete witness. - -The `commuteTimeDist_isHypergraphMetric` declaration at -`Metric/CommuteTime/Instance.lean:40` is the cleanest template; copy -its shape. - -## 4. Register in the CI script - -Edit `lean/scripts/check_metric_instances.sh`: - -```bash -METRICS=( - "resistanceDist:ProofAtlas/Metric/Resistance/Instance.lean" - "commuteTimeDist:ProofAtlas/Metric/CommuteTime/Instance.lean" - "jsDist:ProofAtlas/Metric/JensenShannon/Instance.lean" - "diffusionDist:ProofAtlas/Metric/Diffusion/Instance.lean" - "myDist:ProofAtlas/Metric/MyMetric/Instance.lean" -) -``` - -The CI gate refuses to merge if your instance file contains any -`sorry`. If your first version is incomplete, leave the instance -theorem `sorry`'d and the script will tag it `in progress` rather -than failing the build (existing examples: `jsDist`, `diffusionDist`). - -## 5. (Optional) Compute-layer Float implementation - -If you want the metric to be `#eval`-able from a `#atlas.<verb>` -command, mirror the definition in `Pipeline/MyMetric.lean` returning -`Float` numbers. `Metric/Resistance/...` plus -`Pipeline/Linalg.resistanceMatrix` is the example. For -`commuteTimeDist` this is currently TODO — the hitting-time matrix -inversion is `noncomputable` on the proof side and hasn't been -ported to the `Float` pipeline. - -A compute-layer implementation also gets you a `#atlas.commute`-style -command via the same `Command/` wiring described in -[add-a-mapping.md §4](add-a-mapping.md). - -## 6. The minimum recipe (cheat sheet) - -1. `Metric/MyMetric/Basic.lean` — definition + four axiom lemmas. -2. `Metric/MyMetric/Instance.lean` — `myDist_isHypergraphMetric` - theorem assembling the four axioms and routing through three - existence witnesses. -3. `scripts/check_metric_instances.sh` — add `myDist` to the - `METRICS=()` registry. -4. **Optional**: `Pipeline/MyMetric.lean` for the Float compute - path; `Command/MyCmd.lean` for an `#atlas.my` command. -5. **PR**: use the *New metric proposal* issue template to discuss - the mathematical content and the witness shapes before - implementing. - -## What reviewers will check - -- The instance file is `sorry`-free if you advertise the metric as - certified. CI enforces this. -- `#print axioms myDist_isHypergraphMetric` reports only the - whitelisted axioms (`propext`, `Classical.choice`, `Quot.sound`). - Anything else needs an explicit justification. -- The Mathlib-style docstring tags are present (`**Math.**` on - mathematical content, `**Eng.**` on plumbing). -- The Doyle–Snell-style key identity (or equivalent) is documented - with a paper reference if you cite one. - -## Now try: fill an open `sorry` - -Two metrics in tree are currently `in progress`: - -- `jsDist` (`Metric/JensenShannon/Instance.lean`) — Jensen–Shannon - divergence between stationary neighbourhoods. -- `diffusionDist` (`Metric/Diffusion/Instance.lean`) — heat-kernel - distance at finite time `t`. - -Both follow the exact shape of `commuteTimeDist_isHypergraphMetric`: -`d_self`, `d_symm`, `d_nonneg` are direct; `d_triangle` is the -genuinely hard piece. For `diffusionDist`, the triangle inequality -reduces to Cauchy–Schwarz on the heat-kernel row vectors at time `t`, -which means the discharge is a Mathlib `inner_mul_le_norm_mul_norm` -manipulation plus the heat-kernel positivity already proved in -`Metric/Diffusion/Basic.lean`. - -If you want a first PR, `diffusionDist`'s `d_triangle` discharge is -the shortest path to a green CI tick on a previously-yellow row. - -## Pointers - -- Existing instances to copy: - [`Metric/Resistance/Instance.lean`](../../lean/ProofAtlas/Metric/Resistance/Instance.lean) — the cleanest template, - [`Metric/CommuteTime/Instance.lean`](../../lean/ProofAtlas/Metric/CommuteTime/Instance.lean) — the one this how-to walks through. -- The spec: [`Metric/Axioms.lean`](../../lean/ProofAtlas/Metric/Axioms.lean). -- The CI gate: [`scripts/check_metric_instances.sh`](../../lean/scripts/check_metric_instances.sh). -- For the mapping side, see [add-a-mapping.md](add-a-mapping.md). diff --git a/docs/milestones/phase-1.md b/docs/milestones/phase-1.md new file mode 100644 index 0000000..5a9272f --- /dev/null +++ b/docs/milestones/phase-1.md @@ -0,0 +1,40 @@ +# Phase 1: Foundation + +The goal of Phase 1 is to establish the mathematical definition +and the governance infrastructure. Code follows from the spec. + +## Proof hypergraph definition (#58) + +- [ ] Resolve vertex identity: content hash vs other +- [ ] Resolve vertex structure: what data does a vertex carry +- [ ] Resolve construction method: recursive? builder pattern? +- [ ] Resolve relationship to Lean.Expr +- [ ] Resolve edge semantics: is current Hyperedge right? +- [ ] Resolve acyclicity/connectivity: axiom vs derived? +- [ ] Write definition in white paper (natural language + math) +- [ ] Write ADR documenting the chosen design +- [ ] Implement in Lean on development branch +- [ ] Review, test, PR to main + +## White paper + +- [ ] Update §2.3 (current foundation) to reflect spec-only main +- [ ] Separate short-term goals into this checklist (link from §2.3) +- [ ] §1 problem statement stable +- [ ] §2 strategy stable +- [ ] §3 team/governance stable (done 2026-05-26) +- [ ] Advisor review + +## Governance (done 2026-05-26) + +- [x] Branch protection (main + development) +- [x] CI: lint.sh + required status check +- [x] Lean linters: ImportBan, MathTag +- [x] Branch strategy: main ← development ← feature +- [x] Pre-push hook +- [x] ADR 0007 (main/development split) +- [x] ADR 0008 (sensitivity ∃ → ∀, proposed) + +## Site + +- [ ] Rewrite to match spec-only main (blocked by #58) diff --git a/docs/refactor/directory-layout-proposal.md b/docs/refactor/directory-layout-proposal.md deleted file mode 100644 index 328f20c..0000000 --- a/docs/refactor/directory-layout-proposal.md +++ /dev/null @@ -1,295 +0,0 @@ -# Directory layout refactor — proposal - -> Draft on branch `refactor/directory-layout`. Not committed. No -> code touched. Decide which option you want before any file moves. - -## 1. Current inventory - -Top-level files (`lean/ProofAtlas/*.lean`): - -| File | Lines | Incoming imports | Role | -| --- | ---: | ---: | --- | -| `ProofAtlas.lean` *(at `lean/`, not under `ProofAtlas/`)* | — | external | True root re-export | -| `Command.lean` | 53 | 11 | Facade: re-exports `Command/*` | -| `Examples.lean` | 132 | 1 | Hand-built paper examples of hypergraphs | -| `Hyperbolic.lean` | 28 | 7 | Facade: re-exports `Hyperbolic/*` | -| `LinearAlgebra.lean` | 29 | 11 | Facade: re-exports `LinearAlgebra/*` | -| `Metric.lean` | 33 | 25 | Facade: re-exports `Metric/*` | -| `Open.lean` | 49 | 1 | Open conjectures (intentional `sorry`) | -| `Sequence.lean` | 203 | 1 | "Growing sequences of BDF hypergraphs" | -| `Spectral.lean` | 49 | 9 | Facade: re-exports `Spectral/*` | - -Directories: - -| Dir | Files | Lines | Theme | Notes | -| --- | ---: | ---: | --- | --- | -| `Combinatorial/` | 7 | 947 | Hypergraph core type + helpers | Includes nested `Combinatorial/Hypergraph/` (3 files) | -| `Command/` | 10 | 735 | `#atlas.*` command suite | Clean, single purpose | -| `Export/` | 2 | 282 | `lake exe atlas-export*` binaries | Clean | -| `Hyperbolic/` | 4 | 756 | Gromov δ-hyperbolicity (proof side) | Name ambiguity | -| `LeanExpr/` | 2 | 1014 | `Lean.Expr → Hypergraph` mapping | Single-purpose, big file | -| `LinearAlgebra/` | 3 | 670 | Pseudoinverse, effective resistance | Clean foundation | -| `Markov/` | 3 | 2278 | Markov chain theory (fundamental matrix, Kemeny–Snell, commute time) | Overlaps with Spectral/ | -| `Metric/` | 18 | 4030 | `IsHypergraphMetric` spec + four registered instances | Well-structured | -| `Pipeline/` | 10 | 1551 | Compute layer (Float numerics) | Mixes mappings, data, algorithms | -| `Spectral/` | 9 | 1928 | Spectral analysis (1 file at top, 8 in `RandomWalk/`) | Overlaps with Markov/ | -| `Tactic/` | 1 | 226 | `IncidenceData` widget renderer | Single file | -| `Util/` | 2 | 236 | Linter (`MathTag`) + audit binary | Clean | - -Totals: **80 .lean files**, ~**14,800 lines** under `lean/ProofAtlas/`. - -Namespaces in use (sampled from likely-to-move files): - -``` -namespace ProofAtlas.LeanExpr -- 2 files (LeanExpr/) -namespace ProofAtlas.Markov -- 3 files (Markov/) -namespace ProofAtlas.Pipeline -- many files (Pipeline/) -namespace Hypergraph -- Combinatorial/Hypergraph.lean, Spectral/RandomWalk/* -namespace Hyperedge -- Combinatorial/Hypergraph.lean -``` - -Most namespaces are *path-aligned* (renaming a directory means renaming the namespace too). `Spectral/RandomWalk/*` is the lone exception — those files use `namespace Hypergraph` directly, so they're free to move. - -## 2. Diagnosis - -### (a) Mapping concept is scattered - -The same idea — *extract a hypergraph from a Lean source object* — lives in three different directories: - -- `LeanExpr/ExprToHypergraph.lean` — proof-side `Lean.Expr → Hypergraph` (958 L, currently `sorry`'d in part) -- `Pipeline/Environment.lean` — compute-side `Lean.Expr → IncidenceData` with const-sharing (168 L) -- `Pipeline/DeclIncidence.lean` — compute-side `Environment → IncidenceData` decl-level (224 L) - -A contributor asked to "add a new mapping" today reads three guides and walks through three directories to find the prior art. The `docs/how-to/add-a-mapping.md` already implicitly treats these as one concept. **They should live under one `Mapping/` directory.** - -### (b) `Spectral/` vs `Markov/` vs `LinearAlgebra/` - -These three are confusingly partitioned: - -- `LinearAlgebra/` — Real-valued matrix theory: Moore–Penrose pseudoinverse, effective resistance from Laplacian, spectral expansion. Pure linear algebra, no Markov chains, no random walks. -- `Markov/` — Markov chain theory: fundamental matrix, Kemeny–Snell, abstract commute time. No "random walk" in the name, but it IS about the BDF random walk's hitting-time analysis. -- `Spectral/` — Two distinct things: - - `Spectral/Electrical.lean` (52 L) — Spectral view of `d_H` (sits on top of `LinearAlgebra/SpectralExpansion`). - - `Spectral/RandomWalk/` (8 files, ~1900 L) — Spectral analysis of the BDF random walk: stationary distribution, decomposition, moments, probabilistic bounds. - -`Spectral/RandomWalk/` and `Markov/` are about *the same random walk*. The split is arbitrary and forces contributors to guess "is this lemma about the Markov chain or about its spectrum?". `LinearAlgebra/` is the cleanest of the three; it should stay. - -### (c) `Hyperbolic/` is ambiguous - -Contents are about **Gromov δ-hyperbolicity** of a metric space (the four-point condition). The directory name *Hyperbolic* reads as "hyperbolic geometry" (Lobachevsky / Riemann) at first glance. The site docs and the manifesto consistently say "hyperbolicity" or "δ-hyperbolicity". The directory should match: **`Hyperbolicity/`**. - -### (d) `Combinatorial/` hides the core type - -`Combinatorial/Hypergraph.lean` defines the central `Hypergraph V C` structure that the entire library is built around. Burying it under `Combinatorial/` (a generic "math infrastructure" bucket) makes it harder to find than it needs to be. Compare Mathlib's `Combinatorics/SimpleGraph/Basic.lean` — Mathlib does the same nesting, but they have *dozens* of combinatorial types; we have one. - -Promoting to top-level **`Hypergraph/`** (with `Basic.lean`, `Walk.lean`, `TransitionMatrix.lean`, etc.) is one less level of indirection and matches the namespace already in use (`namespace Hypergraph`). - -The recent `Combinatorial/Hypergraph/` sub-directory (containing `ToSimpleGraph.lean`, `ToDigraph.lean`, `ReductionDemo.lean`) is doubly nested — it's effectively `Hypergraph/Hypergraph/Reduction/`. The double-nesting is a symptom of the same problem. - -### (e) Top-level loose files - -- `Examples.lean` (132 L, "paper examples of hypergraphs") — example data, belongs in a demo directory. -- `Sequence.lean` (203 L, "growing sequences of hypergraphs") — extension of the `Hypergraph` type, belongs under `Hypergraph/`. -- `Open.lean` (49 L, "open conjectures") — small, contains the one intentional `sorry`. Could stay or move under a future `Conjectures/` umbrella. -- The five facades (`Command.lean`, `Hyperbolic.lean`, `LinearAlgebra.lean`, `Metric.lean`, `Spectral.lean`) — useful re-exports but visually clutter the top level. Could move under each subdirectory as `<Dir>/Default.lean` (Mathlib convention) or stay. - -## 3. Proposed layout - -``` -ProofAtlas/ -├── ProofAtlas.lean (at lean/, unchanged — root re-export) -│ -├── Hypergraph/ promoted from Combinatorial/ -│ ├── Basic.lean ← Combinatorial/Hypergraph.lean -│ ├── Decidability.lean ← Combinatorial/Decidability.lean -│ ├── Walk.lean ← Combinatorial/Walk.lean -│ ├── TransitionMatrix.lean ← Combinatorial/TransitionMatrix.lean -│ ├── Sequence.lean ← Sequence.lean (top-level) -│ ├── ToSimpleGraph.lean ← Combinatorial/Hypergraph/ToSimpleGraph.lean -│ └── ToDigraph.lean ← Combinatorial/Hypergraph/ToDigraph.lean -│ -├── Mapping/ new — all extractors in one place -│ ├── ExprColor.lean ← LeanExpr/ExprColor.lean -│ ├── Expr.lean ← LeanExpr/ExprToHypergraph.lean (rename) -│ ├── Environment.lean ← Pipeline/Environment.lean -│ └── Declaration.lean ← Pipeline/DeclIncidence.lean (rename) -│ -├── Metric/ unchanged -│ ├── Axioms.lean -│ ├── Hausdorff.lean -│ ├── GromovHausdorff.lean -│ └── {Resistance, CommuteTime, JensenShannon, Diffusion}/ -│ -├── RandomWalk/ new — merge Markov/ + Spectral/RandomWalk/ -│ ├── FundamentalMatrix.lean ← Markov/FundamentalMatrix.lean -│ ├── KemenySnell.lean ← Markov/KemenySnell.lean -│ ├── CommuteTime.lean ← Markov/CommuteTime.lean -│ └── Spectral/ ← Spectral/RandomWalk/ -│ ├── Stationary.lean -│ ├── Decomposition.lean -│ ├── Expectation.lean -│ ├── SpectralForm.lean -│ ├── Spectrum.lean -│ ├── Moments.lean -│ ├── ProbabilisticBound.lean -│ └── Bridge.lean -│ -├── LinearAlgebra/ unchanged -│ ├── EffectiveResistance.lean -│ ├── MoorePenrose.lean -│ └── SpectralExpansion.lean -│ -├── Hyperbolicity/ renamed from Hyperbolic/ -│ ├── Basic.lean -│ ├── Resistance.lean -│ ├── CommuteTime.lean -│ └── Diffusion.lean -│ -├── Pipeline/ compute layer, slimmer -│ ├── IncidenceData.lean -│ ├── FloatMat.lean -│ ├── Linalg.lean -│ ├── Hyperbolicity.lean -│ ├── Hausdorff.lean -│ ├── Cache.lean -│ └── Export.lean -│ -├── Command/ unchanged -├── Export/ unchanged -├── Tactic/ unchanged -├── Util/ unchanged -│ -├── Demos/ new — example data + smoke tests -│ ├── Paper.lean ← Examples.lean (top-level) -│ └── Pipeline.lean ← Pipeline/Demo.lean -│ -└── Open.lean keep at top — single small file -``` - -**Top-level shrinks from 9 .lean files to 2** (`Open.lean` + `ProofAtlas.lean` at the `lean/` root). The five facade files (`Hyperbolic.lean`, `LinearAlgebra.lean`, `Metric.lean`, `Spectral.lean`, `Command.lean`) get pulled into their respective directories as `Default.lean` per Mathlib convention. - -Where things land: - -- `Spectral/Electrical.lean` (52 L, lives outside `Spectral/RandomWalk/`) — its content is "spectral view of `d_H`", really a resistance-distance file. Move to `Metric/Resistance/Spectral.lean`. -- Spectral facade (`Spectral.lean`) — its role becomes `RandomWalk/Spectral/Default.lean` plus a re-export of `Metric/Resistance/Spectral.lean` from `Metric/Default.lean`. - -## 4. Migration cost - -### File operations - -| Action | Count | -| --- | ---: | -| Files moved (with or without rename) | **~33** | -| Files renamed in place | 4 (`Hyperbolic/*.lean`) | -| Directories created | 4 (`Hypergraph/`, `Mapping/`, `RandomWalk/`, `Demos/`) | -| Directories removed | 4 (`Combinatorial/`, `LeanExpr/`, `Markov/`, `Spectral/`) | -| Directories renamed | 1 (`Hyperbolic/` → `Hyperbolicity/`) | - -### Import statements - -Summing the *Incoming imports* column for every file that moves: **≈ 100 import lines** to update across the codebase. Mechanical sed-style work, but every one must be reviewed for `import` statement that points at the new path. - -### Namespace changes - -This is where the real risk lives. - -- **`namespace ProofAtlas.LeanExpr` → `namespace ProofAtlas.Mapping`** — every `ProofAtlas.LeanExpr.<thing>` reference in the codebase needs renaming. Estimated **~20 references** (most internal to the two files plus a handful of external callers). -- **`namespace ProofAtlas.Markov` → `namespace ProofAtlas.RandomWalk`** — same shape. **~30 references**, since `Markov.KemenySnell` is depended on by 5 other files. -- **`namespace ProofAtlas.Pipeline` for `Environment.lean` + `DeclIncidence.lean`** — these move to `Mapping/` but their `namespace ProofAtlas.Pipeline` declarations would clash with the rest of `Pipeline/`. Options: (a) rename to `namespace ProofAtlas.Mapping`, propagating to ~10 call sites; (b) keep `namespace ProofAtlas.Pipeline` even though the file lives outside `Pipeline/` (works, but confusing). -- **Hypergraph/* files** — already use `namespace Hypergraph` (not `ProofAtlas.Combinatorial.Hypergraph`), so the directory move is a *free* namespace-wise. Zero reference changes for these. -- **Hyperbolic → Hyperbolicity** — files use `namespace Hypergraph` (concept-aligned), so the directory rename is also free. - -Total namespace-reference updates: **~60 sites**, concentrated in the LeanExpr → Mapping and Markov → RandomWalk transitions. - -### docs/ + site/ synchronisation - -Path mentions found in markdown: - -- `docs/architecture.md` — references `Combinatorial/Hypergraph.lean`, `LeanExpr/ExprToHypergraph.lean`, `Pipeline/Environment.lean`, `Pipeline/DeclIncidence.lean`, plus the four-layer diagram (uses old paths). -- `docs/how-to/add-a-mapping.md` — references `Pipeline/Environment.lean`, `Pipeline/DeclIncidence.lean`, `LeanExpr/ExprToHypergraph.lean`. -- `docs/how-to/add-a-metric.md` — references `Metric/CommuteTime/Instance.lean` (no path change for Metric). -- `site/app/docs/*.mdx` — mirrors of the above. - -Estimated **~25 path strings** to update across `docs/` and `site/app/docs/`. Mechanical. - -### CI / scripts - -- `scripts/check_metric_instances.sh` — references `Metric/*/Instance.lean` paths. No change (Metric/ doesn't move). -- `.github/workflows/check.yml` — runs `lake build`, no path strings. No change. -- `lakefile.toml` — references binaries by target name, not file paths. No change. - -### Build / API impact - -- **Public API**: namespace renames (`ProofAtlas.LeanExpr → ProofAtlas.Mapping`, `ProofAtlas.Markov → ProofAtlas.RandomWalk`) are breaking changes for any external `import` statements. The repo is pre-1.0 so this is acceptable per `STATUS.md` ("Backwards compatibility policy: Pre-1.0; API churn is acceptable"). After the refactor, the API should freeze; this is the right moment to do it. -- **Build time**: should not change measurably — same Lean code in slightly different files. -- **Sorry count**: unchanged. - -## 5. Phased plan - -A single PR moving 33 files plus 100 import updates plus 60 namespace renames is reviewable in theory but heavy in practice and one merge conflict away from chaos. Recommended split: - -### **PR1 — Pure renames, no namespace changes** *(low risk, ~half day)* - -1. Rename `Hyperbolic/` → `Hyperbolicity/` (4 files, namespace untouched). -2. Rename `Hyperbolic.lean` (top-level facade) → `Hyperbolicity.lean`. -3. Update imports: 14 files affected (`grep -l "Hyperbolic"` from outside the dir). - -This is cosmetic-only: no semantics change, no namespace touched. CI green, no review controversy. - -**Files affected**: ~14. **Risk**: trivial. **Reviewer time**: 5 minutes. - -### **PR2 — Promote `Hypergraph/` + consolidate `Mapping/`** *(heaviest, ~1–2 days)* - -1. `git mv Combinatorial/* Hypergraph/` (7 files, including the nested `Hypergraph/Hypergraph/*`). -2. `git mv Sequence.lean Hypergraph/Sequence.lean`. -3. `git mv LeanExpr/* Mapping/` (2 files). -4. `git mv Pipeline/Environment.lean Mapping/Environment.lean`. -5. `git mv Pipeline/DeclIncidence.lean Mapping/Declaration.lean`. -6. Namespace renames: - - `ProofAtlas.LeanExpr` → `ProofAtlas.Mapping` (2 files internal + ~20 external). - - `ProofAtlas.Pipeline` → `ProofAtlas.Mapping` for the two moved files (~10 external). -7. Update all `import` statements (~50 lines). -8. Update `docs/architecture.md`, `docs/how-to/add-a-mapping.md`, site mirrors. -9. Update the four-layer diagram in `architecture.md`. - -This is the substantial PR. It touches the most files and changes the most public API. Best done in isolation so a reviewer can see clearly what's a rename vs a logic change. - -**Files affected**: ~35. **Risk**: medium. **Reviewer time**: 1–2 hours. **Needs ADR** documenting the new `Hypergraph/` + `Mapping/` convention. - -### **PR3 — Cleanup: `RandomWalk/`, `Demos/`, facades** *(medium risk, ~half day)* - -1. Create `RandomWalk/`; move `Markov/*` and `Spectral/RandomWalk/*` into it. -2. Rename `ProofAtlas.Markov` → `ProofAtlas.RandomWalk` (~30 external refs). -3. Move `Spectral/Electrical.lean` → `Metric/Resistance/Spectral.lean`. -4. Create `Demos/`; move `Examples.lean` → `Demos/Paper.lean`, `Pipeline/Demo.lean` → `Demos/Pipeline.lean`. -5. Pull the five top-level facades (`Hyperbolicity.lean`, `LinearAlgebra.lean`, `Metric.lean`, `Spectral.lean`, `Command.lean`) into `<Dir>/Default.lean` per Mathlib convention. Update `ProofAtlas.lean` accordingly. -6. Update docs + site mirrors. - -**Files affected**: ~20. **Risk**: medium (namespace rename for Markov → RandomWalk). - -### Inter-PR dependencies - -- PR1 has no dependencies; can land any time. -- PR2 depends on nothing (PR1 can land before or after). -- PR3 depends on PR2 having merged (because PR3's facade restructuring assumes the post-PR2 layout). - -## 6. Alternative: no refactor - -Worth considering before doing anything. The current layout works, builds green, and contributors have managed so far. Costs of *not* refactoring: - -- Every "add a new mapping" PR pays the cost of three-directory confusion (small but recurring). -- Every "I want to find the core type" first-look pays one extra level of indirection (one-time per reader, but a friction visible in onboarding). -- The `Hyperbolic/` vs Lobachevsky ambiguity stays. -- Pre-1.0 is the right window — once external users `require` the library, namespace renames become much more expensive. - -If the answer is "wait until v1.0 is closer", do PR1 (the trivial rename) now and defer PR2/PR3 until then. PR1 alone removes the most user-visible ambiguity at near-zero cost. - ---- - -## Recommendation - -Land **PR1 (Hyperbolic → Hyperbolicity)** this week. It's a 5-minute win. - -For PR2 + PR3, decide based on how soon v1.0 is. If v1.0 is months away, do them now — pre-1.0 is the cheapest window. If v1.0 is weeks away, defer to a v1.1 cleanup so it doesn't compete with stabilisation work. - -Awaiting your call on which PRs to run. diff --git a/docs/tutorial.md b/docs/tutorial.md deleted file mode 100644 index 12f40d2..0000000 --- a/docs/tutorial.md +++ /dev/null @@ -1,166 +0,0 @@ -# Tutorial — your first ProofAtlas profile - -> Canonical source. The site mirrors this at -> [`/docs/tutorial`](https://proof-atlas.vercel.app/docs/tutorial). - -This walk-through gets you from a blank Lean 4 project to a working -`#atlas.profile` in about five minutes. You will need: - -- A working Lean 4 toolchain (`elan` + `lake`). -- VS Code with the Lean 4 extension (for the interactive InfoView, - which is where most `#atlas.*` output renders). - -## 1. Add ProofAtlas to your `lakefile.toml` - -In your project's `lakefile.toml`: - -```toml -[[require]] -name = "proofatlas" -git = "https://github.com/MathNetwork/ProofAtlas" -rev = "main" -subDir = "lean" -``` - -Your `lean-toolchain` must match ProofAtlas's. Check -[`lean/lean-toolchain`](https://github.com/MathNetwork/ProofAtlas/blob/main/lean/lean-toolchain) -in the repo for the current value (currently `leanprover/lean4:v4.30.0-rc2`). - -Then: - -```bash -lake update -lake build -``` - -`lake update` fetches ProofAtlas and Mathlib; `lake build` compiles -them. The first build downloads the Mathlib cache and takes a few -minutes; subsequent builds are incremental. - -For a local checkout instead of the git remote, swap the `git`/`rev` -lines for `path = "/absolute/path/to/hypergraph-geo/lean"`. - -## 2. Try the commands in a `.lean` file - -Open any `.lean` file in your project, side-by-side with the Lean -InfoView pane (in VS Code: `Cmd+Shift+P` → *Lean 4: Infoview: Toggle -Infoview*). Type: - -```lean -import ProofAtlas - -#atlas.graph Nat.add_zero -``` - -The InfoView should show an interactive force-directed graph: small -open circles for subterm vertices, coloured rectangles for hyperedges -(constructor applications), with two range sliders for tuning the -d3-force simulation. Hover any node for a tooltip; drag to pin. - -Next, ask for the full report: - -```lean -#atlas.profile Nat.add_zero -``` - -You'll see the same graph plus a text summary: - -``` -Hypergraph Report - vertices 7 - edges 3 [app=3] - diameter 1.333 - mean dist 1.000 - δ 0.000 - δ / diam 0.000 -``` - -`δ = 0` means the proof is exactly tree-like at this scale (no -non-trivial four-point deviation). - -## 3. Glue two declarations and measure the resistance between them - -```lean -#atlas.resistance Nat.add_zero Nat.add_comm --- R(Nat.add_zero, Nat.add_comm) = 1.179960 -``` - -The two proof terms are merged into a single hypergraph via -*const-sharing*: every `.const Name` reference (`Nat`, `HAdd.hAdd`, -`Eq`, ...) appears as one shared vertex. The result is the effective -electrical resistance between the two root vertices on that combined -graph. - -Read it as "how independent are these two propositions, given the -shared infrastructure they both rely on?" Smaller = more shared -backbone, more entangled. Larger = more independent. - -To see the same glue visually: - -```lean -#atlas.graph Nat.add_zero Nat.add_comm -``` - -The single connected component you see is the const-shared union of -both proof terms. - -## 4. Try the decl-level view - -The commands above work at the *subterm* level (vertices = subterm -occurrences). For a wider lens — vertices = whole declarations, -edges = co-references between them — use the `.ns` variants: - -```lean -#atlas.graph.ns ProofAtlas.Pipeline --- Decl-level hypergraph (ProofAtlas.Pipeline): 240 vertices, 1100 hyperedges -``` - -The first call to any `.ns` command builds a cache of the namespace's -decl-graph (~2 seconds for ~200 declarations); subsequent -`.resistance.ns` and `.delta.ns` queries reuse it. - -## 5. Export from the CLI (no editor required) - -For shell pipelines, snapshots, or any non-interactive use, run: - -```bash -cd lean -lake exe atlas-export Nat.add_zero -``` - -This prints a JSON document with the incidence data, resistance -matrix, and δ. The schema is documented at -[`Export/Profile.lean`](../lean/ProofAtlas/Export/Profile.lean) (a -formally-versioned schema is on the roadmap). - -## Next steps - -- Run any registered metric's quality check: - - ```lean - #atlas.status commuteTimeDist - -- ✓ certified: Hypergraph.commuteTimeDist_isHypergraphMetric is sorry-free. - ``` - -- Browse the [command reference](https://proof-atlas.vercel.app/docs/commands). -- Read the [architecture overview](architecture.md) to see how the - layers fit together. -- Try a how-to: - [add a new mapping](how-to/add-a-mapping.md) or - [add a new metric](how-to/add-a-metric.md). -- See [ROADMAP.md](ROADMAP.md) for where the project is going, - and [STATUS.md](STATUS.md) for what's still open. - -## If something goes wrong - -- `unknown identifier '#atlas.graph'` — `import ProofAtlas` is - missing, or `lake build` hasn't finished. -- *InfoView blank where a widget should be* — the widget didn't - load; check the VS Code Output panel for the Lean server log. -- *Mathlib cache miss on `lake build`* — re-run - `lake exe cache get` then `lake build`. -- A `.ns` command takes 30+ seconds the first time you run it on a - large namespace — that's the C(n,4) δ-enumeration; subsequent - calls are cached. -- Anything else: open an issue with the - [bug report template](https://github.com/MathNetwork/ProofAtlas/issues/new?template=bug_report.md). From 78a219c5bd464322ae81f614fc8f9d53ebf88846 Mon Sep 17 00:00:00 2001 From: Xinze-Li-Moqian <70414198+Xinze-Li-Moqian@users.noreply.github.com> Date: Wed, 27 May 2026 11:59:30 -0400 Subject: [PATCH 09/21] docs(milestones): add two-layer Astrolabe architecture to phase-1 --- docs/milestones/phase-1.md | 39 +++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/docs/milestones/phase-1.md b/docs/milestones/phase-1.md index 5a9272f..4648d38 100644 --- a/docs/milestones/phase-1.md +++ b/docs/milestones/phase-1.md @@ -5,14 +5,39 @@ and the governance infrastructure. Code follows from the spec. ## Proof hypergraph definition (#58) -- [ ] Resolve vertex identity: content hash vs other -- [ ] Resolve vertex structure: what data does a vertex carry -- [ ] Resolve construction method: recursive? builder pattern? -- [ ] Resolve relationship to Lean.Expr -- [ ] Resolve edge semantics: is current Hyperedge right? -- [ ] Resolve acyclicity/connectivity: axiom vs derived? +Two-layer architecture based on Astrolabe (arXiv:2604.10435): + +### Layer 1: AstroNet (generic content-addressable hypergraph) + +From the Astrolabe paper. This is the reusable foundation, not +tied to proof structure or any specific domain. + +- **AstroNerve** = (identity, references, record) + - identity: SHA-256 hash of record content (not of references) + - references: ordered list of other nerve identities (arbitrary length) + - record: opaque string interpreted by domain-specific plugins +- **AstroNet** = collection of nerves satisfying 5 axioms: + content-addressing, self-reference (atoms), injectivity, + closure, no duplicate/self-references for width > 0 +- Depth filtration: inductive stratification by reference nesting + +### Layer 2: BDF proof hypergraph (ProofAtlas-specific) + +Built on top of AstroNet by adding domain constraints: + +- References split into **input** (premises) and **output** (conclusions) +- **Colour** label on each edge (inference rule type) +- **Acyclicity** axiom (no proposition is its own descendant) +- **Connectivity** (one connected component) + +### Design tasks + +- [ ] Formalize AstroNerve / AstroNet in Lean (Layer 1) +- [ ] Define BDF constraints on top (Layer 2) +- [ ] Resolve: how does AstroNerve.record map to Lean.Expr? +- [ ] Resolve: is depth filtration an axiom or derived property? - [ ] Write definition in white paper (natural language + math) -- [ ] Write ADR documenting the chosen design +- [ ] Write ADR documenting the layered design - [ ] Implement in Lean on development branch - [ ] Review, test, PR to main From 7db83883848a50b16d8761ab7b2b1a3c9b994a9d Mon Sep 17 00:00:00 2001 From: Xinze-Li-Moqian <70414198+Xinze-Li-Moqian@users.noreply.github.com> Date: Wed, 27 May 2026 12:02:08 -0400 Subject: [PATCH 10/21] docs(spec): update Basic.lean with two-layer Astrolabe architecture --- lean/ProofAtlas/Hypergraph/Basic.lean | 45 +++++++++++---------------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/lean/ProofAtlas/Hypergraph/Basic.lean b/lean/ProofAtlas/Hypergraph/Basic.lean index ce417cb..9e6884d 100644 --- a/lean/ProofAtlas/Hypergraph/Basic.lean +++ b/lean/ProofAtlas/Hypergraph/Basic.lean @@ -7,38 +7,31 @@ Authors: MathNetwork /-! # Proof hypergraph — design specification -This file specifies the proof hypergraph: the combinatorial -structure that ProofAtlas extracts from formal proofs. +Two-layer architecture (see `docs/milestones/phase-1.md`): -## Design intent +## Layer 1: AstroNet (arXiv:2604.10435) -A proof hypergraph is a directed, ordered, coloured, acyclic -hypergraph where: +Generic content-addressable hypergraph. Reusable foundation. -- **Vertices** are explicit objects with identity and kind - (proposition, term, inference step, etc.), not a bare type - parameter. -- **Hyperedges** represent inference steps: ordered inputs - (premises) → ordered outputs (conclusions), labelled with a - colour (inference rule type). -- **Acyclicity** ensures the graph represents a valid derivation. -- **Connectivity** ensures the graph is one connected component. +- **AstroNerve** = (identity, references, record) + - identity: SHA-256 hash of record (not of references) + - references: ordered list of nerve identities + - record: opaque content string +- **AstroNet** = collection of nerves with 5 axioms: + content-addressing, self-reference, injectivity, closure, + no duplicate/self-references for width > 0 +- Depth filtration: inductive stratification by nesting -## What needs to be defined +## Layer 2: Proof hypergraph (ProofAtlas-specific) -1. `Vertex` — a structure with id, kind, and optional metadata. -2. `Hyperedge` — ordered inputs/outputs referencing vertices, - with colour label. Inputs/outputs disjoint and injective. -3. `Hypergraph` — a collection of vertices and edges satisfying - acyclicity and connectivity. -4. Incidence predicates, reachability, automorphisms. +Built on AstroNet by adding: -## Current status +- References split into **input** / **output** +- **Colour** label (inference rule type) +- **Acyclicity** (no self-descendant) +- **Connectivity** (one component) -This definition is being redesigned on the `development` branch. -The previous implementation used a bare type parameter `V` for -vertices, which prevented vertices from carrying any structural -information. The new design will make vertices first-class objects. +## Status -See the `development` branch for work in progress. +Definition being designed. See issue #58. -/ From 3e340e9e9eb739c000af498a965bdc5e9eafe1f7 Mon Sep 17 00:00:00 2001 From: Xinze-Li-Moqian <70414198+Xinze-Li-Moqian@users.noreply.github.com> Date: Wed, 27 May 2026 12:14:25 -0400 Subject: [PATCH 11/21] docs(milestones): three-layer architecture spec with definitions --- docs/milestones/phase-1.md | 117 ++++++++++++++++++++++++++++--------- 1 file changed, 90 insertions(+), 27 deletions(-) diff --git a/docs/milestones/phase-1.md b/docs/milestones/phase-1.md index 4648d38..b884a3a 100644 --- a/docs/milestones/phase-1.md +++ b/docs/milestones/phase-1.md @@ -5,41 +5,104 @@ and the governance infrastructure. Code follows from the spec. ## Proof hypergraph definition (#58) -Two-layer architecture based on Astrolabe (arXiv:2604.10435): +Three-layer architecture. Each layer adds constraints to the +previous one. Start from the most abstract, add structure only +when needed. -### Layer 1: AstroNet (generic content-addressable hypergraph) +--- -From the Astrolabe paper. This is the reusable foundation, not -tied to proof structure or any specific domain. +### Layer 0: AstroNet (maximally abstract) -- **AstroNerve** = (identity, references, record) - - identity: SHA-256 hash of record content (not of references) - - references: ordered list of other nerve identities (arbitrary length) - - record: opaque string interpreted by domain-specific plugins -- **AstroNet** = collection of nerves satisfying 5 axioms: - content-addressing, self-reference (atoms), injectivity, - closure, no duplicate/self-references for width > 0 -- Depth filtration: inductive stratification by reference nesting +A content-addressable hypergraph with no restrictions. -### Layer 2: BDF proof hypergraph (ProofAtlas-specific) +**Definition 0.1 (AstroNerve).** An *AstroNerve* is a triple +`n = (id, refs, rec)` where: +- `id` is the identity, computed as `hash(rec)` +- `refs` is an ordered list of identities (may include `id` itself) +- `rec` is the record (opaque content) -Built on top of AstroNet by adding domain constraints: +That's it. No restrictions on self-reference, no input/output +distinction, no arity constraints, no acyclicity. A nerve can +reference itself, reference nothing, or reference the same +identity multiple times. -- References split into **input** (premises) and **output** (conclusions) -- **Colour** label on each edge (inference rule type) -- **Acyclicity** axiom (no proposition is its own descendant) -- **Connectivity** (one connected component) +**Definition 0.2 (AstroNet).** An *AstroNet* is a set `N` of +AstroNerves satisfying: +1. **Content-addressing.** `n.id = hash(n.rec)` for all `n ∈ N` +2. **Injectivity.** `n.id = m.id ⟹ n = m` (identity determines nerve) +3. **Closure.** Every referenced identity exists in `N` -### Design tasks +Three axioms. Nothing else. -- [ ] Formalize AstroNerve / AstroNet in Lean (Layer 1) -- [ ] Define BDF constraints on top (Layer 2) -- [ ] Resolve: how does AstroNerve.record map to Lean.Expr? -- [ ] Resolve: is depth filtration an axiom or derived property? -- [ ] Write definition in white paper (natural language + math) -- [ ] Write ADR documenting the layered design -- [ ] Implement in Lean on development branch -- [ ] Review, test, PR to main +**Derived notions:** +- *Width* `w(n) := |refs|` +- *Atom*: a nerve with `w(n) = 0` +- *Depth*: inductively defined when no cycles; `-1` for cyclic nerves + +--- + +### Layer 1: Directed hypergraph + +Add direction by partitioning references. + +**Definition 1.1 (Directed nerve).** A *directed nerve* is an +AstroNerve whose references are partitioned into: +- `inputs = (i₁, ..., iₚ)` — premises +- `outputs = (o₁, ..., oq)` — conclusions + +No constraint on `p` or `q` (either can be 0). +No constraint on overlap (an id can appear in both inputs and outputs). + +**Definition 1.2 (Directed AstroNet).** An AstroNet where every +nerve is a directed nerve. + +--- + +### Layer 2: Proof hypergraph (ProofAtlas-specific) + +Add the constraints that make this a well-formed proof structure. + +**Definition 2.1 (Proof nerve).** A directed nerve with: +- `p ≥ 1, q ≥ 1` (at least one premise and one conclusion) +- `inputs ∩ outputs = ∅` (no id is both premise and conclusion + of the same inference) +- A *colour* `c ∈ C` (inference rule type) + +**Definition 2.2 (Proof hypergraph).** A directed AstroNet where: +- Every non-atom nerve is a proof nerve +- **Acyclic**: no nerve is its own descendant via the + input→output relation +- **Connected**: the undirected incidence graph is connected + +--- + +### Design questions (open) + +- [ ] In Lean, is `hash` an abstract function (`H : Record → Id`) + or a concrete implementation (SHA-256)? +- [ ] Is `Record` a `ByteArray`, a `String`, or a Lean `Expr`? +- [ ] Should `refs` be `List Id` or `Array Id` or `Finset Id`? +- [ ] Is depth a field, a computable function, or a proof-only predicate? + +### First target: formalize Layer 0 + +The first deliverable is Definitions 0.1–0.2 in Lean. + +**What this gives us:** +- The most general content-addressable hypergraph +- Reusable beyond ProofAtlas (any knowledge graph) +- Hash identity = deduplication and cross-graph comparison for free + +**Breakdown:** +- [ ] `AstroNerve` structure in Lean +- [ ] `AstroNet` structure with 3 axioms +- [ ] Basic lemmas: closure, injectivity consequences +- [ ] Test: construct a small net by hand + +**After Layer 0 is on main**, proceed to Layer 1 (direction), +then Layer 2 (proof constraints). + +--- ## White paper From 7a790fa178695717db858ff76e431ad63d18b98c Mon Sep 17 00:00:00 2001 From: Xinze-Li-Moqian <70414198+Xinze-Li-Moqian@users.noreply.github.com> Date: Wed, 27 May 2026 12:25:19 -0400 Subject: [PATCH 12/21] feat: add AstroNerve Layer 0 structure (#58) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Content-addressable by construction: id is derived from record via the identify function, not stored as a field. No constraints at this layer — atoms, self-reference, and hyperedge semantics are defined in upper layers. --- lean/ProofAtlas.lean | 1 + lean/ProofAtlas/AstroNerve.lean | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 lean/ProofAtlas/AstroNerve.lean diff --git a/lean/ProofAtlas.lean b/lean/ProofAtlas.lean index b853dfd..b9be00d 100644 --- a/lean/ProofAtlas.lean +++ b/lean/ProofAtlas.lean @@ -5,6 +5,7 @@ Authors: MathNetwork -/ import ProofAtlas.Util.Linter.MathTag import ProofAtlas.Util.Linter.ImportBan +import ProofAtlas.AstroNerve import ProofAtlas.Hypergraph.Basic /-! diff --git a/lean/ProofAtlas/AstroNerve.lean b/lean/ProofAtlas/AstroNerve.lean new file mode 100644 index 0000000..2e1608e --- /dev/null +++ b/lean/ProofAtlas/AstroNerve.lean @@ -0,0 +1,27 @@ +/- +Copyright (c) 2026 MathNetwork. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: MathNetwork +-/ + +/-! +# AstroNerve — Layer 0 + +A node in a content-addressable hypergraph. +Content-addressing is built into the definition: identity is +not a field but a function of the record. + +See `docs/milestones/phase-1.md` and arXiv:2604.10435. +-/ + +structure AstroNerve {Id : Type} {Record : Type} (identify : Record → Id) where + refs : List Id + record : Record + +namespace AstroNerve + +variable {Id : Type} {Record : Type} {identify : Record → Id} + +def id (n : AstroNerve identify) : Id := identify n.record + +end AstroNerve From fe1ca5c10e529738eb07a5ccc68df9ac3a2f19fe Mon Sep 17 00:00:00 2001 From: Xinze-Li-Moqian <70414198+Xinze-Li-Moqian@users.noreply.github.com> Date: Wed, 27 May 2026 17:05:56 -0400 Subject: [PATCH 13/21] feat: Layer 0 AstroNerve + AstroNet, separate Demo/ (#58) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AstroCore/Core.lean: AstroNerve structure + AstroNet + composable axiom classes (IsInjective, IsClosed) - identify now takes record + refs (Merkle property) - refs carry abstract role tags: List (Role × Id) - Demo/ separated from source (like Mathlib/MathlibTest) - CLAUDE.md: every module must have a demo --- CLAUDE.md | 3 ++ lean/Demo.lean | 1 + lean/Demo/AstroCore.lean | 73 +++++++++++++++++++++++++++++ lean/ProofAtlas.lean | 2 +- lean/ProofAtlas/AstroCore/Core.lean | 70 +++++++++++++++++++++++++++ lean/ProofAtlas/AstroNerve.lean | 27 ----------- lean/lakefile.toml | 4 ++ 7 files changed, 152 insertions(+), 28 deletions(-) create mode 100644 lean/Demo.lean create mode 100644 lean/Demo/AstroCore.lean create mode 100644 lean/ProofAtlas/AstroCore/Core.lean delete mode 100644 lean/ProofAtlas/AstroNerve.lean diff --git a/CLAUDE.md b/CLAUDE.md index 679f747..ac34b44 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,6 +20,9 @@ enforcement lives in CI and the Lean type system, not here. ## Conventions (not yet automated) - **ADRs** for cross-file architectural decisions (`docs/adr/`). +- **Every module must have a demo.** Source code goes in + `ProofAtlas/`, corresponding demos go in `Demo/`. A PR + adding a new module without a demo file is incomplete. ## Pointers diff --git a/lean/Demo.lean b/lean/Demo.lean new file mode 100644 index 0000000..f738986 --- /dev/null +++ b/lean/Demo.lean @@ -0,0 +1 @@ +import Demo.AstroCore diff --git a/lean/Demo/AstroCore.lean b/lean/Demo/AstroCore.lean new file mode 100644 index 0000000..206cc55 --- /dev/null +++ b/lean/Demo/AstroCore.lean @@ -0,0 +1,73 @@ +/- +Copyright (c) 2026 MathNetwork. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: MathNetwork +-/ +import ProofAtlas.AstroCore.Core + +/-! +# AstroNerve demos +-/ + +-- Demo roles: just input and output for now. +inductive DemoRole where + | input | output + deriving Repr + +-- Demo identify: hash the record + number of refs. +def demoIdentify (r : String) (refs : List (DemoRole × UInt64)) : UInt64 := + String.hash (r ++ toString refs.length) + +-- Atom: no references. +def atom1 : AstroNerve demoIdentify := + { refs := [] + record := "hello" } + +def atom2 : AstroNerve demoIdentify := + { refs := [] + record := "world" } + +-- Hyperedge: atom1 as input, atom2 as output. +def edge1 : AstroNerve demoIdentify := + { refs := [(.input, atom1.id), (.output, atom2.id)] + record := "hello implies world" } + +#eval atom1.id -- hash of "hello" with 0 refs +#eval atom2.id -- hash of "world" with 0 refs +#eval edge1.id -- hash of "hello implies world" with 2 refs + +/-! ## Demo 2: structured record with colour -/ + +-- A record that carries colour and a label. +structure ProofRecord where + color : String + label : String + deriving Repr + +-- Identify function for ProofRecord: hash colour + label + refs. +def proofIdentify (r : ProofRecord) (refs : List (DemoRole × UInt64)) : UInt64 := + String.hash (r.color ++ "|" ++ r.label ++ "|" ++ toString refs.length) + +-- Two propositions (atoms). +def propA : AstroNerve proofIdentify := + { refs := [] + record := { color := "prop", label := "Nat.add_zero" } } + +def propB : AstroNerve proofIdentify := + { refs := [] + record := { color := "prop", label := "Nat.add_comm" } } + +-- An inference step: propA as input, propB as output, coloured "rewrite". +def step1 : AstroNerve proofIdentify := + { refs := [(.input, propA.id), (.output, propB.id)] + record := { color := "rewrite", label := "add_comm applied" } } + +-- Same structure but different colour → different id. +def step2 : AstroNerve proofIdentify := + { refs := [(.input, propA.id), (.output, propB.id)] + record := { color := "simp", label := "add_comm applied" } } + +#eval propA.id -- hash of prop|Nat.add_zero +#eval propB.id -- hash of prop|Nat.add_comm +#eval step1.id -- hash of rewrite|add_comm applied|2 +#eval step2.id -- hash of simp|add_comm applied|2 ← different colour, different id! diff --git a/lean/ProofAtlas.lean b/lean/ProofAtlas.lean index b9be00d..cff400f 100644 --- a/lean/ProofAtlas.lean +++ b/lean/ProofAtlas.lean @@ -5,7 +5,7 @@ Authors: MathNetwork -/ import ProofAtlas.Util.Linter.MathTag import ProofAtlas.Util.Linter.ImportBan -import ProofAtlas.AstroNerve +import ProofAtlas.AstroCore.Core import ProofAtlas.Hypergraph.Basic /-! diff --git a/lean/ProofAtlas/AstroCore/Core.lean b/lean/ProofAtlas/AstroCore/Core.lean new file mode 100644 index 0000000..8c240d2 --- /dev/null +++ b/lean/ProofAtlas/AstroCore/Core.lean @@ -0,0 +1,70 @@ +/- +Copyright (c) 2026 MathNetwork. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: MathNetwork +-/ + +/-! +# AstroNerve + AstroNet — the most abstract layer + +AstroNerve: a node in a content-addressable hypergraph. +AstroNet: a collection of nerves with composable axioms. +-/ + +-- A nerve has two fields: +-- refs : ordered list of (role, id) pairs — references to other nerves +-- record : the content of this nerve +-- +-- Id, Role, Record are all abstract types — no concrete choice at this layer. +-- +-- `identify` is the content-addressing function: it takes a record +-- and the refs, and produces the identity. Because id is computed +-- (not stored), content-addressing holds by construction. +structure AstroNerve {Id : Type} {Role : Type} {Record : Type} + (identify : Record → List (Role × Id) → Id) where + refs : List (Role × Id) + record : Record + +namespace AstroNerve + +variable {Id : Type} {Role : Type} {Record : Type} + {identify : Record → List (Role × Id) → Id} + +-- Identity is not a field — it is always computed from record + refs. +-- This is the Merkle property: change anything, id changes. +def id (n : AstroNerve identify) : Id := identify n.record n.refs + +end AstroNerve + +-- A net is just a list of nerves. No axioms baked in. +-- Axioms are separate classes — add what you need, ignore what you don't. +structure AstroNet {Id : Type} {Role : Type} {Record : Type} + (identify : Record → List (Role × Id) → Id) where + nerves : List (AstroNerve identify) + +namespace AstroNet + +variable {Id : Type} {Role : Type} {Record : Type} + {identify : Record → List (Role × Id) → Id} + +-- Injectivity: if two nerves in the net have the same id, they are +-- the same nerve. In other words, id uniquely identifies a nerve. +-- +-- n ∈ net, m ∈ net, n.id = m.id → n = m +class IsInjective (net : AstroNet identify) : Prop where + injectivity : + ∀ n m, n ∈ net.nerves → m ∈ net.nerves → + n.id = m.id → n = m + +-- Closure: for every nerve in the net, every id it references +-- (the .2 of each (role, id) pair in refs) must belong to some +-- nerve that also exists in the net. No dangling references. +-- +-- n ∈ net, (role, targetId) ∈ n.refs → ∃ m ∈ net, m.id = targetId +class IsClosed (net : AstroNet identify) : Prop where + closure : + ∀ n, n ∈ net.nerves → + ∀ ref, ref ∈ n.refs → + ∃ m, m ∈ net.nerves ∧ m.id = ref.2 + +end AstroNet diff --git a/lean/ProofAtlas/AstroNerve.lean b/lean/ProofAtlas/AstroNerve.lean deleted file mode 100644 index 2e1608e..0000000 --- a/lean/ProofAtlas/AstroNerve.lean +++ /dev/null @@ -1,27 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ - -/-! -# AstroNerve — Layer 0 - -A node in a content-addressable hypergraph. -Content-addressing is built into the definition: identity is -not a field but a function of the record. - -See `docs/milestones/phase-1.md` and arXiv:2604.10435. --/ - -structure AstroNerve {Id : Type} {Record : Type} (identify : Record → Id) where - refs : List Id - record : Record - -namespace AstroNerve - -variable {Id : Type} {Record : Type} {identify : Record → Id} - -def id (n : AstroNerve identify) : Id := identify n.record - -end AstroNerve diff --git a/lean/lakefile.toml b/lean/lakefile.toml index ed63858..44f307e 100644 --- a/lean/lakefile.toml +++ b/lean/lakefile.toml @@ -17,3 +17,7 @@ leanOptions = [ { name = "weak.linter.mathlibStandardSet", value = true }, { name = "weak.linter.proofAtlasMathTag", value = true }, ] + +[[lean_lib]] +name = "Demo" +roots = ["Demo"] From 166538b46d5c161a8fab3b053590a073408613bf Mon Sep 17 00:00:00 2001 From: Xinze-Li-Moqian <70414198+Xinze-Li-Moqian@users.noreply.github.com> Date: Wed, 27 May 2026 17:07:21 -0400 Subject: [PATCH 14/21] feat(ci): add demo coverage + demo build checks to lint.sh Every module in ProofAtlas/ must have a Demo/ file, and demos must compile. Enforced in lint.sh (checks 4/7 and 5/7). --- scripts/lint.sh | 43 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/scripts/lint.sh b/scripts/lint.sh index d930aca..0c6fc60 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -17,7 +17,7 @@ echo "===============" # - linter.proofAtlasImportBan (full import Mathlib ban) # - linter.proofAtlasMathTag (docstring tags) echo "" -echo "[1/4] lake build" +echo "[1/7] lake build" if (cd "$LEAN" && lake build); then echo " ✓ build green" else @@ -30,7 +30,7 @@ fi # the human-readable summary in CI logs. Will be removed once # all checks are fully migrated to Level 3. echo "" -echo "[2/4] Metric instance check" +echo "[2/7] Metric instance check" if (cd "$LEAN" && bash scripts/check_metric_instances.sh); then echo " ✓ metric instances OK" else @@ -43,7 +43,7 @@ fi # a checked-in baseline. Adding new sorries fails CI; removing # sorries is encouraged (update the baseline to lock in the win). echo "" -echo "[3/4] Sorry regression check" +echo "[3/7] Sorry regression check" BASELINE_FILE="$LEAN/.sorry-baseline" if [ -f "$BASELINE_FILE" ]; then BASELINE=$(cat "$BASELINE_FILE" | tr -d '[:space:]') @@ -64,11 +64,42 @@ else echo " – skipped (no .sorry-baseline file)" fi -# 4. Import hygiene (shake) — warning only, does not block merge. +# 4. Demo coverage: every module in ProofAtlas/ must have a +# corresponding demo file in Demo/. +echo "" +echo "[4/7] Demo coverage" +MISSING_DEMOS=() +for module_dir in "$LEAN"/ProofAtlas/*/; do + module_name=$(basename "$module_dir") + # Skip non-code modules + if [ "$module_name" = "Util" ]; then continue; fi # linters + if [ "$module_name" = "Hypergraph" ]; then continue; fi # spec-only for now + if [ ! -f "$LEAN/Demo/$module_name.lean" ]; then + MISSING_DEMOS+=("$module_name") + fi +done +if [ ${#MISSING_DEMOS[@]} -gt 0 ]; then + echo " ✗ missing demos for: ${MISSING_DEMOS[*]}" + FAIL=1 +else + echo " ✓ all modules have demos" +fi + +# 5. Demo build +echo "" +echo "[5/7] Demo build" +if (cd "$LEAN" && lake build Demo); then + echo " ✓ demo build green" +else + echo " ✗ demo build failed" + FAIL=1 +fi + +# 6. Import hygiene (shake) — warning only, does not block merge. # Many files have sorry'd code whose imports shake misjudges; # once sorries are filled, tighten this to a hard failure. echo "" -echo "[4/5] Import hygiene (shake)" +echo "[6/7] Import hygiene (shake)" if (cd "$LEAN" && lake exe shake 2>&1 | grep -v "^warning:\|^$\|Note:\|⚠\|Replayed\|deprecated\|Built" | head -20) | grep -q "remove"; then echo " ⚠ shake found unnecessary imports (warning only, not blocking)" (cd "$LEAN" && lake exe shake 2>&1 | grep -v "^warning:\|^$\|Note:\|⚠\|Replayed\|deprecated\|Built" | head -20) | sed 's/^/ /' @@ -78,7 +109,7 @@ fi # 5. Whitepaper sync echo "" -echo "[5/5] Whitepaper sync" +echo "[7/7] Whitepaper sync" if [ -f "$ROOT/scripts/check_whitepaper_sync.sh" ]; then if (cd "$ROOT" && bash scripts/check_whitepaper_sync.sh); then echo " ✓ whitepaper sync OK" From b3276881a1776c8c65045b758189f1e63a4ea491 Mon Sep 17 00:00:00 2001 From: Xinze-Li-Moqian <70414198+Xinze-Li-Moqian@users.noreply.github.com> Date: Wed, 27 May 2026 17:24:21 -0400 Subject: [PATCH 15/21] =?UTF-8?q?chore:=20rename=20AstroCore=20=E2=86=92?= =?UTF-8?q?=20Astrolabe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lean/Demo.lean | 3 +- lean/Demo/AstroCore.lean | 73 --------------------- lean/Demo/Astrolabe/Nerve.lean | 83 ++++++++++++++++++++++++ lean/Demo/Astrolabe/Net.lean | 45 +++++++++++++ lean/ProofAtlas.lean | 3 +- lean/ProofAtlas/AstroCore/Core.lean | 70 -------------------- lean/ProofAtlas/Astrolabe/Nerve.lean | 82 ++++++++++++++++++++++++ lean/ProofAtlas/Astrolabe/Net.lean | 80 +++++++++++++++++++++++ lean/Test.lean | 2 + lean/Test/Astrolabe/Nerve.lean | 96 ++++++++++++++++++++++++++++ lean/Test/Astrolabe/Net.lean | 95 +++++++++++++++++++++++++++ lean/lakefile.toml | 4 ++ scripts/lint.sh | 2 +- 13 files changed, 492 insertions(+), 146 deletions(-) delete mode 100644 lean/Demo/AstroCore.lean create mode 100644 lean/Demo/Astrolabe/Nerve.lean create mode 100644 lean/Demo/Astrolabe/Net.lean delete mode 100644 lean/ProofAtlas/AstroCore/Core.lean create mode 100644 lean/ProofAtlas/Astrolabe/Nerve.lean create mode 100644 lean/ProofAtlas/Astrolabe/Net.lean create mode 100644 lean/Test.lean create mode 100644 lean/Test/Astrolabe/Nerve.lean create mode 100644 lean/Test/Astrolabe/Net.lean diff --git a/lean/Demo.lean b/lean/Demo.lean index f738986..39f56b4 100644 --- a/lean/Demo.lean +++ b/lean/Demo.lean @@ -1 +1,2 @@ -import Demo.AstroCore +import Demo.Astrolabe.Nerve +import Demo.Astrolabe.Net diff --git a/lean/Demo/AstroCore.lean b/lean/Demo/AstroCore.lean deleted file mode 100644 index 206cc55..0000000 --- a/lean/Demo/AstroCore.lean +++ /dev/null @@ -1,73 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ -import ProofAtlas.AstroCore.Core - -/-! -# AstroNerve demos --/ - --- Demo roles: just input and output for now. -inductive DemoRole where - | input | output - deriving Repr - --- Demo identify: hash the record + number of refs. -def demoIdentify (r : String) (refs : List (DemoRole × UInt64)) : UInt64 := - String.hash (r ++ toString refs.length) - --- Atom: no references. -def atom1 : AstroNerve demoIdentify := - { refs := [] - record := "hello" } - -def atom2 : AstroNerve demoIdentify := - { refs := [] - record := "world" } - --- Hyperedge: atom1 as input, atom2 as output. -def edge1 : AstroNerve demoIdentify := - { refs := [(.input, atom1.id), (.output, atom2.id)] - record := "hello implies world" } - -#eval atom1.id -- hash of "hello" with 0 refs -#eval atom2.id -- hash of "world" with 0 refs -#eval edge1.id -- hash of "hello implies world" with 2 refs - -/-! ## Demo 2: structured record with colour -/ - --- A record that carries colour and a label. -structure ProofRecord where - color : String - label : String - deriving Repr - --- Identify function for ProofRecord: hash colour + label + refs. -def proofIdentify (r : ProofRecord) (refs : List (DemoRole × UInt64)) : UInt64 := - String.hash (r.color ++ "|" ++ r.label ++ "|" ++ toString refs.length) - --- Two propositions (atoms). -def propA : AstroNerve proofIdentify := - { refs := [] - record := { color := "prop", label := "Nat.add_zero" } } - -def propB : AstroNerve proofIdentify := - { refs := [] - record := { color := "prop", label := "Nat.add_comm" } } - --- An inference step: propA as input, propB as output, coloured "rewrite". -def step1 : AstroNerve proofIdentify := - { refs := [(.input, propA.id), (.output, propB.id)] - record := { color := "rewrite", label := "add_comm applied" } } - --- Same structure but different colour → different id. -def step2 : AstroNerve proofIdentify := - { refs := [(.input, propA.id), (.output, propB.id)] - record := { color := "simp", label := "add_comm applied" } } - -#eval propA.id -- hash of prop|Nat.add_zero -#eval propB.id -- hash of prop|Nat.add_comm -#eval step1.id -- hash of rewrite|add_comm applied|2 -#eval step2.id -- hash of simp|add_comm applied|2 ← different colour, different id! diff --git a/lean/Demo/Astrolabe/Nerve.lean b/lean/Demo/Astrolabe/Nerve.lean new file mode 100644 index 0000000..d2e2917 --- /dev/null +++ b/lean/Demo/Astrolabe/Nerve.lean @@ -0,0 +1,83 @@ +/- +Copyright (c) 2026 MathNetwork. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: MathNetwork +-/ +import ProofAtlas.Astrolabe.Nerve + +/-! +# Demo: AstroNerve only + +Construct nerves, show that id is computed from record + refs. +-/ + +-- Hash record + full refs content (order and ids matter). +def simpleIdentify (r : String) (refs : Array (Unit × UInt64)) : UInt64 := + let refsStr := String.join (refs.toList.map (fun p => toString p.2)) + String.hash (r ++ refsStr) + +def atom1 : AstroNerve simpleIdentify := + { refs := #[], record := "hello" } + +def atom2 : AstroNerve simpleIdentify := + { refs := #[], record := "world" } + +def nerve3 : AstroNerve simpleIdentify := + { refs := #[((), atom1.id), ((), atom2.id)] + record := "connects hello and world" } + +#eval atom1.id +#eval atom2.id +#eval nerve3.id + +-- Same record "hello" but different refs → different id. +def nerve4 : AstroNerve simpleIdentify := + { refs := #[((), atom2.id)] + record := "hello" } + +#eval nerve4.id -- different from atom1.id (Merkle property) + +-- Methods: +#eval atom1.width -- 0 +#eval nerve3.width -- 2 +#eval atom1.isAtom -- true +#eval nerve3.isAtom -- false +#eval nerve3.refIds -- [atom1.id, atom2.id] +#eval nerve3.refRoles -- [(), ()] + +-- Indexing and lookup: +#eval nerve3.refAt 0 -- first ref +#eval nerve3.refAt 99 -- out of bounds → none +#eval nerve3.containsRef atom1.id -- true +#eval nerve3.containsRef 999 -- false + +-- Deriving new nerves from existing ones: +-- Push a ref → new nerve with new id (Merkle: refs changed → id changed). +def nerve3_extended := nerve3.pushRef ((), atom2.id) +#eval nerve3_extended.width -- 3 (was 2) +#eval nerve3_extended.id -- different from nerve3.id + +-- Pop last ref → new nerve with new id. +def nerve3_trimmed := nerve3.popRef +#eval nerve3_trimmed.width -- 1 (was 2) +#eval nerve3_trimmed.id -- different from nerve3.id + +-- Filter: keep only refs whose id matches atom1. +def nerve3_filtered := nerve3.filterRefs (fun ref => ref.2 == atom1.id) +#eval nerve3_filtered.width -- 1 +#eval nerve3_filtered.id -- different again + +-- Swap: swap first and second ref → new nerve, new id. +def nerve3_swapped := nerve3.swapRefs 0 1 +#eval nerve3_swapped.refIds -- [atom2.id, atom1.id] (order flipped) +#eval nerve3_swapped.id -- different from nerve3.id (order matters) + +-- Reverse all refs. +def nerve3_reversed := nerve3.reverseRefs +#eval nerve3_reversed.refIds -- [atom2.id, atom1.id] +#eval nerve3_reversed.id -- same as swapped (same result) + +-- Set: replace first ref with a different one. +def nerve3_replaced := nerve3.setRef 0 ((), atom2.id) +#eval nerve3_replaced.refIds -- [atom2.id, atom2.id] +#eval nerve3_replaced.id -- different diff --git a/lean/Demo/Astrolabe/Net.lean b/lean/Demo/Astrolabe/Net.lean new file mode 100644 index 0000000..b6b36a0 --- /dev/null +++ b/lean/Demo/Astrolabe/Net.lean @@ -0,0 +1,45 @@ +/- +Copyright (c) 2026 MathNetwork. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: MathNetwork +-/ +import ProofAtlas.Astrolabe.Net +import Demo.Astrolabe.Nerve + +/-! +# Demo: AstroNet + +Build a net step by step. `addNerve!` checks closure and +injectivity at compile time. +-/ + +/-! ## ✓ Good: valid net -/ + +def net0 := AstroNet.empty simpleIdentify +def net1 := net0.addNerve! atom1 +def net2 := net1.addNerve! atom2 +def net3 := net2.addNerve! nerve3 + +#eval net3.size -- 3 +#eval net3.ids -- all ids +#eval net3.hasId atom1.id -- true + +/-! ## ✗ Bad: dangling reference — uncomment to see compile error + +``` +def dangling : AstroNerve simpleIdentify := + { refs := #[((), 999)], record := "bad" } + +def badNet := net0.addNerve! dangling -- FAILS: 999 not in net +``` +-/ + +/-! ## ✗ Bad: duplicate id — uncomment to see compile error + +``` +def atom1_copy : AstroNerve simpleIdentify := + { refs := #[], record := "hello" } -- same id as atom1 + +def badNet2 := net1.addNerve! atom1_copy -- FAILS: id already exists +``` +-/ diff --git a/lean/ProofAtlas.lean b/lean/ProofAtlas.lean index cff400f..0b7aae2 100644 --- a/lean/ProofAtlas.lean +++ b/lean/ProofAtlas.lean @@ -5,7 +5,8 @@ Authors: MathNetwork -/ import ProofAtlas.Util.Linter.MathTag import ProofAtlas.Util.Linter.ImportBan -import ProofAtlas.AstroCore.Core +import ProofAtlas.Astrolabe.Nerve +import ProofAtlas.Astrolabe.Net import ProofAtlas.Hypergraph.Basic /-! diff --git a/lean/ProofAtlas/AstroCore/Core.lean b/lean/ProofAtlas/AstroCore/Core.lean deleted file mode 100644 index 8c240d2..0000000 --- a/lean/ProofAtlas/AstroCore/Core.lean +++ /dev/null @@ -1,70 +0,0 @@ -/- -Copyright (c) 2026 MathNetwork. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Authors: MathNetwork --/ - -/-! -# AstroNerve + AstroNet — the most abstract layer - -AstroNerve: a node in a content-addressable hypergraph. -AstroNet: a collection of nerves with composable axioms. --/ - --- A nerve has two fields: --- refs : ordered list of (role, id) pairs — references to other nerves --- record : the content of this nerve --- --- Id, Role, Record are all abstract types — no concrete choice at this layer. --- --- `identify` is the content-addressing function: it takes a record --- and the refs, and produces the identity. Because id is computed --- (not stored), content-addressing holds by construction. -structure AstroNerve {Id : Type} {Role : Type} {Record : Type} - (identify : Record → List (Role × Id) → Id) where - refs : List (Role × Id) - record : Record - -namespace AstroNerve - -variable {Id : Type} {Role : Type} {Record : Type} - {identify : Record → List (Role × Id) → Id} - --- Identity is not a field — it is always computed from record + refs. --- This is the Merkle property: change anything, id changes. -def id (n : AstroNerve identify) : Id := identify n.record n.refs - -end AstroNerve - --- A net is just a list of nerves. No axioms baked in. --- Axioms are separate classes — add what you need, ignore what you don't. -structure AstroNet {Id : Type} {Role : Type} {Record : Type} - (identify : Record → List (Role × Id) → Id) where - nerves : List (AstroNerve identify) - -namespace AstroNet - -variable {Id : Type} {Role : Type} {Record : Type} - {identify : Record → List (Role × Id) → Id} - --- Injectivity: if two nerves in the net have the same id, they are --- the same nerve. In other words, id uniquely identifies a nerve. --- --- n ∈ net, m ∈ net, n.id = m.id → n = m -class IsInjective (net : AstroNet identify) : Prop where - injectivity : - ∀ n m, n ∈ net.nerves → m ∈ net.nerves → - n.id = m.id → n = m - --- Closure: for every nerve in the net, every id it references --- (the .2 of each (role, id) pair in refs) must belong to some --- nerve that also exists in the net. No dangling references. --- --- n ∈ net, (role, targetId) ∈ n.refs → ∃ m ∈ net, m.id = targetId -class IsClosed (net : AstroNet identify) : Prop where - closure : - ∀ n, n ∈ net.nerves → - ∀ ref, ref ∈ n.refs → - ∃ m, m ∈ net.nerves ∧ m.id = ref.2 - -end AstroNet diff --git a/lean/ProofAtlas/Astrolabe/Nerve.lean b/lean/ProofAtlas/Astrolabe/Nerve.lean new file mode 100644 index 0000000..0331383 --- /dev/null +++ b/lean/ProofAtlas/Astrolabe/Nerve.lean @@ -0,0 +1,82 @@ +/- +Copyright (c) 2026 MathNetwork. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: MathNetwork +-/ + +/-! +# AstroNerve — a node in a content-addressable hypergraph + +Identity is computed from record + refs (Merkle property). +Each reference carries an abstract role tag. +-/ + +-- Two fields: refs (ordered references with roles) and record (content). +-- Id, Role, Record are abstract types. identify computes id from +-- record + refs — content-addressing by construction. +structure AstroNerve {Id : Type} {Role : Type} {Record : Type} + (identify : Record → Array (Role × Id) → Id) where + refs : Array (Role × Id) + record : Record + deriving DecidableEq, Repr + +namespace AstroNerve + +variable {Id : Type} {Role : Type} {Record : Type} + {identify : Record → Array (Role × Id) → Id} + +-- Compute identity from record + refs. Not stored — always derived. +def id (n : AstroNerve identify) : Id := identify n.record n.refs + +-- Number of references this nerve has. +def width (n : AstroNerve identify) : Nat := n.refs.size + +-- True if the nerve has no references (an atom / leaf node). +def isAtom (n : AstroNerve identify) : Bool := n.refs.isEmpty + +-- All referenced ids, in order, without roles. +def refIds (n : AstroNerve identify) : Array Id := n.refs.map (·.2) + +-- All roles, in order, without ids. +def refRoles (n : AstroNerve identify) : Array Role := n.refs.map (·.1) + +-- Does this nerve reference a given id? +def containsRef [BEq Id] (n : AstroNerve identify) (i : Id) : Bool := + n.refs.any (fun ref => ref.2 == i) + +-- Get the i-th ref (returns Option to handle out-of-bounds). +def refAt (n : AstroNerve identify) (i : Nat) : Option (Role × Id) := + n.refs[i]? + +-- Add a ref at the end. Returns a new nerve (new id, Merkle property). +def pushRef (n : AstroNerve identify) (ref : Role × Id) : AstroNerve identify := + { refs := n.refs.push ref, record := n.record } + +-- Remove the last ref. Returns a new nerve (new id). +def popRef (n : AstroNerve identify) : AstroNerve identify := + { refs := n.refs.pop, record := n.record } + +-- Keep only refs matching a predicate. Returns a new nerve (new id). +def filterRefs (n : AstroNerve identify) (p : (Role × Id) → Bool) : AstroNerve identify := + { refs := n.refs.filter p, record := n.record } + +-- Swap two refs by index. Returns a new nerve (new id). +-- Panics if indices out of bounds. +def swapRefs [Inhabited Role] [Inhabited Id] + (n : AstroNerve identify) (i j : Nat) : AstroNerve identify := + let a := n.refs + let tmp := a[i]! + let a := a.set! i (a[j]!) + let a := a.set! j tmp + { refs := a, record := n.record } + +-- Reverse the order of all refs. Returns a new nerve (new id). +def reverseRefs (n : AstroNerve identify) : AstroNerve identify := + { refs := n.refs.reverse, record := n.record } + +-- Replace ref at position i. Returns a new nerve (new id). +-- Panics if index out of bounds. +def setRef (n : AstroNerve identify) (i : Nat) (ref : Role × Id) : AstroNerve identify := + { refs := n.refs.set! i ref, record := n.record } + +end AstroNerve diff --git a/lean/ProofAtlas/Astrolabe/Net.lean b/lean/ProofAtlas/Astrolabe/Net.lean new file mode 100644 index 0000000..844efe5 --- /dev/null +++ b/lean/ProofAtlas/Astrolabe/Net.lean @@ -0,0 +1,80 @@ +/- +Copyright (c) 2026 MathNetwork. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: MathNetwork +-/ +import ProofAtlas.Astrolabe.Nerve + +/-! +# AstroNet — a collection of nerves + +Built incrementally via `empty` and `addNerve!`. +Injectivity and closure checked at compile time. +-/ + +-- A net is an array of nerves sharing the same identify function. +structure AstroNet {Id : Type} [DecidableEq Id] + {Role : Type} {Record : Type} + (identify : Record → Array (Role × Id) → Id) where + nerves : Array (AstroNerve identify) + +namespace AstroNet + +variable {Id : Type} [DecidableEq Id] + {Role : Type} [DecidableEq Role] {Record : Type} [DecidableEq Record] + {identify : Record → Array (Role × Id) → Id} + +-- Create an empty net. +def empty (identify : Record → Array (Role × Id) → Id) : AstroNet identify where + nerves := #[] + +-- Number of nerves in the net. +def size (net : AstroNet identify) : Nat := net.nerves.size + +-- Check if any nerve in the net has this id. +def hasId (net : AstroNet identify) (i : Id) : Bool := + net.nerves.any (fun n => n.id == i) + +-- Find the nerve with this id. Returns none if not found. +def findById (net : AstroNet identify) (i : Id) : Option (AstroNerve identify) := + net.nerves.find? (fun n => n.id == i) + +-- Get the record of the nerve with this id. Returns none if not found. +def getRecord (net : AstroNet identify) (i : Id) : Option Record := + (net.findById i).map (·.record) + +-- Check if a nerve can be safely added: +-- all refs must exist in the net (closure), and id must be fresh (injectivity). +def canAdd (net : AstroNet identify) (n : AstroNerve identify) : Bool := + n.refs.all (fun ref => net.hasId ref.2) && + !net.hasId n.id + +-- Add a nerve. Closure + injectivity checked at compile time. +-- If the check fails, the file won't compile. +def addNerve! (net : AstroNet identify) (n : AstroNerve identify) + (_h : net.canAdd n = true := by native_decide) : + AstroNet identify where + nerves := net.nerves.push n + +-- All ids in the net, in insertion order. +def ids (net : AstroNet identify) : Array Id := + net.nerves.map (·.id) + +-- All atoms (nerves with no refs). +def atoms (net : AstroNet identify) : Array (AstroNerve identify) := + net.nerves.filter (·.isAtom) + +-- All non-atoms (nerves with refs). +def edges (net : AstroNet identify) : Array (AstroNerve identify) := + net.nerves.filter (!·.isAtom) + +-- All nerves that reference a given id (reverse lookup). +def refsTo (net : AstroNet identify) (i : Id) : Array (AstroNerve identify) := + net.nerves.filter (·.containsRef i) + +-- Does the net contain a nerve with this id? +-- (same as hasId, but takes a nerve instead of an id) +def contains (net : AstroNet identify) (n : AstroNerve identify) : Bool := + net.hasId n.id + +end AstroNet diff --git a/lean/Test.lean b/lean/Test.lean new file mode 100644 index 0000000..46b5bb2 --- /dev/null +++ b/lean/Test.lean @@ -0,0 +1,2 @@ +import Test.Astrolabe.Nerve +import Test.Astrolabe.Net diff --git a/lean/Test/Astrolabe/Nerve.lean b/lean/Test/Astrolabe/Nerve.lean new file mode 100644 index 0000000..b484bb1 --- /dev/null +++ b/lean/Test/Astrolabe/Nerve.lean @@ -0,0 +1,96 @@ +/- +Copyright (c) 2026 MathNetwork. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: MathNetwork +-/ +import ProofAtlas.Astrolabe.Nerve + +/-! +# Tests: AstroNerve + +Every method has at least one `#guard` assertion. +If any assertion is false, the file fails to compile. +-/ + +-- Hash record + full refs content (not just size). +-- This makes id sensitive to order and content of refs. +def simpleIdentify (r : String) (refs : Array (Unit × UInt64)) : UInt64 := + let refsStr := String.join (refs.toList.map (fun p => toString p.2)) + String.hash (r ++ refsStr) + +def atom1 : AstroNerve simpleIdentify := + { refs := #[], record := "hello" } + +def atom2 : AstroNerve simpleIdentify := + { refs := #[], record := "world" } + +def nerve3 : AstroNerve simpleIdentify := + { refs := #[((), atom1.id), ((), atom2.id)] + record := "connects hello and world" } + +-- id: same record + same refs → same id +#guard atom1.id == (AstroNerve.mk (identify := simpleIdentify) #[] "hello").id + +-- id: different record → different id +#guard atom1.id != atom2.id + +-- id: same record, different refs → different id (Merkle) +def nerve_hello_with_ref : AstroNerve simpleIdentify := + { refs := #[((), atom2.id)], record := "hello" } +#guard atom1.id != nerve_hello_with_ref.id + +-- width +#guard atom1.width == 0 +#guard atom2.width == 0 +#guard nerve3.width == 2 + +-- isAtom +#guard atom1.isAtom == true +#guard nerve3.isAtom == false + +-- refIds +#guard nerve3.refIds == #[atom1.id, atom2.id] + +-- refRoles +#guard nerve3.refRoles == #[(), ()] + +-- containsRef +#guard nerve3.containsRef atom1.id == true +#guard nerve3.containsRef atom2.id == true +#guard nerve3.containsRef 999 == false +#guard atom1.containsRef atom1.id == false + +-- refAt +#guard nerve3.refAt 0 == some ((), atom1.id) +#guard nerve3.refAt 1 == some ((), atom2.id) +#guard nerve3.refAt 99 == none + +-- pushRef: adds one, width increases, id changes +def pushed := nerve3.pushRef ((), atom2.id) +#guard pushed.width == 3 +#guard pushed.id != nerve3.id + +-- popRef: removes last, width decreases, id changes +def popped := nerve3.popRef +#guard popped.width == 1 +#guard popped.id != nerve3.id + +-- filterRefs: keep matching, id changes +def filtered := nerve3.filterRefs (fun ref => ref.2 == atom1.id) +#guard filtered.width == 1 +#guard filtered.refIds == #[atom1.id] + +-- swapRefs: order changes, id changes +def swapped := nerve3.swapRefs 0 1 +#guard swapped.refIds == #[atom2.id, atom1.id] +#guard swapped.id != nerve3.id + +-- reverseRefs: same as swap for 2 elements +def reversed := nerve3.reverseRefs +#guard reversed.refIds == #[atom2.id, atom1.id] +#guard reversed.id == swapped.id -- same result → same id + +-- setRef: replace, id changes +def replaced := nerve3.setRef 0 ((), atom2.id) +#guard replaced.refIds == #[atom2.id, atom2.id] +#guard replaced.id != nerve3.id diff --git a/lean/Test/Astrolabe/Net.lean b/lean/Test/Astrolabe/Net.lean new file mode 100644 index 0000000..6248d9c --- /dev/null +++ b/lean/Test/Astrolabe/Net.lean @@ -0,0 +1,95 @@ +/- +Copyright (c) 2026 MathNetwork. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: MathNetwork +-/ +import ProofAtlas.Astrolabe.Net + +/-! +# Tests: AstroNet + +Every method has at least one `#guard` assertion. +-/ + +namespace NetTest + +private def simpleIdentify (r : String) (refs : Array (Unit × UInt64)) : UInt64 := + let refsStr := String.join (refs.toList.map (fun p => toString p.2)) + String.hash (r ++ refsStr) + +def atom1 : AstroNerve simpleIdentify := + { refs := #[], record := "hello" } + +def atom2 : AstroNerve simpleIdentify := + { refs := #[], record := "world" } + +def nerve3 : AstroNerve simpleIdentify := + { refs := #[((), atom1.id), ((), atom2.id)] + record := "connects" } + +-- empty +def net0 := AstroNet.empty simpleIdentify +#guard net0.size == 0 + +-- addNerve! atom1 +def net1 := net0.addNerve! atom1 +#guard net1.size == 1 + +-- addNerve! atom2 +def net2 := net1.addNerve! atom2 +#guard net2.size == 2 + +-- addNerve! nerve3 (refs point to atom1, atom2 — both in net2) +def net3 := net2.addNerve! nerve3 +#guard net3.size == 3 + +-- hasId +#guard net3.hasId atom1.id == true +#guard net3.hasId atom2.id == true +#guard net3.hasId nerve3.id == true +#guard net3.hasId 999 == false + +-- ids +#guard net3.ids.size == 3 + +-- findById +#guard (net3.findById atom1.id).isSome == true +#guard (net3.findById 999).isNone == true + +-- getRecord +#guard net3.getRecord atom1.id == some "hello" +#guard net3.getRecord atom2.id == some "world" +#guard net3.getRecord 999 == none + +-- canAdd: adding a fresh nerve is ok +def atom3 : AstroNerve simpleIdentify := + { refs := #[], record := "fresh" } +#guard net3.canAdd atom3 == true + +-- canAdd: duplicate id is rejected +def atom1_dup : AstroNerve simpleIdentify := + { refs := #[], record := "hello" } -- same id as atom1 +#guard net3.canAdd atom1_dup == false + +-- canAdd: dangling ref is rejected +def dangling : AstroNerve simpleIdentify := + { refs := #[((), 999)], record := "bad" } +#guard net0.canAdd dangling == false + +-- atoms: only nerves with no refs +#guard net3.atoms.size == 2 + +-- edges: only nerves with refs +#guard net3.edges.size == 1 + +-- refsTo: which nerves reference atom1? +#guard (net3.refsTo atom1.id).size == 1 -- only nerve3 + +-- refsTo: nobody references nerve3 +#guard (net3.refsTo nerve3.id).size == 0 + +-- contains +#guard net3.contains atom1 == true +#guard net3.contains dangling == false + +end NetTest diff --git a/lean/lakefile.toml b/lean/lakefile.toml index 44f307e..d02f2a2 100644 --- a/lean/lakefile.toml +++ b/lean/lakefile.toml @@ -21,3 +21,7 @@ leanOptions = [ [[lean_lib]] name = "Demo" roots = ["Demo"] + +[[lean_lib]] +name = "Test" +roots = ["Test"] diff --git a/scripts/lint.sh b/scripts/lint.sh index 0c6fc60..c2b4399 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -74,7 +74,7 @@ for module_dir in "$LEAN"/ProofAtlas/*/; do # Skip non-code modules if [ "$module_name" = "Util" ]; then continue; fi # linters if [ "$module_name" = "Hypergraph" ]; then continue; fi # spec-only for now - if [ ! -f "$LEAN/Demo/$module_name.lean" ]; then + if [ ! -f "$LEAN/Demo/$module_name.lean" ] && [ ! -d "$LEAN/Demo/$module_name" ]; then MISSING_DEMOS+=("$module_name") fi done From 64715c0d04de4dcc21d7ddd1151751935997c247 Mon Sep 17 00:00:00 2001 From: Xinze-Li-Moqian <70414198+Xinze-Li-Moqian@users.noreply.github.com> Date: Wed, 27 May 2026 18:31:38 -0400 Subject: [PATCH 16/21] feat(astrolabe): depth filtration, cycle detection, standard methods - DepthResult: .depth n / .cycle / .notFound (clear semantics) - depthOf, isAcyclic, maxDepth, cycleCount, cyclicNerves - degree, orphans, foldl, map, BEq, ToString - isEmpty, atomCount, edgeCount, toList, mapRecords, filterNerves - removeById, mergeNerves, isSubnetOf, nerveAt, ofArray - Deep net demo (4 layers) + cyclic net demo - Full test coverage for all methods --- lean/Demo/Astrolabe/Net.lean | 149 +++++++++++++++++++++++++++ lean/ProofAtlas/Astrolabe/Nerve.lean | 5 + lean/ProofAtlas/Astrolabe/Net.lean | 133 +++++++++++++++++++++++- lean/Test/Astrolabe/Net.lean | 122 ++++++++++++++-------- 4 files changed, 367 insertions(+), 42 deletions(-) diff --git a/lean/Demo/Astrolabe/Net.lean b/lean/Demo/Astrolabe/Net.lean index b6b36a0..dec1eab 100644 --- a/lean/Demo/Astrolabe/Net.lean +++ b/lean/Demo/Astrolabe/Net.lean @@ -43,3 +43,152 @@ def atom1_copy : AstroNerve simpleIdentify := def badNet2 := net1.addNerve! atom1_copy -- FAILS: id already exists ``` -/ + +/-! ## Iteration -/ + +-- Print every nerve's id and width. +#eval net3.nerves.toList.map (fun n => s!"id={n.id} width={n.width} atom={n.isAtom}") + +/-! ## Statistics + conversion -/ + +#eval net3.isEmpty -- false +#eval net0.isEmpty -- true +#eval net3.atomCount -- 2 +#eval net3.edgeCount -- 1 +#eval net3.toList.length -- 3 + +-- Map over records. +#eval net3.mapRecords (fun r => r.length) -- length of each record string + +/-! ## Remove + Merge -/ + +-- Remove atom1: returns array, not net (closure may break). +#eval (net3.removeById atom1.id).size -- 2 + +-- Merge two nets: returns raw array. +def net_a := (AstroNet.empty simpleIdentify).addNerve! atom1 +def net_b := (AstroNet.empty simpleIdentify).addNerve! atom2 +#eval (net_a.mergeNerves net_b).size -- 2 + +/-! ## Subnet + lookup + stats -/ + +#eval net1.isSubnetOf net3 -- true (net1 ⊂ net3) +#eval net3.isSubnetOf net1 -- false +#eval net3.nerveAt 0 -- first nerve added +#eval net3.nerveAt 99 -- none +#eval net3.totalRefs -- 2 (only nerve3 has refs) + +/-! ## ToString -/ + +#eval toString atom1 -- "Nerve(id=..., width=0)" +#eval toString nerve3 -- "Nerve(id=..., width=2)" +#eval toString net3 -- "AstroNet(3 nerves)" + +/-! ## Graph analysis -/ + +-- Degree: how many times is atom1 referenced? +#eval net3.degree atom1.id -- 1 (only nerve3 refs it) +#eval net3.degree nerve3.id -- 0 (nobody refs nerve3) + +-- Orphans: nerves nobody references (root nodes). +#eval net3.orphans.size -- 1 (nerve3) + +-- Fold: sum of all widths. +#eval net3.foldl (fun acc n => acc + n.width) 0 -- 2 + +-- Map: all records. +#eval net3.map (fun n => n.record) + +/-! ## Depth + cycle detection -/ + +-- Depth of atoms = 0, depth of nerve3 = 1 (refs are atoms). +#eval net3.depthOf atom1.id -- .depth 0 +#eval net3.depthOf atom2.id -- .depth 0 +#eval net3.depthOf nerve3.id -- .depth 1 + +-- Build a deeper net: atoms → layer1 → layer2 → layer3 +def a1 : AstroNerve simpleIdentify := { refs := #[], record := "a" } +def a2 : AstroNerve simpleIdentify := { refs := #[], record := "b" } +def a3 : AstroNerve simpleIdentify := { refs := #[], record := "c" } + +-- Layer 1: connects two atoms +def l1 : AstroNerve simpleIdentify := + { refs := #[((), a1.id), ((), a2.id)], record := "l1" } + +-- Layer 2: connects layer 1 nerve + an atom +def l2 : AstroNerve simpleIdentify := + { refs := #[((), l1.id), ((), a3.id)], record := "l2" } + +-- Layer 3: connects two layer-2-depth nerves +def l3 : AstroNerve simpleIdentify := + { refs := #[((), l2.id), ((), l1.id)], record := "l3" } + +def dn0 := AstroNet.empty simpleIdentify +def dn1 := dn0.addNerve! a1 +def dn2 := dn1.addNerve! a2 +def dn3 := dn2.addNerve! a3 +def dn4 := dn3.addNerve! l1 +def dn5 := dn4.addNerve! l2 +def deepNet := dn5.addNerve! l3 + +#eval deepNet.size -- 6 +#eval deepNet.depthOf a1.id -- .depth 0 (atom) +#eval deepNet.depthOf l1.id -- .depth 1 (refs are atoms) +#eval deepNet.depthOf l2.id -- .depth 2 (refs include l1) +#eval deepNet.depthOf l3.id -- .depth 3 (refs include l2) +#eval deepNet.maxDepth -- .depth 3 +#eval deepNet.isAcyclic -- true +#eval deepNet.atomCount -- 3 +#eval deepNet.orphans.size -- 1 (only l3 is unreferenced) + +/-! ## Cycle detection -/ + +-- Create a net with a cycle using ofArray (bypasses addNerve! checks). +-- cycA refs cycB, cycB refs cycA → mutual cycle. +def cycA_id := simpleIdentify "cycA" #[((), simpleIdentify "cycB" #[((), 0)])] +def cycB_id := simpleIdentify "cycB" #[((), simpleIdentify "cycA" #[((), 0)])] + +def cycA : AstroNerve simpleIdentify := + { refs := #[((), cycB_id)], record := "cycA" } +def cycB : AstroNerve simpleIdentify := + { refs := #[((), cycA_id)], record := "cycB" } + +-- Build directly — no axiom check. +def cyclicNet := AstroNet.ofArray #[cycA, cycB] + +#eval cyclicNet.isAcyclic -- false (has cycle!) +#eval cyclicNet.cycleCount -- 2 (both nerves in cycle) +#eval cyclicNet.depthOf cycA.id -- .cycle +#eval cyclicNet.maxDepth -- none + +-- Mix: acyclic atoms + cyclic nerves. +def mixedNet := AstroNet.ofArray #[atom1, atom2, cycA, cycB] +#eval mixedNet.isAcyclic -- false +#eval mixedNet.cycleCount -- 2 (cycA + cycB) +#eval mixedNet.depthOf atom1.id -- .depth 0 (atom is fine) +#eval mixedNet.atomCount -- 2 + +/-! ## Cycle analysis -/ + +-- Build a net with two overlapping cycles: +-- A → B → C → A (cycle 1) +-- A → D → A (cycle 2, shares A) +def mkId (s : String) (refs : Array (Unit × UInt64)) : UInt64 := + simpleIdentify s refs + +def nA : AstroNerve simpleIdentify := { refs := #[((), mkId "B" #[((), mkId "C" #[((), 0)])])], record := "A" } +def nB : AstroNerve simpleIdentify := { refs := #[((), mkId "C" #[((), 0)])], record := "B" } +def nC : AstroNerve simpleIdentify := { refs := #[((), nA.id)], record := "C" } +def nD : AstroNerve simpleIdentify := { refs := #[((), nA.id)], record := "D" } + +def cycleNet := AstroNet.ofArray #[nA, nB, nC, nD] + +-- How many nerves are in cycles? +#eval cycleNet.cycleCount -- 4? or 3? depends on reachability + +-- Strongly connected components: groups of mutually reachable nerves. +-- Each group with size > 1 is a cycle group. +-- TODO: #eval cycleNet.sccs + +-- Print each cycle. +-- TODO: #eval cycleNet.printCycles diff --git a/lean/ProofAtlas/Astrolabe/Nerve.lean b/lean/ProofAtlas/Astrolabe/Nerve.lean index 0331383..cc57ea3 100644 --- a/lean/ProofAtlas/Astrolabe/Nerve.lean +++ b/lean/ProofAtlas/Astrolabe/Nerve.lean @@ -80,3 +80,8 @@ def setRef (n : AstroNerve identify) (i : Nat) (ref : Role × Id) : AstroNerve i { refs := n.refs.set! i ref, record := n.record } end AstroNerve + +-- ToString: show nerve as "Nerve(id=..., width=N)". +instance {Id Role Record : Type} {identify : Record → Array (Role × Id) → Id} + [ToString Id] : ToString (AstroNerve identify) where + toString n := s!"Nerve(id={n.id}, width={n.width})" diff --git a/lean/ProofAtlas/Astrolabe/Net.lean b/lean/ProofAtlas/Astrolabe/Net.lean index 844efe5..a12d0de 100644 --- a/lean/ProofAtlas/Astrolabe/Net.lean +++ b/lean/ProofAtlas/Astrolabe/Net.lean @@ -73,8 +73,139 @@ def refsTo (net : AstroNet identify) (i : Id) : Array (AstroNerve identify) := net.nerves.filter (·.containsRef i) -- Does the net contain a nerve with this id? --- (same as hasId, but takes a nerve instead of an id) def contains (net : AstroNet identify) (n : AstroNerve identify) : Bool := net.hasId n.id +-- Is the net empty? +def isEmpty (net : AstroNet identify) : Bool := net.nerves.isEmpty + +-- Number of atoms. +def atomCount (net : AstroNet identify) : Nat := net.atoms.size + +-- Number of non-atoms (edges). +def edgeCount (net : AstroNet identify) : Nat := net.edges.size + +-- Convert to List. +def toList (net : AstroNet identify) : List (AstroNerve identify) := + net.nerves.toList + +-- Map a function over all nerves' records, producing a list of results. +def mapRecords (net : AstroNet identify) (f : Record → α) : Array α := + net.nerves.map (fun n => f n.record) + +-- Filter nerves by a predicate. Returns a plain array (not a net, +-- since filtered result may violate closure). +def filterNerves (net : AstroNet identify) (p : AstroNerve identify → Bool) : + Array (AstroNerve identify) := + net.nerves.filter p + +-- Remove a nerve by id. Returns a plain array (not a net, +-- since removing a nerve may break closure for nerves that referenced it). +def removeById (net : AstroNet identify) (i : Id) : Array (AstroNerve identify) := + net.nerves.filter (fun n => n.id != i) + +-- Merge two nets. Returns a plain array (not a net, +-- since duplicates / broken closure must be resolved by the caller). +def mergeNerves (net1 net2 : AstroNet identify) : Array (AstroNerve identify) := + net1.nerves ++ net2.nerves + +-- Is every nerve in net1 also in net2 (by id)? +def isSubnetOf (net1 net2 : AstroNet identify) : Bool := + net1.nerves.all (fun n => net2.hasId n.id) + +-- Get the i-th nerve by insertion order. +def nerveAt (net : AstroNet identify) (i : Nat) : Option (AstroNerve identify) := + net.nerves[i]? + +-- Total number of refs across all nerves. +def totalRefs (net : AstroNet identify) : Nat := + net.nerves.foldl (fun acc n => acc + n.width) 0 + +-- How many times is this id referenced across all nerves (in-degree). +def degree (net : AstroNet identify) (i : Id) : Nat := + net.nerves.foldl (fun acc n => + acc + n.refs.foldl (fun c ref => if ref.2 == i then c + 1 else c) 0) 0 + +-- Nerves that no other nerve references (root nodes). +def orphans (net : AstroNet identify) : Array (AstroNerve identify) := + net.nerves.filter (fun n => net.degree n.id == 0) + +-- Fold over all nerves from left. +def foldl (net : AstroNet identify) (f : α → AstroNerve identify → α) (init : α) : α := + net.nerves.foldl f init + +-- Map a function over all nerves. +def map (net : AstroNet identify) (f : AstroNerve identify → α) : Array α := + net.nerves.map f + +-- Result of depth computation: finite depth, cycle, or not found. +inductive DepthResult where + | depth (n : Nat) -- finite depth + | cycle -- participates in a cycle (infinite depth) + | notFound -- id not in the net + deriving Repr, BEq, Inhabited + +-- Compute depth of a nerve in the net. +-- atom = .depth 0, others = .depth (1 + max of refs), cycle = .cycle +partial def depthOf (net : AstroNet identify) (i : Id) : DepthResult := + go i #[] +where + go (i : Id) (visited : Array Id) : DepthResult := + if visited.contains i then .cycle + else + match net.findById i with + | none => .notFound + | some n => + if n.isAtom then .depth 0 + else + let visited := visited.push i + let depths := n.refIds.map (fun refId => go refId visited) + if depths.any (· == .cycle) then .cycle + else if depths.any (· == .notFound) then .notFound + else + let maxD := depths.foldl (fun acc d => + match d with | .depth n => max acc n | _ => acc) 0 + .depth (maxD + 1) + +-- Is the net acyclic? (all nerves have finite depth) +def isAcyclic (net : AstroNet identify) : Bool := + net.nerves.all (fun n => match net.depthOf n.id with | .depth _ => true | _ => false) + +-- Maximum depth in the net (none if any nerve has a cycle). +def maxDepth (net : AstroNet identify) : Option Nat := + let depths := net.nerves.map (fun n => net.depthOf n.id) + if depths.any (· == .cycle) then none + else some (depths.foldl (fun acc d => + match d with | .depth n => max acc n | _ => acc) 0) + +-- Build a net directly from an array, without checking axioms. +-- Use this for testing or when you guarantee correctness externally. +def ofArray (nerves : Array (AstroNerve identify)) : AstroNet identify where + nerves := nerves + +-- Count the number of nerves involved in cycles. +def cycleCount (net : AstroNet identify) : Nat := + net.nerves.filter (fun n => net.depthOf n.id == .cycle) |>.size + +-- All nerves that are part of a cycle. +def cyclicNerves (net : AstroNet identify) : Array (AstroNerve identify) := + net.nerves.filter (fun n => net.depthOf n.id == .cycle) + +-- Depth of every nerve: list of (id, depth result) pairs. +def depthMap (net : AstroNet identify) [ToString Id] : Array (String × DepthResult) := + net.nerves.map (fun n => (toString n.id, net.depthOf n.id)) + end AstroNet + +-- BEq: two nets are equal if they have the same nerves. +instance {α β γ : Type} [DecidableEq α] + {f : γ → Array (β × α) → α} + [BEq (AstroNerve f)] : + BEq (AstroNet f) where + beq a b := a.nerves == b.nerves + +-- ToString: show net as "AstroNet(n nerves)". +instance {α β γ : Type} [DecidableEq α] + {f : γ → Array (β × α) → α} : + ToString (AstroNet f) where + toString net := s!"AstroNet({net.size} nerves)" diff --git a/lean/Test/Astrolabe/Net.lean b/lean/Test/Astrolabe/Net.lean index 6248d9c..d5bf026 100644 --- a/lean/Test/Astrolabe/Net.lean +++ b/lean/Test/Astrolabe/Net.lean @@ -7,8 +7,6 @@ import ProofAtlas.Astrolabe.Net /-! # Tests: AstroNet - -Every method has at least one `#guard` assertion. -/ namespace NetTest @@ -17,36 +15,22 @@ private def simpleIdentify (r : String) (refs : Array (Unit × UInt64)) : UInt64 let refsStr := String.join (refs.toList.map (fun p => toString p.2)) String.hash (r ++ refsStr) -def atom1 : AstroNerve simpleIdentify := - { refs := #[], record := "hello" } - -def atom2 : AstroNerve simpleIdentify := - { refs := #[], record := "world" } - +def atom1 : AstroNerve simpleIdentify := { refs := #[], record := "hello" } +def atom2 : AstroNerve simpleIdentify := { refs := #[], record := "world" } def nerve3 : AstroNerve simpleIdentify := - { refs := #[((), atom1.id), ((), atom2.id)] - record := "connects" } + { refs := #[((), atom1.id), ((), atom2.id)], record := "connects" } --- empty def net0 := AstroNet.empty simpleIdentify -#guard net0.size == 0 - --- addNerve! atom1 def net1 := net0.addNerve! atom1 -#guard net1.size == 1 - --- addNerve! atom2 def net2 := net1.addNerve! atom2 -#guard net2.size == 2 - --- addNerve! nerve3 (refs point to atom1, atom2 — both in net2) def net3 := net2.addNerve! nerve3 + +-- size +#guard net0.size == 0 #guard net3.size == 3 -- hasId #guard net3.hasId atom1.id == true -#guard net3.hasId atom2.id == true -#guard net3.hasId nerve3.id == true #guard net3.hasId 999 == false -- ids @@ -58,38 +42,94 @@ def net3 := net2.addNerve! nerve3 -- getRecord #guard net3.getRecord atom1.id == some "hello" -#guard net3.getRecord atom2.id == some "world" #guard net3.getRecord 999 == none --- canAdd: adding a fresh nerve is ok -def atom3 : AstroNerve simpleIdentify := - { refs := #[], record := "fresh" } +-- canAdd +def atom3 : AstroNerve simpleIdentify := { refs := #[], record := "fresh" } +def atom1_dup : AstroNerve simpleIdentify := { refs := #[], record := "hello" } +def dangling : AstroNerve simpleIdentify := { refs := #[((), 999)], record := "bad" } #guard net3.canAdd atom3 == true - --- canAdd: duplicate id is rejected -def atom1_dup : AstroNerve simpleIdentify := - { refs := #[], record := "hello" } -- same id as atom1 #guard net3.canAdd atom1_dup == false - --- canAdd: dangling ref is rejected -def dangling : AstroNerve simpleIdentify := - { refs := #[((), 999)], record := "bad" } #guard net0.canAdd dangling == false --- atoms: only nerves with no refs +-- atoms / edges #guard net3.atoms.size == 2 - --- edges: only nerves with refs #guard net3.edges.size == 1 --- refsTo: which nerves reference atom1? -#guard (net3.refsTo atom1.id).size == 1 -- only nerve3 - --- refsTo: nobody references nerve3 +-- refsTo +#guard (net3.refsTo atom1.id).size == 1 #guard (net3.refsTo nerve3.id).size == 0 -- contains #guard net3.contains atom1 == true #guard net3.contains dangling == false +-- isEmpty +#guard net0.isEmpty == true +#guard net3.isEmpty == false + +-- atomCount / edgeCount +#guard net3.atomCount == 2 +#guard net3.edgeCount == 1 + +-- toList +#guard net3.toList.length == 3 + +-- mapRecords +#guard net3.mapRecords (fun r => r.length) == #[5, 5, 8] + +-- filterNerves +#guard (net3.filterNerves (·.isAtom)).size == 2 + +-- removeById +#guard (net3.removeById atom1.id).size == 2 +#guard (net3.removeById 999).size == 3 + +-- mergeNerves +def net_x := net0.addNerve! atom1 +def net_y := net0.addNerve! atom2 +#guard (net_x.mergeNerves net_y).size == 2 + +-- isSubnetOf +#guard net1.isSubnetOf net3 == true +#guard net3.isSubnetOf net1 == false + +-- nerveAt +#guard (net3.nerveAt 0).isSome == true +#guard (net3.nerveAt 99).isNone == true + +-- totalRefs +#guard net3.totalRefs == 2 + +-- toString +#guard toString net3 == "AstroNet(3 nerves)" + +-- degree +#guard net3.degree atom1.id == 1 +#guard net3.degree nerve3.id == 0 + +-- orphans +#guard net3.orphans.size == 1 + +-- foldl +#guard net3.foldl (fun acc n => acc + n.width) 0 == 2 + +-- map +#guard (net3.map (·.record)).size == 3 + +-- BEq +#guard (net0 == AstroNet.empty simpleIdentify) == true +#guard (net1 == net2) == false + +-- depthOf +#guard net3.depthOf atom1.id == .depth 0 +#guard net3.depthOf nerve3.id == .depth 1 +#guard net3.depthOf 999 == .notFound + +-- isAcyclic +#guard net3.isAcyclic == true + +-- maxDepth +#guard net3.maxDepth == some 1 + end NetTest From 0299bb355861c748d63e29feb1fc90a12dd535ca Mon Sep 17 00:00:00 2001 From: Xinze-Li-Moqian <70414198+Xinze-Li-Moqian@users.noreply.github.com> Date: Wed, 27 May 2026 18:36:31 -0400 Subject: [PATCH 17/21] feat(astrolabe): add SCC, canReach, printCycles - canReach: DFS reachability between two ids - sccs: strongly connected components - sccCount: number of cycle groups - printCycles: all cycle groups - Remove TODOs from demo --- lean/Demo/Astrolabe/Net.lean | 10 +++---- lean/ProofAtlas/Astrolabe/Net.lean | 44 ++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/lean/Demo/Astrolabe/Net.lean b/lean/Demo/Astrolabe/Net.lean index dec1eab..d3666fb 100644 --- a/lean/Demo/Astrolabe/Net.lean +++ b/lean/Demo/Astrolabe/Net.lean @@ -186,9 +186,7 @@ def cycleNet := AstroNet.ofArray #[nA, nB, nC, nD] -- How many nerves are in cycles? #eval cycleNet.cycleCount -- 4? or 3? depends on reachability --- Strongly connected components: groups of mutually reachable nerves. --- Each group with size > 1 is a cycle group. --- TODO: #eval cycleNet.sccs - --- Print each cycle. --- TODO: #eval cycleNet.printCycles +-- Strongly connected components. +#eval cycleNet.sccs -- all components +#eval cycleNet.sccCount -- number of cycle groups (size > 1) +#eval cycleNet.printCycles -- only the cycle groups diff --git a/lean/ProofAtlas/Astrolabe/Net.lean b/lean/ProofAtlas/Astrolabe/Net.lean index a12d0de..c3a6470 100644 --- a/lean/ProofAtlas/Astrolabe/Net.lean +++ b/lean/ProofAtlas/Astrolabe/Net.lean @@ -197,6 +197,50 @@ def depthMap (net : AstroNet identify) [ToString Id] : Array (String × DepthRes end AstroNet +-- SCC support: outside namespace to avoid implicit parameter issues. + +partial def _root_.AstroNet.canReach {α β γ : Type} [DecidableEq α] + {f : γ → Array (β × α) → α} + (net : AstroNet f) (from_ to_ : α) : Bool := + go from_ #[] +where + go (cur : α) (visited : Array α) : Bool := + if cur == to_ && visited.size > 0 then true + else if visited.contains cur then false + else + match net.nerves.find? (fun n => f n.record n.refs == cur) with + | none => false + | some n => + (n.refs.map Prod.snd).any (fun rid => go rid (visited.push cur)) + +def _root_.AstroNet.sccs {α β γ : Type} [DecidableEq α] + {f : γ → Array (β × α) → α} + (net : AstroNet f) : Array (Array α) := Id.run do + let ids := net.nerves.map (fun n => f n.record n.refs) + let mut assigned : Array α := #[] + let mut components : Array (Array α) := #[] + for i in ids.toList do + if assigned.contains i then continue + let mut comp : Array α := #[i] + for j in ids.toList do + if j == i || assigned.contains j then continue + if net.canReach i j && net.canReach j i then + comp := comp.push j + for c in comp.toList do + assigned := assigned.push c + components := components.push comp + return components + +def _root_.AstroNet.sccCount {α β γ : Type} [DecidableEq α] + {f : γ → Array (β × α) → α} + (net : AstroNet f) : Nat := + (net.sccs.filter (·.size > 1)).size + +def _root_.AstroNet.printCycles {α β γ : Type} [DecidableEq α] + {f : γ → Array (β × α) → α} + (net : AstroNet f) : Array (Array α) := + net.sccs.filter (·.size > 1) + -- BEq: two nets are equal if they have the same nerves. instance {α β γ : Type} [DecidableEq α] {f : γ → Array (β × α) → α} From 6cf19078c443666ba74e2b9a68bd6ed703e34013 Mon Sep 17 00:00:00 2001 From: Xinze-Li-Moqian <70414198+Xinze-Li-Moqian@users.noreply.github.com> Date: Wed, 27 May 2026 19:31:58 -0400 Subject: [PATCH 18/21] chore: clean site to match spec-only main Remove all old docs pages (architecture, commands, metrics, pipeline, theory, tutorial) and demo components. Landing page now honestly reflects current status. --- site/app/docs/architecture/page.mdx | 158 ----- site/app/docs/commands/page.mdx | 193 ------ site/app/docs/how-to/add-a-mapping/page.mdx | 174 ----- site/app/docs/how-to/add-a-metric/page.mdx | 186 ----- site/app/docs/layout.tsx | 110 --- .../hypergraph-metric/commute-time/page.mdx | 8 - .../hypergraph-metric/diffusion/page.mdx | 8 - .../hypergraph-metric/jensen-shannon/page.mdx | 8 - .../docs/metrics/hypergraph-metric/page.mdx | 137 ---- .../hypergraph-metric/resistance/page.mdx | 222 ------ site/app/docs/metrics/page.mdx | 49 -- site/app/docs/page.mdx | 79 --- site/app/docs/pipeline/page.mdx | 15 - site/app/docs/structure/page.mdx | 183 ----- site/app/docs/theory/commute-time/page.mdx | 107 --- site/app/docs/theory/hyperbolicity/page.mdx | 126 ---- site/app/docs/theory/page.mdx | 67 -- site/app/docs/theory/quality/page.mdx | 102 --- site/app/docs/theory/resistance/page.mdx | 113 --- site/app/docs/theory/structure/page.mdx | 106 --- site/app/docs/tutorial/page.mdx | 159 ----- site/app/page.tsx | 105 +-- site/components/AtlasDemo.tsx | 651 ------------------ site/components/LeanSource.tsx | 128 ---- site/components/Mermaid.tsx | 49 -- 25 files changed, 29 insertions(+), 3214 deletions(-) delete mode 100644 site/app/docs/architecture/page.mdx delete mode 100644 site/app/docs/commands/page.mdx delete mode 100644 site/app/docs/how-to/add-a-mapping/page.mdx delete mode 100644 site/app/docs/how-to/add-a-metric/page.mdx delete mode 100644 site/app/docs/layout.tsx delete mode 100644 site/app/docs/metrics/hypergraph-metric/commute-time/page.mdx delete mode 100644 site/app/docs/metrics/hypergraph-metric/diffusion/page.mdx delete mode 100644 site/app/docs/metrics/hypergraph-metric/jensen-shannon/page.mdx delete mode 100644 site/app/docs/metrics/hypergraph-metric/page.mdx delete mode 100644 site/app/docs/metrics/hypergraph-metric/resistance/page.mdx delete mode 100644 site/app/docs/metrics/page.mdx delete mode 100644 site/app/docs/page.mdx delete mode 100644 site/app/docs/pipeline/page.mdx delete mode 100644 site/app/docs/structure/page.mdx delete mode 100644 site/app/docs/theory/commute-time/page.mdx delete mode 100644 site/app/docs/theory/hyperbolicity/page.mdx delete mode 100644 site/app/docs/theory/page.mdx delete mode 100644 site/app/docs/theory/quality/page.mdx delete mode 100644 site/app/docs/theory/resistance/page.mdx delete mode 100644 site/app/docs/theory/structure/page.mdx delete mode 100644 site/app/docs/tutorial/page.mdx delete mode 100644 site/components/AtlasDemo.tsx delete mode 100644 site/components/LeanSource.tsx delete mode 100644 site/components/Mermaid.tsx diff --git a/site/app/docs/architecture/page.mdx b/site/app/docs/architecture/page.mdx deleted file mode 100644 index 4a42188..0000000 --- a/site/app/docs/architecture/page.mdx +++ /dev/null @@ -1,158 +0,0 @@ -{/* Mirror of /docs/architecture.md in the repo. Edit there. */} - -# Architecture - -ProofAtlas turns a Lean 4 declaration into a *hypergraph* and then -measures the geometry of that hypergraph. The library is organised -in four layers; each layer has a single responsibility and exposes a -clean interface to the layers above and below. - -``` - ┌──────────────────────────────────────────────────────────────┐ - │ Layer 4 — Quality │ - │ scripts/check_bdf_metric_instances.sh · IsHypergraphMetric │ - │ CI gate, sorry audit, axiom whitelist │ - └──────────────────────────────────────────────────────────────┘ - ▲ - ┌──────────────────────────┴───────────────────────────────────┐ - │ Layer 3 — Compute (Float, #eval-able) │ - │ Pipeline/IncidenceData Pipeline/Linalg │ - │ Pipeline/Hyperbolicity Pipeline/Cache │ - │ Mapping/{Expr, Environment, Declaration} │ - │ Command/* (#atlas.*) Export/* (lake exe atlas-export) │ - └──────────────────────────┬───────────────────────────────────┘ - ▲ - ┌──────────────────────────┴───────────────────────────────────┐ - │ Layer 2 — Metric (proof side, ℝ-valued) │ - │ Metric/Axioms.lean: IsHypergraphMetric spec │ - │ Metric/{Resistance, CommuteTime, JensenShannon, │ - │ Diffusion}/{Basic, Instance}.lean │ - │ RandomWalk/{KemenySnell, CommuteTime, Spectral/*} │ - │ Hyperbolicity/Basic.lean: generic δ, Hausdorff, GH theorems │ - └──────────────────────────┬───────────────────────────────────┘ - ▲ - ┌──────────────────────────┴───────────────────────────────────┐ - │ Layer 1 — Proof (combinatorial) │ - │ Hypergraph/Basic.lean: Hypergraph V C │ - │ Mapping/Expr.lean: Lean.Expr → Hypergraph │ - └──────────────────────────────────────────────────────────────┘ -``` - -## Layer 1 — Proof - -`Hypergraph/Basic.lean` defines `Hypergraph V C`: a directed, -ordered, coloured, acyclic, connected hypergraph. The four -structural axioms (D, O, C, A) are encoded as fields of the -structure, so they hold by construction. The translation from any -Lean proof term lives in `Mapping/Expr.lean`: - -```lean -def Lean.Expr.toHypergraph (e : Expr) : - Hypergraph (Fin (countSubterms e)) ExprColor -``` - -The load-bearing lemma is `connected`: every non-root DFS vertex is -incident to its parent compound subterm. With that sealed, every -Lean declaration in any library ships with a `Hypergraph` for free. - -## Layer 2 — Metric - -`Metric/Axioms.lean` defines the contract a candidate distance must -satisfy to count as a "hypergraph metric": - -```lean -structure IsHypergraphMetric - (d : Hypergraph V C → WalkParams C → V → V → ℝ) where - d_self, d_symm, d_triangle, d_nonneg -- metric axioms - sensitive_to_direction, _to_ordering, _to_coloring -- existence witnesses -``` - -Four concrete metrics are registered against this spec: - -| metric | file | status | -| --- | --- | --- | -| `resistanceDist` | `Metric/Resistance/Instance.lean` | certified | -| `commuteTimeDist`| `Metric/CommuteTime/Instance.lean` | certified | -| `jsDist` | `Metric/JensenShannon/Instance.lean`| in progress (sorry) | -| `diffusionDist` | `Metric/Diffusion/Instance.lean` | in progress (sorry) | - -All large-scale geometry (`Hyperbolicity/Basic.lean` for δ-hyperbolicity, -the Hausdorff and Gromov–Hausdorff constructions) is parameterised by -`{d} (isMetric : IsHypergraphMetric d)`. Swap a metric and every -downstream theorem follows. - -## Layer 3 — Compute - -The proof layer is `noncomputable` (it uses abstract `ℝ` and carries -proofs). For end-to-end pipelines we need an `#eval`-able -representation: `Pipeline/IncidenceData.lean` gives the same -combinatorial object as arrays of `Nat` vertex indices and `Float` -weights. Everything in `Pipeline/` is total, computable, and free -of proof obligations. - -Two extraction modes feed `IncidenceData`: - -- **Expr-level** (`Mapping/Environment.lean`). - `Exprs.toIncidenceData : Array Expr → IncidenceData × Array Nat` - glues proof terms via const-sharing — every reference to the same - `.const Name` collapses to one shared vertex. -- **Decl-level** (`Mapping/Declaration.lean`). - `Environment.toDeclIncidence : Environment → Array Name → IncidenceData` - builds the co-reference graph of a namespace: vertices are - declarations, hyperedges are maximal application spines inside each - declaration's proof body. - -From `IncidenceData`, `Pipeline/Linalg.lean` computes the Laplacian -and the effective-resistance matrix, and `Pipeline/Hyperbolicity.lean` -computes the four-point δ. `Pipeline/Cache.lean` memoises both per -`(env, namespace)` so repeated `.ns` commands don't re-pay the work. - -The interactive surface is `Command/*`: each `#atlas.<verb>` resolves -identifiers, calls into `Pipeline/`, and either logs a number or -renders a widget. The same compute machinery is wrapped by the -`atlas-export` executable in `Export/` for shell pipelines and CI. - -## Layer 4 — Quality - -`scripts/check_bdf_metric_instances.sh` runs in CI on every push. -For each registered metric in the registry, it walks -`#print axioms` of the corresponding `*_isHypergraphMetric` -instance theorem and refuses to merge if any `sorry` is reachable. - -The `Util/Audit.lean` linter (the `MathTag` linter) forces every -public declaration's docstring to begin with one of `**Math.**`, -`**Eng.**`, or `**Mixed.**` so it's always obvious which lines -correspond to paper content and which are type-theoretic plumbing. - -## Where to touch - -| If you want to | Touch | Recipe | -| --- | --- | --- | -| Add a new mapping | `Mapping/MyMapping.lean` + `Command/MyCmd.lean` | [add-a-mapping](/docs/how-to/add-a-mapping) | -| Add a new metric | `Metric/MyMetric/{Basic,Instance}.lean` + script registry | [add-a-metric](/docs/how-to/add-a-metric) | -| Add a new command | `Command/MyCmd.lean` using `Command/Common.lean` helpers | follow `Command/Resistance.lean` as a template | -| Add a downstream consumer | `Export/MyFormat.lean` + `lakefile.toml` target | follow `Export/Profile.lean` as a template | -| Fill an open `sorry` | the relevant `Metric/*/Basic.lean` | (how-to coming in Wave 2) | - -## Two things this architecture deliberately does - -**Separates spec from instance.** `IsHypergraphMetric` is the -contract; the four metrics are instances of it. Any new theorem in -Layer 2 or 3 takes an `IsHypergraphMetric d` hypothesis, not a -specific distance. New metrics drop in without breaking downstream -results. - -**Separates the proof layer from the compute layer.** Layer 1 + 2 -prove things about abstract `ℝ`-valued metrics; Layer 3 evaluates -their `Float` shadows. A future bridging lemma will pin the -correspondence formally; for now they're kept on speaking terms by -convention. The CI gate on `IsHypergraphMetric` instances ensures -the proof side stays honest; the `#eval`-able compute side -ensures the demo loop is fast. - -## Pointers - -- For a tour of the `#atlas.*` commands: [tutorial](/docs/tutorial) -- For the command reference: [commands](/docs/commands) -- For the theory side: [theory](/docs/theory) -- For the canonical source: `docs/architecture.md` in [the repo](https://github.com/MathNetwork/ProofAtlas) diff --git a/site/app/docs/commands/page.mdx b/site/app/docs/commands/page.mdx deleted file mode 100644 index cd53b25..0000000 --- a/site/app/docs/commands/page.mdx +++ /dev/null @@ -1,193 +0,0 @@ -# Command Reference - -The `#atlas.*` commands run end-to-end inside Lean: they resolve an -identifier, walk its proof term into a hypergraph, run the -chosen computation, and render the result in the VS Code infoview. - -Multi-identifier commands glue the proof terms via const-sharing -before computing; the per-input root vertices end up in -`HypergraphReport.roots`. - -## #atlas.graph - -Draw the hypergraph of one or more declarations. - -```lean -#atlas.graph Nat.add_zero -#atlas.graph Nat.add_zero Nat.add_comm -- shared constants merged -``` - -**Output**: Interactive force-directed graph in VS Code infoview. -Vertices = sub-expressions. Hyperedges = constructor applications. -Colors: app (red), lam (green), forallE (orange), letE (purple), -mdata (gray), proj (brown). - -**What this measures.** Renders the hypergraph of one or more -declarations. Vertices are sub-expressions. Hyperedges are -constructor applications (`app`, `lam`, `forallE`, `letE`, ...), -colored by type. - -→ Theory: [Structure layer](/docs/theory/structure) — what a -hypergraph is, why these four properties capture CIC. - -## #atlas.resistance - -Compute the resistance distance between two declarations. - -```lean -#atlas.resistance Nat.add_zero Nat.add_comm --- R(Nat.add_zero, Nat.add_comm) = 1.179960 -``` - -The two proof terms are glued into one hypergraph via const-sharing; -the result is the effective resistance between their root vertices. - -**What this measures.** How well-connected two declarations are -through their shared constant network. Two proof terms are glued -into a single graph via const-sharing; the resistance distance -between their root vertices reflects how much shared infrastructure -links them. Close = tightly connected through common constants. -Far = few shared paths. - -→ Theory: [Resistance distance](/docs/theory/resistance) — -spectral formula, conductance schedule, Rayleigh monotonicity. - -## #atlas.hausdorff.resistance - -Hausdorff distance between two groups of declarations under the -resistance metric. The `|` token separates the two groups. - -```lean -#atlas.hausdorff.resistance Nat.add_zero | Nat.add_comm Nat.succ_eq_add_one --- Hausdorff(resistance, {Nat.add_zero}, {Nat.add_comm, Nat.succ_eq_add_one}) = 1.752143 -``` - -Every declaration on either side is glued into one hypergraph via -const-sharing; the resistance matrix is computed once on the -combined graph; the Hausdorff distance is taken over the *union of -subtree vertex sets* per group (not the singleton root vertices). - -**What this measures.** How structurally similar two groups of -proof terms are. For each sub-expression in group A, finds the -closest sub-expression in group B (by resistance distance), then -takes the worst case. A small Hausdorff distance means every part -of one proof has a structural counterpart in the other. - -→ Theory: [Resistance distance](/docs/theory/resistance) — -Hausdorff lifts any base metric to sets, inheriting symmetry and -triangle inequality from Mathlib. - -## #atlas.delta - -Compute the Gromov δ-hyperbolicity constant. - -```lean -#atlas.delta Nat.add_zero Nat.add_comm Nat.succ_eq_add_one --- δ = 0.238369, diam = 3.213827, δ/diam = 0.074170 -``` - -**What this measures.** How tree-like the proof structure is at -large scale. A small δ/diam ratio means the shared constant -network has a hierarchical, tree-like backbone. A large ratio means -it has significant non-tree shortcuts. - -→ Theory: [δ-hyperbolicity](/docs/theory/hyperbolicity) — -four-point condition, thin triangles, δ/diam ratio. - -## #atlas.profile - -Comprehensive report: graph + vertices/edges + resistance matrix + δ. - -```lean -#atlas.profile Nat.add_zero -``` - -**What this measures.** Full geometric report: vertex/edge counts, -color distribution, resistance diameter, mean resistance, -δ-hyperbolicity, δ/diam ratio. - -→ Theory: [Overview](/docs/theory) — the three-layer pipeline -from structure to geometry. - -## #atlas.graph.ns - -Decl-level companion to `#atlas.graph`: build the co-reference graph -of an entire namespace. Vertices = declarations, hyperedges = maximal -application spines inside each declaration's proof body. - -```lean -#atlas.graph.ns ProofAtlas.Pipeline --- Decl-level hypergraph (ProofAtlas.Pipeline): 240 vertices, 1100 hyperedges -``` - -**What this measures.** The shape of a *library*, not a single -proof: which declarations cite which, and how dense the co-reference -network is. Vertex colour by declaration kind (theorem / def / -instance / axiom / opaque). - -The first `.ns` call on a namespace pays a one-time cost (~2 s for -~200 decls) to build the decl-graph + resistance matrix; subsequent -`.resistance.ns` and `.delta.ns` on the same namespace are cache -hits. - -## #atlas.resistance.ns - -Effective resistance between two declarations on the decl-level -graph of a namespace. - -```lean -#atlas.resistance.ns ProofAtlas.Pipeline geometricProfile geometricProfileOfExprs --- R(...geometricProfile, ...geometricProfileOfExprs) = 0.331177 --- [same component (#2/24)] -``` - -**What this measures.** How close two declarations are in the -co-reference structure of their namespace. Short names auto-qualify -against the namespace argument. The output also reports whether the -two ends sit in the same weakly-connected component — a different- -component result is `ε⁻¹`-scale noise (the regularised -pseudoinverse). - -→ Theory: same as `#atlas.resistance`; the metric is the same, -the graph it's computed on is different (decls vs subterms). - -## #atlas.delta.ns - -Gromov δ-hyperbolicity of the decl-level graph of a namespace, -restricted to the largest connected component (the full graph is -typically disconnected). - -```lean -#atlas.delta.ns ProofAtlas.Pipeline --- δ = 0.090615 --- diam = 3.839542 --- δ/diam = 0.023600 --- [24 components — δ/diam restricted to largest (215 verts)] -``` - -**What this measures.** Large-scale tree-likeness of the namespace's -co-reference network. A small δ/diam (under ~0.1) means the library's -decl graph is hierarchical; larger ratios mean it has significant -non-tree shortcuts. - -First call on a namespace is the C(n,4) δ enumeration (tens of -seconds for ~200 verts); subsequent calls are cache hits. - -→ Theory: [δ-hyperbolicity](/docs/theory/hyperbolicity). - -## #atlas.status - -Check whether a metric has a verified `IsHypergraphMetric` instance. - -```lean -#atlas.status resistanceDist -- ✓ certified -#atlas.status commuteTimeDist -- ✓ certified -#atlas.status jsDist -- ⚠ in progress -``` - -**What this measures.** Whether a metric's `IsHypergraphMetric` instance -is sorry-free. Reports ✓ (certified), ⚠ (in progress, transitively -uses `sorry`), or ✗ (not registered). - -→ Theory: [Quality (IsHypergraphMetric)](/docs/theory/quality) — the -seven-check certificate every registered metric must satisfy. diff --git a/site/app/docs/how-to/add-a-mapping/page.mdx b/site/app/docs/how-to/add-a-mapping/page.mdx deleted file mode 100644 index f9df56e..0000000 --- a/site/app/docs/how-to/add-a-mapping/page.mdx +++ /dev/null @@ -1,174 +0,0 @@ -{/* Mirror of /docs/how-to/add-a-mapping.md in the repo. Edit there. */} - -# How to add a new mapping - -A *mapping* is an extractor that turns some Lean object into an -`IncidenceData` (and, optionally, a proof-layer `Hypergraph`). The -two mappings currently in tree are: - -| Mapping | Input | Vertex set | Hyperedge | -| --- | --- | --- | --- | -| `Expr.toIncidence` | `Lean.Expr` | Subterm occurrences | one per compound constructor (`app`, `lam`, ...) | -| `Environment.toDeclIncidence` | `Environment + Array Name` | Declarations | one per maximal application spine in a proof body | - -This guide walks through `Environment.toDeclIncidence` as the worked -example because it is the most recent and exhibits all the moving -parts: declaration filtering, vertex labelling, allowed-set -filtering, deduplication, command wiring, and cache integration. - -## 1. Pick the input and decide the vertex set - -A mapping is defined by three choices: - -- **Input type.** What Lean object are you extracting from? - `Environment`, `Array Name`, `Module`, a single `ConstantInfo`? -- **Vertex set.** What does each vertex represent? Subterms, decls, - modules, namespaces? -- **Hyperedge structure.** What does each hyperedge represent? An - inference step, a co-reference, an import dependency? - -For the decl-level mapping these are: `(Environment, Array Name)`, -declarations as vertices, and "one hyperedge per maximal `Expr.app` -spine inside a proof body" as the edge rule (filtered to the -namespace, deduped, self-references removed). - -The hyperedge rule is the load-bearing choice. Two practical -constraints from `IncidenceData`: - -- `inputs` are `Array Nat`; vertex IDs must be allocated upfront in - a deterministic order. The decl mapping uses - `0 .. declNames.size - 1` in the order returned by - `declsInNamespace`. -- `output` is one `Nat`. If the mapping is "many inputs → many - outputs", decompose into one edge per output. - -## 2. Write the extractor in `Pipeline/` - -Drop a new file `lean/ProofAtlas/Mapping/MyMapping.lean`. The file -should export a single primary function with signature -`Source → IncidenceData` (or `Source → IncidenceData × Array Nat` -if you want to return root vertex IDs). - -For decl-level: - -```lean -def Environment.toDeclIncidence - (env : Environment) (declNames : Array Name) : IncidenceData := Id.run do - let mut declToVid : Std.HashMap Name Nat := {} - let mut labels : Array String := #[] - let mut colors : Array String := #[] - for h : i in [:declNames.size] do - let n := declNames[i] - declToVid := declToVid.insert n i - labels := labels.push (toString n) - -- ... per-vertex colour by declaration kind - let allowed : Std.HashSet Name := - declNames.foldl (fun s n => s.insert n) {} - let mut allEdges : Array EdgeRec := #[] - for h : i in [:declNames.size] do - let n := declNames[i] - match env.find? n with - | some info => - match info.value? (allowOpaque := true) with - | some body => - let es := collectDeclEdges allowed declToVid n body - allEdges := allEdges ++ es - | none => pure () - | none => pure () - return { numVertices := declNames.size - edges := allEdges - vertexLabel := labels - vertexColor := colors } -``` - -Three patterns to copy: - -- **Vertex ID is the index in the input array.** Deterministic, - cheap, gives the caller a stable handle. -- **Per-vertex labels and colours go in `vertexLabel` and - `vertexColor`.** These flow straight to the widget; empty arrays - fall back to defaults. -- **Filter inputs to the `allowed` set in the edge builder.** Drop - any reference outside your vertex set; otherwise edges point to - ghost vertices and the resistance matrix gets confusing rows. - -## 3. (Optional) Proof-layer mirror - -If you want quality guarantees on top of the numerics, define a -proof-layer companion that returns a real `Hypergraph V C` and -discharges the structural axioms. - -For v1 of a new mapping this is usually deferred. Ship the -`IncidenceData` version first; add the proof-layer mirror once the -numerical results show the mapping is worth proving theorems about. - -`Mapping/Expr.lean` is the example to copy. - -## 4. Wire a command in `Command/` - -Drop `lean/ProofAtlas/Command/MyCmd.lean`. Boilerplate is in -`Command/Common.lean`. - -```lean -syntax (name := atlasMyCmd) "#atlas.my" ppSpace colGt ident : command - -@[command_elab atlasMyCmd] -def elabAtlasMyCmd : CommandElab := fun stx => do - let arg : Name := stx[1].getId - let env ← getEnv - let data := Pipeline.Environment.toMyMapping env arg - if data.numVertices = 0 then - throwError "#atlas.my: no vertices for {arg}" - logInfo m!"My-mapping ({arg}): {data.numVertices} vertices, {data.numEdges} edges" - let report : Pipeline.HypergraphReport := - { incidence := data, resistance := #[], delta := 0.0, roots := #[] } - renderGraphWithLegend stx report -``` - -Conventions: - -- The command name mirrors the mapping: `Foo.toX` ↔ `#atlas.x`. -- A namespace-scoped mapping gets a `.ns` suffix on the command. -- Errors throw with the command name as prefix (`#atlas.my: ...`) so - users can grep for them. - -## 5. Wire into the facade and the cache - -Two cheap edits: - -- `lean/ProofAtlas/Command.lean`: add the import and a line in the - docstring's quick-reference table. -- `lean/ProofAtlas/Pipeline/Cache.lean` (only if the mapping is - expensive enough to warrant caching, like decl-level): add a - `getOrBuildMyMapping` mirroring `getOrBuildDeclProfile`. - -## 6. The minimum recipe (cheat sheet) - -1. `lean/ProofAtlas/Mapping/MyMapping.lean` — extractor function - returning `IncidenceData`. -2. `lean/ProofAtlas/Command/MyCmd.lean` — `#atlas.my <args>` wrapper. -3. `lean/ProofAtlas/Command.lean` — add the import. -4. `lean/ProofAtlas/Demos/Pipeline.lean` — at least one example - invocation. -5. **Optional**: `Pipeline/Cache.lean` for memoisation, a proof-layer - `Hypergraph` mirror for quality guarantees, a site doc page. -6. **PR**: use the *New mapping proposal* issue template to discuss - before implementing. - -## What reviewers will check - -- The mapping is *total* (no `Option`, no exceptions on - well-formed input). -- Vertex IDs are deterministic across runs. -- The resulting hyperedges satisfy `inputs.size ≥ 1` and - `output ∉ inputs.toList`. -- The command has a single demo invocation in `Demos/Pipeline.lean`. -- The MathTag linter is satisfied (every public def has an - `**Eng.**` / `**Math.**` / `**Mixed.**` tagged docstring). - -## Pointers - -- Existing mappings to copy from: `Mapping/Environment.lean` - (expr-level), `Mapping/Declaration.lean` (decl-level), both in - [the repo](https://github.com/MathNetwork/ProofAtlas/tree/main/lean/ProofAtlas/Mapping). -- For the metric side, see [add-a-metric](/docs/how-to/add-a-metric). diff --git a/site/app/docs/how-to/add-a-metric/page.mdx b/site/app/docs/how-to/add-a-metric/page.mdx deleted file mode 100644 index 8f14d22..0000000 --- a/site/app/docs/how-to/add-a-metric/page.mdx +++ /dev/null @@ -1,186 +0,0 @@ -{/* Mirror of /docs/how-to/add-a-metric.md in the repo. Edit there. */} - -# How to add a new metric - -A *metric* is a function of type - -```lean -Hypergraph V C → WalkParams C → V → V → ℝ -``` - -that satisfies `IsHypergraphMetric`: four metric-space axioms -(identity, symmetry, triangle, non-negativity) and three sensitivity -existence witnesses (direction, ordering, colouring). Four metrics -are currently registered: `resistanceDist`, `commuteTimeDist`, -`jsDist`, and `diffusionDist`. - -This guide walks through `commuteTimeDist` as the worked example -because it is the second-most-mature metric in the tree and -exhibits every part of the recipe. Once you can follow this -example, the open work on `diffusionDist` becomes a productive -exercise. - -## 1. Pick a definition with mathematical content - -A new metric is worth adding if it answers a question the existing -four don't. Three questions to write down before you start coding: - -- **What does this metric measure?** A geometric quantity, an - information-theoretic one, a spectral one? -- **What is the closed-form or characterisation?** A formula, an - optimisation problem, a fixed-point equation? -- **Why is it a metric?** Are the four axioms easy - (resistance distance: spectral formula gives them in 3 lines) or - hard (commute time: routes through the Markov / Doyle–Snell - identity)? - -`commuteTimeDist` answers "how long does the random walk take to -make a round trip between u and v" with the closed form -$d_{ct}(u, v) = h(u, v) + h(v, u)$ where $h$ is the expected first -passage time of the harmonic random walk on the hypergraph. - -## 2. Drop a `Basic.lean` defining the metric - -Create `lean/ProofAtlas/Metric/<MyMetric>/Basic.lean`. Two -deliverables: - -- The `noncomputable def myDist` itself, with type - `Hypergraph V C → WalkParams C → V → V → ℝ`. -- The four metric-axiom lemmas: `myDist_self`, `myDist_symm`, - `myDist_triangle`, `myDist_nonneg`. Mathlib namespacing - convention: each lives in `namespace Hypergraph`. - -For `commuteTimeDist` the deliverable is split across -`Metric/CommuteTime/{HittingTime, KilledChain, CommuteTime}.lean` -because the proofs lean on a sub-development of hitting-time -analysis. For simpler metrics one `Basic.lean` is enough. - -If a lemma needs a hypothesis the user must supply (e.g., -connectedness of the random walk, or full-rank conductance), declare -that hypothesis explicitly. **Don't bake it into a global `axiom`.** -The pattern is to require the hypothesis on each metric-axiom lemma -and re-export it as a top-level hypothesis in the `Instance.lean` -theorem (see step 3). - -## 3. Drop an `Instance.lean` proving `IsHypergraphMetric` - -Create `lean/ProofAtlas/Metric/<MyMetric>/Instance.lean`. The whole -file is one theorem: - -```lean -theorem myDist_isHypergraphMetric - {V C : Type*} [Fintype V] [DecidableEq V] - [hFin : ∀ H : Hypergraph V C, Fintype H.edges] - -- (a) preconditions that propagate from the metric-axiom lemmas - (hPre : ∀ (H : Hypergraph V C) (params : WalkParams C), ...) - -- (b) three existence witnesses - (hWit_direction : ∃ (H H' : Hypergraph V C) (params : WalkParams C) (u v : V), - H.myDist params u v ≠ H'.myDist params u v) - (hWit_ordering : ... ) - (hWit_coloring : ... ) : - IsHypergraphMetric (fun (H : Hypergraph V C) params u v => - H.myDist params u v) := by - refine ⟨?_, ?_, ?_, ?_, hWit_direction, hWit_ordering, hWit_coloring⟩ - · intro H params u -- d_self - exact H.myDist_self params u - · intro H params u v -- d_symm - exact H.myDist_symm params u v - · intro H params u v w -- d_triangle - exact H.myDist_triangle (hPre H params) u v w - · intro H params u v -- d_nonneg - exact H.myDist_nonneg params u v -``` - -The pattern is: - -- **Metric-space axioms close by re-using the lemmas from - `Basic.lean`.** Their preconditions become hypotheses of the - instance theorem. For `commuteTimeDist` this is `hErgodic`, a - five-part conjunction bundling row-stochasticity, non-negativity, - normalised stationary, strict positivity, and convergence to the - stationary projection. -- **Sensitivity witnesses are existence statements.** They're - passed in as hypotheses for now. The fully-discharged versions - are a follow-up — see `Metric/Resistance/Instance.lean` for the - current treatment. - -The `commuteTimeDist_isHypergraphMetric` declaration is the -cleanest template; copy its shape. - -## 4. Register in the CI script - -Edit `lean/scripts/check_bdf_metric_instances.sh`: - -```bash -METRICS=( - "resistanceDist:ProofAtlas/Metric/Resistance/Instance.lean" - "commuteTimeDist:ProofAtlas/Metric/CommuteTime/Instance.lean" - "jsDist:ProofAtlas/Metric/JensenShannon/Instance.lean" - "diffusionDist:ProofAtlas/Metric/Diffusion/Instance.lean" - "myDist:ProofAtlas/Metric/MyMetric/Instance.lean" -) -``` - -The CI gate refuses to merge if your instance file contains any -`sorry`. If your first version is incomplete, leave the instance -theorem `sorry`'d and the script will tag it `in progress` -(existing examples: `jsDist`, `diffusionDist`). - -## 5. (Optional) Compute-layer Float implementation - -If you want the metric to be `#eval`-able from a `#atlas.<verb>` -command, mirror the definition in `Pipeline/MyMetric.lean` returning -`Float` numbers. For `commuteTimeDist` this is currently TODO — the -hitting-time matrix inversion is `noncomputable` on the proof side -and hasn't been ported to the `Float` pipeline. - -## 6. The minimum recipe (cheat sheet) - -1. `Metric/MyMetric/Basic.lean` — definition + four axiom lemmas. -2. `Metric/MyMetric/Instance.lean` — `myDist_isHypergraphMetric` - theorem assembling the four axioms and routing through three - existence witnesses. -3. `scripts/check_bdf_metric_instances.sh` — add `myDist` to the - `METRICS=()` registry. -4. **Optional**: `Pipeline/MyMetric.lean` for the Float compute - path; `Command/MyCmd.lean` for an `#atlas.my` command. -5. **PR**: use the *New metric proposal* issue template to discuss - the mathematical content and the witness shapes before - implementing. - -## What reviewers will check - -- The instance file is `sorry`-free if you advertise the metric as - certified. CI enforces this. -- `#print axioms myDist_isHypergraphMetric` reports only the - whitelisted axioms (`propext`, `Classical.choice`, `Quot.sound`). -- The Mathlib-style docstring tags are present. -- The key identity (Doyle–Snell or equivalent) is documented with a - paper reference if you cite one. - -## Now try: fill an open `sorry` - -Two metrics in tree are currently `in progress`: - -- `jsDist` — Jensen–Shannon divergence between stationary - neighbourhoods. -- `diffusionDist` — heat-kernel distance at finite time `t`. - -Both follow the exact shape of `commuteTimeDist_isHypergraphMetric`: -`d_self`, `d_symm`, `d_nonneg` are direct; `d_triangle` is the -genuinely hard piece. For `diffusionDist`, the triangle inequality -reduces to Cauchy–Schwarz on the heat-kernel row vectors at time -$t$, so the discharge is a Mathlib -`inner_mul_le_norm_mul_norm` manipulation plus the heat-kernel -positivity already proved in `Metric/Diffusion/Basic.lean`. - -If you want a first PR, `diffusionDist`'s `d_triangle` discharge is -the shortest path to a green CI tick on a previously-yellow row. - -## Pointers - -- Existing instances to copy: `Metric/Resistance/Instance.lean` (the - cleanest template), `Metric/CommuteTime/Instance.lean` (the one - this how-to walks through). Both in - [the repo](https://github.com/MathNetwork/ProofAtlas/tree/main/lean/ProofAtlas/Metric). -- For the mapping side, see [add-a-mapping](/docs/how-to/add-a-mapping). diff --git a/site/app/docs/layout.tsx b/site/app/docs/layout.tsx deleted file mode 100644 index 902cc1d..0000000 --- a/site/app/docs/layout.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import Link from "next/link"; -import type { ReactNode } from "react"; -import { TOC } from "@/components/TOC"; - -type NavSection = { - heading: string; - items: { href: string; label: string }[]; -}; - -const NAV: NavSection[] = [ - { - heading: "Overview", - items: [{ href: "/docs", label: "Project layout" }], - }, - { - heading: "Get started", - items: [ - { href: "/docs/tutorial", label: "Tutorial" }, - { href: "/docs/architecture", label: "Architecture" }, - { href: "/docs/how-to/add-a-mapping", label: "How-to: add a mapping" }, - { href: "/docs/how-to/add-a-metric", label: "How-to: add a metric" }, - ], - }, - { - heading: "Step 1 · Structure", - items: [{ href: "/docs/structure", label: "Hypergraph" }], - }, - { - heading: "Step 2 · Metrics", - items: [ - { href: "/docs/metrics", label: "Overview" }, - { href: "/docs/metrics/hypergraph-metric", label: "Hypergraph metric (spec)" }, - { - href: "/docs/metrics/hypergraph-metric/resistance", - label: "— Resistance distance", - }, - { - href: "/docs/metrics/hypergraph-metric/commute-time", - label: "— Commute time distance", - }, - { - href: "/docs/metrics/hypergraph-metric/jensen-shannon", - label: "— Jensen–Shannon distance", - }, - { - href: "/docs/metrics/hypergraph-metric/diffusion", - label: "— Diffusion distance", - }, - ], - }, - { - heading: "Step 3 · Theory", - items: [ - { href: "/docs/theory", label: "Overview" }, - { href: "/docs/theory/structure", label: "— Structure" }, - { href: "/docs/theory/resistance", label: "— Resistance" }, - { href: "/docs/theory/commute-time", label: "— Commute time" }, - { href: "/docs/theory/hyperbolicity", label: "— δ-hyperbolicity" }, - { href: "/docs/theory/quality", label: "— Quality (IsHypergraphMetric)" }, - ], - }, - { - heading: "Step 4 · Pipeline", - items: [{ href: "/docs/pipeline", label: "End-to-end profile" }], - }, - { - heading: "Commands", - items: [{ href: "/docs/commands", label: "#atlas.* reference" }], - }, -]; - -export default function DocsLayout({ children }: { children: ReactNode }) { - return ( - <div className="mx-auto flex w-full max-w-7xl gap-8 px-6 py-10"> - <aside className="hidden w-60 shrink-0 md:block"> - <div className="sticky top-24"> - <nav className="space-y-6 text-sm"> - {NAV.map((section) => ( - <div key={section.heading}> - <div className="mb-2 text-xs font-semibold uppercase tracking-wider text-zinc-500 dark:text-zinc-400"> - {section.heading} - </div> - <ul className="space-y-1"> - {section.items.map((item) => ( - <li key={item.href}> - <Link - href={item.href} - className="block rounded px-2 py-1 text-zinc-700 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800" - > - {item.label} - </Link> - </li> - ))} - </ul> - </div> - ))} - </nav> - </div> - </aside> - <main className="min-w-0 flex-1"> - <article className="max-w-3xl">{children}</article> - </main> - <aside className="hidden w-56 shrink-0 xl:block"> - <div className="sticky top-24"> - <TOC /> - </div> - </aside> - </div> - ); -} diff --git a/site/app/docs/metrics/hypergraph-metric/commute-time/page.mdx b/site/app/docs/metrics/hypergraph-metric/commute-time/page.mdx deleted file mode 100644 index 28c2bd5..0000000 --- a/site/app/docs/metrics/hypergraph-metric/commute-time/page.mdx +++ /dev/null @@ -1,8 +0,0 @@ -# Commute time distance - -TODO. Will cover: - -- Expected hitting + return time under the harmonic random walk. -- Doyle–Snell identity: - $h(u, v) + h(v, u) = \left(\sum_{x,y} C_{x,y}\right) \cdot d_{R}(u, v)$. -- `IsHypergraphMetric` instance status (currently a 5-hypothesis stub). diff --git a/site/app/docs/metrics/hypergraph-metric/diffusion/page.mdx b/site/app/docs/metrics/hypergraph-metric/diffusion/page.mdx deleted file mode 100644 index b8b6755..0000000 --- a/site/app/docs/metrics/hypergraph-metric/diffusion/page.mdx +++ /dev/null @@ -1,8 +0,0 @@ -# Diffusion distance - -TODO. Will cover: - -- Heat-kernel rows $P^t(u, \cdot)$ as multi-scale neighbourhoods. -- $d_{\mathrm{diff}}^t(u, v) = \| P^t(u, \cdot) - P^t(v, \cdot) \|_{\ell^2(\pi^{-1})}$. -- Limit behaviour as $t \to \infty$. -- `IsHypergraphMetric` instance status. diff --git a/site/app/docs/metrics/hypergraph-metric/jensen-shannon/page.mdx b/site/app/docs/metrics/hypergraph-metric/jensen-shannon/page.mdx deleted file mode 100644 index 94e324a..0000000 --- a/site/app/docs/metrics/hypergraph-metric/jensen-shannon/page.mdx +++ /dev/null @@ -1,8 +0,0 @@ -# Jensen–Shannon distance - -TODO. Will cover: - -- Stationary-neighbourhood distributions $p_u, p_v$. -- $\mathrm{JS}(p_u, p_v) = \tfrac{1}{2}\, \mathrm{KL}(p_u \,\|\, m) + \tfrac{1}{2}\, \mathrm{KL}(p_v \,\|\, m)$ with $m = \tfrac{1}{2}(p_u + p_v)$. -- $\sqrt{\mathrm{JS}}$ is a metric (Endres–Schindelin). -- `IsHypergraphMetric` instance status. diff --git a/site/app/docs/metrics/hypergraph-metric/page.mdx b/site/app/docs/metrics/hypergraph-metric/page.mdx deleted file mode 100644 index 78cc9bb..0000000 --- a/site/app/docs/metrics/hypergraph-metric/page.mdx +++ /dev/null @@ -1,137 +0,0 @@ -# Hypergraph metric (the spec) - -`IsHypergraphMetric` is the *specification* a candidate distance must satisfy -to count as a metric on hypergraphs in the sense of ProofAtlas. -It lives in `ProofAtlas/Metric/Axioms.lean`. A distance that registers -against this spec — and that registration is checked at CI time — is -automatically usable in the geometric profile pipeline, the -hyperbolicity machinery, and every downstream theorem. - -This page describes the seven fields of `IsHypergraphMetric`, the -registration mechanism, and how each concrete metric in the next -layer fills them in. - -## Signature - -A candidate distance has type - -$$ -d \;:\; \mathrm{Hypergraph}\, V\, C \;\to\; \mathrm{WalkParams}\, C \;\to\; V \to V \to \mathbb{R} -$$ - -— a function of a hypergraph $H$, a colour-weighting parameter -$\pi \in \mathrm{WalkParams}$, and two vertices $u, v \in V$. - -## The seven fields - -The structure -`IsHypergraphMetric (d : Hypergraph V C → WalkParams C → V → V → ℝ)` -bundles four metric-space axioms and three sensitivity witnesses. -Pulled live from `Metric/Axioms.lean` <SorryBadge file="lean/ProofAtlas/Metric/Axioms.lean" anchor="IsHypergraphMetric" />: - -<LeanSource - file="lean/ProofAtlas/Metric/Axioms.lean" - anchor="IsHypergraphMetric" -/> - -### Metric-space axioms - -**1. Identity.** -$$ -d_H^\pi(u, u) \;=\; 0 -$$ - -**2. Symmetry.** -$$ -d_H^\pi(u, v) \;=\; d_H^\pi(v, u) -$$ - -**3. Triangle inequality.** -$$ -d_H^\pi(u, w) \;\le\; d_H^\pi(u, v) + d_H^\pi(v, w) -$$ - -**4. Non-negativity.** -$$ -0 \;\le\; d_H^\pi(u, v) -$$ - -These four together force $d_H^\pi$ to be a pseudometric (a true -metric if separation $d_H^\pi(u, v) = 0 \Rightarrow u = v$ also -holds, but ProofAtlas does not require separation in the spec). - -### Sensitivity witnesses - -A distance that ignored *which side* an edge points, *what order* its -inputs are in, or *which inference rule* it represents, would be -geometry on the wrong object. The spec demands non-trivial -sensitivity to each of the four structural dimensions (D, O, C, plus -A holds structurally). - -**5. Direction-sensitive.** There exist hypergraphs $H, H'$ -differing only by swapping one hyperedge's inputs and outputs, -together with params and vertices $u, v$, such that -$d_H^\pi(u, v) \ne d_{H'}^\pi(u, v)$. - -**6. Ordering-sensitive.** There exist $H, H'$ differing only by -permuting one hyperedge's input positions, together with params and -$u, v$, such that $d_H^\pi(u, v) \ne d_{H'}^\pi(u, v)$. - -**7. Colour-sensitive.** There exist $H$, two colour weightings -$\pi, \pi'$ on the same combinatorial $H$, and $u, v$, such that -$d_H^\pi(u, v) \ne d_H^{\pi'}(u, v)$. - -The three witnesses are *existential*: a candidate metric proves it -*can* tell two such hypergraphs apart on at least one example. This -is intentionally weak — it lets us register a metric without -quantifying *how* sensitive it is everywhere. - -## Registration - -A metric is *registered* by: - -1. Defining it as - `noncomputable def myDist ... : ℝ` - somewhere in `ProofAtlas/Metric/`. -2. Proving the instance theorem - `theorem myDist_isHypergraphMetric : IsHypergraphMetric (fun H π u v => myDist H π u v)` - in `ProofAtlas/Metric/<MyDist>/Instance.lean`. -3. Adding `<myDist, ProofAtlas/Metric/<MyDist>/Instance.lean>` to - the registry in `scripts/check_bdf_metric_instances.sh`. - -The CI check refuses to ship if the instance theorem still contains -`sorry`. Every metric in the next layer of these docs is registered -this way. - -<Mermaid chart={` -flowchart TB - Spec["IsHypergraphMetric spec<br/>(4 axioms + 3 witnesses)"] - CI["CI: scripts/<br/>check_bdf_metric_instances.sh"] - R["resistanceDist_isHypergraphMetric"] - C["commuteTimeDist_isHypergraphMetric"] - J["jsDist_isHypergraphMetric"] - D["diffusionDist_isHypergraphMetric"] - Spec --> R --> CI - Spec --> C --> CI - Spec --> J --> CI - Spec --> D --> CI - classDef spec fill:#eef,stroke:#33c,stroke-width:1.5px - classDef ci fill:#efe,stroke:#3c3,stroke-width:1.5px - classDef inst fill:#fff,stroke:#333,stroke-width:1.5px - class Spec spec - class CI ci - class R,C,J,D inst -`} /> - -## What lives in the next layer - -The four registered metrics: - -- [Resistance distance](/docs/metrics/hypergraph-metric/resistance) -- [Commute time distance](/docs/metrics/hypergraph-metric/commute-time) -- [Jensen–Shannon distance](/docs/metrics/hypergraph-metric/jensen-shannon) -- [Diffusion distance](/docs/metrics/hypergraph-metric/diffusion) - -Each page describes the metric's mathematical definition, intuition, -Lean signature, and the current state of its seven `IsHypergraphMetric` -fields (proved, parameterised over a hypothesis, or still `sorry`). diff --git a/site/app/docs/metrics/hypergraph-metric/resistance/page.mdx b/site/app/docs/metrics/hypergraph-metric/resistance/page.mdx deleted file mode 100644 index b7984a8..0000000 --- a/site/app/docs/metrics/hypergraph-metric/resistance/page.mdx +++ /dev/null @@ -1,222 +0,0 @@ -# Resistance distance - -## Mathematical definition - -Let $H = (V, E, \kappa)$ be a hypergraph with conductance matrix -$C \in \mathbb{R}^{V \times V}$ derived from the colour weights -$\kappa$, and graph Laplacian $L_\kappa = D - C$ (where $D$ is the -diagonal degree matrix). The *effective resistance distance* between -two vertices $u, v \in V$ is - -$$ -d_H(u, v) \;=\; (e_u - e_v)^\top L_\kappa^{+} (e_u - e_v) -$$ - -where $L_\kappa^+$ is the Moore–Penrose pseudoinverse and $e_u, e_v$ -are the standard basis vectors. Equivalently, because $e_u - e_v$ is -orthogonal to $\ker L_\kappa = \operatorname{span}(\mathbf{1})$, - -$$ -d_H(u, v) \;=\; L_\kappa^+[u, u] + L_\kappa^+[v, v] - 2\, L_\kappa^+[u, v] . -$$ - -## Intuition - -The resistance distance is *defined* by an electrical-network thought -experiment. Convert the hypergraph into a resistor network, attach a -unit current source between the two vertices of interest, and read -off the voltage drop — that's the distance. - -The conversion from hypergraph to graph is *clique expansion*: a -hyperedge with $k$ incident vertices contributes one unit-conductance -resistor between every pair of those vertices (so $\binom{k}{2}$ -resistors in total). For our running example — one hyperedge $e$ -incident to three vertices $u, v, w$ — that means three resistors, -forming a triangle: - -<figure className="my-6"> - <svg viewBox="0 0 760 360" className="w-full max-w-3xl" xmlns="http://www.w3.org/2000/svg"> - <defs> - <marker id="hg-arrow" viewBox="0 0 10 10" refX="9" refY="5" - markerWidth="7" markerHeight="7" orient="auto"> - <path d="M 0 0 L 10 5 L 0 10 z" fill="#666" /> - </marker> - </defs> - - {/* HYPERGRAPH (left) — directed: u, v are premises (in), w is conclusion (out). */} - <g> - <text x="170" y="22" textAnchor="middle" fontSize="13" fill="currentColor" opacity="0.6">hypergraph (directed)</text> - - {/* hyperedge box */} - <rect x="140" y="160" width="60" height="40" rx="6" fill="#fee" stroke="#c33" strokeWidth="1.5" /> - <text x="170" y="185" textAnchor="middle" fontSize="13" fontFamily="ui-monospace, monospace" fill="#c33">e</text> - - {/* Directed arrows: u → e, v → e (inputs); e → w (output) */} - <g stroke="#666" strokeWidth="1.5" fill="none"> - <line x1="78" y1="118" x2="140" y2="165" markerEnd="url(#hg-arrow)" /> - <line x1="262" y1="118" x2="200" y2="165" markerEnd="url(#hg-arrow)" /> - <line x1="170" y1="200" x2="170" y2="278" markerEnd="url(#hg-arrow)" /> - </g> - - {/* Vertices, drawn last so they sit on top of the arrows' starts */} - <circle cx="60" cy="100" r="22" fill="white" stroke="#333" strokeWidth="1.5" /> - <text x="60" y="105" textAnchor="middle" fontSize="14" fontStyle="italic" fill="#333">u</text> - <circle cx="280" cy="100" r="22" fill="white" stroke="#333" strokeWidth="1.5" /> - <text x="280" y="105" textAnchor="middle" fontSize="14" fontStyle="italic" fill="#333">v</text> - <circle cx="170" cy="300" r="22" fill="white" stroke="#333" strokeWidth="1.5" /> - <text x="170" y="305" textAnchor="middle" fontSize="14" fontStyle="italic" fill="#333">w</text> - - {/* in/out labels */} - <text x="30" y="80" fontSize="10" fill="#888">premise</text> - <text x="280" y="80" fontSize="10" fill="#888">premise</text> - <text x="200" y="305" fontSize="10" fill="#888">conclusion</text> - </g> - - {/* CONNECTING ARROW */} - <g> - <line x1="320" y1="200" x2="400" y2="200" stroke="currentColor" strokeWidth="1.5" opacity="0.5" /> - <polygon points="395,195 407,200 395,205" fill="currentColor" opacity="0.5" /> - <text x="360" y="190" textAnchor="middle" fontSize="11" fill="currentColor" opacity="0.6">clique</text> - <text x="360" y="220" textAnchor="middle" fontSize="11" fill="currentColor" opacity="0.6">expansion</text> - </g> - - {/* CIRCUIT (right) — outer rectangular loop with battery */} - <g transform="translate(420, 0)"> - <text x="170" y="22" textAnchor="middle" fontSize="13" fill="currentColor" opacity="0.6">measurement circuit for R(u, v)</text> - - {/* External loop wires */} - <g stroke="#333" strokeWidth="1.5" fill="none"> - <line x1="40" y1="100" x2="40" y2="320" /> - <line x1="300" y1="100" x2="300" y2="320" /> - <line x1="40" y1="320" x2="150" y2="320" /> - <line x1="190" y1="320" x2="300" y2="320" /> - </g> - - {/* Top: u --- R(u,v) --- v */} - <line x1="62" y1="100" x2="130" y2="100" stroke="#333" strokeWidth="1.5" /> - <rect x="130" y="90" width="80" height="20" fill="white" stroke="#333" strokeWidth="1.5" /> - <text x="170" y="105" textAnchor="middle" fontSize="11" fontFamily="ui-monospace, monospace" fill="#333">1Ω</text> - <line x1="210" y1="100" x2="278" y2="100" stroke="#333" strokeWidth="1.5" /> - - {/* R(u, w): u (40, 100) → w (170, 230). Vector (130, 130), len ≈ 184, angle = 45°. - Midpoint (105, 165). Span ±90 covers it. */} - <g transform="translate(105, 165) rotate(45)"> - <line x1="-92" y1="0" x2="-22" y2="0" stroke="#333" strokeWidth="1.5" /> - <rect x="-22" y="-10" width="44" height="20" fill="white" stroke="#333" strokeWidth="1.5" /> - <text x="0" y="4" textAnchor="middle" fontSize="11" fontFamily="ui-monospace, monospace" fill="#333">1Ω</text> - <line x1="22" y1="0" x2="92" y2="0" stroke="#333" strokeWidth="1.5" /> - </g> - - {/* R(v, w): v (300, 100) → w (170, 230). Mirror: midpoint (235, 165), rotate -45°. */} - <g transform="translate(235, 165) rotate(-45)"> - <line x1="-92" y1="0" x2="-22" y2="0" stroke="#333" strokeWidth="1.5" /> - <rect x="-22" y="-10" width="44" height="20" fill="white" stroke="#333" strokeWidth="1.5" /> - <text x="0" y="4" textAnchor="middle" fontSize="11" fontFamily="ui-monospace, monospace" fill="#333">1Ω</text> - <line x1="22" y1="0" x2="92" y2="0" stroke="#333" strokeWidth="1.5" /> - </g> - - {/* Internal w node — drawn AFTER the diagonal resistors so its - white fill cleanly covers the overshoot. */} - <circle cx="170" cy="230" r="18" fill="white" stroke="#333" strokeWidth="1.5" /> - <text x="170" y="235" textAnchor="middle" fontSize="13" fontStyle="italic" fill="#333">w</text> - - {/* u, v terminals — drawn AFTER everything to occlude. */} - <circle cx="40" cy="100" r="22" fill="white" stroke="#333" strokeWidth="1.5" /> - <text x="40" y="105" textAnchor="middle" fontSize="14" fontStyle="italic" fill="#333">u</text> - <circle cx="300" cy="100" r="22" fill="white" stroke="#333" strokeWidth="1.5" /> - <text x="300" y="105" textAnchor="middle" fontSize="14" fontStyle="italic" fill="#333">v</text> - - {/* Battery on bottom wire */} - <g stroke="#333" strokeWidth="2.5" fill="none"> - <line x1="160" y1="306" x2="160" y2="334" /> - <line x1="180" y1="314" x2="180" y2="326" /> - </g> - <text x="153" y="305" textAnchor="middle" fontSize="11" fill="#333">+</text> - <text x="187" y="305" textAnchor="middle" fontSize="11" fill="#333">−</text> - <text x="170" y="350" textAnchor="middle" fontSize="11" fontFamily="ui-monospace, monospace" fill="#666">I = 1 A</text> - </g> - </svg> - <figcaption className="mt-2 text-center text-sm text-zinc-500 dark:text-zinc-400"> - Hyperedge → clique → measurement. The triangle (u, v, w) is the - network. Inject 1 A at u, extract at v via the bottom-loop battery, - and read the voltage drop V(u) − V(v) = R(u, v) = 2/3 Ω. - (1 Ω direct in parallel with two 1 Ω in series = 1·2/3.) - </figcaption> -</figure> - -The resistance distance $d_H(u, v)$ then measures *how hard it is to -disconnect $u$ from $v$ by removing inference steps*. Many short, -parallel proof paths from $u$ to $v$ pull the resistance down; a -single bottleneck pulls it up. It is the natural metric for the -question *how independent are two propositions, given the deductive -structure of the surrounding mathematics?* - -## Lean implementation - -The non-computability comes from the `Real`-valued pseudoinverse on -the proof side. The compute side (`Pipeline/Linalg.lean`) instead -inverts a regularised `Float` matrix via hand-rolled Gaussian -elimination — equivalent up to the small $\varepsilon$ regularisation -because $e_u - e_v$ lies in the image of $L_\kappa$. - -The `IsHypergraphMetric` instance signature, pulled live from the repo -<SorryBadge file="lean/ProofAtlas/Metric/Resistance/Instance.lean" anchor="resistanceDist_isHypergraphMetric_signature" />: - -<LeanSource - file="lean/ProofAtlas/Metric/Resistance/Instance.lean" - anchor="resistanceDist_isHypergraphMetric_signature" -/> - -The signature accepts five preconditions: two structural -(`hConn`, `hRange`) about the harmonic Laplacian, and three -sensitivity witnesses passed in by the caller. The body discharges -all seven `IsHypergraphMetric` fields and contains no `sorry`. - -## `IsHypergraphMetric` status - -| Field | Status | Notes | -|---|---|---| -| `d_self` | ✓ | from `effectiveResistance_self` | -| `d_symm` | ✓ | $L_\kappa^+$ is symmetric | -| `d_triangle` | ✓ | via Schur-complement / Rayleigh argument | -| `d_nonneg` | ✓ | $L_\kappa^+$ is positive semidefinite | -| `sensitive_to_direction` | hypothesis | parameterised over the existence witness | -| `sensitive_to_ordering` | hypothesis | parameterised over the existence witness | -| `sensitive_to_coloring` | hypothesis | parameterised over the existence witness | - -The instance theorem `resistanceDist_isHypergraphMetric` lives at -`ProofAtlas/Metric/Resistance/Instance.lean` and is `sorry`-free; -the three sensitivity witnesses are passed in as preconditions to be -discharged by the caller (one concrete discharge per example -hypergraph). - -## Rayleigh monotonicity - -A clean structural consequence: *adding inference rules never -increases resistance distance.* Formally, if $H'$ is obtained from -$H$ by adding hyperedges (more inference paths), then for every -$u, v$, - -$$ -d_{H'}(u, v) \;\le\; d_H(u, v) . -$$ - -This is the Rayleigh monotonicity principle from electrical network -theory: adding conductors can only reduce effective resistance. For -ProofAtlas it means *introducing new lemmas makes the rest of -mathematics closer, never farther* — exactly the intuition we want -from a knowledge-structure metric. - -## Worked example - -For the toy expression `id Nat` — three vertices (`id`, `Nat`, -the application node) and one hyperedge forming a triangle — the -resistance between any pair is - -$$ -d_H(u, v) \;=\; \tfrac{2}{3} \;\approx\; 0.667 . -$$ - -See the [front page table](/) for the live `#eval` output and the -[Pipeline page](/docs/pipeline) for the full report on a small -multi-declaration environment. diff --git a/site/app/docs/metrics/page.mdx b/site/app/docs/metrics/page.mdx deleted file mode 100644 index 84be302..0000000 --- a/site/app/docs/metrics/page.mdx +++ /dev/null @@ -1,49 +0,0 @@ -# Step 2 · Metrics overview - -ProofAtlas separates the *spec* of a metric from any particular -*instance* of one. The spec is one file. The instances are many. - -## Two layers - -1. **[Hypergraph metric (spec)](/docs/metrics/hypergraph-metric).** The - `IsHypergraphMetric` specification: four metric-space axioms (identity, - symmetry, triangle inequality, non-negativity) plus three - sensitivity witnesses (direction, ordering, colour). Any - candidate distance that satisfies this spec is registered as a - hypergraph metric and automatically usable downstream. Registration - is checked at CI time. - -2. **Registered metrics.** Concrete distances that have a sorry-free - `IsHypergraphMetric` instance theorem. The framework is open — more can - be added at any time. The metrics we currently ship: - - - [Resistance distance](/docs/metrics/hypergraph-metric/resistance) — - effective resistance in the clique-expanded conductance network. - Measures how hard two propositions are to disconnect by removing - inference steps. - - [Commute time distance](/docs/metrics/hypergraph-metric/commute-time) — - expected random-walk round-trip time. Dual to resistance via - Doyle–Snell. - - [Jensen–Shannon distance](/docs/metrics/hypergraph-metric/jensen-shannon) — - information-theoretic divergence between the stationary - neighbourhoods of two vertices. - - [Diffusion distance](/docs/metrics/hypergraph-metric/diffusion) — $L^2$ - distance between heat-kernel rows at finite time, capturing - multi-scale proximity. - -## Why split spec from instance - -Three reasons: - -- *Replaceability.* Any downstream theorem that needs "a metric" - takes an `IsHypergraphMetric` hypothesis, not a specific distance. Swap - in a new metric and every downstream result still applies. - -- *Honesty about sensitivity.* The three sensitivity witnesses force - every registered metric to *demonstrate* it distinguishes the four - structural axes — a metric that quietly ignored direction or colour - would fail to register. - -- *CI discipline.* `scripts/check_bdf_metric_instances.sh` walks the - registry and refuses to merge if any instance is `sorry`'d. The - spec is the contract; CI is the enforcer. diff --git a/site/app/docs/page.mdx b/site/app/docs/page.mdx deleted file mode 100644 index 6572082..0000000 --- a/site/app/docs/page.mdx +++ /dev/null @@ -1,79 +0,0 @@ -# Architecture - -ProofAtlas is organized into four interconnected layers. Each layer -serves a single purpose and exposes a clean interface to the layers -above and below. - -## Layer 1 · Proof layer - -The Lean kernel and a small library of structural definitions on top -of it. This is where mathematics lives in its primary form: -propositions, proof terms, dependencies. - -`ProofAtlas/Combinatorial/` defines the *hypergraph* — directed, -ordered, coloured, acyclic, connected — as a Lean 4 `structure`. -Vertices are propositions; hyperedges are individual inference steps; -the colour records the rule applied. The four structural axioms -(D, O, C, A) are encoded as fields, not as auxiliary lemmas, so they -hold by construction. - -`ProofAtlas/Metric/Axioms.lean` defines the `IsHypergraphMetric` spec: seven -fields that a candidate distance must satisfy to be a "metric on -proof hypergraphs" in the sense of the paper. - -## Layer 2 · Translation layer - -`ProofAtlas/Mapping/Expr.lean` is the bridge. Given any -`e : Lean.Expr`, it produces - -`Lean.Expr.toHypergraph e : Hypergraph (Fin (countSubterms e)) ExprColor` - -with all five structural axioms machine-checked. The proof of the -`connected` field — every non-root DFS vertex is incident to its -parent compound — is the load-bearing lemma; once it is sealed, every -Lean declaration in any library ships with a hypergraph for free. - -## Layer 3 · Compute layer - -`ProofAtlas/Pipeline/` runs the actual numbers. It is intentionally -separate from the proof layer: `Float` arithmetic, no proof -obligations, fully `#eval`-able. The dataflow is - -``` -Lean.Environment - ↓ Environment.toIncidenceData (DFS walk + const-sharing) -IncidenceData - ↓ resistanceMatrix (Gaussian elimination) -Array (Array Float) - ↓ hyperbolicityConstant (4-tuple enumeration) -Float -``` - -The compute layer's only contract with the proof layer is shape: an -`IncidenceData` represents the same combinatorial object as a -`Hypergraph`. A future bridging lemma will pin that down formally; -for now the two layers are kept on speaking terms by convention. - -## Layer 4 · Quality layer - -Tests, linters, and CI. `scripts/check_bdf_metric_instances.sh` -walks the metric registry and refuses to ship a metric whose -`IsHypergraphMetric` instance theorem still contains `sorry`. The -`HypergraphGeo.Linter.MathTag` linter forces every declaration to -tag its docstring as `Math`, `Eng`, or `Mixed`, so it is always -obvious which lines correspond to paper content and which are -type-theoretic plumbing. - -## What this site documents - -The pages in this section trace the four steps of the ProofAtlas -project, mirroring the layer structure: - -- *Step 1* — the hypergraph and the `Lean.Expr` translation. -- *Step 2* — the `IsHypergraphMetric` spec (outer layer) and the four - metrics currently registered against it (resistance, commute time, - Jensen–Shannon, diffusion). -- *Step 3* — the theory developed on top: δ-hyperbolicity, spectral - identities, Hausdorff and Gromov–Hausdorff. -- *Step 4* — the end-to-end profile pipeline and the `#atlas.*` - command suite. diff --git a/site/app/docs/pipeline/page.mdx b/site/app/docs/pipeline/page.mdx deleted file mode 100644 index 10232e0..0000000 --- a/site/app/docs/pipeline/page.mdx +++ /dev/null @@ -1,15 +0,0 @@ -# Step 4 · End-to-end pipeline - -TODO. Will cover: - -- The `geometricProfile : Lean.Environment → IO HypergraphReport` - entry point. -- Const-sharing as the glue: every `.const Name` reference (across - all declarations) maps to a single shared vertex. -- The four computed quantities — incidence data, conductance, - resistance matrix, δ-hyperbolicity — and what each tells you. -- The `#atlas.*` command suite for invocation at the Lean - command prompt (see [Command reference](/docs/commands)). - -For a runnable demo today, see the live demo on the [front -page](/) or `ProofAtlas/Demos/Pipeline.lean` in the repo. diff --git a/site/app/docs/structure/page.mdx b/site/app/docs/structure/page.mdx deleted file mode 100644 index 5427c94..0000000 --- a/site/app/docs/structure/page.mdx +++ /dev/null @@ -1,183 +0,0 @@ -# Step 1 · Hypergraph - -## Definition - -A *hypergraph* (in the sense of [BDF26](#references)) is a tuple -$H = (V, E, c)$ where: - -- $V$ is a finite set of *vertices*, interpreted as propositions of a - formal mathematical language; -- $E$ is a finite set of *hyperedges*, each interpreted as a single - application of a deductive rule; -- $c : E \to C$ is a *colouring*, assigning to every hyperedge the - label of the deductive rule it applies. - -Each hyperedge $e \in E$ is itself a tuple - -$$ -e \;=\; (e^{\mathrm{in}};\, e^{\mathrm{out}}) \;=\; \bigl( (e^{\mathrm{in}}_1, \ldots, e^{\mathrm{in}}_{p_e}); \; (e^{\mathrm{out}}_1, \ldots, e^{\mathrm{out}}_{q_e}) \bigr) -$$ - -with $e^{\mathrm{in}} \in V^{p_e}$ the (ordered) tuple of *premises*, -$e^{\mathrm{out}} \in V^{q_e}$ the (ordered) tuple of *conclusions*, -and $p_e, q_e \ge 1$. - -Within a single edge, all vertices are distinct: premises are -pairwise distinct, conclusions are pairwise distinct, and no premise -coincides with a conclusion of the *same* edge. - -## The four structural axioms - -Following Barkeshli–Douglas–Freedman ([BDF26](#references)), the -hypergraph satisfies: - -- **(D) Directed.** Every edge has a distinguished input side and - output side. Inputs and outputs are typed differently and never - coincide within an edge. - -- **(O) Ordered.** Inputs and outputs are *tuples*, not sets: - $e^{\mathrm{in}}_1 \ne e^{\mathrm{in}}_2$ as positions even if the - underlying vertices were equal. The order matters because the - position carries the deductive role (which premise is the major - premise, etc.). - -- **(C) Coloured.** Each edge carries a colour $c(e) \in C$ recording - *which* deductive rule was applied. Two edges with identical input - and output tuples but different colours are different edges. - -- **(A) Acyclic.** No proposition is its own descendant. Formally, - the transitive closure of the input-to-output relation - - $$ - v \to w \;\iff\; \exists\, e \in E.\; v \in e^{\mathrm{in}} \text{ and } w \in e^{\mathrm{out}} - $$ - - is irreflexive. - -We additionally require $H$ to be *connected*: any two vertices are -linked by an undirected path through hyperedge incidence (a vertex is -*incident* to an edge if it appears in its input or output tuple). - -## Example: modus ponens - -The single inference step - -$$ -\frac{\;A,\; A \Rightarrow B\;}{B}\;\;\text{(MP)} -$$ - -corresponds to a hyperedge with $p_e = 2$, $q_e = 1$, -$e^{\mathrm{in}} = (A,\; A{\Rightarrow}B)$, -$e^{\mathrm{out}} = (B)$, and $c(e) = \text{MP}$. Drawn with vertices -as circles and the hyperedge as a coloured box: - -<Mermaid chart={` -flowchart LR - A(("A")) --> MP["MP"] - AB(("A ⇒ B")) --> MP - MP --> B(("B")) - classDef vertex fill:#fff,stroke:#333,stroke-width:1.5px - classDef edge fill:#fee,stroke:#c33,stroke-width:1.5px - class A,AB,B vertex - class MP edge -`} /> - -## Example: conjunction introduction - -The rule - -$$ -\frac{\;A,\; B\;}{A \wedge B}\;\;(\wedge\text{-I}) -$$ - -is a hyperedge with the same shape ($p_e = 2$, $q_e = 1$) but a -different colour: $c(e) = \wedge\text{-intro}$. The colouring is -what distinguishes these two two-in-one-out edges. - -<Mermaid chart={` -flowchart LR - A(("A")) --> AI["∧-I"] - B(("B")) --> AI - AI --> AB(("A ∧ B")) - classDef vertex fill:#fff,stroke:#333,stroke-width:1.5px - classDef edge fill:#eef,stroke:#33c,stroke-width:1.5px - class A,B,AB vertex - class AI edge -`} /> - -## Example: a small derivation - -Stringing two edges together: from premises $A$, $B$, $B \Rightarrow C$ -we derive $A \wedge C$ in two steps (one MP, then one $\wedge$-I). -The resulting hypergraph has five vertices and two hyperedges: - -<Mermaid chart={` -flowchart LR - A(("A")) --> AI["∧-I"] - B(("B")) --> MP["MP"] - BC(("B ⇒ C")) --> MP - MP --> C(("C")) - C --> AI - AI --> AC(("A ∧ C")) - classDef vertex fill:#fff,stroke:#333,stroke-width:1.5px - classDef edgeMP fill:#fee,stroke:#c33,stroke-width:1.5px - classDef edgeAI fill:#eef,stroke:#33c,stroke-width:1.5px - class A,B,BC,C,AC vertex - class MP edgeMP - class AI edgeAI -`} /> - -## Lean encoding - -The structure lives in -`ProofAtlas/Hypergraph/Basic.lean`: - -``` -structure Hyperedge (V : Type u) (C : Type v) where - inArity : Nat - inVertex : Fin inArity → V - outArity : Nat - outVertex : Fin outArity → V - color : C - inArity_pos : 0 < inArity - outArity_pos : 0 < outArity - inputOutputDisjoint : ∀ i j, inVertex i ≠ outVertex j - inVertex_injective : Function.Injective inVertex - outVertex_injective : Function.Injective outVertex - -structure Hypergraph (V : Type u) (C : Type v) where - edges : Type w - edge : edges → Hyperedge V C - acyclic : Hypergraph.IsAcyclic edge - connected : ∀ u v, Relation.ReflTransGen (incidence) u v -``` - -The axioms (D), (O), (C) live *per edge* as `Hyperedge` fields; -(A) is the cross-edge `acyclic` field on `Hypergraph`. - -## From `Lean.Expr` to hypergraph - -Every Lean proof term `e : Lean.Expr` induces a hypergraph -automatically, via - -``` -Lean.Expr.toHypergraph e : Hypergraph (Fin (countSubterms e)) ExprColor -``` - -defined in `ProofAtlas/Mapping/Expr.lean`. The five -structural proofs (`inArity_pos`, `outArity_pos`, -`inputOutputDisjoint`, `inVertex_injective`, `outVertex_injective`, -plus `acyclic` and `connected`) are all `sorry`-free. - -The load-bearing lemma is `vertex_has_parent`: every non-root DFS -vertex is incident to the hyperedge of its parent compound subterm. -It is proved by induction on the twelve `Lean.Expr` constructors; -the six compound cases each split into a child-block case analysis -with shifted induction hypotheses. - -## References - -[BDF26] Maissam Barkeshli, Michael R. Douglas, Michael H. Freedman, -*Artificial Intelligence and the Structure of Mathematics*, -arXiv:2604.06107, 2026. §2.1. -[arxiv.org/abs/2604.06107](https://arxiv.org/abs/2604.06107) diff --git a/site/app/docs/theory/commute-time/page.mdx b/site/app/docs/theory/commute-time/page.mdx deleted file mode 100644 index 38eb25c..0000000 --- a/site/app/docs/theory/commute-time/page.mdx +++ /dev/null @@ -1,107 +0,0 @@ -# Commute time distance - -The *commute time distance* is the second of the four hypergraph metrics. -It comes from the same random walk as resistance distance, but -measures something subtly different — and that difference matters. - -## Definition - -Let $h(u, v)$ be the *expected first-passage time* of the harmonic -random walk from $u$ to $v$ — the expected number of steps to first -reach $v$ starting from $u$. The commute time distance is the -symmetric round trip: - -$$ -d_{\mathrm{ct}}(u, v) \;=\; h(u, v) + h(v, u). -$$ - -It lives at `ProofAtlas/Metric/CommuteTime/CommuteTime.lean:36`. - -## Intuition: random-walk efficiency - -If you stand at $u$ and let the random walk run, $h(u, v)$ is how -long you expect to wait before bumping into $v$. The commute time -is your wait if you also have to come back. It's a natural notion -of "how reachable is $v$ from $u$" that takes into account *every* -path, weighted by its likelihood. - -A short commute time means there are many likely short routes from -$u$ to $v$ and back. A long commute time means $v$ is a needle in a -haystack from $u$'s vantage point — the walk is likely to get lost -visiting other vertices first. - -## Relationship to resistance - -For a finite connected reversible Markov chain with conductance -$C = (C_{u, v})$, total weight $T = \sum_{u, v} C_{u, v}$, and -effective resistance $R_{\mathrm{eff}}(L_C; u, v)$ of the associated -Laplacian $L_C = D - C$: - -$$ -d_{\mathrm{ct}}(u, v) \;=\; T \cdot R_{\mathrm{eff}}(L_C; u, v). -$$ - -This is the *Chandra–Raghavan–Ruzzo–Smolensky / Doyle–Snell identity*. -Up to the global factor $T$, commute time is just resistance. - -For the harmonic random walk this identity is formalised in -`ProofAtlas/Markov/CommuteTime.lean`. So the question "why have both?" -is sharper than it looks: the multiplicative factor $T$ depends on -the total conductance budget of the hypergraph, which depends on -$|V|$ and the colour weights $\alpha$. - -## Where the two diverge — degree contamination - -The single difference between resistance and commute time is the -$T$ factor. $T$ scales like $|V|$ — total conductance grows with -the graph size — so commute times scale *quadratically* with $|V|$ -while resistances stay $O(1)$ for "geometrically close" pairs. - -This is sometimes called the *degree contamination* of commute time: -on a large random graph, almost every pair has commute time -$\approx 2|V|$, drowning out the geometric information that -resistance preserves. In small-world / scale-free regimes this is -severe. - -For ProofAtlas's use case — small connected components, $|V|$ in -the dozens — the contamination is negligible and the two distances -agree up to a global scale. The choice between them is then -mathematical convenience: commute time is more natural for -"how often does the random walk visit $v$?", resistance for -"how strongly is $v$ wired into the network?". - -## Quality status - -`commuteTimeDist_isHypergraphMetric` is **certified** as of -[the most recent commit](https://github.com/MathNetwork/ProofAtlas/commit/d83231c) -— all four metric axioms come from the proven -`commuteTimeDist_self`, `_symm`, `_triangle`, and -`hittingTime_nonneg` lemmas in -`Metric/CommuteTime/{CommuteTime,HittingTime}.lean`. The -sensitivity witnesses use the same hypothesis pattern as -resistance. - -The compute-layer Float implementation is still TODO — it would -require porting the hitting-time matrix inversion (currently -`noncomputable`) to the `Pipeline/` layer. Once that lands, an -`#atlas.commute A B` command can be wired up to mirror -`#atlas.resistance`. - -## Try it - -`#atlas.commute` is currently not implemented as a command. Until -the Float compute path lands, the closest thing is verifying the -quality certificate: - -→ [`#atlas.status commuteTimeDist`](/docs/commands#atlasstatus) -should report ✓ certified. - -For the metric-agnostic theorems that apply to commute time (and -every other registered metric) see [Hyperbolicity](/docs/theory/hyperbolicity). - -## References - -[BDF26] §5.2 — commute time on hypergraphs.<br /> -[CRRS96] Chandra, Raghavan, Ruzzo, Smolensky, *The electrical -resistance of a graph captures its commute and cover times*, -Computational Complexity 6, 1996. diff --git a/site/app/docs/theory/hyperbolicity/page.mdx b/site/app/docs/theory/hyperbolicity/page.mdx deleted file mode 100644 index 0e53813..0000000 --- a/site/app/docs/theory/hyperbolicity/page.mdx +++ /dev/null @@ -1,126 +0,0 @@ -# Gromov δ-hyperbolicity - -A single real number that summarises the large-scale shape of a -metric space. For hypergraphs it answers: *how tree-like is the -proof landscape?* - -## The four-point condition - -A metric space $(X, d)$ is *δ-hyperbolic* if every four-tuple of -points $w, x, y, z$ satisfies - -$$ -d(w, x) + d(y, z) \;\le\; \max\bigl(d(w, y) + d(x, z),\; d(w, z) + d(x, y)\bigr) + 2\delta. -$$ - -Equivalently, defining the three pairings - -$$ -S_1 = d(w, x) + d(y, z), \quad S_2 = d(w, y) + d(x, z), \quad S_3 = d(w, z) + d(x, y), -$$ - -the *four-point deviation* is -$\mathrm{fpd}(w, x, y, z) = \max(S_1, S_2, S_3) - \operatorname{med}(S_1, S_2, S_3)$, -and the *Gromov δ-hyperbolicity constant* is - -$$ -\delta \;=\; \tfrac{1}{2} \cdot \sup_{w, x, y, z} \mathrm{fpd}(w, x, y, z). -$$ - -A real tree is exactly $\delta = 0$. Hyperbolic spaces (the -hyperbolic plane, Cayley graphs of free groups, $\delta$-thin -metric spaces) have small bounded $\delta$. Euclidean $\mathbb{R}^2$ -has $\delta = \infty$ — for every $\delta_0$ you can find four -points violating the inequality. - -## Intuition — large-scale tree-likeness - -Two equivalent reformulations make the intuition vivid: - -**Thin triangles.** A geodesic triangle is *$\delta$-slim* if every -side lies within distance $\delta$ of the union of the other two. -In a $\delta$-hyperbolic space every triangle is $\delta$-slim. So -"hyperbolic" $\approx$ "no fat triangles". - -**Stability of quasi-geodesics.** In a hyperbolic space, any -$(K, C)$-quasi-geodesic stays within bounded distance of an actual -geodesic with the same endpoints. The bound depends only on -$\delta, K, C$, not the length of the path. Translation: small -detours don't compound — you can't drift far off-course by -accumulating local error. - -**Boundary at infinity.** A hyperbolic space carries a canonical -boundary $\partial_\infty X$ — the equivalence classes of geodesic -rays under finite-distance proximity. For hypergraphs this -gives a candidate notion of *limit theory* even though the -hypergraph itself is finite. - -For proofs the consequence is: tree-like proof landscapes have -predictable global geometry. Even noisy navigation through the -hypergraph stays close to optimal. - -## The δ/diam ratio - -The bare δ depends on the absolute scale of the metric — multiplying -all distances by $c$ multiplies $\delta$ by $c$. The -scale-invariant version is - -$$ -\widetilde\delta \;=\; \frac{\delta}{\operatorname{diam}(H)}. -$$ - -This is the *normalised tree-likeness ratio* of paper §7. Values -near $0$ mean tree-like; values near $1/2$ mean about as -non-tree-like as possible. - -For the demos on the front page: - -| Demo | δ | diam | δ/diam | -|---|---|---|---| -| `id Nat` (triangle) | 0 | 2/3 | 0 | -| `Nat.add_zero ⨯ Nat.add_comm` | ≈ 0.24 | ≈ 3.21 | ≈ 0.074 | -| 3-decl glue | ≈ 0.24 | ≈ 3.21 | ≈ 0.074 | - -Numbers in the few-percent range say "almost tree-like" — exactly -what you'd expect for proof terms with high reuse via const-sharing. - -## Metric-agnostic - -The δ-hyperbolicity machinery quantifies over an arbitrary -`IsHypergraphMetric d`, not a specific metric. Every theorem in -`ProofAtlas/Hyperbolicity/Basic.lean` reads - -```lean -{d : Hypergraph V C → WalkParams C → V → V → ℝ} -{isMetric : IsHypergraphMetric d} -``` - -so swapping resistance for Jensen–Shannon (once the latter -certifies) does not invalidate any δ-result. *That* is what -"Layer 3 is metric-agnostic" buys us. - -## Compute side - -For $|V| \le 30$ the $O(n^4)$ enumeration over four-tuples is fine — -`Pipeline/Hyperbolicity.lean` does it directly. For larger -hypergraphs a Monte-Carlo sampling pass is the planned route (not -yet wired). The compute side runs on `Float` and is called by -[`#atlas.delta`](/docs/commands#atlasdelta). - -## Open problem - -The sole intentional `sorry` in the project is paper Conjecture 7.3 -(`ProofAtlas/Open.lean`): the worst-case hyperbolicity of the -universal hypergraph. Statement is type-checked; proof is open. - -## Try it - -→ [`#atlas.delta A [B C ...]`](/docs/commands#atlasdelta) reports -δ, diam, and δ/diam for the const-shared glue of any declaration -list. - -## References - -[BDF26] §7 — δ-hyperbolicity, Theorem 7.1, Conjecture 7.3.<br /> -[Gro87] Gromov, *Hyperbolic groups*, in *Essays in group theory*, -MSRI Publ. 8, 1987. diff --git a/site/app/docs/theory/page.mdx b/site/app/docs/theory/page.mdx deleted file mode 100644 index fa95d33..0000000 --- a/site/app/docs/theory/page.mdx +++ /dev/null @@ -1,67 +0,0 @@ -# Step 3 · Theory - -The theory stack ProofAtlas formalises is three layers thick. Each -layer builds on the one below, and each has a dedicated page. - -## Layer 1 — Structure - -A *hypergraph* (in the sense of Barkeshli–Douglas–Freedman) is -directed, ordered, coloured, and acyclic. It is the combinatorial -fingerprint of a CIC proof term: every `Lean.Expr` deterministically -lowers to one. - -→ [Structure layer](/docs/theory/structure) - -## Layer 2 — Metric - -Four candidate distances are registered against the -[`IsHypergraphMetric`](/docs/theory/quality) specification: - -| Distance | Page | Intuition | -|---|---|---| -| Resistance | [Resistance](/docs/theory/resistance) | how hard is it to cut the connection? | -| Commute time | [Commute time](/docs/theory/commute-time) | random walk round-trip cost | -| Jensen–Shannon | [Metrics overview](/docs/metrics/hypergraph-metric/jensen-shannon) | divergence of stationary neighbourhoods | -| Diffusion | [Metrics overview](/docs/metrics/hypergraph-metric/diffusion) | heat-kernel distance at scale $t$ | - -Two are sorry-free (`resistanceDist`, `commuteTimeDist`); the other -two are in progress. The [Quality](/docs/theory/quality) page -explains what "certified" means and how to check it via -[`#atlas.status`](/docs/commands#atlasstatus). - -## Layer 3 — Geometry - -Once a distance is an `IsHypergraphMetric`, all of metric geometry applies -to it generically. The headline invariant is -[Gromov δ-hyperbolicity](/docs/theory/hyperbolicity) — a single -real number that says how tree-like the proof landscape is at large -scale. Two further notions, the *Hausdorff distance* between proof -skeletons and the *Gromov–Hausdorff distance* between whole proof -hypergraphs, sit on the same generic API. - -→ [Hyperbolicity](/docs/theory/hyperbolicity) - -## How the pieces fit - -``` - Lean.Expr ──Layer 1──► Hypergraph - │ - │ pick a metric: - ▼ - (V, d) ──Layer 2──► metric space - │ - │ generic geometric theorems: - ▼ - δ, Hausdorff, GH ──Layer 3──► proof landscape -``` - -Every theorem in Layer 3 quantifies over an arbitrary `IsHypergraphMetric`, -so swapping resistance for Jensen–Shannon doesn't invalidate any -result. The four metric definitions are interchangeable inputs into -the same generic theory. - -## References - -[BDF26] Barkeshli, Douglas, Freedman, -*Artificial Intelligence and the Structure of Mathematics*, -arXiv:2604.06107, 2026. §§5–7. diff --git a/site/app/docs/theory/quality/page.mdx b/site/app/docs/theory/quality/page.mdx deleted file mode 100644 index 0ec75de..0000000 --- a/site/app/docs/theory/quality/page.mdx +++ /dev/null @@ -1,102 +0,0 @@ -# Quality — what `IsHypergraphMetric` certifies - -`IsHypergraphMetric d` is the formal spec a candidate distance must satisfy -to enter ProofAtlas's metric registry. Layer 3 (δ-hyperbolicity, -Hausdorff, Gromov–Hausdorff) quantifies over any `d` carrying this -witness — so the spec is what makes the whole "swap any metric, all -theorems still hold" pitch concrete. - -## The seven checks - -`structure IsHypergraphMetric` (defined in `ProofAtlas/Metric/Axioms.lean`) -demands seven fields: - -**Four metric axioms.** For every hypergraph $H$, every walk -parameter choice $\pi$, and all vertices $u, v, w$: - -1. **`d_self`** — $d(u, u) = 0$ -2. **`d_symm`** — $d(u, v) = d(v, u)$ -3. **`d_triangle`** — $d(u, w) \le d(u, v) + d(v, w)$ -4. **`d_nonneg`** — $0 \le d(u, v)$ - -**Three BDF-sensitivity witnesses.** The distance must actually -depend on the BDF structure — not just be a constant or a function -of $|V|$: - -5. **`sensitive_to_direction`** — there exist $H, H'$ differing - only by swapping a hyperedge's inputs and outputs, plus a pair - $(u, v)$ on which $d$ differs. -6. **`sensitive_to_ordering`** — there exist $H, H'$ differing only - by permuting one hyperedge's input positions, plus a pair on - which $d$ differs. -7. **`sensitive_to_coloring`** — there exists $H$ and two colour - weightings $(\pi, \pi')$ on which $d$ differs. - -Fields 1–4 are the textbook metric axioms. Fields 5–7 are -ProofAtlas-specific: they enforce that the metric *uses* the four -BDF structural properties (direction, ordering, colouring, -acyclicity) rather than discarding them. - -## Why the spec is necessary - -Without 5–7, the *discrete metric* $d(u, v) = \mathbf{1}_{u \ne v}$ -would satisfy fields 1–4 trivially. It would also be useless — it -tells us nothing about the proof structure beyond "same or -different". Fields 5–7 rule it out: the discrete metric does not -depend on direction or ordering or colour, so it fails all three -sensitivity witnesses. - -More subtle counterexamples: ad-hoc distances that depend only on -the underlying *undirected* graph (failing direction sensitivity), -or only on the *unordered* hyperedge structure (failing ordering -sensitivity), or only on the *combinatorial* hypergraph without -colour weighting (failing colour sensitivity). The spec -formalises "this metric actually uses everything BDF gives us". - -## Current registry - -Two metrics are certified, two are in progress: - -| Metric | Status | Where the proof lives | -|---|---|---| -| `resistanceDist` | ✓ certified | `Metric/Resistance/Instance.lean` | -| `commuteTimeDist` | ✓ certified | `Metric/CommuteTime/Instance.lean` | -| `jsDist` | ⚠ in progress | `Metric/JensenShannon/Instance.lean` | -| `diffusionDist` | ⚠ in progress | `Metric/Diffusion/Instance.lean` | - -"In progress" means the instance theorem exists but its body still -uses `sorry`. The certified ones have all seven fields discharged -sorry-free; the ergodic preconditions and sensitivity witnesses are -hoisted to instance parameters (see the Resistance instance for the -canonical pattern). - -The check is mechanical: `Lean.collectAxioms` walks the proof's -transitive axiom closure. Sorry-free ⟺ `sorryAx` is not in that -set. - -## Adding a new metric - -The recipe documented in -[`CONTRIBUTING.md`](https://github.com/MathNetwork/ProofAtlas/blob/main/CONTRIBUTING.md): - -1. Define your metric in `ProofAtlas/Metric/YourMetric/Basic.lean`. -2. Prove `yourMetricDist_isHypergraphMetric` in - `ProofAtlas/Metric/YourMetric/Instance.lean`. -3. Run [`#atlas.status yourMetricDist`](/docs/commands#atlasstatus) - — it should show ✓. -4. CI checks automatically (`scripts/check_bdf_metric_instances.sh`). - -Once certified, every theorem in Layer 3 immediately applies to -your metric. You get δ-hyperbolicity, Hausdorff distance, and -Gromov–Hausdorff distance for free. - -## Try it - -→ [`#atlas.status myDist`](/docs/commands#atlasstatus) looks up -the registry and runs the sorry check live. - -## References - -`ProofAtlas/Metric/Axioms.lean` — the `IsHypergraphMetric` definition.<br /> -`scripts/check_bdf_metric_instances.sh` — the CI variant of the -same check. diff --git a/site/app/docs/theory/resistance/page.mdx b/site/app/docs/theory/resistance/page.mdx deleted file mode 100644 index 8389359..0000000 --- a/site/app/docs/theory/resistance/page.mdx +++ /dev/null @@ -1,113 +0,0 @@ -# Resistance distance - -The *effective resistance distance* on a hypergraph is the -oldest and best-behaved of the four candidates: it has a closed-form -spectral expression, satisfies the metric axioms unconditionally, -and is monotone under hyperedge addition. It is the only metric -currently sorry-free. - -## Definition - -Pick a conductance schedule $\kappa : E \times V \times V \to \mathbb{R}_{\ge 0}$ -(see below). The associated weighted Laplacian is -$L_\kappa = D - A$ where $A_{u,v} = \sum_e \kappa(e, u, v)$ is the -total conductance between $u$ and $v$ and $D$ is its row-sum diagonal. -Write $L_\kappa^+$ for the Moore–Penrose pseudoinverse. - -The *resistance distance* between $u, v \in V$ is - -$$ -d_H(u, v) \;=\; (e_u - e_v)^{\top} \, L_\kappa^{+} \, (e_u - e_v) \;=\; L_\kappa^{+}[u,u] + L_\kappa^{+}[v,v] - 2 L_\kappa^{+}[u,v]. -$$ - -Here $e_u \in \mathbb{R}^V$ is the standard basis vector at $u$. - -## Why this is a distance - -Three properties drop out of the spectral formula directly: - -- **$d_H(u, u) = 0$**: the vector $e_u - e_u = 0$ kills the - quadratic form. -- **Symmetry**: $L_\kappa^+$ is symmetric (it's a pseudoinverse of a - symmetric matrix). -- **Non-negativity**: $L_\kappa$ is positive semidefinite (it's a - weighted Laplacian), and so is its pseudoinverse, so the quadratic - form is $\ge 0$. - -Triangle inequality is harder; the proof routes through the -*Klein–Randić formulation* — see `Metric/Resistance/Algebraic.lean`. - -## Intuition: how hard is it to cut the connection? - -Treat each hyperedge as a resistor network with conductance -$\kappa(e, u, v)$ between every incident pair. Then $d_H(u, v)$ is -*literally* the electrical resistance of the resulting circuit -between terminals $u$ and $v$. - -That makes the inequality - -$$ -d_H(u, v) \;\le\; (\text{any unweighted path length between } u \text{ and } v) -$$ - -obvious. It also explains why two vertices in a densely-connected -hyperedge cluster are close (many parallel resistors) while two -vertices joined by a long thin chain are far (resistors in series). - -## Conductance from hyperedges - -The conductance comes from the harmonic schedule - -$$ -\kappa(e, u, v) \;=\; \frac{\alpha(c(e))}{(i_u + 1) \cdot |\mathrm{out}(e)|} -$$ - -when $u$ is the $i_u$-th input of $e$ and $v$ is an output of $e$; -symmetrised by adding $\kappa(e, v, u)$. The $\alpha(c)$ factor -weights edges by colour, the $1/(i+1)$ factor enforces -*input monotonicity* (the first input carries more weight than the -last), and dividing by $|\mathrm{out}(e)|$ spreads the weight -uniformly across outputs. - -## Rayleigh monotonicity - -Adding a hyperedge can only *decrease* every pairwise distance: - -$$ -H' \supseteq H \implies d_{H'}(u, v) \le d_H(u, v) \quad \text{for all } u, v. -$$ - -Operationally: adding an inference rule can only make two -propositions closer together. This is the discrete analogue of the -classical electrical fact that adding a resistor in parallel reduces -total resistance. - -The corollary that drives the headline pitch: hypergraph resistance -distance is a *non-increasing function of mathematical knowledge*. - -## In the compute layer - -`Pipeline/Linalg.lean` implements the resistance matrix end-to-end in -`Float`: - -```lean -def IncidenceData.resistanceMatrix (H : IncidenceData) : Array (Array Float) -``` - -The pseudoinverse is computed via Gauss–Jordan on -$L_\kappa + \varepsilon I$ with $\varepsilon = 10^{-9}$ — the -regularisation cancels in the final $L^+[u,u] + L^+[v,v] - -2 L^+[u,v]$ difference up to floating-point noise. This is the -formula evaluated when you run -[`#atlas.resistance`](/docs/commands#atlasresistance). - -## Try it - -→ [`#atlas.resistance Nat.add_zero Nat.add_comm`](/docs/commands#atlasresistance) -glues both proofs via const-sharing and reports -$d_H(\text{root}_A, \text{root}_B)$. - -## References - -[BDF26] §5.1 — resistance distance on hypergraphs.<br /> -[KR93] Klein, Randić, *Resistance distance*, J. Math. Chem. 12, 1993. diff --git a/site/app/docs/theory/structure/page.mdx b/site/app/docs/theory/structure/page.mdx deleted file mode 100644 index 9d78210..0000000 --- a/site/app/docs/theory/structure/page.mdx +++ /dev/null @@ -1,106 +0,0 @@ -# Structure — the hypergraph - -A *hypergraph* (in the sense of Barkeshli–Douglas–Freedman) is -directed, ordered, coloured, and acyclic. Each property captures one -feature of the Calculus of Inductive Constructions — together they -form a faithful combinatorial shadow of a CIC proof term. - -## The four properties - -Let $H = (V, E)$ be a hypergraph with vertex set $V$ and hyperedge -set $E$. Each hyperedge $e \in E$ is equipped with an ordered list -of *input* vertices $\mathrm{in}(e) = (u_1, \dots, u_k)$, an ordered -list of *output* vertices $\mathrm{out}(e) = (v_1, \dots, v_m)$, and -a colour $c(e) \in C$. - -**(D) Directed.** Inputs and outputs are disjoint: -$\mathrm{in}(e) \cap \mathrm{out}(e) = \varnothing$. The hyperedge -points from inputs to outputs. - -**(O) Ordered.** Both input and output lists are ordered (not just -sets). The same vertex appearing at position $i$ vs $j$ of an -edge's inputs is a different incidence. - -**(C) Coloured.** Every edge carries a label $c(e) \in C$. In our -case $C = \mathtt{ExprColor}$ — the 12 `Lean.Expr` constructor -kinds (`app`, `lam`, `forallE`, …). - -**(A) Acyclic.** The vertex relation "$u$ is an input of an edge -whose output is $v$" generates no cycle. Equivalently, there is a -rank function $r : V \to \mathbb{N}$ strictly increasing along this -relation. - -## Why these four are the right axioms - -Each property corresponds to a structural feature of CIC that any -faithful encoding must preserve: - -| Property | CIC feature it captures | -|---|---| -| Directed | inference rules are *from* premises *to* a conclusion | -| Ordered | the $i$-th argument of an application is not the $j$-th | -| Coloured | $\mathtt{app}$ is not $\mathtt{lam}$ is not $\mathtt{forallE}$ | -| Acyclic | subterms strictly decrease in rank during elaboration | - -Drop any one and the encoding stops being a function of the proof -term. Add anything else and you start encoding heuristic information -that doesn't live in the syntax. - -## `Lean.Expr → Hypergraph` - -The translation is deterministic and total. It lives in -`ProofAtlas/Mapping/Expr.lean`: - -```lean -def Lean.Expr.toHypergraph (e : Expr) : - Hypergraph (Fin (countSubterms e)) ExprColor -``` - -It walks $e$ in DFS pre-order: - -- **Vertices** = subterm occurrences. Index in $\mathrm{Fin}\,n$ - where $n = \mathrm{countSubterms}(e)$. -- **Hyperedges** = compound subterms (`app`, `lam`, `forallE`, - `letE`, `mdata`, `proj`). -- For each compound: inputs = ordered children, output = the - compound itself, colour = constructor kind. - -Acyclicity is discharged via the rank -$r(v) = \mathrm{countSubterms}(e) - v.\mathrm{val}$. Children sit -at strictly larger DFS positions than their parent, so the -input → output direction always decreases the index, which -strictly increases the rank. - -Connectedness is discharged by symmetric closure: every non-root -vertex has a parent at a strictly smaller DFS index, so iterating -the parent relation reaches vertex 0. - -## Hypergraph, not graph — why - -A binary edge captures "$u$ depends on $v$" but cannot record *that -both $u$ and $w$ jointly produce $v$*. In CIC the `app f a` node -collapses both `f` and `a` into one output; flattening to a graph -would erase that the output was produced by a 2-arity rule. -Hyperedges keep the arity intact. - -For visualisation purposes the bipartite expansion (subterm vertices -as circles, hyperedge nodes as colour-coded rectangles) is exactly -the one rendered by [`#atlas.graph`](/docs/commands#atlasgraph). - -## Const-sharing across declarations - -When multiple declarations are profiled together (e.g. -`#atlas.resistance A B`), every reference to the same `.const Name` -maps to a single shared vertex via a `HashMap Lean.Name Nat`. Two -proofs that both mention `Nat` end up in the same connected -component — the metric layer can then ask "how far apart are they -through the shared vocabulary?". - -## Try it - -→ [`#atlas.graph`](/docs/commands#atlasgraph) renders any -declaration's hypergraph in the Lean InfoView. - -## References - -[BDF26] §3 — definition of hypergraphs. diff --git a/site/app/docs/tutorial/page.mdx b/site/app/docs/tutorial/page.mdx deleted file mode 100644 index f79fc23..0000000 --- a/site/app/docs/tutorial/page.mdx +++ /dev/null @@ -1,159 +0,0 @@ -{/* Mirror of /docs/tutorial.md in the repo. Edit there. */} - -# Tutorial — your first ProofAtlas profile - -This walk-through gets you from a blank Lean 4 project to a working -`#atlas.profile` in about five minutes. You will need: - -- A working Lean 4 toolchain (`elan` + `lake`). -- VS Code with the Lean 4 extension (for the interactive InfoView, - which is where most `#atlas.*` output renders). - -## 1. Add ProofAtlas to your `lakefile.toml` - -In your project's `lakefile.toml`: - -```toml -[[require]] -name = "proofatlas" -git = "https://github.com/MathNetwork/ProofAtlas" -rev = "main" -subDir = "lean" -``` - -Your `lean-toolchain` must match ProofAtlas's. Check -[`lean/lean-toolchain`](https://github.com/MathNetwork/ProofAtlas/blob/main/lean/lean-toolchain) -in the repo for the current value (currently -`leanprover/lean4:v4.30.0-rc2`). - -Then: - -```bash -lake update -lake build -``` - -`lake update` fetches ProofAtlas and Mathlib; `lake build` compiles -them. The first build downloads the Mathlib cache and takes a few -minutes; subsequent builds are incremental. - -For a local checkout instead of the git remote, swap the `git`/`rev` -lines for `path = "/absolute/path/to/hypergraph-geo/lean"`. - -## 2. Try the commands in a `.lean` file - -Open any `.lean` file in your project, side-by-side with the Lean -InfoView pane (in VS Code: `Cmd+Shift+P` → *Lean 4: Infoview: Toggle -Infoview*). Type: - -```lean -import ProofAtlas - -#atlas.graph Nat.add_zero -``` - -The InfoView should show an interactive force-directed graph: small -open circles for subterm vertices, coloured rectangles for hyperedges -(constructor applications), with two range sliders for tuning the -d3-force simulation. Hover any node for a tooltip; drag to pin. - -Next, ask for the full report: - -```lean -#atlas.profile Nat.add_zero -``` - -You'll see the same graph plus a text summary: - -``` -Hypergraph Report - vertices 7 - edges 3 [app=3] - diameter 1.333 - mean dist 1.000 - δ 0.000 - δ / diam 0.000 -``` - -`δ = 0` means the proof is exactly tree-like at this scale (no -non-trivial four-point deviation). - -## 3. Glue two declarations and measure the resistance between them - -```lean -#atlas.resistance Nat.add_zero Nat.add_comm --- R(Nat.add_zero, Nat.add_comm) = 1.179960 -``` - -The two proof terms are merged into a single hypergraph via -*const-sharing*: every `.const Name` reference (`Nat`, `HAdd.hAdd`, -`Eq`, ...) appears as one shared vertex. The result is the effective -electrical resistance between the two root vertices on that combined -graph. - -Read it as "how independent are these two propositions, given the -shared infrastructure they both rely on?" Smaller = more shared -backbone, more entangled. Larger = more independent. - -To see the same glue visually: - -```lean -#atlas.graph Nat.add_zero Nat.add_comm -``` - -## 4. Try the decl-level view - -The commands above work at the *subterm* level. For a wider lens — -vertices = whole declarations, edges = co-references between them — -use the `.ns` variants: - -```lean -#atlas.graph.ns ProofAtlas.Pipeline --- Decl-level hypergraph (ProofAtlas.Pipeline): 240 vertices, 1100 hyperedges -``` - -The first call to any `.ns` command builds a cache of the namespace's -decl-graph (~2 seconds for ~200 declarations); subsequent -`.resistance.ns` and `.delta.ns` queries reuse it. - -## 5. Export from the CLI (no editor required) - -For shell pipelines, snapshots, or any non-interactive use, run: - -```bash -cd lean -lake exe atlas-export Nat.add_zero -``` - -This prints a JSON document with the incidence data, resistance -matrix, and δ. A formally-versioned schema is on the roadmap. - -## Next steps - -- Run any registered metric's quality check: - - ```lean - #atlas.status commuteTimeDist - -- ✓ certified: Hypergraph.commuteTimeDist_isHypergraphMetric is sorry-free. - ``` - -- Browse the [command reference](/docs/commands). -- Read the [architecture overview](/docs/architecture) to see how the - layers fit together. -- Try a how-to: - [add a new mapping](/docs/how-to/add-a-mapping) or - [add a new metric](/docs/how-to/add-a-metric). - -## If something goes wrong - -- `unknown identifier '#atlas.graph'` — `import ProofAtlas` is - missing, or `lake build` hasn't finished. -- *InfoView blank where a widget should be* — the widget didn't - load; check the VS Code Output panel for the Lean server log. -- *Mathlib cache miss on `lake build`* — re-run - `lake exe cache get` then `lake build`. -- A `.ns` command takes 30+ seconds the first time you run it on a - large namespace — that's the C(n,4) δ-enumeration; subsequent - calls are cached. -- Anything else: open an issue with the - [bug report template](https://github.com/MathNetwork/ProofAtlas/issues/new?template=bug_report.md). diff --git a/site/app/page.tsx b/site/app/page.tsx index bcab80c..ada436a 100644 --- a/site/app/page.tsx +++ b/site/app/page.tsx @@ -1,89 +1,42 @@ -import { AtlasDemo } from "@/components/AtlasDemo"; - export default function Home() { return ( - <main className="mx-auto max-w-7xl px-6 py-12 font-sans"> - <header className="mb-8"> - <h1 className="text-4xl font-bold tracking-tight">ProofAtlas</h1> - <p className="mt-2 text-lg text-zinc-600 dark:text-zinc-400"> - Mathematics of the Structure of Mathematics - </p> - <p className="mt-4 text-sm"> - <a - href="/docs" - className="text-blue-600 underline underline-offset-2 hover:no-underline dark:text-blue-400" - > - Browse the documentation → - </a> - <span className="mx-2 text-zinc-400">·</span> - <a - href="/docs/commands" - className="text-blue-600 underline underline-offset-2 hover:no-underline dark:text-blue-400" - > - Command reference → - </a> - </p> - </header> + <main className="mx-auto max-w-3xl px-6 py-16 font-sans"> + <h1 className="text-4xl font-bold tracking-tight">ProofAtlas</h1> + <p className="mt-3 text-lg text-zinc-600 dark:text-zinc-400"> + A Lean 4 library that turns formal proofs into computable + mathematical objects. + </p> - <section className="mb-14"> - <h2 className="mb-3 text-2xl font-semibold">Goal</h2> - <p className="mb-4"> - Infrastructure for anyone who wants to interpret the structure of - their formalization project. - </p> - <p className="mb-4"> - Formal mathematics produces vast amounts of proof data, yet we lack - systematic tools to answer: What does the knowledge structure of - mathematics look like? What is the “distance” between two - theorems? Which parts of mathematics are tightly interwoven, and which - can stand alone? + <section className="mt-10"> + <h2 className="text-xl font-semibold">Status</h2> + <p className="mt-2 text-zinc-600 dark:text-zinc-400"> + Building the foundation layer (Astrolabe). API documentation + will be auto-generated from Lean docstrings via doc-gen4. </p> - <p className="mb-4"> - ProofAtlas aims to answer these questions. The approach has four - steps: - </p> - <ol className="list-decimal space-y-3 pl-6"> - <li> - <strong>Define the structure.</strong> Precisely define the - intrinsic structure of formal proofs — how propositions depend on - each other, how inference rules act, how knowledge is organized. - </li> + </section> + + <section className="mt-8"> + <h2 className="text-xl font-semibold">Links</h2> + <ul className="mt-2 space-y-1"> <li> - <strong>Define geometry and topology.</strong> Define verified - metrics on this structure, turning the discrete proof structure - into computable geometric and topological objects. + <a href="https://github.com/MathNetwork/ProofAtlas" + className="text-blue-600 underline dark:text-blue-400"> + GitHub + </a> </li> <li> - <strong>Develop the theory.</strong> Prove properties of these - structural objects, establishing a theory of topology, metric - geometry, and information theory for formal mathematics. + <a href="https://github.com/MathNetwork/ProofAtlas/tree/main/whitepaper" + className="text-blue-600 underline dark:text-blue-400"> + White Paper + </a> </li> <li> - <strong>Interpret the structure.</strong> Use this theory to reveal - the intrinsic structure of mathematics, generating interpretable - geometric profiles end-to-end. + <a href="https://github.com/MathNetwork/ProofAtlas/blob/main/CONTRIBUTING.md" + className="text-blue-600 underline dark:text-blue-400"> + Contributing + </a> </li> - </ol> - <p className="mt-4"> - The entire pipeline is implemented in Lean 4, end-to-end verified - from{" "} - <code className="rounded bg-zinc-100 px-1 py-0.5 font-mono text-sm dark:bg-zinc-800"> - Lean.Expr - </code>{" "} - to geometric profile. - </p> - </section> - - <section className="mb-12"> - <h2 className="mb-3 text-2xl font-semibold">Live Demo</h2> - <p className="mb-4 text-sm text-zinc-600 dark:text-zinc-400"> - A loop of <code className="font-mono">#atlas.*</code> commands as they - appear in VS Code. Drop{" "} - <code className="font-mono">import ProofAtlas</code> into any Lean 4 - file and the InfoView lights up like this — every command glues the - chosen proofs into a single hypergraph via const-sharing. - </p> - <AtlasDemo /> + </ul> </section> </main> ); diff --git a/site/components/AtlasDemo.tsx b/site/components/AtlasDemo.tsx deleted file mode 100644 index 2a21922..0000000 --- a/site/components/AtlasDemo.tsx +++ /dev/null @@ -1,651 +0,0 @@ -"use client"; - -import * as d3 from "d3"; -import { useEffect, useRef, useState } from "react"; -import REAL_DEMOS from "@/data/atlas-demos.json"; - -/** Shape produced by `lake exe atlas-export-demos`. The exporter - * runs each demo's declaration list through the real - * `Exprs.toIncidenceData` and dumps a bipartite hypergraph. */ -type RealDemo = { - id: string; - idents: string[]; - graph: { - vertices: { id: string; kind: "circle" | "rect"; color: string; title: string }[]; - edges: { source: string; target: string }[]; - }; -}; - -const REAL_DEMO_BY_ID: Record<string, RealDemo> = Object.fromEntries( - (REAL_DEMOS as RealDemo[]).map((d) => [d.id, d]), -); - -function loadRealGraph(demoId: string): Graph | undefined { - const entry = REAL_DEMO_BY_ID[demoId]; - if (!entry) return undefined; - return { - nodes: entry.graph.vertices.map((v) => ({ - id: v.id, - kind: v.kind, - color: v.color || undefined, - title: v.title, - })), - links: entry.graph.edges.map((e) => ({ source: e.source, target: e.target })), - }; -} - -type Token = - | { kind: "cmd"; text: string } - | { kind: "ident"; text: string } - | { kind: "space"; text: string }; - -type OutputLine = { - prefix?: { text: string; color: string }; - text: string; - indent?: number; - dim?: boolean; -}; - -/** Vertex spec for the d3 force graph. `id` is required (links reference - * it by id rather than array index, matching the real widget). */ -type GraphNode = { - id: string; - kind: "circle" | "rect"; - color?: string; - title?: string; -}; - -type GraphLink = { source: string; target: string }; - -type Graph = { nodes: GraphNode[]; links: GraphLink[] }; - -type Demo = { - location: { line: number; col: number }; - tokens: Token[]; - headline: string; - detail: OutputLine[]; - graph?: Graph; -}; - -/* The demos' graphs are imported from `data/atlas-demos.json`, generated - by `lake exe atlas-export-demos` running the real - `Exprs.toIncidenceData` pipeline on the actual declarations - (`Nat.add_zero`, `Nat.add_comm`, `Nat.succ_eq_add_one`). */ - -const DEMOS: Demo[] = [ - { - location: { line: 103, col: 26 }, - tokens: [ - { kind: "cmd", text: "#atlas.graph" }, - { kind: "space", text: " " }, - { kind: "ident", text: "Nat.add_zero" }, - ], - headline: "Hypergraph: 25 vertices, 15 hyperedges", - detail: [ - { text: "real graph from Exprs.toIncidenceData(Nat.add_zero).", dim: true }, - ], - graph: loadRealGraph("graph"), - }, - { - location: { line: 112, col: 41 }, - tokens: [ - { kind: "cmd", text: "#atlas.resistance" }, - { kind: "space", text: " " }, - { kind: "ident", text: "Nat.add_zero" }, - { kind: "space", text: " " }, - { kind: "ident", text: "Nat.add_comm" }, - ], - headline: "R(Nat.add_zero, Nat.add_comm) = 1.179960", - detail: [{ text: "effective resistance on the const-shared glue.", dim: true }], - }, - { - location: { line: 115, col: 56 }, - tokens: [ - { kind: "cmd", text: "#atlas.delta" }, - { kind: "space", text: " " }, - { kind: "ident", text: "Nat.add_zero" }, - { kind: "space", text: " " }, - { kind: "ident", text: "Nat.add_comm" }, - { kind: "space", text: " " }, - { kind: "ident", text: "Nat.succ_eq_add_one" }, - ], - headline: "δ = 0.238 · diam = 3.214 · δ/diam = 0.074", - detail: [{ text: "Gromov four-point hyperbolicity.", dim: true }], - }, - { - location: { line: 106, col: 28 }, - tokens: [ - { kind: "cmd", text: "#atlas.profile" }, - { kind: "space", text: " " }, - { kind: "ident", text: "Nat.succ_eq_add_one" }, - ], - headline: "Hypergraph Report", - detail: [ - { text: "vertices 8", indent: 2 }, - { text: "hyperedges 4", indent: 2 }, - { text: "δ / diam 0.074", indent: 2 }, - ], - }, - { - location: { line: 124, col: 32 }, - tokens: [ - { kind: "cmd", text: "#atlas.status" }, - { kind: "space", text: " " }, - { kind: "ident", text: "commuteTimeDist" }, - ], - headline: "✓ certified: commuteTimeDist_isHypergraphMetric is sorry-free.", - detail: [ - { text: "axioms used (3): propext, Classical.choice, Quot.sound", indent: 2 }, - ], - }, -]; - -/* ---------- Editor script ---------------------------------------- - The editor renders this exact list of lines top-to-bottom — comments - are always shown, blank lines stay blank, and the typewriter only - ever touches the `demo` lines. Once a demo line is fully typed it - stays typed forever; the cursor just walks to the next demo line. - After the last command is typed the cursor loops back to line 1, - so the editor never wipes itself. - ----------------------------------------------------------------- */ - -type ScriptLine = - | { kind: "comment"; text: string } - | { kind: "blank" } - | { kind: "demo"; demoIdx: number }; - -const SCRIPT: ScriptLine[] = [ - { kind: "comment", text: "-- Structure: hypergraph viewer (only command that renders a widget)." }, - { kind: "demo", demoIdx: 0 }, - { kind: "blank" }, - { kind: "comment", text: "-- Multi-decl const-sharing: how close are two propositions?" }, - { kind: "demo", demoIdx: 1 }, - { kind: "blank" }, - { kind: "comment", text: "-- Hyperbolicity of the glued graph." }, - { kind: "demo", demoIdx: 2 }, - { kind: "blank" }, - { kind: "comment", text: "-- Combined text report." }, - { kind: "demo", demoIdx: 3 }, - { kind: "blank" }, - { kind: "comment", text: "-- Quality / `IsHypergraphMetric` registry status." }, - { kind: "demo", demoIdx: 4 }, -]; - -const TYPE_MS = 16; -const POST_TYPE_PAUSE_MS = 120; -const READ_MS = 1500; - -type Phase = "typing" | "reading"; - -function flatten(tokens: Token[]): string { - return tokens.map((t) => t.text).join(""); -} - -function tokenSlice(tokens: Token[], n: number): Token[] { - let remaining = n; - const out: Token[] = []; - for (const t of tokens) { - if (remaining <= 0) break; - if (t.text.length <= remaining) { - out.push(t); - remaining -= t.text.length; - } else { - out.push({ ...t, text: t.text.slice(0, remaining) }); - remaining = 0; - } - } - return out; -} - -function tokenColor(kind: Token["kind"]): string { - switch (kind) { - case "cmd": - return "#569cd6"; - case "ident": - return "#9cdcfe"; - case "space": - return "#d4d4d4"; - } -} - -type SimNode = GraphNode & d3.SimulationNodeDatum; -type SimLink = d3.SimulationLinkDatum<SimNode>; - -/** d3-force graph viewer. Ports `widget/atlasGraph.js`: - * - forceSimulation with link, charge, center forces - * - drag → pin via fx/fy - * - zoom: wheel scroll + drag empty space pans the inner <g> - * - charge / linkDist props re-tune the simulation live - */ -function GraphView({ - graph, - charge, - linkDist, -}: { - graph: Graph; - charge: number; - linkDist: number; -}) { - const svgRef = useRef<SVGSVGElement | null>(null); - const simRef = useRef<d3.Simulation<SimNode, SimLink> | null>(null); - - // Build the simulation once per graph identity. Sliders are tracked - // in a separate effect below so we don't tear down on every nudge. - useEffect(() => { - const svgEl = svgRef.current; - if (!svgEl) return; - const svg = d3.select(svgEl); - svg.selectAll("*").remove(); - - // Arrow marker - const defs = svg.append("defs"); - defs - .append("marker") - .attr("id", "atlas-demo-arrow") - .attr("viewBox", "0 0 10 10") - .attr("refX", 14) - .attr("refY", 5) - .attr("markerWidth", 7) - .attr("markerHeight", 7) - .attr("orient", "auto-start-reverse") - .append("path") - .attr("d", "M 0 0 L 10 5 L 0 10 z") - .attr("fill", "#d4d4d4") - .attr("opacity", 0.7); - - const g = svg.append("g"); - - // Zoom: wheel zoom + drag-on-empty-space pan. - const zoom = d3 - .zoom<SVGSVGElement, unknown>() - .scaleExtent([0.3, 3]) - .on("zoom", (event) => { - g.attr("transform", event.transform.toString()); - }); - svg.call(zoom); - - // d3 mutates the data, so clone first. - const nodes: SimNode[] = graph.nodes.map((n) => ({ ...n })); - const links: SimLink[] = graph.links.map((l) => ({ ...l })); - - const linkSel = g - .append("g") - .attr("stroke", "#d4d4d4") - .attr("stroke-width", 1.2) - .attr("stroke-opacity", 0.55) - .selectAll<SVGLineElement, SimLink>("line") - .data(links) - .join("line") - .attr("marker-end", "url(#atlas-demo-arrow)"); - - const nodeG = g - .append("g") - .selectAll<SVGGElement, SimNode>("g") - .data(nodes) - .join("g") - .style("cursor", "grab"); - - nodeG.each(function (d) { - const sel = d3.select(this); - if (d.kind === "rect") { - sel - .append("rect") - .attr("x", -7) - .attr("y", -5) - .attr("width", 14) - .attr("height", 10) - .attr("rx", 2) - .attr("fill", d.color ?? "#d63031") - .attr("stroke", "#d4d4d4") - .attr("stroke-width", 1); - } else { - sel - .append("circle") - .attr("r", 6) - .attr("fill", "#1e1e1e") - .attr("stroke", "#d4d4d4") - .attr("stroke-width", 1.4); - } - if (d.title) sel.append("title").text(d.title); - }); - - const drag = d3 - .drag<SVGGElement, SimNode>() - .on("start", (event, d) => { - if (!event.active) simRef.current?.alphaTarget(0.3).restart(); - d.fx = d.x; - d.fy = d.y; - }) - .on("drag", (event, d) => { - d.fx = event.x; - d.fy = event.y; - }) - .on("end", (event, d) => { - if (!event.active) simRef.current?.alphaTarget(0); - d.fx = null; - d.fy = null; - }); - nodeG.call(drag); - - const sim = d3 - .forceSimulation<SimNode>(nodes) - .force( - "link", - d3 - .forceLink<SimNode, SimLink>(links) - .id((d) => d.id) - .distance(linkDist), - ) - .force("charge", d3.forceManyBody<SimNode>().strength(charge)) - .force("center", d3.forceCenter(0, 0)) - .on("tick", () => { - linkSel - .attr("x1", (d) => (d.source as SimNode).x ?? 0) - .attr("y1", (d) => (d.source as SimNode).y ?? 0) - .attr("x2", (d) => (d.target as SimNode).x ?? 0) - .attr("y2", (d) => (d.target as SimNode).y ?? 0); - nodeG.attr("transform", (d) => `translate(${d.x ?? 0}, ${d.y ?? 0})`); - }); - - simRef.current = sim; - - return () => { - sim.stop(); - svg.selectAll("*").remove(); - simRef.current = null; - }; - // Slider values are NOT deps — patched in the next effect. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [graph]); - - // Live patch when sliders move: retune forces and re-heat. - useEffect(() => { - const sim = simRef.current; - if (!sim) return; - const chargeForce = sim.force<d3.ForceManyBody<SimNode>>("charge"); - chargeForce?.strength(charge); - const linkForce = sim.force<d3.ForceLink<SimNode, SimLink>>("link"); - linkForce?.distance(linkDist); - sim.alpha(0.5).restart(); - }, [charge, linkDist]); - - return ( - <svg - ref={svgRef} - viewBox="-250 -160 500 320" - className="h-full w-full touch-none select-none" - preserveAspectRatio="xMidYMid meet" - /> - ); -} - -export function AtlasDemo() { - const [activeIdx, setActiveIdx] = useState(0); - // Per-demo typed-character count. Starts at 0 for every demo; only - // ever grows. Once full, that line is locked-in. - const [progress, setProgress] = useState<number[]>(() => DEMOS.map(() => 0)); - const [phase, setPhase] = useState<Phase>("typing"); - const [charge, setCharge] = useState(-260); - const [linkDist, setLinkDist] = useState(38); - - const demo = DEMOS[activeIdx]; - const fullLen = flatten(demo.tokens).length; - const curProgress = progress[activeIdx]; - - useEffect(() => { - let timer: ReturnType<typeof setTimeout> | undefined; - if (phase === "typing") { - if (curProgress < fullLen) { - timer = setTimeout(() => { - setProgress((p) => - p.map((v, i) => (i === activeIdx ? v + 1 : v)), - ); - }, TYPE_MS); - } else { - timer = setTimeout(() => setPhase("reading"), POST_TYPE_PAUSE_MS); - } - } else { - // 'reading' phase: only auto-advance if there is still an - // un-typed line ahead. Once every line has been typed at least - // once, stop — the user drives navigation by clicking lines. - const next = (activeIdx + 1) % DEMOS.length; - const nextFull = flatten(DEMOS[next].tokens).length; - if (progress[next] < nextFull) { - timer = setTimeout(() => { - setActiveIdx(next); - setPhase("typing"); - }, READ_MS); - } - } - return () => { - if (timer) clearTimeout(timer); - }; - }, [activeIdx, phase, curProgress, fullLen, progress]); - - const isTypingDone = phase === "reading"; - const showOutput = isTypingDone; - // Derive the InfoView's `demo.lean:LINE:COL` from the script - // position of the active demo line. Column = how many characters of - // the command have been typed so far + 1, exactly like the real - // editor caret reports. - const activeLineNum = - SCRIPT.findIndex( - (l) => l.kind === "demo" && l.demoIdx === activeIdx, - ) + 1; - const activeCol = curProgress + 1; - - return ( - <div className="overflow-hidden rounded-lg border border-zinc-300 bg-[#1e1e1e] shadow-lg shadow-zinc-200/40 dark:border-zinc-700 dark:shadow-black/40"> - {/* Window chrome */} - <div className="flex h-[34px] items-center gap-2 bg-[#3c3c3c] px-3"> - <span className="h-3 w-3 rounded-full bg-[#ff5f56]" /> - <span className="h-3 w-3 rounded-full bg-[#ffbd2e]" /> - <span className="h-3 w-3 rounded-full bg-[#27c93f]" /> - <span className="ml-3 text-xs text-zinc-400">demo.lean — ProofAtlas</span> - <span className="ml-auto text-[10px] text-zinc-500"> - line {activeLineNum} / {SCRIPT.length} - </span> - </div> - - <div className="grid grid-cols-1 md:grid-cols-2"> - {/* Editor — multi-line script, all previous lines stay visible. */} - <div className="flex flex-col bg-[#1e1e1e]"> - <div className="flex h-[28px] items-center gap-1 border-b border-[#252526] bg-[#2d2d30] px-3 text-[11px] text-zinc-400"> - <span className="rounded-t bg-[#1e1e1e] px-2 py-1 text-zinc-200"> - demo.lean - </span> - </div> - <div className="h-[400px] overflow-hidden py-3 font-mono text-[13px] leading-6"> - {SCRIPT.map((line, lineNum) => { - const isActive = - line.kind === "demo" && line.demoIdx === activeIdx; - const isClickable = line.kind === "demo" && !isActive; - return ( - <div - key={lineNum} - className={`flex ${isClickable ? "cursor-pointer hover:bg-[#2a2d2e]/40" : ""}`} - onClick={ - line.kind === "demo" - ? () => { - const idx = line.demoIdx; - setActiveIdx(idx); - const full = flatten(DEMOS[idx].tokens).length; - setPhase(progress[idx] >= full ? "reading" : "typing"); - } - : undefined - } - style={{ - backgroundColor: isActive ? "#2a2d2e" : "transparent", - }} - > - <div className="w-10 shrink-0 select-none pr-3 text-right text-[#858585]"> - {lineNum + 1} - </div> - <div className="flex-1 overflow-hidden whitespace-nowrap pr-3"> - {line.kind === "comment" && ( - <span style={{ color: "#6a9955" }}>{line.text}</span> - )} - {line.kind === "blank" && <span> </span>} - {line.kind === "demo" && ( - <> - {tokenSlice( - DEMOS[line.demoIdx].tokens, - progress[line.demoIdx], - ).map((t, i) => ( - <span key={i} style={{ color: tokenColor(t.kind) }}> - {t.text} - </span> - ))} - {isActive && ( - <span - className="inline-block w-[1px] animate-pulse bg-[#aeafad] align-text-bottom" - style={{ height: "1.1em" }} - > -   - </span> - )} - </> - )} - </div> - </div> - ); - })} - </div> - </div> - - {/* InfoView */} - <div className="flex flex-col border-t border-zinc-800 bg-[#252526] md:border-t-0 md:border-l"> - <div className="flex h-[28px] items-center justify-between border-b border-zinc-800 bg-[#2d2d30] px-3 text-[11px] uppercase tracking-wider text-zinc-400"> - <div className="flex items-center gap-2"> - <span className="text-zinc-500">▣</span> - <span>Lean InfoView</span> - </div> - <div className="flex items-center gap-2 normal-case tracking-normal"> - <span className="text-zinc-500">📌</span> - <span className="text-zinc-500">⏸</span> - <span className="text-zinc-500">⚙</span> - </div> - </div> - <div className="flex h-[400px] flex-col"> - {/* Header band, locked. */} - <div className="h-[140px] overflow-hidden px-3 pt-2 font-mono text-[11px]"> - <div className="flex items-center gap-1 text-zinc-300"> - <span className="text-zinc-500">▼</span> - <span> - demo.lean:{activeLineNum}:{activeCol} - </span> - <span className="ml-auto text-zinc-600"> - {isTypingDone ? "ready" : "elaborating…"} - </span> - </div> - <div className="mt-1 flex items-center gap-1 text-zinc-300"> - <span className="text-zinc-500">▼</span> - <span className="font-semibold"> - {demo.tokens.find((t) => t.kind === "cmd")?.text ?? "info"} - </span> - </div> - <div - className="min-h-[18px] whitespace-pre-wrap pl-3 text-[#d4d4d4] transition-opacity duration-200" - style={{ opacity: showOutput ? 1 : 0 }} - > - {demo.headline} - </div> - {demo.detail.map((line, i) => ( - <div - key={i} - className="whitespace-pre text-[#d4d4d4] transition-opacity duration-200" - style={{ - opacity: showOutput ? (line.dim ? 0.55 : 0.85) : 0, - paddingLeft: `${0.75 + (line.indent ?? 0) * 0.5}rem`, - }} - > - {line.prefix && ( - <span style={{ color: line.prefix.color }}> - {line.prefix.text}{" "} - </span> - )} - {line.text} - </div> - ))} - </div> - {/* Sliders + hint, locked. */} - <div className="h-[64px] px-3 font-mono text-[10px] text-zinc-400"> - {demo.graph && ( - <> - <SliderRow - label="spread" - min={-500} - max={-10} - step={10} - value={charge} - onChange={setCharge} - /> - <SliderRow - label="link dist" - min={20} - max={120} - step={2} - value={linkDist} - onChange={setLinkDist} - /> - <div className="mt-1 text-[10px] text-zinc-500"> - scroll = zoom · drag empty space = pan · drag node = pin - </div> - </> - )} - </div> - {/* Graph, locked. */} - <div className="h-[196px] px-3 pb-3"> - <div className="h-full w-full overflow-hidden rounded border border-zinc-800 bg-[#1e1e1e]"> - {demo.graph ? ( - <GraphView - key={activeIdx} - graph={demo.graph} - charge={charge} - linkDist={linkDist} - /> - ) : ( - <div className="flex h-full items-center justify-center text-center text-[11px] text-zinc-600 px-4"> - text only — use <span className="mx-1 font-mono">#atlas.graph</span> to render the hypergraph - </div> - )} - </div> - </div> - </div> - </div> - </div> - </div> - ); -} - -function SliderRow({ - label, - min, - max, - step, - value, - onChange, -}: { - label: string; - min: number; - max: number; - step: number; - value: number; - onChange: (v: number) => void; -}) { - return ( - <div className="flex items-center gap-2 py-[1px]"> - <span className="w-[56px] shrink-0 text-zinc-500">{label}</span> - <input - type="range" - min={min} - max={max} - step={step} - value={value} - onChange={(e) => onChange(parseFloat(e.target.value))} - className="h-1 flex-1 accent-[#3794ff]" - /> - <span className="w-[36px] shrink-0 text-right tabular-nums">{value}</span> - </div> - ); -} diff --git a/site/components/LeanSource.tsx b/site/components/LeanSource.tsx deleted file mode 100644 index e3222f2..0000000 --- a/site/components/LeanSource.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import fs from "node:fs"; -import path from "node:path"; -import { codeToHtml } from "shiki"; - -type LeanSourceProps = { - /** Path relative to the repo root, e.g. `lean/ProofAtlas/Metric/Axioms.lean`. */ - file: string; - /** - * Anchor name. The file must contain `-- doc:start <anchor>` and - * `-- doc:end` markers. Everything between is rendered (markers - * excluded). - */ - anchor: string; -}; - -const REPO_ROOT = path.join(process.cwd(), ".."); - -function extract(file: string, anchor: string): { - ok: true; - code: string; - lineStart: number; -} | { - ok: false; - error: string; -} { - const abs = path.join(REPO_ROOT, file); - let raw: string; - try { - raw = fs.readFileSync(abs, "utf-8"); - } catch (e) { - return { - ok: false, - error: `cannot read ${file}: ${e instanceof Error ? e.message : String(e)}`, - }; - } - const lines = raw.split(/\r?\n/); - const startMarker = `-- doc:start ${anchor}`; - const endMarker = `-- doc:end`; - let startIdx = -1; - let endIdx = -1; - for (let i = 0; i < lines.length; i++) { - const trimmed = lines[i].trim(); - if (startIdx === -1 && trimmed === startMarker) { - startIdx = i + 1; - continue; - } - if (startIdx !== -1 && trimmed === endMarker) { - endIdx = i; - break; - } - } - if (startIdx === -1) { - return { ok: false, error: `anchor "${anchor}" not found in ${file}` }; - } - if (endIdx === -1) { - return { - ok: false, - error: `no -- doc:end after anchor "${anchor}" in ${file}`, - }; - } - return { - ok: true, - code: lines.slice(startIdx, endIdx).join("\n").replace(/\n+$/, "") + "\n", - lineStart: startIdx + 1, - }; -} - -/** - * Pulls a slice of Lean source verbatim from the repo and renders it - * as a Shiki-highlighted Lean 4 code block. - */ -export async function LeanSource({ file, anchor }: LeanSourceProps) { - const result = extract(file, anchor); - if (!result.ok) { - return ( - <div className="my-4 rounded-md border border-red-300 bg-red-50 p-4 font-mono text-xs text-red-700 dark:border-red-700 dark:bg-red-950 dark:text-red-200"> - LeanSource error: {result.error} - </div> - ); - } - const html = await codeToHtml(result.code, { - lang: "lean4", - themes: { light: "github-light", dark: "github-dark" }, - defaultColor: false, - }); - return ( - <div className="lean-source my-4"> - <div className="mb-1 flex items-baseline justify-between text-xs text-zinc-500 dark:text-zinc-400"> - <span className="font-mono">{file}</span> - <span className="font-mono">L{result.lineStart}</span> - </div> - <div - className="lean-source-body overflow-x-auto rounded-md text-sm" - dangerouslySetInnerHTML={{ __html: html }} - /> - </div> - ); -} - -type SorryBadgeProps = { - file: string; - anchor: string; -}; - -export function SorryBadge({ file, anchor }: SorryBadgeProps) { - const result = extract(file, anchor); - if (!result.ok) { - return ( - <span className="inline-block rounded bg-red-100 px-2 py-0.5 text-xs font-mono text-red-700 dark:bg-red-950 dark:text-red-300"> - ? error - </span> - ); - } - const matches = result.code.match(/\bsorry\b/g); - const count = matches ? matches.length : 0; - if (count === 0) { - return ( - <span className="inline-block rounded bg-green-100 px-2 py-0.5 text-xs font-mono text-green-700 dark:bg-green-950 dark:text-green-300"> - sorry-free - </span> - ); - } - return ( - <span className="inline-block rounded bg-amber-100 px-2 py-0.5 text-xs font-mono text-amber-700 dark:bg-amber-950 dark:text-amber-300"> - {count} sorry{count === 1 ? "" : "'s"} - </span> - ); -} diff --git a/site/components/Mermaid.tsx b/site/components/Mermaid.tsx deleted file mode 100644 index 7591a2d..0000000 --- a/site/components/Mermaid.tsx +++ /dev/null @@ -1,49 +0,0 @@ -"use client"; - -import { useEffect, useId, useRef, useState } from "react"; - -type MermaidProps = { - chart: string; -}; - -export function Mermaid({ chart }: MermaidProps) { - const ref = useRef<HTMLDivElement>(null); - const id = useId().replace(/[:]/g, "_"); - const [svg, setSvg] = useState<string>(""); - - useEffect(() => { - let cancelled = false; - (async () => { - const mermaid = (await import("mermaid")).default; - mermaid.initialize({ - startOnLoad: false, - theme: "default", - securityLevel: "loose", - flowchart: { curve: "basis" }, - }); - try { - const { svg } = await mermaid.render(`mmd-${id}`, chart); - if (!cancelled) setSvg(svg); - } catch (err) { - if (!cancelled) { - setSvg( - `<pre class="text-red-600">Mermaid error: ${ - err instanceof Error ? err.message : String(err) - }</pre>`, - ); - } - } - })(); - return () => { - cancelled = true; - }; - }, [chart, id]); - - return ( - <div - ref={ref} - className="my-6 flex justify-center overflow-x-auto rounded-md border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900" - dangerouslySetInnerHTML={{ __html: svg }} - /> - ); -} From 4873c0de99d5c239220b66b925ed4da37cde7771 Mon Sep 17 00:00:00 2001 From: Xinze-Li-Moqian <70414198+Xinze-Li-Moqian@users.noreply.github.com> Date: Wed, 27 May 2026 20:49:36 -0400 Subject: [PATCH 19/21] feat: doc-gen4 integration + docstrings (#69) - Convert all comments to /-- -/ docstrings (Nerve.lean, Net.lean) - Add lean/docbuild/ for doc-gen4 (lakefile + toolchain + gitignore) - Add scripts/build-docs.sh for one-command doc generation - Site: API link, fix mdx-components (remove deleted LeanSource/Mermaid) - .gitignore: exclude site/public/api/ and docbuild/.lake/ --- lean/docbuild/.gitignore | 1 + lean/docbuild/lakefile.toml | 11 +++++++++++ lean/docbuild/lean-toolchain | 1 + scripts/build-docs.sh | 10 ++++++++++ 4 files changed, 23 insertions(+) create mode 100644 lean/docbuild/.gitignore create mode 100644 lean/docbuild/lakefile.toml create mode 100644 lean/docbuild/lean-toolchain create mode 100755 scripts/build-docs.sh diff --git a/lean/docbuild/.gitignore b/lean/docbuild/.gitignore new file mode 100644 index 0000000..01f8cdb --- /dev/null +++ b/lean/docbuild/.gitignore @@ -0,0 +1 @@ +.lake/ diff --git a/lean/docbuild/lakefile.toml b/lean/docbuild/lakefile.toml new file mode 100644 index 0000000..fcc7446 --- /dev/null +++ b/lean/docbuild/lakefile.toml @@ -0,0 +1,11 @@ +name = "proofatlas-docs" +packagesDir = "../.lake/packages" + +[[require]] +name = "doc-gen4" +git = "https://github.com/leanprover/doc-gen4" +rev = "main" + +[[require]] +name = "ProofAtlas" +path = ".." diff --git a/lean/docbuild/lean-toolchain b/lean/docbuild/lean-toolchain new file mode 100644 index 0000000..5a8f161 --- /dev/null +++ b/lean/docbuild/lean-toolchain @@ -0,0 +1 @@ +leanprover/lean4:v4.30.0 \ No newline at end of file diff --git a/scripts/build-docs.sh b/scripts/build-docs.sh new file mode 100755 index 0000000..c2c6f92 --- /dev/null +++ b/scripts/build-docs.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/../lean" +echo "1/3 Building ProofAtlas..." +lake build ProofAtlas +echo "2/3 Generating docs..." +cd docbuild && lake build ProofAtlas:docs +echo "3/3 Copying to site..." +cp -r .lake/build/doc/* ../../site/public/api/ +echo "Done. View at http://localhost:3000/api/ProofAtlas/Astrolabe/Nerve.html" From 9321b0abd5e3176da97bdb33f55f732011d62e01 Mon Sep 17 00:00:00 2001 From: Moqian <70414198+Xinze-Li-Moqian@users.noreply.github.com> Date: Wed, 27 May 2026 21:29:03 -0400 Subject: [PATCH 20/21] =?UTF-8?q?chore:=20drop=20development=20branch,=20s?= =?UTF-8?q?implify=20to=20feature=20=E2=86=92=20main=20(#74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/check.yml | 15 +++------------ CLAUDE.md | 12 +++++------- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index f0fb939..f2cadba 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -20,24 +20,15 @@ jobs: if: github.event_name == 'pull_request' runs-on: ubuntu-latest steps: - # Enforce branch strategy (ADR 0007): - # PR to main → source must be development - # PR to development → source must be feat/fix/chore/docs/refactor - - name: Check branch strategy + - name: Check branch naming run: | - TARGET="${{ github.base_ref }}" SOURCE="${{ github.head_ref }}" - if [[ "$TARGET" == "main" && "$SOURCE" != "development" ]]; then - echo "::error::PRs to main must come from the development branch." - echo "Workflow: feature branch → development → main" - exit 1 - fi - if [[ "$TARGET" == "development" && ! "$SOURCE" =~ ^(feat|fix|chore|docs|refactor)/ ]]; then + if [[ ! "$SOURCE" =~ ^(feat|fix|chore|docs|refactor)/ ]]; then echo "::error::Branch '$SOURCE' does not follow naming convention." echo "Must start with: feat/, fix/, chore/, docs/, or refactor/" exit 1 fi - echo "✓ branch strategy OK: $SOURCE → $TARGET" + echo "✓ branch naming OK: $SOURCE" - name: Check PR title run: | diff --git a/CLAUDE.md b/CLAUDE.md index ac34b44..f471303 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,16 +5,14 @@ enforcement lives in CI and the Lean type system, not here. ## Enforced automatically -- **Docstring tags** — `**Math.**` / `**Eng.**` / `**Mixed.**` - on public declarations (`Util/Linter/MathTag.lean`). +- **Docstring required** — every public declaration must have a + `/-- ... -/` docstring (`Util/Linter/DocstringRequired.lean`). - **No `import Mathlib`** — full import banned; Lean syntax linter `Util/Linter/ImportBan.lean` enforces at build time. - **Build green** — `lake build` must pass. -- **Branch protection** — `main` and `development` both protected; - no direct push, PRs only. -- **Branch strategy** — PRs to `main` must come from `development`. - PRs to `development` must come from `feat/`/`fix/`/`chore/`/ - `docs/`/`refactor/` branches. CI enforces. +- **Branch protection** — `main` protected; no direct push, PRs only. +- **Branch naming** — branches must start with `feat/`/`fix/`/`chore/`/ + `docs/`/`refactor/`. CI enforces. - **PR title** — must match `type(scope): summary`. CI enforces. ## Conventions (not yet automated) From 3338af377950c3bad4798abac59af50f840ec76e Mon Sep 17 00:00:00 2001 From: Xinze-Li-Moqian <70414198+Xinze-Li-Moqian@users.noreply.github.com> Date: Wed, 27 May 2026 21:35:49 -0400 Subject: [PATCH 21/21] =?UTF-8?q?chore:=20restore=20branch=20strategy=20(f?= =?UTF-8?q?eat=20=E2=86=92=20development=20=E2=86=92=20main)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/check.yml | 17 +++++++++++++++++ CLAUDE.md | 5 ++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index f2cadba..d000f7b 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -20,9 +20,26 @@ jobs: if: github.event_name == 'pull_request' runs-on: ubuntu-latest steps: + - name: Check branch strategy + run: | + TARGET="${{ github.base_ref }}" + SOURCE="${{ github.head_ref }}" + # Feature branches → development only + if [[ "$SOURCE" =~ ^(feat|fix|chore|docs|refactor)/ && "$TARGET" != "development" ]]; then + echo "::error::Feature branches must target development, not $TARGET." + exit 1 + fi + # PRs to main → must come from development + if [[ "$TARGET" == "main" && "$SOURCE" != "development" ]]; then + echo "::error::PRs to main must come from development." + exit 1 + fi + echo "✓ branch strategy OK: $SOURCE → $TARGET" + - name: Check branch naming run: | SOURCE="${{ github.head_ref }}" + if [[ "$SOURCE" == "development" ]]; then exit 0; fi if [[ ! "$SOURCE" =~ ^(feat|fix|chore|docs|refactor)/ ]]; then echo "::error::Branch '$SOURCE' does not follow naming convention." echo "Must start with: feat/, fix/, chore/, docs/, or refactor/" diff --git a/CLAUDE.md b/CLAUDE.md index f471303..5957121 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,7 +10,10 @@ enforcement lives in CI and the Lean type system, not here. - **No `import Mathlib`** — full import banned; Lean syntax linter `Util/Linter/ImportBan.lean` enforces at build time. - **Build green** — `lake build` must pass. -- **Branch protection** — `main` protected; no direct push, PRs only. +- **Branch protection** — `main` and `development` protected; + no direct push, PRs only. +- **Branch strategy** — feature branches (`feat/`/`fix/`/`chore/`/ + `docs/`/`refactor/`) → `development` → `main`. CI enforces. - **Branch naming** — branches must start with `feat/`/`fix/`/`chore/`/ `docs/`/`refactor/`. CI enforces. - **PR title** — must match `type(scope): summary`. CI enforces.