Skip to main content

leodos_protocols/application/compression/
ccsds123.rs

1//! CCSDS 123.0-B-2 Low-Complexity Lossless and Near-Lossless
2//! Multispectral and Hyperspectral Image Compression.
3//!
4//! Implements the predictor and sample-adaptive entropy coder for
5//! 3D image data (NX x NY x NZ). Supports lossless compression
6//! with full or reduced prediction mode, wide/narrow
7//! neighbor-oriented or column-oriented local sums, and default
8//! weight initialization.
9//!
10//! # Limitations
11//!
12//! - Sample-adaptive entropy coder only (no hybrid/block-adaptive)
13//! - BSQ encoding order only
14//! - Lossless mode only (no near-lossless quantization)
15//! - Default weight initialization only (no custom)
16//! - No supplementary information tables
17//! - Dynamic range D limited to 2..=16
18//! - P (prediction bands) limited to 0..=15
19//! - Image dimensions capped by `MAX_DIM`
20
21/// Maximum number of prediction bands.
22const MAX_P: usize = 15;
23/// Maximum weight vector length (3 directional + MAX_P spectral).
24const MAX_CZ: usize = MAX_P + 3;
25
26/// Prediction mode.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum PredictionMode {
29    /// Full prediction (directional + spectral).
30    Full,
31    /// Reduced prediction (spectral only).
32    Reduced,
33}
34
35/// Local sum type.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum LocalSumType {
38    /// Wide neighbor-oriented local sums.
39    WideNeighbor,
40    /// Narrow neighbor-oriented local sums.
41    NarrowNeighbor,
42    /// Wide column-oriented local sums.
43    WideColumn,
44    /// Narrow column-oriented local sums.
45    NarrowColumn,
46}
47
48/// Compressor configuration.
49#[derive(Debug, Clone)]
50pub struct Config {
51    /// Image width (number of columns).
52    pub nx: u16,
53    /// Image height (number of rows).
54    pub ny: u16,
55    /// Number of spectral bands.
56    pub nz: u16,
57    /// Dynamic range in bits (2..=16).
58    pub dynamic_range: u8,
59    /// Whether samples are signed.
60    pub signed_samples: bool,
61    /// Number of prediction bands (0..=15).
62    pub p: u8,
63    /// Prediction mode.
64    pub mode: PredictionMode,
65    /// Local sum type.
66    pub local_sum_type: LocalSumType,
67    /// Weight component resolution (4..=19).
68    pub omega: u8,
69    /// Register size R (max{32, D+Omega+2} .. 64).
70    pub register_size: u8,
71    /// Weight update scaling exponent change interval
72    /// (power of 2 in range 2^4..=2^11).
73    pub t_inc: u16,
74    /// Weight update initial parameter (-6..=9).
75    pub v_min: i8,
76    /// Weight update final parameter (-6..=9).
77    pub v_max: i8,
78    /// Unary length limit for entropy coder (8..=32).
79    pub u_max: u8,
80    /// Initial count exponent (1..=8).
81    pub gamma_0: u8,
82    /// Rescaling counter size (max{4, gamma_0+1}..=11).
83    pub gamma_star: u8,
84    /// Accumulator initialization constant (optional).
85    /// If None, per-band k''_z values default to 0.
86    pub accum_init_k: Option<u8>,
87}
88
89/// Compression/decompression error.
90#[derive(Debug, thiserror::Error)]
91pub enum Error {
92    /// Invalid configuration parameter.
93    #[error("Invalid configuration parameter")]
94    InvalidConfig,
95    /// Output buffer too small.
96    #[error("Output buffer too small")]
97    OutputFull,
98    /// Input bitstream truncated or malformed.
99    #[error("Input bitstream truncated or malformed")]
100    Truncated,
101    /// Image dimensions exceed compile-time limits.
102    #[error("Image dimensions exceed compile-time limits")]
103    ImageTooLarge,
104    /// Scratch buffer too small.
105    #[error("Scratch buffer too small")]
106    ScratchTooSmall,
107}
108
109/// Compute the required scratch buffer size (in `i64` elements)
110/// for a given image configuration.
111///
112/// The caller must provide `&mut [i64]` of at least this many
113/// elements to `compress` / `decompress`.
114pub fn scratch_len(nx: usize, nz: usize) -> usize {
115    // sr_buf: nz * 2 * nx
116    // weights: nz * MAX_CZ
117    // accumulator: nz  (stored as i64, reinterpreted as u64)
118    nz * 2 * nx + nz * MAX_CZ + nz
119}
120
121impl Config {
122    fn validate(&self) -> Result<(), Error> {
123        if self.nx == 0 || self.ny == 0 || self.nz == 0 {
124            return Err(Error::InvalidConfig);
125        }
126        let d = self.dynamic_range;
127        if d < 2 || d > 16 {
128            return Err(Error::InvalidConfig);
129        }
130        if self.p > 15 {
131            return Err(Error::InvalidConfig);
132        }
133        if self.omega < 4 || self.omega > 19 {
134            return Err(Error::InvalidConfig);
135        }
136        let r_min = core::cmp::max(32, d as u8 + self.omega + 2);
137        if self.register_size < r_min || self.register_size > 64 {
138            return Err(Error::InvalidConfig);
139        }
140        if !self.t_inc.is_power_of_two()
141            || self.t_inc < 16
142            || self.t_inc > 2048
143        {
144            return Err(Error::InvalidConfig);
145        }
146        if self.v_min < -6 || self.v_min > 9 {
147            return Err(Error::InvalidConfig);
148        }
149        if self.v_max < self.v_min || self.v_max > 9 {
150            return Err(Error::InvalidConfig);
151        }
152        if self.u_max < 8 || self.u_max > 32 {
153            return Err(Error::InvalidConfig);
154        }
155        if self.gamma_0 < 1 || self.gamma_0 > 8 {
156            return Err(Error::InvalidConfig);
157        }
158        let g_min = core::cmp::max(4, self.gamma_0 + 1);
159        if self.gamma_star < g_min || self.gamma_star > 11 {
160            return Err(Error::InvalidConfig);
161        }
162        if self.nx == 1
163            && self.mode == PredictionMode::Full
164        {
165            return Err(Error::InvalidConfig);
166        }
167        if self.nx == 1
168            && matches!(
169                self.local_sum_type,
170                LocalSumType::WideNeighbor
171                    | LocalSumType::NarrowNeighbor
172            )
173        {
174            return Err(Error::InvalidConfig);
175        }
176        Ok(())
177    }
178
179    fn d(&self) -> u32 {
180        self.dynamic_range as u32
181    }
182
183    fn s_min(&self) -> i64 {
184        if self.signed_samples {
185            -(1i64 << (self.d() - 1))
186        } else {
187            0
188        }
189    }
190
191    fn s_max(&self) -> i64 {
192        if self.signed_samples {
193            (1i64 << (self.d() - 1)) - 1
194        } else {
195            (1i64 << self.d()) - 1
196        }
197    }
198
199    fn s_mid(&self) -> i64 {
200        if self.signed_samples {
201            0
202        } else {
203            1i64 << (self.d() - 1)
204        }
205    }
206
207    /// Number of local differences used for prediction in band z.
208    fn c_z(&self, z: usize) -> usize {
209        let p_star = core::cmp::min(z, self.p as usize);
210        match self.mode {
211            PredictionMode::Reduced => p_star,
212            PredictionMode::Full => p_star + 3,
213        }
214    }
215}
216
217// ── Bit Writer ───────────────────────────────────────────
218
219struct BitWriter<'a> {
220    buf: &'a mut [u8],
221    pos: usize,
222    bit: u32,
223}
224
225impl<'a> BitWriter<'a> {
226    fn new(buf: &'a mut [u8]) -> Self {
227        Self { buf, pos: 0, bit: 0 }
228    }
229
230    fn write_bits(
231        &mut self,
232        value: u64,
233        n: u32,
234    ) -> Result<(), Error> {
235        for i in (0..n).rev() {
236            let b = ((value >> i) & 1) as u8;
237            if self.pos >= self.buf.len() {
238                return Err(Error::OutputFull);
239            }
240            self.buf[self.pos] |= b << (7 - self.bit);
241            self.bit += 1;
242            if self.bit == 8 {
243                self.bit = 0;
244                self.pos += 1;
245            }
246        }
247        Ok(())
248    }
249
250    /// Pad to byte boundary with zeros.
251    fn flush(&mut self) -> Result<(), Error> {
252        if self.bit > 0 {
253            self.bit = 0;
254            self.pos += 1;
255        }
256        Ok(())
257    }
258
259    fn bytes_written(&self) -> usize {
260        if self.bit > 0 {
261            self.pos + 1
262        } else {
263            self.pos
264        }
265    }
266}
267
268// ── Bit Reader ───────────────────────────────────────────
269
270struct BitReader<'a> {
271    buf: &'a [u8],
272    pos: usize,
273    bit: u32,
274}
275
276impl<'a> BitReader<'a> {
277    fn new(buf: &'a [u8]) -> Self {
278        Self { buf, pos: 0, bit: 0 }
279    }
280
281    fn read_bits(&mut self, n: u32) -> Result<u64, Error> {
282        let mut val = 0u64;
283        for _ in 0..n {
284            if self.pos >= self.buf.len() {
285                return Err(Error::Truncated);
286            }
287            let b = (self.buf[self.pos] >> (7 - self.bit)) & 1;
288            val = (val << 1) | b as u64;
289            self.bit += 1;
290            if self.bit == 8 {
291                self.bit = 0;
292                self.pos += 1;
293            }
294        }
295        Ok(val)
296    }
297
298}
299
300// ── Predictor ────────────────────────────────────────────
301
302/// Get sample from the image. Returns s[z][y*nx + x].
303fn get_sample(
304    image: &[u16],
305    nx: usize,
306    ny: usize,
307    nz: usize,
308    z: usize,
309    y: usize,
310    x: usize,
311) -> i64 {
312    let _ = (ny, nz);
313    image[z * ny * nx + y * nx + x] as i64
314}
315
316/// Default weight initialization (eq 33-34).
317fn init_weights_default(
318    cfg: &Config,
319    z: usize,
320    w: &mut [i64],
321) {
322    let p_star = core::cmp::min(z, cfg.p as usize);
323    let omega = cfg.omega as i64;
324
325    // Spectral weights
326    let c_z = cfg.c_z(z);
327    for j in 0..c_z {
328        w[j] = 0;
329    }
330
331    let dir_offset = match cfg.mode {
332        PredictionMode::Full => {
333            // Directional weights = 0
334            w[0] = 0;
335            w[1] = 0;
336            w[2] = 0;
337            3
338        }
339        PredictionMode::Reduced => 0,
340    };
341
342    if p_star > 0 {
343        // w^(1) = 7/8 * 2^omega
344        w[dir_offset] = (7 * (1i64 << omega)) / 8;
345        for i in 1..p_star {
346            w[dir_offset + i] = w[dir_offset + i - 1] / 8;
347        }
348    }
349}
350
351/// Compute weight update scaling exponent rho(t) (eq 50).
352fn scaling_exponent(cfg: &Config, t: usize) -> i64 {
353    let v_min = cfg.v_min as i64;
354    let v_max = cfg.v_max as i64;
355    let t_inc = cfg.t_inc as i64;
356    let nx = cfg.nx as i64;
357    let d = cfg.d() as i64;
358    let omega = cfg.omega as i64;
359
360    let base = v_min + ((t as i64 - nx) / t_inc);
361    let clamped = if base < v_min {
362        v_min
363    } else if base > v_max {
364        v_max
365    } else {
366        base
367    };
368    clamped + d - omega
369}
370
371/// mod*_R function (eq 4): R-bit two's complement modular.
372fn mod_star(x: i64, r: u32) -> i64 {
373    let two_r = 1i64 << r;
374    let half = 1i64 << (r - 1);
375    ((x + half).rem_euclid(two_r)) - half
376}
377
378/// clip function (eq 5).
379fn clip(x: i64, x_min: i64, x_max: i64) -> i64 {
380    if x < x_min {
381        x_min
382    } else if x > x_max {
383        x_max
384    } else {
385        x
386    }
387}
388
389/// sgn+ function (eq 7).
390fn sgn_plus(x: i64) -> i64 {
391    if x >= 0 { 1 } else { -1 }
392}
393
394/// Predicted sample value (eq 39).
395fn predicted_sample(s_tilde: i64) -> i64 {
396    // floor(s_tilde / 2)
397    if s_tilde >= 0 {
398        s_tilde / 2
399    } else {
400        (s_tilde - 1) / 2
401    }
402}
403
404/// Mapped quantizer index (eq 55-56) for lossless mode.
405fn mapped_quantizer_index(
406    cfg: &Config,
407    delta: i64,
408    s_hat: i64,
409    _t: usize,
410) -> u64 {
411    let s_min = cfg.s_min();
412    let s_max = cfg.s_max();
413
414    // theta (eq 56 with m_z(t)=0 for lossless)
415    let theta = core::cmp::min(s_hat - s_min, s_max - s_hat);
416
417    // eq 55 with m_z(t)=0
418    if delta.abs() > theta {
419        (delta.abs() + theta) as u64
420    } else if delta >= 0 {
421        2 * delta as u64
422    } else {
423        2 * (-delta) as u64 - 1
424    }
425}
426
427/// Inverse mapped quantizer index → prediction residual.
428fn unmap_quantizer_index(
429    cfg: &Config,
430    mapped: u64,
431    s_hat: i64,
432) -> i64 {
433    let s_min = cfg.s_min();
434    let s_max = cfg.s_max();
435    let theta =
436        core::cmp::min(s_hat - s_min, s_max - s_hat);
437
438    if mapped as i64 > 2 * theta {
439        let abs_val = mapped as i64 - theta;
440        // theta = min(s_hat-s_min, s_max-s_hat).
441        // If s_hat closer to s_min, large residual goes up (+).
442        // If s_hat closer to s_max, large residual goes down (-).
443        if s_hat - s_min <= s_max - s_hat {
444            abs_val
445        } else {
446            -abs_val
447        }
448    } else if mapped % 2 == 0 {
449        (mapped / 2) as i64
450    } else {
451        -(((mapped + 1) / 2) as i64)
452    }
453}
454
455// ── GPO2 Codeword (§5.4.3.2.2) ──────────────────────────
456
457/// Write GPO2 codeword R_k(j).
458fn write_gpo2(
459    w: &mut BitWriter,
460    j: u64,
461    k: u32,
462    u_max: u32,
463    d: u32,
464) -> Result<(), Error> {
465    let quotient = j >> k;
466    if quotient < u_max as u64 {
467        // quotient zeros, then a one, then k LSBs
468        for _ in 0..quotient {
469            w.write_bits(0, 1)?;
470        }
471        w.write_bits(1, 1)?;
472        if k > 0 {
473            w.write_bits(j & ((1u64 << k) - 1), k)?;
474        }
475    } else {
476        // u_max zeros, then D-bit representation of j
477        for _ in 0..u_max {
478            w.write_bits(0, 1)?;
479        }
480        w.write_bits(j, d)?;
481    }
482    Ok(())
483}
484
485/// Read GPO2 codeword R_k(j).
486fn read_gpo2(
487    r: &mut BitReader,
488    k: u32,
489    u_max: u32,
490    d: u32,
491) -> Result<u64, Error> {
492    let mut quotient = 0u64;
493    loop {
494        let bit = r.read_bits(1)?;
495        if bit == 1 {
496            break;
497        }
498        quotient += 1;
499        if quotient == u_max as u64 {
500            // Read D-bit value directly
501            return r.read_bits(d);
502        }
503    }
504    let remainder = if k > 0 { r.read_bits(k)? } else { 0 };
505    Ok((quotient << k) | remainder)
506}
507
508// ── Compress ─────────────────────────────────────────────
509
510/// Compress a 3D image using CCSDS 123.0-B-2.
511///
512/// `image` is in BSQ order: `image[z * ny * nx + y * nx + x]`.
513/// `scratch` must have at least `scratch_len(nx, nz)` elements.
514///
515/// Returns the number of bytes written to `out`.
516pub fn compress(
517    cfg: &Config,
518    image: &[u16],
519    out: &mut [u8],
520    scratch: &mut [i64],
521) -> Result<usize, Error> {
522    cfg.validate()?;
523
524    let nx = cfg.nx as usize;
525    let ny = cfg.ny as usize;
526    let nz = cfg.nz as usize;
527    let d = cfg.d();
528    let needed = scratch_len(nx, nz);
529
530    if scratch.len() < needed {
531        return Err(Error::ScratchTooSmall);
532    }
533    if image.len() < nx * ny * nz {
534        return Err(Error::Truncated);
535    }
536
537    // Zero output and scratch
538    for b in out.iter_mut() {
539        *b = 0;
540    }
541    for s in scratch[..needed].iter_mut() {
542        *s = 0;
543    }
544
545    // Partition scratch: sr_buf | weights | accumulator
546    let sr_len = nz * 2 * nx;
547    let w_len = nz * MAX_CZ;
548    let (sr_buf, rest) = scratch.split_at_mut(sr_len);
549    let (weights, acc_buf) = rest.split_at_mut(w_len);
550    // Store accumulator as i64, reinterpret
551    let acc = &mut acc_buf[..nz];
552
553    let mut bw = BitWriter::new(out);
554    write_header(cfg, &mut bw)?;
555
556    // Initialize weights
557    for z in 0..nz {
558        let cz = cfg.c_z(z);
559        let wbase = z * MAX_CZ;
560        let mut wslice = [0i64; MAX_CZ];
561        init_weights_default(cfg, z, &mut wslice);
562        for j in 0..cz {
563            weights[wbase + j] = wslice[j];
564        }
565    }
566
567    // Initialize entropy coder
568    let mut counter = 1u64 << cfg.gamma_0;
569    for z in 0..nz {
570        let k_z_pp = match cfg.accum_init_k {
571            Some(k) => k as u32,
572            None => 0,
573        };
574        let k_z_prime = if k_z_pp <= 30 - d {
575            k_z_pp
576        } else {
577            2 * k_z_pp + d - 30
578        };
579        acc[z] = ((3 * (1u64 << (k_z_prime + 6)) - 49)
580            * counter) as i64
581            >> 7;
582    }
583
584    for z in 0..nz {
585        for y in 0..ny {
586            let cur_row = y % 2;
587            let prev_row = (y + 1) % 2;
588
589            for x in 0..nx {
590                let t = y * nx + x;
591                let s = get_sample(image, nx, ny, nz, z, y, x);
592
593                if t == 0 {
594                    let coded = if cfg.signed_samples {
595                        (s - cfg.s_min()) as u64
596                    } else {
597                        s as u64
598                    };
599                    bw.write_bits(coded, d)?;
600                    sr_buf[z * 2 * nx + cur_row * nx + x] = s;
601                    continue;
602                }
603
604                let sigma = local_sum_with_buf(
605                    cfg, sr_buf, nx, z, y, x, cur_row,
606                    prev_row,
607                );
608
609                let mut u_vec = [0i64; MAX_CZ];
610                let cz = cfg.c_z(z);
611                if cz > 0 {
612                    build_u_with_buf(
613                        cfg, sr_buf, sigma, nx, z, y, x,
614                        cur_row, prev_row, &mut u_vec,
615                    );
616                }
617
618                let wbase = z * MAX_CZ;
619                let mut d_hat = 0i64;
620                for j in 0..cz {
621                    d_hat += weights[wbase + j] * u_vec[j];
622                }
623
624                let s_tilde = high_res_predicted_buf(
625                    cfg, d_hat, sigma, t, z, sr_buf, nx,
626                );
627                let s_hat = predicted_sample(s_tilde);
628                let delta = s - s_hat;
629                let mapped =
630                    mapped_quantizer_index(cfg, delta, s_hat, t);
631
632                let mut acc_u = acc[z] as u64;
633                encode_sample(
634                    cfg, &mut bw, mapped, &mut acc_u,
635                    &mut counter, t, z,
636                )?;
637                acc[z] = acc_u as i64;
638
639                sr_buf[z * 2 * nx + cur_row * nx + x] = s;
640
641                // Weight update
642                if cz > 0 {
643                    let e = 2 * s - s_tilde;
644                    let rho = scaling_exponent(cfg, t);
645                    let omega_min =
646                        -(1i64 << (cfg.omega as i64 + 2));
647                    let omega_max =
648                        (1i64 << (cfg.omega as i64 + 2)) - 1;
649
650                    for j in 0..cz {
651                        let inc = (sgn_plus(e)
652                            * shift_right(u_vec[j], rho)
653                            + 1)
654                            / 2;
655                        weights[wbase + j] = clip(
656                            weights[wbase + j] + inc,
657                            omega_min,
658                            omega_max,
659                        );
660                    }
661                }
662            }
663        }
664    }
665
666    bw.flush()?;
667    Ok(bw.bytes_written())
668}
669
670/// Arithmetic right-shift with floor: floor(x * 2^(-shift)).
671/// For positive shift, right-shifts. For negative, left-shifts.
672fn shift_right(x: i64, shift: i64) -> i64 {
673    if shift >= 0 {
674        x >> shift
675    } else {
676        x << (-shift)
677    }
678}
679
680/// Local sum using the 2-row buffer layout.
681fn local_sum_with_buf(
682    cfg: &Config,
683    sr_buf: &[i64],
684    nx: usize,
685    z: usize,
686    y: usize,
687    x: usize,
688    cur_row: usize,
689    prev_row: usize,
690) -> i64 {
691    let base = z * 2 * nx;
692    let get_cur =
693        |xx: usize| -> i64 { sr_buf[base + cur_row * nx + xx] };
694    let get_prev =
695        |xx: usize| -> i64 { sr_buf[base + prev_row * nx + xx] };
696    // For previous band access:
697    let get_prev_band = |zz: usize, row: usize, xx: usize| -> i64 {
698        sr_buf[zz * 2 * nx + row * nx + xx]
699    };
700
701    match cfg.local_sum_type {
702        LocalSumType::WideNeighbor => {
703            if y > 0 && x > 0 && x < nx - 1 {
704                get_cur(x - 1)
705                    + get_prev(x - 1)
706                    + get_prev(x)
707                    + get_prev(x + 1)
708            } else if y > 0 && x == 0 {
709                2 * (get_prev(x) + get_prev(x + 1))
710            } else if y > 0 && x == nx - 1 {
711                get_cur(x - 1)
712                    + get_prev(x - 1)
713                    + 2 * get_prev(x)
714            } else {
715                // y == 0, x > 0
716                4 * get_cur(x - 1)
717            }
718        }
719        LocalSumType::NarrowNeighbor => {
720            if y > 0 && x > 0 && x < nx - 1 {
721                get_prev(x - 1)
722                    + 2 * get_prev(x)
723                    + get_prev(x + 1)
724            } else if y == 0 && x > 0 && z > 0 {
725                let prev_band_row = if y == 0 {
726                    // At y=0, the "previous" row is the last row
727                    // of previous band. But with 2-row buffer we
728                    // only keep 2 rows. For BSQ order, at y=0 of
729                    // band z, band z-1 is fully processed. Its
730                    // last row is at (ny-1) % 2.
731                    (cfg.ny as usize - 1) % 2
732                } else {
733                    prev_row
734                };
735                4 * get_prev_band(z - 1, prev_band_row, x - 1)
736            } else if y > 0 && x == 0 {
737                2 * (get_prev(x) + get_prev(x + 1))
738            } else if y > 0 && x == nx - 1 {
739                2 * (get_prev(x - 1) + get_prev(x))
740            } else {
741                // y == 0, x > 0, z == 0
742                4 * cfg.s_mid()
743            }
744        }
745        LocalSumType::WideColumn => {
746            if y > 0 {
747                4 * get_prev(x)
748            } else {
749                // y == 0, x > 0
750                4 * get_cur(x - 1)
751            }
752        }
753        LocalSumType::NarrowColumn => {
754            if y > 0 {
755                4 * get_prev(x)
756            } else if y == 0 && x > 0 && z > 0 {
757                let prev_band_row =
758                    (cfg.ny as usize - 1) % 2;
759                4 * get_prev_band(z - 1, prev_band_row, x - 1)
760            } else {
761                4 * cfg.s_mid()
762            }
763        }
764    }
765}
766
767/// Build U vector using 2-row buffer layout.
768fn build_u_with_buf(
769    cfg: &Config,
770    sr_buf: &[i64],
771    sigma: i64,
772    nx: usize,
773    z: usize,
774    y: usize,
775    x: usize,
776    cur_row: usize,
777    prev_row: usize,
778    u: &mut [i64],
779) {
780    let p_star = core::cmp::min(z, cfg.p as usize);
781    let base = z * 2 * nx;
782    let get_cur =
783        |xx: usize| -> i64 { sr_buf[base + cur_row * nx + xx] };
784    let get_prev =
785        |xx: usize| -> i64 { sr_buf[base + prev_row * nx + xx] };
786
787    match cfg.mode {
788        PredictionMode::Full => {
789            // dN
790            u[0] = if y > 0 {
791                4 * get_prev(x) - sigma
792            } else {
793                0
794            };
795            // dW
796            u[1] = if y > 0 {
797                if x > 0 {
798                    4 * get_cur(x - 1) - sigma
799                } else {
800                    4 * get_prev(x) - sigma
801                }
802            } else {
803                0
804            };
805            // dNW
806            u[2] = if y > 0 {
807                if x > 0 {
808                    4 * get_prev(x - 1) - sigma
809                } else {
810                    4 * get_prev(x) - sigma
811                }
812            } else {
813                0
814            };
815            // Spectral central local diffs from previous bands
816            for i in 1..=p_star {
817                let bz = z - i;
818                // Need sigma and sr for band bz at (y,x)
819                // Recompute sigma for band bz
820                let sigma_prev = local_sum_with_buf(
821                    cfg, sr_buf, nx, bz, y, x, cur_row,
822                    prev_row,
823                );
824                let bbase = bz * 2 * nx;
825                let sr_val =
826                    sr_buf[bbase + cur_row * nx + x];
827                u[2 + i] = 4 * sr_val - sigma_prev;
828            }
829        }
830        PredictionMode::Reduced => {
831            for i in 1..=p_star {
832                let bz = z - i;
833                let sigma_prev = local_sum_with_buf(
834                    cfg, sr_buf, nx, bz, y, x, cur_row,
835                    prev_row,
836                );
837                let bbase = bz * 2 * nx;
838                let sr_val =
839                    sr_buf[bbase + cur_row * nx + x];
840                u[i - 1] = 4 * sr_val - sigma_prev;
841            }
842        }
843    }
844}
845
846/// High-res predicted value using buffer layout.
847fn high_res_predicted_buf(
848    cfg: &Config,
849    d_hat: i64,
850    sigma: i64,
851    t: usize,
852    z: usize,
853    sr_buf: &[i64],
854    nx: usize,
855) -> i64 {
856    let omega = cfg.omega as u32;
857    let r = cfg.register_size as u32;
858    let s_mid = cfg.s_mid();
859    let s_min = cfg.s_min();
860    let s_max = cfg.s_max();
861
862    if t > 0 {
863        let inner = d_hat
864            + (1i64 << omega) * (sigma - 4 * s_mid)
865            + (1i64 << (omega + 2)) * s_mid
866            + (1i64 << (omega + 1));
867        clip(
868            mod_star(inner, r),
869            (1i64 << (omega + 2)) * s_min
870                + (1i64 << (omega + 1)),
871            (1i64 << (omega + 2)) * s_max
872                + (1i64 << (omega + 1)),
873        )
874    } else if t == 0 && cfg.p > 0 && z > 0 {
875        // s_{z-1}(0) at (y=0, x=0)
876        2 * sr_buf[(z - 1) * 2 * nx]
877    } else {
878        2 * s_mid
879    }
880}
881
882/// Encode a single mapped quantizer index using sample-adaptive
883/// coder (§5.4.3.2).
884fn encode_sample(
885    cfg: &Config,
886    w: &mut BitWriter,
887    mapped: u64,
888    sigma_acc: &mut u64,
889    counter: &mut u64,
890    _t: usize,
891    _z: usize,
892) -> Result<(), Error> {
893    let d = cfg.d();
894    let u_max = cfg.u_max as u32;
895    let gamma_star = cfg.gamma_star as u64;
896
897    // Select k (eq 62)
898    let k = if 2 * *counter > *sigma_acc + (49 * *counter >> 7) {
899        0u32
900    } else {
901        // Find largest k such that
902        // counter * 2^k <= sigma + floor(49/128 * counter)
903        let threshold = *sigma_acc + (49 * *counter >> 7);
904        let mut k = 0u32;
905        while k < d - 2 {
906            if *counter * (1u64 << (k + 1)) > threshold {
907                break;
908            }
909            k += 1;
910        }
911        k
912    };
913
914    // Write GPO2 codeword
915    write_gpo2(w, mapped, k, u_max, d)?;
916
917    // Update accumulator and counter (eq 60-61)
918    if *counter < (1u64 << gamma_star) - 1 {
919        *sigma_acc += mapped;
920        *counter += 1;
921    } else {
922        *sigma_acc = (*sigma_acc + mapped + 1) / 2;
923        *counter = (*counter + 1) / 2;
924    }
925
926    Ok(())
927}
928
929// ── Header ───────────────────────────────────────────────
930
931fn write_header(
932    cfg: &Config,
933    w: &mut BitWriter,
934) -> Result<(), Error> {
935    let d = cfg.d();
936
937    // Image Metadata Essential (12 bytes = 96 bits)
938    w.write_bits(0, 8)?; // User-Defined Data
939    w.write_bits(cfg.nx as u64, 16)?; // X Size
940    w.write_bits(cfg.ny as u64, 16)?; // Y Size
941    w.write_bits(cfg.nz as u64, 16)?; // Z Size
942    let sample_type = if cfg.signed_samples { 1 } else { 0 };
943    w.write_bits(sample_type, 1)?; // Sample Type
944    w.write_bits(0, 1)?; // Reserved
945    let large_d = if d > 16 { 1u64 } else { 0 };
946    w.write_bits(large_d, 1)?; // Large Dynamic Range Flag
947    w.write_bits(d as u64 % 16, 4)?; // Dynamic Range
948    w.write_bits(0, 1)?; // Sample Encoding Order (BSQ)
949    w.write_bits(0, 16)?; // Sub-Frame Interleaving Depth
950    w.write_bits(0, 2)?; // Reserved
951    // Output Word Size (1 byte = value 1, encoded mod 8)
952    w.write_bits(1, 3)?;
953    // Entropy Coder Type: 00 = sample-adaptive
954    w.write_bits(0, 2)?;
955    w.write_bits(0, 1)?; // Reserved
956    // Quantizer Fidelity Control: 00 = lossless
957    w.write_bits(0, 2)?;
958    w.write_bits(0, 2)?; // Reserved
959    // Supplementary Information Table Count: 0
960    w.write_bits(0, 4)?;
961
962    // Predictor Metadata Primary (5 bytes = 40 bits)
963    w.write_bits(0, 1)?; // Reserved
964    w.write_bits(0, 1)?; // Sample Representative Flag = 0
965    w.write_bits(cfg.p as u64, 4)?; // Number of Prediction Bands
966    let pred_mode = match cfg.mode {
967        PredictionMode::Full => 0u64,
968        PredictionMode::Reduced => 1,
969    };
970    w.write_bits(pred_mode, 1)?;
971    w.write_bits(0, 1)?; // Weight Exponent Offset Flag = 0
972    let ls = match cfg.local_sum_type {
973        LocalSumType::WideNeighbor => 0u64,
974        LocalSumType::NarrowNeighbor => 1,
975        LocalSumType::WideColumn => 2,
976        LocalSumType::NarrowColumn => 3,
977    };
978    w.write_bits(ls, 2)?;
979    let r_enc = cfg.register_size as u64 % 64;
980    w.write_bits(r_enc, 6)?;
981    let omega_enc = (cfg.omega - 4) as u64;
982    w.write_bits(omega_enc, 4)?;
983    let t_inc_log = {
984        let mut v = cfg.t_inc;
985        let mut l = 0u32;
986        while v > 1 {
987            v >>= 1;
988            l += 1;
989        }
990        l
991    };
992    w.write_bits((t_inc_log - 4) as u64, 4)?;
993    w.write_bits((cfg.v_min + 6) as u64, 4)?;
994    w.write_bits((cfg.v_max + 6) as u64, 4)?;
995    w.write_bits(0, 1)?; // Weight Exponent Offset Table Flag=0
996    w.write_bits(0, 1)?; // Weight Init Method = default
997    w.write_bits(0, 1)?; // Weight Init Table Flag = 0
998    w.write_bits(0, 5)?; // Weight Init Resolution = 0
999
1000    // Entropy Coder Metadata (sample-adaptive)
1001    w.write_bits(cfg.u_max as u64 % 32, 5)?;
1002    let gamma_star_enc = (cfg.gamma_star - 4) as u64;
1003    w.write_bits(gamma_star_enc, 3)?;
1004    w.write_bits(cfg.gamma_0 as u64 % 8, 3)?;
1005    match cfg.accum_init_k {
1006        Some(k) => {
1007            w.write_bits(k as u64, 4)?;
1008            w.write_bits(0, 1)?; // No table
1009        }
1010        None => {
1011            w.write_bits(0b1111, 4)?; // "all ones" = not specified
1012            w.write_bits(0, 1)?; // No table
1013        }
1014    }
1015
1016    Ok(())
1017}
1018
1019// ── Decompress ───────────────────────────────────────────
1020
1021/// Decompress a CCSDS 123.0-B-2 compressed image.
1022///
1023/// `scratch` must have at least `scratch_len(nx, nz)` elements
1024/// (the header is read first to determine nx/nz, so callers
1025/// should pre-allocate generously or use `read_header_only`).
1026///
1027/// Returns the config and number of samples written.
1028pub fn decompress(
1029    data: &[u8],
1030    image: &mut [u16],
1031    scratch: &mut [i64],
1032) -> Result<(Config, usize), Error> {
1033    let mut br = BitReader::new(data);
1034    let cfg = read_header(&mut br)?;
1035
1036    let nx = cfg.nx as usize;
1037    let ny = cfg.ny as usize;
1038    let nz = cfg.nz as usize;
1039    let d = cfg.d();
1040    let needed = scratch_len(nx, nz);
1041
1042    if scratch.len() < needed {
1043        return Err(Error::ScratchTooSmall);
1044    }
1045    if image.len() < nx * ny * nz {
1046        return Err(Error::OutputFull);
1047    }
1048
1049    for s in scratch[..needed].iter_mut() {
1050        *s = 0;
1051    }
1052
1053    let sr_len = nz * 2 * nx;
1054    let w_len = nz * MAX_CZ;
1055    let (sr_buf, rest) = scratch.split_at_mut(sr_len);
1056    let (weights, acc_buf) = rest.split_at_mut(w_len);
1057    let acc = &mut acc_buf[..nz];
1058
1059    // Initialize weights
1060    for z in 0..nz {
1061        let cz = cfg.c_z(z);
1062        let wbase = z * MAX_CZ;
1063        let mut wslice = [0i64; MAX_CZ];
1064        init_weights_default(&cfg, z, &mut wslice);
1065        for j in 0..cz {
1066            weights[wbase + j] = wslice[j];
1067        }
1068    }
1069
1070    // Initialize entropy coder
1071    let mut counter = 1u64 << cfg.gamma_0;
1072    for z in 0..nz {
1073        let k_z_pp = match cfg.accum_init_k {
1074            Some(k) => k as u32,
1075            None => 0,
1076        };
1077        let k_z_prime = if k_z_pp <= 30 - d {
1078            k_z_pp
1079        } else {
1080            2 * k_z_pp + d - 30
1081        };
1082        acc[z] = ((3 * (1u64 << (k_z_prime + 6)) - 49)
1083            * counter) as i64
1084            >> 7;
1085    }
1086
1087    for z in 0..nz {
1088        for y in 0..ny {
1089            let cur_row = y % 2;
1090            let prev_row = (y + 1) % 2;
1091
1092            for x in 0..nx {
1093                let t = y * nx + x;
1094
1095                if t == 0 {
1096                    let raw = br.read_bits(d)? as i64;
1097                    let s = if cfg.signed_samples {
1098                        raw + cfg.s_min()
1099                    } else {
1100                        raw
1101                    };
1102                    image[z * ny * nx + y * nx + x] = s as u16;
1103                    sr_buf[z * 2 * nx + cur_row * nx + x] = s;
1104                    continue;
1105                }
1106
1107                let sigma = local_sum_with_buf(
1108                    &cfg, sr_buf, nx, z, y, x, cur_row,
1109                    prev_row,
1110                );
1111
1112                let mut u_vec = [0i64; MAX_CZ];
1113                let cz = cfg.c_z(z);
1114                if cz > 0 {
1115                    build_u_with_buf(
1116                        &cfg, sr_buf, sigma, nx, z, y, x,
1117                        cur_row, prev_row, &mut u_vec,
1118                    );
1119                }
1120
1121                let wbase = z * MAX_CZ;
1122                let mut d_hat = 0i64;
1123                for j in 0..cz {
1124                    d_hat += weights[wbase + j] * u_vec[j];
1125                }
1126
1127                let s_tilde = high_res_predicted_buf(
1128                    &cfg, d_hat, sigma, t, z, sr_buf, nx,
1129                );
1130                let s_hat = predicted_sample(s_tilde);
1131
1132                let mut acc_u = acc[z] as u64;
1133                let mapped = decode_sample(
1134                    &cfg, &mut br, &mut acc_u,
1135                    &mut counter, t, z,
1136                )?;
1137                acc[z] = acc_u as i64;
1138
1139                let delta =
1140                    unmap_quantizer_index(&cfg, mapped, s_hat);
1141                let s = s_hat + delta;
1142
1143                image[z * ny * nx + y * nx + x] = s as u16;
1144                sr_buf[z * 2 * nx + cur_row * nx + x] = s;
1145
1146                if cz > 0 {
1147                    let e = 2 * s - s_tilde;
1148                    let rho = scaling_exponent(&cfg, t);
1149                    let omega_min =
1150                        -(1i64 << (cfg.omega as i64 + 2));
1151                    let omega_max =
1152                        (1i64 << (cfg.omega as i64 + 2)) - 1;
1153
1154                    for j in 0..cz {
1155                        let inc = (sgn_plus(e)
1156                            * shift_right(u_vec[j], rho)
1157                            + 1)
1158                            / 2;
1159                        weights[wbase + j] = clip(
1160                            weights[wbase + j] + inc,
1161                            omega_min,
1162                            omega_max,
1163                        );
1164                    }
1165                }
1166            }
1167        }
1168    }
1169
1170    Ok((cfg, nx * ny * nz))
1171}
1172
1173/// Decode a single mapped quantizer index.
1174fn decode_sample(
1175    cfg: &Config,
1176    r: &mut BitReader,
1177    sigma_acc: &mut u64,
1178    counter: &mut u64,
1179    _t: usize,
1180    _z: usize,
1181) -> Result<u64, Error> {
1182    let d = cfg.d();
1183    let u_max = cfg.u_max as u32;
1184    let gamma_star = cfg.gamma_star as u64;
1185
1186    let k = if 2 * *counter > *sigma_acc + (49 * *counter >> 7) {
1187        0u32
1188    } else {
1189        let threshold = *sigma_acc + (49 * *counter >> 7);
1190        let mut k = 0u32;
1191        while k < d - 2 {
1192            if *counter * (1u64 << (k + 1)) > threshold {
1193                break;
1194            }
1195            k += 1;
1196        }
1197        k
1198    };
1199
1200    let mapped = read_gpo2(r, k, u_max, d)?;
1201
1202    // Update accumulator and counter
1203    if *counter < (1u64 << gamma_star) - 1 {
1204        *sigma_acc += mapped;
1205        *counter += 1;
1206    } else {
1207        *sigma_acc = (*sigma_acc + mapped + 1) / 2;
1208        *counter = (*counter + 1) / 2;
1209    }
1210
1211    Ok(mapped)
1212}
1213
1214fn read_header(r: &mut BitReader) -> Result<Config, Error> {
1215    // Image Metadata Essential
1216    let _user_data = r.read_bits(8)?;
1217    let nx = r.read_bits(16)? as u16;
1218    let ny = r.read_bits(16)? as u16;
1219    let nz = r.read_bits(16)? as u16;
1220    let signed_samples = r.read_bits(1)? == 1;
1221    let _reserved = r.read_bits(1)?;
1222    let large_d = r.read_bits(1)?;
1223    let d_enc = r.read_bits(4)? as u8;
1224    let d = if large_d == 1 {
1225        d_enc as u8 + 16
1226    } else if d_enc == 0 {
1227        16
1228    } else {
1229        d_enc
1230    };
1231    let _encoding_order = r.read_bits(1)?; // BSQ=0
1232    let _sub_frame_depth = r.read_bits(16)?;
1233    let _reserved = r.read_bits(2)?;
1234    let b_enc = r.read_bits(3)?;
1235    let _output_word_size = if b_enc == 0 { 8 } else { b_enc };
1236    let entropy_type = r.read_bits(2)?;
1237    if entropy_type != 0 {
1238        return Err(Error::InvalidConfig); // Only sample-adaptive
1239    }
1240    let _reserved = r.read_bits(1)?;
1241    let fidelity = r.read_bits(2)?;
1242    if fidelity != 0 {
1243        return Err(Error::InvalidConfig); // Only lossless
1244    }
1245    let _reserved = r.read_bits(2)?;
1246    let _tau = r.read_bits(4)?;
1247
1248    // Predictor Metadata Primary
1249    let _reserved = r.read_bits(1)?;
1250    let _sr_flag = r.read_bits(1)?;
1251    let p = r.read_bits(4)? as u8;
1252    let pred_mode_bit = r.read_bits(1)?;
1253    let mode = if pred_mode_bit == 0 {
1254        PredictionMode::Full
1255    } else {
1256        PredictionMode::Reduced
1257    };
1258    let _weo_flag = r.read_bits(1)?;
1259    let ls_enc = r.read_bits(2)?;
1260    let local_sum_type = match ls_enc {
1261        0 => LocalSumType::WideNeighbor,
1262        1 => LocalSumType::NarrowNeighbor,
1263        2 => LocalSumType::WideColumn,
1264        _ => LocalSumType::NarrowColumn,
1265    };
1266    let r_enc = r.read_bits(6)? as u8;
1267    let register_size = if r_enc == 0 { 64 } else { r_enc };
1268    let omega = r.read_bits(4)? as u8 + 4;
1269    let t_inc_enc = r.read_bits(4)? as u32;
1270    let t_inc = 1u16 << (t_inc_enc + 4);
1271    let v_min = r.read_bits(4)? as i8 - 6;
1272    let v_max = r.read_bits(4)? as i8 - 6;
1273    let _weo_table_flag = r.read_bits(1)?;
1274    let _weight_init_method = r.read_bits(1)?;
1275    let _weight_init_table = r.read_bits(1)?;
1276    let _weight_init_res = r.read_bits(5)?;
1277
1278    // Entropy Coder Metadata (sample-adaptive)
1279    let u_max_enc = r.read_bits(5)? as u8;
1280    let u_max = if u_max_enc == 0 { 32 } else { u_max_enc };
1281    let gamma_star = r.read_bits(3)? as u8 + 4;
1282    let gamma_0_enc = r.read_bits(3)? as u8;
1283    let gamma_0 = if gamma_0_enc == 0 { 8 } else { gamma_0_enc };
1284    let k_enc = r.read_bits(4)? as u8;
1285    let accum_init_k =
1286        if k_enc == 0b1111 { None } else { Some(k_enc) };
1287    let _table_flag = r.read_bits(1)?;
1288
1289    Ok(Config {
1290        nx,
1291        ny,
1292        nz,
1293        dynamic_range: d,
1294        signed_samples,
1295        p,
1296        mode,
1297        local_sum_type,
1298        omega,
1299        register_size,
1300        t_inc,
1301        v_min,
1302        v_max,
1303        u_max,
1304        gamma_0,
1305        gamma_star,
1306        accum_init_k,
1307    })
1308}
1309
1310// ── Tests ────────────────────────────────────────────────
1311
1312#[cfg(test)]
1313mod tests {
1314    use super::*;
1315
1316    fn default_config(
1317        nx: u16,
1318        ny: u16,
1319        nz: u16,
1320    ) -> Config {
1321        Config {
1322            nx,
1323            ny,
1324            nz,
1325            dynamic_range: 8,
1326            signed_samples: false,
1327            p: core::cmp::min(nz.saturating_sub(1), 3) as u8,
1328            mode: if nx > 1 {
1329                PredictionMode::Full
1330            } else {
1331                PredictionMode::Reduced
1332            },
1333            local_sum_type: if nx > 1 {
1334                LocalSumType::WideNeighbor
1335            } else {
1336                LocalSumType::WideColumn
1337            },
1338            omega: 8,
1339            register_size: 32,
1340            t_inc: 64,
1341            v_min: -1,
1342            v_max: 3,
1343            u_max: 16,
1344            gamma_0: 3,
1345            gamma_star: 6,
1346            accum_init_k: None,
1347        }
1348    }
1349
1350    // scratch_len(4,3) = 3*2*4 + 3*18 + 3 = 81
1351    // scratch_len(8,4) = 4*2*8 + 4*18 + 4 = 140
1352    const SCRATCH: usize = 256;
1353
1354    fn roundtrip(cfg: &Config, image: &[u16]) {
1355        let mut compressed = [0u8; 2048];
1356        let mut scratch = [0i64; SCRATCH];
1357
1358        let n = compress(
1359            cfg, image, &mut compressed, &mut scratch,
1360        )
1361        .unwrap();
1362        assert!(n > 0);
1363
1364        let mut decoded = [0u16; 128];
1365        let n_samples = image.len();
1366        for s in scratch.iter_mut() {
1367            *s = 0;
1368        }
1369        let (_, count) = decompress(
1370            &compressed[..n],
1371            &mut decoded[..n_samples],
1372            &mut scratch,
1373        )
1374        .unwrap();
1375        assert_eq!(count, n_samples);
1376        assert_eq!(&decoded[..n_samples], image);
1377    }
1378
1379    #[test]
1380    fn roundtrip_constant() {
1381        let cfg = default_config(4, 4, 1);
1382        roundtrip(&cfg, &[128u16; 16]);
1383    }
1384
1385    #[test]
1386    fn roundtrip_ramp() {
1387        let cfg = default_config(4, 4, 1);
1388        let mut image = [0u16; 16];
1389        for i in 0..16 {
1390            image[i] = (i * 16) as u16;
1391        }
1392        roundtrip(&cfg, &image);
1393    }
1394
1395    #[test]
1396    fn roundtrip_multiband() {
1397        let cfg = default_config(4, 4, 3);
1398        let mut image = [0u16; 48];
1399        for z in 0..3 {
1400            for i in 0..16 {
1401                image[z * 16 + i] =
1402                    ((z * 50 + i * 10) % 256) as u16;
1403            }
1404        }
1405        roundtrip(&cfg, &image);
1406    }
1407
1408    #[test]
1409    fn roundtrip_reduced_mode() {
1410        let cfg = Config {
1411            mode: PredictionMode::Reduced,
1412            ..default_config(4, 4, 2)
1413        };
1414        let mut image = [0u16; 32];
1415        for i in 0..32 {
1416            image[i] = (i * 7 % 256) as u16;
1417        }
1418        roundtrip(&cfg, &image);
1419    }
1420
1421    #[test]
1422    fn roundtrip_16bit() {
1423        let cfg = Config {
1424            dynamic_range: 16,
1425            register_size: 32,
1426            ..default_config(4, 4, 1)
1427        };
1428        let mut image = [0u16; 16];
1429        for i in 0..16 {
1430            image[i] = (i as u16 * 4000) + 100;
1431        }
1432        roundtrip(&cfg, &image);
1433    }
1434
1435    #[test]
1436    fn roundtrip_column_oriented() {
1437        let cfg = Config {
1438            local_sum_type: LocalSumType::WideColumn,
1439            ..default_config(4, 4, 2)
1440        };
1441        let mut image = [0u16; 32];
1442        for i in 0..32 {
1443            image[i] = (i * 13 % 256) as u16;
1444        }
1445        roundtrip(&cfg, &image);
1446    }
1447
1448    #[test]
1449    fn mapped_index_roundtrip() {
1450        let cfg = default_config(4, 4, 1);
1451        for s_hat in [0i64, 50, 127, 200, 255] {
1452            for delta in [-50i64, -1, 0, 1, 50] {
1453                let s = s_hat + delta;
1454                if s < cfg.s_min() || s > cfg.s_max() {
1455                    continue;
1456                }
1457                let mapped = mapped_quantizer_index(
1458                    &cfg, delta, s_hat, 1,
1459                );
1460                let recovered = unmap_quantizer_index(
1461                    &cfg, mapped, s_hat,
1462                );
1463                assert_eq!(
1464                    recovered, delta,
1465                    "s_hat={s_hat} delta={delta} \
1466                     mapped={mapped}"
1467                );
1468            }
1469        }
1470    }
1471
1472    #[test]
1473    fn gpo2_roundtrip() {
1474        let mut buf = [0u8; 64];
1475        for k in 0..4u32 {
1476            for j in 0u64..20 {
1477                for b in buf.iter_mut() {
1478                    *b = 0;
1479                }
1480                let mut w = BitWriter::new(&mut buf);
1481                write_gpo2(&mut w, j, k, 16, 8).unwrap();
1482                let mut r = BitReader::new(&buf);
1483                let decoded =
1484                    read_gpo2(&mut r, k, 16, 8).unwrap();
1485                assert_eq!(decoded, j, "k={k} j={j}");
1486            }
1487        }
1488    }
1489
1490    #[test]
1491    fn header_roundtrip() {
1492        let cfg = default_config(8, 8, 4);
1493        let mut buf = [0u8; 64];
1494        let mut w = BitWriter::new(&mut buf);
1495        write_header(&cfg, &mut w).unwrap();
1496
1497        let mut r = BitReader::new(&buf);
1498        let decoded = read_header(&mut r).unwrap();
1499
1500        assert_eq!(decoded.nx, cfg.nx);
1501        assert_eq!(decoded.ny, cfg.ny);
1502        assert_eq!(decoded.nz, cfg.nz);
1503        assert_eq!(decoded.dynamic_range, cfg.dynamic_range);
1504        assert_eq!(decoded.p, cfg.p);
1505        assert_eq!(decoded.omega, cfg.omega);
1506        assert_eq!(decoded.v_min, cfg.v_min);
1507        assert_eq!(decoded.v_max, cfg.v_max);
1508        assert_eq!(decoded.gamma_0, cfg.gamma_0);
1509        assert_eq!(decoded.gamma_star, cfg.gamma_star);
1510    }
1511
1512    #[test]
1513    fn single_band_no_prediction() {
1514        let cfg = Config {
1515            p: 0,
1516            mode: PredictionMode::Reduced,
1517            local_sum_type: LocalSumType::WideColumn,
1518            ..default_config(4, 4, 1)
1519        };
1520        roundtrip(&cfg, &[100u16; 16]);
1521    }
1522}