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

# Schwartz Tensor Product Isomorphism via Dimension Peeling

Proves `S(ℝ^{m+1}) ⊗̂ S(ℝ^{n+1}) ≃L S(ℝ^{m+n+2})` by peeling off one dimension
at a time. The key isomorphism `schwartzPeelOff` identifies
`S(ℝ^{d+2}) ≃L NuclearTensorProduct S(ℝ^{d+1}) S(ℝ)`,
and the general result follows by induction on `n`.

## Main definitions

- `RapidDecaySeq.reindex` — reindexing automorphism for polynomially-bounded permutations
- `NuclearTensorProduct.assoc` — associativity of the nuclear tensor product
- `schwartzPointwiseProduct` — product function `h(x,t) = f(x)·g(t)` as Schwartz map
- `schwartzPeelOff` — isomorphism `S(ℝ^{d+2}) ≃L S(ℝ^{d+1}) ⊗̂ S(ℝ)`
- `schwartzTensorEquiv` — isomorphism `S(ℝ^{m+1}) ⊗̂ S(ℝ^{n+1}) ≃L S(ℝ^{m+n+2})`

## References

- Gel'fand-Vilenkin, "Generalized Functions" Vol. 4, Ch. 3-4
- Trèves, "Topological Vector Spaces, Distributions and Kernels", Ch. 51
-/

import SchwartzNuclear.HermiteNuclear
import Nuclear.NuclearTensorProduct

noncomputable section

namespace GaussianField

open scoped NNReal
open MeasureTheory

/-! ## Step 1: Reindexing Automorphism for RapidDecaySeq -/

namespace RapidDecaySeq

/-- A permutation `σ : ℕ ≃ ℕ` is polynomially bounded if there exist `C > 0` and `k`
such that `1 + σ(n) ≤ C * (1 + n)^k` for all `n`. -/
def IsPolyBounded (σ : ℕ ≃ ℕ) : Prop :=
  ∃ C > (0 : ℝ), ∃ k : ℕ, ∀ n : ℕ, (1 + (σ n : ℝ)) ≤ C * (1 + (n : ℝ)) ^ k

