vortex_compressor/builtins/dict/
float.rs1use 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#[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 fn num_children(&self) -> usize {
69 2
70 }
71
72 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 distinct_values_count > stats.value_count() / 2 {
111 return CompressionEstimate::Verdict(EstimateVerdict::Skip);
112 }
113
114 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 let compressed_values =
132 compressor.compress_child(dict.values(), &compress_ctx, self.id(), 0, exec_ctx)?;
133
134 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 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
155macro_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 Ok(unsafe { DictArray::new_unchecked(codes, values).set_all_values_referenced(true) })
194 }};
195}
196
197pub 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
213struct DictEncoder;
215
216trait Encode<T, I> {
218 fn encode(distinct: &[T], values: &[T]) -> Buffer<I>;
220}
221
222macro_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 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}