import SGC.Axioms.Geometry
import Mathlib.LinearAlgebra.Matrix.ToLin
import Mathlib.Analysis.Normed.Algebra.Exponential
import Mathlib.Data.Setoid.Partition
import Mathlib.Tactic

/-!
  # SGC/Renormalization/Lumpability.lean

  Strong Lumpability and Functorial Stability.

  This file establishes that the stability triad (λ_gap, β(t), B(t)) is
  functorial under coarse-graining (renormalization group transformations).

  ## Key Theorem
  - `gap_non_decrease`: λ̄_gap ≥ λ_gap (Spectral gap preservation under coarse-graining)

  ## Design
  Setoid-based partition with explicit quotient map Q.

  **NOTE**: This module implements renormalization via `Setoid` partitions. It relies
  on the **Explicit Weight Pattern** to handle the transformation π → π̄ without
  type-level coercions. See `ARCHITECTURE.md` for the full rationale.
-/

noncomputable section

namespace SGC
section Lumpability
open Finset LinearMap Matrix Real NormedSpace

variable {V : Type*} [Fintype V] [DecidableEq V]

/-! ### 1. Strong Lumpability Definition -/

/-- A partition of V into blocks (coarse-grained states).
    Uses Setoid for clean quotient handling. -/
structure Partition (V : Type*) [DecidableEq V] where
  rel : Setoid V
  decRel : DecidableRel rel.r

/-- The quotient type (coarse-grained state space). -/
abbrev Partition.Quot {V : Type*} [DecidableEq V] (P : Partition V) : Type _ := Quotient P.rel

instance Partition.instDecidableRel {V : Type*} [DecidableEq V] (P : Partition V) :
    DecidableRel P.rel.r := P.decRel

instance Partition.instFintype {V : Type*} [Fintype V] [DecidableEq V] (P : Partition V) :
    Fintype P.Quot := @Quotient.fintype V _ P.rel P.decRel

instance Partition.instDecidableEq {V : Type*} [DecidableEq V] (P : Partition V) :
    DecidableEq P.Quot := @Quotient.decidableEq V P.rel P.decRel

/-- The quotient map: V → V̄. -/
def Partition.quot_map (P : Partition V) : V → P.Quot := Quotient.mk P.rel

/-- The aggregation weight: π̄(ā) = Σ_{x∈ā} π(x). -/
def pi_bar (P : Partition V) (pi_dist : V → ℝ) : P.Quot → ℝ :=
  fun a_bar => ∑ x : V, if P.quot_map x = a_bar then pi_dist x else 0

/-- Helper: sum over equivalence class. -/
lemma pi_bar_eq_sum_class (P : Partition V) (pi_dist : V → ℝ) (a_bar : P.Quot) :
    pi_bar P pi_dist a_bar = ∑ x : V, if P.quot_map x = a_bar then pi_dist x else 0 := rfl

/-- π̄ is positive when π is positive. -/
lemma pi_bar_pos (P : Partition V) {pi_dist : V → ℝ} (hπ : ∀ v, 0 < pi_dist v)
    (a_bar : P.Quot) : 0 < pi_bar P pi_dist a_bar := by
  unfold pi_bar
  obtain ⟨a, ha⟩ := Quotient.exists_rep a_bar
  have h_mem : P.quot_map a = a_bar := by simp [Partition.quot_map, ha]
  apply Finset.sum_pos'
  · intro x _
    by_cases h : P.quot_map x = a_bar <;> simp [h, le_of_lt (hπ x)]
  · exact ⟨a, Finset.mem_univ a, by simp [h_mem, hπ a]⟩

/-- π̄ sums to 1 when π sums to 1. -/
lemma pi_bar_sum_one (P : Partition V) {pi_dist : V → ℝ} (h_sum : ∑ v, pi_dist v = 1) :
    ∑ a_bar, pi_bar P pi_dist a_bar = 1 := by
  unfold pi_bar
  rw [← h_sum]
  rw [Finset.sum_comm]
  apply Finset.sum_congr rfl
  intro x _
  have h_eq : (∑ a_bar : P.Quot, if P.quot_map x = a_bar then pi_dist x else 0) = pi_dist x := by
    rw [Finset.sum_ite_eq]
    simp [Finset.mem_univ]
  exact h_eq

/-! ### 2. Strong Lumpability Condition -/

/-- **Strong Lumpability**: L is strongly lumpable w.r.t. partition P if
    for all x, y in the same block, and all blocks b̄:
    Σ_{z∈b̄} L_{xz} = Σ_{z∈b̄} L_{yz}

    This ensures the quotient generator is well-defined. -/
def IsStronglyLumpable (L : Matrix V V ℝ) (P : Partition V) : Prop :=
  ∀ x y : V, P.rel.r x y → ∀ b_bar : P.Quot,
    (∑ z : V, if P.quot_map z = b_bar then L x z else 0) =
    (∑ z : V, if P.quot_map z = b_bar then L y z else 0)

/-! ### 2b. Approximate Lumpability (Engineering Tolerance)

In physical systems, exact symmetries never hold. A "chair" has scratches; atoms vibrate.
Approximate lumpability captures the engineering reality that coarse-graining works
even when partition symmetries hold only up to tolerance ε.

**Key Insight**: If the lumpability defect is small, the spectral gap degradation is bounded.
This validates that macroscopic models (chairs, forklifts) remain stable under sensor noise.
-/

/-- **Approximate Lumpability**: L is approximately lumpable w.r.t. partition P with
    tolerance ε if for all x, y in the same block, and all blocks b̄:
    |Σ_{z∈b̄} L_{xz} - Σ_{z∈b̄} L_{yz}| ≤ ε

    This is the "Wobbly Chair" condition: real-world symmetries are never exact. -/
