Skip to main content

cram_codec/
interp.rs

1//! SZ3-style multilevel interpolation codec for pointwise (max-abs) bounds,
2//! and for RMSE / PSNR targets through rate control of the bound
3//! ([`encode_rmse_f32`]).
4//!
5//! Aimed at loose bounds on smooth fields, where the wavelet codec's fixed
6//! overheads (centroids, band predictors, outlier map) dominate a tiny
7//! stream.
8//!
9//! 1. **Prediction.** The grid is refined coarse to fine: at stride `s` (a
10//!    power of two, largest first) and for each axis `k` in turn, the points
11//!    whose `k` coordinate is an odd multiple of `s` are predicted from
12//!    already reconstructed points at `±s, ±3s` along `k` — cubic
13//!    `(−1, 9, 9, −1)/16` inside, one-sided quadratics near an edge, linear
14//!    or copy at the boundary. Each level picks linear or cubic by a trial
15//!    cost (one header bit per level).
16//! 2. **Quantization.** `q = round((x − x̃)/2b_ℓ)` with a level bound
17//!    `b_ℓ = b / min(α^(ℓ−1), β) ≤ b` (finer prediction anchors, as in QoZ);
18//!    the reconstruction `x̃ + 2b_ℓq` is cast to f32 and checked against `b`
19//!    exactly as the decoder will compute it, and any point that fails (or
20//!    overflows the code range) is escaped and stored raw. `(α, β)` is picked
21//!    from a small preset list by actual stream size.
22//! 3. **Coding.** Adaptive binary range coding ([`crate::abac`]): a zero flag
23//!    with contexts (level, `log₂` of the local neighbour difference in
24//!    quanta, previous symbol class), a sign bit per level and an adaptive
25//!    Elias-gamma magnitude.
26//!
27//! Prediction is f64 arithmetic on f32 reconstructions in a fixed order, so
28//! encoder and decoder agree bit for bit.
29
30use crate::abac::{BitModel, Decoder, Encoder, UintModel};
31use crate::bitio::{ByteReader, ByteWriter, DecodeError};
32use crate::codec::Decoded;
33
34/// Stream magic of the interpolation codec.
35pub const MAGIC: &[u8; 4] = b"TCI1";
36
37/// Magnitude that marks an escaped (raw f32) point.
38pub(crate) const ESC: u64 = 1 << 24;
39const LVL: usize = 4;
40const ACT: usize = 9;
41const PREV: usize = 3;
42/// `(α, β)` presets for the level-wise bound.
43const TUNE: [(f64, f64); 4] = [(1.0, 1.0), (1.25, 1.5), (1.5, 2.0), (2.0, 4.0)];
44
45struct Models {
46    zero: Vec<BitModel>,
47    sign: Vec<BitModel>,
48    mag: Vec<UintModel>,
49    prev: usize,
50}
51
52impl Models {
53    fn new() -> Self {
54        Self {
55            zero: vec![BitModel::default(); LVL * ACT * PREV],
56            sign: vec![BitModel::default(); LVL],
57            mag: vec![UintModel::default(); LVL * 4],
58            prev: 0,
59        }
60    }
61}
62
63/// Coding context of one point.
64#[derive(Clone, Copy)]
65struct Ctx {
66    lvl: usize,
67    act: usize,
68}
69
70fn levels(dims: &[usize]) -> u32 {
71    let m = dims.iter().copied().max().unwrap_or(1);
72    usize::BITS - (m.max(1) - 1).leading_zeros()
73}
74
75fn level_bound(b: f64, lv: u32, tune: (f64, f64)) -> f64 {
76    b / tune.0.powi(lv as i32 - 1).min(tune.1)
77}
78
79/// Activity bucket: `⌈log₂(1 + |b − a|/step)⌉`, clamped; `ACT − 1` = no right neighbour.
80fn act_bucket(d: Option<f64>, step: f64) -> usize {
81    match d {
82        None => ACT - 1,
83        Some(d) => {
84            let u = (d / step) as u64;
85            ((u64::BITS - u.leading_zeros()) as usize).min(ACT - 2)
86        }
87    }
88}
89
90/// Visit every point of one refinement pass (stride `s`, axis `k`) in
91/// decode order, calling `f(index, prediction, neighbour difference)`; `f`
92/// returns the reconstruction, which is written to `r`.
93fn pass(
94    dims: &[usize],
95    s: usize,
96    k: usize,
97    cubic: bool,
98    r: &mut [f32],
99    mut f: impl FnMut(usize, f64, Option<f64>) -> f32,
100) {
101    let nd = dims.len();
102    if dims[k] <= s {
103        return;
104    }
105    let mut strides = vec![1usize; nd];
106    for j in (0..nd.saturating_sub(1)).rev() {
107        strides[j] = strides[j + 1] * dims[j + 1];
108    }
109    let (start, step): (Vec<usize>, Vec<usize>) = (0..nd)
110        .map(|j| match j.cmp(&k) {
111            std::cmp::Ordering::Less => (0, s),
112            std::cmp::Ordering::Equal => (s, 2 * s),
113            std::cmp::Ordering::Greater => (0, 2 * s),
114        })
115        .unzip();
116    let st = strides[k];
117    let n = dims[k];
118    let mut c = start.clone();
119    loop {
120        let i: usize = c.iter().zip(&strides).map(|(a, b)| a * b).sum();
121        let ck = c[k];
122        let a = r[i - s * st] as f64;
123        let (pred, diff) = if ck + s < n {
124            let b = r[i + s * st] as f64;
125            let p = if cubic {
126                match (ck >= 3 * s, ck + 3 * s < n) {
127                    (true, true) => {
128                        (-(r[i - 3 * s * st] as f64) + 9.0 * a + 9.0 * b - r[i + 3 * s * st] as f64)
129                            / 16.0
130                    }
131                    (false, true) => (3.0 * a + 6.0 * b - r[i + 3 * s * st] as f64) / 8.0,
132                    (true, false) => (-(r[i - 3 * s * st] as f64) + 6.0 * a + 3.0 * b) / 8.0,
133                    (false, false) => (a + b) / 2.0,
134                }
135            } else {
136                (a + b) / 2.0
137            };
138            (p, Some((b - a).abs()))
139        } else {
140            (a, None)
141        };
142        r[i] = f(i, pred, diff);
143        // odometer, last axis fastest
144        let mut j = nd;
145        loop {
146            if j == 0 {
147                return;
148            }
149            j -= 1;
150            c[j] += step[j];
151            if c[j] < dims[j] {
152                break;
153            }
154            c[j] = start[j];
155        }
156    }
157}
158
159fn zctx(m: &Models, cx: Ctx) -> usize {
160    (cx.lvl * ACT + cx.act) * PREV + m.prev
161}
162
163fn mctx(cx: Ctx) -> usize {
164    cx.lvl * 4 + cx.act.min(3)
165}
166
167fn put(e: &mut Encoder, m: &mut Models, cx: Ctx, q: i64, raw: Option<f32>) {
168    let z = zctx(m, cx);
169    if q == 0 && raw.is_none() {
170        e.encode(false, &mut m.zero[z]);
171        m.prev = 0;
172        return;
173    }
174    e.encode(true, &mut m.zero[z]);
175    e.encode(q < 0, &mut m.sign[cx.lvl]);
176    match raw {
177        Some(v) => {
178            m.mag[mctx(cx)].encode(e, ESC - 1);
179            e.encode_raw(v.to_bits() as u64, 32);
180            m.prev = 2;
181        }
182        None => {
183            let a = q.unsigned_abs();
184            m.mag[mctx(cx)].encode(e, a - 1);
185            m.prev = if a == 1 { 1 } else { 2 };
186        }
187    }
188}
189
190/// `Ok(q)` or `Err(raw value)`.
191fn get(d: &mut Decoder, m: &mut Models, cx: Ctx) -> Result<i64, f32> {
192    let z = zctx(m, cx);
193    if !d.decode(&mut m.zero[z]) {
194        m.prev = 0;
195        return Ok(0);
196    }
197    let neg = d.decode(&mut m.sign[cx.lvl]);
198    let a = m.mag[mctx(cx)].decode(d) + 1;
199    if a >= ESC {
200        m.prev = 2;
201        return Err(f32::from_bits(d.decode_raw(32) as u32));
202    }
203    m.prev = if a == 1 { 1 } else { 2 };
204    Ok(if neg { -(a as i64) } else { a as i64 })
205}
206
207/// Quantize one point: `(q, reconstruction, escaped)`.
208#[inline]
209pub(crate) fn quant(x: f32, pred: f64, step: f64, b: f64) -> (i64, f32, bool) {
210    let qf = ((x as f64 - pred) / step).round();
211    if qf.abs() < (ESC - 1) as f64 {
212        let q = qf as i64;
213        let rec = (pred + q as f64 * step) as f32;
214        if ((x as f64) - rec as f64).abs() <= b && rec.is_finite() {
215            return (q, rec, false);
216        }
217    }
218    (0, x, true)
219}
220
221fn encode_with(x: &[f32], dims: &[usize], b: f64, tune: (f64, f64)) -> Vec<u8> {
222    encode_sse(x, dims, b, tune).0
223}
224
225/// [`encode_with`], also returning the reconstruction's squared error.
226fn encode_sse(x: &[f32], dims: &[usize], b: f64, tune: (f64, f64)) -> (Vec<u8>, f64) {
227    let nl = levels(dims);
228    let mut r = vec![0f32; x.len()];
229    let mut m = Models::new();
230    let mut e = Encoder::new();
231    let mut kinds = 0u64;
232    // origin: predicted as 0 at the top level's bound
233    let step0 = 2.0 * level_bound(b, nl.max(1), tune);
234    let (q, rec, esc) = quant(x[0], 0.0, step0, b);
235    put(
236        &mut e,
237        &mut m,
238        Ctx {
239            lvl: LVL - 1,
240            act: ACT - 1,
241        },
242        q,
243        esc.then_some(x[0]),
244    );
245    r[0] = rec;
246    for lv in (1..=nl).rev() {
247        let s = 1usize << (lv - 1);
248        let step = 2.0 * level_bound(b, lv, tune);
249        let lvl = (lv as usize - 1).min(LVL - 1);
250        // trial cost of linear vs cubic for this level
251        let mut cost = [0f64; 2];
252        for (ci, cubic) in [false, true].into_iter().enumerate() {
253            for k in 0..dims.len() {
254                pass(dims, s, k, cubic, &mut r, |i, p, _| {
255                    let (q, rec, esc) = quant(x[i], p, step, b);
256                    cost[ci] += if esc {
257                        40.0
258                    } else if q == 0 {
259                        0.0
260                    } else {
261                        2.0 + 2.0 * (q.unsigned_abs() as f64).log2()
262                    };
263                    rec
264                });
265            }
266        }
267        let cubic = cost[1] <= cost[0];
268        kinds |= (cubic as u64) << lv;
269        for k in 0..dims.len() {
270            pass(dims, s, k, cubic, &mut r, |i, p, d| {
271                let (q, rec, esc) = quant(x[i], p, step, b);
272                put(
273                    &mut e,
274                    &mut m,
275                    Ctx {
276                        lvl,
277                        act: act_bucket(d, step),
278                    },
279                    q,
280                    esc.then_some(x[i]),
281                );
282                rec
283            });
284        }
285    }
286    let mut w = ByteWriter::new();
287    w.bytes(MAGIC);
288    w.u8(dims.len() as u8);
289    for &d in dims {
290        w.varint(d as u64);
291    }
292    w.f64(b);
293    w.u8(TUNE.iter().position(|&t| t == tune).unwrap() as u8);
294    w.varint(kinds);
295    w.bytes(&e.finish());
296    let sse = x
297        .iter()
298        .zip(&r)
299        .map(|(&a, &b)| (a as f64 - b as f64).powi(2))
300        .sum();
301    (w.buf, sse)
302}
303
304/// Encode `x` (row-major, shape `dims`) with `|x − x̂| ≤ b` everywhere,
305/// trying each `(α, β)` preset and keeping the smallest stream.
306pub fn encode_f32(x: &[f32], dims: &[usize], b: f64) -> Vec<u8> {
307    assert_eq!(
308        x.len(),
309        dims.iter().product::<usize>(),
310        "dims do not match data"
311    );
312    assert!(!dims.is_empty() && dims.len() <= 255 && b > 0.0);
313    TUNE.iter()
314        .map(|&t| encode_with(x, dims, b, t))
315        .min_by_key(|v| v.len())
316        .unwrap()
317}
318
319/// Encode for an RMSE target (`rmse` absolute): the bound `b` is rate
320/// controlled per `(α, β)` preset — the largest `b` whose reconstruction
321/// meets the squared-error budget, by secant steps on `log SSE(log b)`
322/// inside a bisection bracket — and the smallest stream wins. The stream is
323/// an ordinary max-abs stream at that `b`; the target is checked on the
324/// exact reconstruction the decoder computes.
325pub fn encode_rmse_f32(x: &[f32], dims: &[usize], rmse: f64) -> Vec<u8> {
326    assert_eq!(
327        x.len(),
328        dims.iter().product::<usize>(),
329        "dims do not match data"
330    );
331    assert!(!dims.is_empty() && dims.len() <= 255 && rmse > 0.0);
332    TUNE.iter()
333        .map(|&t| rate_controlled(x, dims, rmse, t))
334        .min_by_key(|v| v.len())
335        .unwrap()
336}
337
338/// [`encode_rmse_f32`] at `PSNR = 20 log₁₀(range / RMSE)` dB.
339pub fn encode_psnr_f32(x: &[f32], dims: &[usize], psnr: f64) -> Vec<u8> {
340    let (lo, hi) = x
341        .iter()
342        .fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), &v| {
343            (a.min(v as f64), b.max(v as f64))
344        });
345    let range = if hi > lo { hi - lo } else { 1.0 };
346    encode_rmse_f32(x, dims, range / 10f64.powf(psnr / 20.0))
347}
348
349fn rate_controlled(x: &[f32], dims: &[usize], rmse: f64, tune: (f64, f64)) -> Vec<u8> {
350    rate_control(x.len(), rmse, |b| encode_sse(x, dims, b, tune))
351}
352
353/// Rate control of a max-abs bound `b` for an RMSE target over `n` values:
354/// the largest `b` whose stream (`enc(b)` returns it with its exact squared
355/// error) meets the budget, by secant steps on `log SSE(log b)` inside a
356/// bisection bracket (≤ 12 encodes, then halving by 4 until it passes).
357pub(crate) fn rate_control(
358    n: usize,
359    rmse: f64,
360    mut enc: impl FnMut(f64) -> (Vec<u8>, f64),
361) -> Vec<u8> {
362    let budget = rmse * rmse * n as f64;
363    // uniform quantization error ≈ b/√3 at the finest level
364    let mut b = rmse * 3f64.sqrt();
365    // (log b, log sse) of the tightest pass and the loosest fail
366    let mut ok: Option<(f64, f64, Vec<u8>)> = None;
367    let mut bad: Option<(f64, f64)> = None;
368    for _ in 0..12 {
369        let (s, sse) = enc(b);
370        let (lb, ls) = (b.ln(), sse.max(1e-300).ln());
371        if sse <= budget {
372            let close = sse >= budget * 0.99;
373            if ok.as_ref().is_none_or(|o| lb > o.0) {
374                ok = Some((lb, ls, s));
375            }
376            if close {
377                break;
378            }
379        } else if bad.is_none_or(|o| lb < o.0) {
380            bad = Some((lb, ls));
381        }
382        let lt = budget.ln();
383        b = match (&ok, bad) {
384            (Some(o), Some(f)) => {
385                // secant on the bracket ends, kept strictly inside it
386                let t = if f.1 > o.1 {
387                    ((lt - o.1) / (f.1 - o.1)).clamp(0.1, 0.9)
388                } else {
389                    0.5
390                };
391                (o.0 + t * (f.0 - o.0)).exp()
392            }
393            // SSE ∝ b² away from escapes
394            (Some(o), None) => (o.0 + ((lt - o.1) / 2.0).clamp(0.05, 2.0)).exp(),
395            (None, Some(f)) => (f.0 + ((lt - f.1) / 2.0).clamp(-2.0, -0.05)).exp(),
396            (None, None) => unreachable!(),
397        };
398        if !b.is_finite() || b <= 0.0 {
399            break;
400        }
401    }
402    // the bound shrinks until it passes (tiny bounds escape every point: SSE 0)
403    match ok {
404        Some(o) => o.2,
405        None => {
406            let mut b = bad.map_or(rmse, |f| f.0.exp());
407            loop {
408                b *= 0.25;
409                let (s, sse) = enc(b);
410                if sse <= budget {
411                    return s;
412                }
413            }
414        }
415    }
416}
417
418/// Fast-mode preset index into the `(α, β)` table (the one most often kept
419/// by [`encode_f32`] on the seen fields).
420pub const FAST_TUNE: usize = 1;
421
422/// [`encode_f32`] with a single `(α, β)` preset ([`FAST_TUNE`]).
423pub fn encode_fast_f32(x: &[f32], dims: &[usize], b: f64) -> Vec<u8> {
424    assert_eq!(
425        x.len(),
426        dims.iter().product::<usize>(),
427        "dims do not match data"
428    );
429    assert!(!dims.is_empty() && dims.len() <= 255 && b > 0.0);
430    let i = std::env::var("TC_FAST_TUNE")
431        .ok()
432        .and_then(|v| v.parse().ok())
433        .unwrap_or(FAST_TUNE);
434    encode_with(x, dims, b, TUNE[i])
435}
436
437/// Decode an interpolation stream.
438pub fn decode(bytes: &[u8]) -> Result<Decoded, DecodeError> {
439    let mut rd = ByteReader::new(bytes);
440    if rd.take(4)? != MAGIC {
441        return Err(DecodeError::Invalid("magic"));
442    }
443    let nd = rd.u8()? as usize;
444    let dims = (0..nd)
445        .map(|_| rd.varint().map(|v| v as usize))
446        .collect::<Result<Vec<_>, _>>()?;
447    if nd == 0
448        || dims.contains(&0)
449        || dims
450            .iter()
451            .try_fold(1usize, |a, &d| a.checked_mul(d))
452            .is_none_or(|n| n as u64 > 1 << 34)
453    {
454        return Err(DecodeError::Invalid("dims"));
455    }
456    let b = rd.f64()?;
457    let tune = *TUNE
458        .get(rd.u8()? as usize)
459        .ok_or(DecodeError::Invalid("tune"))?;
460    let kinds = rd.varint()?;
461    if b.is_nan() || b <= 0.0 {
462        return Err(DecodeError::Invalid("bound"));
463    }
464    let payload = &bytes[rd.position()..];
465    let mut d = Decoder::new(payload);
466    let mut m = Models::new();
467    let n: usize = dims.iter().product();
468    let mut r = vec![0f32; n];
469    let nl = levels(&dims);
470    let val = |q: Result<i64, f32>, p: f64, step: f64| match q {
471        Ok(q) => (p + q as f64 * step) as f32,
472        Err(v) => v,
473    };
474    let step0 = 2.0 * level_bound(b, nl.max(1), tune);
475    r[0] = val(
476        get(
477            &mut d,
478            &mut m,
479            Ctx {
480                lvl: LVL - 1,
481                act: ACT - 1,
482            },
483        ),
484        0.0,
485        step0,
486    );
487    for lv in (1..=nl).rev() {
488        let s = 1usize << (lv - 1);
489        let step = 2.0 * level_bound(b, lv, tune);
490        let lvl = (lv as usize - 1).min(LVL - 1);
491        let cubic = (kinds >> lv) & 1 == 1;
492        for k in 0..nd {
493            pass(&dims, s, k, cubic, &mut r, |_, p, dd| {
494                val(
495                    get(
496                        &mut d,
497                        &mut m,
498                        Ctx {
499                            lvl,
500                            act: act_bucket(dd, step),
501                        },
502                    ),
503                    p,
504                    step,
505                )
506            });
507        }
508    }
509    if d.overrun() {
510        return Err(DecodeError::Invalid("truncated"));
511    }
512    Ok(Decoded::F32 { dims, data: r })
513}
514
515#[cfg(test)]
516mod tests {
517    use super::*;
518
519    #[test]
520    fn psnr_targets_are_met() {
521        for d in [vec![40, 33, 29], vec![3000]] {
522            let x = field(&d);
523            let (lo, hi) = x
524                .iter()
525                .fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), &v| {
526                    (a.min(v as f64), b.max(v as f64))
527                });
528            let mut last = 0;
529            for p in [40.0, 60.0, 80.0, 100.0] {
530                let s = encode_psnr_f32(&x, &d, p);
531                let Decoded::F32 { data: y, .. } = decode(&s).unwrap() else {
532                    panic!("f32")
533                };
534                let mse = x
535                    .iter()
536                    .zip(&y)
537                    .map(|(&a, &b)| (a as f64 - b as f64).powi(2))
538                    .sum::<f64>()
539                    / x.len() as f64;
540                let got = 20.0 * ((hi - lo) / mse.sqrt()).log10();
541                assert!(got >= p, "{d:?} {p} dB: got {got:.3}");
542                // rate control lands near the target, not far above it
543                assert!(got < p + 3.0, "{d:?} {p} dB: got {got:.3}");
544                assert!(s.len() > last);
545                last = s.len();
546            }
547        }
548    }
549
550    fn field(d: &[usize]) -> Vec<f32> {
551        let n: usize = d.iter().product();
552        (0..n)
553            .map(|i| {
554                let t = i as f32 / n as f32;
555                (t * 40.0).sin() * 3.0
556                    + (i % d[d.len() - 1]) as f32 * 0.01
557                    + if i % 997 == 0 { 50.0 } else { 0.0 }
558            })
559            .collect()
560    }
561
562    #[test]
563    fn round_trips_within_bound() {
564        for d in [
565            vec![1000],
566            vec![33, 70],
567            vec![17, 9, 31],
568            vec![1, 1, 5],
569            vec![1],
570        ] {
571            let x = field(&d);
572            for b in [1e-1, 1e-3, 1e-6] {
573                let bytes = encode_f32(&x, &d, b);
574                let Decoded::F32 { data, dims } = decode(&bytes).unwrap() else {
575                    panic!()
576                };
577                assert_eq!(dims, d);
578                for (a, c) in x.iter().zip(&data) {
579                    assert!(((a - c) as f64).abs() <= b, "{d:?} b={b} {a} {c}");
580                }
581            }
582        }
583    }
584
585    #[test]
586    fn smooth_is_tiny() {
587        let d = [64, 64, 64];
588        let x: Vec<f32> = (0..d.iter().product::<usize>())
589            .map(|i| ((i / 4096) as f32 * 0.05).sin() + ((i % 64) as f32 * 0.03).cos())
590            .collect();
591        let bytes = encode_f32(&x, &d, 1e-2);
592        assert!(bytes.len() < 2000, "{}", bytes.len());
593    }
594}