Skip to main content

nodedb_codec/
alp_rd.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! ALP-RD (Real Doubles) codec for true arbitrary f64 values.
4//!
5//! For full-precision doubles (scientific data, vector embeddings) where
6//! ALP can't find a lossless decimal mapping, ALP-RD exploits the structure
7//! of IEEE 754: the front bits (sign + exponent + high mantissa) are
8//! predictable (few unique patterns), while the tail bits (low mantissa)
9//! are noisy.
10//!
11//! Approach:
12//! 1. Right-shift each f64's bits by `cut` positions (typically 44-52 bits),
13//!    producing a "front" value with few unique values.
14//! 2. Dictionary-encode the front values (usually <256 unique patterns).
15//! 3. Store the tail bits raw (the bottom `cut` bits of each value).
16//!
17//! Compression: ~54 bits/f64 vs 64 raw (~15% reduction). Modest but
18//! consistent and lossless.
19//!
20//! Wire format:
21//! ```text
22//! [4 bytes] value count (LE u32)
23//! [1 byte]  cut position (number of tail bits, 0-63)
24//! [2 bytes] dictionary size (LE u16)
25//! [dict_size × 8 bytes] dictionary entries (LE u64 front values)
26//! [count × 1-2 bytes] dictionary indices (u8 if dict ≤ 256, u16 otherwise)
27//! [count × ceil(cut/8) bytes] tail bits (packed, little-endian)
28//! ```
29
30use std::mem::size_of;
31
32use crate::bounds::{
33    checked_add, checked_capacity, checked_mul, checked_range, decoded_len, encode_input_len,
34    u32_to_usize,
35};
36use crate::error::CodecError;
37
38use crate::CODEC_SAMPLE_SIZE;
39
40// ---------------------------------------------------------------------------
41// Public API
42// ---------------------------------------------------------------------------
43
44/// Encode f64 values using ALP-RD (front-bit dictionary + raw tail bits).
45pub fn encode(values: &[f64]) -> Result<Vec<u8>, CodecError> {
46    let decoded_bytes = checked_mul(values.len(), size_of::<f64>(), "ALP-RD input bytes")?;
47    decoded_len(decoded_bytes, "ALP-RD input")?;
48    let count = encode_input_len(values.len(), "ALP-RD value count")?;
49
50    if values.is_empty() {
51        let mut out = Vec::with_capacity(7);
52        out.extend_from_slice(&0u32.to_le_bytes());
53        out.push(0); // cut
54        out.extend_from_slice(&0u16.to_le_bytes());
55        return Ok(out);
56    }
57
58    // Find optimal cut position.
59    let cut = find_best_cut(values);
60    let bits: Vec<u64> = values.iter().map(|v| v.to_bits()).collect();
61
62    // Split into front and tail.
63    let front_mask: u64 = if cut == 64 { 0 } else { u64::MAX << cut };
64    let tail_mask: u64 = if cut == 0 { 0 } else { (1u64 << cut) - 1 };
65    let tail_bytes_per_value = (cut as usize).div_ceil(8);
66
67    let fronts: Vec<u64> = bits.iter().map(|&b| (b & front_mask) >> cut).collect();
68
69    // Build dictionary from unique front values.
70    let mut dict: Vec<u64> = fronts.clone();
71    dict.sort_unstable();
72    dict.dedup();
73    let dict_size = u16::try_from(dict.len()).map_err(|_| CodecError::ResourceLimit {
74        resource: "ALP-RD dictionary entries".into(),
75        requested: dict.len(),
76        limit: u16::MAX as usize,
77    })?;
78
79    // Map front values to dictionary indices.
80    // Safety: `dict` is built from `fronts` via sort+dedup, so every front value
81    // is guaranteed to exist in the dictionary. binary_search cannot fail here.
82    let indices: Vec<u16> = fronts
83        .iter()
84        .map(|f| {
85            dict.binary_search(f)
86                .map(|idx| idx as u16)
87                .map_err(|_| CodecError::Corrupt {
88                    detail: "ALP-RD front value missing from dictionary".into(),
89                })
90        })
91        .collect::<Result<_, _>>()?;
92
93    let use_u8_indices = dict.len() <= 256;
94
95    // Build output.
96    let dict_bytes = checked_mul(dict.len(), 8, "ALP-RD dictionary")?;
97    let index_bytes = checked_mul(
98        values.len(),
99        if use_u8_indices { 1 } else { 2 },
100        "ALP-RD indices",
101    )?;
102    let tail_bytes = checked_mul(values.len(), tail_bytes_per_value, "ALP-RD tails")?;
103    let capacity = checked_add(
104        checked_add(
105            checked_add(7, dict_bytes, "ALP-RD output")?,
106            index_bytes,
107            "ALP-RD output",
108        )?,
109        tail_bytes,
110        "ALP-RD output",
111    )?;
112    let mut out = Vec::with_capacity(capacity);
113
114    // Header.
115    out.extend_from_slice(&count.to_le_bytes());
116    out.push(cut);
117    out.extend_from_slice(&dict_size.to_le_bytes());
118
119    // Dictionary.
120    for &entry in &dict {
121        out.extend_from_slice(&entry.to_le_bytes());
122    }
123
124    // Indices.
125    if use_u8_indices {
126        for &idx in &indices {
127            out.push(idx as u8);
128        }
129    } else {
130        for &idx in &indices {
131            out.extend_from_slice(&idx.to_le_bytes());
132        }
133    }
134
135    // Tail bits (packed little-endian, tail_bytes_per_value bytes each).
136    for &b in &bits {
137        let tail = b & tail_mask;
138        for byte_idx in 0..tail_bytes_per_value {
139            out.push((tail >> (byte_idx * 8)) as u8);
140        }
141    }
142
143    Ok(out)
144}
145
146/// Decode ALP-RD compressed data back to f64 values.
147pub fn decode(data: &[u8]) -> Result<Vec<f64>, CodecError> {
148    if data.len() < 7 {
149        return Err(CodecError::Truncated {
150            expected: 7,
151            actual: data.len(),
152        });
153    }
154
155    let count = u32_to_usize(
156        u32::from_le_bytes([data[0], data[1], data[2], data[3]]),
157        "ALP-RD value count",
158    )?;
159    decoded_len(
160        checked_mul(count, size_of::<f64>(), "ALP-RD decoded values")?,
161        "ALP-RD",
162    )?;
163    let cut = data[4];
164    let dict_size = usize::from(u16::from_le_bytes([data[5], data[6]]));
165
166    if count == 0 {
167        if dict_size != 0 || data.len() != 7 {
168            return Err(CodecError::Corrupt {
169                detail: "non-canonical empty ALP-RD frame".into(),
170            });
171        }
172        return Ok(Vec::new());
173    }
174
175    if cut > 63 {
176        return Err(CodecError::Corrupt {
177            detail: format!("invalid ALP-RD cut position: {cut}"),
178        });
179    }
180
181    let tail_bytes_per_value = (cut as usize).div_ceil(8);
182    let tail_mask: u64 = if cut == 0 { 0 } else { (1u64 << cut) - 1 };
183    if dict_size == 0 || dict_size > count {
184        return Err(CodecError::Corrupt {
185            detail: "ALP-RD dictionary count is invalid".into(),
186        });
187    }
188    let use_u8_indices = dict_size <= 256;
189
190    // Read dictionary.
191    let mut pos = 7;
192    let dict_bytes = checked_mul(dict_size, 8, "ALP-RD dictionary")?;
193    checked_range(data, pos, dict_bytes, "ALP-RD dictionary")?;
194    let dict_capacity = checked_capacity(dict_size, size_of::<u64>(), "ALP-RD dictionary")?;
195    let mut dict = Vec::with_capacity(dict_capacity);
196    let mut previous = None;
197    for _ in 0..dict_size {
198        let entry = u64::from_le_bytes([
199            data[pos],
200            data[pos + 1],
201            data[pos + 2],
202            data[pos + 3],
203            data[pos + 4],
204            data[pos + 5],
205            data[pos + 6],
206            data[pos + 7],
207        ]);
208        if entry > (u64::MAX >> cut) || previous.is_some_and(|value| entry <= value) {
209            return Err(CodecError::Corrupt {
210                detail: "ALP-RD dictionary is non-canonical".into(),
211            });
212        }
213        previous = Some(entry);
214        dict.push(entry);
215        pos = checked_add(pos, 8, "ALP-RD dictionary cursor")?;
216    }
217
218    // Read indices.
219    let index_bytes = checked_mul(count, if use_u8_indices { 1 } else { 2 }, "ALP-RD indices")?;
220    checked_range(data, pos, index_bytes, "ALP-RD indices")?;
221    let index_capacity = checked_capacity(count, size_of::<usize>(), "ALP-RD indices")?;
222    let mut indices = Vec::with_capacity(index_capacity);
223    if use_u8_indices {
224        for i in 0..count {
225            indices.push(data[pos + i] as usize);
226        }
227        pos = checked_add(pos, count, "ALP-RD index cursor")?;
228    } else {
229        for i in 0..count {
230            let idx_pos = checked_add(
231                pos,
232                checked_mul(i, 2, "ALP-RD index offset")?,
233                "ALP-RD index position",
234            )?;
235            indices.push(u16::from_le_bytes([data[idx_pos], data[idx_pos + 1]]) as usize);
236        }
237        pos = checked_add(pos, index_bytes, "ALP-RD index cursor")?;
238    }
239
240    // Read tail bits.
241    let tail_total = checked_mul(count, tail_bytes_per_value, "ALP-RD tails")?;
242    let tails = checked_range(data, pos, tail_total, "ALP-RD tails")?;
243    if checked_add(pos, tail_total, "ALP-RD frame end")? != data.len() {
244        return Err(CodecError::Corrupt {
245            detail: "trailing bytes after ALP-RD frame".into(),
246        });
247    }
248
249    let value_capacity = checked_capacity(count, size_of::<f64>(), "ALP-RD values")?;
250    let mut values = Vec::with_capacity(value_capacity);
251    for (i, &idx) in indices.iter().enumerate() {
252        if idx >= dict.len() {
253            return Err(CodecError::Corrupt {
254                detail: format!("ALP-RD dict index {idx} out of range (max {})", dict.len()),
255            });
256        }
257
258        let front = dict[idx] << cut;
259        let mut tail = 0u64;
260        let tail_pos = checked_mul(i, tail_bytes_per_value, "ALP-RD tail offset")?;
261        for byte_idx in 0..tail_bytes_per_value {
262            tail |= (tails[tail_pos + byte_idx] as u64) << (byte_idx * 8);
263        }
264        let final_bits = usize::from(cut) % 8;
265        if final_bits != 0 {
266            let last = tails[tail_pos + tail_bytes_per_value - 1];
267            let padding_mask = !((1u8 << final_bits) - 1);
268            if last & padding_mask != 0 {
269                return Err(CodecError::Corrupt {
270                    detail: "non-zero ALP-RD tail padding".into(),
271                });
272            }
273        }
274        tail &= tail_mask;
275
276        values.push(f64::from_bits(front | tail));
277    }
278
279    Ok(values)
280}
281
282// ---------------------------------------------------------------------------
283// Cut position selection
284// ---------------------------------------------------------------------------
285
286/// Find the optimal cut position that minimizes the dictionary size.
287///
288/// Tries cut positions from 44 to 56 bits (typical for f64) and picks
289/// the one that produces the fewest unique front values.
290fn find_best_cut(values: &[f64]) -> u8 {
291    let sample_end = values.len().min(CODEC_SAMPLE_SIZE);
292    let sample = &values[..sample_end];
293    let bits: Vec<u64> = sample.iter().map(|v| v.to_bits()).collect();
294
295    let mut best_cut = 48u8;
296    let mut best_unique = usize::MAX;
297
298    // Try cut positions from 40 to 56 (covering typical f64 patterns).
299    for cut in 40..=56 {
300        let mut fronts: Vec<u64> = bits.iter().map(|&b| b >> cut).collect();
301        fronts.sort_unstable();
302        fronts.dedup();
303
304        if fronts.len() < best_unique {
305            best_unique = fronts.len();
306            best_cut = cut;
307        }
308    }
309
310    best_cut
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316
317    #[test]
318    fn empty_roundtrip() {
319        let encoded = encode(&[]).unwrap();
320        let decoded = decode(&encoded).unwrap();
321        assert!(decoded.is_empty());
322    }
323
324    #[test]
325    fn pi_multiples() {
326        let values: Vec<f64> = (1..1000).map(|i| std::f64::consts::PI * i as f64).collect();
327        let encoded = encode(&values).unwrap();
328        let decoded = decode(&encoded).unwrap();
329        for (i, (a, b)) in values.iter().zip(decoded.iter()).enumerate() {
330            assert_eq!(a.to_bits(), b.to_bits(), "mismatch at {i}");
331        }
332    }
333
334    #[test]
335    fn scientific_data() {
336        let values: Vec<f64> = (0..1000).map(|i| (i as f64 * 0.001).exp()).collect();
337        let encoded = encode(&values).unwrap();
338        let decoded = decode(&encoded).unwrap();
339        for (i, (a, b)) in values.iter().zip(decoded.iter()).enumerate() {
340            assert_eq!(a.to_bits(), b.to_bits(), "mismatch at {i}");
341        }
342    }
343
344    #[test]
345    fn compression_ratio() {
346        let values: Vec<f64> = (1..10_000)
347            .map(|i| std::f64::consts::E * i as f64 + (i as f64).sqrt())
348            .collect();
349        let encoded = encode(&values).unwrap();
350        let raw_size = values.len() * 8;
351        let ratio = raw_size as f64 / encoded.len() as f64;
352        // ALP-RD saves depend on front-bit dictionary size.
353        // For data with many unique exponents, savings may be minimal.
354        // The key test is lossless roundtrip, not compression ratio.
355        assert!(
356            ratio >= 0.95,
357            "ALP-RD should not expand >5%, got {ratio:.2}x"
358        );
359
360        let decoded = decode(&encoded).unwrap();
361        for (a, b) in values.iter().zip(decoded.iter()) {
362            assert_eq!(a.to_bits(), b.to_bits());
363        }
364    }
365
366    #[test]
367    fn special_values() {
368        let values = vec![
369            0.0,
370            -0.0,
371            f64::INFINITY,
372            f64::NEG_INFINITY,
373            f64::NAN,
374            f64::MIN,
375            f64::MAX,
376            f64::MIN_POSITIVE,
377        ];
378        let encoded = encode(&values).unwrap();
379        let decoded = decode(&encoded).unwrap();
380        for (i, (a, b)) in values.iter().zip(decoded.iter()).enumerate() {
381            assert_eq!(a.to_bits(), b.to_bits(), "mismatch at {i}");
382        }
383    }
384
385    #[test]
386    fn identical_values() {
387        let values = vec![42.0f64; 1000];
388        let encoded = encode(&values).unwrap();
389        let decoded = decode(&encoded).unwrap();
390        for (a, b) in values.iter().zip(decoded.iter()) {
391            assert_eq!(a.to_bits(), b.to_bits());
392        }
393        // Dictionary has 1 entry — should be smaller than raw.
394        assert!(encoded.len() < values.len() * 8);
395    }
396
397    #[test]
398    fn rejects_invalid_layout_and_trailing_bytes() {
399        assert!(decode(&[]).is_err());
400        assert!(decode(&[1, 0, 0, 0, 48, 0]).is_err());
401        let mut encoded = encode(&[1.25]).unwrap();
402        encoded.push(0);
403        assert!(matches!(decode(&encoded), Err(CodecError::Corrupt { .. })));
404        let mut invalid_index = encode(&[1.25]).unwrap();
405        let dict_size = usize::from(u16::from_le_bytes([invalid_index[5], invalid_index[6]]));
406        let index_start = 7 + dict_size * 8;
407        invalid_index[index_start] = u8::MAX;
408        assert!(matches!(
409            decode(&invalid_index),
410            Err(CodecError::Corrupt { .. })
411        ));
412
413        let mut padded_tail = Vec::new();
414        padded_tail.extend_from_slice(&1u32.to_le_bytes());
415        padded_tail.push(1);
416        padded_tail.extend_from_slice(&1u16.to_le_bytes());
417        padded_tail.extend_from_slice(&0u64.to_le_bytes());
418        padded_tail.push(0);
419        padded_tail.push(0x80);
420        assert!(matches!(
421            decode(&padded_tail),
422            Err(CodecError::Corrupt { .. })
423        ));
424
425        let mut wide_front = padded_tail;
426        wide_front[7..15].copy_from_slice(&u64::MAX.to_le_bytes());
427        wide_front[16] = 0;
428        assert!(matches!(
429            decode(&wide_front),
430            Err(CodecError::Corrupt { .. })
431        ));
432    }
433}