Skip to main content

dsi_bitstream/utils/
stats.rs

1/*
2 * SPDX-FileCopyrightText: 2023 Inria
3 * SPDX-FileCopyrightText: 2023 Sebastiano Vigna
4 * SPDX-FileCopyrightText: 2024 Tommaso Fontana
5 *
6 * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later
7 */
8
9#[cfg(feature = "mem_dbg")]
10use mem_dbg::{MemDbg, MemSize};
11
12use crate::prelude::{
13    Codes, bit_len_vbyte, len_delta, len_exp_golomb, len_gamma, len_golomb, len_omega, len_pi,
14    len_rice, len_zeta,
15};
16use core::fmt::Debug;
17
18#[cfg(feature = "std")]
19use crate::dispatch::{CodesRead, CodesWrite};
20#[cfg(feature = "std")]
21use crate::prelude::Endianness;
22#[cfg(feature = "std")]
23use crate::prelude::{DynamicCodeRead, DynamicCodeWrite, StaticCodeRead, StaticCodeWrite};
24#[cfg(feature = "std")]
25use std::sync::Mutex;
26
27#[cfg(feature = "serde")]
28use alloc::string::ToString;
29#[cfg(feature = "alloc")]
30use alloc::{vec, vec::Vec};
31
32/// Keeps track of the space needed to store a stream of integers using
33/// different codes.
34///
35/// This structure can be used to determine empirically which code provides the
36/// best compression for a given stream. You have to [update the
37/// structure](Self::update) with the integers in the stream; at any time, you
38/// can examine the statistics or call [`best_code`](Self::best_code) to get the
39/// best code.
40///
41/// The structure keeps tracks of the codes for which the module
42/// [`code_consts`](crate::dispatch::code_consts) provide constants.
43#[derive(Debug, Copy, Clone, PartialEq, Eq)]
44#[cfg_attr(feature = "mem_dbg", derive(MemDbg, MemSize))]
45pub struct CodesStats<
46    // How many ζ codes to consider.
47    const ZETA: usize = 10,
48    // How many Golomb codes to consider.
49    const GOLOMB: usize = 10,
50    // How many Exponential Golomb codes to consider.
51    const EXP_GOLOMB: usize = 10,
52    // How many Rice codes to consider.
53    const RICE: usize = 10,
54    // How many streamlined π codes to consider.
55    const PI: usize = 10,
56> {
57    /// The total number of elements observed.
58    pub total: u64,
59    /// The total space used to store the elements if
60    /// they were stored using the unary code.
61    pub unary: u64,
62    /// The total space used to store the elements if
63    /// they were stored using the gamma code.
64    pub gamma: u64,
65    /// The total space used to store the elements if
66    /// they were stored using the delta code.
67    pub delta: u64,
68    /// The total space used to store the elements if
69    /// they were stored using the omega code.
70    pub omega: u64,
71    /// The total space used to store the elements if
72    /// they were stored using the variable byte code.
73    pub vbyte: u64,
74    /// The total space used to store the elements if they were stored using a
75    /// zeta code. `zeta[0]` represents ζ₁, `zeta[1]` represents ζ₂, and so on.
76    pub zeta: [u64; ZETA],
77    /// The total space used to store the elements if they were stored using a
78    /// Golomb code. `golomb[0]` represents the Golomb code with modulus 1,
79    /// `golomb[1]` represents the Golomb code with modulus 2, and so on.
80    pub golomb: [u64; GOLOMB],
81    /// The total space used to store the elements if they were stored using an
82    /// exponential Golomb code. `exp_golomb[0]` represents the exponential
83    /// Golomb code with parameter 0, `exp_golomb[1]` with parameter 1, and
84    /// so on.
85    pub exp_golomb: [u64; EXP_GOLOMB],
86    /// The total space used to store the elements if they were stored using a
87    /// Rice code. `rice[0]` represents the Rice code with log₂(*b*) = 0,
88    /// `rice[1]` with log₂(*b*) = 1, and so on.
89    pub rice: [u64; RICE],
90    /// The total space used to store the elements if they were stored using a
91    /// pi code. `pi[0]` represents π₂, `pi[1]` represents π₃, and so on.
92    pub pi: [u64; PI],
93}
94
95impl<
96    const ZETA: usize,
97    const GOLOMB: usize,
98    const EXP_GOLOMB: usize,
99    const RICE: usize,
100    const PI: usize,
101> core::default::Default for CodesStats<ZETA, GOLOMB, EXP_GOLOMB, RICE, PI>
102{
103    fn default() -> Self {
104        Self {
105            total: 0,
106            unary: 0,
107            gamma: 0,
108            delta: 0,
109            omega: 0,
110            vbyte: 0,
111            zeta: [0; ZETA],
112            golomb: [0; GOLOMB],
113            exp_golomb: [0; EXP_GOLOMB],
114            rice: [0; RICE],
115            pi: [0; PI],
116        }
117    }
118}
119
120impl<
121    const ZETA: usize,
122    const GOLOMB: usize,
123    const EXP_GOLOMB: usize,
124    const RICE: usize,
125    const PI: usize,
126> CodesStats<ZETA, GOLOMB, EXP_GOLOMB, RICE, PI>
127{
128    /// Update the stats with the lengths of the codes for `n` and return
129    /// `n` for convenience.
130    pub fn update(&mut self, n: u64) -> u64 {
131        self.update_many(n, 1)
132    }
133
134    /// Update the stats with `count` occurrences of `n` and return `n` for convenience.
135    #[inline]
136    pub fn update_many(&mut self, n: u64, count: u64) -> u64 {
137        self.total += count;
138        self.unary += (n + 1) * count;
139        self.gamma += len_gamma(n) as u64 * count;
140        self.delta += len_delta(n) as u64 * count;
141        self.omega += len_omega(n) as u64 * count;
142        self.vbyte += bit_len_vbyte(n) as u64 * count;
143
144        for (k, val) in self.zeta.iter_mut().enumerate() {
145            *val += (len_zeta(n, (k + 1) as _) as u64) * count;
146        }
147        for (b, val) in self.golomb.iter_mut().enumerate() {
148            *val += (len_golomb(n, (b + 1) as _) as u64) * count;
149        }
150        for (k, val) in self.exp_golomb.iter_mut().enumerate() {
151            *val += (len_exp_golomb(n, k as _) as u64) * count;
152        }
153        for (log2_b, val) in self.rice.iter_mut().enumerate() {
154            *val += (len_rice(n, log2_b as _) as u64) * count;
155        }
156        // π₀ = γ and π₁ = ζ₂ in terms of codeword lengths
157        for (k, val) in self.pi.iter_mut().enumerate() {
158            *val += (len_pi(n, (k + 2) as _) as u64) * count;
159        }
160        n
161    }
162
163    /// Combines additively this stats with another one.
164    pub fn add(&mut self, rhs: &Self) {
165        self.total += rhs.total;
166        self.unary += rhs.unary;
167        self.gamma += rhs.gamma;
168        self.delta += rhs.delta;
169        self.omega += rhs.omega;
170        self.vbyte += rhs.vbyte;
171        for (a, b) in self.zeta.iter_mut().zip(rhs.zeta.iter()) {
172            *a += *b;
173        }
174        for (a, b) in self.golomb.iter_mut().zip(rhs.golomb.iter()) {
175            *a += *b;
176        }
177        for (a, b) in self.exp_golomb.iter_mut().zip(rhs.exp_golomb.iter()) {
178            *a += *b;
179        }
180        for (a, b) in self.rice.iter_mut().zip(rhs.rice.iter()) {
181            *a += *b;
182        }
183        for (a, b) in self.pi.iter_mut().zip(rhs.pi.iter()) {
184            *a += *b;
185        }
186    }
187
188    /// Returns the best code for the stream and its space usage.
189    ///
190    /// When VByte is the best code, [`Codes::VByteBe`] is returned as the
191    /// canonical representative (both variants have the same bit length).
192    #[must_use]
193    pub fn best_code(&self) -> (Codes, u64) {
194        let mut best = (Codes::Unary, self.unary);
195        if self.gamma < best.1 {
196            best = (Codes::Gamma, self.gamma);
197        }
198        if self.delta < best.1 {
199            best = (Codes::Delta, self.delta);
200        }
201        if self.omega < best.1 {
202            best = (Codes::Omega, self.omega);
203        }
204        if self.vbyte < best.1 {
205            best = (Codes::VByteBe, self.vbyte);
206        }
207        for (k, val) in self.zeta.iter().enumerate() {
208            if *val < best.1 {
209                best = (Codes::Zeta((k + 1) as _), *val);
210            }
211        }
212        for (b, val) in self.golomb.iter().enumerate() {
213            if *val < best.1 {
214                best = (Codes::Golomb((b + 1) as _), *val);
215            }
216        }
217        for (k, val) in self.exp_golomb.iter().enumerate() {
218            if *val < best.1 {
219                best = (Codes::ExpGolomb(k as _), *val);
220            }
221        }
222        for (log2_b, val) in self.rice.iter().enumerate() {
223            if *val < best.1 {
224                best = (Codes::Rice(log2_b as _), *val);
225            }
226        }
227        for (k, val) in self.pi.iter().enumerate() {
228            if *val < best.1 {
229                best = (Codes::Pi((k + 2) as _), *val);
230            }
231        }
232        best
233    }
234
235    /// Returns a vector of all codes and their space usage, in ascending order by space usage.
236    #[cfg(feature = "alloc")]
237    #[must_use]
238    pub fn get_codes(&self) -> Vec<(Codes, u64)> {
239        let mut codes = vec![
240            (Codes::Unary, self.unary),
241            (Codes::Gamma, self.gamma),
242            (Codes::Delta, self.delta),
243            (Codes::Omega, self.omega),
244            (Codes::VByteBe, self.vbyte),
245        ];
246        for (k, val) in self.zeta.iter().enumerate() {
247            codes.push((Codes::Zeta((k + 1) as _), *val));
248        }
249        for (b, val) in self.golomb.iter().enumerate() {
250            codes.push((Codes::Golomb((b + 1) as _), *val));
251        }
252        for (k, val) in self.exp_golomb.iter().enumerate() {
253            codes.push((Codes::ExpGolomb(k as _), *val));
254        }
255        for (log2_b, val) in self.rice.iter().enumerate() {
256            codes.push((Codes::Rice(log2_b as _), *val));
257        }
258        for (k, val) in self.pi.iter().enumerate() {
259            codes.push((Codes::Pi((k + 2) as _), *val));
260        }
261        // sort them by length
262        codes.sort_by_key(|&(_, len)| len);
263        codes
264    }
265
266    /// Returns the number of bits used by the given code.
267    #[must_use]
268    pub fn bits_for(&self, code: Codes) -> Option<u64> {
269        match code {
270            Codes::Unary => Some(self.unary),
271            Codes::Gamma => Some(self.gamma),
272            Codes::Delta => Some(self.delta),
273            Codes::Omega => Some(self.omega),
274            Codes::VByteBe | Codes::VByteLe => Some(self.vbyte),
275            Codes::Zeta(k) => self.zeta.get(k.checked_sub(1)?).copied(),
276            Codes::Golomb(b) => self.golomb.get(b.checked_sub(1)? as usize).copied(),
277            Codes::ExpGolomb(k) => self.exp_golomb.get(k).copied(),
278            Codes::Rice(log2_b) => self.rice.get(log2_b).copied(),
279            Codes::Pi(k) => self.pi.get(k.checked_sub(2)?).copied(),
280        }
281    }
282}
283
284impl<
285    const ZETA: usize,
286    const GOLOMB: usize,
287    const EXP_GOLOMB: usize,
288    const RICE: usize,
289    const PI: usize,
290> core::ops::AddAssign for CodesStats<ZETA, GOLOMB, EXP_GOLOMB, RICE, PI>
291{
292    /// Combines additively this stats with another one.
293    fn add_assign(&mut self, rhs: Self) {
294        self.add(&rhs);
295    }
296}
297
298impl<
299    const ZETA: usize,
300    const GOLOMB: usize,
301    const EXP_GOLOMB: usize,
302    const RICE: usize,
303    const PI: usize,
304> core::ops::Add for CodesStats<ZETA, GOLOMB, EXP_GOLOMB, RICE, PI>
305{
306    type Output = Self;
307
308    /// Combines additively this stats with another one, creating a new one.
309    fn add(self, rhs: Self) -> Self {
310        let mut res = self;
311        res += rhs;
312        res
313    }
314}
315
316/// Allows calling `.sum()` on an iterator of [`CodesStats`].
317impl<
318    const ZETA: usize,
319    const GOLOMB: usize,
320    const EXP_GOLOMB: usize,
321    const RICE: usize,
322    const PI: usize,
323> core::iter::Sum for CodesStats<ZETA, GOLOMB, EXP_GOLOMB, RICE, PI>
324{
325    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
326        iter.fold(Self::default(), |a, b| a + b)
327    }
328}
329
330#[cfg(feature = "serde")]
331impl<
332    const ZETA: usize,
333    const GOLOMB: usize,
334    const EXP_GOLOMB: usize,
335    const RICE: usize,
336    const PI: usize,
337> serde::Serialize for CodesStats<ZETA, GOLOMB, EXP_GOLOMB, RICE, PI>
338{
339    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
340        use serde::ser::SerializeStruct;
341
342        let mut state = serializer.serialize_struct("CodesStats", 11)?;
343        state.serialize_field("total", &self.total)?;
344        state.serialize_field("unary", &self.unary)?;
345        state.serialize_field("gamma", &self.gamma)?;
346        state.serialize_field("delta", &self.delta)?;
347        state.serialize_field("omega", &self.omega)?;
348        state.serialize_field("vbyte", &self.vbyte)?;
349        // these are array which don't play well with serde, so we convert them to slices
350        state.serialize_field("zeta", &self.zeta.as_slice())?;
351        state.serialize_field("golomb", &self.golomb.as_slice())?;
352        state.serialize_field("exp_golomb", &self.exp_golomb.as_slice())?;
353        state.serialize_field("rice", &self.rice.as_slice())?;
354        state.serialize_field("pi", &self.pi.as_slice())?;
355        state.end()
356    }
357}
358
359#[cfg(feature = "serde")]
360impl<
361    'de,
362    const ZETA: usize,
363    const GOLOMB: usize,
364    const EXP_GOLOMB: usize,
365    const RICE: usize,
366    const PI: usize,
367> serde::Deserialize<'de> for CodesStats<ZETA, GOLOMB, EXP_GOLOMB, RICE, PI>
368{
369    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
370        use serde::de::{MapAccess, Visitor};
371
372        struct CodesStatsVisitor<
373            const ZETA: usize,
374            const GOLOMB: usize,
375            const EXP_GOLOMB: usize,
376            const RICE: usize,
377            const PI: usize,
378        >;
379
380        impl<
381            'de,
382            const ZETA: usize,
383            const GOLOMB: usize,
384            const EXP_GOLOMB: usize,
385            const RICE: usize,
386            const PI: usize,
387        > Visitor<'de> for CodesStatsVisitor<ZETA, GOLOMB, EXP_GOLOMB, RICE, PI>
388        {
389            type Value = CodesStats<ZETA, GOLOMB, EXP_GOLOMB, RICE, PI>;
390
391            fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
392                formatter.write_str("struct CodesStats")
393            }
394
395            fn visit_map<V: MapAccess<'de>>(self, mut map: V) -> Result<Self::Value, V::Error> {
396                let mut total = None;
397                let mut unary = None;
398                let mut gamma = None;
399                let mut delta = None;
400                let mut omega = None;
401                let mut vbyte = None;
402                let mut zeta: Option<[u64; ZETA]> = None;
403                let mut golomb: Option<[u64; GOLOMB]> = None;
404                let mut exp_golomb: Option<[u64; EXP_GOLOMB]> = None;
405                let mut rice: Option<[u64; RICE]> = None;
406                let mut pi: Option<[u64; PI]> = None;
407
408                // Helper to deserialize a Vec<u64> into a fixed-size array
409                fn vec_to_array<E: serde::de::Error, const N: usize>(
410                    v: Vec<u64>,
411                ) -> Result<[u64; N], E> {
412                    v.try_into().map_err(|v: Vec<u64>| {
413                        serde::de::Error::invalid_length(v.len(), &N.to_string().as_str())
414                    })
415                }
416
417                while let Some(key) = map.next_key::<&str>()? {
418                    match key {
419                        "total" => total = Some(map.next_value()?),
420                        "unary" => unary = Some(map.next_value()?),
421                        "gamma" => gamma = Some(map.next_value()?),
422                        "delta" => delta = Some(map.next_value()?),
423                        "omega" => omega = Some(map.next_value()?),
424                        "vbyte" => vbyte = Some(map.next_value()?),
425                        "zeta" => zeta = Some(vec_to_array(map.next_value()?)?),
426                        "golomb" => golomb = Some(vec_to_array(map.next_value()?)?),
427                        "exp_golomb" => exp_golomb = Some(vec_to_array(map.next_value()?)?),
428                        "rice" => rice = Some(vec_to_array(map.next_value()?)?),
429                        "pi" => pi = Some(vec_to_array(map.next_value()?)?),
430                        _ => {
431                            let _ = map.next_value::<serde::de::IgnoredAny>()?;
432                        }
433                    }
434                }
435
436                Ok(CodesStats {
437                    total: total.unwrap_or_default(),
438                    unary: unary.unwrap_or_default(),
439                    gamma: gamma.unwrap_or_default(),
440                    delta: delta.unwrap_or_default(),
441                    omega: omega.unwrap_or_default(),
442                    vbyte: vbyte.unwrap_or_default(),
443                    zeta: zeta.unwrap_or([0; ZETA]),
444                    golomb: golomb.unwrap_or([0; GOLOMB]),
445                    exp_golomb: exp_golomb.unwrap_or([0; EXP_GOLOMB]),
446                    rice: rice.unwrap_or([0; RICE]),
447                    pi: pi.unwrap_or([0; PI]),
448                })
449            }
450        }
451
452        deserializer.deserialize_struct(
453            "CodesStats",
454            &[
455                "total",
456                "unary",
457                "gamma",
458                "delta",
459                "omega",
460                "vbyte",
461                "zeta",
462                "golomb",
463                "exp_golomb",
464                "rice",
465                "pi",
466            ],
467            CodesStatsVisitor::<ZETA, GOLOMB, EXP_GOLOMB, RICE, PI>,
468        )
469    }
470}
471
472/// A struct that can wrap a [`DynamicCodeRead`], [`DynamicCodeWrite`],
473/// [`StaticCodeRead`], or [`StaticCodeWrite`] and compute [`CodesStats`]
474/// for a given stream.
475#[derive(Debug)]
476#[cfg_attr(feature = "mem_dbg", derive(MemDbg, MemSize))]
477#[cfg(feature = "std")]
478pub struct CodesStatsWrapper<
479    W,
480    // How many ζ codes to consider.
481    const ZETA: usize = 10,
482    // How many Golomb codes to consider.
483    const GOLOMB: usize = 10,
484    // How many Exponential Golomb codes to consider.
485    const EXP_GOLOMB: usize = 10,
486    // How many Rice codes to consider.
487    const RICE: usize = 10,
488    // How many streamlined π codes to consider.
489    const PI: usize = 10,
490> {
491    // TODO: figure out how we can do this without a lock.
492    // This is needed because the [`DynamicCodeRead`] and [`DynamicCodeWrite`] traits must have
493    // &self and not &mut self.
494    stats: Mutex<CodesStats<ZETA, GOLOMB, EXP_GOLOMB, RICE, PI>>,
495    wrapped: W,
496}
497
498#[cfg(feature = "std")]
499impl<
500    W,
501    const ZETA: usize,
502    const GOLOMB: usize,
503    const EXP_GOLOMB: usize,
504    const RICE: usize,
505    const PI: usize,
506> CodesStatsWrapper<W, ZETA, GOLOMB, EXP_GOLOMB, RICE, PI>
507{
508    /// Creates a new `CodesStatsWrapper` with the given wrapped value.
509    #[must_use]
510    pub fn new(wrapped: W) -> Self {
511        Self {
512            stats: Mutex::new(CodesStats::default()),
513            wrapped,
514        }
515    }
516
517    /// Returns a reference to the stats.
518    pub const fn stats(&self) -> &Mutex<CodesStats<ZETA, GOLOMB, EXP_GOLOMB, RICE, PI>> {
519        &self.stats
520    }
521
522    /// Consumes the wrapper and returns the inner wrapped value and the stats.
523    #[must_use]
524    pub fn into_inner(self) -> (W, CodesStats<ZETA, GOLOMB, EXP_GOLOMB, RICE, PI>) {
525        (
526            self.wrapped,
527            self.stats.into_inner().expect("mutex is not poisoned"),
528        )
529    }
530}
531
532#[cfg(feature = "std")]
533impl<
534    W: DynamicCodeRead,
535    const ZETA: usize,
536    const GOLOMB: usize,
537    const EXP_GOLOMB: usize,
538    const RICE: usize,
539    const PI: usize,
540> DynamicCodeRead for CodesStatsWrapper<W, ZETA, GOLOMB, EXP_GOLOMB, RICE, PI>
541{
542    #[inline]
543    fn read<E: Endianness, CR: CodesRead<E> + ?Sized>(
544        &self,
545        reader: &mut CR,
546    ) -> Result<u64, CR::Error> {
547        let res = self.wrapped.read(reader)?;
548        self.stats.lock().unwrap().update(res);
549        Ok(res)
550    }
551}
552
553#[cfg(feature = "std")]
554impl<
555    W: StaticCodeRead<E, CR>,
556    const ZETA: usize,
557    const GOLOMB: usize,
558    const EXP_GOLOMB: usize,
559    const RICE: usize,
560    const PI: usize,
561    E: Endianness,
562    CR: CodesRead<E> + ?Sized,
563> StaticCodeRead<E, CR> for CodesStatsWrapper<W, ZETA, GOLOMB, EXP_GOLOMB, RICE, PI>
564{
565    #[inline]
566    fn read(&self, reader: &mut CR) -> Result<u64, CR::Error> {
567        let res = self.wrapped.read(reader)?;
568        self.stats.lock().unwrap().update(res);
569        Ok(res)
570    }
571}
572
573#[cfg(feature = "std")]
574impl<
575    W: DynamicCodeWrite,
576    const ZETA: usize,
577    const GOLOMB: usize,
578    const EXP_GOLOMB: usize,
579    const RICE: usize,
580    const PI: usize,
581> DynamicCodeWrite for CodesStatsWrapper<W, ZETA, GOLOMB, EXP_GOLOMB, RICE, PI>
582{
583    #[inline]
584    fn write<E: Endianness, CW: CodesWrite<E> + ?Sized>(
585        &self,
586        writer: &mut CW,
587        n: u64,
588    ) -> Result<usize, CW::Error> {
589        let res = self.wrapped.write(writer, n)?;
590        self.stats.lock().unwrap().update(n);
591        Ok(res)
592    }
593}
594
595#[cfg(feature = "std")]
596impl<
597    W: StaticCodeWrite<E, CW>,
598    const ZETA: usize,
599    const GOLOMB: usize,
600    const EXP_GOLOMB: usize,
601    const RICE: usize,
602    const PI: usize,
603    E: Endianness,
604    CW: CodesWrite<E> + ?Sized,
605> StaticCodeWrite<E, CW> for CodesStatsWrapper<W, ZETA, GOLOMB, EXP_GOLOMB, RICE, PI>
606{
607    #[inline]
608    fn write(&self, writer: &mut CW, n: u64) -> Result<usize, CW::Error> {
609        let res = self.wrapped.write(writer, n)?;
610        self.stats.lock().unwrap().update(n);
611        Ok(res)
612    }
613}
614
615#[cfg(test)]
616#[cfg(feature = "serde")]
617mod serde_tests {
618    use super::*;
619
620    #[test]
621    fn test_serde_code_stats() -> serde_json::Result<()> {
622        let mut stats: CodesStats = CodesStats::default();
623        for i in 0..100 {
624            stats.update(i);
625        }
626        let json = serde_json::to_string(&stats)?;
627        let deserialized: CodesStats = serde_json::from_str(&json)?;
628        assert_eq!(stats, deserialized);
629        Ok(())
630    }
631
632    #[test]
633    fn test_roundtrip_different_sizes() -> serde_json::Result<()> {
634        let mut stats: CodesStats<10, 20, 5, 8, 6> = CodesStats::default();
635        for i in 0..1000 {
636            stats.update(i);
637        }
638        let json = serde_json::to_string_pretty(&stats)?;
639        let deserialized: CodesStats<10, 20, 5, 8, 6> = serde_json::from_str(&json)?;
640        assert_eq!(stats, deserialized);
641        Ok(())
642    }
643
644    #[test]
645    #[should_panic]
646    fn test_mismatched_sizes() {
647        let mut stats: CodesStats<10, 20, 5, 8, 6> = CodesStats::default();
648        for i in 0..1000 {
649            stats.update(i);
650        }
651        let json = serde_json::to_string_pretty(&stats).unwrap();
652        // This should panic because the JSON has 20 golomb values but we expect 21
653        let _deserialized: CodesStats<10, 21, 5, 8, 6> = serde_json::from_str(&json).unwrap();
654    }
655}