Skip to main content

mdarray_linalg/
utils.rs

1//! Utility functions for matrix printing, shape retrieval, identity
2//! generation, Kronecker product, trace, transpose operations, ...
3//!
4//! This module contains small user-facing utilities, plus hidden unstable
5//! helpers used by backend implementation crates. It is not meant to be a
6//! complete collection of linear algebra utilities at this time.
7
8use mdarray::{Array, Dim, Layout, Shape, Slice, tensor};
9use num_complex::ComplexFloat;
10use num_traits::{One, Zero};
11
12/// Displays a numeric `mdarray` in a human-readable format (NumPy-style)
13pub fn pretty_print<T: ComplexFloat + std::fmt::Display, D0: Dim, D1: Dim>(mat: &Array<T, (D0, D1)>)
14where
15    <T as num_complex::ComplexFloat>::Real: std::fmt::Display,
16{
17    let shape = mat.shape();
18    for i in 0..shape.dim(0) {
19        for j in 0..shape.dim(1) {
20            let v = mat[[i, j]];
21            print!("{:>10.4} {:+.4}i  ", v.re(), v.im(),);
22        }
23        println!();
24    }
25    println!();
26}
27
28// The following backend-oriented helpers are exported for workspace backend
29// crates. They are hidden from generated documentation and may be redesigned
30// before the public API stabilizes.
31/// Safely casts a value to `i32`
32#[doc(hidden)]
33pub fn into_i32<T>(x: T) -> i32
34where
35    T: TryInto<i32>,
36    <T as TryInto<i32>>::Error: std::fmt::Debug,
37{
38    x.try_into().expect("dimension must fit into i32")
39}
40
41/// Make sure that matrix shapes are compatible with `C = A * B`, and
42/// return the dimensions `(m, n, k)` safely cast to `i32`, where `C` is `(m
43/// x n)`, and `k` is the common dimension of `A` and `B`
44#[doc(hidden)]
45pub fn dims3(a_shape: impl Shape, b_shape: impl Shape, c_shape: impl Shape) -> (i32, i32, i32) {
46    let (m, k) = (a_shape.dim(0), a_shape.dim(1));
47    let (k2, n) = (b_shape.dim(0), b_shape.dim(1));
48    let (m2, n2) = (c_shape.dim(0), c_shape.dim(1));
49
50    assert!(m == m2, "a and c must agree in number of rows");
51    assert!(n == n2, "b and c must agree in number of columns");
52    assert!(
53        k == k2,
54        "a's number of columns must be equal to b's number of rows"
55    );
56
57    (into_i32(m), into_i32(n), into_i32(k))
58}
59
60/// Make sure that matrix shapes are compatible with `A * B`, and return
61/// the dimensions `(m, n)` safely cast to `i32`
62#[doc(hidden)]
63pub fn dims2(a_shape: impl Shape, b_shape: impl Shape) -> (i32, i32) {
64    let (m, k) = (a_shape.dim(0), a_shape.dim(1));
65    let (k2, n) = (b_shape.dim(0), b_shape.dim(1));
66
67    assert!(
68        k == k2,
69        "a's number of columns must be equal to b's number of rows"
70    );
71
72    (into_i32(m), into_i32(n))
73}
74
75/// Transposes a matrix in-place. Dimensions stay the same, only the memory ordering changes.
76/// - For square matrices: swaps elements across the main diagonal.
77/// - For rectangular matrices: reshuffles data in a temporary buffer so that the
78///   same `(rows, cols)` slice now represents the transposed layout.
79#[doc(hidden)]
80pub fn transpose_in_place<T, D0, D1, L>(c: &mut Slice<T, (D0, D1), L>)
81where
82    T: ComplexFloat + Default,
83    D0: Dim,
84    D1: Dim,
85    L: Layout,
86{
87    let (m, n) = *c.shape();
88
89    let m = m.size();
90    let n = n.size();
91
92    if n == m {
93        for i in 0..m {
94            for j in (i + 1)..n {
95                c.swap(i * n + j, j * n + i);
96            }
97        }
98    } else {
99        let mut result = tensor![[T::default(); m]; n];
100        for j in 0..n {
101            for i in 0..m {
102                result[j * m + i] = c[i * n + j];
103            }
104        }
105        for j in 0..n {
106            for i in 0..m {
107                c[j * m + i] = result[j * m + i];
108            }
109        }
110    }
111}
112
113/// Conjugates a matrix in-place.
114/// For complex matrices, replaces each element z with its conjugate conj(z).
115/// For real matrices, this is a no-op.
116#[doc(hidden)]
117pub fn conjugate_in_place<T, D0, D1, L>(c: &mut Slice<T, (D0, D1), L>)
118where
119    T: ComplexFloat + Default,
120    D0: Dim,
121    D1: Dim,
122    L: Layout,
123{
124    c.iter_mut().for_each(|elem| *elem = elem.conj());
125}
126
127/// Convert pivot indices to permutation matrix
128#[doc(hidden)]
129pub fn ipiv_to_perm_mat<T: ComplexFloat, D0: Dim, D1: Dim>(
130    ipiv: &[i32],
131    m: usize,
132) -> Array<T, (D0, D1)> {
133    let mut p = Array::from_elem(<(D0, D1) as Shape>::from_dims(&[m, m]), T::zero());
134
135    for i in 0..m {
136        p[[i, i]] = T::one();
137    }
138
139    // Apply row swaps according to LAPACK's ipiv convention
140    for i in 0..ipiv.len() {
141        let pivot_row = (ipiv[i] - 1) as usize; // LAPACK uses 1-based indexing
142        if pivot_row != i {
143            for j in 0..m {
144                let temp = p[[i, j]];
145                p[[i, j]] = p[[pivot_row, j]];
146                p[[pivot_row, j]] = temp;
147            }
148        }
149    }
150
151    p
152}
153
154/// Given an input matrix of shape `(m × n)`, this function creates and returns
155/// a new matrix of shape `(n × m)`, where each element at position `(i, j)` in the
156/// original is moved to position `(j, i)` in the result.
157#[doc(hidden)]
158pub fn to_col_major<T, D0: Dim, D1: Dim, L>(c: &Slice<T, (D0, D1), L>) -> Array<T, (D1, D0)>
159where
160    T: ComplexFloat + Default + Clone,
161    L: Layout,
162{
163    let csh = *c.shape();
164    let (m, n) = (csh.dim(0), csh.dim(1));
165
166    let shape = <(D1, D0) as Shape>::from_dims(&[n, m]);
167    let mut result = Array::<T, (D1, D0)>::zeros(shape);
168
169    for i in 0..m {
170        for j in 0..n {
171            result[[j, i]] = c[[i, j]];
172        }
173    }
174
175    result
176}
177
178/// Computes the trace of a square matrix (sum of diagonal elements).
179/// # Examples
180/// ```
181/// use mdarray::tensor;
182/// use mdarray_linalg::utils::trace;
183///
184/// let a = tensor![[1., 2., 3.],
185///                 [4., 5., 6.],
186///                 [7., 8., 9.]];
187///
188/// let tr = trace(&a);
189/// assert_eq!(tr, 15.0);
190/// ```
191pub fn trace<T, D0, D1, L>(a: &Slice<T, (D0, D1), L>) -> T
192where
193    T: ComplexFloat + std::ops::Add<Output = T> + Copy,
194    D0: Dim,
195    D1: Dim,
196    L: Layout,
197{
198    let ash = *a.shape();
199    let (m, n) = (ash.dim(0), ash.dim(1));
200    assert_eq!(m, n, "trace is only defined for square matrices");
201
202    let mut tr = T::zero();
203    for i in 0..n {
204        tr = tr + a[[i, i]];
205    }
206    tr
207}
208
209/// Creates an identity matrix of size `n x n`.
210/// # Examples
211/// ```
212/// use mdarray::tensor;
213/// use mdarray_linalg::utils::identity;
214///
215/// let i3 = identity::<f64, usize, usize>(3);
216/// assert_eq!(i3, tensor![[1.,0.,0.],[0.,1.,0.],[0.,0.,1.]]);
217/// ```
218pub fn identity<T: Zero + One, D0: Dim, D1: Dim>(n: usize) -> Array<T, (D0, D1)> {
219    Array::<T, (D0, D1)>::from_fn(<(D0, D1) as Shape>::from_dims(&[n, n]), |i| {
220        if i[0] == i[1] { T::one() } else { T::zero() }
221    })
222}
223
224/// Creates a diagonal matrix of size `n x n` with ones on a specified diagonal.
225///
226/// The diagonal can be shifted using `k`:
227/// - `k = 0` → main diagonal (default, standard identity)
228/// - `k > 0` → k-th diagonal above the main one
229/// - `k < 0` → k-th diagonal below the main one
230/// # Examples
231/// ```
232/// use mdarray::{Const, tensor};
233/// use mdarray_linalg::utils::identity_k;
234///
235/// let i3 = identity_k::<f64, Const<3>, Const<3>>(3, 1);
236/// assert_eq!(i3, tensor![[0.,1.,0.],[0.,0.,1.],[0.,0.,0.]]);
237/// ```
238pub fn identity_k<T: Zero + One, D0: Dim, D1: Dim>(n: usize, k: isize) -> Array<T, (D0, D1)> {
239    Array::<T, (D0, D1)>::from_fn(<(D0, D1) as Shape>::from_dims(&[n, n]), |i| {
240        if (i[1] as isize - i[0] as isize) == k {
241            T::one()
242        } else {
243            T::zero()
244        }
245    })
246}
247
248/// Computes the Kronecker product of two 2D tensors.
249///
250/// The Kronecker product of matrices `A (m×n)` and `B (p×q)` is defined as the
251/// block matrix of size `(m*p) × (n*q)` where each element `a[i, j]` of `A`
252/// multiplies the entire matrix `B`.
253///
254/// # Examples
255/// ```
256/// use mdarray::tensor;
257/// use mdarray_linalg::utils::kron;
258///
259/// let a = tensor![[1., 2.],
260///                 [3., 4.]];
261///
262/// let b = tensor![[0., 5.],
263///                 [6., 7.]];
264///
265/// let k = kron(&a, &b);
266///
267/// assert_eq!(k, tensor![
268///     [ 0.,  5.,  0., 10.],
269///     [ 6.,  7., 12., 14.],
270///     [ 0., 15.,  0., 20.],
271///     [18., 21., 24., 28.]
272/// ]);
273/// ```
274pub fn kron<T, D0, D1, La, Lb>(
275    a: &Slice<T, (D0, D1), La>,
276    b: &Slice<T, (D0, D1), Lb>,
277) -> Array<T, (D0, D1)>
278where
279    T: ComplexFloat + std::ops::Mul<Output = T> + Copy,
280    D0: Dim,
281    D1: Dim,
282    La: Layout,
283    Lb: Layout,
284{
285    let ash = *a.shape();
286    let (ma, na) = (ash.dim(0), ash.dim(1));
287
288    let bsh = *b.shape();
289    let (mb, nb) = (bsh.dim(0), bsh.dim(1));
290
291    let out_shape = <(D0, D1) as Shape>::from_dims(&[ma * mb, na * nb]);
292
293    Array::<T, (D0, D1)>::from_fn(out_shape, |idx| {
294        let i = idx[0];
295        let j = idx[1];
296
297        let ai = i / mb;
298        let bi = i % mb;
299        let aj = j / nb;
300        let bj = j % nb;
301
302        a[[ai, aj]] * b[[bi, bj]]
303    })
304}
305
306/// Converts a flat index to multidimensional coordinates.
307///
308/// # Examples
309///
310/// ```
311/// use mdarray::DArray;
312/// use mdarray_linalg::utils::unravel_index;
313///
314/// let x = DArray::<usize, 2>::from_fn([2,3], |i| i[0] + i[1]);
315///
316/// assert_eq!(unravel_index(&x, 0), vec![0, 0]);
317/// assert_eq!(unravel_index(&x, 4), vec![1, 1]);
318/// assert_eq!(unravel_index(&x, 5), vec![1, 2]);
319/// ```
320///
321/// # Panics
322///
323/// Panics if `flat` is out of bounds (>= `x.len()`).
324pub fn unravel_index<T, S: Shape, L: Layout>(x: &Slice<T, S, L>, mut flat: usize) -> Vec<usize> {
325    let rank = x.rank();
326
327    assert!(
328        flat < x.len(),
329        "flat index out of bounds: {} >= {}",
330        flat,
331        x.len()
332    );
333
334    let mut coords = vec![0usize; rank];
335
336    for i in (0..rank).rev() {
337        let dim = x.shape().dim(i);
338        coords[i] = flat % dim;
339        flat /= dim;
340    }
341
342    coords
343}
344
345/// Creates a diagonal matrix from a 1D slice, placing its elements on the main diagonal.
346///
347/// # Examples
348/// ```
349/// use mdarray::{Const, array, view};
350/// use mdarray_linalg::utils::diag;
351///
352/// let v = view![1., 2., 3.];
353/// let d = diag(&v);
354/// assert_eq!(d, array![[1.,0.,0.],[0.,2.,0.],[0.,0.,3.]]);
355/// ```
356pub fn diag<T: Zero + One + Clone, D: Dim>(v: &Slice<T, (D,)>) -> Array<T, (D, D)> {
357    let n = v.dim(0);
358    Array::<T, (D, D)>::from_fn(<(D, D) as Shape>::from_dims(&[n, n]), |i| {
359        if i[0] == i[1] {
360            v[i[0]].clone()
361        } else {
362            T::zero()
363        }
364    })
365}