Migrating to Guppy from pytket

This guide will cover how to migrate quantum programs from pytket to Guppy.

Guppy is a new programming language developed by Quantinuum for the next generation of quantum programs. Its design allows for quantum programs with complex control flow (loops and recursion and more) to be expressed in intuitive Pythonic syntax.

In pytket, a user can build a quantum circuit by appending quantum and classical operations to form a list of gate commands. The user writes a python program which when run builds a pytket Circuit instance. Internally, this circuit is stored as a Directed Acyclic Graph (DAG) which serves as the intermediate representation for the TKET compiler. The TKET compiler will continue to be maintained and will play an important role in the optimization of quantum programs written in Guppy.

Guppy is a compiled language which distinguishes it from Python. The user writes a quantum program with Guppy functions and then compiles this program [1]. The Guppy compiler ensures safety guarantees for quantum programs through its type system. To read more about this type safety, see the language guide section on ownership rules. Once we have a compiled program, we can run it on an emulator or quantum device.

Loading pytket circuit as Guppy functions

A pytket Circuit instance can be invoked as a Guppy function using the guppy.load_pytket method. This feature can be useful for making use of existing pytket circuits directly in Guppy without having to rewrite code from scratch. In the near term, it is also useful to synthesize circuits (e.g. state preparation, permutations) using pytket’s higher level “box” constructs. These synthesized circuits can the be invoked as Guppy functions.

Basic Example

Let’s start with a simple example where we can build a pytket circuit and load it as a Guppy function.

from pytket import Circuit

qft2_circ = Circuit(2)
qft2_circ.H(0)
qft2_circ.CRz(0.5, 1, 0)
qft2_circ.H(1)
qft2_circ.SWAP(0, 1)
[H q[0]; CRz(0.5) q[1], q[0]; H q[1]; SWAP q[0], q[1]; ]

We can now use this pytket circuit to build a Guppy function as follows. We first pass in a string label and then the circuit instance.

from guppylang import guppy

qft_func = guppy.load_pytket("qft_func", qft2_circ)

We now have a Guppy function called qft_func which we can compile or use as a subroutine in other functions.

Note that by default guppy.load_pytket will create Guppy functions which use arrays of qubits as inputs. This means that our qft_func above will have take an array of two qubits as input. If we want the function to take two separate qubit arguments, we can specify use_arrays=False in guppy.load_pytket. Also note that by default, circuits with separate quantum registers become Guppy functions that take multiple arrays of qubits as input.

How to deal with operations unsupported by Guppy

All of the quantum operations supported by Guppy are listed in the quantum and qsystem modules. At present this gate set is more limited than the set of supported pytket operations. All of the operations supported by pytket can be found in the pytket OpType documentation.

If we try to load a pytket circuit with operations which are not in the quantum or qsystem modules we would not be able to load the circuit as a guppy function and compile it. In the code snippet below we will construct a circuit for performing a two-qubit unitary operation which we will specify as a numpy array. This unitary box is not natively supported in Guppy.

import numpy as np
from guppylang import guppy
from pytket.circuit import Circuit, Unitary2qBox, OpType

G = np.array(
    [
        [1, 0, 0, 0],
        [0, 1 / np.sqrt(2), -1 / np.sqrt(2), 0],
        [0, 1 / np.sqrt(2), 1 / np.sqrt(2), 0],
        [0, 0, 0, 1],
    ]
)
G_box = Unitary2qBox(G)

pytket_circ = Circuit(3)
pytket_circ.add_gate(G_box, [0, 1])
pytket_circ.CCX(0, 1, 2)
pytket_circ.add_gate(OpType.CnY, [0, 1, 2])

guppy_func = guppy.load_pytket("circuit_func", pytket_circ)

Treating unknown optypes as opaque operations allows for round trip conversion between compiled Guppy programs and circuits. However we will get an error if we try to invoke the Selene emulator as it cannot execute the opaque op.

sim_result = guppy_func.emulator(n_qubits=3).with_seed(2).run()
---------------------------------------------------------------------------
HugrReadError                             Traceback (most recent call last)
Cell In[4], line 1
----> 1 sim_result = guppy_func.emulator(n_qubits=3).with_seed(2).run()

File /app/docs/.venv/lib/python3.11/site-packages/guppylang/defs.py:84, in GuppyFunctionDefinition.emulator(self, n_qubits, builder)
     81 mod = self.compile()
     83 builder = builder or EmulatorBuilder()
---> 84 return builder.build(mod, n_qubits=n_qubits)

