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