quantrs2_sim/tensor_network/
contraction.rs1use super::tensor::Tensor;
7use quantrs2_core::error::{QuantRS2Error, QuantRS2Result};
8use scirs2_core::ndarray::{ArrayD, Dimension, IxDyn};
9use scirs2_core::Complex64;
10use std::collections::{HashMap, HashSet};
11
12pub trait ContractableNetwork {
14 fn contract_tensors(&mut self, tensor_id1: usize, tensor_id2: usize) -> QuantRS2Result<usize>;
16
17 fn optimize_contraction_order(&mut self) -> QuantRS2Result<()>;
19}
20
21#[derive(Debug, Clone)]
23pub struct ContractionPath {
24 steps: Vec<(usize, usize)>,
26
27 estimated_cost: f64,
29}
30
31impl ContractionPath {
32 pub const fn new(steps: Vec<(usize, usize)>, estimated_cost: f64) -> Self {
34 Self {
35 steps,
36 estimated_cost,
37 }
38 }
39
40 pub fn steps(&self) -> &[(usize, usize)] {
42 &self.steps
43 }
44
45 pub const fn estimated_cost(&self) -> f64 {
47 self.estimated_cost
48 }
49}
50
51pub fn calculate_greedy_contraction_path(
58 tensors: &HashMap<usize, Tensor>,
59 connections: &[(super::tensor::TensorIndex, super::tensor::TensorIndex)],
60) -> QuantRS2Result<ContractionPath> {
61 let mut tensor_connections = HashMap::new();
63 for (t1, t2) in connections {
64 tensor_connections
65 .entry(t1.tensor_id)
66 .or_insert_with(HashSet::new)
67 .insert(t2.tensor_id);
68 tensor_connections
69 .entry(t2.tensor_id)
70 .or_insert_with(HashSet::new)
71 .insert(t1.tensor_id);
72 }
73
74 let mut tensor_dims = HashMap::new();
76 for (&id, tensor) in tensors {
77 tensor_dims.insert(id, tensor.dimensions.iter().product::<usize>());
78 }
79
80 let mut remaining_tensors: HashSet<usize> = tensors.keys().copied().collect();
83 let mut steps = Vec::new();
84 let mut total_cost = 0.0;
85
86 while remaining_tensors.len() > 1 {
87 let mut best_cost = f64::INFINITY;
88 let mut best_pair = None;
89
90 for &t1 in &remaining_tensors {
92 if let Some(connected) = tensor_connections.get(&t1) {
93 for &t2 in connected {
94 if remaining_tensors.contains(&t2) {
95 let combined_dim = tensor_dims[&t1] * tensor_dims[&t2];
97 let cost = combined_dim as f64;
98
99 if cost < best_cost {
100 best_cost = cost;
101 best_pair = Some((t1, t2));
102 }
103 }
104 }
105 }
106 }
107
108 if let Some((t1, t2)) = best_pair {
110 steps.push((t1, t2));
112 total_cost += best_cost;
113
114 remaining_tensors.remove(&t1);
116 remaining_tensors.remove(&t2);
117
118 let new_id = t1; remaining_tensors.insert(new_id);
121
122 let mut new_connections = HashSet::new();
124
125 let mut t1_connected_tensors = Vec::new();
128 if let Some(t1_connections) = tensor_connections.get(&t1) {
129 for &connected_tensor in t1_connections {
130 if connected_tensor != t2 && remaining_tensors.contains(&connected_tensor) {
131 t1_connected_tensors.push(connected_tensor);
132 new_connections.insert(connected_tensor);
133 }
134 }
135 }
136
137 for connected_tensor in t1_connected_tensors {
139 if let Some(other_connections) = tensor_connections.get_mut(&connected_tensor) {
140 other_connections.remove(&t1);
141 other_connections.remove(&t2);
142 other_connections.insert(new_id);
143 }
144 }
145
146 let mut t2_connected_tensors = Vec::new();
149 if let Some(t2_connections) = tensor_connections.get(&t2) {
150 for &connected_tensor in t2_connections {
151 if connected_tensor != t1 && remaining_tensors.contains(&connected_tensor) {
152 t2_connected_tensors.push(connected_tensor);
153 new_connections.insert(connected_tensor);
154 }
155 }
156 }
157
158 for connected_tensor in t2_connected_tensors {
160 if let Some(other_connections) = tensor_connections.get_mut(&connected_tensor) {
161 other_connections.remove(&t1);
162 other_connections.remove(&t2);
163 other_connections.insert(new_id);
164 }
165 }
166
167 tensor_connections.insert(new_id, new_connections);
169
170 tensor_dims.insert(new_id, (tensor_dims[&t1] * tensor_dims[&t2]) / 2);
173 } else {
174 let mut remaining_vec: Vec<_> = remaining_tensors.iter().copied().collect();
176 remaining_vec.sort_unstable();
177
178 if remaining_vec.len() >= 2 {
179 let t1 = remaining_vec[0];
180 let t2 = remaining_vec[1];
181
182 steps.push((t1, t2));
183 total_cost += (tensor_dims[&t1] * tensor_dims[&t2]) as f64;
184
185 remaining_tensors.remove(&t1);
186 remaining_tensors.remove(&t2);
187 remaining_tensors.insert(t1);
188
189 tensor_dims.insert(t1, (tensor_dims[&t1] * tensor_dims[&t2]) / 2);
191 } else {
192 break;
194 }
195 }
196 }
197
198 Ok(ContractionPath::new(steps, total_cost))
199}
200
201pub fn calculate_optimal_contraction_path(
206 tensors: &HashMap<usize, Tensor>,
207 connections: &[(super::tensor::TensorIndex, super::tensor::TensorIndex)],
208) -> QuantRS2Result<ContractionPath> {
209 if let Some(path) = identify_circuit_structure(tensors, connections) {
212 return Ok(path);
213 }
214
215 calculate_greedy_contraction_path(tensors, connections)
217}
218
219fn identify_circuit_structure(
226 tensors: &HashMap<usize, Tensor>,
227 connections: &[(super::tensor::TensorIndex, super::tensor::TensorIndex)],
228) -> Option<ContractionPath> {
229 let mut tensor_connections = HashMap::new();
231 for (t1, t2) in connections {
232 tensor_connections
233 .entry(t1.tensor_id)
234 .or_insert_with(HashSet::new)
235 .insert(t2.tensor_id);
236 tensor_connections
237 .entry(t2.tensor_id)
238 .or_insert_with(HashSet::new)
239 .insert(t1.tensor_id);
240 }
241
242 let mut tensor_ids: Vec<usize> = tensors.keys().copied().collect();
244 tensor_ids.sort_unstable();
245
246 if is_linear_circuit(&tensor_connections, &tensor_ids) {
250 let mut steps = Vec::new();
252 let mut cost = 0.0;
253
254 let ordered_tensors = order_linear_circuit(&tensor_connections, &tensor_ids);
256
257 for ids in ordered_tensors.windows(2) {
259 steps.push((ids[0], ids[1]));
260 cost += 16.0; }
262
263 return Some(ContractionPath::new(steps, cost));
264 }
265
266 if is_star_circuit(&tensor_connections, &tensor_ids) {
270 let mut steps = Vec::new();
272 let mut cost = 0.0;
273
274 let central = find_central_tensor(&tensor_connections);
276
277 let leaf_tensors: Vec<_> = tensor_ids
279 .iter()
280 .filter(|&&id| {
281 id != central
282 && tensor_connections
283 .get(&id)
284 .is_some_and(|conns| conns.contains(¢ral))
285 })
286 .copied()
287 .collect();
288
289 for leaf in leaf_tensors {
290 steps.push((central, leaf));
291 cost += 16.0; }
293
294 return Some(ContractionPath::new(steps, cost));
295 }
296
297 if is_qft_circuit(&tensor_connections, tensors) {
300 return Some(optimize_qft_circuit(&tensor_connections, tensors));
301 }
302
303 if is_qaoa_circuit(&tensor_connections, tensors) {
306 return Some(optimize_qaoa_circuit(&tensor_connections, tensors));
307 }
308
309 None
311}
312
313fn is_qft_circuit(
315 tensor_connections: &HashMap<usize, HashSet<usize>>,
316 tensors: &HashMap<usize, Tensor>,
317) -> bool {
318 let mut hadamard_count = 0;
323 let mut controlled_phase_count = 0;
324 let mut swap_count = 0;
325
326 for tensor in tensors.values() {
328 if tensor.rank == 2 {
330 hadamard_count += 1;
331 } else if tensor.rank == 4 {
332 if tensor.dimensions == vec![2, 2, 2, 2] {
334 controlled_phase_count += 1;
337 }
338
339 if is_swap_like_tensor(tensor) {
341 swap_count += 1;
342 }
343 }
344 }
345
346 hadamard_count > 0 && controlled_phase_count > 0 && hadamard_count >= controlled_phase_count / 2
352}
353
354fn is_swap_like_tensor(tensor: &Tensor) -> bool {
356 tensor.rank == 4 && tensor.dimensions == vec![2, 2, 2, 2]
359}
360
361fn optimize_qft_circuit(
363 tensor_connections: &HashMap<usize, HashSet<usize>>,
364 tensors: &HashMap<usize, Tensor>,
365) -> ContractionPath {
366 let mut ordered_tensors: Vec<usize> = Vec::new();
371 let mut tensor_ids: Vec<usize> = tensors.keys().copied().collect();
372 tensor_ids.sort_unstable();
373
374 let mut steps = Vec::new();
377 let mut cost = 0.0;
378
379 let mut layers = identify_qft_layers(tensor_connections, &tensor_ids);
384
385 for layer in layers {
387 for i in 0..layer.len().saturating_sub(1) {
389 steps.push((layer[i], layer[i + 1]));
390 cost += 16.0; }
392 }
393
394 if steps.is_empty() {
396 for i in 0..tensor_ids.len().saturating_sub(1) {
397 steps.push((tensor_ids[i], tensor_ids[i + 1]));
398 cost += 16.0;
399 }
400 }
401
402 ContractionPath::new(steps, cost)
403}
404
405fn identify_qft_layers(
407 tensor_connections: &HashMap<usize, HashSet<usize>>,
408 tensor_ids: &[usize],
409) -> Vec<Vec<usize>> {
410 let mut degree_groups: HashMap<usize, Vec<usize>> = HashMap::new();
418
419 for &id in tensor_ids {
420 let degree = tensor_connections.get(&id).map_or(0, |conns| conns.len());
421 degree_groups.entry(degree).or_default().push(id);
422 }
423
424 let mut degrees: Vec<usize> = degree_groups.keys().copied().collect();
426 degrees.sort_by(|a, b| b.cmp(a));
427
428 let mut layers = Vec::new();
430 for degree in degrees {
431 if let Some(group) = degree_groups.get(°ree) {
432 layers.push(group.clone());
433 }
434 }
435
436 layers
437}
438
439fn is_qaoa_circuit(
441 tensor_connections: &HashMap<usize, HashSet<usize>>,
442 tensors: &HashMap<usize, Tensor>,
443) -> bool {
444 let mut x_rotation_count = 0;
449 let mut zz_interaction_count = 0;
450
451 for tensor in tensors.values() {
453 if tensor.rank == 2 {
455 x_rotation_count += 1; }
457 else if tensor.rank == 4 {
459 zz_interaction_count += 1; }
461 }
462
463 x_rotation_count > 0 && zz_interaction_count > 0
466}
467
468fn optimize_qaoa_circuit(
470 tensor_connections: &HashMap<usize, HashSet<usize>>,
471 tensors: &HashMap<usize, Tensor>,
472) -> ContractionPath {
473 let mut tensor_ids: Vec<usize> = tensors.keys().copied().collect();
478 tensor_ids.sort_by(|a, b| {
479 if let (Some(tensor_a), Some(tensor_b)) = (tensors.get(a), tensors.get(b)) {
480 tensor_b.rank.cmp(&tensor_a.rank) } else {
482 std::cmp::Ordering::Equal
483 }
484 });
485
486 let mut rank_groups: HashMap<usize, Vec<usize>> = HashMap::new();
488
489 for &id in &tensor_ids {
490 if let Some(tensor) = tensors.get(&id) {
491 rank_groups.entry(tensor.rank).or_default().push(id);
492 }
493 }
494
495 let mut steps = Vec::new();
497 let mut cost = 0.0;
498
499 if let Some(two_qubit_gates) = rank_groups.get(&4) {
501 for (i, &id1) in two_qubit_gates.iter().enumerate() {
502 for &id2 in two_qubit_gates.iter().skip(i + 1) {
503 if tensor_connections
505 .get(&id1)
506 .is_some_and(|conns| conns.contains(&id2))
507 {
508 steps.push((id1, id2));
509 cost += 64.0; }
511 }
512 }
513 }
514
515 if let Some(single_qubit_gates) = rank_groups.get(&2) {
517 for (i, &id1) in single_qubit_gates.iter().enumerate() {
518 for &id2 in single_qubit_gates.iter().skip(i + 1) {
519 if tensor_connections
521 .get(&id1)
522 .is_some_and(|conns| conns.contains(&id2))
523 {
524 steps.push((id1, id2));
525 cost += 16.0; }
527 }
528 }
529 }
530
531 if steps.is_empty() {
534 for i in 0..tensor_ids.len().saturating_sub(1) {
535 steps.push((tensor_ids[i], tensor_ids[i + 1]));
536 cost += 16.0; }
538 }
539
540 ContractionPath::new(steps, cost)
541}
542
543fn is_linear_circuit(
545 tensor_connections: &HashMap<usize, HashSet<usize>>,
546 tensor_ids: &[usize],
547) -> bool {
548 let mut num_endpoints = 0;
550
551 for &id in tensor_ids {
552 let degree = tensor_connections.get(&id).map_or(0, |conns| conns.len());
553
554 if degree > 2 {
555 return false;
557 } else if degree == 1 {
558 num_endpoints += 1;
560 }
561 }
562
563 num_endpoints == 2
565}
566
567fn order_linear_circuit(
569 tensor_connections: &HashMap<usize, HashSet<usize>>,
570 tensor_ids: &[usize],
571) -> Vec<usize> {
572 let mut result = Vec::new();
573
574 let mut current = tensor_ids
576 .iter()
577 .find(|&&id| {
578 tensor_connections
579 .get(&id)
580 .is_some_and(|conns| conns.len() == 1)
581 })
582 .copied();
583
584 if let Some(start) = current {
585 result.push(start);
587 let mut visited = HashSet::new();
588 visited.insert(start);
589
590 while let Some(id) = current {
592 if let Some(connections) = tensor_connections.get(&id) {
593 let next = connections
594 .iter()
595 .find(|&&next_id| !visited.contains(&next_id))
596 .copied();
597
598 if let Some(next_id) = next {
599 result.push(next_id);
600 visited.insert(next_id);
601 current = Some(next_id);
602 } else {
603 current = None;
605 }
606 } else {
607 current = None;
608 }
609 }
610 }
611
612 if result.len() != tensor_ids.len() {
614 return tensor_ids.to_vec();
615 }
616
617 result
618}
619
620fn is_star_circuit(
622 tensor_connections: &HashMap<usize, HashSet<usize>>,
623 tensor_ids: &[usize],
624) -> bool {
625 let mut degree_counts = HashMap::new();
627
628 for &id in tensor_ids {
629 let degree = tensor_connections.get(&id).map_or(0, |conns| conns.len());
630 *degree_counts.entry(degree).or_insert(0) += 1;
631 }
632
633 let high_degree = degree_counts.keys().filter(|&&d| d > 2).count();
636 let degree_one = degree_counts.get(&1).copied().unwrap_or(0);
637
638 high_degree == 1 && degree_one > 2
640}
641
642fn find_central_tensor(tensor_connections: &HashMap<usize, HashSet<usize>>) -> usize {
644 let mut max_degree = 0;
645 let mut central = 0;
646
647 for (&id, connections) in tensor_connections {
648 let degree = connections.len();
649 if degree > max_degree {
650 max_degree = degree;
651 central = id;
652 }
653 }
654
655 central
656}
657
658pub(crate) fn shared_axis_pairs(
664 connections: &[(super::tensor::TensorIndex, super::tensor::TensorIndex)],
665 id1: usize,
666 id2: usize,
667) -> Vec<(usize, usize)> {
668 let mut pairs = Vec::new();
669 for (a, b) in connections {
670 if a.tensor_id == id1 && b.tensor_id == id2 {
671 pairs.push((a.index, b.index));
672 } else if a.tensor_id == id2 && b.tensor_id == id1 {
673 pairs.push((b.index, a.index));
674 }
675 }
676 pairs
677}
678
679pub(crate) fn contract_pair_multi(
691 a: &Tensor,
692 b: &Tensor,
693 shared: &[(usize, usize)],
694) -> QuantRS2Result<Tensor> {
695 for &(ax_a, ax_b) in shared {
696 if ax_a >= a.rank || ax_b >= b.rank {
697 return Err(QuantRS2Error::CircuitValidationFailed(format!(
698 "contract_pair_multi: axis out of range ({ax_a}, {ax_b})"
699 )));
700 }
701 if a.dimensions[ax_a] != b.dimensions[ax_b] {
702 return Err(QuantRS2Error::CircuitValidationFailed(format!(
703 "contract_pair_multi: dimension mismatch {} vs {}",
704 a.dimensions[ax_a], b.dimensions[ax_b]
705 )));
706 }
707 }
708
709 let a_contracted: HashSet<usize> = shared.iter().map(|&(ax, _)| ax).collect();
710 let b_contracted: HashSet<usize> = shared.iter().map(|&(_, ax)| ax).collect();
711
712 let a_free: Vec<usize> = (0..a.rank)
713 .filter(|ax| !a_contracted.contains(ax))
714 .collect();
715 let b_free: Vec<usize> = (0..b.rank)
716 .filter(|ax| !b_contracted.contains(ax))
717 .collect();
718
719 let mut result_dims: Vec<usize> = a_free.iter().map(|&ax| a.dimensions[ax]).collect();
720 result_dims.extend(b_free.iter().map(|&ax| b.dimensions[ax]));
721
722 let result_is_scalar = result_dims.is_empty();
723 let result_shape = if result_is_scalar {
724 IxDyn(&[1usize])
725 } else {
726 IxDyn(result_dims.as_slice())
727 };
728 let mut result_data = ArrayD::<Complex64>::zeros(result_shape);
729
730 for (a_idx, a_val) in a.data.indexed_iter() {
731 let a_raw = a_idx.slice();
732 for (b_idx, b_val) in b.data.indexed_iter() {
733 let b_raw = b_idx.slice();
734
735 if shared
737 .iter()
738 .any(|&(ax_a, ax_b)| a_raw[ax_a] != b_raw[ax_b])
739 {
740 continue;
741 }
742
743 let mut res_idx: Vec<usize> = a_free.iter().map(|&ax| a_raw[ax]).collect();
744 res_idx.extend(b_free.iter().map(|&ax| b_raw[ax]));
745
746 let target = if result_is_scalar {
747 &mut result_data[IxDyn(&[0usize])]
748 } else {
749 &mut result_data[IxDyn(res_idx.as_slice())]
750 };
751 *target += *a_val * *b_val;
752 }
753 }
754
755 let final_data = if result_is_scalar {
756 let scalar = result_data[IxDyn(&[0usize])];
757 ArrayD::from_elem(IxDyn(&[]), scalar)
758 } else {
759 result_data
760 };
761
762 Ok(Tensor::new(final_data))
763}
764
765struct LabeledTensor {
768 tensor: Tensor,
769 labels: Vec<usize>,
770}
771
772pub fn contract_network_along_path(
786 tensors: &mut HashMap<usize, Tensor>,
787 connections: &mut Vec<(super::tensor::TensorIndex, super::tensor::TensorIndex)>,
788 path: &ContractionPath,
789 next_id: &mut usize,
790) -> QuantRS2Result<Tensor> {
791 let _ = next_id; if tensors.is_empty() {
794 return Err(QuantRS2Error::CircuitValidationFailed(
795 "contract_network_along_path: empty tensor network".to_string(),
796 ));
797 }
798
799 let mut next_label = 0usize;
801 let mut working: HashMap<usize, LabeledTensor> = HashMap::new();
802 for (&id, tensor) in tensors.iter() {
803 let labels: Vec<usize> = (0..tensor.rank)
804 .map(|_| {
805 let label = next_label;
806 next_label += 1;
807 label
808 })
809 .collect();
810 working.insert(
811 id,
812 LabeledTensor {
813 tensor: tensor.clone(),
814 labels,
815 },
816 );
817 }
818
819 for (a, b) in connections.iter() {
821 let bond_label = next_label;
822 next_label += 1;
823 if let Some(lt) = working.get_mut(&a.tensor_id) {
824 if a.index < lt.labels.len() {
825 lt.labels[a.index] = bond_label;
826 }
827 }
828 if let Some(lt) = working.get_mut(&b.tensor_id) {
829 if b.index < lt.labels.len() {
830 lt.labels[b.index] = bond_label;
831 }
832 }
833 }
834
835 for &(id1, id2) in path.steps() {
838 if id1 == id2 {
839 continue;
840 }
841 let lt1 = working.remove(&id1).ok_or_else(|| {
842 QuantRS2Error::CircuitValidationFailed(format!(
843 "contract_network_along_path: tensor {id1} missing from path step"
844 ))
845 })?;
846 let lt2 = working.remove(&id2).ok_or_else(|| {
847 QuantRS2Error::CircuitValidationFailed(format!(
848 "contract_network_along_path: tensor {id2} missing from path step"
849 ))
850 })?;
851
852 let merged = contract_labeled(<1, <2)?;
853 working.insert(id1, merged);
854 }
855
856 let mut remaining: Vec<LabeledTensor> = working.into_values().collect();
858 let mut acc = remaining.remove(0);
859 for lt in remaining {
860 acc = contract_labeled(&acc, <)?;
861 }
862
863 Ok(acc.tensor)
864}
865
866fn contract_labeled(a: &LabeledTensor, b: &LabeledTensor) -> QuantRS2Result<LabeledTensor> {
869 let mut shared_pairs = Vec::new();
870 for (ai, &la) in a.labels.iter().enumerate() {
871 for (bi, &lb) in b.labels.iter().enumerate() {
872 if la == lb {
873 shared_pairs.push((ai, bi));
874 }
875 }
876 }
877
878 let merged = contract_pair_multi(&a.tensor, &b.tensor, &shared_pairs)?;
879
880 let a_contracted: HashSet<usize> = shared_pairs.iter().map(|&(ai, _)| ai).collect();
881 let b_contracted: HashSet<usize> = shared_pairs.iter().map(|&(_, bi)| bi).collect();
882
883 let mut labels: Vec<usize> = a
884 .labels
885 .iter()
886 .enumerate()
887 .filter(|&(i, _)| !a_contracted.contains(&i))
888 .map(|(_, &l)| l)
889 .collect();
890 labels.extend(
891 b.labels
892 .iter()
893 .enumerate()
894 .filter(|&(i, _)| !b_contracted.contains(&i))
895 .map(|(_, &l)| l),
896 );
897
898 Ok(LabeledTensor {
899 tensor: merged,
900 labels,
901 })
902}