qfall_math/rational/mat_q/rounding.rs
1// Copyright 2024 Marvin Beckmann
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 includes functionality for rounding instances of [`MatQ`] entrywise.
10
11use super::MatQ;
12use crate::{
13 error::MathError,
14 integer::MatZ,
15 macros::seeded::seedable_function,
16 rational::Q,
17 traits::{MatrixDimensions, MatrixGetEntry, MatrixSetEntry},
18 utils::sample::uniform::SamplerRng,
19};
20
21impl MatQ {
22 /// Rounds all entries of the given rational matrix [`MatQ`] down to the next integer
23 /// as a [`MatZ`].
24 ///
25 /// # Examples
26 /// ```
27 /// use qfall_math::rational::MatQ;
28 /// use qfall_math::integer::MatZ;
29 /// use std::str::FromStr;
30 ///
31 /// let value = MatQ::from_str("[[5/2, 1]]").unwrap();
32 /// assert_eq!(MatZ::from_str("[[2, 1]]").unwrap(), value.floor());
33 ///
34 /// let value = MatQ::from_str("[[-5/2, 1]]").unwrap();
35 /// assert_eq!(MatZ::from_str("[[-3, 1]]").unwrap(), value.floor());
36 /// ```
37 pub fn floor(&self) -> MatZ {
38 let mut out = MatZ::new(self.get_num_rows(), self.get_num_columns());
39 for i in 0..out.get_num_rows() {
40 for j in 0..out.get_num_columns() {
41 let entry = unsafe { self.get_entry_unchecked(i, j) }.floor();
42 unsafe { out.set_entry_unchecked(i, j, entry) };
43 }
44 }
45 out
46 }
47
48 /// Rounds all entries of the given rational matrix [`MatQ`] up to the next integer
49 /// as a [`MatZ`].
50 ///
51 /// # Examples
52 /// ```
53 /// use qfall_math::rational::MatQ;
54 /// use qfall_math::integer::MatZ;
55 /// use std::str::FromStr;
56 ///
57 /// let value = MatQ::from_str("[[5/2, 1]]").unwrap();
58 /// assert_eq!(MatZ::from_str("[[3, 1]]").unwrap(), value.ceil());
59 ///
60 /// let value = MatQ::from_str("[[-5/2, 1]]").unwrap();
61 /// assert_eq!(MatZ::from_str("[[-2, 1]]").unwrap(), value.ceil());
62 /// ```
63 pub fn ceil(&self) -> MatZ {
64 let mut out = MatZ::new(self.get_num_rows(), self.get_num_columns());
65 for i in 0..out.get_num_rows() {
66 for j in 0..out.get_num_columns() {
67 let entry = unsafe { self.get_entry_unchecked(i, j) }.ceil();
68 unsafe { out.set_entry_unchecked(i, j, entry) };
69 }
70 }
71 out
72 }
73
74 /// Rounds all entries of the given rational matrix [`MatQ`] to the closest integer
75 /// as a [`MatZ`].
76 ///
77 /// # Examples
78 /// ```
79 /// use qfall_math::rational::MatQ;
80 /// use qfall_math::integer::MatZ;
81 /// use std::str::FromStr;
82 ///
83 /// let value = MatQ::from_str("[[5/2, 1]]").unwrap();
84 /// assert_eq!(MatZ::from_str("[[3, 1]]").unwrap(), value.round());
85 ///
86 /// let value = MatQ::from_str("[[-5/2, 1]]").unwrap();
87 /// assert_eq!(MatZ::from_str("[[-2, 1]]").unwrap(), value.round());
88 /// ```
89 pub fn round(&self) -> MatZ {
90 let mut out = MatZ::new(self.get_num_rows(), self.get_num_columns());
91 for i in 0..out.get_num_rows() {
92 for j in 0..out.get_num_columns() {
93 let entry = unsafe { self.get_entry_unchecked(i, j) }.round();
94 unsafe { out.set_entry_unchecked(i, j, entry) };
95 }
96 }
97 out
98 }
99
100 /// Returns a matrix, where each entry was simplified using [`Q::simplify`],
101 /// i.e. each entry becomes the smallest rational with the smallest denominator in the range
102 /// `\[entry - |precision|, entry + |precision|\]`.
103 ///
104 /// This function allows to free memory in exchange for the specified loss of
105 /// precision (see Example 3). Be aware that this loss of precision is propagated by
106 /// arithmetic operations depending on the size of the matrices.
107 /// This functions allows to trade precision for efficiency.
108 ///
109 /// This function ensures that simplifying does not change the sign of any entry in the matrix.
110 ///
111 /// Parameters:
112 /// - `precision`: the precision the new entries can differ from `self`.
113 /// Note that the absolute value is relevant, not the sign.
114 ///
115 /// Returns a new [`MatQ`] with each entry being the simplest fraction within the defined range.
116 ///
117 /// # Examples
118 /// ```
119 /// use qfall_math::rational::{MatQ, Q};
120 /// use qfall_math::traits::{MatrixGetEntry, MatrixSetEntry};
121 /// let mut matrix = MatQ::new(1, 2);
122 /// matrix.set_entry(0, 0, Q::from((17, 20))).unwrap();
123 /// let precision = Q::from((1, 20));
124 ///
125 /// let matrix_simplified = matrix.simplify(precision);
126 ///
127 /// assert_eq!(Q::from((4, 5)), matrix_simplified.get_entry(0, 0).unwrap());
128 /// ```
129 ///
130 /// ```
131 /// use qfall_math::rational::{MatQ, Q};
132 /// use qfall_math::traits::{MatrixGetEntry, MatrixSetEntry};
133 /// let mut matrix = MatQ::new(2, 1);
134 /// matrix.set_entry(0, 0, Q::from((3, 2))).unwrap();
135 ///
136 /// let mat_simplified = matrix.simplify(0.5);
137 ///
138 /// assert_eq!(Q::ONE, mat_simplified.get_entry(0, 0).unwrap());
139 /// ```
140 ///
141 /// ## Simplify with reasonable precision loss
142 /// This example uses [`Q::INV_MAX32`], i.e. a loss of precision of at most `1 / 2^31 - 2` behind the decimal point.
143 /// If you require higher precision, [`Q::INV_MAX62`] is available.
144 /// ```
145 /// use qfall_math::rational::{MatQ, Q};
146 /// use qfall_math::traits::{MatrixGetEntry, MatrixSetEntry};
147 /// let mut matrix = MatQ::new(1, 1);
148 /// matrix.set_entry(0, 0, Q::PI).unwrap();
149 ///
150 /// let mat_simplified = matrix.simplify(Q::INV_MAX32);
151 ///
152 /// let entry_simplified = mat_simplified.get_entry(0, 0).unwrap();
153 ///
154 /// assert_ne!(&Q::PI, &entry_simplified);
155 /// assert!(&entry_simplified >= &(Q::PI - Q::INV_MAX32));
156 /// assert!(&entry_simplified <= &(Q::PI + Q::INV_MAX32));
157 /// ```
158 pub fn simplify(&self, precision: impl Into<Q>) -> MatQ {
159 let precision = precision.into();
160 let mut out = MatQ::new(self.get_num_rows(), self.get_num_columns());
161
162 for i in 0..self.get_num_rows() {
163 for j in 0..self.get_num_columns() {
164 let entry = unsafe { self.get_entry_unchecked(i, j) };
165 let simplified_entry = entry.simplify(&precision);
166 unsafe { out.set_entry_unchecked(i, j, simplified_entry) };
167 }
168 }
169
170 out
171 }
172
173 seedable_function!(
174 /// Performs the randomized rounding algorithm entrywise
175 /// by sampling from a discrete Gaussian over the integers shifted
176 /// by `self` with gaussian parameter `r`.
177 ///
178 /// Parameters:
179 /// - `r`: specifies the Gaussian parameter, which is proportional
180 /// to the standard deviation `sigma * sqrt(2 * pi) = r`
181 #[seeded]
182 /// - `seed`: specifies the 256-bit seed of the PRNG used for sampling
183 #[optionally_seeded]
184 /// - `seed`: specifies an optional 256-bit seed of the PRNG used for sampling.
185 #[optionally_seeded]
186 /// If `None` is provided, a fresh [`ThreadRng`](rand::rngs::ThreadRng) is used instead.
187 ///
188 /// Returns the rounded matrix as a [`MatZ`] or an error if `r < 0`.
189 ///
190 /// # Examples
191 /// ```
192 /// use qfall_math::rational::MatQ;
193 /// use std::str::FromStr;
194 ///
195 /// let value = MatQ::from_str("[[5/2, 1]]").unwrap();
196 #[unseeded]
197 /// let rounded = value.randomized_rounding(3).unwrap();
198 #[seeded]
199 /// let rounded = value.randomized_rounding_seeded(3, [42; 32]).unwrap();
200 /// ```
201 ///
202 /// # Errors and Failures
203 /// - Returns a [`MathError`] of type [`InvalidIntegerInput`](MathError::InvalidIntegerInput)
204 /// if `r < 0`.
205 ///
206 /// This function implements randomized rounding according to:
207 /// - \[1\] Peikert, C. (2010, August).
208 /// An efficient and parallel Gaussian sampler for lattices.
209 /// In: Annual Cryptology Conference (pp. 80-97).
210 /// <https://link.springer.com/chapter/10.1007/978-3-642-14623-7_5>
211 pub(crate) fn randomized_rounding(
212 &self,
213 r: impl Into<Q>,
214 seed: Option<[u8; 32]>,
215 ) -> Result<MatZ, MathError> {
216 // derives an independent seed for every call of a seedable function below,
217 // see the documentation of `seedable_function` in `src/macros/seeded.rs`
218 let mut rng = SamplerRng::new(seed);
219
220 let mut out = MatZ::new(self.get_num_rows(), self.get_num_columns());
221 let r = r.into();
222 for i in 0..out.get_num_rows() {
223 for j in 0..out.get_num_columns() {
224 let entry = unsafe { self.get_entry_unchecked(i, j) }
225 .randomized_rounding_optionally_seeded(&r, rng.derive_seed())?;
226 unsafe { out.set_entry_unchecked(i, j, entry) };
227 }
228 }
229 Ok(out)
230 }
231 );
232}
233
234#[cfg(test)]
235mod test_floor {
236 use crate::{integer::MatZ, rational::MatQ};
237 use std::str::FromStr;
238
239 // Ensure that positive rationals are rounded correctly
240 #[test]
241 fn positive() {
242 let value = MatQ::from_str(&format!("[[1/{}, {}/2]]", u64::MAX, i64::MAX)).unwrap();
243 let cmp = MatZ::from_str(&format!("[[0, {}]]", (i64::MAX - 1) / 2)).unwrap();
244
245 assert_eq!(cmp, value.floor());
246 }
247
248 // Ensure that negative rationals are rounded correctly
249 #[test]
250 fn negative() {
251 let value = MatQ::from_str(&format!("[[-1/{}, -{}/2]]", u64::MAX, i64::MAX)).unwrap();
252 let cmp = MatZ::from_str(&format!("[[-1, {}]]", (-i64::MAX - 1) / 2)).unwrap();
253
254 assert_eq!(cmp, value.floor());
255 }
256}
257
258#[cfg(test)]
259mod test_ceil {
260 use crate::{integer::MatZ, rational::MatQ};
261 use std::str::FromStr;
262
263 // Ensure that positive rationals are rounded correctly
264 #[test]
265 fn positive() {
266 let value = MatQ::from_str(&format!("[[1/{}, {}/2]]", u64::MAX, i64::MAX)).unwrap();
267 let cmp = MatZ::from_str(&format!("[[1, {}]]", (i64::MAX - 1) / 2 + 1)).unwrap();
268
269 assert_eq!(cmp, value.ceil());
270 }
271
272 // Ensure that negative rationals are rounded correctly
273 #[test]
274 fn negative() {
275 let value = MatQ::from_str(&format!("[[-1/{}, -{}/2]]", u64::MAX, i64::MAX)).unwrap();
276 let cmp = MatZ::from_str(&format!("[[0, {}]]", (-i64::MAX - 1) / 2 + 1)).unwrap();
277
278 assert_eq!(cmp, value.ceil());
279 }
280}
281
282#[cfg(test)]
283mod test_round {
284 use crate::{integer::MatZ, rational::MatQ};
285 use std::str::FromStr;
286
287 // Ensure that positive rationals are rounded correctly
288 #[test]
289 fn positive() {
290 let value = MatQ::from_str(&format!("[[1/{}, {}/2]]", u64::MAX, i64::MAX)).unwrap();
291 let cmp = MatZ::from_str(&format!("[[0, {}]]", (i64::MAX - 1) / 2 + 1)).unwrap();
292
293 assert_eq!(cmp, value.round());
294 }
295
296 // Ensure that negative rationals are rounded correctly
297 #[test]
298 fn negative() {
299 let value = MatQ::from_str(&format!("[[-1/{}, -{}/2]]", u64::MAX, i64::MAX)).unwrap();
300 let cmp = MatZ::from_str(&format!("[[0, {}]]", (-i64::MAX - 1) / 2 + 1)).unwrap();
301
302 assert_eq!(cmp, value.round());
303 }
304}
305
306#[cfg(test)]
307mod test_randomized_rounding {
308 use crate::rational::MatQ;
309 use crate::utils::sample::test_seed;
310 use std::str::FromStr;
311
312 /// Ensure that a `r < 0` throws an error
313 #[test]
314 fn negative_r() {
315 for seeded in [false, true] {
316 let value = MatQ::from_str("[[5/2, 1]]").unwrap();
317 assert!(
318 value
319 .randomized_rounding_optionally_seeded(-1, seeded.then(test_seed))
320 .is_err()
321 );
322 }
323 }
324
325 /// Checks whether the same seed results in the same sample.
326 #[test]
327 fn same_seed_same_sample() {
328 use crate::rational::MatQ;
329 use std::str::FromStr;
330 let value = MatQ::from_str("[[5/2, 1]]").unwrap();
331 let sample_0 = value.randomized_rounding_seeded(3, [42; 32]).unwrap();
332 let sample_1 = value.randomized_rounding_seeded(3, [42; 32]).unwrap();
333
334 assert_eq!(sample_0, sample_1);
335 }
336
337 /// Checks whether the seeded variant outputs the known sample for a fixed seed.
338 #[test]
339 fn known_answer() {
340 use crate::rational::MatQ;
341 use std::str::FromStr;
342 let value = MatQ::from_str("[[5/2, 1]]").unwrap();
343 let sample = value.randomized_rounding_seeded(3, [42; 32]).unwrap();
344
345 assert_eq!("[[3, 0]]", sample.to_string());
346 }
347
348 /// Checks whether the unseeded and the seeded function are available.
349 #[test]
350 fn availability() {
351 use crate::rational::MatQ;
352 use std::str::FromStr;
353 let value = MatQ::from_str("[[5/2, 1]]").unwrap();
354
355 // explicitly checks the existence of the unseeded and the seeded function
356 let _ = value.randomized_rounding(3);
357 let _ = value.randomized_rounding_seeded(3, test_seed());
358 }
359}