Skip to main content

gam_math/
jet_partitions.rs

1//! Bitmask-coefficient multi-directional jets used by marginal-slope and
2//! latent-survival row kernels.
3//!
4//! The layout stores one coefficient per direction mask. The calculus itself
5//! lives in [`crate::jet_algebra`]: that module owns the layout-agnostic
6//! Leibniz / Faà di Bruno *combinatorics* once, and the scalar (`n_dirs <= 1`)
7//! path here still routes through it so a fix to the rule is a fix to both
8//! representations.
9//!
10//! ## Why this layout is special (and how the hot path exploits it)
11//!
12//! Each direction is seeded *linearly* (one first-derivative slot), so every
13//! direction variable squares to zero. The coefficients therefore form the
14//! commutative **multilinear / set-function algebra**: `coeffs[mask]` is the
15//! coefficient of `Π_{i ∈ mask} ε_i`. In that algebra two facts collapse the
16//! generic combinatorial walkers into tight branch-free arithmetic:
17//!
18//! * **`mul` is the subset (zeta-style) convolution**
19//!   `out[mask] = Σ_{sub ⊆ mask} a[sub] · b[mask \ sub]`.
20//!   The shared `leibniz_product` walker rebuilds two `SlotBuf`s and folds bit
21//!   lists back into masks (`mask_of`) *per subset*; here we enumerate the
22//!   submasks of `mask` directly — `mask \ sub == mask ^ sub` because
23//!   `sub ⊆ mask` — in the **same ascending order** the walker used, so the
24//!   floating-point accumulation is bit-for-bit identical while every
25//!   `SlotBuf`/closure/`mask_of` allocation and indirection disappears
26//!   (`3^K` pure FMAs, no heap, no `dyn`).
27//!
28//! * **`compose_unary` is the truncated Faà di Bruno composition**, computed
29//!   here from the *multilinear powers* of the non-constant part rather than a
30//!   direct set-partition sum. Let `v` be the non-constant part of `self`
31//!   (`v[0] = 0`, `v[mask] = self[mask]`) and let `v^{⊛k}` be the `k`-fold
32//!   *subset convolution* (the multilinear power). The ordered-tuple identity
33//!   `v^{⊛k}[mask] = k! · Σ_{π ⊢ mask, |π| = k} Π_{B ∈ π} v[B]` turns the
34//!   set-partition sum into a degree-4 polynomial in `v`:
35//!
36//!   ```text
37//!   f(self)[mask] = Σ_{k=0}^{4} (f^{(k)} / k!) · v^{⊛k}[mask]      (mask ≠ 0)
38//!   f(self)[0]    = f^{(0)}
39//!   ```
40//!
41//!   The powers themselves are built by the **pointed (lowest-set-bit)
42//!   recurrence**, not by full subset convolutions. Write `ℓ` for the lowest
43//!   set bit of `mask`. In any partition of `mask` exactly one block owns `ℓ`,
44//!   and the `k` blocks of an ordered `k`-tuple are interchangeable, so pinning
45//!   the outer block to the one containing `ℓ` counts each partition once
46//!   instead of `k` times:
47//!
48//!   ```text
49//!   v^{⊛k}[mask] = k · Σ_{B ⊆ mask, ℓ ∈ B} v[B] · v^{⊛(k-1)}[mask \ B]
50//!   ```
51//!
52//!   This is an exact identity (`k = 2, 3, 4` reproduce `v²`, `v³`, `v⁴` to
53//!   roundoff against a brute-force partition sum, gated in `tests`), and it is
54//!   what makes the schedule cheap at small `K`: the complements `mask \ B`
55//!   range over submasks of `mask ^ ℓ` rather than of `mask`, and the surviving
56//!   term set shrinks with `k` because `v^{⊛(k-1)}` vanishes below popcount
57//!   `k-1`. All three powers therefore share **one** descending submask walk
58//!   whose `k = 3` and `k = 4` chains are popcount-suffixes of the `k = 2`
59//!   chain — one walk, one `v[B]` load, and three independent Dot2 chains to
60//!   interleave.
61//!
62//!   Each accumulation is a compensated dot product (Ogita–Rump–Oishi Dot2,
63//!   FMA-split products + TwoSum carry) so the result is computed in ~double
64//!   the working precision and the rounding of `v²` cannot compound through
65//!   `v³`/`v⁴`; the integer multiplicity `k` is applied with its own FMA split
66//!   so it costs one rounding rather than discarding the compensated tail; the
67//!   final per-mask combine is Neumaier-compensated and `wide::f64x4`-vectorised;
68//!   and the whole call runs on reused thread-local scratch with no per-call
69//!   heap traffic.
70//!
71//! ### What this schedule costs
72//!
73//! Everything below is recomputed from the enumeration itself by
74//! `compose_unary_work_model_matches_the_closed_form`, which replays the walk and
75//! counts its steps. A counted model cannot drift the way a prose factor did:
76//! this header once claimed "~3× fewer FLOPs than the partition gather" for a
77//! schedule that in fact cost ~9× as much at the `K` production runs at.
78//!
79//! * the pointed recurrence walks `Σ_{p ≥ 2} C(K,p)·Σ_{k=2..4, k ≤ p} Σ_{j ≥ k-1}
80//!   C(p-1,j)` terms, i.e. `Θ(3^K)`, each a compensated Dot2 (10 flops), plus a
81//!   5-flop multiplicity epilogue per power per mask;
82//! * the partition gather walks `Σ_p C(K,p)·B_{≤4}(p)` terms, i.e. `Θ(5^K/4!)`,
83//!   each `|π|` plain multiplies and an add. (That count is a *lower bound* on
84//!   the gather: it omits the `2^p` per-mask index remap the gather also needs,
85//!   so every comparison below is stated against the gather at its best.)
86//!
87//! ```text
88//!   K                        2     3     4     6      8      9     10     12
89//!   pointed terms            1     7    34   534   6514  21589  69886  696810
90//!   gather terms             5    15    52   855  18002  86472 422005 10306752
91//!   pointed/gather flops  2.5x  3.5x  3.3x  2.0x  0.91x  0.59x  0.37x   0.15x
92//! ```
93//!
94//! Two facts worth carrying:
95//!
96//! * The three-full-subset-convolution schedule this replaced walked **exactly
97//!   4×** as many terms at every `K ≤ 4` (136 against 34 at `K = 4`) — one factor
98//!   of 2 from pinning the block that owns `ℓ`, one from walking submasks of
99//!   `mask ^ ℓ` instead of `mask`. Its flop crossover against the gather sat at
100//!   `K = 10`; the pointed recurrence moves it to `K = 8`.
101//! * At `K = 4` the schedule walks 34 terms against the gather's 52. It does
102//!   strictly *less* combinatorial work than the partition sum it replaced —
103//!   at every `K`, not just past a crossover — and the residual 3.3× flop ratio
104//!   at `K = 4` is entirely the Dot2 compensation: 10 flops a term against ~3.
105//!   That is the accuracy the double-double gate pins, and the only reason to
106//!   prefer this schedule; it is not free, and a reader sizing a new call site
107//!   should plan for it.
108use std::cell::RefCell;
109use std::sync::atomic::{AtomicU64, Ordering};
110use wide::f64x4;
111
112pub static COMPOSE_UNARY_CALLS: AtomicU64 = AtomicU64::new(0);
113pub static MUL_CALLS: AtomicU64 = AtomicU64::new(0);
114
115/// Length of the unary derivative stack `[f, f', f'', f''', f'''']`: composition
116/// is exact through order 4, partitions into `>= 5` blocks are truncated.
117const DERIVS: usize = 5;
118
119#[derive(Clone)]
120pub struct MultiDirJet {
121    pub coeffs: Vec<f64>,
122}
123
124impl MultiDirJet {
125    pub fn zero(n_dirs: usize) -> Self {
126        Self {
127            coeffs: vec![0.0; 1usize << n_dirs],
128        }
129    }
130
131    pub fn constant(n_dirs: usize, value: f64) -> Self {
132        let mut out = Self::zero(n_dirs);
133        out.coeffs[0] = value;
134        out
135    }
136
137    pub fn linear(n_dirs: usize, base: f64, first: &[f64]) -> Self {
138        let mut out = Self::constant(n_dirs, base);
139        for (idx, &value) in first.iter().take(n_dirs).enumerate() {
140            out.coeffs[1usize << idx] = value;
141        }
142        out
143    }
144
145    pub fn with_coeffs(n_dirs: usize, coeffs: &[(usize, f64)]) -> Self {
146        let mut out = Self::zero(n_dirs);
147        for &(mask, value) in coeffs {
148            if mask < out.coeffs.len() {
149                out.coeffs[mask] = value;
150            }
151        }
152        out
153    }
154
155    #[inline]
156    pub fn coeff(&self, mask: usize) -> f64 {
157        self.coeffs[mask]
158    }
159
160    pub fn add(&self, other: &Self) -> Self {
161        Self {
162            coeffs: self
163                .coeffs
164                .iter()
165                .zip(other.coeffs.iter())
166                .map(|(lhs, rhs)| lhs + rhs)
167                .collect(),
168        }
169    }
170
171    pub fn scale(&self, scalar: f64) -> Self {
172        Self {
173            coeffs: self.coeffs.iter().map(|value| scalar * value).collect(),
174        }
175    }
176
177    /// Subset-convolution product `out[mask] = Σ_{sub ⊆ mask} a[sub]·b[mask^sub]`.
178    ///
179    /// Bit-identical to the shared `crate::jet_algebra::leibniz_product` walker
180    /// (the submasks are enumerated in the same ascending order — the walker's
181    /// compacted subset index is a monotone bit-deposit of the submask) while
182    /// dropping its per-subset `SlotBuf`/closure/`mask_of` overhead. The scalar
183    /// `n_dirs == 0` case keeps the shared walker live as its reference.
184    pub fn mul(&self, other: &Self) -> Self {
185        MUL_CALLS.fetch_add(1, Ordering::Relaxed);
186        let count = self.coeffs.len();
187        if count <= 1 {
188            return self.mul_reference(other);
189        }
190        let a = &self.coeffs;
191        let b = &other.coeffs;
192        // Both operands carry the same direction set, so `b` is `count` long too.
193        // With that established once, every `a[sub]`/`b[mask ^ sub]` below is
194        // provably in bounds (`sub, mask ^ sub ⊆ mask < count`), so the inner
195        // submask walk can drop its per-load bounds checks.
196        assert_eq!(
197            b.len(),
198            count,
199            "MultiDirJet::mul operands must share n_dirs"
200        );
201        let mut out = vec![0.0; count];
202        for (mask, slot) in out.iter_mut().enumerate() {
203            // Walk every submask of `mask` in ascending numeric order — the same
204            // order `leibniz_product` accumulates — via the classic gap-fill
205            // increment `next = ((sub | !mask) + 1) & mask`.
206            let mut acc = 0.0;
207            let mut sub = 0usize;
208            // SAFETY: `sub ⊆ mask < count` and `mask ^ sub ⊆ mask < count`, and
209            // both `a` and `b` are `count` long (asserted above).
210            unsafe {
211                loop {
212                    acc += *a.get_unchecked(sub) * *b.get_unchecked(mask ^ sub);
213                    if sub == mask {
214                        break;
215                    }
216                    sub = (sub | !mask).wrapping_add(1) & mask;
217                }
218            }
219            *slot = acc;
220        }
221        Self { coeffs: out }
222    }
223
224    /// The pre-#perf shared-walker product, retained verbatim as the scalar-case
225    /// implementation and as the bit-exact reference for `mul`.
226    fn mul_reference(&self, other: &Self) -> Self {
227        let count = self.coeffs.len();
228        let mut out = vec![0.0; count];
229        for (mask, slot) in out.iter_mut().enumerate() {
230            let bits = bit_positions(mask);
231            *slot = crate::jet_algebra::leibniz_product(
232                bits.as_slice(),
233                |t| self.coeffs[mask_of(t)],
234                |c| other.coeffs[mask_of(c)],
235            );
236        }
237        Self { coeffs: out }
238    }
239
240    /// Exact (order-4 truncated) unary composition `f(self)` from the Taylor
241    /// stack `[f, f', f'', f''', f'''']` at `self.coeff(0)`.
242    ///
243    /// Computed by the truncated-Taylor reassociation (see the module note):
244    /// `f(self) = Σ_{k=0}^{4} (f^{(k)}/k!)·v^{⊛k}` with `v` the non-constant
245    /// part of `self`. The three subset-convolution powers `v²`, `v³`, `v⁴`
246    /// are compensated (Dot2) and the per-mask combine is Neumaier-compensated
247    /// and vectorised, so the result is *more* accurate vs. the true
248    /// real-arithmetic value than the prior naive partition sum (proven against
249    /// a double-double oracle in `tests`). The scalar `n_dirs == 0` case keeps
250    /// the shared Faà di Bruno walker live as its reference.
251    pub fn compose_unary(&self, derivs: [f64; DERIVS]) -> Self {
252        COMPOSE_UNARY_CALLS.fetch_add(1, Ordering::Relaxed);
253        let count = self.coeffs.len();
254        if count <= 1 {
255            return <Self as crate::jet_algebra::JetAlgebra<DERIVS>>::compose_unary(self, derivs);
256        }
257        let mut out = vec![0.0; count];
258        COMPOSE_SCRATCH.with(|cell| {
259            let mut buf = cell.borrow_mut();
260            buf.clear();
261            buf.resize(4 * count, 0.0);
262            compose_unary_coefficients_into(&self.coeffs, derivs, buf.as_mut_slice(), &mut out);
263        });
264        Self { coeffs: out }
265    }
266}
267
268thread_local! {
269    /// Reused composition scratch (`4·count` f64s: v, v², v³, v⁴). Sized up on
270    /// demand and never freed, so a steady-state `compose_unary` does zero heap
271    /// work beyond the owned output `Vec`.
272    static COMPOSE_SCRATCH: RefCell<Vec<f64>> = const { RefCell::new(Vec::new()) };
273}
274
275#[inline]
276fn compose_unary_coefficients_into(
277    coefficients: &[f64],
278    derivs: [f64; DERIVS],
279    scratch: &mut [f64],
280    out: &mut [f64],
281) {
282    let count = coefficients.len();
283    assert!(count > 1 && count.is_power_of_two());
284    assert!(scratch.len() == 4 * count && out.len() == count);
285    let (vbuf, tail) = scratch.split_at_mut(count);
286    let (p2, tail) = tail.split_at_mut(count);
287    let (p3, p4) = tail.split_at_mut(count);
288
289    // v is the non-constant part of the input. The k=0 Taylor term owns the
290    // constant coefficient, so the zero mask must not enter any power.
291    vbuf.copy_from_slice(coefficients);
292    vbuf[0] = 0.0;
293
294    // The three multilinear powers, by the pointed recurrence (module header).
295    multilinear_powers_into(vbuf, p2, p3, p4);
296    // `1/k!` undoes the ordered-tuple overcount of each k-fold subset power
297    // relative to the unordered set-partition sum.
298    let coefficients_by_order = [
299        derivs[1],
300        derivs[2] * 0.5,
301        derivs[3] * (1.0 / 6.0),
302        derivs[4] * (1.0 / 24.0),
303    ];
304    combine_powers(vbuf, p2, p3, p4, coefficients_by_order, out);
305    out[0] = derivs[0];
306}
307
308/// Branchless TwoSum: returns `(s, e)` with `s = fl(a+b)` and `a+b = s+e`
309/// exactly (Knuth/Møller). Used by the compensated power recurrence and combine.
310#[inline(always)]
311fn two_sum(a: f64, b: f64) -> (f64, f64) {
312    let s = a + b;
313    let bb = s - a;
314    let e = (a - (s - bb)) + (b - bb);
315    (s, e)
316}
317
318/// One step of an Ogita–Rump–Oishi Dot2: accumulate `x·y` into `(s, c)` so that
319/// `s + c` carries the running sum in ~twice the working precision. The product
320/// is split into head plus exact FMA error, and the addition's rounding error is
321/// recovered by TwoSum, so neither the product nor the sum silently drops bits.
322#[inline(always)]
323fn dot2_step(s: &mut f64, c: &mut f64, x: f64, y: f64) {
324    let prod = x * y;
325    let prod_err = x.mul_add(y, -prod); // exact: prod + prod_err == x*y
326    let (t, sum_err) = two_sum(*s, prod);
327    *s = t;
328    *c += prod_err + sum_err;
329}
330
331/// `k·(s + c)` for a small integer multiplicity `k`, with `k·s` split into head
332/// plus exact FMA error so the multiplicity costs one final rounding rather than
333/// discarding the compensated tail. For `k ∈ {2, 4}` the split is identically
334/// zero (both are exact scalings); `k = 3` is the case that needs it.
335#[inline(always)]
336fn scaled_compensated(k: f64, s: f64, c: f64) -> f64 {
337    let hi = k * s;
338    let lo = k.mul_add(s, -hi); // exact: hi + lo == k*s
339    hi + (lo + k * c)
340}
341
342/// The multilinear powers `v^{⊛2}`, `v^{⊛3}`, `v^{⊛4}` of the non-constant part
343/// `v`, by the **pointed (lowest-set-bit) recurrence** derived in the module
344/// header:
345///
346/// ```text
347/// v^{⊛k}[mask] = k · Σ_{t ⊊ mask, ℓ ∉ t} v[mask \ t] · v^{⊛(k-1)}[t]
348/// ```
349///
350/// with `ℓ` the lowest set bit of `mask`. Pinning the block that owns `ℓ` counts
351/// each partition once instead of `k` times, and the surviving `t` range over
352/// submasks of `mask ^ ℓ` rather than of `mask` — together an exactly 4× shorter
353/// walk than the three full subset convolutions this replaced, at every
354/// `K ≤ 4` (see `compose_unary_work_model_matches_the_closed_form`).
355///
356/// All three powers share **one** descending walk over the submasks of
357/// `mask ^ ℓ`, because their term sets are nested: `v^{⊛(k-1)}[t]` vanishes
358/// below `popcount(t) = k - 1`, so the `k = 3` and `k = 4` chains are the
359/// `popcount ≥ 2` and `popcount ≥ 3` suffixes of the `k = 2` chain. Sharing the
360/// walk also shares the `v[mask \ t]` load and gives three independent Dot2
361/// dependency chains to interleave, which is what the old kernel's four-way
362/// unroll was buying separately.
363///
364/// Every accumulation is a compensated Dot2, so the rounding of `v²` cannot
365/// compound through `v³`/`v⁴`. Masks below popcount `k` are left at zero: the
366/// `k`-fold multilinear power vanishes there, so the prune is exact.
367#[inline]
368fn multilinear_powers_into(v: &[f64], p2: &mut [f64], p3: &mut [f64], p4: &mut [f64]) {
369    let count = v.len();
370    // SAFETY precondition for the `get_unchecked` loads below, pinned once per
371    // call (negligible next to the walk): all four buffers are `count` long.
372    // Every index read is either `t` or `mask ^ t` for `t ⊆ mask < count`, and
373    // both are submasks of `mask`, hence `< count`. The per-load bounds checks
374    // LLVM cannot elide (the indices are data-dependent) are a real cost across
375    // the exponential walk, and eliding them measured ~20% on the kernel this
376    // replaced.
377    assert!(p2.len() == count && p3.len() == count && p4.len() == count);
378    if count > 0 {
379        p2[0] = 0.0;
380        p3[0] = 0.0;
381        p4[0] = 0.0;
382    }
383    for mask in 1..count {
384        // `v^{⊛k}` vanishes below popcount k, so a popcount-1 mask is all-zero
385        // in every power and never enters a walk.
386        let lowest = mask & mask.wrapping_neg();
387        let rest = mask ^ lowest;
388        if rest == 0 {
389            p2[mask] = 0.0;
390            p3[mask] = 0.0;
391            p4[mask] = 0.0;
392            continue;
393        }
394        let (mut s2, mut c2) = (0.0f64, 0.0f64);
395        let (mut s3, mut c3) = (0.0f64, 0.0f64);
396        let (mut s4, mut c4) = (0.0f64, 0.0f64);
397        // Descending submask walk `t = (t - 1) & rest` over the NONZERO submasks
398        // of `rest` (the classic Gosper-style enumeration). `t = 0` is skipped
399        // because it is the one term whose complement is the whole mask, and
400        // `v^{⊛(k-1)}[0] = 0` for every `k ≥ 2`.
401        let mut t = rest;
402        while t != 0 {
403            // SAFETY: `t ⊆ rest ⊂ mask < count` and `mask ^ t ⊆ mask < count`,
404            // and all four buffers are `count` long (asserted above).
405            unsafe {
406                let block = *v.get_unchecked(mask ^ t);
407                dot2_step(&mut s2, &mut c2, block, *v.get_unchecked(t));
408                let popcount = (t as u64).count_ones();
409                if popcount >= 2 {
410                    dot2_step(&mut s3, &mut c3, block, *p2.get_unchecked(t));
411                    if popcount >= 3 {
412                        dot2_step(&mut s4, &mut c4, block, *p3.get_unchecked(t));
413                    }
414                }
415            }
416            t = (t - 1) & rest;
417        }
418        // The pointed recurrence's multiplicity. `v^{⊛k}[mask]` must be written
419        // before the `k+1` chain of any LATER mask reads it, and `t < mask`
420        // strictly for every `t ⊆ mask ^ ℓ`, so writing all three here keeps the
421        // recurrence's read-before-write order across the ascending mask loop.
422        p2[mask] = scaled_compensated(2.0, s2, c2);
423        p3[mask] = scaled_compensated(3.0, s3, c3);
424        p4[mask] = scaled_compensated(4.0, s4, c4);
425    }
426}
427
428/// `out[mask] = c[0]·p1 + c[1]·p2 + c[2]·p3 + c[3]·p4` for `mask ≥ 1`, with a
429/// Neumaier-compensated four-term accumulation (the powers span growing
430/// magnitudes, so the compensation recovers the bits a naive `+=` would drop)
431/// and a `wide::f64x4` body over four masks at a time. `out[0]` is overwritten
432/// by the caller with the value channel.
433#[inline]
434fn combine_powers(p1: &[f64], p2: &[f64], p3: &[f64], p4: &[f64], c: [f64; 4], out: &mut [f64]) {
435    let n = out.len();
436    let (c1, c2, c3, c4) = (c[0], c[1], c[2], c[3]);
437    let (v1, v2, v3, v4) = (
438        f64x4::splat(c1),
439        f64x4::splat(c2),
440        f64x4::splat(c3),
441        f64x4::splat(c4),
442    );
443    let mut mask = 0usize;
444    // Vector body: four contiguous masks per step. Neumaier compensation is
445    // applied lane-wise; pick the larger magnitude to subtract first.
446    while mask + 4 <= n {
447        let load = |p: &[f64]| f64x4::new([p[mask], p[mask + 1], p[mask + 2], p[mask + 3]]);
448        let mut s = v1 * load(p1);
449        let mut comp = f64x4::splat(0.0);
450        for (cv, pv) in [(v2, p2), (v3, p3), (v4, p4)] {
451            let term = cv * load(pv);
452            let t = s + term;
453            let big_s = s.abs().simd_ge(term.abs());
454            let lost = big_s.blend((s - t) + term, (term - t) + s);
455            comp += lost;
456            s = t;
457        }
458        let res = s + comp;
459        out[mask..mask + 4].copy_from_slice(&res.to_array());
460        mask += 4;
461    }
462    // Scalar tail (and the small-K path where `n < 4`).
463    while mask < n {
464        let mut s = c1 * p1[mask];
465        let mut comp = 0.0f64;
466        for (cv, pv) in [(c2, p2), (c3, p3), (c4, p4)] {
467            let term = cv * pv[mask];
468            let (t, e) = two_sum(s, term);
469            comp += e;
470            s = t;
471        }
472        out[mask] = s + comp;
473        mask += 1;
474    }
475}
476
477impl crate::jet_algebra::JetAlgebra<DERIVS> for MultiDirJet {
478    #[inline]
479    fn derivative(&self, slots: &[usize]) -> f64 {
480        self.coeffs[mask_of(slots)]
481    }
482
483    fn map_derivatives<F>(&self, mut f: F) -> Self
484    where
485        F: FnMut(&[usize]) -> f64,
486    {
487        let mut out = vec![0.0; self.coeffs.len()];
488        for (mask, value) in out.iter_mut().enumerate() {
489            let bits = bit_positions(mask);
490            *value = f(bits.as_slice());
491        }
492        Self { coeffs: out }
493    }
494}
495
496/// The set-bit positions of `mask`, low to high — the differentiation slots of
497/// that coefficient.
498fn bit_positions(mask: usize) -> crate::jet_algebra::SlotBuf {
499    let mut out = crate::jet_algebra::SlotBuf::new();
500    let mut m = mask;
501    while m != 0 {
502        let bit = m.trailing_zeros() as usize;
503        out.push_slot(bit);
504        m &= m - 1;
505    }
506    out
507}
508
509/// Combine a slot-group (list of bit positions) back into a sub-mask.
510fn mask_of(slots: &[usize]) -> usize {
511    slots.iter().fold(0usize, |acc, &b| acc | (1usize << b))
512}
513
514// #932-2 cutover: `MultiDirJet::bilinear` (the 4-coeff `[base, d1, d2, d12]`
515// constructor) and `MultiDirJet::sub` are consumed ONLY by the now test-only hand
516// survival directional/bidirectional oracle (the production flex jet path uses the
517// `flex_jet` runtime jet algebra, not `MultiDirJet`). After the #1521 crate split
518// moved `MultiDirJet` into `gam-math`, those oracle tests live in the dependent
519// `gam` crate, where a `#[cfg(test)]` gate in *this* crate is inactive — so the
520// methods must be plain `pub` inherent methods to be reachable cross-crate. They
521// carry no dead-code cost because `pub` items are part of the crate's public API.
522// Bodies are byte-identical to their former gated form.
523impl MultiDirJet {
524    pub fn bilinear(base: f64, d1: f64, d2: f64, d12: f64) -> Self {
525        Self {
526            coeffs: vec![base, d1, d2, d12],
527        }
528    }
529
530    pub fn sub(&self, other: &Self) -> Self {
531        Self {
532            coeffs: self
533                .coeffs
534                .iter()
535                .zip(other.coeffs.iter())
536                .map(|(lhs, rhs)| lhs - rhs)
537                .collect(),
538        }
539    }
540}
541
542#[cfg(test)]
543mod tests {
544    use super::*;
545
546    /// A flattened set-partition table for a fixed slot count. `parts[i] = (off,
547    /// order)` describes one partition: its `order` block submasks (compacted) are
548    /// `flat[off .. off + order]`.
549    ///
550    /// This direct set-partition sum is the previous production `compose_unary`
551    /// implementation, retained as the **accuracy reference** the new
552    /// truncated-Taylor path is graded against: a double-double oracle is the
553    /// truth, and the test asserts the new path's error-vs-truth is `≤` this naive
554    /// partition sum's error-vs-truth on every randomised program.
555    struct PartTable {
556        flat: Vec<u32>,
557        parts: Vec<(usize, u8)>,
558    }
559
560    thread_local! {
561        /// Cached set-partition tables, indexed by slot count `m`. Entry `m` holds
562        /// every partition of `{0..m}` into `< DERIVS` blocks, in the shared
563        /// walker's recursion order, each block a compacted submask. Pure function
564        /// of `m`, so caching is sound and deterministic.
565        static PARTITION_TABLES: RefCell<Vec<std::rc::Rc<PartTable>>> =
566            const { RefCell::new(Vec::new()) };
567    }
568
569    /// Return cached partition tables for slot counts `0..=n_dirs`.
570    fn partition_tables(n_dirs: usize) -> Vec<std::rc::Rc<PartTable>> {
571        PARTITION_TABLES.with(|cell| {
572            let mut tables = cell.borrow_mut();
573            while tables.len() <= n_dirs {
574                let m = tables.len();
575                tables.push(std::rc::Rc::new(build_partitions(m)));
576            }
577            (0..=n_dirs)
578                .map(|m| std::rc::Rc::clone(&tables[m]))
579                .collect()
580        })
581    }
582
583    /// The previous production `compose_unary`: a direct set-partition (Faà di
584    /// Bruno) sum per output mask, retained as the accuracy reference.
585    fn compose_unary_partition_reference(coeffs: &[f64], derivs: [f64; DERIVS]) -> Vec<f64> {
586        let count = coeffs.len();
587        let n_dirs = count.trailing_zeros() as usize;
588        let tables = partition_tables(n_dirs);
589        let mut out = vec![0.0; count];
590        let mut remap = vec![0usize; count];
591        let mut pos = [0usize; usize::BITS as usize];
592        for (mask, slot) in out.iter_mut().enumerate() {
593            if mask == 0 {
594                *slot = derivs[0];
595                continue;
596            }
597            let mut npos = 0usize;
598            let mut m = mask;
599            while m != 0 {
600                pos[npos] = m.trailing_zeros() as usize;
601                npos += 1;
602                m &= m - 1;
603            }
604            remap[0] = 0;
605            for cb in 1usize..(1usize << npos) {
606                let low = cb.trailing_zeros() as usize;
607                remap[cb] = remap[cb & (cb - 1)] | (1usize << pos[low]);
608            }
609            let table = &tables[npos];
610            let flat = &table.flat;
611            let mut total = 0.0;
612            for &(off, order) in table.parts.iter() {
613                let order = order as usize;
614                let mut prod = derivs[order];
615                for &cb in &flat[off..off + order] {
616                    prod *= coeffs[remap[cb as usize]];
617                }
618                total += prod;
619            }
620            *slot = total;
621        }
622        out
623    }
624
625    /// Enumerate the set-partitions of `{0..m}` with fewer than `DERIVS` blocks, in
626    /// the exact DFS order of [`crate::jet_algebra`]'s `for_each_partition`
627    /// recursion ("place each element into an existing block, else open a new one"),
628    /// each block recorded as a compacted submask of `{0..m}`, flattened.
629    fn build_partitions(m: usize) -> PartTable {
630        fn recurse(
631            elem: usize,
632            m: usize,
633            blocks: &mut [u32; 8],
634            n_blocks: usize,
635            out: &mut PartTable,
636        ) {
637            // Partitions with `>= DERIVS` blocks are truncated (their `f^{(order)}`
638            // is beyond the stack); the block count never decreases, so the whole
639            // subtree contributes nothing and is pruned — matching the walker's
640            // per-partition `order >= derivs.len()` skip.
641            if n_blocks >= DERIVS {
642                return;
643            }
644            if elem == m {
645                let off = out.flat.len();
646                out.flat.extend_from_slice(&blocks[..n_blocks]);
647                out.parts.push((off, n_blocks as u8));
648                return;
649            }
650            for b in 0..n_blocks {
651                blocks[b] |= 1u32 << elem;
652                recurse(elem + 1, m, blocks, n_blocks, out);
653                blocks[b] &= !(1u32 << elem);
654            }
655            blocks[n_blocks] = 1u32 << elem;
656            recurse(elem + 1, m, blocks, n_blocks + 1, out);
657        }
658        let mut out = PartTable {
659            flat: Vec::new(),
660            parts: Vec::new(),
661        };
662        let mut blocks = [0u32; 8];
663        recurse(0, m, &mut blocks, 0, &mut out);
664        out
665    }
666
667    // ── constructors ─────────────────────────────────────────────────────────
668
669    #[test]
670    fn zero_has_correct_length_and_all_zero_coefficients() {
671        let j = MultiDirJet::zero(3);
672        assert_eq!(j.coeffs.len(), 8);
673        assert!(j.coeffs.iter().all(|&v| v == 0.0));
674    }
675
676    #[test]
677    fn constant_has_value_at_mask_zero_and_zeros_elsewhere() {
678        let j = MultiDirJet::constant(2, 5.0);
679        assert_eq!(j.coeffs.len(), 4);
680        assert_eq!(j.coeff(0), 5.0);
681        assert_eq!(j.coeff(1), 0.0);
682        assert_eq!(j.coeff(2), 0.0);
683        assert_eq!(j.coeff(3), 0.0);
684    }
685
686    #[test]
687    fn linear_sets_base_and_per_direction_slots() {
688        let j = MultiDirJet::linear(2, 1.0, &[2.0, 3.0]);
689        assert_eq!(j.coeff(0), 1.0); // constant
690        assert_eq!(j.coeff(1), 2.0); // mask 0b01 — direction 0
691        assert_eq!(j.coeff(2), 3.0); // mask 0b10 — direction 1
692        assert_eq!(j.coeff(3), 0.0); // cross term is zero
693    }
694
695    #[test]
696    fn bilinear_sets_all_four_slots() {
697        let j = MultiDirJet::bilinear(1.0, 2.0, 3.0, 4.0);
698        assert_eq!(j.coeff(0), 1.0);
699        assert_eq!(j.coeff(1), 2.0);
700        assert_eq!(j.coeff(2), 3.0);
701        assert_eq!(j.coeff(3), 4.0);
702    }
703
704    #[test]
705    fn with_coeffs_sets_only_specified_entries() {
706        let j = MultiDirJet::with_coeffs(2, &[(0, 9.0), (3, -1.0)]);
707        assert_eq!(j.coeff(0), 9.0);
708        assert_eq!(j.coeff(1), 0.0);
709        assert_eq!(j.coeff(2), 0.0);
710        assert_eq!(j.coeff(3), -1.0);
711    }
712
713    // ── elementwise arithmetic ────────────────────────────────────────────────
714
715    #[test]
716    fn add_is_elementwise() {
717        let a = MultiDirJet::linear(2, 1.0, &[2.0, 3.0]);
718        let b = MultiDirJet::linear(2, 4.0, &[5.0, 6.0]);
719        let c = a.add(&b);
720        assert_eq!(c.coeff(0), 5.0);
721        assert_eq!(c.coeff(1), 7.0);
722        assert_eq!(c.coeff(2), 9.0);
723        assert_eq!(c.coeff(3), 0.0);
724    }
725
726    #[test]
727    fn scale_multiplies_all_coefficients() {
728        let j = MultiDirJet::linear(2, 1.0, &[2.0, 3.0]);
729        let s = j.scale(2.0);
730        assert_eq!(s.coeff(0), 2.0);
731        assert_eq!(s.coeff(1), 4.0);
732        assert_eq!(s.coeff(2), 6.0);
733        assert_eq!(s.coeff(3), 0.0);
734    }
735
736    #[test]
737    fn sub_is_elementwise_difference() {
738        let a = MultiDirJet::constant(2, 5.0);
739        let b = MultiDirJet::constant(2, 3.0);
740        let c = a.sub(&b);
741        assert_eq!(c.coeff(0), 2.0);
742        assert_eq!(c.coeff(1), 0.0);
743        assert_eq!(c.coeff(2), 0.0);
744        assert_eq!(c.coeff(3), 0.0);
745    }
746
747    // ── mul (subset-convolution) ──────────────────────────────────────────────
748
749    #[test]
750    fn mul_of_constants_is_scalar_product() {
751        let a = MultiDirJet::constant(2, 2.0);
752        let b = MultiDirJet::constant(2, 3.0);
753        let c = a.mul(&b);
754        assert_eq!(c.coeff(0), 6.0);
755        assert_eq!(c.coeff(1), 0.0);
756        assert_eq!(c.coeff(2), 0.0);
757        assert_eq!(c.coeff(3), 0.0);
758    }
759
760    #[test]
761    fn mul_satisfies_leibniz_rule_single_direction() {
762        // (1 + ε) * (1 + ε) = 1 + 2ε
763        let x = MultiDirJet::linear(1, 1.0, &[1.0]);
764        let y = MultiDirJet::linear(1, 1.0, &[1.0]);
765        let z = x.mul(&y);
766        assert_eq!(z.coeff(0), 1.0);
767        assert_eq!(z.coeff(1), 2.0);
768    }
769
770    #[test]
771    fn mul_cross_term_two_independent_directions() {
772        // (1 + ε₁)(1 + ε₂) = 1 + ε₁ + ε₂ + ε₁ε₂
773        let x = MultiDirJet::linear(2, 1.0, &[1.0, 0.0]);
774        let y = MultiDirJet::linear(2, 1.0, &[0.0, 1.0]);
775        let z = x.mul(&y);
776        assert_eq!(z.coeff(0), 1.0);
777        assert_eq!(z.coeff(1), 1.0);
778        assert_eq!(z.coeff(2), 1.0);
779        assert_eq!(z.coeff(3), 1.0);
780    }
781
782    // ── compose_unary: truncated-Taylor reassociation ─────────────────────────
783    //
784    // The new `compose_unary` reassociates the per-mask Faà di Bruno set-partition
785    // sum into a degree-4 polynomial in the subset-convolution power of the
786    // non-constant part. These tests are the accuracy gate: a double-double
787    // oracle is the truth, and the new path's error-vs-truth must be `≤` the old
788    // naive partition sum's error-vs-truth on every randomised program.
789
790    /// Deterministic xorshift64* — no `rand` dependency in the test.
791    struct Rng(u64);
792    impl Rng {
793        fn next_u64(&mut self) -> u64 {
794            let mut x = self.0;
795            x ^= x >> 12;
796            x ^= x << 25;
797            x ^= x >> 27;
798            self.0 = x;
799            x.wrapping_mul(0x2545F4914F6CDD1D)
800        }
801        /// Uniform in `[-scale, scale]`.
802        fn signed(&mut self, scale: f64) -> f64 {
803            let u = (self.next_u64() >> 11) as f64 / (1u64 << 53) as f64; // [0,1)
804            (2.0 * u - 1.0) * scale
805        }
806    }
807
808    // ── A double-double oracle for the exact (order-4 truncated) composition ──
809
810    #[inline]
811    fn two_prod(a: f64, b: f64) -> (f64, f64) {
812        let p = a * b;
813        (p, a.mul_add(b, -p))
814    }
815    #[inline]
816    fn dd_two_sum(a: f64, b: f64) -> (f64, f64) {
817        let s = a + b;
818        let bb = s - a;
819        (s, (a - (s - bb)) + (b - bb))
820    }
821    #[derive(Clone, Copy)]
822    struct Dd {
823        hi: f64,
824        lo: f64,
825    }
826    impl Dd {
827        fn from(x: f64) -> Self {
828            Self { hi: x, lo: 0.0 }
829        }
830        fn mul_f64(self, b: f64) -> Self {
831            let (p, e) = two_prod(self.hi, b);
832            let lo = self.lo.mul_add(b, e);
833            let s = p + lo;
834            Self {
835                hi: s,
836                lo: (p - s) + lo,
837            }
838        }
839        fn add(self, o: Self) -> Self {
840            let (s, e) = dd_two_sum(self.hi, o.hi);
841            let (s2, e2) = dd_two_sum(self.lo, o.lo);
842            let lo = e + s2;
843            let h1 = s + lo;
844            let l1 = (s - h1) + lo;
845            let lo2 = l1 + e2;
846            let h = h1 + lo2;
847            Self {
848                hi: h,
849                lo: (h1 - h) + lo2,
850            }
851        }
852        /// `|self - x|` to ~double precision in the residual (Sterbenz: `x` and
853        /// `hi` agree to ~53 bits, so `x - hi` is essentially exact).
854        fn abs_err_to(self, x: f64) -> f64 {
855            ((x - self.hi) - self.lo).abs()
856        }
857    }
858
859    /// High-precision truth for `compose_unary` via the set-partition reference,
860    /// every product and sum carried in double-double.
861    fn compose_truth(coeffs: &[f64], derivs: [f64; DERIVS]) -> Vec<Dd> {
862        let count = coeffs.len();
863        let n_dirs = count.trailing_zeros() as usize;
864        let tables = partition_tables(n_dirs);
865        let mut out = vec![Dd::from(0.0); count];
866        let mut remap = vec![0usize; count];
867        let mut pos = [0usize; 64];
868        for (mask, slot) in out.iter_mut().enumerate() {
869            if mask == 0 {
870                *slot = Dd::from(derivs[0]);
871                continue;
872            }
873            let mut npos = 0usize;
874            let mut m = mask;
875            while m != 0 {
876                pos[npos] = m.trailing_zeros() as usize;
877                npos += 1;
878                m &= m - 1;
879            }
880            remap[0] = 0;
881            for cb in 1usize..(1usize << npos) {
882                let low = cb.trailing_zeros() as usize;
883                remap[cb] = remap[cb & (cb - 1)] | (1usize << pos[low]);
884            }
885            let table = &tables[npos];
886            let mut total = Dd::from(0.0);
887            for &(off, order) in table.parts.iter() {
888                let order = order as usize;
889                let mut prod = Dd::from(derivs[order]);
890                for &cb in &table.flat[off..off + order] {
891                    prod = prod.mul_f64(coeffs[remap[cb as usize]]);
892                }
893                total = total.add(prod);
894            }
895            *slot = total;
896        }
897        out
898    }
899
900    /// Build a random composite jet so the composition input is a realistic
901    /// non-trivial multilinear element (not just seeded directions).
902    fn random_inner(n_dirs: usize, rng: &mut Rng) -> MultiDirJet {
903        let base = rng.signed(0.8);
904        let first: Vec<f64> = (0..n_dirs).map(|_| rng.signed(0.6)).collect();
905        let a = MultiDirJet::linear(n_dirs, base, &first);
906        let b = MultiDirJet::linear(
907            n_dirs,
908            rng.signed(0.7),
909            &(0..n_dirs).map(|_| rng.signed(0.5)).collect::<Vec<_>>(),
910        );
911        // a*b + a populates the full cross-mask spectrum.
912        a.mul(&b).add(&a)
913    }
914
915    #[test]
916    fn compose_unary_matches_partition_reference_simple() {
917        // exp-like stack on a 2-direction cross jet: every coeff agrees with the
918        // direct set-partition reference to a tight tolerance.
919        let j = MultiDirJet::linear(2, 0.3, &[0.5, -0.4]).mul(&MultiDirJet::linear(
920            2,
921            -0.2,
922            &[0.1, 0.7],
923        ));
924        let d = [0.9_f64, 1.1, -0.7, 0.4, -0.25];
925        let got = j.compose_unary(d);
926        let want = compose_unary_partition_reference(&j.coeffs, d);
927        for (mask, (&g, &w)) in got.coeffs.iter().zip(want.iter()).enumerate() {
928            let tol = 1e-13 * w.abs().max(1.0);
929            assert!(
930                (g - w).abs() <= tol,
931                "mask {mask}: got={g:.17e} want={w:.17e}"
932            );
933        }
934    }
935
936    #[test]
937    fn compose_unary_accuracy_beats_partition_sum_vs_double_double() {
938        // The accuracy gate. Over many random programs at every K used in
939        // production, the new path's error-vs-truth is never worse than the old
940        // naive partition sum's, and is a strict improvement in aggregate.
941        let mut rng = Rng(0x1234_5678_9abc_def0);
942        let mut sum_new = 0.0f64;
943        let mut sum_old = 0.0f64;
944        for &n_dirs in &[2usize, 3, 4, 6, 8] {
945            for _ in 0..200 {
946                let inner = random_inner(n_dirs, &mut rng);
947                let d = [
948                    rng.signed(1.5),
949                    rng.signed(1.5),
950                    rng.signed(2.0),
951                    rng.signed(3.0),
952                    rng.signed(4.0),
953                ];
954                let new = inner.compose_unary(d);
955                let old = compose_unary_partition_reference(&inner.coeffs, d);
956                let truth = compose_truth(&inner.coeffs, d);
957                for mask in 0..inner.coeffs.len() {
958                    let en = truth[mask].abs_err_to(new.coeffs[mask]);
959                    let eo = truth[mask].abs_err_to(old[mask]);
960                    sum_new += en;
961                    sum_old += eo;
962                    // Per-coefficient: new is never materially worse. The 4 ULP
963                    // slack absorbs the rare tie where a differently-grouped but
964                    // equally-valid rounding lands one ULP either way.
965                    let scale = truth[mask].hi.abs().max(1.0);
966                    assert!(
967                        en <= eo + 4.0 * f64::EPSILON * scale,
968                        "K={n_dirs} mask={mask}: new_err={en:.3e} old_err={eo:.3e}"
969                    );
970                }
971            }
972        }
973        // Aggregate: the compensated reassociation is a real improvement.
974        assert!(
975            sum_new <= sum_old,
976            "aggregate error regressed: new={sum_new:.6e} old={sum_old:.6e}"
977        );
978        eprintln!(
979            "compose_unary accuracy: total |err| new={sum_new:.6e} old={sum_old:.6e} \
980             (improvement {:.2}x)",
981            sum_old / sum_new.max(f64::MIN_POSITIVE)
982        );
983    }
984
985    /// `v^{⊛k}[mask] = k! · Σ_{π ⊢ mask, |π| = k} Π_{B ∈ π} v[B]` by direct
986    /// enumeration of the set partitions of `mask` — the *definition* the pointed
987    /// recurrence claims to compute.
988    ///
989    /// Returns `[v², v³, v⁴]` and, per mask, the forward error the pair of
990    /// evaluations is jointly entitled to: this reference accumulates `n` terms
991    /// naively (Wilkinson: `n·u` times the sum of the term magnitudes) after up to
992    /// three roundings per product, and the compensated walk is good to ~`u`, so
993    /// `(n + 4)·EPSILON·Σ|term|` bounds their difference. It is derived from the
994    /// enumeration, not fitted to an observed failure.
995    fn brute_force_multilinear_powers(v: &[f64]) -> ([Vec<f64>; 3], [Vec<f64>; 3]) {
996        fn recurse(
997            elem: usize,
998            bits: &[usize],
999            blocks: &mut Vec<usize>,
1000            v: &[f64],
1001            acc: &mut [f64; 5],
1002            acc_abs: &mut [f64; 5],
1003            acc_count: &mut [u32; 5],
1004        ) {
1005            if blocks.len() > 4 {
1006                return;
1007            }
1008            if elem == bits.len() {
1009                let order = blocks.len();
1010                if order >= 2 {
1011                    let product: f64 = blocks.iter().map(|&b| v[b]).product();
1012                    acc[order] += product;
1013                    acc_abs[order] += product.abs();
1014                    acc_count[order] += 1;
1015                }
1016                return;
1017            }
1018            let bit = 1usize << bits[elem];
1019            for slot in 0..blocks.len() {
1020                blocks[slot] |= bit;
1021                recurse(elem + 1, bits, blocks, v, acc, acc_abs, acc_count);
1022                blocks[slot] &= !bit;
1023            }
1024            blocks.push(bit);
1025            recurse(elem + 1, bits, blocks, v, acc, acc_abs, acc_count);
1026            blocks.pop();
1027        }
1028        let count = v.len();
1029        let mut powers = [vec![0.0; count], vec![0.0; count], vec![0.0; count]];
1030        let mut entitled = [vec![0.0; count], vec![0.0; count], vec![0.0; count]];
1031        let factorial = [1.0, 1.0, 2.0, 6.0, 24.0];
1032        for mask in 1..count {
1033            let mut bits = Vec::new();
1034            let mut rest = mask;
1035            while rest != 0 {
1036                bits.push(rest.trailing_zeros() as usize);
1037                rest &= rest - 1;
1038            }
1039            let mut acc = [0.0f64; 5];
1040            let mut acc_abs = [0.0f64; 5];
1041            let mut acc_count = [0u32; 5];
1042            recurse(
1043                0,
1044                &bits,
1045                &mut Vec::new(),
1046                v,
1047                &mut acc,
1048                &mut acc_abs,
1049                &mut acc_count,
1050            );
1051            for order in 2..=4usize {
1052                powers[order - 2][mask] = factorial[order] * acc[order];
1053                entitled[order - 2][mask] = (f64::from(acc_count[order]) + 4.0)
1054                    * f64::EPSILON
1055                    * factorial[order]
1056                    * acc_abs[order];
1057            }
1058        }
1059        (powers, entitled)
1060    }
1061
1062    /// The pointed (lowest-set-bit) recurrence is an *identity*, not an
1063    /// approximation: pinning the block that owns the lowest set bit and
1064    /// multiplying by `k` reproduces the `k`-fold multilinear power of the
1065    /// brute-force set-partition definition, to the forward error the two
1066    /// evaluations are jointly entitled to.
1067    ///
1068    /// This is the gate on the recurrence the module header derives. The header's
1069    /// cost claims are only worth anything if the cheaper walk computes the same
1070    /// quantity, and that is what this pins.
1071    #[test]
1072    fn pointed_recurrence_reproduces_the_brute_force_multilinear_powers() {
1073        let mut rng = Rng(0x0be1_1ab0_1a5e_c001);
1074        for n_dirs in 1usize..=6 {
1075            for _ in 0..24 {
1076                let count = 1usize << n_dirs;
1077                let mut v: Vec<f64> = (0..count).map(|_| rng.signed(1.0)).collect();
1078                v[0] = 0.0;
1079                let mut p2 = vec![f64::NAN; count];
1080                let mut p3 = vec![f64::NAN; count];
1081                let mut p4 = vec![f64::NAN; count];
1082                multilinear_powers_into(&v, &mut p2, &mut p3, &mut p4);
1083                let (want, entitled) = brute_force_multilinear_powers(&v);
1084                for (order, got) in [&p2, &p3, &p4].iter().enumerate() {
1085                    for mask in 0..count {
1086                        // Graded against the forward error the two evaluations are
1087                        // jointly entitled to (see the reference above), which is a
1088                        // derived bound rather than a fitted tolerance. Where the
1089                        // power vanishes identically the bound is zero and the
1090                        // agreement must be exact.
1091                        let tolerance = entitled[order][mask];
1092                        assert!(
1093                            (got[mask] - want[order][mask]).abs() <= tolerance,
1094                            "K={n_dirs} k={} mask={mask}: pointed={:.17e} partitions={:.17e} \
1095                             tol={tolerance:.3e}",
1096                            order + 2,
1097                            got[mask],
1098                            want[order][mask]
1099                        );
1100                    }
1101                }
1102            }
1103        }
1104    }
1105
1106    /// The two schedules' operation counts, recomputed from the enumeration
1107    /// itself. This is the machine-independent statement of what each schedule
1108    /// costs and where the convolution path becomes the cheaper one; the
1109    /// wall-clock test below can only corroborate it.
1110    ///
1111    /// It exists because the header once claimed the convolution schedule was
1112    /// "~3× fewer FLOPs than the per-mask partition gather" full stop, and a
1113    /// wall-clock test asserted a speedup from `K = 6`. Both were false of the
1114    /// schedule then in the file: three full subset convolutions cost ~9× the
1115    /// gather at the `K = 4` the production entry point uses, and did not come
1116    /// out ahead until `K = 10`. A counted model cannot drift the way a prose
1117    /// factor did.
1118    #[test]
1119    fn compose_unary_work_model_matches_the_closed_form() {
1120        // Dot2: mul, FMA error, TwoSum (6), two carry adds.
1121        const DOT2_FLOPS: u64 = 10;
1122        // `scaled_compensated`: k·s, its FMA error, k·c, and two adds.
1123        const MULTIPLICITY_FLOPS: u64 = 5;
1124
1125        let binom = |n: u32, r: u32| -> u64 {
1126            (0..r).fold(1u64, |acc, i| acc * u64::from(n - i) / (u64::from(i) + 1))
1127        };
1128
1129        // Replay of `multilinear_powers_into`'s enumeration — same mask loop,
1130        // same lowest-bit pin, same descending walk over the submasks of
1131        // `mask ^ lowest`, same popcount gates — counting Dot2 steps instead of
1132        // performing them. This is what makes the closed form below a claim about
1133        // the kernel rather than about itself.
1134        fn walked_terms(n_dirs: u32) -> u64 {
1135            let count = 1usize << n_dirs;
1136            let mut steps = 0u64;
1137            for mask in 1..count {
1138                let lowest = mask & mask.wrapping_neg();
1139                let rest = mask ^ lowest;
1140                if rest == 0 {
1141                    continue;
1142                }
1143                let mut t = rest;
1144                while t != 0 {
1145                    steps += 1;
1146                    let popcount = (t as u64).count_ones();
1147                    if popcount >= 2 {
1148                        steps += 1;
1149                        if popcount >= 3 {
1150                            steps += 1;
1151                        }
1152                    }
1153                    t = (t - 1) & rest;
1154                }
1155            }
1156            steps
1157        }
1158
1159        // Closed form: per mask of popcount p ≥ 2 and each power k ≤ p, the
1160        // submasks t of `mask ^ ℓ` (a (p-1)-set) with popcount(t) ≥ k-1.
1161        let pointed_terms = |n_dirs: u32| -> u64 {
1162            (2..=n_dirs)
1163                .map(|p| {
1164                    let per_mask: u64 = (2..=4u32)
1165                        .filter(|k| *k <= p)
1166                        .map(|k| (k - 1..=p - 1).map(|j| binom(p - 1, j)).sum::<u64>())
1167                        .sum();
1168                    binom(n_dirs, p) * per_mask
1169                })
1170                .sum()
1171        };
1172        let pointed_flops = |n_dirs: u32| -> u64 {
1173            (2..=n_dirs)
1174                .map(|p| {
1175                    let per_mask: u64 = (2..=4u32)
1176                        .filter(|k| *k <= p)
1177                        .map(|k| (k - 1..=p - 1).map(|j| binom(p - 1, j)).sum::<u64>())
1178                        .sum();
1179                    binom(n_dirs, p) * (per_mask * DOT2_FLOPS + 3 * MULTIPLICITY_FLOPS)
1180                })
1181                .sum()
1182        };
1183
1184        // The schedule this replaced: three full subset convolutions v², v³=v²⊛v,
1185        // v⁴=v²⊛v², each pruned at popcount < k, each surviving mask walking all
1186        // 2^popcount of its submasks.
1187        let full_convolution_terms = |n_dirs: u32| -> u64 {
1188            (2..=4u32)
1189                .map(|k| {
1190                    (k..=n_dirs)
1191                        .map(|p| binom(n_dirs, p) * (1u64 << p))
1192                        .sum::<u64>()
1193                })
1194                .sum()
1195        };
1196
1197        // Partition gather: per mask of popcount p, every set partition of a
1198        // p-set into 1..=4 blocks contributes |π| multiplies and one add. This
1199        // omits the gather's own `2^p` per-mask index remap, so it is a lower
1200        // bound on the gather and every comparison below is against its best case.
1201        fn stirling(n: u32, blocks: u32) -> u64 {
1202            let (n, blocks) = (n as usize, blocks as usize);
1203            let mut s = vec![vec![0u64; blocks + 1]; n + 1];
1204            s[0][0] = 1;
1205            for i in 1..=n {
1206                for j in 1..=blocks {
1207                    s[i][j] = (j as u64) * s[i - 1][j] + s[i - 1][j - 1];
1208                }
1209            }
1210            s[n][blocks]
1211        }
1212        let gather_terms = |n_dirs: u32| -> u64 {
1213            1 + (1..=n_dirs)
1214                .map(|p| binom(n_dirs, p) * (1..=4u32).map(|b| stirling(p, b)).sum::<u64>())
1215                .sum::<u64>()
1216        };
1217        let gather_flops = |n_dirs: u32| -> u64 {
1218            1 + (1..=n_dirs)
1219                .map(|p| {
1220                    binom(n_dirs, p)
1221                        * (1..=4u32)
1222                            .map(|b| stirling(p, b) * (u64::from(b) + 1))
1223                            .sum::<u64>()
1224                })
1225                .sum::<u64>()
1226        };
1227
1228        // (a) The closed form describes the walk the kernel actually performs.
1229        for n_dirs in 2..=12u32 {
1230            assert_eq!(
1231                walked_terms(n_dirs),
1232                pointed_terms(n_dirs),
1233                "K={n_dirs}: closed form disagrees with the replayed enumeration"
1234            );
1235        }
1236
1237        // (b) Against the three full subset convolutions this replaced: exactly
1238        // 4× fewer terms across the whole range production runs at — one factor
1239        // of 2 from pinning the block that owns the lowest set bit, one from
1240        // walking submasks of `mask ^ ℓ` rather than of `mask`.
1241        for n_dirs in 2..=4u32 {
1242            assert_eq!(
1243                full_convolution_terms(n_dirs),
1244                4 * pointed_terms(n_dirs),
1245                "K={n_dirs}: the replaced schedule should be exactly 4x this one"
1246            );
1247        }
1248        for n_dirs in 2..=14u32 {
1249            assert!(
1250                pointed_terms(n_dirs) < full_convolution_terms(n_dirs),
1251                "K={n_dirs}: the pointed recurrence must never walk more terms \
1252                 than the full convolutions it replaced ({} vs {})",
1253                pointed_terms(n_dirs),
1254                full_convolution_terms(n_dirs)
1255            );
1256        }
1257
1258        // (c) Against the partition gather: strictly fewer terms at every K,
1259        // including the production K = 4 (34 against 52). The combinatorial
1260        // deficit that made the old schedule indefensible at small K is gone.
1261        for n_dirs in 2..=14u32 {
1262            assert!(
1263                pointed_terms(n_dirs) < gather_terms(n_dirs),
1264                "K={n_dirs}: pointed schedule should walk fewer terms than the \
1265                 partition gather ({} vs {})",
1266                pointed_terms(n_dirs),
1267                gather_terms(n_dirs)
1268            );
1269        }
1270        assert_eq!((pointed_terms(4), gather_terms(4)), (34, 52));
1271
1272        // (d) What remains at the production K = 4 is the compensation premium
1273        // and nothing else: 10 flops a term against the gather's ~3, over fewer
1274        // terms, for a 3.3x flop ratio. That is the price of the ~double-precision
1275        // accumulation the accuracy gate pins — it is bought, not free.
1276        let production_ratio = pointed_flops(4) as f64 / gather_flops(4) as f64;
1277        assert!(
1278            (production_ratio - 3.34).abs() < 0.05,
1279            "at the production K=4 the pointed schedule should cost ~3.34x the \
1280             partition gather's flops, got {production_ratio:.2}x"
1281        );
1282
1283        // (e) The flop crossover, which the pointed recurrence moves from K = 10
1284        // (three full convolutions) to K = 8.
1285        for n_dirs in 2..=7u32 {
1286            assert!(
1287                pointed_flops(n_dirs) > gather_flops(n_dirs),
1288                "K={n_dirs}: below the crossover the compensated schedule is still \
1289                 the more expensive one ({} vs {})",
1290                pointed_flops(n_dirs),
1291                gather_flops(n_dirs)
1292            );
1293        }
1294        for n_dirs in 8..=14u32 {
1295            assert!(
1296                pointed_flops(n_dirs) < gather_flops(n_dirs),
1297                "K={n_dirs}: at and above the K=8 crossover the compensated \
1298                 schedule should also be the cheaper one ({} vs {})",
1299                pointed_flops(n_dirs),
1300                gather_flops(n_dirs)
1301            );
1302        }
1303    }
1304
1305    #[test]
1306    fn compose_unary_speedup_over_partition_sum() {
1307        // Measure ns/call new vs. the previous partition-sum implementation.
1308        // Prints the multiple at every K; the assert is placed where the win
1309        // is large enough that no box can argue with it.
1310        //
1311        // This is corroboration, not the contract. The contract is
1312        // `compose_unary_work_model_matches_the_closed_form`, which counts
1313        // operations: a count is the same on every machine, and a wall clock is
1314        // not. The assert used to sit at `n_dirs >= 6` and was red at main,
1315        // because the schedule then in the file cost ~6x the gather's flops
1316        // there and could not win however the threshold was moved. Keep any
1317        // guard here far past the crossover the counted model reports (K = 8
1318        // for the pointed recurrence) and let the printed line carry the rest.
1319        use std::time::Instant;
1320        let mut rng = Rng(0xfeed_face_dead_beef);
1321        for &n_dirs in &[2usize, 4, 6, 8, 12] {
1322            // The per-call cost spans ~5 orders of magnitude across this K
1323            // range (both schedules are exponential in K), so the sample count
1324            // has to shrink with K or the K=12 arm alone would run for hours.
1325            let n_inputs = if n_dirs >= 12 { 4usize } else { 256 };
1326            let inputs: Vec<(MultiDirJet, [f64; DERIVS])> = (0..n_inputs)
1327                .map(|_| {
1328                    (
1329                        random_inner(n_dirs, &mut rng),
1330                        [
1331                            rng.signed(1.5),
1332                            rng.signed(1.5),
1333                            rng.signed(2.0),
1334                            rng.signed(3.0),
1335                            rng.signed(4.0),
1336                        ],
1337                    )
1338                })
1339                .collect();
1340            let iters = if n_dirs >= 12 { 3usize } else { 200 };
1341            // Warm the scratch / partition tables.
1342            for (j, d) in &inputs {
1343                std::hint::black_box(j.compose_unary(*d));
1344                std::hint::black_box(compose_unary_partition_reference(&j.coeffs, *d));
1345            }
1346            let t0 = Instant::now();
1347            for _ in 0..iters {
1348                for (j, d) in &inputs {
1349                    std::hint::black_box(j.compose_unary(*d));
1350                }
1351            }
1352            let new_ns = t0.elapsed().as_nanos() as f64 / (iters * inputs.len()) as f64;
1353            let t1 = Instant::now();
1354            for _ in 0..iters {
1355                for (j, d) in &inputs {
1356                    std::hint::black_box(compose_unary_partition_reference(&j.coeffs, *d));
1357                }
1358            }
1359            let old_ns = t1.elapsed().as_nanos() as f64 / (iters * inputs.len()) as f64;
1360            eprintln!(
1361                "compose_unary K={n_dirs}: new={new_ns:.1} ns/call  old={old_ns:.1} ns/call  \
1362                 speedup={:.2}x",
1363                old_ns / new_ns
1364            );
1365            // Guard only in an optimised build, and only where the counted
1366            // model says the compensated schedule does ~7x less arithmetic —
1367            // a margin no shared box's load can invert. Debug builds are
1368            // dominated by fixed per-call overhead and are not a guard either.
1369            if !cfg!(debug_assertions) && n_dirs >= 12 {
1370                assert!(
1371                    new_ns < old_ns,
1372                    "K={n_dirs} is far past the crossover the work model reports, \
1373                     so the compensated schedule must win here: new={new_ns:.1}ns \
1374                     old={old_ns:.1}ns"
1375                );
1376            }
1377        }
1378    }
1379}