From afdc0a1e91df2274552c8f29e1219c422c58bbb6 Mon Sep 17 00:00:00 2001 From: ciaranra Date: Fri, 18 Sep 2026 11:42:12 -0600 Subject: [PATCH] Normalize two-qubit noise-rate inputs to signed principal angles --- crates/pecos-engines/src/noise/general.rs | 58 +++++++++++++++++++++++ exp/pecos-neo/src/noise/two_qubit.rs | 26 +++++++++- 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/crates/pecos-engines/src/noise/general.rs b/crates/pecos-engines/src/noise/general.rs index cafa18574..5152da777 100644 --- a/crates/pecos-engines/src/noise/general.rs +++ b/crates/pecos-engines/src/noise/general.rs @@ -1432,8 +1432,21 @@ impl GeneralNoiseModel { /// Calculate the two-qubit gate error rate based on the rotation angle /// /// with additional support for asymmetric scaling and power-law scaling + /// + /// Angles are reduced modulo a full turn into `(-pi, pi]` before selecting + /// the signed coefficients. Thus an unsigned angle such as `7*pi/4` uses + /// the negative branch at `-pi/4`; both half-turn endpoints use `+pi`. #[must_use] pub fn p2_angle_error_rate(&self, angle: f64) -> f64 { + let angle = angle % std::f64::consts::TAU; + let angle = if angle > std::f64::consts::PI { + angle - std::f64::consts::TAU + } else if angle <= -std::f64::consts::PI { + angle + std::f64::consts::TAU + } else { + angle + }; + // Normalize angle by π - convert to a value in [0, 1] range let theta = angle.abs() / std::f64::consts::PI; @@ -3762,6 +3775,51 @@ mod tests { ); } + #[test] + fn test_rzz_error_rate_signed_periodicity() { + use std::f64::consts::{FRAC_PI_2, FRAC_PI_4, PI, TAU}; + + let mut model = GeneralNoiseModel::builder() + .with_p2(0.01) + .with_p2_angle_params(2.0, 0.1, 3.0, 0.2) + .with_p2_angle_power(2.0) + .build(); + let noise = model + .as_any_mut() + .downcast_mut::() + .unwrap(); + + // Distinct coefficients expose selection of the wrong sign branch. + for (angle, scaling) in [ + (0.0, 0.15), + (FRAC_PI_4, 0.3875), + (-FRAC_PI_4, 0.225), + (FRAC_PI_2, 0.95), + (-FRAC_PI_2, 0.6), + (PI, 3.2), + (-PI, 3.2), + ] { + for turns in [-4.0, -1.0, 0.0, 1.0, 4.0] { + let input = angle + turns * TAU; + let actual = noise.p2_angle_error_rate(input); + let expected = 0.01 * scaling; + assert!( + (actual - expected).abs() < 1e-12, + "angle {input}: expected {expected}, got {actual}" + ); + } + } + + // Avoid adding pi before reducing: that can erase tiny signed inputs. + assert!((noise.p2_angle_error_rate(-1e-20) - 0.001).abs() < 1e-12); + assert!((noise.p2_angle_error_rate(1e-20) - 0.002).abs() < 1e-12); + // The positive half-turn endpoint must not absorb neighboring angles. + assert!((noise.p2_angle_error_rate(PI.next_up()) - 0.021).abs() < 1e-12); + assert!((noise.p2_angle_error_rate(PI.next_down()) - 0.032).abs() < 1e-12); + assert!((noise.p2_angle_error_rate((-PI).next_up()) - 0.021).abs() < 1e-12); + assert!((noise.p2_angle_error_rate((-PI).next_down()) - 0.032).abs() < 1e-12); + } + #[test] fn test_noiseless_gates() { // Create a noise model and mark RZ as a noiseless gate diff --git a/exp/pecos-neo/src/noise/two_qubit.rs b/exp/pecos-neo/src/noise/two_qubit.rs index 2b0f21894..2646acef5 100644 --- a/exp/pecos-neo/src/noise/two_qubit.rs +++ b/exp/pecos-neo/src/noise/two_qubit.rs @@ -224,7 +224,8 @@ impl AngleScaling { /// Calculate the scaling factor for a given angle. /// - /// Uses signed radians in [-pi, pi] for asymmetric scaling. + /// Uses signed radians in `(-pi, pi]` for asymmetric scaling. Unsigned + /// angles above pi use the negative branch; a half turn uses positive pi. /// The magnitude is normalized by pi before applying power. /// /// Formula: `offset + linear*|θ/π| + scale*|θ/π|^power` @@ -812,6 +813,29 @@ mod tests { assert!((scaling.scale(Angle64::HALF_TURN) - 1.2).abs() < 1e-10); } + #[test] + fn test_asymmetric_angle_scaling_wrapped_boundaries() { + use std::f64::consts::{FRAC_PI_4, PI, TAU}; + + let scaling = AngleScaling::asymmetric(0.1, 0.0, 2.0, 0.2, 0.0, 3.0, 2.0); + for (angle, expected) in [ + (0.0, 0.15), + (FRAC_PI_4, 0.3875), + (-FRAC_PI_4, 0.225), + (PI, 3.2), + (-PI, 3.2), + ] { + for turns in [-4.0, -1.0, 0.0, 1.0, 4.0] { + let input = angle + turns * TAU; + let actual = scaling.scale(Angle64::from_radians(input)); + assert!( + (actual - expected).abs() < 1e-12, + "angle {input}: expected {expected}, got {actual}" + ); + } + } + } + #[test] fn test_polynomial_angle_scaling() { // Test the full polynomial formula: a + b*|θ/π| + c*|θ/π|^d