p3_commit/domain.rs
1use alloc::collections::BTreeMap;
2use alloc::vec::Vec;
3
4use itertools::Itertools;
5use p3_field::coset::TwoAdicMultiplicativeCoset;
6use p3_field::{ExtensionField, Field, TwoAdicField, batch_multiplicative_inverse};
7use p3_matrix::Matrix;
8use p3_matrix::dense::{RowMajorMatrix, RowMajorMatrixView};
9use p3_matrix::interpolation::Interpolate;
10use p3_util::{log2_ceil_usize, log2_strict_usize};
11
12/// Given a `PolynomialSpace`, `S`, and a subset `R`, a Lagrange selector `P_R` is
13/// a polynomial which is not equal to `0` for every element in `R` but is equal
14/// to `0` for every element of `S` not in `R`.
15///
16/// This struct contains evaluations of several Lagrange selectors for a fixed
17/// `PolynomialSpace` over some collection of points disjoint from that
18/// `PolynomialSpace`.
19///
20/// The Lagrange selector is normalized if it is equal to `1` for every element in `R`.
21/// The LagrangeSelectors given here are not normalized.
22#[derive(Debug)]
23pub struct LagrangeSelectors<T> {
24 /// A Lagrange selector corresponding to the first point in the space.
25 pub is_first_row: T,
26 /// A Lagrange selector corresponding to the last point in the space.
27 pub is_last_row: T,
28 /// A Lagrange selector corresponding the subset of all but the last point.
29 pub is_transition: T,
30 /// The inverse of the vanishing polynomial which is a Lagrange selector corresponding to the empty set
31 pub inv_vanishing: T,
32}
33
34/// Fixing a field, `F`, `PolynomialSpace<Val = F>` denotes an indexed subset of `F^n`
35/// with some additional algebraic structure.
36///
37/// We do not expect `PolynomialSpace` to store this subset, instead it usually contains
38/// some associated data which allows it to generate the subset or pieces of it.
39///
40/// Each `PolynomialSpace` should be part of a family of similar spaces for some
41/// collection of sizes (usually powers of two). Any space other than at the smallest size
42/// should be decomposable into a disjoint collection of smaller spaces. Additionally, the
43/// set of all `PolynomialSpace` of a given size should form a disjoint partition of some
44/// subset of `F^n` which supports a group structure.
45///
46/// The canonical example of a `PolynomialSpace` is a coset `gH` of
47/// a two-adic subgroup `H` of the multiplicative group `F*`. This satisfies the properties
48/// above as cosets partition the group and decompose as `gH = g(H^2) u gh(H^2)` for `h` any
49/// generator of `H`.
50///
51/// The other example in this code base is twin cosets which are sets of the form `gH u g^{-1}H`.
52/// The decomposition above extends easily to this case as `h` is a generator if and only if `h^{-1}`
53/// is and so `gH u g^{-1}H = (g(H^2) u g^{-1}(H^2)) u (gh(H^2) u (gh)^{-1}(H^2))`.
54pub trait PolynomialSpace: Copy {
55 /// The base field `F`.
56 type Val: Field;
57
58 /// The number of elements of the space.
59 fn size(&self) -> usize;
60
61 /// The first point in the space.
62 fn first_point(&self) -> Self::Val;
63
64 /// An algebraic function which takes the i'th element of the space and returns
65 /// the (i+1)'th evaluated on the given point.
66 ///
67 /// When `PolynomialSpace` corresponds to a coset, `gH` this
68 /// function is multiplication by `h` for a chosen generator `h` of `H`.
69 ///
70 /// This function may not exist for other classes of `PolynomialSpace` in which
71 /// case this will return `None`.
72 fn next_point<Ext: ExtensionField<Self::Val>>(&self, x: Ext) -> Option<Ext>;
73
74 /// Return another `PolynomialSpace` with size at least `min_size` disjoint from this space.
75 ///
76 /// When working with spaces of power of two size, this will return a space of size `2^ceil(log_2(min_size))`.
77 /// This will fail if `min_size` is too large. In particular, `log_2(min_size)` should be
78 /// smaller than the `2`-adicity of the field.
79 ///
80 /// This fixes a canonical choice for prover/verifier determinism and LDE caching.
81 ///
82 /// # Panics
83 ///
84 /// Panics if `min_size` is too large for a disjoint domain to be constructed. Verifier-side
85 /// code processing untrusted input should prefer [`Self::try_create_disjoint_domain`], which
86 /// reports this condition as `None` instead of panicking.
87 fn create_disjoint_domain(&self, min_size: usize) -> Self {
88 self.try_create_disjoint_domain(min_size)
89 .unwrap_or_else(|| {
90 panic!("cannot construct a domain of size at least {min_size} disjoint from `self`")
91 })
92 }
93
94 /// The non-panicking counterpart to [`Self::create_disjoint_domain`].
95 ///
96 /// Returns `None` instead of panicking when `min_size` is too large for a disjoint domain
97 /// to be constructed (for two-adic domains, this happens when `log_2(min_size)` is not
98 /// smaller than the field's `2`-adicity). Intended for verifier-side code, which must
99 /// reject malformed or adversarial input rather than panic on it.
100 fn try_create_disjoint_domain(&self, min_size: usize) -> Option<Self>;
101
102 /// Split the `PolynomialSpace` into `num_chunks` smaller `PolynomialSpaces` of equal size.
103 ///
104 /// `num_chunks` must divide `self.size()` (which usually forces it to be a power of 2.) or
105 /// this function will panic.
106 fn split_domains(&self, num_chunks: usize) -> Vec<Self>;
107
108 /// Split a set of polynomial evaluations over this `PolynomialSpace` into a vector
109 /// of polynomial evaluations over each `PolynomialSpace` generated from `split_domains`.
110 ///
111 /// `evals.height()` must equal `self.size()` and `num_chunks` must divide `self.size()`.
112 /// `evals` are assumed to be in standard (not bit-reversed) order.
113 fn split_evals(
114 &self,
115 num_chunks: usize,
116 evals: RowMajorMatrix<Self::Val>,
117 ) -> Vec<RowMajorMatrix<Self::Val>>;
118
119 /// Compute the vanishing polynomial of the space, evaluated at the given point.
120 ///
121 /// This is a polynomial which evaluates to `0` on every point of the
122 /// space `self` and has degree equal to `self.size()`. In other words it is
123 /// a choice of element of the defining ideal of the given set with this extra
124 /// degree property.
125 ///
126 /// In the univariate case, it is equal, up to a linear factor, to the product over
127 /// all elements `x`, of `(X - x)`. In particular this implies it will not evaluate
128 /// to `0` at any point not in `self`.
129 fn vanishing_poly_at_point<Ext: ExtensionField<Self::Val>>(&self, point: Ext) -> Ext;
130
131 /// Compute several Lagrange selectors at a given point.
132 /// - The Lagrange selector of the first point.
133 /// - The Lagrange selector of the last point.
134 /// - The Lagrange selector of everything but the last point.
135 /// - The inverse of the vanishing polynomial.
136 ///
137 /// Note that these may not be normalized.
138 fn selectors_at_point<Ext: ExtensionField<Self::Val>>(
139 &self,
140 point: Ext,
141 ) -> LagrangeSelectors<Ext>;
142
143 /// Compute several Lagrange selectors at all points of the given disjoint `PolynomialSpace`.
144 /// - The Lagrange selector of the first point.
145 /// - The Lagrange selector of the last point.
146 /// - The Lagrange selector of everything but the last point.
147 /// - The inverse of the vanishing polynomial.
148 ///
149 /// Note that these may not be normalized.
150 fn selectors_on_coset(&self, coset: Self) -> LagrangeSelectors<Vec<Self::Val>>;
151
152 /// Evaluate the polynomial defined by `evals` (evaluations over `self`) at `point`.
153 fn evaluate_polynomial_at<Ext: ExtensionField<Self::Val>>(
154 &self,
155 evals: &[Self::Val],
156 point: Ext,
157 ) -> Ext;
158
159 /// Evaluate a periodic column polynomial at `point`.
160 ///
161 /// `col` contains the period-length evaluations: row `i` of the full trace
162 /// gets value `col[i % col.len()]`. The default expands to trace size and
163 /// delegates to [`Self::evaluate_polynomial_at`]; domains with algebraic
164 /// structure (e.g. two-adic cosets) can override for O(period) work.
165 ///
166 /// # Performance
167 ///
168 /// This default is O(`self.size()`) time and allocates a `self.size()`-length
169 /// vector, versus O(`col.len()`) for an override that exploits the domain's
170 /// algebraic structure (e.g. two-adic cosets folding onto a sub-coset). For a
171 /// small period on a large trace this is a large (potentially many-orders-of-
172 /// magnitude) verifier slowdown. Any new `PolynomialSpace` implementor should
173 /// override this method rather than rely on the default.
174 fn evaluate_periodic_column_at<Ext: ExtensionField<Self::Val>>(
175 &self,
176 col: &[Self::Val],
177 point: Ext,
178 ) -> Ext {
179 let n = self.size();
180 let period = col.len();
181 let evals: Vec<Self::Val> = (0..n).map(|i| col[i % period]).collect();
182 self.evaluate_polynomial_at(&evals, point)
183 }
184
185 /// Evaluate several periodic column polynomials at `point`.
186 ///
187 /// The default expands to one call to [`Self::evaluate_periodic_column_at`] per
188 /// column. Domains with algebraic structure (e.g. two-adic cosets) can override
189 /// to batch columns that share a period, paying for one interpolation instead
190 /// of one per column.
191 fn evaluate_periodic_columns_at<Ext: ExtensionField<Self::Val>>(
192 &self,
193 periodic_columns: &[Vec<Self::Val>],
194 point: Ext,
195 ) -> Vec<Ext> {
196 periodic_columns
197 .iter()
198 .map(|col| self.evaluate_periodic_column_at(col, point))
199 .collect()
200 }
201}
202
203impl<Val: TwoAdicField> PolynomialSpace for TwoAdicMultiplicativeCoset<Val> {
204 type Val = Val;
205
206 fn size(&self) -> usize {
207 self.size()
208 }
209
210 fn first_point(&self) -> Self::Val {
211 self.shift()
212 }
213
214 /// Getting the next point corresponds to multiplication by the generator.
215 fn next_point<Ext: ExtensionField<Val>>(&self, x: Ext) -> Option<Ext> {
216 Some(x * self.subgroup_generator())
217 }
218
219 /// Given the coset `gH`, return the disjoint coset `gfK` where `f`
220 /// is a fixed generator of `F^*` and `K` is the unique two-adic subgroup
221 /// of with size `2^(ceil(log_2(min_size)))`.
222 ///
223 /// Returns `None` if `min_size` > `1 << Val::TWO_ADICITY`.
224 fn try_create_disjoint_domain(&self, min_size: usize) -> Option<Self> {
225 // We provide a short proof that these cosets are always disjoint:
226 //
227 // Assume without loss of generality that `|H| <= min_size <= |K|`.
228 // Then we know that `gH` is entirely contained in `gK`. As cosets are
229 // either equal or disjoint, this means that `gH` is disjoint from `g'K`
230 // for every `g'` not contained in `gK`. As `f` is a generator of `F^*`
231 // it does not lie in `K` and so `gf` cannot lie in `gK`.
232 //
233 // Thus `gH` and `gfK` are disjoint.
234
235 // This is `None` if (and only if) `min_size` > `1 << Val::TWO_ADICITY`.
236 Self::new(self.shift() * Val::GENERATOR, log2_ceil_usize(min_size))
237 }
238
239 /// Given the coset `gH` and generator `h` of `H`, let `K = H^{num_chunks}`
240 /// be the unique group of order `|H|/num_chunks`.
241 ///
242 /// Then we decompose `gH` into `gK, ghK, gh^2K, ..., gh^{num_chunks}K`.
243 fn split_domains(&self, num_chunks: usize) -> Vec<Self> {
244 let log_chunks = log2_strict_usize(num_chunks);
245 debug_assert!(log_chunks <= self.log_size());
246 (0..num_chunks)
247 .map(|i| {
248 Self::new(
249 self.shift() * self.subgroup_generator().exp_u64(i as u64),
250 self.log_size() - log_chunks,
251 )
252 .unwrap() // This won't panic as `self.log_size() - log_chunks < self.log_size() < Val::TWO_ADICITY`
253 })
254 .collect()
255 }
256
257 fn split_evals(
258 &self,
259 num_chunks: usize,
260 evals: RowMajorMatrix<Self::Val>,
261 ) -> Vec<RowMajorMatrix<Self::Val>> {
262 debug_assert_eq!(evals.height(), self.size());
263 debug_assert!(log2_strict_usize(num_chunks) <= self.log_size());
264 let height = evals.height();
265 let width = evals.width();
266 let rows_per_chunk = height / num_chunks;
267
268 // Preallocate zeroed buffers per chunk; often faster for field elements.
269 let mut values: Vec<Vec<Self::Val>> = (0..num_chunks)
270 .map(|_| Self::Val::zero_vec(rows_per_chunk * width))
271 .collect();
272
273 // Distribute rows without using modulo: iterate blocks of size num_chunks.
274 for i in 0..rows_per_chunk {
275 let base_row = i * num_chunks;
276 let dst_start = i * width;
277 let dst_end = dst_start + width;
278 for (chunk, dst_vec) in values.iter_mut().enumerate().take(num_chunks) {
279 let r = base_row + chunk;
280 // Safety: r < height == rows_per_chunk * num_chunks
281 let row = unsafe { evals.row_slice_unchecked(r) };
282 dst_vec[dst_start..dst_end].copy_from_slice(&row);
283 }
284 }
285
286 values
287 .into_iter()
288 .map(|v| RowMajorMatrix::new(v, width))
289 .collect()
290 }
291
292 /// Compute the vanishing polynomial at the given point:
293 ///
294 /// `Z_{gH}(X) = g^{-|H|}\prod_{h \in H} (X - gh) = (g^{-1}X)^|H| - 1`
295 fn vanishing_poly_at_point<Ext: ExtensionField<Val>>(&self, point: Ext) -> Ext {
296 (point * self.shift_inverse()).exp_power_of_2(self.log_size()) - Ext::ONE
297 }
298
299 /// Compute several Lagrange selectors at the given point:
300 ///
301 /// Defining the vanishing polynomial by `Z_{gH}(X) = g^{-|H|}\prod_{h \in H} (X - gh) = (g^{-1}X)^|H| - 1` return:
302 /// - `Z_{gH}(X)/(g^{-1}X - 1)`: The Lagrange selector of the point `g`.
303 /// - `Z_{gH}(X)/(g^{-1}X - h^{-1})`: The Lagrange selector of the point `gh^{-1}` where `h` is the generator of `H`.
304 /// - `(g^{-1}X - h^{-1})`: The Lagrange selector of the subset consisting of everything but the point `gh^{-1}`.
305 /// - `1/Z_{gH}(X)`: The inverse of the vanishing polynomial.
306 fn selectors_at_point<Ext: ExtensionField<Val>>(&self, point: Ext) -> LagrangeSelectors<Ext> {
307 let unshifted_point = point * self.shift_inverse();
308 let z_h = unshifted_point.exp_power_of_2(self.log_size()) - Ext::ONE;
309 LagrangeSelectors {
310 is_first_row: z_h / (unshifted_point - Ext::ONE),
311 is_last_row: z_h / (unshifted_point - self.subgroup_generator().inverse()),
312 is_transition: unshifted_point - self.subgroup_generator().inverse(),
313 inv_vanishing: z_h.inverse(),
314 }
315 }
316
317 /// Compute the Lagrange selectors of our space at every point in the coset.
318 ///
319 /// This will error if our space is not the group `H` and if the given
320 /// coset is not disjoint from `H`.
321 fn selectors_on_coset(&self, coset: Self) -> LagrangeSelectors<Vec<Val>> {
322 assert_eq!(self.shift(), Val::ONE);
323 assert_ne!(coset.shift(), Val::ONE);
324 assert!(coset.log_size() >= self.log_size());
325 let rate_bits = coset.log_size() - self.log_size();
326
327 let s_pow_n = coset.shift().exp_power_of_2(self.log_size());
328 // evals of Z_H(X) = X^n - 1
329 let evals = Val::two_adic_generator(rate_bits)
330 .powers()
331 .take(1 << rate_bits)
332 .map(|x| s_pow_n * x - Val::ONE)
333 .collect_vec();
334
335 let xs = coset.iter().collect();
336
337 let single_point_selector = |i: u64| {
338 let coset_i = self.subgroup_generator().exp_u64(i);
339 let denoms = xs.iter().map(|&x| x - coset_i).collect_vec();
340 let invs = batch_multiplicative_inverse(&denoms);
341 evals
342 .iter()
343 .cycle()
344 .zip(invs)
345 .map(|(&z_h, inv)| z_h * inv)
346 .collect_vec()
347 };
348
349 let subgroup_last = self.subgroup_generator().inverse();
350
351 LagrangeSelectors {
352 is_first_row: single_point_selector(0),
353 is_last_row: single_point_selector(self.size() as u64 - 1),
354 is_transition: xs.into_iter().map(|x| x - subgroup_last).collect(),
355 inv_vanishing: batch_multiplicative_inverse(&evals)
356 .into_iter()
357 .cycle()
358 .take(coset.size())
359 .collect(),
360 }
361 }
362
363 fn evaluate_polynomial_at<Ext: ExtensionField<Val>>(&self, evals: &[Val], point: Ext) -> Ext {
364 let evals_mat = RowMajorMatrixView::new(evals, 1);
365 evals_mat.interpolate_coset(self.shift(), point)[0]
366 }
367
368 fn evaluate_periodic_column_at<Ext: ExtensionField<Val>>(
369 &self,
370 col: &[Val],
371 point: Ext,
372 ) -> Ext {
373 let log_period = log2_strict_usize(col.len());
374 let folds = self.log_size() - log_period;
375 let sub_coset = Self::new(self.shift().exp_power_of_2(folds), log_period).unwrap();
376 sub_coset.evaluate_polynomial_at(col, point.exp_power_of_2(folds))
377 }
378
379 /// Evaluate several periodic column polynomials at `point`, sharing one coset
380 /// materialization and batch inversion (via [`Interpolate::interpolate_coset`])
381 /// across all columns of a given period.
382 fn evaluate_periodic_columns_at<Ext: ExtensionField<Val>>(
383 &self,
384 periodic_columns: &[Vec<Val>],
385 point: Ext,
386 ) -> Vec<Ext> {
387 let mut cols_by_period: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
388 for (i, col) in periodic_columns.iter().enumerate() {
389 cols_by_period.entry(col.len()).or_default().push(i);
390 }
391
392 let mut result = Ext::zero_vec(periodic_columns.len());
393 for (period, indices) in cols_by_period {
394 let log_period = log2_strict_usize(period);
395 let folds = self.log_size() - log_period;
396 let sub_shift = self.shift().exp_power_of_2(folds);
397 let sub_point = point.exp_power_of_2(folds);
398
399 // Interleave the columns sharing this period into one row-major matrix
400 // so `interpolate_coset` can evaluate all of them with a single batch
401 // inversion.
402 let k = indices.len();
403 let mut values = Val::zero_vec(period * k);
404 for (col_pos, &orig_idx) in indices.iter().enumerate() {
405 for (row, &v) in periodic_columns[orig_idx].iter().enumerate() {
406 values[row * k + col_pos] = v;
407 }
408 }
409
410 let evals = RowMajorMatrix::new(values, k).interpolate_coset(sub_shift, sub_point);
411 for (col_pos, &orig_idx) in indices.iter().enumerate() {
412 result[orig_idx] = evals[col_pos];
413 }
414 }
415
416 result
417 }
418}
419
420#[cfg(test)]
421mod tests {
422 use alloc::vec;
423 use alloc::vec::Vec;
424
425 use p3_baby_bear::BabyBear;
426 use p3_field::PrimeCharacteristicRing;
427
428 use super::*;
429
430 type F = BabyBear;
431
432 #[test]
433 fn evaluate_periodic_columns_at_matches_per_column_eval() {
434 let domain = TwoAdicMultiplicativeCoset::<F>::new(F::GENERATOR, 4).unwrap();
435 let point = F::from_u32(12345);
436
437 // Two columns of period 4 (sharing a period class with >1 member) plus one
438 // of period 2, to exercise both the grouping and the interleaving.
439 let columns: Vec<Vec<F>> = vec![
440 (0..4).map(F::from_u32).collect(),
441 (0..2).map(|x| F::from_u32(x + 10)).collect(),
442 (0..4).map(|x| F::from_u32(x + 100)).collect(),
443 ];
444
445 let expected: Vec<F> = columns
446 .iter()
447 .map(|col| domain.evaluate_periodic_column_at(col, point))
448 .collect();
449 let actual = domain.evaluate_periodic_columns_at(&columns, point);
450
451 assert_eq!(actual, expected);
452 }
453
454 #[test]
455 fn evaluate_periodic_columns_at_empty() {
456 let domain = TwoAdicMultiplicativeCoset::<F>::new(F::GENERATOR, 4).unwrap();
457 let point = F::from_u32(7);
458 let columns: Vec<Vec<F>> = vec![];
459
460 assert_eq!(
461 domain.evaluate_periodic_columns_at(&columns, point),
462 Vec::<F>::new()
463 );
464 }
465}