1use std::collections::HashMap;
11use std::fmt::Debug;
12
13use anyhow::{Result, anyhow, ensure};
14use poulpy_hal::layouts::Backend;
15
16use crate::layouts::IntPolyInfos;
17use rand_distr::num_traits::{Float, FloatConst, FromPrimitive};
18
19use crate::layouts::{GLWEInfos, GLWEToBackendMut, GLWEToBackendRef};
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub enum Basis {
26 Monomial,
28 Chebyshev,
30}
31
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub enum Parity {
35 Full,
37 Even,
39 Odd,
41}
42
43#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
49pub enum PolynomialInputTransform {
50 #[default]
52 Identity,
53 Square,
55 SquareTimesInput,
57 ChebyshevT2,
59 ChebyshevT2TimesInput,
61}
62
63impl PolynomialInputTransform {
64 fn extra_depth(self) -> usize {
65 match self {
66 Self::Identity => 0,
67 Self::Square | Self::ChebyshevT2 => 1,
68 Self::SquareTimesInput | Self::ChebyshevT2TimesInput => 2,
69 }
70 }
71}
72
73#[derive(Clone, Copy, Debug, PartialEq, Eq)]
77pub enum SplitStrategy {
78 MinDepth,
80 MinMult,
82}
83
84pub const DEFAULT_SPLIT_STRATEGY: SplitStrategy = SplitStrategy::MinDepth;
86
87pub(crate) fn min_depth_split(log_degree: usize) -> usize {
90 debug_assert!(log_degree >= 1, "min_depth_split requires log_degree ≥ 1");
91 let s = log_degree >> 1;
92 let a = (1 << s) + (1 << (log_degree - s)) + log_degree - s - 3;
93 let b = (1 << (s + 1)) + (1 << (log_degree - s - 1)) + log_degree - s - 4;
94 if a > b { s + 1 } else { s }
95}
96
97pub(crate) fn min_mult_split(degree: usize, parity: Parity, basis: Basis) -> usize {
100 let log_degree = bit_len(degree);
101 if log_degree <= 1 {
102 return 1;
103 }
104 let mut best_log_split = 1usize;
105 let mut best_score = (usize::MAX, usize::MAX);
106 for log_split in 1..=log_degree {
107 let score = estimate_op_counts(degree, log_split, parity, basis);
108 if score < best_score {
109 best_score = score;
110 best_log_split = log_split;
111 }
112 }
113 best_log_split
114}
115
116pub(crate) fn split_for_strategy(strategy: SplitStrategy, degree: usize, parity: Parity, basis: Basis) -> usize {
117 match strategy {
118 SplitStrategy::MinDepth => min_depth_split(bit_len(degree)),
119 SplitStrategy::MinMult => min_mult_split(degree, parity, basis),
120 }
121}
122
123fn estimate_op_counts(degree: usize, log_split: usize, parity: Parity, basis: Basis) -> (usize, usize) {
125 let mut baby_degrees: Vec<usize> = Vec::new();
126 simulate_baby_step_decomposition(degree, log_split, &mut baby_degrees);
127 if baby_degrees.is_empty() {
128 return (usize::MAX, usize::MAX);
129 }
130 let pt_ct: usize = baby_degrees.iter().map(|&d| pt_ct_muls_for_baby_step(d, parity)).sum();
131 let giant_steps = baby_degrees.len().saturating_sub(1);
132 let power_basis = estimate_power_basis_muls(degree, log_split, parity, basis);
133 let mut cc = giant_steps + power_basis;
134 let mut pc = pt_ct;
135 let trailing_const = baby_degrees.len() >= 2 && *baby_degrees.last().unwrap() == 0;
136 if trailing_const && degree.is_power_of_two() && cc > 0 {
137 cc -= 1;
138 pc += 1;
139 }
140 (cc, pc)
141}
142
143fn simulate_baby_step_decomposition(degree: usize, log_split: usize, out: &mut Vec<usize>) {
145 let base = 1usize << log_split;
146 if degree < base {
147 out.push(degree);
148 return;
149 }
150 let mut next_power = base;
151 while next_power < (degree >> 1) + 1 {
152 next_power <<= 1;
153 }
154 simulate_baby_step_decomposition(next_power - 1, log_split, out);
155 simulate_baby_step_decomposition(degree - next_power, log_split, out);
156}
157
158fn pt_ct_muls_for_baby_step(degree: usize, parity: Parity) -> usize {
159 let (first, step) = match parity {
160 Parity::Even => (2usize, 2usize),
161 Parity::Odd => (1, 2),
162 Parity::Full => (1, 1),
163 };
164 if degree < first { 0 } else { (degree - first) / step + 1 }
165}
166
167fn estimate_power_basis_muls(degree: usize, log_split: usize, parity: Parity, basis: Basis) -> usize {
168 if degree < 2 {
169 return 0;
170 }
171 let log_degree = bit_len(degree);
172 let largest_pow2 = 1usize << (log_degree - 1);
173 let base = 1usize << log_split;
174
175 let mut targets: Vec<usize> = Vec::new();
176 if largest_pow2 >= 2 {
177 targets.push(largest_pow2);
178 }
179 let baby_limit = base.min(degree + 1);
180 match parity {
181 Parity::Even => targets.extend((4..baby_limit).step_by(2)),
182 Parity::Odd => targets.extend((3..baby_limit).step_by(2)),
183 Parity::Full => targets.extend(3..baby_limit),
184 }
185
186 let mut needed = std::collections::HashSet::<usize>::new();
187 let mut stack = targets;
188 while let Some(n) = stack.pop() {
189 if n <= 1 || !needed.insert(n) {
190 continue;
191 }
192 let (a, b) = split_degree(n);
193 stack.push(a);
194 stack.push(b);
195 if matches!(basis, Basis::Chebyshev) {
196 let c = a.abs_diff(b);
197 if c >= 2 {
198 stack.push(c);
199 }
200 }
201 }
202 needed.len()
203}
204
205fn decompose_bsgs_coeffs<F>(
206 basis: Basis,
207 coeffs: &[F],
208 log_split: usize,
209 max_degree: usize,
210 lead: bool,
211 split_leading: bool,
212 visit: &mut impl FnMut(&[F]) -> Result<()>,
213) -> Result<()>
214where
215 F: Float,
216{
217 let degree = coeffs.len().saturating_sub(1);
218 let base = 1usize << log_split;
219 if degree < base {
220 if split_leading && should_split_leading_baby_step(degree, log_split, max_degree, lead) {
221 let log_degree = bit_len(degree);
222 let smaller = min_depth_split(log_degree);
223 if smaller < log_split {
224 return decompose_bsgs_coeffs(basis, coeffs, smaller, max_degree, lead, split_leading, visit);
225 }
226 }
227 return visit(coeffs);
228 }
229
230 let mut next_power = base;
231 while next_power < (degree >> 1) + 1 {
232 next_power <<= 1;
233 }
234
235 match basis {
236 Basis::Monomial => {
237 decompose_bsgs_coeffs(
238 basis,
239 &coeffs[..next_power],
240 log_split,
241 max_degree,
242 false,
243 split_leading,
244 visit,
245 )?;
246 decompose_bsgs_coeffs(
247 basis,
248 &coeffs[next_power..],
249 log_split,
250 max_degree,
251 lead,
252 split_leading,
253 visit,
254 )
255 }
256 Basis::Chebyshev => {
257 let (q, r) = factorize_coeffs_chebyshev(coeffs, next_power);
258 decompose_bsgs_coeffs(basis, &r, log_split, max_degree, false, split_leading, visit)?;
259 decompose_bsgs_coeffs(basis, &q, log_split, max_degree, lead, split_leading, visit)
260 }
261 }
262}
263
264fn should_split_leading_baby_step(degree: usize, log_split: usize, max_degree: usize, lead: bool) -> bool {
265 if !lead || degree == 0 || log_split <= 1 {
266 return false;
267 }
268 let next_strict_power_of_two = 1usize << bit_len(max_degree);
269 let close_to_next_power = next_strict_power_of_two - (1usize << (log_split - 1));
270 max_degree > close_to_next_power
271}
272
273fn bit_len(n: usize) -> usize {
274 usize::BITS as usize - n.leading_zeros() as usize
275}
276
277fn factorize_coeffs_chebyshev<F>(coeffs: &[F], n: usize) -> (Vec<F>, Vec<F>)
278where
279 F: Float,
280{
281 let mut r = vec![F::zero(); n];
282 r.copy_from_slice(&coeffs[..n]);
283
284 let mut q = vec![F::zero(); coeffs.len() - n];
285 q[0] = coeffs[n];
286
287 let two = F::one() + F::one();
288 for (i, j) in (n + 1..coeffs.len()).zip(1..) {
289 q[i - n] = two * coeffs[i];
290 r[n - j] = r[n - j] - coeffs[i];
291 }
292
293 (q, r)
294}
295
296pub fn split_degree(n: usize) -> (usize, usize) {
302 assert!(n > 1);
303 if n.is_power_of_two() {
304 (n / 2, n / 2)
305 } else {
306 let k = (usize::BITS - (n - 1).leading_zeros()) as usize - 1;
307 let a = (1usize << k) - 1;
308 let b = n + 1 - (1usize << k);
309 (a, b)
310 }
311}
312
313pub fn bsgs_consumed_bits(
357 degree: usize,
358 strategy: SplitStrategy,
359 _parity: Parity,
360 _basis: Basis,
361 input_log_delta: usize,
362 coeff_log_delta: usize,
363) -> usize {
364 if degree == 0 {
365 return 0;
366 }
367 let eval_depth = bsgs_eval_depth(degree, strategy);
368 let pure_depth = bit_len(degree) - degree.is_power_of_two() as usize;
369 let ctpt_depth = if matches!(strategy, SplitStrategy::MinMult) && degree == 5 {
370 1
371 } else {
372 eval_depth - 1
373 };
374 (pure_depth * input_log_delta).max(ctpt_depth * input_log_delta + coeff_log_delta)
375}
376
377#[cfg(test)]
382fn bsgs_consumed_bits_reference(
383 degree: usize,
384 strategy: SplitStrategy,
385 parity: Parity,
386 basis: Basis,
387 input_log_delta: usize,
388 coeff_log_delta: usize,
389) -> usize {
390 if degree == 0 {
391 return 0;
392 }
393 let log_split = split_for_strategy(strategy, degree, parity, basis);
394 let split_leading = matches!(strategy, SplitStrategy::MinDepth);
397 let mut baby_degrees: Vec<usize> = Vec::new();
398 collect_baby_step_degrees(degree, log_split, degree, true, split_leading, &mut baby_degrees);
399
400 let n_baby = baby_degrees.len();
404 let trailing_const = n_baby >= 2 && baby_degrees[n_baby - 1] == 0;
405 let can_fold = trailing_const && degree.is_power_of_two();
406 let n_to_process = if can_fold { n_baby - 1 } else { n_baby };
407
408 let baby_weight = |d: usize| -> usize {
412 let highest = match parity {
413 Parity::Full => d,
414 Parity::Odd => d - (d + 1) % 2,
415 Parity::Even => d - d % 2,
416 };
417 if highest == 0 {
418 0
419 } else {
420 power_basis_depth(highest) * input_log_delta + coeff_log_delta
421 }
422 };
423
424 let mut active: Vec<(usize, usize)> = baby_degrees[..n_to_process].iter().map(|&d| (d, baby_weight(d))).collect();
425
426 while active.len() > 1 {
429 let mut next: Vec<(usize, usize)> = Vec::with_capacity(active.len().div_ceil(2));
430 let mut i = 0;
431 while i < active.len() {
432 let is_last = i + 1 == active.len();
433 if !is_last && active[i].0 == active[i + 1].0 {
434 let gsp = (active[i].0 + 1).next_power_of_two();
435 let x_pow = power_basis_depth(gsp) * input_log_delta;
436 let combined = (x_pow.max(active[i + 1].1) + input_log_delta).max(active[i].1);
438 next.push((2 * gsp - 1, combined));
439 i += 2;
440 } else if is_last && i > 0 {
441 let degree_carry = next.last().map_or(active[i].0, |&(d, _)| d);
442 next.push((degree_carry, active[i].1));
443 i += 1;
444 } else {
445 next.push(active[i]);
446 i += 1;
447 }
448 }
449 active = next;
450 }
451
452 let mut consumed = active[0].1;
453 if can_fold {
454 consumed = consumed.max(power_basis_depth(degree) * input_log_delta + coeff_log_delta);
456 }
457 consumed
458}
459
460#[cfg(test)]
464fn collect_baby_step_degrees(
465 degree: usize,
466 log_split: usize,
467 max_degree: usize,
468 lead: bool,
469 split_leading: bool,
470 out: &mut Vec<usize>,
471) {
472 let base = 1usize << log_split;
473 if degree < base {
474 if split_leading && should_split_leading_baby_step(degree, log_split, max_degree, lead) {
475 let smaller = min_depth_split(bit_len(degree));
476 if smaller < log_split {
477 collect_baby_step_degrees(degree, smaller, max_degree, lead, split_leading, out);
478 return;
479 }
480 }
481 out.push(degree);
482 return;
483 }
484 let mut next_power = base;
485 while next_power < (degree >> 1) + 1 {
486 next_power <<= 1;
487 }
488 collect_baby_step_degrees(next_power - 1, log_split, max_degree, false, split_leading, out);
489 collect_baby_step_degrees(degree - next_power, log_split, max_degree, lead, split_leading, out);
490}
491
492#[cfg(test)]
496fn power_basis_depth(i: usize) -> usize {
497 if i <= 1 {
498 0
499 } else {
500 let (a, b) = split_degree(i);
501 power_basis_depth(a).max(power_basis_depth(b)) + 1
502 }
503}
504
505pub fn bsgs_op_counts(degree: usize, strategy: SplitStrategy, parity: Parity, basis: Basis) -> (usize, usize) {
507 if degree == 0 {
508 return (0, 0);
509 }
510 estimate_op_counts(degree, split_for_strategy(strategy, degree, parity, basis), parity, basis)
511}
512
513pub fn bsgs_eval_depth(degree: usize, strategy: SplitStrategy) -> usize {
514 if degree == 0 {
515 return 0;
516 }
517 let b = bit_len(degree);
518 match strategy {
519 SplitStrategy::MinDepth => b,
520 SplitStrategy::MinMult => {
521 let threshold = (1usize << b) - (1usize << ((b - 1) / 2)) + 1;
522 if degree >= threshold { b + 1 } else { b }
523 }
524 }
525}
526
527pub fn change_of_basis<F: Float>(basis: Basis, a: F, b: F) -> (F, F) {
541 match basis {
542 Basis::Monomial => (F::one(), F::zero()),
543 Basis::Chebyshev => {
544 let two = F::one() + F::one();
545 let span = b - a;
546 (two / span, -(a + b) / span)
547 }
548 }
549}
550
551pub struct Polynomial<F> {
556 pub basis: Basis,
557 pub coeffs: Vec<F>,
558 pub parity: Parity,
559 pub a: F,
561 pub b: F,
570}
571
572impl<F> Polynomial<F>
573where
574 F: Float + FloatConst + FromPrimitive + Debug,
575{
576 pub fn new(basis: Basis, coeffs: Vec<F>) -> Self {
578 let parity = if coeffs.iter().enumerate().all(|(i, &c)| i.is_multiple_of(2) || c == F::zero()) {
579 Parity::Even
580 } else if coeffs
581 .iter()
582 .enumerate()
583 .all(|(i, &c)| !i.is_multiple_of(2) || c == F::zero())
584 {
585 Parity::Odd
586 } else {
587 Parity::Full
588 };
589 Self::new_with_parity(basis, coeffs, parity)
590 }
591
592 pub fn new_with_parity(basis: Basis, coeffs: Vec<F>, parity: Parity) -> Self {
593 let one = F::one();
594 Self {
595 basis,
596 coeffs,
597 parity,
598 a: -one,
599 b: one,
600 }
601 }
602
603 pub fn with_interval(mut self, a: F, b: F) -> Self {
607 self.a = a;
608 self.b = b;
609 self
610 }
611
612 pub fn interval(&self) -> (F, F) {
614 (self.a, self.b)
615 }
616
617 pub fn change_of_basis(&self) -> (F, F) {
621 change_of_basis(self.basis, self.a, self.b)
622 }
623
624 pub fn chebyshev_interpolate<Fun>(degree: usize, a: F, b: F, f: Fun) -> Result<Self>
625 where
626 Fun: Fn(F) -> F,
627 {
628 chebyshev_interpolate(degree, a, b, f)
629 }
630
631 pub fn degree(&self) -> usize {
632 self.coeffs.len().saturating_sub(1)
633 }
634
635 pub fn evaluate(&self, x: F) -> F {
640 evaluate_coeffs(self.basis, &self.coeffs, x)
641 }
642
643 pub fn evaluate_on_interval(&self, x: F) -> F {
647 let (u, w) = self.change_of_basis();
648 self.evaluate(u * x + w)
649 }
650
651 pub fn fold_parity(&self) -> Result<(Self, PolynomialInputTransform)> {
658 let (start, transform) = match (self.basis, self.parity) {
659 (Basis::Monomial, Parity::Even) => (0, PolynomialInputTransform::Square),
660 (Basis::Monomial, Parity::Odd) => (1, PolynomialInputTransform::SquareTimesInput),
661 (Basis::Chebyshev, Parity::Even) => (0, PolynomialInputTransform::ChebyshevT2),
662 (Basis::Chebyshev, Parity::Odd) => (1, PolynomialInputTransform::ChebyshevT2TimesInput),
663 (_, Parity::Full) => return Err(anyhow!("parity folding requires even or odd parity")),
664 };
665 let minimum_degree = if self.parity == Parity::Even { 2 } else { 3 };
666 ensure!(
667 self.degree() >= minimum_degree,
668 "parity folding a {:?} polynomial requires degree ≥ {minimum_degree}",
669 self.parity
670 );
671 ensure!(
672 self.coeffs
673 .iter()
674 .enumerate()
675 .all(|(i, &coefficient)| { i.is_multiple_of(2) == (self.parity == Parity::Even) || coefficient == F::zero() }),
676 "parity folding requires coefficients consistent with {:?} parity",
677 self.parity
678 );
679
680 if self.basis == Basis::Monomial || self.parity == Parity::Even {
681 let coeffs = self.coeffs.iter().skip(start).step_by(2).copied().collect();
682 return Ok((Self::new_with_parity(self.basis, coeffs, Parity::Full), transform));
683 }
684
685 match self.parity {
686 Parity::Odd => {
687 let odd_coeffs: Vec<F> = self.coeffs.iter().skip(1).step_by(2).copied().collect();
692 let mut q = vec![F::zero(); odd_coeffs.len()];
693 let mut r_prev = Vec::new();
694 let mut r = vec![F::one()];
695 let two = F::one() + F::one();
696
697 for (k, &coefficient) in odd_coeffs.iter().enumerate() {
698 for (i, &value) in r.iter().enumerate() {
699 q[i] = q[i] + coefficient * value;
700 }
701 if k + 1 == odd_coeffs.len() {
702 break;
703 }
704
705 let mut next = if k == 0 {
706 vec![-F::one(), two]
707 } else {
708 let mut next = vec![F::zero(); r.len() + 1];
709 for (i, &value) in r.iter().enumerate() {
710 if i == 0 {
711 next[1] = next[1] + two * value;
712 } else {
713 next[i - 1] = next[i - 1] + value;
714 next[i + 1] = next[i + 1] + value;
715 }
716 }
717 for (i, &value) in r_prev.iter().enumerate() {
718 next[i] = next[i] - value;
719 }
720 next
721 };
722 std::mem::swap(&mut r_prev, &mut r);
723 std::mem::swap(&mut r, &mut next);
724 }
725
726 Ok((Self::new_with_parity(Basis::Chebyshev, q, Parity::Full), transform))
727 }
728 _ => unreachable!("monomial and even folds return above"),
729 }
730 }
731
732 pub fn decompose_bsgs_with<C>(
735 &self,
736 split_strategy: SplitStrategy,
737 mut encode: impl FnMut(&[F]) -> Result<C>,
738 ) -> Result<BSGSPolynomial<C>> {
739 ensure!(self.degree() >= 1, "polynomial must have degree ≥ 1");
740
741 let degree = self.degree();
742 let log_split = split_for_strategy(split_strategy, degree, self.parity, self.basis);
743 let base = 1usize << log_split;
744 let split_leading = matches!(split_strategy, SplitStrategy::MinDepth);
745
746 let mut baby_steps = Vec::new();
747 decompose_bsgs_coeffs(
748 self.basis,
749 &self.coeffs,
750 log_split,
751 degree,
752 true,
753 split_leading,
754 &mut |baby_coeffs| {
755 baby_steps.push(encode(baby_coeffs)?);
756 Ok(())
757 },
758 )?;
759
760 Ok(BSGSPolynomial {
761 basis: self.basis,
762 degree,
763 base,
764 baby_steps,
765 parity: self.parity,
766 split_strategy,
767 input_transform: PolynomialInputTransform::Identity,
768 a: self.a.to_f64().expect("interval lower bound must convert to f64"),
769 b: self.b.to_f64().expect("interval upper bound must convert to f64"),
770 })
771 }
772
773 pub fn decompose_bsgs_folded_with<C>(
781 &self,
782 split_strategy: SplitStrategy,
783 encode: impl FnMut(&[F]) -> Result<C>,
784 ) -> Result<BSGSPolynomial<C>> {
785 let (folded, input_transform) = self.fold_parity()?;
786 let mut bsgs = folded.decompose_bsgs_with(split_strategy, encode)?;
787 bsgs.input_transform = input_transform;
788 bsgs.a = self.a.to_f64().expect("interval lower bound must convert to f64");
791 bsgs.b = self.b.to_f64().expect("interval upper bound must convert to f64");
792 Ok(bsgs)
793 }
794}
795
796pub fn evaluate_coeffs<F>(basis: Basis, coeffs: &[F], x: F) -> F
803where
804 F: Float,
805{
806 match basis {
807 Basis::Monomial => {
808 let mut y = F::zero();
809 for &c in coeffs.iter().rev() {
810 y = y * x + c;
811 }
812 y
813 }
814 Basis::Chebyshev => {
815 let n = coeffs.len();
816 if n == 0 {
817 return F::zero();
818 }
819 if n == 1 {
820 return coeffs[0];
821 }
822 let two = F::one() + F::one();
823 let mut b2 = F::zero();
824 let mut b1 = F::zero();
825 for i in (1..n).rev() {
826 let tmp = two * x * b1 - b2 + coeffs[i];
827 b2 = b1;
828 b1 = tmp;
829 }
830 coeffs[0] + x * b1 - b2
831 }
832 }
833}
834
835fn chebyshev_interpolate<F, Fun>(degree: usize, a: F, b: F, f: Fun) -> Result<Polynomial<F>>
843where
844 F: Float + FloatConst + FromPrimitive + Debug,
845 Fun: Fn(F) -> F,
846{
847 ensure!(a < b, "chebyshev_interpolate: expected a < b");
848
849 let n = degree + 1;
850 let two = F::one() + F::one();
851 let half = F::from_f64(0.5).expect("0.5 must be representable");
852 let center = (a + b) * half;
853 let radius = (b - a) * half;
854 let pi_over_n = F::PI() / F::from_usize(n).expect("n must fit in scalar");
855
856 let mut coeffs = vec![F::zero(); n];
857 for k in (1..=n).rev() {
858 let theta = (F::from_usize(k).expect("k must fit in scalar") - half) * pi_over_n;
859 let u = theta.cos();
860 let val = f(center + radius * u);
861 let mut t_prev = F::one();
862 let mut t = u;
863 for coeff in coeffs.iter_mut() {
864 *coeff = *coeff + val * t_prev;
865 let t_next = two * u * t - t_prev;
866 t_prev = t;
867 t = t_next;
868 }
869 }
870
871 let inv_n = F::one() / F::from_usize(n).expect("n must fit in scalar");
872 coeffs[0] = coeffs[0] * inv_n;
873 let two_over_n = two * inv_n;
874 for coeff in coeffs.iter_mut().skip(1) {
875 *coeff = *coeff * two_over_n;
876 }
877
878 Ok(Polynomial::new(Basis::Chebyshev, coeffs).with_interval(a, b))
879}
880
881pub struct BSGSPolynomial<C> {
890 basis: Basis,
891 degree: usize,
892 base: usize,
893 baby_steps: Vec<C>,
894 parity: Parity,
895 split_strategy: SplitStrategy,
896 input_transform: PolynomialInputTransform,
897 a: f64,
901 b: f64,
902}
903
904impl<BE: Backend, C> BSGSPolynomialInfos<BE> for BSGSPolynomial<C>
905where
906 C: GLWEToBackendRef<BE> + GLWEInfos + BSGSMeta + IntPolyInfos,
907{
908 type Coeffs = C;
909
910 fn degree(&self) -> usize {
911 BSGSPolynomial::degree(self)
912 }
913
914 fn baby_steps(&self) -> usize {
915 BSGSPolynomial::baby_steps(self).len()
916 }
917
918 fn baby_step(&self, i: usize) -> &Self::Coeffs {
919 BSGSPolynomial::baby_step(self, i)
920 }
921
922 fn basis(&self) -> Basis {
923 BSGSPolynomial::basis(self)
924 }
925
926 fn parity(&self) -> Parity {
927 BSGSPolynomial::parity(self)
928 }
929
930 fn log_split(&self) -> usize {
931 BSGSPolynomial::log_split(self)
932 }
933
934 fn split_strategy(&self) -> SplitStrategy {
935 self.split_strategy
936 }
937
938 fn input_transform(&self) -> PolynomialInputTransform {
939 self.input_transform
940 }
941}
942
943impl<C> BSGSPolynomial<C> {
944 pub fn basis(&self) -> Basis {
946 self.basis
947 }
948
949 pub fn degree(&self) -> usize {
954 self.degree
955 }
956
957 pub fn base(&self) -> usize {
959 self.base
960 }
961
962 pub fn log_split(&self) -> usize {
964 self.base.trailing_zeros() as usize
965 }
966
967 pub fn eval_depth(&self) -> usize {
969 bsgs_eval_depth(self.degree(), self.split_strategy) + self.input_transform.extra_depth()
970 }
971
972 pub fn consumed_bits(&self, input_log_delta: usize, coeff_log_delta: usize) -> usize {
993 bsgs_consumed_bits(
994 self.degree,
995 self.split_strategy,
996 self.parity,
997 self.basis,
998 input_log_delta,
999 coeff_log_delta,
1000 ) + self.input_transform.extra_depth() * input_log_delta
1001 }
1002
1003 pub fn baby_steps(&self) -> &[C] {
1005 &self.baby_steps
1006 }
1007
1008 pub fn baby_step(&self, i: usize) -> &C {
1012 &self.baby_steps[i]
1013 }
1014
1015 pub fn parity(&self) -> Parity {
1017 self.parity
1018 }
1019
1020 pub fn input_transform(&self) -> PolynomialInputTransform {
1022 self.input_transform
1023 }
1024
1025 pub fn interval(&self) -> (f64, f64) {
1027 (self.a, self.b)
1028 }
1029
1030 pub fn change_of_basis(&self) -> (f64, f64) {
1033 change_of_basis(self.basis, self.a, self.b)
1034 }
1035
1036 pub fn map_baby_steps_ref<D>(&self, mut f: impl FnMut(&C) -> D) -> BSGSPolynomial<D> {
1038 BSGSPolynomial {
1039 basis: self.basis,
1040 degree: self.degree,
1041 base: self.base,
1042 baby_steps: self.baby_steps.iter().map(&mut f).collect(),
1043 parity: self.parity,
1044 split_strategy: self.split_strategy,
1045 input_transform: self.input_transform,
1046 a: self.a,
1047 b: self.b,
1048 }
1049 }
1050}
1051
1052pub trait BSGSMeta {
1059 fn bsgs_log_budget(&self) -> usize;
1060 fn bsgs_log_delta(&self) -> usize;
1061}
1062
1063pub trait SetBSGSMeta: BSGSMeta {
1065 fn set_bsgs_log_budget(&mut self, log_budget: usize);
1066 fn set_bsgs_log_delta(&mut self, log_delta: usize);
1067}
1068
1069pub trait BSGSPolynomialInfos<BE: Backend> {
1071 type Coeffs: GLWEToBackendRef<BE> + IntPolyInfos + GLWEInfos + BSGSMeta;
1072 fn degree(&self) -> usize;
1073 fn baby_steps(&self) -> usize;
1074 fn baby_step(&self, i: usize) -> &Self::Coeffs;
1075 fn basis(&self) -> Basis;
1076 fn parity(&self) -> Parity;
1077 fn log_split(&self) -> usize;
1078 fn split_strategy(&self) -> SplitStrategy;
1079 fn input_transform(&self) -> PolynomialInputTransform {
1080 PolynomialInputTransform::Identity
1081 }
1082}
1083
1084pub trait BabyStep<BE: Backend> {
1086 type Value: GLWEToBackendMut<BE> + GLWEToBackendRef<BE> + GLWEInfos + SetBSGSMeta;
1087 fn degree(&self) -> usize;
1088 fn get(&self) -> &Self::Value;
1089 fn get_mut(&mut self) -> &mut Self::Value;
1090}
1091
1092pub trait PowerBasisHelper<BE: Backend, A> {
1096 fn basis(&self) -> Basis;
1097 fn has_power(&self, power: usize) -> bool;
1098 fn get(&self, power: usize) -> Result<&A>;
1099}
1100
1101pub struct PowerBasis<A> {
1106 pub(crate) basis: Basis,
1107 pub(crate) values: HashMap<usize, A>,
1108}
1109
1110impl<A> PowerBasis<A> {
1111 pub fn new(basis: Basis, x: A) -> Self {
1113 let mut values = HashMap::new();
1114 values.insert(1, x);
1115 Self { basis, values }
1116 }
1117
1118 pub fn basis(&self) -> Basis {
1120 self.basis
1121 }
1122
1123 pub fn get_stored(&self, n: usize) -> Option<&A> {
1125 self.values.get(&n)
1126 }
1127
1128 pub fn contains_power(&self, n: usize) -> bool {
1130 self.values.contains_key(&n)
1131 }
1132
1133 pub fn set_power(&mut self, n: usize, value: A) {
1135 self.values.insert(n, value);
1136 }
1137
1138 pub fn take_power(&mut self, n: usize) -> Option<A> {
1140 self.values.remove(&n)
1141 }
1142}
1143
1144impl<BE: Backend, A> PowerBasisHelper<BE, A> for PowerBasis<A>
1145where
1146 A: GLWEToBackendRef<BE>,
1147{
1148 fn basis(&self) -> Basis {
1149 self.basis
1150 }
1151
1152 fn has_power(&self, power: usize) -> bool {
1153 self.values.contains_key(&power)
1154 }
1155
1156 fn get(&self, power: usize) -> Result<&A> {
1157 self.values
1158 .get(&power)
1159 .ok_or_else(|| anyhow!("PowerBasis: X^{power} not computed; call gen_power or populate first"))
1160 }
1161}
1162
1163#[cfg(test)]
1164mod tests {
1165 use super::*;
1166
1167 fn collect_baby_step_degrees(basis: Basis, degree: usize, log_split: usize, split_leading: bool) -> Vec<usize> {
1168 let coeffs = vec![0.0f64; degree + 1];
1169 let mut degrees = Vec::new();
1170 decompose_bsgs_coeffs(basis, &coeffs, log_split, degree, true, split_leading, &mut |s| {
1171 degrees.push(s.len() - 1);
1172 Ok(())
1173 })
1174 .unwrap();
1175 degrees
1176 }
1177
1178 #[test]
1183 fn consumed_bits_closed_form_matches_reference_full_parity() {
1184 let deltas = [(6, 3), (3, 6), (1, 1), (10, 1), (1, 10), (5, 5), (7, 2), (0, 5), (5, 0)];
1185 for strategy in [SplitStrategy::MinDepth, SplitStrategy::MinMult] {
1186 for basis in [Basis::Monomial, Basis::Chebyshev] {
1187 for degree in 2..=511usize {
1188 for &(din, dco) in &deltas {
1189 let got = bsgs_consumed_bits(degree, strategy, Parity::Full, basis, din, dco);
1190 let want = bsgs_consumed_bits_reference(degree, strategy, Parity::Full, basis, din, dco);
1191 assert_eq!(got, want, "degree {degree} {strategy:?} {basis:?} din={din} dco={dco}");
1192 }
1193 }
1194 }
1195 }
1196 }
1197
1198 #[test]
1201 fn consumed_bits_closed_form_matches_reference_odd_parity() {
1202 let deltas = [(6, 3), (3, 6), (1, 1), (10, 1), (1, 10)];
1203 for strategy in [SplitStrategy::MinDepth, SplitStrategy::MinMult] {
1204 for basis in [Basis::Monomial, Basis::Chebyshev] {
1205 for degree in (3..=511usize).step_by(2) {
1206 for &(din, dco) in &deltas {
1207 let got = bsgs_consumed_bits(degree, strategy, Parity::Odd, basis, din, dco);
1208 let want = bsgs_consumed_bits_reference(degree, strategy, Parity::Odd, basis, din, dco);
1209 assert_eq!(got, want, "degree {degree} {strategy:?} {basis:?} din={din} dco={dco}");
1210 }
1211 }
1212 }
1213 }
1214 }
1215
1216 #[test]
1217 fn change_of_basis_maps_interval_endpoints() {
1218 assert_eq!(change_of_basis(Basis::Monomial, -3.0_f64, 7.0), (1.0, 0.0));
1220
1221 for &(a, b) in &[(-1.0_f64, 1.0), (0.0, 2.0), (-8.0, 8.0), (3.0, 11.0)] {
1223 let (u, w) = change_of_basis(Basis::Chebyshev, a, b);
1224 assert!((u * a + w + 1.0).abs() < 1e-12, "a→-1 failed for [{a},{b}]");
1225 assert!((u * b + w - 1.0).abs() < 1e-12, "b→+1 failed for [{a},{b}]");
1226 }
1227
1228 assert_eq!(change_of_basis(Basis::Chebyshev, -1.0_f64, 1.0), (1.0, 0.0));
1230
1231 let poly = Polynomial::chebyshev_interpolate(8, 0.0_f64, 4.0, |x| x).unwrap();
1234 assert_eq!(poly.interval(), (0.0, 4.0));
1235 let (u, w) = poly.change_of_basis();
1236 assert_eq!((u, w), change_of_basis(Basis::Chebyshev, 0.0, 4.0));
1237 let bsgs = poly
1238 .decompose_bsgs_with(SplitStrategy::MinDepth, |c| Ok::<_, anyhow::Error>(c.to_vec()))
1239 .unwrap();
1240 assert_eq!(bsgs.interval(), (0.0, 4.0));
1241 assert_eq!(bsgs.change_of_basis(), (u, w));
1242 }
1243
1244 #[test]
1245 fn even_chebyshev_t2_fold_matches_source() {
1246 let poly = Polynomial::new_with_parity(
1247 Basis::Chebyshev,
1248 vec![1.0_f64, 0.0, -0.5, 0.0, 0.25, 0.0, 0.125],
1249 Parity::Even,
1250 );
1251 let (folded, transform) = poly.fold_parity().unwrap();
1252
1253 assert_eq!(transform, PolynomialInputTransform::ChebyshevT2);
1254 assert_eq!(folded.coeffs, vec![1.0, -0.5, 0.25, 0.125]);
1255 for i in 0..=64 {
1256 let x = -1.0 + 2.0 * i as f64 / 64.0;
1257 assert!((poly.evaluate(x) - folded.evaluate(2.0 * x * x - 1.0)).abs() < 1e-12);
1258 }
1259 }
1260
1261 #[test]
1262 fn odd_chebyshev_t2_fold_matches_source() {
1263 let poly = Polynomial::new_with_parity(
1264 Basis::Chebyshev,
1265 vec![0.0_f64, 1.0, 0.0, -0.5, 0.0, 0.25, 0.0, -0.125],
1266 Parity::Odd,
1267 );
1268 let (folded, transform) = poly.fold_parity().unwrap();
1269
1270 assert_eq!(transform, PolynomialInputTransform::ChebyshevT2TimesInput);
1271 for i in 0..=64 {
1272 let x = -1.0 + 2.0 * i as f64 / 64.0;
1273 assert!((poly.evaluate(x) - x * folded.evaluate(2.0 * x * x - 1.0)).abs() < 1e-12);
1274 }
1275 }
1276
1277 #[test]
1278 fn parity_fold_bsgs_preserves_source_interval_and_accounts_for_transform() {
1279 let even = Polynomial::new_with_parity(Basis::Chebyshev, vec![1.0_f64, 0.0, 2.0, 0.0, 3.0, 0.0, 4.0], Parity::Even)
1280 .with_interval(-3.0, 5.0);
1281 let even_bsgs = even
1282 .decompose_bsgs_folded_with(SplitStrategy::MinDepth, |c| Ok::<_, anyhow::Error>(c.to_vec()))
1283 .unwrap();
1284 assert_eq!(even_bsgs.input_transform(), PolynomialInputTransform::ChebyshevT2);
1285 assert_eq!(even_bsgs.degree(), 3);
1286 assert_eq!(even_bsgs.parity(), Parity::Full);
1287 assert_eq!(even_bsgs.interval(), (-3.0, 5.0));
1288 assert_eq!(even_bsgs.eval_depth(), bsgs_eval_depth(3, SplitStrategy::MinDepth) + 1);
1289
1290 let odd = Polynomial::new_with_parity(
1291 Basis::Chebyshev,
1292 vec![0.0_f64, 1.0, 0.0, 2.0, 0.0, 3.0, 0.0, 4.0],
1293 Parity::Odd,
1294 );
1295 let odd_bsgs = odd
1296 .decompose_bsgs_folded_with(SplitStrategy::MinDepth, |c| Ok::<_, anyhow::Error>(c.to_vec()))
1297 .unwrap();
1298 assert_eq!(odd_bsgs.input_transform(), PolynomialInputTransform::ChebyshevT2TimesInput);
1299 assert_eq!(odd_bsgs.degree(), 3);
1300 assert_eq!(odd_bsgs.eval_depth(), bsgs_eval_depth(3, SplitStrategy::MinDepth) + 2);
1301 assert_eq!(
1302 odd_bsgs.consumed_bits(5, 3),
1303 bsgs_consumed_bits(3, SplitStrategy::MinDepth, Parity::Full, Basis::Chebyshev, 5, 3) + 10
1304 );
1305 }
1306
1307 #[test]
1308 fn monomial_parity_folds_match_source() {
1309 let even = Polynomial::new_with_parity(Basis::Monomial, vec![1.0_f64, 0.0, -0.5, 0.0, 0.25, 0.0, 0.125], Parity::Even);
1310 let odd = Polynomial::new_with_parity(
1311 Basis::Monomial,
1312 vec![0.0_f64, 1.0, 0.0, -0.5, 0.0, 0.25, 0.0, -0.125],
1313 Parity::Odd,
1314 );
1315 let (even_folded, even_transform) = even.fold_parity().unwrap();
1316 let (odd_folded, odd_transform) = odd.fold_parity().unwrap();
1317
1318 assert_eq!(even_transform, PolynomialInputTransform::Square);
1319 assert_eq!(odd_transform, PolynomialInputTransform::SquareTimesInput);
1320 for i in 0..=64 {
1321 let x = -1.0 + 2.0 * i as f64 / 64.0;
1322 assert!((even.evaluate(x) - even_folded.evaluate(x * x)).abs() < 1e-12);
1323 assert!((odd.evaluate(x) - x * odd_folded.evaluate(x * x)).abs() < 1e-12);
1324 }
1325 }
1326
1327 #[test]
1328 fn power_basis_can_transfer_ownership_of_a_power() {
1329 let mut powers = PowerBasis::new(Basis::Monomial, 1_u32);
1330 powers.set_power(2, 4);
1331 assert_eq!(powers.take_power(2), Some(4));
1332 assert!(!powers.contains_power(2));
1333 }
1334
1335 #[test]
1336 fn min_mult_chebyshev_degree31_uniform_baby_steps() {
1337 let log_split = min_mult_split(31, Parity::Full, Basis::Chebyshev);
1338 assert_eq!(log_split, 3);
1339 assert_eq!(
1340 collect_baby_step_degrees(Basis::Chebyshev, 31, log_split, false),
1341 vec![7, 7, 7, 7]
1342 );
1343 }
1344
1345 #[test]
1346 fn min_depth_chebyshev_degree31_splits_leading_baby_step() {
1347 let log_split = min_depth_split(bit_len(31));
1348 assert_eq!(log_split, 3);
1349 assert_eq!(
1350 collect_baby_step_degrees(Basis::Chebyshev, 31, log_split, true),
1351 vec![7, 7, 7, 3, 1, 1]
1352 );
1353 }
1354
1355 #[test]
1356 fn bsgs_eval_depth_matches_closed_form() {
1357 assert_eq!(bsgs_eval_depth(0, SplitStrategy::MinDepth), 0);
1358 assert_eq!(bsgs_eval_depth(0, SplitStrategy::MinMult), 0);
1359 for d in 1..1024 {
1363 let k = bit_len(d);
1364 let min_mult = if (1usize << k) - d < (1usize << ((k - 1) / 2)) {
1365 k + 1
1366 } else {
1367 k
1368 };
1369 assert_eq!(bsgs_eval_depth(d, SplitStrategy::MinDepth), k, "MinDepth degree {d}");
1370 assert_eq!(bsgs_eval_depth(d, SplitStrategy::MinMult), min_mult, "MinMult degree {d}");
1371 }
1372 }
1373
1374 #[test]
1375 fn power_basis_estimate_caps_baby_steps_at_degree() {
1376 assert_eq!(estimate_power_basis_muls(5, 3, Parity::Full, Basis::Monomial), 4);
1377 assert_eq!(estimate_power_basis_muls(5, 3, Parity::Even, Basis::Monomial), 2);
1378 }
1379}