Skip to main content

cram_codec/
tucker.rs

1//! Tucker (HOSVD) codec, TTHRESH-style, on the shared quantizer / context
2//! coder / range coder stack.
3//!
4//! 1. **HOSVD.** For each mode `k` the eigenvectors of the mode Gram
5//!    `X₍ₖ₎X₍ₖ₎ᵀ` give an orthonormal factor `Uₖ`; the core
6//!    `C = X ×₁U₁ᵀ ×₂U₂ᵀ ×₃U₃ᵀ` concentrates the energy at low indices.
7//! 2. **Ranks by rate–distortion.** The core is quantized with the deadzone
8//!    quantizer at step `Δ`; `Rₖ` = 1 + the last mode-`k` slice with a nonzero
9//!    quantized coefficient.
10//! 3. **Factor quantization, equal slope.** Perturbing column `j` of `Uₖ` by
11//!    `e` costs `‖e‖²·‖C₍ₖ₎[j,:]‖²`, so the column gets step
12//!    `sⱼ = φΔ/‖C₍ₖ₎[j,:]‖` (stored as a quarter-octave exponent). Columns are
13//!    coded as DPCM residuals (order 0/1/2, chosen per column) with a Rice
14//!    split: adaptive Elias-gamma high part + raw low bits.
15//! 4. **Error feedback.** The core is recomputed by least squares against the
16//!    *quantized* factors, `C = X ×ₖ (ÛₖᵀÛₖ)⁻¹Ûₖᵀ`, then quantized and coded
17//!    with the wavelet coefficient coder over a dyadic pseudo-pyramid in core
18//!    index space (energy decays with index the way it does across wavelet
19//!    levels, so the same band / parent / neighbour contexts apply).
20//! 5. **Rate control.** `Δ` is bisected on the measured RMSE of the decoder's
21//!    own reconstruction.
22//!
23//! Reconstruction uses faer GEMMs (sequential): deterministic on a given CPU;
24//! across ISAs the float output may differ in the last ulp. The bitstream
25//! itself is integer-coded and identical everywhere. No max-abs mode
26//! (TTHRESH has none either).
27
28use crate::abac::{Decoder, Encoder, UintModel, ONE};
29use crate::bitio::{ByteReader, ByteWriter, DecodeError};
30use crate::codec::{Decoded, ErrorBound};
31use crate::coefcode::{code_bands, fit_predictors, read_preds, write_preds, Bits, HandModel};
32use crate::linalg::{
33    cholesky, gram_cols, gram_rows, matmul, matmul_tn, solve_lower, solve_lower_t, sym_eig_desc,
34    transpose,
35};
36use crate::wavelet::{as3, Pyramid, MAX_LEVELS};
37
38/// Stream magic.
39pub const MAGIC: &[u8; 4] = b"TCT1";
40
41const RHO: f64 = 0.45;
42/// Factor-step multiplier `φ`.
43const PHI: f64 = 1.0;
44
45/// What the encoder did.
46#[derive(Clone, Debug, Default)]
47pub struct TuckerStats {
48    /// Core payload bytes.
49    pub core_bytes: usize,
50    /// Factor bytes.
51    pub factor_bytes: usize,
52    /// Kept ranks.
53    pub ranks: Vec<usize>,
54    /// Core step.
55    pub delta: f64,
56    /// Measured RMSE.
57    pub rmse: f64,
58    /// Measured max-abs error.
59    pub max_abs: f64,
60}
61
62/// `X ×ₖ M` for `M` (n_k × r): mode `k` becomes size `r`,
63/// `Y[.., j, ..] = Σᵢ M[i, j] X[.., i, ..]`.
64fn mode_mul(x: &[f64], d: [usize; 3], k: usize, m: &[f64], r: usize) -> (Vec<f64>, [usize; 3]) {
65    let n = d[k];
66    let mut od = d;
67    od[k] = r;
68    let y = match k {
69        0 => matmul_tn(m, x, n, r, d[1] * d[2]),
70        2 => matmul(x, m, d[0] * d[1], n, r),
71        _ => {
72            let mut y = Vec::with_capacity(d[0] * r * d[2]);
73            for i in 0..d[0] {
74                let s = &x[i * d[1] * d[2]..(i + 1) * d[1] * d[2]];
75                y.extend(matmul_tn(m, s, n, r, d[2]));
76            }
77            y
78        }
79    };
80    (y, od)
81}
82
83/// Inverse direction: `M` is n × r, mode `k` (size r) grows back to `n`,
84/// `Y[.., i, ..] = Σⱼ M[i, j] X[.., j, ..]`.
85fn mode_mul_t(x: &[f64], d: [usize; 3], k: usize, m: &[f64], n: usize) -> (Vec<f64>, [usize; 3]) {
86    let r = d[k];
87    let mt = transpose(m, n, r);
88    mode_mul(x, d, k, &mt, n)
89}
90
91fn mode_gram(x: &[f64], d: [usize; 3], k: usize) -> Vec<f64> {
92    match k {
93        0 => gram_rows(x, d[0], d[1] * d[2]),
94        2 => gram_cols(x, d[0] * d[1], d[2]),
95        _ => {
96            let mut g = vec![0.0; d[1] * d[1]];
97            for i in 0..d[0] {
98                let s = &x[i * d[1] * d[2]..(i + 1) * d[1] * d[2]];
99                for (a, b) in g.iter_mut().zip(gram_rows(s, d[1], d[2])) {
100                    *a += b;
101                }
102            }
103            g
104        }
105    }
106}
107
108/// Mode-`k` slice norms of a core.
109fn slice_norms(c: &[f64], d: [usize; 3], k: usize) -> Vec<f64> {
110    let mut s = vec![0.0; d[k]];
111    for i in 0..d[0] {
112        for j in 0..d[1] {
113            for l in 0..d[2] {
114                let v = c[(i * d[1] + j) * d[2] + l];
115                s[[i, j, l][k]] += v * v;
116            }
117        }
118    }
119    s.iter().map(|v| v.sqrt()).collect()
120}
121
122fn quantize_core(c: &[f64], delta: f64) -> (Vec<i32>, [u8; 2]) {
123    let inv = 1.0 / delta;
124    let (mut s, mut n) = ([0.0f64; 2], [0u64; 2]);
125    let q = c
126        .iter()
127        .map(|&v| {
128            let a = v.abs() * inv;
129            let m = (a + RHO).floor().min(1e9);
130            if m < 1.0 {
131                return 0;
132            }
133            let j = (m >= 2.0) as usize;
134            s[j] += m - a;
135            n[j] += 1;
136            if v < 0.0 {
137                -(m as i32)
138            } else {
139                m as i32
140            }
141        })
142        .collect();
143    let d = [0, 1].map(|j| {
144        if n[j] > 0 {
145            ((s[j] / n[j] as f64).clamp(0.0, 0.498) * 512.0).round() as u8
146        } else {
147            0
148        }
149    });
150    (q, d)
151}
152
153fn dequantize_core(q: &[i32], delta: f64, d: [u8; 2]) -> Vec<f64> {
154    q.iter()
155        .map(|&v| {
156            if v == 0 {
157                return 0.0;
158            }
159            let m = v.unsigned_abs() as f64 - d[(v.unsigned_abs() >= 2) as usize] as f64 / 512.0;
160            if v < 0 {
161                -m * delta
162            } else {
163                m * delta
164            }
165        })
166        .collect()
167}
168
169/// One quantized factor matrix (n × r) with its column exponents.
170#[derive(Clone, Debug)]
171struct QFactor {
172    n: usize,
173    r: usize,
174    exps: Vec<i32>,
175    q: Vec<i64>, // column-major ints
176}
177
178impl QFactor {
179    fn quantize(u: &[f64], n: usize, full_r: usize, r: usize, norms: &[f64], delta: f64) -> Self {
180        let mut exps = Vec::with_capacity(r);
181        let mut q = Vec::with_capacity(n * r);
182        for j in 0..r {
183            let s = PHI * delta / norms[j].max(1e-300);
184            let e = (4.0 * s.log2()).round().clamp(-4000.0, 4000.0) as i32;
185            let s = 2f64.powf(e as f64 / 4.0);
186            exps.push(e);
187            for i in 0..n {
188                q.push((u[i * full_r + j] / s).round().clamp(-4e15, 4e15) as i64);
189            }
190        }
191        Self { n, r, exps, q }
192    }
193    fn dequantize(&self) -> Vec<f64> {
194        let mut u = vec![0.0; self.n * self.r];
195        for j in 0..self.r {
196            let s = 2f64.powf(self.exps[j] as f64 / 4.0);
197            for i in 0..self.n {
198                u[i * self.r + j] = self.q[j * self.n + i] as f64 * s;
199            }
200        }
201        // a column quantized to all zeros would make the Gram singular
202        for j in 0..self.r {
203            if (0..self.n).all(|i| u[i * self.r + j] == 0.0) {
204                u[(j % self.n) * self.r + j] = 2f64.powf(self.exps[j] as f64 / 4.0);
205            }
206        }
207        u
208    }
209}
210
211#[inline]
212fn dpcm_pred(c: &[i64], i: usize, order: usize) -> i64 {
213    match (order, i) {
214        (0, _) | (_, 0) => 0,
215        (1, _) | (2, 1) => c[i - 1],
216        _ => 2 * c[i - 1] - c[i - 2],
217    }
218}
219
220fn zz(v: i64) -> u64 {
221    ((v << 1) ^ (v >> 63)) as u64
222}
223
224fn unzz(u: u64) -> i64 {
225    ((u >> 1) as i64) ^ -((u & 1) as i64)
226}
227
228fn code_factor<B: Bits>(b: &mut B, f: &mut QFactor, km: &mut UintModel, hm: &mut UintModel) {
229    for j in 0..f.r {
230        let col = f.q[j * f.n..(j + 1) * f.n].to_vec();
231        // encoder picks the DPCM order and Rice parameter; the decoder reads them
232        let res_of = |o: usize| -> Vec<u64> {
233            (0..col.len())
234                .map(|i| zz(col[i] - dpcm_pred(&col, i, o)))
235                .collect()
236        };
237        let bo = (0..3)
238            .min_by_key(|&o| res_of(o).iter().sum::<u64>())
239            .unwrap();
240        let res = res_of(bo);
241        let mean = res.iter().sum::<u64>() / f.n.max(1) as u64;
242        let k = if mean > 2 { (mean / 2).ilog2() } else { 0 };
243        let o =
244            b.bitp(bo & 1 == 1, ONE / 2) as usize | (b.bitp(bo & 2 == 2, ONE / 2) as usize) << 1;
245        let k = b.uint(k as u64, km) as u32;
246        if k > 62 || o > 2 {
247            return; // corrupt stream; decoder output is garbage but bounded
248        }
249        let mut out: Vec<i64> = Vec::with_capacity(f.n);
250        for i in 0..f.n {
251            let u = res[i];
252            let hi = b.uint(u >> k, hm);
253            let mut lo = 0u64;
254            for t in (0..k).rev() {
255                lo = (lo << 1) | b.bitp((u >> t) & 1 == 1, ONE / 2) as u64;
256            }
257            let v = unzz((hi << k) | lo) + dpcm_pred(&out, i, o);
258            out.push(v);
259        }
260        f.q[j * f.n..(j + 1) * f.n].copy_from_slice(&out);
261    }
262}
263
264/// Least-squares core against (non-orthonormal) factors: `X ×ₖ (UᵀU)⁻¹Uᵀ`.
265fn ls_core(x: &[f64], d: [usize; 3], us: &[Vec<f64>], rs: [usize; 3]) -> Vec<f64> {
266    let mut c = x.to_vec();
267    let mut cd = d;
268    for k in 0..3 {
269        let (n, r) = (d[k], rs[k]);
270        let g = matmul_tn(&us[k], &us[k], n, r, r);
271        // quantized columns can be (numerically) dependent on tiny-range
272        // fields: retry with a growing ridge (encoder-side only; the decoder
273        // never solves this system, so streams that factor cleanly are unchanged)
274        let tr = (0..r).map(|i| g[i * r + i]).sum::<f64>() / r as f64;
275        let l = cholesky(&g, r)
276            .or_else(|| {
277                (1..=12).find_map(|e| {
278                    let mut gg = g.clone();
279                    for i in 0..r {
280                        gg[i * r + i] += tr * 10f64.powi(e - 16);
281                    }
282                    cholesky(&gg, r)
283                })
284            })
285            .expect("factor Gram not SPD even with ridge");
286        // Z = (UᵀU)⁻¹Uᵀ (r × n); mode_mul wants M = Zᵀ (n × r)
287        let mut z = transpose(&us[k], n, r);
288        solve_lower(&l, &mut z, r, n);
289        solve_lower_t(&l, &mut z, r, n);
290        let m = transpose(&z, r, n);
291        let (y, yd) = mode_mul(&c, cd, k, &m, r);
292        c = y;
293        cd = yd;
294    }
295    c
296}
297
298fn reconstruct(core: &[f64], rs: [usize; 3], us: &[Vec<f64>], d: [usize; 3]) -> Vec<f64> {
299    let mut y = core.to_vec();
300    let mut yd = rs;
301    for k in 0..3 {
302        let (z, zd) = mode_mul_t(&y, yd, k, &us[k], d[k]);
303        y = z;
304        yd = zd;
305    }
306    y
307}
308
309struct Hosvd {
310    d: [usize; 3],
311    us: Vec<Vec<f64>>, // full orthonormal factors n×n
312    core: Vec<f64>,
313}
314
315fn hosvd(x: &[f64], d: [usize; 3]) -> Hosvd {
316    let us: Vec<Vec<f64>> = (0..3)
317        .map(|k| sym_eig_desc(&mode_gram(x, d, k), d[k]).1)
318        .collect();
319    let mut c = x.to_vec();
320    let mut cd = d;
321    for k in 0..3 {
322        let (y, yd) = mode_mul(&c, cd, k, &us[k], d[k]);
323        c = y;
324        cd = yd;
325    }
326    Hosvd { d, us, core: c }
327}
328
329/// Everything one trial at step `Δ` produces.
330struct Trial {
331    rs: [usize; 3],
332    facs: Vec<QFactor>,
333    q: Vec<i32>,
334    dl: [u8; 2],
335    y: Vec<f64>,
336}
337
338fn trial(x: &[f64], h: &Hosvd, delta: f64) -> Trial {
339    let d = h.d;
340    let (q0, _) = quantize_core(&h.core, delta);
341    let mut rs = [1usize; 3];
342    for i in 0..d[0] {
343        for j in 0..d[1] {
344            for l in 0..d[2] {
345                if q0[(i * d[1] + j) * d[2] + l] != 0 {
346                    rs = [rs[0].max(i + 1), rs[1].max(j + 1), rs[2].max(l + 1)];
347                }
348            }
349        }
350    }
351    let facs: Vec<QFactor> = (0..3)
352        .map(|k| {
353            QFactor::quantize(
354                &h.us[k],
355                d[k],
356                d[k],
357                rs[k],
358                &slice_norms(&h.core, d, k),
359                delta,
360            )
361        })
362        .collect();
363    let ud: Vec<Vec<f64>> = facs.iter().map(|f| f.dequantize()).collect();
364    let core = ls_core(x, d, &ud, rs);
365    let (q, dl) = quantize_core(&core, delta);
366    let y = reconstruct(&dequantize_core(&q, delta, dl), rs, &ud, d);
367    Trial { rs, facs, q, dl, y }
368}
369
370fn err_of(x: &[f64], y: &[f64]) -> (f64, f64) {
371    let mut se = 0.0;
372    let mut mx = 0.0f64;
373    for (a, b) in x.iter().zip(y) {
374        let e = a - *b as f32 as f64;
375        se += e * e;
376        mx = mx.max(e.abs());
377    }
378    ((se / x.len() as f64).sqrt(), mx)
379}
380
381fn search(target: f64, ok: &impl Fn(f64) -> bool) -> f64 {
382    let (mut lo, mut hi) = (target * 0.5, target * 6.0);
383    let mut guard = 0;
384    while !ok(lo) && guard < 40 {
385        hi = lo;
386        lo *= 0.5;
387        guard += 1;
388    }
389    while ok(hi) && guard < 80 {
390        lo = hi;
391        hi *= 2.0;
392        guard += 1;
393    }
394    for _ in 0..12 {
395        let mid = (lo * hi).sqrt();
396        if ok(mid) {
397            lo = mid;
398        } else {
399            hi = mid;
400        }
401    }
402    lo
403}
404
405/// Encode f32 samples (1D–3D, row-major) at a PSNR or RMSE target.
406pub fn encode_f32(x: &[f32], dims: &[usize], bound: ErrorBound) -> (Vec<u8>, TuckerStats) {
407    encode_f32_with(x, dims, bound, false)
408}
409
410/// [`encode_f32`] with the core step from [`crate::wavcodec::fast_step`]
411/// (a formula plus a few error checks) instead of a bisection search.
412pub fn encode_fast_f32(x: &[f32], dims: &[usize], bound: ErrorBound) -> (Vec<u8>, TuckerStats) {
413    encode_f32_with(x, dims, bound, true)
414}
415
416fn encode_f32_with(
417    x: &[f32],
418    dims: &[usize],
419    bound: ErrorBound,
420    fast: bool,
421) -> (Vec<u8>, TuckerStats) {
422    let x: Vec<f64> = x.iter().map(|&v| v as f64).collect();
423    let d = as3(dims);
424    assert_eq!(d.iter().product::<usize>(), x.len());
425    let (lo_v, hi_v) = x
426        .iter()
427        .fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), &v| {
428            (a.min(v), b.max(v))
429        });
430    let target = match bound {
431        ErrorBound::Rmse(r) => r,
432        ErrorBound::Psnr(db) => (hi_v - lo_v) * 10f64.powf(-db / 20.0),
433        ErrorBound::MaxAbs(_) => {
434            panic!("the Tucker codec has no max-abs mode; use the wavelet codec")
435        }
436    }
437    .max(f64::MIN_POSITIVE);
438    let h = hosvd(&x, d);
439    let ok = |delta: f64| err_of(&x, &trial(&x, &h, delta).y).0 <= target;
440    let delta = if fast {
441        let (d, _) = crate::wavcodec::fast_step(target, 4, |d| err_of(&x, &trial(&x, &h, d).y).0);
442        if ok(d) {
443            d
444        } else {
445            search(target, &ok)
446        }
447    } else {
448        search(target, &ok)
449    };
450    let t = trial(&x, &h, delta);
451    let mut w = ByteWriter::new();
452    w.bytes(MAGIC);
453    w.u8(0); // f32
454    w.varint(dims.len() as u64);
455    for &v in dims {
456        w.varint(v as u64);
457    }
458    w.f64(delta);
459    for k in 0..3 {
460        w.varint(t.rs[k] as u64);
461    }
462    w.bytes(&t.dl);
463    for f in &t.facs {
464        let mut prev = 0;
465        for &e in &f.exps {
466            w.svarint((e - prev) as i64);
467            prev = e;
468        }
469    }
470    let fb0 = w.buf.len();
471    let mut e = Encoder::new();
472    let (mut km, mut hm) = (UintModel::default(), UintModel::default());
473    for f in &t.facs {
474        let mut f = f.clone();
475        code_factor(&mut e, &mut f, &mut km, &mut hm);
476    }
477    w.block(&e.finish());
478    let factor_bytes = w.buf.len() - fb0;
479    let p = Pyramid::new(t.rs, MAX_LEVELS);
480    let preds = fit_predictors(&p, &t.q);
481    let cb0 = w.buf.len();
482    write_preds(&mut w, &preds);
483    w.u8(0);
484    let mut e = Encoder::new();
485    let mut qq = t.q.clone();
486    code_bands(&p, &mut qq, &preds, &mut e, &mut HandModel::default());
487    w.block(&e.finish());
488    let core_bytes = w.buf.len() - cb0;
489    let (rmse, max_abs) = err_of(&x, &t.y);
490    let st = TuckerStats {
491        core_bytes,
492        factor_bytes,
493        ranks: t.rs.to_vec(),
494        delta,
495        rmse,
496        max_abs,
497    };
498    (w.buf, st)
499}
500
501/// Decode a Tucker stream.
502pub fn decode(bytes: &[u8]) -> Result<Decoded, DecodeError> {
503    let mut r = ByteReader::new(bytes);
504    if r.take(4)? != MAGIC {
505        return Err(DecodeError::Invalid("magic"));
506    }
507    if r.u8()? != 0 {
508        return Err(DecodeError::Invalid("dtype"));
509    }
510    let nd = r.varint()? as usize;
511    if nd == 0 || nd > 3 {
512        return Err(DecodeError::Invalid("ndims"));
513    }
514    let mut dims = Vec::new();
515    for _ in 0..nd {
516        dims.push(r.varint()? as usize);
517    }
518    let d = as3(&dims);
519    if d.iter()
520        .try_fold(1usize, |a, &v| a.checked_mul(v))
521        .is_none_or(|n| n == 0 || n as u64 > 1 << 34)
522    {
523        return Err(DecodeError::Invalid("dims"));
524    }
525    let delta = r.f64()?;
526    let mut rs = [0usize; 3];
527    for k in 0..3 {
528        rs[k] = r.varint()? as usize;
529        if rs[k] == 0 || rs[k] > d[k] {
530            return Err(DecodeError::Invalid("rank"));
531        }
532    }
533    let dl = [r.u8()?, r.u8()?];
534    let mut facs = Vec::new();
535    for k in 0..3 {
536        let mut exps = Vec::new();
537        let mut prev = 0i64;
538        for _ in 0..rs[k] {
539            prev += r.svarint()?;
540            exps.push(prev.clamp(-4000, 4000) as i32);
541        }
542        facs.push(QFactor {
543            n: d[k],
544            r: rs[k],
545            exps,
546            q: vec![0; d[k] * rs[k]],
547        });
548    }
549    let buf = r.block()?;
550    let mut dd = Decoder::new(buf);
551    let (mut km, mut hm) = (UintModel::default(), UintModel::default());
552    for f in facs.iter_mut() {
553        code_factor(&mut dd, f, &mut km, &mut hm);
554    }
555    let ud: Vec<Vec<f64>> = facs.iter().map(|f| f.dequantize()).collect();
556    let p = Pyramid::new(rs, MAX_LEVELS);
557    let preds = read_preds(&mut r, p.bands.len())?;
558    if r.u8()? != 0 {
559        return Err(DecodeError::Invalid("core model"));
560    }
561    let buf = r.block()?;
562    let mut dd = Decoder::new(buf);
563    let mut q = vec![0i32; p.len()];
564    code_bands(&p, &mut q, &preds, &mut dd, &mut HandModel::default());
565    let y = reconstruct(&dequantize_core(&q, delta, dl), rs, &ud, d);
566    Ok(Decoded::F32 {
567        dims,
568        data: y.iter().map(|&v| v as f32).collect(),
569    })
570}
571
572#[cfg(test)]
573mod tests {
574    use super::*;
575
576    #[test]
577    fn tucker_round_trip_meets_psnr() {
578        let d = [12, 20, 16];
579        let mut x = Vec::new();
580        for i in 0..d[0] {
581            for j in 0..d[1] {
582                for k in 0..d[2] {
583                    let (a, b, c) = (i as f32 / 12.0, j as f32 / 20.0, k as f32 / 16.0);
584                    x.push((3.0 * a).sin() * (2.0 * b).cos() + c * c + 0.1 * (9.0 * a * c).sin());
585                }
586            }
587        }
588        for db in [50.0, 90.0] {
589            let (bytes, st) = encode_f32(&x, &d, ErrorBound::Psnr(db));
590            let Decoded::F32 { data, .. } = decode(&bytes).unwrap() else {
591                panic!()
592            };
593            let xd: Vec<f64> = x.iter().map(|&v| v as f64).collect();
594            let (rmse, _) = err_of(&xd, &data.iter().map(|&v| v as f64).collect::<Vec<_>>());
595            assert_eq!(rmse, st.rmse);
596            let range = xd.iter().cloned().fold(f64::MIN, f64::max)
597                - xd.iter().cloned().fold(f64::MAX, f64::min);
598            assert!(20.0 * (range / rmse).log10() >= db - 1e-9);
599            assert_eq!(encode_f32(&x, &d, ErrorBound::Psnr(db)).0, bytes);
600        }
601    }
602}