This tutorial will cover two related topics:
ProccesorSpec
objects.These objects can be used to define the "specification" of a quantum computer (e.g., device connectivity, the gate-set etc), and which are geared towards multi-qubit devices. Currently, these are mostly encountered in pyGSTi
as an input for generating randomized benchmarking experiments, but they will likely be used more widely in the future.
Circuit
objects.These objects represent quantum circuits. They are a more structured version of Gatestring
objects, and they contain various methods that are useful for manipulating quantum circuits (e.g., simple depth compression) and interfacing pyGSTi
with other quantum circuit standards (e.g., conversion to OpenQasm). Currently, they are mostly encountered in pyGSTi
as the output of randomized benchmarking experiment generation functions, and with related circuit compiler functions.
from __future__ import print_function
import pygsti # the main pyGSTi module
ProcessorSpec
to specify a multi-qubit device.¶The ProcessorSpec
object is designed to encapsulate the specification of a small to medium-scale quantum computer, and to hold a variety of useful things that can be derived from this information. The basic information that a ProcessorSpec
is initialized via is:
The number of qubits in the device, and, optionally, the labels of these qubits.
The target gate-set of the device, as either unitary matrices or using names that point to in-built unitary matrices. E.g., 'Gcnot' is a shorthand for specifying a CNOT gate. Normally this will be the "primitive" gates of the device, although it may sometimes be useful to choose other gate-sets (it depends what you are then going to use the ProcessorSpec
for). Currently only discrete gate-sets are supported. E.g., there is no way to specify an arbitrary $\sigma_z$-rotation as one of the gates in the device. Parameterized gates will likely be supported in the future.
The connectivity of the device.
So let's create a ProcessorSpec
.
The number of qubits the device is for:
nQubits = 4
Pick some names for the qubits. If not specified, the qubit labels default to 0, 1, 2, ...
qubit_labels = ['Q0','Q1','Q2','Q3']
Pick a set of fundamental gates. These can be specified via in-built names,such as 'Gcnot' for a CNOT gate. The full set of in-built names is specified in the dictionary returned by pygsti.tools.internalgates.get_standard_gatename_unitaries()
, and note that there is redundency in this set. E.g., 'Gi' is a identity gate but so is 'Gc0' (as one of the 24 1-qubit Cliffords named as 'Gci' for i = 0, 1, 2, ...).
gate_names = ['Gi', # The idle gate
'Gxpi2', # A X rotation by pi/2
'Gypi2', # A Y rotation by pi/2
'Gzpi2', # A Z rotation by pi/2
'Gh', # The Hadamard gate
'Gcphase'] # The controlled-Z gate.
Additionally, we can define gates with user-specified names and actions, via a dictionary with keys that are strings (gate names) and values that are unitary matrices. For example, if you want to call the hadamard gate 'Ghad' we could do this here. The gate names should all start with a 'G', but are otherwise unrestricted. Here we'll leave this dictionary empty.
nonstd_gate_unitaries = {}
Specify the "availability" of gates: which qubits they can be applied to. When not specified for a gate, it is assumed that it can be applied to all dimension-appropriate sets of qubits. E.g., a 1-qubit gate will be assumed to be applicable to each qubit; a 2-qubit gate will be assumed to be applicable to all ordered pairs of qubits, etc.
Let's make our device have ring connectivity:
availability = {'Gcphase':[('Q0','Q1'),('Q1','Q2'),('Q2','Q3'),('Q3','Q0')]}
We then create a ProcessorSpec
by handing it all of this information. This then generates a variety of auxillary information about the device from this input (e.g., optimal compilations for the Pauli operators and CNOT). The defaults here that haven't been specified will be ok for most purposes. But sometimes they will need to be changed to avoid slow ProcessorSpec initialization - fixes for these issues will likely be implemented in the future.
pspec = pygsti.obj.ProcessorSpec(nQubits, gate_names, nonstd_gate_unitaries=nonstd_gate_unitaries,
availability=availability, qubit_labels=qubit_labels)
ProcessorSpec
objects are not particularly useful on their own. Currently, they are mostly used for interfacing with Circuit
objects, in-built compilation algorithms, and the randomized benchmarking code. However, in the future we expect that they will be used for constructing circuits/gatestrings for other multi-qubit QCVV methods in pyGSTi
.
Circuit
object¶We now introduce the Circuit
objects. These are more a more structured version of a GateString
object, and these objects can be easily converted between each other.
First, we'll demonstrate different ways to initialize a circuit. Whenever you initialize a circuit it is necessary to specify the lines/wires/qubits the circuit is over. To do this either specify:
line_labels
list, which is names for the wires in the circuit.num_lines
, in which case the line labels default to integers starting at 0.We'll make a 2-qubit circuit and name our qubits 'Q0' and 'Q1'.
line_labels=['Q0','Q1']
Circuits do not know what the gates they contain are, in the sense that they do not know what unitaries the things in the circuit correpsond to, with one exception: they are initialized to know that a particular string corresponds to an identity/idle gate. This defaults to the 'I' string, but it can be useful to specify this as something else: often the idle identifier of a ProcessorSpec
, as we do here.
identity=pspec.identity
We can then initialize an empty circuit:
circuit = pygsti.obj.Circuit(line_labels=line_labels, identity=pspec.identity)
We can print out a circuit in a basic string format.
print(circuit)
Qubit Q0 ----- Qubit Q1 -----
We can find out various basic properties of the circuit.
print("The circuit size is = {}".format(circuit.size()))
print("The circuit depth is = {}".format(circuit.depth()))
print("The circuit multi-qubit-gate count is = {}".format(circuit.multiQgate_count()))
The circuit size is = 0 The circuit depth is = 0 The circuit multi-qubit-gate count is = 0
A circuit is essentially just a load of Label
objects, that specify what gate is applied to each wire at each step. So to specify a non-empty circuit from scratch it is useful to import the Label
object.
from pygsti.baseobjs.label import Label as L # A shorthand for a Label
A Label
is basically just a string, corresponding to a gate name (e.g., 'Gcnot'), and a tuple, corresponding to the qubits the gate acts on. We can initialize a label by specifying these things:
label_for_cnot_from_Q0_to_Q1 = L('Gcnot',('Q0','Q1'))
print("The gate's name: ", label_for_cnot_from_Q0_to_Q1.name)
print("The qubits the gate acts on: ", label_for_cnot_from_Q0_to_Q1.qubits)
The gate's name: Gcnot The qubits the gate acts on: ('Q0', 'Q1')
Using labels, we can initializing a non-empty circuit. Below we create a circuit consisting of Hadamard gates and a controlled-Z gate over the two qubits 'Q0' and 'Q1'. For obvious reasons, the gates (i.e., the Label
objects) in this list must act on the qubits in the circuit.
gatestring = [L('Gh','Q0'),L('Gh','Q1'),L('Gcphase',('Q0','Q1')),L('Gh','Q0'),L('Gh','Q1')]
From this gatestring list there is more than one way to create a circuit. By parallelizing the gates, or by doing them in sequence. Implementing the gates in sequence is the default.
circuit1 = pygsti.obj.Circuit(gatestring=gatestring, line_labels=['Q0','Q1'], identity=pspec.identity)
print("A circuit created from a gatestring *without* parallelizing:",end='\n\n')
print(circuit1)
print("The circuit size is = {}".format(circuit1.size()))
print("The circuit depth is = {}".format(circuit1.depth()))
print("The circuit multi-qubit-gate count is = {}".format(circuit1.multiQgate_count()),end='\n\n')
A circuit created from a gatestring *without* parallelizing: Qubit Q0 ---|Gh|-| |-|●Q1|-|Gh|-| |--- Qubit Q1 ---| |-|Gh|-|●Q0|-| |-|Gh|--- The circuit size is = 6 The circuit depth is = 5 The circuit multi-qubit-gate count is = 1
circuit2 = pygsti.obj.Circuit(gatestring=gatestring, line_labels=['Q0','Q1'], parallelize=True, identity=pspec.identity)
print("A circuit created from a gatestring *with* parallelizing:",end='\n\n')
print(circuit2)
print("The circuit size is = {}".format(circuit2.size()))
print("The circuit depth is = {}".format(circuit2.depth()))
print("The circuit multi-qubit-gate count is = {}".format(circuit2.multiQgate_count()))
A circuit created from a gatestring *with* parallelizing: Qubit Q0 ---|Gh|-|●Q1|-|Gh|--- Qubit Q1 ---|Gh|-|●Q0|-|Gh|--- The circuit size is = 6 The circuit depth is = 3 The circuit multi-qubit-gate count is = 1
To create a gatestring that can be converted to a circuit with an unambigious layer structure, these layers can be put into the gatestring as lists or tuples and parallelize
is left as it's default (False).
gatestring = [[L('Gh','Q0'),L('Gh','Q1')],[L('Gcphase',('Q0','Q1')),],[L('Gh','Q0'),],[L('Gh','Q1'),]]
circuit3 = pygsti.obj.Circuit(gatestring=gatestring, line_labels=['Q0','Q1'], identity=pspec.identity)
print("A circuit created from a gatestring *with* explicit layers:",end='\n\n')
print(circuit3)
print("The circuit size is = {}".format(circuit2.size()))
print("The circuit depth is = {}".format(circuit2.depth()))
print("The circuit multi-qubit-gate count is = {}".format(circuit2.multiQgate_count()))
A circuit created from a gatestring *with* explicit layers: Qubit Q0 ---|Gh|-|●Q1|-|Gh|-| |--- Qubit Q1 ---|Gh|-|●Q0|-| |-|Gh|--- The circuit size is = 6 The circuit depth is = 3 The circuit multi-qubit-gate count is = 1
Circuits can be saved in an unambigious string format using [...] to enclose each layer of the circuit. See the format in the imported file below for an example. We can import circuits from a text file, as we now demonstrate.
Before doing so, we create a ProcessorSpec
that will correspond to the device these circuits we're importing are for: they are 5-qubit circuits, containing 'Gcnot' gates (CNOT gates) and 'Gci' gates for i = 0, 1, 2, ..., which denote the 24 1-qubit Clifford gates.
nQubits = 5
gate_names = ['Gc'+format(i) for i in range(24)] + ['Gcnot']
pspec2 = pygsti.obj.ProcessorSpec(nQubits, gate_names)
We first import the circuits as a list of GateString
objects.
gsList = pygsti.io.load_gatestring_list("tutorial_files/MyCircuits.txt")
We then turn them into circuit using the same method as above. It is still necessary to specify the line-labels and the identity element (if you want the circuit to know what the identity gate's name is), and these currently can't be stored in the same file as the circuits and auto-imported.
line_labels = [0,1,2,3,4]
identity=pspec2.identity
circuitList = [pygsti.obj.Circuit(gatestring=gs, line_labels=line_labels, identity=identity) for gs in gsList]
Let's look at the imported circuits
for c in circuitList:
print(c)
Qubit 0 ----- Qubit 1 ----- Qubit 2 ----- Qubit 3 ----- Qubit 4 ----- Qubit 0 ---|Gc11|-| ⊕1 |-|Gc12|--- Qubit 1 ---|Gc18|-| ●0 |-| ●2 |--- Qubit 2 ---|Gc12|-|Gc22|-| ⊕1 |--- Qubit 3 ---| ⊕4 |-|Gc23|-|Gc16|--- Qubit 4 ---| ●3 |-|Gc22|-|Gc21|--- Qubit 0 ---|Gc6|-|Gc4 |-|Gc5 |-|⊕1 |-|Gc15|-| ⊕1 |-|Gc13|--- Qubit 1 ---| |-|Gc20|-|Gc23|-|●0 |-| ●2 |-| ●0 |-|Gc12|--- Qubit 2 ---|●3 |-|Gc11|-|Gc12|-|Gc9|-| ⊕1 |-|Gc12|-|Gc13|--- Qubit 3 ---|⊕2 |-|Gc13|-| ⊕4 |-|Gc2|-|Gc11|-| |-| ⊕4 |--- Qubit 4 ---|Gc9|-|Gc17|-| ●3 |-|Gc1|-|Gc2 |-|Gc13|-| ●3 |---
In contrast to GateString
objects, circuits are meant to be easily editable objects (at least until they are made static). There are only very simple methods for manipulating generic circuits, a couple of which are outlined below.
We can do depth compression, whereby neighbouring 1-qubit gates are combined using the specified pair-wise relations, all gates are shifted as far forward as possible, and idle layers are deleted.
clifford_circuit = circuitList[2]
print("The circuit *before* depth-compression using the 1-qubit gate pair-wise relations:",end='\n\n')
print(clifford_circuit)
clifford_circuit.compress_depth(oneQgate_relations=pspec2.oneQgate_relations)
print("The circuit *after* depth-compression using the 1-qubit gate pair-wise relations:",end='\n\n')
print(clifford_circuit)
The circuit *before* depth-compression using the 1-qubit gate pair-wise relations: Qubit 0 ---|Gc6|-|Gc4 |-|Gc5 |-|⊕1 |-|Gc15|-| ⊕1 |-|Gc13|--- Qubit 1 ---| |-|Gc20|-|Gc23|-|●0 |-| ●2 |-| ●0 |-|Gc12|--- Qubit 2 ---|●3 |-|Gc11|-|Gc12|-|Gc9|-| ⊕1 |-|Gc12|-|Gc13|--- Qubit 3 ---|⊕2 |-|Gc13|-| ⊕4 |-|Gc2|-|Gc11|-| |-| ⊕4 |--- Qubit 4 ---|Gc9|-|Gc17|-| ●3 |-|Gc1|-|Gc2 |-|Gc13|-| ●3 |--- The circuit *after* depth-compression using the 1-qubit gate pair-wise relations: Qubit 0 ---| |-| ⊕1 |-|Gc15|-| ⊕1 |-|Gc13|--- Qubit 1 ---|Gc6 |-| ●0 |-| ●2 |-| ●0 |-|Gc12|--- Qubit 2 ---| ●3 |-|Gc13|-| ⊕1 |-|Gc1 |-| |--- Qubit 3 ---| ⊕2 |-|Gc13|-| ⊕4 |-|Gc4 |-| ⊕4 |--- Qubit 4 ---|Gc20|-| |-| ●3 |-|Gc13|-| ●3 |---
Circuits have methods for things such as appending a circuit, insert a gate, changing gate library, adding idle wires, etc. Below we demonstrate inserting a layer.
clifford_circuit.insert_layer([L('Gcnot',(0,1)),],1)
print(clifford_circuit)
Qubit 0 ---| |-|●1 |-| ⊕1 |-|Gc15|-| ⊕1 |-|Gc13|--- Qubit 1 ---|Gc6 |-|⊕0 |-| ●0 |-| ●2 |-| ●0 |-|Gc12|--- Qubit 2 ---| ●3 |-| |-|Gc13|-| ⊕1 |-|Gc1 |-| |--- Qubit 3 ---| ⊕2 |-| |-|Gc13|-| ⊕4 |-|Gc4 |-| ⊕4 |--- Qubit 4 ---|Gc20|-| |-| |-| ●3 |-|Gc13|-| ●3 |---
Circuit
objects can be easily converted to OpenQasm or Quil strings, using the convert_to_openqasm()
and convert_to_quil()
methods. This conversion is automatic for circuits that containing only gates with name that are in-built into pyGSTi
(see earlier and the docstring of pygsti.tools.internalgates.get_standard_gatename_unitaries()
). This is with some exceptions in the case of Quil: currently not all of the in-built gate names can be converted to quil gate names automatically, but this will be fixed in the future.
For other gate names (or even more crucially, if you have re-purposed any of the gate names that pyGSTi
knows for a different unitary), the desired gate name conversation must be specified as an optional argument for both convert_to_openqasm()
and convert_to_quil()
.
Circuits with qubit labels of the form 'Qi' or with integer labels are auto-converted to the corresponding integer. If this labelling convention is used but the mapping should be different, or if the qubit labelling in the circuit is not of one of these two forms, this mapping should also be handed to these conversion methods.
openqasm = circuit2.convert_to_openqasm()
print(openqasm)
OPENQASM 2.0; include "qelib1.inc"; qreg q[2]; creg cr[2]; h q[0]; h q[1]; barrier q[0], q[1]; cz q[0], q[1]; barrier q[0], q[1]; h q[0]; h q[1]; barrier q[0], q[1]; measure q[0] -> cr[0]; measure q[1] -> cr[1];
pyGSTi
contains a range of simulators, which can be used to simulate circuits. First, let's pick a circuit to simulate.
clifford_circuit = circuitList[2]
Any GateSet
object contains methods for generating probabilities from a string of gates, and these simulators can be applied to a circuit using the simulate()
method of circuit.
Error-free simulations of circuits can be achieved by passing the circuit one of the models contained within a ProcessorSpec
, which are auto-generated when a ProcessorSpec
is initialized. Simulations for imperfect gates require the user to build their own imperfect gateset (although there are also Pauli-error circuit simulators in the RB code).
Here we'll do a perfect-circuit (efficient-in-qubit-number) Clifford simulation, using the 'clifford' model that is, by default, part of a ProcessorSpec.
gateset = pspec2.models['clifford']
Once we have a GateSet
, the simulation is easy:
out = clifford_circuit.simulate(gateset=gateset)
The output is simply the outcome probabilities:
print(out)
OutcomeLabelDict([(('00100',), 0.12499999999999994), (('00111',), 0.12499999999999994), (('01100',), 0.12499999999999994), (('01111',), 0.12499999999999994), (('10100',), 0.12499999999999994), (('10111',), 0.12499999999999994), (('11100',), 0.12499999999999994), (('11111',), 0.12499999999999994)])