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 #[doc(hidden)]
302 fn svd_values_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Tensor> {
303 Err(tenferro_tensor::Error::unsupported(
304 "svd_values",
305 "backend does not implement borrowed singular-values-only decomposition",
306 ))
307 }
308
309 /// Compute public QR outputs `(Q, R)`.
310 ///
311 /// QR is thin: for an `m x n` input, `Q` has shape `m x min(m, n)` and
312 /// `R` has shape `min(m, n) x n`.
313 ///
314 /// # Errors
315 ///
316 /// Returns `Error::Validation` for an unsupported rank, shape, or dtype,
317 /// and a typed `Error::Extension` or backend source when QR execution
318 /// fails.
319 fn qr(&mut self, input: &Tensor) -> tenferro_tensor::Result<Vec<Tensor>>;
320
321 /// Compute public QR outputs `(Q, R)` with explicit options.
322 ///
323 /// `gauge` controls optional sign or phase post-processing.
324 ///
325 /// # Examples
326 ///
327 /// ```rust
328 /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
329 /// use tenferro_linalg::{LinalgBackend, QrGauge, QrOptions};
330 /// use tenferro_tensor::{BackendSessionHost, Tensor};
331 ///
332 /// let input = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 2.0])?;
333 /// let mut host = CpuBackend::new();
334 /// let outputs = host.with_backend_session(|session| {
335 /// with_cpu_exec_session(session, |backend| {
336 /// backend.qr_with_options(
337 /// &input,
338 /// QrOptions::default().gauge(QrGauge::PositiveDiagonal),
339 /// )
340 /// })
341 /// .expect("CpuBackend must expose a CpuExecSession")
342 /// })?;
343 /// assert_eq!(outputs[0].shape(), &[2, 2]);
344 /// # Ok::<(), tenferro_tensor::Error>(())
345 /// ```
346 ///
347 /// # Errors
348 ///
349 /// Returns [`tenferro_tensor::Error::Validation`] containing
350 /// [`tenferro_tensor::ValidationError::RankMismatch`] or
351 /// [`tenferro_tensor::ValidationError::ShapeMismatch`] for an invalid
352 /// matrix input, or [`tenferro_tensor::ValidationError::InvalidArgument`]
353 /// for malformed gauge output metadata, checked size arithmetic, or an
354 /// unavailable compiled provider. A mismatched generated `Q`/`R` dtype is reported as
355 /// [`tenferro_tensor::ValidationError::DTypeMismatch`]. Provider
356 /// unsupported dtype or numerical rejection is
357 /// [`tenferro_tensor::Error::Extension`] with a typed linalg source, while
358 /// provider failures use [`tenferro_tensor::Error::BackendSource`] and a
359 /// backend-resident input uses [`tenferro_tensor::Error::RuntimeState`].
360 fn qr_with_options(
361 &mut self,
362 input: &Tensor,
363 options: QrOptions,
364 ) -> tenferro_tensor::Result<Vec<Tensor>> {
365 let mut outputs = self.qr(input)?;
366 apply_qr_gauge(options.gauge, &mut outputs)?;
367 Ok(outputs)
368 }
369
370 /// Compute public QR outputs `(Q, R)` from a tensor read target.
371 ///
372 /// Backends may canonicalize the input inside the same placement family, but
373 /// must not silently transfer between CPU and GPU memory.
374 ///
375 /// # Examples
376 ///
377 /// ```rust
378 /// use tenferro_linalg::LinalgBackend;
379 /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
380 /// use tenferro_tensor::{BackendSessionHost, TensorRead, TensorView, TypedTensor};
381 ///
382 /// let input = TypedTensor::<f64>::from_vec_col_major(
383 /// vec![2, 2],
384 /// vec![1.0, 0.0, 0.0, 2.0],
385 /// )?;
386 /// let mut host = CpuBackend::new();
387 /// let outputs = host.with_backend_session(|session| {
388 /// with_cpu_exec_session(session, |backend| {
389 /// backend.qr_read(TensorRead::from_view(TensorView::F64(input.as_view())))
390 /// })
391 /// .expect("CpuBackend must expose a CpuExecSession")
392 /// })?;
393 /// assert_eq!(outputs[0].shape(), &[2, 2]);
394 /// assert_eq!(outputs[1].shape(), &[2, 2]);
395 /// # Ok::<(), tenferro_tensor::Error>(())
396 /// ```
397 ///
398 /// # Errors
399 ///
400 /// The default implementation returns `Error::Unsupported` because the
401 /// backend does not accept tensor read targets; implementations may return
402 /// validation or typed backend-source errors.
403 fn qr_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Vec<Tensor>> {
404 Err(tenferro_tensor::Error::unsupported(
405 "qr",
406 "backend does not accept tensor reads at this execution boundary",
407 ))
408 }
409
410 /// Compute public Hermitian eigendecomposition outputs `(values, vectors)`.
411 ///
412 /// The returned vector order is `[values, vectors]`, where `values` has
413 /// shape `[n]` and `vectors` has shape `[n, n]`.
414 ///
415 /// # Errors
416 ///
417 /// Returns `Error::Validation` for a non-square or unsupported-dtype input
418 /// and a typed `Error::Extension` or backend source when eigendecomposition
419 /// fails.
420 fn eigh(&mut self, input: &Tensor) -> tenferro_tensor::Result<Vec<Tensor>>;
421
422 /// Compute public Hermitian eigendecomposition outputs with explicit options.
423 ///
424 /// `derivative_eps` is validated for API consistency, but concrete backend
425 /// execution does not perform AD. `gauge` controls optional eigenvector
426 /// post-processing.
427 ///
428 /// # Examples
429 ///
430 /// ```rust
431 /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
432 /// use tenferro_linalg::{EighGauge, EighOptions, LinalgBackend};
433 /// use tenferro_tensor::{BackendSessionHost, Tensor};
434 ///
435 /// let input = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 2.0])?;
436 /// let mut host = CpuBackend::new();
437 /// let outputs = host.with_backend_session(|session| {
438 /// with_cpu_exec_session(session, |backend| {
439 /// backend.eigh_with_options(
440 /// &input,
441 /// EighOptions::default()
442 /// .gauge(EighGauge::CanonicalPivot)
443 /// .derivative_eps(1.0e-10),
444 /// )
445 /// })
446 /// .expect("CpuBackend must expose a CpuExecSession")
447 /// })?;
448 /// assert_eq!(outputs[0].shape(), &[2]);
449 /// # Ok::<(), tenferro_tensor::Error>(())
450 /// ```
451 ///
452 /// # Errors
453 ///
454 /// Returns [`tenferro_tensor::Error::Validation`] containing
455 /// [`tenferro_tensor::ValidationError::InvalidArgument`] when
456 /// `derivative_eps` is non-finite or non-positive, when canonical gauge
457 /// output metadata is malformed, or when checked output-size arithmetic
458 /// overflows. It can return [`tenferro_tensor::Error::Validation`] with
459 /// [`tenferro_tensor::ValidationError::RankMismatch`] or
460 /// [`tenferro_tensor::ValidationError::ShapeMismatch`] for the
461 /// matrix input, or [`tenferro_tensor::ValidationError::DTypeMismatch`]
462 /// for generated outputs. It can also return
463 /// [`tenferro_tensor::Error::Extension`] with typed
464 /// `tenferro_linalg::Error::UnsupportedDType` or `NonConvergence`, and
465 /// [`tenferro_tensor::Error::BackendSource`] or
466 /// [`tenferro_tensor::Error::RuntimeState`] for provider and placement
467 /// failures.
468 fn eigh_with_options(
469 &mut self,
470 input: &Tensor,
471 options: EighOptions,
472 ) -> tenferro_tensor::Result<Vec<Tensor>> {
473 validate_derivative_eps("eigh_with_options", options.derivative_eps)?;
474 let mut outputs = self.eigh(input)?;
475 apply_eigh_gauge(options.gauge, &mut outputs)?;
476 Ok(outputs)
477 }
478
479 /// Compute public Hermitian eigendecomposition outputs from a tensor read target.
480 ///
481 /// Backends may canonicalize the input inside the same placement family, but
482 /// must not silently transfer between CPU and GPU memory.
483 ///
484 /// # Examples
485 ///
486 /// ```rust
487 /// use tenferro_linalg::LinalgBackend;
488 /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
489 /// use tenferro_tensor::{BackendSessionHost, TensorRead, TensorView, TypedTensor};
490 ///
491 /// let input = TypedTensor::<f64>::from_vec_col_major(
492 /// vec![2, 2],
493 /// vec![1.0, 0.0, 0.0, 2.0],
494 /// )?;
495 /// let mut host = CpuBackend::new();
496 /// let outputs = host.with_backend_session(|session| {
497 /// with_cpu_exec_session(session, |backend| {
498 /// backend.eigh_read(TensorRead::from_view(TensorView::F64(input.as_view())))
499 /// })
500 /// .expect("CpuBackend must expose a CpuExecSession")
501 /// })?;
502 /// assert_eq!(outputs[0].shape(), &[2]);
503 /// assert_eq!(outputs[1].shape(), &[2, 2]);
504 /// # Ok::<(), tenferro_tensor::Error>(())
505 /// ```
506 ///
507 /// # Errors
508 ///
509 /// The default implementation returns `Error::Unsupported` because the
510 /// backend does not accept tensor read targets; implementations may return
511 /// validation or typed backend-source errors.
512 fn eigh_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Vec<Tensor>> {
513 Err(tenferro_tensor::Error::unsupported(
514 "eigh",
515 "backend does not accept tensor reads at this execution boundary",
516 ))
517 }
518
519 /// Compute Cholesky factorization from a tensor read target.
520 ///
521 /// Backends may canonicalize the input inside the same placement family, but
522 /// must not silently transfer between CPU and GPU memory.
523 ///
524 /// # Examples
525 ///
526 /// ```rust
527 /// use tenferro_linalg::LinalgBackend;
528 /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
529 /// use tenferro_tensor::{BackendSessionHost, TensorRead, TensorView, TypedTensor};
530 ///
531 /// let input = TypedTensor::<f64>::from_vec_col_major(
532 /// vec![2, 2],
533 /// vec![4.0, 2.0, 2.0, 3.0],
534 /// )?;
535 /// let mut host = CpuBackend::new();
536 /// let output = host.with_backend_session(|session| {
537 /// with_cpu_exec_session(session, |backend| {
538 /// backend.cholesky_read(TensorRead::from_view(TensorView::F64(input.as_view())))
539 /// })
540 /// .expect("CpuBackend must expose a CpuExecSession")
541 /// })?;
542 /// assert_eq!(output.shape(), &[2, 2]);
543 /// # Ok::<(), tenferro_tensor::Error>(())
544 /// ```
545 ///
546 /// # Errors
547 ///
548 /// The default implementation returns `Error::Unsupported` because the
549 /// backend does not accept tensor read targets; implementations may return
550 /// validation or typed backend-source errors.
551 fn cholesky_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Tensor> {
552 Err(tenferro_tensor::Error::unsupported(
553 "cholesky",
554 "backend does not accept tensor reads at this execution boundary",
555 ))
556 }
557
558 /// Compute public LU outputs from a tensor read target.
559 ///
560 /// Backends may canonicalize the input inside the same placement family, but
561 /// must not silently transfer between CPU and GPU memory.
562 ///
563 /// # Examples
564 ///
565 /// ```rust
566 /// use tenferro_linalg::LinalgBackend;
567 /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
568 /// use tenferro_tensor::{BackendSessionHost, TensorRead, TensorView, TypedTensor};
569 ///
570 /// let input = TypedTensor::<f64>::from_vec_col_major(
571 /// vec![2, 2],
572 /// vec![1.0, 3.0, 2.0, 4.0],
573 /// )?;
574 /// let mut host = CpuBackend::new();
575 /// let outputs = host.with_backend_session(|session| {
576 /// with_cpu_exec_session(session, |backend| {
577 /// backend.lu_read(TensorRead::from_view(TensorView::F64(input.as_view())))
578 /// })
579 /// .expect("CpuBackend must expose a CpuExecSession")
580 /// })?;
581 /// assert_eq!(outputs.len(), 4);
582 /// # Ok::<(), tenferro_tensor::Error>(())
583 /// ```
584 ///
585 /// # Errors
586 ///
587 /// The default implementation returns `Error::Unsupported` because the
588 /// backend does not accept tensor read targets; implementations may return
589 /// validation or typed backend-source errors.
590 fn lu_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Vec<Tensor>> {
591 Err(tenferro_tensor::Error::unsupported(
592 "lu",
593 "backend does not accept tensor reads at this execution boundary",
594 ))
595 }
596
597 /// Compute public full-pivoting LU outputs from a tensor read target.
598 ///
599 /// Backends may canonicalize the input inside the same placement family, but
600 /// must not silently transfer between CPU and GPU memory.
601 ///
602 /// # Examples
603 ///
604 /// ```rust
605 /// use tenferro_linalg::LinalgBackend;
606 /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
607 /// use tenferro_tensor::{BackendSessionHost, TensorRead, TensorView, TypedTensor};
608 ///
609 /// let input = TypedTensor::<f64>::from_vec_col_major(
610 /// vec![2, 2],
611 /// vec![1.0, 3.0, 2.0, 4.0],
612 /// )?;
613 /// let mut host = CpuBackend::new();
614 /// let outputs = host.with_backend_session(|session| {
615 /// with_cpu_exec_session(session, |backend| {
616 /// backend.full_piv_lu_read(TensorRead::from_view(TensorView::F64(input.as_view())))
617 /// })
618 /// .expect("CpuBackend must expose a CpuExecSession")
619 /// })?;
620 /// assert_eq!(outputs.len(), 5);
621 /// # Ok::<(), tenferro_tensor::Error>(())
622 /// ```
623 ///
624 /// # Errors
625 ///
626 /// The default implementation returns `Error::Unsupported` because the
627 /// backend does not accept tensor read targets; implementations may return
628 /// validation or typed backend-source errors.
629 fn full_piv_lu_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Vec<Tensor>> {
630 Err(tenferro_tensor::Error::unsupported(
631 "full_piv_lu",
632 "backend does not accept tensor reads at this execution boundary",
633 ))
634 }
635
636 /// Compute general eigendecomposition outputs from a tensor read target.
637 ///
638 /// Backends may canonicalize the input inside the same placement family, but
639 /// must not silently transfer between CPU and GPU memory.
640 ///
641 /// # Examples
642 ///
643 /// ```rust
644 /// use tenferro_linalg::LinalgBackend;
645 /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
646 /// use tenferro_tensor::{BackendSessionHost, TensorRead, TensorView, TypedTensor};
647 ///
648 /// let input = TypedTensor::<f64>::from_vec_col_major(
649 /// vec![2, 2],
650 /// vec![2.0, 0.0, 0.0, 3.0],
651 /// )?;
652 /// let mut host = CpuBackend::new();
653 /// let outputs = host.with_backend_session(|session| {
654 /// with_cpu_exec_session(session, |backend| {
655 /// backend.eig_read(TensorRead::from_view(TensorView::F64(input.as_view())))
656 /// })
657 /// .expect("CpuBackend must expose a CpuExecSession")
658 /// })?;
659 /// assert_eq!(outputs.len(), 2);
660 /// # Ok::<(), tenferro_tensor::Error>(())
661 /// ```
662 ///
663 /// # Errors
664 ///
665 /// The default implementation returns `Error::Unsupported` because the
666 /// backend does not accept tensor read targets; implementations may return
667 /// validation or typed backend-source errors.
668 fn eig_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Vec<Tensor>> {
669 Err(tenferro_tensor::Error::unsupported(
670 "eig",
671 "backend does not accept tensor reads at this execution boundary",
672 ))
673 }
674
675 #[doc(hidden)]
676 fn eigh_values(&mut self, _input: &Tensor) -> tenferro_tensor::Result<Tensor> {
677 Err(tenferro_tensor::Error::unsupported(
678 "eigh_values",
679 format!(
680 "backend {} does not implement internal Hermitian eigenvalues-only decomposition",
681 std::any::type_name::<Self>()
682 ),
683 ))
684 }
685
686 #[doc(hidden)]
687 fn eigh_values_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Tensor> {
688 Err(tenferro_tensor::Error::unsupported(
689 "eigh_values",
690 "backend does not implement borrowed Hermitian eigenvalues-only decomposition",
691 ))
692 }
693
694 /// Compute public general eigendecomposition outputs `(values, vectors)`.
695 ///
696 /// # Errors
697 ///
698 /// Returns `Error::Validation` for a non-square, rank, or dtype mismatch,
699 /// and a typed `Error::Extension` or backend source when the eigensolver
700 /// fails.
701 fn eig(&mut self, input: &Tensor) -> tenferro_tensor::Result<Vec<Tensor>>;
702
703 #[doc(hidden)]
704 fn eig_values(&mut self, _input: &Tensor) -> tenferro_tensor::Result<Tensor> {
705 Err(tenferro_tensor::Error::unsupported(
706 "eig_values",
707 format!(
708 "backend {} does not implement internal general eigenvalues-only decomposition",
709 std::any::type_name::<Self>()
710 ),
711 ))
712 }
713
714 /// Solve a dense linear system.
715 ///
716 /// # Errors
717 ///
718 /// Returns `Error::Validation` for incompatible matrix/rhs shapes, rank,
719 /// or dtype; `Error::Extension` with `ErrorKind::NumericalFailure` for a
720 /// singular system; or a typed backend source for provider failure.
721 fn solve(&mut self, a: &Tensor, b: &Tensor) -> tenferro_tensor::Result<Tensor>;
722
723 /// Solve a linear system from tensor read targets.
724 ///
725 /// Backends may canonicalize the inputs inside the same placement family,
726 /// but must not silently transfer between CPU and GPU memory.
727 ///
728 /// # Examples
729 ///
730 /// ```rust
731 /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
732 /// use tenferro_linalg::LinalgBackend;
733 /// use tenferro_tensor::{BackendSessionHost, Tensor, TensorRead};
734 ///
735 /// let a = Tensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 0.0, 3.0])?;
736 /// let b = Tensor::from_vec_col_major(vec![2, 1], vec![4.0_f64, 9.0])?;
737 /// let mut host = CpuBackend::new();
738 /// let x = host.with_backend_session(|session| {
739 /// with_cpu_exec_session(session, |backend| {
740 /// backend.solve_read(
741 /// TensorRead::from_tensor(&a),
742 /// TensorRead::from_tensor(&b),
743 /// )
744 /// })
745 /// .expect("CpuBackend must expose a CpuExecSession")
746 /// })?;
747 /// let Tensor::F64(x) = x else { unreachable!("F64 inputs return F64 output") };
748 /// assert_eq!(x.host_data()?, &[2.0, 3.0]);
749 /// # Ok::<(), tenferro_tensor::Error>(())
750 /// ```
751 ///
752 /// # Errors
753 ///
754 /// The default implementation returns `Error::Unsupported` because the
755 /// backend does not accept tensor read targets. Implementations may return
756 /// `Error::Validation` for incompatible shapes or dtypes,
757 /// `Error::RuntimeState` for invalid placement, `Error::Extension` for a
758 /// singular system, or a typed backend-source error.
759 fn solve_read(
760 &mut self,
761 _a: TensorRead<'_>,
762 _b: TensorRead<'_>,
763 ) -> tenferro_tensor::Result<Tensor> {
764 Err(tenferro_tensor::Error::unsupported(
765 "solve",
766 "backend does not accept tensor reads at this execution boundary",
767 ))
768 }
769
770 /// Solve into a caller-owned destination.
771 ///
772 /// The default preserves the ordinary read path and copies its result into
773 /// `out`. Backends with a native destination path may override this method,
774 /// but must validate the destination before the first write and preserve
775 /// the same shape, dtype, placement, aliasing, and error contracts.
776 ///
777 /// # Errors
778 ///
779 /// Returns `tenferro_tensor_core::ShapeMismatch` or
780 /// `tenferro_tensor_core::ValidationError::DTypeMismatch` for incompatible
781 /// destination metadata, `tenferro_tensor_core::ValidationError::InvalidArgument`
782 /// for aliasing or placement violations, `Error::Unsupported` when the
783 /// provider is unavailable, and `Error::Singular` for a singular system.
784 ///
785 /// # Examples
786 ///
787 /// ```rust
788 /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
789 /// use tenferro_linalg::LinalgBackend;
790 /// use tenferro_tensor::{BackendSessionHost, Tensor, TensorRead, TensorWrite};
791 ///
792 /// let a = Tensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 0.0, 4.0])?;
793 /// let b = Tensor::from_vec_col_major(vec![2, 1], vec![4.0_f64, 8.0])?;
794 /// let mut out = Tensor::from_vec_col_major(vec![2, 1], vec![0.0_f64; 2])?;
795 /// let mut host = CpuBackend::new();
796 /// host.with_backend_session(|session| {
797 /// with_cpu_exec_session(session, |backend| {
798 /// backend.solve_read_into(
799 /// TensorRead::from_tensor(&a),
800 /// TensorRead::from_tensor(&b),
801 /// TensorWrite::from_tensor(&mut out),
802 /// )
803 /// })
804 /// .expect("CpuBackend must expose a CpuExecSession")
805 /// })?;
806 /// assert_eq!(out.as_slice::<f64>()?, &[2.0, 2.0]);
807 /// # Ok::<(), tenferro_tensor::Error>(())
808 /// ```
809 fn solve_read_into(
810 &mut self,
811 a: TensorRead<'_>,
812 b: TensorRead<'_>,
813 out: TensorWrite<'_>,
814 ) -> tenferro_tensor::Result<()> {
815 solve_read_into_default(self, a, b, out)
816 }
817
818 #[doc(hidden)]
819 fn lu_solve_prepared(
820 &mut self,
821 _a: &Tensor,
822 _packed_lu: &Tensor,
823 _pivots: &Tensor,
824 _b: &Tensor,
825 _transpose_a: bool,
826 _conjugate_a: bool,
827 ) -> tenferro_tensor::Result<Tensor> {
828 Err(tenferro_tensor::Error::unsupported(
829 "lu_solve_prepared",
830 format!(
831 "backend {} does not implement internal prepared LU solve",
832 std::any::type_name::<Self>()
833 ),
834 ))
835 }
836}
837
838pub(crate) fn solve_read_into_default<B: LinalgBackend + ?Sized>(
839 backend: &mut B,
840 a: TensorRead<'_>,
841 b: TensorRead<'_>,
842 out: TensorWrite<'_>,
843) -> tenferro_tensor::Result<()> {
844 validate_solve_read_into(&a, &b, &out)?;
845 let result = backend.solve_read(a, b)?;
846 backend.copy_read_into(TensorRead::from_tensor(&result), out)
847}
848
849pub(crate) fn validate_solve_read_into(
850 _a: &TensorRead<'_>,
851 b: &TensorRead<'_>,
852 out: &TensorWrite<'_>,
853) -> tenferro_tensor::Result<()> {
854 if b.shape() != out.shape() {
855 return Err(tenferro_tensor::Error::shape_mismatch(
856 "solve_read_into",
857 b.shape().to_vec(),
858 out.shape().to_vec(),
859 ));
860 }
861 if b.dtype() != out.dtype() {
862 return Err(tenferro_tensor::Error::dtype_mismatch(
863 "solve_read_into",
864 b.dtype(),
865 out.dtype(),
866 ));
867 }
868 if b.placement() != out.as_read().placement() {
869 return Err(tenferro_tensor::Error::invalid_argument(
870 "solve_read_into",
871 "out",
872 format!(
873 "destination placement {:?} does not match rhs placement {:?}",
874 out.as_read().placement(),
875 b.placement()
876 ),
877 ));
878 }
879 let inputs = [_a.clone(), b.clone()];
880 tenferro_tensor::backend::validate_read_into_destination("solve_read_into", &inputs, out)
881}