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}