Repository: microsoft/qsharp
Pull request #1997 (2024-11-01 00:00:17)

Noise settings available in Q#
Added ConfigurePauliNoise and ApplyIdleNoise to Diagnostric namespace to control noise from Q# program.

github-actions[bot]: _Change in memory usage detected by benchmark._
## Memory Report for 628b84db84258e5b2b6928583c20e58c41304edc

| Test                        | This Branch | On Main  | Difference |
|-----------------------------|-------------|----------| ---------- |
| compile core + standard lib | 18005745 bytes | 17964197 bytes | 41548 bytes
github-actions[bot]: _Change in memory usage detected by benchmark._
## Memory Report for 1b45dd2237cbe489aa1cb305297904de41069e94

| Test                        | This Branch | On Main  | Difference |
|-----------------------------|-------------|----------| ---------- |
| compile core + standard lib | 18005745 bytes | 17964197 bytes | 41548 bytes
github-actions[bot]: _Change in memory usage detected by benchmark._
## Memory Report for 61068b444462407afc190aec2aad53f866c16a4e

| Test                        | This Branch | On Main  | Difference |
|-----------------------------|-------------|----------| ---------- |
| compile core + standard lib | 18005745 bytes | 17964197 bytes | 41548 bytes
github-actions[bot]: _Change in memory usage detected by benchmark._
## Memory Report for 1f6e514dbb8571cedd88c1cdef569a1c749da6a8

| Test                        | This Branch | On Main  | Difference |
|-----------------------------|-------------|----------| ---------- |
| compile core + standard lib | 18005745 bytes | 17964197 bytes | 41548 bytes
github-actions[bot]: _Change in memory usage detected by benchmark._
## Memory Report for 122ee5891782da8103bc2efec7041cd72cfe62cd

| Test                        | This Branch | On Main  | Difference |
|-----------------------------|-------------|----------| ---------- |
| compile core + standard lib | 18005745 bytes | 17964197 bytes | 41548 bytes
github-actions[bot]: _Change in memory usage detected by benchmark._
## Memory Report for 70b546e277519d44a7e042be1c0cd750ee4497da

| Test                        | This Branch | On Main  | Difference |
|-----------------------------|-------------|----------| ---------- |
| compile core + standard lib | 18005745 bytes | 17964197 bytes | 41548 bytes

Patch:
diff --git a/compiler/qsc_eval/src/backend.rs b/compiler/qsc_eval/src/backend.rs
index 2270851e..c6b98a56 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<Result<Value, String>> {
         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 ConfigurePauliNoise 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/val.rs b/compiler/qsc_eval/src/val.rs
index 52f2679b..5123ad0c 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 b759aca1..5e7a5e20 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 36da25d8..542cec04 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<std::result::Result<Value, String>> {
         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/std/src/Std/Diagnostics.qs b/library/std/src/Std/Diagnostics.qs
index 5f7d4894..290ef0d6 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 b1edf87a..3b840c64 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<Result<Value, String>> {
         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,
         }
     }


Test patch:
diff --git a/compiler/qsc/src/interpret/circuit_tests.rs b/compiler/qsc/src/interpret/circuit_tests.rs
index 2918abda..6f1250df 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 arguments 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/intrinsic/tests.rs b/compiler/qsc_eval/src/intrinsic/tests.rs
index 6ab588d9..ae94342a 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_partial_eval/src/tests/intrinsics.rs b/compiler/qsc_partial_eval/src/tests/intrinsics.rs
index 1cf0f27e..bd82f142 100644
--- a/compiler/qsc_partial_eval/src/tests/intrinsics.rs
+++ b/compiler/qsc_partial_eval/src/tests/intrinsics.rs
@@ -921,6 +921,31 @@ fn call_to_dump_register_does_not_generate_instructions() {
     );
 }
 
+#[test]
+fn use_of_noise_does_not_generate_instructions() {
+    let program = get_rir_program(indoc! {
+        r#"
+        namespace Test {
+            import Std.Diagnostics.*;
+            @EntryPoint()
+            operation Main() : Unit {
+                use q = Qubit();
+                ConfigurePauliNoise(0.2, 0.2, 0.2);
+                ApplyIdleNoise(q);
+            }
+        }
+        "#,
+    });
+    assert_block_instructions(
+        &program,
+        BlockId(0),
+        &expect![[r#"
+            Block:
+                Call id(1), args( Integer(0), Pointer, )
+                Return"#]],
+    );
+}
+
 #[test]
 #[should_panic(expected = "`CheckZero` is not a supported by partial evaluation")]
 fn call_to_check_zero_panics() {
diff --git a/library/src/tests/diagnostics.rs b/library/src/tests/diagnostics.rs
index a7789488..53f649bc 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()),
+    );
+}