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.

By default guppy.load_pytket will create Guppy functions which use arrays of qubits as inputs. This means that our qft_func above will 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.

Using guppy.load_pytket works best when the loaded pytket Circuit contains only unitary quantum gates which will map to Guppy functions which borrow qubits. Guppy allows qubits to be explicitly allocated at runtime and deallocated by measurement. By contrast, pytket treats qubits as alive for the entirety of the quantum program. Measurement and classical logic should ideally be done directly in Guppy. This has the benefit of allowing functions which consume qubits to be type checked according to Guppy’s ownership model.

Warning

Loading pytket circuits with a very large number of gates can drastically increase the size of the compiled program and potentially lead to performance issues. This is because Guppy functions defined from pytket circuits don’t benefit from the asymptotic program compression provided by Guppy loops. Users may wish to consider rewriting large subroutines in Guppy if program size is a concern.

How to deal with operations unsupported by Guppy

Loading pytket circuits works by converting the pytket circuit to a HUGR function under the hood. Guppy also compiles to HUGR meaning effectively we get a pre-compiled Guppy function we can call.

Quantum operations in HUGR are defined in extensions. Most operations in the Guppy std.quantum and std.qsystem modules map directly to the HUGR extensions tket.quantum and tket.qsystem. When loading pytket circuits, pytket OpTypes that match to those extension operations are loaded directly. All of the operations supported by pytket can be found in the pytket OpType documentation.

Unsupported pytket operations are loaded as opaque, which allows a round-trip conversion to be successful. However, such opaque operations cannot be emulated or submitted to devices.

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.

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])

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

We now have a Guppy function that takes three qubits as input. As this function has inputs, it cannot be an entrypoint to an executable program. We therefore have to call circuit_func inside a Guppy function which takes no arguments and allocates some qubits for it to use.

from guppylang.std.quantum import discard_array

@guppy
def guppy_func() -> None:
  qs = array(qubit() for _ in range(3))
  circuit_func(qs)
  discard_array(qs)

We will get an error if we try to invoke the Selene emulator as it cannot execute the opaque op defining the Unitary2qBox in the call to circuit_func.

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

File /app/.venv/lib/python3.12/site-packages/guppylang/defs.py:256, in GuppyFunctionDefinition.emulator(self, n_qubits, builder, libs, platform)
    226 def emulator(
    227     self,
    228     n_qubits: int | None = None,
   (...)    231     platform: Platform = "helios",
    232 ) -> EmulatorInstance:
    233     """Compile this function for emulation with the selene-sim emulator.
    234 
    235     Compiles the function to a HUGR package and builds it using the provided
   (...)    254         An `EmulatorInstance` that can be used to run the function in an emulator.
    255     """
--> 256     return self.with_opt_level(OptimizationLevel.Default).emulator(
    257         n_qubits, builder, libs, platform
    258     )

File /app/.venv/lib/python3.12/site-packages/guppylang/optimizer.py:124, in OptimizerInstance.emulator(self, n_qubits, builder, libs, platform)
    116 def emulator(
    117     self,
    118     n_qubits: int | None = None,
   (...)    121     platform: Platform = "helios",
    122 ) -> EmulatorInstance:
    123     """Compile this function for emulation with the configured optimizations."""
--> 124     return self.definition._emulator(
    125         self.compile_function(), n_qubits, builder, libs, platform
    126     )

File /app/.venv/lib/python3.12/site-packages/guppylang/defs.py:310, in GuppyFunctionDefinition._emulator(self, mod, n_qubits, builder, libs, platform)
    299     from guppylang.decorator import expected_qubits
    301     raise EmulatorBuildError(
    302         ValueError(
    303             "Number of qubits to be used must be specified, either as an "
   (...)    307         )
    308     )
--> 310 return builder.build(mod, n_qubits=qubits, arg_specs=arg_specs)

File /app/.venv/lib/python3.12/site-packages/guppylang/emulator/builder.py:159, in EmulatorBuilder.build(self, package, n_qubits, arg_specs)
    141 """Build an EmulatorInstance from a compiled package.
    142 
    143 Args:
   (...)    155     An EmulatorInstance that can be used to run the compiled program.
    156 """
    157 custom_args = self._custom_args or {}
--> 159 instance = selene_sim.build(  # type: ignore[attr-defined]
    160     package,
    161     name=self._name,
    162     build_dir=self._build_dir,
    163     interface=self._interface,
    164     utilities=self._utilities,
    165     verbose=self._verbose,
    166     planner=self._planner,
    167     progress_bar=self._progress_bar,
    168     strict=self._strict,
    169     save_planner=self._save_planner,
    170     platform=self._platform,
    171     **custom_args,
    172 )
    174 return EmulatorInstance(
    175     _instance=instance, _n_qubits=n_qubits, _arg_specs=tuple(arg_specs)
    176 )

File /app/.venv/lib/python3.12/site-packages/selene_sim/build.py:243, in build(src, name, build_dir, interface, utilities, verbose, planner, progress_bar, strict, save_planner, complete_manifest, **kwargs)
    239 # Walk through the path from the input resource to the selene executable,
    240 # applying each step in turn. If a strict build has been requested, artifact
    241 # kind validation is performed on the output of each step.
    242 for step in steps_to_iterate_over:
--> 243     artifacts.append(step.apply(ctx, artifacts[-1]))
    244     if strict:
    245         assert artifacts[-1].validate_kind(), (
    246             f"Artifact failed validation: {artifacts[-1]}"
    247         )

File /app/.venv/lib/python3.12/site-packages/selene_helios_qis_plugin/build.py:85, in SeleneCompileHUGRToLLVMBitcodeStringStep.apply(cls, build_ctx, input_artifact)
     80 if not HAS_HUGR_QIS_COMPILER:
     81     raise RuntimeError(
     82         "selene-hugr-qis-compiler with appropriate support for multiple QIS targets"
     83         " is required for building. Please install it via pip."
     84     )
---> 85 bitcode = compile_to_bitcode(input_artifact.resource, platform="helios")
     86 return cls._make_artifact(bitcode)

HugrReadError: Pytket op 'Unitary2qBox' is not currently supported by the Selene HUGR-QIS compiler

The solution to handling operations which are not directly supported is to decompose these unsupported operations into gates from the supported extensions 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
new_circuit_func = guppy.load_pytket("guppy_func", pytket_circ)

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

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