{ "cells": [ { "cell_type": "markdown", "id": "e58eb56d67213248", "metadata": {}, "source": [ "# Automatic Encoding for the Steane Code\n", "\n", "**Download this notebook - {nb-download}`steane_encoding.ipynb`**\n", "\n", "Guppy FT is an extension of the Guppy quantum programming language to aid writing, compiling and running fault-tolerant programs by providing a separation of concerns between writing quantum algorithms and quantum error correction (QEC). Researchers are able to write QEC-agnostic programs, and utilize QEC code architectures developed by experts to automatically encode into an error-protected program.\n", "\n", "This notebook provides an introduction to the encoding features of Guppy FT through simple examples." ] }, { "cell_type": "markdown", "id": "6b44df7b8ddf10c6", "metadata": {}, "source": [ "## Encoding a Guppy program\n", "\n", "Encoding happens in four stages:\n", "1. Write a computational Guppy program using {py:mod}`guppylang.std.quantum` operations.\n", "2. Run an encoding pass to convert the computational program into a logical program.\n", "3. (Optional) Run logical-aware optimization passes.\n", "4. Replace the logical operations with physical implementations.\n", "\n", "Let's see a full workflow with a quantum teleportation program." ] }, { "cell_type": "code", "execution_count": 1, "id": "54acb89105fbc218", "metadata": { "ExecuteTime": { "end_time": "2026-08-17T12:22:45.434689Z", "start_time": "2026-08-17T12:22:44.555519Z" } }, "outputs": [], "source": [ "from guppylang import guppy\n", "from guppylang.std import quantum as qlib\n", "from guppylang.std.builtins import array, owned\n", "from guppylang.std.platform import output" ] }, { "cell_type": "markdown", "id": "833269ade1b89599", "metadata": {}, "source": [ "First, we write our computational program. At this stage, we are not concerned with QEC. The program is written assuming that it will run on a noiseless quantum computer using {py:mod}`guppylang.std` operations." ] }, { "cell_type": "code", "execution_count": 2, "id": "initial_id", "metadata": { "ExecuteTime": { "end_time": "2026-08-17T12:22:45.447134Z", "start_time": "2026-08-17T12:22:45.435424Z" } }, "outputs": [], "source": [ "@guppy\n", "def teleport_primitive(src: qlib.qubit @ owned) -> qlib.qubit:\n", " # Create Bell pair\n", " tmp = qlib.qubit()\n", " tgt = qlib.qubit()\n", " qlib.h(tmp)\n", " qlib.cx(tmp, tgt)\n", "\n", " # Teleport\n", " qlib.cx(src, tmp)\n", " qlib.h(src)\n", " if qlib.measure(src).read():\n", " qlib.z(tgt)\n", " if qlib.measure(tmp).read():\n", " qlib.x(tgt)\n", "\n", " return tgt\n", "\n", "\n", "@guppy\n", "def teleport() -> None:\n", " # Qubit to be teleported\n", " src = qlib.qubit()\n", "\n", " # Teleport `src` to `tgt`\n", " tgt = teleport_primitive(src)\n", " output(\"tgt\", qlib.measure(tgt).read())" ] }, { "cell_type": "markdown", "id": "bc1bc32783ddce04", "metadata": {}, "source": [ "Now we can define the QEC code architecture that we would use to encode our program to be fault-tolerant. An architecture includes the necessary encoding and implementation passes to convert our computational program into a runnable, physical package.\n", "\n", "For this example, we will use the Steane architecture." ] }, { "cell_type": "code", "execution_count": 3, "id": "a42c17914d9c1a13", "metadata": { "ExecuteTime": { "end_time": "2026-08-17T12:22:46.489198Z", "start_time": "2026-08-17T12:22:46.461321Z" } }, "outputs": [], "source": [ "from guppyft.code.steane.encode import SteaneBuilder\n", "\n", "# Define a Steane architecture instance.\n", "# The only required parameter is an upper bound\n", "# to the total number of logical blocks available\n", "# during program execution.\n", "\n", "steane = SteaneBuilder().build(n_blocks=3)" ] }, { "cell_type": "markdown", "id": "49eaaf875eeb1d4f", "metadata": {}, "source": [ "We can use our architecture to encode the program, running the full pass from computational to logical and finally to physical, producing a package that we can run." ] }, { "cell_type": "code", "execution_count": 4, "id": "aec1f483331ecde7", "metadata": { "ExecuteTime": { "end_time": "2026-08-17T12:22:47.714661Z", "start_time": "2026-08-17T12:22:46.489812Z" } }, "outputs": [], "source": [ "teleport_unencoded = teleport.compile()\n", "teleport_encoded = steane.encode(teleport_unencoded)" ] }, { "cell_type": "markdown", "id": "15a7b99e-76eb-42eb-a798-6d3c23e53bcb", "metadata": {}, "source": [ "We can emulate the encoded program using Selene locally, or submit it to Nexus cloud to run on proprietary emulators or quantum devices. Here, we illustrate local emulation using Stim (a Clifford circuit simulator) and a simple depolarizing noise model." ] }, { "cell_type": "code", "execution_count": 5, "id": "58c37bb3-f6d9-47f0-97e5-adfaeb729351", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Counter({(('tgt', '0'),): 999, (('tgt', '1'),): 1})\n" ] } ], "source": [ "from guppylang.emulator import EmulatorBuilder\n", "from selene_sim.backends.bundled_error_models import DepolarizingErrorModel\n", "from selene_sim.backends.bundled_simulators import Stim\n", "\n", "output = (\n", " # Create an EmulatorBuilder and instantiate it with the encoded program\n", " EmulatorBuilder()\n", " .build(teleport_encoded, n_qubits=22)\n", " # Then, configure the emulator instance\n", " .with_simulator(Stim(random_seed=42))\n", " .with_error_model(DepolarizingErrorModel(random_seed=42, p_1q=1e-4, p_2q=1e-3))\n", " .with_shots(1000)\n", ").run()\n", "\n", "print(output.collated_counts())" ] }, { "cell_type": "markdown", "id": "fb710d57-9444-4d8e-bdff-10fbac7f14ff", "metadata": {}, "source": [ "For convenience, the Steane architecture provides an {py:meth}`~guppyft.code.steane.encode.SteaneInstance.emulator()` method that automatically encodes the program and builds the emulator. See the API docs for {py:meth}`~guppyft.code.steane.encode.SteaneInstance.emulator()`, under {py:class}`~guppyft.code.steane.encode.SteaneInstance`.\n", "\n", "Furthermore, we can explore the trace of operations that have been emulated. A quick check confirms that the encoded program is considerably deeper." ] }, { "cell_type": "code", "execution_count": 6, "id": "7995f307-304f-4e52-8e5d-7256435d4a7a", "metadata": {}, "outputs": [], "source": [ "from selene_sim.event_hooks.instruction_log import CircuitExtractor\n", "\n", "# Create a CircuitExtractor to log the instructions that would run on the quantum device\n", "logger = CircuitExtractor()\n", "\n", "# Obtain the depth of the unencoded program\n", "output = (\n", " EmulatorBuilder()\n", " .build(teleport_unencoded, n_qubits=3)\n", " .with_simulator(Stim(random_seed=42)) # Use stabilizer state simulation\n", " .with_event_hook(logger)\n", ").run()\n", "\n", "unencoded_depth = logger.shots[0].get_user_circuit().depth()" ] }, { "cell_type": "code", "execution_count": 7, "id": "e4adb0dc-b650-4a78-9b70-0afa737af83c", "metadata": {}, "outputs": [], "source": [ "# Create a new CircuitExtractor instance\n", "logger = CircuitExtractor()\n", "\n", "# Obtain the depth of the encoded program\n", "output = (\n", " EmulatorBuilder()\n", " .build(teleport_encoded, n_qubits=22)\n", " .with_simulator(Stim(random_seed=42)) # Use stabilizer state simulation\n", " .with_event_hook(logger)\n", ").run()\n", "\n", "encoded_depth = logger.shots[0].get_user_circuit().depth()" ] }, { "cell_type": "code", "execution_count": 8, "id": "ac437a35-c2cc-4c3f-92a9-62e4686fc0c5", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Depth (longest sequence of operations):\n", "\tUnencoded program: 11\n", "\tEncoded program: 73\n" ] } ], "source": [ "print(\"Depth (longest sequence of operations):\")\n", "print(f\"\\tUnencoded program: {unencoded_depth}\")\n", "print(f\"\\tEncoded program: {encoded_depth}\")" ] }, { "cell_type": "markdown", "id": "a123cc540483ab42", "metadata": {}, "source": [ "## Dynamic allocation of logical qubits\n", "\n", "Guppy FT provides support for arbitrary control flow through dynamic allocation of logical qubits. Below is an example of qubits being dynamically allocated based on the outcome of a qubit measurement." ] }, { "cell_type": "code", "execution_count": 9, "id": "82b7561a2f3c6ef", "metadata": { "ExecuteTime": { "end_time": "2026-08-17T12:22:51.894843Z", "start_time": "2026-08-17T12:22:49.556645Z" } }, "outputs": [], "source": [ "@guppy\n", "def dynamic_allocation() -> None:\n", " q0 = qlib.qubit()\n", " qlib.h(q0)\n", " if qlib.measure(q0): # If true, allocate a single qubit\n", " q_arr = array(qlib.qubit())\n", " for q in q_arr:\n", " output(\"q\", qlib.measure(q).read())\n", " else: # Otherwise, allocate two qubits\n", " q_arr = array(qlib.qubit(), qlib.qubit())\n", " qlib.cx(q_arr[0], q_arr[1])\n", " for q in q_arr:\n", " output(\"q\", qlib.measure(q).read())\n", "\n", "\n", "dyn_alloc_encoded = (\n", " SteaneBuilder().build(n_blocks=3).encode(dynamic_allocation.compile())\n", ")" ] }, { "cell_type": "markdown", "id": "1cd8b09e3b0b7e0a", "metadata": {}, "source": [ "## Mid-circuit measurement and qubit reuse\n", "\n", "The Quantinuum stack supports mid-circuit measurements and qubit reuse. This support continues at the logical level with Guppy FT.\n", "\n", "We can demonstrate this using the `teleport_primitive` defined above to teleport a qubit twice. Without qubit reuse, this program would require 5 logical qubits. However, only 3 logical qubits are required at any one time, as demonstrated by imposing `n_blocks=3`." ] }, { "cell_type": "code", "execution_count": 10, "id": "6c76fa58fea79088", "metadata": { "ExecuteTime": { "end_time": "2026-08-17T12:22:55.292628Z", "start_time": "2026-08-17T12:22:53.693945Z" } }, "outputs": [], "source": [ "@guppy\n", "def qubit_reuse() -> None:\n", " # Qubit to teleport\n", " src = qlib.qubit()\n", "\n", " # Run teleportation twice\n", " tgt = teleport_primitive(src)\n", " src = teleport_primitive(tgt)\n", "\n", " output(\"src\", qlib.measure(src).read())\n", "\n", "\n", "# Encode with only a single logical block to demonstrate logical reuse.\n", "reuse_encoded = SteaneBuilder().build(n_blocks=3).encode(qubit_reuse.compile())" ] }, { "cell_type": "markdown", "id": "7d7bd1daacf0791e", "metadata": {}, "source": [ "## Dynamic QEC cycle insertion\n", "\n", "In quantum programs with complex control flow, it can be useful to use information obtained during runtime to determine when to insert a QEC cycle. Guppy FT supports dynamic insertion of QEC cycles depending on the logical gates that have been performed.\n", "\n", "In this demonstration, each logical operation is assigned a cost, which is tracked on a per-block basis. At runtime, once a threshold is reached, a QEC cycle is inserted, and the tracking counter is reset to 0." ] }, { "cell_type": "code", "execution_count": 11, "id": "d33452411fa8f020", "metadata": { "ExecuteTime": { "end_time": "2026-08-17T12:22:57.773052Z", "start_time": "2026-08-17T12:22:57.769482Z" } }, "outputs": [], "source": [ "from guppyft.code.steane.encode import QECPolicy, QECStyle\n", "\n", "# Define a QEC policy using Steane style syndrome extraction\n", "# We set the threshold to be 2.\n", "# Both the logical `H` and `CX` gates each have a cost of 1.\n", "qec_policy = QECPolicy(style=QECStyle.Steane, threshold=2)\n", "qec_policy.costs.h = 1.0\n", "qec_policy.costs.cx = 1.0\n", "\n", "# We can now provide the `qec_policy` to define our Steane architecture.\n", "steane_qec = SteaneBuilder().with_qec_policy(qec_policy).build(n_blocks=2)" ] }, { "cell_type": "code", "execution_count": 12, "id": "4f839220ee8e3757", "metadata": { "ExecuteTime": { "end_time": "2026-08-17T12:23:02.654751Z", "start_time": "2026-08-17T12:22:57.773613Z" } }, "outputs": [], "source": [ "# Demonstration program to apply `H` and `CX` gates to two qubits.\n", "@guppy\n", "def my_program() -> None:\n", " q0 = qlib.qubit()\n", " q1 = qlib.qubit()\n", "\n", " # Track costs: [q0 , q1 ]\n", " qlib.h(q0) # [1.0, 0.0]\n", " qlib.cx(q0, q1) # [2.0, 1.0]\n", " # [0.0, 1.0] <-- QEC cycle on q0\n", " qlib.h(q1) # [1.0, 2.0]\n", " # [1.0, 0.0] <-- QEC cycle on q1\n", "\n", " output(\"q0\", qlib.measure(q0).read())\n", " output(\"q1\", qlib.measure(q1).read())\n", "\n", "\n", "# Encode two programs with, and without our QEC policy.\n", "encoded_without_qec_cycles = (\n", " SteaneBuilder().build(n_blocks=2).encode(my_program.compile())\n", ")\n", "encoded_with_qec_cycles = (\n", " SteaneBuilder()\n", " .with_qec_policy(qec_policy)\n", " .build(n_blocks=2)\n", " .encode(my_program.compile())\n", ")" ] }, { "cell_type": "markdown", "id": "9ac1695c-1eae-4aad-8bef-8d8df6e9a6d7", "metadata": {}, "source": [ "Once again, we can use Selene's `CircuitExtractor` to explore the operations that run and check that, indeed, QEC cycles were introduced.\n", "\n", "In this case, we count the number of measurement operations." ] }, { "cell_type": "code", "execution_count": 13, "id": "e04334a6-de0b-4bfd-84b7-ba9fc6bf36b9", "metadata": {}, "outputs": [], "source": [ "# Create a new CircuitExtractor instance\n", "logger = CircuitExtractor()\n", "\n", "# Obtain the number of measurements executed in the case of no QEC cycles inserted\n", "output = (\n", " EmulatorBuilder()\n", " .build(encoded_without_qec_cycles, n_qubits=22)\n", " .with_simulator(Stim(random_seed=42)) # Use stabilizer state simulation\n", " .with_event_hook(logger)\n", ").run()\n", "\n", "n_meas_no_qec = sum(\n", " 1\n", " for cmd in logger.shots[0].get_user_circuit().get_commands()\n", " if \"Measure\" in str(cmd)\n", ")" ] }, { "cell_type": "code", "execution_count": 14, "id": "04adc5fb-3563-4acc-83ab-9555f9e5c8f5", "metadata": {}, "outputs": [], "source": [ "# Create a new CircuitExtractor instance\n", "logger = CircuitExtractor()\n", "\n", "# Obtain the number of measurements executed in the case of QEC cycles inserted\n", "output = (\n", " EmulatorBuilder()\n", " .build(encoded_with_qec_cycles, n_qubits=22)\n", " .with_simulator(Stim(random_seed=42)) # Use stabilizer state simulation\n", " .with_event_hook(logger)\n", ").run()\n", "\n", "n_meas_qec = sum(\n", " 1\n", " for cmd in logger.shots[0].get_user_circuit().get_commands()\n", " if \"Measure\" in str(cmd)\n", ")" ] }, { "cell_type": "code", "execution_count": 15, "id": "69c7c79e-29f5-4728-9a9a-13d827b96304", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Number of physical measurements performed:\n", "\tWithout QEC cycles: 16\n", "\tWith QEC cycles: 48\n" ] } ], "source": [ "print(\"Number of physical measurements performed:\")\n", "print(f\"\\tWithout QEC cycles: {n_meas_no_qec}\")\n", "print(f\"\\tWith QEC cycles: {n_meas_qec}\")" ] }, { "cell_type": "markdown", "id": "28b602c0-e0ae-4b12-81ea-4685d3653cb3", "metadata": {}, "source": [ "Indeed, there are 32 more physical measurements: each QEC cycle uses `(7+1)*2 = 16` physical measurements, since it prepares and measures two logical zero states (7 physical qubits, plus 1 flag ancilla), and we expect two QEC cycles to be introduced." ] }, { "cell_type": "markdown", "id": "1136d5354512a30d", "metadata": {}, "source": [ "## State factories\n", "\n", "Fault-tolerant state preparation often uses a repeat-until-success (RUS) scheme that measures flag ancilla qubits to verify that the state preparation succeeds. In order to run state preparation in parallel, we need to defer reading the measurement outcome on these ancilla qubits, as described in Guppy's language guide (see [Measurement section](https://docs.quantinuum.com/guppy/language_guide/measurement.html)). Our Steane architecture defines a `StateFactory` that handles deferral of measurements and parallelizes RUS state preparation. Users can configure the parameters of the factory both for zero states and magic states.\n", "\n", "The following example configures an architecture instance that prepares zero states in batches of 2, making up to 5 RUS attempts per request." ] }, { "cell_type": "code", "execution_count": 16, "id": "5a17b40c2a25e9a0", "metadata": { "ExecuteTime": { "end_time": "2026-08-17T12:23:07.847462Z", "start_time": "2026-08-17T12:23:04.779760Z" } }, "outputs": [], "source": [ "from guppyft.code.steane.encode import RUSStateFactoryConf\n", "\n", "# Define the state factory configuration.\n", "factory_conf = RUSStateFactoryConf(size=2, max_attempts=5)\n", "\n", "# Use the factory configuration to define our Steane architecture.\n", "steane_code_factories = (\n", " SteaneBuilder()\n", " .with_qec_policy(qec_policy)\n", " .with_zero_factory_conf(factory_conf)\n", " .build(n_blocks=2)\n", ")\n", "qec_factory_encoded = steane_code_factories.encode(my_program.compile())" ] }, { "cell_type": "markdown", "id": "52d25646-4b08-413a-b9ee-b0fe9fbcd8d8", "metadata": {}, "source": [ "Some tools to visualize parallelism will be released in due time. Until then, users can explore the impact of parallelizing state preparation by running on [Quantinuum Helios-1E emulator](https://docs.quantinuum.com/systems/user_guide/emulator_user_guide/emulators/helios_emulators.html) on Nexus cloud. Helios-1E includes memory noise and, hence, parallelization makes a positive impact on program performance thanks to a decrease in shot time." ] } ], "metadata": { "kernelspec": { "display_name": "guppyft (3.14.0)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.12" } }, "nbformat": 4, "nbformat_minor": 5 }