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}