vortex_array/arrays/patched/
array.rs1use std::fmt::Display;
5use std::fmt::Formatter;
6use std::ops::Range;
7
8use vortex_buffer::Buffer;
9use vortex_buffer::BufferMut;
10use vortex_error::VortexResult;
11use vortex_error::vortex_ensure;
12use vortex_error::vortex_err;
13
14use crate::ArrayRef;
15use crate::ArraySlots;
16use crate::Canonical;
17use crate::ExecutionCtx;
18use crate::IntoArray;
19use crate::VortexSessionExecute;
20use crate::array::Array;
21use crate::array::ArrayParts;
22use crate::array::TypedArrayRef;
23use crate::array_slots;
24use crate::arrays::Patched;
25use crate::arrays::PrimitiveArray;
26use crate::arrays::patched::TransposedPatches;
27use crate::arrays::patched::patch_lanes;
28use crate::buffer::BufferHandle;
29use crate::dtype::DType;
30use crate::dtype::IntegerPType;
31use crate::dtype::NativePType;
32use crate::dtype::PType;
33use crate::legacy_session;
34use crate::match_each_native_ptype;
35use crate::match_each_unsigned_integer_ptype;
36use crate::patches::Patches;
37use crate::validity::Validity;
38
39#[derive(Debug, Clone)]
40pub struct PatchedData {
41 pub(super) n_lanes: usize,
45
46 pub(super) offset: usize,
52}
53
54#[array_slots(Patched)]
55pub struct PatchedSlots {
56 pub inner: ArrayRef,
58 pub lane_offsets: ArrayRef,
60 pub patch_indices: ArrayRef,
62 pub patch_values: ArrayRef,
64}
65
66impl Display for PatchedData {
67 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
68 write!(f, "n_lanes: {}, offset: {}", self.n_lanes, self.offset)
69 }
70}
71
72impl PatchedData {
73 pub(crate) fn validate(
74 &self,
75 dtype: &DType,
76 len: usize,
77 slots: &PatchedSlotsView,
78 ) -> VortexResult<()> {
79 vortex_ensure!(
80 slots.inner.dtype() == dtype,
81 "PatchedArray base dtype {} does not match outer dtype {}",
82 slots.inner.dtype(),
83 dtype
84 );
85 vortex_ensure!(
86 slots.inner.len() == len,
87 "PatchedArray base len {} does not match outer len {}",
88 slots.inner.len(),
89 len
90 );
91 vortex_ensure!(
92 slots.patch_indices.len() == slots.patch_values.len(),
93 "PatchedArray patch indices len {} does not match patch values len {}",
94 slots.patch_indices.len(),
95 slots.patch_values.len()
96 );
97 Ok(())
98 }
99}
100
101pub trait PatchedArrayExt: PatchedArraySlotsExt {
102 #[inline]
103 fn n_lanes(&self) -> usize {
104 self.n_lanes
105 }
106
107 #[inline]
108 fn offset(&self) -> usize {
109 self.offset
110 }
111
112 #[inline]
113 #[allow(clippy::disallowed_methods)]
114 fn lane_range(&self, chunk: usize, lane: usize) -> VortexResult<Range<usize>> {
115 assert!(chunk * 1024 <= self.as_ref().len() + self.offset());
116 assert!(lane < self.n_lanes());
117
118 let start = self.lane_offsets().execute_scalar(
119 chunk * self.n_lanes() + lane,
120 &mut legacy_session().create_execution_ctx(),
121 )?;
122 let stop = self.lane_offsets().execute_scalar(
123 chunk * self.n_lanes() + lane + 1,
124 &mut legacy_session().create_execution_ctx(),
125 )?;
126
127 let start = start
128 .as_primitive()
129 .as_::<usize>()
130 .ok_or_else(|| vortex_err!("could not cast lane_offset to usize"))?;
131
132 let stop = stop
133 .as_primitive()
134 .as_::<usize>()
135 .ok_or_else(|| vortex_err!("could not cast lane_offset to usize"))?;
136
137 Ok(start..stop)
138 }
139
140 fn slice_chunks(&self, chunks: Range<usize>) -> VortexResult<Array<Patched>> {
141 let lane_offsets_start = chunks.start * self.n_lanes();
142 let lane_offsets_stop = chunks.end * self.n_lanes() + 1;
143
144 let sliced_lane_offsets = self
145 .lane_offsets()
146 .slice(lane_offsets_start..lane_offsets_stop)?;
147 let indices = self.patch_indices().clone();
148 let values = self.patch_values().clone();
149
150 let begin = (chunks.start * 1024).saturating_sub(self.offset());
151 let end = (chunks.end * 1024)
152 .saturating_sub(self.offset())
153 .min(self.as_ref().len());
154
155 let offset = if chunks.start == 0 { self.offset() } else { 0 };
156 let inner = self.inner().slice(begin..end)?;
157 let len = inner.len();
158 let dtype = self.as_ref().dtype().clone();
159 let slots = PatchedSlots {
160 inner,
161 lane_offsets: sliced_lane_offsets,
162 patch_indices: indices,
163 patch_values: values,
164 }
165 .into_slots();
166
167 Ok(unsafe { Patched::new_unchecked(dtype, len, slots, self.n_lanes(), offset) })
168 }
169}
170
171impl<T: TypedArrayRef<Patched>> PatchedArrayExt for T {}
172
173impl Patched {
174 pub fn from_array_and_patches(
175 inner: ArrayRef,
176 patches: &Patches,
177 ctx: &mut ExecutionCtx,
178 ) -> VortexResult<Array<Patched>> {
179 vortex_ensure!(
180 inner.dtype().eq_with_nullability_superset(patches.dtype()),
181 "array DType must match patches DType"
182 );
183
184 vortex_ensure!(
185 inner.dtype().is_primitive(),
186 "Creating PatchedArray from Patches only supported for primitive arrays"
187 );
188
189 vortex_ensure!(
190 patches.num_patches() <= u32::MAX as usize,
191 "PatchedArray does not support > u32::MAX patch values"
192 );
193
194 vortex_ensure!(
195 patches.values().all_valid(ctx)?,
196 "PatchedArray cannot be built from Patches with nulls"
197 );
198
199 let values_ptype = patches.dtype().as_ptype();
200
201 let TransposedPatches {
202 n_lanes,
203 lane_offsets,
204 indices,
205 values,
206 } = transpose_patches(patches, ctx)?;
207
208 let lane_offsets = PrimitiveArray::from_buffer_handle(
209 BufferHandle::new_host(lane_offsets),
210 PType::U32,
211 Validity::NonNullable,
212 )
213 .into_array();
214 let indices = PrimitiveArray::from_buffer_handle(
215 BufferHandle::new_host(indices),
216 PType::U16,
217 Validity::NonNullable,
218 )
219 .into_array();
220 let values = PrimitiveArray::from_buffer_handle(
221 BufferHandle::new_host(values),
222 values_ptype,
223 Validity::NonNullable,
224 )
225 .into_array();
226
227 let dtype = inner.dtype().clone();
228 let len = inner.len();
229 let slots = PatchedSlots {
230 inner,
231 lane_offsets,
232 patch_indices: indices,
233 patch_values: values,
234 }
235 .into_slots();
236 Ok(unsafe { Self::new_unchecked(dtype, len, slots, n_lanes, 0) })
237 }
238
239 pub(crate) unsafe fn new_unchecked(
240 dtype: DType,
241 len: usize,
242 slots: ArraySlots,
243 n_lanes: usize,
244 offset: usize,
245 ) -> Array<Patched> {
246 unsafe {
247 Array::from_parts_unchecked(
248 ArrayParts::new(Patched, dtype, len, PatchedData { n_lanes, offset })
249 .with_slots(slots),
250 )
251 }
252 }
253}
254
255fn transpose_patches(patches: &Patches, ctx: &mut ExecutionCtx) -> VortexResult<TransposedPatches> {
257 let array_len = patches.array_len();
258 let offset = patches.offset();
259
260 let indices = patches
261 .indices()
262 .clone()
263 .execute::<Canonical>(ctx)?
264 .into_primitive();
265
266 let values = patches
267 .values()
268 .clone()
269 .execute::<Canonical>(ctx)?
270 .into_primitive();
271
272 let indices_ptype = indices.ptype();
273 let values_ptype = values.ptype();
274
275 let indices = indices.buffer_handle().clone().unwrap_host();
276 let values = values.buffer_handle().clone().unwrap_host();
277
278 match_each_unsigned_integer_ptype!(indices_ptype, |I| {
279 match_each_native_ptype!(values_ptype, |V| {
280 let indices: Buffer<I> = Buffer::from_byte_buffer(indices);
281 let values: Buffer<V> = Buffer::from_byte_buffer(values);
282
283 Ok(transpose(
284 indices.as_slice(),
285 values.as_slice(),
286 offset,
287 array_len,
288 ))
289 })
290 })
291}
292
293#[expect(clippy::cast_possible_truncation)]
294fn transpose<I: IntegerPType, V: NativePType>(
295 indices_in: &[I],
296 values_in: &[V],
297 offset: usize,
298 array_len: usize,
299) -> TransposedPatches {
300 let n_chunks = array_len.div_ceil(1024);
302 assert!(
303 n_chunks <= u32::MAX as usize,
304 "Cannot transpose patches for array with >= 4 trillion elements"
305 );
306
307 let n_lanes = patch_lanes::<V>();
308
309 let mut indices_buffer = BufferMut::with_capacity(indices_in.len());
311 let mut values_buffer = BufferMut::with_capacity(values_in.len());
312
313 let mut lane_offsets: BufferMut<u32> = BufferMut::zeroed(n_chunks * n_lanes + 1);
315
316 for index in indices_in {
318 let index = index.as_() - offset;
319 let chunk = index / 1024;
320 let lane = index % n_lanes;
321
322 lane_offsets[chunk * n_lanes + lane + 1] += 1;
323 }
324
325 for index in 1..lane_offsets.len() {
326 lane_offsets[index] += lane_offsets[index - 1];
327 }
328
329 let indices_out = indices_buffer.spare_capacity_mut();
331 let values_out = values_buffer.spare_capacity_mut();
332 for (index, &value) in std::iter::zip(indices_in, values_in) {
333 let index = index.as_() - offset;
334 let chunk = index / 1024;
335 let lane = index % n_lanes;
336
337 let position = &mut lane_offsets[chunk * n_lanes + lane];
338 indices_out[*position as usize].write((index % 1024) as u16);
339 values_out[*position as usize].write(value);
340 *position += 1;
341 }
342
343 unsafe {
344 indices_buffer.set_len(indices_in.len());
345 values_buffer.set_len(values_in.len());
346 }
347
348 for index in indices_in {
349 let index = index.as_() - offset;
350 let chunk = index / 1024;
351 let lane = index % n_lanes;
352
353 lane_offsets[chunk * n_lanes + lane] -= 1;
354 }
355
356 TransposedPatches {
357 n_lanes,
358 lane_offsets: lane_offsets.freeze().into_byte_buffer(),
359 indices: indices_buffer.freeze().into_byte_buffer(),
360 values: values_buffer.freeze().into_byte_buffer(),
361 }
362}
363
364#[cfg(test)]
365mod tests {
366 use vortex_buffer::buffer;
367
368 use super::PatchedSlots;
369 use crate::ArrayRef;
370 use crate::IntoArray;
371 use crate::array_slots;
372 use crate::arrays::Null;
373 use crate::arrays::PrimitiveArray;
374 use crate::validity::Validity;
375
376 #[array_slots(Null)]
377 struct OptionalPatchedSlots {
378 required: ArrayRef,
379 maybe: Option<ArrayRef>,
380 }
381
382 #[test]
383 fn generated_slots_round_trip() {
384 let required = PrimitiveArray::new(buffer![1u8, 2, 3], Validity::NonNullable).into_array();
385 let optional = PrimitiveArray::new(buffer![4u8, 5, 6], Validity::NonNullable).into_array();
386
387 let slot_vec = vec![Some(required.clone()), Some(optional.clone())];
388 let view = OptionalPatchedSlotsView::from_slots(&slot_vec);
389 assert_eq!(view.required.len(), 3);
390 assert_eq!(view.maybe.expect("optional slot").len(), 3);
391
392 let cloned = OptionalPatchedSlots::from_slots(slot_vec.into());
393 assert_eq!(cloned.required.len(), required.len());
394 assert_eq!(cloned.maybe.expect("optional clone").len(), optional.len());
395
396 let rebuilt = PatchedSlots::from_slots(
397 vec![
398 Some(required.clone()),
399 Some(optional.clone()),
400 Some(required.clone()),
401 Some(optional.clone()),
402 ]
403 .into(),
404 );
405 assert_eq!(rebuilt.inner.len(), required.len());
406 assert_eq!(rebuilt.patch_values.len(), optional.len());
407 }
408}