def IsRowSumApproxLumpable (L : Matrix V V ℝ) (P : Partition V) (ε : ℝ) : Prop :=
  ∀ x y : V, P.rel.r x y → ∀ b_bar : P.Quot,
    |∑ z : V, (if P.quot_map z = b_bar then L x z else 0) -
     ∑ z : V, (if P.quot_map z = b_bar then L y z else 0)| ≤ ε

/-- Strong lumpability implies approximate lumpability with ε = 0. -/
lemma strong_implies_approx_zero (L : Matrix V V ℝ) (P : Partition V)
    (hL : IsStronglyLumpable L P) : IsRowSumApproxLumpable L P 0 := by
  intro x y hxy b_bar
  rw [hL x y hxy b_bar]
  simp

/-- Approximate lumpability with ε = 0 implies strong lumpability. -/
lemma approx_zero_implies_strong (L : Matrix V V ℝ) (P : Partition V)
    (hL : IsRowSumApproxLumpable L P 0) : IsStronglyLumpable L P := by
  intro x y hxy b_bar
  have h := hL x y hxy b_bar
  simp only [le_antisymm_iff, abs_nonpos_iff] at h
  linarith [h]

/-! ### 3. Quotient Generator (Simple Form) -/

/-- Row sum over a block: Σ_{k∈B} L_{i,k}. -/
def row_sum_block (L : Matrix V V ℝ) (P : Partition V) (i : V) (B : P.Quot) : ℝ :=
  ∑ k : V, if P.quot_map k = B then L i k else 0

/-- Under strong lumpability, row_sum_block is constant on equivalence classes. -/
lemma row_sum_block_const (L : Matrix V V ℝ) (P : Partition V)
    (hL : IsStronglyLumpable L P) (i j : V) (hij : P.rel.r i j) (B : P.Quot) :
    row_sum_block L P i B = row_sum_block L P j B :=
  hL i j hij B

/-- The quotient generator M on Q (simple form using any representative).
    M_{A,B} = Σ_{k∈B} L_{u,k} for any u ∈ A.

    Under strong lumpability, this is well-defined. -/
def QuotientGeneratorSimple (L : Matrix V V ℝ) (P : Partition V) : Matrix P.Quot P.Quot ℝ :=
  fun A B => row_sum_block L P (Quotient.out A) B

/-- The lift matrix K : V × Q where K_{i,B} = 1 if i ∈ B, else 0. -/
def lift_matrix (P : Partition V) : Matrix V P.Quot ℝ :=
  fun i B => if P.quot_map i = B then 1 else 0

/-! ### 4. The Intertwining Theorem -/

/-- LHS of intertwining: (L * K)_{i,B} = Σ_k L_{ik} * K_{kB} = Σ_{k∈B} L_{ik}. -/
lemma LK_entry (L : Matrix V V ℝ) (P : Partition V) (i : V) (B : P.Quot) :
    (L * lift_matrix P) i B = row_sum_block L P i B := by
  simp only [Matrix.mul_apply, lift_matrix]
  apply Finset.sum_congr rfl
  intro k _
  by_cases h : P.quot_map k = B
  · simp [h]
  · simp [h]

/-- RHS of intertwining: (K * M)_{i,B} = Σ_C K_{iC} * M_{CB} = M_{⟦i⟧,B}. -/
lemma KM_entry (L : Matrix V V ℝ) (P : Partition V) (i : V) (B : P.Quot) :
    (lift_matrix P * QuotientGeneratorSimple L P) i B =
    QuotientGeneratorSimple L P (P.quot_map i) B := by
  simp only [Matrix.mul_apply, lift_matrix, QuotientGeneratorSimple]
  -- Sum over C: if P.quot_map i = C then 1 * row_sum else 0
  -- Only C = P.quot_map i contributes
  have h_sum : (∑ C : P.Quot, (if P.quot_map i = C then (1 : ℝ) else 0) *
      row_sum_block L P (Quotient.out C) B) =
      row_sum_block L P (Quotient.out (P.quot_map i)) B := by
    -- Only the term where C = P.quot_map i contributes
    rw [Finset.sum_eq_single (P.quot_map i)]
    · -- When C = P.quot_map i: the condition is true
      simp only [↓reduceIte, one_mul]
    · -- When C ≠ P.quot_map i: the term is 0
      intro C _ hC
      rw [if_neg (Ne.symm hC), zero_mul]
    · -- P.quot_map i is in univ
      intro h
      exact (h (Finset.mem_univ _)).elim
  exact h_sum

/-- Under strong lumpability, the quotient generator at ⟦i⟧ equals row_sum at i. -/
lemma quot_gen_eq_row_sum (L : Matrix V V ℝ) (P : Partition V)
    (hL : IsStronglyLumpable L P) (i : V) (B : P.Quot) :
    QuotientGeneratorSimple L P (P.quot_map i) B = row_sum_block L P i B := by
  simp only [QuotientGeneratorSimple]
  -- Need: row_sum_block L P (Quotient.out (P.quot_map i)) B = row_sum_block L P i B
  -- Since Quotient.out (P.quot_map i) ≈ i
  have h_out_eq : Quotient.mk P.rel (Quotient.out (P.quot_map i)) = P.quot_map i :=
    Quotient.out_eq (P.quot_map i)
  have h_i_eq : Quotient.mk P.rel i = P.quot_map i := rfl
  have h_equiv : P.rel.r (Quotient.out (P.quot_map i)) i := by
    have h := h_out_eq.trans h_i_eq.symm
    exact Quotient.eq'.mp h
  exact row_sum_block_const L P hL _ i h_equiv B

