polars_core/chunked_array/ops/
gather.rs1#![allow(unsafe_op_in_unsafe_fn)]
2use std::sync::OnceLock;
3
4use polars_arrow::bitmap::Bitmap;
5use polars_arrow::bitmap::bitmask::BitMask;
6use polars_compute::gather::take_unchecked;
7use polars_error::polars_ensure;
8use polars_utils::index::check_bounds;
9
10use crate::prelude::*;
11use crate::series::IsSorted;
12use crate::utils::Container;
13
14pub fn check_bounds_nulls(idx: &PrimitiveArray<IdxSize>, len: IdxSize) -> PolarsResult<()> {
15 let mask = BitMask::from_bitmap(idx.validity().unwrap());
16
17 for (block_idx, block) in idx.values().chunks(32).enumerate() {
19 let mut in_bounds = 0;
20 for (i, x) in block.iter().enumerate() {
21 in_bounds |= ((*x < len) as u32) << i;
22 }
23 let m = mask.get_u32(32 * block_idx);
24 polars_ensure!(m == m & in_bounds, ComputeError: "gather indices are out of bounds");
25 }
26 Ok(())
27}
28
29pub fn check_bounds_ca(indices: &IdxCa, len: IdxSize) -> PolarsResult<()> {
30 let all_valid = indices.downcast_iter().all(|a| {
31 if a.null_count() == 0 {
32 check_bounds(a.values(), len).is_ok()
33 } else {
34 check_bounds_nulls(a, len).is_ok()
35 }
36 });
37 polars_ensure!(all_valid, OutOfBounds: "gather indices are out of bounds");
38 Ok(())
39}
40
41impl<T: PolarsDataType, I: AsRef<[IdxSize]> + ?Sized> ChunkTake<I> for ChunkedArray<T>
42where
43 ChunkedArray<T>: ChunkTakeUnchecked<I>,
44{
45 fn take(&self, indices: &I) -> PolarsResult<Self> {
47 check_bounds(indices.as_ref(), self.len() as IdxSize)?;
48
49 Ok(unsafe { self.take_unchecked(indices) })
51 }
52}
53
54impl<T: PolarsDataType> ChunkTake<IdxCa> for ChunkedArray<T>
55where
56 ChunkedArray<T>: ChunkTakeUnchecked<IdxCa>,
57{
58 fn take(&self, indices: &IdxCa) -> PolarsResult<Self> {
60 check_bounds_ca(indices, self.len() as IdxSize)?;
61
62 Ok(unsafe { self.take_unchecked(indices) })
64 }
65}
66
67fn cumulative_lengths<A: StaticArray>(arrs: &[&A]) -> Vec<IdxSize> {
72 let mut ret = Vec::with_capacity(arrs.len());
73 let mut cumsum: IdxSize = 0;
74 for arr in arrs {
75 ret.push(cumsum);
76 cumsum = cumsum.checked_add(arr.len().try_into().unwrap()).unwrap();
77 }
78 ret
79}
80
81#[rustfmt::skip]
82#[inline]
83fn resolve_chunked_idx(idx: IdxSize, cumlens: &[IdxSize]) -> (usize, usize) {
84 let chunk_idx = cumlens.partition_point(|cl| idx >= *cl) - 1;
85 (chunk_idx, (idx - cumlens[chunk_idx]) as usize)
86}
87
88#[inline]
89unsafe fn target_value_unchecked<'a, A: StaticArray>(
90 targets: &[&'a A],
91 cumlens: &[IdxSize],
92 idx: IdxSize,
93) -> A::ValueT<'a> {
94 let (chunk_idx, arr_idx) = resolve_chunked_idx(idx, cumlens);
95 let arr = targets.get_unchecked(chunk_idx);
96 arr.value_unchecked(arr_idx)
97}
98
99#[inline]
100unsafe fn target_get_unchecked<'a, A: StaticArray>(
101 targets: &[&'a A],
102 cumlens: &[IdxSize],
103 idx: IdxSize,
104) -> Option<A::ValueT<'a>> {
105 let (chunk_idx, arr_idx) = resolve_chunked_idx(idx, cumlens);
106 let arr = targets.get_unchecked(chunk_idx);
107 arr.get_unchecked(arr_idx)
108}
109
110unsafe fn gather_idx_array_unchecked<A: StaticArray>(
111 dtype: ArrowDataType,
112 targets: &[&A],
113 has_nulls: bool,
114 indices: &[IdxSize],
115) -> A {
116 let it = indices.iter().copied();
117 if targets.len() == 1 {
118 let target = targets.first().unwrap();
119 if has_nulls {
120 it.map(|i| target.get_unchecked(i as usize))
121 .collect_arr_trusted_with_dtype(dtype)
122 } else if let Some(sl) = target.as_slice() {
123 it.map(|i| sl.get_unchecked(i as usize).clone())
125 .collect_arr_trusted_with_dtype(dtype)
126 } else {
127 it.map(|i| target.value_unchecked(i as usize))
128 .collect_arr_trusted_with_dtype(dtype)
129 }
130 } else {
131 let cumlens = cumulative_lengths(targets);
132 if has_nulls {
133 it.map(|i| target_get_unchecked(targets, &cumlens, i))
134 .collect_arr_trusted_with_dtype(dtype)
135 } else {
136 it.map(|i| target_value_unchecked(targets, &cumlens, i))
137 .collect_arr_trusted_with_dtype(dtype)
138 }
139 }
140}
141
142impl<T: PolarsDataType, I: AsRef<[IdxSize]> + ?Sized> ChunkTakeUnchecked<I> for ChunkedArray<T>
143where
144 T: PolarsDataType<HasViews = FalseT, IsStruct = FalseT, IsNested = FalseT>,
145{
146 unsafe fn take_unchecked(&self, indices: &I) -> Self {
148 let ca = self;
149 let targets: Vec<_> = ca.downcast_iter().collect();
150 let arr = gather_idx_array_unchecked(
151 ca.dtype().to_arrow(CompatLevel::newest()),
152 &targets,
153 ca.null_count() > 0,
154 indices.as_ref(),
155 );
156 ChunkedArray::from_chunk_iter_like(ca, [arr])
157 }
158}
159
160fn gather_sorted_flag<T: PolarsDataType>(
163 ca: &ChunkedArray<T>,
164 indices: &IdxCa,
165 out: &ChunkedArray<T>,
166) -> IsSorted {
167 let flag = _update_gather_sorted_flag(ca.is_sorted_flag(), indices.is_sorted_flag());
168 if flag == IsSorted::Not || ca.null_count() == 0 || indices.null_count() == 0 {
169 return flag;
170 }
171 let (Some(first), Some(last)) = (out.first_non_null(), out.last_non_null()) else {
173 return flag;
174 };
175 let null_count = out.null_count();
176 if first == null_count || out.len() - 1 - last == null_count {
177 flag
178 } else {
179 IsSorted::Not
180 }
181}
182
183pub fn _update_gather_sorted_flag(sorted_arr: IsSorted, sorted_idx: IsSorted) -> IsSorted {
184 use crate::series::IsSorted::*;
185 match (sorted_arr, sorted_idx) {
186 (_, Not) => Not,
187 (Not, _) => Not,
188 (Ascending, Ascending) => Ascending,
189 (Ascending, Descending) => Descending,
190 (Descending, Ascending) => Descending,
191 (Descending, Descending) => Ascending,
192 }
193}
194
195impl<T: PolarsDataType> ChunkTakeUnchecked<IdxCa> for ChunkedArray<T>
196where
197 T: PolarsDataType<HasViews = FalseT, IsStruct = FalseT, IsNested = FalseT>,
198{
199 unsafe fn take_unchecked(&self, indices: &IdxCa) -> Self {
201 let ca = self;
202 let targets_have_nulls = ca.null_count() > 0;
203 let targets: Vec<_> = ca.downcast_iter().collect();
204
205 let chunks = indices.downcast_iter().map(|idx_arr| {
206 let dtype = ca.dtype().to_arrow(CompatLevel::newest());
207 if idx_arr.null_count() == 0 {
208 gather_idx_array_unchecked(dtype, &targets, targets_have_nulls, idx_arr.values())
209 } else if targets.len() == 1 {
210 let target = targets.first().unwrap();
211 if targets_have_nulls {
212 idx_arr
213 .iter()
214 .map(|i| target.get_unchecked(*i? as usize))
215 .collect_arr_trusted_with_dtype(dtype)
216 } else {
217 idx_arr
218 .iter()
219 .map(|i| Some(target.value_unchecked(*i? as usize)))
220 .collect_arr_trusted_with_dtype(dtype)
221 }
222 } else {
223 let cumlens = cumulative_lengths(&targets);
224 if targets_have_nulls {
225 idx_arr
226 .iter()
227 .map(|i| target_get_unchecked(&targets, &cumlens, *i?))
228 .collect_arr_trusted_with_dtype(dtype)
229 } else {
230 idx_arr
231 .iter()
232 .map(|i| Some(target_value_unchecked(&targets, &cumlens, *i?)))
233 .collect_arr_trusted_with_dtype(dtype)
234 }
235 }
236 });
237
238 let mut out = ChunkedArray::from_chunk_iter_like(ca, chunks);
239 let sorted_flag = gather_sorted_flag(ca, indices, &out);
240
241 out.set_sorted_flag(sorted_flag);
242 out
243 }
244}
245
246impl ChunkTakeUnchecked<IdxCa> for BinaryChunked {
247 unsafe fn take_unchecked(&self, indices: &IdxCa) -> Self {
249 let ca = self;
250 let targets_have_nulls = ca.null_count() > 0;
251 let targets: Vec<_> = ca.downcast_iter().collect();
252
253 let chunks = indices.downcast_iter().map(|idx_arr| {
254 let dtype = ca.dtype().to_arrow(CompatLevel::newest());
255 if targets.len() == 1 {
256 let target = targets.first().unwrap();
257 take_unchecked(&**target, idx_arr)
258 } else {
259 let cumlens = cumulative_lengths(&targets);
260 if targets_have_nulls {
261 let arr: BinaryViewArray = idx_arr
262 .iter()
263 .map(|i| target_get_unchecked(&targets, &cumlens, *i?))
264 .collect_arr_trusted_with_dtype(dtype);
265 arr.to_boxed()
266 } else {
267 let arr: BinaryViewArray = idx_arr
268 .iter()
269 .map(|i| Some(target_value_unchecked(&targets, &cumlens, *i?)))
270 .collect_arr_trusted_with_dtype(dtype);
271 arr.to_boxed()
272 }
273 }
274 });
275
276 let mut out = ChunkedArray::from_chunks(ca.name().clone(), chunks.collect());
277 let sorted_flag = gather_sorted_flag(ca, indices, &out);
278 out.set_sorted_flag(sorted_flag);
279 out
280 }
281}
282
283impl ChunkTakeUnchecked<IdxCa> for StringChunked {
284 unsafe fn take_unchecked(&self, indices: &IdxCa) -> Self {
285 let ca = self;
286 let targets_have_nulls = ca.null_count() > 0;
287 let targets: Vec<_> = ca.downcast_iter().collect();
288
289 let chunks = indices.downcast_iter().map(|idx_arr| {
290 let dtype = ca.dtype().to_arrow(CompatLevel::newest());
291 if targets.len() == 1 {
292 let target = targets.first().unwrap();
293 take_unchecked(&**target, idx_arr)
294 } else {
295 let cumlens = cumulative_lengths(&targets);
296 if targets_have_nulls {
297 let arr: Utf8ViewArray = idx_arr
298 .iter()
299 .map(|i| target_get_unchecked(&targets, &cumlens, *i?))
300 .collect_arr_trusted_with_dtype(dtype);
301 arr.to_boxed()
302 } else {
303 let arr: Utf8ViewArray = idx_arr
304 .iter()
305 .map(|i| Some(target_value_unchecked(&targets, &cumlens, *i?)))
306 .collect_arr_trusted_with_dtype(dtype);
307 arr.to_boxed()
308 }
309 }
310 });
311
312 let mut out = ChunkedArray::from_chunks(ca.name().clone(), chunks.collect());
313 let sorted_flag = gather_sorted_flag(ca, indices, &out);
314 out.set_sorted_flag(sorted_flag);
315 out
316 }
317}
318
319impl<I: AsRef<[IdxSize]> + ?Sized> ChunkTakeUnchecked<I> for BinaryChunked {
320 unsafe fn take_unchecked(&self, indices: &I) -> Self {
322 let indices = IdxCa::mmap_slice(PlSmallStr::EMPTY, indices.as_ref());
323 self.take_unchecked(&indices)
324 }
325}
326
327impl<I: AsRef<[IdxSize]> + ?Sized> ChunkTakeUnchecked<I> for StringChunked {
328 unsafe fn take_unchecked(&self, indices: &I) -> Self {
330 let indices = IdxCa::mmap_slice(PlSmallStr::EMPTY, indices.as_ref());
331 self.take_unchecked(&indices)
332 }
333}
334
335#[cfg(feature = "dtype-struct")]
336impl ChunkTakeUnchecked<IdxCa> for StructChunked {
337 unsafe fn take_unchecked(&self, indices: &IdxCa) -> Self {
338 let a = self.rechunk();
339 let index = indices.rechunk();
340
341 let chunks = a
342 .downcast_iter()
343 .zip(index.downcast_iter())
344 .map(|(arr, idx)| take_unchecked(arr, idx))
345 .collect::<Vec<_>>();
346 self.copy_with_chunks(chunks)
347 }
348}
349
350#[cfg(feature = "dtype-struct")]
351impl<I: AsRef<[IdxSize]> + ?Sized> ChunkTakeUnchecked<I> for StructChunked {
352 unsafe fn take_unchecked(&self, indices: &I) -> Self {
353 let idx = IdxCa::mmap_slice(PlSmallStr::EMPTY, indices.as_ref());
354 self.take_unchecked(&idx)
355 }
356}
357
358impl IdxCa {
359 pub fn with_nullable_idx<T, F: FnOnce(&IdxCa) -> T>(idx: &[NullableIdxSize], f: F) -> T {
360 let validity: Bitmap = idx.iter().map(|idx| !idx.is_null_idx()).collect_trusted();
361 let idx = bytemuck::cast_slice::<_, IdxSize>(idx);
362 let arr = unsafe { polars_arrow::ffi::mmap::slice(idx) };
363 let arr = arr.with_validity_typed(Some(validity));
364 let ca = IdxCa::with_chunk(PlSmallStr::EMPTY, arr);
365
366 f(&ca)
367 }
368}
369
370#[cfg(feature = "dtype-array")]
371impl ChunkTakeUnchecked<IdxCa> for ArrayChunked {
372 unsafe fn take_unchecked(&self, indices: &IdxCa) -> Self {
373 if self.n_chunks() > 1 && should_rechunk(self.len(), indices.len()) {
376 let chunks = vec![take_unchecked(
377 self.rechunk().downcast_as_array(),
378 indices.rechunk().downcast_as_array(),
379 )];
380 return self.copy_with_chunks(chunks);
381 }
382 let ca = self;
383 let targets_have_nulls = ca.null_count() > 0;
384 let targets: Vec<_> = ca.downcast_iter().collect();
385
386 let chunks = indices.downcast_iter().map(|idx_arr| {
387 let dtype = ca.dtype().to_arrow(CompatLevel::newest());
388 if targets.len() == 1 {
389 let target = targets.first().unwrap();
390 take_unchecked(&**target, idx_arr)
391 } else {
392 let cumlens = cumulative_lengths(&targets);
393 if targets_have_nulls {
394 let arr: FixedSizeListArray = idx_arr
395 .iter()
396 .map(|i| target_get_unchecked(&targets, &cumlens, *i?))
397 .collect_arr_trusted_with_dtype(dtype);
398 arr.to_boxed()
399 } else {
400 let arr: FixedSizeListArray = idx_arr
401 .iter()
402 .map(|i| Some(target_value_unchecked(&targets, &cumlens, *i?)))
403 .collect_arr_trusted_with_dtype(dtype);
404 arr.to_boxed()
405 }
406 }
407 });
408
409 let mut out = ca.with_chunks(chunks.collect());
410 let sorted_flag = gather_sorted_flag(ca, indices, &out);
411 out.set_sorted_flag(sorted_flag);
412 out
413 }
414}
415
416#[cfg(feature = "dtype-array")]
417impl<I: AsRef<[IdxSize]> + ?Sized> ChunkTakeUnchecked<I> for ArrayChunked {
418 unsafe fn take_unchecked(&self, indices: &I) -> Self {
419 let idx = IdxCa::mmap_slice(PlSmallStr::EMPTY, indices.as_ref());
420 self.take_unchecked(&idx)
421 }
422}
423
424impl ChunkTakeUnchecked<IdxCa> for ListChunked {
425 unsafe fn take_unchecked(&self, indices: &IdxCa) -> Self {
426 if self.n_chunks() > 1 && should_rechunk(self.len(), indices.len()) {
429 let chunks = vec![take_unchecked(
430 self.rechunk().downcast_as_array(),
431 indices.rechunk().downcast_as_array(),
432 )];
433 return self.copy_with_chunks(chunks);
434 }
435 let ca = self;
436 let targets_have_nulls = ca.null_count() > 0;
437 let targets: Vec<_> = ca.downcast_iter().collect();
438
439 let chunks = indices.downcast_iter().map(|idx_arr| {
440 let dtype = ca.dtype().to_arrow(CompatLevel::newest());
441 if targets.len() == 1 {
442 let target = targets.first().unwrap();
443 take_unchecked(&**target, idx_arr)
444 } else {
445 let cumlens = cumulative_lengths(&targets);
446 if targets_have_nulls {
447 let arr: ListArray<i64> = idx_arr
448 .iter()
449 .map(|i| target_get_unchecked(&targets, &cumlens, *i?))
450 .collect_arr_trusted_with_dtype(dtype);
451 arr.to_boxed()
452 } else {
453 let arr: ListArray<i64> = idx_arr
454 .iter()
455 .map(|i| Some(target_value_unchecked(&targets, &cumlens, *i?)))
456 .collect_arr_trusted_with_dtype(dtype);
457 arr.to_boxed()
458 }
459 }
460 });
461
462 let mut out = ca.with_chunks(chunks.collect());
463 let sorted_flag = gather_sorted_flag(ca, indices, &out);
464 out.set_sorted_flag(sorted_flag);
465 out
466 }
467}
468
469impl<I: AsRef<[IdxSize]> + ?Sized> ChunkTakeUnchecked<I> for ListChunked {
470 unsafe fn take_unchecked(&self, indices: &I) -> Self {
471 let idx = IdxCa::mmap_slice(PlSmallStr::EMPTY, indices.as_ref());
472 self.take_unchecked(&idx)
473 }
474}
475
476fn should_rechunk(n_values: usize, n_indices: usize) -> bool {
477 n_indices > 0 && { (n_values / n_indices) > gather_ratio() }
478}
479
480fn gather_ratio() -> usize {
481 return *GATHER_RECHUNK_RATIO.get_or_init(|| {
482 const NAME: &str = "POLARS_GATHER_RECHUNK_RATIO";
483 std::env::var(NAME)
484 .map(|x| {
485 x.parse::<usize>()
486 .unwrap_or_else(|_| panic!("invalid value for {NAME}: {x}"))
487 })
488 .unwrap_or(const { 64 })
489 });
490
491 static GATHER_RECHUNK_RATIO: OnceLock<usize> = OnceLock::new();
492}