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::f64;
13use std::ffi::CString;
14
15/// Order of the norm
16///
17/// See [`norm`] for more details.
18#[derive(Debug, Clone, Copy)]
19pub enum Ord<'a> {
20    /// String representation of the order
21    Str(&'a str),
22
23    /// Order of the norm
24    P(f64),
25}
26
27impl Default for Ord<'_> {
28    fn default() -> Self {
29        Ord::Str("fro")
30    }
31}
32
33impl std::fmt::Display for Ord<'_> {
34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        match self {
36            Ord::Str(s) => write!(f, "{s}"),
37            Ord::P(p) => write!(f, "{p}"),
38        }
39    }
40}
41
42impl<'a> From<&'a str> for Ord<'a> {
43    fn from(value: &'a str) -> Self {
44        Ord::Str(value)
45    }
46}
47
48impl From<f64> for Ord<'_> {
49    fn from(value: f64) -> Self {
50        Ord::P(value)
51    }
52}
53
54impl<'a> IntoOption<Ord<'a>> for &'a str {
55    fn into_option(self) -> Option<Ord<'a>> {
56        Some(Ord::Str(self))
57    }
58}
59
60impl<'a> IntoOption<Ord<'a>> for f64 {
61    fn into_option(self) -> Option<Ord<'a>> {
62        Some(Ord::P(self))
63    }
64}
65
66/// The sign and natural logarithm of the absolute determinant.
67#[derive(Debug, Clone)]
68pub struct SlogDet {
69    /// Determinant sign.
70    pub sign: Array,
71
72    /// Natural logarithm of the absolute determinant.
73    pub log_abs_det: Array,
74}
75
76/// Compute the determinant of square matrices.
77///
78/// This operation is CPU-only at the pinned MLX version and uses the ambient stream. Integer
79/// inputs are promoted to floating point and leading dimensions are treated as batches.
80///
81/// ```rust
82/// use mlx_rs::{array, linalg, with_stream, Stream};
83///
84/// let matrix = array!([[1.0, 2.0], [3.0, 4.0]]);
85/// let result = with_stream(&Stream::cpu(), || linalg::det(&matrix)).unwrap();
86/// assert!(result.shape().is_empty());
87/// ```
88pub fn det(array: impl AsRef<Array>) -> Result<Array> {
89    let stream = Stream::thread_local_or_default();
90    Array::try_from_op(|res| unsafe {
91        mlx_sys::mlx_linalg_det(res, array.as_ref().as_ptr(), stream.as_ref().as_ptr())
92    })
93}
94
95/// Compute determinant sign and log absolute determinant for square matrices.
96///
97/// This operation is CPU-only at the pinned MLX version and uses the ambient stream. A singular
98/// matrix returns zero sign and negative-infinity `log_abs_det`.
99///
100/// ```rust
101/// use mlx_rs::{array, linalg, with_stream, Stream};
102///
103/// let matrix = array!([[1.0, 2.0], [3.0, 4.0]]);
104/// let result = with_stream(&Stream::cpu(), || linalg::slogdet(&matrix)).unwrap();
105/// assert!(result.sign.shape().is_empty());
106/// assert!(result.log_abs_det.shape().is_empty());
107/// ```
108pub fn slogdet(array: impl AsRef<Array>) -> Result<SlogDet> {
109    let stream = Stream::thread_local_or_default();
110    let (sign, log_abs_det) =
111        <(Array, Array) as Guarded>::try_from_op(|(sign, log_abs_det)| unsafe {
112            mlx_sys::mlx_linalg_slogdet(
113                sign,
114                log_abs_det,
115                array.as_ref().as_ptr(),
116                stream.as_ref().as_ptr(),
117            )
118        })?;
119    Ok(SlogDet { sign, log_abs_det })
120}
121
122/// Axis selection and independent defaults for norm operations.
123#[derive(Debug, Clone, Default, PartialEq, Eq)]
124pub struct NormOptions {
125    /// Axes to reduce.
126    pub axes: Axes,
127
128    /// Keep reduced axes as singleton dimensions.
129    pub keep_dims: bool,
130}
131fn with_norm_axes<T>(axes: &Axes, f: impl FnOnce(*const i32, usize) -> T) -> T {
132    match axes {
133        Axes::All => f(std::ptr::null(), 0),
134        Axes::Axis(axis) => f(axis, 1),
135        Axes::Axes(axes) => f(axes.as_ptr(), axes.len()),
136    }
137}
138fn legacy_norm_options(axes: Option<&[i32]>, keep_dims: Option<bool>) -> NormOptions {
139    NormOptions {
140        axes: axes.map_or(Axes::All, Axes::from),
141        keep_dims: keep_dims.unwrap_or(false),
142    }
143}
144
145/// Compute p-norm of an [`Array`]
146pub fn norm(array: impl AsRef<Array>, ord: f64, options: NormOptions) -> Result<Array> {
147    let stream = Stream::thread_local_or_default();
148    with_norm_axes(&options.axes, |axes, num_axes| {
149        Array::try_from_op(|res| unsafe {
150            mlx_sys::mlx_linalg_norm(
151                res,
152                array.as_ref().as_ptr(),
153                ord,
154                axes,
155                num_axes,
156                options.keep_dims,
157                stream.as_ref().as_ptr(),
158            )
159        })
160    })
161}
162
163/// Compatibility shim for [`norm`].
164#[generate_macro(customize(forwarding_shim = true, root = "$crate::linalg"))]
165#[deprecated(
166    since = "0.26.0",
167    note = "use `with_stream` or `with_device` around `norm` with `NormOptions`"
168)]
169pub fn norm_device<'a>(
170    array: impl AsRef<Array>,
171    ord: f64,
172    #[optional] axes: impl IntoOption<&'a [i32]>,
173    #[optional] keep_dims: impl Into<Option<bool>>,
174    #[optional] stream: impl AsRef<Stream>,
175) -> Result<Array> {
176    let options = legacy_norm_options(axes.into_option(), keep_dims.into());
177    with_stream(stream.as_ref(), || norm(array, ord, options))
178}
179
180/// Matrix or vector norm.
181pub fn norm_matrix(array: impl AsRef<Array>, ord: &str, options: NormOptions) -> Result<Array> {
182    let ord = CString::new(ord).map_err(|e| Exception::custom(format!("{e}")))?;
183    let stream = Stream::thread_local_or_default();
184    with_norm_axes(&options.axes, |axes, num_axes| {
185        Array::try_from_op(|res| unsafe {
186            mlx_sys::mlx_linalg_norm_matrix(
187                res,
188                array.as_ref().as_ptr(),
189                ord.as_ptr(),
190                axes,
191                num_axes,
192                options.keep_dims,
193                stream.as_ref().as_ptr(),
194            )
195        })
196    })
197}
198
199/// Compatibility shim for [`norm_matrix`].
200#[generate_macro(customize(forwarding_shim = true, root = "$crate::linalg"))]
201#[deprecated(
202    since = "0.26.0",
203    note = "use `with_stream` or `with_device` around `norm_matrix` with `NormOptions`"
204)]
205pub fn norm_matrix_device<'a>(
206    array: impl AsRef<Array>,
207    ord: &'a str,
208    #[optional] axes: impl IntoOption<&'a [i32]>,
209    #[optional] keep_dims: impl Into<Option<bool>>,
210    #[optional] stream: impl AsRef<Stream>,
211) -> Result<Array> {
212    let options = legacy_norm_options(axes.into_option(), keep_dims.into());
213    with_stream(stream.as_ref(), || norm_matrix(array, ord, options))
214}
215
216/// Compute the L2 norm of an [`Array`]
217pub fn norm_l2(array: impl AsRef<Array>, options: NormOptions) -> Result<Array> {
218    let stream = Stream::thread_local_or_default();
219    with_norm_axes(&options.axes, |axes, num_axes| {
220        Array::try_from_op(|res| unsafe {
221            mlx_sys::mlx_linalg_norm_l2(
222                res,
223                array.as_ref().as_ptr(),
224                axes,
225                num_axes,
226                options.keep_dims,
227                stream.as_ref().as_ptr(),
228            )
229        })
230    })
231}
232
233/// Compatibility shim for [`norm_l2`].
234#[generate_macro(customize(forwarding_shim = true, root = "$crate::linalg"))]
235#[deprecated(
236    since = "0.26.0",
237    note = "use `with_stream` or `with_device` around `norm_l2` with `NormOptions`"
238)]
239pub fn norm_l2_device<'a>(
240    array: impl AsRef<Array>,
241    #[optional] axes: impl IntoOption<&'a [i32]>,
242    #[optional] keep_dims: impl Into<Option<bool>>,
243    #[optional] stream: impl AsRef<Stream>,
244) -> Result<Array> {
245    let options = legacy_norm_options(axes.into_option(), keep_dims.into());
246    with_stream(stream.as_ref(), || norm_l2(array, options))
247}
248// TODO: Change the original `norm` function to use builder pattern
249// /// Matrix or vector norm.
250// ///
251// /// For values of `ord < 1`, the result is, strictly speaking, not a
252// /// mathematical norm, but it may still be useful for various numerical
253// /// purposes.
254// ///
255// /// The following norms can be calculated:
256// ///
257// /// ord   | norm for matrices            | norm for vectors
258// /// ----- | ---------------------------- | --------------------------
259// /// None  | Frobenius norm               | 2-norm
260// /// 'fro' | Frobenius norm               | --
261// /// inf   | max(sum(abs(x), axis-1))     | max(abs(x))
262// /// -inf  | min(sum(abs(x), axis-1))     | min(abs(x))
263// /// 0     | --                           | sum(x !- 0)
264// /// 1     | max(sum(abs(x), axis-0))     | as below
265// /// -1    | min(sum(abs(x), axis-0))     | as below
266// /// 2     | 2-norm (largest sing. value) | as below
267// /// -2    | smallest singular value      | as below
268// /// other | --                           | sum(abs(x)**ord)**(1./ord)
269// ///
270// /// > Nuclear norm and norms based on singular values are not yet implemented.
271// ///
272// /// The Frobenius norm is given by G. H. Golub and C. F. Van Loan, *Matrix Computations*,
273// ///        Baltimore, MD, Johns Hopkins University Press, 1985, pg. 15
274// ///
275// /// The nuclear norm is the sum of the singular values.
276// ///
277// /// Both the Frobenius and nuclear norm orders are only defined for
278// /// matrices and produce a fatal error when `array.ndim != 2`
279// ///
280// /// # Params
281// ///
282// /// - `array`: input array
283// /// - `ord`: order of the norm, see table
284// /// - `axes`: axes that hold 2d matrices
285// /// - `keep_dims`: if `true` the axes which are normed over are left in the result as dimensions
286// ///   with size one
287// #[generate_macro(customize(forwarding_shim = true, root = "$crate::linalg"))]
288// #[default_device]
289// pub fn norm_device<'a>(
290//     array: impl AsRef<Array>,
291//     #[optional] ord: impl IntoOption<Ord<'a>>,
292//     #[optional] axes: impl IntoOption<&'a [i32]>,
293//     #[optional] keep_dims: impl Into<Option<bool>>,
294//     #[optional] stream: impl AsRef<Stream>,
295// ) -> Result<Array> {
296//     let ord = ord.into_option();
297//     let axes = axes.into_option();
298//     let keep_dims = keep_dims.into().unwrap_or(false);
299
300//     match (ord, axes) {
301//         // If axis and ord are both unspecified, computes the 2-norm of flatten(x).
302//         (None, None) => {
303//             let axes_ptr = std::ptr::null(); // mlx-c already handles the case where axes is null
304//             Array::try_from_op(|res| unsafe {
305//                 mlx_sys::mlx_linalg_norm(
306//                     res,
307//                     array.as_ref().as_ptr(),
308//                     axes_ptr,
309//                     0,
310//                     keep_dims,
311//                     stream.as_ref().as_ptr(),
312//                 )
313//             })
314//         }
315//         // If axis is not provided but ord is, then x must be either 1D or 2D.
316//         //
317//         // Frobenius norm is only supported for matrices
318//         (Some(Ord::Str(ord)), None) => norm_ord_device(array, ord, axes, keep_dims, stream),
319//         (Some(Ord::P(p)), None) => norm_p_device(array, p, axes, keep_dims, stream),
320//         // If axis is provided, but ord is not, then the 2-norm (or Frobenius norm for matrices) is
321//         // computed along the given axes. At most 2 axes can be specified.
322//         (None, Some(axes)) => Array::try_from_op(|res| unsafe {
323//             mlx_sys::mlx_linalg_norm(
324//                 res,
325//                 array.as_ref().as_ptr(),
326//                 axes.as_ptr(),
327//                 axes.len(),
328//                 keep_dims,
329//                 stream.as_ref().as_ptr(),
330//             )
331//         }),
332//         // If both axis and ord are provided, then the corresponding matrix or vector
333//         // norm is computed. At most 2 axes can be specified.
334//         (Some(Ord::Str(ord)), Some(axes)) => norm_ord_device(array, ord, axes, keep_dims, stream),
335//         (Some(Ord::P(p)), Some(axes)) => norm_p_device(array, p, axes, keep_dims, stream),
336//     }
337// }
338
339/// The QR factorization of the input matrix. Returns an error if the input is not valid.
340///
341/// This function supports arrays with at least 2 dimensions. The matrices which are factorized are
342/// assumed to be in the last two dimensions of the input.
343///
344/// Evaluation on the GPU is not yet implemented.
345///
346/// # Params
347///
348/// - `array`: input array
349///
350/// # Example
351///
352/// ```rust
353/// use mlx_rs::{linalg::*, with_stream, Array, Stream};
354///
355/// with_stream(&Stream::cpu(), || {
356///     let a = Array::from_slice(&[2.0f32, 3.0, 1.0, 2.0], &[2, 2]);
357///
358///     let (q, r) = qr(&a).unwrap();
359///
360///     let q_expected = Array::from_slice(&[-0.894427, -0.447214, -0.447214, 0.894427], &[2, 2]);
361///     let r_expected = Array::from_slice(&[-2.23607, -3.57771, 0.0, 0.447214], &[2, 2]);
362///
363///     assert!(q.all_close(&q_expected, None, None, None).unwrap());
364///     assert!(r.all_close(&r_expected, None, None, None).unwrap());
365/// });
366/// ```
367pub fn qr(a: impl AsRef<Array>) -> Result<(Array, Array)> {
368    let stream = Stream::thread_local_or_default();
369    <(Array, Array)>::try_from_op(|(res_0, res_1)| unsafe {
370        mlx_sys::mlx_linalg_qr(res_0, res_1, a.as_ref().as_ptr(), stream.as_ref().as_ptr())
371    })
372}
373
374/// Compatibility shim for [`qr`].
375#[generate_macro(customize(forwarding_shim = true, root = "$crate::linalg"))]
376#[deprecated(
377    since = "0.26.0",
378    note = "use `with_stream` or `with_device` around `qr`"
379)]
380pub fn qr_device(
381    a: impl AsRef<Array>,
382    #[optional] stream: impl AsRef<Stream>,
383) -> Result<(Array, Array)> {
384    crate::with_stream(stream.as_ref(), || qr(a))
385}
386
387/// The Singular Value Decomposition (SVD) of the input matrix. Returns an error if the input is not
388/// valid.
389///
390/// This function supports arrays with at least 2 dimensions. When the input has more than two
391/// dimensions, the function iterates over all indices of the first a.ndim - 2 dimensions and for
392/// each combination SVD is applied to the last two indices.
393///
394/// Evaluation on the GPU is not yet implemented.
395///
396/// # Params
397///
398/// - `array`: input array
399///
400/// # Example
401///
402/// ```rust
403/// use mlx_rs::{linalg::*, with_stream, Array, Stream};
404///
405/// with_stream(&Stream::cpu(), || {
406///     let a = Array::from_slice(&[1.0f32, 2.0, 3.0, 4.0], &[2, 2]);
407///     let (u, s, vt) = svd(&a).unwrap();
408///     let u_expected = Array::from_slice(&[-0.404554, 0.914514, -0.914514, -0.404554], &[2, 2]);
409///     let s_expected = Array::from_slice(&[5.46499, 0.365966], &[2]);
410///     let vt_expected = Array::from_slice(&[-0.576048, -0.817416, -0.817415, 0.576048], &[2, 2]);
411///     assert!(u.all_close(&u_expected, None, None, None).unwrap());
412///     assert!(s.all_close(&s_expected, None, None, None).unwrap());
413///     assert!(vt.all_close(&vt_expected, None, None, None).unwrap());
414/// });
415/// ```
416pub fn svd(array: impl AsRef<Array>) -> Result<(Array, Array, Array)> {
417    let stream = Stream::thread_local_or_default();
418    let v = VectorArray::try_from_op(|res| unsafe {
419        mlx_sys::mlx_linalg_svd(res, array.as_ref().as_ptr(), true, stream.as_ref().as_ptr())
420    })?;
421
422    let vals: SmallVec<[Array; 3]> = v.try_into_values()?;
423    let mut iter = vals.into_iter();
424    let u = iter.next().unwrap();
425    let s = iter.next().unwrap();
426    let vt = iter.next().unwrap();
427
428    Ok((u, s, vt))
429}
430
431/// Compatibility shim for [`svd`].
432#[generate_macro(customize(forwarding_shim = true, root = "$crate::linalg"))]
433#[deprecated(
434    since = "0.26.0",
435    note = "use `with_stream` or `with_device` around `svd`"
436)]
437pub fn svd_device(
438    array: impl AsRef<Array>,
439    #[optional] stream: impl AsRef<Stream>,
440) -> Result<(Array, Array, Array)> {
441    crate::with_stream(stream.as_ref(), || svd(array))
442}
443
444/// Compute the inverse of a square matrix. Returns an error if the input is not valid.
445///
446/// This function supports arrays with at least 2 dimensions. When the input has more than two
447/// dimensions, the inverse is computed for each matrix in the last two dimensions of `a`.
448///
449/// Evaluation on the GPU is not yet implemented.
450///
451/// # Params
452///
453/// - `a`: input array
454///
455/// # Example
456///
457/// ```rust
458/// use mlx_rs::{linalg::*, with_stream, Array, Stream};
459///
460/// with_stream(&Stream::cpu(), || {
461///     let a = Array::from_slice(&[1.0f32, 2.0, 3.0, 4.0], &[2, 2]);
462///     let a_inv = inv(&a).unwrap();
463///     let expected = Array::from_slice(&[-2.0, 1.0, 1.5, -0.5], &[2, 2]);
464///     assert!(a_inv.all_close(&expected, None, None, None).unwrap());
465/// });
466/// ```
467pub fn inv(a: impl AsRef<Array>) -> Result<Array> {
468    let stream = Stream::thread_local_or_default();
469    Array::try_from_op(|res| unsafe {
470        mlx_sys::mlx_linalg_inv(res, a.as_ref().as_ptr(), stream.as_ref().as_ptr())
471    })
472}
473
474/// Compatibility shim for [`inv`].
475#[generate_macro(customize(forwarding_shim = true, root = "$crate::linalg"))]
476#[deprecated(
477    since = "0.26.0",
478    note = "use `with_stream` or `with_device` around `inv`"
479)]
480pub fn inv_device(a: impl AsRef<Array>, #[optional] stream: impl AsRef<Stream>) -> Result<Array> {
481    crate::with_stream(stream.as_ref(), || inv(a))
482}
483
484/// Compute the Cholesky decomposition of a real symmetric positive semi-definite matrix.
485///
486/// This function supports arrays with at least 2 dimensions. When the input has more than two
487/// dimensions, the Cholesky decomposition is computed for each matrix in the last two dimensions of
488/// `a`.
489///
490/// If the input matrix is not symmetric positive semi-definite, behaviour is undefined.
491///
492/// # Params
493///
494/// - `a`: input array
495/// - `upper`: If `true`, return the upper triangular Cholesky factor. If `false`, return the lower
496///   triangular Cholesky factor. Default: `false`.
497pub fn cholesky(a: impl AsRef<Array>, upper: Option<bool>) -> Result<Array> {
498    let stream = Stream::thread_local_or_default();
499    let upper = upper.unwrap_or(false);
500    Array::try_from_op(|res| unsafe {
501        mlx_sys::mlx_linalg_cholesky(res, a.as_ref().as_ptr(), upper, stream.as_ref().as_ptr())
502    })
503}
504
505/// Compatibility shim for [`cholesky`].
506#[generate_macro(customize(forwarding_shim = true, root = "$crate::linalg"))]
507#[deprecated(
508    since = "0.26.0",
509    note = "use `with_stream` or `with_device` around `cholesky`"
510)]
511pub fn cholesky_device(
512    a: impl AsRef<Array>,
513    #[optional] upper: Option<bool>,
514    #[optional] stream: impl AsRef<Stream>,
515) -> Result<Array> {
516    crate::with_stream(stream.as_ref(), || cholesky(a, upper))
517}
518
519/// Compute the inverse of a real symmetric positive semi-definite matrix using it’s Cholesky decomposition.
520///
521/// Please see the python documentation for more details.
522pub fn cholesky_inv(a: impl AsRef<Array>, upper: Option<bool>) -> Result<Array> {
523    let stream = Stream::thread_local_or_default();
524    let upper = upper.unwrap_or(false);
525    Array::try_from_op(|res| unsafe {
526        mlx_sys::mlx_linalg_cholesky_inv(res, a.as_ref().as_ptr(), upper, stream.as_ref().as_ptr())
527    })
528}
529
530/// Compatibility shim for [`cholesky_inv`].
531#[generate_macro(customize(forwarding_shim = true, root = "$crate::linalg"))]
532#[deprecated(
533    since = "0.26.0",
534    note = "use `with_stream` or `with_device` around `cholesky_inv`"
535)]
536pub fn cholesky_inv_device(
537    a: impl AsRef<Array>,
538    #[optional] upper: Option<bool>,
539    #[optional] stream: impl AsRef<Stream>,
540) -> Result<Array> {
541    crate::with_stream(stream.as_ref(), || cholesky_inv(a, upper))
542}
543
544/// Compute the cross product of two arrays along a specified axis.
545///
546/// The cross product is defined for arrays with size 2 or 3 in the specified axis. If the size is 2
547/// then the third value is assumed to be zero.
548pub fn cross(a: impl AsRef<Array>, b: impl AsRef<Array>, axis: Option<i32>) -> Result<Array> {
549    let stream = Stream::thread_local_or_default();
550    let axis = axis.unwrap_or(-1);
551    Array::try_from_op(|res| unsafe {
552        mlx_sys::mlx_linalg_cross(
553            res,
554            a.as_ref().as_ptr(),
555            b.as_ref().as_ptr(),
556            axis,
557            stream.as_ref().as_ptr(),
558        )
559    })
560}
561
562/// Compatibility shim for [`cross`].
563#[generate_macro(customize(forwarding_shim = true, root = "$crate::linalg"))]
564#[deprecated(
565    since = "0.26.0",
566    note = "use `with_stream` or `with_device` around `cross`"
567)]
568pub fn cross_device(
569    a: impl AsRef<Array>,
570    b: impl AsRef<Array>,
571    #[optional] axis: Option<i32>,
572    #[optional] stream: impl AsRef<Stream>,
573) -> Result<Array> {
574    crate::with_stream(stream.as_ref(), || cross(a, b, axis))
575}
576
577/// Compute the eigenvalues and eigenvectors of a complex Hermitian or real symmetric matrix.
578///
579/// This function supports arrays with at least 2 dimensions. When the input has more than two
580/// dimensions, the eigenvalues and eigenvectors are computed for each matrix in the last two
581/// dimensions.
582pub fn eigh(a: impl AsRef<Array>, uplo: Option<&str>) -> Result<(Array, Array)> {
583    let stream = Stream::thread_local_or_default();
584    let a = a.as_ref();
585    let uplo = CString::new(uplo.unwrap_or("L")).map_err(|e| Exception::custom(format!("{e}")))?;
586
587    <(Array, Array) as Guarded>::try_from_op(|(res_0, res_1)| unsafe {
588        mlx_sys::mlx_linalg_eigh(
589            res_0,
590            res_1,
591            a.as_ptr(),
592            uplo.as_ptr(),
593            stream.as_ref().as_ptr(),
594        )
595    })
596}
597
598/// Compatibility shim for [`eigh`].
599#[generate_macro(customize(forwarding_shim = true, root = "$crate::linalg"))]
600#[deprecated(
601    since = "0.26.0",
602    note = "use `with_stream` or `with_device` around `eigh`"
603)]
604pub fn eigh_device(
605    a: impl AsRef<Array>,
606    #[optional] uplo: Option<&str>,
607    #[optional] stream: impl AsRef<Stream>,
608) -> Result<(Array, Array)> {
609    crate::with_stream(stream.as_ref(), || eigh(a, uplo))
610}
611
612/// Compute the eigenvalues of a complex Hermitian or real symmetric matrix.
613///
614/// This function supports arrays with at least 2 dimensions. When the input has more than two
615/// dimensions, the eigenvalues are computed for each matrix in the last two dimensions.
616pub fn eigvalsh(a: impl AsRef<Array>, uplo: Option<&str>) -> Result<Array> {
617    let stream = Stream::thread_local_or_default();
618    let a = a.as_ref();
619    let uplo = CString::new(uplo.unwrap_or("L")).map_err(|e| Exception::custom(format!("{e}")))?;
620    Array::try_from_op(|res| unsafe {
621        mlx_sys::mlx_linalg_eigvalsh(res, a.as_ptr(), uplo.as_ptr(), stream.as_ref().as_ptr())
622    })
623}
624
625/// Compatibility shim for [`eigvalsh`].
626#[generate_macro(customize(forwarding_shim = true, root = "$crate::linalg"))]
627#[deprecated(
628    since = "0.26.0",
629    note = "use `with_stream` or `with_device` around `eigvalsh`"
630)]
631pub fn eigvalsh_device(
632    a: impl AsRef<Array>,
633    #[optional] uplo: Option<&str>,
634    #[optional] stream: impl AsRef<Stream>,
635) -> Result<Array> {
636    crate::with_stream(stream.as_ref(), || eigvalsh(a, uplo))
637}
638
639/// Compute the eigenvalues and eigenvectors of a square matrix.
640///
641/// This function supports arrays with at least 2 dimensions. When the input has more than two
642/// dimensions, the eigenvalues and eigenvectors are computed for each matrix in the last two
643/// dimensions.
644///
645/// Unlike [`eigh`], this function computes eigenvalues for general (not necessarily symmetric
646/// or Hermitian) matrices. The eigenvalues and eigenvectors may be complex.
647///
648/// # Params
649///
650/// - `a`: Input array. Must be a square matrix.
651///
652/// # Returns
653///
654/// A tuple `(eigenvalues, eigenvectors)` where eigenvalues has shape `(..., N)` and
655/// eigenvectors has shape `(..., N, N)`. The eigenvectors are stored as columns.
656///
657/// # Example
658///
659/// ```rust
660/// use mlx_rs::{linalg::*, with_stream, Array, Stream};
661///
662/// with_stream(&Stream::cpu(), || {
663///     let a = Array::from_slice(&[1.0f32, 1.0, 3.0, 4.0], &[2, 2]);
664///     let (eigenvalues, eigenvectors) = eig(&a).unwrap();
665///     // eigenvalues and eigenvectors are complex even for real input
666/// });
667/// ```
668pub fn eig(a: impl AsRef<Array>) -> Result<(Array, Array)> {
669    let stream = Stream::thread_local_or_default();
670    <(Array, Array) as Guarded>::try_from_op(|(res_0, res_1)| unsafe {
671        mlx_sys::mlx_linalg_eig(res_0, res_1, a.as_ref().as_ptr(), stream.as_ref().as_ptr())
672    })
673}
674
675/// Compatibility shim for [`eig`].
676#[generate_macro(customize(forwarding_shim = true, root = "$crate::linalg"))]
677#[deprecated(
678    since = "0.26.0",
679    note = "use `with_stream` or `with_device` around `eig`"
680)]
681pub fn eig_device(
682    a: impl AsRef<Array>,
683    #[optional] stream: impl AsRef<Stream>,
684) -> Result<(Array, Array)> {
685    crate::with_stream(stream.as_ref(), || eig(a))
686}
687
688/// Compute the eigenvalues of a square matrix.
689///
690/// This function supports arrays with at least 2 dimensions. When the input has more than two
691/// dimensions, the eigenvalues are computed for each matrix in the last two dimensions.
692///
693/// Unlike [`eigvalsh`], this function computes eigenvalues for general (not necessarily symmetric
694/// or Hermitian) matrices. The eigenvalues may be complex.
695///
696/// # Params
697///
698/// - `a`: Input array. Must be a square matrix.
699///
700/// # Returns
701///
702/// An array of eigenvalues with shape `(..., N)`.
703///
704/// # Example
705///
706/// ```rust
707/// use mlx_rs::{linalg::*, with_stream, Array, Stream};
708///
709/// with_stream(&Stream::cpu(), || {
710///     let a = Array::from_slice(&[1.0f32, 1.0, 3.0, 4.0], &[2, 2]);
711///     let eigenvalues = eigvals(&a).unwrap();
712/// });
713/// ```
714pub fn eigvals(a: impl AsRef<Array>) -> Result<Array> {
715    let stream = Stream::thread_local_or_default();
716    Array::try_from_op(|res| unsafe {
717        mlx_sys::mlx_linalg_eigvals(res, a.as_ref().as_ptr(), stream.as_ref().as_ptr())
718    })
719}
720
721/// Compatibility shim for [`eigvals`].
722#[generate_macro(customize(forwarding_shim = true, root = "$crate::linalg"))]
723#[deprecated(
724    since = "0.26.0",
725    note = "use `with_stream` or `with_device` around `eigvals`"
726)]
727pub fn eigvals_device(
728    a: impl AsRef<Array>,
729    #[optional] stream: impl AsRef<Stream>,
730) -> Result<Array> {
731    crate::with_stream(stream.as_ref(), || eigvals(a))
732}
733
734/// Compute the (Moore-Penrose) pseudo-inverse of a matrix.
735pub fn pinv(a: impl AsRef<Array>) -> Result<Array> {
736    let stream = Stream::thread_local_or_default();
737    Array::try_from_op(|res| unsafe {
738        mlx_sys::mlx_linalg_pinv(res, a.as_ref().as_ptr(), stream.as_ref().as_ptr())
739    })
740}
741
742/// Compatibility shim for [`pinv`].
743#[generate_macro(customize(forwarding_shim = true, root = "$crate::linalg"))]
744#[deprecated(
745    since = "0.26.0",
746    note = "use `with_stream` or `with_device` around `pinv`"
747)]
748pub fn pinv_device(a: impl AsRef<Array>, #[optional] stream: impl AsRef<Stream>) -> Result<Array> {
749    crate::with_stream(stream.as_ref(), || pinv(a))
750}
751
752/// Compute the inverse of a triangular square matrix.
753///
754/// This function supports arrays with at least 2 dimensions. When the input has more than two
755/// dimensions, the inverse is computed for each matrix in the last two dimensions of a.
756pub fn tri_inv(a: impl AsRef<Array>, upper: Option<bool>) -> Result<Array> {
757    let stream = Stream::thread_local_or_default();
758    let upper = upper.unwrap_or(false);
759    Array::try_from_op(|res| unsafe {
760        mlx_sys::mlx_linalg_tri_inv(res, a.as_ref().as_ptr(), upper, stream.as_ref().as_ptr())
761    })
762}
763
764/// Compatibility shim for [`tri_inv`].
765#[generate_macro(customize(forwarding_shim = true, root = "$crate::linalg"))]
766#[deprecated(
767    since = "0.26.0",
768    note = "use `with_stream` or `with_device` around `tri_inv`"
769)]
770pub fn tri_inv_device(
771    a: impl AsRef<Array>,
772    #[optional] upper: Option<bool>,
773    #[optional] stream: impl AsRef<Stream>,
774) -> Result<Array> {
775    crate::with_stream(stream.as_ref(), || tri_inv(a, upper))
776}
777
778/// Compute the LU factorization of the given matrix A.
779///
780/// Note, unlike the default behavior of scipy.linalg.lu, the pivots are
781/// indices. To reconstruct the input use L[P, :] @ U for 2 dimensions or
782/// mx.take_along_axis(L, P[..., None], axis=-2) @ U for more than 2 dimensions.
783///
784/// To construct the full permuation matrix do:
785///
786/// ```rust,ignore
787/// use mlx_rs::{array, linalg::lu, with_stream, Stream};
788///
789/// // python
790/// // P = mx.put_along_axis(mx.zeros_like(L), p[..., None], mx.array(1.0), axis=-1)
791/// with_stream(&Stream::cpu(), || {
792///     let a = array!([[3.0f32, 1.0, 2.0], [1.0, 8.0, 6.0], [9.0, 2.0, 5.0]]);
793///     let (p, l, u) = lu(&a).unwrap();
794///     let p = mlx_rs::ops::put_along_axis(
795///         mlx_rs::ops::zeros_like(&l),
796///         p.index((Ellipsis, NewAxis)),
797///         array!(1.0),
798///         -1,
799///     ).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}