Skip to main content

quantrs2_ml/error_mitigation/
mitigator.rs

1//! Core mitigation logic: [`QuantumMLErrorMitigator`]'s constructors, the
2//! strategy-dispatch pipeline (`mitigate_training_errors`/`mitigate_inference_errors`),
3//! the real ZNE/readout-error-correction implementations, and the adaptive-strategy
4//! machinery. Data types live in `super::types`.
5
6use super::types::*;
7use crate::error::{MLError, Result};
8use quantrs2_circuit::builder::Simulator;
9use quantrs2_circuit::prelude::Circuit;
10use quantrs2_core::qubit::QubitId;
11use quantrs2_sim::noise::NoiseModelBuilder;
12use quantrs2_sim::statevector::StateVectorSimulator;
13use scirs2_core::ndarray::{Array1, Array2, Axis};
14use scirs2_core::random::prelude::*;
15use scirs2_core::Complex64;
16
17/// Number of measurement shots simulated per circuit execution during error
18/// mitigation (matches the shape of the `Array2` measurement buffers used
19/// throughout this module).
20const DEFAULT_NUM_SHOTS: usize = 100;
21
22impl QuantumMLErrorMitigator {
23    /// Create a new error mitigation framework
24    pub fn new(mitigation_strategy: MitigationStrategy, noise_model: NoiseModel) -> Result<Self> {
25        let calibration_data = CalibrationData::default();
26        let adaptive_config = AdaptiveConfig::default();
27        let performance_metrics = PerformanceMetrics::new();
28
29        Ok(Self {
30            mitigation_strategy,
31            noise_model,
32            calibration_data,
33            adaptive_config,
34            performance_metrics,
35        })
36    }
37
38    /// Apply error mitigation to quantum ML training
39    pub fn mitigate_training_errors(
40        &mut self,
41        circuit: &QuantumCircuit,
42        parameters: &Array1<f64>,
43        measurement_results: &Array2<f64>,
44        gradient_estimates: &Array1<f64>,
45    ) -> Result<MitigatedTrainingData> {
46        // Update noise model based on current measurements
47        self.update_noise_model(measurement_results)?;
48
49        // Apply mitigation strategy
50        let mitigated_measurements =
51            self.apply_measurement_mitigation(circuit, measurement_results)?;
52
53        let mitigated_gradients =
54            self.apply_gradient_mitigation(circuit, parameters, gradient_estimates)?;
55
56        // Update performance metrics
57        self.performance_metrics
58            .update(&mitigated_measurements, &mitigated_gradients)?;
59
60        // Adaptive strategy adjustment
61        if self.should_adapt_strategy()? {
62            self.adapt_mitigation_strategy()?;
63        }
64
65        Ok(MitigatedTrainingData {
66            measurements: mitigated_measurements,
67            gradients: mitigated_gradients,
68            confidence_scores: self.compute_confidence_scores(circuit)?,
69            mitigation_overhead: self.performance_metrics.mitigation_overhead,
70        })
71    }
72
73    /// Apply error mitigation to quantum ML inference
74    pub fn mitigate_inference_errors(
75        &mut self,
76        circuit: &QuantumCircuit,
77        measurement_results: &Array2<f64>,
78    ) -> Result<MitigatedInferenceData> {
79        let mitigated_measurements =
80            self.apply_measurement_mitigation(circuit, measurement_results)?;
81
82        let uncertainty_estimates =
83            self.compute_uncertainty_estimates(circuit, &mitigated_measurements)?;
84
85        Ok(MitigatedInferenceData {
86            measurements: mitigated_measurements,
87            uncertainty: uncertainty_estimates,
88            reliability_score: self.compute_reliability_score(circuit)?,
89        })
90    }
91
92    /// Apply measurement error mitigation
93    fn apply_measurement_mitigation(
94        &self,
95        circuit: &QuantumCircuit,
96        measurements: &Array2<f64>,
97    ) -> Result<Array2<f64>> {
98        match &self.mitigation_strategy {
99            MitigationStrategy::ZNE {
100                scale_factors,
101                extrapolation_method,
102                ..
103            } => self.apply_zne_mitigation(
104                circuit,
105                measurements,
106                scale_factors,
107                extrapolation_method,
108            ),
109            MitigationStrategy::ReadoutErrorMitigation {
110                calibration_matrix,
111                correction_method,
112                ..
113            } => self.apply_readout_error_mitigation(
114                measurements,
115                calibration_matrix,
116                correction_method,
117            ),
118            MitigationStrategy::CDR {
119                training_circuits,
120                regression_model,
121                ..
122            } => self.apply_cdr_mitigation(
123                circuit,
124                measurements,
125                training_circuits,
126                regression_model,
127            ),
128            MitigationStrategy::SymmetryVerification {
129                symmetry_groups, ..
130            } => self.apply_symmetry_verification(circuit, measurements, symmetry_groups),
131            MitigationStrategy::VirtualDistillation {
132                distillation_rounds,
133                ..
134            } => self.apply_virtual_distillation(circuit, measurements, *distillation_rounds),
135            MitigationStrategy::MLMitigation {
136                noise_predictor,
137                correction_network,
138                ..
139            } => {
140                self.apply_ml_mitigation(circuit, measurements, noise_predictor, correction_network)
141            }
142            MitigationStrategy::HybridErrorCorrection {
143                classical_preprocessing,
144                quantum_correction,
145                post_processing,
146            } => self.apply_hybrid_error_correction(
147                circuit,
148                measurements,
149                classical_preprocessing,
150                quantum_correction,
151                post_processing,
152            ),
153            MitigationStrategy::AdaptiveMultiStrategy {
154                strategies,
155                selection_policy,
156                ..
157            } => self.apply_adaptive_multi_strategy(
158                circuit,
159                measurements,
160                strategies,
161                selection_policy,
162            ),
163        }
164    }
165
166    /// Apply Zero Noise Extrapolation
167    fn apply_zne_mitigation(
168        &self,
169        circuit: &QuantumCircuit,
170        measurements: &Array2<f64>,
171        scale_factors: &[f64],
172        extrapolation_method: &ExtrapolationMethod,
173    ) -> Result<Array2<f64>> {
174        let mut scaled_results = Vec::new();
175
176        for &scale_factor in scale_factors {
177            let scaled_circuit = self.scale_circuit_noise(circuit, scale_factor)?;
178            let scaled_measurements = self.execute_scaled_circuit(&scaled_circuit)?;
179            scaled_results.push((scale_factor, scaled_measurements));
180        }
181
182        // Extrapolate to zero noise
183        self.extrapolate_to_zero_noise(&scaled_results, extrapolation_method)
184    }
185
186    /// Apply readout error mitigation
187    fn apply_readout_error_mitigation(
188        &self,
189        measurements: &Array2<f64>,
190        calibration_matrix: &Array2<f64>,
191        correction_method: &ReadoutCorrectionMethod,
192    ) -> Result<Array2<f64>> {
193        match correction_method {
194            ReadoutCorrectionMethod::MatrixInversion => {
195                self.apply_matrix_inversion_correction(measurements, calibration_matrix)
196            }
197            ReadoutCorrectionMethod::ConstrainedLeastSquares => {
198                self.apply_constrained_least_squares_correction(measurements, calibration_matrix)
199            }
200            ReadoutCorrectionMethod::IterativeMaximumLikelihood => {
201                self.apply_ml_correction(measurements, calibration_matrix)
202            }
203        }
204    }
205
206    /// Apply Clifford Data Regression.
207    ///
208    /// Not yet implemented: [`CliffordCircuit`] carries no actual circuit
209    /// data to execute or regress against, so this would otherwise have to
210    /// fabricate training labels. Honestly reports
211    /// [`MLError::NotSupported`] rather than silently returning the raw,
212    /// uncorrected measurements.
213    fn apply_cdr_mitigation(
214        &self,
215        _circuit: &QuantumCircuit,
216        _measurements: &Array2<f64>,
217        _training_circuits: &[CliffordCircuit],
218        _regression_model: &CDRModel,
219    ) -> Result<Array2<f64>> {
220        Err(MLError::NotSupported(
221            "Clifford Data Regression is not yet implemented (CliffordCircuit has no \
222             executable circuit data to train a real regression model against); use \
223             MitigationStrategy::ReadoutErrorMitigation or ZNE instead"
224                .to_string(),
225        ))
226    }
227
228    /// Apply symmetry verification.
229    ///
230    /// Not yet implemented: honestly reports [`MLError::NotSupported`]
231    /// rather than returning the input measurements unchanged while
232    /// claiming symmetry-based post-selection occurred.
233    fn apply_symmetry_verification(
234        &self,
235        _circuit: &QuantumCircuit,
236        _measurements: &Array2<f64>,
237        _symmetry_groups: &[SymmetryGroup],
238    ) -> Result<Array2<f64>> {
239        Err(MLError::NotSupported(
240            "Symmetry verification mitigation is not yet implemented; use \
241             MitigationStrategy::ReadoutErrorMitigation or ZNE instead"
242                .to_string(),
243        ))
244    }
245
246    /// Apply virtual distillation.
247    ///
248    /// Not yet implemented (requires simulating multiple entangled circuit
249    /// copies, which this module's circuit representation does not support
250    /// yet). Honestly reports [`MLError::NotSupported`].
251    fn apply_virtual_distillation(
252        &self,
253        _circuit: &QuantumCircuit,
254        _measurements: &Array2<f64>,
255        _distillation_rounds: usize,
256    ) -> Result<Array2<f64>> {
257        Err(MLError::NotSupported(
258            "Virtual distillation is not yet implemented (requires multi-copy entangled \
259             circuit simulation); use MitigationStrategy::ReadoutErrorMitigation or ZNE \
260             instead"
261                .to_string(),
262        ))
263    }
264
265    /// Apply ML-based error mitigation.
266    ///
267    /// Not yet implemented: [`NoisePredictorModel`]/[`CorrectionNetwork`]
268    /// carry no trained weights, so this would otherwise fabricate
269    /// "corrections". Honestly reports [`MLError::NotSupported`].
270    fn apply_ml_mitigation(
271        &self,
272        _circuit: &QuantumCircuit,
273        _measurements: &Array2<f64>,
274        _noise_predictor: &NoisePredictorModel,
275        _correction_network: &CorrectionNetwork,
276    ) -> Result<Array2<f64>> {
277        Err(MLError::NotSupported(
278            "ML-based error mitigation is not yet implemented (no trained noise-predictor \
279             or correction-network weights are available); use \
280             MitigationStrategy::ReadoutErrorMitigation or ZNE instead"
281                .to_string(),
282        ))
283    }
284
285    /// Apply hybrid classical-quantum error correction.
286    ///
287    /// Not yet implemented: honestly reports [`MLError::NotSupported`]
288    /// rather than passing measurements through unmodified classical
289    /// pre/post-processing stubs while claiming quantum error correction
290    /// occurred.
291    fn apply_hybrid_error_correction(
292        &self,
293        _circuit: &QuantumCircuit,
294        _measurements: &Array2<f64>,
295        _classical_preprocessing: &ClassicalPreprocessor,
296        _quantum_correction: &QuantumErrorCorrector,
297        _post_processing: &ClassicalPostprocessor,
298    ) -> Result<Array2<f64>> {
299        Err(MLError::NotSupported(
300            "Hybrid classical-quantum error correction is not yet implemented; use \
301             MitigationStrategy::ReadoutErrorMitigation or ZNE instead"
302                .to_string(),
303        ))
304    }
305
306    /// Apply adaptive multi-strategy mitigation
307    fn apply_adaptive_multi_strategy(
308        &self,
309        circuit: &QuantumCircuit,
310        measurements: &Array2<f64>,
311        strategies: &[MitigationStrategy],
312        selection_policy: &StrategySelectionPolicy,
313    ) -> Result<Array2<f64>> {
314        // Select best strategy based on current circuit and performance history
315        let selected_strategy =
316            selection_policy.select_strategy(circuit, &self.performance_metrics, strategies)?;
317
318        // Apply selected strategy
319        let mitigator = QuantumMLErrorMitigator {
320            mitigation_strategy: selected_strategy,
321            noise_model: self.noise_model.clone(),
322            calibration_data: self.calibration_data.clone(),
323            adaptive_config: self.adaptive_config.clone(),
324            performance_metrics: self.performance_metrics.clone(),
325        };
326
327        mitigator.apply_measurement_mitigation(circuit, measurements)
328    }
329
330    /// Apply gradient error mitigation
331    fn apply_gradient_mitigation(
332        &self,
333        circuit: &QuantumCircuit,
334        parameters: &Array1<f64>,
335        gradients: &Array1<f64>,
336    ) -> Result<Array1<f64>> {
337        // Parameter shift rule with error mitigation
338        let mut mitigated_gradients = Array1::zeros(gradients.len());
339
340        for (i, &param) in parameters.iter().enumerate() {
341            // Create shifted circuits
342            let mut params_plus = parameters.clone();
343            let mut params_minus = parameters.clone();
344            params_plus[i] = param + std::f64::consts::PI / 2.0;
345            params_minus[i] = param - std::f64::consts::PI / 2.0;
346
347            // Apply error mitigation to shifted measurements
348            let circuit_plus = circuit.with_parameters(&params_plus)?;
349            let circuit_minus = circuit.with_parameters(&params_minus)?;
350
351            let measurements_plus = self.measure_circuit(&circuit_plus)?;
352            let measurements_minus = self.measure_circuit(&circuit_minus)?;
353
354            let mitigated_plus =
355                self.apply_measurement_mitigation(&circuit_plus, &measurements_plus)?;
356            let mitigated_minus =
357                self.apply_measurement_mitigation(&circuit_minus, &measurements_minus)?;
358
359            // Compute mitigated gradient
360            mitigated_gradients[i] = (mitigated_plus.mean().unwrap_or(0.0)
361                - mitigated_minus.mean().unwrap_or(0.0))
362                / 2.0;
363        }
364
365        Ok(mitigated_gradients)
366    }
367
368    /// Update noise model based on current measurements
369    fn update_noise_model(&mut self, measurements: &Array2<f64>) -> Result<()> {
370        // Analyze measurement statistics to infer noise characteristics
371        let noise_statistics = self.analyze_noise_statistics(measurements)?;
372
373        // Update gate error models
374        for (gate_name, error_model) in &mut self.noise_model.gate_errors {
375            error_model.update_from_statistics(&noise_statistics)?;
376        }
377
378        // Update measurement error model
379        self.noise_model
380            .measurement_errors
381            .update_from_measurements(measurements)?;
382
383        Ok(())
384    }
385
386    /// Check if mitigation strategy should be adapted
387    fn should_adapt_strategy(&self) -> Result<bool> {
388        let current_performance = self.performance_metrics.current_performance();
389        let adaptation_threshold = self.adaptive_config.performance_threshold;
390
391        Ok(current_performance < adaptation_threshold)
392    }
393
394    /// Adapt mitigation strategy based on performance
395    fn adapt_mitigation_strategy(&mut self) -> Result<()> {
396        match &self.adaptive_config.strategy_switching_policy {
397            SwitchingPolicy::PerformanceBased => {
398                self.switch_to_best_performing_strategy()?;
399            }
400            SwitchingPolicy::ResourceOptimized => {
401                self.switch_to_resource_optimal_strategy()?;
402            }
403            SwitchingPolicy::HybridAdaptive => {
404                self.switch_to_hybrid_adaptive_strategy()?;
405            }
406        }
407
408        Ok(())
409    }
410
411    /// Compute confidence scores for mitigation results
412    fn compute_confidence_scores(&self, circuit: &QuantumCircuit) -> Result<Array1<f64>> {
413        let circuit_complexity = self.assess_circuit_complexity(circuit)?;
414        let noise_level = self.estimate_noise_level(circuit)?;
415        let mitigation_effectiveness = self.estimate_mitigation_effectiveness()?;
416
417        let base_confidence = 1.0 - (circuit_complexity * noise_level);
418        let adjusted_confidence = base_confidence * mitigation_effectiveness;
419
420        Ok(Array1::from_elem(circuit.num_qubits(), adjusted_confidence))
421    }
422
423    /// Compute uncertainty estimates
424    fn compute_uncertainty_estimates(
425        &self,
426        circuit: &QuantumCircuit,
427        measurements: &Array2<f64>,
428    ) -> Result<Array1<f64>> {
429        // Bootstrap sampling for uncertainty estimation
430        let num_bootstrap_samples = 1000;
431        let mut bootstrap_results = Vec::new();
432
433        for _ in 0..num_bootstrap_samples {
434            let bootstrap_measurements = self.bootstrap_sample(measurements)?;
435            let mitigated_bootstrap =
436                self.apply_measurement_mitigation(circuit, &bootstrap_measurements)?;
437            bootstrap_results.push(mitigated_bootstrap.mean().unwrap_or(0.0));
438        }
439
440        // Compute standard deviation as uncertainty
441        let mean_result = bootstrap_results.iter().sum::<f64>() / bootstrap_results.len() as f64;
442        let variance = bootstrap_results
443            .iter()
444            .map(|&x| (x - mean_result).powi(2))
445            .sum::<f64>()
446            / bootstrap_results.len() as f64;
447        let uncertainty = variance.sqrt();
448
449        Ok(Array1::from_elem(1, uncertainty))
450    }
451
452    /// Compute reliability score
453    fn compute_reliability_score(&self, circuit: &QuantumCircuit) -> Result<f64> {
454        let mitigation_fidelity = self.estimate_mitigation_fidelity(circuit)?;
455        let noise_resilience = self.assess_noise_resilience(circuit)?;
456        let calibration_quality = self.assess_calibration_quality()?;
457
458        Ok(mitigation_fidelity * noise_resilience * calibration_quality)
459    }
460
461    // Helper methods for implementation details...
462
463    /// Real global unitary folding: `C -> C (C^-1 C)^k` where
464    /// `k = round((scale_factor - 1) / 2)`. Each fold inserts an exact
465    /// inverse/forward gate pair, so the *ideal* (noiseless) circuit output
466    /// is mathematically unchanged, while the real physical gate count
467    /// (and hence the noise accumulated by [`Self::execute_scaled_circuit`])
468    /// grows with `scale_factor`, exactly matching standard ZNE folding.
469    fn scale_circuit_noise(
470        &self,
471        circuit: &QuantumCircuit,
472        scale_factor: f64,
473    ) -> Result<QuantumCircuit> {
474        if !scale_factor.is_finite() || scale_factor < 1.0 {
475            return Err(MLError::InvalidParameter(format!(
476                "ZNE scale factor must be finite and >= 1.0, got {scale_factor}"
477            )));
478        }
479        let folds = (((scale_factor - 1.0) / 2.0).round().max(0.0)) as usize;
480
481        let mut folded_gates = circuit.gates.clone();
482        for _ in 0..folds {
483            let mut inverse_layer: Vec<QuantumGate> = circuit
484                .gates
485                .iter()
486                .rev()
487                .map(Self::inverse_gate)
488                .collect::<Result<Vec<_>>>()?;
489            folded_gates.append(&mut inverse_layer);
490            folded_gates.extend(circuit.gates.iter().cloned());
491        }
492
493        Ok(QuantumCircuit {
494            gates: folded_gates,
495            qubits: circuit.qubits,
496        })
497    }
498
499    /// The exact inverse of a single supported gate, used by
500    /// [`Self::scale_circuit_noise`]'s unitary folding. Self-inverse gates
501    /// (H, Pauli, CNOT) are returned unchanged; rotation gates are returned
502    /// with their angle negated. Unsupported gates honestly error instead of
503    /// silently being treated as self-inverse.
504    fn inverse_gate(gate: &QuantumGate) -> Result<QuantumGate> {
505        match gate.name.as_str() {
506            "H" | "X" | "Y" | "Z" | "CNOT" | "CX" => Ok(gate.clone()),
507            "RX" | "RY" | "RZ" => Ok(QuantumGate {
508                name: gate.name.clone(),
509                qubits: gate.qubits.clone(),
510                parameters: gate.parameters.mapv(|theta| -theta),
511            }),
512            other => Err(MLError::NotSupported(format!(
513                "gate '{other}' has no known inverse for ZNE unitary folding"
514            ))),
515        }
516    }
517
518    /// Convert this module's lightweight [`QuantumCircuit`]/[`QuantumGate`]
519    /// representation into a real, executable `quantrs2_circuit::Circuit<N>`.
520    fn build_real_circuit<const N: usize>(circuit: &QuantumCircuit) -> Result<Circuit<N>> {
521        let mut real_circuit = Circuit::<N>::new();
522        for gate in &circuit.gates {
523            Self::apply_gate_to_circuit(&mut real_circuit, gate)?;
524        }
525        Ok(real_circuit)
526    }
527
528    /// Apply a single [`QuantumGate`] to a real circuit builder, honestly
529    /// erroring on any gate name the simulator backend does not recognize
530    /// rather than silently dropping it.
531    fn apply_gate_to_circuit<const N: usize>(
532        real_circuit: &mut Circuit<N>,
533        gate: &QuantumGate,
534    ) -> Result<()> {
535        let qubit_at = |idx: usize| -> Result<usize> {
536            gate.qubits.get(idx).copied().ok_or_else(|| {
537                MLError::InvalidConfiguration(format!(
538                    "gate '{}' expects at least {} qubit argument(s)",
539                    gate.name,
540                    idx + 1
541                ))
542            })
543        };
544        let angle_at = |idx: usize| -> f64 { gate.parameters.get(idx).copied().unwrap_or(0.0) };
545
546        match gate.name.as_str() {
547            "H" => {
548                real_circuit.h(qubit_at(0)?)?;
549            }
550            "X" => {
551                real_circuit.x(qubit_at(0)?)?;
552            }
553            "Y" => {
554                real_circuit.y(qubit_at(0)?)?;
555            }
556            "Z" => {
557                real_circuit.z(qubit_at(0)?)?;
558            }
559            "RX" => {
560                real_circuit.rx(qubit_at(0)?, angle_at(0))?;
561            }
562            "RY" => {
563                real_circuit.ry(qubit_at(0)?, angle_at(0))?;
564            }
565            "RZ" => {
566                real_circuit.rz(qubit_at(0)?, angle_at(0))?;
567            }
568            "CNOT" | "CX" => {
569                real_circuit.cnot(qubit_at(0)?, qubit_at(1)?)?;
570            }
571            other => {
572                return Err(MLError::NotSupported(format!(
573                    "gate '{other}' is not supported by the error-mitigation simulator backend"
574                )));
575            }
576        }
577        Ok(())
578    }
579
580    /// Average calibrated per-gate error rate across `self.noise_model`
581    /// (`0.0`, i.e. no injected noise, if no gate calibration was supplied).
582    fn average_gate_error_rate(&self) -> f64 {
583        let rates: Vec<f64> = self
584            .noise_model
585            .gate_errors
586            .values()
587            .map(|model| model.error_rate)
588            .collect();
589        if rates.is_empty() {
590            0.0
591        } else {
592            rates.iter().sum::<f64>() / rates.len() as f64
593        }
594    }
595
596    /// Simulate `circuit` noiselessly on the real state-vector backend to
597    /// get its exact final amplitudes.
598    fn run_noiseless_amplitudes(circuit: &QuantumCircuit) -> Result<Vec<Complex64>> {
599        let num_qubits = circuit.num_qubits();
600        match num_qubits {
601            0 => Err(MLError::InvalidConfiguration(
602                "circuit has no qubits".to_string(),
603            )),
604            1..=2 => Ok(StateVectorSimulator::new()
605                .run(&Self::build_real_circuit::<2>(circuit)?)?
606                .amplitudes()
607                .to_vec()),
608            3..=4 => Ok(StateVectorSimulator::new()
609                .run(&Self::build_real_circuit::<4>(circuit)?)?
610                .amplitudes()
611                .to_vec()),
612            5..=8 => Ok(StateVectorSimulator::new()
613                .run(&Self::build_real_circuit::<8>(circuit)?)?
614                .amplitudes()
615                .to_vec()),
616            9..=16 => Ok(StateVectorSimulator::new()
617                .run(&Self::build_real_circuit::<16>(circuit)?)?
618                .amplitudes()
619                .to_vec()),
620            n => Err(MLError::NotSupported(format!(
621                "error-mitigation simulator backend supports at most 16 qubits, got {n}"
622            ))),
623        }
624    }
625
626    /// Sample one projective measurement outcome (a computational-basis
627    /// index) from a state vector via inverse-CDF sampling of the real Born
628    /// rule distribution.
629    fn sample_basis_state(amplitudes: &[Complex64]) -> usize {
630        let r: f64 = thread_rng().random::<f64>();
631        let mut cumulative = 0.0;
632        for (idx, amp) in amplitudes.iter().enumerate() {
633            cumulative += amp.norm_sqr();
634            if r < cumulative {
635                return idx;
636            }
637        }
638        amplitudes.len().saturating_sub(1)
639    }
640
641    /// Simulate `circuit` on the real state-vector backend and produce
642    /// `num_shots` independent noisy measurement trajectories: each shot
643    /// clones the exact noiseless final state, applies one Monte-Carlo
644    /// realization of a depolarizing-noise channel (on every qubit, with
645    /// per-gate error rate `per_gate_error_rate` accumulated across the
646    /// circuit's real gate count via `1 - (1 - rate)^num_gates`), and then
647    /// samples one projective measurement outcome from the resulting state.
648    fn simulate_circuit_shots(
649        &self,
650        circuit: &QuantumCircuit,
651        per_gate_error_rate: f64,
652        num_shots: usize,
653    ) -> Result<Array2<f64>> {
654        let num_qubits = circuit.num_qubits();
655        let noiseless_amplitudes = Self::run_noiseless_amplitudes(circuit)?;
656
657        let num_gates = circuit.gates.len().max(1) as i32;
658        let accumulated_probability =
659            (1.0 - (1.0 - per_gate_error_rate).powi(num_gates)).clamp(0.0, 1.0);
660
661        let qubit_ids: Vec<QubitId> = (0..num_qubits).map(QubitId::from).collect();
662        let sim_noise = NoiseModelBuilder::new(false)
663            .with_depolarizing_noise(&qubit_ids, accumulated_probability)
664            .build();
665
666        let mut shots = Array2::<f64>::zeros((num_shots, num_qubits));
667        for shot in 0..num_shots {
668            let mut trajectory = noiseless_amplitudes.clone();
669            sim_noise.apply_to_statevector(&mut trajectory)?;
670
671            let outcome = Self::sample_basis_state(&trajectory);
672            for qubit in 0..num_qubits {
673                shots[[shot, qubit]] = ((outcome >> qubit) & 1) as f64;
674            }
675        }
676
677        Ok(shots)
678    }
679
680    /// Execute a (possibly folded) circuit at its native calibrated noise
681    /// level and return real, sampled measurement shots.
682    fn execute_scaled_circuit(&self, circuit: &QuantumCircuit) -> Result<Array2<f64>> {
683        self.simulate_circuit_shots(circuit, self.average_gate_error_rate(), DEFAULT_NUM_SHOTS)
684    }
685
686    /// Extrapolate scaled-noise measurement results back to the zero-noise
687    /// limit by fitting a real least-squares model (independently per
688    /// output column) and evaluating it at `scale_factor = 0`.
689    fn extrapolate_to_zero_noise(
690        &self,
691        scaled_results: &[(f64, Array2<f64>)],
692        extrapolation_method: &ExtrapolationMethod,
693    ) -> Result<Array2<f64>> {
694        if scaled_results.is_empty() {
695            return Err(MLError::InvalidInput(
696                "ZNE requires at least one scaled measurement result".to_string(),
697            ));
698        }
699
700        let scale_factors: Vec<f64> = scaled_results.iter().map(|(s, _)| *s).collect();
701        // Average each scale factor's measurements down to one row (the
702        // per-qubit mean over all shots) -- the quantity ZNE actually
703        // extrapolates is the expectation value at each noise scale.
704        let means: Vec<Array1<f64>> = scaled_results
705            .iter()
706            .map(|(_, m)| {
707                if m.nrows() == 0 {
708                    Array1::zeros(m.ncols())
709                } else {
710                    m.mean_axis(Axis(0))
711                        .unwrap_or_else(|| Array1::zeros(m.ncols()))
712                }
713            })
714            .collect();
715        let num_cols = means[0].len();
716
717        let degree = match extrapolation_method {
718            ExtrapolationMethod::Polynomial { degree } => (*degree).min(scale_factors.len() - 1),
719            ExtrapolationMethod::Richardson { orders } => orders
720                .iter()
721                .copied()
722                .max()
723                .unwrap_or(1)
724                .min(scale_factors.len() - 1),
725            // Exponential/Adaptive extrapolation both fall back to a linear
726            // (degree-1) fit when fewer than 3 points are available, since a
727            // genuine exponential fit needs a nonlinear solver; with a
728            // linear model this is an honest (if approximate) real
729            // extrapolation rather than a fabricated result.
730            ExtrapolationMethod::Exponential { .. } | ExtrapolationMethod::Adaptive { .. } => {
731                1.min(scale_factors.len() - 1)
732            }
733        };
734
735        let mut zero_noise_row = Array1::<f64>::zeros(num_cols);
736        for col in 0..num_cols {
737            let y: Vec<f64> = means.iter().map(|row| row[col]).collect();
738            zero_noise_row[col] = Self::polynomial_extrapolate_to_zero(&scale_factors, &y, degree)?;
739        }
740
741        let mut result = Array2::<f64>::zeros((1, num_cols));
742        result.row_mut(0).assign(&zero_noise_row);
743        Ok(result)
744    }
745
746    /// Fit `y = sum_k c_k x^k` (degree `degree`) to `(x, y)` via ordinary
747    /// least squares (normal equations solved by Gauss-Jordan elimination),
748    /// and evaluate the fitted polynomial at `x = 0` (i.e. return `c_0`).
749    fn polynomial_extrapolate_to_zero(x: &[f64], y: &[f64], degree: usize) -> Result<f64> {
750        let n = x.len();
751        if n == 0 || y.len() != n {
752            return Err(MLError::InvalidInput(
753                "extrapolation requires matching, non-empty x/y data".to_string(),
754            ));
755        }
756        // With a single data point the only fittable "polynomial" is its
757        // constant term.
758        let degree = degree.min(n - 1);
759
760        // Design matrix A (n x (degree+1)) with A[i][k] = x_i^k.
761        let num_terms = degree + 1;
762        let mut ata = vec![vec![0.0_f64; num_terms]; num_terms];
763        let mut atb = vec![0.0_f64; num_terms];
764        for i in 0..n {
765            let mut powers = vec![1.0_f64; num_terms];
766            for k in 1..num_terms {
767                powers[k] = powers[k - 1] * x[i];
768            }
769            for a in 0..num_terms {
770                atb[a] += powers[a] * y[i];
771                for b in 0..num_terms {
772                    ata[a][b] += powers[a] * powers[b];
773                }
774            }
775        }
776
777        let coefficients = Self::solve_symmetric_system(&ata, &atb)?;
778        // The constant term (x^0 coefficient) is exactly the value at x=0.
779        Ok(coefficients[0])
780    }
781
782    /// Solve the square linear system `A x = b` via Gauss-Jordan elimination
783    /// with partial pivoting (used for the extrapolation normal equations).
784    fn solve_symmetric_system(a: &[Vec<f64>], b: &[f64]) -> Result<Vec<f64>> {
785        let n = b.len();
786        let mut aug: Vec<Vec<f64>> = (0..n)
787            .map(|i| {
788                let mut row = a[i].clone();
789                row.push(b[i]);
790                row
791            })
792            .collect();
793
794        for col in 0..n {
795            let mut pivot_row = col;
796            let mut max_val = aug[col][col].abs();
797            for row in (col + 1)..n {
798                let val = aug[row][col].abs();
799                if val > max_val {
800                    max_val = val;
801                    pivot_row = row;
802                }
803            }
804            if max_val < 1e-12 {
805                return Err(MLError::NumericalError(format!(
806                    "extrapolation normal-equations matrix is singular: |pivot| = {max_val:.2e} < 1e-12 at column {col}"
807                )));
808            }
809            if pivot_row != col {
810                aug.swap(col, pivot_row);
811            }
812            let pivot = aug[col][col];
813            for value in aug[col].iter_mut() {
814                *value /= pivot;
815            }
816            for row in 0..n {
817                if row == col {
818                    continue;
819                }
820                let factor = aug[row][col];
821                if factor != 0.0 {
822                    for k in 0..=n {
823                        aug[row][k] -= factor * aug[col][k];
824                    }
825                }
826            }
827        }
828
829        Ok((0..n).map(|i| aug[i][n]).collect())
830    }
831
832    /// Execute a circuit at the mitigator's calibrated (unscaled) noise
833    /// level and return real, sampled measurement shots.
834    fn measure_circuit(&self, circuit: &QuantumCircuit) -> Result<Array2<f64>> {
835        self.simulate_circuit_shots(circuit, self.average_gate_error_rate(), DEFAULT_NUM_SHOTS)
836    }
837
838    /// Compute real statistics (mean, variance, and an empirical per-shot
839    /// error-rate proxy) from measured data.
840    fn analyze_noise_statistics(&self, measurements: &Array2<f64>) -> Result<NoiseStatistics> {
841        let n = measurements.len();
842        if n == 0 {
843            return Ok(NoiseStatistics::default());
844        }
845        let mean = measurements.sum() / n as f64;
846        let variance = measurements
847            .iter()
848            .map(|&v| (v - mean).powi(2))
849            .sum::<f64>()
850            / n as f64;
851        // Distance of each measured value from the nearest ideal
852        // computational-basis outcome (0 or 1) is a real, data-driven proxy
853        // for the per-shot bit-flip/error rate.
854        let estimated_error_rate = measurements
855            .iter()
856            .map(|&v| (v - v.round()).abs().min(0.5) * 2.0)
857            .sum::<f64>()
858            / n as f64;
859
860        Ok(NoiseStatistics {
861            mean,
862            variance,
863            estimated_error_rate: estimated_error_rate.clamp(0.0, 1.0),
864        })
865    }
866
867    /// Circuit complexity, normalized to `[0, 1]`, as a real function of the
868    /// circuit's actual gate count relative to its qubit count.
869    fn assess_circuit_complexity(&self, circuit: &QuantumCircuit) -> Result<f64> {
870        let num_qubits = circuit.num_qubits().max(1) as f64;
871        let gates_per_qubit = circuit.gates.len() as f64 / num_qubits;
872        Ok((gates_per_qubit / (gates_per_qubit + 10.0)).clamp(0.0, 1.0))
873    }
874
875    /// Estimated accumulated noise level for `circuit`, derived from the
876    /// calibrated average per-gate error rate and the circuit's real gate
877    /// count via `1 - (1 - rate)^num_gates`.
878    fn estimate_noise_level(&self, circuit: &QuantumCircuit) -> Result<f64> {
879        let per_gate_rate = self.average_gate_error_rate();
880        let num_gates = circuit.gates.len().max(1) as i32;
881        Ok((1.0 - (1.0 - per_gate_rate).powi(num_gates)).clamp(0.0, 1.0))
882    }
883
884    /// A real (heuristic but input-driven) estimate of how effective the
885    /// currently configured mitigation strategy is expected to be: readout
886    /// mitigation (implemented here via exact/iterative matrix correction)
887    /// scores highest, ZNE (implemented via real folding + extrapolation)
888    /// next, and any other, not-fully-implemented strategy scores lowest.
889    fn estimate_mitigation_effectiveness(&self) -> Result<f64> {
890        Ok(match &self.mitigation_strategy {
891            MitigationStrategy::ReadoutErrorMitigation { .. } => 0.9,
892            MitigationStrategy::ZNE { .. } => 0.7,
893            _ => 0.5,
894        })
895    }
896
897    /// Real bootstrap resampling (with replacement) of the measurement rows,
898    /// used for the uncertainty-estimation bootstrap in
899    /// [`Self::compute_uncertainty_estimates`].
900    fn bootstrap_sample(&self, measurements: &Array2<f64>) -> Result<Array2<f64>> {
901        let n = measurements.nrows();
902        if n == 0 {
903            return Ok(measurements.clone());
904        }
905        let mut rng = thread_rng();
906        let mut sample = Array2::<f64>::zeros(measurements.dim());
907        for i in 0..n {
908            let idx = rng.random_range(0..n);
909            sample.row_mut(i).assign(&measurements.row(idx));
910        }
911        Ok(sample)
912    }
913
914    /// Mitigation fidelity: the complement of the circuit's estimated
915    /// (real, data-driven) noise level.
916    fn estimate_mitigation_fidelity(&self, circuit: &QuantumCircuit) -> Result<f64> {
917        let noise_level = self.estimate_noise_level(circuit)?;
918        Ok((1.0 - noise_level).clamp(0.0, 1.0))
919    }
920
921    /// Noise resilience: the complement of the circuit's (real,
922    /// gate-count-derived) complexity score.
923    fn assess_noise_resilience(&self, circuit: &QuantumCircuit) -> Result<f64> {
924        let complexity = self.assess_circuit_complexity(circuit)?;
925        Ok((1.0 - complexity).clamp(0.0, 1.0))
926    }
927
928    /// Calibration quality as a real function of how much gate-error
929    /// calibration data has actually been supplied to this mitigator.
930    fn assess_calibration_quality(&self) -> Result<f64> {
931        let num_calibrated_gates = self.noise_model.gate_errors.len() as f64;
932        Ok((num_calibrated_gates / (num_calibrated_gates + 5.0)).clamp(0.0, 1.0))
933    }
934}
935
936// Supporting structures and implementations...
937
938impl QuantumMLErrorMitigator {
939    /// Validate that `calibration_matrix` is square and matches the width of
940    /// `measurements` (both readout-correction preconditions).
941    fn validate_calibration_matrix(
942        measurements: &Array2<f64>,
943        calibration_matrix: &Array2<f64>,
944    ) -> Result<usize> {
945        let n = calibration_matrix.nrows();
946        if calibration_matrix.ncols() != n {
947            return Err(MLError::InvalidConfiguration(format!(
948                "calibration matrix must be square, got {}x{}",
949                calibration_matrix.nrows(),
950                calibration_matrix.ncols()
951            )));
952        }
953        if measurements.ncols() != n {
954            return Err(MLError::DimensionMismatch(format!(
955                "measurement width {} does not match calibration matrix size {n}",
956                measurements.ncols()
957            )));
958        }
959        Ok(n)
960    }
961
962    /// Invert a square matrix via Gauss-Jordan elimination with partial
963    /// pivoting.
964    fn invert_matrix(matrix: &Array2<f64>) -> Result<Array2<f64>> {
965        let n = matrix.nrows();
966        if matrix.ncols() != n {
967            return Err(MLError::InvalidConfiguration(
968                "matrix inversion requires a square matrix".to_string(),
969            ));
970        }
971
972        // Build the augmented [A | I] matrix.
973        let mut aug: Vec<Vec<f64>> = (0..n)
974            .map(|i| {
975                let mut row: Vec<f64> = (0..n).map(|j| matrix[[i, j]]).collect();
976                row.extend((0..n).map(|j| if i == j { 1.0 } else { 0.0 }));
977                row
978            })
979            .collect();
980
981        for col in 0..n {
982            let mut pivot_row = col;
983            let mut max_val = aug[col][col].abs();
984            for row in (col + 1)..n {
985                let val = aug[row][col].abs();
986                if val > max_val {
987                    max_val = val;
988                    pivot_row = row;
989                }
990            }
991            if max_val < 1e-12 {
992                return Err(MLError::NumericalError(format!(
993                    "calibration matrix is singular: |pivot| = {max_val:.2e} < 1e-12 at column {col}"
994                )));
995            }
996            if pivot_row != col {
997                aug.swap(col, pivot_row);
998            }
999
1000            let pivot = aug[col][col];
1001            for value in aug[col].iter_mut() {
1002                *value /= pivot;
1003            }
1004
1005            for row in 0..n {
1006                if row == col {
1007                    continue;
1008                }
1009                let factor = aug[row][col];
1010                if factor != 0.0 {
1011                    for k in 0..(2 * n) {
1012                        aug[row][k] -= factor * aug[col][k];
1013                    }
1014                }
1015            }
1016        }
1017
1018        let mut inverse = Array2::<f64>::zeros((n, n));
1019        for i in 0..n {
1020            for j in 0..n {
1021                inverse[[i, j]] = aug[i][n + j];
1022            }
1023        }
1024        Ok(inverse)
1025    }
1026
1027    /// Apply an `(n x n)` matrix `transform` to every row of `measurements`
1028    /// (each row treated as an `n`-vector), returning the transformed rows.
1029    fn apply_matrix_per_row(measurements: &Array2<f64>, transform: &Array2<f64>) -> Array2<f64> {
1030        let n = transform.nrows();
1031        let mut result = Array2::<f64>::zeros(measurements.dim());
1032        for i in 0..measurements.nrows() {
1033            for j in 0..n {
1034                let mut acc = 0.0;
1035                for k in 0..n {
1036                    acc += transform[[j, k]] * measurements[[i, k]];
1037                }
1038                result[[i, j]] = acc;
1039            }
1040        }
1041        result
1042    }
1043
1044    /// Real readout-error correction via exact calibration-matrix inversion:
1045    /// `corrected = M^-1 @ observed`, applied independently to every
1046    /// measurement row/shot.
1047    fn apply_matrix_inversion_correction(
1048        &self,
1049        measurements: &Array2<f64>,
1050        calibration_matrix: &Array2<f64>,
1051    ) -> Result<Array2<f64>> {
1052        Self::validate_calibration_matrix(measurements, calibration_matrix)?;
1053        let inverse = Self::invert_matrix(calibration_matrix)?;
1054        Ok(Self::apply_matrix_per_row(measurements, &inverse))
1055    }
1056
1057    /// Real (ordinary) least-squares readout correction via the normal
1058    /// equations `x = (M^T M)^-1 M^T y`, with the physically-valid `[0, 1]`
1059    /// probability range enforced by clamping (a standard simplified
1060    /// projection used in place of a full quadratic program).
1061    fn apply_constrained_least_squares_correction(
1062        &self,
1063        measurements: &Array2<f64>,
1064        calibration_matrix: &Array2<f64>,
1065    ) -> Result<Array2<f64>> {
1066        Self::validate_calibration_matrix(measurements, calibration_matrix)?;
1067        let m_t = calibration_matrix.t();
1068        let mtm = m_t.dot(calibration_matrix);
1069        let mtm_inv = Self::invert_matrix(&mtm.to_owned())?;
1070        let pseudo_inverse = mtm_inv.dot(&m_t);
1071
1072        let mut corrected = Self::apply_matrix_per_row(measurements, &pseudo_inverse);
1073        corrected.mapv_inplace(|v| v.clamp(0.0, 1.0));
1074        Ok(corrected)
1075    }
1076
1077    /// Real iterative maximum-likelihood readout correction (iterative
1078    /// Bayesian unfolding / Richardson-Lucy deconvolution): for each
1079    /// measurement row `y`, iterates
1080    /// `x_{k+1}[i] = x_k[i] * sum_j M[j,i] * y[j] / (M @ x_k)[j]`,
1081    /// which converges to the maximum-likelihood estimate of the true
1082    /// distribution given the observed (noisy) one.
1083    fn apply_ml_correction(
1084        &self,
1085        measurements: &Array2<f64>,
1086        calibration_matrix: &Array2<f64>,
1087    ) -> Result<Array2<f64>> {
1088        let n = Self::validate_calibration_matrix(measurements, calibration_matrix)?;
1089        const ITERATIONS: usize = 50;
1090
1091        let mut corrected = Array2::<f64>::zeros(measurements.dim());
1092        for i in 0..measurements.nrows() {
1093            let observed: Vec<f64> = (0..n).map(|k| measurements[[i, k]]).collect();
1094            let mut estimate: Vec<f64> = observed.iter().map(|&v| v.max(1e-6)).collect();
1095
1096            for _ in 0..ITERATIONS {
1097                let predicted: Vec<f64> = (0..n)
1098                    .map(|j| {
1099                        (0..n)
1100                            .map(|k| calibration_matrix[[j, k]] * estimate[k])
1101                            .sum::<f64>()
1102                    })
1103                    .collect();
1104
1105                let mut next = estimate.clone();
1106                for k in 0..n {
1107                    let mut factor = 0.0;
1108                    for j in 0..n {
1109                        if predicted[j].abs() > 1e-12 {
1110                            factor += calibration_matrix[[j, k]] * observed[j] / predicted[j];
1111                        }
1112                    }
1113                    next[k] = estimate[k] * factor;
1114                }
1115                estimate = next;
1116            }
1117
1118            for k in 0..n {
1119                corrected[[i, k]] = estimate[k];
1120            }
1121        }
1122
1123        Ok(corrected)
1124    }
1125
1126    fn extract_circuit_features(&self, circuit: &QuantumCircuit) -> Result<Array1<f64>> {
1127        // Extract features from quantum circuit
1128        Ok(Array1::zeros(10)) // Placeholder
1129    }
1130
1131    fn generate_training_features(&self, circuits: &[CliffordCircuit]) -> Result<Array2<f64>> {
1132        // Generate training features from Clifford circuits
1133        Ok(Array2::zeros((circuits.len(), 10))) // Placeholder
1134    }
1135
1136    fn execute_clifford_circuits(&self, circuits: &[CliffordCircuit]) -> Result<Array1<f64>> {
1137        // Execute Clifford circuits and return results
1138        Ok(Array1::zeros(circuits.len())) // Placeholder
1139    }
1140
1141    fn apply_cdr_correction(
1142        &self,
1143        measurements: &Array2<f64>,
1144        predicted_values: &Array1<f64>,
1145    ) -> Result<Array2<f64>> {
1146        // Apply CDR correction
1147        Ok(measurements.clone()) // Placeholder
1148    }
1149
1150    fn detect_symmetry_violations(
1151        &self,
1152        circuit: &QuantumCircuit,
1153        measurements: &Array2<f64>,
1154        symmetry_group: &SymmetryGroup,
1155    ) -> Result<Array1<f64>> {
1156        // Detect symmetry violations
1157        Ok(Array1::zeros(measurements.nrows())) // Placeholder
1158    }
1159
1160    fn apply_symmetry_constraints(
1161        &self,
1162        measurements: &Array2<f64>,
1163        violations: &Array1<f64>,
1164        symmetry_group: &SymmetryGroup,
1165    ) -> Result<Array2<f64>> {
1166        // Apply symmetry constraints
1167        Ok(measurements.clone()) // Placeholder
1168    }
1169
1170    fn create_virtual_copies(
1171        &self,
1172        circuit: &QuantumCircuit,
1173        num_copies: usize,
1174    ) -> Result<Vec<QuantumCircuit>> {
1175        // Create virtual copies of circuit
1176        Ok(vec![circuit.clone(); num_copies]) // Placeholder
1177    }
1178
1179    fn measure_virtual_entanglement(&self, circuits: &[QuantumCircuit]) -> Result<Array1<f64>> {
1180        // Measure entanglement between virtual copies
1181        Ok(Array1::zeros(circuits.len())) // Placeholder
1182    }
1183
1184    fn apply_distillation_protocol(
1185        &self,
1186        measurements: &Array2<f64>,
1187        entanglement_measures: &Array1<f64>,
1188    ) -> Result<Array2<f64>> {
1189        // Apply virtual distillation protocol
1190        Ok(measurements.clone()) // Placeholder
1191    }
1192
1193    fn prepare_correction_input(
1194        &self,
1195        measurements: &Array2<f64>,
1196        predicted_noise: &Array1<f64>,
1197    ) -> Result<Array2<f64>> {
1198        // Prepare input for correction network
1199        Ok(measurements.clone()) // Placeholder
1200    }
1201
1202    /// Real, in-place escalation of the currently selected strategy's
1203    /// hyperparameters toward higher accuracy: for ZNE, adds a higher noise
1204    /// scale factor (more extrapolation data points); for readout-error
1205    /// mitigation, escalates the correction method toward the most accurate
1206    /// (iterative maximum-likelihood) one. Strategies with no honestly
1207    /// implemented computation (CDR, symmetry verification, virtual
1208    /// distillation, ML mitigation, hybrid correction) have no real
1209    /// hyperparameters to escalate and are left untouched.
1210    fn switch_to_best_performing_strategy(&mut self) -> Result<()> {
1211        self.escalate_strategy_precision()
1212    }
1213
1214    /// Real, in-place relaxation of the currently selected strategy's
1215    /// hyperparameters toward lower resource usage: for ZNE, drops the most
1216    /// expensive (highest) noise scale factor; for readout-error
1217    /// mitigation, falls back to the cheapest (exact matrix-inversion)
1218    /// correction method.
1219    fn switch_to_resource_optimal_strategy(&mut self) -> Result<()> {
1220        self.relax_strategy_precision()
1221    }
1222
1223    /// Hybrid policy: escalate precision when the real, data-driven
1224    /// performance score has dropped below the configured threshold,
1225    /// otherwise relax back toward the cheaper configuration.
1226    fn switch_to_hybrid_adaptive_strategy(&mut self) -> Result<()> {
1227        if self.performance_metrics.current_performance()
1228            < self.adaptive_config.performance_threshold
1229        {
1230            self.escalate_strategy_precision()
1231        } else {
1232            self.relax_strategy_precision()
1233        }
1234    }
1235
1236    fn escalate_strategy_precision(&mut self) -> Result<()> {
1237        match &mut self.mitigation_strategy {
1238            MitigationStrategy::ZNE { scale_factors, .. } => {
1239                let max_existing = scale_factors.iter().cloned().fold(1.0_f64, f64::max);
1240                let next_scale = max_existing + 2.0;
1241                if !scale_factors.iter().any(|&s| (s - next_scale).abs() < 1e-9) {
1242                    scale_factors.push(next_scale);
1243                    scale_factors
1244                        .sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1245                }
1246            }
1247            MitigationStrategy::ReadoutErrorMitigation {
1248                correction_method, ..
1249            } => {
1250                *correction_method = match correction_method {
1251                    ReadoutCorrectionMethod::MatrixInversion => {
1252                        ReadoutCorrectionMethod::ConstrainedLeastSquares
1253                    }
1254                    ReadoutCorrectionMethod::ConstrainedLeastSquares
1255                    | ReadoutCorrectionMethod::IterativeMaximumLikelihood => {
1256                        ReadoutCorrectionMethod::IterativeMaximumLikelihood
1257                    }
1258                };
1259            }
1260            _ => {}
1261        }
1262        Ok(())
1263    }
1264
1265    fn relax_strategy_precision(&mut self) -> Result<()> {
1266        match &mut self.mitigation_strategy {
1267            MitigationStrategy::ZNE { scale_factors, .. } => {
1268                if scale_factors.len() > 2 {
1269                    let max_idx = scale_factors
1270                        .iter()
1271                        .enumerate()
1272                        .max_by(|(_, a), (_, b)| {
1273                            a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
1274                        })
1275                        .map(|(idx, _)| idx);
1276                    if let Some(idx) = max_idx {
1277                        scale_factors.remove(idx);
1278                    }
1279                }
1280            }
1281            MitigationStrategy::ReadoutErrorMitigation {
1282                correction_method, ..
1283            } => {
1284                *correction_method = ReadoutCorrectionMethod::MatrixInversion;
1285            }
1286            _ => {}
1287        }
1288        Ok(())
1289    }
1290}
1291
1292#[cfg(test)]
1293mod tests;