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}