Skip to main content

datacortex_core/model/
engine.rs

1//! CMEngine -- orchestrates all context models + mixer + APM.
2//!
3//! Phase 5+: Multi-output ContextMap engine.
4//!
5//! Each order model (O1-O9) now produces 3 predictions:
6//! - StateMap prediction (primary)
7//! - Run-count prediction (consecutive same-bit tracking)
8//! - Byte-history prediction (last byte seen in this context)
9//!
10//! Total mixer inputs: 1 (O0) + 9*3 (O1-O9) + 9 (other) = 37.
11//!
12//! Both presets use:
13//! - Triple logistic mixer (fine 64K + med 16K + coarse 4K)
14//! - 7-stage APM cascade
15//!
16//! Probability always in [1, 4095] -- clamped after every operation.
17//! CRITICAL: Encoder/decoder must use IDENTICAL prediction sequence.
18
19use crate::mixer::apm::APMStage;
20use crate::mixer::dual_mixer::{NUM_MODELS, byte_class};
21use crate::mixer::isse::IsseChain;
22use crate::mixer::multi_set_mixer::MultiSetMixer;
23use crate::model::cm_model::{AssociativeContextModel, ChecksumContextModel, ContextModel};
24use crate::model::dmc_model::DmcModel;
25use crate::model::indirect_model::IndirectModel;
26use crate::model::json_model::JsonModel;
27use crate::model::match_model::MatchModel;
28use crate::model::order0::Order0Model;
29use crate::model::ppm_model::{PpmConfig, PpmModel};
30use crate::model::run_model::RunModel;
31use crate::model::sparse_model::SparseModel;
32use crate::model::word_model::WordModel;
33
34/// Configuration for the CM engine -- controls memory/quality trade-off.
35///
36/// Each field specifies the ContextMap size in bytes for the corresponding model.
37/// Larger sizes reduce hash collisions and improve prediction accuracy at the
38/// cost of more memory and slightly slower cache performance.
39#[derive(Debug, Clone)]
40pub struct CMConfig {
41    /// Order-1 ContextMap size (default: 32MB).
42    pub order1_size: usize,
43    /// Order-2 ContextMap size (default: 16MB).
44    pub order2_size: usize,
45    /// Order-3 ChecksumContextMap size (default: 32MB).
46    pub order3_size: usize,
47    /// Order-4 ChecksumContextMap size (default: 32MB).
48    pub order4_size: usize,
49    /// Order-5 AssociativeContextMap size (default: 32MB).
50    pub order5_size: usize,
51    /// Order-6 AssociativeContextMap size (default: 16MB).
52    pub order6_size: usize,
53    /// Order-7 AssociativeContextMap size (default: 32MB).
54    pub order7_size: usize,
55    /// Order-8 AssociativeContextMap size (default: 32MB).
56    pub order8_size: usize,
57    /// Order-9 AssociativeContextMap size (default: 16MB).
58    pub order9_size: usize,
59    /// Match model ring buffer size in bytes (default: 16MB). Must be power of 2.
60    pub match_ring_size: usize,
61    /// Match model hash table entry count (default: 8M). Must be power of 2.
62    pub match_hash_size: usize,
63    /// Word model ContextMap size (default: 16MB).
64    pub word_size: usize,
65    /// Sparse model ContextMap size per gap context (default: 8MB, 16MB total).
66    pub sparse_size: usize,
67    /// Run model ContextMap size (default: 4MB).
68    pub run_size: usize,
69    /// JSON model ContextMap size (default: 8MB).
70    pub json_size: usize,
71    /// PPM model table sizes configuration.
72    pub ppm_config: PpmConfig,
73}
74
75impl CMConfig {
76    /// Balanced preset: production sizes (~256MB CM + ~360MB PPM).
77    pub fn balanced() -> Self {
78        CMConfig {
79            order1_size: 1 << 25,      // 32MB
80            order2_size: 1 << 24,      // 16MB
81            order3_size: 1 << 25,      // 32MB
82            order4_size: 1 << 25,      // 32MB
83            order5_size: 1 << 25,      // 32MB
84            order6_size: 1 << 24,      // 16MB
85            order7_size: 1 << 25,      // 32MB
86            order8_size: 1 << 25,      // 32MB
87            order9_size: 1 << 24,      // 16MB
88            match_ring_size: 16 << 20, // 16MB
89            match_hash_size: 8 << 20,  // 8M entries
90            word_size: 1 << 24,        // 16MB
91            sparse_size: 1 << 23,      // 8MB per gap (16MB total)
92            run_size: 1 << 22,         // 4MB
93            json_size: 1 << 23,        // 8MB
94            ppm_config: PpmConfig::scaled_4x(),
95        }
96    }
97
98    /// Max preset: 2x everything for better compression (~512MB CM + ~360MB PPM).
99    /// Doubles all ContextMap sizes, match ring, and hash table.
100    /// More context slots = fewer collisions = better predictions.
101    pub fn max() -> Self {
102        CMConfig {
103            order1_size: 1 << 26,      // 64MB
104            order2_size: 1 << 25,      // 32MB
105            order3_size: 1 << 26,      // 64MB
106            order4_size: 1 << 26,      // 64MB
107            order5_size: 1 << 26,      // 64MB
108            order6_size: 1 << 25,      // 32MB
109            order7_size: 1 << 26,      // 64MB
110            order8_size: 1 << 26,      // 64MB
111            order9_size: 1 << 25,      // 32MB
112            match_ring_size: 32 << 20, // 32MB
113            match_hash_size: 16 << 20, // 16M entries
114            word_size: 1 << 25,        // 32MB
115            sparse_size: 1 << 24,      // 16MB per gap (32MB total)
116            run_size: 1 << 23,         // 8MB
117            json_size: 1 << 24,        // 16MB
118            ppm_config: PpmConfig::scaled_4x(),
119        }
120    }
121}
122
123/// Context mixing engine -- orchestrates all models, mixer, and APM.
124pub struct CMEngine {
125    // --- Models ---
126    /// Order-0: 256-context partial byte predictor.
127    order0: Order0Model,
128    /// Order-1: previous byte + partial byte context. ContextMap 32MB.
129    order1: ContextModel,
130    /// Order-2: previous 2 bytes + partial byte context. ContextMap 16MB.
131    order2: ContextModel,
132    /// Order-3: previous 3 bytes + partial byte context. ChecksumContextMap 32MB.
133    order3: ChecksumContextModel,
134    /// Order-4: previous 4 bytes + partial byte context. ChecksumContextMap 32MB.
135    order4: ChecksumContextModel,
136    /// Order-5: previous 5 bytes + partial byte context. AssociativeContextMap 32MB.
137    order5: AssociativeContextModel,
138    /// Order-6: previous 6 bytes + partial byte context. AssociativeContextMap 16MB.
139    order6: AssociativeContextModel,
140    /// Order-7: previous 7 bytes + partial byte context. AssociativeContextMap 32MB.
141    order7: AssociativeContextModel,
142    /// Order-8: previous 8 bytes + partial byte context. AssociativeContextMap 32MB.
143    order8: AssociativeContextModel,
144    /// Order-9: previous 9 bytes + partial byte context. AssociativeContextMap 16MB.
145    order9: AssociativeContextModel,
146    /// Match model: ring buffer (16MB) + hash table (8M entries).
147    match_model: MatchModel,
148    /// Word model: word boundary context. ContextMap 16MB.
149    word_model: WordModel,
150    /// Sparse model: skip-byte context for periodic patterns. 8MB total.
151    sparse_model: SparseModel,
152    /// Run model: run-length context. 2MB.
153    run_model: RunModel,
154    /// JSON model: structure-aware context. 4MB.
155    json_model: JsonModel,
156    /// Indirect model: second-order context prediction. ~2MB.
157    indirect_model: IndirectModel,
158    /// PPM model: byte-level prediction by partial matching (PPMd Method D).
159    /// Different paradigm from CM — trie/hash-based, byte-level, adaptive order with exclusion.
160    ppm_model: PpmModel,
161    /// DMC model: bit-level dynamic Markov compression automaton.
162    /// State-cloning captures sub-byte and cross-byte patterns.
163    dmc_model: DmcModel,
164
165    // --- Mixer + APM ---
166    /// Triple logistic mixer (fine 64K + medium 16K + coarse 4K).
167    mixer: MultiSetMixer,
168    /// APM Stage 1: 2K contexts (c0 * bpos), 50% blend.
169    apm1: APMStage,
170    /// APM Stage 2: 16K contexts (c1 * bpos * byte_class), 25% blend.
171    apm2: APMStage,
172    /// APM Stage 3: 4K contexts (match_q * c1_top4 * bpos), 20% blend.
173    apm3: APMStage,
174    /// APM Stage 4: 4K contexts (byte_class pair transition + run), 15% blend.
175    apm4: APMStage,
176    /// APM Stage 5: 2K contexts (c3_top4 * c2_top4 * bpos), 12% blend.
177    /// Captures longer-range byte patterns (trigram character class).
178    apm5: APMStage,
179    /// APM Stage 6: 2K contexts (match_length_quantized * c1_class * bpos), 10% blend.
180    /// Match-dependent refinement.
181    apm6: APMStage,
182    /// APM Stage 7: 2K contexts (line_pos_q * bpos * c1_class), 10% blend.
183    /// Position-within-line aware refinement.
184    apm7: APMStage,
185    /// ISSE model: 3-level ICM→ISSE→ISSE chain used as model #37 in the mixer.
186    /// Provides a complementary ZPAQ-style cascaded prediction.
187    isse_model: IsseChain,
188
189    // --- Context state ---
190    /// Partial byte being decoded (1-255). Starts at 1.
191    c0: u32,
192    /// Last completed byte.
193    c1: u8,
194    /// Second-to-last completed byte.
195    c2: u8,
196    /// Third-to-last completed byte.
197    c3: u8,
198    /// Fourth-to-last completed byte.
199    c4: u8,
200    /// Fifth-to-last completed byte.
201    c5: u8,
202    /// Sixth-to-last completed byte.
203    c6: u8,
204    /// Seventh-to-last completed byte.
205    c7: u8,
206    /// Eighth-to-last completed byte.
207    c8: u8,
208    /// Ninth-to-last completed byte.
209    c9: u8,
210    /// Bit position within current byte (0-7).
211    bpos: u8,
212    /// Byte-level run length (consecutive identical bytes).
213    run_len: u8,
214    /// Distance since last newline (quantized).
215    line_pos: u16,
216}
217
218impl CMEngine {
219    /// Create a new CM engine with balanced (default) configuration.
220    pub fn new() -> Self {
221        Self::with_config(CMConfig::balanced())
222    }
223
224    /// Create a CM engine with a specific configuration.
225    pub fn with_config(config: CMConfig) -> Self {
226        CMEngine {
227            order0: Order0Model::new(),
228            order1: ContextModel::new(config.order1_size),
229            order2: ContextModel::new(config.order2_size),
230            order3: ChecksumContextModel::new(config.order3_size),
231            order4: ChecksumContextModel::new(config.order4_size),
232            order5: AssociativeContextModel::new(config.order5_size),
233            order6: AssociativeContextModel::new(config.order6_size),
234            order7: AssociativeContextModel::new(config.order7_size),
235            order8: AssociativeContextModel::new(config.order8_size),
236            order9: AssociativeContextModel::new(config.order9_size),
237            match_model: MatchModel::with_sizes(config.match_ring_size, config.match_hash_size),
238            word_model: WordModel::with_size(config.word_size),
239            sparse_model: SparseModel::with_size(config.sparse_size),
240            run_model: RunModel::with_size(config.run_size),
241            json_model: JsonModel::with_size(config.json_size),
242            indirect_model: IndirectModel::new(),
243            ppm_model: PpmModel::with_config(config.ppm_config),
244            dmc_model: DmcModel::new_single(),
245            mixer: MultiSetMixer::new(),
246            apm1: APMStage::new(2048, 55),  // c0(256) * bpos(8) = 2048
247            apm2: APMStage::new(16384, 30), // c1*bpos*byte_class = 256*8*8 = 16K
248            apm3: APMStage::new(4096, 25), // match_q(4) * c2_top2(4) * c1_top4(16) * bpos(8) = 2048, use 4K
249            apm4: APMStage::new(4096, 15), // bclass(8) * bc2(8) * bpos(8) * run_q(4) -> 4K
250            apm5: APMStage::new(4096, 15), // c3_top4(16) * c2_top4(16) * bpos(8) -> 2K mapped to 4K
251            apm6: APMStage::new(2048, 12), // match_q(4) * c1_class(8) * bpos(8) -> 256 mapped to 2K
252            apm7: APMStage::new(4096, 12), // line_pos_q(4) * bpos(8) * c1_class(8) -> 256 mapped to 4K
253            isse_model: IsseChain::new(),
254            c0: 1,
255            c1: 0,
256            c2: 0,
257            c3: 0,
258            c4: 0,
259            c5: 0,
260            c6: 0,
261            c7: 0,
262            c8: 0,
263            c9: 0,
264            bpos: 0,
265            run_len: 0,
266            line_pos: 0,
267        }
268    }
269
270    /// Predict probability of the next bit being 1.
271    /// Returns 12-bit probability in [1, 4095].
272    ///
273    /// CRITICAL: encoder and decoder must call this with identical state.
274    #[inline(always)]
275    pub fn predict(&mut self) -> u32 {
276        // --- Gather predictions from each model ---
277        let c0 = self.c0;
278        let c1 = self.c1;
279        let c2 = self.c2;
280        let c3 = self.c3;
281        let c4 = self.c4;
282        let c5 = self.c5;
283        let c6 = self.c6;
284        let c7 = self.c7;
285        let bpos = self.bpos;
286
287        // Order-0: context is the partial byte. Single prediction.
288        let p0 = self.order0.predict(c0 as usize);
289
290        // Order-1 through Order-9: each produces (state_p, run_p).
291        let h1 = order1_hash(c1, c0);
292        let (p1_s, p1_r) = self.order1.predict_multi(h1);
293
294        let h2 = order2_hash(c2, c1, c0);
295        let (p2_s, p2_r) = self.order2.predict_multi(h2);
296
297        let h3 = order3_hash(c3, c2, c1, c0);
298        let (p3_s, p3_r) = self.order3.predict_multi(h3);
299
300        let h4 = order4_hash(c4, c3, c2, c1, c0);
301        let (p4_s, p4_r) = self.order4.predict_multi(h4);
302
303        let h5 = order5_hash(c5, c4, c3, c2, c1, c0);
304        let (p5_s, p5_r) = self.order5.predict_multi(h5);
305
306        let h6 = order6_hash(c6, c5, c4, c3, c2, c1, c0);
307        let (p6_s, p6_r) = self.order6.predict_multi(h6);
308
309        let h7 = order7_hash(c7, c6, c5, c4, c3, c2, c1, c0);
310        let (p7_s, p7_r) = self.order7.predict_multi(h7);
311
312        let c8 = self.c8;
313        let h8 = order8_hash(c8, c7, c6, c5, c4, c3, c2, c1, c0);
314        let (p8_s, p8_r) = self.order8.predict_multi(h8);
315
316        let c9 = self.c9;
317        let h9 = order9_hash(c9, c8, c7, c6, c5, c4, c3, c2, c1, c0);
318        let (p9_s, p9_r) = self.order9.predict_multi(h9);
319
320        // Match model.
321        let p_match = self.match_model.predict(c0, bpos, c1, c2, c3);
322
323        // Word model.
324        let p_word = self.word_model.predict(c0, bpos, c1);
325
326        // Sparse model (skip-byte patterns).
327        let p_sparse = self.sparse_model.predict(c0, c1, c2, c3);
328
329        // Run model (run-length patterns).
330        let p_run = self.run_model.predict(c0, bpos, c1);
331
332        // JSON model (structure-aware).
333        let p_json = self.json_model.predict(c0, bpos, c1);
334
335        // Indirect model (second-order context).
336        let p_indirect = self.indirect_model.predict(c0, bpos, c1);
337
338        // PPM model (byte-level prediction by partial matching).
339        // PPM updates at byte level; here we just convert cached byte probs to bit prediction.
340        let p_ppm = self.ppm_model.predict_bit(bpos, c0);
341
342        // DMC model (bit-level dynamic Markov compression).
343        let p_dmc = self.dmc_model.predict();
344
345        // ISSE model (3-level ICM→ISSE→ISSE chain with cross-context hashes).
346        let p_isse = self.isse_model.predict(c0, c1, c2, c3, bpos);
347
348        // --- Mix (28 models) ---
349        // Layout: [O0, O1_s, O1_r, O2_s, O2_r, ..., O9_s, O9_r,
350        //          match, word, sparse, run, json, indirect, ppm, dmc, isse]
351        let predictions: [u32; NUM_MODELS] = [
352            p0, p1_s, p1_r, p2_s, p2_r, p3_s, p3_r, p4_s, p4_r, p5_s, p5_r, p6_s, p6_r, p7_s, p7_r,
353            p8_s, p8_r, p9_s, p9_r, p_match, p_word, p_sparse, p_run, p_json, p_indirect, p_ppm,
354            p_dmc, p_isse,
355        ];
356        let bclass = byte_class(c1);
357        let match_q = self.match_model.match_length_quantized();
358        let run_q = quantize_run_for_mixer(self.run_len);
359
360        let mixed = self
361            .mixer
362            .predict(&predictions, c0, c1, c2, bpos, bclass, match_q, run_q, 0);
363
364        // --- APM cascade ---
365        // Stage 1: context = c0_partial(8b) + bpos(3b) + run_q(2b) = 13 bits -> 2048 contexts (folded).
366        let apm1_ctx = (((c0 as usize & 0xFF) << 3) | bpos as usize)
367            .wrapping_mul(5)
368            .wrapping_add(run_q as usize & 0x3)
369            & 2047;
370        let after_apm1 = self.apm1.predict(mixed, apm1_ctx);
371
372        // Stage 2: context = c1(8b) * bpos(3b) + byte_class(3b) + c2_top4(4b).
373        let apm2_ctx = (((c1 as usize) << 3 | bpos as usize) * 8 + bclass as usize)
374            .wrapping_mul(17)
375            .wrapping_add(c2 as usize >> 4)
376            & 16383;
377        let after_apm2 = self.apm2.predict(after_apm1, apm2_ctx);
378
379        // Stage 3: context = match_q(2b) * c1_top4(4b) * bpos(3b) * c2_top2(2b) + match_len_q(2b).
380        let apm3_ctx = ((match_q as usize * 512)
381            + ((c2 as usize >> 6) << 7)
382            + ((c1 as usize >> 4) << 3)
383            + bpos as usize)
384            .wrapping_mul(5)
385            .wrapping_add(match_q as usize)
386            & 4095;
387        let after_apm3 = self.apm3.predict(after_apm2, apm3_ctx);
388
389        // Stage 4 (Neural APM): byte-class pair transition + run context.
390        // Uses byte_class(c1) x byte_class(c2) x bpos x run_q as context.
391        // This captures character-class transitions and run patterns that
392        // other APM stages miss.
393        let bc2 = byte_class(c2);
394        let apm4_ctx = (bclass as usize * 8 + bc2 as usize)
395            .wrapping_mul(33)
396            .wrapping_add(bpos as usize * 4 + run_q as usize)
397            & 4095;
398        let after_apm4 = self.apm4.predict(after_apm3, apm4_ctx);
399
400        // Stage 5: Longer-range byte pattern context.
401        // (c3_top4, c2_top4, c1_top2, bpos) — captures trigram character patterns.
402        let apm5_ctx = ((c3 as usize >> 4).wrapping_mul(67) + (c2 as usize >> 4))
403            .wrapping_mul(67)
404            .wrapping_add((c1 as usize >> 6) * 8 + bpos as usize)
405            & 4095;
406        let after_apm5 = self.apm5.predict(after_apm4, apm5_ctx);
407
408        // Stage 6: Match-dependent refinement (match_q, c1_class, bpos).
409        // When a match is active, this helps the APM adapt to match confidence.
410        let apm6_ctx = (match_q as usize * 64 + bclass as usize * 8 + bpos as usize) & 2047;
411        let after_apm6 = self.apm6.predict(after_apm5, apm6_ctx);
412
413        // Stage 7: Position-within-line + byte nibble context.
414        // (line_pos_q, c0_partial, bpos) — captures column-dependent patterns.
415        let line_pos_q = quantize_line_pos(self.line_pos);
416        let apm7_ctx = ((line_pos_q as usize).wrapping_mul(67) + (c0 as usize & 0xFF))
417            .wrapping_mul(67)
418            .wrapping_add(bpos as usize)
419            & 4095;
420        let final_p = self.apm7.predict(after_apm6, apm7_ctx);
421
422        final_p.clamp(1, 4095)
423    }
424
425    /// Update all models after observing `bit`.
426    ///
427    /// CRITICAL: encoder and decoder must call this with identical state and bit.
428    #[inline(always)]
429    pub fn update(&mut self, bit: u8) {
430        // Update APM (reverse order: stage 7 first, then 6, 5, 4, 3, 2, 1).
431        self.apm7.update(bit);
432        self.apm6.update(bit);
433        self.apm5.update(bit);
434        self.apm4.update(bit);
435        self.apm3.update(bit);
436        self.apm2.update(bit);
437        self.apm1.update(bit);
438
439        // Update mixer.
440        self.mixer.update(bit);
441
442        // Update all models.
443        self.order0.update(self.c0 as usize, bit);
444        self.order1.update(bit);
445        self.order2.update(bit);
446        self.order3.update(bit);
447        self.order4.update(bit);
448        self.order5.update(bit);
449        self.order6.update(bit);
450        self.order7.update(bit);
451        self.order8.update(bit);
452        self.order9.update(bit);
453        self.match_model
454            .update(bit, self.bpos, self.c0, self.c1, self.c2);
455        self.word_model.update(bit);
456        self.sparse_model.update(bit);
457        self.run_model.update(bit);
458        self.json_model.update(bit);
459        self.indirect_model.update(bit);
460        self.dmc_model.update(bit);
461        self.isse_model.update(bit, self.c0, self.bpos);
462
463        // Advance context state.
464        self.c0 = (self.c0 << 1) | bit as u32;
465        self.bpos += 1;
466
467        if self.bpos >= 8 {
468            // Byte complete. Extract byte value and reset.
469            let byte = (self.c0 & 0xFF) as u8;
470            // Track run length.
471            if byte == self.c1 {
472                self.run_len = self.run_len.saturating_add(1);
473            } else {
474                self.run_len = 1;
475            }
476            // Track line position.
477            if byte == b'\n' {
478                self.line_pos = 0;
479            } else {
480                self.line_pos = self.line_pos.saturating_add(1);
481            }
482
483            // Update PPM model at byte level (NOT per-bit).
484            self.ppm_model.update_byte(byte);
485
486            // Notify DMC model of byte completion (for state reset to byte context).
487            self.dmc_model.on_byte_complete(byte);
488
489            self.c9 = self.c8;
490            self.c8 = self.c7;
491            self.c7 = self.c6;
492            self.c6 = self.c5;
493            self.c5 = self.c4;
494            self.c4 = self.c3;
495            self.c3 = self.c2;
496            self.c2 = self.c1;
497            self.c1 = byte;
498            self.c0 = 1; // reset partial byte
499            self.bpos = 0;
500        }
501    }
502}
503
504impl Default for CMEngine {
505    fn default() -> Self {
506        Self::new()
507    }
508}
509
510// --- Context hash functions ---
511// Use FNV-1a style hashing with different seeds per order for speed
512// and reasonable distribution. The seed ensures different orders produce
513// different hashes even with overlapping byte sequences.
514
515/// FNV-1a offset basis.
516const FNV_OFFSET: u32 = 0x811C9DC5;
517/// FNV-1a prime.
518const FNV_PRIME: u32 = 0x01000193;
519
520/// Order-1 context hash: combines last byte with partial byte.
521#[inline]
522fn order1_hash(c1: u8, c0: u32) -> u32 {
523    let mut h = FNV_OFFSET;
524    h ^= c1 as u32;
525    h = h.wrapping_mul(FNV_PRIME);
526    h ^= c0 & 0xFF;
527    h = h.wrapping_mul(FNV_PRIME);
528    h
529}
530
531/// Order-2 context hash: combines last 2 bytes with partial byte.
532#[inline]
533fn order2_hash(c2: u8, c1: u8, c0: u32) -> u32 {
534    let mut h = FNV_OFFSET;
535    h ^= c2 as u32;
536    h = h.wrapping_mul(FNV_PRIME);
537    h ^= c1 as u32;
538    h = h.wrapping_mul(FNV_PRIME);
539    h ^= c0 & 0xFF;
540    h = h.wrapping_mul(FNV_PRIME);
541    h
542}
543
544/// Order-3 context hash: combines last 3 bytes with partial byte.
545#[inline]
546fn order3_hash(c3: u8, c2: u8, c1: u8, c0: u32) -> u32 {
547    let mut h = FNV_OFFSET;
548    h ^= c3 as u32;
549    h = h.wrapping_mul(FNV_PRIME);
550    h ^= c2 as u32;
551    h = h.wrapping_mul(FNV_PRIME);
552    h ^= c1 as u32;
553    h = h.wrapping_mul(FNV_PRIME);
554    h ^= c0 & 0xFF;
555    h = h.wrapping_mul(FNV_PRIME);
556    h
557}
558
559/// Order-4 context hash: combines last 4 bytes with partial byte.
560#[inline]
561fn order4_hash(c4: u8, c3: u8, c2: u8, c1: u8, c0: u32) -> u32 {
562    let mut h = FNV_OFFSET;
563    h ^= c4 as u32;
564    h = h.wrapping_mul(FNV_PRIME);
565    h ^= c3 as u32;
566    h = h.wrapping_mul(FNV_PRIME);
567    h ^= c2 as u32;
568    h = h.wrapping_mul(FNV_PRIME);
569    h ^= c1 as u32;
570    h = h.wrapping_mul(FNV_PRIME);
571    h ^= c0 & 0xFF;
572    h = h.wrapping_mul(FNV_PRIME);
573    h
574}
575
576/// Order-5 context hash: combines last 5 bytes with partial byte.
577#[inline]
578fn order5_hash(c5: u8, c4: u8, c3: u8, c2: u8, c1: u8, c0: u32) -> u32 {
579    let mut h = FNV_OFFSET;
580    h ^= c5 as u32;
581    h = h.wrapping_mul(FNV_PRIME);
582    h ^= c4 as u32;
583    h = h.wrapping_mul(FNV_PRIME);
584    h ^= c3 as u32;
585    h = h.wrapping_mul(FNV_PRIME);
586    h ^= c2 as u32;
587    h = h.wrapping_mul(FNV_PRIME);
588    h ^= c1 as u32;
589    h = h.wrapping_mul(FNV_PRIME);
590    h ^= c0 & 0xFF;
591    h = h.wrapping_mul(FNV_PRIME);
592    h
593}
594
595/// Order-6 context hash: combines last 6 bytes with partial byte.
596#[inline]
597fn order6_hash(c6: u8, c5: u8, c4: u8, c3: u8, c2: u8, c1: u8, c0: u32) -> u32 {
598    let mut h = FNV_OFFSET;
599    h ^= c6 as u32;
600    h = h.wrapping_mul(FNV_PRIME);
601    h ^= c5 as u32;
602    h = h.wrapping_mul(FNV_PRIME);
603    h ^= c4 as u32;
604    h = h.wrapping_mul(FNV_PRIME);
605    h ^= c3 as u32;
606    h = h.wrapping_mul(FNV_PRIME);
607    h ^= c2 as u32;
608    h = h.wrapping_mul(FNV_PRIME);
609    h ^= c1 as u32;
610    h = h.wrapping_mul(FNV_PRIME);
611    h ^= c0 & 0xFF;
612    h = h.wrapping_mul(FNV_PRIME);
613    h
614}
615
616/// Order-7 context hash: combines last 7 bytes with partial byte.
617#[inline]
618#[allow(clippy::too_many_arguments)]
619fn order7_hash(c7: u8, c6: u8, c5: u8, c4: u8, c3: u8, c2: u8, c1: u8, c0: u32) -> u32 {
620    let mut h = FNV_OFFSET;
621    h ^= c7 as u32;
622    h = h.wrapping_mul(FNV_PRIME);
623    h ^= c6 as u32;
624    h = h.wrapping_mul(FNV_PRIME);
625    h ^= c5 as u32;
626    h = h.wrapping_mul(FNV_PRIME);
627    h ^= c4 as u32;
628    h = h.wrapping_mul(FNV_PRIME);
629    h ^= c3 as u32;
630    h = h.wrapping_mul(FNV_PRIME);
631    h ^= c2 as u32;
632    h = h.wrapping_mul(FNV_PRIME);
633    h ^= c1 as u32;
634    h = h.wrapping_mul(FNV_PRIME);
635    h ^= c0 & 0xFF;
636    h = h.wrapping_mul(FNV_PRIME);
637    h
638}
639
640/// Order-8 context hash: combines last 8 bytes with partial byte.
641#[inline]
642#[allow(clippy::too_many_arguments)]
643fn order8_hash(c8: u8, c7: u8, c6: u8, c5: u8, c4: u8, c3: u8, c2: u8, c1: u8, c0: u32) -> u32 {
644    let mut h = FNV_OFFSET;
645    h ^= c8 as u32;
646    h = h.wrapping_mul(FNV_PRIME);
647    h ^= c7 as u32;
648    h = h.wrapping_mul(FNV_PRIME);
649    h ^= c6 as u32;
650    h = h.wrapping_mul(FNV_PRIME);
651    h ^= c5 as u32;
652    h = h.wrapping_mul(FNV_PRIME);
653    h ^= c4 as u32;
654    h = h.wrapping_mul(FNV_PRIME);
655    h ^= c3 as u32;
656    h = h.wrapping_mul(FNV_PRIME);
657    h ^= c2 as u32;
658    h = h.wrapping_mul(FNV_PRIME);
659    h ^= c1 as u32;
660    h = h.wrapping_mul(FNV_PRIME);
661    h ^= c0 & 0xFF;
662    h = h.wrapping_mul(FNV_PRIME);
663    h
664}
665
666/// Order-9 context hash: combines last 9 bytes with partial byte.
667#[inline]
668#[allow(clippy::too_many_arguments)]
669fn order9_hash(
670    c9: u8,
671    c8: u8,
672    c7: u8,
673    c6: u8,
674    c5: u8,
675    c4: u8,
676    c3: u8,
677    c2: u8,
678    c1: u8,
679    c0: u32,
680) -> u32 {
681    let mut h = FNV_OFFSET;
682    h ^= c9 as u32;
683    h = h.wrapping_mul(FNV_PRIME);
684    h ^= c8 as u32;
685    h = h.wrapping_mul(FNV_PRIME);
686    h ^= c7 as u32;
687    h = h.wrapping_mul(FNV_PRIME);
688    h ^= c6 as u32;
689    h = h.wrapping_mul(FNV_PRIME);
690    h ^= c5 as u32;
691    h = h.wrapping_mul(FNV_PRIME);
692    h ^= c4 as u32;
693    h = h.wrapping_mul(FNV_PRIME);
694    h ^= c3 as u32;
695    h = h.wrapping_mul(FNV_PRIME);
696    h ^= c2 as u32;
697    h = h.wrapping_mul(FNV_PRIME);
698    h ^= c1 as u32;
699    h = h.wrapping_mul(FNV_PRIME);
700    h ^= c0 & 0xFF;
701    h = h.wrapping_mul(FNV_PRIME);
702    h
703}
704
705/// Quantize run length to 0-3 for mixer context.
706#[inline]
707fn quantize_run_for_mixer(run_len: u8) -> u8 {
708    match run_len {
709        0..=1 => 0,
710        2..=3 => 1,
711        4..=8 => 2,
712        _ => 3,
713    }
714}
715
716/// Quantize line position to 0-3 for APM context.
717#[inline]
718fn quantize_line_pos(line_pos: u16) -> u8 {
719    match line_pos {
720        0..=3 => 0,   // start of line (indentation)
721        4..=15 => 1,  // early in line
722        16..=63 => 2, // mid-line
723        _ => 3,       // late in line
724    }
725}
726
727#[cfg(test)]
728mod tests {
729    use super::*;
730
731    #[test]
732    fn initial_prediction_is_balanced() {
733        let mut engine = CMEngine::new();
734        let p = engine.predict();
735        // Should be near 2048 (all models start balanced).
736        assert!(
737            (1800..=2200).contains(&p),
738            "initial prediction should be near 2048, got {p}"
739        );
740    }
741
742    #[test]
743    fn prediction_always_in_range() {
744        let mut engine = CMEngine::new();
745        let data = b"Hello, World! This is a test of the CM engine.";
746        for &byte in data {
747            for bpos in 0..8 {
748                let p = engine.predict();
749                assert!(
750                    (1..=4095).contains(&p),
751                    "prediction out of range at bpos {bpos}: {p}"
752                );
753                let bit = (byte >> (7 - bpos)) & 1;
754                engine.update(bit);
755            }
756        }
757    }
758
759    #[test]
760    fn context_state_tracks_correctly() {
761        let mut engine = CMEngine::new();
762        // Feed byte 0x42 (01000010)
763        let byte: u8 = 0x42;
764        for bpos in 0..8 {
765            let _p = engine.predict();
766            let bit = (byte >> (7 - bpos)) & 1;
767            engine.update(bit);
768        }
769        // After byte complete, c1 should be 0x42, c0 should be 1.
770        assert_eq!(engine.c1, 0x42);
771        assert_eq!(engine.c0, 1);
772        assert_eq!(engine.bpos, 0);
773    }
774
775    #[test]
776    fn repeated_byte_adapts() {
777        let mut engine = CMEngine::new();
778        let byte: u8 = b'A';
779        let mut total_bits: f64 = 0.0;
780        let mut first_byte_bits: f64 = 0.0;
781
782        for iteration in 0..50 {
783            let mut byte_bits: f64 = 0.0;
784            for bpos in 0..8 {
785                let p = engine.predict();
786                let bit = (byte >> (7 - bpos)) & 1;
787                let prob_of_bit = if bit == 1 {
788                    p as f64 / 4096.0
789                } else {
790                    1.0 - p as f64 / 4096.0
791                };
792                byte_bits += -prob_of_bit.max(0.001).log2();
793                engine.update(bit);
794            }
795            if iteration == 0 {
796                first_byte_bits = byte_bits;
797            }
798            total_bits += byte_bits;
799        }
800
801        let avg = total_bits / 50.0;
802        assert!(
803            avg < first_byte_bits,
804            "engine should improve: first={first_byte_bits:.2}, avg={avg:.2}"
805        );
806    }
807
808    #[test]
809    fn hash_functions_differ() {
810        let h1 = order1_hash(65, 1);
811        let h2 = order2_hash(0, 65, 1);
812        let h3 = order3_hash(0, 0, 65, 1);
813        // All should produce different hashes.
814        assert_ne!(h1, h2);
815        assert_ne!(h2, h3);
816    }
817
818    #[test]
819    fn engine_deterministic() {
820        // Two engines with same input must produce same predictions.
821        let data = b"determinism test";
822        let mut e1 = CMEngine::new();
823        let mut e2 = CMEngine::new();
824
825        for &byte in data.iter() {
826            for bpos in 0..8 {
827                let p1 = e1.predict();
828                let p2 = e2.predict();
829                assert_eq!(p1, p2, "engines diverged at bpos {bpos}");
830                let bit = (byte >> (7 - bpos)) & 1;
831                e1.update(bit);
832                e2.update(bit);
833            }
834        }
835    }
836}