/rust/registry/src/index.crates.io-1949cf8c6b5b557f/diskann-utils-0.54.0/src/sampling/random.rs
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) Microsoft Corporation. |
3 | | * Licensed under the MIT license. |
4 | | */ |
5 | | |
6 | | use rand::{rngs::StdRng, Rng}; |
7 | | use rand_distr::StandardNormal; |
8 | | |
9 | | pub trait RoundFromf32 { |
10 | | fn round_from_f32(x: f32) -> Self; |
11 | | } |
12 | | |
13 | | impl RoundFromf32 for f32 { |
14 | 0 | fn round_from_f32(x: f32) -> Self { |
15 | 0 | x |
16 | 0 | } |
17 | | } |
18 | | impl RoundFromf32 for i8 { |
19 | 0 | fn round_from_f32(x: f32) -> Self { |
20 | 0 | x.round() as i8 |
21 | 0 | } |
22 | | } |
23 | | impl RoundFromf32 for u8 { |
24 | 0 | fn round_from_f32(x: f32) -> Self { |
25 | 0 | x.round() as u8 |
26 | 0 | } |
27 | | } |
28 | | impl RoundFromf32 for half::f16 { |
29 | 0 | fn round_from_f32(x: f32) -> Self { |
30 | 0 | half::f16::from_f32(x) |
31 | 0 | } |
32 | | } |
33 | | |
34 | | pub trait WithApproximateNorm: Sized { |
35 | | fn with_approximate_norm(dim: usize, norm: f32, rng: &mut StdRng) -> Vec<Self>; |
36 | | } |
37 | | |
38 | | impl WithApproximateNorm for f32 { |
39 | 0 | fn with_approximate_norm(dim: usize, norm: f32, rng: &mut StdRng) -> Vec<Self> { |
40 | 0 | generate_random_vector_with_norm_signed(dim, norm, true, rng, |x: f32| x) |
41 | 0 | } |
42 | | } |
43 | | |
44 | | impl WithApproximateNorm for half::f16 { |
45 | 0 | fn with_approximate_norm(dim: usize, norm: f32, rng: &mut StdRng) -> Vec<Self> { |
46 | | // Small QOL improvement, `diskann_wide::cast_f32_to_f16` works under `Miri` while `half::f16::from_f32` |
47 | | // does not. |
48 | 0 | generate_random_vector_with_norm_signed(dim, norm, true, rng, diskann_wide::cast_f32_to_f16) |
49 | 0 | } |
50 | | } |
51 | | |
52 | | impl WithApproximateNorm for u8 { |
53 | 0 | fn with_approximate_norm(dim: usize, norm: f32, rng: &mut StdRng) -> Vec<Self> { |
54 | 0 | generate_random_vector_with_norm_signed(dim, norm, false, rng, |x| x as u8) |
55 | 0 | } |
56 | | } |
57 | | |
58 | | impl WithApproximateNorm for i8 { |
59 | 0 | fn with_approximate_norm(dim: usize, norm: f32, rng: &mut StdRng) -> Vec<Self> { |
60 | 0 | generate_random_vector_with_norm_signed(dim, norm, true, rng, |x| x as i8) |
61 | 0 | } |
62 | | } |
63 | | |
64 | | // This function uses StandardNormal distribution. StandardNormal creates uniformly |
65 | | // distributed points on sphere surface, making the graph easier to navigate. |
66 | 0 | fn generate_random_vector_with_norm_signed<T, F>( |
67 | 0 | dim: usize, |
68 | 0 | norm: f32, |
69 | 0 | signed: bool, |
70 | 0 | rng: &mut StdRng, |
71 | 0 | f: F, |
72 | 0 | ) -> Vec<T> |
73 | 0 | where |
74 | 0 | F: Fn(f32) -> T, |
75 | | { |
76 | 0 | let mut vec: Vec<f32> = (0..dim).map(|_| rng.sample(StandardNormal)).collect(); Unexecuted instantiation: diskann_utils::sampling::random::generate_random_vector_with_norm_signed::<half::binary16::f16, diskann_wide::reference::cast_f32_to_f16>::{closure#0}Unexecuted instantiation: diskann_utils::sampling::random::generate_random_vector_with_norm_signed::<i8, <i8 as diskann_utils::sampling::random::WithApproximateNorm>::with_approximate_norm::{closure#0}>::{closure#0}Unexecuted instantiation: diskann_utils::sampling::random::generate_random_vector_with_norm_signed::<f32, <f32 as diskann_utils::sampling::random::WithApproximateNorm>::with_approximate_norm::{closure#0}>::{closure#0}Unexecuted instantiation: diskann_utils::sampling::random::generate_random_vector_with_norm_signed::<u8, <u8 as diskann_utils::sampling::random::WithApproximateNorm>::with_approximate_norm::{closure#0}>::{closure#0} |
77 | 0 | let current_norm: f32 = vec.iter().map(|x| x * x).sum::<f32>().sqrt(); Unexecuted instantiation: diskann_utils::sampling::random::generate_random_vector_with_norm_signed::<half::binary16::f16, diskann_wide::reference::cast_f32_to_f16>::{closure#1}Unexecuted instantiation: diskann_utils::sampling::random::generate_random_vector_with_norm_signed::<i8, <i8 as diskann_utils::sampling::random::WithApproximateNorm>::with_approximate_norm::{closure#0}>::{closure#1}Unexecuted instantiation: diskann_utils::sampling::random::generate_random_vector_with_norm_signed::<f32, <f32 as diskann_utils::sampling::random::WithApproximateNorm>::with_approximate_norm::{closure#0}>::{closure#1}Unexecuted instantiation: diskann_utils::sampling::random::generate_random_vector_with_norm_signed::<u8, <u8 as diskann_utils::sampling::random::WithApproximateNorm>::with_approximate_norm::{closure#0}>::{closure#1} |
78 | 0 | let scale = norm / current_norm; |
79 | 0 | if signed { |
80 | 0 | vec.iter_mut().for_each(|x| *x *= scale); Unexecuted instantiation: diskann_utils::sampling::random::generate_random_vector_with_norm_signed::<half::binary16::f16, diskann_wide::reference::cast_f32_to_f16>::{closure#2}Unexecuted instantiation: diskann_utils::sampling::random::generate_random_vector_with_norm_signed::<i8, <i8 as diskann_utils::sampling::random::WithApproximateNorm>::with_approximate_norm::{closure#0}>::{closure#2}Unexecuted instantiation: diskann_utils::sampling::random::generate_random_vector_with_norm_signed::<f32, <f32 as diskann_utils::sampling::random::WithApproximateNorm>::with_approximate_norm::{closure#0}>::{closure#2}Unexecuted instantiation: diskann_utils::sampling::random::generate_random_vector_with_norm_signed::<u8, <u8 as diskann_utils::sampling::random::WithApproximateNorm>::with_approximate_norm::{closure#0}>::{closure#2} |
81 | | } else { |
82 | 0 | vec.iter_mut().for_each(|x| *x = (*x * scale).abs()); Unexecuted instantiation: diskann_utils::sampling::random::generate_random_vector_with_norm_signed::<half::binary16::f16, diskann_wide::reference::cast_f32_to_f16>::{closure#3}Unexecuted instantiation: diskann_utils::sampling::random::generate_random_vector_with_norm_signed::<i8, <i8 as diskann_utils::sampling::random::WithApproximateNorm>::with_approximate_norm::{closure#0}>::{closure#3}Unexecuted instantiation: diskann_utils::sampling::random::generate_random_vector_with_norm_signed::<f32, <f32 as diskann_utils::sampling::random::WithApproximateNorm>::with_approximate_norm::{closure#0}>::{closure#3}Unexecuted instantiation: diskann_utils::sampling::random::generate_random_vector_with_norm_signed::<u8, <u8 as diskann_utils::sampling::random::WithApproximateNorm>::with_approximate_norm::{closure#0}>::{closure#3} |
83 | | }; |
84 | 0 | vec.into_iter().map(f).collect() |
85 | 0 | } Unexecuted instantiation: diskann_utils::sampling::random::generate_random_vector_with_norm_signed::<half::binary16::f16, diskann_wide::reference::cast_f32_to_f16> Unexecuted instantiation: diskann_utils::sampling::random::generate_random_vector_with_norm_signed::<i8, <i8 as diskann_utils::sampling::random::WithApproximateNorm>::with_approximate_norm::{closure#0}>Unexecuted instantiation: diskann_utils::sampling::random::generate_random_vector_with_norm_signed::<f32, <f32 as diskann_utils::sampling::random::WithApproximateNorm>::with_approximate_norm::{closure#0}>Unexecuted instantiation: diskann_utils::sampling::random::generate_random_vector_with_norm_signed::<u8, <u8 as diskann_utils::sampling::random::WithApproximateNorm>::with_approximate_norm::{closure#0}> |
86 | | |
87 | | #[cfg(test)] |
88 | | mod tests { |
89 | | use super::*; |
90 | | use rand::SeedableRng; |
91 | | use rstest::rstest; |
92 | | |
93 | | #[rstest] |
94 | | #[case(1, 0.01)] |
95 | | #[case(100, 0.01)] |
96 | | #[case(171, 5.0)] |
97 | | #[case(1024, 100.7)] |
98 | | fn test_generate_random_vector_with_norm_f32(#[case] dim: usize, #[case] norm: f32) { |
99 | | let seed = 42; |
100 | | let mut rng = StdRng::seed_from_u64(seed); |
101 | | let vec: Vec<f32> = WithApproximateNorm::with_approximate_norm(dim, norm, &mut rng); |
102 | | let computed_norm: f32 = vec.iter().map(|x| x * x).sum::<f32>().sqrt(); |
103 | | let tolerance = 1e-5; |
104 | | assert!((computed_norm - norm).abs() / norm < tolerance); |
105 | | } |
106 | | |
107 | | #[rstest] |
108 | | #[case(1, 0.01)] |
109 | | #[case(100, 0.01)] |
110 | | #[case(171, 5.0)] |
111 | | #[case(1024, 100.7)] |
112 | | fn test_generate_random_vector_with_norm_half_f16(#[case] dim: usize, #[case] norm: f32) { |
113 | | let seed = 42; |
114 | | let mut rng = StdRng::seed_from_u64(seed); |
115 | | let vec: Vec<half::f16> = WithApproximateNorm::with_approximate_norm(dim, norm, &mut rng); |
116 | | let computed_norm: f32 = vec |
117 | | .iter() |
118 | | .map(|x| { |
119 | | let val: f32 = x.to_f32(); |
120 | | val * val |
121 | | }) |
122 | | .sum::<f32>() |
123 | | .sqrt(); |
124 | | let tolerance = 1e-2; // half precision |
125 | | assert!((computed_norm - norm).abs() / norm < tolerance); |
126 | | } |
127 | | |
128 | | #[rstest] |
129 | | #[case(17, 50.0)] |
130 | | #[case(1024, 1007.0)] |
131 | | fn test_generate_random_vector_with_norm_u8(#[case] dim: usize, #[case] norm: f32) { |
132 | | let seed = 42; |
133 | | let mut rng = StdRng::seed_from_u64(seed); |
134 | | let vec: Vec<u8> = WithApproximateNorm::with_approximate_norm(dim, norm, &mut rng); |
135 | | let computed_norm: f32 = vec |
136 | | .iter() |
137 | | .map(|&x| { |
138 | | let val: f32 = x as f32; |
139 | | val * val |
140 | | }) |
141 | | .sum::<f32>() |
142 | | .sqrt(); |
143 | | let tolerance = 1e-1; // due to quantization |
144 | | assert!((computed_norm - norm).abs() / norm < tolerance); |
145 | | } |
146 | | |
147 | | #[rstest] |
148 | | #[case(17, 50.0)] |
149 | | #[case(1024, 1007.0)] |
150 | | fn test_generate_random_vector_with_norm_i8(#[case] dim: usize, #[case] norm: f32) { |
151 | | let seed = 42; |
152 | | let mut rng = StdRng::seed_from_u64(seed); |
153 | | let vec: Vec<i8> = WithApproximateNorm::with_approximate_norm(dim, norm, &mut rng); |
154 | | let computed_norm: f32 = vec |
155 | | .iter() |
156 | | .map(|&x| { |
157 | | let val: f32 = x as f32; |
158 | | val * val |
159 | | }) |
160 | | .sum::<f32>() |
161 | | .sqrt(); |
162 | | let tolerance = 1e-1; // due to quantization |
163 | | assert!((computed_norm - norm).abs() / norm < tolerance); |
164 | | } |
165 | | |
166 | | #[rstest] |
167 | | #[case(3.6f32, 4i8)] |
168 | | #[case(2.3f32, 2i8)] |
169 | | #[case(-1.5f32, -2i8)] |
170 | | fn test_round_f32_to_i8(#[case] input: f32, #[case] expected: i8) { |
171 | | let result: i8 = RoundFromf32::round_from_f32(input); |
172 | | assert_eq!(result, expected); |
173 | | } |
174 | | |
175 | | #[rstest] |
176 | | #[case(3.6f32, 4u8)] |
177 | | #[case(2.3f32, 2u8)] |
178 | | #[case(-1.5f32, 0u8)] |
179 | | fn test_round_f32_to_u8(#[case] input: f32, #[case] expected: u8) { |
180 | | let result: u8 = RoundFromf32::round_from_f32(input); |
181 | | assert_eq!(result, expected); |
182 | | } |
183 | | |
184 | | #[rstest] |
185 | | #[case(3.6f32, half::f16::from_f32(3.6f32))] |
186 | | #[case(2.3f32, half::f16::from_f32(2.3f32))] |
187 | | #[case(-1.5f32, half::f16::from_f32(-1.5f32))] |
188 | | fn test_round_f32_to_f16(#[case] input: f32, #[case] expected: half::f16) { |
189 | | let result: half::f16 = RoundFromf32::round_from_f32(input); |
190 | | assert_eq!(result, expected); |
191 | | } |
192 | | |
193 | | #[rstest] |
194 | | #[case(3.6f32, 3.6f32)] |
195 | | #[case(2.3f32, 2.3f32)] |
196 | | #[case(-1.5f32, -1.5f32)] |
197 | | fn test_round_f32_to_f32(#[case] input: f32, #[case] expected: f32) { |
198 | | let result: f32 = RoundFromf32::round_from_f32(input); |
199 | | assert_eq!(result, expected); |
200 | | } |
201 | | |
202 | | /// Test that generated points are evenly distributed on a circle. |
203 | | /// |
204 | | /// **Testing methodology:** |
205 | | /// 1. Split the circle into 36 buckets (signed) or 9 buckets (unsigned), each covering 10 degrees |
206 | | /// 2. Generate points and count how many fall into each angular bucket |
207 | | /// 3. Check that each bucket's count is within `tolerance_sigmas × σ` of the expected count, |
208 | | /// where σ = sqrt(expected) is the statistical noise for random sampling |
209 | | /// 4. Fail if any bucket deviates too much (indicates clustering instead of uniform distribution) |
210 | | /// |
211 | | /// **Tolerance levels:** |
212 | | /// - tolerance_sigmas = 1.0 → Very strict, only allows ±1σ deviation (about 68% of buckets would naturally fall within this) |
213 | | /// - tolerance_sigmas = 3.0 → Moderate, allows ±3σ deviation (99.7% would naturally fall within this) |
214 | | /// - tolerance_sigmas = 6.0 → Very lenient, allows ±6σ deviation (99.9997% would naturally fall within this) |
215 | | #[rstest] |
216 | | #[case(true, 500, 3.0, 42)] |
217 | | #[case(true, 500, 3.0, 43)] |
218 | | #[case(true, 500, 3.0, 44)] |
219 | | #[case(false, 500, 3.0, 42)] |
220 | | #[case(false, 500, 3.0, 43)] |
221 | | #[case(false, 500, 3.0, 44)] |
222 | | fn test_generate_random_vector_with_norm_signed_produces_uniform_distribution_on_circle( |
223 | | #[case] signed: bool, |
224 | | #[case] expected_per_bucket: usize, |
225 | | #[case] tolerance_sigmas: f32, |
226 | | #[case] seed: u64, |
227 | | ) { |
228 | | let dim = 2; |
229 | | let norm = 1.0; |
230 | | let mut rng = StdRng::seed_from_u64(seed); |
231 | | |
232 | | // Step 1: Pick number of buckets and calculate samples |
233 | | let num_buckets = if signed { 36 } else { 9 }; |
234 | | let num_samples = num_buckets * expected_per_bucket; |
235 | | |
236 | | // Generate samples |
237 | | let samples: Vec<Vec<f32>> = (0..num_samples) |
238 | | .map(|_| generate_random_vector_with_norm_signed(dim, norm, signed, &mut rng, |x| x)) |
239 | | .collect(); |
240 | | |
241 | | // Step 2: Count hits per bucket |
242 | | let mut counts = vec![0usize; num_buckets]; |
243 | | |
244 | | for sample in &samples { |
245 | | let theta = sample[1].atan2(sample[0]); // atan2(y, x) returns [-π, π] |
246 | | |
247 | | // Map to bucket: floor(θ / 2π × buckets) |
248 | | let bucket = if signed { |
249 | | // Full circle [0, 2π) → [0, 36) |
250 | | let normalized_theta = if theta < 0.0 { |
251 | | theta + 2.0 * std::f32::consts::PI |
252 | | } else { |
253 | | theta |
254 | | }; |
255 | | ((normalized_theta / (2.0 * std::f32::consts::PI)) * num_buckets as f32).floor() |
256 | | as usize |
257 | | % num_buckets |
258 | | } else { |
259 | | // First quadrant [0, π/2) → [0, 9) |
260 | | ((theta / (std::f32::consts::PI / 2.0)) * num_buckets as f32).floor() as usize |
261 | | }; |
262 | | |
263 | | counts[bucket] += 1; |
264 | | } |
265 | | |
266 | | // Step 3: Check each bucket is within tolerance_sigmas × σ |
267 | | // Noise per bucket: σ ≈ sqrt(expected) |
268 | | // Threshold: |observed - expected| / expected > tolerance_sigmas / sqrt(expected) |
269 | | let sigma = (expected_per_bucket as f32).sqrt(); |
270 | | let threshold = tolerance_sigmas / sigma; |
271 | | |
272 | | let failed_count = counts |
273 | | .iter() |
274 | | .filter(|&&observed| { |
275 | | let deviation = (observed as f32 - expected_per_bucket as f32).abs() |
276 | | / expected_per_bucket as f32; |
277 | | deviation > threshold |
278 | | }) |
279 | | .count(); |
280 | | |
281 | | assert_eq!( |
282 | | failed_count, |
283 | | 0, |
284 | | "Distribution not uniform: {} out of {} bucket(s) had point counts that deviated more than {}σ from expected. \ |
285 | | This indicates the generator is producing clustered points instead of evenly distributed points on the circle surface.", |
286 | | failed_count, |
287 | | num_buckets, |
288 | | tolerance_sigmas |
289 | | ); |
290 | | } |
291 | | } |