Skip to main content

mlx_rs/
linalg.rs

1//! Linear algebra operations.
2//!
3//! At this MLX pin, these operations are CPU-only; select the CPU stream by running them inside
4//! [`crate::with_stream`] with [`crate::Stream::cpu`].
5
6use crate::error::{Exception, Result};
7use crate::utils::guard::Guarded;
8use crate::utils::{IntoOption, VectorArray};
9use crate::{with_stream, Array, Axes, Stream};
10use mlx_internal_macros::generate_macro;
11use smallvec::SmallVec;
12use std::ffi::CString;
13
14/// Order of the norm
15///
16/// See [`norm`] for more details.
17#[derive(Debug, Clone, Copy)]
18pub enum Ord<'a> {
19    /// String representation of the order
20    Str(&'a str),
21
22    /// Order of the norm
23    P(f64),
24}
25
26impl Default for Ord<'_> {
27    fn default() -> Self {
28        Ord::Str("fro")
29    }
30}
31
32impl std::fmt::Display for Ord<'_> {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        match self {
35            Ord::Str(s) => write!(f, "{s}"),
36            Ord::P(p) => write!(f, "{p}"),
37        }
38    }
39}
40
41impl<'a> From<&'a str> for Ord<'a> {
42    fn from(value: &'a str) -> Self {
43        Ord::Str(value)
44    }
45}
46
47impl From<f64> for Ord<'_> {
48    fn from(value: f64) -> Self {
49        Ord::P(value)
50    }
51}
52
53impl<'a> IntoOption<Ord<'a>> for &'a str {
54    fn into_option(self) -> Option<Ord<'a>> {
55        Some(Ord::Str(self))
56    }
57}
58
59impl<'a> IntoOption<Ord<'a>> for f64 {
60    fn into_option(self) -> Option<Ord<'a>> {
61        Some(Ord::P(self))
62    }
63}
64
65/// The sign and natural logarithm of the absolute determinant.
66#[derive(Debug, Clone)]
67pub struct SlogDet {
68    /// Determinant sign.
69    pub sign: Array,
70
71    /// Natural logarithm of the absolute determinant.
72    pub log_abs_det: Array,
73}
74
75/// Compute the determinant of square matrices.
76///
77/// This operation is CPU-only at the pinned MLX version and uses the ambient stream. Integer
78/// inputs are promoted to floating point and leading dimensions are treated as batches.
79///
80/// ```rust
81/// use mlx_rs::{array, linalg, with_stream, Stream};
82///
83/// let matrix = array!([[1.0, 2.0], [3.0, 4.0]]);
84/// let result = with_stream(&Stream::cpu(), || linalg::det(&matrix)).unwrap();
85/// assert!(result.shape().is_empty());
86/// ```
87pub fn det(array: impl AsRef<Array>) -> Result<Array> {
88    let stream = Stream::thread_local_or_default();
89    Array::try_from_op(|res| unsafe {
90        mlx_sys::mlx_linalg_det(res, array.as_ref().as_ptr(), stream.as_ref().as_ptr())
91    })
92}
93
94/// Compute determinant sign and log absolute determinant for square matrices.
95///
96/// This operation is CPU-only at the pinned MLX version and uses the ambient stream. A singular
97/// matrix returns zero sign and negative-infinity `log_abs_det`.
98///
99/// ```rust
100/// use mlx_rs::{array, linalg, with_stream, Stream};
101///
102/// let matrix = array!([[1.0, 2.0], [3.0, 4.0]]);
103/// let result = with_stream(&Stream::cpu(), || linalg::slogdet(&matrix)).unwrap();
104/// assert!(result.sign.shape().is_empty());
105/// assert!(result.log_abs_det.shape().is_empty());
106/// ```
107pub fn slogdet(array: impl AsRef<Array>) -> Result<SlogDet> {
108    let stream = Stream::thread_local_or_default();
109    let (sign, log_abs_det) =
110        <(Array, Array) as Guarded>::try_from_op(|(sign, log_abs_det)| unsafe {
111            mlx_sys::mlx_linalg_slogdet(
112                sign,
113                log_abs_det,
114                array.as_ref().as_ptr(),
115                stream.as_ref().as_ptr(),
116            )
117        })?;
118    Ok(SlogDet { sign, log_abs_det })
119}
120
121/// Axis selection and independent defaults for norm operations.
122#[derive(Debug, Clone, Default, PartialEq, Eq)]
123pub struct NormOptions {
124    /// Axes to reduce.
125    pub axes: Axes,
126
127    /// Keep reduced axes as singleton dimensions.
128    pub keep_dims: bool,
129}
130fn with_norm_axes<T>(axes: &Axes, f: impl FnOnce(*const i32, usize) -> T) -> T {
131    match axes {
132        Axes::All => f(std::ptr::null(), 0),
133        Axes::Axis(axis) => f(axis, 1),
134        Axes::Axes(axes) => f(axes.as_ptr(), axes.len()),
135    }
136}
137fn legacy_norm_options(axes: Option<&[i32]>, keep_dims: Option<bool>) -> NormOptions {
138    NormOptions {
139        axes: axes.map_or(Axes::All, Axes::from),
140        keep_dims: keep_dims.unwrap_or(false),
141    }
142}
143
144/// Compute p-norm of an [`Array`]
145pub fn norm(array: impl AsRef<Array>, ord: f64, options: NormOptions) -> Result<Array> {
146    let stream = Stream::thread_local_or_default();
147    with_norm_axes(&options.axes, |axes, num_axes| {
148        Array::try_from_op(|res| unsafe {
149            mlx_sys::mlx_linalg_norm(
150                res,
151                array.as_ref().as_ptr(),
152                ord,
153                axes,
154                num_axes,
155                options.keep_dims,
156                stream.as_ref().as_ptr(),
157            )
158        })
159    })
160}
161
162/// Compatibility shim for [`norm`].
163#[generate_macro(customize(forwarding_shim = true, root = "$crate::linalg"))]
164#[deprecated(
165    since = "0.26.0",
166    note = "use `with_stream` or `with_device` around `norm` with `NormOptions`"
167)]
168pub fn norm_device<'a>(
169    array: impl AsRef<Array>,
170    ord: f64,
171    #[optional] axes: impl IntoOption<&'a [i32]>,
172    #[optional] keep_dims: impl Into<Option<bool>>,
173    #[optional] stream: impl AsRef<Stream>,
174) -> Result<Array> {
175    let options = legacy_norm_options(axes.into_option(), keep_dims.into());
176    with_stream(stream.as_ref(), || norm(array, ord, options))
177}
178
179/// Matrix or vector norm.
180pub fn norm_matrix(array: impl AsRef<Array>, ord: &str, options: NormOptions) -> Result<Array> {
181    let ord = CString::new(ord).map_err(|e| Exception::custom(format!("{e}")))?;
182    let stream = Stream::thread_local_or_default();
183    with_norm_axes(&options.axes, |axes, num_axes| {
184        Array::try_from_op(|res| unsafe {
185            mlx_sys::mlx_linalg_norm_matrix(
186                res,
187                array.as_ref().as_ptr(),
188                ord.as_ptr(),
189                axes,
190                num_axes,
191                options.keep_dims,
192                stream.as_ref().as_ptr(),
193            )
194        })
195    })
196}
197
198/// Compatibility shim for [`norm_matrix`].
199#[generate_macro(customize(forwarding_shim = true, root = "$crate::linalg"))]
200#[deprecated(
201    since = "0.26.0",
202    note = "use `with_stream` or `with_device` around `norm_matrix` with `NormOptions`"
203)]
204pub fn norm_matrix_device<'a>(
205    array: impl AsRef<Array>,
206    ord: &'a str,
207    #[optional] axes: impl IntoOption<&'a [i32]>,
208    #[optional] keep_dims: impl Into<Option<bool>>,
209    #[optional] stream: impl AsRef<Stream>,
210) -> Result<Array> {
211    let options = legacy_norm_options(axes.into_option(), keep_dims.into());
212    with_stream(stream.as_ref(), || norm_matrix(array, ord, options))
213}
214
215/// Compute the L2 norm of an [`Array`]
216pub fn norm_l2(array: impl AsRef<Array>, options: NormOptions) -> Result<Array> {
217    let stream = Stream::thread_local_or_default();
218    with_norm_axes(&options.axes, |axes, num_axes| {
219        Array::try_from_op(|res| unsafe {
220            mlx_sys::mlx_linalg_norm_l2(
221                res,
222                array.as_ref().as_ptr(),
223                axes,
224                num_axes,
225                options.keep_dims,
226                stream.as_ref().as_ptr(),
227            )
228        })
229    })
230}
231
232/// Compatibility shim for [`norm_l2`].
233#[generate_macro(customize(forwarding_shim = true, root = "$crate::linalg"))]
234#[deprecated(
235    since = "0.26.0",
236    note = "use `with_stream` or `with_device` around `norm_l2` with `NormOptions`"
237)]
238pub fn norm_l2_device<'a>(
239    array: impl AsRef<Array>,
240    #[optional] axes: impl IntoOption<&'a [i32]>,
241    #[optional] keep_dims: impl Into<Option<bool>>,
242    #[optional] stream: impl AsRef<Stream>,
243) -> Result<Array> {
244    let options = legacy_norm_options(axes.into_option(), keep_dims.into());
245    with_stream(stream.as_ref(), || norm_l2(array, options))
246}
247// TODO: Change the original `norm` function to use builder pattern
248// /// Matrix or vector norm.
249// ///
250// /// For values of `ord < 1`, the result is, strictly speaking, not a
251// /// mathematical norm, but it may still be useful for various numerical
252// /// purposes.
253// ///
254// /// The following norms can be calculated:
255// ///
256// /// ord   | norm for matrices            | norm for vectors
257// /// ----- | ---------------------------- | --------------------------
258// /// None  | Frobenius norm               | 2-norm
259// /// 'fro' | Frobenius norm               | --
260// /// inf   | max(sum(abs(x), axis-1))     | max(abs(x))
261// /// -inf  | min(sum(abs(x), axis-1))     | min(abs(x))
262// /// 0     | --                           | sum(x !- 0)
263// /// 1     | max(sum(abs(x), axis-0))     | as below
264// /// -1    | min(sum(abs(x), axis-0))     | as below
265// /// 2     | 2-norm (largest sing. value) | as below
266// /// -2    | smallest singular value      | as below
267// /// other | --                           | sum(abs(x)**ord)**(1./ord)
268// ///
269// /// > Nuclear norm and norms based on singular values are not yet implemented.
270// ///
271// /// The Frobenius norm is given by G. H. Golub and C. F. Van Loan, *Matrix Computations*,
272// ///        Baltimore, MD, Johns Hopkins University Press, 1985, pg. 15
273// ///
274// /// The nuclear norm is the sum of the singular values.
275// ///
276// /// Both the Frobenius and nuclear norm orders are only defined for
277// /// matrices and produce a fatal error when `array.ndim != 2`
278// ///
279// /// # Params
280// ///
281// /// - `array`: input array
282// /// - `ord`: order of the norm, see table
283// /// - `axes`: axes that hold 2d matrices
284// /// - `keep_dims`: if `true` the axes which are normed over are left in the result as dimensions
285// ///   with size one
286// #[generate_macro(customize(forwarding_shim = true, root = "$crate::linalg"))]
287// #[default_device]
288// pub fn norm_device<'a>(
289//     array: impl AsRef<Array>,
290//     #[optional] ord: impl IntoOption<Ord<'a>>,
291//     #[optional] axes: impl IntoOption<&'a [i32]>,
292//     #[optional] keep_dims: impl Into<Option<bool>>,
293//     #[optional] stream: impl AsRef<Stream>,
294// ) -> Result<Array> {
295//     let ord = ord.into_option();
296//     let axes = axes.into_option();
297//     let keep_dims = keep_dims.into().unwrap_or(false);
298
299//     match (ord, axes) {
300//         // If axis and ord are both unspecified, computes the 2-norm of flatten(x).
301//         (None, None) => {
302//             let axes_ptr = std::ptr::null(); // mlx-c already handles the case where axes is null
303//             Array::try_from_op(|res| unsafe {
304//                 mlx_sys::mlx_linalg_norm(
305//                     res,
306//                     array.as_ref().as_ptr(),
307//                     axes_ptr,
308//                     0,
309//                     keep_dims,
310//                     stream.as_ref().as_ptr(),
311//                 )
312//             })
313//         }
314//         // If axis is not provided but ord is, then x must be either 1D or 2D.
315//         //
316//         // Frobenius norm is only supported for matrices
317//         (Some(Ord::Str(ord)), None) => norm_ord_device(array, ord, axes, keep_dims, stream),
318//         (Some(Ord::P(p)), None) => norm_p_device(array, p, axes, keep_dims, stream),
319//         // If axis is provided, but ord is not, then the 2-norm (or Frobenius norm for matrices) is
320//         // computed along the given axes. At most 2 axes can be specified.
321//         (None, Some(axes)) => Array::try_from_op(|res| unsafe {
322//             mlx_sys::mlx_linalg_norm(
323//                 res,
324//                 array.as_ref().as_ptr(),
325//                 axes.as_ptr(),
326//                 axes.len(),
327//                 keep_dims,
328//                 stream.as_ref().as_ptr(),
329//             )
330//         }),
331//         // If both axis and ord are provided, then the corresponding matrix or vector
332//         // norm is computed. At most 2 axes can be specified.
333//         (Some(Ord::Str(ord)), Some(axes)) => norm_ord_device(array, ord, axes, keep_dims, stream),
334//         (Some(Ord::P(p)), Some(axes)) => norm_p_device(array, p, axes, keep_dims, stream),
335//     }
336// }
337
338/// The QR factorization of the input matrix. Returns an error if the input is not valid.
339///
340/// This function supports arrays with at least 2 dimensions. The matrices which are factorized are
341/// assumed to be in the last two dimensions of the input.
342///
343/// Evaluation on the GPU is not yet implemented.
344///
345/// # Params
346///
347/// - `array`: input array
348///
349/// # Example
350///
351/// ```rust
352/// use mlx_rs::{linalg::*, with_stream, Array, Stream};
353///
354/// with_stream(&Stream::cpu(), || {
355///     let a = Array::from_slice(&[2.0f32, 3.0, 1.0, 2.0], &[2, 2]);
356///
357///     let (q, r) = qr(&a).unwrap();
358///
359///     let q_expected = Array::from_slice(&[-0.894427, -0.447214, -0.447214, 0.894427], &[2, 2]);
360///     let r_expected = Array::from_slice(&[-2.23607, -3.57771, 0.0, 0.447214], &[2, 2]);
361///
362///     assert!(q.all_close(&q_expected, None, None, None).unwrap());
363///     assert!(r.all_close(&r_expected, None, None, None).unwrap());
364/// });
365/// ```
366pub fn qr(a: impl AsRef<Array>) -> Result<(Array, Array)> {
367    let stream = Stream::thread_local_or_default();
368    <(Array, Array)>::try_from_op(|(res_0, res_1)| unsafe {
369        mlx_sys::mlx_linalg_qr(res_0, res_1, a.as_ref().as_ptr(), stream.as_ref().as_ptr())
370    })
371}
372
373/// Compatibility shim for [`qr`].
374#[generate_macro(customize(forwarding_shim = true, root = "$crate::linalg"))]
375#[deprecated(
376    since = "0.26.0",
377    note = "use `with_stream` or `with_device` around `qr`"
378)]
379pub fn qr_device(
380    a: impl AsRef<Array>,
381    #[optional] stream: impl AsRef<Stream>,
382) -> Result<(Array, Array)> {
383    crate::with_stream(stream.as_ref(), || qr(a))
384}
385
386/// The Singular Value Decomposition (SVD) of the input matrix. Returns an error if the input is not
387/// valid.
388///
389/// This function supports arrays with at least 2 dimensions. When the input has more than two
390/// dimensions, the function iterates over all indices of the first a.ndim - 2 dimensions and for
391/// each combination SVD is applied to the last two indices.
392///
393/// Evaluation on the GPU is not yet implemented.
394///
395/// # Params
396///
397/// - `array`: input array
398///
399/// # Example
400///
401/// ```rust
402/// use mlx_rs::{linalg::*, with_stream, Array, Stream};
403///
404/// with_stream(&Stream::cpu(), || {
405///     let a = Array::from_slice(&[1.0f32, 2.0, 3.0, 4.0], &[2, 2]);
406///     let (u, s, vt) = svd(&a).unwrap();
407///     let u_expected = Array::from_slice(&[-0.404554, 0.914514, -0.914514, -0.404554], &[2, 2]);
408///     let s_expected = Array::from_slice(&[5.46499, 0.365966], &[2]);
409///     let vt_expected = Array::from_slice(&[-0.576048, -0.817416, -0.817415, 0.576048], &[2, 2]);
410///     assert!(u.all_close(&u_expected, None, None, None).unwrap());
411///     assert!(s.all_close(&s_expected, None, None, None).unwrap());
412///     assert!(vt.all_close(&vt_expected, None, None, None).unwrap());
413/// });
414/// ```
415pub fn svd(array: impl AsRef<Array>) -> Result<(Array, Array, Array)> {
416    let stream = Stream::thread_local_or_default();
417    let v = VectorArray::try_from_op(|res| unsafe {
418        mlx_sys::mlx_linalg_svd(res, array.as_ref().as_ptr(), true, stream.as_ref().as_ptr())
419    })?;
420
421    let vals: SmallVec<[Array; 3]> = v.try_into_values()?;
422    let mut iter = vals.into_iter();
423    let u = iter.next().unwrap();
424    let s = iter.next().unwrap();
425    let vt = iter.next().unwrap();
426
427    Ok((u, s, vt))
428}
429
430/// Compatibility shim for [`svd`].
431#[generate_macro(customize(forwarding_shim = true, root = "$crate::linalg"))]
432#[deprecated(
433    since = "0.26.0",
434    note = "use `with_stream` or `with_device` around `svd`"
435)]
436pub fn svd_device(
437    array: impl AsRef<Array>,
438    #[optional] stream: impl AsRef<Stream>,
439) -> Result<(Array, Array, Array)> {
440    crate::with_stream(stream.as_ref(), || svd(array))
441}
442
443/// Compute the inverse of a square matrix. Returns an error if the input is not valid.
444///
445/// This function supports arrays with at least 2 dimensions. When the input has more than two
446/// dimensions, the inverse is computed for each matrix in the last two dimensions of `a`.
447///
448/// Evaluation on the GPU is not yet implemented.
449///
450/// # Params
451///
452/// - `a`: input array
453///
454/// # Example
455///
456/// ```rust
457/// use mlx_rs::{linalg::*, with_stream, Array, Stream};
458///
459/// with_stream(&Stream::cpu(), || {
460///     let a = Array::from_slice(&[1.0f32, 2.0, 3.0, 4.0], &[2, 2]);
461///     let a_inv = inv(&a).unwrap();
462///     let expected = Array::from_slice(&[-2.0, 1.0, 1.5, -0.5], &[2, 2]);
463///     assert!(a_inv.all_close(&expected, None, None, None).unwrap());
464/// });
465/// ```
466pub fn inv(a: impl AsRef<Array>) -> Result<Array> {
467    let stream = Stream::thread_local_or_default();
468    Array::try_from_op(|res| unsafe {
469        mlx_sys::mlx_linalg_inv(res, a.as_ref().as_ptr(), stream.as_ref().as_ptr())
470    })
471}
472
473/// Compatibility shim for [`inv`].
474#[generate_macro(customize(forwarding_shim = true, root = "$crate::linalg"))]
475#[deprecated(
476    since = "0.26.0",
477    note = "use `with_stream` or `with_device` around `inv`"
478)]
479pub fn inv_device(a: impl AsRef<Array>, #[optional] stream: impl AsRef<Stream>) -> Result<Array> {
480    crate::with_stream(stream.as_ref(), || inv(a))
481}
482
483/// Compute the Cholesky decomposition of a real symmetric positive semi-definite matrix.
484///
485/// This function supports arrays with at least 2 dimensions. When the input has more than two
486/// dimensions, the Cholesky decomposition is computed for each matrix in the last two dimensions of
487/// `a`.
488///
489/// If the input matrix is not symmetric positive semi-definite, behaviour is undefined.
490///
491/// # Params
492///
493/// - `a`: input array
494/// - `upper`: If `true`, return the upper triangular Cholesky factor. If `false`, return the lower
495///   triangular Cholesky factor. Default: `false`.
496pub fn cholesky(a: impl AsRef<Array>, upper: Option<bool>) -> Result<Array> {
497    let stream = Stream::thread_local_or_default();
498    let upper = upper.unwrap_or(false);
499    Array::try_from_op(|res| unsafe {
500        mlx_sys::mlx_linalg_cholesky(res, a.as_ref().as_ptr(), upper, stream.as_ref().as_ptr())
501    })
502}
503
504/// Compatibility shim for [`cholesky`].
505#[generate_macro(customize(forwarding_shim = true, root = "$crate::linalg"))]
506#[deprecated(
507    since = "0.26.0",
508    note = "use `with_stream` or `with_device` around `cholesky`"
509)]
510pub fn cholesky_device(
511    a: impl AsRef<Array>,
512    #[optional] upper: Option<bool>,
513    #[optional] stream: impl AsRef<Stream>,
514) -> Result<Array> {
515    crate::with_stream(stream.as_ref(), || cholesky(a, upper))
516}
517
518/// Compute the inverse of a real symmetric positive semi-definite matrix using it’s Cholesky decomposition.
519///
520/// Please see the python documentation for more details.
521pub fn cholesky_inv(a: impl AsRef<Array>, upper: Option<bool>) -> Result<Array> {
522    let stream = Stream::thread_local_or_default();
523    let upper = upper.unwrap_or(false);
524    Array::try_from_op(|res| unsafe {
525        mlx_sys::mlx_linalg_cholesky_inv(res, a.as_ref().as_ptr(), upper, stream.as_ref().as_ptr())
526    })
527}
528
529/// Compatibility shim for [`cholesky_inv`].
530#[generate_macro(customize(forwarding_shim = true, root = "$crate::linalg"))]
531#[deprecated(
532    since = "0.26.0",
533    note = "use `with_stream` or `with_device` around `cholesky_inv`"
534)]
535pub fn cholesky_inv_device(
536    a: impl AsRef<Array>,
537    #[optional] upper: Option<bool>,
538    #[optional] stream: impl AsRef<Stream>,
539) -> Result<Array> {
540    crate::with_stream(stream.as_ref(), || cholesky_inv(a, upper))
541}
542
543/// Compute the cross product of two arrays along a specified axis.
544///
545/// The cross product is defined for arrays with size 2 or 3 in the specified axis. If the size is 2
546/// then the third value is assumed to be zero.
547pub fn cross(a: impl AsRef<Array>, b: impl AsRef<Array>, axis: Option<i32>) -> Result<Array> {
548    let stream = Stream::thread_local_or_default();
549    let axis = axis.unwrap_or(-1);
550    Array::try_from_op(|res| unsafe {
551        mlx_sys::mlx_linalg_cross(
552            res,
553            a.as_ref().as_ptr(),
554            b.as_ref().as_ptr(),
555            axis,
556            stream.as_ref().as_ptr(),
557        )
558    })
559}
560
561/// Compatibility shim for [`cross`].
562#[generate_macro(customize(forwarding_shim = true, root = "$crate::linalg"))]
563#[deprecated(
564    since = "0.26.0",
565    note = "use `with_stream` or `with_device` around `cross`"
566)]
567pub fn cross_device(
568    a: impl AsRef<Array>,
569    b: impl AsRef<Array>,
570    #[optional] axis: Option<i32>,
571    #[optional] stream: impl AsRef<Stream>,
572) -> Result<Array> {
573    crate::with_stream(stream.as_ref(), || cross(a, b, axis))
574}
575
576/// Compute the eigenvalues and eigenvectors of a complex Hermitian or real symmetric matrix.
577///
578/// This function supports arrays with at least 2 dimensions. When the input has more than two
579/// dimensions, the eigenvalues and eigenvectors are computed for each matrix in the last two
580/// dimensions.
581pub fn eigh(a: impl AsRef<Array>, uplo: Option<&str>) -> Result<(Array, Array)> {
582    let stream = Stream::thread_local_or_default();
583    let a = a.as_ref();
584    let uplo = CString::new(uplo.unwrap_or("L")).map_err(|e| Exception::custom(format!("{e}")))?;
585
586    <(Array, Array) as Guarded>::try_from_op(|(res_0, res_1)| unsafe {
587        mlx_sys::mlx_linalg_eigh(
588            res_0,
589            res_1,
590            a.as_ptr(),
591            uplo.as_ptr(),
592            stream.as_ref().as_ptr(),
593        )
594    })
595}
596
597/// Compatibility shim for [`eigh`].
598#[generate_macro(customize(forwarding_shim = true, root = "$crate::linalg"))]
599#[deprecated(
600    since = "0.26.0",
601    note = "use `with_stream` or `with_device` around `eigh`"
602)]
603pub fn eigh_device(
604    a: impl AsRef<Array>,
605    #[optional] uplo: Option<&str>,
606    #[optional] stream: impl AsRef<Stream>,
607) -> Result<(Array, Array)> {
608    crate::with_stream(stream.as_ref(), || eigh(a, uplo))
609}
610
611/// Compute the eigenvalues of a complex Hermitian or real symmetric matrix.
612///
613/// This function supports arrays with at least 2 dimensions. When the input has more than two
614/// dimensions, the eigenvalues are computed for each matrix in the last two dimensions.
615pub fn eigvalsh(a: impl AsRef<Array>, uplo: Option<&str>) -> Result<Array> {
616    let stream = Stream::thread_local_or_default();
617    let a = a.as_ref();
618    let uplo = CString::new(uplo.unwrap_or("L")).map_err(|e| Exception::custom(format!("{e}")))?;
619    Array::try_from_op(|res| unsafe {
620        mlx_sys::mlx_linalg_eigvalsh(res, a.as_ptr(), uplo.as_ptr(), stream.as_ref().as_ptr())
621    })
622}
623
624/// Compatibility shim for [`eigvalsh`].
625#[generate_macro(customize(forwarding_shim = true, root = "$crate::linalg"))]
626#[deprecated(
627    since = "0.26.0",
628    note = "use `with_stream` or `with_device` around `eigvalsh`"
629)]
630pub fn eigvalsh_device(
631    a: impl AsRef<Array>,
632    #[optional] uplo: Option<&str>,
633    #[optional] stream: impl AsRef<Stream>,
634) -> Result<Array> {
635    crate::with_stream(stream.as_ref(), || eigvalsh(a, uplo))
636}
637
638/// Compute the eigenvalues and eigenvectors of a square matrix.
639///
640/// This function supports arrays with at least 2 dimensions. When the input has more than two
641/// dimensions, the eigenvalues and eigenvectors are computed for each matrix in the last two
642/// dimensions.
643///
644/// Unlike [`eigh`], this function computes eigenvalues for general (not necessarily symmetric
645/// or Hermitian) matrices. The eigenvalues and eigenvectors may be complex.
646///
647/// # Params
648///
649/// - `a`: Input array. Must be a square matrix.
650///
651/// # Returns
652///
653/// A tuple `(eigenvalues, eigenvectors)` where eigenvalues has shape `(..., N)` and
654/// eigenvectors has shape `(..., N, N)`. The eigenvectors are stored as columns.
655///
656/// # Example
657///
658/// ```rust
659/// use mlx_rs::{linalg::*, with_stream, Array, Stream};
660///
661/// with_stream(&Stream::cpu(), || {
662///     let a = Array::from_slice(&[1.0f32, 1.0, 3.0, 4.0], &[2, 2]);
663///     let (eigenvalues, eigenvectors) = eig(&a).unwrap();
664///     // eigenvalues and eigenvectors are complex even for real input
665/// });
666/// ```
667pub fn eig(a: impl AsRef<Array>) -> Result<(Array, Array)> {
668    let stream = Stream::thread_local_or_default();
669    <(Array, Array) as Guarded>::try_from_op(|(res_0, res_1)| unsafe {
670        mlx_sys::mlx_linalg_eig(res_0, res_1, a.as_ref().as_ptr(), stream.as_ref().as_ptr())
671    })
672}
673
674/// Compatibility shim for [`eig`].
675#[generate_macro(customize(forwarding_shim = true, root = "$crate::linalg"))]
676#[deprecated(
677    since = "0.26.0",
678    note = "use `with_stream` or `with_device` around `eig`"
679)]
680pub fn eig_device(
681    a: impl AsRef<Array>,
682    #[optional] stream: impl AsRef<Stream>,
683) -> Result<(Array, Array)> {
684    crate::with_stream(stream.as_ref(), || eig(a))
685}
686
687/// Compute the eigenvalues of a square matrix.
688///
689/// This function supports arrays with at least 2 dimensions. When the input has more than two
690/// dimensions, the eigenvalues are computed for each matrix in the last two dimensions.
691///
692/// Unlike [`eigvalsh`], this function computes eigenvalues for general (not necessarily symmetric
693/// or Hermitian) matrices. The eigenvalues may be complex.
694///
695/// # Params
696///
697/// - `a`: Input array. Must be a square matrix.
698///
699/// # Returns
700///
701/// An array of eigenvalues with shape `(..., N)`.
702///
703/// # Example
704///
705/// ```rust
706/// use mlx_rs::{linalg::*, with_stream, Array, Stream};
707///
708/// with_stream(&Stream::cpu(), || {
709///     let a = Array::from_slice(&[1.0f32, 1.0, 3.0, 4.0], &[2, 2]);
710///     let eigenvalues = eigvals(&a).unwrap();
711/// });
712/// ```
713pub fn eigvals(a: impl AsRef<Array>) -> Result<Array> {
714    let stream = Stream::thread_local_or_default();
715    Array::try_from_op(|res| unsafe {
716        mlx_sys::mlx_linalg_eigvals(res, a.as_ref().as_ptr(), stream.as_ref().as_ptr())
717    })
718}
719
720/// Compatibility shim for [`eigvals`].
721#[generate_macro(customize(forwarding_shim = true, root = "$crate::linalg"))]
722#[deprecated(
723    since = "0.26.0",
724    note = "use `with_stream` or `with_device` around `eigvals`"
725)]
726pub fn eigvals_device(
727    a: impl AsRef<Array>,
728    #[optional] stream: impl AsRef<Stream>,
729) -> Result<Array> {
730    crate::with_stream(stream.as_ref(), || eigvals(a))
731}
732
733/// Compute the (Moore-Penrose) pseudo-inverse of a matrix.
734pub fn pinv(a: impl AsRef<Array>) -> Result<Array> {
735    let stream = Stream::thread_local_or_default();
736    Array::try_from_op(|res| unsafe {
737        mlx_sys::mlx_linalg_pinv(res, a.as_ref().as_ptr(), stream.as_ref().as_ptr())
738    })
739}
740
741/// Compatibility shim for [`pinv`].
742#[generate_macro(customize(forwarding_shim = true, root = "$crate::linalg"))]
743#[deprecated(
744    since = "0.26.0",
745    note = "use `with_stream` or `with_device` around `pinv`"
746)]
747pub fn pinv_device(a: impl AsRef<Array>, #[optional] stream: impl AsRef<Stream>) -> Result<Array> {
748    crate::with_stream(stream.as_ref(), || pinv(a))
749}
750
751/// Compute the inverse of a triangular square matrix.
752///
753/// This function supports arrays with at least 2 dimensions. When the input has more than two
754/// dimensions, the inverse is computed for each matrix in the last two dimensions of a.
755pub fn tri_inv(a: impl AsRef<Array>, upper: Option<bool>) -> Result<Array> {
756    let stream = Stream::thread_local_or_default();
757    let upper = upper.unwrap_or(false);
758    Array::try_from_op(|res| unsafe {
759        mlx_sys::mlx_linalg_tri_inv(res, a.as_ref().as_ptr(), upper, stream.as_ref().as_ptr())
760    })
761}
762
763/// Compatibility shim for [`tri_inv`].
764#[generate_macro(customize(forwarding_shim = true, root = "$crate::linalg"))]
765#[deprecated(
766    since = "0.26.0",
767    note = "use `with_stream` or `with_device` around `tri_inv`"
768)]
769pub fn tri_inv_device(
770    a: impl AsRef<Array>,
771    #[optional] upper: Option<bool>,
772    #[optional] stream: impl AsRef<Stream>,
773) -> Result<Array> {
774    crate::with_stream(stream.as_ref(), || tri_inv(a, upper))
775}
776
777/// Compute the LU factorization of the given matrix A.
778///
779/// Note, unlike the default behavior of scipy.linalg.lu, the pivots are
780/// indices. To reconstruct the input use L[P, :] @ U for 2 dimensions or
781/// mx.take_along_axis(L, P[..., None], axis=-2) @ U for more than 2 dimensions.
782///
783/// To construct the full permutation matrix do:
784///
785/// ```rust
786/// use mlx_rs::{array, linalg::lu, ops, with_stream, Stream};
787/// use mlx_rs::ops::indexing::{IndexOp, Ellipsis, NewAxis};
788///
789/// with_stream(&Stream::cpu(), || {
790///     let a = array!([[3.0f32, 1.0, 2.0], [1.0, 8.0, 6.0], [9.0, 2.0, 5.0]]);
791///     let (p, l, u) = lu(&a).unwrap();
792///     let p = ops::indexing::put_along_axis(
793///         mlx_rs::ops::zeros_like(&l).unwrap(),
794///         p.index((Ellipsis, NewAxis)),
795///         array!(1.0),
796///         -1,
797///     ).unwrap();
798///     let reconstructed = p.matmul(&l).unwrap().matmul(&u).unwrap();
799///     assert!(ops::all_close(&reconstructed, &a, None, None, None).unwrap());
800/// });
801/// ```
802///
803/// # Params
804///
805/// - `a`: input array
806/// - `stream`: stream to execute the operation
807///
808/// # Returns
809///
810/// The `p`, `L`, and `U` arrays, such that `A = L[P, :] @ U`
811pub fn lu(a: impl AsRef<Array>) -> Result<(Array, Array, Array)> {
812    let stream = Stream::thread_local_or_default();
813    let v = Vec::<Array>::try_from_op(|res| unsafe {
814        mlx_sys::mlx_linalg_lu(res, a.as_ref().as_ptr(), stream.as_ref().as_ptr())
815    })?;
816    let mut iter = v.into_iter();
817    let p = iter.next().ok_or_else(|| Exception::custom("missing P"))?;
818    let l = iter.next().ok_or_else(|| Exception::custom("missing L"))?;
819    let u = iter.next().ok_or_else(|| Exception::custom("missing U"))?;
820    Ok((p, l, u))
821}
822
823/// Compatibility shim for [`lu`].
824#[generate_macro(customize(forwarding_shim = true, root = "$crate::linalg"))]
825#[deprecated(
826    since = "0.26.0",
827    note = "use `with_stream` or `with_device` around `lu`"
828)]
829pub fn lu_device(
830    a: impl AsRef<Array>,
831    #[optional] stream: impl AsRef<Stream>,
832) -> Result<(Array, Array, Array)> {
833    crate::with_stream(stream.as_ref(), || lu(a))
834}
835
836/// Computes a compact representation of the LU factorization.
837///
838/// # Params
839///
840/// - `a`: input array
841/// - `stream`: stream to execute the operation
842///
843/// # Returns
844///
845/// The `LU` matrix and `pivots` array.
846pub fn lu_factor(a: impl AsRef<Array>) -> Result<(Array, Array)> {
847    let stream = Stream::thread_local_or_default();
848    <(Array, Array)>::try_from_op(|(res_0, res_1)| unsafe {
849        mlx_sys::mlx_linalg_lu_factor(res_0, res_1, a.as_ref().as_ptr(), stream.as_ref().as_ptr())
850    })
851}
852
853/// Compatibility shim for [`lu_factor`].
854#[generate_macro(customize(forwarding_shim = true, root = "$crate::linalg"))]
855#[deprecated(
856    since = "0.26.0",
857    note = "use `with_stream` or `with_device` around `lu_factor`"
858)]
859pub fn lu_factor_device(
860    a: impl AsRef<Array>,
861    #[optional] stream: impl AsRef<Stream>,
862) -> Result<(Array, Array)> {
863    crate::with_stream(stream.as_ref(), || lu_factor(a))
864}
865
866/// Compute the solution to a system of linear equations `AX = B`
867///
868/// # Params
869///
870/// - `a`: input array
871/// - `b`: input array
872/// - `stream`: stream to execute the operation
873///
874/// # Returns
875///
876/// The unique solution to the system `AX = B`
877pub fn solve(a: impl AsRef<Array>, b: impl AsRef<Array>) -> Result<Array> {
878    let stream = Stream::thread_local_or_default();
879    Array::try_from_op(|res| unsafe {
880        mlx_sys::mlx_linalg_solve(
881            res,
882            a.as_ref().as_ptr(),
883            b.as_ref().as_ptr(),
884            stream.as_ref().as_ptr(),
885        )
886    })
887}
888
889/// Compatibility shim for [`solve`].
890#[generate_macro(customize(forwarding_shim = true, root = "$crate::linalg"))]
891#[deprecated(
892    since = "0.26.0",
893    note = "use `with_stream` or `with_device` around `solve`"
894)]
895pub fn solve_device(
896    a: impl AsRef<Array>,
897    b: impl AsRef<Array>,
898    #[optional] stream: impl AsRef<Stream>,
899) -> Result<Array> {
900    crate::with_stream(stream.as_ref(), || solve(a, b))
901}
902
903/// Computes the solution of a triangular system of linear equations `AX = B`
904///
905/// # Params
906///
907/// - `a`: input array
908/// - `b`: input array
909/// - `upper`: whether the matrix is upper triangular. Default: `false`
910/// - `stream`: stream to execute the operation
911///
912/// # Returns
913///
914/// The unique solution to the system `AX = B`
915pub fn solve_triangular(
916    a: impl AsRef<Array>,
917    b: impl AsRef<Array>,
918    upper: impl Into<Option<bool>>,
919) -> Result<Array> {
920    let stream = Stream::thread_local_or_default();
921    let upper = upper.into().unwrap_or(false);
922
923    Array::try_from_op(|res| unsafe {
924        mlx_sys::mlx_linalg_solve_triangular(
925            res,
926            a.as_ref().as_ptr(),
927            b.as_ref().as_ptr(),
928            upper,
929            stream.as_ref().as_ptr(),
930        )
931    })
932}
933
934/// Compatibility shim for [`solve_triangular`].
935#[generate_macro(customize(forwarding_shim = true, root = "$crate::linalg"))]
936#[deprecated(
937    since = "0.26.0",
938    note = "use `with_stream` or `with_device` around `solve_triangular`"
939)]
940pub fn solve_triangular_device(
941    a: impl AsRef<Array>,
942    b: impl AsRef<Array>,
943    #[optional] upper: impl Into<Option<bool>>,
944    #[optional] stream: impl AsRef<Stream>,
945) -> Result<Array> {
946    crate::with_stream(stream.as_ref(), || solve_triangular(a, b, upper))
947}
948
949#[cfg(test)]
950mod tests {
951    use float_eq::assert_float_eq;
952
953    use crate::{
954        array,
955        ops::{eye, indexing::IndexOp, tril, triu},
956        with_device, with_stream, Device, StreamOrDevice,
957    };
958
959    use super::*;
960
961    // The tests below are adapted from the swift bindings tests
962    // and they are not exhaustive. Additional tests should be added
963    // to cover the error cases
964
965    #[test]
966    fn test_norm_no_axes() {
967        let a = Array::from_iter(0..9, &[9]) - 4;
968        let b = a.reshape(&[3, 3]).unwrap();
969
970        assert_float_eq!(
971            norm_l2(&a, NormOptions::default())
972                .unwrap()
973                .item_exact::<f32>(),
974            7.74597,
975            abs <= 0.001
976        );
977        assert_float_eq!(
978            norm_l2(&b, NormOptions::default())
979                .unwrap()
980                .item_exact::<f32>(),
981            7.74597,
982            abs <= 0.001
983        );
984
985        assert_float_eq!(
986            norm_matrix(&b, "fro", NormOptions::default())
987                .unwrap()
988                .item_exact::<f32>(),
989            7.74597,
990            abs <= 0.001
991        );
992
993        assert_float_eq!(
994            norm(&a, f64::INFINITY, NormOptions::default())
995                .unwrap()
996                .item_exact::<f32>(),
997            4.0,
998            abs <= 0.001
999        );
1000        assert_float_eq!(
1001            norm(&b, f64::INFINITY, NormOptions::default())
1002                .unwrap()
1003                .item_exact::<f32>(),
1004            9.0,
1005            abs <= 0.001
1006        );
1007
1008        assert_float_eq!(
1009            norm(&a, f64::NEG_INFINITY, NormOptions::default())
1010                .unwrap()
1011                .item_exact::<f32>(),
1012            0.0,
1013            abs <= 0.001
1014        );
1015        assert_float_eq!(
1016            norm(&b, f64::NEG_INFINITY, NormOptions::default())
1017                .unwrap()
1018                .item_exact::<f32>(),
1019            2.0,
1020            abs <= 0.001
1021        );
1022
1023        assert_float_eq!(
1024            norm(&a, 1.0, NormOptions::default())
1025                .unwrap()
1026                .item_exact::<f32>(),
1027            20.0,
1028            abs <= 0.001
1029        );
1030        assert_float_eq!(
1031            norm(&b, 1.0, NormOptions::default())
1032                .unwrap()
1033                .item_exact::<f32>(),
1034            7.0,
1035            abs <= 0.001
1036        );
1037
1038        assert_float_eq!(
1039            norm(&a, -1.0, NormOptions::default())
1040                .unwrap()
1041                .item_exact::<f32>(),
1042            0.0,
1043            abs <= 0.001
1044        );
1045        assert_float_eq!(
1046            norm(&b, -1.0, NormOptions::default())
1047                .unwrap()
1048                .item_exact::<f32>(),
1049            6.0,
1050            abs <= 0.001
1051        );
1052    }
1053
1054    #[test]
1055    fn test_norm_axis() {
1056        let c = Array::from_slice(&[1, 2, 3, -1, 1, 4], &[2, 3]);
1057
1058        let result = norm_l2(
1059            &c,
1060            NormOptions {
1061                axes: Axes::from([0]),
1062                ..Default::default()
1063            },
1064        )
1065        .unwrap();
1066        let expected = Array::from_slice(&[1.41421, 2.23607, 5.0], &[3]);
1067        assert!(result.all_close(&expected, None, None, None).unwrap());
1068    }
1069
1070    #[test]
1071    fn test_norm_axes() {
1072        let m = Array::from_iter(0..8, &[2, 2, 2]);
1073
1074        let result = norm_l2(
1075            &m,
1076            NormOptions {
1077                axes: Axes::from([1, 2]),
1078                ..Default::default()
1079            },
1080        )
1081        .unwrap();
1082        let expected = Array::from_slice(&[3.74166, 11.225], &[2]);
1083        assert!(result.all_close(&expected, None, None, None).unwrap());
1084    }
1085
1086    #[test]
1087    fn test_qr() {
1088        let a = Array::from_slice(&[2.0f32, 3.0, 1.0, 2.0], &[2, 2]);
1089
1090        let (q, r) = with_device(Device::cpu(), || qr(&a)).unwrap();
1091
1092        let q_expected = Array::from_slice(&[-0.894427, -0.447214, -0.447214, 0.894427], &[2, 2]);
1093        let r_expected = Array::from_slice(&[-2.23607, -3.57771, 0.0, 0.447214], &[2, 2]);
1094
1095        assert!(q.all_close(&q_expected, None, None, None).unwrap());
1096        assert!(r.all_close(&r_expected, None, None, None).unwrap());
1097    }
1098
1099    // The tests below are adapted from the c++ tests
1100
1101    #[test]
1102    fn test_svd() {
1103        // eval_gpu is not implemented yet.
1104        let stream = StreamOrDevice::cpu();
1105
1106        // 0D and 1D returns error
1107        let a = Array::from_f32(0.0);
1108        assert!(with_stream(stream.as_ref(), || svd(&a)).is_err());
1109
1110        let a = Array::from_slice(&[0.0, 1.0], &[2]);
1111        assert!(with_stream(stream.as_ref(), || svd(&a)).is_err());
1112
1113        // Unsupported types returns error
1114        let a = Array::from_slice(&[0, 1], &[1, 2]);
1115        assert!(with_stream(stream.as_ref(), || svd(&a)).is_err());
1116        // TODO: wait for random
1117    }
1118
1119    #[test]
1120    fn test_inv() {
1121        // eval_gpu is not implemented yet.
1122        let stream = StreamOrDevice::cpu();
1123
1124        // 0D and 1D returns error
1125        let a = Array::from_f32(0.0);
1126        assert!(with_stream(stream.as_ref(), || inv(&a)).is_err());
1127
1128        let a = Array::from_slice(&[0.0, 1.0], &[2]);
1129        assert!(with_stream(stream.as_ref(), || inv(&a)).is_err());
1130
1131        // Unsupported types returns error
1132        let a = Array::from_slice(&[1, 2, 3, 4, 5, 6], &[2, 3]);
1133        assert!(with_stream(stream.as_ref(), || inv(&a)).is_err());
1134        // TODO: wait for random
1135    }
1136
1137    #[test]
1138    fn test_cholesky() {
1139        // eval_gpu is not implemented yet.
1140        let stream = StreamOrDevice::cpu();
1141
1142        // 0D and 1D returns error
1143        let a = Array::from_f32(0.0);
1144        assert!(with_stream(stream.as_ref(), || cholesky(&a, None)).is_err());
1145
1146        let a = Array::from_slice(&[0.0, 1.0], &[2]);
1147        assert!(with_stream(stream.as_ref(), || cholesky(&a, None)).is_err());
1148
1149        // Unsupported types returns error
1150        let a = Array::from_slice(&[0, 1, 1, 2], &[2, 2]);
1151        assert!(with_stream(stream.as_ref(), || cholesky(&a, None)).is_err());
1152
1153        // Non-square returns error
1154        let a = Array::from_slice(&[1, 2, 3, 4, 5, 6], &[2, 3]);
1155        assert!(with_stream(stream.as_ref(), || cholesky(&a, None)).is_err());
1156        // TODO: wait for random
1157    }
1158
1159    // The unit test below is adapted from the python unit test `test_linalg.py/test_lu`
1160    #[test]
1161    fn test_lu() {
1162        let scalar = array!(1.0);
1163        let result = with_device(Device::cpu(), || lu(&scalar));
1164        assert!(result.is_err());
1165
1166        // # Test 3x3 matrix
1167        let a = array!([[3.0f32, 1.0, 2.0], [1.0, 8.0, 6.0], [9.0, 2.0, 5.0]]);
1168        let (p, l, u) = with_device(Device::cpu(), || lu(&a)).unwrap();
1169        let a_rec = l.index((p, ..)).matmul(u).unwrap();
1170        assert_array_all_close!(a, a_rec);
1171    }
1172
1173    // The unit test below is adapted from the python unit test `test_linalg.py/test_lu_factor`
1174    #[test]
1175    fn test_lu_factor() {
1176        crate::random::seed(7).unwrap();
1177
1178        // Test 3x3 matrix
1179        let a = crate::random::uniform::<_, f32>(0.0, 1.0, &[5, 5], None).unwrap();
1180        let (lu, pivots) = with_device(Device::cpu(), || lu_factor(&a)).unwrap();
1181        let shape = a.shape();
1182        let n = shape[shape.len() - 1];
1183
1184        let pivots = pivots.to_vec_exact::<u32>().unwrap();
1185        let mut perm: Vec<u32> = (0..n as u32).collect();
1186        for (i, p) in pivots.iter().enumerate() {
1187            perm.swap(i, *p as usize);
1188        }
1189
1190        let l = tril(&lu, -1)
1191            .and_then(|l| l.add(eye::<f32>(n, None, None)?))
1192            .unwrap();
1193        let u = triu(&lu, None).unwrap();
1194
1195        let lhs = l.matmul(&u).unwrap();
1196        let perm = Array::from_slice(&perm, &[n]);
1197        let rhs = a.index((perm, ..));
1198        assert_array_all_close!(lhs, rhs);
1199    }
1200
1201    // The unit test below is adapted from the python unit test `test_linalg.py/test_solve`
1202    #[test]
1203    fn test_solve() {
1204        crate::random::seed(7).unwrap();
1205
1206        // Test 3x3 matrix with 1D rhs
1207        let a = array!([[3.0f32, 1.0, 2.0], [1.0, 8.0, 6.0], [9.0, 2.0, 5.0]]);
1208        let b = array!([11.0f32, 35.0, 28.0]);
1209
1210        let result = with_device(Device::cpu(), || solve(&a, &b)).unwrap();
1211        let expected = array!([1.0f32, 2.0, 3.0]);
1212        assert_array_all_close!(result, expected);
1213    }
1214
1215    #[test]
1216    fn test_solve_triangular() {
1217        let a = array!([[4.0f32, 0.0, 0.0], [2.0, 3.0, 0.0], [1.0, -2.0, 5.0]]);
1218        let b = array!([8.0f32, 14.0, 3.0]);
1219
1220        let result = with_device(Device::cpu(), || solve_triangular(&a, &b, false)).unwrap();
1221        let expected = array!([2.0f32, 3.333_333_3, 1.533_333_3]);
1222        assert_array_all_close!(result, expected);
1223    }
1224
1225    // The tests below are adapted from the python unit test `test_linalg.py/test_eig`
1226    #[test]
1227    fn test_eig() {
1228        use crate::ops::expand_dims;
1229
1230        // Helper to check eigenvalues and eigenvectors
1231        fn check_eigs_and_vecs(a: &Array) {
1232            let (eig_vals, eig_vecs) = with_device(Device::cpu(), || eig(a)).unwrap();
1233
1234            // Check A @ eig_vecs == eig_vals * eig_vecs
1235            let lhs = a.matmul(&eig_vecs).unwrap();
1236            // eig_vals[..., None, :] * eig_vecs - broadcast eigenvalues
1237            // For a 1D eigenvalues array (n,), we need shape (1, n) to broadcast with eigenvectors (n, n)
1238            // For batched eigenvalues (..., n), we need shape (..., 1, n)
1239            let eig_vals_broadcast = expand_dims(&eig_vals, -2).unwrap();
1240            let rhs = eig_vals_broadcast.multiply(&eig_vecs).unwrap();
1241            assert!(
1242                lhs.all_close(&rhs, 1e-4, 1e-4, None).unwrap(),
1243                "A @ eig_vecs should equal eig_vals * eig_vecs"
1244            );
1245
1246            // Check eigvals returns same values
1247            let eig_vals_only = with_device(Device::cpu(), || eigvals(a)).unwrap();
1248            assert!(
1249                eig_vals
1250                    .all_close(&eig_vals_only, 1e-4, 1e-4, None)
1251                    .unwrap(),
1252                "eigvals should return same eigenvalues as eig"
1253            );
1254        }
1255
1256        // Test a simple 2x2 matrix
1257        let a = array!([[1.0f32, 1.0], [3.0, 4.0]]);
1258        check_eigs_and_vecs(&a);
1259
1260        // Test complex eigenvalues (rotation-like matrix)
1261        let a = array!([[1.0f32, -1.0], [1.0, 1.0]]);
1262        check_eigs_and_vecs(&a);
1263
1264        // Test a larger random matrix
1265        crate::random::seed(1).unwrap();
1266        let a = crate::random::normal::<f32>(&[5, 5], None, None, None).unwrap();
1267        check_eigs_and_vecs(&a);
1268
1269        // Test with batched input
1270        let a = crate::random::normal::<f32>(&[3, 5, 5], None, None, None).unwrap();
1271        check_eigs_and_vecs(&a);
1272    }
1273
1274    #[test]
1275    fn test_eig_errors() {
1276        // 1D array should fail
1277        let a = array!([1.0f32, 2.0]);
1278        assert!(with_device(Device::cpu(), || eig(&a)).is_err());
1279        assert!(with_device(Device::cpu(), || eigvals(&a)).is_err());
1280
1281        // Non-square matrix should fail
1282        let a = array!([[1.0f32, 2.0], [3.0, 4.0], [5.0, 6.0]]);
1283        assert!(with_device(Device::cpu(), || eig(&a)).is_err());
1284        assert!(with_device(Device::cpu(), || eigvals(&a)).is_err());
1285    }
1286}