/-- **The Intertwining Theorem (Dynkin Formula)**: L * K = K * M.

    The original dynamics L and quotient dynamics M are related by the lift operator.
    This is the fundamental algebraic property of strong lumpability. -/
theorem intertwining (L : Matrix V V ℝ) (P : Partition V) (hL : IsStronglyLumpable L P) :
    L * lift_matrix P = lift_matrix P * QuotientGeneratorSimple L P := by
  ext i B
  rw [LK_entry, KM_entry, quot_gen_eq_row_sum L P hL]

/-- Power intertwining: L^n * K = K * M^n by induction. -/
theorem intertwining_pow (L : Matrix V V ℝ) (P : Partition V) (hL : IsStronglyLumpable L P)
    (n : ℕ) : L ^ n * lift_matrix P = lift_matrix P * (QuotientGeneratorSimple L P) ^ n := by
  induction n with
  | zero => simp [Matrix.one_mul, Matrix.mul_one]
  | succ n ih =>
    calc L ^ (n + 1) * lift_matrix P
        = L ^ n * L * lift_matrix P := by rw [pow_succ]
      _ = L ^ n * (L * lift_matrix P) := by rw [Matrix.mul_assoc]
      _ = L ^ n * (lift_matrix P * QuotientGeneratorSimple L P) := by rw [intertwining L P hL]
      _ = (L ^ n * lift_matrix P) * QuotientGeneratorSimple L P := by rw [Matrix.mul_assoc]
      _ = (lift_matrix P * (QuotientGeneratorSimple L P) ^ n) * QuotientGeneratorSimple L P := by rw [ih]
      _ = lift_matrix P * ((QuotientGeneratorSimple L P) ^ n * QuotientGeneratorSimple L P) := by rw [Matrix.mul_assoc]
      _ = lift_matrix P * (QuotientGeneratorSimple L P) ^ (n + 1) := by rw [← pow_succ]

/-! ### 5. Weighted Quotient Generator -/

/-- The weighted quotient generator L̄ on V̄.
    L̄_{āb̄} = (1/π̄(ā)) * Σ_{x∈ā} π(x) * (Σ_{z∈b̄} L_{xz})

    Under strong lumpability, this equals the simple form. -/
def QuotientGenerator (L : Matrix V V ℝ) (P : Partition V) (pi_dist : V → ℝ)
    (_hπ : ∀ v, 0 < pi_dist v) : Matrix P.Quot P.Quot ℝ :=
  fun a_bar b_bar =>
    let sum_over_a := ∑ x : V, if P.quot_map x = a_bar
      then pi_dist x * row_sum_block L P x b_bar
      else 0
    sum_over_a / pi_bar P pi_dist a_bar

/-- Under strong lumpability, weighted and simple quotient generators agree. -/
lemma quotient_generator_eq_simple (L : Matrix V V ℝ) (P : Partition V) (pi_dist : V → ℝ)
    (hπ : ∀ v, 0 < pi_dist v) (hL : IsStronglyLumpable L P) (A B : P.Quot) :
    QuotientGenerator L P pi_dist hπ A B = QuotientGeneratorSimple L P A B := by
  simp only [QuotientGenerator, QuotientGeneratorSimple]
  -- The sum is Σ_{x∈A} π(x) * row_sum_block = row_sum_block(rep) * Σ_{x∈A} π(x)
  -- since row_sum_block is constant on A by lumpability
  obtain ⟨a, ha⟩ := Quotient.exists_rep A
  have h_const : ∀ x, P.quot_map x = A → row_sum_block L P x B = row_sum_block L P a B := by
    intro x hx
    have h_eq : P.rel.r x a := by
      simp only [Partition.quot_map] at hx
      rw [← ha] at hx
      exact Quotient.eq'.mp hx
    exact row_sum_block_const L P hL x a h_eq B
  have h_sum : (∑ x, if P.quot_map x = A then pi_dist x * row_sum_block L P x B else 0) =
      row_sum_block L P a B * (∑ x, if P.quot_map x = A then pi_dist x else 0) := by
    rw [Finset.mul_sum]
    apply Finset.sum_congr rfl
    intro x _
    by_cases hx : P.quot_map x = A
    · simp only [hx, ↓reduceIte]
      rw [h_const x hx]
      ring
    · simp [hx]
  rw [h_sum]
  have h_pi_bar_eq : (∑ x, if P.quot_map x = A then pi_dist x else 0) = pi_bar P pi_dist A := rfl
  rw [h_pi_bar_eq]
  have hπ_bar_pos : 0 < pi_bar P pi_dist A := pi_bar_pos P hπ A
  have hπ_bar_ne : pi_bar P pi_dist A ≠ 0 := ne_of_gt hπ_bar_pos
  rw [mul_div_assoc, div_self hπ_bar_ne, mul_one]
  -- Now show: row_sum_block L P a B = row_sum_block L P (Quotient.out A) B
  have h_a_in_A : P.quot_map a = A := ha
  have h_out_in_A : Quotient.mk P.rel (Quotient.out A) = A := Quotient.out_eq A
  have h_equiv : P.rel.r a (Quotient.out A) := by
    have h1 : Quotient.mk P.rel a = A := h_a_in_A
    have h2 := h1.trans h_out_in_A.symm
    exact Quotient.eq'.mp h2
  exact row_sum_block_const L P hL a _ h_equiv B

/-! ### 4. Quotient Map as Linear Operator -/

