1use std::{iter, ops::Deref, slice};
28
29use binius_compute::{Allocator, BufferData, CollectIntoAllocVec, VecLike};
30use binius_field::{Field, PackedField, field::FieldOps};
31use binius_utils::rayon::{
32 prelude::*,
33 task_size::{IndexedParallelIteratorExt, WorkPerItem, min_len_for_bytes},
34};
35
36use crate::{FieldBuffer, FieldVec};
37
38pub trait Hypercube {
44 fn basis<F: FieldOps>(coord: &F) -> [F; 2];
48
49 fn expand_var<F: FieldOps>(value: &F, coord: &F) -> [F; 2];
55
56 fn contract_var<F: FieldOps>(lo: &mut F, hi: &F);
69
70 fn extrapolate_line<F: FieldOps>(lo: F, hi: F, coord: F) -> F;
80
81 fn eq_one_var<F: FieldOps>(x: F, y: F) -> F {
89 let [x_0, x_1] = Self::basis(&x);
91 let [y_0, y_1] = Self::basis(&y);
92 x_0 * y_0 + x_1 * y_1
93 }
94}
95
96#[derive(Debug)]
101pub struct OneCube;
102
103impl Hypercube for OneCube {
104 #[inline(always)]
105 fn basis<F: FieldOps>(coord: &F) -> [F; 2] {
106 [F::one() - coord, coord.clone()]
107 }
108
109 #[inline(always)]
110 fn expand_var<F: FieldOps>(value: &F, coord: &F) -> [F; 2] {
111 let prod = value.clone() * coord;
113 [value.clone() - &prod, prod]
114 }
115
116 #[inline(always)]
117 fn contract_var<F: FieldOps>(lo: &mut F, hi: &F) {
118 *lo += hi;
120 }
121
122 #[inline(always)]
123 fn extrapolate_line<F: FieldOps>(lo: F, hi: F, coord: F) -> F {
124 lo.clone() + (hi - lo) * coord
127 }
128
129 #[inline(always)]
130 fn eq_one_var<F: FieldOps>(x: F, y: F) -> F {
131 if F::Scalar::CHARACTERISTIC == 2 {
137 x + y + F::one()
138 } else {
139 let one = F::one();
140 x.clone() * y.clone() + (one.clone() - x) * (one - y)
141 }
142 }
143}
144
145#[derive(Debug)]
154pub struct InfCube;
155
156impl Hypercube for InfCube {
157 #[inline(always)]
158 fn basis<F: FieldOps>(coord: &F) -> [F; 2] {
159 [F::one(), coord.clone()]
160 }
161
162 #[inline(always)]
163 fn expand_var<F: FieldOps>(value: &F, coord: &F) -> [F; 2] {
164 [value.clone(), value.clone() * coord]
166 }
167
168 #[inline(always)]
169 fn contract_var<F: FieldOps>(_lo: &mut F, _hi: &F) {
170 }
173
174 #[inline(always)]
175 fn extrapolate_line<F: FieldOps>(lo: F, hi: F, coord: F) -> F {
176 lo + hi * coord
178 }
179
180 #[inline(always)]
181 fn eq_one_var<F: FieldOps>(x: F, y: F) -> F {
182 F::one() + x * y
184 }
185}
186
187pub fn tensor_prod_eq_ind<Cube: Hypercube, P: PackedField>(
214 values: FieldBuffer<P, Vec<P>>,
215 extra_query_coordinates: &[P::Scalar],
216) -> FieldBuffer<P, Vec<P>> {
217 let start_log_len = values.log_len();
218 let final_log_len = start_log_len + extra_query_coordinates.len();
219 let mut data = values.into_inner();
220
221 let final_packed_len = packed_words::<P>(final_log_len);
225 data.reserve_exact(final_packed_len.saturating_sub(data.len()));
226
227 tensor_prod_eq_ind_reserved::<Cube, P, _>(
228 FieldBuffer::new(start_log_len, data),
229 extra_query_coordinates,
230 )
231}
232
233const fn packed_words<P: PackedField>(log_len: usize) -> usize {
237 1usize << log_len.saturating_sub(P::LOG_WIDTH)
238}
239
240fn tensor_prod_eq_ind_reserved<Cube: Hypercube, P: PackedField, Data: VecLike<P>>(
250 values: FieldBuffer<P, Data>,
251 extra_query_coordinates: &[P::Scalar],
252) -> FieldBuffer<P, Data> {
253 let start_log_len = values.log_len();
254 let final_log_len = start_log_len + extra_query_coordinates.len();
255 let mut data = values.into_inner();
256
257 debug_assert!(data.capacity() >= packed_words::<P>(final_log_len));
259
260 let sub_width_count = extra_query_coordinates
265 .len()
266 .min(P::LOG_WIDTH.saturating_sub(start_log_len));
267 let (sub_width_coords, packed_coords) = extra_query_coordinates.split_at(sub_width_count);
268
269 for (i, &r_i) in sub_width_coords.iter().enumerate() {
273 let log_len = start_log_len + i;
274 let packed_r_i = P::broadcast(r_i);
275 let (lo, _) = data[0].interleave(P::zero(), log_len);
276 let [lo, hi] = Cube::expand_var(&lo, &packed_r_i);
277 data[0] = lo.interleave(hi, log_len).0;
278 }
279
280 for &r_i in packed_coords {
285 let packed_r_i = P::broadcast(r_i);
286 let old_packed = data.len();
287
288 let low_ptr = data.as_mut_ptr();
292 let high = &mut data.spare_capacity_mut()[..old_packed];
293 let low = unsafe { slice::from_raw_parts_mut(low_ptr, old_packed) };
296 (low, high)
299 .into_par_iter()
300 .with_min_task(WorkPerItem::FieldMuls)
301 .for_each(|(low_i, high_i)| {
302 let [new_low, new_high] = Cube::expand_var(low_i, &packed_r_i);
303 *low_i = new_low;
304 high_i.write(new_high);
305 });
306 unsafe { data.set_len(2 * old_packed) };
308 }
309
310 FieldBuffer::new(final_log_len, data)
311}
312
313pub fn eq_ind_partial_eval<Cube: Hypercube, P: PackedField>(point: &[P::Scalar]) -> FieldBuffer<P> {
323 scaled_eq_ind_partial_eval::<Cube, P>(point, P::Scalar::ONE)
325}
326
327pub fn eq_ind_partial_eval_in<Cube: Hypercube, A: Allocator, P: PackedField>(
331 alloc: &A,
332 point: &[P::Scalar],
333) -> FieldVec<P, A> {
334 let packed_len = packed_words::<P>(point.len());
336 scaled_eq_ind_partial_eval_into::<Cube, P, _>(
337 point,
338 P::Scalar::ONE,
339 alloc.alloc::<P>(packed_len),
340 )
341}
342
343pub fn scaled_eq_ind_partial_eval<Cube: Hypercube, P: PackedField>(
353 point: &[P::Scalar],
354 scale: P::Scalar,
355) -> FieldBuffer<P> {
356 let packed_len = packed_words::<P>(point.len());
358 scaled_eq_ind_partial_eval_into::<Cube, P, _>(point, scale, Vec::with_capacity(packed_len))
359}
360
361pub fn scaled_eq_ind_partial_eval_into<Cube: Hypercube, P: PackedField, Data: VecLike<P>>(
374 point: &[P::Scalar],
375 scale: P::Scalar,
376 mut buffer: Data,
377) -> FieldBuffer<P, Data> {
378 assert!(
379 buffer.capacity() >= packed_words::<P>(point.len()),
380 "precondition: buffer capacity must cover the packed expansion length"
381 );
382
383 buffer.clear();
386 buffer.push(P::from_scalars(iter::once(scale)));
387 let seed = FieldBuffer::new(0, buffer);
388
389 let low_len = (point.len() / 2).max(P::LOG_WIDTH).min(point.len());
399 let (low_coords, high_coords) = point.split_at(low_len);
400
401 let low = tensor_prod_eq_ind_reserved::<Cube, P, Data>(seed, low_coords);
404
405 if high_coords.is_empty() {
407 return low;
408 }
409
410 let block = low.as_ref().len();
414 let mut data = low.into_inner();
415
416 let high = eq_ind_partial_eval_scalars::<Cube, P::Scalar>(high_coords);
418 let total = block * high.len();
419 debug_assert_eq!(total, packed_words::<P>(point.len()));
420
421 let first_ptr = data.as_mut_ptr();
425 let spare = &mut data.spare_capacity_mut()[..total - block];
426 let first = unsafe { slice::from_raw_parts(first_ptr, block) };
429
430 let min_len = (min_len_for_bytes::<P>() / block).max(1);
433 spare
434 .par_chunks_mut(block)
435 .zip(high[1..].par_iter())
436 .with_min_len(min_len)
437 .for_each(|(dst, &coeff)| {
438 let coeff = P::broadcast(coeff);
439 for (dst_i, &src_i) in iter::zip(dst, first) {
440 dst_i.write(src_i * coeff);
441 }
442 });
443 unsafe { data.set_len(total) };
445
446 let coeff = P::broadcast(high[0]);
449 for word in &mut data[..block] {
450 *word *= coeff;
451 }
452
453 FieldBuffer::new(point.len(), data)
454}
455
456pub fn eq_ind_truncate_low_inplace<Cube: Hypercube, P: PackedField, Data: BufferData<P>>(
469 values: &mut FieldBuffer<P, Data>,
470 truncated_log_len: usize,
471) {
472 assert!(
473 truncated_log_len <= values.log_len(),
474 "precondition: truncated_log_len must be at most values.log_len()"
475 );
476
477 for log_len in (truncated_log_len..values.log_len()).rev() {
479 {
480 let mut split = values.split_half_mut();
481 let (mut lo, hi) = split.halves();
482 (lo.as_mut(), hi.as_ref())
485 .into_par_iter()
486 .with_min_task_bytes::<[P; 2]>()
487 .for_each(|(zero, one)| {
488 Cube::contract_var(zero, one);
489 });
490 }
491
492 values.truncate(log_len);
493 }
494}
495
496pub fn eq_ind<Cube: Hypercube, F: FieldOps>(x: &[F], y: &[F]) -> F {
504 assert_eq!(x.len(), y.len(), "pre-condition: x and y must be the same length");
505 iter::zip(x, y)
507 .map(|(x, y)| Cube::eq_one_var(x.clone(), y.clone()))
508 .product()
509}
510
511pub fn eq_ind_zero<Cube: Hypercube, F: FieldOps>(point: &[F]) -> F {
519 point
521 .iter()
522 .map(|y| {
523 let [y_0, _] = Cube::basis(y);
524 y_0
525 })
526 .product()
527}
528
529pub fn eq_ind_partial_eval_scalars<Cube: Hypercube, F: FieldOps>(point: &[F]) -> Vec<F> {
533 scaled_eq_ind_partial_eval_scalars::<Cube, F>(point, F::one())
535}
536
537pub fn scaled_eq_ind_partial_eval_scalars<Cube: Hypercube, F: FieldOps>(
542 point: &[F],
543 scale: F,
544) -> Vec<F> {
545 let mut result = Vec::with_capacity(1 << point.len());
547 result.push(scale);
549
550 for r_i in point {
551 let len = result.len();
558 for j in 0..len {
559 let [lo, hi] = Cube::expand_var(&result[j], r_i);
560 result[j] = lo;
561 result.push(hi);
562 }
563 }
564 result
565}
566
567pub fn fold_highest_var_inplace<Cube: Hypercube, P: PackedField, Data: BufferData<P>>(
580 values: &mut FieldBuffer<P, Data>,
581 scalar: P::Scalar,
582) {
583 let broadcast_scalar = P::broadcast(scalar);
586 {
587 let mut split = values.split_half_mut();
589 let (mut lo, mut hi) = split.halves();
590 (lo.as_mut(), hi.as_mut())
592 .into_par_iter()
593 .with_min_task(WorkPerItem::FieldMuls)
594 .for_each(|(lo_i, hi_i)| {
595 *lo_i = Cube::extrapolate_line(*lo_i, *hi_i, broadcast_scalar);
596 });
597 }
598
599 values.truncate(values.log_len() - 1);
601}
602
603pub fn fold_highest_var<
619 Cube: Hypercube,
620 A: Allocator,
621 P: PackedField,
622 Data: Deref<Target = [P]>,
623>(
624 alloc: &A,
625 values: &FieldBuffer<P, Data>,
626 scalar: P::Scalar,
627) -> FieldVec<P, A> {
628 assert!(values.log_len() > 0, "precondition: buffer must have at least one variable");
629
630 let broadcast_scalar = P::broadcast(scalar);
632 let (lo, hi) = values.split_half();
633
634 let data = (lo.as_ref(), hi.as_ref())
637 .into_par_iter()
638 .with_min_task(WorkPerItem::FieldMuls)
639 .map(|(&lo_i, &hi_i)| Cube::extrapolate_line(lo_i, hi_i, broadcast_scalar))
640 .collect_into_alloc_vec(alloc);
641 FieldBuffer::new(values.log_len() - 1, data)
642}
643
644#[cfg(test)]
645mod tests {
646 use binius_compute::GlobalAllocator;
647 use binius_utils::rayon::task_size::{min_len_for_bytes, min_len_for_work};
648 use proptest::prelude::*;
649 use rand::prelude::*;
650
651 use super::*;
652 use crate::test_utils::{B128, Packed128b, random_field_buffer, random_scalars};
653
654 type P = Packed128b;
655 type F = B128;
656
657 #[test]
658 fn expand_var_matches_scaled_basis() {
659 let mut rng = StdRng::seed_from_u64(0);
660
661 let [value, coord] = [(); 2].map(|_| random_scalars::<F>(&mut rng, 1)[0]);
664 assert_eq!(
665 OneCube::expand_var(&value, &coord),
666 OneCube::basis(&coord).map(|b_i| b_i * value)
667 );
668 assert_eq!(
669 InfCube::expand_var(&value, &coord),
670 InfCube::basis(&coord).map(|b_i| b_i * value)
671 );
672 }
673
674 #[test]
675 fn contract_var_inverts_expand_var() {
676 let mut rng = StdRng::seed_from_u64(0);
677
678 let [value, coord] = [(); 2].map(|_| random_scalars::<F>(&mut rng, 1)[0]);
680
681 let [mut lo, hi] = OneCube::expand_var(&value, &coord);
682 OneCube::contract_var(&mut lo, &hi);
683 assert_eq!(lo, value);
684
685 let [mut lo, hi] = InfCube::expand_var(&value, &coord);
686 InfCube::contract_var(&mut lo, &hi);
687 assert_eq!(lo, value);
688 }
689
690 #[test]
691 fn eq_one_var_matches_basis_definition() {
692 let mut rng = StdRng::seed_from_u64(0);
693
694 let [x, y] = [(); 2].map(|_| random_scalars::<F>(&mut rng, 1)[0]);
697 let eq_from_basis = |[x_0, x_1]: [F; 2], [y_0, y_1]: [F; 2]| x_0 * y_0 + x_1 * y_1;
698 assert_eq!(
699 OneCube::eq_one_var(x, y),
700 eq_from_basis(OneCube::basis(&x), OneCube::basis(&y))
701 );
702 assert_eq!(
703 InfCube::eq_one_var(x, y),
704 eq_from_basis(InfCube::basis(&x), InfCube::basis(&y))
705 );
706 }
707
708 #[test]
709 fn inf_cube_eq_ind_zero_is_one() {
710 let mut rng = StdRng::seed_from_u64(0);
711
712 for n_vars in [0, 1, 5] {
714 let point = random_scalars::<F>(&mut rng, n_vars);
715 assert_eq!(eq_ind_zero::<InfCube, F>(&point), F::ONE);
716
717 assert_eq!(
719 eq_ind_zero::<InfCube, F>(&point),
720 eq_ind::<InfCube, F>(&vec![F::ZERO; n_vars], &point)
721 );
722 }
723 }
724
725 fn inf_cube_reference(point: &[F]) -> Vec<F> {
727 (0..1 << point.len())
729 .map(|index| {
730 point
731 .iter()
732 .enumerate()
733 .filter(|(i, _)| index >> i & 1 == 1)
734 .map(|(_, r_i)| *r_i)
735 .product()
736 })
737 .collect()
738 }
739
740 fn eval_monomial_basis(coeffs: &[F], point: &[F]) -> F {
742 coeffs
744 .iter()
745 .enumerate()
746 .map(|(index, coeff)| {
747 *coeff
748 * point
749 .iter()
750 .enumerate()
751 .filter(|(i, _)| index >> i & 1 == 1)
752 .map(|(_, x_i)| *x_i)
753 .product::<F>()
754 })
755 .sum()
756 }
757
758 #[test]
759 fn inf_cube_expansion_matches_the_tensor_of_bases() {
760 let mut rng = StdRng::seed_from_u64(0);
761
762 for n_vars in [0, 1, 2, 5, 8] {
764 let point = random_scalars::<F>(&mut rng, n_vars);
765 let expansion = eq_ind_partial_eval::<InfCube, P>(&point);
766 let expansion_scalars = expansion.iter_scalars().collect::<Vec<_>>();
767 assert_eq!(expansion_scalars, inf_cube_reference(&point), "mismatch at {n_vars} vars");
768 }
769 }
770
771 #[test]
772 fn inf_cube_expansion_holds_the_monomial_coefficients_of_the_indicator() {
773 let mut rng = StdRng::seed_from_u64(0);
774
775 for n_vars in [0, 1, 2, 5] {
781 let point = random_scalars::<F>(&mut rng, n_vars);
782 let coeffs = eq_ind_partial_eval_scalars::<InfCube, F>(&point);
783
784 let x = random_scalars::<F>(&mut rng, n_vars);
785 assert_eq!(eval_monomial_basis(&coeffs, &x), eq_ind::<InfCube, F>(&x, &point));
786 }
787 }
788
789 #[test]
790 fn inf_cube_expansion_is_the_evaluation_functional() {
791 let mut rng = StdRng::seed_from_u64(0);
792
793 for n_vars in [0, 1, 2, 5] {
796 let point = random_scalars::<F>(&mut rng, n_vars);
797 let coeffs = random_scalars::<F>(&mut rng, 1 << n_vars);
798
799 let expansion = eq_ind_partial_eval_scalars::<InfCube, F>(&point);
800 let inner_product = iter::zip(&coeffs, &expansion)
801 .map(|(c, e)| *c * e)
802 .sum::<F>();
803 assert_eq!(inner_product, eval_monomial_basis(&coeffs, &point));
804 }
805 }
806
807 #[test]
808 fn extrapolate_line_matches_basis_definition() {
809 let mut rng = StdRng::seed_from_u64(0);
810
811 let [lo, hi, coord] = [(); 3].map(|_| random_scalars::<F>(&mut rng, 1)[0]);
814 let from_basis = |[b_0, b_1]: [F; 2]| lo * b_0 + hi * b_1;
815 assert_eq!(OneCube::extrapolate_line(lo, hi, coord), from_basis(OneCube::basis(&coord)));
816 assert_eq!(InfCube::extrapolate_line(lo, hi, coord), from_basis(InfCube::basis(&coord)));
817 }
818
819 #[test]
820 fn inf_cube_fold_fixes_the_highest_variable() {
821 let mut rng = StdRng::seed_from_u64(0);
822
823 for n_vars in [1, 2, 5] {
826 let original = random_field_buffer::<P>(&mut rng, n_vars);
827 let challenge = random_scalars::<F>(&mut rng, 1)[0];
828 let x = random_scalars::<F>(&mut rng, n_vars - 1);
829
830 let out_of_place =
831 fold_highest_var::<InfCube, _, _, _>(&GlobalAllocator, &original, challenge);
832 let mut in_place = original.clone();
833 fold_highest_var_inplace::<InfCube, _, _>(&mut in_place, challenge);
834 assert_eq!(out_of_place, in_place);
835
836 let original = original.iter_scalars().collect::<Vec<_>>();
837 let folded = in_place.iter_scalars().collect::<Vec<_>>();
838 let full_point = [x.as_slice(), &[challenge]].concat();
839 assert_eq!(
840 eval_monomial_basis(&folded, &x),
841 eval_monomial_basis(&original, &full_point)
842 );
843 }
844 }
845
846 #[test]
847 fn growth_above_the_split_threshold_matches_the_inline_path() {
848 let mut rng = StdRng::seed_from_u64(9);
849
850 let min_len = min_len_for_work(WorkPerItem::FieldMuls);
858 let n_vars = (2 * min_len * P::WIDTH).next_power_of_two().ilog2() as usize;
859 let point = random_scalars::<F>(&mut rng, n_vars);
860
861 let packed = eq_ind_partial_eval::<OneCube, P>(&point);
862 let reference = eq_ind_partial_eval_scalars::<OneCube, F>(&point);
863 assert!(packed.iter_scalars().eq(reference.iter().copied()));
864 }
865
866 #[test]
867 fn truncation_above_the_split_threshold_matches_the_inline_path() {
868 let mut rng = StdRng::seed_from_u64(0);
869
870 let min_len = min_len_for_bytes::<[P; 2]>();
878 let n_vars = (2 * min_len * P::WIDTH).next_power_of_two().ilog2() as usize;
879 let point = random_scalars::<F>(&mut rng, n_vars);
880
881 let mut truncated = eq_ind_partial_eval::<OneCube, P>(&point);
883 eq_ind_truncate_low_inplace::<OneCube, _, _>(&mut truncated, n_vars - 1);
884 assert_eq!(truncated, eq_ind_partial_eval::<OneCube, P>(&point[..n_vars - 1]));
885 }
886
887 proptest! {
888 #![proptest_config(ProptestConfig::with_cases(16))]
889
890 #[test]
891 fn the_two_engines_agree(
892 seed in any::<u64>(),
893 log_n in 0usize..=8,
894 ) {
895 let mut rng = StdRng::seed_from_u64(seed);
896 let point = random_scalars::<F>(&mut rng, log_n);
897 let scale = random_scalars::<F>(&mut rng, 1)[0];
898
899 prop_assert_eq!(
902 eq_ind_partial_eval::<OneCube, P>(&point).iter_scalars().collect::<Vec<_>>(),
903 eq_ind_partial_eval_scalars::<OneCube, F>(&point)
904 );
905 prop_assert_eq!(
906 eq_ind_partial_eval::<InfCube, P>(&point).iter_scalars().collect::<Vec<_>>(),
907 eq_ind_partial_eval_scalars::<InfCube, F>(&point)
908 );
909 prop_assert_eq!(
910 scaled_eq_ind_partial_eval::<OneCube, P>(&point, scale)
911 .iter_scalars()
912 .collect::<Vec<_>>(),
913 scaled_eq_ind_partial_eval_scalars::<OneCube, F>(&point, scale)
914 );
915 prop_assert_eq!(
916 scaled_eq_ind_partial_eval::<InfCube, P>(&point, scale)
917 .iter_scalars()
918 .collect::<Vec<_>>(),
919 scaled_eq_ind_partial_eval_scalars::<InfCube, F>(&point, scale)
920 );
921
922 prop_assert_eq!(
930 eq_ind_partial_eval::<OneCube, F>(&point).into_inner(),
931 eq_ind_partial_eval_scalars::<OneCube, F>(&point)
932 );
933 prop_assert_eq!(
934 eq_ind_partial_eval::<InfCube, F>(&point).into_inner(),
935 eq_ind_partial_eval_scalars::<InfCube, F>(&point)
936 );
937 }
938
939 #[test]
940 fn scaling_commutes_with_the_expansion(
941 seed in any::<u64>(),
942 log_n in 0usize..=8,
943 ) {
944 let mut rng = StdRng::seed_from_u64(seed);
945 let point = random_scalars::<F>(&mut rng, log_n);
946 let scale = random_scalars::<F>(&mut rng, 1)[0];
947
948 let one_scaled = scaled_eq_ind_partial_eval::<OneCube, P>(&point, scale);
950 let one_plain = eq_ind_partial_eval::<OneCube, P>(&point);
951 for (got, base) in one_scaled.iter_scalars().zip(one_plain.iter_scalars()) {
952 prop_assert_eq!(got, scale * base);
953 }
954
955 let inf_scaled = scaled_eq_ind_partial_eval::<InfCube, P>(&point, scale);
956 let inf_plain = eq_ind_partial_eval::<InfCube, P>(&point);
957 for (got, base) in inf_scaled.iter_scalars().zip(inf_plain.iter_scalars()) {
958 prop_assert_eq!(got, scale * base);
959 }
960 }
961
962 #[test]
963 fn truncation_strips_trailing_variables(
964 seed in any::<u64>(),
965 log_n in 0usize..=8,
966 ) {
967 let mut rng = StdRng::seed_from_u64(seed);
968 let point = random_scalars::<F>(&mut rng, log_n);
969
970 for truncated_log_len in 0..=log_n {
972 let mut one_cube = eq_ind_partial_eval::<OneCube, P>(&point);
973 eq_ind_truncate_low_inplace::<OneCube, _, _>(&mut one_cube, truncated_log_len);
974 prop_assert_eq!(
975 one_cube,
976 eq_ind_partial_eval::<OneCube, P>(&point[..truncated_log_len])
977 );
978
979 let mut inf_cube = eq_ind_partial_eval::<InfCube, P>(&point);
980 eq_ind_truncate_low_inplace::<InfCube, _, _>(&mut inf_cube, truncated_log_len);
981 prop_assert_eq!(
982 inf_cube,
983 eq_ind_partial_eval::<InfCube, P>(&point[..truncated_log_len])
984 );
985 }
986 }
987
988 #[test]
989 fn the_split_agrees_with_the_doubling_rounds(
990 seed in any::<u64>(),
991 n_vars in 0usize..=10,
992 ) {
993 let mut rng = StdRng::seed_from_u64(seed);
999 let point = random_scalars::<F>(&mut rng, n_vars);
1000 let scale = random_scalars::<F>(&mut rng, 1)[0];
1001
1002 let seeded = || {
1004 let mut buffer = Vec::with_capacity(1 << n_vars.saturating_sub(P::LOG_WIDTH));
1005 buffer.push(P::from_scalars(iter::once(scale)));
1006 FieldBuffer::new(0, buffer)
1007 };
1008
1009 prop_assert_eq!(
1010 scaled_eq_ind_partial_eval::<OneCube, P>(&point, scale),
1011 tensor_prod_eq_ind_reserved::<OneCube, P, _>(seeded(), &point)
1012 );
1013 prop_assert_eq!(
1014 scaled_eq_ind_partial_eval::<InfCube, P>(&point, scale),
1015 tensor_prod_eq_ind_reserved::<InfCube, P, _>(seeded(), &point)
1016 );
1017 }
1018 }
1019
1020 #[test]
1021 fn the_split_path_agrees_with_the_scalar_engine() {
1022 let mut rng = StdRng::seed_from_u64(0);
1023
1024 for n_vars in (0..=8).chain([13, 14, 19]) {
1031 let point = random_scalars::<F>(&mut rng, n_vars);
1032 let scale = random_scalars::<F>(&mut rng, 1)[0];
1033
1034 let packed = scaled_eq_ind_partial_eval::<OneCube, P>(&point, scale);
1035 let scalars = scaled_eq_ind_partial_eval_scalars::<OneCube, F>(&point, scale);
1036 assert!(packed.iter_scalars().eq(scalars), "one cube at {n_vars} vars");
1037
1038 let packed = scaled_eq_ind_partial_eval::<InfCube, P>(&point, scale);
1039 let scalars = scaled_eq_ind_partial_eval_scalars::<InfCube, F>(&point, scale);
1040 assert!(packed.iter_scalars().eq(scalars), "inf cube at {n_vars} vars");
1041 }
1042 }
1043}