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}