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}