/-
Copyright (c) 2026 Michael R. Douglas. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.

# 1D Lattice Heat Kernel Convergence

Proves that the lattice heat kernel on ℤ/Nℤ converges to the continuum
heat kernel on S¹_L as N → ∞.

## Main results

- `latticeEigenvalue1d_tendsto` — eigenvalue convergence: (4N²/L²)sin²(πk/N) → (2πk/L)²
- `latticeDFTCoeff1d_tendsto` — DFT coefficient convergence via Riemann sums
- `latticeEigenvalue1d_ge_8m` — eigenvalue lower bound via Jordan's inequality

## Mathematical background

On the 1D lattice ℤ/Nℤ with spacing a = L/N, the Laplacian eigenvalues are:

  `λ_k = (4/a²) sin²(πk/N) = (2πk/L)² · sinc²(πk/N)`

As N → ∞, `sinc(πk/N) → 1`, so `λ_k → (2πk/L)²` = continuum eigenvalue.

## References

- Glimm-Jaffe, *Quantum Physics*, §6.1
- Simon, *The P(φ)₂ Euclidean QFT*, Ch. I
-/

import Lattice.HeatKernel
import SmoothCircle.Restriction
import SmoothCircle.Eigenvalues
import SmoothCircle.Nuclear
import HeatKernel.Bilinear

noncomputable section

namespace GaussianField

open Filter Real _root_.GaussianField.Matrix

variable (L : ℝ) [hL : Fact (0 < L)]

/-! ## 1D lattice heat kernel bilinear form -/

/-- **1D lattice heat kernel bilinear form applied to circle test functions.**

  `K_t^N(f, g) = Σ_x (r_N f)(x) · (K_t · r_N g)(x)`

where `r_N` is the circle restriction map with √(L/N) normalization. -/
def latticeHeatKernelBilinear1d (N : ℕ) [NeZero N] (t : ℝ)
    (f g : SmoothMap_Circle L ℝ) : ℝ :=
  let a := circleSpacing L N
  let fN : FinLatticeField 1 N := fun x => circleRestriction L N f (x 0)
  let gN : FinLatticeField 1 N := fun x => circleRestriction L N g (x 0)
  let KgN := (latticeHeatKernelMatrix 1 N a t).mulVec gN
  ∑ x : FinLatticeSites 1 N, fN x * KgN x

/-! ## Eigenvalue convergence -/

/-- `1/(N+1) → 0` as `N → ∞`. -/
private theorem tendsto_inv_succ :
    Tendsto (fun N : ℕ => (1 : ℝ) / ((N : ℝ) + 1)) atTop (nhds 0) := by
  simp_rw [one_div]
  apply Filter.Tendsto.comp tendsto_inv_atTop_zero
  apply Filter.Tendsto.atTop_add
  · exact tendsto_natCast_atTop_atTop (R := ℝ)
  · exact tendsto_const_nhds

/-- `c/(N+1) → 0` as `N → ∞` for any constant c. -/
private theorem tendsto_const_div_succ (c : ℝ) :
    Tendsto (fun N : ℕ => c / ((N : ℝ) + 1)) atTop (nhds 0) := by
  have h := tendsto_inv_succ
  simp_rw [show ∀ N : ℕ, c / ((N : ℝ) + 1) = c * (1 / ((N : ℝ) + 1)) from
    fun N => by ring]
  rw [show (0 : ℝ) = c * 0 from by ring]
  exact h.const_mul c

/-- **Eigenvalue convergence: lattice → continuum as N → ∞.**

For each fixed frequency k ≥ 0, the 1D lattice Laplacian eigenvalue
converges to the continuum circle eigenvalue:

  `(4(N+1)²/L²) sin²(πk/(N+1)) → (2πk/L)²`

For k = 0: both sides are 0.
For k > 0: write `sin²(x) = x² · sinc²(x)` and use `sinc(0) = 1`. -/
theorem latticeEigenvalue1d_tendsto (k : ℕ) :
    Tendsto
      (fun N : ℕ => (4 * ((N : ℝ) + 1) ^ 2 / L ^ 2) *
        sin (π * k / ((N : ℝ) + 1)) ^ 2)
      atTop
      (nhds ((2 * π * k / L) ^ 2)) := by
  by_cases hk : k = 0
  · -- k = 0: both sides are 0
    subst hk; simp [sin_zero]
  · -- k > 0: use sinc
    -- Let x_N = π*k/(N+1). We have sin(x_N) = x_N * sinc(x_N), so
    -- sin(x_N)² = x_N² * sinc(x_N)² and
    -- (4(N+1)²/L²) * x_N² = (4(N+1)²/L²) * (πk/(N+1))² = (2πk/L)²
    -- Hence the sequence equals (2πk/L)² * sinc(x_N)² → (2πk/L)² * 1
    have hk' : (k : ℝ) ≠ 0 := Nat.cast_ne_zero.mpr hk
    -- Rewrite using sinc: for x ≠ 0, sin(x) = x * sinc(x)
    have hxN : ∀ N : ℕ, π * (k : ℝ) / ((N : ℝ) + 1) ≠ 0 := by
      intro N; positivity
    -- Show the sequence equals (2πk/L)² * sinc(πk/(N+1))²
    suffices hsuff : Tendsto
        (fun N : ℕ => (2 * π * (k : ℝ) / L) ^ 2 *
          sinc (π * k / ((N : ℝ) + 1)) ^ 2)
        atTop (nhds ((2 * π * k / L) ^ 2)) by
      refine hsuff.congr (fun N => ?_)
      -- Need: (4(N+1)²/L²) * sin(πk/(N+1))² = (2πk/L)² * sinc(πk/(N+1))²
      rw [sinc_of_ne_zero (hxN N)]
      have hN1 : ((N : ℝ) + 1) ≠ 0 := by positivity
      have hL' : L ≠ 0 := ne_of_gt hL.out
      field_simp
      ring
    -- Now show (2πk/L)² * sinc(πk/(N+1))² → (2πk/L)²
    -- Since sinc(πk/(N+1)) → sinc(0) = 1 and sinc²(.) → 1² = 1
    conv => rhs; rw [show (2 * π * (k : ℝ) / L) ^ 2 =
        (2 * π * k / L) ^ 2 * sinc 0 ^ 2 from by rw [sinc_zero]; ring]
    apply Tendsto.const_mul
    apply Tendsto.pow
    -- sinc(πk/(N+1)) → sinc(0) as N → ∞
    exact (continuous_sinc.tendsto 0).comp (tendsto_const_div_succ (π * k))

