Skip to main content

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}