Skip to main content

tenferro_ad/
eager_ops.rs

1use std::sync::Arc;
2
3use computegraph::GraphOperation;
4use num_complex::{Complex32, Complex64};
5use tenferro_ops::broadcast::{
6    broadcast_error_to_validation, broadcast_in_dim_extent_error, broadcast_input_plan,
7    broadcast_shape, broadcast_shapes,
8};
9use tenferro_ops::dim_expr::DimExpr;
10use tenferro_ops::std_tensor_op::StdTensorOp;
11use tenferro_tensor::{
12    DType, DotGeneralConfig, GatherConfig, PadConfig, ScatterConfig, SliceConfig, Tensor,
13    TensorValue,
14};
15
16use crate::eager::{
17    eager_capture_active, eager_grad_recording_enabled, eager_op_profile_start, exec_single_output,
18    exec_single_output_read, maybe_print_eager_op_profile, profile_eager_op_section,
19    record_eager_op_profile, record_eager_outputs, record_eager_value_outputs, EagerTensor,
20};
21use crate::eager_exec::exec_dot_general_with_conj_on_tensor_reads;
22use crate::error::{Error, Result};
23
24pub(crate) fn broadcast_binary(
25    op: &'static str,
26    lhs: &EagerTensor,
27    rhs: &EagerTensor,
28) -> Result<(EagerTensor, EagerTensor)> {
29    ensure_same_context(lhs, rhs)?;
30    let shape =
31        broadcast_shape(lhs.shape(), rhs.shape()).map_err(|err| broadcast_error(op, err))?;
32    Ok((
33        broadcast_to(op, lhs, &shape)?,
34        broadcast_to(op, rhs, &shape)?,
35    ))
36}
37
38pub(crate) fn broadcast_ternary(
39    op: &'static str,
40    first: &EagerTensor,
41    second: &EagerTensor,
42    third: &EagerTensor,
43) -> Result<(EagerTensor, EagerTensor, EagerTensor)> {
44    ensure_same_context(first, second)?;
45    ensure_same_context(first, third)?;
46    let shape = broadcast_shapes([first.shape(), second.shape(), third.shape()])
47        .map_err(|err| broadcast_error(op, err))?;
48    Ok((
49        broadcast_to(op, first, &shape)?,
50        broadcast_to(op, second, &shape)?,
51        broadcast_to(op, third, &shape)?,
52    ))
53}
54
55fn broadcast_to(
56    op: &'static str,
57    input: &EagerTensor,
58    target_shape: &[usize],
59) -> Result<EagerTensor> {
60    let input_shape = input.shape();
61    if input_shape == target_shape {
62        return Ok(input.clone());
63    }
64
65    let plan =
66        broadcast_input_plan(input_shape, target_shape).map_err(|err| broadcast_error(op, err))?;
67    let source = if plan.source_shape == input_shape {
68        input.clone()
69    } else {
70        input.reshape(&plan.source_shape)?
71    };
72    source.broadcast_in_dim(target_shape, &plan.dims)
73}
74
75fn broadcast_error(op: &'static str, err: tenferro_ops::broadcast::BroadcastError) -> Error {
76    tenferro_tensor::Error::validation(op, broadcast_error_to_validation(err)).into()
77}
78
79fn ensure_same_context(lhs: &EagerTensor, rhs: &EagerTensor) -> Result<()> {
80    if !lhs.same_context(rhs) {
81        return Err(Error::ContextMismatch {
82            lhs: lhs.ctx_id(),
83            rhs: rhs.ctx_id(),
84        });
85    }
86    Ok(())
87}
88
89impl std::ops::Add for &EagerTensor {
90    type Output = Result<EagerTensor>;
91
92    fn add(self, rhs: &EagerTensor) -> Result<EagerTensor> {
93        EagerTensor::add(self, rhs)
94    }
95}
96
97impl std::ops::Sub for &EagerTensor {
98    type Output = Result<EagerTensor>;
99
100    fn sub(self, rhs: &EagerTensor) -> Result<EagerTensor> {
101        EagerTensor::sub(self, rhs)
102    }
103}
104
105impl std::ops::Mul for &EagerTensor {
106    type Output = Result<EagerTensor>;
107
108    fn mul(self, rhs: &EagerTensor) -> Result<EagerTensor> {
109        EagerTensor::mul(self, rhs)
110    }
111}
112
113impl std::ops::Div for &EagerTensor {
114    type Output = Result<EagerTensor>;
115
116    fn div(self, rhs: &EagerTensor) -> Result<EagerTensor> {
117        EagerTensor::div(self, rhs)
118    }
119}
120
121impl std::ops::Rem for &EagerTensor {
122    type Output = Result<EagerTensor>;
123
124    fn rem(self, rhs: &EagerTensor) -> Result<EagerTensor> {
125        EagerTensor::rem(self, rhs)
126    }
127}
128
129impl std::ops::Neg for &EagerTensor {
130    type Output = Result<EagerTensor>;
131
132    fn neg(self) -> Result<EagerTensor> {
133        EagerTensor::neg(self)
134    }
135}
136
137impl EagerTensor {
138    /// Elementwise addition.
139    ///
140    /// # Examples
141    ///
142    /// ```
143    /// use tenferro_cpu::CpuBackend;
144    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
145    ///
146    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
147    /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap(), ctx.clone()).unwrap();
148    /// let y = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2], vec![3.0_f64, 4.0]).unwrap(), ctx.clone()).unwrap();
149    /// let z = x.add(&y).unwrap();
150    ///
151    /// assert_eq!(z.value().unwrap().as_slice::<f64>().unwrap(), &[4.0, 6.0]);
152    /// # Ok::<(), tenferro_ad::Error>(())
153    /// ```
154    ///
155    /// # Errors
156    ///
157    /// Returns [`Error::ContextMismatch`] for tensors from different eager
158    /// runtimes, [`tenferro_tensor::Error::Validation`] with
159    /// `ShapeMismatch`/`DTypeMismatch` for incompatible operands, or a typed
160    /// backend/runtime-state error during execution.
161    pub fn add(&self, other: &Self) -> Result<Self> {
162        let (lhs, rhs) = broadcast_binary("add", self, other)?;
163        lhs.binary_op(&rhs, StdTensorOp::Add)
164    }
165
166    /// Elementwise subtraction.
167    ///
168    /// # Errors
169    ///
170    /// Returns [`Error::ContextMismatch`] for tensors from different eager
171    /// runtimes, [`tenferro_tensor::Error::Validation`] with
172    /// `ShapeMismatch`/`DTypeMismatch` for incompatible operands, or a typed
173    /// backend/runtime-state error during execution.
174    pub fn sub(&self, other: &Self) -> Result<Self> {
175        let (lhs, rhs) = broadcast_binary("sub", self, other)?;
176        lhs.binary_op(&rhs, StdTensorOp::Sub)
177    }
178
179    /// Elementwise multiplication.
180    ///
181    /// # Examples
182    ///
183    /// ```
184    /// use tenferro_cpu::CpuBackend;
185    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
186    ///
187    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
188    /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap(), ctx.clone()).unwrap();
189    /// let y = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2], vec![3.0_f64, 4.0]).unwrap(), ctx.clone()).unwrap();
190    /// let z = x.mul(&y).unwrap();
191    ///
192    /// assert_eq!(z.value().unwrap().as_slice::<f64>().unwrap(), &[3.0, 8.0]);
193    /// # Ok::<(), tenferro_ad::Error>(())
194    /// ```
195    ///
196    /// # Errors
197    ///
198    /// Returns [`Error::ContextMismatch`] for tensors from different eager
199    /// runtimes, [`tenferro_tensor::Error::Validation`] with
200    /// `ShapeMismatch`/`DTypeMismatch` for incompatible operands, or a typed
201    /// backend/runtime-state error during execution.
202    pub fn mul(&self, other: &Self) -> Result<Self> {
203        let (lhs, rhs) = broadcast_binary("mul", self, other)?;
204        lhs.binary_op(&rhs, StdTensorOp::Mul)
205    }
206
207    /// Negate the tensor.
208    ///
209    /// # Examples
210    ///
211    /// ```
212    /// use tenferro_cpu::CpuBackend;
213    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
214    ///
215    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
216    /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2], vec![1.0_f64, -2.0]).unwrap(), ctx.clone()).unwrap();
217    /// let y = x.neg().unwrap();
218    ///
219    /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[-1.0, 2.0]);
220    /// # Ok::<(), tenferro_ad::Error>(())
221    /// ```
222    ///
223    /// # Errors
224    ///
225    /// Returns [`tenferro_tensor::Error::Unsupported`] when the backend does
226    /// not implement negation for the dtype, or a typed backend/runtime-state
227    /// error during execution.
228    pub fn neg(&self) -> Result<Self> {
229        self.unary_op(StdTensorOp::Neg)
230    }
231
232    /// Elementwise exponential.
233    ///
234    /// # Examples
235    ///
236    /// ```
237    /// use tenferro_cpu::CpuBackend;
238    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
239    ///
240    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
241    /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![1], vec![0.0_f64]).unwrap(), ctx.clone()).unwrap();
242    /// let y = x.exp().unwrap();
243    ///
244    /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[1.0]);
245    /// # Ok::<(), tenferro_ad::Error>(())
246    /// ```
247    ///
248    /// # Errors
249    ///
250    /// Returns [`tenferro_tensor::Error::Unsupported`] when the backend does
251    /// not implement exponentiation for the dtype, or a typed backend/
252    /// runtime-state error during execution.
253    pub fn exp(&self) -> Result<Self> {
254        self.unary_op(StdTensorOp::Exp)
255    }
256
257    /// Reduce sum over the requested axes.
258    ///
259    /// # Examples
260    ///
261    /// ```
262    /// use tenferro_cpu::CpuBackend;
263    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
264    ///
265    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
266    /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0]).unwrap(), ctx.clone()).unwrap();
267    /// let y = x.reduce_sum(None).unwrap();
268    ///
269    /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[10.0]);
270    /// # Ok::<(), tenferro_ad::Error>(())
271    /// ```
272    ///
273    /// # Errors
274    ///
275    /// Returns [`tenferro_tensor::Error::Validation`] with `AxisOutOfBounds` or
276    /// `DuplicateAxis` for an invalid reduction axis, or a typed
277    /// unsupported/backend/runtime-state error for the selected dtype.
278    pub fn reduce_sum(&self, axes: Option<&[usize]>) -> Result<Self> {
279        let axes = axes.map_or_else(|| (0..self.shape().len()).collect(), <[usize]>::to_vec);
280        validate_eager_axes("EagerTensor::reduce_sum", self.shape().len(), &axes)?;
281        self.unary_op(StdTensorOp::ReduceSum { axes })
282    }
283
284    /// Sum elementwise squares over the requested axes.
285    ///
286    /// Each value is squared in its input dtype before reduction. The initial
287    /// supported dtypes are `f32` and `f64`; other dtypes return a typed
288    /// unsupported error. Passing an empty axis slice returns the elementwise
289    /// square without reducing rank.
290    ///
291    /// This operation is useful when the squared sum is needed directly. Use
292    /// the linalg norm APIs when a square root or complex magnitude semantics
293    /// are required.
294    ///
295    /// # Errors
296    ///
297    /// Returns a typed validation error for invalid axes, a typed unsupported
298    /// error for other dtypes, or a typed backend or runtime-state error during
299    /// execution.
300    pub fn reduce_sum_squares(&self, axes: &[usize]) -> Result<Self> {
301        validate_eager_axes("EagerTensor::reduce_sum_squares", self.shape().len(), axes)?;
302        self.unary_op(StdTensorOp::ReduceSumSquares {
303            axes: axes.to_vec(),
304        })
305    }
306
307    /// Execute a dot-general contraction eagerly.
308    ///
309    /// # Examples
310    ///
311    /// ```
312    /// use tenferro_cpu::CpuBackend;
313    /// use tenferro_ad::{DotGeneralConfig, EagerRuntime, EagerTensor, Tensor};
314    ///
315    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
316    /// let a = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2, 3], vec![1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap(), ctx.clone()).unwrap();
317    /// let b = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![3, 2], vec![1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap(), ctx.clone()).unwrap();
318    /// let c = a.dot_general(&b, DotGeneralConfig {
319    ///     lhs_contracting_dims: vec![1],
320    ///     rhs_contracting_dims: vec![0],
321    ///     lhs_batch_dims: vec![],
322    ///     rhs_batch_dims: vec![],
323    /// }).unwrap();
324    ///
325    /// assert_eq!(c.shape(), &[2, 2]);
326    /// # Ok::<(), tenferro_ad::Error>(())
327    /// ```
328    ///
329    /// # Errors
330    ///
331    /// Returns [`tenferro_tensor::Error::Validation`] with `RankMismatch`,
332    /// `AxisOutOfBounds`, `DuplicateAxis`, `ShapeMismatch`, or `DTypeMismatch`
333    /// when `config` or the operands are invalid; backend and runtime-state
334    /// failures retain their typed sources.
335    pub fn dot_general(&self, other: &Self, config: DotGeneralConfig) -> Result<Self> {
336        validate_eager_dot_general_config(
337            "EagerTensor::dot_general",
338            &config,
339            self.shape().len(),
340            other.shape().len(),
341        )?;
342        self.binary_op(other, StdTensorOp::DotGeneral { config })
343    }
344
345    /// Execute a dot-general contraction, optionally conjugating either operand.
346    ///
347    /// Untracked tensors route the conjugation flags directly to the backend so
348    /// the conjugated operand does not need to be materialized. Tracked tensors
349    /// fall back to explicit `Conj` plus `DotGeneral` so reverse-mode AD keeps
350    /// the same graph semantics as the standard eager ops.
351    ///
352    /// # Errors
353    ///
354    /// Returns [`Error::ContextMismatch`] for operands from different eager
355    /// runtimes, [`tenferro_tensor::Error::Validation`] for rank/axis/shape or
356    /// dtype mismatches in `config`, or a typed backend/runtime-state error.
357    pub fn dot_general_with_conj(
358        &self,
359        other: &Self,
360        config: DotGeneralConfig,
361        lhs_conj: bool,
362        rhs_conj: bool,
363    ) -> Result<Self> {
364        if !self.same_context(other) {
365            return Err(Error::ContextMismatch {
366                lhs: self.ctx_id(),
367                rhs: other.ctx_id(),
368            });
369        }
370        validate_eager_dot_general_config(
371            "EagerTensor::dot_general_with_conj",
372            &config,
373            self.shape().len(),
374            other.shape().len(),
375        )?;
376
377        if !self.requires_grad && !other.requires_grad {
378            let ctx = Arc::clone(&self.ctx);
379            let mut backend = ctx.lock_backend()?;
380            let output = exec_dot_general_with_conj_on_tensor_reads(
381                self.tensor_read(),
382                other.tensor_read(),
383                &config,
384                lhs_conj,
385                rhs_conj,
386                &mut *backend,
387            )?;
388            drop(backend);
389            return Self::new_untracked_result(ctx, output);
390        }
391
392        match (lhs_conj, rhs_conj) {
393            (false, false) => self.dot_general(other, config),
394            (true, false) => self.conj()?.dot_general(other, config),
395            (false, true) => {
396                let rhs = other.conj()?;
397                self.dot_general(&rhs, config)
398            }
399            (true, true) => {
400                let lhs = self.conj()?;
401                let rhs = other.conj()?;
402                lhs.dot_general(&rhs, config)
403            }
404        }
405    }
406
407    /// Scale by a real scalar: `y = factor * x`.
408    ///
409    /// Integer factors are rounded to the nearest integer before multiplication,
410    /// boolean factors map finite zero to `false` and other finite values to
411    /// `true`, and complex tensors receive a zero-imaginary scalar.
412    ///
413    /// # Errors
414    ///
415    /// Returns [`Error::TensorRuntime`] with
416    /// [`tenferro_tensor::ValidationError::InvalidArgument`] when an integer or
417    /// boolean factor is non-finite or outside the input dtype's range. Backend
418    /// and runtime execution failures retain their typed source variants.
419    /// # Examples
420    ///
421    /// ```rust
422    /// # use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
423    /// # use tenferro_cpu::CpuBackend;
424    /// # let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
425    /// # let x = EagerTensor::from_tensor_in(
426    /// #     Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap(),
427    /// #     ctx,
428    /// # )?;
429    /// let scaled = x.scale_real(2.0)?;
430    /// assert_eq!(scaled.value()?.as_slice::<f64>()?, &[2.0, 4.0]);
431    /// # Ok::<(), tenferro_ad::Error>(())
432    /// ```
433    pub fn scale_real(&self, factor: f64) -> Result<Self> {
434        let scalar = match self.dtype() {
435            DType::F64 => Tensor::from_vec_col_major(vec![], vec![factor])?,
436            DType::F32 => Tensor::from_vec_col_major(vec![], vec![factor as f32])?,
437            DType::I32 => Tensor::from_vec_col_major(vec![], vec![round_real_to_i32(factor)?])?,
438            DType::I64 => Tensor::from_vec_col_major(vec![], vec![round_real_to_i64(factor)?])?,
439            DType::Bool => Tensor::from_vec_col_major(vec![], vec![bool_from_real(factor)?])?,
440            DType::C64 => Tensor::from_vec_col_major(vec![], vec![Complex64::new(factor, 0.0)])?,
441            DType::C32 => {
442                Tensor::from_vec_col_major(vec![], vec![Complex32::new(factor as f32, 0.0)])?
443            }
444        };
445        let scalar = EagerTensor::from_tensor_in(scalar, Arc::clone(&self.ctx))?;
446        self.mul(&scalar)
447    }
448
449    /// Scale a complex tensor by a complex scalar: `y = factor * x`.
450    ///
451    /// # Errors
452    ///
453    /// Returns [`Error::TensorRuntime`] with
454    /// [`tenferro_tensor::ValidationError::InvalidArgument`] for a non-complex
455    /// input dtype. Backend and runtime execution failures retain their typed
456    /// source variants.
457    /// # Examples
458    ///
459    /// ```rust
460    /// # use num_complex::Complex64;
461    /// # use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
462    /// # use tenferro_cpu::CpuBackend;
463    /// # let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
464    /// # let x = EagerTensor::from_tensor_in(
465    /// #     Tensor::from_vec_col_major(
466    /// #         vec![2],
467    /// #         vec![Complex64::new(1.0, 2.0), Complex64::new(3.0, 4.0)],
468    /// #     )
469    /// #     .unwrap(),
470    /// #     ctx,
471    /// # )?;
472    /// let scaled = x.scale_complex(Complex64::new(0.0, 1.0))?;
473    /// assert_eq!(
474    ///     scaled.value()?.as_slice::<Complex64>()?,
475    ///     &[Complex64::new(-2.0, 1.0), Complex64::new(-4.0, 3.0)],
476    /// );
477    /// # Ok::<(), tenferro_ad::Error>(())
478    /// ```
479    pub fn scale_complex(&self, factor: Complex64) -> Result<Self> {
480        let scalar = match self.dtype() {
481            DType::C64 => Tensor::from_vec_col_major(vec![], vec![factor])?,
482            DType::C32 => Tensor::from_vec_col_major(
483                vec![],
484                vec![Complex32::new(factor.re as f32, factor.im as f32)],
485            )?,
486            dtype => {
487                return Err(Error::TensorRuntime(
488                    tenferro_tensor::Error::invalid_argument(
489                        "scale_complex",
490                        "dtype",
491                        format!("requires complex tensor dtype, got {dtype:?}"),
492                    ),
493                ));
494            }
495        };
496        let scalar = EagerTensor::from_tensor_in(scalar, Arc::clone(&self.ctx))?;
497        self.mul(&scalar)
498    }
499
500    /// Matrix multiplication for rank-2 tensors.
501    ///
502    /// This is a convenience wrapper over [`Self::dot_general`] that
503    /// contracts the left matrix's column axis with the right matrix's row
504    /// axis.
505    ///
506    /// # Examples
507    ///
508    /// ```
509    /// use tenferro_cpu::CpuBackend;
510    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
511    ///
512    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
513    /// let a = EagerTensor::from_tensor_in(
514    ///     Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0]).unwrap(),
515    ///     ctx.clone(),
516    /// ).unwrap();
517    /// let b = EagerTensor::from_tensor_in(
518    ///     Tensor::from_vec_col_major(vec![2, 1], vec![5.0_f64, 6.0]).unwrap(),
519    ///     ctx,
520    /// ).unwrap();
521    /// let c = a.matmul(&b).unwrap();
522    ///
523    /// assert_eq!(c.shape(), &[2, 1]);
524    /// assert_eq!(c.value().unwrap().as_slice::<f64>().unwrap(), &[23.0, 34.0]);
525    /// # Ok::<(), tenferro_ad::Error>(())
526    /// ```
527    ///
528    /// # Errors
529    ///
530    /// Returns [`tenferro_tensor::ValidationError::RankMismatch`] when either operand is
531    /// not rank 2, `ShapeMismatch` when the inner dimensions differ, or a typed
532    /// dtype/backend/runtime-state error during the contraction.
533    pub fn matmul(&self, other: &Self) -> Result<Self> {
534        let lhs_shape = self.shape();
535        let rhs_shape = other.shape();
536        if lhs_shape.len() != 2 {
537            return Err(tenferro_tensor::Error::rank_mismatch("matmul", 2, lhs_shape.len()).into());
538        }
539        if rhs_shape.len() != 2 {
540            return Err(tenferro_tensor::Error::rank_mismatch("matmul", 2, rhs_shape.len()).into());
541        }
542        if lhs_shape[1] != rhs_shape[0] {
543            return Err(
544                tenferro_tensor::Error::shape_mismatch("matmul", lhs_shape, rhs_shape).into(),
545            );
546        }
547        self.dot_general(
548            other,
549            DotGeneralConfig {
550                lhs_contracting_dims: vec![1],
551                rhs_contracting_dims: vec![0],
552                lhs_batch_dims: vec![],
553                rhs_batch_dims: vec![],
554            },
555        )
556    }
557
558    /// Permute tensor axes.
559    ///
560    /// # Examples
561    ///
562    /// ```
563    /// use tenferro_cpu::CpuBackend;
564    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
565    ///
566    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
567    /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(
568    ///     vec![2, 3],
569    ///     vec![1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0],
570    /// ).unwrap(), ctx.clone()).unwrap();
571    /// let y = x.transpose(&[1, 0]).unwrap();
572    ///
573    /// assert_eq!(y.shape(), &[3, 2]);
574    /// let materialized = y.to_tensor().unwrap();
575    /// assert_eq!(materialized.as_slice::<f64>().unwrap(), &[1.0, 3.0, 5.0, 2.0, 4.0, 6.0]);
576    /// # Ok::<(), tenferro_ad::Error>(())
577    /// ```
578    ///
579    /// # Errors
580    ///
581    /// Returns [`tenferro_tensor::Error::Validation`] with `AxisOutOfBounds`
582    /// or `DuplicateAxis` when `perm` is not a permutation, or a typed
583    /// backend/runtime-state error while creating the view.
584    pub fn transpose(&self, perm: &[usize]) -> Result<Self> {
585        let op = StdTensorOp::Transpose {
586            perm: perm.to_vec(),
587        };
588        // INVARIANT: the result must own a group independent of `self`; the
589        // explicit duplicate is the ownership boundary before making a view.
590        let base = self.to_tensor()?;
591        let value = TensorValue::from_tensor(base)
592            .transpose_view(perm)
593            .map_err(Error::TensorRuntime)?;
594        Self::nary_value_op(&[self], op, value)
595    }
596
597    /// Reshape without changing element order.
598    ///
599    /// # Examples
600    ///
601    /// ```
602    /// use tenferro_cpu::CpuBackend;
603    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
604    ///
605    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
606    /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(
607    ///     vec![2, 3],
608    ///     vec![1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0],
609    /// ).unwrap(), ctx.clone()).unwrap();
610    /// let y = x.reshape(&[6]).unwrap();
611    ///
612    /// assert_eq!(y.shape(), &[6]);
613    /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
614    /// # Ok::<(), tenferro_ad::Error>(())
615    /// ```
616    ///
617    /// # Errors
618    ///
619    /// Returns [`tenferro_tensor::ValidationError::ShapeMismatch`] when the element count
620    /// changes, `InvalidArgument` when the target shape product overflows, or a
621    /// typed backend/runtime-state error.
622    pub fn reshape(&self, shape: &[usize]) -> Result<Self> {
623        let op = StdTensorOp::Reshape {
624            to_shape: DimExpr::from_concrete(shape),
625        };
626        // INVARIANT: a returned eager tensor cannot borrow `self`'s group, so
627        // the explicit duplicate precedes metadata-only view construction.
628        let base = self.to_tensor()?;
629        if let Ok(value) = TensorValue::from_tensor(base).reshape_view(shape) {
630            return Self::nary_value_op(&[self], op, value);
631        }
632        self.unary_op(op)
633    }
634
635    /// Slice with explicit start, limit, and stride per axis.
636    ///
637    /// # Examples
638    ///
639    /// ```
640    /// use tenferro_cpu::CpuBackend;
641    /// use tenferro_ad::{EagerRuntime, EagerTensor, SliceConfig, Tensor};
642    ///
643    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
644    /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![4], vec![1.0_f64, 2.0, 3.0, 4.0]).unwrap(), ctx.clone()).unwrap();
645    /// let y = x
646    ///     .slice(SliceConfig {
647    ///         starts: vec![1],
648    ///         limits: vec![3],
649    ///         strides: vec![1],
650    ///     })
651    ///     .unwrap();
652    ///
653    /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[2.0, 3.0]);
654    /// # Ok::<(), tenferro_ad::Error>(())
655    /// ```
656    ///
657    /// # Errors
658    ///
659    /// Returns [`tenferro_tensor::Error::Validation`] with
660    /// `AxisOutOfBounds`/`InvalidArgument` when starts, limits, or strides are
661    /// invalid, or a typed backend/runtime-state error while creating the view.
662    pub fn slice(&self, config: SliceConfig) -> Result<Self> {
663        // INVARIANT: the result must retain an independent owner while the
664        // input handle remains live; this is an explicit duplicate, not an
665        // implicit backend transfer.
666        let base = self.to_tensor()?;
667        let value = TensorValue::from_tensor(base)
668            .slice_view(&config)
669            .map_err(Error::TensorRuntime)?;
670        Self::nary_value_op(&[self], StdTensorOp::Slice(config), value)
671    }
672
673    /// Broadcast into a larger shape with explicit dimension placement.
674    ///
675    /// # Examples
676    ///
677    /// ```
678    /// use tenferro_cpu::CpuBackend;
679    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
680    ///
681    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
682    /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![3], vec![1.0_f64, 2.0, 3.0]).unwrap(), ctx.clone()).unwrap();
683    /// let y = x.broadcast_in_dim(&[3, 2], &[0]).unwrap();
684    ///
685    /// assert_eq!(y.shape(), &[3, 2]);
686    /// # Ok::<(), tenferro_ad::Error>(())
687    /// ```
688    ///
689    /// # Errors
690    ///
691    /// Returns [`tenferro_tensor::Error::Validation`] with `AxisOutOfBounds`,
692    /// `DuplicateAxis`, or `ShapeMismatch` when `shape`/`dims` cannot broadcast
693    /// the input, or a typed backend/runtime-state error.
694    pub fn broadcast_in_dim(&self, shape: &[usize], dims: &[usize]) -> Result<Self> {
695        if let Some(error) = broadcast_in_dim_extent_error(self.shape(), shape, dims) {
696            return Err(broadcast_error("EagerTensor::broadcast_in_dim", error));
697        }
698        let op = StdTensorOp::BroadcastInDim {
699            shape: DimExpr::from_concrete(shape),
700            dims: dims.to_vec(),
701        };
702        // INVARIANT: output descriptors cannot borrow the input's move-only
703        // allocation group, so this explicit duplicate owns the view's root.
704        let base = self.to_tensor()?;
705        let value = TensorValue::from_tensor(base)
706            .broadcast_in_dim_view(shape, dims)
707            .map_err(Error::TensorRuntime)?;
708        Self::nary_value_op(&[self], op, value)
709    }
710
711    /// Convert the tensor to a different dtype using checked conversion.
712    ///
713    /// Use [`cast`](Self::cast) when a lossy dtype projection is intended.
714    ///
715    /// # Examples
716    ///
717    /// ```
718    /// use tenferro_cpu::CpuBackend;
719    /// use tenferro_ad::{DType, EagerRuntime, EagerTensor, Tensor};
720    ///
721    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
722    /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2], vec![1.0_f64, -2.0]).unwrap(), ctx.clone()).unwrap();
723    /// let y = x.convert(DType::C64).unwrap();
724    ///
725    /// assert_eq!(y.dtype(), DType::C64);
726    /// assert_eq!(y.shape(), &[2]);
727    /// # Ok::<(), tenferro_ad::Error>(())
728    /// ```
729    ///
730    /// # Errors
731    ///
732    /// Returns [`tenferro_tensor::Error::UnsupportedDTypeConversion`] when the
733    /// requested pair is outside tenferro's checked dtype-promotion lattice.
734    /// Use [`cast`](Self::cast) for explicit lossy projection; backend
735    /// execution can additionally return a typed runtime-state error.
736    pub fn convert(&self, to: DType) -> Result<Self> {
737        tenferro_tensor::validate::validate_convert_dtype("EagerTensor::convert", self.dtype(), to)
738            .map_err(Error::TensorRuntime)?;
739        self.cast(to)
740    }
741
742    /// Cast the tensor to a different dtype using explicit dtype projection.
743    ///
744    /// `cast` may truncate, narrow precision, project complex values to their
745    /// real component, or use boolean truthiness where the backend supports the
746    /// requested projection.
747    ///
748    /// # Examples
749    ///
750    /// ```
751    /// use tenferro_cpu::CpuBackend;
752    /// use tenferro_ad::{DType, EagerRuntime, EagerTensor, Tensor};
753    ///
754    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
755    /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2], vec![1.2_f64, -2.8]).unwrap(), ctx.clone()).unwrap();
756    /// let y = x.cast(DType::I32).unwrap();
757    ///
758    /// assert_eq!(y.value().unwrap().as_slice::<i32>().unwrap(), &[1, -2]);
759    /// # Ok::<(), tenferro_ad::Error>(())
760    /// ```
761    /// # Errors
762    ///
763    /// Returns a typed [`tenferro_tensor::Error::Unsupported`] when the eager
764    /// backend cannot project the requested dtype, or a backend/runtime-state
765    /// error during execution.
766    pub fn cast(&self, to: DType) -> Result<Self> {
767        self.unary_op(StdTensorOp::Convert {
768            from: self.dtype(),
769            to,
770        })
771    }
772
773    /// Pad with zeros using StableHLO-style edge and interior padding.
774    ///
775    /// # Examples
776    ///
777    /// ```
778    /// use tenferro_cpu::CpuBackend;
779    /// use tenferro_ad::{EagerRuntime, EagerTensor, PadConfig, Tensor};
780    ///
781    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
782    /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap(), ctx.clone()).unwrap();
783    /// let y = x
784    ///     .pad(PadConfig {
785    ///         edge_padding_low: vec![1],
786    ///         edge_padding_high: vec![1],
787    ///         interior_padding: vec![1],
788    ///     })
789    ///     .unwrap();
790    ///
791    /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[0.0, 1.0, 0.0, 2.0, 0.0]);
792    /// # Ok::<(), tenferro_ad::Error>(())
793    /// ```
794    /// # Errors
795    ///
796    /// Returns [`tenferro_runtime::Error::TensorRuntime`] containing
797    /// [`tenferro_tensor::ValidationError::InvalidArgument`] when a
798    /// padding vector has a length different from the input rank, interior
799    /// padding is negative, or edge/interior padding produces a negative
800    /// dimension or checked output-size arithmetic overflows.
801    /// Backend execution and unavailable runtime state are propagated as their
802    /// typed [`tenferro_runtime::Error::TensorRuntime`] or
803    /// [`tenferro_runtime::Error::RuntimeState`] variants.
804    pub fn pad(&self, config: PadConfig) -> Result<Self> {
805        self.unary_op(StdTensorOp::Pad(config))
806    }
807
808    /// Reverse the order of elements along the requested axes.
809    ///
810    /// # Examples
811    ///
812    /// ```
813    /// use tenferro_cpu::CpuBackend;
814    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
815    ///
816    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
817    /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![4], vec![1.0_f64, 2.0, 3.0, 4.0]).unwrap(), ctx.clone()).unwrap();
818    /// let y = x.reverse(&[0]).unwrap();
819    ///
820    /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[4.0, 3.0, 2.0, 1.0]);
821    /// # Ok::<(), tenferro_ad::Error>(())
822    /// ```
823    /// # Errors
824    ///
825    /// Returns [`tenferro_tensor::Error::Validation`] with `AxisOutOfBounds` or
826    /// `DuplicateAxis` for an invalid axis list, or a typed backend/
827    /// runtime-state error during execution.
828    pub fn reverse(&self, axes: &[usize]) -> Result<Self> {
829        validate_eager_axes("EagerTensor::reverse", self.shape().len(), axes)?;
830        self.unary_op(StdTensorOp::Reverse {
831            axes: axes.to_vec(),
832        })
833    }
834
835    /// Gather slices from `self` using integer start indices.
836    ///
837    /// # Examples
838    ///
839    /// ```
840    /// use tenferro_cpu::CpuBackend;
841    /// use tenferro_ad::{EagerRuntime, EagerTensor, GatherConfig, Tensor};
842    ///
843    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
844    /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(
845    ///     vec![5],
846    ///     vec![10.0_f64, 20.0, 30.0, 40.0, 50.0],
847    /// ).unwrap(), ctx.clone()).unwrap();
848    /// let indices = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![3], vec![4_i64, 1, 0]).unwrap(), ctx.clone()).unwrap();
849    /// let y = x
850    ///     .gather(
851    ///         &indices,
852    ///         GatherConfig {
853    ///             offset_dims: vec![],
854    ///             collapsed_slice_dims: vec![0],
855    ///             start_index_map: vec![0],
856    ///             index_vector_dim: 1,
857    ///             slice_sizes: vec![1],
858    ///         },
859    ///     )
860    ///     .unwrap();
861    ///
862    /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[50.0, 20.0, 10.0]);
863    /// # Ok::<(), tenferro_ad::Error>(())
864    /// ```
865    /// # Errors
866    ///
867    /// Returns [`tenferro_tensor::Error::Validation`] when the gather
868    /// configuration has an invalid rank, axis, shape, or index dtype, or a
869    /// typed backend/runtime-state error.
870    pub fn gather(&self, indices: &Self, config: GatherConfig) -> Result<Self> {
871        self.binary_op(indices, StdTensorOp::Gather(config))
872    }
873
874    /// Scatter updates into `self` using StableHLO scatter semantics.
875    ///
876    /// # Examples
877    ///
878    /// ```
879    /// use tenferro_cpu::CpuBackend;
880    /// use tenferro_ad::{EagerRuntime, EagerTensor, ScatterConfig, Tensor};
881    ///
882    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
883    /// let operand = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![4], vec![0.0_f64, 0.0, 0.0, 0.0]).unwrap(), ctx.clone()).unwrap();
884    /// let indices = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2, 1], vec![1_i64, 3]).unwrap(), ctx.clone()).unwrap();
885    /// let updates = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2], vec![5.0_f64, 7.0]).unwrap(), ctx.clone()).unwrap();
886    /// let result = operand
887    ///     .scatter(
888    ///         &indices,
889    ///         &updates,
890    ///         ScatterConfig {
891    ///             update_window_dims: vec![],
892    ///             inserted_window_dims: vec![0],
893    ///             scatter_dims_to_operand_dims: vec![0],
894    ///             index_vector_dim: 1,
895    ///         },
896    ///     )
897    ///     .unwrap();
898    ///
899    /// assert_eq!(result.value().unwrap().as_slice::<f64>().unwrap(), &[0.0, 5.0, 0.0, 7.0]);
900    /// # Ok::<(), tenferro_ad::Error>(())
901    /// ```
902    /// # Errors
903    ///
904    /// Returns [`tenferro_tensor::Error::Validation`] when the scatter
905    /// configuration, index/update shapes, or index dtype is invalid, or a
906    /// typed backend/runtime-state error.
907    pub fn scatter(&self, indices: &Self, updates: &Self, config: ScatterConfig) -> Result<Self> {
908        self.ternary_op(indices, updates, StdTensorOp::Scatter(config))
909    }
910
911    /// Slice using runtime start indices.
912    ///
913    /// # Examples
914    ///
915    /// ```
916    /// use tenferro_cpu::CpuBackend;
917    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
918    ///
919    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
920    /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![5], vec![1.0_f64, 2.0, 3.0, 4.0, 5.0]).unwrap(), ctx.clone()).unwrap();
921    /// let starts = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![1], vec![2_i64]).unwrap(), ctx.clone()).unwrap();
922    /// let y = x.dynamic_slice(&starts, &[2]).unwrap();
923    ///
924    /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[3.0, 4.0]);
925    /// # Ok::<(), tenferro_ad::Error>(())
926    /// ```
927    /// # Errors
928    ///
929    /// Returns [`tenferro_tensor::Error::Validation`] when `starts` has the
930    /// wrong dtype/shape or `sizes` exceeds the operand rank, including an
931    /// `AxisOutOfBounds` or `ShapeMismatch`, or a typed backend/runtime-state
932    /// error.
933    pub fn dynamic_slice(&self, starts: &Self, sizes: &[usize]) -> Result<Self> {
934        self.binary_op(
935            starts,
936            StdTensorOp::DynamicSlice {
937                slice_sizes: sizes.to_vec(),
938            },
939        )
940    }
941
942    /// Concatenate tensors along one axis.
943    ///
944    /// # Examples
945    ///
946    /// ```
947    /// use tenferro_cpu::CpuBackend;
948    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
949    ///
950    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
951    /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap(), ctx.clone()).unwrap();
952    /// let y = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2], vec![3.0_f64, 4.0]).unwrap(), ctx.clone()).unwrap();
953    /// let z = EagerTensor::concatenate(&[&x, &y], 0).unwrap();
954    ///
955    /// assert_eq!(z.value().unwrap().as_slice::<f64>().unwrap(), &[1.0, 2.0, 3.0, 4.0]);
956    /// # Ok::<(), tenferro_ad::Error>(())
957    /// ```
958    /// # Errors
959    ///
960    /// Returns [`tenferro_tensor::ValidationError::InvalidArgument`] when `tensors` is
961    /// empty or `axis` is outside the rank, `ShapeMismatch`/`DTypeMismatch`
962    /// when inputs cannot be concatenated, or a typed backend/runtime-state
963    /// error.
964    pub fn concatenate(tensors: &[&Self], axis: usize) -> Result<Self> {
965        Self::nary_op(
966            tensors,
967            StdTensorOp::Concatenate {
968                axis,
969                input_count: tensors.len(),
970            },
971        )
972    }
973
974    /// Extract the diagonal along two axes.
975    ///
976    /// # Examples
977    ///
978    /// ```
979    /// use tenferro_cpu::CpuBackend;
980    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
981    ///
982    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
983    /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(
984    ///     vec![3, 3],
985    ///     vec![1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0],
986    /// ).unwrap(), ctx.clone()).unwrap();
987    /// let y = x.extract_diag(0, 1).unwrap();
988    ///
989    /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[1.0, 5.0, 9.0]);
990    /// # Ok::<(), tenferro_ad::Error>(())
991    /// ```
992    /// # Errors
993    ///
994    /// Returns [`tenferro_tensor::Error::Validation`] with `RankMismatch`,
995    /// `AxisOutOfBounds`, or `DuplicateAxis` when the selected axes cannot form
996    /// a diagonal, or a typed backend/runtime-state error.
997    pub fn extract_diag(&self, axis_a: usize, axis_b: usize) -> Result<Self> {
998        self.unary_op(StdTensorOp::ExtractDiag { axis_a, axis_b })
999    }
1000
1001    /// Embed a vector or lower-rank tensor along a diagonal.
1002    ///
1003    /// # Examples
1004    ///
1005    /// ```
1006    /// use tenferro_cpu::CpuBackend;
1007    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1008    ///
1009    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
1010    /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![3], vec![1.0_f64, 2.0, 3.0]).unwrap(), ctx.clone()).unwrap();
1011    /// let y = x.embed_diag(0, 1).unwrap();
1012    ///
1013    /// assert_eq!(y.shape(), &[3, 3]);
1014    /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[1.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 3.0]);
1015    /// # Ok::<(), tenferro_ad::Error>(())
1016    /// ```
1017    /// # Errors
1018    ///
1019    /// Returns [`tenferro_tensor::Error::Validation`] with `RankMismatch`,
1020    /// `AxisOutOfBounds`, or `DuplicateAxis` when the diagonal axes are not
1021    /// valid for embedding, or a typed backend/runtime-state error.
1022    pub fn embed_diag(&self, axis_a: usize, axis_b: usize) -> Result<Self> {
1023        self.unary_op(StdTensorOp::EmbedDiag { axis_a, axis_b })
1024    }
1025
1026    /// Keep the lower triangle and zero the rest.
1027    ///
1028    /// # Examples
1029    ///
1030    /// ```
1031    /// use tenferro_cpu::CpuBackend;
1032    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1033    ///
1034    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
1035    /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0]).unwrap(), ctx.clone()).unwrap();
1036    /// let y = x.tril(0).unwrap();
1037    ///
1038    /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[1.0, 2.0, 0.0, 4.0]);
1039    /// # Ok::<(), tenferro_ad::Error>(())
1040    /// ```
1041    /// # Errors
1042    ///
1043    /// Returns [`tenferro_tensor::ValidationError::RankMismatch`] when the operand is not
1044    /// a matrix, or a typed unsupported/backend/runtime-state error.
1045    pub fn tril(&self, k: i64) -> Result<Self> {
1046        self.unary_op(StdTensorOp::Tril { k })
1047    }
1048
1049    /// Keep the upper triangle and zero the rest.
1050    ///
1051    /// # Examples
1052    ///
1053    /// ```
1054    /// use tenferro_cpu::CpuBackend;
1055    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1056    ///
1057    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
1058    /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0]).unwrap(), ctx.clone()).unwrap();
1059    /// let y = x.triu(0).unwrap();
1060    ///
1061    /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[1.0, 0.0, 3.0, 4.0]);
1062    /// # Ok::<(), tenferro_ad::Error>(())
1063    /// ```
1064    /// # Errors
1065    ///
1066    /// Returns [`tenferro_tensor::ValidationError::RankMismatch`] when the operand is not
1067    /// a matrix, or a typed unsupported/backend/runtime-state error.
1068    pub fn triu(&self, k: i64) -> Result<Self> {
1069        self.unary_op(StdTensorOp::Triu { k })
1070    }
1071
1072    /// Reduce product over the requested axes.
1073    ///
1074    /// # Examples
1075    ///
1076    /// ```
1077    /// use tenferro_cpu::CpuBackend;
1078    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1079    ///
1080    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
1081    /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0]).unwrap(), ctx.clone()).unwrap();
1082    /// let y = x.reduce_prod(None).unwrap();
1083    ///
1084    /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[24.0]);
1085    /// # Ok::<(), tenferro_ad::Error>(())
1086    /// ```
1087    /// # Errors
1088    ///
1089    /// Returns [`tenferro_tensor::Error::Validation`] with `AxisOutOfBounds` or
1090    /// `DuplicateAxis` for an invalid reduction axis, or a typed
1091    /// unsupported/backend/runtime-state error for the selected dtype.
1092    pub fn reduce_prod(&self, axes: Option<&[usize]>) -> Result<Self> {
1093        let axes = axes.map_or_else(|| (0..self.shape().len()).collect(), <[usize]>::to_vec);
1094        validate_eager_axes("EagerTensor::reduce_prod", self.shape().len(), &axes)?;
1095        self.unary_op(StdTensorOp::ReduceProd { axes })
1096    }
1097
1098    /// Reduce maximum over the requested axes.
1099    ///
1100    /// # Examples
1101    ///
1102    /// ```
1103    /// use tenferro_cpu::CpuBackend;
1104    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1105    ///
1106    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
1107    /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0]).unwrap(), ctx.clone()).unwrap();
1108    /// let y = x.reduce_max(None).unwrap();
1109    ///
1110    /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[4.0]);
1111    /// # Ok::<(), tenferro_ad::Error>(())
1112    /// ```
1113    /// # Errors
1114    ///
1115    /// Returns [`tenferro_tensor::Error::Validation`] with `AxisOutOfBounds` or
1116    /// `DuplicateAxis` for an invalid reduction axis, or a typed
1117    /// unsupported/backend/runtime-state error for the selected dtype.
1118    pub fn reduce_max(&self, axes: Option<&[usize]>) -> Result<Self> {
1119        let axes = axes.map_or_else(|| (0..self.shape().len()).collect(), <[usize]>::to_vec);
1120        validate_eager_axes("EagerTensor::reduce_max", self.shape().len(), &axes)?;
1121        self.unary_op(StdTensorOp::ReduceMax { axes })
1122    }
1123
1124    /// Reduce minimum over the requested axes.
1125    ///
1126    /// # Examples
1127    ///
1128    /// ```
1129    /// use tenferro_cpu::CpuBackend;
1130    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1131    ///
1132    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
1133    /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0]).unwrap(), ctx.clone()).unwrap();
1134    /// let y = x.reduce_min(None).unwrap();
1135    ///
1136    /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[1.0]);
1137    /// # Ok::<(), tenferro_ad::Error>(())
1138    /// ```
1139    /// # Errors
1140    ///
1141    /// Returns [`tenferro_tensor::Error::Validation`] with `AxisOutOfBounds` or
1142    /// `DuplicateAxis` for an invalid reduction axis, or a typed
1143    /// unsupported/backend/runtime-state error for the selected dtype.
1144    pub fn reduce_min(&self, axes: Option<&[usize]>) -> Result<Self> {
1145        let axes = axes.map_or_else(|| (0..self.shape().len()).collect(), <[usize]>::to_vec);
1146        validate_eager_axes("EagerTensor::reduce_min", self.shape().len(), &axes)?;
1147        self.unary_op(StdTensorOp::ReduceMin { axes })
1148    }
1149
1150    pub(crate) fn unary_op(&self, op: StdTensorOp) -> Result<Self> {
1151        Self::nary_op(&[self], op)
1152    }
1153
1154    pub(crate) fn binary_op(&self, other: &Self, op: StdTensorOp) -> Result<Self> {
1155        Self::nary_op(&[self, other], op)
1156    }
1157
1158    pub(crate) fn ternary_op(&self, b: &Self, c: &Self, op: StdTensorOp) -> Result<Self> {
1159        Self::nary_op(&[self, b, c], op)
1160    }
1161
1162    pub(crate) fn nary_value_op(
1163        tensors: &[&Self],
1164        op: StdTensorOp,
1165        value: TensorValue,
1166    ) -> Result<Self> {
1167        let Some(first) = tensors.first() else {
1168            return Err(empty_nary_input_error(&op));
1169        };
1170
1171        let ctx = Arc::clone(&first.ctx);
1172        for tensor in tensors.iter().skip(1) {
1173            if !first.same_context(tensor) {
1174                return Err(Error::ContextMismatch {
1175                    lhs: first.ctx_id(),
1176                    rhs: tensor.ctx_id(),
1177                });
1178            }
1179        }
1180
1181        if !eager_grad_recording_enabled()
1182            || (!eager_capture_active() && !tensors.iter().any(|tensor| tensor.requires_grad))
1183        {
1184            return Self::new_untracked_value_result(ctx, value);
1185        }
1186
1187        let output_ref = &value;
1188        let mut recorded = record_eager_value_outputs(&op, &[output_ref], tensors)?;
1189        let trace = recorded.traces.pop().ok_or_else(|| {
1190            Error::Internal(format!("expected one eager trace for {:?}, got 0", op))
1191        })?;
1192        let semantic_trace = recorded.semantic_traces.pop().flatten();
1193
1194        Self::new_result_value(
1195            ctx,
1196            trace.key,
1197            value,
1198            trace.requires_grad,
1199            trace.trace,
1200            semantic_trace,
1201        )
1202    }
1203
1204    pub(crate) fn nary_op(tensors: &[&Self], op: StdTensorOp) -> Result<Self> {
1205        let total_started = eager_op_profile_start();
1206        let Some(first) = tensors.first() else {
1207            return Err(empty_nary_input_error(&op));
1208        };
1209        let expected = op.input_count();
1210        if tensors.len() != expected {
1211            return Err(wrong_nary_input_count_error(&op, expected, tensors.len()));
1212        }
1213
1214        let ctx = Arc::clone(&first.ctx);
1215        profile_eager_op_section("nary_op.context_check", || -> Result<()> {
1216            for tensor in tensors.iter().skip(1) {
1217                if !first.same_context(tensor) {
1218                    return Err(Error::ContextMismatch {
1219                        lhs: first.ctx_id(),
1220                        rhs: tensor.ctx_id(),
1221                    });
1222                }
1223            }
1224            Ok(())
1225        })?;
1226
1227        let any_requires_grad = profile_eager_op_section("nary_op.requires_grad_scan", || {
1228            eager_grad_recording_enabled()
1229                && (eager_capture_active() || tensors.iter().any(|tensor| tensor.requires_grad))
1230        });
1231        if !any_requires_grad {
1232            let input_reads = profile_eager_op_section("nary_op.collect_input_reads", || {
1233                tensors
1234                    .iter()
1235                    .map(|tensor| tensor.tensor_read())
1236                    .collect::<Vec<_>>()
1237            });
1238            let output = profile_eager_op_section("nary_op.exec_single_output_read", || {
1239                exec_single_output_read(&op, &input_reads, &ctx)
1240            })?;
1241            let result = profile_eager_op_section("nary_op.new_untracked_result", || {
1242                Self::new_untracked_result(ctx, output)
1243            });
1244            if let Some(total_started) = total_started {
1245                record_eager_op_profile("nary_op.total", total_started.elapsed());
1246                maybe_print_eager_op_profile();
1247            }
1248            return result;
1249        }
1250
1251        let input_arcs = profile_eager_op_section("nary_op.materialize_inputs", || {
1252            tensors
1253                .iter()
1254                .map(|tensor| tensor.to_tensor().map(Arc::new))
1255                .collect::<Result<Vec<_>>>()
1256        })?;
1257        let inputs: Vec<&Tensor> = profile_eager_op_section("nary_op.collect_inputs", || {
1258            input_arcs.iter().map(|tensor| tensor.as_ref()).collect()
1259        });
1260        let output = profile_eager_op_section("nary_op.exec_single_output", || {
1261            exec_single_output(&op, &inputs, &ctx)
1262        })?;
1263
1264        let outputs = vec![&output];
1265        let mut recorded = profile_eager_op_section("nary_op.record_outputs", || {
1266            record_eager_outputs(&op, &outputs, tensors)
1267        })?;
1268        let trace = recorded.traces.pop().ok_or_else(|| {
1269            Error::Internal(format!("expected one eager trace for {:?}, got 0", op))
1270        })?;
1271        let semantic_trace = recorded.semantic_traces.pop().flatten();
1272
1273        let result = profile_eager_op_section("nary_op.new_tracked_result", || {
1274            Self::new_result_with_semantic_trace(
1275                ctx,
1276                trace.key,
1277                output,
1278                trace.requires_grad,
1279                trace.trace,
1280                semantic_trace,
1281            )
1282        });
1283        if let Some(total_started) = total_started {
1284            record_eager_op_profile("nary_op.total", total_started.elapsed());
1285            maybe_print_eager_op_profile();
1286        }
1287        result
1288    }
1289}
1290
1291fn validate_eager_axes(op: &'static str, rank: usize, axes: &[usize]) -> Result<()> {
1292    tenferro_tensor::validate::validate_unique_axes(op, "axis", rank, axes)
1293        .map_err(Error::TensorRuntime)
1294}
1295
1296fn validate_eager_dot_general_config(
1297    _op: &'static str,
1298    config: &DotGeneralConfig,
1299    lhs_rank: usize,
1300    rhs_rank: usize,
1301) -> Result<()> {
1302    config
1303        .validate_dims_with_ranks(lhs_rank, rhs_rank)
1304        .map_err(Error::TensorRuntime)
1305}
1306
1307fn empty_nary_input_error(op: &StdTensorOp) -> Error {
1308    Error::TensorRuntime(tenferro_tensor::Error::invalid_argument(
1309        eager_validation_op_name(op),
1310        "inputs",
1311        "operation requires at least one input tensor",
1312    ))
1313}
1314
1315fn wrong_nary_input_count_error(op: &StdTensorOp, expected: usize, actual: usize) -> Error {
1316    Error::TensorRuntime(tenferro_tensor::Error::invalid_argument(
1317        eager_validation_op_name(op),
1318        "inputs",
1319        format!("operation expects {expected} inputs, got {actual}"),
1320    ))
1321}
1322
1323fn eager_validation_op_name(op: &StdTensorOp) -> &'static str {
1324    match op {
1325        StdTensorOp::Concatenate { .. } => "concatenate",
1326        _ => "eager_nary_op",
1327    }
1328}
1329
1330fn finite_real_factor(value: f64) -> Result<f64> {
1331    if value.is_finite() {
1332        Ok(value)
1333    } else {
1334        Err(Error::TensorRuntime(
1335            tenferro_tensor::Error::invalid_argument(
1336                "scale_real",
1337                "factor",
1338                format!("real scalar must be finite, got {value}"),
1339            ),
1340        ))
1341    }
1342}
1343
1344fn round_real_to_i64(value: f64) -> Result<i64> {
1345    let rounded = finite_real_factor(value)?.round();
1346    if rounded < i64::MIN as f64 || rounded >= -(i64::MIN as f64) {
1347        return Err(Error::TensorRuntime(
1348            tenferro_tensor::Error::invalid_argument(
1349                "scale_real",
1350                "factor",
1351                format!("rounded real scalar {rounded} is out of i64 range"),
1352            ),
1353        ));
1354    }
1355    Ok(rounded as i64)
1356}
1357
1358fn round_real_to_i32(value: f64) -> Result<i32> {
1359    let rounded = round_real_to_i64(value)?;
1360    i32::try_from(rounded).map_err(|_| {
1361        Error::TensorRuntime(tenferro_tensor::Error::invalid_argument(
1362            "scale_real",
1363            "factor",
1364            format!("rounded real scalar {rounded} is out of i32 range"),
1365        ))
1366    })
1367}
1368
1369fn bool_from_real(value: f64) -> Result<bool> {
1370    Ok(finite_real_factor(value)? != 0.0)
1371}