Skip to main content

tensor4all_tensorbackend/
matrix.rs

1//! Dense column-major matrix type and utility functions.
2//!
3//! [`Matrix<T>`] is a simple dense 2D matrix in column-major layout, indexed
4//! by `m[[row, col]]`. It is the shared dense matrix boundary for tensor4all
5//! crates that need flat buffers and backend-backed matrix multiplication.
6//!
7//! # Examples
8//!
9//! ```
10//! use tensor4all_tensorbackend::{from_vec2d, Matrix};
11//!
12//! let m = from_vec2d(vec![
13//!     vec![1.0_f64, 2.0],
14//!     vec![3.0, 4.0],
15//! ]);
16//! assert_eq!(m.nrows(), 2);
17//! assert_eq!(m.ncols(), 2);
18//! assert_eq!(m[[0, 1]], 2.0);
19//! assert_eq!(m[[1, 0]], 3.0);
20//! ```
21
22use anyhow::{ensure, Context, Result};
23use num_complex::{Complex32, Complex64};
24use num_traits::{One, Zero};
25use std::ops::{Index, IndexMut};
26use tenferro::{DType, Tensor, TensorScalar, TypedTensor};
27use tenferro_ad::EagerTensor;
28use tenferro_linalg::EagerTensorLinalgExt;
29
30/// A dense 2D matrix in column-major layout.
31///
32/// Access elements with `m[[row, col]]` syntax. Data is stored contiguously
33/// in column-major order, so flat buffers use `row + nrows * col`.
34///
35/// # Examples
36///
37/// ```
38/// use tensor4all_tensorbackend::Matrix;
39///
40/// let mut m = Matrix::zeros(2, 3);
41/// m[[0, 1]] = 5.0_f64;
42/// assert_eq!(m[[0, 1]], 5.0);
43/// assert_eq!(m[[0, 0]], 0.0);
44/// assert_eq!(m.nrows(), 2);
45/// assert_eq!(m.ncols(), 3);
46/// ```
47#[derive(Debug, Clone)]
48pub struct Matrix<T> {
49    data: Vec<T>,
50    nrows: usize,
51    ncols: usize,
52}
53
54fn checked_matrix_len(nrows: usize, ncols: usize) -> Option<usize> {
55    nrows.checked_mul(ncols)
56}
57
58/// Error returned when converting a [`TypedTensor`] into a [`Matrix`].
59///
60/// Use this when accepting dynamic tensor-shaped values at a dense-matrix
61/// boundary. It reports whether conversion failed because the tensor was not a
62/// rank-2 matrix or because its host buffer could not be consumed.
63///
64/// # Examples
65///
66/// ```
67/// use tenferro::TypedTensor;
68/// use tensor4all_tensorbackend::Matrix;
69/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
70///
71/// let tensor = TypedTensor::from_vec_col_major(vec![2, 1, 1], vec![1.0_f64, 2.0])?;
72/// let err = Matrix::try_from_typed_tensor(tensor).unwrap_err();
73/// assert!(err.to_string().contains("rank-2 tensor"));
74/// # Ok(())
75/// # }
76/// ```
77#[derive(Debug, thiserror::Error)]
78pub enum MatrixTensorConversionError {
79    /// The input tensor rank was not two.
80    #[error("expected a rank-2 tensor, got shape {shape:?}")]
81    Rank {
82        /// Tensor shape that failed the rank check.
83        shape: Vec<usize>,
84    },
85    /// The tensor did not contain an owned host buffer that can be consumed.
86    #[error("failed to consume typed tensor host buffer: {message}")]
87    HostBuffer {
88        /// Backend conversion error reported by tenferro.
89        message: String,
90    },
91}
92
93/// Error returned when matrix shape or index validation fails.
94///
95/// Constructors use this type for malformed dimensions or payloads. In-place
96/// mutation helpers use it to reject caller-supplied indices before changing
97/// any matrix values.
98///
99/// # Examples
100///
101/// ```
102/// use tensor4all_tensorbackend::{try_from_vec2d, MatrixShapeError};
103///
104/// let err = try_from_vec2d(vec![vec![1.0_f64, 2.0], vec![3.0]]).unwrap_err();
105/// assert!(matches!(
106///     err,
107///     MatrixShapeError::RaggedRows {
108///         row: 1,
109///         expected: 2,
110///         actual: 1,
111///     }
112/// ));
113/// ```
114#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
115pub enum MatrixShapeError {
116    /// A later row had a different length than the first row.
117    #[error("row {row} has length {actual}, expected {expected}")]
118    RaggedRows {
119        /// Zero-based row number with the mismatched length.
120        row: usize,
121        /// Column count established by the first row.
122        expected: usize,
123        /// Actual number of entries in `row`.
124        actual: usize,
125    },
126    /// The matrix element count overflowed `usize`.
127    #[error("matrix shape {nrows}x{ncols} overflows usize")]
128    ShapeOverflow {
129        /// Number of rows.
130        nrows: usize,
131        /// Number of columns.
132        ncols: usize,
133    },
134    /// The requested row index is outside the matrix.
135    #[error("row index {index} is out of bounds for {nrows} rows")]
136    RowIndexOutOfBounds {
137        /// Rejected zero-based row index.
138        index: usize,
139        /// Number of rows in the matrix.
140        nrows: usize,
141    },
142    /// The requested column index is outside the matrix.
143    #[error("column index {index} is out of bounds for {ncols} columns")]
144    ColumnIndexOutOfBounds {
145        /// Rejected zero-based column index.
146        index: usize,
147        /// Number of columns in the matrix.
148        ncols: usize,
149    },
150    /// The flat data length did not match the matrix shape.
151    #[error("matrix data has length {actual}, expected {expected}")]
152    DataLengthMismatch {
153        /// Number of supplied elements.
154        actual: usize,
155        /// Number of elements implied by the shape.
156        expected: usize,
157    },
158}
159
160/// Error returned by matrix multiplication entry points.
161///
162/// Wraps the backend/einsum diagnostic, preserving its source chain.
163#[derive(Debug, thiserror::Error)]
164#[error("matrix multiplication failed: {source}")]
165pub struct MatrixMulError {
166    /// Original backend or einsum diagnostic.
167    #[source]
168    pub source: anyhow::Error,
169}
170
171impl From<anyhow::Error> for MatrixMulError {
172    fn from(source: anyhow::Error) -> Self {
173        Self { source }
174    }
175}
176
177/// Error returned by [`lowest_hermitian_eigenpair`].
178///
179/// The eigensolver is intended for small Rayleigh-Ritz projected matrices; it validates shape and Hermitian structure before calling the backend Hermitian
180/// eigendecomposition, symmetrizing only roundoff that is within the requested
181/// tolerance. Non-Hermitian effective operators are rejected explicitly instead
182/// of silently taking a real part.
183#[derive(Debug, thiserror::Error)]
184pub enum HermitianEigenError {
185    /// The matrix has zero rows and columns, so it has no eigenpair.
186    #[error("Hermitian eigenpair requires a non-empty matrix")]
187    Empty,
188    /// The input is not square.
189    #[error("Hermitian eigenpair requires a square matrix, got {nrows}x{ncols}")]
190    NonSquare {
191        /// Number of matrix rows.
192        nrows: usize,
193        /// Number of matrix columns.
194        ncols: usize,
195    },
196    /// The Hermitian validation tolerance was negative or not finite.
197    #[error("Hermitian tolerance must be finite and non-negative, got {tolerance}")]
198    InvalidTolerance {
199        /// Rejected tolerance value.
200        tolerance: f64,
201    },
202    /// A matrix entry violates `A[i, j] = conj(A[j, i])` within tolerance.
203    #[error(
204        "matrix is not Hermitian at ({row}, {col}): difference {difference} exceeds tolerance {tolerance}"
205    )]
206    NonHermitian {
207        /// Row of the first offending entry.
208        row: usize,
209        /// Column of the first offending entry.
210        col: usize,
211        /// Absolute Hermitian residual for the offending pair.
212        difference: f64,
213        /// Effective tolerance used for this entry pair.
214        tolerance: f64,
215    },
216    /// The backend returned an output with an unexpected dtype.
217    #[error("{output} output dtype mismatch: expected {expected}, got {actual}")]
218    DType {
219        /// Output tensor name.
220        output: &'static str,
221        /// Expected dtype string.
222        expected: String,
223        /// Actual dtype string.
224        actual: String,
225    },
226    /// The backend returned an output with an unexpected shape.
227    #[error("{output} output shape mismatch: expected {expected:?}, got {actual:?}")]
228    Shape {
229        /// Output tensor name.
230        output: &'static str,
231        /// Expected shape.
232        expected: Vec<usize>,
233        /// Actual shape.
234        actual: Vec<usize>,
235    },
236    /// A Hermitian backend eigenvalue had a non-negligible imaginary part.
237    #[error(
238        "Hermitian eigenvalue {index} has imaginary part {imaginary}, exceeding tolerance {tolerance}"
239    )]
240    NonRealEigenvalue {
241        /// Eigenvalue position in the backend output.
242        index: usize,
243        /// Absolute imaginary part.
244        imaginary: f64,
245        /// Tolerance used for validation.
246        tolerance: f64,
247    },
248    /// The tenferro backend rejected or failed the eigendecomposition.
249    #[error("Hermitian eigendecomposition failed: {source}")]
250    Backend {
251        /// Original backend diagnostic.
252        #[source]
253        source: Box<dyn std::error::Error + Send + Sync + 'static>,
254    },
255}
256
257/// Small Hermitian eigenpair returned by [`lowest_hermitian_eigenpair`].
258///
259/// `eigenvector` stores the Ritz vector coefficients in ordinary vector order.
260/// It has length equal to the input matrix dimension and is normalized according
261/// to the backend eigendecomposition.
262#[derive(Debug, Clone, PartialEq)]
263pub struct HermitianEigenpair<T> {
264    /// Smallest eigenvalue of the Hermitian matrix.
265    pub eigenvalue: f64,
266    /// Corresponding eigenvector coefficients.
267    pub eigenvector: Vec<T>,
268}
269
270/// Full eigendecomposition of a small Hermitian projected matrix.
271///
272/// `eigenvectors` stores one normalized eigenvector per column in column-major
273/// [`Matrix`] layout. Eigenvalues are returned in the backend's ascending
274/// Hermitian eigensolver order.
275///
276/// # Examples
277///
278/// ```
279/// use tensor4all_tensorbackend::{hermitian_eigendecomposition, Matrix};
280///
281/// let matrix = Matrix::from_col_major_vec(2, 2, vec![1.0, 0.0, 0.0, 2.0]);
282/// let decomp = hermitian_eigendecomposition(&matrix, 1.0e-12).unwrap();
283/// assert_eq!(decomp.eigenvalues, vec![1.0, 2.0]);
284/// assert_eq!(decomp.eigenvectors.nrows(), 2);
285/// assert_eq!(decomp.eigenvectors.ncols(), 2);
286/// ```
287#[derive(Debug, Clone)]
288pub struct HermitianEigendecomposition<T> {
289    /// Real eigenvalues of the Hermitian matrix.
290    pub eigenvalues: Vec<f64>,
291    /// Eigenvector matrix with one eigenvector in each column.
292    pub eigenvectors: Matrix<T>,
293}
294
295/// Scalar types supported by [`lowest_hermitian_eigenpair`].
296///
297/// The current backend path is used for `f64` and `Complex64`, which are the
298/// scalar types needed by tensor4all's Hermitian Krylov and DMRG algorithms.
299pub trait HermitianEigenScalar: TensorScalar + MatrixScalar {
300    #[doc(hidden)]
301    fn hermitian_difference(a_ij: Self, a_ji: Self) -> f64;
302
303    #[doc(hidden)]
304    fn hermitian_scale(a_ij: Self, a_ji: Self) -> f64;
305
306    #[doc(hidden)]
307    fn symmetrized_hermitian_pair(a_ij: Self, a_ji: Self) -> (Self, Self);
308
309    #[doc(hidden)]
310    fn eigenvalues_from_tensor(
311        tensor: Tensor,
312        tolerance: f64,
313    ) -> std::result::Result<(Vec<usize>, Vec<f64>), HermitianEigenError>;
314
315    #[doc(hidden)]
316    fn to_complex64(value: Self) -> Complex64;
317}
318
319impl HermitianEigenScalar for f64 {
320    fn hermitian_difference(a_ij: Self, a_ji: Self) -> f64 {
321        (a_ij - a_ji).abs()
322    }
323
324    fn hermitian_scale(a_ij: Self, a_ji: Self) -> f64 {
325        a_ij.abs().max(a_ji.abs()).max(1.0)
326    }
327
328    fn symmetrized_hermitian_pair(a_ij: Self, a_ji: Self) -> (Self, Self) {
329        let value = 0.5 * (a_ij + a_ji);
330        (value, value)
331    }
332
333    fn eigenvalues_from_tensor(
334        tensor: Tensor,
335        _tolerance: f64,
336    ) -> std::result::Result<(Vec<usize>, Vec<f64>), HermitianEigenError> {
337        let values = typed_eigh_output::<f64>("eigenvalues", tensor)?;
338        Ok((
339            values.shape().to_vec(),
340            values
341                .as_slice()
342                .map_err(|source| HermitianEigenError::Backend {
343                    source: Box::new(source),
344                })?
345                .to_vec(),
346        ))
347    }
348
349    fn to_complex64(value: Self) -> Complex64 {
350        Complex64::new(value, 0.0)
351    }
352}
353
354impl HermitianEigenScalar for Complex64 {
355    fn hermitian_difference(a_ij: Self, a_ji: Self) -> f64 {
356        (a_ij - a_ji.conj()).norm()
357    }
358
359    fn hermitian_scale(a_ij: Self, a_ji: Self) -> f64 {
360        a_ij.norm().max(a_ji.norm()).max(1.0)
361    }
362
363    fn symmetrized_hermitian_pair(a_ij: Self, a_ji: Self) -> (Self, Self) {
364        let value = 0.5 * (a_ij + a_ji.conj());
365        (value, value.conj())
366    }
367
368    fn eigenvalues_from_tensor(
369        tensor: Tensor,
370        tolerance: f64,
371    ) -> std::result::Result<(Vec<usize>, Vec<f64>), HermitianEigenError> {
372        if tensor.dtype() == DType::F64 {
373            let values = typed_eigh_output::<f64>("eigenvalues", tensor)?;
374            let values_slice =
375                values
376                    .as_slice()
377                    .map_err(|source| HermitianEigenError::Backend {
378                        source: Box::new(source),
379                    })?;
380            return Ok((values.shape().to_vec(), values_slice.to_vec()));
381        }
382
383        let values = typed_eigh_output::<Complex64>("eigenvalues", tensor)?;
384        let values_slice = values
385            .as_slice()
386            .map_err(|source| HermitianEigenError::Backend {
387                source: Box::new(source),
388            })?;
389        let mut real_values = Vec::with_capacity(values_slice.len());
390        for (index, value) in values_slice.iter().copied().enumerate() {
391            let imaginary = value.im.abs();
392            let allowed = tolerance * value.norm().max(1.0);
393            if imaginary > allowed {
394                return Err(HermitianEigenError::NonRealEigenvalue {
395                    index,
396                    imaginary,
397                    tolerance: allowed,
398                });
399            }
400            real_values.push(value.re);
401        }
402        Ok((values.shape().to_vec(), real_values))
403    }
404
405    fn to_complex64(value: Self) -> Complex64 {
406        value
407    }
408}
409
410impl<T> Matrix<T> {
411    /// Fallibly create a matrix from column-major data after checked shape validation.
412    ///
413    /// # Errors
414    /// Returns [`MatrixShapeError::ShapeOverflow`] when the shape exceeds
415    /// `usize`, or [`MatrixShapeError::DataLengthMismatch`] when the payload
416    /// length does not match the shape.
417    pub fn try_from_col_major_vec(
418        nrows: usize,
419        ncols: usize,
420        data: Vec<T>,
421    ) -> std::result::Result<Self, MatrixShapeError> {
422        let expected = nrows
423            .checked_mul(ncols)
424            .ok_or(MatrixShapeError::ShapeOverflow { nrows, ncols })?;
425        if data.len() != expected {
426            return Err(MatrixShapeError::DataLengthMismatch {
427                actual: data.len(),
428                expected,
429            });
430        }
431        Ok(Self { nrows, ncols, data })
432    }
433
434    /// Create a matrix from raw column-major data.
435    ///
436    /// # Panics
437    ///
438    /// Panics if `nrows * ncols` overflows or if `data.len() != nrows * ncols`.
439    ///
440    /// # Examples
441    ///
442    /// ```
443    /// use tensor4all_tensorbackend::Matrix;
444    ///
445    /// let m = Matrix::from_col_major_vec(2, 2, vec![1.0, 3.0, 2.0, 4.0]);
446    /// assert_eq!(m[[0, 0]], 1.0);
447    /// assert_eq!(m[[0, 1]], 2.0);
448    /// assert_eq!(m[[1, 0]], 3.0);
449    /// assert_eq!(m[[1, 1]], 4.0);
450    /// ```
451    pub fn from_col_major_vec(nrows: usize, ncols: usize, data: Vec<T>) -> Self {
452        let expected = checked_matrix_len(nrows, ncols);
453        assert!(
454            expected.is_some(),
455            "matrix shape product overflow: {nrows} rows * {ncols} columns"
456        );
457        let expected = expected.unwrap_or(0);
458        assert_eq!(data.len(), expected);
459        Self { data, nrows, ncols }
460    }
461
462    /// View the underlying column-major data as a contiguous slice.
463    ///
464    /// # Examples
465    ///
466    /// ```
467    /// use tensor4all_tensorbackend::Matrix;
468    ///
469    /// let m = Matrix::from_col_major_vec(2, 2, vec![1, 3, 2, 4]);
470    /// assert_eq!(m.as_col_major_slice(), &[1, 3, 2, 4]);
471    /// ```
472    pub fn as_col_major_slice(&self) -> &[T] {
473        &self.data
474    }
475
476    /// View the underlying column-major data as a mutable contiguous slice.
477    ///
478    /// The slice uses `row + nrows * col` ordering. This is useful for kernels
479    /// that validate dimensions once and then operate over contiguous columns.
480    ///
481    /// # Examples
482    ///
483    /// ```
484    /// use tensor4all_tensorbackend::Matrix;
485    ///
486    /// let mut m = Matrix::from_col_major_vec(2, 2, vec![1, 3, 2, 4]);
487    /// m.as_col_major_mut_slice()[1] = 30;
488    /// assert_eq!(m[[1, 0]], 30);
489    /// ```
490    pub fn as_col_major_mut_slice(&mut self) -> &mut [T] {
491        &mut self.data
492    }
493
494    /// Consume the matrix and return its owned column-major buffer.
495    ///
496    /// The returned buffer uses `row + nrows * col` ordering. Use this when
497    /// transferring matrix storage to another column-major dense container
498    /// without cloning.
499    ///
500    /// # Examples
501    ///
502    /// ```
503    /// use tensor4all_tensorbackend::Matrix;
504    ///
505    /// let m = Matrix::from_col_major_vec(2, 2, vec![1.0, 3.0, 2.0, 4.0]);
506    /// let data = m.into_col_major_vec();
507    /// assert_eq!(data, vec![1.0, 3.0, 2.0, 4.0]);
508    /// ```
509    pub fn into_col_major_vec(self) -> Vec<T> {
510        self.data
511    }
512
513    /// Borrow this matrix as an owned tenferro [`TypedTensor`].
514    ///
515    /// This clones the matrix buffer and preserves column-major layout. Use
516    /// [`Matrix::into_typed_tensor`] when the matrix can be consumed.
517    ///
518    /// # Examples
519    ///
520    /// ```
521    /// use tensor4all_tensorbackend::Matrix;
522    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
523    ///
524    /// let m = Matrix::from_col_major_vec(2, 2, vec![1.0_f64, 3.0, 2.0, 4.0]);
525    /// let tensor = m.to_typed_tensor();
526    /// assert_eq!(tensor.shape(), &[2, 2]);
527    /// assert_eq!(tensor.as_slice()?, &[1.0, 3.0, 2.0, 4.0]);
528    /// assert_eq!(m.as_col_major_slice(), &[1.0, 3.0, 2.0, 4.0]);
529    /// # Ok(())
530    /// # }
531    /// ```
532    pub fn to_typed_tensor(&self) -> TypedTensor<T>
533    where
534        T: TensorScalar,
535    {
536        crate::require_invariant(
537            TypedTensor::from_vec_col_major(vec![self.nrows, self.ncols], self.data.clone()),
538            "validated matrix rejected by tenferro",
539        )
540    }
541
542    /// Consume this matrix as a tenferro [`TypedTensor`] without cloning.
543    ///
544    /// The tensor shape is `[nrows, ncols]`, and the owned data remains in
545    /// column-major layout.
546    ///
547    /// # Examples
548    ///
549    /// ```
550    /// use tensor4all_tensorbackend::Matrix;
551    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
552    ///
553    /// let m = Matrix::from_col_major_vec(2, 2, vec![1.0_f64, 3.0, 2.0, 4.0]);
554    /// let tensor = m.into_typed_tensor();
555    /// assert_eq!(tensor.shape(), &[2, 2]);
556    /// assert_eq!(tensor.as_slice()?, &[1.0, 3.0, 2.0, 4.0]);
557    /// # Ok(())
558    /// # }
559    /// ```
560    pub fn into_typed_tensor(self) -> TypedTensor<T>
561    where
562        T: TensorScalar,
563    {
564        crate::require_invariant(
565            TypedTensor::from_vec_col_major(vec![self.nrows, self.ncols], self.data),
566            "validated matrix rejected by tenferro",
567        )
568    }
569
570    /// Consume a rank-2 tenferro [`TypedTensor`] as a [`Matrix`].
571    ///
572    /// The input tensor must have shape `[nrows, ncols]` and an owned host
573    /// buffer. The buffer is reused without cloning and interpreted as
574    /// column-major matrix storage.
575    ///
576    /// # Errors
577    ///
578    /// Returns [`MatrixTensorConversionError::Rank`] if the tensor is not
579    /// rank-2, or [`MatrixTensorConversionError::HostBuffer`] if tenferro
580    /// cannot export the tensor as an owned host buffer.
581    ///
582    /// # Examples
583    ///
584    /// ```
585    /// use tenferro::TypedTensor;
586    /// use tensor4all_tensorbackend::Matrix;
587    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
588    ///
589    /// let tensor = TypedTensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 3.0, 2.0, 4.0])?;
590    /// let m = Matrix::try_from_typed_tensor(tensor)?;
591    /// assert_eq!(m.nrows(), 2);
592    /// assert_eq!(m.ncols(), 2);
593    /// assert_eq!(m[[0, 1]], 2.0);
594    /// # Ok(())
595    /// # }
596    /// ```
597    pub fn try_from_typed_tensor(
598        tensor: TypedTensor<T>,
599    ) -> std::result::Result<Self, MatrixTensorConversionError>
600    where
601        T: TensorScalar + Clone,
602    {
603        let (shape, data) = tensor.into_vec_col_major().map_err(|source| {
604            MatrixTensorConversionError::HostBuffer {
605                message: source.to_string(),
606            }
607        })?;
608        if shape.len() != 2 {
609            return Err(MatrixTensorConversionError::Rank { shape });
610        }
611        Ok(Self::from_col_major_vec(shape[0], shape[1], data))
612    }
613
614    fn offset(&self, row: usize, col: usize) -> usize {
615        assert!(
616            row < self.nrows,
617            "matrix row index {row} out of bounds (bound: {})",
618            self.nrows
619        );
620        assert!(
621            col < self.ncols,
622            "matrix column index {col} out of bounds (bound: {})",
623            self.ncols
624        );
625        row + self.nrows * col
626    }
627
628    /// Number of rows
629    pub fn nrows(&self) -> usize {
630        self.nrows
631    }
632
633    /// Number of columns
634    pub fn ncols(&self) -> usize {
635        self.ncols
636    }
637}
638
639/// Compute the smallest eigenpair of a small Hermitian projected matrix.
640///
641/// This function validates that `matrix` is square, non-empty, and Hermitian
642/// within `hermitian_tol`, symmetrizes accepted roundoff as `(A + A†) / 2`,
643/// then calls tenferro's Hermitian eigendecomposition. It is intended for
644/// Rayleigh-Ritz projected Krylov matrices, whose dimension is bounded by the
645/// Krylov subspace size. It must not be used to materialize a full
646/// tensor-network effective Hamiltonian.
647///
648/// # Arguments
649/// * `matrix` - Small dense Hermitian matrix in column-major [`Matrix`] layout.
650/// * `hermitian_tol` - Relative tolerance for `A[i, j] = conj(A[j, i])`,
651///
652///   applied as `hermitian_tol * max(1, |A[i,j]|, |A[j,i]|)`.
653///   Typical values are `1e-12` for `f64`/`Complex64` projected matrices.
654///
655/// # Returns
656/// The smallest real eigenvalue and the corresponding normalized eigenvector
657/// coefficients.
658///
659/// # Errors
660/// Returns [`HermitianEigenError`] if the matrix is empty, non-square,
661/// non-Hermitian within `hermitian_tol`, or if the backend eigendecomposition
662/// fails or returns an unexpected dtype/shape.
663///
664/// # Examples
665///
666/// ```
667/// use tensor4all_tensorbackend::{lowest_hermitian_eigenpair, Matrix};
668///
669/// let matrix = Matrix::from_col_major_vec(2, 2, vec![2.0_f64, 1.0, 1.0, 2.0]);
670/// let pair = lowest_hermitian_eigenpair(&matrix, 1.0e-12).unwrap();
671///
672/// assert!((pair.eigenvalue - 1.0).abs() < 1.0e-12);
673/// assert_eq!(pair.eigenvector.len(), 2);
674/// ```
675pub fn lowest_hermitian_eigenpair<T>(
676    matrix: &Matrix<T>,
677    hermitian_tol: f64,
678) -> std::result::Result<HermitianEigenpair<T>, HermitianEigenError>
679where
680    T: HermitianEigenScalar,
681{
682    let decomp = hermitian_eigendecomposition(matrix, hermitian_tol)?;
683
684    let (min_col, eigenvalue) = decomp
685        .eigenvalues
686        .iter()
687        .copied()
688        .enumerate()
689        .min_by(|(_, a), (_, b)| a.total_cmp(b))
690        .ok_or(HermitianEigenError::Empty)?;
691
692    let n = decomp.eigenvalues.len();
693    let vector_data = decomp.eigenvectors.as_col_major_slice();
694    let start = n * min_col;
695    let eigenvector = vector_data[start..start + n].to_vec();
696
697    Ok(HermitianEigenpair {
698        eigenvalue,
699        eigenvector,
700    })
701}
702
703/// Compute all eigenpairs of a small Hermitian projected matrix.
704///
705/// This validates Hermitian structure and symmetrizes accepted roundoff before
706/// calling the backend. It is meant for bounded Krylov/Rayleigh-Ritz matrices,
707/// not full tensor-network materialization.
708///
709/// # Arguments
710///
711/// * `matrix` - Square Hermitian matrix in column-major [`Matrix`] layout.
712/// * `hermitian_tol` - Relative tolerance for checking `A = A†`, applied per
713///
714///   entry pair with scale `max(1, |A[i,j]|, |A[j,i]|)`.
715///
716/// # Returns
717///
718/// All real eigenvalues and all eigenvectors of `matrix`.
719///
720/// # Errors
721///
722/// Returns [`HermitianEigenError`] if `matrix` is not square, is not Hermitian
723/// within `hermitian_tol`, or the backend eigensolver fails.
724///
725/// # Examples
726///
727/// ```
728/// use tensor4all_tensorbackend::{hermitian_eigendecomposition, Matrix};
729///
730/// let matrix = Matrix::from_col_major_vec(2, 2, vec![3.0, 0.0, 0.0, 5.0]);
731/// let decomp = hermitian_eigendecomposition(&matrix, 1.0e-12).unwrap();
732/// assert_eq!(decomp.eigenvalues, vec![3.0, 5.0]);
733/// assert_eq!(decomp.eigenvectors.as_col_major_slice().len(), 4);
734/// ```
735pub fn hermitian_eigendecomposition<T>(
736    matrix: &Matrix<T>,
737    hermitian_tol: f64,
738) -> std::result::Result<HermitianEigendecomposition<T>, HermitianEigenError>
739where
740    T: HermitianEigenScalar,
741{
742    let matrix = validate_and_symmetrize_hermitian_matrix(matrix, hermitian_tol)?;
743
744    let n = matrix.nrows();
745    let input_tensor =
746        T::into_tensor(vec![n, n], matrix.as_col_major_slice().to_vec()).map_err(|source| {
747            HermitianEigenError::Backend {
748                source: Box::new(source),
749            }
750        })?;
751    let eager_ctx = crate::default_eager_ctx().map_err(|source| HermitianEigenError::Backend {
752        source: Box::new(source),
753    })?;
754    let input = EagerTensor::from_tensor_in(input_tensor, eager_ctx).map_err(|source| {
755        HermitianEigenError::Backend {
756            source: Box::new(source),
757        }
758    })?;
759    let (values, vectors) = input
760        .eigh()
761        .map_err(|source| HermitianEigenError::Backend {
762            source: Box::new(source),
763        })?;
764    let values = values
765        .to_tensor()
766        .map_err(|source| HermitianEigenError::Backend {
767            source: Box::new(source),
768        })?;
769    let vectors = vectors
770        .to_tensor()
771        .map_err(|source| HermitianEigenError::Backend {
772            source: Box::new(source),
773        })?;
774
775    let (values_shape, eigenvalues) = T::eigenvalues_from_tensor(values, hermitian_tol)?;
776    ensure_eigh_shape("eigenvalues", &values_shape, &[n])?;
777    let vectors = typed_eigh_output::<T>("eigenvectors", vectors)?;
778    ensure_eigh_shape("eigenvectors", vectors.shape(), &[n, n])?;
779
780    Ok(HermitianEigendecomposition {
781        eigenvalues,
782        eigenvectors: Matrix::from_col_major_vec(
783            n,
784            n,
785            vectors
786                .as_slice()
787                .map_err(|source| HermitianEigenError::Backend {
788                    source: Box::new(source),
789                })?
790                .to_vec(),
791        ),
792    })
793}
794
795/// Compute the first column of `exp(exponent * A)` for a small Hermitian matrix.
796///
797/// Krylov exponential routines use this for the projected matrix action on the
798/// first basis vector. The returned coefficients are complex even when `A` is
799/// real because real-time evolution has complex phases.
800///
801/// # Arguments
802///
803/// * `matrix` - Square Hermitian matrix in column-major [`Matrix`] layout.
804/// * `exponent` - Scalar multiplier in `exp(exponent * A)`.
805/// * `hermitian_tol` - Relative tolerance for checking `A = A†`; accepted
806///
807///   roundoff is symmetrized before eigensolving.
808///
809/// # Returns
810///
811/// The first column of the matrix exponential.
812///
813/// # Errors
814///
815/// Returns [`HermitianEigenError`] if Hermitian validation or eigensolving
816/// fails.
817///
818/// # Examples
819///
820/// ```
821/// use num_complex::Complex64;
822/// use tensor4all_tensorbackend::{hermitian_exponential_first_column, Matrix};
823///
824/// let matrix = Matrix::from_col_major_vec(2, 2, vec![1.0, 0.0, 0.0, 2.0]);
825/// let column = hermitian_exponential_first_column(
826///     &matrix,
827///     Complex64::new(0.0, -0.5),
828///     1.0e-12,
829/// ).unwrap();
830/// let expected = Complex64::new(0.5_f64.cos(), -0.5_f64.sin());
831/// assert!((column[0] - expected).norm() < 1.0e-12);
832/// assert!(column[1].norm() < 1.0e-12);
833/// ```
834pub fn hermitian_exponential_first_column<T>(
835    matrix: &Matrix<T>,
836    exponent: Complex64,
837    hermitian_tol: f64,
838) -> std::result::Result<Vec<Complex64>, HermitianEigenError>
839where
840    T: HermitianEigenScalar,
841{
842    let decomp = hermitian_eigendecomposition(matrix, hermitian_tol)?;
843    let n = decomp.eigenvalues.len();
844    let vectors = decomp.eigenvectors.as_col_major_slice();
845    let mut result = vec![Complex64::new(0.0, 0.0); n];
846
847    for col in 0..n {
848        let lambda = decomp.eigenvalues[col];
849        let phase = (exponent * lambda).exp();
850        let first_component = T::to_complex64(vectors[col * n]).conj();
851        for row in 0..n {
852            result[row] += T::to_complex64(vectors[row + col * n]) * phase * first_component;
853        }
854    }
855
856    Ok(result)
857}
858
859fn validate_and_symmetrize_hermitian_matrix<T>(
860    matrix: &Matrix<T>,
861    hermitian_tol: f64,
862) -> std::result::Result<Matrix<T>, HermitianEigenError>
863where
864    T: HermitianEigenScalar,
865{
866    if !hermitian_tol.is_finite() || hermitian_tol < 0.0 {
867        return Err(HermitianEigenError::InvalidTolerance {
868            tolerance: hermitian_tol,
869        });
870    }
871    if matrix.nrows() != matrix.ncols() {
872        return Err(HermitianEigenError::NonSquare {
873            nrows: matrix.nrows(),
874            ncols: matrix.ncols(),
875        });
876    }
877    if matrix.nrows() == 0 {
878        return Err(HermitianEigenError::Empty);
879    }
880
881    let n = matrix.nrows();
882    let mut data = matrix.as_col_major_slice().to_vec();
883    for col in 0..matrix.ncols() {
884        for row in 0..=col {
885            let row_col = matrix[[row, col]];
886            let col_row = matrix[[col, row]];
887            let difference = T::hermitian_difference(row_col, col_row);
888            let tolerance = hermitian_tol * T::hermitian_scale(row_col, col_row);
889            if difference > tolerance {
890                return Err(HermitianEigenError::NonHermitian {
891                    row,
892                    col,
893                    difference,
894                    tolerance,
895                });
896            }
897            let (row_col, col_row) = T::symmetrized_hermitian_pair(row_col, col_row);
898            data[row + n * col] = row_col;
899            data[col + n * row] = col_row;
900        }
901    }
902    Ok(Matrix::from_col_major_vec(n, n, data))
903}
904
905fn typed_eigh_output<T>(
906    output: &'static str,
907    tensor: Tensor,
908) -> std::result::Result<TypedTensor<T>, HermitianEigenError>
909where
910    T: TensorScalar,
911{
912    let actual = tensor.dtype();
913    T::into_typed(tensor).map_err(|_| HermitianEigenError::DType {
914        output,
915        expected: format!("{:?}", T::dtype()),
916        actual: format!("{actual:?}"),
917    })
918}
919
920fn ensure_eigh_shape(
921    output: &'static str,
922    actual: &[usize],
923    expected: &[usize],
924) -> std::result::Result<(), HermitianEigenError> {
925    if actual != expected {
926        return Err(HermitianEigenError::Shape {
927            output,
928            expected: expected.to_vec(),
929            actual: actual.to_vec(),
930        });
931    }
932    Ok(())
933}
934
935impl<T: Clone> Matrix<T> {
936    /// Create a new matrix filled with a constant value.
937    ///
938    /// # Panics
939    ///
940    /// Panics if `nrows * ncols` overflows.
941    ///
942    /// # Examples
943    ///
944    /// ```
945    /// use tensor4all_tensorbackend::Matrix;
946    ///
947    /// let m = Matrix::from_elem(2, 3, 7.0);
948    /// assert_eq!(m[[0, 0]], 7.0);
949    /// assert_eq!(m[[1, 2]], 7.0);
950    /// ```
951    pub fn from_elem(nrows: usize, ncols: usize, elem: T) -> Self {
952        let len = checked_matrix_len(nrows, ncols);
953        assert!(
954            len.is_some(),
955            "matrix shape product overflow: {nrows} rows * {ncols} columns"
956        );
957        let len = len.unwrap_or(0);
958        Self {
959            data: vec![elem; len],
960            nrows,
961            ncols,
962        }
963    }
964}
965
966impl<T: Clone + Zero> Matrix<T> {
967    /// Fallibly create a zero-filled matrix after checked shape validation.
968    ///
969    /// # Errors
970    /// Returns [`MatrixShapeError::ShapeOverflow`] when the shape exceeds
971    /// `usize`.
972    pub fn try_zeros(nrows: usize, ncols: usize) -> std::result::Result<Self, MatrixShapeError> {
973        let len = nrows
974            .checked_mul(ncols)
975            .ok_or(MatrixShapeError::ShapeOverflow { nrows, ncols })?;
976        Self::try_from_col_major_vec(nrows, ncols, vec![T::zero(); len])
977    }
978
979    /// Create a zeros matrix
980    ///
981    /// # Panics
982    ///
983    /// Panics if `nrows * ncols` overflows.
984    ///
985    /// # Examples
986    ///
987    /// ```
988    /// use tensor4all_tensorbackend::Matrix;
989    ///
990    /// let m = Matrix::<f64>::zeros(2, 3);
991    /// assert_eq!(m.nrows(), 2);
992    /// assert_eq!(m.ncols(), 3);
993    /// assert_eq!(m[[0, 0]], 0.0);
994    /// assert_eq!(m[[1, 2]], 0.0);
995    /// ```
996    pub fn zeros(nrows: usize, ncols: usize) -> Self {
997        let len = checked_matrix_len(nrows, ncols);
998        assert!(
999            len.is_some(),
1000            "matrix shape product overflow: {nrows} rows * {ncols} columns"
1001        );
1002        let len = len.unwrap_or(0);
1003        Self {
1004            data: vec![T::zero(); len],
1005            nrows,
1006            ncols,
1007        }
1008    }
1009}
1010
1011impl<T> Index<[usize; 2]> for Matrix<T> {
1012    type Output = T;
1013
1014    fn index(&self, idx: [usize; 2]) -> &Self::Output {
1015        &self.data[self.offset(idx[0], idx[1])]
1016    }
1017}
1018
1019impl<T> IndexMut<[usize; 2]> for Matrix<T> {
1020    fn index_mut(&mut self, idx: [usize; 2]) -> &mut Self::Output {
1021        let offset = self.offset(idx[0], idx[1]);
1022        &mut self.data[offset]
1023    }
1024}
1025
1026/// Create a matrix from a 2D vector, returning an error for ragged rows.
1027///
1028/// Each inner `Vec` is one row. The resulting matrix is stored internally in
1029/// column-major order.
1030///
1031/// # Errors
1032///
1033/// Returns [`MatrixShapeError::RaggedRows`] when any row has a different length
1034/// than the first row.
1035///
1036/// # Examples
1037///
1038/// ```
1039/// use tensor4all_tensorbackend::try_from_vec2d;
1040///
1041/// let m = try_from_vec2d(vec![
1042///     vec![1.0, 2.0],
1043///     vec![3.0, 4.0],
1044/// ])?;
1045/// assert_eq!(m.nrows(), 2);
1046/// assert_eq!(m.ncols(), 2);
1047/// assert_eq!(m[[0, 1]], 2.0);
1048/// assert_eq!(m[[1, 0]], 3.0);
1049/// # Ok::<(), tensor4all_tensorbackend::MatrixShapeError>(())
1050/// ```
1051pub fn try_from_vec2d<T: Clone + Zero>(
1052    data: Vec<Vec<T>>,
1053) -> std::result::Result<Matrix<T>, MatrixShapeError> {
1054    let nrows = data.len();
1055    let ncols = data.first().map_or(0, Vec::len);
1056    for (row, values) in data.iter().enumerate() {
1057        let actual = values.len();
1058        if actual != ncols {
1059            return Err(MatrixShapeError::RaggedRows {
1060                row,
1061                expected: ncols,
1062                actual,
1063            });
1064        }
1065    }
1066    let mut m = Matrix::zeros(nrows, ncols);
1067    for i in 0..nrows {
1068        for j in 0..ncols {
1069            m[[i, j]] = data[i][j].clone();
1070        }
1071    }
1072    Ok(m)
1073}
1074
1075/// Create a matrix from a rectangular 2D vector.
1076///
1077/// Each inner `Vec` is one row. The resulting matrix is stored internally in
1078/// column-major order.
1079///
1080/// # Panics
1081///
1082/// Panics if the row lengths are not all equal or if the rectangular shape's
1083/// element count overflows `usize`. Use [`try_from_vec2d`] when row-shaped
1084/// input comes from users, files, or other fallible boundaries to receive a
1085/// typed error for ragged rows.
1086///
1087/// # Examples
1088///
1089/// ```
1090/// use tensor4all_tensorbackend::from_vec2d;
1091///
1092/// let m = from_vec2d(vec![
1093///     vec![1.0, 2.0],
1094///     vec![3.0, 4.0],
1095/// ]);
1096/// assert_eq!(m.nrows(), 2);
1097/// assert_eq!(m.ncols(), 2);
1098/// assert_eq!(m[[0, 1]], 2.0);
1099/// assert_eq!(m[[1, 0]], 3.0);
1100/// ```
1101pub fn from_vec2d<T: Clone + Zero>(data: Vec<Vec<T>>) -> Matrix<T> {
1102    let result = try_from_vec2d(data);
1103    let error_message = match &result {
1104        Ok(_) => String::new(),
1105        Err(error) => error.to_string(),
1106    };
1107    assert!(result.is_ok(), "{error_message}");
1108    match result {
1109        Ok(matrix) => matrix,
1110        Err(_) => Matrix {
1111            data: Vec::new(),
1112            nrows: 0,
1113            ncols: 0,
1114        },
1115    }
1116}
1117
1118/// Get a submatrix by selecting specific rows and columns.
1119///
1120/// # Panics
1121///
1122/// Panics if any row is not less than `m.nrows()` or any column is not less
1123/// than `m.ncols()`.
1124///
1125/// # Examples
1126///
1127/// ```
1128/// use tensor4all_tensorbackend::{from_vec2d, submatrix};
1129///
1130/// let m = from_vec2d(vec![
1131///     vec![1.0, 2.0, 3.0],
1132///     vec![4.0, 5.0, 6.0],
1133///     vec![7.0, 8.0, 9.0],
1134/// ]);
1135/// let sub = submatrix(&m, &[0, 2], &[1, 2]);
1136/// assert_eq!(sub.nrows(), 2);
1137/// assert_eq!(sub.ncols(), 2);
1138/// assert_eq!(sub[[0, 0]], 2.0); // m[0, 1]
1139/// assert_eq!(sub[[1, 1]], 9.0); // m[2, 2]
1140/// ```
1141pub fn submatrix<T: Clone + Zero>(m: &Matrix<T>, rows: &[usize], cols: &[usize]) -> Matrix<T> {
1142    assert!(
1143        rows.iter().all(|&row| row < m.nrows),
1144        "submatrix row index out of bounds"
1145    );
1146    assert!(
1147        cols.iter().all(|&col| col < m.ncols),
1148        "submatrix column index out of bounds"
1149    );
1150
1151    let mut data = Vec::with_capacity(rows.len() * cols.len());
1152    let source = m.as_col_major_slice();
1153    for &col in cols {
1154        let col_start = col * m.nrows;
1155        for &row in rows {
1156            let offset = col_start + row;
1157            // SAFETY: rows and cols are range-checked above, and Matrix stores
1158            // exactly nrows * ncols values in column-major order.
1159            data.push(unsafe { source.get_unchecked(offset).clone() });
1160        }
1161    }
1162    Matrix::from_col_major_vec(rows.len(), cols.len(), data)
1163}
1164
1165/// Swap two rows in a matrix in-place.
1166///
1167/// No-op if `a == b`, after validating that the index exists.
1168///
1169/// # Errors
1170///
1171/// Returns [`MatrixShapeError::RowIndexOutOfBounds`] if either index is not
1172/// less than `m.nrows()`.
1173///
1174/// # Examples
1175///
1176/// ```
1177/// use tensor4all_tensorbackend::{from_vec2d, swap_rows};
1178///
1179/// let mut m = from_vec2d(vec![vec![1.0, 2.0], vec![3.0, 4.0]]);
1180/// swap_rows(&mut m, 0, 1).unwrap();
1181/// assert_eq!(m[[0, 0]], 3.0);
1182/// assert_eq!(m[[1, 0]], 1.0);
1183/// ```
1184pub fn swap_rows<T>(m: &mut Matrix<T>, a: usize, b: usize) -> Result<(), MatrixShapeError> {
1185    for index in [a, b] {
1186        if index >= m.nrows {
1187            return Err(MatrixShapeError::RowIndexOutOfBounds {
1188                index,
1189                nrows: m.nrows,
1190            });
1191        }
1192    }
1193    if a == b {
1194        return Ok(());
1195    }
1196    let nrows = m.nrows;
1197    let ncols = m.ncols;
1198    let data = m.as_col_major_mut_slice();
1199    for j in 0..ncols {
1200        data.swap(a + nrows * j, b + nrows * j);
1201    }
1202    Ok(())
1203}
1204
1205/// Swap two columns in a matrix in-place.
1206///
1207/// No-op if `a == b`, after validating that the index exists.
1208///
1209/// # Errors
1210///
1211/// Returns [`MatrixShapeError::ColumnIndexOutOfBounds`] if either index is not
1212/// less than `m.ncols()`.
1213///
1214/// # Examples
1215///
1216/// ```
1217/// use tensor4all_tensorbackend::{from_vec2d, swap_cols};
1218///
1219/// let mut m = from_vec2d(vec![vec![1.0, 2.0], vec![3.0, 4.0]]);
1220/// swap_cols(&mut m, 0, 1).unwrap();
1221/// assert_eq!(m[[0, 0]], 2.0);
1222/// assert_eq!(m[[0, 1]], 1.0);
1223/// ```
1224pub fn swap_cols<T>(m: &mut Matrix<T>, a: usize, b: usize) -> Result<(), MatrixShapeError> {
1225    for index in [a, b] {
1226        if index >= m.ncols {
1227            return Err(MatrixShapeError::ColumnIndexOutOfBounds {
1228                index,
1229                ncols: m.ncols,
1230            });
1231        }
1232    }
1233    if a == b {
1234        return Ok(());
1235    }
1236    let nrows = m.nrows;
1237    let start_a = nrows * a;
1238    let start_b = nrows * b;
1239    let data = m.as_col_major_mut_slice();
1240    if start_a < start_b {
1241        let (left, right) = data.split_at_mut(start_b);
1242        left[start_a..start_a + nrows].swap_with_slice(&mut right[..nrows]);
1243    } else {
1244        let (left, right) = data.split_at_mut(start_a);
1245        right[..nrows].swap_with_slice(&mut left[start_b..start_b + nrows]);
1246    }
1247    Ok(())
1248}
1249
1250/// Transpose the matrix.
1251///
1252/// # Examples
1253///
1254/// ```
1255/// use tensor4all_tensorbackend::{from_vec2d, transpose};
1256///
1257/// let m = from_vec2d(vec![vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]]);
1258/// let mt = transpose(&m);
1259/// assert_eq!(mt.nrows(), 3);
1260/// assert_eq!(mt.ncols(), 2);
1261/// assert_eq!(mt[[0, 0]], 1.0);
1262/// assert_eq!(mt[[2, 1]], 6.0);
1263/// ```
1264pub fn transpose<T: Clone + Zero>(m: &Matrix<T>) -> Matrix<T> {
1265    let mut result = Matrix::zeros(m.ncols, m.nrows);
1266    for j in 0..m.ncols {
1267        for i in 0..m.nrows {
1268            result[[j, i]] = m[[i, j]].clone();
1269        }
1270    }
1271    result
1272}
1273
1274/// Find the position and value of the maximum absolute value in a submatrix.
1275///
1276/// Searches within the rectangular region defined by `rows x cols` ranges.
1277/// Returns `(row, col, value)` of the element with the largest `|value|^2`.
1278///
1279/// # Panics
1280///
1281/// Panics if either range is empty or either end is out of bounds (`rows.end > a.nrows()` or `cols.end > a.ncols()`).
1282///
1283/// # Examples
1284///
1285/// ```
1286/// use tensor4all_tensorbackend::{from_vec2d, submatrix_argmax};
1287///
1288/// let m = from_vec2d(vec![
1289///     vec![1.0_f64, 2.0, 3.0],
1290///     vec![4.0, 9.0, 6.0],
1291///     vec![7.0, 8.0, 5.0],
1292/// ]);
1293/// let (row, col, val) = submatrix_argmax(&m, 0..3, 0..3);
1294/// assert_eq!(row, 1);
1295/// assert_eq!(col, 1);
1296/// assert_eq!(val, 9.0);
1297/// ```
1298pub fn submatrix_argmax<T: MatrixScalar>(
1299    a: &Matrix<T>,
1300    rows: std::ops::Range<usize>,
1301    cols: std::ops::Range<usize>,
1302) -> (usize, usize, T) {
1303    assert!(!rows.is_empty(), "rows must not be empty");
1304    assert!(!cols.is_empty(), "cols must not be empty");
1305    assert!(rows.end <= a.nrows, "row range out of bounds");
1306    assert!(cols.end <= a.ncols, "column range out of bounds");
1307
1308    let data = a.as_col_major_slice();
1309    let first_offset = rows.start + a.nrows * cols.start;
1310    // SAFETY: the non-empty ranges are checked against the matrix shape above.
1311    let first = unsafe { *data.get_unchecked(first_offset) };
1312    let mut max_val: f64 = first.matrix_abs_sq();
1313    let mut max_row = rows.start;
1314    let mut max_col = cols.start;
1315    let row_start = rows.start;
1316    let row_end = rows.end;
1317    let col_start = cols.start;
1318    let col_end = cols.end;
1319
1320    for c in col_start..col_end {
1321        let col_start_offset = row_start + a.nrows * c;
1322        for (offset, r) in (col_start_offset..).zip(row_start..row_end) {
1323            // SAFETY: row and column loops stay within the checked ranges.
1324            let value = unsafe { *data.get_unchecked(offset) };
1325            let val: f64 = value.matrix_abs_sq();
1326            if val > max_val {
1327                max_val = val;
1328                max_row = r;
1329                max_col = c;
1330            }
1331        }
1332    }
1333
1334    let max_offset = max_row + a.nrows * max_col;
1335    // SAFETY: max_row/max_col were selected from the checked ranges.
1336    (max_row, max_col, unsafe { *data.get_unchecked(max_offset) })
1337}
1338
1339/// BLAS-backed matrix multiplication dispatch.
1340///
1341/// Implemented for all scalar types supported by tenferro einsum
1342/// (f64, f32, Complex64, Complex32). This trait is sealed — external
1343/// types cannot implement it.
1344pub trait BlasMul: Sized {
1345    #[doc(hidden)]
1346    fn blas_mat_mul(a: &Matrix<Self>, b: &Matrix<Self>) -> Result<Matrix<Self>>;
1347
1348    #[doc(hidden)]
1349    fn blas_mat_mul_owned(a: Matrix<Self>, b: Matrix<Self>) -> Result<Matrix<Self>>;
1350}
1351
1352fn dot_general_matrices<T>(
1353    a_tensor: Tensor,
1354    b_tensor: Tensor,
1355    m: usize,
1356    n: usize,
1357    expected_len: usize,
1358) -> Result<Matrix<T>>
1359where
1360    T: TensorScalar,
1361{
1362    use crate::context::with_default_session;
1363    use tenferro::TensorSessionOpsExt;
1364
1365    let c = with_default_session(|session| a_tensor.matmul(&b_tensor, session))
1366        .context("matrix multiplication failed")?;
1367    let c = T::into_typed(c)
1368        .map_err(|error| anyhow::anyhow!("matrix multiplication returned wrong dtype: {error}"))?;
1369    let result = Matrix::try_from_typed_tensor(c)?;
1370    ensure!(
1371        result.nrows() == m && result.ncols() == n,
1372        "matrix multiplication returned shape {}x{} for expected shape {}x{}",
1373        result.nrows(),
1374        result.ncols(),
1375        m,
1376        n
1377    );
1378    ensure!(
1379        result.as_col_major_slice().len() == expected_len,
1380        "matrix multiplication returned {} values for expected shape {}x{}",
1381        result.as_col_major_slice().len(),
1382        m,
1383        n
1384    );
1385    Ok(result)
1386}
1387
1388macro_rules! impl_blas_mul {
1389    ($($t:ty),*) => {
1390        $(
1391        impl BlasMul for $t {
1392            fn blas_mat_mul(a: &Matrix<Self>, b: &Matrix<Self>) -> Result<Matrix<Self>> {
1393                let m = a.nrows();
1394                let k = a.ncols();
1395                let n = b.ncols();
1396                ensure!(
1397                    b.nrows() == k,
1398                    "matrix dimensions must agree for multiplication: left is {}x{}, right is {}x{}",
1399                    m,
1400                    k,
1401                    b.nrows(),
1402                    n
1403                );
1404                // Reject an overflowing output element count before any tensor
1405                // conversion or backend call, matching the constructor contract.
1406                let expected_len = m.checked_mul(n).ok_or_else(|| {
1407                    anyhow::anyhow!(
1408                        "matrix multiplication output shape {m}x{n} overflows usize"
1409                    )
1410                })?;
1411
1412                let a_tensor: Tensor = a.to_typed_tensor().into();
1413                let b_tensor: Tensor = b.to_typed_tensor().into();
1414                dot_general_matrices::<$t>(a_tensor, b_tensor, m, n, expected_len)
1415            }
1416
1417            fn blas_mat_mul_owned(a: Matrix<Self>, b: Matrix<Self>) -> Result<Matrix<Self>> {
1418                let m = a.nrows();
1419                let k = a.ncols();
1420                let n = b.ncols();
1421                ensure!(
1422                    b.nrows() == k,
1423                    "matrix dimensions must agree for multiplication: left is {}x{}, right is {}x{}",
1424                    m,
1425                    k,
1426                    b.nrows(),
1427                    n
1428                );
1429                let expected_len = m.checked_mul(n).ok_or_else(|| {
1430                    anyhow::anyhow!(
1431                        "matrix multiplication output shape {m}x{n} overflows usize"
1432                    )
1433                })?;
1434
1435                let a_tensor: Tensor = a.into_typed_tensor().into();
1436                let b_tensor: Tensor = b.into_typed_tensor().into();
1437                dot_general_matrices::<$t>(a_tensor, b_tensor, m, n, expected_len)
1438            }
1439        }
1440        )*
1441    };
1442}
1443
1444impl_blas_mul!(f64, f32, num_complex::Complex64, num_complex::Complex32);
1445
1446/// Scalar bound for dense backend matrix utilities.
1447///
1448/// This is the storage/linalg-layer scalar trait. Higher-level crates may
1449/// extend it with domain-specific methods, but matrix utilities only rely on
1450/// these algebraic operations and absolute-value comparisons.
1451pub trait MatrixScalar:
1452    Clone
1453    + Copy
1454    + Zero
1455    + One
1456    + std::ops::Add<Output = Self>
1457    + std::ops::Sub<Output = Self>
1458    + std::ops::Mul<Output = Self>
1459    + std::ops::Div<Output = Self>
1460    + std::ops::Neg<Output = Self>
1461    + Default
1462    + Send
1463    + Sync
1464    + BlasMul
1465    + 'static
1466{
1467    /// Squared absolute value as `f64`.
1468    fn matrix_abs_sq(self) -> f64;
1469}
1470
1471impl MatrixScalar for f64 {
1472    fn matrix_abs_sq(self) -> f64 {
1473        self * self
1474    }
1475}
1476
1477impl MatrixScalar for f32 {
1478    fn matrix_abs_sq(self) -> f64 {
1479        (self * self) as f64
1480    }
1481}
1482
1483impl MatrixScalar for Complex64 {
1484    fn matrix_abs_sq(self) -> f64 {
1485        self.norm_sqr()
1486    }
1487}
1488
1489impl MatrixScalar for Complex32 {
1490    fn matrix_abs_sq(self) -> f64 {
1491        self.norm_sqr() as f64
1492    }
1493}
1494
1495/// Matrix multiplication: A * B.
1496///
1497/// Uses BLAS-backed einsum via tenferro for high performance.
1498///
1499/// # Errors
1500///
1501/// Returns an error when the operation fails (a shape or index mismatch, or
1502/// /// a backend failure).
1503///
1504/// # Examples
1505///
1506/// ```
1507/// use tensor4all_tensorbackend::{from_vec2d, mat_mul};
1508///
1509/// let a = from_vec2d(vec![vec![1.0_f64, 2.0], vec![3.0, 4.0]]);
1510/// let b = from_vec2d(vec![vec![5.0, 6.0], vec![7.0, 8.0]]);
1511/// let c = mat_mul(&a, &b).unwrap();
1512/// assert!((c[[0, 0]] - 19.0).abs() < 1e-10);
1513/// assert!((c[[0, 1]] - 22.0).abs() < 1e-10);
1514/// assert!((c[[1, 0]] - 43.0).abs() < 1e-10);
1515/// assert!((c[[1, 1]] - 50.0).abs() < 1e-10);
1516/// ```
1517pub fn mat_mul<T: BlasMul>(a: &Matrix<T>, b: &Matrix<T>) -> Result<Matrix<T>, MatrixMulError> {
1518    T::blas_mat_mul(a, b).map_err(MatrixMulError::from)
1519}
1520
1521/// Matrix multiplication: consume `A` and `B`, returning `A * B`.
1522///
1523/// Uses BLAS-backed einsum via tenferro. Compared with [`mat_mul`], this
1524/// reuses the input matrix buffers when building tenferro tensors.
1525///
1526/// # Errors
1527///
1528/// Returns an error when the operation fails (a shape or index mismatch, or
1529/// /// a backend failure).
1530///
1531/// # Examples
1532///
1533/// ```
1534/// use tensor4all_tensorbackend::{from_vec2d, mat_mul_owned};
1535///
1536/// let a = from_vec2d(vec![vec![1.0_f64, 2.0], vec![3.0, 4.0]]);
1537/// let b = from_vec2d(vec![vec![5.0, 6.0], vec![7.0, 8.0]]);
1538/// let c = mat_mul_owned(a, b).unwrap();
1539/// assert_eq!(c.as_col_major_slice(), &[19.0, 43.0, 22.0, 50.0]);
1540/// ```
1541pub fn mat_mul_owned<T: BlasMul>(a: Matrix<T>, b: Matrix<T>) -> Result<Matrix<T>, MatrixMulError> {
1542    T::blas_mat_mul_owned(a, b).map_err(MatrixMulError::from)
1543}
1544
1545/// Batched matrix multiplication for column-major matrices with one shared shape.
1546///
1547/// Computes `C[p] = A[p] * B[p]` for `batch` matrices. Each `A[p]` is an
1548/// `m x k` column-major matrix and each `B[p]` is a `k x n` column-major
1549/// matrix. The input buffers store complete matrices consecutively, and the
1550/// returned buffer stores `batch` consecutive `m x n` column-major outputs.
1551///
1552/// # Errors
1553///
1554/// Returns an error if the input buffer lengths do not match the declared
1555/// shapes or if the backend rejects the batched GEMM.
1556///
1557/// # Examples
1558///
1559/// ```
1560/// use tensor4all_tensorbackend::batched_mat_mul_same_shape;
1561///
1562/// let a = vec![1.0_f64, 3.0, 2.0, 4.0];
1563/// let b = vec![5.0_f64, 7.0, 6.0, 8.0];
1564/// let out = batched_mat_mul_same_shape(1, 2, 2, 2, &a, &b).unwrap();
1565/// assert_eq!(out, vec![19.0, 43.0, 22.0, 50.0]);
1566/// ```
1567pub fn batched_mat_mul_same_shape<T>(
1568    batch: usize,
1569    m: usize,
1570    k: usize,
1571    n: usize,
1572    a: &[T],
1573    b: &[T],
1574) -> Result<Vec<T>, MatrixMulError>
1575where
1576    T: tenferro::TensorScalar + Copy,
1577{
1578    batched_mat_mul_same_shape_owned(batch, m, k, n, a.to_vec(), b.to_vec())
1579}
1580
1581/// Batched matrix multiplication while consuming column-major input buffers.
1582///
1583/// This is the owned-buffer counterpart of [`batched_mat_mul_same_shape`].
1584/// It avoids cloning the two input batches when callers have just built the
1585/// contiguous buffers for a backend call.
1586///
1587/// # Errors
1588///
1589/// Returns an error if the input buffer lengths do not match the declared
1590/// shapes or if the backend rejects the batched GEMM.
1591pub fn batched_mat_mul_same_shape_owned<T>(
1592    batch: usize,
1593    m: usize,
1594    k: usize,
1595    n: usize,
1596    a: Vec<T>,
1597    b: Vec<T>,
1598) -> Result<Vec<T>, MatrixMulError>
1599where
1600    T: tenferro::TensorScalar + Copy,
1601{
1602    validate_batched_mat_mul_inputs(batch, m, k, n, a.len(), b.len())?;
1603
1604    let a_tensor = T::into_tensor(vec![m, k, batch], a)
1605        .map_err(|error| MatrixMulError::from(anyhow::Error::new(error)))?;
1606    let b_tensor = T::into_tensor(vec![k, n, batch], b)
1607        .map_err(|error| MatrixMulError::from(anyhow::Error::new(error)))?;
1608    // TensorSessionOpsExt exposes rank-2 matmul but not arbitrary batched dot.
1609    // Keep one backend execution by expressing [m,k,b] × [k,n,b] as einsum.
1610    let c = crate::tenferro_bridge::einsum_native_tensors_owned(
1611        vec![(a_tensor, vec![0, 1, 2]), (b_tensor, vec![1, 3, 2])],
1612        &[0, 3, 2],
1613    )
1614    .context("batched matrix multiplication failed")?;
1615    let c = T::into_typed(c).map_err(|error| {
1616        MatrixMulError::from(anyhow::anyhow!(
1617            "batched matrix multiplication returned wrong dtype: {error}"
1618        ))
1619    })?;
1620    let (_shape, data) = c
1621        .into_vec_col_major()
1622        .map_err(|error| MatrixMulError::from(anyhow::Error::new(error)))?;
1623    let expected_len = batch
1624        .checked_mul(m)
1625        .and_then(|value| value.checked_mul(n))
1626        .ok_or_else(|| {
1627            MatrixMulError::from(anyhow::anyhow!(
1628                "batched matrix multiplication output shape overflows"
1629            ))
1630        })?;
1631    if data.len() != expected_len {
1632        return Err(MatrixMulError::from(anyhow::anyhow!(
1633            "batched matrix multiplication returned {} values for expected shape {}x{}x{}",
1634            data.len(),
1635            m,
1636            n,
1637            batch
1638        )));
1639    }
1640    Ok(data)
1641}
1642
1643fn validate_batched_mat_mul_inputs(
1644    batch: usize,
1645    m: usize,
1646    k: usize,
1647    n: usize,
1648    a_len: usize,
1649    b_len: usize,
1650) -> Result<()> {
1651    let expected_a_len = batch
1652        .checked_mul(m)
1653        .and_then(|value| value.checked_mul(k))
1654        .ok_or_else(|| anyhow::anyhow!("batched matrix multiplication left shape overflows"))?;
1655    let expected_b_len = batch
1656        .checked_mul(k)
1657        .and_then(|value| value.checked_mul(n))
1658        .ok_or_else(|| anyhow::anyhow!("batched matrix multiplication right shape overflows"))?;
1659    ensure!(
1660        a_len == expected_a_len,
1661        "batched matrix multiplication left buffer has length {}, expected {}",
1662        a_len,
1663        expected_a_len
1664    );
1665    ensure!(
1666        b_len == expected_b_len,
1667        "batched matrix multiplication right buffer has length {}, expected {}",
1668        b_len,
1669        expected_b_len
1670    );
1671    Ok(())
1672}
1673
1674#[cfg(test)]
1675mod tests;