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.

Pytket is compatible with Python 3.6, 3.7 and 3.8. 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-aqt

  • pytket-cirq

  • pytket-honeywell

  • pytket-projectq

  • pytket-pyquil

  • pytket-pyzx

  • pytket-qiskit

  • pytket-qsharp

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

Designing a Quantum Circuit

The quantum circuit is an abstraction of computation using quantum resources, designed by initialising a system into a fixed state, then mutating it via sequences of instructions/gates.

The native circuit interface built into pytket allows us to build circuits and use them directly.

from pytket import Circuit
c = Circuit(2,2) # define a circuit with 2 qubits and 2 bits
c.H(0)           # add a Hadamard gate to qubit 0
c.Rz(0.25, 0)    # add an Rz gate of angle 0.25*pi to qubit 0
c.CX(1,0)        # add a CX gate with control qubit 1 and target qubit 0
c.measure_all()  # measure qubits 0 and 1, recording the results in bits 0 and 1

Building directly in pytket provides many handy shortcuts and higher-level components for circuits, including custom gate definitions, circuit composition, gates with symbolic parameters, and conditional gates. These are explored in detail in the circuit generation, conditional gate and symbolic circuit examples.

On the other hand, pytket’s flexibility of interface allows you to take circuits defined in a number of languages, including raw source code languages such as OpenQASM and Quipper, or embedded python frameworks such as Qiskit and Cirq.

from pytket.qasm import circuit_from_qasm
c = circuit_from_qasm("my_qasm_file.qasm")

Or, if an extension module like pytket-qiskit is installed,

from qiskit import QuantumCircuit
qc = QuantumCircuit()
# ...
from pytket.qiskit import qiskit_to_tk
c = qiskit_to_tk(qc)

Running on a Backend

Designing a circuit is good, but our real goal is to run them on a quantum device. pytket presents a uniform interface for a number of hardware devices from several providers, as well as a selection of simulators. Many of the extension modules for pytket include Backend classes, which can be used interchangeably.

On quantum hardware, the observable outputs of the circuit are the final states of the classical registers. These are returned via a shot table – a table of outcomes where the columns correspond to the individual classical bits, and each row is the result from a single run of the circuit.

from pytket.backends.ibm import IBMQBackend
b = IBMQBackend("ibmq_london")
# ...
from pytket.backends.forest import ForestBackend
b = ForestBackend("Aspen-8")
# ...
from pytket.backends.aqt import AQTBackend
b = AQTBackend(access_token, "sim")
# ...
b.compile_circuit(c)        # performs the minimal compilation to satisfy the device/simulator constraints
handle = b.process_circuit(c, 10)   # run the circuit 10 times
shots = b.get_shots(handle) # retrieve and return the readouts
print(shots)
[[0 0]
 [1 0]
 [0 0]
 [1 0]
 [0 0]
 [0 0]
 [1 0]
 [1 0]
 [0 0]
 [1 0]]

If the ordering of results is not needed, the get_counts method instead returns a summary of the frequencies of each result.

counts = b.get_counts(handle)
print(counts)
{(0, 0): 5, (1, 0): 5}

All of these are stochastically sampled. Whilst this is how the real devices run, it is more convenient to have a complete description of the state of the system when testing that your circuit design was correctly implemented. Some simulators allow you to inspect the final statevector when a pure quantum circuit (one without any measurements) is run on the initial state \(|0\rangle^{\otimes n}\). This makes it easy to test running our circuit design on some test input state.

initial_state = Circuit(3)      # Initialise the system in 1/sqrt(2) (|011> + |101>)
initial_state.H(0)
initial_state.X(1)
initial_state.X(2)
initial_state.CX(0, 1)

increment = Circuit(3)
increment.CCX(2, 1, 0)
increment.CX(2, 1)
increment.X(2)

final_state = initial_state.copy()
final_state.append(increment)

from pytket.backends.ibm import AerStateBackend
b = AerStateBackend()
b.compile_circuit(initial_state)
b.compile_circuit(final_state)
handles = b.process_circuits([initial_state, final_state])
s0 = b.get_state(handles[0])    # Check that the initial state is 1/sqrt(2) (|011> + |101>)
print(s0.round(10))             # Round to ignore floating-point error in simulation
s1 = b.get_state(handles[1])    # Check that the incrementer has mapped this to 1/sqrt(2) (|100> + |110>)
print(s1.round(10))
[0.        +0.j 0.        +0.j 0.        +0.j 0.70710678+0.j
 0.        +0.j 0.70710678+0.j 0.        +0.j 0.        +0.j]
[ 0. -0.j  -0. +0.j   0. +0.j   0. -0.j   0.5-0.5j  0. +0.j   0.5-0.5j
 -0. +0.j ]

Given that global phase of a quantum system is unobservable (it can never affect the evolution of the system or measurent outcomes), these are the correct states we were looking for.

We look in more detail at the supported backends and their features in the backends example.