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