Skip to main content

cram_codec/
lorenzo.rs

1//! Lorenzo-predictor codec for pointwise (max-abs) bounds, and for RMSE /
2//! PSNR targets through rate control of the bound.
3//!
4//! Aimed at rough fields with one smooth (or repeating) axis, such as
5//! particle trajectories stored time × particle, where interpolating across
6//! the rough axis costs more than it predicts.
7//!
8//! 1. **Prediction**, in row-major order on f32 reconstructions. Mode 0 is
9//!    the full N-D Lorenzo predictor (inclusion–exclusion over the axes whose
10//!    coordinate is > 0); mode `k + 1` is the previous point along axis `k`
11//!    (the N-D predictor where that coordinate is 0). The mode is picked by
12//!    real stream size.
13//! 2. **Quantization**, `q = round((x − x̃)/2b)`, checked on the exact f32
14//!    reconstruction; failures are escaped raw (as in [`crate::interp`]).
15//! 3. **Coding.** Adaptive binary range coding: a zero flag with contexts
16//!    (symbol class of the previous point along the prediction axis and of
17//!    the side neighbour, `log₂` of the last step along the prediction axis
18//!    in quanta), a sign bit keyed on the previous point's sign, and an
19//!    adaptive Elias-gamma magnitude keyed on the two neighbour classes.
20
21use crate::abac::{BitModel, Decoder, Encoder, UintModel};
22use crate::bitio::{ByteReader, ByteWriter, DecodeError};
23use crate::codec::Decoded;
24use crate::interp::{quant, rate_control, ESC};
25
26/// Stream magic of the Lorenzo codec.
27pub const MAGIC: &[u8; 4] = b"TCL1";
28
29const ACT: usize = 9;
30/// Neighbour symbol classes: 0 zero, 1 `|q| = 1`, 2 larger or escaped, 3 none.
31const CLS: usize = 4;
32
33struct Models {
34    zero: Vec<BitModel>,
35    sign: Vec<BitModel>,
36    mag: Vec<UintModel>,
37}
38
39impl Models {
40    fn new() -> Self {
41        Self {
42            zero: vec![BitModel::default(); CLS * CLS * ACT],
43            sign: vec![BitModel::default(); 3],
44            mag: vec![UintModel::default(); CLS * CLS],
45        }
46    }
47}
48
49/// Coding context of one point.
50#[derive(Clone, Copy)]
51struct Ctx {
52    up: usize,
53    side: usize,
54    act: usize,
55    /// 0 none or zero, 1 positive, 2 negative (previous point along the axis).
56    sgn: usize,
57}
58
59/// Per-point symbol record: class in bits 0–1, negative sign in bit 2.
60fn record(q: i64, esc: bool) -> u8 {
61    let c = if esc || q.unsigned_abs() > 1 {
62        2
63    } else {
64        q.unsigned_abs() as u8
65    };
66    c | ((q < 0) as u8) << 2
67}
68
69fn strides(dims: &[usize]) -> Vec<usize> {
70    let mut st = vec![1usize; dims.len()];
71    for j in (0..dims.len().saturating_sub(1)).rev() {
72        st[j] = st[j + 1] * dims[j + 1];
73    }
74    st
75}
76
77/// Number of prediction modes for `nd` axes (1D has only the previous point).
78fn modes(nd: usize) -> u8 {
79    if nd == 1 {
80        1
81    } else {
82        nd as u8 + 1
83    }
84}
85
86/// Visit every point in row-major order with `f(index, prediction, ctx)`,
87/// which returns `(reconstruction, record)`.
88fn walk(
89    dims: &[usize],
90    mode: u8,
91    b: f64,
92    r: &mut [f32],
93    rec: &mut [u8],
94    mut f: impl FnMut(usize, f64, Ctx) -> (f32, u8),
95) {
96    let nd = dims.len();
97    let st = strides(dims);
98    // prediction axis (mode 0: the slowest), side axis: the fastest other one
99    let k = if mode == 0 { 0 } else { mode as usize - 1 };
100    let side = if k == nd - 1 {
101        nd.checked_sub(2)
102    } else {
103        Some(nd - 1)
104    };
105    let step = 2.0 * b;
106    let n: usize = dims.iter().product();
107    let mut c = vec![0usize; nd];
108    for i in 0..n {
109        let along = c[k] > 0;
110        let pred = if along && mode != 0 {
111            r[i - st[k]] as f64
112        } else {
113            // N-D Lorenzo over the axes with coordinate > 0
114            let live: Vec<usize> = (0..nd).filter(|&j| c[j] > 0).map(|j| st[j]).collect();
115            let mut p = 0f64;
116            for m in 1u32..(1 << live.len()) {
117                let off: usize = (0..live.len())
118                    .filter(|&t| m >> t & 1 == 1)
119                    .map(|t| live[t])
120                    .sum();
121                let v = r[i - off] as f64;
122                p += if m.count_ones() % 2 == 1 { v } else { -v };
123            }
124            p
125        };
126        let (up, sgn, act) = if along {
127            let u = rec[i - st[k]];
128            let act = if c[k] > 1 {
129                let d = (r[i - st[k]] as f64 - r[i - 2 * st[k]] as f64).abs() / step;
130                let u = d as u64;
131                ((u64::BITS - u.leading_zeros()) as usize).min(ACT - 2)
132            } else {
133                ACT - 1
134            };
135            let sgn = if u & 3 == 0 { 0 } else { 1 + (u >> 2) as usize };
136            ((u & 3) as usize, sgn, act)
137        } else {
138            (CLS - 1, 0, ACT - 1)
139        };
140        let sd = match side {
141            Some(s) if c[s] > 0 => (rec[i - st[s]] & 3) as usize,
142            _ => CLS - 1,
143        };
144        let (v, q) = f(
145            i,
146            pred,
147            Ctx {
148                up,
149                side: sd,
150                act,
151                sgn,
152            },
153        );
154        r[i] = v;
155        rec[i] = q;
156        for j in (0..nd).rev() {
157            c[j] += 1;
158            if c[j] < dims[j] {
159                break;
160            }
161            c[j] = 0;
162        }
163    }
164}
165
166fn put(e: &mut Encoder, m: &mut Models, cx: Ctx, q: i64, raw: Option<f32>) {
167    let z = (cx.up * CLS + cx.side) * ACT + cx.act;
168    if q == 0 && raw.is_none() {
169        e.encode(false, &mut m.zero[z]);
170        return;
171    }
172    e.encode(true, &mut m.zero[z]);
173    e.encode(q < 0, &mut m.sign[cx.sgn]);
174    let mc = cx.up * CLS + cx.side;
175    match raw {
176        Some(v) => {
177            m.mag[mc].encode(e, ESC - 1);
178            e.encode_raw(v.to_bits() as u64, 32);
179        }
180        None => m.mag[mc].encode(e, q.unsigned_abs() - 1),
181    }
182}
183
184/// `Ok(q)` or `Err(raw value)`.
185fn get(d: &mut Decoder, m: &mut Models, cx: Ctx) -> Result<i64, f32> {
186    let z = (cx.up * CLS + cx.side) * ACT + cx.act;
187    if !d.decode(&mut m.zero[z]) {
188        return Ok(0);
189    }
190    let neg = d.decode(&mut m.sign[cx.sgn]);
191    let a = m.mag[cx.up * CLS + cx.side].decode(d) + 1;
192    if a >= ESC {
193        return Err(f32::from_bits(d.decode_raw(32) as u32));
194    }
195    Ok(if neg { -(a as i64) } else { a as i64 })
196}
197
198/// One stream at bound `b` in prediction `mode`, with its squared error.
199fn encode_sse(x: &[f32], dims: &[usize], b: f64, mode: u8) -> (Vec<u8>, f64) {
200    let n = x.len();
201    let mut r = vec![0f32; n];
202    let mut rc = vec![0u8; n];
203    let mut m = Models::new();
204    let mut e = Encoder::new();
205    let step = 2.0 * b;
206    walk(dims, mode, b, &mut r, &mut rc, |i, p, cx| {
207        let (q, v, esc) = quant(x[i], p, step, b);
208        put(&mut e, &mut m, cx, q, esc.then_some(x[i]));
209        (v, record(q, esc))
210    });
211    let mut w = ByteWriter::new();
212    w.bytes(MAGIC);
213    w.u8(dims.len() as u8);
214    for &d in dims {
215        w.varint(d as u64);
216    }
217    w.f64(b);
218    w.u8(mode);
219    w.bytes(&e.finish());
220    let sse = x
221        .iter()
222        .zip(&r)
223        .map(|(&a, &b)| (a as f64 - b as f64).powi(2))
224        .sum();
225    (w.buf, sse)
226}
227
228fn check(x: &[f32], dims: &[usize]) {
229    assert_eq!(
230        x.len(),
231        dims.iter().product::<usize>(),
232        "dims do not match data"
233    );
234    assert!(!dims.is_empty() && dims.len() <= 8);
235}
236
237/// The prediction mode with the smallest stream at bound `b`, and that stream.
238fn best_mode(x: &[f32], dims: &[usize], b: f64) -> (u8, Vec<u8>) {
239    (0..modes(dims.len()))
240        .map(|md| (md, encode_sse(x, dims, b, md).0))
241        .min_by_key(|(_, s)| s.len())
242        .unwrap()
243}
244
245/// Encode `x` (row-major, shape `dims`) with `|x − x̂| ≤ b` everywhere,
246/// trying each prediction mode and keeping the smallest stream.
247pub fn encode_f32(x: &[f32], dims: &[usize], b: f64) -> Vec<u8> {
248    check(x, dims);
249    assert!(b > 0.0);
250    best_mode(x, dims, b).1
251}
252
253/// Encode for an RMSE target (`rmse` absolute): the mode is picked at the
254/// bound `rmse·√3`, then that mode's bound is rate controlled as in
255/// [`crate::interp::encode_rmse_f32`]. The target is checked on the exact
256/// reconstruction.
257pub fn encode_rmse_f32(x: &[f32], dims: &[usize], rmse: f64) -> Vec<u8> {
258    check(x, dims);
259    assert!(rmse > 0.0);
260    let (mode, _) = best_mode(x, dims, rmse * 3f64.sqrt());
261    rate_control(x.len(), rmse, |b| encode_sse(x, dims, b, mode))
262}
263
264/// [`encode_rmse_f32`] at `PSNR = 20 log₁₀(range / RMSE)` dB.
265pub fn encode_psnr_f32(x: &[f32], dims: &[usize], psnr: f64) -> Vec<u8> {
266    let (lo, hi) = x
267        .iter()
268        .fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), &v| {
269            (a.min(v as f64), b.max(v as f64))
270        });
271    let range = if hi > lo { hi - lo } else { 1.0 };
272    encode_rmse_f32(x, dims, range / 10f64.powf(psnr / 20.0))
273}
274
275/// Decode a Lorenzo stream.
276pub fn decode(bytes: &[u8]) -> Result<Decoded, DecodeError> {
277    let mut rd = ByteReader::new(bytes);
278    if rd.take(4)? != MAGIC {
279        return Err(DecodeError::Invalid("magic"));
280    }
281    let nd = rd.u8()? as usize;
282    let dims = (0..nd)
283        .map(|_| rd.varint().map(|v| v as usize))
284        .collect::<Result<Vec<_>, _>>()?;
285    if nd == 0
286        || nd > 8
287        || dims.contains(&0)
288        || dims
289            .iter()
290            .try_fold(1usize, |a, &d| a.checked_mul(d))
291            .is_none_or(|n| n as u64 > 1 << 34)
292    {
293        return Err(DecodeError::Invalid("dims"));
294    }
295    let b = rd.f64()?;
296    let mode = rd.u8()?;
297    if b.is_nan() || b <= 0.0 {
298        return Err(DecodeError::Invalid("bound"));
299    }
300    if mode >= modes(nd) {
301        return Err(DecodeError::Invalid("mode"));
302    }
303    let mut d = Decoder::new(&bytes[rd.position()..]);
304    let mut m = Models::new();
305    let n: usize = dims.iter().product();
306    let mut r = vec![0f32; n];
307    let mut rc = vec![0u8; n];
308    let step = 2.0 * b;
309    walk(&dims, mode, b, &mut r, &mut rc, |_, p, cx| {
310        match get(&mut d, &mut m, cx) {
311            Ok(q) => ((p + q as f64 * step) as f32, record(q, false)),
312            Err(v) => (v, record(0, true)),
313        }
314    });
315    if d.overrun() {
316        return Err(DecodeError::Invalid("truncated"));
317    }
318    Ok(Decoded::F32 { dims, data: r })
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324
325    fn field(d: &[usize]) -> Vec<f32> {
326        let n: usize = d.iter().product();
327        (0..n)
328            .map(|i| {
329                let t = i as f32 / n as f32;
330                (t * 40.0).sin() * 3.0
331                    + ((i * 7919) % 13) as f32 * 0.4
332                    + if i % 997 == 0 { 50.0 } else { 0.0 }
333            })
334            .collect()
335    }
336
337    #[test]
338    fn round_trips_within_bound_in_every_mode() {
339        for d in [
340            vec![1000],
341            vec![33, 70],
342            vec![17, 9, 31],
343            vec![1, 1, 5],
344            vec![1],
345        ] {
346            let x = field(&d);
347            for b in [1e-1, 1e-3, 1e-6] {
348                for md in 0..modes(d.len()) {
349                    let (bytes, sse) = encode_sse(&x, &d, b, md);
350                    let Decoded::F32 { data, dims } = decode(&bytes).unwrap() else {
351                        panic!()
352                    };
353                    assert_eq!(dims, d);
354                    let mut got = 0f64;
355                    for (a, c) in x.iter().zip(&data) {
356                        assert!(((a - c) as f64).abs() <= b, "{d:?} b={b} m={md} {a} {c}");
357                        got += (*a as f64 - *c as f64).powi(2);
358                    }
359                    assert_eq!(got, sse);
360                }
361            }
362        }
363    }
364
365    #[test]
366    fn psnr_targets_are_met() {
367        let d = [64, 50];
368        let x = field(&d);
369        let (lo, hi) = x
370            .iter()
371            .fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), &v| {
372                (a.min(v as f64), b.max(v as f64))
373            });
374        for p in [40.0, 70.0, 100.0] {
375            let s = encode_psnr_f32(&x, &d, p);
376            let Decoded::F32 { data: y, .. } = decode(&s).unwrap() else {
377                panic!()
378            };
379            let mse = x
380                .iter()
381                .zip(&y)
382                .map(|(&a, &b)| (a as f64 - b as f64).powi(2))
383                .sum::<f64>()
384                / x.len() as f64;
385            assert!(20.0 * ((hi - lo) / mse.sqrt()).log10() >= p);
386        }
387    }
388
389    #[test]
390    fn picks_the_repeating_axis() {
391        // each particle holds a value for 8 steps (axis 0, staggered per
392        // particle); random across particles
393        let d = [64, 64];
394        let x: Vec<f32> = (0..64 * 64)
395            .map(|i| {
396                let (t, p) = (i / 64, i % 64);
397                ((p * 2654435761usize) % 1000) as f32 * 0.01
398                    + (((p + 64 * ((t + p) / 8)) * 40503) % 97) as f32 * 0.05
399            })
400            .collect();
401        let (mode, _) = best_mode(&x, &d, 1e-3);
402        assert_eq!(mode, 1);
403    }
404}