Skip to main content

cram_codec/
codec.rs

1//! The bit-accounted QTT codec.
2//!
3//! # Encoder
4//!
5//! One *pass* at absolute quantization step `Δ` runs TT-SVD left to right
6//! with **error feedback**: core `k` is computed from the remainder that is
7//! least-squares optimal *given the already-quantized prefix*, quantized, and
8//! only then is the remainder refit against it.
9//!
10//! Let `P_{k−1}` be the quantized prefix (rows = prefix indices, cols =
11//! `r_{k−1}`), `H = P_{k−1}ᵀP_{k−1}` its Gram, and `C` the LS remainder
12//! (`X = (P_{k−1}⊗I)·C + E`, `E ⊥ range`). In the metric `H̃ = H ⊗ I_{n_k}`:
13//!
14//! 1. eigendecompose the whitened Gram of `C` → singular values `σ_j` and an
15//!    `H̃`-orthonormal core `G` (the canonical left-orthogonal gauge, now
16//!    w.r.t. the true prefix);
17//! 2. quantize column `j` of `G` with step `Δ / σ̂_j`, `σ̂_j = 2^{e_j/4}`,
18//!    `e_j = round(4 log₂ σ_j)` — the equal-slope rule: perturbing column `j`
19//!    costs `σ_j²·step²/12` per entry, so every entry ends up with the same
20//!    distortion `Δ²/12` as an entry of the last core. Columns that round to
21//!    all zeros are dropped, so **ranks are chosen by rate–distortion, not by
22//!    a separate tolerance**;
23//! 3. refit `C ← (QᵀH̃Q)⁻¹ QᵀH̃ C`, `H ← QᵀH̃Q`, and add the exact discarded
24//!    energy `‖C‖²_H̃ − ‖proj‖²` to the running SSE.
25//!
26//! The last core (the LS remainder, carrying the norm) is quantized with step
27//! `Δ`; its error `eᵀH̃e` is added too. The SSE is therefore known **exactly at
28//! encode time** without reconstructing the tensor (the error pieces are
29//! mutually orthogonal). `Δ` is then found by a bracketed log–log secant search
30//! on that SSE.
31//!
32//! # Error modes
33//!
34//! - [`ErrorBound::Rmse`] / [`ErrorBound::Psnr`] (range-normalised): the
35//!   searched SSE is followed by an actual decode; if the true RMSE on the
36//!   original (unpadded) cells exceeds the target the search is redone.
37//! - [`ErrorBound::MaxAbs`] (SZ-style pointwise bound `b`): the TT is coded at
38//!   an RMSE target `f·b` for a few `f`, the encoder decodes its own stream,
39//!   and every cell with `|x − x̂| > b` gets a correction `k = round((x−x̂)/2b)`
40//!   (then `|x − (x̂+2bk)| ≤ b`), stored as entropy-coded position gaps + values
41//!   (a raw-value escape covers float-cast edge cases). The `f` giving the
42//!   smallest total stream wins.
43//!
44//! # Bit account
45//!
46//! The stream is self-describing: magic, dtype, digit order, padding policy,
47//! shape, `Δ`, all ranks, all per-column step exponents, every frequency
48//! table, all payloads, the residual stream. `bytes.len()` is the size.
49
50use crate::bitio::{ByteReader, ByteWriter, DecodeError};
51use crate::linalg::{
52    cholesky, gram_rows, kron_eye_apply, matmul, matmul_tn, solve_lower, solve_lower_t,
53    sym_eig_desc,
54};
55use crate::rans::{decode_blocks, encode_blocks};
56use crate::tensorize::{DigitOrder, Layout, PadPolicy};
57use crate::tt::{contract, gd_rank};
58
59const MAGIC: &[u8; 4] = b"QTT1";
60
61/// Error target.
62#[derive(Clone, Copy, Debug, PartialEq)]
63pub enum ErrorBound {
64    /// Root-mean-square error over the original cells, absolute units.
65    Rmse(f64),
66    /// Range-normalised PSNR in dB: `20 log₁₀(range / RMSE)`.
67    Psnr(f64),
68    /// Pointwise `|x − x̂| ≤ b`, absolute units.
69    MaxAbs(f64),
70}
71
72/// Encoder configuration.
73#[derive(Clone, Copy, Debug, PartialEq)]
74pub struct Config {
75    /// Error target.
76    pub bound: ErrorBound,
77    /// Digit order of the tensorization.
78    pub order: DigitOrder,
79    /// Padding policy for non-power-of-two axes.
80    pub pad: PadPolicy,
81    /// Cap ranks at the Gavish–Donoho threshold of each unfolding.
82    pub gavish_donoho: bool,
83    /// Hard rank cap.
84    pub max_rank: usize,
85    /// Max-abs only: skip the TT candidates and code the field as the sparse
86    /// residual stream alone (the `f = 0` candidate; cheap, wins on sparse
87    /// peaky data).
88    pub residual_only: bool,
89}
90
91impl Config {
92    /// Defaults (Morton, mixed radix, no GD, rank cap 1024) for a bound.
93    pub fn new(bound: ErrorBound) -> Self {
94        Self {
95            bound,
96            order: DigitOrder::Morton,
97            pad: PadPolicy::MixedRadix,
98            gavish_donoho: false,
99            max_rank: 1024,
100            residual_only: false,
101        }
102    }
103}
104
105/// Sample type of the original array (decoded output has the same type).
106#[derive(Clone, Copy, Debug, PartialEq, Eq)]
107pub enum Dtype {
108    /// 32-bit float.
109    F32,
110    /// 64-bit float.
111    F64,
112}
113
114/// What the encoder did, for benchmarking.
115#[derive(Clone, Debug, PartialEq)]
116pub struct EncodeStats {
117    /// Total stream size in bytes (everything).
118    pub bytes: usize,
119    /// `8·bytes / cells`.
120    pub bits_per_value: f64,
121    /// Bytes before the core payload (header, ranks, step exponents).
122    pub header_bytes: usize,
123    /// Core payload bytes (tables + rANS + raw bits).
124    pub core_bytes: usize,
125    /// Residual-correction stream bytes (max-abs mode).
126    pub residual_bytes: usize,
127    /// Number of corrected cells.
128    pub n_residual: usize,
129    /// TT bond ranks.
130    pub ranks: Vec<usize>,
131    /// Tensor mode sizes.
132    pub modes: Vec<usize>,
133    /// Final absolute step `Δ`.
134    pub delta: f64,
135    /// Encoder passes run.
136    pub passes: usize,
137    /// Achieved RMSE over the original cells.
138    pub rmse: f64,
139    /// Achieved max-abs error over the original cells.
140    pub max_abs: f64,
141    /// Value range of the input.
142    pub range: f64,
143}
144
145/// A decoded array.
146#[derive(Clone, Debug, PartialEq)]
147pub enum Decoded {
148    /// f32 samples.
149    F32 {
150        /// Shape.
151        dims: Vec<usize>,
152        /// Row-major samples.
153        data: Vec<f32>,
154    },
155    /// f64 samples.
156    F64 {
157        /// Shape.
158        dims: Vec<usize>,
159        /// Row-major samples.
160        data: Vec<f64>,
161    },
162}
163
164/// One quantized pass.
165#[derive(Clone, Debug)]
166struct QPass {
167    ranks: Vec<usize>,
168    exps: Vec<Vec<i32>>,
169    q: Vec<Vec<i64>>,
170    sse: f64,
171}
172
173/// `Δ·2^{−e/4}`, exact in binary floating point.
174fn col_step(delta: f64, e: i32) -> f64 {
175    const FRAC: [f64; 4] = [
176        1.0,
177        0.840_896_415_253_714_5, // 2^(-1/4)
178        core::f64::consts::FRAC_1_SQRT_2,
179        0.594_603_557_501_360_5, // 2^(-3/4)
180    ];
181    let a = e.div_euclid(4);
182    let b = e.rem_euclid(4) as usize;
183    delta * FRAC[b] * 2f64.powi(-a)
184}
185
186fn kron_eye(h: &[f64], r: usize, n: usize) -> Vec<f64> {
187    let m = r * n;
188    let mut out = vec![0.0; m * m];
189    for a in 0..r {
190        for b in 0..r {
191            let v = h[a * r + b];
192            if v != 0.0 {
193                for i in 0..n {
194                    out[(a * n + i) * m + b * n + i] = v;
195                }
196            }
197        }
198    }
199    out
200}
201
202fn quantized_pass(x: &[f64], modes: &[usize], delta: f64, gd: bool, max_rank: usize) -> QPass {
203    let d = modes.len();
204    let mut ranks = vec![1usize];
205    let mut exps = Vec::new();
206    let mut qs = Vec::new();
207    let mut sse = 0.0;
208    let mut c = x.to_vec();
209    let mut h = vec![1.0];
210    let mut r_prev = 1usize;
211    let mut cols: usize = x.len();
212    for &nk in modes.iter().take(d.saturating_sub(1)) {
213        let m = r_prev * nk;
214        cols /= nk;
215        if r_prev == 0 {
216            ranks.push(0);
217            exps.push(Vec::new());
218            qs.push(Vec::new());
219            continue;
220        }
221        let ht = kron_eye(&h, r_prev, nk);
222        // Whitened spectrum and H̃-orthonormal candidate core G (m × kc).
223        let (sig, g, kc, c_energy) = if m <= cols {
224            let cc = gram_rows(&c, m, cols);
225            let l = cholesky(&ht, m).expect("prefix Gram is SPD");
226            let t = matmul(&cc, &l, m, m, m);
227            let gw = matmul_tn(&l, &t, m, m, m);
228            let (lam, vw) = sym_eig_desc(&gw, m);
229            let sig: Vec<f64> = lam.iter().map(|&v| v.max(0.0).sqrt()).collect();
230            let energy: f64 = lam.iter().map(|v| v.max(0.0)).sum();
231            let kc = candidate_count(&sig, delta, gd, m, cols, max_rank);
232            let mut g = vec![0.0; m * kc];
233            for i in 0..m {
234                g[i * kc..i * kc + kc].copy_from_slice(&vw[i * m..i * m + kc]);
235            }
236            solve_lower_t(&l, &mut g, m, kc);
237            (sig, g, kc, energy)
238        } else {
239            let htc = kron_eye_apply(&h, r_prev, nk, &c, cols);
240            let gw = matmul_tn(&c, &htc, m, cols, cols);
241            let (lam, v) = sym_eig_desc(&gw, cols);
242            let sig: Vec<f64> = lam.iter().map(|&v| v.max(0.0).sqrt()).collect();
243            let energy: f64 = lam.iter().map(|v| v.max(0.0)).sum();
244            let smax = sig.first().copied().unwrap_or(0.0);
245            let kc = candidate_count(&sig, delta, gd, m, cols, max_rank).min(
246                sig.iter()
247                    .take_while(|&&s| s > 1e-13 * smax && s > 0.0)
248                    .count(),
249            );
250            let mut vk = vec![0.0; cols * kc];
251            for i in 0..cols {
252                vk[i * kc..i * kc + kc].copy_from_slice(&v[i * cols..i * cols + kc]);
253            }
254            let mut g = matmul(&c, &vk, m, cols, kc);
255            for j in 0..kc {
256                for i in 0..m {
257                    g[i * kc + j] /= sig[j];
258                }
259            }
260            (sig, g, kc, energy)
261        };
262        // Quantize columns with equal-slope steps; drop all-zero columns.
263        let mut keep: Vec<(i32, Vec<i64>)> = Vec::new();
264        for j in 0..kc {
265            let e = (4.0 * sig[j].log2()).round() as i32;
266            let step = col_step(delta, e);
267            let col: Vec<i64> = (0..m)
268                .map(|i| (g[i * kc + j] / step).round() as i64)
269                .collect();
270            if col.iter().any(|&v| v != 0) {
271                keep.push((e, col));
272            }
273        }
274        // Refit; drop trailing columns if the quantized Gram is singular.
275        loop {
276            let r = keep.len();
277            if r == 0 {
278                sse += c_energy;
279                break;
280            }
281            let mut qm = vec![0.0; m * r];
282            for (j, (e, col)) in keep.iter().enumerate() {
283                let step = col_step(delta, *e);
284                for i in 0..m {
285                    qm[i * r + j] = col[i] as f64 * step;
286                }
287            }
288            let htq = kron_eye_apply(&h, r_prev, nk, &qm, r);
289            let hk = matmul_tn(&qm, &htq, m, r, r);
290            // Drop trailing (smallest-σ) columns while the quantized Gram is
291            // singular or ill-conditioned: the metric bookkeeping (and the
292            // refit) degrade like cond(Hk), and a near-dependent column buys
293            // no rate anyway.
294            let lk = match cholesky(&hk, r) {
295                Some(lk) if chol_cond_ok(&lk, r) => lk,
296                _ => {
297                    keep.pop();
298                    continue;
299                }
300            };
301            let b = matmul_tn(&htq, &c, m, r, cols);
302            // W = Hk⁻¹ B through an explicit inverse (Hk ≈ I, well conditioned)
303            // so the r×cols product is one GEMM instead of two triangular
304            // sweeps; projected energy = ⟨B, W⟩.
305            let mut hinv = vec![0.0; r * r];
306            for i in 0..r {
307                hinv[i * r + i] = 1.0;
308            }
309            solve_lower(&lk, &mut hinv, r, r);
310            solve_lower_t(&lk, &mut hinv, r, r);
311            let w = matmul(&hinv, &b, r, r, cols);
312            let proj: f64 = b.iter().zip(&w).map(|(x, y)| x * y).sum();
313            sse += (c_energy - proj).max(0.0);
314            c = w;
315            h = hk;
316            break;
317        }
318        let r = keep.len();
319        let mut qcore = vec![0i64; m * r];
320        let mut ecore = Vec::with_capacity(r);
321        for (j, (e, col)) in keep.iter().enumerate() {
322            ecore.push(*e);
323            for i in 0..m {
324                qcore[i * r + j] = col[i];
325            }
326        }
327        ranks.push(r);
328        exps.push(ecore);
329        qs.push(qcore);
330        r_prev = r;
331        if r == 0 {
332            c.clear();
333        }
334    }
335    // Last core: (r_{d−1}·n_d) × 1, step Δ, error in the H̃ metric.
336    let nd = *modes.last().unwrap();
337    if r_prev == 0 {
338        qs.push(Vec::new());
339    } else {
340        let m = r_prev * nd;
341        let ht = kron_eye(&h, r_prev, nd);
342        let q: Vec<i64> = c.iter().map(|&v| (v / delta).round() as i64).collect();
343        let e: Vec<f64> = c
344            .iter()
345            .zip(&q)
346            .map(|(&v, &qi)| v - qi as f64 * delta)
347            .collect();
348        let he = matmul(&ht, &e, m, m, 1);
349        sse += e.iter().zip(&he).map(|(a, b)| a * b).sum::<f64>().max(0.0);
350        qs.push(q);
351    }
352    ranks.push(1);
353    QPass {
354        ranks,
355        exps,
356        q: qs,
357        sse,
358    }
359}
360
361/// `min/max` of the Cholesky diagonal ≥ 1e-3, i.e. cond(Hk) ≲ 1e6.
362fn chol_cond_ok(l: &[f64], r: usize) -> bool {
363    let d: Vec<f64> = (0..r).map(|i| l[i * r + i]).collect();
364    let (mn, mx) = d
365        .iter()
366        .fold((f64::INFINITY, 0.0f64), |(a, b), &v| (a.min(v), b.max(v)));
367    mn >= 1e-3 * mx
368}
369
370fn candidate_count(
371    sig: &[f64],
372    delta: f64,
373    gd: bool,
374    m: usize,
375    n: usize,
376    max_rank: usize,
377) -> usize {
378    // A column with σ < Δ/2 quantizes to all zeros (|G_ij| ≤ 1 in the gauge).
379    let mut k = sig.iter().take_while(|&&s| s >= 0.35 * delta).count();
380    if gd {
381        k = k.min(gd_rank(sig, m, n));
382    }
383    k.min(max_rank)
384}
385
386/// Rebuild float cores from quantized integers — shared by encoder and decoder
387/// so both contract bit-identical numbers.
388fn rebuild_cores(
389    modes: &[usize],
390    ranks: &[usize],
391    exps: &[Vec<i32>],
392    q: &[Vec<i64>],
393    delta: f64,
394) -> Vec<Vec<f64>> {
395    let d = modes.len();
396    let mut cores = Vec::with_capacity(d);
397    for k in 0..d {
398        let r = ranks[k + 1];
399        let qk = &q[k];
400        if k + 1 < d {
401            let steps: Vec<f64> = exps[k].iter().map(|&e| col_step(delta, e)).collect();
402            cores.push(
403                qk.iter()
404                    .enumerate()
405                    .map(|(i, &v)| v as f64 * steps[i % r])
406                    .collect(),
407            );
408        } else {
409            cores.push(qk.iter().map(|&v| v as f64 * delta).collect());
410        }
411    }
412    cores
413}
414
415fn reconstruct(layout: &Layout, pass: &QPass, delta: f64) -> Vec<f64> {
416    if pass.ranks.contains(&0) {
417        return vec![0.0; layout.len()];
418    }
419    let cores = rebuild_cores(&layout.modes, &pass.ranks, &pass.exps, &pass.q, delta);
420    layout.from_tensor(&contract(&layout.modes, &pass.ranks, &cores))
421}
422
423/// Round to the output dtype (what the decoder hands back).
424fn cast(v: f64, dt: Dtype) -> f64 {
425    match dt {
426        Dtype::F32 => v as f32 as f64,
427        Dtype::F64 => v,
428    }
429}
430
431/// Find the largest `Δ` with pass SSE ≤ `target_sse`.
432fn search_delta(
433    tensor: &[f64],
434    modes: &[usize],
435    cfg: &Config,
436    target_sse: f64,
437    guess: f64,
438    passes: &mut usize,
439) -> (f64, QPass) {
440    let mut run = |delta: f64| {
441        *passes += 1;
442        quantized_pass(tensor, modes, delta, cfg.gavish_donoho, cfg.max_rank)
443    };
444    let mut lo: Option<(f64, f64, QPass)> = None; // feasible (delta, sse, pass)
445    let mut hi: Option<(f64, f64)> = None; // infeasible
446                                           // Lowest-SSE pass seen, returned if the target is unreachable (rank cap).
447    let mut best_infeasible: Option<(f64, f64, QPass)> = None;
448    let mut delta = guess;
449    for _ in 0..14 {
450        let p = run(delta);
451        let s = p.sse;
452        if s <= target_sse {
453            if lo.as_ref().is_none_or(|l| delta > l.0) {
454                lo = Some((delta, s, p));
455            }
456        } else {
457            if hi.is_none_or(|h| delta < h.0) {
458                hi = Some((delta, s));
459            }
460            if best_infeasible.as_ref().is_none_or(|b| s < b.1) {
461                best_infeasible = Some((delta, s, p));
462            } else if lo.is_none() {
463                // Shrinking Δ stopped helping: the rank cap binds.
464                break;
465            }
466        }
467        delta = match (&lo, &hi) {
468            (Some((dl, sl, _)), Some((dh, sh))) => {
469                if dh / dl < 1.03 {
470                    break;
471                }
472                // log–log secant inside the bracket, kept away from the ends.
473                let (a, b) = (dl.ln(), dh.ln());
474                let (fa, fb) = ((sl.max(1e-300)).ln(), sh.ln());
475                let t = if fb > fa {
476                    ((target_sse.ln() - fa) / (fb - fa)).clamp(0.15, 0.85)
477                } else {
478                    0.5
479                };
480                (a + t * (b - a)).exp()
481            }
482            (Some((dl, sl, _)), None) => {
483                if *sl <= 0.0 {
484                    dl * 4.0
485                } else {
486                    dl * (target_sse / sl).sqrt().clamp(1.05, 8.0)
487                }
488            }
489            (None, Some((dh, sh))) => dh * (target_sse / sh).sqrt().clamp(0.1, 0.95),
490            (None, None) => unreachable!(),
491        };
492    }
493    match lo {
494        Some((d, _, p)) => (d, p),
495        None => {
496            let (d, _, p) = best_infeasible.expect("at least one pass ran");
497            (d, p)
498        }
499    }
500}
501
502/// Sparse max-abs corrections: positions, integer multiples of `2·bound`
503/// (0 = raw escape), and the escaped raw values.
504struct Residual {
505    bound: f64,
506    pos: Vec<usize>,
507    vals: Vec<i64>,
508    raw: Vec<f64>,
509}
510
511struct Built {
512    layout: Layout,
513    delta: f64,
514    pass: QPass,
515}
516
517/// Serialize; returns (bytes, header bytes, core bytes, residual bytes).
518fn write_stream(
519    dt: Dtype,
520    b: &Built,
521    residual: Option<&Residual>,
522) -> (Vec<u8>, usize, usize, usize) {
523    let mut w = ByteWriter::new();
524    w.bytes(MAGIC);
525    w.u8(match dt {
526        Dtype::F32 => 0,
527        Dtype::F64 => 1,
528    });
529    w.u8(match b.layout.order {
530        DigitOrder::RowMajor => 0,
531        DigitOrder::Morton => 1,
532    });
533    w.u8(match b.layout.pad {
534        PadPolicy::MixedRadix => 0,
535        PadPolicy::Pow2 => 1,
536    });
537    w.varint(b.layout.dims.len() as u64);
538    for &n in &b.layout.dims {
539        w.varint(n as u64);
540    }
541    w.f64(b.delta);
542    let d = b.layout.modes.len();
543    for k in 1..d {
544        w.varint(b.pass.ranks[k] as u64);
545    }
546    for k in 0..d.saturating_sub(1) {
547        let mut prev = 0i64;
548        for &e in &b.pass.exps[k] {
549            w.svarint(e as i64 - prev);
550            prev = e as i64;
551        }
552    }
553    let header = w.buf.len();
554    // A zero rank anywhere means an all-zero tensor: no core payload (the
555    // decoder expects none).
556    let blocks: Vec<&[i64]> = if b.pass.ranks.contains(&0) {
557        Vec::new()
558    } else {
559        b.pass.q.iter().map(|v| v.as_slice()).collect()
560    };
561    encode_blocks(&mut w, &blocks);
562    let core = w.buf.len() - header;
563    match residual {
564        None => w.u8(0),
565        Some(Residual {
566            bound,
567            pos,
568            vals,
569            raw,
570        }) => {
571            w.u8(1);
572            w.f64(*bound);
573            w.varint(pos.len() as u64);
574            let mut gaps = Vec::with_capacity(pos.len());
575            let mut prev = 0usize;
576            for (i, &p) in pos.iter().enumerate() {
577                gaps.push(if i == 0 { p } else { p - prev - 1 } as i64);
578                prev = p;
579            }
580            encode_blocks(&mut w, &[&gaps, vals.as_slice()]);
581            for &v in raw {
582                match dt {
583                    Dtype::F32 => w.bytes(&(v as f32).to_le_bytes()),
584                    Dtype::F64 => w.bytes(&v.to_le_bytes()),
585                }
586            }
587        }
588    }
589    let residual_bytes = w.buf.len() - header - core;
590    (w.buf, header, core, residual_bytes)
591}
592
593fn errors(orig: &[f64], rec: &[f64]) -> (f64, f64) {
594    let mut s = 0.0;
595    let mut mx: f64 = 0.0;
596    for (a, b) in orig.iter().zip(rec) {
597        let e = a - b;
598        s += e * e;
599        mx = mx.max(e.abs());
600    }
601    ((s / orig.len() as f64).sqrt(), mx)
602}
603
604/// Encode an f32 array of shape `dims` (row-major).
605pub fn encode_f32(data: &[f32], dims: &[usize], cfg: &Config) -> (Vec<u8>, EncodeStats) {
606    let x: Vec<f64> = data.iter().map(|&v| v as f64).collect();
607    encode_impl(&x, dims, cfg, Dtype::F32)
608}
609
610/// Encode an f64 array of shape `dims` (row-major).
611pub fn encode_f64(data: &[f64], dims: &[usize], cfg: &Config) -> (Vec<u8>, EncodeStats) {
612    encode_impl(data, dims, cfg, Dtype::F64)
613}
614
615fn encode_impl(x: &[f64], dims: &[usize], cfg: &Config, dt: Dtype) -> (Vec<u8>, EncodeStats) {
616    let n: usize = dims.iter().product();
617    assert_eq!(x.len(), n, "encode: data length != product of dims");
618    assert!(x.iter().all(|v| v.is_finite()), "encode: non-finite input");
619    let layout = Layout::new(dims, cfg.order, cfg.pad);
620    let tensor = layout.to_tensor(&layout.pad_grid(x));
621    let (mn, mx) = x
622        .iter()
623        .fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), &v| {
624            (a.min(v), b.max(v))
625        });
626    let range = (mx - mn).max(f64::MIN_POSITIVE);
627    let ntens = layout.tensor_len() as f64;
628    let mut passes = 0usize;
629
630    let l2_build = |rmse: f64, guess: f64, passes: &mut usize| -> Built {
631        let mut target = rmse * rmse * ntens;
632        let mut g = guess;
633        let mut out = None;
634        for _ in 0..4 {
635            let (delta, pass) = search_delta(&tensor, &layout.modes, cfg, target, g, passes);
636            let rec: Vec<f64> = reconstruct(&layout, &pass, delta)
637                .iter()
638                .map(|&v| cast(v, dt))
639                .collect();
640            let (true_rmse, _) = errors(x, &rec);
641            // Stop if met, or if the search could not even meet its own
642            // (predicted) target — tightening further cannot help.
643            let ok = true_rmse <= rmse || pass.sse > target;
644            out = Some(Built {
645                layout: layout.clone(),
646                delta,
647                pass,
648            });
649            if ok {
650                break;
651            }
652            target *= (rmse / true_rmse).powi(2) * 0.98;
653            g = delta * 0.9;
654        }
655        out.unwrap()
656    };
657
658    let finish = |b: Built, residual: Option<Residual>, passes: usize| {
659        let (bytes, header, core, res) = write_stream(dt, &b, residual.as_ref());
660        let rec = match decode(&bytes).expect("self-decode") {
661            Decoded::F32 { data, .. } => data.iter().map(|&v| v as f64).collect::<Vec<_>>(),
662            Decoded::F64 { data, .. } => data,
663        };
664        let (rmse, max_abs) = errors(x, &rec);
665        let stats = EncodeStats {
666            bytes: bytes.len(),
667            bits_per_value: 8.0 * bytes.len() as f64 / n as f64,
668            header_bytes: header,
669            core_bytes: core,
670            residual_bytes: res,
671            n_residual: residual.as_ref().map_or(0, |r| r.pos.len()),
672            ranks: b.pass.ranks.clone(),
673            modes: layout.modes.clone(),
674            delta: b.delta,
675            passes,
676            rmse,
677            max_abs,
678            range,
679        };
680        (bytes, stats)
681    };
682
683    match cfg.bound {
684        ErrorBound::Rmse(r) | ErrorBound::Psnr(r) => {
685            let rmse = match cfg.bound {
686                ErrorBound::Psnr(p) => range / 10f64.powf(p / 20.0),
687                _ => r,
688            };
689            let b = l2_build(rmse, rmse * 12f64.sqrt(), &mut passes);
690            finish(b, None, passes)
691        }
692        ErrorBound::MaxAbs(bound) => {
693            let mut best: Option<(usize, Built, Residual)> = None;
694            let mut guess = bound;
695            let mut prev_f = 0.0;
696            // f = 0 is the residual-only candidate (TT quantized to zero): it
697            // wins when the field has no low-rank structure (noise).
698            let fracs: &[f64] = if cfg.residual_only {
699                &[0.0]
700            } else {
701                &[0.0, 0.25, 0.4, 0.6]
702            };
703            for &f in fracs {
704                let b = if f == 0.0 {
705                    passes += 1;
706                    let delta = 1e300;
707                    let pass = quantized_pass(&tensor, &layout.modes, delta, false, 0);
708                    Built {
709                        layout: layout.clone(),
710                        delta,
711                        pass,
712                    }
713                } else {
714                    if prev_f > 0.0 {
715                        guess *= f / prev_f;
716                    }
717                    prev_f = f;
718                    let b = l2_build(f * bound, guess, &mut passes);
719                    guess = b.delta;
720                    b
721                };
722                let rec = reconstruct(&layout, &b.pass, b.delta);
723                let mut pos = Vec::new();
724                let mut vals = Vec::new();
725                let mut raw = Vec::new();
726                for (i, (&xi, &ri)) in x.iter().zip(&rec).enumerate() {
727                    if (xi - cast(ri, dt)).abs() > bound {
728                        let k = ((xi - ri) / (2.0 * bound)).round() as i64;
729                        let cand = cast(ri + 2.0 * bound * k as f64, dt);
730                        pos.push(i);
731                        if k != 0 && (xi - cand).abs() <= bound {
732                            vals.push(k);
733                        } else {
734                            vals.push(0);
735                            raw.push(xi);
736                        }
737                    }
738                }
739                let res = Residual {
740                    bound,
741                    pos,
742                    vals,
743                    raw,
744                };
745                let (bytes, _, _, _) = write_stream(dt, &b, Some(&res));
746                if best.as_ref().is_none_or(|(s, _, _)| bytes.len() < *s) {
747                    best = Some((bytes.len(), b, res));
748                }
749            }
750            let (_, b, res) = best.unwrap();
751            finish(b, Some(res), passes)
752        }
753    }
754}
755
756/// Decode a stream produced by [`encode_f32`] / [`encode_f64`].
757pub fn decode(bytes: &[u8]) -> Result<Decoded, DecodeError> {
758    let mut r = ByteReader::new(bytes);
759    if r.take(4)? != MAGIC {
760        return Err(DecodeError::Invalid("magic"));
761    }
762    let dt = match r.u8()? {
763        0 => Dtype::F32,
764        1 => Dtype::F64,
765        _ => return Err(DecodeError::Invalid("dtype")),
766    };
767    let order = match r.u8()? {
768        0 => DigitOrder::RowMajor,
769        1 => DigitOrder::Morton,
770        _ => return Err(DecodeError::Invalid("order")),
771    };
772    let pad = match r.u8()? {
773        0 => PadPolicy::MixedRadix,
774        1 => PadPolicy::Pow2,
775        _ => return Err(DecodeError::Invalid("pad")),
776    };
777    let nd = r.varint()? as usize;
778    if nd == 0 || nd > 16 {
779        return Err(DecodeError::Invalid("ndims"));
780    }
781    let dims: Vec<usize> = (0..nd)
782        .map(|_| Ok(r.varint()? as usize))
783        .collect::<Result<_, DecodeError>>()?;
784    if dims.contains(&0)
785        || dims
786            .iter()
787            .try_fold(1usize, |a, &n| a.checked_mul(n))
788            .is_none()
789    {
790        return Err(DecodeError::Invalid("dims"));
791    }
792    let layout = Layout::new(&dims, order, pad);
793    let delta = r.f64()?;
794    let d = layout.modes.len();
795    let mut ranks = vec![1usize];
796    for _ in 1..d {
797        let rk = r.varint()? as usize;
798        if rk > 1 << 16 {
799            return Err(DecodeError::Invalid("rank"));
800        }
801        ranks.push(rk);
802    }
803    ranks.push(1);
804    let mut exps = Vec::new();
805    for k in 0..d.saturating_sub(1) {
806        let mut prev = 0i64;
807        let mut e = Vec::with_capacity(ranks[k + 1]);
808        for _ in 0..ranks[k + 1] {
809            prev += r.svarint()?;
810            e.push(i32::try_from(prev).map_err(|_| DecodeError::Invalid("exponent"))?);
811        }
812        exps.push(e);
813    }
814    let any_zero = ranks.contains(&0);
815    let lens: Vec<usize> = (0..d)
816        .map(|k| {
817            if any_zero {
818                0
819            } else {
820                ranks[k] * layout.modes[k] * ranks[k + 1]
821            }
822        })
823        .collect();
824    let q = decode_blocks(&mut r, &lens)?;
825    let pass = QPass {
826        ranks,
827        exps,
828        q,
829        sse: 0.0,
830    };
831    let rec = reconstruct(&layout, &pass, delta);
832    let mut out: Vec<f64> = rec.iter().map(|&v| cast(v, dt)).collect();
833    if r.u8()? == 1 {
834        let bound = r.f64()?;
835        let nres = r.varint()? as usize;
836        if nres > out.len() {
837            return Err(DecodeError::Invalid("residual count"));
838        }
839        let bl = decode_blocks(&mut r, &[nres, nres])?;
840        let (gaps, vals) = (&bl[0], &bl[1]);
841        let mut p = 0usize;
842        for i in 0..nres {
843            p = if i == 0 {
844                gaps[0] as usize
845            } else {
846                p + 1 + gaps[i] as usize
847            };
848            if p >= out.len() {
849                return Err(DecodeError::Invalid("residual position"));
850            }
851            out[p] = if vals[i] == 0 {
852                match dt {
853                    Dtype::F32 => f32::from_le_bytes(r.take(4)?.try_into().unwrap()) as f64,
854                    Dtype::F64 => f64::from_le_bytes(r.take(8)?.try_into().unwrap()),
855                }
856            } else {
857                cast(rec[p] + 2.0 * bound * vals[i] as f64, dt)
858            };
859        }
860    }
861    Ok(match dt {
862        Dtype::F32 => Decoded::F32 {
863            dims,
864            data: out.iter().map(|&v| v as f32).collect(),
865        },
866        Dtype::F64 => Decoded::F64 { dims, data: out },
867    })
868}
869
870#[cfg(test)]
871mod tests {
872    use super::*;
873
874    fn field(n: usize) -> Vec<f64> {
875        (0..n)
876            .map(|i| {
877                let t = i as f64 / n as f64;
878                (30.0 * t).sin() * (-2.0 * t).exp()
879                    + 0.3 * (7.0 * t).cos()
880                    + if t > 0.6 { 0.5 } else { 0.0 }
881            })
882            .collect()
883    }
884
885    fn as_f64(d: Decoded) -> Vec<f64> {
886        match d {
887            Decoded::F32 { data, .. } => data.iter().map(|&v| v as f64).collect(),
888            Decoded::F64 { data, .. } => data,
889        }
890    }
891
892    #[test]
893    fn predicted_sse_matches_reconstruction() {
894        let x = field(1 << 12);
895        let layout = Layout::new(&[1 << 12], DigitOrder::RowMajor, PadPolicy::Pow2);
896        for delta in [1e-2, 1e-3, 1e-4] {
897            let p = quantized_pass(&x, &layout.modes, delta, false, 512);
898            let rec = reconstruct(&layout, &p, delta);
899            let sse: f64 = x.iter().zip(&rec).map(|(a, b)| (a - b) * (a - b)).sum();
900            // Agreement is limited by the Gram-route eigensolver, not the bookkeeping.
901            assert!((sse - p.sse).abs() <= 1e-3 * sse, "{sse} vs {}", p.sse);
902        }
903    }
904
905    #[test]
906    fn psnr_and_rmse_bounds_hold() {
907        let x = field(3000); // non power of two: 3000 = 3·5³·2³
908        for psnr in [40.0, 60.0, 80.0] {
909            let cfg = Config::new(ErrorBound::Psnr(psnr));
910            let (bytes, st) = encode_f64(&x, &[3000], &cfg);
911            let rec = as_f64(decode(&bytes).unwrap());
912            let (rmse, _) = errors(&x, &rec);
913            let achieved = 20.0 * (st.range / rmse).log10();
914            assert!(achieved >= psnr - 1e-9, "psnr {achieved} < {psnr}");
915            assert_eq!(st.bytes, bytes.len());
916        }
917    }
918
919    #[test]
920    fn max_abs_bound_holds_f32_2d() {
921        let (h, w) = (48usize, 80usize);
922        let x: Vec<f32> = (0..h * w)
923            .map(|i| {
924                let (a, b) = ((i / w) as f32 / h as f32, (i % w) as f32 / w as f32);
925                (6.0 * a).sin() * (4.0 * b).cos() + if a + b > 1.1 { 0.7 } else { 0.0 }
926            })
927            .collect();
928        for rel in [1e-2, 1e-3, 1e-4] {
929            let bound = rel * 2.4;
930            for order in [DigitOrder::Morton, DigitOrder::RowMajor] {
931                let mut cfg = Config::new(ErrorBound::MaxAbs(bound));
932                cfg.order = order;
933                let (bytes, st) = encode_f32(&x, &[h, w], &cfg);
934                let Decoded::F32 { data, dims } = decode(&bytes).unwrap() else {
935                    panic!()
936                };
937                assert_eq!(dims, vec![h, w]);
938                let mx = x
939                    .iter()
940                    .zip(&data)
941                    .map(|(a, b)| (a - b).abs() as f64)
942                    .fold(0.0, f64::max);
943                assert!(mx <= bound, "max {mx} > {bound}");
944                assert_eq!(st.max_abs, mx);
945            }
946        }
947    }
948
949    #[test]
950    fn deterministic_stream_and_decode() {
951        let x = field(1 << 11);
952        let cfg = Config::new(ErrorBound::MaxAbs(1e-3));
953        let (a, _) = encode_f64(&x, &[1 << 11], &cfg);
954        let (b, _) = encode_f64(&x, &[1 << 11], &cfg);
955        assert_eq!(a, b);
956        assert_eq!(decode(&a).unwrap(), decode(&a).unwrap());
957    }
958
959    #[test]
960    fn low_rank_signal_is_tiny() {
961        let n = 1 << 16;
962        let x: Vec<f64> = (0..n)
963            .map(|i| (0.001 * i as f64).sin() + 0.5 * (0.0003 * i as f64).cos())
964            .collect();
965        let (bytes, st) = encode_f64(&x, &[n], &Config::new(ErrorBound::Psnr(80.0)));
966        assert!(st.ranks.iter().all(|&r| r <= 6), "{:?}", st.ranks);
967        assert!(bytes.len() < 2000, "{} bytes", bytes.len());
968    }
969
970    #[test]
971    fn rank_cap_and_zero_rank_still_meet_max_abs() {
972        // White noise at rank cap 4: the L2 target is unreachable, so the
973        // residual stream must carry the bound. A huge Δ quantizes everything
974        // to zero (rank 0); the stream must still parse.
975        let mut seed = 9u64;
976        let x: Vec<f64> = (0..4096)
977            .map(|_| {
978                seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
979                (seed >> 11) as f64 / (1u64 << 53) as f64 - 0.5
980            })
981            .collect();
982        let mut cfg = Config::new(ErrorBound::MaxAbs(1e-3));
983        cfg.max_rank = 4;
984        let (bytes, st) = encode_f64(&x, &[4096], &cfg);
985        let rec = as_f64(decode(&bytes).unwrap());
986        assert!(errors(&x, &rec).1 <= 1e-3, "{}", st.max_abs);
987        let (bytes, _) = encode_f64(&x, &[4096], &Config::new(ErrorBound::MaxAbs(0.45)));
988        let rec = as_f64(decode(&bytes).unwrap());
989        assert!(errors(&x, &rec).1 <= 0.45);
990    }
991
992    #[test]
993    fn rejects_garbage() {
994        assert!(decode(b"nope").is_err());
995        let x = field(256);
996        let (bytes, _) = encode_f64(&x, &[256], &Config::new(ErrorBound::Psnr(50.0)));
997        assert!(decode(&bytes[..bytes.len() - 3]).is_err());
998    }
999}