Skip to main content

tensor4all_tensorbackend/
explicit.rs

1//! Opt-in explicit/concrete execution frontend.
2//!
3//! This is the second, opt-in frontend of
4//! [tensor4all-rs#859](https://github.com/tensor4all/tensor4all-rs/issues/859): a
5//! caller-supplied [`CpuExecutionContext`] plus one concrete session that a whole
6//! stage or batch reuses, instead of the process-global convenience surface in
7//! the compatibility entry point. The compatibility frontend keeps its own signatures,
8//! defaults and behaviour; nothing here reroutes it.
9//!
10//! Properties of this frontend, from the coexistence record
11//! (`docs/design/859-dual-frontend-coexistence.md`):
12//!
13//! - **One explicit entry.** [`CpuExecutionContext::with_concrete_session`] lends the
14//!   context's engine for the whole closure, so a stage borrows it once rather than once
15//!   per operation, and the engine's prepared plans and buffer pool stay warm.
16//! - **No process-global selector.** No route here selects an execution context or
17//!   backend from a default context, a thread-local override or an environment
18//!   variable. (Upstream profiling hooks may still read their own environment flags;
19//!   those do not choose where the work runs.)
20//! - **No eager, trace or AD records.** These routes reach only concrete
21//!   `Tensor`/engine operations; they never construct `EagerTensor`,
22//!   semantic nodes or gradient slots, and they take no eager owner lock. Tracked
23//!   values are not accepted at all, so no detach can happen implicitly.
24//! - **No implicit fallback.** An unsupported dtype or shape returns the
25//!   backend's typed error. Nothing here retries on the global or CPU-eager route.
26//! - **Explicit conversion at the boundary.** Crossing between this frontend and
27//!   the compatibility frontend is [`LogicalTensor`](crate::LogicalTensor), which
28//!   carries dtype, shape and column-major data and no backend identity.
29//!
30//! The session is a borrow of one [`tenferro_cpu::CpuEngine`], not a value
31//! representation: the concrete values stay the ordinary tenferro tensors.
32//!
33//! # Examples
34//!
35//! ```
36//! use tensor4all_tensorbackend::{CpuExecutionContext, explicit::Session};
37//! use tenferro::Tensor;
38//! use tensor4all_tensorbackend::CpuBackend;
39//!
40//! let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
41//! let x = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0])?;
42//!
43//! let product = context.with_concrete_session(|session: &mut Session<'_>| {
44//!     session.qr(&x).map(|(q, _r)| q)
45//! })?;
46//! assert_eq!(product.expect("QR succeeds").shape(), &[2, 2]);
47//! # Ok::<(), Box<dyn std::error::Error>>(())
48//! ```
49
50use tenferro::Tensor as NativeTensor;
51use tenferro::TensorSessionOpsExt;
52use tenferro_einsum::{ConcreteEinsumPlan, EinsumSubscripts, TensorEinsumExt};
53use tenferro_linalg::TensorLinalgExt;
54
55use tenferro_tensor::Engine;
56
57use crate::context::{CpuExecutionContext, CpuExecutionContextError};
58use crate::einsum_ids::{
59    build_binary_einsum_ids, checked_native_einsum_labels, common_dtype, convert_native_tensor_in,
60};
61
62/// One explicit concrete session over a caller-held CPU engine.
63///
64/// The session borrows an engine for the whole of its life. Inside
65/// [`CpuExecutionContext::with_concrete_session`] that is the context's engine,
66/// borrowed for the closure; a caller that owns an engine ([`crate::CpuBackend`])
67/// builds the same session with [`Session::new`] and keeps it across as many
68/// operations as it wants. Every route on it runs through that one engine, so a
69/// stage does not pay an entry per operation.
70///
71/// # Examples
72///
73/// ```
74/// use tensor4all_tensorbackend::CpuExecutionContext;
75/// use tenferro::Tensor;
76/// use tensor4all_tensorbackend::CpuBackend;
77///
78/// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
79/// let a = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0])?;
80/// let b = Tensor::from_vec_col_major(vec![2, 2], vec![0.0_f64, 1.0, 1.0, 0.0])?;
81/// let (ab, a_again) = context.with_concrete_session(
82///     |session| -> Result<_, Box<dyn std::error::Error + Send + Sync>> {
83///         let ab = session.contraction(&a, &[1], &b, &[0])?;
84///         // One session, two operations.
85///         let a_again = session.permute(&a, &[1, 0])?;
86///         Ok((ab, a_again))
87///     },
88/// )??;
89/// // `a` is `[[1, 3], [2, 4]]` column-major and `b` swaps the axes, so `a * b`
90/// // is `[[3, 1], [4, 2]]`.
91/// assert_eq!(ab.as_slice::<f64>()?, &[3.0, 4.0, 1.0, 2.0]);
92/// assert_eq!(a_again.as_slice::<f64>()?, &[1.0, 3.0, 2.0, 4.0]);
93/// # Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
94/// ```
95pub struct Session<'session> {
96    engine: &'session mut dyn Engine,
97}
98
99impl std::fmt::Debug for Session<'_> {
100    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101        formatter
102            .debug_struct("explicit::Session")
103            .finish_non_exhaustive()
104    }
105}
106
107/// Failure reported by this frontend's compatibility bridges.
108///
109/// The original tenferro or tenferro-ad diagnostic is retained as the source.
110#[derive(Debug, thiserror::Error)]
111pub enum ConversionError {
112    /// Materializing a tracked value into a plain concrete value failed.
113    #[error("materializing a tracked value failed: {source}")]
114    Detach {
115        /// Original tenferro diagnostic.
116        #[source]
117        source: Box<dyn std::error::Error + Send + Sync + 'static>,
118    },
119    /// Adopting a plain concrete value into an eager runtime failed.
120    #[error("adopting a concrete value failed: {source}")]
121    Lift {
122        /// Original tenferro or tenferro-ad diagnostic.
123        #[source]
124        source: Box<dyn std::error::Error + Send + Sync + 'static>,
125    },
126}
127
128/// Materialize a tracked eager value into a plain concrete value on `engine`.
129///
130/// This is the explicit AD to plain boundary of the coexistence contract: the caller names
131/// the conversion, so tracking never disappears silently, and `no_grad` on an eager value is
132/// not this operation and does not select the concrete representation. The value keeps its
133/// dtype, logical shape and column-major layout; the result is an ordinary concrete tensor
134/// that this frontend's routes accept. Materializing a lazy or prepared value needs an
135/// engine to evaluate it, so the caller passes the one it wants the copy to run on.
136///
137/// # Errors
138///
139/// Returns [`ConversionError::Detach`] when the tracked value cannot be materialized, for
140/// example because it is a lazy or prepared value that has not been evaluated.
141///
142/// # Examples
143///
144/// ```
145/// use tensor4all_tensorbackend::{explicit, CpuExecutionContext};
146/// use tenferro::Tensor;
147/// use tenferro_ad::EagerTensor;
148/// use tensor4all_tensorbackend::CpuBackend;
149///
150/// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
151/// let plain = Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0])?;
152/// let tracked = explicit::lift(&context, plain)?;
153/// let back = context.with_backend(|backend| explicit::detach(&tracked, backend.engine_mut()))?;
154/// assert_eq!(back.as_slice::<f64>()?, &[1.0, 2.0]);
155/// # Ok::<(), Box<dyn std::error::Error>>(())
156/// ```
157pub fn detach(
158    eager: &tenferro_ad::EagerTensor,
159    engine: &mut dyn Engine,
160) -> Result<NativeTensor, ConversionError> {
161    eager
162        .to_tensor(engine)
163        .map_err(|source| ConversionError::Detach {
164            source: Box::new(source),
165        })
166}
167
168/// Adopt a plain concrete value into `context`'s eager runtime.
169///
170/// This is the explicit plain to AD boundary: the result is a tracked value in a graph owned
171/// by `context`'s own eager runtime, and nothing about it is inferred from the caller's
172/// ambient `no_grad` or capture state. The value is moved into the runtime, because a
173/// concrete tensor has one owner. Use [`detach`] for the opposite direction.
174///
175/// # Errors
176///
177/// Returns [`ConversionError::Lift`] when the context's eager runtime cannot be initialized
178/// or when the value cannot be adopted, for example because it is not a concrete host value.
179///
180/// # Examples
181///
182/// ```
183/// use tensor4all_tensorbackend::{explicit, CpuExecutionContext};
184/// use tenferro::Tensor;
185/// use tensor4all_tensorbackend::CpuBackend;
186///
187/// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
188/// let plain = Tensor::from_vec_col_major(vec![2], vec![3.0_f64, 4.0])?;
189/// let tracked = explicit::lift(&context, plain)?;
190/// // The adopted value is tracked, and its runtime is the context's own.
191/// let back = context.with_backend(|backend| explicit::detach(&tracked, backend.engine_mut()))?;
192/// assert_eq!(back.as_slice::<f64>()?, &[3.0, 4.0]);
193/// # Ok::<(), Box<dyn std::error::Error>>(())
194/// ```
195pub fn lift(
196    context: &CpuExecutionContext,
197    tensor: NativeTensor,
198) -> Result<tenferro_ad::EagerTensor, ConversionError> {
199    let runtime = context
200        .eager_runtime()
201        .map_err(|source| ConversionError::Lift {
202            source: Box::new(source),
203        })?;
204    tenferro_ad::EagerTensor::from_tensor_in(tensor, runtime).map_err(|source| {
205        ConversionError::Lift {
206            source: Box::new(source),
207        }
208    })
209}
210
211/// One prepared N-ary einsum, reusable across operations of a session.
212///
213/// Preparing validates the labels and plans the contraction once; every
214/// [`PreparedEinsum::execute`] and [`PreparedEinsum::execute_into`] reuses that plan, so
215/// a stage keeps one plan across as many evaluations as it needs instead of preparing
216/// per call. The plan borrows its operands, so it cannot outlive them, and it holds no
217/// execution state: the session stays the caller's.
218///
219/// # Examples
220///
221/// ```
222/// use tensor4all_tensorbackend::{explicit::PreparedEinsum, CpuExecutionContext};
223/// use tenferro::Tensor;
224/// use tensor4all_tensorbackend::CpuBackend;
225/// use tenferro_tensor::{TensorRead, TensorWrite};
226///
227/// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
228/// let a = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0])?;
229/// let b = Tensor::from_vec_col_major(vec![2, 1], vec![5.0_f64, 6.0])?;
230/// let reads = [TensorRead::from_tensor(&a), TensorRead::from_tensor(&b)];
231/// let plan = PreparedEinsum::prepare(&reads, &[&[0, 1], &[1, 2]], &[0, 2])?;
232///
233/// // Reused across a stage, including into a caller-provided destination.
234/// let mut out = Tensor::from_vec_col_major(vec![2, 1], vec![0.0_f64, 0.0])?;
235/// context.with_concrete_session(|session| -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
236///     let product = plan.execute(session)?;
237///     assert_eq!(product.as_slice::<f64>()?, &[23.0, 34.0]);
238///     plan.execute_into(session, TensorWrite::Tensor(&mut out))?;
239///     Ok(())
240/// })??;
241/// assert_eq!(out.as_slice::<f64>()?, &[23.0, 34.0]);
242/// # Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
243/// ```
244pub struct PreparedEinsum<'operands> {
245    plan: ConcreteEinsumPlan,
246    operands: Vec<tenferro_tensor::TensorRead<'operands>>,
247}
248
249impl std::fmt::Debug for PreparedEinsum<'_> {
250    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
251        formatter
252            .debug_struct("explicit::PreparedEinsum")
253            .field("operands", &self.operands.len())
254            .finish_non_exhaustive()
255    }
256}
257
258impl<'operands> PreparedEinsum<'operands> {
259    /// Plan an N-ary einsum over borrowed operands.
260    ///
261    /// # Errors
262    ///
263    /// Returns [`tenferro_einsum::Error::InvalidSubscripts`] for malformed labels, a
264    /// label list whose length does not match the operand's rank, or a label-list count
265    /// that does not match the operand count, and [`tenferro_einsum::Error`] from the
266    /// contraction planner when the expression cannot be planned.
267    pub fn prepare(
268        operands: &[tenferro_tensor::TensorRead<'operands>],
269        input_ids: &[&[usize]],
270        output_ids: &[usize],
271    ) -> tenferro_einsum::Result<Self> {
272        if operands.len() != input_ids.len() {
273            return Err(tenferro_einsum::Error::InvalidSubscripts {
274                message: format!(
275                    "einsum needs one label list per operand: {} operands, {} label lists",
276                    operands.len(),
277                    input_ids.len()
278                ),
279            });
280        }
281        let invalid = |error: anyhow::Error| tenferro_einsum::Error::InvalidSubscripts {
282            message: format!("{error}"),
283        };
284        let inputs = input_ids
285            .iter()
286            .map(|ids| checked_native_einsum_labels(ids))
287            .collect::<anyhow::Result<Vec<_>>>()
288            .map_err(invalid)?;
289        let output = checked_native_einsum_labels(output_ids).map_err(invalid)?;
290        let subscripts = EinsumSubscripts { inputs, output };
291        let plan = ConcreteEinsumPlan::prepare_read_subscripts(operands, &subscripts)?;
292        Ok(Self {
293            plan,
294            operands: operands.to_vec(),
295        })
296    }
297
298    /// Evaluate the prepared plan on `session`, allocating the result.
299    ///
300    /// # Errors
301    ///
302    /// Returns [`tenferro_einsum::Error::Tensor`] when the session rejects the operands
303    /// or the contraction itself fails, and
304    /// [`tenferro_einsum::Error::Validation`] when the operands no longer match the
305    /// planned expression.
306    pub fn execute(&self, session: &mut Session<'_>) -> tenferro_einsum::Result<NativeTensor> {
307        self.plan
308            .execute_read(self.operands.as_slice(), session.engine())
309    }
310
311    /// Evaluate the prepared plan into a caller-provided destination.
312    ///
313    /// The destination is fully written and is never zero-filled first. Operands and
314    /// destination must already share one dtype; this route does not convert into a
315    /// caller-owned output.
316    ///
317    /// # Errors
318    ///
319    /// Returns [`tenferro_einsum::Error::Tensor`] when the session rejects the operands,
320    /// the destination or the contraction, and
321    /// [`tenferro_einsum::Error::Validation`] when the operands no longer match the
322    /// planned expression.
323    pub fn execute_into(
324        &self,
325        session: &mut Session<'_>,
326        out: tenferro_tensor::TensorWrite<'_>,
327    ) -> tenferro_einsum::Result<()> {
328        self.plan
329            .execute_read_into(self.operands.as_slice(), session.engine(), out)
330    }
331}
332
333/// Promote a heterogeneous operand set to the dtype both frontends contract in.
334///
335/// Returns an empty vector when every operand already has the common dtype, so the
336/// common case allocates nothing.
337///
338/// # Errors
339///
340/// Returns [`tenferro_einsum::Error::Tensor`] when an operand cannot be converted.
341fn promote_operands(
342    engine: &mut dyn Engine,
343    operands: &[&NativeTensor],
344) -> tenferro_einsum::Result<Vec<NativeTensor>> {
345    let dtypes = operands
346        .iter()
347        .map(|tensor| tensor.dtype())
348        .collect::<Vec<_>>();
349    let target = common_dtype(&dtypes);
350    if operands.iter().all(|tensor| tensor.dtype() == target) {
351        return Ok(Vec::new());
352    }
353    operands
354        .iter()
355        .map(|tensor| {
356            convert_native_tensor_in(engine, tensor, target).map_err(tenferro_einsum::Error::Tensor)
357        })
358        .collect()
359}
360
361impl<'session> Session<'session> {
362    /// Wrap a borrowed engine as this frontend's session.
363    ///
364    /// A caller that owns an engine - a [`crate::CpuBackend`], an engine it built
365    /// itself, or one borrowed for a stage - builds the same [`Session`] the
366    /// callback-scoped [`CpuExecutionContext::with_concrete_session`] hands out, so a
367    /// long stage can keep one session across many operations without entering a
368    /// context. The engine stays borrowed for the whole life of the returned session:
369    /// this constructor never enters, replaces or reroutes an engine.
370    ///
371    /// # Examples
372    ///
373    /// ```
374    /// use tensor4all_tensorbackend::{explicit::Session, CpuBackend};
375    /// use tenferro::Tensor;
376    ///
377    /// let mut backend = CpuBackend::with_threads(1)?;
378    /// let a = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0])?;
379    ///
380    /// let mut session = Session::new(backend.engine_mut());
381    /// // One session, two operations.
382    /// let transposed = session.permute(&a, &[1, 0])?;
383    /// let total = session.sum(&transposed)?;
384    /// assert_eq!(total.as_slice::<f64>()?, &[10.0]);
385    /// # Ok::<(), Box<dyn std::error::Error>>(())
386    /// ```
387    pub fn new(engine: &'session mut dyn Engine) -> Self {
388        Self { engine }
389    }
390
391    /// The engine behind this explicit session.
392    ///
393    /// Exposed so a prepared plan can evaluate itself on the caller's engine; it is
394    /// not a second entry point, and it never consults another backend.
395    pub fn engine(&mut self) -> &mut dyn Engine {
396        self.engine
397    }
398
399    /// Reshape a concrete tensor without changing its column-major linearization.
400    ///
401    /// # Errors
402    ///
403    /// Returns [`tenferro_tensor::Error::Validation`] when the element counts
404    /// differ or the requested layout is invalid,
405    /// [`tenferro_tensor::Error::UnsupportedDType`] for a dtype the backend cannot
406    /// reshape, and [`tenferro_tensor::Error::BackendSource`] when the operation
407    /// itself fails. It never retries on another route.
408    ///
409    /// # Examples
410    ///
411    /// ```
412    /// use tensor4all_tensorbackend::CpuExecutionContext;
413    /// use tenferro::Tensor;
414    /// use tensor4all_tensorbackend::CpuBackend;
415    ///
416    /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
417    /// let x = Tensor::from_vec_col_major(vec![4], vec![1.0_f64, 2.0, 3.0, 4.0])?;
418    /// let reshaped = context.with_concrete_session(|session| session.reshape(&x, &[2, 2]))??;
419    /// assert_eq!(reshaped.as_slice::<f64>()?, &[1.0, 2.0, 3.0, 4.0]);
420    /// # Ok::<(), Box<dyn std::error::Error>>(())
421    /// ```
422    pub fn reshape(
423        &mut self,
424        tensor: &NativeTensor,
425        shape: &[usize],
426    ) -> tenferro_tensor::Result<NativeTensor> {
427        tensor.reshape(shape, self.engine)
428    }
429
430    /// Permute the axes of a concrete tensor.
431    ///
432    /// # Errors
433    ///
434    /// Returns [`tenferro_tensor::Error::Validation`] when `perm` is not a
435    /// permutation of the tensor's axes, and
436    /// [`tenferro_tensor::Error::BackendSource`] when the operation itself fails.
437    ///
438    /// # Examples
439    ///
440    /// ```
441    /// use tensor4all_tensorbackend::CpuExecutionContext;
442    /// use tenferro::Tensor;
443    /// use tensor4all_tensorbackend::CpuBackend;
444    ///
445    /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
446    /// let x = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0])?;
447    /// let transposed = context.with_concrete_session(|session| session.permute(&x, &[1, 0]))??;
448    /// assert_eq!(transposed.as_slice::<f64>()?, &[1.0, 3.0, 2.0, 4.0]);
449    /// # Ok::<(), Box<dyn std::error::Error>>(())
450    /// ```
451    pub fn permute(
452        &mut self,
453        tensor: &NativeTensor,
454        perm: &[usize],
455    ) -> tenferro_tensor::Result<NativeTensor> {
456        tensor.transpose(perm, self.engine)
457    }
458
459    /// Binary contraction along the given axes, preserving the remaining axes in
460    /// order.
461    ///
462    /// The operand identity and axis building is the same code path the
463    /// compatibility frontend uses, so both frontends contract identically; only
464    /// the session differs.
465    ///
466    /// # Errors
467    ///
468    /// Returns [`tenferro_einsum::Error::InvalidSubscripts`] when the axis lists are
469    /// malformed or of different length, and [`tenferro_einsum::Error::Tensor`] when the
470    /// backend rejects the operands, when an operand cannot be promoted, or when the
471    /// contraction itself fails.
472    ///
473    /// Operands are promoted to a common dtype exactly as the compatibility frontend
474    /// promotes them: a heterogeneous set contracts in the promoted dtype, and a set
475    /// that already shares one dtype is used as is.
476    ///
477    /// # Examples
478    ///
479    /// ```
480    /// use tensor4all_tensorbackend::CpuExecutionContext;
481    /// use tenferro::Tensor;
482    /// use tensor4all_tensorbackend::CpuBackend;
483    ///
484    /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
485    /// let a = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0])?;
486    /// let b = Tensor::from_vec_col_major(vec![2, 1], vec![5.0_f64, 6.0])?;
487    /// let product =
488    ///     context.with_concrete_session(|session| session.contraction(&a, &[1], &b, &[0]))??;
489    /// assert_eq!(product.as_slice::<f64>()?, &[23.0, 34.0]);
490    /// # Ok::<(), Box<dyn std::error::Error>>(())
491    /// ```
492    pub fn contraction(
493        &mut self,
494        lhs: &NativeTensor,
495        lhs_axes: &[usize],
496        rhs: &NativeTensor,
497        rhs_axes: &[usize],
498    ) -> tenferro_einsum::Result<NativeTensor> {
499        let (lhs_ids, rhs_ids, output_ids) =
500            build_binary_einsum_ids(lhs.shape().len(), lhs_axes, rhs.shape().len(), rhs_axes)
501                .map_err(|error| tenferro_einsum::Error::InvalidSubscripts {
502                    message: format!("{error}"),
503                })?;
504        let subscripts = EinsumSubscripts {
505            inputs: vec![lhs_ids, rhs_ids],
506            output: output_ids,
507        };
508        let operands = [lhs, rhs];
509        let promoted = promote_operands(self.engine, &operands)?;
510        let operands = if promoted.is_empty() {
511            operands.to_vec()
512        } else {
513            promoted.iter().collect::<Vec<_>>()
514        };
515        operands.einsum_subscripts(&subscripts, self.engine)
516    }
517
518    /// N-ary einsum from integer labels, evaluated on this session.
519    ///
520    /// The labels are validated by the same helper the compatibility frontend
521    /// uses; the evaluation is tenferro-einsum's session-direct einsum, so it does
522    /// not compile a semantic graph or start a runtime worker.
523    ///
524    /// # Errors
525    ///
526    /// Returns [`tenferro_einsum::Error::InvalidSubscripts`] for malformed labels, a
527    /// label list whose length does not match the operand's rank, or a label-list
528    /// count that does not match the operand count, and
529    /// [`tenferro_einsum::Error::Tensor`] when the backend rejects the operands, when an
530    /// operand cannot be promoted, or when the contraction itself fails.
531    ///
532    /// Operands are promoted to a common dtype exactly as the compatibility frontend
533    /// promotes them: a heterogeneous set contracts in the promoted dtype, and a set
534    /// that already shares one dtype is used as is.
535    ///
536    /// # Examples
537    ///
538    /// ```
539    /// use tensor4all_tensorbackend::CpuExecutionContext;
540    /// use tenferro::Tensor;
541    /// use tensor4all_tensorbackend::CpuBackend;
542    ///
543    /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
544    /// let a = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0])?;
545    /// let b = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 1.0])?;
546    /// let product = context.with_concrete_session(|session| {
547    ///     session.einsum(&[&a, &b], &[&[0, 1], &[1, 2]], &[0, 2])
548    /// })??;
549    /// assert_eq!(product.as_slice::<f64>()?, &[1.0, 2.0, 3.0, 4.0]);
550    /// # Ok::<(), Box<dyn std::error::Error>>(())
551    /// ```
552    pub fn einsum(
553        &mut self,
554        operands: &[&NativeTensor],
555        input_ids: &[&[usize]],
556        output_ids: &[usize],
557    ) -> tenferro_einsum::Result<NativeTensor> {
558        if operands.len() != input_ids.len() {
559            return Err(tenferro_einsum::Error::InvalidSubscripts {
560                message: format!(
561                    "einsum needs one label list per operand: {} operands, {} label lists",
562                    operands.len(),
563                    input_ids.len()
564                ),
565            });
566        }
567        let inputs = input_ids
568            .iter()
569            .map(|ids| checked_native_einsum_labels(ids))
570            .collect::<anyhow::Result<Vec<_>>>()
571            .map_err(|error| tenferro_einsum::Error::InvalidSubscripts {
572                message: format!("{error}"),
573            })?;
574        let subscripts = EinsumSubscripts {
575            inputs,
576            output: checked_native_einsum_labels(output_ids).map_err(|error| {
577                tenferro_einsum::Error::InvalidSubscripts {
578                    message: format!("{error}"),
579                }
580            })?,
581        };
582        let promoted = promote_operands(self.engine, operands)?;
583        let operands: Vec<&NativeTensor> = if promoted.is_empty() {
584            operands.to_vec()
585        } else {
586            promoted.iter().collect()
587        };
588        operands.einsum_subscripts(&subscripts, self.engine)
589    }
590
591    /// Sum every element of a concrete tensor, returning the rank-0 result.
592    ///
593    /// The compatibility frontend wraps the same rank-0 tensor in a
594    /// `BackendScalar`; this route stays on the concrete value so it works without
595    /// the compatibility feature.
596    ///
597    /// # Errors
598    ///
599    /// Returns [`tenferro_tensor::Error::Validation`] for an unsupported reduction
600    /// shape and [`tenferro_tensor::Error::BackendSource`] when the reduction itself
601    /// fails.
602    ///
603    /// # Examples
604    ///
605    /// ```
606    /// use tensor4all_tensorbackend::CpuExecutionContext;
607    /// use tenferro::Tensor;
608    /// use tensor4all_tensorbackend::CpuBackend;
609    ///
610    /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
611    /// let x = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0])?;
612    /// let total = context.with_concrete_session(|session| session.sum(&x))??;
613    /// assert_eq!(total.shape(), &[] as &[usize]);
614    /// assert_eq!(total.as_slice::<f64>()?, &[10.0]);
615    /// # Ok::<(), Box<dyn std::error::Error>>(())
616    /// ```
617    pub fn sum(&mut self, tensor: &NativeTensor) -> tenferro_tensor::Result<NativeTensor> {
618        if tensor.shape().is_empty() {
619            return tensor.duplicate();
620        }
621        let axes = (0..tensor.shape().len()).collect::<Vec<_>>();
622        tensor.reduce_sum(Some(&axes), self.engine)
623    }
624
625    /// Conjugate a concrete tensor.
626    ///
627    /// A real tensor is returned unchanged; a complex tensor is conjugated
628    /// elementwise.
629    ///
630    /// # Errors
631    ///
632    /// Returns [`tenferro_tensor::Error::Unsupported`] for a dtype the backend cannot
633    /// conjugate and [`tenferro_tensor::Error::BackendSource`] when the operation
634    /// itself fails.
635    ///
636    /// # Examples
637    ///
638    /// ```
639    /// use tensor4all_tensorbackend::CpuExecutionContext;
640    /// use tenferro::Tensor;
641    /// use tensor4all_tensorbackend::CpuBackend;
642    /// use num_complex::Complex64;
643    ///
644    /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
645    /// let x = Tensor::from_vec_col_major(
646    ///     vec![2],
647    ///     vec![Complex64::new(1.0, 2.0), Complex64::new(3.0, -4.0)],
648    /// )?;
649    /// let conjugated = context.with_concrete_session(|session| session.conj(&x))??;
650    /// assert_eq!(
651    ///     conjugated.as_slice::<Complex64>()?,
652    ///     &[Complex64::new(1.0, -2.0), Complex64::new(3.0, 4.0)]
653    /// );
654    /// # Ok::<(), Box<dyn std::error::Error>>(())
655    /// ```
656    pub fn conj(&mut self, tensor: &NativeTensor) -> tenferro_tensor::Result<NativeTensor> {
657        tensor.conj(self.engine)
658    }
659
660    /// N-ary einsum over borrowed read inputs, without materializing them.
661    ///
662    /// Same contract as [`Session::einsum`], except that the operands are
663    /// [`TensorRead`](tenferro_tensor::TensorRead) views: a non-contiguous or lazily
664    /// represented operand stays borrowed until the contract evaluates it.
665    ///
666    /// # Errors
667    ///
668    /// Returns [`tenferro_einsum::Error::InvalidSubscripts`] for malformed labels or a
669    /// label list whose length does not match the operand's rank, and
670    /// [`tenferro_einsum::Error::Tensor`] when the backend rejects the operands or the
671    /// contraction itself fails.
672    ///
673    /// # Examples
674    ///
675    /// ```
676    /// use tensor4all_tensorbackend::CpuExecutionContext;
677    /// use tenferro::Tensor;
678    /// use tensor4all_tensorbackend::CpuBackend;
679    /// use tenferro_tensor::TensorRead;
680    ///
681    /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
682    /// let a = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0])?;
683    /// let b = Tensor::from_vec_col_major(vec![2, 1], vec![5.0_f64, 6.0])?;
684    /// let reads = [TensorRead::from_tensor(&a), TensorRead::from_tensor(&b)];
685    /// let product = context
686    ///     .with_concrete_session(|session| session.einsum_reads(&reads, &[&[0, 1], &[1, 2]], &[0, 2]))??;
687    /// assert_eq!(product.as_slice::<f64>()?, &[23.0, 34.0]);
688    /// # Ok::<(), Box<dyn std::error::Error>>(())
689    /// ```
690    pub fn einsum_reads(
691        &mut self,
692        operands: &[tenferro_tensor::TensorRead<'_>],
693        input_ids: &[&[usize]],
694        output_ids: &[usize],
695    ) -> tenferro_einsum::Result<NativeTensor> {
696        if operands.len() != input_ids.len() {
697            return Err(tenferro_einsum::Error::InvalidSubscripts {
698                message: format!(
699                    "einsum needs one label list per operand: {} operands, {} label lists",
700                    operands.len(),
701                    input_ids.len()
702                ),
703            });
704        }
705        let invalid = |error: anyhow::Error| tenferro_einsum::Error::InvalidSubscripts {
706            message: format!("{error}"),
707        };
708        let inputs = input_ids
709            .iter()
710            .map(|ids| checked_native_einsum_labels(ids))
711            .collect::<anyhow::Result<Vec<_>>>()
712            .map_err(invalid)?;
713        let output = checked_native_einsum_labels(output_ids).map_err(invalid)?;
714        let subscripts = EinsumSubscripts { inputs, output };
715        let plan = ConcreteEinsumPlan::prepare_read_subscripts(operands, &subscripts)?;
716        plan.execute_read(operands, self.engine)
717    }
718
719    /// Binary contraction into a caller-provided destination.
720    ///
721    /// The destination is fully written and is never zero-filled first. The operands
722    /// and the destination must already share one dtype: this route does not convert
723    /// into a caller-owned output, and a mismatch is rejected typed by the backend.
724    ///
725    /// # Errors
726    ///
727    /// Returns [`tenferro_einsum::Error::InvalidSubscripts`] when the axis lists are
728    /// malformed or of different length, and [`tenferro_einsum::Error::Tensor`] when the
729    /// backend rejects the operands, the destination or the contraction.
730    ///
731    /// # Examples
732    ///
733    /// ```
734    /// use tensor4all_tensorbackend::CpuExecutionContext;
735    /// use tenferro::Tensor;
736    /// use tensor4all_tensorbackend::CpuBackend;
737    /// use tenferro_tensor::TensorWrite;
738    ///
739    /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
740    /// let a = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0])?;
741    /// let b = Tensor::from_vec_col_major(vec![2, 1], vec![5.0_f64, 6.0])?;
742    /// let mut out = Tensor::from_vec_col_major(vec![2, 1], vec![0.0_f64, 0.0])?;
743    /// context.with_concrete_session(|session| {
744    ///     session.contraction_into(&a, &[1], &b, &[0], TensorWrite::Tensor(&mut out))
745    /// })??;
746    /// assert_eq!(out.as_slice::<f64>()?, &[23.0, 34.0]);
747    /// # Ok::<(), Box<dyn std::error::Error>>(())
748    /// ```
749    pub fn contraction_into(
750        &mut self,
751        lhs: &NativeTensor,
752        lhs_axes: &[usize],
753        rhs: &NativeTensor,
754        rhs_axes: &[usize],
755        out: tenferro_tensor::TensorWrite<'_>,
756    ) -> tenferro_einsum::Result<()> {
757        let (lhs_ids, rhs_ids, output_ids) =
758            build_binary_einsum_ids(lhs.shape().len(), lhs_axes, rhs.shape().len(), rhs_axes)
759                .map_err(|error| tenferro_einsum::Error::InvalidSubscripts {
760                    message: format!("{error}"),
761                })?;
762        let subscripts = EinsumSubscripts {
763            inputs: vec![lhs_ids, rhs_ids],
764            output: output_ids,
765        };
766        let inputs = [
767            tenferro_tensor::TensorRead::from_tensor(lhs),
768            tenferro_tensor::TensorRead::from_tensor(rhs),
769        ];
770        let plan = ConcreteEinsumPlan::prepare_read_subscripts(&inputs[..], &subscripts)?;
771        plan.execute_read_into(&inputs[..], self.engine, out)
772    }
773
774    /// Solve `A X = B` for column-major matrices on this session.
775    ///
776    /// Same kernel, conversion and validation as
777    /// [`solve_matrix`](crate::solve_matrix), entered through this session instead of
778    /// the process-global one.
779    ///
780    /// # Errors
781    ///
782    /// Returns [`BackendLinalgError`](crate::BackendLinalgError) under the same
783    /// conditions as [`solve_matrix`](crate::solve_matrix).
784    ///
785    /// # Examples
786    ///
787    /// ```
788    /// use tensor4all_tensorbackend::{CpuExecutionContext, Matrix};
789    /// use tensor4all_tensorbackend::CpuBackend;
790    ///
791    /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
792    /// let a = Matrix::from_col_major_vec(2, 2, vec![2.0_f64, 0.0, 0.0, 4.0]);
793    /// let b = Matrix::from_col_major_vec(2, 1, vec![6.0_f64, 8.0]);
794    /// let x = context.with_concrete_session(|session| session.solve_matrix(&a, &b))??;
795    /// assert_eq!(x.as_col_major_slice(), &[3.0, 2.0]);
796    /// # Ok::<(), Box<dyn std::error::Error>>(())
797    /// ```
798    #[cfg(feature = "global-defaults")]
799    pub fn solve_matrix<T>(
800        &mut self,
801        a: &crate::matrix::Matrix<T>,
802        b: &crate::matrix::Matrix<T>,
803    ) -> Result<crate::matrix::Matrix<T>, crate::backend::BackendLinalgError>
804    where
805        T: crate::backend::BackendLinalgScalar + Copy,
806        NativeTensor: From<tenferro::TypedTensor<T>>,
807    {
808        crate::solve_matrix_in(self.engine, a, b)
809    }
810
811    /// Solve a triangular system for column-major matrices on this session.
812    ///
813    /// Same kernel, conversion and validation as
814    /// [`triangular_solve_matrix`](crate::triangular_solve_matrix), entered through this
815    /// session instead of the process-global one. `left_side`, `lower`, `transpose_a`
816    /// and `unit_diagonal` have the same meaning as on that entry.
817    ///
818    /// # Errors
819    ///
820    /// Returns [`BackendLinalgError`](crate::BackendLinalgError) under the same
821    /// conditions as [`triangular_solve_matrix`](crate::triangular_solve_matrix).
822    ///
823    /// # Examples
824    ///
825    /// ```
826    /// use tensor4all_tensorbackend::{CpuExecutionContext, Matrix};
827    /// use tensor4all_tensorbackend::CpuBackend;
828    ///
829    /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
830    /// let a = Matrix::from_col_major_vec(2, 2, vec![2.0_f64, 1.0, 0.0, 4.0]);
831    /// let b = Matrix::from_col_major_vec(2, 1, vec![4.0_f64, 4.0]);
832    /// let x = context.with_concrete_session(|session| {
833    ///     session.triangular_solve_matrix(&a, &b, true, true, false, false)
834    /// })??;
835    /// assert_eq!(x.as_col_major_slice(), &[2.0, 0.5]);
836    /// # Ok::<(), Box<dyn std::error::Error>>(())
837    /// ```
838    #[cfg(feature = "global-defaults")]
839    #[allow(clippy::too_many_arguments)]
840    pub fn triangular_solve_matrix<T>(
841        &mut self,
842        a: &crate::matrix::Matrix<T>,
843        b: &crate::matrix::Matrix<T>,
844        left_side: bool,
845        lower: bool,
846        transpose_a: bool,
847        unit_diagonal: bool,
848    ) -> Result<crate::matrix::Matrix<T>, crate::backend::BackendLinalgError>
849    where
850        T: crate::backend::BackendLinalgScalar + Copy,
851        NativeTensor: From<tenferro::TypedTensor<T>>,
852    {
853        crate::triangular_solve_matrix_in(
854            self.engine,
855            a,
856            b,
857            left_side,
858            lower,
859            transpose_a,
860            unit_diagonal,
861        )
862    }
863
864    /// Complete-pivoting LU decomposition of a column-major matrix on this session.
865    ///
866    /// Same kernel and conversion as
867    /// [`full_piv_lu_matrix`](crate::full_piv_lu_matrix), entered through this session
868    /// instead of the process-global one.
869    ///
870    /// # Errors
871    ///
872    /// Returns [`BackendLinalgError`](crate::BackendLinalgError) under the same
873    /// conditions as [`full_piv_lu_matrix`](crate::full_piv_lu_matrix).
874    ///
875    /// # Examples
876    ///
877    /// ```
878    /// use tensor4all_tensorbackend::{CpuExecutionContext, Matrix};
879    /// use tensor4all_tensorbackend::CpuBackend;
880    ///
881    /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
882    /// let a = Matrix::from_col_major_vec(2, 2, vec![0.0_f64, 1.0, 2.0, 3.0]);
883    /// let factors = context.with_concrete_session(|session| session.full_piv_lu_matrix(&a))??;
884    /// assert_eq!(factors.p.nrows(), 2);
885    /// assert_eq!(factors.q.ncols(), 2);
886    /// # Ok::<(), Box<dyn std::error::Error>>(())
887    /// ```
888    #[cfg(feature = "global-defaults")]
889    pub fn full_piv_lu_matrix<T>(
890        &mut self,
891        a: &crate::matrix::Matrix<T>,
892    ) -> Result<crate::backend::FullPivLuMatrixResult<T>, crate::backend::BackendLinalgError>
893    where
894        T: crate::backend::BackendLinalgScalar + Copy,
895    {
896        crate::full_piv_lu_matrix_in(self.engine, a)
897    }
898
899    /// Thin/economy QR of a typed tensor on this session.
900    ///
901    /// Same kernel and conversion as [`qr_backend`](crate::qr_backend), entered through
902    /// this session instead of the process-global one.
903    ///
904    /// # Errors
905    ///
906    /// Returns [`BackendLinalgError`](crate::BackendLinalgError) under the same
907    /// conditions as [`qr_backend`](crate::qr_backend).
908    ///
909    /// # Examples
910    ///
911    /// ```
912    /// use tensor4all_tensorbackend::CpuExecutionContext;
913    /// use tenferro::TypedTensor;
914    /// use tensor4all_tensorbackend::CpuBackend;
915    ///
916    /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
917    /// let a = TypedTensor::<f64>::from_vec_col_major(vec![2, 2], vec![1.0, 2.0, 3.0, 4.0])?;
918    /// let (q, r) = context.with_concrete_session(|session| session.qr_backend(a))??;
919    /// assert_eq!(q.shape(), &[2, 2]);
920    /// assert_eq!(r.shape(), &[2, 2]);
921    /// # Ok::<(), Box<dyn std::error::Error>>(())
922    /// ```
923    #[cfg(feature = "global-defaults")]
924    pub fn qr_backend<T>(
925        &mut self,
926        a: tenferro::TypedTensor<T>,
927    ) -> Result<
928        (tenferro::TypedTensor<T>, tenferro::TypedTensor<T>),
929        crate::backend::BackendLinalgError,
930    >
931    where
932        T: crate::backend::BackendLinalgScalar,
933    {
934        use tenferro::TensorScalar;
935
936        let (shape, data) = a.into_vec_col_major().map_err(|error| {
937            crate::backend::BackendLinalgError::from(anyhow::anyhow!(
938                "QR input host access failed: {error}"
939            ))
940        })?;
941        let tensor: NativeTensor =
942            <T as TensorScalar>::into_tensor(shape, data).map_err(|error| {
943                crate::backend::BackendLinalgError::from(anyhow::anyhow!(
944                    "QR input tensor construction failed: {error}"
945                ))
946            })?;
947        crate::qr_backend_in(self.engine, tensor)
948    }
949
950    /// Thin/economy SVD of a typed tensor on this session.
951    ///
952    /// Same kernel and conversion as [`svd_backend`](crate::svd_backend), entered through
953    /// this session instead of the process-global one.
954    ///
955    /// # Errors
956    ///
957    /// Returns [`BackendLinalgError`](crate::BackendLinalgError) under the same
958    /// conditions as [`svd_backend`](crate::svd_backend).
959    ///
960    /// # Examples
961    ///
962    /// ```
963    /// use tensor4all_tensorbackend::CpuExecutionContext;
964    /// use tenferro::TypedTensor;
965    /// use tensor4all_tensorbackend::CpuBackend;
966    ///
967    /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
968    /// let a = TypedTensor::<f64>::from_vec_col_major(vec![2, 2], vec![3.0, 0.0, 0.0, 2.0])?;
969    /// let factors = context.with_concrete_session(|session| session.svd_backend(&a))??;
970    /// assert_eq!(factors.s().as_slice()?, &[3.0, 2.0]);
971    /// # Ok::<(), Box<dyn std::error::Error>>(())
972    /// ```
973    #[cfg(feature = "global-defaults")]
974    pub fn svd_backend<T>(
975        &mut self,
976        a: &tenferro::TypedTensor<T>,
977    ) -> Result<crate::backend::SvdResult<T>, crate::backend::BackendLinalgError>
978    where
979        T: crate::backend::BackendLinalgScalar,
980    {
981        use tenferro::TensorScalar;
982
983        let tensor: NativeTensor = <T as TensorScalar>::into_tensor(
984            a.shape().to_vec(),
985            a.host_data()
986                .map_err(|error| {
987                    crate::backend::BackendLinalgError::from(anyhow::anyhow!(
988                        "SVD input host access failed: {error}"
989                    ))
990                })?
991                .to_vec(),
992        )
993        .map_err(|error| {
994            crate::backend::BackendLinalgError::from(anyhow::anyhow!(
995                "SVD input tensor construction failed: {error}"
996            ))
997        })?;
998        crate::svd_backend_in(self.engine, tensor)
999    }
1000
1001    /// Matrix multiplication `A * B` on this session.
1002    ///
1003    /// The same kernel and shape validation as
1004    /// [`mat_mul`](crate::mat_mul), entered through this session instead of the
1005    /// process-global one, and returning the shared
1006    /// [`Matrix`](crate::Matrix) container.
1007    ///
1008    /// # Errors
1009    ///
1010    /// Returns [`MatrixMulError`](crate::MatrixMulError) when the shapes disagree or
1011    /// the session operation fails.
1012    ///
1013    /// # Examples
1014    ///
1015    /// ```
1016    /// use tensor4all_tensorbackend::{CpuExecutionContext, Matrix};
1017    /// use tensor4all_tensorbackend::CpuBackend;
1018    ///
1019    /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
1020    /// let a = Matrix::from_col_major_vec(2, 2, vec![1.0_f64, 2.0, 3.0, 4.0]);
1021    /// let b = Matrix::from_col_major_vec(2, 2, vec![1.0_f64, 0.0, 0.0, 1.0]);
1022    /// let c = context.with_concrete_session(|session| session.mat_mul(&a, &b))??;
1023    /// assert_eq!(c.as_col_major_slice(), &[1.0, 2.0, 3.0, 4.0]);
1024    /// # Ok::<(), Box<dyn std::error::Error>>(())
1025    /// ```
1026    #[cfg(feature = "global-defaults")]
1027    pub fn mat_mul<T: crate::matrix::BlasMul>(
1028        &mut self,
1029        a: &crate::matrix::Matrix<T>,
1030        b: &crate::matrix::Matrix<T>,
1031    ) -> Result<crate::matrix::Matrix<T>, crate::matrix::MatrixMulError> {
1032        crate::mat_mul_in(self.engine, a, b)
1033    }
1034
1035    /// Thin/economy QR decomposition.
1036    ///
1037    /// # Errors
1038    ///
1039    /// Returns [`tenferro_tensor::Error::Validation`] for an unsupported dtype or
1040    /// rank, and [`tenferro_tensor::Error::BackendSource`] when the factorization
1041    /// itself fails.
1042    ///
1043    /// # Examples
1044    ///
1045    /// ```
1046    /// use tensor4all_tensorbackend::CpuExecutionContext;
1047    /// use tenferro::Tensor;
1048    /// use tensor4all_tensorbackend::CpuBackend;
1049    ///
1050    /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
1051    /// let a = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0])?;
1052    /// let (q, r) = context.with_concrete_session(|session| session.qr(&a))??;
1053    /// // `A = Q R` is reproduced to rounding, which is the correctness property
1054    /// // of a factorization.
1055    /// let reconstructed =
1056    ///     context.with_concrete_session(|session| session.contraction(&q, &[1], &r, &[0]))??;
1057    /// for (got, want) in reconstructed.as_slice::<f64>()?.iter().zip([1.0, 2.0, 3.0, 4.0]) {
1058    ///     assert!((got - want).abs() < 1e-12, "got {got}, want {want}");
1059    /// }
1060    /// # Ok::<(), Box<dyn std::error::Error>>(())
1061    /// ```
1062    pub fn qr(
1063        &mut self,
1064        tensor: &NativeTensor,
1065    ) -> tenferro_tensor::Result<(NativeTensor, NativeTensor)> {
1066        tensor.qr(self.engine)
1067    }
1068
1069    /// Thin/economy SVD.
1070    ///
1071    /// # Errors
1072    ///
1073    /// Returns [`tenferro_tensor::Error::Validation`] for an unsupported dtype or
1074    /// rank, and [`tenferro_tensor::Error::BackendSource`] when the factorization
1075    /// itself fails.
1076    ///
1077    /// # Examples
1078    ///
1079    /// ```
1080    /// use tensor4all_tensorbackend::CpuExecutionContext;
1081    /// use tenferro::Tensor;
1082    /// use tensor4all_tensorbackend::CpuBackend;
1083    ///
1084    /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
1085    /// let a = Tensor::from_vec_col_major(vec![2, 2], vec![3.0_f64, 0.0, 0.0, 2.0])?;
1086    /// let (u, s, vt) = context.with_concrete_session(|session| session.svd(&a))??;
1087    /// // The singular values of diag(3, 2) are 3 and 2.
1088    /// assert_eq!(s.as_slice::<f64>()?, &[3.0, 2.0]);
1089    /// assert_eq!(u.shape(), &[2, 2]);
1090    /// assert_eq!(vt.shape(), &[2, 2]);
1091    /// # Ok::<(), Box<dyn std::error::Error>>(())
1092    /// ```
1093    pub fn svd(
1094        &mut self,
1095        tensor: &NativeTensor,
1096    ) -> tenferro_tensor::Result<(NativeTensor, NativeTensor, NativeTensor)> {
1097        tensor.svd(self.engine)
1098    }
1099
1100    /// Solve `A X = B` for a square `A`.
1101    ///
1102    /// # Errors
1103    ///
1104    /// Returns [`tenferro_tensor::Error::Validation`] when `lhs` is not square, when
1105    /// the operands have mismatched shapes, or for an unsupported dtype, and
1106    /// [`tenferro_tensor::Error::BackendSource`] when the solve itself fails.
1107    ///
1108    /// # Examples
1109    ///
1110    /// ```
1111    /// use tensor4all_tensorbackend::CpuExecutionContext;
1112    /// use tenferro::Tensor;
1113    /// use tensor4all_tensorbackend::CpuBackend;
1114    ///
1115    /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
1116    /// let a = Tensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 0.0, 4.0])?;
1117    /// let b = Tensor::from_vec_col_major(vec![2, 1], vec![6.0_f64, 8.0])?;
1118    /// let x = context.with_concrete_session(|session| session.solve(&a, &b))??;
1119    /// assert_eq!(x.as_slice::<f64>()?, &[3.0, 2.0]);
1120    /// # Ok::<(), Box<dyn std::error::Error>>(())
1121    /// ```
1122    pub fn solve(
1123        &mut self,
1124        lhs: &NativeTensor,
1125        rhs: &NativeTensor,
1126    ) -> tenferro_tensor::Result<NativeTensor> {
1127        lhs.solve(rhs, self.engine)
1128    }
1129
1130    /// Solve `A X = B` where `A` is triangular.
1131    ///
1132    /// - `left_side`: solve `A X = B` when true, `X A = B` when false;
1133    /// - `lower`: `A` is lower triangular;
1134    /// - `transpose_a`: solve with `A` transposed;
1135    /// - `unit_diagonal`: treat the diagonal of `A` as one, ignoring the stored
1136    ///   values.
1137    ///
1138    /// # Errors
1139    ///
1140    /// Returns [`tenferro_tensor::Error::Validation`] for a shape mismatch or an
1141    /// unsupported dtype, and [`tenferro_tensor::Error::BackendSource`] when the solve
1142    /// itself fails.
1143    ///
1144    /// # Examples
1145    ///
1146    /// ```
1147    /// use tensor4all_tensorbackend::CpuExecutionContext;
1148    /// use tenferro::Tensor;
1149    /// use tensor4all_tensorbackend::CpuBackend;
1150    ///
1151    /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
1152    /// // Lower triangular, column-major.
1153    /// let a = Tensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 1.0, 0.0, 4.0])?;
1154    /// let b = Tensor::from_vec_col_major(vec![2, 1], vec![4.0_f64, 4.0])?;
1155    /// let x = context.with_concrete_session(|session| {
1156    ///     session.triangular_solve(&a, &b, true, true, false, false)
1157    /// })??;
1158    /// assert_eq!(x.as_slice::<f64>()?, &[2.0, 0.5]);
1159    /// # Ok::<(), Box<dyn std::error::Error>>(())
1160    /// ```
1161    pub fn triangular_solve(
1162        &mut self,
1163        lhs: &NativeTensor,
1164        rhs: &NativeTensor,
1165        left_side: bool,
1166        lower: bool,
1167        transpose_a: bool,
1168        unit_diagonal: bool,
1169    ) -> tenferro_tensor::Result<NativeTensor> {
1170        lhs.triangular_solve(
1171            rhs,
1172            left_side,
1173            lower,
1174            transpose_a,
1175            unit_diagonal,
1176            self.engine,
1177        )
1178    }
1179
1180    /// Full-pivoting LU decomposition `P A Q = L U`, returning
1181    /// `(P, L, U, Q, parity)`.
1182    ///
1183    /// # Errors
1184    ///
1185    /// Returns [`tenferro_tensor::Error::Validation`] for a non-square `A` or an
1186    /// unsupported dtype, and [`tenferro_tensor::Error::BackendSource`] when the
1187    /// factorization itself fails.
1188    ///
1189    /// # Examples
1190    ///
1191    /// ```
1192    /// use tensor4all_tensorbackend::CpuExecutionContext;
1193    /// use tenferro::Tensor;
1194    /// use tensor4all_tensorbackend::CpuBackend;
1195    ///
1196    /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
1197    /// let a = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0])?;
1198    /// let (p, l, u, q, _parity) = context
1199    ///     .with_concrete_session(|session| session.full_piv_lu(&a))?
1200    ///     .expect("full-pivoting LU");
1201    /// for factor in [&p, &l, &u, &q] {
1202    ///     assert_eq!(factor.shape(), &[2, 2]);
1203    /// }
1204    /// // Column-major, so index 0 is (0, 0), index 1 is (1, 0) and index 2 is (0, 1).
1205    /// let l = l.as_slice::<f64>()?;
1206    /// let u = u.as_slice::<f64>()?;
1207    /// assert_eq!(l[2], 0.0, "L is lower triangular");
1208    /// assert_eq!(u[1], 0.0, "U is upper triangular");
1209    /// # Ok::<(), Box<dyn std::error::Error>>(())
1210    /// ```
1211    pub fn full_piv_lu(
1212        &mut self,
1213        tensor: &NativeTensor,
1214    ) -> tenferro_tensor::Result<(
1215        NativeTensor,
1216        NativeTensor,
1217        NativeTensor,
1218        NativeTensor,
1219        NativeTensor,
1220    )> {
1221        tensor.full_piv_lu(self.engine)
1222    }
1223}
1224
1225impl CpuExecutionContext {
1226    /// Run `f` inside one explicit concrete session of this context.
1227    ///
1228    /// The context's engine is borrowed once for the whole closure, so a stage
1229    /// borrows it once and reuses its prepared plans and buffer pool. All routes on
1230    /// the [`Session`] run on that engine only: nothing here consults the
1231    /// process-global default context, and nothing falls back to it.
1232    ///
1233    /// # Examples
1234    ///
1235    /// ```
1236    /// use tensor4all_tensorbackend::CpuExecutionContext;
1237    /// use tenferro::Tensor;
1238    /// use tensor4all_tensorbackend::CpuBackend;
1239    ///
1240    /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
1241    /// let x = Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 3.0])?;
1242    /// let doubled = context.with_concrete_session(|session| session.reshape(&x, &[2]))??;
1243    /// assert_eq!(doubled.as_slice::<f64>()?, &[1.0, 3.0]);
1244    /// # Ok::<(), Box<dyn std::error::Error>>(())
1245    /// ```
1246    ///
1247    /// # Errors
1248    ///
1249    /// Returns [`CpuExecutionContextError::Reentered`] when the calling thread is
1250    /// already inside a session of this context. A session entered from a Rayon
1251    /// worker runs inline on a serial engine that shares the context engine's
1252    /// prepared-plan cache, so a worker entry completes instead of waiting for a
1253    /// pool it belongs to (tensor4all-rs#830).
1254    ///
1255    /// # Panics
1256    ///
1257    /// Never. A panic in `f` propagates after the engine borrow is released.
1258    pub fn with_concrete_session<R>(
1259        &self,
1260        f: impl FnOnce(&mut Session<'_>) -> R,
1261    ) -> Result<R, CpuExecutionContextError> {
1262        self.with_session(|engine| f(&mut Session::new(engine)))
1263    }
1264}
1265
1266#[cfg(test)]
1267mod tests;