Skip to main content

mlx_rs/ops/indexing/
mod.rs

1//! Indexing Arrays
2//!
3//! # Overview
4//!
5//! Due to limitations in the `std::ops::Index` and `std::ops::IndexMut` traits (only references can
6//! be returned), the indexing is achieved with the [`IndexOp`] and [`IndexMutOp`] traits where
7//! arrays can be indexed with [`IndexOp::index()`] and [`IndexMutOp::index_mut()`] respectively.
8//!
9//! The following types can be used as indices:
10//!
11//! | Type | Description |
12//! |------|-------------|
13//! | [`i32`] | An integer index |
14//! | [`Array`] | Use an array to index another array |
15//! | `&Array` | Use a reference to an array to index another array |
16//! | [`std::ops::Range<i32>`] | A range index |
17//! | [`std::ops::RangeFrom<i32>`] | A range index |
18//! | [`std::ops::RangeFull`] | A range index |
19//! | [`std::ops::RangeInclusive<i32>`] | A range index |
20//! | [`std::ops::RangeTo<i32>`] | A range index |
21//! | [`std::ops::RangeToInclusive<i32>`] | A range index |
22//! | [`StrideBy`] | A range index with stride |
23//! | [`NewAxis`] | Add a new axis |
24//! | [`Ellipsis`] | Consume all axes |
25//!
26//! # Single axis indexing
27//!
28//! | Indexing Operation | `mlx` (python) | `mlx-swift` | `mlx-rs` |
29//! |--------------------|--------|-------|------|
30//! | integer | `arr[1]` | `arr[1]` | `arr.index(1)` |
31//! | range expression | `arr[1:3]` | `arr[1..<3]` | `arr.index(1..3)` |
32//! | full range | `arr[:]` | `arr[0...]` | `arr.index(..)` |
33//! | range with stride | `arr[::2]` | `arr[.stride(by: 2)]` | `arr.index((..).stride_by(2))` |
34//! | ellipsis (consuming all axes) | `arr[...]` | `arr[.ellipsis]` | `arr.index(Ellipsis)` |
35//! | newaxis | `arr[None]` | `arr[.newAxis]` | `arr.index(NewAxis)` |
36//! | mlx array `i` | `arr[i]` | `arr[i]` | `arr.index(i)` |
37//!
38//! # Multi-axes indexing
39//!
40//! Multi-axes indexing with combinations of the above operations is also supported by combining the
41//! operations in a tuple with the restriction that `Ellipsis` can only be used once.
42//!
43//! ## Examples
44//!
45//! ```rust
46//! // See the multi-dimensional example code for mlx python https://ml-explore.github.io/mlx/build/html/usage/indexing.html
47//!
48//! use mlx_rs::{Array, ops::indexing::*};
49//!
50//! let a = Array::from_iter(0..8, &[2, 2, 2]);
51//!
52//! // a[:, :, 0]
53//! let mut s1 = a.index((.., .., 0));
54//!
55//! let expected = Array::from_slice(&[0, 2, 4, 6], &[2, 2]);
56//! assert!(s1.eq_exact(&expected).unwrap());
57//!
58//! // a[..., 0]
59//! let mut s2 = a.index((Ellipsis, 0));
60//!
61//! let expected = Array::from_slice(&[0, 2, 4, 6], &[2, 2]);
62//! assert!(s1.eq_exact(&expected).unwrap());
63//! ```
64//!
65//! # Set values with indexing
66//!
67//! The same indexing operations (single or multiple) can be used to set values in an array using
68//! the [`IndexMutOp`] trait.
69//!
70//! ## Example
71//!
72//! ```rust
73//! use mlx_rs::{Array, ops::indexing::*};
74//!
75//! let mut a = Array::from_slice(&[1, 2, 3], &[3]);
76//! a.index_mut(2, Array::from_int(0));
77//!
78//! let expected = Array::from_slice(&[1, 2, 0], &[3]);
79//! assert!(a.eq_exact(&expected).unwrap());
80//! ```
81//!
82//! ```rust
83//! use mlx_rs::{Array, ops::indexing::*};
84//!
85//! let mut a = Array::from_iter(0i32..20, &[2, 2, 5]);
86//!
87//! // writing using slices -- this ends up covering two elements
88//! a.index_mut((0..1, 1..2, 2..4), Array::from_int(88));
89//!
90//! let expected = Array::from_slice(
91//!     &[
92//!         0, 1, 2, 3, 4, 5, 6, 88, 88, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
93//!     ],
94//!     &[2, 2, 5],
95//! );
96//! assert!(a.eq_exact(&expected).unwrap());
97//! ```
98
99use std::{borrow::Cow, ops::Bound, rc::Rc};
100
101use mlx_internal_macros::generate_macro;
102
103use crate::{
104    error::{Exception, Result},
105    utils::guard::Guarded,
106    Array, Stream, StreamOrDevice,
107};
108
109pub(crate) mod index_impl;
110pub(crate) mod indexmut_impl;
111mod indexupdate_impl;
112
113/* -------------------------------------------------------------------------- */
114/*                                Custom types                                */
115/* -------------------------------------------------------------------------- */
116
117/// New axis indexing operation.
118///
119/// See the module level documentation for more information.
120#[derive(Debug, Clone, Copy)]
121pub struct NewAxis;
122
123/// Ellipsis indexing operation.
124///
125/// See the module level documentation for more information.
126#[derive(Debug, Clone, Copy)]
127pub struct Ellipsis;
128
129/// How an indexed update combines with selected source elements.
130#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
131pub enum UpdateMode {
132    /// Replace selected elements.
133    #[default]
134    Replace,
135    /// Add updates to selected elements.
136    Add,
137    /// Take the minimum of updates and selected elements.
138    Min,
139    /// Take the maximum of updates and selected elements.
140    Max,
141    /// Multiply updates with selected elements.
142    Product,
143}
144
145/// Error returned by a functional indexed update.
146#[derive(Debug, thiserror::Error)]
147pub enum IndexUpdateError {
148    /// A slice stride was zero.
149    #[error("index update stride for axis {axis} must not be zero")]
150    ZeroStride {
151        /// The source axis whose slice used a zero stride.
152        axis: usize,
153    },
154
155    /// The upstream runtime rejected the operation.
156    #[error(transparent)]
157    Exception(#[from] Exception),
158}
159
160/// Stride indexing operation.
161///
162/// See the module level documentation for more information.
163#[derive(Debug, Clone, Copy)]
164pub struct StrideBy<I> {
165    /// The inner iterator
166    pub inner: I,
167
168    /// The stride
169    pub stride: i32,
170}
171
172/// Helper trait for creating a stride indexing operation.
173pub trait IntoStrideBy: Sized {
174    /// Create a stride indexing operation.
175    fn stride_by(self, stride: i32) -> StrideBy<Self>;
176}
177
178impl<T> IntoStrideBy for T {
179    fn stride_by(self, stride: i32) -> StrideBy<Self> {
180        StrideBy {
181            inner: self,
182            stride,
183        }
184    }
185}
186
187/// Range indexing operation.
188#[derive(Debug, Clone)]
189pub struct RangeIndex {
190    start: Bound<i32>,
191    stop: Bound<i32>,
192    stride: i32,
193}
194
195impl RangeIndex {
196    pub(crate) fn new(start: Bound<i32>, stop: Bound<i32>, stride: Option<i32>) -> Self {
197        let stride = stride.unwrap_or(1);
198        Self {
199            start,
200            stop,
201            stride,
202        }
203    }
204
205    pub(crate) fn is_full(&self) -> bool {
206        matches!(self.start, Bound::Unbounded)
207            && matches!(self.stop, Bound::Unbounded)
208            && self.stride == 1
209    }
210
211    pub(crate) fn stride(&self) -> i32 {
212        self.stride
213    }
214
215    pub(crate) fn start(&self, size: i32) -> i32 {
216        match self.start {
217            Bound::Included(start) => start,
218            Bound::Excluded(start) => start + 1,
219            Bound::Unbounded => {
220                // ref swift binding
221                // _start ?? (stride < 0 ? size - 1 : 0)
222
223                if self.stride.is_negative() {
224                    size - 1
225                } else {
226                    0
227                }
228            }
229        }
230    }
231
232    pub(crate) fn absolute_start(&self, size: i32) -> i32 {
233        // ref swift binding
234        // return start < 0 ? start + size : start
235
236        let start = self.start(size);
237        if start.is_negative() {
238            start + size
239        } else {
240            start
241        }
242    }
243
244    pub(crate) fn end(&self, size: i32) -> i32 {
245        match self.stop {
246            Bound::Included(stop) => stop + 1,
247            Bound::Excluded(stop) => stop,
248            Bound::Unbounded => {
249                // ref swift binding
250                // _end ?? (stride < 0 ? -size - 1 : size)
251
252                if self.stride.is_negative() {
253                    -size - 1
254                } else {
255                    size
256                }
257            }
258        }
259    }
260
261    pub(crate) fn absolute_end(&self, size: i32) -> i32 {
262        // ref swift binding
263        // return end < 0 ? end + size : end
264
265        let end = self.end(size);
266        if end.is_negative() {
267            end + size
268        } else {
269            end
270        }
271    }
272}
273
274/// Indexing operation for arrays.
275#[derive(Debug, Clone)]
276pub enum ArrayIndexOp<'a> {
277    /// An `Ellipsis` is used to consume all axes
278    ///
279    /// This is equivalent to `...` in python
280    Ellipsis,
281
282    /// A single index operation
283    ///
284    /// This is equivalent to `arr[1]` in python
285    TakeIndex {
286        /// The index to take
287        index: i32,
288    },
289
290    /// Indexing with an array
291    TakeArray {
292        /// The indices to take
293        indices: Rc<Array>, // TODO: remove `Rc` because `Array` is `Clone`
294    },
295
296    /// Indexing with an array reference
297    TakeArrayRef {
298        /// The indices to take
299        indices: &'a Array,
300    },
301
302    /// Indexing with a range
303    ///
304    /// This is equivalent to `arr[1:3]` in python
305    Slice(RangeIndex),
306
307    /// New axis operation
308    ///
309    /// This is equivalent to `arr[None]` in python
310    ExpandDims,
311}
312
313impl ArrayIndexOp<'_> {
314    fn is_array_or_index(&self) -> bool {
315        // Using the full match syntax to avoid forgetting to add new variants
316        match self {
317            ArrayIndexOp::TakeIndex { .. }
318            | ArrayIndexOp::TakeArrayRef { .. }
319            | ArrayIndexOp::TakeArray { .. } => true,
320            ArrayIndexOp::Ellipsis | ArrayIndexOp::Slice(_) | ArrayIndexOp::ExpandDims => false,
321        }
322    }
323    fn is_array(&self) -> bool {
324        // Using the full match syntax to avoid forgetting to add new variants
325        match self {
326            ArrayIndexOp::TakeArray { .. } | ArrayIndexOp::TakeArrayRef { .. } => true,
327            ArrayIndexOp::TakeIndex { .. }
328            | ArrayIndexOp::Ellipsis
329            | ArrayIndexOp::Slice(_)
330            | ArrayIndexOp::ExpandDims => false,
331        }
332    }
333}
334
335/* -------------------------------------------------------------------------- */
336/*                                Custom traits                               */
337/* -------------------------------------------------------------------------- */
338
339/// Trait for custom indexing operations.
340///
341/// Out of bounds indexing is allowed and wouldn't return an error.
342pub trait TryIndexOp<Idx> {
343    /// Try to index the array with the given index.
344    fn try_index_device(&self, i: Idx, stream: impl AsRef<Stream>) -> Result<Array>;
345
346    /// Try to index the array with the given index.
347    fn try_index(&self, i: Idx) -> Result<Array> {
348        self.try_index_device(i, StreamOrDevice::default())
349    }
350}
351
352/// Trait for custom indexing operations.
353///
354/// This is implemented for all types that implement `TryIndexOp`.
355pub trait IndexOp<Idx>: TryIndexOp<Idx> {
356    /// Index the array with the given index.
357    fn index_device(&self, i: Idx, stream: impl AsRef<Stream>) -> Array {
358        self.try_index_device(i, stream).unwrap()
359    }
360
361    /// Index the array with the given index.
362    fn index(&self, i: Idx) -> Array {
363        self.try_index(i).unwrap()
364    }
365}
366
367impl<T, Idx> IndexOp<Idx> for T where T: TryIndexOp<Idx> {}
368
369/// Trait for custom mutable indexing operations.
370pub trait TryIndexMutOp<Idx, Val> {
371    /// Try to index the array with the given index and set the value.
372    fn try_index_mut_device(&mut self, i: Idx, val: Val, stream: impl AsRef<Stream>) -> Result<()>;
373
374    /// Try to index the array with the given index and set the value.
375    fn try_index_mut(&mut self, i: Idx, val: Val) -> Result<()> {
376        self.try_index_mut_device(i, val, StreamOrDevice::default())
377    }
378}
379
380/// Trait for functional indexed updates.
381///
382/// The source is unchanged. Slice bounds clip to the source shape, negative
383/// strides are supported, and empty slices are no-ops. Updates broadcast to
384/// the selection and cast to the source dtype. Reduction modes combine
385/// duplicate advanced indices; replacement does not define which duplicate
386/// wins.
387pub trait TryIndexUpdateOp<Idx, Value> {
388    /// Return a new array with `update` applied at `index` according to `mode`.
389    fn try_index_update(
390        &self,
391        index: Idx,
392        update: Value,
393        mode: UpdateMode,
394    ) -> std::result::Result<Array, IndexUpdateError>;
395}
396// TODO: should `Val` impl `AsRef<Array>` or `Into<Array>`?
397
398/// Trait for custom mutable indexing operations.
399pub trait IndexMutOp<Idx, Val>: TryIndexMutOp<Idx, Val> {
400    /// Index the array with the given index and set the value.
401    fn index_mut_device(&mut self, i: Idx, val: Val, stream: impl AsRef<Stream>) {
402        self.try_index_mut_device(i, val, stream).unwrap()
403    }
404
405    /// Index the array with the given index and set the value.
406    fn index_mut(&mut self, i: Idx, val: Val) {
407        self.try_index_mut(i, val).unwrap()
408    }
409}
410
411impl<T, Idx, Val> IndexMutOp<Idx, Val> for T where T: TryIndexMutOp<Idx, Val> {}
412
413/// Trait for custom indexing operations.
414pub trait ArrayIndex<'a> {
415    /// `mlx` allows out of bounds indexing.
416    fn index_op(self) -> ArrayIndexOp<'a>;
417}
418
419/* -------------------------------------------------------------------------- */
420/*                               Implementation                               */
421/* -------------------------------------------------------------------------- */
422
423// Implement public bindings
424impl Array {
425    /// Take elements along an axis.
426    ///
427    /// The elements are taken from `indices` along the specified axis. If the axis is not specified
428    /// the array is treated as a flattened 1-D array prior to performing the take.
429    ///
430    /// See [`Array::take_all`] for the flattened array.
431    ///
432    /// # Params
433    ///
434    /// - `indices`: The indices to take from the array.
435    /// - `axis`: The axis along which to take the elements.
436    pub fn take_axis(&self, indices: impl AsRef<Array>, axis: i32) -> Result<Array> {
437        let stream = Stream::thread_local_or_default();
438        Array::try_from_op(|res| unsafe {
439            mlx_sys::mlx_take_axis(
440                res,
441                self.as_ptr(),
442                indices.as_ref().as_ptr(),
443                axis,
444                stream.as_ref().as_ptr(),
445            )
446        })
447    }
448
449    /// Compatibility shim for [`take_axis`].
450    #[deprecated(
451        since = "0.26.0",
452        note = "use `with_stream` or `with_device` around `take_axis`"
453    )]
454    pub fn take_axis_device(
455        &self,
456        indices: impl AsRef<Array>,
457        axis: i32,
458        stream: impl AsRef<Stream>,
459    ) -> Result<Array> {
460        crate::with_stream(stream.as_ref(), || self.take_axis(indices, axis))
461    }
462
463    /// Take elements from flattened 1-D array.
464    ///
465    /// # Params
466    ///
467    /// - `indices`: The indices to take from the array.
468    pub fn take(&self, indices: impl AsRef<Array>) -> Result<Array> {
469        let stream = Stream::thread_local_or_default();
470        Array::try_from_op(|res| unsafe {
471            mlx_sys::mlx_take(
472                res,
473                self.as_ptr(),
474                indices.as_ref().as_ptr(),
475                stream.as_ref().as_ptr(),
476            )
477        })
478    }
479
480    /// Compatibility shim for [`take`].
481    #[deprecated(
482        since = "0.26.0",
483        note = "use `with_stream` or `with_device` around `take`"
484    )]
485    pub fn take_device(
486        &self,
487        indices: impl AsRef<Array>,
488        stream: impl AsRef<Stream>,
489    ) -> Result<Array> {
490        crate::with_stream(stream.as_ref(), || self.take(indices))
491    }
492
493    /// Take values along an axis at the specified indices.
494    ///
495    /// If no axis is specified, the array is flattened to 1D prior to the indexing operation.
496    ///
497    /// # Params
498    ///
499    /// - `indices`: The indices to take from the array.
500    /// - `axis`: Axis in the input to take the values from.
501    pub fn take_along_axis(
502        &self,
503        indices: impl AsRef<Array>,
504        axis: impl Into<Option<i32>>,
505    ) -> Result<Array> {
506        let stream = Stream::thread_local_or_default();
507        let (input, axis) = match axis.into() {
508            None => (Cow::Owned(self.reshape(&[-1])?), 0),
509            Some(ax) => (Cow::Borrowed(self), ax),
510        };
511
512        Array::try_from_op(|res| unsafe {
513            mlx_sys::mlx_take_along_axis(
514                res,
515                input.as_ptr(),
516                indices.as_ref().as_ptr(),
517                axis,
518                stream.as_ref().as_ptr(),
519            )
520        })
521    }
522
523    /// Compatibility shim for [`take_along_axis`].
524    #[deprecated(
525        since = "0.26.0",
526        note = "use `with_stream` or `with_device` around `take_along_axis`"
527    )]
528    pub fn take_along_axis_device(
529        &self,
530        indices: impl AsRef<Array>,
531        axis: impl Into<Option<i32>>,
532        stream: impl AsRef<Stream>,
533    ) -> Result<Array> {
534        crate::with_stream(stream.as_ref(), || self.take_along_axis(indices, axis))
535    }
536
537    /// Put values along an axis at the specified indices.
538    ///
539    /// If no axis is specified, the array is flattened to 1D prior to the indexing operation.
540    ///
541    /// # Params
542    /// - indices: Indices array. These should be broadcastable with the input array excluding the `axis` dimension.
543    /// - values: Values array. These should be broadcastable with the indices.
544    /// - axis: Axis in the destination to put the values to.
545    /// - stream: stream or device to evaluate on.
546    pub fn put_along_axis(
547        &self,
548        indices: impl AsRef<Array>,
549        values: impl AsRef<Array>,
550        axis: impl Into<Option<i32>>,
551    ) -> Result<Array> {
552        let stream = Stream::thread_local_or_default();
553        match axis.into() {
554            None => {
555                let input = self.reshape(&[-1])?;
556                let array = Array::try_from_op(|res| unsafe {
557                    mlx_sys::mlx_put_along_axis(
558                        res,
559                        input.as_ptr(),
560                        indices.as_ref().as_ptr(),
561                        values.as_ref().as_ptr(),
562                        0,
563                        stream.as_ref().as_ptr(),
564                    )
565                })?;
566                let array = array.reshape(self.shape())?;
567                Ok(array)
568            }
569            Some(ax) => Array::try_from_op(|res| unsafe {
570                mlx_sys::mlx_put_along_axis(
571                    res,
572                    self.as_ptr(),
573                    indices.as_ref().as_ptr(),
574                    values.as_ref().as_ptr(),
575                    ax,
576                    stream.as_ref().as_ptr(),
577                )
578            }),
579        }
580    }
581
582    /// Compatibility shim for [`put_along_axis`].
583    #[deprecated(
584        since = "0.26.0",
585        note = "use `with_stream` or `with_device` around `put_along_axis`"
586    )]
587    pub fn put_along_axis_device(
588        &self,
589        indices: impl AsRef<Array>,
590        values: impl AsRef<Array>,
591        axis: impl Into<Option<i32>>,
592        stream: impl AsRef<Stream>,
593    ) -> Result<Array> {
594        crate::with_stream(stream.as_ref(), || {
595            self.put_along_axis(indices, values, axis)
596        })
597    }
598}
599
600/// Indices of the maximum values along the axis.
601///
602/// See [`argmax_all`] for the flattened array.
603///
604/// # Params
605///
606/// - `a`: The input array.
607/// - `axis`: Axis to reduce over
608/// - `keep_dims`: Keep reduced axes as singleton dimensions, defaults to False.
609pub fn argmax_axis(
610    a: impl AsRef<Array>,
611    axis: i32,
612    keep_dims: impl Into<Option<bool>>,
613) -> Result<Array> {
614    let stream = Stream::thread_local_or_default();
615    let keep_dims = keep_dims.into().unwrap_or(false);
616
617    Array::try_from_op(|res| unsafe {
618        mlx_sys::mlx_argmax_axis(
619            res,
620            a.as_ref().as_ptr(),
621            axis,
622            keep_dims,
623            stream.as_ref().as_ptr(),
624        )
625    })
626}
627
628/// Compatibility shim for [`argmax_axis`].
629#[generate_macro(customize(forwarding_shim = true, root = "$crate::ops::indexing"))]
630#[deprecated(
631    since = "0.26.0",
632    note = "use `with_stream` or `with_device` around `argmax_axis`"
633)]
634pub fn argmax_axis_device(
635    a: impl AsRef<Array>,
636    axis: i32,
637    #[optional] keep_dims: impl Into<Option<bool>>,
638    #[optional] stream: impl AsRef<Stream>,
639) -> Result<Array> {
640    crate::with_stream(stream.as_ref(), || argmax_axis(a, axis, keep_dims))
641}
642
643/// Indices of the maximum value over the entire array.
644///
645/// # Params
646///
647/// - `a`: The input array.
648/// - `keep_dims`: Keep reduced axes as singleton dimensions, defaults to False.
649pub fn argmax(a: impl AsRef<Array>, keep_dims: impl Into<Option<bool>>) -> Result<Array> {
650    let stream = Stream::thread_local_or_default();
651    let keep_dims = keep_dims.into().unwrap_or(false);
652
653    Array::try_from_op(|res| unsafe {
654        mlx_sys::mlx_argmax(
655            res,
656            a.as_ref().as_ptr(),
657            keep_dims,
658            stream.as_ref().as_ptr(),
659        )
660    })
661}
662
663/// Compatibility shim for [`argmax`].
664#[generate_macro(customize(forwarding_shim = true, root = "$crate::ops::indexing"))]
665#[deprecated(
666    since = "0.26.0",
667    note = "use `with_stream` or `with_device` around `argmax`"
668)]
669pub fn argmax_device(
670    a: impl AsRef<Array>,
671    #[optional] keep_dims: impl Into<Option<bool>>,
672    #[optional] stream: impl AsRef<Stream>,
673) -> Result<Array> {
674    crate::with_stream(stream.as_ref(), || argmax(a, keep_dims))
675}
676
677/// Indices of the minimum values along the axis.
678///
679/// See [`argmin_all`] for the flattened array.
680///
681/// # Params
682///
683/// - `a`: The input array.
684/// - `axis`: Axis to reduce over.
685/// - `keep_dims`: Keep reduced axes as singleton dimensions, defaults to False.
686pub fn argmin_axis(
687    a: impl AsRef<Array>,
688    axis: i32,
689    keep_dims: impl Into<Option<bool>>,
690) -> Result<Array> {
691    let stream = Stream::thread_local_or_default();
692    let keep_dims = keep_dims.into().unwrap_or(false);
693
694    Array::try_from_op(|res| unsafe {
695        mlx_sys::mlx_argmin_axis(
696            res,
697            a.as_ref().as_ptr(),
698            axis,
699            keep_dims,
700            stream.as_ref().as_ptr(),
701        )
702    })
703}
704
705/// Compatibility shim for [`argmin_axis`].
706#[generate_macro(customize(forwarding_shim = true, root = "$crate::ops::indexing"))]
707#[deprecated(
708    since = "0.26.0",
709    note = "use `with_stream` or `with_device` around `argmin_axis`"
710)]
711pub fn argmin_axis_device(
712    a: impl AsRef<Array>,
713    axis: i32,
714    #[optional] keep_dims: impl Into<Option<bool>>,
715    #[optional] stream: impl AsRef<Stream>,
716) -> Result<Array> {
717    crate::with_stream(stream.as_ref(), || argmin_axis(a, axis, keep_dims))
718}
719
720/// Indices of the minimum value over the entire array.
721///
722/// # Params
723///
724/// - `a`: The input array.
725/// - `keep_dims`: Keep reduced axes as singleton dimensions, defaults to False.
726pub fn argmin(a: impl AsRef<Array>, keep_dims: impl Into<Option<bool>>) -> Result<Array> {
727    let stream = Stream::thread_local_or_default();
728    let keep_dims = keep_dims.into().unwrap_or(false);
729
730    Array::try_from_op(|res| unsafe {
731        mlx_sys::mlx_argmin(
732            res,
733            a.as_ref().as_ptr(),
734            keep_dims,
735            stream.as_ref().as_ptr(),
736        )
737    })
738}
739
740/// Compatibility shim for [`argmin`].
741#[generate_macro(customize(forwarding_shim = true, root = "$crate::ops::indexing"))]
742#[deprecated(
743    since = "0.26.0",
744    note = "use `with_stream` or `with_device` around `argmin`"
745)]
746pub fn argmin_device(
747    a: impl AsRef<Array>,
748    #[optional] keep_dims: impl Into<Option<bool>>,
749    #[optional] stream: impl AsRef<Stream>,
750) -> Result<Array> {
751    crate::with_stream(stream.as_ref(), || argmin(a, keep_dims))
752}
753
754/// See [`Array::take_along_axis`]
755pub fn take_along_axis(
756    a: impl AsRef<Array>,
757    indices: impl AsRef<Array>,
758    axis: impl Into<Option<i32>>,
759) -> Result<Array> {
760    a.as_ref().take_along_axis(indices, axis)
761}
762
763/// Compatibility shim for [`take_along_axis`].
764#[generate_macro(customize(forwarding_shim = true, root = "$crate::ops::indexing"))]
765#[deprecated(
766    since = "0.26.0",
767    note = "use `with_stream` or `with_device` around `take_along_axis`"
768)]
769pub fn take_along_axis_device(
770    a: impl AsRef<Array>,
771    indices: impl AsRef<Array>,
772    #[optional] axis: impl Into<Option<i32>>,
773    #[optional] stream: impl AsRef<Stream>,
774) -> Result<Array> {
775    crate::with_stream(stream.as_ref(), || take_along_axis(a, indices, axis))
776}
777
778/// See [`Array::put_along_axis`]
779pub fn put_along_axis(
780    a: impl AsRef<Array>,
781    indices: impl AsRef<Array>,
782    values: impl AsRef<Array>,
783    axis: impl Into<Option<i32>>,
784) -> Result<Array> {
785    a.as_ref().put_along_axis(indices, values, axis)
786}
787
788/// Compatibility shim for [`put_along_axis`].
789#[generate_macro(customize(forwarding_shim = true, root = "$crate::ops::indexing"))]
790#[deprecated(
791    since = "0.26.0",
792    note = "use `with_stream` or `with_device` around `put_along_axis`"
793)]
794pub fn put_along_axis_device(
795    a: impl AsRef<Array>,
796    indices: impl AsRef<Array>,
797    values: impl AsRef<Array>,
798    #[optional] axis: impl Into<Option<i32>>,
799    #[optional] stream: impl AsRef<Stream>,
800) -> Result<Array> {
801    crate::with_stream(stream.as_ref(), || put_along_axis(a, indices, values, axis))
802}
803
804/// See [`Array::take`]
805pub fn take_axis(a: impl AsRef<Array>, indices: impl AsRef<Array>, axis: i32) -> Result<Array> {
806    a.as_ref().take_axis(indices, axis)
807}
808
809/// Compatibility shim for [`take_axis`].
810#[generate_macro(customize(forwarding_shim = true, root = "$crate::ops::indexing"))]
811#[deprecated(
812    since = "0.26.0",
813    note = "use `with_stream` or `with_device` around `take_axis`"
814)]
815pub fn take_axis_device(
816    a: impl AsRef<Array>,
817    indices: impl AsRef<Array>,
818    axis: i32,
819    #[optional] stream: impl AsRef<Stream>,
820) -> Result<Array> {
821    crate::with_stream(stream.as_ref(), || take_axis(a, indices, axis))
822}
823
824/// See [`Array::take_all`]
825pub fn take(a: impl AsRef<Array>, indices: impl AsRef<Array>) -> Result<Array> {
826    a.as_ref().take(indices)
827}
828
829/// Compatibility shim for [`take`].
830#[generate_macro(customize(forwarding_shim = true, root = "$crate::ops::indexing"))]
831#[deprecated(
832    since = "0.26.0",
833    note = "use `with_stream` or `with_device` around `take`"
834)]
835pub fn take_device(
836    a: impl AsRef<Array>,
837    indices: impl AsRef<Array>,
838    #[optional] stream: impl AsRef<Stream>,
839) -> Result<Array> {
840    crate::with_stream(stream.as_ref(), || take(a, indices))
841}
842
843/// Returns the `k` largest elements from the input along a given axis.
844///
845/// The elements will not necessarily be in sorted order.
846///
847/// See [`topk_all`] for the flattened array.
848///
849/// # Params
850///
851/// - `a`: The input array.
852/// - `k`: The number of elements to return.
853/// - `axis`: Axis to sort over. Default to `-1` if not specified.
854pub fn topk_axis(a: impl AsRef<Array>, k: i32, axis: i32) -> Result<Array> {
855    let stream = Stream::thread_local_or_default();
856    Array::try_from_op(|res| unsafe {
857        mlx_sys::mlx_topk_axis(res, a.as_ref().as_ptr(), k, axis, stream.as_ref().as_ptr())
858    })
859}
860
861/// Compatibility shim for [`topk_axis`].
862#[generate_macro(customize(forwarding_shim = true, root = "$crate::ops::indexing"))]
863#[deprecated(
864    since = "0.26.0",
865    note = "use `with_stream` or `with_device` around `topk_axis`"
866)]
867pub fn topk_axis_device(
868    a: impl AsRef<Array>,
869    k: i32,
870    axis: i32,
871    #[optional] stream: impl AsRef<Stream>,
872) -> Result<Array> {
873    crate::with_stream(stream.as_ref(), || topk_axis(a, k, axis))
874}
875
876/// Returns the `k` largest elements from the flattened input array.
877pub fn topk(a: impl AsRef<Array>, k: i32) -> Result<Array> {
878    let stream = Stream::thread_local_or_default();
879    Array::try_from_op(|res| unsafe {
880        mlx_sys::mlx_topk(res, a.as_ref().as_ptr(), k, stream.as_ref().as_ptr())
881    })
882}
883
884/// Compatibility shim for [`topk`].
885#[generate_macro(customize(forwarding_shim = true, root = "$crate::ops::indexing"))]
886#[deprecated(
887    since = "0.26.0",
888    note = "use `with_stream` or `with_device` around `topk`"
889)]
890pub fn topk_device(
891    a: impl AsRef<Array>,
892    k: i32,
893    #[optional] stream: impl AsRef<Stream>,
894) -> Result<Array> {
895    crate::with_stream(stream.as_ref(), || topk(a, k))
896}
897
898/// Scatter updates to the array at the given indices along a single axis.
899///
900/// # Params
901///
902/// - `a`: Input array
903/// - `indices`: Indices array specifying positions to scatter into
904/// - `updates`: Values to scatter
905/// - `axis`: The axis along which to scatter
906pub fn scatter_single(
907    a: impl AsRef<Array>,
908    indices: impl AsRef<Array>,
909    updates: impl AsRef<Array>,
910    axis: i32,
911) -> Result<Array> {
912    let stream = Stream::thread_local_or_default();
913    Array::try_from_op(|res| unsafe {
914        mlx_sys::mlx_scatter_single(
915            res,
916            a.as_ref().as_ptr(),
917            indices.as_ref().as_ptr(),
918            updates.as_ref().as_ptr(),
919            axis,
920            stream.as_ref().as_ptr(),
921        )
922    })
923}
924
925/// Compatibility shim for [`scatter_single`].
926#[generate_macro(customize(forwarding_shim = true, root = "$crate::ops::indexing"))]
927#[deprecated(
928    since = "0.26.0",
929    note = "use `with_stream` or `with_device` around `scatter_single`"
930)]
931pub fn scatter_single_device(
932    a: impl AsRef<Array>,
933    indices: impl AsRef<Array>,
934    updates: impl AsRef<Array>,
935    axis: i32,
936    #[optional] stream: impl AsRef<Stream>,
937) -> Result<Array> {
938    crate::with_stream(stream.as_ref(), || {
939        scatter_single(a, indices, updates, axis)
940    })
941}
942
943/// Scatter-add updates to the array at the given indices along a single axis.
944///
945/// Adds the update values to the existing values at the specified indices.
946///
947/// # Params
948///
949/// - `a`: Input array
950/// - `indices`: Indices array specifying positions to scatter into
951/// - `updates`: Values to add
952/// - `axis`: The axis along which to scatter
953pub fn scatter_add_single(
954    a: impl AsRef<Array>,
955    indices: impl AsRef<Array>,
956    updates: impl AsRef<Array>,
957    axis: i32,
958) -> Result<Array> {
959    let stream = Stream::thread_local_or_default();
960    Array::try_from_op(|res| unsafe {
961        mlx_sys::mlx_scatter_add_single(
962            res,
963            a.as_ref().as_ptr(),
964            indices.as_ref().as_ptr(),
965            updates.as_ref().as_ptr(),
966            axis,
967            stream.as_ref().as_ptr(),
968        )
969    })
970}
971
972/// Compatibility shim for [`scatter_add_single`].
973#[generate_macro(customize(forwarding_shim = true, root = "$crate::ops::indexing"))]
974#[deprecated(
975    since = "0.26.0",
976    note = "use `with_stream` or `with_device` around `scatter_add_single`"
977)]
978pub fn scatter_add_single_device(
979    a: impl AsRef<Array>,
980    indices: impl AsRef<Array>,
981    updates: impl AsRef<Array>,
982    axis: i32,
983    #[optional] stream: impl AsRef<Stream>,
984) -> Result<Array> {
985    crate::with_stream(stream.as_ref(), || {
986        scatter_add_single(a, indices, updates, axis)
987    })
988}
989
990/// Scatter-max updates to the array at the given indices along a single axis.
991///
992/// Takes the maximum of the update values and existing values at the specified indices.
993///
994/// # Params
995///
996/// - `a`: Input array
997/// - `indices`: Indices array specifying positions to scatter into
998/// - `updates`: Values to compare
999/// - `axis`: The axis along which to scatter
1000pub fn scatter_max_single(
1001    a: impl AsRef<Array>,
1002    indices: impl AsRef<Array>,
1003    updates: impl AsRef<Array>,
1004    axis: i32,
1005) -> Result<Array> {
1006    let stream = Stream::thread_local_or_default();
1007    Array::try_from_op(|res| unsafe {
1008        mlx_sys::mlx_scatter_max_single(
1009            res,
1010            a.as_ref().as_ptr(),
1011            indices.as_ref().as_ptr(),
1012            updates.as_ref().as_ptr(),
1013            axis,
1014            stream.as_ref().as_ptr(),
1015        )
1016    })
1017}
1018
1019/// Compatibility shim for [`scatter_max_single`].
1020#[generate_macro(customize(forwarding_shim = true, root = "$crate::ops::indexing"))]
1021#[deprecated(
1022    since = "0.26.0",
1023    note = "use `with_stream` or `with_device` around `scatter_max_single`"
1024)]
1025pub fn scatter_max_single_device(
1026    a: impl AsRef<Array>,
1027    indices: impl AsRef<Array>,
1028    updates: impl AsRef<Array>,
1029    axis: i32,
1030    #[optional] stream: impl AsRef<Stream>,
1031) -> Result<Array> {
1032    crate::with_stream(stream.as_ref(), || {
1033        scatter_max_single(a, indices, updates, axis)
1034    })
1035}
1036
1037/// Scatter-min updates to the array at the given indices along a single axis.
1038///
1039/// Takes the minimum of the update values and existing values at the specified indices.
1040///
1041/// # Params
1042///
1043/// - `a`: Input array
1044/// - `indices`: Indices array specifying positions to scatter into
1045/// - `updates`: Values to compare
1046/// - `axis`: The axis along which to scatter
1047pub fn scatter_min_single(
1048    a: impl AsRef<Array>,
1049    indices: impl AsRef<Array>,
1050    updates: impl AsRef<Array>,
1051    axis: i32,
1052) -> Result<Array> {
1053    let stream = Stream::thread_local_or_default();
1054    Array::try_from_op(|res| unsafe {
1055        mlx_sys::mlx_scatter_min_single(
1056            res,
1057            a.as_ref().as_ptr(),
1058            indices.as_ref().as_ptr(),
1059            updates.as_ref().as_ptr(),
1060            axis,
1061            stream.as_ref().as_ptr(),
1062        )
1063    })
1064}
1065
1066/// Compatibility shim for [`scatter_min_single`].
1067#[generate_macro(customize(forwarding_shim = true, root = "$crate::ops::indexing"))]
1068#[deprecated(
1069    since = "0.26.0",
1070    note = "use `with_stream` or `with_device` around `scatter_min_single`"
1071)]
1072pub fn scatter_min_single_device(
1073    a: impl AsRef<Array>,
1074    indices: impl AsRef<Array>,
1075    updates: impl AsRef<Array>,
1076    axis: i32,
1077    #[optional] stream: impl AsRef<Stream>,
1078) -> Result<Array> {
1079    crate::with_stream(stream.as_ref(), || {
1080        scatter_min_single(a, indices, updates, axis)
1081    })
1082}
1083
1084/// Scatter-prod updates to the array at the given indices along a single axis.
1085///
1086/// Multiplies the update values with existing values at the specified indices.
1087///
1088/// # Params
1089///
1090/// - `a`: Input array
1091/// - `indices`: Indices array specifying positions to scatter into
1092/// - `updates`: Values to multiply
1093/// - `axis`: The axis along which to scatter
1094pub fn scatter_prod_single(
1095    a: impl AsRef<Array>,
1096    indices: impl AsRef<Array>,
1097    updates: impl AsRef<Array>,
1098    axis: i32,
1099) -> Result<Array> {
1100    let stream = Stream::thread_local_or_default();
1101    Array::try_from_op(|res| unsafe {
1102        mlx_sys::mlx_scatter_prod_single(
1103            res,
1104            a.as_ref().as_ptr(),
1105            indices.as_ref().as_ptr(),
1106            updates.as_ref().as_ptr(),
1107            axis,
1108            stream.as_ref().as_ptr(),
1109        )
1110    })
1111}
1112
1113/// Compatibility shim for [`scatter_prod_single`].
1114#[generate_macro(customize(forwarding_shim = true, root = "$crate::ops::indexing"))]
1115#[deprecated(
1116    since = "0.26.0",
1117    note = "use `with_stream` or `with_device` around `scatter_prod_single`"
1118)]
1119pub fn scatter_prod_single_device(
1120    a: impl AsRef<Array>,
1121    indices: impl AsRef<Array>,
1122    updates: impl AsRef<Array>,
1123    axis: i32,
1124    #[optional] stream: impl AsRef<Stream>,
1125) -> Result<Array> {
1126    crate::with_stream(stream.as_ref(), || {
1127        scatter_prod_single(a, indices, updates, axis)
1128    })
1129}
1130
1131/// Gather elements from the array at the given indices along a single axis.
1132///
1133/// # Params
1134///
1135/// - `a`: Input array
1136/// - `indices`: Indices array specifying positions to gather from
1137/// - `axis`: The axis along which to gather
1138/// - `slice_sizes`: The sizes of the slices to gather
1139pub fn gather_single(
1140    a: impl AsRef<Array>,
1141    indices: impl AsRef<Array>,
1142    axis: i32,
1143    slice_sizes: &[i32],
1144) -> Result<Array> {
1145    let stream = Stream::thread_local_or_default();
1146    Array::try_from_op(|res| unsafe {
1147        mlx_sys::mlx_gather_single(
1148            res,
1149            a.as_ref().as_ptr(),
1150            indices.as_ref().as_ptr(),
1151            axis,
1152            slice_sizes.as_ptr(),
1153            slice_sizes.len(),
1154            stream.as_ref().as_ptr(),
1155        )
1156    })
1157}
1158
1159/// Compatibility shim for [`gather_single`].
1160#[generate_macro(customize(forwarding_shim = true, root = "$crate::ops::indexing"))]
1161#[deprecated(
1162    since = "0.26.0",
1163    note = "use `with_stream` or `with_device` around `gather_single`"
1164)]
1165pub fn gather_single_device(
1166    a: impl AsRef<Array>,
1167    indices: impl AsRef<Array>,
1168    axis: i32,
1169    slice_sizes: &[i32],
1170    #[optional] stream: impl AsRef<Stream>,
1171) -> Result<Array> {
1172    crate::with_stream(stream.as_ref(), || {
1173        gather_single(a, indices, axis, slice_sizes)
1174    })
1175}
1176
1177/// Scatter values into an array at locations where mask is true.
1178///
1179/// # Params
1180///
1181/// - `a`: Input array
1182/// - `mask`: Boolean mask array indicating where to scatter
1183/// - `src`: Source values to scatter
1184pub fn masked_scatter(
1185    a: impl AsRef<Array>,
1186    mask: impl AsRef<Array>,
1187    src: impl AsRef<Array>,
1188) -> Result<Array> {
1189    let stream = Stream::thread_local_or_default();
1190    Array::try_from_op(|res| unsafe {
1191        mlx_sys::mlx_masked_scatter(
1192            res,
1193            a.as_ref().as_ptr(),
1194            mask.as_ref().as_ptr(),
1195            src.as_ref().as_ptr(),
1196            stream.as_ref().as_ptr(),
1197        )
1198    })
1199}
1200
1201/// Compatibility shim for [`masked_scatter`].
1202#[generate_macro(customize(forwarding_shim = true, root = "$crate::ops::indexing"))]
1203#[deprecated(
1204    since = "0.26.0",
1205    note = "use `with_stream` or `with_device` around `masked_scatter`"
1206)]
1207pub fn masked_scatter_device(
1208    a: impl AsRef<Array>,
1209    mask: impl AsRef<Array>,
1210    src: impl AsRef<Array>,
1211    #[optional] stream: impl AsRef<Stream>,
1212) -> Result<Array> {
1213    crate::with_stream(stream.as_ref(), || masked_scatter(a, mask, src))
1214}
1215
1216/* -------------------------------------------------------------------------- */
1217/*                              Helper functions                              */
1218/* -------------------------------------------------------------------------- */
1219fn count_non_new_axis_operations(operations: &[ArrayIndexOp]) -> usize {
1220    operations
1221        .iter()
1222        .filter(|op| !matches!(op, ArrayIndexOp::ExpandDims))
1223        .count()
1224}
1225fn expand_ellipsis_operations<'a>(
1226    ndim: usize,
1227    operations: &'a [ArrayIndexOp<'a>],
1228) -> Cow<'a, [ArrayIndexOp<'a>]> {
1229    let ellipsis_count = operations
1230        .iter()
1231        .filter(|op| matches!(op, ArrayIndexOp::Ellipsis))
1232        .count();
1233    if ellipsis_count == 0 {
1234        return Cow::Borrowed(operations);
1235    }
1236
1237    if ellipsis_count > 1 {
1238        panic!("Indexing with multiple ellipsis is not supported");
1239    }
1240
1241    let ellipsis_pos = operations
1242        .iter()
1243        .position(|op| matches!(op, ArrayIndexOp::Ellipsis))
1244        .unwrap();
1245    let prefix = &operations[..ellipsis_pos];
1246    let suffix = &operations[(ellipsis_pos + 1)..];
1247    let expand_range =
1248        count_non_new_axis_operations(prefix)..(ndim - count_non_new_axis_operations(suffix));
1249    let expand = expand_range.map(|_| (..).index_op());
1250
1251    let mut expanded = Vec::with_capacity(ndim);
1252    expanded.extend_from_slice(prefix);
1253    expanded.extend(expand);
1254    expanded.extend_from_slice(suffix);
1255
1256    Cow::Owned(expanded)
1257}
1258
1259#[cfg(test)]
1260mod tests {
1261    use super::*;
1262    use crate::{array, ops::reshape, Array};
1263
1264    // Tests adapted from C++ `ops_tests.cpp/test scatter`
1265    #[test]
1266    fn test_scatter_single() {
1267        // Single element scatter
1268        let input = Array::zeros::<f32>(&[4]).unwrap();
1269        let indices = Array::from_slice(&[0u32, 1], &[2]);
1270        let updates = Array::ones::<f32>(&[2, 1]).unwrap();
1271        let out = scatter_single(&input, &indices, &updates, 0).unwrap();
1272        let expected = array!([1.0f32, 1.0, 0.0, 0.0]);
1273        assert!(out.all_close(&expected, 1e-5, 1e-5, None).unwrap());
1274    }
1275
1276    #[test]
1277    fn test_scatter_add_single() {
1278        // Single element scatter add
1279        let input = Array::ones::<f32>(&[4]).unwrap();
1280        let indices = Array::from_slice(&[0u32, 0, 3], &[3]);
1281        let updates = Array::ones::<f32>(&[3, 1]).unwrap();
1282        let out = scatter_add_single(&input, &indices, &updates, 0).unwrap();
1283        let expected = array!([3.0f32, 1.0, 1.0, 2.0]);
1284        assert!(out.all_close(&expected, 1e-5, 1e-5, None).unwrap());
1285    }
1286
1287    #[test]
1288    fn test_scatter_max_single() {
1289        // Single element scatter max
1290        let input = Array::ones::<f32>(&[4]).unwrap();
1291        let indices = Array::from_slice(&[0u32, 0, 3], &[3]);
1292        let updates = reshape(array!([1.0f32, 6.0, -2.0]), &[3, 1]).unwrap();
1293        let out = scatter_max_single(&input, &indices, &updates, 0).unwrap();
1294        let expected = array!([6.0f32, 1.0, 1.0, 1.0]);
1295        assert!(out.all_close(&expected, 1e-5, 1e-5, None).unwrap());
1296    }
1297
1298    #[test]
1299    fn test_scatter_min_single() {
1300        // Single element scatter min
1301        let input = Array::ones::<f32>(&[4]).unwrap();
1302        let indices = Array::from_slice(&[0u32, 0, 3], &[3]);
1303        let updates = reshape(array!([1.0f32, -6.0, 2.0]), &[3, 1]).unwrap();
1304        let out = scatter_min_single(&input, &indices, &updates, 0).unwrap();
1305        let expected = array!([-6.0f32, 1.0, 1.0, 1.0]);
1306        assert!(out.all_close(&expected, 1e-5, 1e-5, None).unwrap());
1307    }
1308
1309    #[test]
1310    fn test_scatter_prod_single() {
1311        // Single element scatter prod
1312        let input = Array::ones::<f32>(&[4]).unwrap();
1313        let indices = Array::from_slice(&[0u32, 0, 3], &[3]);
1314        let updates = Array::full::<f32>(&[3, 1], array!(2.0f32)).unwrap();
1315        let out = scatter_prod_single(&input, &indices, &updates, 0).unwrap();
1316        let expected = array!([4.0f32, 1.0, 1.0, 2.0]);
1317        assert!(out.all_close(&expected, 1e-5, 1e-5, None).unwrap());
1318    }
1319
1320    #[test]
1321    fn test_gather_single() {
1322        // Simple gather test
1323        let input = Array::from_slice(&[0.0f32, 1.0, 2.0, 3.0], &[4]);
1324        let indices = Array::from_slice(&[1u32, 3], &[2]);
1325        let out = gather_single(&input, &indices, 0, &[1]).unwrap();
1326        let expected = array!([[1.0f32], [3.0]]);
1327        assert!(out.all_close(&expected, 1e-5, 1e-5, None).unwrap());
1328    }
1329
1330    #[test]
1331    fn test_masked_scatter() {
1332        // Simple masked scatter test
1333        let input = Array::from_slice(&[1.0f32, 2.0, 3.0, 4.0], &[4]);
1334        let mask = Array::from_slice(&[true, false, true, false], &[4]);
1335        let src = Array::from_slice(&[10.0f32, 20.0], &[2]);
1336        let out = masked_scatter(&input, &mask, &src).unwrap();
1337        let expected = array!([10.0f32, 2.0, 20.0, 4.0]);
1338        assert!(out.all_close(&expected, 1e-5, 1e-5, None).unwrap());
1339    }
1340}