1use super::compression::{NcdVariant, try_ncd_bytes_backend};
4use super::generation::{GenerationRng, pick_generated_byte, try_generate_rate_backend_chain};
5use super::metrics::{
6 empirical_entropy_bytes, try_biased_entropy_rate_backend, try_cross_entropy_rate_backend,
7 try_entropy_rate_backend, try_joint_entropy_rate_backend, try_mutual_information_rate_backend,
8 try_ned_rate_backend, try_nte_rate_backend,
9};
10use super::types::{CompressionBackend, GenerationConfig, GenerationUpdateMode, RateBackend};
11use crate::aligned_prefix;
12use crate::error::{InfotheoryError, InfotheoryResult};
13use crate::mixture::{OnlineBytePredictor, RateBackendPredictorCheckpoint};
14use crate::prediction::{
15 BinaryPrediction, BitOrder, BitStreamSemantics, BytePrefixMass,
16 binary_prediction_from_log_probs,
17};
18use crate::spec::{CanonicalBytes, CompiledCompressionBackend, CompiledRateBackend};
19
20pub fn get_default_ctx() -> InfotheoryResult<InfotheoryCtx> {
22 crate::get_default_ctx()
23}
24
25pub fn set_default_ctx(ctx: InfotheoryCtx) {
27 crate::set_default_ctx(ctx);
28}
29
30#[derive(Clone)]
32pub struct InfotheoryCtx {
33 pub rate_backend: CompiledRateBackend,
35 pub compression_backend: CompiledCompressionBackend,
37}
38
39pub struct RateBackendSession {
41 predictor: crate::mixture::RateBackendPredictor,
42}
43
44#[derive(Clone)]
61pub struct RateBackendBitSession {
62 backend_code: CanonicalBytes,
63 predictor: crate::mixture::RateBackendPredictor,
64 semantics: BitStreamSemantics,
65 min_prob: f64,
66 prefix: Option<BufferedBytePrefix>,
67 discardable_scopes: usize,
68}
69
70#[derive(Clone)]
83pub struct RateBackendBitSessionCheckpoint {
84 backend_code: CanonicalBytes,
85 predictor: crate::mixture::RateBackendPredictorCheckpoint,
86 semantics: BitStreamSemantics,
87 prefix: Option<BufferedBytePrefix>,
88}
89
90#[derive(Clone, Copy, Debug, Eq, PartialEq)]
91enum BufferedByteUpdateMode {
92 Adaptive,
93 Frozen,
94}
95
96impl BufferedByteUpdateMode {
97 fn verb(self) -> &'static str {
98 match self {
99 Self::Adaptive => "adaptive",
100 Self::Frozen => "conditioning-only",
101 }
102 }
103}
104
105#[derive(Clone)]
106struct BufferedBytePrefix {
107 kind: BufferedBytePrefixKind,
108 update_mode: Option<BufferedByteUpdateMode>,
109}
110
111#[derive(Clone)]
112#[allow(clippy::large_enum_variant)]
116enum BufferedBytePrefixKind {
117 Mass(BytePrefixMass),
118 NativeMsb {
119 start_checkpoint: Option<Box<RateBackendPredictorCheckpoint>>,
124 symbol: u8,
125 bits: usize,
126 },
127}
128
129impl BufferedBytePrefix {
130 fn new_mass(mass: BytePrefixMass) -> Self {
131 Self {
132 kind: BufferedBytePrefixKind::Mass(mass),
133 update_mode: None,
134 }
135 }
136
137 fn new_native_msb(start_checkpoint: Option<RateBackendPredictorCheckpoint>) -> Self {
138 Self {
139 kind: BufferedBytePrefixKind::NativeMsb {
140 start_checkpoint: start_checkpoint.map(Box::new),
141 symbol: 0,
142 bits: 0,
143 },
144 update_mode: None,
145 }
146 }
147
148 fn has_partial_bits(&self) -> bool {
149 match &self.kind {
150 BufferedBytePrefixKind::Mass(mass) => mass.has_partial_bits(),
151 BufferedBytePrefixKind::NativeMsb { bits, .. } => *bits > 0,
152 }
153 }
154
155 fn record_mode(&mut self, requested: BufferedByteUpdateMode) -> InfotheoryResult<()> {
156 if let Some(active) = self.update_mode
157 && self.has_partial_bits()
158 && active != requested
159 {
160 return Err(InfotheoryError::runtime(format!(
161 "byte-packed bit sessions cannot mix {} and {} updates within the same buffered byte; finish the byte with one mode, use `try_observe_bit`/`try_condition_bit` to handle this error explicitly, or switch to BitStreamSemantics::BinaryTokens for mid-byte mode changes",
162 active.verb(),
163 requested.verb(),
164 )));
165 }
166 self.update_mode = Some(requested);
167 Ok(())
168 }
169}
170
171fn byte_packed_total_symbols(total_bits: Option<u64>) -> Result<Option<u64>, String> {
172 let Some(total_bits) = total_bits else {
173 return Ok(None);
174 };
175 if total_bits % 8 != 0 {
176 return Err(format!(
177 "byte-packed bit streams require a whole number of bytes; got {total_bits} bits. Use BitStreamSemantics::BinaryTokens for arbitrary-length bit streams"
178 ));
179 }
180 Ok(Some(total_bits / 8))
181}
182
183fn total_symbols_for_bit_semantics(
184 total_bits: Option<u64>,
185 semantics: BitStreamSemantics,
186) -> Result<Option<u64>, String> {
187 match semantics {
188 BitStreamSemantics::BytePacked { .. } => byte_packed_total_symbols(total_bits),
189 BitStreamSemantics::BinaryTokens => Ok(total_bits),
190 }
191}
192
193impl RateBackendBitSession {
194 fn observed_native_prefix_bit(symbol: u8, bit_idx: usize) -> bool {
195 (symbol & (1u8 << (7 - bit_idx))) != 0
196 }
197
198 fn restore_start_checkpoint_abort_and_discard(
202 &mut self,
203 start: RateBackendPredictorCheckpoint,
204 ) -> Result<bool, String> {
205 self.predictor.restore_checkpoint(&start);
206 let abort_res = self.predictor.abort_empty_native_msb_byte_prefix();
207 self.predictor.discard_checkpoint(start);
208 abort_res
209 }
210
211 fn release_inflight_prefix_checkpoint_for_restore(&mut self) {
212 let Some(prefix) = self.prefix.take() else {
213 return;
214 };
215 if let BufferedBytePrefixKind::NativeMsb {
216 start_checkpoint: Some(checkpoint),
217 ..
218 } = prefix.kind
219 {
220 self.predictor.discard_checkpoint(*checkpoint);
221 }
222 }
223
224 fn discard_inflight_prefix_checkpoint(&mut self) {
225 let Some(prefix) = self.prefix.take() else {
226 return;
227 };
228 match prefix.kind {
229 BufferedBytePrefixKind::Mass(_) => {}
230 BufferedBytePrefixKind::NativeMsb {
231 start_checkpoint: Some(checkpoint),
232 ..
233 } => {
234 self.restore_start_checkpoint_abort_and_discard(*checkpoint)
235 .expect("native MSB prefix start checkpoint must be empty");
236 }
237 BufferedBytePrefixKind::NativeMsb {
238 start_checkpoint: None,
239 bits,
240 ..
241 } => {
242 if bits == 0 {
243 self.predictor
244 .abort_empty_native_msb_byte_prefix()
245 .expect("empty native MSB prefix abort must succeed");
246 } else {
247 panic!(
248 "discarding an adaptive native MSB byte-prefix with observed bits; \
249 missing prefix start checkpoint"
250 );
251 }
252 }
253 }
254 }
255
256 fn abandon_inflight_prefix_for_lifecycle_reset(&mut self) {
257 let Some(prefix) = self.prefix.take() else {
258 return;
259 };
260 match prefix.kind {
261 BufferedBytePrefixKind::Mass(_) => {}
262 BufferedBytePrefixKind::NativeMsb {
263 start_checkpoint: Some(checkpoint),
264 ..
265 } => {
266 self.restore_start_checkpoint_abort_and_discard(*checkpoint)
267 .expect("native MSB prefix start checkpoint must be empty");
268 }
269 BufferedBytePrefixKind::NativeMsb {
270 start_checkpoint: None,
271 bits: 0,
272 ..
273 } => {
274 self.predictor
275 .abort_empty_native_msb_byte_prefix()
276 .expect("empty native MSB prefix abort must succeed");
277 }
278 BufferedBytePrefixKind::NativeMsb {
279 start_checkpoint: None,
280 bits: _,
281 ..
282 } => {
283 self.predictor
284 .abandon_incomplete_native_msb_byte_prefix_for_lifecycle()
285 .expect("native MSB prefix lifecycle abandonment must succeed");
286 }
287 }
288 }
289
290 fn normalize_prefix_for_checkpoint_clear(&mut self) -> InfotheoryResult<bool> {
295 let Some(prefix) = self.prefix.take() else {
296 return Ok(true);
297 };
298 let update_mode = prefix.update_mode;
299 match prefix.kind {
300 BufferedBytePrefixKind::Mass(mass) => {
301 self.prefix = Some(BufferedBytePrefix {
302 kind: BufferedBytePrefixKind::Mass(mass),
303 update_mode,
304 });
305 Ok(true)
306 }
307 BufferedBytePrefixKind::NativeMsb {
308 start_checkpoint: None,
309 bits: 0,
310 ..
311 } => {
312 self.predictor
313 .abort_empty_native_msb_byte_prefix()
314 .map_err(InfotheoryError::runtime)?;
315 Ok(true)
316 }
317 BufferedBytePrefixKind::NativeMsb {
318 start_checkpoint: Some(checkpoint),
319 symbol,
320 bits,
321 } => {
322 let checkpoint = *checkpoint;
323 self.restore_start_checkpoint_abort_and_discard(checkpoint)
324 .map_err(InfotheoryError::runtime)?;
325
326 if bits == 0 {
327 return Ok(true);
328 }
329
330 let mut logps = [0.0f64; 256];
331 self.predictor.fill_log_probs(&mut logps);
332
333 let mut mass = BytePrefixMass::from_log_probs(&logps, BitOrder::MsbFirst);
334 for bit_idx in 0..bits {
335 mass.observe(Self::observed_native_prefix_bit(symbol, bit_idx));
336 }
337 self.prefix = Some(BufferedBytePrefix {
338 kind: BufferedBytePrefixKind::Mass(mass),
339 update_mode,
340 });
341 Ok(true)
342 }
343 BufferedBytePrefixKind::NativeMsb {
344 start_checkpoint: None,
345 symbol,
346 bits,
347 } => {
348 self.prefix = Some(BufferedBytePrefix {
349 kind: BufferedBytePrefixKind::NativeMsb {
350 start_checkpoint: None,
351 symbol,
352 bits,
353 },
354 update_mode,
355 });
356 Ok(false)
357 }
358 }
359 }
360
361 pub fn from_backend(
366 backend: CompiledRateBackend,
367 total_bits: Option<u64>,
368 semantics: BitStreamSemantics,
369 ) -> InfotheoryResult<Self> {
370 Self::from_backend_with_min_prob(
371 backend,
372 total_bits,
373 semantics,
374 crate::mixture::DEFAULT_MIN_PROB,
375 )
376 }
377
378 pub(crate) fn from_backend_with_min_prob(
379 backend: CompiledRateBackend,
380 total_bits: Option<u64>,
381 semantics: BitStreamSemantics,
382 min_prob: f64,
383 ) -> InfotheoryResult<Self> {
384 let total_symbols = total_symbols_for_bit_semantics(total_bits, semantics)
385 .map_err(InfotheoryError::runtime)?;
386 let mut predictor = match semantics {
387 BitStreamSemantics::BinaryTokens => {
388 crate::runtime::build_rate_backend_binary_token_predictor(&backend, min_prob)
389 }
390 BitStreamSemantics::BytePacked { .. } => {
391 if !backend.supports_byte_prefix_mass() {
392 return Err(InfotheoryError::invalid_backend_config(format!(
393 "backend '{}' does not support BitStreamSemantics::BytePacked",
394 backend.canonical_name()
395 )));
396 }
397 if !backend.supports_efficient_byte_packed_bit_sessions() {
398 return Err(InfotheoryError::invalid_backend_config(format!(
399 "backend '{}' can expose byte probabilities but does not support efficient BitStreamSemantics::BytePacked sessions; use BitStreamSemantics::BinaryTokens or a backend with native or cached byte-prefix support",
400 backend.canonical_name()
401 )));
402 }
403 crate::runtime::build_rate_backend_predictor(&backend, min_prob)
404 }
405 }
406 .map_err(InfotheoryError::invalid_backend_config)?;
407 predictor
408 .begin_stream(total_symbols)
409 .map_err(InfotheoryError::runtime)?;
410 let backend_code = backend.canonical_bytes().clone();
411 Ok(Self {
412 backend_code,
413 predictor,
414 semantics,
415 min_prob,
416 prefix: None,
417 discardable_scopes: 0,
418 })
419 }
420
421 pub fn from_spec(
426 backend: RateBackend,
427 total_bits: Option<u64>,
428 semantics: BitStreamSemantics,
429 ) -> InfotheoryResult<Self> {
430 let compiled = backend
431 .compile()
432 .map_err(|err| InfotheoryError::invalid_backend_config(err.to_string()))?;
433 Self::from_backend(compiled, total_bits, semantics)
434 }
435
436 pub fn predict_bit(&mut self) -> BinaryPrediction {
438 match self.semantics {
439 BitStreamSemantics::BinaryTokens => binary_prediction_from_log_probs(
440 self.predictor.log_prob(0),
441 self.predictor.log_prob(1),
442 self.min_prob,
443 ),
444 BitStreamSemantics::BytePacked { order } => {
445 if self.prefix.is_none()
446 && order == BitOrder::MsbFirst
447 && self.predictor.has_native_msb_byte_prefix()
448 {
449 self.try_begin_native_msb_prefix(None).unwrap_or_else(|err| {
450 panic!(
451 "RateBackendPredictor failed to begin native MSB byte-prefix: {err} \
452 (contract violation; BytePrefixMass fallback is not safe on Err)"
453 )
454 });
455 }
456 self.ensure_mass_prefix(order);
457 let native_bits = match &self.prefix.as_ref().expect("prefix initialized").kind {
458 BufferedBytePrefixKind::Mass(mass) => return mass.prediction(),
459 BufferedBytePrefixKind::NativeMsb { bits, .. } => *bits,
460 };
461 let p1 = self
462 .predictor
463 .native_msb_prefix_prob_one(native_bits)
464 .expect(
465 "native_msb_prefix_prob_one failed or returned error for active NativeMsb \
466 prefix (RateBackendBitSession invariant; predictors must uphold finite \
467 contract or report via Result consistently)",
468 );
469 BinaryPrediction::from_prob_one(p1, self.min_prob)
470 }
471 }
472 }
473
474 pub fn predict_one(&mut self) -> f64 {
476 self.predict_bit().p1
477 }
478
479 pub fn checkpoint(&mut self) -> RateBackendBitSessionCheckpoint {
486 debug_assert_eq!(
487 self.discardable_scopes, 0,
488 "RateBackendBitSession checkpoints are invalid inside discardable simulation scopes"
489 );
490 RateBackendBitSessionCheckpoint {
491 backend_code: self.backend_code.clone(),
492 predictor: self.predictor.checkpoint(),
493 semantics: self.semantics,
494 prefix: self.prefix.clone(),
495 }
496 }
497
498 #[cfg(any(feature = "aixi", test))]
499 pub(crate) fn begin_discardable_scope(&mut self) {
500 self.discardable_scopes = self.discardable_scopes.saturating_add(1);
501 }
502
503 #[cfg(feature = "aixi")]
504 pub(crate) fn clear_discardable_scopes(&mut self) {
505 self.discardable_scopes = 0;
506 }
507
508 pub fn restore_checkpoint(
518 &mut self,
519 checkpoint: &RateBackendBitSessionCheckpoint,
520 ) -> InfotheoryResult<()> {
521 if self.backend_code != checkpoint.backend_code || self.semantics != checkpoint.semantics {
522 return Err(InfotheoryError::runtime(
523 "RateBackendBitSession checkpoint belongs to a different backend or bit semantics",
524 ));
525 }
526 self.release_inflight_prefix_checkpoint_for_restore();
527 self.predictor.restore_checkpoint(&checkpoint.predictor);
528 let mut restored_prefix = checkpoint.prefix.clone();
529 if let Some(BufferedBytePrefix {
530 kind:
531 BufferedBytePrefixKind::NativeMsb {
532 start_checkpoint: Some(start_checkpoint),
533 symbol,
534 bits,
535 },
536 ..
537 }) = checkpoint.prefix.as_ref()
538 {
539 let restored_state = self.predictor.checkpoint();
540 self.predictor.restore_checkpoint(start_checkpoint.as_ref());
541 let abort_res = self.predictor.abort_empty_native_msb_byte_prefix();
542 if let Err(err) = abort_res {
543 self.predictor.restore_checkpoint(&restored_state);
544 self.predictor.discard_checkpoint(restored_state);
545 return Err(InfotheoryError::runtime(err));
546 }
547 let fresh_start = self.predictor.native_prefix_start_checkpoint();
548 match self.predictor.begin_native_msb_byte_prefix() {
549 Ok(true) => {}
550 Ok(false) => {
551 self.predictor.discard_checkpoint(fresh_start);
552 self.predictor.restore_checkpoint(&restored_state);
553 self.predictor.discard_checkpoint(restored_state);
554 return Err(InfotheoryError::runtime(
555 "stored checkpoint requires native MSB-first byte-prefix support during restore",
556 ));
557 }
558 Err(err) => {
559 self.predictor.discard_checkpoint(fresh_start);
560 self.predictor.restore_checkpoint(&restored_state);
561 self.predictor.discard_checkpoint(restored_state);
562 return Err(InfotheoryError::runtime(err));
563 }
564 }
565 for bit_idx in 0..*bits {
566 let bit = Self::observed_native_prefix_bit(*symbol, bit_idx);
567 if let Err(err) = self.predictor.observe_native_msb_prefix_bit(bit_idx, bit) {
568 self.predictor.restore_checkpoint(&restored_state);
569 self.predictor.discard_checkpoint(fresh_start);
570 self.predictor.discard_checkpoint(restored_state);
571 return Err(InfotheoryError::runtime(err));
572 }
573 }
574 self.predictor.discard_checkpoint(restored_state);
575 if let Some(prefix) = restored_prefix.as_mut()
576 && let BufferedBytePrefixKind::NativeMsb {
577 start_checkpoint, ..
578 } = &mut prefix.kind
579 {
580 *start_checkpoint = Some(Box::new(fresh_start));
581 }
582 self.prefix = restored_prefix;
583 return Ok(());
584 }
585 self.prefix = restored_prefix;
586 Ok(())
587 }
588
589 pub fn clear_checkpoints_if_supported(&mut self) {
596 match self.normalize_prefix_for_checkpoint_clear() {
597 Ok(true) => self.predictor.clear_checkpoints_if_supported(),
598 Ok(false) => {}
599 Err(err) => {
600 panic!("failed to normalize native byte-prefix before checkpoint clear: {err}")
601 }
602 }
603 }
604
605 pub fn try_step_bit(&mut self, bit: bool) -> InfotheoryResult<BinaryPrediction> {
607 let prediction = self.predict_bit();
608 self.try_observe_bit(bit)?;
609 Ok(prediction)
610 }
611
612 pub fn step_bit(&mut self, bit: bool) -> BinaryPrediction {
614 self.try_step_bit(bit)
615 .unwrap_or_else(|err| panic!("step_bit rejected an invalid bit-session update: {err}"))
616 }
617
618 pub fn try_observe_bit(&mut self, bit: bool) -> InfotheoryResult<()> {
620 match self.semantics {
621 BitStreamSemantics::BinaryTokens => {
622 self.predictor.update(u8::from(bit));
623 Ok(())
624 }
625 BitStreamSemantics::BytePacked { order } => {
626 self.update_byte_packed_bit(bit, order, BufferedByteUpdateMode::Adaptive)
627 }
628 }
629 }
630
631 pub fn observe_bit(&mut self, bit: bool) {
633 self.try_observe_bit(bit).unwrap_or_else(|err| {
634 panic!("observe_bit rejected an invalid bit-session update: {err}")
635 });
636 }
637
638 pub fn try_condition_bit(&mut self, bit: bool) -> InfotheoryResult<()> {
640 match self.semantics {
641 BitStreamSemantics::BinaryTokens => {
642 self.predictor.update_frozen(u8::from(bit));
643 Ok(())
644 }
645 BitStreamSemantics::BytePacked { order } => {
646 self.update_byte_packed_bit(bit, order, BufferedByteUpdateMode::Frozen)
647 }
648 }
649 }
650
651 pub fn condition_bit(&mut self, bit: bool) {
653 self.try_condition_bit(bit).unwrap_or_else(|err| {
654 panic!("condition_bit rejected an invalid bit-session update: {err}")
655 });
656 }
657
658 pub fn reset_frozen(&mut self, total_bits: Option<u64>) -> InfotheoryResult<()> {
660 let total_symbols = total_symbols_for_bit_semantics(total_bits, self.semantics)
661 .map_err(InfotheoryError::runtime)?;
662 self.abandon_inflight_prefix_for_lifecycle_reset();
663 self.predictor
664 .reset_frozen(total_symbols)
665 .map_err(InfotheoryError::runtime)
666 }
667
668 pub fn finish(&mut self) -> InfotheoryResult<()> {
670 if matches!(self.semantics, BitStreamSemantics::BytePacked { .. })
671 && self
672 .prefix
673 .as_ref()
674 .is_some_and(BufferedBytePrefix::has_partial_bits)
675 {
676 return Err(InfotheoryError::runtime(
677 "byte-packed bit streams must finish on a whole-byte boundary; use BitStreamSemantics::BinaryTokens for arbitrary-length bit streams",
678 ));
679 }
680 self.discard_inflight_prefix_checkpoint();
681 self.predictor
682 .finish_stream()
683 .map_err(InfotheoryError::runtime)
684 }
685
686 fn ensure_prefix_for_update(
687 &mut self,
688 order: BitOrder,
689 update_mode: BufferedByteUpdateMode,
690 ) -> InfotheoryResult<()> {
691 if self.prefix.is_some() {
692 return Ok(());
693 }
694 if order == BitOrder::MsbFirst && self.predictor.has_native_msb_byte_prefix() {
695 let checkpoint = if update_mode == BufferedByteUpdateMode::Frozen {
696 Some(self.predictor.native_prefix_start_checkpoint())
697 } else {
698 None
699 };
700 match self.try_begin_native_msb_prefix(checkpoint) {
701 Ok(true) => return Ok(()),
702 Ok(false) => {}
703 Err(err) => return Err(InfotheoryError::runtime(err)),
704 }
705 }
706 self.ensure_mass_prefix(order);
707 Ok(())
708 }
709
710 fn try_begin_native_msb_prefix(
716 &mut self,
717 start_checkpoint: Option<RateBackendPredictorCheckpoint>,
718 ) -> Result<bool, String> {
719 match self.predictor.begin_native_msb_byte_prefix() {
720 Ok(true) => {
721 self.prefix = Some(BufferedBytePrefix::new_native_msb(start_checkpoint));
722 Ok(true)
723 }
724 Ok(false) => {
725 if let Some(checkpoint) = start_checkpoint {
726 self.predictor.discard_checkpoint(checkpoint);
727 }
728 Ok(false)
729 }
730 Err(err) => {
731 if let Some(checkpoint) = start_checkpoint {
732 self.predictor.discard_checkpoint(checkpoint);
733 }
734 Err(err)
735 }
736 }
737 }
738
739 fn ensure_mass_prefix(&mut self, order: BitOrder) {
740 if self.prefix.is_some() {
741 return;
742 }
743 let mut logps = [0.0f64; 256];
744 self.predictor.fill_log_probs(&mut logps);
745 self.prefix = Some(BufferedBytePrefix::new_mass(
746 BytePrefixMass::from_log_probs(&logps, order),
747 ));
748 }
749
750 fn update_byte_packed_bit(
751 &mut self,
752 bit: bool,
753 order: BitOrder,
754 update_mode: BufferedByteUpdateMode,
755 ) -> InfotheoryResult<()> {
756 self.ensure_prefix_for_update(order, update_mode)?;
757 let needs_prefix_checkpoint =
758 update_mode != BufferedByteUpdateMode::Adaptive || self.discardable_scopes == 0;
759 let prefix = self.prefix.as_mut().expect("prefix initialized");
760 prefix.record_mode(update_mode)?;
761 match &mut prefix.kind {
762 BufferedBytePrefixKind::Mass(mass) => {
763 mass.observe(bit);
764 if mass.is_complete() {
765 let symbol = mass.symbol();
766 match update_mode {
767 BufferedByteUpdateMode::Adaptive => self.predictor.update(symbol),
768 BufferedByteUpdateMode::Frozen => self.predictor.update_frozen(symbol),
769 }
770 self.prefix = None;
771 }
772 }
773 BufferedBytePrefixKind::NativeMsb {
774 start_checkpoint,
775 symbol,
776 bits,
777 } => {
778 if start_checkpoint.is_none() && *bits == 0 && needs_prefix_checkpoint {
779 *start_checkpoint =
780 Some(Box::new(self.predictor.native_prefix_start_checkpoint()));
781 }
782 match update_mode {
783 BufferedByteUpdateMode::Adaptive => self
784 .predictor
785 .observe_native_msb_prefix_bit(*bits, bit)
786 .map_err(InfotheoryError::runtime)?,
787 BufferedByteUpdateMode::Frozen => self
788 .predictor
789 .condition_native_msb_prefix_bit_for_rollback(*bits, bit)
790 .map_err(InfotheoryError::runtime)?,
791 }
792 if bit {
793 *symbol |= 1u8 << (7 - *bits);
794 }
795 *bits += 1;
796 if *bits == 8 {
797 let completed_symbol = *symbol;
798 if update_mode == BufferedByteUpdateMode::Frozen {
799 let checkpoint_box = start_checkpoint.take().ok_or_else(|| {
800 InfotheoryError::runtime(
801 "native byte-prefix frozen update is missing its start checkpoint",
802 )
803 })?;
804 let checkpoint = *checkpoint_box;
805 self.restore_start_checkpoint_abort_and_discard(checkpoint)
806 .map_err(InfotheoryError::runtime)?;
807 self.predictor.update_frozen(completed_symbol);
808 } else {
809 self.predictor
810 .finish_native_msb_byte_prefix(completed_symbol)
811 .map_err(InfotheoryError::runtime)?;
812 if let Some(checkpoint) = start_checkpoint.take() {
813 self.predictor.discard_checkpoint(*checkpoint);
814 }
815 }
816 self.prefix = None;
817 }
818 }
819 }
820 Ok(())
821 }
822}
823
824impl crate::prediction::OnlineBitPredictor for RateBackendBitSession {
825 fn begin_bit_stream(
826 &mut self,
827 total_bits: Option<u64>,
828 semantics: BitStreamSemantics,
829 ) -> Result<(), String> {
830 if semantics != self.semantics {
831 return Err(
832 "bit stream semantics are fixed for a RateBackendBitSession; create a new session"
833 .to_string(),
834 );
835 }
836 let total_symbols = total_symbols_for_bit_semantics(total_bits, self.semantics)?;
837 self.abandon_inflight_prefix_for_lifecycle_reset();
838 self.predictor.begin_fresh_stream(total_symbols)
839 }
840
841 fn finish_bit_stream(&mut self) -> Result<(), String> {
842 self.finish().map_err(|err| err.to_string())
843 }
844
845 fn bit_prediction(&mut self) -> BinaryPrediction {
846 self.predict_bit()
847 }
848
849 fn update_bit(&mut self, bit: bool) {
850 self.observe_bit(bit);
851 }
852
853 fn update_bit_frozen(&mut self, bit: bool) {
854 self.condition_bit(bit);
855 }
856}
857
858impl RateBackendSession {
859 pub fn from_backend(
864 backend: CompiledRateBackend,
865 total_symbols: Option<u64>,
866 ) -> InfotheoryResult<Self> {
867 let mut predictor = crate::runtime::build_rate_backend_predictor_default(&backend)
868 .map_err(InfotheoryError::invalid_backend_config)?;
869 predictor
870 .begin_stream(total_symbols)
871 .map_err(InfotheoryError::runtime)?;
872 Ok(Self { predictor })
873 }
874
875 pub fn from_spec(backend: RateBackend, total_symbols: Option<u64>) -> InfotheoryResult<Self> {
877 let compiled = backend
878 .compile()
879 .map_err(|err| InfotheoryError::invalid_backend_config(err.to_string()))?;
880 Self::from_backend(compiled, total_symbols)
881 }
882
883 pub fn observe(&mut self, data: &[u8]) {
885 for &byte in data {
886 self.predictor.update(byte);
887 }
888 }
889
890 pub fn condition(&mut self, data: &[u8]) {
892 for &byte in data {
893 self.predictor.update_frozen(byte);
894 }
895 }
896
897 pub fn reset_frozen(&mut self, total_symbols: Option<u64>) -> InfotheoryResult<()> {
899 self.predictor
900 .reset_frozen(total_symbols)
901 .map_err(InfotheoryError::runtime)
902 }
903
904 pub fn begin_stream(&mut self, total_symbols: Option<u64>) -> InfotheoryResult<()> {
910 self.predictor
911 .begin_fresh_stream(total_symbols)
912 .map_err(InfotheoryError::runtime)
913 }
914
915 pub fn fill_log_probs(&mut self, out: &mut [f64; 256]) {
917 self.predictor.fill_log_probs(out);
918 }
919
920 pub fn generate_bytes(&mut self, bytes: usize, config: GenerationConfig) -> Vec<u8> {
922 if bytes == 0 {
923 return Vec::new();
924 }
925
926 let mut out = Vec::with_capacity(bytes);
927 let mut logps = [0.0f64; 256];
928 let mut rng = GenerationRng::new(config.seed);
929
930 for _ in 0..bytes {
931 match &mut self.predictor {
932 #[cfg(feature = "backend-rosa")]
933 crate::mixture::RateBackendPredictor::Rosa { .. } => {
934 for (sym, slot) in logps.iter_mut().enumerate() {
935 *slot = self.predictor.log_prob(sym as u8);
936 }
937 }
938 _ => self.predictor.fill_log_probs(&mut logps),
939 }
940 let byte = pick_generated_byte(&logps, config, &mut rng);
941 match config.update_mode {
942 GenerationUpdateMode::Adaptive => self.predictor.update(byte),
943 GenerationUpdateMode::Frozen => self.predictor.update_frozen(byte),
944 }
945 out.push(byte);
946 }
947
948 out
949 }
950
951 pub fn finish(&mut self) -> InfotheoryResult<()> {
953 self.predictor
954 .finish_stream()
955 .map_err(InfotheoryError::runtime)
956 }
957}
958
959impl InfotheoryCtx {
960 pub fn try_default() -> InfotheoryResult<Self> {
962 Self::from_specs(
963 RateBackend::try_default()?,
964 CompressionBackend::try_default()?,
965 )
966 }
967
968 pub fn new(
970 rate_backend: CompiledRateBackend,
971 compression_backend: CompiledCompressionBackend,
972 ) -> Self {
973 Self {
974 rate_backend,
975 compression_backend,
976 }
977 }
978
979 pub fn from_specs(
981 rate_backend: RateBackend,
982 compression_backend: CompressionBackend,
983 ) -> InfotheoryResult<Self> {
984 Ok(Self {
985 rate_backend: rate_backend
986 .compile()
987 .map_err(|err| InfotheoryError::invalid_backend_config(err.to_string()))?,
988 compression_backend: compression_backend
989 .compile()
990 .map_err(|err| InfotheoryError::invalid_backend_config(err.to_string()))?,
991 })
992 }
993
994 pub fn try_with_zpaq(method: impl Into<crate::api::ZpaqMethodSpec>) -> InfotheoryResult<Self> {
996 Self::from_specs(
997 RateBackend::RosaPlus { max_order: -1 },
998 CompressionBackend::zpaq(method),
999 )
1000 }
1001
1002 pub fn try_compress_size(&self, data: &[u8]) -> InfotheoryResult<u64> {
1004 crate::api::compression::try_compress_size_backend(data, &self.compression_backend)
1005 }
1006
1007 pub fn try_compress_size_chain(&self, parts: &[&[u8]]) -> InfotheoryResult<u64> {
1009 crate::api::compression::try_compress_size_chain_backend(parts, &self.compression_backend)
1010 }
1011
1012 pub fn rate_backend_session(
1014 &self,
1015 total_symbols: Option<u64>,
1016 ) -> InfotheoryResult<RateBackendSession> {
1017 RateBackendSession::from_backend(self.rate_backend.clone(), total_symbols)
1018 }
1019
1020 pub fn rate_backend_bit_session(
1022 &self,
1023 total_bits: Option<u64>,
1024 semantics: BitStreamSemantics,
1025 ) -> InfotheoryResult<RateBackendBitSession> {
1026 RateBackendBitSession::from_backend(self.rate_backend.clone(), total_bits, semantics)
1027 }
1028
1029 pub fn try_entropy_rate_bytes(&self, data: &[u8]) -> InfotheoryResult<f64> {
1031 try_entropy_rate_backend(data, &self.rate_backend)
1032 }
1033
1034 pub fn try_biased_entropy_rate_bytes(&self, data: &[u8]) -> InfotheoryResult<f64> {
1036 try_biased_entropy_rate_backend(data, &self.rate_backend)
1037 }
1038
1039 pub fn try_cross_entropy_rate_bytes(
1041 &self,
1042 test_data: &[u8],
1043 train_data: &[u8],
1044 ) -> InfotheoryResult<f64> {
1045 try_cross_entropy_rate_backend(test_data, train_data, &self.rate_backend)
1046 }
1047
1048 pub fn try_cross_entropy_bytes(
1050 &self,
1051 test_data: &[u8],
1052 train_data: &[u8],
1053 ) -> InfotheoryResult<f64> {
1054 self.try_cross_entropy_rate_bytes(test_data, train_data)
1055 }
1056
1057 pub fn try_joint_entropy_rate_bytes(&self, x: &[u8], y: &[u8]) -> InfotheoryResult<f64> {
1059 let (x, y) = aligned_prefix(x, y);
1060 if x.is_empty() {
1061 return Ok(0.0);
1062 }
1063 try_joint_entropy_rate_backend(x, y, &self.rate_backend)
1064 }
1065
1066 pub fn try_conditional_entropy_rate_bytes(&self, x: &[u8], y: &[u8]) -> InfotheoryResult<f64> {
1068 let (x, y) = aligned_prefix(x, y);
1069 if x.is_empty() {
1070 return Ok(0.0);
1071 }
1072 let h_xy = self.try_joint_entropy_rate_bytes(x, y)?;
1073 let h_y = self.try_entropy_rate_bytes(y)?;
1074 Ok((h_xy - h_y).max(0.0))
1075 }
1076
1077 pub fn try_cross_entropy_conditional_chain(
1080 &self,
1081 prefix_parts: &[&[u8]],
1082 data: &[u8],
1083 ) -> InfotheoryResult<f64> {
1084 crate::runtime::try_cross_entropy_conditional_chain_backend(
1085 prefix_parts,
1086 data,
1087 &self.rate_backend,
1088 )
1089 }
1090
1091 pub fn try_generate_bytes(&self, prompt: &[u8], bytes: usize) -> InfotheoryResult<Vec<u8>> {
1093 self.try_generate_bytes_with_config(prompt, bytes, GenerationConfig::default())
1094 }
1095
1096 pub fn try_generate_bytes_with_config(
1098 &self,
1099 prompt: &[u8],
1100 bytes: usize,
1101 config: GenerationConfig,
1102 ) -> InfotheoryResult<Vec<u8>> {
1103 try_generate_rate_backend_chain(&[prompt], bytes, &self.rate_backend, config)
1104 }
1105
1106 pub fn try_generate_bytes_conditional_chain(
1108 &self,
1109 prefix_parts: &[&[u8]],
1110 bytes: usize,
1111 ) -> InfotheoryResult<Vec<u8>> {
1112 self.try_generate_bytes_conditional_chain_with_config(
1113 prefix_parts,
1114 bytes,
1115 GenerationConfig::default(),
1116 )
1117 }
1118
1119 pub fn try_generate_bytes_conditional_chain_with_config(
1121 &self,
1122 prefix_parts: &[&[u8]],
1123 bytes: usize,
1124 config: GenerationConfig,
1125 ) -> InfotheoryResult<Vec<u8>> {
1126 try_generate_rate_backend_chain(prefix_parts, bytes, &self.rate_backend, config)
1127 }
1128
1129 pub fn try_ncd_bytes(&self, x: &[u8], y: &[u8], variant: NcdVariant) -> InfotheoryResult<f64> {
1131 try_ncd_bytes_backend(x, y, &self.compression_backend, variant)
1132 }
1133
1134 pub fn try_mutual_information_rate_bytes(&self, x: &[u8], y: &[u8]) -> InfotheoryResult<f64> {
1136 try_mutual_information_rate_backend(x, y, &self.rate_backend)
1137 }
1138
1139 pub fn try_mutual_information_bytes(&self, x: &[u8], y: &[u8]) -> InfotheoryResult<f64> {
1141 self.try_mutual_information_rate_bytes(x, y)
1142 }
1143
1144 pub fn try_conditional_entropy_bytes(&self, x: &[u8], y: &[u8]) -> InfotheoryResult<f64> {
1146 let (x, y) = aligned_prefix(x, y);
1147 let h_xy = self.try_joint_entropy_rate_bytes(x, y)?;
1148 let h_y = self.try_entropy_rate_bytes(y)?;
1149 Ok((h_xy - h_y).max(0.0))
1150 }
1151
1152 pub fn try_ned_bytes(&self, x: &[u8], y: &[u8]) -> InfotheoryResult<f64> {
1154 try_ned_rate_backend(x, y, &self.rate_backend)
1155 }
1156
1157 pub fn try_ned_cons_bytes(&self, x: &[u8], y: &[u8]) -> InfotheoryResult<f64> {
1159 let (x, y) = aligned_prefix(x, y);
1160 let h_x = self.try_entropy_rate_bytes(x)?;
1161 let h_y = self.try_entropy_rate_bytes(y)?;
1162 let h_xy = self.try_joint_entropy_rate_bytes(x, y)?;
1163 let min_h = h_x.min(h_y);
1164 if h_xy == 0.0 {
1165 Ok(0.0)
1166 } else {
1167 Ok(((h_xy - min_h) / h_xy).clamp(0.0, 1.0))
1168 }
1169 }
1170
1171 pub fn try_nte_bytes(&self, x: &[u8], y: &[u8]) -> InfotheoryResult<f64> {
1173 try_nte_rate_backend(x, y, &self.rate_backend)
1174 }
1175
1176 pub fn try_intrinsic_dependence_bytes(&self, data: &[u8]) -> InfotheoryResult<f64> {
1180 let h_empirical = empirical_entropy_bytes(data);
1181 if h_empirical < 1e-9 {
1182 return Ok(0.0);
1183 }
1184 let h_rate = self.try_entropy_rate_bytes(data)?;
1185 Ok(((h_empirical - h_rate) / h_empirical).clamp(0.0, 1.0))
1186 }
1187
1188 pub fn try_resistance_to_transformation_bytes(
1190 &self,
1191 x: &[u8],
1192 tx: &[u8],
1193 ) -> InfotheoryResult<f64> {
1194 let (x, tx) = aligned_prefix(x, tx);
1195 let h_x = self.try_entropy_rate_bytes(x)?;
1196 if h_x < 1e-9 {
1197 return Ok(0.0);
1198 }
1199 let mi = self.try_mutual_information_bytes(x, tx)?;
1200 Ok((mi / h_x).clamp(0.0, 1.0))
1201 }
1202}
1203
1204#[cfg(test)]
1205mod tests {
1206 use super::*;
1207 #[allow(unused_imports)]
1211 use crate::prediction::OnlineBitPredictor;
1212
1213 #[cfg(feature = "backend-ctw")]
1214 fn ctw_checkpoint_depth(session: &RateBackendBitSession) -> usize {
1215 match &session.predictor {
1216 crate::mixture::RateBackendPredictor::Ctw {
1217 checkpoint_depth, ..
1218 } => *checkpoint_depth,
1219 crate::mixture::RateBackendPredictor::FacCtw {
1220 checkpoint_depth, ..
1221 } => *checkpoint_depth,
1222 _ => panic!("expected ctw predictor"),
1223 }
1224 }
1225
1226 #[cfg(feature = "backend-ctw")]
1227 fn ctw_native_prefix_progress(session: &RateBackendBitSession) -> Option<usize> {
1228 match &session.predictor {
1229 crate::mixture::RateBackendPredictor::Ctw {
1230 native_prefix_progress,
1231 ..
1232 }
1233 | crate::mixture::RateBackendPredictor::FacCtw {
1234 native_prefix_progress,
1235 ..
1236 } => *native_prefix_progress,
1237 _ => panic!("expected ctw predictor"),
1238 }
1239 }
1240
1241 #[cfg(feature = "backend-ctw")]
1242 fn buffered_native_prefix_bits(session: &RateBackendBitSession) -> Option<usize> {
1243 match session.prefix.as_ref().map(|prefix| &prefix.kind) {
1244 Some(BufferedBytePrefixKind::NativeMsb { bits, .. }) => Some(*bits),
1245 _ => None,
1246 }
1247 }
1248
1249 #[cfg(feature = "backend-ctw")]
1250 fn buffered_native_prefix_has_start_checkpoint(
1251 session: &RateBackendBitSession,
1252 ) -> Option<bool> {
1253 match session.prefix.as_ref().map(|prefix| &prefix.kind) {
1254 Some(BufferedBytePrefixKind::NativeMsb {
1255 start_checkpoint, ..
1256 }) => Some(start_checkpoint.is_some()),
1257 _ => None,
1258 }
1259 }
1260
1261 #[cfg(all(feature = "backend-calibrated", feature = "backend-ctw"))]
1262 fn buffered_native_prefix_has_calibrated_start_checkpoint(
1263 session: &RateBackendBitSession,
1264 ) -> Option<bool> {
1265 match session.prefix.as_ref().map(|prefix| &prefix.kind) {
1266 Some(BufferedBytePrefixKind::NativeMsb {
1267 start_checkpoint: Some(checkpoint),
1268 ..
1269 }) => Some(matches!(
1270 checkpoint.as_ref(),
1271 crate::mixture::RateBackendPredictorCheckpoint::CalibratedNativePrefixStart { .. }
1272 )),
1273 Some(BufferedBytePrefixKind::NativeMsb {
1274 start_checkpoint: None,
1275 ..
1276 }) => Some(false),
1277 _ => None,
1278 }
1279 }
1280
1281 #[cfg(feature = "backend-ctw")]
1282 fn new_ctw_byte_packed_session() -> RateBackendBitSession {
1283 RateBackendBitSession::from_spec(
1284 RateBackend::Ctw { depth: 4 },
1285 Some(8),
1286 BitStreamSemantics::BytePacked {
1287 order: BitOrder::MsbFirst,
1288 },
1289 )
1290 .expect("ctw byte-packed session")
1291 }
1292
1293 #[cfg(feature = "backend-ctw")]
1294 fn new_fac_ctw_byte_packed_session() -> RateBackendBitSession {
1295 RateBackendBitSession::from_spec(
1296 RateBackend::FacCtw {
1297 base_depth: 4,
1298 num_percept_bits: 8,
1299 encoding_bits: 8,
1300 msb_first: None,
1301 },
1302 Some(8),
1303 BitStreamSemantics::BytePacked {
1304 order: BitOrder::MsbFirst,
1305 },
1306 )
1307 .expect("fac-ctw byte-packed session")
1308 }
1309
1310 #[cfg(all(feature = "backend-calibrated", feature = "backend-ctw"))]
1311 fn new_calibrated_ctw_byte_packed_session() -> RateBackendBitSession {
1312 RateBackendBitSession::from_spec(
1313 RateBackend::Calibrated {
1314 spec: std::sync::Arc::new(crate::api::CalibratedSpec::new(
1315 RateBackend::Ctw { depth: 4 },
1316 crate::api::CalibrationContextKind::TextRepeat,
1317 )),
1318 },
1319 Some(8),
1320 BitStreamSemantics::BytePacked {
1321 order: BitOrder::MsbFirst,
1322 },
1323 )
1324 .expect("calibrated ctw byte-packed session")
1325 }
1326
1327 #[cfg(feature = "backend-ctw")]
1328 #[test]
1329 fn frozen_native_byte_completion_releases_start_checkpoint() {
1330 for mut session in [
1331 new_ctw_byte_packed_session(),
1332 new_fac_ctw_byte_packed_session(),
1333 ] {
1334 for bit in [true, false, true, false, false, true, true, false] {
1335 session
1336 .try_condition_bit(bit)
1337 .expect("condition full frozen native byte");
1338 }
1339 assert!(session.prefix.is_none());
1340 assert_eq!(ctw_checkpoint_depth(&session), 0);
1341 assert_eq!(ctw_native_prefix_progress(&session), None);
1342
1343 let pred = session.predict_bit();
1344 assert!(pred.p0.is_finite() && pred.p1.is_finite());
1345 assert_eq!(buffered_native_prefix_bits(&session), Some(0));
1346 assert_eq!(ctw_native_prefix_progress(&session), Some(0));
1347 }
1348 }
1349
1350 #[cfg(feature = "backend-ctw")]
1351 #[test]
1352 fn reset_frozen_discards_inflight_native_prefix_checkpoint() {
1353 let mut session = new_ctw_byte_packed_session();
1354 session.try_condition_bit(true).expect("conditioning bit");
1355 assert_eq!(ctw_checkpoint_depth(&session), 1);
1356
1357 session.reset_frozen(Some(8)).expect("reset frozen");
1358 assert_eq!(ctw_checkpoint_depth(&session), 0);
1359 }
1360
1361 #[cfg(feature = "backend-ctw")]
1362 #[test]
1363 fn begin_bit_stream_discards_inflight_native_prefix_checkpoint() {
1364 let mut session = new_ctw_byte_packed_session();
1365 session.try_condition_bit(true).expect("conditioning bit");
1366 assert_eq!(ctw_checkpoint_depth(&session), 1);
1367
1368 session
1369 .begin_bit_stream(
1370 Some(8),
1371 BitStreamSemantics::BytePacked {
1372 order: BitOrder::MsbFirst,
1373 },
1374 )
1375 .expect("begin bit stream");
1376 assert_eq!(ctw_checkpoint_depth(&session), 0);
1377 }
1378
1379 #[cfg(feature = "backend-ctw")]
1380 #[test]
1381 fn clear_checkpoints_after_empty_native_restore_drops_prefix_cleanly() {
1382 for mut session in [
1383 new_ctw_byte_packed_session(),
1384 new_fac_ctw_byte_packed_session(),
1385 ] {
1386 let _ = session.predict_bit();
1387 let checkpoint = session.checkpoint();
1388 session
1389 .try_condition_bit(true)
1390 .expect("conditioning bit after checkpoint");
1391
1392 session
1393 .restore_checkpoint(&checkpoint)
1394 .expect("restore empty native prefix checkpoint");
1395 assert_eq!(buffered_native_prefix_bits(&session), Some(0));
1396 assert_eq!(ctw_native_prefix_progress(&session), Some(0));
1397
1398 session.clear_checkpoints_if_supported();
1399 assert!(session.prefix.is_none());
1400 assert_eq!(ctw_checkpoint_depth(&session), 0);
1401 assert_eq!(ctw_native_prefix_progress(&session), None);
1402
1403 let pred = session.predict_bit();
1404 assert!(pred.p0.is_finite() && pred.p1.is_finite());
1405 assert_eq!(buffered_native_prefix_bits(&session), Some(0));
1406 assert_eq!(ctw_native_prefix_progress(&session), Some(0));
1407 }
1408 }
1409
1410 #[cfg(feature = "backend-ctw")]
1411 #[test]
1412 fn clear_checkpoints_converts_partial_native_prefix_to_mass() {
1413 for mut session in [
1414 new_ctw_byte_packed_session(),
1415 new_fac_ctw_byte_packed_session(),
1416 ] {
1417 session.try_condition_bit(true).expect("first prefix bit");
1418 session.try_condition_bit(false).expect("second prefix bit");
1419 assert_eq!(buffered_native_prefix_bits(&session), Some(2));
1420 assert_eq!(ctw_native_prefix_progress(&session), Some(2));
1421
1422 let before_clear = session.predict_bit();
1423 session.clear_checkpoints_if_supported();
1424
1425 assert!(matches!(
1426 session.prefix.as_ref().map(|prefix| &prefix.kind),
1427 Some(BufferedBytePrefixKind::Mass(_))
1428 ));
1429 assert_eq!(ctw_checkpoint_depth(&session), 0);
1430 assert_eq!(ctw_native_prefix_progress(&session), None);
1431
1432 let after_clear = session.predict_bit();
1433 assert!(
1434 (before_clear.p1 - after_clear.p1).abs() <= 1e-12,
1435 "native-to-mass conversion changed prefix prediction: before={} after={}",
1436 before_clear.p1,
1437 after_clear.p1
1438 );
1439
1440 for bit in [true, false, true, false, true, false] {
1441 session
1442 .try_condition_bit(bit)
1443 .expect("finish converted mass prefix");
1444 }
1445 session
1446 .finish()
1447 .expect("finish whole byte after conversion");
1448 }
1449 }
1450
1451 #[cfg(feature = "backend-ctw")]
1452 #[test]
1453 fn reset_frozen_rolls_back_adaptive_partial_native_prefix() {
1454 for mut session in [
1455 new_ctw_byte_packed_session(),
1456 new_fac_ctw_byte_packed_session(),
1457 ] {
1458 session.try_observe_bit(true).expect("adaptive prefix bit");
1459 assert_eq!(ctw_checkpoint_depth(&session), 1);
1460 assert_eq!(ctw_native_prefix_progress(&session), Some(1));
1461
1462 session.reset_frozen(Some(8)).expect("reset frozen");
1463
1464 assert_eq!(ctw_checkpoint_depth(&session), 0);
1465 assert_eq!(ctw_native_prefix_progress(&session), None);
1466 let mut fresh = match &session.predictor {
1467 crate::mixture::RateBackendPredictor::Ctw { .. } => new_ctw_byte_packed_session(),
1468 crate::mixture::RateBackendPredictor::FacCtw { .. } => {
1469 new_fac_ctw_byte_packed_session()
1470 }
1471 _ => unreachable!("test only constructs CTW-family sessions"),
1472 };
1473 let reset_prediction = session.predict_bit();
1474 let fresh_prediction = fresh.predict_bit();
1475 assert!(
1476 (reset_prediction.p1 - fresh_prediction.p1).abs() <= 1e-12,
1477 "partial adaptive native prefix leaked into reset state: reset={} fresh={}",
1478 reset_prediction.p1,
1479 fresh_prediction.p1
1480 );
1481 }
1482 }
1483
1484 #[cfg(feature = "backend-ctw")]
1485 #[test]
1486 fn discardable_scope_avoids_adaptive_native_prefix_checkpoint() {
1487 for mut session in [
1488 new_ctw_byte_packed_session(),
1489 new_fac_ctw_byte_packed_session(),
1490 ] {
1491 session.begin_discardable_scope();
1492 let _ = session.predict_bit();
1493 session
1494 .try_observe_bit(true)
1495 .expect("discardable adaptive prefix bit");
1496
1497 assert_eq!(buffered_native_prefix_bits(&session), Some(1));
1498 assert_eq!(
1499 buffered_native_prefix_has_start_checkpoint(&session),
1500 Some(false),
1501 "discardable adaptive prefixes must not retain restore-only checkpoints",
1502 );
1503 assert_eq!(ctw_checkpoint_depth(&session), 0);
1504
1505 for bit in [false, true, false, true, false, true, false] {
1506 session
1507 .try_observe_bit(bit)
1508 .expect("finish discardable adaptive byte");
1509 }
1510 assert!(session.prefix.is_none());
1511 assert_eq!(ctw_checkpoint_depth(&session), 0);
1512 assert_eq!(ctw_native_prefix_progress(&session), None);
1513 }
1514 }
1515
1516 #[cfg(feature = "backend-ctw")]
1517 #[test]
1518 fn discardable_scope_keeps_frozen_native_prefix_checkpoint() {
1519 let mut session = new_ctw_byte_packed_session();
1520 session.begin_discardable_scope();
1521 session
1522 .try_condition_bit(true)
1523 .expect("discardable frozen prefix bit");
1524
1525 assert_eq!(buffered_native_prefix_bits(&session), Some(1));
1526 assert_eq!(
1527 buffered_native_prefix_has_start_checkpoint(&session),
1528 Some(true),
1529 "frozen native prefixes still need a start checkpoint to avoid learning action bytes",
1530 );
1531 assert_eq!(ctw_checkpoint_depth(&session), 1);
1532 }
1533
1534 #[cfg(all(feature = "backend-calibrated", feature = "backend-ctw"))]
1535 #[test]
1536 fn calibrated_native_prefix_supports_update_only_bits() {
1537 let bits = [true, false, true, false, false, true, true, false];
1538
1539 let mut adaptive = new_calibrated_ctw_byte_packed_session();
1540 for bit in bits {
1541 adaptive
1542 .try_observe_bit(bit)
1543 .expect("adaptive calibrated update-only bit");
1544 }
1545 assert!(adaptive.prefix.is_none());
1546 adaptive.finish().expect("finish adaptive calibrated byte");
1547
1548 let mut frozen = new_calibrated_ctw_byte_packed_session();
1549 for bit in bits {
1550 frozen
1551 .try_condition_bit(bit)
1552 .expect("frozen calibrated update-only bit");
1553 }
1554 assert!(frozen.prefix.is_none());
1555 frozen.finish().expect("finish frozen calibrated byte");
1556 }
1557
1558 #[cfg(all(feature = "backend-calibrated", feature = "backend-ctw"))]
1559 #[test]
1560 fn calibrated_adaptive_prefix_uses_lightweight_start_checkpoint() {
1561 let mut session = new_calibrated_ctw_byte_packed_session();
1562 session
1563 .try_observe_bit(true)
1564 .expect("adaptive calibrated prefix bit");
1565 assert_eq!(buffered_native_prefix_bits(&session), Some(1));
1566 assert_eq!(
1567 buffered_native_prefix_has_calibrated_start_checkpoint(&session),
1568 Some(true),
1569 "calibrated adaptive prefixes must store only the wrapped predictor checkpoint"
1570 );
1571
1572 session.reset_frozen(Some(8)).expect("reset frozen");
1573 let mut fresh = new_calibrated_ctw_byte_packed_session();
1574 let reset_prediction = session.predict_bit();
1575 let fresh_prediction = fresh.predict_bit();
1576 assert!(
1577 (reset_prediction.p1 - fresh_prediction.p1).abs() <= 1e-12,
1578 "abandoned calibrated prefix leaked into reset state: reset={} fresh={}",
1579 reset_prediction.p1,
1580 fresh_prediction.p1
1581 );
1582 }
1583
1584 #[cfg(feature = "backend-ctw")]
1585 #[test]
1586 fn begin_bit_stream_rolls_back_adaptive_partial_native_prefix() {
1587 let mut session = new_fac_ctw_byte_packed_session();
1588 session.try_observe_bit(true).expect("adaptive prefix bit");
1589 assert_eq!(ctw_checkpoint_depth(&session), 1);
1590 assert_eq!(ctw_native_prefix_progress(&session), Some(1));
1591
1592 session
1593 .begin_bit_stream(
1594 Some(8),
1595 BitStreamSemantics::BytePacked {
1596 order: BitOrder::MsbFirst,
1597 },
1598 )
1599 .expect("begin bit stream");
1600
1601 assert_eq!(ctw_checkpoint_depth(&session), 0);
1602 assert_eq!(ctw_native_prefix_progress(&session), None);
1603 session
1604 .try_observe_bit(false)
1605 .expect("native prefix re-entry after adaptive abort");
1606 }
1607
1608 #[cfg(feature = "backend-ctw")]
1609 #[test]
1610 fn restore_checkpoint_discards_abandoned_native_prefix_checkpoint() {
1611 let mut session = new_ctw_byte_packed_session();
1612 let checkpoint = session.checkpoint();
1613 assert_eq!(ctw_checkpoint_depth(&session), 1);
1614
1615 session.try_condition_bit(true).expect("conditioning bit");
1616 assert_eq!(ctw_checkpoint_depth(&session), 2);
1617
1618 session
1619 .restore_checkpoint(&checkpoint)
1620 .expect("restore checkpoint");
1621 assert_eq!(ctw_checkpoint_depth(&session), 1);
1622 }
1623
1624 #[cfg(feature = "backend-ctw")]
1625 #[test]
1626 fn restore_mid_prefix_checkpoint_rebuilds_frozen_native_rollback_point() {
1627 let mut session = new_ctw_byte_packed_session();
1628 session.try_condition_bit(true).expect("conditioning bit");
1629 let checkpoint = session.checkpoint();
1630 assert_eq!(ctw_checkpoint_depth(&session), 2);
1631
1632 session
1633 .try_condition_bit(false)
1634 .expect("conditioning second bit");
1635 session
1636 .restore_checkpoint(&checkpoint)
1637 .expect("restore mid-prefix checkpoint");
1638 assert_eq!(ctw_checkpoint_depth(&session), 2);
1639
1640 for bit in [false, true, false, true, false, true, false] {
1641 session
1642 .try_condition_bit(bit)
1643 .expect("finish restored byte");
1644 }
1645 assert_eq!(ctw_checkpoint_depth(&session), 1);
1646 }
1647
1648 #[cfg(feature = "backend-mixture")]
1652 #[test]
1653 fn predict_bit_mass_fallback_when_native_begin_returns_false() {
1654 use crate::mixture::{
1655 BayesMixture, DEFAULT_MIN_PROB, ExpertConfig, MixtureRuntime, RateBackendPredictor,
1656 };
1657
1658 #[derive(Clone)]
1659 struct MockNativeNoCheckpoint;
1660
1661 impl OnlineBytePredictor for MockNativeNoCheckpoint {
1662 fn log_prob(&mut self, _symbol: u8) -> f64 {
1663 -(256.0f64).ln()
1664 }
1665
1666 fn update(&mut self, _symbol: u8) {}
1667
1668 fn has_native_msb_byte_prefix(&self) -> bool {
1669 true
1670 }
1671 }
1672
1673 let configs = [ExpertConfig::uniform("native", || {
1674 Box::new(MockNativeNoCheckpoint) as Box<dyn OnlineBytePredictor>
1675 })];
1676 let runtime = MixtureRuntime::Bayes(BayesMixture::new(&configs));
1677 let mut predictor = RateBackendPredictor::Mixture { runtime };
1678 predictor.begin_stream(Some(1)).expect("begin_stream");
1679
1680 let mut session = RateBackendBitSession {
1681 backend_code: CanonicalBytes::from(b"test-native-fallback".to_vec()),
1682 predictor,
1683 semantics: BitStreamSemantics::BytePacked {
1684 order: BitOrder::MsbFirst,
1685 },
1686 min_prob: DEFAULT_MIN_PROB,
1687 prefix: None,
1688 discardable_scopes: 0,
1689 };
1690 let pred = session.predict_bit();
1691 assert!(
1692 (pred.p0 + pred.p1 - 1.0).abs() < 1e-9,
1693 "mass fallback prediction must normalize: p0={} p1={}",
1694 pred.p0,
1695 pred.p1
1696 );
1697 assert!(pred.p1.is_finite());
1698 assert!(matches!(
1699 session.prefix.as_ref().map(|p| &p.kind),
1700 Some(BufferedBytePrefixKind::Mass(_))
1701 ));
1702 }
1703
1704 #[test]
1707 fn record_buffered_byte_prefix_kind_and_related_sizes() {
1708 let byte_prefix_mass = std::mem::size_of::<BytePrefixMass>();
1709 let buffered_kind = std::mem::size_of::<BufferedBytePrefixKind>();
1710 let rate_ckpt = std::mem::size_of::<RateBackendPredictorCheckpoint>();
1711 let opt_rate_ckpt = std::mem::size_of::<Option<RateBackendPredictorCheckpoint>>();
1712 eprintln!(
1713 "BufferedBytePrefixKind sizes: \
1714 BufferedBytePrefixKind={}B (Mass~{}B inline; NativeMsb Option<RateCheckpoint>~{}B; \
1715 RateBackendPredictorCheckpoint enum={}B). start_checkpoint is boxed; Full variant \
1716 is Box<RateBackendPredictor> so compact checkpoint variants stay small.",
1717 buffered_kind, byte_prefix_mass, opt_rate_ckpt, rate_ckpt
1718 );
1719 assert_eq!(
1723 buffered_kind, byte_prefix_mass,
1724 "post-box BufferedBytePrefixKind (NativeMsb) must not exceed Mass variant size ({} vs {}); \
1725 native-MSB rewind checkpoints must stay out-of-line",
1726 buffered_kind, byte_prefix_mass
1727 );
1728 }
1729}