1use std::cell::RefCell;
22use std::cmp::Reverse;
23use std::collections::{HashMap, HashSet};
24use std::env;
25use std::time::{Duration, Instant};
26
27use anyhow::Result;
28use petgraph::algo::connected_components;
29use petgraph::prelude::*;
30use tenferro_einsum::{EagerEinsumExt, EinsumSubscripts};
31use tensor4all_tensorbackend::{
32 einsum_native_tensor_reads, einsum_native_tensors_owned, NativeTensorReadInput,
33};
34
35#[cfg(test)]
36use crate::defaults::DynId;
37use crate::defaults::IdxTensorError;
38use crate::defaults::{DynIndex, IdxTensor};
39
40use crate::index_like::IndexLike;
41#[derive(Debug, Clone, Hash, PartialEq, Eq)]
42struct ContractOperandSignature {
43 dims: Vec<usize>,
44 ids: Vec<usize>,
45 is_diag: bool,
46}
47
48#[derive(Debug, Clone, Hash, PartialEq, Eq)]
49struct ContractSignature {
50 operands: Vec<ContractOperandSignature>,
51 output_ids: Vec<usize>,
52 output_dims: Vec<usize>,
53}
54
55#[derive(Debug, Default, Clone)]
56struct ContractProfileEntry {
57 calls: usize,
58 total_time: Duration,
59}
60
61thread_local! {
62 static CONTRACT_PROFILE_STATE: RefCell<HashMap<ContractSignature, ContractProfileEntry>> =
63 RefCell::new(HashMap::new());
64}
65
66fn contract_profile_enabled() -> bool {
67 env::var("T4A_PROFILE_CONTRACT").is_ok()
68}
69
70fn record_contract_profile(signature: ContractSignature, elapsed: Duration) {
71 if !contract_profile_enabled() {
72 return;
73 }
74 CONTRACT_PROFILE_STATE.with(|state| {
75 let mut state = state.borrow_mut();
76 let entry = state.entry(signature).or_default();
77 entry.calls += 1;
78 entry.total_time += elapsed;
79 });
80}
81
82pub fn reset_contract_profile() {
84 CONTRACT_PROFILE_STATE.with(|state| state.borrow_mut().clear());
85}
86
87pub fn print_and_reset_contract_profile() {
89 if !contract_profile_enabled() {
90 return;
91 }
92 CONTRACT_PROFILE_STATE.with(|state| {
93 let mut entries: Vec<_> = state
94 .borrow()
95 .iter()
96 .map(|(k, v)| (k.clone(), v.clone()))
97 .collect();
98 state.borrow_mut().clear();
99 entries.sort_by_key(|(_, entry)| Reverse(entry.total_time));
100
101 eprintln!("=== contract Profile ===");
102 for (idx, (signature, entry)) in entries.into_iter().take(20).enumerate() {
103 let operands = signature
104 .operands
105 .iter()
106 .map(|operand| {
107 format!(
108 "dims={:?} ids={:?}{}",
109 operand.dims,
110 operand.ids,
111 if operand.is_diag { " diag" } else { "" }
112 )
113 })
114 .collect::<Vec<_>>()
115 .join(" ; ");
116 eprintln!(
117 "#{idx:02} calls={} total={:.3}s per_call={:.3}us output_dims={:?} output_ids={:?}",
118 entry.calls,
119 entry.total_time.as_secs_f64(),
120 entry.total_time.as_secs_f64() * 1e6 / entry.calls as f64,
121 signature.output_dims,
122 signature.output_ids,
123 );
124 eprintln!(" {operands}");
125 }
126 });
127}
128
129#[derive(Clone, Copy, Debug)]
150pub struct ContractionOptions<'a> {
151 pub retain_indices: &'a [DynIndex],
153}
154
155impl<'a> ContractionOptions<'a> {
156 pub fn new() -> Self {
158 Self {
159 retain_indices: &[],
160 }
161 }
162
163 pub fn with_retain_indices(mut self, retain_indices: &'a [DynIndex]) -> Self {
165 self.retain_indices = retain_indices;
166 self
167 }
168}
169
170impl Default for ContractionOptions<'_> {
171 fn default() -> Self {
172 Self::new()
173 }
174}
175
176#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
208pub struct PairwiseContractionOptions {
209 pub lhs_conj: bool,
211 pub rhs_conj: bool,
213}
214
215impl PairwiseContractionOptions {
216 pub fn new() -> Self {
228 Self::default()
229 }
230
231 pub fn with_lhs_conj(mut self, lhs_conj: bool) -> Self {
243 self.lhs_conj = lhs_conj;
244 self
245 }
246
247 pub fn with_rhs_conj(mut self, rhs_conj: bool) -> Self {
259 self.rhs_conj = rhs_conj;
260 self
261 }
262
263 pub(crate) fn has_conj(self) -> bool {
264 self.lhs_conj || self.rhs_conj
265 }
266}
267
268pub fn contract(tensors: &[&IdxTensor]) -> std::result::Result<IdxTensor, IdxTensorError> {
284 contract_with_options(tensors, ContractionOptions::new())
285}
286
287pub fn contract_with_options(
295 tensors: &[&IdxTensor],
296 options: ContractionOptions<'_>,
297) -> std::result::Result<IdxTensor, IdxTensorError> {
298 contract_with_options_impl(tensors, options).map_err(IdxTensorError::from)
299}
300
301pub fn contract_owned(tensors: Vec<IdxTensor>) -> std::result::Result<IdxTensor, IdxTensorError> {
309 contract_owned_with_options(tensors, ContractionOptions::new())
310}
311
312pub fn contract_owned_with_options(
320 tensors: Vec<IdxTensor>,
321 options: ContractionOptions<'_>,
322) -> std::result::Result<IdxTensor, IdxTensorError> {
323 let tensor_refs = tensors.iter().collect::<Vec<_>>();
324 let components =
325 find_tensor_connected_components_with_retained(&tensor_refs, options.retain_indices);
326 if components.len() > 1 {
327 return Err(IdxTensorError::from(anyhow::anyhow!(
328 "Tensors form disconnected components; use explicit outer_product operations for an intentional disconnected product"
329 )));
330 }
331 drop(tensor_refs);
332 contract_owned_with_options_impl(tensors, options).map_err(IdxTensorError::from)
333}
334
335pub fn contract_pair(
347 lhs: &IdxTensor,
348 rhs: &IdxTensor,
349) -> std::result::Result<IdxTensor, IdxTensorError> {
350 lhs.try_contract_pairwise_default_with_options(rhs, PairwiseContractionOptions::new())
351 .map_err(IdxTensorError::from)
352}
353
354pub fn contract_pair_with_operand_options(
396 lhs: &IdxTensor,
397 rhs: &IdxTensor,
398 options: PairwiseContractionOptions,
399) -> std::result::Result<IdxTensor, IdxTensorError> {
400 lhs.try_contract_pairwise_default_with_options(rhs, options)
401 .map_err(IdxTensorError::from)
402}
403
404pub fn contract_pair_with_options(
412 lhs: &IdxTensor,
413 rhs: &IdxTensor,
414 options: ContractionOptions<'_>,
415) -> std::result::Result<IdxTensor, IdxTensorError> {
416 contract_with_options(&[lhs, rhs], options)
417}
418
419pub fn tensordot(
426 lhs: &IdxTensor,
427 rhs: &IdxTensor,
428 pairs: &[(DynIndex, DynIndex)],
429) -> std::result::Result<IdxTensor, IdxTensorError> {
430 lhs.try_tensordot_pairwise_explicit(rhs, pairs)
431 .map_err(IdxTensorError::from)
432}
433
434pub fn outer_product(
445 lhs: &IdxTensor,
446 rhs: &IdxTensor,
447) -> std::result::Result<IdxTensor, IdxTensorError> {
448 lhs.try_outer_product_pairwise(rhs)
449 .map_err(IdxTensorError::from)
450}
451
452fn contract_owned_with_options_impl(
461 tensors: Vec<IdxTensor>,
462 options: ContractionOptions<'_>,
463) -> Result<IdxTensor> {
464 match tensors.len() {
465 0 => Err(anyhow::anyhow!("No tensors to contract")),
466 _ => {
467 let tensor_refs = tensors.iter().collect::<Vec<_>>();
468 validate_retained_indices_exist(&tensor_refs, options.retain_indices)?;
469
470 if tensors.len() == 1 {
471 drop(tensor_refs);
472 let Some(tensor) = tensors.into_iter().next() else {
473 return Err(anyhow::anyhow!("No tensors to contract"));
474 };
475 return Ok(tensor);
476 }
477
478 let has_structured_storage = tensor_refs
479 .iter()
480 .map(|tensor| has_dense_axis_classes(tensor).map(|dense| !dense))
481 .collect::<Result<Vec<_>>>()?
482 .into_iter()
483 .any(|structured| structured);
484 let requires_borrowed_path =
485 tensor_refs.iter().any(|tensor| tensor.tracks_grad()) || has_structured_storage;
486 if requires_borrowed_path {
487 return contract_with_options(&tensor_refs, options).map_err(anyhow::Error::from);
488 }
489
490 let components = find_tensor_connected_components_with_retained(
491 &tensor_refs,
492 options.retain_indices,
493 );
494 if components.len() > 1 {
495 return Err(anyhow::anyhow!(
496 "Tensors form disconnected components; use explicit outer_product operations for an intentional disconnected product"
497 ));
498 }
499
500 let plan = build_contraction_plan(&tensor_refs, options)?;
501 drop(tensor_refs);
502 let native_operands = tensors
503 .into_iter()
504 .enumerate()
505 .map(|(tensor_idx, tensor)| {
506 Ok((
507 tensor.as_inner()?.duplicate_value()?,
508 plan.input_ids[tensor_idx].clone(),
509 ))
510 })
511 .collect::<Result<Vec<_>>>()?;
512 let result_native = einsum_native_tensors_owned(native_operands, &plan.output_ids)?;
513 IdxTensor::from_native_with_axis_classes(
514 plan.result_indices,
515 result_native,
516 plan.result_axis_classes,
517 )
518 }
519 }
520}
521
522fn has_dense_axis_classes(tensor: &IdxTensor) -> Result<bool> {
523 Ok(tensor
524 .axis_classes()
525 .iter()
526 .copied()
527 .eq(0..tensor.indices().len()))
528}
529
530fn contract_with_options_impl(
531 tensors: &[&IdxTensor],
532 options: ContractionOptions<'_>,
533) -> Result<IdxTensor> {
534 match tensors.len() {
535 0 => Err(anyhow::anyhow!("No tensors to contract")),
536 _ => {
537 validate_retained_indices_exist(tensors, options.retain_indices)?;
538 if tensors.len() == 1 {
539 return Ok((*tensors[0]).clone());
540 }
541
542 let components =
544 find_tensor_connected_components_with_retained(tensors, options.retain_indices);
545 if components.len() > 1 {
546 return Err(anyhow::anyhow!(
547 "Disconnected tensor network: {} components found",
548 components.len()
549 ));
550 }
551
552 let has_structured_storage = tensors
553 .iter()
554 .map(|tensor| has_dense_axis_classes(tensor).map(|dense| !dense))
555 .collect::<Result<Vec<_>>>()?
556 .into_iter()
557 .any(|structured| structured);
558 let has_grad = tensors.iter().any(|tensor| tensor.tracks_grad());
559 if has_structured_storage || has_grad {
560 let plan = build_contraction_plan(tensors, options)?;
561 return IdxTensor::contract_structured_payloads_nary(
562 tensors,
563 plan.result_indices,
564 plan.input_ids,
565 plan.output_ids,
566 );
567 }
568
569 contract_impl(tensors, options)
571 }
572 }
573}
574
575#[derive(Debug, Clone)]
584#[cfg(test)]
585pub(crate) struct AxisUnionFind {
586 parent: HashMap<DynId, DynId>,
588 rank: HashMap<DynId, usize>,
590}
591
592#[cfg(test)]
593impl AxisUnionFind {
594 pub fn new() -> Self {
596 Self {
597 parent: HashMap::new(),
598 rank: HashMap::new(),
599 }
600 }
601
602 pub fn make_set(&mut self, id: DynId) {
604 use std::collections::hash_map::Entry;
605 if let Entry::Vacant(e) = self.parent.entry(id) {
606 e.insert(id);
607 self.rank.insert(id, 0);
608 }
609 }
610
611 pub fn find(&mut self, id: DynId) -> DynId {
614 self.make_set(id);
615 if self.parent[&id] != id {
616 let root = self.find(self.parent[&id]);
617 self.parent.insert(id, root);
618 }
619 self.parent[&id]
620 }
621
622 pub fn union(&mut self, a: DynId, b: DynId) {
625 let root_a = self.find(a);
626 let root_b = self.find(b);
627
628 if root_a == root_b {
629 return;
630 }
631
632 let rank_a = self.rank[&root_a];
633 let rank_b = self.rank[&root_b];
634
635 if rank_a < rank_b {
636 self.parent.insert(root_a, root_b);
637 } else if rank_a > rank_b {
638 self.parent.insert(root_b, root_a);
639 } else {
640 self.parent.insert(root_b, root_a);
641 if let Some(rank) = self.rank.get_mut(&root_a) {
642 *rank += 1;
643 }
644 }
645 }
646
647 pub fn remap(&mut self, id: DynId) -> DynId {
649 self.find(id)
650 }
651
652 pub fn remap_ids(&mut self, ids: &[DynId]) -> Vec<DynId> {
654 ids.iter().map(|id| self.find(*id)).collect()
655 }
656}
657
658#[cfg(test)]
659impl Default for AxisUnionFind {
660 fn default() -> Self {
661 Self::new()
662 }
663}
664
665#[cfg(test)]
674pub(crate) fn remap_tensor_ids(tensors: &[&IdxTensor], uf: &mut AxisUnionFind) -> Vec<Vec<DynId>> {
675 tensors
676 .iter()
677 .map(|t| t.indices.iter().map(|idx| uf.find(*idx.id())).collect())
678 .collect()
679}
680
681#[cfg(test)]
683pub(crate) fn remap_output_ids(output: &[DynIndex], uf: &mut AxisUnionFind) -> Vec<DynId> {
684 output.iter().map(|idx| uf.find(*idx.id())).collect()
685}
686
687#[cfg(test)]
692pub(crate) fn collect_sizes(
693 tensors: &[&IdxTensor],
694 uf: &mut AxisUnionFind,
695) -> HashMap<DynId, usize> {
696 let mut sizes = HashMap::new();
697
698 for tensor in tensors {
699 let dims = tensor.dims();
700 for (idx, &dim) in tensor.indices.iter().zip(dims.iter()) {
701 let rep = uf.find(*idx.id());
702 sizes.entry(rep).or_insert(dim);
703 }
704 }
705
706 sizes
707}
708
709fn contract_impl(tensors: &[&IdxTensor], options: ContractionOptions<'_>) -> Result<IdxTensor> {
722 let plan = build_contraction_plan(tensors, options)?;
724
725 let mut sizes: HashMap<usize, usize> = HashMap::new();
730 for (tensor_idx, tensor) in tensors.iter().enumerate() {
731 let dims = tensor.dims();
732 for (pos, &dim) in dims.iter().enumerate() {
733 let internal_id = plan.input_ids[tensor_idx][pos];
734 match sizes.entry(internal_id) {
735 std::collections::hash_map::Entry::Vacant(entry) => {
736 entry.insert(dim);
737 }
738 std::collections::hash_map::Entry::Occupied(entry) => {
739 if *entry.get() != dim {
740 return Err(anyhow::anyhow!(
741 "Internal label shape mismatch: label {} has dimensions {} and {}",
742 internal_id,
743 entry.get(),
744 dim
745 ));
746 }
747 }
748 }
749 }
750 }
751
752 let profile_signature = contract_profile_enabled().then(|| ContractSignature {
753 operands: tensors
754 .iter()
755 .enumerate()
756 .map(|(tensor_idx, tensor)| ContractOperandSignature {
757 dims: tensor.dims().to_vec(),
758 ids: plan.input_ids[tensor_idx].clone(),
759 is_diag: tensor.is_diag(),
760 })
761 .collect(),
762 output_ids: plan.output_ids.clone(),
763 output_dims: plan.output_ids.iter().map(|id| sizes[id]).collect(),
764 });
765 let profile_started = contract_profile_enabled().then(Instant::now);
766
767 let result = execute_contraction_plan(tensors, &plan, !options.retain_indices.is_empty())?;
768 if let (Some(signature), Some(started)) = (profile_signature, profile_started) {
769 record_contract_profile(signature, started.elapsed());
770 }
771 Ok(result)
772}
773
774fn execute_contraction_plan(
775 tensors: &[&IdxTensor],
776 plan: &ContractionPlan,
777 has_retained_indices: bool,
778) -> Result<IdxTensor> {
779 let any_grad = tensors.iter().any(|tensor| tensor.tracks_grad());
780 let first_dtype = tensors[0].as_inner()?.dtype();
781 let same_dtype = tensors
782 .iter()
783 .map(|tensor| Ok(tensor.as_inner()?.dtype() == first_dtype))
784 .collect::<Result<Vec<_>>>()?
785 .into_iter()
786 .all(|same| same);
787 let has_non_dense_axis_classes = tensors
788 .iter()
789 .map(|tensor| {
790 Ok(tensor
791 .axis_classes()
792 .iter()
793 .copied()
794 .enumerate()
795 .any(|(axis, class)| axis != class))
796 })
797 .collect::<Result<Vec<_>>>()?
798 .into_iter()
799 .any(|non_dense| non_dense);
800
801 if any_grad && same_dtype && has_non_dense_axis_classes && !has_retained_indices {
802 let mut iter = tensors.iter();
805 let Some(first) = iter.next() else {
806 return Err(anyhow::anyhow!("No tensors to contract"));
807 };
808 let mut result = (*first).clone();
809 for tensor in iter {
810 result = contract_pair(&result, tensor)?;
811 }
812 return Ok(result);
813 }
814
815 if any_grad {
816 let operands = tensors
817 .iter()
818 .map(|tensor| tensor.as_inner())
819 .collect::<Result<Vec<_>>>()?;
820 let subscripts = build_einsum_subscripts_from_usize_ids(&plan.input_ids, &plan.output_ids)?;
821 let result = operands.as_slice().einsum_subscripts(&subscripts)?;
822 return IdxTensor::from_inner_with_axis_classes(
823 plan.result_indices.clone(),
824 result,
825 plan.result_axis_classes.clone(),
826 );
827 }
828
829 let native_operands = tensors
830 .iter()
831 .enumerate()
832 .map(|(tensor_idx, tensor)| {
833 Ok((
834 NativeTensorReadInput::Borrowed(tensor.as_inner()?.tensor_read()),
835 plan.input_ids[tensor_idx].as_slice(),
836 ))
837 })
838 .collect::<Result<Vec<_>>>()?;
839 let operand_refs = native_operands
840 .iter()
841 .map(|(tensor, ids)| (tensor, *ids))
842 .collect::<Vec<_>>();
843 let result_native = einsum_native_tensor_reads(&operand_refs, &plan.output_ids)?;
844 IdxTensor::from_native_with_axis_classes(
845 plan.result_indices.clone(),
846 result_native,
847 plan.result_axis_classes.clone(),
848 )
849}
850
851fn build_einsum_subscripts_from_usize_ids(
852 input_ids: &[Vec<usize>],
853 output_ids: &[usize],
854) -> Result<EinsumSubscripts> {
855 let inputs = input_ids
856 .iter()
857 .map(|ids| {
858 ids.iter()
859 .map(|&id| {
860 u32::try_from(id)
861 .map_err(|_| anyhow::anyhow!("einsum label {id} exceeds u32 range"))
862 })
863 .collect::<Result<Vec<_>>>()
864 })
865 .collect::<Result<Vec<_>>>()?;
866 let output = output_ids
867 .iter()
868 .map(|&id| {
869 u32::try_from(id).map_err(|_| anyhow::anyhow!("einsum label {id} exceeds u32 range"))
870 })
871 .collect::<Result<Vec<_>>>()?;
872 let input_refs = inputs.iter().map(Vec::as_slice).collect::<Vec<_>>();
873 Ok(EinsumSubscripts::new(&input_refs, &output))
874}
875
876#[derive(Debug, Clone)]
878struct ContractionPlan {
879 input_ids: Vec<Vec<usize>>,
880 output_ids: Vec<usize>,
881 result_indices: Vec<DynIndex>,
882 result_axis_classes: Vec<usize>,
883}
884
885fn build_contraction_plan(
886 tensors: &[&IdxTensor],
887 options: ContractionOptions<'_>,
888) -> Result<ContractionPlan> {
889 let retained_indices: HashSet<DynIndex> = options.retain_indices.iter().cloned().collect();
890 let (input_ids, internal_id_to_original) = build_internal_ids(tensors, &retained_indices)?;
891
892 let mut counts: HashMap<usize, usize> = HashMap::new();
893 for ids in &input_ids {
894 for &internal_id in ids {
895 *counts.entry(internal_id).or_insert(0) += 1;
896 }
897 }
898 let mut output_ids = Vec::new();
899 let mut seen_output = HashSet::new();
900 let mut found_retained = HashSet::new();
901
902 for (tensor_idx, tensor) in tensors.iter().enumerate() {
903 for (axis, idx) in tensor.indices.iter().enumerate() {
904 let internal_id = input_ids[tensor_idx][axis];
905 let should_output = counts[&internal_id] == 1 || retained_indices.contains(idx);
906 if should_output && seen_output.insert(internal_id) {
907 output_ids.push(internal_id);
908 }
909 if retained_indices.contains(idx) {
910 found_retained.insert(idx.clone());
911 }
912 }
913 }
914
915 for retained in retained_indices {
916 if !found_retained.contains(&retained) {
917 return Err(anyhow::anyhow!(
918 "Retained index {:?} does not appear in the input tensors",
919 retained
920 ));
921 }
922 }
923
924 let result_indices: Vec<DynIndex> = output_ids
925 .iter()
926 .map(|&internal_id| {
927 let (tensor_idx, pos) = internal_id_to_original[&internal_id];
928 tensors[tensor_idx].indices[pos].clone()
929 })
930 .collect();
931 validate_unique_output_indices(&result_indices)?;
932 let result_axis_classes =
933 output_axis_classes(tensors, &input_ids, &output_ids, &internal_id_to_original)?;
934
935 Ok(ContractionPlan {
936 input_ids,
937 output_ids,
938 result_indices,
939 result_axis_classes,
940 })
941}
942
943fn validate_retained_indices_exist(
944 tensors: &[&IdxTensor],
945 retain_indices: &[DynIndex],
946) -> Result<()> {
947 for retain in retain_indices {
948 let found = tensors
949 .iter()
950 .any(|tensor| tensor.indices().iter().any(|idx| idx == retain));
951 if !found {
952 return Err(anyhow::anyhow!(
953 "Retained index {:?} does not appear in the input tensors",
954 retain
955 ));
956 }
957 }
958 Ok(())
959}
960
961fn validate_unique_output_indices(indices: &[DynIndex]) -> Result<()> {
962 let mut seen = HashSet::new();
963 for idx in indices {
964 if !seen.insert(idx.clone()) {
965 return Err(anyhow::anyhow!(
966 "Contraction result would contain duplicate output indices"
967 ));
968 }
969 }
970 Ok(())
971}
972
973fn output_axis_classes(
974 tensors: &[&IdxTensor],
975 ixs: &[Vec<usize>],
976 output: &[usize],
977 internal_id_to_original: &HashMap<usize, (usize, usize)>,
978) -> Result<Vec<usize>> {
979 fn find(parent: &mut [usize], value: usize) -> usize {
980 if parent[value] != value {
981 parent[value] = find(parent, parent[value]);
982 }
983 parent[value]
984 }
985
986 fn union(parent: &mut [usize], lhs: usize, rhs: usize) {
987 let lhs_root = find(parent, lhs);
988 let rhs_root = find(parent, rhs);
989 if lhs_root != rhs_root {
990 parent[rhs_root] = lhs_root;
991 }
992 }
993
994 let mut class_offsets = Vec::with_capacity(tensors.len());
995 let mut next_node = 0usize;
996 for tensor in tensors {
997 class_offsets.push(next_node);
998 let payload_rank = tensor
999 .axis_classes()
1000 .iter()
1001 .copied()
1002 .max()
1003 .map(|value| value + 1)
1004 .unwrap_or(0);
1005 next_node += payload_rank;
1006 }
1007 let mut parent: Vec<usize> = (0..next_node).collect();
1008 let mut axes_by_internal_id: HashMap<usize, Vec<usize>> = HashMap::new();
1009
1010 for (tensor_idx, tensor) in tensors.iter().enumerate() {
1011 for (axis, &internal_id) in ixs[tensor_idx].iter().enumerate() {
1012 let class_id = tensor.axis_classes()[axis];
1013 let node = class_offsets[tensor_idx] + class_id;
1014 axes_by_internal_id
1015 .entry(internal_id)
1016 .or_default()
1017 .push(node);
1018 }
1019 }
1020
1021 for nodes in axes_by_internal_id.values() {
1022 if let Some((&first, rest)) = nodes.split_first() {
1023 for &node in rest {
1024 union(&mut parent, first, node);
1025 }
1026 }
1027 }
1028
1029 let mut root_to_class = HashMap::new();
1030 let mut next_class = 0usize;
1031 output
1032 .iter()
1033 .map(|internal_id| {
1034 let (tensor_idx, axis) = internal_id_to_original[internal_id];
1035 let class_id = tensors[tensor_idx].axis_classes()[axis];
1036 let node = class_offsets[tensor_idx] + class_id;
1037 let root = find(&mut parent, node);
1038 Ok(*root_to_class.entry(root).or_insert_with(|| {
1039 let class = next_class;
1040 next_class += 1;
1041 class
1042 }))
1043 })
1044 .collect::<Result<Vec<_>>>()
1045}
1046
1047#[allow(clippy::type_complexity)]
1055fn build_internal_ids(
1056 tensors: &[&IdxTensor],
1057 retained_indices: &HashSet<DynIndex>,
1058) -> Result<(Vec<Vec<usize>>, HashMap<usize, (usize, usize)>)> {
1059 let mut next_id = 0usize;
1060 let mut index_to_internal: HashMap<DynIndex, usize> = HashMap::new();
1061 let mut retained_index_to_internal: HashMap<DynIndex, usize> = HashMap::new();
1062 let mut assigned: HashMap<(usize, usize), usize> = HashMap::new();
1063 let mut internal_id_to_original: HashMap<usize, (usize, usize)> = HashMap::new();
1064
1065 for ti in 0..tensors.len() {
1066 for tj in (ti + 1)..tensors.len() {
1067 for (pi, idx_i) in tensors[ti].indices.iter().enumerate() {
1068 for (pj, idx_j) in tensors[tj].indices.iter().enumerate() {
1069 if idx_i.is_contractable(idx_j) {
1070 let key_i = (ti, pi);
1071 let key_j = (tj, pj);
1072
1073 match (assigned.get(&key_i).copied(), assigned.get(&key_j).copied()) {
1074 (None, None) => {
1075 let internal_id = if let Some(&id) = index_to_internal.get(idx_i) {
1076 id
1077 } else {
1078 let id = next_id;
1079 next_id += 1;
1080 index_to_internal.insert(idx_i.clone(), id);
1081 internal_id_to_original.insert(id, key_i);
1082 id
1083 };
1084 assigned.insert(key_i, internal_id);
1085 assigned.insert(key_j, internal_id);
1086 if idx_i != idx_j {
1087 index_to_internal.insert(idx_j.clone(), internal_id);
1088 }
1089 }
1090 (Some(id), None) => {
1091 assigned.insert(key_j, id);
1092 index_to_internal.insert(idx_j.clone(), id);
1093 }
1094 (None, Some(id)) => {
1095 assigned.insert(key_i, id);
1096 index_to_internal.insert(idx_i.clone(), id);
1097 }
1098 (Some(_id_i), Some(_id_j)) => {
1099 }
1101 }
1102 }
1103 }
1104 }
1105 }
1106 }
1107
1108 for (tensor_idx, tensor) in tensors.iter().enumerate() {
1110 for (pos, idx) in tensor.indices.iter().enumerate() {
1111 let key = (tensor_idx, pos);
1112 if let std::collections::hash_map::Entry::Vacant(e) = assigned.entry(key) {
1113 let internal_id = if retained_indices.contains(idx) {
1114 if let Some(&id) = retained_index_to_internal.get(idx) {
1115 id
1116 } else {
1117 let id = next_id;
1118 next_id += 1;
1119 retained_index_to_internal.insert(idx.clone(), id);
1120 internal_id_to_original.insert(id, key);
1121 id
1122 }
1123 } else {
1124 let id = next_id;
1125 next_id += 1;
1126 internal_id_to_original.insert(id, key);
1127 id
1128 };
1129 e.insert(internal_id);
1130 }
1131 }
1132 }
1133
1134 let ixs: Vec<Vec<usize>> = tensors
1136 .iter()
1137 .enumerate()
1138 .map(|(tensor_idx, tensor)| {
1139 (0..tensor.indices.len())
1140 .map(|pos| assigned[&(tensor_idx, pos)])
1141 .collect()
1142 })
1143 .collect();
1144
1145 Ok((ixs, internal_id_to_original))
1146}
1147
1148fn has_contractable_indices(a: &IdxTensor, b: &IdxTensor) -> bool {
1154 a.indices
1155 .iter()
1156 .any(|idx_a| b.indices.iter().any(|idx_b| idx_a.is_contractable(idx_b)))
1157}
1158
1159#[allow(dead_code)]
1163fn find_tensor_connected_components(tensors: &[&IdxTensor]) -> Vec<Vec<usize>> {
1164 find_tensor_connected_components_with_retained(tensors, &[])
1165}
1166
1167fn find_tensor_connected_components_with_retained(
1168 tensors: &[&IdxTensor],
1169 retain_indices: &[DynIndex],
1170) -> Vec<Vec<usize>> {
1171 let n = tensors.len();
1172 if n == 0 {
1173 return vec![];
1174 }
1175 if n == 1 {
1176 return vec![vec![0]];
1177 }
1178
1179 let mut graph = UnGraph::<(), ()>::new_undirected();
1181 let nodes: Vec<_> = (0..n).map(|_| graph.add_node(())).collect();
1182
1183 for i in 0..n {
1184 for j in (i + 1)..n {
1185 if has_contractable_indices(tensors[i], tensors[j]) {
1186 graph.add_edge(nodes[i], nodes[j], ());
1187 }
1188 }
1189 }
1190
1191 if !retain_indices.is_empty() {
1192 for i in 0..n {
1193 for j in (i + 1)..n {
1194 if shares_retained_index(tensors[i], tensors[j], retain_indices) {
1195 graph.add_edge(nodes[i], nodes[j], ());
1196 }
1197 }
1198 }
1199 }
1200
1201 let num_components = connected_components(&graph);
1203
1204 if num_components == 1 {
1205 return vec![(0..n).collect()];
1206 }
1207
1208 use petgraph::visit::Dfs;
1210 let mut visited = vec![false; n];
1211 let mut components = Vec::new();
1212
1213 for start in 0..n {
1214 if !visited[start] {
1215 let mut component = Vec::new();
1216 let mut dfs = Dfs::new(&graph, nodes[start]);
1217 while let Some(node) = dfs.next(&graph) {
1218 let idx = node.index();
1219 if !visited[idx] {
1220 visited[idx] = true;
1221 component.push(idx);
1222 }
1223 }
1224 component.sort();
1225 components.push(component);
1226 }
1227 }
1228
1229 components.sort_by_key(|c| c[0]);
1230 components
1231}
1232
1233fn shares_retained_index(a: &IdxTensor, b: &IdxTensor, retain_indices: &[DynIndex]) -> bool {
1234 retain_indices.iter().any(|retain| {
1235 a.indices().iter().any(|idx_a| idx_a == retain)
1236 && b.indices().iter().any(|idx_b| idx_b == retain)
1237 })
1238}
1239
1240#[cfg(test)]
1241mod tests;