binius_ntt/
odd_interpolate.rs

1// Copyright 2024-2025 Irreducible Inc.
2
3use binius_field::BinaryField;
4use binius_math::Matrix;
5use binius_utils::{bail, checked_arithmetics::log2_ceil_usize};
6
7use crate::{additive_ntt::AdditiveNTT, error::Error, twiddle::TwiddleAccess};
8
9pub struct OddInterpolate<F: BinaryField> {
10	vandermonde_inverse: Matrix<F>,
11	ell: usize,
12}
13
14impl<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).$
18	pub fn new<TA>(d: usize, ell: usize, twiddle_access: &[TA]) -> Result<Self, Error>
19	where
20		TA: 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.
25		let vandermonde = novel_vandermonde(d, ell, twiddle_access)?;
26
27		let mut vandermonde_inverse = Matrix::zeros(d, d);
28		vandermonde.inverse_into(&mut vandermonde_inverse)?;
29
30		Ok(Self {
31			vandermonde_inverse,
32			ell,
33		})
34	}
35
36	/// 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$.
48	pub fn inverse_transform<NTT>(&self, ntt: &NTT, data: &mut [F]) -> Result<(), Error>
49	where
50		// REVIEW: generalize this to any P: PackedField<Scalar=F>
51		NTT: AdditiveNTT<F>,
52	{
53		let d = self.vandermonde_inverse.m();
54		let ell = self.ell;
55
56		if data.len() != d << ell {
57			bail!(Error::OddInterpolateIncorrectLength {
58				expected_len: d << ell
59			});
60		}
61
62		let log_required_domain_size = log2_ceil_usize(d) + ell;
63		if ntt.log_domain_size() < log_required_domain_size {
64			bail!(Error::DomainTooSmall {
65				log_required_domain_size
66			});
67		}
68
69		for (i, chunk) in data.chunks_exact_mut(1 << ell).enumerate() {
70			ntt.inverse_transform(chunk, i as u32, 0, ell)?;
71		}
72
73		// 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$.)
78		let mut bases = vec![F::ZERO; d];
79		let mut novel = vec![F::ZERO; d];
80		// TODO: use `Matrix::mul_into`, implement when data is a slice of type `P: PackedField<Scalar=F>`.
81		for stride in 0..1 << ell {
82			(0..d).for_each(|i| bases[i] = data[i << ell | stride]);
83			self.vandermonde_inverse.mul_vec_into(&bases, &mut novel);
84			(0..d).for_each(|i| data[i << ell | stride] = novel[i]);
85		}
86
87		Ok(())
88	}
89}
90
91/// 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
96	F: BinaryField,
97	TA: TwiddleAccess<F>,
98{
99	if d == 0 {
100		return Ok(Matrix::zeros(0, 0));
101	}
102
103	let log_d = log2_ceil_usize(d);
104
105	// This will contain the evaluations of $X^{(\ell)}_{j}(w^{(\ell)}_i)$. As usual, indexing goes from 0..d-1.
106	let mut x_ell = Matrix::zeros(d, d);
107
108	// $X_0$ is the function "1".
109	(0..d).for_each(|j| x_ell[(j, 0)] = F::ONE);
110
111	let log_required_domain_size = log_d + ell;
112	if twiddle_access.len() < log_required_domain_size {
113		bail!(Error::DomainTooSmall {
114			log_required_domain_size
115		});
116	}
117
118	for (j, twiddle_access_j_plus_ell) in twiddle_access[ell..ell + log_d].iter().enumerate() {
119		assert!(twiddle_access_j_plus_ell.log_n() >= log_d - 1 - j);
120
121		for 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		}
125
126		// Note that the jth column of x_ell is the ordered list of values $X_j(w_i)$ for i = 0, ..., d-1.
127		for k in 1..(1 << j).min(d - (1 << j)) {
128			for t in 0..d {
129				x_ell[(t, k + (1 << j))] = x_ell[(t, k)] * x_ell[(t, 1 << j)];
130			}
131		}
132	}
133
134	Ok(x_ell)
135}
136
137#[cfg(test)]
138mod tests {
139	use std::iter::repeat_with;
140
141	use binius_field::{BinaryField32b, Field};
142	use rand::{rngs::StdRng, SeedableRng};
143
144	use super::*;
145	use crate::single_threaded::SingleThreadedNTT;
146
147	#[test]
148	fn test_interpolate_odd() {
149		type F = BinaryField32b;
150		let max_ell = 8;
151		let max_d = 10;
152
153		let mut rng = StdRng::seed_from_u64(0);
154		let ntt = SingleThreadedNTT::<F>::new(max_ell + log2_ceil_usize(max_d)).unwrap();
155
156		for ell in 0..max_ell {
157			for d in 0..max_d {
158				let expected_novel = repeat_with(|| F::random(&mut rng))
159					.take(d << ell)
160					.collect::<Vec<_>>();
161
162				let mut ntt_evals = expected_novel.clone();
163				// zero-pad to the next power of two to apply the forward transform.
164				let 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.
167				ntt.forward_transform(&mut ntt_evals, 0, 0, next_log_n)
168					.unwrap();
169
170				let odd_interpolate = OddInterpolate::new(d, ell, &ntt.s_evals).unwrap();
171				odd_interpolate
172					.inverse_transform(&ntt, &mut ntt_evals[..d << ell])
173					.unwrap();
174
175				assert_eq!(expected_novel, &ntt_evals[..d << ell]);
176			}
177		}
178	}
179}