1use std::num::NonZeroUsize;
7
8#[cfg(feature = "flatbuffers")]
9use flatbuffers::{FlatBufferBuilder, WIPOffset};
10use rand::{
11 Rng,
12 distr::{Distribution, StandardUniform},
13};
14use thiserror::Error;
15
16#[cfg(feature = "flatbuffers")]
17use super::utils::{bool_to_sign, sign_to_bool};
18use super::{
19 TargetDim,
20 utils::{TransformFailed, check_dims, is_sign, subsample_indices},
21};
22#[cfg(feature = "flatbuffers")]
23use crate::flatbuffers as fb;
24use crate::{
25 algorithms::hadamard_transform,
26 alloc::{Allocator, AllocatorError, Poly, ScopedAllocator, TryClone},
27 utils,
28};
29
30#[derive(Debug)]
55#[cfg_attr(test, derive(PartialEq))]
56pub struct DoubleHadamard<A>
57where
58 A: Allocator,
59{
60 signs0: Poly<[u32], A>,
67 signs1: Poly<[u32], A>,
68
69 target_dim: usize,
71
72 subsample: Option<Poly<[u32], A>>,
74}
75
76impl<A> DoubleHadamard<A>
77where
78 A: Allocator,
79{
80 pub fn new<R>(
99 dim: NonZeroUsize,
100 target_dim: TargetDim,
101 rng: &mut R,
102 allocator: A,
103 ) -> Result<Self, AllocatorError>
104 where
105 R: Rng + ?Sized,
106 {
107 let dim = dim.get();
108
109 let target_dim = match target_dim {
110 TargetDim::Override(target) => target.get(),
111 TargetDim::Same => dim,
112 TargetDim::Natural => dim,
113 };
114
115 let intermediate_dim = dim.max(target_dim);
120
121 let mut sample = |_: usize| {
123 let sign: bool = StandardUniform {}.sample(rng);
124 if sign { 0x8000_0000 } else { 0 }
125 };
126
127 let signs0 = Poly::from_iter((0..dim).map(&mut sample), allocator.clone())?;
130 let signs1 = Poly::from_iter((0..intermediate_dim).map(&mut sample), allocator.clone())?;
131
132 let subsample = if dim > target_dim {
133 Some(subsample_indices(rng, dim, target_dim, allocator)?)
134 } else {
135 None
136 };
137
138 Ok(Self {
139 signs0,
140 signs1,
141 target_dim,
142 subsample,
143 })
144 }
145
146 pub fn try_from_parts(
147 signs0: Poly<[u32], A>,
148 signs1: Poly<[u32], A>,
149 subsample: Option<Poly<[u32], A>>,
150 ) -> Result<Self, DoubleHadamardError> {
151 type E = DoubleHadamardError;
152 if signs0.is_empty() {
153 return Err(E::Signs0Empty);
154 }
155 if signs1.len() < signs0.len() {
156 return Err(E::Signs1TooSmall);
157 }
158 if !signs0.iter().copied().all(is_sign) {
159 return Err(E::Signs0Invalid);
160 }
161 if !signs1.iter().copied().all(is_sign) {
162 return Err(E::Signs1Invalid);
163 }
164
165 let target_dim = if let Some(ref subsample) = subsample {
167 if !utils::is_strictly_monotonic(subsample.iter()) {
168 return Err(E::SubsampleNotMonotonic);
169 }
170
171 match subsample.last() {
172 Some(last) => {
173 if *last as usize >= signs1.len() {
174 return Err(E::LastSubsampleTooLarge);
181 }
182 }
183 None => {
184 return Err(E::InvalidSubsampleLength);
186 }
187 }
188
189 debug_assert!(
190 subsample.len() < signs1.len(),
191 "since we've verified monotonicity and the last element, this is implied"
192 );
193
194 subsample.len()
195 } else {
196 signs1.len()
198 };
199
200 Ok(Self {
201 signs0,
202 signs1,
203 target_dim,
204 subsample,
205 })
206 }
207
208 pub fn input_dim(&self) -> usize {
210 self.signs0.len()
211 }
212
213 pub fn output_dim(&self) -> usize {
215 self.target_dim
216 }
217
218 pub fn preserves_norms(&self) -> bool {
223 self.subsample.is_none()
224 }
225
226 fn intermediate_dim(&self) -> usize {
227 self.input_dim().max(self.output_dim())
228 }
229
230 pub fn transform_into(
239 &self,
240 dst: &mut [f32],
241 src: &[f32],
242 allocator: ScopedAllocator<'_>,
243 ) -> Result<(), TransformFailed> {
244 check_dims(dst, src, self.input_dim(), self.output_dim())?;
245
246 if let Some(indices) = self.subsample.as_ref() {
247 let mut tmp = Poly::broadcast(0.0f32, self.intermediate_dim(), allocator)?;
248 self.transform_full(&mut tmp, src);
249 let rescale = ((tmp.len() as f32) / (indices.len() as f32)).sqrt();
250 dst.iter_mut()
251 .zip(indices.iter())
252 .for_each(|(d, s)| *d = tmp[*s as usize] * rescale);
253 } else {
254 dst[src.len()..].fill(0.0);
256 self.transform_full(dst, src);
257 }
258 Ok(())
259 }
260
261 fn transform_full(&self, tmp: &mut [f32], src: &[f32]) {
262 let intermediate_dim = self.intermediate_dim();
263
264 std::iter::zip(tmp.iter_mut(), src.iter())
265 .zip(self.signs0.iter())
266 .for_each(|((dst, src), sign)| *dst = f32::from_bits(src.to_bits() ^ sign));
267
268 let split = 1usize << (usize::BITS - intermediate_dim.leading_zeros() - 1);
269
270 #[expect(clippy::unwrap_used)]
275 hadamard_transform(&mut tmp[..split]).unwrap();
276
277 tmp.iter_mut()
281 .zip(self.signs1.iter())
282 .for_each(|(dst, sign)| *dst = f32::from_bits(dst.to_bits() ^ sign));
283
284 #[expect(clippy::unwrap_used)]
285 hadamard_transform(&mut tmp[intermediate_dim - split..]).unwrap();
286 }
287}
288
289impl<A> TryClone for DoubleHadamard<A>
290where
291 A: Allocator,
292{
293 fn try_clone(&self) -> Result<Self, AllocatorError> {
294 Ok(Self {
295 signs0: self.signs0.try_clone()?,
296 signs1: self.signs1.try_clone()?,
297 target_dim: self.target_dim,
298 subsample: self.subsample.try_clone()?,
299 })
300 }
301}
302
303#[derive(Debug, Clone, Copy, Error, PartialEq)]
304#[non_exhaustive]
305pub enum DoubleHadamardError {
306 #[error("first signs stage cannot be empty")]
307 Signs0Empty,
308 #[error("first signs stage has invalid coding")]
309 Signs0Invalid,
310
311 #[error("invalid sign representation for second stage")]
312 Signs1Invalid,
313 #[error("second sign stage must be at least as large as the first stage")]
314 Signs1TooSmall,
315
316 #[error("subsample length must equal `target_dim`")]
317 InvalidSubsampleLength,
318 #[error("subsample indices is not monotonic")]
319 SubsampleNotMonotonic,
320 #[error("last subsample index exceeded intermediate dim")]
321 LastSubsampleTooLarge,
322
323 #[error(transparent)]
324 AllocatorError(#[from] AllocatorError),
325}
326
327#[cfg(feature = "flatbuffers")]
329impl<A> DoubleHadamard<A>
330where
331 A: Allocator,
332{
333 pub(crate) fn pack<'a, FA>(
336 &self,
337 buf: &mut FlatBufferBuilder<'a, FA>,
338 ) -> WIPOffset<fb::transforms::DoubleHadamard<'a>>
339 where
340 FA: flatbuffers::Allocator + 'a,
341 {
342 let signs0 = buf.create_vector_from_iter(self.signs0.iter().copied().map(sign_to_bool));
344 let signs1 = buf.create_vector_from_iter(self.signs1.iter().copied().map(sign_to_bool));
345
346 let subsample = self
348 .subsample
349 .as_ref()
350 .map(|indices| buf.create_vector(indices));
351
352 fb::transforms::DoubleHadamard::create(
353 buf,
354 &fb::transforms::DoubleHadamardArgs {
355 signs0: Some(signs0),
356 signs1: Some(signs1),
357 subsample,
358 },
359 )
360 }
361
362 pub(crate) fn try_unpack(
365 alloc: A,
366 proto: fb::transforms::DoubleHadamard<'_>,
367 ) -> Result<Self, DoubleHadamardError> {
368 let signs0 = Poly::from_iter(proto.signs0().iter().map(bool_to_sign), alloc.clone())?;
369 let signs1 = Poly::from_iter(proto.signs1().iter().map(bool_to_sign), alloc.clone())?;
370
371 let subsample = match proto.subsample() {
372 Some(subsample) => Some(Poly::from_iter(subsample.into_iter(), alloc)?),
373 None => None,
374 };
375
376 Self::try_from_parts(signs0, signs1, subsample)
377 }
378}
379
380#[cfg(test)]
385#[cfg(not(miri))]
386mod tests {
387 use diskann_utils::lazy_format;
388 use rand::{SeedableRng, rngs::StdRng};
389
390 use super::*;
391 use crate::{
392 algorithms::transforms::{Transform, TransformKind, test_utils},
393 alloc::GlobalAllocator,
394 test_util::{Check, LimitedAllocator},
395 };
396
397 test_utils::delegate_transformer!(DoubleHadamard<GlobalAllocator>);
398
399 #[test]
400 fn test_double_hadamard() {
401 let natural_errors = test_utils::ErrorSetup {
407 norm: Check::ulp(5),
408 l2: Check::ulp(5),
409 ip: Check::absrel(2.5e-5, 2e-4),
410 };
411
412 let subsampled_errors = test_utils::ErrorSetup {
418 norm: Check::absrel(0.0, 2e-2),
419 l2: Check::absrel(0.0, 2e-2),
420 ip: Check::skip(),
421 };
422
423 let target_dim = |v| TargetDim::Override(NonZeroUsize::new(v).unwrap());
424 let dim_combos = [
425 (15, 15, true, TargetDim::Same, &natural_errors),
427 (15, 15, true, TargetDim::Natural, &natural_errors),
428 (16, 16, true, TargetDim::Same, &natural_errors),
429 (16, 16, true, TargetDim::Natural, &natural_errors),
430 (256, 256, true, TargetDim::Same, &natural_errors),
431 (1000, 1000, true, TargetDim::Same, &natural_errors),
432 (15, 16, true, target_dim(16), &natural_errors),
434 (100, 128, true, target_dim(128), &natural_errors),
435 (15, 32, true, target_dim(32), &natural_errors),
436 (16, 64, true, target_dim(64), &natural_errors),
437 (1024, 1023, false, target_dim(1023), &subsampled_errors),
439 (1000, 999, false, target_dim(999), &subsampled_errors),
440 ];
441
442 let trials_per_combo = 20;
443 let trials_per_dim = 100;
444
445 let mut rng = StdRng::seed_from_u64(0x6d1699abe066147);
446 for (input, output, preserves_norms, target, errors) in dim_combos {
447 let input_nz = NonZeroUsize::new(input).unwrap();
448 for trial in 0..trials_per_combo {
449 let ctx = &lazy_format!(
450 "input dim = {}, output dim = {}, macro trial {} of {}",
451 input,
452 output,
453 trial,
454 trials_per_combo
455 );
456
457 let mut checker = |io: test_utils::IO<'_>, context: &dyn std::fmt::Display| {
458 let d = input.min(output);
459 assert_ne!(&io.input0[..d], &io.output0[..d]);
460 assert_ne!(&io.input1[..d], &io.output1[..d]);
461 test_utils::check_errors(io, context, errors);
462 };
463
464 let mut rng_clone = rng.clone();
466
467 {
469 let transformer = DoubleHadamard::new(
470 NonZeroUsize::new(input).unwrap(),
471 target,
472 &mut rng,
473 GlobalAllocator,
474 )
475 .unwrap();
476
477 assert_eq!(transformer.input_dim(), input);
478 assert_eq!(transformer.output_dim(), output);
479 assert_eq!(transformer.preserves_norms(), preserves_norms);
480
481 test_utils::test_transform(
482 &transformer,
483 trials_per_dim,
484 &mut checker,
485 &mut rng,
486 ctx,
487 );
488 }
489
490 {
492 let kind = TransformKind::DoubleHadamard { target_dim: target };
493 let transformer =
494 Transform::new(kind, input_nz, Some(&mut rng_clone), GlobalAllocator)
495 .unwrap();
496
497 assert_eq!(transformer.input_dim(), input);
498 assert_eq!(transformer.output_dim(), output);
499 assert_eq!(transformer.preserves_norms(), preserves_norms);
500
501 test_utils::test_transform(
502 &transformer,
503 trials_per_dim,
504 &mut checker,
505 &mut rng_clone,
506 ctx,
507 )
508 }
509 }
510
511 let mut allocation_rng = rng.clone();
512 let transformer =
513 DoubleHadamard::new(input_nz, target, &mut allocation_rng, GlobalAllocator)
514 .unwrap();
515 let mut src = vec![1.0; input];
516 let mut dst = vec![f32::NAN; output + 2];
517 for value in [1.0, 0.0, -0.0] {
518 src.fill(value);
519 if !preserves_norms {
520 let allocator = LimitedAllocator::new(0);
521 assert_eq!(
522 transformer.transform_into(
523 &mut dst[1..=output],
524 &src,
525 ScopedAllocator::new(&allocator),
526 ),
527 Err(TransformFailed::AllocatorError(AllocatorError))
528 );
529 }
530 let allocator = LimitedAllocator::new(usize::from(!preserves_norms));
531 transformer
532 .transform_into(&mut dst[1..=output], &src, ScopedAllocator::new(&allocator))
533 .unwrap();
534 assert_eq!(allocator.remaining(), 0);
535 assert!(dst[1..=output].iter().all(|value| value.is_finite()));
536 if value == 0.0 {
537 assert!(dst[1..=output].iter().all(|value| *value == 0.0));
538 }
539 assert!(dst[0].is_nan() && dst[output + 1].is_nan());
540 }
541 }
542 }
543
544 #[cfg(feature = "flatbuffers")]
545 mod serialization {
546 use super::*;
547 use crate::flatbuffers::to_flatbuffer;
548
549 #[test]
550 fn double_hadamard() {
551 let mut rng = StdRng::seed_from_u64(0x123456789abcdef0);
552 let alloc = GlobalAllocator;
553
554 let test_cases = [
556 (5, TargetDim::Same),
558 (8, TargetDim::Same),
559 (10, TargetDim::Natural),
560 (16, TargetDim::Natural),
561 (8, TargetDim::Override(NonZeroUsize::new(12).unwrap())),
563 (10, TargetDim::Override(NonZeroUsize::new(12).unwrap())),
564 (15, TargetDim::Override(NonZeroUsize::new(16).unwrap())),
566 (16, TargetDim::Override(NonZeroUsize::new(16).unwrap())),
567 (15, TargetDim::Override(NonZeroUsize::new(32).unwrap())),
568 (16, TargetDim::Override(NonZeroUsize::new(32).unwrap())),
569 (15, TargetDim::Override(NonZeroUsize::new(10).unwrap())),
571 (16, TargetDim::Override(NonZeroUsize::new(10).unwrap())),
572 ];
573
574 for (dim, target_dim) in test_cases {
575 let transform = DoubleHadamard::new(
576 NonZeroUsize::new(dim).unwrap(),
577 target_dim,
578 &mut rng,
579 alloc,
580 )
581 .unwrap();
582 let data = to_flatbuffer(|buf| transform.pack(buf));
583
584 let proto = flatbuffers::root::<fb::transforms::DoubleHadamard>(&data).unwrap();
585 let reloaded = DoubleHadamard::try_unpack(alloc, proto).unwrap();
586
587 assert_eq!(transform, reloaded);
588 }
589
590 let gen_err = |x: DoubleHadamard<_>| -> DoubleHadamardError {
591 let data = to_flatbuffer(|buf| x.pack(buf));
592 let proto = flatbuffers::root::<fb::transforms::DoubleHadamard>(&data).unwrap();
593 DoubleHadamard::try_unpack(alloc, proto).unwrap_err()
594 };
595
596 type E = DoubleHadamardError;
597 let error_cases = [
598 (
600 vec![0, 0, 0, 0, 0], vec![0, 0, 0, 0], 4,
603 None,
604 E::Signs1TooSmall,
605 ),
606 (
608 vec![], vec![0, 0, 0, 0],
610 4,
611 None,
612 E::Signs0Empty,
613 ),
614 (
616 vec![0, 0, 0, 0],
617 vec![0, 0, 0, 0],
618 3,
619 Some(vec![0, 2, 1]), E::SubsampleNotMonotonic,
621 ),
622 (
624 vec![0, 0, 0, 0],
625 vec![0, 0, 0, 0],
626 3,
627 Some(vec![0, 1, 1]), E::SubsampleNotMonotonic,
629 ),
630 (
632 vec![0, 0, 0], vec![0, 0, 0], 2,
635 Some(vec![0, 3]), E::LastSubsampleTooLarge,
637 ),
638 (
640 vec![0, 0, 0], vec![0, 0, 0], 2,
643 Some(vec![]), E::InvalidSubsampleLength,
645 ),
646 ];
647
648 let poly = |v: &Vec<u32>| Poly::from_iter(v.iter().copied(), alloc).unwrap();
649
650 for (signs0, signs1, target_dim, subsample, expected) in error_cases.iter() {
651 println!(
652 "on case ({:?}, {:?}, {}, {:?})",
653 signs0, signs1, target_dim, subsample,
654 );
655 let err = gen_err(DoubleHadamard {
656 signs0: poly(signs0),
657 signs1: poly(signs1),
658 target_dim: *target_dim,
659 subsample: subsample.as_ref().map(poly),
660 });
661
662 assert_eq!(
663 err, *expected,
664 "failed for case ({:?}, {:?}, {}, {:?})",
665 signs0, signs1, target_dim, subsample
666 );
667 }
668 }
669 }
670}