Skip to main content

numcodecs_bit_round/
lib.rs

1//! [![CI Status]][workflow] [![MSRV]][repo] [![Latest Version]][crates.io] [![Rust Doc Crate]][docs.rs] [![Rust Doc Main]][docs]
2//!
3//! [CI Status]: https://img.shields.io/github/actions/workflow/status/juntyr/numcodecs-rs/ci.yml?branch=main
4//! [workflow]: https://github.com/juntyr/numcodecs-rs/actions/workflows/ci.yml?query=branch%3Amain
5//!
6//! [MSRV]: https://img.shields.io/badge/MSRV-1.87.0-blue
7//! [repo]: https://github.com/juntyr/numcodecs-rs
8//!
9//! [Latest Version]: https://img.shields.io/crates/v/numcodecs-bit-round
10//! [crates.io]: https://crates.io/crates/numcodecs-bit-round
11//!
12//! [Rust Doc Crate]: https://img.shields.io/docsrs/numcodecs-bit-round
13//! [docs.rs]: https://docs.rs/numcodecs-bit-round/
14//!
15//! [Rust Doc Main]: https://img.shields.io/badge/docs-main-blue
16//! [docs]: https://juntyr.github.io/numcodecs-rs/numcodecs_bit_round
17//!
18//! Bit rounding codec implementation for the [`numcodecs`] API.
19
20use 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)]
33/// Codec providing floating-point bit rounding.
34///
35/// Drops the specified number of bits from the floating point mantissa,
36/// leaving an array that is more amenable to compression. The number of
37/// bits to keep should be determined by information analysis of the data
38/// to be compressed.
39///
40/// The approach is based on the paper by Klöwer et al. 2021
41/// (<https://www.nature.com/articles/s43588-021-00156-2>).
42pub struct BitRoundCodec {
43    /// Bit rounding mode.
44    #[serde(flatten)]
45    pub mode: BitRoundMode,
46    /// The codec's encoding format version. Do not provide this parameter explicitly.
47    #[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)]
54/// Bit rounding mode
55pub enum BitRoundMode {
56    /// Directly specify the number of bits of the mantissa to keep.
57    #[serde(rename = "keepbits")]
58    Keepbits {
59        /// The number of bits of the mantissa to keep.
60        ///
61        /// The valid range depends on the dtype of the input data.
62        ///
63        /// If keepbits is equal to the bitlength of the dtype's mantissa, no
64        /// transformation is performed.
65        keepbits: u8,
66    },
67    /// Pointwise absolute error.
68    #[serde(rename = "abs")]
69    AbsoluteError {
70        /// The pointwise absolute error bound to preserve.
71        ///
72        /// This error bound guarantees that
73        /// `$|x - \hat{x}| \leq \epsilon_{abs}$`.
74        eb_abs: NonNegative<f64>,
75    },
76    /// Pointwise relative error.
77    #[serde(rename = "rel")]
78    RelativeError {
79        /// The pointwise relative error bound to preserve.
80        ///
81        /// This error bound guarantees that
82        /// `$|x - \hat{x}| \leq |x| \cdot \epsilon_{rel}$`.
83        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)]
134/// Errors that may occur when applying the [`BitRoundCodec`].
135pub enum BitRoundCodecError {
136    /// [`BitRoundCodec`] does not support the dtype
137    #[error("BitRound does not support the dtype {0}")]
138    UnsupportedDtype(AnyArrayDType),
139    /// [`BitRoundCodec`] encode `keepbits` exceed the mantissa size for `dtype`
140    #[error("BitRound encode {keepbits} bits exceed the mantissa size for {dtype}")]
141    ExcessiveKeepBits {
142        /// The number of bits of the mantissa to keep
143        keepbits: u8,
144        /// The `dtype` of the data to encode
145        dtype: AnyArrayDType,
146    },
147    /// [`BitRoundCodec`] cannot decode into the provided array
148    #[error("BitRound cannot decode into the provided array")]
149    MismatchedDecodeIntoArray {
150        /// The source of the error
151        #[from]
152        source: AnyArrayAssignError,
153    },
154}
155
156#[expect(clippy::derive_partial_eq_without_eq)] // floats are not Eq
157#[derive(Copy, Clone, PartialEq, PartialOrd, Hash)]
158/// Non-negative floating point number
159pub 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
199/// Floating-point bit rounding, which drops the specified number of bits from
200/// the floating point mantissa.
201///
202/// See <https://github.com/milankl/BitInformation.jl> for the the original
203/// implementation in Julia.
204///
205/// # Errors
206///
207/// Errors with [`BitRoundCodecError::ExcessiveKeepBits`] if `keepbits` exceeds
208/// [`T::MANITSSA_BITS`][`Float::MANITSSA_BITS`].
209pub 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                // subnormal, infinite, and NaN values are hard so just keep
231                // them as is
232                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    // Early return if no bit rounding needs to happen
250    // - required since the ties to even impl does not work in this case
251    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        // subnormal, infinite, and NaN values are hard so just keep them as is
259        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        // half of unit in last place (ulp)
279        let ulp_half = T::MANTISSA_MASK >> (keepbits + 1);
280        // mask to zero out trailing mantissa bits
281        let keep_mask = !(T::MANTISSA_MASK >> keepbits);
282        // shift to extract the least significant bit of the exponent
283        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        // keepbits must be within the range of the mantissa bits of single precision.
295        #[expect(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
296        // no sign loss or truncation since we clamp to between 0 and a u32
297        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        // add ulp/2 with ties to even
306        bits += self.ulp_half + ((bits >> self.shift) & T::BINARY_ONE);
307
308        // set the trailing bits to zero
309        bits &= self.keep_mask;
310
311        T::from_binary(bits)
312    }
313}
314
315/// Floating point types.
316pub trait Float: Sized + Copy + std::ops::Div<Self, Output = Self> {
317    /// Number of significant digits in base 2
318    const MANITSSA_BITS: u32;
319    /// Binary mask to extract only the mantissa bits
320    const MANTISSA_MASK: Self::Binary;
321    /// Binary `0x1`
322    const BINARY_ONE: Self::Binary;
323
324    /// Dtype of this type
325    const TY: AnyArrayDType;
326
327    /// Binary representation of this type
328    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    /// Bit-cast the floating point value to its binary representation
337    fn to_binary(self) -> Self::Binary;
338    /// Bit-cast the binary representation into a floating point value
339    fn from_binary(u: Self::Binary) -> Self;
340
341    /// Returns the floating point category of the number
342    fn is_normal(self) -> bool;
343
344    /// Returns the floor of the base-2 logarithm as a signed integer
345    fn normal_log2_floor(self) -> i16;
346
347    /// Computes the absolute value
348    #[must_use]
349    fn abs(self) -> Self;
350
351    /// Convert from an [`f64`] value
352    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        // tie to even rounds up as the offset exponent is odd
449        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        // tie to even rounds down as the offset exponent is even
474        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        // tie to even rounds up as the offset exponent is odd
507        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        // tie to even rounds up as the offset exponent is odd
549        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        // tie to even rounds down as the offset exponent is even
574        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        // tie to even rounds up as the offset exponent is odd
607        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}