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