/-- Reindexing `a.val ∘ σ` is rapidly decreasing when `σ⁻¹` is polynomially bounded.
Change of variables `n = σ m`: `∑_m |a(σ m)| (1+m)^k = ∑_n |a(n)| (1+σ⁻¹(n))^k`,
then bound `(1+σ⁻¹(n))^k ≤ (C(1+n)^p)^k` using the poly bound on `σ⁻¹`. -/
lemma reindex_rapid_decay (σ : ℕ ≃ ℕ) (hσ_inv : IsPolyBounded σ.symm)
    (a : RapidDecaySeq) (k : ℕ) :
    Summable (fun m => |a.val (σ m)| * (1 + (m : ℝ)) ^ k) := by
  obtain ⟨C, hC, p, hbound⟩ := hσ_inv
  -- Change of variables n = σ m: ∑_m f(σ m) = ∑_n f(n) with m = σ⁻¹(n)
  -- So |a(σ m)| (1+m)^k becomes |a(n)| (1+σ⁻¹(n))^k
  -- Bound: (1+σ⁻¹(n))^k ≤ (C(1+n)^p)^k = C^k (1+n)^{pk}
  set k' := p * k
  suffices h : Summable (fun n => |a.val n| * (1 + (σ.symm n : ℝ)) ^ k) by
    have : (fun m => |a.val (σ m)| * (1 + (m : ℝ)) ^ k) =
        (fun n => |a.val n| * (1 + (σ.symm n : ℝ)) ^ k) ∘ σ := by
      ext m; simp [Function.comp, Equiv.symm_apply_apply]
    rw [this]; exact σ.summable_iff.mpr h
  apply Summable.of_nonneg_of_le (fun _ => by positivity)
  · intro n
    calc |a.val n| * (1 + (σ.symm n : ℝ)) ^ k
        ≤ |a.val n| * (C * (1 + (n : ℝ)) ^ p) ^ k := by
          apply mul_le_mul_of_nonneg_left _ (abs_nonneg _)
          exact pow_le_pow_left₀ (by positivity) (hbound n) k
      _ = C ^ k * (|a.val n| * (1 + (n : ℝ)) ^ k') := by
          rw [mul_pow, ← pow_mul]; ring
  · exact (a.rapid_decay k').mul_left _

/-- Reindexing a rapidly decreasing sequence by a permutation with poly-bounded inverse. -/
def reindexVal (σ : ℕ ≃ ℕ) (hσ_inv : IsPolyBounded σ.symm) (a : RapidDecaySeq) :
    RapidDecaySeq where
  val := a.val ∘ σ
  rapid_decay k := reindex_rapid_decay σ hσ_inv a k

/-- The reindexing map is linear. -/
def reindexLM (σ : ℕ ≃ ℕ) (hσ_inv : IsPolyBounded σ.symm) :
    RapidDecaySeq →ₗ[ℝ] RapidDecaySeq where
  toFun := reindexVal σ hσ_inv
  map_add' a b := ext fun m => by simp [reindexVal, add_val]
  map_smul' r a := ext fun m => by simp [reindexVal, smul_val]

/-- Seminorm bound for the reindexing map. For each target seminorm `k`,
the reindexed sequence is bounded by a source seminorm (at a potentially higher index). -/
private lemma reindexLM_isBounded (σ : ℕ ≃ ℕ)
    (hσ_inv : IsPolyBounded σ.symm) :
    Seminorm.IsBounded rapidDecaySeminorm rapidDecaySeminorm (reindexLM σ hσ_inv) := by
  intro k
  obtain ⟨C, hC, p, hbound⟩ := hσ_inv
  -- We need: ∑ |a(σ m)| (1+m)^k ≤ D * ∑ |a n| (1+n)^{k'}
  -- Change of variables: m = σ.symm n, so ∑_m = ∑_n
  -- and (1+m)^k = (1 + σ.symm n)^k ≤ (C(1+n)^p)^k = C^k (1+n)^{pk}
  set k' := p * k
  refine ⟨{k'}, ⟨C ^ k, by positivity⟩, fun a => ?_⟩
  simp only [Seminorm.comp_apply, Finset.sup_singleton]
  show ∑' m, |a.val (σ m)| * (1 + (m : ℝ)) ^ k ≤
    C ^ k * (∑' n, |a.val n| * (1 + (n : ℝ)) ^ k')
  -- Change of variables n = σ m (so m = σ⁻¹ n):
  -- ∑_m |a(σ m)| (1+m)^k = ∑_n |a(n)| (1+σ⁻¹(n))^k
  -- Then bound (1+σ⁻¹(n))^k ≤ (C(1+n)^p)^k = C^k (1+n)^{pk}
  -- Step 1: Change of variables via σ bijection
  set f : ℕ → ℝ := fun n => |a.val n| * (1 + (σ.symm n : ℝ)) ^ k
  have h_cov : ∑' m, |a.val (σ m)| * (1 + (m : ℝ)) ^ k = ∑' n, f n := by
    have : ∀ m, |a.val (σ m)| * (1 + (m : ℝ)) ^ k = f (σ m) :=
      fun m => by simp [f, Equiv.symm_apply_apply]
    simp_rw [this]; exact Equiv.tsum_eq σ f
  rw [h_cov]
  -- Step 2: Pointwise bound
  have h_pw : ∀ n, |a.val n| * (1 + (σ.symm n : ℝ)) ^ k ≤
      C ^ k * (|a.val n| * (1 + (n : ℝ)) ^ k') := fun n =>
    calc |a.val n| * (1 + (σ.symm n : ℝ)) ^ k
        ≤ |a.val n| * (C * (1 + (n : ℝ)) ^ p) ^ k := by
          apply mul_le_mul_of_nonneg_left _ (abs_nonneg _)
          exact pow_le_pow_left₀ (by positivity) (hbound n) k
      _ = C ^ k * (|a.val n| * (1 + (n : ℝ)) ^ k') := by
          rw [mul_pow, ← pow_mul]; ring
  have h_summ_f : Summable f := Summable.of_nonneg_of_le
    (fun _ => by positivity [f]) h_pw ((a.rapid_decay k').mul_left _)
  calc ∑' n, f n
      ≤ ∑' n, C ^ k * (|a.val n| * (1 + (n : ℝ)) ^ k') :=
        h_summ_f.tsum_le_tsum h_pw ((a.rapid_decay k').mul_left _)
    _ = C ^ k * ∑' n, |a.val n| * (1 + (n : ℝ)) ^ k' := tsum_mul_left

/-- The reindexing continuous linear map. -/
def reindexCLM (σ : ℕ ≃ ℕ) (hσ_inv : IsPolyBounded σ.symm) :
    RapidDecaySeq →L[ℝ] RapidDecaySeq where
  toLinearMap := reindexLM σ hσ_inv
  cont := WithSeminorms.continuous_of_isBounded
    rapidDecay_withSeminorms rapidDecay_withSeminorms
    _ (reindexLM_isBounded σ hσ_inv)

/-- The reindexing continuous linear equivalence.
Given a polynomially bounded permutation `σ` (with polynomially bounded inverse),
produces an automorphism of `RapidDecaySeq`. -/
def reindex (σ : ℕ ≃ ℕ) (hσ : IsPolyBounded σ) (hσ_inv : IsPolyBounded σ.symm) :
    RapidDecaySeq ≃L[ℝ] RapidDecaySeq :=
  ContinuousLinearEquiv.equivOfInverse
    (reindexCLM σ hσ_inv)
    (reindexCLM σ.symm (by rwa [Equiv.symm_symm]))
    (fun a => ext fun m => by simp [reindexCLM, reindexLM, reindexVal])
    (fun a => ext fun m => by simp [reindexCLM, reindexLM, reindexVal])

@[simp] lemma reindex_val (σ : ℕ ≃ ℕ) (hσ : IsPolyBounded σ)
    (hσ_inv : IsPolyBounded σ.symm) (a : RapidDecaySeq) (m : ℕ) :
    (reindex σ hσ hσ_inv a).val m = a.val (σ m) := rfl

end RapidDecaySeq

/-! ## Step 2: Tensor Product Associativity -/

namespace NuclearTensorProduct

/-- The associativity permutation for Cantor pairing.
Maps the "left-associated" index `pair(pair(i,j), k)` to the
"right-associated" index `pair(i, pair(j,k))`. -/
private def assocPerm : ℕ ≃ ℕ :=
  -- ℕ → ℕ×ℕ → (ℕ×ℕ)×ℕ → ℕ×(ℕ×ℕ) → ℕ
  Nat.pairEquiv.symm.trans <|
    (Equiv.prodCongr Nat.pairEquiv.symm (Equiv.refl ℕ)).trans <|
    (Equiv.prodAssoc ℕ ℕ ℕ).trans <|
    (Equiv.prodCongr (Equiv.refl ℕ) Nat.pairEquiv).trans <|
    Nat.pairEquiv

/-- Cantor pairing bound: `1 + pair(i,j) ≤ (2(1+i)(1+j))²`.
Reproduced from NuclearTensorProduct (where it is private). -/
private lemma one_add_pair_le_sq' (i j : ℕ) :
    (1 + (Nat.pair i j : ℝ)) ≤ (2 * (1 + (i : ℝ)) * (1 + (j : ℝ))) ^ 2 := by
  have hi : (0 : ℝ) ≤ i := Nat.cast_nonneg i
  have hj : (0 : ℝ) ≤ j := Nat.cast_nonneg j
  have h_pair : (Nat.pair i j : ℝ) ≤ ((i : ℝ) + j + 1) ^ 2 := by
    exact_mod_cast nat_pair_bound i j
  calc (1 : ℝ) + Nat.pair i j
      ≤ 1 + (i + j + 1) ^ 2 := by linarith
    _ ≤ (i + j + 2) ^ 2 := by nlinarith
    _ ≤ (2 * (1 + i) * (1 + j)) ^ 2 := by
        exact pow_le_pow_left₀ (by positivity) (by nlinarith) _

/-- The associativity permutation is polynomially bounded.
Components satisfy `i, j, k ≤ n`, so `pair(i, pair(j,k)) ≤ poly(n)`. -/
private lemma assocPerm_polyBounded : RapidDecaySeq.IsPolyBounded assocPerm := by
  -- pair(a,b) ≤ (2(1+a)(1+b))^2, so double pairing gives degree ≤ 10
  refine ⟨64, by positivity, 10, fun n => ?_⟩
  simp only [assocPerm, Equiv.trans_apply, Equiv.prodCongr_apply, Equiv.prodAssoc_apply,
    Nat.pairEquiv]
  set p := Nat.unpair n
  set q := Nat.unpair p.1
  have hp1 := Nat.unpair_left_le n
  have hk := Nat.unpair_right_le n
  have hi_le : q.1 ≤ n := le_trans (Nat.unpair_left_le p.1) hp1
  have hj_le : q.2 ≤ n := le_trans (Nat.unpair_right_le p.1) hp1
  have hk_le : p.2 ≤ n := hk
  -- pair(j,k) ≤ (2(1+j)(1+k))^2 ≤ (2(1+n)^2)^2 = 4(1+n)^4
  have hjk : (1 + (Nat.pair q.2 p.2 : ℝ)) ≤ (2 * (1 + (n : ℝ)) * (1 + (n : ℝ))) ^ 2 := by
    calc (1 + (Nat.pair q.2 p.2 : ℝ))
        ≤ (2 * (1 + (q.2 : ℝ)) * (1 + (p.2 : ℝ))) ^ 2 := one_add_pair_le_sq' q.2 p.2
      _ ≤ (2 * (1 + (n : ℝ)) * (1 + (n : ℝ))) ^ 2 := by
          apply pow_le_pow_left₀ (by positivity)
          have hj_n : (q.2 : ℝ) ≤ (n : ℝ) := Nat.cast_le.mpr hj_le
          have hk_n : (p.2 : ℝ) ≤ (n : ℝ) := Nat.cast_le.mpr hk_le
          nlinarith
  -- pair(i, pair(j,k)) ≤ (2(1+i)(1+pair(j,k)))^2 ≤ (2(1+n) · 4(1+n)^4)^2
  calc (1 + (Nat.pair q.1 (Nat.pair q.2 p.2) : ℝ))
      ≤ (2 * (1 + (q.1 : ℝ)) * (1 + (Nat.pair q.2 p.2 : ℝ))) ^ 2 :=
        one_add_pair_le_sq' q.1 (Nat.pair q.2 p.2)
    _ ≤ (2 * (1 + (n : ℝ)) * (2 * (1 + (n : ℝ)) * (1 + (n : ℝ))) ^ 2) ^ 2 := by
        apply pow_le_pow_left₀ (by positivity)
        -- Goal: 2*(1+q.1)*(1+pair(q.2,p.2)) ≤ 2*(1+n)*(2*(1+n)*(1+n))^2
        have h1 : (1 + (q.1 : ℝ)) ≤ (1 + (n : ℝ)) := by
          have := (Nat.cast_le (α := ℝ)).mpr hi_le; linarith
        calc 2 * (1 + (q.1 : ℝ)) * (1 + (Nat.pair q.2 p.2 : ℝ))
            ≤ 2 * (1 + (n : ℝ)) * (1 + (Nat.pair q.2 p.2 : ℝ)) := by
              apply mul_le_mul_of_nonneg_right _ (by positivity)
              linarith
          _ ≤ 2 * (1 + (n : ℝ)) * (2 * (1 + (n : ℝ)) * (1 + (n : ℝ))) ^ 2 := by
              apply mul_le_mul_of_nonneg_left hjk (by positivity)
    _ = 64 * (1 + (n : ℝ)) ^ 10 := by ring

/-- The inverse associativity permutation is polynomially bounded.
Symmetric to `assocPerm_polyBounded`: components are ≤ n, double Cantor pairing
gives polynomial growth. -/
private lemma assocPerm_symm_polyBounded : RapidDecaySeq.IsPolyBounded assocPerm.symm := by
  -- assocPerm.symm maps pair(i, pair(j,k)) to pair(pair(i,j), k)
  -- All components i,j,k ≤ n, so the result is polynomially bounded
  refine ⟨64, by positivity, 10, fun n => ?_⟩
  -- assocPerm.symm n: unpair n to get (i, pair(j,k)), unpair the second to get (j,k),
  -- then pair(pair(i,j), k)
  -- All three components i,j,k ≤ n
  set p := Nat.unpair n
  set r := Nat.unpair p.2
  have hi_le : p.1 ≤ n := Nat.unpair_left_le n
  have hjk_le : p.2 ≤ n := Nat.unpair_right_le n
  have hj_le : r.1 ≤ n := le_trans (Nat.unpair_left_le p.2) hjk_le
  have hk_le : r.2 ≤ n := le_trans (Nat.unpair_right_le p.2) hjk_le
  -- Need to show 1 + assocPerm.symm n ≤ 64 * (1+n)^10
  -- assocPerm.symm n = pair(pair(p.1, r.1), r.2)
  -- pair(p.1, r.1) ≤ (2(1+p.1)(1+r.1))^2 ≤ (2(1+n)^2)^2 = 4(1+n)^4
  -- pair(pair(p.1,r.1), r.2) ≤ (2(1+pair(p.1,r.1))(1+r.2))^2 ≤ (2·4(1+n)^4·(1+n))^2 = 64(1+n)^10
  simp only [assocPerm, Equiv.symm_trans_apply, Equiv.prodCongr_symm, Equiv.refl_symm,
    Equiv.prodCongr_apply, Nat.pairEquiv]
  -- After simp, the goal should involve pair(pair(p.1,r.1), r.2)
  have hij : (1 + (Nat.pair p.1 r.1 : ℝ)) ≤ (2 * (1 + (n : ℝ)) * (1 + (n : ℝ))) ^ 2 := by
    calc (1 + (Nat.pair p.1 r.1 : ℝ))
        ≤ (2 * (1 + (p.1 : ℝ)) * (1 + (r.1 : ℝ))) ^ 2 := one_add_pair_le_sq' p.1 r.1
      _ ≤ (2 * (1 + (n : ℝ)) * (1 + (n : ℝ))) ^ 2 := by
          apply pow_le_pow_left₀ (by positivity)
          have hi_n : (p.1 : ℝ) ≤ (n : ℝ) := Nat.cast_le.mpr hi_le
          have hj_n : (r.1 : ℝ) ≤ (n : ℝ) := Nat.cast_le.mpr hj_le
          nlinarith
  calc (1 + (Nat.pair (Nat.pair p.1 r.1) r.2 : ℝ))
      ≤ (2 * (1 + (Nat.pair p.1 r.1 : ℝ)) * (1 + (r.2 : ℝ))) ^ 2 :=
        one_add_pair_le_sq' (Nat.pair p.1 r.1) r.2
    _ ≤ (2 * (2 * (1 + (n : ℝ)) * (1 + (n : ℝ))) ^ 2 * (1 + (n : ℝ))) ^ 2 := by
        apply pow_le_pow_left₀ (by positivity)
        have hk_n : (r.2 : ℝ) ≤ (n : ℝ) := Nat.cast_le.mpr hk_le
        have h1 : (1 + (r.2 : ℝ)) ≤ (1 + (n : ℝ)) := by linarith
        calc 2 * (1 + (Nat.pair p.1 r.1 : ℝ)) * (1 + (r.2 : ℝ))
            ≤ 2 * (2 * (1 + (n : ℝ)) * (1 + (n : ℝ))) ^ 2 * (1 + (r.2 : ℝ)) := by
              apply mul_le_mul_of_nonneg_right _ (by positivity)
              linarith [hij]
          _ ≤ 2 * (2 * (1 + (n : ℝ)) * (1 + (n : ℝ))) ^ 2 * (1 + (n : ℝ)) := by
              apply mul_le_mul_of_nonneg_left h1 (by positivity)
    _ = 64 * (1 + (n : ℝ)) ^ 10 := by ring

/-- **Associativity of the nuclear tensor product.**
`(E₁ ⊗̂ E₂) ⊗̂ E₃ ≃L[ℝ] E₁ ⊗̂ (E₂ ⊗̂ E₃)`

Since all three are `RapidDecaySeq`, this is a reindexing by the
Cantor pairing associativity permutation. -/
def assoc (E₁ E₂ E₃ : Type*) :
    NuclearTensorProduct (NuclearTensorProduct E₁ E₂) E₃ ≃L[ℝ]
    NuclearTensorProduct E₁ (NuclearTensorProduct E₂ E₃) :=
  RapidDecaySeq.reindex assocPerm assocPerm_polyBounded assocPerm_symm_polyBounded

end NuclearTensorProduct

/-! ## Step 3: Dimension Peeling Isomorphism -/

/-- **One-dimension peeling isomorphism.**

`S(ℝ^{d+2}) ≃L[ℝ] NuclearTensorProduct S(ℝ^{d+1}) S(ℝ)`

This identifies a Schwartz function on `ℝ^{d+2}` with an element of
`S(ℝ^{d+1}) ⊗̂ S(ℝ)` via the Hermite basis expansion. The isomorphism
is `schwartzRapidDecayEquivNd (d+1)` — literally the existing isomorphism
to `RapidDecaySeq`, since `NuclearTensorProduct = RapidDecaySeq` by definition.

The key insight is that `multiIndexEquiv (d+1)` peels off the last coordinate
via `Fin.succFunEquiv` then Cantor-pairs — which is exactly how
`NuclearTensorProduct` pairs two sequence spaces. -/
noncomputable def schwartzPeelOff (d : ℕ) :
    SchwartzMap (EuclideanSpace ℝ (Fin (d + 2))) ℝ ≃L[ℝ]
    NuclearTensorProduct
      (SchwartzMap (EuclideanSpace ℝ (Fin (d + 1))) ℝ)
      (SchwartzMap ℝ ℝ) :=
  schwartzRapidDecayEquivNd (d + 1)

/-! ## Step 4: Schwartz Pointwise Product -/

/-- The pointwise product `h(x, t) = f(x) · g(t)` of a Schwartz function on `ℝ^{d+1}`
and a Schwartz function on `ℝ`, as a Schwartz function on `ℝ^{d+2}`.

Defined as the inverse image of the pure tensor `f ⊗ g` under `schwartzPeelOff`,
which encodes the same function via the Hermite basis expansion. The value
characterization `schwartzPointwiseProduct_apply` shows that this equals
`f(euclideanInit x) * g(x (Fin.last _))` pointwise. -/
noncomputable def schwartzPointwiseProduct (d : ℕ)
    (f : SchwartzMap (EuclideanSpace ℝ (Fin (d + 1))) ℝ)
    (g : SchwartzMap ℝ ℝ) :
    SchwartzMap (EuclideanSpace ℝ (Fin (d + 2))) ℝ :=
  (schwartzPeelOff d).symm (NuclearTensorProduct.pure f g)

/-- **Canonicity of the peeling isomorphism.**

The inverse of `schwartzPeelOff` sends a pure tensor `f ⊗ g` to the
pointwise product `(x,t) ↦ f(x)·g(t)`. -/
theorem schwartzPeelOff_pure (d : ℕ)
    (f : SchwartzMap (EuclideanSpace ℝ (Fin (d + 1))) ℝ)
    (g : SchwartzMap ℝ ℝ) :
    (schwartzPeelOff d).symm (NuclearTensorProduct.pure f g) =
      schwartzPointwiseProduct d f g :=
  rfl

/-- The pointwise product Schwartz function evaluates to `f(init x) · g(last x)`.

This follows from the Hermite expansion: the pure tensor `f ⊗ g` encodes the
coefficient sequence `c_m = c_{unpair(m).1}(f) · c_{unpair(m).2}(g)`, and the
Hermite series reconstruction converges to `f(init x) · g(last x)` by the
Fubini-based product factorization of Hermite coefficients. -/
theorem schwartzPointwiseProduct_apply (d : ℕ)
    (f : SchwartzMap (EuclideanSpace ℝ (Fin (d + 1))) ℝ)
    (g : SchwartzMap ℝ ℝ)
    (x : EuclideanSpace ℝ (Fin (d + 2))) :
    schwartzPointwiseProduct d f g x =
      f (euclideanInit (d + 1) x) * g (x (Fin.last (d + 1))) := by
  -- Abbreviations for the Hermite coefficient sequences
  set a := schwartzRapidDecayEquivNd d f
  set b := schwartzRapidDecayEquiv1D g
  -- Step 1: Unfold to the Hermite series tsum
  show (schwartzRapidDecayEquivNd (d + 1)).symm
    (show RapidDecaySeq from NuclearTensorProduct.pure f g) x = _
  rw [schwartzRapidDecayEquivNd_symm_apply]
  -- Step 2: Rewrite each summand using pure tensor factorization and basis factorization
  -- (pure f g).val n = a.val (unpair n).1 * b.val (unpair n).2
  have h_pure : ∀ n, (NuclearTensorProduct.pure f g).val n =
      a.val (Nat.unpair n).1 * b.val (Nat.unpair n).2 := by
    intro n
    simp only [NuclearTensorProduct.pure_val,
      DyninMityaginSpace.coeff,
      RapidDecaySeq.coeffCLM, ContinuousLinearMap.comp_apply,
      RapidDecaySeq.coeffLM]
    rfl
  simp_rw [h_pure, hermiteFunctionNd_unpair]
  -- Goal: ∑' n, (a.val i * b.val j) * (Phi_i(init x) * psi_j(last x)) = f(init x) * g(last x)
  -- where i = (unpair n).1, j = (unpair n).2
  -- Define the factor functions
  set F : ℕ → ℝ := fun i => a.val i *
    hermiteFunctionNd (d + 1) ((multiIndexEquiv d).symm i) (euclideanInit (d + 1) x)
  set G : ℕ → ℝ := fun j => b.val j * hermiteFunction j (x (Fin.last (d + 1)))
  -- Step 3: Rearrange (a_i * b_j) * (Phi_i * psi_j) = F i * G j
  conv_lhs => arg 1; ext n; rw [show
    a.val (Nat.unpair n).1 * b.val (Nat.unpair n).2 *
      (hermiteFunctionNd (d + 1) ((multiIndexEquiv d).symm (Nat.unpair n).1)
        (euclideanInit (d + 1) x) *
      hermiteFunction (Nat.unpair n).2 (x (Fin.last (d + 1)))) =
    F (Nat.unpair n).1 * G (Nat.unpair n).2 from by simp only [F, G]; ring]
  -- Step 4: Convert from Cantor-paired ℕ to ℕ × ℕ indexing
  -- ∑' n, F (unpair n).1 * G (unpair n).2 = ∑' (p : ℕ × ℕ), F p.1 * G p.2
  -- Use change of variables: the Cantor pairing bijection
  have h_eq_prod : ∑' n, F (Nat.unpair n).1 * G (Nat.unpair n).2 =
      ∑' (p : ℕ × ℕ), F p.1 * G p.2 :=
    Nat.pairEquiv.symm.tsum_eq (fun p : ℕ × ℕ => F p.1 * G p.2)
  rw [h_eq_prod]
  -- Step 5: Factor the product sum into a product of two sums
  -- Summability of F and G
  have hF : Summable F :=
    schwartzRapidDecayEquivNd_summable d a (euclideanInit (d + 1) x)
  have hG : Summable G :=
    schwartzRapidDecayEquiv1D_summable b (x (Fin.last (d + 1)))
  -- Product summability (from norm summability)
  have hFG : Summable (fun p : ℕ × ℕ => F p.1 * G p.2) :=
    summable_mul_of_summable_norm hF.norm hG.norm
  -- Apply the product formula: ∑' (i,j), F i * G j = (∑' i, F i) * (∑' j, G j)
  rw [← hF.tsum_mul_tsum hG hFG]
  -- Step 6: Each factor tsum reconstructs f and g
  -- ∑' i, F i = ∑' i, a.val i * Phi_i(init x) = f(init x)
  have hF_eq : ∑' i, F i = f (euclideanInit (d + 1) x) := by
    rw [show ∑' i, F i = ∑' i, a.val i *
        hermiteFunctionNd (d + 1) ((multiIndexEquiv d).symm i)
          (euclideanInit (d + 1) x) from rfl]
    rw [← schwartzRapidDecayEquivNd_symm_apply d a (euclideanInit (d + 1) x)]
    -- (schwartzRapidDecayEquivNd d).symm a = (schwartzRapidDecayEquivNd d).symm (equiv d f) = f
    simp [a]
  -- ∑' j, G j = ∑' j, b.val j * psi_j(last x) = g(last x)
  have hG_eq : ∑' j, G j = g (x (Fin.last (d + 1))) := by
    rw [show ∑' j, G j = ∑' j, b.val j *
        hermiteFunction j (x (Fin.last (d + 1))) from rfl]
    rw [← schwartzRapidDecayEquiv1D_symm_apply b (x (Fin.last (d + 1)))]
    simp [b]
  rw [hF_eq, hG_eq]

/-! ## Step 6: General Tensor Equivalence by Induction -/

/-- **General Schwartz tensor product isomorphism.**

`NuclearTensorProduct S(ℝ^{m+1}) S(ℝ^{n+1}) ≃L[ℝ] S(ℝ^{m+n+2})`

Since `NuclearTensorProduct E₁ E₂ = RapidDecaySeq` (definitionally, for any types),
and `schwartzRapidDecayEquivNd` gives `S(ℝ^d) ≃L RapidDecaySeq`, the isomorphism
is just composition with `schwartzRapidDecayEquivNd`.

The composition of these maps encodes the inductive dimension-peeling:
- Base case (`n = 0`): `S(ℝ^{m+1}) ⊗̂ S(ℝ) ≃L S(ℝ^{m+2})` via `schwartzPeelOff`
- Inductive step: peel, reassociate, apply IH, then peel again -/
noncomputable def schwartzTensorEquiv (m n : ℕ) :
    NuclearTensorProduct
      (SchwartzMap (EuclideanSpace ℝ (Fin (m + 1))) ℝ)
      (SchwartzMap (EuclideanSpace ℝ (Fin (n + 1))) ℝ) ≃L[ℝ]
    SchwartzMap (EuclideanSpace ℝ (Fin (m + n + 2))) ℝ :=
  -- NuclearTensorProduct is always RapidDecaySeq, and schwartzRapidDecayEquivNd
  -- gives S(ℝ^{d+1}) ≃L RapidDecaySeq. The isomorphism is the inverse of
  -- the latter. The Fin arithmetic m + n + 2 = (m + n + 1) + 1 holds by omega.
  have h : m + n + 2 = (m + n + 1) + 1 := by omega
  h ▸ (schwartzRapidDecayEquivNd (m + n + 1)).symm

end GaussianField
