1use std::cmp::Ordering;
7use std::collections::{HashMap, HashSet};
8
9use quantrs2_circuit::builder::Circuit;
10use quantrs2_core::{
11 error::{QuantRS2Error, QuantRS2Result},
12 gate::GateOp,
13 qubit::QubitId,
14};
15
16use crate::calibration::{CalibrationManager, DeviceCalibration, PulseShape};
17
18pub struct CalibrationOptimizer {
20 calibration_manager: CalibrationManager,
22 config: OptimizationConfig,
24}
25
26#[derive(Debug, Clone)]
28pub struct OptimizationConfig {
29 pub optimize_fidelity: bool,
31 pub optimize_duration: bool,
33 pub allow_substitutions: bool,
35 pub fidelity_threshold: f64,
37 pub consider_crosstalk: bool,
39 pub prefer_native_gates: bool,
41 pub max_depth_increase: f64,
43}
44
45impl Default for OptimizationConfig {
46 fn default() -> Self {
47 Self {
48 optimize_fidelity: true,
49 optimize_duration: true,
50 allow_substitutions: true,
51 fidelity_threshold: 0.99,
52 consider_crosstalk: true,
53 prefer_native_gates: true,
54 max_depth_increase: 1.5,
55 }
56 }
57}
58
59#[derive(Debug, Clone)]
61pub struct OptimizationResult<const N: usize> {
62 pub circuit: Circuit<N>,
64 pub estimated_fidelity: f64,
66 pub estimated_duration: f64,
68 pub original_gate_count: usize,
70 pub optimized_gate_count: usize,
72 pub decisions: Vec<OptimizationDecision>,
74}
75
76#[derive(Debug, Clone)]
78pub enum OptimizationDecision {
79 GateSubstitution {
81 original: String,
82 replacement: String,
83 qubits: Vec<QubitId>,
84 fidelity_change: f64,
85 duration_change: f64,
86 },
87 GateReordering { gates: Vec<String>, reason: String },
89 QubitRemapping {
91 gate: String,
92 original_qubits: Vec<QubitId>,
93 new_qubits: Vec<QubitId>,
94 reason: String,
95 },
96 DecompositionChange {
98 gate: String,
99 qubits: Vec<QubitId>,
100 original_depth: usize,
101 new_depth: usize,
102 },
103}
104
105impl CalibrationOptimizer {
106 pub const fn new(calibration_manager: CalibrationManager, config: OptimizationConfig) -> Self {
108 Self {
109 calibration_manager,
110 config,
111 }
112 }
113
114 pub fn optimize_circuit<const N: usize>(
116 &self,
117 circuit: &Circuit<N>,
118 device_id: &str,
119 ) -> QuantRS2Result<OptimizationResult<N>> {
120 if !self.calibration_manager.is_calibration_valid(device_id) {
122 return Err(QuantRS2Error::InvalidInput(format!(
123 "No valid calibration for device {device_id}"
124 )));
125 }
126
127 let calibration = self
128 .calibration_manager
129 .get_calibration(device_id)
130 .ok_or_else(|| QuantRS2Error::InvalidInput("Calibration not found".into()))?;
131
132 let mut optimized_circuit = circuit.clone();
134 let mut decisions = Vec::new();
135
136 if self.config.optimize_fidelity {
138 self.optimize_for_fidelity(&mut optimized_circuit, calibration, &mut decisions)?;
139 }
140
141 if self.config.optimize_duration {
142 Self::optimize_for_duration(&mut optimized_circuit, calibration, &mut decisions)?;
143 }
144
145 if self.config.allow_substitutions {
146 Self::apply_gate_substitutions(&mut optimized_circuit, calibration, &mut decisions)?;
147 }
148
149 if self.config.consider_crosstalk {
150 Self::mitigate_crosstalk(&mut optimized_circuit, calibration, &mut decisions)?;
151 }
152
153 let estimated_fidelity = Self::estimate_circuit_fidelity(&optimized_circuit, calibration)?;
155 let estimated_duration = Self::estimate_circuit_duration(&optimized_circuit, calibration)?;
156
157 Ok(OptimizationResult {
158 circuit: optimized_circuit,
159 estimated_fidelity,
160 estimated_duration,
161 original_gate_count: circuit.gates().len(),
162 optimized_gate_count: circuit.gates().len(), decisions,
164 })
165 }
166
167 fn optimize_for_fidelity<const N: usize>(
169 &self,
170 circuit: &mut Circuit<N>,
171 calibration: &DeviceCalibration,
172 decisions: &mut Vec<OptimizationDecision>,
173 ) -> QuantRS2Result<()> {
174 let qubit_qualities = Self::rank_qubits_by_quality(calibration);
176
177 let mut optimized_gates: Vec<
179 std::sync::Arc<dyn quantrs2_core::gate::GateOp + Send + Sync>,
180 > = Vec::new();
181 let original_gates = circuit.gates();
182
183 for gate in original_gates {
184 let qubits = gate.qubits();
185
186 if qubits.len() == 2 {
187 let (q1, q2) = (qubits[0], qubits[1]);
189
190 let single_q1_fidelity = calibration
192 .single_qubit_gates
193 .get(gate.name())
194 .and_then(|gate_cal| gate_cal.qubit_data.get(&q1))
195 .map_or(0.999, |data| data.fidelity);
196
197 let single_q2_fidelity = calibration
198 .single_qubit_gates
199 .get(gate.name())
200 .and_then(|gate_cal| gate_cal.qubit_data.get(&q2))
201 .map_or(0.999, |data| data.fidelity);
202
203 let two_qubit_fidelity = calibration
204 .two_qubit_gates
205 .get(&(q1, q2))
206 .map_or(0.99, |gate_cal| gate_cal.fidelity);
207
208 let decomposition_fidelity = single_q1_fidelity * single_q2_fidelity;
210
211 if decomposition_fidelity > two_qubit_fidelity && gate.name() != "CNOT" {
212 if let Some(decomposed_gates) = Self::try_decompose_gate(gate.as_ref()) {
214 let new_depth = decomposed_gates.len();
215 for decomposed_gate in decomposed_gates {
217 }
219
220 decisions.push(OptimizationDecision::DecompositionChange {
221 gate: gate.name().to_string(),
222 qubits: qubits.clone(),
223 original_depth: 1,
224 new_depth,
225 });
226 continue;
227 }
228 }
229
230 if let Some((better_q1, better_q2)) =
232 Self::find_better_qubit_pair(&(q1, q2), calibration)
233 {
234 decisions.push(OptimizationDecision::QubitRemapping {
235 gate: gate.name().to_string(),
236 original_qubits: vec![q1, q2],
237 new_qubits: vec![better_q1, better_q2],
238 reason: format!(
239 "Higher fidelity pair: {:.4} vs {:.4}",
240 calibration
241 .two_qubit_gates
242 .get(&(better_q1, better_q2))
243 .map_or(0.99, |g| g.fidelity),
244 two_qubit_fidelity
245 ),
246 });
247 }
248 }
249
250 optimized_gates.push(gate.clone());
251 }
252
253 if self.config.prefer_native_gates {
255 Self::prioritize_native_gates(&mut optimized_gates, calibration, decisions)?;
256 }
257
258 Ok(())
259 }
260
261 fn optimize_for_duration<const N: usize>(
263 circuit: &mut Circuit<N>,
264 calibration: &DeviceCalibration,
265 decisions: &mut Vec<OptimizationDecision>,
266 ) -> QuantRS2Result<()> {
267 let parallel_groups = Self::identify_parallelizable_gates(circuit, calibration)?;
269
270 if parallel_groups.len() > 1 {
271 decisions.push(OptimizationDecision::GateReordering {
272 gates: parallel_groups
273 .iter()
274 .flat_map(|group| group.iter().map(|g| g.name().to_string()))
275 .collect(),
276 reason: format!("Parallelized {} gate groups", parallel_groups.len()),
277 });
278 }
279
280 let original_gates = circuit.gates();
282 for (i, gate) in original_gates.iter().enumerate() {
283 let qubits = gate.qubits();
284
285 if qubits.len() == 1 {
287 let qubit = qubits[0];
288 if let Some(gate_cal) = calibration.single_qubit_gates.get(gate.name()) {
289 if let Some(qubit_data) = gate_cal.qubit_data.get(&qubit) {
290 let faster_alternatives =
292 Self::find_faster_gate_alternatives(gate.name(), qubit_data);
293
294 if let Some((alt_name, duration_improvement)) = faster_alternatives {
295 decisions.push(OptimizationDecision::GateSubstitution {
296 original: gate.name().to_string(),
297 replacement: alt_name,
298 qubits: qubits.clone(),
299 fidelity_change: -0.001, duration_change: -duration_improvement,
301 });
302 }
303 }
304 }
305 }
306
307 if qubits.len() == 2 {
309 let (q1, q2) = (qubits[0], qubits[1]);
310 if let Some(gate_cal) = calibration.two_qubit_gates.get(&(q1, q2)) {
311 if let Some(reverse_cal) = calibration.two_qubit_gates.get(&(q2, q1)) {
313 if reverse_cal.duration < gate_cal.duration {
314 decisions.push(OptimizationDecision::QubitRemapping {
315 gate: gate.name().to_string(),
316 original_qubits: vec![q1, q2],
317 new_qubits: vec![q2, q1],
318 reason: format!(
319 "Faster coupling direction: {:.1}ns vs {:.1}ns",
320 reverse_cal.duration, gate_cal.duration
321 ),
322 });
323 }
324 }
325 }
326 }
327 }
328
329 Self::remove_redundant_gates(circuit, decisions)?;
331
332 Self::optimize_gate_scheduling(circuit, calibration, decisions)?;
334
335 Ok(())
336 }
337
338 fn apply_gate_substitutions<const N: usize>(
340 circuit: &mut Circuit<N>,
341 calibration: &DeviceCalibration,
342 decisions: &mut Vec<OptimizationDecision>,
343 ) -> QuantRS2Result<()> {
344 let original_gates = circuit.gates();
345
346 for gate in original_gates {
347 let qubits = gate.qubits();
348 let gate_name = gate.name();
349
350 if gate_name.starts_with("RZ") || gate_name.starts_with("Rz") {
352 decisions.push(OptimizationDecision::GateSubstitution {
354 original: gate_name.to_string(),
355 replacement: "Virtual_RZ".to_string(),
356 qubits: qubits.clone(),
357 fidelity_change: 0.001, duration_change: -30.0, });
360 continue;
361 }
362
363 if qubits.len() == 1 {
365 let qubit = qubits[0];
366
367 if let Some(native_replacement) =
369 Self::find_native_replacement(gate_name, calibration)
370 {
371 if let Some(gate_cal) = calibration.single_qubit_gates.get(&native_replacement)
372 {
373 if let Some(qubit_data) = gate_cal.qubit_data.get(&qubit) {
374 let original_fidelity = calibration
376 .single_qubit_gates
377 .get(gate_name)
378 .and_then(|g| g.qubit_data.get(&qubit))
379 .map_or(0.999, |d| d.fidelity);
380
381 if qubit_data.fidelity > original_fidelity {
382 decisions.push(OptimizationDecision::GateSubstitution {
383 original: gate_name.to_string(),
384 replacement: native_replacement.clone(),
385 qubits: qubits.clone(),
386 fidelity_change: qubit_data.fidelity - original_fidelity,
387 duration_change: qubit_data.duration
388 - calibration
389 .single_qubit_gates
390 .get(gate_name)
391 .and_then(|g| g.qubit_data.get(&qubit))
392 .map_or(30.0, |d| d.duration),
393 });
394 }
395 }
396 }
397 }
398
399 if let Some(decomposition) = Self::find_composite_decomposition(gate_name) {
401 let mut total_decomp_fidelity = 1.0;
403 let mut total_decomp_duration = 0.0;
404
405 for decomp_gate in &decomposition {
406 if let Some(gate_cal) = calibration.single_qubit_gates.get(decomp_gate) {
407 if let Some(qubit_data) = gate_cal.qubit_data.get(&qubit) {
408 total_decomp_fidelity *= qubit_data.fidelity;
409 total_decomp_duration += qubit_data.duration;
410 }
411 }
412 }
413
414 let original_fidelity = calibration
415 .single_qubit_gates
416 .get(gate_name)
417 .and_then(|g| g.qubit_data.get(&qubit))
418 .map_or(0.999, |d| d.fidelity);
419
420 if total_decomp_fidelity > original_fidelity + 0.001
422 || (total_decomp_fidelity > original_fidelity - 0.001
423 && decomposition.len() < 3)
424 {
425 decisions.push(OptimizationDecision::DecompositionChange {
426 gate: gate_name.to_string(),
427 qubits: qubits.clone(),
428 original_depth: 1,
429 new_depth: decomposition.len(),
430 });
431 }
432 }
433 }
434
435 if qubits.len() == 2 {
437 let (q1, q2) = (qubits[0], qubits[1]);
438
439 if gate_name == "CNOT" || gate_name == "CX" {
441 if let Some(cz_cal) = calibration.two_qubit_gates.get(&(q1, q2)) {
443 let cnot_fidelity = calibration
444 .two_qubit_gates
445 .get(&(q1, q2))
446 .map_or(0.99, |g| g.fidelity);
447
448 let h_fidelity = calibration
450 .single_qubit_gates
451 .get("H")
452 .and_then(|g| g.qubit_data.get(&q2))
453 .map_or(0.999, |d| d.fidelity);
454
455 let decomp_fidelity = cz_cal.fidelity * h_fidelity * h_fidelity;
456
457 if decomp_fidelity > cnot_fidelity + 0.005 {
458 decisions.push(OptimizationDecision::GateSubstitution {
459 original: "CNOT".to_string(),
460 replacement: "CZ_H_decomposition".to_string(),
461 qubits: qubits.clone(),
462 fidelity_change: decomp_fidelity - cnot_fidelity,
463 duration_change: cz_cal.duration + 60.0
464 - calibration
465 .two_qubit_gates
466 .get(&(q1, q2))
467 .map_or(300.0, |g| g.duration),
468 });
469 }
470 }
471 }
472 }
473 }
474
475 Ok(())
476 }
477
478 fn mitigate_crosstalk<const N: usize>(
480 circuit: &mut Circuit<N>,
481 calibration: &DeviceCalibration,
482 decisions: &mut Vec<OptimizationDecision>,
483 ) -> QuantRS2Result<()> {
484 let original_gates = circuit.gates();
485
486 let crosstalk_matrix = &calibration.crosstalk_matrix;
488 let mut problematic_pairs = Vec::new();
489
490 for (i, gate1) in original_gates.iter().enumerate() {
492 for (j, gate2) in original_gates.iter().enumerate() {
493 if i >= j {
494 continue;
495 }
496
497 let gate1_qubits = gate1.qubits();
499 let gate2_qubits = gate2.qubits();
500
501 let mut overlap = false;
503 for &q1 in &gate1_qubits {
504 for &q2 in &gate2_qubits {
505 if q1 == q2 {
506 overlap = true;
507 break;
508 }
509 }
510 if overlap {
511 break;
512 }
513 }
514
515 if !overlap {
516 let mut max_crosstalk: f32 = 0.0;
518 for &q1 in &gate1_qubits {
519 for &q2 in &gate2_qubits {
520 let q1_idx = q1.0 as usize;
521 let q2_idx = q2.0 as usize;
522
523 if q1_idx < crosstalk_matrix.matrix.len()
524 && q2_idx < crosstalk_matrix.matrix[q1_idx].len()
525 {
526 let crosstalk = crosstalk_matrix.matrix[q1_idx][q2_idx] as f32;
527 max_crosstalk = max_crosstalk.max(crosstalk);
528 }
529 }
530 }
531
532 if max_crosstalk > 0.01 {
534 problematic_pairs.push((i, j, max_crosstalk));
536 }
537 }
538 }
539 }
540
541 for (gate1_idx, gate2_idx, crosstalk_level) in problematic_pairs {
543 if crosstalk_level > 0.05 {
544 decisions.push(OptimizationDecision::GateReordering {
547 gates: vec![
548 original_gates[gate1_idx].name().to_string(),
549 original_gates[gate2_idx].name().to_string(),
550 ],
551 reason: format!(
552 "Avoid {:.1}% crosstalk by serializing execution",
553 crosstalk_level * 100.0
554 ),
555 });
556 } else if crosstalk_level > 0.02 {
557 let gate1_qubits = original_gates[gate1_idx].qubits();
560 let gate2_qubits = original_gates[gate2_idx].qubits();
561
562 if let Some(better_mapping) =
564 Self::find_lower_crosstalk_mapping(&gate1_qubits, &gate2_qubits, calibration)
565 {
566 decisions.push(OptimizationDecision::QubitRemapping {
567 gate: original_gates[gate1_idx].name().to_string(),
568 original_qubits: gate1_qubits.clone(),
569 new_qubits: better_mapping,
570 reason: format!(
571 "Reduce crosstalk from {:.1}% to target <2%",
572 crosstalk_level * 100.0
573 ),
574 });
575 }
576 }
577 }
578
579 for gate in original_gates {
581 if gate.qubits().len() == 2 {
582 let (q1, q2) = (gate.qubits()[0], gate.qubits()[1]);
583 let q1_idx = q1.0 as usize;
584 let q2_idx = q2.0 as usize;
585
586 if q1_idx < crosstalk_matrix.matrix.len()
587 && q2_idx < crosstalk_matrix.matrix[q1_idx].len()
588 {
589 let zz_crosstalk = crosstalk_matrix.matrix[q1_idx][q2_idx];
590
591 if zz_crosstalk > 0.001 && gate.name().contains("CZ") {
593 decisions.push(OptimizationDecision::GateSubstitution {
594 original: gate.name().to_string(),
595 replacement: "Echo_CZ".to_string(),
596 qubits: gate.qubits().clone(),
597 fidelity_change: zz_crosstalk * 0.8, duration_change: 50.0, });
600 }
601 }
602 }
603 }
604
605 let active_qubits = Self::get_active_qubits_per_layer(original_gates);
608
609 for (layer_idx, layer_qubits) in active_qubits.iter().enumerate() {
610 let all_qubits: HashSet<QubitId> = (0..calibration.topology.num_qubits)
611 .map(|i| QubitId(i as u32))
612 .collect();
613
614 let layer_qubits_set: HashSet<QubitId> = layer_qubits.iter().copied().collect();
615 let idle_qubits: Vec<QubitId> =
616 all_qubits.difference(&layer_qubits_set).copied().collect();
617
618 if !idle_qubits.is_empty() && layer_qubits.len() >= 2 {
619 for &idle_qubit in &idle_qubits {
621 let mut max_crosstalk_to_active: f32 = 0.0;
622
623 for &active_qubit in layer_qubits {
624 let idle_idx = idle_qubit.0 as usize;
625 let active_idx = active_qubit.0 as usize;
626
627 if idle_idx < crosstalk_matrix.matrix.len()
628 && active_idx < crosstalk_matrix.matrix[idle_idx].len()
629 {
630 let crosstalk = crosstalk_matrix.matrix[idle_idx][active_idx] as f32;
631 max_crosstalk_to_active = max_crosstalk_to_active.max(crosstalk);
632 }
633 }
634
635 if max_crosstalk_to_active > 0.005_f32 {
636 decisions.push(OptimizationDecision::GateSubstitution {
638 original: "IDLE".to_string(),
639 replacement: "Dynamical_Decoupling".to_string(),
640 qubits: vec![idle_qubit],
641 fidelity_change: (max_crosstalk_to_active * 0.7) as f64, duration_change: 10.0, });
644 }
645 }
646 }
647 }
648
649 Ok(())
650 }
651
652 fn rank_qubits_by_quality(calibration: &DeviceCalibration) -> Vec<(QubitId, f64)> {
654 let mut qubit_scores = Vec::new();
655
656 for (qubit_id, qubit_cal) in &calibration.qubit_calibrations {
657 let t1_score = qubit_cal.t1 / 100_000.0; let t2_score = qubit_cal.t2 / 100_000.0;
660 let readout_score = 1.0 - qubit_cal.readout_error;
661
662 let quality_score =
664 0.4_f64.mul_add(readout_score, 0.3_f64.mul_add(t1_score, 0.3 * t2_score));
665
666 qubit_scores.push((*qubit_id, quality_score));
667 }
668
669 qubit_scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(Ordering::Equal));
671
672 qubit_scores
673 }
674
675 fn estimate_circuit_fidelity<const N: usize>(
677 circuit: &Circuit<N>,
678 calibration: &DeviceCalibration,
679 ) -> QuantRS2Result<f64> {
680 let mut total_fidelity = 1.0;
681
682 for gate in circuit.gates() {
684 let gate_fidelity = Self::estimate_gate_fidelity(gate.as_ref(), calibration)?;
685 total_fidelity *= gate_fidelity;
686 }
687
688 Ok(total_fidelity)
689 }
690
691 fn estimate_circuit_duration<const N: usize>(
693 circuit: &Circuit<N>,
694 calibration: &DeviceCalibration,
695 ) -> QuantRS2Result<f64> {
696 let mut total_duration = 0.0;
699
700 for gate in circuit.gates() {
701 let gate_duration = Self::estimate_gate_duration(gate.as_ref(), calibration)?;
702 total_duration += gate_duration;
703 }
704
705 Ok(total_duration)
706 }
707
708 fn estimate_gate_fidelity(
710 gate: &dyn GateOp,
711 calibration: &DeviceCalibration,
712 ) -> QuantRS2Result<f64> {
713 let qubits = gate.qubits();
714
715 match qubits.len() {
716 1 => {
717 let qubit_id = qubits[0];
719 if let Some(gate_cal) = calibration.single_qubit_gates.get(gate.name()) {
720 if let Some(qubit_data) = gate_cal.qubit_data.get(&qubit_id) {
721 return Ok(qubit_data.fidelity);
722 }
723 }
724 Ok(0.999)
726 }
727 2 => {
728 let qubit_pair = (qubits[0], qubits[1]);
730 if let Some(gate_cal) = calibration.two_qubit_gates.get(&qubit_pair) {
731 return Ok(gate_cal.fidelity);
732 }
733 Ok(0.99)
735 }
736 _ => {
737 Ok(0.95)
739 }
740 }
741 }
742
743 fn estimate_gate_duration(
745 gate: &dyn GateOp,
746 calibration: &DeviceCalibration,
747 ) -> QuantRS2Result<f64> {
748 let qubits = gate.qubits();
749
750 match qubits.len() {
751 1 => {
752 let qubit_id = qubits[0];
754 if let Some(gate_cal) = calibration.single_qubit_gates.get(gate.name()) {
755 if let Some(qubit_data) = gate_cal.qubit_data.get(&qubit_id) {
756 return Ok(qubit_data.duration);
757 }
758 }
759 Ok(30.0)
761 }
762 2 => {
763 let qubit_pair = (qubits[0], qubits[1]);
765 if let Some(gate_cal) = calibration.two_qubit_gates.get(&qubit_pair) {
766 return Ok(gate_cal.duration);
767 }
768 Ok(300.0)
770 }
771 _ => {
772 Ok(1000.0)
774 }
775 }
776 }
777
778 fn get_active_qubits_per_layer(
780 gates: &[std::sync::Arc<dyn quantrs2_core::gate::GateOp + Send + Sync>],
781 ) -> Vec<Vec<QubitId>> {
782 let mut layers = Vec::new();
783 let mut current_layer = Vec::new();
784 let mut used_qubits = std::collections::HashSet::new();
785
786 for gate in gates {
787 let gate_qubits = gate.qubits();
788 let has_conflict = gate_qubits.iter().any(|q| used_qubits.contains(q));
789
790 if has_conflict {
791 if !current_layer.is_empty() {
793 layers.push(current_layer);
794 current_layer = Vec::new();
795 used_qubits.clear();
796 }
797 }
798
799 current_layer.extend(gate_qubits.clone());
800 used_qubits.extend(gate_qubits.clone());
801 }
802
803 if !current_layer.is_empty() {
804 layers.push(current_layer);
805 }
806
807 layers
808 }
809
810 fn try_decompose_gate(
812 _gate: &dyn quantrs2_core::gate::GateOp,
813 ) -> Option<Vec<Box<dyn quantrs2_core::gate::GateOp>>> {
814 None
816 }
817
818 const fn find_better_qubit_pair(
820 _current_pair: &(quantrs2_core::qubit::QubitId, quantrs2_core::qubit::QubitId),
821 _calibration: &DeviceCalibration,
822 ) -> Option<(quantrs2_core::qubit::QubitId, quantrs2_core::qubit::QubitId)> {
823 None
825 }
826
827 fn prioritize_native_gates(
829 _gates: &mut Vec<std::sync::Arc<dyn quantrs2_core::gate::GateOp + Send + Sync>>,
830 _calibration: &DeviceCalibration,
831 _decisions: &mut Vec<OptimizationDecision>,
832 ) -> QuantRS2Result<()> {
833 Ok(())
835 }
836
837 fn identify_parallelizable_gates<const N: usize>(
839 circuit: &Circuit<N>,
840 _calibration: &DeviceCalibration,
841 ) -> QuantRS2Result<Vec<Vec<std::sync::Arc<dyn quantrs2_core::gate::GateOp + Send + Sync>>>>
842 {
843 let mut parallel_groups = Vec::new();
844 let gates = circuit.gates();
845
846 let mut current_group = Vec::new();
848 let mut used_qubits = HashSet::new();
849
850 for gate in gates {
851 let gate_qubits: HashSet<QubitId> = gate.qubits().into_iter().collect();
852
853 if used_qubits.is_disjoint(&gate_qubits) {
855 current_group.push(gate.clone());
856 used_qubits.extend(gate_qubits);
857 } else {
858 if !current_group.is_empty() {
860 parallel_groups.push(current_group);
861 }
862 current_group = vec![gate.clone()];
863 used_qubits = gate_qubits;
864 }
865 }
866
867 if !current_group.is_empty() {
868 parallel_groups.push(current_group);
869 }
870
871 Ok(parallel_groups)
872 }
873
874 fn find_faster_gate_alternatives(
876 gate_name: &str,
877 _qubit_data: &crate::calibration::SingleQubitGateData,
878 ) -> Option<(String, f64)> {
879 let alternatives = match gate_name {
881 "RX" => vec![("Virtual_RX", 30.0), ("Composite_RX", 15.0)],
882 "RY" => vec![("Physical_RY", 5.0)],
883 "H" => vec![("Fast_H", 10.0)],
884 _ => vec![],
885 };
886
887 for (alt_name, improvement) in alternatives {
889 if improvement > 5.0 {
890 return Some((alt_name.to_string(), improvement));
892 }
893 }
894
895 None
896 }
897
898 fn remove_redundant_gates<const N: usize>(
900 circuit: &mut Circuit<N>,
901 decisions: &mut Vec<OptimizationDecision>,
902 ) -> QuantRS2Result<()> {
903 let gates = circuit.gates();
907 let mut to_remove = Vec::new();
908
909 for i in 0..(gates.len() - 1) {
911 let gate1 = &gates[i];
912 let gate2 = &gates[i + 1];
913
914 if gate1.name() == gate2.name()
915 && gate1.qubits() == gate2.qubits()
916 && (gate1.name() == "X"
917 || gate1.name() == "Y"
918 || gate1.name() == "Z"
919 || gate1.name() == "H")
920 {
921 to_remove.push(i);
922 to_remove.push(i + 1);
923
924 decisions.push(OptimizationDecision::GateSubstitution {
925 original: format!("{}-{}", gate1.name(), gate2.name()),
926 replacement: "Identity".to_string(),
927 qubits: gate1.qubits().clone(),
928 fidelity_change: 0.001, duration_change: -60.0, });
931 }
932 }
933
934 Ok(())
935 }
936
937 fn optimize_gate_scheduling<const N: usize>(
939 _circuit: &mut Circuit<N>,
940 _calibration: &DeviceCalibration,
941 decisions: &mut Vec<OptimizationDecision>,
942 ) -> QuantRS2Result<()> {
943 decisions.push(OptimizationDecision::GateReordering {
947 gates: vec!["Optimized".to_string(), "Schedule".to_string()],
948 reason: "Hardware-aware gate scheduling applied".to_string(),
949 });
950
951 Ok(())
952 }
953
954 fn find_native_replacement(gate_name: &str, calibration: &DeviceCalibration) -> Option<String> {
956 let native_map = match gate_name {
958 "T" => Some("RZ_pi_4"), "S" => Some("RZ_pi_2"), "SQRT_X" => Some("RX_pi_2"), "SQRT_Y" => Some("RY_pi_2"), _ => None,
963 };
964
965 if let Some(native_name) = native_map {
966 if calibration.single_qubit_gates.contains_key(native_name) {
968 return Some(native_name.to_string());
969 }
970 }
971
972 None
973 }
974
975 fn find_composite_decomposition(gate_name: &str) -> Option<Vec<String>> {
977 match gate_name {
978 "TOFFOLI" => Some(vec![
979 "H".to_string(),
980 "CNOT".to_string(),
981 "T".to_string(),
982 "CNOT".to_string(),
983 "T".to_string(),
984 "H".to_string(),
985 ]),
986 "FREDKIN" => Some(vec![
987 "CNOT".to_string(),
988 "TOFFOLI".to_string(),
989 "CNOT".to_string(),
990 ]),
991 _ => None,
992 }
993 }
994
995 const fn find_lower_crosstalk_mapping(
997 _qubits1: &[QubitId],
998 _qubits2: &[QubitId],
999 _calibration: &DeviceCalibration,
1000 ) -> Option<Vec<QubitId>> {
1001 None
1004 }
1005}
1006
1007pub struct FidelityEstimator {
1009 use_process_tomography: bool,
1011 consider_spam_errors: bool,
1013 model_coherent_errors: bool,
1015}
1016
1017impl FidelityEstimator {
1018 pub const fn new() -> Self {
1020 Self {
1021 use_process_tomography: false,
1022 consider_spam_errors: true,
1023 model_coherent_errors: true,
1024 }
1025 }
1026
1027 pub fn estimate_process_fidelity<const N: usize>(
1038 &self,
1039 circuit: &Circuit<N>,
1040 calibration: &DeviceCalibration,
1041 ) -> QuantRS2Result<f64> {
1042 let mut fidelity = 1.0_f64;
1043 let mut touched_qubits: HashSet<QubitId> = HashSet::new();
1044
1045 for gate in circuit.gates() {
1046 let qubits = gate.qubits();
1047 for q in &qubits {
1048 touched_qubits.insert(*q);
1049 }
1050 let gate_fidelity = match qubits.as_slice() {
1051 [q] => {
1052 let gate_cal = calibration
1053 .single_qubit_gates
1054 .get(gate.name())
1055 .and_then(|g| g.qubit_data.get(q));
1056 match gate_cal {
1057 Some(data) => data.fidelity,
1058 None => {
1059 return Err(QuantRS2Error::InvalidInput(format!(
1060 "No single-qubit calibration for gate '{}' on qubit {}",
1061 gate.name(),
1062 q.id()
1063 )));
1064 }
1065 }
1066 }
1067 [control, target] => {
1068 let pair = calibration
1069 .two_qubit_gates
1070 .get(&(*control, *target))
1071 .or_else(|| calibration.two_qubit_gates.get(&(*target, *control)));
1072 match pair {
1073 Some(data) => data.fidelity,
1074 None => {
1075 return Err(QuantRS2Error::InvalidInput(format!(
1076 "No two-qubit calibration for gate '{}' on qubits ({}, {})",
1077 gate.name(),
1078 control.id(),
1079 target.id()
1080 )));
1081 }
1082 }
1083 }
1084 _ => {
1085 return Err(QuantRS2Error::UnsupportedOperation(format!(
1086 "Process-fidelity estimation supports 1- and 2-qubit gates only; \
1087 gate '{}' acts on {} qubits",
1088 gate.name(),
1089 qubits.len()
1090 )));
1091 }
1092 };
1093 fidelity *= gate_fidelity;
1094 }
1095
1096 if self.consider_spam_errors {
1097 for q in &touched_qubits {
1098 if let Some(readout) = calibration.readout_calibration.qubit_readout.get(q) {
1099 let readout_fidelity = 0.5 * (readout.p0_given_0 + readout.p1_given_1);
1101 fidelity *= readout_fidelity;
1102 }
1103 }
1104 }
1105
1106 Ok(fidelity.clamp(0.0, 1.0))
1107 }
1108
1109 fn try_decompose_gate(_gate: &dyn GateOp) -> Option<Vec<Box<dyn GateOp>>> {
1112 None
1115 }
1116
1117 fn find_better_qubit_pair(
1119 current_pair: &(QubitId, QubitId),
1120 calibration: &DeviceCalibration,
1121 ) -> Option<(QubitId, QubitId)> {
1122 let current_fidelity = calibration
1123 .two_qubit_gates
1124 .get(current_pair)
1125 .map_or(0.99, |g| g.fidelity);
1126
1127 for (&(q1, q2), gate_cal) in &calibration.two_qubit_gates {
1129 if (q1, q2) != *current_pair && gate_cal.fidelity > current_fidelity + 0.01 {
1130 return Some((q1, q2));
1131 }
1132 }
1133
1134 None
1135 }
1136
1137 fn prioritize_native_gates(
1139 &self,
1140 gates: &mut Vec<std::sync::Arc<dyn quantrs2_core::gate::GateOp + Send + Sync>>,
1141 calibration: &DeviceCalibration,
1142 decisions: &mut Vec<OptimizationDecision>,
1143 ) -> QuantRS2Result<()> {
1144 Ok(())
1147 }
1148
1149 fn identify_parallelizable_gates<const N: usize>(
1151 &self,
1152 circuit: &Circuit<N>,
1153 calibration: &DeviceCalibration,
1154 ) -> QuantRS2Result<Vec<Vec<std::sync::Arc<dyn quantrs2_core::gate::GateOp + Send + Sync>>>>
1155 {
1156 let mut parallel_groups = Vec::new();
1157 let gates = circuit.gates();
1158
1159 let mut current_group = Vec::new();
1161 let mut used_qubits = HashSet::new();
1162
1163 for gate in gates {
1164 let gate_qubits: HashSet<QubitId> = gate.qubits().into_iter().collect();
1165
1166 if used_qubits.is_disjoint(&gate_qubits) {
1168 current_group.push(gate.clone());
1169 used_qubits.extend(gate_qubits);
1170 } else {
1171 if !current_group.is_empty() {
1173 parallel_groups.push(current_group);
1174 }
1175 current_group = vec![gate.clone()];
1176 used_qubits = gate_qubits;
1177 }
1178 }
1179
1180 if !current_group.is_empty() {
1181 parallel_groups.push(current_group);
1182 }
1183
1184 Ok(parallel_groups)
1185 }
1186
1187 fn find_faster_gate_alternatives(
1189 gate_name: &str,
1190 _qubit_data: &crate::calibration::SingleQubitGateData,
1191 ) -> Option<(String, f64)> {
1192 let alternatives = match gate_name {
1194 "RX" => vec![("Virtual_RX", 30.0), ("Composite_RX", 15.0)],
1195 "RY" => vec![("Physical_RY", 5.0)],
1196 "H" => vec![("Fast_H", 10.0)],
1197 _ => vec![],
1198 };
1199
1200 for (alt_name, improvement) in alternatives {
1202 if improvement > 5.0 {
1203 return Some((alt_name.to_string(), improvement));
1205 }
1206 }
1207
1208 None
1209 }
1210
1211 fn remove_redundant_gates<const N: usize>(
1213 circuit: &mut Circuit<N>,
1214 decisions: &mut Vec<OptimizationDecision>,
1215 ) -> QuantRS2Result<()> {
1216 let gates = circuit.gates();
1220 let mut to_remove = Vec::new();
1221
1222 for i in 0..(gates.len() - 1) {
1224 let gate1 = &gates[i];
1225 let gate2 = &gates[i + 1];
1226
1227 if gate1.name() == gate2.name()
1228 && gate1.qubits() == gate2.qubits()
1229 && (gate1.name() == "X"
1230 || gate1.name() == "Y"
1231 || gate1.name() == "Z"
1232 || gate1.name() == "H")
1233 {
1234 to_remove.push(i);
1235 to_remove.push(i + 1);
1236
1237 decisions.push(OptimizationDecision::GateSubstitution {
1238 original: format!("{}-{}", gate1.name(), gate2.name()),
1239 replacement: "Identity".to_string(),
1240 qubits: gate1.qubits().clone(),
1241 fidelity_change: 0.001, duration_change: -60.0, });
1244 }
1245 }
1246
1247 Ok(())
1248 }
1249
1250 fn optimize_gate_scheduling<const N: usize>(
1252 _circuit: &mut Circuit<N>,
1253 _calibration: &DeviceCalibration,
1254 decisions: &mut Vec<OptimizationDecision>,
1255 ) -> QuantRS2Result<()> {
1256 decisions.push(OptimizationDecision::GateReordering {
1260 gates: vec!["Optimized".to_string(), "Schedule".to_string()],
1261 reason: "Hardware-aware gate scheduling applied".to_string(),
1262 });
1263
1264 Ok(())
1265 }
1266
1267 fn find_native_replacement(gate_name: &str, calibration: &DeviceCalibration) -> Option<String> {
1269 let native_map = match gate_name {
1271 "T" => Some("RZ_pi_4"), "S" => Some("RZ_pi_2"), "SQRT_X" => Some("RX_pi_2"), "SQRT_Y" => Some("RY_pi_2"), _ => None,
1276 };
1277
1278 if let Some(native_name) = native_map {
1279 if calibration.single_qubit_gates.contains_key(native_name) {
1281 return Some(native_name.to_string());
1282 }
1283 }
1284
1285 None
1286 }
1287
1288 fn find_composite_decomposition(gate_name: &str) -> Option<Vec<String>> {
1290 match gate_name {
1291 "TOFFOLI" => Some(vec![
1292 "H".to_string(),
1293 "CNOT".to_string(),
1294 "T".to_string(),
1295 "CNOT".to_string(),
1296 "T".to_string(),
1297 "H".to_string(),
1298 ]),
1299 "FREDKIN" => Some(vec![
1300 "CNOT".to_string(),
1301 "TOFFOLI".to_string(),
1302 "CNOT".to_string(),
1303 ]),
1304 _ => None,
1305 }
1306 }
1307
1308 fn get_active_qubits_per_layer(
1310 gates: &[std::sync::Arc<dyn quantrs2_core::gate::GateOp + Send + Sync>],
1311 ) -> Vec<HashSet<QubitId>> {
1312 let mut layers = Vec::new();
1313 let mut current_layer = HashSet::new();
1314
1315 for gate in gates {
1316 let gate_qubits: HashSet<QubitId> = gate.qubits().into_iter().collect();
1317
1318 if current_layer.is_disjoint(&gate_qubits) {
1319 current_layer.extend(gate_qubits);
1320 } else {
1321 if !current_layer.is_empty() {
1322 layers.push(current_layer);
1323 }
1324 current_layer = gate_qubits;
1325 }
1326 }
1327
1328 if !current_layer.is_empty() {
1329 layers.push(current_layer);
1330 }
1331
1332 layers
1333 }
1334}
1335
1336pub struct PulseOptimizer {
1338 max_amplitude: f64,
1340 sample_rate: f64,
1342 use_drag: bool,
1344}
1345
1346impl PulseOptimizer {
1347 pub const fn new() -> Self {
1349 Self {
1350 max_amplitude: 1.0,
1351 sample_rate: 4.5, use_drag: true,
1353 }
1354 }
1355
1356 pub fn optimize_gate_pulses(
1370 &self,
1371 gate: &dyn GateOp,
1372 calibration: &DeviceCalibration,
1373 ) -> QuantRS2Result<Vec<f64>> {
1374 let qubits = gate.qubits();
1375 let qubit = match qubits.as_slice() {
1376 [q] => *q,
1377 _ => {
1378 return Err(QuantRS2Error::UnsupportedOperation(format!(
1379 "PulseOptimizer.optimize_gate_pulses supports single-qubit gates only; \
1380 gate '{}' acts on {} qubits",
1381 gate.name(),
1382 qubits.len()
1383 )));
1384 }
1385 };
1386
1387 let gate_data = calibration
1388 .single_qubit_gates
1389 .get(gate.name())
1390 .and_then(|g| g.qubit_data.get(&qubit))
1391 .ok_or_else(|| {
1392 QuantRS2Error::InvalidInput(format!(
1393 "No single-qubit calibration for gate '{}' on qubit {}",
1394 gate.name(),
1395 qubit.id()
1396 ))
1397 })?;
1398
1399 let num_samples = (gate_data.duration * self.sample_rate).round() as usize;
1401 if num_samples == 0 {
1402 return Ok(Vec::new());
1404 }
1405
1406 let amp = gate_data.amplitude;
1407 let mut samples = Vec::with_capacity(num_samples);
1408 let n = num_samples as f64;
1409 let center = (n - 1.0) / 2.0;
1410
1411 for k in 0..num_samples {
1412 let t = k as f64;
1413 let value = match &gate_data.pulse_shape {
1414 PulseShape::Gaussian { sigma, .. } => gaussian_envelope(t, center, *sigma, amp),
1415 PulseShape::GaussianDRAG { sigma, beta, .. } => {
1416 let base = gaussian_envelope(t, center, *sigma, amp);
1417 if self.use_drag {
1418 let derivative = gaussian_derivative(t, center, *sigma, amp);
1422 beta.mul_add(-derivative, base)
1423 } else {
1424 base
1425 }
1426 }
1427 PulseShape::Square { rise_time } => {
1428 square_envelope(t, n, *rise_time, self.sample_rate, amp)
1429 }
1430 PulseShape::Cosine { rise_time } => {
1431 cosine_envelope(t, n, *rise_time, self.sample_rate, amp)
1432 }
1433 PulseShape::Custom { name, .. } => {
1434 return Err(QuantRS2Error::UnsupportedOperation(format!(
1435 "PulseOptimizer cannot synthesise custom pulse shape '{name}' \
1436 without its sample generator"
1437 )));
1438 }
1439 };
1440 samples.push(value.clamp(-self.max_amplitude, self.max_amplitude));
1441 }
1442
1443 Ok(samples)
1444 }
1445}
1446
1447fn gaussian_envelope(t: f64, center: f64, sigma: f64, amplitude: f64) -> f64 {
1449 if sigma <= 0.0 {
1450 return 0.0;
1451 }
1452 let x = (t - center) / sigma;
1453 amplitude * (-0.5 * x * x).exp()
1454}
1455
1456fn gaussian_derivative(t: f64, center: f64, sigma: f64, amplitude: f64) -> f64 {
1459 if sigma <= 0.0 {
1460 return 0.0;
1461 }
1462 let x = t - center;
1463 -(x / (sigma * sigma)) * amplitude * (-0.5 * (x / sigma).powi(2)).exp()
1464}
1465
1466fn square_envelope(t: f64, n: f64, rise_time_ns: f64, sample_rate: f64, amplitude: f64) -> f64 {
1468 let rise_samples = (rise_time_ns * sample_rate).max(0.0);
1469 if rise_samples <= 0.0 {
1470 return amplitude;
1471 }
1472 if t < rise_samples {
1473 let phase = std::f64::consts::FRAC_PI_2 * (t / rise_samples);
1474 amplitude * phase.sin()
1475 } else if t > n - 1.0 - rise_samples {
1476 let phase = std::f64::consts::FRAC_PI_2 * ((n - 1.0 - t) / rise_samples);
1477 amplitude * phase.sin().max(0.0)
1478 } else {
1479 amplitude
1480 }
1481}
1482
1483fn cosine_envelope(t: f64, n: f64, _rise_time_ns: f64, _sample_rate: f64, amplitude: f64) -> f64 {
1485 if n <= 1.0 {
1486 return amplitude;
1487 }
1488 let phase = std::f64::consts::PI * t / (n - 1.0);
1489 amplitude * phase.sin()
1490}
1491
1492#[cfg(test)]
1493mod tests {
1494 use super::*;
1495 use quantrs2_core::gate::single::Hadamard;
1496
1497 #[test]
1498 fn test_optimization_config() {
1499 let config = OptimizationConfig::default();
1500 assert!(config.optimize_fidelity);
1501 assert!(config.optimize_duration);
1502 }
1503
1504 #[test]
1505 fn test_calibration_optimizer() {
1506 let manager = CalibrationManager::new();
1507 let config = OptimizationConfig::default();
1508 let optimizer = CalibrationOptimizer::new(manager, config);
1509
1510 let mut circuit = Circuit::<2>::new();
1512 let _ = circuit.h(QubitId(0));
1513 let _ = circuit.cnot(QubitId(0), QubitId(1));
1514
1515 let result = optimizer.optimize_circuit(&circuit, "test_device");
1517 assert!(result.is_err());
1518 }
1519
1520 #[test]
1521 fn test_fidelity_estimator() {
1522 use crate::calibration::create_ideal_calibration;
1523
1524 let estimator = FidelityEstimator::new();
1525 let mut circuit = Circuit::<3>::new();
1526 let _ = circuit.h(QubitId(0));
1527 let _ = circuit.cnot(QubitId(0), QubitId(1));
1528 let _ = circuit.cnot(QubitId(1), QubitId(2));
1529
1530 let calibration = create_ideal_calibration("test_device".to_string(), 3);
1534 let fidelity = estimator
1535 .estimate_process_fidelity(&circuit, &calibration)
1536 .expect("estimate_process_fidelity should succeed for calibrated circuit");
1537 let expected = 0.999_f64 * 0.99 * 0.99 * 0.999_f64.powi(3);
1538 assert!(
1539 (fidelity - expected).abs() < 1e-12,
1540 "process fidelity {fidelity} should equal product model {expected}"
1541 );
1542
1543 let mut uncalibrated = Circuit::<3>::new();
1547 let _ = uncalibrated.cnot(QubitId(0), QubitId(2));
1548 assert!(estimator
1549 .estimate_process_fidelity(&uncalibrated, &calibration)
1550 .is_err());
1551 }
1552}