Multi-Qubit Devices: the ProcessorSpec object

This tutorial covers the creation and use of ProccesorSpec objects. These objects are used to define the "specification" of a quantum information processor (QIP) (e.g., device connectivity, the gate-set, etc.), and are particularly geared towards multi-qubit devices. Currently, these are mostly encountered in pyGSTi as an input for generating randomized benchmarking experiments, but they will be used more widely in future releases.

In [1]:
import pygsti

Using a 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:

  1. The number of qubits in the device, and, optionally, the labels of these qubits.

  2. 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. "Continuously parameterized" gates such as this may be supported in the future.

  3. The connectivity of the device.

So let's create a ProcessorSpec.

The number of qubits the device is for:

In [2]:
n_qubits = 4

Next, we pick some names for the qubits. These are akin to the line labels in a Circuit object (see the Circuit tutorial). Qubits are typically labelled by names beginning with "Q" or integers (if not specified, the qubit labels default to the integers $0, 1, 2, \ldots$). Here we choose:

In [3]:
qubit_labels = ['Q0','Q1','Q2','Q3']

Next, we 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, and note that there is redundency in this set. E.g., 'Gi' is a 1-qubit identity gate but so is 'Gc0' (as one of the 24 1-qubit Cliffords named as 'Gci' for i = 0, 1, 2, ...). Note that typically we do not specify an idle/identity gate as one of the primitives, unless there's a particular type of global-idle gate we're trying to model. (Specifying an idle gate may also be more appropriate for 1- and 2-qubit devices, since in these small-system cases we may label each circuit layer separatey.)

In [4]:
gate_names = ['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.

In [5]:
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:

In [6]:
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.

In [7]:
pspec = pygsti.obj.ProcessorSpec(num_qubits=n_qubits, gate_names=gate_names,
                                 availability=availability, qubit_labels=qubit_labels,
                                 construct_models=('clifford', 'target'))

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/circuits for other multi-qubit QCVV methods in pyGSTi.

Simulating circuits

When a ProcessorSpec is created, it creates (and contains) several models (Model objects) of device's behavior. These are contained in the .models member, which is a dictionary:

In [8]:
odict_keys(['clifford', 'target'])

So our pspec has two models, one labelled 'clifford', the other 'target'. Both of these are models of the perfect (noise-free) gates. (Models with imperfect gates require the user to build their own imperfect Model.)

As demonstrated toward the end of the Circuit tutorial, once we have a model simulating circuit outcomes is easy. Here we'll do a perfect-gates simulation, using the 'clifford' model (uses an efficient-in-qubit-number stabilizer-state propagation technique):

In [ ]:
model = pspec.models['clifford']
clifford_circuit = pygsti.obj.Circuit([ [('Gh','Q0'),('Gh','Q1'),('Gxpi2','Q3')],
                                         ('Gcphase','Q0','Q1'), ('Gcphase','Q1','Q2'),
# TODO: The following call crashes with error
# TypeError: Cannot convert pygsti.objects.replib.fastreplib.SBOpRepEmbedded to pygsti.objects.replib.fastreplib.DMOpRep
out = clifford_circuit.simulate(model)
print('\n'.join(['%s = %g' % (ol,p) for ol,p in out.items()]))
Qubit Q0 ---| Gh  |-|CQ1|-|   |-|Gh|---
Qubit Q1 ---| Gh  |-|CQ0|-|CQ2|-|Gh|---
Qubit Q2 ---|     |-|   |-|CQ1|-|  |---
Qubit Q3 ---|Gxpi2|-|   |-|   |-|  |---

TypeError                                 Traceback (most recent call last)
TypeError: Cannot convert pygsti.objects.replib.fastreplib.SBStateRep to pygsti.objects.replib.fastreplib.DMStateRep
Exception ignored in: 'pygsti.objects.replib.fastreplib.convert_rhoreps'
Traceback (most recent call last):
  File "/Users/sserita/Documents/repos/pyGSTi/pygsti/objects/", line 218, in _bulk_fill_probs_block
    replib.DM_mapfill_probs_block(self, array_to_fill, slice(0, array_to_fill.shape[0]),  # all indices
TypeError: Cannot convert pygsti.objects.replib.fastreplib.SBStateRep to pygsti.objects.replib.fastreplib.DMStateRep
TypeError                                 Traceback (most recent call last)
TypeError: Cannot convert pygsti.objects.replib.fastreplib.SBOpRepEmbedded to pygsti.objects.replib.fastreplib.DMOpRep

The keys of the outcome dictionary out are things like ('00',) instead of just '00' because of possible intermediate outcomes. See the Instruments tutorial if you're interested in learning more about intermediate outcomes. Note also that zero-probabilites are not included in out.keys().

If you're interested in creating imperfect models, see the tutorials on "explicit" models and "implicit" models. Note that if you're interested in simulating RB data there are separate Pauli-error circuit simulators within the pygsti.extras.rb package which take as input perfect models and produce noisy RB.

In [ ]: