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

# Lattice GFF as Pushforward of the Standard Multivariate Gaussian

The lattice Gaussian Free Field (`latticeGaussianMeasure d N a mass ha hmass`)
is a centered Gaussian on `Configuration (FinLatticeField d N)` with
covariance kernel `(1/a^d) M_a^{-1}` (Glimm-Jaffe-aligned). The
covariance operator has spectral decomposition

  `C = ∑ k, λ_k · (e_k ⊗ e_k)`

where `(e_k)` are the orthonormal eigenvectors
(`massEigenvectorBasis`) and `λ_k > 0` are the eigenvalues
(`massEigenvalues`). Defining

  `ξ_k(ω) := ω(e_k) / √(λ_k)`

makes the `(ξ_k)` an i.i.d. standard `N(0,1)` family. Equivalently,
the pushforward of `latticeGaussianMeasure` by the orthogonalization
map `gffOrthonormalProj` is the standard multivariate Gaussian
`Measure.pi (fun _ => gaussianReal 0 1)` on `FinLatticeSites d N → ℝ`.

This is the change-of-variables that makes the abstract polynomial-chaos
concentration theorem (`MarkovSemigroups.Gaussian.PolynomialChaosConcentration`,
upstream Janson Theorem 5.10) directly applicable to the lattice GFF.
Wick polynomials in the GFF correspond to multivariate Hermite
polynomials in the orthogonalized variables (proved in
`GaussianField/WickMultivariate.lean`).

## Main definitions

- `gffOrthonormalCoord` — the k-th orthogonalized coordinate
  `ω ↦ ω(e_k) / √(λ_k)`.
- `gffOrthonormalProj` — bundled into a vector-valued map.

## Main theorems

- `gffOrthonormalCoord_normal` — each `ξ_k` is standard `N(0,1)` under
  the lattice GFF.
- `gffOrthonormalCoord_independent` — distinct `ξ_k` are independent.
- `gffOrthonormalProj_pushforward_eq_stdGaussian` — the pushforward
  measure equals the product Gaussian.
- `gffOrthonormalProj_eq_charFun` — characteristic-function form,
  closer to the existing `Density.lean` infrastructure.

## References

- S. Janson, *Gaussian Hilbert Spaces*, Cambridge (1997), §1.3
  (orthogonal expansion of a Gaussian Hilbert space).

## Status

API + axiom skeleton (2026-05-08). Definitions are concrete; the four
main theorems are stated as axioms with proof-strategy docstrings. The
primary tool will be the existing `latticeGaussianFieldLaw_fourier`
in `GaussianField/Density.lean` plus characteristic-function uniqueness
(`MeasureTheory.Measure.ext_of_charFunDual`).
-/

import GaussianField.Density
import GaussianField.IsGaussian
import Lattice.SpectralCovariance
import Lattice.Covariance
import Mathlib.Probability.Distributions.Gaussian.Real
import Mathlib.Probability.Distributions.Gaussian.HasGaussianLaw.Independence
import Mathlib.MeasureTheory.Constructions.Pi

noncomputable section

namespace GaussianField

open MeasureTheory ProbabilityTheory

variable (d N : ℕ) [NeZero N]

/-- The k-th orthogonalized field coordinate:
`ξ_k(ω) := ω(e_k) · √(a^d · λ_k)`,
where `e_k = massEigenvectorBasis d N a mass k` and
`λ_k = massEigenvalues d N a mass k > 0`. As a function of `ω`, this
is linear, continuous, and (under the lattice GFF measure) a standard
`N(0,1)` random variable.

The GJ-aligned variance is `Var(ω(e_k)) = (a^d λ_k)⁻¹` (since
`T_GJ(e_k) = (a^d λ_k)^{-1/2} e_k`, see
`lattice_covariance_GJ_eq_spectral` in `Lattice/Covariance.lean`),
so the multiplier `√(a^d λ_k)` rescales to unit variance. -/
noncomputable def gffOrthonormalCoord
    (a mass : ℝ) (_ha : 0 < a) (_hmass : 0 < mass)
    (k : FinLatticeSites d N) :
    Configuration (FinLatticeField d N) → ℝ :=
  fun ω =>
    ω (fun x => (massEigenvectorBasis d N a mass k : EuclideanSpace ℝ _) x) *
      Real.sqrt (a ^ d * massEigenvalues d N a mass k)

