1use crate::coding::{bits_needed, checked_count, checked_decode_count, pack_bits, unpack_bits};
61use crate::error::{EncodingError, Result};
62
63const HEADER_SIZE: usize = 19;
66
67const EXCEPTION_SIZE: usize = 12;
69
70const MAX_EXPONENT: u8 = 18;
72
73const F10: [f64; 19] = [
76 1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14, 1e15, 1e16,
77 1e17, 1e18,
78];
79
80const IF10: [f64; 19] = [
86 1e-0, 1e-1, 1e-2, 1e-3, 1e-4, 1e-5, 1e-6, 1e-7, 1e-8, 1e-9, 1e-10, 1e-11, 1e-12, 1e-13, 1e-14,
87 1e-15, 1e-16, 1e-17, 1e-18,
88];
89
90const EXPONENT_SAMPLE: usize = 64;
92
93const EXCEPTION_BITS: usize = 96;
96
97#[derive(Debug, Clone, Copy)]
99pub struct AlpEncoder;
100
101#[derive(Debug, Clone, Copy)]
103pub struct AlpDecoder;
104
105#[inline]
111#[must_use]
112pub fn decode_value(encoded: i64, e: u8, f: u8) -> f64 {
113 encoded as f64 * F10[f as usize] * IF10[e as usize]
114}
115
116#[inline]
121fn encode_value(v: f64, e: u8, f: u8) -> Option<i64> {
122 let scaled = v * F10[e as usize] * IF10[f as usize];
123 if !scaled.is_finite() {
124 return None;
125 }
126 let rounded = scaled.round_ties_even();
127 if !(rounded >= -(2f64.powi(62)) && rounded <= 2f64.powi(62)) {
130 return None;
131 }
132 let encoded = rounded as i64;
133 (decode_value(encoded, e, f).to_bits() == v.to_bits()).then_some(encoded)
136}
137
138fn choose_exponents(values: &[f64]) -> (u8, u8) {
147 let step = (values.len() / EXPONENT_SAMPLE).max(1);
148 let sample: Vec<f64> = values.iter().copied().step_by(step).collect();
149 let n = sample.len();
150
151 let mut best = (0u8, 0u8);
152 let mut best_bits = usize::MAX;
153
154 for e in 0..=MAX_EXPONENT {
155 for f in 0..=e {
156 let mut min = i64::MAX;
157 let mut max = i64::MIN;
158 let mut ok = 0usize;
159 for &v in &sample {
160 if let Some(i) = encode_value(v, e, f) {
161 min = min.min(i);
162 max = max.max(i);
163 ok += 1;
164 }
165 }
166 if ok == 0 {
167 continue;
168 }
169 let width = usize::from(bits_needed(max.wrapping_sub(min) as u64));
170 let bits = n * width + (n - ok) * EXCEPTION_BITS;
171 if bits < best_bits {
175 best_bits = bits;
176 best = (e, f);
177 }
178 }
179 }
180
181 best
182}
183
184impl AlpEncoder {
185 pub fn encode(values: &[f64]) -> Result<Vec<u8>> {
192 if values.is_empty() {
193 return Err(EncodingError::EmptyInput {
194 context: "ALP encoder",
195 });
196 }
197 let count = checked_count(values.len())?;
198 let (e, f) = choose_exponents(values);
199
200 let mut encoded: Vec<i64> = Vec::with_capacity(values.len());
201 let mut exceptions: Vec<(u32, f64)> = Vec::new();
202 let mut filler: Option<i64> = None;
203
204 for (idx, &v) in values.iter().enumerate() {
205 match encode_value(v, e, f) {
206 Some(i) => {
207 filler.get_or_insert(i);
208 encoded.push(i);
209 }
210 None => {
211 exceptions.push(u32::try_from(idx).map_or((u32::MAX, v), |i| (i, v)));
216 encoded.push(i64::MIN); }
218 }
219 }
220
221 let filler = filler.unwrap_or(0);
222 for (idx, _) in &exceptions {
223 if let Some(slot) = encoded.get_mut(*idx as usize) {
224 *slot = filler;
225 }
226 }
227
228 let reference = encoded.iter().copied().min().unwrap_or(0);
229 let offsets: Vec<u64> = encoded
230 .iter()
231 .map(|&v| v.wrapping_sub(reference) as u64)
232 .collect();
233 let bit_width = bits_needed(offsets.iter().copied().max().unwrap_or(0));
234
235 let data_bytes = (offsets.len() as u64 * u64::from(bit_width)).div_ceil(8) as usize;
236 let mut buf =
237 Vec::with_capacity(HEADER_SIZE + data_bytes + exceptions.len() * EXCEPTION_SIZE);
238 buf.extend_from_slice(&count.to_le_bytes());
239 buf.push(e);
240 buf.push(f);
241 buf.push(bit_width);
242 buf.extend_from_slice(&reference.to_le_bytes());
243 buf.extend_from_slice(&checked_count(exceptions.len())?.to_le_bytes());
244
245 pack_bits(&offsets, bit_width, &mut buf);
246
247 for (idx, value) in &exceptions {
248 buf.extend_from_slice(&idx.to_le_bytes());
249 buf.extend_from_slice(&value.to_bits().to_le_bytes());
250 }
251
252 Ok(buf)
253 }
254}
255
256impl AlpDecoder {
257 pub fn decode(data: &[u8]) -> Result<Vec<f64>> {
264 if data.len() < HEADER_SIZE {
265 return Err(EncodingError::CorruptData {
266 detail: format!(
267 "ALP block is {} bytes, need at least {HEADER_SIZE}",
268 data.len()
269 ),
270 });
271 }
272
273 let count = checked_decode_count(
274 u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize,
275 "ALP",
276 )?;
277 let e = data[4];
278 let f = data[5];
279 let bit_width = data[6];
280 let reference = i64::from_le_bytes([
281 data[7], data[8], data[9], data[10], data[11], data[12], data[13], data[14],
282 ]);
283 let exception_count = checked_decode_count(
284 u32::from_le_bytes([data[15], data[16], data[17], data[18]]) as usize,
285 "ALP exception",
286 )?;
287
288 if e > MAX_EXPONENT || f > e {
289 return Err(EncodingError::CorruptData {
290 detail: format!("ALP exponents out of range: e={e}, f={f}"),
291 });
292 }
293
294 let data_bytes = (count as u64 * u64::from(bit_width)).div_ceil(8) as usize;
295 let body = &data[HEADER_SIZE..];
296 if body.len() < data_bytes + exception_count * EXCEPTION_SIZE {
297 return Err(EncodingError::CorruptData {
298 detail: "ALP block is truncated".to_string(),
299 });
300 }
301
302 let offsets = unpack_bits(&body[..data_bytes], count, bit_width)?;
303 let mut values: Vec<f64> = offsets
304 .into_iter()
305 .map(|o| decode_value(reference.wrapping_add(o as i64), e, f))
306 .collect();
307
308 let mut pos = data_bytes;
309 for _ in 0..exception_count {
310 let idx = u32::from_le_bytes([body[pos], body[pos + 1], body[pos + 2], body[pos + 3]])
311 as usize;
312 let bits = u64::from_le_bytes([
313 body[pos + 4],
314 body[pos + 5],
315 body[pos + 6],
316 body[pos + 7],
317 body[pos + 8],
318 body[pos + 9],
319 body[pos + 10],
320 body[pos + 11],
321 ]);
322 let slot = values
323 .get_mut(idx)
324 .ok_or_else(|| EncodingError::CorruptData {
325 detail: format!(
326 "ALP exception position {idx} is outside a {count}-value block"
327 ),
328 })?;
329 *slot = f64::from_bits(bits);
330 pos += EXCEPTION_SIZE;
331 }
332
333 Ok(values)
334 }
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340
341 fn roundtrip(values: &[f64]) -> Vec<f64> {
342 let encoded = AlpEncoder::encode(values).expect("encode");
343 AlpDecoder::decode(&encoded).expect("decode")
344 }
345
346 fn assert_bitwise(values: &[f64]) {
347 let decoded = roundtrip(values);
348 assert_eq!(decoded.len(), values.len());
349 for (i, (a, b)) in values.iter().zip(&decoded).enumerate() {
350 assert_eq!(
351 a.to_bits(),
352 b.to_bits(),
353 "value {i} changed: {a} ({:#x}) -> {b} ({:#x})",
354 a.to_bits(),
355 b.to_bits()
356 );
357 }
358 }
359
360 #[test]
361 fn two_decimal_sensor_readings_round_trip() {
362 let values: Vec<f64> = (0..1024)
363 .map(|i| 200.0 + f64::from(i % 500) / 100.0)
364 .collect();
365 assert_bitwise(&values);
366 }
367
368 #[test]
371 fn decimal_data_compresses_far_better_than_xor_codecs() {
372 let values: Vec<f64> = (0..4096)
373 .map(|i| 230.0 + f64::from(i % 2000) / 100.0)
374 .collect();
375 let raw = values.len() * 8;
376 let alp = AlpEncoder::encode(&values).unwrap().len();
377 let chimp = crate::chimp::ChimpEncoder::encode(&values).unwrap().len();
378 let gorilla = crate::gorilla::GorillaEncoder::encode(&values)
379 .unwrap()
380 .len();
381 let patas = crate::patas::PatasEncoder::encode(&values).unwrap().len();
382
383 assert!(
384 alp * 4 < raw,
385 "ALP should compress decimal data at least 4x: {alp} vs {raw}"
386 );
387 assert!(
388 alp < chimp && alp < gorilla && alp < patas,
389 "ALP {alp} did not beat chimp {chimp} / gorilla {gorilla} / patas {patas}"
390 );
391 }
392
393 #[test]
394 fn integers_round_trip() {
395 let values: Vec<f64> = (0..500).map(f64::from).collect();
396 assert_bitwise(&values);
397 }
398
399 #[test]
400 fn constant_column_round_trips() {
401 assert_bitwise(&[42.5; 300]);
402 }
403
404 #[test]
405 fn single_value() {
406 assert_bitwise(&[1.5]);
407 }
408
409 #[test]
410 fn special_values_become_exceptions_and_survive() {
411 let values = vec![
412 0.0,
413 -0.0,
414 f64::NAN,
415 f64::INFINITY,
416 f64::NEG_INFINITY,
417 f64::MIN,
418 f64::MAX,
419 f64::MIN_POSITIVE,
420 1.5,
421 -2.25,
422 ];
423 let decoded = roundtrip(&values);
424 for (i, (a, b)) in values.iter().zip(&decoded).enumerate() {
425 assert_eq!(a.to_bits(), b.to_bits(), "special value {i} changed");
426 }
427 }
428
429 #[test]
433 fn non_decimal_doubles_still_round_trip() {
434 let values: Vec<f64> = (0..256)
435 .map(|i| f64::from(i).mul_add(0.000_000_000_123_456_789, std::f64::consts::PI))
436 .collect();
437 assert_bitwise(&values);
438 }
439
440 #[test]
441 fn negative_and_mixed_signs() {
442 let values: Vec<f64> = (0..1000).map(|i| (f64::from(i) - 500.0) / 8.0).collect();
443 assert_bitwise(&values);
444 }
445
446 #[test]
447 fn empty_input_is_rejected() {
448 assert!(AlpEncoder::encode(&[]).is_err());
449 }
450
451 #[test]
452 fn truncated_block_is_rejected() {
453 let encoded = AlpEncoder::encode(&[1.0, 2.0, 3.0]).unwrap();
454 assert!(AlpDecoder::decode(&encoded[..HEADER_SIZE - 1]).is_err());
455 assert!(AlpDecoder::decode(&encoded[..encoded.len() - 1]).is_err());
456 }
457
458 #[test]
459 fn corrupt_exponents_are_rejected() {
460 let mut encoded = AlpEncoder::encode(&[1.0, 2.0, 3.0]).unwrap();
461 encoded[4] = 99; assert!(AlpDecoder::decode(&encoded).is_err());
463 }
464
465 #[test]
469 fn reciprocal_table_matches_division() {
470 for i in 0..=MAX_EXPONENT as usize {
471 assert_eq!(
472 IF10[i].to_bits(),
473 (1.0 / F10[i]).to_bits(),
474 "IF10[{i}] does not match 1/F10[{i}]"
475 );
476 }
477 }
478}
479
480#[cfg(test)]
481mod proptests {
482 use super::*;
483 use proptest::prelude::*;
484
485 proptest! {
486 #[test]
490 fn roundtrip_arbitrary_f64(values in prop::collection::vec(any::<f64>(), 1..300)) {
491 let encoded = AlpEncoder::encode(&values).unwrap();
492 let decoded = AlpDecoder::decode(&encoded).unwrap();
493 prop_assert_eq!(decoded.len(), values.len());
494 for (a, b) in values.iter().zip(&decoded) {
495 prop_assert_eq!(a.to_bits(), b.to_bits());
496 }
497 }
498
499 #[test]
503 fn roundtrip_decimals(
504 scaled in prop::collection::vec(-1_000_000i64..1_000_000, 1..300),
505 digits in 0u32..6,
506 ) {
507 let scale = 10f64.powi(digits as i32);
508 let values: Vec<f64> = scaled.iter().map(|&v| v as f64 / scale).collect();
509 let encoded = AlpEncoder::encode(&values).unwrap();
510 let decoded = AlpDecoder::decode(&encoded).unwrap();
511 for (a, b) in values.iter().zip(&decoded) {
512 prop_assert_eq!(a.to_bits(), b.to_bits());
513 }
514 }
515
516 #[test]
518 fn decode_never_panics(bytes in prop::collection::vec(any::<u8>(), 0..512)) {
519 let _ = AlpDecoder::decode(&bytes);
520 }
521 }
522}