/-- The averaging/quotient map Q : (V → ℝ) → (V̄ → ℝ).
    (Q f)(ā) = (1/π̄(ā)) * Σ_{x∈ā} π(x) * f(x) -/
def Q_map (P : Partition V) (pi_dist : V → ℝ) (_hπ : ∀ v, 0 < pi_dist v) :
    (V → ℝ) →ₗ[ℝ] (P.Quot → ℝ) where
  toFun f a_bar :=
    (∑ x : V, if P.quot_map x = a_bar then pi_dist x * f x else 0) / pi_bar P pi_dist a_bar
  map_add' f g := by
    ext a_bar
    show (∑ x, if P.quot_map x = a_bar then pi_dist x * (f x + g x) else 0) / _ = _
    have h_num : (∑ x, if P.quot_map x = a_bar then pi_dist x * (f x + g x) else 0) =
        (∑ x, if P.quot_map x = a_bar then pi_dist x * f x else 0) +
        (∑ x, if P.quot_map x = a_bar then pi_dist x * g x else 0) := by
      rw [← Finset.sum_add_distrib]
      apply Finset.sum_congr rfl
      intro x _
      by_cases h : P.quot_map x = a_bar
      · simp only [h, ↓reduceIte, mul_add]
      · simp only [h, ↓reduceIte, add_zero]
    rw [h_num, add_div]
    rfl
  map_smul' c f := by
    ext a_bar
    simp only [RingHom.id_apply, Pi.smul_apply, smul_eq_mul]
    have h_num : (∑ x, if P.quot_map x = a_bar then pi_dist x * (c * f x) else 0) =
        c * (∑ x, if P.quot_map x = a_bar then pi_dist x * f x else 0) := by
      rw [Finset.mul_sum]
      apply Finset.sum_congr rfl
      intro x _
      by_cases h : P.quot_map x = a_bar
      · simp [h]; ring
      · simp [h]
    rw [h_num, mul_div_assoc]

/-- The lifting map: (V̄ → ℝ) → (V → ℝ).
    (lift g)(x) = g(q(x)) -/
def lift_map (P : Partition V) : (P.Quot → ℝ) →ₗ[ℝ] (V → ℝ) where
  toFun g x := g (P.quot_map x)
  map_add' f g := by ext x; simp [Pi.add_apply]
  map_smul' c f := by ext x; simp [Pi.smul_apply, smul_eq_mul]

/-! ### 5. Block-Constant Functions and Lift Isometry -/

/-- A function is block-constant if it's constant on equivalence classes. -/
def IsBlockConstant (P : Partition V) (f : V → ℝ) : Prop :=
  ∀ x y : V, P.rel.r x y → f x = f y

/-- The lift of a function on Q to V is block-constant. -/
lemma lift_is_block_constant (P : Partition V) (g : P.Quot → ℝ) :
    IsBlockConstant P (fun x => g (P.quot_map x)) := fun x y hxy => by
  simp only [Partition.quot_map]
  congr 1
  exact Quotient.sound hxy

/-- **Lift Isometry**: ⟨lift(f), lift(f)⟩_π = ⟨f, f⟩_π̄.
    The lift preserves the weighted L² norm. -/
lemma lift_inner_pi_eq (P : Partition V) (pi_dist : V → ℝ) (g : P.Quot → ℝ) :
    inner_pi pi_dist (fun x => g (P.quot_map x)) (fun x => g (P.quot_map x)) =
    inner_pi (pi_bar P pi_dist) g g := by
  simp only [inner_pi, pi_bar]
  -- LHS: Σ_x π(x) * g([x])²
  -- RHS: Σ_A (Σ_{x∈A} π(x)) * g(A)²
  -- Rewrite LHS by grouping by equivalence class
  have h_group : ∀ x : V, pi_dist x * g (P.quot_map x) * g (P.quot_map x) =
      ∑ A : P.Quot, if P.quot_map x = A then pi_dist x * g A * g A else 0 := fun x => by
    rw [Finset.sum_ite_eq, if_pos (Finset.mem_univ _)]
  simp_rw [h_group]
  rw [Finset.sum_comm]
  apply Finset.sum_congr rfl
  intro A _
  have h_factor : (∑ x, if P.quot_map x = A then pi_dist x * g A * g A else 0) =
      (∑ x, if P.quot_map x = A then pi_dist x else 0) * g A * g A := by
    rw [Finset.sum_mul, Finset.sum_mul]
    apply Finset.sum_congr rfl
    intro x _
    by_cases h : P.quot_map x = A
    · simp [h]
    · simp [h]
  rw [h_factor]

/-- Block-constant functions correspond exactly to lifted functions. -/
lemma block_constant_iff_lift (P : Partition V) (f : V → ℝ) :
    IsBlockConstant P f ↔ ∃ g : P.Quot → ℝ, f = fun x => g (P.quot_map x) := by
  constructor
  · intro hf
    -- Define g on representatives
    use fun A => f (Quotient.out A)
    ext x
    simp only [Partition.quot_map]
    have h_eq : P.rel.r x (Quotient.out (Quotient.mk P.rel x)) := by
      have := Quotient.out_eq (Quotient.mk P.rel x)
      exact Quotient.eq'.mp this.symm
    exact hf x _ h_eq
  · intro ⟨g, hg⟩
    rw [hg]
    exact lift_is_block_constant P g

/-! ### 7. Vector Intertwining and Quadratic Form Preservation -/

/-- The lift function: given f : Q → ℝ, produce a block-constant function on V. -/
def lift_fun (P : Partition V) (f : P.Quot → ℝ) : V → ℝ := fun x => f (P.quot_map x)

