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