The Quantum Computer

PyQuil is used to build Quil (Quantum Instruction Language) programs and execute them on simulated or real quantum devices. Quil is an opinionated quantum instruction language: its basic belief is that in the near term quantum computers will operate as coprocessors, working in concert with traditional CPUs. This means that Quil is designed to execute on a Quantum Abstract Machine (QAM) that has a shared classical/quantum architecture at its core.

A QAM must, therefore, implement certain abstract methods to manipulate classical and quantum states, such as loading programs, writing to shared classical memory, and executing programs.

The program execution itself is sent from pyQuil to quantum computer endpoints, which will be one of two options:

  • A Rigetti Quantum Virtual Machine (QVM)
  • A Rigetti Quantum Processing Unit (QPU)

Within pyQuil, there is a QVM object and a QPU object which use the exposed APIs of the QVM and QPU servers, respectively.

On this page, we’ll learn a bit about the QVM and QPU. Then we will show you how to use them from pyQuil with a The QuantumComputer.

For information on constructing quantum programs, please refer back to Programs and Gates.

The Quantum Virtual Machine (QVM)

The Rigetti Quantum Virtual Machine is an implementation of the Quantum Abstract Machine from A Practical Quantum Instruction Set Architecture. [1] It is implemented in ANSI Common LISP and executes programs specified in Quil.

The QVM is a wavefunction simulation of unitary evolution with classical control flow and shared quantum classical memory.

The QVM is part of the Forest SDK, and it’s available for you to use on your local machine. After downloading and installing the SDK, you can initialize a local QVM server by typing qvm -S into your terminal. You should see the following message.

$ qvm -S
******************************
* Welcome to the Rigetti QVM *
******************************
Copyright (c) 2018 Rigetti Computing.

(Configured with 2048 MiB of workspace and 8 workers.)

[2018-11-06 18:18:18] Starting server on port 5000.

By default, the server is started on port 5000 on your local machine. Consequently, the endpoint which the pyQuil QVM will default to for the QVM address is "http://127.0.0.1:5000". When you run your program, a pyQuil client will send a Quil program to the QVM server and wait for a response back.

It’s also possible to use the QVM from the command line. You can write a Quil program in its own file:

# example.quil

DECLARE ro BIT[1]
RX(pi/2) 0
CZ 0 1

and then execute it with the QVM directly from the command line:

$ qvm -e < example.quil

[2018-11-30 11:13:58] Reading program.
[2018-11-30 11:13:58] Allocating memory for QVM of 2 qubits.
[2018-11-30 11:13:58] Allocation completed in 4 ms.
[2018-11-30 11:13:58] Loading quantum program.
[2018-11-30 11:13:58] Executing quantum program.
[2018-11-30 11:13:58] Execution completed in 6 ms.
[2018-11-30 11:13:58] Printing 2-qubit state.
[2018-11-30 11:13:58] Amplitudes:
[2018-11-30 11:13:58]   |00>: 0.0, P=  0.0%
[2018-11-30 11:13:58]   |01>: 0.0-1.0i, P=100.0%
[2018-11-30 11:13:58]   |10>: 0.0, P=  0.0%
[2018-11-30 11:13:58]   |11>: 0.0, P=  0.0%
[2018-11-30 11:13:58] Classical memory (low -> high indexes):
[2018-11-30 11:13:58]     ro:  1 0

For a detailed description of how to use the qvm from the command line, see The QVM manual page or type man qvm in your terminal.

We also offer a Wavefunction Simulator (formerly a part of the QVM object), which allows users to contruct and inspect wavefunctions of quantum programs. Learn more about the Wavefunction Simulator here.

The Quantum Processing Unit (QPU)

To access a QPU endpoint, you will have to sign up for Quantum Cloud Services (QCS). Documentation for getting started with your Quantum Machine Image (QMI) is found here. Using QCS, you will ssh into your QMI, and reserve a QPU lattice for a particular time block.

When your reservation begins, you will be authorized to access the QPU. A configuration file will be automatically populated for you with the proper QPU endpoint for your reservation. Both your QMI and the QPU are located on premises, giving you low latency access to the QPU server. That server accepts jobs in the form of ``BinaryExecutableRequest``s, which is precisely what you get back when you compile your program in pyQuil and target the QPU (more on this soon). This request contains all the information necessary to run your program on the control rack which sends and receives waveforms from the QPU, so that you can receive classified readout results (``0``s and ``1``s).

