Skip to main content

tensogram_szip/
params.rs

1// (C) Copyright 2026- ECMWF and individual contributors.
2//
3// This software is licensed under the terms of the Apache Licence Version 2.0
4// which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
5// In applying this licence, ECMWF does not waive the privileges and immunities
6// granted to it by virtue of its status as an intergovernmental organisation nor
7// does it submit to any jurisdiction.
8
9//! AEC parameters and flag constants matching the libaec C API.
10//!
11//! These constants are byte-compatible with `libaec_sys` so that
12//! `tensogram-encodings` can swap implementations at build time.
13
14/// Parameters for AEC encoding/decoding.
15#[derive(Debug, Clone)]
16pub struct AecParams {
17    /// Bits per input sample (1–32).
18    pub bits_per_sample: u32,
19    /// Samples per coding block (8, 16, 32, or 64).
20    pub block_size: u32,
21    /// Number of blocks per Reference Sample Interval.
22    pub rsi: u32,
23    /// Combination of `AEC_DATA_*` flags.
24    pub flags: u32,
25}
26
27// ── Flag constants (matching libaec.h values exactly) ────────────────────────
28
29/// Input samples are signed (two's complement).
30pub const AEC_DATA_SIGNED: u32 = 1;
31/// Samples are stored in 3-byte containers (for 17–24 bit).
32pub const AEC_DATA_3BYTE: u32 = 2;
33/// Samples are stored MSB-first (big-endian).
34pub const AEC_DATA_MSB: u32 = 4;
35/// Enable preprocessor (unit-delay delta coding).
36pub const AEC_DATA_PREPROCESS: u32 = 8;
37/// Allow k=13 extension (reserved, unused in this implementation).
38pub const AEC_ALLOW_K13: u32 = 16;
39/// Pad each RSI to byte boundary.
40pub const AEC_PAD_RSI: u32 = 32;
41/// Allow non-standard block sizes (any even number).
42pub const AEC_NOT_ENFORCE: u32 = 64;
43/// Use restricted set of code options (for ≤4 bit samples).
44pub const AEC_RESTRICTED: u32 = 128;
45
46// ── Derived helpers ──────────────────────────────────────────────────────────
47
48/// Compute the byte width of each sample container in the input/output buffer.
49pub(crate) fn sample_byte_width(bits_per_sample: u32, flags: u32) -> usize {
50    let nbytes = (bits_per_sample as usize).div_ceil(8);
51    if nbytes == 3 && flags & AEC_DATA_3BYTE == 0 {
52        4 // 17–24 bit in 4-byte containers unless 3BYTE flag is set
53    } else {
54        nbytes
55    }
56}
57
58/// Apply automatic flag adjustments (mirrors libaec `effective_flags`).
59///
60/// - Sets `AEC_DATA_3BYTE` for 17–24 bit samples so the codec reads
61///   3-byte containers instead of defaulting to 4-byte.
62pub(crate) fn effective_flags(params: &AecParams) -> u32 {
63    let mut flags = params.flags;
64    if params.bits_per_sample > 16 && params.bits_per_sample <= 24 {
65        flags |= AEC_DATA_3BYTE;
66    }
67    flags
68}
69
70/// Compute the ID length (number of bits for the coding option identifier).
71///
72/// Matches libaec exactly:
73/// - bits_per_sample > 16 → 5
74/// - bits_per_sample > 8  → 4
75/// - bits_per_sample ≤ 8 + RESTRICTED:
76///     - ≤ 2 → 1
77///     - ≤ 4 → 2
78///     - > 4 → error (should be caught during validation)
79/// - bits_per_sample ≤ 8 (normal) → 3
80pub(crate) fn id_len(bits_per_sample: u32, flags: u32) -> u32 {
81    if bits_per_sample > 16 {
82        5
83    } else if bits_per_sample > 8 {
84        4
85    } else if flags & AEC_RESTRICTED != 0 {
86        if bits_per_sample <= 2 {
87            1
88        } else {
89            2 // bits_per_sample 3 or 4
90        }
91    } else {
92        3
93    }
94}
95
96/// Maximum split parameter k = (1 << id_len) - 3.
97pub(crate) fn kmax(id_len: u32) -> u32 {
98    (1u32 << id_len).saturating_sub(3)
99}
100
101/// Validate AEC parameters and return an error if invalid.
102pub(crate) fn validate(params: &AecParams) -> Result<(), crate::AecError> {
103    use crate::AecError;
104
105    if params.bits_per_sample == 0 || params.bits_per_sample > 32 {
106        return Err(AecError::Config(format!(
107            "bits_per_sample must be 1–32, got {}",
108            params.bits_per_sample
109        )));
110    }
111
112    if params.block_size == 0 {
113        return Err(AecError::Config("block_size must be non-zero".to_string()));
114    }
115    if params.flags & AEC_NOT_ENFORCE != 0 {
116        if params.block_size & 1 != 0 {
117            return Err(AecError::Config(format!(
118                "block_size must be even, got {}",
119                params.block_size
120            )));
121        }
122    } else if !matches!(params.block_size, 8 | 16 | 32 | 64) {
123        return Err(AecError::Config(format!(
124            "block_size must be 8, 16, 32, or 64, got {}",
125            params.block_size
126        )));
127    }
128
129    if params.rsi == 0 || params.rsi > 4096 {
130        return Err(AecError::Config(format!(
131            "rsi must be 1–4096, got {}",
132            params.rsi
133        )));
134    }
135
136    if params.flags & AEC_RESTRICTED != 0 && params.bits_per_sample > 4 {
137        return Err(AecError::Config(format!(
138            "AEC_RESTRICTED requires bits_per_sample ≤ 4, got {}",
139            params.bits_per_sample
140        )));
141    }
142
143    Ok(())
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_id_len_values() {
152        assert_eq!(id_len(32, 0), 5);
153        assert_eq!(id_len(24, 0), 5);
154        assert_eq!(id_len(17, 0), 5);
155        assert_eq!(id_len(16, 0), 4);
156        assert_eq!(id_len(9, 0), 4);
157        assert_eq!(id_len(8, 0), 3);
158        assert_eq!(id_len(1, 0), 3);
159        assert_eq!(id_len(2, AEC_RESTRICTED), 1);
160        assert_eq!(id_len(4, AEC_RESTRICTED), 2);
161    }
162
163    #[test]
164    fn test_kmax_values() {
165        assert_eq!(kmax(5), 29);
166        assert_eq!(kmax(4), 13);
167        assert_eq!(kmax(3), 5);
168        assert_eq!(kmax(2), 1);
169        assert_eq!(kmax(1), 0); // restricted, ≤2 bit: no split options, only uncomp/zero/se
170    }
171
172    #[test]
173    fn test_sample_byte_width() {
174        assert_eq!(sample_byte_width(8, 0), 1);
175        assert_eq!(sample_byte_width(16, 0), 2);
176        assert_eq!(sample_byte_width(24, 0), 4); // no 3BYTE flag → 4-byte container
177        assert_eq!(sample_byte_width(24, AEC_DATA_3BYTE), 3);
178        assert_eq!(sample_byte_width(32, 0), 4);
179    }
180
181    #[test]
182    fn test_effective_flags_auto_3byte() {
183        let p = AecParams {
184            bits_per_sample: 24,
185            block_size: 16,
186            rsi: 128,
187            flags: AEC_DATA_PREPROCESS,
188        };
189        let f = effective_flags(&p);
190        assert!(f & AEC_DATA_3BYTE != 0);
191        assert!(f & AEC_DATA_PREPROCESS != 0);
192    }
193
194    #[test]
195    fn test_validate_ok() {
196        let p = AecParams {
197            bits_per_sample: 8,
198            block_size: 16,
199            rsi: 128,
200            flags: AEC_DATA_PREPROCESS,
201        };
202        assert!(validate(&p).is_ok());
203    }
204
205    #[test]
206    fn test_validate_bad_bps() {
207        let p = AecParams {
208            bits_per_sample: 0,
209            block_size: 16,
210            rsi: 128,
211            flags: 0,
212        };
213        assert!(validate(&p).is_err());
214
215        let p2 = AecParams {
216            bits_per_sample: 33,
217            block_size: 16,
218            rsi: 128,
219            flags: 0,
220        };
221        assert!(validate(&p2).is_err());
222    }
223
224    #[test]
225    fn test_validate_bad_block_size() {
226        let p = AecParams {
227            bits_per_sample: 8,
228            block_size: 12,
229            rsi: 128,
230            flags: 0,
231        };
232        assert!(validate(&p).is_err());
233
234        // With NOT_ENFORCE, even block sizes are allowed
235        let p2 = AecParams {
236            bits_per_sample: 8,
237            block_size: 12,
238            rsi: 128,
239            flags: AEC_NOT_ENFORCE,
240        };
241        assert!(validate(&p2).is_ok());
242    }
243}