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