Skip to main content

hurray_core/descriptor/
statistics.rs

1//! Statistics section — binary encode/decode.
2//!
3//! The statistics section is a fixed **72-byte** block present when the
4//! `HAS_STATISTICS` flag is set. All fields are advisory (optimization hints)
5//! and MUST NOT be relied upon for correctness.
6//!
7//! Wire layout (all little-endian):
8//! ```text
9//! offset  0: computed_mask   uint32
10//! offset  4: _reserved       uint32   MUST be 0
11//! offset  8: nnz             uint64
12//! offset 16: sparsity_ratio  float64
13//! offset 24: value_min       float64
14//! offset 32: value_max       float64
15//! offset 40: value_abs_max   float64
16//! offset 48: value_mean      float64
17//! offset 56: value_stddev    float64
18//! offset 64: nm_n            uint8
19//! offset 65: nm_m            uint8
20//! offset 66: has_nan         uint8  (0x01 or 0x00)
21//! offset 67: has_inf         uint8  (0x01 or 0x00)
22//! offset 68: _reserved2      uint8[4]  MUST be 0
23//! ```
24//! Total = 72 bytes.
25
26use crate::descriptor::cursor::{ByteCursor, ByteWriter};
27use crate::{Error, Result};
28
29/// Total byte length of the encoded statistics section.
30const STATISTICS_BYTE_LEN: usize = 72;
31
32/// Bitmask identifying which statistics fields contain valid data.
33///
34/// A reader MUST check the relevant bit before using any statistics field.
35/// Fields whose bit is not set MUST be treated as unknown.
36///
37/// Bits 6–31 are reserved and MUST be `0`. A reader MUST reject a descriptor
38/// whose `computed_mask` has any reserved bit set.
39///
40/// # Examples
41///
42/// ```
43/// use hurray_core::descriptor::StatisticsMask;
44///
45/// let mask = StatisticsMask(StatisticsMask::NNZ_VALID | StatisticsMask::VALUE_RANGE_VALID);
46/// assert!(mask.nnz_valid());
47/// assert!(mask.value_range_valid());
48/// assert!(!mask.sparsity_valid());
49/// ```
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct StatisticsMask(pub u32);
52
53impl StatisticsMask {
54    /// Bit 0: `nnz` field is valid.
55    pub const NNZ_VALID: u32 = 1 << 0;
56    /// Bit 1: `sparsity_ratio` field is valid.
57    pub const SPARSITY_VALID: u32 = 1 << 1;
58    /// Bit 2: `value_min`, `value_max`, `value_abs_max` are valid.
59    pub const VALUE_RANGE_VALID: u32 = 1 << 2;
60    /// Bit 3: `value_mean`, `value_stddev` are valid.
61    pub const VALUE_STATS_VALID: u32 = 1 << 3;
62    /// Bit 4: `nm_n`, `nm_m` are valid.
63    pub const NM_SPARSITY_VALID: u32 = 1 << 4;
64    /// Bit 5: `has_nan`, `has_inf` are valid.
65    pub const NAN_INF_VALID: u32 = 1 << 5;
66
67    /// Bitmask covering all reserved bits (bits 6 and above).
68    const RESERVED_MASK: u32 = !0x3F;
69
70    /// Returns `true` if the NNZ_VALID bit is set.
71    #[inline]
72    pub fn nnz_valid(self) -> bool {
73        self.0 & Self::NNZ_VALID != 0
74    }
75
76    /// Returns `true` if the SPARSITY_VALID bit is set.
77    #[inline]
78    pub fn sparsity_valid(self) -> bool {
79        self.0 & Self::SPARSITY_VALID != 0
80    }
81
82    /// Returns `true` if the VALUE_RANGE_VALID bit is set.
83    #[inline]
84    pub fn value_range_valid(self) -> bool {
85        self.0 & Self::VALUE_RANGE_VALID != 0
86    }
87
88    /// Returns `true` if the VALUE_STATS_VALID bit is set.
89    #[inline]
90    pub fn value_stats_valid(self) -> bool {
91        self.0 & Self::VALUE_STATS_VALID != 0
92    }
93
94    /// Returns `true` if the NM_SPARSITY_VALID bit is set.
95    #[inline]
96    pub fn nm_sparsity_valid(self) -> bool {
97        self.0 & Self::NM_SPARSITY_VALID != 0
98    }
99
100    /// Returns `true` if the NAN_INF_VALID bit is set.
101    #[inline]
102    pub fn nan_inf_valid(self) -> bool {
103        self.0 & Self::NAN_INF_VALID != 0
104    }
105}
106
107/// Advisory statistics about a tensor's data buffer.
108///
109/// Statistics reflect the tensor data at write time. A reader MUST NOT rely on
110/// any statistic for correctness — they are optimization hints only (algorithm
111/// selection, memory pre-allocation, routing decisions).
112///
113/// Each group of fields is guarded by a bit in `computed_mask`. A reader MUST
114/// check the relevant bit via [`StatisticsMask`] before using any field.
115///
116/// Note: derives `PartialEq` but NOT `Eq` — `f64` NaN semantics make `Eq` unsound.
117///
118/// # Examples
119///
120/// ```
121/// use hurray_core::descriptor::{Statistics, StatisticsMask};
122///
123/// let stats = Statistics {
124///     computed_mask: StatisticsMask(StatisticsMask::NNZ_VALID),
125///     nnz: 42,
126///     sparsity_ratio: 0.0,
127///     value_min: 0.0,
128///     value_max: 0.0,
129///     value_abs_max: 0.0,
130///     value_mean: 0.0,
131///     value_stddev: 0.0,
132///     nm_n: 0,
133///     nm_m: 0,
134///     has_nan: false,
135///     has_inf: false,
136/// };
137/// assert!(stats.computed_mask.nnz_valid());
138/// assert_eq!(stats.nnz, 42);
139/// ```
140#[derive(Debug, Clone, PartialEq)]
141pub struct Statistics {
142    /// Bitmask indicating which fields contain valid data.
143    pub computed_mask: StatisticsMask,
144    /// Number of non-zero elements. Valid when `NNZ_VALID` is set.
145    pub nnz: u64,
146    /// Fraction of zero elements `[0.0, 1.0]`. Valid when `SPARSITY_VALID` is set.
147    pub sparsity_ratio: f64,
148    /// Minimum element value (dequantized). Valid when `VALUE_RANGE_VALID` is set.
149    pub value_min: f64,
150    /// Maximum element value (dequantized). Valid when `VALUE_RANGE_VALID` is set.
151    pub value_max: f64,
152    /// Maximum absolute element value. Valid when `VALUE_RANGE_VALID` is set.
153    pub value_abs_max: f64,
154    /// Arithmetic mean of all elements. Valid when `VALUE_STATS_VALID` is set.
155    pub value_mean: f64,
156    /// Population standard deviation. Valid when `VALUE_STATS_VALID` is set.
157    pub value_stddev: f64,
158    /// N in N:M structured sparsity. Valid when `NM_SPARSITY_VALID` is set.
159    pub nm_n: u8,
160    /// M in N:M structured sparsity. Valid when `NM_SPARSITY_VALID` is set.
161    pub nm_m: u8,
162    /// `true` if at least one NaN element is present. Valid when `NAN_INF_VALID` is set.
163    pub has_nan: bool,
164    /// `true` if at least one infinity is present. Valid when `NAN_INF_VALID` is set.
165    pub has_inf: bool,
166}
167
168impl Statistics {
169    /// Encodes the statistics section into `w` as an exact 72-byte block.
170    pub(crate) fn encode_into(&self, w: &mut ByteWriter) {
171        w.write_u32_le(self.computed_mask.0); // offset  0
172        w.write_u32_le(0u32); // offset  4 — _reserved
173        w.write_u64_le(self.nnz); // offset  8
174        w.write_f64_le(self.sparsity_ratio); // offset 16
175        w.write_f64_le(self.value_min); // offset 24
176        w.write_f64_le(self.value_max); // offset 32
177        w.write_f64_le(self.value_abs_max); // offset 40
178        w.write_f64_le(self.value_mean); // offset 48
179        w.write_f64_le(self.value_stddev); // offset 56
180        w.write_u8(self.nm_n); // offset 64
181        w.write_u8(self.nm_m); // offset 65
182        w.write_u8(u8::from(self.has_nan)); // offset 66
183        w.write_u8(u8::from(self.has_inf)); // offset 67
184        w.write_zeros(4); // offset 68 — _reserved2
185    }
186
187    /// Decodes a 72-byte statistics block from `cursor`.
188    ///
189    /// # Errors
190    ///
191    /// - [`Error::StatisticsReservedMaskBitsSet`] if `computed_mask` bits ≥ 6 are set.
192    /// - [`Error::ReservedBytesNonZero`] if any `_reserved` field is non-zero.
193    /// - [`Error::DescriptorTruncated`] if fewer than 72 bytes remain.
194    pub(crate) fn decode_from(cursor: &mut ByteCursor<'_>) -> Result<Self> {
195        let start_pos = cursor.pos();
196
197        let computed_mask_raw = cursor.read_u32_le()?; // offset  0
198        let reserved1 = cursor.read_u32_le()?; // offset  4
199        let nnz = cursor.read_u64_le()?; // offset  8
200        let sparsity_ratio = cursor.read_f64_le()?; // offset 16
201        let value_min = cursor.read_f64_le()?; // offset 24
202        let value_max = cursor.read_f64_le()?; // offset 32
203        let value_abs_max = cursor.read_f64_le()?; // offset 40
204        let value_mean = cursor.read_f64_le()?; // offset 48
205        let value_stddev = cursor.read_f64_le()?; // offset 56
206        let nm_n = cursor.read_u8()?; // offset 64
207        let nm_m = cursor.read_u8()?; // offset 65
208        let has_nan_raw = cursor.read_u8()?; // offset 66
209        let has_inf_raw = cursor.read_u8()?; // offset 67
210        let reserved2 = cursor.read_bytes(4)?; // offset 68
211
212        // Validate that we read exactly STATISTICS_BYTE_LEN bytes.
213        debug_assert_eq!(cursor.pos() - start_pos, STATISTICS_BYTE_LEN);
214
215        // Reject reserved bits in computed_mask (bits 6–31).
216        if computed_mask_raw & StatisticsMask::RESERVED_MASK != 0 {
217            return Err(Error::StatisticsReservedMaskBitsSet {
218                mask: computed_mask_raw,
219            });
220        }
221
222        if reserved1 != 0 {
223            return Err(Error::ReservedBytesNonZero {
224                field: "statistics._reserved",
225            });
226        }
227
228        if reserved2 != [0u8, 0, 0, 0] {
229            return Err(Error::ReservedBytesNonZero {
230                field: "statistics._reserved2",
231            });
232        }
233
234        Ok(Self {
235            computed_mask: StatisticsMask(computed_mask_raw),
236            nnz,
237            sparsity_ratio,
238            value_min,
239            value_max,
240            value_abs_max,
241            value_mean,
242            value_stddev,
243            nm_n,
244            nm_m,
245            has_nan: has_nan_raw != 0,
246            has_inf: has_inf_raw != 0,
247        })
248    }
249}
250
251// ── Tests ─────────────────────────────────────────────────────────────────────
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256    use crate::descriptor::cursor::{ByteCursor, ByteWriter};
257
258    fn sample_stats() -> Statistics {
259        Statistics {
260            computed_mask: StatisticsMask(
261                StatisticsMask::NNZ_VALID
262                    | StatisticsMask::SPARSITY_VALID
263                    | StatisticsMask::VALUE_RANGE_VALID
264                    | StatisticsMask::VALUE_STATS_VALID
265                    | StatisticsMask::NM_SPARSITY_VALID
266                    | StatisticsMask::NAN_INF_VALID,
267            ),
268            nnz: 1000,
269            sparsity_ratio: 0.5,
270            value_min: -1.0,
271            value_max: 1.0,
272            value_abs_max: 1.0,
273            value_mean: 0.0,
274            value_stddev: 0.5,
275            nm_n: 2,
276            nm_m: 4,
277            has_nan: false,
278            has_inf: true,
279        }
280    }
281
282    #[test]
283    fn statistics_round_trip() {
284        let stats = sample_stats();
285        let mut w = ByteWriter::new();
286        stats.encode_into(&mut w);
287        let bytes = w.into_vec();
288        assert_eq!(bytes.len(), STATISTICS_BYTE_LEN);
289        let mut c = ByteCursor::new(&bytes, bytes.len());
290        let decoded = Statistics::decode_from(&mut c).unwrap();
291        assert_eq!(decoded.computed_mask, stats.computed_mask);
292        assert_eq!(decoded.nnz, stats.nnz);
293        assert_eq!(decoded.sparsity_ratio, stats.sparsity_ratio);
294        assert_eq!(decoded.value_min, stats.value_min);
295        assert_eq!(decoded.value_max, stats.value_max);
296        assert_eq!(decoded.value_abs_max, stats.value_abs_max);
297        assert_eq!(decoded.value_mean, stats.value_mean);
298        assert_eq!(decoded.value_stddev, stats.value_stddev);
299        assert_eq!(decoded.nm_n, stats.nm_n);
300        assert_eq!(decoded.nm_m, stats.nm_m);
301        assert_eq!(decoded.has_nan, stats.has_nan);
302        assert_eq!(decoded.has_inf, stats.has_inf);
303    }
304
305    #[test]
306    fn statistics_reserved_mask_bits_rejected() {
307        let mut w = ByteWriter::new();
308        // Set bit 6 (first reserved bit).
309        w.write_u32_le(0x0000_0040u32);
310        w.write_zeros(68);
311        let bytes = w.into_vec();
312        let mut c = ByteCursor::new(&bytes, bytes.len());
313        let err = Statistics::decode_from(&mut c).unwrap_err();
314        assert!(matches!(err, Error::StatisticsReservedMaskBitsSet { .. }));
315    }
316
317    #[test]
318    fn statistics_reserved_bytes_rejected() {
319        let mut w = ByteWriter::new();
320        w.write_u32_le(0u32); // computed_mask = 0 (valid)
321        w.write_u32_le(1u32); // _reserved — non-zero, must be rejected
322        w.write_zeros(64);
323        let bytes = w.into_vec();
324        let mut c = ByteCursor::new(&bytes, bytes.len());
325        let err = Statistics::decode_from(&mut c).unwrap_err();
326        assert!(matches!(err, Error::ReservedBytesNonZero { .. }));
327    }
328
329    #[test]
330    fn statistics_reserved2_bytes_rejected() {
331        let mut w = ByteWriter::new();
332        w.write_u32_le(0u32); // computed_mask
333        w.write_u32_le(0u32); // _reserved
334        w.write_zeros(60); // nnz through has_inf
335        w.write_u8(0x01); // _reserved2[0] non-zero
336        w.write_zeros(3);
337        let bytes = w.into_vec();
338        let mut c = ByteCursor::new(&bytes, bytes.len());
339        let err = Statistics::decode_from(&mut c).unwrap_err();
340        assert!(matches!(err, Error::ReservedBytesNonZero { .. }));
341    }
342
343    #[test]
344    fn encoded_length_is_72_bytes() {
345        let mut w = ByteWriter::new();
346        sample_stats().encode_into(&mut w);
347        assert_eq!(w.len(), STATISTICS_BYTE_LEN);
348    }
349}