1use std::cell::RefCell;
23use std::cmp::Reverse;
24use std::collections::{HashMap, HashSet};
25use std::env;
26use std::time::{Duration, Instant};
27
28use anyhow::Result;
29use petgraph::algo::connected_components;
30use petgraph::prelude::*;
31use tenferro::TensorValue;
32use tenferro_einsum::{EagerEinsumExt, EinsumSubscripts};
33use tensor4all_tensorbackend::{
34 einsum_native_tensor_reads, einsum_native_tensors_owned, NativeTensorReadInput,
35};
36
37#[cfg(test)]
38use crate::defaults::DynId;
39use crate::defaults::IdxTensorError;
40use crate::defaults::{DynIndex, IdxTensor};
41
42use crate::index_like::IndexLike;
43#[derive(Debug, Clone, Hash, PartialEq, Eq)]
44struct ContractOperandSignature {
45 dims: Vec<usize>,
46 ids: Vec<usize>,
47 is_diag: bool,
48}
49
50#[derive(Debug, Clone, Hash, PartialEq, Eq)]
51struct ContractSignature {
52 operands: Vec<ContractOperandSignature>,
53 output_ids: Vec<usize>,
54 output_dims: Vec<usize>,
55}
56
57#[derive(Debug, Default, Clone)]
58struct ContractProfileEntry {
59 calls: usize,
60 total_time: Duration,
61}
62
63thread_local! {
64 static CONTRACT_PROFILE_STATE: RefCell<HashMap<ContractSignature, ContractProfileEntry>> =
65 RefCell::new(HashMap::new());
66}
67
68fn contract_profile_enabled() -> bool {
69 env::var("T4A_PROFILE_CONTRACT").is_ok()
70}
71
72fn record_contract_profile(signature: ContractSignature, elapsed: Duration) {
73 if !contract_profile_enabled() {
74 return;
75 }
76 CONTRACT_PROFILE_STATE.with(|state| {
77 let mut state = state.borrow_mut();
78 let entry = state.entry(signature).or_default();
79 entry.calls += 1;
80 entry.total_time += elapsed;
81 });
82}
83
84pub fn reset_contract_profile() {
86 CONTRACT_PROFILE_STATE.with(|state| state.borrow_mut().clear());
87}
88
89pub fn print_and_reset_contract_profile() {
91 if !contract_profile_enabled() {
92 return;
93 }
94 CONTRACT_PROFILE_STATE.with(|state| {
95 let mut entries: Vec<_> = state
96 .borrow()
97 .iter()
98 .map(|(k, v)| (k.clone(), v.clone()))
99 .collect();
100 state.borrow_mut().clear();
101 entries.sort_by_key(|(_, entry)| Reverse(entry.total_time));
102
103 eprintln!("=== contract Profile ===");
104 for (idx, (signature, entry)) in entries.into_iter().take(20).enumerate() {
105 let operands = signature
106 .operands
107 .iter()
108 .map(|operand| {
109 format!(
110 "dims={:?} ids={:?}{}",
111 operand.dims,
112 operand.ids,
113 if operand.is_diag { " diag" } else { "" }
114 )
115 })
116 .collect::<Vec<_>>()
117 .join(" ; ");
118 eprintln!(
119 "#{idx:02} calls={} total={:.3}s per_call={:.3}us output_dims={:?} output_ids={:?}",
120 entry.calls,
121 entry.total_time.as_secs_f64(),
122 entry.total_time.as_secs_f64() * 1e6 / entry.calls as f64,
123 signature.output_dims,
124 signature.output_ids,
125 );
126 eprintln!(" {operands}");
127 }
128 });
129}
130
131#[derive(Clone, Copy, Debug)]
152pub struct ContractionOptions<'a> {
153 pub retain_indices: &'a [DynIndex],
155}
156
157impl<'a> ContractionOptions<'a> {
158 pub fn new() -> Self {
160 Self {
161 retain_indices: &[],
162 }
163 }
164
165 pub fn with_retain_indices(mut self, retain_indices: &'a [DynIndex]) -> Self {
167 self.retain_indices = retain_indices;
168 self
169 }
170}
171
172impl Default for ContractionOptions<'_> {
173 fn default() -> Self {
174 Self::new()
175 }
176}
177
178#[derive(Clone)]
205pub struct PreparedContraction {
206 expected_indices: Vec<Vec<DynIndex>>,
207 expected_dims: Vec<Vec<usize>>,
208 expected_axis_classes: Vec<Vec<usize>>,
209 plan: ContractionPlan,
210 has_retained_indices: bool,
211}
212
213impl std::fmt::Debug for PreparedContraction {
214 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215 formatter
216 .debug_struct("PreparedContraction")
217 .field("operand_count", &self.expected_indices.len())
218 .field("result_rank", &self.plan.result_indices.len())
219 .field("has_retained_indices", &self.has_retained_indices)
220 .finish_non_exhaustive()
221 }
222}
223
224impl PreparedContraction {
225 pub fn new(
261 tensors: &[&IdxTensor],
262 options: ContractionOptions<'_>,
263 ) -> std::result::Result<Self, IdxTensorError> {
264 Self::new_impl(tensors, options).map_err(IdxTensorError::from)
265 }
266
267 fn new_impl(tensors: &[&IdxTensor], options: ContractionOptions<'_>) -> Result<Self> {
268 if tensors.is_empty() {
269 return Err(anyhow::anyhow!("No tensors to contract"));
270 }
271 validate_retained_indices_exist(tensors, options.retain_indices)?;
272 if tensors.len() > 1 {
273 let components =
274 find_tensor_connected_components_with_retained(tensors, options.retain_indices);
275 if components.len() > 1 {
276 return Err(anyhow::anyhow!(
277 "Disconnected tensor network: {} components found",
278 components.len()
279 ));
280 }
281 }
282 let plan = build_contraction_plan(tensors, options)?;
283 Ok(Self {
284 expected_indices: tensors
285 .iter()
286 .map(|tensor| tensor.indices().to_vec())
287 .collect(),
288 expected_dims: tensors.iter().map(|tensor| tensor.dims()).collect(),
289 expected_axis_classes: tensors
290 .iter()
291 .map(|tensor| tensor.axis_classes().to_vec())
292 .collect(),
293 plan,
294 has_retained_indices: !options.retain_indices.is_empty(),
295 })
296 }
297
298 pub fn execute(
333 &self,
334 tensors: &[&IdxTensor],
335 ) -> std::result::Result<IdxTensor, IdxTensorError> {
336 self.validate_operands(tensors)?;
337 self.execute_impl(tensors).map_err(IdxTensorError::from)
338 }
339
340 fn validate_operands(&self, tensors: &[&IdxTensor]) -> std::result::Result<(), IdxTensorError> {
341 if tensors.len() != self.expected_indices.len() {
342 return Err(IdxTensorError::ShapeMismatch {
343 operation: "prepared contraction",
344 expected: format!("{} operands", self.expected_indices.len()),
345 actual: format!("{} operands", tensors.len()),
346 });
347 }
348 for (operand, tensor) in tensors.iter().enumerate() {
349 let dims = tensor.dims();
350 if dims != self.expected_dims[operand] {
351 return Err(IdxTensorError::ShapeMismatch {
352 operation: "prepared contraction",
353 expected: format!(
354 "operand {operand} dimensions {:?}",
355 self.expected_dims[operand]
356 ),
357 actual: format!("operand {operand} dimensions {dims:?}"),
358 });
359 }
360 if tensor.indices() != self.expected_indices[operand] {
361 return Err(IdxTensorError::ShapeMismatch {
362 operation: "prepared contraction",
363 expected: format!(
364 "operand {operand} indices {:?}",
365 self.expected_indices[operand]
366 ),
367 actual: format!("operand {operand} indices {:?}", tensor.indices()),
368 });
369 }
370 if tensor.axis_classes() != self.expected_axis_classes[operand] {
371 return Err(IdxTensorError::ShapeMismatch {
372 operation: "prepared contraction",
373 expected: format!(
374 "operand {operand} axis classes {:?}",
375 self.expected_axis_classes[operand]
376 ),
377 actual: format!("operand {operand} axis classes {:?}", tensor.axis_classes()),
378 });
379 }
380 }
381 Ok(())
382 }
383
384 fn execute_impl(&self, tensors: &[&IdxTensor]) -> Result<IdxTensor> {
385 if tensors.len() == 1 {
386 return Ok((*tensors[0]).clone());
387 }
388 if tensors.len() == 2 && !self.has_retained_indices {
389 return tensors[0].try_contract_pairwise_default_with_options(
390 tensors[1],
391 PairwiseContractionOptions::new(),
392 );
393 }
394 let has_structured_storage = tensors
395 .iter()
396 .map(|tensor| has_dense_axis_classes(tensor).map(|dense| !dense))
397 .collect::<Result<Vec<_>>>()?
398 .into_iter()
399 .any(|structured| structured);
400 let has_grad = tensors.iter().any(|tensor| tensor.tracks_grad());
401 if has_structured_storage || has_grad {
402 return IdxTensor::contract_structured_payloads_nary(
403 tensors,
404 self.plan.result_indices.clone(),
405 self.plan.input_ids.clone(),
406 self.plan.output_ids.clone(),
407 );
408 }
409 execute_contraction_plan(tensors, &self.plan, self.has_retained_indices)
410 }
411}
412
413#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
445pub struct PairwiseContractionOptions {
446 pub lhs_conj: bool,
448 pub rhs_conj: bool,
450}
451
452impl PairwiseContractionOptions {
453 pub fn new() -> Self {
465 Self::default()
466 }
467
468 pub fn with_lhs_conj(mut self, lhs_conj: bool) -> Self {
480 self.lhs_conj = lhs_conj;
481 self
482 }
483
484 pub fn with_rhs_conj(mut self, rhs_conj: bool) -> Self {
496 self.rhs_conj = rhs_conj;
497 self
498 }
499
500 pub(crate) fn has_conj(self) -> bool {
501 self.lhs_conj || self.rhs_conj
502 }
503}
504
505pub fn contract(tensors: &[&IdxTensor]) -> std::result::Result<IdxTensor, IdxTensorError> {
521 contract_with_options(tensors, ContractionOptions::new())
522}
523
524pub fn contract_with_options(
532 tensors: &[&IdxTensor],
533 options: ContractionOptions<'_>,
534) -> std::result::Result<IdxTensor, IdxTensorError> {
535 contract_with_options_impl(tensors, options).map_err(IdxTensorError::from)
536}
537
538pub fn contract_owned(tensors: Vec<IdxTensor>) -> std::result::Result<IdxTensor, IdxTensorError> {
546 contract_owned_with_options(tensors, ContractionOptions::new())
547}
548
549pub fn contract_owned_with_options(
557 tensors: Vec<IdxTensor>,
558 options: ContractionOptions<'_>,
559) -> std::result::Result<IdxTensor, IdxTensorError> {
560 let tensor_refs = tensors.iter().collect::<Vec<_>>();
561 let components =
562 find_tensor_connected_components_with_retained(&tensor_refs, options.retain_indices);
563 if components.len() > 1 {
564 return Err(IdxTensorError::from(anyhow::anyhow!(
565 "Tensors form disconnected components; use explicit outer_product operations for an intentional disconnected product"
566 )));
567 }
568 drop(tensor_refs);
569 contract_owned_with_options_impl(tensors, options).map_err(IdxTensorError::from)
570}
571
572pub fn contract_pair(
584 lhs: &IdxTensor,
585 rhs: &IdxTensor,
586) -> std::result::Result<IdxTensor, IdxTensorError> {
587 lhs.try_contract_pairwise_default_with_options(rhs, PairwiseContractionOptions::new())
588 .map_err(IdxTensorError::from)
589}
590
591pub fn contract_pair_with_operand_options(
633 lhs: &IdxTensor,
634 rhs: &IdxTensor,
635 options: PairwiseContractionOptions,
636) -> std::result::Result<IdxTensor, IdxTensorError> {
637 lhs.try_contract_pairwise_default_with_options(rhs, options)
638 .map_err(IdxTensorError::from)
639}
640
641pub fn contract_pair_with_options(
649 lhs: &IdxTensor,
650 rhs: &IdxTensor,
651 options: ContractionOptions<'_>,
652) -> std::result::Result<IdxTensor, IdxTensorError> {
653 contract_with_options(&[lhs, rhs], options)
654}
655
656pub fn tensordot(
663 lhs: &IdxTensor,
664 rhs: &IdxTensor,
665 pairs: &[(DynIndex, DynIndex)],
666) -> std::result::Result<IdxTensor, IdxTensorError> {
667 lhs.try_tensordot_pairwise_explicit(rhs, pairs)
668 .map_err(IdxTensorError::from)
669}
670
671pub fn outer_product(
682 lhs: &IdxTensor,
683 rhs: &IdxTensor,
684) -> std::result::Result<IdxTensor, IdxTensorError> {
685 lhs.try_outer_product_pairwise(rhs)
686 .map_err(IdxTensorError::from)
687}
688
689fn contract_owned_with_options_impl(
698 tensors: Vec<IdxTensor>,
699 options: ContractionOptions<'_>,
700) -> Result<IdxTensor> {
701 match tensors.len() {
702 0 => Err(anyhow::anyhow!("No tensors to contract")),
703 _ => {
704 let tensor_refs = tensors.iter().collect::<Vec<_>>();
705 validate_retained_indices_exist(&tensor_refs, options.retain_indices)?;
706
707 if tensors.len() == 1 {
708 drop(tensor_refs);
709 let Some(tensor) = tensors.into_iter().next() else {
710 return Err(anyhow::anyhow!("No tensors to contract"));
711 };
712 return Ok(tensor);
713 }
714
715 let has_structured_storage = tensor_refs
716 .iter()
717 .map(|tensor| has_dense_axis_classes(tensor).map(|dense| !dense))
718 .collect::<Result<Vec<_>>>()?
719 .into_iter()
720 .any(|structured| structured);
721 let requires_borrowed_path =
722 tensor_refs.iter().any(|tensor| tensor.tracks_grad()) || has_structured_storage;
723 if requires_borrowed_path {
724 return contract_with_options(&tensor_refs, options).map_err(anyhow::Error::from);
725 }
726
727 let components = find_tensor_connected_components_with_retained(
728 &tensor_refs,
729 options.retain_indices,
730 );
731 if components.len() > 1 {
732 return Err(anyhow::anyhow!(
733 "Tensors form disconnected components; use explicit outer_product operations for an intentional disconnected product"
734 ));
735 }
736
737 let plan = build_contraction_plan(&tensor_refs, options)?;
738 drop(tensor_refs);
739 let native_operands = tensors
740 .into_iter()
741 .enumerate()
742 .map(|(tensor_idx, tensor)| {
743 Ok((
744 tensor.as_inner()?.duplicate_value()?,
745 plan.input_ids[tensor_idx].clone(),
746 ))
747 })
748 .collect::<Result<Vec<_>>>()?;
749 let result_native = einsum_native_tensors_owned(native_operands, &plan.output_ids)?;
750 IdxTensor::from_untracked_native_with_axis_classes(
751 plan.result_indices,
752 result_native,
753 plan.result_axis_classes,
754 )
755 }
756 }
757}
758
759fn has_dense_axis_classes(tensor: &IdxTensor) -> Result<bool> {
760 Ok(tensor
761 .axis_classes()
762 .iter()
763 .copied()
764 .eq(0..tensor.indices().len()))
765}
766
767fn contract_with_options_impl(
768 tensors: &[&IdxTensor],
769 options: ContractionOptions<'_>,
770) -> Result<IdxTensor> {
771 match tensors.len() {
772 0 => Err(anyhow::anyhow!("No tensors to contract")),
773 _ => {
774 validate_retained_indices_exist(tensors, options.retain_indices)?;
775 if tensors.len() == 1 {
776 return Ok((*tensors[0]).clone());
777 }
778
779 let components =
781 find_tensor_connected_components_with_retained(tensors, options.retain_indices);
782 if components.len() > 1 {
783 return Err(anyhow::anyhow!(
784 "Disconnected tensor network: {} components found",
785 components.len()
786 ));
787 }
788
789 if tensors.len() == 2 && options.retain_indices.is_empty() {
790 return tensors[0].try_contract_pairwise_default_with_options(
791 tensors[1],
792 PairwiseContractionOptions::new(),
793 );
794 }
795
796 let has_structured_storage = tensors
797 .iter()
798 .map(|tensor| has_dense_axis_classes(tensor).map(|dense| !dense))
799 .collect::<Result<Vec<_>>>()?
800 .into_iter()
801 .any(|structured| structured);
802 let has_grad = tensors.iter().any(|tensor| tensor.tracks_grad());
803 if has_structured_storage || has_grad {
804 let plan = build_contraction_plan(tensors, options)?;
805 return IdxTensor::contract_structured_payloads_nary(
806 tensors,
807 plan.result_indices,
808 plan.input_ids,
809 plan.output_ids,
810 );
811 }
812
813 contract_impl(tensors, options)
815 }
816 }
817}
818
819#[derive(Debug, Clone)]
828#[cfg(test)]
829pub(crate) struct AxisUnionFind {
830 parent: HashMap<DynId, DynId>,
832 rank: HashMap<DynId, usize>,
834}
835
836#[cfg(test)]
837impl AxisUnionFind {
838 pub fn new() -> Self {
840 Self {
841 parent: HashMap::new(),
842 rank: HashMap::new(),
843 }
844 }
845
846 pub fn make_set(&mut self, id: DynId) {
848 use std::collections::hash_map::Entry;
849 if let Entry::Vacant(e) = self.parent.entry(id) {
850 e.insert(id);
851 self.rank.insert(id, 0);
852 }
853 }
854
855 pub fn find(&mut self, id: DynId) -> DynId {
858 self.make_set(id);
859 if self.parent[&id] != id {
860 let root = self.find(self.parent[&id]);
861 self.parent.insert(id, root);
862 }
863 self.parent[&id]
864 }
865
866 pub fn union(&mut self, a: DynId, b: DynId) {
869 let root_a = self.find(a);
870 let root_b = self.find(b);
871
872 if root_a == root_b {
873 return;
874 }
875
876 let rank_a = self.rank[&root_a];
877 let rank_b = self.rank[&root_b];
878
879 if rank_a < rank_b {
880 self.parent.insert(root_a, root_b);
881 } else if rank_a > rank_b {
882 self.parent.insert(root_b, root_a);
883 } else {
884 self.parent.insert(root_b, root_a);
885 if let Some(rank) = self.rank.get_mut(&root_a) {
886 *rank += 1;
887 }
888 }
889 }
890
891 pub fn remap(&mut self, id: DynId) -> DynId {
893 self.find(id)
894 }
895
896 pub fn remap_ids(&mut self, ids: &[DynId]) -> Vec<DynId> {
898 ids.iter().map(|id| self.find(*id)).collect()
899 }
900}
901
902#[cfg(test)]
903impl Default for AxisUnionFind {
904 fn default() -> Self {
905 Self::new()
906 }
907}
908
909#[cfg(test)]
918pub(crate) fn remap_tensor_ids(tensors: &[&IdxTensor], uf: &mut AxisUnionFind) -> Vec<Vec<DynId>> {
919 tensors
920 .iter()
921 .map(|t| t.indices.iter().map(|idx| uf.find(*idx.id())).collect())
922 .collect()
923}
924
925#[cfg(test)]
927pub(crate) fn remap_output_ids(output: &[DynIndex], uf: &mut AxisUnionFind) -> Vec<DynId> {
928 output.iter().map(|idx| uf.find(*idx.id())).collect()
929}
930
931#[cfg(test)]
936pub(crate) fn collect_sizes(
937 tensors: &[&IdxTensor],
938 uf: &mut AxisUnionFind,
939) -> HashMap<DynId, usize> {
940 let mut sizes = HashMap::new();
941
942 for tensor in tensors {
943 let dims = tensor.dims();
944 for (idx, &dim) in tensor.indices.iter().zip(dims.iter()) {
945 let rep = uf.find(*idx.id());
946 sizes.entry(rep).or_insert(dim);
947 }
948 }
949
950 sizes
951}
952
953fn contract_impl(tensors: &[&IdxTensor], options: ContractionOptions<'_>) -> Result<IdxTensor> {
966 let plan = build_contraction_plan(tensors, options)?;
968
969 let mut sizes: HashMap<usize, usize> = HashMap::new();
974 for (tensor_idx, tensor) in tensors.iter().enumerate() {
975 let dims = tensor.dims();
976 for (pos, &dim) in dims.iter().enumerate() {
977 let internal_id = plan.input_ids[tensor_idx][pos];
978 match sizes.entry(internal_id) {
979 std::collections::hash_map::Entry::Vacant(entry) => {
980 entry.insert(dim);
981 }
982 std::collections::hash_map::Entry::Occupied(entry) => {
983 if *entry.get() != dim {
984 return Err(anyhow::anyhow!(
985 "Internal label shape mismatch: label {} has dimensions {} and {}",
986 internal_id,
987 entry.get(),
988 dim
989 ));
990 }
991 }
992 }
993 }
994 }
995
996 let profile_signature = contract_profile_enabled().then(|| ContractSignature {
997 operands: tensors
998 .iter()
999 .enumerate()
1000 .map(|(tensor_idx, tensor)| ContractOperandSignature {
1001 dims: tensor.dims().to_vec(),
1002 ids: plan.input_ids[tensor_idx].clone(),
1003 is_diag: tensor.is_diag(),
1004 })
1005 .collect(),
1006 output_ids: plan.output_ids.clone(),
1007 output_dims: plan.output_ids.iter().map(|id| sizes[id]).collect(),
1008 });
1009 let profile_started = contract_profile_enabled().then(Instant::now);
1010
1011 let result = execute_contraction_plan(tensors, &plan, !options.retain_indices.is_empty())?;
1012 if let (Some(signature), Some(started)) = (profile_signature, profile_started) {
1013 record_contract_profile(signature, started.elapsed());
1014 }
1015 Ok(result)
1016}
1017
1018#[cfg(feature = "tenferro-cuda")]
1025pub(crate) fn contract_pair_via_plan(
1026 lhs: &IdxTensor,
1027 rhs: &IdxTensor,
1028 options: PairwiseContractionOptions,
1029) -> Result<IdxTensor> {
1030 let lhs_owned;
1031 let rhs_owned;
1032 let (lhs, rhs) = match (options.lhs_conj, options.rhs_conj) {
1033 (false, false) => (lhs, rhs),
1034 (true, false) => {
1035 lhs_owned = lhs.conj();
1036 (&lhs_owned, rhs)
1037 }
1038 (false, true) => {
1039 rhs_owned = rhs.conj();
1040 (lhs, &rhs_owned)
1041 }
1042 (true, true) => {
1043 lhs_owned = lhs.conj();
1044 rhs_owned = rhs.conj();
1045 (&lhs_owned, &rhs_owned)
1046 }
1047 };
1048 let tensors = [lhs, rhs];
1049 let plan = build_contraction_plan(&tensors, ContractionOptions::new())?;
1050 execute_contraction_plan(&tensors, &plan, false)
1051}
1052
1053fn execute_contraction_plan(
1054 tensors: &[&IdxTensor],
1055 plan: &ContractionPlan,
1056 has_retained_indices: bool,
1057) -> Result<IdxTensor> {
1058 #[cfg(feature = "tenferro-cuda")]
1064 if !tensors.is_empty() && tensors.iter().all(|tensor| tensor.is_cuda_resident()) {
1065 let operands = tensors
1066 .iter()
1067 .map(|tensor| tensor.as_inner())
1068 .collect::<Result<Vec<_>>>()?;
1069 let subscripts = build_einsum_subscripts_from_usize_ids(&plan.input_ids, &plan.output_ids)?;
1070 let result = operands.as_slice().einsum_subscripts(&subscripts)?;
1071 return IdxTensor::from_inner_with_axis_classes(
1072 plan.result_indices.clone(),
1073 result,
1074 plan.result_axis_classes.clone(),
1075 );
1076 }
1077 let any_grad = tensors.iter().any(|tensor| tensor.tracks_grad());
1078 if any_grad {
1079 let first_dtype = tensors[0].as_inner()?.dtype();
1080 let same_dtype = tensors
1081 .iter()
1082 .map(|tensor| Ok(tensor.as_inner()?.dtype() == first_dtype))
1083 .collect::<Result<Vec<_>>>()?
1084 .into_iter()
1085 .all(|same| same);
1086 let has_non_dense_axis_classes = tensors.iter().any(|tensor| {
1087 tensor
1088 .axis_classes()
1089 .iter()
1090 .copied()
1091 .enumerate()
1092 .any(|(axis, class)| axis != class)
1093 });
1094
1095 if same_dtype && has_non_dense_axis_classes && !has_retained_indices {
1096 let mut iter = tensors.iter();
1099 let Some(first) = iter.next() else {
1100 return Err(anyhow::anyhow!("No tensors to contract"));
1101 };
1102 let mut result = (*first).clone();
1103 for tensor in iter {
1104 result = contract_pair(&result, tensor)?;
1105 }
1106 return Ok(result);
1107 }
1108
1109 let operands = tensors
1110 .iter()
1111 .map(|tensor| tensor.as_inner())
1112 .collect::<Result<Vec<_>>>()?;
1113 let subscripts = build_einsum_subscripts_from_usize_ids(&plan.input_ids, &plan.output_ids)?;
1114 let result = operands.as_slice().einsum_subscripts(&subscripts)?;
1115 return IdxTensor::from_inner_with_axis_classes(
1116 plan.result_indices.clone(),
1117 result,
1118 plan.result_axis_classes.clone(),
1119 );
1120 }
1121
1122 let native_operands = tensors
1123 .iter()
1124 .enumerate()
1125 .map(|(tensor_idx, tensor)| {
1126 Ok((
1127 NativeTensorReadInput::Borrowed(tensor.as_inner()?.tensor_read()),
1128 plan.input_ids[tensor_idx].as_slice(),
1129 ))
1130 })
1131 .collect::<Result<Vec<_>>>()?;
1132 let operand_refs = native_operands
1133 .iter()
1134 .map(|(tensor, ids)| (tensor, *ids))
1135 .collect::<Vec<_>>();
1136 let result_native = einsum_native_tensor_reads(&operand_refs, &plan.output_ids)?;
1137 let mut common: Option<std::sync::Arc<tenferro_ad::EagerRuntime>> = None;
1142 let mut mixed = false;
1143 for tensor in tensors {
1144 if let Some(runtime) = tensor.eager_runtime() {
1145 match &common {
1146 None => common = Some(runtime),
1147 Some(first) => {
1148 if first.id() != runtime.id() {
1149 mixed = true;
1150 }
1151 }
1152 }
1153 }
1154 }
1155 match (common, mixed) {
1156 (Some(runtime), false) => {
1157 let inner = tenferro_ad::extension::adopt_untracked_eager_value(
1158 runtime,
1159 TensorValue::from_tensor(result_native),
1160 )?;
1161 IdxTensor::from_inner_with_axis_classes(
1162 plan.result_indices.clone(),
1163 inner,
1164 plan.result_axis_classes.clone(),
1165 )
1166 }
1167 _ => IdxTensor::from_untracked_native_with_axis_classes(
1168 plan.result_indices.clone(),
1169 result_native,
1170 plan.result_axis_classes.clone(),
1171 ),
1172 }
1173}
1174
1175fn build_einsum_subscripts_from_usize_ids(
1176 input_ids: &[Vec<usize>],
1177 output_ids: &[usize],
1178) -> Result<EinsumSubscripts> {
1179 let inputs = input_ids
1180 .iter()
1181 .map(|ids| {
1182 ids.iter()
1183 .map(|&id| {
1184 u32::try_from(id)
1185 .map_err(|_| anyhow::anyhow!("einsum label {id} exceeds u32 range"))
1186 })
1187 .collect::<Result<Vec<_>>>()
1188 })
1189 .collect::<Result<Vec<_>>>()?;
1190 let output = output_ids
1191 .iter()
1192 .map(|&id| {
1193 u32::try_from(id).map_err(|_| anyhow::anyhow!("einsum label {id} exceeds u32 range"))
1194 })
1195 .collect::<Result<Vec<_>>>()?;
1196 let input_refs = inputs.iter().map(Vec::as_slice).collect::<Vec<_>>();
1197 Ok(EinsumSubscripts::new(&input_refs, &output))
1198}
1199
1200#[derive(Debug, Clone)]
1202struct ContractionPlan {
1203 input_ids: Vec<Vec<usize>>,
1204 output_ids: Vec<usize>,
1205 result_indices: Vec<DynIndex>,
1206 result_axis_classes: Vec<usize>,
1207}
1208
1209fn build_contraction_plan(
1210 tensors: &[&IdxTensor],
1211 options: ContractionOptions<'_>,
1212) -> Result<ContractionPlan> {
1213 let retained_indices: HashSet<DynIndex> = options.retain_indices.iter().cloned().collect();
1214 let (input_ids, internal_id_to_original) = build_internal_ids(tensors, &retained_indices)?;
1215
1216 let mut counts: HashMap<usize, usize> = HashMap::new();
1217 for ids in &input_ids {
1218 for &internal_id in ids {
1219 *counts.entry(internal_id).or_insert(0) += 1;
1220 }
1221 }
1222 let mut output_ids = Vec::new();
1223 let mut seen_output = HashSet::new();
1224 let mut found_retained = HashSet::new();
1225
1226 for (tensor_idx, tensor) in tensors.iter().enumerate() {
1227 for (axis, idx) in tensor.indices.iter().enumerate() {
1228 let internal_id = input_ids[tensor_idx][axis];
1229 let should_output = counts[&internal_id] == 1 || retained_indices.contains(idx);
1230 if should_output && seen_output.insert(internal_id) {
1231 output_ids.push(internal_id);
1232 }
1233 if retained_indices.contains(idx) {
1234 found_retained.insert(idx.clone());
1235 }
1236 }
1237 }
1238
1239 for retained in retained_indices {
1240 if !found_retained.contains(&retained) {
1241 return Err(anyhow::anyhow!(
1242 "Retained index {:?} does not appear in the input tensors",
1243 retained
1244 ));
1245 }
1246 }
1247
1248 let result_indices: Vec<DynIndex> = output_ids
1249 .iter()
1250 .map(|&internal_id| {
1251 let (tensor_idx, pos) = internal_id_to_original[&internal_id];
1252 tensors[tensor_idx].indices[pos].clone()
1253 })
1254 .collect();
1255 validate_unique_output_indices(&result_indices)?;
1256 let result_axis_classes =
1257 output_axis_classes(tensors, &input_ids, &output_ids, &internal_id_to_original)?;
1258
1259 Ok(ContractionPlan {
1260 input_ids,
1261 output_ids,
1262 result_indices,
1263 result_axis_classes,
1264 })
1265}
1266
1267fn validate_retained_indices_exist(
1268 tensors: &[&IdxTensor],
1269 retain_indices: &[DynIndex],
1270) -> Result<()> {
1271 for retain in retain_indices {
1272 let found = tensors
1273 .iter()
1274 .any(|tensor| tensor.indices().iter().any(|idx| idx == retain));
1275 if !found {
1276 return Err(anyhow::anyhow!(
1277 "Retained index {:?} does not appear in the input tensors",
1278 retain
1279 ));
1280 }
1281 }
1282 Ok(())
1283}
1284
1285fn validate_unique_output_indices(indices: &[DynIndex]) -> Result<()> {
1286 let mut seen = HashSet::new();
1287 for idx in indices {
1288 if !seen.insert(idx.clone()) {
1289 return Err(anyhow::anyhow!(
1290 "Contraction result would contain duplicate output indices"
1291 ));
1292 }
1293 }
1294 Ok(())
1295}
1296
1297fn output_axis_classes(
1298 tensors: &[&IdxTensor],
1299 ixs: &[Vec<usize>],
1300 output: &[usize],
1301 internal_id_to_original: &HashMap<usize, (usize, usize)>,
1302) -> Result<Vec<usize>> {
1303 fn find(parent: &mut [usize], value: usize) -> usize {
1304 if parent[value] != value {
1305 parent[value] = find(parent, parent[value]);
1306 }
1307 parent[value]
1308 }
1309
1310 fn union(parent: &mut [usize], lhs: usize, rhs: usize) {
1311 let lhs_root = find(parent, lhs);
1312 let rhs_root = find(parent, rhs);
1313 if lhs_root != rhs_root {
1314 parent[rhs_root] = lhs_root;
1315 }
1316 }
1317
1318 let mut class_offsets = Vec::with_capacity(tensors.len());
1319 let mut next_node = 0usize;
1320 for tensor in tensors {
1321 class_offsets.push(next_node);
1322 let payload_rank = tensor
1323 .axis_classes()
1324 .iter()
1325 .copied()
1326 .max()
1327 .map(|value| value + 1)
1328 .unwrap_or(0);
1329 next_node += payload_rank;
1330 }
1331 let mut parent: Vec<usize> = (0..next_node).collect();
1332 let mut axes_by_internal_id: HashMap<usize, Vec<usize>> = HashMap::new();
1333
1334 for (tensor_idx, tensor) in tensors.iter().enumerate() {
1335 for (axis, &internal_id) in ixs[tensor_idx].iter().enumerate() {
1336 let class_id = tensor.axis_classes()[axis];
1337 let node = class_offsets[tensor_idx] + class_id;
1338 axes_by_internal_id
1339 .entry(internal_id)
1340 .or_default()
1341 .push(node);
1342 }
1343 }
1344
1345 for nodes in axes_by_internal_id.values() {
1346 if let Some((&first, rest)) = nodes.split_first() {
1347 for &node in rest {
1348 union(&mut parent, first, node);
1349 }
1350 }
1351 }
1352
1353 let mut root_to_class = HashMap::new();
1354 let mut next_class = 0usize;
1355 output
1356 .iter()
1357 .map(|internal_id| {
1358 let (tensor_idx, axis) = internal_id_to_original[internal_id];
1359 let class_id = tensors[tensor_idx].axis_classes()[axis];
1360 let node = class_offsets[tensor_idx] + class_id;
1361 let root = find(&mut parent, node);
1362 Ok(*root_to_class.entry(root).or_insert_with(|| {
1363 let class = next_class;
1364 next_class += 1;
1365 class
1366 }))
1367 })
1368 .collect::<Result<Vec<_>>>()
1369}
1370
1371#[allow(clippy::type_complexity)]
1379fn build_internal_ids(
1380 tensors: &[&IdxTensor],
1381 retained_indices: &HashSet<DynIndex>,
1382) -> Result<(Vec<Vec<usize>>, HashMap<usize, (usize, usize)>)> {
1383 let mut next_id = 0usize;
1384 let mut index_to_internal: HashMap<DynIndex, usize> = HashMap::new();
1385 let mut retained_index_to_internal: HashMap<DynIndex, usize> = HashMap::new();
1386 let mut assigned: HashMap<(usize, usize), usize> = HashMap::new();
1387 let mut internal_id_to_original: HashMap<usize, (usize, usize)> = HashMap::new();
1388
1389 for ti in 0..tensors.len() {
1390 for tj in (ti + 1)..tensors.len() {
1391 for (pi, idx_i) in tensors[ti].indices.iter().enumerate() {
1392 for (pj, idx_j) in tensors[tj].indices.iter().enumerate() {
1393 if idx_i.is_contractable(idx_j) {
1394 let key_i = (ti, pi);
1395 let key_j = (tj, pj);
1396
1397 match (assigned.get(&key_i).copied(), assigned.get(&key_j).copied()) {
1398 (None, None) => {
1399 let internal_id = if let Some(&id) = index_to_internal.get(idx_i) {
1400 id
1401 } else {
1402 let id = next_id;
1403 next_id += 1;
1404 index_to_internal.insert(idx_i.clone(), id);
1405 internal_id_to_original.insert(id, key_i);
1406 id
1407 };
1408 assigned.insert(key_i, internal_id);
1409 assigned.insert(key_j, internal_id);
1410 if idx_i != idx_j {
1411 index_to_internal.insert(idx_j.clone(), internal_id);
1412 }
1413 }
1414 (Some(id), None) => {
1415 assigned.insert(key_j, id);
1416 index_to_internal.insert(idx_j.clone(), id);
1417 }
1418 (None, Some(id)) => {
1419 assigned.insert(key_i, id);
1420 index_to_internal.insert(idx_i.clone(), id);
1421 }
1422 (Some(_id_i), Some(_id_j)) => {
1423 }
1425 }
1426 }
1427 }
1428 }
1429 }
1430 }
1431
1432 for (tensor_idx, tensor) in tensors.iter().enumerate() {
1434 for (pos, idx) in tensor.indices.iter().enumerate() {
1435 let key = (tensor_idx, pos);
1436 if let std::collections::hash_map::Entry::Vacant(e) = assigned.entry(key) {
1437 let internal_id = if retained_indices.contains(idx) {
1438 if let Some(&id) = retained_index_to_internal.get(idx) {
1439 id
1440 } else {
1441 let id = next_id;
1442 next_id += 1;
1443 retained_index_to_internal.insert(idx.clone(), id);
1444 internal_id_to_original.insert(id, key);
1445 id
1446 }
1447 } else {
1448 let id = next_id;
1449 next_id += 1;
1450 internal_id_to_original.insert(id, key);
1451 id
1452 };
1453 e.insert(internal_id);
1454 }
1455 }
1456 }
1457
1458 let ixs: Vec<Vec<usize>> = tensors
1460 .iter()
1461 .enumerate()
1462 .map(|(tensor_idx, tensor)| {
1463 (0..tensor.indices.len())
1464 .map(|pos| assigned[&(tensor_idx, pos)])
1465 .collect()
1466 })
1467 .collect();
1468
1469 Ok((ixs, internal_id_to_original))
1470}
1471
1472fn has_contractable_indices(a: &IdxTensor, b: &IdxTensor) -> bool {
1478 a.indices
1479 .iter()
1480 .any(|idx_a| b.indices.iter().any(|idx_b| idx_a.is_contractable(idx_b)))
1481}
1482
1483#[allow(dead_code)]
1487fn find_tensor_connected_components(tensors: &[&IdxTensor]) -> Vec<Vec<usize>> {
1488 find_tensor_connected_components_with_retained(tensors, &[])
1489}
1490
1491fn find_tensor_connected_components_with_retained(
1492 tensors: &[&IdxTensor],
1493 retain_indices: &[DynIndex],
1494) -> Vec<Vec<usize>> {
1495 let n = tensors.len();
1496 if n == 0 {
1497 return vec![];
1498 }
1499 if n == 1 {
1500 return vec![vec![0]];
1501 }
1502
1503 let mut graph = UnGraph::<(), ()>::new_undirected();
1505 let nodes: Vec<_> = (0..n).map(|_| graph.add_node(())).collect();
1506
1507 for i in 0..n {
1508 for j in (i + 1)..n {
1509 if has_contractable_indices(tensors[i], tensors[j]) {
1510 graph.add_edge(nodes[i], nodes[j], ());
1511 }
1512 }
1513 }
1514
1515 if !retain_indices.is_empty() {
1516 for i in 0..n {
1517 for j in (i + 1)..n {
1518 if shares_retained_index(tensors[i], tensors[j], retain_indices) {
1519 graph.add_edge(nodes[i], nodes[j], ());
1520 }
1521 }
1522 }
1523 }
1524
1525 let num_components = connected_components(&graph);
1527
1528 if num_components == 1 {
1529 return vec![(0..n).collect()];
1530 }
1531
1532 use petgraph::visit::Dfs;
1534 let mut visited = vec![false; n];
1535 let mut components = Vec::new();
1536
1537 for start in 0..n {
1538 if !visited[start] {
1539 let mut component = Vec::new();
1540 let mut dfs = Dfs::new(&graph, nodes[start]);
1541 while let Some(node) = dfs.next(&graph) {
1542 let idx = node.index();
1543 if !visited[idx] {
1544 visited[idx] = true;
1545 component.push(idx);
1546 }
1547 }
1548 component.sort();
1549 components.push(component);
1550 }
1551 }
1552
1553 components.sort_by_key(|c| c[0]);
1554 components
1555}
1556
1557fn shares_retained_index(a: &IdxTensor, b: &IdxTensor, retain_indices: &[DynIndex]) -> bool {
1558 retain_indices.iter().any(|retain| {
1559 a.indices().iter().any(|idx_a| idx_a == retain)
1560 && b.indices().iter().any(|idx_b| idx_b == retain)
1561 })
1562}
1563
1564#[cfg(test)]
1565mod tests;