Skip to main content

ryg_rans_rs_cli/container/
model.rs

1//! # Canonical model encoding and normalization
2//!
3//! Implements the normative deterministic model normalization algorithm.
4//! All arithmetic is integer-only.  Results are platform-independent.
5
6use crate::error::{AppError, FormatError};
7use sha2::{Digest, Sha256};
8
9/// A canonical sparse frequency model.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct FrequencyModel {
12    /// Normalized frequencies, indexed by symbol.
13    pub frequencies: Vec<u32>,
14    /// Cumulative frequencies (length 257).
15    pub cumulative: Vec<u32>,
16    /// Scale bits used.
17    pub scale_bits: u8,
18    /// Number of active (non-zero) symbols.
19    pub active_symbols: usize,
20}
21
22impl FrequencyModel {
23    /// Build a normalized model from a raw histogram.
24    ///
25    /// Algorithm:
26    /// 1. Count occurrences per symbol (0..255).
27    /// 2. Compute quota: `floor(count[sym] * total / total_count)`.
28    /// 3. Enforce minimum 1 per observed symbol.
29    /// 4. Distribute remainder in descending remainder order.
30    /// 5. If over target due to minimum-1 enforcement, remove from
31    ///    symbols with smallest remainder, never going below 1.
32    ///
33    /// This is fully deterministic — no floating point, no platform dependency.
34    pub fn build(histogram: &[u64; 256], scale_bits: u8) -> Result<Self, AppError> {
35        let target_total = 1u64 << scale_bits;
36
37        // Count total observed symbols
38        let total_count: u64 = histogram.iter().sum();
39        if total_count == 0 {
40            return Err(AppError::Format(FormatError {
41                detail: "empty histogram: no data to build model".into(),
42                block_index: None,
43                offset: None,
44            }));
45        }
46
47        let mut frequencies = [0u64; 256];
48        let mut remainders = [0u64; 256];
49        let mut active_count = 0usize;
50
51        // Phase 1: compute base frequency and remainder for each symbol
52        for (sym, &count) in histogram.iter().enumerate() {
53            if count == 0 {
54                continue;
55            }
56            let quota_num = count * target_total;
57            let base = quota_num / total_count;
58            let rem = quota_num % total_count;
59
60            frequencies[sym] = base.max(1); // minimum 1
61            remainders[sym] = rem;
62            active_count += 1;
63        }
64
65        // Phase 2: compute current sum and adjust
66        let mut current_sum: u64 = frequencies.iter().sum();
67
68        // If below target, add units in descending remainder order
69        if current_sum < target_total {
70            // Collect symbols with non-zero remainders, sorted by remainder descending
71            let mut candidates: Vec<(u64, usize)> = remainders
72                .iter()
73                .enumerate()
74                .filter(|&(sym_idx, &r)| r > 0 && frequencies[sym_idx] > 0)
75                .map(|(sym, &rem)| (rem, sym))
76                .collect();
77            // Sort by remainder descending, then by original count descending, then by symbol
78            candidates.sort_by(|a, b| {
79                b.0.cmp(&a.0) // remainder descending
80                    .then_with(|| histogram[a.1].cmp(&histogram[b.1]).reverse()) // count descending
81                    .then_with(|| a.1.cmp(&b.1)) // symbol ascending
82            });
83
84            for (_, sym) in candidates.iter().cycle() {
85                if current_sum >= target_total {
86                    break;
87                }
88                frequencies[*sym] += 1;
89                current_sum += 1;
90            }
91        }
92
93        // If above target (due to minimum-1 enforcement), remove units
94        if current_sum > target_total {
95            let excess = (current_sum - target_total) as usize;
96
97            // Collect symbols eligible for reduction (frequency > 1)
98            let mut candidates: Vec<(u64, usize)> = (0..256)
99                .filter(|&sym| frequencies[sym] > 1)
100                .map(|sym| (remainders[sym], sym))
101                .collect();
102            // Sort by remainder ascending, then by frequency descending, then by symbol descending
103            candidates.sort_by(|a, b| {
104                a.0.cmp(&b.0) // remainder ascending
105                    .then_with(|| b.1.cmp(&a.1)) // frequency descending
106                    .then_with(|| b.1.cmp(&a.1)) // symbol descending
107            });
108
109            for (_, sym) in candidates.iter() {
110                if excess == 0 {
111                    break;
112                }
113                if frequencies[*sym] > 1 {
114                    frequencies[*sym] -= 1;
115                }
116            }
117        }
118
119        // Ensure exact match
120        debug_assert_eq!(frequencies.iter().sum::<u64>(), target_total);
121
122        // Convert to u32 (safe: target_total <= u32::MAX for scale_bits <= 31)
123        let freq_u32: Vec<u32> = frequencies.iter().map(|&f| f as u32).collect();
124
125        // Build cumulative
126        let mut cum = Vec::with_capacity(257);
127        let mut acc = 0u32;
128        cum.push(0);
129        for &f in freq_u32.iter() {
130            acc = acc.checked_add(f).unwrap();
131            cum.push(acc);
132        }
133
134        Ok(Self {
135            frequencies: freq_u32,
136            cumulative: cum,
137            scale_bits,
138            active_symbols: active_count,
139        })
140    }
141
142    /// Serialize to canonical binary format.
143    pub fn to_bytes(&self) -> Vec<u8> {
144        let entries: Vec<(u8, u32)> = (0..=255u16)
145            .filter(|&s| self.frequencies[s as usize] > 0)
146            .map(|s| (s as u8, self.frequencies[s as usize]))
147            .collect();
148
149        let mut buf = Vec::with_capacity(2 + entries.len() * 5);
150        buf.extend_from_slice(&(entries.len() as u16).to_le_bytes());
151        for (sym, freq) in &entries {
152            buf.push(*sym);
153            buf.extend_from_slice(&freq.to_le_bytes());
154        }
155        buf
156    }
157
158    /// Parse from canonical binary format.
159    pub fn from_bytes(bytes: &[u8], scale_bits: u8) -> Result<Self, AppError> {
160        if bytes.len() < 2 {
161            return Err(AppError::Format(FormatError {
162                detail: "model too short".into(),
163                block_index: None,
164                offset: None,
165            }));
166        }
167
168        let entry_count = u16::from_le_bytes([bytes[0], bytes[1]]) as usize;
169        let expected_len = 2 + entry_count * 5;
170        if bytes.len() < expected_len {
171            return Err(AppError::Format(FormatError {
172                detail: format!(
173                    "model truncated: expected {} bytes, got {}",
174                    expected_len,
175                    bytes.len()
176                ),
177                block_index: None,
178                offset: None,
179            }));
180        }
181
182        let mut frequencies = vec![0u32; 256];
183        let mut prev_sym: Option<u8> = None;
184
185        for i in 0..entry_count {
186            let offset = 2 + i * 5;
187            let sym = bytes[offset];
188            let freq = u32::from_le_bytes([
189                bytes[offset + 1],
190                bytes[offset + 2],
191                bytes[offset + 3],
192                bytes[offset + 4],
193            ]);
194
195            // Check ascending order
196            if let Some(prev) = prev_sym {
197                if sym <= prev {
198                    return Err(AppError::Format(FormatError {
199                        detail: format!("duplicate or non-ascending symbol {} after {}", sym, prev),
200                        block_index: None,
201                        offset: None,
202                    }));
203                }
204            }
205
206            if freq == 0 {
207                return Err(AppError::Format(FormatError {
208                    detail: format!("zero frequency for symbol {}", sym),
209                    block_index: None,
210                    offset: None,
211                }));
212            }
213
214            frequencies[sym as usize] = freq;
215            prev_sym = Some(sym);
216        }
217
218        // Verify sum
219        let total = 1u64 << scale_bits;
220        let sum: u64 = frequencies.iter().map(|&f| f as u64).sum();
221        if sum != total {
222            return Err(AppError::Format(FormatError {
223                detail: format!(
224                    "model frequency sum {} does not match target {}",
225                    sum, total
226                ),
227                block_index: None,
228                offset: None,
229            }));
230        }
231
232        // Build cumulative
233        let mut cum = Vec::with_capacity(257);
234        let mut acc = 0u32;
235        cum.push(0);
236        for &f in frequencies.iter() {
237            acc = acc.checked_add(f).unwrap();
238            cum.push(acc);
239        }
240
241        let active = frequencies.iter().filter(|&&f| f > 0).count();
242
243        Ok(Self {
244            frequencies,
245            cumulative: cum,
246            scale_bits,
247            active_symbols: active,
248        })
249    }
250
251    /// Compute SHA-256 of the canonical serialization.
252    pub fn sha256(&self) -> [u8; 32] {
253        let bytes = self.to_bytes();
254        let mut hasher = Sha256::new();
255        hasher.update(&bytes);
256        let result = hasher.finalize();
257        let mut hash = [0u8; 32];
258        hash.copy_from_slice(&result);
259        hash
260    }
261
262    /// Build a uniform model: all 256 symbols get equal (or nearly equal) frequency.
263    pub fn build_uniform(scale_bits: u8) -> Self {
264        let total = 1u64 << scale_bits;
265        let base = total / 256;
266        let mut frequencies = vec![base as u32; 256];
267        let remainder = total - base * 256;
268        if remainder > 0 {
269            frequencies[255] += remainder as u32;
270        }
271
272        let mut cum = Vec::with_capacity(257);
273        let mut acc = 0u32;
274        cum.push(0);
275        for &f in frequencies.iter() {
276            acc = acc.checked_add(f).unwrap();
277            cum.push(acc);
278        }
279
280        Self {
281            frequencies,
282            cumulative: cum,
283            scale_bits,
284            active_symbols: 256,
285        }
286    }
287}
288
289/// Compute a histogram (count of each byte value).
290pub fn compute_histogram(data: &[u8]) -> [u64; 256] {
291    let mut hist = [0u64; 256];
292    for &b in data {
293        hist[b as usize] += 1;
294    }
295    hist
296}