Skip to main content

quantrs2_sim/tensor_network/
contraction.rs

1//! Contraction strategies for tensor networks
2//!
3//! This module provides algorithms and interfaces for contracting
4//! tensor networks efficiently.
5
6use 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
12/// Trait for a network of tensors that can be contracted
13pub trait ContractableNetwork {
14    /// Contract two tensors in the network, returning the ID of the resulting tensor
15    fn contract_tensors(&mut self, tensor_id1: usize, tensor_id2: usize) -> QuantRS2Result<usize>;
16
17    /// Optimize the contraction order of the network
18    fn optimize_contraction_order(&mut self) -> QuantRS2Result<()>;
19}
20
21/// A contraction path for a tensor network
22#[derive(Debug, Clone)]
23pub struct ContractionPath {
24    /// The sequence of tensor pairs to contract
25    steps: Vec<(usize, usize)>,
26
27    /// Estimated computational cost of this contraction path
28    estimated_cost: f64,
29}
30
31impl ContractionPath {
32    /// Create a new contraction path
33    pub const fn new(steps: Vec<(usize, usize)>, estimated_cost: f64) -> Self {
34        Self {
35            steps,
36            estimated_cost,
37        }
38    }
39
40    /// Get the steps in this contraction path
41    pub fn steps(&self) -> &[(usize, usize)] {
42        &self.steps
43    }
44
45    /// Get the estimated cost of this contraction path
46    pub const fn estimated_cost(&self) -> f64 {
47        self.estimated_cost
48    }
49}
50
51/// Calculate the optimal contraction path for a tensor network
52///
53/// This function implements a greedy algorithm to determine a good
54/// contraction order for a tensor network. It's not guaranteed to find
55/// the optimal path, but it should produce reasonable results for
56/// most practical cases.
57pub fn calculate_greedy_contraction_path(
58    tensors: &HashMap<usize, Tensor>,
59    connections: &[(super::tensor::TensorIndex, super::tensor::TensorIndex)],
60) -> QuantRS2Result<ContractionPath> {
61    // Step 1: Build a graph of tensor connections
62    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    // Step 2: Calculate dimensions of each tensor
75    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    // Step 3: Greedy algorithm: repeatedly find the pair of tensors that,
81    // when contracted, minimizes the size of the resulting tensor
82    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        // Find the best pair to contract next
91        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                        // Calculate cost of contracting t1 and t2
96                        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 we found a pair to contract
109        if let Some((t1, t2)) = best_pair {
110            // Add to our contraction steps
111            steps.push((t1, t2));
112            total_cost += best_cost;
113
114            // Remove contracted tensors
115            remaining_tensors.remove(&t1);
116            remaining_tensors.remove(&t2);
117
118            // Add new contracted tensor
119            let new_id = t1; // Reuse first tensor's ID
120            remaining_tensors.insert(new_id);
121
122            // Update connections for the new tensor
123            let mut new_connections = HashSet::new();
124
125            // Merge connections from t1
126            // First collect all connected tensors from t1
127            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            // Now update their connections
138            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            // Merge connections from t2
147            // First collect all connected tensors from t2
148            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            // Now update their connections
159            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            // Set the new tensor's connections
168            tensor_connections.insert(new_id, new_connections);
169
170            // Update the dimension of the new tensor (simplified)
171            // In a real implementation, we'd calculate this based on the actual tensors
172            tensor_dims.insert(new_id, (tensor_dims[&t1] * tensor_dims[&t2]) / 2);
173        } else {
174            // No connected tensors found, just contract the first two remaining
175            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                // Update dimensions
190                tensor_dims.insert(t1, (tensor_dims[&t1] * tensor_dims[&t2]) / 2);
191            } else {
192                // Only one tensor left, we're done
193                break;
194            }
195        }
196    }
197
198    Ok(ContractionPath::new(steps, total_cost))
199}
200
201/// Calculate the optimal contraction path using a more advanced algorithm
202///
203/// This function implements a more sophisticated algorithm that takes into account
204/// the structure of the tensor network to find a better contraction path.
205pub fn calculate_optimal_contraction_path(
206    tensors: &HashMap<usize, Tensor>,
207    connections: &[(super::tensor::TensorIndex, super::tensor::TensorIndex)],
208) -> QuantRS2Result<ContractionPath> {
209    // First, check if we can identify a specific circuit structure that has
210    // a known optimal contraction pattern
211    if let Some(path) = identify_circuit_structure(tensors, connections) {
212        return Ok(path);
213    }
214
215    // If no special structure is identified, fall back to the greedy algorithm
216    calculate_greedy_contraction_path(tensors, connections)
217}
218
219/// Identify common quantum circuit structures and return their optimal contraction paths
220///
221/// This function analyzes the tensor network to identify if it corresponds to a
222/// common quantum circuit structure (like a linear circuit, GHZ state preparation,
223/// or a quantum Fourier transform). If identified, returns a pre-computed optimal
224/// contraction path.
225fn identify_circuit_structure(
226    tensors: &HashMap<usize, Tensor>,
227    connections: &[(super::tensor::TensorIndex, super::tensor::TensorIndex)],
228) -> Option<ContractionPath> {
229    // Build a graph of tensor connections for analysis
230    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    // Get a sorted list of tensor IDs
243    let mut tensor_ids: Vec<usize> = tensors.keys().copied().collect();
244    tensor_ids.sort_unstable();
245
246    // Pattern 1: Linear Circuit (CNOT chain)
247    // In a linear circuit, most tensors connect to exactly 2 others,
248    // forming a chain-like structure
249    if is_linear_circuit(&tensor_connections, &tensor_ids) {
250        // For linear circuits, we should contract from one end to the other
251        let mut steps = Vec::new();
252        let mut cost = 0.0;
253
254        // Order tensors by their position in the chain
255        let ordered_tensors = order_linear_circuit(&tensor_connections, &tensor_ids);
256
257        // Contract tensors in sequence
258        for ids in ordered_tensors.windows(2) {
259            steps.push((ids[0], ids[1]));
260            cost += 16.0; // Simplified cost model (2^2 * 2^2)
261        }
262
263        return Some(ContractionPath::new(steps, cost));
264    }
265
266    // Pattern 2: Star-shaped Circuit (like GHZ state preparation)
267    // In a star circuit, one central tensor connects to many others,
268    // and those others have few connections
269    if is_star_circuit(&tensor_connections, &tensor_ids) {
270        // For star circuits, we should contract the leaf nodes with the central node
271        let mut steps = Vec::new();
272        let mut cost = 0.0;
273
274        // Find the central tensor (the one with most connections)
275        let central = find_central_tensor(&tensor_connections);
276
277        // Contract all leaf tensors with the central one
278        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(&central))
285            })
286            .copied()
287            .collect();
288
289        for leaf in leaf_tensors {
290            steps.push((central, leaf));
291            cost += 16.0; // Simplified cost model
292        }
293
294        return Some(ContractionPath::new(steps, cost));
295    }
296
297    // Pattern 3: Quantum Fourier Transform (QFT) Circuit
298    // QFT has a specific pattern of controlled-phase gates
299    if is_qft_circuit(&tensor_connections, tensors) {
300        return Some(optimize_qft_circuit(&tensor_connections, tensors));
301    }
302
303    // Pattern 4: QAOA Circuit
304    // QAOA has alternating layers of problem and mixer Hamiltonians
305    if is_qaoa_circuit(&tensor_connections, tensors) {
306        return Some(optimize_qaoa_circuit(&tensor_connections, tensors));
307    }
308
309    // No special structure identified
310    None
311}
312
313/// Check if the tensor network represents a Quantum Fourier Transform circuit
314fn is_qft_circuit(
315    tensor_connections: &HashMap<usize, HashSet<usize>>,
316    tensors: &HashMap<usize, Tensor>,
317) -> bool {
318    // QFT typically has a triangular pattern of controlled-phase gates
319    // followed by Hadamard gates and swaps
320
321    // Count gate types and specific patterns that indicate a QFT structure
322    let mut hadamard_count = 0;
323    let mut controlled_phase_count = 0;
324    let mut swap_count = 0;
325
326    // This is a simplified check - a full check would inspect the actual tensor structure
327    for tensor in tensors.values() {
328        // Check dimensions to guess if it's a single-qubit gate (rank 2) or two-qubit gate (rank 4)
329        if tensor.rank == 2 {
330            hadamard_count += 1;
331        } else if tensor.rank == 4 {
332            // Try to classify the two-qubit gate
333            if tensor.dimensions == vec![2, 2, 2, 2] {
334                // Controlled-phase gates have entries at the (0,0), (1,1), (2,2), (3,3) positions
335                // with specific phases - this is a simplified check
336                controlled_phase_count += 1;
337            }
338
339            // Count potential swap gates
340            if is_swap_like_tensor(tensor) {
341                swap_count += 1;
342            }
343        }
344    }
345
346    // A QFT circuit typically has Hadamard gates on all qubits and controlled-phase gates
347    // The specific pattern is a Hadamard gate on each qubit, followed by controlled-phase gates
348    // with decreasing rotation angles, and finally SWAP gates to reverse the qubits
349
350    // This is a simplified heuristic
351    hadamard_count > 0 && controlled_phase_count > 0 && hadamard_count >= controlled_phase_count / 2
352}
353
354/// Check if a tensor might represent a SWAP-like operation
355fn is_swap_like_tensor(tensor: &Tensor) -> bool {
356    // SWAP gates have a pattern where the permutation of indices is non-trivial
357    // This is a simplified check - a full check would inspect the actual tensor values
358    tensor.rank == 4 && tensor.dimensions == vec![2, 2, 2, 2]
359}
360
361/// Generate an optimized contraction path for a QFT circuit
362fn optimize_qft_circuit(
363    tensor_connections: &HashMap<usize, HashSet<usize>>,
364    tensors: &HashMap<usize, Tensor>,
365) -> ContractionPath {
366    // QFT circuits are best contracted starting from the least significant qubit (bottom)
367    // and working upward. This follows the natural decomposition of the QFT.
368
369    // Build the tensor IDs in the desired contraction order
370    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    // Sort tensors by their connectivity pattern
375    // In a QFT, we want to contract from bottom to top for optimal efficiency
376    let mut steps = Vec::new();
377    let mut cost = 0.0;
378
379    // This is a simplified implementation - in a full implementation,
380    // we'd analyze the QFT structure more carefully
381
382    // First, try to identify layers of gates in the QFT
383    let mut layers = identify_qft_layers(tensor_connections, &tensor_ids);
384
385    // Contract each layer from bottom to top
386    for layer in layers {
387        // Contract tensors within the layer
388        for i in 0..layer.len().saturating_sub(1) {
389            steps.push((layer[i], layer[i + 1]));
390            cost += 16.0; // Simplified cost model
391        }
392    }
393
394    // If we couldn't identify layers properly, fall back to a basic contraction strategy
395    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
405/// Identify layers of a QFT circuit for optimal contraction
406fn identify_qft_layers(
407    tensor_connections: &HashMap<usize, HashSet<usize>>,
408    tensor_ids: &[usize],
409) -> Vec<Vec<usize>> {
410    // Group tensors into layers based on their connections
411    // In a QFT, we expect a specific pattern of connections between gates
412
413    // This is a simplified implementation - in a real QFT optimizer,
414    // we'd analyze the structure more carefully
415
416    // For now, just group tensors by their degree (number of connections)
417    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    // Order the groups by degree (descending)
425    let mut degrees: Vec<usize> = degree_groups.keys().copied().collect();
426    degrees.sort_by(|a, b| b.cmp(a));
427
428    // Create layers based on degree groups
429    let mut layers = Vec::new();
430    for degree in degrees {
431        if let Some(group) = degree_groups.get(&degree) {
432            layers.push(group.clone());
433        }
434    }
435
436    layers
437}
438
439/// Check if the tensor network represents a QAOA circuit
440fn is_qaoa_circuit(
441    tensor_connections: &HashMap<usize, HashSet<usize>>,
442    tensors: &HashMap<usize, Tensor>,
443) -> bool {
444    // QAOA has alternating layers of problem Hamiltonian (typically ZZ interactions)
445    // and mixer Hamiltonian (typically X rotations)
446
447    // Count gate types associated with QAOA
448    let mut x_rotation_count = 0;
449    let mut zz_interaction_count = 0;
450
451    // This is a simplified check - a full check would inspect the actual tensor structure
452    for tensor in tensors.values() {
453        // Single-qubit gate (possibly X rotation)
454        if tensor.rank == 2 {
455            x_rotation_count += 1; // Assume some are X rotations
456        }
457        // Two-qubit gate (possibly ZZ interaction)
458        else if tensor.rank == 4 {
459            zz_interaction_count += 1; // Assume some are ZZ interactions
460        }
461    }
462
463    // QAOA typically has alternating layers of problem and mixer Hamiltonians,
464    // so we expect to see both ZZ interactions and X rotations
465    x_rotation_count > 0 && zz_interaction_count > 0
466}
467
468/// Generate an optimized contraction path for a QAOA circuit
469fn optimize_qaoa_circuit(
470    tensor_connections: &HashMap<usize, HashSet<usize>>,
471    tensors: &HashMap<usize, Tensor>,
472) -> ContractionPath {
473    // For QAOA circuits, we want to prioritize contracting the problem Hamiltonian terms
474    // (typically ZZ interactions) before the mixer Hamiltonian terms (X rotations)
475
476    // First, sort tensors by rank (higher rank first)
477    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) // Higher rank first
481        } else {
482            std::cmp::Ordering::Equal
483        }
484    });
485
486    // Group tensors by rank (for QAOA, rank 4 = two-qubit gates, rank 2 = single-qubit gates)
487    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    // Create contraction steps prioritizing two-qubit gates (ZZ interactions)
496    let mut steps = Vec::new();
497    let mut cost = 0.0;
498
499    // First, contract the two-qubit gates (problem Hamiltonian)
500    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                // Check if these tensors are connected
504                if tensor_connections
505                    .get(&id1)
506                    .is_some_and(|conns| conns.contains(&id2))
507                {
508                    steps.push((id1, id2));
509                    cost += 64.0; // Higher cost for two-qubit gate contraction (2^3 * 2^3)
510                }
511            }
512        }
513    }
514
515    // Then, contract the single-qubit gates (mixer Hamiltonian)
516    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                // Check if these tensors are connected
520                if tensor_connections
521                    .get(&id1)
522                    .is_some_and(|conns| conns.contains(&id2))
523                {
524                    steps.push((id1, id2));
525                    cost += 16.0; // Lower cost for single-qubit gate contraction (2^2 * 2^2)
526                }
527            }
528        }
529    }
530
531    // If no steps were created (no direct connections found),
532    // fall back to a simple sequential contraction
533    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; // Default cost
537        }
538    }
539
540    ContractionPath::new(steps, cost)
541}
542
543/// Check if the tensor network represents a linear circuit
544fn is_linear_circuit(
545    tensor_connections: &HashMap<usize, HashSet<usize>>,
546    tensor_ids: &[usize],
547) -> bool {
548    // Check that most tensors have exactly 2 connections (except the endpoints)
549    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            // If any tensor has more than 2 connections, it's not linear
556            return false;
557        } else if degree == 1 {
558            // Count tensors with only one connection (should be exactly 2 for a chain)
559            num_endpoints += 1;
560        }
561    }
562
563    // A linear circuit should have exactly 2 endpoints
564    num_endpoints == 2
565}
566
567/// Order tensors in a linear circuit from one end to the other
568fn 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    // Find one endpoint
575    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        // Start from this endpoint
586        result.push(start);
587        let mut visited = HashSet::new();
588        visited.insert(start);
589
590        // Keep adding the next unvisited neighbor
591        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                    // No more unvisited neighbors
604                    current = None;
605                }
606            } else {
607                current = None;
608            }
609        }
610    }
611
612    // If we couldn't order it (not actually linear), just return original order
613    if result.len() != tensor_ids.len() {
614        return tensor_ids.to_vec();
615    }
616
617    result
618}
619
620/// Check if the tensor network represents a star-shaped circuit
621fn is_star_circuit(
622    tensor_connections: &HashMap<usize, HashSet<usize>>,
623    tensor_ids: &[usize],
624) -> bool {
625    // Count degrees of each tensor
626    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    // A star circuit has one central node with high degree,
634    // and many leaf nodes with degree 1
635    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    // One high-degree node and multiple degree-1 nodes
639    high_degree == 1 && degree_one > 2
640}
641
642/// Find the central tensor in a star-shaped circuit
643fn 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
658/// Extract the shared axis pairs between two tensors from a connection list.
659///
660/// Each returned `(axis_in_id1, axis_in_id2)` corresponds to a bond linking the
661/// two tensors, oriented so the first element indexes `id1` and the second
662/// `id2`.
663pub(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
679/// Contract two tensors over a set of shared index pairs, summing over all of
680/// them simultaneously (a multi-index Einstein summation).
681///
682/// `shared[i] = (axis_in_a, axis_in_b)` lists the axis pairs to contract; the
683/// two axes in each pair must have equal dimension. The result's axes are `a`'s
684/// non-contracted axes (in order) followed by `b`'s non-contracted axes (in
685/// order). With an empty `shared` slice this is the outer (tensor) product.
686///
687/// This is the genuine contraction primitive used to execute contraction paths;
688/// it performs real arithmetic over the tensor data rather than returning an
689/// existing tensor.
690pub(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            // Keep only element pairs whose shared indices agree.
736            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
765/// A tensor together with a global label for each of its axes. Two axes that
766/// carry the same label are bonded and get summed over when their tensors meet.
767struct LabeledTensor {
768    tensor: Tensor,
769    labels: Vec<usize>,
770}
771
772/// Contract a tensor network to a single tensor by executing the given
773/// contraction `path`.
774///
775/// The `connections` list defines the bonds of the network; each bond is turned
776/// into a shared axis label. The `path` gives the order of pairwise
777/// contractions — for each step `(id1, id2)` the two tensors are contracted over
778/// all axes they share (matching labels), and the merged tensor takes the place
779/// of `id1` (the same "merge into the first ID" convention produced by
780/// [`calculate_greedy_contraction_path`]). Any tensors left uncontracted by the
781/// path are folded together via outer products.
782///
783/// This performs the real contraction arithmetic via [`contract_pair_multi`];
784/// it no longer returns an arbitrary existing tensor.
785pub 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; // IDs follow the path's merge-into-first convention.
792
793    if tensors.is_empty() {
794        return Err(QuantRS2Error::CircuitValidationFailed(
795            "contract_network_along_path: empty tensor network".to_string(),
796        ));
797    }
798
799    // Assign a fresh unique label to every axis of every tensor.
800    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    // Bond the endpoints of each connection by giving them a shared label.
820    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    // Execute the path, contracting each pair over their shared (matching-label)
836    // axes and storing the merged tensor under the first ID.
837    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(&lt1, &lt2)?;
853        working.insert(id1, merged);
854    }
855
856    // Fold any remaining tensors (a well-formed path leaves exactly one).
857    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, &lt)?;
861    }
862
863    Ok(acc.tensor)
864}
865
866/// Contract two labeled tensors over every axis pair whose labels match,
867/// returning the merged tensor with its surviving axis labels.
868fn 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}