Skip to main content

chronix_encoding/
chimp.rs

1//! Chimp float encoding (VLDB 2022).
2//!
3//! Chimp improves upon Gorilla by exploiting the observation that XOR values
4//! of successive floating-point values cluster their leading zeros. It uses a
5//! leading-zero bucket scheme to reduce the overhead of encoding the number
6//! of leading zeros.
7//!
8//! ## Leading-zero buckets
9//!
10//! | Bucket | Range (leading zeros) | Stored as |
11//! |--------|----------------------|-----------|
12//! | 0      | 0–7                  | 3 bits    |
13//! | 1      | 8–15                 | 3 bits    |
14//! | 2      | 16–23                | 3 bits    |
15//! | 3      | 24–31                | 3 bits    |
16//! | 4      | 32–39                | 3 bits    |
17//! | 5      | 40–47                | 3 bits    |
18//! | 6      | 48–55                | 3 bits    |
19//! | 7      | 56–63                | 3 bits    |
20//!
21//! ## Chimp128 ring-buffer optimisation
22//!
23//! [`Chimp128Encoder`] / [`Chimp128Decoder`] implement the Chimp128 variant
24//! from the same VLDB 2022 paper.  A ring buffer of the last 128 raw `f64`
25//! bit-patterns is maintained.  For each new value the encoder computes the
26//! XOR against the previous value *and* against every ring-buffer entry,
27//! picking whichever reference yields the most leading zeros.  When a
28//! ring-buffer reference wins, a 7-bit index is stored so the decoder can
29//! reconstruct the same XOR base.
30//!
31//! This improves compression by 5–15 % on highly periodic workloads
32//! (e.g. fixed-interval sensor data that cycles through a limited set of
33//! levels) at the cost of ~1 KiB of encoder/decoder state.
34
35use crate::coding::{checked_count, checked_decode_count};
36use crate::delta::{BitReader, BitWriter};
37use crate::error::{EncodingError, Result};
38
39/// Chimp float encoder (VLDB 2022).
40#[derive(Debug, Clone, Copy)]
41pub struct ChimpEncoder;
42
43/// Chimp float decoder.
44#[derive(Debug, Clone, Copy)]
45pub struct ChimpDecoder;
46
47/// Number of leading-zero buckets.
48const NUM_BUCKETS: u8 = 8;
49
50/// Map a leading zero count (0..=64) to a 3-bit bucket index.
51#[inline]
52fn leading_zeros_to_bucket(lz: u32) -> u8 {
53    (lz as u8 / 8).min(NUM_BUCKETS - 1)
54}
55
56/// Map a bucket index back to the minimum leading zeros in that bucket.
57#[inline]
58fn bucket_to_leading_zeros(bucket: u8) -> u32 {
59    u32::from(bucket) * 8
60}
61
62impl ChimpEncoder {
63    /// Encode a sequence of `f64` values.
64    ///
65    /// # Errors
66    ///
67    /// Returns [`EncodingError::EmptyInput`] if `values` is empty.
68    pub fn encode(values: &[f64]) -> Result<Vec<u8>> {
69        if values.is_empty() {
70            return Err(EncodingError::EmptyInput {
71                context: "chimp encoder",
72            });
73        }
74
75        // Pre-allocate: worst case ~12 bytes per value + 12 header
76        let mut writer = BitWriter::with_capacity(12 + values.len() * 12);
77
78        Self::encode_core(values, &mut writer)?;
79
80        Ok(writer.finish())
81    }
82
83    /// Shared encode logic — writes into a pre-existing `BitWriter`
84    /// to allow callers to reuse buffers.
85    ///
86    /// # Errors
87    ///
88    /// Returns [`EncodingError::CorruptData`] if the value count exceeds `u32::MAX`.
89    fn encode_core(values: &[f64], writer: &mut BitWriter) -> Result<()> {
90        // Value count (validated to fit in u32)
91        let count = checked_count(values.len())?;
92        writer.write_bits(u64::from(count), 32);
93
94        // First value: raw 64-bit
95        let mut prev_bits = values[0].to_bits();
96        writer.write_bits(prev_bits, 64);
97
98        let mut prev_leading = u8::MAX; // no prior leading zeros
99        let mut prev_trailing = 0_u8;
100
101        #[allow(clippy::explicit_counter_loop)]
102        // ring_pos wraps modulo RING_SIZE, not a plain counter
103        for &val in &values[1..] {
104            let bits = val.to_bits();
105            let xor = prev_bits ^ bits;
106
107            if xor == 0 {
108                // Case 0: identical value → single 0 bit
109                writer.write_bit(false);
110            } else {
111                let leading = xor.leading_zeros();
112                let trailing = xor.trailing_zeros();
113
114                let bucket = leading_zeros_to_bucket(leading);
115                let bucket_lz = bucket_to_leading_zeros(bucket);
116
117                // Check if we can reuse the previous window
118                let can_reuse = prev_leading != u8::MAX
119                    && leading >= u32::from(prev_leading)
120                    && trailing >= u32::from(prev_trailing);
121
122                if can_reuse {
123                    // Case 1: reuse previous leading/trailing window
124                    // prefix: 10
125                    writer.write_bits(0b10, 2);
126                    let prev_meaningful = 64 - u32::from(prev_leading) - u32::from(prev_trailing);
127                    let shifted = xor >> u32::from(prev_trailing);
128                    writer.write_bits(shifted, prev_meaningful as u8);
129                } else {
130                    // Case 2: new window with bucket
131                    // prefix: 11
132                    writer.write_bits(0b11, 2);
133                    // 3-bit bucket
134                    writer.write_bits(u64::from(bucket), 3);
135                    // 6-bit trailing zero count
136                    writer.write_bits(u64::from(trailing), 6);
137                    // meaningful bits (adjusted for bucket granularity)
138                    let adj_leading = bucket_lz;
139                    let adj_meaningful = 64 - adj_leading - trailing;
140                    let shifted = xor >> trailing;
141                    // Store meaningful-1 to handle adj_meaningful==64
142                    writer.write_bits(u64::from(adj_meaningful.saturating_sub(1)), 6);
143                    writer.write_bits(shifted, adj_meaningful as u8);
144
145                    prev_leading = bucket_lz as u8;
146                    prev_trailing = trailing as u8;
147                }
148            }
149
150            prev_bits = bits;
151        }
152
153        Ok(())
154    }
155}
156
157impl ChimpDecoder {
158    /// Decode a Chimp-encoded byte buffer back to `f64` values.
159    ///
160    /// # Errors
161    ///
162    /// Returns [`EncodingError::CorruptData`] if the data is malformed.
163    pub fn decode(data: &[u8]) -> Result<Vec<f64>> {
164        if data.len() < 4 {
165            return Err(EncodingError::CorruptData {
166                detail: "chimp data too short for header".to_string(),
167            });
168        }
169
170        let mut reader = BitReader::new(data);
171
172        let count = checked_decode_count(reader.read_bits(32)? as usize, "Chimp")?;
173        if count == 0 {
174            return Ok(Vec::new());
175        }
176
177        let mut result = Vec::with_capacity(count);
178        Self::decode_core(&mut reader, count, &mut result)?;
179        Ok(result)
180    }
181
182    /// Decode into a pre-allocated output vector for zero-alloc reuse.
183    ///
184    /// # Errors
185    ///
186    /// Returns [`EncodingError::CorruptData`] if the data is malformed.
187    pub fn decode_into(data: &[u8], output: &mut Vec<f64>) -> Result<()> {
188        if data.len() < 4 {
189            return Err(EncodingError::CorruptData {
190                detail: "chimp data too short for header".to_string(),
191            });
192        }
193
194        let mut reader = BitReader::new(data);
195        let count = checked_decode_count(reader.read_bits(32)? as usize, "Chimp")?;
196        if count == 0 {
197            return Ok(());
198        }
199
200        output.clear();
201        output.reserve(count);
202        Self::decode_core(&mut reader, count, output)
203    }
204
205    /// Shared decode logic operating on a pre-sized output buffer.
206    fn decode_core(reader: &mut BitReader<'_>, count: usize, result: &mut Vec<f64>) -> Result<()> {
207        let first_bits = reader.read_bits(64)?;
208        result.push(f64::from_bits(first_bits));
209
210        let mut prev_bits = first_bits;
211        let mut prev_leading: u8 = u8::MAX;
212        let mut prev_trailing: u8 = 0;
213
214        for _ in 1..count {
215            if !reader.read_bit()? {
216                // Case 0: identical
217                result.push(f64::from_bits(prev_bits));
218                continue;
219            }
220
221            if reader.read_bit()? {
222                // Case 2: new window (prefix was 11)
223                let bucket = reader.read_bits(3)? as u8;
224                let trailing = reader.read_bits(6)? as u32;
225                let adj_leading = bucket_to_leading_zeros(bucket);
226                let meaningful = reader.read_bits(6)? as u32 + 1; // stored as m-1
227                if adj_leading + meaningful + trailing > 64 {
228                    return Err(EncodingError::CorruptData {
229                        detail: format!(
230                            "chimp case-2: leading ({adj_leading}) + meaningful ({meaningful}) + trailing ({trailing}) exceeds 64"
231                        ),
232                    });
233                }
234                let shifted = reader.read_bits(meaningful as u8)?;
235                let xor = shifted << trailing;
236                let bits = prev_bits ^ xor;
237                result.push(f64::from_bits(bits));
238                prev_bits = bits;
239
240                prev_leading = adj_leading as u8;
241                prev_trailing = trailing as u8;
242            } else {
243                // Case 1: reuse window (prefix was 10)
244                if prev_leading == u8::MAX {
245                    return Err(EncodingError::CorruptData {
246                        detail: "chimp case-1 encountered before any case-2 window".to_string(),
247                    });
248                }
249                let meaningful = 64 - u32::from(prev_leading) - u32::from(prev_trailing);
250                let shifted = reader.read_bits(meaningful as u8)?;
251                let xor = shifted << u32::from(prev_trailing);
252                let bits = prev_bits ^ xor;
253                result.push(f64::from_bits(bits));
254                prev_bits = bits;
255            }
256        }
257
258        Ok(())
259    }
260}
261
262/// Size of the Chimp128 ring buffer.
263const RING_SIZE: usize = 128;
264
265/// Chimp128 float encoder — ring-buffer variant (VLDB 2022, §4.2).
266///
267/// Maintains a ring buffer of the last 128 raw `f64` bit-patterns
268/// and picks the reference that yields the most leading zeros in the
269/// XOR, encoding a 7-bit index when a ring-buffer entry beats the
270/// default sequential reference.
271#[derive(Debug, Clone, Copy)]
272pub struct Chimp128Encoder;
273
274/// Chimp128 float decoder.
275#[derive(Debug, Clone, Copy)]
276pub struct Chimp128Decoder;
277
278impl Chimp128Encoder {
279    /// Encode a sequence of `f64` values using the Chimp128 ring-buffer scheme.
280    ///
281    /// # Errors
282    ///
283    /// Returns [`EncodingError::EmptyInput`] if `values` is empty.
284    pub fn encode(values: &[f64]) -> Result<Vec<u8>> {
285        if values.is_empty() {
286            return Err(EncodingError::EmptyInput {
287                context: "chimp128 encoder",
288            });
289        }
290
291        let mut writer = BitWriter::with_capacity(12 + values.len() * 12);
292
293        // Value count
294        let count = checked_count(values.len())?;
295        writer.write_bits(u64::from(count), 32);
296
297        // First value: raw 64-bit
298        let first_bits = values[0].to_bits();
299        writer.write_bits(first_bits, 64);
300
301        // Ring buffer of raw bit-patterns (circular)
302        let mut ring = [0u64; RING_SIZE];
303        ring[0] = first_bits;
304        let mut ring_len: usize = 1; // how many valid entries (max RING_SIZE)
305        let mut ring_pos: usize = 1; // next write position (mod RING_SIZE)
306
307        // Hash table for O(1) ring lookup.
308        //
309        // Maps the top 8 bits of each bit-pattern to the most recent ring
310        // index with that prefix. Entries sharing the top 8 bits are
311        // guaranteed to produce XORs with ≥8 leading zeros. For periodic
312        // data (where Chimp128 shines), this typically yields exact or
313        // near-exact matches without scanning all 128 entries.
314        //
315        // Sentinel value u8::MAX means no entry for that prefix yet.
316        // 256 bytes of state — negligible compared to the 1 KiB ring.
317        let mut ring_hash = [u8::MAX; 256];
318        ring_hash[(first_bits >> 56) as usize] = 0;
319
320        let mut prev_bits = first_bits;
321        let mut prev_leading: u8 = u8::MAX;
322        let mut prev_trailing: u8 = 0;
323
324        #[allow(clippy::explicit_counter_loop)]
325        // ring_pos wraps modulo RING_SIZE, not a plain counter
326        for &val in &values[1..] {
327            let bits = val.to_bits();
328            let seq_xor = prev_bits ^ bits;
329
330            if seq_xor == 0 {
331                // Case 0: identical to previous — single 0 bit
332                writer.write_bit(false);
333            } else {
334                // Search ring buffer for best reference (most leading zeros)
335                //
336                // O(1) hash probe first, O(128) scan only as fallback.
337                let seq_leading = seq_xor.leading_zeros();
338                let mut best_leading = seq_leading;
339                let mut best_xor = seq_xor;
340                let mut best_idx: Option<u8> = None;
341
342                // Phase 1: Hash probe — check if there's a ring entry with
343                // the same top-8-bit prefix. Such entries share ≥8 high-order
344                // bits, guaranteeing ≥8 leading zeros in the XOR.
345                let hash_key = (bits >> 56) as usize;
346                let hash_candidate = ring_hash[hash_key];
347                let mut skip_scan = false;
348
349                if hash_candidate != u8::MAX {
350                    let ci = hash_candidate as usize;
351                    let ref_xor = ring[ci] ^ bits;
352                    if ref_xor == 0 {
353                        // Perfect match via hash — skip scan entirely.
354                        best_leading = 64;
355                        best_xor = 0;
356                        best_idx = Some(ci as u8);
357                        skip_scan = true;
358                    } else {
359                        let rl = ref_xor.leading_zeros();
360                        if rl > best_leading {
361                            best_leading = rl;
362                            best_xor = ref_xor;
363                            best_idx = Some(ci as u8);
364                        }
365                        // If hash candidate already gives ≥24 leading zeros
366                        // (matching top 3 bytes), the marginal improvement
367                        // from scanning all 128 entries is <1 bit savings.
368                        if best_leading >= 24 {
369                            skip_scan = true;
370                        }
371                    }
372                }
373
374                // Phase 2: Full scan fallback (rare for periodic data).
375                if !skip_scan {
376                    let search_len = ring_len.min(RING_SIZE);
377                    #[allow(clippy::needless_range_loop)] // index doubles as the ring reference id
378                    for i in 0..search_len {
379                        let ref_xor = ring[i] ^ bits;
380                        if ref_xor == 0 {
381                            best_xor = 0;
382                            best_idx = Some(i as u8);
383                            break;
384                        }
385                        let rl = ref_xor.leading_zeros();
386                        if rl > best_leading {
387                            best_leading = rl;
388                            best_xor = ref_xor;
389                            best_idx = Some(i as u8);
390                        }
391                    }
392                }
393
394                // Non-zero XOR — write the '1' prefix
395                writer.write_bit(true);
396
397                if best_xor == 0 {
398                    // Ring reference gave exact match
399                    // Bit 1 = 1 (ring reference), 7-bit index, then case-0-like
400                    // We encode: [1: ring_ref][7: index][0: xor_is_zero]
401                    writer.write_bit(true);
402                    writer.write_bits(u64::from(best_idx.unwrap_or(0)), 7);
403                    // The XOR against the ring entry is 0, but we already wrote
404                    // the '1' prefix meaning non-zero seq XOR. We need to signal
405                    // that the ring-ref XOR itself is zero.
406                    writer.write_bit(false); // ring xor == 0
407                } else {
408                    let xor = best_xor;
409                    let leading = xor.leading_zeros();
410                    let trailing = xor.trailing_zeros();
411                    let bucket = leading_zeros_to_bucket(leading);
412                    let bucket_lz = bucket_to_leading_zeros(bucket);
413
414                    if let Some(idx) = best_idx {
415                        // Ring reference is better
416                        writer.write_bit(true); // use ring reference
417                        writer.write_bits(u64::from(idx), 7);
418                        writer.write_bit(true); // ring xor != 0
419
420                        // Encode the XOR using Chimp bucket scheme
421                        Self::encode_xor(
422                            &mut writer,
423                            xor,
424                            leading,
425                            trailing,
426                            bucket,
427                            bucket_lz,
428                            &mut prev_leading,
429                            &mut prev_trailing,
430                        );
431                    } else {
432                        // Sequential reference (no ring entry was better)
433                        writer.write_bit(false); // use sequential reference
434
435                        // Check if we can reuse the previous window
436                        let can_reuse = prev_leading != u8::MAX
437                            && leading >= u32::from(prev_leading)
438                            && trailing >= u32::from(prev_trailing);
439
440                        if can_reuse {
441                            // Case 1: reuse previous leading/trailing
442                            writer.write_bit(false); // prefix bit = 0 for reuse
443                            let prev_meaningful =
444                                64 - u32::from(prev_leading) - u32::from(prev_trailing);
445                            let shifted = xor >> u32::from(prev_trailing);
446                            writer.write_bits(shifted, prev_meaningful as u8);
447                        } else {
448                            // Case 2: new window with bucket
449                            writer.write_bit(true); // prefix bit = 1 for new window
450                            writer.write_bits(u64::from(bucket), 3);
451                            writer.write_bits(u64::from(trailing), 6);
452                            let adj_leading = bucket_lz;
453                            let adj_meaningful = 64 - adj_leading - trailing;
454                            let shifted = xor >> trailing;
455                            writer.write_bits(u64::from(adj_meaningful.saturating_sub(1)), 6);
456                            writer.write_bits(shifted, adj_meaningful as u8);
457
458                            prev_leading = bucket_lz as u8;
459                            prev_trailing = trailing as u8;
460                        }
461                    }
462                }
463            }
464
465            // Push into ring buffer and update hash index
466            ring[ring_pos % RING_SIZE] = bits;
467            ring_hash[(bits >> 56) as usize] = (ring_pos % RING_SIZE) as u8;
468            ring_pos += 1;
469            if ring_len < RING_SIZE {
470                ring_len += 1;
471            }
472
473            prev_bits = bits;
474        }
475
476        Ok(writer.finish())
477    }
478
479    /// Encode a non-zero XOR value using Chimp's bucket scheme (used for
480    /// ring-buffer references where we always write a fresh window).
481    #[inline]
482    fn encode_xor(
483        writer: &mut BitWriter,
484        xor: u64,
485        _leading: u32,
486        trailing: u32,
487        bucket: u8,
488        bucket_lz: u32,
489        _prev_leading: &mut u8,
490        _prev_trailing: &mut u8,
491    ) {
492        // For ring references we always write a full window (case 2 style)
493        // since the decoder doesn't share prev_leading/prev_trailing state
494        // with a ring-referenced XOR.
495        writer.write_bits(u64::from(bucket), 3);
496        writer.write_bits(u64::from(trailing), 6);
497        let adj_leading = bucket_lz;
498        let adj_meaningful = 64 - adj_leading - trailing;
499        let shifted = xor >> trailing;
500        writer.write_bits(u64::from(adj_meaningful.saturating_sub(1)), 6);
501        writer.write_bits(shifted, adj_meaningful as u8);
502
503        // Do NOT update prev_leading/prev_trailing after a
504        // ring-reference encode. The sequential XOR path and the ring-ref
505        // path are independent state machines — contaminating one from
506        // the other causes ~0.1% of subsequent sequential encodes to
507        // produce the wrong leading/trailing window, which the decoder
508        // cannot reconstruct.
509    }
510}
511
512impl Chimp128Decoder {
513    /// Decode a Chimp128-encoded byte buffer back to `f64` values.
514    ///
515    /// # Errors
516    ///
517    /// Returns [`EncodingError::CorruptData`] if the data is malformed.
518    pub fn decode(data: &[u8]) -> Result<Vec<f64>> {
519        if data.len() < 4 {
520            return Err(EncodingError::CorruptData {
521                detail: "chimp128 data too short for header".to_string(),
522            });
523        }
524
525        let mut reader = BitReader::new(data);
526        let count = checked_decode_count(reader.read_bits(32)? as usize, "Chimp128")?;
527        if count == 0 {
528            return Ok(Vec::new());
529        }
530
531        let mut result = Vec::with_capacity(count);
532        Self::decode_core(&mut reader, count, &mut result)?;
533        Ok(result)
534    }
535
536    /// Decode into a pre-allocated output vector for zero-alloc reuse.
537    ///
538    /// # Errors
539    ///
540    /// Returns [`EncodingError::CorruptData`] if the data is malformed.
541    pub fn decode_into(data: &[u8], output: &mut Vec<f64>) -> Result<()> {
542        if data.len() < 4 {
543            return Err(EncodingError::CorruptData {
544                detail: "chimp128 data too short for header".to_string(),
545            });
546        }
547
548        let mut reader = BitReader::new(data);
549        let count = checked_decode_count(reader.read_bits(32)? as usize, "Chimp128")?;
550        if count == 0 {
551            return Ok(());
552        }
553
554        output.clear();
555        output.reserve(count);
556        Self::decode_core(&mut reader, count, output)
557    }
558
559    /// Shared decode logic.
560    fn decode_core(reader: &mut BitReader<'_>, count: usize, result: &mut Vec<f64>) -> Result<()> {
561        let first_bits = reader.read_bits(64)?;
562        result.push(f64::from_bits(first_bits));
563
564        let mut ring = [0u64; RING_SIZE];
565        ring[0] = first_bits;
566        let mut ring_len: usize = 1;
567        let mut ring_pos: usize = 1;
568
569        let mut prev_bits = first_bits;
570        let mut prev_leading: u8 = u8::MAX;
571        let mut prev_trailing: u8 = 0;
572
573        for _ in 1..count {
574            if !reader.read_bit()? {
575                // Case 0: identical to previous
576                result.push(f64::from_bits(prev_bits));
577                // Push into ring
578                ring[ring_pos % RING_SIZE] = prev_bits;
579                ring_pos += 1;
580                if ring_len < RING_SIZE {
581                    ring_len += 1;
582                }
583                continue;
584            }
585
586            // Non-zero sequential XOR — check reference type
587            let use_ring = reader.read_bit()?;
588
589            if use_ring {
590                let idx = reader.read_bits(7)? as usize;
591                if idx >= ring_len.min(RING_SIZE) {
592                    return Err(EncodingError::CorruptData {
593                        detail: format!(
594                            "chimp128: ring index {idx} out of range (ring_len={})",
595                            ring_len.min(RING_SIZE)
596                        ),
597                    });
598                }
599                let ref_bits = ring[idx];
600
601                let ring_xor_nonzero = reader.read_bit()?;
602                if !ring_xor_nonzero {
603                    // Ring XOR is zero — value == ring entry
604                    let bits = ref_bits;
605                    result.push(f64::from_bits(bits));
606                    prev_bits = bits;
607                } else {
608                    // Decode XOR against ring entry (always full window / case-2 style)
609                    let bucket = reader.read_bits(3)? as u8;
610                    let trailing = reader.read_bits(6)? as u32;
611                    let adj_leading = bucket_to_leading_zeros(bucket);
612                    let meaningful = reader.read_bits(6)? as u32 + 1;
613                    if adj_leading + meaningful + trailing > 64 {
614                        return Err(EncodingError::CorruptData {
615                            detail: format!(
616                                "chimp128 ring-ref: leading ({adj_leading}) + meaningful ({meaningful}) + trailing ({trailing}) exceeds 64"
617                            ),
618                        });
619                    }
620                    let shifted = reader.read_bits(meaningful as u8)?;
621                    let xor = shifted << trailing;
622                    let bits = ref_bits ^ xor;
623                    result.push(f64::from_bits(bits));
624                    prev_bits = bits;
625
626                    // Do NOT update prev_leading/prev_trailing
627                    // from ring-ref decodes — must match encoder behavior
628                    // where ring-ref and sequential paths are independent.
629                }
630            } else {
631                // Sequential reference
632                let new_window = reader.read_bit()?;
633                if new_window {
634                    // Case 2: new window with bucket
635                    let bucket = reader.read_bits(3)? as u8;
636                    let trailing = reader.read_bits(6)? as u32;
637                    let adj_leading = bucket_to_leading_zeros(bucket);
638                    let meaningful = reader.read_bits(6)? as u32 + 1;
639                    if adj_leading + meaningful + trailing > 64 {
640                        return Err(EncodingError::CorruptData {
641                            detail: format!(
642                                "chimp128 case-2: leading ({adj_leading}) + meaningful ({meaningful}) + trailing ({trailing}) exceeds 64"
643                            ),
644                        });
645                    }
646                    let shifted = reader.read_bits(meaningful as u8)?;
647                    let xor = shifted << trailing;
648                    let bits = prev_bits ^ xor;
649                    result.push(f64::from_bits(bits));
650                    prev_bits = bits;
651
652                    prev_leading = adj_leading as u8;
653                    prev_trailing = trailing as u8;
654                } else {
655                    // Case 1: reuse previous leading/trailing window
656                    if prev_leading == u8::MAX {
657                        return Err(EncodingError::CorruptData {
658                            detail: "chimp128 case-1 encountered before any case-2 window"
659                                .to_string(),
660                        });
661                    }
662                    let meaningful = 64 - u32::from(prev_leading) - u32::from(prev_trailing);
663                    let shifted = reader.read_bits(meaningful as u8)?;
664                    let xor = shifted << u32::from(prev_trailing);
665                    let bits = prev_bits ^ xor;
666                    result.push(f64::from_bits(bits));
667                    prev_bits = bits;
668                }
669            }
670
671            // Push decoded value into ring
672            ring[ring_pos % RING_SIZE] = prev_bits;
673            ring_pos += 1;
674            if ring_len < RING_SIZE {
675                ring_len += 1;
676            }
677        }
678
679        Ok(())
680    }
681}
682
683#[cfg(test)]
684mod tests {
685    use super::*;
686
687    #[test]
688    fn roundtrip_constant() {
689        let values = vec![42.0_f64; 100];
690        let encoded = ChimpEncoder::encode(&values).unwrap();
691        let decoded = ChimpDecoder::decode(&encoded).unwrap();
692        assert_eq!(values.len(), decoded.len());
693        for (a, b) in values.iter().zip(decoded.iter()) {
694            assert_eq!(a.to_bits(), b.to_bits());
695        }
696    }
697
698    #[test]
699    fn roundtrip_sinwave() {
700        let values: Vec<f64> = (0..1000)
701            .map(|i| (i as f64 * 0.01).sin() * 100.0 + 50.0)
702            .collect();
703        let encoded = ChimpEncoder::encode(&values).unwrap();
704        let decoded = ChimpDecoder::decode(&encoded).unwrap();
705        assert_eq!(values.len(), decoded.len());
706        for (a, b) in values.iter().zip(decoded.iter()) {
707            assert_eq!(a.to_bits(), b.to_bits());
708        }
709    }
710
711    #[test]
712    fn roundtrip_special_values() {
713        let values = vec![
714            0.0_f64,
715            -0.0,
716            f64::NAN,
717            f64::INFINITY,
718            f64::NEG_INFINITY,
719            f64::MIN,
720            f64::MAX,
721            f64::MIN_POSITIVE,
722            f64::EPSILON,
723            1.0,
724            -1.0,
725        ];
726        let encoded = ChimpEncoder::encode(&values).unwrap();
727        let decoded = ChimpDecoder::decode(&encoded).unwrap();
728        assert_eq!(values.len(), decoded.len());
729        for (a, b) in values.iter().zip(decoded.iter()) {
730            assert_eq!(a.to_bits(), b.to_bits(), "mismatch for value {a}");
731        }
732    }
733
734    #[test]
735    fn roundtrip_single() {
736        let values = vec![std::f64::consts::PI];
737        let encoded = ChimpEncoder::encode(&values).unwrap();
738        let decoded = ChimpDecoder::decode(&encoded).unwrap();
739        assert_eq!(values[0].to_bits(), decoded[0].to_bits());
740    }
741
742    #[test]
743    fn empty_error() {
744        assert!(ChimpEncoder::encode(&[]).is_err());
745    }
746
747    #[test]
748    fn compression_ratio_regular_metrics() {
749        // Simulate slowly-varying integer-like metrics (e.g., temperature * 10)
750        // that change by small amounts — ideal for XOR encoders
751        let values: Vec<f64> = (0..10_000)
752            .map(|i| {
753                // Slowly varying: 50.0, 50.0, 50.1, 50.1, 50.2, …
754                50.0 + (i / 100) as f64 * 0.1
755            })
756            .collect();
757        let raw_size = values.len() * 8;
758        let encoded = ChimpEncoder::encode(&values).unwrap();
759        #[allow(clippy::cast_precision_loss)]
760        let ratio = raw_size as f64 / encoded.len() as f64;
761        // Should achieve > 2x on data with high temporal locality
762        assert!(
763            ratio > 2.0,
764            "Expected ratio > 2x, got {ratio:.1}x ({raw_size} raw, {} encoded)",
765            encoded.len()
766        );
767    }
768
769    #[test]
770    fn decode_into_reuses_buffer() {
771        let values: Vec<f64> = (0..500).map(|i| i as f64 * 0.5).collect();
772        let encoded = ChimpEncoder::encode(&values).unwrap();
773
774        let mut output = Vec::new();
775        ChimpDecoder::decode_into(&encoded, &mut output).unwrap();
776        assert_eq!(values.len(), output.len());
777        for (a, b) in values.iter().zip(output.iter()) {
778            assert_eq!(a.to_bits(), b.to_bits());
779        }
780
781        // Decode again into the same buffer — should clear+reuse
782        let values2: Vec<f64> = (0..300).map(|i| i as f64 * 1.1).collect();
783        let encoded2 = ChimpEncoder::encode(&values2).unwrap();
784        ChimpDecoder::decode_into(&encoded2, &mut output).unwrap();
785        assert_eq!(values2.len(), output.len());
786        for (a, b) in values2.iter().zip(output.iter()) {
787            assert_eq!(a.to_bits(), b.to_bits());
788        }
789    }
790
791    #[test]
792    fn roundtrip_1m_floats() {
793        // Verify correctness on 1M values
794        let values: Vec<f64> = (0..1_000_000)
795            .map(|i| 50.0 + (i as f64 * 0.001).sin() * 10.0)
796            .collect();
797        let encoded = ChimpEncoder::encode(&values).unwrap();
798        let decoded = ChimpDecoder::decode(&encoded).unwrap();
799        assert_eq!(values.len(), decoded.len());
800        // Spot check first, middle, and last
801        assert_eq!(values[0].to_bits(), decoded[0].to_bits());
802        assert_eq!(values[500_000].to_bits(), decoded[500_000].to_bits());
803        assert_eq!(values[999_999].to_bits(), decoded[999_999].to_bits());
804    }
805
806    // ── Chimp128 tests ───────────────────────────────────────────────
807
808    #[test]
809    fn chimp128_roundtrip_constant() {
810        let values = vec![42.0_f64; 200];
811        let encoded = Chimp128Encoder::encode(&values).unwrap();
812        let decoded = Chimp128Decoder::decode(&encoded).unwrap();
813        assert_eq!(values.len(), decoded.len());
814        for (a, b) in values.iter().zip(decoded.iter()) {
815            assert_eq!(a.to_bits(), b.to_bits());
816        }
817    }
818
819    #[test]
820    fn chimp128_roundtrip_sinwave() {
821        // Sinusoidal data — Chimp128 should handle well due to repeating
822        // patterns in the ring buffer.
823        let values: Vec<f64> = (0..2000)
824            .map(|i| (i as f64 * 0.01).sin() * 100.0 + 50.0)
825            .collect();
826        let encoded = Chimp128Encoder::encode(&values).unwrap();
827        let decoded = Chimp128Decoder::decode(&encoded).unwrap();
828        assert_eq!(values.len(), decoded.len());
829        for (a, b) in values.iter().zip(decoded.iter()) {
830            assert_eq!(a.to_bits(), b.to_bits());
831        }
832    }
833
834    #[test]
835    fn chimp128_roundtrip_special_values() {
836        let values = vec![
837            0.0_f64,
838            -0.0,
839            f64::NAN,
840            f64::INFINITY,
841            f64::NEG_INFINITY,
842            f64::MIN,
843            f64::MAX,
844            f64::MIN_POSITIVE,
845            f64::EPSILON,
846            1.0,
847            -1.0,
848        ];
849        let encoded = Chimp128Encoder::encode(&values).unwrap();
850        let decoded = Chimp128Decoder::decode(&encoded).unwrap();
851        assert_eq!(values.len(), decoded.len());
852        for (a, b) in values.iter().zip(decoded.iter()) {
853            assert_eq!(a.to_bits(), b.to_bits(), "mismatch for value {a}");
854        }
855    }
856
857    #[test]
858    fn chimp128_beats_chimp_on_periodic() {
859        // Periodic sensor data: cycles through a small set of values
860        // repeatedly.  Chimp128's ring buffer should find exact or near
861        // matches and compress better than plain Chimp.
862        let base_values: Vec<f64> = (0..64).map(|i| 20.0 + (i as f64) * 0.5).collect();
863        let values: Vec<f64> = base_values.iter().cycle().take(5000).copied().collect();
864
865        let chimp_encoded = ChimpEncoder::encode(&values).unwrap();
866        let chimp128_encoded = Chimp128Encoder::encode(&values).unwrap();
867
868        // Verify correctness
869        let decoded = Chimp128Decoder::decode(&chimp128_encoded).unwrap();
870        assert_eq!(values.len(), decoded.len());
871        for (a, b) in values.iter().zip(decoded.iter()) {
872            assert_eq!(a.to_bits(), b.to_bits());
873        }
874
875        // Chimp128 should produce equal or smaller output
876        assert!(
877            chimp128_encoded.len() <= chimp_encoded.len(),
878            "Expected Chimp128 ({} bytes) <= Chimp ({} bytes) on periodic data",
879            chimp128_encoded.len(),
880            chimp_encoded.len(),
881        );
882    }
883
884    #[test]
885    fn chimp128_empty_error() {
886        assert!(Chimp128Encoder::encode(&[]).is_err());
887    }
888
889    #[test]
890    fn chimp128_single_value() {
891        let values = vec![std::f64::consts::PI];
892        let encoded = Chimp128Encoder::encode(&values).unwrap();
893        let decoded = Chimp128Decoder::decode(&encoded).unwrap();
894        assert_eq!(values[0].to_bits(), decoded[0].to_bits());
895    }
896
897    #[test]
898    fn chimp128_decode_into() {
899        let values: Vec<f64> = (0..500).map(|i| i as f64 * 0.5).collect();
900        let encoded = Chimp128Encoder::encode(&values).unwrap();
901        let mut output = Vec::new();
902        Chimp128Decoder::decode_into(&encoded, &mut output).unwrap();
903        assert_eq!(values.len(), output.len());
904        for (a, b) in values.iter().zip(output.iter()) {
905            assert_eq!(a.to_bits(), b.to_bits());
906        }
907    }
908}
909
910#[cfg(test)]
911mod proptests {
912    use super::*;
913    use proptest::prelude::*;
914
915    proptest! {
916        #[test]
917        fn roundtrip_arbitrary(values in proptest::collection::vec(any::<f64>(), 1..500)) {
918            let encoded = ChimpEncoder::encode(&values).unwrap();
919            let decoded = ChimpDecoder::decode(&encoded).unwrap();
920            prop_assert_eq!(values.len(), decoded.len());
921            for (a, b) in values.iter().zip(decoded.iter()) {
922                prop_assert_eq!(a.to_bits(), b.to_bits());
923            }
924        }
925
926        #[test]
927        fn chimp128_roundtrip_arbitrary(values in proptest::collection::vec(any::<f64>(), 1..500)) {
928            let encoded = Chimp128Encoder::encode(&values).unwrap();
929            let decoded = Chimp128Decoder::decode(&encoded).unwrap();
930            prop_assert_eq!(values.len(), decoded.len());
931            for (a, b) in values.iter().zip(decoded.iter()) {
932                prop_assert_eq!(a.to_bits(), b.to_bits());
933            }
934        }
935    }
936}