/-! ## Lattice real Fourier basis and DFT coefficients -/

/-- The lattice real Fourier basis function on ℤ/Nℤ.

Mirrors the continuum `fourierBasisFun` but on the discrete lattice:
- Index 0: `1/√N` (constant)
- Index 2k-1 (k ≥ 1): `√(2/N) · cos(2πk·val/N)` (cosine)
- Index 2k (k ≥ 1): `√(2/N) · sin(2πk·val/N)` (sine) -/
def latticeFourierBasisFun (N : ℕ) : ℕ → ZMod N → ℝ
  | 0 => fun _ => 1 / Real.sqrt N
  | n + 1 =>
    let k := (n / 2 + 1 : ℕ)
    if n % 2 = 0 then
      fun z => Real.sqrt (2 / N) * Real.cos (2 * π * k * (ZMod.val z : ℝ) / N)
    else
      fun z => Real.sqrt (2 / N) * Real.sin (2 * π * k * (ZMod.val z : ℝ) / N)

/-- The lattice Fourier basis functions are uniformly bounded by `√(2/N)`. -/
private theorem latticeFourierBasisFun_abs_le (N : ℕ) [NeZero N] (m : ℕ) (z : ZMod N) :
    |latticeFourierBasisFun N m z| ≤ Real.sqrt (2 / N) := by
  cases m with
  | zero =>
    simp only [latticeFourierBasisFun]
    rw [abs_of_nonneg (by positivity)]
    have hN_pos : (0 : ℝ) < N := Nat.cast_pos.mpr (NeZero.pos N)
    rw [div_le_iff₀ (Real.sqrt_pos_of_pos hN_pos)]
    calc 1 = Real.sqrt 1 := Real.sqrt_one.symm
      _ ≤ Real.sqrt 2 := Real.sqrt_le_sqrt (by norm_num)
      _ = Real.sqrt (2 / ↑N) * Real.sqrt ↑N := by
          rw [← Real.sqrt_mul (by positivity : (0 : ℝ) ≤ 2 / ↑N),
            show (2 : ℝ) / ↑N * ↑N = 2 from by field_simp]
  | succ n =>
    simp only [latticeFourierBasisFun]; split
    · rw [abs_mul, abs_of_nonneg (Real.sqrt_nonneg _)]
      calc Real.sqrt (2 / ↑N) * |Real.cos _|
          ≤ Real.sqrt (2 / ↑N) * 1 :=
            mul_le_mul_of_nonneg_left (Real.abs_cos_le_one _) (Real.sqrt_nonneg _)
        _ = Real.sqrt (2 / ↑N) := mul_one _
    · rw [abs_mul, abs_of_nonneg (Real.sqrt_nonneg _)]
      calc Real.sqrt (2 / ↑N) * |Real.sin _|
          ≤ Real.sqrt (2 / ↑N) * 1 :=
            mul_le_mul_of_nonneg_left (Real.abs_sin_le_one _) (Real.sqrt_nonneg _)
        _ = Real.sqrt (2 / ↑N) := mul_one _

/-- **DFT coefficient of a restricted circle function at mode m.**

The inner product of the restricted function with the m-th lattice
Fourier basis function. For m ≥ N, this is 0 (no lattice mode).

The √(L/N) normalization in `circleRestriction` combined with the
1/√N normalization in `latticeFourierBasisFun` ensures these approximate
the continuum Fourier coefficients `DyninMityaginSpace.coeff m f`. -/
def latticeDFTCoeff1d (N : ℕ) [NeZero N]
    (f : SmoothMap_Circle L ℝ) (m : ℕ) : ℝ :=
  if m < N then
    ∑ z : ZMod N,
      circleRestriction L N f z * latticeFourierBasisFun N m z
  else 0

/-- DFT coefficients vanish beyond the lattice cutoff. -/
theorem latticeDFTCoeff1d_zero_of_ge (N : ℕ) [NeZero N]
    (f : SmoothMap_Circle L ℝ) (m : ℕ) (hm : N ≤ m) :
    latticeDFTCoeff1d L N f m = 0 := by
  unfold latticeDFTCoeff1d
  rw [if_neg (not_lt.mpr hm)]

/-- **Flat uniform bound on DFT coefficients.**

The lattice DFT coefficient is a sum of N terms, each bounded by
`√(L/N) · ‖f‖_C⁰ · √(2/N)`. Summing gives `N · √(L/N) · √(2/N) = √(2L)`.

