New in Forest 2 - QuantumComputer

PyQuil is for constructing and running quantum programs on real quantum computers. With the release of pyQuil 2, we have changed parts of the API to better reflect that focus. Instead of swapping between a QVMConnection and a QPUConnection, you will primarily deal with a QuantumComputer with consistent API and behavior regardless of

  • QVM / QPU
  • Presence of noise model
  • Device topology

Running a program

Let's show how you can run a simple program on a QuantumComputer first we start with the relevant imports.


In [1]:
from pyquil import Program
from pyquil.gates import *

We'll write a function that takes a list of qubits and returns a pyQuil Program that constructs an entangled "GHZ" state. This is a generalization of the two-qubit Bell state.


In [2]:
def ghz_state(qubits):
    """Create a GHZ state on the given list of qubits by applying
    a Hadamard gate to the first qubit followed by a chain of CNOTs
    """
    program = Program()
    program += H(qubits[0])
    for q1, q2 in zip(qubits, qubits[1:]):
        program += CNOT(q1, q2)
    return program

For example, creating a GHZ state on qubits 1, 2, and 3 would look like:


In [3]:
program = ghz_state(qubits=[0, 1, 2])
print(program)


H 0
CNOT 0 1
CNOT 1 2

Debugging with WavefunctionSimulator

We can check that this program gives us the desired wavefunction by using WavefunctionSimulator.wavefunction()


In [4]:
from pyquil.api import WavefunctionSimulator
wfn = WavefunctionSimulator().wavefunction(program)
print(wfn)


(0.7071067812+0j)|000> + (0.7071067812+0j)|111>

We can't get the wavefunction from a real quantum computer though, so instead we'll sample bitstrings. We expect to always measure the bitstring 000 or the bitstring 111 based on the definition of a GHZ state and confirmed by our wavefunction simulation.

get_qc

We'll construct a QuantumComputer via the helper method get_qc. You may be tempted to use the QuantumComputer constructor directly. Please refer to the advanced documentation to see how to do that. Our program uses 3 qubits, so we'll ask for a 3-qubit QVM.


In [5]:
from pyquil import get_qc
qc = get_qc('3q-qvm')
qc


Out[5]:
QuantumComputer[name="3q-qvm"]

We can do a quick check to make sure it has 3 qubits


In [6]:
qc.qubits()


Out[6]:
[0, 1, 2]

Sampling with run_and_measure

QuantumComputer.run_and_measure will run a given program (that does not have explicit MEASURE instructions) and then measure all qubits present in the quantum computer.


In [7]:
bitstrings = qc.run_and_measure(program, trials=10)
bitstrings


Out[7]:
{0: array([1, 0, 0, 1, 1, 1, 1, 0, 1, 0]),
 1: array([1, 0, 0, 1, 1, 1, 1, 0, 1, 0]),
 2: array([1, 0, 0, 1, 1, 1, 1, 0, 1, 0])}

Let's programatically verify that we always measure 000 or 111 by "summing" each bitstring and checking if it's eather 0 (for 000) or 3 (for 111)


In [8]:
import numpy as np
bitstring_array = np.vstack(bitstrings[q] for q in qc.qubits()).T
sums = np.sum(bitstring_array, axis=1)
sums


Out[8]:
array([3, 0, 0, 3, 3, 3, 3, 0, 3, 0])

In [9]:
sample_is_ghz = np.logical_or(sums == 0, sums == 3)
sample_is_ghz


Out[9]:
array([ True,  True,  True,  True,  True,  True,  True,  True,  True,
        True])

In [10]:
np.all(sample_is_ghz)


Out[10]:
True

Change alert: run_and_measure will return a dictionary of 1d bitstrings.

Not a 2d array. To demonstrate why, consider a lattice whose qubits are not contiguously indexed from 0.


In [11]:
# TODO: we need a lattice that is not zero-indexed
# qc = get_qc('Aspen-0-3Q-B')
# qc.run_and_measure(ghz_state(qubits=[1,2,3]))

Change alert: All qubits are measured

PyQuil 1.x's run_and_measure would only measure qubits used in the given program. Now all qubits (per qc.qubits()) are measured. This is easier to reason about and reflects the reality of running on a QPU. When accounting for noise or when running QCVV tasks, you may be interested in the measurement results of qubits that weren't even used in your program!


In [12]:
qc = get_qc('4q-qvm')
bitstrings = qc.run_and_measure(Program(X(0), X(1), X(2)), trials=10)
bitstrings


Out[12]:
{0: array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1]),
 1: array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1]),
 2: array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1]),
 3: array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])}

You can drop qubits you're not interested in by indexing into the returned dictionary


In [13]:
# Stacking everything
np.vstack(bitstrings[q] for q in qc.qubits()).T


Out[13]:
array([[1, 1, 1, 0],
       [1, 1, 1, 0],
       [1, 1, 1, 0],
       [1, 1, 1, 0],
       [1, 1, 1, 0],
       [1, 1, 1, 0],
       [1, 1, 1, 0],
       [1, 1, 1, 0],
       [1, 1, 1, 0],
       [1, 1, 1, 0]])

In [14]:
# Stacking what you want (contrast with above)
qubits = [0, 1, 2]
np.vstack(bitstrings[q] for q in qubits).T


