Skip to main content

snarkvm_algorithms/fft/
domain.rs

1// Copyright (c) 2019-2026 Provable Inc.
2// This file is part of the snarkVM library.
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at:
7
8// http://www.apache.org/licenses/LICENSE-2.0
9
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! This module contains an `EvaluationDomain` abstraction for
17//! performing various kinds of polynomial arithmetic on top of
18//! the scalar field.
19//!
20//! In pairing-based SNARKs like GM17, we need to calculate
21//! a quotient polynomial over a target polynomial with roots
22//! at distinct points associated with each constraint of the
23//! constraint system. In order to be efficient, we choose these
24//! roots to be the powers of a 2^n root of unity in the field.
25//! This allows us to perform polynomial operations in O(n)
26//! by performing an O(n log n) FFT over such a domain.
27
28use crate::{
29    cfg_chunks_mut,
30    cfg_into_iter,
31    cfg_iter,
32    cfg_iter_mut,
33    fft::{DomainCoeff, SparsePolynomial},
34};
35use snarkvm_fields::{FftField, FftParameters, Field, batch_inversion};
36#[cfg(not(feature = "serial"))]
37use snarkvm_utilities::max_available_threads;
38use snarkvm_utilities::{execute_with_max_available_threads, serialize::*};
39
40use rand::RngExt;
41use std::{borrow::Cow, fmt};
42
43use anyhow::{Result, ensure};
44
45#[cfg(feature = "serial")]
46use itertools::Itertools;
47#[cfg(not(feature = "serial"))]
48use rayon::prelude::*;
49
50/// Returns the ceiling of the base-2 logarithm of `x`.
51///
52/// ```
53/// use snarkvm_algorithms::fft::domain::log2;
54///
55/// assert_eq!(log2(16), 4);
56/// assert_eq!(log2(17), 5);
57/// assert_eq!(log2(1), 0);
58/// assert_eq!(log2(0), 0);
59/// assert_eq!(log2(usize::MAX), (core::mem::size_of::<usize>() * 8) as u32);
60/// assert_eq!(log2(1 << 15), 15);
61/// assert_eq!(log2(2usize.pow(18)), 18);
62/// ```
63pub fn log2(x: usize) -> u32 {
64    if x == 0 {
65        0
66    } else if x.is_power_of_two() {
67        1usize.leading_zeros() - x.leading_zeros()
68    } else {
69        0usize.leading_zeros() - x.leading_zeros()
70    }
71}
72
73// minimum size of a parallelized chunk
74#[allow(unused)]
75#[cfg(not(feature = "serial"))]
76const MIN_PARALLEL_CHUNK_SIZE: usize = 1 << 7;
77
78/// Defines a domain over which finite field (I)FFTs can be performed. Works
79/// only for fields that have a large multiplicative subgroup of size that is
80/// a power-of-2.
81#[derive(Copy, Clone, Hash, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)]
82pub struct EvaluationDomain<F: FftField> {
83    /// The size of the domain.
84    pub size: u64,
85    /// `log_2(self.size)`.
86    pub log_size_of_group: u32,
87    /// Size of the domain as a field element.
88    pub size_as_field_element: F,
89    /// Inverse of the size in the field.
90    pub size_inv: F,
91    /// A generator of the subgroup.
92    pub group_gen: F,
93    /// Inverse of the generator of the subgroup.
94    pub group_gen_inv: F,
95    /// Inverse of the multiplicative generator of the finite field.
96    pub generator_inv: F,
97}
98
99impl<F: FftField> fmt::Debug for EvaluationDomain<F> {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        write!(f, "Multiplicative subgroup of size {}", self.size)
102    }
103}
104
105impl<F: FftField> EvaluationDomain<F> {
106    /// Sample an element that is *not* in the domain.
107    pub fn sample_element_outside_domain<R: RngExt>(&self, rng: &mut R) -> F {
108        let mut t = F::rand(rng);
109        while self.evaluate_vanishing_polynomial(t).is_zero() {
110            t = F::rand(rng);
111        }
112        t
113    }
114
115    /// Construct a domain that is large enough for evaluations of a polynomial
116    /// having `num_coeffs` coefficients.
117    pub fn new(num_coeffs: usize) -> Option<Self> {
118        // Compute the size of our evaluation domain
119        let size = num_coeffs.checked_next_power_of_two()? as u64;
120        let log_size_of_group = size.trailing_zeros();
121
122        // libfqfft uses > https://github.com/scipr-lab/libfqfft/blob/e0183b2cef7d4c5deb21a6eaf3fe3b586d738fe0/libfqfft/evaluation_domain/domains/basic_radix2_domain.tcc#L33
123        if log_size_of_group > F::FftParameters::TWO_ADICITY {
124            return None;
125        }
126
127        // Compute the generator for the multiplicative subgroup.
128        // It should be the 2^(log_size_of_group) root of unity.
129        let group_gen = F::get_root_of_unity(size as usize)?;
130
131        // Check that it is indeed the 2^(log_size_of_group) root of unity.
132        debug_assert_eq!(group_gen.pow([size]), F::one());
133
134        let size_as_field_element = F::from(size);
135        let size_inv = size_as_field_element.inverse()?;
136
137        Some(EvaluationDomain {
138            size,
139            log_size_of_group,
140            size_as_field_element,
141            size_inv,
142            group_gen,
143            group_gen_inv: group_gen.inverse()?,
144            generator_inv: F::multiplicative_generator().inverse()?,
145        })
146    }
147
148    /// Return the size of a domain that is large enough for evaluations of a
149    /// polynomial having `num_coeffs` coefficients.
150    pub fn compute_size_of_domain(num_coeffs: usize) -> Option<usize> {
151        let size = num_coeffs.checked_next_power_of_two()?;
152        if size.trailing_zeros() <= F::FftParameters::TWO_ADICITY { Some(size) } else { None }
153    }
154
155    /// Return the size of `self`.
156    pub fn size(&self) -> usize {
157        self.size as usize
158    }
159
160    /// Compute an FFT.
161    pub fn fft<T: DomainCoeff<F>>(&self, coeffs: &[T]) -> Vec<T> {
162        let mut coeffs = coeffs.to_vec();
163        self.fft_in_place(&mut coeffs);
164        coeffs
165    }
166
167    /// Compute an FFT, modifying the vector in place.
168    pub fn fft_in_place<T: DomainCoeff<F>>(&self, coeffs: &mut Vec<T>) {
169        execute_with_max_available_threads(|| {
170            coeffs.resize(self.size(), T::zero());
171            self.in_order_fft_in_place(&mut *coeffs);
172        });
173    }
174
175    /// Compute an IFFT.
176    pub fn ifft<T: DomainCoeff<F>>(&self, evals: &[T]) -> Vec<T> {
177        let mut evals = evals.to_vec();
178        self.ifft_in_place(&mut evals);
179        evals
180    }
181
182    /// Compute an IFFT, modifying the vector in place.
183    #[inline]
184    pub fn ifft_in_place<T: DomainCoeff<F>>(&self, evals: &mut Vec<T>) {
185        execute_with_max_available_threads(|| {
186            evals.resize(self.size(), T::zero());
187            self.in_order_ifft_in_place(&mut *evals);
188        });
189    }
190
191    /// Compute an FFT over a coset of the domain.
192    pub fn coset_fft<T: DomainCoeff<F>>(&self, coeffs: &[T]) -> Vec<T> {
193        let mut coeffs = coeffs.to_vec();
194        self.coset_fft_in_place(&mut coeffs);
195        coeffs
196    }
197
198    /// Compute an FFT over a coset of the domain, modifying the input vector
199    /// in place.
200    pub fn coset_fft_in_place<T: DomainCoeff<F>>(&self, coeffs: &mut Vec<T>) {
201        execute_with_max_available_threads(|| {
202            Self::distribute_powers(coeffs, F::multiplicative_generator());
203            self.fft_in_place(coeffs);
204        });
205    }
206
207    /// Compute an IFFT over a coset of the domain.
208    pub fn coset_ifft<T: DomainCoeff<F>>(&self, evals: &[T]) -> Vec<T> {
209        let mut evals = evals.to_vec();
210        self.coset_ifft_in_place(&mut evals);
211        evals
212    }
213
214    /// Compute an IFFT over a coset of the domain, modifying the input vector
215    /// in place.
216    pub fn coset_ifft_in_place<T: DomainCoeff<F>>(&self, evals: &mut Vec<T>) {
217        execute_with_max_available_threads(|| {
218            evals.resize(self.size(), T::zero());
219            self.in_order_coset_ifft_in_place(&mut *evals);
220        });
221    }
222
223    /// Multiply the `i`-th element of `coeffs` with `g^i`.
224    fn distribute_powers<T: DomainCoeff<F>>(coeffs: &mut [T], g: F) {
225        Self::distribute_powers_and_mul_by_const(coeffs, g, F::one());
226    }
227
228    /// Multiply the `i`-th element of `coeffs` with `c*g^i`.
229    #[cfg(feature = "serial")]
230    fn distribute_powers_and_mul_by_const<T: DomainCoeff<F>>(coeffs: &mut [T], g: F, c: F) {
231        // invariant: pow = c*g^i at the ith iteration of the loop
232        let mut pow = c;
233        coeffs.iter_mut().for_each(|coeff| {
234            *coeff *= pow;
235            pow *= &g
236        })
237    }
238
239    /// Multiply the `i`-th element of `coeffs` with `c*g^i`.
240    #[cfg(not(feature = "serial"))]
241    fn distribute_powers_and_mul_by_const<T: DomainCoeff<F>>(coeffs: &mut [T], g: F, c: F) {
242        let min_parallel_chunk_size = 1024;
243        let num_cpus_available = max_available_threads();
244        let num_elem_per_thread = core::cmp::max(coeffs.len() / num_cpus_available, min_parallel_chunk_size);
245
246        cfg_chunks_mut!(coeffs, num_elem_per_thread).enumerate().for_each(|(i, chunk)| {
247            let offset = c * g.pow([(i * num_elem_per_thread) as u64]);
248            let mut pow = offset;
249            chunk.iter_mut().for_each(|coeff| {
250                *coeff *= pow;
251                pow *= &g
252            })
253        });
254    }
255
256    /// Evaluate all the lagrange polynomials defined by this domain at the
257    /// point `tau`.
258    pub fn evaluate_all_lagrange_coefficients(&self, tau: F) -> Vec<F> {
259        // Evaluate all Lagrange polynomials
260        let size = self.size as usize;
261        let t_size = tau.pow([self.size]);
262        let one = F::one();
263        if t_size.is_one() {
264            let mut u = vec![F::zero(); size];
265            let mut omega_i = one;
266            for x in u.iter_mut().take(size) {
267                if omega_i == tau {
268                    *x = one;
269                    break;
270                }
271                omega_i *= &self.group_gen;
272            }
273            u
274        } else {
275            let l0 = (t_size - one) * self.size_inv;
276            let mut u = vec![F::zero(); size];
277            let mut ls = vec![F::zero(); size];
278
279            // Fill `u[i] = tau - g^i` and `ls[i] = l0 * g^i`, where `g = group_gen`. Both
280            // sequences are powers of `g`, so a block can seed itself with `g^start`
281            // instead of walking there from zero, which is what lets the fill
282            // run in parallel.
283            //
284            // Same shape as `distribute_powers_and_mul_by_const` above.
285            #[cfg(not(feature = "serial"))]
286            let chunk = core::cmp::max(size / max_available_threads(), 1024);
287            #[cfg(feature = "serial")]
288            let chunk = core::cmp::max(size, 1);
289
290            // The chunks run unordered, so each seeds itself rather than carrying `r` from
291            // its predecessor. `chunk_gen^i` is the same seed as `g^(i*chunk)` from a
292            // ladder over `log2(chunks)` bits instead of 64.
293            let chunk_gen = self.group_gen.pow([chunk as u64]);
294            cfg_chunks_mut!(u, chunk).zip(cfg_chunks_mut!(ls, chunk)).enumerate().for_each(
295                |(i, (u_chunk, ls_chunk))| {
296                    let mut r = chunk_gen.pow([i as u64]);
297                    let mut l = l0 * r;
298                    u_chunk.iter_mut().zip(ls_chunk.iter_mut()).for_each(|(tau_minus_r, l_i)| {
299                        *tau_minus_r = tau - r;
300                        *l_i = l;
301                        r *= &self.group_gen;
302                        l *= &self.group_gen;
303                    });
304                },
305            );
306
307            batch_inversion(u.as_mut_slice());
308            cfg_iter_mut!(u, 1_000).zip_eq(ls).for_each(|(tau_minus_r, l)| {
309                *tau_minus_r = l * *tau_minus_r;
310            });
311            u
312        }
313    }
314
315    /// Return the sparse vanishing polynomial.
316    pub fn vanishing_polynomial(&self) -> SparsePolynomial<F> {
317        let coeffs = [(0, -F::one()), (self.size(), F::one())];
318        SparsePolynomial::from_coefficients(coeffs)
319    }
320
321    /// This evaluates the vanishing polynomial for this domain at tau.
322    /// For multiplicative subgroups, this polynomial is `z(X) = X^self.size -
323    /// 1`.
324    pub fn evaluate_vanishing_polynomial(&self, tau: F) -> F {
325        tau.pow([self.size]) - F::one()
326    }
327
328    /// Return an iterator over the elements of the domain.
329    pub fn elements(&self) -> Elements<F> {
330        Elements { cur_elem: F::one(), cur_pow: 0, domain: *self }
331    }
332
333    /// The target polynomial is the zero polynomial in our
334    /// evaluation domain, so we must perform division over
335    /// a coset.
336    pub fn divide_by_vanishing_poly_on_coset_in_place(&self, evals: &mut [F]) {
337        let i = self.evaluate_vanishing_polynomial(F::multiplicative_generator()).inverse().unwrap();
338
339        cfg_iter_mut!(evals).for_each(|eval| *eval *= &i);
340    }
341
342    /// Given an index in the `other` subdomain, return an index into this
343    /// domain `self` This assumes the `other`'s elements are also `self`'s
344    /// first elements
345    pub fn reindex_by_subdomain(&self, other: &Self, index: usize) -> Result<usize> {
346        ensure!(self.size() > other.size(), "other.size() must be smaller than self.size()");
347
348        // Let this subgroup be G, and the subgroup we're re-indexing by be S.
349        // Since its a subgroup, the 0th element of S is at index 0 in G, the first
350        // element of S is at index |G|/|S|, the second at 2*|G|/|S|, etc.
351        // Thus for an index i that corresponds to S, the index in G is i*|G|/|S|
352        let period = self.size() / other.size();
353        if index < other.size() {
354            Ok(index * period)
355        } else {
356            // Let i now be the index of this element in G \ S
357            // Let x be the number of elements in G \ S, for every element in S. Then x =
358            // (|G|/|S| - 1). At index i in G \ S, the number of elements in S
359            // that appear before the index in G to which i corresponds to, is
360            // floor(i / x) + 1. The +1 is because index 0 of G is S_0, so the
361            // position is offset by at least one. The floor(i / x) term is
362            // because after x elements in G \ S, there is one more element from S
363            // that will have appeared in G.
364            let i = index - other.size();
365            let x = period - 1;
366            Ok(i + (i / x) + 1)
367        }
368    }
369
370    /// Perform O(n) multiplication of two polynomials that are presented by
371    /// their evaluations in the domain.
372    /// Returns the evaluations of the product over the domain.
373    pub fn mul_polynomials_in_evaluation_domain(&self, self_evals: Vec<F>, other_evals: &[F]) -> Result<Vec<F>> {
374        let mut result = self_evals;
375
376        ensure!(result.len() == other_evals.len());
377        cfg_iter_mut!(result).zip_eq(other_evals).for_each(|(a, b)| *a *= b);
378
379        Ok(result)
380    }
381}
382
383impl<F: FftField> EvaluationDomain<F> {
384    pub fn precompute_fft(&self) -> FFTPrecomputation<F> {
385        execute_with_max_available_threads(|| FFTPrecomputation {
386            roots: self.roots_of_unity(self.group_gen),
387            domain: *self,
388        })
389    }
390
391    pub fn precompute_ifft(&self) -> IFFTPrecomputation<F> {
392        execute_with_max_available_threads(|| IFFTPrecomputation {
393            inverse_roots: self.roots_of_unity(self.group_gen_inv),
394            domain: *self,
395        })
396    }
397
398    pub(crate) fn in_order_fft_in_place<T: DomainCoeff<F>>(&self, x_s: &mut [T]) {
399        #[cfg(all(feature = "cuda", target_arch = "x86_64"))]
400        // SNP TODO: how to set threshold and check that the type is Fr
401        if self.size >= 32 && std::mem::size_of::<T>() == 32 {
402            let result = snarkvm_algorithms_cuda::NTT(
403                self.size as usize,
404                x_s,
405                snarkvm_algorithms_cuda::NTTInputOutputOrder::NN,
406                snarkvm_algorithms_cuda::NTTDirection::Forward,
407                snarkvm_algorithms_cuda::NTTType::Standard,
408            );
409            if result.is_ok() {
410                return;
411            }
412        }
413
414        let pc = self.precompute_fft();
415        self.fft_helper_in_place_with_pc(x_s, FFTOrder::II, &pc)
416    }
417
418    pub fn in_order_fft_with_pc<T: DomainCoeff<F>>(&self, x_s: &[T], pc: &FFTPrecomputation<F>) -> Vec<T> {
419        let mut x_s = x_s.to_vec();
420        if self.size() != x_s.len() {
421            x_s.extend(core::iter::repeat_n(T::zero(), self.size() - x_s.len()));
422        }
423        self.fft_helper_in_place_with_pc(&mut x_s, FFTOrder::II, pc);
424        x_s
425    }
426
427    pub(crate) fn in_order_ifft_in_place<T: DomainCoeff<F>>(&self, x_s: &mut [T]) {
428        #[cfg(all(feature = "cuda", target_arch = "x86_64"))]
429        // SNP TODO: how to set threshold
430        if self.size >= 32 && std::mem::size_of::<T>() == 32 {
431            let result = snarkvm_algorithms_cuda::NTT(
432                self.size as usize,
433                x_s,
434                snarkvm_algorithms_cuda::NTTInputOutputOrder::NN,
435                snarkvm_algorithms_cuda::NTTDirection::Inverse,
436                snarkvm_algorithms_cuda::NTTType::Standard,
437            );
438            if result.is_ok() {
439                return;
440            }
441        }
442
443        let pc = self.precompute_ifft();
444        self.ifft_helper_in_place_with_pc(x_s, FFTOrder::II, &pc);
445        cfg_iter_mut!(x_s, 1_000).for_each(|val| *val *= self.size_inv);
446    }
447
448    pub(crate) fn in_order_coset_ifft_in_place<T: DomainCoeff<F>>(&self, x_s: &mut [T]) {
449        #[cfg(all(feature = "cuda", target_arch = "x86_64"))]
450        // SNP TODO: how to set threshold
451        if self.size >= 32 && std::mem::size_of::<T>() == 32 {
452            let result = snarkvm_algorithms_cuda::NTT(
453                self.size as usize,
454                x_s,
455                snarkvm_algorithms_cuda::NTTInputOutputOrder::NN,
456                snarkvm_algorithms_cuda::NTTDirection::Inverse,
457                snarkvm_algorithms_cuda::NTTType::Coset,
458            );
459            if result.is_ok() {
460                return;
461            }
462        }
463
464        let pc = self.precompute_ifft();
465        self.ifft_helper_in_place_with_pc(x_s, FFTOrder::II, &pc);
466        let coset_shift = self.generator_inv;
467        Self::distribute_powers_and_mul_by_const(x_s, coset_shift, self.size_inv);
468    }
469
470    #[allow(unused)]
471    pub(crate) fn in_order_fft_in_place_with_pc<T: DomainCoeff<F>>(
472        &self,
473        x_s: &mut [T],
474        pre_comp: &FFTPrecomputation<F>,
475    ) {
476        #[cfg(all(feature = "cuda", target_arch = "x86_64"))]
477        // SNP TODO: how to set threshold
478        if self.size >= 32 && std::mem::size_of::<T>() == 32 {
479            let result = snarkvm_algorithms_cuda::NTT(
480                self.size as usize,
481                x_s,
482                snarkvm_algorithms_cuda::NTTInputOutputOrder::NN,
483                snarkvm_algorithms_cuda::NTTDirection::Forward,
484                snarkvm_algorithms_cuda::NTTType::Standard,
485            );
486            if result.is_ok() {
487                return;
488            }
489        }
490
491        self.fft_helper_in_place_with_pc(x_s, FFTOrder::II, pre_comp)
492    }
493
494    pub(crate) fn out_order_fft_in_place_with_pc<T: DomainCoeff<F>>(
495        &self,
496        x_s: &mut [T],
497        pre_comp: &FFTPrecomputation<F>,
498    ) {
499        self.fft_helper_in_place_with_pc(x_s, FFTOrder::IO, pre_comp)
500    }
501
502    pub(crate) fn in_order_ifft_in_place_with_pc<T: DomainCoeff<F>>(
503        &self,
504        x_s: &mut [T],
505        pre_comp: &IFFTPrecomputation<F>,
506    ) {
507        #[cfg(all(feature = "cuda", target_arch = "x86_64"))]
508        // SNP TODO: how to set threshold
509        if self.size >= 32 && std::mem::size_of::<T>() == 32 {
510            let result = snarkvm_algorithms_cuda::NTT(
511                self.size as usize,
512                x_s,
513                snarkvm_algorithms_cuda::NTTInputOutputOrder::NN,
514                snarkvm_algorithms_cuda::NTTDirection::Inverse,
515                snarkvm_algorithms_cuda::NTTType::Standard,
516            );
517            if result.is_ok() {
518                return;
519            }
520        }
521
522        self.ifft_helper_in_place_with_pc(x_s, FFTOrder::II, pre_comp);
523        cfg_iter_mut!(x_s, 1_000).for_each(|val| *val *= self.size_inv);
524    }
525
526    pub(crate) fn out_order_ifft_in_place_with_pc<T: DomainCoeff<F>>(
527        &self,
528        x_s: &mut [T],
529        pre_comp: &IFFTPrecomputation<F>,
530    ) {
531        self.ifft_helper_in_place_with_pc(x_s, FFTOrder::OI, pre_comp);
532        cfg_iter_mut!(x_s, 1_000).for_each(|val| *val *= self.size_inv);
533    }
534
535    #[allow(unused)]
536    pub(crate) fn in_order_coset_ifft_in_place_with_pc<T: DomainCoeff<F>>(
537        &self,
538        x_s: &mut [T],
539        pre_comp: &IFFTPrecomputation<F>,
540    ) {
541        #[cfg(all(feature = "cuda", target_arch = "x86_64"))]
542        // SNP TODO: how to set threshold
543        if self.size >= 32 && std::mem::size_of::<T>() == 32 {
544            let result = snarkvm_algorithms_cuda::NTT(
545                self.size as usize,
546                x_s,
547                snarkvm_algorithms_cuda::NTTInputOutputOrder::NN,
548                snarkvm_algorithms_cuda::NTTDirection::Inverse,
549                snarkvm_algorithms_cuda::NTTType::Coset,
550            );
551            if result.is_ok() {
552                return;
553            }
554        }
555
556        self.ifft_helper_in_place_with_pc(x_s, FFTOrder::II, pre_comp);
557        let coset_shift = self.generator_inv;
558        Self::distribute_powers_and_mul_by_const(x_s, coset_shift, self.size_inv);
559    }
560
561    fn fft_helper_in_place_with_pc<T: DomainCoeff<F>>(
562        &self,
563        x_s: &mut [T],
564        ord: FFTOrder,
565        pre_comp: &FFTPrecomputation<F>,
566    ) {
567        use FFTOrder::*;
568        let pc = pre_comp.precomputation_for_subdomain(self).unwrap();
569
570        let log_len = log2(x_s.len());
571
572        if ord == OI {
573            self.oi_helper_with_roots(x_s, &pc.roots);
574        } else {
575            self.io_helper_with_roots(x_s, &pc.roots);
576        }
577
578        if ord == II {
579            derange_helper(x_s, log_len);
580        }
581    }
582
583    // Handles doing an IFFT with handling of being in order and out of order.
584    // The results here must all be divided by |x_s|,
585    // which is left up to the caller to do.
586    fn ifft_helper_in_place_with_pc<T: DomainCoeff<F>>(
587        &self,
588        x_s: &mut [T],
589        ord: FFTOrder,
590        pre_comp: &IFFTPrecomputation<F>,
591    ) {
592        use FFTOrder::*;
593        let pc = pre_comp.precomputation_for_subdomain(self).unwrap();
594
595        let log_len = log2(x_s.len());
596
597        if ord == II {
598            derange_helper(x_s, log_len);
599        }
600
601        if ord == IO {
602            self.io_helper_with_roots(x_s, &pc.inverse_roots);
603        } else {
604            self.oi_helper_with_roots(x_s, &pc.inverse_roots);
605        }
606    }
607
608    /// Computes the first `self.size / 2` roots of unity for the entire domain.
609    /// e.g. for the domain [1, g, g^2, ..., g^{n - 1}], it computes
610    // [1, g, g^2, ..., g^{(n/2) - 1}]
611    #[cfg(feature = "serial")]
612    pub fn roots_of_unity(&self, root: F) -> Vec<F> {
613        compute_powers_serial((self.size as usize) / 2, root)
614    }
615
616    /// Computes the first `self.size / 2` roots of unity.
617    #[cfg(not(feature = "serial"))]
618    pub fn roots_of_unity(&self, root: F) -> Vec<F> {
619        // TODO: check if this method can replace parallel compute powers.
620        let log_size = log2(self.size as usize);
621        // early exit for short inputs
622        if log_size <= LOG_ROOTS_OF_UNITY_PARALLEL_SIZE {
623            compute_powers_serial((self.size as usize) / 2, root)
624        } else {
625            let mut temp = root;
626            // w, w^2, w^4, w^8, ..., w^(2^(log_size - 1))
627            let log_powers: Vec<F> = (0..(log_size - 1))
628                .map(|_| {
629                    let old_value = temp;
630                    temp.square_in_place();
631                    old_value
632                })
633                .collect();
634
635            // allocate the return array and start the recursion
636            let mut powers = vec![F::zero(); 1 << (log_size - 1)];
637            Self::roots_of_unity_recursive(&mut powers, &log_powers);
638            powers
639        }
640    }
641
642    #[cfg(not(feature = "serial"))]
643    fn roots_of_unity_recursive(out: &mut [F], log_powers: &[F]) {
644        assert_eq!(out.len(), 1 << log_powers.len());
645        // base case: just compute the powers sequentially,
646        // g = log_powers[0], out = [1, g, g^2, ...]
647        if log_powers.len() <= LOG_ROOTS_OF_UNITY_PARALLEL_SIZE as usize {
648            out[0] = F::one();
649            for idx in 1..out.len() {
650                out[idx] = out[idx - 1] * log_powers[0];
651            }
652            return;
653        }
654
655        // recursive case:
656        // 1. split log_powers in half
657        let (lr_lo, lr_hi) = log_powers.split_at(log_powers.len().div_ceil(2));
658        let mut scr_lo = vec![F::default(); 1 << lr_lo.len()];
659        let mut scr_hi = vec![F::default(); 1 << lr_hi.len()];
660        // 2. compute each half individually
661        rayon::join(
662            || Self::roots_of_unity_recursive(&mut scr_lo, lr_lo),
663            || Self::roots_of_unity_recursive(&mut scr_hi, lr_hi),
664        );
665        // 3. recombine halves
666        // At this point, out is a blank slice.
667        out.par_chunks_mut(scr_lo.len()).zip(&scr_hi).for_each(|(out_chunk, scr_hi)| {
668            for (out_elem, scr_lo) in out_chunk.iter_mut().zip(&scr_lo) {
669                *out_elem = *scr_hi * scr_lo;
670            }
671        });
672    }
673
674    #[inline(always)]
675    fn butterfly_fn_io<T: DomainCoeff<F>>(((lo, hi), root): ((&mut T, &mut T), &F)) {
676        let neg = *lo - *hi;
677        *lo += *hi;
678        *hi = neg;
679        *hi *= *root;
680    }
681
682    #[inline(always)]
683    fn butterfly_fn_oi<T: DomainCoeff<F>>(((lo, hi), root): ((&mut T, &mut T), &F)) {
684        *hi *= *root;
685        let neg = *lo - *hi;
686        *lo += *hi;
687        *hi = neg;
688    }
689
690    #[allow(clippy::too_many_arguments)]
691    fn apply_butterfly<T: DomainCoeff<F>, G: Fn(((&mut T, &mut T), &F)) + Copy + Sync + Send>(
692        g: G,
693        xi: &mut [T],
694        roots: &[F],
695        step: usize,
696        chunk_size: usize,
697        num_chunks: usize,
698        max_threads: usize,
699        gap: usize,
700    ) {
701        cfg_chunks_mut!(xi, chunk_size).for_each(|cxi| {
702            let (lo, hi) = cxi.split_at_mut(gap);
703            // If the chunk is sufficiently big that parallelism helps,
704            // we parallelize the butterfly operation within the chunk.
705
706            if gap > MIN_GAP_SIZE_FOR_PARALLELISATION && num_chunks < max_threads {
707                cfg_iter_mut!(lo, 1000).zip(hi).zip(cfg_iter!(roots, 1000).step_by(step)).for_each(g);
708            } else {
709                lo.iter_mut().zip(hi).zip(roots.iter().step_by(step)).for_each(g);
710            }
711        });
712    }
713
714    #[allow(clippy::unnecessary_to_owned)]
715    fn io_helper_with_roots<T: DomainCoeff<F>>(&self, xi: &mut [T], roots: &[F]) {
716        let mut roots = std::borrow::Cow::Borrowed(roots);
717
718        let mut step = 1;
719        let mut first = true;
720
721        #[cfg(not(feature = "serial"))]
722        let max_threads = snarkvm_utilities::parallel::max_available_threads();
723        #[cfg(feature = "serial")]
724        let max_threads = 1;
725
726        let mut gap = xi.len() / 2;
727        while gap > 0 {
728            // each butterfly cluster uses 2*gap positions
729            let chunk_size = 2 * gap;
730            let num_chunks = xi.len() / chunk_size;
731
732            // Only compact roots to achieve cache locality/compactness if
733            // the roots lookup is done a significant amount of times
734            // Which also implies a large lookup stride.
735            if num_chunks >= MIN_NUM_CHUNKS_FOR_COMPACTION {
736                if !first {
737                    roots = Cow::Owned(cfg_into_iter!(roots.into_owned()).step_by(step * 2).collect());
738                }
739                step = 1;
740                roots.to_mut().shrink_to_fit();
741            } else {
742                step = num_chunks;
743            }
744            first = false;
745
746            Self::apply_butterfly(
747                Self::butterfly_fn_io,
748                xi,
749                &roots[..],
750                step,
751                chunk_size,
752                num_chunks,
753                max_threads,
754                gap,
755            );
756
757            gap /= 2;
758        }
759    }
760
761    fn oi_helper_with_roots<T: DomainCoeff<F>>(&self, xi: &mut [T], roots_cache: &[F]) {
762        // The `cmp::min` is only necessary for the case where
763        // `MIN_NUM_CHUNKS_FOR_COMPACTION = 1`. Else, notice that we compact
764        // the roots cache by a stride of at least `MIN_NUM_CHUNKS_FOR_COMPACTION`.
765
766        let compaction_max_size =
767            core::cmp::min(roots_cache.len() / 2, roots_cache.len() / MIN_NUM_CHUNKS_FOR_COMPACTION);
768        let mut compacted_roots = vec![F::default(); compaction_max_size];
769
770        #[cfg(not(feature = "serial"))]
771        let max_threads = snarkvm_utilities::parallel::max_available_threads();
772        #[cfg(feature = "serial")]
773        let max_threads = 1;
774
775        let mut gap = 1;
776        while gap < xi.len() {
777            // each butterfly cluster uses 2*gap positions
778            let chunk_size = 2 * gap;
779            let num_chunks = xi.len() / chunk_size;
780
781            // Only compact roots to achieve cache locality/compactness if
782            // the roots lookup is done a significant amount of times
783            // Which also implies a large lookup stride.
784            let (roots, step) = if num_chunks >= MIN_NUM_CHUNKS_FOR_COMPACTION && gap < xi.len() / 2 {
785                cfg_iter_mut!(compacted_roots[..gap], 1_000)
786                    .zip(cfg_iter!(roots_cache[..(gap * num_chunks)], 1_000).step_by(num_chunks))
787                    .for_each(|(a, b)| *a = *b);
788                (&compacted_roots[..gap], 1)
789            } else {
790                (roots_cache, num_chunks)
791            };
792
793            Self::apply_butterfly(Self::butterfly_fn_oi, xi, roots, step, chunk_size, num_chunks, max_threads, gap);
794
795            gap *= 2;
796        }
797    }
798}
799
800/// The minimum number of chunks at which root compaction
801/// is beneficial.
802const MIN_NUM_CHUNKS_FOR_COMPACTION: usize = 1 << 7;
803
804/// The minimum size of a chunk at which parallelization of `butterfly`s is
805/// beneficial. This value was chosen empirically.
806const MIN_GAP_SIZE_FOR_PARALLELISATION: usize = 1 << 10;
807
808// minimum size at which to parallelize.
809#[cfg(not(feature = "serial"))]
810const LOG_ROOTS_OF_UNITY_PARALLEL_SIZE: u32 = 7;
811
812#[inline]
813pub(super) fn bitrev(a: u64, log_len: u32) -> u64 {
814    a.reverse_bits() >> (64 - log_len)
815}
816
817pub(crate) fn derange<T>(xi: &mut [T]) {
818    derange_helper(xi, log2(xi.len()))
819}
820
821fn derange_helper<T>(xi: &mut [T], log_len: u32) {
822    for idx in 1..(xi.len() as u64 - 1) {
823        let ridx = bitrev(idx, log_len);
824        if idx < ridx {
825            xi.swap(idx as usize, ridx as usize);
826        }
827    }
828}
829
830#[derive(PartialEq, Eq, Debug)]
831enum FFTOrder {
832    /// Both the input and the output of the FFT must be in-order.
833    II,
834    /// The input of the FFT must be in-order, but the output does not have to
835    /// be.
836    IO,
837    /// The input of the FFT can be out of order, but the output must be
838    /// in-order.
839    OI,
840}
841
842pub(crate) fn compute_powers_serial<F: Field>(size: usize, root: F) -> Vec<F> {
843    compute_powers_and_mul_by_const_serial(size, root, F::one())
844}
845
846pub(crate) fn compute_powers_and_mul_by_const_serial<F: Field>(size: usize, root: F, c: F) -> Vec<F> {
847    let mut value = c;
848    (0..size)
849        .map(|_| {
850            let old_value = value;
851            value *= root;
852            old_value
853        })
854        .collect()
855}
856
857#[allow(unused)]
858#[cfg(not(feature = "serial"))]
859pub(crate) fn compute_powers<F: Field>(size: usize, g: F) -> Vec<F> {
860    if size < MIN_PARALLEL_CHUNK_SIZE {
861        return compute_powers_serial(size, g);
862    }
863    // compute the number of threads we will be using.
864    let num_cpus_available = max_available_threads();
865    let num_elem_per_thread = core::cmp::max(size / num_cpus_available, MIN_PARALLEL_CHUNK_SIZE);
866    let num_cpus_used = size / num_elem_per_thread;
867
868    // Split up the powers to compute across each thread evenly.
869    let res: Vec<F> = (0..num_cpus_used)
870        .into_par_iter()
871        .flat_map(|i| {
872            let offset = g.pow([(i * num_elem_per_thread) as u64]);
873            // Compute the size that this chunks' output should be
874            // (num_elem_per_thread, unless there are less than num_elem_per_thread elements
875            // remaining)
876            let num_elements_to_compute = core::cmp::min(size - i * num_elem_per_thread, num_elem_per_thread);
877            compute_powers_and_mul_by_const_serial(num_elements_to_compute, g, offset)
878        })
879        .collect();
880    res
881}
882
883/// An iterator over the elements of the domain.
884#[derive(Clone)]
885pub struct Elements<F: FftField> {
886    cur_elem: F,
887    cur_pow: u64,
888    domain: EvaluationDomain<F>,
889}
890
891impl<F: FftField> Iterator for Elements<F> {
892    type Item = F;
893
894    fn next(&mut self) -> Option<F> {
895        if self.cur_pow == self.domain.size {
896            None
897        } else {
898            let cur_elem = self.cur_elem;
899            self.cur_elem *= &self.domain.group_gen;
900            self.cur_pow += 1;
901            Some(cur_elem)
902        }
903    }
904}
905
906/// An iterator over the elements of the domain.
907#[derive(Clone, Eq, PartialEq, Debug, CanonicalDeserialize, CanonicalSerialize)]
908pub struct FFTPrecomputation<F: FftField> {
909    roots: Vec<F>,
910    domain: EvaluationDomain<F>,
911}
912
913impl<F: FftField> FFTPrecomputation<F> {
914    pub fn to_ifft_precomputation(&self) -> IFFTPrecomputation<F> {
915        let mut inverse_roots = self.roots.clone();
916        snarkvm_fields::batch_inversion(&mut inverse_roots);
917        IFFTPrecomputation { inverse_roots, domain: self.domain }
918    }
919
920    pub fn precomputation_for_subdomain<'a>(&'a self, domain: &EvaluationDomain<F>) -> Option<Cow<'a, Self>> {
921        if domain.size() == 1 {
922            return Some(Cow::Owned(Self { roots: vec![], domain: *domain }));
923        }
924        if &self.domain == domain {
925            Some(Cow::Borrowed(self))
926        } else if domain.size() < self.domain.size() {
927            let size_ratio = self.domain.size() / domain.size();
928            let roots = self.roots.iter().step_by(size_ratio).copied().collect();
929            Some(Cow::Owned(Self { roots, domain: *domain }))
930        } else {
931            None
932        }
933    }
934}
935
936/// An iterator over the elements of the domain.
937#[derive(Clone, Eq, PartialEq, Debug, CanonicalSerialize, CanonicalDeserialize)]
938pub struct IFFTPrecomputation<F: FftField> {
939    inverse_roots: Vec<F>,
940    domain: EvaluationDomain<F>,
941}
942
943impl<F: FftField> IFFTPrecomputation<F> {
944    pub fn precomputation_for_subdomain<'a>(&'a self, domain: &EvaluationDomain<F>) -> Option<Cow<'a, Self>> {
945        if domain.size() == 1 {
946            return Some(Cow::Owned(Self { inverse_roots: vec![], domain: *domain }));
947        }
948        if &self.domain == domain {
949            Some(Cow::Borrowed(self))
950        } else if domain.size() < self.domain.size() {
951            let size_ratio = self.domain.size() / domain.size();
952            let inverse_roots = self.inverse_roots.iter().step_by(size_ratio).copied().collect();
953            Some(Cow::Owned(Self { inverse_roots, domain: *domain }))
954        } else {
955            None
956        }
957    }
958}
959
960#[cfg(test)]
961mod tests {
962    #[cfg(all(feature = "cuda", target_arch = "x86_64"))]
963    use crate::fft::domain::FFTOrder;
964    use crate::fft::{DensePolynomial, EvaluationDomain};
965    use rand::RngExt;
966    use snarkvm_curves::bls12_377::Fr;
967    use snarkvm_fields::{FftField, Field, One, Zero};
968    use snarkvm_utilities::{TestRng, Uniform};
969
970    #[test]
971    fn vanishing_polynomial_evaluation() {
972        let rng = &mut TestRng::default();
973        for coeffs in 0..10 {
974            let domain = EvaluationDomain::<Fr>::new(coeffs).unwrap();
975            let z = domain.vanishing_polynomial();
976            for _ in 0..100 {
977                let point = rng.random();
978                assert_eq!(z.evaluate(point), domain.evaluate_vanishing_polynomial(point))
979            }
980        }
981    }
982
983    #[test]
984    fn vanishing_polynomial_vanishes_on_domain() {
985        for coeffs in 0..1000 {
986            let domain = EvaluationDomain::<Fr>::new(coeffs).unwrap();
987            let z = domain.vanishing_polynomial();
988            for point in domain.elements() {
989                assert!(z.evaluate(point).is_zero())
990            }
991        }
992    }
993
994    #[test]
995    fn size_of_elements() {
996        for coeffs in 1..10 {
997            let size = 1 << coeffs;
998            let domain = EvaluationDomain::<Fr>::new(size).unwrap();
999            let domain_size = domain.size();
1000            assert_eq!(domain_size, domain.elements().count());
1001        }
1002    }
1003
1004    #[test]
1005    fn elements_contents() {
1006        for coeffs in 1..10 {
1007            let size = 1 << coeffs;
1008            let domain = EvaluationDomain::<Fr>::new(size).unwrap();
1009            for (i, element) in domain.elements().enumerate() {
1010                assert_eq!(element, domain.group_gen.pow([i as u64]));
1011            }
1012        }
1013    }
1014
1015    /// Test that lagrange interpolation for a random polynomial at a random
1016    /// point works.
1017    #[test]
1018    fn non_systematic_lagrange_coefficients_test() {
1019        let mut rng = TestRng::default();
1020        for domain_dimension in 1..10 {
1021            let domain_size = 1 << domain_dimension;
1022            let domain = EvaluationDomain::<Fr>::new(domain_size).unwrap();
1023            // Get random point & lagrange coefficients
1024            let random_point = Fr::rand(&mut rng);
1025            let lagrange_coefficients = domain.evaluate_all_lagrange_coefficients(random_point);
1026
1027            // Sample the random polynomial, evaluate it over the domain and the random
1028            // point.
1029            let random_polynomial = DensePolynomial::<Fr>::rand(domain_size - 1, &mut rng);
1030            let polynomial_evaluations = domain.fft(random_polynomial.coeffs());
1031            let actual_evaluations = random_polynomial.evaluate(random_point);
1032
1033            // Do lagrange interpolation, and compare against the actual evaluation
1034            let mut interpolated_evaluation = Fr::zero();
1035            for i in 0..domain_size {
1036                interpolated_evaluation += lagrange_coefficients[i] * polynomial_evaluations[i];
1037            }
1038            assert_eq!(actual_evaluations, interpolated_evaluation);
1039        }
1040    }
1041
1042    /// Test that lagrange coefficients for a point in the domain is correct.
1043    #[test]
1044    fn systematic_lagrange_coefficients_test() {
1045        // This runs in time O(N^2) in the domain size, so keep the domain dimension
1046        // low. We generate lagrange coefficients for each element in the
1047        // domain.
1048        for domain_dimension in 1..5 {
1049            let domain_size = 1 << domain_dimension;
1050            let domain = EvaluationDomain::<Fr>::new(domain_size).unwrap();
1051            let all_domain_elements: Vec<Fr> = domain.elements().collect();
1052            for (i, domain_element) in all_domain_elements.iter().enumerate().take(domain_size) {
1053                let lagrange_coefficients = domain.evaluate_all_lagrange_coefficients(*domain_element);
1054                for (j, lagrange_coefficient) in lagrange_coefficients.iter().enumerate().take(domain_size) {
1055                    // Lagrange coefficient for the evaluation point, which should be 1
1056                    if i == j {
1057                        assert_eq!(*lagrange_coefficient, Fr::one());
1058                    } else {
1059                        assert_eq!(*lagrange_coefficient, Fr::zero());
1060                    }
1061                }
1062            }
1063        }
1064    }
1065
1066    /// Tests that the roots of unity result is the same as domain.elements().
1067    #[test]
1068    fn test_roots_of_unity() {
1069        let max_degree = 10;
1070        for log_domain_size in 0..max_degree {
1071            let domain_size = 1 << log_domain_size;
1072            let domain = EvaluationDomain::<Fr>::new(domain_size).unwrap();
1073            let actual_roots = domain.roots_of_unity(domain.group_gen);
1074            for &value in &actual_roots {
1075                assert!(domain.evaluate_vanishing_polynomial(value).is_zero());
1076            }
1077            let expected_roots_elements = domain.elements();
1078            for (expected, &actual) in expected_roots_elements.zip(&actual_roots) {
1079                assert_eq!(expected, actual);
1080            }
1081            assert_eq!(actual_roots.len(), domain_size / 2);
1082        }
1083    }
1084
1085    /// Tests that the FFTs output the correct result.
1086    #[test]
1087    fn test_fft_correctness() {
1088        // This assumes a correct polynomial evaluation at point procedure.
1089        // It tests consistency of FFT/IFFT, and coset_fft/coset_ifft,
1090        // along with testing that each individual evaluation is correct.
1091
1092        let mut rng = TestRng::default();
1093
1094        // Runs in time O(degree^2)
1095        let log_degree = 5;
1096        let degree = 1 << log_degree;
1097        let random_polynomial = DensePolynomial::<Fr>::rand(degree - 1, &mut rng);
1098
1099        for log_domain_size in log_degree..(log_degree + 2) {
1100            let domain_size = 1 << log_domain_size;
1101            let domain = EvaluationDomain::<Fr>::new(domain_size).unwrap();
1102            let polynomial_evaluations = domain.fft(&random_polynomial.coeffs);
1103            let polynomial_coset_evaluations = domain.coset_fft(&random_polynomial.coeffs);
1104            for (i, x) in domain.elements().enumerate() {
1105                let coset_x = Fr::multiplicative_generator() * x;
1106
1107                assert_eq!(polynomial_evaluations[i], random_polynomial.evaluate(x));
1108                assert_eq!(polynomial_coset_evaluations[i], random_polynomial.evaluate(coset_x));
1109            }
1110
1111            let randon_polynomial_from_subgroup =
1112                DensePolynomial::from_coefficients_vec(domain.ifft(&polynomial_evaluations));
1113            let random_polynomial_from_coset =
1114                DensePolynomial::from_coefficients_vec(domain.coset_ifft(&polynomial_coset_evaluations));
1115
1116            assert_eq!(
1117                random_polynomial, randon_polynomial_from_subgroup,
1118                "degree = {degree}, domain size = {domain_size}"
1119            );
1120            assert_eq!(
1121                random_polynomial, random_polynomial_from_coset,
1122                "degree = {degree}, domain size = {domain_size}"
1123            );
1124        }
1125    }
1126
1127    /// Tests that FFT precomputation is correctly subdomained
1128    #[test]
1129    fn test_fft_precomputation() {
1130        for i in 1..10 {
1131            let big_domain = EvaluationDomain::<Fr>::new(i).unwrap();
1132            let pc = big_domain.precompute_fft();
1133            for j in 1..i {
1134                let small_domain = EvaluationDomain::<Fr>::new(j).unwrap();
1135                let small_pc = small_domain.precompute_fft();
1136                assert_eq!(pc.precomputation_for_subdomain(&small_domain).unwrap().as_ref(), &small_pc);
1137            }
1138        }
1139    }
1140
1141    /// Tests that IFFT precomputation is correctly subdomained
1142    #[test]
1143    fn test_ifft_precomputation() {
1144        for i in 1..10 {
1145            let big_domain = EvaluationDomain::<Fr>::new(i).unwrap();
1146            let pc = big_domain.precompute_ifft();
1147            for j in 1..i {
1148                let small_domain = EvaluationDomain::<Fr>::new(j).unwrap();
1149                let small_pc = small_domain.precompute_ifft();
1150                assert_eq!(pc.precomputation_for_subdomain(&small_domain).unwrap().as_ref(), &small_pc);
1151            }
1152        }
1153    }
1154
1155    /// Tests that IFFT precomputation can be correctly computed from
1156    /// FFT precomputation
1157    #[test]
1158    fn test_ifft_precomputation_from_fft() {
1159        for i in 1..10 {
1160            let domain = EvaluationDomain::<Fr>::new(i).unwrap();
1161            let pc = domain.precompute_ifft();
1162            let fft_pc = domain.precompute_fft();
1163            assert_eq!(pc, fft_pc.to_ifft_precomputation())
1164        }
1165    }
1166
1167    /// Tests that the FFTs output the correct result.
1168    #[cfg(all(feature = "cuda", target_arch = "x86_64"))]
1169    #[test]
1170    fn test_fft_correctness_cuda() {
1171        let mut rng = TestRng::default();
1172        for log_domain in 2..20 {
1173            println!("Testing domain size {log_domain}");
1174            let domain_size = 1 << log_domain;
1175            let random_polynomial = DensePolynomial::<Fr>::rand(domain_size - 1, &mut rng);
1176            let mut polynomial_evaluations = random_polynomial.coeffs.clone();
1177            let mut polynomial_evaluations_cuda = random_polynomial.coeffs.clone();
1178
1179            let domain = EvaluationDomain::<Fr>::new(domain_size).unwrap();
1180            let pc = domain.precompute_fft();
1181            domain.fft_helper_in_place_with_pc(&mut polynomial_evaluations, FFTOrder::II, &pc);
1182
1183            if snarkvm_algorithms_cuda::NTT::<Fr>(
1184                domain_size,
1185                &mut polynomial_evaluations_cuda,
1186                snarkvm_algorithms_cuda::NTTInputOutputOrder::NN,
1187                snarkvm_algorithms_cuda::NTTDirection::Forward,
1188                snarkvm_algorithms_cuda::NTTType::Standard,
1189            )
1190            .is_err()
1191            {
1192                println!("cuda error!");
1193            }
1194
1195            assert_eq!(polynomial_evaluations, polynomial_evaluations_cuda, "domain size = {domain_size}");
1196
1197            // iNTT
1198            if snarkvm_algorithms_cuda::NTT::<Fr>(
1199                domain_size,
1200                &mut polynomial_evaluations_cuda,
1201                snarkvm_algorithms_cuda::NTTInputOutputOrder::NN,
1202                snarkvm_algorithms_cuda::NTTDirection::Inverse,
1203                snarkvm_algorithms_cuda::NTTType::Standard,
1204            )
1205            .is_err()
1206            {
1207                println!("cuda error!");
1208            }
1209            assert_eq!(random_polynomial.coeffs, polynomial_evaluations_cuda, "domain size = {domain_size}");
1210
1211            // Coset NTT
1212            polynomial_evaluations = random_polynomial.coeffs.clone();
1213            let domain = EvaluationDomain::<Fr>::new(domain_size).unwrap();
1214            let pc = domain.precompute_fft();
1215            EvaluationDomain::<Fr>::distribute_powers(&mut polynomial_evaluations, Fr::multiplicative_generator());
1216            domain.fft_helper_in_place_with_pc(&mut polynomial_evaluations, FFTOrder::II, &pc);
1217
1218            if snarkvm_algorithms_cuda::NTT::<Fr>(
1219                domain_size,
1220                &mut polynomial_evaluations_cuda,
1221                snarkvm_algorithms_cuda::NTTInputOutputOrder::NN,
1222                snarkvm_algorithms_cuda::NTTDirection::Forward,
1223                snarkvm_algorithms_cuda::NTTType::Coset,
1224            )
1225            .is_err()
1226            {
1227                println!("cuda error!");
1228            }
1229
1230            assert_eq!(polynomial_evaluations, polynomial_evaluations_cuda, "domain size = {domain_size}");
1231
1232            // Coset iNTT
1233            if snarkvm_algorithms_cuda::NTT::<Fr>(
1234                domain_size,
1235                &mut polynomial_evaluations_cuda,
1236                snarkvm_algorithms_cuda::NTTInputOutputOrder::NN,
1237                snarkvm_algorithms_cuda::NTTDirection::Inverse,
1238                snarkvm_algorithms_cuda::NTTType::Coset,
1239            )
1240            .is_err()
1241            {
1242                println!("cuda error!");
1243            }
1244            assert_eq!(random_polynomial.coeffs, polynomial_evaluations_cuda, "domain size = {domain_size}");
1245        }
1246    }
1247
1248    #[test]
1249    fn test_reindex_by_subdomain_is_a_permutation() {
1250        // `(4, 2)` makes `period - 1` equal 1, and `(8, 1)` is the smallest subdomain.
1251        for (big, small) in [(8usize, 2usize), (16, 4), (32, 8), (64, 2), (128, 32), (4, 2), (8, 1)] {
1252            let g = EvaluationDomain::<Fr>::new(big).unwrap();
1253            let s = EvaluationDomain::<Fr>::new(small).unwrap();
1254            let mut image = (0..g.size()).map(|i| g.reindex_by_subdomain(&s, i).unwrap()).collect::<Vec<_>>();
1255            // The subdomain's elements are every `period`-th element of the domain.
1256            for (i, j) in image.iter().enumerate().take(s.size()) {
1257                assert_eq!(*j, i * (big / small), "big={big} small={small} i={i}");
1258            }
1259            image.sort_unstable();
1260            assert_eq!(image, (0..g.size()).collect::<Vec<_>>(), "big={big} small={small} not a permutation");
1261        }
1262    }
1263}