This bound is uniform in both N and m, providing a dominating function
for dominated convergence when combined with eigenvalue decay. -/
theorem latticeDFTCoeff1d_flat_bound (f : SmoothMap_Circle L ℝ) :
    ∀ N m, |latticeDFTCoeff1d L (N + 1) f m| ≤
      Real.sqrt (2 * L) * SmoothMap_Circle.sobolevSeminorm 0 f := by
  intro N m
  by_cases hm : m < N + 1
  · simp only [latticeDFTCoeff1d, if_pos hm]
    set M := N + 1 with hM_def
    have hM_pos : (0 : ℝ) < M := by positivity
    have h_term : ∀ z : ZMod M,
        |circleRestriction L M f z * latticeFourierBasisFun M m z| ≤
        Real.sqrt (L / M) * SmoothMap_Circle.sobolevSeminorm 0 f *
          Real.sqrt (2 / M) := by
      intro z; rw [abs_mul]
      have h_cr : |circleRestriction L M f z| ≤
          Real.sqrt (L / M) * SmoothMap_Circle.sobolevSeminorm 0 f := by
        rw [circleRestriction_apply, abs_mul, abs_of_nonneg (Real.sqrt_nonneg _)]
        apply mul_le_mul_of_nonneg_left _ (Real.sqrt_nonneg _)
        have h := SmoothMap_Circle.norm_iteratedDeriv_le_sobolevSeminorm' f 0
          (circlePoint L M z)
        simp only [iteratedDeriv_zero] at h
        rwa [Real.norm_eq_abs] at h
      exact mul_le_mul h_cr (latticeFourierBasisFun_abs_le M m z) (abs_nonneg _)
        (mul_nonneg (Real.sqrt_nonneg _)
          (SmoothMap_Circle.sobolevSeminorm_nonneg 0 f))
    calc |∑ z : ZMod M,
            circleRestriction L M f z * latticeFourierBasisFun M m z|
        ≤ ∑ z : ZMod M,
            |circleRestriction L M f z * latticeFourierBasisFun M m z| :=
          Finset.abs_sum_le_sum_abs _ _
      _ ≤ ∑ _ : ZMod M,
            (Real.sqrt (L / M) * SmoothMap_Circle.sobolevSeminorm 0 f *
              Real.sqrt (2 / M)) :=
          Finset.sum_le_sum (fun z _ => h_term z)
      _ = ↑M * (Real.sqrt (L / ↑M) * SmoothMap_Circle.sobolevSeminorm 0 f *
            Real.sqrt (2 / ↑M)) := by
          rw [Finset.sum_const, nsmul_eq_mul]
          congr 1
          rw [show Finset.univ.card = (M : ℕ) from ZMod.card M]
      _ = ↑M * (Real.sqrt (L / ↑M) * Real.sqrt (2 / ↑M)) *
            SmoothMap_Circle.sobolevSeminorm 0 f := by ring
      _ = Real.sqrt (2 * L) * SmoothMap_Circle.sobolevSeminorm 0 f := by
          congr 1
          rw [← Real.sqrt_mul (div_nonneg hL.out.le hM_pos.le)]
          rw [show L / ↑M * (2 / ↑M) = 2 * L / ↑M ^ 2 from by
            field_simp]
          rw [Real.sqrt_div (by linarith [hL.out] : (0 : ℝ) ≤ 2 * L)]
          rw [Real.sqrt_sq hM_pos.le]; field_simp
  · rw [latticeDFTCoeff1d_zero_of_ge L (N + 1) f m (by omega), abs_zero]
    exact mul_nonneg (Real.sqrt_nonneg _)
      (SmoothMap_Circle.sobolevSeminorm_nonneg 0 f)

/-! ## Explicit 1D lattice eigenvalues -/

/-- The 1D lattice Laplacian eigenvalue at mode m, using `fourierFreq` indexing.

`λ_m = (4/a²) sin²(π · fourierFreq(m) / N)`

This matches the continuum ordering where modes are:
constant, cos(2πx/L), sin(2πx/L), cos(4πx/L), ... -/
def latticeEigenvalue1d (N : ℕ) (a : ℝ) (m : ℕ) : ℝ :=
  (4 / a ^ 2) * sin (π * (SmoothMap_Circle.fourierFreq m : ℝ) / N) ^ 2

/-- Eigenvalue at mode 0 is 0 (constant mode has zero Laplacian eigenvalue). -/
theorem latticeEigenvalue1d_zero (N : ℕ) (a : ℝ) :
    latticeEigenvalue1d N a 0 = 0 := by
  simp [latticeEigenvalue1d, SmoothMap_Circle.fourierFreq, sin_zero]

/-- 1D lattice eigenvalues are nonneg. -/
theorem latticeEigenvalue1d_nonneg (N : ℕ) (a : ℝ) (m : ℕ) :
    0 ≤ latticeEigenvalue1d N a m := by
  unfold latticeEigenvalue1d
  apply mul_nonneg (div_nonneg (by norm_num) (sq_nonneg _)) (sq_nonneg _)

/-- **1D lattice eigenvalue converges to continuum eigenvalue.**

