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;