Skip to main content

infotheory/api/
compression.rs

1//! Compression-focused public API surface.
2
3use rayon::prelude::*;
4
5use crate::error::{InfotheoryError, InfotheoryResult};
6use crate::spec::CompiledCompressionBackend;
7
8use crate::runtime::CompressionRuntime;
9use crate::with_default_ctx;
10
11/// Per-call control over operation-level parallelism.
12#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
13pub enum OperationParallelism {
14    /// Execute operation-level work serially.
15    Serial,
16    /// Use adaptive/default parallel operation behavior.
17    #[default]
18    Auto,
19    /// Execute operation-level work on a bounded Rayon pool with `threads`.
20    Threads(usize),
21}
22
23/// NCD compute options (operation-level parallelism only).
24#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
25pub struct NcdComputeOptions {
26    /// Operation-level parallelism policy used while computing NCD values. (external parellization, doesn't affect compression algorithm itself)
27    pub parallelism: OperationParallelism,
28}
29
30/// Compute compressed size (bytes) for a logical concatenation of `parts` using `backend`.
31pub fn try_compress_size_chain_backend(
32    parts: &[&[u8]],
33    backend: &CompiledCompressionBackend,
34) -> InfotheoryResult<u64> {
35    let mut runtime = crate::runtime::build_compression_runtime(backend)
36        .map_err(InfotheoryError::invalid_backend_config)?;
37    runtime.compress_size_chain(parts)
38}
39
40/// Compute compressed size (bytes) for `data` using `backend`.
41pub fn try_compress_size_backend(
42    data: &[u8],
43    backend: &CompiledCompressionBackend,
44) -> InfotheoryResult<u64> {
45    let mut runtime = crate::runtime::build_compression_runtime(backend)
46        .map_err(InfotheoryError::invalid_backend_config)?;
47    runtime.compress_size(data)
48}
49
50/// Compress `data` with `backend` and return encoded bytes.
51pub fn try_compress_bytes_backend(
52    data: &[u8],
53    backend: &CompiledCompressionBackend,
54) -> InfotheoryResult<Vec<u8>> {
55    let mut runtime = crate::runtime::build_compression_runtime(backend)
56        .map_err(InfotheoryError::invalid_backend_config)?;
57    runtime.compress_bytes(data)
58}
59
60/// Decompress `input` with `backend` and return decoded bytes.
61pub fn try_decompress_bytes_backend(
62    input: &[u8],
63    backend: &CompiledCompressionBackend,
64) -> InfotheoryResult<Vec<u8>> {
65    let mut runtime = crate::runtime::build_compression_runtime(backend)
66        .map_err(InfotheoryError::invalid_backend_config)?;
67    runtime.decompress_bytes(input)
68}
69
70/// Normalized compression-distance formula variant.
71#[derive(Clone, Copy, Debug, Eq, PartialEq)]
72pub enum NcdVariant {
73    /// Vitanyi-style NCD: `(C(xy) - min(C(x), C(y))) / max(C(x), C(y))`.
74    Vitanyi,
75    /// Symmetric Vitanyi-style NCD using `min(C(xy), C(yx))`.
76    SymVitanyi,
77    /// Constructive NCD: `(C(xy) - min(C(x), C(y))) / C(xy)`.
78    Cons,
79    /// Symmetric constructive NCD using `min(C(xy), C(yx))` as denominator.
80    SymCons,
81}
82
83#[inline(always)]
84fn ncd_from_sizes(cx: u64, cy: u64, cxy: u64, cyx: Option<u64>, variant: NcdVariant) -> f64 {
85    let min_c = cx.min(cy) as f64;
86    let max_c = cx.max(cy) as f64;
87
88    match variant {
89        NcdVariant::Vitanyi => {
90            if max_c == 0.0 {
91                0.0
92            } else {
93                (cxy as f64 - min_c) / max_c
94            }
95        }
96        NcdVariant::SymVitanyi => {
97            let m = cxy.min(cyx.expect("cyx required for SymVitanyi")) as f64;
98            if max_c == 0.0 {
99                0.0
100            } else {
101                (m - min_c) / max_c
102            }
103        }
104        NcdVariant::Cons => {
105            let denom = cxy as f64;
106            if denom == 0.0 {
107                0.0
108            } else {
109                (cxy as f64 - min_c) / denom
110            }
111        }
112        NcdVariant::SymCons => {
113            let m = cxy.min(cyx.expect("cyx required for SymCons")) as f64;
114            if m == 0.0 { 0.0 } else { (m - min_c) / m }
115        }
116    }
117}
118
119#[inline(always)]
120/// Compute NCD for byte slices using the thread-local default context.
121pub fn try_ncd_bytes_default(x: &[u8], y: &[u8], variant: NcdVariant) -> InfotheoryResult<f64> {
122    with_default_ctx(|ctx| ctx.try_ncd_bytes(x, y, variant))
123}
124
125/// Compute NCD for byte slices with an explicit compression backend.
126pub fn try_ncd_bytes_backend(
127    x: &[u8],
128    y: &[u8],
129    backend: &CompiledCompressionBackend,
130    variant: NcdVariant,
131) -> InfotheoryResult<f64> {
132    try_ncd_bytes_backend_with_options(x, y, backend, variant, NcdComputeOptions::default())
133}
134
135/// Compute NCD for byte slices with an explicit compression backend and
136/// explicit operation-level parallelism controls.
137pub fn try_ncd_bytes_backend_with_options(
138    x: &[u8],
139    y: &[u8],
140    backend: &CompiledCompressionBackend,
141    variant: NcdVariant,
142    options: NcdComputeOptions,
143) -> InfotheoryResult<f64> {
144    let compute = || -> InfotheoryResult<f64> {
145        let (cx, cy) = rayon::join(
146            || try_compress_size_backend(x, backend),
147            || try_compress_size_backend(y, backend),
148        );
149        let cx = cx?;
150        let cy = cy?;
151
152        let cxy = try_compress_size_chain_backend(&[x, y], backend)?;
153
154        let cyx = match variant {
155            NcdVariant::SymVitanyi | NcdVariant::SymCons => {
156                Some(try_compress_size_chain_backend(&[y, x], backend)?)
157            }
158            _ => None,
159        };
160
161        Ok(ncd_from_sizes(cx, cy, cxy, cyx, variant))
162    };
163
164    match options.parallelism {
165        OperationParallelism::Serial => {
166            let cx = try_compress_size_backend(x, backend)?;
167            let cy = try_compress_size_backend(y, backend)?;
168            let cxy = try_compress_size_chain_backend(&[x, y], backend)?;
169            let cyx = match variant {
170                NcdVariant::SymVitanyi | NcdVariant::SymCons => {
171                    Some(try_compress_size_chain_backend(&[y, x], backend)?)
172                }
173                _ => None,
174            };
175            Ok(ncd_from_sizes(cx, cy, cxy, cyx, variant))
176        }
177        OperationParallelism::Auto => compute(),
178        OperationParallelism::Threads(threads) => {
179            if threads <= 1 {
180                return try_ncd_bytes_backend_with_options(
181                    x,
182                    y,
183                    backend,
184                    variant,
185                    NcdComputeOptions {
186                        parallelism: OperationParallelism::Serial,
187                    },
188                );
189            }
190            let pool = rayon::ThreadPoolBuilder::new()
191                .num_threads(threads)
192                .build()
193                .map_err(|err| {
194                    InfotheoryError::runtime(format!("failed to build rayon pool: {err}"))
195                })?;
196            pool.install(compute)
197        }
198    }
199}
200
201/// Compute an `n x n` pairwise NCD matrix (row-major) using the thread-local default context.
202///
203/// `out[i * n + j]` corresponds to `NCD(datas[i], datas[j])`.
204pub fn try_ncd_matrix_bytes_default(
205    datas: &[Vec<u8>],
206    variant: NcdVariant,
207) -> InfotheoryResult<Vec<f64>> {
208    with_default_ctx(|ctx| try_ncd_matrix_bytes_backend(datas, &ctx.compression_backend, variant))
209}
210
211/// Compute an `n x n` pairwise NCD matrix (row-major) with an explicit compression backend.
212///
213/// `out[i * n + j]` corresponds to `NCD(datas[i], datas[j])`.
214pub fn try_ncd_matrix_bytes_backend(
215    datas: &[Vec<u8>],
216    backend: &CompiledCompressionBackend,
217    variant: NcdVariant,
218) -> InfotheoryResult<Vec<f64>> {
219    try_ncd_matrix_bytes_backend_with_options(datas, backend, variant, NcdComputeOptions::default())
220}
221
222/// Compute an `n x n` pairwise NCD matrix with explicit operation-level
223/// parallelism controls.
224pub fn try_ncd_matrix_bytes_backend_with_options(
225    datas: &[Vec<u8>],
226    backend: &CompiledCompressionBackend,
227    variant: NcdVariant,
228    options: NcdComputeOptions,
229) -> InfotheoryResult<Vec<f64>> {
230    let compute = || try_ncd_matrix_bytes_backend_impl(datas, backend, variant);
231    match options.parallelism {
232        OperationParallelism::Serial => {
233            try_ncd_matrix_bytes_backend_serial(datas, backend, variant)
234        }
235        OperationParallelism::Auto => compute(),
236        OperationParallelism::Threads(threads) => {
237            if threads <= 1 {
238                return try_ncd_matrix_bytes_backend_serial(datas, backend, variant);
239            }
240            let pool = rayon::ThreadPoolBuilder::new()
241                .num_threads(threads)
242                .build()
243                .map_err(|err| {
244                    InfotheoryError::runtime(format!("failed to build rayon pool: {err}"))
245                })?;
246            pool.install(compute)
247        }
248    }
249}
250
251fn try_ncd_matrix_bytes_backend_impl(
252    datas: &[Vec<u8>],
253    backend: &CompiledCompressionBackend,
254    variant: NcdVariant,
255) -> InfotheoryResult<Vec<f64>> {
256    let n = datas.len();
257    let cx = datas
258        .par_iter()
259        .map(|d| try_compress_size_backend(d, backend))
260        .collect::<Vec<_>>()
261        .into_iter()
262        .collect::<InfotheoryResult<Vec<_>>>()?;
263
264    let mut out = vec![0.0f64; n * n];
265
266    match variant {
267        NcdVariant::SymVitanyi | NcdVariant::SymCons => {
268            let pairs = (0..n)
269                .flat_map(|i| (i + 1..n).map(move |j| (i, j)))
270                .collect::<Vec<_>>();
271            let pair_results = pairs
272                .into_par_iter()
273                .map(|(i, j)| -> InfotheoryResult<(usize, usize, f64)> {
274                    let x = &datas[i];
275                    let y = &datas[j];
276                    let cxy = try_compress_size_chain_backend(&[x, y], backend)?;
277                    let cyx = try_compress_size_chain_backend(&[y, x], backend)?;
278
279                    let d = ncd_from_sizes(cx[i], cx[j], cxy, Some(cyx), variant);
280                    Ok((i, j, d))
281                })
282                .collect::<Vec<_>>();
283            for entry in pair_results {
284                let (i, j, d) = entry?;
285                out[i * n + j] = d;
286                out[j * n + i] = d;
287            }
288        }
289        NcdVariant::Vitanyi | NcdVariant::Cons => {
290            let rows = (0..n)
291                .into_par_iter()
292                .map(|i| -> InfotheoryResult<Vec<(usize, usize, f64)>> {
293                    let x = &datas[i];
294                    let mut row = Vec::with_capacity(n);
295                    for j in 0..n {
296                        let d = if i == j {
297                            0.0
298                        } else {
299                            let y = &datas[j];
300                            let cxy = try_compress_size_chain_backend(&[x, y], backend)?;
301                            ncd_from_sizes(cx[i], cx[j], cxy, None, variant)
302                        };
303                        row.push((i, j, d));
304                    }
305                    Ok(row)
306                })
307                .collect::<Vec<_>>();
308            for row in rows {
309                for (i, j, d) in row? {
310                    out[i * n + j] = d;
311                }
312            }
313        }
314    }
315
316    Ok(out)
317}
318
319fn try_ncd_matrix_bytes_backend_serial(
320    datas: &[Vec<u8>],
321    backend: &CompiledCompressionBackend,
322    variant: NcdVariant,
323) -> InfotheoryResult<Vec<f64>> {
324    let n = datas.len();
325    let mut cx = Vec::with_capacity(n);
326    for d in datas {
327        cx.push(try_compress_size_backend(d, backend)?);
328    }
329    let mut out = vec![0.0f64; n * n];
330    for i in 0..n {
331        for j in 0..n {
332            if i == j {
333                out[i * n + j] = 0.0;
334                continue;
335            }
336            let cxy = try_compress_size_chain_backend(
337                &[datas[i].as_slice(), datas[j].as_slice()],
338                backend,
339            )?;
340            let cyx = match variant {
341                NcdVariant::SymVitanyi | NcdVariant::SymCons => {
342                    Some(try_compress_size_chain_backend(
343                        &[datas[j].as_slice(), datas[i].as_slice()],
344                        backend,
345                    )?)
346                }
347                _ => None,
348            };
349            out[i * n + j] = ncd_from_sizes(cx[i], cx[j], cxy, cyx, variant);
350        }
351    }
352    Ok(out)
353}