Repository: microsoft/qsharp
Pull request #1939 (2024-09-26 23:38:29)

Fix bug preventing display of circuits where same qubit measured more than once
The logic for returning the count of measurements incorrectly reported every qubit as being measured only once no matter how many times it was measured. This was breaking circuit display for non-base profile programs.


Patch:
diff --git a/compiler/qsc_circuit/src/builder.rs b/compiler/qsc_circuit/src/builder.rs
index 0df85ba1..ebc491eb 100644
--- a/compiler/qsc_circuit/src/builder.rs
+++ b/compiler/qsc_circuit/src/builder.rs
@@ -59,8 +59,6 @@ impl Backend for Builder {
             let mapped_q = self.map(q);
             // In the Circuit schema, result id is per-qubit
             let res_id = self.num_measurements_for_qubit(mapped_q);
-            // We don't actually need the Remapper since we're not
-            // remapping any qubits, but it's handy for keeping track of measurements
             let id = self.remapper.m(q);
 
             self.push_gate(measurement_gate(mapped_q.0, res_id));
@@ -249,21 +247,6 @@ impl Builder {
         self.circuit.operations.push(gate);
     }
 
-    fn num_measurements_by_qubit(&self) -> IndexMap<usize, usize> {
-        self.remapper.qubit_measurement_counts.iter().fold(
-            IndexMap::default(),
-            |mut map: IndexMap<usize, usize>, (q, _)| {
-                match map.get_mut(q.0) {
-                    Some(rs) => *rs += 1,
-                    None => {
-                        map.insert(q.0, 1);
-                    }
-                }
-                map
-            },
-        )
-    }
-
     fn num_measurements_for_qubit(&self, qubit: WireId) -> usize {
         self.remapper
             .qubit_measurement_counts
@@ -273,19 +256,17 @@ impl Builder {
     }
 
     fn finish_circuit(&self, mut circuit: Circuit) -> Circuit {
-        let by_qubit = self.num_measurements_by_qubit();
-
         // add deferred measurements
         if self.config.base_profile {
-            for (qubit, _) in &by_qubit {
+            for (qubit, _) in &self.remapper.qubit_measurement_counts {
                 // guaranteed one measurement per qubit, so result is always 0
-                circuit.operations.push(measurement_gate(qubit, 0));
+                circuit.operations.push(measurement_gate(qubit.0, 0));
             }
         }
 
         // add qubit declarations
         for i in 0..self.remapper.num_qubits() {
-            let num_measurements = by_qubit.get(i).map_or(0, |c| *c);
+            let num_measurements = self.num_measurements_for_qubit(WireId(i));
             circuit.qubits.push(crate::circuit::Qubit {
                 id: i,
                 num_children: num_measurements,
diff --git a/npm/qsharp/ux/circuit.tsx b/npm/qsharp/ux/circuit.tsx
index 5675698d..e7e1895d 100644
--- a/npm/qsharp/ux/circuit.tsx
+++ b/npm/qsharp/ux/circuit.tsx
@@ -39,6 +39,7 @@ function ZoomableCircuit(props: { circuit: qviz.Circuit }) {
   const circuitDiv = useRef<HTMLDivElement>(null);
   const [zoomLevel, setZoomLevel] = useState(100);
   const [rendering, setRendering] = useState(true);
+  const [zoomOnResize, setZoomOnResize] = useState(true);
 
   useEffect(() => {
     // Enable "rendering" text while the circuit is being drawn
@@ -67,7 +68,7 @@ function ZoomableCircuit(props: { circuit: qviz.Circuit }) {
         window.removeEventListener("resize", onResize);
       };
     }
-  }, [rendering]);
+  }, [rendering, zoomOnResize]);
 
   useEffect(() => {
     updateWidth();
@@ -77,7 +78,7 @@ function ZoomableCircuit(props: { circuit: qviz.Circuit }) {
     <div>
       <div>
         {rendering ? null : (
-          <ZoomControl zoom={zoomLevel} onChange={setZoomLevel} />
+          <ZoomControl zoom={zoomLevel} onInput={userSetZoomLevel} />
         )}
       </div>
       <div>
@@ -94,6 +95,10 @@ function ZoomableCircuit(props: { circuit: qviz.Circuit }) {
    * based on the new window width.
    */
   function onResize() {
+    if (!zoomOnResize) {
+      return;
+    }
+
     const [container, svg] = [circuitDiv.current, currentSvg()];
     if (container && svg) {
       // Recalculate the zoom level based on the container width
@@ -151,6 +156,11 @@ function ZoomableCircuit(props: { circuit: qviz.Circuit }) {
   function currentSvg(): SVGElement | undefined {
     return circuitDiv.current?.querySelector(".qviz") ?? undefined;
   }
+
+  function userSetZoomLevel(zoom: number) {
+    setZoomOnResize(false);
+    setZoomLevel(zoom);
+  }
 }
 
 function Unrenderable(props: { qubits: number; operations: number }) {
@@ -178,10 +188,7 @@ function Unrenderable(props: { qubits: number; operations: number }) {
   return <div class="qs-circuit-error">{errorDiv}</div>;
 }
 
-function ZoomControl(props: {
-  zoom: number;
-  onChange: (zoom: number) => void;
-}) {
+function ZoomControl(props: { zoom: number; onInput: (zoom: number) => void }) {
   return (
     <p>
       <label htmlFor="qs-circuit-zoom">Zoom </label>
@@ -193,7 +200,7 @@ function ZoomControl(props: {
         step="10"
         value={props.zoom}
         onInput={(e) =>
-          props.onChange(parseInt((e.target as HTMLInputElement).value) || 0)
+          props.onInput(parseInt((e.target as HTMLInputElement).value) || 0)
         }
       />
       %
diff --git a/widgets/src/qsharp_widgets/__init__.py b/widgets/src/qsharp_widgets/__init__.py
index 837dcccc..219f3aaf 100644
--- a/widgets/src/qsharp_widgets/__init__.py
+++ b/widgets/src/qsharp_widgets/__init__.py
@@ -179,3 +179,4 @@ class Circuit(anywidget.AnyWidget):
     def __init__(self, circuit):
         super().__init__()
         self.circuit_json = circuit.json()
+        self.layout.overflow = "visible scroll"


Test patch:
diff --git a/compiler/qsc/src/interpret/circuit_tests.rs b/compiler/qsc/src/interpret/circuit_tests.rs
index 9de00e3d..2918abda 100644
--- a/compiler/qsc/src/interpret/circuit_tests.rs
+++ b/compiler/qsc/src/interpret/circuit_tests.rs
@@ -72,6 +72,36 @@ fn one_gate() {
     .assert_eq(&circ.to_string());
 }
 
+#[test]
+fn measure_same_qubit_twice() {
+    let mut interpreter = interpreter(
+        r"
+            namespace Test {
+                @EntryPoint()
+                operation Main() : Result[] {
+                    use q = Qubit();
+                    H(q);
+                    let r1 = M(q);
+                    let r2 = M(q);
+                    [r1, r2]
+                }
+            }
+        ",
+        Profile::Unrestricted,
+    );
+
+    let circ = interpreter
+        .circuit(CircuitEntryPoint::EntryPoint, false)
+        .expect("circuit generation should succeed");
+
+    expect![["
+        q_0    ── H ──── M ──── M ──
+                         ╘══════╪═══
+                                ╘═══
+    "]]
+    .assert_eq(&circ.to_string());
+}
+
 #[test]
 fn toffoli() {
     let mut interpreter = interpreter(