Skip to main content

quantrs2_sim/
parallel_tensor_optimization.rs

1//! Parallel Tensor Network Optimization
2//!
3//! This module provides advanced parallel processing strategies for tensor network
4//! contractions, optimizing for modern multi-core and distributed architectures.
5
6use crate::prelude::SimulatorError;
7use scirs2_core::ndarray::{ArrayD, Dimension, IxDyn};
8use scirs2_core::parallel_ops::{
9    current_num_threads, IndexedParallelIterator, ParallelIterator, ThreadPool, ThreadPoolBuilder,
10};
11use scirs2_core::Complex64;
12use std::collections::{HashMap, HashSet, VecDeque};
13use std::sync::{Arc, Mutex, RwLock};
14use std::thread;
15use std::time::{Duration, Instant};
16
17use crate::error::Result;
18
19/// Parallel processing configuration for tensor networks
20#[derive(Debug, Clone)]
21pub struct ParallelTensorConfig {
22    /// Number of worker threads
23    pub num_threads: usize,
24    /// Chunk size for parallel operations
25    pub chunk_size: usize,
26    /// Enable work-stealing between threads
27    pub enable_work_stealing: bool,
28    /// Memory threshold for switching to parallel mode
29    pub parallel_threshold_bytes: usize,
30    /// Load balancing strategy
31    pub load_balancing: LoadBalancingStrategy,
32    /// Enable NUMA-aware scheduling
33    pub numa_aware: bool,
34    /// Thread affinity settings
35    pub thread_affinity: ThreadAffinityConfig,
36}
37
38impl Default for ParallelTensorConfig {
39    fn default() -> Self {
40        Self {
41            num_threads: current_num_threads(), // SciRS2 POLICY compliant
42            chunk_size: 1024,
43            enable_work_stealing: true,
44            parallel_threshold_bytes: 1024 * 1024, // 1MB
45            load_balancing: LoadBalancingStrategy::DynamicWorkStealing,
46            numa_aware: true,
47            thread_affinity: ThreadAffinityConfig::default(),
48        }
49    }
50}
51
52/// Load balancing strategies for parallel tensor operations
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum LoadBalancingStrategy {
55    /// Static round-robin distribution
56    RoundRobin,
57    /// Dynamic work-stealing
58    DynamicWorkStealing,
59    /// NUMA-aware distribution
60    NumaAware,
61    /// Cost-based distribution
62    CostBased,
63    /// Adaptive strategy selection
64    Adaptive,
65}
66
67/// Thread affinity configuration
68#[derive(Debug, Clone, Default)]
69pub struct ThreadAffinityConfig {
70    /// Enable CPU affinity
71    pub enable_affinity: bool,
72    /// CPU core mapping
73    pub core_mapping: Vec<usize>,
74    /// NUMA node preferences
75    pub numa_preferences: HashMap<usize, usize>,
76}
77
78/// Work unit for parallel tensor contraction
79#[derive(Debug, Clone)]
80pub struct TensorWorkUnit {
81    /// Unique identifier for the work unit
82    pub id: usize,
83    /// Input tensor indices
84    pub input_tensors: Vec<usize>,
85    /// Output tensor index
86    pub output_tensor: usize,
87    /// Contraction indices
88    pub contraction_indices: Vec<Vec<usize>>,
89    /// Estimated computational cost
90    pub estimated_cost: f64,
91    /// Memory requirement
92    pub memory_requirement: usize,
93    /// Dependencies (must complete before this unit)
94    pub dependencies: HashSet<usize>,
95    /// Priority level
96    pub priority: i32,
97}
98
99/// Work queue for managing parallel tensor operations
100#[derive(Debug)]
101pub struct TensorWorkQueue {
102    /// Pending work units
103    pending: Mutex<VecDeque<TensorWorkUnit>>,
104    /// Completed work units
105    completed: RwLock<HashSet<usize>>,
106    /// Work units in progress
107    in_progress: RwLock<HashMap<usize, Instant>>,
108    /// Total work units
109    total_units: usize,
110    /// Configuration
111    config: ParallelTensorConfig,
112}
113
114impl TensorWorkQueue {
115    /// Create new work queue
116    #[must_use]
117    pub fn new(work_units: Vec<TensorWorkUnit>, config: ParallelTensorConfig) -> Self {
118        let total_units = work_units.len();
119        let mut pending = VecDeque::from(work_units);
120
121        // Sort by priority and dependencies
122        pending.make_contiguous().sort_by(|a, b| {
123            b.priority
124                .cmp(&a.priority)
125                .then_with(|| a.dependencies.len().cmp(&b.dependencies.len()))
126        });
127
128        Self {
129            pending: Mutex::new(pending),
130            completed: RwLock::new(HashSet::new()),
131            in_progress: RwLock::new(HashMap::new()),
132            total_units,
133            config,
134        }
135    }
136
137    /// Get next available work unit
138    pub fn get_work(&self) -> Option<TensorWorkUnit> {
139        // Use expect() for lock poisoning - if locks are poisoned, we have a bigger problem
140        let mut pending = self
141            .pending
142            .lock()
143            .expect("pending lock should not be poisoned");
144        let completed = self
145            .completed
146            .read()
147            .expect("completed lock should not be poisoned");
148
149        // Find a work unit whose dependencies are satisfied
150        for i in 0..pending.len() {
151            let work_unit = &pending[i];
152            let dependencies_satisfied = work_unit
153                .dependencies
154                .iter()
155                .all(|dep| completed.contains(dep));
156
157            if dependencies_satisfied {
158                // Safety: i is always within bounds (from for loop condition)
159                let work_unit = pending
160                    .remove(i)
161                    .expect("index i is guaranteed to be within bounds");
162
163                // Mark as in progress
164                drop(completed);
165                let mut in_progress = self
166                    .in_progress
167                    .write()
168                    .expect("in_progress lock should not be poisoned");
169                in_progress.insert(work_unit.id, Instant::now());
170
171                return Some(work_unit);
172            }
173        }
174
175        None
176    }
177
178    /// Mark work unit as completed
179    pub fn complete_work(&self, work_id: usize) {
180        let mut completed = self
181            .completed
182            .write()
183            .expect("completed lock should not be poisoned");
184        completed.insert(work_id);
185
186        let mut in_progress = self
187            .in_progress
188            .write()
189            .expect("in_progress lock should not be poisoned");
190        in_progress.remove(&work_id);
191    }
192
193    /// Check if all work is completed
194    pub fn is_complete(&self) -> bool {
195        let completed = self
196            .completed
197            .read()
198            .expect("completed lock should not be poisoned");
199        completed.len() == self.total_units
200    }
201
202    /// Get progress statistics
203    pub fn get_progress(&self) -> (usize, usize, usize) {
204        let completed = self
205            .completed
206            .read()
207            .expect("completed lock should not be poisoned")
208            .len();
209        let in_progress = self
210            .in_progress
211            .read()
212            .expect("in_progress lock should not be poisoned")
213            .len();
214        let pending = self
215            .pending
216            .lock()
217            .expect("pending lock should not be poisoned")
218            .len();
219        (completed, in_progress, pending)
220    }
221}
222
223/// Parallel tensor contraction engine
224pub struct ParallelTensorEngine {
225    /// Configuration
226    config: ParallelTensorConfig,
227    /// Worker thread pool
228    thread_pool: ThreadPool, // SciRS2 POLICY compliant
229    /// Performance statistics
230    stats: Arc<Mutex<ParallelTensorStats>>,
231}
232
233/// Performance statistics for parallel tensor operations
234#[derive(Debug, Clone, Default)]
235pub struct ParallelTensorStats {
236    /// Total contractions performed
237    pub total_contractions: u64,
238    /// Total computation time
239    pub total_computation_time: Duration,
240    /// Total parallel efficiency (0.0 to 1.0)
241    pub parallel_efficiency: f64,
242    /// Memory usage statistics
243    pub peak_memory_usage: usize,
244    /// Thread utilization statistics
245    pub thread_utilization: Vec<f64>,
246    /// Load balancing effectiveness
247    pub load_balance_factor: f64,
248    /// Cache hit rate for intermediate results
249    pub cache_hit_rate: f64,
250}
251
252impl ParallelTensorEngine {
253    /// Create new parallel tensor engine
254    pub fn new(config: ParallelTensorConfig) -> Result<Self> {
255        let thread_pool = ThreadPoolBuilder::new() // SciRS2 POLICY compliant
256            .num_threads(config.num_threads)
257            .build()
258            .map_err(|e| {
259                SimulatorError::InitializationFailed(format!("Thread pool creation failed: {e}"))
260            })?;
261
262        Ok(Self {
263            config,
264            thread_pool,
265            stats: Arc::new(Mutex::new(ParallelTensorStats::default())),
266        })
267    }
268
269    /// Perform parallel tensor network contraction
270    pub fn contract_network(
271        &self,
272        tensors: &[ArrayD<Complex64>],
273        contraction_sequence: &[ContractionPair],
274    ) -> Result<ArrayD<Complex64>> {
275        let start_time = Instant::now();
276
277        // Create work units from contraction sequence
278        let work_units = self.create_work_units(tensors, contraction_sequence)?;
279
280        // Create work queue
281        let work_queue = Arc::new(TensorWorkQueue::new(work_units, self.config.clone()));
282
283        // Storage for intermediate results
284        let intermediate_results =
285            Arc::new(RwLock::new(HashMap::<usize, ArrayD<Complex64>>::new()));
286
287        // Initialize with input tensors
288        {
289            let mut results = intermediate_results
290                .write()
291                .expect("intermediate_results lock should not be poisoned");
292            for (i, tensor) in tensors.iter().enumerate() {
293                results.insert(i, tensor.clone());
294            }
295        }
296
297        // Execute contractions in parallel
298        let final_result = self.execute_parallel_contractions(work_queue, intermediate_results)?;
299
300        // Update statistics
301        let elapsed = start_time.elapsed();
302        let mut stats = self
303            .stats
304            .lock()
305            .expect("stats lock should not be poisoned");
306        stats.total_contractions += contraction_sequence.len() as u64;
307        stats.total_computation_time += elapsed;
308
309        // Calculate parallel efficiency (simplified)
310        let sequential_estimate = self.estimate_sequential_time(contraction_sequence);
311        stats.parallel_efficiency = sequential_estimate.as_secs_f64() / elapsed.as_secs_f64();
312
313        Ok(final_result)
314    }
315
316    /// Create work units from contraction sequence
317    fn create_work_units(
318        &self,
319        tensors: &[ArrayD<Complex64>],
320        contraction_sequence: &[ContractionPair],
321    ) -> Result<Vec<TensorWorkUnit>> {
322        let mut work_units: Vec<TensorWorkUnit> = Vec::new();
323        let mut next_tensor_id = tensors.len();
324
325        for (i, contraction) in contraction_sequence.iter().enumerate() {
326            let estimated_cost = self.estimate_contraction_cost(contraction, tensors)?;
327            let memory_requirement = self.estimate_memory_requirement(contraction, tensors)?;
328
329            // Determine dependencies
330            let mut dependencies = HashSet::new();
331            for &input_id in &[contraction.tensor1_id, contraction.tensor2_id] {
332                if input_id >= tensors.len() {
333                    // This is an intermediate result, find which work unit produces it
334                    for prev_unit in &work_units {
335                        if prev_unit.output_tensor == input_id {
336                            dependencies.insert(prev_unit.id);
337                            break;
338                        }
339                    }
340                }
341            }
342
343            let work_unit = TensorWorkUnit {
344                id: i,
345                input_tensors: vec![contraction.tensor1_id, contraction.tensor2_id],
346                output_tensor: next_tensor_id,
347                contraction_indices: vec![
348                    contraction.tensor1_indices.clone(),
349                    contraction.tensor2_indices.clone(),
350                ],
351                estimated_cost,
352                memory_requirement,
353                dependencies,
354                priority: self.calculate_priority(estimated_cost, memory_requirement),
355            };
356
357            work_units.push(work_unit);
358            next_tensor_id += 1;
359        }
360
361        Ok(work_units)
362    }
363
364    /// Execute parallel contractions using work queue
365    fn execute_parallel_contractions(
366        &self,
367        work_queue: Arc<TensorWorkQueue>,
368        intermediate_results: Arc<RwLock<HashMap<usize, ArrayD<Complex64>>>>,
369    ) -> Result<ArrayD<Complex64>> {
370        let num_threads = self.config.num_threads;
371        let mut handles = Vec::new();
372
373        // Spawn worker threads
374        for thread_id in 0..num_threads {
375            let work_queue = work_queue.clone();
376            let intermediate_results = intermediate_results.clone();
377            let config = self.config.clone();
378
379            let handle = thread::spawn(move || {
380                Self::worker_thread(thread_id, work_queue, intermediate_results, config)
381            });
382            handles.push(handle);
383        }
384
385        // Wait for all threads to complete
386        for handle in handles {
387            handle.join().map_err(|e| {
388                SimulatorError::ComputationError(format!("Thread join failed: {e:?}"))
389            })??;
390        }
391
392        // Find the final result (tensor with highest ID)
393        let results = intermediate_results
394            .read()
395            .expect("intermediate_results lock should not be poisoned");
396        let max_id = results.keys().max().copied().unwrap_or(0);
397        Ok(results[&max_id].clone())
398    }
399
400    /// Worker thread function
401    fn worker_thread(
402        _thread_id: usize,
403        work_queue: Arc<TensorWorkQueue>,
404        intermediate_results: Arc<RwLock<HashMap<usize, ArrayD<Complex64>>>>,
405        _config: ParallelTensorConfig,
406    ) -> Result<()> {
407        while !work_queue.is_complete() {
408            if let Some(work_unit) = work_queue.get_work() {
409                // Get input tensors
410                let tensor1 = {
411                    let results = intermediate_results
412                        .read()
413                        .expect("intermediate_results lock should not be poisoned");
414                    results[&work_unit.input_tensors[0]].clone()
415                };
416
417                let tensor2 = {
418                    let results = intermediate_results
419                        .read()
420                        .expect("intermediate_results lock should not be poisoned");
421                    results[&work_unit.input_tensors[1]].clone()
422                };
423
424                // Perform contraction
425                let result = Self::perform_tensor_contraction(
426                    &tensor1,
427                    &tensor2,
428                    &work_unit.contraction_indices[0],
429                    &work_unit.contraction_indices[1],
430                )?;
431
432                // Store result
433                {
434                    let mut results = intermediate_results
435                        .write()
436                        .expect("intermediate_results lock should not be poisoned");
437                    results.insert(work_unit.output_tensor, result);
438                }
439
440                // Mark work as completed
441                work_queue.complete_work(work_unit.id);
442            } else {
443                // No work available, wait briefly
444                thread::sleep(Duration::from_millis(1));
445            }
446        }
447
448        Ok(())
449    }
450
451    /// Perform an einsum-style pairwise tensor contraction.
452    ///
453    /// Contracts axes `indices1` of `tensor1` against axes `indices2` of
454    /// `tensor2` (paired positionally, so `indices1[k]` is summed with
455    /// `indices2[k]` and their dimensions must match). The output carries the
456    /// free (uncontracted) axes of `tensor1` followed by those of `tensor2`:
457    ///
458    /// `out[f1, f2] = Σ_c tensor1[f1, c] * tensor2[f2, c]`
459    ///
460    /// where `c` ranges over all combinations of the contracted axes.
461    fn perform_tensor_contraction(
462        tensor1: &ArrayD<Complex64>,
463        tensor2: &ArrayD<Complex64>,
464        indices1: &[usize],
465        indices2: &[usize],
466    ) -> Result<ArrayD<Complex64>> {
467        let shape1 = tensor1.shape().to_vec();
468        let shape2 = tensor2.shape().to_vec();
469
470        if indices1.len() != indices2.len() {
471            return Err(SimulatorError::InvalidInput(format!(
472                "Contraction requires equal numbers of contracted axes, got {} and {}",
473                indices1.len(),
474                indices2.len()
475            )));
476        }
477        for (&a, &b) in indices1.iter().zip(indices2.iter()) {
478            if a >= shape1.len() || b >= shape2.len() {
479                return Err(SimulatorError::InvalidInput(format!(
480                    "Contracted axis out of range: tensor1 axis {a} (rank {}), tensor2 axis {b} (rank {})",
481                    shape1.len(),
482                    shape2.len()
483                )));
484            }
485            if shape1[a] != shape2[b] {
486                return Err(SimulatorError::InvalidInput(format!(
487                    "Contracted dimension mismatch: tensor1 axis {a} has size {}, tensor2 axis {b} has size {}",
488                    shape1[a], shape2[b]
489                )));
490            }
491        }
492
493        // Partition axes into free (kept) and contracted (summed).
494        let free1: Vec<usize> = (0..shape1.len())
495            .filter(|i| !indices1.contains(i))
496            .collect();
497        let free2: Vec<usize> = (0..shape2.len())
498            .filter(|i| !indices2.contains(i))
499            .collect();
500
501        let free1_dims: Vec<usize> = free1.iter().map(|&i| shape1[i]).collect();
502        let free2_dims: Vec<usize> = free2.iter().map(|&i| shape2[i]).collect();
503        let contracted_dims: Vec<usize> = indices1.iter().map(|&i| shape1[i]).collect();
504
505        let output_shape: Vec<usize> = free1_dims
506            .iter()
507            .copied()
508            .chain(free2_dims.iter().copied())
509            .collect();
510        let mut output = ArrayD::zeros(IxDyn(&output_shape));
511
512        let free1_size: usize = free1_dims.iter().product::<usize>().max(1);
513        let free2_size: usize = free2_dims.iter().product::<usize>().max(1);
514        let contracted_size: usize = contracted_dims.iter().product::<usize>().max(1);
515
516        // Reusable multi-index scratch buffers.
517        let mut idx1 = vec![0usize; shape1.len()];
518        let mut idx2 = vec![0usize; shape2.len()];
519        let mut out_idx = vec![0usize; output_shape.len()];
520
521        for f1 in 0..free1_size {
522            let f1_idx = unravel_index(f1, &free1_dims);
523            for (pos, &axis) in free1.iter().enumerate() {
524                idx1[axis] = f1_idx[pos];
525                out_idx[pos] = f1_idx[pos];
526            }
527            for f2 in 0..free2_size {
528                let f2_idx = unravel_index(f2, &free2_dims);
529                for (pos, &axis) in free2.iter().enumerate() {
530                    idx2[axis] = f2_idx[pos];
531                    out_idx[free1.len() + pos] = f2_idx[pos];
532                }
533
534                let mut acc = Complex64::new(0.0, 0.0);
535                for c in 0..contracted_size {
536                    let c_idx = unravel_index(c, &contracted_dims);
537                    for (pos, (&a, &b)) in indices1.iter().zip(indices2.iter()).enumerate() {
538                        idx1[a] = c_idx[pos];
539                        idx2[b] = c_idx[pos];
540                    }
541                    acc += tensor1[IxDyn(&idx1)] * tensor2[IxDyn(&idx2)];
542                }
543                output[IxDyn(&out_idx)] = acc;
544            }
545        }
546
547        Ok(output)
548    }
549
550    /// Estimate computational cost of a contraction
551    fn estimate_contraction_cost(
552        &self,
553        contraction: &ContractionPair,
554        _tensors: &[ArrayD<Complex64>],
555    ) -> Result<f64> {
556        // Simplified cost estimation based on dimension products
557        let cost = contraction.tensor1_indices.len() as f64
558            * contraction.tensor2_indices.len() as f64
559            * 1000.0; // Base cost factor
560        Ok(cost)
561    }
562
563    /// Estimate memory requirement for a contraction
564    const fn estimate_memory_requirement(
565        &self,
566        _contraction: &ContractionPair,
567        _tensors: &[ArrayD<Complex64>],
568    ) -> Result<usize> {
569        // Simplified memory estimation
570        Ok(1024 * 1024) // 1MB placeholder
571    }
572
573    /// Calculate priority for work unit
574    fn calculate_priority(&self, cost: f64, memory: usize) -> i32 {
575        // Higher cost and lower memory = higher priority
576        let cost_factor = (cost / 1000.0) as i32;
577        let memory_factor = (1_000_000 / (memory + 1)) as i32;
578        cost_factor + memory_factor
579    }
580
581    /// Estimate sequential execution time
582    const fn estimate_sequential_time(&self, contraction_sequence: &[ContractionPair]) -> Duration {
583        let estimated_ops = contraction_sequence.len() as u64 * 1000; // Simplified
584        Duration::from_millis(estimated_ops)
585    }
586
587    /// Get performance statistics
588    #[must_use]
589    pub fn get_stats(&self) -> ParallelTensorStats {
590        self.stats
591            .lock()
592            .expect("stats lock should not be poisoned")
593            .clone()
594    }
595}
596
597/// Contraction pair specification
598#[derive(Debug, Clone)]
599pub struct ContractionPair {
600    /// First tensor ID
601    pub tensor1_id: usize,
602    /// Second tensor ID
603    pub tensor2_id: usize,
604    /// Indices to contract on first tensor
605    pub tensor1_indices: Vec<usize>,
606    /// Indices to contract on second tensor
607    pub tensor2_indices: Vec<usize>,
608}
609
610/// Advanced parallel tensor contraction strategies
611pub mod strategies {
612    use super::{
613        ArrayD, Complex64, ContractionPair, LoadBalancingStrategy, NumaTopology,
614        ParallelTensorConfig, ParallelTensorEngine, Result,
615    };
616
617    /// Work-stealing parallel contraction
618    pub fn work_stealing_contraction(
619        tensors: &[ArrayD<Complex64>],
620        contraction_sequence: &[ContractionPair],
621        num_threads: usize,
622    ) -> Result<ArrayD<Complex64>> {
623        let config = ParallelTensorConfig {
624            num_threads,
625            load_balancing: LoadBalancingStrategy::DynamicWorkStealing,
626            ..Default::default()
627        };
628
629        let engine = ParallelTensorEngine::new(config)?;
630        engine.contract_network(tensors, contraction_sequence)
631    }
632
633    /// NUMA-aware parallel contraction
634    pub fn numa_aware_contraction(
635        tensors: &[ArrayD<Complex64>],
636        contraction_sequence: &[ContractionPair],
637        numa_topology: &NumaTopology,
638    ) -> Result<ArrayD<Complex64>> {
639        let config = ParallelTensorConfig {
640            load_balancing: LoadBalancingStrategy::NumaAware,
641            numa_aware: true,
642            ..Default::default()
643        };
644
645        let engine = ParallelTensorEngine::new(config)?;
646        engine.contract_network(tensors, contraction_sequence)
647    }
648
649    /// Adaptive parallel contraction with dynamic load balancing
650    pub fn adaptive_contraction(
651        tensors: &[ArrayD<Complex64>],
652        contraction_sequence: &[ContractionPair],
653    ) -> Result<ArrayD<Complex64>> {
654        let config = ParallelTensorConfig {
655            load_balancing: LoadBalancingStrategy::Adaptive,
656            enable_work_stealing: true,
657            ..Default::default()
658        };
659
660        let engine = ParallelTensorEngine::new(config)?;
661        engine.contract_network(tensors, contraction_sequence)
662    }
663}
664
665/// NUMA topology information
666#[derive(Debug, Clone)]
667pub struct NumaTopology {
668    /// Number of NUMA nodes
669    pub num_nodes: usize,
670    /// CPU cores per node
671    pub cores_per_node: Vec<usize>,
672    /// Memory per node (bytes)
673    pub memory_per_node: Vec<usize>,
674}
675
676impl Default for NumaTopology {
677    fn default() -> Self {
678        let num_cores = current_num_threads(); // SciRS2 POLICY compliant
679        Self {
680            num_nodes: 1,
681            cores_per_node: vec![num_cores],
682            memory_per_node: vec![8 * 1024 * 1024 * 1024], // 8GB default
683        }
684    }
685}
686
687/// Convert a row-major linear index into its multi-index for the given `dims`.
688///
689/// An empty `dims` slice yields an empty multi-index (the single element of a
690/// zero-dimensional / scalar axis set).
691fn unravel_index(mut linear: usize, dims: &[usize]) -> Vec<usize> {
692    let mut multi = vec![0usize; dims.len()];
693    for i in (0..dims.len()).rev() {
694        let d = dims[i];
695        multi[i] = linear % d;
696        linear /= d;
697    }
698    multi
699}
700
701#[cfg(test)]
702mod tests {
703    use super::*;
704    use scirs2_core::ndarray::Array;
705
706    #[test]
707    fn test_parallel_tensor_engine() {
708        let config = ParallelTensorConfig::default();
709        let engine =
710            ParallelTensorEngine::new(config).expect("should create parallel tensor engine");
711
712        // Create simple test tensors
713        let tensor1 = Array::zeros(IxDyn(&[2, 2]));
714        let tensor2 = Array::zeros(IxDyn(&[2, 2]));
715        let tensors = vec![tensor1, tensor2];
716
717        // Simple contraction sequence
718        let contraction = ContractionPair {
719            tensor1_id: 0,
720            tensor2_id: 1,
721            tensor1_indices: vec![1],
722            tensor2_indices: vec![0],
723        };
724
725        let result = engine.contract_network(&tensors, &[contraction]);
726        assert!(result.is_ok());
727    }
728
729    #[test]
730    fn test_work_queue() {
731        let work_unit = TensorWorkUnit {
732            id: 0,
733            input_tensors: vec![0, 1],
734            output_tensor: 2,
735            contraction_indices: vec![vec![0], vec![1]],
736            estimated_cost: 100.0,
737            memory_requirement: 1024,
738            dependencies: HashSet::new(),
739            priority: 1,
740        };
741
742        let config = ParallelTensorConfig::default();
743        let queue = TensorWorkQueue::new(vec![work_unit], config);
744
745        let work = queue.get_work();
746        assert!(work.is_some());
747
748        queue.complete_work(0);
749        assert!(queue.is_complete());
750    }
751
752    #[test]
753    fn test_parallel_strategies() {
754        let tensor1 = Array::ones(IxDyn(&[2, 2]));
755        let tensor2 = Array::ones(IxDyn(&[2, 2]));
756        let tensors = vec![tensor1, tensor2];
757
758        let contraction = ContractionPair {
759            tensor1_id: 0,
760            tensor2_id: 1,
761            tensor1_indices: vec![1],
762            tensor2_indices: vec![0],
763        };
764
765        let result = strategies::work_stealing_contraction(&tensors, &[contraction], 2);
766        assert!(result.is_ok());
767    }
768
769    /// Regression: `perform_tensor_contraction` previously returned an all-zero
770    /// placeholder. It must now compute the real contraction; a matrix product
771    /// is the canonical single-index contraction.
772    #[test]
773    fn test_perform_tensor_contraction_matrix_product() {
774        // A (2x3) . B (3x2) via contracting A axis 1 with B axis 0.
775        let a = Array::from_shape_vec(
776            IxDyn(&[2, 3]),
777            vec![
778                Complex64::new(1.0, 0.0),
779                Complex64::new(2.0, 0.0),
780                Complex64::new(3.0, 0.0),
781                Complex64::new(4.0, 0.0),
782                Complex64::new(5.0, 0.0),
783                Complex64::new(6.0, 0.0),
784            ],
785        )
786        .expect("A shape");
787        let b = Array::from_shape_vec(
788            IxDyn(&[3, 2]),
789            vec![
790                Complex64::new(1.0, 0.0),
791                Complex64::new(0.0, 0.0),
792                Complex64::new(0.0, 0.0),
793                Complex64::new(1.0, 0.0),
794                Complex64::new(1.0, 0.0),
795                Complex64::new(1.0, 0.0),
796            ],
797        )
798        .expect("B shape");
799
800        let out = ParallelTensorEngine::perform_tensor_contraction(&a, &b, &[1], &[0])
801            .expect("contraction should succeed");
802        assert_eq!(out.shape(), &[2, 2]);
803        // Expected A.B = [[4,5],[10,11]].
804        assert!((out[IxDyn(&[0, 0])] - Complex64::new(4.0, 0.0)).norm() < 1e-12);
805        assert!((out[IxDyn(&[0, 1])] - Complex64::new(5.0, 0.0)).norm() < 1e-12);
806        assert!((out[IxDyn(&[1, 0])] - Complex64::new(10.0, 0.0)).norm() < 1e-12);
807        assert!((out[IxDyn(&[1, 1])] - Complex64::new(11.0, 0.0)).norm() < 1e-12);
808    }
809
810    /// A contraction with no shared axes is an outer product; verify that too.
811    #[test]
812    fn test_perform_tensor_contraction_outer_product() {
813        let a = Array::from_shape_vec(
814            IxDyn(&[2]),
815            vec![Complex64::new(2.0, 0.0), Complex64::new(3.0, 0.0)],
816        )
817        .expect("A shape");
818        let b = Array::from_shape_vec(
819            IxDyn(&[2]),
820            vec![Complex64::new(5.0, 0.0), Complex64::new(7.0, 0.0)],
821        )
822        .expect("B shape");
823
824        let out = ParallelTensorEngine::perform_tensor_contraction(&a, &b, &[], &[])
825            .expect("outer product should succeed");
826        assert_eq!(out.shape(), &[2, 2]);
827        assert!((out[IxDyn(&[0, 0])] - Complex64::new(10.0, 0.0)).norm() < 1e-12);
828        assert!((out[IxDyn(&[0, 1])] - Complex64::new(14.0, 0.0)).norm() < 1e-12);
829        assert!((out[IxDyn(&[1, 0])] - Complex64::new(15.0, 0.0)).norm() < 1e-12);
830        assert!((out[IxDyn(&[1, 1])] - Complex64::new(21.0, 0.0)).norm() < 1e-12);
831    }
832}