1// Copyright 2024-2025 Irreducible Inc.
23use binius_field::BinaryField;
4use binius_math::Matrix;
5use binius_utils::{bail, checked_arithmetics::log2_ceil_usize};
67use crate::{additive_ntt::AdditiveNTT, error::Error, twiddle::TwiddleAccess};
89pub struct OddInterpolate<F: BinaryField> {
10 vandermonde_inverse: Matrix<F>,
11 ell: usize,
12}
1314impl<F: BinaryField> OddInterpolate<F> {
15/// Create a new odd interpolator into novel polynomial basis for domains of size $d \times 2^{\ell}$.
16 /// Takes a reference to NTT twiddle factors to seed the "Vandermonde" matrix and compute its inverse.
17 /// Time complexity is $\mathcal{O}(d^3).$
18pub fn new<TA>(d: usize, ell: usize, twiddle_access: &[TA]) -> Result<Self, Error>
19where
20TA: TwiddleAccess<F>,
21 {
22// TODO: This constructor should accept an `impl AdditiveNTT` instead of an
23 // `impl TwiddleAccess`. It can use `AdditiveNTT::get_subspace_eval` instead of the twiddle
24 // accessors directly. `AdditiveNTT` is a more public interface.
25let vandermonde = novel_vandermonde(d, ell, twiddle_access)?;
2627let mut vandermonde_inverse = Matrix::zeros(d, d);
28 vandermonde.inverse_into(&mut vandermonde_inverse)?;
2930Ok(Self {
31 vandermonde_inverse,
32 ell,
33 })
34 }
3536/// Let $L/\mathbb F_2$ be a binary field, and fix an $\mathbb F_2$-basis $1=:\beta_0,\ldots, \beta_{r-1}$ as usual.
37 /// Let $d\geq 1$ be an odd integer and let $\ell\geq 0$ be an integer. Let
38 /// $[a_0,\ldots, a_{d\times 2^{\ell} - 1}]$ be a list of elements of $L$. There is a unique univariate polynomial
39 /// $P(X)\in L\[X\]$ of degree less than $d\times 2^{\ell}$ such that the *evaluations* of $P$ on the "first" $d\times 2^{\ell}$
40 /// elements of $L$ (in little-Endian binary counting order with respect to the basis $\beta_0,\ldots, \beta_{r}$)
41 /// are precisely $a_0,\ldots, a_{d\times 2^{\ell} - 1}$.
42 ///
43 /// We efficiently compute the coefficients of $P(X)$ with respect to the Novel Polynomial Basis (itself taken
44 /// with respect to the given ordered list $\beta_0,\ldots, \beta_{r-1}$).
45 ///
46 /// Time complexity is $\mathcal{O}(d^2\times 2^{\ell} + \ell 2^{\ell})$, thus this routine is intended to be used
47 /// for small values of $d$.
48pub fn inverse_transform<NTT>(&self, ntt: &NTT, data: &mut [F]) -> Result<(), Error>
49where
50// REVIEW: generalize this to any P: PackedField<Scalar=F>
51NTT: AdditiveNTT<F>,
52 {
53let d = self.vandermonde_inverse.m();
54let ell = self.ell;
5556if data.len() != d << ell {
57bail!(Error::OddInterpolateIncorrectLength {
58 expected_len: d << ell
59 });
60 }
6162let log_required_domain_size = log2_ceil_usize(d) + ell;
63if ntt.log_domain_size() < log_required_domain_size {
64bail!(Error::DomainTooSmall {
65 log_required_domain_size
66 });
67 }
6869for (i, chunk) in data.chunks_exact_mut(1 << ell).enumerate() {
70 ntt.inverse_transform(chunk, i as u32, 0, ell)?;
71 }
7273// Given M and a vector v, do the "strided product" M v. In more detail: we assume matrix is $d\times d$,
74 // and vector is $d\times 2^{\ell}$. For each $i$ in $0,\ldots, 2^{\ell-1}$, let $v_i$ be the subvect
75 // given by those entries whose index is congruent to $i$ mod $2^{\ell}$. Then this computes $M v_i$,
76 // and finally "interleaves" the result (which means that we treat $M v_i = w_i$ for each $i$ and then conjure
77 // up the associated vector $w$.)
78let mut bases = vec![F::ZERO; d];
79let mut novel = vec![F::ZERO; d];
80// TODO: use `Matrix::mul_into`, implement when data is a slice of type `P: PackedField<Scalar=F>`.
81for stride in 0..1 << ell {
82 (0..d).for_each(|i| bases[i] = data[i << ell | stride]);
83self.vandermonde_inverse.mul_vec_into(&bases, &mut novel);
84 (0..d).for_each(|i| data[i << ell | stride] = novel[i]);
85 }
8687Ok(())
88 }
89}
9091/// Compute the Vandermonde matrix: $X^{(\ell)}_i(w^{\ell}_j)$, where $w^{\ell}_j$ is the $j^{\text{th}}$ element of the field
92/// with respect to the $\beta^{(\ell)}_i$ in little Endian order. The matrix has dimensions $d\times d$.
93/// The key trick is that $\widehat{W}^{(\ell)}_i(\beta^{\ell}_j) = $\widehat{W}_{i+\ell}(\beta_{j+\ell})$.
94fn novel_vandermonde<F, TA>(d: usize, ell: usize, twiddle_access: &[TA]) -> Result<Matrix<F>, Error>
95where
96F: BinaryField,
97 TA: TwiddleAccess<F>,
98{
99if d == 0 {
100return Ok(Matrix::zeros(0, 0));
101 }
102103let log_d = log2_ceil_usize(d);
104105// This will contain the evaluations of $X^{(\ell)}_{j}(w^{(\ell)}_i)$. As usual, indexing goes from 0..d-1.
106let mut x_ell = Matrix::zeros(d, d);
107108// $X_0$ is the function "1".
109(0..d).for_each(|j| x_ell[(j, 0)] = F::ONE);
110111let log_required_domain_size = log_d + ell;
112if twiddle_access.len() < log_required_domain_size {
113bail!(Error::DomainTooSmall {
114 log_required_domain_size
115 });
116 }
117118for (j, twiddle_access_j_plus_ell) in twiddle_access[ell..ell + log_d].iter().enumerate() {
119assert!(twiddle_access_j_plus_ell.log_n() >= log_d - 1 - j);
120121for i in 0..d {
122 x_ell[(i, 1 << j)] = twiddle_access_j_plus_ell.get(i >> (j + 1))
123 + if (i >> j) & 1 == 1 { F::ONE } else { F::ZERO };
124 }
125126// Note that the jth column of x_ell is the ordered list of values $X_j(w_i)$ for i = 0, ..., d-1.
127for k in 1..(1 << j).min(d - (1 << j)) {
128for t in 0..d {
129 x_ell[(t, k + (1 << j))] = x_ell[(t, k)] * x_ell[(t, 1 << j)];
130 }
131 }
132 }
133134Ok(x_ell)
135}
136137#[cfg(test)]
138mod tests {
139use std::iter::repeat_with;
140141use binius_field::{BinaryField32b, Field};
142use rand::{rngs::StdRng, SeedableRng};
143144use super::*;
145use crate::single_threaded::SingleThreadedNTT;
146147#[test]
148fn test_interpolate_odd() {
149type F = BinaryField32b;
150let max_ell = 8;
151let max_d = 10;
152153let mut rng = StdRng::seed_from_u64(0);
154let ntt = SingleThreadedNTT::<F>::new(max_ell + log2_ceil_usize(max_d)).unwrap();
155156for ell in 0..max_ell {
157for d in 0..max_d {
158let expected_novel = repeat_with(|| F::random(&mut rng))
159 .take(d << ell)
160 .collect::<Vec<_>>();
161162let mut ntt_evals = expected_novel.clone();
163// zero-pad to the next power of two to apply the forward transform.
164let next_log_n = log2_ceil_usize(expected_novel.len());
165 ntt_evals.resize(1 << next_log_n, F::ZERO);
166// apply forward transform and then run our odd interpolation routine.
167ntt.forward_transform(&mut ntt_evals, 0, 0, next_log_n)
168 .unwrap();
169170let odd_interpolate = OddInterpolate::new(d, ell, &ntt.s_evals).unwrap();
171 odd_interpolate
172 .inverse_transform(&ntt, &mut ntt_evals[..d << ell])
173 .unwrap();
174175assert_eq!(expected_novel, &ntt_evals[..d << ell]);
176 }
177 }
178 }
179}