The quantum computer

pyQuil is used to build Quil (Quantum Instruction Language) programs and execute them on simulated or real quantum processors. 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 Quantum Virtual Machine (QVM)

  • A 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 QuantumComputer object.

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

The Quantum Virtual Machine (QVM)

The 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.

As we learned in the pre-requisites the QVM is part of the Quil SDK, and it’s available for you to use on your local machine.

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

We also offer a wavefunction simulator, which allows users to contruct and inspect wavefunctions of quantum programs. You can 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 can be found here. Once you’ve been authorized to access a QPU you can submit requests to it using pyQuil.

For information on available QPUs, you can check out your 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 quantum processor .quantum_processor : this specifies the topology and Instruction Set Architecture (ISA) of the targeted processor 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 .quantum_processor will match the processor 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, quantum_processor, 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

  • 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, ...) -> QuantumComputer:
from pyquil import get_qc

QPU_NAME="Aspen-M-3"

# Get a QPU
# qc = get_qc(QPU_NAME)  # QPU_NAME is just a string naming the quantum_processor

# Get a QVM with the same topology as the QPU
# qc = get_qc(QPU_NAME, as_qvm=True)

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

As a reminder, you will have to join QCS to get access to a specific quantum processor. Check out our documentation for QCS and join the waitlist if you don’t have access already.

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

Note

This page just covers the essentials, but you can customize the behavior of compilation, execution and more using the various parameters on get_qc(), see the API documentation to see everything that is available.

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 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.quantum_processor
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 explained here.

The .run(...) method

When using the .run(...) method, you are responsible for compiling your program before running it. For example:

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

qc = get_qc("8q-qvm")

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

executable = qc.compile(p)
result = qc.run(executable)  # .run takes in a compiled program
bitstrings = result.get_register_map().get("ro")
print(bitstrings)

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].

In addition to readout data, the result of .run(...) includes other information about the job’s execution, such as the run duration. See QAMExecutionResult for details.

.execute and .get_result

The .run(...) method is itself a convenience wrapper around two other methods: .execute(...) and .get_result(...). run makes your program appear synchronous (request and then wait for the response), when in reality on some backends (such as a live QPU), execution is in fact asynchronous (request execution, then request results at a later time). For finer-grained control over your program execution process, you can use these two methods in place of .run. This is most useful when you want to execute work concurrently - for that, please see “Advanced Usage.”

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. For 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 topologies of the quantum processors are the same, programs compiled and run on the QVM will be able to run on the QPU and vice 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_NAME="Aspen-M-3"
qpu = get_qc(QPU_NAME)
qvm = get_qc(QPU_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.

Differences between a QVM and a QPU based QuantumComputer

As mentioned above, pyQuil is designed such that code based on a QuantumComputer can be used in more or less the same way, regardless of whether it is based on a QVM or QPU. However, depending on which you are using, the subcompoments have additional features worth knowing about.

For instance, if your code targets a QVM, qc.qam will be a QVM`` instance, and qc.compiler will be a QVMCompiler instance. However, if your code targets a QPU, qc.qam will be a QPU instance, and qc.compiler will be a QPUCompiler instance.

While these subcomponents follow common interfaces, namely QAM and AbstractCompiler, there may be some methods or properties that are accessible on the QPU-based instances but not on the QVM-based instances, and vice versa.

You can access these features and keep your code robust by performing type checks on qc.qam and/or qc.compiler. For example, if you wanted to refresh the calibration program, which only applies to QPU-based QuantumComputers, but still want a script that works for both QVM and QPU targets, you could do the following:

from pyquil import get_qc
from pyquil.api import QPUCompiler

qc = get_qc("2q-qvm")  # or "Aspen-M-3"

if isinstance(qc.compiler, QPUCompiler):
    # Working with a QPU - refresh calibrations
    qc.compiler.get_calibration_program(force_refresh=True)

Requesting a job cancellation

Jobs submitted to a QPU are queued for execution before they are run. While a job is pending execution, you can request that the job be cancelled with :py:meth:~pyquil.api.QPU#cancel. This functionality is unavailable on the QVM, so you can use a similar strategy to above to make sure the Quantum Abstract Machine (QAM) backing the QuantumComputer is indeed a QPU to safely call this method:

 from pyquil.api import QPU

job_handle = qc.qam.execute(p)

if isinstance(qc.qam, QPU):
     try:
         qc.qam.cancel(job_handle)
         print("Job was cancelled")
     except QpuApiError:
         # If this error was raised, then the job failed to be cancelled.
         result = qc.qam.get_result(job_handle)

Providing your own quantum processor topology

You can provide your own quantum processor topology by specifying qubits (as numeric indices) and edges. Here is an example that uses a subset of the instruction set architecture of a Rigetti QPU to specify a 16 qubit topology.

import networkx as nx
from pyquil import get_qc
from pyquil.quantum_processor import NxQuantumProcessor
from pyquil.noise import decoherence_noise_with_asymmetric_ro

qpu = get_qc("Aspen-M-3")
isa = qpu.to_compiler_isa()
qubits = sorted(int(k) for k in isa.qubits.keys())[:16]
edges = [(q1, q2) for q1 in qubits for q2 in qubits if f"{q1}-{q2}" in isa.edges]

# 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)
quantum_processor = NxQuantumProcessor(topo)
quantum_processor.noise_model = decoherence_noise_with_asymmetric_ro(quantum_processor.to_compiler_isa())  # Optional

Now that you have your quantum processor, you could set qc.compiler.quantum_processor to point to your new quantum processor, or use it to make new objects.