/-- lift_fun equals lift_matrix applied to f.

    The sum Σ_B (if ⟦i⟧ = B then 1 else 0) * f(B) has only one non-zero term
    when B = ⟦i⟧, giving f(⟦i⟧). Uses Finset.sum_eq_single pattern. -/
lemma lift_fun_eq_mulVec (P : Partition V) (f : P.Quot → ℝ) :
    lift_fun P f = lift_matrix P *ᵥ f := by
  ext i
  simp only [lift_fun, Matrix.mulVec, lift_matrix, Partition.quot_map]
  -- Goal: f(⟦i⟧) = Σ_B K_{iB} * f(B) where K = Matrix.of (indicator)
  -- Use sum_eq_single: only B = ⟦i⟧ contributes
  have goal_eq : (∑ j : P.Quot, Matrix.of (fun x B => if Quotient.mk P.rel x = B then (1:ℝ) else 0) i j * f j) =
      f (Quotient.mk P.rel i) := by
    rw [Finset.sum_eq_single (Quotient.mk P.rel i)]
    · simp only [Matrix.of_apply, ite_true, one_mul]
    · intro B _ hB
      simp only [Matrix.of_apply]
      rw [if_neg (Ne.symm hB), zero_mul]
    · intro h; exact absurd (Finset.mem_univ _) h
  exact goal_eq.symm

/-- **Step 1: Vector Intertwining** - L *ᵥ (lift f) = lift (M *ᵥ f).

    The generator L applied to a lifted function equals the lift of M applied to f.
    This follows directly from the matrix intertwining L * K = K * M. -/
theorem L_lift_eq (L : Matrix V V ℝ) (P : Partition V) (hL : IsStronglyLumpable L P)
    (f : P.Quot → ℝ) : L *ᵥ (lift_fun P f) = lift_fun P (QuotientGeneratorSimple L P *ᵥ f) := by
  simp only [lift_fun_eq_mulVec, Matrix.mulVec_mulVec, intertwining L P hL]

/-- Generalized lift inner product for two different functions. -/
lemma lift_inner_pi_eq' (P : Partition V) (pi_dist : V → ℝ) (f g : P.Quot → ℝ) :
    inner_pi pi_dist (fun x => f (P.quot_map x)) (fun x => g (P.quot_map x)) =
    inner_pi (pi_bar P pi_dist) f g := by
  simp only [inner_pi, pi_bar]
  -- LHS: Σ_x π(x) * f([x]) * g([x])
  -- RHS: Σ_A (Σ_{x∈A} π(x)) * f(A) * g(A)
  -- Fiberwise sum pattern: group by equivalence class
  have h_group : ∀ x : V, pi_dist x * f (P.quot_map x) * g (P.quot_map x) =
      ∑ A : P.Quot, if P.quot_map x = A then pi_dist x * f A * g A else 0 := fun x => by
    rw [Finset.sum_ite_eq, if_pos (Finset.mem_univ _)]
  simp_rw [h_group]
  rw [Finset.sum_comm]
  apply Finset.sum_congr rfl
  intro A _
  have h_factor : (∑ x, if P.quot_map x = A then pi_dist x * f A * g A else 0) =
      (∑ x, if P.quot_map x = A then pi_dist x else 0) * f A * g A := by
    rw [Finset.sum_mul, Finset.sum_mul]
    apply Finset.sum_congr rfl
    intro x _
    by_cases h : P.quot_map x = A <;> simp [h]
  rw [h_factor]

/-- **Forward Quadratic Form**: ⟨lift(f), L·lift(f)⟩_π = ⟨f, M·f⟩_π̄.
    Combines L_lift_eq with lift_inner_pi_eq'. Uses Finset.sum_fiberwise pattern. -/
lemma forward_quadratic_form_eq (L : Matrix V V ℝ) (P : Partition V) (pi_dist : V → ℝ)
    (hL : IsStronglyLumpable L P) (f : P.Quot → ℝ) :
    inner_pi pi_dist (lift_fun P f) (L *ᵥ lift_fun P f) =
    inner_pi (pi_bar P pi_dist) f (QuotientGeneratorSimple L P *ᵥ f) := by
  rw [L_lift_eq L P hL f]
  -- Now: ⟨lift(f), lift(M·f)⟩_π = ⟨f, M·f⟩_π̄
  exact lift_inner_pi_eq' P pi_dist f (QuotientGeneratorSimple L P *ᵥ f)

/-- **Backward Quadratic Form**: ⟨L·lift(f), lift(f)⟩_π = ⟨M·f, f⟩_π̄.
    Symmetric version of forward_quadratic_form_eq using inner_pi_comm. -/
lemma backward_quadratic_form_eq (L : Matrix V V ℝ) (P : Partition V) (pi_dist : V → ℝ)
    (hL : IsStronglyLumpable L P) (f : P.Quot → ℝ) :
    inner_pi pi_dist (L *ᵥ lift_fun P f) (lift_fun P f) =
    inner_pi (pi_bar P pi_dist) (QuotientGeneratorSimple L P *ᵥ f) f := by
  rw [inner_pi_comm, forward_quadratic_form_eq L P pi_dist hL f, inner_pi_comm]

/-! ### 7. Dirichlet Form (Quadratic Definition Path)

The key insight (Grok): Instead of defining H as a matrix -(L + Lᵀ)/2 and proving transpose
identities that don't hold for π-weighted inner products, we define the **Dirichlet form**
directly as the symmetric part of the quadratic form:

  ℰ(u) := (1/2)[⟨u, Lu⟩_π + ⟨Lu, u⟩_π]