Proved from `latticeEigenvalue1d_tendsto` applied to `fourierFreq(m)`. -/
theorem latticeEigenvalue1d_tendsto_continuum (m : ℕ) :
    Tendsto (fun N : ℕ =>
      latticeEigenvalue1d (N + 1) (circleSpacing L (N + 1)) m)
      atTop (nhds (HasLaplacianEigenvalues.eigenvalue
        (E := SmoothMap_Circle L ℝ) m)) := by
  simp only [latticeEigenvalue1d, circleSpacing]
  change Tendsto (fun N : ℕ =>
    4 / (L / ↑(N + 1)) ^ 2 *
      sin (π * ↑(SmoothMap_Circle.fourierFreq m) / ↑(N + 1)) ^ 2)
    atTop (nhds ((2 * π * ↑(SmoothMap_Circle.fourierFreq m) / L) ^ 2))
  -- (4 / (L/(N+1))²) * sin²(π·freq/((N+1))) → (2π·freq/L)²
  -- = (4(N+1)²/L²) * sin²(π·freq/(N+1)) → (2π·freq/L)²
  -- which is exactly latticeEigenvalue1d_tendsto applied to k = fourierFreq m
  have h := latticeEigenvalue1d_tendsto L (SmoothMap_Circle.fourierFreq m)
  convert h using 1
  ext N
  have hN : ((N : ℝ) + 1) ≠ 0 := by positivity
  have hL' : L ≠ 0 := ne_of_gt hL.out
  field_simp
  push_cast
  ring

/-! ## Eigenvalue lower bound via Jordan's inequality -/

/-- For `m ≥ 1`, the `fourierFreq` squared is at least `m/2`.

Since `fourierFreq m = (m-1)/2 + 1` for `m ≥ 1`, we have
`2 · fourierFreq m ≥ m` and `fourierFreq m ≥ 1`, giving
`fourierFreq(m)² ≥ fourierFreq(m) ≥ m/2`. -/
private theorem fourierFreq_sq_ge_half (m : ℕ) (hm : 1 ≤ m) :
    (m : ℝ) / 2 ≤ (SmoothMap_Circle.fourierFreq m : ℝ) ^ 2 := by
  have h_freq_ge_1 : (1 : ℝ) ≤ (SmoothMap_Circle.fourierFreq m : ℝ) := by
    have : 1 ≤ SmoothMap_Circle.fourierFreq m := by
      cases m with
      | zero => omega
      | succ n => simp only [SmoothMap_Circle.fourierFreq]; omega
    exact_mod_cast this
  have h2 : (m : ℝ) ≤ 2 * (SmoothMap_Circle.fourierFreq m : ℝ) := by
    have : m ≤ 2 * SmoothMap_Circle.fourierFreq m := by
      cases m with
      | zero => omega
      | succ n => simp only [SmoothMap_Circle.fourierFreq]; omega
    exact_mod_cast this
  calc (m : ℝ) / 2
      ≤ (SmoothMap_Circle.fourierFreq m : ℝ) := by linarith
    _ ≤ (SmoothMap_Circle.fourierFreq m : ℝ) ^ 2 := by
        rw [sq]; exact le_mul_of_one_le_left (by linarith) h_freq_ge_1

/-- **Eigenvalue lower bound via Jordan's inequality.**

For `m ≥ 1`, the lattice eigenvalue satisfies `λ_m ≥ 8m/L²`, uniformly in N.

Proof chain:
1. Jordan: `sin(πj/M) ≥ 2j/M` for `j ≤ M/2`
2. Square: `sin²(πj/M) ≥ 4j²/M²`
3. Multiply: `(4M²/L²) · sin²(πj/M) ≥ 16j²/L²`
4. Use `fourierFreq(m)² ≥ m/2`: `16j²/L² ≥ 8m/L²` -/
theorem latticeEigenvalue1d_ge_8m (N m : ℕ) (hm : 1 ≤ m)
    (hm_lt : m < N + 1) :
    8 * (m : ℝ) / L ^ 2 ≤
    latticeEigenvalue1d (N + 1) (circleSpacing L (N + 1)) m := by
  unfold latticeEigenvalue1d
  push_cast [circleSpacing]
  set M := (N : ℝ) + 1
  set j := (SmoothMap_Circle.fourierFreq m : ℝ)
  have hM_pos : 0 < M := by positivity
  have hL' : 0 < L := hL.out
  have h_coeff : 4 / (L / M) ^ 2 = 4 * M ^ 2 / L ^ 2 := by field_simp
  rw [h_coeff]
  have hj_pos : 0 < j := by
    show 0 < (SmoothMap_Circle.fourierFreq m : ℝ)
    have : 1 ≤ SmoothMap_Circle.fourierFreq m := by
      cases m with
      | zero => omega
      | succ n => simp only [SmoothMap_Circle.fourierFreq]; omega
    positivity
  have hj_le_half : j ≤ M / 2 := by
    show (SmoothMap_Circle.fourierFreq m : ℝ) ≤ ((N : ℝ) + 1) / 2
    have h_nat : SmoothMap_Circle.fourierFreq m ≤ (N + 1) / 2 := by
      cases m with
      | zero => omega
      | succ n => simp only [SmoothMap_Circle.fourierFreq]; omega
    calc (SmoothMap_Circle.fourierFreq m : ℝ)
        ≤ ((N + 1) / 2 : ℕ) := by exact_mod_cast h_nat
      _ ≤ ((N : ℝ) + 1) / 2 := by
          have h := Nat.div_mul_le_self (N + 1) 2
          have : (↑((N + 1) / 2) : ℝ) * 2 ≤ ↑(N + 1) := by exact_mod_cast h
          push_cast at this ⊢; linarith
  have harg : 0 ≤ π * j / M := by positivity
  have harg_le : π * j / M ≤ π / 2 := by
    rw [div_le_div_iff₀ hM_pos (by norm_num : (0:ℝ) < 2)]
    nlinarith [pi_pos]
  have hjordan : 2 / π * (π * j / M) ≤ sin (π * j / M) :=
    mul_le_sin harg harg_le
  have hsin_sq : (2 * j / M) ^ 2 ≤ sin (π * j / M) ^ 2 := by
    have : 2 / π * (π * j / M) = 2 * j / M := by field_simp
    rw [← this, sq, sq]
    exact mul_self_le_mul_self (by positivity) hjordan
  have h_freq_sq : (m : ℝ) / 2 ≤ j ^ 2 := by
    show (m : ℝ) / 2 ≤ (SmoothMap_Circle.fourierFreq m : ℝ) ^ 2
    exact fourierFreq_sq_ge_half m hm
  calc 8 * (m : ℝ) / L ^ 2
      = 16 * ((m : ℝ) / 2) / L ^ 2 := by ring
    _ ≤ 16 * j ^ 2 / L ^ 2 := by
        apply div_le_div_of_nonneg_right _ (sq_nonneg L)
        linarith [h_freq_sq]
    _ = 4 * M ^ 2 / L ^ 2 * (2 * j / M) ^ 2 := by
        field_simp; ring
    _ ≤ 4 * M ^ 2 / L ^ 2 * sin (π * j / M) ^ 2 := by
        apply mul_le_mul_of_nonneg_left hsin_sq; positivity

