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}