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