/-- The bundled orthogonalization map: takes a configuration to the
vector of its orthogonalized coordinates indexed by lattice sites
(equivalently, by eigenvalue indices, since `massEigenvectorBasis` is
indexed by `FinLatticeSites d N`). -/
noncomputable def gffOrthonormalProj
    (a mass : ℝ) (ha : 0 < a) (hmass : 0 < mass) :
    Configuration (FinLatticeField d N) → (FinLatticeSites d N → ℝ) :=
  fun ω k => gffOrthonormalCoord d N a mass ha hmass k ω

/-! ## Variance computation

The k-th eigenvector `e_k` has GJ-covariance
`⟨T_GJ(e_k), T_GJ(e_k)⟩ = (a^d λ_k)⁻¹`. Combined with `pairing_is_gaussian`
this gives `Var(ω(e_k)) = (a^d λ_k)⁻¹`, so multiplying by `√(a^d λ_k)`
rescales to unit variance. -/

/-- The GJ-covariance of the k-th eigenvector with itself is `(a^d λ_k)⁻¹`. -/
theorem latticeCovarianceGJ_eigenvector_inner_self
    (a mass : ℝ) (ha : 0 < a) (hmass : 0 < mass)
    (k : FinLatticeSites d N) :
    let e_k : FinLatticeField d N :=
      fun x => (massEigenvectorBasis d N a mass k : EuclideanSpace ℝ _) x
    @inner ℝ ell2' _
        (latticeCovarianceGJ d N a mass ha hmass e_k)
        (latticeCovarianceGJ d N a mass ha hmass e_k) =
      (a^d * massEigenvalues d N a mass k)⁻¹ := by
  intro e_k
  -- This is `covariance(latticeCovarianceGJ) e_k e_k`.
  show GaussianField.covariance (latticeCovarianceGJ d N a mass ha hmass) e_k e_k =
    (a^d * massEigenvalues d N a mass k)⁻¹
  rw [lattice_covariance_GJ_eq_spectral d N a mass ha hmass e_k e_k]
  -- Goal: (a^d)⁻¹ * Σ_j λ_j⁻¹ · c_j(e_k)² = (a^d λ_k)⁻¹
  -- where c_j(e_k) = Σ_x v_j(x) · v_k(x) = ⟨v_j, v_k⟩ = δ_{jk}.
  have h_inner : ∀ j : FinLatticeSites d N,
      (∑ x, (massEigenvectorBasis d N a mass j : EuclideanSpace ℝ _) x * e_k x) =
      if j = k then (1 : ℝ) else 0 := by
    intro j
    -- This sum is `inner ℝ v_j v_k` in EuclideanSpace, which orthonormality
    -- gives as the indicator. Convert via dotProduct (mirroring the pattern
    -- in `massEigenbasis_sum_mul_sum_eq_site_inner`).
    have h_orth :
        @inner ℝ (EuclideanSpace ℝ (FinLatticeSites d N)) _
          (massEigenvectorBasis d N a mass j)
          (massEigenvectorBasis d N a mass k) =
        if j = k then (1 : ℝ) else 0 :=
      orthonormal_iff_ite.mp (massEigenvectorBasis d N a mass).orthonormal j k
    have h_eq :
        (∑ x, (massEigenvectorBasis d N a mass j : EuclideanSpace ℝ _) x * e_k x) =
        @inner ℝ (EuclideanSpace ℝ (FinLatticeSites d N)) _
          (massEigenvectorBasis d N a mass j)
          (massEigenvectorBasis d N a mass k) := by
      change _ =
        ((massEigenvectorBasis d N a mass k).ofLp ⬝ᵥ
          star (massEigenvectorBasis d N a mass j).ofLp)
      simp [dotProduct, star_trivial, e_k, mul_comm]
    rw [h_eq]; exact h_orth
  rw [show (∑ j : FinLatticeSites d N,
        (massEigenvalues d N a mass j)⁻¹ *
          (∑ x, (massEigenvectorBasis d N a mass j : EuclideanSpace ℝ _) x * e_k x) *
          (∑ x, (massEigenvectorBasis d N a mass j : EuclideanSpace ℝ _) x * e_k x)) =
      (massEigenvalues d N a mass k)⁻¹ from by
    rw [Finset.sum_eq_single k]
    · rw [h_inner k, if_pos rfl]; ring
    · intro j _ hjk
      rw [h_inner j, if_neg hjk]; ring
    · intro h; exact (h (Finset.mem_univ _)).elim]
  ring

