qfall_math/integer/mat_z/
basis_reductions.rs

1// Copyright © 2025 Niklas Siemer
2//
3// This file is part of qFALL-math.
4//
5// qFALL-math is free software: you can redistribute it and/or modify it under
6// the terms of the Mozilla Public License Version 2.0 as published by the
7// Mozilla Foundation. See <https://mozilla.org/en-US/MPL/2.0/>.
8
9//! This module contains implementations of basis reduction algorithms.
10
11use super::MatZ;
12use crate::{rational::Q, traits::MatrixDimensions};
13use flint_sys::fmpz_mat::{fmpz_mat_is_reduced, fmpz_mat_lll_storjohann};
14
15impl MatZ {
16    /// Performs the (modified Storjohann) LLL algorithm on the matrix `self`.
17    /// This algorithm expects `self` to be a basis.
18    /// The reduced matrix is a (δ, η)-reduced basis.
19    ///
20    /// Let the matrix be [b_1, b_2, ..., b_n]. Then, it is (δ, η)-LLL-reduced if
21    /// - for any i > j, we have |μ_{i,j}| <= η,
22    /// - for any i < n, we have δ|b_i*|^2 <= |b_{i+1}* + μ_{i+1,i}b_i*|^2,
23    ///
24    /// where μ_{i,j} =〈b_i, b_j*〉/〈b_j*, b_j*〉and b_i* is the i-th vector
25    /// of the Gram-Schmidt orthogonalization of our matrix.
26    ///
27    /// Parameters:
28    /// - `delta`: mainly defines the quality of the reduced
29    ///   basis with higher quality the closer it's chosen to 1.
30    ///   Needs to be chosen between 0.25 < δ <= 1.
31    /// - `eta`: defines the maximum deviation per vector
32    ///   from the Gram-Schmidt orthogonalisation.
33    ///   Needs to be chosen between 0.5 <= η < √δ.
34    ///
35    /// Choosing δ=0.99 and η=0.501 optimizes the quality of the basis and
36    /// is a good choice to start from. Decreasing δ or increasing η will
37    /// increase efficiency but decrease the quality of the reduced basis.
38    ///
39    /// # Examples
40    /// ```
41    /// use qfall_math::integer::MatZ;
42    ///
43    /// let mut matrix = MatZ::sample_uniform(2, 2, 0, 65537).unwrap();
44    ///
45    /// let reduced_matrix = matrix.lll(0.75, 0.501);
46    /// ```
47    ///
48    /// # Panics ...
49    /// - if δ is not in (0.25, 1].
50    /// - if η is not in [0.5, √δ).
51    /// - if `self` can't be a basis, i.e. #rows < #columns.
52    pub fn lll(&self, delta: impl Into<Q>, eta: impl Into<Q>) -> MatZ {
53        let delta: Q = delta.into();
54        let eta: Q = eta.into();
55
56        if delta > Q::ONE || delta <= Q::from(0.25) {
57            panic!("δ needs to be chosen between 0.25 < δ <= 1.");
58        }
59        if eta < Q::from(0.5) || eta >= delta.sqrt() {
60            panic!("η needs to be chosen between 0.5 <= η < √δ.");
61        }
62        if self.get_num_rows() < self.get_num_columns() {
63            panic!("The n-by-m matrix is required to form a basis, i.e. n can't be larger than m.");
64        }
65
66        // ensure that LLL operates on column vectors as basis vectors
67        let mut transposed = self.transpose();
68        unsafe {
69            fmpz_mat_lll_storjohann(&mut transposed.matrix, &delta.value, &eta.value);
70        };
71
72        transposed.transpose()
73    }
74
75    /// Checks if the basis `self` is (δ, η)-reduced.
76    ///
77    /// Definition of (δ, η)-reduced:
78    /// Let the matrix be [b_1, b_2, ..., b_n]. Then, it is (δ, η)-LLL-reduced if
79    /// - for any i > j, we have |μ_{i,j}| <= η,
80    /// - for any i < n, we have δ|b_i*|^2 <= |b_{i+1}* + μ_{i+1,i}b_i*|^2,
81    ///
82    /// where μ_{i,j} =〈b_i, b_j*〉/〈b_j*, b_j*〉and b_i* is the i-th vector
83    /// of the Gram-Schmidt orthogonalization of our matrix.
84    ///
85    /// Parameters:
86    /// - `delta`: mainly defines the quality of the reduced
87    ///   basis with higher quality the closer it's chosen to 1.
88    ///   If δ > 1, the output will always be `false`.
89    /// - `eta`: defines the maximum deviation per vector
90    ///   from the Gram-Schmidt orthogonalisation.
91    ///   If η < 0, the output will always be `false`.
92    ///
93    /// If `self` has `|rows| > |columns|`, it can't be a basis and therefore,
94    /// the output of this algorithm will always be `false`.
95    ///
96    /// Returns `true` if the matrix is a basis that is (δ, η)-reduced.
97    /// Otherwise, it returns `false`.
98    ///
99    /// # Examples
100    /// ```
101    /// use qfall_math::integer::MatZ;
102    ///
103    /// let mut matrix = MatZ::sample_uniform(2, 2, 0, 65537).unwrap();
104    /// let reduced_matrix = matrix.lll(0.75, 0.501);
105    ///
106    /// let check = reduced_matrix.is_reduced(0.75, 0.501);
107    ///
108    /// assert!(check);
109    /// ```
110    pub fn is_reduced(&self, delta: impl Into<Q>, eta: impl Into<Q>) -> bool {
111        let delta: Q = delta.into();
112        let eta: Q = eta.into();
113        let delta = f64::from(&delta);
114        let eta = f64::from(&eta);
115
116        // ensure that LLL-check operates on column vectors as basis vectors
117        let transposed = self.transpose();
118
119        0 != unsafe { fmpz_mat_is_reduced(&transposed.matrix, delta, eta) }
120    }
121}
122
123#[cfg(test)]
124mod test_lll {
125    use crate::integer::MatZ;
126
127    /// Ensure that a (0.99, 0.501)-LLL-reduced matrix is (0.99, 0.501)-reduced.
128    #[test]
129    fn reduction_works() {
130        let mat = MatZ::sample_uniform(5, 4, 0, 257).unwrap();
131
132        let reduced_mat = mat.lll(0.99, 0.501);
133
134        assert!(reduced_mat.is_reduced(0.99, 0.501));
135    }
136
137    /// Ensure that a (0.75, 0.75)-LLL-reduced matrix is (0.75, 0.75)-reduced
138    /// for large numbers.
139    #[test]
140    fn large_number() {
141        let mat = MatZ::sample_uniform(3, 3, i64::MAX, u64::MAX).unwrap();
142
143        let reduced_mat = mat.lll(0.75, 0.75);
144
145        assert!(reduced_mat.is_reduced(0.75, 0.75));
146    }
147
148    /// Ensures that choosing δ <= 0.25 will result in a panic.
149    #[test]
150    #[should_panic]
151    fn small_delta() {
152        let mat = MatZ::identity(2, 2);
153
154        mat.lll(0.25, 0.501);
155    }
156
157    /// Ensures that choosing δ > 1 will result in a panic.
158    #[test]
159    #[should_panic]
160    fn large_delta() {
161        let mat = MatZ::identity(3, 5);
162
163        mat.lll(1.01, 0.501);
164    }
165
166    /// Ensures that choosing η < 0.5 will result in a panic.
167    #[test]
168    #[should_panic]
169    fn small_eta() {
170        let mat = MatZ::identity(2, 2);
171
172        mat.lll(0.75, 0.499);
173    }
174
175    /// Ensures that choosing η > √δ will result in a panic.
176    #[test]
177    #[should_panic]
178    fn large_eta() {
179        let mat = MatZ::identity(4, 4);
180
181        mat.lll(0.75, 0.867);
182    }
183
184    /// Ensures that choosing the number of rows smaller than the number of columns results in a panic.
185    #[test]
186    #[should_panic]
187    fn not_basis() {
188        let mat = MatZ::identity(3, 4);
189
190        mat.lll(0.75, 0.75);
191    }
192}
193
194#[cfg(test)]
195mod test_is_reduced {
196    use crate::integer::MatZ;
197    use std::str::FromStr;
198
199    /// Ensure that the identity matrix is optimally reduced.
200    #[test]
201    fn identity_reduced() {
202        let mat = MatZ::identity(4, 4);
203
204        assert!(mat.is_reduced(1.0, 0.0));
205    }
206
207    /// Ensure that a (0.75, 0.75)-LLL-reduced matrix is (0.75, 0.75)-reduced.
208    #[test]
209    fn reduced_lll() {
210        let mat = MatZ::sample_uniform(3, 3, 0, 257).unwrap();
211        let reduced_mat = mat.lll(0.75, 0.75);
212
213        assert!(reduced_mat.is_reduced(0.75, 0.75));
214    }
215
216    /// Ensures that a fixed matrix that was previously uniformly generated within [0,257)
217    /// is not (0.75, 0.75)-reduced.
218    #[test]
219    fn not_reduced() {
220        let mat = MatZ::from_str("[[55, 115, 28],[115, 86, 60],[134, 141, 67]]").unwrap();
221
222        assert!(!mat.is_reduced(0.75, 0.75));
223    }
224
225    /// Ensures that [`MatZ::is_reduced`] always outputs `false`
226    /// for nonsense parameters, i.e.
227    /// - δ > 1,
228    /// - η < 0,
229    /// - #rows > #columns.
230    #[test]
231    fn nonsense_parameters() {
232        let square_mat = MatZ::identity(2, 2);
233        let non_square_mat = MatZ::identity(2, 3);
234
235        assert!(!square_mat.is_reduced(1.01, 0.75));
236        assert!(!square_mat.is_reduced(0.75, -0.01));
237        assert!(!non_square_mat.is_reduced(0.75, 0.75));
238    }
239}