This is physically motivated: the energy decay rate is d/dt ‖u‖²_π = 2⟨u, Lu⟩_π.

Since ⟨u, Lu⟩_π = ⟨Lu, u⟩_π by inner_pi_comm, we have ℰ(u) = ⟨u, Lu⟩_π.
-/

/-- **Dirichlet Form**: The symmetric quadratic form ℰ(u) = (1/2)[⟨u, Lu⟩ + ⟨Lu, u⟩].
    By inner_pi_comm, this equals ⟨u, Lu⟩_π. -/
def DirichletForm (L : Matrix V V ℝ) (pi_dist : V → ℝ) (u : V → ℝ) : ℝ :=
  (1/2 : ℝ) * (inner_pi pi_dist u (L *ᵥ u) + inner_pi pi_dist (L *ᵥ u) u)

/-- **Dirichlet Form Simplification**: ℰ(u) = ⟨u, Lu⟩_π by inner_pi_comm. -/
lemma dirichlet_form_eq (L : Matrix V V ℝ) (pi_dist : V → ℝ) (u : V → ℝ) :
    DirichletForm L pi_dist u = inner_pi pi_dist u (L *ᵥ u) := by
  simp only [DirichletForm]
  rw [inner_pi_comm (L *ᵥ u) u]
  ring

/-- **Dirichlet Form on Quotient** -/
def DirichletForm_bar (L : Matrix V V ℝ) (P : Partition V) (pi_dist : V → ℝ) (f : P.Quot → ℝ) : ℝ :=
  DirichletForm (QuotientGeneratorSimple L P) (pi_bar P pi_dist) f

/-- **Dirichlet Form Lift Equality**: ℰ(lift(f)) = ℰ̄(f).

    This is THE key lemma that enables gap_non_decrease.
    Proof: immediate from forward_quadratic_form_eq + backward_quadratic_form_eq. -/
theorem dirichlet_form_lift_eq (L : Matrix V V ℝ) (P : Partition V) (pi_dist : V → ℝ)
    (hL : IsStronglyLumpable L P) (f : P.Quot → ℝ) :
    DirichletForm L pi_dist (lift_fun P f) = DirichletForm_bar L P pi_dist f := by
  simp only [DirichletForm, DirichletForm_bar]
  -- (1/2)[⟨lift(f), L·lift(f)⟩_π + ⟨L·lift(f), lift(f)⟩_π]
  -- = (1/2)[⟨f, M·f⟩_π̄ + ⟨M·f, f⟩_π̄] by forward + backward quadratic form eqs
  rw [forward_quadratic_form_eq L P pi_dist hL f]
  rw [backward_quadratic_form_eq L P pi_dist hL f]

/-! ### 7. Spectrum Containment (Dirichlet Form Approach)

Using the Dirichlet form ℰ(u) = ⟨u, Lu⟩_π (proven to descend via dirichlet_form_lift_eq),
we define Rayleigh quotients and spectral gaps without needing matrix transpose. -/

/-- **Rayleigh Quotient** using Dirichlet form: R(u) = ℰ(u) / ‖u‖²_π -/
def RayleighQuotient (L : Matrix V V ℝ) (pi_dist : V → ℝ) (u : V → ℝ) : ℝ :=
  DirichletForm L pi_dist u / inner_pi pi_dist u u

/-- The set of Rayleigh quotients over all non-zero functions orthogonal to constants. -/
def RayleighSet (L : Matrix V V ℝ) (pi_dist : V → ℝ) : Set ℝ :=
  { r | ∃ v : V → ℝ, v ≠ 0 ∧ inner_pi pi_dist v constant_vec_one = 0 ∧
    r = RayleighQuotient L pi_dist v }

/-- The set of Rayleigh quotients restricted to block-constant functions. -/
def RayleighSetBlockConstant (L : Matrix V V ℝ) (P : Partition V) (pi_dist : V → ℝ) : Set ℝ :=
  { r | ∃ v : V → ℝ, v ≠ 0 ∧ IsBlockConstant P v ∧
    inner_pi pi_dist v constant_vec_one = 0 ∧
    r = RayleighQuotient L pi_dist v }

/-- The set of Rayleigh quotients on the quotient space. -/
def RayleighSetQuot (L : Matrix V V ℝ) (P : Partition V) (pi_dist : V → ℝ) : Set ℝ :=
  { r | ∃ f : P.Quot → ℝ, f ≠ 0 ∧
    inner_pi (pi_bar P pi_dist) f (fun _ => 1) = 0 ∧
    r = RayleighQuotient (QuotientGeneratorSimple L P) (pi_bar P pi_dist) f }

/-- Block-constant Rayleigh set is a subset of the full Rayleigh set. -/
lemma rayleigh_block_subset (L : Matrix V V ℝ) (P : Partition V) (pi_dist : V → ℝ) :
    RayleighSetBlockConstant L P pi_dist ⊆ RayleighSet L pi_dist := by
  intro r ⟨v, hv_ne, _, hv_orth, hv_eq⟩
  exact ⟨v, hv_ne, hv_orth, hv_eq⟩

/-- **Subset Infimum Monotonicity**: inf over a subset ≥ inf over the whole set. -/
lemma sInf_subset_ge {S T : Set ℝ} (hST : S ⊆ T) (hS : S.Nonempty) (hT_bdd : BddBelow T) :
    sInf T ≤ sInf S := by
  apply csInf_le_csInf hT_bdd hS hST