/-- **Each orthogonalized coordinate is standard Gaussian.**

Under `latticeGaussianMeasure d N a mass ha hmass`, the random variable
`ξ_k = gffOrthonormalCoord d N a mass ha hmass k` has distribution
`gaussianReal 0 1` (mean zero, variance one).

**Reference:** Janson §1.3.

**Proof:** Combine `pairing_is_gaussian` (the pushforward of `ω ↦ ω(e_k)`
is `gaussianReal 0 ⟨T(e_k), T(e_k)⟩.toNNReal`) with the variance helper
`latticeCovarianceGJ_eigenvector_inner_self` (which computes the variance
to be `(a^d λ_k)⁻¹`). The scaling by `√(a^d λ_k)` rescales the variance
to 1 via `gaussianReal_map_const_mul`. -/
theorem gffOrthonormalCoord_normal
    (a mass : ℝ) (ha : 0 < a) (hmass : 0 < mass)
    (k : FinLatticeSites d N) :
    Measure.map (gffOrthonormalCoord d N a mass ha hmass k)
      (latticeGaussianMeasure d N a mass ha hmass) =
    gaussianReal 0 1 := by
  -- Setup
  let e_k : FinLatticeField d N :=
    fun x => (massEigenvectorBasis d N a mass k : EuclideanSpace ℝ _) x
  let c : ℝ := Real.sqrt (a^d * massEigenvalues d N a mass k)
  have hev_pos : 0 < massEigenvalues d N a mass k :=
    massOperatorMatrix_eigenvalues_pos d N a mass ha hmass k
  have ha_d_pos : 0 < a^d := pow_pos ha d
  have h_prod_pos : 0 < a^d * massEigenvalues d N a mass k := mul_pos ha_d_pos hev_pos
  have hc_nn : 0 ≤ c := Real.sqrt_nonneg _
  have hc_sq : c^2 = a^d * massEigenvalues d N a mass k := by
    show (Real.sqrt (a^d * massEigenvalues d N a mass k))^2 = _
    rw [sq, Real.mul_self_sqrt h_prod_pos.le]
  -- Step 1: gffOrthonormalCoord k = (c * ·) ∘ (· e_k) (by commutativity)
  have h_fun_eq : (gffOrthonormalCoord d N a mass ha hmass k :
        Configuration (FinLatticeField d N) → ℝ) =
      (c * ·) ∘ (fun ω : Configuration (FinLatticeField d N) => ω e_k) := by
    funext ω
    show ω e_k * c = c * ω e_k
    ring
  -- Step 2: Measure.map composition
  have h_meas_mul : Measurable (fun x : ℝ => c * x) :=
    (continuous_const.mul continuous_id).measurable
  have h_meas_eval : Measurable (fun ω : Configuration (FinLatticeField d N) => ω e_k) :=
    configuration_eval_measurable e_k
  rw [h_fun_eq, ← Measure.map_map h_meas_mul h_meas_eval]
  -- Step 3: Apply pairing_is_gaussian
  unfold latticeGaussianMeasure
  rw [pairing_is_gaussian (latticeCovarianceGJ d N a mass ha hmass) e_k]
  -- Goal: Measure.map (c * ·) (gaussianReal 0 ⟨Te_k, Te_k⟩.toNNReal) = gaussianReal 0 1
  rw [latticeCovarianceGJ_eigenvector_inner_self d N a mass ha hmass k]
  -- Goal: Measure.map (c * ·) (gaussianReal 0 ((a^d λ_k)⁻¹).toNNReal) = gaussianReal 0 1
  rw [gaussianReal_map_const_mul]
  -- Goal: gaussianReal (c * 0) (⟨c², _⟩ * ((a^d λ_k)⁻¹).toNNReal) = gaussianReal 0 1
  congr 1
  · ring
  · -- ⟨c², _⟩ * (a^d λ_k)⁻¹.toNNReal = 1 in ℝ≥0
    apply NNReal.eq
    push_cast
    rw [hc_sq, Real.coe_toNNReal _ (inv_nonneg.mpr h_prod_pos.le)]
    rw [mul_inv_cancel₀ h_prod_pos.ne']

/-! ## Off-diagonal eigenvector covariance and the joint-Gaussianness CLM

The pieces needed to discharge `gffOrthonormalCoord_independent`:
* The off-diagonal companion of `latticeCovarianceGJ_eigenvector_inner_self`,
  giving `⟨T_GJ(e_i), T_GJ(e_j)⟩ = 0` for `i ≠ j` by orthonormality of
  the eigenvector basis.
* A continuous-linear bundling `gffOrthonormalProjCLM` of the projection
  `gffOrthonormalProj`, used to upgrade `IsGaussian (latticeGaussianMeasure)`
  to `IsGaussian (latticeGaussianMeasure.map gffOrthonormalProj)` via
  `isGaussian_map_of_measurable`. -/

/-- The GJ-covariance of two distinct eigenvectors is zero. -/
theorem latticeCovarianceGJ_eigenvector_inner_off
    (a mass : ℝ) (ha : 0 < a) (hmass : 0 < mass)
    (i j : FinLatticeSites d N) (hij : i ≠ j) :
    let e_i : FinLatticeField d N :=
      fun x => (massEigenvectorBasis d N a mass i : EuclideanSpace ℝ _) x
    let e_j : FinLatticeField d N :=
      fun x => (massEigenvectorBasis d N a mass j : EuclideanSpace ℝ _) x
    @inner ℝ ell2' _
        (latticeCovarianceGJ d N a mass ha hmass e_i)
        (latticeCovarianceGJ d N a mass ha hmass e_j) =
      0 := by
  intro e_i e_j
  show GaussianField.covariance (latticeCovarianceGJ d N a mass ha hmass) e_i e_j = 0
  rw [lattice_covariance_GJ_eq_spectral d N a mass ha hmass e_i e_j]
  -- Goal: (a^d)⁻¹ * Σ_k λ_k⁻¹ · c_k(e_i) · c_k(e_j) = 0
  -- where c_k(e_i) = δ_{ki}, c_k(e_j) = δ_{kj}; product is 0 since i ≠ j.
  have h_inner : ∀ k l : FinLatticeSites d N,
      (∑ x, (massEigenvectorBasis d N a mass k : EuclideanSpace ℝ _) x *
        (massEigenvectorBasis d N a mass l : EuclideanSpace ℝ _) x) =
      if k = l then (1 : ℝ) else 0 := by
    intro k l
    have h_orth :
        @inner ℝ (EuclideanSpace ℝ (FinLatticeSites d N)) _
          (massEigenvectorBasis d N a mass k)
          (massEigenvectorBasis d N a mass l) =
        if k = l then (1 : ℝ) else 0 :=
      orthonormal_iff_ite.mp (massEigenvectorBasis d N a mass).orthonormal k l
    have h_eq :
        (∑ x, (massEigenvectorBasis d N a mass k : EuclideanSpace ℝ _) x *
          (massEigenvectorBasis d N a mass l : EuclideanSpace ℝ _) x) =
        @inner ℝ (EuclideanSpace ℝ (FinLatticeSites d N)) _
          (massEigenvectorBasis d N a mass k)
          (massEigenvectorBasis d N a mass l) := by
      change _ =
        ((massEigenvectorBasis d N a mass l).ofLp ⬝ᵥ
          star (massEigenvectorBasis d N a mass k).ofLp)
      simp [dotProduct, star_trivial, mul_comm]
    rw [h_eq]; exact h_orth
  -- Each summand has factor c_k(e_i) · c_k(e_j) = δ_{ki} · δ_{kj}.
  -- Since i ≠ j, this is 0 for every k (k = i forces δ_{kj} = 0; k ≠ i forces δ_{ki} = 0).
  rw [show (∑ k : FinLatticeSites d N,
        (massEigenvalues d N a mass k)⁻¹ *
          (∑ x, (massEigenvectorBasis d N a mass k : EuclideanSpace ℝ _) x * e_i x) *
          (∑ x, (massEigenvectorBasis d N a mass k : EuclideanSpace ℝ _) x * e_j x)) =
      (0 : ℝ) from ?_]
  · ring
  apply Finset.sum_eq_zero
  intro k _
  rw [h_inner k i, h_inner k j]
  by_cases hki : k = i
  · -- k = i, so by i ≠ j, k ≠ j, second indicator is 0
    rw [if_neg (hki ▸ hij : k ≠ j)]; ring
  · rw [if_neg hki]; ring

/-- Evaluation of a configuration at a fixed test function, packaged as
a continuous linear map `Configuration E →L[ℝ] ℝ`. Continuity is
`WeakDual.eval_continuous`; linearity is the topological-dual pairing. -/
private noncomputable def gffEvalCLM (f : FinLatticeField d N) :
    Configuration (FinLatticeField d N) →L[ℝ] ℝ :=
  ⟨(topDualPairing ℝ (FinLatticeField d N)).flip f, WeakDual.eval_continuous f⟩

omit [NeZero N] in
@[simp] private lemma gffEvalCLM_apply (f : FinLatticeField d N)
    (ω : Configuration (FinLatticeField d N)) :
    gffEvalCLM d N f ω = ω f := rfl

/-- The orthogonalization map `ω ↦ (k ↦ ω(e_k) · √(a^d λ_k))` packaged
as a continuous linear map. Used to discharge joint Gaussianness in
`gffOrthonormalCoord_independent`. -/
private noncomputable def gffOrthonormalProjCLM
    (a mass : ℝ) (_ha : 0 < a) (_hmass : 0 < mass) :
    Configuration (FinLatticeField d N) →L[ℝ] (FinLatticeSites d N → ℝ) :=
  ContinuousLinearMap.pi fun k =>
    Real.sqrt (a^d * massEigenvalues d N a mass k) •
      gffEvalCLM d N
        (fun x => (massEigenvectorBasis d N a mass k : EuclideanSpace ℝ _) x)

private lemma gffOrthonormalProjCLM_eq
    (a mass : ℝ) (ha : 0 < a) (hmass : 0 < mass) :
    ((gffOrthonormalProjCLM d N a mass ha hmass) :
        Configuration (FinLatticeField d N) → FinLatticeSites d N → ℝ) =
      gffOrthonormalProj d N a mass ha hmass := by
  funext ω k
  show Real.sqrt (a^d * massEigenvalues d N a mass k) *
      ω (fun x => (massEigenvectorBasis d N a mass k : EuclideanSpace ℝ _) x) =
    ω (fun x => (massEigenvectorBasis d N a mass k : EuclideanSpace ℝ _) x) *
      Real.sqrt (a^d * massEigenvalues d N a mass k)
  ring

lemma gffOrthonormalProj_measurable
    (a mass : ℝ) (ha : 0 < a) (hmass : 0 < mass) :
    Measurable (gffOrthonormalProj d N a mass ha hmass) := by
  apply measurable_pi_iff.mpr
  intro k
  exact (configuration_eval_measurable _).mul_const _

/-- The lattice GFF (in GJ normalization) is Gaussian on `Configuration`
in Mathlib's sense. Bridges the `latticeGaussianMeasure` definition to
`GaussianField.measure_isGaussian`. -/
instance latticeGaussianMeasure_isGaussian
    (a mass : ℝ) (ha : 0 < a) (hmass : 0 < mass) :
    IsGaussian (latticeGaussianMeasure d N a mass ha hmass) :=
  GaussianField.measure_isGaussian _

private theorem gffOrthonormalProj_isGaussian
    (a mass : ℝ) (ha : 0 < a) (hmass : 0 < mass) :
    IsGaussian ((latticeGaussianMeasure d N a mass ha hmass).map
      (gffOrthonormalProj d N a mass ha hmass)) := by
  rw [show gffOrthonormalProj d N a mass ha hmass =
      ((gffOrthonormalProjCLM d N a mass ha hmass) :
        Configuration (FinLatticeField d N) → FinLatticeSites d N → ℝ) from
      (gffOrthonormalProjCLM_eq d N a mass ha hmass).symm]
  exact isGaussian_map_of_measurable
    ((gffOrthonormalProjCLM_eq d N a mass ha hmass).symm ▸
      gffOrthonormalProj_measurable d N a mass ha hmass)

/-- **Distinct orthogonalized coordinates are independent.**

Under `latticeGaussianMeasure`, the family `(ξ_k)_{k ∈ FinLatticeSites d N}`
is mutually independent. Combined with `gffOrthonormalCoord_normal`,
this means the family is i.i.d. standard Gaussian.

**Reference:** Janson §1.4 (uncorrelated jointly Gaussian variables
are independent).

**Proof:** Joint Gaussianness comes from packaging the orthogonalization
map as a CLM (`gffOrthonormalProjCLM`) and pushing forward the lattice
GFF (which is Gaussian on `Configuration` via `measure_isGaussian`).
Pairwise covariances vanish on the eigenbasis by orthonormality
(`latticeCovarianceGJ_eigenvector_inner_off`). Mathlib's
`HasGaussianLaw.iIndepFun_of_covariance_eq_zero` then closes the goal. -/
theorem gffOrthonormalCoord_independent
    (a mass : ℝ) (ha : 0 < a) (hmass : 0 < mass) :
    iIndepFun (fun k : FinLatticeSites d N =>
      gffOrthonormalCoord d N a mass ha hmass k)
      (latticeGaussianMeasure d N a mass ha hmass) := by
  set μ := latticeGaussianMeasure d N a mass ha hmass with hμ
  -- Joint Gaussianness of the coordinate family.
  haveI hGauss : IsGaussian (μ.map (gffOrthonormalProj d N a mass ha hmass)) :=
    gffOrthonormalProj_isGaussian d N a mass ha hmass
  have hX : HasGaussianLaw
      (fun ω : Configuration (FinLatticeField d N) =>
        fun k => gffOrthonormalCoord d N a mass ha hmass k ω) μ := ⟨hGauss⟩
  -- Pairwise covariances vanish for distinct indices.
  refine hX.iIndepFun_of_covariance_eq_zero ?_
  intro i j hij
  -- Notation
  set e_i : FinLatticeField d N :=
    fun x => (massEigenvectorBasis d N a mass i : EuclideanSpace ℝ _) x with he_i
  set e_j : FinLatticeField d N :=
    fun x => (massEigenvectorBasis d N a mass j : EuclideanSpace ℝ _) x with he_j
  set c_i : ℝ := Real.sqrt (a^d * massEigenvalues d N a mass i) with hc_i
  set c_j : ℝ := Real.sqrt (a^d * massEigenvalues d N a mass j) with hc_j
  -- Both eigenvector pairings are centered (mean zero) under μ.
  have h_centered_i : μ[fun ω : Configuration (FinLatticeField d N) => ω e_i] = 0 := by
    show ∫ ω : Configuration (FinLatticeField d N), ω e_i ∂μ = 0
    exact measure_centered (latticeCovarianceGJ d N a mass ha hmass) e_i
  have h_centered_j : μ[fun ω : Configuration (FinLatticeField d N) => ω e_j] = 0 := by
    show ∫ ω : Configuration (FinLatticeField d N), ω e_j ∂μ = 0
    exact measure_centered (latticeCovarianceGJ d N a mass ha hmass) e_j
  -- L² integrability of each pairing.
  have hLp_i : MemLp (fun ω : Configuration (FinLatticeField d N) => ω e_i) 2 μ := by
    exact_mod_cast pairing_memLp (latticeCovarianceGJ d N a mass ha hmass) e_i 2
  have hLp_j : MemLp (fun ω : Configuration (FinLatticeField d N) => ω e_j) 2 μ := by
    exact_mod_cast pairing_memLp (latticeCovarianceGJ d N a mass ha hmass) e_j 2
  -- ξ_i = (· e_i) * c_i, ξ_j = (· e_j) * c_j, so cov is c_i * c_j * cov of evaluations.
  have h_cov_eval :
      cov[fun ω : Configuration (FinLatticeField d N) => ω e_i,
          fun ω : Configuration (FinLatticeField d N) => ω e_j; μ] =
        ∫ ω, (ω e_i) * (ω e_j) ∂μ := by
    rw [covariance_eq_sub hLp_i hLp_j, h_centered_i, h_centered_j, mul_zero, sub_zero]
    rfl
  have h_cross :
      ∫ ω : Configuration (FinLatticeField d N), (ω e_i) * (ω e_j) ∂μ = 0 := by
    rw [show μ = GaussianField.measure (latticeCovarianceGJ d N a mass ha hmass) from rfl]
    rw [cross_moment_eq_covariance (latticeCovarianceGJ d N a mass ha hmass) e_i e_j]
    have := latticeCovarianceGJ_eigenvector_inner_off d N a mass ha hmass i j hij
    show GaussianField.covariance (latticeCovarianceGJ d N a mass ha hmass) e_i e_j = 0
    exact this
  -- Pull out the constants.
  have h_cov_zero :
      cov[fun ω : Configuration (FinLatticeField d N) => ω e_i,
          fun ω : Configuration (FinLatticeField d N) => ω e_j; μ] = 0 := by
    rw [h_cov_eval, h_cross]
  -- Combine with the c_i, c_j multiplications.
  show cov[fun ω => ω e_i * c_i, fun ω => ω e_j * c_j; μ] = 0
  rw [covariance_mul_const_left, covariance_mul_const_right, h_cov_zero,
    zero_mul, zero_mul]

/-- **The pushforward of the lattice GFF under orthogonalization is
the standard multivariate Gaussian.**

  `Measure.map gffOrthonormalProj (latticeGaussianMeasure …) = Π_k gaussianReal 0 1`

**Proof:** Each marginal is `N(0,1)` (`gffOrthonormalCoord_normal`)
and the family is mutually independent (`gffOrthonormalCoord_independent`).
Mathlib's `iIndepFun_iff_map_fun_eq_pi_map` then identifies the
joint pushforward with the product of marginals. -/
theorem gffOrthonormalProj_pushforward_eq_stdGaussian
    (a mass : ℝ) (ha : 0 < a) (hmass : 0 < mass) :
    Measure.map (gffOrthonormalProj d N a mass ha hmass)
      (latticeGaussianMeasure d N a mass ha hmass) =
    Measure.pi (fun _ : FinLatticeSites d N => gaussianReal 0 1) := by
  set μ := latticeGaussianMeasure d N a mass ha hmass with hμ
  have h_indep := gffOrthonormalCoord_independent d N a mass ha hmass
  have h_AE : ∀ i : FinLatticeSites d N,
      AEMeasurable (gffOrthonormalCoord d N a mass ha hmass i) μ := by
    intro i
    exact ((configuration_eval_measurable _).mul_const _).aemeasurable
  have h_eq :=
    (iIndepFun_iff_map_fun_eq_pi_map (μ := μ)
      (f := fun i => gffOrthonormalCoord d N a mass ha hmass i) h_AE).mp h_indep
  -- gffOrthonormalProj is `fun ω i ↦ gffOrthonormalCoord ... i ω` by definition.
  rw [show gffOrthonormalProj d N a mass ha hmass =
      fun ω i => gffOrthonormalCoord d N a mass ha hmass i ω from rfl, h_eq]
  congr 1
  funext i
  exact gffOrthonormalCoord_normal d N a mass ha hmass i

/-- **Characteristic-functional form of the bridge** (alternative formulation).

The pushforward measure has the characteristic functional
`exp(-(1/2) ‖t‖²)` where the norm is the standard Euclidean norm on
`FinLatticeSites d N → ℝ`. Equivalent to
`gffOrthonormalProj_pushforward_eq_stdGaussian` by uniqueness of the
characteristic functional, but useful as a target form when proving
via `MeasureTheory.Measure.ext_of_charFunDual` (the same uniqueness
tool used in `GaussianField/Density.lean`). -/
theorem gffOrthonormalProj_charFun
    (a mass : ℝ) (ha : 0 < a) (hmass : 0 < mass)
    (t : FinLatticeSites d N → ℝ) :
    ∫ x : FinLatticeSites d N → ℝ,
      Complex.exp (Complex.I * ↑(∑ k, t k * x k))
        ∂(Measure.map (gffOrthonormalProj d N a mass ha hmass)
          (latticeGaussianMeasure d N a mass ha hmass)) =
    Complex.exp (-(1 / 2 : ℂ) * ↑(∑ k, t k ^ 2)) := by
  rw [gffOrthonormalProj_pushforward_eq_stdGaussian d N a mass ha hmass]
  -- Decompose the exponential of the sum into a product of exponentials.
  have h_decomp : ∀ x : FinLatticeSites d N → ℝ,
      Complex.exp (Complex.I * ↑(∑ k, t k * x k)) =
        ∏ k, Complex.exp (Complex.I * ↑(t k * x k)) := by
    intro x
    rw [show (↑(∑ k, t k * x k) : ℂ) = ∑ k, ((t k * x k : ℝ) : ℂ) from by push_cast; rfl]
    rw [Finset.mul_sum, Complex.exp_sum]
  simp_rw [h_decomp]
  rw [show
      ∫ x : FinLatticeSites d N → ℝ, ∏ k, Complex.exp (Complex.I * ↑(t k * x k))
        ∂(Measure.pi fun _ : FinLatticeSites d N => gaussianReal 0 1) =
      ∏ k : FinLatticeSites d N,
        ∫ x : ℝ, Complex.exp (Complex.I * ↑(t k * x)) ∂(gaussianReal 0 1) from
    integral_fintype_prod_eq_prod
      (f := fun k (x : ℝ) => Complex.exp (Complex.I * ↑(t k * x)))]
  -- Each factor is the characteristic function of N(0,1) at t k.
  have h_each : ∀ k : FinLatticeSites d N,
      ∫ x : ℝ, Complex.exp (Complex.I * ↑(t k * x)) ∂(gaussianReal 0 1) =
        Complex.exp (-((t k : ℂ) ^ 2) / 2) := by
    intro k
    have h_rewrite : ∀ x : ℝ,
        Complex.exp (Complex.I * ↑(t k * x)) =
          Complex.exp (↑(t k) * ↑x * Complex.I) := by
      intro x
      push_cast
      ring_nf
    simp_rw [h_rewrite]
    rw [← charFun_apply_real, charFun_gaussianReal]
    push_cast
    ring_nf
  simp_rw [h_each]
  rw [← Complex.exp_sum]
  congr 1
  push_cast
  rw [Finset.mul_sum]
  refine Finset.sum_congr rfl ?_
  intro k _
  ring

end GaussianField
