he_ring/number_ring/
odd_cyclotomic.rs

1use std::alloc::{Allocator, Global};
2use std::cmp::max;
3
4use bluestein::BluesteinFFT;
5use dense_poly::DensePolyRing;
6use feanor_math::algorithms::eea::{signed_eea, signed_gcd};
7use feanor_math::algorithms::int_factor::factor;
8use feanor_math::algorithms::cyclotomic::cyclotomic_polynomial;
9use feanor_math::algorithms::unity_root::{get_prim_root_of_unity, get_prim_root_of_unity_pow2};
10use feanor_math::algorithms::fft::*;
11use feanor_math::integer::IntegerRingStore;
12use feanor_math::integer::*;
13use feanor_math::rings::poly::*;
14use feanor_math::divisibility::DivisibilityRing;
15use feanor_math::primitive_int::*;
16use feanor_math::ring::*;
17use feanor_math::homomorphism::*;
18use feanor_math::rings::poly::sparse_poly::SparsePolyRing;
19use feanor_math::rings::zn::*;
20use subvector::SubvectorView;
21use tracing::instrument;
22use feanor_math::seq::*;
23
24use crate::euler_phi_squarefree;
25use crate::cyclotomic::*;
26use super::{HECyclotomicNumberRing, HECyclotomicNumberRingMod, HENumberRing, HENumberRingMod};
27
28///
29/// Represents `Z[𝝵_n]` for an odd `n`.
30/// 
31pub struct OddCyclotomicNumberRing {
32    n_factorization_squarefree: Vec<i64>,
33}
34
35impl OddCyclotomicNumberRing {
36
37    pub fn new(n: usize) -> Self {
38        assert!(n % 2 == 1);
39        assert!(n > 1);
40        let factorization = factor(StaticRing::<i64>::RING, n as i64);
41        // while most of the arithmetic still works with non-squarefree n, our statements about the geometry
42        // of the number ring as lattice don't hold anymore (currently this refers to the `norm1_to_norm2_expansion_factor`
43        // functions)
44        for (_, e) in &factorization {
45            assert!(*e == 1, "n = {} is not squarefree", n);
46        }
47        Self {
48            n_factorization_squarefree: factorization.iter().map(|(p, _)| *p).collect(),
49        }
50    }
51
52    ///
53    /// Returns a bound on
54    /// ```text
55    ///   sup_(x in R \ {0}) | x |_can / | x |'_inf
56    /// ```
57    /// where `| . |'_inf` is similar to `| . |_inf`, but takes the inf-norm w.r.t.
58    /// the powerful basis representation. The powerful basis is given by the monomials
59    /// `X^(n i1 / p1 + ... + n ir / pr)` for `0 <= ik < phi(pk) = pk - 1`, and `n = p1 ... pr` is
60    /// squarefree with prime factors `p1, ..., pr`.
61    /// 
62    /// To compare, the standard inf norm `| . |_inf` is the inf-norm w.r.t. the
63    /// coefficient basis representation, which is just given by the monomials `X^i`
64    /// for `0 <= i < phi(n)`. It has the disadvantage that it is not compatible with
65    /// the tensor-product factorization
66    /// ```text
67    ///   Q[X]/(Phi_n) = Q[X]/(Phi_p1) ⊗ ... ⊗ Q[X]/(Phi_pr)
68    /// ```
69    /// 
70    pub fn powful_inf_to_can_norm_expansion_factor(&self) -> f64 {
71        let rank = euler_phi_squarefree(&self.n_factorization_squarefree);
72        // a simple estimate; it holds, as for any `x` with `|x|_inf <= b`, the coefficients
73        // under the canonical embedding are clearly `<= nb` in absolute value, thus the canonical
74        // norm is at most `n sqrt(n)`
75        (rank as f64).powi(3).sqrt()
76    }
77
78    ///
79    /// Returns a bound on
80    /// ```text
81    ///   sup_(x in R \ {0}) | x |'_inf / | x |_can
82    /// ```
83    /// For the distinction of standard inf-norm and powerful inf-norm, see
84    /// the doc of [`OddCyclotomicNumberRing::powful_inf_to_can_norm_expansion_factor()`].
85    /// 
86    pub fn can_to_powful_inf_norm_expansion_factor(&self) -> f64 {
87        // if `n = p` is a prime, we can give an explicit inverse to the matrix
88        // `A = ( zeta^(ij) )` where `i in (Z/pZ)*` and `j in { 0, ..., p - 2 }` by
89        // `A^-1 = ( zeta^(ij) - zeta^j ) / p` with `i in { 0, ..., p - 2 }` and `j in (Z/pZ)*`.
90        // This clearly shows that in this case, then expansion factor is at most 
91        // `(p - 1) | zeta^(ij) - zeta^j | / p < 2`. By the tensor product compatibility of
92        // the powerful inf-norm, we thus get this bound
93        2f64.powi(self.n_factorization_squarefree.len() as i32)
94    }
95
96    ///
97    /// Returns a bound on
98    /// ```text
99    ///   sup_(x in R \ {0}) | x |_inf / | x |'_inf
100    /// ```
101    /// For the distinction of standard inf-norm and powerful inf-norm, see
102    /// the doc of [`OddCyclotomicNumberRing::powful_inf_to_can_norm_expansion_factor()`].
103    /// 
104    pub fn powful_inf_to_inf_norm_expansion_factor(&self) -> f64 {
105        // TODO: Fix
106        // conjecture: this is `<= n`; I have no proof currently, but note the following:
107        // If the powerful-basis indices `n1 i1 / p1 + ... + nr ir / pr` were distributed
108        // at random, about `n / phi(n)` of them would have to be "reduced", i.e. fall
109        // into `{ phi(n), ..., n - 1 }` modulo `n`. Each of them contributes to the inf-operator
110        // norm, up to the maximal coefficient of `Phi_n`. This maximal coefficient seems
111        // to behave as `n^(1/r)`, and `n / phi(n) ~ n^((r - 1)/r)`
112        let rank = euler_phi_squarefree(&self.n_factorization_squarefree);
113        return rank as f64;
114    }
115}
116
117impl Clone for OddCyclotomicNumberRing {
118    fn clone(&self) -> Self {
119        Self {
120            n_factorization_squarefree: self.n_factorization_squarefree.clone()
121        }
122    }
123}
124
125impl PartialEq for OddCyclotomicNumberRing {
126
127    fn eq(&self, other: &Self) -> bool {
128        self.n_factorization_squarefree == other.n_factorization_squarefree
129    }
130}
131
132impl HENumberRing for OddCyclotomicNumberRing {
133
134    type Decomposed = OddCyclotomicDecomposedNumberRing<BluesteinFFT<zn_64::ZnBase, zn_64::ZnBase, Identity<zn_64::Zn>>>;
135
136    fn can_to_inf_norm_expansion_factor(&self) -> f64 {
137        self.powful_inf_to_inf_norm_expansion_factor() * self.can_to_powful_inf_norm_expansion_factor()
138    }
139
140    fn inf_to_can_norm_expansion_factor(&self) -> f64 {
141        let rank = euler_phi_squarefree(&self.n_factorization_squarefree);
142        // a simple estimate; it holds, as for any `x` with `|x|_inf <= b`, the coefficients
143        // under the canonical embedding are clearly `<= nb` in absolute value, thus the canonical
144        // norm is at most `n sqrt(n)`
145        (rank as f64).powi(3).sqrt()
146    }
147
148    fn mod_p(&self, Fp: zn_64::Zn) -> Self::Decomposed {
149        let n_factorization = &self.n_factorization_squarefree;
150        let n = n_factorization.iter().copied().product::<i64>();
151
152        let Fp_as_field = (&Fp).as_field().ok().unwrap();
153        let zeta = get_prim_root_of_unity(&Fp_as_field, 2 * n as usize).unwrap();
154        let zeta = Fp_as_field.get_ring().unwrap_element(zeta);
155        let log2_m = StaticRing::<i64>::RING.abs_log2_ceil(&n).unwrap() + 1;
156        let zeta_m = get_prim_root_of_unity_pow2(&Fp_as_field, log2_m).unwrap();
157        let zeta_m = Fp_as_field.get_ring().unwrap_element(zeta_m);
158        let fft_table = BluesteinFFT::new(Fp.clone(), zeta, zeta_m, n as usize, log2_m, Global);
159
160        return OddCyclotomicDecomposedNumberRing::create_squarefree(fft_table, Fp, &self.n_factorization_squarefree, Global);
161    }
162
163    fn mod_p_required_root_of_unity(&self) -> usize {
164        let n = <_ as HECyclotomicNumberRing>::n(self);
165        let log2_m = StaticRing::<i64>::RING.abs_log2_ceil(&(n as i64)).unwrap() + 2;
166        return n << log2_m;
167    }
168
169    fn generating_poly<P>(&self, poly_ring: P) -> El<P>
170        where P: RingStore,
171            P::Type: PolyRing + DivisibilityRing,
172            <<P::Type as RingExtension>::BaseRing as RingStore>::Type: IntegerRing
173    {
174        cyclotomic_polynomial(&poly_ring, <_ as HECyclotomicNumberRing>::n(self) as usize)
175    }
176
177    fn rank(&self) -> usize {
178        euler_phi_squarefree(&self.n_factorization_squarefree) as usize
179    }
180}
181
182impl HECyclotomicNumberRing for OddCyclotomicNumberRing {
183
184    fn n(&self) -> usize {
185        self.n_factorization_squarefree.iter().copied().product::<i64>() as usize
186    }
187}
188
189///
190/// Represents `Z[𝝵_n]` for an odd `n`, but uses of the tensor decomposition
191/// `Z[𝝵_n] = Z[𝝵_n1] ⊗ Z[𝝵_n2]` for various computational tasks (where `n = n1 n2`
192/// is a factorization into coprime factors).
193/// 
194#[derive(Clone)]
195pub struct CompositeCyclotomicNumberRing {
196    tensor_factor1: OddCyclotomicNumberRing,
197    tensor_factor2: OddCyclotomicNumberRing
198}
199
200impl CompositeCyclotomicNumberRing {
201
202    pub fn new(n1: usize, n2: usize) -> Self {
203        assert!(n1 % 2 == 1);
204        assert!(n2 % 2 == 1);
205        assert!(n1 > 1);
206        assert!(n2 > 1);
207        assert!(signed_gcd(n1 as i64, n2 as i64, StaticRing::<i64>::RING) == 1);
208        Self {
209            tensor_factor1: OddCyclotomicNumberRing::new(n1),
210            tensor_factor2: OddCyclotomicNumberRing::new(n2)
211        }
212    }
213}
214
215impl PartialEq for CompositeCyclotomicNumberRing {
216
217    fn eq(&self, other: &Self) -> bool {
218        self.tensor_factor1 == other.tensor_factor1 && self.tensor_factor2 == other.tensor_factor2
219    }
220}
221
222impl HECyclotomicNumberRing for CompositeCyclotomicNumberRing {
223
224    fn n(&self) -> usize {
225        <_ as HECyclotomicNumberRing>::n(&self.tensor_factor1) * <_ as HECyclotomicNumberRing>::n(&self.tensor_factor2)
226    }
227}
228
229impl HENumberRing for CompositeCyclotomicNumberRing {
230
231    type Decomposed = CompositeCyclotomicDecomposedNumberRing<BluesteinFFT<zn_64::ZnBase, zn_64::ZnBase, Identity<zn_64::Zn>>>;
232
233    fn mod_p(&self, Fp: zn_64::Zn) -> Self::Decomposed {
234        let n1 = <_ as HECyclotomicNumberRing>::n(&self.tensor_factor1) as i64;
235        let n2 = <_ as HECyclotomicNumberRing>::n(&self.tensor_factor2) as i64;
236        let n = n1 * n2;
237        let r1 = <_ as HENumberRing>::rank(&self.tensor_factor1) as i64;
238        let r2 = <_ as HENumberRing>::rank(&self.tensor_factor2) as i64;
239
240        let poly_ring = SparsePolyRing::new(StaticRing::<i64>::RING, "X");
241        let poly_ring = &poly_ring;
242        let Phi_n1 = self.tensor_factor1.generating_poly(&poly_ring);
243        let Phi_n2 = self.tensor_factor2.generating_poly(&poly_ring);
244        let hom = Fp.can_hom(Fp.integer_ring()).unwrap().compose(Fp.integer_ring().can_hom(poly_ring.base_ring()).unwrap());
245        let hom_ref = &hom;
246
247        let (s, t, d) = signed_eea(n1, n2, StaticRing::<i64>::RING);
248        assert_eq!(1, d);
249
250        // the main task is to create a sparse representation of the two matrices that
251        // represent the conversion from powerful basis to coefficient basis and back;
252        // everything else is done by `OddCyclotomicNumberRing::mod_p()`
253
254        let mut small_to_coeff_conversion_matrix = (0..(r1 * r2)).map(|_| Vec::new()).collect::<Vec<_>>();
255        let mut coeff_to_small_conversion_matrix = (0..(r1 * r2)).map(|_| Vec::new()).collect::<Vec<_>>();
256
257        let dense_poly_ring = DensePolyRing::new(poly_ring.base_ring(), "X");
258        let Phi_n_sparse = cyclotomic_polynomial(&poly_ring, n as usize);
259        let Phi_n = dense_poly_ring.can_hom(&poly_ring).unwrap().map_ref(&Phi_n_sparse);
260        let mut X_pow_i = None;
261        for i in 0..(n1 * n2) {
262
263            let i1 = ((t * i % n1) + n1) % n1;
264            let i2 = ((s * i % n2) + n2) % n2;
265            debug_assert_eq!(i, (i1 * n / n1 + i2 * n / n2) % n);
266
267            if i < r1 * r2 {
268                let X1_power_reduced = poly_ring.div_rem_monic(poly_ring.pow(poly_ring.indeterminate(), i1 as usize), &Phi_n1).1;
269                let X2_power_reduced = poly_ring.div_rem_monic(poly_ring.pow(poly_ring.indeterminate(), i2 as usize), &Phi_n2).1;
270                
271                coeff_to_small_conversion_matrix[i as usize] = poly_ring.terms(&X1_power_reduced).flat_map(|(c1, j1)| poly_ring.terms(&X2_power_reduced).map(move |(c2, j2)| 
272                    (j1 + j2 * r1 as usize, hom_ref.map(poly_ring.base_ring().mul_ref(c1, c2))
273                ))).collect::<Vec<_>>();
274            }
275            
276            if i1 < r1 && i2 < r2 {
277                if let Some(X_pow_i) = &X_pow_i {
278                    small_to_coeff_conversion_matrix[(i2 * r1 + i1) as usize] = dense_poly_ring.terms(X_pow_i).map(|(c, i)| {
279                        assert!(i < (r1 * r2) as usize);
280                        (i, hom_ref.map_ref(c))
281                    }).collect::<Vec<_>>();
282                } else {
283                    small_to_coeff_conversion_matrix[(i2 * r1 + i1) as usize] = vec![(i as usize, hom_ref.codomain().one())];
284                }
285            }
286
287            if i == (r1 * r2) - 1 {
288                X_pow_i = Some(dense_poly_ring.from_terms([(dense_poly_ring.base_ring().one(), (r1 * r2 - 1) as usize)]));
289            }
290            if let Some(X_pow_i) = &mut X_pow_i {
291                dense_poly_ring.mul_assign_monomial(X_pow_i, 1);
292                let lc = dense_poly_ring.coefficient_at(X_pow_i, (r1 * r2) as usize);
293                // *X_pow_i = dense_poly_ring.div_rem_monic(std::mem::replace(X_pow_i, dense_poly_ring.zero()), &Phi_n).1;
294                if dense_poly_ring.base_ring().is_zero(&lc) {
295                    // do nothing
296                } else if dense_poly_ring.base_ring().is_one(&lc) {
297                    dense_poly_ring.get_ring().add_assign_from_terms(X_pow_i, dense_poly_ring.terms(&Phi_n).map(|(c, i)| (dense_poly_ring.base_ring().negate(dense_poly_ring.base_ring().clone_el(c)), i)));
298                } else if dense_poly_ring.base_ring().is_neg_one(&lc) {
299                    dense_poly_ring.get_ring().add_assign_from_terms(X_pow_i, dense_poly_ring.terms(&Phi_n).map(|(c, i)| (dense_poly_ring.base_ring().clone_el(c), i)));
300                } else {
301                    let lc = dense_poly_ring.base_ring().clone_el(lc);
302                    dense_poly_ring.get_ring().add_assign_from_terms(X_pow_i, dense_poly_ring.terms(&Phi_n).map(|(c, i)| (dense_poly_ring.base_ring().negate(dense_poly_ring.base_ring().mul_ref(c, &lc)), i)));
303                }
304            }
305        }
306
307        CompositeCyclotomicDecomposedNumberRing {
308            small_to_coeff_conversion_matrix: small_to_coeff_conversion_matrix,
309            coeff_to_small_conversion_matrix: coeff_to_small_conversion_matrix,
310            tensor_factor1: self.tensor_factor1.mod_p(Fp.clone()),
311            tensor_factor2: self.tensor_factor2.mod_p(Fp)
312        }
313    }
314
315    fn mod_p_required_root_of_unity(&self) -> usize {
316        let n = <_ as HECyclotomicNumberRing>::n(self);
317        let log2_m = max(
318            StaticRing::<i64>::RING.abs_log2_ceil(&(<_ as HECyclotomicNumberRing>::n(&self.tensor_factor1) as i64)).unwrap() + 2,
319            StaticRing::<i64>::RING.abs_log2_ceil(&(<_ as HECyclotomicNumberRing>::n(&self.tensor_factor2) as i64)).unwrap() + 2
320        );
321        return n << log2_m;
322    }
323
324    fn inf_to_can_norm_expansion_factor(&self) -> f64 {
325        return <_ as HENumberRing>::inf_to_can_norm_expansion_factor(&self.tensor_factor1) * <_ as HENumberRing>::inf_to_can_norm_expansion_factor(&self.tensor_factor2);
326    }
327
328    fn can_to_inf_norm_expansion_factor(&self) -> f64 {
329        return <_ as HENumberRing>::can_to_inf_norm_expansion_factor(&self.tensor_factor1) * <_ as HENumberRing>::can_to_inf_norm_expansion_factor(&self.tensor_factor2);
330    }
331
332    fn generating_poly<P>(&self, poly_ring: P) -> El<P>
333        where P: RingStore,
334            P::Type: PolyRing + DivisibilityRing,
335            <<P::Type as RingExtension>::BaseRing as RingStore>::Type: IntegerRing
336    {
337        cyclotomic_polynomial(&poly_ring, <_ as HECyclotomicNumberRing>::n(self) as usize)
338    }
339
340    fn rank(&self) -> usize {
341        <_ as HENumberRing>::rank(&self.tensor_factor1) * <_ as HENumberRing>::rank(&self.tensor_factor2)
342    }
343}
344
345pub struct OddCyclotomicDecomposedNumberRing<F, A = Global> 
346    where F: FFTAlgorithm<zn_64::ZnBase> + PartialEq,
347        A: Allocator + Clone
348{
349    ring: zn_64::Zn,
350    fft_table: F,
351    /// contains `usize::MAX` whenenver the fft output index corresponds to a non-primitive root of unity, and an index otherwise
352    fft_output_indices_to_indices: Vec<usize>,
353    zeta_pow_rank: Vec<(usize, zn_64::ZnEl)>,
354    rank: usize,
355    allocator: A
356}
357
358impl<F, A> PartialEq for OddCyclotomicDecomposedNumberRing<F, A> 
359    where F: FFTAlgorithm<zn_64::ZnBase> + PartialEq,
360        A: Allocator + Clone
361{
362    fn eq(&self, other: &Self) -> bool {
363        self.ring.get_ring() == other.ring.get_ring() && self.fft_table == other.fft_table
364    }
365}
366
367impl<F, A> OddCyclotomicDecomposedNumberRing<F, A> 
368    where F: FFTAlgorithm<zn_64::ZnBase> + PartialEq,
369        A: Allocator + Clone
370{
371    fn create_squarefree(fft_table: F, Fp: zn_64::Zn, n_factorization: &[i64], allocator: A) -> Self {
372        let n = n_factorization.iter().copied().product::<i64>();
373        let rank = euler_phi_squarefree(&n_factorization) as usize;
374
375        let Fp_as_field = (&Fp).as_field().ok().unwrap();
376        let poly_ring = SparsePolyRing::new(Fp_as_field.clone(), "X");
377        let cyclotomic_poly = cyclotomic_polynomial(&poly_ring, n as usize);
378        assert_eq!(poly_ring.degree(&cyclotomic_poly).unwrap(), rank);
379        let mut zeta_pow_rank = Vec::new();
380        for (a, i) in poly_ring.terms(&cyclotomic_poly) {
381            if i != rank {
382                zeta_pow_rank.push((i, Fp.negate(Fp_as_field.get_ring().unwrap_element(Fp_as_field.clone_el(a)))));
383            }
384        }
385        zeta_pow_rank.sort_unstable_by_key(|(i, _)| *i);
386
387        let fft_output_indices_to_indices = (0..fft_table.len()).scan(0, |state, i| {
388            let power = fft_table.unordered_fft_permutation(i);
389            if n_factorization.iter().all(|p| power as i64 % *p != 0) {
390                *state += 1;
391                return Some(*state - 1);
392            } else {
393                return Some(usize::MAX);
394            }
395        }).collect::<Vec<_>>();
396
397        return Self { ring: Fp, fft_table, zeta_pow_rank, rank, allocator, fft_output_indices_to_indices };
398    }
399
400    ///
401    /// Computing this "generalized FFT" requires evaluating a polynomial at all primitive
402    /// `n`-th roots of unity. However, the base FFT will compute the evaluation at all `n`-th
403    /// roots of unity. This function gives an iterator over the index pairs `(i, j)`, where `i` 
404    /// is an index into the vector of evaluations, and `j` is an index into the output of the base 
405    /// FFT.
406    /// 
407    fn fft_output_indices<'a>(&'a self) -> impl Iterator<Item = (usize, usize)> + 'a {
408        self.fft_output_indices_to_indices.iter().enumerate().filter_map(|(i, j)| if *j == usize::MAX { None } else { Some((*j, i)) })
409    }
410}
411
412impl<F, A> HENumberRingMod for OddCyclotomicDecomposedNumberRing<F, A> 
413    where F: Send + Sync + FFTAlgorithm<zn_64::ZnBase> + PartialEq,
414        A: Send + Sync + Allocator + Clone
415{
416    fn mult_basis_to_small_basis<V>(&self, mut data: V)
417        where V: VectorViewMut<zn_64::ZnEl>
418    {
419        let ring = self.base_ring();
420        let mut tmp = Vec::with_capacity_in(self.fft_table.len(), &self.allocator);
421        tmp.extend((0..self.fft_table.len()).map(|_| ring.zero()));
422        for (i, j) in self.fft_output_indices() {
423            tmp[j] = ring.clone_el(data.at(i));
424        }
425
426        self.fft_table.unordered_inv_fft(&mut tmp[..], ring);
427
428        for i in (self.rank()..self.fft_table.len()).rev() {
429            let factor = ring.clone_el(&tmp[i]);
430            for (j, c) in self.zeta_pow_rank.iter() {
431                let mut add = ring.clone_el(&factor);
432                ring.mul_assign_ref(&mut add, c);
433                ring.add_assign(&mut tmp[i - self.rank() + *j], add);
434            }
435        }
436
437        for i in 0..self.rank() {
438            *data.at_mut(i) = ring.clone_el(&tmp[i]);
439        }
440    }
441
442    fn small_basis_to_mult_basis<V>(&self, mut data: V)
443        where V: VectorViewMut<zn_64::ZnEl>
444    {
445        let ring = self.base_ring();
446        let mut tmp = Vec::with_capacity_in(self.fft_table.len(), self.allocator.clone());
447        tmp.extend((0..self.fft_table.len()).map(|_| ring.zero()));
448        for i in 0..self.rank() {
449            tmp[i] = ring.clone_el(data.at(i));
450        }
451
452        self.fft_table.unordered_fft(&mut tmp[..], ring);
453
454        for (i, j) in self.fft_output_indices() {
455            *data.at_mut(i) = ring.clone_el(&tmp[j]); 
456        }
457    }
458
459    fn coeff_basis_to_small_basis<V>(&self, _data: V)
460        where V: VectorViewMut<zn_64::ZnEl>
461    {}
462
463    fn small_basis_to_coeff_basis<V>(&self, _data: V)
464        where V: VectorViewMut<zn_64::ZnEl>
465    {}
466
467    fn rank(&self) -> usize {
468        self.rank
469    }
470
471    fn base_ring(&self) -> &zn_64::Zn {
472        &self.ring
473    }
474}
475
476impl<F, A> HECyclotomicNumberRingMod for OddCyclotomicDecomposedNumberRing<F, A> 
477    where F: Send + Sync + FFTAlgorithm<zn_64::ZnBase> + PartialEq,
478        A: Send + Sync + Allocator + Clone
479{
480    fn n(&self) -> usize {
481        self.fft_table.len()
482    }
483
484    fn permute_galois_action<V1, V2>(&self, src: V1, mut dst: V2, galois_element: CyclotomicGaloisGroupEl)
485        where V1: VectorView<zn_64::ZnEl>,
486            V2: SwappableVectorViewMut<zn_64::ZnEl>
487    {
488        assert_eq!(self.rank(), src.len());
489        assert_eq!(self.rank(), dst.len());
490        let ring = self.base_ring();
491        let galois_group = self.galois_group();
492        let index_ring = galois_group.underlying_ring();
493        let hom = index_ring.can_hom(&StaticRing::<i64>::RING).unwrap();
494        
495        for (j, i) in self.fft_output_indices() {
496            *dst.at_mut(j) = ring.clone_el(src.at(self.fft_output_indices_to_indices[self.fft_table.unordered_fft_permutation_inv(
497                index_ring.smallest_positive_lift(index_ring.mul(galois_group.to_ring_el(galois_element), hom.map(self.fft_table.unordered_fft_permutation(i) as i64))) as usize
498            )]));
499        }
500    }
501}
502
503///
504/// The small basis is given by 
505/// ```text
506///   1 ⊗ 1,            𝝵1 ⊗ 1,            𝝵1^2 ⊗ 1,           ...,  𝝵1^(n1 - 1) ⊗ 1,
507///   1 ⊗ 𝝵2,           𝝵1 ⊗ 𝝵2,           𝝵1^2 ⊗ 𝝵2,          ...,  𝝵1^(n1 - 1) ⊗ 𝝵2,
508///   ...
509///   1 ⊗ 𝝵2^(n2 - 1),  𝝵1 ⊗ 𝝵2^(n2 - 1),  𝝵1^2 ⊗ 𝝵2^(n2 - 1), ...,  𝝵1^(n1 - 1) ⊗ 𝝵2^(n2 - 1)
510/// ```
511/// 
512pub struct CompositeCyclotomicDecomposedNumberRing<F, A = Global> 
513    where F: FFTAlgorithm<zn_64::ZnBase> + PartialEq,
514        A: Allocator + Clone
515{
516    tensor_factor1: OddCyclotomicDecomposedNumberRing<F, A>,
517    tensor_factor2: OddCyclotomicDecomposedNumberRing<F, A>,
518    // the `i`-th entry is none if the `i`-th small basis vector equals the `i`-th coeff basis vector,
519    // and otherwise, it contains the coeff basis representation of the `i`-th small basis vector
520    small_to_coeff_conversion_matrix: Vec<Vec<(usize, zn_64::ZnEl)>>,
521    // same as `small_to_coeff_conversion_matrix` but with small and coeff basis swapped
522    coeff_to_small_conversion_matrix: Vec<Vec<(usize, zn_64::ZnEl)>>
523}
524
525impl<F, A> PartialEq for CompositeCyclotomicDecomposedNumberRing<F, A> 
526    where F: FFTAlgorithm<zn_64::ZnBase> + PartialEq,
527        A: Allocator + Clone
528{
529    fn eq(&self, other: &Self) -> bool {
530        self.tensor_factor1 == other.tensor_factor1 && self.tensor_factor2 == other.tensor_factor2
531    }
532}
533
534impl<F, A> HENumberRingMod for CompositeCyclotomicDecomposedNumberRing<F, A> 
535    where F: Send + Sync + FFTAlgorithm<zn_64::ZnBase> + PartialEq,
536        A: Send + Sync + Allocator + Clone
537{
538    #[instrument(skip_all)]
539    fn small_basis_to_mult_basis<V>(&self, mut data: V)
540        where V: SwappableVectorViewMut<zn_64::ZnEl>
541    {
542        for i in 0..self.tensor_factor2.rank() {
543            self.tensor_factor1.small_basis_to_mult_basis(SubvectorView::new(&mut data).restrict((i * self.tensor_factor1.rank())..((i + 1) * self.tensor_factor1.rank())));
544        }
545        for j in 0..self.tensor_factor1.rank() {
546            self.tensor_factor2.small_basis_to_mult_basis(SubvectorView::new(&mut data).restrict(j..).step_by_view(self.tensor_factor1.rank()));
547        }
548    }
549
550    #[instrument(skip_all)]
551    fn mult_basis_to_small_basis<V>(&self, mut data: V)
552        where V: SwappableVectorViewMut<zn_64::ZnEl>
553    {
554        for j in 0..self.tensor_factor1.rank() {
555            self.tensor_factor2.mult_basis_to_small_basis(SubvectorView::new(&mut data).restrict(j..).step_by_view(self.tensor_factor1.rank()));
556        }
557        for i in 0..self.tensor_factor2.rank() {
558            self.tensor_factor1.mult_basis_to_small_basis(SubvectorView::new(&mut data).restrict((i * self.tensor_factor1.rank())..((i + 1) * self.tensor_factor1.rank())));
559        }
560    }
561
562    #[instrument(skip_all)]
563    fn coeff_basis_to_small_basis<V>(&self, mut data: V)
564        where V: SwappableVectorViewMut<zn_64::ZnEl>
565    {
566        let mut result = Vec::with_capacity_in(self.rank(), &self.tensor_factor1.allocator);
567        result.resize_with(self.rank(), || self.base_ring().zero());
568        for i in 0..self.rank() {
569            for (j, c) in &self.coeff_to_small_conversion_matrix[i] {
570                self.base_ring().add_assign(&mut result[*j], self.base_ring().mul_ref(data.at(i), c));
571            }
572        }
573        for (i, c) in result.drain(..).enumerate() {
574            *data.at_mut(i) = c;
575        }
576    }
577
578    #[instrument(skip_all)]
579    fn small_basis_to_coeff_basis<V>(&self, mut data: V)
580        where V: SwappableVectorViewMut<zn_64::ZnEl>
581    {
582        let mut result = Vec::with_capacity_in(self.rank(), &self.tensor_factor1.allocator);
583        result.resize_with(self.rank(), || self.base_ring().zero());
584        for i in 0..self.rank() {
585            for (j, c) in &self.small_to_coeff_conversion_matrix[i] {
586                self.base_ring().add_assign(&mut result[*j], self.base_ring().mul_ref(data.at(i), c));
587            }
588        }
589        for (i, c) in result.drain(..).enumerate() {
590            *data.at_mut(i) = c;
591        }
592    }
593
594    fn rank(&self) -> usize {
595        self.tensor_factor1.rank() * self.tensor_factor2.rank()
596    }
597
598    fn base_ring(&self) -> &zn_64::Zn {
599        self.tensor_factor1.base_ring()
600    }
601}
602
603impl<F, A> HECyclotomicNumberRingMod for CompositeCyclotomicDecomposedNumberRing<F, A> 
604    where F: Send + Sync + FFTAlgorithm<zn_64::ZnBase> + PartialEq,
605        A: Send + Sync + Allocator + Clone
606{
607    fn n(&self) -> usize {
608        self.tensor_factor1.n() * self.tensor_factor2.n()
609    }
610
611    fn permute_galois_action<V1, V2>(&self, src: V1, mut dst: V2, galois_element: CyclotomicGaloisGroupEl)
612        where V1: VectorView<zn_64::ZnEl>,
613            V2: SwappableVectorViewMut<zn_64::ZnEl>
614    {
615        let index_ring = self.galois_group();
616        let ring_factor1 = self.tensor_factor1.galois_group();
617        let ring_factor2 = self.tensor_factor2.galois_group();
618
619        let g1 = ring_factor1.from_representative(index_ring.representative(galois_element) as i64);
620        let g2 = ring_factor2.from_representative(index_ring.representative(galois_element) as i64);
621        let mut tmp = Vec::with_capacity_in(self.rank(), &self.tensor_factor1.allocator);
622        tmp.resize_with(self.rank(), || self.base_ring().zero());
623        for i in 0..self.tensor_factor2.rank() {
624            self.tensor_factor1.permute_galois_action(
625                SubvectorView::new(&src).restrict((i * self.tensor_factor1.rank())..((i + 1) * self.tensor_factor1.rank())), 
626                &mut tmp[(i * self.tensor_factor1.rank())..((i + 1) * self.tensor_factor1.rank())], 
627                g1
628            );
629        }
630        for j in 0..self.tensor_factor1.rank() {
631            self.tensor_factor2.permute_galois_action(
632                SubvectorView::new(&tmp[..]).restrict(j..).step_by_view(self.tensor_factor1.rank()), 
633                SubvectorView::new(&mut dst).restrict(j..).step_by_view(self.tensor_factor1.rank()), 
634                g2
635            );
636        }
637    }
638}
639
640#[cfg(test)]
641use feanor_math::assert_el_eq;
642#[cfg(test)]
643use crate::ciphertext_ring::double_rns_ring;
644#[cfg(test)]
645use crate::ciphertext_ring::single_rns_ring;
646#[cfg(test)]
647use crate::number_ring::quotient;
648#[cfg(test)]
649use crate::ring_literal;
650
651#[test]
652fn test_odd_cyclotomic_double_rns_ring() {
653    double_rns_ring::test_with_number_ring(OddCyclotomicNumberRing::new(5));
654    double_rns_ring::test_with_number_ring(OddCyclotomicNumberRing::new(7));
655    double_rns_ring::test_with_number_ring(CompositeCyclotomicNumberRing::new(3, 5));
656    double_rns_ring::test_with_number_ring(CompositeCyclotomicNumberRing::new(3, 7));
657}
658
659#[test]
660fn test_odd_cyclotomic_single_rns_ring() {
661    single_rns_ring::test_with_number_ring(OddCyclotomicNumberRing::new(5));
662    single_rns_ring::test_with_number_ring(OddCyclotomicNumberRing::new(7));
663    single_rns_ring::test_with_number_ring(CompositeCyclotomicNumberRing::new(3, 5));
664    single_rns_ring::test_with_number_ring(CompositeCyclotomicNumberRing::new(3, 7));
665}
666
667#[test]
668fn test_odd_cyclotomic_decomposition_ring() {
669    quotient::test_with_number_ring(OddCyclotomicNumberRing::new(5));
670    quotient::test_with_number_ring(OddCyclotomicNumberRing::new(7));
671    let ring = CompositeCyclotomicNumberRing::new(3, 5);
672    quotient::test_with_number_ring(ring);
673    quotient::test_with_number_ring(CompositeCyclotomicNumberRing::new(3, 7));
674}
675
676#[test]
677fn test_small_coeff_basis_conversion() {
678    let ring = zn_64::Zn::new(241);
679    let number_ring = CompositeCyclotomicNumberRing::new(3, 5);
680    let decomposition = number_ring.mod_p(ring);
681
682    let arr_create = |data: [i32; 8]| std::array::from_fn::<_, 8, _>(|i| ring.int_hom().map(data[i]));
683    let assert_arr_eq = |fst: [zn_64::ZnEl; 8], snd: [zn_64::ZnEl; 8]| assert!(
684        fst.iter().zip(snd.iter()).all(|(x, y)| ring.eq_el(x, y)),
685        "expected {:?} = {:?}",
686        std::array::from_fn::<_, 8, _>(|i| ring.format(&fst[i])),
687        std::array::from_fn::<_, 8, _>(|i| ring.format(&snd[i]))
688    );
689
690    let original = arr_create([1, 0, 0, 0, 0, 0, 0, 0]);
691    let expected = arr_create([1, 0, 0, 0, 0, 0, 0, 0]);
692    let mut actual = original;
693    decomposition.coeff_basis_to_small_basis(&mut actual);
694    assert_arr_eq(expected, actual);
695    decomposition.small_basis_to_coeff_basis(&mut actual);
696    assert_arr_eq(original, actual);
697    
698    // 𝝵_15 = 𝝵_3^-1 ⊗ 𝝵_5^2 = (-1 - 𝝵_3) ⊗ 𝝵_5^2
699    let original = arr_create([0, 1, 0, 0, 0, 0, 0, 0]);
700    let expected = arr_create([0, 0, 0, 0, 240, 240, 0, 0]);
701    let mut actual = original;
702    decomposition.coeff_basis_to_small_basis(&mut actual);
703    assert_arr_eq(expected, actual);
704    decomposition.small_basis_to_coeff_basis(&mut actual);
705    assert_arr_eq(original, actual);
706
707    let original = arr_create([0, 0, 240, 0, 0, 0, 0, 0]);
708    let expected = arr_create([0, 1, 0, 1, 0, 1, 0, 1]);
709    let mut actual = original;
710    decomposition.coeff_basis_to_small_basis(&mut actual);
711    assert_arr_eq(expected, actual);
712    decomposition.small_basis_to_coeff_basis(&mut actual);
713    assert_arr_eq(original, actual);
714
715    let original = arr_create([0, 0, 0, 1, 0, 0, 0, 0]);
716    let expected = arr_create([0, 0, 1, 0, 0, 0, 0, 0]);
717    let mut actual = original;
718    decomposition.coeff_basis_to_small_basis(&mut actual);
719    assert_arr_eq(expected, actual);
720    decomposition.small_basis_to_coeff_basis(&mut actual);
721    assert_arr_eq(original, actual);
722
723    let original = arr_create([0, 0, 0, 0, 0, 1, 0, 0]);
724    let expected = arr_create([0, 1, 0, 0, 0, 0, 0, 0]);
725    let mut actual = original;
726    decomposition.coeff_basis_to_small_basis(&mut actual);
727    assert_arr_eq(expected, actual);
728    decomposition.small_basis_to_coeff_basis(&mut actual);
729    assert_arr_eq(original, actual);
730}
731
732#[test]
733fn test_permute_galois_automorphism() {
734    let Fp = zn_64::Zn::new(257);
735    let R = quotient::NumberRingQuotientBase::new(OddCyclotomicNumberRing::new(7), Fp);
736    let gal_el = |x: i64| R.galois_group().from_representative(x);
737
738    assert_el_eq!(R, ring_literal(&R, &[0, 0, 1, 0, 0, 0]), R.get_ring().apply_galois_action(&ring_literal(&R, &[0, 1, 0, 0, 0, 0]), gal_el(2)));
739    assert_el_eq!(R, ring_literal(&R, &[0, 0, 0, 1, 0, 0]), R.get_ring().apply_galois_action(&ring_literal(&R, &[0, 1, 0, 0, 0, 0]), gal_el(3)));
740    assert_el_eq!(R, ring_literal(&R, &[0, 0, 0, 0, 1, 0]), R.get_ring().apply_galois_action(&ring_literal(&R, &[0, 0, 1, 0, 0, 0]), gal_el(2)));
741    assert_el_eq!(R, ring_literal(&R, &[-1, -1, -1, -1, -1, -1]), R.get_ring().apply_galois_action(&ring_literal(&R, &[0, 0, 1, 0, 0, 0]), gal_el(3)));
742
743    let R = quotient::NumberRingQuotientBase::new(CompositeCyclotomicNumberRing::new(5, 3), Fp);
744    let gal_el = |x: i64| R.galois_group().from_representative(x);
745
746    assert_el_eq!(R, ring_literal(&R, &[0, 0, 1, 0, 0, 0, 0, 0]), R.get_ring().apply_galois_action(&ring_literal(&R, &[0, 1, 0, 0, 0, 0, 0, 0]), gal_el(2)));
747    assert_el_eq!(R, ring_literal(&R, &[0, 0, 0, 0, 1, 0, 0, 0]), R.get_ring().apply_galois_action(&ring_literal(&R, &[0, 1, 0, 0, 0, 0, 0, 0]), gal_el(4)));
748    assert_el_eq!(R, ring_literal(&R, &[-1, 1, 0, -1, 1, -1, 0, 1]), R.get_ring().apply_galois_action(&ring_literal(&R, &[0, 1, 0, 0, 0, 0, 0, 0]), gal_el(8)));
749    assert_el_eq!(R, ring_literal(&R, &[-1, 1, 0, -1, 1, -1, 0, 1]), R.get_ring().apply_galois_action(&ring_literal(&R, &[0, 0, 0, 0, 1, 0, 0, 0]), gal_el(2)));
750}