/-- **Rayleigh Quotient Lift Equality**: R(lift(f)) = R̄(f).

    Key lemma: For block-constant u = lift(f), the Rayleigh quotient on V equals
    the Rayleigh quotient on V̄. Uses dirichlet_form_lift_eq and lift_inner_pi_eq'. -/
lemma rayleigh_quotient_lift_eq (L : Matrix V V ℝ) (P : Partition V) (pi_dist : V → ℝ)
    (hL : IsStronglyLumpable L P) (f : P.Quot → ℝ) :
    RayleighQuotient L pi_dist (lift_fun P f) =
    RayleighQuotient (QuotientGeneratorSimple L P) (pi_bar P pi_dist) f := by
  simp only [RayleighQuotient, DirichletForm_bar] at *
  -- Numerator: ℰ(lift(f)) = ℰ̄(f) by dirichlet_form_lift_eq
  have h_num : DirichletForm L pi_dist (lift_fun P f) =
               DirichletForm (QuotientGeneratorSimple L P) (pi_bar P pi_dist) f := by
    rw [← DirichletForm_bar]
    exact dirichlet_form_lift_eq L P pi_dist hL f
  -- Denominator: ‖lift(f)‖²_π = ‖f‖²_π̄ by lift_inner_pi_eq'
  have h_denom : inner_pi pi_dist (lift_fun P f) (lift_fun P f) =
                 inner_pi (pi_bar P pi_dist) f f :=
    lift_inner_pi_eq' P pi_dist f f
  rw [h_num, h_denom]

/-- **Dirichlet Gap** (Rayleigh quotient infimum): inf of ⟨u, Lu⟩_π / ‖u‖²_π over u ⊥ π.

    **IMPORTANT (Reversibility Caveat)**:
    - For **reversible** generators (L self-adjoint in L²(π)), this equals the spectral gap
      (smallest non-zero eigenvalue), and the min-max theorem applies.
    - For **non-reversible** generators, this is the coercivity constant of the symmetric
      part H = (L + L*)/2, NOT the eigenvalue-based spectral gap. Eigenvalues may be complex
      and the Rayleigh-Ritz characterization fails.

    The theorem `dirichlet_gap_non_decrease` below is algebraically valid for all generators,
    but its interpretation as "spectral gap preservation" requires reversibility.

    **For non-reversible systems**: Use `trajectory_closure_bound` in `Approximate.lean`,
    which provides valid dynamical bounds without spectral assumptions. -/
def DirichletGap (L : Matrix V V ℝ) (pi_dist : V → ℝ) : ℝ :=
  sInf (RayleighSet L pi_dist)

/-- **Dirichlet Gap on Quotient**: The Rayleigh infimum on the coarse-grained space. -/
def DirichletGap_bar (L : Matrix V V ℝ) (P : Partition V) (pi_dist : V → ℝ) : ℝ :=
  sInf (RayleighSetQuot L P pi_dist)

/-! ### 9b. Approximate Lumpability (RETIRED)

**Historical Note**: The static `approximate_gap_leakage_bound` axiom has been retired.

Our investigation moved to the dynamic, path-wise perspective formalized in
`SGC.Renormalization.Approximate.trajectory_closure_bound`. We found that static
spectral gap perturbation bounds were insufficient to capture the finite-time
validity horizons (t ≪ 1/ε) inherent in NCD systems.

The new approach:
1. Defines `IsApproxLumpable` via operator norm of the defect D = (I-Π)LΠ
2. Proves `trajectory_closure_bound` via Duhamel's principle (O(ε·t) error)
3. Proves `spectral_stability` via Weyl inequality (eigenvalue tracking)
4. Proves `NCD_uniform_error_bound` for uniform-in-time O(ε/γ) bounds

**Migration**: Use `SGC.Approximate.IsApproxLumpable` and the verified theorem stack.

**References**:
- Stewart & Sun, "Matrix Perturbation Theory" (1990)
- Kato, "Perturbation Theory for Linear Operators" (1995)
- Simon & Ando, "Aggregation of Variables in Dynamic Systems" (1961)
-/

/-- **Dirichlet Gap Non-Decrease (Simple Form)**: inf over block-constant ≥ inf over all.

    This is pure set theory: restricting an infimum to a subset cannot decrease it. -/
theorem dirichlet_gap_block_ge_all (L : Matrix V V ℝ) (P : Partition V) (pi_dist : V → ℝ)
    (hS : (RayleighSetBlockConstant L P pi_dist).Nonempty)
    (hT_bdd : BddBelow (RayleighSet L pi_dist)) :
    DirichletGap L pi_dist ≤ sInf (RayleighSetBlockConstant L P pi_dist) := by
  exact sInf_subset_ge (rayleigh_block_subset L P pi_dist) hS hT_bdd

/-- **Rayleigh Set Quotient = Rayleigh Set Block-Constant** via lift bijection.

    Every block-constant function u is lift(f) for some f, and R(u) = R̄(f). -/
lemma rayleigh_set_quot_eq_block (L : Matrix V V ℝ) (P : Partition V) (pi_dist : V → ℝ)
    (hL : IsStronglyLumpable L P) :
    RayleighSetQuot L P pi_dist =
    { r | ∃ f : P.Quot → ℝ, f ≠ 0 ∧
      inner_pi (pi_bar P pi_dist) f (fun _ => 1) = 0 ∧
      r = RayleighQuotient L pi_dist (lift_fun P f) } := by
  ext r
  constructor
  · intro ⟨f, hf_ne, hf_orth, hr⟩
    refine ⟨f, hf_ne, hf_orth, ?_⟩
    rw [hr, rayleigh_quotient_lift_eq L P pi_dist hL f]
  · intro ⟨f, hf_ne, hf_orth, hr⟩
    refine ⟨f, hf_ne, hf_orth, ?_⟩
    rw [← rayleigh_quotient_lift_eq L P pi_dist hL f, hr]

