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/// One column-major matrix multiply in a shared-buffer grouped GEMM.
178///
179/// The three offsets address element positions in the caller-owned flat
180/// buffers. The left block has shape `rows x contracted`, the right block has
181/// shape `contracted x cols`, and the output block has shape `rows x cols`.
182/// Jobs may share either input block, but output spans must be disjoint because
183/// the operation has no reduction mode.
184///
185/// # Examples
186///
187/// ```
188/// use tensor4all_tensorbackend::GroupedGemmJob;
189///
190/// let job = GroupedGemmJob::new(8, 4, 0, 2, 3, 5);
191/// assert_eq!(job.out_offset(), 8);
192/// assert_eq!(job.lhs_offset(), 4);
193/// assert_eq!(job.rhs_offset(), 0);
194/// assert_eq!((job.rows(), job.contracted(), job.cols()), (2, 3, 5));
195/// ```
196#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
197pub struct GroupedGemmJob {
198 out_offset: usize,
199 lhs_offset: usize,
200 rhs_offset: usize,
201 rows: usize,
202 contracted: usize,
203 cols: usize,
204}
205
206impl GroupedGemmJob {
207 /// Construct a column-major grouped-GEMM job.
208 #[allow(clippy::too_many_arguments)]
209 pub fn new(
210 out_offset: usize,
211 lhs_offset: usize,
212 rhs_offset: usize,
213 rows: usize,
214 contracted: usize,
215 cols: usize,
216 ) -> Self {
217 Self {
218 out_offset,
219 lhs_offset,
220 rhs_offset,
221 rows,
222 contracted,
223 cols,
224 }
225 }
226
227 /// Return the output element offset.
228 pub fn out_offset(&self) -> usize {
229 self.out_offset
230 }
231
232 /// Return the left-input element offset.
233 pub fn lhs_offset(&self) -> usize {
234 self.lhs_offset
235 }
236
237 /// Return the right-input element offset.
238 pub fn rhs_offset(&self) -> usize {
239 self.rhs_offset
240 }
241
242 /// Return the output row count.
243 pub fn rows(&self) -> usize {
244 self.rows
245 }
246
247 /// Return the contracted dimension.
248 pub fn contracted(&self) -> usize {
249 self.contracted
250 }
251
252 /// Return the output column count.
253 pub fn cols(&self) -> usize {
254 self.cols
255 }
256}
257
258/// Resource limits for a shared-buffer grouped GEMM.
259///
260/// `max_working_bytes` bounds the temporary descriptor translation owned by
261/// the tensorbackend facade. It does not attempt to account for provider-owned
262/// internal workspace. The default is unlimited; callers with a strict memory
263/// contract should set an explicit limit and include the descriptor metadata
264/// in that budget.
265///
266/// # Examples
267///
268/// ```
269/// use tensor4all_tensorbackend::GroupedGemmOptions;
270///
271/// let options = GroupedGemmOptions { max_working_bytes: 4096 };
272/// assert_eq!(options.max_working_bytes, 4096);
273/// assert_eq!(GroupedGemmOptions::default().max_working_bytes, usize::MAX);
274/// ```
275#[derive(Clone, Copy, Debug, PartialEq, Eq)]
276pub struct GroupedGemmOptions {
277 /// Maximum temporary bytes used to translate jobs for the provider.
278 pub max_working_bytes: usize,
279}
280
281impl Default for GroupedGemmOptions {
282 fn default() -> Self {
283 Self {
284 max_working_bytes: usize::MAX,
285 }
286 }
287}
288
289/// Validation or backend error from a shared-buffer grouped GEMM.
290///
291/// Validation is completed before the configured backend session is entered,
292/// so these errors leave the caller's output unchanged.
293///
294/// # Examples
295///
296/// ```
297/// use tensor4all_tensorbackend::{
298/// grouped_mat_mul_shared, GroupedGemmError, GroupedGemmJob, GroupedGemmOptions,
299/// };
300///
301/// let error = grouped_mat_mul_shared(
302/// &[1.0_f64],
303/// &[1.0_f64],
304/// &mut [0.0_f64],
305/// &[GroupedGemmJob::new(1, 0, 0, 1, 1, 1)],
306/// GroupedGemmOptions::default(),
307/// )
308/// .unwrap_err();
309/// assert!(matches!(error, GroupedGemmError::BufferOutOfBounds { .. }));
310/// ```
311#[derive(Debug, thiserror::Error)]
312pub enum GroupedGemmError {
313 /// A matrix dimension product overflowed `usize`.
314 #[error("grouped GEMM job {job} {dimension} dimensions overflow usize")]
315 DimensionOverflow {
316 /// Zero-based job index.
317 job: usize,
318 /// Dimension product that overflowed.
319 dimension: &'static str,
320 },
321 /// An offset plus the required span overflowed `usize`.
322 #[error("grouped GEMM job {job} {buffer} span overflows usize")]
323 SpanOverflow {
324 /// Zero-based job index.
325 job: usize,
326 /// Buffer whose span overflowed.
327 buffer: &'static str,
328 },
329 /// A job's input or output span exceeds its caller-owned buffer.
330 #[error(
331 "grouped GEMM job {job} {buffer} span [{offset}, {required_end}) exceeds buffer length {available}"
332 )]
333 BufferOutOfBounds {
334 /// Zero-based job index.
335 job: usize,
336 /// Buffer whose span was rejected.
337 buffer: &'static str,
338 /// Starting element offset.
339 offset: usize,
340 /// Exclusive required end offset.
341 required_end: usize,
342 /// Available element count.
343 available: usize,
344 },
345 /// Two output spans overlap without a reduction contract.
346 #[error("grouped GEMM output spans for jobs {first} and {second} overlap")]
347 OverlappingOutputs {
348 /// First job in source order.
349 first: usize,
350 /// Later overlapping job in source order.
351 second: usize,
352 },
353 /// Jobs sharing one left-input offset disagree on its matrix shape.
354 #[error(
355 "grouped GEMM jobs {first} and {second} share lhs offset with incompatible dimensions"
356 )]
357 IncompatibleSharedLhs {
358 /// First job in source order.
359 first: usize,
360 /// Later incompatible job in source order.
361 second: usize,
362 },
363 /// Jobs sharing one right-input offset disagree on its matrix shape.
364 #[error(
365 "grouped GEMM jobs {first} and {second} share rhs offset with incompatible dimensions"
366 )]
367 IncompatibleSharedRhs {
368 /// First job in source order.
369 first: usize,
370 /// Later incompatible job in source order.
371 second: usize,
372 },
373 /// The descriptor translation exceeds the caller's working-memory limit.
374 #[error(
375 "grouped GEMM descriptor translation needs {required} working bytes, limit is {limit}"
376 )]
377 WorkingMemoryExceeded {
378 /// Bytes required for the translated descriptor array.
379 required: usize,
380 /// Caller-provided maximum.
381 limit: usize,
382 },
383 /// The configured provider or tensor view rejected an otherwise validated request.
384 #[error("grouped GEMM backend execution failed: {source}")]
385 Backend {
386 /// Original tenferro diagnostic.
387 #[source]
388 source: anyhow::Error,
389 },
390}
391
392type GroupedGemmSpan = (usize, usize);
393
394fn grouped_gemm_dimension_spans(
395 job_index: usize,
396 job: &GroupedGemmJob,
397) -> std::result::Result<(usize, usize, usize), GroupedGemmError> {
398 let lhs = job
399 .rows
400 .checked_mul(job.contracted)
401 .ok_or(GroupedGemmError::DimensionOverflow {
402 job: job_index,
403 dimension: "lhs",
404 })?;
405 let rhs = job
406 .contracted
407 .checked_mul(job.cols)
408 .ok_or(GroupedGemmError::DimensionOverflow {
409 job: job_index,
410 dimension: "rhs",
411 })?;
412 let output = job
413 .rows
414 .checked_mul(job.cols)
415 .ok_or(GroupedGemmError::DimensionOverflow {
416 job: job_index,
417 dimension: "output",
418 })?;
419 Ok((lhs, rhs, output))
420}
421
422fn grouped_gemm_checked_span(
423 job_index: usize,
424 buffer: &'static str,
425 offset: usize,
426 elements: usize,
427 available: usize,
428) -> std::result::Result<GroupedGemmSpan, GroupedGemmError> {
429 let end = offset
430 .checked_add(elements)
431 .ok_or(GroupedGemmError::SpanOverflow {
432 job: job_index,
433 buffer,
434 })?;
435 if end > available {
436 return Err(GroupedGemmError::BufferOutOfBounds {
437 job: job_index,
438 buffer,
439 offset,
440 required_end: end,
441 available,
442 });
443 }
444 Ok((offset, end))
445}
446
447fn grouped_gemm_validate<T>(
448 lhs: &[T],
449 rhs: &[T],
450 output: &[T],
451 jobs: &[GroupedGemmJob],
452 options: GroupedGemmOptions,
453) -> std::result::Result<(), GroupedGemmError> {
454 let descriptor_bytes = jobs
455 .len()
456 .checked_mul(std::mem::size_of::<tenferro_tensor::backend::GroupedGemmJob>())
457 .ok_or(GroupedGemmError::WorkingMemoryExceeded {
458 required: usize::MAX,
459 limit: options.max_working_bytes,
460 })?;
461 if descriptor_bytes > options.max_working_bytes {
462 return Err(GroupedGemmError::WorkingMemoryExceeded {
463 required: descriptor_bytes,
464 limit: options.max_working_bytes,
465 });
466 }
467
468 for (job_index, job) in jobs.iter().enumerate() {
469 let (lhs_elements, rhs_elements, output_elements) =
470 grouped_gemm_dimension_spans(job_index, job)?;
471 let output_span = grouped_gemm_checked_span(
472 job_index,
473 "output",
474 job.out_offset,
475 output_elements,
476 output.len(),
477 )?;
478 grouped_gemm_checked_span(job_index, "lhs", job.lhs_offset, lhs_elements, lhs.len())?;
479 grouped_gemm_checked_span(job_index, "rhs", job.rhs_offset, rhs_elements, rhs.len())?;
480
481 for (previous_index, previous) in jobs[..job_index].iter().enumerate() {
482 let (_, _, previous_output_elements) =
483 grouped_gemm_dimension_spans(previous_index, previous)?;
484 let previous_output = grouped_gemm_checked_span(
485 previous_index,
486 "output",
487 previous.out_offset,
488 previous_output_elements,
489 output.len(),
490 )?;
491 if output_span.0 < previous_output.1 && previous_output.0 < output_span.1 {
492 return Err(GroupedGemmError::OverlappingOutputs {
493 first: previous_index,
494 second: job_index,
495 });
496 }
497 if job.lhs_offset == previous.lhs_offset
498 && (job.rows, job.contracted) != (previous.rows, previous.contracted)
499 {
500 return Err(GroupedGemmError::IncompatibleSharedLhs {
501 first: previous_index,
502 second: job_index,
503 });
504 }
505 if job.rhs_offset == previous.rhs_offset
506 && (job.contracted, job.cols) != (previous.contracted, previous.cols)
507 {
508 return Err(GroupedGemmError::IncompatibleSharedRhs {
509 first: previous_index,
510 second: job_index,
511 });
512 }
513 }
514 }
515 Ok(())
516}
517
518fn grouped_mat_mul_shared_in_session<T: MatrixScalar + TensorScalar>(
519 lhs: &[T],
520 rhs: &[T],
521 output: &mut [T],
522 jobs: &[GroupedGemmJob],
523 session: &mut dyn tenferro_tensor::BackendSession,
524) -> std::result::Result<(), GroupedGemmError> {
525 // Reference: tenferro-rs commit 007e3bb6c1187a2569d237b2bc6e6ad486f2b4f4,
526 // crates/tenferro-cpu/benches/grouped_gemm.rs lines 1--165. The upstream
527 // descriptor is translated here so it never becomes tensorbackend's
528 // downstream public API.
529 let native_jobs: Vec<_> = jobs
530 .iter()
531 .map(|job| {
532 tenferro_tensor::backend::GroupedGemmJob::new(
533 job.out_offset,
534 job.lhs_offset,
535 job.rhs_offset,
536 job.rows,
537 job.contracted,
538 job.cols,
539 )
540 })
541 .collect();
542 let config = tenferro_tensor::backend::GroupedGemmConfig::new(
543 &native_jobs,
544 tenferro_tensor::DotGeneralAccumulation::overwrite(T::dtype()).map_err(|source| {
545 GroupedGemmError::Backend {
546 source: anyhow::Error::new(source),
547 }
548 })?,
549 );
550 let lhs_view = tenferro_tensor::TypedTensorView::from_slice([lhs.len()], [1], 0, lhs).map_err(
551 |source| GroupedGemmError::Backend {
552 source: anyhow::Error::new(source),
553 },
554 )?;
555 let rhs_view = tenferro_tensor::TypedTensorView::from_slice([rhs.len()], [1], 0, rhs).map_err(
556 |source| GroupedGemmError::Backend {
557 source: anyhow::Error::new(source),
558 },
559 )?;
560 let output_view =
561 tenferro_tensor::TypedTensorViewMut::from_slice([output.len()], [1], 0, output).map_err(
562 |source| GroupedGemmError::Backend {
563 source: anyhow::Error::new(source),
564 },
565 )?;
566 use tenferro_tensor::{TensorRead, TensorWrite};
567 session
568 .grouped_gemm_cached(
569 Some(0),
570 TensorRead::from_view(T::tensor_view(lhs_view)),
571 TensorRead::from_view(T::tensor_view(rhs_view)),
572 &config,
573 TensorWrite::from_view(T::tensor_view_mut(output_view)),
574 )
575 .map_err(|source| GroupedGemmError::Backend {
576 source: anyhow::Error::new(source),
577 })
578}
579
580/// Execute grouped column-major GEMMs over shared caller-owned buffers.
581///
582/// Each job computes `output[out_offset..] = lhs[lhs_offset..] *
583/// rhs[rhs_offset..]` for its declared matrix dimensions. Input spans may be
584/// reused by multiple jobs without copying their payload. The output buffer is
585/// mutated only after all descriptor, span, alias, and working-budget checks
586/// pass. The default process-global context supplies the configured provider;
587/// use [`grouped_mat_mul_shared_with_backend`] when the caller owns the
588/// backend explicitly.
589///
590/// # Errors
591///
592/// Returns [`GroupedGemmError`] for checked arithmetic, buffer bounds,
593/// incompatible shared shapes, overlapping outputs, working-budget, view, or
594/// configured-provider failures. Invalid requests are rejected before backend
595/// execution and leave `output` unchanged.
596///
597/// # Examples
598///
599/// ```
600/// use tensor4all_tensorbackend::{
601/// grouped_mat_mul_shared, GroupedGemmJob, GroupedGemmOptions,
602/// };
603///
604/// let jobs = [GroupedGemmJob::new(0, 0, 0, 1, 1, 1)];
605/// let mut output = [0.0_f64];
606/// grouped_mat_mul_shared(
607/// &[3.0], &[4.0], &mut output, &jobs, GroupedGemmOptions::default(),
608/// )?;
609/// assert_eq!(output, [12.0]);
610/// # Ok::<(), tensor4all_tensorbackend::GroupedGemmError>(())
611/// ```
612pub fn grouped_mat_mul_shared<T: MatrixScalar + TensorScalar>(
613 lhs: &[T],
614 rhs: &[T],
615 output: &mut [T],
616 jobs: &[GroupedGemmJob],
617 options: GroupedGemmOptions,
618) -> std::result::Result<(), GroupedGemmError> {
619 grouped_gemm_validate(lhs, rhs, output, jobs, options)?;
620 if jobs.is_empty() {
621 return Ok(());
622 }
623 crate::context::with_default_session(|session| {
624 grouped_mat_mul_shared_in_session(lhs, rhs, output, jobs, session)
625 })
626}
627
628/// Execute grouped GEMMs through one caller-configured CPU backend.
629///
630/// This is the explicit-provider counterpart to [`grouped_mat_mul_shared`].
631/// The backend's configured provider, thread count, and execution domain are
632/// preserved; this function never constructs a fallback backend.
633///
634/// # Errors
635///
636/// Rejects the request before touching `output`, with
637/// [`GroupedGemmError::DimensionOverflow`] or
638/// [`GroupedGemmError::SpanOverflow`] on checked descriptor arithmetic,
639/// [`GroupedGemmError::BufferOutOfBounds`] when a job's span leaves `lhs`,
640/// `rhs`, or `output`, [`GroupedGemmError::OverlappingOutputs`] when two jobs
641/// write the same element, [`GroupedGemmError::IncompatibleSharedLhs`] or
642/// [`GroupedGemmError::IncompatibleSharedRhs`] when jobs sharing an input
643/// offset disagree on its shape, and
644/// [`GroupedGemmError::WorkingMemoryExceeded`] when the batch exceeds
645/// `options.max_working_bytes`. Returns [`GroupedGemmError::Backend`] when
646/// building a view over a buffer fails or when `backend`'s configured
647/// provider fails to execute the batch; `output` may then be partially
648/// written.
649pub fn grouped_mat_mul_shared_with_backend<T: MatrixScalar + TensorScalar>(
650 backend: &mut tenferro_cpu::CpuBackend,
651 lhs: &[T],
652 rhs: &[T],
653 output: &mut [T],
654 jobs: &[GroupedGemmJob],
655 options: GroupedGemmOptions,
656) -> std::result::Result<(), GroupedGemmError> {
657 grouped_gemm_validate(lhs, rhs, output, jobs, options)?;
658 if jobs.is_empty() {
659 return Ok(());
660 }
661 use tenferro_tensor::BackendSessionHost;
662 backend.with_backend_session(|session| {
663 grouped_mat_mul_shared_in_session(lhs, rhs, output, jobs, session)
664 })
665}
666
667/// Execute grouped GEMMs while consuming all three flat buffers.
668///
669/// The returned vector is the supplied output buffer after the grouped
670/// operation. Consuming the inputs avoids caller-side ownership bookkeeping;
671/// it does not duplicate their payloads inside the grouped descriptor bridge.
672///
673/// # Errors
674///
675/// Returns [`GroupedGemmError`] using the same validation and provider rules as
676/// [`grouped_mat_mul_shared`].
677pub fn grouped_mat_mul_shared_owned<T: MatrixScalar + TensorScalar>(
678 lhs: Vec<T>,
679 rhs: Vec<T>,
680 mut output: Vec<T>,
681 jobs: &[GroupedGemmJob],
682 options: GroupedGemmOptions,
683) -> std::result::Result<Vec<T>, GroupedGemmError> {
684 grouped_mat_mul_shared(&lhs, &rhs, &mut output, jobs, options)?;
685 Ok(output)
686}
687
688/// Error returned by [`lowest_hermitian_eigenpair`].
689///
690/// The eigensolver is intended for small Rayleigh-Ritz projected matrices; it validates shape and Hermitian structure before calling the backend Hermitian
691/// eigendecomposition, symmetrizing only roundoff that is within the requested
692/// tolerance. Non-Hermitian effective operators are rejected explicitly instead
693/// of silently taking a real part.
694#[derive(Debug, thiserror::Error)]
695pub enum HermitianEigenError {
696 /// The matrix has zero rows and columns, so it has no eigenpair.
697 #[error("Hermitian eigenpair requires a non-empty matrix")]
698 Empty,
699 /// The input is not square.
700 #[error("Hermitian eigenpair requires a square matrix, got {nrows}x{ncols}")]
701 NonSquare {
702 /// Number of matrix rows.
703 nrows: usize,
704 /// Number of matrix columns.
705 ncols: usize,
706 },
707 /// The Hermitian validation tolerance was negative or not finite.
708 #[error("Hermitian tolerance must be finite and non-negative, got {tolerance}")]
709 InvalidTolerance {
710 /// Rejected tolerance value.
711 tolerance: f64,
712 },
713 /// A matrix entry violates `A[i, j] = conj(A[j, i])` within tolerance.
714 #[error(
715 "matrix is not Hermitian at ({row}, {col}): difference {difference} exceeds tolerance {tolerance}"
716 )]
717 NonHermitian {
718 /// Row of the first offending entry.
719 row: usize,
720 /// Column of the first offending entry.
721 col: usize,
722 /// Absolute Hermitian residual for the offending pair.
723 difference: f64,
724 /// Effective tolerance used for this entry pair.
725 tolerance: f64,
726 },
727 /// The backend returned an output with an unexpected dtype.
728 #[error("{output} output dtype mismatch: expected {expected}, got {actual}")]
729 DType {
730 /// Output tensor name.
731 output: &'static str,
732 /// Expected dtype string.
733 expected: String,
734 /// Actual dtype string.
735 actual: String,
736 },
737 /// The backend returned an output with an unexpected shape.
738 #[error("{output} output shape mismatch: expected {expected:?}, got {actual:?}")]
739 Shape {
740 /// Output tensor name.
741 output: &'static str,
742 /// Expected shape.
743 expected: Vec<usize>,
744 /// Actual shape.
745 actual: Vec<usize>,
746 },
747 /// A Hermitian backend eigenvalue had a non-negligible imaginary part.
748 #[error(
749 "Hermitian eigenvalue {index} has imaginary part {imaginary}, exceeding tolerance {tolerance}"
750 )]
751 NonRealEigenvalue {
752 /// Eigenvalue position in the backend output.
753 index: usize,
754 /// Absolute imaginary part.
755 imaginary: f64,
756 /// Tolerance used for validation.
757 tolerance: f64,
758 },
759 /// The tenferro backend rejected or failed the eigendecomposition.
760 #[error("Hermitian eigendecomposition failed: {source}")]
761 Backend {
762 /// Original backend diagnostic.
763 #[source]
764 source: Box<dyn std::error::Error + Send + Sync + 'static>,
765 },
766}
767
768/// Small Hermitian eigenpair returned by [`lowest_hermitian_eigenpair`].
769///
770/// `eigenvector` stores the Ritz vector coefficients in ordinary vector order.
771/// It has length equal to the input matrix dimension and is normalized according
772/// to the backend eigendecomposition.
773#[derive(Debug, Clone, PartialEq)]
774pub struct HermitianEigenpair<T> {
775 /// Smallest eigenvalue of the Hermitian matrix.
776 pub eigenvalue: f64,
777 /// Corresponding eigenvector coefficients.
778 pub eigenvector: Vec<T>,
779}
780
781/// Full eigendecomposition of a small Hermitian projected matrix.
782///
783/// `eigenvectors` stores one normalized eigenvector per column in column-major
784/// [`Matrix`] layout. Eigenvalues are returned in the backend's ascending
785/// Hermitian eigensolver order.
786///
787/// # Examples
788///
789/// ```
790/// use tensor4all_tensorbackend::{hermitian_eigendecomposition, Matrix};
791///
792/// let matrix = Matrix::from_col_major_vec(2, 2, vec![1.0, 0.0, 0.0, 2.0]);
793/// let decomp = hermitian_eigendecomposition(&matrix, 1.0e-12).unwrap();
794/// assert_eq!(decomp.eigenvalues, vec![1.0, 2.0]);
795/// assert_eq!(decomp.eigenvectors.nrows(), 2);
796/// assert_eq!(decomp.eigenvectors.ncols(), 2);
797/// ```
798#[derive(Debug, Clone)]
799pub struct HermitianEigendecomposition<T> {
800 /// Real eigenvalues of the Hermitian matrix.
801 pub eigenvalues: Vec<f64>,
802 /// Eigenvector matrix with one eigenvector in each column.
803 pub eigenvectors: Matrix<T>,
804}
805
806/// Scalar types supported by [`lowest_hermitian_eigenpair`].
807///
808/// The current backend path is used for `f64` and `Complex64`, which are the
809/// scalar types needed by tensor4all's Hermitian Krylov and DMRG algorithms.
810pub trait HermitianEigenScalar: TensorScalar + MatrixScalar {
811 #[doc(hidden)]
812 fn hermitian_difference(a_ij: Self, a_ji: Self) -> f64;
813
814 #[doc(hidden)]
815 fn hermitian_scale(a_ij: Self, a_ji: Self) -> f64;
816
817 #[doc(hidden)]
818 fn symmetrized_hermitian_pair(a_ij: Self, a_ji: Self) -> (Self, Self);
819
820 #[doc(hidden)]
821 fn eigenvalues_from_tensor(
822 tensor: Tensor,
823 tolerance: f64,
824 ) -> std::result::Result<(Vec<usize>, Vec<f64>), HermitianEigenError>;
825
826 #[doc(hidden)]
827 fn to_complex64(value: Self) -> Complex64;
828}
829
830impl HermitianEigenScalar for f64 {
831 fn hermitian_difference(a_ij: Self, a_ji: Self) -> f64 {
832 (a_ij - a_ji).abs()
833 }
834
835 fn hermitian_scale(a_ij: Self, a_ji: Self) -> f64 {
836 a_ij.abs().max(a_ji.abs()).max(1.0)
837 }
838
839 fn symmetrized_hermitian_pair(a_ij: Self, a_ji: Self) -> (Self, Self) {
840 let value = 0.5 * (a_ij + a_ji);
841 (value, value)
842 }
843
844 fn eigenvalues_from_tensor(
845 tensor: Tensor,
846 _tolerance: f64,
847 ) -> std::result::Result<(Vec<usize>, Vec<f64>), HermitianEigenError> {
848 let values = typed_eigh_output::<f64>("eigenvalues", tensor)?;
849 Ok((
850 values.shape().to_vec(),
851 values
852 .as_slice()
853 .map_err(|source| HermitianEigenError::Backend {
854 source: Box::new(source),
855 })?
856 .to_vec(),
857 ))
858 }
859
860 fn to_complex64(value: Self) -> Complex64 {
861 Complex64::new(value, 0.0)
862 }
863}
864
865impl HermitianEigenScalar for Complex64 {
866 fn hermitian_difference(a_ij: Self, a_ji: Self) -> f64 {
867 (a_ij - a_ji.conj()).norm()
868 }
869
870 fn hermitian_scale(a_ij: Self, a_ji: Self) -> f64 {
871 a_ij.norm().max(a_ji.norm()).max(1.0)
872 }
873
874 fn symmetrized_hermitian_pair(a_ij: Self, a_ji: Self) -> (Self, Self) {
875 let value = 0.5 * (a_ij + a_ji.conj());
876 (value, value.conj())
877 }
878
879 fn eigenvalues_from_tensor(
880 tensor: Tensor,
881 tolerance: f64,
882 ) -> std::result::Result<(Vec<usize>, Vec<f64>), HermitianEigenError> {
883 if tensor.dtype() == DType::F64 {
884 let values = typed_eigh_output::<f64>("eigenvalues", tensor)?;
885 let values_slice =
886 values
887 .as_slice()
888 .map_err(|source| HermitianEigenError::Backend {
889 source: Box::new(source),
890 })?;
891 return Ok((values.shape().to_vec(), values_slice.to_vec()));
892 }
893
894 let values = typed_eigh_output::<Complex64>("eigenvalues", tensor)?;
895 let values_slice = values
896 .as_slice()
897 .map_err(|source| HermitianEigenError::Backend {
898 source: Box::new(source),
899 })?;
900 let mut real_values = Vec::with_capacity(values_slice.len());
901 for (index, value) in values_slice.iter().copied().enumerate() {
902 let imaginary = value.im.abs();
903 let allowed = tolerance * value.norm().max(1.0);
904 if imaginary > allowed {
905 return Err(HermitianEigenError::NonRealEigenvalue {
906 index,
907 imaginary,
908 tolerance: allowed,
909 });
910 }
911 real_values.push(value.re);
912 }
913 Ok((values.shape().to_vec(), real_values))
914 }
915
916 fn to_complex64(value: Self) -> Complex64 {
917 value
918 }
919}
920
921impl<T> Matrix<T> {
922 /// Fallibly create a matrix from column-major data after checked shape validation.
923 ///
924 /// # Errors
925 /// Returns [`MatrixShapeError::ShapeOverflow`] when the shape exceeds
926 /// `usize`, or [`MatrixShapeError::DataLengthMismatch`] when the payload
927 /// length does not match the shape.
928 pub fn try_from_col_major_vec(
929 nrows: usize,
930 ncols: usize,
931 data: Vec<T>,
932 ) -> std::result::Result<Self, MatrixShapeError> {
933 let expected = nrows
934 .checked_mul(ncols)
935 .ok_or(MatrixShapeError::ShapeOverflow { nrows, ncols })?;
936 if data.len() != expected {
937 return Err(MatrixShapeError::DataLengthMismatch {
938 actual: data.len(),
939 expected,
940 });
941 }
942 Ok(Self { nrows, ncols, data })
943 }
944
945 /// Create a matrix from raw column-major data.
946 ///
947 /// # Panics
948 ///
949 /// Panics if `nrows * ncols` overflows or if `data.len() != nrows * ncols`.
950 ///
951 /// # Examples
952 ///
953 /// ```
954 /// use tensor4all_tensorbackend::Matrix;
955 ///
956 /// let m = Matrix::from_col_major_vec(2, 2, vec![1.0, 3.0, 2.0, 4.0]);
957 /// assert_eq!(m[[0, 0]], 1.0);
958 /// assert_eq!(m[[0, 1]], 2.0);
959 /// assert_eq!(m[[1, 0]], 3.0);
960 /// assert_eq!(m[[1, 1]], 4.0);
961 /// ```
962 pub fn from_col_major_vec(nrows: usize, ncols: usize, data: Vec<T>) -> Self {
963 let expected = checked_matrix_len(nrows, ncols);
964 assert!(
965 expected.is_some(),
966 "matrix shape product overflow: {nrows} rows * {ncols} columns"
967 );
968 let expected = expected.unwrap_or(0);
969 assert_eq!(data.len(), expected);
970 Self { data, nrows, ncols }
971 }
972
973 /// View the underlying column-major data as a contiguous slice.
974 ///
975 /// # Examples
976 ///
977 /// ```
978 /// use tensor4all_tensorbackend::Matrix;
979 ///
980 /// let m = Matrix::from_col_major_vec(2, 2, vec![1, 3, 2, 4]);
981 /// assert_eq!(m.as_col_major_slice(), &[1, 3, 2, 4]);
982 /// ```
983 pub fn as_col_major_slice(&self) -> &[T] {
984 &self.data
985 }
986
987 /// View the underlying column-major data as a mutable contiguous slice.
988 ///
989 /// The slice uses `row + nrows * col` ordering. This is useful for kernels
990 /// that validate dimensions once and then operate over contiguous columns.
991 ///
992 /// # Examples
993 ///
994 /// ```
995 /// use tensor4all_tensorbackend::Matrix;
996 ///
997 /// let mut m = Matrix::from_col_major_vec(2, 2, vec![1, 3, 2, 4]);
998 /// m.as_col_major_mut_slice()[1] = 30;
999 /// assert_eq!(m[[1, 0]], 30);
1000 /// ```
1001 pub fn as_col_major_mut_slice(&mut self) -> &mut [T] {
1002 &mut self.data
1003 }
1004
1005 /// Consume the matrix and return its owned column-major buffer.
1006 ///
1007 /// The returned buffer uses `row + nrows * col` ordering. Use this when
1008 /// transferring matrix storage to another column-major dense container
1009 /// without cloning.
1010 ///
1011 /// # Examples
1012 ///
1013 /// ```
1014 /// use tensor4all_tensorbackend::Matrix;
1015 ///
1016 /// let m = Matrix::from_col_major_vec(2, 2, vec![1.0, 3.0, 2.0, 4.0]);
1017 /// let data = m.into_col_major_vec();
1018 /// assert_eq!(data, vec![1.0, 3.0, 2.0, 4.0]);
1019 /// ```
1020 pub fn into_col_major_vec(self) -> Vec<T> {
1021 self.data
1022 }
1023
1024 /// Appends `right`'s columns to the end of this matrix in place, in
1025 /// column-major order.
1026 ///
1027 /// Reuses existing spare `Vec` capacity via amortized-doubling growth
1028 /// (`Vec::extend_from_slice`) instead of always reallocating and copying
1029 /// every existing element the way building a fresh concatenated `Matrix`
1030 /// via [`Matrix::try_from_col_major_vec`] would. Column-major layout
1031 /// makes appending columns an append-to-the-end operation on the flat
1032 /// buffer, so no existing element moves.
1033 ///
1034 /// # Errors
1035 /// Returns [`MatrixShapeError::ShapeOverflow`] when the combined column
1036 /// count would overflow `usize`.
1037 ///
1038 /// # Panics
1039 /// Panics in debug builds if `right`'s row count differs from `self`'s.
1040 /// Callers must guarantee matching row counts.
1041 pub(crate) fn append_columns(
1042 &mut self,
1043 right: &Matrix<T>,
1044 ) -> std::result::Result<(), MatrixShapeError>
1045 where
1046 T: Clone,
1047 {
1048 debug_assert_eq!(
1049 self.nrows(),
1050 right.nrows(),
1051 "append_columns requires matching row counts: {} vs {}",
1052 self.nrows(),
1053 right.nrows()
1054 );
1055 let new_ncols = self.ncols().checked_add(right.ncols()).ok_or(
1056 // The true combined column count is exactly what overflows here, so
1057 // it cannot be reported without fabricating a value (a saturating
1058 // add would always read back as `usize::MAX`, which carries no
1059 // information about the actual operands). Report the current,
1060 // real shape of `self` instead.
1061 MatrixShapeError::ShapeOverflow {
1062 nrows: self.nrows(),
1063 ncols: self.ncols(),
1064 },
1065 )?;
1066 self.data.extend_from_slice(right.as_col_major_slice());
1067 self.ncols = new_ncols;
1068 Ok(())
1069 }
1070
1071 /// Borrow this matrix as an owned tenferro [`TypedTensor`].
1072 ///
1073 /// This clones the matrix buffer and preserves column-major layout. Use
1074 /// [`Matrix::into_typed_tensor`] when the matrix can be consumed.
1075 ///
1076 /// # Examples
1077 ///
1078 /// ```
1079 /// use tensor4all_tensorbackend::Matrix;
1080 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1081 ///
1082 /// let m = Matrix::from_col_major_vec(2, 2, vec![1.0_f64, 3.0, 2.0, 4.0]);
1083 /// let tensor = m.to_typed_tensor();
1084 /// assert_eq!(tensor.shape(), &[2, 2]);
1085 /// assert_eq!(tensor.as_slice()?, &[1.0, 3.0, 2.0, 4.0]);
1086 /// assert_eq!(m.as_col_major_slice(), &[1.0, 3.0, 2.0, 4.0]);
1087 /// # Ok(())
1088 /// # }
1089 /// ```
1090 pub fn to_typed_tensor(&self) -> TypedTensor<T>
1091 where
1092 T: TensorScalar,
1093 {
1094 crate::require_invariant(
1095 TypedTensor::from_vec_col_major(vec![self.nrows, self.ncols], self.data.clone()),
1096 "validated matrix rejected by tenferro",
1097 )
1098 }
1099
1100 /// Consume this matrix as a tenferro [`TypedTensor`] without cloning.
1101 ///
1102 /// The tensor shape is `[nrows, ncols]`, and the owned data remains in
1103 /// column-major layout.
1104 ///
1105 /// # Examples
1106 ///
1107 /// ```
1108 /// use tensor4all_tensorbackend::Matrix;
1109 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1110 ///
1111 /// let m = Matrix::from_col_major_vec(2, 2, vec![1.0_f64, 3.0, 2.0, 4.0]);
1112 /// let tensor = m.into_typed_tensor();
1113 /// assert_eq!(tensor.shape(), &[2, 2]);
1114 /// assert_eq!(tensor.as_slice()?, &[1.0, 3.0, 2.0, 4.0]);
1115 /// # Ok(())
1116 /// # }
1117 /// ```
1118 pub fn into_typed_tensor(self) -> TypedTensor<T>
1119 where
1120 T: TensorScalar,
1121 {
1122 crate::require_invariant(
1123 TypedTensor::from_vec_col_major(vec![self.nrows, self.ncols], self.data),
1124 "validated matrix rejected by tenferro",
1125 )
1126 }
1127
1128 /// Consume a rank-2 tenferro [`TypedTensor`] as a [`Matrix`].
1129 ///
1130 /// The input tensor must have shape `[nrows, ncols]` and an owned host
1131 /// buffer. The buffer is reused without cloning and interpreted as
1132 /// column-major matrix storage.
1133 ///
1134 /// # Errors
1135 ///
1136 /// Returns [`MatrixTensorConversionError::Rank`] if the tensor is not
1137 /// rank-2, or [`MatrixTensorConversionError::HostBuffer`] if tenferro
1138 /// cannot export the tensor as an owned host buffer.
1139 ///
1140 /// # Examples
1141 ///
1142 /// ```
1143 /// use tenferro::TypedTensor;
1144 /// use tensor4all_tensorbackend::Matrix;
1145 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1146 ///
1147 /// let tensor = TypedTensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 3.0, 2.0, 4.0])?;
1148 /// let m = Matrix::try_from_typed_tensor(tensor)?;
1149 /// assert_eq!(m.nrows(), 2);
1150 /// assert_eq!(m.ncols(), 2);
1151 /// assert_eq!(m[[0, 1]], 2.0);
1152 /// # Ok(())
1153 /// # }
1154 /// ```
1155 pub fn try_from_typed_tensor(
1156 tensor: TypedTensor<T>,
1157 ) -> std::result::Result<Self, MatrixTensorConversionError>
1158 where
1159 T: TensorScalar + Clone,
1160 {
1161 let (shape, data) = tensor.into_vec_col_major().map_err(|source| {
1162 MatrixTensorConversionError::HostBuffer {
1163 message: source.to_string(),
1164 }
1165 })?;
1166 if shape.len() != 2 {
1167 return Err(MatrixTensorConversionError::Rank { shape });
1168 }
1169 Ok(Self::from_col_major_vec(shape[0], shape[1], data))
1170 }
1171
1172 fn offset(&self, row: usize, col: usize) -> usize {
1173 assert!(
1174 row < self.nrows,
1175 "matrix row index {row} out of bounds (bound: {})",
1176 self.nrows
1177 );
1178 assert!(
1179 col < self.ncols,
1180 "matrix column index {col} out of bounds (bound: {})",
1181 self.ncols
1182 );
1183 row + self.nrows * col
1184 }
1185
1186 /// Number of rows
1187 pub fn nrows(&self) -> usize {
1188 self.nrows
1189 }
1190
1191 /// Number of columns
1192 pub fn ncols(&self) -> usize {
1193 self.ncols
1194 }
1195}
1196
1197/// Compute the smallest eigenpair of a small Hermitian projected matrix.
1198///
1199/// This function validates that `matrix` is square, non-empty, and Hermitian
1200/// within `hermitian_tol`, symmetrizes accepted roundoff as `(A + A†) / 2`,
1201/// then calls tenferro's Hermitian eigendecomposition. It is intended for
1202/// Rayleigh-Ritz projected Krylov matrices, whose dimension is bounded by the
1203/// Krylov subspace size. It must not be used to materialize a full
1204/// tensor-network effective Hamiltonian.
1205///
1206/// # Arguments
1207/// * `matrix` - Small dense Hermitian matrix in column-major [`Matrix`] layout.
1208/// * `hermitian_tol` - Relative tolerance for `A[i, j] = conj(A[j, i])`,
1209///
1210/// applied as `hermitian_tol * max(1, |A[i,j]|, |A[j,i]|)`.
1211/// Typical values are `1e-12` for `f64`/`Complex64` projected matrices.
1212///
1213/// # Returns
1214/// The smallest real eigenvalue and the corresponding normalized eigenvector
1215/// coefficients.
1216///
1217/// # Errors
1218/// Returns [`HermitianEigenError`] if the matrix is empty, non-square,
1219/// non-Hermitian within `hermitian_tol`, or if the backend eigendecomposition
1220/// fails or returns an unexpected dtype/shape.
1221///
1222/// # Examples
1223///
1224/// ```
1225/// use tensor4all_tensorbackend::{lowest_hermitian_eigenpair, Matrix};
1226///
1227/// let matrix = Matrix::from_col_major_vec(2, 2, vec![2.0_f64, 1.0, 1.0, 2.0]);
1228/// let pair = lowest_hermitian_eigenpair(&matrix, 1.0e-12).unwrap();
1229///
1230/// assert!((pair.eigenvalue - 1.0).abs() < 1.0e-12);
1231/// assert_eq!(pair.eigenvector.len(), 2);
1232/// ```
1233pub fn lowest_hermitian_eigenpair<T>(
1234 matrix: &Matrix<T>,
1235 hermitian_tol: f64,
1236) -> std::result::Result<HermitianEigenpair<T>, HermitianEigenError>
1237where
1238 T: HermitianEigenScalar,
1239{
1240 let decomp = hermitian_eigendecomposition(matrix, hermitian_tol)?;
1241
1242 let (min_col, eigenvalue) = decomp
1243 .eigenvalues
1244 .iter()
1245 .copied()
1246 .enumerate()
1247 .min_by(|(_, a), (_, b)| a.total_cmp(b))
1248 .ok_or(HermitianEigenError::Empty)?;
1249
1250 let n = decomp.eigenvalues.len();
1251 let vector_data = decomp.eigenvectors.as_col_major_slice();
1252 let start = n * min_col;
1253 let eigenvector = vector_data[start..start + n].to_vec();
1254
1255 Ok(HermitianEigenpair {
1256 eigenvalue,
1257 eigenvector,
1258 })
1259}
1260
1261/// Compute all eigenpairs of a small Hermitian projected matrix.
1262///
1263/// This validates Hermitian structure and symmetrizes accepted roundoff before
1264/// calling the backend. It is meant for bounded Krylov/Rayleigh-Ritz matrices,
1265/// not full tensor-network materialization.
1266///
1267/// # Arguments
1268///
1269/// * `matrix` - Square Hermitian matrix in column-major [`Matrix`] layout.
1270/// * `hermitian_tol` - Relative tolerance for checking `A = A†`, applied per
1271///
1272/// entry pair with scale `max(1, |A[i,j]|, |A[j,i]|)`.
1273///
1274/// # Returns
1275///
1276/// All real eigenvalues and all eigenvectors of `matrix`.
1277///
1278/// # Errors
1279///
1280/// Returns [`HermitianEigenError`] if `matrix` is not square, is not Hermitian
1281/// within `hermitian_tol`, or the backend eigensolver fails.
1282///
1283/// # Examples
1284///
1285/// ```
1286/// use tensor4all_tensorbackend::{hermitian_eigendecomposition, Matrix};
1287///
1288/// let matrix = Matrix::from_col_major_vec(2, 2, vec![3.0, 0.0, 0.0, 5.0]);
1289/// let decomp = hermitian_eigendecomposition(&matrix, 1.0e-12).unwrap();
1290/// assert_eq!(decomp.eigenvalues, vec![3.0, 5.0]);
1291/// assert_eq!(decomp.eigenvectors.as_col_major_slice().len(), 4);
1292/// ```
1293pub fn hermitian_eigendecomposition<T>(
1294 matrix: &Matrix<T>,
1295 hermitian_tol: f64,
1296) -> std::result::Result<HermitianEigendecomposition<T>, HermitianEigenError>
1297where
1298 T: HermitianEigenScalar,
1299{
1300 let matrix = validate_and_symmetrize_hermitian_matrix(matrix, hermitian_tol)?;
1301
1302 let n = matrix.nrows();
1303 let input_tensor =
1304 T::into_tensor(vec![n, n], matrix.into_col_major_vec()).map_err(|source| {
1305 HermitianEigenError::Backend {
1306 source: Box::new(source),
1307 }
1308 })?;
1309 let eager_ctx = crate::default_eager_ctx().map_err(|source| HermitianEigenError::Backend {
1310 source: Box::new(source),
1311 })?;
1312 let input = EagerTensor::from_tensor_in(input_tensor, eager_ctx).map_err(|source| {
1313 HermitianEigenError::Backend {
1314 source: Box::new(source),
1315 }
1316 })?;
1317 let (values, vectors) = input
1318 .eigh()
1319 .map_err(|source| HermitianEigenError::Backend {
1320 source: Box::new(source),
1321 })?;
1322 let values = values
1323 .to_tensor()
1324 .map_err(|source| HermitianEigenError::Backend {
1325 source: Box::new(source),
1326 })?;
1327 let vectors = vectors
1328 .to_tensor()
1329 .map_err(|source| HermitianEigenError::Backend {
1330 source: Box::new(source),
1331 })?;
1332
1333 let (values_shape, eigenvalues) = T::eigenvalues_from_tensor(values, hermitian_tol)?;
1334 ensure_eigh_shape("eigenvalues", &values_shape, &[n])?;
1335 let vectors = typed_eigh_output::<T>("eigenvectors", vectors)?;
1336 ensure_eigh_shape("eigenvectors", vectors.shape(), &[n, n])?;
1337
1338 Ok(HermitianEigendecomposition {
1339 eigenvalues,
1340 eigenvectors: Matrix::from_col_major_vec(
1341 n,
1342 n,
1343 vectors
1344 .as_slice()
1345 .map_err(|source| HermitianEigenError::Backend {
1346 source: Box::new(source),
1347 })?
1348 .to_vec(),
1349 ),
1350 })
1351}
1352
1353/// Compute the first column of `exp(exponent * A)` for a small Hermitian matrix.
1354///
1355/// Krylov exponential routines use this for the projected matrix action on the
1356/// first basis vector. The returned coefficients are complex even when `A` is
1357/// real because real-time evolution has complex phases.
1358///
1359/// # Arguments
1360///
1361/// * `matrix` - Square Hermitian matrix in column-major [`Matrix`] layout.
1362/// * `exponent` - Scalar multiplier in `exp(exponent * A)`.
1363/// * `hermitian_tol` - Relative tolerance for checking `A = A†`; accepted
1364///
1365/// roundoff is symmetrized before eigensolving.
1366///
1367/// # Returns
1368///
1369/// The first column of the matrix exponential.
1370///
1371/// # Errors
1372///
1373/// Returns [`HermitianEigenError`] if Hermitian validation or eigensolving
1374/// fails.
1375///
1376/// # Examples
1377///
1378/// ```
1379/// use num_complex::Complex64;
1380/// use tensor4all_tensorbackend::{hermitian_exponential_first_column, Matrix};
1381///
1382/// let matrix = Matrix::from_col_major_vec(2, 2, vec![1.0, 0.0, 0.0, 2.0]);
1383/// let column = hermitian_exponential_first_column(
1384/// &matrix,
1385/// Complex64::new(0.0, -0.5),
1386/// 1.0e-12,
1387/// ).unwrap();
1388/// let expected = Complex64::new(0.5_f64.cos(), -0.5_f64.sin());
1389/// assert!((column[0] - expected).norm() < 1.0e-12);
1390/// assert!(column[1].norm() < 1.0e-12);
1391/// ```
1392pub fn hermitian_exponential_first_column<T>(
1393 matrix: &Matrix<T>,
1394 exponent: Complex64,
1395 hermitian_tol: f64,
1396) -> std::result::Result<Vec<Complex64>, HermitianEigenError>
1397where
1398 T: HermitianEigenScalar,
1399{
1400 let decomp = hermitian_eigendecomposition(matrix, hermitian_tol)?;
1401 let n = decomp.eigenvalues.len();
1402 let vectors = decomp.eigenvectors.as_col_major_slice();
1403 let mut result = vec![Complex64::new(0.0, 0.0); n];
1404
1405 for col in 0..n {
1406 let lambda = decomp.eigenvalues[col];
1407 let phase = (exponent * lambda).exp();
1408 let first_component = T::to_complex64(vectors[col * n]).conj();
1409 for row in 0..n {
1410 result[row] += T::to_complex64(vectors[row + col * n]) * phase * first_component;
1411 }
1412 }
1413
1414 Ok(result)
1415}
1416
1417fn validate_and_symmetrize_hermitian_matrix<T>(
1418 matrix: &Matrix<T>,
1419 hermitian_tol: f64,
1420) -> std::result::Result<Matrix<T>, HermitianEigenError>
1421where
1422 T: HermitianEigenScalar,
1423{
1424 if !hermitian_tol.is_finite() || hermitian_tol < 0.0 {
1425 return Err(HermitianEigenError::InvalidTolerance {
1426 tolerance: hermitian_tol,
1427 });
1428 }
1429 if matrix.nrows() != matrix.ncols() {
1430 return Err(HermitianEigenError::NonSquare {
1431 nrows: matrix.nrows(),
1432 ncols: matrix.ncols(),
1433 });
1434 }
1435 if matrix.nrows() == 0 {
1436 return Err(HermitianEigenError::Empty);
1437 }
1438
1439 let n = matrix.nrows();
1440 let mut data = matrix.as_col_major_slice().to_vec();
1441 for col in 0..matrix.ncols() {
1442 for row in 0..=col {
1443 let row_col = matrix[[row, col]];
1444 let col_row = matrix[[col, row]];
1445 let difference = T::hermitian_difference(row_col, col_row);
1446 let tolerance = hermitian_tol * T::hermitian_scale(row_col, col_row);
1447 if difference > tolerance {
1448 return Err(HermitianEigenError::NonHermitian {
1449 row,
1450 col,
1451 difference,
1452 tolerance,
1453 });
1454 }
1455 let (row_col, col_row) = T::symmetrized_hermitian_pair(row_col, col_row);
1456 data[row + n * col] = row_col;
1457 data[col + n * row] = col_row;
1458 }
1459 }
1460 Ok(Matrix::from_col_major_vec(n, n, data))
1461}
1462
1463fn typed_eigh_output<T>(
1464 output: &'static str,
1465 tensor: Tensor,
1466) -> std::result::Result<TypedTensor<T>, HermitianEigenError>
1467where
1468 T: TensorScalar,
1469{
1470 let actual = tensor.dtype();
1471 T::into_typed(tensor).map_err(|_| HermitianEigenError::DType {
1472 output,
1473 expected: format!("{:?}", T::dtype()),
1474 actual: format!("{actual:?}"),
1475 })
1476}
1477
1478fn ensure_eigh_shape(
1479 output: &'static str,
1480 actual: &[usize],
1481 expected: &[usize],
1482) -> std::result::Result<(), HermitianEigenError> {
1483 if actual != expected {
1484 return Err(HermitianEigenError::Shape {
1485 output,
1486 expected: expected.to_vec(),
1487 actual: actual.to_vec(),
1488 });
1489 }
1490 Ok(())
1491}
1492
1493impl<T: Clone> Matrix<T> {
1494 /// Create a new matrix filled with a constant value.
1495 ///
1496 /// # Panics
1497 ///
1498 /// Panics if `nrows * ncols` overflows.
1499 ///
1500 /// # Examples
1501 ///
1502 /// ```
1503 /// use tensor4all_tensorbackend::Matrix;
1504 ///
1505 /// let m = Matrix::from_elem(2, 3, 7.0);
1506 /// assert_eq!(m[[0, 0]], 7.0);
1507 /// assert_eq!(m[[1, 2]], 7.0);
1508 /// ```
1509 pub fn from_elem(nrows: usize, ncols: usize, elem: T) -> Self {
1510 let len = checked_matrix_len(nrows, ncols);
1511 assert!(
1512 len.is_some(),
1513 "matrix shape product overflow: {nrows} rows * {ncols} columns"
1514 );
1515 let len = len.unwrap_or(0);
1516 Self {
1517 data: vec![elem; len],
1518 nrows,
1519 ncols,
1520 }
1521 }
1522}
1523
1524impl<T: Clone + Zero> Matrix<T> {
1525 /// Fallibly create a zero-filled matrix after checked shape validation.
1526 ///
1527 /// # Errors
1528 /// Returns [`MatrixShapeError::ShapeOverflow`] when the shape exceeds
1529 /// `usize`.
1530 pub fn try_zeros(nrows: usize, ncols: usize) -> std::result::Result<Self, MatrixShapeError> {
1531 let len = nrows
1532 .checked_mul(ncols)
1533 .ok_or(MatrixShapeError::ShapeOverflow { nrows, ncols })?;
1534 Self::try_from_col_major_vec(nrows, ncols, vec![T::zero(); len])
1535 }
1536
1537 /// Create a zeros matrix
1538 ///
1539 /// # Panics
1540 ///
1541 /// Panics if `nrows * ncols` overflows.
1542 ///
1543 /// # Examples
1544 ///
1545 /// ```
1546 /// use tensor4all_tensorbackend::Matrix;
1547 ///
1548 /// let m = Matrix::<f64>::zeros(2, 3);
1549 /// assert_eq!(m.nrows(), 2);
1550 /// assert_eq!(m.ncols(), 3);
1551 /// assert_eq!(m[[0, 0]], 0.0);
1552 /// assert_eq!(m[[1, 2]], 0.0);
1553 /// ```
1554 pub fn zeros(nrows: usize, ncols: usize) -> Self {
1555 let len = checked_matrix_len(nrows, ncols);
1556 assert!(
1557 len.is_some(),
1558 "matrix shape product overflow: {nrows} rows * {ncols} columns"
1559 );
1560 let len = len.unwrap_or(0);
1561 Self {
1562 data: vec![T::zero(); len],
1563 nrows,
1564 ncols,
1565 }
1566 }
1567}
1568
1569impl<T> Index<[usize; 2]> for Matrix<T> {
1570 type Output = T;
1571
1572 fn index(&self, idx: [usize; 2]) -> &Self::Output {
1573 &self.data[self.offset(idx[0], idx[1])]
1574 }
1575}
1576
1577impl<T> IndexMut<[usize; 2]> for Matrix<T> {
1578 fn index_mut(&mut self, idx: [usize; 2]) -> &mut Self::Output {
1579 let offset = self.offset(idx[0], idx[1]);
1580 &mut self.data[offset]
1581 }
1582}
1583
1584/// Create a matrix from a 2D vector, returning an error for ragged rows.
1585///
1586/// Each inner `Vec` is one row. The resulting matrix is stored internally in
1587/// column-major order.
1588///
1589/// # Errors
1590///
1591/// Returns [`MatrixShapeError::RaggedRows`] when any row has a different length
1592/// than the first row.
1593///
1594/// # Examples
1595///
1596/// ```
1597/// use tensor4all_tensorbackend::try_from_vec2d;
1598///
1599/// let m = try_from_vec2d(vec![
1600/// vec![1.0, 2.0],
1601/// vec![3.0, 4.0],
1602/// ])?;
1603/// assert_eq!(m.nrows(), 2);
1604/// assert_eq!(m.ncols(), 2);
1605/// assert_eq!(m[[0, 1]], 2.0);
1606/// assert_eq!(m[[1, 0]], 3.0);
1607/// # Ok::<(), tensor4all_tensorbackend::MatrixShapeError>(())
1608/// ```
1609pub fn try_from_vec2d<T: Clone + Zero>(
1610 data: Vec<Vec<T>>,
1611) -> std::result::Result<Matrix<T>, MatrixShapeError> {
1612 let nrows = data.len();
1613 let ncols = data.first().map_or(0, Vec::len);
1614 for (row, values) in data.iter().enumerate() {
1615 let actual = values.len();
1616 if actual != ncols {
1617 return Err(MatrixShapeError::RaggedRows {
1618 row,
1619 expected: ncols,
1620 actual,
1621 });
1622 }
1623 }
1624 let mut m = Matrix::zeros(nrows, ncols);
1625 for j in 0..ncols {
1626 for i in 0..nrows {
1627 m[[i, j]] = data[i][j].clone();
1628 }
1629 }
1630 Ok(m)
1631}
1632
1633/// Create a matrix from a rectangular 2D vector.
1634///
1635/// Each inner `Vec` is one row. The resulting matrix is stored internally in
1636/// column-major order.
1637///
1638/// # Panics
1639///
1640/// Panics if the row lengths are not all equal or if the rectangular shape's
1641/// element count overflows `usize`. Use [`try_from_vec2d`] when row-shaped
1642/// input comes from users, files, or other fallible boundaries to receive a
1643/// typed error for ragged rows.
1644///
1645/// # Examples
1646///
1647/// ```
1648/// use tensor4all_tensorbackend::from_vec2d;
1649///
1650/// let m = from_vec2d(vec![
1651/// vec![1.0, 2.0],
1652/// vec![3.0, 4.0],
1653/// ]);
1654/// assert_eq!(m.nrows(), 2);
1655/// assert_eq!(m.ncols(), 2);
1656/// assert_eq!(m[[0, 1]], 2.0);
1657/// assert_eq!(m[[1, 0]], 3.0);
1658/// ```
1659pub fn from_vec2d<T: Clone + Zero>(data: Vec<Vec<T>>) -> Matrix<T> {
1660 let result = try_from_vec2d(data);
1661 let error_message = match &result {
1662 Ok(_) => String::new(),
1663 Err(error) => error.to_string(),
1664 };
1665 assert!(result.is_ok(), "{error_message}");
1666 match result {
1667 Ok(matrix) => matrix,
1668 Err(_) => Matrix {
1669 data: Vec::new(),
1670 nrows: 0,
1671 ncols: 0,
1672 },
1673 }
1674}
1675
1676/// Get a submatrix by selecting specific rows and columns.
1677///
1678/// # Panics
1679///
1680/// Panics if any row is not less than `m.nrows()` or any column is not less
1681/// than `m.ncols()`.
1682///
1683/// # Examples
1684///
1685/// ```
1686/// use tensor4all_tensorbackend::{from_vec2d, submatrix};
1687///
1688/// let m = from_vec2d(vec![
1689/// vec![1.0, 2.0, 3.0],
1690/// vec![4.0, 5.0, 6.0],
1691/// vec![7.0, 8.0, 9.0],
1692/// ]);
1693/// let sub = submatrix(&m, &[0, 2], &[1, 2]);
1694/// assert_eq!(sub.nrows(), 2);
1695/// assert_eq!(sub.ncols(), 2);
1696/// assert_eq!(sub[[0, 0]], 2.0); // m[0, 1]
1697/// assert_eq!(sub[[1, 1]], 9.0); // m[2, 2]
1698/// ```
1699pub fn submatrix<T: Clone + Zero>(m: &Matrix<T>, rows: &[usize], cols: &[usize]) -> Matrix<T> {
1700 assert!(
1701 rows.iter().all(|&row| row < m.nrows),
1702 "submatrix row index out of bounds"
1703 );
1704 assert!(
1705 cols.iter().all(|&col| col < m.ncols),
1706 "submatrix column index out of bounds"
1707 );
1708
1709 let mut data = Vec::with_capacity(rows.len() * cols.len());
1710 let source = m.as_col_major_slice();
1711 for &col in cols {
1712 let col_start = col * m.nrows;
1713 for &row in rows {
1714 let offset = col_start + row;
1715 // SAFETY: rows and cols are range-checked above, and Matrix stores
1716 // exactly nrows * ncols values in column-major order.
1717 data.push(unsafe { source.get_unchecked(offset).clone() });
1718 }
1719 }
1720 Matrix::from_col_major_vec(rows.len(), cols.len(), data)
1721}
1722
1723/// Swap two rows in a matrix in-place.
1724///
1725/// No-op if `a == b`, after validating that the index exists.
1726///
1727/// # Errors
1728///
1729/// Returns [`MatrixShapeError::RowIndexOutOfBounds`] if either index is not
1730/// less than `m.nrows()`.
1731///
1732/// # Examples
1733///
1734/// ```
1735/// use tensor4all_tensorbackend::{from_vec2d, swap_rows};
1736///
1737/// let mut m = from_vec2d(vec![vec![1.0, 2.0], vec![3.0, 4.0]]);
1738/// swap_rows(&mut m, 0, 1).unwrap();
1739/// assert_eq!(m[[0, 0]], 3.0);
1740/// assert_eq!(m[[1, 0]], 1.0);
1741/// ```
1742pub fn swap_rows<T>(m: &mut Matrix<T>, a: usize, b: usize) -> Result<(), MatrixShapeError> {
1743 for index in [a, b] {
1744 if index >= m.nrows {
1745 return Err(MatrixShapeError::RowIndexOutOfBounds {
1746 index,
1747 nrows: m.nrows,
1748 });
1749 }
1750 }
1751 if a == b {
1752 return Ok(());
1753 }
1754 let nrows = m.nrows;
1755 let ncols = m.ncols;
1756 let data = m.as_col_major_mut_slice();
1757 for j in 0..ncols {
1758 data.swap(a + nrows * j, b + nrows * j);
1759 }
1760 Ok(())
1761}
1762
1763/// Swap two columns in a matrix in-place.
1764///
1765/// No-op if `a == b`, after validating that the index exists.
1766///
1767/// # Errors
1768///
1769/// Returns [`MatrixShapeError::ColumnIndexOutOfBounds`] if either index is not
1770/// less than `m.ncols()`.
1771///
1772/// # Examples
1773///
1774/// ```
1775/// use tensor4all_tensorbackend::{from_vec2d, swap_cols};
1776///
1777/// let mut m = from_vec2d(vec![vec![1.0, 2.0], vec![3.0, 4.0]]);
1778/// swap_cols(&mut m, 0, 1).unwrap();
1779/// assert_eq!(m[[0, 0]], 2.0);
1780/// assert_eq!(m[[0, 1]], 1.0);
1781/// ```
1782pub fn swap_cols<T>(m: &mut Matrix<T>, a: usize, b: usize) -> Result<(), MatrixShapeError> {
1783 for index in [a, b] {
1784 if index >= m.ncols {
1785 return Err(MatrixShapeError::ColumnIndexOutOfBounds {
1786 index,
1787 ncols: m.ncols,
1788 });
1789 }
1790 }
1791 if a == b {
1792 return Ok(());
1793 }
1794 let nrows = m.nrows;
1795 let start_a = nrows * a;
1796 let start_b = nrows * b;
1797 let data = m.as_col_major_mut_slice();
1798 if start_a < start_b {
1799 let (left, right) = data.split_at_mut(start_b);
1800 left[start_a..start_a + nrows].swap_with_slice(&mut right[..nrows]);
1801 } else {
1802 let (left, right) = data.split_at_mut(start_a);
1803 right[..nrows].swap_with_slice(&mut left[start_b..start_b + nrows]);
1804 }
1805 Ok(())
1806}
1807
1808/// Transpose the matrix.
1809///
1810/// # Examples
1811///
1812/// ```
1813/// use tensor4all_tensorbackend::{from_vec2d, transpose};
1814///
1815/// let m = from_vec2d(vec![vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]]);
1816/// let mt = transpose(&m);
1817/// assert_eq!(mt.nrows(), 3);
1818/// assert_eq!(mt.ncols(), 2);
1819/// assert_eq!(mt[[0, 0]], 1.0);
1820/// assert_eq!(mt[[2, 1]], 6.0);
1821/// ```
1822pub fn transpose<T: Clone + Zero>(m: &Matrix<T>) -> Matrix<T> {
1823 let mut result = Matrix::zeros(m.ncols, m.nrows);
1824 for j in 0..m.ncols {
1825 for i in 0..m.nrows {
1826 result[[j, i]] = m[[i, j]].clone();
1827 }
1828 }
1829 result
1830}
1831
1832/// Find the position and value of the maximum absolute value in a submatrix.
1833///
1834/// Searches within the rectangular region defined by `rows x cols` ranges.
1835/// Returns `(row, col, value)` of the element with the largest `|value|^2`.
1836///
1837/// # Panics
1838///
1839/// Panics if either range is empty or either end is out of bounds (`rows.end > a.nrows()` or `cols.end > a.ncols()`).
1840///
1841/// # Examples
1842///
1843/// ```
1844/// use tensor4all_tensorbackend::{from_vec2d, submatrix_argmax};
1845///
1846/// let m = from_vec2d(vec![
1847/// vec![1.0_f64, 2.0, 3.0],
1848/// vec![4.0, 9.0, 6.0],
1849/// vec![7.0, 8.0, 5.0],
1850/// ]);
1851/// let (row, col, val) = submatrix_argmax(&m, 0..3, 0..3);
1852/// assert_eq!(row, 1);
1853/// assert_eq!(col, 1);
1854/// assert_eq!(val, 9.0);
1855/// ```
1856pub fn submatrix_argmax<T: MatrixScalar>(
1857 a: &Matrix<T>,
1858 rows: std::ops::Range<usize>,
1859 cols: std::ops::Range<usize>,
1860) -> (usize, usize, T) {
1861 assert!(!rows.is_empty(), "rows must not be empty");
1862 assert!(!cols.is_empty(), "cols must not be empty");
1863 assert!(rows.end <= a.nrows, "row range out of bounds");
1864 assert!(cols.end <= a.ncols, "column range out of bounds");
1865
1866 let data = a.as_col_major_slice();
1867 let first_offset = rows.start + a.nrows * cols.start;
1868 // SAFETY: the non-empty ranges are checked against the matrix shape above.
1869 let first = unsafe { *data.get_unchecked(first_offset) };
1870 let mut max_val: f64 = first.matrix_abs_sq();
1871 let mut max_row = rows.start;
1872 let mut max_col = cols.start;
1873 let row_start = rows.start;
1874 let row_end = rows.end;
1875 let col_start = cols.start;
1876 let col_end = cols.end;
1877
1878 for c in col_start..col_end {
1879 let col_start_offset = row_start + a.nrows * c;
1880 for (offset, r) in (col_start_offset..).zip(row_start..row_end) {
1881 // SAFETY: row and column loops stay within the checked ranges.
1882 let value = unsafe { *data.get_unchecked(offset) };
1883 let val: f64 = value.matrix_abs_sq();
1884 if val > max_val {
1885 max_val = val;
1886 max_row = r;
1887 max_col = c;
1888 }
1889 }
1890 }
1891
1892 let max_offset = max_row + a.nrows * max_col;
1893 // SAFETY: max_row/max_col were selected from the checked ranges.
1894 (max_row, max_col, unsafe { *data.get_unchecked(max_offset) })
1895}
1896
1897/// BLAS-backed matrix multiplication dispatch.
1898///
1899/// Implemented for all scalar types supported by tenferro einsum
1900/// (f64, f32, Complex64, Complex32). This trait is sealed — external
1901/// types cannot implement it.
1902pub trait BlasMul: Sized {
1903 #[doc(hidden)]
1904 fn blas_mat_mul(a: &Matrix<Self>, b: &Matrix<Self>) -> Result<Matrix<Self>>;
1905
1906 #[doc(hidden)]
1907 fn blas_mat_mul_owned(a: Matrix<Self>, b: Matrix<Self>) -> Result<Matrix<Self>>;
1908}
1909
1910fn dot_general_matrices<T>(
1911 a_tensor: Tensor,
1912 b_tensor: Tensor,
1913 m: usize,
1914 n: usize,
1915 expected_len: usize,
1916) -> Result<Matrix<T>>
1917where
1918 T: TensorScalar,
1919{
1920 use crate::context::with_default_session;
1921 use tenferro::TensorSessionOpsExt;
1922
1923 let c = with_default_session(|session| a_tensor.matmul(&b_tensor, session))
1924 .context("matrix multiplication failed")?;
1925 let c = T::into_typed(c)
1926 .map_err(|error| anyhow::anyhow!("matrix multiplication returned wrong dtype: {error}"))?;
1927 let result = Matrix::try_from_typed_tensor(c)?;
1928 ensure!(
1929 result.nrows() == m && result.ncols() == n,
1930 "matrix multiplication returned shape {}x{} for expected shape {}x{}",
1931 result.nrows(),
1932 result.ncols(),
1933 m,
1934 n
1935 );
1936 ensure!(
1937 result.as_col_major_slice().len() == expected_len,
1938 "matrix multiplication returned {} values for expected shape {}x{}",
1939 result.as_col_major_slice().len(),
1940 m,
1941 n
1942 );
1943 Ok(result)
1944}
1945
1946macro_rules! impl_blas_mul {
1947 ($($t:ty),*) => {
1948 $(
1949 impl BlasMul for $t {
1950 fn blas_mat_mul(a: &Matrix<Self>, b: &Matrix<Self>) -> Result<Matrix<Self>> {
1951 let m = a.nrows();
1952 let k = a.ncols();
1953 let n = b.ncols();
1954 ensure!(
1955 b.nrows() == k,
1956 "matrix dimensions must agree for multiplication: left is {}x{}, right is {}x{}",
1957 m,
1958 k,
1959 b.nrows(),
1960 n
1961 );
1962 // Reject an overflowing output element count before any tensor
1963 // conversion or backend call, matching the constructor contract.
1964 let expected_len = m.checked_mul(n).ok_or_else(|| {
1965 anyhow::anyhow!(
1966 "matrix multiplication output shape {m}x{n} overflows usize"
1967 )
1968 })?;
1969
1970 let a_tensor: Tensor = a.to_typed_tensor().into();
1971 let b_tensor: Tensor = b.to_typed_tensor().into();
1972 dot_general_matrices::<$t>(a_tensor, b_tensor, m, n, expected_len)
1973 }
1974
1975 fn blas_mat_mul_owned(a: Matrix<Self>, b: Matrix<Self>) -> Result<Matrix<Self>> {
1976 let m = a.nrows();
1977 let k = a.ncols();
1978 let n = b.ncols();
1979 ensure!(
1980 b.nrows() == k,
1981 "matrix dimensions must agree for multiplication: left is {}x{}, right is {}x{}",
1982 m,
1983 k,
1984 b.nrows(),
1985 n
1986 );
1987 let expected_len = m.checked_mul(n).ok_or_else(|| {
1988 anyhow::anyhow!(
1989 "matrix multiplication output shape {m}x{n} overflows usize"
1990 )
1991 })?;
1992
1993 let a_tensor: Tensor = a.into_typed_tensor().into();
1994 let b_tensor: Tensor = b.into_typed_tensor().into();
1995 dot_general_matrices::<$t>(a_tensor, b_tensor, m, n, expected_len)
1996 }
1997 }
1998 )*
1999 };
2000}
2001
2002impl_blas_mul!(f64, f32, num_complex::Complex64, num_complex::Complex32);
2003
2004/// Scalar bound for dense backend matrix utilities.
2005///
2006/// This is the storage/linalg-layer scalar trait. Higher-level crates may
2007/// extend it with domain-specific methods, but matrix utilities only rely on
2008/// these algebraic operations and absolute-value comparisons.
2009pub trait MatrixScalar:
2010 Clone
2011 + Copy
2012 + Zero
2013 + One
2014 + std::ops::Add<Output = Self>
2015 + std::ops::Sub<Output = Self>
2016 + std::ops::Mul<Output = Self>
2017 + std::ops::Div<Output = Self>
2018 + std::ops::Neg<Output = Self>
2019 + Default
2020 + Send
2021 + Sync
2022 + BlasMul
2023 + 'static
2024{
2025 /// Squared absolute value as `f64`.
2026 fn matrix_abs_sq(self) -> f64;
2027}
2028
2029impl MatrixScalar for f64 {
2030 fn matrix_abs_sq(self) -> f64 {
2031 self * self
2032 }
2033}
2034
2035impl MatrixScalar for f32 {
2036 fn matrix_abs_sq(self) -> f64 {
2037 (self * self) as f64
2038 }
2039}
2040
2041impl MatrixScalar for Complex64 {
2042 fn matrix_abs_sq(self) -> f64 {
2043 self.norm_sqr()
2044 }
2045}
2046
2047impl MatrixScalar for Complex32 {
2048 fn matrix_abs_sq(self) -> f64 {
2049 self.norm_sqr() as f64
2050 }
2051}
2052
2053/// Matrix multiplication: A * B.
2054///
2055/// Uses BLAS-backed einsum via tenferro for high performance.
2056///
2057/// # Errors
2058///
2059/// Returns an error when the operation fails (a shape or index mismatch, or
2060/// /// a backend failure).
2061///
2062/// # Examples
2063///
2064/// ```
2065/// use tensor4all_tensorbackend::{from_vec2d, mat_mul};
2066///
2067/// let a = from_vec2d(vec![vec![1.0_f64, 2.0], vec![3.0, 4.0]]);
2068/// let b = from_vec2d(vec![vec![5.0, 6.0], vec![7.0, 8.0]]);
2069/// let c = mat_mul(&a, &b).unwrap();
2070/// assert!((c[[0, 0]] - 19.0).abs() < 1e-10);
2071/// assert!((c[[0, 1]] - 22.0).abs() < 1e-10);
2072/// assert!((c[[1, 0]] - 43.0).abs() < 1e-10);
2073/// assert!((c[[1, 1]] - 50.0).abs() < 1e-10);
2074/// ```
2075pub fn mat_mul<T: BlasMul>(a: &Matrix<T>, b: &Matrix<T>) -> Result<Matrix<T>, MatrixMulError> {
2076 T::blas_mat_mul(a, b).map_err(MatrixMulError::from)
2077}
2078
2079/// Matrix multiplication: consume `A` and `B`, returning `A * B`.
2080///
2081/// Uses BLAS-backed einsum via tenferro. Compared with [`mat_mul`], this
2082/// reuses the input matrix buffers when building tenferro tensors.
2083///
2084/// # Errors
2085///
2086/// Returns an error when the operation fails (a shape or index mismatch, or
2087/// /// a backend failure).
2088///
2089/// # Examples
2090///
2091/// ```
2092/// use tensor4all_tensorbackend::{from_vec2d, mat_mul_owned};
2093///
2094/// let a = from_vec2d(vec![vec![1.0_f64, 2.0], vec![3.0, 4.0]]);
2095/// let b = from_vec2d(vec![vec![5.0, 6.0], vec![7.0, 8.0]]);
2096/// let c = mat_mul_owned(a, b).unwrap();
2097/// assert_eq!(c.as_col_major_slice(), &[19.0, 43.0, 22.0, 50.0]);
2098/// ```
2099pub fn mat_mul_owned<T: BlasMul>(a: Matrix<T>, b: Matrix<T>) -> Result<Matrix<T>, MatrixMulError> {
2100 T::blas_mat_mul_owned(a, b).map_err(MatrixMulError::from)
2101}
2102
2103/// Batched matrix multiplication for column-major matrices with one shared shape.
2104///
2105/// Computes `C[p] = A[p] * B[p]` for `batch` matrices. Each `A[p]` is an
2106/// `m x k` column-major matrix and each `B[p]` is a `k x n` column-major
2107/// matrix. The input buffers store complete matrices consecutively, and the
2108/// returned buffer stores `batch` consecutive `m x n` column-major outputs.
2109///
2110/// # Errors
2111///
2112/// Returns an error if the input buffer lengths do not match the declared
2113/// shapes or if the backend rejects the batched GEMM.
2114///
2115/// # Examples
2116///
2117/// ```
2118/// use tensor4all_tensorbackend::batched_mat_mul_same_shape;
2119///
2120/// let a = vec![1.0_f64, 3.0, 2.0, 4.0];
2121/// let b = vec![5.0_f64, 7.0, 6.0, 8.0];
2122/// let out = batched_mat_mul_same_shape(1, 2, 2, 2, &a, &b).unwrap();
2123/// assert_eq!(out, vec![19.0, 43.0, 22.0, 50.0]);
2124/// ```
2125pub fn batched_mat_mul_same_shape<T>(
2126 batch: usize,
2127 m: usize,
2128 k: usize,
2129 n: usize,
2130 a: &[T],
2131 b: &[T],
2132) -> Result<Vec<T>, MatrixMulError>
2133where
2134 T: tenferro::TensorScalar + Copy,
2135{
2136 batched_mat_mul_same_shape_owned(batch, m, k, n, a.to_vec(), b.to_vec())
2137}
2138
2139/// Batched matrix multiplication while consuming column-major input buffers.
2140///
2141/// This is the owned-buffer counterpart of [`batched_mat_mul_same_shape`].
2142/// It avoids cloning the two input batches when callers have just built the
2143/// contiguous buffers for a backend call.
2144///
2145/// # Errors
2146///
2147/// Returns an error if the input buffer lengths do not match the declared
2148/// shapes or if the backend rejects the batched GEMM.
2149pub fn batched_mat_mul_same_shape_owned<T>(
2150 batch: usize,
2151 m: usize,
2152 k: usize,
2153 n: usize,
2154 a: Vec<T>,
2155 b: Vec<T>,
2156) -> Result<Vec<T>, MatrixMulError>
2157where
2158 T: tenferro::TensorScalar + Copy,
2159{
2160 validate_batched_mat_mul_inputs(batch, m, k, n, a.len(), b.len())?;
2161
2162 let a_tensor = T::into_tensor(vec![m, k, batch], a)
2163 .map_err(|error| MatrixMulError::from(anyhow::Error::new(error)))?;
2164 let b_tensor = T::into_tensor(vec![k, n, batch], b)
2165 .map_err(|error| MatrixMulError::from(anyhow::Error::new(error)))?;
2166 // TensorSessionOpsExt exposes rank-2 matmul but not arbitrary batched dot.
2167 // Keep one backend execution by expressing [m,k,b] × [k,n,b] as einsum.
2168 let c = crate::tenferro_bridge::einsum_native_tensors_owned(
2169 vec![(a_tensor, vec![0, 1, 2]), (b_tensor, vec![1, 3, 2])],
2170 &[0, 3, 2],
2171 )
2172 .context("batched matrix multiplication failed")?;
2173 let c = T::into_typed(c).map_err(|error| {
2174 MatrixMulError::from(anyhow::anyhow!(
2175 "batched matrix multiplication returned wrong dtype: {error}"
2176 ))
2177 })?;
2178 let (_shape, data) = c
2179 .into_vec_col_major()
2180 .map_err(|error| MatrixMulError::from(anyhow::Error::new(error)))?;
2181 let expected_len = batch
2182 .checked_mul(m)
2183 .and_then(|value| value.checked_mul(n))
2184 .ok_or_else(|| {
2185 MatrixMulError::from(anyhow::anyhow!(
2186 "batched matrix multiplication output shape overflows"
2187 ))
2188 })?;
2189 if data.len() != expected_len {
2190 return Err(MatrixMulError::from(anyhow::anyhow!(
2191 "batched matrix multiplication returned {} values for expected shape {}x{}x{}",
2192 data.len(),
2193 m,
2194 n,
2195 batch
2196 )));
2197 }
2198 Ok(data)
2199}
2200
2201fn validate_batched_mat_mul_inputs(
2202 batch: usize,
2203 m: usize,
2204 k: usize,
2205 n: usize,
2206 a_len: usize,
2207 b_len: usize,
2208) -> Result<()> {
2209 let expected_a_len = batch
2210 .checked_mul(m)
2211 .and_then(|value| value.checked_mul(k))
2212 .ok_or_else(|| anyhow::anyhow!("batched matrix multiplication left shape overflows"))?;
2213 let expected_b_len = batch
2214 .checked_mul(k)
2215 .and_then(|value| value.checked_mul(n))
2216 .ok_or_else(|| anyhow::anyhow!("batched matrix multiplication right shape overflows"))?;
2217 ensure!(
2218 a_len == expected_a_len,
2219 "batched matrix multiplication left buffer has length {}, expected {}",
2220 a_len,
2221 expected_a_len
2222 );
2223 ensure!(
2224 b_len == expected_b_len,
2225 "batched matrix multiplication right buffer has length {}, expected {}",
2226 b_len,
2227 expected_b_len
2228 );
2229 Ok(())
2230}
2231
2232#[cfg(test)]
2233mod tests;