Skip to main content

vortex_compressor/builtins/dict/
float.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! Float-specific dictionary encoding implementation.
5//!
6//! Vortex encoders must always produce unsigned integer codes; signed codes are only accepted for
7//! external compatibility.
8
9use vortex_array::ArrayId;
10use vortex_array::ArrayRef;
11use vortex_array::ArrayView;
12use vortex_array::Canonical;
13use vortex_array::ExecutionCtx;
14use vortex_array::IntoArray;
15use vortex_array::VTable;
16use vortex_array::arrays::Dict;
17use vortex_array::arrays::DictArray;
18use vortex_array::arrays::Primitive;
19use vortex_array::arrays::PrimitiveArray;
20use vortex_array::arrays::dict::DictArrayExt;
21use vortex_array::arrays::dict::DictArraySlotsExt;
22use vortex_array::arrays::primitive::PrimitiveArrayExt;
23use vortex_array::dtype::half::f16;
24use vortex_array::validity::Validity;
25use vortex_buffer::Buffer;
26use vortex_error::VortexExpect;
27use vortex_error::VortexResult;
28
29use crate::CascadingCompressor;
30use crate::builtins::IntDictScheme;
31use crate::scheme::ChildSelection;
32use crate::scheme::CompressionEstimate;
33use crate::scheme::CompressorContext;
34use crate::scheme::DeferredEstimate;
35use crate::scheme::DescendantExclusion;
36use crate::scheme::EstimateVerdict;
37use crate::scheme::Scheme;
38use crate::scheme::SchemeExt;
39use crate::stats::ArrayAndStats;
40use crate::stats::FloatErasedStats;
41use crate::stats::FloatStats;
42use crate::stats::GenerateStatsOptions;
43
44/// Dictionary encoding for low-cardinality float values.
45#[derive(Debug, Copy, Clone, PartialEq, Eq)]
46pub struct FloatDictScheme;
47
48impl Scheme for FloatDictScheme {
49    fn scheme_name(&self) -> &'static str {
50        "vortex.float.dict"
51    }
52
53    fn matches(&self, canonical: &Canonical) -> bool {
54        canonical.dtype().is_float()
55    }
56
57    fn produced_encodings(&self) -> Vec<ArrayId> {
58        vec![Dict.id()]
59    }
60
61    fn stats_options(&self) -> GenerateStatsOptions {
62        GenerateStatsOptions {
63            count_distinct_values: true,
64        }
65    }
66
67    /// Children: values=0, codes=1.
68    fn num_children(&self) -> usize {
69        2
70    }
71
72    /// Float dict codes (child 1) are compact unsigned integers that should not be
73    /// dict-encoded again. Float dict values (child 0) flow through ALP into integer-land,
74    /// where integer dict encoding is redundant since the values are already deduplicated at
75    /// the float level.
76    ///
77    /// Additional exclusions for codes (IntSequenceScheme, IntRunEndScheme, FoRScheme,
78    /// ZigZagScheme, SparseScheme, RLE) are expressed as pull rules on those schemes in
79    /// vortex-btrblocks.
80    fn descendant_exclusions(&self) -> Vec<DescendantExclusion> {
81        vec![
82            DescendantExclusion {
83                excluded: IntDictScheme.id(),
84                children: ChildSelection::One(1),
85            },
86            DescendantExclusion {
87                excluded: IntDictScheme.id(),
88                children: ChildSelection::One(0),
89            },
90        ]
91    }
92
93    fn expected_compression_ratio(
94        &self,
95        data: &ArrayAndStats,
96        _compress_ctx: CompressorContext,
97        exec_ctx: &mut ExecutionCtx,
98    ) -> CompressionEstimate {
99        let stats = data.float_stats(exec_ctx);
100
101        if stats.value_count() == 0 {
102            return CompressionEstimate::Verdict(EstimateVerdict::Skip);
103        }
104
105        let distinct_values_count = stats.distinct_count().vortex_expect(
106            "this must be present since `DictScheme` declared that we need distinct values",
107        );
108
109        // If > 50% of the values are distinct, skip dictionary scheme.
110        if distinct_values_count > stats.value_count() / 2 {
111            return CompressionEstimate::Verdict(EstimateVerdict::Skip);
112        }
113
114        // Let sampling determine the expected ratio.
115        CompressionEstimate::Deferred(DeferredEstimate::Sample)
116    }
117
118    fn compress(
119        &self,
120        compressor: &CascadingCompressor,
121        data: &ArrayAndStats,
122        compress_ctx: CompressorContext,
123        exec_ctx: &mut ExecutionCtx,
124    ) -> VortexResult<ArrayRef> {
125        let stats = data.float_stats(exec_ctx);
126        let dict = dictionary_encode(data.array_as_primitive(), &stats)?;
127
128        let has_all_values_referenced = dict.has_all_values_referenced();
129
130        // Values = child 0.
131        let compressed_values =
132            compressor.compress_child(dict.values(), &compress_ctx, self.id(), 0, exec_ctx)?;
133
134        // Codes = child 1.
135        let narrowed_codes = dict
136            .codes()
137            .clone()
138            .execute::<PrimitiveArray>(exec_ctx)?
139            .narrow(exec_ctx)?
140            .into_array();
141        let compressed_codes =
142            compressor.compress_child(&narrowed_codes, &compress_ctx, self.id(), 1, exec_ctx)?;
143
144        // SAFETY: compressing codes or values does not alter the invariants.
145        unsafe {
146            Ok(
147                DictArray::new_unchecked(compressed_codes, compressed_values)
148                    .set_all_values_referenced(has_all_values_referenced)
149                    .into_array(),
150            )
151        }
152    }
153}
154
155/// Encodes a typed float array into a [`DictArray`] using the pre-computed distinct values.
156macro_rules! typed_encode {
157    ($source_array:ident, $stats:ident, $typed:ident, $typ:ty) => {{
158        let distinct = $typed.distinct().vortex_expect(
159            "this must be present since `DictScheme` declared that we need distinct values",
160        );
161
162        let values_validity = match $source_array.validity()? {
163            Validity::NonNullable => Validity::NonNullable,
164            _ => Validity::AllValid,
165        };
166        let codes_validity = $source_array.validity()?;
167
168        let values: Buffer<$typ> = distinct.distinct_values().iter().map(|x| x.0).collect();
169
170        let max_code = values.len();
171        let codes = if max_code <= u8::MAX as usize {
172            let buf = <DictEncoder as Encode<$typ, u8>>::encode(
173                &values,
174                $source_array.as_slice::<$typ>(),
175            );
176            PrimitiveArray::new(buf, codes_validity).into_array()
177        } else if max_code <= u16::MAX as usize {
178            let buf = <DictEncoder as Encode<$typ, u16>>::encode(
179                &values,
180                $source_array.as_slice::<$typ>(),
181            );
182            PrimitiveArray::new(buf, codes_validity).into_array()
183        } else {
184            let buf = <DictEncoder as Encode<$typ, u32>>::encode(
185                &values,
186                $source_array.as_slice::<$typ>(),
187            );
188            PrimitiveArray::new(buf, codes_validity).into_array()
189        };
190
191        let values = PrimitiveArray::new(values, values_validity).into_array();
192        // SAFETY: enforced by the DictEncoder.
193        Ok(unsafe { DictArray::new_unchecked(codes, values).set_all_values_referenced(true) })
194    }};
195}
196
197/// Compresses a floating-point array into a dictionary array according to attached stats.
198///
199/// # Errors
200///
201/// Returns an error if unable to compute validity.
202pub fn dictionary_encode(
203    array: ArrayView<'_, Primitive>,
204    stats: &FloatStats,
205) -> VortexResult<DictArray> {
206    match stats.erased() {
207        FloatErasedStats::F16(typed) => typed_encode!(array, stats, typed, f16),
208        FloatErasedStats::F32(typed) => typed_encode!(array, stats, typed, f32),
209        FloatErasedStats::F64(typed) => typed_encode!(array, stats, typed, f64),
210    }
211}
212
213/// Stateless encoder that maps values to dictionary codes via a `HashMap`.
214struct DictEncoder;
215
216/// Trait for encoding values of type `T` into codes of type `I`.
217trait Encode<T, I> {
218    /// Using the distinct value set, turn the values into a set of codes.
219    fn encode(distinct: &[T], values: &[T]) -> Buffer<I>;
220}
221
222/// Implements [`Encode`] for a float type using its bit representation as the hash key.
223macro_rules! impl_encode {
224    ($typ:ty, $utyp:ty) => { impl_encode!($typ, $utyp, u8, u16, u32); };
225    ($typ:ty, $utyp:ty, $($ityp:ty),+) => {
226        $(
227        impl Encode<$typ, $ityp> for DictEncoder {
228            #[expect(clippy::cast_possible_truncation)]
229            fn encode(distinct: &[$typ], values: &[$typ]) -> Buffer<$ityp> {
230                let mut codes =
231                    vortex_utils::aliases::hash_map::HashMap::<$utyp, $ityp>::with_capacity(
232                        distinct.len(),
233                    );
234                for (code, &value) in distinct.iter().enumerate() {
235                    codes.insert(value.to_bits(), code as $ityp);
236                }
237
238                let mut output = vortex_buffer::BufferMut::with_capacity(values.len());
239                for value in values {
240                    // Any code lookups which fail are for nulls, so their value does not matter.
241                    output.push(codes.get(&value.to_bits()).copied().unwrap_or_default());
242                }
243
244                output.freeze()
245            }
246        }
247        )*
248    };
249}
250
251impl_encode!(f16, u16);
252impl_encode!(f32, u32);
253impl_encode!(f64, u64);
254
255#[cfg(test)]
256mod tests {
257    use vortex_array::IntoArray;
258    use vortex_array::VortexSessionExecute;
259    use vortex_array::arrays::BoolArray;
260    use vortex_array::arrays::PrimitiveArray;
261    use vortex_array::arrays::dict::DictArraySlotsExt;
262    use vortex_array::assert_arrays_eq;
263    use vortex_array::validity::Validity;
264    use vortex_buffer::buffer;
265    use vortex_error::VortexResult;
266
267    use super::dictionary_encode;
268    use crate::stats::FloatStats;
269    use crate::stats::GenerateStatsOptions;
270
271    #[test]
272    fn test_float_dict_encode() -> VortexResult<()> {
273        let mut ctx = vortex_array::array_session().create_execution_ctx();
274        let values = buffer![1f32, 2f32, 2f32, 0f32, 1f32];
275        let validity =
276            Validity::Array(BoolArray::from_iter([true, true, true, false, true]).into_array());
277        let array = PrimitiveArray::new(values, validity);
278
279        let stats = FloatStats::generate_opts(
280            &array,
281            GenerateStatsOptions {
282                count_distinct_values: true,
283            },
284            &mut ctx,
285        );
286        let dict_array = dictionary_encode(array.as_view(), &stats)?;
287        assert_eq!(dict_array.values().len(), 2);
288        assert_eq!(dict_array.codes().len(), 5);
289
290        let expected = PrimitiveArray::new(
291            buffer![1f32, 2f32, 2f32, 1f32, 1f32],
292            Validity::Array(BoolArray::from_iter([true, true, true, false, true]).into_array()),
293        )
294        .into_array();
295        let undict = dict_array
296            .as_array()
297            .clone()
298            .execute::<PrimitiveArray>(&mut ctx)?
299            .into_array();
300        assert_arrays_eq!(undict, expected, &mut ctx);
301        Ok(())
302    }
303}