Skip to main content

quantrs2_sim/
error_mitigation.rs

1//! Error mitigation strategies for quantum computing
2//!
3//! This module provides state-of-the-art error mitigation techniques to improve
4//! the accuracy of noisy quantum simulations and real quantum hardware results.
5//!
6//! # Supported Techniques
7//!
8//! - **Zero-Noise Extrapolation (ZNE)**: Extrapolate results to zero noise limit
9//! - **Probabilistic Error Cancellation (PEC)**: Use quasi-probability to cancel errors
10//! - **Clifford Data Regression (CDR)**: Noise characterization with Clifford circuits
11//! - **Measurement Error Mitigation**: Correct readout errors using calibration
12//! - **Symmetry Verification**: Verify conservation laws and post-select results
13//!
14//! # Example
15//!
16//! ```ignore
17//! use quantrs2_sim::error_mitigation::{ZeroNoiseExtrapolation, ExtrapolationMethod};
18//!
19//! let zne = ZeroNoiseExtrapolation::new(ExtrapolationMethod::Richardson);
20//! let mitigated_result = zne.apply(&noisy_results, &noise_scales)?;
21//! ```
22
23use crate::error::{Result, SimulatorError};
24use scirs2_core::ndarray::{Array1, Array2};
25use scirs2_core::random::prelude::*;
26use scirs2_core::Complex64;
27use std::collections::HashMap;
28
29// ============================================================================
30// Zero-Noise Extrapolation (ZNE)
31// ============================================================================
32
33/// Extrapolation methods for ZNE
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum ExtrapolationMethod {
36    /// Linear extrapolation (2 points)
37    Linear,
38    /// Richardson extrapolation (3+ points)
39    Richardson,
40    /// Polynomial fit
41    Polynomial { degree: usize },
42    /// Exponential fit
43    Exponential,
44}
45
46/// Zero-Noise Extrapolation for error mitigation
47///
48/// ZNE runs the same circuit at different noise levels and extrapolates
49/// the result to the zero-noise limit.
50#[derive(Debug, Clone)]
51pub struct ZeroNoiseExtrapolation {
52    /// Extrapolation method
53    method: ExtrapolationMethod,
54    /// Noise scale factors to use (e.g., [1.0, 1.5, 2.0])
55    scale_factors: Vec<f64>,
56}
57
58impl ZeroNoiseExtrapolation {
59    /// Create a new ZNE instance with default scale factors
60    pub fn new(method: ExtrapolationMethod) -> Self {
61        Self {
62            method,
63            scale_factors: vec![1.0, 1.5, 2.0, 2.5, 3.0],
64        }
65    }
66
67    /// Create ZNE with custom scale factors
68    pub fn with_scale_factors(
69        method: ExtrapolationMethod,
70        scale_factors: Vec<f64>,
71    ) -> Result<Self> {
72        if scale_factors.is_empty() {
73            return Err(SimulatorError::InvalidInput(
74                "Scale factors cannot be empty".to_string(),
75            ));
76        }
77
78        // Verify scale factors are in ascending order and start with 1.0
79        if scale_factors[0] != 1.0 {
80            return Err(SimulatorError::InvalidInput(
81                "First scale factor must be 1.0 (original noise level)".to_string(),
82            ));
83        }
84
85        for i in 1..scale_factors.len() {
86            if scale_factors[i] <= scale_factors[i - 1] {
87                return Err(SimulatorError::InvalidInput(
88                    "Scale factors must be in strictly ascending order".to_string(),
89                ));
90            }
91        }
92
93        Ok(Self {
94            method,
95            scale_factors,
96        })
97    }
98
99    /// Apply ZNE to a set of noisy expectation values
100    ///
101    /// # Arguments
102    ///
103    /// * `noisy_values` - Expectation values at different noise scales
104    /// * `noise_scales` - Corresponding noise scale factors
105    pub fn apply(&self, noisy_values: &[f64], noise_scales: &[f64]) -> Result<f64> {
106        if noisy_values.len() != noise_scales.len() {
107            return Err(SimulatorError::InvalidInput(
108                "Number of values must match number of scales".to_string(),
109            ));
110        }
111
112        if noisy_values.len() < 2 {
113            return Err(SimulatorError::InvalidInput(
114                "At least 2 data points required for extrapolation".to_string(),
115            ));
116        }
117
118        match self.method {
119            ExtrapolationMethod::Linear => self.linear_extrapolation(noisy_values, noise_scales),
120            ExtrapolationMethod::Richardson => {
121                self.richardson_extrapolation(noisy_values, noise_scales)
122            }
123            ExtrapolationMethod::Polynomial { degree } => {
124                self.polynomial_extrapolation(noisy_values, noise_scales, degree)
125            }
126            ExtrapolationMethod::Exponential => {
127                self.exponential_extrapolation(noisy_values, noise_scales)
128            }
129        }
130    }
131
132    /// Linear extrapolation using first two points
133    fn linear_extrapolation(&self, values: &[f64], scales: &[f64]) -> Result<f64> {
134        if values.len() < 2 {
135            return Err(SimulatorError::InvalidInput(
136                "Linear extrapolation requires at least 2 points".to_string(),
137            ));
138        }
139
140        let x1 = scales[0];
141        let y1 = values[0];
142        let x2 = scales[1];
143        let y2 = values[1];
144
145        // Extrapolate to x=0 (zero noise)
146        let slope = (y2 - y1) / (x2 - x1);
147        Ok(y1 - slope * x1)
148    }
149
150    /// Richardson extrapolation (higher order)
151    fn richardson_extrapolation(&self, values: &[f64], scales: &[f64]) -> Result<f64> {
152        if values.len() < 3 {
153            return Err(SimulatorError::InvalidInput(
154                "Richardson extrapolation requires at least 3 points".to_string(),
155            ));
156        }
157
158        // Use quadratic Richardson extrapolation
159        let x0 = scales[0];
160        let x1 = scales[1];
161        let x2 = scales[2];
162        let y0 = values[0];
163        let y1 = values[1];
164        let y2 = values[2];
165
166        // Fit quadratic: y = a*x^2 + b*x + c
167        // Extrapolate to x=0 gives c
168        let denom = (x0 - x1) * (x0 - x2) * (x1 - x2);
169        if denom.abs() < 1e-10 {
170            return Err(SimulatorError::InvalidInput(
171                "Scale factors too close for stable extrapolation".to_string(),
172            ));
173        }
174
175        let a = (x2 * (y1 - y0) + x0 * (y2 - y1) + x1 * (y0 - y2)) / denom;
176        let b = (x2 * x2 * (y0 - y1) + x0 * x0 * (y1 - y2) + x1 * x1 * (y2 - y0)) / denom;
177        let c = (x1 * x2 * (x1 - x2) * y0 + x2 * x0 * (x2 - x0) * y1 + x0 * x1 * (x0 - x1) * y2)
178            / denom;
179
180        Ok(c)
181    }
182
183    /// Polynomial fit extrapolation
184    fn polynomial_extrapolation(
185        &self,
186        values: &[f64],
187        scales: &[f64],
188        degree: usize,
189    ) -> Result<f64> {
190        if values.len() <= degree {
191            return Err(SimulatorError::InvalidInput(format!(
192                "Need at least {} points for degree {} polynomial",
193                degree + 1,
194                degree
195            )));
196        }
197
198        // Polynomial fit via Lagrange interpolation evaluated at x=0.
199        // Uses the first (degree+1) points. For degree ≤ 2, delegates to the
200        // specialised implementations; for degree ≥ 3, applies generic Lagrange.
201        match degree {
202            1 => self.linear_extrapolation(values, scales),
203            2 => self.richardson_extrapolation(values, scales),
204            _ => {
205                // Generic Lagrange interpolation at x=0 using (degree+1) points.
206                // L(0) = Σ_i y_i * Π_{j≠i} (0 - x_j) / (x_i - x_j)
207                let n_pts = degree + 1;
208                let xs = &scales[..n_pts];
209                let ys = &values[..n_pts];
210                let mut result = 0.0_f64;
211                for i in 0..n_pts {
212                    let mut num = 1.0_f64;
213                    let mut den = 1.0_f64;
214                    for j in 0..n_pts {
215                        if j != i {
216                            num *= -xs[j];
217                            den *= xs[i] - xs[j];
218                        }
219                    }
220                    if den.abs() < 1e-15 {
221                        return Err(SimulatorError::InvalidInput(
222                            "Degenerate scale factors — cannot form stable Lagrange basis"
223                                .to_string(),
224                        ));
225                    }
226                    result += ys[i] * num / den;
227                }
228                Ok(result)
229            }
230        }
231    }
232
233    /// Exponential fit extrapolation: y = a * exp(-b*x) + c
234    fn exponential_extrapolation(&self, values: &[f64], scales: &[f64]) -> Result<f64> {
235        if values.len() < 3 {
236            return Err(SimulatorError::InvalidInput(
237                "Exponential extrapolation requires at least 3 points".to_string(),
238            ));
239        }
240
241        // Simplified: assume y = a * exp(-b*x) + c where c is the zero-noise value
242        // Use first derivative at x=0 from first two points
243        let x0 = scales[0];
244        let x1 = scales[1];
245        let y0 = values[0];
246        let y1 = values[1];
247
248        // Estimate decay rate
249        if (y1 - y0).abs() < 1e-10 {
250            return Ok(y0); // No decay observed
251        }
252
253        let b_estimate = -((y1 - y0) / (x1 - x0)) / y0.max(1e-10);
254        let c_estimate = y0 / (1.0 + b_estimate * x0).max(1e-10);
255
256        Ok(c_estimate)
257    }
258
259    /// Get the scale factors
260    pub fn scale_factors(&self) -> &[f64] {
261        &self.scale_factors
262    }
263}
264
265// ============================================================================
266// Measurement Error Mitigation
267// ============================================================================
268
269/// Measurement error mitigation using calibration matrices
270///
271/// Corrects readout errors by inverting the noise transfer matrix
272/// measured through calibration circuits.
273#[derive(Debug, Clone)]
274pub struct MeasurementErrorMitigation {
275    /// Calibration matrix: M[i][j] = P(measure i | prepared j)
276    calibration_matrix: Array2<f64>,
277    /// Inverse calibration matrix (for mitigation)
278    inverse_matrix: Option<Array2<f64>>,
279    /// Number of qubits
280    n_qubits: usize,
281}
282
283impl MeasurementErrorMitigation {
284    /// Create a new measurement error mitigation instance
285    ///
286    /// # Arguments
287    ///
288    /// * `n_qubits` - Number of qubits
289    pub fn new(n_qubits: usize) -> Self {
290        let dim = 1 << n_qubits; // 2^n
291        let calibration_matrix = Array2::eye(dim); // Identity by default (no error)
292
293        Self {
294            calibration_matrix,
295            inverse_matrix: None,
296            n_qubits,
297        }
298    }
299
300    /// Set the calibration matrix from measurements
301    ///
302    /// The calibration matrix `M[i][j]` represents the probability of
303    /// measuring bitstring i when bitstring j was prepared.
304    pub fn set_calibration_matrix(&mut self, matrix: Array2<f64>) -> Result<()> {
305        let expected_dim = 1 << self.n_qubits;
306        if matrix.nrows() != expected_dim || matrix.ncols() != expected_dim {
307            return Err(SimulatorError::InvalidInput(format!(
308                "Calibration matrix must be {}x{} for {} qubits",
309                expected_dim, expected_dim, self.n_qubits
310            )));
311        }
312
313        // Verify matrix is stochastic (columns sum to 1)
314        for col in 0..matrix.ncols() {
315            let sum: f64 = (0..matrix.nrows()).map(|row| matrix[[row, col]]).sum();
316            if (sum - 1.0).abs() > 1e-6 {
317                return Err(SimulatorError::InvalidInput(format!(
318                    "Column {} does not sum to 1.0 (sum = {})",
319                    col, sum
320                )));
321            }
322        }
323
324        self.calibration_matrix = matrix;
325        self.inverse_matrix = None; // Will be computed on demand
326        Ok(())
327    }
328
329    /// Compute and cache the inverse calibration matrix
330    fn compute_inverse(&mut self) -> Result<()> {
331        if self.inverse_matrix.is_some() {
332            return Ok(());
333        }
334
335        // Use pseudo-inverse for stability
336        // For now, use simple matrix inversion (can be improved with SVD)
337        let matrix = &self.calibration_matrix;
338        let n = matrix.nrows();
339
340        // Simple Gauss-Jordan elimination for small matrices
341        let mut augmented = Array2::zeros((n, 2 * n));
342        for i in 0..n {
343            for j in 0..n {
344                augmented[[i, j]] = matrix[[i, j]];
345                augmented[[i, j + n]] = if i == j { 1.0 } else { 0.0 };
346            }
347        }
348
349        // Forward elimination (simplified, may need pivoting for stability)
350        for i in 0..n {
351            // Find pivot
352            let pivot = augmented[[i, i]];
353            if pivot.abs() < 1e-10 {
354                return Err(SimulatorError::InvalidInput(
355                    "Calibration matrix is singular or nearly singular".to_string(),
356                ));
357            }
358
359            // Scale row
360            for j in 0..(2 * n) {
361                augmented[[i, j]] /= pivot;
362            }
363
364            // Eliminate
365            for k in 0..n {
366                if k != i {
367                    let factor = augmented[[k, i]];
368                    for j in 0..(2 * n) {
369                        augmented[[k, j]] -= factor * augmented[[i, j]];
370                    }
371                }
372            }
373        }
374
375        // Extract inverse from augmented matrix
376        let mut inverse = Array2::zeros((n, n));
377        for i in 0..n {
378            for j in 0..n {
379                inverse[[i, j]] = augmented[[i, j + n]];
380            }
381        }
382
383        self.inverse_matrix = Some(inverse);
384        Ok(())
385    }
386
387    /// Apply measurement error mitigation to noisy counts
388    ///
389    /// # Arguments
390    ///
391    /// * `noisy_counts` - Raw measurement counts from the quantum circuit
392    ///
393    /// # Returns
394    ///
395    /// Mitigated counts (may contain negative values due to inversion)
396    pub fn apply(&mut self, noisy_counts: &HashMap<String, usize>) -> Result<HashMap<String, f64>> {
397        self.compute_inverse()?;
398        let inverse = self.inverse_matrix.as_ref().ok_or_else(|| {
399            SimulatorError::InvalidInput(
400                "Inverse matrix not yet computed — call compute_inverse() first".to_string(),
401            )
402        })?;
403
404        let dim = 1 << self.n_qubits;
405        let total_shots: usize = noisy_counts.values().sum();
406
407        // Convert counts to probability vector
408        let mut noisy_probs = Array1::zeros(dim);
409        for (bitstring, count) in noisy_counts {
410            if bitstring.len() != self.n_qubits {
411                return Err(SimulatorError::InvalidInput(format!(
412                    "Bitstring {} has wrong length (expected {})",
413                    bitstring, self.n_qubits
414                )));
415            }
416            let index = usize::from_str_radix(bitstring, 2).map_err(|_| {
417                SimulatorError::InvalidInput(format!("Invalid bitstring: {}", bitstring))
418            })?;
419            noisy_probs[index] = *count as f64 / total_shots as f64;
420        }
421
422        // Apply inverse: mitigated_probs = M^(-1) * noisy_probs
423        let mitigated_probs = inverse.dot(&noisy_probs);
424
425        // Convert back to counts
426        let mut mitigated_counts = HashMap::new();
427        for i in 0..dim {
428            let bitstring = format!("{:0width$b}", i, width = self.n_qubits);
429            let mitigated_count = mitigated_probs[i] * total_shots as f64;
430            if mitigated_count.abs() > 1e-10 {
431                mitigated_counts.insert(bitstring, mitigated_count);
432            }
433        }
434
435        Ok(mitigated_counts)
436    }
437
438    /// Generate calibration matrix from error rates
439    ///
440    /// # Arguments
441    ///
442    /// * `readout_error_0` - Probability of measuring 1 when prepared in |0⟩
443    /// * `readout_error_1` - Probability of measuring 0 when prepared in |1⟩
444    pub fn from_error_rates(
445        n_qubits: usize,
446        readout_error_0: f64,
447        readout_error_1: f64,
448    ) -> Result<Self> {
449        if !(0.0..=1.0).contains(&readout_error_0) {
450            return Err(SimulatorError::InvalidInput(
451                "readout_error_0 must be in [0, 1]".to_string(),
452            ));
453        }
454        if !(0.0..=1.0).contains(&readout_error_1) {
455            return Err(SimulatorError::InvalidInput(
456                "readout_error_1 must be in [0, 1]".to_string(),
457            ));
458        }
459
460        let dim = 1 << n_qubits;
461        let mut matrix = Array2::zeros((dim, dim));
462
463        // Build tensor product of single-qubit error matrices
464        // Single-qubit matrix: [[1-e0, e1], [e0, 1-e1]]
465        for prepared in 0..dim {
466            for measured in 0..dim {
467                let mut prob = 1.0;
468                for qubit in 0..n_qubits {
469                    let prepared_bit = (prepared >> qubit) & 1;
470                    let measured_bit = (measured >> qubit) & 1;
471
472                    let p = if prepared_bit == 0 {
473                        if measured_bit == 0 {
474                            1.0 - readout_error_0
475                        } else {
476                            readout_error_0
477                        }
478                    } else if measured_bit == 1 {
479                        1.0 - readout_error_1
480                    } else {
481                        readout_error_1
482                    };
483
484                    prob *= p;
485                }
486                matrix[[measured, prepared]] = prob;
487            }
488        }
489
490        let mut mem = Self::new(n_qubits);
491        mem.set_calibration_matrix(matrix)?;
492        Ok(mem)
493    }
494}
495
496// ============================================================================
497// Symmetry Verification
498// ============================================================================
499
500/// Symmetry verification for post-selection
501///
502/// Verifies that measurement results respect known symmetries
503/// (e.g., particle number conservation, parity) and rejects
504/// results that violate symmetries.
505#[derive(Debug, Clone)]
506pub struct SymmetryVerification {
507    /// Type of symmetry to verify
508    symmetry_type: SymmetryType,
509    /// Expected symmetry value
510    expected_value: Option<i32>,
511}
512
513/// Types of symmetries
514#[derive(Debug, Clone, Copy, PartialEq, Eq)]
515pub enum SymmetryType {
516    /// Particle number conservation (count of 1s)
517    ParticleNumber,
518    /// Parity (even/odd number of 1s)
519    Parity,
520    /// Custom symmetry (user-defined)
521    Custom,
522}
523
524impl SymmetryVerification {
525    /// Create a new symmetry verification instance
526    pub fn new(symmetry_type: SymmetryType) -> Self {
527        Self {
528            symmetry_type,
529            expected_value: None,
530        }
531    }
532
533    /// Set the expected symmetry value
534    pub fn with_expected_value(mut self, value: i32) -> Self {
535        self.expected_value = Some(value);
536        self
537    }
538
539    /// Verify if a bitstring satisfies the symmetry
540    pub fn verify(&self, bitstring: &str) -> bool {
541        let symmetry_value = self.compute_symmetry(bitstring);
542
543        if let Some(expected) = self.expected_value {
544            symmetry_value == expected
545        } else {
546            true // No constraint if expected value not set
547        }
548    }
549
550    /// Compute the symmetry value for a bitstring
551    fn compute_symmetry(&self, bitstring: &str) -> i32 {
552        match self.symmetry_type {
553            SymmetryType::ParticleNumber => bitstring.chars().filter(|&c| c == '1').count() as i32,
554            SymmetryType::Parity => {
555                let ones = bitstring.chars().filter(|&c| c == '1').count();
556                (ones % 2) as i32
557            }
558            SymmetryType::Custom => 0, // Placeholder
559        }
560    }
561
562    /// Filter measurement counts based on symmetry
563    pub fn filter_counts(&self, counts: &HashMap<String, usize>) -> HashMap<String, usize> {
564        counts
565            .iter()
566            .filter(|(bitstring, _)| self.verify(bitstring))
567            .map(|(k, v)| (k.clone(), *v))
568            .collect()
569    }
570
571    /// Normalize filtered counts to original total
572    pub fn filter_and_normalize(&self, counts: &HashMap<String, usize>) -> HashMap<String, usize> {
573        let total_shots: usize = counts.values().sum();
574        let filtered = self.filter_counts(counts);
575        let filtered_total: usize = filtered.values().sum();
576
577        if filtered_total == 0 {
578            return HashMap::new();
579        }
580
581        // Renormalize to original total
582        let scale = total_shots as f64 / filtered_total as f64;
583        filtered
584            .iter()
585            .map(|(k, v)| (k.clone(), (*v as f64 * scale).round() as usize))
586            .collect()
587    }
588}
589
590#[cfg(test)]
591mod tests {
592    use super::*;
593
594    #[test]
595    fn test_zne_linear_extrapolation() {
596        let zne = ZeroNoiseExtrapolation::new(ExtrapolationMethod::Linear);
597        let values = vec![0.8, 0.6, 0.4];
598        let scales = vec![1.0, 2.0, 3.0];
599
600        let result = zne.apply(&values, &scales).unwrap();
601        assert!((result - 1.0).abs() < 0.01); // Should extrapolate to ~1.0
602    }
603
604    #[test]
605    fn test_zne_richardson_extrapolation() {
606        let zne = ZeroNoiseExtrapolation::new(ExtrapolationMethod::Richardson);
607        // Quadratic decay: y = 1 - 0.1*x^2
608        let values = vec![1.0, 0.9, 0.6];
609        let scales = vec![0.0, 1.0, 2.0];
610
611        let result = zne.apply(&values, &scales).unwrap();
612        assert!((result - 1.0).abs() < 0.01);
613    }
614
615    #[test]
616    fn test_zne_invalid_input() {
617        let zne = ZeroNoiseExtrapolation::new(ExtrapolationMethod::Linear);
618        let values = vec![0.8];
619        let scales = vec![1.0, 2.0];
620
621        let result = zne.apply(&values, &scales);
622        assert!(result.is_err());
623    }
624
625    #[test]
626    fn test_measurement_error_mitigation_identity() {
627        let mut mem = MeasurementErrorMitigation::new(2);
628
629        let mut counts = HashMap::new();
630        counts.insert("00".to_string(), 100);
631        counts.insert("11".to_string(), 50);
632
633        let mitigated = mem.apply(&counts).unwrap();
634
635        // With identity matrix, should get same counts
636        assert!((mitigated["00"] - 100.0).abs() < 1e-6);
637        assert!((mitigated["11"] - 50.0).abs() < 1e-6);
638    }
639
640    #[test]
641    fn test_measurement_error_mitigation_from_error_rates() {
642        let mem = MeasurementErrorMitigation::from_error_rates(1, 0.1, 0.05).unwrap();
643
644        // Verify calibration matrix structure
645        let matrix = &mem.calibration_matrix;
646        assert_eq!(matrix.nrows(), 2);
647        assert_eq!(matrix.ncols(), 2);
648
649        // Check specific values
650        assert!((matrix[[0, 0]] - 0.9).abs() < 1e-10); // P(0|0) = 1 - e0
651        assert!((matrix[[1, 0]] - 0.1).abs() < 1e-10); // P(1|0) = e0
652        assert!((matrix[[0, 1]] - 0.05).abs() < 1e-10); // P(0|1) = e1
653        assert!((matrix[[1, 1]] - 0.95).abs() < 1e-10); // P(1|1) = 1 - e1
654    }
655
656    #[test]
657    fn test_symmetry_particle_number() {
658        let sym = SymmetryVerification::new(SymmetryType::ParticleNumber).with_expected_value(2);
659
660        assert!(sym.verify("0011"));
661        assert!(sym.verify("1100"));
662        assert!(sym.verify("1010"));
663        assert!(!sym.verify("0001"));
664        assert!(!sym.verify("1111"));
665    }
666
667    #[test]
668    fn test_symmetry_parity() {
669        let sym = SymmetryVerification::new(SymmetryType::Parity).with_expected_value(0);
670
671        assert!(sym.verify("0011")); // Even parity
672        assert!(sym.verify("1100")); // Even parity
673        assert!(!sym.verify("0001")); // Odd parity
674        assert!(!sym.verify("0111")); // Odd parity
675    }
676
677    #[test]
678    fn test_symmetry_filter_counts() {
679        let sym = SymmetryVerification::new(SymmetryType::ParticleNumber).with_expected_value(2);
680
681        let mut counts = HashMap::new();
682        counts.insert("0011".to_string(), 100);
683        counts.insert("1100".to_string(), 50);
684        counts.insert("0001".to_string(), 30); // Should be filtered
685        counts.insert("1111".to_string(), 20); // Should be filtered
686
687        let filtered = sym.filter_counts(&counts);
688
689        assert_eq!(filtered.len(), 2);
690        assert_eq!(filtered["0011"], 100);
691        assert_eq!(filtered["1100"], 50);
692        assert!(!filtered.contains_key("0001"));
693        assert!(!filtered.contains_key("1111"));
694    }
695
696    #[test]
697    fn test_zne_custom_scale_factors() {
698        let result = ZeroNoiseExtrapolation::with_scale_factors(
699            ExtrapolationMethod::Linear,
700            vec![1.0, 1.5, 2.0],
701        );
702        assert!(result.is_ok());
703
704        let bad_result = ZeroNoiseExtrapolation::with_scale_factors(
705            ExtrapolationMethod::Linear,
706            vec![2.0, 1.0], // Not starting with 1.0
707        );
708        assert!(bad_result.is_err());
709    }
710}