Repository: microsoft/qsharp
Pull request #587 (2023-08-15 21:55:13)

Support for shots in Python
Adds a `run()` method to the Python library that takes a `shots=` parameter.

minestarks: @swernli @billti this is now ready for review
minestarks: @billti / @idavis  do you mind giving a signoff for the Python changes please? Thank you!

Patch:
diff --git a/compiler/qsc/src/interpret/stateful.rs b/compiler/qsc/src/interpret/stateful.rs
index 451578ee..96ac3432 100644
--- a/compiler/qsc/src/interpret/stateful.rs
+++ b/compiler/qsc/src/interpret/stateful.rs
@@ -170,6 +170,8 @@ pub struct Interpreter {
     target: TargetProfile,
 }
 
+pub type LineResult = Result<Value, Vec<LineError>>;
+
 impl Interpreter {
     /// # Errors
     /// If the compilation of the standard library fails, an error is returned.
@@ -356,11 +358,7 @@ impl Interpreter {
     /// If the parsing of the line fails, an error is returned.
     /// If the compilation of the line fails, an error is returned.
     /// If there is a runtime error when interpreting the line, an error is returned.
-    pub fn interpret_line(
-        &mut self,
-        receiver: &mut impl Receiver,
-        line: &str,
-    ) -> Result<Value, Vec<LineError>> {
+    pub fn interpret_line(&mut self, receiver: &mut impl Receiver, line: &str) -> LineResult {
         let mut result = Value::unit();
 
         let label = self.next_line_label();
@@ -445,6 +443,119 @@ impl Interpreter {
         Ok(result)
     }
 
+    /// Runs the given entry expression on a new instance of the environment and simulator,
+    /// but using the current compilation.
+    /// # Errors
+    /// If the parsing of the expr fails, an error is returned.
+    /// If the compilation of the expr fails, an error is returned.
+    /// If there is a runtime error when generating code for the expr, an error is returned.
+    /// # Panics
+    /// If internal compiler state is inconsistent, a panic may occur.
+    pub fn run(
+        &mut self,
+        receiver: &mut impl Receiver,
+        expr: &str,
+        shots: u32,
+    ) -> Result<Vec<LineResult>, Vec<LineError>> {
+        let mut fragments = self
+            .compiler
+            .compile_expr("<entry>", expr)
+            .map_err(|errors| {
+                errors
+                    .into_iter()
+                    .map(|error| {
+                        LineError(WithSource::from_map(
+                            self.compiler.source_map(),
+                            error.into(),
+                        ))
+                    })
+                    .collect::<Vec<_>>()
+            })?;
+
+        let pass_errors = fragments
+            .iter_mut()
+            .flat_map(|fragment| {
+                self.passes
+                    .run(self.store.core(), self.compiler.assigner_mut(), fragment)
+            })
+            .collect::<Vec<_>>();
+        if !pass_errors.is_empty() {
+            return Err(pass_errors
+                .into_iter()
+                .map(|error| {
+                    LineError(WithSource::from_map(
+                        self.compiler.source_map(),
+                        error.into(),
+                    ))
+                })
+                .collect());
+        }
+
+        let mut stmt_id = None;
+        for fragment in fragments {
+            match fragment {
+                Fragment::Item(item) => match item.kind {
+                    qsc_hir::hir::ItemKind::Callable(callable) => {
+                        let callable = self.lower_callable_decl(&callable);
+
+                        self.callables
+                            .insert(qsc_eval::lower::lower_local_item_id(item.id), callable);
+                    }
+                    qsc_hir::hir::ItemKind::Namespace(..) => {}
+                    qsc_hir::hir::ItemKind::Ty(..) => {
+                        self.udts
+                            .insert(qsc_eval::lower::lower_local_item_id(item.id));
+                    }
+                },
+                Fragment::Stmt(stmt) => assert!(
+                    stmt_id.replace(self.lower_stmt(&stmt)).is_none(),
+                    "expression should yield exactly one statement"
+                ),
+            }
+        }
+
+        let Some(stmt_id) = stmt_id else {
+            panic!("expression should yield exactly one statement");
+        };
+
+        let globals = Lookup {
+            fir_store: &self.fir_store,
+            package: self.package,
+            udts: &self.udts,
+            callables: &self.callables,
+        };
+
+        let mut results: Vec<LineResult> = Vec::new();
+        for _i in 0..shots {
+            results.push(
+                match eval_stmt(
+                    stmt_id,
+                    &globals,
+                    &mut Env::with_empty_scope(),
+                    &mut SparseSim::new(),
+                    self.package,
+                    receiver,
+                ) {
+                    Ok(value) => Ok(value),
+                    Err((error, call_stack)) => {
+                        let stack_trace = if call_stack.is_empty() {
+                            None
+                        } else {
+                            Some(self.render_call_stack(call_stack, &error))
+                        };
+
+                        Err(vec![LineError(WithSource::from_map(
+                            self.compiler.source_map(),
+                            WithStack::new(error, stack_trace).into(),
+                        ))])
+                    }
+                },
+            );
+        }
+
+        Ok(results)
+    }
+
     /// # Errors
     /// If the currently configured target profile is not `TargetProfile::Base`, an error is returned.
     /// If the parsing of the expr fails, an error is returned.
diff --git a/pip/qsharp/__init__.py b/pip/qsharp/__init__.py
index 892e7daa..e689c6ca 100644
--- a/pip/qsharp/__init__.py
+++ b/pip/qsharp/__init__.py
@@ -1,7 +1,7 @@
 # Copyright (c) Microsoft Corporation.
 # Licensed under the MIT License.
 
-from ._qsharp import init, eval, eval_file, compile
+from ._qsharp import init, eval, eval_file, run, compile
 
 from ._native import Result, Pauli, QSharpError, TargetProfile
 
@@ -20,6 +20,7 @@ __all__ = [
     "init",
     "eval",
     "eval_file",
+    "run",
     "compile",
     "Result",
     "Pauli",
diff --git a/pip/qsharp/_native.pyi b/pip/qsharp/_native.pyi
index 02551af6..853e3e51 100644
--- a/pip/qsharp/_native.pyi
+++ b/pip/qsharp/_native.pyi
@@ -46,6 +46,22 @@ class Interpreter:
 
         :returns value: The value returned by the last statement in the input.
 
+        :raises QSharpError: If there is an error interpreting the input.
+        """
+        ...
+    def run(
+        self, entry_expr: str, shots: int, output_fn: Callable[[Output], None]
+    ) -> Any:
+        """
+        Runs the given Q# expressin for the given number of shots.
+        Each shot uses an independent instance of the simulator.
+
+        :param entry_expr: The entry expression.
+        :param shots: The number of shots to run.
+        :param output_fn: A callback function that will be called with each output.
+
+        :returns values: A list of results or runtime errors.
+
         :raises QSharpError: If there is an error interpreting the input.
         """
         ...
diff --git a/pip/qsharp/_qsharp.py b/pip/qsharp/_qsharp.py
index bec474ff..174c21e6 100644
--- a/pip/qsharp/_qsharp.py
+++ b/pip/qsharp/_qsharp.py
@@ -47,7 +47,7 @@ def eval(source):
     return get_interpreter().interpret(source, callback)
 
 
-def eval_file(path) -> None:
+def eval_file(path):
     """
     Reads Q# source code from a file and evaluates it.
 
@@ -59,6 +59,25 @@ def eval_file(path) -> None:
     return eval(f.read())
 
 
+def run(entry_expr, shots):
+    """
+    Runs the given Q# expressin for the given number of shots.
+    Each shot uses an independent instance of the simulator.
+
+    :param entry_expr: The entry expression.
+    :param shots: The number of shots to run.
+
+    :returns values: A list of results or runtime errors.
+
+    :raises QSharpError: If there is an error interpreting the input.
+    """
+
+    def callback(output):
+        print(output)
+
+    return get_interpreter().run(entry_expr, shots, callback)
+
+
 def compile(entry_expr):
     """
     Compiles the Q# source code into a program that can be submitted to a target.
diff --git a/pip/samples/sample.ipynb b/pip/samples/sample.ipynb
index 7e1f0f59..706b07bf 100644
--- a/pip/samples/sample.ipynb
+++ b/pip/samples/sample.ipynb
@@ -12,7 +12,7 @@
     },
     {
       "cell_type": "code",
-      "execution_count": 3,
+      "execution_count": 1,
       "id": "1e8e4faa",
       "metadata": {},
       "outputs": [
@@ -26,8 +26,7 @@
         }
       ],
       "source": [
-        "import qsharp\n",
-        "qsharp.init()"
+        "import qsharp"
       ]
     },
     {
@@ -42,7 +41,7 @@
     },
     {
       "cell_type": "code",
-      "execution_count": 4,
+      "execution_count": 2,
       "id": "9df62352",
       "metadata": {
         "vscode": {
@@ -106,10 +105,10 @@
         {
           "data": {
             "text/plain": [
-              "Result.One"
+              "One"
             ]
           },
-          "execution_count": 4,
+          "execution_count": 2,
           "metadata": {},
           "output_type": "execute_result"
         }
@@ -160,7 +159,7 @@
         {
           "data": {
             "text/plain": [
-              "Result.One"
+              "One"
             ]
           },
           "execution_count": 3,
@@ -292,7 +291,7 @@
     },
     {
       "cell_type": "code",
-      "execution_count": 8,
+      "execution_count": 6,
       "id": "e70a95d9",
       "metadata": {},
       "outputs": [
@@ -300,14 +299,14 @@
           "name": "stdout",
           "output_type": "stream",
           "text": [
-            "Error: Qubit2 released while not in |0⟩ state\n",
+            "Error: Qubit0 released while not in |0⟩ state\n",
             "Call stack:\n",
             "    at QIR.Runtime.__quantum__rt__qubit_release in qir.qs\n",
             "    at Foo in <expression>\n",
             "\u001b[31mQsc.Eval.ReleasedQubitNotZero\u001b[0m\n",
             "\n",
             "  \u001b[31m×\u001b[0m runtime error\n",
-            "\u001b[31m  ╰─▶ \u001b[0mQubit2 released while not in |0⟩ state\n",
+            "\u001b[31m  ╰─▶ \u001b[0mQubit0 released while not in |0⟩ state\n",
             "\n"
           ]
         }
@@ -329,20 +328,20 @@
     },
     {
       "cell_type": "code",
-      "execution_count": 9,
+      "execution_count": 7,
       "id": "d40d86cb",
       "metadata": {},
       "outputs": [
         {
           "ename": "QSharpError",
-          "evalue": "Error: Qubit3 released while not in |0⟩ state\nCall stack:\n    at QIR.Runtime.__quantum__rt__qubit_release in qir.qs\n    at Foo in <expression>\n\u001b[31mQsc.Eval.ReleasedQubitNotZero\u001b[0m\n\n  \u001b[31m×\u001b[0m runtime error\n\u001b[31m  ╰─▶ \u001b[0mQubit3 released while not in |0⟩ state\n",
+          "evalue": "Error: Qubit1 released while not in |0⟩ state\nCall stack:\n    at QIR.Runtime.__quantum__rt__qubit_release in qir.qs\n    at Foo in <expression>\n\u001b[31mQsc.Eval.ReleasedQubitNotZero\u001b[0m\n\n  \u001b[31m×\u001b[0m runtime error\n\u001b[31m  ╰─▶ \u001b[0mQubit1 released while not in |0⟩ state\n",
           "output_type": "error",
           "traceback": [
-            "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
-            "\u001b[1;31mQSharpError\u001b[0m                               Traceback (most recent call last)",
-            "Cell \u001b[1;32mIn[9], line 1\u001b[0m\n\u001b[1;32m----> 1\u001b[0m qsharp\u001b[39m.\u001b[39;49meval(\u001b[39m\"\u001b[39;49m\u001b[39moperation Foo() : Unit \u001b[39;49m\u001b[39m{\u001b[39;49m\u001b[39m use q = Qubit(); X(q) } Foo()\u001b[39;49m\u001b[39m\"\u001b[39;49m)\n",
-            "File \u001b[1;32mc:\\src\\qsharp\\.venv\\Lib\\site-packages\\qsharp\\_qsharp.py:24\u001b[0m, in \u001b[0;36meval\u001b[1;34m(source)\u001b[0m\n\u001b[0;32m     21\u001b[0m \u001b[39mdef\u001b[39;00m \u001b[39mcallback\u001b[39m(output):\n\u001b[0;32m     22\u001b[0m     \u001b[39mprint\u001b[39m(output)\n\u001b[1;32m---> 24\u001b[0m \u001b[39mreturn\u001b[39;00m _interpreter\u001b[39m.\u001b[39;49minterpret(source, callback)\n",
-            "\u001b[1;31mQSharpError\u001b[0m: Error: Qubit3 released while not in |0⟩ state\nCall stack:\n    at QIR.Runtime.__quantum__rt__qubit_release in qir.qs\n    at Foo in <expression>\n\u001b[31mQsc.Eval.ReleasedQubitNotZero\u001b[0m\n\n  \u001b[31m×\u001b[0m runtime error\n\u001b[31m  ╰─▶ \u001b[0mQubit3 released while not in |0⟩ state\n"
+            "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
+            "\u001b[0;31mQSharpError\u001b[0m                               Traceback (most recent call last)",
+            "Cell \u001b[0;32mIn[7], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m qsharp\u001b[39m.\u001b[39;49meval(\u001b[39m\"\u001b[39;49m\u001b[39moperation Foo() : Unit \u001b[39;49m\u001b[39m{\u001b[39;49m\u001b[39m use q = Qubit(); X(q) } Foo()\u001b[39;49m\u001b[39m\"\u001b[39;49m)\n",
+            "File \u001b[0;32m~/miniconda3/envs/qsharp/lib/python3.11/site-packages/qsharp/_qsharp.py:47\u001b[0m, in \u001b[0;36meval\u001b[0;34m(source)\u001b[0m\n\u001b[1;32m     44\u001b[0m \u001b[39mdef\u001b[39;00m \u001b[39mcallback\u001b[39m(output):\n\u001b[1;32m     45\u001b[0m     \u001b[39mprint\u001b[39m(output)\n\u001b[0;32m---> 47\u001b[0m \u001b[39mreturn\u001b[39;00m get_interpreter()\u001b[39m.\u001b[39;49minterpret(source, callback)\n",
+            "\u001b[0;31mQSharpError\u001b[0m: Error: Qubit1 released while not in |0⟩ state\nCall stack:\n    at QIR.Runtime.__quantum__rt__qubit_release in qir.qs\n    at Foo in <expression>\n\u001b[31mQsc.Eval.ReleasedQubitNotZero\u001b[0m\n\n  \u001b[31m×\u001b[0m runtime error\n\u001b[31m  ╰─▶ \u001b[0mQubit1 released while not in |0⟩ state\n"
           ]
         }
       ],
@@ -360,7 +359,7 @@
     },
     {
       "cell_type": "code",
-      "execution_count": 10,
+      "execution_count": 8,
       "id": "1b55e53c",
       "metadata": {
         "scrolled": false,
@@ -384,7 +383,7 @@
               "  <tbody>\n",
               "    <tr>\n",
               "  <td style=\"text-align: left\">\n",
-              "    <span style=\"display: inline-block\">|01111⟩</span>\n",
+              "    <span style=\"display: inline-block\">|011⟩</span>\n",
               "  </td>\n",
               "  <td style=\"text-align: left\">\n",
               "    <span style=\"display: inline-block\">1.0000+0.0000𝑖</span>\n",
@@ -404,7 +403,7 @@
             ],
             "text/plain": [
               "STATE:\n",
-              "|01111⟩: 1.0000+0.0000𝑖"
+              "|011⟩: 1.0000+0.0000𝑖"
             ]
           },
           "metadata": {},
@@ -413,14 +412,14 @@
         {
           "data": {
             "text/plain": [
-              "Error: Qubit4 released while not in |0⟩ state\n",
+              "Error: Qubit2 released while not in |0⟩ state\n",
               "Call stack:\n",
               "    at QIR.Runtime.__quantum__rt__qubit_release in qir.qs\n",
               "    at Bar in <expression>\n",
               "\u001b[31mQsc.Eval.ReleasedQubitNotZero\u001b[0m\n",
               "\n",
               "  \u001b[31m×\u001b[0m runtime error\n",
-              "\u001b[31m  ╰─▶ \u001b[0mQubit4 released while not in |0⟩ state\n"
+              "\u001b[31m  ╰─▶ \u001b[0mQubit2 released while not in |0⟩ state\n"
             ]
           },
           "metadata": {},
@@ -449,7 +448,7 @@
     },
     {
       "cell_type": "code",
-      "execution_count": 11,
+      "execution_count": 9,
       "id": "bd25ae87",
       "metadata": {
         "vscode": {
@@ -484,7 +483,7 @@
               "  <tbody>\n",
               "    <tr>\n",
               "  <td style=\"text-align: left\">\n",
-              "    <span style=\"display: inline-block\">|011111⟩</span>\n",
+              "    <span style=\"display: inline-block\">|0111⟩</span>\n",
               "  </td>\n",
               "  <td style=\"text-align: left\">\n",
               "    <span style=\"display: inline-block\">1.0000+0.0000𝑖</span>\n",
@@ -504,7 +503,7 @@
             ],
             "text/plain": [
               "STATE:\n",
-              "|011111⟩: 1.0000+0.0000𝑖"
+              "|0111⟩: 1.0000+0.0000𝑖"
             ]
           },
           "metadata": {},
@@ -537,7 +536,7 @@
               "  <tbody>\n",
               "    <tr>\n",
               "  <td style=\"text-align: left\">\n",
-              "    <span style=\"display: inline-block\">|111111⟩</span>\n",
+              "    <span style=\"display: inline-block\">|1111⟩</span>\n",
               "  </td>\n",
               "  <td style=\"text-align: left\">\n",
               "    <span style=\"display: inline-block\">1.0000+0.0000𝑖</span>\n",
@@ -557,7 +556,7 @@
             ],
             "text/plain": [
               "STATE:\n",
-              "|111111⟩: 1.0000+0.0000𝑖"
+              "|1111⟩: 1.0000+0.0000𝑖"
             ]
           },
           "metadata": {},
@@ -590,7 +589,7 @@
               "  <tbody>\n",
               "    <tr>\n",
               "  <td style=\"text-align: left\">\n",
-              "    <span style=\"display: inline-block\">|111111⟩</span>\n",
+              "    <span style=\"display: inline-block\">|0111⟩</span>\n",
               "  </td>\n",
               "  <td style=\"text-align: left\">\n",
               "    <span style=\"display: inline-block\">1.0000+0.0000𝑖</span>\n",
@@ -610,7 +609,7 @@
             ],
             "text/plain": [
               "STATE:\n",
-              "|111111⟩: 1.0000+0.0000𝑖"
+              "|0111⟩: 1.0000+0.0000𝑖"
             ]
           },
           "metadata": {},
@@ -619,10 +618,10 @@
         {
           "data": {
             "text/html": [
-              "<p>Result: One</p>"
+              "<p>Result: Zero</p>"
             ],
             "text/plain": [
-              "Result: One"
+              "Result: Zero"
             ]
           },
           "metadata": {},
@@ -643,7 +642,7 @@
               "  <tbody>\n",
               "    <tr>\n",
               "  <td style=\"text-align: left\">\n",
-              "    <span style=\"display: inline-block\">|011111⟩</span>\n",
+              "    <span style=\"display: inline-block\">|0111⟩</span>\n",
               "  </td>\n",
               "  <td style=\"text-align: left\">\n",
               "    <span style=\"display: inline-block\">1.0000+0.0000𝑖</span>\n",
@@ -663,7 +662,7 @@
             ],
             "text/plain": [
               "STATE:\n",
-              "|011111⟩: 1.0000+0.0000𝑖"
+              "|0111⟩: 1.0000+0.0000𝑖"
             ]
           },
           "metadata": {},
@@ -696,7 +695,7 @@
               "  <tbody>\n",
               "    <tr>\n",
               "  <td style=\"text-align: left\">\n",
-              "    <span style=\"display: inline-block\">|111111⟩</span>\n",
+              "    <span style=\"display: inline-block\">|0111⟩</span>\n",
               "  </td>\n",
               "  <td style=\"text-align: left\">\n",
               "    <span style=\"display: inline-block\">1.0000+0.0000𝑖</span>\n",
@@ -716,7 +715,7 @@
             ],
             "text/plain": [
               "STATE:\n",
-              "|111111⟩: 1.0000+0.0000𝑖"
+              "|0111⟩: 1.0000+0.0000𝑖"
             ]
           },
           "metadata": {},
@@ -725,10 +724,10 @@
         {
           "data": {
             "text/html": [
-              "<p>Result: One</p>"
+              "<p>Result: Zero</p>"
             ],
             "text/plain": [
-              "Result: One"
+              "Result: Zero"
             ]
           },
           "metadata": {},
@@ -756,6 +755,307 @@
         "\n",
         "Main()"
       ]
+    },
+    {
+      "cell_type": "markdown",
+      "id": "2a2d9e7d",
+      "metadata": {},
+      "source": [
+        "Running multiple shots for an expression. Each shot uses an independent instance of the simulator. A list of results (or runtime errors) is returned."
+      ]
+    },
+    {
+      "cell_type": "code",
+      "execution_count": 10,
+      "id": "eb3cd29f",
+      "metadata": {
+        "vscode": {
+          "languageId": "qsharp"
+        }
+      },
+      "outputs": [],
+      "source": [
+        "%%qsharp\n",
+        "\n",
+        "operation RandomBit() : Result {\n",
+        "    use q = Qubit();\n",
+        "    H(q);\n",
+        "    let res = M(q);\n",
+        "    Reset(q);\n",
+        "    return res;\n",
+        "}"
+      ]
+    },
+    {
+      "cell_type": "code",
+      "execution_count": 11,
+      "id": "9a9f5335",
+      "metadata": {},
+      "outputs": [
+        {
+          "data": {
+            "text/plain": [
+              "[Zero, Zero, Zero, Zero, Zero, One, One, One, One, Zero]"
+            ]
+          },
+          "execution_count": 11,
+          "metadata": {},
+          "output_type": "execute_result"
+        }
+      ],
+      "source": [
+        "results = qsharp.run(\"RandomBit()\", 10)\n",
+        "\n",
+        "results"
+      ]
+    },
+    {
+      "cell_type": "markdown",
+      "id": "6a476e6d",
+      "metadata": {},
+      "source": [
+        "The results can then be processed, e.g. plotted in a histogram using popular Python libraries."
+      ]
+    },
+    {
+      "cell_type": "code",
+      "execution_count": 12,
+      "id": "7bd77379",
+      "metadata": {},
+      "outputs": [
+        {
+          "name": "stdout",
+          "output_type": "stream",
+          "text": [
+            "Collecting matplotlib\n",
+            "  Downloading matplotlib-3.7.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (11.6 MB)\n",
+            "\u001b[2K     \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m11.6/11.6 MB\u001b[0m \u001b[31m20.0 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m00:01\u001b[0m00:01\u001b[0m\n",
+            "\u001b[?25hCollecting contourpy>=1.0.1 (from matplotlib)\n",
+            "  Downloading contourpy-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (300 kB)\n",
+            "\u001b[2K     \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m300.4/300.4 kB\u001b[0m \u001b[31m17.4 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
+            "\u001b[?25hCollecting cycler>=0.10 (from matplotlib)\n",
+            "  Downloading cycler-0.11.0-py3-none-any.whl (6.4 kB)\n",
+            "Collecting fonttools>=4.22.0 (from matplotlib)\n",
+            "  Downloading fonttools-4.42.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (4.9 MB)\n",
+            "\u001b[2K     \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m4.9/4.9 MB\u001b[0m \u001b[31m18.6 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m00:01\u001b[0m00:01\u001b[0m\n",
+            "\u001b[?25hCollecting kiwisolver>=1.0.1 (from matplotlib)\n",
+            "  Downloading kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.4 MB)\n",
+            "\u001b[2K     \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m1.4/1.4 MB\u001b[0m \u001b[31m21.6 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0ma \u001b[36m0:00:01\u001b[0m\n",
+            "\u001b[?25hRequirement already satisfied: numpy>=1.20 in /home/minestarks/miniconda3/envs/qsharp/lib/python3.11/site-packages (from matplotlib) (1.25.2)\n",
+            "Requirement already satisfied: packaging>=20.0 in /home/minestarks/miniconda3/envs/qsharp/lib/python3.11/site-packages (from matplotlib) (23.1)\n",
+            "Collecting pillow>=6.2.0 (from matplotlib)\n",
+            "  Downloading Pillow-10.0.0-cp311-cp311-manylinux_2_28_x86_64.whl (3.4 MB)\n",
+            "\u001b[2K     \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m3.4/3.4 MB\u001b[0m \u001b[31m18.7 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m00:01\u001b[0m00:01\u001b[0m\n",
+            "\u001b[?25hCollecting pyparsing<3.1,>=2.3.1 (from matplotlib)\n",
+            "  Downloading pyparsing-3.0.9-py3-none-any.whl (98 kB)\n",
+            "\u001b[2K     \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m98.3/98.3 kB\u001b[0m \u001b[31m13.2 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
+            "\u001b[?25hRequirement already satisfied: python-dateutil>=2.7 in /home/minestarks/miniconda3/envs/qsharp/lib/python3.11/site-packages (from matplotlib) (2.8.2)\n",
+            "Requirement already satisfied: six>=1.5 in /home/minestarks/miniconda3/envs/qsharp/lib/python3.11/site-packages (from python-dateutil>=2.7->matplotlib) (1.16.0)\n",
+            "Installing collected packages: pyparsing, pillow, kiwisolver, fonttools, cycler, contourpy, matplotlib\n",
+            "Successfully installed contourpy-1.1.0 cycler-0.11.0 fonttools-4.42.1 kiwisolver-1.4.5 matplotlib-3.7.2 pillow-10.0.0 pyparsing-3.0.9\n",
+            "Note: you may need to restart the kernel to use updated packages.\n"
+          ]
+        }
+      ],
+      "source": [
+        "%pip install matplotlib"
+      ]
+    },
+    {
+      "cell_type": "code",
+      "execution_count": 13,
+      "id": "83cee4e8",
+      "metadata": {},
+      "outputs": [
+        {
+          "data": {
+            "image/png": "iVBORw0KGgoAAAANSUhEUgAAAhYAAAGdCAYAAABO2DpVAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAA9hAAAPYQGoP6dpAAAWKUlEQVR4nO3de4xUhdn48Wd1uyPK7iIIArKA1htesApoKG0Fq1KqBm2ijcFKkLRR1gsSkwptao3VxaSvsU3teomtTaNCGkNtqkLVFDAIFbAabZUKal0vgIruAm8cLTu/P5ru791ykYFnhMHPJzkJc+acOc8kzO43Z87O1JRKpVIAACTYb08PAADsO4QFAJBGWAAAaYQFAJBGWAAAaYQFAJBGWAAAaYQFAJCm9rM+YGdnZ7z99ttRX18fNTU1n/XhAYBdUCqVYuPGjTFw4MDYb7/tn5f4zMPi7bffjqamps/6sABAgra2thg0aNB27//Mw6K+vj4i/j1YQ0PDZ314AGAXdHR0RFNTU9fv8e35zMPiP29/NDQ0CAsAqDKfdhmDizcBgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIU3ZYvPXWW3HJJZdEnz59okePHnHiiSfGihUrKjEbAFBlyvqukA8++CDGjBkT48aNi8ceeyz69u0br7zyShx88MGVmg8AqCJlhcWtt94aTU1N8etf/7pr3eGHH54+FABQncp6K+QPf/hDjBw5Mi688MLo169fnHzyyXHPPffscJ9isRgdHR3dFgBg31TWGYtXX301WltbY8aMGTFr1qxYvnx5XH311VFXVxeTJ0/e5j4tLS1x4403pgz7aYZe/8hnchyoVq/PPmdPjwDs42pKpVJpZzeuq6uLkSNHxtNPP9217uqrr47ly5fH0qVLt7lPsViMYrHYdbujoyOampqivb09GhoadmP0rQkL2DFhAeyqjo6OaGxs/NTf32W9FTJgwIA47rjjuq0bNmxYvPHGG9vdp1AoRENDQ7cFANg3lRUWY8aMiVWrVnVb949//COGDBmSOhQAUJ3KCotrr702li1bFrfcckusXr06Hnjggbj77rujubm5UvMBAFWkrLAYNWpUzJs3Lx588ME44YQT4qabborbb789Jk2aVKn5AIAqUtZfhUREnHvuuXHuuedWYhYAoMr5rhAAII2wAADSCAsAII2wAADSCAsAII2wAADSCAsAII2wAADSCAsAII2wAADSCAsAII2wAADSCAsAII2wAADSCAsAII2wAADSCAsAII2wAADSCAsAII2wAADSCAsAII2wAADSCAsAII2wAADSCAsAII2wAADSCAsAII2wAADSCAsAII2wAADSCAsAII2wAADSCAsAII2wAADSCAsAII2wAADSCAsAII2wAADSCAsAII2wAADSCAsAII2wAADSCAsAII2wAADSCAsAII2wAADSCAsAII2wAADSlBUWP/7xj6Ompqbbcuyxx1ZqNgCgytSWu8Pxxx8fTzzxxP9/gNqyHwIA2EeVXQW1tbXRv3//SswCAFS5sq+xeOWVV2LgwIFxxBFHxKRJk+KNN97Y4fbFYjE6Ojq6LQDAvqmssDjttNPivvvui/nz50dra2u89tpr8dWvfjU2bty43X1aWlqisbGxa2lqatrtoQGAvVNNqVQq7erOH374YQwZMiRuu+22mDp16ja3KRaLUSwWu253dHREU1NTtLe3R0NDw64eepuGXv9I6uPBvub12efs6RGAKtXR0RGNjY2f+vt7t6687NWrVxx99NGxevXq7W5TKBSiUCjszmEAgCqxW59jsWnTplizZk0MGDAgax4AoIqVFRbXXXddLFq0KF5//fV4+umn44ILLoj9998/Lr744krNBwBUkbLeCnnzzTfj4osvjvfffz/69u0bX/nKV2LZsmXRt2/fSs0HAFSRssJizpw5lZoDANgH+K4QACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACDNboXF7Nmzo6amJqZPn540DgBQzXY5LJYvXx533XVXDB8+PHMeAKCK7VJYbNq0KSZNmhT33HNPHHzwwdkzAQBVapfCorm5Oc4555w488wzs+cBAKpYbbk7zJkzJ5599tlYvnz5Tm1fLBajWCx2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UOV8jgUAkEZYAABphAUAkEZYAABphAUAkEZYAABphAUAkEZYAABphAUAkEZYAABphAUAkEZYAABphAUAkEZYAABphAUAkEZYAABphAUAkEZYAABphAUAkEZYAABphAUAkEZYAABphAUAkEZYAABphAUAkEZYAABphAUAkEZYAABphAUAkEZYAABphAUAkEZYAABphAUAkEZYAABphAUAkEZYAABphAUAkEZYAABphAUAkEZYAABphAUAkEZYAABphAUAkEZYAABphAUAkEZYAABphAUAkEZYAABpdiks7rjjjhg6dGgccMABcdppp8UzzzyTPRcAUIXKDou5c+fGjBkz4oYbbohnn302TjrppBg/fnysX7++EvMBAFWk7LC47bbb4rvf/W5MmTIljjvuuLjzzjvjwAMPjF/96leVmA8AqCK15Wz88ccfx8qVK2PmzJld6/bbb78488wzY+nSpdvcp1gsRrFY7Lrd3t4eEREdHR27Mu8OdRb/N/0xYV9SidfdnuC1DttXqdf5fx63VCrtcLuywuK9996LLVu2xKGHHtpt/aGHHhovv/zyNvdpaWmJG2+8cav1TU1N5RwaSNB4+56eAKi0Sr/ON27cGI2Njdu9v6yw2BUzZ86MGTNmdN3u7OyMDRs2RJ8+faKmpqbSh2cP6ejoiKampmhra4uGhoY9PQ5QIV7rnx+lUik2btwYAwcO3OF2ZYXFIYccEvvvv3+sW7eu2/p169ZF//79t7lPoVCIQqHQbV2vXr3KOSxVrKGhwQ8b+BzwWv982NGZiv8o6+LNurq6GDFiRDz55JNd6zo7O+PJJ5+M0aNHlz8hALBPKfutkBkzZsTkyZNj5MiRceqpp8btt98emzdvjilTplRiPgCgipQdFt/+9rfj3XffjR/96Eexdu3a+NKXvhTz58/f6oJOPt8KhULccMMNW70NBuxbvNb5bzWlT/u7EQCAneS7QgCANMICAEgjLACANMICAEgjLNihhQsXRk1NzXaXcePG7ekRgQpoa2uLyy67LAYOHBh1dXUxZMiQuOaaa+L999/f06OxlxMW7NCXv/zleOedd7Za7rrrrqipqYlp06bt0uN+/PHHyZMCWV599dUYOXJkvPLKK/Hggw/G6tWr48477+z6MMQNGzbs6RHZiwkLdqiuri769+/fbfnggw/iuuuui1mzZsWFF14YEREvvvhiTJgwIXr27BmHHnpofOc734n33nuv63HGjh0bV155ZUyfPj0OOeSQGD9+fERELFq0KE499dQoFAoxYMCAuP766+Nf//rXHnmuwL81NzdHXV1d/OlPf4rTTz89Bg8eHBMmTIgnnngi3nrrrfjBD34QERFDhw6NW265JS677LKor6+PwYMHx913393tsdra2uKiiy6KXr16Re/evWPixInx+uuv74FnxWdFWFCWDz/8MCZOnBhjx46Nm266qWvdGWecESeffHKsWLEi5s+fH+vWrYuLLrqo276/+c1voq6uLpYsWRJ33nlnvPXWW/HNb34zRo0aFc8//3y0trbGvffeGz/5yU/2xFMDImLDhg2xYMGCmDZtWvTo0aPbff37949JkybF3Llzu746+3/+539i5MiR8de//jWmTZsWV1xxRaxatSoiIj755JMYP3581NfXx1NPPRVLliyJnj17xje+8Q1nLfdlJdhJW7ZsKU2YMKE0bNiwUkdHR9f6m266qXT22Wd327atra0UEaVVq1aVSqVS6fTTTy+dfPLJ3baZNWtW6Zhjjil1dnZ2rbvjjjtKPXv2LG3ZsqWCzwTYnmXLlpUiojRv3rxt3n/bbbeVIqK0bt260pAhQ0qXXHJJ132dnZ2lfv36lVpbW0ulUqn029/+dqvXeLFYLPXo0aO0YMGCij4P9pyKf206+45Zs2bF0qVL45lnnon6+vqu9c8//3z8+c9/jp49e261z5o1a+Loo4+OiIgRI0Z0u++ll16K0aNHR01NTde6MWPGxKZNm+LNN9+MwYMHV+iZAJ+mtJMfyjx8+PCuf9fU1ET//v1j/fr1EfHvnw2rV6/u9vMiIuKjjz6KNWvW5A3LXkVYsFPmzJkTP/3pT+ORRx6Jo446qtt9mzZtivPOOy9uvfXWrfYbMGBA178POuigis8J7J4jjzwyampq4qWXXooLLrhgq/tfeumlOPjgg6Nv374REfGFL3yh2/01NTXR2dkZEf/+2TBixIi4//77t3qc/+zPvkdY8Kmee+65mDp1asyePbvrosv/65RTTomHHnoohg4dGrW1O/9fatiwYfHQQw9FqVTqOmuxZMmSqK+vj0GDBqXND+y8Pn36xFlnnRW//OUv49prr+12ncXatWvj/vvvj0svvbTbmcbtOeWUU2Lu3LnRr1+/aGhoqOTY7EVcvMkOvffee3H++efH2LFj45JLLom1a9d2W959991obm6ODRs2xMUXXxzLly+PNWvWxIIFC2LKlCmxZcuW7T72tGnToq2tLa666qp4+eWX4+GHH44bbrghZsyYEfvt578m7Cm/+MUvolgsxvjx42Px4sXR1tYW8+fPj7POOisOO+ywuPnmm3fqcSZNmhSHHHJITJw4MZ566ql47bXXYuHChXH11VfHm2++WeFnwZ7ipzc79Mgjj8Q///nPePTRR2PAgAFbLaNGjYqBAwfGkiVLYsuWLXH22WfHiSeeGNOnT49evXrtMBAOO+ywePTRR+OZZ56Jk046KS6//PKYOnVq/PCHP/wMnyHw34466qhYsWJFHHHEEXHRRRfFF7/4xfje974X48aNi6VLl0bv3r136nEOPPDAWLx4cQwePDi+9a1vxbBhw2Lq1Knx0UcfOYOxD/O16QBAGmcsAIA0wgIASCMsAIA0wgIASCMsAIA0wgIASCMsAIA0wgIASCMsAIA0wgIASCMsAIA0wgIASPP/ADlNpvHh2gBcAAAAAElFTkSuQmCC",
+            "text/plain": [
+              "<Figure size 640x480 with 1 Axes>"
+            ]
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        }
+      ],
+      "source": [
+        "import matplotlib.pyplot as plt\n",
+        "import numpy as np\n",
+        "from collections import Counter\n",
+        "\n",
+        "# Sort the results so that the histogram labels appear in the correct order\n",
+        "results.sort()\n",
+        "# Count the number of times each result appears\n",
+        "counts = Counter(results)\n",
+        "\n",
+        "(values, counts) = counts.keys(), counts.values()\n",
+        "xlabels = np.arange(len(counts))\n",
+        "plt.bar(xlabels, counts)\n",
+        "plt.xticks(xlabels, values)\n",
+        "plt.show()"
+      ]
+    },
+    {
+      "cell_type": "code",
+      "execution_count": 14,
+      "id": "eaf766f4",
+      "metadata": {},
+      "outputs": [
+        {
+          "name": "stdout",
+          "output_type": "stream",
+          "text": [
+            "Collecting pandas\n",
+            "  Downloading pandas-2.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (12.2 MB)\n",
+            "\u001b[2K     \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m12.2/12.2 MB\u001b[0m \u001b[31m17.9 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m00:01\u001b[0m00:01\u001b[0m\n",
+            "\u001b[?25hRequirement already satisfied: python-dateutil>=2.8.2 in /home/minestarks/miniconda3/envs/qsharp/lib/python3.11/site-packages (from pandas) (2.8.2)\n",
+            "Collecting pytz>=2020.1 (from pandas)\n",
+            "  Downloading pytz-2023.3-py2.py3-none-any.whl (502 kB)\n",
+            "\u001b[2K     \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m502.3/502.3 kB\u001b[0m \u001b[31m13.5 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m00:01\u001b[0m\n",
+            "\u001b[?25hCollecting tzdata>=2022.1 (from pandas)\n",
+            "  Downloading tzdata-2023.3-py2.py3-none-any.whl (341 kB)\n",
+            "\u001b[2K     \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m341.8/341.8 kB\u001b[0m \u001b[31m20.2 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
+            "\u001b[?25hRequirement already satisfied: numpy>=1.21.0 in /home/minestarks/miniconda3/envs/qsharp/lib/python3.11/site-packages (from pandas) (1.25.2)\n",
+            "Requirement already satisfied: six>=1.5 in /home/minestarks/miniconda3/envs/qsharp/lib/python3.11/site-packages (from python-dateutil>=2.8.2->pandas) (1.16.0)\n",
+            "Installing collected packages: pytz, tzdata, pandas\n",
+            "Successfully installed pandas-2.0.3 pytz-2023.3 tzdata-2023.3\n",
+            "Note: you may need to restart the kernel to use updated packages.\n"
+          ]
+        }
+      ],
+      "source": [
+        "%pip install pandas"
+      ]
+    },
+    {
+      "cell_type": "code",
+      "execution_count": 15,
+      "id": "8095640a",
+      "metadata": {},
+      "outputs": [
+        {
+          "data": {
+            "text/plain": [
+              "<Axes: >"
+            ]
+          },
+          "execution_count": 15,
+          "metadata": {},
+          "output_type": "execute_result"
+        },
+        {
+          "data": {
+            "image/png": "iVBORw0KGgoAAAANSUhEUgAAAhYAAAGvCAYAAAAHapzzAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAA9hAAAPYQGoP6dpAAAXaUlEQVR4nO3da4wVhfn48ecgckS7uyIrCGFFKVZEBSuoIVSjouLd2qReQlNirYmyXomJpUbRGrv4opY2WKTGljSNxbSJtakVL4gYC1jAeK8WFHSRIiq6CzQeld3fi8b9/7eA7Vme5eyBzyeZFzNnxnlMXPebmTk7hfb29vYAAEjQq9IDAAC7D2EBAKQRFgBAGmEBAKQRFgBAGmEBAKQRFgBAGmEBAKTpvatP2NbWFuvWrYuampooFAq7+vQAQBe0t7fHpk2bYvDgwdGr146vS+zysFi3bl00NDTs6tMCAAmam5tjyJAhO/x8l4dFTU1NRPx7sNra2l19egCgC1pbW6OhoaHj9/iO7PKw+OL2R21trbAAgCrz3x5j8PAmAJBGWAAAaYQFAJBGWAAAaYQFAJBGWAAAaYQFAJBGWAAAaYQFAJBGWAAAacoOi3fffTe+853vRP/+/aNv375x9NFHx/Lly7tjNgCgypT1rpCPPvooxo8fH6eccko8+uijceCBB8bKlSujX79+3TUfAFBFygqLu+66KxoaGuLXv/51x7ZDDz00fSgAoDqVdSvkT3/6U4wdOza+/e1vx4ABA+LrX/963HfffV96TKlUitbW1k4LALB7KuuKxVtvvRWzZ8+OqVOnxg9/+MNYtmxZXHvttdGnT5+YPHnydo9pamqK22+/PWXYanfIDx6p9AjsQmtmnFPpEQB2uUJ7e3v7/7pznz59YuzYsbF48eKObddee20sW7YslixZst1jSqVSlEqljvXW1tZoaGiIlpaWqK2t3YnRq4+w2LMIC2B30traGnV1df/193dZt0IGDRoUI0eO7LTtiCOOiHfeeWeHxxSLxaitre20AAC7p7LCYvz48fHGG2902vaPf/wjhg4dmjoUAFCdygqLG264IZYuXRo//vGPY9WqVfHAAw/EL3/5y2hsbOyu+QCAKlJWWBx33HHx0EMPxe9+97s46qij4o477oiZM2fGpEmTums+AKCKlPWtkIiIc889N84999zumAUAqHLeFQIApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAECassLitttui0Kh0GkZMWJEd80GAFSZ3uUecOSRR8aTTz75//4Bvcv+RwAAu6myq6B3795x0EEHdccsAECVK/sZi5UrV8bgwYNj2LBhMWnSpHjnnXe+dP9SqRStra2dFgBg91RWWJxwwgkxd+7cmD9/fsyePTtWr14dJ554YmzatGmHxzQ1NUVdXV3H0tDQsNNDAwA9U6G9vb29qwd//PHHMXTo0Lj77rvj8ssv3+4+pVIpSqVSx3pra2s0NDRES0tL1NbWdvXUVemQHzxS6RHYhdbMOKfSIwCkaW1tjbq6uv/6+3unnrzcf//942tf+1qsWrVqh/sUi8UoFos7cxoAoErs1N+x2Lx5c7z55psxaNCgrHkAgCpWVljceOONsWjRolizZk0sXrw4Lrzwwthrr73i0ksv7a75AIAqUtatkLVr18all14aH374YRx44IHxjW98I5YuXRoHHnhgd80HAFSRssJi3rx53TUHALAb8K4QACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACCNsAAA0ggLACDNToXFjBkzolAoxPXXX580DgBQzbocFsuWLYs5c+bEqFGjMucBAKpYl8Ji8+bNMWnSpLjvvvuiX79+2TMBAFWqS2HR2NgY55xzTpx22mnZ8wAAVax3uQfMmzcvnn/++Vi2bNn/tH+pVIpSqdSx3traWu4pAYAqUVZYNDc3x3XXXRdPPPFE7LPPPv/TMU1NTXH77bd3aTiAanHIDx6p9AjsQmtmnFPpEXqssm6FrFixIjZs2BDHHnts9O7dO3r37h2LFi2Kn//859G7d+/YunXrNsdMmzYtWlpaOpbm5ua04QGAnqWsKxYTJkyIl19+udO2yy67LEaMGBE33XRT7LXXXtscUywWo1gs7tyUAEBVKCssampq4qijjuq0bb/99ov+/ftvsx0A2PP4y5sAQJqyvxXyn55++umEMQCA3YErFgBAGmEBAKQRFgBAGmEBAKQRFgBAGmEBAKQRFgBAGmEBAKQRFgBAGmEBAKQRFgBAGmEBAKQRFgBAGmEBAKQRFgBAGmEBAKQRFgBAGmEBAKQRFgBAGmEBAKQRFgBAGmEBAKQRFgBAGmEBAKQRFgBAGmEBAKQRFgBAGmEBAKQRFgBAGmEBAKQRFgBAGmEBAKQRFgBAGmEBAKQRFgBAGmEBAKQRFgBAGmEBAKQRFgBAGmEBAKQRFgBAGmEBAKQRFgBAGmEBAKQRFgBAGmEBAKQRFgBAGmEBAKQpKyxmz54do0aNitra2qitrY1x48bFo48+2l2zAQBVpqywGDJkSMyYMSNWrFgRy5cvj1NPPTUuuOCCePXVV7trPgCgivQuZ+fzzjuv0/qdd94Zs2fPjqVLl8aRRx6ZOhgAUH3KCov/39atW+P3v/99bNmyJcaNG7fD/UqlUpRKpY711tbWrp4SAOjhyn548+WXX46vfOUrUSwW48orr4yHHnooRo4cucP9m5qaoq6urmNpaGjYqYEBgJ6r7LA4/PDD44UXXojnnnsurrrqqpg8eXK89tprO9x/2rRp0dLS0rE0Nzfv1MAAQM9V9q2QPn36xPDhwyMiYsyYMbFs2bL42c9+FnPmzNnu/sViMYrF4s5NCQBUhZ3+OxZtbW2dnqEAAPZcZV2xmDZtWpx11llx8MEHx6ZNm+KBBx6Ip59+Oh577LHumg8AqCJlhcWGDRviu9/9bvzzn/+Murq6GDVqVDz22GNx+umnd9d8AEAVKSss7r///u6aAwDYDXhXCACQRlgAAGmEBQCQRlgAAGmEBQCQRlgAAGmEBQCQRlgAAGmEBQCQRlgAAGmEBQCQRlgAAGmEBQCQRlgAAGmEBQCQRlgAAGmEBQCQRlgAAGmEBQCQRlgAAGmEBQCQRlgAAGmEBQCQRlgAAGmEBQCQRlgAAGmEBQCQRlgAAGmEBQCQRlgAAGmEBQCQRlgAAGmEBQCQRlgAAGmEBQCQRlgAAGmEBQCQRlgAAGmEBQCQRlgAAGmEBQCQRlgAAGmEBQCQRlgAAGmEBQCQRlgAAGmEBQCQRlgAAGnKCoumpqY47rjjoqamJgYMGBDf/OY344033uiu2QCAKlNWWCxatCgaGxtj6dKl8cQTT8Rnn30WZ5xxRmzZsqW75gMAqkjvcnaeP39+p/W5c+fGgAEDYsWKFXHSSSelDgYAVJ+ywuI/tbS0RETEAQccsMN9SqVSlEqljvXW1tadOSUA0IN1+eHNtra2uP7662P8+PFx1FFH7XC/pqamqKur61gaGhq6ekoAoIfrclg0NjbGK6+8EvPmzfvS/aZNmxYtLS0dS3Nzc1dPCQD0cF26FXL11VfHn//853jmmWdiyJAhX7pvsViMYrHYpeEAgOpSVli0t7fHNddcEw899FA8/fTTceihh3bXXABAFSorLBobG+OBBx6Ihx9+OGpqamL9+vUREVFXVxd9+/btlgEBgOpR1jMWs2fPjpaWljj55JNj0KBBHcuDDz7YXfMBAFWk7FshAAA74l0hAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAEAaYQEApBEWAECassPimWeeifPOOy8GDx4chUIh/vjHP3bDWABANSo7LLZs2RKjR4+Oe+65pzvmAQCqWO9yDzjrrLPirLPO6o5ZAIAq5xkLACBN2VcsylUqlaJUKnWst7a2dvcpAYAK6fYrFk1NTVFXV9exNDQ0dPcpAYAK6fawmDZtWrS0tHQszc3N3X1KAKBCuv1WSLFYjGKx2N2nAQB6gLLDYvPmzbFq1aqO9dWrV8cLL7wQBxxwQBx88MGpwwEA1aXssFi+fHmccsopHetTp06NiIjJkyfH3Llz0wYDAKpP2WFx8sknR3t7e3fMAgBUOX/HAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDTCAgBIIywAgDRdCot77rknDjnkkNhnn33ihBNOiL/97W/ZcwEAVajssHjwwQdj6tSpMX369Hj++edj9OjRMXHixNiwYUN3zAcAVJGyw+Luu++OK664Ii677LIYOXJk3HvvvbHvvvvGr371q+6YDwCoIr3L2fnTTz+NFStWxLRp0zq29erVK0477bRYsmTJdo8plUpRKpU61ltaWiIiorW1tSvzVrW20r8qPQK70J743/iezM/3nmVP/Pn+4t+5vb39S/crKyw++OCD2Lp1awwcOLDT9oEDB8brr7++3WOampri9ttv32Z7Q0NDOaeGqlM3s9ITAN1lT/753rRpU9TV1e3w87LCoiumTZsWU6dO7Vhva2uLjRs3Rv/+/aNQKHT36amw1tbWaGhoiObm5qitra30OEAiP997lvb29ti0aVMMHjz4S/crKyzq6+tjr732ivfee6/T9vfeey8OOuig7R5TLBajWCx22rb//vuXc1p2A7W1tf7HA7spP997ji+7UvGFsh7e7NOnT4wZMyYWLFjQsa2trS0WLFgQ48aNK39CAGC3UvatkKlTp8bkyZNj7Nixcfzxx8fMmTNjy5Ytcdlll3XHfABAFSk7LC6++OJ4//3349Zbb43169fHMcccE/Pnz9/mgU6I+PetsOnTp29zOwyofn6+2Z5C+3/73ggAwP/Iu0IAgDTCAgBIIywAgDTCAgBIIywAgDTCgm6zdu3aWLt2baXHABJ9/vnn8eSTT8acOXNi06ZNERGxbt262Lx5c4Uno6cQFqRqa2uLH/3oR1FXVxdDhw6NoUOHxv777x933HFHtLW1VXo8YCe8/fbbcfTRR8cFF1wQjY2N8f7770dExF133RU33nhjhaejp+j2l5CxZ7n55pvj/vvvjxkzZsT48eMj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",
+            "text/plain": [
+              "<Figure size 640x480 with 1 Axes>"
+            ]
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        }
+      ],
+      "source": [
+        "import pandas\n",
+        "from collections import Counter\n",
+        "\n",
+        "# Sort the results so that the histogram labels appear in the correct order\n",
+        "results.sort()\n",
+        "pandas.Series(results).value_counts(sort=False).plot(kind='bar')"
+      ]
+    },
+    {
+      "cell_type": "markdown",
+      "id": "990642e4",
+      "metadata": {},
+      "source": [
+        "A compiler error in the entry expression:"
+      ]
+    },
+    {
+      "cell_type": "code",
+      "execution_count": 16,
+      "id": "f045ad9c",
+      "metadata": {},
+      "outputs": [
+        {
+          "ename": "QSharpError",
+          "evalue": "\u001b[31mQsc.TypeCk.TyMismatch\u001b[0m\n\n  \u001b[31m×\u001b[0m type error\n\u001b[31m  ╰─▶ \u001b[0mexpected Unit, found String\n   ╭────\n \u001b[2m1\u001b[0m │ RandomBit(\"a\")\n   · \u001b[35;1m──────────────\u001b[0m\n   ╰────\n",
+          "output_type": "error",
+          "traceback": [
+            "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
+            "\u001b[0;31mQSharpError\u001b[0m                               Traceback (most recent call last)",
+            "Cell \u001b[0;32mIn[16], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m qsharp\u001b[39m.\u001b[39;49meval_with_shots(\u001b[39m\"\"\"\u001b[39;49m\u001b[39mRandomBit(\u001b[39;49m\u001b[39m\"\u001b[39;49m\u001b[39ma\u001b[39;49m\u001b[39m\"\u001b[39;49m\u001b[39m)\u001b[39;49m\u001b[39m\"\"\"\u001b[39;49m, \u001b[39m10\u001b[39;49m)\n",
+            "File \u001b[0;32m~/miniconda3/envs/qsharp/lib/python3.11/site-packages/qsharp/_qsharp.py:66\u001b[0m, in \u001b[0;36meval_with_shots\u001b[0;34m(entry_expr, shots)\u001b[0m\n\u001b[1;32m     63\u001b[0m \u001b[39mdef\u001b[39;00m \u001b[39mcallback\u001b[39m(output):\n\u001b[1;32m     64\u001b[0m     \u001b[39mprint\u001b[39m(output)\n\u001b[0;32m---> 66\u001b[0m \u001b[39mreturn\u001b[39;00m _interpreter\u001b[39m.\u001b[39;49meval_with_shots(entry_expr, shots, callback)\n",
+            "\u001b[0;31mQSharpError\u001b[0m: \u001b[31mQsc.TypeCk.TyMismatch\u001b[0m\n\n  \u001b[31m×\u001b[0m type error\n\u001b[31m  ╰─▶ \u001b[0mexpected Unit, found String\n   ╭────\n \u001b[2m1\u001b[0m │ RandomBit(\"a\")\n   · \u001b[35;1m──────────────\u001b[0m\n   ╰────\n"
+          ]
+        }
+      ],
+      "source": [
+        "qsharp.run(\"\"\"RandomBit(\"a\")\"\"\", 10)"
+      ]
+    },
+    {
+      "cell_type": "markdown",
+      "id": "0ba56462",
+      "metadata": {},
+      "source": [
+        "Some shots throw runtime errors:"
+      ]
+    },
+    {
+      "cell_type": "code",
+      "execution_count": 17,
+      "id": "9b85eb2d",
+      "metadata": {
+        "vscode": {
+          "languageId": "qsharp"
+        }
+      },
+      "outputs": [],
+      "source": [
+        "%%qsharp\n",
+        "\n",
+        "operation Bad() : Unit {\n",
+        "    use q = Qubit();\n",
+        "    H(q);\n",
+        "    let res = M(q);\n",
+        "    if (res == One) {\n",
+        "        // Do something bad, sometimes\n",
+        "        use q2 = Qubit();\n",
+        "        X(q2);\n",
+        "    }\n",
+        "}"
+      ]
+    },
+    {
+      "cell_type": "code",
+      "execution_count": 18,
+      "id": "2445df05",
+      "metadata": {},
+      "outputs": [
+        {
+          "data": {
+            "text/plain": [
+              "[None,\n",
+              " None,\n",
+              " None,\n",
+              " None,\n",
+              " module.QSharpError('Error: Qubit1 released while not in |0⟩ state\\nCall stack:\\n    at QIR.Runtime.__quantum__rt__qubit_release in qir.qs\\n    at Bad in <expression>\\n\\x1b[31mQsc.Eval.ReleasedQubitNotZero\\x1b[0m\\n\\n  \\x1b[31m×\\x1b[0m runtime error\\n\\x1b[31m  ╰─▶ \\x1b[0mQubit1 released while not in |0⟩ state\\n'),\n",
+              " None,\n",
+              " None,\n",
+              " module.QSharpError('Error: Qubit1 released while not in |0⟩ state\\nCall stack:\\n    at QIR.Runtime.__quantum__rt__qubit_release in qir.qs\\n    at Bad in <expression>\\n\\x1b[31mQsc.Eval.ReleasedQubitNotZero\\x1b[0m\\n\\n  \\x1b[31m×\\x1b[0m runtime error\\n\\x1b[31m  ╰─▶ \\x1b[0mQubit1 released while not in |0⟩ state\\n'),\n",
+              " None,\n",
+              " None]"
+            ]
+          },
+          "execution_count": 18,
+          "metadata": {},
+          "output_type": "execute_result"
+        }
+      ],
+      "source": [
+        "qsharp.run(\"Bad()\", 10)"
+      ]
     }
   ],
   "metadata": {
diff --git a/pip/src/interpreter.rs b/pip/src/interpreter.rs
index 3f9abbbb..71ce072f 100644
--- a/pip/src/interpreter.rs
+++ b/pip/src/interpreter.rs
@@ -5,7 +5,10 @@ use crate::displayable_output::{DisplayableOutput, DisplayableState};
 use miette::Report;
 use num_bigint::BigUint;
 use num_complex::Complex64;
-use pyo3::{create_exception, exceptions::PyException, prelude::*, types::PyList, types::PyTuple};
+use pyo3::{
+    create_exception, exceptions::PyException, prelude::*, pyclass::CompareOp, types::PyList,
+    types::PyTuple,
+};
 use qsc::{
     fir,
     interpret::{
@@ -94,7 +97,27 @@ impl Interpreter {
         }
     }
 
-    #[allow(clippy::unused_self)]
+    fn run(
+        &mut self,
+        py: Python,
+        entry_expr: &str,
+        shots: u32,
+        callback: Option<PyObject>,
+    ) -> PyResult<Py<PyList>> {
+        let mut receiver = OptionalCallbackReceiver { callback, py };
+        match self.interpreter.run(&mut receiver, entry_expr, shots) {
+            Ok(results) => Ok(PyList::new(
+                py,
+                results.into_iter().map(|res| match res {
+                    Ok(v) => ValueWrapper(v).into_py(py),
+                    Err(errors) => QSharpError::new_err(format_errors(errors)).into_py(py),
+                }),
+            )
+            .into_py(py)),
+            Err(errors) => Err(QSharpError::new_err(format_errors(errors))),
+        }
+    }
+
     fn qir(&mut self, _py: Python, entry_expr: &str) -> PyResult<String> {
         match self.interpreter.qirgen(entry_expr) {
             Ok(qir) => Ok(qir),
@@ -152,12 +175,47 @@ impl Output {
 }
 
 #[pyclass(unsendable)]
+#[derive(PartialEq)]
 /// A Q# measurement result.
 pub(crate) enum Result {
     Zero,
     One,
 }
 
+#[pymethods]
+impl Result {
+    fn __repr__(&self) -> String {
+        match self {
+            Result::Zero => "Zero".to_owned(),
+            Result::One => "One".to_owned(),
+        }
+    }
+
+    fn __str__(&self) -> String {
+        self.__repr__()
+    }
+
+    fn __hash__(&self) -> u32 {
+        match self {
+            Result::Zero => 0,
+            Result::One => 1,
+        }
+    }
+
+    fn __richcmp__(&self, other: &Self, op: CompareOp) -> bool {
+        let this = i32::from(*self == Result::One);
+        let other = i32::from(*other == Result::One);
+        match op {
+            CompareOp::Lt => this < other,
+            CompareOp::Le => this <= other,
+            CompareOp::Eq => this == other,
+            CompareOp::Ne => this != other,
+            CompareOp::Gt => this > other,
+            CompareOp::Ge => this >= other,
+        }
+    }
+}
+
 #[pyclass(unsendable)]
 /// A Q# Pauli operator.
 pub(crate) enum Pauli {


Test patch:
diff --git a/compiler/qsc/src/interpret/stateful/tests.rs b/compiler/qsc/src/interpret/stateful/tests.rs
index b30d3b05..8c2a3c87 100644
--- a/compiler/qsc/src/interpret/stateful/tests.rs
+++ b/compiler/qsc/src/interpret/stateful/tests.rs
@@ -2,7 +2,7 @@
 // Licensed under the MIT License.
 
 mod given_interpreter {
-    use crate::interpret::stateful::{Interpreter, LineError};
+    use crate::interpret::stateful::{Interpreter, LineError, LineResult};
     use expect_test::Expect;
     use miette::Diagnostic;
     use qsc_eval::{output::CursorReceiver, val::Value};
@@ -10,7 +10,7 @@ mod given_interpreter {
     use qsc_passes::PackageType;
     use std::{fmt::Write, io::Cursor, iter, str::from_utf8};
 
-    fn line(interpreter: &mut Interpreter, line: &str) -> (Result<Value, Vec<LineError>>, String) {
+    fn line(interpreter: &mut Interpreter, line: &str) -> (LineResult, String) {
         let mut cursor = Cursor::new(Vec::<u8>::new());
         let mut receiver = CursorReceiver::new(&mut cursor);
         (
@@ -19,6 +19,16 @@ mod given_interpreter {
         )
     }
 
+    fn run(
+        interpreter: &mut Interpreter,
+        expr: &str,
+        shots: u32,
+    ) -> (Result<Vec<LineResult>, Vec<LineError>>, String) {
+        let mut cursor = Cursor::new(Vec::<u8>::new());
+        let mut receiver = CursorReceiver::new(&mut cursor);
+        (interpreter.run(&mut receiver, expr, shots), receiver.dump())
+    }
+
     fn entry(
         interpreter: &mut Interpreter,
     ) -> (
@@ -908,6 +918,93 @@ mod given_interpreter {
                 !3 = !{i32 1, !"dynamic_result_management", i1 false}
             "#]].assert_eq(&res);
         }
+
+        #[test]
+        fn run_with_shots() {
+            let mut interpreter = get_interpreter();
+            let (result, output) = line(&mut interpreter, "operation Foo() : Int { 1 }");
+            is_only_value(&result, &output, &Value::unit());
+            let (results, output) = run(&mut interpreter, "Foo()", 5);
+            assert_eq!(output, String::new());
+            let results = results.expect("run() should succeed");
+            assert_eq!(results.len(), 5);
+            for r in results {
+                let val = r.expect("individual run should succeed");
+                assert_eq!(val, Value::Int(1));
+            }
+        }
+
+        #[test]
+        fn run_parse_error() {
+            let mut interpreter = get_interpreter();
+            let (results, _) = run(&mut interpreter, "Foo)", 5);
+            results.expect_err("run() should fail");
+        }
+
+        #[test]
+        fn run_compile_error() {
+            let mut interpreter = get_interpreter();
+            let (results, _) = run(&mut interpreter, "Foo()", 5);
+            results.expect_err("run() should fail");
+        }
+
+        #[test]
+        fn run_multiple_statements() {
+            let mut interpreter = get_interpreter();
+            let (result, output) = line(&mut interpreter, "operation Foo() : Int { 1 }");
+            is_only_value(&result, &output, &Value::unit());
+            let (result, output) = line(&mut interpreter, "operation Bar() : Int { 2 }");
+            is_only_value(&result, &output, &Value::unit());
+            let (results, output) = run(&mut interpreter, "{ Foo(); Bar() }", 5);
+            assert_eq!(output, String::new());
+            let results = results.expect("run() should succeed");
+            assert_eq!(results.len(), 5);
+            for r in results {
+                let val = r.expect("individual run should succeed");
+                assert_eq!(val, Value::Int(2));
+            }
+        }
+
+        #[test]
+        fn run_runtime_failure() {
+            let mut interpreter = get_interpreter();
+            let (result, output) = line(
+                &mut interpreter,
+                r#"operation Foo() : Int { fail "failed" }"#,
+            );
+            is_only_value(&result, &output, &Value::unit());
+            let (results, output) = run(&mut interpreter, "Foo()", 5);
+            assert_eq!(output, String::new());
+            let results = results.expect("run() should succeed");
+            assert_eq!(results.len(), 5);
+            for r in results {
+                r.expect_err("individual run should fail");
+            }
+        }
+
+        #[test]
+        fn run_output_merged() {
+            let mut interpreter = get_interpreter();
+            let (result, output) = line(
+                &mut interpreter,
+                r#"operation Foo() : Unit { Message("hello!") }"#,
+            );
+            is_only_value(&result, &output, &Value::unit());
+            let (results, output) = run(&mut interpreter, "Foo()", 5);
+            expect![[r#"
+                hello!
+                hello!
+                hello!
+                hello!
+                hello!"#]]
+            .assert_eq(&output);
+            let results = results.expect("run() should succeed");
+            assert_eq!(results.len(), 5);
+            for r in results {
+                let val = r.expect("individual run should succeed");
+                assert_eq!(val, Value::unit());
+            }
+        }
     }
 
     #[cfg(test)]
diff --git a/pip/tests/test_interpreter.py b/pip/tests/test_interpreter.py
index f67663fa..24177886 100644
--- a/pip/tests/test_interpreter.py
+++ b/pip/tests/test_interpreter.py
@@ -153,3 +153,21 @@ def test_qirgen() -> None:
     e.interpret("operation Program() : Result { use q = Qubit(); return M(q) }")
     qir = e.qir("Program()")
     assert isinstance(qir, str)
+
+
+def test_run_with_shots() -> None:
+    e = Interpreter(TargetProfile.Full)
+
+    def callback(output):
+        nonlocal called
+        called += 1
+        assert output.__repr__() == "Hello, world!"
+
+    called = 0
+    e.interpret('operation Foo() : Unit { Message("Hello, world!"); }', callback)
+    assert called == 0
+
+    value = e.run("Foo()", 5, callback)
+    assert called == 5
+
+    assert value == [None, None, None, None, None]