1use 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#[derive(Clone, Copy, Debug, PartialEq)]
63pub enum ErrorBound {
64 Rmse(f64),
66 Psnr(f64),
68 MaxAbs(f64),
70}
71
72#[derive(Clone, Copy, Debug, PartialEq)]
74pub struct Config {
75 pub bound: ErrorBound,
77 pub order: DigitOrder,
79 pub pad: PadPolicy,
81 pub gavish_donoho: bool,
83 pub max_rank: usize,
85 pub residual_only: bool,
89}
90
91impl Config {
92 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
107pub enum Dtype {
108 F32,
110 F64,
112}
113
114#[derive(Clone, Debug, PartialEq)]
116pub struct EncodeStats {
117 pub bytes: usize,
119 pub bits_per_value: f64,
121 pub header_bytes: usize,
123 pub core_bytes: usize,
125 pub residual_bytes: usize,
127 pub n_residual: usize,
129 pub ranks: Vec<usize>,
131 pub modes: Vec<usize>,
133 pub delta: f64,
135 pub passes: usize,
137 pub rmse: f64,
139 pub max_abs: f64,
141 pub range: f64,
143}
144
145#[derive(Clone, Debug, PartialEq)]
147pub enum Decoded {
148 F32 {
150 dims: Vec<usize>,
152 data: Vec<f32>,
154 },
155 F64 {
157 dims: Vec<usize>,
159 data: Vec<f64>,
161 },
162}
163
164#[derive(Clone, Debug)]
166struct QPass {
167 ranks: Vec<usize>,
168 exps: Vec<Vec<i32>>,
169 q: Vec<Vec<i64>>,
170 sse: f64,
171}
172
173fn col_step(delta: f64, e: i32) -> f64 {
175 const FRAC: [f64; 4] = [
176 1.0,
177 0.840_896_415_253_714_5, core::f64::consts::FRAC_1_SQRT_2,
179 0.594_603_557_501_360_5, ];
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 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 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 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 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 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 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
361fn 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 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
386fn 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
423fn cast(v: f64, dt: Dtype) -> f64 {
425 match dt {
426 Dtype::F32 => v as f32 as f64,
427 Dtype::F64 => v,
428 }
429}
430
431fn 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; let mut hi: Option<(f64, f64)> = None; 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 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 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
502struct 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
517fn 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 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
604pub 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
610pub 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 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 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
756pub 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 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); 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 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}