Skip to main content

lib_q_stark_commit/
domain.rs

1use alloc::vec::Vec;
2
3use itertools::Itertools;
4use lib_q_stark_field::coset::TwoAdicMultiplicativeCoset;
5use lib_q_stark_field::{
6    ExtensionField,
7    Field,
8    TwoAdicField,
9    batch_multiplicative_inverse,
10};
11use lib_q_stark_matrix::Matrix;
12use lib_q_stark_matrix::dense::RowMajorMatrix;
13use lib_q_stark_util::{
14    log2_ceil_usize,
15    log2_strict_usize,
16};
17
18/// Given a `PolynomialSpace`, `S`, and a subset `R`, a Lagrange selector `P_R` is
19/// a polynomial which is not equal to `0` for every element in `R` but is equal
20/// to `0` for every element of `S` not in `R`.
21///
22/// This struct contains evaluations of several Lagrange selectors for a fixed
23/// `PolynomialSpace` over some collection of points disjoint from that
24/// `PolynomialSpace`.
25///
26/// The Lagrange selector is normalized if it is equal to `1` for every element in `R`.
27/// The LagrangeSelectors given here are not normalized.
28#[derive(Debug)]
29pub struct LagrangeSelectors<T> {
30    /// A Lagrange selector corresponding to the first point in the space.
31    pub is_first_row: T,
32    /// A Lagrange selector corresponding to the last point in the space.
33    pub is_last_row: T,
34    /// A Lagrange selector corresponding the subset of all but the last point.
35    pub is_transition: T,
36    /// The inverse of the vanishing polynomial which is a Lagrange selector corresponding to the empty set
37    pub inv_vanishing: T,
38}
39
40/// Fixing a field, `F`, `PolynomialSpace<Val = F>` denotes an indexed subset of `F^n`
41/// with some additional algebraic structure.
42///
43/// We do not expect `PolynomialSpace` to store this subset, instead it usually contains
44/// some associated data which allows it to generate the subset or pieces of it.
45///
46/// Each `PolynomialSpace` should be part of a family of similar spaces for some
47/// collection of sizes (usually powers of two). Any space other than at the smallest size
48/// should be decomposable into a disjoint collection of smaller spaces. Additionally, the
49/// set of all `PolynomialSpace` of a given size should form a disjoint partition of some
50/// subset of `F^n` which supports a group structure.
51///
52/// The canonical example of a `PolynomialSpace` is a coset `gH` of
53/// a two-adic subgroup `H` of the multiplicative group `F*`. This satisfies the properties
54/// above as cosets partition the group and decompose as `gH = g(H^2) u gh(H^2)` for `h` any
55/// generator of `H`.
56///
57/// The other example in this code base is twin cosets which are sets of the form `gH u g^{-1}H`.
58/// The decomposition above extends easily to this case as `h` is a generator if and only if `h^{-1}`
59/// 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))`.
60pub trait PolynomialSpace: Copy {
61    /// The base field `F`.
62    type Val: Field;
63
64    /// The number of elements of the space.
65    fn size(&self) -> usize;
66
67    /// The first point in the space.
68    fn first_point(&self) -> Self::Val;
69
70    /// An algebraic function which takes the i'th element of the space and returns
71    /// the (i+1)'th evaluated on the given point.
72    ///
73    /// When `PolynomialSpace` corresponds to a coset, `gH` this
74    /// function is multiplication by `h` for a chosen generator `h` of `H`.
75    ///
76    /// This function may not exist for other classes of `PolynomialSpace` in which
77    /// case this will return `None`.
78    fn next_point<Ext: ExtensionField<Self::Val>>(&self, x: Ext) -> Option<Ext>;
79
80    /// Return another `PolynomialSpace` with size at least `min_size` disjoint from this space.
81    ///
82    /// When working with spaces of power of two size, this will return a space of size `2^ceil(log_2(min_size))`.
83    /// This will fail if `min_size` is too large. In particular, `log_2(min_size)` should be
84    /// smaller than the `2`-adicity of the field.
85    ///
86    /// This fixes a canonical choice for prover/verifier determinism and LDE caching.
87    ///
88    /// # Panics
89    /// Implementations backed by a two-adic domain (the only kind in this workspace today) will
90    /// panic if `min_size` exceeds `1 << Val::TWO_ADICITY`. Callers that process
91    /// untrusted/adversarial `min_size` values (e.g. a proof verifier deriving it from a claimed
92    /// degree) MUST validate it themselves before calling this, or use
93    /// [`try_create_disjoint_domain`](Self::try_create_disjoint_domain) instead.
94    fn create_disjoint_domain(&self, min_size: usize) -> Self;
95
96    /// Fallible sibling of [`create_disjoint_domain`](Self::create_disjoint_domain), for callers
97    /// (such as proof verifiers) that must reject an out-of-range `min_size` instead of panicking.
98    /// Returns `None` exactly under the conditions documented on `create_disjoint_domain`'s
99    /// `# Panics` section.
100    ///
101    /// The default implementation simply delegates to the infallible method, so it is only
102    /// non-panicking for implementations that override it; `TwoAdicMultiplicativeCoset` (the only
103    /// implementation in this workspace) overrides it with a genuinely non-panicking check.
104    fn try_create_disjoint_domain(&self, min_size: usize) -> Option<Self> {
105        Some(self.create_disjoint_domain(min_size))
106    }
107
108    /// Split the `PolynomialSpace` into `num_chunks` smaller `PolynomialSpaces` of equal size.
109    ///
110    /// `num_chunks` must divide `self.size()` (which usually forces it to be a power of 2.) or
111    /// this function will panic.
112    fn split_domains(&self, num_chunks: usize) -> Vec<Self>;
113
114    /// Split a set of polynomial evaluations over this `PolynomialSpace` into a vector
115    /// of polynomial evaluations over each `PolynomialSpace` generated from `split_domains`.
116    ///
117    /// `evals.height()` must equal `self.size()` and `num_chunks` must divide `self.size()`.
118    /// `evals` are assumed to be in standard (not bit-reversed) order.
119    fn split_evals(
120        &self,
121        num_chunks: usize,
122        evals: RowMajorMatrix<Self::Val>,
123    ) -> Vec<RowMajorMatrix<Self::Val>>;
124
125    /// Compute the vanishing polynomial of the space, evaluated at the given point.
126    ///
127    /// This is a polynomial which evaluates to `0` on every point of the
128    /// space `self` and has degree equal to `self.size()`. In other words it is
129    /// a choice of element of the defining ideal of the given set with this extra
130    /// degree property.
131    ///
132    /// In the univariate case, it is equal, up to a linear factor, to the product over
133    /// all elements `x`, of `(X - x)`. In particular this implies it will not evaluate
134    /// to `0` at any point not in `self`.
135    fn vanishing_poly_at_point<Ext: ExtensionField<Self::Val>>(&self, point: Ext) -> Ext;
136
137    /// Compute several Lagrange selectors at a given point.
138    /// - The Lagrange selector of the first point.
139    /// - The Lagrange selector of the last point.
140    /// - The Lagrange selector of everything but the last point.
141    /// - The inverse of the vanishing polynomial.
142    ///
143    /// Note that these may not be normalized.
144    fn selectors_at_point<Ext: ExtensionField<Self::Val>>(
145        &self,
146        point: Ext,
147    ) -> LagrangeSelectors<Ext>;
148
149    /// Compute several Lagrange selectors at all points of the given disjoint `PolynomialSpace`.
150    /// - The Lagrange selector of the first point.
151    /// - The Lagrange selector of the last point.
152    /// - The Lagrange selector of everything but the last point.
153    /// - The inverse of the vanishing polynomial.
154    ///
155    /// Note that these may not be normalized.
156    fn selectors_on_coset(&self, coset: Self) -> LagrangeSelectors<Vec<Self::Val>>;
157}
158
159impl<Val: TwoAdicField> PolynomialSpace for TwoAdicMultiplicativeCoset<Val> {
160    type Val = Val;
161
162    fn size(&self) -> usize {
163        self.size()
164    }
165
166    fn first_point(&self) -> Self::Val {
167        self.shift()
168    }
169
170    /// Getting the next point corresponds to multiplication by the generator.
171    fn next_point<Ext: ExtensionField<Val>>(&self, x: Ext) -> Option<Ext> {
172        Some(x * self.subgroup_generator())
173    }
174
175    /// Given the coset `gH`, return the disjoint coset `gfK` where `f`
176    /// is a fixed generator of `F^*` and `K` is the unique two-adic subgroup
177    /// of with size `2^(ceil(log_2(min_size)))`.
178    ///
179    /// # Panics
180    ///
181    /// This will panic if `min_size` > `1 << Val::TWO_ADICITY`.
182    fn create_disjoint_domain(&self, min_size: usize) -> Self {
183        // We provide a short proof that these cosets are always disjoint:
184        //
185        // Assume without loss of generality that `|H| <= min_size <= |K|`.
186        // Then we know that `gH` is entirely contained in `gK`. As cosets are
187        // either equal or disjoint, this means that `gH` is disjoint from `g'K`
188        // for every `g'` not contained in `gK`. As `f` is a generator of `F^*`
189        // it does not lie in `K` and so `gf` cannot lie in `gK`.
190        //
191        // Thus `gH` and `gfK` are disjoint.
192        self.try_create_disjoint_domain(min_size)
193            .unwrap_or_else(|| {
194                panic!(
195                    "create_disjoint_domain: min_size {min_size} exceeds 1 << Val::TWO_ADICITY \
196                 ({}); use try_create_disjoint_domain to handle this without panicking",
197                    Val::TWO_ADICITY
198                )
199            })
200    }
201
202    /// See [`PolynomialSpace::try_create_disjoint_domain`]. Never panics; returns `None` exactly
203    /// when `log2_ceil_usize(min_size) > Val::TWO_ADICITY` (the same condition under which
204    /// `create_disjoint_domain` would panic).
205    fn try_create_disjoint_domain(&self, min_size: usize) -> Option<Self> {
206        Self::new(self.shift() * Val::GENERATOR, log2_ceil_usize(min_size))
207    }
208
209    /// Given the coset `gH` and generator `h` of `H`, let `K = H^{num_chunks}`
210    /// be the unique group of order `|H|/num_chunks`.
211    ///
212    /// Then we decompose `gH` into `gK, ghK, gh^2K, ..., gh^{num_chunks}K`.
213    fn split_domains(&self, num_chunks: usize) -> Vec<Self> {
214        let log_chunks = log2_strict_usize(num_chunks);
215        debug_assert!(log_chunks <= self.log_size());
216        (0..num_chunks)
217            .map(|i| {
218                Self::new(
219                    self.shift() * self.subgroup_generator().exp_u64(i as u64),
220                    self.log_size() - log_chunks,
221                )
222                .unwrap() // This won't panic as `self.log_size() - log_chunks < self.log_size() < Val::TWO_ADICITY`
223            })
224            .collect()
225    }
226
227    fn split_evals(
228        &self,
229        num_chunks: usize,
230        evals: RowMajorMatrix<Self::Val>,
231    ) -> Vec<RowMajorMatrix<Self::Val>> {
232        debug_assert_eq!(evals.height(), self.size());
233        debug_assert!(log2_strict_usize(num_chunks) <= self.log_size());
234        let height = evals.height();
235        let width = evals.width();
236        let rows_per_chunk = height / num_chunks;
237
238        // Preallocate zeroed buffers per chunk; often faster for field elements.
239        let mut values: Vec<Vec<Self::Val>> = (0..num_chunks)
240            .map(|_| Self::Val::zero_vec(rows_per_chunk * width))
241            .collect();
242
243        // Distribute rows without using modulo: iterate blocks of size num_chunks.
244        for i in 0..rows_per_chunk {
245            let base_row = i * num_chunks;
246            let dst_start = i * width;
247            let dst_end = dst_start + width;
248            for (chunk, dst_vec) in values.iter_mut().enumerate().take(num_chunks) {
249                let r = base_row + chunk;
250                // Safety: r < height == rows_per_chunk * num_chunks
251                let row = unsafe { evals.row_slice_unchecked(r) };
252                dst_vec[dst_start..dst_end].copy_from_slice(&row);
253            }
254        }
255
256        values
257            .into_iter()
258            .map(|v| RowMajorMatrix::new(v, width))
259            .collect()
260    }
261
262    /// Compute the vanishing polynomial at the given point:
263    ///
264    /// `Z_{gH}(X) = g^{-|H|}\prod_{h \in H} (X - gh) = (g^{-1}X)^|H| - 1`
265    fn vanishing_poly_at_point<Ext: ExtensionField<Val>>(&self, point: Ext) -> Ext {
266        (point * self.shift_inverse()).exp_power_of_2(self.log_size()) - Ext::ONE
267    }
268
269    /// Compute several Lagrange selectors at the given point:
270    ///
271    /// Defining the vanishing polynomial by `Z_{gH}(X) = g^{-|H|}\prod_{h \in H} (X - gh) = (g^{-1}X)^|H| - 1` return:
272    /// - `Z_{gH}(X)/(g^{-1}X - 1)`: The Lagrange selector of the point `g`.
273    /// - `Z_{gH}(X)/(g^{-1}X - h^{-1})`: The Lagrange selector of the point `gh^{-1}` where `h` is the generator of `H`.
274    /// - `(g^{-1}X - h^{-1})`: The Lagrange selector of the subset consisting of everything but the point `gh^{-1}`.
275    /// - `1/Z_{gH}(X)`: The inverse of the vanishing polynomial.
276    fn selectors_at_point<Ext: ExtensionField<Val>>(&self, point: Ext) -> LagrangeSelectors<Ext> {
277        let unshifted_point = point * self.shift_inverse();
278        let z_h = unshifted_point.exp_power_of_2(self.log_size()) - Ext::ONE;
279        LagrangeSelectors {
280            is_first_row: z_h / (unshifted_point - Ext::ONE),
281            is_last_row: z_h / (unshifted_point - self.subgroup_generator().inverse()),
282            is_transition: unshifted_point - self.subgroup_generator().inverse(),
283            inv_vanishing: z_h.inverse(),
284        }
285    }
286
287    /// Compute the Lagrange selectors of our space at every point in the coset.
288    ///
289    /// This will error if our space is not the group `H` and if the given
290    /// coset is not disjoint from `H`.
291    fn selectors_on_coset(&self, coset: Self) -> LagrangeSelectors<Vec<Val>> {
292        assert_eq!(self.shift(), Val::ONE);
293        assert_ne!(coset.shift(), Val::ONE);
294        assert!(coset.log_size() >= self.log_size());
295        let rate_bits = coset.log_size() - self.log_size();
296
297        let s_pow_n = coset.shift().exp_power_of_2(self.log_size());
298        // evals of Z_H(X) = X^n - 1
299        let evals = Val::two_adic_generator(rate_bits)
300            .powers()
301            .take(1 << rate_bits)
302            .map(|x| s_pow_n * x - Val::ONE)
303            .collect_vec();
304
305        let xs = coset.iter().collect();
306
307        let single_point_selector = |i: u64| {
308            let coset_i = self.subgroup_generator().exp_u64(i);
309            let denoms = xs.iter().map(|&x| x - coset_i).collect_vec();
310            let invs = batch_multiplicative_inverse(&denoms);
311            evals
312                .iter()
313                .cycle()
314                .zip(invs)
315                .map(|(&z_h, inv)| z_h * inv)
316                .collect_vec()
317        };
318
319        let subgroup_last = self.subgroup_generator().inverse();
320
321        LagrangeSelectors {
322            is_first_row: single_point_selector(0),
323            is_last_row: single_point_selector(self.size() as u64 - 1),
324            is_transition: xs.into_iter().map(|x| x - subgroup_last).collect(),
325            inv_vanishing: batch_multiplicative_inverse(&evals)
326                .into_iter()
327                .cycle()
328                .take(coset.size())
329                .collect(),
330        }
331    }
332}
333
334#[cfg(test)]
335mod tests {
336    use lib_q_stark_baby_bear::BabyBear;
337    use lib_q_stark_field::{
338        PrimeCharacteristicRing,
339        PrimeField32,
340    };
341
342    use super::*;
343
344    type F = BabyBear;
345
346    fn coset(shift: F, log_size: usize) -> TwoAdicMultiplicativeCoset<F> {
347        TwoAdicMultiplicativeCoset::new(shift, log_size).unwrap()
348    }
349
350    fn sorted_u32(points: impl IntoIterator<Item = F>) -> Vec<u32> {
351        let mut v: Vec<u32> = points.into_iter().map(|p| p.as_canonical_u32()).collect();
352        v.sort_unstable();
353        v
354    }
355
356    #[test]
357    fn size_and_first_point_match_constructor_arguments() {
358        let c = coset(F::new(7), 3);
359        assert_eq!(PolynomialSpace::size(&c), 8);
360        assert_eq!(PolynomialSpace::first_point(&c), F::new(7));
361    }
362
363    #[test]
364    fn next_point_is_multiplication_by_the_subgroup_generator() {
365        let c = coset(F::new(5), 4);
366        let g = c.subgroup_generator();
367        let x = F::new(123);
368        assert_eq!(PolynomialSpace::next_point::<F>(&c, x), Some(x * g));
369    }
370
371    /// The subgroup generator has order exactly `size()`, so walking `next_point` around the
372    /// coset `size()` times must return to the start. This is the group-law identity the whole
373    /// `PolynomialSpace` abstraction rests on (`h^{|H|} = 1`).
374    #[test]
375    fn next_point_returns_to_start_after_exactly_size_steps() {
376        let c = coset(F::new(11), 5);
377        let mut x = PolynomialSpace::first_point(&c);
378        for _ in 0..PolynomialSpace::size(&c) {
379            x = PolynomialSpace::next_point::<F>(&c, x).unwrap();
380        }
381        assert_eq!(x, PolynomialSpace::first_point(&c));
382    }
383
384    /// `vanishing_poly_at_point` must be the zero polynomial exactly on the coset's own points.
385    /// This is the soundness-relevant property of the whole module: every FRI/DEEP quotient in
386    /// the STARK built on top of this crate divides by this polynomial, so if it is nonzero on an
387    /// in-domain point (or zero off-domain), proofs either fail to verify honestly or a cheating
388    /// prover gets a spurious zero to hide behind.
389    #[test]
390    fn vanishing_poly_is_zero_exactly_on_the_coset() {
391        let c = coset(F::new(9), 4);
392        for point in c.iter() {
393            assert_eq!(
394                PolynomialSpace::vanishing_poly_at_point::<F>(&c, point),
395                F::ZERO
396            );
397        }
398
399        // `create_disjoint_domain` is contractually guaranteed to be disjoint from `c` (see the
400        // proof in its doc comment, exercised separately below), so every one of its points is
401        // guaranteed to lie outside `c` and must NOT be a root of `c`'s vanishing polynomial.
402        let disjoint = PolynomialSpace::create_disjoint_domain(&c, PolynomialSpace::size(&c));
403        for point in disjoint.iter() {
404            assert_ne!(
405                PolynomialSpace::vanishing_poly_at_point::<F>(&c, point),
406                F::ZERO
407            );
408        }
409    }
410
411    /// Negative control for the test above: prove it can actually distinguish "zero" from
412    /// "nonzero" rather than passing vacuously (e.g. because of a mixed-up domain). Deliberately
413    /// assert the wrong polarity and confirm the test harness reports it red.
414    #[test]
415    fn vanishing_poly_negative_control_distinguishes_in_from_out_of_domain() {
416        let c = coset(F::new(9), 4);
417        let on_domain_is_zero =
418            PolynomialSpace::vanishing_poly_at_point::<F>(&c, PolynomialSpace::first_point(&c)) ==
419                F::ZERO;
420        let disjoint = PolynomialSpace::create_disjoint_domain(&c, PolynomialSpace::size(&c));
421        let off_domain_is_zero = PolynomialSpace::vanishing_poly_at_point::<F>(
422            &c,
423            PolynomialSpace::first_point(&disjoint),
424        ) == F::ZERO;
425        // If these ever agreed, `vanishing_poly_is_zero_exactly_on_the_coset` above would be
426        // unable to tell in-domain from out-of-domain and would pass no matter what the
427        // implementation did.
428        assert_ne!(on_domain_is_zero, off_domain_is_zero);
429    }
430
431    /// `create_disjoint_domain`'s two claims, checked directly: the returned coset (a) has size
432    /// `min_size` rounded up to a power of two, and (b) shares no point with the original coset
433    /// (the short proof for this is in the doc comment on the trait method).
434    #[test]
435    fn create_disjoint_domain_has_correct_size_and_shares_no_point() {
436        let c = coset(F::new(13), 3); // size 8
437        let c_points = sorted_u32(c.iter());
438        for min_size in [1usize, 2, 3, 7, 8, 9, 20, 64] {
439            let k = PolynomialSpace::create_disjoint_domain(&c, min_size);
440            assert_eq!(PolynomialSpace::size(&k), min_size.next_power_of_two());
441            assert_eq!(k.shift(), c.shift() * F::GENERATOR);
442
443            let k_points = sorted_u32(k.iter());
444            let mut merged = c_points.clone();
445            merged.extend(&k_points);
446            merged.sort_unstable();
447            merged.dedup();
448            // No duplicates survive dedup <=> the two point sets were disjoint.
449            assert_eq!(merged.len(), c_points.len() + k_points.len());
450        }
451    }
452
453    #[test]
454    fn try_create_disjoint_domain_agrees_with_the_infallible_version_in_range() {
455        let c = coset(F::new(13), 3);
456        for min_size in [1usize, 5, 8, 100] {
457            assert_eq!(
458                PolynomialSpace::try_create_disjoint_domain(&c, min_size).map(|k| k.shift()),
459                Some(PolynomialSpace::create_disjoint_domain(&c, min_size).shift())
460            );
461        }
462    }
463
464    #[test]
465    fn try_create_disjoint_domain_rejects_out_of_range_min_size() {
466        let c = coset(F::new(13), 3);
467        // `F::TWO_ADICITY` itself is exactly representable...
468        assert!(PolynomialSpace::try_create_disjoint_domain(&c, 1 << F::TWO_ADICITY).is_some());
469        // ...but one more element than that requires one more bit of two-adicity than the field has.
470        assert!(
471            PolynomialSpace::try_create_disjoint_domain(&c, (1 << F::TWO_ADICITY) + 1).is_none()
472        );
473    }
474
475    #[test]
476    #[should_panic(expected = "exceeds 1 << Val::TWO_ADICITY")]
477    fn create_disjoint_domain_panics_out_of_range_where_try_returns_none() {
478        let c = coset(F::new(13), 3);
479        let _ = PolynomialSpace::create_disjoint_domain(&c, (1 << F::TWO_ADICITY) + 1);
480    }
481
482    /// `split_domains` decomposes `gH` into cosets of the index-`num_chunks` subgroup `K <= H`.
483    /// Group theory says those cosets exactly partition `gH`: pairwise disjoint, and their union
484    /// recovers every point of the original coset. Check both halves of that claim directly
485    /// instead of trusting the doc comment's proof sketch.
486    #[test]
487    fn split_domains_partition_the_original_coset() {
488        let c = coset(F::new(17), 4); // size 16
489        let orig_points = sorted_u32(c.iter());
490        for &num_chunks in &[1usize, 2, 4, 8, 16] {
491            let subs = PolynomialSpace::split_domains(&c, num_chunks);
492            assert_eq!(subs.len(), num_chunks);
493
494            let mut all_points: Vec<u32> = Vec::new();
495            for s in &subs {
496                assert_eq!(PolynomialSpace::size(s), c.size() / num_chunks);
497                all_points.extend(s.iter().map(|p| p.as_canonical_u32()));
498            }
499            all_points.sort_unstable();
500            assert_eq!(
501                all_points, orig_points,
502                "split_domains({num_chunks}) did not exactly partition the original coset"
503            );
504        }
505    }
506
507    /// Negative control: corrupt the recombination (drop the last sub-domain) and confirm the
508    /// partition check above would in fact catch a broken split — i.e. it is not vacuously true
509    /// because e.g. both sides happen to be sorted-empty.
510    #[test]
511    fn split_domains_negative_control_detects_a_missing_chunk() {
512        let c = coset(F::new(17), 4);
513        let orig_points = sorted_u32(c.iter());
514        let subs = PolynomialSpace::split_domains(&c, 4);
515        let mut all_points: Vec<u32> = Vec::new();
516        // Deliberately drop one sub-domain, simulating a broken split.
517        for s in &subs[..subs.len() - 1] {
518            all_points.extend(s.iter().map(|p| p.as_canonical_u32()));
519        }
520        all_points.sort_unstable();
521        assert_ne!(all_points, orig_points);
522    }
523
524    /// `split_evals` must place row `r` of the input into chunk `r % num_chunks` at position
525    /// `r / num_chunks` — the same decimation `split_domains` performs on points (chunk `c`'s
526    /// domain is `g^c * K` for `K = H^{num_chunks}`, so its `i`-th point is the `(i*num_chunks+c)`-th
527    /// point of the original domain).
528    #[test]
529    fn split_evals_decimates_rows_to_match_split_domains() {
530        let c = coset(F::new(3), 4); // size 16
531        let width = 2;
532        let height = c.size();
533        let values: Vec<F> = (0..height * width)
534            .map(|i| F::new((i / width) as u32))
535            .collect();
536        let evals = RowMajorMatrix::new(values, width);
537
538        let num_chunks = 4;
539        let chunks = PolynomialSpace::split_evals(&c, num_chunks, evals);
540        assert_eq!(chunks.len(), num_chunks);
541        let rows_per_chunk = height / num_chunks;
542        for (chunk_idx, chunk) in chunks.iter().enumerate() {
543            assert_eq!(chunk.height(), rows_per_chunk);
544            for i in 0..rows_per_chunk {
545                let expected_row = i * num_chunks + chunk_idx;
546                let row = chunk.row_slice(i).unwrap();
547                assert_eq!(row[0], F::new(expected_row as u32));
548            }
549        }
550    }
551
552    /// `selectors_on_coset` is a batched, independently-derived recomputation of
553    /// `selectors_at_point` (see the two doc comments: same closed forms, different code paths —
554    /// one via `batch_multiplicative_inverse`, one via direct field division). Cross-checking them
555    /// against each other is a much stronger test than checking either in isolation, since a bug
556    /// shared by both derivations would not show up in either alone — but a bug in just one of the
557    /// two implementations will.
558    #[test]
559    fn selectors_on_coset_agrees_with_selectors_at_point_for_every_point() {
560        let h = coset(F::ONE, 3); // the subgroup H itself, size 8
561        let disjoint_coset = PolynomialSpace::create_disjoint_domain(&h, 2 * h.size()); // size 16, rate 2
562
563        let batched = PolynomialSpace::selectors_on_coset(&h, disjoint_coset);
564        let points: Vec<F> = disjoint_coset.iter().collect();
565        assert_eq!(points.len(), batched.is_first_row.len());
566
567        for (i, &x) in points.iter().enumerate() {
568            let single = PolynomialSpace::selectors_at_point::<F>(&h, x);
569            assert_eq!(single.is_first_row, batched.is_first_row[i]);
570            assert_eq!(single.is_last_row, batched.is_last_row[i]);
571            assert_eq!(single.is_transition, batched.is_transition[i]);
572            assert_eq!(single.inv_vanishing, batched.inv_vanishing[i]);
573        }
574    }
575
576    /// Negative control: two different points of `disjoint_coset` must not, in general, produce
577    /// identical selector values — otherwise the per-point comparison above could pass simply
578    /// because every entry is some constant, independent of which point is plugged in.
579    #[test]
580    fn selectors_negative_control_values_actually_vary_by_point() {
581        let h = coset(F::ONE, 3);
582        let disjoint_coset = PolynomialSpace::create_disjoint_domain(&h, 2 * h.size());
583        let mut points = disjoint_coset.iter();
584        let x0 = points.next().unwrap();
585        let x1 = points.next().unwrap();
586        let s0 = PolynomialSpace::selectors_at_point::<F>(&h, x0);
587        let s1 = PolynomialSpace::selectors_at_point::<F>(&h, x1);
588        assert_ne!(s0.is_first_row, s1.is_first_row);
589    }
590}