Skip to main content

nuvec_core/
ops.rs

1//! Array operations as free functions with MLX's names: the mirrors of the [`Array`]
2//! methods.
3//!
4//! Every operation whose first argument is an array is a method, and the method is the
5//! canonical, documented form: `x.sum(-1)`, `x.matmul(&w.t())`, `m.select(&a, 0.0)`. This
6//! module holds the same operations as free functions with MLX's names and the same
7//! parameters (the receiver becomes the first argument), so MLX code ports line by line:
8//! `sum(&x, -1)`, `matmul(&x, &w.t())`, `where_(&m, &a, 0.0)`. A few operations exist only
9//! here because their first argument is not one array: [`concatenate`], [`stack`],
10//! [`where_`] and the constructors ([`zeros`], [`ones`], [`full`], [`arange`]).
11//!
12//! Operations return [`Array`], never `Result`: invalid arguments give a *failed array*
13//! carrying [`crate::Error::Op`] with the operation's name and the caller's source location,
14//! and every array computed from it fails with the same error, surfacing at
15//! [`Array::check`], [`Array::eval`], [`Array::item`] or [`Array::to_vec`].
16//!
17//! ```
18//! use nuvec_core::{Array, array, ops};
19//!
20//! let x = array![[1.0, 2.0], [3.0, 4.0]];
21//! let a = x.exp().sum(-1);
22//! let b = ops::sum(&ops::exp(&x), -1);
23//! assert_eq!(a.to_vec::<f32>().unwrap(), b.to_vec::<f32>().unwrap());
24//! ```
25
26// The implementation functions behind the public operations (they return `Result`).
27pub(crate) mod imp;
28
29use std::panic::Location;
30
31use smallvec::SmallVec;
32
33use crate::axes::{self, Axes, AxesSpec, Axis, IntoShape, ScanOptions, VarOptions};
34use crate::primitive::BinaryOp;
35use crate::scalar::{Scalar, ScalarOrArray};
36use crate::{Array, Category, DType, Error, Result, at_least_float};
37
38// ── Helpers ─────────────────────────────────────────────────────────────────
39
40/// Axes in the `isize` form of the implementation functions.
41fn isize_axes(axes: &[usize]) -> Vec<isize> {
42    axes.iter().map(|&a| a as isize).collect()
43}
44
45/// A weak operand as an array next to `like` (its dtype adapts).
46fn operand(value: ScalarOrArray, like: DType) -> Result<Array> {
47    value.into_array(Some(like))
48}
49
50/// The arrays among two operands, for failure reporting.
51fn arrays_of<'a>(a: &'a ScalarOrArray, b: &'a ScalarOrArray) -> SmallVec<[&'a Array; 2]> {
52    [a, b]
53        .into_iter()
54        .filter_map(|x| match x {
55            ScalarOrArray::Array(a) => Some(a),
56            ScalarOrArray::Scalar(_) => None,
57        })
58        .collect()
59}
60
61/// Both operands of a binary operation as arrays (MLX's `to_arrays`): a weak scalar adapts
62/// to the other operand's dtype; two scalars take their default dtypes.
63fn resolve_pair(a: &ScalarOrArray, b: &ScalarOrArray) -> Result<(Array, Array)> {
64    let like = |x: &ScalarOrArray| match x {
65        ScalarOrArray::Array(a) => Some(a.dtype()),
66        ScalarOrArray::Scalar(_) => None,
67    };
68    let (la, lb) = (like(a), like(b));
69    let conv = |x: &ScalarOrArray, other: Option<DType>| match x {
70        ScalarOrArray::Array(a) => Ok(a.clone()),
71        ScalarOrArray::Scalar(s) => s.to_array(other),
72    };
73    Ok((conv(a, lb)?, conv(b, la)?))
74}
75
76#[inline]
77fn binary_op(
78    name: &'static str,
79    at: &'static Location<'static>,
80    op: BinaryOp,
81    a: ScalarOrArray,
82    b: ScalarOrArray,
83) -> Array {
84    match (a, b) {
85        (ScalarOrArray::Array(a), ScalarOrArray::Array(b)) => {
86            Array::sticky(name, at, &[&a, &b], || imp::binary(op, &a, &b))
87        }
88        (a, b) => {
89            let ins = arrays_of(&a, &b);
90            Array::sticky(name, at, &ins, || {
91                let (x, y) = resolve_pair(&a, &b)?;
92                imp::binary(op, &x, &y)
93            })
94        }
95    }
96}
97
98// ── Element-wise binary ─────────────────────────────────────────────────────
99
100macro_rules! binary_ops {
101    ($($name:ident => $op:ident),* $(,)?) => {$(
102        #[doc = concat!("Element-wise `", stringify!($name), "`; see [`Array::", stringify!($name), "`]. Either operand may be a weak Rust number.")]
103        #[track_caller]
104        pub fn $name(a: impl Into<ScalarOrArray>, b: impl Into<ScalarOrArray>) -> Array {
105            binary_op(stringify!($name), Location::caller(), BinaryOp::$op, a.into(), b.into())
106        }
107    )*};
108}
109
110binary_ops! {
111    add => Add,
112    subtract => Subtract,
113    multiply => Multiply,
114    divide => Divide,
115    maximum => Maximum,
116    minimum => Minimum,
117    logaddexp => LogAddExp,
118    equal => Equal,
119    not_equal => NotEqual,
120    less => Less,
121    less_equal => LessEqual,
122    greater => Greater,
123    greater_equal => GreaterEqual,
124}
125
126// ── Element-wise unary ──────────────────────────────────────────────────────
127
128macro_rules! unary_ops {
129    ($($name:ident),* $(,)?) => {$(
130        #[doc = concat!("Element-wise `", stringify!($name), "`; see [`Array::", stringify!($name), "`].")]
131        #[track_caller]
132        pub fn $name(a: &Array) -> Array {
133            Array::sticky(stringify!($name), Location::caller(), &[a], || imp::$name(a))
134        }
135    )*};
136}
137
138unary_ops!(
139    negative,
140    abs,
141    exp,
142    log,
143    sqrt,
144    sin,
145    cos,
146    tanh,
147    sigmoid,
148    erfinv,
149    sign,
150    conjugate,
151    stop_gradient,
152    contiguous,
153    copy
154);
155
156/// Element-wise selection: `a` where `condition` is true, else `b` (MLX's `where`); see
157/// [`Array::select`]. Any operand may be a weak Rust number.
158#[track_caller]
159pub fn where_(
160    condition: impl Into<ScalarOrArray>,
161    a: impl Into<ScalarOrArray>,
162    b: impl Into<ScalarOrArray>,
163) -> Array {
164    let (condition, a, b) = (condition.into(), a.into(), b.into());
165    let mut ins: SmallVec<[&Array; 3]> = SmallVec::new();
166    for x in [&condition, &a, &b] {
167        if let ScalarOrArray::Array(x) = x {
168            ins.push(x);
169        }
170    }
171    Array::sticky("where", Location::caller(), &ins, || {
172        let (a, b) = resolve_pair(&a, &b)?;
173        let condition = condition.clone().into_array(None)?;
174        imp::where_(condition, a, b)
175    })
176}
177
178/// The elements converted to `dtype`; see [`Array::astype`].
179#[track_caller]
180pub fn astype(a: &Array, dtype: DType) -> Array {
181    Array::sticky("astype", Location::caller(), &[a], || imp::astype(a, dtype))
182}
183
184/// Matrix product; see [`Array::matmul`].
185#[track_caller]
186pub fn matmul(a: &Array, b: &Array) -> Array {
187    Array::sticky("matmul", Location::caller(), &[a, b], || imp::matmul(a, b))
188}
189
190/// `alpha · (a @ b) + beta · c`; see [`Array::addmm`].
191#[track_caller]
192pub fn addmm(c: &Array, a: &Array, b: &Array, alpha: f32, beta: f32) -> Array {
193    Array::sticky("addmm", Location::caller(), &[c, a, b], || {
194        imp::addmm(c, a, b, alpha, beta)
195    })
196}
197
198// ── Reductions ──────────────────────────────────────────────────────────────
199
200#[derive(Clone, Copy)]
201enum Reduce {
202    Sum,
203    Prod,
204    Max,
205    Min,
206    All,
207    Any,
208    Mean,
209    LogSumExp,
210    Var(i32),
211    Std(i32),
212}
213
214impl Reduce {
215    fn name(self) -> &'static str {
216        match self {
217            Self::Sum => "sum",
218            Self::Prod => "prod",
219            Self::Max => "max",
220            Self::Min => "min",
221            Self::All => "all",
222            Self::Any => "any",
223            Self::Mean => "mean",
224            Self::LogSumExp => "logsumexp",
225            Self::Var(_) => "var",
226            Self::Std(_) => "std",
227        }
228    }
229}
230
231/// The body of every reduction (non-generic: the public functions only convert their
232/// arguments).
233fn reduce_op(
234    at: &'static Location<'static>,
235    a: &Array,
236    axes: AxesSpec,
237    keepdims: bool,
238    op: Reduce,
239) -> Array {
240    Array::sticky(op.name(), at, &[a], || reduce(a, &axes, keepdims, op))
241}
242
243fn reduce(a: &Array, axes: &AxesSpec, keepdims: bool, op: Reduce) -> Result<Array> {
244    let axes = axes.sorted(a.ndim())?;
245    let list = isize_axes(&axes);
246    if axes.is_empty() {
247        // No axis is reduced (MLX C++'s `sum(a, {})`).
248        return match op {
249            Reduce::Sum | Reduce::Prod | Reduce::Max | Reduce::Min => Ok(a.clone()),
250            Reduce::All | Reduce::Any => imp::astype(a, DType::Bool),
251            Reduce::Mean => imp::astype(a, at_least_float(a.dtype())),
252            Reduce::LogSumExp => logsumexp_none(a),
253            Reduce::Var(ddof) => variance(a, &axes, keepdims, ddof),
254            Reduce::Std(ddof) => imp::sqrt(&variance(a, &axes, keepdims, ddof)?),
255        };
256    }
257    match op {
258        Reduce::Sum => imp::sum(a, &list, keepdims),
259        Reduce::Prod => imp::prod(a, &list, keepdims),
260        Reduce::Max => imp::max(a, &list, keepdims),
261        Reduce::Min => imp::min(a, &list, keepdims),
262        Reduce::All => imp::all(a, &list, keepdims),
263        Reduce::Any => imp::any(a, &list, keepdims),
264        Reduce::Mean => imp::mean(a, &list, keepdims),
265        Reduce::LogSumExp => imp::logsumexp(a, &list, keepdims),
266        Reduce::Var(ddof) => variance(a, &axes, keepdims, ddof),
267        Reduce::Std(ddof) => imp::sqrt(&variance(a, &axes, keepdims, ddof)?),
268    }
269}
270
271/// `log(exp(a))` element-wise, as MLX's `logsumexp` over no axes: `a`, with the same
272/// handling of infinities.
273fn logsumexp_none(a: &Array) -> Result<Array> {
274    let a = imp::astype(a, at_least_float(a.dtype()))?;
275    let maxval = imp::stop_gradient(&a)?;
276    let out = imp::log(&imp::exp(&imp::subtract(&a, &maxval)?)?)?;
277    let out = imp::add(&out, &maxval)?;
278    imp::where_(
279        imp::equal(&imp::abs(&maxval)?, f64::INFINITY)?,
280        &maxval,
281        &out,
282    )
283}
284
285/// The mean over `axes` (sorted; empty = none) of a floating-point `a`.
286fn mean_over(a: &Array, axes: &[usize], keepdims: bool) -> Result<Array> {
287    if axes.is_empty() {
288        return Ok(a.clone());
289    }
290    imp::mean(a, &isize_axes(axes), keepdims)
291}
292
293/// The variance over `axes`, in at least float32 (the input is converted first, so integers
294/// never wrap): `mean((a − mean(a))²)`, scaled by `n / max(n − ddof, 0)` when `ddof` is not 0.
295fn variance(a: &Array, axes: &[usize], keepdims: bool, ddof: i32) -> Result<Array> {
296    let dtype = at_least_float(a.dtype());
297    let a = imp::astype(a, dtype)?;
298    // Two passes, `mean((a − mean(a))²)`: no cancellation for large means.
299    let mu = if axes.is_empty() {
300        a.clone()
301    } else {
302        imp::mean(&a, &isize_axes(axes), true)?
303    };
304    let d = imp::subtract(&a, &mu)?;
305    let v = mean_over(&imp::multiply(&d, &d)?, axes, keepdims)?;
306    if ddof == 0 {
307        return Ok(v);
308    }
309    let n: f64 = axes.iter().map(|&d| a.dims()[d] as f64).product();
310    let n = imp::astype(&crate::creation::from_data(&[n], &[])?, dtype)?;
311    let rest = imp::maximum(&imp::subtract(&n, f64::from(ddof))?, 0.0)?;
312    imp::multiply(&v, &imp::divide(&n, &rest)?)
313}
314
315macro_rules! reductions {
316    ($($name:ident, $keep:ident => $op:expr, $doc:literal;)*) => {$(
317        #[doc = concat!($doc, " over `axes`; see [`Array::", stringify!($name), "`].")]
318        #[track_caller]
319        pub fn $name(a: &Array, axes: impl Axes) -> Array {
320            reduce_op(Location::caller(), a, axes.axes_spec(), false, $op)
321        }
322        #[doc = concat!($doc, " over `axes`, keeping them as size-1 dimensions; see [`Array::", stringify!($keep), "`].")]
323        #[track_caller]
324        pub fn $keep(a: &Array, axes: impl Axes) -> Array {
325            reduce_op(Location::caller(), a, axes.axes_spec(), true, $op)
326        }
327    )*};
328}
329
330reductions! {
331    sum, sum_keepdims => Reduce::Sum, "The sum";
332    prod, prod_keepdims => Reduce::Prod, "The product";
333    max, max_keepdims => Reduce::Max, "The maximum";
334    min, min_keepdims => Reduce::Min, "The minimum";
335    all, all_keepdims => Reduce::All, "Whether every element is non-zero";
336    any, any_keepdims => Reduce::Any, "Whether any element is non-zero";
337    mean, mean_keepdims => Reduce::Mean, "The mean";
338    logsumexp, logsumexp_keepdims => Reduce::LogSumExp, "`log(sum(exp(a)))`, computed stably,";
339    var, var_keepdims => Reduce::Var(0), "The variance";
340    std, std_keepdims => Reduce::Std(0), "The standard deviation";
341}
342
343/// The variance over `axes` with [`VarOptions`]; see [`Array::var_with`].
344#[track_caller]
345pub fn var_with(a: &Array, axes: impl Axes, options: VarOptions) -> Array {
346    reduce_op(
347        Location::caller(),
348        a,
349        axes.axes_spec(),
350        options.keepdims,
351        Reduce::Var(options.ddof),
352    )
353}
354
355/// The standard deviation over `axes` with [`VarOptions`]; see [`Array::std_with`].
356#[track_caller]
357pub fn std_with(a: &Array, axes: impl Axes, options: VarOptions) -> Array {
358    reduce_op(
359        Location::caller(),
360        a,
361        axes.axes_spec(),
362        options.keepdims,
363        Reduce::Std(options.ddof),
364    )
365}
366
367fn arg_reduce_op(
368    at: &'static Location<'static>,
369    a: &Array,
370    axis: Option<i64>,
371    keepdims: bool,
372    max: bool,
373) -> Array {
374    let name = if max { "argmax" } else { "argmin" };
375    Array::sticky(name, at, &[a], || {
376        let axis = axes::one_axis(axis, a.ndim())?.map(|d| d as isize);
377        if max {
378            imp::argmax(a, axis, keepdims)
379        } else {
380            imp::argmin(a, axis, keepdims)
381        }
382    })
383}
384
385/// Indices (uint32) of the maxima along one axis (`..`: the flattened array); see
386/// [`Array::argmax`].
387#[track_caller]
388pub fn argmax(a: &Array, axis: impl Axis) -> Array {
389    arg_reduce_op(Location::caller(), a, axis.axis_spec(), false, true)
390}
391
392/// [`argmax`] keeping the reduced axis; see [`Array::argmax_keepdims`].
393#[track_caller]
394pub fn argmax_keepdims(a: &Array, axis: impl Axis) -> Array {
395    arg_reduce_op(Location::caller(), a, axis.axis_spec(), true, true)
396}
397
398/// Indices (uint32) of the minima along one axis (`..`: the flattened array); see
399/// [`Array::argmin`].
400#[track_caller]
401pub fn argmin(a: &Array, axis: impl Axis) -> Array {
402    arg_reduce_op(Location::caller(), a, axis.axis_spec(), false, false)
403}
404
405/// [`argmin`] keeping the reduced axis; see [`Array::argmin_keepdims`].
406#[track_caller]
407pub fn argmin_keepdims(a: &Array, axis: impl Axis) -> Array {
408    arg_reduce_op(Location::caller(), a, axis.axis_spec(), true, false)
409}
410
411fn softmax_op(at: &'static Location<'static>, a: &Array, spec: AxesSpec) -> Array {
412    Array::sticky("softmax", at, &[a], || {
413        let axes = spec.sorted(a.ndim())?;
414        if axes.is_empty() {
415            // Over no axes: every element is its own group (1, or NaN where `a` is not
416            // finite), as MLX.
417            let a = imp::astype(a, at_least_float(a.dtype()))?;
418            let ex = imp::exp(&imp::subtract(&a, &imp::stop_gradient(&a)?)?)?;
419            return imp::divide(&ex, &ex);
420        }
421        imp::softmax(a, &isize_axes(&axes))
422    })
423}
424
425/// `exp(a) / sum(exp(a))` over `axes`; see [`Array::softmax`].
426#[track_caller]
427pub fn softmax(a: &Array, axes: impl Axes) -> Array {
428    softmax_op(Location::caller(), a, axes.axes_spec())
429}
430
431// ── Scans and sorting ───────────────────────────────────────────────────────
432
433type ScanImpl = fn(&Array, Option<isize>, bool, bool) -> Result<Array>;
434
435fn scan_op(
436    name: &'static str,
437    at: &'static Location<'static>,
438    a: &Array,
439    axis: Option<i64>,
440    options: ScanOptions,
441    f: ScanImpl,
442) -> Array {
443    Array::sticky(name, at, &[a], || {
444        let axis = axes::one_axis(axis, a.ndim())?.map(|d| d as isize);
445        f(a, axis, options.reverse, !options.exclusive)
446    })
447}
448
449macro_rules! scans {
450    ($($name:ident, $with:ident => $imp:path, $doc:literal;)*) => {$(
451        #[doc = concat!($doc, " along one axis (`..`: the flattened array); see [`Array::", stringify!($name), "`].")]
452        #[track_caller]
453        pub fn $name(a: &Array, axis: impl Axis) -> Array {
454            scan_op(stringify!($name), Location::caller(), a, axis.axis_spec(), ScanOptions::default(), $imp)
455        }
456        #[doc = concat!($doc, " with [`ScanOptions`]; see [`Array::", stringify!($with), "`].")]
457        #[track_caller]
458        pub fn $with(a: &Array, axis: impl Axis, options: ScanOptions) -> Array {
459            scan_op(stringify!($name), Location::caller(), a, axis.axis_spec(), options, $imp)
460        }
461    )*};
462}
463
464scans! {
465    cumsum, cumsum_with => crate::sorting::cumsum, "The cumulative sum";
466    cumprod, cumprod_with => crate::sorting::cumprod, "The cumulative product";
467    cummax, cummax_with => crate::sorting::cummax, "The running maximum";
468    cummin, cummin_with => crate::sorting::cummin, "The running minimum";
469    logcumsumexp, logcumsumexp_with => crate::sorting::logcumsumexp, "The running `log(Σ exp)`";
470}
471
472#[derive(Clone, Copy)]
473enum Sorting {
474    Sort,
475    ArgSort,
476    Partition(i32),
477    ArgPartition(i32),
478    TopK(i32),
479}
480
481fn sorting_op(at: &'static Location<'static>, a: &Array, axis: Option<i64>, op: Sorting) -> Array {
482    let name = match op {
483        Sorting::Sort => "sort",
484        Sorting::ArgSort => "argsort",
485        Sorting::Partition(_) => "partition",
486        Sorting::ArgPartition(_) => "argpartition",
487        Sorting::TopK(_) => "topk",
488    };
489    Array::sticky(name, at, &[a], || {
490        let axis = axes::one_axis(axis, a.ndim())?.map(|d| d as isize);
491        match op {
492            Sorting::Sort => crate::sorting::sort(a, axis),
493            Sorting::ArgSort => crate::sorting::argsort(a, axis),
494            Sorting::Partition(kth) => crate::sorting::partition(a, kth as isize, axis),
495            Sorting::ArgPartition(kth) => crate::sorting::argpartition(a, kth as isize, axis),
496            Sorting::TopK(k) => {
497                let k = usize::try_from(k).map_err(|_| {
498                    Error::InvalidArgument(format!("topk: k must not be negative, got {k}"))
499                })?;
500                crate::sorting::topk(a, k, axis)
501            }
502        }
503    })
504}
505
506/// The array sorted along one axis; see [`Array::sort`].
507#[track_caller]
508pub fn sort(a: &Array, axis: impl Axis) -> Array {
509    sorting_op(Location::caller(), a, axis.axis_spec(), Sorting::Sort)
510}
511
512/// The indices (uint32) that sort the array along one axis; see [`Array::argsort`].
513#[track_caller]
514pub fn argsort(a: &Array, axis: impl Axis) -> Array {
515    sorting_op(Location::caller(), a, axis.axis_spec(), Sorting::ArgSort)
516}
517
518/// The array partitioned around position `kth` along one axis; see [`Array::partition`].
519#[track_caller]
520pub fn partition(a: &Array, kth: i32, axis: impl Axis) -> Array {
521    sorting_op(
522        Location::caller(),
523        a,
524        axis.axis_spec(),
525        Sorting::Partition(kth),
526    )
527}
528
529/// The indices (uint32) that partition the array; see [`Array::argpartition`].
530#[track_caller]
531pub fn argpartition(a: &Array, kth: i32, axis: impl Axis) -> Array {
532    sorting_op(
533        Location::caller(),
534        a,
535        axis.axis_spec(),
536        Sorting::ArgPartition(kth),
537    )
538}
539
540/// The `k` largest elements along one axis; see [`Array::topk`].
541#[track_caller]
542pub fn topk(a: &Array, k: i32, axis: impl Axis) -> Array {
543    sorting_op(Location::caller(), a, axis.axis_spec(), Sorting::TopK(k))
544}
545
546// ── Shapes and views ────────────────────────────────────────────────────────
547
548fn reshape_op(at: &'static Location<'static>, a: &Array, spec: &[i64]) -> Array {
549    Array::sticky("reshape", at, &[a], || {
550        imp::reshape_to(a, axes::reshape_dims(spec, a.size())?)
551    })
552}
553
554/// The same elements in another shape (one `-1` inferred); see [`Array::reshape`].
555#[track_caller]
556pub fn reshape(a: &Array, shape: impl IntoShape) -> Array {
557    reshape_op(Location::caller(), a, &shape.shape_spec())
558}
559
560fn transpose_op(at: &'static Location<'static>, a: &Array, spec: AxesSpec) -> Array {
561    Array::sticky("transpose", at, &[a], || {
562        let perm: SmallVec<[usize; 4]> = match spec {
563            AxesSpec::All => (0..a.ndim()).rev().collect(),
564            _ => spec.list(a.ndim())?,
565        };
566        if perm.len() != a.ndim() {
567            return Err(Error::InvalidArgument(format!(
568                "transpose: {} axes for an array of {} dimensions",
569                perm.len(),
570                a.ndim()
571            )));
572        }
573        imp::transpose(a, &perm)
574    })
575}
576
577/// The axes reordered (`..`: reversed); see [`Array::transpose`].
578#[track_caller]
579pub fn transpose(a: &Array, axes: impl Axes) -> Array {
580    transpose_op(Location::caller(), a, axes.axes_spec())
581}
582
583fn broadcast_op(at: &'static Location<'static>, a: &Array, spec: &[i64]) -> Array {
584    Array::sticky("broadcast_to", at, &[a], || {
585        imp::broadcast_to(a, &axes::dims(spec)?)
586    })
587}
588
589/// The array broadcast to `shape`; see [`Array::broadcast_to`].
590#[track_caller]
591pub fn broadcast_to(a: &Array, shape: impl IntoShape) -> Array {
592    broadcast_op(Location::caller(), a, &shape.shape_spec())
593}
594
595fn expand_dims_op(at: &'static Location<'static>, a: &Array, spec: AxesSpec) -> Array {
596    Array::sticky("expand_dims", at, &[a], || {
597        let AxesSpec::List(list) = &spec else {
598            return Err(Error::InvalidArgument(
599                "expand_dims: the new axes must be listed".into(),
600            ));
601        };
602        let out_ndim = a.ndim() + list.len();
603        let positions = spec.sorted(out_ndim)?;
604        let mut shape: crate::shape::Shape = a.dims().into();
605        for &p in &positions {
606            shape.insert(p, 1);
607        }
608        imp::reshape_to(a, shape)
609    })
610}
611
612/// Size-1 dimensions inserted at `axes` (positions in the result); see
613/// [`Array::expand_dims`].
614#[track_caller]
615pub fn expand_dims(a: &Array, axes: impl Axes) -> Array {
616    expand_dims_op(Location::caller(), a, axes.axes_spec())
617}
618
619fn squeeze_op(at: &'static Location<'static>, a: &Array, spec: AxesSpec) -> Array {
620    Array::sticky("squeeze", at, &[a], || {
621        if spec == AxesSpec::All {
622            return imp::squeeze(a, &[]);
623        }
624        let list = spec.list(a.ndim())?;
625        if list.is_empty() {
626            return Ok(a.clone());
627        }
628        imp::squeeze(a, &isize_axes(&list))
629    })
630}
631
632/// Size-1 dimensions removed (`..`: all of them); see [`Array::squeeze`].
633#[track_caller]
634pub fn squeeze(a: &Array, axes: impl Axes) -> Array {
635    squeeze_op(Location::caller(), a, axes.axes_spec())
636}
637
638fn repeat_op(at: &'static Location<'static>, a: &Array, repeats: i32, axis: Option<i64>) -> Array {
639    Array::sticky("repeat", at, &[a], || {
640        let repeats = usize::try_from(repeats).map_err(|_| {
641            Error::InvalidArgument(format!(
642                "repeat: repeats must not be negative, got {repeats}"
643            ))
644        })?;
645        imp::repeat(
646            a,
647            repeats,
648            axes::one_axis(axis, a.ndim())?.map(|d| d as isize),
649        )
650    })
651}
652
653/// Each element repeated `repeats` times along one axis; see [`Array::repeat`].
654#[track_caller]
655pub fn repeat(a: &Array, repeats: i32, axis: impl Axis) -> Array {
656    repeat_op(Location::caller(), a, repeats, axis.axis_spec())
657}
658
659fn split_op(
660    at: &'static Location<'static>,
661    a: &Array,
662    parts: i32,
663    axis: Option<i64>,
664) -> Vec<Array> {
665    let count = usize::try_from(parts).unwrap_or(0).max(1);
666    Array::sticky_vec("split", at, &[a], count, || {
667        let parts = usize::try_from(parts).map_err(|_| {
668            Error::InvalidArgument(format!("split: parts must not be negative, got {parts}"))
669        })?;
670        crate::indexing::split(a, parts, axes::given_axis(axis, a.ndim())?)
671    })
672}
673
674/// The array split into `parts` equal parts along `axis`; see [`Array::split`].
675#[track_caller]
676pub fn split(a: &Array, parts: i32, axis: impl Axis) -> Vec<Array> {
677    split_op(Location::caller(), a, parts, axis.axis_spec())
678}
679
680fn split_at_op(
681    at: &'static Location<'static>,
682    a: &Array,
683    indices: &[i32],
684    axis: Option<i64>,
685) -> Vec<Array> {
686    Array::sticky_vec("split", at, &[a], indices.len() + 1, || {
687        let indices = indices
688            .iter()
689            .map(|&i| {
690                usize::try_from(i).map_err(|_| {
691                    Error::InvalidArgument(format!("split: index {i} must not be negative"))
692                })
693            })
694            .collect::<Result<Vec<_>>>()?;
695        crate::indexing::split_at(a, &indices, axes::given_axis(axis, a.ndim())?)
696    })
697}
698
699/// The array split before each of `indices` along `axis`; see [`Array::split_at`].
700#[track_caller]
701pub fn split_at(a: &Array, indices: &[i32], axis: impl Axis) -> Vec<Array> {
702    split_at_op(Location::caller(), a, indices, axis.axis_spec())
703}
704
705/// The array split into `N` equal parts along `axis`; see [`Array::split_n`].
706#[track_caller]
707pub fn split_n<const N: usize>(a: &Array, axis: impl Axis) -> [Array; N] {
708    let parts = split_op(
709        Location::caller(),
710        a,
711        i32::try_from(N).unwrap_or(i32::MAX),
712        axis.axis_spec(),
713    );
714    parts
715        .try_into()
716        .unwrap_or_else(|v: Vec<Array>| std::array::from_fn(|i| v[i % v.len()].clone()))
717}
718
719fn join_op(
720    name: &'static str,
721    at: &'static Location<'static>,
722    arrays: &[Array],
723    axis: Option<i64>,
724) -> Array {
725    let ins: SmallVec<[&Array; 4]> = arrays.iter().collect();
726    Array::sticky(name, at, &ins, || {
727        let ndim = arrays.first().map_or(0, Array::ndim);
728        let axis = axes::given_axis(axis, ndim)?;
729        if name == "stack" {
730            crate::indexing::stack(arrays, axis)
731        } else {
732            crate::indexing::concatenate(arrays, axis)
733        }
734    })
735}
736
737/// `arrays` joined along `axis` (MLX's `concatenate`); dtypes are promoted.
738///
739/// ```
740/// use nuvec_core::{array, ops::concatenate};
741///
742/// let c = concatenate(&[array![[1.0, 2.0]], array![[3.0, 4.0]]], 0);
743/// assert_eq!(c.shape(), [2, 2]);
744/// ```
745#[doc(alias = "concat")]
746#[track_caller]
747pub fn concatenate(arrays: &[Array], axis: impl Axis) -> Array {
748    join_op("concatenate", Location::caller(), arrays, axis.axis_spec())
749}
750
751/// `arrays` (one shape) stacked along a new axis `axis` (MLX's `stack`).
752///
753/// ```
754/// use nuvec_core::{array, ops::stack};
755///
756/// assert_eq!(stack(&[array![1, 2], array![3, 4]], -1).shape(), [2, 2]);
757/// ```
758#[track_caller]
759pub fn stack(arrays: &[Array], axis: impl Axis) -> Array {
760    join_op("stack", Location::caller(), arrays, axis.axis_spec())
761}
762
763// ── Gathers and scatters ────────────────────────────────────────────────────
764
765fn take_op(at: &'static Location<'static>, a: &Array, indices: &Array, axis: Option<i64>) -> Array {
766    Array::sticky("take", at, &[a, indices], || {
767        crate::indexing::take(
768            a,
769            indices,
770            axes::one_axis(axis, a.ndim())?.map(|d| d as isize),
771        )
772    })
773}
774
775/// The elements at `indices` along one axis (`..`: the flattened array); see
776/// [`Array::take`].
777#[track_caller]
778pub fn take(a: &Array, indices: &Array, axis: impl Axis) -> Array {
779    take_op(Location::caller(), a, indices, axis.axis_spec())
780}
781
782fn take_along_op(
783    at: &'static Location<'static>,
784    a: &Array,
785    indices: &Array,
786    axis: Option<i64>,
787) -> Array {
788    Array::sticky("take_along_axis", at, &[a, indices], || {
789        crate::indexing::take_along_axis(a, indices, axes::given_axis(axis, a.ndim())?)
790    })
791}
792
793/// The elements at `indices` along `axis`; see [`Array::take_along_axis`].
794#[track_caller]
795pub fn take_along_axis(a: &Array, indices: &Array, axis: impl Axis) -> Array {
796    take_along_op(Location::caller(), a, indices, axis.axis_spec())
797}
798
799fn scatter_op(
800    at: &'static Location<'static>,
801    a: &Array,
802    indices: &Array,
803    values: ScalarOrArray,
804    axis: Option<i64>,
805    add: bool,
806) -> Array {
807    let name = if add {
808        "scatter_add_along_axis"
809    } else {
810        "put_along_axis"
811    };
812    let mut ins: SmallVec<[&Array; 3]> = SmallVec::from_slice(&[a, indices]);
813    if let ScalarOrArray::Array(v) = &values {
814        ins.push(v);
815    }
816    Array::sticky(name, at, &ins, || {
817        let values = operand(values.clone(), a.dtype())?;
818        let axis = axes::given_axis(axis, a.ndim())?;
819        if add {
820            crate::indexing::scatter_add_along_axis(a, indices, &values, axis)
821        } else {
822            crate::indexing::put_along_axis(a, indices, &values, axis)
823        }
824    })
825}
826
827/// A copy with `values` placed at `indices` along `axis`; see [`Array::put_along_axis`].
828#[track_caller]
829pub fn put_along_axis(
830    a: &Array,
831    indices: &Array,
832    values: impl Into<ScalarOrArray>,
833    axis: impl Axis,
834) -> Array {
835    scatter_op(
836        Location::caller(),
837        a,
838        indices,
839        values.into(),
840        axis.axis_spec(),
841        false,
842    )
843}
844
845/// A copy with `values` added at `indices` along `axis` (repeated indices accumulate); see
846/// [`Array::scatter_add_along_axis`].
847#[track_caller]
848pub fn scatter_add_along_axis(
849    a: &Array,
850    indices: &Array,
851    values: impl Into<ScalarOrArray>,
852    axis: impl Axis,
853) -> Array {
854    scatter_op(
855        Location::caller(),
856        a,
857        indices,
858        values.into(),
859        axis.axis_spec(),
860        true,
861    )
862}
863
864/// A copy of `a` with the region at `index` replaced by `update`; see
865/// [`Array::slice_update`].
866#[track_caller]
867pub fn slice_update(a: &Array, update: &Array, index: impl crate::ArrayIndex) -> Array {
868    a.slice_update(update, index)
869}
870
871// ── Quantization and convolution ────────────────────────────────────────────
872
873fn group_bits(op: &'static str, group_size: i32, bits: i32) -> Result<(usize, u32)> {
874    match (usize::try_from(group_size), u32::try_from(bits)) {
875        (Ok(g), Ok(b)) => Ok((g, b)),
876        _ => Err(Error::InvalidArgument(format!(
877            "{op}: group size {group_size} and bits {bits} must not be negative"
878        ))),
879    }
880}
881
882/// `w` quantized along its last axis: `(w_q, scales, biases)`; see [`Array::quantize`].
883#[track_caller]
884pub fn quantize(w: &Array, group_size: i32, bits: i32) -> (Array, Array, Array) {
885    let [q, s, b] = Array::sticky_n("quantize", Location::caller(), &[w], || {
886        let (g, n) = group_bits("quantize", group_size, bits)?;
887        let (q, s, b) = crate::quantized::quantize(w, g, n)?;
888        Ok([q, s, b])
889    });
890    (q, s, b)
891}
892
893/// The weights of [`quantize`] restored; see [`Array::dequantize`].
894#[track_caller]
895pub fn dequantize(w: &Array, scales: &Array, biases: &Array, group_size: i32, bits: i32) -> Array {
896    Array::sticky(
897        "dequantize",
898        Location::caller(),
899        &[w, scales, biases],
900        || {
901            let (g, n) = group_bits("dequantize", group_size, bits)?;
902            crate::quantized::dequantize(w, scales, biases, g, n)
903        },
904    )
905}
906
907/// `x · dequantize(w)ᵀ` (or without the transpose); see [`Array::quantized_matmul`].
908#[track_caller]
909#[allow(clippy::too_many_arguments)]
910pub fn quantized_matmul(
911    x: &Array,
912    w: &Array,
913    scales: &Array,
914    biases: &Array,
915    transpose: bool,
916    group_size: i32,
917    bits: i32,
918) -> Array {
919    Array::sticky(
920        "quantized_matmul",
921        Location::caller(),
922        &[x, w, scales, biases],
923        || {
924            let (g, n) = group_bits("quantized_matmul", group_size, bits)?;
925            crate::quantized::quantized_matmul(x, w, scales, biases, transpose, g, n)
926        },
927    )
928}
929
930/// Quantized products of gathered matrices (mixture-of-experts layers); see
931/// [`Array::gather_qmm`].
932#[track_caller]
933#[allow(clippy::too_many_arguments)]
934pub fn gather_qmm(
935    x: &Array,
936    w: &Array,
937    scales: &Array,
938    biases: &Array,
939    lhs_indices: Option<&Array>,
940    rhs_indices: Option<&Array>,
941    transpose: bool,
942    group_size: i32,
943    bits: i32,
944    sorted_indices: bool,
945) -> Array {
946    let mut ins: SmallVec<[&Array; 6]> = SmallVec::from_slice(&[x, w, scales, biases]);
947    ins.extend(lhs_indices);
948    ins.extend(rhs_indices);
949    Array::sticky("gather_qmm", Location::caller(), &ins, || {
950        let (g, n) = group_bits("gather_qmm", group_size, bits)?;
951        crate::quantized::gather_qmm(
952            x,
953            w,
954            scales,
955            biases,
956            lhs_indices,
957            rhs_indices,
958            transpose,
959            g,
960            n,
961            sorted_indices,
962        )
963    })
964}
965
966/// Products of gathered matrices; see [`Array::gather_mm`].
967#[track_caller]
968pub fn gather_mm(
969    a: &Array,
970    b: &Array,
971    lhs_indices: Option<&Array>,
972    rhs_indices: Option<&Array>,
973    sorted_indices: bool,
974) -> Array {
975    let mut ins: SmallVec<[&Array; 4]> = SmallVec::from_slice(&[a, b]);
976    ins.extend(lhs_indices);
977    ins.extend(rhs_indices);
978    Array::sticky("gather_mm", Location::caller(), &ins, || {
979        crate::quantized::gather_mm(a, b, lhs_indices, rhs_indices, sorted_indices)
980    })
981}
982
983/// A 1-D convolution of `[N, L, C_in]` with `[C_out, K, C_in / groups]`; see
984/// [`Array::conv1d`].
985#[track_caller]
986pub fn conv1d(
987    input: &Array,
988    weight: &Array,
989    stride: i32,
990    padding: i32,
991    dilation: i32,
992    groups: i32,
993) -> Array {
994    Array::sticky("conv1d", Location::caller(), &[input, weight], || {
995        let p = |name: &str, v: i32| {
996            usize::try_from(v).map_err(|_| {
997                Error::InvalidArgument(format!("conv1d: {name} must not be negative, got {v}"))
998            })
999        };
1000        crate::conv::conv1d(
1001            input,
1002            weight,
1003            p("stride", stride)?,
1004            p("padding", padding)?,
1005            p("dilation", dilation)?,
1006            p("groups", groups)?,
1007        )
1008    })
1009}
1010
1011// ── Creation ────────────────────────────────────────────────────────────────
1012
1013/// An array of zeros of `shape`.
1014///
1015/// ```
1016/// use nuvec_core::{DType, ops::zeros};
1017///
1018/// assert_eq!(zeros([2, 3], DType::F32).shape(), [2, 3]);
1019/// assert_eq!(zeros((), DType::I32).ndim(), 0);
1020/// ```
1021#[track_caller]
1022pub fn zeros(shape: impl IntoShape, dtype: DType) -> Array {
1023    let spec = shape.shape_spec();
1024    Array::constructor("zeros", Location::caller(), dtype, &spec, |shape| {
1025        imp::zeros(shape, dtype)
1026    })
1027}
1028
1029/// An array of ones of `shape`.
1030#[track_caller]
1031pub fn ones(shape: impl IntoShape, dtype: DType) -> Array {
1032    let spec = shape.shape_spec();
1033    Array::constructor("ones", Location::caller(), dtype, &spec, |shape| {
1034        imp::ones(shape, dtype)
1035    })
1036}
1037
1038/// An array of `shape` with every element `value`: a Rust number is weak, as in MLX (`0.5`
1039/// gives float32, `1` int32), and an array keeps its dtype (`Array::from(7u8)` gives uint8).
1040///
1041/// ```
1042/// use nuvec_core::{Array, DType, ops::full};
1043///
1044/// assert_eq!(full([2, 2], 0.5).dtype(), DType::F32);
1045/// let a = full([2, 2], Array::from(7u8));
1046/// assert_eq!((a.dtype(), a.to_vec::<u8>().unwrap()), (DType::U8, vec![7; 4]));
1047/// ```
1048#[track_caller]
1049pub fn full(shape: impl IntoShape, value: impl Into<ScalarOrArray>) -> Array {
1050    full_op(Location::caller(), &shape.shape_spec(), value.into())
1051}
1052
1053fn full_op(at: &'static Location<'static>, spec: &[i64], value: ScalarOrArray) -> Array {
1054    let dtype = match &value {
1055        ScalarOrArray::Array(a) => a.dtype(),
1056        ScalarOrArray::Scalar(s) => s.default_dtype(),
1057    };
1058    if let ScalarOrArray::Array(v) = &value
1059        && let Some(e) = v.slot_error()
1060    {
1061        return Array::failed(e, dtype, &axes::dims(spec).unwrap_or_default());
1062    }
1063    Array::constructor("full", at, dtype, spec, |shape| match &value {
1064        // A number: the buffer is filled now, on the host (unified memory), as `zeros`.
1065        ScalarOrArray::Scalar(s) => imp::filled(shape, dtype, |_| s.canon()),
1066        ScalarOrArray::Array(v) => imp::contiguous(&imp::broadcast_to(v, shape)?),
1067    })
1068}
1069
1070/// Evenly spaced values `start, start + step, …` below `stop` (above it for a negative
1071/// step), as MLX: int32 when every argument is an integer (int64 when they do not fit),
1072/// float32 when any is a float. Use [`Array::astype`] for another dtype.
1073///
1074/// ```
1075/// use nuvec_core::{DType, ops::arange};
1076///
1077/// assert_eq!(arange(0, 5, 2).to_vec::<i32>().unwrap(), [0, 2, 4]);
1078/// assert_eq!(arange(0.0, 1.0, 0.25).dtype(), DType::F32);
1079/// ```
1080#[track_caller]
1081pub fn arange(start: impl Into<Scalar>, stop: impl Into<Scalar>, step: impl Into<Scalar>) -> Array {
1082    let (start, stop, step) = (start.into(), stop.into(), step.into());
1083    let at = Location::caller();
1084    let bounds = [start, stop, step];
1085    let dtype = if bounds.iter().any(|s| matches!(s, Scalar::Float(_))) {
1086        DType::F32
1087    } else if bounds
1088        .iter()
1089        .all(|s| s.default_dtype().category() != Category::Complex)
1090        && bounds.iter().all(|s| match s {
1091            Scalar::Int(v) => i32::try_from(*v).is_ok(),
1092            _ => true,
1093        })
1094    {
1095        DType::I32
1096    } else {
1097        DType::I64
1098    };
1099    Array::sticky("arange", at, &[], || {
1100        let f = |s: Scalar| match s {
1101            Scalar::Bool(b) => Ok(f64::from(u8::from(b))),
1102            Scalar::Int(v) => Ok(v as f64),
1103            Scalar::Float(x) => Ok(x),
1104            Scalar::Complex(_) => Err(Error::unsupported_dtype("arange", DType::Complex64)),
1105        };
1106        imp::arange(f(start)?, f(stop)?, f(step)?, dtype)
1107    })
1108}
1109
1110/// Zeros of `a`'s shape and dtype; see [`Array::zeros_like`].
1111#[track_caller]
1112pub fn zeros_like(a: &Array) -> Array {
1113    Array::sticky("zeros_like", Location::caller(), &[a], || {
1114        imp::broadcast_to(&imp::zeros(&[], a.dtype())?, a.dims())
1115    })
1116}
1117
1118/// Ones of `a`'s shape and dtype; see [`Array::ones_like`].
1119#[track_caller]
1120pub fn ones_like(a: &Array) -> Array {
1121    Array::sticky("ones_like", Location::caller(), &[a], || {
1122        imp::broadcast_to(&imp::ones(&[], a.dtype())?, a.dims())
1123    })
1124}
1125
1126/// `value` (a weak scalar takes `a`'s dtype) in `a`'s shape; see [`Array::full_like`].
1127#[track_caller]
1128pub fn full_like(a: &Array, value: impl Into<ScalarOrArray>) -> Array {
1129    let value = value.into();
1130    Array::sticky("full_like", Location::caller(), &[a], || {
1131        let v = imp::astype(&operand(value.clone(), a.dtype())?, a.dtype())?;
1132        imp::contiguous(&imp::broadcast_to(&v, a.dims())?)
1133    })
1134}
1135
1136// ── Methods ─────────────────────────────────────────────────────────────────
1137
1138/// The methods of [`Array`], one `impl` block per family, each method delegating to the
1139/// free function of the same name (or the one given after `=`). The list is also the table
1140/// the surface tests check.
1141macro_rules! methods {
1142    ($(
1143        $(#[$gdoc:meta])*
1144        impl {
1145            $(
1146                $(#[$doc:meta])*
1147                fn $name:ident(&self $(, $p:ident: $t:ty)*) -> $ret:ty $(= $target:ident)?;
1148            )*
1149        }
1150    )*) => {
1151        $(
1152            $(#[$gdoc])*
1153            #[allow(clippy::should_implement_trait, clippy::too_many_arguments)]
1154            impl Array {
1155                $(
1156                    $(#[$doc])*
1157                    #[track_caller]
1158                    pub fn $name(&self $(, $p: $t)*) -> $ret {
1159                        methods!(@call $name [$($target)?] (self $(, $p)*))
1160                    }
1161                )*
1162            }
1163        )*
1164
1165        /// The names of the operations that exist as a method and as a free function of
1166        /// the same parameters (for the surface tests).
1167        #[doc(hidden)]
1168        pub fn __op_names() -> &'static [&'static str] {
1169            &[$($(stringify!($name),)*)*]
1170        }
1171    };
1172    (@call $name:ident [] ($($args:tt)*)) => { $name($($args)*) };
1173    (@call $name:ident [$target:ident] ($($args:tt)*)) => { $target($($args)*) };
1174}
1175
1176methods! {
1177    /// # Element-wise binary operations
1178    ///
1179    /// The other operand is an array or a weak Rust number (it adapts to this array's dtype);
1180    /// dtypes are promoted and shapes broadcast as in MLX. The operators `+ - * /`
1181    /// are the same operations.
1182    impl {
1183        /// Element-wise `self + other`. Integers wrap; for booleans this is logical OR.
1184        ///
1185        /// ```
1186        /// use nuvec_core::array;
1187        ///
1188        /// let x = array![1.0, 2.0];
1189        /// assert_eq!(x.add(1.0).to_vec::<f32>().unwrap(), [2.0, 3.0]);
1190        /// assert_eq!(x.add(&x).to_vec::<f32>().unwrap(), [2.0, 4.0]);
1191        /// ```
1192        fn add(&self, other: impl Into<ScalarOrArray>) -> Array;
1193        /// Element-wise `self - other`. Integers wrap; for booleans this is logical XOR.
1194        ///
1195        /// ```
1196        /// use nuvec_core::array;
1197        ///
1198        /// assert_eq!(array![3, 5].subtract(1).to_vec::<i32>().unwrap(), [2, 4]);
1199        /// ```
1200        fn subtract(&self, other: impl Into<ScalarOrArray>) -> Array;
1201        /// Element-wise `self * other`. Integers wrap; for booleans this is logical AND.
1202        ///
1203        /// ```
1204        /// use nuvec_core::{DType, array};
1205        ///
1206        /// let y = array![1u8, 2].multiply(3);
1207        /// assert_eq!((y.dtype(), y.to_vec::<u8>().unwrap()), (DType::U8, vec![3, 6]));
1208        /// ```
1209        fn multiply(&self, other: impl Into<ScalarOrArray>) -> Array;
1210        /// Element-wise true division; integers are promoted to floating point.
1211        ///
1212        /// ```
1213        /// use nuvec_core::array;
1214        ///
1215        /// assert_eq!(array![1, 3].divide(2).to_vec::<f32>().unwrap(), [0.5, 1.5]);
1216        /// ```
1217        fn divide(&self, other: impl Into<ScalarOrArray>) -> Array;
1218        /// Element-wise maximum; NaN propagates. Not defined for complex numbers.
1219        ///
1220        /// ```
1221        /// use nuvec_core::array;
1222        ///
1223        /// assert_eq!(array![-1.0, 2.0].maximum(0.0).to_vec::<f32>().unwrap(), [0.0, 2.0]);
1224        /// ```
1225        fn maximum(&self, other: impl Into<ScalarOrArray>) -> Array;
1226        /// Element-wise minimum; NaN propagates. Not defined for complex numbers.
1227        fn minimum(&self, other: impl Into<ScalarOrArray>) -> Array;
1228        /// Element-wise `log(exp(self) + exp(other))`, computed stably, in floating point.
1229        fn logaddexp(&self, other: impl Into<ScalarOrArray>) -> Array;
1230        /// Element-wise `self == other` as a bool array.
1231        ///
1232        /// ```
1233        /// use nuvec_core::array;
1234        ///
1235        /// assert_eq!(array![1, 2].equal(2).to_vec::<bool>().unwrap(), [false, true]);
1236        /// ```
1237        fn equal(&self, other: impl Into<ScalarOrArray>) -> Array;
1238        /// Element-wise `self != other` as a bool array.
1239        fn not_equal(&self, other: impl Into<ScalarOrArray>) -> Array;
1240        /// Element-wise `self < other` as a bool array.
1241        fn less(&self, other: impl Into<ScalarOrArray>) -> Array;
1242        /// Element-wise `self <= other` as a bool array.
1243        fn less_equal(&self, other: impl Into<ScalarOrArray>) -> Array;
1244        /// Element-wise `self > other` as a bool array.
1245        ///
1246        /// ```
1247        /// use nuvec_core::array;
1248        ///
1249        /// assert_eq!(array![1.0, 3.0].greater(2.0).to_vec::<bool>().unwrap(), [false, true]);
1250        /// ```
1251        fn greater(&self, other: impl Into<ScalarOrArray>) -> Array;
1252        /// Element-wise `self >= other` as a bool array.
1253        fn greater_equal(&self, other: impl Into<ScalarOrArray>) -> Array;
1254    }
1255
1256    /// # Element-wise unary operations
1257    impl {
1258        /// Element-wise `-self`. Integers wrap. Not defined for booleans.
1259        fn negative(&self) -> Array;
1260        /// Element-wise absolute value; the float32 magnitude for complex numbers.
1261        ///
1262        /// ```
1263        /// use nuvec_core::array;
1264        ///
1265        /// assert_eq!(array![-2, 3].abs().to_vec::<i32>().unwrap(), [2, 3]);
1266        /// ```
1267        fn abs(&self) -> Array;
1268        /// Element-wise `e^self`, in at least float32.
1269        ///
1270        /// ```
1271        /// use nuvec_core::array;
1272        ///
1273        /// assert_eq!(array![0.0].exp().item::<f32>().unwrap(), 1.0);
1274        /// ```
1275        fn exp(&self) -> Array;
1276        /// Element-wise natural logarithm, in at least float32.
1277        ///
1278        /// ```
1279        /// use nuvec_core::array;
1280        ///
1281        /// assert_eq!(array![1.0].log().item::<f32>().unwrap(), 0.0);
1282        /// ```
1283        fn log(&self) -> Array;
1284        /// Element-wise square root, in at least float32.
1285        ///
1286        /// ```
1287        /// use nuvec_core::array;
1288        ///
1289        /// assert_eq!(array![4, 9].sqrt().to_vec::<f32>().unwrap(), [2.0, 3.0]);
1290        /// ```
1291        fn sqrt(&self) -> Array;
1292        /// Element-wise sine, in at least float32.
1293        fn sin(&self) -> Array;
1294        /// Element-wise cosine, in at least float32.
1295        fn cos(&self) -> Array;
1296        /// Element-wise hyperbolic tangent, in at least float32.
1297        ///
1298        /// ```
1299        /// use nuvec_core::array;
1300        ///
1301        /// assert_eq!(array![0.0].tanh().item::<f32>().unwrap(), 0.0);
1302        /// ```
1303        fn tanh(&self) -> Array;
1304        /// Element-wise logistic function `1 / (1 + e^-self)`, in at least float32.
1305        ///
1306        /// ```
1307        /// use nuvec_core::array;
1308        ///
1309        /// assert_eq!(array![0.0].sigmoid().item::<f32>().unwrap(), 0.5);
1310        /// ```
1311        fn sigmoid(&self) -> Array;
1312        /// Element-wise inverse error function, in at least float32.
1313        fn erfinv(&self) -> Array;
1314        /// Element-wise sign: `-1`, `0` or `1` in the array's dtype (NaN gives 0).
1315        fn sign(&self) -> Array;
1316        /// Element-wise complex conjugate; real arrays are returned unchanged.
1317        fn conjugate(&self) -> Array;
1318        /// The array unchanged, but no gradient flows through it. Shares the data.
1319        ///
1320        /// ```
1321        /// use nuvec_core::{Array, array, grad};
1322        ///
1323        /// let g = grad(|x: &Array| x.multiply(&x.stop_gradient()).sum(..))(&array![3.0]).unwrap();
1324        /// assert_eq!(g.item::<f32>().unwrap(), 3.0); // d(x·c)/dx with c = x held constant
1325        /// ```
1326        fn stop_gradient(&self) -> Array;
1327        /// The array in row-major memory: itself if it already is, else a copy.
1328        fn contiguous(&self) -> Array;
1329        /// An independent copy in a buffer of its own (MLX's `mx.copy`), for snapshots: no
1330        /// `compile` simplification, constant folding or in-place update removes it or
1331        /// makes it share memory with `self`, so updating `self` in place afterwards leaves
1332        /// the copy unchanged. Derivatives pass through unchanged.
1333        ///
1334        /// ```
1335        /// use nuvec_core::array;
1336        ///
1337        /// let cache = array![1.0, 2.0, 3.0];
1338        /// let snapshot = cache.copy();
1339        /// let cache = cache.at(0).set(9.0); // in place where `cache` owns its buffer
1340        /// assert_eq!(snapshot.to_vec::<f32>().unwrap(), [1.0, 2.0, 3.0]);
1341        /// assert_eq!(cache.to_vec::<f32>().unwrap(), [9.0, 2.0, 3.0]);
1342        /// ```
1343        ///
1344        /// # Memory
1345        ///
1346        /// The copy's own bytes; no scratch.
1347        fn copy(&self) -> Array;
1348    }
1349
1350    /// # Selection, conversion and matrix products
1351    impl {
1352        /// `a` where `self` (the condition, converted to bool) is true, else `b` (MLX's
1353        /// `where`); `a` and `b` may be weak Rust numbers.
1354        ///
1355        /// ```
1356        /// use nuvec_core::array;
1357        ///
1358        /// let m = array![true, false];
1359        /// let a = array![1.0, 2.0];
1360        /// assert_eq!(m.select(&a, 0.0).to_vec::<f32>().unwrap(), [1.0, 0.0]);
1361        /// ```
1362        #[doc(alias = "where")]
1363        fn select(&self, a: impl Into<ScalarOrArray>, b: impl Into<ScalarOrArray>) -> Array = where_;
1364        /// The elements converted to `dtype`. Float-to-integer conversion saturates (NaN
1365        /// becomes 0); conversion to bool tests for non-zero; complex-to-real keeps the real
1366        /// part.
1367        ///
1368        /// ```
1369        /// use nuvec_core::{DType, array};
1370        ///
1371        /// assert_eq!(array![1.7, -2.2].astype(DType::I32).to_vec::<i32>().unwrap(), [1, -2]);
1372        /// ```
1373        fn astype(&self, dtype: DType) -> Array;
1374        /// The matrix product `self @ other` (`[..., M, K] @ [..., K, N]`, batch dimensions
1375        /// broadcast; 1-D operands as in NumPy). The promoted type must be floating point.
1376        ///
1377        /// ```
1378        /// use nuvec_core::array;
1379        ///
1380        /// let a = array![[1.0, 2.0], [3.0, 4.0]];
1381        /// assert_eq!(a.matmul(&a.t()).to_vec::<f32>().unwrap(), [5.0, 11.0, 11.0, 25.0]);
1382        /// let bad = a.matmul(&array![1.0, 2.0, 3.0]);
1383        /// assert!(bad.is_failed());
1384        /// ```
1385        ///
1386        /// With at most [`ROW_INDEPENDENT_ROWS`](crate::ROW_INDEPENDENT_ROWS) rows, each
1387        /// row of the result has the bits of that row's product alone (multi-row
1388        /// matrix-vector kernels on the GPU).
1389        ///
1390        /// # Memory
1391        ///
1392        /// No scratch, except split-K GEMM on the GPU (few output tiles over a long K):
1393        /// `partitions × M × N × 4` bytes of float32 partial products, `partitions` being
1394        /// `K / 16 / tiles` rounded up to a power of two in `2..=32` (`tiles = ⌈M/32⌉·⌈N/32⌉`).
1395        /// Operands that are neither row- nor column-major are copied first.
1396        fn matmul(&self, other: &Array) -> Array;
1397        /// `alpha · (a @ b) + beta · self`, with `self` broadcast to the product's shape
1398        /// (MLX's `addmm`; one GEMM with an epilogue on the GPU).
1399        ///
1400        /// ```
1401        /// use nuvec_core::array;
1402        ///
1403        /// let bias = array![1.0, 1.0];
1404        /// let y = bias.addmm(&array![[1.0, 2.0]], &array![[1.0, 0.0], [0.0, 1.0]], 1.0, 1.0);
1405        /// assert_eq!(y.to_vec::<f32>().unwrap(), [2.0, 3.0]);
1406        /// ```
1407        fn addmm(&self, a: &Array, b: &Array, alpha: f32, beta: f32) -> Array;
1408    }
1409
1410    /// # Reductions
1411    ///
1412    /// Over [`Axes`]: `-1`, `[0, 2]`, `..` (all), `1..` (trailing); an empty list reduces
1413    /// nothing. Each has a `*_keepdims` twin that keeps the reduced axes as size-1 dimensions.
1414    impl {
1415        /// The sum over `axes`. Integers and bools widen to 32 or 64 bits, as in MLX.
1416        ///
1417        /// ```
1418        /// use nuvec_core::array;
1419        ///
1420        /// let x = array![[1, 2, 3], [4, 5, 6]];
1421        /// assert_eq!(x.sum(-1).to_vec::<i32>().unwrap(), [6, 15]);
1422        /// assert_eq!(x.sum(..).item::<i32>().unwrap(), 21);
1423        /// ```
1424        fn sum(&self, axes: impl Axes) -> Array;
1425        /// [`Array::sum`] keeping the reduced axes as size-1 dimensions.
1426        ///
1427        /// ```
1428        /// use nuvec_core::array;
1429        ///
1430        /// assert_eq!(array![[1, 2, 3], [4, 5, 6]].sum_keepdims(0).shape(), [1, 3]);
1431        /// ```
1432        fn sum_keepdims(&self, axes: impl Axes) -> Array;
1433        /// The product over `axes`, widened as [`Array::sum`].
1434        ///
1435        /// ```
1436        /// use nuvec_core::array;
1437        ///
1438        /// assert_eq!(array![2, 3, 4].prod(..).item::<i32>().unwrap(), 24);
1439        /// ```
1440        fn prod(&self, axes: impl Axes) -> Array;
1441        /// [`Array::prod`] keeping the reduced axes.
1442        fn prod_keepdims(&self, axes: impl Axes) -> Array;
1443        /// The maximum over `axes`; NaN propagates; zero-length axes fail.
1444        ///
1445        /// ```
1446        /// use nuvec_core::array;
1447        ///
1448        /// assert_eq!(array![[1, 5], [7, 2]].max(1).to_vec::<i32>().unwrap(), [5, 7]);
1449        /// ```
1450        fn max(&self, axes: impl Axes) -> Array;
1451        /// [`Array::max`] keeping the reduced axes.
1452        fn max_keepdims(&self, axes: impl Axes) -> Array;
1453        /// The minimum over `axes`; NaN propagates; zero-length axes fail.
1454        fn min(&self, axes: impl Axes) -> Array;
1455        /// [`Array::min`] keeping the reduced axes.
1456        fn min_keepdims(&self, axes: impl Axes) -> Array;
1457        /// Whether every element over `axes` is non-zero.
1458        ///
1459        /// ```
1460        /// use nuvec_core::array;
1461        ///
1462        /// assert!(!array![true, false].all(..).item::<bool>().unwrap());
1463        /// ```
1464        fn all(&self, axes: impl Axes) -> Array;
1465        /// [`Array::all`] keeping the reduced axes.
1466        fn all_keepdims(&self, axes: impl Axes) -> Array;
1467        /// Whether any element over `axes` is non-zero.
1468        fn any(&self, axes: impl Axes) -> Array;
1469        /// [`Array::any`] keeping the reduced axes.
1470        fn any_keepdims(&self, axes: impl Axes) -> Array;
1471        /// The mean over `axes`, in at least float32. Empty reductions are NaN.
1472        ///
1473        /// ```
1474        /// use nuvec_core::array;
1475        ///
1476        /// assert_eq!(array![1, 2, 3, 4].mean(..).item::<f32>().unwrap(), 2.5);
1477        /// ```
1478        fn mean(&self, axes: impl Axes) -> Array;
1479        /// [`Array::mean`] keeping the reduced axes.
1480        fn mean_keepdims(&self, axes: impl Axes) -> Array;
1481        /// `log(sum(exp(self)))` over `axes`, computed stably (the maximum subtracted first).
1482        ///
1483        /// ```
1484        /// use nuvec_core::array;
1485        ///
1486        /// let l = array![0.0, 0.0].logsumexp(..).item::<f32>().unwrap();
1487        /// assert!((l - 2f32.ln()).abs() < 1e-6);
1488        /// ```
1489        fn logsumexp(&self, axes: impl Axes) -> Array;
1490        /// [`Array::logsumexp`] keeping the reduced axes.
1491        fn logsumexp_keepdims(&self, axes: impl Axes) -> Array;
1492        /// The variance over `axes` (`mean((x − mean(x))²)`), in at least float32.
1493        ///
1494        /// ```
1495        /// use nuvec_core::{VarOptions, array};
1496        ///
1497        /// let x = array![1.0, 2.0, 3.0, 4.0];
1498        /// assert_eq!(x.var(..).item::<f32>().unwrap(), 1.25);
1499        /// let sample = x.var_with(.., VarOptions { ddof: 1, ..Default::default() });
1500        /// assert!((sample.item::<f32>().unwrap() - 5.0 / 3.0).abs() < 1e-6);
1501        /// ```
1502        fn var(&self, axes: impl Axes) -> Array;
1503        /// [`Array::var`] keeping the reduced axes.
1504        fn var_keepdims(&self, axes: impl Axes) -> Array;
1505        /// [`Array::var`] with [`VarOptions`] (`ddof`, `keepdims`).
1506        fn var_with(&self, axes: impl Axes, options: VarOptions) -> Array;
1507        /// The standard deviation over `axes` (the square root of [`Array::var`]).
1508        ///
1509        /// ```
1510        /// use nuvec_core::array;
1511        ///
1512        /// assert_eq!(array![1.0, 3.0].std(..).item::<f32>().unwrap(), 1.0);
1513        /// ```
1514        fn std(&self, axes: impl Axes) -> Array;
1515        /// [`Array::std`] keeping the reduced axes.
1516        fn std_keepdims(&self, axes: impl Axes) -> Array;
1517        /// [`Array::std`] with [`VarOptions`] (`ddof`, `keepdims`).
1518        fn std_with(&self, axes: impl Axes, options: VarOptions) -> Array;
1519        /// The index (uint32) of the maximum along one [`Axis`] (`..`: of the flattened
1520        /// array). The first occurrence wins; the first NaN wins over any number.
1521        ///
1522        /// ```
1523        /// use nuvec_core::array;
1524        ///
1525        /// let x = array![[1, 9, 3], [7, 2, 8]];
1526        /// assert_eq!(x.argmax(-1).to_vec::<u32>().unwrap(), [1, 2]);
1527        /// assert_eq!(x.argmax(..).item::<u32>().unwrap(), 1);
1528        /// ```
1529        fn argmax(&self, axis: impl Axis) -> Array;
1530        /// [`Array::argmax`] keeping the reduced axis (every axis for `..`).
1531        fn argmax_keepdims(&self, axis: impl Axis) -> Array;
1532        /// The index (uint32) of the minimum along one axis (`..`: of the flattened array).
1533        fn argmin(&self, axis: impl Axis) -> Array;
1534        /// [`Array::argmin`] keeping the reduced axis (every axis for `..`).
1535        fn argmin_keepdims(&self, axis: impl Axis) -> Array;
1536        /// `exp(self) / sum(exp(self))` over `axes`, with the maximum subtracted first.
1537        ///
1538        /// ```
1539        /// use nuvec_core::array;
1540        ///
1541        /// assert_eq!(array![1.0, 1.0].softmax(-1).to_vec::<f32>().unwrap(), [0.5, 0.5]);
1542        /// ```
1543        fn softmax(&self, axes: impl Axes) -> Array;
1544    }
1545
1546    /// # Scans and sorting
1547    ///
1548    /// Along one [`Axis`]; `..` works on the flattened array.
1549    impl {
1550        /// The cumulative sum along one axis. Booleans sum as int32.
1551        ///
1552        /// ```
1553        /// use nuvec_core::{ScanOptions, array};
1554        ///
1555        /// let a = array![1, 2, 3, 4];
1556        /// assert_eq!(a.cumsum(0).to_vec::<i32>().unwrap(), [1, 3, 6, 10]);
1557        /// let opts = ScanOptions { reverse: true, exclusive: true };
1558        /// assert_eq!(a.cumsum_with(0, opts).to_vec::<i32>().unwrap(), [9, 7, 4, 0]);
1559        /// ```
1560        #[doc(alias = "cumulative_sum")]
1561        fn cumsum(&self, axis: impl Axis) -> Array;
1562        /// [`Array::cumsum`] with [`ScanOptions`] (`reverse`, `exclusive`).
1563        fn cumsum_with(&self, axis: impl Axis, options: ScanOptions) -> Array;
1564        /// The cumulative product along one axis.
1565        ///
1566        /// ```
1567        /// use nuvec_core::array;
1568        ///
1569        /// assert_eq!(array![1, 2, 3].cumprod(..).to_vec::<i32>().unwrap(), [1, 2, 6]);
1570        /// ```
1571        #[doc(alias = "cumulative_prod")]
1572        fn cumprod(&self, axis: impl Axis) -> Array;
1573        /// [`Array::cumprod`] with [`ScanOptions`].
1574        fn cumprod_with(&self, axis: impl Axis, options: ScanOptions) -> Array;
1575        /// The running maximum along one axis (NaN propagates).
1576        fn cummax(&self, axis: impl Axis) -> Array;
1577        /// [`Array::cummax`] with [`ScanOptions`].
1578        fn cummax_with(&self, axis: impl Axis, options: ScanOptions) -> Array;
1579        /// The running minimum along one axis (NaN propagates).
1580        fn cummin(&self, axis: impl Axis) -> Array;
1581        /// [`Array::cummin`] with [`ScanOptions`].
1582        fn cummin_with(&self, axis: impl Axis, options: ScanOptions) -> Array;
1583        /// The running `log(Σ exp)` along one axis, in floating point.
1584        fn logcumsumexp(&self, axis: impl Axis) -> Array;
1585        /// [`Array::logcumsumexp`] with [`ScanOptions`].
1586        fn logcumsumexp_with(&self, axis: impl Axis, options: ScanOptions) -> Array;
1587        /// The array sorted along one axis: ascending, stable, NaN last.
1588        ///
1589        /// ```
1590        /// use nuvec_core::array;
1591        ///
1592        /// assert_eq!(array![3, 1, 2].sort(-1).to_vec::<i32>().unwrap(), [1, 2, 3]);
1593        /// ```
1594        fn sort(&self, axis: impl Axis) -> Array;
1595        /// The indices (uint32) that sort the array along one axis, as [`Array::sort`].
1596        ///
1597        /// ```
1598        /// use nuvec_core::array;
1599        ///
1600        /// assert_eq!(array![3, 1, 2].argsort(0).to_vec::<u32>().unwrap(), [1, 2, 0]);
1601        /// ```
1602        fn argsort(&self, axis: impl Axis) -> Array;
1603        /// The array partitioned along one axis: the element at `kth` (negative counts from
1604        /// the end) is the one a sort would put there, smaller ones before, larger ones after.
1605        fn partition(&self, kth: i32, axis: impl Axis) -> Array;
1606        /// The indices (uint32) that partition the array, as [`Array::partition`].
1607        fn argpartition(&self, kth: i32, axis: impl Axis) -> Array;
1608        /// The `k` largest elements along one axis, in ascending order.
1609        ///
1610        /// ```
1611        /// use nuvec_core::array;
1612        ///
1613        /// assert_eq!(array![5, 1, 9, 3, 7].topk(2, 0).to_vec::<i32>().unwrap(), [7, 9]);
1614        /// ```
1615        fn topk(&self, k: i32, axis: impl Axis) -> Array;
1616    }
1617
1618    /// # Shapes and views
1619    impl {
1620        /// The same elements in another shape ([`IntoShape`]: `[b, l, -1]`, one `-1`
1621        /// inferred). Shares the data when the layout permits.
1622        ///
1623        /// ```
1624        /// use nuvec_core::array;
1625        ///
1626        /// let x = array![[1, 2, 3], [4, 5, 6]];
1627        /// let [b, n] = x.shape_n::<2>().unwrap();
1628        /// assert_eq!(x.reshape([n, b]).shape(), [3, 2]);
1629        /// assert_eq!(x.reshape([-1]).shape(), [6]);
1630        /// ```
1631        fn reshape(&self, shape: impl IntoShape) -> Array;
1632        /// The axes reordered: output axis `i` is input axis `axes[i]` (`..` reverses them, as
1633        /// MLX's default). Shares the data.
1634        ///
1635        /// ```
1636        /// use nuvec_core::{DType, ops::zeros};
1637        ///
1638        /// let x = zeros([2, 3, 4], DType::F32);
1639        /// assert_eq!(x.transpose([0, 2, 1]).shape(), [2, 4, 3]);
1640        /// assert_eq!(x.transpose(..).shape(), [4, 3, 2]);
1641        /// ```
1642        #[doc(alias = "permute_dims")]
1643        fn transpose(&self, axes: impl Axes) -> Array;
1644        /// The array presented as the larger `shape` (NumPy broadcasting). Shares the data.
1645        ///
1646        /// ```
1647        /// use nuvec_core::array;
1648        ///
1649        /// assert_eq!(array![1, 2].broadcast_to([3, 2]).shape(), [3, 2]);
1650        /// ```
1651        fn broadcast_to(&self, shape: impl IntoShape) -> Array;
1652        /// Size-1 dimensions inserted at `axes` (positions in the result). Shares the data.
1653        ///
1654        /// ```
1655        /// use nuvec_core::array;
1656        ///
1657        /// assert_eq!(array![1, 2].expand_dims([0, -1]).shape(), [1, 2, 1]);
1658        /// ```
1659        fn expand_dims(&self, axes: impl Axes) -> Array;
1660        /// Size-1 dimensions removed: the given `axes`, or all of them for `..`. Shares the
1661        /// data.
1662        ///
1663        /// ```
1664        /// use nuvec_core::array;
1665        ///
1666        /// assert_eq!(array![[[1], [2]]].squeeze(..).shape(), [2]);
1667        /// ```
1668        fn squeeze(&self, axes: impl Axes) -> Array;
1669        /// Each element repeated `repeats` times along one axis (`..`: the flattened array).
1670        ///
1671        /// ```
1672        /// use nuvec_core::array;
1673        ///
1674        /// assert_eq!(array![1, 2].repeat(2, 0).to_vec::<i32>().unwrap(), [1, 1, 2, 2]);
1675        /// ```
1676        fn repeat(&self, repeats: i32, axis: impl Axis) -> Array;
1677        /// The array split into `parts` equal parts along `axis` (views); `parts` failed
1678        /// arrays if the axis does not divide.
1679        ///
1680        /// ```
1681        /// use nuvec_core::array;
1682        ///
1683        /// let parts = array![1, 2, 3, 4, 5, 6].split(3, 0);
1684        /// assert_eq!(parts[2].to_vec::<i32>().unwrap(), [5, 6]);
1685        /// ```
1686        fn split(&self, parts: i32, axis: impl Axis) -> Vec<Array>;
1687        /// The array split before each of the (non-decreasing) positions `indices` along
1688        /// `axis`: `indices.len() + 1` views.
1689        ///
1690        /// ```
1691        /// use nuvec_core::array;
1692        ///
1693        /// let parts = array![1, 2, 3, 4].split_at(&[1, 3], 0);
1694        /// assert_eq!(parts[1].to_vec::<i32>().unwrap(), [2, 3]);
1695        /// ```
1696        fn split_at(&self, indices: &[i32], axis: impl Axis) -> Vec<Array>;
1697    }
1698
1699    /// # Gathers and scatters
1700    ///
1701    /// See also [`Array::i`] and [`Array::at`].
1702    impl {
1703        /// The elements at `indices` along one axis (`..`: the flattened array): the axis is
1704        /// replaced by `indices`' dimensions (MLX's `take`). Negative indices count from the
1705        /// end.
1706        ///
1707        /// ```
1708        /// use nuvec_core::array;
1709        ///
1710        /// let table = array![[0.0, 0.5], [1.0, 1.5], [2.0, 2.5]];
1711        /// let rows = table.take(&array![2, 0], 0);
1712        /// assert_eq!(rows.to_vec::<f32>().unwrap(), [2.0, 2.5, 0.0, 0.5]);
1713        /// ```
1714        fn take(&self, indices: &Array, axis: impl Axis) -> Array;
1715        /// The elements at `indices` along `axis` (MLX's `take_along_axis`).
1716        ///
1717        /// ```
1718        /// use nuvec_core::array;
1719        ///
1720        /// let a = array![[10.0, 20.0], [30.0, 40.0]];
1721        /// let i = array![[1], [0]];
1722        /// assert_eq!(a.take_along_axis(&i, 1).to_vec::<f32>().unwrap(), [20.0, 30.0]);
1723        /// ```
1724        fn take_along_axis(&self, indices: &Array, axis: impl Axis) -> Array;
1725        /// A copy with `values` placed at `indices` along `axis` (MLX's `put_along_axis`).
1726        ///
1727        /// ```
1728        /// use nuvec_core::array;
1729        ///
1730        /// let a = array![0, 0, 0, 0];
1731        /// let b = a.put_along_axis(&array![3, -4], &array![7, 9], 0);
1732        /// assert_eq!(b.to_vec::<i32>().unwrap(), [9, 0, 0, 7]);
1733        /// ```
1734        fn put_along_axis(&self, indices: &Array, values: impl Into<ScalarOrArray>, axis: impl Axis) -> Array;
1735        /// A copy with `values` added at `indices` along `axis`; repeated indices accumulate.
1736        ///
1737        /// ```
1738        /// use nuvec_core::array;
1739        ///
1740        /// let a = array![0, 0];
1741        /// assert_eq!(a.scatter_add_along_axis(&array![1, 1], 5, 0).to_vec::<i32>().unwrap(), [0, 10]);
1742        /// ```
1743        fn scatter_add_along_axis(&self, indices: &Array, values: impl Into<ScalarOrArray>, axis: impl Axis) -> Array;
1744    }
1745
1746    /// # Quantization and convolution
1747    impl {
1748        /// The matrix quantized along its last axis as MLX's affine quantization:
1749        /// `(w_q, scales, biases)`.
1750        ///
1751        /// ```
1752        /// use nuvec_core::{DType, ops::ones};
1753        ///
1754        /// let (wq, scales, biases) = ones([2, 64], DType::F32).quantize(64, 4);
1755        /// assert_eq!((wq.shape(), scales.shape(), biases.shape()), (&[2, 8][..], &[2, 1][..], &[2, 1][..]));
1756        /// ```
1757        fn quantize(&self, group_size: i32, bits: i32) -> (Array, Array, Array);
1758        /// The weights of [`Array::quantize`] restored from `self` (the packed weights).
1759        fn dequantize(&self, scales: &Array, biases: &Array, group_size: i32, bits: i32) -> Array;
1760        /// `self · dequantize(w)ᵀ` (`transpose`) or `self · dequantize(w)`. Up to
1761        /// [`ROW_INDEPENDENT_ROWS`](crate::ROW_INDEPENDENT_ROWS) rows run as
1762        /// matrix-vector products, each row with the bits of its own product.
1763        ///
1764        /// # Memory
1765        ///
1766        /// No scratch, except quantized split-K (many rows, transposed weights, one batch):
1767        /// `split × M × N × 4` bytes, `split = min(512 / tiles, K / max(group, 32))`
1768        /// reduced until it divides `K` (`tiles = ⌈M/32⌉·⌈N/32⌉`).
1769        fn quantized_matmul(&self, w: &Array, scales: &Array, biases: &Array, transpose: bool, group_size: i32, bits: i32) -> Array;
1770        /// Quantized products of gathered matrices (MLX's `gather_qmm`). Up to
1771        /// `ROW_INDEPENDENT_ROWS` tokens of up to 8 experts run one product per row, sorted
1772        /// or not, each with the bits of its own product.
1773        ///
1774        /// # Memory
1775        ///
1776        /// With sorted indices over many rows: the experts' row counts and offsets (int32,
1777        /// two per expert) and an int32 per row; otherwise none.
1778        fn gather_qmm(&self, w: &Array, scales: &Array, biases: &Array, lhs_indices: Option<&Array>, rhs_indices: Option<&Array>, transpose: bool, group_size: i32, bits: i32, sorted_indices: bool) -> Array;
1779        /// Products of gathered matrices (MLX's `gather_mm`). One row per output batch
1780        /// (up to `ROW_INDEPENDENT_ROWS` tokens of up to 8 experts) runs MLX's gathered
1781        /// matrix-vector kernels, each row with the bits of its own product.
1782        ///
1783        /// # Memory
1784        ///
1785        /// As [`Array::gather_qmm`]: the sorted path's counts and offsets; otherwise none
1786        /// (operands that are not row-major are copied first).
1787        fn gather_mm(&self, b: &Array, lhs_indices: Option<&Array>, rhs_indices: Option<&Array>, sorted_indices: bool) -> Array;
1788        /// A 1-D convolution of `self` (`[N, L, C_in]`) with `weight` (`[C_out, K, C_in /
1789        /// groups]`).
1790        ///
1791        /// ```
1792        /// use nuvec_core::array;
1793        ///
1794        /// let x = array![[[1.0], [2.0], [3.0]]]; // [1, 3, 1]
1795        /// let w = array![[[1.0], [1.0]]]; // [1, 2, 1]
1796        /// assert_eq!(x.conv1d(&w, 1, 0, 1, 1).to_vec::<f32>().unwrap(), [3.0, 5.0]);
1797        /// ```
1798        fn conv1d(&self, weight: &Array, stride: i32, padding: i32, dilation: i32, groups: i32) -> Array;
1799    }
1800
1801    /// # Creation from an existing array
1802    impl {
1803        /// Zeros of this array's shape and dtype.
1804        ///
1805        /// ```
1806        /// use nuvec_core::array;
1807        ///
1808        /// assert_eq!(array![1u8, 2].zeros_like().to_vec::<u8>().unwrap(), [0, 0]);
1809        /// ```
1810        fn zeros_like(&self) -> Array;
1811        /// Ones of this array's shape and dtype.
1812        fn ones_like(&self) -> Array;
1813        /// `value` (a weak scalar takes this array's dtype) in this array's shape and dtype.
1814        ///
1815        /// ```
1816        /// use nuvec_core::array;
1817        ///
1818        /// assert_eq!(array![1.0, 2.0].full_like(7).to_vec::<f32>().unwrap(), [7.0, 7.0]);
1819        /// ```
1820        fn full_like(&self, value: impl Into<ScalarOrArray>) -> Array;
1821    }
1822}
1823
1824/// # Shortcuts
1825impl Array {
1826    /// The array with all axes reversed (the matrix transpose for 2-D arrays). Shares the
1827    /// data.
1828    ///
1829    /// ```
1830    /// use nuvec_core::array;
1831    ///
1832    /// assert_eq!(array![[1, 2, 3]].t().shape(), [3, 1]);
1833    /// ```
1834    #[track_caller]
1835    pub fn t(&self) -> Array {
1836        transpose(self, ..)
1837    }
1838
1839    /// The array split into `N` equal parts along `axis`, for destructuring:
1840    /// `let [q, gate] = qg.split_n::<2>(-1);`. `N` failed arrays if the axis does not divide.
1841    ///
1842    /// ```
1843    /// use nuvec_core::array;
1844    ///
1845    /// let qg = array![[1.0, 2.0, 3.0, 4.0]];
1846    /// let [q, g] = qg.split_n::<2>(-1);
1847    /// assert_eq!((q.shape(), g.to_vec::<f32>().unwrap()), (&[1, 2][..], vec![3.0, 4.0]));
1848    /// ```
1849    #[track_caller]
1850    pub fn split_n<const N: usize>(&self, axis: impl Axis) -> [Array; N] {
1851        split_n::<N>(self, axis)
1852    }
1853}
1854
1855#[cfg(test)]
1856mod tests {
1857    use super::*;
1858
1859    #[test]
1860    fn located_failures() {
1861        let a = zeros([2, 3], DType::F32);
1862        let b = zeros([4, 5], DType::F32);
1863        let line = line!() + 1;
1864        let c = a.matmul(&b);
1865        let err = c.clone().check().unwrap_err();
1866        assert_eq!(err.op_name(), Some("matmul"));
1867        let at = err.location().unwrap();
1868        assert!(at.file().ends_with("ops.rs") && at.line() == line, "{err}");
1869        // Sticky: the first error wins.
1870        let d = (c.exp() + 1.0).sum(-1);
1871        assert_eq!(d.check().unwrap_err(), err);
1872    }
1873
1874    #[test]
1875    fn empty_axes_reduce_nothing() {
1876        let x = Array::from_slice(&[1i8, 2, 3, 4], [2, 2]);
1877        let s = x.sum([]);
1878        assert_eq!((s.shape(), s.dtype()), (&[2, 2][..], DType::I8));
1879        assert_eq!(x.all(Vec::<i32>::new()).dtype(), DType::Bool);
1880        assert_eq!(x.mean([]).dtype(), DType::F32);
1881        assert_eq!(x.sum(..).item::<i32>().unwrap(), 10);
1882        // Integers never wrap in the variance (computed in float).
1883        let v = Array::from([100i8, -100, 50])
1884            .var(..)
1885            .item::<f32>()
1886            .unwrap();
1887        assert!((v - 7222.222).abs() < 0.01, "{v}");
1888    }
1889}