/-! ## Spectral expansion -/

/-- **Lattice heat kernel bilinear form equals the Mathlib spectral expansion.**

Uses `bilinear_exp_eq_spectral` applied to the negative Laplacian matrix.
The eigenvectors and eigenvalues are from Mathlib's abstract spectral theorem. -/
theorem latticeHeatKernelBilinear1d_eq_mathlib_spectral (N : ℕ) [NeZero N] (t : ℝ)
    (f g : SmoothMap_Circle L ℝ) :
    let a := circleSpacing L N
    let hM := negLaplacianMatrix_isHermitian 1 N a
    let fN : FinLatticeSites 1 N → ℝ := fun x => circleRestriction L N f (x 0)
    let gN : FinLatticeSites 1 N → ℝ := fun x => circleRestriction L N g (x 0)
    latticeHeatKernelBilinear1d L N t f g =
    ∑ k : FinLatticeSites 1 N,
      Real.exp (-t * hM.eigenvalues k) *
      (∑ x, (hM.eigenvectorBasis k : EuclideanSpace ℝ (FinLatticeSites 1 N)) x * fN x) *
      (∑ x, (hM.eigenvectorBasis k : EuclideanSpace ℝ (FinLatticeSites 1 N)) x * gN x) := by
  intro a hM fN gN
  unfold latticeHeatKernelBilinear1d latticeHeatKernelMatrix
  exact Matrix.IsHermitian.bilinear_exp_eq_spectral hM t fN gN

/-! ## Coefficient convergence (Riemann sum) -/

/-- **Normalization identity**: the product of circle restriction and lattice
Fourier basis equals (L/N) times the product of the original function with
the continuum Fourier basis, both evaluated at the lattice point. -/
private theorem restriction_times_latticeBasis
    (N : ℕ) [NeZero N] (f : SmoothMap_Circle L ℝ) (m : ℕ) (_hm : m < N)
    (z : ZMod N) :
    circleRestriction L N f z * latticeFourierBasisFun N m z =
    (L / N) * (f (circlePoint L N z) *
      SmoothMap_Circle.fourierBasisFun (L := L) m (circlePoint L N z)) := by
  have hN_pos : (0 : ℝ) < N := Nat.cast_pos.mpr (NeZero.pos N)
  have hL' : L ≠ 0 := ne_of_gt hL.out
  have hN_ne : (N : ℝ) ≠ 0 := ne_of_gt hN_pos
  have sqrt_factor (c : ℝ) (_hc : 0 ≤ c) :
      Real.sqrt (L / N) * Real.sqrt (c / N) = L / N * Real.sqrt (c / L) := by
    rw [← Real.sqrt_mul (div_nonneg hL.out.le hN_pos.le)]
    have : L / ↑N * (c / ↑N) = (L / ↑N) ^ 2 * (c / L) := by field_simp
    rw [this, Real.sqrt_mul (sq_nonneg _),
      Real.sqrt_sq (div_nonneg hL.out.le hN_pos.le)]
  -- Key: √(L/N) * (1/√N) = (L/N) * (1/√L)
  have sqrt_factor_one :
      Real.sqrt (L / ↑N) * (1 / Real.sqrt ↑N) = L / ↑N * (1 / Real.sqrt L) := by
    have h1 : (1 : ℝ) / Real.sqrt ↑N = Real.sqrt (1 / ↑N) := by
      rw [one_div, one_div, ← Real.sqrt_inv]
    have h2 : (1 : ℝ) / Real.sqrt L = Real.sqrt (1 / L) := by
      rw [one_div, one_div, ← Real.sqrt_inv]
    rw [h1, sqrt_factor 1 (by norm_num), h2]
  -- Key: arg rewrite for trig arguments
  have arg_eq (k : ℕ) : 2 * π * ↑k * (ZMod.val z : ℝ) / ↑N =
      2 * π * ↑k * circlePoint L N z / L := by
    simp only [circlePoint]; field_simp
  rcases m with _ | n
  · -- m = 0: √(L/N) * f(pt) * (1/√N) = (L/N) * (f(pt) * (1/√L))
    simp only [circleRestriction_apply, circleSpacing_eq,
      latticeFourierBasisFun, SmoothMap_Circle.fourierBasisFun]
    calc Real.sqrt (L / ↑N) * f (circlePoint L N z) * (1 / Real.sqrt ↑N)
        = (Real.sqrt (L / ↑N) * (1 / Real.sqrt ↑N)) * f (circlePoint L N z) := by ring
      _ = (L / ↑N * (1 / Real.sqrt L)) * f (circlePoint L N z) := by
            rw [sqrt_factor_one]
      _ = L / ↑N * (f (circlePoint L N z) * (1 / Real.sqrt L)) := by ring
  · -- m = n+1
    simp only [circleRestriction_apply, circleSpacing_eq,
      latticeFourierBasisFun, SmoothMap_Circle.fourierBasisFun]
    -- The goal has unapplied lambdas inside if-then-else; split_ifs to resolve
    split_ifs with h
    · -- cosine case: n % 2 = 0
      -- LHS: √(L/N) * f(pt) * (√(2/N) * cos(2πk·val/N))
      -- RHS: (L/N) * (f(pt) * (√(2/L) * cos(2πk·pt/L)))
      simp only [arg_eq]
      calc Real.sqrt (L / ↑N) * f (circlePoint L N z) *
            (Real.sqrt (2 / ↑N) * cos (2 * π * ↑(n / 2 + 1) * circlePoint L N z / L))
          = (Real.sqrt (L / ↑N) * Real.sqrt (2 / ↑N)) *
            (f (circlePoint L N z) * cos (2 * π * ↑(n / 2 + 1) * circlePoint L N z / L)) := by ring
        _ = (L / ↑N * Real.sqrt (2 / L)) *
            (f (circlePoint L N z) * cos (2 * π * ↑(n / 2 + 1) * circlePoint L N z / L)) := by
              rw [sqrt_factor 2 (by norm_num)]
        _ = L / ↑N * (f (circlePoint L N z) *
            (Real.sqrt (2 / L) * cos (2 * π * ↑(n / 2 + 1) * circlePoint L N z / L))) := by ring
    · -- sine case: n % 2 ≠ 0
      simp only [arg_eq]
      calc Real.sqrt (L / ↑N) * f (circlePoint L N z) *
            (Real.sqrt (2 / ↑N) * sin (2 * π * ↑(n / 2 + 1) * circlePoint L N z / L))
          = (Real.sqrt (L / ↑N) * Real.sqrt (2 / ↑N)) *
            (f (circlePoint L N z) * sin (2 * π * ↑(n / 2 + 1) * circlePoint L N z / L)) := by ring
        _ = (L / ↑N * Real.sqrt (2 / L)) *
            (f (circlePoint L N z) * sin (2 * π * ↑(n / 2 + 1) * circlePoint L N z / L)) := by
              rw [sqrt_factor 2 (by norm_num)]
        _ = L / ↑N * (f (circlePoint L N z) *
            (Real.sqrt (2 / L) * sin (2 * π * ↑(n / 2 + 1) * circlePoint L N z / L))) := by ring