/-- Lift preserves orthogonality to constants: f ⊥ π̄ iff lift(f) ⊥ π. -/
lemma lift_orthog_iff (P : Partition V) (pi_dist : V → ℝ) (f : P.Quot → ℝ) :
    inner_pi (pi_bar P pi_dist) f (fun _ => 1) = 0 ↔
    inner_pi pi_dist (lift_fun P f) constant_vec_one = 0 := by
  -- ⟨lift(f), 1⟩_π = ⟨f, 1⟩_π̄ by lift_inner_pi_eq' with g = constant 1
  -- lift_inner_pi_eq' gives: inner_pi π (x ↦ f([x])) (x ↦ g([x])) = inner_pi π̄ f g
  -- With g = (fun _ => 1), we get the equality we need
  unfold lift_fun constant_vec_one
  have h := lift_inner_pi_eq' P pi_dist f (fun _ => 1)
  -- h : inner_pi π (x ↦ f([x])) (x ↦ 1) = inner_pi π̄ f (fun _ => 1)
  constructor <;> intro heq <;> linarith [h]

/-- lift_fun is block-constant. -/
lemma lift_fun_is_block_constant (P : Partition V) (f : P.Quot → ℝ) :
    IsBlockConstant P (lift_fun P f) := by
  intro x y hxy
  simp only [lift_fun]
  -- x and y are in the same class, so quot_map x = quot_map y
  have h_eq : P.quot_map x = P.quot_map y := Quotient.eq'.mpr hxy
  rw [h_eq]

/-- **Dirichlet Gap Non-Decrease**: γ̄ ≥ γ where γ = inf R(u).

    Coarse-graining cannot decrease the Dirichlet gap because:
    - γ(L) = inf over ALL u ⊥ π of the Rayleigh quotient R(u)
    - γ(L̄) = inf over quotient functions f ⊥ π̄ = inf over BLOCK-CONSTANT u ⊥ π
    - Since block-constant functions form a subset: inf(subset) ≥ inf(total)

    **Reversibility Caveat**: This is algebraically valid for ANY generator L.
    However, the interpretation as "spectral gap preservation" requires L to be
    self-adjoint in L²(π) (reversible/detailed balance). For non-reversible systems,
    this bounds the coercivity constant, not the eigenvalue gap.

    **For non-reversible systems**: Use `trajectory_closure_bound` instead, which
    provides valid dynamical (predictive) bounds without spectral assumptions. -/
theorem dirichlet_gap_non_decrease (L : Matrix V V ℝ) (P : Partition V) (pi_dist : V → ℝ)
    (hL : IsStronglyLumpable L P)
    (hS : (RayleighSetBlockConstant L P pi_dist).Nonempty)
    (hT_bdd : BddBelow (RayleighSet L pi_dist)) :
    DirichletGap_bar L P pi_dist ≥ DirichletGap L pi_dist := by
  simp only [DirichletGap_bar, DirichletGap]
  -- Key: RayleighSetQuot and RayleighSetBlockConstant have the same infimum
  -- because every block-constant u = lift(f) for unique f, and R(lift(f)) = R̄(f)
  have h_eq : sInf (RayleighSetQuot L P pi_dist) = sInf (RayleighSetBlockConstant L P pi_dist) := by
    -- Show the sets are equal via the lift bijection
    have h_set_eq : RayleighSetQuot L P pi_dist = RayleighSetBlockConstant L P pi_dist := by
      ext r
      simp only [RayleighSetQuot, RayleighSetBlockConstant, Set.mem_setOf_eq]
      constructor
      · -- Quot → Block: given R̄(f) = r, show R(lift f) = r with lift f block-constant
        intro ⟨f, hf_ne, hf_orth, hr⟩
        refine ⟨lift_fun P f, ?_, lift_fun_is_block_constant P f, ?_, ?_⟩
        · -- lift(f) ≠ 0
          intro h_zero
          apply hf_ne
          ext A
          have := congr_fun h_zero (Quotient.out A)
          simp only [lift_fun, Pi.zero_apply, Partition.quot_map] at this
          rwa [Quotient.out_eq] at this
        · -- lift(f) ⊥ π
          rw [← lift_orthog_iff]; exact hf_orth
        · -- R(lift f) = r
          rw [rayleigh_quotient_lift_eq L P pi_dist hL f, hr]
      · -- Block → Quot: given block-constant u with R(u) = r, find f with R̄(f) = r
        intro ⟨u, hu_ne, hu_block, hu_orth, hr⟩
        -- Since u is block-constant, u = lift(g) for some g
        obtain ⟨g, hg_eq⟩ := (block_constant_iff_lift P u).mp hu_block
        refine ⟨g, ?_, ?_, ?_⟩
        · -- g ≠ 0
          intro hg_zero
          apply hu_ne
          rw [hg_eq]
          ext x; simp [hg_zero]
        · -- g ⊥ π̄
          rw [lift_orthog_iff]
          convert hu_orth using 2
          exact hg_eq.symm
        · -- R̄(g) = r
          rw [← rayleigh_quotient_lift_eq L P pi_dist hL g]
          convert hr using 2
          exact hg_eq.symm
    rw [h_set_eq]
  rw [h_eq]
  exact dirichlet_gap_block_ge_all L P pi_dist hS hT_bdd

end Lumpability
end SGC
