use std::borrow::Cow;
use ndarray::{Array, ArrayBase, Data, Dimension};
use numcodecs::{
AnyArray, AnyArrayAssignError, AnyArrayDType, AnyArrayView, AnyArrayViewMut, AnyCowArray,
Codec, StaticCodec, StaticCodecConfig, StaticCodecVersion,
};
use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use thiserror::Error;
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct BitRoundCodec {
#[serde(flatten)]
pub mode: BitRoundMode,
#[serde(default, rename = "_version")]
pub version: StaticCodecVersion<2, 0, 0>,
}
#[derive(Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "mode")]
#[serde(deny_unknown_fields)]
pub enum BitRoundMode {
#[serde(rename = "keepbits")]
Keepbits {
keepbits: u8,
},
#[serde(rename = "abs")]
AbsoluteError {
eb_abs: NonNegative<f64>,
},
#[serde(rename = "rel")]
RelativeError {
eb_rel: NonNegative<f64>,
},
}
impl Codec for BitRoundCodec {
type Error = BitRoundCodecError;
fn encode(&self, data: AnyCowArray) -> Result<AnyArray, Self::Error> {
match data {
AnyCowArray::F32(data) => Ok(AnyArray::F32(bit_round(data, &self.mode)?)),
AnyCowArray::F64(data) => Ok(AnyArray::F64(bit_round(data, &self.mode)?)),
encoded => Err(BitRoundCodecError::UnsupportedDtype(encoded.dtype())),
}
}
fn decode(&self, encoded: AnyCowArray) -> Result<AnyArray, Self::Error> {
match encoded {
AnyCowArray::F32(encoded) => Ok(AnyArray::F32(encoded.into_owned())),
AnyCowArray::F64(encoded) => Ok(AnyArray::F64(encoded.into_owned())),
encoded => Err(BitRoundCodecError::UnsupportedDtype(encoded.dtype())),
}
}
fn decode_into(
&self,
encoded: AnyArrayView,
mut decoded: AnyArrayViewMut,
) -> Result<(), Self::Error> {
if !matches!(encoded.dtype(), AnyArrayDType::F32 | AnyArrayDType::F64) {
return Err(BitRoundCodecError::UnsupportedDtype(encoded.dtype()));
}
Ok(decoded.assign(&encoded)?)
}
}
impl StaticCodec for BitRoundCodec {
const CODEC_ID: &'static str = "bit-round.rs";
type Config<'de> = Self;
fn from_config(config: Self::Config<'_>) -> Self {
config
}
fn get_config(&self) -> StaticCodecConfig<'_, Self> {
StaticCodecConfig::from(self)
}
}
#[derive(Debug, Error)]
pub enum BitRoundCodecError {
#[error("BitRound does not support the dtype {0}")]
UnsupportedDtype(AnyArrayDType),
#[error("BitRound encode {keepbits} bits exceed the mantissa size for {dtype}")]
ExcessiveKeepBits {
keepbits: u8,
dtype: AnyArrayDType,
},
#[error("BitRound cannot decode into the provided array")]
MismatchedDecodeIntoArray {
#[from]
source: AnyArrayAssignError,
},
}
#[expect(clippy::derive_partial_eq_without_eq)] #[derive(Copy, Clone, PartialEq, PartialOrd, Hash)]
pub struct NonNegative<T: Float>(T);
impl Serialize for NonNegative<f64> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_f64(self.0)
}
}
impl<'de> Deserialize<'de> for NonNegative<f64> {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let x = f64::deserialize(deserializer)?;
if x >= 0.0 {
Ok(Self(x))
} else {
Err(serde::de::Error::invalid_value(
serde::de::Unexpected::Float(x),
&"a non-negative value",
))
}
}
}
impl JsonSchema for NonNegative<f64> {
fn schema_name() -> Cow<'static, str> {
Cow::Borrowed("NonNegativeF64")
}
fn schema_id() -> Cow<'static, str> {
Cow::Borrowed(concat!(module_path!(), "::", "NonNegative<f64>"))
}
fn json_schema(_gen: &mut SchemaGenerator) -> Schema {
json_schema!({
"type": "number",
"minimum": 0.0
})
}
}
pub fn bit_round<T: Float, S: Data<Elem = T>, D: Dimension>(
data: ArrayBase<S, D>,
mode: &BitRoundMode,
) -> Result<Array<T, D>, BitRoundCodecError> {
let (keepbits, keep_non_normal) = match mode {
BitRoundMode::Keepbits { keepbits } => {
let keepbits = *keepbits;
if u32::from(keepbits) > T::MANITSSA_BITS {
return Err(BitRoundCodecError::ExcessiveKeepBits {
keepbits,
dtype: T::TY,
});
}
(u32::from(keepbits), false)
}
BitRoundMode::AbsoluteError { eb_abs } => {
let eb_abs = T::from_f64(eb_abs.0);
let mut encoded = data.into_owned();
encoded.mapv_inplace(|x| {
if !x.is_normal() {
return x;
}
let keepbits = BitRounder::keepbits_from_eb_rel(NonNegative(eb_abs / x.abs()));
let bit_round = BitRounder::new(keepbits);
bit_round.apply(x)
});
return Ok(encoded);
}
BitRoundMode::RelativeError { eb_rel } => (BitRounder::keepbits_from_eb_rel(*eb_rel), true),
};
let mut encoded = data.into_owned();
if keepbits == T::MANITSSA_BITS {
return Ok(encoded);
}
let bit_round = BitRounder::new(keepbits);
encoded.mapv_inplace(|x| {
if keep_non_normal && !x.is_normal() {
return x;
}
bit_round.apply(x)
});
Ok(encoded)
}
struct BitRounder<T: Float> {
ulp_half: T::Binary,
keep_mask: T::Binary,
shift: u32,
}
impl<T: Float> BitRounder<T> {
#[inline]
fn new(keepbits: u32) -> Self {
let ulp_half = T::MANTISSA_MASK >> (keepbits + 1);
let keep_mask = !(T::MANTISSA_MASK >> keepbits);
let shift = T::MANITSSA_BITS - keepbits;
Self {
ulp_half,
keep_mask,
shift,
}
}
fn keepbits_from_eb_rel(eb_rel: NonNegative<T>) -> u32 {
let keepbits = -(eb_rel.0.normal_log2_floor()) - 1;
#[expect(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
let keepbits = i64::from(keepbits).clamp(0, i64::from(T::MANITSSA_BITS)) as u32;
keepbits
}
#[inline]
fn apply(&self, x: T) -> T {
let mut bits = T::to_binary(x);
bits += self.ulp_half + ((bits >> self.shift) & T::BINARY_ONE);
bits &= self.keep_mask;
T::from_binary(bits)
}
}
pub trait Float: Sized + Copy + std::ops::Div<Self, Output = Self> {
const MANITSSA_BITS: u32;
const MANTISSA_MASK: Self::Binary;
const BINARY_ONE: Self::Binary;
const TY: AnyArrayDType;
type Binary: Copy
+ std::ops::Not<Output = Self::Binary>
+ std::ops::Shr<u32, Output = Self::Binary>
+ std::ops::Add<Self::Binary, Output = Self::Binary>
+ std::ops::AddAssign<Self::Binary>
+ std::ops::BitAnd<Self::Binary, Output = Self::Binary>
+ std::ops::BitAndAssign<Self::Binary>;
fn to_binary(self) -> Self::Binary;
fn from_binary(u: Self::Binary) -> Self;
fn is_normal(self) -> bool;
fn normal_log2_floor(self) -> i16;
#[must_use]
fn abs(self) -> Self;
fn from_f64(x: f64) -> Self;
}
impl Float for f32 {
type Binary = u32;
const BINARY_ONE: Self::Binary = 1;
const MANITSSA_BITS: u32 = Self::MANTISSA_DIGITS - 1;
const MANTISSA_MASK: Self::Binary = (1 << Self::MANITSSA_BITS) - 1;
const TY: AnyArrayDType = AnyArrayDType::F32;
fn to_binary(self) -> Self::Binary {
self.to_bits()
}
fn from_binary(u: Self::Binary) -> Self {
Self::from_bits(u)
}
fn is_normal(self) -> bool {
self.is_normal()
}
fn normal_log2_floor(self) -> i16 {
(((self.to_bits() >> 23) & 0xff) as i16) - 127
}
fn abs(self) -> Self {
self.abs()
}
#[expect(clippy::cast_possible_truncation)]
fn from_f64(x: f64) -> Self {
x as Self
}
}
impl Float for f64 {
type Binary = u64;
const BINARY_ONE: Self::Binary = 1;
const MANITSSA_BITS: u32 = Self::MANTISSA_DIGITS - 1;
const MANTISSA_MASK: Self::Binary = (1 << Self::MANITSSA_BITS) - 1;
const TY: AnyArrayDType = AnyArrayDType::F64;
fn to_binary(self) -> Self::Binary {
self.to_bits()
}
fn from_binary(u: Self::Binary) -> Self {
Self::from_bits(u)
}
fn is_normal(self) -> bool {
self.is_normal()
}
fn normal_log2_floor(self) -> i16 {
(((self.to_bits() >> 52) & 0x7ff) as i16) - 1023
}
fn abs(self) -> Self {
self.abs()
}
fn from_f64(x: f64) -> Self {
x
}
}
#[cfg(test)]
#[expect(clippy::unwrap_used)]
mod tests {
use ndarray::{Array1, ArrayView1};
use super::*;
#[test]
#[expect(clippy::too_many_lines)]
fn no_mantissa() {
assert_eq!(
bit_round(
ArrayView1::from(&[0.0_f32]),
&BitRoundMode::Keepbits { keepbits: 0 }
)
.unwrap(),
Array1::from_vec(vec![0.0_f32])
);
assert_eq!(
bit_round(
ArrayView1::from(&[1.0_f32]),
&BitRoundMode::Keepbits { keepbits: 0 }
)
.unwrap(),
Array1::from_vec(vec![1.0_f32])
);
assert_eq!(
bit_round(
ArrayView1::from(&[1.5_f32]),
&BitRoundMode::Keepbits { keepbits: 0 }
)
.unwrap(),
Array1::from_vec(vec![2.0_f32])
);
assert_eq!(
bit_round(
ArrayView1::from(&[2.0_f32]),
&BitRoundMode::Keepbits { keepbits: 0 }
)
.unwrap(),
Array1::from_vec(vec![2.0_f32])
);
assert_eq!(
bit_round(
ArrayView1::from(&[2.5_f32]),
&BitRoundMode::Keepbits { keepbits: 0 }
)
.unwrap(),
Array1::from_vec(vec![2.0_f32])
);
assert_eq!(
bit_round(
ArrayView1::from(&[3.0_f32]),
&BitRoundMode::Keepbits { keepbits: 0 }
)
.unwrap(),
Array1::from_vec(vec![2.0_f32])
);
assert_eq!(
bit_round(
ArrayView1::from(&[3.5_f32]),
&BitRoundMode::Keepbits { keepbits: 0 }
)
.unwrap(),
Array1::from_vec(vec![4.0_f32])
);
assert_eq!(
bit_round(
ArrayView1::from(&[4.0_f32]),
&BitRoundMode::Keepbits { keepbits: 0 }
)
.unwrap(),
Array1::from_vec(vec![4.0_f32])
);
assert_eq!(
bit_round(
ArrayView1::from(&[5.0_f32]),
&BitRoundMode::Keepbits { keepbits: 0 }
)
.unwrap(),
Array1::from_vec(vec![4.0_f32])
);
assert_eq!(
bit_round(
ArrayView1::from(&[6.0_f32]),
&BitRoundMode::Keepbits { keepbits: 0 }
)
.unwrap(),
Array1::from_vec(vec![8.0_f32])
);
assert_eq!(
bit_round(
ArrayView1::from(&[7.0_f32]),
&BitRoundMode::Keepbits { keepbits: 0 }
)
.unwrap(),
Array1::from_vec(vec![8.0_f32])
);
assert_eq!(
bit_round(
ArrayView1::from(&[8.0_f32]),
&BitRoundMode::Keepbits { keepbits: 0 }
)
.unwrap(),
Array1::from_vec(vec![8.0_f32])
);
assert_eq!(
bit_round(
ArrayView1::from(&[0.0_f64]),
&BitRoundMode::Keepbits { keepbits: 0 }
)
.unwrap(),
Array1::from_vec(vec![0.0_f64])
);
assert_eq!(
bit_round(
ArrayView1::from(&[1.0_f64]),
&BitRoundMode::Keepbits { keepbits: 0 }
)
.unwrap(),
Array1::from_vec(vec![1.0_f64])
);
assert_eq!(
bit_round(
ArrayView1::from(&[1.5_f64]),
&BitRoundMode::Keepbits { keepbits: 0 }
)
.unwrap(),
Array1::from_vec(vec![2.0_f64])
);
assert_eq!(
bit_round(
ArrayView1::from(&[2.0_f64]),
&BitRoundMode::Keepbits { keepbits: 0 }
)
.unwrap(),
Array1::from_vec(vec![2.0_f64])
);
assert_eq!(
bit_round(
ArrayView1::from(&[2.5_f64]),
&BitRoundMode::Keepbits { keepbits: 0 }
)
.unwrap(),
Array1::from_vec(vec![2.0_f64])
);
assert_eq!(
bit_round(
ArrayView1::from(&[3.0_f64]),
&BitRoundMode::Keepbits { keepbits: 0 }
)
.unwrap(),
Array1::from_vec(vec![2.0_f64])
);
assert_eq!(
bit_round(
ArrayView1::from(&[3.5_f64]),
&BitRoundMode::Keepbits { keepbits: 0 }
)
.unwrap(),
Array1::from_vec(vec![4.0_f64])
);
assert_eq!(
bit_round(
ArrayView1::from(&[4.0_f64]),
&BitRoundMode::Keepbits { keepbits: 0 }
)
.unwrap(),
Array1::from_vec(vec![4.0_f64])
);
assert_eq!(
bit_round(
ArrayView1::from(&[5.0_f64]),
&BitRoundMode::Keepbits { keepbits: 0 }
)
.unwrap(),
Array1::from_vec(vec![4.0_f64])
);
assert_eq!(
bit_round(
ArrayView1::from(&[6.0_f64]),
&BitRoundMode::Keepbits { keepbits: 0 }
)
.unwrap(),
Array1::from_vec(vec![8.0_f64])
);
assert_eq!(
bit_round(
ArrayView1::from(&[7.0_f64]),
&BitRoundMode::Keepbits { keepbits: 0 }
)
.unwrap(),
Array1::from_vec(vec![8.0_f64])
);
assert_eq!(
bit_round(
ArrayView1::from(&[8.0_f64]),
&BitRoundMode::Keepbits { keepbits: 0 }
)
.unwrap(),
Array1::from_vec(vec![8.0_f64])
);
}
#[test]
#[expect(clippy::cast_possible_truncation)]
fn full_mantissa() {
fn full<T: Float>(x: T) -> T {
T::from_binary(T::to_binary(x) + T::MANTISSA_MASK)
}
for v in [0.0_f32, 1.0_f32, 2.0_f32, 3.0_f32, 4.0_f32] {
assert_eq!(
bit_round(
ArrayView1::from(&[full(v)]),
&BitRoundMode::Keepbits {
keepbits: f32::MANITSSA_BITS as u8
}
)
.unwrap(),
Array1::from_vec(vec![full(v)])
);
}
for v in [0.0_f64, 1.0_f64, 2.0_f64, 3.0_f64, 4.0_f64] {
assert_eq!(
bit_round(
ArrayView1::from(&[full(v)]),
&BitRoundMode::Keepbits {
keepbits: f64::MANITSSA_BITS as u8
}
)
.unwrap(),
Array1::from_vec(vec![full(v)])
);
}
}
#[test]
fn normal_log2_floor_f32() {
for e in -100_i16..100 {
let b = f32::from(e).exp2();
for f in [0.55, 0.75, 0.9, 1.0, 1.1, 1.5, 1.95] {
let x = b * f;
#[expect(clippy::cast_possible_truncation)]
let math = x.log2().floor() as i16;
let binary = x.normal_log2_floor();
assert_eq!(math, binary, "{x}");
}
}
assert_eq!(i32::from(0.0_f32.normal_log2_floor()), f32::MIN_EXP - 2);
}
#[test]
fn normal_log2_floor_f64() {
for e in -100_i32..100 {
let b = f64::from(e).exp2();
for f in [0.55, 0.75, 0.9, 1.0, 1.1, 1.5, 1.95] {
let x = b * f;
#[expect(clippy::cast_possible_truncation)]
let math = x.log2().floor() as i16;
let binary = x.normal_log2_floor();
assert_eq!(math, binary, "{x}");
}
}
assert_eq!(i32::from(0.0_f64.normal_log2_floor()), f64::MIN_EXP - 2);
}
}