/-- The lattice DFT coefficient equals a Riemann sum of f times fourierBasisFun. -/
private theorem latticeDFTCoeff1d_eq_riemann_sum
    (N : ℕ) [NeZero N] (f : SmoothMap_Circle L ℝ) (m : ℕ) (hm : m < N) :
    latticeDFTCoeff1d L N f m =
    ∑ z : ZMod N, (L / N) * (f (circlePoint L N z) *
      SmoothMap_Circle.fourierBasisFun (L := L) m (circlePoint L N z)) := by
  simp only [latticeDFTCoeff1d, if_pos hm]
  congr 1; ext z
  exact restriction_times_latticeBasis L N f m hm z

/-- **Riemann sum convergence for continuous periodic functions on [0, L].**

The left-endpoint Riemann sum with N+1 equally spaced points on [0, L]
converges to the integral as N → ∞. This follows from uniform continuity
of g on the compact interval [0, L]:

- Get δ from uniform continuity for tolerance ε/L
- For N large enough, spacing L/(N+1) < δ
- On each subinterval [ka, (ka+a)], |g(x) - g(ka)| < ε/L
- Total error = Σ_k |∫_{ka}^{(k+1)a} (g(x) - g(ka)) dx| ≤ (N+1) · a · (ε/L) = ε -/
theorem riemann_sum_periodic_tendsto
    (g : ℝ → ℝ) (hg : Continuous g) (_hper : Function.Periodic g L) :
    Tendsto (fun N : ℕ =>
      ∑ k ∈ Finset.range (N + 1),
        (L / (↑(N + 1) : ℝ)) * g (↑k * L / (↑(N + 1) : ℝ)))
      atTop (nhds (∫ x in Set.Icc 0 L, g x)) := by
  have hL_pos : (0 : ℝ) < L := hL.out
  have hL_ne : L ≠ 0 := ne_of_gt hL_pos
  -- Convert ∫ x in Set.Icc 0 L to interval integral ∫ x in (0:ℝ)..L
  have icc_eq : ∫ x in Set.Icc 0 L, g x = ∫ x in (0 : ℝ)..L, g x := by
    rw [MeasureTheory.integral_Icc_eq_integral_Ioc,
      intervalIntegral.integral_of_le hL_pos.le]
  rw [icc_eq]
  -- Use Metric.tendsto_atTop: suffices to show for all ε > 0, eventually close
  rw [Metric.tendsto_atTop]
  intro ε hε
  -- Step 1: Uniform continuity of g on [0, L] (Heine-Cantor)
  have huc : UniformContinuousOn g (Set.Icc 0 L) :=
    isCompact_Icc.uniformContinuousOn_of_continuous hg.continuousOn
  rw [Metric.uniformContinuousOn_iff] at huc
  -- Step 2: Get δ from uniform continuity for tolerance ε / L
  obtain ⟨δ, hδ_pos, hδ⟩ := huc (ε / (2 * L)) (div_pos hε (by positivity))
  -- Step 3: For N large enough, spacing L/(N+1) < δ
  obtain ⟨N₀, hN₀⟩ : ∃ N₀ : ℕ, L / (↑N₀ + 1) < δ := by
    obtain ⟨n, hn⟩ := exists_nat_gt (L / δ)
    refine ⟨n, ?_⟩
    rw [div_lt_iff₀ (by positivity : (0:ℝ) < ↑n + 1)]
    have := (div_lt_iff₀ hδ_pos).mp hn
    linarith
  refine ⟨N₀, fun N hN => ?_⟩
  -- Abbreviation: a = L/(N+1) is the mesh width
  set M := N + 1 with hM_def
  have hM_pos : (0 : ℝ) < M := by positivity
  have hM_ne : (M : ℝ) ≠ 0 := ne_of_gt hM_pos
  set a := L / (M : ℝ) with ha_def
  have ha_pos : 0 < a := div_pos hL_pos hM_pos
  -- The mesh is small enough: a < δ
  have ha_lt_δ : a < δ := by
    calc a = L / (↑N + 1) := by simp [ha_def, hM_def]
      _ ≤ L / (↑N₀ + 1) := by
          apply div_le_div_of_nonneg_left hL_pos.le
            (by positivity : (0 : ℝ) < ↑N₀ + 1)
          exact_mod_cast Nat.add_le_add_right hN 1
      _ < δ := hN₀
  -- Step 4: Rewrite the Riemann sum and integral using subintervals
  -- Node points: p(k) = k * a for k = 0, ..., M
  set p : ℕ → ℝ := fun k => (k : ℝ) * a with hp_def
  -- p is monotone
  have hp_mono : ∀ k l : ℕ, k ≤ l → p k ≤ p l := by
    intro k l hkl; simp [hp_def]; exact mul_le_mul_of_nonneg_right (Nat.cast_le.mpr hkl) ha_pos.le
  -- p(0) = 0 and p(M) = L
  have hp_zero : p 0 = 0 := by simp [hp_def]
  have hp_M : p M = L := by simp [hp_def, ha_def]; field_simp
  -- g is interval integrable on each subinterval
  have hint : ∀ k < M, IntervalIntegrable g MeasureTheory.volume (p k) (p (k + 1)) :=
    fun k _ => hg.intervalIntegrable _ _
  -- Split the integral: ∫₀ᴸ g = ∑_k ∫_{p(k)}^{p(k+1)} g
  have h_split : ∫ x in (0 : ℝ)..L, g x =
      ∑ k ∈ Finset.range M, ∫ x in p k..p (k + 1), g x := by
    rw [← hp_zero, ← hp_M]
    exact (intervalIntegral.sum_integral_adjacent_intervals hint).symm
  -- The Riemann sum: ∑_k a * g(p(k)) = ∑_k ∫_{p(k)}^{p(k+1)} g(p(k))
  have h_riemann : ∀ k ∈ Finset.range M,
      a * g (p k) = ∫ x in p k..p (k + 1), g (p k) := by
    intro k _
    rw [intervalIntegral.integral_const]
    -- (p (k+1) - p k) • g (p k) = a * g (p k), with smul = mul for ℝ
    simp only [hp_def, smul_eq_mul]; push_cast; ring
  -- Rewrite the Riemann sum as a Finset.range M sum
  have h_sum_eq : ∑ k ∈ Finset.range (N + 1),
      (L / (↑(N + 1) : ℝ)) * g (↑k * L / (↑(N + 1) : ℝ)) =
      ∑ k ∈ Finset.range M, (a * g (p k)) := by
    congr 1; ext k; simp only [ha_def, hp_def, hM_def]; ring_nf
  -- Step 5: Bound the error
  rw [Real.dist_eq, h_sum_eq, h_split]
  -- |∑_k (∫ g(p(k)) - ∫ g)| = |∑_k ∫ (g(p(k)) - g)|
  -- Rewrite: sum - sum = sum of differences, then use h_riemann and integral_sub
  have h_rewrite : ∑ k ∈ Finset.range M, (a * g (p k)) -
      ∑ k ∈ Finset.range M, ∫ x in p k..p (k + 1), g x =
      ∑ k ∈ Finset.range M, ∫ x in p k..p (k + 1), (g (p k) - g x) := by
    rw [← Finset.sum_sub_distrib]
    apply Finset.sum_congr rfl
    intro k hk
    rw [h_riemann k hk, ← intervalIntegral.integral_sub
      (intervalIntegrable_const) (hg.intervalIntegrable _ _)]
  rw [h_rewrite]
  -- Bound: |∑_k ∫ (g(p(k)) - g(x)) dx| ≤ ∑_k |∫ (g(p(k)) - g(x)) dx|
  calc |∑ k ∈ Finset.range M, ∫ x in p k..p (k + 1), (g (p k) - g x)|
      ≤ ∑ k ∈ Finset.range M, |∫ x in p k..p (k + 1), (g (p k) - g x)| :=
        Finset.abs_sum_le_sum_abs _ _
    _ ≤ ∑ k ∈ Finset.range M, (ε / (2 * L) * |p (k + 1) - p k|) := by
        apply Finset.sum_le_sum
        intro k hk
        rw [← Real.norm_eq_abs]
        apply intervalIntegral.norm_integral_le_of_norm_le_const
        intro x hx
        -- x ∈ Ι (p k) (p (k+1)) = Set.uIoc (p k) (p (k+1))
        -- Since p k ≤ p (k+1), this is (p k, p (k+1)]
        rw [Set.uIoc_of_le (hp_mono k (k + 1) (Nat.le_succ k))] at hx
        -- ‖g(p(k)) - g(x)‖ ≤ ε/(2L) by uniform continuity
        rw [Real.norm_eq_abs]
        have hk_lt : k < M := Finset.mem_range.mp hk
        -- p(k) ∈ [0, L]
        have hpk_mem : p k ∈ Set.Icc 0 L := by
          constructor
          · exact mul_nonneg (Nat.cast_nonneg k) ha_pos.le
          · calc p k = (k : ℝ) * a := rfl
              _ ≤ (M : ℝ) * a := by
                  apply mul_le_mul_of_nonneg_right _ ha_pos.le
                  exact_mod_cast hk_lt.le
              _ = L := by simp [ha_def]; field_simp
        -- x ∈ [0, L]
        have hx_mem : x ∈ Set.Icc 0 L := by
          constructor
          · linarith [hx.1, mul_nonneg (Nat.cast_nonneg k) ha_pos.le]
          · calc x ≤ p (k + 1) := hx.2
              _ ≤ p M := hp_mono _ _ hk_lt
              _ = L := hp_M
        -- dist (p k) x < δ since both are in the same subinterval of width a < δ
        have hdist : dist (p k) x < δ := by
          rw [Real.dist_eq]
          have h1 : p k ≤ x := le_of_lt hx.1
          have h2 : x - p k ≤ a := by
            have : x ≤ p (k + 1) := hx.2
            have : p (k + 1) - p k = a := by simp [hp_def]; ring
            linarith
          rw [abs_of_nonpos (by linarith)]
          linarith
        rw [← Real.dist_eq]
        exact le_of_lt (hδ _ hpk_mem _ hx_mem hdist)
    _ = ε / 2 := by
        -- Each |p(k+1) - p(k)| = a, sum of M terms of (ε/(2L)) * a = ε/2
        simp only [hp_def]
        have : ∀ k ∈ Finset.range M,
            ε / (2 * L) * |(↑(k + 1) : ℝ) * a - (↑k : ℝ) * a| = ε / (2 * L) * a := by
          intro k _
          congr 1
          push_cast
          have : ((k : ℝ) + 1) * a - (k : ℝ) * a = a := by ring
          rw [this, abs_of_pos ha_pos]
        rw [Finset.sum_congr rfl this, Finset.sum_const, Finset.card_range, nsmul_eq_mul]
        simp [ha_def, hM_def]
        field_simp
    _ < ε := by linarith

