thermite_special/specialized/generic/sh.rs
1//! Real spherical harmonics, evaluated directly from Cartesian components.
2//!
3//! # Conventions
4//!
5//! Orthonormal real spherical harmonics. The `CS` const parameter selects the phase
6//! convention: `false` for the standard real-SH tables (sphericart, most math
7//! references), `true` for the Condon-Shortley phase. Everything below describes
8//! `CS = false`. See [the phase section](#the-condon-shortley-phase) for what the
9//! other one changes.
10//!
11//! ```math
12//! \int_{S^2} Y_{\ell m}^2 \, d\Omega = 1,
13//! \qquad
14//! Y_{\ell m} =
15//! \begin{cases}
16//! \sqrt{2}\, K_\ell^m P_\ell^m(\cos\theta)\cos(m\varphi) & m > 0 \\
17//! K_\ell^0 P_\ell(\cos\theta) & m = 0 \\
18//! \sqrt{2}\, K_\ell^{|m|} P_\ell^{|m|}(\cos\theta)\sin(|m|\varphi) & m < 0
19//! \end{cases}
20//! ```
21//!
22//! with `$K_\ell^m = \sqrt{\tfrac{2\ell+1}{4\pi}\tfrac{(\ell-m)!}{(\ell+m)!}}$` and
23//! `$P_\ell^m$` the associated Legendre functions _without_ `$(-1)^m$`. So
24//! `$Y_{00} = \sqrt{1/4\pi}$`, `$Y_{1,-1} = \sqrt{3/4\pi}\,y$`, `$Y_{10} = \sqrt{3/4\pi}\,z$`,
25//! `$Y_{11} = \sqrt{3/4\pi}\,x$`.
26//!
27//! Outputs are written in the flat `l * (l + 1) + m` order (`m` from `-l` to `l`),
28//! the layout every SH-lighting pipeline uses.
29//!
30//! ## The Condon-Shortley phase
31//!
32//! The two conventions differ by `$(-1)^{|m|}$`: odd `|m|` is negated, even `|m|`
33//! agrees exactly. Which one a body of data was projected against is _not_ recoverable
34//! from the data (the difference is invisible in any rotationally-averaged or
35//! squared quantity), so mixing them is a silent, plausible-looking wrong answer.
36//! Hence the explicit parameter rather than a fixed choice.
37//!
38//! Sloan's widely-copied `SHEval` generated code (_Efficient Spherical Harmonic
39//! Evaluation_, JCGT 2(2), 2013) **does** carry the phase. Its diagonal recurrence
40//! is `P_m^m = (1 - 2m) P_{m-1}^{m-1}`, negative for every `m >= 1`. Its order-3
41//! listing emits `pSH[3] = -0.48860251 * x`, matching `CS = true` here, while
42//! `CS = false` gives `+0.48860251 * x`.
43//!
44//! `CS` is baked into the constant table, so neither choice costs an instruction.
45//! Internally it is applied in two places, the second easy to overlook: the diagonal
46//! seeds for odd `m` (which propagates to a whole column, and to both the `+m` and
47//! `-m` slots that share it), and _every_ `z`-derivative ratio in `f`, because that
48//! ratio crosses between adjacent columns whose signs always disagree.
49//!
50//! # Algorithm
51//!
52//! No trigonometry and no division anywhere. The evaluation factors each harmonic as
53//! `$Y_{\ell,\pm m} = q_\ell^m(z) \cdot \{c_m, s_m\}$` where
54//!
55//! * `$c_m + i s_m = (x + iy)^m$`, accumulated by the complex-multiplication pair
56//! recurrence. Since `$x + iy = \sin\theta\, e^{i\varphi}$` on the unit sphere, this
57//! _is_ `$\sin^m\theta \{\cos, \sin\}(m\varphi)$`, i.e. the `$\sin^m\theta$` factor of
58//! `$P_\ell^m$` moved into the azimuthal term, which removes the `$1/\sin\theta$`
59//! pole from every recurrence (the factoring used by sphericart, Bigi et al.,
60//! J. Chem. Phys. 159, 064802, 2023).
61//! * `$q_\ell^m(z)$` is the fully-normalized sin-factored associated Legendre part,
62//! via the standard normalized three-term recurrences (Holmes & Featherstone 2002,
63//! J. Geodesy 76): a constant diagonal, one `$\sqrt{2m+3}\, z$` step, then
64//! `$q_\ell^m = a_\ell^m z\, q_{\ell-1}^m - b_\ell^m q_{\ell-2}^m$`. All coefficients
65//! are precomputed at compile time ([`ShConsts`]). Intermediate values stay `O(1)`,
66//! so there is no overflow at any order either format can index.
67//!
68//! Cost is `O(L^2)` FMAs per call (two per harmonic past the seeds) with zero
69//! transcendentals.
70//!
71//! # Domain and gradient semantics
72//!
73//! `(x, y, z)` is assumed to be a **unit vector**. Nothing renormalizes. Off the unit
74//! sphere the recurrences still evaluate a perfectly good polynomial in `(x, y, z)`
75//! (the one that agrees with `$Y_{\ell m}$` on the sphere), which is exactly what
76//! [`sh_d_impl`]'s derivatives differentiate: the **ambient Cartesian gradient of that
77//! polynomial form**, evaluated at the given point. This is the convention machine
78//! learning interatomic potentials and finite-difference checks want. A caller who
79//! needs the _tangential_ (spherical) gradient projects out the radial component:
80//! `g_tan = g - (g . n) n`.
81//!
82//! The derivative combinations are exact identities on the recurrence outputs:
83//! `$\partial_x c_m = m c_{m-1}$`, `$\partial_y c_m = -m s_{m-1}$` (and the mirrored
84//! pair for `$s_m$`), and `$\partial_z q_\ell^m = f_\ell^m q_\ell^{m+1}$` where
85//! `$f_\ell^m$` is a tabulated norm ratio. So the gradient pass reuses every value
86//! the value pass produced and adds no new recurrences.
87
88use core::f64::consts::PI;
89
90use thermite::{
91 math::{CoreMath, PrimalProjection, policy::Policy, scalar::Unwrap},
92 prelude::*,
93 register::FloatElement,
94};
95
96/// Triangular index base: `q_l^m` lives at `tri(l) + m`.
97///
98/// `tri(L) + L = L(L+3)/2 < (L+1)^2` for every `L`, so the triangular tables always
99/// fit in the same `N = (L+1)^2` allocation the flat output uses.
100#[inline(always)]
101pub const fn tri(l: usize) -> usize {
102 l * (l + 1) / 2
103}
104
105/// `sqrt` for positive finite values in const context.
106///
107/// Bit-shift seed plus Newton iterations. Converges to within 1 ulp long before the
108/// iteration cap for any normal positive input. Not guaranteed correctly rounded
109/// (irrelevant at 1 ulp for approximation coefficients), but fully deterministic,
110/// which is what matters for reproducible tables.
111const fn csqrt(x: f64) -> f64 {
112 assert!(x > 0.0 && x < f64::INFINITY);
113
114 // Halving the exponent bits lands within ~2x of sqrt(x), and each Newton step then
115 // squares the relative accuracy, so 6 steps are already past f64 precision.
116 let mut y = f64::from_bits((x.to_bits() >> 1) + 0x1FF8_0000_0000_0000);
117
118 let mut i = 0;
119 while i < 8 {
120 y = 0.5 * (y + x / y);
121 i += 1;
122 }
123
124 y
125}
126
127/// Precomputed recurrence coefficients for all `(l, m)` with `l <= L`.
128///
129/// Every array is sized `N = (L+1)^2` (the flat output size) rather than its exact
130/// need, because the exact sizes (`L+1`, triangular) are generic const expressions
131/// that stable Rust cannot spell in a type. The waste is compile-time data only.
132///
133/// Indexing: `qmm` and `em` by `m`, and `a`, `nb`, `f` by `tri(l) + m`.
134///
135/// Instantiated two ways. `ShTable<E, N>` over a scalar element is the compile-time
136/// form behind [`ShConsts`], read by the unrolled kernels. `ShTable<V, N>` over a
137/// _vector_ is the runtime form produced by [`sh_table_impl`] and consumed by
138/// [`sh_eval_impl`], which is what lifts the degree cap and makes the kernels work on
139/// element types that have no const table.
140///
141/// `#[repr(C)]` so that the two are layout-compatible when the element and vector
142/// types are (the scalar-math layer reinterprets `&mut ShTable<f32, N>` as
143/// `&mut ShTable<Vector<f32>, N>`). `repr(Rust)` gives no such guarantee across
144/// distinct type arguments.
145///
146/// `Clone` but deliberately not `Copy`: a table is `6 * N` elements (about 4.8 KB at
147/// `L = 4` on `f32x8`, 54 KB at `L = 16`), and implicit copies of that are not
148/// something to make easy. Pass it by reference. It is read-only after filling.
149#[repr(C)]
150#[derive(Clone, Debug)]
151pub struct ShTable<E, const N: usize> {
152 /// Diagonal values `q_m^m`, pure constants, since the `$\sin^m\theta$` that made
153 /// the diagonal `z`-dependent lives in the azimuthal recurrence instead.
154 pub qmm: [E; N],
155 /// First off-diagonal step: `q_{m+1}^m = em[m] * z * q_m^m`, `em[m] = sqrt(2m+3)`.
156 pub em: [E; N],
157 /// Three-term recurrence: `q_l^m = a * z * q_{l-1}^m + nb * q_{l-2}^m`.
158 pub a: [E; N],
159 /// The `b` coefficient, stored negated so the recurrence is a single `mul_adde`.
160 pub nb: [E; N],
161 /// `z`-derivative norm ratio: `d(q_l^m)/dz = f[tri(l)+m] * q_l^{m+1}` (zero at `m = l`).
162 pub f: [E; N],
163 /// `m` as a float, for the azimuthal derivative factor (`d c_m = m c_{m-1}` etc.).
164 pub mf: [E; N],
165}
166
167const fn build_f64<const L: usize, const N: usize, const CS: bool>() -> ShTable<f64, N> {
168 assert!(N == (L + 1) * (L + 1));
169
170 let mut t = ShTable {
171 qmm: [0.0; N],
172 em: [0.0; N],
173 a: [0.0; N],
174 nb: [0.0; N],
175 f: [0.0; N],
176 mf: [0.0; N],
177 };
178
179 let mut m = 0;
180 while m <= L {
181 t.mf[m] = m as f64;
182 m += 1;
183 }
184
185 // Diagonal: q_0^0 = Y_00 = sqrt(1/4pi), and each step multiplies by
186 // sqrt((2m+1)/(2m)), with one extra sqrt(2) at m = 1. That is the sqrt(2 - delta_{m0})
187 // of the real-harmonic normalization entering the recurrence exactly once.
188 t.qmm[0] = csqrt(1.0 / (4.0 * PI));
189
190 let mut m = 1;
191 while m <= L {
192 let mut d = csqrt((2 * m + 1) as f64 / (2 * m) as f64);
193 if m == 1 {
194 d *= csqrt(2.0);
195 }
196 t.qmm[m] = t.qmm[m - 1] * d;
197 m += 1;
198 }
199
200 let mut m = 0;
201 while m < L {
202 t.em[m] = csqrt((2 * m + 3) as f64);
203 m += 1;
204 }
205
206 let mut l = 1;
207 while l <= L {
208 let lf = l as f64;
209
210 let mut mm = 0;
211 while mm <= l {
212 let k = tri(l) + mm;
213 let mf = mm as f64;
214
215 if l >= mm + 2 {
216 // Holmes & Featherstone fully-normalized coefficients. The common
217 // sqrt(2 - delta) / 4pi prefactors cancel in the ratios, so these are
218 // identical for the m = 0 and m > 0 columns.
219 t.a[k] = csqrt(((2.0 * lf + 1.0) * (2.0 * lf - 1.0)) / ((lf - mf) * (lf + mf)));
220 t.nb[k] = -csqrt(
221 ((2.0 * lf + 1.0) * (lf - mf - 1.0) * (lf + mf - 1.0)) / ((lf - mf) * (lf + mf) * (2.0 * lf - 3.0)),
222 );
223 }
224
225 // d(q_l^m)/dz = f * q_l^{m+1}: the norm ratio n_{l,m}/n_{l,m+1} applied to
226 // the classical dQ_l^m/dz = Q_l^{m+1}. The m = 0 column picks up a 1/sqrt(2)
227 // from sqrt(2 - delta_{m0}) changing between the columns.
228 t.f[k] = if mm == l {
229 0.0 // q_l^{l+1} = 0
230 } else if mm == 0 {
231 csqrt(lf * (lf + 1.0) / 2.0)
232 } else {
233 csqrt((lf - mf) * (lf + mf + 1.0))
234 };
235
236 mm += 1;
237 }
238
239 l += 1;
240 }
241
242 // --- Condon-Shortley phase, if requested: scale column m by (-1)^m ---
243 //
244 // Two touch-ups suffice, and the second is the one that is easy to miss.
245 //
246 // Every q_l^m in a column is generated from that column's diagonal seed by
247 // recurrences that are linear and homogeneous in it, so negating q_m^m negates
248 // the whole column. Since the +m and -m outputs share one q, both slots
249 // flip together, which is the (-1)^|m| the convention asks for.
250 //
251 // But `f` relates ADJACENT columns (d(q_l^m)/dz = f * q_l^{m+1}), whose signs
252 // now always disagree: the ratio (-1)^m / (-1)^{m+1} is -1 for every m. So the
253 // derivative ratios flip globally, independent of m's parity. Miss this and the
254 // values are right while every z-gradient carries the wrong sign.
255 if CS {
256 let mut m = 1;
257 while m <= L {
258 if m % 2 == 1 {
259 t.qmm[m] = -t.qmm[m];
260 }
261 m += 1;
262 }
263
264 let mut l = 0;
265 while l <= L {
266 let mut mm = 0;
267 // Strictly below the diagonal: f is an exact zero at mm == l, and
268 // negating that would only manufacture a -0.0.
269 while mm < l {
270 t.f[tri(l) + mm] = -t.f[tri(l) + mm];
271 mm += 1;
272 }
273 l += 1;
274 }
275 }
276
277 t
278}
279
280const fn arr_to_f32<const N: usize>(a: &[f64; N]) -> [f32; N] {
281 let mut o = [0.0f32; N];
282 let mut i = 0;
283 while i < N {
284 o[i] = a[i] as f32;
285 i += 1;
286 }
287 o
288}
289
290const fn build_f32<const L: usize, const N: usize, const CS: bool>() -> ShTable<f32, N> {
291 let t = build_f64::<L, N, CS>();
292
293 ShTable {
294 qmm: arr_to_f32(&t.qmm),
295 em: arr_to_f32(&t.em),
296 a: arr_to_f32(&t.a),
297 nb: arr_to_f32(&t.nb),
298 f: arr_to_f32(&t.f),
299 mf: arr_to_f32(&t.mf),
300 }
301}
302
303/// Compile-time spherical-harmonic coefficient tables for one element type, at one
304/// degree and one phase convention.
305///
306/// Follows the per-element const-table pattern of `thermite-complex`'s `Weideman`
307/// trait: the `f64` table is computed once in const eval and narrowed per element, so
308/// the kernels read plain constants and pay no runtime conversion. `CS` is baked into
309/// the table rather than applied at runtime, so the phase costs literally nothing.
310/// The two conventions differ only in which constants get emitted.
311pub trait ShConsts<const L: usize, const N: usize, const CS: bool>: FloatElement {
312 const TABLE: ShTable<Self, N>;
313}
314
315impl<const L: usize, const N: usize, const CS: bool> ShConsts<L, N, CS> for f64 {
316 const TABLE: ShTable<f64, N> = build_f64::<L, N, CS>();
317}
318
319impl<const L: usize, const N: usize, const CS: bool> ShConsts<L, N, CS> for f32 {
320 const TABLE: ShTable<f32, N> = build_f32::<L, N, CS>();
321}
322
323impl<V: FloatVector, const N: usize> ShTable<V, N> {
324 /// An all-zero table, to be filled by [`sh_table_impl`].
325 #[inline(always)]
326 pub fn zeroed() -> Self {
327 Self {
328 qmm: [V::ZERO; N],
329 em: [V::ZERO; N],
330 a: [V::ZERO; N],
331 nb: [V::ZERO; N],
332 f: [V::ZERO; N],
333 mf: [V::ZERO; N],
334 }
335 }
336
337 /// Lifts every entry into a composite `W` whose primal is `V`, via
338 /// [`from_primal`](PrimalProjection::from_primal), so constants with zeroed
339 /// augmentation. The identity copy when `W` is its own primal.
340 ///
341 /// Hand-rolled loops rather than `array::map`, which fails to inline in
342 /// `target_feature` code.
343 #[inline(always)]
344 pub fn lift<W>(&self) -> ShTable<W, N>
345 where
346 W: FloatVector + PrimalProjection<Primal = V>,
347 {
348 let mut out = ShTable::<W, N>::zeroed();
349 let mut i = 0;
350 while i < N {
351 out.qmm[i] = W::from_primal(self.qmm[i]);
352 out.em[i] = W::from_primal(self.em[i]);
353 out.a[i] = W::from_primal(self.a[i]);
354 out.nb[i] = W::from_primal(self.nb[i]);
355 out.f[i] = W::from_primal(self.f[i]);
356 out.mf[i] = W::from_primal(self.mf[i]);
357 i += 1;
358 }
359 out
360 }
361}
362
363/// Scalar-layer bridge for the table arguments, mirroring `thermite`'s `Unwrap for
364/// &mut [Vector<R>; N]`.
365///
366/// The `ScalarSpecialMath` aggregate runs the vector kernels at width 1, wrapping each
367/// argument on the way in. A table is a by-reference parameter, so it is reinterpreted
368/// in place rather than copied. This is sound because `Vector<R>` is
369/// `#[repr(transparent)]` over `Storage<R>`, `R: Register<Storage = R>` pins that to
370/// `R`, and [`ShTable`] is `#[repr(C)]` so the two instantiations agree on layout.
371impl<'a, R, const N: usize> Unwrap for &'a ShTable<Vector<R>, N>
372where
373 R: thermite::register::Register<Storage = R>,
374{
375 type Unwrapped = &'a ShTable<R, N>;
376
377 #[inline(always)]
378 fn wrap(value: Self::Unwrapped) -> Self {
379 // SAFETY: see the doc comment above (repr(transparent) + repr(C)).
380 unsafe { &*(value as *const ShTable<R, N> as *const ShTable<Vector<R>, N>) }
381 }
382
383 #[inline(always)]
384 fn unwrap(self) -> Self::Unwrapped {
385 // SAFETY: as in `wrap`.
386 unsafe { &*(self as *const ShTable<Vector<R>, N> as *const ShTable<R, N>) }
387 }
388}
389
390impl<'a, R, const N: usize> Unwrap for &'a mut ShTable<Vector<R>, N>
391where
392 R: thermite::register::Register<Storage = R>,
393{
394 type Unwrapped = &'a mut ShTable<R, N>;
395
396 #[inline(always)]
397 fn wrap(value: Self::Unwrapped) -> Self {
398 // SAFETY: see the doc comment above (repr(transparent) + repr(C)).
399 unsafe { &mut *(value as *mut ShTable<R, N> as *mut ShTable<Vector<R>, N>) }
400 }
401
402 #[inline(always)]
403 fn unwrap(self) -> Self::Unwrapped {
404 // SAFETY: as in `wrap`.
405 unsafe { &mut *(self as *mut ShTable<Vector<R>, N> as *mut ShTable<R, N>) }
406 }
407}
408
409// Index-safety invariant shared by everything below: `N == (L + 1)^2` is
410// const-asserted in both kernels, and for `m <= l <= L`
411// flat: l(l+1) + m <= L(L+1) + L = N - 1
412// triangular: tri(l) + m <= L(L+3)/2 < N
413// column: m <= L < N
414// so every access is in bounds by construction. The checked-indexing forms are not
415// used because their bounds checks defeat LLVM's unroller and scheduler (measured:
416// the whole kernel stayed rolled with panic paths at every store).
417
418/// Unchecked fixed-array read under the module's index invariant.
419#[inline(always)]
420fn at<T: Copy, const N: usize>(a: &[T; N], i: usize) -> T {
421 debug_assert!(i < N);
422 // SAFETY: see the index invariant above.
423 unsafe { *a.get_unchecked(i) }
424}
425
426/// Unchecked fixed-array write under the module's index invariant.
427#[inline(always)]
428fn put<T, const N: usize>(a: &mut [T; N], i: usize, v: T) {
429 debug_assert!(i < N);
430 // SAFETY: see the index invariant above.
431 unsafe {
432 *a.get_unchecked_mut(i) = v;
433 }
434}
435
436/// Writes `q * {c, s}` into the two `(l, +-m)` output slots.
437///
438/// `m = 0` stores `q` directly: `c_0 = 1` and the `-m` slot is the same slot.
439#[inline(always)]
440fn emit<V: FloatVector, const N: usize>(out: &mut [V; N], l: usize, m: usize, q: V, c: V, s: V) {
441 let base = l * (l + 1);
442
443 if m == 0 {
444 put(out, base, q);
445 } else {
446 put(out, base + m, q * c);
447 put(out, base - m, q * s);
448 }
449}
450
451/// Highest degree the stamped ladders below cover. Beyond it the kernels fall back to
452/// the rolled, runtime-coefficient path ([`sh_table_impl`] + [`sh_eval_impl`]), which
453/// is correct at any degree but roughly an order of magnitude slower. Extending the
454/// ladder is mechanical: append literals to every `0 1 2 ... 16` list.
455pub const MAX_DEGREE: usize = 16;
456
457// --- The literal ladders ---
458//
459// LLVM's unroller declines these triangular nests outright: measured on the loop
460// form, L = 4 and L = 8 produced near-identical fully-rolled code with runtime
461// l*(l+1) index arithmetic (`imul`/`shl`) and table loads through a register index,
462// even with all bounds checks elided. So the unrolling is done in the source (the
463// same guard-ladder fix the interleave engine uses): every (m, l) pair through
464// MAX_DEGREE is stamped with LITERAL indices behind `if <lit> <= L` guards. `L` is
465// a monomorphized constant, so dead pairs fold away and live indices become
466// compile-time constants: coefficient loads fold to `vbroadcast` from `.rodata`,
467// stores get fixed offsets, and no integer arithmetic survives to runtime.
468//
469// Hygiene note: the recurrence state (`q_prev`/`q_cur`, `c`/`s`) is threaded between
470// rules as `ident` arguments. Locals introduced in one expansion are invisible to
471// tokens written in another rule, but a captured ident keeps its context.
472
473/// Value-kernel ladder: z-recurrence columns fused with the `emit` sink.
474macro_rules! sh_value_columns {
475 ($L:ident, $V:ident, $t:ident, $x:ident, $y:ident, $z:ident, $out:ident) => {
476 let mut c = $V::ONE;
477 let mut s = $V::ZERO;
478 sh_value_columns!(@m $L, $V, $t, $x, $y, $z, c, s, $out;
479 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16);
480 };
481 (@m $L:ident, $V:ident, $t:ident, $x:ident, $y:ident, $z:ident, $c:ident, $s:ident, $out:ident; $($mv:literal)*) => { $(
482 if $mv <= $L {
483 let q_diag = $V::splat(at(&$t.qmm, $mv));
484 emit($out, $mv, $mv, q_diag, $c, $s);
485
486 if $mv < $L {
487 let mut q_prev = q_diag;
488 let mut q_cur = ($z * q_prev) * $V::splat(at(&$t.em, $mv));
489 emit($out, $mv + 1, $mv, q_cur, $c, $s);
490
491 sh_value_columns!(@l $L, $V, $t, $z, $c, $s, $out, $mv, q_prev, q_cur;
492 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16);
493
494 // (c, s) *= (x + iy)
495 let c_next = $x.difference_of_products($c, $y, $s);
496 let s_next = $x.sum_of_products($s, $y, $c);
497 $c = c_next;
498 $s = s_next;
499 }
500 }
501 )* };
502 (@l $L:ident, $V:ident, $t:ident, $z:ident, $c:ident, $s:ident, $out:ident, $mv:literal, $qp:ident, $qc:ident; $($lv:literal)*) => { $(
503 if $lv >= $mv + 2 && $lv <= $L {
504 let q_next = ($z * $qc)
505 .mul_adde($V::splat(at(&$t.a, tri($lv) + $mv)), $qp * $V::splat(at(&$t.nb, tri($lv) + $mv)));
506 emit($out, $lv, $mv, q_next, $c, $s);
507 $qp = $qc;
508 $qc = q_next;
509 }
510 )* };
511}
512
513/// Scratch-fill ladder: the same z-recurrence with the triangular `q` array as the
514/// sink (pass 1 of [`sh_d_impl`]). Kept separate from [`sh_value_columns`] rather
515/// than parameterized by a sink callback, since the duplication is ~20 lines and the
516/// parameterized form costs far more in readability.
517macro_rules! sh_q_columns {
518 ($L:ident, $V:ident, $t:ident, $z:ident, $q:ident) => {
519 sh_q_columns!(@m $L, $V, $t, $z, $q; 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16);
520 };
521 (@m $L:ident, $V:ident, $t:ident, $z:ident, $q:ident; $($mv:literal)*) => { $(
522 if $mv <= $L {
523 let q_diag = $V::splat(at(&$t.qmm, $mv));
524 put(&mut $q, tri($mv) + $mv, q_diag);
525
526 if $mv < $L {
527 let mut q_prev = q_diag;
528 let mut q_cur = ($z * q_prev) * $V::splat(at(&$t.em, $mv));
529 put(&mut $q, tri($mv + 1) + $mv, q_cur);
530
531 sh_q_columns!(@l $L, $V, $t, $z, $q, $mv, q_prev, q_cur;
532 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16);
533 }
534 }
535 )* };
536 (@l $L:ident, $V:ident, $t:ident, $z:ident, $q:ident, $mv:literal, $qp:ident, $qc:ident; $($lv:literal)*) => { $(
537 if $lv >= $mv + 2 && $lv <= $L {
538 let q_next = ($z * $qc)
539 .mul_adde($V::splat(at(&$t.a, tri($lv) + $mv)), $qp * $V::splat(at(&$t.nb, tri($lv) + $mv)));
540 put(&mut $q, tri($lv) + $mv, q_next);
541 $qp = $qc;
542 $qc = q_next;
543 }
544 )* };
545}
546
547/// Gradient-emission ladder (pass 2 of [`sh_d_impl`]): reads the `q` scratch, no
548/// recurrence state beyond the rolling `(c, s)` / `(cp, sp)` azimuthal window, so the
549/// inner rule is stateless.
550macro_rules! sh_grad_columns {
551 ($L:ident, $V:ident, $t:ident, $x:ident, $y:ident, $z:ident, $q:ident, $out:ident, $ddx:ident, $ddy:ident, $ddz:ident) => {
552 let mut c = $V::ONE;
553 let mut s = $V::ZERO;
554 let mut cp = $V::ZERO; // unused at m = 0 (the m factor is zero there)
555 let mut sp = $V::ZERO;
556 sh_grad_columns!(@m $L, $V, $t, $x, $y, $z, c, s, cp, sp, $q, $out, $ddx, $ddy, $ddz;
557 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16);
558 };
559 (@m $L:ident, $V:ident, $t:ident, $x:ident, $y:ident, $z:ident, $c:ident, $s:ident, $cp:ident, $sp:ident, $q:ident, $out:ident, $ddx:ident, $ddy:ident, $ddz:ident; $($mv:literal)*) => { $(
560 if $mv <= $L {
561 let mv = $V::splat(at(&$t.mf, $mv));
562
563 sh_grad_columns!(@l $L, $V, $t, $c, $s, $cp, $sp, $q, $out, $ddx, $ddy, $ddz, $mv, mv;
564 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16);
565
566 if $mv < $L {
567 let c_next = $x.difference_of_products($c, $y, $s);
568 let s_next = $x.sum_of_products($s, $y, $c);
569 $cp = $c;
570 $sp = $s;
571 $c = c_next;
572 $s = s_next;
573 }
574 }
575 )* };
576 (@l $L:ident, $V:ident, $t:ident, $c:ident, $s:ident, $cp:ident, $sp:ident, $q:ident, $out:ident, $ddx:ident, $ddy:ident, $ddz:ident, $mv:literal, $mfv:ident; $($lv:literal)*) => { $(
577 if $lv >= $mv && $lv <= $L {
578 let k = tri($lv) + $mv;
579 let base = $lv * ($lv + 1);
580
581 let qv = at(&$q, k);
582
583 // q_l^{m+1}, the z-derivative partner, zero on the diagonal.
584 let qn = if $mv == $lv { $V::ZERO } else { at(&$q, k + 1) };
585 let dq = $V::splat(at(&$t.f, k)) * qn;
586
587 if $mv == 0 {
588 // Y_{l,0} = q_l^0(z): no x/y dependence in the polynomial form.
589 put($out, base, qv);
590 put($ddx, base, $V::ZERO);
591 put($ddy, base, $V::ZERO);
592 put($ddz, base, dq);
593 } else {
594 put($out, base + $mv, qv * $c);
595 put($out, base - $mv, qv * $s);
596
597 // d{c,s}_m = m * {c,s}_{m-1} rotated: dc/dx = m c', dc/dy = -m s',
598 // ds/dx = m s', ds/dy = m c'.
599 let mq = $mfv * qv;
600 let mq_cp = mq * $cp;
601 let mq_sp = mq * $sp;
602
603 put($ddx, base + $mv, mq_cp);
604 put($ddy, base + $mv, -mq_sp);
605 put($ddx, base - $mv, mq_sp);
606 put($ddy, base - $mv, mq_cp);
607
608 put($ddz, base + $mv, dq * $c);
609 put($ddz, base - $mv, dq * $s);
610 }
611 }
612 )* };
613}
614
615/// All real spherical harmonics through degree `L` at the unit direction `(x, y, z)`.
616///
617/// `out[l * (l + 1) + m]` receives `$Y_{\ell m}$` for `m` in `-l..=l`, and `N` must
618/// equal `(L + 1)^2` (compile-time checked). See the module docs for
619/// conventions, the algorithm, and the unit-vector requirement.
620///
621/// The policy parameter is currently unused (the evaluation is pure polynomial
622/// arithmetic with one fixed, FMA-preferring lowering). It is accepted so the
623/// signature matches every sibling kernel and leaves room for policy-driven variants.
624// The `unused_comparisons`/`unused_assignments` allows are ladder artifacts: the
625// `0 <= L` guard of the first stamped column and the dead state hand-off of the last
626// stamped row are structurally unavoidable in machine-stamped straight-line code.
627#[allow(
628 clippy::extra_unused_type_parameters,
629 clippy::int_plus_one,
630 unused_comparisons,
631 unused_assignments
632)]
633#[inline(always)]
634pub fn sh_impl<P, E, V, const L: usize, const N: usize, const CS: bool>(x: V, y: V, z: V, out: &mut [V; N])
635where
636 P: Policy,
637 E: FloatElement + ShConsts<L, N, CS>,
638 V: FloatVector<Element = E> + CoreMath,
639{
640 const {
641 assert!(N == (L + 1) * (L + 1));
642 }
643
644 // Above the stamped ladder there is no unrolled code to run, so this delegates to
645 // the general path rather than silently leaving the high bands unwritten. Note the
646 // guard cannot live in the caller: rustc monomorphizes both arms of an `if const`
647 // whose condition involves a generic const parameter, so a cap assert here would
648 // fire from a statically-dead call site.
649 if const { L > MAX_DEGREE } {
650 let mut table = ShTable::<V, N>::zeroed();
651 sh_table_impl::<V, L, N, CS>(&mut table);
652 sh_eval_impl::<V, L, N>(&table, x, y, z, out);
653 return;
654 }
655
656 let t = &<E as ShConsts<L, N, CS>>::TABLE;
657
658 sh_value_columns!(L, V, t, x, y, z, out);
659}
660
661/// [`sh_impl`] plus the ambient Cartesian gradient of every harmonic.
662///
663/// `out` receives the values exactly as [`sh_impl`] produces them, and `ddx`/`ddy`/`ddz`
664/// receive `$\partial Y_{\ell m}/\partial\{x,y,z\}$` of the polynomial form at the
665/// given (unit) input. See the module docs for what that means off the sphere and
666/// how to project to the tangential gradient.
667///
668/// Two passes over an internal `q` scratch: the pure `z`-recurrence first, then one
669/// combining sweep that emits values and all three derivatives from tabulated ratios,
670/// with no recurrences beyond those [`sh_impl`] already runs.
671// PERF: the [V; N] scratch is zero-initialized (O(N) stores) and lives on the stack,
672// ~4 KB at L = 10 / f32x8. Fine for a leaf. Revisit (MaybeUninit or caller scratch)
673// if profiles ever notice.
674#[allow(
675 clippy::extra_unused_type_parameters,
676 clippy::int_plus_one,
677 unused_comparisons,
678 unused_assignments
679)]
680#[inline(always)]
681pub fn sh_d_impl<P, E, V, const L: usize, const N: usize, const CS: bool>(
682 x: V,
683 y: V,
684 z: V,
685 out: &mut [V; N],
686 ddx: &mut [V; N],
687 ddy: &mut [V; N],
688 ddz: &mut [V; N],
689) where
690 P: Policy,
691 E: FloatElement + ShConsts<L, N, CS>,
692 V: FloatVector<Element = E> + CoreMath,
693{
694 const {
695 assert!(N == (L + 1) * (L + 1));
696 }
697
698 // See `sh_impl`: above the ladder, delegate to the general path.
699 if const { L > MAX_DEGREE } {
700 let mut table = ShTable::<V, N>::zeroed();
701 sh_table_impl::<V, L, N, CS>(&mut table);
702 sh_eval_d_impl::<V, L, N>(&table, x, y, z, out, ddx, ddy, ddz);
703 return;
704 }
705
706 let t = &<E as ShConsts<L, N, CS>>::TABLE;
707
708 // Pass 1: every q_l^m, by column, into triangular scratch.
709 let mut q = [V::ZERO; N];
710 sh_q_columns!(L, V, t, z, q);
711
712 // Pass 2: combine scratch, tabulated ratios, and the rolling azimuthal window
713 // ((c, s) at column m, (cp, sp) at column m - 1) into values and gradients.
714 sh_grad_columns!(L, V, t, x, y, z, q, out, ddx, ddy, ddz);
715}
716
717// --- The general path: runtime coefficients, rolled loops, any degree ---
718//
719// The same normalized recurrence as the unrolled kernels above, with the constants
720// computed rather than tabulated. That buys two things the const-table path cannot
721// offer: degrees beyond `MAX_DEGREE` (whose table would be `6 * (L+1)^2` entries of
722// rodata, roughly half a megabyte at `L = 100`), and element types that have no
723// `ShConsts` impl at all, which is every composite.
724//
725// Numerically this IS the fast path, not an approximation of it: identical
726// recurrence, identical `O(1)` intermediates, no overflow at any degree. Only the
727// provenance of the coefficients differs, so the two can be diffed directly.
728//
729// All three are `#[inline(always)]`, not `#[inline]`. That is rule zero, not a
730// preference: target features propagate into a callee only when it is inlined, so a
731// merely-`#[inline]` kernel that rustc declines to inline compiles at the base ISA. It
732// was measured doing exactly that: 1851 instructions of SSE2 with no FMA and not one
733// `ymm` register, called from an AVX2 caller.
734//
735// Everything here needs nothing beyond `FloatVector` (add, mul, div, sqrt). Notably
736// `l` and `m` are carried as running `V` values incremented by `V::ONE` rather than
737// converted from integers, which keeps even the element-conversion traits out of the
738// bounds. Integer values this small are exact in any float format.
739
740/// Computes the recurrence coefficients for degree `L` into a runtime table.
741///
742/// The expensive half of the general path (two `sqrt` and two divisions per `(l, m)`),
743/// and the reason it is a separate entry point: it depends only on `L` and `CS`, never
744/// on the direction, so a caller evaluating many directions computes it once.
745///
746/// `CS` is baked in here, which is why [`sh_eval_impl`] does not take it. A filled
747/// table already knows its phase convention.
748#[allow(clippy::extra_unused_type_parameters)]
749#[inline(always)]
750pub fn sh_table_impl<V, const L: usize, const N: usize, const CS: bool>(t: &mut ShTable<V, N>)
751where
752 V: FloatVector,
753{
754 const {
755 assert!(N == (L + 1) * (L + 1));
756 }
757
758 let two = V::TWO;
759
760 // q_0^0 = sqrt(1/4pi) = (1/sqrt(pi)) / 2, exactly what the fast table seeds with.
761 let mut mag = V::FRAC_1_SQRT_PI / two;
762 put(&mut t.qmm, 0, mag);
763
764 // Diagonal: multiply by sqrt((2m+1)/(2m)) per step, with one extra sqrt(2) at
765 // m = 1 (the sqrt(2 - delta_{m0}) of the real normalization, entering once).
766 //
767 // The Condon-Shortley sign is applied to each entry as it is stored, and the
768 // recurrence is carried in `mag`, which stays unsigned. Feeding a SIGNED entry
769 // back into the next step instead would compound the phases: column m would come
770 // out with (-1)^(number of odd columns below it) rather than (-1)^m, which is
771 // right for odd m and wrong for even m.
772 let mut mv = V::ZERO;
773 let mut m = 1;
774 while m <= L {
775 mv += V::ONE;
776 let two_m = mv * two;
777 let mut d = ((two_m + V::ONE) / two_m).sqrt();
778 if m == 1 {
779 d *= two.sqrt();
780 }
781 mag *= d;
782 put(&mut t.qmm, m, if CS && m % 2 == 1 { -mag } else { mag });
783 m += 1;
784 }
785
786 let mut mv = V::ZERO;
787 let mut m = 0;
788 while m <= L {
789 put(&mut t.mf, m, mv);
790
791 if m < L {
792 put(&mut t.em, m, (mv * two + V::ONE + two).sqrt());
793 }
794
795 let mut lv = mv;
796 let mut l = m;
797 while l <= L {
798 let k = tri(l) + m;
799
800 let lm_lo = lv - mv; // l - m
801 let lm_hi = lv + mv; // l + m
802 let denom = lm_lo * lm_hi;
803 let two_l = lv * two;
804
805 if l >= m + 2 {
806 put(&mut t.a, k, ((two_l + V::ONE) * (two_l - V::ONE) / denom).sqrt());
807 put(
808 &mut t.nb,
809 k,
810 -(((two_l + V::ONE) * (lm_lo - V::ONE) * (lm_hi - V::ONE)) / (denom * (two_l - two - V::ONE)))
811 .sqrt(),
812 );
813 }
814
815 // d(q_l^m)/dz = f * q_l^{m+1}. Zero on the diagonal, and the m = 0 column
816 // picks up a 1/sqrt(2) because sqrt(2 - delta_{m0}) differs between the
817 // two columns the ratio spans.
818 if l > m {
819 let fv = if m == 0 {
820 (lv * (lv + V::ONE) / two).sqrt()
821 } else {
822 (lm_lo * (lm_hi + V::ONE)).sqrt()
823 };
824
825 // The ratio crosses columns m and m + 1, whose Condon-Shortley signs
826 // always disagree, so under CS every f flips regardless of parity.
827 put(&mut t.f, k, if CS { -fv } else { fv });
828 }
829
830 lv += V::ONE;
831 l += 1;
832 }
833
834 mv += V::ONE;
835 m += 1;
836 }
837}
838
839/// Evaluates all harmonics through degree `L` from a table filled by [`sh_table_impl`].
840///
841/// The rolled counterpart of [`sh_impl`], for any `L` and any `V`. The phase convention
842/// comes from the table, so there is no `CS` parameter here.
843#[allow(clippy::extra_unused_type_parameters)]
844#[inline(always)]
845pub fn sh_eval_impl<V, const L: usize, const N: usize>(t: &ShTable<V, N>, x: V, y: V, z: V, out: &mut [V; N])
846where
847 V: FloatVector + CoreMath,
848{
849 const {
850 assert!(N == (L + 1) * (L + 1));
851 }
852
853 let mut c = V::ONE;
854 let mut s = V::ZERO;
855
856 let mut m = 0;
857 while m <= L {
858 let q_diag = at(&t.qmm, m);
859 emit(out, m, m, q_diag, c, s);
860
861 if m < L {
862 let mut q_prev = q_diag;
863 let mut q_cur = (z * q_prev) * at(&t.em, m);
864 emit(out, m + 1, m, q_cur, c, s);
865
866 let mut l = m + 2;
867 while l <= L {
868 let k = tri(l) + m;
869 let q_next = (z * q_cur).mul_adde(at(&t.a, k), q_prev * at(&t.nb, k));
870 emit(out, l, m, q_next, c, s);
871
872 q_prev = q_cur;
873 q_cur = q_next;
874 l += 1;
875 }
876
877 let c_next = x.difference_of_products(c, y, s);
878 let s_next = x.sum_of_products(s, y, c);
879 c = c_next;
880 s = s_next;
881 }
882
883 m += 1;
884 }
885}
886
887/// [`sh_eval_impl`] over a table stored in `W`'s _primal_ type, each coefficient
888/// lifted through [`from_primal`](PrimalProjection::from_primal) as it is read.
889///
890/// The generic fallback behind `spherical_harmonics_with` now that tables are
891/// `Self::Primal`-typed. For a type that is its own primal (`Vector`, `Compensated`)
892/// the lift is the identity and this folds to exactly [`sh_eval_impl`], FMAs
893/// included. A composite gets correct-but-unspecialized code (its constants carry
894/// zeroed augmentation through full composite multiplies), which is why `Dual`
895/// overrides the method with [`sh_eval_mixed_impl`] instead.
896#[allow(clippy::extra_unused_type_parameters)]
897#[inline(always)]
898pub fn sh_eval_lifted_impl<W, const L: usize, const N: usize>(
899 t: &ShTable<W::Primal, N>,
900 x: W,
901 y: W,
902 z: W,
903 out: &mut [W; N],
904) where
905 W: FloatVector + PrimalProjection + CoreMath,
906{
907 const {
908 assert!(N == (L + 1) * (L + 1));
909 }
910
911 let mut c = W::ONE;
912 let mut s = W::ZERO;
913
914 let mut m = 0;
915 while m <= L {
916 let q_diag = W::from_primal(at(&t.qmm, m));
917 emit(out, m, m, q_diag, c, s);
918
919 if m < L {
920 let mut q_prev = q_diag;
921 let mut q_cur = (z * q_prev) * W::from_primal(at(&t.em, m));
922 emit(out, m + 1, m, q_cur, c, s);
923
924 let mut l = m + 2;
925 while l <= L {
926 let k = tri(l) + m;
927 let q_next = (z * q_cur).mul_adde(W::from_primal(at(&t.a, k)), q_prev * W::from_primal(at(&t.nb, k)));
928 emit(out, l, m, q_next, c, s);
929
930 q_prev = q_cur;
931 q_cur = q_next;
932 l += 1;
933 }
934
935 let c_next = x.difference_of_products(c, y, s);
936 let s_next = x.sum_of_products(s, y, c);
937 c = c_next;
938 s = s_next;
939 }
940
941 m += 1;
942 }
943}
944
945/// [`sh_eval_impl`] with the coefficients kept in a _different_, simpler type than the
946/// values.
947///
948/// The case this exists for is a composite `W` (a `Dual`, say) evaluated against a
949/// table of plain real coefficients. Every recurrence constant has a zero derivative,
950/// so carrying it as a `Dual` means computing `a.re * 0.0` cross terms for each one,
951/// which LLVM cannot fold away under strict IEEE (`a.re` could be an infinity or a
952/// NaN). Typing the table by `R`'s primal instead turns each of those into
953/// `Dual * real`, which `thermite-dual` implements as `1 + N` multiplies rather than
954/// `1 + 2N`.
955///
956/// It also shrinks the table itself, which is the larger saving in practice: a
957/// `ShTable<Dual<V, 3>, 25>` is 600 vector stores to fill, against 150 for
958/// `ShTable<V, 25>`, and the general path fills one per call.
959///
960/// Deliberately _not_ a generalization of [`sh_eval_impl`]. The single-type version
961/// folds its recurrence into `mul_adde`, and no fused multiply-add spans two operand
962/// types, so merging them would cost the real path its FMAs to benefit the composite
963/// one. The duplicated body is about twenty lines and neither copy has to compromise.
964#[allow(clippy::extra_unused_type_parameters)]
965#[inline(always)]
966pub fn sh_eval_mixed_impl<W, R, const L: usize, const N: usize>(
967 t: &ShTable<R::Primal, N>,
968 x: W,
969 y: W,
970 z: W,
971 out: &mut [W; N],
972) where
973 W: FloatVector + core::ops::Mul<R, Output = W> + CoreMath,
974 R: PrimalProjection,
975{
976 const {
977 assert!(N == (L + 1) * (L + 1));
978 }
979
980 let mut c = W::ONE;
981 let mut s = W::ZERO;
982
983 let mut m = 0;
984 while m <= L {
985 // The table is `R::Primal`-typed. `R::from_primal` lifts an entry to `R` (the
986 // identity for a plain real `R`), and `W::ONE * r` lifts that into `W` without
987 // needing a conversion trait: for a real `W` it is the identity LLVM folds
988 // away, and for a `Dual` it produces the constant with a zero derivative
989 // directly.
990 let q_diag = W::ONE * R::from_primal(at(&t.qmm, m));
991 emit(out, m, m, q_diag, c, s);
992
993 if m < L {
994 let mut q_prev = q_diag;
995 let mut q_cur = (z * q_prev) * R::from_primal(at(&t.em, m));
996 emit(out, m + 1, m, q_cur, c, s);
997
998 let mut l = m + 2;
999 while l <= L {
1000 let k = tri(l) + m;
1001 let q_next = (z * q_cur) * R::from_primal(at(&t.a, k)) + q_prev * R::from_primal(at(&t.nb, k));
1002 emit(out, l, m, q_next, c, s);
1003
1004 q_prev = q_cur;
1005 q_cur = q_next;
1006 l += 1;
1007 }
1008
1009 let c_next = x.difference_of_products(c, y, s);
1010 let s_next = x.sum_of_products(s, y, c);
1011 c = c_next;
1012 s = s_next;
1013 }
1014
1015 m += 1;
1016 }
1017}
1018
1019/// [`sh_eval_impl`] plus the ambient Cartesian gradients. The rolled counterpart of
1020/// [`sh_d_impl`], with the same two-pass structure and gradient semantics.
1021#[allow(clippy::extra_unused_type_parameters, clippy::too_many_arguments)]
1022#[inline(always)]
1023pub fn sh_eval_d_impl<V, const L: usize, const N: usize>(
1024 t: &ShTable<V, N>,
1025 x: V,
1026 y: V,
1027 z: V,
1028 out: &mut [V; N],
1029 ddx: &mut [V; N],
1030 ddy: &mut [V; N],
1031 ddz: &mut [V; N],
1032) where
1033 V: FloatVector + CoreMath,
1034{
1035 const {
1036 assert!(N == (L + 1) * (L + 1));
1037 }
1038
1039 // Pass 1: the z-recurrence into triangular scratch.
1040 let mut q = [V::ZERO; N];
1041
1042 let mut m = 0;
1043 while m <= L {
1044 let q_diag = at(&t.qmm, m);
1045 put(&mut q, tri(m) + m, q_diag);
1046
1047 if m < L {
1048 let mut q_prev = q_diag;
1049 let mut q_cur = (z * q_prev) * at(&t.em, m);
1050 put(&mut q, tri(m + 1) + m, q_cur);
1051
1052 let mut l = m + 2;
1053 while l <= L {
1054 let k = tri(l) + m;
1055 let q_next = (z * q_cur).mul_adde(at(&t.a, k), q_prev * at(&t.nb, k));
1056 put(&mut q, k, q_next);
1057
1058 q_prev = q_cur;
1059 q_cur = q_next;
1060 l += 1;
1061 }
1062 }
1063
1064 m += 1;
1065 }
1066
1067 // Pass 2: values and gradients from the scratch and the tabulated ratios.
1068 let mut c = V::ONE;
1069 let mut s = V::ZERO;
1070 let mut cp = V::ZERO;
1071 let mut sp = V::ZERO;
1072
1073 let mut m = 0;
1074 while m <= L {
1075 let mv = at(&t.mf, m);
1076
1077 let mut l = m;
1078 while l <= L {
1079 let k = tri(l) + m;
1080 let base = l * (l + 1);
1081
1082 let qv = at(&q, k);
1083 let qn = if m == l { V::ZERO } else { at(&q, k + 1) };
1084 let dq = at(&t.f, k) * qn;
1085
1086 if m == 0 {
1087 put(out, base, qv);
1088 put(ddx, base, V::ZERO);
1089 put(ddy, base, V::ZERO);
1090 put(ddz, base, dq);
1091 } else {
1092 put(out, base + m, qv * c);
1093 put(out, base - m, qv * s);
1094
1095 let mq = mv * qv;
1096 let mq_cp = mq * cp;
1097 let mq_sp = mq * sp;
1098
1099 put(ddx, base + m, mq_cp);
1100 put(ddy, base + m, -mq_sp);
1101 put(ddx, base - m, mq_sp);
1102 put(ddy, base - m, mq_cp);
1103
1104 put(ddz, base + m, dq * c);
1105 put(ddz, base - m, dq * s);
1106 }
1107
1108 l += 1;
1109 }
1110
1111 if m < L {
1112 let c_next = x.difference_of_products(c, y, s);
1113 let s_next = x.sum_of_products(s, y, c);
1114 cp = c;
1115 sp = s;
1116 c = c_next;
1117 s = s_next;
1118 }
1119
1120 m += 1;
1121 }
1122}