diff --git a/compiler/qsc/src/interpret/circuit_tests.rs b/compiler/qsc/src/interpret/circuit_tests.rs index 2918abda6f..53d3731732 100644 --- a/compiler/qsc/src/interpret/circuit_tests.rs +++ b/compiler/qsc/src/interpret/circuit_tests.rs @@ -513,6 +513,34 @@ fn custom_intrinsic_mixed_args() { assert_eq!(circ.operations.len(), 1); } +#[test] +fn custom_intrinsic_apply_idle_noise() { + let mut interpreter = interpreter( + r" + namespace Test { + import Std.Diagnostics.*; + @EntryPoint() + operation Main() : Unit { + ConfigurePauliNoise(BitFlipNoise(1.0)); + use q = Qubit(); + ApplyIdleNoise(q); + } + }", + Profile::Unrestricted, + ); + + let circ = interpreter + .circuit(CircuitEntryPoint::EntryPoint, false) + .expect("circuit generation should succeed"); + + // ConfigurePauliNoise has no qubit qrguments so it shouldn't show up. + // ApplyIdleNoise is a quantum operation so it shows up. + expect![[r#" + q_0 ApplyIdleNoise + "#]] + .assert_eq(&circ.to_string()); +} + #[test] fn operation_with_qubits() { let mut interpreter = interpreter( diff --git a/compiler/qsc_eval/src/backend.rs b/compiler/qsc_eval/src/backend.rs index 2270851e8a..2a6d4df654 100644 --- a/compiler/qsc_eval/src/backend.rs +++ b/compiler/qsc_eval/src/backend.rs @@ -101,6 +101,9 @@ pub trait Backend { fn qubit_is_zero(&mut self, _q: usize) -> bool { unimplemented!("qubit_is_zero operation"); } + /// Executes custom intrinsic specified by `_name`. + /// Returns None if this intrinsic is unknown. + /// Otherwise returns Some(Result), with the Result from intrinsic. fn custom_intrinsic(&mut self, _name: &str, _arg: Value) -> Option> { None } @@ -137,14 +140,17 @@ impl SparseSim { #[must_use] pub fn new_with_noise(noise: &PauliNoise) -> Self { - Self { - sim: QuantumSim::new(None), - noise: *noise, - rng: if noise.is_noiseless() { - None - } else { - Some(StdRng::from_entropy()) - }, + let mut sim = SparseSim::new(); + sim.set_noise(noise); + sim + } + + fn set_noise(&mut self, noise: &PauliNoise) { + self.noise = *noise; + if noise.is_noiseless() { + self.rng = None; + } else { + self.rng = Some(StdRng::from_entropy()); } } @@ -393,6 +399,26 @@ impl Backend for SparseSim { | "AccountForEstimatesInternal" | "BeginRepeatEstimatesInternal" | "EndRepeatEstimatesInternal" => Some(Ok(Value::unit())), + "ConfigurePauliNoise" => { + let [xv, yv, zv] = &*arg.unwrap_tuple() else { + panic!("tuple arity for SetPauliNoise intrinsic should be 3"); + }; + let px = xv.get_double(); + let py = yv.get_double(); + let pz = zv.get_double(); + match PauliNoise::from_probabilities(px, py, pz) { + Ok(noise) => { + self.set_noise(&noise); + Some(Ok(Value::unit())) + } + Err(message) => Some(Err(message)), + } + } + "ApplyIdleNoise" => { + let q = arg.unwrap_qubit().0; + self.apply_noise(q); + Some(Ok(Value::unit())) + } _ => None, } } diff --git a/compiler/qsc_eval/src/intrinsic/tests.rs b/compiler/qsc_eval/src/intrinsic/tests.rs index 6ab588d929..ae94342a13 100644 --- a/compiler/qsc_eval/src/intrinsic/tests.rs +++ b/compiler/qsc_eval/src/intrinsic/tests.rs @@ -143,7 +143,7 @@ impl Backend for CustomSim { match name { "Add1" => Some(Ok(Value::Int(arg.unwrap_int() + 1))), "Check" => Some(Err("cannot verify input".to_string())), - _ => None, + _ => self.sim.custom_intrinsic(name, arg), } } } @@ -1595,3 +1595,23 @@ fn start_counting_qubits_called_twice_before_stop_fails() { &expect!["qubits already counted"], ); } + +#[test] +fn check_pauli_noise() { + check_intrinsic_output( + "", + indoc! {"{ + import Std.Diagnostics.*; + use q = Qubit(); + ConfigurePauliNoise(BitFlipNoise(1.0)); + ApplyIdleNoise(q); + ConfigurePauliNoise(NoNoise()); + DumpMachine(); + Reset(q); + }"}, + &expect![[r#" + STATE: + |1⟩: 1.0000+0.0000𝑖 + "#]], + ); +} diff --git a/compiler/qsc_eval/src/val.rs b/compiler/qsc_eval/src/val.rs index 52f2679b90..5123ad0cc4 100644 --- a/compiler/qsc_eval/src/val.rs +++ b/compiler/qsc_eval/src/val.rs @@ -263,6 +263,14 @@ impl Value { v } + #[must_use] + pub fn get_double(&self) -> f64 { + let Value::Double(v) = self else { + panic!("value should be Double, got {}", self.type_name()); + }; + *v + } + /// Convert the [Value] into a global tuple /// # Panics /// This will panic if the [Value] is not a [`Value::Global`]. diff --git a/compiler/qsc_partial_eval/src/lib.rs b/compiler/qsc_partial_eval/src/lib.rs index b759aca1df..5e7a5e2050 100644 --- a/compiler/qsc_partial_eval/src/lib.rs +++ b/compiler/qsc_partial_eval/src/lib.rs @@ -1344,6 +1344,7 @@ impl<'a> PartialEvaluator<'a> { | "AccountForEstimatesInternal" | "BeginRepeatEstimatesInternal" | "EndRepeatEstimatesInternal" + | "ApplyIdleNoise" | "GlobalPhase" => Ok(Value::unit()), // The following intrinsic functions and operations should never make it past conditional compilation and // the capabilities check pass. diff --git a/compiler/qsc_partial_eval/src/management.rs b/compiler/qsc_partial_eval/src/management.rs index 36da25d8e5..542cec04de 100644 --- a/compiler/qsc_partial_eval/src/management.rs +++ b/compiler/qsc_partial_eval/src/management.rs @@ -133,7 +133,9 @@ impl Backend for QuantumIntrinsicsChecker { ) -> Option> { match name { "BeginEstimateCaching" => Some(Ok(Value::Bool(true))), - "EndEstimateCaching" | "GlobalPhase" => Some(Ok(Value::unit())), + "EndEstimateCaching" | "GlobalPhase" | "ConfigurePauliNoise" | "ApplyIdleNoise" => { + Some(Ok(Value::unit())) + } _ => None, } } diff --git a/library/src/tests/diagnostics.rs b/library/src/tests/diagnostics.rs index a7789488c1..53f649bc22 100644 --- a/library/src/tests/diagnostics.rs +++ b/library/src/tests/diagnostics.rs @@ -424,3 +424,35 @@ fn check_dumpoperation_with_extra_qubits_relative_phase_not_reflected_in_matrix( "#]] .assert_eq(&output); } + +#[test] +fn check_bit_flip_noise_values() { + test_expression( + "Std.Diagnostics.BitFlipNoise(0.3)", + &Value::Tuple([Value::Double(0.3), Value::Double(0.0), Value::Double(0.0)].into()), + ); +} + +#[test] +fn check_phase_flip_noise_values() { + test_expression( + "Std.Diagnostics.PhaseFlipNoise(0.3)", + &Value::Tuple([Value::Double(0.0), Value::Double(0.0), Value::Double(0.3)].into()), + ); +} + +#[test] +fn check_depolarizing_noise_values() { + test_expression( + "Std.Diagnostics.DepolarizingNoise(0.3)", + &Value::Tuple([Value::Double(0.1), Value::Double(0.1), Value::Double(0.1)].into()), + ); +} + +#[test] +fn check_no_noise_values() { + test_expression( + "Std.Diagnostics.NoNoise()", + &Value::Tuple([Value::Double(0.0), Value::Double(0.0), Value::Double(0.0)].into()), + ); +} diff --git a/library/std/src/Std/Diagnostics.qs b/library/std/src/Std/Diagnostics.qs index 5f7d4894fa..290ef0d619 100644 --- a/library/std/src/Std/Diagnostics.qs +++ b/library/std/src/Std/Diagnostics.qs @@ -368,6 +368,68 @@ operation StopCountingQubits() : Int { body intrinsic; } +/// # Summary +/// Configures Pauli noise for simulation. +/// +/// # Description +/// This function configures Pauli noise for simulation. Parameters represent +/// probabilities of applying X, Y, and Z gates and must add up to at most 1.0. +/// Noise is applied after each gate and before each measurement in the simulator +/// backend. Decompositions may affect the number of times noise is applied. +/// Use 0.0 for all parameters to simulate without noise. +/// +/// # Input +/// ## px +/// Probability of applying X gate. +/// ## py +/// Probability of applying Y gate. +/// ## pz +/// Probability of applying Z gate. +function ConfigurePauliNoise(px : Double, py : Double, pz : Double) : Unit { + body intrinsic; +} + +/// # Summary +/// Applies configured noise to a qubit. +/// +/// # Description +/// This operation applies configured noise to a qubit during simulation. For example, +/// if configured noise is a bit-flip noise with 5% probability, the X gate will be applied +/// with 5% probability. If no noise is configured, no noise is applied. +/// This is useful to simulate noise during idle periods. It could also be used to +/// apply noise immediately after qubit allocation. +/// +/// # Input +/// ## qubit +/// The qubit to which noise is applied. +operation ApplyIdleNoise(qubit : Qubit) : Unit { + body intrinsic; +} + +/// # Summary +/// The bit flip noise with probability `p`. +function BitFlipNoise(p : Double) : (Double, Double, Double) { + (p, 0.0, 0.0) +} + +/// # Summary +/// The phase flip noise with probability `p`. +function PhaseFlipNoise(p : Double) : (Double, Double, Double) { + (0.0, 0.0, p) +} + +/// # Summary +/// The depolarizing noise with probability `p`. +function DepolarizingNoise(p : Double) : (Double, Double, Double) { + (p / 3.0, p / 3.0, p / 3.0) +} + +/// # Summary +/// No noise for noiseless operation. +function NoNoise() : (Double, Double, Double) { + (0.0, 0.0, 0.0) +} + export DumpMachine, DumpRegister, @@ -381,4 +443,10 @@ export StartCountingFunction, StopCountingFunction, StartCountingQubits, - StopCountingQubits; + StopCountingQubits, + ConfigurePauliNoise, + ApplyIdleNoise, + BitFlipNoise, + PhaseFlipNoise, + DepolarizingNoise, + NoNoise; diff --git a/resource_estimator/src/counts.rs b/resource_estimator/src/counts.rs index b1edf87a03..3b840c6411 100644 --- a/resource_estimator/src/counts.rs +++ b/resource_estimator/src/counts.rs @@ -522,7 +522,6 @@ impl Backend for LogicalCounter { fn custom_intrinsic(&mut self, name: &str, arg: Value) -> Option> { match name { - "GlobalPhase" => Some(Ok(Value::unit())), "BeginEstimateCaching" => { let values = arg.unwrap_tuple(); let [cache_name, cache_variant] = array::from_fn(|i| values[i].clone()); @@ -565,6 +564,7 @@ impl Backend for LogicalCounter { .map(|()| Value::unit()), ) } + "GlobalPhase" | "ConfigurePauliNoise" | "ApplyIdleNoise" => Some(Ok(Value::unit())), _ => None, } }