/-- **DFT coefficient converges to Fourier coefficient.**

For each fixed mode m, as N → ∞ the lattice DFT coefficient of the restricted
test function converges to the continuum DMS Fourier coefficient.
This is Riemann sum convergence for `∫ f(x) φ_m(x) dx`. -/
theorem latticeDFTCoeff1d_tendsto
    (f : SmoothMap_Circle L ℝ) (m : ℕ) :
    Tendsto (fun N : ℕ => latticeDFTCoeff1d L (N + 1) f m)
      atTop (nhds (DyninMityaginSpace.coeff m f)) := by
  -- DyninMityaginSpace.coeff m f = fourierCoeffReal m f = ∫ x in Icc 0 L, f x * φ_m x
  -- This follows by unfolding the chain:
  --   coeff m := (coeffCLM m).comp(equiv) → coeffCLM m (equiv f)
  --   → (equiv f).val m → (toRapidDecay f).val m → fourierCoeffReal m f
  have hcoeff : DyninMityaginSpace.coeff m f =
      ∫ x in Set.Icc 0 L, f x * SmoothMap_Circle.fourierBasisFun (L := L) m x := by
    show ((RapidDecaySeq.coeffCLM m).comp
      (SmoothMap_Circle.smoothCircleRapidDecayEquiv (L := L)).toContinuousLinearMap) f =
      ∫ x in Set.Icc 0 L, f x * SmoothMap_Circle.fourierBasisFun (L := L) m x
    simp [RapidDecaySeq.coeffCLM, RapidDecaySeq.coeffLM,
      SmoothMap_Circle.smoothCircleRapidDecayEquiv,
      SmoothMap_Circle.toRapidDecayLM, SmoothMap_Circle.toRapidDecay,
      SmoothMap_Circle.fourierCoeffReal]
  rw [hcoeff]
  set g := fun x => f x * SmoothMap_Circle.fourierBasisFun (L := L) m x
    with hg_def
  have hg_cont : Continuous g :=
    f.continuous.mul
      (SmoothMap_Circle.fourierBasisFun_smooth m).continuous
  have hg_per : Function.Periodic g L := fun x => by
    simp only [hg_def]
    rw [f.periodic x, SmoothMap_Circle.fourierBasisFun_periodic m x]
  apply (riemann_sum_periodic_tendsto L g hg_cont hg_per).congr'
  apply Filter.eventually_atTop.mpr
  refine ⟨m, fun N hN => ?_⟩
  have hm : m < N + 1 := by omega
  -- Beta-reduce the lambda applications
  show ∑ k ∈ Finset.range (N + 1), L / ↑(N + 1) * g (↑k * L / ↑(N + 1)) =
    latticeDFTCoeff1d L (N + 1) f m
  rw [latticeDFTCoeff1d_eq_riemann_sum L (N + 1) f m hm]
  -- Now: Finset.range sum of g = ZMod sum of f * fourierBasisFun at circlePoint
  -- Convert ZMod sum to Finset.range sum via Fin
  symm; rw [← Fin.sum_univ_eq_sum_range]
  congr 1


end GaussianField

end
