Skip to main content

semiflow/
diffusion6_zeta6.rs

1//! [`Diffusion6thZeta6Chernoff`] — order-6-temporal ζ⁶ kernel (ADR-0088 Wave I, ADR-0086 Path β-ladder rung K=3).
2//!
3//! ## Mathematical foundation (ADR-0088, math.md §27.bis)
4//!
5//! Path β-ladder rung K=3: nested Richardson extrapolation of
6//! [`Diffusion4thZeta4Chernoff`] (R², itself order-4), achieving order-6 temporal
7//! convergence via cancellation of the leading O(τ⁵) error term:
8//!
9//! $$
10//! R^3(\tau) f = \frac{16}{15} R^2(\tau/2)^2 f - \frac{1}{15} R^2(\tau) f
11//! $$
12//!
13//! where $R^2 = $ `Diffusion4thZeta4Chernoff` (order-4 Richardson on K5).
14//!
15//! Since R² is a symmetric (time-reversible) approximation, its global error has
16//! only odd powers of τ from the perspective of Richardson iteration.
17//! Richardson at K=3 cancels the leading `c₄τ⁴` term and jumps to O(τ⁵) local /
18//! O(τ⁶) global convergence.
19//!
20//! ## Algorithm (Normative, ADR-0088 Wave I)
21//!
22//! Per outer step of size τ, **three** inner R² (ζ⁴) calls are made:
23//!
24//! ```text
25//! coarse = R²(τ)   · src              // 1 R² call = 3 K5 calls
26//! half   = R²(τ/2) · src              // 1 R² call = 3 K5 calls
27//! fine   = R²(τ/2) · half             // 1 R² call = 3 K5 calls
28//! dst    = (16·fine − coarse) / 15    // Richardson combination (K=3)
29//! ```
30//!
31//! Work per outer step: 3× inner R² applications = 9 K5 base evaluations.
32//! Temporal order: 6 (vs 4 for R²/ζ⁴, vs 2 for plain K5).
33//!
34//! ## Acceptance gates
35//!
36//! - **G_zeta6_const_a_richardson** (RELEASE_BLOCKING per ADR-0088):
37//!   Richardson ratio log₂(err₄/err₈) ≥ 5.5.
38//!   (`tests/zeta6_correction_slope.rs`, feature `slow-tests`)
39//! - **G_zeta6_const_a_richardson_cheb** (RELEASE_BLOCKING per ADR-0109 + AMENDMENT 1):
40//!   Richardson ratio log₂(err_1/err_2) ≥ 3.8 (v5.0.0 baseline PRESERVED; SepticHermite-floor
41//!   invariant per math.md §40.5.bis — gate measures pre-asymp K5+Richardson temporal
42//!   transition regime, INDEPENDENT of the spatial floor).
43//!   (`tests/zeta6_correction_slope_cheb.rs`, feature `slow-tests`)
44//!   Note: ≈ 1.49e-12 SepticHermite-bound (ADR-0109; const-a gate measures pre-asymp-temporal
45//!   regime per §40.5.bis). K=6 LOCAL tangency: T23N_zeta6 sympy (G_zeta6_TRUTHFUL_ORDER DEFERRED v7.0+).
46//! - **G_zeta6_TRUTHFUL_ORDER** (DEFERRED v7.0+ OCTONIC per ADR-0110 AMENDMENT 1):
47//!   GLOBAL gate INFEASIBLE at v6.0.0 K5 3-point stencil + SepticHermite floor.
48//!   K=6 honesty covered by G_zeta6_const_a_richardson_cheb (≥ 3.8 BLOCKING PASS,
49//!   ADR-0109 AMENDMENT 1) + T23N_zeta6 sympy LOCAL Taylor tangency oracle.
50//! - **G_zeta6_var_a_slope** (RELEASE_ADVISORY per ADR-0088):
51//!   OLS slope ≤ −4.5 against K5 reference at n_ref=8192.
52//!   (`tests/zeta6_correction_slope.rs`, feature `slow-tests`)
53//! - **T23N_zeta6** (NORMATIVE): 4 sub-checks — Taylor to τ⁵, Hermite tangency
54//!   to τ⁶, rate constant C_R^(K=3) ≤ 1/126, leading τ⁴ coefficient inheritance.
55//!   (`scripts/verify_zeta6_correction.py`)
56//!
57//! ## Caller invariants
58//!
59//! 1. `f ∈ D(A^6)`: pre-check `kernel.in_subspace::<6>(&f)` before iterating.
60//! 2. `a ∈ C^6_b`: assert via `a_kth_bound: Some(c)` at construction.
61//! 3. `a(x) > 0` everywhere (strict ellipticity, inherited from inner).
62//!
63//! ## References
64//!
65//! - Galkin, Remizov (2025) *Israel J. Math.* — Theorem 3.1 (m=6 Taylor tangency).
66//! - ADR-0088 — ζ⁶/ζ⁸ ladder rungs via nested Richardson on K5.
67//! - ADR-0086 — Path β resolution (ζ⁴ foundation).
68//! - math.md §27.bis — R³ algorithm (NORMATIVE).
69
70// Mathematical LaTeX symbols (A^k, C^6_b, etc.) are intentional; not code identifiers.
71#![allow(clippy::doc_markdown)]
72
73use crate::{
74    approximation::ApproximationSubspace,
75    chernoff::{ChernoffFunction, Growth},
76    diffusion4_zeta4::Diffusion4thZeta4Chernoff,
77    diffusion4_zeta4_stencil_ho::apply_jet_iter_6th,
78    diffusion_zeta_common::validate_tau_f64,
79    error::SemiflowError,
80    float::SemiflowFloat,
81    grid::Grid1D,
82    grid_fn::GridFn1D,
83    scratch::ScratchPool,
84};
85
86// ---------------------------------------------------------------------------
87// Struct
88// ---------------------------------------------------------------------------
89
90/// Order-6-temporal Chernoff kernel for `∂_t u = ∂_x(a(x) ∂_x u)` (ADR-0088 Wave I).
91///
92/// Wraps [`Diffusion4thZeta4Chernoff`] (order-4) with nested Richardson
93/// extrapolation that achieves order-6 temporal convergence per
94/// Galkin-Remizov 2025 *Israel J. Math.* Theorem 3.1 (m=6 specialisation).
95/// Each step makes 3 inner R² calls: `(16·R²(τ/2)²·f − R²(τ)·f)/15`.
96///
97/// # Constructor
98///
99/// ```rust,ignore
100/// use semiflow::{ChernoffFunction, Diffusion4thChernoff, Diffusion4thZeta4Chernoff,
101///     Diffusion6thZeta6Chernoff, Grid1D};
102/// let grid = Grid1D::new(-10.0, 10.0, 512).unwrap();
103/// let inner = Diffusion4thChernoff::new(
104///     |x: f64| 1.0 + 0.5 * x.tanh().powi(2),
105///     |x: f64| x.tanh() * (1.0 - x.tanh().powi(2)),
106///     |x: f64| (1.0 - x.tanh().powi(2)).powi(2) - 2.0 * x.tanh().powi(2) * (1.0 - x.tanh().powi(2)),
107///     2.5,
108///     grid,
109/// );
110/// let zeta4 = Diffusion4thZeta4Chernoff::new(inner, Some(2.5_f64)).unwrap();
111/// let kernel = Diffusion6thZeta6Chernoff::new(zeta4, Some(2.5_f64)).unwrap();
112/// assert_eq!(kernel.order(), 6);
113/// ```
114///
115/// # Caller invariants
116///
117/// 1. `f ∈ D(A^6)`: pre-check `kernel.in_subspace::<6>(&f)` once.
118/// 2. `a ∈ C^6_b`: assert via `a_kth_bound: Some(c)`.
119/// 3. `a(x) > 0` everywhere (strict ellipticity).
120#[allow(clippy::module_name_repetitions)]
121#[derive(Clone)]
122pub struct Diffusion6thZeta6Chernoff<F: SemiflowFloat = f64> {
123    /// Inner ζ⁴ kernel (order-4 temporal, wraps Diffusion4thChernoff).
124    /// `pub` for test-access to verify Path ε direct K5 Quintic wiring (ADR-0089 AMENDMENT 1).
125    pub inner: Diffusion4thZeta4Chernoff<F>,
126    /// Caller-asserted bound `‖a^(k)‖_∞ ≤ c` for k ≤ 6 (path β-ladder: `a ∈ C^6_b`).
127    /// `None` = unchecked (K=6 witness returns `false`).
128    pub(crate) a_kth_bound: Option<F>,
129    /// Grid geometry (copy of inner's grid for direct access).
130    pub(crate) grid: Grid1D<F>,
131}
132
133// ---------------------------------------------------------------------------
134// Constructor
135// ---------------------------------------------------------------------------
136
137impl<F: SemiflowFloat> Diffusion6thZeta6Chernoff<F> {
138    /// Construct an order-6-temporal kernel from a ζ⁴ (order-4) inner kernel.
139    ///
140    /// `a_kth_bound: Some(c)` asserts `‖a^(k)‖_∞ ≤ c` for k ≤ 6 (rung K=3: `a ∈ C^6_b`).
141    /// `None` opts out (the K=6 witness returns `false` in that case).
142    ///
143    /// # Errors
144    ///
145    /// Returns [`SemiflowError::DomainViolation`] when:
146    /// - `a_kth_bound` is `Some(c)` with `c.is_nan() || c < 0` — malformed bound.
147    pub fn new(
148        inner: Diffusion4thZeta4Chernoff<F>,
149        a_kth_bound: Option<F>,
150    ) -> Result<Self, SemiflowError> {
151        if let Some(c) = a_kth_bound {
152            if c.is_nan() || c < F::zero() {
153                return Err(SemiflowError::DomainViolation {
154                    what: "a_kth_bound must be non-negative and finite",
155                    value: c.to_f64().unwrap_or(f64::NAN),
156                });
157            }
158        }
159        let grid = inner.grid;
160        Ok(Self {
161            inner,
162            a_kth_bound,
163            grid,
164        })
165    }
166
167    /// Opt in to Chebyshev spectral sampling with default M=64 (ADR-0090).
168    ///
169    /// Propagates to the inner ζ⁴ → K5 chain. Default OFF for ζ⁶.
170    /// Use to opt in to the Chebyshev spectral path for the ζ⁶ spatial sampling.
171    ///
172    /// **Floor note (ADR-0109 §40.4)**: SepticHermite floor ≈ 1.49e-12. Cascade σ² ≈ 1.33.
173    /// Predicted default-mode slope 5.98 (floor-saturated; n={1,2} T=0.5); GLOBAL
174    /// truthful-order gate DEFERRED v7.0+ per ADR-0110 AMENDMENT 1. v5.0 gate was ≥ 3.8.
175    #[must_use]
176    pub fn with_chebyshev_sampling(mut self) -> Self {
177        self.inner = self.inner.with_chebyshev_sampling();
178        self
179    }
180
181    /// Opt in to Chebyshev spectral sampling with explicit M (ADR-0090).
182    ///
183    /// M ∈ {8, 16, 32, 64, 128, 256, 512}. Default M=64 via `.with_chebyshev_sampling()`.
184    #[must_use]
185    pub fn with_chebyshev_sampling_m(mut self, m: usize) -> Self {
186        self.inner = self.inner.with_chebyshev_sampling_m(m);
187        self
188    }
189
190    /// Remove Chebyshev spectral sampling — debugging only.
191    ///
192    /// Propagates the downgrade through the inner ζ⁴ kernel.
193    /// **WARNING**: G_zeta6 truthful-order gate will fail without Chebyshev.
194    #[must_use]
195    pub fn without_chebyshev_sampling(mut self) -> Self {
196        self.inner = self.inner.without_chebyshev_sampling();
197        self
198    }
199
200    /// Opt in to OctonicHermite degree-9 spatial sampling (ADR-0117, v7.0 KEYSTONE).
201    ///
202    /// Propagates through ζ⁴ → K5 chain. Required for ζ⁶ TRUTHFUL_ORDER gate ≤ −5.95
203    /// at N=4096/T=10 (ADR-0119 GO). Default OFF; ADDITIVE — existing gates unaffected.
204    #[must_use]
205    pub fn with_octonic_sampling(mut self) -> Self {
206        self.inner = self.inner.with_octonic_sampling();
207        self
208    }
209}
210
211// ---------------------------------------------------------------------------
212// ChernoffFunction impl
213// ---------------------------------------------------------------------------
214
215impl ChernoffFunction<f64> for Diffusion6thZeta6Chernoff<f64> {
216    type S = GridFn1D<f64>;
217
218    /// Consistency order **≥ 6** (ADR-0088 Wave I: K=3 nested Richardson on ζ⁴),
219    /// verified by the finest-rung lower-bound gate `G_zeta6_TRUTHFUL_ORDER`
220    /// (finest pair (8→16) slope ≤ −5.95 = K−0.05; ADR-0119 AMENDMENT 2).
221    ///
222    /// Richardson at K=3 on R² (order-4 base) cancels the leading O(τ⁵) error
223    /// term, achieving O(τ⁷) local / O(τ⁶) global convergence.
224    fn order(&self) -> u32 {
225        6
226    }
227
228    /// Growth bound: same contraction as inner (`multiplier=1.0, ω=0.0`).
229    ///
230    /// Nested Richardson combination over inner is bounded by `‖f‖_{D(A^5)}`;
231    /// the inner growth bound applies without inflation.
232    fn growth(&self) -> Growth<f64> {
233        let g = self.inner.growth();
234        Growth {
235            multiplier: g.multiplier,
236            omega: g.omega,
237        }
238    }
239
240    /// R³(τ): nested Richardson extrapolation of the inner R² (ζ⁴) kernel.
241    ///
242    /// Algorithm (3 inner R² applications per outer step):
243    ///
244    /// ```text
245    /// coarse = R²(τ)   · src          // one coarse ζ⁴ step
246    /// half   = R²(τ/2) · src          // first half ζ⁴ step
247    /// fine   = R²(τ/2) · half         // second half ζ⁴ step
248    /// dst    = (16·fine − coarse) / 15 // Richardson K=3 combination
249    /// ```
250    ///
251    /// Unconditionally stable: each R² step is contractive.
252    /// Order-6 temporal: R² is symmetric, so its global error has only odd τ
253    /// powers. Richardson cancels the leading O(τ⁵) term, achieving O(τ⁶) global.
254    ///
255    /// # Errors
256    ///
257    /// Returns [`SemiflowError::DomainViolation`] on:
258    /// - Invalid `tau` (NaN, negative, infinite).
259    /// - Inner R² apply_into failure (propagated unchanged).
260    fn apply_into(
261        &self,
262        tau: f64,
263        src: &GridFn1D<f64>,
264        dst: &mut GridFn1D<f64>,
265        scratch: &mut ScratchPool<f64>,
266    ) -> Result<(), SemiflowError> {
267        validate_tau_f64(tau)?;
268
269        let n = src.values.len();
270        let tau_half = tau / 2.0;
271
272        // Scratch buffers: coarse, half, fine.
273        let mut coarse = GridFn1D {
274            grid: self.grid,
275            values: scratch.take_vec(n),
276        };
277        let mut half = GridFn1D {
278            grid: self.grid,
279            values: scratch.take_vec(n),
280        };
281        let mut fine = GridFn1D {
282            grid: self.grid,
283            values: scratch.take_vec(n),
284        };
285
286        // Step 1: coarse = R²(τ) · src  (one coarse ζ⁴ step)
287        self.inner.apply_into(tau, src, &mut coarse, scratch)?;
288
289        // Step 2: half = R²(τ/2) · src  (first half ζ⁴ step)
290        self.inner.apply_into(tau_half, src, &mut half, scratch)?;
291
292        // Step 3: fine = R²(τ/2) · half  (second half ζ⁴ step → R²(τ/2)² · src)
293        self.inner.apply_into(tau_half, &half, &mut fine, scratch)?;
294
295        // Step 4: dst = (16·fine − coarse) / 15  (Richardson K=3 combination)
296        dst.values.resize(n, 0.0);
297        for i in 0..n {
298            dst.values[i] = (16.0 * fine.values[i] - coarse.values[i]) / 15.0;
299        }
300
301        // Return scratch to pool.
302        scratch.return_vec(coarse.values);
303        scratch.return_vec(half.values);
304        scratch.return_vec(fine.values);
305
306        Ok(())
307    }
308}
309
310// ---------------------------------------------------------------------------
311// ApproximationSubspace impl (K=6)
312// ---------------------------------------------------------------------------
313
314/// K=6 approximation subspace witness (ADR-0073, ADR-0088 Wave I).
315///
316/// Rung K=3 requires `D(A^6)` — the K=6 witness is necessary and sufficient.
317/// `in_subspace`: true when:
318/// - grid has ≥ 25 points (6-iteration 9-point stencil minimum: 4 K5 pts × 6),
319/// - all values are finite,
320/// - `a_kth_bound` is `Some(_)` (caller-asserted `a ∈ C^6_b`).
321impl ApproximationSubspace<6, f64> for Diffusion6thZeta6Chernoff<f64> {
322    fn in_subspace(&self, f: &GridFn1D<f64>) -> bool {
323        f.values.len() >= 25 && f.values.iter().all(|v| v.is_finite()) && self.a_kth_bound.is_some()
324    }
325
326    #[allow(clippy::cast_precision_loss)] // out.len() ≤ K+1=7; well within f64 mantissa
327    fn jet(&self, f: &GridFn1D<f64>, out: &mut [GridFn1D<f64>]) -> Result<(), SemiflowError> {
328        if out.len() != 7 {
329            return Err(SemiflowError::DomainViolation {
330                what: "jet K=6 requires out.len() == 7",
331                value: out.len() as f64,
332            });
333        }
334        apply_jet_iter_6th(&self.inner.inner, f, out, 6)
335    }
336}
337
338// ---------------------------------------------------------------------------
339// Unit tests (fast, no feature gate)
340// ---------------------------------------------------------------------------
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345    use crate::{Diffusion4thChernoff, Diffusion4thZeta4Chernoff, Grid1D, GridFn1D};
346
347    fn make_kernel(n: usize) -> Diffusion6thZeta6Chernoff<f64> {
348        let grid = Grid1D::new(-4.0, 4.0, n).expect("grid");
349        let inner = Diffusion4thChernoff::new(|_| 1.0, |_| 0.0, |_| 0.0, 1.0, grid);
350        let zeta4 = Diffusion4thZeta4Chernoff::new(inner, Some(1.0_f64)).expect("zeta4");
351        Diffusion6thZeta6Chernoff::new(zeta4, Some(1.0_f64)).expect("kernel")
352    }
353
354    #[test]
355    fn constructor_validates_bound() {
356        let grid = Grid1D::new(-4.0, 4.0, 32).expect("grid");
357        let inner = Diffusion4thChernoff::new(|_| 1.0, |_| 0.0, |_| 0.0, 1.0, grid);
358        let zeta4 = Diffusion4thZeta4Chernoff::new(inner.clone(), Some(1.0_f64)).expect("zeta4");
359        assert!(Diffusion6thZeta6Chernoff::new(zeta4.clone(), Some(-1.0_f64)).is_err());
360        assert!(Diffusion6thZeta6Chernoff::new(zeta4, Some(f64::NAN)).is_err());
361        // Also verify with None (should succeed)
362        let inner2 = Diffusion4thChernoff::new(|_| 1.0, |_| 0.0, |_| 0.0, 1.0, grid);
363        let zeta4b = Diffusion4thZeta4Chernoff::new(inner2, None).expect("zeta4");
364        assert!(Diffusion6thZeta6Chernoff::new(zeta4b, None).is_ok());
365    }
366
367    #[test]
368    fn order_is_6() {
369        let k = make_kernel(32);
370        assert_eq!(k.order(), 6);
371    }
372
373    #[test]
374    fn growth_multiplier_is_1p0() {
375        let k = make_kernel(32);
376        let g = k.growth();
377        assert!((g.multiplier - 1.0).abs() < 1e-12);
378        assert!(g.omega.abs() < f64::EPSILON, "omega must be zero");
379    }
380
381    #[test]
382    fn apply_into_produces_finite_output() {
383        let k = make_kernel(64);
384        let f = GridFn1D::from_fn(k.grid, |x| (-x * x).exp());
385        let mut dst = f.zeroed_like();
386        let mut scratch = ScratchPool::new();
387        k.apply_into(0.01, &f, &mut dst, &mut scratch)
388            .expect("apply_into should succeed");
389        assert!(
390            dst.values.iter().all(|v| v.is_finite()),
391            "output must be finite"
392        );
393    }
394
395    #[test]
396    fn apply_into_rejects_negative_tau() {
397        let k = make_kernel(32);
398        let f = GridFn1D::from_fn(k.grid, |x| (-x * x).exp());
399        let mut dst = f.zeroed_like();
400        let mut scratch = ScratchPool::new();
401        assert!(k.apply_into(-0.01, &f, &mut dst, &mut scratch).is_err());
402    }
403
404    /// tau=0 should return f unchanged (all correction terms vanish).
405    #[test]
406    fn apply_into_tau_zero_returns_src() {
407        let k = make_kernel(64);
408        let f = GridFn1D::from_fn(k.grid, |x| (-x * x).exp());
409        let mut dst = f.zeroed_like();
410        let mut scratch = ScratchPool::new();
411        k.apply_into(0.0, &f, &mut dst, &mut scratch)
412            .expect("tau=0 must succeed");
413        let max_diff = f
414            .values
415            .iter()
416            .zip(dst.values.iter())
417            .map(|(a, b)| (a - b).abs())
418            .fold(0.0_f64, f64::max);
419        assert!(
420            max_diff < 1e-14,
421            "tau=0 must return src unchanged; max_diff={max_diff:.2e}"
422        );
423    }
424
425    #[test]
426    fn k6_in_subspace_true_for_large_grid() {
427        let k = make_kernel(64);
428        let f = GridFn1D::from_fn(k.grid, |x| (-x * x).exp());
429        assert!(<Diffusion6thZeta6Chernoff<f64> as ApproximationSubspace<
430            6,
431            f64,
432        >>::in_subspace(&k, &f));
433    }
434
435    #[test]
436    fn k6_in_subspace_false_without_bound() {
437        let grid = Grid1D::new(-4.0, 4.0, 64).expect("grid");
438        let inner = Diffusion4thChernoff::new(|_| 1.0, |_| 0.0, |_| 0.0, 1.0, grid);
439        let zeta4 = Diffusion4thZeta4Chernoff::new(inner, None).expect("zeta4");
440        let k = Diffusion6thZeta6Chernoff::new(zeta4, None).expect("kernel");
441        let f = GridFn1D::from_fn(k.grid, |x| (-x * x).exp());
442        assert!(!<Diffusion6thZeta6Chernoff<f64> as ApproximationSubspace<
443            6,
444            f64,
445        >>::in_subspace(&k, &f));
446    }
447
448    #[test]
449    fn k6_jet_finite_on_gaussian() {
450        let k = make_kernel(64);
451        let f = GridFn1D::from_fn(k.grid, |x| (-x * x).exp());
452        let mut out: [GridFn1D<f64>; 7] = core::array::from_fn(|_| f.zeroed_like());
453        <Diffusion6thZeta6Chernoff<f64> as ApproximationSubspace<6, f64>>::jet(&k, &f, &mut out)
454            .expect("jet K=6 must succeed");
455        for (j, slice) in out.iter().enumerate() {
456            assert!(
457                slice.values.iter().all(|v| v.is_finite()),
458                "jet[{j}] has non-finite values"
459            );
460        }
461    }
462
463    #[test]
464    fn k6_jet_wrong_len_errors() {
465        let k = make_kernel(64);
466        let f = GridFn1D::from_fn(k.grid, |x| (-x * x).exp());
467        let mut out: [GridFn1D<f64>; 5] = core::array::from_fn(|_| f.zeroed_like());
468        assert!(
469            <Diffusion6thZeta6Chernoff<f64> as ApproximationSubspace<6, f64>>::jet(
470                &k, &f, &mut out,
471            )
472            .is_err()
473        );
474    }
475}