1use std::borrow::Cow;
21
22use ndarray::{Array, ArrayBase, Data, Dimension};
23use numcodecs::{
24 AnyArray, AnyArrayAssignError, AnyArrayDType, AnyArrayView, AnyArrayViewMut, AnyCowArray,
25 Codec, StaticCodec, StaticCodecConfig, StaticCodecVersion,
26};
27use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
28use serde::{Deserialize, Deserializer, Serialize, Serializer};
29use thiserror::Error;
30
31#[derive(Clone, Serialize, Deserialize, JsonSchema)]
32#[schemars(deny_unknown_fields)]
33pub struct BitRoundCodec {
43 #[serde(flatten)]
45 pub mode: BitRoundMode,
46 #[serde(default, rename = "_version")]
48 pub version: StaticCodecVersion<2, 0, 0>,
49}
50
51#[derive(Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
52#[serde(tag = "mode")]
53#[serde(deny_unknown_fields)]
54pub enum BitRoundMode {
56 #[serde(rename = "keepbits")]
58 Keepbits {
59 keepbits: u8,
66 },
67 #[serde(rename = "abs")]
69 AbsoluteError {
70 eb_abs: NonNegative<f64>,
75 },
76 #[serde(rename = "rel")]
78 RelativeError {
79 eb_rel: NonNegative<f64>,
84 },
85}
86
87impl Codec for BitRoundCodec {
88 type Error = BitRoundCodecError;
89
90 fn encode(&self, data: AnyCowArray) -> Result<AnyArray, Self::Error> {
91 match data {
92 AnyCowArray::F32(data) => Ok(AnyArray::F32(bit_round(data, &self.mode)?)),
93 AnyCowArray::F64(data) => Ok(AnyArray::F64(bit_round(data, &self.mode)?)),
94 encoded => Err(BitRoundCodecError::UnsupportedDtype(encoded.dtype())),
95 }
96 }
97
98 fn decode(&self, encoded: AnyCowArray) -> Result<AnyArray, Self::Error> {
99 match encoded {
100 AnyCowArray::F32(encoded) => Ok(AnyArray::F32(encoded.into_owned())),
101 AnyCowArray::F64(encoded) => Ok(AnyArray::F64(encoded.into_owned())),
102 encoded => Err(BitRoundCodecError::UnsupportedDtype(encoded.dtype())),
103 }
104 }
105
106 fn decode_into(
107 &self,
108 encoded: AnyArrayView,
109 mut decoded: AnyArrayViewMut,
110 ) -> Result<(), Self::Error> {
111 if !matches!(encoded.dtype(), AnyArrayDType::F32 | AnyArrayDType::F64) {
112 return Err(BitRoundCodecError::UnsupportedDtype(encoded.dtype()));
113 }
114
115 Ok(decoded.assign(&encoded)?)
116 }
117}
118
119impl StaticCodec for BitRoundCodec {
120 const CODEC_ID: &'static str = "bit-round.rs";
121
122 type Config<'de> = Self;
123
124 fn from_config(config: Self::Config<'_>) -> Self {
125 config
126 }
127
128 fn get_config(&self) -> StaticCodecConfig<'_, Self> {
129 StaticCodecConfig::from(self)
130 }
131}
132
133#[derive(Debug, Error)]
134pub enum BitRoundCodecError {
136 #[error("BitRound does not support the dtype {0}")]
138 UnsupportedDtype(AnyArrayDType),
139 #[error("BitRound encode {keepbits} bits exceed the mantissa size for {dtype}")]
141 ExcessiveKeepBits {
142 keepbits: u8,
144 dtype: AnyArrayDType,
146 },
147 #[error("BitRound cannot decode into the provided array")]
149 MismatchedDecodeIntoArray {
150 #[from]
152 source: AnyArrayAssignError,
153 },
154}
155
156#[expect(clippy::derive_partial_eq_without_eq)] #[derive(Copy, Clone, PartialEq, PartialOrd, Hash)]
158pub struct NonNegative<T: Float>(T);
160
161impl Serialize for NonNegative<f64> {
162 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
163 serializer.serialize_f64(self.0)
164 }
165}
166
167impl<'de> Deserialize<'de> for NonNegative<f64> {
168 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
169 let x = f64::deserialize(deserializer)?;
170
171 if x >= 0.0 {
172 Ok(Self(x))
173 } else {
174 Err(serde::de::Error::invalid_value(
175 serde::de::Unexpected::Float(x),
176 &"a non-negative value",
177 ))
178 }
179 }
180}
181
182impl JsonSchema for NonNegative<f64> {
183 fn schema_name() -> Cow<'static, str> {
184 Cow::Borrowed("NonNegativeF64")
185 }
186
187 fn schema_id() -> Cow<'static, str> {
188 Cow::Borrowed(concat!(module_path!(), "::", "NonNegative<f64>"))
189 }
190
191 fn json_schema(_gen: &mut SchemaGenerator) -> Schema {
192 json_schema!({
193 "type": "number",
194 "minimum": 0.0
195 })
196 }
197}
198
199pub fn bit_round<T: Float, S: Data<Elem = T>, D: Dimension>(
210 data: ArrayBase<S, D>,
211 mode: &BitRoundMode,
212) -> Result<Array<T, D>, BitRoundCodecError> {
213 let (keepbits, keep_non_normal) = match mode {
214 BitRoundMode::Keepbits { keepbits } => {
215 let keepbits = *keepbits;
216 if u32::from(keepbits) > T::MANITSSA_BITS {
217 return Err(BitRoundCodecError::ExcessiveKeepBits {
218 keepbits,
219 dtype: T::TY,
220 });
221 }
222 (u32::from(keepbits), false)
223 }
224 BitRoundMode::AbsoluteError { eb_abs } => {
225 let eb_abs = T::from_f64(eb_abs.0);
226
227 let mut encoded = data.into_owned();
228
229 encoded.mapv_inplace(|x| {
230 if !x.is_normal() {
233 return x;
234 }
235
236 let keepbits = BitRounder::keepbits_from_eb_rel(NonNegative(eb_abs / x.abs()));
237 let bit_round = BitRounder::new(keepbits);
238
239 bit_round.apply(x)
240 });
241
242 return Ok(encoded);
243 }
244 BitRoundMode::RelativeError { eb_rel } => (BitRounder::keepbits_from_eb_rel(*eb_rel), true),
245 };
246
247 let mut encoded = data.into_owned();
248
249 if keepbits == T::MANITSSA_BITS {
252 return Ok(encoded);
253 }
254
255 let bit_round = BitRounder::new(keepbits);
256
257 encoded.mapv_inplace(|x| {
258 if keep_non_normal && !x.is_normal() {
260 return x;
261 }
262
263 bit_round.apply(x)
264 });
265
266 Ok(encoded)
267}
268
269struct BitRounder<T: Float> {
270 ulp_half: T::Binary,
271 keep_mask: T::Binary,
272 shift: u32,
273}
274
275impl<T: Float> BitRounder<T> {
276 #[inline]
277 fn new(keepbits: u32) -> Self {
278 let ulp_half = T::MANTISSA_MASK >> (keepbits + 1);
280 let keep_mask = !(T::MANTISSA_MASK >> keepbits);
282 let shift = T::MANITSSA_BITS - keepbits;
284
285 Self {
286 ulp_half,
287 keep_mask,
288 shift,
289 }
290 }
291
292 fn keepbits_from_eb_rel(eb_rel: NonNegative<T>) -> u32 {
293 let keepbits = -(eb_rel.0.normal_log2_floor()) - 1;
294 #[expect(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
296 let keepbits = i64::from(keepbits).clamp(0, i64::from(T::MANITSSA_BITS)) as u32;
298 keepbits
299 }
300
301 #[inline]
302 fn apply(&self, x: T) -> T {
303 let mut bits = T::to_binary(x);
304
305 bits += self.ulp_half + ((bits >> self.shift) & T::BINARY_ONE);
307
308 bits &= self.keep_mask;
310
311 T::from_binary(bits)
312 }
313}
314
315pub trait Float: Sized + Copy + std::ops::Div<Self, Output = Self> {
317 const MANITSSA_BITS: u32;
319 const MANTISSA_MASK: Self::Binary;
321 const BINARY_ONE: Self::Binary;
323
324 const TY: AnyArrayDType;
326
327 type Binary: Copy
329 + std::ops::Not<Output = Self::Binary>
330 + std::ops::Shr<u32, Output = Self::Binary>
331 + std::ops::Add<Self::Binary, Output = Self::Binary>
332 + std::ops::AddAssign<Self::Binary>
333 + std::ops::BitAnd<Self::Binary, Output = Self::Binary>
334 + std::ops::BitAndAssign<Self::Binary>;
335
336 fn to_binary(self) -> Self::Binary;
338 fn from_binary(u: Self::Binary) -> Self;
340
341 fn is_normal(self) -> bool;
343
344 fn normal_log2_floor(self) -> i16;
346
347 #[must_use]
349 fn abs(self) -> Self;
350
351 fn from_f64(x: f64) -> Self;
353}
354
355impl Float for f32 {
356 type Binary = u32;
357
358 const BINARY_ONE: Self::Binary = 1;
359 const MANITSSA_BITS: u32 = Self::MANTISSA_DIGITS - 1;
360 const MANTISSA_MASK: Self::Binary = (1 << Self::MANITSSA_BITS) - 1;
361 const TY: AnyArrayDType = AnyArrayDType::F32;
362
363 fn to_binary(self) -> Self::Binary {
364 self.to_bits()
365 }
366
367 fn from_binary(u: Self::Binary) -> Self {
368 Self::from_bits(u)
369 }
370
371 fn is_normal(self) -> bool {
372 self.is_normal()
373 }
374
375 fn normal_log2_floor(self) -> i16 {
376 (((self.to_bits() >> 23) & 0xff) as i16) - 127
377 }
378
379 fn abs(self) -> Self {
380 self.abs()
381 }
382
383 #[expect(clippy::cast_possible_truncation)]
384 fn from_f64(x: f64) -> Self {
385 x as Self
386 }
387}
388
389impl Float for f64 {
390 type Binary = u64;
391
392 const BINARY_ONE: Self::Binary = 1;
393 const MANITSSA_BITS: u32 = Self::MANTISSA_DIGITS - 1;
394 const MANTISSA_MASK: Self::Binary = (1 << Self::MANITSSA_BITS) - 1;
395 const TY: AnyArrayDType = AnyArrayDType::F64;
396
397 fn to_binary(self) -> Self::Binary {
398 self.to_bits()
399 }
400
401 fn from_binary(u: Self::Binary) -> Self {
402 Self::from_bits(u)
403 }
404
405 fn is_normal(self) -> bool {
406 self.is_normal()
407 }
408
409 fn normal_log2_floor(self) -> i16 {
410 (((self.to_bits() >> 52) & 0x7ff) as i16) - 1023
411 }
412
413 fn abs(self) -> Self {
414 self.abs()
415 }
416
417 fn from_f64(x: f64) -> Self {
418 x
419 }
420}
421
422#[cfg(test)]
423#[expect(clippy::unwrap_used)]
424mod tests {
425 use ndarray::{Array1, ArrayView1};
426
427 use super::*;
428
429 #[test]
430 #[expect(clippy::too_many_lines)]
431 fn no_mantissa() {
432 assert_eq!(
433 bit_round(
434 ArrayView1::from(&[0.0_f32]),
435 &BitRoundMode::Keepbits { keepbits: 0 }
436 )
437 .unwrap(),
438 Array1::from_vec(vec![0.0_f32])
439 );
440 assert_eq!(
441 bit_round(
442 ArrayView1::from(&[1.0_f32]),
443 &BitRoundMode::Keepbits { keepbits: 0 }
444 )
445 .unwrap(),
446 Array1::from_vec(vec![1.0_f32])
447 );
448 assert_eq!(
450 bit_round(
451 ArrayView1::from(&[1.5_f32]),
452 &BitRoundMode::Keepbits { keepbits: 0 }
453 )
454 .unwrap(),
455 Array1::from_vec(vec![2.0_f32])
456 );
457 assert_eq!(
458 bit_round(
459 ArrayView1::from(&[2.0_f32]),
460 &BitRoundMode::Keepbits { keepbits: 0 }
461 )
462 .unwrap(),
463 Array1::from_vec(vec![2.0_f32])
464 );
465 assert_eq!(
466 bit_round(
467 ArrayView1::from(&[2.5_f32]),
468 &BitRoundMode::Keepbits { keepbits: 0 }
469 )
470 .unwrap(),
471 Array1::from_vec(vec![2.0_f32])
472 );
473 assert_eq!(
475 bit_round(
476 ArrayView1::from(&[3.0_f32]),
477 &BitRoundMode::Keepbits { keepbits: 0 }
478 )
479 .unwrap(),
480 Array1::from_vec(vec![2.0_f32])
481 );
482 assert_eq!(
483 bit_round(
484 ArrayView1::from(&[3.5_f32]),
485 &BitRoundMode::Keepbits { keepbits: 0 }
486 )
487 .unwrap(),
488 Array1::from_vec(vec![4.0_f32])
489 );
490 assert_eq!(
491 bit_round(
492 ArrayView1::from(&[4.0_f32]),
493 &BitRoundMode::Keepbits { keepbits: 0 }
494 )
495 .unwrap(),
496 Array1::from_vec(vec![4.0_f32])
497 );
498 assert_eq!(
499 bit_round(
500 ArrayView1::from(&[5.0_f32]),
501 &BitRoundMode::Keepbits { keepbits: 0 }
502 )
503 .unwrap(),
504 Array1::from_vec(vec![4.0_f32])
505 );
506 assert_eq!(
508 bit_round(
509 ArrayView1::from(&[6.0_f32]),
510 &BitRoundMode::Keepbits { keepbits: 0 }
511 )
512 .unwrap(),
513 Array1::from_vec(vec![8.0_f32])
514 );
515 assert_eq!(
516 bit_round(
517 ArrayView1::from(&[7.0_f32]),
518 &BitRoundMode::Keepbits { keepbits: 0 }
519 )
520 .unwrap(),
521 Array1::from_vec(vec![8.0_f32])
522 );
523 assert_eq!(
524 bit_round(
525 ArrayView1::from(&[8.0_f32]),
526 &BitRoundMode::Keepbits { keepbits: 0 }
527 )
528 .unwrap(),
529 Array1::from_vec(vec![8.0_f32])
530 );
531
532 assert_eq!(
533 bit_round(
534 ArrayView1::from(&[0.0_f64]),
535 &BitRoundMode::Keepbits { keepbits: 0 }
536 )
537 .unwrap(),
538 Array1::from_vec(vec![0.0_f64])
539 );
540 assert_eq!(
541 bit_round(
542 ArrayView1::from(&[1.0_f64]),
543 &BitRoundMode::Keepbits { keepbits: 0 }
544 )
545 .unwrap(),
546 Array1::from_vec(vec![1.0_f64])
547 );
548 assert_eq!(
550 bit_round(
551 ArrayView1::from(&[1.5_f64]),
552 &BitRoundMode::Keepbits { keepbits: 0 }
553 )
554 .unwrap(),
555 Array1::from_vec(vec![2.0_f64])
556 );
557 assert_eq!(
558 bit_round(
559 ArrayView1::from(&[2.0_f64]),
560 &BitRoundMode::Keepbits { keepbits: 0 }
561 )
562 .unwrap(),
563 Array1::from_vec(vec![2.0_f64])
564 );
565 assert_eq!(
566 bit_round(
567 ArrayView1::from(&[2.5_f64]),
568 &BitRoundMode::Keepbits { keepbits: 0 }
569 )
570 .unwrap(),
571 Array1::from_vec(vec![2.0_f64])
572 );
573 assert_eq!(
575 bit_round(
576 ArrayView1::from(&[3.0_f64]),
577 &BitRoundMode::Keepbits { keepbits: 0 }
578 )
579 .unwrap(),
580 Array1::from_vec(vec![2.0_f64])
581 );
582 assert_eq!(
583 bit_round(
584 ArrayView1::from(&[3.5_f64]),
585 &BitRoundMode::Keepbits { keepbits: 0 }
586 )
587 .unwrap(),
588 Array1::from_vec(vec![4.0_f64])
589 );
590 assert_eq!(
591 bit_round(
592 ArrayView1::from(&[4.0_f64]),
593 &BitRoundMode::Keepbits { keepbits: 0 }
594 )
595 .unwrap(),
596 Array1::from_vec(vec![4.0_f64])
597 );
598 assert_eq!(
599 bit_round(
600 ArrayView1::from(&[5.0_f64]),
601 &BitRoundMode::Keepbits { keepbits: 0 }
602 )
603 .unwrap(),
604 Array1::from_vec(vec![4.0_f64])
605 );
606 assert_eq!(
608 bit_round(
609 ArrayView1::from(&[6.0_f64]),
610 &BitRoundMode::Keepbits { keepbits: 0 }
611 )
612 .unwrap(),
613 Array1::from_vec(vec![8.0_f64])
614 );
615 assert_eq!(
616 bit_round(
617 ArrayView1::from(&[7.0_f64]),
618 &BitRoundMode::Keepbits { keepbits: 0 }
619 )
620 .unwrap(),
621 Array1::from_vec(vec![8.0_f64])
622 );
623 assert_eq!(
624 bit_round(
625 ArrayView1::from(&[8.0_f64]),
626 &BitRoundMode::Keepbits { keepbits: 0 }
627 )
628 .unwrap(),
629 Array1::from_vec(vec![8.0_f64])
630 );
631 }
632
633 #[test]
634 #[expect(clippy::cast_possible_truncation)]
635 fn full_mantissa() {
636 fn full<T: Float>(x: T) -> T {
637 T::from_binary(T::to_binary(x) + T::MANTISSA_MASK)
638 }
639
640 for v in [0.0_f32, 1.0_f32, 2.0_f32, 3.0_f32, 4.0_f32] {
641 assert_eq!(
642 bit_round(
643 ArrayView1::from(&[full(v)]),
644 &BitRoundMode::Keepbits {
645 keepbits: f32::MANITSSA_BITS as u8
646 }
647 )
648 .unwrap(),
649 Array1::from_vec(vec![full(v)])
650 );
651 }
652
653 for v in [0.0_f64, 1.0_f64, 2.0_f64, 3.0_f64, 4.0_f64] {
654 assert_eq!(
655 bit_round(
656 ArrayView1::from(&[full(v)]),
657 &BitRoundMode::Keepbits {
658 keepbits: f64::MANITSSA_BITS as u8
659 }
660 )
661 .unwrap(),
662 Array1::from_vec(vec![full(v)])
663 );
664 }
665 }
666
667 #[test]
668 fn normal_log2_floor_f32() {
669 for e in -100_i16..100 {
670 let b = f32::from(e).exp2();
671 for f in [0.55, 0.75, 0.9, 1.0, 1.1, 1.5, 1.95] {
672 let x = b * f;
673
674 #[expect(clippy::cast_possible_truncation)]
675 let math = x.log2().floor() as i16;
676 let binary = x.normal_log2_floor();
677
678 assert_eq!(math, binary, "{x}");
679 }
680 }
681
682 assert_eq!(i32::from(0.0_f32.normal_log2_floor()), f32::MIN_EXP - 2);
683 }
684
685 #[test]
686 fn normal_log2_floor_f64() {
687 for e in -100_i32..100 {
688 let b = f64::from(e).exp2();
689 for f in [0.55, 0.75, 0.9, 1.0, 1.1, 1.5, 1.95] {
690 let x = b * f;
691
692 #[expect(clippy::cast_possible_truncation)]
693 let math = x.log2().floor() as i16;
694 let binary = x.normal_log2_floor();
695
696 assert_eq!(math, binary, "{x}");
697 }
698 }
699
700 assert_eq!(i32::from(0.0_f64.normal_log2_floor()), f64::MIN_EXP - 2);
701 }
702}