1use std::num::NonZeroUsize;
7
8use diskann_linalg::{self, Transpose};
9use diskann_utils::views::rowmajor::{Matrix, MatrixMut};
10#[cfg(feature = "flatbuffers")]
11use flatbuffers::{FlatBufferBuilder, WIPOffset};
12use rand::Rng;
13#[cfg(feature = "flatbuffers")]
14use thiserror::Error;
15
16use super::{
17 TargetDim,
18 utils::{TransformFailed, check_dims},
19};
20#[cfg(feature = "flatbuffers")]
21use crate::flatbuffers as fb;
22
23#[derive(Debug, Clone)]
32#[cfg_attr(test, derive(PartialEq))]
33pub struct RandomRotation {
34 transform: diskann_utils::views::rowmajor::Owned<f32>,
36}
37
38impl RandomRotation {
39 pub fn new<R>(dim: NonZeroUsize, target_dim: TargetDim, rng: &mut R) -> Self
57 where
58 R: Rng + ?Sized,
59 {
60 let dim = dim.get();
61
62 let (target_dim, matrix_dim) = match target_dim {
78 TargetDim::Same | TargetDim::Natural => (dim, dim),
79 TargetDim::Override(target) => {
80 let target_dim = target.get();
81 if target_dim <= dim {
82 (target_dim, dim)
83 } else {
84 (target_dim, target_dim)
85 }
86 }
87 };
88
89 #[expect(clippy::unwrap_used)]
92 let initial = diskann_utils::views::rowmajor::Owned::try_from_data(
93 diskann_linalg::random_distance_preserving_matrix(matrix_dim, rng).into(),
94 matrix_dim,
95 matrix_dim,
96 )
97 .unwrap();
98
99 let transform = match target_dim.cmp(&dim) {
101 std::cmp::Ordering::Equal => initial,
102 std::cmp::Ordering::Less => {
103 let indices = rand::seq::index::sample(rng, dim, target_dim);
104 let scaling = (dim as f32 / target_dim as f32).sqrt();
105
106 let mut transform =
107 diskann_utils::views::rowmajor::Owned::from_element(target_dim, dim, 0.0f32);
108 std::iter::zip(transform.rows_mut(), indices.iter()).for_each(|(ro, ri)| {
109 std::iter::zip(ro.iter_mut(), initial.row(ri).iter()).for_each(|(o, i)| {
110 *o = scaling * (*i);
111 })
112 });
113 transform
114 }
115 std::cmp::Ordering::Greater => {
116 let mut transform =
117 diskann_utils::views::rowmajor::Owned::from_element(target_dim, dim, 0.0f32);
118 std::iter::zip(transform.rows_mut(), initial.rows())
119 .for_each(|(o, i)| o.copy_from_slice(&i[..dim]));
120 transform
121 }
122 };
123
124 Self { transform }
125 }
126
127 pub fn input_dim(&self) -> usize {
129 self.transform.ncols()
130 }
131
132 pub fn output_dim(&self) -> usize {
134 self.transform.nrows()
135 }
136
137 pub fn preserves_norms(&self) -> bool {
142 self.output_dim() >= self.input_dim()
143 }
144
145 pub fn transform_into(&self, dst: &mut [f32], src: &[f32]) -> Result<(), TransformFailed> {
154 let input_dim = self.input_dim();
155 let output_dim = self.output_dim();
156 check_dims(dst, src, input_dim, output_dim)?;
157 diskann_linalg::sgemm(
158 Transpose::None,
159 Transpose::None,
160 output_dim,
161 1,
162 input_dim,
163 1.0,
164 self.transform.as_slice(),
165 src,
166 None,
167 dst,
168 )?;
169 Ok(())
170 }
171}
172
173#[cfg(feature = "flatbuffers")]
174#[cfg_attr(docsrs, doc(cfg(feature = "flatbuffers")))]
175#[derive(Debug, Clone, Copy, Error, PartialEq)]
176#[non_exhaustive]
177pub enum RandomRotationError {
178 #[error("buffer size not product of rows and columns")]
179 IncorrectDim,
180 #[error("number of rows cannot be zero")]
181 RowsZero,
182 #[error("number of cols cannot be zero")]
183 ColsZero,
184}
185
186#[cfg(feature = "flatbuffers")]
188impl RandomRotation {
189 pub(crate) fn pack<'a, A>(
192 &self,
193 buf: &mut FlatBufferBuilder<'a, A>,
194 ) -> WIPOffset<fb::transforms::RandomRotation<'a>>
195 where
196 A: flatbuffers::Allocator + 'a,
197 {
198 let data = buf.create_vector(self.transform.as_slice());
199
200 fb::transforms::RandomRotation::create(
201 buf,
202 &fb::transforms::RandomRotationArgs {
203 data: Some(data),
204 nrows: self.transform.nrows() as u32,
205 ncols: self.transform.ncols() as u32,
206 },
207 )
208 }
209
210 pub(crate) fn try_unpack(
213 proto: fb::transforms::RandomRotation<'_>,
214 ) -> Result<Self, RandomRotationError> {
215 let nrows = proto.nrows();
216 let ncols = proto.ncols();
217 if nrows == 0 {
218 return Err(RandomRotationError::RowsZero);
219 }
220 if ncols == 0 {
221 return Err(RandomRotationError::ColsZero);
222 }
223
224 let data = proto.data().into_iter().collect();
225 let transform = diskann_utils::views::rowmajor::Owned::try_from_data(
226 data,
227 nrows as usize,
228 ncols as usize,
229 )
230 .map_err(|_| RandomRotationError::IncorrectDim)?;
231
232 Ok(Self { transform })
233 }
234}
235
236#[cfg(test)]
241mod tests {
242 use diskann_utils::lazy_format;
243 use rand::{SeedableRng, rngs::StdRng};
244
245 use super::*;
246 use crate::{
247 algorithms::transforms::{Transform, TransformFailed, TransformKind, test_utils},
248 alloc::GlobalAllocator,
249 test_util::Check,
250 };
251
252 impl test_utils::Transformer for RandomRotation {
253 fn input_dim_(&self) -> usize {
254 self.input_dim()
255 }
256 fn output_dim_(&self) -> usize {
257 self.output_dim()
258 }
259 fn transform_into_(&self, dst: &mut [f32], src: &[f32]) -> Result<(), TransformFailed> {
260 self.transform_into(dst, src)
261 }
262 }
263
264 #[test]
265 fn test_transform_matrix() {
266 let nonsubsampled_errors = test_utils::ErrorSetup {
267 norm: Check::ulp(10),
268 l2: Check::ulp(10),
269 ip: Check::absrel(2e-5, 1e-4),
270 };
271
272 let subsampled_errors = test_utils::ErrorSetup {
276 norm: Check::absrel(0.0, 0.18),
277 l2: Check::absrel(0.0, 0.18),
278 ip: Check::skip(),
279 };
280
281 let target_dim = |v| TargetDim::Override(NonZeroUsize::new(v).unwrap());
282
283 let dim_combos = [
285 (15, 15, true, TargetDim::Same, &nonsubsampled_errors),
287 (15, 15, true, TargetDim::Natural, &nonsubsampled_errors),
288 (16, 16, true, TargetDim::Same, &nonsubsampled_errors),
289 (100, 100, true, TargetDim::Same, &nonsubsampled_errors),
290 (100, 100, true, TargetDim::Natural, &nonsubsampled_errors),
291 (256, 256, true, TargetDim::Same, &nonsubsampled_errors),
292 (15, 20, true, target_dim(20), &nonsubsampled_errors),
294 (256, 200, false, target_dim(200), &subsampled_errors),
296 ];
297
298 let trials_per_combo = 20;
299 let trials_per_dim = 50;
300
301 let mut rng = StdRng::seed_from_u64(0x30e37c10c36cc64b);
302 for (input, output, preserves_norms, target, errors) in dim_combos {
303 let input_nz = NonZeroUsize::new(input).unwrap();
304 for trial in 0..trials_per_combo {
305 let ctx = &lazy_format!(
306 "input dim = {}, output dim = {}, macro trial {} of {}",
307 input,
308 output,
309 trial,
310 trials_per_combo
311 );
312
313 let mut checker = |io: test_utils::IO<'_>, context: &dyn std::fmt::Display| {
314 test_utils::check_errors(io, context, errors);
315 };
316
317 let mut rng_clone = rng.clone();
319
320 {
322 let transformer =
323 RandomRotation::new(NonZeroUsize::new(input).unwrap(), target, &mut rng);
324 assert_eq!(transformer.input_dim(), input, "{}", ctx);
325 assert_eq!(transformer.output_dim(), output, "{}", ctx);
326 assert_eq!(transformer.preserves_norms(), preserves_norms, "{}", ctx);
327
328 test_utils::test_transform(
329 &transformer,
330 trials_per_dim,
331 &mut checker,
332 &mut rng,
333 ctx,
334 );
335 }
336
337 {
339 let kind = TransformKind::RandomRotation { target_dim: target };
340 let transformer =
341 Transform::new(kind, input_nz, Some(&mut rng_clone), GlobalAllocator)
342 .unwrap();
343
344 assert_eq!(transformer.input_dim(), input);
345 assert_eq!(transformer.output_dim(), output);
346 assert_eq!(transformer.preserves_norms(), preserves_norms);
347
348 test_utils::test_transform(
349 &transformer,
350 trials_per_dim,
351 &mut checker,
352 &mut rng_clone,
353 ctx,
354 )
355 }
356 }
357 }
358 }
359
360 #[cfg(feature = "flatbuffers")]
361 mod serialization {
362 use super::*;
363 use crate::flatbuffers::to_flatbuffer;
364
365 #[test]
366 fn random_rotation() {
367 let mut rng = StdRng::seed_from_u64(0x123456789abcdef0);
368
369 let test_cases = [
371 (5, TargetDim::Same),
372 (10, TargetDim::Natural),
373 (8, TargetDim::Override(NonZeroUsize::new(12).unwrap())),
374 (15, TargetDim::Override(NonZeroUsize::new(10).unwrap())),
375 ];
376
377 for (dim, target_dim) in test_cases {
378 let transform =
379 RandomRotation::new(NonZeroUsize::new(dim).unwrap(), target_dim, &mut rng);
380 let data = to_flatbuffer(|buf| transform.pack(buf));
381
382 let proto = flatbuffers::root::<fb::transforms::RandomRotation>(&data).unwrap();
383 let reloaded = RandomRotation::try_unpack(proto).unwrap();
384
385 assert_eq!(transform, reloaded);
386 }
387
388 {
390 let data = to_flatbuffer(|buf| {
391 let data = buf.create_vector::<f32>(&[1.0, 0.0, 0.0, 1.0]); fb::transforms::RandomRotation::create(
393 buf,
394 &fb::transforms::RandomRotationArgs {
395 data: Some(data),
396 nrows: 0, ncols: 2,
398 },
399 )
400 });
401
402 let proto = flatbuffers::root::<fb::transforms::RandomRotation>(&data).unwrap();
403 let err = RandomRotation::try_unpack(proto).unwrap_err();
404 assert_eq!(err, RandomRotationError::RowsZero);
405 }
406
407 {
408 let data = to_flatbuffer(|buf| {
409 let data = buf.create_vector::<f32>(&[1.0, 0.0, 0.0, 1.0]);
410 fb::transforms::RandomRotation::create(
411 buf,
412 &fb::transforms::RandomRotationArgs {
413 data: Some(data), nrows: 2,
415 ncols: 0, },
417 )
418 });
419
420 let proto = flatbuffers::root::<fb::transforms::RandomRotation>(&data).unwrap();
421 let err = RandomRotation::try_unpack(proto).unwrap_err();
422 assert_eq!(err, RandomRotationError::ColsZero);
423 }
424
425 {
426 let data = to_flatbuffer(|buf| {
427 let data = buf.create_vector::<f32>(&[1.0, 0.0, 0.0]); fb::transforms::RandomRotation::create(
429 buf,
430 &fb::transforms::RandomRotationArgs {
431 data: Some(data),
432 nrows: 2,
433 ncols: 2, },
435 )
436 });
437
438 let proto = flatbuffers::root::<fb::transforms::RandomRotation>(&data).unwrap();
439 let err = RandomRotation::try_unpack(proto).unwrap_err();
440 assert_eq!(err, RandomRotationError::IncorrectDim);
441 }
442 }
443 }
444}