Out[14]:
array([[1, 1, 1],
       [1, 1, 1],
       [1, 1, 1],
       [1, 1, 1],
       [1, 1, 1],
       [1, 1, 1],
       [1, 1, 1],
       [1, 1, 1],
       [1, 1, 1],
       [1, 1, 1]])

Change alert: run_and_measure works with noise models now.

In pyQuil 1.x, run_and_measure would not work with noise models. Now noise models are supported. Pre-configured noise models can be used via get_qc('xxxx-noisy-qvm').

As a consequence, run_and_measure for large numbers of trials will be slower in Pyquil 2.


In [15]:
qc = get_qc('3q-noisy-qvm')
bitstrings = qc.run_and_measure(program, trials=10)
bitstrings


Out[15]:
{0: array([0, 1, 1, 0, 0, 1, 1, 0, 1, 1]),
 1: array([0, 1, 1, 0, 0, 1, 1, 0, 1, 1]),
 2: array([0, 1, 1, 0, 0, 1, 1, 0, 1, 1])}

In [16]:
bitstring_array = np.vstack(bitstrings[q] for q in qc.qubits()).T
sums = np.sum(bitstring_array, axis=1)
sums


Out[16]:
array([0, 3, 3, 0, 0, 3, 3, 0, 3, 3])

In [17]:
# Noise means now we measure things other than 000 or 111
np.all(np.logical_or(sums == 0, sums == 3))


Out[17]:
True

list_quantum_computers

You can find all possible arguments to get_qc with list_quantum_computers


In [18]:
from pyquil import list_quantum_computers
# TODO: unauthenticated endpoint
# list_quantum_computers()

QuantumComputers have a topology

An important restriction when running on a real quantum computer is the mapping of qubits to the supported two-qubit gates. The QVM is designed to provide increasing levels of "realism" to guarantee that if your program executes successfully on get_qc("Aspen-xxx-noisy-qvm") then it will execute successfully on get_qc("Aspen-xxx")*

* guarantee not currently guaranteed. This is a work in progress.

Inspecting the topology

You can access a topology by qc.qubit_topology(), which will return a NetworkX representation of qubit connectivity. You can access the full set of supported instructions by qc.get_isa(). For example, we include a generic QVM named "9q-square-qvm" that has a square topology.


In [19]:
qc = get_qc('9q-square-qvm')
%matplotlib inline
import networkx as nx
nx.draw(qc.qubit_topology())
from matplotlib import pyplot as plt
_ = plt.title('9q-square-qvm', fontsize=18)


What If I don't want a topology?

WavefunctionSimulator still has no notion of qubit connectivity, so feel free to use that for simulating quantum algorithms that you aren't concerned about running on an actual QPU.

Above we used get_qc("3q-qvm"), "4q-qvm", and indeed you can do any "{n}q-qvm" (subject to computational resource constraints). These QVM's are constructed with a topology! It just happens to be fully connected


In [20]:
nx.draw(get_qc('5q-qvm').qubit_topology())
_ = plt.title('5q-qvm is fully connected', fontsize=16)


Heirarchy of realism

  • WavefunctionSimulator to debug algorithm
  • get_qc("5q-qvm") to debug sampling
  • get_qc("9q-square-qvm") to debug mapping to a lattice
  • get_qc("9q-square-noisy-qvm") to debug generic noise characteristics
  • get_qc("Aspen-0-16Q-A-qvm") to debug mapping to a real lattice
  • get_qc("Aspen-0-16Q-A-noisy-qvm") to debug noise characteristics of a real device
  • get_qc("Aspen-0-16Q-A") to run on a real device

"What is a QuantumComputer?" Advanced Edition

A QuantumComputer is a wrapper around three constituent parts, each of which has a programatic interface that must be respected by all classes that implement the interface. By having clear interfaces we can write backend-agnostic methods on QuantumComputer and mix-and-match backing objects.

The following diagram shows the three objects that must be provided when constructing a QuantumComputer "by hand". The abstract classes are backed in grey with example implementing classes listed below. Please consult the api reference for details on each interface.

As an example, let's construct a 5-qubit QVM with one central node and only even numbered qubits.


In [21]:
topology = nx.from_edgelist([
    (10, 2),
    (10, 4),
    (10, 6),
    (10, 8),
])
from pyquil.device import NxDevice
device = NxDevice(topology)

from pyquil.api._qac import AbstractCompiler
class MyLazyCompiler(AbstractCompiler):
    def quil_to_native_quil(self, program):
        return program
    
    def native_quil_to_executable(self, nq_program):
        return nq_program
    
    
from pyquil.api import QuantumComputer, QVM, ForestConnection
my_qc = QuantumComputer(
    name='my-qvm',
    qam=QVM(connection=ForestConnection()),
    device=device,
    compiler=MyLazyCompiler(),
)

nx.draw(my_qc.qubit_topology())



In [22]:
my_qc.run_and_measure(Program(X(10)), trials=5)


Out[22]:
{2: array([0, 0, 0, 0, 0]),
 4: array([0, 0, 0, 0, 0]),
 6: array([0, 0, 0, 0, 0]),
 8: array([0, 0, 0, 0, 0]),
 10: array([1, 1, 1, 1, 1])}