Skip to main content

tenferro_linalg/
backend.rs

1use std::ops::Range;
2
3use tenferro_tensor::{BackendSession, Tensor, TensorRead, TensorWrite};
4
5/// Compact provider-neutral Householder QR payload used by backend hooks.
6#[doc(hidden)]
7#[derive(Debug)]
8pub struct CompactQrResult {
9    pub(crate) packed: Tensor,
10    pub(crate) coeff: Tensor,
11}
12
13pub(crate) use crate::error::unsupported_dtype;
14use crate::extension::{
15    apply_eigh_gauge, apply_qr_gauge, apply_svd_gauge, validate_derivative_eps, EighOptions,
16    QrOptions, SvdOptions,
17};
18use crate::RankRevealingQrOptions;
19
20/// Backend surface required by the linalg extension runtime.
21///
22/// # Examples
23///
24/// ```rust
25/// use tenferro_cpu::{with_cpu_exec_session, CpuBackend, CpuExecSession};
26/// use tenferro_linalg::backend::LinalgBackend;
27/// use tenferro_tensor::BackendSessionHost;
28///
29/// fn assert_linalg_backend<B: LinalgBackend>() {}
30///
31/// assert_linalg_backend::<CpuExecSession<'static>>();
32/// let mut host = CpuBackend::new();
33/// host.with_backend_session(|session| {
34///     with_cpu_exec_session(session, |_backend| ())
35///         .expect("CpuBackend must expose a CpuExecSession");
36/// });
37/// ```
38pub trait LinalgBackend: BackendSession {
39    /// Compute a Cholesky factorization.
40    ///
41    /// # Errors
42    ///
43    /// Returns `Error::Validation` for non-matrix, non-square, or unsupported
44    /// input dtypes; `Error::Extension` with `ErrorKind::NumericalFailure`
45    /// when the matrix is not positive definite; or a typed backend source
46    /// when the provider cannot execute the factorization.
47    fn cholesky(&mut self, input: &Tensor) -> tenferro_tensor::Result<Tensor>;
48
49    /// Solve a triangular linear system with explicit side, triangle,
50    /// transpose, and unit-diagonal flags.
51    ///
52    /// # Errors
53    ///
54    /// Returns `Error::Validation` for incompatible matrix/rhs shapes, rank,
55    /// or dtype; `Error::Extension` with `ErrorKind::NumericalFailure` for a
56    /// singular or zero-diagonal system; or a typed backend source for a
57    /// provider failure.
58    fn triangular_solve(
59        &mut self,
60        a: &Tensor,
61        b: &Tensor,
62        left_side: bool,
63        lower: bool,
64        transpose_a: bool,
65        unit_diagonal: bool,
66    ) -> tenferro_tensor::Result<Tensor>;
67
68    // INVARIANT: the six flags are the public triangular-solve contract and mirror the owned hook.
69    #[allow(clippy::too_many_arguments)]
70    /// Solve a triangular linear system from tensor read targets.
71    ///
72    /// Backends may canonicalize the inputs inside the same placement family,
73    /// but must not silently transfer between CPU and GPU memory.
74    ///
75    /// # Examples
76    ///
77    /// ```rust
78    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
79    /// use tenferro_linalg::LinalgBackend;
80    /// use tenferro_tensor::{BackendSessionHost, Tensor, TensorRead};
81    ///
82    /// let a = Tensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 1.0, 3.0])?;
83    /// let b = Tensor::from_vec_col_major(vec![2, 1], vec![4.0_f64, 9.0])?;
84    /// let mut host = CpuBackend::new();
85    /// let x = host.with_backend_session(|session| {
86    ///     with_cpu_exec_session(session, |backend| {
87    ///         backend.triangular_solve_read(
88    ///             TensorRead::from_tensor(&a),
89    ///             TensorRead::from_tensor(&b),
90    ///             true,
91    ///             false,
92    ///             false,
93    ///             false,
94    ///         )
95    ///     })
96    ///     .expect("CpuBackend must expose a CpuExecSession")
97    /// })?;
98    /// let Tensor::F64(x) = x else { unreachable!("F64 inputs return F64 output") };
99    /// assert_eq!(x.host_data()?, &[0.5, 3.0]);
100    /// # Ok::<(), tenferro_tensor::Error>(())
101    /// ```
102    ///
103    /// # Errors
104    ///
105    /// The default implementation returns `Error::Unsupported` because the
106    /// backend does not accept tensor read targets. Implementations may return
107    /// `Error::Validation` for incompatible shapes or dtypes,
108    /// `Error::RuntimeState` for invalid placement, `Error::Extension` for a
109    /// singular system, or a typed backend-source error.
110    fn triangular_solve_read(
111        &mut self,
112        _a: TensorRead<'_>,
113        _b: TensorRead<'_>,
114        _left_side: bool,
115        _lower: bool,
116        _transpose_a: bool,
117        _unit_diagonal: bool,
118    ) -> tenferro_tensor::Result<Tensor> {
119        Err(tenferro_tensor::Error::unsupported(
120            "triangular_solve",
121            "backend does not accept tensor reads at this execution boundary",
122        ))
123    }
124
125    /// Compute public LU outputs `(P, L, U, parity)`.
126    ///
127    /// # Errors
128    ///
129    /// Returns `Error::Validation` when the input is not a supported matrix or
130    /// dtype, and `Error::Extension` or a typed backend source when LU
131    /// execution or pivot storage fails.
132    fn lu(&mut self, input: &Tensor) -> tenferro_tensor::Result<Vec<Tensor>>;
133
134    #[doc(hidden)]
135    fn lu_factor(&mut self, _input: &Tensor) -> tenferro_tensor::Result<Vec<Tensor>> {
136        Err(tenferro_tensor::Error::unsupported(
137            "lu_factor",
138            format!(
139                "backend {} does not implement internal packed LU factorization",
140                std::any::type_name::<Self>()
141            ),
142        ))
143    }
144
145    /// Compute complete-pivot LU outputs `(P, L, U, Q, parity)`.
146    ///
147    /// The reconstruction convention is `A = P^T * L * U * Q`, equivalently
148    /// `P * A * Q^T = L * U`. `parity` is a scalar real tensor containing
149    /// `+1` or `-1`: `F32` for `F32`/`C32` inputs and `F64` for `F64`/`C64`
150    /// inputs.
151    ///
152    /// # Errors
153    ///
154    /// Returns `Error::Validation` for an invalid rank, square-shape
155    /// requirement, or dtype, and `Error::Extension` or a typed backend source
156    /// when complete-pivot factorization cannot be executed.
157    fn full_piv_lu(&mut self, input: &Tensor) -> tenferro_tensor::Result<Vec<Tensor>>;
158
159    /// Solve a linear system through the complete-pivot LU path.
160    ///
161    /// With `transpose_a = false`, this solves `A * x = b`. With
162    /// `transpose_a = true`, this solves `A^T * x = b`.
163    ///
164    /// # Errors
165    ///
166    /// Returns `Error::Validation` for incompatible coefficient/rhs shapes or
167    /// dtypes, `Error::Extension` with `ErrorKind::NumericalFailure` for a
168    /// singular system, or a typed backend source for provider failure.
169    fn full_piv_lu_solve(
170        &mut self,
171        a: &Tensor,
172        b: &Tensor,
173        transpose_a: bool,
174    ) -> tenferro_tensor::Result<Tensor>;
175
176    /// Compute public SVD outputs `(U, S, Vt)`.
177    ///
178    /// # Errors
179    ///
180    /// Returns `Error::Validation` for an unsupported rank or dtype and a
181    /// typed `Error::Extension` or backend source when the solver fails.
182    fn svd(&mut self, input: &Tensor) -> tenferro_tensor::Result<Vec<Tensor>>;
183
184    /// Compute public SVD outputs `(U, S, Vt)` with explicit options.
185    ///
186    /// `derivative_eps` is validated for API consistency, but concrete backend
187    /// execution does not perform AD. `gauge` controls optional singular-vector
188    /// post-processing.
189    ///
190    /// # Examples
191    ///
192    /// ```rust
193    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
194    /// use tenferro_linalg::{LinalgBackend, SvdGauge, SvdOptions};
195    /// use tenferro_tensor::{BackendSessionHost, Tensor};
196    ///
197    /// let input = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 2.0])?;
198    /// let mut host = CpuBackend::new();
199    /// let outputs = host.with_backend_session(|session| {
200    ///     with_cpu_exec_session(session, |backend| {
201    ///         backend.svd_with_options(
202    ///             &input,
203    ///             SvdOptions::default().gauge(SvdGauge::CanonicalPivot),
204    ///         )
205    ///     })
206    ///     .expect("CpuBackend must expose a CpuExecSession")
207    /// })?;
208    /// assert_eq!(outputs[1].shape(), &[2]);
209    /// # Ok::<(), tenferro_tensor::Error>(())
210    /// ```
211    ///
212    /// # Errors
213    ///
214    /// Returns [`tenferro_tensor::Error::Validation`] containing
215    /// [`tenferro_tensor::ValidationError::InvalidArgument`] when
216    /// `derivative_eps` is non-finite or non-positive, or when canonical gauge
217    /// output metadata is malformed. It can return
218    /// [`tenferro_tensor::Error::Validation`] with
219    /// [`tenferro_tensor::ValidationError::RankMismatch`],
220    /// [`tenferro_tensor::ValidationError::ShapeMismatch`], or
221    /// [`tenferro_tensor::ValidationError::DTypeMismatch`] for the input
222    /// or generated outputs, [`tenferro_tensor::Error::Extension`] with the
223    /// typed `tenferro_linalg::Error::UnsupportedDType` or
224    /// `NonConvergence` source, [`tenferro_tensor::Error::BackendSource`] for
225    /// provider calls, and [`tenferro_tensor::Error::RuntimeState`] for
226    /// placement failures. A CPU provider that was not compiled is reported
227    /// as [`tenferro_tensor::ValidationError::InvalidArgument`] on the
228    /// provider configuration.
229    fn svd_with_options(
230        &mut self,
231        input: &Tensor,
232        options: SvdOptions,
233    ) -> tenferro_tensor::Result<Vec<Tensor>> {
234        validate_derivative_eps("svd_with_options", options.derivative_eps)?;
235        let mut outputs = self.svd(input)?;
236        apply_svd_gauge(options.gauge, &mut outputs)?;
237        Ok(outputs)
238    }
239
240    /// Compute public full-matrices SVD outputs `(U, S, Vt)` with `U` shaped
241    /// `m x m` and `Vt` shaped `n x n`, so the trailing `Vt` rows span the
242    /// input's right nullspace.
243    ///
244    /// # Errors
245    ///
246    /// The default implementation returns `Error::Unsupported`: a backend that
247    /// does not implement the full variant reports it explicitly rather than
248    /// silently falling back to the thin decomposition. Implementing backends
249    /// may additionally return `Error::Validation` for an unsupported rank or
250    /// dtype and a typed backend source when the solver fails.
251    fn svd_full(&mut self, _input: &Tensor) -> tenferro_tensor::Result<Vec<Tensor>> {
252        Err(tenferro_tensor::Error::unsupported(
253            "svd_full",
254            format!(
255                "backend {} does not implement full-matrices SVD",
256                std::any::type_name::<Self>()
257            ),
258        ))
259    }
260
261    #[doc(hidden)]
262    fn svd_values(&mut self, _input: &Tensor) -> tenferro_tensor::Result<Tensor> {
263        Err(tenferro_tensor::Error::unsupported(
264            "svd_values",
265            format!(
266                "backend {} does not implement internal singular-values-only decomposition",
267                std::any::type_name::<Self>()
268            ),
269        ))
270    }
271
272    /// Compute a singular value decomposition from a tensor read target.
273    ///
274    /// Backends may canonicalize the input inside the same placement family, but
275    /// must not silently transfer between CPU and GPU memory.
276    ///
277    /// # Examples
278    ///
279    /// ```rust
280    /// use tenferro_linalg::LinalgBackend;
281    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
282    /// use tenferro_tensor::{BackendSessionHost, TensorRead, TensorView, TypedTensor};
283    ///
284    /// let input = TypedTensor::<f64>::from_vec_col_major(
285    ///     vec![2, 2],
286    ///     vec![1.0, 0.0, 0.0, 2.0],
287    /// )?;
288    /// let mut host = CpuBackend::new();
289    /// let outputs = host.with_backend_session(|session| {
290    ///     with_cpu_exec_session(session, |backend| {
291    ///         backend.svd_read(TensorRead::from_view(TensorView::F64(input.as_view())))
292    ///     })
293    ///     .expect("CpuBackend must expose a CpuExecSession")
294    /// })?;
295    /// assert_eq!(outputs[1].shape(), &[2]);
296    /// # Ok::<(), tenferro_tensor::Error>(())
297    /// ```
298    ///
299    /// # Errors
300    ///
301    /// The default implementation returns `Error::Unsupported` because the
302    /// backend does not accept tensor read targets; an implementation may instead
303    /// return validation or typed backend-source errors after canonicalizing
304    /// the view.
305    fn svd_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Vec<Tensor>> {
306        Err(tenferro_tensor::Error::unsupported(
307            "svd",
308            "backend does not accept tensor reads at this execution boundary",
309        ))
310    }
311
312    #[doc(hidden)]
313    fn svd_values_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Tensor> {
314        Err(tenferro_tensor::Error::unsupported(
315            "svd_values",
316            "backend does not implement borrowed singular-values-only decomposition",
317        ))
318    }
319
320    /// Compute public QR outputs `(Q, R)`.
321    ///
322    /// QR is thin: for an `m x n` input, `Q` has shape `m x min(m, n)` and
323    /// `R` has shape `min(m, n) x n`.
324    ///
325    /// # Errors
326    ///
327    /// Returns `Error::Validation` for an unsupported rank, shape, or dtype,
328    /// and a typed `Error::Extension` or backend source when QR execution
329    /// fails.
330    fn qr(&mut self, input: &Tensor) -> tenferro_tensor::Result<Vec<Tensor>>;
331
332    /// Compute public QR outputs `(Q, R)` with explicit options.
333    ///
334    /// `gauge` controls optional sign or phase post-processing.
335    ///
336    /// # Examples
337    ///
338    /// ```rust
339    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
340    /// use tenferro_linalg::{LinalgBackend, QrGauge, QrOptions};
341    /// use tenferro_tensor::{BackendSessionHost, Tensor};
342    ///
343    /// let input = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 2.0])?;
344    /// let mut host = CpuBackend::new();
345    /// let outputs = host.with_backend_session(|session| {
346    ///     with_cpu_exec_session(session, |backend| {
347    ///         backend.qr_with_options(
348    ///             &input,
349    ///             QrOptions::default().gauge(QrGauge::PositiveDiagonal),
350    ///         )
351    ///     })
352    ///     .expect("CpuBackend must expose a CpuExecSession")
353    /// })?;
354    /// assert_eq!(outputs[0].shape(), &[2, 2]);
355    /// # Ok::<(), tenferro_tensor::Error>(())
356    /// ```
357    ///
358    /// # Errors
359    ///
360    /// Returns [`tenferro_tensor::Error::Validation`] containing
361    /// [`tenferro_tensor::ValidationError::RankMismatch`] or
362    /// [`tenferro_tensor::ValidationError::ShapeMismatch`] for an invalid
363    /// matrix input, or [`tenferro_tensor::ValidationError::InvalidArgument`]
364    /// for malformed gauge output metadata, checked size arithmetic, or an
365    /// unavailable compiled provider. A mismatched generated `Q`/`R` dtype is reported as
366    /// [`tenferro_tensor::ValidationError::DTypeMismatch`]. Provider
367    /// unsupported dtype or numerical rejection is
368    /// [`tenferro_tensor::Error::Extension`] with a typed linalg source, while
369    /// provider failures use [`tenferro_tensor::Error::BackendSource`] and a
370    /// backend-resident input uses [`tenferro_tensor::Error::RuntimeState`].
371    fn qr_with_options(
372        &mut self,
373        input: &Tensor,
374        options: QrOptions,
375    ) -> tenferro_tensor::Result<Vec<Tensor>> {
376        let mut outputs = self.qr(input)?;
377        apply_qr_gauge(options.gauge, &mut outputs)?;
378        Ok(outputs)
379    }
380
381    /// Compute public QR outputs `(Q, R)` from a tensor read target.
382    ///
383    /// Backends may canonicalize the input inside the same placement family, but
384    /// must not silently transfer between CPU and GPU memory.
385    ///
386    /// # Examples
387    ///
388    /// ```rust
389    /// use tenferro_linalg::LinalgBackend;
390    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
391    /// use tenferro_tensor::{BackendSessionHost, TensorRead, TensorView, TypedTensor};
392    ///
393    /// let input = TypedTensor::<f64>::from_vec_col_major(
394    ///     vec![2, 2],
395    ///     vec![1.0, 0.0, 0.0, 2.0],
396    /// )?;
397    /// let mut host = CpuBackend::new();
398    /// let outputs = host.with_backend_session(|session| {
399    ///     with_cpu_exec_session(session, |backend| {
400    ///         backend.qr_read(TensorRead::from_view(TensorView::F64(input.as_view())))
401    ///     })
402    ///     .expect("CpuBackend must expose a CpuExecSession")
403    /// })?;
404    /// assert_eq!(outputs[0].shape(), &[2, 2]);
405    /// assert_eq!(outputs[1].shape(), &[2, 2]);
406    /// # Ok::<(), tenferro_tensor::Error>(())
407    /// ```
408    ///
409    /// # Errors
410    ///
411    /// The default implementation returns `Error::Unsupported` because the
412    /// backend does not accept tensor read targets; implementations may return
413    /// validation or typed backend-source errors.
414    fn qr_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Vec<Tensor>> {
415        Err(tenferro_tensor::Error::unsupported(
416            "qr",
417            "backend does not accept tensor reads at this execution boundary",
418        ))
419    }
420
421    /// Compute public QR outputs `(Q, R)` from a tensor read target with options.
422    ///
423    /// Device backends override this hook to keep gauge processing in the input
424    /// placement. The default is appropriate for host backends.
425    ///
426    /// # Examples
427    ///
428    /// ```rust
429    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
430    /// use tenferro_linalg::{LinalgBackend, QrGauge, QrOptions};
431    /// use tenferro_tensor::{BackendSessionHost, Tensor, TensorRead};
432    ///
433    /// let input = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 2.0])?;
434    /// let mut host = CpuBackend::new();
435    /// let outputs = host.with_backend_session(|session| {
436    ///     with_cpu_exec_session(session, |backend| {
437    ///         backend.qr_with_options_read(
438    ///             TensorRead::from_tensor(&input),
439    ///             QrOptions::default().gauge(QrGauge::PositiveDiagonal),
440    ///         )
441    ///     })
442    ///     .expect("CpuBackend must expose a CpuExecSession")
443    /// })?;
444    /// assert_eq!(outputs[0].shape(), &[2, 2]);
445    /// # Ok::<(), tenferro_tensor::Error>(())
446    /// ```
447    ///
448    /// # Errors
449    ///
450    /// Returns the validation, unsupported-dtype, numerical, placement, or
451    /// typed backend/provider errors from [`LinalgBackend::qr_read`], plus
452    /// gauge metadata and host-access errors from the default host gauge path.
453    fn qr_with_options_read(
454        &mut self,
455        input: TensorRead<'_>,
456        options: QrOptions,
457    ) -> tenferro_tensor::Result<Vec<Tensor>> {
458        let mut outputs = self.qr_read(input)?;
459        apply_qr_gauge(options.gauge, &mut outputs)?;
460        Ok(outputs)
461    }
462
463    #[doc(hidden)]
464    fn rank_revealing_qr(
465        &mut self,
466        _input: &Tensor,
467        _options: RankRevealingQrOptions,
468    ) -> tenferro_tensor::Result<Vec<Tensor>> {
469        Err(tenferro_tensor::Error::unsupported(
470            "rank_revealing_qr",
471            "backend does not implement rank-revealing QR",
472        ))
473    }
474
475    #[doc(hidden)]
476    fn rank_revealing_qr_read(
477        &mut self,
478        _input: TensorRead<'_>,
479        _options: RankRevealingQrOptions,
480    ) -> tenferro_tensor::Result<Vec<Tensor>> {
481        Err(tenferro_tensor::Error::unsupported(
482            "rank_revealing_qr",
483            "backend does not implement borrowed rank-revealing QR",
484        ))
485    }
486
487    #[doc(hidden)]
488    fn householder_qr(&mut self, _input: &Tensor) -> tenferro_tensor::Result<CompactQrResult> {
489        Err(tenferro_tensor::Error::unsupported(
490            "householder_qr",
491            "backend does not implement compact Householder QR",
492        ))
493    }
494
495    #[doc(hidden)]
496    fn householder_qr_from_factors(
497        &mut self,
498        _q: &Tensor,
499        _r: &Tensor,
500    ) -> tenferro_tensor::Result<CompactQrResult> {
501        Err(tenferro_tensor::Error::unsupported(
502            "householder_qr_from_factors",
503            "backend does not implement compact Householder QR factor import",
504        ))
505    }
506
507    #[doc(hidden)]
508    fn householder_qr_append(
509        &mut self,
510        _packed: &Tensor,
511        _coeff: &Tensor,
512        _block: &Tensor,
513    ) -> tenferro_tensor::Result<CompactQrResult> {
514        Err(tenferro_tensor::Error::unsupported(
515            "householder_qr_append",
516            "backend does not implement compact Householder QR append",
517        ))
518    }
519
520    #[doc(hidden)]
521    fn householder_qr_r(
522        &mut self,
523        _packed: &Tensor,
524        _coeff: &Tensor,
525        _options: QrOptions,
526    ) -> tenferro_tensor::Result<Tensor> {
527        Err(tenferro_tensor::Error::unsupported(
528            "householder_qr_r",
529            "backend does not implement compact Householder QR extraction",
530        ))
531    }
532
533    #[doc(hidden)]
534    fn householder_qr_q_columns(
535        &mut self,
536        _packed: &Tensor,
537        _coeff: &Tensor,
538        _columns: Range<usize>,
539        _options: QrOptions,
540    ) -> tenferro_tensor::Result<Tensor> {
541        Err(tenferro_tensor::Error::unsupported(
542            "householder_qr_q_columns",
543            "backend does not implement compact Householder Q-column materialization",
544        ))
545    }
546
547    /// Compute public Hermitian eigendecomposition outputs `(values, vectors)`.
548    ///
549    /// The returned vector order is `[values, vectors]`, where `values` has
550    /// shape `[n]` and `vectors` has shape `[n, n]`.
551    ///
552    /// # Errors
553    ///
554    /// Returns `Error::Validation` for a non-square or unsupported-dtype input
555    /// and a typed `Error::Extension` or backend source when eigendecomposition
556    /// fails.
557    fn eigh(&mut self, input: &Tensor) -> tenferro_tensor::Result<Vec<Tensor>>;
558
559    /// Compute public Hermitian eigendecomposition outputs with explicit options.
560    ///
561    /// `derivative_eps` is validated for API consistency, but concrete backend
562    /// execution does not perform AD. `gauge` controls optional eigenvector
563    /// post-processing.
564    ///
565    /// # Examples
566    ///
567    /// ```rust
568    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
569    /// use tenferro_linalg::{EighGauge, EighOptions, LinalgBackend};
570    /// use tenferro_tensor::{BackendSessionHost, Tensor};
571    ///
572    /// let input = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 2.0])?;
573    /// let mut host = CpuBackend::new();
574    /// let outputs = host.with_backend_session(|session| {
575    ///     with_cpu_exec_session(session, |backend| {
576    ///         backend.eigh_with_options(
577    ///             &input,
578    ///             EighOptions::default()
579    ///                 .gauge(EighGauge::CanonicalPivot)
580    ///                 .derivative_eps(1.0e-10),
581    ///         )
582    ///     })
583    ///     .expect("CpuBackend must expose a CpuExecSession")
584    /// })?;
585    /// assert_eq!(outputs[0].shape(), &[2]);
586    /// # Ok::<(), tenferro_tensor::Error>(())
587    /// ```
588    ///
589    /// # Errors
590    ///
591    /// Returns [`tenferro_tensor::Error::Validation`] containing
592    /// [`tenferro_tensor::ValidationError::InvalidArgument`] when
593    /// `derivative_eps` is non-finite or non-positive, when canonical gauge
594    /// output metadata is malformed, or when checked output-size arithmetic
595    /// overflows. It can return [`tenferro_tensor::Error::Validation`] with
596    /// [`tenferro_tensor::ValidationError::RankMismatch`] or
597    /// [`tenferro_tensor::ValidationError::ShapeMismatch`] for the
598    /// matrix input, or [`tenferro_tensor::ValidationError::DTypeMismatch`]
599    /// for generated outputs. It can also return
600    /// [`tenferro_tensor::Error::Extension`] with typed
601    /// `tenferro_linalg::Error::UnsupportedDType` or `NonConvergence`, and
602    /// [`tenferro_tensor::Error::BackendSource`] or
603    /// [`tenferro_tensor::Error::RuntimeState`] for provider and placement
604    /// failures.
605    fn eigh_with_options(
606        &mut self,
607        input: &Tensor,
608        options: EighOptions,
609    ) -> tenferro_tensor::Result<Vec<Tensor>> {
610        validate_derivative_eps("eigh_with_options", options.derivative_eps)?;
611        let mut outputs = self.eigh(input)?;
612        apply_eigh_gauge(options.gauge, &mut outputs)?;
613        Ok(outputs)
614    }
615
616    /// Compute public Hermitian eigendecomposition outputs from a tensor read target.
617    ///
618    /// Backends may canonicalize the input inside the same placement family, but
619    /// must not silently transfer between CPU and GPU memory.
620    ///
621    /// # Examples
622    ///
623    /// ```rust
624    /// use tenferro_linalg::LinalgBackend;
625    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
626    /// use tenferro_tensor::{BackendSessionHost, TensorRead, TensorView, TypedTensor};
627    ///
628    /// let input = TypedTensor::<f64>::from_vec_col_major(
629    ///     vec![2, 2],
630    ///     vec![1.0, 0.0, 0.0, 2.0],
631    /// )?;
632    /// let mut host = CpuBackend::new();
633    /// let outputs = host.with_backend_session(|session| {
634    ///     with_cpu_exec_session(session, |backend| {
635    ///         backend.eigh_read(TensorRead::from_view(TensorView::F64(input.as_view())))
636    ///     })
637    ///     .expect("CpuBackend must expose a CpuExecSession")
638    /// })?;
639    /// assert_eq!(outputs[0].shape(), &[2]);
640    /// assert_eq!(outputs[1].shape(), &[2, 2]);
641    /// # Ok::<(), tenferro_tensor::Error>(())
642    /// ```
643    ///
644    /// # Errors
645    ///
646    /// The default implementation returns `Error::Unsupported` because the
647    /// backend does not accept tensor read targets; implementations may return
648    /// validation or typed backend-source errors.
649    fn eigh_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Vec<Tensor>> {
650        Err(tenferro_tensor::Error::unsupported(
651            "eigh",
652            "backend does not accept tensor reads at this execution boundary",
653        ))
654    }
655
656    /// Compute Cholesky factorization from a tensor read target.
657    ///
658    /// Backends may canonicalize the input inside the same placement family, but
659    /// must not silently transfer between CPU and GPU memory.
660    ///
661    /// # Examples
662    ///
663    /// ```rust
664    /// use tenferro_linalg::LinalgBackend;
665    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
666    /// use tenferro_tensor::{BackendSessionHost, TensorRead, TensorView, TypedTensor};
667    ///
668    /// let input = TypedTensor::<f64>::from_vec_col_major(
669    ///     vec![2, 2],
670    ///     vec![4.0, 2.0, 2.0, 3.0],
671    /// )?;
672    /// let mut host = CpuBackend::new();
673    /// let output = host.with_backend_session(|session| {
674    ///     with_cpu_exec_session(session, |backend| {
675    ///         backend.cholesky_read(TensorRead::from_view(TensorView::F64(input.as_view())))
676    ///     })
677    ///     .expect("CpuBackend must expose a CpuExecSession")
678    /// })?;
679    /// assert_eq!(output.shape(), &[2, 2]);
680    /// # Ok::<(), tenferro_tensor::Error>(())
681    /// ```
682    ///
683    /// # Errors
684    ///
685    /// The default implementation returns `Error::Unsupported` because the
686    /// backend does not accept tensor read targets; implementations may return
687    /// validation or typed backend-source errors.
688    fn cholesky_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Tensor> {
689        Err(tenferro_tensor::Error::unsupported(
690            "cholesky",
691            "backend does not accept tensor reads at this execution boundary",
692        ))
693    }
694
695    /// Compute public LU outputs from a tensor read target.
696    ///
697    /// Backends may canonicalize the input inside the same placement family, but
698    /// must not silently transfer between CPU and GPU memory.
699    ///
700    /// # Examples
701    ///
702    /// ```rust
703    /// use tenferro_linalg::LinalgBackend;
704    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
705    /// use tenferro_tensor::{BackendSessionHost, TensorRead, TensorView, TypedTensor};
706    ///
707    /// let input = TypedTensor::<f64>::from_vec_col_major(
708    ///     vec![2, 2],
709    ///     vec![1.0, 3.0, 2.0, 4.0],
710    /// )?;
711    /// let mut host = CpuBackend::new();
712    /// let outputs = host.with_backend_session(|session| {
713    ///     with_cpu_exec_session(session, |backend| {
714    ///         backend.lu_read(TensorRead::from_view(TensorView::F64(input.as_view())))
715    ///     })
716    ///     .expect("CpuBackend must expose a CpuExecSession")
717    /// })?;
718    /// assert_eq!(outputs.len(), 4);
719    /// # Ok::<(), tenferro_tensor::Error>(())
720    /// ```
721    ///
722    /// # Errors
723    ///
724    /// The default implementation returns `Error::Unsupported` because the
725    /// backend does not accept tensor read targets; implementations may return
726    /// validation or typed backend-source errors.
727    fn lu_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Vec<Tensor>> {
728        Err(tenferro_tensor::Error::unsupported(
729            "lu",
730            "backend does not accept tensor reads at this execution boundary",
731        ))
732    }
733
734    /// Compute public full-pivoting LU outputs from a tensor read target.
735    ///
736    /// Backends may canonicalize the input inside the same placement family, but
737    /// must not silently transfer between CPU and GPU memory.
738    ///
739    /// # Examples
740    ///
741    /// ```rust
742    /// use tenferro_linalg::LinalgBackend;
743    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
744    /// use tenferro_tensor::{BackendSessionHost, TensorRead, TensorView, TypedTensor};
745    ///
746    /// let input = TypedTensor::<f64>::from_vec_col_major(
747    ///     vec![2, 2],
748    ///     vec![1.0, 3.0, 2.0, 4.0],
749    /// )?;
750    /// let mut host = CpuBackend::new();
751    /// let outputs = host.with_backend_session(|session| {
752    ///     with_cpu_exec_session(session, |backend| {
753    ///         backend.full_piv_lu_read(TensorRead::from_view(TensorView::F64(input.as_view())))
754    ///     })
755    ///     .expect("CpuBackend must expose a CpuExecSession")
756    /// })?;
757    /// assert_eq!(outputs.len(), 5);
758    /// # Ok::<(), tenferro_tensor::Error>(())
759    /// ```
760    ///
761    /// # Errors
762    ///
763    /// The default implementation returns `Error::Unsupported` because the
764    /// backend does not accept tensor read targets; implementations may return
765    /// validation or typed backend-source errors.
766    fn full_piv_lu_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Vec<Tensor>> {
767        Err(tenferro_tensor::Error::unsupported(
768            "full_piv_lu",
769            "backend does not accept tensor reads at this execution boundary",
770        ))
771    }
772
773    /// Compute general eigendecomposition outputs from a tensor read target.
774    ///
775    /// Backends may canonicalize the input inside the same placement family, but
776    /// must not silently transfer between CPU and GPU memory.
777    ///
778    /// # Examples
779    ///
780    /// ```rust
781    /// use tenferro_linalg::LinalgBackend;
782    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
783    /// use tenferro_tensor::{BackendSessionHost, TensorRead, TensorView, TypedTensor};
784    ///
785    /// let input = TypedTensor::<f64>::from_vec_col_major(
786    ///     vec![2, 2],
787    ///     vec![2.0, 0.0, 0.0, 3.0],
788    /// )?;
789    /// let mut host = CpuBackend::new();
790    /// let outputs = host.with_backend_session(|session| {
791    ///     with_cpu_exec_session(session, |backend| {
792    ///         backend.eig_read(TensorRead::from_view(TensorView::F64(input.as_view())))
793    ///     })
794    ///     .expect("CpuBackend must expose a CpuExecSession")
795    /// })?;
796    /// assert_eq!(outputs.len(), 2);
797    /// # Ok::<(), tenferro_tensor::Error>(())
798    /// ```
799    ///
800    /// # Errors
801    ///
802    /// The default implementation returns `Error::Unsupported` because the
803    /// backend does not accept tensor read targets; implementations may return
804    /// validation or typed backend-source errors.
805    fn eig_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Vec<Tensor>> {
806        Err(tenferro_tensor::Error::unsupported(
807            "eig",
808            "backend does not accept tensor reads at this execution boundary",
809        ))
810    }
811
812    #[doc(hidden)]
813    fn eigh_values(&mut self, _input: &Tensor) -> tenferro_tensor::Result<Tensor> {
814        Err(tenferro_tensor::Error::unsupported(
815            "eigh_values",
816            format!(
817                "backend {} does not implement internal Hermitian eigenvalues-only decomposition",
818                std::any::type_name::<Self>()
819            ),
820        ))
821    }
822
823    #[doc(hidden)]
824    fn eigh_values_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Tensor> {
825        Err(tenferro_tensor::Error::unsupported(
826            "eigh_values",
827            "backend does not implement borrowed Hermitian eigenvalues-only decomposition",
828        ))
829    }
830
831    /// Compute public general eigendecomposition outputs `(values, vectors)`.
832    ///
833    /// # Errors
834    ///
835    /// Returns `Error::Validation` for a non-square, rank, or dtype mismatch,
836    /// and a typed `Error::Extension` or backend source when the eigensolver
837    /// fails.
838    fn eig(&mut self, input: &Tensor) -> tenferro_tensor::Result<Vec<Tensor>>;
839
840    #[doc(hidden)]
841    fn eig_values(&mut self, _input: &Tensor) -> tenferro_tensor::Result<Tensor> {
842        Err(tenferro_tensor::Error::unsupported(
843            "eig_values",
844            format!(
845                "backend {} does not implement internal general eigenvalues-only decomposition",
846                std::any::type_name::<Self>()
847            ),
848        ))
849    }
850
851    /// Solve a dense linear system.
852    ///
853    /// # Errors
854    ///
855    /// Returns `Error::Validation` for incompatible matrix/rhs shapes, rank,
856    /// or dtype; `Error::Extension` with `ErrorKind::NumericalFailure` for a
857    /// singular system; or a typed backend source for provider failure.
858    fn solve(&mut self, a: &Tensor, b: &Tensor) -> tenferro_tensor::Result<Tensor>;
859
860    /// Solve a linear system from tensor read targets.
861    ///
862    /// Backends may canonicalize the inputs inside the same placement family,
863    /// but must not silently transfer between CPU and GPU memory.
864    ///
865    /// # Examples
866    ///
867    /// ```rust
868    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
869    /// use tenferro_linalg::LinalgBackend;
870    /// use tenferro_tensor::{BackendSessionHost, Tensor, TensorRead};
871    ///
872    /// let a = Tensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 0.0, 3.0])?;
873    /// let b = Tensor::from_vec_col_major(vec![2, 1], vec![4.0_f64, 9.0])?;
874    /// let mut host = CpuBackend::new();
875    /// let x = host.with_backend_session(|session| {
876    ///     with_cpu_exec_session(session, |backend| {
877    ///         backend.solve_read(
878    ///             TensorRead::from_tensor(&a),
879    ///             TensorRead::from_tensor(&b),
880    ///         )
881    ///     })
882    ///     .expect("CpuBackend must expose a CpuExecSession")
883    /// })?;
884    /// let Tensor::F64(x) = x else { unreachable!("F64 inputs return F64 output") };
885    /// assert_eq!(x.host_data()?, &[2.0, 3.0]);
886    /// # Ok::<(), tenferro_tensor::Error>(())
887    /// ```
888    ///
889    /// # Errors
890    ///
891    /// The default implementation returns `Error::Unsupported` because the
892    /// backend does not accept tensor read targets. Implementations may return
893    /// `Error::Validation` for incompatible shapes or dtypes,
894    /// `Error::RuntimeState` for invalid placement, `Error::Extension` for a
895    /// singular system, or a typed backend-source error.
896    fn solve_read(
897        &mut self,
898        _a: TensorRead<'_>,
899        _b: TensorRead<'_>,
900    ) -> tenferro_tensor::Result<Tensor> {
901        Err(tenferro_tensor::Error::unsupported(
902            "solve",
903            "backend does not accept tensor reads at this execution boundary",
904        ))
905    }
906
907    /// Solve into a caller-owned destination.
908    ///
909    /// The default preserves the ordinary read path and copies its result into
910    /// `out`. Backends with a native destination path may override this method,
911    /// but must validate the destination before the first write and preserve
912    /// the same shape, dtype, placement, aliasing, and error contracts.
913    ///
914    /// # Errors
915    ///
916    /// Returns `tenferro_tensor_core::ShapeMismatch` or
917    /// `tenferro_tensor_core::ValidationError::DTypeMismatch` for incompatible
918    /// destination metadata, `tenferro_tensor_core::ValidationError::InvalidArgument`
919    /// for aliasing or placement violations, `Error::Unsupported` when the
920    /// provider is unavailable, and `Error::Singular` for a singular system.
921    ///
922    /// # Examples
923    ///
924    /// ```rust
925    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
926    /// use tenferro_linalg::LinalgBackend;
927    /// use tenferro_tensor::{BackendSessionHost, Tensor, TensorRead, TensorWrite};
928    ///
929    /// let a = Tensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 0.0, 4.0])?;
930    /// let b = Tensor::from_vec_col_major(vec![2, 1], vec![4.0_f64, 8.0])?;
931    /// let mut out = Tensor::from_vec_col_major(vec![2, 1], vec![0.0_f64; 2])?;
932    /// let mut host = CpuBackend::new();
933    /// host.with_backend_session(|session| {
934    ///     with_cpu_exec_session(session, |backend| {
935    ///         backend.solve_read_into(
936    ///             TensorRead::from_tensor(&a),
937    ///             TensorRead::from_tensor(&b),
938    ///             TensorWrite::from_tensor(&mut out),
939    ///         )
940    ///     })
941    ///     .expect("CpuBackend must expose a CpuExecSession")
942    /// })?;
943    /// assert_eq!(out.as_slice::<f64>()?, &[2.0, 2.0]);
944    /// # Ok::<(), tenferro_tensor::Error>(())
945    /// ```
946    fn solve_read_into(
947        &mut self,
948        a: TensorRead<'_>,
949        b: TensorRead<'_>,
950        out: TensorWrite<'_>,
951    ) -> tenferro_tensor::Result<()> {
952        solve_read_into_default(self, a, b, out)
953    }
954
955    #[doc(hidden)]
956    fn lu_solve_prepared(
957        &mut self,
958        _a: &Tensor,
959        _packed_lu: &Tensor,
960        _pivots: &Tensor,
961        _b: &Tensor,
962        _transpose_a: bool,
963        _conjugate_a: bool,
964    ) -> tenferro_tensor::Result<Tensor> {
965        Err(tenferro_tensor::Error::unsupported(
966            "lu_solve_prepared",
967            format!(
968                "backend {} does not implement internal prepared LU solve",
969                std::any::type_name::<Self>()
970            ),
971        ))
972    }
973}
974
975pub(crate) fn solve_read_into_default<B: LinalgBackend + ?Sized>(
976    backend: &mut B,
977    a: TensorRead<'_>,
978    b: TensorRead<'_>,
979    out: TensorWrite<'_>,
980) -> tenferro_tensor::Result<()> {
981    validate_solve_read_into(&a, &b, &out)?;
982    let result = backend.solve_read(a, b)?;
983    backend.copy_read_into(TensorRead::from_tensor(&result), out)
984}
985
986pub(crate) fn validate_solve_read_into(
987    _a: &TensorRead<'_>,
988    b: &TensorRead<'_>,
989    out: &TensorWrite<'_>,
990) -> tenferro_tensor::Result<()> {
991    if b.shape() != out.shape() {
992        return Err(tenferro_tensor::Error::shape_mismatch(
993            "solve_read_into",
994            b.shape().to_vec(),
995            out.shape().to_vec(),
996        ));
997    }
998    if b.dtype() != out.dtype() {
999        return Err(tenferro_tensor::Error::dtype_mismatch(
1000            "solve_read_into",
1001            b.dtype(),
1002            out.dtype(),
1003        ));
1004    }
1005    if b.placement() != out.as_read().placement() {
1006        return Err(tenferro_tensor::Error::invalid_argument(
1007            "solve_read_into",
1008            "out",
1009            format!(
1010                "destination placement {:?} does not match rhs placement {:?}",
1011                out.as_read().placement(),
1012                b.placement()
1013            ),
1014        ));
1015    }
1016    let inputs = [_a.clone(), b.clone()];
1017    tenferro_tensor::backend::validate_read_into_destination("solve_read_into", &inputs, out)
1018}