QEC Decoder Toolkit¶
Quantinuum offers a real-time hybrid compute capability for Quantum Error Correction (QEC) workflows. This capability executes Web Assembly (Wasm) in the H-Series stack and enables use of libraries (e.g. linear algebra and graph algorithms) and complex datastructures (e.g. vectors and graphs) during real-time execution of a quantum circuit. This environment is hereby referred to as WebAssembly Virtual Machine (WAVM). Unlocking experimentation of basic QEC workflows is an important milestone, as Quantinuum plans to upgrade H-Series hardware and deploy practical QEC protocols at scale. The QEC workflow presented here has been used in the following quantum error correction results: [arxiv.2208.01863, arxiv.2305.03828].
Wasm is an industry standard for sandboxed computing. To compile Wasm binaries, a low-level programming language, such as Rust or C, are used to compose desired classical functions before compilation. The quantum computing workflow will then contain a call to Wasm. Wasm is used because it is fast, safe, expressive, and easy to compile with familiar programming languages. Measurement results are processed on WAVM, where a decoding algorithm is used to determine recovery operations and update quantum circuits in real-time. Once generated, the Wasm binary can be injected into a TKET workflow. “Async” Wasm calls involve execution of a void (no output) Wasm function during the TKET circuit execution. The Wasm result is polled until a non-void (one integer output) Wasm function is used to access the Wasm result. This is an important feature that helps reduce qubit idling time and ultimately minimizes the impact of memory error in TKET + Wasm programs.
This article is presented in two sections. The first section, WebAssembly in the H-Series Stack, discusses steps to generate a Wasm compilation project using Rust or C. In addition, guidance is presented on using Wasm with TKET and use cases that may lead to job failure. The second section presents two QEC use cases and one repeat-until-success demonstration. The Mid-circuit Measurement and Reset (MCMR) and Qubit Reuse features are used in the QEC use cases, which is also showcased here: Mid-Circuit Measurement. An explanation of the H-Series submission workflow is found here: Job Submissions. The Wasm module is imported from TKET alongside several optional conditional operators. More information on the conditional operations available can be found in the TKET user manual (Classical and Conditional Operations).
WebAssembly in the H-Series Stack¶
Wasm is an industry standard for sandboxed computing. It offers efficient execution of classical programs and Just-in-Time (JIT) from Wasm to x64/ arm64 is extremely efficient. A WebAssembly Virtual Machine (WAVM) requires as little as tens of microseconds to start or shutdown. Wasm can be generated by composing Rust and C code, followed by a compilation step with the Rust compiler or LLVM toolchain (Clang compiler). Once generated, the Wasm binary can be injected into the TKET workflow. The main benefit is to enable classical compute in the H-Series stack during real-time execution of the quantum circuit.
The primary purpose of WAVM is to provide a real-time classical compute environment for QEC decoding. Wasm programs that are submitted to H-Series usually contain functions accepting integer arguments (measurement results as binary outcomes converted to decimal) and functions returning integer values that map to a conditional prescribed action to correct for errors in QEC programs. These functions can be computationally complex (NP-Complete). Usually, the function attempts to determine the most likely error or errors based on the inputted measurement results. Conditionally prescribed actions are applied once the Wasm processing on the measurement result is complete. These actions are usually fixed-angle 1-qubit operations, such as Pauli-\(\hat{X}\), Pauli-\(\hat{Y}\) or Pauli-\(\hat{Z}\), and need to be compiled into a H-Series consumable gate with TKET.
The end user is responsible for generating a Wasm binary from a source file (usually written in C or Rust) and a TKET circuit with operations defined in the Wasm binary. Both the TKET circuit and the Wasm binary are submitted to the H-Series device. The Wasm binary is loaded into WAVM, co-located with H-Series hardware. The TKET circuit is executed on the hardware. For Wasm operations on the TKET circuit, network calls are made to WAVM to execute the Wasm binary. “Asynchronous” (async) Wasm calls involve execution of a void (no output) Wasm function during the TKET circuit execution. The Wasm result is polled until a non-void (one integer output) Wasm function is used to access the Wasm result.
Wasm Lifecyle¶
Decoders must be implemented in low langauges and compiled to Wasm binaries. The Wasm program lifecycle is shown below.
The following subsections will go over:
Installation of Rust and C
Project Setup
Unit Testing Wasm and C
Compilation from Rust or C to Wasm
It is recommended to use either Rust or C for the compilation language to Wasm. The languages have functionality that easily compiles to Wasm. For other language options see Wasm Getting Started Developer’s page.
The following software is required to define and test decoders, and to compile Wasm binaries.
CMake v3.13
Clang v17.0.3
Cargo v1.77.2
Pytket v1.32.0
Pytket-Quantinuum v0.37.0
Rust | C with Clang Toolchain | ||
---|---|---|---|
1. Installation | Install Rust using the Rust Installer. Windows Users: Before running the installer, you'll need to install the Microsoft Build Tools. If you're new to Rust, see the The Rust Programming Language book. Note that Cargo is the Rust package manager. For more info on Cargo, see The Cargo Book. If using Visual Studio code, you can also install the `rust-analyzer` extension in the Extensions tab. |
For Windows 10/11 users, instruction should be followed here to install clang. For Ubuntu 20.04 users, the development tools group needs to be installed with Aptitude. The LLVM and Clang compilers need to be installed separately. sudo apt-get install build-essential MacOS users must install XCode command line tools to enable clang usage. xcode-select --install LLVM is also available via HomeBrew. CMake also needs to be installed to invoke the C compilation. Windows users can use the Visual Studio installer. CMake is available through both Ubuntu and macOS (brew). Downloads for CMake are here. |
|
2. Project Setup |
|
Create a project directory: mkdir project . The project directory should contain the following subdirectories and files:
|
|
3. Function Implementation | Examples of the types of functions that can be put in Rust source code are provided in the respective folders within each of the example folders. Define the classical functions you plan to use in your hybrid compute program. These will be the classical functions that your quantum program will run during execution. In Rust, edit the src/lib.rs file.#[no_mangle] : Ensures the function name isn’t changed by Rust’s name mangling, making it accessible from other languages or tools. |
Examples of the types of functions that can be put in C source code are provided in the respective folders within each of the example folders. Define the classical functions you plan to use in your hybrid compute program. These will be the classical functions that your quantum program will run during execution. For C, edit the src/lib.c file. The standard C library cannot be used in a program that is intended to be compiled to Wasm. | |
4. Unit Tests | The Cargo Tests module can be used to test the rust source link. Instructions on writing tests can be found here. All tests are defined under the code block with the annotation #[cfg(test)] . This tells the cargo to only run tests when the following command is used: cargo test . Also that annotation prevents the test being included in the compiled Wasm binaryThe test module’s parent’s items are brought into scope within the test code-block with use super::* . Within the code-block annotated by #[cfg(test)] , each unit test is annotated with #[test] .For each rust project, first navigating into to the same level as the cargo.toml file, and then running, cargo test will run all the tests to verify correctness of the rust source code. |
Each C directory contains a CMakeLists.txt and a test_lib.cc file. These file use GoogleTest to unit test the methods defined in src/lib.c. GoogleTest cannot be used with the MSCV clang compiler on Windows 10/ 11. Mingw32 clang or the MSVC toolchain need to be specified instead. test_lib.cc contains an implementation for each test. Two header files are needed:
TEST macro for writing tests. The following code defines an individual test named TestName in the test suite TestSuite .
The following commands need to be called within the tests subdirectory:
|
|
5. Wasm Compilation | Run: cargo build --release --target wasm32-unknown-unknown . This compiles your Rust code to Wasm. The Wasm binary can be found in the ./target/wasm32-unknown-unknown/release/ directory.Adding #![no_std] into src/lib.rs will disable the use of the standard library when compiling to Wasm. |
The C compilation uses the LLVM toolchain and requires that no standard headers be used within the program. The compilation can be invoked with the clang compiler with specific options to compile lib.c into a Wasm binary, lib.wasm:clang --target=wasm32 --no-standard-libraries -Wl,--no-entry -Wl,--export-all -o lib.wasm lib.c The following compiler options are used upon Clang invocation:
The Wasm is generated in the directory specified when calling the clang compiler. CMakeFiles.txt are also provided for each example. The CMake enables Wasm compilatin and automatically uses the compiler and linker flags when clang is invoked. CMake must be called in the root location of each project (at the same directory level as CMakeLists.txt). For compilation on Windows with the MSVC clang toolchain: cmake -G "Ninja" -B build -DCMAKE_TOOLCHAIN_FILE=wasm-toolchain.cmake For Linux and macOS: cmake -G "Unix Makefiles" -B build -DCMAKE_TOOLCHAIN_FILE=wasm-toolchain.cmake Once the CMake configurations are built, the following command will trigger compilation of the Wasm binary: cmake --build build |
Injecting Wasm into TKET¶
To inject Wasm into a TKET program and enable processing on the H-Series devices or emulators, three components must be incorporated in the workflow.
pytket.wasm.WasmFileHandler: Enables addition of the Wasm module you create to the TKET program. Additionally, checks Wasm binary is compatible with Wasm standards (number of input parameters and input/ output parameter types) via a boolean kwarg,
check_file
.pytket.circuit.Circuit.add_wasm_to_reg: Adds the linked Wasm to the TKET program as a classical operation to act across specified registers.
pytket.circuit.Circuit.add_wasm: Adds the linked Wasm to the TKET program as a classical operation to act on specific bits.
wasm_file_handler kwarg on
QuantinuumBackend.process_circuits
: The linked Wasm module must be specified during job submission to H-Series.
Program Requirements¶
There are specific constraints that must be satisfied before Wasm binaries can be submitted for execution to the real-time hybrid-compute environment. The table below specifies these constraints in more detail.
Requirements | Description |
---|---|
Program Size | The combined TKET + Wasm program should be of size ~1.3 MBs or less. There is a hard limit of 6 MBs for programs submitted to H-Series. |
C Source | Standard C library not supported (WASI breaks WAVM). |
Rust Source | Rust standard library is supported. |
Input Arguments |
|
Method Signatures |
|
Mandatory Functions |
|
TKET Restrictions |
|
Other Restrictions |
|
Estimating Program Size¶
The guidance provided by Quantinuum is to limit program size to c1.3 MBs. The hard limit on program size is 6 MBs, otherwise the job will fail. For real-time hybrid compute jobs involving both quantum circuits and Wasm binaries, the program size must be estimated using the methods defined below.
The estimate_wasm_size
method accepts a pathlib.Path
object to the Wasm binary on local disk. The os
module is used to estimate the size of the Wasm binary in MBs.
import os
import pathlib
wasm_path = pathlib.Path("surface_code/c/surface_code.wasm")
wasm_path.is_file()
def estimate_wasm_size(
path: pathlib.Path
) -> int:
r"""Returns the size of the inputted file in MBs.
:param path: path to Wasm binary on local disk
:param_type: pathlib.Path
:returns: int
"""
return os.path.getsize(path) / 1024**2
To estimate the size of a TKET circuit, the circuit_to_qasm_str
must be used to convert the circuit into a qasm string. The sys
module can be used to estimate the size of the of the QASM string in MBs.
import sys
from pytket.circuit import Circuit
from pytket.qasm import circuit_to_qasm_str
def estimate_circuit_size(
circuit: Circuit
) -> int:
qasm_str = circuit_to_qasm_str(circuit, header="hqslib1")
return sys.getsizeof(qasm_str) / 1024**2
Asynchronous Wasm¶
Asynchronous (asnc) Wasm can help minimize qubit idle-time. Async Wasm involves void calls during job execution and integer call when quantum correction is required. The schematic below shows four circuit primitives: Initialization, 2 Syndrome Extraction blocks and Actions dependent on Wasm result. Void Wasm calls are non-blocking, they call a Wasm function without blocking execution of the quantum circuit. Each void call updates an in-memory Wasm variable. This can also be referred to as a Wasm register. Successive void calls update the value of the in-memory Wasm variable. when corrections are required, an integer Wasm function is called to write the in-memory Wasm variable to a classical register on the TKET circuit. The TKET classical register is then used to conditionally perform gate operations on the qubit registers. It is best practice to reset the in-memory Wasm variable on a per-shot basis. During implementation of decoder, the in-memory Wasm variables are defined as global variables in C or Rust source.
Note
In-memory Wasm variables are automatically reset on a per-chunk basis. Users are advised to reset on a per-shot basis by using a void Wasm call at the end of a circuit.
FAQS¶
Is this a library of blackbox QEC decoders?
No, this is a server-side capability that runs user-defined classical decoders during real-time execution of a quantum circuit. The user is responsible for defining decoders and their workflow.
Why is Wasm useful for my use case?
WebAssembly enables the execution of classical logic during the real-time execution of a quantum circuit. This is useful for QEC workflows with look-up and algorithmic decoders. The quantum circuit can apply Wasm on a specific classical register to perform processing on the register value. The output of the Wasm function can be stored in the same register, or passed to new register.
Why is this release relevant as Quantinuum upgrades its hardware?
Practical QEC schemes require real-time hybrid computation. This new capability is the first step to achieving this on H-Series hardware.
Why can I not use a higher-level programming language, such as Julia?
There are two requirement: (1) the higher-level language is has support for Wasm compilation, and (2) the compiled Wasm must be fast and execute in ~2 milliseconds. This is best achieved with low-level languages like C and Rust. The main issue with Julia, Python, etc. is that they are either interpreted or have garbage collection. Neither is suitable for fast, deterministic execution within qubit coherence time.
Is it possible for me to submit arbitrary Wasm to H-Series?
No, it is not possible to submit arbitrary Wasm to H-Series. Each Wasm function must satisfy a Wasm standard. Each function is either a void function or a 32-bit integer function. For 32-bit integer Wasm function, the function must return only one 32-bit integer. The Wasm function can only accept (multiple) 32-bit integer arguments, or no arguments.
WAVM is an isolated memory-safe environment and communicates via network calls with the H-Series chip. This help alleviate the danger of end users submitting customized Wasm binaries.
How do I prevent qubits from idling and protect my program from memory error when using Wasm?
For Wasm calls during circuit execution, it is recommended to use async Wasm calls. This is the use of void Wasm call to process the value of a register and to store some desired value as an in-memory Wasm variable. After the circuit, or subcircuit, has completed execution, the in-memory Wasm variable is retrieved using an integer function and applied to the specified classical register. For in-memory Wasm variables, a Wasm call is required at the end of each shot to restore the in-memory Wasm variable to a null value.
Can I also use Wasm with the H-Series emulator?
Yes, Wasm is available for use on both the H-Series hardware and emulator.
Can I use parallelization in the WAVM?
No, parallelization on a single core and across multiple cores is not supported.
Troubleshooting¶
There are known issues that cause failure during execution of the submitted Wasm binary. These issues are detailed below with the cause of failure, potential resolutions and the error code/ message.
Wasm program timeout
Use Case: A user’s QASM program has many void Wasm calls (no output expected) eventually followed by a non-void Wasm call (one output expected).
Failure: If multiple void requests are made in a short interval and then a non-void request is made, the summation of execution time for all the voids + non-void could exceed the RTE timeout of 250ms. The RTE is waiting for a response from the non-void request. An error of type
Response 0: error: 0xFFFFFFE6 CCE receive timed out - file: rtexec.c line: xxxx
, can send the system into maintenance mode without failing the job, requiring the job to be manually paused by operators.Resolution: Run the QASM on a simulator or try to determine the number of Wasm function calls from the QASM and adjust the order is possible.
Error Code: 3002
Error Message:
WASM error: WASM failed during run-time. Execution time of function '<function name>' exceeded maximum <timeout value> msec
Large Wasm binary with large number of exported functions.
Use Case: A user codes a source program to be converted to Wasm. Source could be any high-level language that supports Wasm output. The source program contains every possible Wasm function the user creates, whether or not used by a TKET program and marks all the functions to be exported. Any of the User’s TKET programs use this single Wasm binary.
Failure: If the time taken to load the large Wasm binary into memory exceeds a threshold, then a Wasm LOAD FAILURE will be reported. Loading Wasm into memory requires many steps, but the time consuming step is to compile each exported Wasm function. Note: The number of exported functions in a Wasm binary is not necessarily the cause of the LOAD timeout, but rather the time it takes to compile all exported functions. 1000 exported functions could compile within the timeout whereas 10 very large exported functions could exceed the timeout.
Resolution: Keep Wasm binaries to a small size with only the required Wasm functions needed for a particular TKET program.
Error Code: 3002
Error Message:
WASM error: WASM failed during binary load. <appended CCE message>
A list of the possible “messages” that could be substituted in for
: Failed to load wasm binary to module
Wasm binary does not export function: <function name>
No wasm functions are exported
Failed wasm_request for init()
WASM function causes WAVM Exception.
Use Case: A user codes a high level source program and converts to Wasm. A source function does memory management (alloc, free, etc). The program may not have been unit tested.
Failure: A Wasm function uses a pthread as opposed to a main thread. When this function is executed, it may pass the first time and then fail the next time. In these cases, the TKET+Wasm job is either Paused or Failed. This failure can be seen in two ways:
either a WAVM Exception is caught (e.g. Div/0);
SEGV (Segmentation Violation) occurs in the pthread executing the Wasm function.
Resolution: User must unit test their Wasm functions as much as possible. Quantinuum can also provide debugging by obtaining the TKET and Wasm from the submitted job. Note: C with pthreads have known issues with Exception handling when using 3rd party libraries (e.g. WAVM Lib). As from above, some WAVM Exceptions can be caught by the pthread while others are not, but the SEGV can be caught.
Error Code: 3002
Error Message:
WASM error: WASM failed during run-time. Execution time of function '<function name>' exceeded maximum <timeout value> msec
A list of the possible WAVM exceptions that can be caught:
wavm.outOfBoundsMemoryAccess
wavm.outOfBoundsTableAccess
wavm.outOfBoundsDataSegmentAccess
wavm.stackOverflow
wavm.integerDivideByZeroOrOverflow
wavm.invalidFloatOperation
wavm.invokeSignatureMismatch
wavm.reachedUnreachable
wavm.indirectCallSignatureMismatch
wavm.uninitializedTableElement
wavm.calledAbort
wavm.calledUnimplementedIntrinsic
wavm.outOfMemory
wavm.misalignedAtomicMemoryAccess
wavm.waitOnUnsharedMemory
wavm.invalidArgument
Use Cases¶
Three use cases are provided to demonstrate application of the QEC Decoder Toolkit via the real-time hybrid-compute environment. Each subdirectory contains the relevant source-code to unit test and compile Wasm binaries.
Repeat Until Success: Conditionally adding quantum operations to a circuit based on equality comparisons with an in-memory Wasm variable.
Repetition Code: \([[3, 1, 2]]\) code to encode 1 logical qubit into 3 physical qubits with code distance 2.
Surface Code: \([[9, 1, 3]]\) code to encode 1 logical qubit into 9 physical qubits with code distance 3.
Repeat Until Success¶
This use case conditionally adds quantum operations to a circuit based on equality comparisons with an in-memory Wasm variable. Only integer Wasm calls are used. The C and Rust source, in addition to build scripts, can be found here.
A 2-qubit circuit is initialized. Subsequently, the add_c_register
is used to add 4 classical registers. The first argument defines the name of the classical register. The second argument defines the size of the classical register. The following classical registers are added manually:
creg0
with 1 bitcreg1
with 1 bitcond
with 32 bitscount
with 32 bits
from pytket.circuit import Circuit, Qubit
from pytket.circuit.logic_exp import reg_lt
circuit = Circuit(2, name=f"RUS Circuit")
# Add classical registers
creg0 = circuit.add_c_register("creg0", 1)
creg1 = circuit.add_c_register("creg1", 1)
cond = circuit.add_c_register("cond", 32)
count = circuit.add_c_register("count", 32)
The pytket.wasm.WasmFileHandler
instance is used to link to the Wasm module.
from pathlib import Path
from pytket.wasm import WasmFileHandler
wasm_file_path = Path("repeat_until_success/c/rus.wasm")
wfh = WasmFileHandler(wasm_file_path, check_file=True)
print(repr(wfh))
Functions in wasm file with the uid 46d4ce3c80fca04f23fd6472d634177481be814da9bea23d0553a5de862bdfc5:
function '__wasm_call_ctors' with 0 i32 parameter(s) and 0 i32 return value(s)
function 'init' with 0 i32 parameter(s) and 0 i32 return value(s)
function 'add_count' with 2 i32 parameter(s) and 1 i32 return value(s)
function 'reset_total_meas' with 0 i32 parameter(s) and 0 i32 return value(s)
The linked Wasm module is added to pytket.circuit.Circuit
instance using add_wasm_to_reg
. The Wasm method add_count
is called and the bit registers creg1
and count
are supplied. The output of add_count
is written to cond
. All the operations in this block are conditional operations on the decimal value of the bit register cond
.
n_repetitions = 5
cond_execute = 3
for loop_iter in range(1, n_repetitions + 1):
circuit.H(0, condition=reg_lt(cond, cond_execute))
circuit.CX(0, 1, condition=reg_lt(cond, cond_execute))
circuit.Measure(Qubit(1), creg1[0], condition=reg_lt(cond, cond_execute))
# Call Wasm
circuit.add_wasm_to_reg(
"add_count", wfh, [creg1, count], [cond], condition=reg_lt(cond, cond_execute)
)
circuit.add_c_setreg(loop_iter, count, condition=reg_lt(cond, cond_execute))
circuit.Reset(0, condition=reg_lt(cond, cond_execute))
circuit.Measure(Qubit(0), creg0[0])
circuit.Measure(Qubit(1), creg1[0])
[RangePredicate([0,2]) cond[0], cond[1], cond[2], cond[3], cond[4], cond[5], cond[6], cond[7], cond[8], cond[9], cond[10], cond[11], cond[12], cond[13], cond[14], cond[15], cond[16], cond[17], cond[18], cond[19], cond[20], cond[21], cond[22], cond[23], cond[24], cond[25], cond[26], cond[27], cond[28], cond[29], cond[30], cond[31], tk_SCRATCH_BIT[0]; RangePredicate([0,2]) cond[0], cond[1], cond[2], cond[3], cond[4], cond[5], cond[6], cond[7], cond[8], cond[9], cond[10], cond[11], cond[12], cond[13], cond[14], cond[15], cond[16], cond[17], cond[18], cond[19], cond[20], cond[21], cond[22], cond[23], cond[24], cond[25], cond[26], cond[27], cond[28], cond[29], cond[30], cond[31], tk_SCRATCH_BIT[1]; RangePredicate([0,2]) cond[0], cond[1], cond[2], cond[3], cond[4], cond[5], cond[6], cond[7], cond[8], cond[9], cond[10], cond[11], cond[12], cond[13], cond[14], cond[15], cond[16], cond[17], cond[18], cond[19], cond[20], cond[21], cond[22], cond[23], cond[24], cond[25], cond[26], cond[27], cond[28], cond[29], cond[30], cond[31], tk_SCRATCH_BIT[2]; RangePredicate([0,2]) cond[0], cond[1], cond[2], cond[3], cond[4], cond[5], cond[6], cond[7], cond[8], cond[9], cond[10], cond[11], cond[12], cond[13], cond[14], cond[15], cond[16], cond[17], cond[18], cond[19], cond[20], cond[21], cond[22], cond[23], cond[24], cond[25], cond[26], cond[27], cond[28], cond[29], cond[30], cond[31], tk_SCRATCH_BIT[3]; IF ([tk_SCRATCH_BIT[0]] == 1) THEN H q[0]; IF ([tk_SCRATCH_BIT[1]] == 1) THEN CX q[0], q[1]; IF ([tk_SCRATCH_BIT[2]] == 1) THEN Measure q[1] --> creg1[0]; IF ([tk_SCRATCH_BIT[3]] == 1) THEN WASM creg1[0], count[0], count[1], count[2], count[3], count[4], count[5], count[6], count[7], count[8], count[9], count[10], count[11], count[12], count[13], count[14], count[15], count[16], count[17], count[18], count[19], count[20], count[21], count[22], count[23], count[24], count[25], count[26], count[27], count[28], count[29], count[30], count[31], cond[0], cond[1], cond[2], cond[3], cond[4], cond[5], cond[6], cond[7], cond[8], cond[9], cond[10], cond[11], cond[12], cond[13], cond[14], cond[15], cond[16], cond[17], cond[18], cond[19], cond[20], cond[21], cond[22], cond[23], cond[24], cond[25], cond[26], cond[27], cond[28], cond[29], cond[30], cond[31], _w[0]; RangePredicate([0,2]) cond[0], cond[1], cond[2], cond[3], cond[4], cond[5], cond[6], cond[7], cond[8], cond[9], cond[10], cond[11], cond[12], cond[13], cond[14], cond[15], cond[16], cond[17], cond[18], cond[19], cond[20], cond[21], cond[22], cond[23], cond[24], cond[25], cond[26], cond[27], cond[28], cond[29], cond[30], cond[31], tk_SCRATCH_BIT[4]; RangePredicate([0,2]) cond[0], cond[1], cond[2], cond[3], cond[4], cond[5], cond[6], cond[7], cond[8], cond[9], cond[10], cond[11], cond[12], cond[13], cond[14], cond[15], cond[16], cond[17], cond[18], cond[19], cond[20], cond[21], cond[22], cond[23], cond[24], cond[25], cond[26], cond[27], cond[28], cond[29], cond[30], cond[31], tk_SCRATCH_BIT[5]; RangePredicate([0,2]) cond[0], cond[1], cond[2], cond[3], cond[4], cond[5], cond[6], cond[7], cond[8], cond[9], cond[10], cond[11], cond[12], cond[13], cond[14], cond[15], cond[16], cond[17], cond[18], cond[19], cond[20], cond[21], cond[22], cond[23], cond[24], cond[25], cond[26], cond[27], cond[28], cond[29], cond[30], cond[31], tk_SCRATCH_BIT[6]; RangePredicate([0,2]) cond[0], cond[1], cond[2], cond[3], cond[4], cond[5], cond[6], cond[7], cond[8], cond[9], cond[10], cond[11], cond[12], cond[13], cond[14], cond[15], cond[16], cond[17], cond[18], cond[19], cond[20], cond[21], cond[22], cond[23], cond[24], cond[25], cond[26], cond[27], cond[28], cond[29], cond[30], cond[31], tk_SCRATCH_BIT[7]; RangePredicate([0,2]) cond[0], cond[1], cond[2], cond[3], cond[4], cond[5], cond[6], cond[7], cond[8], cond[9], cond[10], cond[11], cond[12], cond[13], cond[14], cond[15], cond[16], cond[17], cond[18], cond[19], cond[20], cond[21], cond[22], cond[23], cond[24], cond[25], cond[26], cond[27], cond[28], cond[29], cond[30], cond[31], tk_SCRATCH_BIT[8]; RangePredicate([0,2]) cond[0], cond[1], cond[2], cond[3], cond[4], cond[5], cond[6], cond[7], cond[8], cond[9], cond[10], cond[11], cond[12], cond[13], cond[14], cond[15], cond[16], cond[17], cond[18], cond[19], cond[20], cond[21], cond[22], cond[23], cond[24], cond[25], cond[26], cond[27], cond[28], cond[29], cond[30], cond[31], tk_SCRATCH_BIT[9]; IF ([tk_SCRATCH_BIT[4]] == 1) THEN SetBits(10000000000000000000000000000000) count[0], count[1], count[2], count[3], count[4], count[5], count[6], count[7], count[8], count[9], count[10], count[11], count[12], count[13], count[14], count[15], count[16], count[17], count[18], count[19], count[20], count[21], count[22], count[23], count[24], count[25], count[26], count[27], count[28], count[29], count[30], count[31]; IF ([tk_SCRATCH_BIT[5]] == 1) THEN Reset q[0]; IF ([tk_SCRATCH_BIT[6]] == 1) THEN H q[0]; IF ([tk_SCRATCH_BIT[7]] == 1) THEN CX q[0], q[1]; IF ([tk_SCRATCH_BIT[8]] == 1) THEN Measure q[1] --> creg1[0]; IF ([tk_SCRATCH_BIT[9]] == 1) THEN WASM creg1[0], count[0], count[1], count[2], count[3], count[4], count[5], count[6], count[7], count[8], count[9], count[10], count[11], count[12], count[13], count[14], count[15], count[16], count[17], count[18], count[19], count[20], count[21], count[22], count[23], count[24], count[25], count[26], count[27], count[28], count[29], count[30], count[31], cond[0], cond[1], cond[2], cond[3], cond[4], cond[5], cond[6], cond[7], cond[8], cond[9], cond[10], cond[11], cond[12], cond[13], cond[14], cond[15], cond[16], cond[17], cond[18], cond[19], cond[20], cond[21], cond[22], cond[23], cond[24], cond[25], cond[26], cond[27], cond[28], cond[29], cond[30], cond[31], _w[0]; RangePredicate([0,2]) cond[0], cond[1], cond[2], cond[3], cond[4], cond[5], cond[6], cond[7], cond[8], cond[9], cond[10], cond[11], cond[12], cond[13], cond[14], cond[15], cond[16], cond[17], cond[18], cond[19], cond[20], cond[21], cond[22], cond[23], cond[24], cond[25], cond[26], cond[27], cond[28], cond[29], cond[30], cond[31], tk_SCRATCH_BIT[10]; RangePredicate([0,2]) cond[0], cond[1], cond[2], cond[3], cond[4], cond[5], cond[6], cond[7], cond[8], cond[9], cond[10], cond[11], cond[12], cond[13], cond[14], cond[15], cond[16], cond[17], cond[18], cond[19], cond[20], cond[21], cond[22], cond[23], cond[24], cond[25], cond[26], cond[27], cond[28], cond[29], cond[30], cond[31], tk_SCRATCH_BIT[11]; RangePredicate([0,2]) cond[0], cond[1], cond[2], cond[3], cond[4], cond[5], cond[6], cond[7], cond[8], cond[9], cond[10], cond[11], cond[12], cond[13], cond[14], cond[15], cond[16], cond[17], cond[18], cond[19], cond[20], cond[21], cond[22], cond[23], cond[24], cond[25], cond[26], cond[27], cond[28], cond[29], cond[30], cond[31], tk_SCRATCH_BIT[12]; RangePredicate([0,2]) cond[0], cond[1], cond[2], cond[3], cond[4], cond[5], cond[6], cond[7], cond[8], cond[9], cond[10], cond[11], cond[12], cond[13], cond[14], cond[15], cond[16], cond[17], cond[18], cond[19], cond[20], cond[21], cond[22], cond[23], cond[24], cond[25], cond[26], cond[27], cond[28], cond[29], cond[30], cond[31], tk_SCRATCH_BIT[13]; RangePredicate([0,2]) cond[0], cond[1], cond[2], cond[3], cond[4], cond[5], cond[6], cond[7], cond[8], cond[9], cond[10], cond[11], cond[12], cond[13], cond[14], cond[15], cond[16], cond[17], cond[18], cond[19], cond[20], cond[21], cond[22], cond[23], cond[24], cond[25], cond[26], cond[27], cond[28], cond[29], cond[30], cond[31], tk_SCRATCH_BIT[14]; RangePredicate([0,2]) cond[0], cond[1], cond[2], cond[3], cond[4], cond[5], cond[6], cond[7], cond[8], cond[9], cond[10], cond[11], cond[12], cond[13], cond[14], cond[15], cond[16], cond[17], cond[18], cond[19], cond[20], cond[21], cond[22], cond[23], cond[24], cond[25], cond[26], cond[27], cond[28], cond[29], cond[30], cond[31], tk_SCRATCH_BIT[15]; IF ([tk_SCRATCH_BIT[10]] == 1) THEN SetBits(01000000000000000000000000000000) count[0], count[1], count[2], count[3], count[4], count[5], count[6], count[7], count[8], count[9], count[10], count[11], count[12], count[13], count[14], count[15], count[16], count[17], count[18], count[19], count[20], count[21], count[22], count[23], count[24], count[25], count[26], count[27], count[28], count[29], count[30], count[31]; IF ([tk_SCRATCH_BIT[11]] == 1) THEN Reset q[0]; IF ([tk_SCRATCH_BIT[12]] == 1) THEN H q[0]; IF ([tk_SCRATCH_BIT[13]] == 1) THEN CX q[0], q[1]; IF ([tk_SCRATCH_BIT[14]] == 1) THEN Measure q[1] --> creg1[0]; IF ([tk_SCRATCH_BIT[15]] == 1) THEN WASM creg1[0], count[0], count[1], count[2], count[3], count[4], count[5], count[6], count[7], count[8], count[9], count[10], count[11], count[12], count[13], count[14], count[15], count[16], count[17], count[18], count[19], count[20], count[21], count[22], count[23], count[24], count[25], count[26], count[27], count[28], count[29], count[30], count[31], cond[0], cond[1], cond[2], cond[3], cond[4], cond[5], cond[6], cond[7], cond[8], cond[9], cond[10], cond[11], cond[12], cond[13], cond[14], cond[15], cond[16], cond[17], cond[18], cond[19], cond[20], cond[21], cond[22], cond[23], cond[24], cond[25], cond[26], cond[27], cond[28], cond[29], cond[30], cond[31], _w[0]; RangePredicate([0,2]) cond[0], cond[1], cond[2], cond[3], cond[4], cond[5], cond[6], cond[7], cond[8], cond[9], cond[10], cond[11], cond[12], cond[13], cond[14], cond[15], cond[16], cond[17], cond[18], cond[19], cond[20], cond[21], cond[22], cond[23], cond[24], cond[25], cond[26], cond[27], cond[28], cond[29], cond[30], cond[31], tk_SCRATCH_BIT[16]; RangePredicate([0,2]) cond[0], cond[1], cond[2], cond[3], cond[4], cond[5], cond[6], cond[7], cond[8], cond[9], cond[10], cond[11], cond[12], cond[13], cond[14], cond[15], cond[16], cond[17], cond[18], cond[19], cond[20], cond[21], cond[22], cond[23], cond[24], cond[25], cond[26], cond[27], cond[28], cond[29], cond[30], cond[31], tk_SCRATCH_BIT[17]; RangePredicate([0,2]) cond[0], cond[1], cond[2], cond[3], cond[4], cond[5], cond[6], cond[7], cond[8], cond[9], cond[10], cond[11], cond[12], cond[13], cond[14], cond[15], cond[16], cond[17], cond[18], cond[19], cond[20], cond[21], cond[22], cond[23], cond[24], cond[25], cond[26], cond[27], cond[28], cond[29], cond[30], cond[31], tk_SCRATCH_BIT[18]; RangePredicate([0,2]) cond[0], cond[1], cond[2], cond[3], cond[4], cond[5], cond[6], cond[7], cond[8], cond[9], cond[10], cond[11], cond[12], cond[13], cond[14], cond[15], cond[16], cond[17], cond[18], cond[19], cond[20], cond[21], cond[22], cond[23], cond[24], cond[25], cond[26], cond[27], cond[28], cond[29], cond[30], cond[31], tk_SCRATCH_BIT[19]; RangePredicate([0,2]) cond[0], cond[1], cond[2], cond[3], cond[4], cond[5], cond[6], cond[7], cond[8], cond[9], cond[10], cond[11], cond[12], cond[13], cond[14], cond[15], cond[16], cond[17], cond[18], cond[19], cond[20], cond[21], cond[22], cond[23], cond[24], cond[25], cond[26], cond[27], cond[28], cond[29], cond[30], cond[31], tk_SCRATCH_BIT[20]; RangePredicate([0,2]) cond[0], cond[1], cond[2], cond[3], cond[4], cond[5], cond[6], cond[7], cond[8], cond[9], cond[10], cond[11], cond[12], cond[13], cond[14], cond[15], cond[16], cond[17], cond[18], cond[19], cond[20], cond[21], cond[22], cond[23], cond[24], cond[25], cond[26], cond[27], cond[28], cond[29], cond[30], cond[31], tk_SCRATCH_BIT[21]; IF ([tk_SCRATCH_BIT[16]] == 1) THEN SetBits(11000000000000000000000000000000) count[0], count[1], count[2], count[3], count[4], count[5], count[6], count[7], count[8], count[9], count[10], count[11], count[12], count[13], count[14], count[15], count[16], count[17], count[18], count[19], count[20], count[21], count[22], count[23], count[24], count[25], count[26], count[27], count[28], count[29], count[30], count[31]; IF ([tk_SCRATCH_BIT[17]] == 1) THEN Reset q[0]; IF ([tk_SCRATCH_BIT[18]] == 1) THEN H q[0]; IF ([tk_SCRATCH_BIT[19]] == 1) THEN CX q[0], q[1]; IF ([tk_SCRATCH_BIT[20]] == 1) THEN Measure q[1] --> creg1[0]; IF ([tk_SCRATCH_BIT[21]] == 1) THEN WASM creg1[0], count[0], count[1], count[2], count[3], count[4], count[5], count[6], count[7], count[8], count[9], count[10], count[11], count[12], count[13], count[14], count[15], count[16], count[17], count[18], count[19], count[20], count[21], count[22], count[23], count[24], count[25], count[26], count[27], count[28], count[29], count[30], count[31], cond[0], cond[1], cond[2], cond[3], cond[4], cond[5], cond[6], cond[7], cond[8], cond[9], cond[10], cond[11], cond[12], cond[13], cond[14], cond[15], cond[16], cond[17], cond[18], cond[19], cond[20], cond[21], cond[22], cond[23], cond[24], cond[25], cond[26], cond[27], cond[28], cond[29], cond[30], cond[31], _w[0]; RangePredicate([0,2]) cond[0], cond[1], cond[2], cond[3], cond[4], cond[5], cond[6], cond[7], cond[8], cond[9], cond[10], cond[11], cond[12], cond[13], cond[14], cond[15], cond[16], cond[17], cond[18], cond[19], cond[20], cond[21], cond[22], cond[23], cond[24], cond[25], cond[26], cond[27], cond[28], cond[29], cond[30], cond[31], tk_SCRATCH_BIT[22]; RangePredicate([0,2]) cond[0], cond[1], cond[2], cond[3], cond[4], cond[5], cond[6], cond[7], cond[8], cond[9], cond[10], cond[11], cond[12], cond[13], cond[14], cond[15], cond[16], cond[17], cond[18], cond[19], cond[20], cond[21], cond[22], cond[23], cond[24], cond[25], cond[26], cond[27], cond[28], cond[29], cond[30], cond[31], tk_SCRATCH_BIT[23]; RangePredicate([0,2]) cond[0], cond[1], cond[2], cond[3], cond[4], cond[5], cond[6], cond[7], cond[8], cond[9], cond[10], cond[11], cond[12], cond[13], cond[14], cond[15], cond[16], cond[17], cond[18], cond[19], cond[20], cond[21], cond[22], cond[23], cond[24], cond[25], cond[26], cond[27], cond[28], cond[29], cond[30], cond[31], tk_SCRATCH_BIT[24]; RangePredicate([0,2]) cond[0], cond[1], cond[2], cond[3], cond[4], cond[5], cond[6], cond[7], cond[8], cond[9], cond[10], cond[11], cond[12], cond[13], cond[14], cond[15], cond[16], cond[17], cond[18], cond[19], cond[20], cond[21], cond[22], cond[23], cond[24], cond[25], cond[26], cond[27], cond[28], cond[29], cond[30], cond[31], tk_SCRATCH_BIT[25]; RangePredicate([0,2]) cond[0], cond[1], cond[2], cond[3], cond[4], cond[5], cond[6], cond[7], cond[8], cond[9], cond[10], cond[11], cond[12], cond[13], cond[14], cond[15], cond[16], cond[17], cond[18], cond[19], cond[20], cond[21], cond[22], cond[23], cond[24], cond[25], cond[26], cond[27], cond[28], cond[29], cond[30], cond[31], tk_SCRATCH_BIT[26]; RangePredicate([0,2]) cond[0], cond[1], cond[2], cond[3], cond[4], cond[5], cond[6], cond[7], cond[8], cond[9], cond[10], cond[11], cond[12], cond[13], cond[14], cond[15], cond[16], cond[17], cond[18], cond[19], cond[20], cond[21], cond[22], cond[23], cond[24], cond[25], cond[26], cond[27], cond[28], cond[29], cond[30], cond[31], tk_SCRATCH_BIT[27]; IF ([tk_SCRATCH_BIT[22]] == 1) THEN SetBits(00100000000000000000000000000000) count[0], count[1], count[2], count[3], count[4], count[5], count[6], count[7], count[8], count[9], count[10], count[11], count[12], count[13], count[14], count[15], count[16], count[17], count[18], count[19], count[20], count[21], count[22], count[23], count[24], count[25], count[26], count[27], count[28], count[29], count[30], count[31]; IF ([tk_SCRATCH_BIT[23]] == 1) THEN Reset q[0]; IF ([tk_SCRATCH_BIT[24]] == 1) THEN H q[0]; IF ([tk_SCRATCH_BIT[25]] == 1) THEN CX q[0], q[1]; IF ([tk_SCRATCH_BIT[26]] == 1) THEN Measure q[1] --> creg1[0]; IF ([tk_SCRATCH_BIT[27]] == 1) THEN WASM creg1[0], count[0], count[1], count[2], count[3], count[4], count[5], count[6], count[7], count[8], count[9], count[10], count[11], count[12], count[13], count[14], count[15], count[16], count[17], count[18], count[19], count[20], count[21], count[22], count[23], count[24], count[25], count[26], count[27], count[28], count[29], count[30], count[31], cond[0], cond[1], cond[2], cond[3], cond[4], cond[5], cond[6], cond[7], cond[8], cond[9], cond[10], cond[11], cond[12], cond[13], cond[14], cond[15], cond[16], cond[17], cond[18], cond[19], cond[20], cond[21], cond[22], cond[23], cond[24], cond[25], cond[26], cond[27], cond[28], cond[29], cond[30], cond[31], _w[0]; Measure q[1] --> creg1[0]; RangePredicate([0,2]) cond[0], cond[1], cond[2], cond[3], cond[4], cond[5], cond[6], cond[7], cond[8], cond[9], cond[10], cond[11], cond[12], cond[13], cond[14], cond[15], cond[16], cond[17], cond[18], cond[19], cond[20], cond[21], cond[22], cond[23], cond[24], cond[25], cond[26], cond[27], cond[28], cond[29], cond[30], cond[31], tk_SCRATCH_BIT[28]; RangePredicate([0,2]) cond[0], cond[1], cond[2], cond[3], cond[4], cond[5], cond[6], cond[7], cond[8], cond[9], cond[10], cond[11], cond[12], cond[13], cond[14], cond[15], cond[16], cond[17], cond[18], cond[19], cond[20], cond[21], cond[22], cond[23], cond[24], cond[25], cond[26], cond[27], cond[28], cond[29], cond[30], cond[31], tk_SCRATCH_BIT[29]; IF ([tk_SCRATCH_BIT[28]] == 1) THEN SetBits(10100000000000000000000000000000) count[0], count[1], count[2], count[3], count[4], count[5], count[6], count[7], count[8], count[9], count[10], count[11], count[12], count[13], count[14], count[15], count[16], count[17], count[18], count[19], count[20], count[21], count[22], count[23], count[24], count[25], count[26], count[27], count[28], count[29], count[30], count[31]; IF ([tk_SCRATCH_BIT[29]] == 1) THEN Reset q[0]; Measure q[0] --> creg0[0]; ]
A QuantinuumBackend
instance is instantiated with the backend target specified as a H-Series emulator, H2-1E
.
from pytket.extensions.quantinuum import QuantinuumBackend
backend = QuantinuumBackend("H2-1E")
backend.login();
The RUS circuit is compiled to the H-Series gate set.
compiled_circuit = backend.get_compiled_circuit(circuit, optimisation_level=0)
from pytket.circuit.display import render_circuit_jupyter
render_circuit_jupyter(compiled_circuit)
The combined program size can be estimated below.
program_size = estimate_circuit_size(compiled_circuit) + estimate_wasm_size(wasm_file_path)
print(f"{program_size} MBs")
0.0029287338256835938 MBs
The RUS circuit is submitted to the H-Series emulator using process_circuits
. The wasm_file_handler
keyword argument is used to submit the linked Wasm module to H-Series as well.
handle = backend.process_circuit(compiled_circuit, n_shots=100, wasm_file_handler=wfh)
The status of the circuit can be queried with the circuit_status
method.
status = backend.circuit_status(handle)
print(status)
print(status.message)
CircuitStatus(status=<StatusEnum.QUEUED: 'Circuit is queued.'>, message='{"name": "RUS Circuit", "submit-date": "2024-10-28T05:07:29.594466", "result-date": null, "queue-position": null, "cost": null, "error": null}', error_detail=None, completed_time=None, queued_time=None, submitted_time=None, running_time=None, cancelled_time=None, error_time=None, queue_position=None)
{"name": "RUS Circuit", "submit-date": "2024-10-28T05:07:29.594466", "result-date": null, "queue-position": null, "cost": null, "error": null}
Once the job result is ready get_result
can be used to retrieve the data.
result = backend.get_result(handle)
distribution = result.get_counts(creg1)
logical_fidelity = distribution.get((0,)) / sum(distribution.values())
print(logical_fidelity)
0.1
Repetition Code¶
\([[3, 1, 2]]\) code to encode 1 logical qubit into 3 physical qubits with code distance 2. Integer Wasm calls are used to perform decoding via a look-up table. Void Wasm calls are not in this use case. The C and Rust source, in addition to the build scripts, can be found here.
First, the physical qubit register is initialized in the \(|000\rangle\) state encoding the logical \(|0\rangle\) state. One ancilla qubit is used to perform two syndrome measurements:
\(\hat{Z}_{q[0]} \hat{Z}_{q[1]} \hat{I}_{q[2]}\)
\(\hat{I}_{q[0]} \hat{Z}_{q[1]} \hat{Z}_{q[2]}\)
Subsequently, classically-conditioned operations are used to correct any errors on the physical qubits using the syndrome measurement results. Finally, direct measurements on the physical qubits are performed to verify the final state of the logical qubit is \(|0\rangle\). After each syndrome extraction, Wasm functions are used to determine which qubits are to be corrected.
from pathlib import Path
from pytket.wasm import WasmFileHandler
wasm_file_path = Path().cwd().joinpath("repetition_code") / "c" / "repetition_code.wasm"
wasm_file_handler = WasmFileHandler(wasm_file_path, check_file=True)
wasm_file_handler
Functions in wasm file with the uid 97b45de1e5402963424bf1688b7a41c91a33d984a9c1344892b92379fc1387cc:
function '__wasm_call_ctors' with 0 i32 parameter(s) and 0 i32 return value(s)
function 'init' with 0 i32 parameter(s) and 0 i32 return value(s)
function 'decode3' with 1 i32 parameter(s) and 1 i32 return value(s)
function 'reset_syn_old' with 0 i32 parameter(s) and 0 i32 return value(s)
from pytket.circuit import Circuit, CircBox
def get_syndrome_extraction_box() -> CircBox:
r"""This generates a syndrome extraction
primitive with two data qubits and one
ancilla qubit. The first qubit, q[0],
is the ancilla qubit. q[1] and q[2] are
data qubits.
:returns: CircBox
"""
circuit = Circuit(3, 1)
circuit.name = "syndrome_extraction"
circuit.CX(1, 0)
circuit.CX(2, 0)
circuit.Measure(0, 0)
circuit.Reset(0)
return CircBox(circuit)
syndrome_extraction = get_syndrome_extraction_box()
The Pauli-\(X\) gate is applied on the circuit based on the value of a classical register. This value is computed by a Wasm function. This CircBox
primitive does not contain any Wasm operations, since Wasm operations must be added directly to the main circuit. Only the correction to the qubits is defined below.
def get_feedforward_box() -> CircBox:
circuit = Circuit(3)
circuit.name = "Conditional Not"
creg = circuit.add_c_register("creg", 3)
for i in range(3):
circuit.X(circuit.qubits[i], condition_bits=[creg[i]], condition_value=1)
return CircBox(circuit)
feedforward_box = get_feedforward_box()
Classical expressions can be applied to classical bits using the pytket.circuit.logic_exp
submodule. These expressions perform boolean operations on classical bit registers.
The QubitRegister
and BitRegister
objects are used to allocate qubits and bits to the pytket.circuit.Circuit
instance. The circuit contains two quantum registers, q
and a
, and three classical registers:
c
pfu
syn
The cell below adds the necessary operations to measure the stabilizers \(\hat{Z}_0 \hat{Z}_1\) and \(\hat{Z}_1 \hat{Z}_2\). Three measurement operations are applied to qubits within qreg
. The outcome of the measurements is stored in the corresponding bits within creg
, i.e. a measurement on qreg[0]
will have a measurement outcome stored in creg[0]
. Three Measurement operations are added to each qubit in qreg
and the corresponding bit in creg
.
from pytket.circuit import Circuit, QubitRegister, BitRegister
circuit = Circuit()
circuit.name = "repetition-code"
qreg = QubitRegister("q", 3)
circuit.add_q_register(qreg)
areg = QubitRegister("a", 1)
circuit.add_q_register(areg)
creg = BitRegister("c", 3)
circuit.add_c_register(creg)
pfu = BitRegister("pfu", 3)
circuit.add_c_register(pfu)
syn = BitRegister("syn", 2)
circuit.add_c_register(syn)
circuit.add_circbox(syndrome_extraction, [areg[0], qreg[0], qreg[1], syn[0]])
circuit.add_circbox(syndrome_extraction, [areg[0], qreg[1], qreg[2], syn[1]])
circuit.add_wasm_to_reg("decode3", wasm_file_handler, [syn], [pfu])
circuit.add_circbox(feedforward_box, list(qreg) + list(pfu))
circuit.add_circbox(syndrome_extraction, [areg[0], qreg[0], qreg[1], syn[0]])
circuit.add_circbox(syndrome_extraction, [areg[0], qreg[1], qreg[2], syn[1]])
circuit.add_wasm_to_reg("decode3", wasm_file_handler, [syn], [pfu])
circuit.add_circbox(feedforward_box, list(qreg) + list(pfu))
for i in range(3):
circuit.Measure(qreg[i], creg[i])
circuit.add_wasm_to_reg("reset_syn_old", wasm_file_handler, [], []);
from pytket.circuit.display import render_circuit_jupyter
render_circuit_jupyter(circuit)
The repetition code circuit with the Wasm operation can be submitted to the H1-1E
emulator using the usual workflow: compilation, costing, submission, status check and, finally, job retrieval. The linked Wasm module must be specified during job submission.
from pytket.extensions.quantinuum import QuantinuumBackend
backend = QuantinuumBackend(device_name="H1-1E")
backend.login()
compiled_circuit = backend.get_compiled_circuit(circuit, optimisation_level=0)
render_circuit_jupyter(compiled_circuit)
The combined size of the TKET + Wasm program is estimated below in MBs.
program_size = estimate_circuit_size(compiled_circuit) + estimate_wasm_size(wasm_file_path)
print(f"{program_size} MBs")
0.0025129318237304688 MBs
n_shots = 1000
handle = backend.process_circuit(
compiled_circuit, n_shots=n_shots, wasm_file_handler=wasm_file_handler
)
backend.circuit_status(handle)
CircuitStatus(status=<StatusEnum.QUEUED: 'Circuit is queued.'>, message='{"name": "repetition-code", "submit-date": "2024-10-28T05:08:34.273746", "result-date": null, "queue-position": null, "cost": null, "error": null}', error_detail=None, completed_time=None, queued_time=None, submitted_time=None, running_time=None, cancelled_time=None, error_time=None, queue_position=None)
result = backend.get_result(handle)
result.get_distribution(creg)
{(0, 0, 0): 0.988, (0, 0, 1): 0.005, (0, 1, 0): 0.002, (1, 0, 0): 0.005}
The logical outcome is the parity of the data qubits after measurement and correction. For example, if the register creg
is 111, then the raw outcome is 1^1^1 = 1.
If the correction is 000 (000^111 = 111), after correction the logical outcome is 1^1^1 = 1
If the correction is 010 (010^111 = 101), after correction the logical outcome is 1^0^1 = 0
If a logical zero state is prepared, then in the example above, the raw outcome is incorrect and the first corrected outcome is also incorrect (logical failure). The second corrected outcome is correct.
My logical fidelity is, \(\frac{N_{success}}{N_{shots}}\), where \(N_{success}\) is the number of successfully corrected outcomes and \(N_{shots}\) is the number of shots total.
import numpy as np
import pandas as pd
from pytket.backends.backendresult import BackendResult
from pytket.circuit import BitRegister
def logical_error(
result: BackendResult,
n_shots: int,
classical_register: BitRegister,
pauli_frame_register: BitRegister,
) -> pd.DataFrame:
raw0_tot = 0
cor0_tot = 0
raw1_tot = 0
cor1_tot = 0
raw_meas = result.get_shots(creg)
corrs = result.get_shots(pfu)
for i in range(0, len(raw_meas)):
raw_log = sum(raw_meas[i]) % 2
corr_log = (raw_log + sum(corrs[i])) % 2
if raw_log == 0:
raw0_tot += 1
if raw_log == 1:
raw1_tot += 1
if corr_log == 0:
cor0_tot += 1
if corr_log == 1:
cor1_tot += 1
raw0_tot = raw0_tot / n_shots
raw1_tot = raw1_tot / n_shots
cor0_tot = cor0_tot / n_shots
cor1_tot = cor1_tot / n_shots
return pd.Series(
[
["Raw Zeros", raw0_tot],
["Raw Ones", raw1_tot],
["Corrected Zeros", cor0_tot],
["Corrected Ones", cor1_tot],
]
)
logical_error(result, n_shots, creg, pfu).head()
0 [Raw Zeros, 0.988]
1 [Raw Ones, 0.012]
2 [Corrected Zeros, 0.988]
3 [Corrected Ones, 0.012]
dtype: object
Surface-17 Code¶
This use case demonstrates asynchronous (async) Wasm calls for a 9-qubit surface-code problem with 8 ancillary qubits, the surface-17 code. This is a [[9, 1, 3]] code, which can correct any single qubit error fault-toralantly. In the schematic below, each edge in the graph is a qubit, and each plaquette is a stabilizer with neighbouring nodes as support qubits. A blue plaquette is a Pauli-\(\hat{X}\) stabilizer and a red plaquette Pauli-\(\hat{Z}\) stabilizer. Plaquettes defined at the boundary have weight-2 stabilizers, but otherwise are weight-4. A stabilizer defined at each plaquette must have even commutativity with stabilizers defined by neighboring plaquettes. The C and Rust source, in addition to the build scripts, can be found here.
During execution of the circuit, a Wasm call uses the syndrome measurements to define an in-memory Wasm variable, PFU
(Pauli Frame Update), via a decoder look-up table. Towards the end of a circuit, an integer Wasm function is used to retrieve the in-memory Wasm variable and perform conditional Pauli-\(Z\) operations select qubits. The benefit of async calls is ability to accumulate Wasm results until they are needed for conditional operations, which helps minimize the impact of noise on a job during machine operation.
Below, the \(Z\)-syndrome is extracted. The circuit to perform this is defined over 9-qubits and encapsulated within a pytket.circuit.CircBox
. The CircBox
will perform the following \(\hat{Z}\) syndrome measurements:
\(\hat{Z}_0 \hat{Z}_1\)
\(\hat{Z}_1 \hat{Z}_2 \hat{Z}_4 \hat{Z}_5\)
\(\hat{Z}_3 \hat{Z}_4 \hat{Z}_6 \hat{Z}_7\)
\(\hat{Z}_7 \hat{Z}_8\)
from pytket.circuit import Circuit, CircBox
def extract_z_syndrome() -> CircBox:
circuit = Circuit()
a = circuit.add_q_register("a", 4)
q = circuit.add_q_register("q", 9)
c = circuit.add_c_register("c", 4)
circuit.CX(q[1], a[0])
circuit.CX(q[0], a[0])
circuit.Measure(a[0], c[0])
circuit.add_barrier([a[1], q[2], q[1], q[5], q[4]])
circuit.CX(q[2], a[1])
circuit.CX(q[1], a[1])
circuit.CX(q[5], a[1])
circuit.CX(q[4], a[1])
circuit.Measure(a[1], c[1])
circuit.add_barrier([a[1], q[2], q[1], q[5], q[4]])
circuit.add_barrier([a[2], q[4], q[3], q[7], q[6]])
circuit.CX(q[4], a[2])
circuit.CX(q[3], a[2])
circuit.CX(q[7], a[2])
circuit.CX(q[6], a[2])
circuit.Measure(a[2], c[2])
circuit.add_barrier([a[2], q[4], q[3], q[7], q[6]])
circuit.CX(q[8], a[3])
circuit.CX(q[7], a[3])
circuit.Measure(a[3], c[3])
return CircBox(circuit)
Next, the \(X\)-syndrome is extracted. The circuit to perform this is defined over 9-qubits and encapsulated within a pytket.circuit.CircBox
. The CircBox
will perform the following \(\hat{X}\)-syndrome measurements:
\(\hat{X}_2 \hat{X}_5\)
\(\hat{X}_4 \hat{X}_5 \hat{X}_7 \hat{X}_8\)
\(\hat{X}_0 \hat{X}_1 \hat{X}_3 \hat{X}_4\)
\(\hat{X}_3 \hat{X}_6\)
def extract_x_syndrome() -> CircBox:
circuit = Circuit()
a = circuit.add_q_register("a", 4)
q = circuit.add_q_register("q", 9)
c = circuit.add_c_register("c", 4)
for qubit in a:
circuit.H(qubit)
circuit.CX(a[0], q[2])
circuit.CX(a[0], q[5])
circuit.H(a[0])
circuit.Measure(a[0], c[0])
circuit.add_barrier([a[1], q[5], q[8], q[4], q[7]])
circuit.CX(a[1], q[5])
circuit.CX(a[1], q[8])
circuit.CX(a[1], q[4])
circuit.CX(a[1], q[7])
circuit.H(a[1])
circuit.Measure(a[1], c[1])
circuit.add_barrier([a[1], q[5], q[8], q[4], q[7]])
circuit.add_barrier([a[2], q[1], q[4], q[0], q[3]])
circuit.CX(a[2], q[1])
circuit.CX(a[2], q[4])
circuit.CX(a[2], q[0])
circuit.CX(a[2], q[3])
circuit.H(a[2])
circuit.Measure(a[2], c[2])
circuit.add_barrier([a[2], q[0], q[3], q[7], q[6]])
circuit.CX(a[3], q[3])
circuit.CX(a[3], q[6])
circuit.H(a[3])
circuit.Measure(a[3], c[3])
return CircBox(circuit)
Now the Pauli-\(X\) destabilizers are applied to the circuit based on the value of a specified 4-bit classical register. The circuit to perform this is defined over 9-qubits and 4-bits and encapsulated within a pytket.circuit.CircBox
. Each qubit is prepared in the \(| + \rangle\) state. The \(\hat{Z}\)-syndromes are used to conditionally apply a Pauli-\(\hat{X}\) operator where the syndrome result is -1 (bit value 1). This operation is known as the \(\hat{X}\) destabilizer and the \(\hat{Z}\)-syndromes are now valued at 0. This ensures the logical state is fixed by the \(\hat{Z}\)-stabilizers, in addition to the \(\hat{X}\)-stabilizers.
def apply_x_destabillizers() -> CircBox:
circuit = Circuit()
q = circuit.add_q_register("q", 9)
syn = circuit.add_c_register("syn", 4)
circuit.X(q[1], condition_bits=[syn[0]], condition_value=1)
circuit.X(q[2], condition_bits=[syn[0]], condition_value=1)
circuit.X(q[2], condition_bits=[syn[1]], condition_value=1)
circuit.X(q[7], condition_bits=[syn[2]], condition_value=1)
circuit.X(q[8], condition_bits=[syn[2]], condition_value=1)
circuit.X(q[8], condition_bits=[syn[3]], condition_value=1)
return CircBox(circuit)
The reset_operations
function returns an \(N\)-qubit CircBox
instance that applies the OpType.Reset
operation to a specified number of qubits.
def reset_operations(n_qubits: int) -> CircBox:
circuit = Circuit(n_qubits)
for qubit in circuit.qubits:
circuit.Reset(qubit)
return CircBox(circuit)
The hadamard_operations
function returns an \(N\)-qubit CircBox
instance that applies the Optype.H
gate to a specified number of qubits.
def hadamard_operations(n_qubits: int) -> CircBox:
circuit = Circuit(n_qubits)
for qubit in circuit.qubits:
circuit.H(qubit)
return CircBox(circuit)
The method below generates a CircBox
instance that applies conditional Pauli-\(Z\) operations. The input parameter, n_bits
, is used to control three registers:
the size of the qubit register
the size of a first classical register
the size of a second classical register
def apply_x_correction(n_bits: int) -> CircBox:
circuit = Circuit()
q = circuit.add_q_register("q", n_bits)
bits0 = circuit.add_c_register("bits0", n_bits)
for i, c in enumerate(list(bits0)):
circuit.Z(q[i], condition_bits=[c], condition_value=1)
return CircBox(circuit)
The CircBox
instances are instantiated below.
extract_z_syndrome_box = extract_z_syndrome()
extract_x_syndrome_box = extract_x_syndrome()
apply_x_destabillizers_box = apply_x_destabillizers()
hadamard_operation_box = hadamard_operations(9)
reset_operation_box = reset_operations(4)
apply_x_correction_box = apply_x_correction(9)
The wasm binary is loaded below using WasmFileHandler
.
from pathlib import Path
from pytket.wasm import WasmFileHandler
wasm_path = Path("surface_code/c/surface_code.wasm")
if wasm_path.is_file():
wasm_file_handler = WasmFileHandler(wasm_path)
else:
raise FileNotFoundError(f"{wasm_path} not found")
The WasmFileHandler
prints all the methods available for use in the TKET circuit. The init
method does not need to be used by the TKET circuit, and is instead required to start WAVM.
wasm_file_handler
Functions in wasm file with the uid 4bfd1b244b351fef9be63a49fb7007f77232c27ec6b4e1a7865b6dfd7c5e80ab:
function '__wasm_call_ctors' with 0 i32 parameter(s) and 0 i32 return value(s)
function 'init' with 0 i32 parameter(s) and 0 i32 return value(s)
function 'set_pfu_value' with 1 i32 parameter(s) and 0 i32 return value(s)
function 'update_pfu' with 1 i32 parameter(s) and 0 i32 return value(s)
function 'get_pfu' with 0 i32 parameter(s) and 1 i32 return value(s)
function 'reset_pfu' with 0 i32 parameter(s) and 0 i32 return value(s)
The circuit is constructed by adding the CircBox
instances, as shown below. The circuit prepares each qubit in the \(| + \rangle\) state. Initially, there is a \(Z\)-syndrome extraction and \(X\)-destabilizer operation to ensure the circuit is in the code space of the surface code. After each syndrome extraction, the OpType.Reset
operation needs to be used on the ancilla registers, az
and ax
.
A 1-Dimensional minimum matching problem is solved to guess the \(\hat{Z}\) error. A look-up style decoder is used to store the Pauli-Frame update as in-memory Wasm variable. Void calls are used to update this variable so the circuit can continue execution without waiting for decoding to finish. This helps prevent qubit idling and memory error affecting the circuit.
from pytket.circuit import Circuit
circuit = Circuit()
ax = circuit.add_q_register("ax", 4)
az = circuit.add_q_register("az", 4)
q = circuit.add_q_register("q", 9)
syn_x = circuit.add_c_register("syn_x", 4)
syn_z = circuit.add_c_register("syn_z", 4)
pfu = circuit.add_c_register("pfu", 9)
creg = circuit.add_c_register("creg", 9)
circuit.add_circbox(hadamard_operation_box, list(q))
circuit.add_circbox(extract_z_syndrome_box, list(az) + list(q) + list(syn_z))
circuit.add_circbox(reset_operation_box, list(az))
circuit.add_circbox(apply_x_destabillizers_box, list(q) + list(syn_z))
circuit.add_circbox(extract_x_syndrome_box, list(ax) + list(q) + list(syn_x))
circuit.add_wasm_to_reg("set_pfu_value", wasm_file_handler, [syn_x], [])
circuit.add_circbox(reset_operation_box, list(ax))
circuit.add_circbox(extract_x_syndrome_box, list(ax) + list(q) + list(syn_x))
circuit.add_wasm_to_reg("update_pfu", wasm_file_handler, [syn_x], [])
circuit.add_wasm_to_reg("get_pfu", wasm_file_handler, [], [pfu])
circuit.add_circbox(apply_x_correction_box, list(q) + list(pfu))
circuit.add_circbox(hadamard_operation_box, list(q))
for qubit, bit in zip(list(q), list(creg)):
circuit.Measure(qubit, bit)
circuit.add_wasm_to_reg("reset_pfu", wasm_file_handler, [], []);
from pytket.circuit.display import render_circuit_jupyter
render_circuit_jupyter(circuit)
The QuantinuumBackend
object is constructed and the circuit is compiled and submitted using the usual workflow. The WasmFileHandler
instance is submitted using the kwarg
on process_circuits
.
from pytket.extensions.quantinuum import QuantinuumBackend
backend = QuantinuumBackend(device_name="H1-1E")
backend.login()
compiled_circuit = backend.get_compiled_circuit(circuit, optimisation_level=0)
The combined TKET + Wasm program size is estimated below in MBs.
program_size = estimate_circuit_size(compiled_circuit) + estimate_wasm_size(wasm_file_path)
print(f"{program_size} MBs")
0.008947372436523438 MBs
handle = backend.process_circuit(
compiled_circuit, n_shots=500, wasm_file_handler=wasm_file_handler
)
backend.circuit_status(handle)
CircuitStatus(status=<StatusEnum.QUEUED: 'Circuit is queued.'>, message='{"name": "job", "submit-date": "2024-10-28T05:10:13.469657", "result-date": null, "queue-position": null, "cost": null, "error": null}', error_detail=None, completed_time=None, queued_time=None, submitted_time=None, running_time=None, cancelled_time=None, error_time=None, queue_position=None)
result = backend.get_result(handle)
The shots table for the creg
registers is retrieved below. The post-processing should show all syndrome measurements are zero after correction.
shot_table = result.get_shots(creg)
for i in range(10):
m = shot_table[i]
s1 = (int(m[6]) + int(m[3])) % 2
s2 = (int(m[3]) + int(m[4]) + int(m[1]) + int(m[0])) % 2
s3 = (int(m[8 - 0]) + int(m[8 - 1]) + int(m[8 - 3]) + int(m[8 - 4])) % 2
s4 = (int(m[5]) + int(m[2])) % 2
l = (int(m[8]) + int(m[7]) + int(m[6])) % 2
outcome = (s1, s2, s3, s4, l)
print(outcome)
(0, 0, 0, 0, 0)
(0, 0, 0, 0, 0)
(0, 0, 0, 0, 0)
(0, 0, 0, 0, 0)
(0, 0, 0, 0, 0)
(0, 0, 0, 0, 0)
(0, 0, 0, 0, 0)
(0, 0, 0, 0, 0)
(0, 0, 0, 0, 0)
(0, 0, 0, 0, 0)