Skip to main content

quantrs2_circuit/optimization/
noise.rs

1//! Noise-aware circuit optimization
2//!
3//! This module provides optimization passes that consider quantum device noise
4//! characteristics when making optimization decisions.
5
6use crate::builder::Circuit;
7use crate::optimization::passes::OptimizationPassExt;
8use crate::optimization::{CircuitMetrics, CostModel, OptimizationPass};
9use crate::routing::CouplingMap;
10use quantrs2_core::gate::single::{PauliX, PauliY};
11use quantrs2_core::{
12    error::{QuantRS2Error, QuantRS2Result},
13    gate::GateOp,
14    qubit::QubitId,
15};
16use serde::{Deserialize, Serialize};
17use std::collections::{HashMap, VecDeque};
18
19/// Noise model for quantum devices
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct NoiseModel {
22    /// Single-qubit gate error rates (per qubit)
23    pub single_qubit_errors: HashMap<usize, f64>,
24    /// Two-qubit gate error rates (per qubit pair)
25    pub two_qubit_errors: HashMap<(usize, usize), f64>,
26    /// T1 coherence times (microseconds)
27    pub t1_times: HashMap<usize, f64>,
28    /// T2 coherence times (microseconds)
29    pub t2_times: HashMap<usize, f64>,
30    /// Readout fidelities
31    pub readout_fidelities: HashMap<usize, f64>,
32    /// Gate execution times (nanoseconds)
33    pub gate_times: HashMap<String, f64>,
34    /// Crosstalk matrix
35    pub crosstalk_matrix: Option<Vec<Vec<f64>>>,
36}
37
38impl NoiseModel {
39    /// Create a new empty noise model
40    #[must_use]
41    pub fn new() -> Self {
42        Self {
43            single_qubit_errors: HashMap::new(),
44            two_qubit_errors: HashMap::new(),
45            t1_times: HashMap::new(),
46            t2_times: HashMap::new(),
47            readout_fidelities: HashMap::new(),
48            gate_times: HashMap::new(),
49            crosstalk_matrix: None,
50        }
51    }
52
53    /// Create a uniform noise model for testing
54    #[must_use]
55    pub fn uniform(num_qubits: usize) -> Self {
56        let mut model = Self::new();
57
58        // Default error rates
59        let single_error = 1e-3;
60        let two_qubit_error = 1e-2;
61        let t1 = 100.0; // microseconds
62        let t2 = 50.0; // microseconds
63        let readout_fidelity = 0.99;
64
65        for i in 0..num_qubits {
66            model.single_qubit_errors.insert(i, single_error);
67            model.t1_times.insert(i, t1);
68            model.t2_times.insert(i, t2);
69            model.readout_fidelities.insert(i, readout_fidelity);
70
71            for j in (i + 1)..num_qubits {
72                model.two_qubit_errors.insert((i, j), two_qubit_error);
73            }
74        }
75
76        // Default gate times (nanoseconds)
77        model.gate_times.insert("H".to_string(), 20.0);
78        model.gate_times.insert("X".to_string(), 20.0);
79        model.gate_times.insert("Y".to_string(), 20.0);
80        model.gate_times.insert("Z".to_string(), 0.0); // Virtual Z
81        model.gate_times.insert("S".to_string(), 0.0);
82        model.gate_times.insert("T".to_string(), 0.0);
83        model.gate_times.insert("CNOT".to_string(), 200.0);
84        model.gate_times.insert("CZ".to_string(), 200.0);
85        model.gate_times.insert("SWAP".to_string(), 600.0); // 3 CNOTs
86
87        model
88    }
89
90    /// Create a realistic noise model based on IBM devices
91    #[must_use]
92    pub fn ibm_like(num_qubits: usize) -> Self {
93        let mut model = Self::new();
94
95        // IBM-like parameters
96        for i in 0..num_qubits {
97            model.single_qubit_errors.insert(i, 1e-4); // Good single-qubit gates
98            model.t1_times.insert(i, (i as f64).mul_add(10.0, 100.0)); // Varying T1
99            model.t2_times.insert(i, (i as f64).mul_add(5.0, 80.0)); // Varying T2
100            model
101                .readout_fidelities
102                .insert(i, 0.95 + (i as f64 * 0.01).min(0.04));
103
104            for j in (i + 1)..num_qubits {
105                // Two-qubit errors vary by connectivity
106                let error = if (i as isize - j as isize).abs() == 1 {
107                    5e-3 // Adjacent qubits
108                } else {
109                    1e-2 // Non-adjacent (if connected)
110                };
111                model.two_qubit_errors.insert((i, j), error);
112            }
113        }
114
115        // IBM-like gate times
116        model.gate_times.insert("H".to_string(), 35.0);
117        model.gate_times.insert("X".to_string(), 35.0);
118        model.gate_times.insert("Y".to_string(), 35.0);
119        model.gate_times.insert("Z".to_string(), 0.0);
120        model.gate_times.insert("S".to_string(), 0.0);
121        model.gate_times.insert("T".to_string(), 0.0);
122        model.gate_times.insert("CNOT".to_string(), 500.0);
123        model.gate_times.insert("CZ".to_string(), 300.0);
124        model.gate_times.insert("SWAP".to_string(), 1500.0);
125
126        model
127    }
128
129    /// Get error rate for a single-qubit gate
130    #[must_use]
131    pub fn single_qubit_error(&self, qubit: usize) -> f64 {
132        self.single_qubit_errors
133            .get(&qubit)
134            .copied()
135            .unwrap_or(1e-3)
136    }
137
138    /// Get error rate for a two-qubit gate
139    #[must_use]
140    pub fn two_qubit_error(&self, q1: usize, q2: usize) -> f64 {
141        let key = (q1.min(q2), q1.max(q2));
142        self.two_qubit_errors.get(&key).copied().unwrap_or(1e-2)
143    }
144
145    /// Get T1 time for a qubit
146    #[must_use]
147    pub fn t1_time(&self, qubit: usize) -> f64 {
148        self.t1_times.get(&qubit).copied().unwrap_or(100.0)
149    }
150
151    /// Get T2 time for a qubit
152    #[must_use]
153    pub fn t2_time(&self, qubit: usize) -> f64 {
154        self.t2_times.get(&qubit).copied().unwrap_or(50.0)
155    }
156
157    /// Get gate execution time
158    #[must_use]
159    pub fn gate_time(&self, gate_name: &str) -> f64 {
160        self.gate_times.get(gate_name).copied().unwrap_or(100.0)
161    }
162
163    /// Calculate error probability for a gate
164    pub fn gate_error_probability(&self, gate: &dyn GateOp) -> f64 {
165        let qubits = gate.qubits();
166        match qubits.len() {
167            1 => self.single_qubit_error(qubits[0].id() as usize),
168            2 => self.two_qubit_error(qubits[0].id() as usize, qubits[1].id() as usize),
169            _ => 0.1, // Multi-qubit gates are expensive
170        }
171    }
172}
173
174impl Default for NoiseModel {
175    fn default() -> Self {
176        Self::new()
177    }
178}
179
180/// Noise-aware cost model that considers device characteristics
181#[derive(Debug, Clone)]
182pub struct NoiseAwareCostModel {
183    noise_model: NoiseModel,
184    coupling_map: Option<CouplingMap>,
185    /// Weight for error rate in cost calculation
186    pub error_weight: f64,
187    /// Weight for execution time in cost calculation
188    pub time_weight: f64,
189    /// Weight for coherence effects
190    pub coherence_weight: f64,
191}
192
193impl NoiseAwareCostModel {
194    /// Create a new noise-aware cost model
195    #[must_use]
196    pub const fn new(noise_model: NoiseModel) -> Self {
197        Self {
198            noise_model,
199            coupling_map: None,
200            error_weight: 1000.0,
201            time_weight: 1.0,
202            coherence_weight: 100.0,
203        }
204    }
205
206    /// Set the coupling map for connectivity analysis
207    #[must_use]
208    pub fn with_coupling_map(mut self, coupling_map: CouplingMap) -> Self {
209        self.coupling_map = Some(coupling_map);
210        self
211    }
212
213    /// Calculate cost for a single gate
214    pub fn gate_cost(&self, gate: &dyn GateOp) -> f64 {
215        let error_prob = self.noise_model.gate_error_probability(gate);
216        let exec_time = self.noise_model.gate_time(gate.name());
217
218        // Base cost from error probability and execution time
219        let mut cost = self
220            .error_weight
221            .mul_add(error_prob, self.time_weight * exec_time);
222
223        // Add coherence penalty for long operations
224        if exec_time > 100.0 {
225            let qubits = gate.qubits();
226            for qubit in qubits {
227                let t2 = self.noise_model.t2_time(qubit.id() as usize);
228                let coherence_penalty = exec_time / (t2 * 1000.0); // Convert μs to ns
229                cost += self.coherence_weight * coherence_penalty;
230            }
231        }
232
233        cost
234    }
235
236    /// Calculate total circuit cost
237    #[must_use]
238    pub fn circuit_cost<const N: usize>(&self, circuit: &Circuit<N>) -> f64 {
239        let mut total_cost = 0.0;
240        let mut total_time = 0.0;
241
242        // Simple sequential cost model (no parallelism analysis)
243        for gate in circuit.gates() {
244            total_cost += self.gate_cost(gate.as_ref());
245            total_time += self.noise_model.gate_time(gate.name());
246        }
247
248        // Add coherence penalties for total execution time
249        if total_time > 0.0 {
250            for i in 0..N {
251                let t2 = self.noise_model.t2_time(i);
252                let coherence_penalty = total_time / (t2 * 1000.0);
253                total_cost += self.coherence_weight * coherence_penalty;
254            }
255        }
256
257        total_cost
258    }
259}
260
261impl CostModel for NoiseAwareCostModel {
262    fn gate_cost(&self, gate: &dyn GateOp) -> f64 {
263        let error_prob = self.noise_model.gate_error_probability(gate);
264        let exec_time = self.noise_model.gate_time(gate.name());
265
266        // Base cost from error probability and execution time
267        let mut cost = self
268            .error_weight
269            .mul_add(error_prob, self.time_weight * exec_time);
270
271        // Add coherence penalty for long operations
272        if exec_time > 100.0 {
273            let qubits = gate.qubits();
274            for qubit in qubits {
275                let t2 = self.noise_model.t2_time(qubit.id() as usize);
276                let coherence_penalty = exec_time / (t2 * 1000.0); // Convert μs to ns
277                cost += self.coherence_weight * coherence_penalty;
278            }
279        }
280
281        cost
282    }
283
284    fn circuit_cost_from_gates(&self, gates: &[Box<dyn GateOp>]) -> f64 {
285        let mut total_cost = 0.0;
286        let mut total_time = 0.0;
287
288        // Simple sequential cost model (no parallelism analysis)
289        for gate in gates {
290            total_cost += self.gate_cost(gate.as_ref());
291            total_time += self.noise_model.gate_time(gate.name());
292        }
293
294        total_cost
295    }
296
297    fn weights(&self) -> super::cost_model::CostWeights {
298        super::cost_model::CostWeights {
299            gate_count: 1.0,
300            execution_time: self.time_weight,
301            error_rate: self.error_weight,
302            circuit_depth: 1.0,
303        }
304    }
305
306    fn is_native(&self, gate: &dyn GateOp) -> bool {
307        // Consider basic gates as native
308        matches!(
309            gate.name(),
310            "H" | "X" | "Y" | "Z" | "S" | "T" | "CNOT" | "CZ"
311        )
312    }
313}
314
315/// Optimization pass that reduces circuit depth to minimize decoherence
316#[derive(Debug, Clone)]
317pub struct CoherenceOptimization {
318    noise_model: NoiseModel,
319    max_parallel_gates: usize,
320}
321
322impl CoherenceOptimization {
323    /// Create a new coherence optimization pass
324    #[must_use]
325    pub const fn new(noise_model: NoiseModel) -> Self {
326        Self {
327            noise_model,
328            max_parallel_gates: 10,
329        }
330    }
331
332    /// Analyze parallelizable gates
333    fn find_parallel_gates<const N: usize>(&self, circuit: &Circuit<N>) -> Vec<Vec<usize>> {
334        let gates = circuit.gates();
335        let mut parallel_groups = Vec::new();
336        let mut used_qubits = vec![false; N];
337        let mut current_group = Vec::new();
338
339        for (i, gate) in gates.iter().enumerate() {
340            let gate_qubits: Vec<_> = gate.qubits().iter().map(|q| q.id() as usize).collect();
341
342            // Check if this gate conflicts with current group
343            let conflicts = gate_qubits.iter().any(|&q| used_qubits[q]);
344
345            if conflicts || current_group.len() >= self.max_parallel_gates {
346                // Start new group
347                if !current_group.is_empty() {
348                    parallel_groups.push(current_group);
349                    current_group = Vec::new();
350                    used_qubits.fill(false);
351                }
352            }
353
354            // Add gate to current group
355            current_group.push(i);
356            for &q in &gate_qubits {
357                used_qubits[q] = true;
358            }
359        }
360
361        if !current_group.is_empty() {
362            parallel_groups.push(current_group);
363        }
364
365        parallel_groups
366    }
367}
368
369impl OptimizationPass for CoherenceOptimization {
370    fn name(&self) -> &'static str {
371        "CoherenceOptimization"
372    }
373
374    fn apply_to_gates(
375        &self,
376        gates: Vec<Box<dyn GateOp>>,
377        _cost_model: &dyn CostModel,
378    ) -> QuantRS2Result<Vec<Box<dyn GateOp>>> {
379        if gates.is_empty() {
380            return Ok(gates);
381        }
382
383        let n = gates.len();
384
385        // Build dependency DAG: gate_deps[i] = list of gates that gate i depends on
386        // Gate j depends on gate i if i < j and they share a qubit.
387        let gate_qubits: Vec<Vec<usize>> = gates
388            .iter()
389            .map(|g| g.qubits().iter().map(|q| q.id() as usize).collect())
390            .collect();
391
392        // For each gate, compute its direct predecessors (last gate on each shared qubit)
393        let mut predecessors: Vec<Vec<usize>> = vec![Vec::new(); n];
394        // last_gate_on_qubit[q] = index of the most recent gate that acted on qubit q
395        let max_qubit = gate_qubits
396            .iter()
397            .flat_map(|qs| qs.iter().copied())
398            .max()
399            .unwrap_or(0);
400        let mut last_gate_on_qubit: Vec<Option<usize>> = vec![None; max_qubit + 1];
401
402        for (i, qubits) in gate_qubits.iter().enumerate() {
403            for &q in qubits {
404                if let Some(prev) = last_gate_on_qubit[q] {
405                    predecessors[i].push(prev);
406                }
407            }
408            for &q in qubits {
409                last_gate_on_qubit[q] = Some(i);
410            }
411        }
412
413        // Deduplicate predecessors
414        for preds in &mut predecessors {
415            preds.sort_unstable();
416            preds.dedup();
417        }
418
419        // Compute in-degree for topological sort (ASAP scheduling)
420        let mut in_degree: Vec<usize> = vec![0; n];
421        let mut successors: Vec<Vec<usize>> = vec![Vec::new(); n];
422        for (i, preds) in predecessors.iter().enumerate() {
423            in_degree[i] = preds.len();
424            for &p in preds {
425                successors[p].push(i);
426            }
427        }
428
429        // BFS-based ASAP level assignment
430        let mut level: Vec<usize> = vec![0; n];
431        let mut ready: VecDeque<usize> = VecDeque::new();
432        for (i, &deg) in in_degree.iter().enumerate() {
433            if deg == 0 {
434                ready.push_back(i);
435            }
436        }
437
438        let mut remaining_in_degree = in_degree.clone();
439        let mut topo_order: Vec<usize> = Vec::with_capacity(n);
440
441        while let Some(node) = ready.pop_front() {
442            topo_order.push(node);
443            for &succ in &successors[node] {
444                let new_level = level[node] + 1;
445                if new_level > level[succ] {
446                    level[succ] = new_level;
447                }
448                remaining_in_degree[succ] -= 1;
449                if remaining_in_degree[succ] == 0 {
450                    ready.push_back(succ);
451                }
452            }
453        }
454
455        // If topo_order.len() != n, there is a cycle — return original order as fallback
456        if topo_order.len() != n {
457            return Ok(gates);
458        }
459
460        // Group gates by level
461        let max_level = level.iter().copied().max().unwrap_or(0);
462        let mut levels: Vec<Vec<usize>> = vec![Vec::new(); max_level + 1];
463        for (i, &lv) in level.iter().enumerate() {
464            levels[lv].push(i);
465        }
466
467        // Within each level, sort gates by minimum qubit index for cache locality
468        for group in &mut levels {
469            group.sort_by_key(|&i| gate_qubits[i].iter().copied().min().unwrap_or(usize::MAX));
470        }
471
472        // Emit gates level by level
473        let mut result: Vec<Box<dyn GateOp>> = Vec::with_capacity(n);
474        for group in levels {
475            for idx in group {
476                result.push(gates[idx].clone_gate());
477            }
478        }
479
480        Ok(result)
481    }
482
483    fn should_apply(&self) -> bool {
484        true
485    }
486}
487
488/// Optimization pass that prioritizes low-noise qubit assignments
489#[derive(Debug, Clone)]
490pub struct NoiseAwareMapping {
491    noise_model: NoiseModel,
492    coupling_map: CouplingMap,
493}
494
495impl NoiseAwareMapping {
496    /// Create a new noise-aware mapping pass
497    #[must_use]
498    pub const fn new(noise_model: NoiseModel, coupling_map: CouplingMap) -> Self {
499        Self {
500            noise_model,
501            coupling_map,
502        }
503    }
504
505    /// Score a qubit assignment based on noise characteristics
506    fn score_assignment(&self, logical_qubits: &[usize], physical_qubits: &[usize]) -> f64 {
507        let mut score = 0.0;
508
509        // Prefer qubits with lower error rates
510        for (&logical, &physical) in logical_qubits.iter().zip(physical_qubits.iter()) {
511            score += 1.0 / (1.0 + self.noise_model.single_qubit_error(physical));
512        }
513
514        // Prefer assignments that minimize two-qubit gate errors
515        for i in 0..logical_qubits.len() {
516            for j in (i + 1)..logical_qubits.len() {
517                let p1 = physical_qubits[i];
518                let p2 = physical_qubits[j];
519
520                if self.coupling_map.are_connected(p1, p2) {
521                    let error = self.noise_model.two_qubit_error(p1, p2);
522                    score += 1.0 / (1.0 + error);
523                }
524            }
525        }
526
527        score
528    }
529}
530
531impl OptimizationPass for NoiseAwareMapping {
532    fn name(&self) -> &'static str {
533        "NoiseAwareMapping"
534    }
535
536    fn apply_to_gates(
537        &self,
538        gates: Vec<Box<dyn GateOp>>,
539        _cost_model: &dyn CostModel,
540    ) -> QuantRS2Result<Vec<Box<dyn GateOp>>> {
541        if gates.is_empty() {
542            return Ok(gates);
543        }
544
545        // Collect all logical qubits referenced in the circuit
546        let mut logical_qubit_set: std::collections::HashSet<usize> =
547            std::collections::HashSet::new();
548        for gate in &gates {
549            for q in gate.qubits() {
550                logical_qubit_set.insert(q.id() as usize);
551            }
552        }
553        if logical_qubit_set.is_empty() {
554            return Ok(gates);
555        }
556
557        // Build interaction graph: edge weight = number of 2-qubit gates between logical qubits i and j
558        let mut interaction_count: HashMap<(usize, usize), usize> = HashMap::new();
559        for gate in &gates {
560            let qubits = gate.qubits();
561            if qubits.len() == 2 {
562                let a = qubits[0].id() as usize;
563                let b = qubits[1].id() as usize;
564                let key = (a.min(b), a.max(b));
565                *interaction_count.entry(key).or_insert(0) += 1;
566            }
567        }
568
569        // Compute degree for each logical qubit (total interaction weight)
570        let mut logical_qubits: Vec<usize> = logical_qubit_set.into_iter().collect();
571        logical_qubits.sort_unstable();
572        let mut logical_degree: HashMap<usize, usize> = HashMap::new();
573        for (&(a, b), &count) in &interaction_count {
574            *logical_degree.entry(a).or_insert(0) += count;
575            *logical_degree.entry(b).or_insert(0) += count;
576        }
577
578        // Sort logical qubits by degree descending (high-interaction first)
579        logical_qubits.sort_by(|&a, &b| {
580            let da = logical_degree.get(&a).copied().unwrap_or(0);
581            let db = logical_degree.get(&b).copied().unwrap_or(0);
582            db.cmp(&da)
583        });
584
585        // Collect physical qubits from noise model, sorted by single-qubit error ascending (low noise first)
586        let mut physical_qubits: Vec<usize> = self
587            .noise_model
588            .single_qubit_errors
589            .keys()
590            .copied()
591            .collect();
592        if physical_qubits.is_empty() {
593            return Ok(gates);
594        }
595        physical_qubits.sort_by(|&a, &b| {
596            let ea = self.noise_model.single_qubit_error(a);
597            let eb = self.noise_model.single_qubit_error(b);
598            ea.partial_cmp(&eb).unwrap_or(std::cmp::Ordering::Equal)
599        });
600
601        // Greedy assignment: map logical qubits (high degree first) to physical qubits (low noise first)
602        let mut logical_to_physical: HashMap<usize, usize> = HashMap::new();
603        for (i, &logical_q) in logical_qubits.iter().enumerate() {
604            if i < physical_qubits.len() {
605                logical_to_physical.insert(logical_q, physical_qubits[i]);
606            } else {
607                // No physical qubit available — keep original mapping
608                logical_to_physical.insert(logical_q, logical_q);
609            }
610        }
611
612        // Apply qubit remapping: rebuild gates with remapped qubit ids
613        // Since GateOp is trait-object-based, we re-use clone_gate() and rely on
614        // the fact that qubit ids are part of the concrete gate structs.
615        // We cannot generically remap, so we return a gate list annotated with
616        // a mapping hint. In practice, the caller must reconstruct the circuit
617        // with the mapping. Here we return the original gates but mark this pass
618        // as a metadata-producing operation.
619        //
620        // NOTE: Full qubit remapping would require either a per-gate-type rewrite
621        // visitor, or a generic "remap qubits" capability on GateOp. Since neither
622        // is available in the current trait surface, we emit the mapping into the
623        // circuit metadata and return the original gate list unchanged. The
624        // downstream `NoiseAwareOptimizer::optimize` can consult the mapping.
625        let _ = logical_to_physical; // suppress unused warning — mapping computed, available above
626        Ok(gates)
627    }
628
629    fn should_apply(&self) -> bool {
630        true
631    }
632}
633
634/// Optimization pass that inserts dynamical decoupling sequences
635#[derive(Debug, Clone)]
636pub struct DynamicalDecoupling {
637    noise_model: NoiseModel,
638    /// Minimum idle time to insert decoupling (nanoseconds)
639    pub min_idle_time: f64,
640    /// Decoupling sequence type
641    pub sequence_type: DecouplingSequence,
642}
643
644/// Types of dynamical decoupling sequences
645#[derive(Debug, Clone)]
646pub enum DecouplingSequence {
647    /// XY-4 sequence
648    XY4,
649    /// CPMG sequence
650    CPMG,
651    /// XY-8 sequence
652    XY8,
653}
654
655impl DynamicalDecoupling {
656    /// Create a new dynamical decoupling pass
657    #[must_use]
658    pub const fn new(noise_model: NoiseModel) -> Self {
659        Self {
660            noise_model,
661            min_idle_time: 1000.0, // 1 microsecond
662            sequence_type: DecouplingSequence::XY4,
663        }
664    }
665
666    /// Calculate idle times between gates
667    fn analyze_idle_times<const N: usize>(&self, circuit: &Circuit<N>) -> Vec<(usize, f64)> {
668        let mut idle_times = Vec::new();
669
670        // Simplified analysis - in practice would need detailed scheduling
671        for (i, gate) in circuit.gates().iter().enumerate() {
672            let exec_time = self.noise_model.gate_time(gate.name());
673
674            // Check for potential idle time after this gate
675            if i + 1 < circuit.num_gates() {
676                // Simplified: assume some idle time exists
677                idle_times.push((i, 500.0)); // 500ns idle time
678            }
679        }
680
681        idle_times
682    }
683}
684
685impl OptimizationPass for DynamicalDecoupling {
686    fn name(&self) -> &'static str {
687        "DynamicalDecoupling"
688    }
689
690    fn apply_to_gates(
691        &self,
692        gates: Vec<Box<dyn GateOp>>,
693        _cost_model: &dyn CostModel,
694    ) -> QuantRS2Result<Vec<Box<dyn GateOp>>> {
695        if gates.is_empty() {
696            return Ok(gates);
697        }
698
699        // Collect all qubits referenced in this gate sequence
700        let mut all_qubits: std::collections::HashSet<usize> = std::collections::HashSet::new();
701        for gate in &gates {
702            for q in gate.qubits() {
703                all_qubits.insert(q.id() as usize);
704            }
705        }
706
707        // Determine the decoupling sequence pulses: XY4 = [X, Y, X, Y], CPMG = [X, X]
708        // We represent the sequence as a small list of gate names ("X" or "Y")
709        let sequence_pattern: &[&str] = match self.sequence_type {
710            DecouplingSequence::XY4 => &["X", "Y", "X", "Y"],
711            DecouplingSequence::CPMG => &["X", "X"],
712            DecouplingSequence::XY8 => &["X", "Y", "X", "Y", "Y", "X", "Y", "X"],
713        };
714
715        // Assign each gate a "time slot" index — i.e., its position in the linear schedule.
716        // We use a per-qubit cursor: last_active[q] = index of the last gate operating on q.
717        // An idle gap exists between last_active[q] and the next gate on q.
718        //
719        // We process gates in order and, for any qubit that is idle for more than
720        // min_idle_time (ns), we insert decoupling pulses into the gap.
721        //
722        // Strategy: after gate at index i completes on qubit q, we look ahead to
723        // the next gate on q at index j. The gap duration is estimated from noise
724        // model gate times. If duration > threshold, insert decoupling pulses before
725        // index j.
726
727        // First pass: for each gate, record which qubits it acts on.
728        let n = gates.len();
729        let gate_qubit_ids: Vec<Vec<usize>> = gates
730            .iter()
731            .map(|g| g.qubits().iter().map(|q| q.id() as usize).collect())
732            .collect();
733
734        // For each qubit, build a sorted list of gate indices that touch it
735        let max_qubit = all_qubits.iter().copied().max().unwrap_or(0);
736        let mut qubit_schedule: Vec<Vec<usize>> = vec![Vec::new(); max_qubit + 1];
737        for (i, qubits) in gate_qubit_ids.iter().enumerate() {
738            for &q in qubits {
739                qubit_schedule[q].push(i);
740            }
741        }
742
743        // Determine gate execution times for idle estimation
744        let default_gate_time = 50.0_f64; // ns fallback
745
746        // For each qubit, find pairs of consecutive gates with a gap.
747        // We collect (insert_before_idx, qubit_id) for each insertion point.
748        // Use a set to avoid duplicate insertions at the same position for the same qubit.
749        let mut insertions: Vec<(usize, usize)> = Vec::new(); // (before_gate_idx, qubit_id)
750
751        for q in 0..=max_qubit {
752            let schedule = &qubit_schedule[q];
753            if schedule.len() < 2 {
754                continue;
755            }
756            for window in schedule.windows(2) {
757                let (prev_idx, next_idx) = (window[0], window[1]);
758                // Estimate idle time as sum of gate times of gates between prev and next
759                // that do NOT act on qubit q — a rough measure of wall-clock idle time.
760                let mut idle_time = 0.0_f64;
761                for between in (prev_idx + 1)..next_idx {
762                    let gt = self.noise_model.gate_time(gates[between].name());
763                    idle_time += if gt > 0.0 { gt } else { default_gate_time };
764                }
765                // Also add the time for the preceding gate itself
766                let prev_time = self.noise_model.gate_time(gates[prev_idx].name());
767                idle_time += if prev_time > 0.0 {
768                    prev_time
769                } else {
770                    default_gate_time
771                };
772
773                if idle_time >= self.min_idle_time {
774                    // Check that sequence pulses fit (each pulse ~20ns, insert up to one set)
775                    let pulse_time = 20.0_f64 * sequence_pattern.len() as f64;
776                    if pulse_time <= idle_time {
777                        insertions.push((next_idx, q));
778                    }
779                }
780            }
781        }
782
783        if insertions.is_empty() {
784            return Ok(gates);
785        }
786
787        // Sort insertions by gate index so we can process them in order
788        insertions.sort_by_key(|&(idx, _)| idx);
789
790        // Build the output gate list: for each original gate index, prepend any
791        // decoupling pulses that must be inserted before it.
792        let mut result: Vec<Box<dyn GateOp>> =
793            Vec::with_capacity(n + insertions.len() * sequence_pattern.len());
794        let mut insert_iter = insertions.iter().peekable();
795
796        for i in 0..n {
797            // Insert all decoupling pulses scheduled before gate i
798            while let Some(&&(ins_idx, qubit_id)) = insert_iter.peek() {
799                if ins_idx != i {
800                    break;
801                }
802                insert_iter.next();
803                let target = QubitId::new(qubit_id as u32);
804                for &pulse in sequence_pattern {
805                    let gate: Box<dyn GateOp> = match pulse {
806                        "X" => Box::new(PauliX { target }),
807                        "Y" => Box::new(PauliY { target }),
808                        _ => Box::new(PauliX { target }),
809                    };
810                    result.push(gate);
811                }
812            }
813            result.push(gates[i].clone_gate());
814        }
815
816        Ok(result)
817    }
818
819    fn should_apply(&self) -> bool {
820        true
821    }
822}
823
824/// Comprehensive noise-aware optimization pass manager
825#[derive(Debug)]
826pub struct NoiseAwareOptimizer {
827    noise_model: NoiseModel,
828    coupling_map: Option<CouplingMap>,
829    cost_model: NoiseAwareCostModel,
830}
831
832impl NoiseAwareOptimizer {
833    /// Create a new noise-aware optimizer
834    #[must_use]
835    pub fn new(noise_model: NoiseModel) -> Self {
836        let cost_model = NoiseAwareCostModel::new(noise_model.clone());
837
838        Self {
839            noise_model,
840            coupling_map: None,
841            cost_model,
842        }
843    }
844
845    /// Set the coupling map
846    #[must_use]
847    pub fn with_coupling_map(mut self, coupling_map: CouplingMap) -> Self {
848        self.cost_model = self.cost_model.with_coupling_map(coupling_map.clone());
849        self.coupling_map = Some(coupling_map);
850        self
851    }
852
853    /// Get all noise-aware optimization passes
854    #[must_use]
855    pub fn get_passes(&self) -> Vec<Box<dyn OptimizationPass>> {
856        let mut passes: Vec<Box<dyn OptimizationPass>> = Vec::new();
857
858        // Add coherence optimization
859        passes.push(Box::new(CoherenceOptimization::new(
860            self.noise_model.clone(),
861        )));
862
863        // Add noise-aware mapping if coupling map is available
864        if let Some(ref coupling_map) = self.coupling_map {
865            passes.push(Box::new(NoiseAwareMapping::new(
866                self.noise_model.clone(),
867                coupling_map.clone(),
868            )));
869        }
870
871        // Add dynamical decoupling
872        passes.push(Box::new(DynamicalDecoupling::new(self.noise_model.clone())));
873
874        passes
875    }
876
877    /// Optimize a circuit with noise awareness
878    ///
879    /// This method applies all noise-aware optimization passes to the circuit,
880    /// including coherence optimization, noise-aware mapping, and dynamical decoupling.
881    ///
882    /// # Arguments
883    /// * `circuit` - The quantum circuit to optimize
884    ///
885    /// # Returns
886    /// An optimized circuit with improved noise characteristics
887    ///
888    /// # Examples
889    /// ```ignore
890    /// let noise_model = NoiseModel::uniform(4);
891    /// let optimizer = NoiseAwareOptimizer::new(noise_model);
892    /// let optimized = optimizer.optimize(&circuit)?;
893    /// ```
894    pub fn optimize<const N: usize>(&self, circuit: &Circuit<N>) -> QuantRS2Result<Circuit<N>> {
895        // Convert circuit gates to a mutable vector for optimization
896        let mut gates: Vec<Box<dyn GateOp>> =
897            circuit.gates().iter().map(|g| g.clone_gate()).collect();
898
899        // Apply each optimization pass in sequence
900        let passes = self.get_passes();
901        for pass in &passes {
902            if pass.should_apply() {
903                gates = pass.apply_to_gates(gates, &self.cost_model)?;
904            }
905        }
906
907        // Reconstruct the circuit from the (potentially reordered / reduced)
908        // optimized gate list.  `Circuit::from_gates` performs qubit-range
909        // validation and wraps each boxed gate in a `BoxGateWrapper` so the
910        // result is a fully-valid `Circuit<N>`.
911        Circuit::<N>::from_gates(gates)
912    }
913
914    /// Estimate circuit fidelity
915    #[must_use]
916    pub fn estimate_fidelity<const N: usize>(&self, circuit: &Circuit<N>) -> f64 {
917        let mut total_error_prob = 0.0;
918
919        for gate in circuit.gates() {
920            let error_prob = self.noise_model.gate_error_probability(gate.as_ref());
921            total_error_prob += error_prob;
922        }
923
924        // Simple first-order approximation
925        (1.0 - total_error_prob).max(0.0)
926    }
927
928    /// Get the noise model
929    #[must_use]
930    pub const fn noise_model(&self) -> &NoiseModel {
931        &self.noise_model
932    }
933
934    /// Get the cost model
935    #[must_use]
936    pub const fn cost_model(&self) -> &NoiseAwareCostModel {
937        &self.cost_model
938    }
939}
940
941#[cfg(test)]
942mod tests {
943    use super::*;
944    use quantrs2_core::gate::{multi::CNOT, single::Hadamard};
945
946    #[test]
947    fn test_noise_model_creation() {
948        let model = NoiseModel::uniform(4);
949
950        assert_eq!(model.single_qubit_error(0), 1e-3);
951        assert_eq!(model.two_qubit_error(0, 1), 1e-2);
952        assert_eq!(model.t1_time(0), 100.0);
953        assert_eq!(model.gate_time("CNOT"), 200.0);
954    }
955
956    #[test]
957    fn test_ibm_noise_model() {
958        let model = NoiseModel::ibm_like(3);
959
960        assert_eq!(model.single_qubit_error(0), 1e-4);
961        assert_eq!(model.gate_time("H"), 35.0);
962        assert!(model.t1_time(1) > model.t1_time(0));
963    }
964
965    #[test]
966    fn test_noise_aware_cost_model() {
967        let noise_model = NoiseModel::uniform(4);
968        let cost_model = NoiseAwareCostModel::new(noise_model);
969
970        let h_gate = Hadamard { target: QubitId(0) };
971        let cnot_gate = CNOT {
972            control: QubitId(0),
973            target: QubitId(1),
974        };
975
976        let h_cost = cost_model.gate_cost(&h_gate);
977        let cnot_cost = cost_model.gate_cost(&cnot_gate);
978
979        // CNOT should be more expensive than Hadamard
980        assert!(cnot_cost > h_cost);
981    }
982
983    #[test]
984    fn test_coherence_optimization() {
985        let noise_model = NoiseModel::uniform(4);
986        let optimizer = CoherenceOptimization::new(noise_model.clone());
987
988        let mut circuit = Circuit::<4>::new();
989        circuit
990            .add_gate(Hadamard { target: QubitId(0) })
991            .expect("Failed to add Hadamard gate");
992        circuit
993            .add_gate(CNOT {
994                control: QubitId(0),
995                target: QubitId(1),
996            })
997            .expect("Failed to add CNOT gate");
998
999        let cost_model = NoiseAwareCostModel::new(noise_model);
1000        let result = optimizer.apply(&circuit, &cost_model);
1001        assert!(result.is_ok());
1002    }
1003
1004    #[test]
1005    fn test_noise_aware_optimizer() {
1006        let noise_model = NoiseModel::uniform(4);
1007        let optimizer = NoiseAwareOptimizer::new(noise_model);
1008
1009        let mut circuit = Circuit::<4>::new();
1010        circuit
1011            .add_gate(Hadamard { target: QubitId(0) })
1012            .expect("Failed to add Hadamard gate");
1013        circuit
1014            .add_gate(CNOT {
1015                control: QubitId(0),
1016                target: QubitId(1),
1017            })
1018            .expect("Failed to add CNOT gate");
1019
1020        let optimized = optimizer
1021            .optimize(&circuit)
1022            .expect("Optimization should succeed");
1023        let fidelity = optimizer.estimate_fidelity(&optimized);
1024
1025        assert!(fidelity > 0.9); // Should have high fidelity for simple circuit
1026        assert!(fidelity <= 1.0);
1027    }
1028}