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