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    /// // Arrays, vectors, and slices use the same dynamic-shape contract.
611    /// for y in [x.reshape([6])?, x.reshape(vec![6])?, x.reshape(&[6][..])?] {
612    ///     assert_eq!(y.shape(), &[6]);
613    ///     assert_eq!(y.value()?.as_slice::<f64>()?, &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
614    /// }
615    /// # Ok::<(), tenferro_ad::Error>(())
616    /// ```
617    ///
618    /// # Errors
619    ///
620    /// Returns [`tenferro_tensor::ValidationError::ShapeMismatch`] when the element count
621    /// changes, `InvalidArgument` when the target shape product overflows, or a
622    /// typed backend/runtime-state error.
623    pub fn reshape(&self, shape: impl tenferro_tensor::IntoShapeVec) -> Result<Self> {
624        let shape = shape.into_shape_vec();
625        let op = StdTensorOp::Reshape {
626            to_shape: DimExpr::from_concrete(&shape),
627        };
628        // INVARIANT: a returned eager tensor cannot borrow `self`'s group, so
629        // the explicit duplicate precedes metadata-only view construction.
630        let base = self.to_tensor()?;
631        if let Ok(value) = TensorValue::from_tensor(base).reshape_view(shape) {
632            return Self::nary_value_op(&[self], op, value);
633        }
634        self.unary_op(op)
635    }
636
637    /// Slice with explicit start, limit, and stride per axis.
638    ///
639    /// # Examples
640    ///
641    /// ```
642    /// use tenferro_cpu::CpuBackend;
643    /// use tenferro_ad::{EagerRuntime, EagerTensor, SliceConfig, Tensor};
644    ///
645    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
646    /// 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();
647    /// let y = x
648    ///     .slice(SliceConfig {
649    ///         starts: vec![1],
650    ///         limits: vec![3],
651    ///         strides: vec![1],
652    ///     })
653    ///     .unwrap();
654    ///
655    /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[2.0, 3.0]);
656    /// # Ok::<(), tenferro_ad::Error>(())
657    /// ```
658    ///
659    /// # Errors
660    ///
661    /// Returns [`tenferro_tensor::Error::Validation`] with
662    /// `AxisOutOfBounds`/`InvalidArgument` when starts, limits, or strides are
663    /// invalid, or a typed backend/runtime-state error while creating the view.
664    pub fn slice(&self, config: SliceConfig) -> Result<Self> {
665        // INVARIANT: the result must retain an independent owner while the
666        // input handle remains live; this is an explicit duplicate, not an
667        // implicit backend transfer.
668        let base = self.to_tensor()?;
669        let value = TensorValue::from_tensor(base)
670            .slice_view(&config)
671            .map_err(Error::TensorRuntime)?;
672        Self::nary_value_op(&[self], StdTensorOp::Slice(config), value)
673    }
674
675    /// Broadcast into a larger shape with explicit dimension placement.
676    ///
677    /// # Examples
678    ///
679    /// ```
680    /// use tenferro_cpu::CpuBackend;
681    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
682    ///
683    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
684    /// 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();
685    /// let y = x.broadcast_in_dim(&[3, 2], &[0]).unwrap();
686    ///
687    /// assert_eq!(y.shape(), &[3, 2]);
688    /// # Ok::<(), tenferro_ad::Error>(())
689    /// ```
690    ///
691    /// # Errors
692    ///
693    /// Returns [`tenferro_tensor::Error::Validation`] with `AxisOutOfBounds`,
694    /// `DuplicateAxis`, or `ShapeMismatch` when `shape`/`dims` cannot broadcast
695    /// the input, or a typed backend/runtime-state error.
696    pub fn broadcast_in_dim(&self, shape: &[usize], dims: &[usize]) -> Result<Self> {
697        if let Some(error) = broadcast_in_dim_extent_error(self.shape(), shape, dims) {
698            return Err(broadcast_error("EagerTensor::broadcast_in_dim", error));
699        }
700        let op = StdTensorOp::BroadcastInDim {
701            shape: DimExpr::from_concrete(shape),
702            dims: dims.to_vec(),
703        };
704        // INVARIANT: output descriptors cannot borrow the input's move-only
705        // allocation group, so this explicit duplicate owns the view's root.
706        let base = self.to_tensor()?;
707        let value = TensorValue::from_tensor(base)
708            .broadcast_in_dim_view(shape, dims)
709            .map_err(Error::TensorRuntime)?;
710        Self::nary_value_op(&[self], op, value)
711    }
712
713    /// Convert the tensor to a different dtype using checked conversion.
714    ///
715    /// Use [`cast`](Self::cast) when a lossy dtype projection is intended.
716    ///
717    /// # Examples
718    ///
719    /// ```
720    /// use tenferro_cpu::CpuBackend;
721    /// use tenferro_ad::{DType, EagerRuntime, EagerTensor, Tensor};
722    ///
723    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
724    /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2], vec![1.0_f64, -2.0]).unwrap(), ctx.clone()).unwrap();
725    /// let y = x.convert(DType::C64).unwrap();
726    ///
727    /// assert_eq!(y.dtype(), DType::C64);
728    /// assert_eq!(y.shape(), &[2]);
729    /// # Ok::<(), tenferro_ad::Error>(())
730    /// ```
731    ///
732    /// # Errors
733    ///
734    /// Returns [`tenferro_tensor::Error::UnsupportedDTypeConversion`] when the
735    /// requested pair is outside tenferro's checked dtype-promotion lattice.
736    /// Use [`cast`](Self::cast) for explicit lossy projection; backend
737    /// execution can additionally return a typed runtime-state error.
738    pub fn convert(&self, to: DType) -> Result<Self> {
739        tenferro_tensor::validate::validate_convert_dtype("EagerTensor::convert", self.dtype(), to)
740            .map_err(Error::TensorRuntime)?;
741        self.cast(to)
742    }
743
744    /// Cast the tensor to a different dtype using explicit dtype projection.
745    ///
746    /// `cast` may truncate, narrow precision, project complex values to their
747    /// real component, or use boolean truthiness where the backend supports the
748    /// requested projection.
749    ///
750    /// # Examples
751    ///
752    /// ```
753    /// use tenferro_cpu::CpuBackend;
754    /// use tenferro_ad::{DType, EagerRuntime, EagerTensor, Tensor};
755    ///
756    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
757    /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2], vec![1.2_f64, -2.8]).unwrap(), ctx.clone()).unwrap();
758    /// let y = x.cast(DType::I32).unwrap();
759    ///
760    /// assert_eq!(y.value().unwrap().as_slice::<i32>().unwrap(), &[1, -2]);
761    /// # Ok::<(), tenferro_ad::Error>(())
762    /// ```
763    /// # Errors
764    ///
765    /// Returns a typed [`tenferro_tensor::Error::Unsupported`] when the eager
766    /// backend cannot project the requested dtype, or a backend/runtime-state
767    /// error during execution.
768    pub fn cast(&self, to: DType) -> Result<Self> {
769        self.unary_op(StdTensorOp::Convert {
770            from: self.dtype(),
771            to,
772        })
773    }
774
775    /// Pad with zeros using StableHLO-style edge and interior padding.
776    ///
777    /// # Examples
778    ///
779    /// ```
780    /// use tenferro_cpu::CpuBackend;
781    /// use tenferro_ad::{EagerRuntime, EagerTensor, PadConfig, Tensor};
782    ///
783    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
784    /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap(), ctx.clone()).unwrap();
785    /// let y = x
786    ///     .pad(PadConfig {
787    ///         edge_padding_low: vec![1],
788    ///         edge_padding_high: vec![1],
789    ///         interior_padding: vec![1],
790    ///     })
791    ///     .unwrap();
792    ///
793    /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[0.0, 1.0, 0.0, 2.0, 0.0]);
794    /// # Ok::<(), tenferro_ad::Error>(())
795    /// ```
796    /// # Errors
797    ///
798    /// Returns [`tenferro_runtime::Error::TensorRuntime`] containing
799    /// [`tenferro_tensor::ValidationError::InvalidArgument`] when a
800    /// padding vector has a length different from the input rank, interior
801    /// padding is negative, or edge/interior padding produces a negative
802    /// dimension or checked output-size arithmetic overflows.
803    /// Backend execution and unavailable runtime state are propagated as their
804    /// typed [`tenferro_runtime::Error::TensorRuntime`] or
805    /// [`tenferro_runtime::Error::RuntimeState`] variants.
806    pub fn pad(&self, config: PadConfig) -> Result<Self> {
807        self.unary_op(StdTensorOp::Pad(config))
808    }
809
810    /// Reverse the order of elements along the requested axes.
811    ///
812    /// # Examples
813    ///
814    /// ```
815    /// use tenferro_cpu::CpuBackend;
816    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
817    ///
818    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
819    /// 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();
820    /// let y = x.reverse(&[0]).unwrap();
821    ///
822    /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[4.0, 3.0, 2.0, 1.0]);
823    /// # Ok::<(), tenferro_ad::Error>(())
824    /// ```
825    /// # Errors
826    ///
827    /// Returns [`tenferro_tensor::Error::Validation`] with `AxisOutOfBounds` or
828    /// `DuplicateAxis` for an invalid axis list, or a typed backend/
829    /// runtime-state error during execution.
830    pub fn reverse(&self, axes: &[usize]) -> Result<Self> {
831        validate_eager_axes("EagerTensor::reverse", self.shape().len(), axes)?;
832        self.unary_op(StdTensorOp::Reverse {
833            axes: axes.to_vec(),
834        })
835    }
836
837    /// Gather slices from `self` using integer start indices.
838    ///
839    /// # Examples
840    ///
841    /// ```
842    /// use tenferro_cpu::CpuBackend;
843    /// use tenferro_ad::{EagerRuntime, EagerTensor, GatherConfig, Tensor};
844    ///
845    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
846    /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(
847    ///     vec![5],
848    ///     vec![10.0_f64, 20.0, 30.0, 40.0, 50.0],
849    /// ).unwrap(), ctx.clone()).unwrap();
850    /// let indices = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![3], vec![4_i64, 1, 0]).unwrap(), ctx.clone()).unwrap();
851    /// let y = x
852    ///     .gather(
853    ///         &indices,
854    ///         GatherConfig {
855    ///             offset_dims: vec![],
856    ///             collapsed_slice_dims: vec![0],
857    ///             start_index_map: vec![0],
858    ///             index_vector_dim: 1,
859    ///             slice_sizes: vec![1],
860    ///         },
861    ///     )
862    ///     .unwrap();
863    ///
864    /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[50.0, 20.0, 10.0]);
865    /// # Ok::<(), tenferro_ad::Error>(())
866    /// ```
867    /// # Errors
868    ///
869    /// Returns [`tenferro_tensor::Error::Validation`] when the gather
870    /// configuration has an invalid rank, axis, shape, or index dtype, or a
871    /// typed backend/runtime-state error.
872    pub fn gather(&self, indices: &Self, config: GatherConfig) -> Result<Self> {
873        self.binary_op(indices, StdTensorOp::Gather(config))
874    }
875
876    /// Scatter updates into `self` using StableHLO scatter semantics.
877    ///
878    /// # Examples
879    ///
880    /// ```
881    /// use tenferro_cpu::CpuBackend;
882    /// use tenferro_ad::{EagerRuntime, EagerTensor, ScatterConfig, Tensor};
883    ///
884    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
885    /// 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();
886    /// let indices = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2, 1], vec![1_i64, 3]).unwrap(), ctx.clone()).unwrap();
887    /// let updates = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2], vec![5.0_f64, 7.0]).unwrap(), ctx.clone()).unwrap();
888    /// let result = operand
889    ///     .scatter(
890    ///         &indices,
891    ///         &updates,
892    ///         ScatterConfig {
893    ///             update_window_dims: vec![],
894    ///             inserted_window_dims: vec![0],
895    ///             scatter_dims_to_operand_dims: vec![0],
896    ///             index_vector_dim: 1,
897    ///         },
898    ///     )
899    ///     .unwrap();
900    ///
901    /// assert_eq!(result.value().unwrap().as_slice::<f64>().unwrap(), &[0.0, 5.0, 0.0, 7.0]);
902    /// # Ok::<(), tenferro_ad::Error>(())
903    /// ```
904    /// # Errors
905    ///
906    /// Returns [`tenferro_tensor::Error::Validation`] when the scatter
907    /// configuration, index/update shapes, or index dtype is invalid, or a
908    /// typed backend/runtime-state error.
909    pub fn scatter(&self, indices: &Self, updates: &Self, config: ScatterConfig) -> Result<Self> {
910        self.ternary_op(indices, updates, StdTensorOp::Scatter(config))
911    }
912
913    /// Slice using runtime start indices.
914    ///
915    /// # Examples
916    ///
917    /// ```
918    /// use tenferro_cpu::CpuBackend;
919    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
920    ///
921    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
922    /// 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();
923    /// let starts = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![1], vec![2_i64]).unwrap(), ctx.clone()).unwrap();
924    /// let y = x.dynamic_slice(&starts, &[2]).unwrap();
925    ///
926    /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[3.0, 4.0]);
927    /// # Ok::<(), tenferro_ad::Error>(())
928    /// ```
929    /// # Errors
930    ///
931    /// Returns [`tenferro_tensor::Error::Validation`] when `starts` has the
932    /// wrong dtype/shape or `sizes` exceeds the operand rank, including an
933    /// `AxisOutOfBounds` or `ShapeMismatch`, or a typed backend/runtime-state
934    /// error.
935    pub fn dynamic_slice(&self, starts: &Self, sizes: &[usize]) -> Result<Self> {
936        self.binary_op(
937            starts,
938            StdTensorOp::DynamicSlice {
939                slice_sizes: sizes.to_vec(),
940            },
941        )
942    }
943
944    /// Concatenate tensors along one axis.
945    ///
946    /// # Examples
947    ///
948    /// ```
949    /// use tenferro_cpu::CpuBackend;
950    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
951    ///
952    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
953    /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap(), ctx.clone()).unwrap();
954    /// let y = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2], vec![3.0_f64, 4.0]).unwrap(), ctx.clone()).unwrap();
955    /// let z = EagerTensor::concatenate(&[&x, &y], 0).unwrap();
956    ///
957    /// assert_eq!(z.value().unwrap().as_slice::<f64>().unwrap(), &[1.0, 2.0, 3.0, 4.0]);
958    /// # Ok::<(), tenferro_ad::Error>(())
959    /// ```
960    /// # Errors
961    ///
962    /// Returns [`tenferro_tensor::ValidationError::InvalidArgument`] when `tensors` is
963    /// empty or `axis` is outside the rank, `ShapeMismatch`/`DTypeMismatch`
964    /// when inputs cannot be concatenated, or a typed backend/runtime-state
965    /// error.
966    pub fn concatenate(tensors: &[&Self], axis: usize) -> Result<Self> {
967        Self::nary_op(
968            tensors,
969            StdTensorOp::Concatenate {
970                axis,
971                input_count: tensors.len(),
972            },
973        )
974    }
975
976    /// Extract the diagonal along two axes.
977    ///
978    /// # Examples
979    ///
980    /// ```
981    /// use tenferro_cpu::CpuBackend;
982    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
983    ///
984    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
985    /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(
986    ///     vec![3, 3],
987    ///     vec![1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0],
988    /// ).unwrap(), ctx.clone()).unwrap();
989    /// let y = x.extract_diag(0, 1).unwrap();
990    ///
991    /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[1.0, 5.0, 9.0]);
992    /// # Ok::<(), tenferro_ad::Error>(())
993    /// ```
994    /// # Errors
995    ///
996    /// Returns [`tenferro_tensor::Error::Validation`] with `RankMismatch`,
997    /// `AxisOutOfBounds`, or `DuplicateAxis` when the selected axes cannot form
998    /// a diagonal, or a typed backend/runtime-state error.
999    pub fn extract_diag(&self, axis_a: usize, axis_b: usize) -> Result<Self> {
1000        self.unary_op(StdTensorOp::ExtractDiag { axis_a, axis_b })
1001    }
1002
1003    /// Embed a vector or lower-rank tensor along a diagonal.
1004    ///
1005    /// # Examples
1006    ///
1007    /// ```
1008    /// use tenferro_cpu::CpuBackend;
1009    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1010    ///
1011    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
1012    /// 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();
1013    /// let y = x.embed_diag(0, 1).unwrap();
1014    ///
1015    /// assert_eq!(y.shape(), &[3, 3]);
1016    /// 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]);
1017    /// # Ok::<(), tenferro_ad::Error>(())
1018    /// ```
1019    /// # Errors
1020    ///
1021    /// Returns [`tenferro_tensor::Error::Validation`] with `RankMismatch`,
1022    /// `AxisOutOfBounds`, or `DuplicateAxis` when the diagonal axes are not
1023    /// valid for embedding, or a typed backend/runtime-state error.
1024    pub fn embed_diag(&self, axis_a: usize, axis_b: usize) -> Result<Self> {
1025        self.unary_op(StdTensorOp::EmbedDiag { axis_a, axis_b })
1026    }
1027
1028    /// Keep the lower triangle and zero the rest.
1029    ///
1030    /// # Examples
1031    ///
1032    /// ```
1033    /// use tenferro_cpu::CpuBackend;
1034    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1035    ///
1036    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
1037    /// 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();
1038    /// let y = x.tril(0).unwrap();
1039    ///
1040    /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[1.0, 2.0, 0.0, 4.0]);
1041    /// # Ok::<(), tenferro_ad::Error>(())
1042    /// ```
1043    /// # Errors
1044    ///
1045    /// Returns [`tenferro_tensor::ValidationError::RankMismatch`] when the operand is not
1046    /// a matrix, or a typed unsupported/backend/runtime-state error.
1047    pub fn tril(&self, k: i64) -> Result<Self> {
1048        self.unary_op(StdTensorOp::Tril { k })
1049    }
1050
1051    /// Keep the upper triangle and zero the rest.
1052    ///
1053    /// # Examples
1054    ///
1055    /// ```
1056    /// use tenferro_cpu::CpuBackend;
1057    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1058    ///
1059    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
1060    /// 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();
1061    /// let y = x.triu(0).unwrap();
1062    ///
1063    /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[1.0, 0.0, 3.0, 4.0]);
1064    /// # Ok::<(), tenferro_ad::Error>(())
1065    /// ```
1066    /// # Errors
1067    ///
1068    /// Returns [`tenferro_tensor::ValidationError::RankMismatch`] when the operand is not
1069    /// a matrix, or a typed unsupported/backend/runtime-state error.
1070    pub fn triu(&self, k: i64) -> Result<Self> {
1071        self.unary_op(StdTensorOp::Triu { k })
1072    }
1073
1074    /// Reduce product over the requested axes.
1075    ///
1076    /// # Examples
1077    ///
1078    /// ```
1079    /// use tenferro_cpu::CpuBackend;
1080    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1081    ///
1082    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
1083    /// 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();
1084    /// let y = x.reduce_prod(None).unwrap();
1085    ///
1086    /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[24.0]);
1087    /// # Ok::<(), tenferro_ad::Error>(())
1088    /// ```
1089    /// # Errors
1090    ///
1091    /// Returns [`tenferro_tensor::Error::Validation`] with `AxisOutOfBounds` or
1092    /// `DuplicateAxis` for an invalid reduction axis, or a typed
1093    /// unsupported/backend/runtime-state error for the selected dtype.
1094    pub fn reduce_prod(&self, axes: Option<&[usize]>) -> Result<Self> {
1095        let axes = axes.map_or_else(|| (0..self.shape().len()).collect(), <[usize]>::to_vec);
1096        validate_eager_axes("EagerTensor::reduce_prod", self.shape().len(), &axes)?;
1097        self.unary_op(StdTensorOp::ReduceProd { axes })
1098    }
1099
1100    /// Reduce maximum over the requested axes.
1101    ///
1102    /// # Examples
1103    ///
1104    /// ```
1105    /// use tenferro_cpu::CpuBackend;
1106    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1107    ///
1108    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
1109    /// 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();
1110    /// let y = x.reduce_max(None).unwrap();
1111    ///
1112    /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[4.0]);
1113    /// # Ok::<(), tenferro_ad::Error>(())
1114    /// ```
1115    /// # Errors
1116    ///
1117    /// Returns [`tenferro_tensor::Error::Validation`] with `AxisOutOfBounds` or
1118    /// `DuplicateAxis` for an invalid reduction axis, or a typed
1119    /// unsupported/backend/runtime-state error for the selected dtype.
1120    pub fn reduce_max(&self, axes: Option<&[usize]>) -> Result<Self> {
1121        let axes = axes.map_or_else(|| (0..self.shape().len()).collect(), <[usize]>::to_vec);
1122        validate_eager_axes("EagerTensor::reduce_max", self.shape().len(), &axes)?;
1123        self.unary_op(StdTensorOp::ReduceMax { axes })
1124    }
1125
1126    /// Reduce minimum over the requested axes.
1127    ///
1128    /// # Examples
1129    ///
1130    /// ```
1131    /// use tenferro_cpu::CpuBackend;
1132    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1133    ///
1134    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
1135    /// 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();
1136    /// let y = x.reduce_min(None).unwrap();
1137    ///
1138    /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[1.0]);
1139    /// # Ok::<(), tenferro_ad::Error>(())
1140    /// ```
1141    /// # Errors
1142    ///
1143    /// Returns [`tenferro_tensor::Error::Validation`] with `AxisOutOfBounds` or
1144    /// `DuplicateAxis` for an invalid reduction axis, or a typed
1145    /// unsupported/backend/runtime-state error for the selected dtype.
1146    pub fn reduce_min(&self, axes: Option<&[usize]>) -> Result<Self> {
1147        let axes = axes.map_or_else(|| (0..self.shape().len()).collect(), <[usize]>::to_vec);
1148        validate_eager_axes("EagerTensor::reduce_min", self.shape().len(), &axes)?;
1149        self.unary_op(StdTensorOp::ReduceMin { axes })
1150    }
1151
1152    pub(crate) fn unary_op(&self, op: StdTensorOp) -> Result<Self> {
1153        Self::nary_op(&[self], op)
1154    }
1155
1156    pub(crate) fn binary_op(&self, other: &Self, op: StdTensorOp) -> Result<Self> {
1157        Self::nary_op(&[self, other], op)
1158    }
1159
1160    pub(crate) fn ternary_op(&self, b: &Self, c: &Self, op: StdTensorOp) -> Result<Self> {
1161        Self::nary_op(&[self, b, c], op)
1162    }
1163
1164    pub(crate) fn nary_value_op(
1165        tensors: &[&Self],
1166        op: StdTensorOp,
1167        value: TensorValue,
1168    ) -> Result<Self> {
1169        let Some(first) = tensors.first() else {
1170            return Err(empty_nary_input_error(&op));
1171        };
1172
1173        let ctx = Arc::clone(&first.ctx);
1174        for tensor in tensors.iter().skip(1) {
1175            if !first.same_context(tensor) {
1176                return Err(Error::ContextMismatch {
1177                    lhs: first.ctx_id(),
1178                    rhs: tensor.ctx_id(),
1179                });
1180            }
1181        }
1182
1183        if !eager_grad_recording_enabled()
1184            || (!eager_capture_active() && !tensors.iter().any(|tensor| tensor.requires_grad))
1185        {
1186            return Self::new_untracked_value_result(ctx, value);
1187        }
1188
1189        let output_ref = &value;
1190        let mut recorded = record_eager_value_outputs(&op, &[output_ref], tensors)?;
1191        let trace = recorded.traces.pop().ok_or_else(|| {
1192            Error::Internal(format!("expected one eager trace for {:?}, got 0", op))
1193        })?;
1194        let semantic_trace = recorded.semantic_traces.pop().flatten();
1195
1196        Self::new_result_value(
1197            ctx,
1198            trace.key,
1199            value,
1200            trace.requires_grad,
1201            trace.trace,
1202            semantic_trace,
1203        )
1204    }
1205
1206    pub(crate) fn nary_op(tensors: &[&Self], op: StdTensorOp) -> Result<Self> {
1207        let total_started = eager_op_profile_start();
1208        let Some(first) = tensors.first() else {
1209            return Err(empty_nary_input_error(&op));
1210        };
1211        let expected = op.input_count();
1212        if tensors.len() != expected {
1213            return Err(wrong_nary_input_count_error(&op, expected, tensors.len()));
1214        }
1215
1216        let ctx = Arc::clone(&first.ctx);
1217        profile_eager_op_section("nary_op.context_check", || -> Result<()> {
1218            for tensor in tensors.iter().skip(1) {
1219                if !first.same_context(tensor) {
1220                    return Err(Error::ContextMismatch {
1221                        lhs: first.ctx_id(),
1222                        rhs: tensor.ctx_id(),
1223                    });
1224                }
1225            }
1226            Ok(())
1227        })?;
1228
1229        let any_requires_grad = profile_eager_op_section("nary_op.requires_grad_scan", || {
1230            eager_grad_recording_enabled()
1231                && (eager_capture_active() || tensors.iter().any(|tensor| tensor.requires_grad))
1232        });
1233        if !any_requires_grad {
1234            let input_reads = profile_eager_op_section("nary_op.collect_input_reads", || {
1235                tensors
1236                    .iter()
1237                    .map(|tensor| tensor.tensor_read())
1238                    .collect::<Vec<_>>()
1239            });
1240            let output = profile_eager_op_section("nary_op.exec_single_output_read", || {
1241                exec_single_output_read(&op, &input_reads, &ctx)
1242            })?;
1243            let result = profile_eager_op_section("nary_op.new_untracked_result", || {
1244                Self::new_untracked_result(ctx, output)
1245            });
1246            if let Some(total_started) = total_started {
1247                record_eager_op_profile("nary_op.total", total_started.elapsed());
1248                maybe_print_eager_op_profile();
1249            }
1250            return result;
1251        }
1252
1253        let input_arcs = profile_eager_op_section("nary_op.materialize_inputs", || {
1254            tensors
1255                .iter()
1256                .map(|tensor| tensor.to_tensor().map(Arc::new))
1257                .collect::<Result<Vec<_>>>()
1258        })?;
1259        let inputs: Vec<&Tensor> = profile_eager_op_section("nary_op.collect_inputs", || {
1260            input_arcs.iter().map(|tensor| tensor.as_ref()).collect()
1261        });
1262        let output = profile_eager_op_section("nary_op.exec_single_output", || {
1263            exec_single_output(&op, &inputs, &ctx)
1264        })?;
1265
1266        let outputs = vec![&output];
1267        let mut recorded = profile_eager_op_section("nary_op.record_outputs", || {
1268            record_eager_outputs(&op, &outputs, tensors)
1269        })?;
1270        let trace = recorded.traces.pop().ok_or_else(|| {
1271            Error::Internal(format!("expected one eager trace for {:?}, got 0", op))
1272        })?;
1273        let semantic_trace = recorded.semantic_traces.pop().flatten();
1274
1275        let result = profile_eager_op_section("nary_op.new_tracked_result", || {
1276            Self::new_result_with_semantic_trace(
1277                ctx,
1278                trace.key,
1279                output,
1280                trace.requires_grad,
1281                trace.trace,
1282                semantic_trace,
1283            )
1284        });
1285        if let Some(total_started) = total_started {
1286            record_eager_op_profile("nary_op.total", total_started.elapsed());
1287            maybe_print_eager_op_profile();
1288        }
1289        result
1290    }
1291}
1292
1293fn validate_eager_axes(op: &'static str, rank: usize, axes: &[usize]) -> Result<()> {
1294    tenferro_tensor::validate::validate_unique_axes(op, "axis", rank, axes)
1295        .map_err(Error::TensorRuntime)
1296}
1297
1298fn validate_eager_dot_general_config(
1299    _op: &'static str,
1300    config: &DotGeneralConfig,
1301    lhs_rank: usize,
1302    rhs_rank: usize,
1303) -> Result<()> {
1304    config
1305        .validate_dims_with_ranks(lhs_rank, rhs_rank)
1306        .map_err(Error::TensorRuntime)
1307}
1308
1309fn empty_nary_input_error(op: &StdTensorOp) -> Error {
1310    Error::TensorRuntime(tenferro_tensor::Error::invalid_argument(
1311        eager_validation_op_name(op),
1312        "inputs",
1313        "operation requires at least one input tensor",
1314    ))
1315}
1316
1317fn wrong_nary_input_count_error(op: &StdTensorOp, expected: usize, actual: usize) -> Error {
1318    Error::TensorRuntime(tenferro_tensor::Error::invalid_argument(
1319        eager_validation_op_name(op),
1320        "inputs",
1321        format!("operation expects {expected} inputs, got {actual}"),
1322    ))
1323}
1324
1325fn eager_validation_op_name(op: &StdTensorOp) -> &'static str {
1326    match op {
1327        StdTensorOp::Concatenate { .. } => "concatenate",
1328        _ => "eager_nary_op",
1329    }
1330}
1331
1332fn finite_real_factor(value: f64) -> Result<f64> {
1333    if value.is_finite() {
1334        Ok(value)
1335    } else {
1336        Err(Error::TensorRuntime(
1337            tenferro_tensor::Error::invalid_argument(
1338                "scale_real",
1339                "factor",
1340                format!("real scalar must be finite, got {value}"),
1341            ),
1342        ))
1343    }
1344}
1345
1346fn round_real_to_i64(value: f64) -> Result<i64> {
1347    let rounded = finite_real_factor(value)?.round();
1348    if rounded < i64::MIN as f64 || rounded >= -(i64::MIN as f64) {
1349        return Err(Error::TensorRuntime(
1350            tenferro_tensor::Error::invalid_argument(
1351                "scale_real",
1352                "factor",
1353                format!("rounded real scalar {rounded} is out of i64 range"),
1354            ),
1355        ));
1356    }
1357    Ok(rounded as i64)
1358}
1359
1360fn round_real_to_i32(value: f64) -> Result<i32> {
1361    let rounded = round_real_to_i64(value)?;
1362    i32::try_from(rounded).map_err(|_| {
1363        Error::TensorRuntime(tenferro_tensor::Error::invalid_argument(
1364            "scale_real",
1365            "factor",
1366            format!("rounded real scalar {rounded} is out of i32 range"),
1367        ))
1368    })
1369}
1370
1371fn bool_from_real(value: f64) -> Result<bool> {
1372    Ok(finite_real_factor(value)? != 0.0)
1373}