1use std::ops::Range;
2use std::sync::Arc;
3
4use computegraph::graph::GraphBuilder;
5use computegraph::types::{OperationRole, ValueRef};
6use tenferro_ops::std_tensor_op::StdTensorOp;
7use tenferro_tensor::{
8 GatherConfig, ShapeMismatch, ShapeVec, SliceConfig, Tensor, TypedTensor, ValidationError,
9};
10
11use crate::checkpoint::CheckpointNode;
12use crate::error::{Error, ErrorPhase, Result};
13use crate::metadata::{register_scoped_value_metadata, tensor_meta, MetadataScopeChain};
14use crate::shape_constraint::ConstraintScopeChain;
15use crate::shape_infer::promote_dtypes;
16use crate::sym_dim::SymDim;
17use crate::traced::{
18 apply_binary_preserve_input_dtypes, infer_traced_single_output_shape, merge_traced_inputs_map,
19 next_traced_id, try_concrete_shape,
20};
21use crate::TracedTensor;
22
23fn normalize_existing_axis(op: &'static str, axis: isize, rank: usize) -> Result<usize> {
24 let normalized = if axis >= 0 {
25 axis as usize
26 } else {
27 rank.checked_sub(axis.unsigned_abs()).ok_or_else(|| {
28 Error::validation(
29 op,
30 ErrorPhase::GraphBuild,
31 ValidationError::AxisOutOfBounds {
32 axis: axis.unsigned_abs(),
33 rank,
34 },
35 )
36 })?
37 };
38 if normalized >= rank {
39 return Err(Error::validation(
40 op,
41 ErrorPhase::GraphBuild,
42 ValidationError::AxisOutOfBounds {
43 axis: axis.unsigned_abs(),
44 rank,
45 },
46 ));
47 }
48 Ok(normalized)
49}
50
51fn normalize_insert_axis(op: &'static str, axis: isize, rank: usize) -> Result<usize> {
52 let insert_rank = rank.checked_add(1).ok_or_else(|| {
53 Error::validation(
54 op,
55 ErrorPhase::GraphBuild,
56 ValidationError::AxisOutOfBounds {
57 axis: axis.unsigned_abs(),
58 rank,
59 },
60 )
61 })?;
62 let normalized = if axis >= 0 {
63 axis as usize
64 } else {
65 insert_rank
66 .checked_sub(axis.unsigned_abs())
67 .ok_or_else(|| {
68 Error::validation(
69 op,
70 ErrorPhase::GraphBuild,
71 ValidationError::AxisOutOfBounds {
72 axis: axis.unsigned_abs(),
73 rank: insert_rank,
74 },
75 )
76 })?
77 };
78 if normalized > rank {
79 return Err(Error::validation(
80 op,
81 ErrorPhase::GraphBuild,
82 ValidationError::AxisOutOfBounds {
83 axis: axis.unsigned_abs(),
84 rank: insert_rank,
85 },
86 ));
87 }
88 Ok(normalized)
89}
90
91fn index_select_config(
92 shape: &[usize],
93 axis: isize,
94 positions: &[usize],
95) -> Result<(Tensor, GatherConfig, Vec<usize>)> {
96 let axis = normalize_existing_axis("index_select", axis, shape.len())?;
97 let axis_extent = shape[axis];
98 for &position in positions {
99 if position >= axis_extent {
100 return Err(Error::validation(
101 "index_select",
102 ErrorPhase::GraphBuild,
103 ValidationError::InvalidArgument {
104 argument: "positions",
105 message: format!(
106 "position {position} out of bounds for axis {axis} with extent {axis_extent}"
107 ),
108 },
109 ));
110 }
111 }
112
113 let mut out_shape = shape.to_vec();
114 out_shape[axis] = positions.len();
115
116 let mut slice_sizes = shape.to_vec();
117 slice_sizes[axis] = 1;
118
119 let offset_dims = (0..shape.len()).filter(|&dim| dim != axis).collect();
120 let index_data = positions
121 .iter()
122 .map(|&position| {
123 i64::try_from(position).map_err(|_| {
124 Error::validation(
125 "index_select",
126 ErrorPhase::GraphBuild,
127 ValidationError::InvalidArgument {
128 argument: "positions",
129 message: format!("position {position} cannot be represented as i64"),
130 },
131 )
132 })
133 })
134 .collect::<Result<Vec<_>>>()?;
135 let indices = Tensor::I64(TypedTensor::from_vec_col_major(
136 vec![positions.len(), 1],
137 index_data,
138 )?);
139
140 let config = GatherConfig {
141 offset_dims,
142 collapsed_slice_dims: vec![axis],
143 start_index_map: vec![axis],
144 index_vector_dim: 1,
145 slice_sizes,
146 };
147
148 Ok((indices, config, out_shape))
149}
150
151fn validate_stack_shapes(op: &'static str, shapes: &[&[usize]]) -> Result<()> {
152 let Some(first) = shapes.first() else {
153 return Err(Error::validation(
154 op,
155 ErrorPhase::GraphBuild,
156 ValidationError::InvalidArgument {
157 argument: "tensors",
158 message: "stack requires at least one input".into(),
159 },
160 ));
161 };
162 for shape in shapes.iter().skip(1) {
163 if *shape != *first {
164 return Err(Error::validation(
165 op,
166 ErrorPhase::GraphBuild,
167 ShapeMismatch::IncompatibleShapes {
168 lhs: ShapeVec::from_vec(first.to_vec()),
169 rhs: ShapeVec::from_vec(shape.to_vec()),
170 }
171 .into(),
172 ));
173 }
174 }
175 Ok(())
176}
177
178#[derive(Clone, Debug)]
179enum AxisSelection {
180 Slice {
181 axis: usize,
182 range: Range<usize>,
183 step: usize,
184 },
185 Take {
186 axis: usize,
187 indices: Vec<usize>,
188 },
189}
190
191fn concrete_shape_for_axis_slice(tensor: &TracedTensor, op: &'static str) -> Result<Vec<usize>> {
192 try_concrete_shape(tensor).ok_or_else(|| {
193 Error::validation(
194 op,
195 ErrorPhase::GraphBuild,
196 ValidationError::InvalidArgument {
197 argument: "shape",
198 message: format!("{op} requires a concrete shape hint"),
199 },
200 )
201 })
202}
203
204fn validate_axis_selection(
205 op: &'static str,
206 rank: usize,
207 seen: &mut [bool],
208 axis: usize,
209) -> Result<()> {
210 if axis >= rank {
211 return Err(Error::validation(
212 op,
213 ErrorPhase::GraphBuild,
214 ValidationError::AxisOutOfBounds { axis, rank },
215 ));
216 }
217 if seen[axis] {
218 return Err(Error::validation(
219 op,
220 ErrorPhase::GraphBuild,
221 ValidationError::DuplicateAxis {
222 axis,
223 role: "selection",
224 },
225 ));
226 }
227 seen[axis] = true;
228 Ok(())
229}
230
231fn apply_slice_axis_config(
232 op: &'static str,
233 shape: &[usize],
234 selections: &[AxisSelection],
235) -> Result<Option<SliceConfig>> {
236 let mut starts = vec![0; shape.len()];
237 let mut limits = shape.to_vec();
238 let mut strides = vec![1; shape.len()];
239 let mut has_slice = false;
240 for selection in selections {
241 let AxisSelection::Slice { axis, range, step } = selection else {
242 continue;
243 };
244 if *step == 0 {
245 return Err(Error::validation(
246 op,
247 ErrorPhase::GraphBuild,
248 ValidationError::InvalidSliceStep { step: 0 },
249 ));
250 }
251 let extent = shape[*axis];
252 if range.start > range.end || range.end > extent {
253 let start = isize::try_from(range.start).map_err(|_| {
254 Error::validation(op, ErrorPhase::GraphBuild, ValidationError::IntegerOverflow)
255 })?;
256 let end = isize::try_from(range.end).map_err(|_| {
257 Error::validation(op, ErrorPhase::GraphBuild, ValidationError::IntegerOverflow)
258 })?;
259 return Err(Error::validation(
260 op,
261 ErrorPhase::GraphBuild,
262 ValidationError::InvalidSliceBounds {
263 start,
264 end,
265 axis_len: extent,
266 },
267 ));
268 }
269 starts[*axis] = range.start;
270 limits[*axis] = range.end;
271 strides[*axis] = *step;
272 has_slice = true;
273 }
274 Ok(has_slice.then_some(SliceConfig {
275 starts,
276 limits,
277 strides,
278 }))
279}
280
281#[derive(Clone, Debug)]
297pub struct TracedSliceBuilder<'a> {
298 tensor: &'a TracedTensor,
299 selections: Vec<AxisSelection>,
300}
301
302impl<'a> TracedSliceBuilder<'a> {
303 fn new(tensor: &'a TracedTensor) -> Self {
304 Self {
305 tensor,
306 selections: Vec::new(),
307 }
308 }
309
310 pub fn axis(mut self, axis: usize, range: Range<usize>) -> Self {
322 self.selections.push(AxisSelection::Slice {
323 axis,
324 range,
325 step: 1,
326 });
327 self
328 }
329
330 pub fn axis_step(mut self, axis: usize, range: Range<usize>, step: usize) -> Self {
342 self.selections
343 .push(AxisSelection::Slice { axis, range, step });
344 self
345 }
346
347 pub fn take_axis(mut self, axis: usize, indices: &[usize]) -> Self {
359 self.selections.push(AxisSelection::Take {
360 axis,
361 indices: indices.to_vec(),
362 });
363 self
364 }
365
366 pub fn apply(self) -> Result<TracedTensor> {
385 let shape = concrete_shape_for_axis_slice(self.tensor, "slice_builder")?;
386 let mut seen = vec![false; shape.len()];
387 for selection in &self.selections {
388 let axis = match selection {
389 AxisSelection::Slice { axis, .. } | AxisSelection::Take { axis, .. } => *axis,
390 };
391 validate_axis_selection("slice_builder", shape.len(), &mut seen, axis)?;
392 }
393
394 let mut output = self.tensor.clone();
395 if let Some(config) = apply_slice_axis_config("slice_builder", &shape, &self.selections)? {
396 output = output.slice(config)?;
397 }
398 for selection in self.selections {
399 if let AxisSelection::Take { axis, indices } = selection {
400 output = output.take_axis(axis, &indices)?;
401 }
402 }
403 Ok(output)
404 }
405}
406
407impl TracedTensor {
408 pub fn slice_axis(&self, axis: usize, range: Range<usize>) -> Result<Self> {
426 self.slice_builder().axis(axis, range).apply()
427 }
428
429 pub fn slice_builder(&self) -> TracedSliceBuilder<'_> {
441 TracedSliceBuilder::new(self)
442 }
443
444 pub fn take_axis(&self, axis: usize, indices: &[usize]) -> Result<Self> {
462 let axis = isize::try_from(axis).map_err(|_| {
463 Error::validation(
464 "take_axis",
465 ErrorPhase::GraphBuild,
466 ValidationError::InvalidArgument {
467 argument: "axis",
468 message: format!("axis {axis} cannot be represented as isize"),
469 },
470 )
471 })?;
472 self.index_select(axis, indices)
473 }
474
475 pub fn index_select(&self, axis: isize, positions: &[usize]) -> Result<Self> {
512 let shape = try_concrete_shape(self).ok_or_else(|| {
513 Error::validation(
514 "index_select",
515 ErrorPhase::GraphBuild,
516 ValidationError::InvalidArgument {
517 argument: "shape",
518 message: "index_select requires a concrete shape hint".into(),
519 },
520 )
521 })?;
522 let (indices_tensor, config, out_shape) = index_select_config(&shape, axis, positions)?;
523 let indices = TracedTensor::from_tensor_concrete_shape(indices_tensor)?;
524 apply_binary_preserve_input_dtypes(
525 StdTensorOp::Gather(config),
526 self,
527 &indices,
528 out_shape.len(),
529 Some(out_shape.into_iter().map(SymDim::from).collect()),
530 self.dtype,
531 )
532 }
533
534 pub fn stack(tensors: &[&Self], dim: isize) -> Result<Self> {
568 let first = tensors.first().copied().ok_or_else(|| {
569 Error::validation(
570 "stack",
571 ErrorPhase::GraphBuild,
572 ValidationError::InvalidArgument {
573 argument: "tensors",
574 message: "stack requires at least one input".into(),
575 },
576 )
577 })?;
578 let first_shape = try_concrete_shape(first).ok_or_else(|| {
579 Error::validation(
580 "stack",
581 ErrorPhase::GraphBuild,
582 ValidationError::InvalidArgument {
583 argument: "shape",
584 message: "stack requires concrete shape hints".into(),
585 },
586 )
587 })?;
588 let mut shapes = Vec::with_capacity(tensors.len());
589 shapes.push(first_shape.as_slice());
590 let mut owned_shapes = Vec::with_capacity(tensors.len().saturating_sub(1));
591 for tensor in tensors.iter().copied().skip(1) {
592 owned_shapes.push(try_concrete_shape(tensor).ok_or_else(|| {
593 Error::validation(
594 "stack",
595 ErrorPhase::GraphBuild,
596 ValidationError::InvalidArgument {
597 argument: "shape",
598 message: "stack requires concrete shape hints".into(),
599 },
600 )
601 })?);
602 }
603 shapes.extend(owned_shapes.iter().map(Vec::as_slice));
604 validate_stack_shapes("stack", &shapes)?;
605
606 let axis = normalize_insert_axis("stack", dim, first.rank)?;
607 let mut expanded_shape = first_shape;
608 expanded_shape.insert(axis, 1);
609 let mut out_shape = expanded_shape.clone();
610 out_shape[axis] = tensors.len();
611 let expanded = tensors
612 .iter()
613 .map(|tensor| tensor.reshape(&expanded_shape))
614 .collect::<Result<Vec<_>>>()?;
615 let refs = expanded.iter().collect::<Vec<_>>();
616 apply_nary_concatenate(
617 &refs,
618 axis,
619 out_shape.into_iter().map(SymDim::from).collect(),
620 )
621 }
622
623 pub fn concatenate(tensors: &[&Self], axis: usize) -> Result<Self> {
631 let first = tensors.first().copied().ok_or_else(|| {
632 Error::validation(
633 "concatenate",
634 ErrorPhase::GraphBuild,
635 ValidationError::InvalidArgument {
636 argument: "tensors",
637 message: "concatenate requires at least one input".into(),
638 },
639 )
640 })?;
641 if axis >= first.rank {
642 return Err(Error::validation(
643 "concatenate",
644 ErrorPhase::GraphBuild,
645 ValidationError::AxisOutOfBounds {
646 axis,
647 rank: first.rank,
648 },
649 ));
650 }
651 for tensor in tensors.iter().copied().skip(1) {
652 if tensor.rank != first.rank {
653 return Err(Error::validation(
654 "concatenate",
655 ErrorPhase::GraphBuild,
656 ValidationError::RankMismatch {
657 expected: first.rank,
658 actual: tensor.rank,
659 },
660 ));
661 }
662 }
663
664 let op = StdTensorOp::Concatenate {
665 axis,
666 input_count: tensors.len(),
667 };
668 let (_, out_shape_hint) =
669 infer_traced_single_output_shape("TracedTensor::concatenate", &op, tensors)?;
670 let out_shape = out_shape_hint.ok_or_else(|| {
671 Error::Internal("concatenate shape inference returned no shape hint".into())
672 })?;
673 apply_nary_concatenate(tensors, axis, out_shape)
674 }
675}
676
677fn apply_nary_concatenate(
678 tensors: &[&TracedTensor],
679 axis: usize,
680 out_shape: Vec<SymDim>,
681) -> Result<TracedTensor> {
682 let out_dtype = promote_dtypes(tensors.iter().map(|tensor| tensor.dtype));
683 let tensors = tensors
684 .iter()
685 .map(|tensor| {
686 if tensor.dtype != out_dtype {
687 tensor.cast(out_dtype)
688 } else {
689 Ok((*tensor).clone())
690 }
691 })
692 .collect::<Result<Vec<_>>>()?;
693
694 let mut builder = GraphBuilder::new();
695 for tensor in &tensors {
696 builder.add_parent(tensor.graph.clone());
697 }
698 let input_refs = tensors
699 .iter()
700 .map(|tensor| ValueRef::External(tensor.graph.values()[tensor.val].key.clone()))
701 .collect::<Vec<_>>();
702 let outputs = builder.add_operation(
703 StdTensorOp::Concatenate {
704 axis,
705 input_count: tensors.len(),
706 },
707 input_refs,
708 OperationRole::Primary,
709 );
710 builder.set_outputs(outputs.clone());
711 let graph = Arc::new(builder.build());
712 let metadata_scope =
714 super::traced::register_metadata_or_runtime_state(register_scoped_value_metadata(
715 graph.values()[outputs[0]].key.clone(),
716 tensor_meta(out_dtype, out_shape.clone()),
717 ))?;
718
719 let inputs_map = merge_traced_inputs_map(tensors.iter());
720 let mut extra_roots = Vec::new();
721 let mut checkpoint_chain = None;
722 for tensor in &tensors {
723 extra_roots.extend(tensor.extra_roots.iter().cloned());
724 checkpoint_chain =
725 CheckpointNode::merge_chains(checkpoint_chain, tensor.checkpoint_chain.clone());
726 }
727 Ok(TracedTensor {
728 id: next_traced_id(),
729 rank: out_shape.len(),
730 dtype: out_dtype,
731 graph,
732 val: outputs[0],
733 data: None,
734 shape_hint: Some(out_shape),
735 inputs_map,
736 extra_roots,
737 checkpoint_chain,
738 metadata_scopes: MetadataScopeChain::with_new(
739 metadata_scope,
740 tensors.iter().map(|tensor| &tensor.metadata_scopes),
741 ),
742 constraint_scopes: ConstraintScopeChain::merge(
743 tensors.iter().map(|tensor| &tensor.constraint_scopes),
744 ),
745 })
746}
747
748#[cfg(test)]
749mod tests {
750 use std::ops::Range;
751
752 use tenferro_tensor::{DType, ErrorKind, ValidationError, ValidationKind};
753
754 use super::{
755 apply_slice_axis_config, concrete_shape_for_axis_slice, index_select_config,
756 normalize_existing_axis, normalize_insert_axis, validate_axis_selection,
757 validate_stack_shapes, AxisSelection,
758 };
759 use crate::TracedTensor;
760
761 #[test]
762 fn axis_normalization_handles_ranks_larger_than_isize_max() {
763 assert_eq!(normalize_existing_axis("test", 0, usize::MAX).unwrap(), 0);
764 assert_eq!(
765 normalize_existing_axis("test", -1, usize::MAX).unwrap(),
766 usize::MAX - 1
767 );
768 assert_eq!(
769 normalize_insert_axis("test", -1, usize::MAX - 1).unwrap(),
770 usize::MAX - 1
771 );
772 assert!(normalize_insert_axis("test", -1, usize::MAX).is_err());
773 }
774
775 #[test]
776 fn axis_normalization_reports_positive_and_negative_bounds() {
777 assert_eq!(normalize_existing_axis("test", 1, 3).unwrap(), 1);
778 assert_eq!(normalize_existing_axis("test", -1, 3).unwrap(), 2);
779 assert_eq!(normalize_insert_axis("test", 3, 3).unwrap(), 3);
780 assert_eq!(normalize_insert_axis("test", -1, 3).unwrap(), 3);
781
782 for error in [
783 normalize_existing_axis("test", 3, 3).unwrap_err(),
784 normalize_existing_axis("test", -4, 3).unwrap_err(),
785 normalize_insert_axis("test", 4, 3).unwrap_err(),
786 normalize_insert_axis("test", -5, 3).unwrap_err(),
787 ] {
788 assert_eq!(
789 error.kind(),
790 ErrorKind::Validation(ValidationKind::AxisOutOfBounds)
791 );
792 }
793 }
794
795 #[test]
796 fn index_select_config_validates_positions_and_builds_gather_metadata() {
797 let (indices, config, output_shape) = index_select_config(&[3, 4], -1, &[3, 1]).unwrap();
798 assert_eq!(indices.as_slice::<i64>().unwrap(), &[3, 1]);
799 assert_eq!(output_shape, vec![3, 2]);
800 assert_eq!(config.collapsed_slice_dims, vec![1]);
801 assert_eq!(config.offset_dims, vec![0]);
802 assert_eq!(config.start_index_map, vec![1]);
803 assert_eq!(config.slice_sizes, vec![3, 1]);
804
805 let error = index_select_config(&[3], 0, &[3]).unwrap_err();
806 assert!(matches!(
807 error,
808 crate::Error::Validation {
809 source: ValidationError::InvalidArgument {
810 argument: "positions",
811 ..
812 },
813 ..
814 }
815 ));
816 }
817
818 #[test]
819 fn stack_shape_and_selection_helpers_cover_validation_contracts() {
820 let first = [2usize, 3];
821 let same = [2usize, 3];
822 let different = [2usize, 4];
823 assert!(validate_stack_shapes("stack", &[&first, &same]).is_ok());
824 assert!(matches!(
825 validate_stack_shapes("stack", &[]).unwrap_err(),
826 crate::Error::Validation {
827 source: ValidationError::InvalidArgument {
828 argument: "tensors",
829 ..
830 },
831 ..
832 }
833 ));
834 assert!(matches!(
835 validate_stack_shapes("stack", &[&first, &different]).unwrap_err(),
836 crate::Error::Validation {
837 source: ValidationError::ShapeMismatch(_),
838 ..
839 }
840 ));
841
842 let mut seen = vec![false; 2];
843 validate_axis_selection("slice", 2, &mut seen, 1).unwrap();
844 assert!(matches!(
845 validate_axis_selection("slice", 2, &mut seen, 1).unwrap_err(),
846 crate::Error::Validation {
847 source: ValidationError::DuplicateAxis { axis: 1, .. },
848 ..
849 }
850 ));
851 assert!(matches!(
852 validate_axis_selection("slice", 2, &mut seen, 2).unwrap_err(),
853 crate::Error::Validation {
854 source: ValidationError::AxisOutOfBounds { axis: 2, rank: 2 },
855 ..
856 }
857 ));
858 }
859
860 #[test]
861 fn slice_config_distinguishes_take_only_from_ranges_and_rejects_bad_ranges() {
862 let take_only = [AxisSelection::Take {
863 axis: 0,
864 indices: vec![1],
865 }];
866 assert!(apply_slice_axis_config("slice", &[3], &take_only)
867 .unwrap()
868 .is_none());
869
870 let ranges = [AxisSelection::Slice {
871 axis: 0,
872 range: 1..3,
873 step: 2,
874 }];
875 assert_eq!(
876 apply_slice_axis_config("slice", &[4], &ranges)
877 .unwrap()
878 .unwrap()
879 .strides,
880 vec![2]
881 );
882
883 let zero_step = [AxisSelection::Slice {
884 axis: 0,
885 range: 0..1,
886 step: 0,
887 }];
888 assert!(matches!(
889 apply_slice_axis_config("slice", &[2], &zero_step).unwrap_err(),
890 crate::Error::Validation {
891 source: ValidationError::InvalidSliceStep { step: 0 },
892 ..
893 }
894 ));
895
896 let bad_bounds = [AxisSelection::Slice {
897 axis: 0,
898 range: Range { start: 2, end: 4 },
899 step: 1,
900 }];
901 assert!(matches!(
902 apply_slice_axis_config("slice", &[3], &bad_bounds).unwrap_err(),
903 crate::Error::Validation {
904 source: ValidationError::InvalidSliceBounds { axis_len: 3, .. },
905 ..
906 }
907 ));
908 }
909
910 #[test]
911 fn public_slice_stack_and_concatenate_paths_preserve_structured_errors() {
912 let x = TracedTensor::from_vec_col_major(
913 vec![4, 5],
914 (0..20).map(|value| value as f64).collect(),
915 )
916 .unwrap();
917 let sliced = x
918 .slice_builder()
919 .axis(0, 1..4)
920 .take_axis(1, &[4, 0])
921 .apply()
922 .unwrap();
923 assert_eq!(sliced.try_concrete_shape(), Some(vec![3, 2]));
924
925 assert!(matches!(
926 x.slice_builder()
927 .axis(0, 0..2)
928 .axis_step(0, 0..2, 1)
929 .apply(),
930 Err(crate::Error::Validation {
931 source: ValidationError::DuplicateAxis { .. },
932 ..
933 })
934 ));
935
936 let symbolic = TracedTensor::input_symbolic_shape(DType::F64, 2).unwrap();
937 assert!(matches!(
938 concrete_shape_for_axis_slice(&symbolic, "slice_builder"),
939 Err(crate::Error::Validation {
940 source: ValidationError::InvalidArgument {
941 argument: "shape",
942 ..
943 },
944 ..
945 })
946 ));
947 assert!(matches!(
948 symbolic.index_select(0, &[0]),
949 Err(crate::Error::Validation {
950 source: ValidationError::InvalidArgument {
951 argument: "shape",
952 ..
953 },
954 ..
955 })
956 ));
957
958 let a = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
959 let b = TracedTensor::from_vec_col_major(vec![2], vec![3.0_f64, 4.0]).unwrap();
960 let stacked = TracedTensor::stack(&[&a, &b], -1).unwrap();
961 assert_eq!(stacked.try_concrete_shape(), Some(vec![2, 2]));
962 assert!(matches!(
963 TracedTensor::stack(&[], 0),
964 Err(crate::Error::Validation {
965 source: ValidationError::InvalidArgument {
966 argument: "tensors",
967 ..
968 },
969 ..
970 })
971 ));
972 assert!(matches!(
973 TracedTensor::stack(&[&a, &x], 0),
974 Err(crate::Error::Validation {
975 source: ValidationError::ShapeMismatch(_),
976 ..
977 })
978 ));
979 assert!(matches!(
980 TracedTensor::stack(&[&a, &b], 2),
981 Err(crate::Error::Validation {
982 source: ValidationError::AxisOutOfBounds { .. },
983 ..
984 })
985 ));
986
987 let c = TracedTensor::from_vec_col_major(vec![2], vec![3.0_f32, 4.0]).unwrap();
988 let concatenated = TracedTensor::concatenate(&[&a, &c], 0).unwrap();
989 assert_eq!(concatenated.dtype, DType::F64);
990 assert_eq!(concatenated.try_concrete_shape(), Some(vec![4]));
991 assert!(matches!(
992 TracedTensor::concatenate(&[], 0),
993 Err(crate::Error::Validation {
994 source: ValidationError::InvalidArgument {
995 argument: "tensors",
996 ..
997 },
998 ..
999 })
1000 ));
1001 assert!(matches!(
1002 TracedTensor::concatenate(&[&a, &b], 1),
1003 Err(crate::Error::Validation {
1004 source: ValidationError::AxisOutOfBounds { .. },
1005 ..
1006 })
1007 ));
1008 let rank_one = TracedTensor::from_vec_col_major(vec![1, 2], vec![1.0_f64, 2.0]).unwrap();
1009 assert!(matches!(
1010 TracedTensor::concatenate(&[&a, &rank_one], 0),
1011 Err(crate::Error::Validation {
1012 source: ValidationError::RankMismatch { .. },
1013 ..
1014 })
1015 ));
1016 }
1017}