For information on available lattices, you can check out your dashboard at https://qcs.rigetti.com/dashboard after you’ve been invited to QCS.

The QuantumComputer

The QuantumComputer abstraction offered by pyQuil provides an easy access point to the most critical objects used in pyQuil for building and executing your quantum programs. We will cover the main methods and attributes on this page. The QuantumComputer API Reference provides a reference for all of its methods and options.

At a high level, the QuantumComputer wraps around our favorite quantum computing tools:

  • A quantum abstract machine .qam : this is our general purpose quantum computing device, which implements the required abstract methods described above. It is implemented as a QVM or QPU object in pyQuil.
  • A compiler .compiler : this determines how we manipulate the Quil input to something more efficient when possible, and then into a form which our QAM can accept as input.
  • A device .device : this specifies the topology and Instruction Set Architecture (ISA) of the targeted device by listing the supported 1Q and 2Q gates.

When you instantiate a QuantumComputer instance, these subcomponents will be compatible with each other. So, if you get a QPU implementation for the .qam, you will have a QPUCompiler for the .compiler, and your .device will match the device used by the .compiler.

The QuantumComputer instance makes methods available which are built on the above objects. If you need more fine grained controls for your work, you might try exploring what is offered by these objects.

For more information on each of the above, check out the following pages:

Instantiation

A decent amount of information needs to be provided to initialize the compiler, device, and qam attributes, much of which is already in your config files (or provided reasonable defaults when running locally). Typically, you will want a QuantumComputer which either:

  • pertains to a real, available QPU device
  • is a QVM but mimics the topology of a QPU
  • is some generic QVM

All of this can be accomplished with get_qc().

def get_qc(name: str, *, as_qvm: bool = None, noisy: bool = None,
           connection: ForestConnection = None) -> QuantumComputer:
from pyquil import get_qc

# Get a QPU
qc = get_qc(QPU_LATTICE_NAME)  # QPU_LATTICE_NAME is just a string naming the device

# Get a QVM with the same topology as the QPU lattice
qc = get_qc(QPU_LATTICE_NAME, as_qvm=True)
# or, equivalently
qc = get_qc(f"{QPU_LATTICE_NAME}-qvm")

# A fully connected QVM
number_of_qubits = 10
qc = get_qc(f"{number_of_qubits}q-qvm")

For now, you will have to join QCS to get QPU_LATTICE_NAME by running the qcs lattices command from your QMI. Access to the QPU is only possible from a QMI, during a booked reservation. If this sounds unfamiliar, check out our documentation for QCS and join the waitlist.

For more information about creating and adding your own noise models, check out Noise and Quantum Computation.

Note

When connecting to a QVM locally (such as with get_qc(..., as_qvm=True)) you’ll have to set up the QVM in server mode.

Methods

Now that you have your qc, there’s a lot you can do with it. Most users will want to use compile, run or run_and_measure, and qubits very regularly. The general flow of use would look like this:

from pyquil import get_qc, Program
from pyquil.gates import *

qc = get_qc('9q-square-qvm')            # not general to any number of qubits, 9q-square-qvm is special

qubits = qc.qubits()                    # this information comes from qc.device
p = Program()
# ... build program, potentially making use of the qubits list

compiled_program = qc.compile(p)        # this makes multiple calls to qc.compiler

results = qc.run(compiled_program)      # this makes multiple calls to qc.qam

Note

In addition to a running QVM server, you will need a running quilc server to compile your program. Setting up both of these is very easy, as explained here.

The .run_and_measure(...) method

This is the most high level way to run your program. With this method, you are not responsible for compiling your program before running it, nor do you have to specify any MEASURE instructions; all qubits will get measured.

from pyquil import Program, get_qc
from pyquil.gates import X

qc = get_qc("8q-qvm")

p = Program(X(0))

results = qc.run_and_measure(p, trials=5)
print(results)

trials specifies how many times to run this program. Let’s see our results:

