Skip to main content

tensor4all_tensorbackend/
matrix.rs

1//! Dense column-major matrix type and utility functions.
2//!
3//! [`Matrix<T>`] is a simple dense 2D matrix in column-major layout, indexed
4//! by `m[[row, col]]`. It is the shared dense matrix boundary for tensor4all
5//! crates that need flat buffers and backend-backed matrix multiplication.
6//!
7//! # Examples
8//!
9//! ```
10//! use tensor4all_tensorbackend::{from_vec2d, Matrix};
11//!
12//! let m = from_vec2d(vec![
13//!     vec![1.0_f64, 2.0],
14//!     vec![3.0, 4.0],
15//! ]);
16//! assert_eq!(m.nrows(), 2);
17//! assert_eq!(m.ncols(), 2);
18//! assert_eq!(m[[0, 1]], 2.0);
19//! assert_eq!(m[[1, 0]], 3.0);
20//! ```
21
22use anyhow::{ensure, Context, Result};
23use num_complex::{Complex32, Complex64};
24use num_traits::{One, Zero};
25use std::ops::{Index, IndexMut};
26use tenferro::{DType, Tensor, TensorScalar, TypedTensor};
27use tenferro_ad::EagerTensor;
28use tenferro_linalg::EagerTensorLinalgExt;
29
30/// A dense 2D matrix in column-major layout.
31///
32/// Access elements with `m[[row, col]]` syntax. Data is stored contiguously
33/// in column-major order, so flat buffers use `row + nrows * col`.
34///
35/// # Examples
36///
37/// ```
38/// use tensor4all_tensorbackend::Matrix;
39///
40/// let mut m = Matrix::zeros(2, 3);
41/// m[[0, 1]] = 5.0_f64;
42/// assert_eq!(m[[0, 1]], 5.0);
43/// assert_eq!(m[[0, 0]], 0.0);
44/// assert_eq!(m.nrows(), 2);
45/// assert_eq!(m.ncols(), 3);
46/// ```
47#[derive(Debug, Clone)]
48pub struct Matrix<T> {
49    data: Vec<T>,
50    nrows: usize,
51    ncols: usize,
52}
53
54fn checked_matrix_len(nrows: usize, ncols: usize) -> Option<usize> {
55    nrows.checked_mul(ncols)
56}
57
58/// Error returned when converting a [`TypedTensor`] into a [`Matrix`].
59///
60/// Use this when accepting dynamic tensor-shaped values at a dense-matrix
61/// boundary. It reports whether conversion failed because the tensor was not a
62/// rank-2 matrix or because its host buffer could not be consumed.
63///
64/// # Examples
65///
66/// ```
67/// use tenferro::TypedTensor;
68/// use tensor4all_tensorbackend::Matrix;
69/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
70///
71/// let tensor = TypedTensor::from_vec_col_major(vec![2, 1, 1], vec![1.0_f64, 2.0])?;
72/// let err = Matrix::try_from_typed_tensor(tensor).unwrap_err();
73/// assert!(err.to_string().contains("rank-2 tensor"));
74/// # Ok(())
75/// # }
76/// ```
77#[derive(Debug, thiserror::Error)]
78pub enum MatrixTensorConversionError {
79    /// The input tensor rank was not two.
80    #[error("expected a rank-2 tensor, got shape {shape:?}")]
81    Rank {
82        /// Tensor shape that failed the rank check.
83        shape: Vec<usize>,
84    },
85    /// The tensor did not contain an owned host buffer that can be consumed.
86    #[error("failed to consume typed tensor host buffer: {message}")]
87    HostBuffer {
88        /// Backend conversion error reported by tenferro.
89        message: String,
90    },
91}
92
93/// Error returned when matrix shape or index validation fails.
94///
95/// Constructors use this type for malformed dimensions or payloads. In-place
96/// mutation helpers use it to reject caller-supplied indices before changing
97/// any matrix values.
98///
99/// # Examples
100///
101/// ```
102/// use tensor4all_tensorbackend::{try_from_vec2d, MatrixShapeError};
103///
104/// let err = try_from_vec2d(vec![vec![1.0_f64, 2.0], vec![3.0]]).unwrap_err();
105/// assert!(matches!(
106///     err,
107///     MatrixShapeError::RaggedRows {
108///         row: 1,
109///         expected: 2,
110///         actual: 1,
111///     }
112/// ));
113/// ```
114#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
115pub enum MatrixShapeError {
116    /// A later row had a different length than the first row.
117    #[error("row {row} has length {actual}, expected {expected}")]
118    RaggedRows {
119        /// Zero-based row number with the mismatched length.
120        row: usize,
121        /// Column count established by the first row.
122        expected: usize,
123        /// Actual number of entries in `row`.
124        actual: usize,
125    },
126    /// The matrix element count overflowed `usize`.
127    #[error("matrix shape {nrows}x{ncols} overflows usize")]
128    ShapeOverflow {
129        /// Number of rows.
130        nrows: usize,
131        /// Number of columns.
132        ncols: usize,
133    },
134    /// The requested row index is outside the matrix.
135    #[error("row index {index} is out of bounds for {nrows} rows")]
136    RowIndexOutOfBounds {
137        /// Rejected zero-based row index.
138        index: usize,
139        /// Number of rows in the matrix.
140        nrows: usize,
141    },
142    /// The requested column index is outside the matrix.
143    #[error("column index {index} is out of bounds for {ncols} columns")]
144    ColumnIndexOutOfBounds {
145        /// Rejected zero-based column index.
146        index: usize,
147        /// Number of columns in the matrix.
148        ncols: usize,
149    },
150    /// The flat data length did not match the matrix shape.
151    #[error("matrix data has length {actual}, expected {expected}")]
152    DataLengthMismatch {
153        /// Number of supplied elements.
154        actual: usize,
155        /// Number of elements implied by the shape.
156        expected: usize,
157    },
158}
159
160/// Error returned by matrix multiplication entry points.
161///
162/// Wraps the backend/einsum diagnostic, preserving its source chain.
163#[derive(Debug, thiserror::Error)]
164#[error("matrix multiplication failed: {source}")]
165pub struct MatrixMulError {
166    /// Original backend or einsum diagnostic.
167    #[source]
168    pub source: anyhow::Error,
169}
170
171impl From<anyhow::Error> for MatrixMulError {
172    fn from(source: anyhow::Error) -> Self {
173        Self { source }
174    }
175}
176
177/// 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;