Skip to main content

vortex_compressor/scheme/
estimate.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! Compression ratio estimation types returned by schemes.
5
6use std::fmt;
7
8use vortex_array::ExecutionCtx;
9use vortex_error::VortexResult;
10
11use crate::CascadingCompressor;
12use crate::scheme::CompressorContext;
13use crate::stats::ArrayAndStats;
14
15/// Closure type for [`DeferredEstimate::Callback`].
16///
17/// The compressor calls this with the same arguments it would pass to sampling, plus the best
18/// [`EstimateScore`] observed so far (if any). The closure must resolve directly to a terminal
19/// [`EstimateVerdict`].
20///
21/// The `best_so_far` threshold is an early-exit hint. If your scheme's maximum achievable
22/// compression ratio is not strictly greater than
23/// `best_so_far.and_then(EstimateScore::finite_ratio)`, you should return
24/// [`EstimateVerdict::Skip`]. Returning a ratio equal to the threshold is permitted but will
25/// lose to the prior best due to strict `>` tie-breaking in the selector. Use the threshold
26/// only as an early-exit hint, never to perform additional work.
27#[rustfmt::skip]
28pub type EstimateFn = dyn FnOnce(
29        &CascadingCompressor,
30        &ArrayAndStats,
31        Option<EstimateScore>,
32        CompressorContext,
33        &mut ExecutionCtx,
34    ) -> VortexResult<EstimateVerdict>
35    + Send
36    + Sync;
37
38/// The result of a [`Scheme`](crate::scheme::Scheme)'s compression ratio estimation.
39///
40/// This type is returned by
41/// [`Scheme::expected_compression_ratio`](crate::scheme::Scheme::expected_compression_ratio) to
42/// tell the compressor how promising this scheme is for a given array without performing any
43/// expensive work.
44///
45/// [`CompressionEstimate::Verdict`] means the scheme already knows the terminal answer.
46/// [`CompressionEstimate::Deferred`] means the compressor must do extra work before the scheme can
47/// produce a terminal answer.
48#[derive(Debug)]
49pub enum CompressionEstimate {
50    /// The scheme already knows the terminal estimation verdict.
51    Verdict(EstimateVerdict),
52
53    /// The compressor must perform deferred work to resolve the terminal estimation verdict.
54    Deferred(DeferredEstimate),
55}
56
57/// The terminal answer to a compression estimate request.
58#[derive(Debug)]
59pub enum EstimateVerdict {
60    /// Do not use this scheme for this array.
61    Skip,
62
63    /// Always use this scheme, as it is definitively the best choice.
64    ///
65    /// Some examples include decimal byte parts and temporal decomposition.
66    ///
67    /// The compressor will select this scheme immediately without evaluating further candidates.
68    /// Schemes that return `AlwaysUse` must be mutually exclusive per canonical type (enforced by
69    /// [`Scheme::matches`]), otherwise the winner depends silently on registration order.
70    ///
71    /// [`Scheme::matches`]: crate::scheme::Scheme::matches
72    AlwaysUse,
73
74    /// The estimated compression ratio. This must be greater than `1.0` to be considered by the
75    /// compressor, otherwise it is worse than the canonical encoding.
76    Ratio(f64),
77}
78
79/// Deferred work that can resolve to a terminal [`EstimateVerdict`].
80pub enum DeferredEstimate {
81    /// The scheme cannot cheaply estimate its ratio, so the compressor should compress a small
82    /// sample to determine effectiveness.
83    Sample,
84
85    /// A fallible estimation requiring a custom expensive computation.
86    ///
87    /// Use this only when the scheme needs to perform trial encoding or other costly checks to
88    /// determine its compression ratio. The callback returns an [`EstimateVerdict`] directly, so
89    /// it cannot request more sampling or another deferred callback.
90    ///
91    /// The compressor evaluates all immediate [`CompressionEstimate::Verdict`] results before
92    /// invoking any deferred callback, and passes the best [`EstimateScore`] observed so far to
93    /// the callback. This lets the callback return [`EstimateVerdict::Skip`] without performing
94    /// expensive work when its maximum achievable ratio cannot beat the current best. See
95    /// [`EstimateFn`] for the full contract.
96    Callback(Box<EstimateFn>),
97}
98
99/// Ranked estimate used for comparing non-terminal compression candidates.
100#[derive(Debug, Clone, Copy, PartialEq)]
101pub enum EstimateScore {
102    /// A finite compression ratio. Higher means a smaller amount of data, so it is better.
103    FiniteCompression(f64),
104    /// Trial compression produced a 0-byte output.
105    ///
106    /// This has no finite ratio and is not eligible for scheme selection.
107    ///
108    /// TODO(connor): A zero-byte sample usually means the sampler happened to hit an all-null
109    /// sample. Improve this logic so we can distinguish real zero-byte wins from sampling artifacts.
110    ZeroBytes,
111}
112
113impl EstimateScore {
114    /// Converts measured sample sizes into a ranked estimate.
115    pub(crate) fn from_sample_sizes(before_nbytes: u64, after_nbytes: u64) -> Self {
116        if after_nbytes == 0 {
117            Self::ZeroBytes
118        } else {
119            Self::FiniteCompression(before_nbytes as f64 / after_nbytes as f64)
120        }
121    }
122
123    /// Returns the finite compression ratio, or [`None`] for the zero-byte special case.
124    ///
125    /// Callers comparing a scheme's maximum achievable ratio against a "best so far" threshold
126    /// should use this to extract a numeric value from an [`EstimateScore`].
127    pub fn finite_ratio(self) -> Option<f64> {
128        match self {
129            Self::FiniteCompression(ratio) => Some(ratio),
130            Self::ZeroBytes => None,
131        }
132    }
133
134    /// Returns whether this estimate is eligible to compete.
135    pub(crate) fn is_valid(self) -> bool {
136        match self {
137            Self::FiniteCompression(ratio) => {
138                ratio.is_finite() && !ratio.is_subnormal() && ratio > 1.0
139            }
140            Self::ZeroBytes => false,
141        }
142    }
143
144    /// Returns whether this estimate beats another valid estimate.
145    pub(crate) fn beats(self, other: Self) -> bool {
146        match (self, other) {
147            (Self::ZeroBytes, _) => false,
148            (Self::FiniteCompression(_), Self::ZeroBytes) => true,
149            (Self::FiniteCompression(ratio), Self::FiniteCompression(best_ratio)) => {
150                ratio > best_ratio
151            }
152        }
153    }
154}
155
156impl fmt::Debug for DeferredEstimate {
157    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158        match self {
159            DeferredEstimate::Sample => write!(f, "Sample"),
160            DeferredEstimate::Callback(_) => write!(f, "Callback(..)"),
161        }
162    }
163}