{0: array([1, 1, 1, 1, 1]),
 1: array([0, 0, 0, 0, 0]),
 2: array([0, 0, 0, 0, 0]),
 3: array([0, 0, 0, 0, 0]),
 4: array([0, 0, 0, 0, 0]),
 5: array([0, 0, 0, 0, 0]),
 6: array([0, 0, 0, 0, 0]),
 7: array([0, 0, 0, 0, 0])}

The return value is a dictionary from qubit index to results for all trials. Every qubit in the lattice is measured for you, and as expected, qubit 0 has been flipped to the excited state for each trial.

The .run(...) method

The lower-level .run(...) method gives you more control over how you want to build and compile your program than .run_and_measure does. You are responsible for compiling your program before running it. The above program would be written in this way to execute with run:

from pyquil import Program, get_qc
from pyquil.gates import X, MEASURE

qc = get_qc("8q-qvm")

p = Program()
ro = p.declare('ro', 'BIT', 1)
p += X(0)
p += MEASURE(0, ro[0])
p += MEASURE(1, ro[1])
p.wrap_in_numshots_loop(5)

executable = qc.compile(p)
bitstrings = qc.run(executable)  # .run takes in a compiled program, unlike .run_and_measure
print(bitstrings)

By specifying MEASURE ourselves, we will only get the results that we are interested in. To be completely equivalent to the previous example, we would have to measure all eight qubits.

The results returned is a list of lists of integers. In the above case, that’s

[[1, 0], [1, 0], [1, 0], [1, 0], [1, 0]]

Let’s unpack this. The outer list is an enumeration over the trials; the argument given to wrap_in_numshots_loop will match the length of results.

The inner list, on the other hand, is an enumeration over the results stored in the memory region named ro, which we use as our readout register. We see that the result of this program is that the memory region ro[0] now stores the state of qubit 0, which should be 1 after an \(X\)-gate. See Declaring Memory and Measurement for more details about declaring and accessing classical memory regions.

Tip

Get the results for qubit 0 with numpy.array(bitstrings)[:,0].

Providing Your Own Device Topology

It is simple to provide your own device topology as long as you can give your qubits each a number, and specify which edges exist. Here is an example, using the topology of our 16Q chip (two octagons connected by a square):

import networkx as nx

from pyquil.device import NxDevice, gates_in_isa
from pyquil.noise import decoherence_noise_with_asymmetric_ro

qubits = [0, 1, 2, 3, 4, 5, 6, 7, 10, 11, 12, 13, 14, 15, 16, 17]  # qubits are numbered by octagon
edges = [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 7), (7, 0),  # first octagon
         (1, 16), (2, 15),  # connections across the square
         (10, 11), (11, 12), (13, 14), (14, 15), (15, 16), (16, 17), (10, 17)] # second octagon

# Build the NX graph
topo = nx.from_edgelist(edges)
# You would uncomment the next line if you have disconnected qubits
# topo.add_nodes_from(qubits)
device = NxDevice(topo)
device.noise_model = decoherence_noise_with_asymmetric_ro(gates_in_isa(device.get_isa()))  # Optional

Now that you have your device, you could set qc.device and qc.compiler.device to point to your new device, or use it to make new objects.

Simulating the QPU using the QVM

The QAM methods are intended to be used in the same way, whether a QVM or QPU is being targeted. Everywhere on this page, you can swap out the type of the QAM (QVM <=> QPU) and you will still get reasonable results back. As long as the topology of the devices are the same, programs compiled and ran on the QVM will be able to run on the QPU and visa-versa. Since QuantumComputer is built on the QAM abstract class, its methods will also work for both QAM implementations.

This makes the QVM a powerful tool for testing quantum programs before executing them on the QPU.

qpu = get_qc(QPU_LATTICE_NAME)
qvm = get_qc(QPU_LATTICE_NAME, as_qvm=True)

By simply providing as_qvm=True, we get a QVM which will have the same topology as the named QPU. It’s a good idea to run your programs against the QVM before booking QPU time to iron out bugs. To learn more about how to add noise models to your virtual QuantumComputer instance, check out Noise and Quantum Computation.

In the next section, we will see how to use the Wavefunction Simulator aspect of the Rigetti QVM to inspect the full wavefunction set up by a Quil program.

[1]https://arxiv.org/abs/1608.03355