Skip to main content

oxideav_opus/
celt_pulse_cache.rs

1//! CELT §4.3.4.1 *Bits to Pulses* pulse-cost cache
2//! (RFC 6716 §4.3.4 / §4.3.4.1).
3//!
4//! The §4.3.4 *Shape* decode codes each band's unit-norm spectral
5//! shape with Pulse Vector Quantisation (PVQ): a band of `N` MDCT
6//! coefficients is represented by `K` signed integer pulse positions
7//! whose absolute values sum to `K`. Before the shape can be decoded
8//! the allocator must answer the §4.3.4.1 *Bits to Pulses* question:
9//! given a per-band bit budget `B`, what is the largest pulse count
10//! `K` whose PVQ codeword cost does not exceed `B`?
11//!
12//! The exact cost is `log2(V(N, K))` plus a small framing overhead,
13//! where `V(N, K)` is the number of PVQ codepoints with `K` pulses in
14//! `N` slots (the round-32 [`crate::celt_pvq_v`] recurrence). Computing
15//! that at decode time for every candidate `K` is expensive, so the
16//! §4.3.4.1 procedure precomputes the cost curve for the `(band, LM)`
17//! combinations that occur at the codec's normal operating bitrates and
18//! stores them as two flat tables:
19//!
20//! * [`CACHE_INDEX50`] — a 105-entry `i16` table mapping each
21//!   `(band, LM)` tuple to a byte offset into [`CACHE_BITS50`], or the
22//!   sentinel `-1` for tuples the allocator handles with a closed-form
23//!   path.
24//! * [`CACHE_BITS50`] — a 392-byte run-packed table; each run holds the
25//!   monotone cost curve `qbits[1..=maxK]` for one `(N, max-pulses)`
26//!   profile, in the codec's `BITRES = 3` units (1/8 bits / Q3).
27//!
28//! This module owns only the §4.3.4.1 *cost-cache lookup surface*: the
29//! two tables, the `(band, LM)` → offset indexing rule, the run reader,
30//! and the bits-to-pulses inversion scan. The §4.3.4 PVQ shape decode
31//! that consumes the returned `K` ([`crate::celt_pvq_decode`]) and the
32//! §4.3.3 allocator that computes the per-band budget `B` run at their
33//! own call sites.
34//!
35//! ## Indexing
36//!
37//! The 21 CELT nominal bands are indexed `band ∈ 0..=20`
38//! ([`crate::celt_band_layout::CELT_NUM_BANDS`]). Each band's actual
39//! coefficient count `N` depends on the frame size through
40//! `LM = log2(frame_size / 120)`, with `LM ∈ 0..=4` covering the
41//! 2.5 / 5 / 10 / 20 ms frames plus the short-block transient variant.
42//! There are `21 × 5 = 105` distinct `(band, LM)` tuples, which is
43//! exactly the length of [`CACHE_INDEX50`]. The tuple maps to the flat
44//! index in **band-major** order:
45//!
46//! ```text
47//! i      = band * CACHE_LM_COUNT + LM
48//! offset = CACHE_INDEX50[i]
49//! ```
50//!
51//! A `-1` offset is a sentinel ([`CACHE_INDEX_SENTINEL`]) meaning the
52//! `(band, LM)` has no cached cost curve — the band is a single
53//! coefficient (no pulse packing) or small enough that the allocator
54//! uses a direct formula. The eight sentinels are band 0 at all five
55//! LM values plus band 1 at `LM ∈ {0, 1, 2}`.
56//!
57//! ## Run format
58//!
59//! A run at byte `off` in [`CACHE_BITS50`] is:
60//!
61//! ```text
62//! CACHE_BITS50[off]            = maxK       (1 byte: max K this run supports)
63//! CACHE_BITS50[off + 1]        = qbits[1]   (cost for K = 1, in 1/8 bits)
64//! CACHE_BITS50[off + 2]        = qbits[2]
65//! ...
66//! CACHE_BITS50[off + maxK]     = qbits[maxK]
67//! ```
68//!
69//! so the run occupies `1 + maxK` bytes. `K = 0` is implicit (cost 0)
70//! and never stored. `qbits[1..=maxK]` is monotone non-decreasing.
71//! Several `(band, LM)` tuples that share the same `(N, max-pulses)`
72//! profile point at the same run, so the 105-entry index resolves to
73//! only 23 distinct runs.
74//!
75//! ## Bits to pulses
76//!
77//! Given a per-band budget `b_target` in 1/8 bits,
78//! [`bits_to_pulses`] performs the §4.3.4.1 inversion: scan the run's
79//! `qbits[1..=maxK]` and return the largest `K` whose cost fits the
80//! budget. The scan is a linear walk over a constant-bounded run
81//! (`maxK ≤ 40`); the monotone property would also admit a binary
82//! search. A sentinel `(band, LM)` returns
83//! [`PulseCacheError::SentinelTuple`] so the caller can route to its
84//! closed-form path rather than guessing.
85//!
86//! ## Units
87//!
88//! All cost values are in 1/8 bits (Q3, the `BITRES = 3` convention
89//! shared with [`crate::celt_alloc_search`]). `bits_to_pulses` takes
90//! the budget `b_target` in the same units and returns a pure pulse
91//! count.
92//!
93//! ## Provenance
94//!
95//! Narrative: RFC 6716 §4.3.4 / §4.3.4.1 (*Bits to Pulses*) in
96//! `docs/audio/opus/rfc6716-opus.txt`, plus the run-format trace
97//! `docs/audio/opus/pulse-cache-format-trace.md` (the band-major
98//! indexing rule, run packing, sentinel pattern, and qbits → bits
99//! conversion). Numeric tables: the 105-entry `cache_index50` and
100//! 392-byte `cache_bits50` sequences from
101//! `docs/audio/opus/tables/cache-index50.csv` and
102//! `docs/audio/opus/tables/cache-bits50.csv` (see the `.meta`
103//! sidecars for the canonical layout). The values are reproduced
104//! inline so the cache is available without filesystem I/O at runtime.
105
106use crate::celt_band_layout::CELT_NUM_BANDS;
107
108/// Number of frame-size (`LM`) columns indexing [`CACHE_INDEX50`].
109///
110/// `LM ∈ 0..=4` covers the 2.5 / 5 / 10 / 20 ms frames plus the
111/// short-block transient variant (RFC 6716 §4.3.4; the run-format
112/// trace §2). The cache is keyed on five LM values, unlike the
113/// four-value `LM ∈ 0..=3` axis of [`crate::celt_cache_caps50`].
114pub const CACHE_LM_COUNT: usize = 5;
115
116/// Total entries in [`CACHE_INDEX50`]: `21 × 5 = 105`.
117pub const CACHE_INDEX_LEN: usize = CELT_NUM_BANDS * CACHE_LM_COUNT;
118
119/// Total bytes in [`CACHE_BITS50`].
120pub const CACHE_BITS_LEN: usize = 392;
121
122/// Sentinel value in [`CACHE_INDEX50`]: the `(band, LM)` tuple has no
123/// cached cost curve and the allocator must use its closed-form path
124/// (run-format trace §2 / §5).
125pub const CACHE_INDEX_SENTINEL: i16 = -1;
126
127/// Upper bound on a single run's `maxK` (run-format trace §4: the four
128/// largest runs cap at 40).
129pub const CACHE_MAX_PULSES: u8 = 40;
130
131/// §4.3.4.1 `cache_index50`: maps the band-major `(band, LM)` flat
132/// index to a byte offset into [`CACHE_BITS50`], or
133/// [`CACHE_INDEX_SENTINEL`].
134///
135/// Layout: `i = band * CACHE_LM_COUNT + LM`, band-major, so the first
136/// five entries are band 0 at `LM = 0..=4`. The eight `-1` entries are
137/// band 0 (all LM) plus band 1 (`LM ∈ {0, 1, 2}`).
138///
139/// Numeric facts from `docs/audio/opus/tables/cache-index50.csv`.
140pub static CACHE_INDEX50: [i16; CACHE_INDEX_LEN] = [
141    -1, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 41, 41, 41, 82, 82, 123, 164, 200, 222, 0, 0, 0, 0,
142    0, 0, 0, 0, 41, 41, 41, 41, 123, 123, 123, 164, 164, 240, 266, 283, 295, 41, 41, 41, 41, 41,
143    41, 41, 41, 123, 123, 123, 123, 240, 240, 240, 266, 266, 305, 318, 328, 336, 123, 123, 123,
144    123, 123, 123, 123, 123, 240, 240, 240, 240, 305, 305, 305, 318, 318, 343, 351, 358, 364, 240,
145    240, 240, 240, 240, 240, 240, 240, 305, 305, 305, 305, 343, 343, 343, 351, 351, 370, 376, 382,
146    387,
147];
148
149/// §4.3.4.1 `cache_bits50`: run-packed PVQ cost curves.
150///
151/// Each run is `[maxK, qbits[1], …, qbits[maxK]]` in 1/8 bits (Q3).
152/// Walk a run via the offset stored in [`CACHE_INDEX50`]; see the
153/// module docs for the run format. The 23 distinct runs pack into
154/// exactly [`CACHE_BITS_LEN`] bytes.
155///
156/// Numeric facts from `docs/audio/opus/tables/cache-bits50.csv`.
157pub static CACHE_BITS50: [u8; CACHE_BITS_LEN] = [
158    40, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
159    7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 40, 15, 23, 28, 31, 34, 36, 38, 39, 41, 42, 43, 44, 45, 46, 47,
160    47, 49, 50, 51, 52, 53, 54, 55, 55, 57, 58, 59, 60, 61, 62, 63, 63, 65, 66, 67, 68, 69, 70, 71,
161    71, 40, 20, 33, 41, 48, 53, 57, 61, 64, 66, 69, 71, 73, 75, 76, 78, 80, 82, 85, 87, 89, 91, 92,
162    94, 96, 98, 101, 103, 105, 107, 108, 110, 112, 114, 117, 119, 121, 123, 124, 126, 128, 40, 23,
163    39, 51, 60, 67, 73, 79, 83, 87, 91, 94, 97, 100, 102, 105, 107, 111, 115, 118, 121, 124, 126,
164    129, 131, 135, 139, 142, 145, 148, 150, 153, 155, 159, 163, 166, 169, 172, 174, 177, 179, 35,
165    28, 49, 65, 78, 89, 99, 107, 114, 120, 126, 132, 136, 141, 145, 149, 153, 159, 165, 171, 176,
166    180, 185, 189, 192, 199, 205, 211, 216, 220, 225, 229, 232, 239, 245, 251, 21, 33, 58, 79, 97,
167    112, 125, 137, 148, 157, 166, 174, 182, 189, 195, 201, 207, 217, 227, 235, 243, 251, 17, 35,
168    63, 86, 106, 123, 139, 152, 165, 177, 187, 197, 206, 214, 222, 230, 237, 250, 25, 31, 55, 75,
169    91, 105, 117, 128, 138, 146, 154, 161, 168, 174, 180, 185, 190, 200, 208, 215, 222, 229, 235,
170    240, 245, 255, 16, 36, 65, 89, 110, 128, 144, 159, 173, 185, 196, 207, 217, 226, 234, 242, 250,
171    11, 41, 74, 103, 128, 151, 172, 191, 209, 225, 241, 255, 9, 43, 79, 110, 138, 163, 186, 207,
172    227, 246, 12, 39, 71, 99, 123, 144, 164, 182, 198, 214, 228, 241, 253, 9, 44, 81, 113, 142,
173    168, 192, 214, 235, 255, 7, 49, 90, 127, 160, 191, 220, 247, 6, 51, 95, 134, 170, 203, 234, 7,
174    47, 87, 123, 155, 184, 212, 237, 6, 52, 97, 137, 174, 208, 240, 5, 57, 106, 151, 192, 231, 5,
175    59, 111, 158, 202, 243, 5, 55, 103, 147, 187, 224, 5, 60, 113, 161, 206, 248, 4, 65, 122, 175,
176    224, 4, 67, 127, 182, 234,
177];
178
179/// Errors returned by the §4.3.4.1 cache lookup.
180#[derive(Debug, Clone, Copy, PartialEq, Eq)]
181pub enum PulseCacheError {
182    /// `band` is outside `0..21` (RFC 6716 §4.3 Table 55 band count).
183    BandOutOfRange { band: usize },
184    /// `lm` is outside `0..5` (the cache's five-column LM axis).
185    LmOutOfRange { lm: usize },
186    /// The `(band, LM)` tuple maps to [`CACHE_INDEX_SENTINEL`]: it has
187    /// no cached cost curve and the caller must use the §4.3.4.1
188    /// closed-form path.
189    SentinelTuple { band: usize, lm: usize },
190    /// `k` is outside the run's stored `1..=maxK` cost curve (`k = 0`
191    /// has implicit cost 0 and is never stored; `k > maxK` exceeds the
192    /// run's supported pulse count).
193    PulseCountOutOfRange { k: u8, max_k: u8 },
194}
195
196/// Resolve the band-major `(band, LM)` flat index into [`CACHE_INDEX50`]
197/// (RFC 6716 §4.3.4.1; run-format trace §2).
198///
199/// Returns [`PulseCacheError::BandOutOfRange`] / `LmOutOfRange` for
200/// out-of-range inputs.
201pub const fn cache_flat_index(band: usize, lm: usize) -> Result<usize, PulseCacheError> {
202    if band >= CELT_NUM_BANDS {
203        return Err(PulseCacheError::BandOutOfRange { band });
204    }
205    if lm >= CACHE_LM_COUNT {
206        return Err(PulseCacheError::LmOutOfRange { lm });
207    }
208    Ok(band * CACHE_LM_COUNT + lm)
209}
210
211/// Return the [`CACHE_BITS50`] byte offset for a `(band, LM)` tuple, or
212/// the sentinel error.
213///
214/// Returns [`PulseCacheError::SentinelTuple`] when the index entry is
215/// [`CACHE_INDEX_SENTINEL`] (the caller must route to its closed-form
216/// path), or the range errors from [`cache_flat_index`].
217pub const fn cache_run_offset(band: usize, lm: usize) -> Result<usize, PulseCacheError> {
218    let i = match cache_flat_index(band, lm) {
219        Ok(i) => i,
220        Err(e) => return Err(e),
221    };
222    let off = CACHE_INDEX50[i];
223    if off == CACHE_INDEX_SENTINEL {
224        return Err(PulseCacheError::SentinelTuple { band, lm });
225    }
226    Ok(off as usize)
227}
228
229/// Return the maximum pulse count `maxK` the run for `(band, LM)`
230/// supports (the run's leading byte).
231///
232/// Returns the same errors as [`cache_run_offset`].
233pub const fn cache_max_pulses(band: usize, lm: usize) -> Result<u8, PulseCacheError> {
234    let off = match cache_run_offset(band, lm) {
235        Ok(off) => off,
236        Err(e) => return Err(e),
237    };
238    Ok(CACHE_BITS50[off])
239}
240
241/// Return the §4.3.4.1 cost `qbits[k]` (1/8 bits) of coding exactly `k`
242/// pulses for `(band, LM)`.
243///
244/// `k` must be in `1..=maxK`. `k = 0` has implicit cost 0 (no codeword)
245/// and is rejected. Returns [`PulseCacheError::SentinelTuple`] for a
246/// sentinel tuple, or [`PulseCacheError::PulseCountOutOfRange`] when
247/// `k` is 0 or exceeds the run's `maxK`.
248pub const fn cache_pulse_cost(band: usize, lm: usize, k: u8) -> Result<u8, PulseCacheError> {
249    let off = match cache_run_offset(band, lm) {
250        Ok(off) => off,
251        Err(e) => return Err(e),
252    };
253    let max_k = CACHE_BITS50[off];
254    if k == 0 || k > max_k {
255        return Err(PulseCacheError::PulseCountOutOfRange { k, max_k });
256    }
257    Ok(CACHE_BITS50[off + k as usize])
258}
259
260/// §4.3.4.1 *Bits to Pulses* inversion: the largest pulse count `K`
261/// whose cost fits the per-band budget `b_target` (1/8 bits).
262///
263/// Scans the run's monotone cost curve `qbits[1..=maxK]` and returns
264/// the largest `K` with `qbits[K] <= b_target`, or `0` when not even a
265/// single pulse fits. `K = maxK` is allowed (the whole run fits).
266///
267/// Returns [`PulseCacheError::SentinelTuple`] for a sentinel tuple
268/// (the caller uses its closed-form path), or the range errors from
269/// [`cache_flat_index`].
270pub const fn bits_to_pulses(band: usize, lm: usize, b_target: u8) -> Result<u8, PulseCacheError> {
271    let off = match cache_run_offset(band, lm) {
272        Ok(off) => off,
273        Err(e) => return Err(e),
274    };
275    let max_k = CACHE_BITS50[off];
276    let mut k: u8 = 0;
277    let mut probe: u8 = 1;
278    while probe <= max_k {
279        if CACHE_BITS50[off + probe as usize] > b_target {
280            break;
281        }
282        k = probe;
283        probe += 1;
284    }
285    Ok(k)
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    #[test]
293    fn index_table_length_matches_band_lm_grid() {
294        assert_eq!(CACHE_INDEX50.len(), CACHE_INDEX_LEN);
295        assert_eq!(CACHE_INDEX_LEN, CELT_NUM_BANDS * CACHE_LM_COUNT);
296        assert_eq!(CACHE_INDEX_LEN, 105);
297    }
298
299    #[test]
300    fn bits_table_length_is_392() {
301        assert_eq!(CACHE_BITS50.len(), CACHE_BITS_LEN);
302        assert_eq!(CACHE_BITS_LEN, 392);
303    }
304
305    #[test]
306    fn exactly_eight_sentinels_in_bands_zero_and_one() {
307        let sentinels: Vec<usize> = (0..CACHE_INDEX_LEN)
308            .filter(|&i| CACHE_INDEX50[i] == CACHE_INDEX_SENTINEL)
309            .collect();
310        assert_eq!(sentinels.len(), 8);
311        // Band 0 at all five LM = flat indices 0..=4; band 1 at
312        // LM 0,1,2 = flat indices 5,6,7.
313        assert_eq!(sentinels, vec![0, 1, 2, 3, 4, 5, 6, 7]);
314    }
315
316    #[test]
317    fn sentinel_lookup_returns_sentinel_error() {
318        // Band 0, LM 0 is a sentinel.
319        assert_eq!(
320            cache_run_offset(0, 0),
321            Err(PulseCacheError::SentinelTuple { band: 0, lm: 0 })
322        );
323        // Band 1, LM 2 is a sentinel; LM 3 is the first non-sentinel.
324        assert_eq!(
325            cache_run_offset(1, 2),
326            Err(PulseCacheError::SentinelTuple { band: 1, lm: 2 })
327        );
328        assert_eq!(cache_run_offset(1, 3), Ok(0));
329    }
330
331    #[test]
332    fn twenty_three_distinct_run_offsets() {
333        let mut offsets: Vec<i16> = CACHE_INDEX50
334            .iter()
335            .copied()
336            .filter(|&o| o != CACHE_INDEX_SENTINEL)
337            .collect();
338        offsets.sort_unstable();
339        offsets.dedup();
340        assert_eq!(offsets.len(), 23);
341        assert_eq!(
342            offsets,
343            vec![
344                0, 41, 82, 123, 164, 200, 222, 240, 266, 283, 295, 305, 318, 328, 336, 343, 351,
345                358, 364, 370, 376, 382, 387,
346            ]
347        );
348    }
349
350    #[test]
351    fn runs_pack_every_byte_exactly() {
352        // Walking each distinct run by its leading maxK byte must cover
353        // all 392 bytes with no gaps or overlaps.
354        let mut offsets: Vec<usize> = CACHE_INDEX50
355            .iter()
356            .copied()
357            .filter(|&o| o != CACHE_INDEX_SENTINEL)
358            .map(|o| o as usize)
359            .collect();
360        offsets.sort_unstable();
361        offsets.dedup();
362        let mut total = 0usize;
363        for &off in &offsets {
364            let max_k = CACHE_BITS50[off] as usize;
365            total += 1 + max_k;
366        }
367        assert_eq!(total, CACHE_BITS_LEN);
368    }
369
370    #[test]
371    fn each_run_cost_curve_is_monotone_nondecreasing() {
372        let mut offsets: Vec<usize> = CACHE_INDEX50
373            .iter()
374            .copied()
375            .filter(|&o| o != CACHE_INDEX_SENTINEL)
376            .map(|o| o as usize)
377            .collect();
378        offsets.sort_unstable();
379        offsets.dedup();
380        for &off in &offsets {
381            let max_k = CACHE_BITS50[off] as usize;
382            for k in 2..=max_k {
383                assert!(
384                    CACHE_BITS50[off + k] >= CACHE_BITS50[off + k - 1],
385                    "run at {off} not monotone at k={k}"
386                );
387            }
388        }
389    }
390
391    #[test]
392    fn max_pulses_caps_at_forty() {
393        let mut offsets: Vec<usize> = CACHE_INDEX50
394            .iter()
395            .copied()
396            .filter(|&o| o != CACHE_INDEX_SENTINEL)
397            .map(|o| o as usize)
398            .collect();
399        offsets.sort_unstable();
400        offsets.dedup();
401        for &off in &offsets {
402            assert!(CACHE_BITS50[off] <= CACHE_MAX_PULSES);
403        }
404    }
405
406    #[test]
407    fn first_run_is_flat_seven() {
408        // Run at offset 0 (band 1 / LM 3): maxK = 40, qbits[1..=40] = 7.
409        assert_eq!(cache_max_pulses(1, 3), Ok(40));
410        for k in 1..=40u8 {
411            assert_eq!(cache_pulse_cost(1, 3, k), Ok(7));
412        }
413    }
414
415    #[test]
416    fn pulse_cost_rejects_zero_and_overflow_k() {
417        // Run at offset 0 has maxK = 40.
418        assert_eq!(
419            cache_pulse_cost(1, 3, 0),
420            Err(PulseCacheError::PulseCountOutOfRange { k: 0, max_k: 40 })
421        );
422        assert_eq!(
423            cache_pulse_cost(1, 3, 41),
424            Err(PulseCacheError::PulseCountOutOfRange { k: 41, max_k: 40 })
425        );
426    }
427
428    #[test]
429    fn bits_to_pulses_flat_run_fits_all_at_budget_seven() {
430        // Flat run (qbits all 7): a budget of 7 fits all 40 pulses.
431        assert_eq!(bits_to_pulses(1, 3, 7), Ok(40));
432        // A budget of 6 fits none (every cost is 7 > 6).
433        assert_eq!(bits_to_pulses(1, 3, 6), Ok(0));
434    }
435
436    #[test]
437    fn bits_to_pulses_picks_exact_threshold() {
438        // Run at offset 41 (band 2 / LM 2): qbits[1]=15, qbits[2]=23,
439        // qbits[3]=28, ... A budget of 23 should fit exactly K=2.
440        assert_eq!(cache_pulse_cost(2, 2, 1), Ok(15));
441        assert_eq!(cache_pulse_cost(2, 2, 2), Ok(23));
442        assert_eq!(cache_pulse_cost(2, 2, 3), Ok(28));
443        assert_eq!(bits_to_pulses(2, 2, 23), Ok(2));
444        // One below the K=2 cost fits only K=1.
445        assert_eq!(bits_to_pulses(2, 2, 22), Ok(1));
446        // One below the K=1 cost fits nothing.
447        assert_eq!(bits_to_pulses(2, 2, 14), Ok(0));
448    }
449
450    #[test]
451    fn bits_to_pulses_saturating_budget_returns_max_k() {
452        // A budget of 255 (the max byte) fits the entire run.
453        let max_k = cache_max_pulses(2, 2).unwrap();
454        assert_eq!(bits_to_pulses(2, 2, 255), Ok(max_k));
455    }
456
457    #[test]
458    fn bits_to_pulses_on_sentinel_signals_closed_form() {
459        assert_eq!(
460            bits_to_pulses(0, 0, 100),
461            Err(PulseCacheError::SentinelTuple { band: 0, lm: 0 })
462        );
463    }
464
465    #[test]
466    fn lookup_rejects_out_of_range_band_and_lm() {
467        assert_eq!(
468            cache_flat_index(CELT_NUM_BANDS, 0),
469            Err(PulseCacheError::BandOutOfRange {
470                band: CELT_NUM_BANDS
471            })
472        );
473        assert_eq!(
474            cache_flat_index(0, CACHE_LM_COUNT),
475            Err(PulseCacheError::LmOutOfRange { lm: CACHE_LM_COUNT })
476        );
477    }
478
479    #[test]
480    fn bits_to_pulses_monotone_in_budget() {
481        // For a fixed non-sentinel tuple, K is non-decreasing in budget.
482        let mut prev = 0u8;
483        for b in 0..=255u8 {
484            let k = bits_to_pulses(5, 4, b).unwrap();
485            assert!(k >= prev, "K decreased at budget {b}");
486            prev = k;
487        }
488        // At saturation it reaches the run's maxK.
489        assert_eq!(prev, cache_max_pulses(5, 4).unwrap());
490    }
491
492    #[test]
493    fn last_run_at_offset_387_has_max_k_four() {
494        // Run at offset 387 (band 20 / LM 4): maxK = 4, qbits =
495        // [67, 127, 182, 234].
496        assert_eq!(cache_run_offset(20, 4), Ok(387));
497        assert_eq!(cache_max_pulses(20, 4), Ok(4));
498        assert_eq!(cache_pulse_cost(20, 4, 1), Ok(67));
499        assert_eq!(cache_pulse_cost(20, 4, 4), Ok(234));
500    }
501}