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 rational::Q,
16 traits::{MatrixDimensions, MatrixGetEntry, MatrixSetEntry},
17};
18
19impl MatQ {
20 /// Rounds all entries of the given rational matrix [`MatQ`] down to the next integer
21 /// as a [`MatZ`].
22 ///
23 /// # Examples
24 /// ```
25 /// use qfall_math::rational::MatQ;
26 /// use qfall_math::integer::MatZ;
27 /// use std::str::FromStr;
28 ///
29 /// let value = MatQ::from_str("[[5/2, 1]]").unwrap();
30 /// assert_eq!(MatZ::from_str("[[2, 1]]").unwrap(), value.floor());
31 ///
32 /// let value = MatQ::from_str("[[-5/2, 1]]").unwrap();
33 /// assert_eq!(MatZ::from_str("[[-3, 1]]").unwrap(), value.floor());
34 /// ```
35 pub fn floor(&self) -> MatZ {
36 let mut out = MatZ::new(self.get_num_rows(), self.get_num_columns());
37 for i in 0..out.get_num_rows() {
38 for j in 0..out.get_num_columns() {
39 let entry = unsafe { self.get_entry_unchecked(i, j) }.floor();
40 unsafe { out.set_entry_unchecked(i, j, entry) };
41 }
42 }
43 out
44 }
45
46 /// Rounds all entries of the given rational matrix [`MatQ`] up to the next integer
47 /// as a [`MatZ`].
48 ///
49 /// # Examples
50 /// ```
51 /// use qfall_math::rational::MatQ;
52 /// use qfall_math::integer::MatZ;
53 /// use std::str::FromStr;
54 ///
55 /// let value = MatQ::from_str("[[5/2, 1]]").unwrap();
56 /// assert_eq!(MatZ::from_str("[[3, 1]]").unwrap(), value.ceil());
57 ///
58 /// let value = MatQ::from_str("[[-5/2, 1]]").unwrap();
59 /// assert_eq!(MatZ::from_str("[[-2, 1]]").unwrap(), value.ceil());
60 /// ```
61 pub fn ceil(&self) -> MatZ {
62 let mut out = MatZ::new(self.get_num_rows(), self.get_num_columns());
63 for i in 0..out.get_num_rows() {
64 for j in 0..out.get_num_columns() {
65 let entry = unsafe { self.get_entry_unchecked(i, j) }.ceil();
66 unsafe { out.set_entry_unchecked(i, j, entry) };
67 }
68 }
69 out
70 }
71
72 /// Rounds all entries of the given rational matrix [`MatQ`] to the closest integer
73 /// as a [`MatZ`].
74 ///
75 /// # Examples
76 /// ```
77 /// use qfall_math::rational::MatQ;
78 /// use qfall_math::integer::MatZ;
79 /// use std::str::FromStr;
80 ///
81 /// let value = MatQ::from_str("[[5/2, 1]]").unwrap();
82 /// assert_eq!(MatZ::from_str("[[3, 1]]").unwrap(), value.round());
83 ///
84 /// let value = MatQ::from_str("[[-5/2, 1]]").unwrap();
85 /// assert_eq!(MatZ::from_str("[[-2, 1]]").unwrap(), value.round());
86 /// ```
87 pub fn round(&self) -> MatZ {
88 let mut out = MatZ::new(self.get_num_rows(), self.get_num_columns());
89 for i in 0..out.get_num_rows() {
90 for j in 0..out.get_num_columns() {
91 let entry = unsafe { self.get_entry_unchecked(i, j) }.round();
92 unsafe { out.set_entry_unchecked(i, j, entry) };
93 }
94 }
95 out
96 }
97
98 /// Returns a matrix, where each entry was simplified using [`Q::simplify`],
99 /// i.e. each entry becomes the smallest rational with the smallest denominator in the range
100 /// `\[entry - |precision|, entry + |precision|\]`.
101 ///
102 /// This function allows to free memory in exchange for the specified loss of
103 /// precision (see Example 3). Be aware that this loss of precision is propagated by
104 /// arithmetic operations depending on the size of the matrices.
105 /// This functions allows to trade precision for efficiency.
106 ///
107 /// This function ensures that simplifying does not change the sign of any entry in the matrix.
108 ///
109 /// Parameters:
110 /// - `precision`: the precision the new entries can differ from `self`.
111 /// Note that the absolute value is relevant, not the sign.
112 ///
113 /// Returns a new [`MatQ`] with each entry being the simplest fraction within the defined range.
114 ///
115 /// # Examples
116 /// ```
117 /// use qfall_math::rational::{MatQ, Q};
118 /// use qfall_math::traits::{MatrixGetEntry, MatrixSetEntry};
119 /// let mut matrix = MatQ::new(1, 2);
120 /// matrix.set_entry(0, 0, Q::from((17, 20))).unwrap();
121 /// let precision = Q::from((1, 20));
122 ///
123 /// let matrix_simplified = matrix.simplify(precision);
124 ///
125 /// assert_eq!(Q::from((4, 5)), matrix_simplified.get_entry(0, 0).unwrap());
126 /// ```
127 ///
128 /// ```
129 /// use qfall_math::rational::{MatQ, Q};
130 /// use qfall_math::traits::{MatrixGetEntry, MatrixSetEntry};
131 /// let mut matrix = MatQ::new(2, 1);
132 /// matrix.set_entry(0, 0, Q::from((3, 2))).unwrap();
133 ///
134 /// let mat_simplified = matrix.simplify(0.5);
135 ///
136 /// assert_eq!(Q::ONE, mat_simplified.get_entry(0, 0).unwrap());
137 /// ```
138 ///
139 /// ## Simplify with reasonable precision loss
140 /// This example uses [`Q::INV_MAX32`], i.e. a loss of precision of at most `1 / 2^31 - 2` behind the decimal point.
141 /// If you require higher precision, [`Q::INV_MAX62`] is available.
142 /// ```
143 /// use qfall_math::rational::{MatQ, Q};
144 /// use qfall_math::traits::{MatrixGetEntry, MatrixSetEntry};
145 /// let mut matrix = MatQ::new(1, 1);
146 /// matrix.set_entry(0, 0, Q::PI).unwrap();
147 ///
148 /// let mat_simplified = matrix.simplify(Q::INV_MAX32);
149 ///
150 /// let entry_simplified = mat_simplified.get_entry(0, 0).unwrap();
151 ///
152 /// assert_ne!(&Q::PI, &entry_simplified);
153 /// assert!(&entry_simplified >= &(Q::PI - Q::INV_MAX32));
154 /// assert!(&entry_simplified <= &(Q::PI + Q::INV_MAX32));
155 /// ```
156 pub fn simplify(&self, precision: impl Into<Q>) -> MatQ {
157 let precision = precision.into();
158 let mut out = MatQ::new(self.get_num_rows(), self.get_num_columns());
159
160 for i in 0..self.get_num_rows() {
161 for j in 0..self.get_num_columns() {
162 let entry = unsafe { self.get_entry_unchecked(i, j) };
163 let simplified_entry = entry.simplify(&precision);
164 unsafe { out.set_entry_unchecked(i, j, simplified_entry) };
165 }
166 }
167
168 out
169 }
170
171 /// Performs the randomized rounding algorithm entrywise
172 /// by sampling from a discrete Gaussian over the integers shifted
173 /// by `self` with gaussian parameter `r`.
174 ///
175 /// Parameters:
176 /// - `r`: specifies the Gaussian parameter, which is proportional
177 /// to the standard deviation `sigma * sqrt(2 * pi) = r`
178 ///
179 /// Returns the rounded matrix as a [`MatZ`] or an error if `r < 0`.
180 ///
181 /// # Examples
182 /// ```
183 /// use qfall_math::rational::MatQ;
184 /// use std::str::FromStr;
185 ///
186 /// let value = MatQ::from_str("[[5/2, 1]]").unwrap();
187 /// let rounded = value.randomized_rounding(3).unwrap();
188 /// ```
189 ///
190 /// # Errors and Failures
191 /// - Returns a [`MathError`] of type [`InvalidIntegerInput`](MathError::InvalidIntegerInput)
192 /// if `r < 0`.
193 ///
194 /// This function implements randomized rounding according to:
195 /// - \[1\] Peikert, C. (2010, August).
196 /// An efficient and parallel Gaussian sampler for lattices.
197 /// In: Annual Cryptology Conference (pp. 80-97).
198 /// <https://link.springer.com/chapter/10.1007/978-3-642-14623-7_5>
199 pub fn randomized_rounding(&self, r: impl Into<Q>) -> Result<MatZ, MathError> {
200 let mut out = MatZ::new(self.get_num_rows(), self.get_num_columns());
201 let r = r.into();
202 for i in 0..out.get_num_rows() {
203 for j in 0..out.get_num_columns() {
204 let entry = unsafe { self.get_entry_unchecked(i, j) }.randomized_rounding(&r)?;
205 unsafe { out.set_entry_unchecked(i, j, entry) };
206 }
207 }
208 Ok(out)
209 }
210}
211
212#[cfg(test)]
213mod test_floor {
214 use crate::{integer::MatZ, rational::MatQ};
215 use std::str::FromStr;
216
217 // Ensure that positive rationals are rounded correctly
218 #[test]
219 fn positive() {
220 let value = MatQ::from_str(&format!("[[1/{}, {}/2]]", u64::MAX, i64::MAX)).unwrap();
221 let cmp = MatZ::from_str(&format!("[[0, {}]]", (i64::MAX - 1) / 2)).unwrap();
222
223 assert_eq!(cmp, value.floor());
224 }
225
226 // Ensure that negative rationals are rounded correctly
227 #[test]
228 fn negative() {
229 let value = MatQ::from_str(&format!("[[-1/{}, -{}/2]]", u64::MAX, i64::MAX)).unwrap();
230 let cmp = MatZ::from_str(&format!("[[-1, {}]]", (-i64::MAX - 1) / 2)).unwrap();
231
232 assert_eq!(cmp, value.floor());
233 }
234}
235
236#[cfg(test)]
237mod test_ceil {
238 use crate::{integer::MatZ, rational::MatQ};
239 use std::str::FromStr;
240
241 // Ensure that positive rationals are rounded correctly
242 #[test]
243 fn positive() {
244 let value = MatQ::from_str(&format!("[[1/{}, {}/2]]", u64::MAX, i64::MAX)).unwrap();
245 let cmp = MatZ::from_str(&format!("[[1, {}]]", (i64::MAX - 1) / 2 + 1)).unwrap();
246
247 assert_eq!(cmp, value.ceil());
248 }
249
250 // Ensure that negative rationals are rounded correctly
251 #[test]
252 fn negative() {
253 let value = MatQ::from_str(&format!("[[-1/{}, -{}/2]]", u64::MAX, i64::MAX)).unwrap();
254 let cmp = MatZ::from_str(&format!("[[0, {}]]", (-i64::MAX - 1) / 2 + 1)).unwrap();
255
256 assert_eq!(cmp, value.ceil());
257 }
258}
259
260#[cfg(test)]
261mod test_round {
262 use crate::{integer::MatZ, rational::MatQ};
263 use std::str::FromStr;
264
265 // Ensure that positive rationals are rounded correctly
266 #[test]
267 fn positive() {
268 let value = MatQ::from_str(&format!("[[1/{}, {}/2]]", u64::MAX, i64::MAX)).unwrap();
269 let cmp = MatZ::from_str(&format!("[[0, {}]]", (i64::MAX - 1) / 2 + 1)).unwrap();
270
271 assert_eq!(cmp, value.round());
272 }
273
274 // Ensure that negative rationals are rounded correctly
275 #[test]
276 fn negative() {
277 let value = MatQ::from_str(&format!("[[-1/{}, -{}/2]]", u64::MAX, i64::MAX)).unwrap();
278 let cmp = MatZ::from_str(&format!("[[0, {}]]", (-i64::MAX - 1) / 2 + 1)).unwrap();
279
280 assert_eq!(cmp, value.round());
281 }
282}
283
284#[cfg(test)]
285mod test_randomized_rounding {
286 use crate::rational::MatQ;
287 use std::str::FromStr;
288
289 /// Ensure that a `r < 0` throws an error
290 #[test]
291 fn negative_r() {
292 let value = MatQ::from_str("[[5/2, 1]]").unwrap();
293 assert!(value.randomized_rounding(-1).is_err());
294 }
295}