Skip to main content

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