Skip to main content

tenferro_linalg/
backend.rs

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