Skip to main content

nodedb_codec/
alp.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! ALP (Adaptive Lossless floating-Point) codec for f64 columns.
4//!
5//! Most real-world float metrics originate as fixed-point decimals (e.g.,
6//! `23.5`, `99.99`). ALP finds the optimal power-of-10 multiplier that
7//! converts them to integers **losslessly**: `23.5 × 10 = 235` round-trips
8//! exactly via `235 / 10 = 23.5`. The resulting integers have tiny bit-widths
9//! and compress 3-6x better than Gorilla with SIMD-friendly bit-packing.
10//!
11//! Values that don't round-trip exactly (true arbitrary doubles, NaN, Inf)
12//! are stored as exceptions — their original f64 bits are preserved separately.
13//!
14//! Wire format:
15//! ```text
16//! [4 bytes] total value count (LE u32)
17//! [1 byte]  exponent e (power of 10: factor = 10^e, range 0-18)
18//! [1 byte]  factor index f (combined encode/decode factor)
19//! [4 bytes] exception count (LE u32)
20//! [exception_count × 12 bytes] exceptions: (index: u32, value_bits: u64)
21//! [N bytes] FastLanes-packed encoded integers (FOR + bit-pack)
22//! ```
23//!
24//! Reference: "ALP: Adaptive Lossless floating-Point Compression"
25//! (Afroozeh et al., SIGMOD 2023)
26
27use std::mem::size_of;
28
29use crate::bounds::{
30    checked_add, checked_capacity, checked_mul, checked_range, decoded_len, encode_input_len,
31    encode_u32_len, u32_to_usize,
32};
33use crate::error::CodecError;
34use crate::fastlanes;
35
36/// Maximum exponent (10^18 fits in i64).
37const MAX_EXPONENT: u8 = 18;
38
39/// Powers of 10 as f64 for the encode multiplier.
40const POW10: [f64; 19] = [
41    1.0,
42    10.0,
43    100.0,
44    1_000.0,
45    10_000.0,
46    100_000.0,
47    1_000_000.0,
48    10_000_000.0,
49    100_000_000.0,
50    1_000_000_000.0,
51    10_000_000_000.0,
52    100_000_000_000.0,
53    1_000_000_000_000.0,
54    10_000_000_000_000.0,
55    100_000_000_000_000.0,
56    1_000_000_000_000_000.0,
57    10_000_000_000_000_000.0,
58    100_000_000_000_000_000.0,
59    1_000_000_000_000_000_000.0,
60];
61
62/// Inverse powers of 10 for the decode divisor.
63const INV_POW10: [f64; 19] = [
64    1.0,
65    0.1,
66    0.01,
67    0.001,
68    0.000_1,
69    0.000_01,
70    0.000_001,
71    0.000_000_1,
72    0.000_000_01,
73    0.000_000_001,
74    0.000_000_000_1,
75    0.000_000_000_01,
76    0.000_000_000_001,
77    0.000_000_000_000_1,
78    0.000_000_000_000_01,
79    0.000_000_000_000_001,
80    0.000_000_000_000_000_1,
81    0.000_000_000_000_000_01,
82    0.000_000_000_000_000_001,
83];
84
85use crate::CODEC_SAMPLE_SIZE;
86
87// ---------------------------------------------------------------------------
88// Public encode / decode API
89// ---------------------------------------------------------------------------
90
91/// Encode a slice of f64 values using ALP compression.
92///
93/// Finds the optimal decimal exponent, converts to integers, stores
94/// exceptions for values that don't round-trip, and bit-packs the integers
95/// via FastLanes.
96pub fn encode(values: &[f64]) -> Result<Vec<u8>, CodecError> {
97    let decoded_bytes = checked_mul(values.len(), size_of::<f64>(), "ALP input bytes")?;
98    decoded_len(decoded_bytes, "ALP input")?;
99    let count = encode_input_len(values.len(), "ALP value count")?;
100
101    if values.is_empty() {
102        let mut out = Vec::with_capacity(11);
103        out.extend_from_slice(&0u32.to_le_bytes()); // count
104        out.push(0); // encode exponent
105        out.push(0); // decode exponent
106        out.push(0); // mode
107        out.extend_from_slice(&0u32.to_le_bytes()); // exception count
108        return Ok(out);
109    }
110
111    // Step 1: Find optimal parameters by sampling.
112    let params = find_best_params(values);
113    let factor = POW10[params.encode_exp as usize];
114
115    // Step 2: Encode values to integers, collect exceptions.
116    let mut encoded_ints = Vec::with_capacity(values.len());
117    let mut exceptions: Vec<(u32, u64)> = Vec::new();
118
119    for (i, &val) in values.iter().enumerate() {
120        let encoded = try_alp_encode(val, factor, params.decode_exp, params.mode);
121        match encoded {
122            Some(int_val) => {
123                encoded_ints.push(int_val);
124            }
125            None => {
126                // Exception: store original bits, use 0 as placeholder in int array.
127                exceptions.push((
128                    u32::try_from(i).map_err(|_| CodecError::ResourceLimit {
129                        resource: "ALP exception index".into(),
130                        requested: i,
131                        limit: u32::MAX as usize,
132                    })?,
133                    val.to_bits(),
134                ));
135                encoded_ints.push(0);
136            }
137        }
138    }
139
140    // Step 3: Build output.
141    let exception_count = encode_u32_len(exceptions.len(), "ALP exception count")?;
142    let packed_ints = fastlanes::encode(&encoded_ints)?;
143
144    let exceptions_size = checked_mul(exceptions.len(), 12, "ALP exceptions")?;
145    let capacity = checked_add(
146        checked_add(11, exceptions_size, "ALP output")?,
147        packed_ints.len(),
148        "ALP output",
149    )?;
150    let mut out = Vec::with_capacity(capacity);
151
152    // Header.
153    out.extend_from_slice(&count.to_le_bytes());
154    out.push(params.encode_exp);
155    out.push(params.decode_exp);
156    out.push(match params.mode {
157        DecodeMode::MultiplyInverse => 0,
158        DecodeMode::DivideByFactor => 1,
159    });
160    out.extend_from_slice(&exception_count.to_le_bytes());
161
162    // Exceptions.
163    for &(idx, bits) in &exceptions {
164        out.extend_from_slice(&idx.to_le_bytes());
165        out.extend_from_slice(&bits.to_le_bytes());
166    }
167
168    // Packed integers.
169    out.extend_from_slice(&packed_ints);
170
171    Ok(out)
172}
173
174/// Decode ALP-compressed bytes back to f64 values.
175pub fn decode(data: &[u8]) -> Result<Vec<f64>, CodecError> {
176    const HEADER_SIZE: usize = 11; // 4 + 1 + 1 + 1 + 4
177
178    if data.len() < HEADER_SIZE {
179        return Err(CodecError::Truncated {
180            expected: HEADER_SIZE,
181            actual: data.len(),
182        });
183    }
184
185    let count = u32_to_usize(
186        u32::from_le_bytes([data[0], data[1], data[2], data[3]]),
187        "ALP value count",
188    )?;
189    decoded_len(
190        checked_mul(count, size_of::<f64>(), "ALP decoded values")?,
191        "ALP",
192    )?;
193    let encode_exp = data[4];
194    let decode_exp = data[5];
195    let mode = match data[6] {
196        0 => DecodeMode::MultiplyInverse,
197        1 => DecodeMode::DivideByFactor,
198        m => {
199            return Err(CodecError::Corrupt {
200                detail: format!("invalid ALP decode mode {m}"),
201            });
202        }
203    };
204    let exception_count = u32_to_usize(
205        u32::from_le_bytes([data[7], data[8], data[9], data[10]]),
206        "ALP exception count",
207    )?;
208
209    if count == 0 {
210        if exception_count != 0 || data.len() != HEADER_SIZE {
211            return Err(CodecError::Corrupt {
212                detail: "non-canonical empty ALP frame".into(),
213            });
214        }
215        return Ok(Vec::new());
216    }
217    if exception_count > count {
218        return Err(CodecError::Corrupt {
219            detail: "ALP exception count exceeds value count".into(),
220        });
221    }
222
223    if encode_exp > MAX_EXPONENT || decode_exp > MAX_EXPONENT {
224        return Err(CodecError::Corrupt {
225            detail: format!("invalid ALP exponents encode={encode_exp}, decode={decode_exp}"),
226        });
227    }
228
229    // Read exceptions.
230    decoded_len(
231        checked_mul(
232            exception_count,
233            size_of::<(usize, u64)>(),
234            "ALP exception allocation",
235        )?,
236        "ALP exceptions",
237    )?;
238    let exceptions_size = checked_mul(exception_count, 12, "ALP exceptions")?;
239    let exceptions_end = checked_add(HEADER_SIZE, exceptions_size, "ALP exceptions")?;
240    checked_range(data, HEADER_SIZE, exceptions_size, "ALP exceptions")?;
241
242    let exception_capacity = checked_capacity(
243        exception_count,
244        size_of::<(usize, u64)>(),
245        "ALP exception allocation",
246    )?;
247    let mut exceptions = Vec::with_capacity(exception_capacity);
248    let mut pos = HEADER_SIZE;
249    let mut previous_index = None;
250    for _ in 0..exception_count {
251        let idx = u32_to_usize(
252            u32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]),
253            "ALP exception index",
254        )?;
255        if idx >= count || previous_index.is_some_and(|previous| idx <= previous) {
256            return Err(CodecError::Corrupt {
257                detail: "ALP exceptions must be ordered unique in-range indices".into(),
258            });
259        }
260        previous_index = Some(idx);
261        let bits = u64::from_le_bytes([
262            data[pos + 4],
263            data[pos + 5],
264            data[pos + 6],
265            data[pos + 7],
266            data[pos + 8],
267            data[pos + 9],
268            data[pos + 10],
269            data[pos + 11],
270        ]);
271        exceptions.push((idx, bits));
272        pos += 12;
273    }
274
275    // Decode FastLanes-packed integers.
276    let encoded_ints =
277        fastlanes::decode(&data[exceptions_end..]).map_err(|e| CodecError::Corrupt {
278            detail: format!("ALP fastlanes decode: {e}"),
279        })?;
280
281    if encoded_ints.len() != count {
282        return Err(CodecError::Corrupt {
283            detail: format!(
284                "ALP int count mismatch: expected {count}, got {}",
285                encoded_ints.len()
286            ),
287        });
288    }
289
290    // Reconstruct f64 values using the same decode mode that was selected during encode.
291    let value_capacity = checked_capacity(count, size_of::<f64>(), "ALP values")?;
292    let mut values = Vec::with_capacity(value_capacity);
293    for &int_val in &encoded_ints {
294        values.push(alp_decode_value(int_val, decode_exp, mode));
295    }
296
297    // Patch exceptions. The encoder uses a zero integer placeholder and only
298    // emits an exception when the original value cannot round-trip.
299    let factor = POW10[encode_exp as usize];
300    for &(idx, bits) in &exceptions {
301        let value = f64::from_bits(bits);
302        if encoded_ints[idx] != 0 || try_alp_encode(value, factor, decode_exp, mode).is_some() {
303            return Err(CodecError::Corrupt {
304                detail: "non-canonical ALP exception".into(),
305            });
306        }
307        values[idx] = value;
308    }
309
310    Ok(values)
311}
312
313/// Check what fraction of values can be ALP-encoded at the best exponent.
314///
315/// Returns a value between 0.0 and 1.0. Values > 0.95 mean ALP is a good
316/// fit. Used by auto-detection to choose between ALP and Gorilla.
317pub fn alp_encodability(values: &[f64]) -> f64 {
318    if values.is_empty() {
319        return 1.0;
320    }
321
322    let sample_end = values.len().min(CODEC_SAMPLE_SIZE);
323    let sample = &values[..sample_end];
324    let params = find_best_params(sample);
325    let factor = POW10[params.encode_exp as usize];
326
327    let encodable = sample
328        .iter()
329        .filter(|&&v| try_alp_encode(v, factor, params.decode_exp, params.mode).is_some())
330        .count();
331
332    encodable as f64 / sample.len() as f64
333}
334
335// ---------------------------------------------------------------------------
336// ALP core: exponent selection + encode/decode
337// ---------------------------------------------------------------------------
338
339/// Decode mode: whether to multiply by inverse or divide by factor.
340///
341/// IEEE 754 multiplication and division produce different bit patterns
342/// for the same logical value (e.g., `3 * 0.1 != 3 / 10.0` at the bit level).
343/// ALP tries both and picks whichever matches the original data.
344#[derive(Debug, Clone, Copy, PartialEq, Eq)]
345enum DecodeMode {
346    /// Decode via `encoded * INV_POW10[e]` (matches data computed as `x * 0.1`).
347    MultiplyInverse,
348    /// Decode via `encoded / POW10[e]` (matches data computed as `x / 10`).
349    DivideByFactor,
350}
351
352/// Try to ALP-encode a single f64 value.
353///
354/// Returns `Some(i64)` if the value round-trips exactly, `None` otherwise.
355#[inline]
356fn try_alp_encode(value: f64, factor: f64, decode_exp: u8, mode: DecodeMode) -> Option<i64> {
357    if !value.is_finite() {
358        return None;
359    }
360
361    let scaled = value * factor;
362    if scaled < i64::MIN as f64 || scaled > i64::MAX as f64 {
363        return None;
364    }
365
366    let encoded = scaled.round() as i64;
367
368    let decoded = match mode {
369        DecodeMode::MultiplyInverse => encoded as f64 * INV_POW10[decode_exp as usize],
370        DecodeMode::DivideByFactor => encoded as f64 / POW10[decode_exp as usize],
371    };
372
373    if decoded.to_bits() == value.to_bits() {
374        Some(encoded)
375    } else {
376        None
377    }
378}
379
380/// Reconstruct a single f64 from an ALP-encoded integer.
381#[inline]
382fn alp_decode_value(encoded: i64, decode_exp: u8, mode: DecodeMode) -> f64 {
383    match mode {
384        DecodeMode::MultiplyInverse => encoded as f64 * INV_POW10[decode_exp as usize],
385        DecodeMode::DivideByFactor => encoded as f64 / POW10[decode_exp as usize],
386    }
387}
388
389/// Find the best (exponent, factor_index) pair for a set of values.
390///
391/// Tries all exponents 0-18 and picks the one that encodes the most
392/// values losslessly. The factor_index may differ from exponent when
393/// the decode factor produces better roundtrips.
394/// ALP parameters determined by sampling.
395struct AlpParams {
396    encode_exp: u8,
397    decode_exp: u8,
398    mode: DecodeMode,
399}
400
401/// Find the best ALP parameters by trying all exponents and both decode modes.
402fn find_best_params(values: &[f64]) -> AlpParams {
403    let sample_end = values.len().min(CODEC_SAMPLE_SIZE);
404    let sample = &values[..sample_end];
405
406    let mut best = AlpParams {
407        encode_exp: 0,
408        decode_exp: 0,
409        mode: DecodeMode::MultiplyInverse,
410    };
411    let mut best_count: usize = 0;
412
413    for e in 0..=MAX_EXPONENT {
414        let factor = POW10[e as usize];
415
416        for mode in [DecodeMode::MultiplyInverse, DecodeMode::DivideByFactor] {
417            // Try same decode exponent.
418            let count = sample
419                .iter()
420                .filter(|&&v| try_alp_encode(v, factor, e, mode).is_some())
421                .count();
422
423            if count > best_count {
424                best_count = count;
425                best = AlpParams {
426                    encode_exp: e,
427                    decode_exp: e,
428                    mode,
429                };
430            }
431
432            // Try neighboring decode exponents.
433            if e > 0 {
434                let count_alt = sample
435                    .iter()
436                    .filter(|&&v| try_alp_encode(v, factor, e - 1, mode).is_some())
437                    .count();
438                if count_alt > best_count {
439                    best_count = count_alt;
440                    best = AlpParams {
441                        encode_exp: e,
442                        decode_exp: e - 1,
443                        mode,
444                    };
445                }
446            }
447            if e < MAX_EXPONENT {
448                let count_alt = sample
449                    .iter()
450                    .filter(|&&v| try_alp_encode(v, factor, e + 1, mode).is_some())
451                    .count();
452                if count_alt > best_count {
453                    best_count = count_alt;
454                    best = AlpParams {
455                        encode_exp: e,
456                        decode_exp: e + 1,
457                        mode,
458                    };
459                }
460            }
461        }
462    }
463
464    best
465}
466
467#[cfg(test)]
468mod tests {
469    use super::*;
470
471    fn encode(values: &[f64]) -> Vec<u8> {
472        super::encode(values).expect("test ALP encode")
473    }
474
475    #[test]
476    fn empty_roundtrip() {
477        let encoded = encode(&[]);
478        let decoded = decode(&encoded).unwrap();
479        assert!(decoded.is_empty());
480    }
481
482    #[test]
483    fn single_value() {
484        let values = vec![23.5f64];
485        let encoded = encode(&values);
486        let decoded = decode(&encoded).unwrap();
487        assert_eq!(decoded[0].to_bits(), values[0].to_bits());
488    }
489
490    #[test]
491    fn integer_values() {
492        // Integer values: exponent=0, factor=1.
493        let values: Vec<f64> = (0..1000).map(|i| i as f64).collect();
494        let encoded = encode(&values);
495        let decoded = decode(&encoded).unwrap();
496        for (a, b) in values.iter().zip(decoded.iter()) {
497            assert_eq!(a.to_bits(), b.to_bits(), "mismatch: {a} vs {b}");
498        }
499    }
500
501    #[test]
502    fn decimal_values_one_digit() {
503        // Values like 23.5, 99.1 — exponent=1, factor=10.
504        let values: Vec<f64> = (0..1000).map(|i| i as f64 * 0.1).collect();
505        let encoded = encode(&values);
506        let decoded = decode(&encoded).unwrap();
507        for (i, (a, b)) in values.iter().zip(decoded.iter()).enumerate() {
508            assert_eq!(a.to_bits(), b.to_bits(), "mismatch at {i}: {a} vs {b}");
509        }
510    }
511
512    #[test]
513    fn decimal_values_two_digits() {
514        // Values like 99.99 — exponent=2, factor=100.
515        let values: Vec<f64> = (0..1000).map(|i| i as f64 * 0.01).collect();
516        let encoded = encode(&values);
517        let decoded = decode(&encoded).unwrap();
518        for (i, (a, b)) in values.iter().zip(decoded.iter()).enumerate() {
519            assert_eq!(a.to_bits(), b.to_bits(), "mismatch at {i}: {a} vs {b}");
520        }
521    }
522
523    #[test]
524    fn typical_cpu_metrics() {
525        // CPU percentages: 0.0 to 100.0 with 0.1 resolution.
526        let mut values = Vec::with_capacity(10_000);
527        let mut rng: u64 = 42;
528        for _ in 0..10_000 {
529            rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
530            let cpu = ((rng >> 33) as f64 / (u32::MAX as f64)) * 100.0;
531            // Round to 0.1 to simulate typical metric resolution.
532            let cpu = (cpu * 10.0).round() / 10.0;
533            values.push(cpu);
534        }
535        let encoded = encode(&values);
536        let decoded = decode(&encoded).unwrap();
537        for (i, (a, b)) in values.iter().zip(decoded.iter()).enumerate() {
538            assert_eq!(a.to_bits(), b.to_bits(), "mismatch at {i}: {a} vs {b}");
539        }
540
541        // ALP should compress much better than raw.
542        let raw_size = values.len() * 8;
543        let ratio = raw_size as f64 / encoded.len() as f64;
544        assert!(
545            ratio > 3.0,
546            "ALP should compress CPU metrics >3x, got {ratio:.1}x"
547        );
548    }
549
550    #[test]
551    fn temperature_readings() {
552        // Temperatures: -40.0 to 50.0 with 0.01 resolution.
553        let mut values = Vec::with_capacity(10_000);
554        let mut rng: u64 = 99;
555        for _ in 0..10_000 {
556            rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
557            let temp = ((rng >> 33) as f64 / (u32::MAX as f64)) * 90.0 - 40.0;
558            let temp = (temp * 100.0).round() / 100.0;
559            values.push(temp);
560        }
561        let encoded = encode(&values);
562        let decoded = decode(&encoded).unwrap();
563        for (i, (a, b)) in values.iter().zip(decoded.iter()).enumerate() {
564            assert_eq!(a.to_bits(), b.to_bits(), "mismatch at {i}: {a} vs {b}");
565        }
566    }
567
568    #[test]
569    fn exception_handling() {
570        // Mix of ALP-encodable and non-encodable values.
571        let mut values = vec![23.5, 99.9, 100.1, 50.0];
572        values.push(f64::NAN);
573        values.push(f64::INFINITY);
574        values.push(f64::NEG_INFINITY);
575        values.push(std::f64::consts::PI); // Irrational — likely exception.
576
577        let encoded = encode(&values);
578        let decoded = decode(&encoded).unwrap();
579
580        for (i, (a, b)) in values.iter().zip(decoded.iter()).enumerate() {
581            assert_eq!(
582                a.to_bits(),
583                b.to_bits(),
584                "mismatch at {i}: {a} ({:016x}) vs {b} ({:016x})",
585                a.to_bits(),
586                b.to_bits()
587            );
588        }
589    }
590
591    #[test]
592    fn all_exceptions() {
593        // All values are irrational — all exceptions.
594        let values: Vec<f64> = (1..100).map(|i| std::f64::consts::PI * i as f64).collect();
595        let encoded = encode(&values);
596        let decoded = decode(&encoded).unwrap();
597        for (i, (a, b)) in values.iter().zip(decoded.iter()).enumerate() {
598            assert_eq!(a.to_bits(), b.to_bits(), "mismatch at {i}");
599        }
600    }
601
602    #[test]
603    fn encodability_check() {
604        // Decimal values should have high encodability.
605        let decimal_values: Vec<f64> = (0..1000).map(|i| i as f64 * 0.1).collect();
606        assert!(alp_encodability(&decimal_values) > 0.95);
607
608        // PI multiples: may still be encodable at high exponents, but the
609        // resulting integers will have wide bit-widths (poor compression).
610        // The key metric is that decimals have HIGHER encodability than irrationals.
611        let irrational_values: Vec<f64> =
612            (1..1000).map(|i| std::f64::consts::PI * i as f64).collect();
613        let irrational_enc = alp_encodability(&irrational_values);
614        // Decimal values should always be better than irrational ones.
615        assert!(
616            alp_encodability(&decimal_values) >= irrational_enc,
617            "decimal encodability should be >= irrational"
618        );
619    }
620
621    #[test]
622    fn better_than_gorilla_for_decimals() {
623        let values: Vec<f64> = (0..10_000).map(|i| i as f64 * 0.1).collect();
624        let alp_encoded = encode(&values);
625        let gorilla_encoded = crate::gorilla::encode_f64(&values);
626
627        assert!(
628            alp_encoded.len() < gorilla_encoded.len(),
629            "ALP ({} bytes) should beat Gorilla ({} bytes) on decimal data",
630            alp_encoded.len(),
631            gorilla_encoded.len()
632        );
633    }
634
635    #[test]
636    fn zero_values() {
637        let values = vec![0.0f64; 1000];
638        let encoded = encode(&values);
639        let decoded = decode(&encoded).unwrap();
640        for (a, b) in values.iter().zip(decoded.iter()) {
641            assert_eq!(a.to_bits(), b.to_bits());
642        }
643    }
644
645    #[test]
646    fn negative_zero() {
647        let values = vec![-0.0f64, 0.0, -0.0];
648        let encoded = encode(&values);
649        let decoded = decode(&encoded).unwrap();
650        for (i, (a, b)) in values.iter().zip(decoded.iter()).enumerate() {
651            assert_eq!(a.to_bits(), b.to_bits(), "mismatch at {i}");
652        }
653    }
654
655    #[test]
656    fn rejects_noncanonical_exception_layout() {
657        let mut encoded = encode(&[f64::NAN, 1.0]);
658        let first_exception = 11;
659        encoded[first_exception..first_exception + 4].copy_from_slice(&2u32.to_le_bytes());
660        assert!(matches!(decode(&encoded), Err(CodecError::Corrupt { .. })));
661        let mut empty = encode(&[]);
662        empty.push(0);
663        assert!(matches!(decode(&empty), Err(CodecError::Corrupt { .. })));
664
665        let mut bad_exponent = encode(&[1.0]);
666        bad_exponent[4] = MAX_EXPONENT + 1;
667        assert!(matches!(
668            decode(&bad_exponent),
669            Err(CodecError::Corrupt { .. })
670        ));
671
672        let mut nonzero_placeholder = encode(&[f64::NAN]);
673        let nested_min = 11 + 12 + 6 + 3;
674        nonzero_placeholder[nested_min..nested_min + 8].copy_from_slice(&1i64.to_le_bytes());
675        assert!(matches!(
676            decode(&nonzero_placeholder),
677            Err(CodecError::Corrupt { .. })
678        ));
679    }
680
681    #[test]
682    fn truncated_input_errors() {
683        assert!(decode(&[]).is_err());
684        assert!(decode(&[1, 0, 0, 0, 0, 0, 0, 0, 0]).is_err()); // too short
685    }
686
687    #[test]
688    fn large_dataset() {
689        let mut values = Vec::with_capacity(100_000);
690        let mut rng: u64 = 12345;
691        for _ in 0..100_000 {
692            rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
693            let val = ((rng >> 33) as f64 / (u32::MAX as f64)) * 1000.0;
694            let val = (val * 100.0).round() / 100.0; // 2 decimal places
695            values.push(val);
696        }
697        let encoded = encode(&values);
698        let decoded = decode(&encoded).unwrap();
699        assert_eq!(decoded.len(), values.len());
700        for (i, (a, b)) in values.iter().zip(decoded.iter()).enumerate() {
701            assert_eq!(a.to_bits(), b.to_bits(), "mismatch at {i}");
702        }
703    }
704}