he_ring/number_ring/hypercube/
structure.rs

1use std::cmp::max;
2
3use feanor_math::algorithms::discrete_log::discrete_log;
4use feanor_math::algorithms::eea::{signed_gcd, signed_lcm};
5use feanor_math::divisibility::DivisibilityRingStore;
6use feanor_math::iters::clone_slice;
7use feanor_math::algorithms::int_factor::factor;
8use feanor_math::iters::multi_cartesian_product;
9use feanor_math::ring::*;
10use feanor_math::rings::finite::FiniteRingStore;
11use feanor_math::rings::zn::zn_64::*;
12use feanor_math::homomorphism::*;
13use feanor_math::rings::zn::*;
14use feanor_math::rings::zn::zn_rns;
15use feanor_math::seq::*;
16use feanor_math::wrapper::RingElementWrapper;
17use serde::{Deserialize, Serialize};
18
19use crate::{cyclotomic::*, ZZi64};
20use crate::euler_phi;
21
22///
23/// Represents a hypercube, which is a map
24/// ```text
25///   h: { 0, ..., m1 - 1 } x ... x { 0, ..., mr - 1 } -> (Z/nZ)^*
26///                      a1,  ...,  ar                 -> prod_i gi^ai
27/// ```
28/// such that the composition `(mod <p>) ∘ h` is a bijection.
29/// 
30/// We use the following notation:
31///  - `n` and `p` as above
32///  - `d` is the order of `<p>` as subgroup of `(Z/nZ)*`
33///  - `mi` is the length of the `i`-th "hypercube dimension" as above
34///  - `gi` is the generator of the `i`-th hypercube dimension
35/// 
36#[derive(Clone)]
37pub struct HypercubeStructure {
38    pub(super) galois_group: CyclotomicGaloisGroup,
39    pub(super) p: CyclotomicGaloisGroupEl,
40    pub(super) d: usize,
41    pub(super) ms: Vec<usize>,
42    pub(super) ord_gs: Vec<usize>,
43    pub(super) gs: Vec<CyclotomicGaloisGroupEl>,
44    pub(super) representations: Vec<(CyclotomicGaloisGroupEl, /* first element is frobenius */ Box<[usize]>)>,
45    pub(super) choice: HypercubeTypeData
46}
47
48#[derive(Clone, Serialize, Deserialize, PartialEq)]
49pub enum HypercubeTypeData {
50    Generic, 
51    /// if the hypercube dimensions correspond directly to prime power factors of `n`, 
52    /// we store this correspondence here, as it can be used to explicitly work with the
53    /// relationship between hypercube dimensions and tensor factors of `Z[𝝵]`
54    CyclotomicTensorProductHypercube(Vec<(i64, usize)>)
55}
56
57impl PartialEq for HypercubeStructure {
58    fn eq(&self, other: &Self) -> bool {
59        self.galois_group == other.galois_group && 
60            self.galois_group.eq_el(self.p, other.p) &&
61            self.d == other.d && 
62            self.ms == other.ms &&
63            self.gs.iter().zip(other.gs.iter()).all(|(l, r)| self.galois_group.eq_el(*l, *r)) &&
64
65            self.choice == other.choice
66    }
67}
68
69impl HypercubeStructure {
70
71    pub fn new(galois_group: CyclotomicGaloisGroup, p: CyclotomicGaloisGroupEl, d: usize, ms: Vec<usize>, gs: Vec<CyclotomicGaloisGroupEl>) -> Self {
72        assert_eq!(ms.len(), gs.len());
73        // check order of p
74        assert!(galois_group.is_identity(galois_group.pow(p, d as i64)));
75        for (factor, _) in factor(ZZi64, d as i64) {
76            assert!(!galois_group.is_identity(galois_group.pow(p, d as i64 / factor)));
77        }
78        // check whether the given values indeed define a bijection modulo `<p>`
79        let mut all_elements = multi_cartesian_product([(0..d)].into_iter().chain(ms.iter().map(|mi| 0..*mi)), |idxs| (
80            galois_group.prod(idxs.iter().zip([&p].into_iter().chain(gs.iter())).map(|(i, g)| galois_group.pow(*g, *i as i64))),
81            clone_slice(idxs)
82        ), |_, x| *x).collect::<Vec<_>>();
83        all_elements.sort_unstable_by_key(|(g, _)| galois_group.representative(*g));
84        assert!((1..all_elements.len()).all(|i| !galois_group.eq_el(all_elements[i - 1].0, all_elements[i].0)), "not a bijection");
85        assert_eq!(galois_group.group_order(), all_elements.len());
86
87        return Self {
88            galois_group: galois_group,
89            p: p,
90            d: d,
91            ms: ms,
92            ord_gs: gs.iter().map(|g| galois_group.element_order(*g)).collect(),
93            gs: gs,
94            choice: HypercubeTypeData::Generic,
95            representations: all_elements
96        };
97    }
98
99    ///
100    /// Computes "the" Halevi-Shoup hypercube as described in <https://ia.cr/2014/873>.
101    /// 
102    /// Note that the Halevi-Shoup hypercube is unique except for the ordering of prime
103    /// factors of `n`. This function uses a deterministic but unspecified ordering.
104    /// 
105    pub fn halevi_shoup_hypercube(galois_group: CyclotomicGaloisGroup, p: i64) -> Self {
106
107        ///
108        /// Stores information about a factor in the representation `(Z/nZ)* = (Z/n1Z)* x ... (Z/nrZ)*`
109        /// and about `<p> <= (Z/niZ)^*` (considering `p` to be the "orthogonal" projection of `p in (Z/nZ)*`
110        /// into `(Z/niZ)*`).
111        /// 
112        /// The one exception is the case `ni = 2^e`, since `(Z/2^eZ)*` is not cyclic (for `e > 2`).
113        /// We then store it as a single factor (if `(Z/2^eZ)* = <p, g>` for some generator `g`) or as
114        /// two factors otherwise.
115        /// 
116        struct HypercubeDimension {
117            g_main: ZnEl,
118            order_of_projected_p: i64,
119            group_order: i64,
120            factor_n: (i64, usize)
121        }
122
123        let n = galois_group.n() as i64;
124        assert!(signed_gcd(n, p, ZZi64) == 1, "n and p must be coprime");
125
126        // the unit group (Z/nZ)* decomposes as X (Z/niZ)*; this gives rise to the natural hypercube structure,
127        // although technically many possible hypercube structures are possible
128        let mut factorization = factor(ZZi64, n);
129        // this makes debugging easier, since we have a canonical order
130        factorization.sort_unstable_by_key(|(p, _)| *p);
131        let Zn_rns = zn_rns::Zn::new(factorization.iter().map(|(q, k)| Zn::new(ZZi64.pow(*q, *k) as u64)).collect(), ZZi64);
132        let Zn = Zn::new(n as u64);
133        let iso = Zn.into_can_hom(zn_big::Zn::new(ZZi64, n)).ok().unwrap().compose((&Zn_rns).into_can_iso(zn_big::Zn::new(ZZi64, n)).ok().unwrap());
134        let from_crt = |index: usize, value: ZnEl| iso.map(Zn_rns.from_congruence((0..factorization.len()).map(|j| if j == index { value } else { Zn_rns.at(j).one() })));
135
136        let mut dimensions = Vec::new();
137        for (i, (q, k)) in factorization.iter().enumerate() {
138            let Zqk = Zn_rns.at(i);
139            if *q == 2 {
140                // `(Z/2^kZ)*` is an exception, since it is not cyclic
141                if *k == 1 {
142                    continue;
143                } else if *k == 2 {
144                    unimplemented!()
145                } else {
146                    // `(Z/2^kZ)*` is isomorphic to `<g1> x <g2>` where `<g1> ~ Z/2^(k - 2)Z` and `<g2> ~ Z/2Z`
147                    let g1 = Zqk.int_hom().map(5);
148                    let ord_g1 = ZZi64.pow(*q, *k as usize - 2);
149                    let g2 = Zqk.can_hom(&ZZi64).unwrap().map(-1);
150                    if p % 4 == 1 {
151                        // `p` is in `<g1>`
152                        let logg1_p = unit_group_dlog(Zqk, g1, ord_g1, Zqk.can_hom(&ZZi64).unwrap().map(p)).unwrap();
153                        dimensions.push(HypercubeDimension {
154                            order_of_projected_p: ord_g1 / signed_gcd(logg1_p, ord_g1, &ZZi64), 
155                            group_order: ord_g1,
156                            g_main: from_crt(i, g1),
157                            factor_n: (2, *k),
158                        });
159                        dimensions.push(HypercubeDimension {
160                            order_of_projected_p: 1, 
161                            group_order: 2,
162                            g_main: from_crt(i, g2),
163                            factor_n: (2, *k),
164                        });
165                    } else {
166                        // `<p, g1> = (Z/2^kZ)*` and `p * g2 in <g1>`
167                        let logg1_pg2 = unit_group_dlog(Zqk, g1, ord_g1, Zqk.mul(Zqk.can_hom(&ZZi64).unwrap().map(p), g2)).unwrap();
168                        dimensions.push(HypercubeDimension {
169                            order_of_projected_p: max(ord_g1 / signed_gcd(logg1_pg2, ord_g1, &ZZi64), 2),
170                            group_order: 2 * ord_g1,
171                            g_main: from_crt(i, g1),
172                            factor_n: (2, *k)
173                        });
174                    }
175                }
176            } else {
177                // `(Z/q^kZ)*` is cyclic
178                let g = get_multiplicative_generator(*Zqk, &[(*q, *k)]);
179                let ord_g = euler_phi(&[(*q, *k)]);
180                let logg_p = unit_group_dlog(Zqk, g, ord_g, Zqk.can_hom(&ZZi64).unwrap().map(p)).unwrap();
181                let ord_p = ord_g / signed_gcd(logg_p, ord_g, ZZi64);
182                dimensions.push(HypercubeDimension {
183                    order_of_projected_p: ord_p, 
184                    group_order: ord_g,
185                    g_main: from_crt(i, g),
186                    factor_n: (*q, *k)
187                });
188            }
189        }
190
191        dimensions.sort_by_key(|dim| -(dim.order_of_projected_p as i64));
192        let mut current_d = 1;
193        let lengths = dimensions.iter().map(|dim| {
194            let new_d = signed_lcm(current_d, dim.order_of_projected_p as i64, ZZi64);
195            let len = dim.group_order as i64 / (new_d / current_d);
196            current_d = new_d;
197            return len as usize;
198        }).collect::<Vec<_>>();
199
200        let mut result = Self::new(
201            galois_group,
202            galois_group.from_representative(p),
203            current_d as usize,
204            lengths,
205            dimensions.iter().map(|dim| galois_group.from_ring_el(dim.g_main)).collect()
206        );
207        result.choice = HypercubeTypeData::CyclotomicTensorProductHypercube(dimensions.iter().map(|dim| dim.factor_n).collect());
208        return result;
209    }
210
211    ///
212    /// Applies the hypercube structure map to the unit vector multiple `steps * e_(dim_idx)`,
213    /// i.e. computes the galois automorphism corresponding to the shift by `steps` steps
214    /// along the `dim_idx`-th hypercube dimension.
215    /// 
216    pub fn map_1d(&self, dim_idx: usize, steps: i64) -> CyclotomicGaloisGroupEl {
217        assert!(dim_idx < self.dim_count());
218        self.galois_group.pow(self.gs[dim_idx], steps)
219    }
220
221    ///
222    /// Applies the hypercube structure map to the given vector.
223    /// 
224    /// It is not enforced that the entries of the vector are contained in
225    /// `{ 0, ..., m1 - 1 } x ... x { 0, ..., mr - 1 }`, for values outside this
226    /// range the natural extension of `h` to `Z^r` is used, i.e.
227    /// ```text
228    ///   h:       Z^r      ->   (Z/nZ)^*
229    ///      a1,  ...,  ar  -> prod_i gi^ai
230    /// ```
231    /// 
232    pub fn map(&self, idxs: &[i64]) -> CyclotomicGaloisGroupEl {
233        assert_eq!(self.ms.len(), idxs.len());
234        self.galois_group.prod(idxs.iter().zip(self.gs.iter()).map(|(i, g)| self.galois_group.pow(*g, *i)))
235    }
236
237    ///
238    /// Same as [`HypercubeStructure::map()`], but for a vector with
239    /// unsigned entries.
240    /// 
241    pub fn map_usize(&self, idxs: &[usize]) -> CyclotomicGaloisGroupEl {
242        assert_eq!(self.ms.len(), idxs.len());
243        self.galois_group.prod(idxs.iter().zip(self.gs.iter()).map(|(i, g)| self.galois_group.pow(*g, *i as i64)))
244    }
245
246    ///
247    /// Computes the "standard preimage" of the given `g` under `h`.
248    /// 
249    /// This is the vector `(a0, a1, ..., ar)` such that `g = p^a0 h(a1, ..., ar)` and
250    /// each `ai` is within `{ 0, ..., mi - 1 }`.
251    /// 
252    pub fn std_preimage(&self, g: CyclotomicGaloisGroupEl) -> &[usize] {
253        let idx = self.representations.binary_search_by_key(&self.galois_group.representative(g), |(g, _)| self.galois_group.representative(*g)).unwrap();
254        return &self.representations[idx].1;
255    }
256
257    ///
258    /// Returns whether each dimension of the hypercube corresponds to a factor `ni` of
259    /// `n` (with `ni` coprime to `n/ni`). This is the case for the Halevi-Shoup hypercube,
260    /// and very useful in some situations. If this is the case, you can query the factor
261    /// of `n` corresponding to some dimension by [`HypercubeStructure::factor_of_n()`].
262    /// 
263    pub fn is_tensor_product_compatible(&self) -> bool {
264        match self.choice {
265            HypercubeTypeData::CyclotomicTensorProductHypercube(_) => true,
266            HypercubeTypeData::Generic => false
267        }
268    }
269
270    ///
271    /// Returns the factor `ni` of `n` (coprime to `n/ni`) which the `i`-th hypercube
272    /// dimension corresponds to. This is only applicable if the hypercube was constructed
273    /// from a (partial) factorization of `n`, i.e. [`HypercubeStructure::is_tensor_product_compatible()`]
274    /// returns true. Otherwise, this function will return `None`.
275    /// 
276    pub fn factor_of_n(&self, dim_idx: usize) -> Option<i64> {
277        assert!(dim_idx < self.dim_count());
278        match &self.choice {
279            HypercubeTypeData::CyclotomicTensorProductHypercube(factors_n) => Some(ZZi64.pow(factors_n[dim_idx].0, factors_n[dim_idx].1)),
280            HypercubeTypeData::Generic => None
281        }
282    }
283
284    ///
285    /// Returns `p` as an element of `(Z/nZ)*`.
286    /// 
287    pub fn p(&self) -> CyclotomicGaloisGroupEl {
288        self.p
289    }
290
291    ///
292    /// Returns the Galois automorphism corresponding to the power-of-`p^power`
293    /// frobenius automorphism of the slot ring.
294    /// 
295    pub fn frobenius(&self, power: i64) -> CyclotomicGaloisGroupEl {
296        self.galois_group.pow(self.p(), power)
297    }
298
299    ///
300    /// Returns the rank `d` of the slot ring.
301    /// 
302    pub fn d(&self) -> usize {
303        self.d
304    }
305
306    ///
307    /// Returns the length `mi` of the `i`-th hypercube dimension.
308    /// 
309    pub fn m(&self, i: usize) -> usize {
310        assert!(i < self.ms.len());
311        self.ms[i]
312    }
313
314    ///
315    /// Returns the generator `gi` corresponding to the `i`-th hypercube dimension.
316    /// 
317    pub fn g(&self, i: usize) -> CyclotomicGaloisGroupEl {
318        assert!(i < self.ms.len());
319        self.gs[i]
320    }
321
322    ///
323    /// Returns the order of `gi` in the group `(Z/nZ)*`.
324    /// 
325    pub fn ord_g(&self, i: usize) -> usize {
326        assert!(i < self.ms.len());
327        self.ord_gs[i]
328    }
329
330    ///
331    /// Returns `n`, i.e. the multiplicative order of the root of unity of the main ring.
332    /// 
333    pub fn n(&self) -> usize {
334        self.galois_group().n()
335    }
336
337    ///
338    /// Returns the number of dimensions in the hypercube.
339    /// 
340    pub fn dim_count(&self) -> usize {
341        self.gs.len()
342    }
343
344    ///
345    /// Returns the Galois group isomorphic to `(Z/nZ)*` that this hypercube
346    /// describes.
347    /// 
348    pub fn galois_group(&self) -> &CyclotomicGaloisGroup {
349        &self.galois_group
350    }
351
352    ///
353    /// Returns the number of elements of `{ 0, ..., m1 - 1 } x ... x { 0, ..., mr - 1 }`
354    /// or equivalently `(Z/nZ)*/<p>`, which is equal to the to the number of slots of 
355    /// `Fp[X]/(Phi_n(X))`.
356    /// 
357    pub fn element_count(&self) -> usize {
358        ZZi64.prod(self.ms.iter().map(|mi| *mi as i64)) as usize
359    }
360
361    ///
362    /// Creates an iterator that yields a value for each element of `{ 0, ..., m1 - 1 } x ... x { 0, ..., mr - 1 }` 
363    /// resp. `(Z/nZ)*/<p>`. Hence, these elements correspond to the slots of `Fp[X]/(Phi_n(X))`.
364    /// 
365    /// The given closure will be called on each element of `{ 0, ..., m1 - 1 } x ... x { 0, ..., mr - 1 }`.
366    /// The returned iterator will iterate over the results of the closure.
367    /// 
368    pub fn hypercube_iter<'b, G, T>(&'b self, for_slot: G) -> impl ExactSizeIterator<Item = T> + use<'b, G, T>
369        where G: 'b + Clone + FnMut(&[usize]) -> T,
370            T: 'b
371    {
372        let mut it = multi_cartesian_product(
373            self.ms.iter().map(|l| (0..*l)),
374            for_slot,
375            |_, x| *x
376        );
377        (0..self.element_count()).map(move |_| it.next().unwrap())
378    }
379
380    ///
381    /// Creates an iterator that one representative of each element of `(Z/nZ)*/<p>`, which
382    /// also is in the image of this hypercube structure.
383    /// 
384    /// The order is compatible with [`HypercubeStructure::hypercube_iter()`].
385    /// 
386    pub fn element_iter<'b>(&'b self) -> impl ExactSizeIterator<Item = CyclotomicGaloisGroupEl> + use<'b> {
387        self.hypercube_iter(|idxs| self.map_usize(idxs))
388    }
389}
390
391pub fn get_multiplicative_generator(ring: Zn, factorization: &[(i64, usize)]) -> ZnEl {
392    assert_eq!(*ring.modulus(), ZZi64.prod(factorization.iter().map(|(p, e)| ZZi64.pow(*p, *e))));
393    assert!(*ring.modulus() % 2 == 1, "for even n, Z/nZ* is either equal to Z/(n/2)Z* or not cyclic");
394    let mut rng = oorandom::Rand64::new(ring.integer_ring().default_hash(ring.modulus()) as u128);
395    let order = euler_phi(factorization);
396    let order_factorization = factor(&ZZi64, order);
397    'test_generator: loop {
398        let potential_generator = ring.random_element(|| rng.rand_u64());
399        if !ring.is_unit(&potential_generator) {
400            continue 'test_generator;
401        }
402        for (p, _) in &order_factorization {
403            if ring.is_one(&ring.pow(potential_generator, (order / p) as usize)) {
404                continue 'test_generator;
405            }
406        }
407        return potential_generator;
408    }
409}
410
411pub fn unit_group_dlog(ring: &Zn, base: ZnEl, order: i64, value: ZnEl) -> Option<i64> {
412    discrete_log(
413        RingElementWrapper::new(&ring, value), 
414        &RingElementWrapper::new(&ring, base), 
415        order, 
416        |x, y| x * y, 
417        RingElementWrapper::new(&ring, ring.one())
418    )
419}
420
421#[test]
422fn test_halevi_shoup_hypercube() {
423    let galois_group = CyclotomicGaloisGroup::new(11 * 31);
424    let hypercube_structure = HypercubeStructure::halevi_shoup_hypercube(galois_group, 2);
425    assert_eq!(10, hypercube_structure.d());
426    assert_eq!(2, hypercube_structure.dim_count());
427
428    assert_eq!(1, hypercube_structure.m(0));
429    assert_eq!(30, hypercube_structure.m(1));
430
431    let galois_group = CyclotomicGaloisGroup::new(32);
432    let hypercube_structure = HypercubeStructure::halevi_shoup_hypercube(galois_group, 7);
433    assert_eq!(4, hypercube_structure.d());
434    assert_eq!(1, hypercube_structure.dim_count());
435
436    assert_eq!(4, hypercube_structure.m(0));
437
438    let galois_group = CyclotomicGaloisGroup::new(32);
439    let hypercube_structure = HypercubeStructure::halevi_shoup_hypercube(galois_group, 17);
440    assert_eq!(2, hypercube_structure.d());
441    assert_eq!(2, hypercube_structure.dim_count());
442
443    assert_eq!(4, hypercube_structure.m(0));
444    assert_eq!(2, hypercube_structure.m(1));
445}
446
447#[test]
448fn test_serialization() {
449    for hypercube in [
450        HypercubeStructure::halevi_shoup_hypercube(CyclotomicGaloisGroup::new(11 * 31), 2),
451        HypercubeStructure::halevi_shoup_hypercube(CyclotomicGaloisGroup::new(32), 7),
452        HypercubeStructure::halevi_shoup_hypercube(CyclotomicGaloisGroup::new(32), 17)
453    ] {
454        let serializer = serde_assert::Serializer::builder().is_human_readable(true).build();
455        let tokens = hypercube.serialize(&serializer).unwrap();
456        let mut deserializer = serde_assert::Deserializer::builder(tokens).is_human_readable(true).build();
457        let deserialized_hypercube = HypercubeStructure::deserialize(&mut deserializer).unwrap();
458
459        assert!(hypercube.galois_group() == deserialized_hypercube.galois_group());
460        assert_eq!(hypercube.dim_count(), deserialized_hypercube.dim_count());
461        assert_eq!(hypercube.is_tensor_product_compatible(), deserialized_hypercube.is_tensor_product_compatible());
462        for i in 0..hypercube.dim_count() {
463            assert_eq!(hypercube.m(i), deserialized_hypercube.m(i));
464            assert!(hypercube.galois_group().eq_el(hypercube.g(i), deserialized_hypercube.g(i)));
465            assert_eq!(hypercube.ord_g(i), deserialized_hypercube.ord_g(i));
466        }
467
468        let serializer = serde_assert::Serializer::builder().is_human_readable(false).build();
469        let tokens = hypercube.serialize(&serializer).unwrap();
470        let mut deserializer = serde_assert::Deserializer::builder(tokens).is_human_readable(false).build();
471        let deserialized_hypercube = HypercubeStructure::deserialize(&mut deserializer).unwrap();
472
473        assert!(hypercube.galois_group() == deserialized_hypercube.galois_group());
474        assert_eq!(hypercube.dim_count(), deserialized_hypercube.dim_count());
475        assert_eq!(hypercube.is_tensor_product_compatible(), deserialized_hypercube.is_tensor_product_compatible());
476        for i in 0..hypercube.dim_count() {
477            assert_eq!(hypercube.m(i), deserialized_hypercube.m(i));
478            assert!(hypercube.galois_group().eq_el(hypercube.g(i), deserialized_hypercube.g(i)));
479            assert_eq!(hypercube.ord_g(i), deserialized_hypercube.ord_g(i));
480        }
481    }
482}