Skip to main content

cram_codec/
wavcodec.rs

1//! The wavelet codec: CDF 9/7 pyramid → deadzone quantizer → context model →
2//! adaptive binary range coding, with a SPERR-style outlier stream for
3//! pointwise bounds and an integer 5/3 lossless mode.
4//!
5//! # Lossy path
6//!
7//! 1. Forward multi-level CDF 9/7 ([`crate::wavelet`]); every coefficient is
8//!    multiplied by its band's synthesis-basis norm, so one uniform step `Δ`
9//!    is equal-slope (MSE-matched) across bands.
10//! 2. Deadzone uniform quantizer `q = sgn(c)·⌊|c|/Δ + ρ⌋` (`ρ < ½` widens the
11//!    zero bin); reconstruction at the per-(class, |q|∈{1,2,≥3}) centroid
12//!    `|ĉ| = (|q| − δ)Δ`, the 30 `δ`s transmitted in 1/512 units.
13//! 3. Coefficients coded by [`crate::coefcode`] (hand contexts) or a learned
14//!    model ([`crate::learned`]).
15//! 4. Rate control: for a PSNR/RMSE target `Δ` is bisected on the *measured*
16//!    error of the actual decoder output.
17//!
18//! # Max-abs mode
19//!
20//! For a bound `b` the wavelet layer is coded at `Δ = f·b` for several `f`;
21//! the encoder decodes its own output and every cell with `|x − x̂| > b` gets a
22//! correction `k = round((x − x̂)/2b)` (then `|x − (x̂ + 2bk)| ≤ b`), coded as a
23//! context-modelled significance map plus `|k|`; cells a float cast still
24//! pushes over `b` are stored raw. The `f` with the smallest total wins and
25//! the bound is re-verified on the exact decoder output before returning.
26
27use crate::abac::{BitModel, Decoder, Encoder, UintModel};
28use crate::bitio::{ByteReader, ByteWriter, DecodeError};
29use crate::codec::{Decoded, Dtype, ErrorBound};
30use crate::coefcode::{
31    band_class, code_bands, code_bands_tiled, fit_predictors, read_preds, tile_live, write_preds,
32    Bits, HandModel, Pred, NCLASS,
33};
34use crate::wavelet::{
35    as3, forward_53, forward_97, inverse_53, inverse_97, Pyramid, MAX_LEVELS, MAX_LEVELS_1D,
36};
37
38/// Stream magic of the wavelet codec.
39pub const MAGIC: &[u8; 4] = b"TCW1";
40
41/// Which coefficient model codes the stream.
42#[derive(Clone, Debug, Default)]
43pub enum ModelChoice {
44    /// Hand-designed contexts.
45    #[default]
46    Hand,
47    /// A learned network (pretrained, or overfitted and shipped in-stream).
48    Learned(crate::learned::LearnedSpec),
49}
50
51/// Wavelet-codec options.
52#[derive(Clone, Debug)]
53pub struct WavConfig {
54    /// Error target.
55    pub bound: ErrorBound,
56    /// Deadzone rounding offset `ρ` (0.5 = plain rounding).
57    pub rho: f64,
58    /// Coefficient model.
59    pub model: ModelChoice,
60    /// Fast mode: step from a formula on the target ([`fast_step`]) with at
61    /// most a few corrective error checks instead of a bisection search.
62    pub fast: bool,
63    /// Code the final coefficients with the configured model, the hand model
64    /// and the hand model with all-zero tiles skipped (model tag 3; CPU
65    /// decoders only), and keep the smallest. Off by default so plain
66    /// wavelet streams stay GPU-decodable.
67    pub compact: bool,
68}
69
70impl WavConfig {
71    /// Defaults for a bound.
72    pub fn new(bound: ErrorBound) -> Self {
73        Self {
74            bound,
75            rho: 0.45,
76            model: ModelChoice::Hand,
77            fast: false,
78            compact: false,
79        }
80    }
81}
82
83/// What the encoder did.
84#[derive(Clone, Debug, Default)]
85pub struct WavStats {
86    /// Total bytes.
87    pub bytes: usize,
88    /// Bits per value.
89    pub bits_per_value: f64,
90    /// Coefficient payload bytes.
91    pub coef_bytes: usize,
92    /// Model bytes shipped in the stream (overfitted mode).
93    pub model_bytes: usize,
94    /// Outlier stream bytes.
95    pub outlier_bytes: usize,
96    /// Number of corrected cells.
97    pub n_outliers: usize,
98    /// Final step.
99    pub delta: f64,
100    /// Measured RMSE.
101    pub rmse: f64,
102    /// Measured max-abs error.
103    pub max_abs: f64,
104    /// Input range.
105    pub range: f64,
106    /// Rate-control passes.
107    pub passes: usize,
108}
109
110const NDELTA: usize = NCLASS * 3;
111
112fn max_levels(dims: &[usize]) -> usize {
113    if dims.iter().filter(|&&d| d > 1).count() <= 1 {
114        MAX_LEVELS_1D
115    } else {
116        MAX_LEVELS
117    }
118}
119
120/// Forward-transformed, band-weighted coefficients.
121pub struct Coeffs {
122    /// Pyramid.
123    pub p: Pyramid,
124    /// Weighted coefficients (volume layout).
125    pub c: Vec<f64>,
126    /// Weight of each cell's band.
127    pub w: Vec<f64>,
128    /// Class of each cell's band.
129    pub cls: Vec<u8>,
130    /// Mean removed before the transform (keeps the LL band small).
131    pub mean: f64,
132}
133
134impl Coeffs {
135    /// Transform `x` with shape `dims`.
136    pub fn new(x: &[f64], dims: &[usize]) -> Self {
137        let p = Pyramid::new(as3(dims), max_levels(dims));
138        let mean = x.iter().sum::<f64>() / x.len().max(1) as f64;
139        let mut c: Vec<f64> = x.iter().map(|&v| v - mean).collect();
140        forward_97(&p, &mut c);
141        let (w, cls) = band_maps(&p);
142        for (v, wi) in c.iter_mut().zip(&w) {
143            *v *= wi;
144        }
145        Self { p, c, w, cls, mean }
146    }
147}
148
149fn band_maps(p: &Pyramid) -> (Vec<f64>, Vec<u8>) {
150    let [_, ny, nx] = p.dims;
151    let mut w = vec![0.0; p.len()];
152    let mut cls = vec![0u8; p.len()];
153    for b in &p.bands {
154        let wb = p.band_weight(b);
155        let cb = band_class(b);
156        for z in b.start[0]..b.end[0] {
157            for y in b.start[1]..b.end[1] {
158                for x in b.start[2]..b.end[2] {
159                    let i = (z * ny + y) * nx + x;
160                    w[i] = wb;
161                    cls[i] = cb;
162                }
163            }
164        }
165    }
166    (w, cls)
167}
168
169/// Deadzone quantization with centroid offsets.
170pub fn quantize(co: &Coeffs, delta: f64, rho: f64) -> (Vec<i32>, [u8; NDELTA]) {
171    let inv = 1.0 / delta;
172    let mut sum = [0.0f64; NDELTA];
173    let mut cnt = [0u64; NDELTA];
174    let q: Vec<i32> =
175        co.c.iter()
176            .zip(&co.cls)
177            .map(|(&c, &k)| {
178                let a = c.abs() * inv;
179                let m = (a + rho).floor();
180                if m >= 1.0 {
181                    let m = m.min(1.0e9);
182                    let j = k as usize * 3 + (m as usize).min(3) - 1;
183                    sum[j] += m - a;
184                    cnt[j] += 1;
185                    if c < 0.0 {
186                        -(m as i32)
187                    } else {
188                        m as i32
189                    }
190                } else {
191                    0
192                }
193            })
194            .collect();
195    let mut d = [0u8; NDELTA];
196    for j in 0..NDELTA {
197        if cnt[j] > 0 {
198            d[j] = ((sum[j] / cnt[j] as f64).clamp(0.0, 0.498) * 512.0).round() as u8;
199        }
200    }
201    (q, d)
202}
203
204/// Dequantize + inverse transform → reconstruction (f64, row-major).
205pub fn reconstruct(p: &Pyramid, q: &[i32], delta: f64, d: &[u8; NDELTA], mean: f64) -> Vec<f64> {
206    let (w, cls) = band_maps(p);
207    let mut c: Vec<f64> = q
208        .iter()
209        .zip(w.iter().zip(&cls))
210        .map(|(&v, (&wi, &k))| {
211            if v == 0 {
212                return 0.0;
213            }
214            let m = v.unsigned_abs() as f64;
215            let j = k as usize * 3 + (v.unsigned_abs() as usize).min(3) - 1;
216            let r = (m - d[j] as f64 / 512.0) * delta / wi;
217            if v < 0 {
218                -r
219            } else {
220                r
221            }
222        })
223        .collect();
224    inverse_97(p, &mut c);
225    for v in c.iter_mut() {
226        *v += mean;
227    }
228    c
229}
230
231fn cast(y: &[f64], dt: Dtype) -> Vec<f64> {
232    match dt {
233        Dtype::F32 => y.iter().map(|&v| v as f32 as f64).collect(),
234        Dtype::F64 => y.to_vec(),
235    }
236}
237
238fn err_stats(x: &[f64], y: &[f64]) -> (f64, f64) {
239    let mut se = 0.0;
240    let mut mx = 0.0f64;
241    for (a, b) in x.iter().zip(y) {
242        let e = a - b;
243        se += e * e;
244        mx = mx.max(e.abs());
245    }
246    ((se / x.len() as f64).sqrt(), mx)
247}
248
249fn range_of(x: &[f64]) -> f64 {
250    let (lo, hi) = x
251        .iter()
252        .fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), &v| {
253            (a.min(v), b.max(v))
254        });
255    hi - lo
256}
257
258/// Model tag of the hand model with the tile layer ([`code_bands_tiled`]).
259const TAG_HAND_TILED: u8 = 3;
260
261fn code_coefs(p: &Pyramid, q: &[i32], model: &ModelChoice, w: &mut ByteWriter) -> usize {
262    code_coefs_opt(p, q, model, false, w)
263}
264
265/// Coefficient section; `compact` keeps the smallest of the configured
266/// model, the hand model and the tiled hand model. Returns the model bytes.
267fn code_coefs_opt(
268    p: &Pyramid,
269    q: &[i32],
270    model: &ModelChoice,
271    compact: bool,
272    w: &mut ByteWriter,
273) -> usize {
274    let preds: Vec<Pred> = if std::env::var("TC_NOPRED").is_ok() {
275        vec![Pred::default(); p.bands.len()]
276    } else {
277        fit_predictors(p, q)
278    };
279    write_preds(w, &preds);
280    // (model tag + shipped model, payload) of one coder; None = tiled hand
281    let body = |m: Option<&ModelChoice>| -> (Vec<u8>, Vec<u8>) {
282        let mut h = ByteWriter::new();
283        let mut e = Encoder::new();
284        let mut qq = q.to_vec();
285        match m {
286            Some(ModelChoice::Hand) => {
287                h.u8(0);
288                code_bands(p, &mut qq, &preds, &mut e, &mut HandModel::default());
289            }
290            Some(ModelChoice::Learned(spec)) => {
291                let mut mm = spec.prepare(p, q, &preds, &mut h);
292                code_bands(p, &mut qq, &preds, &mut e, &mut mm);
293            }
294            None => {
295                h.u8(TAG_HAND_TILED);
296                let live = tile_live(p, q, &preds);
297                code_bands_tiled(
298                    p,
299                    &mut qq,
300                    &preds,
301                    &mut e,
302                    &mut HandModel::default(),
303                    Some(&live),
304                );
305            }
306        }
307        (h.buf, e.finish())
308    };
309    let mut opts = vec![body(Some(model))];
310    if compact {
311        if !matches!(model, ModelChoice::Hand) {
312            opts.push(body(Some(&ModelChoice::Hand)));
313        }
314        opts.push(body(None));
315    }
316    let (h, pay) = opts
317        .into_iter()
318        .min_by_key(|(h, p)| h.len() + p.len())
319        .unwrap();
320    w.bytes(&h);
321    w.block(&pay);
322    h.len()
323}
324
325fn decode_coefs(p: &Pyramid, r: &mut ByteReader) -> Result<Vec<i32>, DecodeError> {
326    let preds = read_preds(r, p.bands.len())?;
327    let tag = r.u8()?;
328    let mut q = vec![0i32; p.len()];
329    match tag {
330        TAG_HAND_TILED => {
331            let buf = r.block()?;
332            let mut d = Decoder::new(buf);
333            code_bands_tiled(p, &mut q, &preds, &mut d, &mut HandModel::default(), None);
334            if d.overrun() {
335                return Err(DecodeError::Truncated);
336            }
337        }
338        0 => {
339            let buf = r.block()?;
340            let mut d = Decoder::new(buf);
341            code_bands(p, &mut q, &preds, &mut d, &mut HandModel::default());
342            if d.overrun() {
343                return Err(DecodeError::Truncated);
344            }
345        }
346        _ => {
347            let mut m = crate::learned::read_model(tag, r)?;
348            let buf = r.block()?;
349            let mut d = Decoder::new(buf);
350            code_bands(p, &mut q, &preds, &mut d, &mut m);
351            if d.overrun() {
352                return Err(DecodeError::Truncated);
353            }
354        }
355    }
356    Ok(q)
357}
358
359// ------------------------------------------------------------------ outliers
360
361struct OutlierModel {
362    flag: [BitModel; 8],
363    sign: BitModel,
364    mag: UintModel,
365}
366
367impl Default for OutlierModel {
368    fn default() -> Self {
369        Self {
370            flag: [BitModel::new(2000); 8],
371            sign: BitModel::default(),
372            mag: UintModel::default(),
373        }
374    }
375}
376
377fn code_outliers<B: Bits>(dims: [usize; 3], k: &mut [i32], b: &mut B) {
378    let mut m = OutlierModel::default();
379    let [nz, ny, nx] = dims;
380    for z in 0..nz {
381        for y in 0..ny {
382            for x in 0..nx {
383                let i = (z * ny + y) * nx + x;
384                let c = (x > 0 && k[i - 1] != 0) as usize
385                    | ((y > 0 && k[i - nx] != 0) as usize) << 1
386                    | ((z > 0 && k[i - nx * ny] != 0) as usize) << 2;
387                if !b.bit(k[i] != 0, &mut m.flag[c]) {
388                    k[i] = 0;
389                    continue;
390                }
391                let neg = b.bit(k[i] < 0, &mut m.sign);
392                let a = b.uint(k[i].unsigned_abs().saturating_sub(1) as u64, &mut m.mag) as i32 + 1;
393                k[i] = if neg { -a } else { a };
394            }
395        }
396    }
397}
398
399/// Apply corrections `k` (step `2b`) to `y`, then cast.
400fn apply_outliers(y: &mut [f64], k: &[i32], b: f64) {
401    for (v, &ki) in y.iter_mut().zip(k) {
402        if ki != 0 {
403            *v += 2.0 * b * ki as f64;
404        }
405    }
406}
407
408// ------------------------------------------------------------------ encode
409
410struct Header {
411    dt: Dtype,
412    dims: Vec<usize>,
413    lossless: bool,
414}
415
416fn write_header(w: &mut ByteWriter, h: &Header) {
417    w.bytes(MAGIC);
418    w.u8(match h.dt {
419        Dtype::F32 => 0,
420        Dtype::F64 => 1,
421    });
422    w.varint(h.dims.len() as u64);
423    for &d in &h.dims {
424        w.varint(d as u64);
425    }
426    w.u8(h.lossless as u8);
427}
428
429fn encode_at(
430    x: &[f64],
431    co: &Coeffs,
432    h: &Header,
433    delta: f64,
434    cfg: &WavConfig,
435    maxabs: Option<f64>,
436) -> (Vec<u8>, WavStats, Vec<f64>) {
437    let (q, d) = quantize(co, delta, cfg.rho);
438    let mut w = ByteWriter::new();
439    write_header(&mut w, h);
440    w.f64(delta);
441    w.f64(co.mean);
442    w.bytes(&d);
443    let b0 = w.buf.len();
444    let model_bytes = code_coefs_opt(&co.p, &q, &cfg.model, cfg.compact, &mut w);
445    let coef_bytes = w.buf.len() - b0 - model_bytes;
446    let mut y = cast(&reconstruct(&co.p, &q, delta, &d, co.mean), h.dt);
447    let mut st = WavStats {
448        delta,
449        coef_bytes,
450        model_bytes,
451        ..Default::default()
452    };
453    if let Some(b) = maxabs {
454        let b0 = w.buf.len();
455        w.u8(1);
456        w.f64(b);
457        let k: Vec<i32> = x
458            .iter()
459            .zip(&y)
460            .map(|(&a, &v)| {
461                let r = a - v;
462                if r.abs() > b {
463                    (r / (2.0 * b)).round() as i32
464                } else {
465                    0
466                }
467            })
468            .collect();
469        let mut e = Encoder::new();
470        code_outliers(co.p.dims, &mut k.clone(), &mut e);
471        w.block(&e.finish());
472        apply_outliers(&mut y, &k, b);
473        let mut y = cast(&y, h.dt);
474        // raw escapes for cells a float cast still pushes past b
475        let esc: Vec<usize> = (0..x.len()).filter(|&i| (x[i] - y[i]).abs() > b).collect();
476        w.varint(esc.len() as u64);
477        let mut last = 0;
478        for &i in &esc {
479            w.varint((i - last) as u64);
480            last = i;
481            match h.dt {
482                Dtype::F32 => w.bytes(&(x[i] as f32).to_le_bytes()),
483                Dtype::F64 => w.f64(x[i]),
484            }
485            y[i] = x[i];
486        }
487        st.n_outliers = k.iter().filter(|&&v| v != 0).count() + esc.len();
488        st.outlier_bytes = w.buf.len() - b0;
489        let (rmse, mx) = err_stats(x, &y);
490        st.rmse = rmse;
491        st.max_abs = mx;
492        st.bytes = w.buf.len();
493        return (w.buf, st, y);
494    }
495    w.u8(0); // no outlier stream
496    let (rmse, mx) = err_stats(x, &y);
497    st.rmse = rmse;
498    st.max_abs = mx;
499    st.bytes = w.buf.len();
500    (w.buf, st, y)
501}
502
503/// Fast-mode step ratios `Δ / target`: RMSE targets (`Δ = k·rmse`) and
504/// max-abs bounds (`Δ = k·b`). Calibrated on the 19 seen fields.
505pub const FAST_K_RMSE: f64 = 4.0;
506/// See [`FAST_K_RMSE`].
507pub const FAST_K_MAXABS: f64 = 2.2;
508
509/// Step for an RMSE target: start from the formula `Δ = k·t`, then take
510/// log–log secant steps on the measured error (`rmse ∝ Δ^γ`), aiming 1% under
511/// the target, for at most `checks` trials. Returns the largest tried step
512/// that met the target (or the last one, halved, when none did) and the
513/// number of trials.
514pub fn fast_step(t: f64, checks: usize, rmse_at: impl Fn(f64) -> f64) -> (f64, usize) {
515    let k = std::env::var("TC_FAST_K")
516        .ok()
517        .and_then(|v| v.parse().ok())
518        .unwrap_or(FAST_K_RMSE);
519    let aim = 0.99 * t;
520    let mut d = k * t;
521    let mut prev: Option<(f64, f64)> = None;
522    let mut best: Option<f64> = None;
523    let mut n = 0;
524    while n < checks {
525        let e = rmse_at(d).max(f64::MIN_POSITIVE);
526        n += 1;
527        if e <= t && best.is_none_or(|b| d > b) {
528            best = Some(d);
529        }
530        if (e - aim).abs() <= 0.01 * t {
531            break;
532        }
533        let g = match prev {
534            Some((d0, e0)) if (e / e0).ln().abs() > 1e-6 => {
535                ((e / e0).ln() / (d / d0).ln()).clamp(0.2, 2.0)
536            }
537            _ => 1.0,
538        };
539        prev = Some((d, e));
540        d *= (aim / e).powf(1.0 / g);
541    }
542    (best.unwrap_or(d * 0.5), n)
543}
544
545/// Coefficient-domain estimate of [`trial_rmse`]: the band-weighted
546/// quantization error, without the inverse transform.
547fn coef_rmse(co: &Coeffs, delta: f64, rho: f64) -> f64 {
548    let (q, d) = quantize(co, delta, rho);
549    let mut se = 0.0;
550    for ((&c, &v), &k) in co.c.iter().zip(&q).zip(&co.cls) {
551        let r = if v == 0 {
552            0.0
553        } else {
554            let j = k as usize * 3 + (v.unsigned_abs() as usize).min(3) - 1;
555            (v.unsigned_abs() as f64 - d[j] as f64 / 512.0) * delta * (v.signum() as f64)
556        };
557        se += (c - r) * (c - r);
558    }
559    (se / co.c.len().max(1) as f64).sqrt()
560}
561
562/// Error of the lossy layer alone at step `delta` (no entropy coding).
563fn trial_rmse(x: &[f64], co: &Coeffs, delta: f64, rho: f64, dt: Dtype) -> f64 {
564    let (q, d) = quantize(co, delta, rho);
565    err_stats(x, &cast(&reconstruct(&co.p, &q, delta, &d, co.mean), dt)).0
566}
567
568fn encode_impl(x: &[f64], dims: &[usize], dt: Dtype, cfg: &WavConfig) -> (Vec<u8>, WavStats) {
569    assert_eq!(dims.iter().product::<usize>(), x.len());
570    let range = range_of(x);
571    let co = Coeffs::new(x, dims);
572    let h = Header {
573        dt,
574        dims: dims.to_vec(),
575        lossless: false,
576    };
577    let rmse_target = match cfg.bound {
578        ErrorBound::Rmse(r) => Some(r),
579        ErrorBound::Psnr(db) => Some(range * 10f64.powf(-db / 20.0)),
580        ErrorBound::MaxAbs(_) => None,
581    };
582    let (bytes, mut st) = if let Some(t) = rmse_target {
583        let t = t.max(f64::MIN_POSITIVE);
584        if cfg.fast {
585            // secant on the coefficient-domain error, then verify for real
586            let (mut d, mut passes) = fast_step(t, 5, |d| coef_rmse(&co, d, cfg.rho));
587            for _ in 0..3 {
588                let e = trial_rmse(x, &co, d, cfg.rho, dt);
589                passes += 1;
590                if e <= t {
591                    break;
592                }
593                d *= 0.99 * t / e;
594            }
595            // last resort: halve until met (never taken on the seen fields)
596            while trial_rmse(x, &co, d, cfg.rho, dt) > t && passes < 60 {
597                d *= 0.5;
598                passes += 1;
599            }
600            let (b, mut s, _) = encode_at(x, &co, &h, d, cfg, None);
601            s.passes = passes;
602            s.range = range;
603            s.bits_per_value = 8.0 * b.len() as f64 / x.len() as f64;
604            return (b, s);
605        }
606        // bracket then bisect log Δ on the measured RMSE
607        let mut lo = t * 0.5;
608        let mut hi = t * 8.0;
609        let mut passes = 0;
610        while trial_rmse(x, &co, lo, cfg.rho, dt) > t && passes < 60 {
611            hi = lo;
612            lo *= 0.5;
613            passes += 1;
614        }
615        while trial_rmse(x, &co, hi, cfg.rho, dt) <= t && passes < 60 {
616            lo = hi;
617            hi *= 2.0;
618            passes += 1;
619        }
620        for _ in 0..14 {
621            let mid = (lo * hi).sqrt();
622            if trial_rmse(x, &co, mid, cfg.rho, dt) <= t {
623                lo = mid;
624            } else {
625                hi = mid;
626            }
627            passes += 1;
628        }
629        let (b, mut s, _) = encode_at(x, &co, &h, lo, cfg, None);
630        s.passes = passes;
631        (b, s)
632    } else {
633        let ErrorBound::MaxAbs(b) = cfg.bound else {
634            unreachable!()
635        };
636        if cfg.fast {
637            let k = std::env::var("TC_FAST_KM")
638                .ok()
639                .and_then(|v| v.parse().ok())
640                .unwrap_or(FAST_K_MAXABS);
641            let (bytes, mut s, _) = encode_at(x, &co, &h, k * b, cfg, Some(b));
642            s.passes = 1;
643            s.range = range;
644            s.bits_per_value = 8.0 * bytes.len() as f64 / x.len() as f64;
645            return (bytes, s);
646        }
647        // search the wavelet step with the hand model (cheap), then code the
648        // winner with the configured model
649        let hand = WavConfig {
650            model: ModelChoice::Hand,
651            ..cfg.clone()
652        };
653        let mut best: Option<(usize, f64)> = None;
654        let mut passes = 0;
655        for f in [1.0, 1.4, 1.8, 2.2, 2.7, 3.3, 4.0, 5.0] {
656            let (bytes, _, _) = encode_at(x, &co, &h, f * b, &hand, Some(b));
657            passes += 1;
658            if best.is_none_or(|(n, _)| bytes.len() < n) {
659                best = Some((bytes.len(), f));
660            }
661        }
662        let (bytes, mut s, _) = encode_at(x, &co, &h, best.unwrap().1 * b, cfg, Some(b));
663        s.passes = passes;
664        (bytes, s)
665    };
666    st.range = range;
667    st.bits_per_value = 8.0 * bytes.len() as f64 / x.len() as f64;
668    (bytes, st)
669}
670
671/// Encode f32 samples (row-major, `dims` 1D–3D).
672pub fn encode_f32(x: &[f32], dims: &[usize], cfg: &WavConfig) -> (Vec<u8>, WavStats) {
673    let x: Vec<f64> = x.iter().map(|&v| v as f64).collect();
674    encode_impl(&x, dims, Dtype::F32, cfg)
675}
676
677/// Encode f64 samples.
678pub fn encode_f64(x: &[f64], dims: &[usize], cfg: &WavConfig) -> (Vec<u8>, WavStats) {
679    encode_impl(x, dims, Dtype::F64, cfg)
680}
681
682/// Lossless coding of integer samples via the reversible 5/3 transform.
683pub fn encode_i32_lossless(x: &[i32], dims: &[usize]) -> Vec<u8> {
684    let p = Pyramid::new(as3(dims), max_levels(dims));
685    let mut c: Vec<i64> = x.iter().map(|&v| v as i64).collect();
686    forward_53(&p, &mut c);
687    let mut w = ByteWriter::new();
688    write_header(
689        &mut w,
690        &Header {
691            dt: Dtype::F32,
692            dims: dims.to_vec(),
693            lossless: true,
694        },
695    );
696    // 5/3 coefficients of i32 data fit in i32 except for extreme inputs
697    assert!(
698        c.iter().all(|v| v.unsigned_abs() < (1 << 30)),
699        "input range too large for lossless mode"
700    );
701    let q: Vec<i32> = c.iter().map(|&v| v as i32).collect();
702    code_coefs(&p, &q, &ModelChoice::Hand, &mut w);
703    w.buf
704}
705
706/// Decode a lossless stream from [`encode_i32_lossless`].
707pub fn decode_i32_lossless(bytes: &[u8]) -> Result<(Vec<usize>, Vec<i32>), DecodeError> {
708    let mut r = ByteReader::new(bytes);
709    let (_, dims, lossless) = read_header(&mut r)?;
710    if !lossless {
711        return Err(DecodeError::Invalid("not a lossless stream"));
712    }
713    let p = Pyramid::new(as3(&dims), max_levels(&dims));
714    let q = decode_coefs(&p, &mut r)?;
715    let mut c: Vec<i64> = q.iter().map(|&v| v as i64).collect();
716    inverse_53(&p, &mut c);
717    Ok((dims, c.iter().map(|&v| v as i32).collect()))
718}
719
720fn read_header(r: &mut ByteReader) -> Result<(Dtype, Vec<usize>, bool), DecodeError> {
721    if r.take(4)? != MAGIC {
722        return Err(DecodeError::Invalid("magic"));
723    }
724    let dt = match r.u8()? {
725        0 => Dtype::F32,
726        1 => Dtype::F64,
727        _ => return Err(DecodeError::Invalid("dtype")),
728    };
729    let nd = r.varint()? as usize;
730    if nd == 0 || nd > 3 {
731        return Err(DecodeError::Invalid("ndims"));
732    }
733    let mut dims = Vec::with_capacity(nd);
734    for _ in 0..nd {
735        dims.push(r.varint()? as usize);
736    }
737    if dims.contains(&0)
738        || dims
739            .iter()
740            .try_fold(1usize, |a, &d| a.checked_mul(d))
741            .is_none_or(|n| n as u64 > 1 << 34)
742    {
743        return Err(DecodeError::Invalid("dims"));
744    }
745    let lossless = r.u8()? != 0;
746    Ok((dt, dims, lossless))
747}
748
749/// Decode a lossy wavelet stream.
750pub fn decode(bytes: &[u8]) -> Result<Decoded, DecodeError> {
751    let mut r = ByteReader::new(bytes);
752    let (dt, dims, lossless) = read_header(&mut r)?;
753    if lossless {
754        return Err(DecodeError::Invalid(
755            "lossless stream: use decode_i32_lossless",
756        ));
757    }
758    let p = Pyramid::new(as3(&dims), max_levels(&dims));
759    let delta = r.f64()?;
760    let mean = r.f64()?;
761    let mut d = [0u8; NDELTA];
762    d.copy_from_slice(r.take(NDELTA)?);
763    let q = decode_coefs(&p, &mut r)?;
764    let mut y = cast(&reconstruct(&p, &q, delta, &d, mean), dt);
765    if r.u8()? == 1 {
766        let b = r.f64()?;
767        {
768            let buf = r.block()?;
769            let mut k = vec![0i32; p.len()];
770            let mut dd = Decoder::new(buf);
771            code_outliers(p.dims, &mut k, &mut dd);
772            apply_outliers(&mut y, &k, b);
773            y = cast(&y, dt);
774            let n = r.varint()? as usize;
775            let mut i = 0usize;
776            for _ in 0..n {
777                i += r.varint()? as usize;
778                if i >= y.len() {
779                    return Err(DecodeError::Invalid("escape index"));
780                }
781                y[i] = match dt {
782                    Dtype::F32 => f32::from_le_bytes(r.take(4)?.try_into().unwrap()) as f64,
783                    Dtype::F64 => r.f64()?,
784                };
785            }
786        }
787    }
788    Ok(match dt {
789        Dtype::F32 => Decoded::F32 {
790            dims,
791            data: y.iter().map(|&v| v as f32).collect(),
792        },
793        Dtype::F64 => Decoded::F64 { dims, data: y },
794    })
795}
796
797#[cfg(test)]
798mod tests {
799    use super::*;
800
801    fn field(d: [usize; 3]) -> Vec<f32> {
802        let mut v = Vec::new();
803        for i in 0..d[0] {
804            for j in 0..d[1] {
805                for k in 0..d[2] {
806                    let (x, y, z) = (
807                        i as f64 / d[0] as f64,
808                        j as f64 / d[1] as f64,
809                        k as f64 / d[2] as f64,
810                    );
811                    v.push(
812                        ((6.0 * x + 3.0 * y).sin() * (4.0 * z).cos()
813                            + 0.3 * (20.0 * x * y).sin()
814                            + if z > 0.6 { 0.5 } else { 0.0 }) as f32,
815                    );
816                }
817            }
818        }
819        v
820    }
821
822    #[test]
823    fn psnr_target_met_and_deterministic() {
824        let d = [20, 33, 47];
825        let x = field(d);
826        for db in [40.0, 70.0, 100.0] {
827            let cfg = WavConfig::new(ErrorBound::Psnr(db));
828            let (bytes, st) = encode_f32(&x, &d, &cfg);
829            let Decoded::F32 { data, .. } = decode(&bytes).unwrap() else {
830                panic!()
831            };
832            let xd: Vec<f64> = x.iter().map(|&v| v as f64).collect();
833            let yd: Vec<f64> = data.iter().map(|&v| v as f64).collect();
834            let (rmse, _) = err_stats(&xd, &yd);
835            assert_eq!(rmse, st.rmse, "decoder matches encoder self-check");
836            assert!(20.0 * (st.range / rmse).log10() >= db - 1e-9);
837            assert_eq!(encode_f32(&x, &d, &cfg).0, bytes);
838        }
839    }
840
841    #[test]
842    fn maxabs_bound_holds() {
843        let d = [16, 24, 40];
844        let x = field(d);
845        let range = 2.6;
846        for rel in [1e-2, 1e-4] {
847            let b = rel * range;
848            let (bytes, st) = encode_f32(&x, &d, &WavConfig::new(ErrorBound::MaxAbs(b)));
849            let Decoded::F32 { data, .. } = decode(&bytes).unwrap() else {
850                panic!()
851            };
852            let mx = x
853                .iter()
854                .zip(&data)
855                .map(|(&a, &v)| (a as f64 - v as f64).abs())
856                .fold(0.0, f64::max);
857            assert!(mx <= b, "{mx} > {b}");
858            assert_eq!(mx, st.max_abs);
859        }
860    }
861
862    #[test]
863    fn compact_round_trips_and_never_grows() {
864        // a smooth 2D field at a loose target leaves most tiles empty
865        let d = [1, 200, 260];
866        let x = field(d);
867        for b in [
868            ErrorBound::Psnr(45.0),
869            ErrorBound::Psnr(90.0),
870            ErrorBound::MaxAbs(1e-3),
871        ] {
872            let plain = encode_f32(&x, &d, &WavConfig::new(b)).0;
873            let cfg = WavConfig {
874                compact: true,
875                ..WavConfig::new(b)
876            };
877            let (bytes, st) = encode_f32(&x, &d, &cfg);
878            assert!(bytes.len() <= plain.len());
879            let Decoded::F32 { data, .. } = decode(&bytes).unwrap() else {
880                panic!()
881            };
882            let xd: Vec<f64> = x.iter().map(|&v| v as f64).collect();
883            let yd: Vec<f64> = data.iter().map(|&v| v as f64).collect();
884            assert_eq!(err_stats(&xd, &yd), (st.rmse, st.max_abs));
885        }
886        // the tiled coder on its own, including a 1D and a 3D pyramid
887        for d in [[1, 1, 3000], [1, 200, 260], [20, 33, 47]] {
888            let x: Vec<f64> = field(d).iter().map(|&v| v as f64).collect();
889            let co = Coeffs::new(&x, &d);
890            let (q, _) = quantize(&co, 0.05, 0.45);
891            let preds = fit_predictors(&co.p, &q);
892            let live = tile_live(&co.p, &q, &preds);
893            let mut e = Encoder::new();
894            let mut qq = q.clone();
895            code_bands_tiled(
896                &co.p,
897                &mut qq,
898                &preds,
899                &mut e,
900                &mut HandModel::default(),
901                Some(&live),
902            );
903            assert_eq!(qq, q);
904            let buf = e.finish();
905            let mut got = vec![0i32; q.len()];
906            let mut dd = Decoder::new(&buf);
907            code_bands_tiled(
908                &co.p,
909                &mut got,
910                &preds,
911                &mut dd,
912                &mut HandModel::default(),
913                None,
914            );
915            assert_eq!(got, q, "{d:?}");
916        }
917    }
918
919    #[test]
920    fn one_d_and_f64() {
921        let x: Vec<f64> = (0..5000)
922            .map(|i| (i as f64 * 0.01).sin() + (i as f64 * 0.37).cos() * 0.1)
923            .collect();
924        let (bytes, st) = encode_f64(&x, &[5000], &WavConfig::new(ErrorBound::Psnr(80.0)));
925        let Decoded::F64 { data, .. } = decode(&bytes).unwrap() else {
926            panic!()
927        };
928        assert_eq!(err_stats(&x, &data).0, st.rmse);
929    }
930
931    #[test]
932    fn lossless_round_trip() {
933        let d = [9, 17, 30];
934        let x: Vec<i32> = field(d)
935            .iter()
936            .enumerate()
937            .map(|(i, &v)| (v * 30000.0) as i32 + (i % 5) as i32)
938            .collect();
939        let bytes = encode_i32_lossless(&x, &d);
940        let (dd, y) = decode_i32_lossless(&bytes).unwrap();
941        assert_eq!(dd, d.to_vec());
942        assert_eq!(y, x);
943        assert!(bytes.len() < x.len() * 4);
944    }
945}