Getting Started

The \(\mathrm{t|ket}\rangle\) compiler is a powerful tool for optimising and manipulating platform-agnostic quantum circuits, focussed on enabling superior performance on NISQ (Noisy Intermediate-Scale Quantum) devices. The pytket package provides an API for interacting with \(\mathrm{t|ket}\rangle\) and transpiling to and from other popular quantum circuit specifications.

We recommend using a Python 3.6+ environment when possible. Install pytket from PyPI using:

pip install pytket

This will install the \(\mathrm{t|ket}\rangle\) compiler binaries as well as the pytket package. For those using an older version of pytket, keep up to date by installing with the --upgrade flag for additional features and bug fixes.

There are separate packages for managing the interoperability between pytket and other quantum software packages which can also be installed via PyPI:

  • pytket_cirq

  • pytket_projectq

  • pytket_pyquil

  • pytket_pyzx

  • pytket_qiskit

All of these are licensed under a Non-Commercial Use Software Licence.

The Circuit Class

Circuit objects provide an abstraction of quantum circuits. They consist of a set of qubits/quantum wires and a collection of operations applied to them in a given order. These wires have open inputs and outputs, rather than assuming any fixed input state.

We can start by building a circuit with some blank wires and add some instructions.

from pytket import Circuit
c = Circuit(2,2)
c.H(0)
c.Rz(0.25, 0)
c.CX(1,0)

Breaking this down: c = Circuit(2,2) creates a new circuit with two qubits and two bits. Each qubit is identified by its numerical index, ranging from 0 to (no. of qubits)-1. c.H(0) applies a Hadamard gate to qubit 0. Instructions are always added to the end of the circuit. Rotational gates can be parameterised by the angle of rotation, specified in half-turns - for example c.Rz(0.25, 0) adds a Z rotation by \(\frac{\pi}{4}\) radians. Multi-qubit operations will take an ordered sequence of qubit indices, such as c.CX(1,0) applying a CX gate with the control on qubit 1 and the target on qubit 0.

We can add measurements onto qubits to observe their state and store the result in a given bit.

c.Measure(0,0)
c.Measure(1,1)

We assume all measurements are made in the computational (Z) basis - others can be performed by adding the appropriate change-of-basis operation before the measurement. By default, bits are also referenced by their indices in an anonymous classical register. When transpiling to another quantum SDK (e.g. Qiskit), a single classical register is created, collating all measurement results.

Some more advanced circuit construction features exist, such as the ability to construct the inverse circuit for any pure circuit (i.e. when it has no measurements).

c_dg = c.dagger()

When constructing larger circuits, it is often useful to define small subroutines which can be applied multiple times. We can use this idea in pytket with circuit appending:

c_sub = Circuit(2)
c_sub.CX(0, 1)
c_sub.Rz(1.25, 1)
c_sub.CX(0, 1)
c_sub.Rz(0.5, 1)

c.append(c_sub)
c.H(0)
c.append(c_sub)

c.append(c_sub) will copy c_sub into c, and attach the input for qubit \(i\) in c_sub to the output for qubit \(i\) in c. Further information on how to interact directly with Circuit objects can be found in the API reference for the Circuit class.

Compilation

In classical computing, a compiler is required to translate from a high-level language (e.g. C++) to the instruction set of the target machine (e.g. RISC-V). During this process, the program is automatically optimised to reduce the number of instructions, memory consumption, energy consumption, etc. The same needs exist in quantum computing to take a circuit description and enable it to run on a given simulator, hardware device, or cloud system (such as the IBM Quantum Experience or Rigetti’s Quantum Cloud Service).

pytket is a retargetable compiler, with the ability to accept and produce circuits for any of the following:

  • Google Cirq

  • IBM Qiskit

  • OpenQASM

  • Rigetti pyQuil

  • ProjectQ compilation engines

  • PyZX

This translation facility allows, for example, circuits generated by the variational forms in Qiskit Aqua to be run on Rigetti QCS.

However, near-term quantum devices do not currently provide the nice abstraction of a “perfect” machine - they are troubled by imperfect fidelity of operations, gradual decoherence over time, and restricted qubit-adjacency only allowing two-qubit gates between specific positions on the architecture.

In pytket, the Circuit class is used the main representation of any circuit during compilation, all the way from the logical circuit the programmer designs for an unrestricted abstract machine, to the final circuit that conforms to all constraints of the target device. Each of the device constraints are captured by a Predicate, which asserts that a circuit satisfies a given property. Every optimisation or step of the compilation chain (“Pass”) can guarantee that certain Predicate s will be satisfied, potentially at the cost of invalidating others.

One such architectural constraint is the problem of qubit connectivity on heterogeneous architectures which can be solved by a quantum compiler with placement and routing procedures. This takes the adjacency graph of the architecture’s qubits (the coupling map) and identifies a good mapping from the qubits of the circuit to the positions on the device to make it possible to perform as many of the two-qubit operations as possible. The circuit is then modified by introducing swaps to rearrange the logical qubits such that any multi-qubit operations occur between neighbouring physical qubits.

These tasks are both performed by applying pytket’s routing pass. pytket.passes.gen_routing_pass takes a Device object (encapsulating the coupling map of the device, in addition to error and timing information) and produces a compiler pass that can be applied to Circuit s to guarantee that they conform to the required qubit connectivity graph. We can build a basic device from an Architecture which specifies only the connectivity graph. More on the Device and Architecture classes can be found in the API Reference.

from pytket import Architecture, CompilationUnit, Device
from pytket.passes import gen_routing_pass
arc = Architecture([(0,1), (1,2), (2,3)])
dev = Device(arc)
route_pass = gen_routing_pass(dev)

cu = CompilationUnit(circuit)
route_pass.apply(cu)

compiled_circ = cu.circuit
print(cu.initial_map)
print(cu.final_map)

The compiled circuit is semantically identical to the original logical circuit but will now only have multi-qubit gates between pairs of nodes in the coupling map. The map between the logical qubits and the physical nodes they live on will change throughout the resulting circuit due to the inserted swaps. However, any measurement operations will still map logical qubits to the same classical bits, so the qubit permutations will not affect any readouts from the device.

The CompilationUnit is a wrapper for the Circuit class which tracks key properties throughout compilation. For example, the initial_map and final_map give the mappings from the original qubits of the logical circuit to their corresponding locations at the beginning or the end of the compiled circuit. This allows us to inspect where route_pass allocated the logical qubits on the device, or which qubits to append gates to if we wish to extend the circuit.

Routing typically sacrifices circuit size/depth (from inserting swaps) to satisfy device constraints. Sadly, the problems of decoherence and imperfect fidelities mean that physical devices will accumulate noise proportional to the size/depth of the circuit being run. \(\mathrm{t|ket}\rangle\) provides a selection of optimisation passes to rewrite parts of the circuit to produce an equivalent one with fewer operations/less depth, including some recommended sequences for ease of use. SynthesiseIBM is one such sequence to compile into the standard gate set for IBM Q devices (U1, U2, U3, CX), designed to preserve qubit connectivity and gate directedness, making it safe to apply after routing.

from pytket.passes import SynthesiseIBM
SynthesiseIBM().apply(cu)

To see these in action, take a look at the jupyter notebooks on the pytket GitHub repository.