File /app/docs/.venv/lib/python3.11/site-packages/guppylang/emulator/builder.py:83, in EmulatorBuilder.build(self, package, n_qubits)
     72 def build(self, package: Package, n_qubits: int) -> EmulatorInstance:
     73     """Build an EmulatorInstance from a compiled package.
     74 
     75     Args:
   (...)     80         An EmulatorInstance that can be used to run the compiled program.
     81     """
---> 83     instance = selene_sim.build(  # type: ignore[attr-defined]
     84         package,
     85         name=self._name,
     86         build_dir=self._build_dir,
     87         interface=self._interface,
     88         utilities=self._utilities,
     89         verbose=self._verbose,
     90         planner=self._planner,
     91         progress_bar=self._progress_bar,
     92         strict=self._strict,
     93         save_planner=self._save_planner,
     94     )
     96     return EmulatorInstance(_instance=instance, _n_qubits=n_qubits)

File /app/docs/.venv/lib/python3.11/site-packages/selene_sim/build.py:217, in build(src, name, build_dir, interface, utilities, verbose, planner, progress_bar, strict, save_planner, **kwargs)
    213 # Walk through the path from the input resource to the selene executable,
    214 # applying each step in turn. If a strict build has been requested, artifact
    215 # kind validation is performed on the output of each step.
    216 for step in steps_to_iterate_over:
--> 217     artifacts.append(step.apply(ctx, artifacts[-1]))
    218     if strict:
    219         assert artifacts[-1].validate_kind(), (
    220             f"Artifact failed validation: {artifacts[-1]}"
    221         )

File /app/docs/.venv/lib/python3.11/site-packages/selene_helios_qis_plugin/build.py:66, in SeleneCompileHUGRToLLVMBitcodeStringStep.apply(cls, build_ctx, input_artifact)
     64 if build_ctx.verbose:
     65     print("Converting HUGR envelope bytes to LLVM Bitcode")
---> 66 bitcode = compile_to_bitcode(input_artifact.resource)
     67 return cls._make_artifact(bitcode)

HugrReadError: Error loading HUGR package.

Caused by:
    0: failed to import hugr generated by guppylang (guppylang-internals-v0.23.0)-v0.21.3
    1: extension resolution error
    2: OpaqueOp:TKET1.tk1op in Node(4) requires extension TKET1, but it could not be found in the extension list used during resolution. The available extensions are: arithmetic.conversions, arithmetic.float, arithmetic.float.types, arithmetic.int, arithmetic.int.types, collections.array, collections.list, collections.static_array, collections.value_array, guppylang, logic, prelude, ptr, tket.bool, tket.debug, tket.futures, tket.guppy, tket.qsystem, tket.qsystem.random, tket.qsystem.utils, tket.quantum, tket.result, tket.rotation, tket.wasm

The solution to handling operations which are not directly supported in Guppy is to decompose these unsupported operations into gates from quantum and qsystem before loading the circuit. This can be readily done with the AutoRebase pass from pytket.

from pytket.passes import AutoRebase, DecomposeBoxes

rebase_pass = AutoRebase({OpType.H, OpType.Rz, OpType.CX}) # Specify a universal gate set  

DecomposeBoxes().apply(pytket_circ) # Decompose the Unitary2qBox to primitive gates

rebase_pass.apply(pytket_circ) # Convert all gates in pytket_circ to {H, Rz, CX}

# Load rebased circuit as a Guppy function
guppy_func = guppy.load_pytket("guppy_func", pytket_circ)

Now our compiled Guppy program should contain no opaque operations and is executable on the emulator.

Compilation and optimization of quantum programs

The pytket library contains many compilation passes for transforming quantum programs. Many of these passes are designed to optimize quantum circuits to reduce the number of entangling quantum gates. Using pytket, we can also convert programs to the native gateset of a quantum device and solve for qubit connectivity constraints.

As of August 2025, optimization of compiled Guppy programs is still at an early stage. When using guppy.emulator() the quantum program is converted to the qsystem native instructions with no further optimization. In the near term, it is planned to optimize regions of these programs with the kind of compiler passes that are already available in pytket using an updated version of the TKET compiler.

If you want to optimize your quantum program using pytket, you can create a pytket circuit and optimize it. This optimized program can then be loaded as a Guppy function by using guppy.load_pytket.

Execution of quantum programs

The Guppy language comes with a built in emulator module built on top of Selene for the execution of quantum programs. This emulator can execute compiled Guppy programs which include control flow and constructs from the standard library. There are two simulation modes available namely stabilizer and statevector which are provided via the Stim and QuEST backends respectively. Selene also supports statevector output for testing and debugging.

For more information on the Selene emulator see the Selene documentation.

Footnotes