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)]
43#[cfg_attr(test, derive(PartialEq))]
44pub struct PaddingHadamard<A>
45where
46 A: Allocator,
47{
48 signs: Poly<[u32], A>,
61
62 padded_dim: usize,
65
66 subsample: Option<Poly<[u32], A>>,
69}
70
71impl<A> PaddingHadamard<A>
72where
73 A: Allocator,
74{
75 pub fn new<R>(
95 dim: NonZeroUsize,
96 target: TargetDim,
97 rng: &mut R,
98 allocator: A,
99 ) -> Result<Self, AllocatorError>
100 where
101 R: Rng + ?Sized,
102 {
103 let signs = Poly::from_iter(
104 (0..dim.get()).map(|_| {
105 let sign: bool = StandardUniform {}.sample(rng);
106 if sign { 0x8000_0000 } else { 0 }
107 }),
108 allocator.clone(),
109 )?;
110
111 let (padded_dim, target_dim) = match target {
112 TargetDim::Same => (dim.get().next_power_of_two(), dim.get()),
113 TargetDim::Natural => {
114 let next = dim.get().next_power_of_two();
115 (next, next)
116 }
117 TargetDim::Override(target) => {
118 (target.max(dim).get().next_power_of_two(), target.get())
119 }
120 };
121
122 let subsample = if padded_dim > target_dim {
123 Some(subsample_indices(rng, padded_dim, target_dim, allocator)?)
124 } else {
125 None
126 };
127
128 Ok(Self {
129 signs,
130 padded_dim,
131 subsample,
132 })
133 }
134
135 pub fn try_from_parts(
138 signs: Poly<[u32], A>,
139 padded_dim: usize,
140 subsample: Option<Poly<[u32], A>>,
141 ) -> Result<Self, PaddingHadamardError> {
142 if !signs.iter().copied().all(is_sign) {
143 return Err(PaddingHadamardError::InvalidSignRepresentation);
144 }
145
146 if signs.len() > padded_dim {
147 return Err(PaddingHadamardError::SignsTooLong);
148 }
149
150 if !padded_dim.is_power_of_two() {
151 return Err(PaddingHadamardError::DimNotPowerOfTwo);
152 }
153
154 if let Some(ref subsample) = subsample {
155 if !utils::is_strictly_monotonic(subsample.iter()) {
156 return Err(PaddingHadamardError::SubsampleNotMonotonic);
157 }
158
159 if let Some(last) = subsample.last() {
160 if *last as usize >= padded_dim {
161 return Err(PaddingHadamardError::LastSubsampleTooLarge);
162 }
163 } else {
164 return Err(PaddingHadamardError::SubsampleEmpty);
165 }
166 }
167
168 Ok(Self {
169 signs,
170 padded_dim,
171 subsample,
172 })
173 }
174
175 pub fn input_dim(&self) -> usize {
177 self.signs.len()
178 }
179
180 pub fn output_dim(&self) -> usize {
182 match &self.subsample {
183 None => self.padded_dim,
184 Some(v) => v.len(),
185 }
186 }
187
188 pub fn preserves_norms(&self) -> bool {
193 self.subsample.is_none()
194 }
195
196 fn copy_and_flip_signs(&self, dst: &mut [f32], src: &[f32]) {
205 debug_assert_eq!(dst.len(), self.padded_dim);
206 debug_assert_eq!(src.len(), self.input_dim());
207
208 std::iter::zip(dst.iter_mut(), src.iter())
210 .zip(self.signs.iter())
211 .for_each(|((dst, src), sign)| *dst = f32::from_bits(src.to_bits() ^ sign));
212
213 dst.iter_mut()
215 .skip(self.input_dim())
216 .for_each(|dst| *dst = 0.0);
217 }
218
219 pub fn transform_into(
228 &self,
229 dst: &mut [f32],
230 src: &[f32],
231 allocator: ScopedAllocator<'_>,
232 ) -> Result<(), TransformFailed> {
233 let input_dim = self.input_dim();
234 let output_dim = self.output_dim();
235 check_dims(dst, src, input_dim, output_dim)?;
236
237 match &self.subsample {
239 None => {
240 self.copy_and_flip_signs(dst, src);
242
243 #[expect(clippy::unwrap_used)]
248 hadamard_transform(dst).unwrap();
249 }
250 Some(indices) => {
251 let mut tmp = Poly::broadcast(0.0f32, self.padded_dim, allocator)?;
252
253 self.copy_and_flip_signs(&mut tmp, src);
254
255 #[expect(clippy::unwrap_used)]
260 hadamard_transform(&mut tmp).unwrap();
261
262 let rescale = ((tmp.len() as f32) / (indices.len() as f32)).sqrt();
263 debug_assert_eq!(dst.len(), indices.len());
264 std::iter::zip(dst.iter_mut(), indices.iter()).for_each(
265 |(d, i): (&mut f32, &u32)| {
266 *d = tmp[*i as usize] * rescale;
267 },
268 );
269 }
270 }
271
272 Ok(())
273 }
274}
275
276impl<A> TryClone for PaddingHadamard<A>
277where
278 A: Allocator,
279{
280 fn try_clone(&self) -> Result<Self, AllocatorError> {
281 Ok(Self {
282 signs: self.signs.try_clone()?,
283 padded_dim: self.padded_dim,
284 subsample: self.subsample.try_clone()?,
285 })
286 }
287}
288
289#[derive(Debug, Clone, Copy, Error, PartialEq)]
291#[non_exhaustive]
292pub enum PaddingHadamardError {
293 #[error("an invalid sign representation was discovered")]
294 InvalidSignRepresentation,
295 #[error("`signs` length exceeds `padded_dim`")]
296 SignsTooLong,
297 #[error("padded dim is not a power of two")]
298 DimNotPowerOfTwo,
299 #[error("subsample indices cannot be empty")]
300 SubsampleEmpty,
301 #[error("subsample indices is not monotonic")]
302 SubsampleNotMonotonic,
303 #[error("last subsample index exceeded `padded_dim`")]
304 LastSubsampleTooLarge,
305 #[error(transparent)]
306 AllocatorError(#[from] AllocatorError),
307}
308
309#[cfg(feature = "flatbuffers")]
310impl<A> PaddingHadamard<A>
311where
312 A: Allocator,
313{
314 pub(crate) fn pack<'a, FA>(
316 &self,
317 buf: &mut FlatBufferBuilder<'a, FA>,
318 ) -> WIPOffset<fb::transforms::PaddingHadamard<'a>>
319 where
320 FA: flatbuffers::Allocator + 'a,
321 {
322 let signs = buf.create_vector_from_iter(self.signs.iter().copied().map(sign_to_bool));
324
325 let subsample = self
327 .subsample
328 .as_ref()
329 .map(|indices| buf.create_vector(indices));
330
331 fb::transforms::PaddingHadamard::create(
333 buf,
334 &fb::transforms::PaddingHadamardArgs {
335 signs: Some(signs),
336 padded_dim: self.padded_dim as u32,
337 subsample,
338 },
339 )
340 }
341
342 pub(crate) fn try_unpack(
345 alloc: A,
346 proto: fb::transforms::PaddingHadamard<'_>,
347 ) -> Result<Self, PaddingHadamardError> {
348 let signs = Poly::from_iter(proto.signs().iter().map(bool_to_sign), alloc.clone())?;
349
350 let subsample = match proto.subsample() {
351 Some(subsample) => Some(Poly::from_iter(subsample.into_iter(), alloc)?),
352 None => None,
353 };
354
355 Self::try_from_parts(signs, proto.padded_dim() as usize, subsample)
356 }
357}
358
359#[cfg(test)]
364mod tests {
365 #[cfg(not(miri))]
366 use crate::algorithms::transforms::{Transform, TransformKind, test_utils};
367 #[cfg(not(miri))]
368 use diskann_utils::lazy_format;
369 use rand::{SeedableRng, rngs::StdRng};
370
371 use super::*;
372 use crate::alloc::GlobalAllocator;
373 #[cfg(not(miri))]
374 use crate::test_util::Check;
375
376 #[test]
379 fn test_sign_flipping() {
380 let mut rng = StdRng::seed_from_u64(0xf8ee12b1e9f33dbd);
381 let dim = 14;
382
383 let transform = PaddingHadamard::new(
384 NonZeroUsize::new(dim).unwrap(),
385 TargetDim::Same,
386 &mut rng,
387 GlobalAllocator,
388 )
389 .unwrap();
390
391 assert_eq!(transform.input_dim(), dim);
392 assert_eq!(transform.output_dim(), dim);
393
394 let positive = vec![1.0f32; dim];
395 let negative = vec![-1.0f32; dim];
396
397 let mut output = vec![f32::INFINITY; 16];
398
399 transform.copy_and_flip_signs(&mut output, &positive);
401
402 let mut unflipped = 0;
403 let mut flipped = 0;
404 std::iter::zip(output.iter(), transform.signs.iter())
405 .enumerate()
406 .for_each(|(i, (o, s))| {
407 if *s == 0x8000_0000 {
408 flipped += 1;
409 assert_eq!(*o, -1.0, "expected entry {} to be flipped", i);
410 } else {
411 unflipped += 1;
412 assert_eq!(*o, 1.0, "expected entry {} to be unchanged", i);
413 }
414 });
415
416 assert!(unflipped > 0);
418 assert!(flipped > 0);
419
420 assert_eq!(output[14], 0.0f32);
422 assert_eq!(output[15], 0.0f32);
423
424 output.fill(f32::INFINITY);
426 transform.copy_and_flip_signs(&mut output, &negative);
427 std::iter::zip(output.iter(), transform.signs.iter())
428 .enumerate()
429 .for_each(|(i, (o, s))| {
430 if *s == 0x8000_0000 {
431 assert_eq!(*o, 1.0, "expected entry {} to be flipped", i);
432 } else {
433 assert_eq!(*o, -1.0, "expected entry {} to be unchanged", i);
434 }
435 });
436
437 assert_eq!(output[14], 0.0f32);
439 assert_eq!(output[15], 0.0f32);
440 }
441
442 #[cfg(not(miri))]
443 test_utils::delegate_transformer!(PaddingHadamard<GlobalAllocator>);
444
445 #[test]
448 #[cfg(not(miri))]
449 fn test_padding_hadamard() {
450 let natural_errors = test_utils::ErrorSetup {
456 norm: Check::ulp(4),
457 l2: Check::ulp(4),
458 ip: Check::absrel(5.0e-6, 2e-4),
459 };
460
461 let subsampled_errors = test_utils::ErrorSetup {
467 norm: Check::absrel(0.0, 1e-1),
468 l2: Check::absrel(0.0, 1e-1),
469 ip: Check::skip(),
470 };
471
472 let target_dim = |v| TargetDim::Override(NonZeroUsize::new(v).unwrap());
473
474 let dim_combos = [
475 (15, 16, true, target_dim(16), &natural_errors),
477 (15, 16, true, TargetDim::Natural, &natural_errors),
478 (16, 16, true, TargetDim::Same, &natural_errors),
479 (16, 16, true, TargetDim::Natural, &natural_errors),
480 (16, 32, true, target_dim(32), &natural_errors),
481 (16, 64, true, target_dim(64), &natural_errors),
482 (100, 128, true, target_dim(128), &natural_errors),
483 (100, 128, true, TargetDim::Natural, &natural_errors),
484 (256, 256, true, target_dim(256), &natural_errors),
485 (1000, 1000, false, TargetDim::Same, &subsampled_errors),
487 (500, 1000, false, target_dim(1000), &subsampled_errors),
488 ];
489
490 let trials_per_combo = 20;
491 let trials_per_dim = 100;
492
493 let mut rng = StdRng::seed_from_u64(0x6d1699abe0626147);
494 for (input, output, preserves_norms, target, errors) in dim_combos {
495 let input_nz = NonZeroUsize::new(input).unwrap();
496 for trial in 0..trials_per_combo {
497 let ctx = lazy_format!(
498 "input dim = {}, output dim = {}, macro trial {} of {}",
499 input,
500 output,
501 trial,
502 trials_per_combo
503 );
504
505 let mut checker = |io: test_utils::IO<'_>, context: &dyn std::fmt::Display| {
506 assert_ne!(io.input0, &io.output0[..input]);
507 assert_ne!(io.input1, &io.output1[..input]);
508 test_utils::check_errors(io, context, errors);
509 };
510
511 let mut rng_clone = rng.clone();
513
514 {
516 let transformer = PaddingHadamard::new(
517 NonZeroUsize::new(input).unwrap(),
518 target,
519 &mut rng,
520 GlobalAllocator,
521 )
522 .unwrap();
523
524 assert_eq!(transformer.input_dim(), input);
525 assert_eq!(transformer.output_dim(), output);
526 assert_eq!(transformer.preserves_norms(), preserves_norms);
527
528 test_utils::test_transform(
529 &transformer,
530 trials_per_dim,
531 &mut checker,
532 &mut rng,
533 &ctx,
534 )
535 }
536
537 {
539 let kind = TransformKind::PaddingHadamard { target_dim: target };
540 let transformer =
541 Transform::new(kind, input_nz, Some(&mut rng_clone), GlobalAllocator)
542 .unwrap();
543
544 assert_eq!(transformer.input_dim(), input);
545 assert_eq!(transformer.output_dim(), output);
546 assert_eq!(transformer.preserves_norms(), preserves_norms);
547
548 test_utils::test_transform(
549 &transformer,
550 trials_per_dim,
551 &mut checker,
552 &mut rng_clone,
553 &ctx,
554 )
555 }
556 }
557 }
558 }
559
560 #[cfg(feature = "flatbuffers")]
561 mod serialization {
562 use super::*;
563 use crate::{flatbuffers::to_flatbuffer, poly};
564
565 #[test]
566 fn padding_hadamard() {
567 let mut rng = StdRng::seed_from_u64(0x123456789abcdef0);
568 let alloc = GlobalAllocator;
569
570 let test_cases = [
572 (5, TargetDim::Same),
573 (10, TargetDim::Natural),
574 (16, TargetDim::Natural),
575 (8, TargetDim::Override(NonZeroUsize::new(12).unwrap())),
576 (15, TargetDim::Override(NonZeroUsize::new(10).unwrap())),
577 ];
578
579 for (dim, target_dim) in test_cases {
580 let transform = PaddingHadamard::new(
581 NonZeroUsize::new(dim).unwrap(),
582 target_dim,
583 &mut rng,
584 alloc,
585 )
586 .unwrap();
587 let data = to_flatbuffer(|buf| transform.pack(buf));
588
589 let proto = flatbuffers::root::<fb::transforms::PaddingHadamard>(&data).unwrap();
590 let reloaded = PaddingHadamard::try_unpack(alloc, proto).unwrap();
591
592 assert_eq!(transform, reloaded);
593 }
594
595 let gen_err = |x: PaddingHadamard<_>| -> PaddingHadamardError {
596 let data = to_flatbuffer(|buf| x.pack(buf));
597 let proto = flatbuffers::root::<fb::transforms::PaddingHadamard>(&data).unwrap();
598 PaddingHadamard::try_unpack(alloc, proto).unwrap_err()
599 };
600
601 {
603 let err = gen_err(PaddingHadamard {
604 signs: poly!([0, 0, 0, 0, 0], alloc).unwrap(), padded_dim: 4,
606 subsample: None,
607 });
608
609 assert_eq!(err, PaddingHadamardError::SignsTooLong);
610 }
611
612 {
614 let err = gen_err(PaddingHadamard {
615 signs: poly!([0, 0, 0, 0, 0], alloc).unwrap(),
616 padded_dim: 5, subsample: None,
618 });
619
620 assert_eq!(err, PaddingHadamardError::DimNotPowerOfTwo);
621 }
622
623 {
625 let err = gen_err(PaddingHadamard {
626 signs: poly!([0, 0, 0, 0], alloc).unwrap(),
627 padded_dim: 4,
628 subsample: Some(poly!([], alloc).unwrap()), });
630
631 assert_eq!(err, PaddingHadamardError::SubsampleEmpty);
632 }
633
634 {
636 let err = gen_err(PaddingHadamard {
637 signs: poly!([0, 0, 0, 0], alloc).unwrap(),
638 padded_dim: 4,
639 subsample: Some(poly!([0, 2, 2], alloc).unwrap()), });
641 assert_eq!(err, PaddingHadamardError::SubsampleNotMonotonic);
642 }
643
644 {
646 let err = gen_err(PaddingHadamard {
647 signs: poly!([0, 0, 0, 0], alloc).unwrap(),
648 padded_dim: 4,
649 subsample: Some(poly!([0, 1, 2, 3, 4], alloc).unwrap()),
650 });
651
652 assert_eq!(err, PaddingHadamardError::LastSubsampleTooLarge);
653 }
654
655 {
657 let err = gen_err(PaddingHadamard {
658 signs: poly!([0, 0, 0, 0], alloc).unwrap(),
659 padded_dim: 4,
660 subsample: Some(poly!([0, 4], alloc).unwrap()),
661 });
662
663 assert_eq!(err, PaddingHadamardError::LastSubsampleTooLarge);
664 }
665 }
666 }
667}