1use alloc::format;
13use alloc::vec::Vec;
14
15use pamoja_core::{Error, Result};
16
17const QUANTIZED_LIMIT: f32 = 9.2e18;
20
21fn write_uvarint(mut value: u64, out: &mut Vec<u8>) {
24 loop {
25 let mut byte = (value & 0x7f) as u8;
26 value >>= 7;
27 if value != 0 {
28 byte |= 0x80;
29 }
30 out.push(byte);
31 if value == 0 {
32 break;
33 }
34 }
35}
36
37fn read_uvarint(bytes: &[u8], pos: &mut usize) -> Result<u64> {
40 let mut result = 0u64;
41 let mut shift = 0u32;
42 loop {
43 let byte = *bytes
44 .get(*pos)
45 .ok_or_else(|| Error::Codec("the batch ends part-way through a value".into()))?;
46 *pos += 1;
47 if shift == 63 && byte > 1 {
48 return Err(Error::Codec(
49 "a value in the batch does not fit in 64 bits".into(),
50 ));
51 }
52 result |= u64::from(byte & 0x7f) << shift;
53 if byte & 0x80 == 0 {
54 return Ok(result);
55 }
56 shift += 7;
57 }
58}
59
60fn round(value: f32) -> f32 {
63 const WHOLE: f32 = 8_388_608.0;
64 if value.is_nan() || value <= -WHOLE || value >= WHOLE {
65 return value;
66 }
67 let truncated = value as i32 as f32;
68 let fraction = value - truncated;
69 if fraction >= 0.5 {
70 truncated + 1.0
71 } else if fraction <= -0.5 {
72 truncated - 1.0
73 } else {
74 truncated
75 }
76}
77
78fn zigzag(value: i64) -> u64 {
81 ((value << 1) ^ (value >> 63)) as u64
82}
83
84fn unzigzag(value: u64) -> i64 {
85 ((value >> 1) as i64) ^ -((value & 1) as i64)
86}
87
88pub fn encode_deltas(samples: &[i64]) -> Vec<u8> {
110 let mut out = Vec::new();
111 write_uvarint(samples.len() as u64, &mut out);
112 let mut previous = 0i64;
113 for &sample in samples {
114 let delta = sample.wrapping_sub(previous);
115 write_uvarint(zigzag(delta), &mut out);
116 previous = sample;
117 }
118 out
119}
120
121pub fn decode_deltas(bytes: &[u8]) -> Result<Vec<i64>> {
137 let mut pos = 0;
138 let count = read_uvarint(bytes, &mut pos)?;
139 let mut samples = Vec::new();
142 let mut previous = 0i64;
143 for _ in 0..count {
144 let delta = unzigzag(read_uvarint(bytes, &mut pos)?);
145 let sample = previous.wrapping_add(delta);
146 samples.push(sample);
147 previous = sample;
148 }
149 match bytes.len() - pos {
150 0 => Ok(samples),
151 extra => Err(Error::Codec(format!(
152 "{extra} bytes follow the batch's last sample"
153 ))),
154 }
155}
156
157#[derive(Clone, Copy, Debug)]
186pub struct Quantizer {
187 scale: f32,
188}
189
190impl Quantizer {
191 pub fn new(scale: f32) -> Self {
203 Self { scale }
204 }
205
206 pub fn encode(&self, readings: &[f32]) -> Result<Vec<u8>> {
222 self.check_scale()?;
223 let mut samples = Vec::with_capacity(readings.len());
224 for (index, &reading) in readings.iter().enumerate() {
225 if !reading.is_finite() {
226 return Err(Error::Codec(format!(
227 "reading {index} is {reading}, which cannot be quantized"
228 )));
229 }
230 let scaled = round(reading * self.scale);
231 if !scaled.is_finite() || scaled <= -QUANTIZED_LIMIT || scaled >= QUANTIZED_LIMIT {
232 return Err(Error::Codec(format!(
233 "reading {index} is {reading}, too large for a scale of {}",
234 self.scale
235 )));
236 }
237 samples.push(scaled as i64);
238 }
239 Ok(encode_deltas(&samples))
240 }
241
242 pub fn decode(&self, bytes: &[u8]) -> Result<Vec<f32>> {
258 self.check_scale()?;
259 let samples = decode_deltas(bytes)?;
260 Ok(samples
261 .iter()
262 .map(|&sample| sample as f32 / self.scale)
263 .collect())
264 }
265
266 fn check_scale(&self) -> Result<()> {
268 if self.scale.is_finite() && self.scale > 0.0 {
269 Ok(())
270 } else {
271 Err(Error::Codec(format!(
272 "a quantizer's scale must be a positive, finite number, not {}",
273 self.scale
274 )))
275 }
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282
283 #[test]
284 fn an_integer_batch_round_trips() {
285 for samples in [
286 vec![],
287 vec![0],
288 vec![42],
289 vec![-5, -4, -3, -2],
290 vec![1000, 1001, 1003, 1002, 999],
291 vec![i64::MIN, 0, i64::MAX],
292 ] {
293 let bytes = encode_deltas(&samples);
294 assert_eq!(decode_deltas(&bytes).expect("decode"), samples);
295 }
296 }
297
298 #[test]
299 fn a_slow_series_is_far_smaller_than_raw() {
300 let samples: Vec<i64> = (0..100).map(|i| 5000 + i).collect();
301 let bytes = encode_deltas(&samples);
302 assert!(bytes.len() < samples.len() * 2);
304 }
305
306 #[test]
307 fn truncated_bytes_are_a_codec_error() {
308 match decode_deltas(&[3]) {
310 Err(Error::Codec(reason)) => {
311 assert_eq!(reason, "the batch ends part-way through a value");
312 }
313 other => panic!("a batch cut short decoded: {other:?}"),
314 }
315 }
316
317 #[test]
318 fn a_value_past_64_bits_is_refused_rather_than_truncated() {
319 let mut largest = Vec::new();
320 write_uvarint(u64::MAX, &mut largest);
321 assert_eq!(largest.len(), 10);
322 assert_eq!(largest[9], 0x01);
323 let mut pos = 0;
324 assert_eq!(
325 read_uvarint(&largest, &mut pos).expect("u64::MAX"),
326 u64::MAX
327 );
328
329 for tenth in [0x02, 0x7f, 0x81] {
330 let mut bytes = largest.clone();
331 bytes[9] = tenth;
332 bytes.push(0x00);
333 let mut pos = 0;
334 match read_uvarint(&bytes, &mut pos) {
335 Err(Error::Codec(reason)) => {
336 assert_eq!(reason, "a value in the batch does not fit in 64 bits");
337 }
338 other => panic!("a tenth byte of {tenth:#04x} read as {other:?}"),
339 }
340 }
341 }
342
343 #[test]
344 fn rounding_matches_the_standard_library_at_its_edges() {
345 for value in [
346 0.0,
347 -0.0,
348 0.5,
349 -0.5,
350 1.5,
351 -1.5,
352 2.5,
353 -2.5,
354 0.499_999_97,
355 -0.499_999_97,
356 8_388_607.5,
357 -8_388_607.5,
358 8_388_608.0,
359 16_777_217.0,
360 f32::MAX,
361 f32::MIN,
362 f32::MIN_POSITIVE,
363 f32::INFINITY,
364 f32::NEG_INFINITY,
365 ] {
366 assert_eq!(round(value), value.round(), "rounding {value}");
367 }
368 assert!(round(f32::NAN).is_nan());
369 }
370
371 proptest::proptest! {
372 #[test]
373 fn rounding_matches_the_standard_library(value in proptest::num::f32::ANY) {
374 let ours = round(value);
375 let theirs = value.round();
376 proptest::prop_assert!(ours == theirs || (ours.is_nan() && theirs.is_nan()));
377 }
378 }
379
380 #[test]
381 fn varints_cover_the_boundaries() {
382 for value in [0u64, 1, 127, 128, 16_383, 16_384, u64::MAX] {
383 let mut out = Vec::new();
384 write_uvarint(value, &mut out);
385 let mut pos = 0;
386 assert_eq!(read_uvarint(&out, &mut pos).expect("read"), value);
387 assert_eq!(pos, out.len());
388 }
389 }
390
391 #[test]
392 fn varints_match_the_canonical_leb128_encodings() {
393 let cases: [(u64, &[u8]); 4] = [
394 (0, &[0x00]),
395 (1, &[0x01]),
396 (128, &[0x80, 0x01]),
397 (300, &[0xAC, 0x02]),
398 ];
399 for (value, encoded) in cases {
400 let mut out = Vec::new();
401 write_uvarint(value, &mut out);
402 assert_eq!(out, encoded, "varint of {value}");
403 }
404 }
405
406 #[test]
407 fn zigzag_matches_the_protobuf_mapping() {
408 let cases: [(i64, u64); 6] = [
409 (0, 0),
410 (-1, 1),
411 (1, 2),
412 (-2, 3),
413 (2, 4),
414 (2_147_483_647, 4_294_967_294),
415 ];
416 for (signed, unsigned) in cases {
417 assert_eq!(zigzag(signed), unsigned, "zigzag of {signed}");
418 assert_eq!(unzigzag(unsigned), signed, "unzigzag of {unsigned}");
419 }
420 }
421
422 #[test]
423 fn a_quantizer_round_trips_within_its_precision() {
424 let quantizer = Quantizer::new(100.0);
425 let readings = [4.0, 4.62, 5.13, 4.77, 3.98];
426 let packed = quantizer.encode(&readings).expect("encode");
427 let restored = quantizer.decode(&packed).expect("decode");
428 for (original, decoded) in readings.iter().zip(&restored) {
429 assert!((original - decoded).abs() <= 0.005 + f32::EPSILON);
430 }
431 }
432
433 #[test]
434 fn a_reading_that_is_not_a_number_is_refused_rather_than_packed_as_zero() {
435 let quantizer = Quantizer::new(100.0);
436 for (readings, index) in [
437 (vec![20.0, f32::NAN], 1),
438 (vec![f32::INFINITY], 0),
439 (vec![20.0, 20.1, f32::NEG_INFINITY], 2),
440 ] {
441 match quantizer.encode(&readings) {
442 Err(Error::Codec(reason)) => {
443 assert!(
444 reason.starts_with(&format!("reading {index} is")),
445 "{reason}"
446 );
447 }
448 other => panic!("{readings:?} was packed: {other:?}"),
449 }
450 }
451 }
452
453 #[test]
454 fn a_reading_too_large_for_its_scale_is_refused() {
455 let quantizer = Quantizer::new(1e10);
456 match quantizer.encode(&[1e10]) {
457 Err(Error::Codec(reason)) => assert!(reason.contains("too large"), "{reason}"),
458 other => panic!("an overflowing reading was packed: {other:?}"),
459 }
460 assert!(Quantizer::new(1e10).encode(&[1.0]).is_ok());
461 }
462
463 #[test]
464 fn a_scale_that_is_not_a_positive_number_is_refused() {
465 for scale in [0.0, -100.0, f32::NAN, f32::INFINITY] {
466 let quantizer = Quantizer::new(scale);
467 assert!(matches!(quantizer.encode(&[1.0]), Err(Error::Codec(_))));
468 assert!(matches!(quantizer.decode(&[0]), Err(Error::Codec(_))));
469 }
470 }
471
472 #[test]
473 fn bytes_after_the_last_sample_are_refused() {
474 let mut bytes = encode_deltas(&[1, 2, 3]);
475 bytes.extend_from_slice(&encode_deltas(&[4]));
476 match decode_deltas(&bytes) {
477 Err(Error::Codec(reason)) => {
478 assert_eq!(reason, "2 bytes follow the batch's last sample");
479 }
480 other => panic!("two batches run together decoded: {other:?}"),
481 }
482 }
483}