Skip to main content

infotheory/api/
metrics.rs

1//! Information-theoretic metric and scoring API surface.
2//!
3//! Functions in this module split cleanly into two families:
4//!
5//! - **Algorithmic**: take a [`CompiledRateBackend`] (or use the default context
6//!   backend) and produce entropy-rate / cross-entropy / mutual-information /
7//!   normalized-distance estimates driven by the rate model. Algorithm-specific
8//!   parameters (such as ROSA's `max_order`) live inside the backend's variant.
9//! - **Empirical** (`empirical_*`): order-0 / IID Shannon plug-in estimators.
10//!   They treat the input as IID symbols and estimate entropy from observed
11//!   symbol frequencies. They never take an order parameter; if higher-order
12//!   structure matters, use a context-aware [`RateBackend`] (e.g. CTW).
13
14use crate::error::{InfotheoryError, InfotheoryResult};
15use crate::spec::CompiledRateBackend;
16
17use crate::{aligned_prefix, with_default_ctx};
18
19#[inline(always)]
20/// Fallible entropy-rate estimate `Ĥ(X)` (bits per symbol) using the default context backend.
21pub fn try_entropy_rate_bytes(data: &[u8]) -> InfotheoryResult<f64> {
22    with_default_ctx(|ctx| ctx.try_entropy_rate_bytes(data))
23}
24
25#[inline(always)]
26/// Fallible biased/plugin entropy-rate estimate using the default context backend.
27pub fn try_biased_entropy_rate_bytes(data: &[u8]) -> InfotheoryResult<f64> {
28    with_default_ctx(|ctx| ctx.try_biased_entropy_rate_bytes(data))
29}
30
31/// Mutual information rate estimate under an explicit `backend`.
32///
33/// Inputs are aligned to the shared prefix length.
34pub fn try_mutual_information_rate_backend(
35    x: &[u8],
36    y: &[u8],
37    backend: &CompiledRateBackend,
38) -> InfotheoryResult<f64> {
39    let (x, y) = aligned_prefix(x, y);
40    if x.is_empty() {
41        return Ok(0.0);
42    }
43    let h_x = try_entropy_rate_backend(x, backend)?;
44    let h_y = try_entropy_rate_backend(y, backend)?;
45    let h_xy = try_joint_entropy_rate_backend(x, y, backend)?;
46    Ok((h_x + h_y - h_xy).max(0.0))
47}
48
49/// Normalized entropy distance under an explicit `backend`.
50///
51/// Returns a value in `[0, 1]` after clamping.
52pub fn try_ned_rate_backend(
53    x: &[u8],
54    y: &[u8],
55    backend: &CompiledRateBackend,
56) -> InfotheoryResult<f64> {
57    let (x, y) = aligned_prefix(x, y);
58    if x.is_empty() {
59        return Ok(0.0);
60    }
61    let h_x = try_entropy_rate_backend(x, backend)?;
62    let h_y = try_entropy_rate_backend(y, backend)?;
63    let h_xy = try_joint_entropy_rate_backend(x, y, backend)?;
64    let min_h = h_x.min(h_y);
65    let max_h = h_x.max(h_y);
66    if max_h == 0.0 {
67        Ok(0.0)
68    } else {
69        Ok(((h_xy - min_h) / max_h).clamp(0.0, 1.0))
70    }
71}
72
73/// Normalized transform effort (variation-of-information form) under an explicit `backend`.
74///
75/// Returns a value in `[0, 2]` after clamping.
76pub fn try_nte_rate_backend(
77    x: &[u8],
78    y: &[u8],
79    backend: &CompiledRateBackend,
80) -> InfotheoryResult<f64> {
81    let (x, y) = aligned_prefix(x, y);
82    if x.is_empty() {
83        return Ok(0.0);
84    }
85    let h_x = try_entropy_rate_backend(x, backend)?;
86    let h_y = try_entropy_rate_backend(y, backend)?;
87    let h_xy = try_joint_entropy_rate_backend(x, y, backend)?;
88    let max_h = h_x.max(h_y);
89    if max_h == 0.0 {
90        Ok(0.0)
91    } else {
92        let vi = (h_xy - h_x).max(0.0) + (h_xy - h_y).max(0.0);
93        Ok((vi / max_h).clamp(0.0, 2.0))
94    }
95}
96
97/// Fallible entropy-rate estimate of `data` using the explicit rate `backend`.
98pub fn try_entropy_rate_backend(
99    data: &[u8],
100    backend: &CompiledRateBackend,
101) -> InfotheoryResult<f64> {
102    crate::runtime::try_entropy_rate_backend_direct(data, backend)
103}
104
105/// Fallible biased/plugin entropy rate of `data` using the explicit rate `backend`.
106pub fn try_biased_entropy_rate_backend(
107    data: &[u8],
108    backend: &CompiledRateBackend,
109) -> InfotheoryResult<f64> {
110    if !backend.capabilities().supports_biased_entropy {
111        Err(InfotheoryError::unsupported(
112            "biased/plugin entropy is not supported for zpaq rate backends",
113        ))
114    } else {
115        crate::try_frozen_plugin_rate_backend(data, &[data], backend)
116    }
117}
118
119/// Fallible cross-entropy `H_{train}(test)` — score `test_data` under a model trained on `train_data`.
120pub fn try_cross_entropy_rate_backend(
121    test_data: &[u8],
122    train_data: &[u8],
123    backend: &CompiledRateBackend,
124) -> InfotheoryResult<f64> {
125    crate::runtime::try_cross_entropy_rate_backend_direct(test_data, train_data, backend)
126}
127
128/// Fallible joint entropy rate `H(X,Y)` using an explicit `backend`.
129pub fn try_joint_entropy_rate_backend(
130    x: &[u8],
131    y: &[u8],
132    backend: &CompiledRateBackend,
133) -> InfotheoryResult<f64> {
134    crate::runtime::try_joint_entropy_rate_backend_direct(x, y, backend)
135}
136
137/// Empirical (zero-order, IID) Shannon entropy `H₀(X)` in bits/symbol.
138///
139/// Treats the input as a sequence of IID byte symbols and returns the plug-in
140/// Shannon entropy of the observed byte frequencies. Use a context-aware
141/// [`crate::api::RateBackend`] via [`try_entropy_rate_bytes`] for higher-order estimation.
142#[inline(always)]
143pub fn empirical_entropy_bytes(data: &[u8]) -> f64 {
144    if data.is_empty() {
145        return 0.0;
146    }
147
148    let mut counts = [0u64; 256];
149    for &b in data {
150        counts[b as usize] += 1;
151    }
152
153    let n = data.len() as f64;
154    let mut h = 0.0f64;
155    for &count in &counts {
156        if count > 0 {
157            let p = count as f64 / n;
158            h -= p * p.log2();
159        }
160    }
161    h
162}
163
164/// Empirical (zero-order, IID) joint Shannon entropy `H₀(X,Y)` over aligned prefixes.
165///
166/// Treats aligned `(x[i], y[i])` pairs as IID samples from a joint distribution
167/// over 65536 outcomes and returns the plug-in Shannon entropy.
168#[inline(always)]
169pub fn empirical_joint_entropy_bytes(x: &[u8], y: &[u8]) -> f64 {
170    let (x, y) = aligned_prefix(x, y);
171    let n = x.len();
172    if n == 0 {
173        return 0.0;
174    }
175
176    let mut counts = vec![0u64; 256 * 256];
177    for i in 0..n {
178        let pair_idx = (x[i] as usize) * 256 + (y[i] as usize);
179        counts[pair_idx] += 1;
180    }
181
182    let n_f64 = n as f64;
183    let mut h = 0.0f64;
184    for &c in &counts {
185        if c > 0 {
186            let p = c as f64 / n_f64;
187            h -= p * p.log2();
188        }
189    }
190    h
191}
192
193#[inline(always)]
194/// Fallible joint entropy-rate estimate `H(X,Y)` with the default context backend.
195pub fn try_joint_entropy_rate_bytes(x: &[u8], y: &[u8]) -> InfotheoryResult<f64> {
196    with_default_ctx(|ctx| ctx.try_joint_entropy_rate_bytes(x, y))
197}
198
199#[inline(always)]
200/// Fallible conditional entropy-rate estimate `H(X|Y)` with the default context backend.
201pub fn try_conditional_entropy_rate_bytes(x: &[u8], y: &[u8]) -> InfotheoryResult<f64> {
202    with_default_ctx(|ctx| ctx.try_conditional_entropy_rate_bytes(x, y))
203}
204
205#[inline(always)]
206/// Fallible conditional entropy estimate using the default context rate backend.
207pub fn try_conditional_entropy_bytes(x: &[u8], y: &[u8]) -> InfotheoryResult<f64> {
208    with_default_ctx(|ctx| ctx.try_conditional_entropy_bytes(x, y))
209}
210
211#[inline(always)]
212/// Fallible mutual-information estimate `I(X;Y)` using the default context rate backend.
213pub fn try_mutual_information_bytes(x: &[u8], y: &[u8]) -> InfotheoryResult<f64> {
214    with_default_ctx(|ctx| ctx.try_mutual_information_bytes(x, y))
215}
216
217/// Empirical (zero-order, IID) mutual information `I₀(X;Y)` from byte histograms.
218pub fn empirical_mutual_information_bytes(x: &[u8], y: &[u8]) -> f64 {
219    let (x, y) = aligned_prefix(x, y);
220    let h_x = empirical_entropy_bytes(x);
221    let h_y = empirical_entropy_bytes(y);
222    let h_xy = empirical_joint_entropy_bytes(x, y);
223    (h_x + h_y - h_xy).max(0.0)
224}
225
226#[inline(always)]
227/// Fallible mutual-information rate estimate with the default context backend.
228pub fn try_mutual_information_rate_bytes(x: &[u8], y: &[u8]) -> InfotheoryResult<f64> {
229    with_default_ctx(|ctx| ctx.try_mutual_information_rate_bytes(x, y))
230}
231
232#[inline(always)]
233/// Fallible normalized entropy distance (NED) estimate with the default context backend.
234pub fn try_ned_bytes(x: &[u8], y: &[u8]) -> InfotheoryResult<f64> {
235    with_default_ctx(|ctx| ctx.try_ned_bytes(x, y))
236}
237
238/// Empirical (zero-order, IID) normalized entropy distance:
239/// `(H₀(X,Y) - min(H₀(X), H₀(Y))) / max(H₀(X), H₀(Y))`.
240pub fn empirical_ned_bytes(x: &[u8], y: &[u8]) -> f64 {
241    let (x, y) = aligned_prefix(x, y);
242    let h_x = empirical_entropy_bytes(x);
243    let h_y = empirical_entropy_bytes(y);
244    let h_xy = empirical_joint_entropy_bytes(x, y);
245    let min_h = h_x.min(h_y);
246    let max_h = h_x.max(h_y);
247    if max_h == 0.0 {
248        0.0
249    } else {
250        ((h_xy - min_h) / max_h).clamp(0.0, 1.0)
251    }
252}
253
254#[inline(always)]
255/// Fallible entropy-rate NED estimate with the default context backend.
256pub fn try_ned_rate_bytes(x: &[u8], y: &[u8]) -> InfotheoryResult<f64> {
257    with_default_ctx(|ctx| ctx.try_ned_bytes(x, y))
258}
259
260#[inline(always)]
261/// Fallible constructive NED estimate with the default context backend.
262pub fn try_ned_cons_bytes(x: &[u8], y: &[u8]) -> InfotheoryResult<f64> {
263    with_default_ctx(|ctx| ctx.try_ned_cons_bytes(x, y))
264}
265
266/// Empirical (zero-order, IID) constructive normalized entropy distance:
267/// `(H₀(X,Y) - min(H₀(X), H₀(Y))) / H₀(X,Y)`.
268pub fn empirical_ned_cons_bytes(x: &[u8], y: &[u8]) -> f64 {
269    let (x, y) = aligned_prefix(x, y);
270    let h_x = empirical_entropy_bytes(x);
271    let h_y = empirical_entropy_bytes(y);
272    let h_xy = empirical_joint_entropy_bytes(x, y);
273    let min_h = h_x.min(h_y);
274    if h_xy == 0.0 {
275        0.0
276    } else {
277        ((h_xy - min_h) / h_xy).clamp(0.0, 1.0)
278    }
279}
280
281#[inline(always)]
282/// Fallible entropy-rate constructive NED estimate with the default context backend.
283pub fn try_ned_cons_rate_bytes(x: &[u8], y: &[u8]) -> InfotheoryResult<f64> {
284    with_default_ctx(|ctx| ctx.try_ned_cons_bytes(x, y))
285}
286
287#[inline(always)]
288/// Fallible normalized transform-effort (NTE/VI-based) estimate with the default context backend.
289pub fn try_nte_bytes(x: &[u8], y: &[u8]) -> InfotheoryResult<f64> {
290    with_default_ctx(|ctx| ctx.try_nte_bytes(x, y))
291}
292
293/// Empirical (zero-order, IID) NTE estimate using variation of information
294/// normalized by `max(H₀(X), H₀(Y))`.
295pub fn empirical_nte_bytes(x: &[u8], y: &[u8]) -> f64 {
296    let (x, y) = aligned_prefix(x, y);
297    let h_x = empirical_entropy_bytes(x);
298    let h_y = empirical_entropy_bytes(y);
299    let h_xy = empirical_joint_entropy_bytes(x, y);
300    let vi = 2.0 * h_xy - h_x - h_y;
301    let max_h = h_x.max(h_y);
302    if max_h == 0.0 {
303        0.0
304    } else {
305        (vi / max_h).clamp(0.0, 2.0)
306    }
307}
308
309#[inline(always)]
310/// Fallible entropy-rate NTE estimate with the default context backend.
311pub fn try_nte_rate_bytes(x: &[u8], y: &[u8]) -> InfotheoryResult<f64> {
312    with_default_ctx(|ctx| ctx.try_nte_bytes(x, y))
313}
314
315#[inline(always)]
316pub(crate) fn byte_histogram(data: &[u8]) -> [f64; 256] {
317    let mut counts = [0u64; 256];
318    for &b in data {
319        counts[b as usize] += 1;
320    }
321    let n = data.len() as f64;
322    let mut probs = [0.0f64; 256];
323    if n == 0.0 {
324        return probs;
325    }
326    for i in 0..256 {
327        probs[i] = counts[i] as f64 / n;
328    }
329    probs
330}
331
332#[inline(always)]
333/// Total variation distance between the byte distributions of `x` and `y`.
334///
335/// TVD is an empirical, order-0 quantity by construction.
336pub fn tvd_bytes(x: &[u8], y: &[u8]) -> f64 {
337    if x.is_empty() || y.is_empty() {
338        return 0.0;
339    }
340    let p_x = byte_histogram(x);
341    let p_y = byte_histogram(y);
342
343    let mut sum = 0.0f64;
344    for i in 0..256 {
345        sum += (p_x[i] - p_y[i]).abs();
346    }
347
348    (sum / 2.0).clamp(0.0, 1.0)
349}
350
351#[inline(always)]
352/// Normalized Hellinger distance between the byte distributions of `x` and `y`.
353///
354/// NHD is an empirical, order-0 quantity by construction.
355pub fn nhd_bytes(x: &[u8], y: &[u8]) -> f64 {
356    if x.is_empty() || y.is_empty() {
357        return 0.0;
358    }
359    let p_x = byte_histogram(x);
360    let p_y = byte_histogram(y);
361
362    let mut bc = 0.0f64;
363    for i in 0..256 {
364        bc += (p_x[i] * p_y[i]).sqrt();
365    }
366
367    (1.0 - bc).max(0.0).sqrt()
368}
369
370#[inline(always)]
371/// Fallible cross-entropy estimate `H_train(test)` using the default context rate backend.
372pub fn try_cross_entropy_bytes(test_data: &[u8], train_data: &[u8]) -> InfotheoryResult<f64> {
373    with_default_ctx(|ctx| ctx.try_cross_entropy_bytes(test_data, train_data))
374}
375
376/// Empirical (zero-order, IID) cross-entropy `H₀_q(p) = -Σ p(x) log₂ q(x)` between
377/// the byte histograms of `test_data` (treated as `p`) and `train_data` (treated as `q`).
378pub fn empirical_cross_entropy_bytes(test_data: &[u8], train_data: &[u8]) -> f64 {
379    if test_data.is_empty() {
380        return 0.0;
381    }
382    let p_x = byte_histogram(test_data);
383    let p_y = byte_histogram(train_data);
384    let mut h = 0.0f64;
385    for i in 0..256 {
386        if p_x[i] > 0.0 {
387            let q_y = p_y[i].max(1e-12);
388            h -= p_x[i] * q_y.log2();
389        }
390    }
391    h
392}
393
394#[inline(always)]
395/// Fallible cross-entropy *rate* estimate with the default context backend.
396pub fn try_cross_entropy_rate_bytes(test_data: &[u8], train_data: &[u8]) -> InfotheoryResult<f64> {
397    with_default_ctx(|ctx| ctx.try_cross_entropy_rate_bytes(test_data, train_data))
398}
399
400/// KL divergence `D_KL(P || Q)` between the byte histograms of `x` and `y` (bits).
401///
402/// `D_KL` is an empirical, order-0 quantity by construction.
403pub fn d_kl_bytes(x: &[u8], y: &[u8]) -> f64 {
404    if x.is_empty() || y.is_empty() {
405        return 0.0;
406    }
407    let p_x = byte_histogram(x);
408    let p_y = byte_histogram(y);
409    let mut d_kl = 0.0f64;
410    for i in 0..256 {
411        if p_x[i] > 0.0 {
412            let q_y = p_y[i].max(1e-12);
413            d_kl += p_x[i] * (p_x[i] / q_y).log2();
414        }
415    }
416    d_kl.max(0.0)
417}
418
419/// Jensen-Shannon divergence between the byte histograms of `x` and `y` (bits).
420///
421/// JSD is an empirical, order-0 quantity by construction.
422pub fn js_div_bytes(x: &[u8], y: &[u8]) -> f64 {
423    if x.is_empty() || y.is_empty() {
424        return 0.0;
425    }
426    let p_x = byte_histogram(x);
427    let p_y = byte_histogram(y);
428    let mut m = [0.0f64; 256];
429    for i in 0..256 {
430        m[i] = 0.5 * (p_x[i] + p_y[i]);
431    }
432
433    let mut kl_pm = 0.0f64;
434    let mut kl_qm = 0.0f64;
435    for i in 0..256 {
436        if p_x[i] > 0.0 {
437            kl_pm += p_x[i] * (p_x[i] / m[i]).log2();
438        }
439        if p_y[i] > 0.0 {
440            kl_qm += p_y[i] * (p_y[i] / m[i]).log2();
441        }
442    }
443    (0.5 * kl_pm + 0.5 * kl_qm).max(0.0)
444}
445
446#[inline(always)]
447/// Fallible intrinsic dependence estimate:
448/// `(H₀(X) - Ĥ(X)) / H₀(X)`, where `H₀` is the order-0 / empirical entropy and
449/// `Ĥ` is the entropy rate produced by the default context's rate backend.
450pub fn try_intrinsic_dependence_bytes(data: &[u8]) -> InfotheoryResult<f64> {
451    with_default_ctx(|ctx| ctx.try_intrinsic_dependence_bytes(data))
452}
453
454#[inline(always)]
455/// Fallible resistance-to-transformation estimate:
456/// `I(X; T(X)) / H(X)` for `tx = T(x)`, using the default context rate backend.
457pub fn try_resistance_to_transformation_bytes(x: &[u8], tx: &[u8]) -> InfotheoryResult<f64> {
458    with_default_ctx(|ctx| ctx.try_resistance_to_transformation_bytes(x, tx))
459}
460
461/// Empirical (zero-order, IID) resistance-to-transformation estimate:
462/// `I₀(X; T(X)) / H₀(X)` for `tx = T(x)`.
463pub fn empirical_resistance_to_transformation_bytes(x: &[u8], tx: &[u8]) -> f64 {
464    let (x, tx) = aligned_prefix(x, tx);
465    let h_x = empirical_entropy_bytes(x);
466    if h_x < 1e-9 {
467        0.0
468    } else {
469        (empirical_mutual_information_bytes(x, tx) / h_x).clamp(0.0, 1.0)
470    }
471}