tenferro_runtime/traced.rs
1use std::fmt;
2use std::sync::atomic::{AtomicU64, Ordering};
3use std::sync::Arc;
4
5use num_complex::{Complex32, Complex64};
6use tenferro_ops::broadcast::{
7 broadcast_error_to_validation, broadcast_in_dim_extent_error, broadcast_input_plan,
8 broadcast_shape, broadcast_shapes, BroadcastError,
9};
10use tenferro_ops::dim_expr::DimExpr;
11use tenferro_ops::input_key::TensorInputKey;
12use tenferro_ops::std_tensor_op::StdTensorOp;
13use tenferro_tensor::{
14 CompareDir, DType, DotGeneralConfig, Error as TensorError, GatherConfig, IntoShapeVec,
15 PadConfig, ScatterConfig, ShapeMismatch, SliceConfig, ValidationError,
16};
17
18use super::error::{Error, ErrorPhase, Result};
19use super::sym_dim::SymDim;
20use crate::checkpoint::RetainedValue;
21use crate::scalar_semantics::{bool_from_real_for_op, round_real_to_i32_for_op, round_real_to_i64};
22use crate::trace::{TraceContext, TraceValue};
23use crate::trace_ops::{append_core, finish_value};
24
25static NEXT_INPUT_ID: AtomicU64 = AtomicU64::new(0);
26
27pub(crate) fn next_input_key() -> TensorInputKey {
28 TensorInputKey::User {
29 id: NEXT_INPUT_ID.fetch_add(1, Ordering::Relaxed),
30 }
31}
32
33/// A typed view of one semantic value inside a [`TraceContext`].
34///
35/// `rank`, `dtype` and `shape_hint` are an immutable cache of the context
36/// metadata this value was appended with; the context keeps the authoritative
37/// metadata.
38#[derive(Clone)]
39pub struct TracedTensor {
40 pub(crate) ctx: TraceContext,
41 pub(crate) value: TraceValue,
42 pub(crate) rank: usize,
43 pub(crate) dtype: DType,
44 pub(crate) shape_hint: Option<Vec<SymDim>>,
45 pub(crate) data: Option<Arc<RetainedValue>>,
46 /// Identity this tensor's axes use in `shape_hint`, unique per handle.
47 pub(crate) symbolic_id: u64,
48}
49
50impl fmt::Debug for TracedTensor {
51 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52 f.debug_struct("TracedTensor")
53 .field("rank", &self.rank)
54 .field("dtype", &self.dtype)
55 .field("shape_hint", &self.shape_hint)
56 .field("has_data", &self.data.is_some())
57 .finish_non_exhaustive()
58 }
59}
60
61pub(crate) fn try_concrete_shape(tensor: &TracedTensor) -> Option<Vec<usize>> {
62 tensor
63 .shape_hint
64 .as_ref()?
65 .iter()
66 .map(SymDim::constant_value)
67 .collect()
68}
69
70fn graph_validation(op: &'static str, source: impl Into<ValidationError>) -> Error {
71 Error::validation(op, ErrorPhase::GraphBuild, source.into())
72}
73
74fn graph_invalid_argument(
75 op: &'static str,
76 argument: &'static str,
77 message: impl Into<String>,
78) -> Error {
79 graph_validation(
80 op,
81 ValidationError::InvalidArgument {
82 argument,
83 message: message.into(),
84 },
85 )
86}
87
88fn graph_broadcast_error(op: &'static str, error: BroadcastError) -> Error {
89 graph_validation(op, broadcast_error_to_validation(error))
90}
91
92fn graph_tensor_error(op: &'static str, error: TensorError) -> Error {
93 match error {
94 TensorError::Validation { source, .. } => graph_validation(op, source),
95 other => Error::TensorRuntime(other),
96 }
97}
98
99fn graph_error_with_context(op: &'static str, error: Error) -> Error {
100 match error {
101 Error::Validation { source, .. } => graph_validation(op, source),
102 other => other,
103 }
104}
105
106pub(crate) fn concrete_shape(tensor: &TracedTensor) -> Result<Vec<usize>> {
107 tensor
108 .shape_hint
109 .as_ref()
110 .ok_or_else(|| {
111 graph_invalid_argument(
112 "TracedTensor::concrete_shape",
113 "shape",
114 format!(
115 "missing shape hint for traced tensor at rank {}",
116 tensor.rank
117 ),
118 )
119 })?
120 .iter()
121 .map(|dim| {
122 dim.constant_value().ok_or_else(|| {
123 graph_invalid_argument(
124 "TracedTensor::concrete_shape",
125 "shape",
126 format!(
127 "symbolic dimension in shape hint for tensor at rank {}",
128 tensor.rank
129 ),
130 )
131 })
132 })
133 .collect()
134}
135
136/// Broadcast a traced tensor to `target_shape`.
137///
138/// Expanding singleton axes are first reshaped away so the existing
139/// `BroadcastInDim` transpose rule reduces them correctly during VJP.
140pub(crate) fn broadcast_to(tensor: &TracedTensor, target_shape: &[usize]) -> Result<TracedTensor> {
141 let tensor_shape = concrete_shape(tensor)?;
142 if tensor_shape == target_shape {
143 return Ok(tensor.clone());
144 }
145
146 let plan = broadcast_input_plan(&tensor_shape, target_shape)
147 .map_err(|err| graph_broadcast_error("broadcast_to", err))?;
148
149 let source = if plan.source_shape == tensor_shape {
150 tensor.clone()
151 } else {
152 tensor.reshape(&plan.source_shape)?
153 };
154 source.broadcast_in_dim(target_shape, &plan.dims)
155}
156
157/// Broadcast two tensors to a common shape.
158pub(crate) fn broadcast_binary(
159 a: &TracedTensor,
160 b: &TracedTensor,
161) -> Result<(TracedTensor, TracedTensor)> {
162 if a.shape_hint == b.shape_hint && a.rank == b.rank {
163 return Ok((a.clone(), b.clone()));
164 }
165 if (try_concrete_shape(a).is_none() || try_concrete_shape(b).is_none()) && a.rank == b.rank {
166 return Ok((a.clone(), b.clone()));
167 }
168 let a_shape = concrete_shape(a)?;
169 let b_shape = concrete_shape(b)?;
170 let target = broadcast_shape(&a_shape, &b_shape)
171 .map_err(|err| graph_broadcast_error("broadcast_binary", err))?;
172 Ok((broadcast_to(a, &target)?, broadcast_to(b, &target)?))
173}
174
175pub(crate) fn broadcast_ternary(
176 a: &TracedTensor,
177 b: &TracedTensor,
178 c: &TracedTensor,
179) -> Result<(TracedTensor, TracedTensor, TracedTensor)> {
180 let a_shape = concrete_shape(a)?;
181 let b_shape = concrete_shape(b)?;
182 let c_shape = concrete_shape(c)?;
183 let target = broadcast_shapes([a_shape.as_slice(), b_shape.as_slice(), c_shape.as_slice()])
184 .map_err(|err| graph_broadcast_error("broadcast_ternary", err))?;
185 Ok((
186 broadcast_to(a, &target)?,
187 broadcast_to(b, &target)?,
188 broadcast_to(c, &target)?,
189 ))
190}
191
192fn scale_with_constant(input: &TracedTensor, op: StdTensorOp) -> Result<TracedTensor> {
193 let scalar = apply_nullary(op, 0, input.dtype, Some(vec![]), &input.ctx)?;
194 // A binary op applies to operands of one shape, so the rank-0 factor has to be
195 // broadcast here, exactly as `TracedTensor::add`/`mul` do through
196 // `broadcast_binary`. Without it the graph carries a rank-0 `Mul` operand: the
197 // CPU backend happens to accept one, the CUDA backend rejects it with
198 // `incompatible shapes: lhs=[], rhs=<input shape>`, so the same graph would run
199 // on one backend and not the other.
200 let (input, scalar) = broadcast_binary(input, &scalar)?;
201 apply_binary(
202 StdTensorOp::Mul,
203 &input,
204 &scalar,
205 input.rank,
206 input.shape_hint.clone(),
207 )
208}
209
210fn try_inferred_output_dtype(
211 op: &StdTensorOp,
212 inputs: &[DType],
213 context: &'static str,
214) -> Result<DType> {
215 crate::shape_infer::infer_output_dtype_at(op, inputs, ErrorPhase::GraphBuild)
216 .map_err(|err| graph_error_with_context(context, err))
217}
218
219fn checked_shape_product_for_graph_build(
220 shape: &[usize],
221 context: &'static str,
222 _role: &'static str,
223) -> Result<usize> {
224 shape.iter().copied().try_fold(1usize, |acc, dim| {
225 acc.checked_mul(dim)
226 .ok_or_else(|| graph_validation(context, ValidationError::IntegerOverflow))
227 })
228}
229
230fn validate_concrete_reshape_shape(input: &TracedTensor, shape: &[usize]) -> Result<()> {
231 let to = checked_shape_product_for_graph_build(shape, "TracedTensor::reshape", "target")?;
232 let Some(input_shape) = try_concrete_shape(input) else {
233 return Ok(());
234 };
235 let from =
236 checked_shape_product_for_graph_build(&input_shape, "TracedTensor::reshape", "input")?;
237 if from != to {
238 return Err(graph_validation(
239 "TracedTensor::reshape",
240 ShapeMismatch::ReshapeElementCount { from, to },
241 ));
242 }
243 Ok(())
244}
245
246fn traced_input_shape_exprs(input_idx: usize, tensor: &TracedTensor) -> Vec<DimExpr> {
247 match tensor.shape_hint.as_ref() {
248 Some(shape) => shape
249 .iter()
250 .enumerate()
251 .map(|(axis, dim)| {
252 dim.constant_value()
253 .map_or(DimExpr::InputDim { input_idx, axis }, DimExpr::Const)
254 })
255 .collect(),
256 None => (0..tensor.rank)
257 .map(|axis| DimExpr::InputDim { input_idx, axis })
258 .collect(),
259 }
260}
261
262fn traced_input_sym_shape(tensor: &TracedTensor) -> Vec<SymDim> {
263 tensor.shape_hint.clone().unwrap_or_else(|| {
264 (0..tensor.rank)
265 .map(|axis| SymDim::tensor_axis(tensor.symbolic_id(), axis))
266 .collect()
267 })
268}
269
270pub(crate) fn infer_traced_single_output_shape(
271 op_name: &'static str,
272 op: &StdTensorOp,
273 inputs: &[&TracedTensor],
274) -> Result<(usize, Option<Vec<SymDim>>)> {
275 let input_shape_exprs: Vec<Vec<DimExpr>> = inputs
276 .iter()
277 .enumerate()
278 .map(|(input_idx, tensor)| traced_input_shape_exprs(input_idx, tensor))
279 .collect();
280 let input_shape_refs: Vec<&[DimExpr]> = input_shape_exprs.iter().map(Vec::as_slice).collect();
281 let output_shapes = crate::shape_infer::infer_output_shapes(op, &input_shape_refs)
282 .map_err(|err| graph_error_with_context(op_name, err))?;
283 let output_shape = output_shapes.first().ok_or_else(|| {
284 Error::Internal(format!("{op_name}: shape inference returned no outputs"))
285 })?;
286 if output_shapes.len() != 1 {
287 return Err(Error::Internal(format!(
288 "{op_name}: expected single-output shape inference, got {} outputs",
289 output_shapes.len()
290 )));
291 }
292
293 let input_sym_shapes: Vec<Vec<SymDim>> = inputs
294 .iter()
295 .map(|tensor| traced_input_sym_shape(tensor))
296 .collect();
297 let input_sym_refs: Vec<&[SymDim]> = input_sym_shapes.iter().map(Vec::as_slice).collect();
298 let out_shape_hint = output_shape
299 .iter()
300 .map(|dim| SymDim::from_dim_expr(dim, &input_sym_refs))
301 .collect();
302 Ok((output_shape.len(), Some(out_shape_hint)))
303}
304
305fn reduction_output_meta(
306 tensor: &TracedTensor,
307 axes: &[usize],
308 op: &'static str,
309) -> Result<(usize, Option<Vec<SymDim>>)> {
310 let mut seen = vec![false; tensor.rank];
311 for &axis in axes {
312 if axis >= tensor.rank {
313 return Err(graph_validation(
314 op,
315 ValidationError::AxisOutOfBounds {
316 axis,
317 rank: tensor.rank,
318 },
319 ));
320 }
321 if seen[axis] {
322 return Err(graph_validation(
323 op,
324 ValidationError::DuplicateAxis {
325 axis,
326 role: "reduction",
327 },
328 ));
329 }
330 seen[axis] = true;
331 }
332
333 let out_shape_hint = tensor.shape_hint.as_ref().map(|shape| {
334 (0..shape.len())
335 .filter(|d| !axes.contains(d))
336 .map(|d| shape[d].clone())
337 .collect()
338 });
339 Ok((tensor.rank - axes.len(), out_shape_hint))
340}
341
342fn validate_traced_axis(tensor: &TracedTensor, axis: usize, op: &'static str) -> Result<()> {
343 if axis >= tensor.rank {
344 return Err(graph_validation(
345 op,
346 ValidationError::AxisOutOfBounds {
347 axis,
348 rank: tensor.rank,
349 },
350 ));
351 }
352 Ok(())
353}
354
355fn validate_traced_axes(rank: usize, axes: &[usize], op: &'static str) -> Result<()> {
356 let mut seen = vec![false; rank];
357 for &axis in axes {
358 if axis >= rank {
359 return Err(graph_validation(
360 op,
361 ValidationError::AxisOutOfBounds { axis, rank },
362 ));
363 }
364 if seen[axis] {
365 return Err(graph_validation(
366 op,
367 ValidationError::DuplicateAxis { axis, role: "axis" },
368 ));
369 }
370 seen[axis] = true;
371 }
372 Ok(())
373}
374
375fn validate_traced_insert_axis(rank: usize, axis: usize, op: &'static str) -> Result<()> {
376 if axis > rank {
377 return Err(graph_invalid_argument(
378 op,
379 "axis",
380 format!("axis {axis} out of bounds for rank {rank} insertion"),
381 ));
382 }
383 Ok(())
384}
385
386fn validate_traced_perm(rank: usize, perm: &[usize], op: &'static str) -> Result<()> {
387 if perm.len() != rank {
388 return Err(graph_validation(
389 op,
390 ValidationError::InvalidPermutationLength {
391 expected: rank,
392 actual: perm.len(),
393 },
394 ));
395 }
396 let mut seen = vec![false; rank];
397 for &axis in perm {
398 if axis >= rank {
399 return Err(graph_validation(
400 op,
401 ValidationError::AxisOutOfBounds { axis, rank },
402 ));
403 }
404 if seen[axis] {
405 return Err(graph_validation(
406 op,
407 ValidationError::DuplicateAxis {
408 axis,
409 role: "permutation",
410 },
411 ));
412 }
413 seen[axis] = true;
414 }
415 Ok(())
416}
417
418fn validate_broadcast_in_dim_args(
419 input: &TracedTensor,
420 output_shape: &[SymDim],
421 dims: &[usize],
422 op: &'static str,
423) -> Result<()> {
424 if dims.len() != input.rank {
425 return Err(graph_validation(
426 op,
427 ValidationError::RankMismatch {
428 expected: input.rank,
429 actual: dims.len(),
430 },
431 ));
432 }
433
434 let concrete_input_shape: Option<Vec<usize>> = input
435 .shape_hint
436 .as_ref()
437 .and_then(|shape| shape.iter().map(SymDim::constant_value).collect());
438 let concrete_output_shape = output_shape
439 .iter()
440 .map(SymDim::constant_value)
441 .collect::<Option<Vec<_>>>();
442 if let (Some(input_shape), Some(output_shape)) = (
443 concrete_input_shape.as_deref(),
444 concrete_output_shape.as_deref(),
445 ) && let Some(error) = broadcast_in_dim_extent_error(input_shape, output_shape, dims)
446 {
447 return Err(graph_broadcast_error(op, error));
448 }
449
450 let mut seen = vec![false; output_shape.len()];
451 for &dim in dims {
452 if dim >= output_shape.len() {
453 return Err(graph_validation(
454 op,
455 ValidationError::AxisOutOfBounds {
456 axis: dim,
457 rank: output_shape.len(),
458 },
459 ));
460 }
461 if seen[dim] {
462 return Err(graph_validation(
463 op,
464 ValidationError::DuplicateAxis {
465 axis: dim,
466 role: "broadcast",
467 },
468 ));
469 }
470 seen[dim] = true;
471 }
472
473 Ok(())
474}
475
476impl std::ops::Add for &TracedTensor {
477 type Output = Result<TracedTensor>;
478
479 fn add(self, rhs: &TracedTensor) -> Result<TracedTensor> {
480 TracedTensor::add(self, rhs)
481 }
482}
483
484impl std::ops::Sub for &TracedTensor {
485 type Output = Result<TracedTensor>;
486
487 fn sub(self, rhs: &TracedTensor) -> Result<TracedTensor> {
488 TracedTensor::sub(self, rhs)
489 }
490}
491
492impl std::ops::Mul for &TracedTensor {
493 type Output = Result<TracedTensor>;
494
495 fn mul(self, rhs: &TracedTensor) -> Result<TracedTensor> {
496 TracedTensor::mul(self, rhs)
497 }
498}
499
500impl std::ops::Mul<f64> for &TracedTensor {
501 type Output = Result<TracedTensor>;
502
503 fn mul(self, rhs: f64) -> Result<TracedTensor> {
504 self.scale_real(rhs)
505 }
506}
507
508impl std::ops::Mul<&TracedTensor> for f64 {
509 type Output = Result<TracedTensor>;
510
511 fn mul(self, rhs: &TracedTensor) -> Result<TracedTensor> {
512 rhs.scale_real(self)
513 }
514}
515
516impl std::ops::Neg for &TracedTensor {
517 type Output = Result<TracedTensor>;
518
519 fn neg(self) -> Self::Output {
520 TracedTensor::neg(self)
521 }
522}
523
524impl std::ops::Div for &TracedTensor {
525 type Output = Result<TracedTensor>;
526
527 fn div(self, rhs: &TracedTensor) -> Result<TracedTensor> {
528 TracedTensor::div(self, rhs)
529 }
530}
531
532impl std::ops::Rem for &TracedTensor {
533 type Output = Result<TracedTensor>;
534
535 fn rem(self, rhs: &TracedTensor) -> Result<TracedTensor> {
536 TracedTensor::rem(self, rhs)
537 }
538}
539
540impl TracedTensor {
541 /// Return the concrete tensor data attached to this traced value, if any.
542 ///
543 /// Placeholder tensors created with `input_concrete_shape` or
544 /// `input_symbolic_shape` have no attached data until execution bindings
545 /// provide it.
546 ///
547 /// # Examples
548 ///
549 /// ```
550 /// use tenferro_runtime::{TraceContext, DType, TracedTensor};
551 ///
552 /// let trace = TraceContext::new();
553 /// let concrete = trace.from_vec_col_major(vec![1], vec![1.0_f64]).unwrap();
554 /// assert!(concrete.attached_value().is_some());
555 ///
556 /// let placeholder = trace.input_symbolic_shape(DType::F64, 1).unwrap();
557 /// assert!(placeholder.attached_value().is_none());
558 /// ```
559 pub fn attached_value(&self) -> Option<&Arc<RetainedValue>> {
560 self.data.as_ref()
561 }
562
563 /// Return the tensor element dtype recorded for this traced value.
564 ///
565 /// # Examples
566 ///
567 /// ```
568 /// use tenferro_runtime::{DType, TraceContext};
569 ///
570 /// let trace = TraceContext::new();
571 /// let x = trace.from_vec_col_major(vec![2], vec![1.0_f64, 2.0])?;
572 /// assert_eq!(x.dtype(), DType::F64);
573 /// # Ok::<(), Box<dyn std::error::Error>>(())
574 /// ```
575 pub fn dtype(&self) -> DType {
576 self.dtype
577 }
578
579 /// Return the rank recorded for this traced value.
580 ///
581 /// # Examples
582 ///
583 /// ```
584 /// use tenferro_runtime::TraceContext;
585 ///
586 /// let trace = TraceContext::new();
587 /// let matrix = trace.from_vec_col_major(vec![2, 3], vec![0.0_f64; 6])?;
588 /// assert_eq!(matrix.rank(), 2);
589 /// assert_eq!(matrix.neg()?.rank(), 2);
590 /// # Ok::<(), Box<dyn std::error::Error>>(())
591 /// ```
592 pub fn rank(&self) -> usize {
593 self.rank
594 }
595
596 /// Borrow the trace context this value belongs to.
597 ///
598 /// # Examples
599 ///
600 /// ```
601 /// use tenferro_runtime::TraceContext;
602 ///
603 /// let trace = TraceContext::new();
604 /// let x = trace.from_vec_col_major(vec![2], vec![1.0_f64, 2.0])?;
605 /// let y = x.neg()?;
606 ///
607 /// // A derived value stays in its producer's context.
608 /// assert!(y.context().same_context(&trace));
609 /// # Ok::<(), Box<dyn std::error::Error>>(())
610 /// ```
611 pub fn context(&self) -> &TraceContext {
612 &self.ctx
613 }
614
615 /// Return the context value behind this tensor.
616 #[doc(hidden)]
617 pub fn trace_value(&self) -> TraceValue {
618 self.value
619 }
620
621 /// Replace this tensor's value with a bound input of the same context.
622 ///
623 /// Used by checkpointing, which rebinds the traced value to the input that
624 /// holds the evaluated data.
625 #[doc(hidden)]
626 pub fn rebind_bound(&mut self, value: TraceValue, data: Arc<RetainedValue>) -> Result<()> {
627 let rebased = crate::trace_ops::finish_bound_value(&self.ctx, value, data)?;
628 *self = rebased;
629 Ok(())
630 }
631
632 /// Symbolic tensor id used for rank-only shape hints.
633 fn symbolic_id(&self) -> u64 {
634 self.symbolic_id
635 }
636
637 /// Returns `true` iff every dim of this tensor's `shape_hint` is a
638 /// constant `SymDim` (i.e. the shape is fully known at graph-build time).
639 ///
640 /// # Examples
641 ///
642 /// ```
643 /// use tenferro_tensor::DType;
644 /// use tenferro_runtime::{TracedTensor};
645 ///
646 /// use tenferro_runtime::TraceContext;
647 /// let trace = TraceContext::new();
648 /// let a = trace.from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap();
649 /// let b = trace.input_symbolic_shape(DType::F64, 2).unwrap();
650 /// assert!(a.is_concrete_shape());
651 /// assert!(!b.is_concrete_shape());
652 /// ```
653 pub fn is_concrete_shape(&self) -> bool {
654 try_concrete_shape(self).is_some()
655 }
656
657 /// Return the fully-concrete shape of this tensor, if every dim of
658 /// its shape-hint is a constant `SymDim`. Returns `None` if any
659 /// dimension is symbolic.
660 ///
661 /// This is the counterpart to [`Self::is_concrete_shape`] for callers
662 /// that need to *use* the concrete shape (e.g. external composition
663 /// wrappers building `broadcast_in_dim` payloads from known shapes).
664 ///
665 /// # Examples
666 ///
667 /// ```
668 /// use tenferro_tensor::DType;
669 /// use tenferro_runtime::{TracedTensor};
670 ///
671 /// use tenferro_runtime::TraceContext;
672 /// let trace = TraceContext::new();
673 /// let a = trace.from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap();
674 /// assert_eq!(a.try_concrete_shape(), Some(vec![2, 3]));
675 ///
676 /// let b = trace.input_symbolic_shape(DType::F64, 2).unwrap();
677 /// assert!(b.try_concrete_shape().is_none());
678 /// ```
679 pub fn try_concrete_shape(&self) -> Option<Vec<usize>> {
680 try_concrete_shape(self)
681 }
682
683 /// Return the concrete tensor shape.
684 ///
685 /// Returns an error when a shape hint is missing or any dimension is
686 /// symbolic. Composite traced ops that require concrete sizes should
687 /// propagate this error instead of panicking.
688 ///
689 /// # Errors
690 ///
691 /// Returns [`Error::Validation`] with `InvalidArgument` when this tensor
692 /// has no shape hint or any dimension is symbolic.
693 pub fn concrete_shape(&self) -> Result<Vec<usize>> {
694 concrete_shape(self)
695 }
696
697 /// If this `TracedTensor` is a leaf (single-node input graph),
698 /// return its input key. Computed tensors return `None`.
699 pub fn input_key(&self) -> Option<TensorInputKey> {
700 self.ctx.input_key(self.value)
701 }
702
703 /// Elementwise addition with NumPy-style broadcasting.
704 ///
705 /// Prefer using the `+` operator when it reads naturally.
706 ///
707 /// A longer expression such as `a + b + c` does not compose because the
708 /// first `+` returns `Result<TracedTensor, Error>`, so the second `+`
709 /// would receive a result rather than a tensor. Use `?` at each step or
710 /// the explicit fallible method chain shown below when the operation
711 /// sequence is more important than notation:
712 ///
713 /// # Examples
714 ///
715 /// ```rust
716 /// # use tenferro_runtime::{Error, TracedTensor};
717 /// use tenferro_runtime::TraceContext;
718 /// let trace = TraceContext::new();
719 /// # let x = trace.from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
720 /// # let z = trace.from_vec_col_major(vec![2], vec![3.0_f64, 4.0]).unwrap();
721 /// let y = x.add(&z);
722 /// let y2 = &x + &z;
723 /// # fn add_three(
724 /// # a: &TracedTensor,
725 /// # b: &TracedTensor,
726 /// # c: &TracedTensor,
727 /// # ) -> Result<TracedTensor, Error> {
728 /// let ab = (a + b)?;
729 /// let sum = (&ab + c)?;
730 /// let method_chain = a.add(b)?.add(c)?;
731 /// let _ = method_chain;
732 /// # Ok(sum)
733 /// # }
734 /// ```
735 ///
736 /// Tenferro prioritizes robust error handling over the conciseness of
737 /// chained operator notation; the explicit fallible methods are the
738 /// canonical form for longer sequences.
739 ///
740 /// # Errors
741 ///
742 /// Returns [`Error::Validation`] with `ShapeMismatch` when operand shapes
743 /// cannot be broadcast, or [`Error::RuntimeStateSource`] when graph
744 /// metadata registration fails.
745 ///
746 /// # Deferred errors
747 ///
748 /// If symbolic dimensions prevent shape comparison during graph
749 /// construction, the same `ShapeMismatch` can be reported during
750 /// compilation or execution, with the corresponding [`ErrorPhase`].
751 pub fn add(&self, other: &TracedTensor) -> Result<TracedTensor> {
752 let (lhs, rhs) = broadcast_binary(self, other)?;
753 apply_binary(
754 StdTensorOp::Add,
755 &lhs,
756 &rhs,
757 lhs.rank,
758 lhs.shape_hint.clone(),
759 )
760 }
761
762 /// Elementwise subtraction with NumPy-style broadcasting.
763 ///
764 /// Prefer using the `-` operator when it reads naturally.
765 ///
766 /// # Errors
767 ///
768 /// Returns [`Error::Validation`] with `ShapeMismatch` when operand shapes
769 /// cannot be broadcast, or [`Error::RuntimeStateSource`] when graph
770 /// metadata registration fails.
771 ///
772 /// # Deferred errors
773 ///
774 /// If symbolic dimensions prevent shape comparison during graph
775 /// construction, the same `ShapeMismatch` can be reported during
776 /// compilation or execution, with the corresponding [`ErrorPhase`].
777 pub fn sub(&self, other: &TracedTensor) -> Result<TracedTensor> {
778 let (lhs, rhs) = broadcast_binary(self, other)?;
779 apply_binary(
780 StdTensorOp::Sub,
781 &lhs,
782 &rhs,
783 lhs.rank,
784 lhs.shape_hint.clone(),
785 )
786 }
787
788 /// Elementwise multiplication with NumPy-style broadcasting.
789 ///
790 /// Prefer using the `*` operator when it reads naturally.
791 ///
792 /// # Examples
793 ///
794 /// ```rust
795 /// # use tenferro_runtime::{TracedTensor};
796 /// use tenferro_runtime::TraceContext;
797 /// let trace = TraceContext::new();
798 /// # let x = trace.from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
799 /// # let z = trace.from_vec_col_major(vec![2], vec![3.0_f64, 4.0]).unwrap();
800 /// let y = x.mul(&z);
801 /// let y2 = &x * &z;
802 /// ```
803 ///
804 /// # Errors
805 ///
806 /// Returns [`Error::Validation`] with `ShapeMismatch` when operand shapes
807 /// cannot be broadcast, or [`Error::RuntimeStateSource`] when graph
808 /// metadata registration fails.
809 ///
810 /// # Deferred errors
811 ///
812 /// If symbolic ranks prevent shape comparison during graph construction,
813 /// the same `ShapeMismatch` can be reported during compilation or
814 /// execution, with the corresponding [`ErrorPhase`].
815 pub fn mul(&self, other: &TracedTensor) -> Result<TracedTensor> {
816 let (lhs, rhs) = broadcast_binary(self, other)?;
817 apply_binary(
818 StdTensorOp::Mul,
819 &lhs,
820 &rhs,
821 lhs.rank,
822 lhs.shape_hint.clone(),
823 )
824 }
825
826 /// Elementwise division with NumPy-style broadcasting.
827 ///
828 /// Prefer using the `/` operator when it reads naturally.
829 ///
830 /// # Examples
831 ///
832 /// ```rust
833 /// # use tenferro_runtime::{TracedTensor};
834 /// use tenferro_runtime::TraceContext;
835 /// let trace = TraceContext::new();
836 /// # let x = trace.from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
837 /// # let z = trace.from_vec_col_major(vec![2], vec![3.0_f64, 4.0]).unwrap();
838 /// let y = x.div(&z);
839 /// let y2 = &x / &z;
840 /// ```
841 ///
842 /// # Errors
843 ///
844 /// Returns [`Error::Validation`] with `ShapeMismatch` when operand shapes
845 /// cannot be broadcast, or [`Error::RuntimeStateSource`] when graph
846 /// metadata registration fails.
847 ///
848 /// # Deferred errors
849 ///
850 /// If symbolic ranks prevent shape comparison during graph construction,
851 /// the same `ShapeMismatch` can be reported during compilation or
852 /// execution, with the corresponding [`ErrorPhase`]. For integer inputs,
853 /// a zero divisor is reported during execution as
854 /// [`Error::TensorRuntime`] containing a
855 /// [`tenferro_tensor::Error::Extension`] classified as
856 /// `tenferro_tensor::ErrorKind::NumericalFailure` and retaining the typed
857 /// backend source; floating-point and complex zero divisors follow their
858 /// numeric semantics instead.
859 pub fn div(&self, other: &TracedTensor) -> Result<TracedTensor> {
860 let (lhs, rhs) = broadcast_binary(self, other)?;
861 apply_binary(
862 StdTensorOp::Div,
863 &lhs,
864 &rhs,
865 lhs.rank,
866 lhs.shape_hint.clone(),
867 )
868 }
869
870 /// Elementwise remainder with NumPy-style broadcasting.
871 ///
872 /// Prefer using the `%` operator when it reads naturally.
873 ///
874 /// # Errors
875 ///
876 /// Returns [`Error::Validation`] with `ShapeMismatch` when operand shapes
877 /// cannot be broadcast, [`Error::Unsupported`] at
878 /// [`ErrorPhase::GraphBuild`] when either operand has a complex dtype, or
879 /// [`Error::RuntimeStateSource`] when graph metadata registration fails.
880 ///
881 /// # Deferred errors
882 ///
883 /// If symbolic ranks prevent shape comparison during graph construction,
884 /// the same `ShapeMismatch` can be reported during compilation or
885 /// execution, with the corresponding [`ErrorPhase`]. For integer inputs,
886 /// a zero divisor is reported during execution as
887 /// [`Error::TensorRuntime`] containing a
888 /// [`tenferro_tensor::Error::Extension`] classified as
889 /// `tenferro_tensor::ErrorKind::NumericalFailure` and retaining the typed
890 /// backend source; floating-point zero divisors follow their numeric
891 /// semantics.
892 pub fn rem(&self, other: &TracedTensor) -> Result<TracedTensor> {
893 let (lhs, rhs) = broadcast_binary(self, other)?;
894 apply_binary(
895 StdTensorOp::Rem,
896 &lhs,
897 &rhs,
898 lhs.rank,
899 lhs.shape_hint.clone(),
900 )
901 }
902
903 /// Elementwise comparison with NumPy-style broadcasting.
904 ///
905 /// # Errors
906 ///
907 /// Returns [`Error::Validation`] with `ShapeMismatch` when the concrete
908 /// operands cannot be broadcast, [`Error::Unsupported`] when ordered
909 /// comparison rejects a complex dtype, or [`Error::RuntimeStateSource`]
910 /// when result metadata cannot be registered.
911 ///
912 /// # Deferred errors
913 ///
914 /// With same-rank symbolic operands, shape compatibility is retained as a
915 /// graph constraint. A concrete mismatch is reported later as
916 /// [`Error::TensorRuntime`] containing a typed validation source, with the
917 /// failure phase identifying compilation or execution.
918 pub fn compare(&self, other: &TracedTensor, dir: CompareDir) -> Result<TracedTensor> {
919 apply_broadcast_binary_op(StdTensorOp::Compare(dir), self, other)
920 }
921
922 /// Elementwise maximum with NumPy-style broadcasting.
923 ///
924 /// # Errors
925 ///
926 /// Returns [`Error::Validation`] with `ShapeMismatch` when the concrete
927 /// operands cannot be broadcast, [`Error::Unsupported`] when ordered
928 /// maximum rejects a complex dtype, or [`Error::RuntimeStateSource`] when
929 /// result metadata cannot be registered.
930 ///
931 /// # Deferred errors
932 ///
933 /// With same-rank symbolic operands, the broadcast constraint may fail at
934 /// compile or execution and is returned as [`Error::TensorRuntime`] with
935 /// its typed validation source.
936 pub fn maximum(&self, other: &TracedTensor) -> Result<TracedTensor> {
937 apply_broadcast_binary_op(StdTensorOp::Maximum, self, other)
938 }
939
940 /// Elementwise minimum with NumPy-style broadcasting.
941 ///
942 /// # Errors
943 ///
944 /// Returns [`Error::Validation`] with `ShapeMismatch` when the concrete
945 /// operands cannot be broadcast, [`Error::Unsupported`] when ordered
946 /// minimum rejects a complex dtype, or [`Error::RuntimeStateSource`] when
947 /// result metadata cannot be registered.
948 ///
949 /// # Deferred errors
950 ///
951 /// With same-rank symbolic operands, the broadcast constraint may fail at
952 /// compile or execution and is returned as [`Error::TensorRuntime`] with
953 /// its typed validation source.
954 pub fn minimum(&self, other: &TracedTensor) -> Result<TracedTensor> {
955 apply_broadcast_binary_op(StdTensorOp::Minimum, self, other)
956 }
957
958 /// Select values from `on_true` or `on_false` using `condition`.
959 ///
960 /// # Errors
961 ///
962 /// Returns [`Error::Validation`] with `InvalidArgument` when an operand
963 /// lacks concrete shape metadata, or `ShapeMismatch` when the concrete
964 /// condition and branches cannot share a broadcast shape. Dtype promotion
965 /// failures are returned as [`Error::TensorRuntime`] with the typed
966 /// `UnsupportedDTypeConversion` source; metadata failures retain
967 /// [`Error::RuntimeStateSource`].
968 pub fn where_select(
969 condition: &TracedTensor,
970 on_true: &TracedTensor,
971 on_false: &TracedTensor,
972 ) -> Result<TracedTensor> {
973 apply_broadcast_ternary_op(StdTensorOp::Select, condition, on_true, on_false)
974 }
975
976 /// Alias for [`Self::where_select`].
977 ///
978 /// # Errors
979 ///
980 /// Returns the same concrete failures as [`Self::where_select`]:
981 /// [`Error::Validation`] with `InvalidArgument`/`ShapeMismatch` for shape
982 /// metadata or broadcasting, [`Error::TensorRuntime`] with
983 /// `UnsupportedDTypeConversion` for failed promotion, and
984 /// [`Error::RuntimeStateSource`] for metadata registration.
985 pub fn select(
986 condition: &TracedTensor,
987 on_true: &TracedTensor,
988 on_false: &TracedTensor,
989 ) -> Result<TracedTensor> {
990 Self::where_select(condition, on_true, on_false)
991 }
992
993 /// Clamp values elementwise between lower and upper bounds.
994 ///
995 /// # Errors
996 ///
997 /// Returns [`Error::Validation`] with `InvalidArgument` when an operand
998 /// lacks concrete shape metadata, `ShapeMismatch` when bounds cannot be
999 /// broadcast with the input, [`Error::Unsupported`] for an ordered
1000 /// complex dtype, or [`Error::RuntimeStateSource`] when metadata cannot be
1001 /// registered.
1002 pub fn clamp(&self, lower: &TracedTensor, upper: &TracedTensor) -> Result<TracedTensor> {
1003 apply_broadcast_ternary_op(StdTensorOp::Clamp, self, lower, upper)
1004 }
1005
1006 fn apply_same_shape_unary(&self, op: StdTensorOp) -> Result<TracedTensor> {
1007 apply_unary(op, self, self.rank, self.shape_hint.clone())
1008 }
1009
1010 /// Elementwise negation.
1011 ///
1012 /// Prefer using the unary `-` operator when it reads naturally.
1013 ///
1014 /// # Examples
1015 ///
1016 /// ```rust
1017 /// # use tenferro_runtime::{TracedTensor};
1018 /// use tenferro_runtime::TraceContext;
1019 /// let trace = TraceContext::new();
1020 /// # let x = trace.from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1021 /// let y = x.neg().unwrap();
1022 /// let y2 = (-&x).unwrap();
1023 /// ```
1024 ///
1025 /// # Errors
1026 ///
1027 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1028 /// is unavailable or poisoned while recording the unary result.
1029 pub fn neg(&self) -> Result<TracedTensor> {
1030 self.apply_same_shape_unary(StdTensorOp::Neg)
1031 }
1032
1033 /// Elementwise complex conjugate.
1034 ///
1035 /// # Examples
1036 ///
1037 /// ```rust
1038 /// # use num_complex::Complex64;
1039 /// # use tenferro_runtime::{TracedTensor};
1040 /// use tenferro_runtime::TraceContext;
1041 /// let trace = TraceContext::new();
1042 /// # let x = trace.from_vec_col_major(
1043 /// # vec![2],
1044 /// # vec![Complex64::new(1.0, 2.0), Complex64::new(3.0, 4.0)],
1045 /// # )
1046 /// # .unwrap();
1047 /// let y = x.conj().unwrap();
1048 /// ```
1049 ///
1050 /// # Errors
1051 ///
1052 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1053 /// is unavailable or poisoned while recording the unary result.
1054 pub fn conj(&self) -> Result<TracedTensor> {
1055 self.apply_same_shape_unary(StdTensorOp::Conj)
1056 }
1057
1058 /// Elementwise absolute value.
1059 ///
1060 /// Complex inputs return real magnitudes (`C32 -> F32`, `C64 -> F64`).
1061 ///
1062 /// # Examples
1063 ///
1064 /// ```rust
1065 /// # use tenferro_runtime::{TracedTensor};
1066 /// use tenferro_runtime::TraceContext;
1067 /// let trace = TraceContext::new();
1068 /// # let x = trace.from_vec_col_major(vec![2], vec![-1.0_f64, 2.0]).unwrap();
1069 /// let y = x.abs().unwrap();
1070 /// ```
1071 ///
1072 /// # Errors
1073 ///
1074 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1075 /// is unavailable or poisoned while recording the unary result.
1076 pub fn abs(&self) -> Result<TracedTensor> {
1077 self.apply_same_shape_unary(StdTensorOp::Abs)
1078 }
1079
1080 /// Elementwise sign.
1081 ///
1082 /// # Examples
1083 ///
1084 /// ```rust
1085 /// # use tenferro_runtime::{TracedTensor};
1086 /// use tenferro_runtime::TraceContext;
1087 /// let trace = TraceContext::new();
1088 /// # let x = trace.from_vec_col_major(vec![2], vec![-1.0_f64, 2.0]).unwrap();
1089 /// let y = x.sign().unwrap();
1090 /// ```
1091 ///
1092 /// # Errors
1093 ///
1094 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1095 /// is unavailable or poisoned while recording the unary result.
1096 pub fn sign(&self) -> Result<TracedTensor> {
1097 self.apply_same_shape_unary(StdTensorOp::Sign)
1098 }
1099
1100 /// Scale by a real scalar: `y = factor * x`.
1101 ///
1102 /// # Examples
1103 ///
1104 /// ```rust
1105 /// # use tenferro_runtime::{TracedTensor};
1106 /// use tenferro_runtime::TraceContext;
1107 /// let trace = TraceContext::new();
1108 /// # let x = trace.from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1109 /// let y = x.scale_real(2.0)?;
1110 /// # Ok::<(), tenferro_runtime::Error>(())
1111 /// ```
1112 ///
1113 /// # Errors
1114 ///
1115 /// Returns [`Error::Validation`] with `InvalidArgument` when an integer or
1116 /// boolean factor is non-finite or out of range for the input dtype, or
1117 /// [`Error::RuntimeStateSource`] when output metadata registration fails.
1118 pub fn scale_real(&self, factor: f64) -> Result<TracedTensor> {
1119 let op = match self.dtype {
1120 DType::F64 => StdTensorOp::constant(factor),
1121 DType::F32 => StdTensorOp::constant(factor as f32),
1122 DType::I32 => StdTensorOp::constant(round_real_to_i32_for_op("scale_real", factor)?),
1123 DType::I64 => StdTensorOp::constant(round_real_to_i64(factor)?),
1124 DType::Bool => StdTensorOp::constant(bool_from_real_for_op("scale_real", factor)?),
1125 DType::C64 => StdTensorOp::constant(Complex64::new(factor, 0.0)),
1126 DType::C32 => StdTensorOp::constant(Complex32::new(factor as f32, 0.0)),
1127 // An externally defined scalar has no traced constant, so the closure
1128 // rejects it instead of guessing one.
1129 DType::External(_) => {
1130 return Err(graph_invalid_argument(
1131 "scale_real",
1132 "dtype",
1133 format!("requires a preset tensor dtype, got {:?}", self.dtype),
1134 ));
1135 }
1136 };
1137 scale_with_constant(self, op)
1138 }
1139
1140 /// Scale by a complex scalar: `y = factor * x`.
1141 ///
1142 /// Only complex tensors support complex scaling. For a real scalar factor
1143 /// that should preserve the input dtype, prefer [`scale_real`](Self::scale_real).
1144 ///
1145 /// # Examples
1146 ///
1147 /// ```rust
1148 /// use num_complex::Complex64;
1149 /// # use tenferro_runtime::{TracedTensor};
1150 /// use tenferro_runtime::TraceContext;
1151 /// let trace = TraceContext::new();
1152 /// # let x = trace.from_vec_col_major(
1153 /// # vec![2],
1154 /// # vec![Complex64::new(1.0, 0.0), Complex64::new(2.0, 0.0)],
1155 /// # )
1156 /// # .unwrap();
1157 /// let y = x.scale_complex(Complex64::new(0.0, 1.0)).unwrap(); // multiply by i
1158 /// ```
1159 ///
1160 /// # Errors
1161 ///
1162 /// Returns [`Error::Validation`] with `InvalidArgument` when a complex
1163 /// factor is applied to a non-complex dtype, or
1164 /// [`Error::RuntimeStateSource`] when output metadata registration fails.
1165 pub fn scale_complex(&self, factor: Complex64) -> Result<TracedTensor> {
1166 match self.dtype {
1167 DType::C64 => scale_with_constant(self, StdTensorOp::constant(factor)),
1168 DType::C32 => scale_with_constant(
1169 self,
1170 StdTensorOp::constant(Complex32::new(factor.re as f32, factor.im as f32)),
1171 ),
1172 DType::F32
1173 | DType::F64
1174 | DType::I32
1175 | DType::I64
1176 | DType::Bool
1177 | DType::External(_) => Err(graph_invalid_argument(
1178 "scale_complex",
1179 "dtype",
1180 format!("requires complex tensor dtype, got {:?}", self.dtype),
1181 )),
1182 }
1183 }
1184
1185 /// Elementwise exponential.
1186 ///
1187 /// # Examples
1188 ///
1189 /// ```rust
1190 /// # use tenferro_runtime::{TracedTensor};
1191 /// use tenferro_runtime::TraceContext;
1192 /// let trace = TraceContext::new();
1193 /// # let x = trace.from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1194 /// let y = x.exp().unwrap();
1195 /// ```
1196 ///
1197 /// # Errors
1198 ///
1199 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1200 /// is unavailable or poisoned while recording the unary result.
1201 pub fn exp(&self) -> Result<TracedTensor> {
1202 self.apply_same_shape_unary(StdTensorOp::Exp)
1203 }
1204
1205 /// Elementwise natural logarithm.
1206 ///
1207 /// # Examples
1208 ///
1209 /// ```rust
1210 /// # use tenferro_runtime::{TracedTensor};
1211 /// use tenferro_runtime::TraceContext;
1212 /// let trace = TraceContext::new();
1213 /// # let x = trace.from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1214 /// let y = x.log().unwrap();
1215 /// ```
1216 ///
1217 /// # Errors
1218 ///
1219 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1220 /// is unavailable or poisoned while recording the unary result.
1221 pub fn log(&self) -> Result<TracedTensor> {
1222 self.apply_same_shape_unary(StdTensorOp::Log)
1223 }
1224
1225 /// Elementwise sine.
1226 ///
1227 /// # Examples
1228 ///
1229 /// ```rust
1230 /// # use tenferro_runtime::{TracedTensor};
1231 /// use tenferro_runtime::TraceContext;
1232 /// let trace = TraceContext::new();
1233 /// # let x = trace.from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1234 /// let y = x.sin().unwrap();
1235 /// ```
1236 ///
1237 /// # Errors
1238 ///
1239 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1240 /// is unavailable or poisoned while recording the unary result.
1241 pub fn sin(&self) -> Result<TracedTensor> {
1242 self.apply_same_shape_unary(StdTensorOp::Sin)
1243 }
1244
1245 /// Elementwise cosine.
1246 ///
1247 /// # Examples
1248 ///
1249 /// ```rust
1250 /// # use tenferro_runtime::{TracedTensor};
1251 /// use tenferro_runtime::TraceContext;
1252 /// let trace = TraceContext::new();
1253 /// # let x = trace.from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1254 /// let y = x.cos().unwrap();
1255 /// ```
1256 ///
1257 /// # Errors
1258 ///
1259 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1260 /// is unavailable or poisoned while recording the unary result.
1261 pub fn cos(&self) -> Result<TracedTensor> {
1262 self.apply_same_shape_unary(StdTensorOp::Cos)
1263 }
1264
1265 /// Elementwise hyperbolic tangent.
1266 ///
1267 /// # Examples
1268 ///
1269 /// ```rust
1270 /// # use tenferro_runtime::{TracedTensor};
1271 /// use tenferro_runtime::TraceContext;
1272 /// let trace = TraceContext::new();
1273 /// # let x = trace.from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1274 /// let y = x.tanh().unwrap();
1275 /// ```
1276 ///
1277 /// # Errors
1278 ///
1279 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1280 /// is unavailable or poisoned while recording the unary result.
1281 pub fn tanh(&self) -> Result<TracedTensor> {
1282 self.apply_same_shape_unary(StdTensorOp::Tanh)
1283 }
1284
1285 /// Elementwise square root.
1286 ///
1287 /// # Examples
1288 ///
1289 /// ```rust
1290 /// # use tenferro_runtime::{TracedTensor};
1291 /// use tenferro_runtime::TraceContext;
1292 /// let trace = TraceContext::new();
1293 /// # let x = trace.from_vec_col_major(vec![2], vec![1.0_f64, 4.0]).unwrap();
1294 /// let y = x.sqrt().unwrap();
1295 /// ```
1296 ///
1297 /// # Errors
1298 ///
1299 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1300 /// is unavailable or poisoned while recording the unary result.
1301 pub fn sqrt(&self) -> Result<TracedTensor> {
1302 self.apply_same_shape_unary(StdTensorOp::Sqrt)
1303 }
1304
1305 /// Elementwise reciprocal square root.
1306 ///
1307 /// # Examples
1308 ///
1309 /// ```rust
1310 /// # use tenferro_runtime::{TracedTensor};
1311 /// use tenferro_runtime::TraceContext;
1312 /// let trace = TraceContext::new();
1313 /// # let x = trace.from_vec_col_major(vec![2], vec![1.0_f64, 4.0]).unwrap();
1314 /// let y = x.rsqrt().unwrap();
1315 /// ```
1316 ///
1317 /// # Errors
1318 ///
1319 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1320 /// is unavailable or poisoned while recording the unary result.
1321 pub fn rsqrt(&self) -> Result<TracedTensor> {
1322 self.apply_same_shape_unary(StdTensorOp::Rsqrt)
1323 }
1324
1325 /// Elementwise power with NumPy-style broadcasting.
1326 ///
1327 /// # Examples
1328 ///
1329 /// ```rust
1330 /// # use tenferro_runtime::{TracedTensor};
1331 /// use tenferro_runtime::TraceContext;
1332 /// let trace = TraceContext::new();
1333 /// # let base = trace.from_vec_col_major(vec![2], vec![2.0_f64, 3.0]).unwrap();
1334 /// # let exp = trace.from_vec_col_major(vec![2], vec![3.0_f64, 2.0]).unwrap();
1335 /// let y = base.pow(&exp);
1336 /// ```
1337 ///
1338 /// # Errors
1339 ///
1340 /// Returns [`Error::Validation`] with `ShapeMismatch` when the concrete
1341 /// operands cannot be broadcast, or [`Error::RuntimeStateSource`] when
1342 /// result metadata cannot be registered.
1343 ///
1344 /// # Deferred errors
1345 ///
1346 /// A symbolic broadcast mismatch or integer negative exponent is
1347 /// discovered at compile or execution and is returned as
1348 /// [`Error::TensorRuntime`] with a typed `ShapeMismatch` or
1349 /// `NegativeIntegerExponent` numerical source and the corresponding
1350 /// [`ErrorPhase`].
1351 pub fn pow(&self, other: &TracedTensor) -> Result<TracedTensor> {
1352 let (lhs, rhs) = broadcast_binary(self, other)?;
1353 apply_binary(
1354 StdTensorOp::Pow,
1355 &lhs,
1356 &rhs,
1357 lhs.rank,
1358 lhs.shape_hint.clone(),
1359 )
1360 }
1361
1362 /// Elementwise `exp(x) - 1`.
1363 ///
1364 /// # Examples
1365 ///
1366 /// ```rust
1367 /// # use tenferro_runtime::{TracedTensor};
1368 /// use tenferro_runtime::TraceContext;
1369 /// let trace = TraceContext::new();
1370 /// # let x = trace.from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1371 /// let y = x.expm1().unwrap();
1372 /// ```
1373 ///
1374 /// # Errors
1375 ///
1376 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1377 /// is unavailable or poisoned while recording the unary result.
1378 pub fn expm1(&self) -> Result<TracedTensor> {
1379 self.apply_same_shape_unary(StdTensorOp::Expm1)
1380 }
1381
1382 /// Elementwise `log(1 + x)`.
1383 ///
1384 /// # Examples
1385 ///
1386 /// ```rust
1387 /// # use tenferro_runtime::{TracedTensor};
1388 /// use tenferro_runtime::TraceContext;
1389 /// let trace = TraceContext::new();
1390 /// # let x = trace.from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1391 /// let y = x.log1p().unwrap();
1392 /// ```
1393 ///
1394 /// # Errors
1395 ///
1396 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1397 /// is unavailable or poisoned while recording the unary result.
1398 pub fn log1p(&self) -> Result<TracedTensor> {
1399 self.apply_same_shape_unary(StdTensorOp::Log1p)
1400 }
1401
1402 /// Elementwise error function `erf(x)`, for real `F32`/`F64` tensors.
1403 ///
1404 /// `erf(+-0) = +-0`, `erf(+-inf) = +-1`, and `NaN` stays `NaN`. The
1405 /// derivative is `2/sqrt(pi) * exp(-x^2)`.
1406 ///
1407 /// # Examples
1408 ///
1409 /// ```rust
1410 /// # use tenferro_runtime::{TracedTensor};
1411 /// use tenferro_runtime::TraceContext;
1412 /// let trace = TraceContext::new();
1413 /// let x = trace.from_vec_col_major(vec![2], vec![0.0_f64, 1.0])?;
1414 /// let y = x.erf()?;
1415 /// assert_eq!(y.dtype(), tenferro_runtime::DType::F64);
1416 /// # Ok::<(), tenferro_runtime::Error>(())
1417 /// ```
1418 ///
1419 /// # Errors
1420 ///
1421 /// Returns [`Error::Unsupported`] (phase `GraphBuild`) for complex,
1422 /// integer, or `Bool` input, or [`Error::RuntimeStateSource`] when the
1423 /// graph metadata registry is unavailable or poisoned while recording the
1424 /// unary result.
1425 pub fn erf(&self) -> Result<TracedTensor> {
1426 if !matches!(self.dtype, DType::F32 | DType::F64) {
1427 return Err(Error::unsupported(
1428 "TracedTensor::erf",
1429 ErrorPhase::GraphBuild,
1430 format!(
1431 "erf is defined for real F32/F64 tensors, got {:?}",
1432 self.dtype
1433 ),
1434 ));
1435 }
1436 self.apply_same_shape_unary(StdTensorOp::Erf)
1437 }
1438
1439 /// Convert the tensor to a different dtype using checked conversion.
1440 ///
1441 /// Use [`cast`](Self::cast) when a lossy dtype projection is intended.
1442 ///
1443 /// # Examples
1444 ///
1445 /// ```rust
1446 /// use tenferro_runtime::DType;
1447 /// use tenferro_runtime::TraceContext;
1448 /// # use tenferro_runtime::{TracedTensor};
1449 /// let trace = TraceContext::new();
1450 /// # let x = trace.from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1451 ///
1452 /// let y = x.convert(DType::C64)?;
1453 /// # Ok::<(), tenferro_runtime::Error>(())
1454 /// ```
1455 ///
1456 /// # Errors
1457 ///
1458 /// Returns [`tenferro_tensor::Error::UnsupportedDTypeConversion`] when the
1459 /// requested pair is outside tenferro's checked dtype-promotion lattice,
1460 /// or [`Error::Validation`] when graph metadata rejects the conversion.
1461 /// Use [`cast`](Self::cast) for explicit lossy dtype projection.
1462 pub fn convert(&self, to: DType) -> Result<TracedTensor> {
1463 tenferro_tensor::validate::validate_convert_dtype("TracedTensor::convert", self.dtype, to)?;
1464 self.cast(to)
1465 }
1466
1467 /// Cast the tensor to a different dtype using explicit dtype projection.
1468 ///
1469 /// `cast` may truncate, narrow precision, project complex values to their
1470 /// real component, or use boolean truthiness where the backend supports the
1471 /// requested projection.
1472 ///
1473 /// # Examples
1474 ///
1475 /// ```rust
1476 /// use tenferro_runtime::DType;
1477 /// use tenferro_runtime::TraceContext;
1478 /// # use tenferro_runtime::{TracedTensor};
1479 /// let trace = TraceContext::new();
1480 /// # let x = trace.from_vec_col_major(vec![2], vec![1.2_f64, -2.8]).unwrap();
1481 ///
1482 /// let y = x.cast(DType::I32).unwrap();
1483 /// ```
1484 ///
1485 /// # Errors
1486 ///
1487 /// Returns [`Error::TensorRuntime`] containing
1488 /// `UnsupportedDTypeConversion` when the requested input-to-target
1489 /// projection is not supported, or [`Error::RuntimeStateSource`] when
1490 /// converted-output metadata cannot be registered.
1491 pub fn cast(&self, to: DType) -> Result<TracedTensor> {
1492 if self.dtype == to {
1493 return Ok(self.clone());
1494 }
1495
1496 apply_unary_with_dtype(
1497 StdTensorOp::Convert {
1498 from: self.dtype,
1499 to,
1500 },
1501 self,
1502 self.rank,
1503 self.shape_hint.clone(),
1504 to,
1505 )
1506 }
1507
1508 /// Generalized tensor contraction.
1509 ///
1510 /// The output layout is `[lhs free..., rhs free..., batch...]`: batch axes
1511 /// come last (see [`DotGeneralConfig`]).
1512 ///
1513 /// # Examples
1514 ///
1515 /// ```rust
1516 /// # use tenferro_runtime::{DotGeneralConfig, TracedTensor};
1517 /// use tenferro_runtime::TraceContext;
1518 /// let trace = TraceContext::new();
1519 /// # let a = trace.from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap();
1520 /// # let b = trace.from_vec_col_major(vec![3, 4], vec![1.0_f64; 12]).unwrap();
1521 /// # let config = DotGeneralConfig {
1522 /// # lhs_contracting_dims: [1].as_slice().into(),
1523 /// # rhs_contracting_dims: [0].as_slice().into(),
1524 /// # lhs_batch_dims: [].as_slice().into(),
1525 /// # rhs_batch_dims: [].as_slice().into(),
1526 /// # };
1527 /// let y = a.dot_general(&b, config)?;
1528 /// # Ok::<(), tenferro_runtime::Error>(())
1529 /// ```
1530 ///
1531 /// # Errors
1532 ///
1533 /// Returns [`Error::Validation`] with `RankMismatch`, `AxisOutOfBounds`,
1534 /// `DuplicateAxis`, or `AxisRoleConflict` when dimension numbers are
1535 /// invalid for the operand ranks, and [`Error::RuntimeStateSource`] when
1536 /// output metadata cannot be registered.
1537 ///
1538 /// # Deferred errors
1539 ///
1540 /// Contracting or batch dimensions whose sizes are symbolic are checked
1541 /// when concrete inputs reach compilation or execution. A mismatch is
1542 /// returned as [`Error::TensorRuntime`] with a typed `ShapeMismatch`
1543 /// source and its corresponding [`ErrorPhase`].
1544 pub fn dot_general(
1545 &self,
1546 other: &TracedTensor,
1547 config: DotGeneralConfig,
1548 ) -> Result<TracedTensor> {
1549 config
1550 .validate_dims_with_ranks(self.rank, other.rank)
1551 .map_err(|err| graph_tensor_error("dot_general", err))?;
1552 let lhs_free: Vec<usize> = (0..self.rank)
1553 .filter(|d| {
1554 !config.lhs_contracting_dims.contains(d) && !config.lhs_batch_dims.contains(d)
1555 })
1556 .collect();
1557 let rhs_free: Vec<usize> = (0..other.rank)
1558 .filter(|d| {
1559 !config.rhs_contracting_dims.contains(d) && !config.rhs_batch_dims.contains(d)
1560 })
1561 .collect();
1562 let out_rank = config.lhs_batch_dims.len() + lhs_free.len() + rhs_free.len();
1563 let out_shape_hint = match (&self.shape_hint, &other.shape_hint) {
1564 (Some(lhs_shape), Some(rhs_shape)) => {
1565 let mut out_shape = Vec::with_capacity(out_rank);
1566 for &d in &lhs_free {
1567 out_shape.push(lhs_shape[d].clone());
1568 }
1569 for &d in &rhs_free {
1570 out_shape.push(rhs_shape[d].clone());
1571 }
1572 for &d in &config.lhs_batch_dims {
1573 out_shape.push(lhs_shape[d].clone());
1574 }
1575 Some(out_shape)
1576 }
1577 _ => None,
1578 };
1579
1580 apply_binary(
1581 StdTensorOp::DotGeneral { config },
1582 self,
1583 other,
1584 out_rank,
1585 out_shape_hint,
1586 )
1587 }
1588
1589 /// Matrix multiplication for rank-2 tensors.
1590 ///
1591 /// # Errors
1592 ///
1593 /// Returns [`Error::Validation`] with `RankMismatch` when either operand is
1594 /// not rank 2, `ShapeMismatch::ContractedDimensions` when known matrix
1595 /// dimensions differ, or [`Error::RuntimeStateSource`] when output
1596 /// metadata cannot be registered.
1597 ///
1598 /// # Deferred errors
1599 ///
1600 /// If either contracted dimension is symbolic, the mismatch is discovered
1601 /// at compilation or execution and returned as [`Error::TensorRuntime`]
1602 /// with its typed `ShapeMismatch` source.
1603 pub fn matmul(&self, other: &TracedTensor) -> Result<TracedTensor> {
1604 if self.rank != 2 {
1605 return Err(graph_validation(
1606 "TracedTensor::matmul",
1607 ValidationError::RankMismatch {
1608 expected: 2,
1609 actual: self.rank,
1610 },
1611 ));
1612 }
1613 if other.rank != 2 {
1614 return Err(graph_validation(
1615 "TracedTensor::matmul",
1616 ValidationError::RankMismatch {
1617 expected: 2,
1618 actual: other.rank,
1619 },
1620 ));
1621 }
1622 if let (Some(lhs_shape), Some(rhs_shape)) = (&self.shape_hint, &other.shape_hint)
1623 && let (Some(lhs_cols), Some(rhs_rows)) =
1624 (lhs_shape[1].constant_value(), rhs_shape[0].constant_value())
1625 && lhs_cols != rhs_rows
1626 {
1627 return Err(graph_validation(
1628 "TracedTensor::matmul",
1629 ShapeMismatch::ContractedDimensions {
1630 lhs_axis: 1,
1631 lhs_size: lhs_cols,
1632 rhs_axis: 0,
1633 rhs_size: rhs_rows,
1634 },
1635 ));
1636 }
1637 self.dot_general(
1638 other,
1639 DotGeneralConfig {
1640 lhs_contracting_dims: [1].as_slice().into(),
1641 rhs_contracting_dims: [0].as_slice().into(),
1642 lhs_batch_dims: [].as_slice().into(),
1643 rhs_batch_dims: [].as_slice().into(),
1644 },
1645 )
1646 }
1647
1648 /// Sum over the given axes.
1649 ///
1650 /// # Examples
1651 ///
1652 /// ```rust
1653 /// # use tenferro_runtime::{TracedTensor};
1654 /// use tenferro_runtime::TraceContext;
1655 /// let trace = TraceContext::new();
1656 /// # let x = trace.from_vec_col_major(vec![2, 2], vec![1.0_f64; 4]).unwrap();
1657 /// let total = x.reduce_sum(None)?;
1658 /// let rows = x.reduce_sum(Some(&[1]))?;
1659 /// let identity = x.reduce_sum(Some(&[]))?;
1660 /// assert_eq!(total.rank(), 0);
1661 /// assert_eq!(rows.rank(), 1);
1662 /// assert_eq!(identity.rank(), 2);
1663 /// # Ok::<(), tenferro_runtime::Error>(())
1664 /// ```
1665 ///
1666 /// # Errors
1667 ///
1668 /// Returns [`Error::Validation`] with `AxisOutOfBounds` when an axis is
1669 /// outside the input rank or `DuplicateAxis` when `axes` repeats an axis,
1670 /// or [`Error::RuntimeStateSource`] when output metadata cannot be
1671 /// registered.
1672 pub fn reduce_sum(&self, axes: Option<&[usize]>) -> Result<TracedTensor> {
1673 let axes = axes.map_or_else(|| (0..self.rank).collect(), <[usize]>::to_vec);
1674 let (out_rank, out_shape_hint) =
1675 reduction_output_meta(self, &axes, "TracedTensor::reduce_sum")?;
1676 apply_unary(
1677 StdTensorOp::ReduceSum { axes },
1678 self,
1679 out_rank,
1680 out_shape_hint,
1681 )
1682 }
1683
1684 /// Sum elementwise squares over the requested axes.
1685 ///
1686 /// Each value is squared in its input dtype before reduction. The initial
1687 /// supported dtypes are `f32` and `f64`; other dtypes return a typed
1688 /// unsupported error during execution. `None` reduces every axis, like
1689 /// the rest of the reduction family; `Some(&[])` returns the elementwise
1690 /// square without reducing rank.
1691 ///
1692 /// This operation is useful when the squared sum is needed directly. Use
1693 /// the linalg norm APIs when a square root or complex magnitude semantics
1694 /// are required.
1695 ///
1696 /// # Errors
1697 ///
1698 /// Returns a typed validation error for invalid axes or a typed
1699 /// runtime-state error while registering output metadata.
1700 ///
1701 /// # Deferred errors
1702 ///
1703 /// Unsupported dtypes and backend execution failures are reported when the
1704 /// compiled graph is executed.
1705 /// # Examples
1706 ///
1707 /// ```rust
1708 /// # use tenferro_runtime::{TracedTensor};
1709 /// use tenferro_runtime::TraceContext;
1710 /// let trace = TraceContext::new();
1711 /// # let x = trace.from_vec_col_major(
1712 /// # vec![2, 2],
1713 /// # vec![1.0_f64, 2.0, 3.0, 4.0],
1714 /// # )?;
1715 /// let squares = x.reduce_sum_squares(Some(&[1]))?;
1716 /// assert_eq!(squares.rank(), 1);
1717 /// let total = x.reduce_sum_squares(None)?;
1718 /// assert_eq!(total.rank(), 0);
1719 /// # Ok::<(), tenferro_runtime::Error>(())
1720 /// ```
1721 pub fn reduce_sum_squares(&self, axes: Option<&[usize]>) -> Result<TracedTensor> {
1722 let axes = axes.map_or_else(|| (0..self.rank).collect(), <[usize]>::to_vec);
1723 let (out_rank, out_shape_hint) =
1724 reduction_output_meta(self, &axes, "TracedTensor::reduce_sum_squares")?;
1725 apply_unary(
1726 StdTensorOp::ReduceSumSquares { axes },
1727 self,
1728 out_rank,
1729 out_shape_hint,
1730 )
1731 }
1732
1733 /// Reduce by taking the maximum along the given axes.
1734 ///
1735 /// Used by tropical (max-plus) compositions: a max-plus reduction over
1736 /// an axis is `ReduceMax` on that axis.
1737 ///
1738 /// # Examples
1739 ///
1740 /// ```rust
1741 /// # use tenferro_runtime::{TracedTensor};
1742 /// use tenferro_runtime::TraceContext;
1743 /// let trace = TraceContext::new();
1744 /// # let x = trace.from_vec_col_major(vec![2, 2], vec![1.0_f64; 4]).unwrap();
1745 /// let y = x.reduce_max(Some(&[0]))?;
1746 /// # Ok::<(), tenferro_runtime::Error>(())
1747 /// ```
1748 ///
1749 /// # Errors
1750 ///
1751 /// Returns [`Error::Validation`] with `AxisOutOfBounds` when an axis is
1752 /// outside the input rank or `DuplicateAxis` when `axes` repeats an axis,
1753 /// [`Error::Unsupported`] when a non-empty maximum reduction receives a
1754 /// complex dtype, or [`Error::RuntimeStateSource`] when output metadata
1755 /// cannot be registered.
1756 pub fn reduce_max(&self, axes: Option<&[usize]>) -> Result<TracedTensor> {
1757 let axes = axes.map_or_else(|| (0..self.rank).collect(), <[usize]>::to_vec);
1758 let (out_rank, out_shape_hint) =
1759 reduction_output_meta(self, &axes, "TracedTensor::reduce_max")?;
1760 try_apply_unary(
1761 StdTensorOp::ReduceMax { axes },
1762 self,
1763 out_rank,
1764 out_shape_hint,
1765 "TracedTensor::reduce_max",
1766 )
1767 }
1768
1769 /// Reduce by taking the minimum along the given axes.
1770 ///
1771 /// Used by tropical (min-plus) compositions: a min-plus reduction over
1772 /// an axis is `ReduceMin` on that axis.
1773 ///
1774 /// # Examples
1775 ///
1776 /// ```rust
1777 /// # use tenferro_runtime::{TracedTensor};
1778 /// use tenferro_runtime::TraceContext;
1779 /// let trace = TraceContext::new();
1780 /// # let x = trace.from_vec_col_major(vec![2, 2], vec![1.0_f64; 4]).unwrap();
1781 /// let y = x.reduce_min(Some(&[0]))?;
1782 /// # Ok::<(), tenferro_runtime::Error>(())
1783 /// ```
1784 ///
1785 /// # Errors
1786 ///
1787 /// Returns [`Error::Validation`] with `AxisOutOfBounds` when an axis is
1788 /// outside the input rank or `DuplicateAxis` when `axes` repeats an axis,
1789 /// [`Error::Unsupported`] when a non-empty minimum reduction receives a
1790 /// complex dtype, or [`Error::RuntimeStateSource`] when output metadata
1791 /// cannot be registered.
1792 pub fn reduce_min(&self, axes: Option<&[usize]>) -> Result<TracedTensor> {
1793 let axes = axes.map_or_else(|| (0..self.rank).collect(), <[usize]>::to_vec);
1794 let (out_rank, out_shape_hint) =
1795 reduction_output_meta(self, &axes, "TracedTensor::reduce_min")?;
1796 try_apply_unary(
1797 StdTensorOp::ReduceMin { axes },
1798 self,
1799 out_rank,
1800 out_shape_hint,
1801 "TracedTensor::reduce_min",
1802 )
1803 }
1804
1805 /// Reduce by taking the product along the given axes.
1806 ///
1807 /// # Examples
1808 ///
1809 /// ```rust
1810 /// # use tenferro_runtime::{TracedTensor};
1811 /// use tenferro_runtime::TraceContext;
1812 /// let trace = TraceContext::new();
1813 /// # let x = trace.from_vec_col_major(vec![2, 2], vec![1.0_f64; 4]).unwrap();
1814 /// let y = x.reduce_prod(Some(&[0]))?;
1815 /// # Ok::<(), tenferro_runtime::Error>(())
1816 /// ```
1817 ///
1818 /// # Errors
1819 ///
1820 /// Returns [`Error::Validation`] with `AxisOutOfBounds` when an axis is
1821 /// outside the input rank or `DuplicateAxis` when `axes` repeats an axis,
1822 /// or [`Error::RuntimeStateSource`] when output metadata cannot be
1823 /// registered.
1824 pub fn reduce_prod(&self, axes: Option<&[usize]>) -> Result<TracedTensor> {
1825 let axes = axes.map_or_else(|| (0..self.rank).collect(), <[usize]>::to_vec);
1826 let (out_rank, out_shape_hint) =
1827 reduction_output_meta(self, &axes, "TracedTensor::reduce_prod")?;
1828 apply_unary(
1829 StdTensorOp::ReduceProd { axes },
1830 self,
1831 out_rank,
1832 out_shape_hint,
1833 )
1834 }
1835
1836 /// Reshape without changing element order.
1837 ///
1838 /// # Examples
1839 ///
1840 /// ```rust
1841 /// # use tenferro_runtime::{TracedTensor};
1842 /// use tenferro_runtime::TraceContext;
1843 /// let trace = TraceContext::new();
1844 /// # let x = trace.from_vec_col_major(vec![4], vec![1.0_f64; 4]).unwrap();
1845 /// for y in [x.reshape([2, 2])?, x.reshape(vec![2, 2])?, x.reshape(&[2, 2][..])?] {
1846 /// assert_eq!(y.try_concrete_shape(), Some(vec![2, 2]));
1847 /// }
1848 /// assert!(x.reshape([3]).is_err());
1849 /// # Ok::<(), tenferro_runtime::Error>(())
1850 /// ```
1851 ///
1852 /// # Errors
1853 ///
1854 /// Returns [`Error::Validation`] with `ShapeMismatch::ReshapeElementCount`
1855 /// when a concrete input has a different element count, or
1856 /// `IntegerOverflow` when the target shape product overflows `usize`.
1857 pub fn reshape(&self, shape: impl IntoShapeVec) -> Result<TracedTensor> {
1858 let shape = shape.into_shape_vec();
1859 validate_concrete_reshape_shape(self, &shape)?;
1860 apply_unary_with_dtype(
1861 StdTensorOp::Reshape {
1862 to_shape: DimExpr::from_concrete(&shape),
1863 },
1864 self,
1865 shape.len(),
1866 Some(shape.iter().copied().map(SymDim::from).collect()),
1867 self.dtype,
1868 )
1869 }
1870
1871 /// Return a symbolic expression for the size of one axis, suitable as
1872 /// an `InputDim`-style reference when composing with
1873 /// [`TracedTensor::reshape_sym`].
1874 ///
1875 /// Semantics: if this tensor's `shape_hint` has a symbolic
1876 /// (non-constant) entry for `axis`, that entry is returned
1877 /// verbatim. Otherwise — including when `shape_hint[axis]` is a
1878 /// concrete `SymDim::Concrete(n)` — a
1879 /// `SymDim::tensor_axis(self.symbolic_id(), axis)` reference is returned so the
1880 /// resulting graph remains shape-polymorphic if the same graph is
1881 /// later evaluated against a differently-shaped binding.
1882 ///
1883 /// For a canonical "what is the size of this axis?" query that
1884 /// reports the concrete size when it is known, prefer
1885 /// [`Self::axis_sym_dim`].
1886 ///
1887 /// # Examples
1888 ///
1889 /// ```rust
1890 /// # use tenferro_runtime::{TracedTensor};
1891 /// use tenferro_runtime::TraceContext;
1892 /// let trace = TraceContext::new();
1893 /// # let x = trace.from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap();
1894 /// let rows = x.sym_size(0)?;
1895 /// let cols = x.sym_size(1)?;
1896 /// let y = x.reshape_sym(&[rows * cols]).unwrap();
1897 /// # Ok::<(), tenferro_runtime::Error>(())
1898 /// ```
1899 ///
1900 /// # Errors
1901 ///
1902 /// Returns [`Error::Validation`] with `AxisOutOfBounds` when `axis` is
1903 /// outside this tensor's rank.
1904 pub fn sym_size(&self, axis: usize) -> Result<SymDim> {
1905 validate_traced_axis(self, axis, "TracedTensor::sym_size")?;
1906 Ok(self
1907 .shape_hint
1908 .as_ref()
1909 .and_then(|shape| shape.get(axis))
1910 .filter(|dim| dim.constant_value().is_none())
1911 .cloned()
1912 .unwrap_or_else(|| SymDim::tensor_axis(self.symbolic_id(), axis)))
1913 }
1914
1915 /// Return the canonical `SymDim` for `axis` — the concrete
1916 /// `SymDim::Concrete(n)` when the size is known, otherwise a symbolic
1917 /// expression identifying this tensor's axis.
1918 ///
1919 /// Unlike [`Self::sym_size`], this method does **not** rewrite
1920 /// concrete axes into `TensorAxis` references. It is the accessor
1921 /// external composition wrappers should use when building mixed
1922 /// concrete/symbolic target shapes for operations like
1923 /// [`Self::broadcast_in_dim_sym`].
1924 ///
1925 /// # Examples
1926 ///
1927 /// ```
1928 /// use tenferro_tensor::DType;
1929 /// use tenferro_runtime::{TracedTensor};
1930 ///
1931 /// use tenferro_runtime::TraceContext;
1932 /// let trace = TraceContext::new();
1933 /// let a = trace.from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap();
1934 /// // Concrete axis: reports the constant size.
1935 /// assert_eq!(a.axis_sym_dim(0).unwrap().constant_value(), Some(2));
1936 ///
1937 /// let b = trace.input_symbolic_shape(DType::F64, 2).unwrap();
1938 /// // Fully symbolic leaf: reports a TensorAxis reference.
1939 /// assert!(b.axis_sym_dim(0).unwrap().constant_value().is_none());
1940 /// ```
1941 ///
1942 /// # Errors
1943 ///
1944 /// Returns [`Error::Validation`] with `AxisOutOfBounds` when `axis` is
1945 /// outside this tensor's rank.
1946 pub fn axis_sym_dim(&self, axis: usize) -> Result<SymDim> {
1947 validate_traced_axis(self, axis, "TracedTensor::axis_sym_dim")?;
1948 match self.shape_hint.as_ref().and_then(|shape| shape.get(axis)) {
1949 Some(dim) => Ok(dim.clone()),
1950 None => Ok(SymDim::tensor_axis(self.symbolic_id(), axis)),
1951 }
1952 }
1953
1954 /// Return the full symbolic shape of this tensor when it is known.
1955 ///
1956 /// Every traced value carries its symbolic extents: a constant axis reports
1957 /// its constant size, and every other axis reports a [`SymDim::tensor_axis`]
1958 /// naming this handle's own axis, whatever the builder inferred, so a shape
1959 /// built from several handles can be converted without confusing them. `None`
1960 /// only appears for a value whose extents the builder left unknown; build
1961 /// those axis-by-axis via [`Self::axis_sym_dim`].
1962 ///
1963 /// # Examples
1964 ///
1965 /// ```
1966 /// use tenferro_tensor::DType;
1967 /// use tenferro_runtime::TraceContext;
1968 ///
1969 /// let trace = TraceContext::new();
1970 /// let a = trace.from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap();
1971 /// assert!(a.sym_shape().is_some());
1972 /// assert_eq!(a.sym_shape().unwrap().len(), 2);
1973 ///
1974 /// // A symbolic placeholder reports its symbolic extents, not `None`.
1975 /// let b = trace.input_symbolic_shape(DType::F64, 2).unwrap();
1976 /// assert_eq!(b.sym_shape().map(<[_]>::len), Some(2));
1977 /// assert!(b.sym_shape().unwrap().iter().all(|dim| dim.constant_value().is_none()));
1978 /// ```
1979 pub fn sym_shape(&self) -> Option<&[SymDim]> {
1980 self.shape_hint.as_deref()
1981 }
1982
1983 /// Reshape using symbolic dimensions derived from traced tensor axes.
1984 ///
1985 /// # Examples
1986 ///
1987 /// ```rust
1988 /// # use tenferro_runtime::{TracedTensor};
1989 /// use tenferro_runtime::TraceContext;
1990 /// let trace = TraceContext::new();
1991 /// # let x = trace.from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap();
1992 /// let rows = x.sym_size(0)?;
1993 /// let cols = x.sym_size(1)?;
1994 /// let y = x.reshape_sym(&[rows * cols]).unwrap();
1995 /// # Ok::<(), tenferro_runtime::Error>(())
1996 /// ```
1997 ///
1998 /// # Errors
1999 ///
2000 /// Returns [`Error::SymbolicShapeConversion`] when a supplied symbolic
2001 /// dimension cannot be mapped to this graph, or [`Error::RuntimeStateSource`]
2002 /// when result metadata cannot be registered.
2003 ///
2004 /// # Deferred errors
2005 ///
2006 /// Element-count compatibility for symbolic dimensions is checked when
2007 /// concrete inputs reach compilation or execution. A mismatch is returned
2008 /// as [`Error::TensorRuntime`] with a typed `ShapeMismatch` source.
2009 pub fn reshape_sym(&self, shape: &[SymDim]) -> Result<TracedTensor> {
2010 let tensor_map = [(self.symbolic_id(), 0usize)];
2011 let to_shape = shape
2012 .iter()
2013 .map(|dim| {
2014 dim.to_dim_expr(&tensor_map)
2015 .map_err(|source| Error::SymbolicShapeConversion {
2016 op: "TracedTensor::reshape_sym",
2017 phase: ErrorPhase::GraphBuild,
2018 source,
2019 })
2020 })
2021 .collect::<Result<Vec<_>>>()?;
2022 let out_shape_hint = Some(shape.to_vec());
2023 apply_unary(
2024 StdTensorOp::Reshape { to_shape },
2025 self,
2026 shape.len(),
2027 out_shape_hint,
2028 )
2029 }
2030
2031 /// Broadcast into a larger shape with explicit dimension placement.
2032 ///
2033 /// # Examples
2034 ///
2035 /// ```rust
2036 /// # use tenferro_runtime::{TracedTensor};
2037 /// use tenferro_runtime::TraceContext;
2038 /// let trace = TraceContext::new();
2039 /// # let x = trace.from_vec_col_major(vec![3], vec![1.0_f64; 3]).unwrap();
2040 /// let y = x.broadcast_in_dim(&[2, 3], &[1])?;
2041 /// # Ok::<(), tenferro_runtime::Error>(())
2042 /// ```
2043 ///
2044 /// # Errors
2045 ///
2046 /// Returns [`Error::Validation`] with `RankMismatch` when `dims` does not
2047 /// have one entry per input axis, `AxisOutOfBounds` or `DuplicateAxis` for
2048 /// an invalid output mapping, or `InvalidArgument` when known dimensions
2049 /// cannot broadcast. [`Error::RuntimeStateSource`] reports failure to
2050 /// register the result metadata.
2051 pub fn broadcast_in_dim(&self, shape: &[usize], dims: &[usize]) -> Result<TracedTensor> {
2052 let out_shape_hint: Vec<SymDim> = shape.iter().copied().map(SymDim::from).collect();
2053 validate_broadcast_in_dim_args(
2054 self,
2055 &out_shape_hint,
2056 dims,
2057 "TracedTensor::broadcast_in_dim",
2058 )?;
2059 apply_unary(
2060 StdTensorOp::BroadcastInDim {
2061 shape: DimExpr::from_concrete(shape),
2062 dims: dims.to_vec(),
2063 },
2064 self,
2065 shape.len(),
2066 Some(out_shape_hint),
2067 )
2068 }
2069
2070 /// Broadcast into a symbolic target shape with explicit dimension
2071 /// placement.
2072 ///
2073 /// Unlike [`Self::broadcast_in_dim`], each axis of `shape` is a
2074 /// [`SymDim`], so the target shape can mix concrete sizes (via
2075 /// `SymDim::from(n)`) with symbolic references to this tensor's axes
2076 /// (via [`Self::axis_sym_dim`]) or to axes of other traced tensors.
2077 ///
2078 /// When `shape` contains a `SymDim` that references a traced tensor
2079 /// other than `self`, the referenced tensor(s) must be supplied in
2080 /// `shape_refs`. They are wired into the built op as auxiliary
2081 /// shape-reference inputs — the op does not read their data, only
2082 /// their runtime shape. `shape_refs` must be listed in the same order
2083 /// in which their tensor IDs first appear when walking `shape` after
2084 /// any references to `self`. Usually the simplest correct thing is to
2085 /// pass each unique non-self reference tensor once.
2086 ///
2087 /// # Examples
2088 ///
2089 /// ```
2090 /// use tenferro_runtime::{TracedTensor};
2091 ///
2092 /// use tenferro_runtime::TraceContext;
2093 /// let trace = TraceContext::new();
2094 /// let a = trace.from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap();
2095 /// let b = trace.from_vec_col_major(vec![3, 4], vec![1.0_f64; 12]).unwrap();
2096 /// let m = a.axis_sym_dim(0)?;
2097 /// let k = a.axis_sym_dim(1)?;
2098 /// let n = b.axis_sym_dim(1)?;
2099 /// // Broadcast `a[m, k]` to `[m, k, n]`, placing `a`'s axes at 0, 1
2100 /// // and taking `n` from `b` as an auxiliary shape reference.
2101 /// let a_b = a.broadcast_in_dim_sym(&[m, k, n], &[0, 1], &[&b])?;
2102 /// assert_eq!(a_b.rank(), 3);
2103 /// # Ok::<(), tenferro_runtime::Error>(())
2104 /// ```
2105 ///
2106 /// # Errors
2107 ///
2108 /// Returns [`Error::Validation`] with `RankMismatch`, `AxisOutOfBounds`,
2109 /// `DuplicateAxis`, or `InvalidArgument` when the output mapping or shape
2110 /// references are invalid, [`Error::SymbolicShapeConversion`] for an
2111 /// unmappable symbolic dimension, or [`Error::RuntimeStateSource`] when
2112 /// metadata cannot be registered.
2113 ///
2114 /// # Deferred errors
2115 ///
2116 /// If a symbolic output dimension is smaller than a non-unit input axis,
2117 /// the concrete broadcast check is deferred to compilation or execution
2118 /// and is returned as [`Error::TensorRuntime`] with a typed validation
2119 /// source.
2120 pub fn broadcast_in_dim_sym(
2121 &self,
2122 shape: &[SymDim],
2123 dims: &[usize],
2124 shape_refs: &[&TracedTensor],
2125 ) -> Result<TracedTensor> {
2126 validate_broadcast_in_dim_args(self, shape, dims, "TracedTensor::broadcast_in_dim_sym")?;
2127
2128 // Build a dedup'd list of shape-reference tensors (first occurrence
2129 // wins) and index them starting at 1 — the primary input `self`
2130 // is at 0.
2131 let mut dedup_refs: Vec<&TracedTensor> = Vec::with_capacity(shape_refs.len());
2132 let mut tensor_map: Vec<(u64, usize)> = vec![(self.symbolic_id(), 0)];
2133 for &t in shape_refs {
2134 if !tensor_map.iter().any(|(id, _)| *id == t.symbolic_id()) {
2135 let idx = tensor_map.len();
2136 tensor_map.push((t.symbolic_id(), idx));
2137 dedup_refs.push(t);
2138 }
2139 }
2140
2141 let to_shape: Vec<DimExpr> = shape
2142 .iter()
2143 .map(|dim| {
2144 dim.to_dim_expr(&tensor_map)
2145 .map_err(|source| Error::SymbolicShapeConversion {
2146 op: "broadcast_in_dim_sym",
2147 phase: ErrorPhase::GraphBuild,
2148 source,
2149 })
2150 })
2151 .collect::<Result<Vec<_>>>()?;
2152
2153 // Trim auxiliary shape-reference inputs down to those actually
2154 // used by the generated `DimExpr`s. If the target shape resolved
2155 // to all constants (the concrete-shape case) the op is a plain
2156 // unary broadcast with no extra parents. Otherwise the op needs
2157 // a contiguous prefix of shape-ref inputs covering every
2158 // referenced `input_idx`.
2159 let max_used_idx = DimExpr::max_input_idx_all(&to_shape).unwrap_or(0);
2160 let used_refs: Vec<&TracedTensor> = dedup_refs.into_iter().take(max_used_idx).collect();
2161
2162 let out_shape_hint = Some(shape.to_vec());
2163 apply_unary_with_shape_refs(
2164 StdTensorOp::BroadcastInDim {
2165 shape: to_shape,
2166 dims: dims.to_vec(),
2167 },
2168 self,
2169 &used_refs,
2170 shape.len(),
2171 out_shape_hint,
2172 )
2173 }
2174
2175 /// Slice with explicit start, limit, and stride per axis.
2176 ///
2177 /// # Errors
2178 ///
2179 /// Returns [`Error::Validation`] with `RankMismatch` when the start/limit/
2180 /// stride vectors do not match the input rank, `InvalidSliceStep` when a
2181 /// stride is zero, `InvalidSliceBounds` when a limit precedes its start or
2182 /// exceeds a concrete input extent, or [`Error::RuntimeStateSource`] when
2183 /// output metadata cannot be registered.
2184 pub fn slice(&self, config: SliceConfig) -> Result<TracedTensor> {
2185 let op = StdTensorOp::Slice(config);
2186 let (out_rank, out_shape_hint) =
2187 infer_traced_single_output_shape("TracedTensor::slice", &op, &[self])?;
2188 apply_unary(op, self, out_rank, out_shape_hint)
2189 }
2190
2191 /// Pad with zeros using StableHLO-style edge and interior padding.
2192 ///
2193 /// # Errors
2194 ///
2195 /// Returns [`Error::Validation`] with `RankMismatch` when padding vectors
2196 /// do not match the input rank, `InvalidArgument` for negative interior
2197 /// padding, or `IntegerOverflow` when the padded extent exceeds `usize`.
2198 /// [`Error::RuntimeStateSource`] is returned when output metadata cannot be
2199 /// registered.
2200 pub fn pad(&self, config: PadConfig) -> Result<TracedTensor> {
2201 let op = StdTensorOp::Pad(config);
2202 let (out_rank, out_shape_hint) =
2203 infer_traced_single_output_shape("TracedTensor::pad", &op, &[self])?;
2204 apply_unary(op, self, out_rank, out_shape_hint)
2205 }
2206
2207 /// Reverse the order of elements along the requested axes.
2208 ///
2209 /// # Errors
2210 ///
2211 /// Returns [`Error::Validation`] with `AxisOutOfBounds` when an axis is
2212 /// outside the input rank or `DuplicateAxis` when `axes` repeats one, or
2213 /// [`Error::RuntimeStateSource`] when result metadata cannot be
2214 /// registered.
2215 pub fn reverse(&self, axes: &[usize]) -> Result<TracedTensor> {
2216 validate_traced_axes(self.rank, axes, "TracedTensor::reverse")?;
2217 apply_unary(
2218 StdTensorOp::Reverse {
2219 axes: axes.to_vec(),
2220 },
2221 self,
2222 self.rank,
2223 self.shape_hint.clone(),
2224 )
2225 }
2226
2227 /// Gather slices from `self` using integer start indices.
2228 ///
2229 /// # Errors
2230 ///
2231 /// Returns [`Error::Validation`] with `RankMismatch`, `AxisOutOfBounds`,
2232 /// `DuplicateAxis`, or `ShapeMismatch` when indices or the gather
2233 /// configuration is incompatible with the input, and
2234 /// [`Error::RuntimeStateSource`] when output metadata cannot be
2235 /// registered.
2236 ///
2237 /// # Clamping
2238 ///
2239 /// Index values are clamped with StableHLO gather clamp semantics: an
2240 /// out-of-range or negative index is clamped into `[0, dim - window]` rather than rejected.
2241 /// Only a `slice_sizes` window that exceeds its operand dimension is rejected,
2242 /// which is a graph-build failure.
2243 pub fn gather(&self, indices: &TracedTensor, config: GatherConfig) -> Result<TracedTensor> {
2244 let op = StdTensorOp::Gather(config);
2245 let (out_rank, out_shape_hint) =
2246 infer_traced_single_output_shape("TracedTensor::gather", &op, &[self, indices])?;
2247 apply_binary_preserve_input_dtypes(op, self, indices, out_rank, out_shape_hint, self.dtype)
2248 }
2249
2250 /// Scatter updates into `self` using StableHLO scatter semantics.
2251 ///
2252 /// # Errors
2253 ///
2254 /// Returns [`Error::Validation`] with `RankMismatch`, `AxisOutOfBounds`,
2255 /// `DuplicateAxis`, or `ShapeMismatch` when indices, updates, or the
2256 /// scatter configuration is incompatible, [`Error::TensorRuntime`] with
2257 /// `UnsupportedDTypeConversion` when dtype promotion cannot be
2258 /// represented, or [`Error::RuntimeStateSource`] when output metadata
2259 /// cannot be registered.
2260 ///
2261 /// # Clamping
2262 ///
2263 /// Index *values* clamp with StableHLO scatter semantics: an out-of-range or
2264 /// negative index is clamped rather than rejected, into the operand's extent.
2265 ///
2266 /// # Deferred errors
2267 ///
2268 /// The index and update *shapes* and the scatter configuration are checked
2269 /// after binding: an incompatible one is returned as [`Error::TensorRuntime`]
2270 /// with its typed validation source and [`ErrorPhase::Execution`]. An update
2271 /// window that exceeds the operand is the one exception: the CPU backend
2272 /// rejects it, while the CUDA kernel drops the update, so it is not part of
2273 /// this contract.
2274 pub fn scatter(
2275 &self,
2276 indices: &TracedTensor,
2277 updates: &TracedTensor,
2278 config: ScatterConfig,
2279 ) -> Result<TracedTensor> {
2280 let op = StdTensorOp::Scatter(config);
2281 let (out_rank, out_shape_hint) = infer_traced_single_output_shape(
2282 "TracedTensor::scatter",
2283 &op,
2284 &[self, indices, updates],
2285 )?;
2286 let out_dtype = crate::shape_infer::promote_dtype(self.dtype, updates.dtype);
2287 let operand = if self.dtype != out_dtype {
2288 self.cast(out_dtype)?
2289 } else {
2290 self.clone()
2291 };
2292 let updates = if updates.dtype != out_dtype {
2293 updates.cast(out_dtype)?
2294 } else {
2295 updates.clone()
2296 };
2297 apply_ternary_with_output_dtype(
2298 op,
2299 &operand,
2300 indices,
2301 &updates,
2302 out_rank,
2303 out_shape_hint,
2304 out_dtype,
2305 )
2306 }
2307
2308 /// Slice using runtime start indices.
2309 ///
2310 /// # Errors
2311 ///
2312 /// Returns [`Error::Validation`] with `RankMismatch` when `sizes` has a rank
2313 /// other than the operand's, with `ShapeMismatch` when a `sizes` window
2314 /// exceeds a concrete operand dimension, and with `InvalidArgument` when
2315 /// `starts` is not a rank-1 vector holding one entry per operand axis. These
2316 /// are graph-build checks (`ErrorPhase::GraphBuild`, #2160) mirroring the
2317 /// `gather` sibling. The eager surface reaches the same conditions at
2318 /// execution, where the backends name the oversized window `InvalidArgument`
2319 /// (`"configuration"` on CPU, `"slice_sizes"` on CUDA) rather than
2320 /// `ShapeMismatch`. Also returns [`Error::RuntimeStateSource`] when output
2321 /// metadata cannot be registered.
2322 ///
2323 /// # Clamping
2324 ///
2325 /// Start *values* clamp with StableHLO `DynamicSlice` semantics: an
2326 /// out-of-range or negative start is clamped rather than rejected, into
2327 /// `[0, dim - window]`. Only the `slice_sizes` window itself is rejected, and
2328 /// it is rejected when the graph is built. An operand extent or a `starts`
2329 /// length that is still symbolic at that point defers its check to the phase
2330 /// that supplies the extent, and the reported [`ErrorPhase`] says which.
2331 pub fn dynamic_slice(&self, starts: &TracedTensor, sizes: &[usize]) -> Result<TracedTensor> {
2332 let op = StdTensorOp::DynamicSlice {
2333 slice_sizes: sizes.to_vec(),
2334 };
2335 let (out_rank, out_shape_hint) =
2336 infer_traced_single_output_shape("TracedTensor::dynamic_slice", &op, &[self, starts])?;
2337 apply_binary_preserve_input_dtypes(op, self, starts, out_rank, out_shape_hint, self.dtype)
2338 }
2339
2340 /// Keep the lower triangle and zero the rest.
2341 ///
2342 /// # Examples
2343 ///
2344 /// ```rust
2345 /// # use tenferro_runtime::{TracedTensor};
2346 /// use tenferro_runtime::TraceContext;
2347 /// let trace = TraceContext::new();
2348 /// let matrix = trace.from_vec_col_major(vec![2, 2], vec![1.0_f64; 4])?;
2349 /// let lower = matrix.tril(0)?;
2350 /// assert_eq!(lower.rank(), 2);
2351 /// # Ok::<(), tenferro_runtime::Error>(())
2352 /// ```
2353 ///
2354 /// # Errors
2355 ///
2356 /// Returns [`Error::RuntimeStateSource`] when traced output metadata
2357 /// registration is unavailable or inconsistent with the graph.
2358 pub fn tril(&self, k: i64) -> Result<TracedTensor> {
2359 apply_unary(
2360 StdTensorOp::Tril { k },
2361 self,
2362 self.rank,
2363 self.shape_hint.clone(),
2364 )
2365 }
2366
2367 /// Keep the upper triangle and zero the rest.
2368 ///
2369 /// # Examples
2370 ///
2371 /// ```rust
2372 /// # use tenferro_runtime::{TracedTensor};
2373 /// use tenferro_runtime::TraceContext;
2374 /// let trace = TraceContext::new();
2375 /// let matrix = trace.from_vec_col_major(vec![2, 2], vec![1.0_f64; 4])?;
2376 /// let upper = matrix.triu(0)?;
2377 /// assert_eq!(upper.rank(), 2);
2378 /// # Ok::<(), tenferro_runtime::Error>(())
2379 /// ```
2380 ///
2381 /// # Errors
2382 ///
2383 /// Returns [`Error::RuntimeStateSource`] when traced output metadata
2384 /// registration is unavailable or inconsistent with the graph.
2385 pub fn triu(&self, k: i64) -> Result<TracedTensor> {
2386 apply_unary(
2387 StdTensorOp::Triu { k },
2388 self,
2389 self.rank,
2390 self.shape_hint.clone(),
2391 )
2392 }
2393
2394 /// Permute tensor axes.
2395 ///
2396 /// # Examples
2397 ///
2398 /// ```rust
2399 /// # use tenferro_runtime::{TracedTensor};
2400 /// use tenferro_runtime::TraceContext;
2401 /// let trace = TraceContext::new();
2402 /// # let x = trace.from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap();
2403 /// let y = x.transpose(&[1, 0])?;
2404 /// # Ok::<(), tenferro_runtime::Error>(())
2405 /// ```
2406 ///
2407 /// # Errors
2408 ///
2409 /// Returns [`Error::Validation`] with `InvalidPermutationLength`,
2410 /// `AxisOutOfBounds`, or `DuplicateAxis` when `perm` is not a valid
2411 /// permutation of the tensor axes, or [`Error::RuntimeStateSource`] when
2412 /// output metadata registration fails.
2413 pub fn transpose(&self, perm: &[usize]) -> Result<TracedTensor> {
2414 validate_traced_perm(self.rank, perm, "TracedTensor::transpose")?;
2415 let out_shape_hint = self
2416 .shape_hint
2417 .as_ref()
2418 .map(|shape| perm.iter().map(|&p| shape[p].clone()).collect());
2419 apply_unary(
2420 StdTensorOp::Transpose {
2421 perm: perm.to_vec(),
2422 },
2423 self,
2424 self.rank,
2425 out_shape_hint,
2426 )
2427 }
2428
2429 /// Extract the diagonal along two axes.
2430 ///
2431 /// # Examples
2432 ///
2433 /// ```rust
2434 /// # use tenferro_runtime::{TracedTensor};
2435 /// use tenferro_runtime::TraceContext;
2436 /// let trace = TraceContext::new();
2437 /// # let x = trace.from_vec_col_major(vec![2, 2], vec![1.0_f64; 4]).unwrap();
2438 /// let y = x.extract_diag(0, 1)?;
2439 /// # Ok::<(), tenferro_runtime::Error>(())
2440 /// ```
2441 ///
2442 /// # Errors
2443 ///
2444 /// Returns [`Error::Validation`] with `AxisOutOfBounds` when either axis
2445 /// is outside the input rank or `InvalidArgument` when `axis_a == axis_b`.
2446 pub fn extract_diag(&self, axis_a: usize, axis_b: usize) -> Result<TracedTensor> {
2447 validate_traced_axis(self, axis_a, "TracedTensor::extract_diag")?;
2448 validate_traced_axis(self, axis_b, "TracedTensor::extract_diag")?;
2449 if axis_a == axis_b {
2450 return Err(graph_invalid_argument(
2451 "TracedTensor::extract_diag",
2452 "axes",
2453 "diagonal axes must be distinct",
2454 ));
2455 }
2456 let op = StdTensorOp::ExtractDiag { axis_a, axis_b };
2457 let (out_rank, out_shape_hint) =
2458 infer_traced_single_output_shape("TracedTensor::extract_diag", &op, &[self])?;
2459 apply_unary(op, self, out_rank, out_shape_hint)
2460 }
2461
2462 /// Embed a vector or lower-rank tensor along a diagonal.
2463 ///
2464 /// # Examples
2465 ///
2466 /// ```rust
2467 /// # use tenferro_runtime::{TracedTensor};
2468 /// use tenferro_runtime::TraceContext;
2469 /// let trace = TraceContext::new();
2470 /// # let x = trace.from_vec_col_major(vec![2], vec![1.0_f64; 2]).unwrap();
2471 /// let y = x.embed_diag(0, 1)?;
2472 /// # Ok::<(), tenferro_runtime::Error>(())
2473 /// ```
2474 ///
2475 /// # Errors
2476 ///
2477 /// Returns [`Error::Validation`] with `AxisOutOfBounds` when `axis_a` is
2478 /// outside the input rank or `InvalidArgument` when `axis_b` is not a
2479 /// valid insertion axis.
2480 pub fn embed_diag(&self, axis_a: usize, axis_b: usize) -> Result<TracedTensor> {
2481 validate_traced_axis(self, axis_a, "TracedTensor::embed_diag")?;
2482 validate_traced_insert_axis(self.rank, axis_b, "TracedTensor::embed_diag")?;
2483 let out_shape_hint = self.shape_hint.as_ref().map(|shape| {
2484 let mut out_shape = shape.clone();
2485 out_shape.insert(axis_b, shape[axis_a].clone());
2486 out_shape
2487 });
2488 apply_unary(
2489 StdTensorOp::EmbedDiag { axis_a, axis_b },
2490 self,
2491 self.rank + 1,
2492 out_shape_hint,
2493 )
2494 }
2495
2496 /// Return the runtime size of one axis as a scalar `f64` tensor.
2497 ///
2498 /// The result is metadata-derived and therefore has no gradient.
2499 ///
2500 /// # Examples
2501 ///
2502 /// ```
2503 /// use tenferro_cpu::CpuEngine;
2504 /// use tenferro_runtime::{ExecutionEngine, GraphCompiler, Runtime, TracedTensor};
2505 ///
2506 /// use tenferro_runtime::TraceContext;
2507 /// let trace = TraceContext::new();
2508 /// let x = trace.from_vec_col_major(vec![2, 3], vec![1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap();
2509 /// let cols = x.shape_of(1)?;
2510 /// let mut compiler = GraphCompiler::new();
2511 /// let program = compiler.compile(&cols).unwrap();
2512 /// let mut engine = CpuEngine::serial();
2513 /// let mut builder = Runtime::builder();
2514 /// builder
2515 /// .register_engine(tenferro_cpu::runtime_engine_registration(&engine).unwrap())
2516 /// .unwrap();
2517 /// let runtime = builder.build().unwrap();
2518 /// let prepared = runtime
2519 /// .prepare(&program, &[], &[engine.target()])
2520 /// .unwrap();
2521 /// let mut outputs = runtime
2522 /// .run(&mut [&mut engine], &prepared, &[])
2523 /// .unwrap();
2524 /// let out = outputs.remove(0).into_tensor(&mut engine).unwrap();
2525 /// assert_eq!(out.shape(), &[] as &[usize]);
2526 /// # Ok::<(), tenferro_runtime::Error>(())
2527 /// ```
2528 ///
2529 /// # Errors
2530 ///
2531 /// Returns [`Error::Validation`] with `AxisOutOfBounds` when `axis` is
2532 /// outside the input rank, or [`Error::RuntimeStateSource`] when scalar
2533 /// output metadata cannot be registered.
2534 pub fn shape_of(&self, axis: usize) -> Result<TracedTensor> {
2535 validate_traced_axis(self, axis, "TracedTensor::shape_of")?;
2536 apply_unary_with_dtype(
2537 StdTensorOp::ShapeOf { axis },
2538 self,
2539 0,
2540 Some(vec![]),
2541 DType::F64,
2542 )
2543 }
2544
2545 /// Truncate this tensor along `axis` to the first `size` elements.
2546 ///
2547 /// `size` is read at runtime from a scalar traced tensor. Values are
2548 /// rounded to the nearest integer, clamped to `[0, self.shape[axis]]`,
2549 /// and the output keeps the same element dtype as the input.
2550 ///
2551 /// # Examples
2552 ///
2553 /// ```
2554 /// use tenferro_cpu::CpuEngine;
2555 /// use tenferro_runtime::{ExecutionEngine, GraphCompiler, Runtime, TracedTensor};
2556 ///
2557 /// use tenferro_runtime::TraceContext;
2558 /// let trace = TraceContext::new();
2559 /// let x = trace.from_vec_col_major(vec![4], vec![1.0_f64, 2.0, 3.0, 4.0]).unwrap();
2560 /// let size = trace.from_vec_col_major(vec![], vec![2.0_f64]).unwrap();
2561 /// let y = x.dynamic_truncate(&size, 0)?;
2562 /// let mut compiler = GraphCompiler::new();
2563 /// let program = compiler.compile(&y).unwrap();
2564 /// let mut engine = CpuEngine::serial();
2565 /// let mut builder = Runtime::builder();
2566 /// builder
2567 /// .register_engine(tenferro_cpu::runtime_engine_registration(&engine).unwrap())
2568 /// .unwrap();
2569 /// let runtime = builder.build().unwrap();
2570 /// let prepared = runtime
2571 /// .prepare(&program, &[], &[engine.target()])
2572 /// .unwrap();
2573 /// let mut outputs = runtime
2574 /// .run(&mut [&mut engine], &prepared, &[])
2575 /// .unwrap();
2576 /// let out = outputs.remove(0).into_tensor(&mut engine).unwrap();
2577 /// assert_eq!(out.shape(), &[2]);
2578 /// # Ok::<(), tenferro_runtime::Error>(())
2579 /// ```
2580 ///
2581 /// # Errors
2582 ///
2583 /// Returns [`Error::Validation`] with `AxisOutOfBounds` when `axis` is
2584 /// outside the input rank or `RankMismatch` when `size` is not scalar.
2585 ///
2586 /// # Deferred errors
2587 ///
2588 /// At execution, non-`f32`/`f64`/`i64` size dtypes return
2589 /// [`Error::TensorRuntime`] with `Unsupported`, non-finite size values
2590 /// return a typed `InvalidArgument`, and an empty scalar buffer returns a
2591 /// typed runtime-state source.
2592 pub fn dynamic_truncate(&self, size: &TracedTensor, axis: usize) -> Result<TracedTensor> {
2593 validate_traced_axis(self, axis, "TracedTensor::dynamic_truncate")?;
2594 if size.rank != 0 {
2595 return Err(graph_validation(
2596 "TracedTensor::dynamic_truncate",
2597 ValidationError::RankMismatch {
2598 expected: 0,
2599 actual: size.rank,
2600 },
2601 ));
2602 }
2603 apply_binary_preserve_input_dtypes(
2604 StdTensorOp::DynamicTruncate { axis },
2605 self,
2606 size,
2607 self.rank,
2608 None,
2609 self.dtype,
2610 )
2611 }
2612
2613 /// Pad this tensor with zeros along `axis` to match `reference.shape[axis]`.
2614 ///
2615 /// If `reference` is smaller along that axis, this is a no-op.
2616 ///
2617 /// # Examples
2618 ///
2619 /// ```
2620 /// use tenferro_cpu::CpuEngine;
2621 /// use tenferro_runtime::{ExecutionEngine, GraphCompiler, Runtime, TracedTensor};
2622 ///
2623 /// use tenferro_runtime::TraceContext;
2624 /// let trace = TraceContext::new();
2625 /// let x = trace.from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
2626 /// let reference = trace.from_vec_col_major(vec![4], vec![0.0_f64, 0.0, 0.0, 0.0]).unwrap();
2627 /// let y = x.pad_to_match(&reference, 0)?;
2628 /// let mut compiler = GraphCompiler::new();
2629 /// let program = compiler.compile(&y).unwrap();
2630 /// let mut engine = CpuEngine::serial();
2631 /// let mut builder = Runtime::builder();
2632 /// builder
2633 /// .register_engine(tenferro_cpu::runtime_engine_registration(&engine).unwrap())
2634 /// .unwrap();
2635 /// let runtime = builder.build().unwrap();
2636 /// let prepared = runtime
2637 /// .prepare(&program, &[], &[engine.target()])
2638 /// .unwrap();
2639 /// let mut outputs = runtime
2640 /// .run(&mut [&mut engine], &prepared, &[])
2641 /// .unwrap();
2642 /// let out = outputs.remove(0).into_tensor(&mut engine).unwrap();
2643 /// assert_eq!(out.shape(), &[4]);
2644 /// # Ok::<(), tenferro_runtime::Error>(())
2645 /// ```
2646 ///
2647 /// # Errors
2648 ///
2649 /// Returns [`Error::Validation`] with `AxisOutOfBounds` when `axis` is
2650 /// outside either tensor's rank, or [`Error::RuntimeStateSource`] when
2651 /// output metadata cannot be registered.
2652 pub fn pad_to_match(&self, reference: &TracedTensor, axis: usize) -> Result<TracedTensor> {
2653 validate_traced_axis(self, axis, "TracedTensor::pad_to_match")?;
2654 validate_traced_axis(reference, axis, "TracedTensor::pad_to_match")?;
2655 let op = StdTensorOp::PadToMatch { axis };
2656 let (out_rank, out_shape_hint) = infer_traced_single_output_shape(
2657 "TracedTensor::pad_to_match",
2658 &op,
2659 &[self, reference],
2660 )?;
2661 apply_binary_preserve_input_dtypes(
2662 op,
2663 self,
2664 reference,
2665 out_rank,
2666 out_shape_hint,
2667 self.dtype,
2668 )
2669 }
2670}
2671
2672pub(crate) fn apply_unary(
2673 op: StdTensorOp,
2674 input: &TracedTensor,
2675 out_rank: usize,
2676 out_shape_hint: Option<Vec<SymDim>>,
2677) -> Result<TracedTensor> {
2678 let out_dtype = try_inferred_output_dtype(&op, &[input.dtype], "apply_unary")?;
2679 apply_unary_with_dtype(op, input, out_rank, out_shape_hint, out_dtype)
2680}
2681
2682fn try_apply_unary(
2683 op: StdTensorOp,
2684 input: &TracedTensor,
2685 out_rank: usize,
2686 out_shape_hint: Option<Vec<SymDim>>,
2687 context: &'static str,
2688) -> Result<TracedTensor> {
2689 let out_dtype = try_inferred_output_dtype(&op, &[input.dtype], context)?;
2690 apply_unary_with_dtype(op, input, out_rank, out_shape_hint, out_dtype)
2691}
2692
2693pub(crate) fn apply_unary_with_dtype(
2694 op: StdTensorOp,
2695 input: &TracedTensor,
2696 out_rank: usize,
2697 out_shape_hint: Option<Vec<SymDim>>,
2698 out_dtype: DType,
2699) -> Result<TracedTensor> {
2700 let outputs = append_core(&input.ctx, &op, &[input.value])?;
2701 finish_value(&input.ctx, outputs[0], out_rank, out_dtype, out_shape_hint)
2702}
2703
2704/// Apply a unary-primary op that additionally references one or more
2705/// tensors for shape resolution only.
2706///
2707/// The primary `input` becomes op input 0; each tensor in `shape_refs`
2708/// becomes op input 1, 2, … in order. Used by
2709/// [`TracedTensor::broadcast_in_dim_sym`] when the target shape
2710/// references axes of tensors other than the primary input; the op
2711/// reads only their runtime shape, not their data.
2712pub(crate) fn apply_unary_with_shape_refs(
2713 op: StdTensorOp,
2714 input: &TracedTensor,
2715 shape_refs: &[&TracedTensor],
2716 out_rank: usize,
2717 out_shape_hint: Option<Vec<SymDim>>,
2718) -> Result<TracedTensor> {
2719 let mut inputs = Vec::with_capacity(1 + shape_refs.len());
2720 inputs.push(input.value);
2721 for tensor in shape_refs {
2722 ensure_same_context(input, tensor)?;
2723 inputs.push(tensor.value);
2724 }
2725 let outputs = append_core(&input.ctx, &op, &inputs)?;
2726 finish_value(
2727 &input.ctx,
2728 outputs[0],
2729 out_rank,
2730 input.dtype,
2731 out_shape_hint,
2732 )
2733}
2734
2735pub(crate) fn apply_nullary(
2736 op: StdTensorOp,
2737 rank: usize,
2738 dtype: DType,
2739 shape_hint: Option<Vec<SymDim>>,
2740 ctx: &TraceContext,
2741) -> Result<TracedTensor> {
2742 let outputs = append_core(ctx, &op, &[])?;
2743 finish_value(ctx, outputs[0], rank, dtype, shape_hint)
2744}
2745
2746pub(crate) fn apply_binary(
2747 op: StdTensorOp,
2748 lhs: &TracedTensor,
2749 rhs: &TracedTensor,
2750 out_rank: usize,
2751 out_shape_hint: Option<Vec<SymDim>>,
2752) -> Result<TracedTensor> {
2753 let input_dtype = crate::shape_infer::promote_dtype_for_binary_op(&op, lhs.dtype, rhs.dtype);
2754 let out_dtype = try_inferred_output_dtype(&op, &[lhs.dtype, rhs.dtype], "apply_binary")?;
2755
2756 // Insert Convert ops when an input dtype differs from the primitive input dtype.
2757 let lhs = if lhs.dtype != input_dtype {
2758 lhs.cast(input_dtype)?
2759 } else {
2760 lhs.clone()
2761 };
2762 let rhs = if rhs.dtype != input_dtype {
2763 rhs.cast(input_dtype)?
2764 } else {
2765 rhs.clone()
2766 };
2767
2768 apply_binary_with_output_dtype(op, &lhs, &rhs, out_rank, out_shape_hint, out_dtype)
2769}
2770
2771fn try_apply_binary(
2772 op: StdTensorOp,
2773 lhs: &TracedTensor,
2774 rhs: &TracedTensor,
2775 out_rank: usize,
2776 out_shape_hint: Option<Vec<SymDim>>,
2777 context: &'static str,
2778) -> Result<TracedTensor> {
2779 let input_dtype = crate::shape_infer::promote_dtype_for_binary_op(&op, lhs.dtype, rhs.dtype);
2780 let out_dtype = try_inferred_output_dtype(&op, &[lhs.dtype, rhs.dtype], context)?;
2781
2782 let lhs = if lhs.dtype != input_dtype {
2783 lhs.cast(input_dtype)?
2784 } else {
2785 lhs.clone()
2786 };
2787 let rhs = if rhs.dtype != input_dtype {
2788 rhs.cast(input_dtype)?
2789 } else {
2790 rhs.clone()
2791 };
2792
2793 apply_binary_with_output_dtype(op, &lhs, &rhs, out_rank, out_shape_hint, out_dtype)
2794}
2795
2796pub(crate) fn apply_binary_preserve_input_dtypes(
2797 op: StdTensorOp,
2798 lhs: &TracedTensor,
2799 rhs: &TracedTensor,
2800 out_rank: usize,
2801 out_shape_hint: Option<Vec<SymDim>>,
2802 out_dtype: DType,
2803) -> Result<TracedTensor> {
2804 apply_binary_with_output_dtype(op, lhs, rhs, out_rank, out_shape_hint, out_dtype)
2805}
2806
2807pub(crate) fn apply_broadcast_binary_op(
2808 op: StdTensorOp,
2809 lhs: &TracedTensor,
2810 rhs: &TracedTensor,
2811) -> Result<TracedTensor> {
2812 let (lhs, rhs) = broadcast_binary(lhs, rhs)?;
2813 try_apply_binary(
2814 op,
2815 &lhs,
2816 &rhs,
2817 lhs.rank,
2818 lhs.shape_hint.clone(),
2819 "broadcast_binary",
2820 )
2821}
2822
2823pub(crate) fn apply_broadcast_ternary_op(
2824 op: StdTensorOp,
2825 first: &TracedTensor,
2826 second: &TracedTensor,
2827 third: &TracedTensor,
2828) -> Result<TracedTensor> {
2829 let (first, second, third) = broadcast_ternary(first, second, third)?;
2830 try_apply_ternary(
2831 op,
2832 &first,
2833 &second,
2834 &third,
2835 first.rank,
2836 first.shape_hint.clone(),
2837 "broadcast_ternary",
2838 )
2839}
2840
2841fn try_apply_ternary(
2842 op: StdTensorOp,
2843 first: &TracedTensor,
2844 second: &TracedTensor,
2845 third: &TracedTensor,
2846 out_rank: usize,
2847 out_shape_hint: Option<Vec<SymDim>>,
2848 context: &'static str,
2849) -> Result<TracedTensor> {
2850 let out_dtype =
2851 try_inferred_output_dtype(&op, &[first.dtype, second.dtype, third.dtype], context)?;
2852 let (first, second, third) = match op {
2853 StdTensorOp::Select => {
2854 let value_dtype = crate::shape_infer::promote_dtype(second.dtype, third.dtype);
2855 let second = if second.dtype != value_dtype {
2856 second.cast(value_dtype)?
2857 } else {
2858 second.clone()
2859 };
2860 let third = if third.dtype != value_dtype {
2861 third.cast(value_dtype)?
2862 } else {
2863 third.clone()
2864 };
2865 (first.clone(), second, third)
2866 }
2867 _ => {
2868 let input_dtype =
2869 crate::shape_infer::promote_dtypes([first.dtype, second.dtype, third.dtype]);
2870 let first = if first.dtype != input_dtype {
2871 first.cast(input_dtype)?
2872 } else {
2873 first.clone()
2874 };
2875 let second = if second.dtype != input_dtype {
2876 second.cast(input_dtype)?
2877 } else {
2878 second.clone()
2879 };
2880 let third = if third.dtype != input_dtype {
2881 third.cast(input_dtype)?
2882 } else {
2883 third.clone()
2884 };
2885 (first, second, third)
2886 }
2887 };
2888 apply_ternary_with_output_dtype(
2889 op,
2890 &first,
2891 &second,
2892 &third,
2893 out_rank,
2894 out_shape_hint,
2895 out_dtype,
2896 )
2897}
2898
2899fn apply_binary_with_output_dtype(
2900 op: StdTensorOp,
2901 lhs: &TracedTensor,
2902 rhs: &TracedTensor,
2903 out_rank: usize,
2904 out_shape_hint: Option<Vec<SymDim>>,
2905 out_dtype: DType,
2906) -> Result<TracedTensor> {
2907 ensure_same_context(lhs, rhs)?;
2908 let outputs = append_core(&lhs.ctx, &op, &[lhs.value, rhs.value])?;
2909 finish_value(&lhs.ctx, outputs[0], out_rank, out_dtype, out_shape_hint)
2910}
2911
2912fn apply_ternary_with_output_dtype(
2913 op: StdTensorOp,
2914 first: &TracedTensor,
2915 second: &TracedTensor,
2916 third: &TracedTensor,
2917 out_rank: usize,
2918 out_shape_hint: Option<Vec<SymDim>>,
2919 out_dtype: DType,
2920) -> Result<TracedTensor> {
2921 ensure_same_context(first, second)?;
2922 ensure_same_context(first, third)?;
2923 let outputs = append_core(&first.ctx, &op, &[first.value, second.value, third.value])?;
2924 finish_value(&first.ctx, outputs[0], out_rank, out_dtype, out_shape_hint)
2925}
2926
2927/// Reject a combination of traced values that belong to different contexts.
2928pub(crate) fn ensure_same_context(lhs: &TracedTensor, rhs: &TracedTensor) -> Result<()> {
2929 if lhs.ctx.same_context(&rhs.ctx) {
2930 return Ok(());
2931 }
2932 Err(Error::invalid_argument(
2933 "TracedTensor",
2934 ErrorPhase::GraphBuild,
2935 "context",
2936 "traced values from different contexts cannot be combined; build both in the same trace",
2937 ))
2938}
2939
2940mod composite_ops;
2941
2942#[cfg(test)]
2943mod tests;