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}