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}