Skip to main content

helpers_codec/common/
sampling.rs

1// MIT License
2//
3// Copyright (c) 2026 Raja Lehtihet & Wael El Oraiby
4//
5// Permission is hereby granted, free of charge, to any person obtaining a copy
6// of this software and associated documentation files (the "Software"), to deal
7// in the Software without restriction, including without limitation the rights
8// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9// copies of the Software, and to permit persons to whom the Software is
10// furnished to do so, subject to the following conditions:
11//
12// The above copyright notice and this permission notice shall be included in all
13// copies or substantial portions of the Software.
14//
15// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21// SOFTWARE.
22
23//! Example sampling helpers backed by `rand_core::CryptoRng`.
24//!
25//! These routines provide simple building blocks for experimentation:
26//! - uniform sampling in `Z_q`,
27//! - centered-binomial noise sampling,
28//! - discrete Gaussian-like sampling via Box-Muller,
29//! - explicit rejection sampling helpers.
30
31use core::f64::consts::TAU;
32
33use rand_core::CryptoRng;
34
35use nc_polynomial::{PolynomialError, RingContext, RingElem};
36
37/// Errors returned by example sampling helpers.
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub enum SamplingError {
40    /// Rejection sampling requires a positive upper bound.
41    InvalidUpperBound,
42    /// Centered binomial parameter must be positive.
43    InvalidEta,
44    /// Gaussian sigma must be finite and positive.
45    InvalidSigma,
46    /// Ring element construction or ring arithmetic failed.
47    Polynomial(PolynomialError),
48}
49
50impl core::fmt::Display for SamplingError {
51    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
52        match self {
53            Self::InvalidUpperBound => write!(f, "invalid upper bound, expected > 0"),
54            Self::InvalidEta => write!(f, "invalid eta, expected eta >= 1"),
55            Self::InvalidSigma => write!(f, "invalid sigma, expected finite sigma > 0"),
56            Self::Polynomial(err) => write!(f, "polynomial operation failed: {err}"),
57        }
58    }
59}
60
61impl std::error::Error for SamplingError {}
62
63impl From<PolynomialError> for SamplingError {
64    fn from(value: PolynomialError) -> Self {
65        Self::Polynomial(value)
66    }
67}
68
69/// Samples a uniform value from `[0, upper_bound)` using rejection sampling.
70pub fn sample_rejection_u64_below_example<R: CryptoRng>(
71    upper_bound: u64,
72    rng: &mut R,
73) -> Result<u64, SamplingError> {
74    if upper_bound == 0 {
75        return Err(SamplingError::InvalidUpperBound);
76    }
77
78    // Rejection-sampling setup:
79    // - `range` is the cardinality of `u64` outputs.
80    // - `zone` is the largest multiple of `upper_bound` inside `range`.
81    // Any random value below `zone` can be mapped with `% upper_bound`
82    // without modulo bias.
83    let range = (u64::MAX as u128) + 1;
84    let ub = upper_bound as u128;
85    let zone = range - (range % ub);
86
87    loop {
88        // Draw and accept only values in the unbiased zone.
89        let candidate = rng.next_u64() as u128;
90        if candidate < zone {
91            return Ok((candidate % ub) as u64);
92        }
93    }
94}
95
96/// Samples one polynomial with coefficients uniformly random in `Z_q`.
97pub fn sample_uniform_poly_example<R: CryptoRng>(
98    ctx: &RingContext,
99    rng: &mut R,
100) -> Result<RingElem, SamplingError> {
101    let mut coeffs = vec![0_u64; ctx.max_degree() + 1];
102    for coeff in &mut coeffs {
103        // Independent uniform sampling for each coefficient.
104        *coeff = sample_rejection_u64_below_example(ctx.modulus(), rng)?;
105    }
106    // `ctx.element(...)` canonicalizes modulo the ring polynomial.
107    Ok(ctx.element(&coeffs)?)
108}
109
110/// Samples one polynomial with centered-binomial noise of parameter `eta`.
111///
112/// Each coefficient is generated as `sum(a_i) - sum(b_i)` over `eta` Bernoulli bits,
113/// then mapped into `Z_q`.
114pub fn sample_cbd_poly_example<R: CryptoRng>(
115    ctx: &RingContext,
116    eta: u8,
117    rng: &mut R,
118) -> Result<RingElem, SamplingError> {
119    if eta == 0 {
120        return Err(SamplingError::InvalidEta);
121    }
122
123    let mut coeffs = vec![0_u64; ctx.max_degree() + 1];
124    for coeff in &mut coeffs {
125        // CBD(eta): difference between two eta-bit Hamming weights.
126        let mut a = 0_i64;
127        let mut b = 0_i64;
128        for _ in 0..eta {
129            a += (rng.next_u32() & 1) as i64;
130            b += (rng.next_u32() & 1) as i64;
131        }
132        // Map signed noise into `[0, q)` representation.
133        *coeff = wrap_signed_to_modulus(a - b, ctx.modulus());
134    }
135
136    Ok(ctx.element(&coeffs)?)
137}
138
139/// Samples one polynomial with approximate discrete Gaussian coefficients.
140///
141/// This example helper uses a Box-Muller normal sampler and rounds to nearest integer.
142pub fn sample_discrete_gaussian_poly_example<R: CryptoRng>(
143    ctx: &RingContext,
144    sigma: f64,
145    rng: &mut R,
146) -> Result<RingElem, SamplingError> {
147    if !(sigma.is_finite() && sigma > 0.0) {
148        return Err(SamplingError::InvalidSigma);
149    }
150
151    let mut coeffs = vec![0_u64; ctx.max_degree() + 1];
152    for coeff in &mut coeffs {
153        // Sample one integer noise value and wrap it into `Z_q`.
154        let sampled = sample_box_muller_integer(rng, sigma);
155        *coeff = wrap_signed_to_modulus(sampled, ctx.modulus());
156    }
157
158    Ok(ctx.element(&coeffs)?)
159}
160
161fn sample_box_muller_integer<R: CryptoRng>(rng: &mut R, sigma: f64) -> i64 {
162    // Box-Muller transform:
163    // z0 = sqrt(-2 ln u1) * cos(2 pi u2), where u1/u2 are uniform(0,1).
164    // Then scale by sigma and round to nearest integer.
165    let mut u1 = sample_unit_open_closed(rng);
166    while u1 <= 0.0 {
167        u1 = sample_unit_open_closed(rng);
168    }
169    let u2 = sample_unit_open_closed(rng);
170
171    let radius = (-2.0 * u1.ln()).sqrt();
172    let z0 = radius * (TAU * u2).cos();
173    (z0 * sigma).round() as i64
174}
175
176fn sample_unit_open_closed<R: CryptoRng>(rng: &mut R) -> f64 {
177    // Convert 53 random bits into a floating-point fraction in (0, 1).
178    const SCALE: f64 = 1.0 / ((1_u64 << 53) as f64);
179    let raw = rng.next_u64() >> 11;
180    let mut out = (raw as f64) * SCALE;
181    // Clamp away exact endpoints to keep logarithm and cosine stable.
182    if out <= 0.0 {
183        out = f64::from_bits(1);
184    }
185    if out >= 1.0 {
186        out = 1.0 - f64::EPSILON;
187    }
188    out
189}
190
191fn wrap_signed_to_modulus(value: i64, modulus: u64) -> u64 {
192    // Normalize signed integer into canonical residue class `[0, q)`.
193    let m = modulus as i128;
194    let reduced = ((value as i128 % m) + m) % m;
195    reduced as u64
196}
197
198#[cfg(test)]
199mod tests {
200    use super::{
201        SamplingError, sample_cbd_poly_example, sample_discrete_gaussian_poly_example,
202        sample_rejection_u64_below_example, sample_uniform_poly_example,
203    };
204    use core::convert::Infallible;
205    use nc_polynomial::RingContext;
206    use rand_core::{TryCryptoRng, TryRng};
207
208    #[derive(Debug, Clone)]
209    struct TestRng {
210        state: u64,
211    }
212
213    impl TestRng {
214        fn new(seed: u64) -> Self {
215            Self { state: seed }
216        }
217
218        fn step(&mut self) -> u64 {
219            let mut x = self.state;
220            x ^= x >> 12;
221            x ^= x << 25;
222            x ^= x >> 27;
223            self.state = x;
224            x.wrapping_mul(0x2545_F491_4F6C_DD1D)
225        }
226    }
227
228    impl TryRng for TestRng {
229        type Error = Infallible;
230
231        fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
232            Ok(self.step() as u32)
233        }
234
235        fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
236            Ok(self.step())
237        }
238
239        fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Self::Error> {
240            let mut offset = 0;
241            while offset < dest.len() {
242                let word = self.step().to_le_bytes();
243                let chunk = (dest.len() - offset).min(8);
244                dest[offset..offset + chunk].copy_from_slice(&word[..chunk]);
245                offset += chunk;
246            }
247            Ok(())
248        }
249    }
250
251    impl TryCryptoRng for TestRng {}
252
253    fn ctx() -> RingContext {
254        RingContext::from_parts(8, 998_244_353, &[1, 0, 0, 0, 0, 0, 0, 0, 1], 3)
255            .expect("context should build")
256    }
257
258    #[test]
259    fn rejection_sampling_respects_bounds() {
260        let mut rng = TestRng::new(7);
261        for bound in [1_u64, 2, 3, 17, 1_000_000, 998_244_353] {
262            for _ in 0..200 {
263                // Every output must stay in `[0, bound)`.
264                let sampled = sample_rejection_u64_below_example(bound, &mut rng)
265                    .expect("sampling should work");
266                assert!(sampled < bound);
267            }
268        }
269    }
270
271    #[test]
272    fn rejection_sampling_rejects_zero_bound() {
273        let mut rng = TestRng::new(11);
274        let err = sample_rejection_u64_below_example(0, &mut rng).expect_err("expected error");
275        assert_eq!(err, SamplingError::InvalidUpperBound);
276    }
277
278    #[test]
279    fn uniform_poly_sampling_outputs_valid_ring_element() {
280        let ctx = ctx();
281        let mut rng = TestRng::new(13);
282        let sampled = sample_uniform_poly_example(&ctx, &mut rng).expect("sampling should work");
283
284        // Shape and domain checks.
285        assert_eq!(sampled.coefficients().len(), ctx.max_degree() + 1);
286        assert!(sampled.coefficients().iter().all(|&c| c < ctx.modulus()));
287    }
288
289    #[test]
290    fn cbd_poly_sampling_outputs_small_noise_mod_q() {
291        let ctx = ctx();
292        let mut rng = TestRng::new(17);
293        let sampled = sample_cbd_poly_example(&ctx, 3, &mut rng).expect("sampling should work");
294
295        // Interpret coefficient residues as signed values near zero and check bounds.
296        let eta = 3_i64;
297        for &coeff in sampled.coefficients() {
298            let signed = if coeff > ctx.modulus() / 2 {
299                coeff as i64 - ctx.modulus() as i64
300            } else {
301                coeff as i64
302            };
303            assert!(signed >= -eta && signed <= eta);
304        }
305    }
306
307    #[test]
308    fn cbd_sampling_rejects_eta_zero() {
309        let ctx = ctx();
310        let mut rng = TestRng::new(19);
311        let err = sample_cbd_poly_example(&ctx, 0, &mut rng).expect_err("expected error");
312        assert_eq!(err, SamplingError::InvalidEta);
313    }
314
315    #[test]
316    fn gaussian_sampling_accepts_positive_sigma() {
317        let ctx = ctx();
318        let mut rng = TestRng::new(23);
319        let sampled = sample_discrete_gaussian_poly_example(&ctx, 2.5, &mut rng)
320            .expect("sampling should work");
321        assert_eq!(sampled.coefficients().len(), ctx.max_degree() + 1);
322    }
323
324    #[test]
325    fn gaussian_sampling_rejects_invalid_sigma() {
326        let ctx = ctx();
327        let mut rng = TestRng::new(29);
328        let err =
329            sample_discrete_gaussian_poly_example(&ctx, 0.0, &mut rng).expect_err("expected error");
330        assert_eq!(err, SamplingError::InvalidSigma);
331    }
332}