Welcome to the Docs for the Forest SDK!Â¶
The Rigetti Forest Software Development Kit includes pyQuil, the Rigetti Quil Compiler (quilc), and the Quantum Virtual Machine (qvm).
Longtime users of Rigetti Forest will notice a few changes. First, the SDK now contains a downloadable compiler and a QVM. Second, the SDK contains pyQuil 2.0, with significant updates to previous versions. As a result, programs written using previous versions of the Forest toolkit will need to be updated to pyQuil 2.0 to be compatible with the QVM or compiler.
After installing the SDK and updating pyQuil in Installation and Getting Started, see the migration guide to get caught up on whatâ€™s new!
Quantum Cloud Services will provide users with a dedicated Quantum Machine Image, which will come prepackaged with the Forest SDK. Weâ€™re releasing a Preview to the Forest SDK now, so current users can begin migrating code (and share feedback with us early and often!). Longtime Forest users should start with the Migration Guide which outlines key changes in this SDK Preview release.
If youâ€™re new to Forest, we hope this documentation will provide everything you need to get up and running with the toolkit. Once youâ€™ve oriented yourself here, proceed to the section Installation and Getting Started to get started. If youâ€™re new to quantum computing, you also go to our section on Introduction to Quantum Computing. There, youâ€™ll learn the basic concepts needed to write quantum software. You can also work through an introduction to quantum computing in a jupyter notebook; launch the notebook from the source folder in pyquilâ€™s docs:
cd pyquil/docs/source
jupyter notebook intro_to_qc.ipynb
A few terms to orient you as you get started with Forest:
pyQuil: An open source Python library to help you write and run quantum programs. The source is hosted on github.
Quil: The Quantum Instruction Language standard. Instructions written in Quil can be executed on any implementation of a quantum abstract machine, such as the quantum virtual machine (QVM), or on a real quantum processing unit (QPU). More details regarding Quil can be found in the whitepaper, A Practical Quantum Instruction Set Architecture.
 QVM: The Quantum Virtual Machine is an open source implementation of a quantum abstract machine on
classical hardware. The QVM lets you use a regular computer to simulate a small quantum computer and execute Quil programs. Find QVM on GitHub.
QPU: Quantum processing unit. This refers to the physical hardware chip which we run quantum programs on.
Quil Compiler: The compiler,
quilc
, compiles Quil written for one quantum abstract machine (QAM) to another. Our open source compiler will take arbitrary Quil and compile it for the given QAM, according to its supported instruction set architecture. Find quilc on GitHub.Forest SDK: Our software development kit, optimized for nearterm quantum computers that operate as coprocessors, working in concert with traditional processors to run hybrid quantumclassical algorithms. For references on problems addressable with nearterm quantum computers, see Quantum Computing in the NISQ era and beyond.
Our flagship product Quantum Cloud Services offers users an onpremise, dedicated access point to our quantum computers. This access point is a fullyconfigured VM, which we call a Quantum Machine Image. A QMI is bundled with the same downloadable SDK mentioned above, and a command line interface (CLI), which is used for scheduling compute time on our quantum computers. To sign up for our waitlist, please click the link above. If youâ€™d like to access to our quantum computers for research, please email support@rigetti.com.
Note
To join our user community, connect to the Rigetti Slack workspace using this invite.
ContentsÂ¶
Installation and Getting StartedÂ¶
To make full use of the Rigetti Forest SDK, you will need pyQuil, the QVM, and the Quil Compiler. On this page, we will take you through the process of installing all three of these. We also step you through running a basic pyQuil program.
Note
If youâ€™re running from a Quantum Machine Image, installation has been completed for you. Continue to Getting Started.
Upgrading or Installing pyQuilÂ¶
PyQuil 2.0 is our library for generating and executing Quil programs on the Rigetti Forest platform.
Before you install, we recommend that you activate a Python 3.6+ virtual environment. Then, install pyQuil using pip:
pip install pyquil
For those of you that already have pyQuil, you can upgrade with:
pip install upgrade pyquil
If you would like to stay up to date with the latest changes and bug fixes, you can also opt to install pyQuil from the source here.
Note
PyQuil requires Python 3.6 or later.
Downloading the QVM and CompilerÂ¶
The Forest 2.0 Downloadable SDK Preview currently contains:
The Rigetti Quantum Virtual Machine (
qvm
) which allows highperformance simulation of Quil programsThe Rigetti Quil Compiler (
quilc
) which allows compilation and optimization of Quil programs to native gate sets
The QVM and the compiler are packed as program binaries that are accessed through the command line. Both of them provide support for direct commandline interaction, as well as a server mode. The server mode is required for use with pyQuil.
Download the Forest SDK here, where you can find links for Windows, macOS, Linux (.deb), Linux (.rpm), and Linux (barebones).
All installation mechanisms, except the barebones package, require administrative privileges to install. To use the QVM and Quil Compiler from the barebones package, you will have to install the prerequisite dependencies on your own.
Note
You can also find the open source code for quilc and qvm on GitHub, where you can find instructions for compiling, installing, and contributing to the compiler and QVM.
Installing on WindowsÂ¶
Download the Windows distribution by clicking on the appropriate link on the SDK download page.
Open the file forestsdk.msi
by double clicking on it in your Downloads folder, and follow the system prompts.
Upon successful installation, one should be able to open a new terminal window and run the following two commands:
qvm version
quilc version
To uninstall the Forest SDK, search for â€śAdd or remove programsâ€ť in the Windows search bar. Click on â€śAdd or remove programsâ€ť and, in the resulting window, search for â€śForest SDK for Windowsâ€ť in the list of applications and click on â€śUninstallâ€ť to remove it.
Installing on macOSÂ¶
Download the macOS distribution by clicking on the appropriate link on the SDK download page.
Mount the file forestsdk.dmg
by double clicking on it in your Downloads folder. From there, open forestsdk.pkg
by
doubleclicking on it. Follow the installation instructions.
Upon successful installation, one should be able to open a new terminal window and run the following two commands:
qvm version
quilc version
To uninstall, delete the following files:
/usr/local/bin/qvm
/usr/local/bin/quilc
/usr/local/share/man/man1/qvm.1
/usr/local/share/man/man1/quilc.1
Installing the QVM and Compiler on Linux (deb)Â¶
Download the Debian distribution by clicking on the appropriate link on the SDK download page. Unpack the tarball and change to that directory by doing:
tar xf forestsdklinuxdeb.tar.bz2
cd forestsdk2.0rc2linuxdeb
From here, run the following command:
sudo ./forestsdk2.0rc2linuxdeb.run
Upon successful installation, one should be able to run the following two commands:
qvm version
quilc version
To uninstall, type:
sudo apt remove forestsdk
Installing the QVM and Compiler on Linux (rpm)Â¶
Download the RPMbased distribution by clicking on the appropriate link on the SDK download page. Unpack the tarball and change to that directory by doing:
tar xf forestsdklinuxrpm.tar.bz2
cd forestsdk2.0rc2linuxrpm
From here, run the following command:
sudo ./forestsdk2.0rc2linuxrpm.run
Upon successful installation, one should be able to run the following two commands:
qvm version
quilc version
To uninstall, type:
sudo rpm e forestsdk
# or
sudo yum uninstall forestsdk
Installing the QVM and Compiler on Linux (barebones)Â¶
The barebones installation only contains the executable binaries and
manual pages, and doesnâ€™t contain any of the requisite dynamic
libraries. As such, installation doesnâ€™t require administrative or
sudo
privileges.
First, unpack the tarball and change to that directory by doing:
tar xf forestsdklinuxbarebones.tar.bz2
cd forestsdk2.1linuxbarebones
From here, run the following command:
./forestsdk2.1linuxbarebones.run
Upon successful installation, this will have created a new directory rigetti
in your home directory that contains all
of the binary and documentation artifacts.
This method of installation requires one, through whatever means, to install shared libraries for BLAS, LAPACK, and libffi. On a Debianderivative system, this could be accomplished with
sudo aptget install liblapackdev libblasdev libffidev libzmq3dev
Or on any rhelderivative systems (e.g. Amazon Linux) with
sudo yum install y lapackdevel blasdevel epelrelease
sudo yum install y zeromq3devel
To uninstall, remove the directory ~/rigetti
.
Getting StartedÂ¶
To get started using the SDK, you can either interact with the QVM and the compiler directly from the command line, or you can run them in server mode and use them with pyQuil. In this section, weâ€™re going to explain how to do the latter.
For more information about directly interacting with the QVM and the compiler, refer to their respective manual pages.
After installation, you can read the manual pages by opening a new terminal window and typing man qvm
(for the QVM)
or man quilc
(for the compiler). Quit out of the manual page by typing q
.
Setting Up Server Mode for PyQuilÂ¶
Note
This set up is only necessary to run pyQuil locally. If youâ€™re running in a QMI, this has already been done for you.
Itâ€™s easy to start up local servers for the QVM and quilc on your laptop. You should have two terminal windows open
to run in the background. We recommend using a resource such as tmux
for running and managing multiple programs in one
terminal.
### CONSOLE 1
$ qvm S
Welcome to the Rigetti QVM
(Configured with 10240 MiB of workspace and 8 workers.)
[20180920 15:39:50] Starting server on port 5000.
### CONSOLE 2
$ quilc S
...  Launching quilc.
...  Spawning server at (tcp://*:5555) .
Thatâ€™s it! Youâ€™re all set up to run pyQuil locally. Your programs will make requests to these server endpoints to compile your Quil programs to native Quil, and to simulate those programs on the QVM.
NOTE: Prior to quilc version 1.10 there existed two methods for communicating with the quilc server: over HTTP by creating the server with the S
flag, or over RPCQ by creating the server with the R
flag. The HTTP server mode was deprecated in early 2019, and removed in mid 2019. The S
and R
flags now both start the RPCQ server.
Run Your First ProgramÂ¶
Now that our local endpoints are up and running, we can start running pyQuil programs! We will run a simple program on the Quantum Virtual Machine (QVM).
The program we will create prepares a fully entangled state between two qubits, called a Bell State. This state is in an equal superposition between \(\ket{00}\) and \(\ket{11}\), meaning that it is equally likely that a measurement will result in measuring both qubits in the ground state or both qubits in the excited state. For more details about the physics behind these concepts, see Introduction to Quantum Computing.
To begin, start up python however you like. You can open a jupyter notebook (type jupyter notebook
in your terminal),
open an interactive python notebook in your terminal (with ipython3
), or simply launch python in your terminal
(type python3
). Recall that you need Python 3.6+ to use pyQuil.
Import a few things from pyQuil:
from pyquil import Program, get_qc
from pyquil.gates import *
The Program
object allows us to build up a Quil program. get_qc()
connects us to a
QuantumComputer
object, which specifies what our program should run on (see: The Quantum Computer). Weâ€™ve also imported all (*
)
gates from the pyquil.gates
module, which allows us to add operations to our program (Programs and Gates).
Note
PyQuil also provides a handy function for you to ensure that a local qvm and quilc are currently running in
your environment. To make sure both are available you execute from pyquil.api import local_forest_runtime
and then use
local_forest_runtime()
. This will start qvm and quilc instances using subprocesses if they have not already been started.
You can also use it as a context manager as in the following example:
from pyquil import get_qc, Program
from pyquil.gates import CNOT, Z
from pyquil.api import local_forest_runtime
prog = Program(Z(0), CNOT(0, 1))
with local_forest_runtime():
qvm = get_qc('9qsquareqvm')
results = qvm.run_and_measure(prog, trials=10)
Next, letâ€™s construct our Bell State.
# construct a Bell State program
p = Program(H(0), CNOT(0, 1))
Weâ€™ve accomplished this by driving qubit 0 into a superposition state (thatâ€™s what the â€śHâ€ť gate does), and then creating an entangled state between qubits 0 and 1 (thatâ€™s what the â€śCNOTâ€ť gate does). Finally, weâ€™ll want to run our program:
# run the program on a QVM
qc = get_qc('9qsquareqvm')
result = qc.run_and_measure(p, trials=10)
print(result[0])
print(result[1])
Compare the two arrays of measurement results. The results will be correlated between the qubits and random from shot to shot.
The qc
is a simulated quantum computer. By specifying we want to .run_and_measure
, weâ€™ve told our QVM to run
the program specified above, collapse the state with a measurement, and return the results to us. trials
refers to
the number of times we run the whole program.
The call to run_and_measure
will make a request to the two servers we
started up in the previous section: first, to the quilc
server
instance to compile the Quil program into native Quil, and then to the qvm
server
instance to simulate and return measurement results of the program 10 times. If you open up the terminal windows where your servers
are running, you should see output printed to the console regarding the requests you just made.
In the following sections, weâ€™ll cover gates, program construction & execution, and go into detail about our Quantum Virtual Machine, our QPUs, noise models and more. If youâ€™ve used pyQuil before, continue on to our New in Forest 2  Other. Once youâ€™re set with that, jump to Programs and Gates to continue.
New in Forest 2  Running on the QVMÂ¶
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 (compared to pyQuil 1.x) to better reflect that focus.
Program construction is largely the sameÂ¶
To construct runandmeasurestyle programs, there are no changes in program construction. When using explicit MEASURE
instructions or using parametric programs for massive speed increases, please read about the new quil memory model.
Below, we build a program that constructs a Bell state.
[1]:
from pyquil import Program
from pyquil.gates import *
program = Program(
H(0),
CNOT(0, 1),
)
print(program)
H 0
CNOT 0 1
Unphysical methods deprecated in QVMConnection
Â¶
In pyQuil 1.x, you would use the QVMConnection
to run quantum programs on a webhosted quantum simulator (the â€śQuantum Virtual Machineâ€ť). To run on a real quantum processor (QPU), you would switch all instances of QVMConnection
to QPUConnection
. However, QVMConnection
let you do many unphysical operations that are unsupported on a real QPU. These methods are detailed below and have been deprecated in favor of a new API that clearly delineats the physical from the unphysical.
WavefunctionÂ¶
When simulating a quantum program, we can inspect its wavefunction. This is very useful! It isâ€“howeverâ€“impossible to inspect the wavefunction of a real quantum processor. This is one of the central tenets of quantum information! Attempting to switch instances of QVMConnection
to QPUConnection
results in an error. Additionally, using QVMConnection
with a noise model will cause the wavefunction
call to behave badly.
The old wayÂ¶
[2]:
from pyquil.api import QVMConnection
wfn_old = QVMConnection().wavefunction(program)
print(wfn_old)
(0.7071067812+0j)00> + (0.7071067812+0j)11>
The new wayÂ¶
WavefunctionSimulator
encapsulates all functionality that requires peering into a wavefunction. This also opens the door for different types of simulators other than those backed by a wavefunction. For example, you can simulate a quantum circuit with a density matrix simulation or a path integral simulation.
[3]:
from pyquil.api import WavefunctionSimulator
wfn = WavefunctionSimulator().wavefunction(program)
print(wfn)
(0.7071067812+0j)00> + (0.7071067812+0j)11>
ExpectationÂ¶
Many quantum algorithms involve calculating expectation values of quantum observables. We can represent quantum observables with PauliSum
s. When using a simulator, it is possible to exactly calculate expectation values (a consequence of having access to the full wavefunction) whereas when running on a QPU you must estimate expectation values by sampling from many runs of a program.
Letâ€™s use our program defined above to construct a bell state.
[4]:
print(program)
H 0
CNOT 0 1
And we wish to measure the following quantum observables \(O\)  the expected value of the 0th bit \(O=(1Z_0)/2\)  the expected value of the 1st bit \(O=(1Z_1)/2\)  the expected value of the exclusive or (XOR) between the two qubits \(O=(1Z_0Z_1)/2\)
Exercise for the reader: convince yourself that \((1Z_0Z_1)/2\) is the XOR function
[5]:
from pyquil.paulis import sZ
z0 = (1sZ(0))*0.5
z1 = (1sZ(1))*0.5
xor = (1sZ(0)*sZ(1))*0.5
A Bell state is entangled, so each of the 1qubit operators should give an expectation value of 0.5 (as each qubit taken individually has a 50% change of being measured in the 0 or 1 state) whereas the twoqubit operator should give an expectation value of 0 (as the two qubits considered together will always have even parity)
The old wayÂ¶
Because of technical details, QVMConnection.expectation
requires transforming your observable represented as a PauliSum
into a program, and then remembering to multiply back in the coefficients. A more usable API was introduced in Pyquil 1.9 and was given the name pauli_expectation
.
[6]:
for observable in [z0, z1, xor]:
expectation = QVMConnection().pauli_expectation(prep_prog=program, pauli_terms=observable)
print(observable, '\t', expectation)
(0.5+0j)*I + (0.5+0j)*Z0 (0.4999999999999999+0j)
(0.5+0j)*I + (0.5+0j)*Z1 (0.4999999999999999+0j)
(0.5+0j)*I + (0.5+0j)*Z0*Z1 0j
The new wayÂ¶
If you want analytical expectation values, the solution is to use WavefunctionSimulator.expectation
Note that the method is not named pauli_expectation
as we do not support the pyQuil<1.9 way of using Program
s to represent PauliSum
s
[7]:
for observable in [z0, z1, xor]:
expectation = WavefunctionSimulator().expectation(prep_prog=program, pauli_terms=observable)
print(observable, '\t', expectation)
(0.5+0j)*I + (0.5+0j)*Z0 (0.4999999999999999+0j)
(0.5+0j)*I + (0.5+0j)*Z1 (0.4999999999999999+0j)
(0.5+0j)*I + (0.5+0j)*Z0*Z1 0j
Sampling expectations on a QPU.Â¶
Estimating expectation values via sampling from a QPU is often something we would like to do! Please look forward for helper functions for sampling observables in the near future.
Async methods removedÂ¶
pyQuil 2 is tailored for Rigettiâ€™s Quantum Cloud Services (QCS). In prior releases, executing programs on either the QVM or QPU involved communicating with a cloudhosted endpoint. Now, you are empowered with
A preconfigured quantum machine image (QMI) with your own dedicated qvm and quilc instance.
lowlatency QPU access all to yourself during a scheduled time window
A downloadable local version of the qvm and quilc
Taken together, there is no longer any motivation for supporting asynchronous access to either the QVM or QPU.
The old wayÂ¶
When running many programs it was often possible to reduce runtime by batching jobs and exploiting the async queue. The following example does not work in pyQuil 2 but gives a sketch about how this would have worked.
import numpy as np
cxn = QVMConnection()
thetas = np.linspace(0, 2*np.pi, 20)
jobs = [Program(RY(theta, 0)) for theta in thetas]
job_ids = [cxn.run_and_measure_async(job, qubits=[0], trials=1000) for job in jobs]
bitstrings = [np.asarray(cxn.wait_for_job(job)) for job in jobs]
The notquitenew wayÂ¶
Since this is such an important use case, there have been many changes to support running many programs as quickly as possible. We demonstrate an equivalent, synchronous version of the example given above. To idiomatically run this set of jobs, there are additional features you should use that are not covered in this document. Please continue reading the documentation, especially the page covering parametric programs.
[8]:
import numpy as np
cxn = QVMConnection()
thetas = np.linspace(0, np.pi, 20)
bitstrings = [np.asarray(cxn.run_and_measure(Program(RY(theta, 0)), qubits=[0], trials=1000))
for theta in thetas]
The resultÂ¶
[9]:
%matplotlib inline
from matplotlib import pyplot as plt
averages = [np.mean(bs[:,0]) for bs in bitstrings]
_ = plt.plot(thetas, averages, 'o')
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.
[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 twoqubit Bell state.
[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:
[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()
[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 3qubit QVM.
[5]:
from pyquil import get_qc
qc = get_qc('3qqvm')
qc
[5]:
QuantumComputer[name="3qqvm"]
We can do a quick check to make sure it has 3 qubits
[6]:
qc.qubits()
[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.
[7]:
bitstrings = qc.run_and_measure(program, trials=10)
bitstrings
[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)
[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
[8]:
array([3, 0, 0, 3, 3, 3, 3, 0, 3, 0])
[9]:
sample_is_ghz = np.logical_or(sums == 0, sums == 3)
sample_is_ghz
[9]:
array([ True, True, True, True, True, True, True, True, True,
True])
[10]:
np.all(sample_is_ghz)
[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.
[11]:
# TODO: we need a lattice that is not zeroindexed
# qc = get_qc('Aspen03QB')
# 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!
[12]:
qc = get_qc('4qqvm')
bitstrings = qc.run_and_measure(Program(X(0), X(1), X(2)), trials=10)
bitstrings
[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
[13]:
# Stacking everything
np.vstack([bitstrings[q] for q in qc.qubits()]).T
[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]])
[14]:
# Stacking what you want (contrast with above)
qubits = [0, 1, 2]
np.vstack([bitstrings[q] for q in qubits]).T
[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. Preconfigured noise models can be used via get_qc('xxxxnoisyqvm')
.
As a consequence, run_and_measure
for large numbers of trials will be slower in Pyquil 2.
[15]:
qc = get_qc('3qnoisyqvm')
bitstrings = qc.run_and_measure(program, trials=10)
bitstrings
[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])}
[16]:
bitstring_array = np.vstack([bitstrings[q] for q in qc.qubits()]).T
sums = np.sum(bitstring_array, axis=1)
sums
[16]:
array([0, 3, 3, 0, 0, 3, 3, 0, 3, 3])
[17]:
# Noise means now we measure things other than 000 or 111
np.all(np.logical_or(sums == 0, sums == 3))
[17]:
True
list_quantum_computers
Â¶
You can find all possible arguments to get_qc
with list_quantum_computers
[18]:
from pyquil import list_quantum_computers
# TODO: unauthenticated endpoint
# list_quantum_computers()
QuantumComputer
s have a topologyÂ¶
An important restriction when running on a real quantum computer is the mapping of qubits to the supported twoqubit gates. The QVM is designed to provide increasing levels of â€śrealismâ€ť to guarantee that if your program executes successfully on get_qc("Aspenxxxnoisyqvm")
then it will execute successfully on get_qc("Aspenxxx")
*
* 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 "9qsquareqvm"
that has a square topology.
[19]:
qc = get_qc('9qsquareqvm')
%matplotlib inline
import networkx as nx
nx.draw(qc.qubit_topology())
from matplotlib import pyplot as plt
_ = plt.title('9qsquareqvm', 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("3qqvm")
, "4qqvm"
, and indeed you can do any "{n}qqvm"
(subject to computational resource constraints). These QVMâ€™s are constructed with a topology! It just happens to be fully connected
[20]:
nx.draw(get_qc('5qqvm').qubit_topology())
_ = plt.title('5qqvm is fully connected', fontsize=16)
Heirarchy of realismÂ¶
WavefunctionSimulator
to debug algorithmget_qc("5qqvm")
to debug samplingget_qc("9qsquareqvm")
to debug mapping to a latticeget_qc("9qsquarenoisyqvm"
) to debug generic noise characteristicsget_qc("Aspen016QAqvm")
to debug mapping to a real latticeget_qc("Aspen016QAnoisyqvm")
to debug noise characteristics of a real deviceget_qc("Aspen016QA")
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 backendagnostic methods on QuantumComputer
and mixandmatch 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 5qubit QVM with one central node and only even numbered qubits.
[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, *, protoquil=None):
return program
def native_quil_to_executable(self, nq_program):
return nq_program
from pyquil.api import QuantumComputer, QVM, ForestConnection
my_qc = QuantumComputer(
name='myqvm',
qam=QVM(connection=ForestConnection()),
device=device,
compiler=MyLazyCompiler(),
)
nx.draw(my_qc.qubit_topology())
[22]:
my_qc.run_and_measure(Program(X(10)), trials=5)
[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])}
New in Forest 2  Parametric ProgramsÂ¶
pyQuil is for constructing and running hybrid quantum/classical algorithms on real quantum computers. With the release of pyQuil 2, we have changed parts of the API to take advantage of some exciting new features available on QCS.
A hybrid algorithm involves using the quantum computer to create a quantum state that would be difficult to prepare classically; measure it in a way particular to your problem; and then update your procedure for creating the state so that the measurements are closer to the correct answer. A real hybrid algorithm involves structured ansatze like QAOA for optimization or a UCC ansatz for chemistry. Here, weâ€™ll do a much simpler parameterized program
[1]:
from pyquil import Program, get_qc
from pyquil.gates import *
def ansatz(theta):
program = Program()
program += RY(theta, 0)
return program
print(ansatz(theta=0.2))
RY(0.2) 0
Scan over the parameter (the old way)Â¶
For this extrordinarily simple ansatz, we can discretize the parameter theta and try all possible values. As the number of parameters increases, the number of combinations increases exponentially so doing a full grid search will become intractable for anything more than ~two parameters.
[2]:
import numpy as np
qc = get_qc("9qsquareqvm")
thetas = np.linspace(0, 2*np.pi, 21)
results = []
for theta in thetas:
program = ansatz(theta)
bitstrings = qc.run_and_measure(program, trials=1000)
results.append(np.mean(bitstrings[0]))
[3]:
%matplotlib inline
from matplotlib import pyplot as plt
plt.plot(thetas, results, 'o')
plt.xlabel(r'$\theta$', fontsize=18)
_ = plt.ylabel(r'$\langle \Psi(\theta)  \frac{1  Z}{2}  \Psi(\theta) \rangle$',
fontsize=18)
Do an optimization (the old way)Â¶
Instead of doing a full grid search, we will employ a classical optimizer to find the best parameter values. Here we use scipy to find the theta that results in sampling the most 1
s in our resultant bitstrings.
[4]:
def objective_function(theta):
program = ansatz(theta[0])
bitstrings = qc.run_and_measure(program, trials=1000)
result = np.mean(bitstrings[0])
return result
import scipy.optimize
res = scipy.optimize.minimize(objective_function, x0=[0.1], method='COBYLA')
res
[4]:
fun: 1.0
maxcv: 0.0
message: 'Optimization terminated successfully.'
nfev: 13
status: 1
success: True
x: array([3.1])
[5]:
plt.plot(thetas, results, label='scan')
plt.plot([res.x], [res.fun], '*', ms=20, label='optimization result')
plt.legend()
[5]:
<matplotlib.legend.Legend at 0x1015dedf28>
CompilationÂ¶
Prior to QCS, a QPU job would be routed via a series of cloudbased queues and compilation steps. With Forest 2, you are in control of the two stages of compilation so you can amortize the cost of compiling. Your QMI and all classical infrastructure is hosted on the Rigetti premises, so network latency is minimal.
Quil to native quilÂ¶
The first step of compilation converts gates to their hardwaresupported equivalent. For example, our parametric RY is converted into RXâ€™s and RZâ€™s because these are physically realizable on a Rigetti QPU
[6]:
nq_program = qc.compiler.quil_to_native_quil(ansatz(theta=0.5))
print(nq_program)
PRAGMA EXPECTED_REWIRING "#(0 1 2 3 4 5 6 7 8)"
RX(pi/2) 0
RZ(0.5) 0
RX(pi/2) 0
PRAGMA CURRENT_REWIRING "#(0 1 2 3 4 5 6 7 8)"
PRAGMA EXPECTED_REWIRING "#(0 1 2 3 4 5 6 7 8)"
PRAGMA CURRENT_REWIRING "#(0 1 2 3 4 5 6 7 8)"
Native quil to executableÂ¶
The second step of compilation will turn named gates into calibrated pulses stored in a binary format suitable for consumption by the control electronics. This means that you can fully compile a given program and run it many times with minimal classical overhead.
Note: since weâ€™re using a QVM, for which there is no binary format, this stage is mocked out and you can see the original Quil inside the PyQuilExecutableResponse
that is returned. When running on the QPU, this will return a BinaryExecutableResponse
whose contents are opaque.
TODO: obscure the contents of PyQuilExecutableResponse
: https://github.com/rigetti/pyquil/issues/700
[7]:
qc.compiler.native_quil_to_executable(nq_program)
[7]:
PyQuilExecutableResponse(attributes={'native_quil_metadata': {'finalrewiring': [0, 1, 2, 3, 4, 5, 6, 7, 8], 'topological_swaps': 0, 'gate_depth': 3, 'gate_volume': 3, 'program_duration': 18.01, 'program_fidelity': 1.0, 'multiqubit_gate_depth': 0}, 'num_shots': 1}, program='PRAGMA EXPECTED_REWIRING "#(0 1 2 3 4 5 6 7 8)"\nRX(pi/2) 0\nRZ(0.5) 0\nRX(pi/2) 0\nPRAGMA CURRENT_REWIRING "#(0 1 2 3 4 5 6 7 8)"\nPRAGMA EXPECTED_REWIRING "#(0 1 2 3 4 5 6 7 8)"\nPRAGMA CURRENT_REWIRING "#(0 1 2 3 4 5 6 7 8)"\n')
Parametric compilationÂ¶
This doesnâ€™t buy us much if we have to know exactly what circuit we want to run before compiling it and amortizing the compilation cost. Maybe you could get away with it when youâ€™re doing a parameter scan, but for hybrid algorithms, the circuit parameter (here: theta
) depends on the results of a circuit before. This is the essence of hybrid programming! Therefore, all compilation steps have been upgraded to support named, symbolic parameters that will be updated at runtime with minimal
overhead.
With this feature, you can compile a parametric program once and run it many times will different parameter values and you need not know the parameter values at compilation time.
There are a couple of prerequisites to use this feature effectively from PyQuil, which we address in this document.
First, you must declare a parameter when constructing your quil program. When declaring a named classical variable, you must specify at least a name and a type. It is conventional to make sure the Python variable name of the reference memory matches the Quil variable name. In our case, we name both things theta
. Our circuit above would be modified in this way:
[8]:
program = Program()
theta = program.declare('theta', memory_type='REAL')
program += RY(theta, 0)
print(program)
DECLARE theta REAL[1]
RY(theta) 0
MeasuringÂ¶
In the documentation so far, weâ€™ve been using the run_and_measure
functionality of QuantumComputer
. Itâ€™s time to get our hands dirty and introduce explicit measure instructions.
Above, we declared a classical piece of memory, weâ€™ve given it a name (theta
), and weâ€™ve given it a type (REAL
). The bits that we measure (or â€śread outâ€ť â€“ ro
for short) must now also be declared, given a name, and a type. Additionally, weâ€™ll usually be measuring more than one qubit so we can give this register a size.
The index of the readout register need not match the qubit index. For example below, we create a bell state on qubits 5 and 6 and measure the readout results into ro[0]
and ro[1]
.
Note: The readout register must be named â€śroâ€ť (for now)
[9]:
program = Program()
ro = program.declare('ro', memory_type='BIT', memory_size=2)
program += H(5)
program += CNOT(5, 6)
program += MEASURE(5, ro[0])
program += MEASURE(6, ro[1])
print(program)
DECLARE ro BIT[2]
H 5
CNOT 5 6
MEASURE 5 ro[0]
MEASURE 6 ro[1]
Our very simple ansatz only has one qubit, so the measurement is quite simple.
[10]:
program = Program()
theta = program.declare('theta', memory_type='REAL')
ro = program.declare('ro', memory_type='BIT', memory_size=1)
program += RY(theta, 0)
program += MEASURE(0, ro[0])
print(program)
DECLARE theta REAL[1]
DECLARE ro BIT[1]
RY(theta) 0
MEASURE 0 ro[0]
Number of shotsÂ¶
The number of trials is compiled into the executable binary, so we must specify this number prior to compilation.
TODO: add to str / repr https://github.com/rigetti/pyquil/issues/701
[11]:
program = Program()
theta = program.declare('theta', memory_type='REAL')
ro = program.declare('ro', memory_type='BIT', memory_size=1)
program += RY(theta, 0)
program += MEASURE(0, ro[0])
program.wrap_in_numshots_loop(shots=1000)
print(program)
DECLARE theta REAL[1]
DECLARE ro BIT[1]
RY(theta) 0
MEASURE 0 ro[0]
Using qc.run()
Â¶
To use the lowerlevel but more powerful qc.run
interface, we have had to take control of these three things
We decalred a readout register named
ro
of typeBIT
and included explicitMEASURE
instructions. Since this sets up a (potentially sprase) mapping from qubits to classical addresses, we can expectqc.run()
to return the classic 2d ndarray of yore instead of the dictionary returned byrun_and_measure
We have called
program.wrap_in_numshots_loop()
prior to compilation so the number of shots can be encoded in an efficient binary representation of the programWe have taken control of compilation; either by calling
qc.compile(program)
or by using the lowerlevel functions:nq_program = qc.compiler.quil_to_native_quil(program) executable = qc.compiler.native_quil_to_executable(nq_program)
[12]:
def ansatz(theta):
program = Program()
ro = program.declare('ro', memory_type='BIT', memory_size=1)
program += RY(theta, 0)
program += MEASURE(0, ro[0])
return program
print(ansatz(theta=np.pi))
DECLARE ro BIT[1]
RY(pi) 0
MEASURE 0 ro[0]
We can run the program with a preset angle (here, theta = np.pi
).
[13]:
program = ansatz(theta=np.pi)
program.wrap_in_numshots_loop(shots=5)
executable = qc.compile(program)
bitstrings = qc.run(executable)
print(bitstrings.shape)
bitstrings
(5, 1)
[13]:
array([[1],
[1],
[1],
[1],
[1]])
Scan over the parameter (the new way)Â¶
Finally, all the pieces are in place to compile and run parameterized executable binaries. We declare parameters that will be compiled symbolically into the binary allowing us to amortize the cost of compilation when running hybrid algorithms.
[14]:
def ansatz():
program = Program()
theta = program.declare('theta', memory_type='REAL')
ro = program.declare('ro', memory_type='BIT', memory_size=1)
program += RY(theta, 0)
program += MEASURE(0, ro[0])
return program
print(ansatz())
DECLARE theta REAL[1]
DECLARE ro BIT[1]
RY(theta) 0
MEASURE 0 ro[0]
Using memory_map
Â¶
Now, when we call qc.run
we provide a memory_map
argument which will substitute in values for previouslydeclared Quil variables in a precompiled executable.
[15]:
program = ansatz() # look ma, no arguments!
program.wrap_in_numshots_loop(shots=1000)
executable = qc.compile(program)
thetas = np.linspace(0, 2*np.pi, 21)
results = []
for theta in thetas:
bitstrings = qc.run(executable, memory_map={'theta': [theta]})
results.append(np.mean(bitstrings[:, 0]))
%matplotlib inline
from matplotlib import pyplot as plt
plt.plot(thetas, results, 'o')
plt.xlabel(r'$\theta$', fontsize=18)
_ = plt.ylabel(r'$\langle \Psi(\theta)  \frac{1  Z}{2}  \Psi(\theta) \rangle$', fontsize=18)
Do an optimization (the new way)Â¶
Since parameters are compiled symbolically, we can do hybrid algorithms just as fast as parameter scans.
[16]:
program = ansatz() # look ma, no arguments!
program.wrap_in_numshots_loop(shots=1000)
executable = qc.compile(program)
def objective_function(thetas):
bitstrings = qc.run(executable, memory_map={'theta': thetas})
result = np.mean(bitstrings[:, 0])
return result
res = scipy.optimize.minimize(objective_function, x0=[0.1], method='COBYLA')
res
[16]:
fun: 1.0
maxcv: 0.0
message: 'Optimization terminated successfully.'
nfev: 12
status: 1
success: True
x: array([3.1])
[17]:
plt.plot(thetas, results, label='scan')
plt.plot([res.x], [res.fun], '*', ms=20, label='optimization result')
plt.legend()
[17]:
<matplotlib.legend.Legend at 0x1015f13898>
New in Forest 2  OtherÂ¶
There are many other changes to the Forest SDK (comprising pyQuil, Quil, the Quil Compiler, and the QVM).
Note
For installation & setup, follow the download instructions in the section Installation and Getting Started at the top of the page.
Updates to the Quil languageÂ¶
The primary differences in the programming language Quil 1.0 (as appearing in pyQuil 1.x) and Quil 2 (as appearing in pyQuil 2) amount to an enhanced memory model. Whereas the classical memory model in Quil 1.0 amounted to an flat bit array of indefinite size, the memory model in Quil 2 is segmented into typed, sized, named regions.
In terms of compatibility with Quil 1.0, this primarily changes how MEASURE
instructions are formulated, since their
classical address targets must be modified to fit the new framework. In terms of new functionality, this allows angle
values to be read in from classical memory.
DAGGER
and CONTROLLED
modifiersÂ¶
Quil 2 also introduces easier ways to manipulate gates by using gate modifiers. Two gate modifiers are supported currently,
DAGGER
and CONTROLLED
.
DAGGER
can be written before a gate to refer to its inverse. For instance:
DAGGER RX(pi/3) 0
would have the same effect as:
RX(pi/3) 0
DAGGER
can be applied to any gate, but also circuits defined with DEFCIRCUIT
. This allows for easy reversal of unitary circuits:
DEFCIRCUIT BELL:
H 0
CNOT 0 1
# construct a Bell state
BELL
# disentangle, bringing us back to identity
DAGGER BELL
DECLARE
Â¶
Classical memory regions must be explicitly requested and named by a Quil program using DECLARE
directive. A generic
DECLARE
directive has the following syntax:
DECLARE regionname type([count])? (SHARING parentregionname (OFFSET (offsetcount offsettype)+))?
The nonkeyword items have the following allowable values:
regionname
: any nonkeyword formal name.type
: one ofREAL
,BIT
,OCTET
, orINTEGER
parentregionname
: any nonkeyword formal name previously used asregionname
in a differentDECLARE
statement.offsetcount
: a nonnegative integer.offsettype
: the same allowable values astype
.
Here are some examples:
DECLARE beta REAL[32]
DECLARE ro BIT[128]
DECLARE betabits BIT[1436] SHARING beta
DECLARE fourthbitinbeta1 BIT SHARING beta OFFSET 1 REAL 4 BIT
In order, the intention of these DECLARE
statements is:
Allocate an array called
beta
of length 32, each entry of which is aREAL
number.Allocate an array called
ro
of length 128, each entry of which is aBIT
.Name an array called
betabits
, which is an overlay onto the existing arraybeta
, so that the bit representations of elements ofbeta
can be directly examined and manipulated.Name a single
BIT
calledfourthbitinbeta1
which overlays the fourth bit of the bit representation of theREAL
valuebeta[1]
.
Backwards compatibilityÂ¶
Quil 1.0 is not compatible with Quil 2 in the following ways:
The unnamed memory references
[n]
and[nm]
have no direct equivalent in Quil 2 and must be replaced by named memory references. (This primarily affectsMEASURE
instructions.)The classical memory manipulation instructions have been modified: the operands of
AND
have been reversed (so that in Quil 2, the left operand is the target address) andOR
has been replaced byIOR
and its operands reversed (so that, again, in Quil 2 the left operand is the target address).
In all other instances, Quil 1.0 will operate identically with Quil 2.
When confronted with program text conforming to Quil 1.0, pyQuil 2 will automatically rewrite MEASURE q [n]
to
MEASURE q ro[n]
and insert a DECLARE
statement which allocates a BIT
array of the appropriate size named
ro
.
Updates to ForestÂ¶
In Forest 1.3, job submission to the QPU was done from your workstation and the ability was gated by on user ID. In Forest 2, job submission to the QPU must be done from your remote virtual machine, called a QMI (Quantum Machine Image).
In Forest 1.3, user data persisted indefinitely in cloud storage and could be accessed using the assigned job ID. In Forest 2, user data is stored only transiently, and it is the userâ€™s responsibility to handle longterm data storage on their QMI.
Forest 1.3 refered to the software developer kit (pyQuil, QVM, Quilc) and the cloud platform for submitting jobs. Forest 2 is the SDK which you can install on your own computer or use preinstalled on a QMI. The entire platform is called Quantum Cloud Services (QCS).
Example: Computing the bond energy of molecular hydrogen, pyQuil 1.9 vs 2.0Â¶
By way of example, letâ€™s consider the following pyQuil 1.9 program, which computes the natural bond distance in molecular hydrogen using a VQEtype algorithm:
from pyquil.api import QVMConnection
from pyquil.quil import Program
def setup_forest_objects():
qvm = QVMConnection()
return qvm
def build_wf_ansatz_prep(theta):
program = Program(f"""
# set up initial state
X 0
X 1
# build the exponentiated operator
RX(pi/2) 0
H 1
H 2
H 3
CNOT 0 1
CNOT 1 2
CNOT 2 3
RZ({theta}) 3
CNOT 2 3
CNOT 1 2
CNOT 0 1
RX(pi/2) 0
H 1
H 2
H 3
# measure out the results
MEASURE 0 [0]
MEASURE 1 [1]
MEASURE 2 [2]
MEASURE 3 [3]""")
return program
# some constants
bond_step, bond_min, bond_max = 0.05, 0, 200
angle_step, angle_min, angle_max = 0.1, 0, 63
convolution_coefficients = [0.1698845197777728, 0.16988451977777283, 0.2188630663199042,
0.2188630663199042]
shots = 1000
# set up the Forest object
qvm = setup_forest_objects()
# get all the unweighted expectations for all the sample wavefunctions
occupations = list(range(angle_min, angle_max))
indices = list(range(4))
for offset in occupations:
# set up the Program object, each time we have a new parameter
program = build_wf_ansatz_prep(angle_min + offset * angle_step)
bitstrings = qvm.run(program, indices, trials=shots)
totals = [0, 0, 0, 0]
for bitstring in bitstrings:
for index in indices:
totals[index] += bitstring[index]
occupations[offset] = [t / shots for t in totals]
# compute minimum energy as a function of bond length
min_energies = list(range(bond_min, bond_max))
for bond_length in min_energies:
energies = []
for offset in range(angle_min, angle_max):
energy = 0
for j in range(4):
energy += occupations[offset][j] * convolution_coefficients[j]
energies.append(energy)
min_energies[bond_length] = min(energies)
min_index = min_energies.index(min(min_energies))
min_energy, relaxed_length = min_energies[min_index], min_index * bond_step
In order to port this code to pyQuil 2.0, we need change only one thing: the part referencing QVMConnection
should be replaced by an equivalent part referencing a QuantumComputer
connected to a QVM
. Specifically, the following
snippet
from pyquil.api import QVMConnection
def setup_forest_objects():
qvm = QVMConnection()
return qvm
can be changed to
from pyquil.api import get_qc
def setup_forest_objects():
qc = get_qc("9qsquareqvm")
return qc
and the references to qvm
in the main body are changed to qc
instead. Since the QuantumComputer
object also
exposes a run
routine and pyQuil itself automatically rewrites 1.9style MEASURE
instructions into 2.0style
instructions, this is all we need to do.
If we are willing to be more intrusive, we can also take advantage of pyQuil 2.0â€™s classical memory and parametric
programs. The first piece to change is the Quil program itself: we remove the argument theta
from the Python
function build_wf_ansatz_prep
, with the intention of letting the QPU fill it in later. In turn, we modify the Quil
program itself to have a REAL
memory parameter named theta
. We also declare a few BIT
s for our MEASURE
instructions to target.
def build_wf_ansatz_prep():
program = Program("""
# set up memory
DECLARE ro BIT[4]
DECLARE theta REAL
# set up initial state
X 0
X 1
# build the exponentiated operator
RX(pi/2) 0
H 1
H 2
H 3
CNOT 0 1
CNOT 1 2
CNOT 2 3
RZ(theta) 3
CNOT 2 3
CNOT 1 2
CNOT 0 1
RX(pi/2) 0
H 1
H 2
H 3
# measure out the results
MEASURE 0 ro[0]
MEASURE 1 ro[1]
MEASURE 2 ro[2]
MEASURE 3 ro[3]""")
return program
Next, we modify the execution loop. Rather than reformulating the Program
object each time, we build and compile it
once, then use the .load()
method to transfer the parametric program to the (simulated) quantum device. We then set
only the angle value within the inner loop, and we change to using .run()
and .wait()
methods to manage control
between us and the quantum device.
More specifically, the old execution loop
# get all the unweighted expectations for all the sample wavefunctions
occupations = list(range(angle_min, angle_max))
indices = list(range(4))
for offset in occupations:
# set up the Program object, each time we have a new parameter
program = build_wf_ansatz_prep(angle_min + offset * angle_step)
bitstrings = qvm.run(program, indices, trials=shots)
totals = [0, 0, 0, 0]
for bitstring in bitstrings:
for index in indices:
totals[index] += bitstring[index]
occupations[offset] = [t / shots for t in totals]
becomes
# set up the Program object, ONLY ONCE
program = build_wf_ansatz_prep().wrap_in_numshots_loop(shots=shots)
binary = qc.compile(program)
# get all the unweighted expectations for all the sample wavefunctions
occupations = list(range(angle_min, angle_max))
indices = list(range(4))
for offset in occupations:
bitstrings = qc.run(binary, {'theta': [angle_min + offset * angle_step]})
totals = [0, 0, 0, 0]
for bitstring in bitstrings:
for index in indices:
totals[index] += bitstring[index]
occupations[offset] = [t / shots for t in totals]
Overall, the resulting program looks like this:
from pyquil.api import get_qc
from pyquil.quil import Program
def setup_forest_objects():
qc = get_qc("9qsquareqvm")
return qc
def build_wf_ansatz_prep():
program = Program("""
# set up memory
DECLARE ro BIT[4]
DECLARE theta REAL
# set up initial state
X 0
X 1
# build the exponentiated operator
RX(pi/2) 0
H 1
H 2
H 3
CNOT 0 1
CNOT 1 2
CNOT 2 3
RZ(theta) 3
CNOT 2 3
CNOT 1 2
CNOT 0 1
RX(pi/2) 0
H 1
H 2
H 3
# measure out the results
MEASURE 0 ro[0]
MEASURE 1 ro[1]
MEASURE 2 ro[2]
MEASURE 3 ro[3]""")
return program
# some constants
bond_step, bond_min, bond_max = 0.05, 0, 200
angle_step, angle_min, angle_max = 0.1, 0, 63
convolution_coefficients = [0.1698845197777728, 0.16988451977777283, 0.2188630663199042,
0.2188630663199042]
shots = 1000
# set up the Forest object
qc = setup_forest_objects()
# set up the Program object, ONLY ONCE
program = build_wf_ansatz_prep().wrap_in_numshots_loop(shots=shots)
binary = qc.compile(program)
# get all the unweighted expectations for all the sample wavefunctions
occupations = list(range(angle_min, angle_max))
indices = list(range(4))
for offset in occupations:
bitstrings = qc.run(binary, {'theta': [angle_min + offset * angle_step]})
totals = [0, 0, 0, 0]
for bitstring in bitstrings:
for index in indices:
totals[index] += bitstring[index]
occupations[offset] = [t / shots for t in totals]
# compute minimum energy as a function of bond length
min_energies = list(range(bond_min, bond_max))
for bond_length in min_energies:
energies = []
for offset in range(angle_min, angle_max):
energy = 0
for j in range(4):
energy += occupations[offset][j] * convolution_coefficients[j]
energies.append(energy)
min_energies[bond_length] = min(energies)
min_index = min_energies.index(min(min_energies))
min_energy, relaxed_length = min_energies[min_index], min_index * bond_step
MiscellaneaÂ¶
Quil promises that a BIT is 1 bit and that an OCTET is 8 bits. Quil does not promise, however, the size or layout of INTEGER or REAL. These are implementationdependent.
On the QPU, INTEGER
refers to an unsigned integer stored in a 48bit wide littleendian word, and REAL
refers to
a 48bit wide littleendian fixed point number of type <0.48>. In general, these datatypes are implementationdependent.
OCTET
always refers to an 8bit wide unsigned integer and is independent of implementation.
Memory regions are all â€śglobalâ€ť: DECLARE
directives cannot appear in the body of a DEFCIRCUIT
.
On the QVM, INTEGER is a twoâ€™s complement signed 64bit integer. REAL is an IEEE754 doubleprecision floatingpoint number.
Error reportingÂ¶
Because the Forest 2.0 execution model is no longer asynchronous, our error reporting model has also changed. Rather than writing to technical support with a job ID, users will need to provide all pertinent details to how they produced an error.
PyQuil 2 makes this task easy with the function decorator @pyquil_protect
, found in the module
pyquil.api
. By decorating a failing function (or a function that has the potential to fail), any
unhandled exceptions will cause an error log to be written to disk (at a userspecifiable location). For example, the
nonsense code block
from pyquil.api import pyquil_protect
...
@pyquil_protect
def my_function():
...
qc.qam.load(qc)
...
my_function()
causes the following error to be printed:
>>> PYQUIL_PROTECT <<<
An uncaught exception was raised in a function wrapped in pyquil_protect. We are writing out a
log file to "/Users/your_name/Documents/pyquil/pyquil_error.log".
Along with a description of what you were doing when the error occurred, send this file to Rigetti Computing
support by email at support@rigetti.com for assistance.
>>> PYQUIL_PROTECT <<<
as well as the following log file to be written to disk at the indicated location:
{
"stack_trace": [
{
"name": "pyquil_protect_wrapper",
"filename": "/Users/your_name/Documents/pyquil/pyquil/error_reporting.py",
"line_number": 197,
"locals": {
"e": "TypeError('quil_binary argument must be a QVMExecutableResponse. This error is typically triggered by
forgetting to pass (nativized) Quil to native_quil_to_executable or by using a compiler meant to be used
for jobs bound for a QPU.',)",
"old_filename": "'pyquil_error.log'",
"kwargs": "{}",
"args": "()",
"log_filename": "'pyquil_error.log'",
"func": "<function my_function at 0x106dc4510>"
}
},
{
"name": "my_function",
"filename": "<stdin>",
"line_number": 10,
"locals": {
"offset": "0",
"occupations": "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50,
51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62]"
}
},
{
"name": "wrapper",
"filename": "/Users/your_name/Documents/pyquil/pyquil/error_reporting.py",
"line_number": 228,
"locals": {
"pre_entry": "CallLogValue(timestamp_in=datetime.datetime(2018, 9, 11, 18, 40, 19, 65538),
timestamp_out=None, return_value=None)",
"key": "run('<pyquil.api._qvm.QVM object at 0x1027e3940>', )",
"kwargs": "{}",
"args": "(<pyquil.api._qvm.QVM object at 0x1027e3940>,)",
"func": "<function QVM.run at 0x106db4e18>"
}
},
{
"name": "run",
"filename": "/Users/your_name/Documents/pyquil/pyquil/api/_qvm.py",
"line_number": 376,
"locals": {
"self": "<pyquil.api._qvm.QVM object at 0x1027e3940>",
"__class__": "<class 'pyquil.api._qvm.QVM'>"
}
}
],
"timestamp": "20180911T18:40:19.253286",
"call_log": {
"__init__('<pyquil.api._qvm.QVM object at 0x1027e3940>', '<pyquil.api._base_connection.ForestConnection object at
0x1027e3588>', )": {
"timestamp_in": "20180911T18:40:18.967750",
"timestamp_out": "20180911T18:40:18.968170",
"return_value": "None"
},
"run('<pyquil.api._qvm.QVM object at 0x1027e3940>', )": {
"timestamp_in": "20180911T18:40:19.065538",
"timestamp_out": null,
"return_value": null
}
},
"exception": "TypeError('quil_binary argument must be a QVMExecutableResponse. This error is typically triggered
by forgetting to pass (nativized) Quil to native_quil_to_executable or by using a compiler meant to be used for
jobs bound for a QPU.',)",
"system_info": {
"python_version": "3.6.3 (default, Jan 25 2018, 13:55:02) \n[GCC 4.2.1 Compatible Apple LLVM 9.0.0
(clang900.0.39.2)]",
"pyquil_version": "2.0.0internal.1"
}
}
Please attach such a logfile to any request for support.
Parametric ProgramsÂ¶
In PyQuil 1.x, there was an object named ParametricProgram
:
# This function returns a quantum circuit with different rotation angles on a gate on qubit 0
def rotator(angle):
return Program(RX(angle, 0))
from pyquil.parametric import ParametricProgram
par_p = ParametricProgram(rotator) # This produces a new type of parameterized program object
This object has been removed from PyQuil 2. Please consider simply using a Python function for the above functionality:
par_p = rotator
Or using declared classical memory:
p = Program()
angle = p.declare('angle', 'REAL')
p += RX(angle, 0)
Programs and GatesÂ¶
Note
If youâ€™re running locally, remember set up the QVM and quilc in server mode before trying to use them: Setting Up Server Mode for PyQuil.
IntroductionÂ¶
Quantum programs are written in Forest using the Program
object. This Program
abstraction will help us
compose Quil programs.
from pyquil import Program
Programs are constructed by adding quantum gates to it, which are defined in the gates
module. We can import all
standard gates with the following:
from pyquil.gates import *
Letâ€™s instantiate a Program
and add an operation to it. We will act an X
gate on qubit 0.
p = Program()
p += X(0)
All qubits begin in the ground state. This means that if we measure a qubit without applying operations on it, we expect to receive
a 0 result. The X
gate will rotate qubit 0 from the ground state to the excited state, so a measurement immediately
after should return a 1 result. More details about gate operations are explained in Introduction to Quantum Computing.
We can print our pyQuil program (print(p)
) to see the equivalent Quil representation:
X 0
This isnâ€™t going to be very useful to us without measurements. In pyQuil 2.0, we have to DECLARE
a memory space
to read measurement results, which we call â€śreadout resultsâ€ť and abbreviate as ro
. With measurement, our whole program
looks like this:
from pyquil import Program
from pyquil.gates import *
p = Program()
ro = p.declare('ro', 'BIT', 1)
p += X(0)
p += MEASURE(0, ro[0])
print(p)
DECLARE ro BIT[1]
X 0
MEASURE 0 ro[0]
Weâ€™ve instantiated a program, declared a memory space named ro
with one single bit of memory, applied
an X
gate on qubit 0, and finally measured qubit 0 into the zeroth index of the memory space named ro
.
Awesome! Thatâ€™s all we need to get results back. Now we can actually see what happens if we run this program on the Quantum Virtual Machine (QVM). We just have to add a few lines to do this.
from pyquil import get_qc
...
qc = get_qc('1qqvm') # You can make any 'nqqvm' this way for any reasonable 'n'
executable = qc.compile(p)
result = qc.run(executable)
print(result)
Congratulations! You just ran your program on the QVM. The returned value should be:
[[1]]
For more information on what the above result means, and on executing quantum programs on the QVM in general, see The Quantum Computer. The remainder of this section of the docs will be dedicated to constructing programs in detail, an essential part of becoming fluent in quantum programming.
The Standard Gate SetÂ¶
The following gates methods come standard with Quil and gates.py
:
Pauli gates
I
,X
,Y
,Z
Hadamard gate:
H
Phase gates:
PHASE(theta)
,S
,T
Controlled phase gates:
CZ
,XY
,CPHASE00(alpha)
,CPHASE01(alpha)
,CPHASE10(alpha)
,CPHASE(alpha)
Cartesian rotation gates:
RX(theta)
,RY(theta)
,RZ(theta)
Controlled \(X\) gates:
CNOT
,CCNOT
Swap gates:
SWAP
,CSWAP
,ISWAP
,PSWAP(alpha)
The parameterized gates take a real or complex floating point number as an argument.
Declaring MemoryÂ¶
Classical memory regions must be explicitly requested and named by a Quil program using the DECLARE
directive.
Details about can be found in the migration guide or in pyquil.quil.Program.declare()
.
In pyQuil, we declare memory with the .declare
method on a Program
. Letâ€™s inspect the function signature
# pyquil.quil.Program
def declare(self, name, memory_type='BIT', memory_size=1, shared_region=None, offsets=None):
and break down each argument:
name
is any name you want to give this memory region.
memory_type
is one of'REAL'
,'BIT'
,'OCTET'
, or'INTEGER'
(given as a string). OnlyBIT
andOCTET
always have a determined size, which is 1 bit and 8 bits respectively.
memory_size
is the number of elements of that type to reserve.
shared_region
andoffsets
allow you to alias memory regions. For example, you might want to name the third bit in your readout array asq3_ro
.SHARING
is currently disallowed for our QPUs, so we wonâ€™t focus on this here.
Now we can get into an example.
from pyquil import Program
p = Program()
ro = p.declare('ro', 'BIT', 16)
theta = p.declare('theta', 'REAL')
Warning
.declare
cannot be chained, since it doesnâ€™t return a modified Program
object.
Notice that the .declare
method returns a reference to the memory weâ€™ve just declared. We will need this reference
to make use of these memory spaces again. Letâ€™s see how the Quil is looking so far:
DECLARE ro BIT[16]
DECLARE theta REAL[1]
Thatâ€™s all we have to do to declare the memory. Continue to the next section on Measurement to learn more about
using ro
to store measured readout results. Check out Parametric Compilation to see how you might use
theta
to compile gate parameters dynamically.
MeasurementÂ¶
There are several ways you can handle measurements in your program. We will start with the simplest method â€“ letting
the QuantumComputer
abstraction do it for us.
from pyquil import Program, get_qc
from pyquil.gates import H, CNOT
# Get our QuantumComputer instance, with a Quantum Virutal Machine (QVM) backend
qc = get_qc("8qqvm")
# Construct a simple Bell State
p = Program(H(0), CNOT(0, 1))
results = qc.run_and_measure(p, trials=10)
print(results)
{0: array([1, 1, 0, 1, 0, 0, 1, 1, 0, 1]),
1: array([1, 1, 0, 1, 0, 0, 1, 1, 0, 1]),
2: array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
3: array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
4: array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
5: array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
6: array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
7: array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])}
The method .run_and_measure
will handle declaring memory for readout results, adding MEASURE
instructions for each
qubit in the QVM, telling the QVM how many trials to run, running and returning the measurement results.
You might sometimes want finer grained control. In this case, weâ€™re probably only interested in the results on
qubits 0 and 1, but .run_and_measure
returns the results for all eight qubits in the QVM. We can change our program
to be more particular about what we want.
from pyquil import Program
from pyquil.gates import *
p = Program()
ro = p.declare('ro', 'BIT', 2)
p += H(0)
p += CNOT(0, 1)
p += MEASURE(0, ro[0])
p += MEASURE(1, ro[1])
In the last two lines, weâ€™ve added our MEASURE
instructions, saying that we want to store the result of qubit 0
into the 0th bit of ro
, and the result of qubit 1 into the 1st bit of ro
. The following snippet could be a
useful way to measure many qubits, in particular, on a lattice that doesnâ€™t start at qubit 0 (although you can
use the compiler to reindex your qubits):
qubits = [5, 6, 7]
# ...
for i, q in enumerate(qubits):
p += MEASURE(q, ro[i])
Note
The QPU can only handle MEASURE
final programs. You canâ€™t operate gates after measurements.
Specifying the number of trialsÂ¶
Quantum computing is inherently probabilistic. We often have to repeat the same experiment many times to get the
results we need. Sometimes we expect the results to all be the same, such as when we apply no gates, or only an X
gate. When we prepare a superposition state, we expect probabilistic outcomes, such as a 50% probability measuring 0 or 1.
The number of shots (also called trials) is the number of times to execute a program at once. This determines the length of the results that are returned.
As we saw above, the .run_and_measure
method of the QuantumComputer
object can handle multiple executions of a program.
If you would like more explicit control for representing multishot execution, another way to do this is
with .wrap_in_numshots_loop
. This puts the number of shots to be run in the representation of the program itself,
as opposed to in the arguments list of the execution method itself. Below, we specify that our program should
be executed 1000 times.
p = Program()
... # build up your program here...
p.wrap_in_numshots_loop(1000)
Note
Did You Know?
The word â€śshotâ€ť comes from experimental physics where an experiment is performed many times, and each result is called a shot.
Parametric CompilationÂ¶
Modern quantum algorithms are often parametric, following a hybrid model. In this hybrid model, the program ansatz (template of gates) is fixed, and iteratively updated with new parameters. These new parameters are often determined by an update given by a classical optimizer. Depending on the complexity of the algorithm, problem of interest, and capabilities of the classical optimizer, this loop may need to run many times. In order to efficiently operate within this hybrid model, parametric compilation can be used.
Parametric compilation allows one to compile the program ansatz just once. Making use of declared memory regions, we can load values to the parametric gates at execution time, after compilation. Taking the compiler out of the execution loop for programs like this offers a huge performance improvement compared to compiling the program each time a parameter update is required. (Some more details about this and an example are found here.)
The first step is to build our parametric program, which functions like a template for all the precise programs we will run. Below we create a simple example program to illustrate, which puts the qubit onto the equator of the Bloch Sphere and then rotates it around the Z axis for some variable angle theta before applying another X pulse and measuring.
import numpy as np
from pyquil import Program
from pyquil.gates import RX, RZ, MEASURE
qubit = 0
p = Program()
ro = p.declare("ro", "BIT", 1)
theta_ref = p.declare("theta", "REAL")
p += RX(np.pi / 2, qubit)
p += RZ(theta_ref, qubit)
p += RX(np.pi / 2, qubit)
p += MEASURE(qubit, ro[0])
Note
The example program, although simple, is actually more than just a toy example. It is similar to an experiment which measures the qubit frequency.
Notice how theta
hasnâ€™t been specified yet. The next steps will have to involve a QuantumComputer
or a compiler
implementation. For simplicity, we will demonstrate with a QuantumComputer
instance.
from pyquil import get_qc
# Get a Quantum Virtual Machine to simulate execution
qc = get_qc("1qqvm")
executable = qc.compile(p)
We are able to compile our program, even with theta
still not specified. Now we want to run our program with theta
filled in for, say, 200 values between \(0\) and \(2\pi\). We demonstrate this below.
# Somewhere to store each list of results
parametric_measurements = []
for theta in np.linspace(0, 2 * np.pi, 200):
# Get the results of the run with the value we want to execute with
bitstrings = qc.run(executable, {'theta': [theta]})
# Store our results
parametric_measurements.append(bitstrings)
In the example here, if you called qc.run(executable)
and didnâ€™t specify 'theta'
, the program would apply
RZ(0, qubit)
for every execution.
Note
Classical memory defaults to zero. If you donâ€™t specify a value for a declared memory region, it will be zero.
Gate ModifiersÂ¶
Gate applications in Quil can be preceded by a gate modifier. There are three supported modifiers:
DAGGER
, CONTROLLED
, and FORKED
. The DAGGER
modifier represents the dagger of the gate. For instance,
DAGGER RX(pi/3) 0
would have an equivalent effect to RX(pi/3) 0
.
The CONTROLLED
modifier takes a gate and makes it a controlled gate. For instance, one could write the Toffoli gate in any of the three following ways:
CCNOT 0 1 2
CONTROLLED CNOT 0 1 2
CONTROLLED CONTROLLED X 0 1 2
Note
The letter C
in the gate name has no semantic significance in Quil. To make a controlled Y
gate, one cannot write CY
, but rather one has to write CONTROLLED Y
.
The FORKED
modifier allows for a parametric gate to be applied, with the specific choice of parameters conditional on a qubit value. For a parametric gate G
with k parameters,
FORKED G(u1, ..., uk, v1, ..., vk) c q1 ... qn
is equivalent to
if c == 0:
G(u1, ..., uk) q1 ... qn
else if c == 1:
G(v1, ..., vk) q1 ... qn
extended by linearity for general c
. Note that the total number of parameters in the forked gate has doubled.
All gates (objects deriving from the Gate
class) provide the
methods Gate.dagger()
, Gate.controlled(control_qubit)
, and Gate.forked(fork_qubit, alt_params)
that
can be used to programmatically apply the DAGGER
, CONTROLLED
, and FORKED
modifiers.
For example, to produce the controlledNOT gate (CNOT
) with
control qubit 0
and target qubit 1
prog = Program(X(1).controlled(0))
To produce the doublycontrolled NOT gate (CCNOT
) with
control qubits 0
and 1
and target qubit 2
you can stack
the controlled
modifier, or simply pass a list of control qubits
prog = Program(X(2).controlled(0).controlled(1))
prog = Program(X(2).controlled([0, 1]))
You can achieve the oftused controloff gate (flip the target qubit
1
if the control qubit 0
is zero) with
prog = Program(X(0), X(1).controlled(0), X(0))
The gate FORKED RX(pi/2, pi) 0 1
may be produced by
prog = Program(RX(np.pi/2, 1).forked(0, [np.pi]))
Defining New GatesÂ¶
New gates can be easily added inline to Quil programs. All you need is a matrix representation of the gate. For example, below we define a \(\sqrt{X}\) gate.
import numpy as np
from pyquil import Program
from pyquil.quil import DefGate
# First we define the new gate from a matrix
sqrt_x = np.array([[ 0.5+0.5j, 0.50.5j],
[ 0.50.5j, 0.5+0.5j]])
# Get the Quil definition for the new gate
sqrt_x_definition = DefGate("SQRTX", sqrt_x)
# Get the gate constructor
SQRT_X = sqrt_x_definition.get_constructor()
# Then we can use the new gate
p = Program()
p += sqrt_x_definition
p += SQRT_X(0)
print(p)
DEFGATE SQRTX:
0.5+0.5i, 0.50.5i
0.50.5i, 0.5+0.5i
SQRTX 0
Below we show how we can define \(X_0\otimes \sqrt{X_1}\) as a single gate.
# A multiqubit defgate example
x_gate_matrix = np.array(([0.0, 1.0], [1.0, 0.0]))
sqrt_x = np.array([[ 0.5+0.5j, 0.50.5j],
[ 0.50.5j, 0.5+0.5j]])
x_sqrt_x = np.kron(x_gate_matrix, sqrt_x)
Now we can use this gate in the same way that we used SQRT_X
, but we will pass it two arguments
rather than one, since it operates on two qubits.
x_sqrt_x_definition = DefGate("XSQRTX", x_sqrt_x)
X_SQRT_X = x_sqrt_x_definition.get_constructor()
# Then we can use the new gate
p = Program(x_sqrt_x_definition, X_SQRT_X(0, 1))
Tip
To inspect the wavefunction that will result from applying your new gate, you can use
the Wavefunction Simulator
(e.g. print(WavefunctionSimulator().wavefunction(p))
).
Defining Parametric GatesÂ¶
Letâ€™s say we want to have a controlled RX gate. Since RX is a parametric gate, we need a slightly different way of defining it than in the previous section.
from pyquil import Program, WavefunctionSimulator
from pyquil.quilatom import Parameter, quil_sin, quil_cos
from pyquil.quilbase import DefGate
import numpy as np
# Define the new gate from a matrix
theta = Parameter('theta')
crx = np.array([
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, quil_cos(theta / 2), 1j * quil_sin(theta / 2)],
[0, 0, 1j * quil_sin(theta / 2), quil_cos(theta / 2)]
])
gate_definition = DefGate('CRX', crx, [theta])
CRX = gate_definition.get_constructor()
# Create our program and use the new parametric gate
p = Program()
p += gate_definition
p += H(0)
p += CRX(np.pi/2)(0, 1)
quil_sin
and quil_cos
work as the regular sines and cosines, but they support the parametrization. Parametrized
functions you can use with pyQuil are: quil_sin
, quil_cos
, quil_sqrt
, quil_exp
, and quil_cis
.
Tip
To inspect the wavefunction that will result from applying your new gate, you can use
the Wavefunction Simulator
(e.g. print(WavefunctionSimulator().wavefunction(p))
).
Defining Permutation GatesÂ¶
Note
quilc
supports permutation gate syntax since version 1.8.0
. pyQuil introduced support in version 2.8.0
.
Some gates can be compactly represented as a permutation. For example, CCNOT
gate can be represented by the matrix
import numpy as np
from pyquil.quilbase import DefGate
ccnot_matrix = np.array([
[1, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 1, 0]
])
ccnot_gate = DefGate("CCNOT", ccnot_matrix)
# etc
It can equivalently be defined by the permutation
import numpy as np
from pyquil.quilbase import DefPermutationGate
ccnot_gate = DefPermutationGate("CCNOT", [0, 1, 2, 3, 4, 5, 7, 6])
# etc
PragmasÂ¶
PRAGMA
directives give users more control over how Quil programs are processed or simulated but generally do not
change the semantics of the Quil program itself. As a general rule of thumb, deleting all PRAGMA
directives in a Quil
program should leave a valid and semantically equivalent program.
In pyQuil, PRAGMA
directives play many roles, such as controlling the behavior of gates in noisy simulations,
or commanding the Quil compiler to perform actions in a certain way. Here, we will cover the basics of two very
common use cases for including a PRAGMA
in your program: qubit rewiring and delays. For a more comprehensive
review of what pragmas are and what the compiler supports, check out The Quil Compiler. For more information about
PRAGMA
in Quil, see
A Practical Quantum ISA, and
Simulating Quantum Processor Errors.
Specifying A Qubit Rewiring SchemeÂ¶
Qubit rewiring is one of the most powerful features of the Quil compiler. We are able to write Quil programs which are agnostic to the topology of the chip, and the compiler will intelligently relabel our qubits to give better performance.
When we intend to run a program on the QPU, sometimes we write programs which use specific qubits targeting a specific device topology, perhaps to achieve a highperformance program. Other times, we write programs that are agnostic to the underlying topology, thereby making the programs more portable. Qubit rewiring accommodates both use cases in an automatic way.
Consider the following program.
from pyquil import Program
from pyquil.gates import *
p = Program(X(3))
Weâ€™ve tested this on the QVM, and weâ€™ve reserved a lattice on the QPU which has qubits 4, 5, and 6, but not qubit 3. Rather than rewrite our program for each reservation, we modify our program to tell the compiler to do this for us.
from pyquil.quil import Pragma
p = Program(Pragma('INITIAL_REWIRING', ['"GREEDY"']))
p += X(3)
Now, when we pass our program through the compiler (such as with QuantumComputer.compile()
) we will get native Quil
with the qubit reindexed to one of 4, 5, or 6. If qubit 3 is available, and we donâ€™t want that pulse to be applied to
any other qubit, we would instead use Pragma('INITIAL_REWIRING', ['"NAIVE"']]
. Detailed information about the
available options is here.
Note
In general, we assume that the qubits youâ€™re supplying as input are also the ones which you prefer to operate on, and so NAIVE rewiring is the default.
Asking for a DelayÂ¶
At times, we may want to add a delay in our program. Usually this is associated with qubit characterization. Delays
are not regular gate operations, and they do not affect the abstract semantics of the Quil program, so theyâ€™re implemented with a PRAGMA
directive.
# ...
# qubit index and time in seconds must be defined and provided
# the time argument accepts exponential notation e.g. 200e9
p += Pragma('DELAY', [qubit], str(time))
Warning
These delays currently have effects on the real QPU. They have no effect on QVMâ€™s even when those QVMâ€™s have noise models applied.
Warning
Keep in mind, the program duration is currently capped at 15 seconds, and the length of the program is multiplied by the number of shots. If you have a 1000 shot program, where each shot contains a 100ms delay, you wonâ€™t be able to execute it.
Ways to Construct ProgramsÂ¶
PyQuil supports a variety of methods for constructing programs however you prefer.
Multiple instructions can be applied at once, and programs can be added together. PyQuil can also produce a
Program
by interpreting raw Quil text. You can still use the more pyQuil 1.X style of using
the .inst
method to add instruction gates. Thus, the following are all valid programs:
# Preferred method
p = Program()
p += X(0)
p += Y(1)
print(p)
# Multiple instructions in declaration
print(Program(X(0), Y(1)))
# A composition of two programs
print(Program(X(0)) + Program(Y(1)))
# Raw Quil with newlines
print(Program("X 0\nY 1"))
# Raw Quil comma separated
print(Program("X 0", "Y 1"))
# Chained inst; less preferred
print(Program().inst(X(0)).inst(Y(1)))
All of the above methods will produce the same output:
X 0
Y 1
The pyquil.parser
submodule provides a frontend to other similar parser
functionality.
Fixing a Mistaken InstructionÂ¶
If an instruction was appended to a program incorrectly, you can pop it off.
p = Program(X(0), Y(1))
print(p)
print("We can fix by popping:")
p.pop()
print(p)
X 0
Y 1
We can fix by popping:
X 0
QPUallowable QuilÂ¶
Apart from DECLARE
and PRAGMA
directives, a program must break into the following three regions, each optional:
A
RESET
command.A sequence of quantum gate applications.
A sequence of
MEASURE
commands.
The only memory that is writeable is the region named ro
, and only through MEASURE
instructions. All other
memory is readonly.
The keyword SHARING
is disallowed.
Compilation is unavailable for invocations of DEFGATE
s with parameters read from classical memory.
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 QuantumComputer object.
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 simulates the unitary evolution of a wavefunction with classical control. The QVM has a plethora of other features, including:
Stochastic purestate evolution, density matrix evolution, and Pauli noise channels;
Shared memory access to the quantum state, allowing direct NumPy access to the state without copying or transmission delay; and
A fast justintime compilation mode for rapid simulation of large programs with many qubits.
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.)
[20181106 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 < example.quil
[20181130 11:13:58] Reading program.
[20181130 11:13:58] Allocating memory for QVM of 2 qubits.
[20181130 11:13:58] Allocation completed in 4 ms.
[20181130 11:13:58] Loading quantum program.
[20181130 11:13:58] Executing quantum program.
[20181130 11:13:58] Execution completed in 6 ms.
[20181130 11:13:58] Printing 2qubit state.
[20181130 11:13:58] Amplitudes:
[20181130 11:13:58] 00>: 0.0, P= 0.0%
[20181130 11:13:58] 01>: 0.01.0i, P=100.0%
[20181130 11:13:58] 10>: 0.0, P= 0.0%
[20181130 11:13:58] 11>: 0.0, P= 0.0%
[20181130 11:13:58] Classical memory (low > high indexes):
[20181130 11:13:58] ro: 1 0
The QVM offers a simple benchmarking mode with qvm verbose
benchmark
. Example output looks like this:
$ ./qvm verbose benchmark
******************************
* Welcome to the Rigetti QVM *
******************************
Copyright (c) 20162019 Rigetti Computing.
(Configured with 8192 MiB of workspace and 8 workers.)
<135>1 20190501T18:26:14Z workstation.local qvm 96177   Selected simulation method: purestate
<135>1 20190501T18:26:15Z workstation.local qvm 96177   Computing baseline serial norm timing...
<135>1 20190501T18:26:15Z workstation.local qvm 96177   Baseline serial norm timing: 96 ms
<135>1 20190501T18:26:15Z workstation.local qvm 96177   Starting "bell" benchmark with 26 qubits...
; Transition H 0 took 686 ms (gc: 0 ms; alloc: 65536 bytes)
; Transition CNOT 0 1 took 651 ms (gc: 0 ms; alloc: 0 bytes)
; Transition CNOT 1 2 took 658 ms (gc: 0 ms; alloc: 32656 bytes)
; Transition CNOT 2 3 took 661 ms (gc: 0 ms; alloc: 0 bytes)
; Transition CNOT 3 4 took 650 ms (gc: 0 ms; alloc: 0 bytes)
; Transition CNOT 4 5 took 662 ms (gc: 0 ms; alloc: 0 bytes)
; Transition CNOT 5 6 took 673 ms (gc: 0 ms; alloc: 0 bytes)
[...]
<135>1 20190501T18:30:13Z workstation.local qvm 96288   Total time for program run: 24385 ms
The QVM also has mode for faster execution of long quantum programs
operating on a large number of qubits, called compiled
mode. Compiled mode can be enabled by adding c
to the command
line options. Observe the speedup in the benchmark:
$ ./qvm verbose benchmark c
******************************
* Welcome to the Rigetti QVM *
******************************
Copyright (c) 20162019 Rigetti Computing.
(Configured with 8192 MiB of workspace and 8 workers.)
<135>1 20190501T18:28:07Z workstation.local qvm 96285   Selected simulation method: purestate
<135>1 20190501T18:28:08Z workstation.local qvm 96285   Computing baseline serial norm timing...
<135>1 20190501T18:28:08Z workstation.local qvm 96285   Baseline serial norm timing: 95 ms
<135>1 20190501T18:28:08Z workstation.local qvm 96285   Starting "bell" benchmark with 26 qubits...
; Compiling program loaded into QVM...
; Compiled in 87 ms.
; Optimization eliminated 26 instructions ( 50.0%).
; Transition compiled{ FUSEDGATE0 1 0 } took 138 ms (gc: 0 ms; alloc: 0 bytes)
; Transition compiled{ CNOT 1 2 } took 144 ms (gc: 0 ms; alloc: 0 bytes)
; Transition compiled{ CNOT 2 3 } took 137 ms (gc: 0 ms; alloc: 0 bytes)
; Transition compiled{ CNOT 3 4 } took 143 ms (gc: 0 ms; alloc: 0 bytes)
; Transition compiled{ CNOT 4 5 } took 95 ms (gc: 0 ms; alloc: 0 bytes)
; Transition compiled{ CNOT 5 6 } took 75 ms (gc: 0 ms; alloc: 0 bytes)
[...]
<135>1 20190501T18:29:12Z workstation.local qvm 96287   Total time for program run: 2416 ms
The runtime reduced to 2.4 seconds from 24 seconds, a 10x speedup.
Note
Compiled mode speeds up the execution of a program at the cost of an initial compilation. Note in the above example that compilation took 87 ms. If you are running small programs with low qubit counts, this cost may be significant, and it may be worth executing in the usual (â€śinterpretedâ€ť) mode. However, if your programs contain a large number of qubits or a large number of instructions, the initial cost is far outweighed by the benefits.
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 (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 a BinaryExecutableRequest
object, 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
classical binary readout results.
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 aQVM
orQPU
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}qqvm")
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('9qsquareqvm') # not general to any number of qubits, 9qsquareqvm 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("8qqvm")
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 lowerlevel .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("8qqvm")
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)
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 topologies of the devices 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 = 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.
The Wavefunction SimulatorÂ¶
Formerly a part of the QVM object in pyQuil, the Wavefunction Simulator allows you to directly inspect the wavefunction of a quantum state prepared by your program. Because of the probabilistic nature of quantum information, the programs youâ€™ll be running on the QPU can give a distribution of outputs. When running on the QPU or QVM, you would aggregate results (anywhere from tens of trials to 100k+!) that you can sample to get back a distribution.
With the Wavefunction Simulator, you can look at the distribution without having to collect samples from your program. This can save a lot of time for small programs. Letâ€™s walk through a basic example of using WavefunctionSimulator:
from pyquil import Program
from pyquil.gates import *
from pyquil.api import WavefunctionSimulator
wf_sim = WavefunctionSimulator()
coin_flip = Program(H(0))
wf_sim.wavefunction(coin_flip)
<pyquil.wavefunction.Wavefunction at 0x1088a2c10>
The return value is a Wavefunction object that stores the amplitudes of the quantum state. We can print this object
coin_flip = Program(H(0))
wavefunction = wf_sim.wavefunction(coin_flip)
print(wavefunction)
(0.7071067812+0j)0> + (0.7071067812+0j)1>
to see the amplitudes listed as a sum of computational basis states. We can index into those amplitudes directly or look at a dictionary of associated outcome probabilities.
assert wavefunction[0] == 1 / np.sqrt(2)
# The amplitudes are stored as a numpy array on the Wavefunction object
print(wavefunction.amplitudes)
prob_dict = wavefunction.get_outcome_probs() # extracts the probabilities of outcomes as a dict
print(prob_dict)
prob_dict.keys() # these store the bitstring outcomes
assert len(wavefunction) == 1 # gives the number of qubits
[ 0.70710678+0.j 0.70710678+0.j]
{'1': 0.49999999999999989, '0': 0.49999999999999989}
It is important to remember that this wavefunction
method is a useful debugging tool for small quantum systems, and
cannot be feasibly obtained on a quantum processor.
MultiQubit Basis EnumerationÂ¶
The WavefunctionSimulator enumerates bitstrings such that qubit 0
is the least significant bit (LSB)
and therefore on the right end of a bitstring as shown in the table below which contains some
examples.
bitstring 
qubit_(n1) 
â€¦ 
qubit_2 
qubit_1 
qubit_0 

1â€¦101 
1 
â€¦ 
1 
0 
1 
0â€¦110 
0 
â€¦ 
1 
1 
0 
This convention is counter to that often found in the quantum computing literature where
bitstrings are often ordered such that the lowestindex qubit is on the left.
The vector representation of a wavefunction assumes the â€ścanonicalâ€ť ordering of basis elements.
I.e., for two qubits this order is 00, 01, 10, 11
.
In the typical Dirac notation for quantum states, the tensor product of two different degrees of
freedom is not always explicitly understood as having a fixed order of those degrees of freedom.
This is in contrast to the kronecker product between matrices which uses the same mathematical
symbol and is clearly not commutative.
This, however, becomes important when writing things down as coefficient vectors or matrices:
As a consequence there arise some subtle but important differences in the ordering of wavefunction and multiqubit gate matrix coefficients. According to our conventions the matrix
corresponds to the Quil instruction CNOT(1, 0)
which is counter to how most other people in the
field order their tensor product factors (or more specifically their kronecker products).
In this convention CNOT(0, 1)
is given by
For additional information why we decided on this basis ordering check out our note Someone shouts, â€ś01000>!â€ť Who is Excited?.
The Quil CompilerÂ¶
Expectations for Program ContentsÂ¶
The QPUs have much more limited natural gate sets than the standard gate set offered by pyQuil: on
Rigetti QPUs, the gate operators are constrained to lie in RZ(Î¸)
, RX(k*Ď€/2)
, CZ
and
XY
; and the gates are required to act on physically available hardware (for singlequbit gates,
this means acting only on live qubits, and for qubitpair gates, this means acting on neighboring
qubits). However, as a programmer, it is often (though not always) desirable to to be able to write
programs which donâ€™t take these details into account. This generally leads to more portable code if
one isnâ€™t tied to a specific set of gates or QPU architecture. To ameliorate these limitations, the
Rigetti software toolkit contains an optimizing compiler that translates arbitrary Quil to native
Quil and native Quil to executables suitable for Rigetti hardware.
Interacting with the CompilerÂ¶
After installing the SDK, the Quil Compiler, quilc
is available on your
local machine. You can initialize a local quilc
server by typing quilc R
into your
terminal. You should see the following message.
$ quilc S
++
 W E L C O M E 
 T O T H E 
 R I G E T T I 
 Q U I L 
 C O M P I L E R 
++
Copyright (c) 2018 Rigetti Computing.
...  Launching quilc.
...  Spawning server at (tcp://*:5555) .
To get a description of quilc
and its options and examples of command line use, see the quilc README or type man quilc
in your terminal.
A QuantumComputer
object supplied by the function pyquil.api.get_qc()
comes equipped with a
connection to your local Rigetti Quil compiler. This can be accessed using the instance method .compile()
,
as in the following:
from pyquil.quil import Pragma, Program
from pyquil.api import get_qc
from pyquil.gates import CNOT, H
qc = get_qc("9qsquareqvm")
ep = qc.compile(Program(H(0), CNOT(0,1), CNOT(1,2)))
print(ep.program) # here ep is of type PyquilExecutableResponse, which is not always inspectable
with output
RZ(pi/2) 0
RX(pi/2) 0
RZ(pi/2) 1
RX(pi/2) 1
CZ 1 0
RX(pi/2) 1
RZ(pi/2) 2
RX(pi/2) 2
CZ 2 1
RZ(pi/2) 0
RZ(pi/2) 1
RX(pi/2) 2
RZ(pi/2) 2
The compiler connection is also available directly via the property qc.compiler
. The
precise class of this object changes based on context (e.g., QPUCompiler
,
QVMCompiler
), but it always conforms to the interface laid out by pyquil.api._qac
:
compiler.quil_to_native_quil(program, *, protoquil)
: This method converts a Quil program into native Quil, according to the ISA that the compiler is initialized with. The input parameter is specified as aProgram
object. The optionalprotoquil
keyword argument instructs the compiler to restrict both its input and output to protoquil (Quil code that can be executed on a QPU). If the server is started with theP
option, or you specifyprotoquil=True
the returnedProgram
object will be equipped with a.metadata
property that gives extraneous information about the compilation output (e.g., gate depth, as well as many others). This call blocks until Quil compilation finishes.compiler.native_quil_to_executable(nq_program)
: This method converts a native Quil program, which is promised to consist only of native gates for a given ISA, into an executable suitable for submission to one of a QVM or a QPU. This call blocks until the executable is generated.
The instance method qc.compile
described above is a combination of these two methods: first the
incoming Quil is nativized, and then that is immediately turned into an executable. Accordingly,
the previous example snippet is identical to the following:
from pyquil.quil import Pragma, Program
from pyquil.api import get_qc
from pyquil.gates import CNOT, H
qc = get_qc("9qsquareqvm")
p = Program(H(0), CNOT(0,1), CNOT(1,2))
np = qc.compiler.quil_to_native_quil(p, protoquil=True)
print(np.metadata)
ep = qc.compiler.native_quil_to_executable(np)
print(ep.program) # here ep is of type PyquilExecutableResponse, which is not always inspectable
TimeoutsÂ¶
If your circuit is sufficiently complex the compiler may require more time than is permitted by
default (10
seconds). To change this timeout, either use the compiler_timeout option to
get_qc
qc = get_qc(..., compiler_timeout=100) # 100 seconds
or use the set_timeout method on the compiler object:
qc = get_qc(...)
qc.compiler.set_timeout(100) # 100 seconds
The timeout is specified in units of seconds.
Legal compiler inputÂ¶
The QPU is not able to execute all possible Quil programs. At present, a Quil program qualifies for execution if has the following form:
The program may begin with a
RESET
instruction. (If provided, the QPU will actively reset the state of the quantum device to the ground state before program execution. If omitted, the QPU will wait for a relaxation period to pass before program execution instead.)This is then followed by a block of native quantum gates. A gate is native if it is of the form
RZ(Î¸)
for any valueÎ¸
,RX(k*Ď€/2)
for an integerk
, orCZ q0 q1
forq0
,q1
a pair of qubits participating in a qubitqubit interaction. Some devices provide theXY(Î¸) q0 q1
two qubit gate.This is then followed by a block of
MEASURE
instructions.
To instruct the compiler to produce Quil code that can be executed on a QPU, you can use the
protoquil
keyword in a call to compiler.quil_to_native_quil(program, protoquil=True)
or
qc.compile(program, protoquil=True)
.
Note
If your compiler server is started with the protoquil option P
(as is the case for your QMI
compiler) then specifying protoquil=False
will override the server and forcefully disable
protoquil. Specifying protoquil=None
defers to the serverâ€™s choice.
Compilation metadataÂ¶
When your compiler is started with the P
option, the compiler.quil_to_native_quil()
method
will return both the compiled program and a dictionary of statistics for the compiled program. This
dictionary contains the keys
final_rewiring
: see section below on rewirings.gate_depth
: the longest subsequence of compiled instructions where adjacent instructions share resources.multiqubit_gate_depth
: likegate_depth
but restricted to multiqubit gates.gate_volume
: total number of gates in the compiled program.program_duration
: program duration with parallel executation of gates (using hardcoded values of individual gate durations).qpu_runtime_estimation
: estimated runtime on a Rigetti QPU (in milliseconds). This is extrapolated from a single shot of a 16Q program with final measurements on all 16 qubits. If you are running a parametric program then you should estimate the total runtime assize of parameter space * estimated runtime of single shot
. This should be treated only as an approximation.program_fidelity
: the estimated fidelity of the compiled program.topological_swaps
: the number of topological swaps incurred during compilation of the program.
For example, to inspect the qpu_runtime_estimation
you might do the following:
from pyquil import get_qc, Program
# If you have a reserved lattice, use it here
qc = get_qc("Aspen44QA")
# Otherwise use a QVM
# qc = get_qc("8qqvm")
# Likely you will have a more complex program:
p = Program("RX(pi) 0")
native_p = qc.compiler.quil_to_native_quil(p)
# The program will now have only native gates
print(native_p)
# And also a dictionary, with the above keys
print(native_p.native_quil_metadata["qpu_runtime_estimation"])
Regionspecific compiler features through PRAGMAÂ¶
The Quil compiler can also be communicated with through PRAGMA
commands embedded in the Quil
program.
Note
The interface to the Quil compiler from pyQuil is under construction, and some of the PRAGMA
directives will soon be replaced by finergrained method calls.
Preserved regionsÂ¶
The compiler can be circumvented in userspecified regions. The start of such a region is denoted by
PRAGMA PRESERVE_BLOCK
, and the end is denoted by PRAGMA END_PRESERVE_BLOCK
. The Quil
compiler promises not to modify any instructions contained in such a region.
Warning
If a preserved block is not legal QPU input, then it is not guaranteed to execute or it may produced unexpected results.
The following is an example of a program that prepares a Bell state on qubits 0 and 1, then performs
a time delay to invite noisy system interaction before measuring the qubits. The time delay region
is marked by PRAGMA PRESERVE_BLOCK
and PRAGMA END_PRESERVE_BLOCK
; without these delimiters,
the compiler will remove the identity gates that serve to provide the time delay. However, the
regions outside of the PRAGMA
region will still be compiled, converting the Bell state preparation
to the native gate set.
DECLARE ro BIT[2]
# prepare a Bell state
H 0
CNOT 0 1
# wait a while
PRAGMA PRESERVE_BLOCK
I 0
I 1
I 0
I 1
# ...
I 0
I 1
PRAGMA END_PRESERVE_BLOCK
# and read out the results
MEASURE 0 ro[0]
MEASURE 1 ro[1]
Parallelizable regionsÂ¶
The compiler can sometimes arrange gate sequences more cleverly if the user gives it hints about
sequences of gates that commute. A region containing commuting sequences is bookended by
PRAGMA COMMUTING_BLOCKS
and PRAGMA END_COMMUTING_BLOCKS
; within such a region, a given
commuting sequence is bookended by PRAGMA BLOCK
and PRAGMA END_BLOCK
.
Warning
Lying to the compiler about what blocks can commute can cause incorrect results.
The following snippet demonstrates this hinting syntax in a context typical of VQEtype algorithms: after a first stage of performing some state preparation on individual qubits, there is a second stage of â€śmixing operationsâ€ť that both reuse qubit resources and mutually commute, followed by a final rotation and measurement. The following program is naturally laid out on a ring with vertices (read either clockwise or counterclockwise) as 0, 1, 2, 3. After scheduling the first round of preparation gates, the compiler will use the hinting to schedule the first and third blocks (which utilize qubit pairs 01 and 23) before the second and fourth blocks (which utilize qubit pairs 12 and 03), resulting in a reduction in circuit depth by one half. Without hinting, the compiler will instead execute the blocks in their written order.
DECLARE ro BIT[4]
# Stage one
H 0
H 1
H 2
H 3
# Stage two
PRAGMA COMMUTING_BLOCKS
PRAGMA BLOCK
CNOT 0 1
RZ(0.4) 1
CNOT 0 1
PRAGMA END_BLOCK
PRAGMA BLOCK
CNOT 1 2
RZ(0.6) 2
CNOT 1 2
PRAGMA END_BLOCK
PRAGMA BLOCK
CNOT 2 3
RZ(0.8) 3
CNOT 2 3
PRAGMA END_BLOCK
PRAGMA BLOCK
CNOT 0 3
RZ(0.9) 3
CNOT 0 3
PRAGMA END_BLOCK
PRAGMA END_COMMUTING_BLOCKS
# Stage three
H 0
H 1
H 2
H 3
MEASURE 0 ro[0]
MEASURE 1 ro[1]
MEASURE 2 ro[2]
MEASURE 3 ro[3]
RewiringsÂ¶
When a Quil program contains multiqubit instructions that do not name qubitqubit links present on a
target device, the compiler will rearrange the qubits so that execution becomes possible. In order to
help the user understand what rearrangement may have been done, the compiler emits comments at various
points in the raw Quil code (which is not currently visible from a pyQuil Program
objectâ€™s .out()
method): # Entering rewiring
and # Exiting rewiring
. From the perspective of the user, both
comments serve the same purpose: # Entering rewiring: #(n0 n1 ... nk)
indicates that the logical
qubit labeled j
in the program has been assigned to lie on the physical qubit labeled nj
on
the device. This is strictly for humanreadability: these comments are discarded and have no effect.
SWAPsÂ¶
When the compiler needs to move an instructionâ€™s qubits closer it will insert SWAP
gates which
can be costly. If, however, the swaps are inserted at the very beginning of the program, the
compiler can treat them as virtual swaps which do not appear in the resulting program but instead
affect the initial rewiring of the program.
For example, consider running a CZ
on nonneighboring qubits on a linear device:
import networkx as nx
from pyquil import Program, get_qc
from pyquil.api._quantum_computer import _get_qvm_with_topology
from pyquil.gates import CZ
graph = nx.from_edgelist([(0, 1), (1, 2)])
qc = _get_qvm_with_topology(name="line", topology=graph)
p = Program(CZ(0, 2))
print(qc.compile(p).program)
CZ 2 1
We see that the resulting program has only a single CZ
even though the original program would
usually require the insertion of a SWAP
gate. The compiler instead opted to just relabel (or
rewire) the qubits, thus not inflating the number of gates in the result.
For larger and more complex programs (with more entanglement) it may not always be possible to avoid
inserting swaps. For example, the following program requires a SWAP
that increases its gate depth:
import networkx as nx
from pyquil import Program, get_qc
from pyquil.api._quantum_computer import _get_qvm_with_topology
from pyquil.gates import H, CZ
graph = nx.from_edgelist([(0, 1), (1, 2)])
qc = _get_qvm_with_topology(name="line", topology=graph)
p = Program(CZ(0, 1), H(0), CZ(1, 2), CZ(0, 2))
print(qc.compile(p).program)
CZ 2 1
RX(pi/2) 2
RX(pi/2) 2
CZ 2 1
CZ 1 0
RZ(pi/2) 1
RX(pi/2) 1
RX(pi/2) 1
RX(pi/2) 2
RX(pi/2) 2
XY(pi) 2 1
RX(pi/2) 1
CZ 1 0
RZ(pi/2) 1
RX(pi/2) 2
RX(pi/2) 2
Note
SWAP
gates generally cost three CZ
gates or three XY
gates. However, if your device
has both CZ
and XY
gates available, then the compiler can produce a SWAP
gate that
uses only two twoqubit gates (one CZ
and one XY
).
Initial rewiringÂ¶
In addition, you have some control over how the compiler constructs its
rewiring, which is controlled by PRAGMA INITIAL_REWIRING
. The syntax is as follows.
# <type> can be NAIVE, RANDOM, PARTIAL, or GREEDY
#
# The double quotes are required.
PRAGMA INITIAL_REWIRING "<type>"
Including this before any nonpragmas will allow the compiler to alter its rewiring behavior.
Note
Each initial rewiring strategy is described in more detail after the discussion about defaults.
When no INITIAL_REWIRING
pragma is provided the compiler will choose one of two options
depending on the program:
NAIVE
: The qubits used in all instructions in the program satisfy the topological constraints of the device.PARTIAL
: Otherwise.
For example, if your program consists of twoqubit instructions where the qubits in each instruction are nearest neighbors on the device, the compiler will employ the native strategy:
from pyquil import Program, get_qc
from pyquil.gates import CZ
qc = get_qc("Aspen8", as_qvm=True)
p = Program(CZ(3, 4))
print(qc.compile(p).program)
CZ 3 4
In the above example, CZ 3 4 touches qubits that are already nearest neighbors (and support a CZ instruction) and so the compiler employs the naive strategy (and thus does not rewire those qubits to use better ones).
If however, the program uses qubits that must be rewired, then the compiler defaults to the partial strategy:
from pyquil import Program, get_qc
from pyquil.gates import CZ
qc = get_qc("Aspen8", as_qvm=True)
p = Program(CZ(3, 4))
print(qc.compile(p).program)
RZ(pi/2) 0
RX(pi/2) 0
RZ(pi/2) 0
RZ(pi/2) 1
XY(pi) 1 0
RZ(pi/2) 1
RX(pi/2) 1
RZ(pi/2) 1
XY(pi) 1 0
RZ(pi/2) 0
RX(pi/2) 0
In this mode, the compiler chooses the naive
mapping between logical qubits and physical qubits,
where logical qubit i
is assigned to physical qubit i
. With this initial rewiring, the
compiler will generally not move an instructionâ€™s qubits around even if it results in a poor
execution fidelity. For example assume that Aspen8
has a lowfidelity CZ 0 1
, then
compiling this program with naive rewiring will not move the CZ
to a better qubit pair:
from pyquil import Program, get_qc
from pyquil.gates import CZ
qc = get_qc("Aspen8", as_qvm=True)
p = Program('PRAGMA INITIAL_REWIRING "NAIVE"', CZ(0, 1))
print(qc.compile(p).program)
PRAGMA INITIAL_REWIRING "NAIVE"
CZ 0 1
If, however, your program includes an instruction that does not use neighboring qubits the compiler will be required to insert swaps (virtual or real, see swaps) that might affect the logicalphysical qubit mapping. For example,
from pyquil import Program, get_qc
from pyquil.gates import CZ
qc = get_qc("Aspen8", as_qvm=True)
p = Program('PRAGMA INITIAL_REWIRING "NAIVE"', CZ(0, 2))
print(qc.compile(p).program)
PRAGMA INITIAL_REWIRING "NAIVE"
CZ 6 5
In the above program CZ 0 2
is not a native instruction (meaning it cannot be directly executed
on the target device) and so the compiler must insert a swap (virtual, in this case) into the
program. When rewiring must occur in this mode it is not guaranteed that the resulting program
will have optimal fidelity.
In this mode, the compiler begins with an empty mapping from logical qubits to physical qubits. During the progression of compilation this mapping will be filledin, and thus at any point the mapping is said to be partial. Generally this gives the compiler the opportunity to assign a logicaltophysical qubit mapping that optimizes the fidelity of the resulting program by incorporating fidelity information about any qubit in the device ISA.
For example, if the instruction CZ 0 1
has poor fidelity, under the partial rewiring strategy
the compiler can find an alternative that improves the program fidelity:
from pyquil import Program, get_qc
from pyquil.gates import CZ
qc = get_qc("Aspen8", as_qvm=True)
p = Program('PRAGMA INITIAL_REWIRING "PARTIAL"', CZ(0, 1))
print(qc.compile(p).program)
PRAGMA INITIAL_REWIRING "PARTIAL"
CZ 20 27
Here the compiler sees that the instruction CZ 20 27
will produce a program with better fidelity
and so opts to reassign qubits in the original program.
In this mode, the compiler chooses an initial mapping between logical and physical qubits based upon
a greedy optimization of the distances between qubits used in the program and those available on
the device. When compared to the PARTIAL
strategy it is generally more efficient because it uses
a simple heuristic; however, it will also produce a program with worse overall fidelity. If
compilation feels too slow and youâ€™re willing to trade fidelity for compilation speed, then you may
see success with this strategy.
Generally, as quantum software engineers, we want to maximize the execution fidelity of our programs. In other cases, however, for example in QCVV, we want to have more control about where instructions are placed.
Desired effect 
Recommended initial rewiring strategy 

Maximize program execution fidelity 

Preserve, where possible, the qubits used in the input program 

Faster qubit allocation at expense of fidelity 

Note that each of these have drawbacks described in the sections above.
Common Error MessagesÂ¶
The compiler itself is subject to some limitations, and some of the more commonly observed errors follow:
! ! ! Error: Matrices do not lie in the same projective class.
The compiler attempted to decompose an operator as native Quil instructions, and the resulting instructions do not match the original operator. This can happen when the original operator is not a unitary matrix, and could indicate an invalidDEFGATE
block. In some rare circumstances, it can also happen due to floating point precision issues. In the latter case, the issue is resolved simply by recompiling the program. If you issue cannot be solved, please contact support@rigetti.com or post an issue to the github project page.
Noise and Quantum ComputationÂ¶
Modeling Noisy Quantum GatesÂ¶
Pure States vs. Mixed StatesÂ¶
Errors in quantum computing can introduce classical uncertainty in what the underlying state is. When this happens we sometimes need to consider not only wavefunctions but also probabilistic sums of wavefunctions when we are uncertain as to which one we have. For example, if we think that an X gate was accidentally applied to a qubit with a 5050 chance then we would say that there is a 50% chance we have the \(\ket{0}\) state and a 50% chance that we have a \(\ket{1}\) state. This is called an â€śimpureâ€ť or â€śmixedâ€ťstate in that it isnâ€™t just a wavefunction (which is pure) but instead a distribution over wavefunctions. We describe this with something called a density matrix, which is generally an operator. Pure states have very simple density matrices that we can write as an outer product of a ket vector \(\ket{\psi}\) with its own bra version \(\bra{\psi}=\ket{\psi}^\dagger\). For a pure state the density matrix is simply
The expectation value of an operator for a mixed state is given by
where \(\tr{\cdot}\) is the trace of an operator, which is the sum of its diagonal elements, which is independent of choice of basis. Pure state density matrices satisfy
which you can easily verify for \(\rho_\psi\) assuming that the state is normalized. If we want to describe a situation with classical uncertainty between states \(\rho_1\) and \(\rho_2\), then we can take their weighted sum
where \(p\in [0,1]\) gives the classical probability that the state is \(\rho_1\).
Note that classical uncertainty in the wavefunction is markedly different from superpositions. We can represent superpositions using wavefunctions, but use density matrices to describe distributions over wavefunctions. You can read more about density matrices here [DensityMatrix].
Quantum Gate ErrorsÂ¶
For a quantum gate given by its unitary operator \(U\), a â€śquantum gate errorâ€ť describes the scenario in which the actually induced transformation deviates from \(\ket{\psi} \mapsto U\ket{\psi}\). There are two basic types of quantum gate errors:
coherent errors are those that preserve the purity of the input state, i.e., instead of the above mapping we carry out a perturbed, but unitary operation \(\ket{\psi} \mapsto \tilde{U}\ket{\psi}\), where \(\tilde{U} \neq U\).
incoherent errors are those that do not preserve the purity of the input state, in this case we must actually represent the evolution in terms of density matrices. The state \(\rho := \ket{\psi}\bra{\psi}\) is then mapped as
\[\rho \mapsto \sum_{j=1}^m K_j\rho K_j^\dagger,\]where the operators \(\{K_1, K_2, \dots, K_m\}\) are called Kraus operators and must obey \(\sum_{j=1}^m K_j^\dagger K_j = I\) to conserve the trace of \(\rho\). Maps expressed in the above form are called Kraus maps. It can be shown that every physical map on a finite dimensional quantum system can be represented as a Kraus map, though this representation is not generally unique. You can find more information about quantum operations here
In a way, coherent errors are in principle amendable by more precisely calibrated control. Incoherent errors are more tricky.
Why Do Incoherent Errors Happen?Â¶
When a quantum system (e.g., the qubits on a quantum processor) is not perfectly isolated from its environment it generally coevolves with the degrees of freedom it couples to. The implication is that while the total time evolution of system and environment can be assumed to be unitary, restriction to the system state generally is not.
Letâ€™s throw some math at this for clarity: Let our total Hilbert space be given by the tensor product of system and environment Hilbert spaces: \(\mathcal{H} = \mathcal{H}_S \otimes \mathcal{H}_E\). Our system â€śnot being perfectly isolatedâ€ť must be translated to the statement that the global Hamiltonian contains a contribution that couples the system and environment:
where \(V\) nontrivally acts on both the system and the environment. Consequently, even if we started in an initial state that factorized over system and environment \(\ket{\psi}_{S,0}\otimes \ket{\psi}_{E,0}\) if everything evolves by the SchrĂ¶dinger equation
the final state will generally not admit such a factorization.
A Toy ModelÂ¶
In this (somewhat technical) section we show how environment interaction can corrupt an identity gate and derive its Kraus map. For simplicity, let us assume that we are in a reference frame in which both the system and environment Hamiltonianâ€™s vanish \(H_S = 0, H_E = 0\) and where the crosscoupling is small even when multiplied by the duration of the time evolution \(\\frac{tV}{\hbar}\^2 \sim \epsilon \ll 1\) (any operator norm \(\\cdot\\) will do here). Let us further assume that \(V = \sqrt{\epsilon} V_S \otimes V_E\) (the more general case is given by a sum of such terms) and that the initial environment state satisfies \(\bra{\psi}_{E,0} V_E\ket{\psi}_{E,0} = 0\). This turns out to be a very reasonable assumption in practice but a more thorough discussion exceeds our scope.
Then the joint system + environment state \(\rho = \rho_{S,0} \otimes \rho_{E,0}\) (now written as a density matrix) evolves as
Using the BakerCampbellHausdorff theorem we can expand this to second order in \(\epsilon\)
We can insert the initially factorizable state \(\rho = \rho_{S,0} \otimes \rho_{E,0}\) and trace over the environmental degrees of freedom to obtain
where the coefficient in front of the second part is by our initial assumption very small \(\gamma := \frac{\epsilon t^2}{2\hbar^2}\tr{V_E^2 \rho_{E,0}} \ll 1\). This evolution happens to be approximately equal to a Kraus map with operators \(K_1 := I  \frac{\gamma}{2} V_S^2, K_2:= \sqrt{\gamma} V_S\):
This agrees to \(O(\epsilon^{3/2})\) with the result of our derivation above. This type of derivation can be extended to many other cases with little complication and a very similar argument is used to derive the Lindblad master equation.
Noisy Gates on the Rigetti QVMÂ¶
As of today, users of our Forest SDK can annotate their Quil programs by certain pragma statements that inform the QVM that a particular gate on specific target qubits should be replaced by an imperfect realization given by a Kraus map.
The QVM propagates pure states â€” so how does it simulate noisy gates? It does so by yielding the correct outcomes in the average over many executions of the Quil program: When the noisy version of a gate should be applied the QVM makes a random choice which Kraus operator is applied to the current state with a probability that ensures that the average over many executions is equivalent to the Kraus map. In particular, a particular Kraus operator \(K_j\) is applied to \(\ket{\psi}_S\)
with probability \(p_j:= \bra{\psi}_S K_j^\dagger K_j \ket{\psi}_S\). In the average over many execution \(N \gg 1\) we therefore find that
where \(j_n\) is the chosen Kraus operator label in the \(n\)th trial. This is clearly a Kraus map itself! And we can group identical terms and rewrite it as
where \(N_{\ell}\) is the number of times that Kraus operator label \(\ell\) was selected. For large enough \(N\) we know that \(N_{\ell} \approx N p_\ell\) and therefore
which proves our claim. The consequence is that noisy gate simulations must generally be repeated many times to obtain representative results.
Getting StartedÂ¶
Come up with a good model for your noise. We will provide some examples below and may add more such examples to our public repositories over time. Alternatively, you can characterize the gate under consideration using Quantum Process Tomography or Gate Set Tomography and use the resulting process matrices to obtain a very accurate noise model for a particular QPU.
Define your Kraus operators as a list of numpy arrays
kraus_ops = [K1, K2, ..., Km]
.For your Quil program
p
, call:p.define_noisy_gate("MY_NOISY_GATE", [q1, q2], kraus_ops)
where you should replace
MY_NOISY_GATE
with the gate of interest andq1, q2
with the indices of the qubits.
Scroll down for some examples!
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import binom
import matplotlib.colors as colors
%matplotlib inline
from pyquil import Program, get_qc
from pyquil.gates import CZ, H, I, X, MEASURE
from scipy.linalg import expm
# We could ask for "2qnoisyqvm" but we will be specifying
# our noise model as PRAGMAs on the Program itself.
qc = get_qc('2qqvm')
Example 1: Amplitude DampingÂ¶
Amplitude damping channels are imperfect identity maps with Kraus operators
where \(p\) is the probability that a qubit in the \(\ket{1}\) state decays to the \(\ket{0}\) state.
def damping_channel(damp_prob=.1):
"""
Generate the Kraus operators corresponding to an amplitude damping
noise channel.
:params float damp_prob: The onestep damping probability.
:return: A list [k1, k2] of the Kraus operators that parametrize the map.
:rtype: list
"""
damping_op = np.sqrt(damp_prob) * np.array([[0, 1],
[0, 0]])
residual_kraus = np.diag([1, np.sqrt(1damp_prob)])
return [residual_kraus, damping_op]
def append_kraus_to_gate(kraus_ops, g):
"""
Follow a gate `g` by a Kraus map described by `kraus_ops`.
:param list kraus_ops: The Kraus operators.
:param numpy.ndarray g: The unitary gate.
:return: A list of transformed Kraus operators.
"""
return [kj.dot(g) for kj in kraus_ops]
def append_damping_to_gate(gate, damp_prob=.1):
"""
Generate the Kraus operators corresponding to a given unitary
single qubit gate followed by an amplitude damping noise channel.
:params np.ndarraylist gate: The 2x2 unitary gate matrix.
:params float damp_prob: The onestep damping probability.
:return: A list [k1, k2] of the Kraus operators that parametrize the map.
:rtype: list
"""
return append_kraus_to_gate(damping_channel(damp_prob), gate)
%%time
# single step damping probability
damping_per_I = 0.02
# number of program executions
trials = 200
results_damping = []
lengths = np.arange(0, 201, 10, dtype=int)
for jj, num_I in enumerate(lengths):
print("\r{}/{}, ".format(jj, len(lengths)), end="")
p = Program(X(0))
# want increasing number of Igates
p.inst([I(0) for _ in range(num_I)])
p.inst(MEASURE(0, 0))
# overload identity I on qc 0
p.define_noisy_gate("I", [0], append_damping_to_gate(np.eye(2), damping_per_I))
p.wrap_in_numshots_loop(trials)
qc.qam.random_seed = int(num_I)
res = qc.run(p)
results_damping.append([np.mean(res), np.std(res) / np.sqrt(trials)])
results_damping = np.array(results_damping)
dense_lengths = np.arange(0, lengths.max()+1, .2)
survival_probs = (1damping_per_I)**dense_lengths
logpmf = binom.logpmf(np.arange(trials+1)[np.newaxis, :], trials, survival_probs[:, np.newaxis])/np.log(10)
DARK_TEAL = '#48737F'
FUSCHIA = "#D6619E"
BEIGE = '#EAE8C6'
cm = colors.LinearSegmentedColormap.from_list('anglemap', ["white", FUSCHIA, BEIGE], N=256, gamma=1.5)
plt.figure(figsize=(14, 6))
plt.pcolor(dense_lengths, np.arange(trials+1)/trials, logpmf.T, cmap=cm, vmin=4, vmax=logpmf.max())
plt.plot(dense_lengths, survival_probs, c=BEIGE, label="Expected mean")
plt.errorbar(lengths, results_damping[:,0], yerr=2*results_damping[:,1], c=DARK_TEAL,
label=r"noisy qvm, errorbars $ = \pm 2\hat{\sigma}$", marker="o")
cb = plt.colorbar()
cb.set_label(r"$\log_{10} \mathrm{Pr}(n_1; n_{\rm trials}, p_{\rm survival}(t))$", size=20)
plt.title("Amplitude damping model of a single qubit", size=20)
plt.xlabel(r"Time $t$ [arb. units]", size=14)
plt.ylabel(r"$n_1/n_{\rm trials}$", size=14)
plt.legend(loc="best", fontsize=18)
plt.xlim(*lengths[[0, 1]])
plt.ylim(0, 1)
Example 2: Dephased CZgateÂ¶
Dephasing is usually characterized through a qubitâ€™s \(T_2\) time. For a single qubit the dephasing Kraus operators are
where \(p = (1  \exp(T_{\rm gate}/T_2))/2\) is the probability that the qubit is dephased over the time interval of interest, \(I_2\) is the \(2\times 2\)identity matrix and \(\sigma_Z\) is the PauliZ operator.
For two qubits, we must construct a Kraus map that has four different outcomes:
No dephasing
Qubit 1 dephases
Qubit 2 dephases
Both dephase
The Kraus operators for this are given by
where we assumed a dephasing probability \(p\) for the first qubit and \(q\) for the second.
Dephasing is a diagonal error channel and the CZ gate is also diagonal, therefore we can get the combined map of dephasing and the CZ gate simply by composing \(U_{\rm CZ}\) the unitary representation of CZ with each Kraus operator
Note that this is not always accurate, because a CZ gate is often achieved through nondiagonal interaction Hamiltonians! However, for sufficiently small dephasing probabilities it should always provide a good starting point.
def dephasing_kraus_map(p=.1):
"""
Generate the Kraus operators corresponding to a dephasing channel.
:params float p: The onestep dephasing probability.
:return: A list [k1, k2] of the Kraus operators that parametrize the map.
:rtype: list
"""
return [np.sqrt(1p)*np.eye(2), np.sqrt(p)*np.diag([1, 1])]
def tensor_kraus_maps(k1, k2):
"""
Generate the Kraus map corresponding to the composition
of two maps on different qubits.
:param list k1: The Kraus operators for the first qubit.
:param list k2: The Kraus operators for the second qubit.
:return: A list of tensored Kraus operators.
"""
return [np.kron(k1j, k2l) for k1j in k1 for k2l in k2]
%%time
# single step damping probabilities
ps = np.linspace(.001, .5, 200)
# number of program executions
trials = 500
results = []
for jj, p in enumerate(ps):
corrupted_CZ = append_kraus_to_gate(
tensor_kraus_maps(
dephasing_kraus_map(p),
dephasing_kraus_map(p)
),
np.diag([1, 1, 1, 1]))
print("\r{}/{}, ".format(jj, len(ps)), end="")
# make Bellstate
p = Program(H(0), H(1), CZ(0,1), H(1))
p.inst(MEASURE(0, 0))
p.inst(MEASURE(1, 1))
# overload CZ on qc 0
p.define_noisy_gate("CZ", [0, 1], corrupted_CZ)
p.wrap_in_numshots_loop(trials)
qc.qam.random_seed = jj
res = qc.run(p)
results.append(res)
results = np.array(results)
Z1s = (2*results[:,:,0]1.)
Z2s = (2*results[:,:,1]1.)
Z1Z2s = Z1s * Z2s
Z1m = np.mean(Z1s, axis=1)
Z2m = np.mean(Z2s, axis=1)
Z1Z2m = np.mean(Z1Z2s, axis=1)
plt.figure(figsize=(14, 6))
plt.axhline(y=1.0, color=FUSCHIA, alpha=.5, label="Bell state")
plt.plot(ps, Z1Z2m, "x", c=FUSCHIA, label=r"$\overline{Z_1 Z_2}$")
plt.plot(ps, 12*ps, "", c=FUSCHIA, label=r"$\langle Z_1 Z_2\rangle_{\rm theory}$")
plt.plot(ps, Z1m, "o", c=DARK_TEAL, label=r"$\overline{Z}_1$")
plt.plot(ps, 0*ps, "", c=DARK_TEAL, label=r"$\langle Z_1\rangle_{\rm theory}$")
plt.plot(ps, Z2m, "d", c="k", label=r"$\overline{Z}_2$")
plt.plot(ps, 0*ps, "", c="k", label=r"$\langle Z_2\rangle_{\rm theory}$")
plt.xlabel(r"Dephasing probability $p$", size=18)
plt.ylabel(r"$Z$moment", size=18)
plt.title(r"$Z$moments for a Bellstate prepared with dephased CZ", size=18)
plt.xlim(0, .5)
plt.legend(fontsize=18)
Adding Decoherence NoiseÂ¶
In this example, we investigate how a program might behave on a
nearterm device that is subject to T1 and T2type noise using the convenience function
pyquil.noise.add_decoherence_noise()
. The same module also contains some other useful
functions to define your own types of noise models, e.g.,
pyquil.noise.tensor_kraus_maps()
for generating multiqubit noise processes,
pyquil.noise.combine_kraus_maps()
for describing the succession of two noise processes and
pyquil.noise.append_kraus_to_gate()
which allows appending a noise process to a unitary
gate.
from pyquil.quil import Program
from pyquil.paulis import PauliSum, PauliTerm, exponentiate, exponential_map, trotterize
from pyquil.gates import MEASURE, H, Z, RX, RZ, CZ
import numpy as np
The TaskÂ¶
We want to prepare \(e^{i \theta XY}\) and measure it in the \(Z\) basis.
from numpy import pi
theta = pi/3
xy = PauliTerm('X', 0) * PauliTerm('Y', 1)
The Idiomatic PyQuil ProgramÂ¶
prog = exponential_map(xy)(theta)
print(prog)
H 0
RX(pi/2) 1
CNOT 0 1
RZ(2*pi/3) 1
CNOT 0 1
H 0
RX(pi/2) 1
The Compiled ProgramÂ¶
To run on a real device, we must compile each program to the native gate set for the device. The highlevel noise model is similarly constrained to use a small, native gate set. In particular, we can use
\(I\)
\(RZ(\theta)\)
\(RX(\pm \pi/2)\)
\(CZ\)
For simplicity, the compiled program is given below but generally you will want to use a compiler to do this step for you.
def get_compiled_prog(theta):
return Program([
RZ(pi/2, 0),
RX(pi/2, 0),
RZ(pi/2, 1),
RX( pi/2, 1),
CZ(1, 0),
RZ(pi/2, 1),
RX(pi/2, 1),
RZ(theta, 1),
RX( pi/2, 1),
CZ(1, 0),
RX( pi/2, 0),
RZ( pi/2, 0),
RZ(pi/2, 1),
RX( pi/2, 1),
RZ(pi/2, 1),
])
Scan Over Noise ParametersÂ¶
We perform a scan over three levels of noise, each at 20 theta points.
Specifically, we investigate T1 values of 1, 3, and 10 us. By default, T2 = T1 / 2, 1 qubit gates take 50 ns, and 2 qubit gates take 150 ns.
In alignment with the device, \(I\) and parametric \(RZ\) are noiseless while \(RX\) and \(CZ\) gates experience 1q and 2q gate noise, respectively.
from pyquil.api import QVMConnection
cxn = QVMConnection()
t1s = np.logspace(6, 5, num=3)
thetas = np.linspace(pi, pi, num=20)
t1s * 1e6 # us
array([ 1. , 3.16227766, 10. ])
from pyquil.noise import add_decoherence_noise
records = []
for theta in thetas:
for t1 in t1s:
prog = get_compiled_prog(theta)
noisy = add_decoherence_noise(prog, T1=t1).inst([
MEASURE(0, 0),
MEASURE(1, 1),
])
bitstrings = np.array(cxn.run(noisy, [0,1], 1000))
# Expectation of Z0 and Z1
z0, z1 = 1  2*np.mean(bitstrings, axis=0)
# Expectation of ZZ by computing the parity of each pair
zz = 1  (np.sum(bitstrings, axis=1) % 2).mean() * 2
record = {
'z0': z0,
'z1': z1,
'zz': zz,
'theta': theta,
't1': t1,
}
records += [record]
Plot the ResultsÂ¶
Note that to run the code below you will need to install the pandas and seaborn packages.
%matplotlib inline
from matplotlib import pyplot as plt
import seaborn as sns
sns.set(style='ticks', palette='colorblind')
import pandas as pd
df_all = pd.DataFrame(records)
fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(12,4))
for t1 in t1s:
df = df_all.query('t1 == @t1')
ax1.plot(df['theta'], df['z0'], 'o')
ax2.plot(df['theta'], df['z1'], 'o')
ax3.plot(df['theta'], df['zz'], 'o', label='T1 = {:.0f} us'.format(t1*1e6))
ax3.legend(loc='best')
ax1.set_ylabel('Z0')
ax2.set_ylabel('Z1')
ax3.set_ylabel('ZZ')
ax2.set_xlabel(r'$\theta$')
fig.tight_layout()
Modeling Readout NoiseÂ¶
QubitReadout can be corrupted in a variety of ways. The two most relevant error mechanisms on the Rigetti QPU right now are:
Transmission line noise that makes a 0state look like a 1state or vice versa. We call this classical readout bitflip error. This type of readout noise can be reduced by tailoring optimal readout pulses and using superconducting, quantum limited amplifiers to amplify the readout signal before it is corrupted by classical noise at the higher temperature stages of our cryostats.
T1 qubit decay during readout (our readout operations can take more than a Âµsecond unless they have been specially optimized), which leads to readout signals that initially behave like 1states but then collapse to something resembling a 0state. We will call this T1readout error. This type of readout error can be reduced by achieving shorter readout pulses relative to the T1 time, i.e., one can try to reduce the readout pulse length, or increase the T1 time or both.
Qubit MeasurementsÂ¶
This section provides the necessary theoretical foundation for accurately modeling noisy quantum measurements on superconducting quantum processors. It relies on some of the abstractions (density matrices, Kraus maps) introduced in our notebook on gate noise models.
The most general type of measurement performed on a single qubit at a single time can be characterized by some set \(\mathcal{O}\) of measurement outcomes, e.g., in the simplest case \(\mathcal{O} = \{0, 1\}\), and some unnormalized quantum channels (see notebook on gate noise models) that encapsulate: 1. the probability of that outcome, and 2. how the qubit state is affected conditional on the measurement outcome.
Here the outcome is understood as classical information that has been extracted from the quantum system.
Projective, Ideal MeasurementÂ¶
The simplest case that is usually taught in introductory quantum mechanics and quantum information courses are Bornâ€™s rule and the projection postulate which state that there exist a complete set of orthogonal projection operators
i.e., one for each measurement outcome. Any projection operator must satisfy \(\Pi_x^\dagger = \Pi_x = \Pi_x^2\) and for an orthogonal set of projectors any two members satisfy
and for a complete set we additionally demand that \(\sum_{x\in\mathcal{O}} \Pi_x = 1\). Following our introduction to gate noise, we write quantum states as density matrices, as this is more general and in closer correspondence with classical probability theory.
With these, the probability of outcome \(x\) is given by \(p(x) = \tr{\Pi_x \rho \Pi_x} = \tr{\Pi_x^2 \rho} = \tr{\Pi_x \rho}\) and the post measurement state is
which is the projection postulate applied to mixed states.
If we were a sloppy quantum programmer and accidentally erased the measurement outcome, then our best guess for the post measurement state would be given by something that looks an awful lot like a Kraus map:
The completeness of the projector set ensures that the trace of the post measurement is still 1 and the Kraus map form of this expression ensures that \(\rho_{\text{post measurement}}\) is a positive (semi)definite operator.
Classical Readout BitFlip ErrorÂ¶
Consider now the ideal measurement as above, but where the outcome \(x\) is transmitted across a noisy classical channel that produces a final outcome \(x'\in \mathcal{O}' = \{0', 1'\}\) according to some conditional probabilities \(p(x'x)\) that can be recorded in the assignment probability matrix
Note that this matrix has only two independent parameters as each column must be a valid probability distribution, i.e. all elements are nonnegative and each column sums to 1.
This matrix allows us to obtain the probabilities \(\mathbf{p}' := (p(x'=0), p(x'=1))^T\) from the original outcome probabilities \(\mathbf{p} := (p(x=0), p(x=1))^T\) via \(\mathbf{p}' = P_{x'x}\mathbf{p}\). The difference relative to the ideal case above is that now an outcome \(x' = 0\) does not necessarily imply that the post measurement state is truly \(\Pi_{0} \rho \Pi_{0} / p(x=0)\). Instead, the post measurement state given a noisy outcome \(x'\) must be
where
where we have exploited the cyclical property of the trace \(\tr{ABC}=\tr{BCA}\) and the projection property \(\Pi_x^2 = \Pi_x\). This has allowed us to derive the noisy outcome probabilities from a set of positive operators
that must sum to 1:
The above result is a type of generalized Bayesâ€™ theorem that is extremely useful for this type of (slightly) generalized measurement and the family of operators \(\{E_{x'} x' \in \mathcal{O}'\}\) whose expectations given the probabilities is called a positive operator valued measure (POVM). These operators are not generally orthogonal nor valid projection operators, but they naturally arise in this scenario. This is not yet the most general type of measurement, but it will get us pretty far.
How to Model \(T_1\) ErrorÂ¶
T1 type errors fall outside our framework so far as they involve a scenario in which the quantum state itself is corrupted during the measurement process in a way that potentially erases the premeasurement information as opposed to a loss of purely classical information. The most appropriate framework for describing this is given by that of measurement instruments, but for the practical purpose of arriving at a relatively simple description, we propose describing this by a T1 damping Kraus map followed by the noisy readout process as described above.
Further ReadingÂ¶
Chapter 3 of John Preskillâ€™s lecture notes http://www.theory.caltech.edu/people/preskill/ph229/notes/chap3.pdf
Working with Readout NoiseÂ¶
Come up with a good guess for your readout noise parameters \(p(00)\) and \(p(11)\); the offdiagonals then follow from the normalization of \(P_{x'x}\). If your assignment fidelity \(F\) is given, and you assume that the classical bit flip noise is roughly symmetric, then a good approximation is to set \(p(00)=p(11)=F\).
For your Quil program
p
and a qubit indexq
call:p.define_noisy_readout(q, p00, p11)
where you should replace
p00
andp11
with the assumed probabilities.
Scroll down for some examples!
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
from pyquil.quil import Program, MEASURE, Pragma
from pyquil.api.qvm import QVMConnection
from pyquil.gates import I, X, RX, H, CNOT
from pyquil.noise import (estimate_bitstring_probs, correct_bitstring_probs,
bitstring_probs_to_z_moments, estimate_assignment_probs)
DARK_TEAL = '#48737F'
FUSCHIA = '#D6619E'
BEIGE = '#EAE8C6'
cxn = QVMConnection()
Example 1: Rabi Sequence with Noisy ReadoutÂ¶
%%time
# number of angles
num_theta = 101
# number of program executions
trials = 200
thetas = np.linspace(0, 2*np.pi, num_theta)
p00s = [1., 0.95, 0.9, 0.8]
results_rabi = np.zeros((num_theta, len(p00s)))
for jj, theta in enumerate(thetas):
for kk, p00 in enumerate(p00s):
cxn.random_seed = hash((jj, kk))
p = Program(RX(theta, 0))
# assume symmetric noise p11 = p00
p.define_noisy_readout(0, p00=p00, p11=p00)
p.measure(0, 0)
res = cxn.run(p, [0], trials=trials)
results_rabi[jj, kk] = np.sum(res)
CPU times: user 1.2 s, sys: 73.6 ms, total: 1.27 s
Wall time: 3.97 s
plt.figure(figsize=(14, 6))
for jj, (p00, c) in enumerate(zip(p00s, [DARK_TEAL, FUSCHIA, "k", "gray"])):
plt.plot(thetas, results_rabi[:, jj]/trials, c=c, label=r"$p(00)=p(11)={:g}$".format(p00))
plt.legend(loc="best")
plt.xlim(*thetas[[0,1]])
plt.ylim(.1, 1.1)
plt.grid(alpha=.5)
plt.xlabel(r"RX angle $\theta$ [radian]", size=16)
plt.ylabel(r"Excited state fraction $n_1/n_{\rm trials}$", size=16)
plt.title("Effect of classical readout noise on Rabi contrast.", size=18)
<matplotlib.text.Text at 0x104314250>
Example 2: Estimate the Assignment ProbabilitiesÂ¶
Here we will estimate \(P_{x'x}\) ourselves! You can run some simple experiments to estimate the assignment probability matrix directly from a QPU.
On a perfect quantum computer
estimate_assignment_probs(0, 1000, cxn, Program())
array([[ 1., 0.],
[ 0., 1.]])
On an imperfect quantum computer
cxn.seed = None
header0 = Program().define_noisy_readout(0, .85, .95)
header1 = Program().define_noisy_readout(1, .8, .9)
header2 = Program().define_noisy_readout(2, .9, .85)
ap0 = estimate_assignment_probs(0, 100000, cxn, header0)
ap1 = estimate_assignment_probs(1, 100000, cxn, header1)
ap2 = estimate_assignment_probs(2, 100000, cxn, header2)
print(ap0, ap1, ap2, sep="\n")
[[ 0.84967 0.04941]
[ 0.15033 0.95059]]
[[ 0.80058 0.09993]
[ 0.19942 0.90007]]
[[ 0.90048 0.14988]
[ 0.09952 0.85012]]
Example 3: Correct for Noisy ReadoutÂ¶
3a) Correcting the Rabi Signal from AboveÂ¶
ap_last = np.array([[p00s[1], 1  p00s[1]],
[1  p00s[1], p00s[1]]])
corrected_last_result = [correct_bitstring_probs([1p, p], [ap_last])[1] for p in results_rabi[:, 1] / trials]
plt.figure(figsize=(14, 6))
for jj, (p00, c) in enumerate(zip(p00s, [DARK_TEAL, FUSCHIA, "k", "gray"])):
if jj not in [0, 3]:
continue
plt.plot(thetas, results_rabi[:, jj]/trials, c=c, label=r"$p(00)=p(11)={:g}$".format(p00), alpha=.3)
plt.plot(thetas, corrected_last_result, c="red", label=r"Corrected $p(00)=p(11)={:g}$".format(p00s[1]))
plt.legend(loc="best")
plt.xlim(*thetas[[0,1]])
plt.ylim(.1, 1.1)
plt.grid(alpha=.5)
plt.xlabel(r"RX angle $\theta$ [radian]", size=16)
plt.ylabel(r"Excited state fraction $n_1/n_{\rm trials}$", size=16)
plt.title("Corrected contrast", size=18)
<matplotlib.text.Text at 0x1055e7310>
We find that the corrected signal is fairly noisy (and sometimes exceeds the allowed interval \([0,1]\)) due to the overall very small number of samples \(n=200\).
3b) Corrupting and Correcting GHZ State CorrelationsÂ¶
In this example we will create a GHZ state \(\frac{1}{\sqrt{2}}\left[\left000\right\rangle + \left111\right\rangle \right]\) and measure its outcome probabilities with and without the above noise model. We will then see how the PauliZ moments that indicate the qubit correlations are corrupted (and corrected) using our API.
ghz_prog = Program(H(0), CNOT(0, 1), CNOT(1, 2),
MEASURE(0, 0), MEASURE(1, 1), MEASURE(2, 2))
print(ghz_prog)
results = cxn.run(ghz_prog, [0, 1, 2], trials=10000)
H 0
CNOT 0 1
CNOT 1 2
MEASURE 0 [0]
MEASURE 1 [1]
MEASURE 2 [2]
header = header0 + header1 + header2
noisy_ghz = header + ghz_prog
print(noisy_ghz)
noisy_results = cxn.run(noisy_ghz, [0, 1, 2], trials=10000)
PRAGMA READOUTPOVM 0 "(0.85 0.050000000000000044 0.15000000000000002 0.95)"
PRAGMA READOUTPOVM 1 "(0.8 0.09999999999999998 0.19999999999999996 0.9)"
PRAGMA READOUTPOVM 2 "(0.9 0.15000000000000002 0.09999999999999998 0.85)"
H 0
CNOT 0 1
CNOT 1 2
MEASURE 0 [0]
MEASURE 1 [1]
MEASURE 2 [2]
probs = estimate_bitstring_probs(results)
probs[0, 0, 0], probs[1, 1, 1]
(0.50419999999999998, 0.49580000000000002)
As expected the outcomes 000
and 111
each have roughly
probability \(1/2\).
noisy_probs = estimate_bitstring_probs(noisy_results)
noisy_probs[0, 0, 0], noisy_probs[1, 1, 1]
(0.30869999999999997, 0.3644)
The noisecorrupted outcome probabilities deviate significantly from their ideal values!
corrected_probs = correct_bitstring_probs(noisy_probs, [ap0, ap1, ap2])
corrected_probs[0, 0, 0], corrected_probs[1, 1, 1]
(0.50397601453064977, 0.49866843912900716)
The corrected outcome probabilities are much closer to the ideal value.
We expect these to all be very small
zmoments = bitstring_probs_to_z_moments(probs)
zmoments[1, 0, 0], zmoments[0, 1, 0], zmoments[0, 0, 1]
(0.0083999999999999631, 0.0083999999999999631, 0.0083999999999999631)
We expect these to all be close to 1.
zmoments[1, 1, 0], zmoments[0, 1, 1], zmoments[1, 0, 1]
(1.0, 1.0, 1.0)
zmoments_corr = bitstring_probs_to_z_moments(corrected_probs)
zmoments_corr[1, 0, 0], zmoments_corr[0, 1, 0], zmoments_corr[0, 0, 1]
(0.0071476770049732075, 0.0078641261685578612, 0.0088462563282706852)
zmoments_corr[1, 1, 0], zmoments_corr[0, 1, 1], zmoments_corr[1, 0, 1]
(0.99477496902638118, 1.0008376440216553, 1.0149652015905912)
Overall the correction can restore the contrast in our multiqubit observables, though we also see that the correction can lead to slightly nonphysical expectations. This effect is reduced the more samples we take.
Alternative: A global Pauli error modelÂ¶
The Rigetti QVM has support for emulating certain types of noise models. One such model is parametric Pauli noise, which is defined by a set of 6 probabilities:
The probabilities \(P_X\), \(P_Y\), and \(P_Z\) which define respectively the probability of a Pauli \(X\), \(Y\), or \(Z\) gate getting applied to each qubit after every gate application. These probabilities are called the gate noise probabilities.
The probabilities \(P_X'\), \(P_Y'\), and \(P_Z'\) which define respectively the probability of a Pauli \(X\), \(Y\), or \(Z\) gate getting applied to the qubit being measured before it is measured. These probabilities are called the measurement noise probabilities.
We can instantiate a noisy QVM by creating a new connection with these probabilities specified.
# 20% chance of a X gate being applied after gate applications and before measurements.
gate_noise_probs = [0.2, 0.0, 0.0]
meas_noise_probs = [0.2, 0.0, 0.0]
noisy_qvm = qvm(gate_noise=gate_noise_probs, measurement_noise=meas_noise_probs)
We can test this by applying an \(X\)gate and measuring. Nominally,
we should always measure 1
.
p = Program().inst(X(0)).measure(0, 0)
print("Without Noise: {}".format(qvm.run(p, [0], 10)))
print("With Noise : {}".format(noisy_qvm.run(p, [0], 10)))
Without Noise: [[1], [1], [1], [1], [1], [1], [1], [1], [1], [1]]
With Noise : [[0], [0], [0], [0], [0], [1], [1], [1], [1], [0]]
Advanced UsageÂ¶
Note
If youâ€™re running locally, remember set up the QVM and quilc in server mode before trying to use them: Setting Up Server Mode for PyQuil.
PyQuil Configuration FilesÂ¶
Network endpoints for the Rigetti Forest infrastructure and information pertaining to QPU access are
stored in configuration files located by default at ~/.qcs_config
and ~/.forest_config
.
The file paths can be customized by setting the environment variables QCS_CONFIG
or
FOREST_CONFIG
, respectively.
These files are present on your QMI, and the default configuration will work for most users.
Authentication credentials for Forest server are read from ~/.qcs/user_auth_token
(note: no file extension). You can download this token from https://qcs.rigetti.com/auth/token.
The default QCS config file on any QMI looks similar to the following:
# .qcs_config
[Rigetti Forest]
url = https://forestserver.qcs.rigetti.com
user_id = 4fd1239111eb52ec35c2262765ae4c4f
[QPU]
exec_on_engage = bash exec_on_engage.sh
where
url
is the endpoint that pyQuil uses for device information
exec_on_engage
specifies the shell command that the QMI will launch when the QMI becomes QPUengaged. It would have no effect if you are running locally, but is important if you are running on the QMI. By default, it runs theexec_on_engage.sh
shell script. Itâ€™s best to leave the configuration as is, and edit that script. More documentation aboutexec_on_engage.sh
can be found in the QCS docs here.
Note
PyQuil itself reads these values out using the helper class pyquil._config.PyquilConfig
. PyQuil users should not
ever need to touch this class directly.
If youâ€™d like to change your pyQuil configuration, you can do so in your runtime environment or by editing these configuration files. All configuration options derive their values from one of three places, in decreasing order of precedence:
The runtime environment, set using export in Unixbased platforms, and setx in Windows.
QCS configuration files noted above:
.qcs_config
and.forest_config
, or their userconfigured file paths. The two are not interchangeable; each config value will be looked up in exactly one of these files.The default values specified in
pyquil.api._config
. These should not be changed by any Pyquil user, as doing so may lead to difficulttodebug behavior.
These are all the configuration options available to you, and where they can be set:
Setting 
Environment 
Configuration File 

Forest Server URL Source of device information 

Key: 
Dispatch URL Provides QPU engagement authorization 

Key: 
User Authentication Token Path File path to the authentication token obtained from qcs. 

Key: 
QCS URL QCS website 

Key: 
QPU URL Send binaries to the QPU 

Key: 
QVM URL Simulator 

Key: 
QPU Compiler URL Send native Quil and receive a binary 

Key: 
Local Compiler URL Send Quil and receive native Quil 

Key: 
The configuration options omitted from this table but present in pyquil.api._config
are
deprecated and should not be used.
Using Qubit PlaceholdersÂ¶
Note
The functionality provided inline by QubitPlaceholders
is similar to writing a function which returns a
Program
, with qubit indices taken as arguments to the function.
In pyQuil, we typically use integers to identify qubits
from pyquil import Program
from pyquil.gates import CNOT, H
print(Program(H(0), CNOT(0, 1)))
H 0
CNOT 0 1
However, when running on real, nearterm QPUs we care about what
particular physical qubits our program will run on. In fact, we may want
to run the same program on an assortment of different qubits. This is
where using QubitPlaceholder
s comes in.
from pyquil.quilatom import QubitPlaceholder
q0 = QubitPlaceholder()
q1 = QubitPlaceholder()
p = Program(H(q0), CNOT(q0, q1))
print(p)
H {q4402789176}
CNOT {q4402789176} {q4402789120}
If you try to use this program directly, it will not work
print(p.out())
RuntimeError: Qubit q4402789176 has not been assigned an index
Instead, you must explicitly map the placeholders to physical qubits. By
default, the function address_qubits
will address qubits from 0 to
N.
from pyquil.quil import address_qubits
print(address_qubits(p))
H 0
CNOT 0 1
The real power comes into play when you provide an explicit mapping:
print(address_qubits(prog, qubit_mapping={
q0: 14,
q1: 19,
}))
H 14
CNOT 14 19
RegisterÂ¶
Usually, your algorithm will use an assortment of qubits. You can use
the convenience function QubitPlaceholder.register()
to request a
list of qubits to build your program.
qbyte = QubitPlaceholder.register(8)
p_evens = Program(H(q) for q in qbyte)
print(address_qubits(p_evens, {q: i*2 for i, q in enumerate(qbyte)}))
H 0
H 2
H 4
H 6
H 8
H 10
H 12
H 14
Classical Control FlowÂ¶
Note
Classical control flow is not yet supported on the QPU.
Here are a couple quick examples that show how much richer a Quil program can be with classical control flow. In this first example, we create a while loop by following these steps:
Declare a register called
flag_register
to use as a boolean test for looping.Initialize this register to
1
, so our while loop will execute. This is often called the loop preamble or loop initialization.Write the body of the loop in its own
Program
. This will be a program that applies an \(X\) gate followed by an \(H\) gate on our qubit.Use the
while_do()
method to add control flow.
from pyquil import Program
from pyquil.gates import *
# Initialize the Program and declare a 1 bit memory space for our boolean flag
outer_loop = Program()
flag_register = outer_loop.declare('flag_register', 'BIT')
# Set the initial flag value to 1
outer_loop += MOVE(flag_register, 1)
# Define the body of the loop with a new Program
inner_loop = Program()
inner_loop += Program(X(0), H(0))
inner_loop += MEASURE(0, flag_register)
# Run inner_loop in a loop until flag_register is 0
outer_loop.while_do(flag_register, inner_loop)
print(outer_loop)
DECLARE flag_register BIT[1]
MOVE flag_register 1
LABEL @START1
JUMPUNLESS @END2 flag_register
X 0
H 0
MEASURE 0 flag_register
JUMP @START1
LABEL @END2
Notice that the outer_loop
program applied a Quil instruction directly to a
classical register. There are several classical commands that can be used in this fashion:
NOT
which flips a classical bitAND
which operates on two classical bitsIOR
which operates on two classical bitsMOVE
which moves the value of a classical bit at one classical address into anotherEXCHANGE
which swaps the value of two classical bits
In this next example, we show how to do conditional branching in the
form of the traditional if
construct as in many programming
languages. Much like the last example, we construct programs for each
branch of the if
, and put it all together by using the if_then()
method.
# Declare our memory spaces
branching_prog = Program()
test_register = branching_prog.declare('test_register', 'BIT')
ro = branching_prog.declare('ro', 'BIT')
# Construct each branch of our ifstatement. We can have empty branches
# simply by having empty programs.
then_branch = Program(X(0))
else_branch = Program()
# Construct our program so that the result in test_register is equally likely to be a 0 or 1
branching_prog += H(1)
branching_prog += MEASURE(1, test_register)
# Add the conditional branching
branching_prog.if_then(test_register, then_branch, else_branch)
# Measure qubit 0 into our readout register
branching_prog += MEASURE(0, ro)
print(branching_prog)
DECLARE test_register BIT[1]
DECLARE ro BIT[1]
H 1
MEASURE 1 test_register
JUMPWHEN @THEN1 test_register
JUMP @END2
LABEL @THEN1
X 0
LABEL @END2
MEASURE 0 ro
We can run this program a few times to see what we get in the readout register ro
.
from pyquil import get_qc
qc = get_qc("2qqvm")
branching_prog.wrap_in_numshots_loop(10)
qc.run(branching_prog)
[[1], [1], [1], [0], [1], [0], [0], [1], [1], [0]]
Pauli Operator AlgebraÂ¶
Many algorithms require manipulating sums of Pauli combinations, such as
\(\sigma = \frac{1}{2}I  \frac{3}{4}X_0Y_1Z_3 + (52i)Z_1X_2,\) where
\(G_n\) indicates the gate \(G\) acting on qubit \(n\). We
can represent such sums by constructing PauliTerm
and PauliSum
.
The above sum can be constructed as follows:
from pyquil.paulis import ID, sX, sY, sZ
# Pauli term takes an operator "X", "Y", "Z", or "I"; a qubit to act on, and
# an optional coefficient.
a = 0.5 * ID()
b = 0.75 * sX(0) * sY(1) * sZ(3)
c = (52j) * sZ(1) * sX(2)
# Construct a sum of Pauli terms.
sigma = a + b + c
print(f"sigma = {sigma}")
sigma = (0.5+0j)*I + (0.75+0j)*X0*Y1*Z3 + (52j)*Z1*X2
Right now, the primary thing one can do with Pauli terms and sums is to construct the exponential of the Pauli term, i.e., \(\exp[i\beta\sigma]\). This is accomplished by constructing a parameterized Quil program that is evaluated when passed values for the coefficients of the angle \(\beta\).
Related to exponentiating Pauli sums, we provide utility functions for finding the commuting subgroups of a Pauli sum and approximating the exponential with the SuzukiTrotter approximation through fourth order.
When arithmetic is done with Pauli sums, simplification is automatically done.
The following shows an instructive example of all three.
from pyquil.paulis import exponential_map
sigma_cubed = sigma * sigma * sigma
print(f"Simplified: {sigma_cubed}\n")
# Produce Quil code to compute exp[iX]
H = 1.0 * sX(0)
print(f"Quil to compute exp[iX] on qubit 0:\n"
f"{exponential_map(H)(1.0)}")
Simplified: (32.4687530j)*I + (16.734375+15j)*X0*Y1*Z3 + (71.5625144.625j)*Z1*X2
Quil to compute exp[iX] on qubit 0:
H 0
RZ(2.0) 0
H 0
exponential_map
returns a function allowing you to fill in a multiplicative
constant later. This commonly occurs in variational algorithms. The function
exponential_map
is used to compute \(\exp[i \alpha H]\) without explicitly filling in a
value for \(\alpha\).
expH = exponential_map(H)
print(f"0:\n{expH(0.0)}\n")
print(f"1:\n{expH(1.0)}\n")
print(f"2:\n{expH(2.0)}")
0:
H 0
RZ(0) 0
H 0
1:
H 0
RZ(2.0) 0
H 0
2:
H 0
RZ(4.0) 0
H 0
To take it one step further, you can use Parametric Compilation with exponential_map
. For instance:
ham = sZ(0) * sZ(1)
prog = Program()
theta = prog.declare('theta', 'REAL')
prog += exponential_map(ham)(theta)
TroubleshootingÂ¶
If youâ€™re having any trouble running your Pyquil programs locally or on the QPU, please check the following things before sending a support request. It will save you time and make it easier for us to help!
Ensure that your pyQuil version is up to date. If youâ€™re using
pip
, you can do this withpip freeze
. Within your script, you can use__version__
:import pyquil print(pyquil.__version__)
You can update pyQuil with
pip
usingpip install pyquil upgrade
. You can find the latest version available at our releases page or on PyPi.If the error appears to be authenticationrelated, or makes any mention of your
user_auth_token
, then please update your token following the directions at https://qcs.rigetti.com/auth/token.Run your script with debug logging enabled. If youâ€™re running a script, you can enable that using an environment variable:
LOG_LEVEL=DEBUG pyquil my_script.py
import logging from pyquil.api._logger import logger logger.setLevel(logging.DEBUG)
If the problem still isnâ€™t clear, then we can help! Please send your debug log to us,
along with the contents of your ~/.qcs_config
and ~/.forest_config
files, at our
support page. Thanks for using pyQuil!
ExercisesÂ¶
Exercise 1: Quantum DiceÂ¶
Write a quantum program to simulate throwing an 8sided die. The Python function you should produce is:
def throw_octahedral_die():
# return the result of throwing an 8 sided die, an int between 1 and 8, by running a quantum program
Next, extend the program to work for any kind of fair die:
def throw_polyhedral_die(num_sides):
# return the result of throwing a num_sides sided die by running a quantum program
Exercise 2: Controlled GatesÂ¶
We can use the full generality of NumPy to construct new gate matrices.
Write a function
controlled
which takes a \(2\times 2\) matrix \(U\) representing a single qubit operator, and makes a \(4\times 4\) matrix which is a controlled variant of \(U\), with the first argument being the control qubit.Write a Quil program to define a controlled\(Y\) gate in this manner. Find the wavefunction when applying this gate to qubit 1 controlled by qubit 0.
Exercise 3: Groverâ€™s AlgorithmÂ¶
Write a quantum program for the singleshot Groverâ€™s algorithm. The Python function you should produce is:
# data is an array of 0's and 1's such that there are exactly three times as many
# 0's as 1's
def single_shot_grovers(data):
# return an index that contains the value 1
As an example: single_shot_grovers([0,0,1,0])
should return 2.
HINT  Remember that the Groverâ€™s diffusion operator is:
Exercise 4: Prisonerâ€™s DilemmaÂ¶
A classic strategy game is the prisonerâ€™s dilemma where two prisoners get the minimal penalty if they collaborate and stay silent, get zero penalty if one of them defects and the other collaborates (incurring maximum penalty) and get intermediate penalty if they both defect. This game has an equilibrium where both defect and incur intermediate penalty.
However, things change dramatically when we allow for quantum strategies leading to the Quantum Prisonerâ€™s Dilemma.
Can you design a program that simulates this game?
Exercise 5: Quantum Fourier TransformÂ¶
The quantum Fourier transform (QFT) is a quantum implementation of the discrete Fourier transform. The Fourier transform can be used to transform a function from the time domain into the frequency domain.
 Compute the discrete Fourier transform of
[0, 1, 0, 0, 0, 0, 0, 0]
, using pyQuil: Write a state preparation quantum program.
Write a function to make a 3qubit QFT program, taking qubit indices as arguments.
Combine your solutions to part a and b into one program and use the
WavefunctionSimulator
to get the solution.
Note
For a more challenging initial state, try 01100100
.
SolutionÂ¶
Part a: Prepare the initial stateÂ¶
We are going to apply the QFT on the amplitudes of the states.
We want to prepare a state that corresponds to the sequence for which we want to compute the discrete Fourier transform. As the exercise hinted in part b, we need 3 qubits to transform an 8 bit sequence. It is simplest to understand if we think of the qubits as three digits in a binary string (aka bitstring). There are 8 possible values the bitstring can have, and in our quantum state, each of these possibilities has an amplitude. Our 8 indices in the QFT sequence label each of these states. For clarity:
\(000\rangle\) => 10000000
\(001\rangle\) => 01000000
â€¦
\(111\rangle\) > 00000001
The sequence we want to compute is 01000000
, so our initial state is simply \(001\rangle\). For a bitstring with more
than one 1
, we would want an equal superposition over all the selected states. (E.g. 01100000
would be an
equal superposition of \(001\rangle\) and \(010\rangle\)).
To set up the \(001\rangle\) state, we only have to apply one \(X\)gate to the zeroth qubit.
from pyquil import Program
from pyquil.gates import *
state_prep = Program(X(0))
We can verify that this works by computing its wavefunction with the
Wavefunction Simulator. However, we need to add some â€śdummyâ€ť qubits,
because otherwise wavefunction
would return a twoelement vector for only qubit 0.
from pyquil.api import WavefunctionSimulator
add_dummy_qubits = Program(I(1), I(2)) # The identity gate I has no affect
wf_sim = WavefunctionSimulator()
wavefunction = wf_sim.wavefunction(state_prep + add_dummy_qubits)
print(wavefunction)
(1+0j)001>
Weâ€™ll need wf_sim
for part c, too.
Part b: Three qubit QFT programÂ¶
In this part, we define a function, qft3
, to make a 3qubit QFT quantum program. The algorithm
is nicely described on this page.
It is a mix of Hadamard and CPHASE gates, with a SWAP gate for bit reversal correction.
from math import pi
def qft3(q0, q1, q2):
p = Program()
p += [SWAP(q0, q2),
H(q0),
CPHASE(pi / 2.0, q0, q1),
H(q1),
CPHASE(pi / 4.0, q0, q2),
CPHASE(pi / 2.0, q1, q2),
H(q2)]
return p
There is a very important detail to recognize here: The function
qft3
doesnâ€™t compute the QFT, but rather it makes a quantum
program to compute the QFT on qubits q0
, q1
, and q2
.
We can see what this program looks like in Quil notation with print(qft(0, 1, 2))
.
SWAP 0 2
H 0
CPHASE(pi/2) 0 1
H 1
CPHASE(pi/4) 0 2
CPHASE(pi/2) 1 2
H 2
Part c: Execute the QFTÂ¶
Combining parts a and b:
compute_qft_prog = state_prep + qft3(0, 1, 2)
wavefunction = wf_sim.wavefunction(compute_qft_prog)
print(wavefunction.amplitudes)
array([ 3.53553391e01+0.j , 2.50000000e010.25j ,
2.16489014e170.35355339j, 2.50000000e010.25j ,
3.53553391e01+0.j , 2.50000000e01+0.25j ,
2.16489014e17+0.35355339j, 2.50000000e01+0.25j ])
We can verify this works by computing the inverse FFT on the output with NumPy and seeing that we get back our input (with some floating point error).
from numpy.fft import ifft
ifft(wavefunction.amplitudes, norm="ortho")
array([0.+0.00000000e+00j, 1.+9.38127079e17j, 0.+0.00000000e+00j,
0.1.53080850e17j, 0.+0.00000000e+00j, 0.6.31965379e17j,
0.+0.00000000e+00j, 0.1.53080850e17j])
After ignoring the terms that are on the order of 1e17
, we get [0, 1, 0, 0, 0, 0, 0, 0]
, which was our input!
Example: The MeyerPenny GameÂ¶
To create intuition for quantum algorithms, it is useful (and fun) to play with the abstraction that the software provides.
The MeyerPenny Game 1 is a simple example weâ€™ll use from quantum game theory. The interested reader may want to read more about quantum game theory in the article Toward a general theory of quantum games 2. The MeyerPenny Game goes as follows:
The Starship Enterprise, during one of its deepspace missions, is facing an immediate calamity at the edge of a wormhole, when a powerful alien suddenly appears. The alien, named Q, offers to help Picard, the captain of the Enterprise, under the condition that Picard beats Q in a simple game of heads or tails.
The rulesÂ¶
Picard is to place a penny heads up into an opaque box. Then Picard and Q take turns to flip or not flip the penny without being able to see it; first Q then P then Q again. After this the penny is revealed;â€ŠQ wins if it shows heads (H), while tails (T) makes Picard the winner.
Picard vs. QÂ¶
Picard quickly estimates that his chance of winning is 50% and agrees to play the game. He loses the first round and insists on playing again. To his surprise Q agrees, and they continue playing several rounds more, each of which Picard loses. How is that possible?
What Picard did not anticipate is that Q has access to quantum tools. Instead of flipping the penny, Q puts the penny into a superposition of heads and tails proportional to the quantum state \(H\rangle+T\rangle\). Then no matter whether Picard flips the penny or not, it will stay in a superposition (though the relative sign might change). In the third step Q undoes the superposition and always finds the penny to show heads.
Letâ€™s see how this works!
To simulate the game we first construct the corresponding quantum circuit, which takes two qubits: one to simulate Picardâ€™s choice whether or not to flip the penny, and the other to represent the penny. The initial state for all qubits is \(0\rangle\) (which is mapped to \(T\rangle\), tails). To simulate Picardâ€™s decision, we assume that he chooses randomly whether or not to flip the coin, in agreement with the optimal strategy for the classic pennyflip game. This random choice can be created by putting one qubit into an equal superposition, e.g. with the Hadamard gate \(H\), and then measure its state. The measurement will show heads or tails with equal probability \(p_h=p_t=0.5\).
To simulate the penny flip game we take the second qubit and put it into its excited state \(1\rangle\) (which is mapped to \(H\rangle\), heads) by applying the X (or NOT) gate. Qâ€™s first move is to apply the Hadamard gate H. Picardâ€™s decision about the flip is simulated as a CNOT operation where the control bit is the outcome of the random number generator described above. Finally Q applies a Hadamard gate again, before we measure the outcome. The full circuit is shown in the figure below.
In pyQuilÂ¶
We first import and initialize the necessary tools 3
from pyquil import Program
from pyquil.api import WavefunctionSimulator
from pyquil.gates import *
wf_sim = WavefunctionSimulator()
p = Program()
and then wire it all up into the overall measurement circuit; remember that qubit 0 is the penny, and qubit 1 represents Picardâ€™s choice.
p += X(0)
p += H(0)
p += H(1)
p += CNOT(1, 0)
p += H(0)
We use the quantum mechanics principle of deferred measurement to keep all the measurement logic separate from the gates.
Our method call to the WavefunctionSimulator
will handle measuring for us 4.
Finally, we play the game several times. (Remember to run your qvm server.)
wf_sim.run_and_measure(p, trials=10)
array([[1, 1],
[1, 1],
[1, 1],
[1, 1],
[1, 1],
[1, 0],
[1, 1],
[1, 1],
[1, 1],
[1, 0]])
In each trial, the first number is the outcome of the game, whereas the second number represents Picardâ€™s choice to flip or not flip the penny.
Inspecting the results, we see that no matter what Picard does, Q will always win!
 1
 2
 3
See more: Programs and Gates
 4
More about measurements and
run_and_measure
: Measurement
ChangelogÂ¶
next (In development)Â¶
AnnouncementsÂ¶
Improvements and ChangesÂ¶
BugfixesÂ¶
Fix key error for unmeasured memory regions (@notmgsk, @ameyerrigetti, #1156)
v2.28.0 (January 26, 2021)Â¶
AnnouncementsÂ¶
Improvements and ChangesÂ¶
BugfixesÂ¶
Fix parsing error for parameterized
DEFCIRUCIT
s (@ameyerrigetti, #1295)
v2.27.0 (December 30, 2020)Â¶
AnnouncementsÂ¶
Switched to Github Actions.
Improvements and ChangesÂ¶
Bump RPCQ dependency to 3.6.0 (@notmgsk, #1286).
Tests can be run in parallel (@notmgsk, #1289).
BugfixesÂ¶
Fix hanging test due to ZMQ bug (@notmgsk).
Fix unitary comparison in Quil compilation test (@notmgsk).
Fix parsing comments in Lark grammar (@notmgsk, #1290).
v2.26.0 (December 10, 2020)Â¶
AnnouncementsÂ¶
QuilT brings the dimension of time to your quantum programs! QuilT is an extension of Quil which allows one to develop quantum programs at the level of pulses and waveforms and brings an unprecedented level of finegrained control over the QPU.
Improvements and ChangesÂ¶
Unpacking bitstrings is significantly faster (@mhodsonrigetti, @notmgsk, #1276).
Parsing is now performed using Lark rather than ANTLR, often allowing a 10x improvement in parsing large and complex programs (@notmgsk, #1278).
Gates now generally allow a â€śformalâ€ť qubit label as in
DEFCIRCUIT
, rather than requiring a numeric index (#1257).Program
objects come with additional QuilT related properties, such ascalibrations
,waveforms
, andframes
(#1257).The
AbstractCompiler
classes come with tools for performing calibration of programs. Namely,get_calibration_program
provides a program for calibrating against recent QPU settings (#1257).rewrite_arithmetic
now converts phase angle from radians to revolutions (#1257).Readout is more permissive, and does not require the destination to be named
"ro"
(#1257).The default value for
QPU_COMPILER_URL
has been updated to point to Rigettiâ€™s translation service. This changes allows one to use the translation service to translate a QuilT program and receive the binary payload without having a QPU reservation (#1257).
BugfixesÂ¶
v2.25.0 (November 17, 2020)Â¶
AnnouncementsÂ¶
Improvements and ChangesÂ¶
Timeout configuration has been revamped.
get_qc
now accepts acompiler_timeout
option, andQVMCompiler
andQPUCompiler
provide aset_timeout
method, which should greatly simplify the task of changing the default timeout.QVMCompiler
also provides aquilc_client
property so that it shares the same interface asQPUCompiler
. Documentation has been updated to reflect these changes (@notmgsk, @kalzoo, #1273).
BugfixesÂ¶
v2.24.0 (November 5, 2020)Â¶
AnnouncementsÂ¶
Improvements and ChangesÂ¶
run_and_measure
now only measures the qubits that are used in a program (rather than all qubits on the device) when the target QAM is a QVM without noise. This prevents the QVM from exhausting memory when it tries to allocate for e.g. 32 qubits when only e.g. 2 qubits are used in the program (@notmgsk, #1252).Include a
py.typed
so that libraries that depend on pyquil can validate their typing against it (@notmgsk, #1256).Removed warnings expected in normal workflows that cannot be avoided programmatically. This included the warning about passing native Quil to
native_quil_to_executable
. Documentation has been updated to clarify expected behavior (@mhodsonrigetti, gh1267).
BugfixesÂ¶
Fixed incorrect return type hint for the
exponential_map
function, which now accepts bothfloat
andMemoryReference
types for exponentiation (@mhodsonrigetti, gh1243).
v2.23.1 (September 9, 2020)Â¶
AnnouncementsÂ¶
Improvements and ChangesÂ¶
Push new pyquil versions to pypi as part of CI/CD pipelines (@notmgsk, gh1249)
BugfixesÂ¶
Allow
np.ndarray
inDefPermutationGate
(@notmgsk, gh1248)
v2.23.0 (September 7, 2020)Â¶
AnnouncementsÂ¶
Improvements and ChangesÂ¶
Compiler connection timeouts are now entirely userconfigurable (@kalzoo, gh1246)
BugfixesÂ¶
Do not issue a warning if OAuth2 token returns a string (@erichulburd, gh1244)
v2.22.0 (August 3, 2020)Â¶
AnnouncementsÂ¶
Improvements and ChangesÂ¶
Various improvements and updates to the documentation.
BugfixesÂ¶
v2.21.1 (July 15, 2020)Â¶
AnnouncementsÂ¶
This is just a cosmetic update, to trigger a new docker build.
Improvements and ChangesÂ¶
BugfixesÂ¶
Fix type hinting (@notmgsk, gh1230)
v2.21.0 (July 14, 2020)Â¶
AnnouncementsÂ¶
Improvements and ChangesÂ¶
Documentation for Compiler, Advanced Usage, and Troubleshooting sections updated (@notmgsk, gh1220).
Use numeric abstract base classes for type checking (@kilimanjaro, gh1219).
Add XY to docs (@notmgsk, gh1226).
BugfixesÂ¶
Fix damping after dephasing noise model (@maxradin, gh1217).
v2.20 (June 5, 2020)Â¶
AnnouncementsÂ¶
Improvements and ChangesÂ¶
Added a PyQuil only
rewrite_arithmetic
handler, deprecating the previous RPC call toquilc
innative_quil_to_executable
(@kilimanjaro, gh1210).
BugfixesÂ¶
Fix link in documentation (@notmgsk, gh1204).
Add
RX(0) _
to the native gates of a Nq qvm (@notmgsk, gh1211).
v2.19 (March 26, 2020)Â¶
AnnouncementsÂ¶
Improvements and ChangesÂ¶
Add a section to
CONTRIBUTING.md
about publishing packages to condaforge (@appleby, gh1186).Correctly insert state preparation code in
Experiment
s before main program code (@notmgsk, gh1189).controlled
modifier now accepts either a Sequence of control qubits or a single control qubit. Previously, only a single control qubit was supported (@adamglos92, gh1196).
BugfixesÂ¶
Fix flakiness in
test_run
inpyquil/test/test_quantum_computer.py
(@appleby, gh1190).Fix a bug in QuantumComputer.experiment that resulted in a TypeError being raised when called multiple times on the same experiment when the underlying QAM was a QVM based on a physical device (@appleby, gh1188).
v2.18 (March 3, 2020)Â¶
AnnouncementsÂ¶
Improvements and ChangesÂ¶
BugfixesÂ¶
Fixed the QCS access request link in the README (@amyfbrown, gh1171).
Fix the SDK download link and instructions in the docs (@amyfbrown, gh1173).
Fix broken link to example now in foresttutorials (@jlapeyre, gh1181).
Removed HALT from valid Protoquil / supported Quil. (@kilimanjaro, gh1176).
Fix error in comment in Noise and Quantum Computation page (@jlapeyre gh1180)
v2.17 (January 30, 2020)Â¶
AnnouncementsÂ¶
In order to make the pyQuil examples more accessible, we recently made a new repository, rigetti/foresttutorials, which is set up so that the example notebooks can be run via a web browser in a preconfigured execution environment on Binder. The pyQuil README now has a â€ślaunch binderâ€ť badge for running these tutorial notebooks, as well as a â€śQuickstartâ€ť section explaining how they work. To run the tutorial notebooks, click the badge in the README or the link here (@karalekas, gh1167).
Improvements and ChangesÂ¶
Pin the
antlr4python3runtime
package to belowv4.8
(@karalekas, gh1163).Expand upon the acknowledgements file to mention contributions from preQCS and list previous maintainers (@karalekas, gh1165).
Use the rigetti/gitlabpipelines repositoryâ€™s template YAMLs in the
.gitlabci.yml
, and add a section toCONTRIBUTING.md
about the CI/CD pipelines (@karalekas, gh1166).Add another round of improvements to the README (@karalekas, gh1168).
BugfixesÂ¶
Replace references to nonexistent
endpoint
init arg when constructingQPUCompiler
s intest_qpu.py
(@appleby, gh1164).Preserve program metadata when constructing and manipulating
Experiment
objects (@kilimanjaro, gh1160).
v2.16 (January 10, 2020)Â¶
AnnouncementsÂ¶
The
TomographyExperiment
class has been renamed toExperiment
. In addition, there is a newQuantumComputer.calibration
method for performing readout calibration on a providedExperiment
, and utilities for applying the results of the calibration to correct for symmetrized readout error.ExperimentSetting
objects now also have anadditional_expectations
attribute for extracting simultaneously measurable expectation values from a single setting when usingQuantumComputer.experiment
(@karalekas, gh1152, gh1158).
Improvements and ChangesÂ¶
Type hints have been added to the
quil.py
file (@rht, gh1115, gh1134).Use Black for code style and enforce it (along with a line length of 100) via the
style
(flake8
) andformatcheck
(black check
) CI jobs (@karalekas, gh1132).Ignore fewer
flake8
style rules, add theflake8bugbear
plugin, and rename the stylerelatedMakefile
targets and CI jobs so that they have a uniform naming convention:checkall
,checkformat
,checkstyle
, andchecktypes
(@karalekas, gh1133).Added type hints to
noise.py
, began verifying in the CI (@rht, gh1136).Improved reStructuredText markup in docstrings (@peterjc, gh1141).
Add helper to separate
ExperimentResults
by groups of qubits on which their operator acts (@kylegulshen, gh1078).Added typing to the
pyquil/latex
module and added the module to thechecktypes
CI job (@karalekas, gh1142).Add helper to merge
TomographyExperiment
s in theexperiment
moduleâ€™s_group.py
file. Movegroup_experiments
fromoperator_estimation.py
to_group.py
and rename togroup_settings
but maintain backwards compatibility (@kylegulshen, gh1077).The code in
gate_matrices.py
,numpy_simulator.py
,reference_simulator.py
, andunitary_tools.py
has been typed and reorganized into a newsimulation
subdirectory, maintaining backwards compatibility (@karalekas, gh1143).Added a
.travis.yml
file to enable Travis CI for externalcontributor builds, and upgraded GitLab CI style checks to py37 (@karalekas, gh1145).Delete
api/_job.py
,JobConnection
, andSyncConnection
, which have been deprecated for over a year and a half (@karalekas, gh1144).Added typing to the
pyquil/experiment
module and added the module to thechecktypes
CI job (@karalekas, gh1146).Use
dataclasses
instead ofnamedtuples
in thepyquil/device
module, and add type annotations to the entire module (@karalekas, gh1149).Reduced the number of
mypy
errors inpaulis.py
(@rht, gh1147).Compile to XY gates as well as CZ gates on dummy QVMs (@ecpeterson, gh1151).
QAM.write_memory
now accepts either aSequence
of values or a single value (@tommymoffat, gh1114).Added type hints for all remaining toplevel files (@karalekas, gh1150).
Added type annotations to the whole
pyquil.api
module (@karalekas, gh1157).
BugfixesÂ¶
Donâ€™t attach pipes to stdout/stderr when starting quilc and qvm processes in
local_forest_runtime
. This prevents the pipe buffers from getting full and causing hung quilc/qvm for long running processes (@appleby, gh1122).Pass a sequence to
np.vstack
to avoid aFutureWarning
, and add a protoquil keyword argument toMyLazyCompiler.quil_to_native_quil
to avoid aTypeError
in themigration2qc.ipynb
notebook (@appleby, gh1138).Removed unused method
Program._out()
inquil.py
(@rht, gh1137).Fixed string concatenation style, caused by
black
(@peterjc, gh1139).
v2.15 (December 20, 2019)Â¶
AnnouncementsÂ¶
PyQuil now supports encryption for communication with the QPU. It does so by requesting an
Engagement
from Forest Dispatch, which includes the keys necessary for encryption along with the endpoints to use. This workflow is managed by the newForestSession
class, and in the general case is transparent to the user (@kalzoo, gh1123).
Improvements and ChangesÂ¶
LaTeX circuit output now ignores
RESET
instructions by default, rendering instead the (equivalent) program withRESET
omitted (@kilimanjaro, gh1118)Broadened the scope of
flake8
compliance to the include theexamples
anddocs
directories, and thus the whole repository (@tommymoffat, gh1113).DEFGATE ... AS PAULISUM
is now supported (@ecpeterson, gh1125).Add unit test for validating Trotterization order (@jmbr, gh1120).
Updated the authentication mechanism to Forest server. Preferentially use credentials found at
~/.qcs/user_auth_credentials
and fall back to~/.qcs/qmi_auth_credentials
(@erichulburd, gh1123).The log level can now be controlled with the
LOG_LEVEL
environment variable, set toLOG_LEVEL=DEBUG
to help diagnose problems. In addition, certain errors will no longer print their entire stack trace outside ofDEBUG
mode, for a cleaner console and better user experience. This is only true for errors where the cause is well known (@kalzoo, gh1123).Connection to the QPU compiler now supports both ZeroMQ and HTTP(S) (@kalzoo, gh1127).
Bump quilc / qvm parent Docker images to v1.15.1 (@karalekas, gh1128).
BugfixesÂ¶
Pinned the
mypy
version to work around issue with nested types causing themake typecheck
CI job to fail (@erichulburd, gh1119).Minor fixes for
examples/1.3_vqe_demo.py
andexamples/quantum_walk.ipynb
(@appleby, gh1116).Only request engagement from Forest Dispatch when QPU and QPU Compiler addresses are not provided by other configuration sources (@kalzoo, gh1130).
v2.14 (November 25, 2019)Â¶
AnnouncementsÂ¶
There is a new
QuantumComputer.experiment
method for running a collection of quantum programs as defined by aTomographyExperiment
. These objects have a main program body and a collection of state preparation and measurement specifications, which capture the structure of many nearterm applications and algorithms like the variational quantum eigensolver (VQE). In addition, theTomographyExperiment
encodes information about symmetrization, active qubit reset, and the number of shots to perform on the quantum backend (e.g. the QVM or QPU). For more information check out the API documentation sections on the Quantum Computer and on the Experiment Module (@karalekas, gh1100).
Improvements and ChangesÂ¶
Type hints have been added to the
PauliTerm
class (@rht, gh1075).The
rigetti/forest
Docker image now has less noisy output due to stdout and stderr redirection to log filesentrypoint.sh
(@karalekas, gh1105).Added a
make typecheck
target to runmypy
over a subset of the pyquil sources, and enabled typechecks in the GitLab CI pipeline (@appleby, gh1098).Added support for the
XY
(parameterizediSWAP
) gate family inProgram
s and inISA
s (@ecpeterson, gh1096, gh1107, gh1111).Removed the
tox.ini
andreadthedocs.yml
files (@karalekas, gh1108).Type hints have been added to the
PauliSum
class (@rht, gh1104).
BugfixesÂ¶
Fixed a bug in the LaTeX output of controlled unitary operations (@kilimanjaro, gh1103).
Fixed an example of using the
qc.run
method in the docs to correctly declare the size of a memory register (@appleby, gh1099).Specify UTF8 encoding when opening files that might contain nonascii characters, such as when reading the pyquil README.md file in setup.py or when serializing / deserializing pyquil.experiment objects to/from JSON (@appleby, gh1102).
v2.13 (November 7, 2019)Â¶
AnnouncementsÂ¶
Rather than installing pyQuil from PyPI, condaforge, or the source directly, users with Docker installed can pull and run the
`rigetti/forest
<https://hub.docker.com/r/rigetti/forest>`__ Docker image to quickly get started with compiling and simulating quantum programs! When running the image, a user will be dropped into anipython
REPL that has pyQuil and its requirements preinstalled, along with quilc and qvm servers running in the background (@karalekas, gh1035, gh1039).Circuit diagram generation has had a makeover! In particular, the
pyquil.latex
module provides two mechanisms for generating diagrams from pyQuil programs:pyquil.latex.to_latex
generates humanreadable LaTeX output expressing aProgram
as a circuit diagram, andpyquil.latex.display
renders aProgram
as anIPython.display.Image
for inline viewing in Jupyter Notebooks. Learn more about these features in the new example notebook (@kilimanjaro, gh1074).
Improvements and ChangesÂ¶
Added a
Makefile
with some simple targets for performing common build operations like creating and uploading a package (@karalekas, gh1032).Replaced symmetrization in
operator_estimation
with functionality contained withinQuantumComputer.run_symmetrized_readout
(@kylegulshen, gh1047).As part of the CI, we now package and push to TestPyPI on every commit, which derisks breaking the
setup.py
and aids with testing (@karalekas, gh1017).We now calculate code coverage as part of the CI pipeline (@karalekas, gh1052).
Moved the program generation from
measure_observables
into its own private function (@kylegulshen, gh1043).All uses of
__future__
andsix
have been dropped (@karalekas, gh1060).The
conftest.py
has been moved to the project root dir (@karalekas, gh1064).Using
protoquil
as a positional argument toqc.compile
has been deprecated, and it is now a keywordonly argument (@karalekas, gh1071).PauliSum
objects are now hashable (@ecpeterson, gh1073).The code in
device.py
as been reorganized into a newdevice
subdirectory in a completely backwardscompatible fashion (@karalekas, gh1066, gh1094).PauliTerm
andPauliSum
now have__repr__
methods (@karalekas, gh1080).The experimentschemarelated code in
operator_estimation.py
has been moved into a newexperiment
subdirectory (@karalekas, gh1084, gh1094).The keyword arguments to
measure_observables
are now captured as part of theTomographyExperiment
class (@karalekas, gh1090).Type hints have been added to the
pyquil.gates
,pyquil.quilatom
, andpyquil.quilbase
modules (@appleby, gh999).We now support Python 3.8 and it is tested in the CI (@karalekas, gh1093).
BugfixesÂ¶
Updated
examples/meyer_penny_game.py
with the correct path to the Meyer Penny game exercise indocs/source/exercises.rst
(@appleby, gh1045).Fixed the Slack Workspace invite link in the README (@amyfbrown, gh1042).
QPU.reset()
now checks whetherpyquil_config.qpu_url
exists before updating the endpoint so as not to break custom connections (@kylegulshen, gh1072).Fixed pretty printing of parameter expressions where Ď€ is involved (@notmgsk, gh1076).
Fixed a regression in
PyQVM.execute
that prevented it from running programs containing userdefined gates (@appleby, gh1067).Remove some stale code for pulling quilc version info (@notmgsk, gh1089).
v2.12 (September 28, 2019)Â¶
AnnouncementsÂ¶
There is now a Contributing Guide for those who would like to participate in the development of pyQuil. Check it out! In addition, pyQuil now has a Bug Report Template, and a Feature Request Template, which contain sections to fill out when filing a bug or suggesting an enhancement (@karalekas, gh985, gh986, gh996).
Improvements and ChangesÂ¶
The
local_qvm
context manager has been renamed tolocal_forest_runtime
, which now checks if the designated ports are used before startingqvm
/quilc
. The originallocal_qvm
has been deprecated (@sauercrowd, gh976).The test suite for pyQuil now runs against both Python 3.6 and 3.7 to ensure compatibility with the two most recent versions of Python (@karalekas, gh987).
Add support for the
FORKED
gate modifier (@kilimanjaro, gh989).Deleted the deprecated modules
parameters.py
andqpu.py
(@karalekas, gh991).The test suite for pyQuil now runs much faster, by setting the default value of the
useseed
option forpytest
toTrue
(@karalekas, gh992).Support nongate instructions (e.g.
MEASURE
) into_latex()
(@notmgsk, gh975).Test suite has been updated to reduce the use of deprecated features (@kilimanjaro, gh998, gh1005).
Certain tests have been marked as â€śslowâ€ť, and are skipped unless the
runslow
option is specified forpytest
(@kilimanjaro, gh1001).PauliSum
objects can now be constructed from strings viafrom_compact_str()
andPauliTerm.from_compact_str()
supports multiqubit strings (@jlbosse, gh984).
BugfixesÂ¶
Strength two symmetrization was not correctly producing orthogonal arrays due to erroneous truncation, which has been fixed (@kylegulshen, gh990).
The
STORE
instruction now acceptsint
orfloat
in addition toMemoryReference
as itssource
argument. As a result, you can nowSTORE
an immediate value into a memory register. Also, theEQ
,LT
,LE
,GT
, andGE
instructions now all acceptfloat
in addition toint
orMemoryReference
as their third and final argument. As a result, you can now perform classical comparisons against an immediatefloat
value. Finally, theCONVERT
instruction now accepts any valid memory reference designator (aMemoryReference
, a string, or a tuple of type(str, int)
) for both its arguments (@appleby, gh1010).Raise an error if a gate with nonconstant parameters is provided to
lifted_gate
(@notmgsk, gh1012).
v2.11 (September 3, 2019)Â¶
AnnouncementsÂ¶
PyQuilâ€™s changelog has been overhauled and rewritten in Markdown instead of RST, and can be found in the toplevel directory of the repository as the CHANGELOG.md file (which is the standard for most GitHub repositories). However, during the build process, we use
pandoc
to convert it back to RST so that it can be included as part of the ReadTheDocs documentation here (@karalekas, gh945, gh973).
Improvements and ChangesÂ¶
Test suite attempts to retry specific tests that fail often. Tests are retried only a single time (@notmgsk, gh951).
The
QuantumComputer.run_symmetrized_readout()
method has been revamped, and now has options for using more advanced forms of readout symmetrization (@joshcombes, gh919).The ProtoQuil restrictions built in to PyQVM have been removed (@ecpeterson, gh874).
Add the ability to query for other memory regions after both QPU and QVM runs. This removes a previously unnecessary restriction on the QVM, although
ro
remains the only QPUwriteable memory region during Quil execution (@ecpeterson, gh873).Now, running
QuantumComputer.reset()
(andQuantumComputer.compile()
when using the QPU) additionally resets the connection information for the underlyingQVM
/QPU
andQVMCompiler
/QPUCompiler
objects, which should resolve bugs that arise due to stale clients/connections (@karalekas, gh872).In addition to the simultaneous 1Q RB fidelities contained in device specs prior to this release, there are now 1Q RB fidelities for nonsimultaneous gate operation. The names of these fields have been changed for clarity, and standard errors for both fidelities have been added as well. Finally, deprecation warnings have been added regarding the
fCPHASE
andfBellState
device spec fields, which are no longer routinely updated and will be removed in release v2.13 (@jvalery2, gh968).The NOTICE has been updated to accurately reflect the thirdparty software used in pyQuil (@karalekas, gh979).
PyQuil now sends â€śmodernâ€ť ISA payloads to quilc, which must be of version >=
1.10.0
. Check out the details ofget_isa
for information on how to specify custom payloads (@ecpeterson, gh961).
BugfixesÂ¶
The
MemoryReference
warnings have been removed from the unit tests (@maxKenngott, gh950).The
merge_programs
function now supports merging programs withDefPermutationGate
, instead of throwing an error, and avoids redundant readout declaration (@kylegulshen, gh971).Remove unsound logic to fill out nonâ€śroâ€ť memory regions when targeting a QPU (@notmgsk, gh982).
v2.10 (July 31, 2019)Â¶
Improvements and ChangesÂ¶
Rewrote the README, adding a more indepth overview of the purpose of pyQuil as a library, as well as two badges â€“ one for PyPI downloads and another for the Forest Slack workspace. Also, included an example section for how to get started with running a simple Bell state program on the QVM (@karalekas, gh946, gh949).
The test suite for
pyquil.operator_estimation
now has an (optional) faster version that uses fixed random seeds instead of averaging over several experiments. This can be enabled with theuseseed
command line option when runningpytest
(@msohaibalam, gh928).Deleted the deprecated modules
job_results.py
andkraus.py
(@karalekas, gh957).Updated the examples README. Removed an outdated notebook. Updated remaining notebooks to use
MemoryReference
, and fix any parts that were broken (@notmgsk, gh820).The
AbstractCompiler.quil_to_native_quil()
function now accepts aprotoquil
keyword which tells the compiler to restrict both input and output to protoquil (i.e. Quil code executable on a QPU). Additionally, the compiler will return a metadata dictionary that contains statistics about the compiled program, e.g. its estimated QPU runtime. See the compiler docs for more information (@notmgsk, gh940).Updated the QCS and Slack invite links on the
index.rst
docs page (@starktech23, gh965).Provided example code for reading out the QPU runtime estimation for a program (@notmgsk, gh963).
BugfixesÂ¶
unitary_tools.lifted_gate()
was not properly handling modifiers such asDAGGER
andCONTROLLED
(@kylegulshen, gh931).Fixed warnings raised by Sphinx when building the documentation (@appleby, gh929).
v2.9.1 (June 28, 2019)Â¶
BugfixesÂ¶
Relaxed the requirement for a quilc server to exist when users of the
QuantumComputer
object only want to do simulation work with aQVM
orpyQVM
backend (@karalekas, gh934).
v2.9 (June 25, 2019)Â¶
AnnouncementsÂ¶
PyQuil now has a Pull Request Template, which contains a checklist of things that must be completed (if applicable) before a PR can be merged (@karalekas, gh921).
Improvements and ChangesÂ¶
Removed a bunch of logic around creating inverse gates from userdefined gates in
Program.dagger()
in favor of a simpler call toGate.dagger()
(@notmgsk, gh887).The
RESET
instruction now works correctly withQubitPlaceholder
objects and theaddress_qubits
function (@jclapis, gh910).ReferenceDensitySimulator
can now have a state that is persistent between rounds ofrun
orrun_and_measure
(@joshcombes, gh920).
BugfixesÂ¶
Small negative probabilities were causing
ReferenceDensitySimulator
to fail (@joshcombes, gh908).The
dagger
function was incorrectly dropping gate modifiers likeCONTROLLED
(@jclapis, gh914).Negative numbers in classical instruction arguments were not being parsed (@notmgsk, gh917).
Inline math rendering was not working correctly in
intro.rst
(@appleby, gh927).
Thanks to community member @jclapis for the contributions to this release!
v2.8 (May 20, 2019)Â¶
Improvements and ChangesÂ¶
PyQuil now verifies that you are using the correct version of the QVM and quilc (@karalekas, gh913).
Added support for defining permutation gates for use with the latest version of quilc (@notmgsk, gh891).
The rpcq dependency requirement has been raised to v2.5.1 (@notmgsk, gh911).
Added a note about the QVMâ€™s compilation mode to the documentation (@stylewarning, gh900).
Some measure_observables params now have the
Optional
type specification (@msohaibalam, gh903).
BugfixesÂ¶
Preserve modifiers during
address_qubits
(@notmgsk, gh907).
v2.7.2 (May 3, 2019)Â¶
BugfixesÂ¶
An additional backwardsincompatible change from gh870 snuck through 2.7.1, and is addressed in this patch release (@karalekas, gh901).
v2.7.1 (April 30, 2019)Â¶
BugfixesÂ¶
The changes to operator estimation (gh870, gh896) were not made in a backwardscompatible fashion, and therefore this patch release aims to remedy that. Going forward, there will be much more stringent requirements around backwards compatibility and deprecation (@karalekas, gh899).
v2.7 (April 29, 2019)Â¶
Improvements and ChangesÂ¶
Standard deviation > standard error in operator estimation (@msohaibalam, gh870).
Update what pyQuil expects from quilc in terms of rewiring pragmas â€“ they are now comments rather than distinct instructions (@ecpeterson, gh878).
Allow users to deprioritize QPU jobs â€“ mostly a Rigettiinternal feature (@jvalery2, gh877).
Remove the
qubits
field from theTomographyExperiment
dataclass (@msohaibalam, gh896).
BugfixesÂ¶
Ensure that shots arenâ€™t lost when passing a
Program
throughaddress_qubits
(@notmgsk, gh895).Fixed the
conda
install command in the README (@seandiscovery, gh890).
v2.6 (March 29, 2019)Â¶
Improvements and ChangesÂ¶
Added a CODEOWNERS file for default reviewers (@karalekas, gh855).
Bifurcated the
QPUCompiler
endpoint parameter into two â€“quilc_endpoint
andqpu_compiler_endpoint
â€“ to reflect changes in Quantum Cloud Services (@karalekas, gh856).Clarified documentation around the DELAY pragma (@willzeng, gh862).
Added information about the
local_qvm
context manager to the getting started documentation (@willzeng, gh851).Added strict version lower bounds on the rpcq and networkx dependencies (@notmgsk, gh828).
A slice of a
Program
object now returns aProgram
object (@notmgsk, gh848).
BugfixesÂ¶
Added a nonNone default timeout to the
QVMCompiler
object and theget_benchmarker
function (@karalekas, gh850, gh854).Fixed the docstring for the
apply_clifford_to_pauli
function (@kylegulshen, gh836).Allowed the
apply_clifford_to_pauli
function to now work with the Identity as input (@msohaibalam, gh849).Updated a stale link to the Rigetti Forest Slack workspace (@karalekas, gh860).
Fixed a notation typo in the documentation for noise (@willzeng, gh861).
An
IndexError
is now raised when trying to access an outofbounds entry in aMemoryReference
(@notmgsk, gh819).Added a check to ensure that
measure_observables
takes as many shots as requested (@marcusps, gh846).
Special thanks to @willzeng for all the contributions this release!
v2.5 (March 6, 2019)Â¶
Improvements and ChangesÂ¶
PyQuilâ€™s Gate objects now expose
.controlled(q)
and.dagger()
modifiers, which turn a gate respectively into its controlled variant, conditional on the qubitq
, or into its inverse.The operator estimation suiteâ€™s
measure_observables
method now exposes areadout_symmetrize
argument, which helps mitigate a machineâ€™s fidelity asymmetry between recognizing a qubit in the ground state versus the excited state.The
MEASURE
instruction in pyQuil now has a mandatory second argument. Previously, the second argument could be omitted to induce â€śmeasurement for effectâ€ť, without storing the readout result to a classical register, but users found this to be a common source of accidental error and a generally rude surprise. To ensure the user really intends to measure only for effect, we now require that they supply an explicitNone
as the second argument.
BugfixesÂ¶
Some stale tests have been brought into the modern era.
v2.4 (February 14, 2019)Â¶
AnnouncementsÂ¶
The Quil Compiler (quilc) and the Quantum Virtual Machine (QVM), which are part of the Forest SDK, have been open sourced! In addition to downloading the binaries, you can now build these applications locally from source, or run them via the Docker images rigetti/quilc and rigetti/qvm. These Docker images are now used as the
services
in the GitLab CI build plan YAML (gh792, gh794, gh795).
Improvements and ChangesÂ¶
The
WavefunctionSimulator
now supports the use of parametric Quil programs, via thememory_map
parameter for its various methods (gh787).Operator estimation data structures introduced in v2.2 have changed. Previously,
ExperimentSettings
had two members:in_operator
andout_operator
. Theout_operator
is unchanged, butin_operator
has been renamed toin_state
and its data type is nowTensorProductState
instead ofPauliTerm
. It was always an abuse of notation to interpret pauli operators as defining initial states. Analogous to the Pauli helper functions sI, sX, sY, and sZ,TensorProductState
objects are constructed by multiplying together terms generated by the helper functions plusX, minusX, plusY, minusY, plusZ, and minusZ. This functionality enables process tomography and process DFE (gh770).Operator estimation now offers a â€śgreedyâ€ť method for grouping tomographylike experiments that share a natural tensor product basis (ntpb), as an alternative to the clique cover version (gh754).
The
quilc
endpoint for rewriting Quil parameter arithmetic has been changed fromresolve_gate_parameter_arithmetic
torewrite_arithmetic
(gh802).The difference between ProtoQuil and QPUsupported Quil is now better defined (gh798).
BugfixesÂ¶
Resolved an issue with postgate noise in the pyQVM (gh801).
A
TypeError
with a useful error message is now raised when aProgram
object is run on a QPUbackedQuantumComputer
, rather than a confusingAttributeError
(gh799).
v2.3 (January 28, 2019)Â¶
PyQuil 2.3 is the latest release of pyQuil, Rigettiâ€™s toolkit for constructing and running quantum programs. A major new feature is the release of a new suite of simulators:
Weâ€™re proud to introduce the first iteration of a Pythonbased quantum virtual machine (QVM) called PyQVM. This QVM is completely contained within pyQuil and does not need any external dependencies. Try using it with
get_qc("9qsquarepyqvm")
or explore thepyquil.pyqvm.PyQVM
object directly. Underthehood, there are three quantum simulator backends:ReferenceWavefunctionSimulator
uses standard matrixvector multiplication to evolve a statevector. This includes a suite of tools inpyquil.unitary_tools
for dealing with unitary matrices.NumpyWavefunctionSimulator
uses numpyâ€™s tensordot functionality to efficiently evolve a statevector. For most simulations, performance is quite good.ReferenceDensitySimulator
uses matrixmatrix multiplication to evolve a density matrix.
Matrix representations of Quil standard gates are included in
pyquil.gate_matrices
(gh552).The density simulator has extremely limited support for Krausoperator based noise models. Let us know if youâ€™re interested in contributing more robust noisemodel support.
This functionality should be considered experimental and may undergo minor API changes.
Important changes to noteÂ¶
Quil math functions (like COS, SIN, â€¦) used to be ambiguous with respect to case sensitivity. They are now casesensitive and should be uppercase (gh774).
In the next release of pyQuil, communication with quilc will happen exclusively via the rpcq protocol.
LocalQVMCompiler
andLocalBenchmarkConnection
will be removed in favor of a unifiedQVMCompiler
andBenchmarkConnection
. This change should be transparent if you useget_qc
andget_benchmarker
, respectively. In anticipation of this change we recommend that you upgrade your version of quilc to 1.3, released Jan 30, 2019 (gh730).When using a paramaterized gate, the QPU control electronics only allowed multiplying parameters by powers of two. If you only ever multiply a parameter by the same constant, this isnâ€™t too much of a problem because you can fold the multiplicative constant into the definition of the parameter. However, if you are multiplying the same variable (e.g.
gamma
in QAOA) by different constants (e.g. weighted maxcut edge weights) it doesnâ€™t work. PyQuil will now transparently handle the latter case by expanding to a vector of parameters with the constants folded in, allowing you to multiply variables by whatever you want (gh707).
Bug fixes and improvementsÂ¶
The CZ gate fidelity metric available in the Specs object now has its associated standard error, which is accessible from the method
Specs.fCZ_std_errs
(gh751).Operator estimation code now correctly handles identity terms with coefficients. Previously, it would always estimate these terms as 1.0 (gh758).
Operator estimation results include the total number of counts (shots) taken.
Operator estimation JSON serialization uses utf8. Please let us know if this causes problems (gh769).
The example quantum die program now can roll dice that are not powers of two (gh749).
The teleportation and Meyer penny game examples had a syntax error (gh778, gh772).
When running on the QPU, you could get into trouble if the QPU name passed to
get_qc
did not match the lattice you booked. This is now validated (gh771).
We extend thanks to community member @estamm12 for their contribution to this release.
v2.2 (January 4, 2019)Â¶
PyQuil 2.2 is the latest release of pyQuil, Rigettiâ€™s toolkit for constructing and running quantum programs. Bug fixes and improvements include:
pauli.is_zero
andpaulis.is_identity
would sometimes return erroneous answers (gh710).Parameter expressions involving addition and subtraction are now converted to Quil with spaces around the operators, e.g.
theta + 2
instead oftheta+2
. This disambiguates subtracting two parameters, e.g.alpha  beta
is not one variable namedalphabeta
(gh743).T1 is accounted for in T2 noise models (gh745).
Documentation improvements (gh723, gh719, gh720, gh728, gh732, gh742).
Support for PNG generation of circuit diagrams via LaTeX (gh745).
Weâ€™ve started transitioning to using Gitlab as our continuous integration provider for pyQuil (gh741, gh752).
This release includes a new module for facilitating the estimation of quantum observables/operators (gh682). Firstclass support for estimating observables should make it easier to express nearterm algorithms. This release includes:
data structures for expressing tomographylike experiments and their results
grouping of experiment settings that can be simultaneously estimated
functionality to executing a tomographylike experiment on a quantum computer
Please look forward to more features and polish in future releases. Donâ€™t hesitate to submit feedback or suggestions as GitHub issues.
We extend thanks to community member @petterwittek for their contribution to this release.
Bugfix release 2.2.1 was released January 11 to maintain compatibility with the latest version of the quilc compiler (gh759).
v2.1 (November 30, 2018)Â¶
PyQuil 2.1 is an incremental release of pyQuil, Rigettiâ€™s toolkit for constructing and running quantum programs. Changes include:
Major documentation improvements.
QuantumComputer.run()
accepts an optionalmemory_map
parameter to facilitate running parametric executables (gh657).QuantumComputer.reset()
will reset the state of a QAM to recover from an error condition (gh703).Bug fixes (gh674, gh696).
Quil parser improvements (gh689, gh685).
Optional interleaver argument when generating RB sequences (gh673).
Our GitHub organization name has changed from
rigetticomputing
torigetti
(gh713).
v2.0 (November 1, 2018)Â¶
PyQuil 2.0 is a major release of pyQuil, Rigettiâ€™s toolkit for constructing and running quantum programs. This release contains many major changes including:
The introduction of Quantum Cloud Services. Access Rigettiâ€™s QPUs from colocated classical compute resources for minimal latency. The web API for running QVM and QPU jobs has been deprecated and cannot be accessed with pyQuil 2.0
Advances in classical control systems and compilation allowing the precompilation of parametric binary executables for rapid hybrid algorithm iteration.
Changes to Quilâ€”our quantum instruction languageâ€”to provide easier ways of interacting with classical memory.
The new QCS access model and features will allow you to execute hybrid quantum algorithms several orders of magnitude (!) faster than the previous web endpoint. However, to fully exploit these speed increases you must update your programs to use the latest pyQuil features and APIs. Please read the documentation on what is New in Forest 2 for a comprehensive migration guide.
An incomplete list of significant changes:
Python 2 is no longer supported. Please use Python 3.6+
Parametric gates are now normal functions. You can no longer write
RX(pi/2)(0)
to get a QuilRX(pi/2) 0
instruction. Just useRX(pi/2, 0)
.Gates support keyword arguments, so you can write
RX(angle=pi/2, qubit=0)
.All
async
methods have been removed fromQVMConnection
andQVMConnection
is deprecated.QPUConnection
has been removed in accordance with the QCS access model. Usepyquil.get_qc
as the primary means of interacting with the QVM or QPU.WavefunctionSimulator
allows unfettered access to wavefunction properties and routines. These methods and properties previously lived onQVMConnection
and have been deprecated there.Classical memory in Quil must be declared with a name and type. Please read New in Forest 2 for more.
Compilation has changed. There are now different
Compiler
objects that target either the QPU or QVM. You must explicitly compile your programs to run on a QPU or a realistic QVM.
Version 2.0.1 was released on November 9, 2018 and includes documentation changes only. This release is only available as a git tag. We have not pushed a new package to PyPI.
v1.9 (June 6, 2018)Â¶
Weâ€™re happy to announce the release of pyQuil 1.9. PyQuil is Rigettiâ€™s toolkit for constructing and running quantum programs. This release is the latest in our series of regular releases, and itâ€™s filled with convenience features, enhancements, bug fixes, and documentation improvements.
Special thanks to community members @sethuiyer, @vtomole, @rht, @akarazeev, @ejdanderson, @markf94, @playadust, and @kadora626 for contributing to this release!
Qubit placeholdersÂ¶
One of the focuses of this release is a reworked concept of â€śQubit
Placeholdersâ€ť. These are logical qubits that can be used to construct
programs. Now, a program containing qubit placeholders must be
â€śaddressedâ€ť prior to running on a QPU or QVM. The addressing stage
involves mapping each qubit placeholder to a physical qubit (represented
as an integer). For example, if you have a 3 qubit circuit that you want
to run on different sections of the Agave chip, you now can prepare one
Program and address it to many different subgraphs of the chip topology.
Check out the QubitPlaceholder
example notebook for more.
To support this idea, weâ€™ve refactored parts of Pyquil to remove the
assumption that qubits can be â€śsortedâ€ť. While true for integer qubit
labels, this probably isnâ€™t true in general. A notable change can be
found in the construction of a PauliSum
: now terms will stay in the
order they were constructed.
PauliTerm
now remembers the order of its operations.sX(1)*sZ(2)
will compile to different Quil code thansZ(2)*sX(1)
, although the terms will still be equal according to the__eq__
method. DuringPauliSum
combination of like terms, a warning will be emitted if two terms are combined that have different orders of operation.PauliTerm.id()
takes an optional argumentsort_ops
which defaults to True for backwards compatibility. However, this function should not be used for comparing termtype like it has been used previously. UsePauliTerm.operations_as_set()
instead. In the future,sort_ops
will default to False and will eventually be removed.Program.alloc()
has been deprecated. Please instantiateQubitPlaceholder()
directly or request a â€śregisterâ€ť (list) ofn
placeholders by using the class constructorQubitPlaceholder.register(n)
.Programs must contain either (1) all instantiated qubits with integer indexes or (2) all placeholder qubits of type
QubitPlaceholder
. We have found that most users usebut (2) will become useful with larger and more diverse devices.
Programs that contain qubit placeholders must be explicitly addressed prior to execution. Previously, qubits would be assigned â€śunder the hoodâ€ť to integers 0â€¦N. Now, you must use
address_qubits
which returns a new program with all qubits indexed depending on thequbit_mapping
argument. The original program is unaffected and can be â€śreaddressedâ€ť multiple times.PauliTerm
can now acceptQubitPlaceholder
in addition to integers.QubitPlaceholder
is no longer a subclass ofQubit
.LabelPlaceholder
is no longer a subclass ofLabel
.QuilAtom
subclassesâ€™ hash functions have changed.
Randomized benchmarking sequence generationÂ¶
Pyquil now includes support for performing a simple benchmarking routine
 randomized benchmarking. There is a new method in the
CompilerConnection
that will return sequences of pyquil programs,
corresponding to elements of the Clifford group. These programs are
uniformly randomly sampled, and have the property that they compose to
the identity. When concatenated and run as one program, these programs
can be used in a procedure called randomized benchmarking to gain
insight about the fidelity of operations on a QPU.
In addition, the CompilerConnection
has another new method,
apply_clifford_to_pauli
which conjugates PauliTerms
by
Program
that are composed of Clifford gates. That is to say, given a
circuit C, that contains only gates corresponding to elements of the
Clifford group, and a tensor product of elements P, from the Pauli
group, this method will compute $PCP^{dagger}$
Such a procedure can
be used in various ways. An example is predicting the effect a Clifford
circuit will have on an input state modeled as a density matrix, which
can be written as a sum of Pauli matrices.
Ease of UseÂ¶
This release includes some qualityoflife improvements such as the
ability to initialize programs with generator expressions, sensible
defaults for Program.measure_all
, and sensible defaults for
classical_addresses
in run
methods.
Program
can be initiated with a generator expression.Program.measure_all
(with no arguments) will measure all qubits in a program.classical_addresses
is now optional in QVM and QPUrun
methods. By default, any classical addresses targeted byMEASURE
will be returned.QVMConnection.pauli_expectation
acceptsPauliSum
as arguments. This offers a more sensible API compared toQVMConnection.expectation
.pyQuil will now retry jobs every 10 seconds if the QPU is retuning.
CompilerConnection.compile
now takes an optional argumentisa
that allows percompilation specification of the target ISA.An empty program will trigger an exception if you try to run it.
Supported versions of PythonÂ¶
We strongly support using Python 3 with Pyquil. Although this release works with Python 2, we are dropping official support for this legacy language and moving to community support for Python 2. The next major release of Pyquil will introduce Python 3.5+ only features and will no longer work without modification for Python 2.
Bug fixesÂ¶
shift_quantum_gates
has been removed. Users who relied on this functionality should useQubitPlaceholder
andaddress_qubits
to achieve the same result. Users should also doublecheck data resulting from use of this function as there were several edge cases which would cause the shift to be applied incorrectly resulting in badlyaddressed qubits.Slightly perturbed angles when performing RX gates under a Kraus noise model could result in incorrect behavior.
The quantum die example returned incorrect values when
n = 2^m
.
Introduction to Quantum ComputingÂ¶
With every breakthrough in science there is the potential for new technology. For over twenty years, researchers have done inspiring work in quantum mechanics, transforming it from a theory for understanding nature into a fundamentally new way to engineer computing technology. This field, quantum computing, is beautifully interdisciplinary, and impactful in two major ways:
It reorients the relationship between physics and computer science. Physics does not just place restrictions on what computers we can design, it also grants new power and inspiration.
It can simulate nature at its most fundamental level, allowing us to solve deep problems in quantum chemistry, materials discovery, and more.
Quantum computing has come a long way, and in the next few years there will be significant breakthroughs in the field. To get here, however, we have needed to change our intuition for computation in many ways. As with other paradigms â€” such as objectoriented programming, functional programming, distributed programming, or any of the other marvelous ways of thinking that have been expressed in code over the years â€” even the basic tenants of quantum computing opens up vast new potential for computation.
However, unlike other paradigms, quantum computing goes further. It requires an extension of classical probability theory. This extension, and the core of quantum computing, can be formulated in terms of linear algebra. Therefore, we begin our investigation into quantum computing with linear algebra and probability.
From Bit to QubitÂ¶
Probabilistic Bits as Vector SpacesÂ¶
From an operational perspective, a bit is described by the results of measurements performed on it. Let the possible results of measuring a bit (0 or 1) be represented by orthonormal basis vectors \(\vec{0}\) and \(\vec{1}\). We will call these vectors outcomes. These outcomes span a twodimensional vector space that represents a probabilistic bit. A probabilistic bit can be represented as a vector
where \(a\) represents the probability of the bit being 0 and \(b\) represents the probability of the bit being 1. This clearly also requires that \(a+b=1\). In this picture the system (the probabilistic bit) is a twodimensional real vector space and a state of a system is a particular vector in that vector space.
import numpy as np
import matplotlib.pyplot as plt
outcome_0 = np.array([1.0, 0.0])
outcome_1 = np.array([0.0, 1.0])
a = 0.75
b = 0.25
prob_bit = a * outcome_0 + b * outcome_1
X, Y = prob_bit
plt.figure()
ax = plt.gca()
ax.quiver(X, Y, angles='xy', scale_units='xy', scale=1)
ax.set_xlim([0, 1])
ax.set_ylim([0, 1])
plt.draw()
plt.show()
Given some state vector, like the one plotted above, we can find the probabilities associated with each outcome by projecting the vector onto the basis outcomes. This gives us the following rule:
where \(\operatorname{Pr}(0)\) and \(\operatorname{Pr}(1)\) are the probabilities of the 0 and 1 outcomes respectively.
Dirac NotationÂ¶
Physicists have introduced a convenient notation for the vector transposes and dot products we used in the previous example. This notation, called Dirac notation in honor of the great theoretical physicist Paul Dirac, allows us to define
Thus, we can rewrite our â€śmeasurement ruleâ€ť in this notation as
We will use this notation throughout the rest of this introduction.
Multiple Probabilistic BitsÂ¶
This vector space interpretation of a single probabilistic bit can be straightforwardly extended to multiple bits. Let us take two coins as an example (labelled 0 and 1 instead of H and T since we are programmers). Their states can be represented as
where \(1_u\) represents the outcome 1 on coin \(u\). The combined system of the two coins has four possible outcomes \(\{ 0_u0_v,\;0_u1_v,\;1_u0_v,\;1_u1_v\}\) that are the basis states of a larger fourdimensional vector space. The rule for constructing a combined state is to take the tensor product of individual states, e.g.
Then, the combined space is simply the space spanned by the tensor products of all pairs of basis vectors of the two smaller spaces.
Similarly, the combined state for \(n\) such probabilistic bits is a vector of size \(2^n\) and is given by \(\bigotimes_{i=0}^{n1}\,v_i\rangle\). We will talk more about these larger spaces in the quantum case, but it is important to note that not all composite states can be written as tensor products of substates (e.g. consider the state \(\frac{1}{2}\,0_u0_v\rangle + \frac{1}{2}\,1_u1_v\rangle\)). The most general composite state of \(n\) probabilistic bits can be written as \(\sum_{j=0}^{2^n  1} a_{j} (\bigotimes_{i=0}^{n1}\,b_{ij}\rangle\) where each \(b_{ij} \in \{0, 1\}\) and \(a_j \in \mathbb{R}\), i.e. as a linear combination (with real coefficients) of tensor products of basis states. Note that this still gives us \(2^n\) possible states.
QubitsÂ¶
Quantum mechanics rewrites these rules to some extent. A quantum bit, called a qubit, is the quantum analog of a bit in that it has two outcomes when it is measured. Similar to the previous section, a qubit can also be represented in a vector space, but with complex coefficients instead of real ones. A qubit system is a twodimensional complex vector space, and the state of a qubit is a complex vector in that space. Again we will define a basis of outcomes \(\{\,0\rangle, \,1\rangle\}\) and let a generic qubit state be written as
Since these coefficients can be imaginary, they cannot be simply interpreted as probabilities of their associated outcomes. Instead we rewrite the rule for outcomes in the following manner:
and as long as \(\alpha^2 + \beta^2 = 1\) we are able to recover acceptable probabilities for outcomes based on our new complex vector.
This switch to complex vectors means that rather than representing a state vector in a plane, we instead represent the vector on a sphere (called the Bloch sphere in quantum mechanics literature). From this perspective the quantum state corresponding to an outcome of 0 is represented by:
Notice that the two axes in the horizontal plane have been labeled \(x\) and \(y\), implying that \(z\) is the vertical axis (not labeled). Physicists use the convention that a qubitâ€™s \(\{\,0\rangle, \,1\rangle\}\) states are the positive and negative unit vectors along the z axis, respectively. These axes will be useful later in this document.
Multiple qubits are represented in precisely the same way, by taking linear combinations (with complex coefficients, now) of tensor products of basis states. Thus \(n\) qubits have \(2^n\) possible states.
An Important DistinctionÂ¶
The probabilistic states described above represent ignorance of an underlying state, like 0 or 1 for probabilistic bits. This is not true for quantum states. The nature of quantum states is a deep topic with no full scientific consensus. However, nogo theorems like Bellâ€™s Theorem have ruled out the option of local hidden variable theories for quantum mechanics. Effectively, these say that quantum states canâ€™t be interpreted as purely representing ignorance of an underlying local objective state. In practice this means that a pure quantum state simply is the complex vector described in the last section, and we consider it just as â€śrealâ€ť as a headsup coin. This distinction between quantum and classical states is foundational for understanding quantum computing.
Some CodeÂ¶
Let us take a look at some code in pyQuil to see how these quantum states play out. We will dive deeper into quantum operations and pyQuil in the following sections. Note that in order to run these examples you will need to install pyQuil and download the QVM and Compiler. Each of the code snippets below will be immediately followed by its output.
# Imports for pyQuil (ignore for now)
import numpy as np
from pyquil.quil import Program
from pyquil.api import WavefunctionSimulator
# create a WavefunctionSimulator object
wavefunction_simulator = WavefunctionSimulator()
# pyQuil is based around operations (or gates) so we will start with the most
# basic one: the identity operation, called I. I takes one argument, the index
# of the qubit that it should be applied to.
from pyquil.gates import I
# Make a quantum program that allocates one qubit (qubit #0) and does nothing to it
p = Program(I(0))
# Quantum states are called wavefunctions for historical reasons.
# We can run this basic program on our connection to the simulator.
# This call will return the state of our qubits after we run program p.
# This api call returns a tuple, but we'll ignore the second value for now.
wavefunction = wavefunction_simulator.wavefunction(p)
# wavefunction is a Wavefunction object that stores a quantum state as a list of amplitudes
alpha, beta = wavefunction
print("Our qubit is in the state alpha={} and beta={}".format(alpha, beta))
print("The probability of measuring the qubit in outcome 0 is {}".format(abs(alpha)**2))
print("The probability of measuring the qubit in outcome 1 is {}".format(abs(beta)**2))
Our qubit is in the state alpha=(1+0j) and beta=0j
The probability of measuring the qubit in outcome 0 is 1.0
The probability of measuring the qubit in outcome 1 is 0.0
Applying an operation to our qubit affects the probability of each outcome.
# We can import the qubit "flip" operation, called X, and see what it does.
# We will learn more about this operation in the next section.
from pyquil.gates import X
p = Program(X(0))
wavefunc = wavefunction_simulator.wavefunction(p)
alpha, beta = wavefunc
print("Our qubit is in the state alpha={} and beta={}".format(alpha, beta))
print("The probability of measuring the qubit in outcome 0 is {}".format(abs(alpha)**2))
print("The probability of measuring the qubit in outcome 1 is {}".format(abs(beta)**2))
Our qubit is in the state alpha=0j and beta=(1+0j)
The probability of measuring the qubit in outcome 0 is 0.0
The probability of measuring the qubit in outcome 1 is 1.0
In this case we have flipped the probability of outcome 0 into the probability of outcome 1 for our qubit. We can also investigate what happens to the state of multiple qubits. Weâ€™d expect the state of multiple qubits to grow exponentially in size, as their vectors are tensored together.
# Multiple qubits also produce the expected scaling of the state.
p = Program(I(0), I(1))
wavefunction = wavefunction_simulator.wavefunction(p)
print("The quantum state is of dimension:", len(wavefunction.amplitudes))
p = Program(I(0), I(1), I(2), I(3))
wavefunction = wavefunction_simulator.wavefunction(p)
print("The quantum state is of dimension:", len(wavefunction.amplitudes))
p = Program()
for x in range(10):
p += I(x)
wavefunction = wavefunction_simulator.wavefunction(p)
print("The quantum state is of dimension:", len(wavefunction.amplitudes))
The quantum state is of dimension: 4
The quantum state is of dimension: 16
The quantum state is of dimension: 1024
Letâ€™s look at the actual value for the state of two qubits combined. The resulting dictionary of this method contains outcomes as keys and the probabilities of those outcomes as values.
# wavefunction(Program) returns a coefficient array that corresponds to outcomes in the following order
wavefunction = wavefunction_simulator.wavefunction(Program(I(0), I(1)))
print(wavefunction.get_outcome_probs())
{'00': 1.0, '01': 0.0, '10': 0.0, '11': 0.0}
Qubit OperationsÂ¶
In the previous section we introduced our first two operations: the I
(or Identity) operation and the X
(or NOT) operation. In this section we will get into some
more details on what these operations are.
Quantum states are complex vectors on the Bloch sphere, and quantum operations are matrices with two properties:
They are reversible.
When applied to a state vector on the Bloch sphere, the resulting vector is also on the Bloch sphere.
Matrices that satisfy these two properties are called unitary matrices. Such matrices have the characteristic property that their complex conjugate transpose is equal to their inverse, a property directly linked to the requirement that the probabilities of measuring qubits in any of the allowed states must sum to 1. Applying an operation to a quantum state is the same as multiplying a vector by one of these matrices. Such an operation is called a gate.
Since individual qubits are twodimensional vectors, operations on individual qubits are 2x2 matrices. The identity matrix leaves the state vector unchanged:
so the program that applies this operation to the zero state is just
p = Program(I(0))
print(wavefunction_simulator.wavefunction(p))
(1+0j)0>
Pauli OperatorsÂ¶
Letâ€™s revisit the X
gate introduced above. It is one of three important singlequbit gates,
called the Pauli operators:
from pyquil.gates import X, Y, Z
p = Program(X(0))
wavefunction = wavefunction_simulator.wavefunction(p)
print("X0> = ", wavefunction)
print("The outcome probabilities are", wavefunction.get_outcome_probs())
print("This looks like a bit flip.\n")
p = Program(Y(0))
wavefunction = wavefunction_simulator.wavefunction(p)
print("Y0> = ", wavefunction)
print("The outcome probabilities are", wavefunction.get_outcome_probs())
print("This also looks like a bit flip.\n")
p = Program(Z(0))
wavefunction = wavefunction_simulator.wavefunction(p)
print("Z0> = ", wavefunction)
print("The outcome probabilities are", wavefunction.get_outcome_probs())
print("This state looks unchanged.")
X0> = (1+0j)1>
The outcome probabilities are {'0': 0.0, '1': 1.0}
This looks like a bit flip.
Y0> = 1j1>
The outcome probabilities are {'0': 0.0, '1': 1.0}
This also looks like a bit flip.
Z0> = (1+0j)0>
The outcome probabilities are {'0': 1.0, '1': 0.0}
This state looks unchanged.
The Pauli matrices have a visual interpretation: they perform 180degree rotations of
qubit state vectors on the Bloch sphere. They operate about their respective axes
as shown in the Bloch sphere depicted above. For example, the X
gate performs a 180degree
rotation about the \(x\) axis. This explains the results of our code above: for a state vector
initially in the \(+z\) direction, both X
and Y
gates will rotate it to \(z\),
and the Z
gate will leave it unchanged.
However, notice that while the X
and Y
gates produce the same outcome probabilities, they
actually produce different states. These states are not distinguished if they are measured
immediately, but they produce different results in larger programs.
Quantum programs are built by applying successive gate operations:
# Composing qubit operations is the same as multiplying matrices sequentially
p = Program(X(0), Y(0), Z(0))
wavefunction = wavefunction_simulator.wavefunction(p)
print("ZYX0> = ", wavefunction)
print("With outcome probabilities\n", wavefunction.get_outcome_probs())
ZYX0> = [ 0.1.j 0.+0.j]
With outcome probabilities
{'0': 1.0, '1': 0.0}
MultiQubit OperationsÂ¶
Operations can also be applied to composite states of multiple qubits.
One common example is the controlledNOT or CNOT
gate that works on two
qubits. Its matrix form is:
Letâ€™s take a look at how we could use a CNOT
gate in pyQuil.
from pyquil.gates import CNOT
p = Program(CNOT(0, 1))
wavefunction = wavefunction_simulator.wavefunction(p)
print("CNOT00> = ", wavefunction)
print("With outcome probabilities\n", wavefunction.get_outcome_probs(), "\n")
p = Program(X(0), CNOT(0, 1))
wavefunction = wavefunction_simulator.wavefunction(p)
print("CNOT01> = ", wavefunction)
print("With outcome probabilities\n", wavefunction.get_outcome_probs(), "\n")
p = Program(X(1), CNOT(0, 1))
wavefunction = wavefunction_simulator.wavefunction(p)
print("CNOT10> = ", wavefunction)
print("With outcome probabilities\n", wavefunction.get_outcome_probs(), "\n")
p = Program(X(0), X(1), CNOT(0, 1))
wavefunction = wavefunction_simulator.wavefunction(p)
print("CNOT11> = ", wavefunction)
print("With outcome probabilities\n", wavefunction.get_outcome_probs(), "\n")
CNOT00> = (1+0j)00>
With outcome probabilities
{'00': 1.0, '01': 0.0, '10': 0.0, '11': 0.0}
CNOT01> = (1+0j)11>
With outcome probabilities
{'00': 0.0, '01': 0.0, '10': 0.0, '11': 1.0}
CNOT10> = (1+0j)10>
With outcome probabilities
{'00': 0.0, '01': 0.0, '10': 1.0, '11': 0.0}
CNOT11> = (1+0j)01>
With outcome probabilities
{'00': 0.0, '01': 1.0, '10': 0.0, '11': 0.0}
The CNOT
gate does what its name implies: the state of the second qubit is flipped
(negated) if and only if the state of the first qubit is 1 (true).
Another twoqubit gate example is the SWAP
gate, which swaps the \(01\rangle\)
and \(10\rangle\) states:
from pyquil.gates import SWAP
p = Program(X(0), SWAP(0,1))
wavefunction = wavefunction_simulator.wavefunction(p)
print("SWAP01> = ", wavefunction)
print("With outcome probabilities\n", wavefunction.get_outcome_probs())
SWAP01> = (1+0j)10>
With outcome probabilities
{'00': 0.0, '01': 0.0, '10': 1.0, '11': 0.0}
In summary, quantum computing operations are composed of a series of complex matrices applied to complex vectors. These matrices must be unitary (meaning that their complex conjugate transpose is equal to their inverse) because the overall probability of all outcomes must always sum to one.
The Quantum Abstract MachineÂ¶
We now have enough background to introduce the programming model that underlies Quil. This is a hybrid quantumclassical model in which \(N\) qubits interact with \(M\) classical bits:
These qubits and classical bits come with a defined gate set, e.g. which gate operations can be applied to which qubits. Different kinds of quantum computing hardware place different limitations on what gates can be applied, and the fixed gate set represents these limitations.
Full details on the Quantum Abstract Machine and Quil can be found in the Quil whitepaper.
The next section on measurements will describe the interaction between the classical and quantum parts of a Quantum Abstract Machine (QAM).
Qubit MeasurementsÂ¶
Measurements have two effects:
They project the state vector onto one of the basic outcomes
(optional) They store the outcome of the measurement in a classical bit.
Hereâ€™s a simple example:
# Create a program that stores the outcome of measuring qubit #0 into classical register [0]
p = Program()
classical_register = p.declare('ro', 'BIT', 1)
p += Program(I(0)).measure(0, classical_register[0])
Up until this point we have used the quantum simulator to cheat a little bit â€” we have
actually looked at the wavefunction that comes back. However, on real
quantum hardware, we are unable to directly look at the wavefunction.
Instead we only have access to the classical bits that are affected by
measurements. This functionality is emulated by QuantumComputer.run()
. Note that the run
command is to be applied on the compiled version of the program.
from pyquil import get_qc
qc = get_qc('9qsquareqvm')
print (qc.run(qc.compile(p)))
[[0]]
We see that the classical register reports a value of zero. However, if we had flipped the qubit before measurement then we obtain:
p = Program()
classical_register = p.declare('ro', 'BIT', 1)
p += Program(X(0)) # Flip the qubit
p.measure(0, classical_register[0]) # Measure the qubit
print (qc.run(qc.compile(p)))
[[1]]
These measurements are deterministic, e.g. if we make them multiple times then we always get the same outcome:
p = Program()
classical_register = p.declare('ro', 'BIT', 1)
p += Program(X(0)) # Flip the qubit
p.measure(0, classical_register[0]) # Measure the qubit
trials = 10
p.wrap_in_numshots_loop(shots=trials)
print (qc.run(qc.compile(p)))
[[1], [1], [1], [1], [1], [1], [1], [1], [1], [1]]
Classical/Quantum InteractionÂ¶
However this is not the case in general â€” measurements can affect the quantum state as well. In fact, measurements act like projections onto the outcome basis states. To show how this works, we first introduce a new singlequbit gate, the Hadamard gate. The matrix form of the Hadamard gate is:
The following pyQuil code shows how we can use the Hadamard gate:
from pyquil.gates import H
# The Hadamard produces what is called a superposition state
coin_program = Program(H(0))
wavefunction = wavefunction_simulator.wavefunction(coin_program)
print("H0> = ", wavefunction)
print("With outcome probabilities\n", wavefunction.get_outcome_probs())
H0> = (0.7071067812+0j)0> + (0.7071067812+0j)1>
With outcome probabilities
{'0': 0.49999999999999989, '1': 0.49999999999999989}
A qubit in this state will be measured half of the time in the \(0\rangle\) state, and half of the time in the \(1\rangle\) state. In a sense, this qubit truly is a random variable representing a coin. In fact, there are many wavefunctions that will give this same operational outcome. There is a continuous family of states of the form
that represent the outcomes of an unbiased coin. Being able to work with all of these different new states is part of what gives quantum computing extra power over regular bits.
p = Program()
ro = p.declare('ro', 'BIT', 1)
p += Program(H(0)).measure(0, ro[0])
# Measure qubit #0 a number of times
p.wrap_in_numshots_loop(shots=10)
# We see probabilistic results of about half 1's and half 0's
print (qc.run(qc.compile(p)))
[[0], [1], [1], [0], [1], [0], [0], [1], [0], [0]]
pyQuil allows us to look at the wavefunction after a measurement as well:
coin_program = Program(H(0))
print ("Before measurement: H0> = ", wavefunction_simulator.wavefunction(coin_program), "\n")
ro = coin_program.declare('ro', 'BIT', 1)
coin_program.measure(0, ro[0])
for _ in range(5):
print ("After measurement: ", wavefunction_simulator.wavefunction(coin_program))
Before measurement: H0> = (0.7071067812+0j)0> + (0.7071067812+0j)1>
After measurement: (1+0j)1>
After measurement: (1+0j)1>
After measurement: (1+0j)1>
After measurement: (1+0j)1>
After measurement: (1+0j)1>
We can clearly see that measurement has an effect on the quantum state independent of what is stored classically. We begin in a state that has a 5050 probability of being \(0\rangle\) or \(1\rangle\). After measurement, the state changes into being entirely in \(0\rangle\) or entirely in \(1\rangle\) according to which outcome was obtained. This is the phenomenon referred to as the collapse of the wavefunction. Mathematically, the wavefunction is being projected onto the vector of the obtained outcome and subsequently rescaled to unit norm.
# This happens with bigger systems too, as can be seen with this program,
# which prepares something called a Bell state (a special kind of "entangled state")
bell_program = Program(H(0), CNOT(0, 1))
wavefunction = wavefunction_simulator.wavefunction(bell_program)
print("Before measurement: Bell state = ", wavefunction, "\n")
classical_regs = bell_program.declare('ro', 'BIT', 2)
bell_program.measure(0, classical_regs[0]).measure(1, classical_regs[1])
for _ in range(5):
wavefunction = wavefunction_simulator.wavefunction(bell_program)
print("After measurement: ", wavefunction.get_outcome_probs())
Before measurement: Bell state = (0.7071067812+0j)00> + (0.7071067812+0j)11>
After measurement: {'00': 0.0, '01': 0.0, '10': 0.0, '11': 1.0}
After measurement: {'00': 0.0, '01': 0.0, '10': 0.0, '11': 1.0}
After measurement: {'00': 0.0, '01': 0.0, '10': 0.0, '11': 1.0}
After measurement: {'00': 0.0, '01': 0.0, '10': 0.0, '11': 1.0}
After measurement: {'00': 0.0, '01': 0.0, '10': 0.0, '11': 1.0}
The above program prepares entanglement because, even though there are random outcomes, after every measurement both qubits are in the same state. They are either both \(0\rangle\) or both \(1\rangle\). This special kind of correlation is part of what makes quantum mechanics so unique and powerful.
Classical ControlÂ¶
There are also ways of introducing classical control of quantum programs. For example, we can use the state of classical bits to determine what quantum operations to run.
true_branch = Program(X(7)) # if branch
false_branch = Program(I(7)) # else branch
# Branch on ro[1]
p = Program()
ro = p.declare('ro', 'BIT', 8)
p += Program(X(0)).measure(0, ro[1]).if_then(ro[1], true_branch, false_branch)
# Measure qubit #7 into ro[7]
p.measure(7, ro[7])
# Run and check register [7]
print (qc.run(qc.compile(p)))
[[1 1]]
The second [1] here means that qubit 7 was indeed flipped.
Example: The Probabilistic Halting ProblemÂ¶
A fun example is to create a program that has an exponentially increasing chance of halting, but that may run forever!
p = Program()
ro = p.declare('ro', 'BIT', 1)
inside_loop = Program(H(0)).measure(0, ro[0])
p.inst(X(0)).while_do(ro[0], inside_loop)
qc = get_qc('9qsquareqvm')
print (qc.run(qc.compile(p)))
[[0]]
Next StepsÂ¶
We hope that you have enjoyed your whirlwind tour of quantum computing. You are now ready to check out the Installation and Getting Started guide!
If you would like to learn more, Nielsen and Chuangâ€™s Quantum Computation and Quantum Information is a particularly excellent resource for newcomers to the field.
If youâ€™re interested in learning about the software behind quantum computing, take a look at our blog posts on The Quantum Software Challenge.
QuilTÂ¶
QuilT is an extension to Quil which introduces pulselevel control to quantum programs. With QuilT one can describe a program at a level lower than is typically permitted in circuittype programs, with explicit control over the RF waveforms played by the QPUâ€™s control hardware. In particular this imbues programs with a notion of time, hence the T suffix.
The Quil compiler quilc was developed to support most users in their pursuit for producing an optimal program from a highlevel language. In contrast QuilT was developed to enable the lowlevel and precise control desired by powerusers. For example, for many users the implementation details of a Hadamard gate are not particularly important, and indeed the behindthescenes realisation of a Hadamard gate are likely to change over time as gate implementations are recalibrated to provide the best results. If you instead you are interested in those details, and in particular you want to control those details, then pulselevel control with QuilT is the way to go. With QuilT you can define precisely what you mean by H 0, you can perform experiments to characterize the underlying hardware such as determining T1. The hardware is almost at your fingertips.
Note
QuilT is not yet finalized and subject to change. The following link should be updated upon the release of QuilT.
For examples, see the adjacent notebooks. For more information, see the QuilT proposal at the Quil project homepage.
Getting Up and Running with QuilTÂ¶
Language DocumentationÂ¶
See https://github.com/rigetti/quil/tree/master/rfcs/analog (in particular, proposal.md
and spec_changes.md
) for documentation on the QuilT language.
Construct a QuantumComputer
object linked to the QuilT compilerÂ¶
[1]:
from pyquil import Program, get_qc
qc = get_qc("Aspen8")
As a sanity check, the following call should work.
[2]:
qc.compiler.get_version_info()
[2]:
{'quilc': {'quilc': '1.22.0', 'githash': '3b3160c'}, 'qpu_compiler': {}}
Get QuilT CalibrationsÂ¶
A production QPU has a set of calibrations associated with it. These include frame definitions, gate and measurement calibrations, and custom waveforms. Below we show how to get the default calibrations.
[3]:
cals = qc.compiler.calibration_program
The calibration_program
property of QPUCompiler
provides cached access to the QPU calibration information. Upon first using this property a request will be made for the calibration information and may take some time to complete. Subsequent usage of this property will use the cached calibrations and thus will be instantaneous. It should be noted therefore that calibrations will vary with time and should be regularly refreshed though the specifics of when to refresh the
calibrations is left as an exercise for the user. See QPUCompiler#refresh_calibration_program
.
Frame DefinitionsÂ¶
Frame definitions correspond to specific hardware channels. These have a name (e.g. 0 "ro_rx"
for the hardware readout receive channel on Qubit 0), and some metadata (DAC sample rate, initial frame frequency, and a direction).
Note: These are fixed and should not be edited. If you wish to set a frameâ€™s frequency to one different from its initial frequency, your QuilT program should use SETFREQUENCY
(for an absolute value) or SHIFTFREQUENCY
(for a relative shift).
[4]:
from pyquil.quilatom import Frame
# Look for CZ frames.
cz_frames = filter(lambda f: f[0].name == "cz", cals.frames.items())
# The first elt is the frame (of type Frame) and the second elt is
# the frame definition (of type DefFrame).
print(next(cz_frames)[1])
DEFFRAME 0 7 "cz":
DIRECTION: "tx"
INITIALFREQUENCY: 298517072.1886101
CENTERFREQUENCY: 375000000.0
HARDWAREOBJECT: "q0_ff"
SAMPLERATE: 1000000000.0
Gate CalibrationsÂ¶
Gate and Measurement calibrations present the current QuilT specification of Rigettiâ€™s native gates.
[5]:
print(len(cals.calibrations), "total calibrations, peeking at first two:\n")
for defn in cals.calibrations[:2]:
print(defn)
396 total calibrations, peeking at first two:
DEFCAL RX(pi/2) 0:
FENCE 0
NONBLOCKING PULSE 0 "rf" drag_gaussian(duration: 1.2e07, fwhm: 3e08, t0: 6e08, anh: 190000000.0, alpha: 0.5128791143256078, scale: 0.31430257697073, phase: 0.0, detuning: 0.0)
FENCE 0
DEFCAL RX(pi/2) 0:
FENCE 0
NONBLOCKING PULSE 0 "rf" drag_gaussian(duration: 1.2e07, fwhm: 3e08, t0: 6e08, anh: 190000000.0, alpha: 0.5128791143256078, scale: 0.31430257697073, phase: 0.0, detuning: 0.0)
FENCE 0
Waveform DefinitionsÂ¶
Certain gates (e.g. RX
gates above) use template waveforms. Others, notably CZ
gates, use custom waveforms. The waveforms
member maps waveform names to their definitions.
[6]:
cals.waveforms
[6]:
{'q0_q7_cz/CZ': <pyquil.quilbase.DefWaveform at 0x7f1d60341350>,
'q0_q7_cphase/sqrtCPHASE': <pyquil.quilbase.DefWaveform at 0x7f1d60341590>,
'q0_q7_xy/sqrtiSWAP': <pyquil.quilbase.DefWaveform at 0x7f1d60341610>,
'q1_q16_cz/CZ': <pyquil.quilbase.DefWaveform at 0x7f1d603415d0>,
'q1_q16_cphase/sqrtCPHASE': <pyquil.quilbase.DefWaveform at 0x7f1d60341690>,
'q1_q16_xy/sqrtiSWAP': <pyquil.quilbase.DefWaveform at 0x7f1d60341650>,
'q1_q2_cz/CZ': <pyquil.quilbase.DefWaveform at 0x7f1d60341710>,
'q1_q2_cphase/sqrtCPHASE': <pyquil.quilbase.DefWaveform at 0x7f1d603417d0>,
'q1_q2_xy/sqrtiSWAP': <pyquil.quilbase.DefWaveform at 0x7f1d60341790>,
'q10_q11_cz/CZ': <pyquil.quilbase.DefWaveform at 0x7f1d60341850>,
'q10_q11_cphase/sqrtCPHASE': <pyquil.quilbase.DefWaveform at 0x7f1d60341890>,
'q10_q11_xy/sqrtiSWAP': <pyquil.quilbase.DefWaveform at 0x7f1d60341810>,
'q10_q17_cz/CZ': <pyquil.quilbase.DefWaveform at 0x7f1d603418d0>,
'q10_q17_cphase/sqrtCPHASE': <pyquil.quilbase.DefWaveform at 0x7f1d60341990>,
'q10_q17_xy/sqrtiSWAP': <pyquil.quilbase.DefWaveform at 0x7f1d60341a10>,
'q11_q12_cz/CZ': <pyquil.quilbase.DefWaveform at 0x7f1d603419d0>,
'q11_q12_cphase/sqrtCPHASE': <pyquil.quilbase.DefWaveform at 0x7f1d60341a90>,
'q11_q12_xy/sqrtiSWAP': <pyquil.quilbase.DefWaveform at 0x7f1d60341b10>,
'q11_q26_cz/CZ': <pyquil.quilbase.DefWaveform at 0x7f1d60341ad0>,
'q11_q26_cphase/sqrtCPHASE': <pyquil.quilbase.DefWaveform at 0x7f1d60341b90>,
'q11_q26_xy/sqrtiSWAP': <pyquil.quilbase.DefWaveform at 0x7f1d60341c10>,
'q12_q13_cz/CZ': <pyquil.quilbase.DefWaveform at 0x7f1d60341c50>,
'q12_q13_cphase/sqrtCPHASE': <pyquil.quilbase.DefWaveform at 0x7f1d60341c90>,
'q12_q13_xy/sqrtiSWAP': <pyquil.quilbase.DefWaveform at 0x7f1d60341bd0>,
'q12_q25_cz/CZ': <pyquil.quilbase.DefWaveform at 0x7f1d60341cd0>,
'q12_q25_cphase/sqrtCPHASE': <pyquil.quilbase.DefWaveform at 0x7f1d60341d90>,
'q12_q25_xy/sqrtiSWAP': <pyquil.quilbase.DefWaveform at 0x7f1d60341e10>,
'q15_q16_cz/CZ': <pyquil.quilbase.DefWaveform at 0x7f1d60341dd0>,
'q15_q16_cphase/sqrtCPHASE': <pyquil.quilbase.DefWaveform at 0x7f1d60341e90>,
'q15_q16_xy/sqrtiSWAP': <pyquil.quilbase.DefWaveform at 0x7f1d60341e50>,
'q16_q17_cz/CZ': <pyquil.quilbase.DefWaveform at 0x7f1d60341ed0>,
'q16_q17_cphase/sqrtCPHASE': <pyquil.quilbase.DefWaveform at 0x7f1d60341f90>,
'q16_q17_xy/sqrtiSWAP': <pyquil.quilbase.DefWaveform at 0x7f1d60341f50>,
'q2_q15_cz/CZ': <pyquil.quilbase.DefWaveform at 0x7f1d60341f10>,
'q2_q15_cphase/sqrtCPHASE': <pyquil.quilbase.DefWaveform at 0x7f1d603720d0>,
'q2_q15_xy/sqrtiSWAP': <pyquil.quilbase.DefWaveform at 0x7f1d60372150>,
'q2_q3_cz/CZ': <pyquil.quilbase.DefWaveform at 0x7f1d60372110>,
'q2_q3_cphase/sqrtCPHASE': <pyquil.quilbase.DefWaveform at 0x7f1d603721d0>,
'q2_q3_xy/sqrtiSWAP': <pyquil.quilbase.DefWaveform at 0x7f1d60372190>,
'q20_q21_cz/CZ': <pyquil.quilbase.DefWaveform at 0x7f1d60372210>,
'q20_q21_cphase/sqrtCPHASE': <pyquil.quilbase.DefWaveform at 0x7f1d603722d0>,
'q20_q21_xy/sqrtiSWAP': <pyquil.quilbase.DefWaveform at 0x7f1d60372350>,
'q20_q27_cz/CZ': <pyquil.quilbase.DefWaveform at 0x7f1d60372310>,
'q20_q27_cphase/sqrtCPHASE': <pyquil.quilbase.DefWaveform at 0x7f1d603723d0>,
'q20_q27_xy/sqrtiSWAP': <pyquil.quilbase.DefWaveform at 0x7f1d60372450>,
'q21_q22_cz/CZ': <pyquil.quilbase.DefWaveform at 0x7f1d60372410>,
'q21_q22_cphase/sqrtCPHASE': <pyquil.quilbase.DefWaveform at 0x7f1d603724d0>,
'q21_q22_xy/sqrtiSWAP': <pyquil.quilbase.DefWaveform at 0x7f1d60372490>,
'q21_q36_cz/CZ': <pyquil.quilbase.DefWaveform at 0x7f1d60372510>,
'q21_q36_cphase/sqrtCPHASE': <pyquil.quilbase.DefWaveform at 0x7f1d603725d0>,
'q21_q36_xy/sqrtiSWAP': <pyquil.quilbase.DefWaveform at 0x7f1d60372590>,
'q22_q23_cz/CZ': <pyquil.quilbase.DefWaveform at 0x7f1d60372610>,
'q22_q23_cphase/sqrtCPHASE': <pyquil.quilbase.DefWaveform at 0x7f1d603726d0>,
'q22_q23_xy/sqrtiSWAP': <pyquil.quilbase.DefWaveform at 0x7f1d60372750>,
'q22_q35_cz/CZ': <pyquil.quilbase.DefWaveform at 0x7f1d60372710>,
'q22_q35_cphase/sqrtCPHASE': <pyquil.quilbase.DefWaveform at 0x7f1d603727d0>,
'q22_q35_xy/sqrtiSWAP': <pyquil.quilbase.DefWaveform at 0x7f1d60372850>,
'q23_q24_cz/CZ': <pyquil.quilbase.DefWaveform at 0x7f1d60372810>,
'q23_q24_cphase/sqrtCPHASE': <pyquil.quilbase.DefWaveform at 0x7f1d603728d0>,
'q23_q24_xy/sqrtiSWAP': <pyquil.quilbase.DefWaveform at 0x7f1d60372950>,
'q24_q25_cz/CZ': <pyquil.quilbase.DefWaveform at 0x7f1d60372910>,
'q24_q25_cphase/sqrtCPHASE': <pyquil.quilbase.DefWaveform at 0x7f1d603729d0>,
'q24_q25_xy/sqrtiSWAP': <pyquil.quilbase.DefWaveform at 0x7f1d60372990>,
'q25_q26_cz/CZ': <pyquil.quilbase.DefWaveform at 0x7f1d60372a10>,
'q25_q26_cphase/sqrtCPHASE': <pyquil.quilbase.DefWaveform at 0x7f1d60372ad0>,
'q25_q26_xy/sqrtiSWAP': <pyquil.quilbase.DefWaveform at 0x7f1d60372a90>,
'q26_q27_cz/CZ': <pyquil.quilbase.DefWaveform at 0x7f1d60372b10>,
'q26_q27_cphase/sqrtCPHASE': <pyquil.quilbase.DefWaveform at 0x7f1d60372bd0>,
'q26_q27_xy/sqrtiSWAP': <pyquil.quilbase.DefWaveform at 0x7f1d60372b90>,
'q3_q4_cz/CZ': <pyquil.quilbase.DefWaveform at 0x7f1d60372c10>,
'q3_q4_cphase/sqrtCPHASE': <pyquil.quilbase.DefWaveform at 0x7f1d60372cd0>,
'q3_q4_xy/sqrtiSWAP': <pyquil.quilbase.DefWaveform at 0x7f1d60372d50>,
'q30_q31_cz/CZ': <pyquil.quilbase.DefWaveform at 0x7f1d60372d10>,
'q30_q31_cphase/sqrtCPHASE': <pyquil.quilbase.DefWaveform at 0x7f1d60372dd0>,
'q30_q31_xy/sqrtiSWAP': <pyquil.quilbase.DefWaveform at 0x7f1d60372e50>,
'q30_q37_cz/CZ': <pyquil.quilbase.DefWaveform at 0x7f1d60372e10>,
'q30_q37_cphase/sqrtCPHASE': <pyquil.quilbase.DefWaveform at 0x7f1d60372ed0>,
'q30_q37_xy/sqrtiSWAP': <pyquil.quilbase.DefWaveform at 0x7f1d60372e90>,
'q31_q32_cz/CZ': <pyquil.quilbase.DefWaveform at 0x7f1d60372f10>,
'q31_q32_cphase/sqrtCPHASE': <pyquil.quilbase.DefWaveform at 0x7f1d60372fd0>,
'q31_q32_xy/sqrtiSWAP': <pyquil.quilbase.DefWaveform at 0x7f1d60372f90>,
'q32_q33_cz/CZ': <pyquil.quilbase.DefWaveform at 0x7f1d6032b050>,
'q32_q33_cphase/sqrtCPHASE': <pyquil.quilbase.DefWaveform at 0x7f1d6032b110>,
'q32_q33_xy/sqrtiSWAP': <pyquil.quilbase.DefWaveform at 0x7f1d6032b190>,
'q33_q34_cz/CZ': <pyquil.quilbase.DefWaveform at 0x7f1d6032b150>,
'q33_q34_cphase/sqrtCPHASE': <pyquil.quilbase.DefWaveform at 0x7f1d6032b210>,
'q33_q34_xy/sqrtiSWAP': <pyquil.quilbase.DefWaveform at 0x7f1d6032b1d0>,
'q34_q35_cz/CZ': <pyquil.quilbase.DefWaveform at 0x7f1d6032b250>,
'q34_q35_cphase/sqrtCPHASE': <pyquil.quilbase.DefWaveform at 0x7f1d6032b310>,
'q34_q35_xy/sqrtiSWAP': <pyquil.quilbase.DefWaveform at 0x7f1d6032b2d0>,
'q35_q36_cz/CZ': <pyquil.quilbase.DefWaveform at 0x7f1d6032b350>,
'q35_q36_cphase/sqrtCPHASE': <pyquil.quilbase.DefWaveform at 0x7f1d6032b410>,
'q35_q36_xy/sqrtiSWAP': <pyquil.quilbase.DefWaveform at 0x7f1d6032b490>,
'q36_q37_cz/CZ': <pyquil.quilbase.DefWaveform at 0x7f1d6032b450>,
'q36_q37_cphase/sqrtCPHASE': <pyquil.quilbase.DefWaveform at 0x7f1d6032b510>,
'q36_q37_xy/sqrtiSWAP': <pyquil.quilbase.DefWaveform at 0x7f1d6032b4d0>,
'q4_q5_cz/CZ': <pyquil.quilbase.DefWaveform at 0x7f1d6032b550>,
'q4_q5_cphase/sqrtCPHASE': <pyquil.quilbase.DefWaveform at 0x7f1d6032b610>,
'q4_q5_xy/sqrtiSWAP': <pyquil.quilbase.DefWaveform at 0x7f1d6032b5d0>,
'q5_q6_cz/CZ': <pyquil.quilbase.DefWaveform at 0x7f1d6032b650>,
'q5_q6_cphase/sqrtCPHASE': <pyquil.quilbase.DefWaveform at 0x7f1d6032b710>,
'q5_q6_xy/sqrtiSWAP': <pyquil.quilbase.DefWaveform at 0x7f1d6032b6d0>,
'q6_q7_cz/CZ': <pyquil.quilbase.DefWaveform at 0x7f1d6032b750>,
'q6_q7_cphase/sqrtCPHASE': <pyquil.quilbase.DefWaveform at 0x7f1d6032b810>,
'q6_q7_xy/sqrtiSWAP': <pyquil.quilbase.DefWaveform at 0x7f1d6032b7d0>}
Here is what one of these definitions looks like.
[7]:
print(next(iter(cals.waveforms.values()), None))
DEFWAVEFORM q0_q7_cz/CZ:
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.00012178364559662996, 0.0017544499272112704, 0.013457291321491907, 0.0568509693447061, 0.1403395097840485, 0.22382805022339092, 0.26722172824660506, 0.27892456964088574, 0.28055723592250037, 0.2806746072122791, 0.2806789371383469, 0.28067901878029905, 0.28067901956426555, 0.28067901956808755, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.280679019568097, 0.28067901956808755, 0.28067901956426555, 0.28067901878029905, 0.2806789371383469, 0.2806746072122791, 0.28055723592250037, 0.27892456964088574, 0.26722172824660495, 0.2238280502233906, 0.14033950978404605, 0.05685096934470464, 0.013457291321491488, 0.0017544499272112081, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
Compiling and running a QuilT ProgramÂ¶
There are three ways to access the compiler from PyQuil:
qc.compile
is the usual pipeline, and only works for Quil codeqc.compiler.quil_to_native_quil
is the entry point for compiling Quil to native Quilqc.compiler.native_quil_to_executable
is the entry point for compiling QuilT programs
In particular, the usual workflow of just delegating to qc.compile
does not currently work with QuilT. If you wish to use QuilT right now, your workflow should involve 1. calling qc.compiler.quil_to_native_quil
on code blocks which do not involve QuilT or QuilT calibrations 2. subsequently calling qc.compiler.native_quil_to_executable
on blocks involving QuilT
Compiling Quil ProgramsÂ¶
qc.compiler.native_quil_to_executable
requires native Quil + QuilT operations. In particular, it is assumed that the only gates used are those Rigetti native gates, or ones for which you have provided explicit calibrations. For example, the program below expresses
DECLARE ro BIT
H 0
CNOT 0 1
MEASURE 0 ro
using Rigetti native gates by first using qc.compiler.quil_to_native_quil
.
[8]:
prog = Program(
'DECLARE ro BIT',
'H 0',
'CNOT 0 1',
'MEASURE 0 ro'
)
compiled = qc.compiler.quil_to_native_quil(prog)
[9]:
exe = qc.compiler.native_quil_to_executable(compiled)
Note: The above compilation may be done even when not on an active QPU reservation. However,
as always, the executable cannot be run until on an active QPU reservation
the QPU settings used for compilation may go stale
Therefore, we suggest that although you may rely on qc.compiler.native_quil_to_executable
for development purposes (for example, to verify correct QuilT syntax), when executing on a QPU all QuilT programs should be compiled afresh.
Running the executable proceeds as before:
[10]:
qc.run(exe)
[10]:
array([[0]])
Another example: a simple T1 experimentÂ¶
As an example of mixing Quil with the new QuilT instructions, we consider a simple T1 experiment. In short, we
excite the qubit state
wait some amount of time
measure out
In this example, we do not do any further data analysis. The purpose here is simply to demonstrate how to compile and run a QuilT program.
[11]:
def t1_program(time, qubit, num_shots=1000):
prog = Program( "DECLARE ro BIT\n"
f"RX(pi) {qubit}\n"
f"FENCE 0\n"
f"DELAY {qubit} {time}\n"
f"MEASURE {qubit} ro")
prog.wrap_in_numshots_loop(num_shots)
return prog
[12]:
import numpy as np
probs = []
times = np.geomspace(20e9, 60e4, 20)
for time in times:
prog = t1_program(time, 0)
exe = qc.compiler.native_quil_to_executable(prog)
results = qc.run(exe)
prob = np.sum(results) / results.size
probs.append(prob)
print(f"time: {time:.2e} \tprob: {prob:.2}")
time: 2.00e08 prob: 0.97
time: 3.88e08 prob: 0.97
time: 7.54e08 prob: 0.97
time: 1.47e07 prob: 0.95
time: 2.85e07 prob: 0.92
time: 5.53e07 prob: 0.94
time: 1.07e06 prob: 0.9
time: 2.08e06 prob: 0.86
time: 4.05e06 prob: 0.77
time: 7.86e06 prob: 0.64
time: 1.53e05 prob: 0.45
time: 2.96e05 prob: 0.25
time: 5.76e05 prob: 0.17
time: 1.12e04 prob: 0.16
time: 2.17e04 prob: 0.17
time: 4.22e04 prob: 0.16
time: 8.19e04 prob: 0.17
time: 1.59e03 prob: 0.14
time: 3.09e03 prob: 0.16
time: 6.00e03 prob: 0.14
[13]:
%matplotlib inline
import matplotlib.pyplot as plt
plt.semilogx(times, probs, '')
plt.xlabel('time (s)')
plt.ylabel('p')
plt.show()
Using a Custom CalibrationÂ¶
All gate and measure applications in a QuilT program are translated according to either usersupplied or Rigetti calibrations. To easily check which calibration applies to a specific gate, use the Program.match_calibrations
method.
[14]:
from math import pi
from pyquil.gates import RX
print(cals.get_calibration(RX(pi, 0)))
DEFCAL RX(pi) 0:
FENCE 0
NONBLOCKING PULSE 0 "rf" drag_gaussian(duration: 1.2e07, fwhm: 3e08, t0: 6e08, anh: 190000000.0, alpha: 0.5128791143256078, scale: 0.63376686965814, phase: 0.0, detuning: 0.0)
FENCE 0
None of the above programs needed us to specify calibrations. By default, the Rigetti native calibrations are used. However, if you specify a calibration in a program, it will take precedence over the native calibrations.
[15]:
prog = t1_program(1e6, 0)
# Note: we did NOT specify a calibration for RX(pi) 0 in our previous program
assert prog.match_calibrations(RX(pi, 0)) is None
# The QuilT translator provided the default: namely, the same calibration we obtained for ourselves
# with `qc.compiler.get_calibration_program()`.
In the example below, we use a custom calibration, we conjugate the usual pulse with a frequency shift. Thereâ€™s no motivation for the particular value used, beyond simply showing what is possible.
[16]:
prog = Program("""DEFCAL RX(pi/2) 0:
FENCE 0
SETSCALE 0 "rf" 0.353088482172993
SHIFTFREQUENCY 0 "rf" 1e6
NONBLOCKING PULSE 0 "rf" drag_gaussian(duration: 6.000000000000001e08, fwhm: 1.5000000000000002e08, t0: 3.0000000000000004e08, anh: 210000000.0, alpha: 6.389096630631076)
SHIFTFREQUENCY 0 "rf" 1e6
FENCE 0
DECLARE ro BIT
RX(pi/2) 0
MEASURE 0 ro""")
print(prog)
DEFCAL RX(pi/2) 0:
FENCE 0
SETSCALE 0 "rf" 0.353088482172993
SHIFTFREQUENCY 0 "rf" 1000000.0
NONBLOCKING PULSE 0 "rf" drag_gaussian(duration: 6.000000000000001e08, fwhm: 1.5000000000000002e08, t0: 3.0000000000000004e08, anh: 210000000.0, alpha: 6.389096630631076)
SHIFTFREQUENCY 0 "rf" 1000000.0
FENCE 0
DECLARE ro BIT[1]
RX(pi/2) 0
MEASURE 0 ro[0]
[17]:
print(prog.get_calibration(RX(pi/2, 0)))
DEFCAL RX(pi/2) 0:
FENCE 0
SETSCALE 0 "rf" 0.353088482172993
SHIFTFREQUENCY 0 "rf" 1000000.0
NONBLOCKING PULSE 0 "rf" drag_gaussian(duration: 6.000000000000001e08, fwhm: 1.5000000000000002e08, t0: 3.0000000000000004e08, anh: 210000000.0, alpha: 6.389096630631076)
SHIFTFREQUENCY 0 "rf" 1000000.0
FENCE 0
[18]:
exe = qc.compiler.native_quil_to_executable(prog)
[19]:
qc.run(exe)
[19]:
array([[1]])
Of course, it is not required to use calibrations. One can construct an equivalent program by replacing the RX
gate with the body of the calibration:
[20]:
prog = Program("""
DECLARE ro BIT
FENCE 0
SETSCALE 0 "rf" 0.353088482172993
SHIFTFREQUENCY 0 "rf" 1e6
NONBLOCKING PULSE 0 "rf" drag_gaussian(duration: 6.000000000000001e08, fwhm: 1.5000000000000002e08, t0: 3.0000000000000004e08, anh: 210000000.0, alpha: 6.389096630631076)
SHIFTFREQUENCY 0 "rf" 1e6
FENCE 0
MEASURE 0 ro
""".strip())
[21]:
exe = qc.compiler.native_quil_to_executable(prog)
[22]:
qc.run(exe)
[22]:
array([[0]])
Parametric CalibrationsÂ¶
Some calibrations (e.g. for RX
) are defined for specific parameter values. Others may depend on general symbolic values, as with RZ
.
[23]:
from pyquil.gates import RZ
print(cals.get_calibration(RZ(pi,0)))
DEFCAL RZ(%theta) 0:
FENCE 0
SHIFTPHASE 0 "rf" 1.0*%theta
SHIFTPHASE 0 7 "xy" 0.5*%theta
FENCE 0
To get more information about how the matching calibration applies to a specific gate, use Program.match_calibrations
. The result is a CalibrationMatch
object which indicates not just the calibration, but the value for parameters.
[24]:
match = cals.match_calibrations(RZ(pi,0))
print(match.cal)
print(match.settings)
DEFCAL RZ(%theta) 0:
FENCE 0
SHIFTPHASE 0 "rf" 1.0*%theta
SHIFTPHASE 0 7 "xy" 0.5*%theta
FENCE 0
{Parameter('theta'): 3.141592653589793}
You may conveniently recover the body of the calibration, with the matched parameters substituted, using Program.calibrate
.
[25]:
instrs = cals.calibrate(RZ(pi,0))
for instr in instrs:
print(instr)
FENCE 0
SHIFTPHASE 0 "rf" 3.141592653589793
SHIFTPHASE 0 7 "xy" 1.5707963267948966
FENCE 0
NonNative GatesÂ¶
As mentioned above, the qc.compiler.native_quil_to_executable
call will provide calibrations for Rigetti native gates, if they are not provided by a user. However, a program with nonnative gates and no corresponding userprovided calibrations will result in a compilation failure.
[26]:
prog = Program("DECLARE ro BIT\n"
"H 0\n"
"CNOT 0 1\n"
"MEASURE 0 ro")
try:
qc.compiler.native_quil_to_executable(prog)
except Exception as e:
print("Fails on nonnative operations {H, CNOT} as expected.")
Fails on nonnative operations {H, CNOT} as expected.
[1]:
%matplotlib inline
%config InlineBackend.figure_formats = ['svg']
import numpy as np
import matplotlib.pyplot as plt
Pulses and WaveformsÂ¶
A waveform is simply a time varying signal. When a pulse operation occurs, a digital to analog converter produces a signal by combinding the following data:
a time varying complex function $ u(t) $ which describes the baseband waveform
a frequency \(f\), in Hertz,
a phase \(\theta\), in radians,
a unitless scale \(\alpha\).
The signal generated from this has the mathematical form
where \(\text{Re}\) denotes the real part of a complex number.
In accordance with the usual conventions, we will refer to the real part of \(u(t)\) as the inphase component, and the imaginary part as the quadrature component. A general complex number \(z = x + iy\) will sometimes be referred to as an IQvalue, with \(I = x\) and \(Q = y\).
QuilT provides the programmer with pulse and capturelevel control, both by allowing for custom baseband waveforms, as well as allowing for direct control over the runtime frequency, phase, and scale.
Waveform Templates and ReferencesÂ¶
There are two ways to specify a QuilT waveform:  by using a preexisting template, which has a shape dictated by certain parameters  by referencing a custom waveform definition.
Consider the following QuilT code:
PULSE 0 "rf" flat(duration: 1e6, iq: 0.5 + 0.5*i)
This denotes a pulse operation, on frame 0 "rf"
, with a total duration of one microsecond and with baseband waveform given by the flat
template, (corresponding to \(u(t) = 1/2 + i/2\) for the duration of the signal). The resulting signal produced depends also on the phase, frequency, and scaling factor associated with frame 0 "rf"
. These may be set explicitly, as in
SETSCALE 0 "rf" 1.0
SETFREQUENCY 0 "rf" 5e9
SETPHASE 0 "rf" 0.0
On the other hand, a custom waveform may be defined and used. This is done using the DEFWAVEFORM
form,
DEFWAVEFORM my_waveform:
0.01, 0.01+0.01*i, ...
the body of which consists of a list of complex numbers. Such a custom waveform may be referenced directly by name, as in
PULSE 0 "rf" my_waveform.
The precise meaning of this depends on the sample rate of the associated frame: this indicates the number of samples per second consumed by the underlying digitaltoanalog converter. For example, suppose that the definition of 0 "rf"
looked like this
DEFFRAME 0 "rf":
SAMPLERATE: 1000000000.0
INITIALFREQUENCY: 4807541957.13474
DIRECTION: "tx"
and my_waveform
has a definition consisting of complex numbers \(z_1, \ldots, z_N\). Then, letting \(r=10^9\) denote the sample rate, the resulting pulse has total duration \(\left \lceil{\frac{N}{r}}\right \rceil\), corresponding to the baseband waveform $ u(t) = z_{\left `:nbsphinxmath:lfloor{tr}`:nbsphinxmath:right `:nbsphinxmath:rfloor`}. $ Here $:nbsphinxmath:left `:nbsphinxmath:lceil{x}`:nbsphinxmath:right `:nbsphinxmath:rceil $ and
:math:left lfloor{x}right rfloor` denote the ceiling and floor of \(x\), respectively.
As before, this baseband waveform is combined with the frameâ€™s scale, frequency, and phase during digital to analog conversion.
A Catalog of Template WaveformsÂ¶
Rigetti provides a number of templates by default. These include
flat
, corresponding to simple rectangular waveformsgaussian
, for a Gaussian waveformdrag_gaussian
, for a Gaussian waveform modified by the Derivative Removal by Adiabatic Gate (DRAG) techniquehrm_gaussian
, for a DRAG Gaussian waveform with secondorder correctionserf_square
, for a flat waveform with smooth edges derived from the Gaussian error functionboxcar_kernel
, for a flat waveform which is normalized to integrate to 1
Each of these waveforms has a corresponding definition in pyquil.quiltwaveforms
. In addition to providing documentation on the meaning of each of the waveform parameters, this module also contains routines for generating samples for each template.
Below we look at each individidually, discussing the meaning of various parameters and plotting the real part of the waveform envelope.
[2]:
from pyquil.quilatom import TemplateWaveform
def plot_waveform(wf: TemplateWaveform, sample_rate: float):
""" Plot a template waveform by sampling at the specified sample rate. """
samples = wf.samples(sample_rate)
times = np.arange(len(samples))/sample_rate
print(wf)
plt.plot(times, samples.real)
plt.show()
flatÂ¶
A flat waveform is simple: it represents a constant signal for a certain duration. There are two required parameters:
duration
: the length of the waveform, in secondsiq
: a complex number
[3]:
from pyquil.quiltwaveforms import FlatWaveform
plot_waveform(FlatWaveform(duration=1e6, iq=1.0), sample_rate=1e9)
flat(duration: 1e06, iq: 1.0)
Scale, Phase, DetuningÂ¶
In addition to the parameters specific to each template waveform, there are also a few generic parameters. One of these we have met: each template waveform has a required duration
argument, indicating the length of the waveform in seconds.
The other arguments are optional, and are used to modulate the basic shape. These are
scale \(\alpha\), which has the effect of scaling the baseband \(u(t) \mapsto \alpha u(t)\)
phase \(\theta\), in radians, which has the effect of a phase shift on the baseband \(u(t) \mapsto e^{i \theta} u(t)\)
detuning \(f_d\), in Hertz, which has the effect of \(u(t) \mapsto e^{2 \pi i f_d} u(t)\)
These may be provided as arguments to any template waveform, e.g.
PULSE 0 "rf" flat(duration: 1e8, iq: 1.0, scale: 0.3, phase: 1.570796, detuning: 1e8)
Below we consider this by way of the PyQuil bindings.
[4]:
plot_waveform(FlatWaveform(duration=1e6, iq=1.0, detuning=1e7), sample_rate=1e9)
flat(duration: 1e06, iq: 1.0, detuning: 10000000.0)
gaussianÂ¶
Several of the template waveforms provided are derived from a standard (unnormalized) Gaussian. Here we have
where \(t_0\) denotes the center of the Gaussian, and \(\sigma\) is the usual standard deviation.
By QuilT convention, this is parameterized by the Gaussianâ€™s full width at half maximum (FWHM), which is defined to be
As with all QuilT waveforms, a QuilT gaussian
has a finite duration, and thus corresponds to a truncation of a true Gaussian.
In short, the parameters are:
duration
: the duration of the waveform, in seconds. The Gaussian will be truncated to \([0, \text{duration}]\)t0
: the center of the Gaussian, in secondsfwhm
: the full width half maximum of the Gaussian, in seconds
[5]:
from pyquil.quiltwaveforms import GaussianWaveform
plot_waveform(GaussianWaveform(duration=1e6, t0=5e7, fwhm=4e7), sample_rate=1e9)
gaussian(duration: 1e06, fwhm: 4e07, t0: 5e07)
drag_gaussianÂ¶
The drag_gaussian
waveform extends the basic Gaussian with an additional correction factor (cf. https://arxiv.org/abs/1809.04919 and references therein). The shape is given by
where \(\eta\) is the anharmonicity constant, in Hz, and \(\alpha\) is a dimensionless shape parameter.
As before, rather than providing \(\sigma\) explicity, drag_gaussian
takes a fullwidth half max parameter.
In summary, the required arguments to drag_gaussian
are:
duration
: the duration of the waveform, in seconds. The Gaussian will be truncated to \([0, \text{duration}]\)t0
: the center of the Gaussian, in secondsfwhm
: the full width half maximum of the Gaussian, in secondsanh
: the anharmonicity constant \(\eta\), in Hertzalpha
: dimensionless shape parameter \(\alpha\).
[6]:
from pyquil.quiltwaveforms import DragGaussianWaveform
plot_waveform(
DragGaussianWaveform(duration=1e6, t0=5e7, fwhm=4e7, anh=1.1, alpha=1.0),
sample_rate=1e9
)
drag_gaussian(duration: 1e06, fwhm: 4e07, t0: 5e07, anh: 1.1, alpha: 1.0)
Of course, we only plotted the real part of the waveform above. The imaginary part is relevant when the baseband waveform is converted to a passband waveform, as we can demonstrate below via the optional detuning
argument.
[7]:
from pyquil.quiltwaveforms import DragGaussianWaveform
plot_waveform(
DragGaussianWaveform(duration=1e6, t0=5e7, fwhm=4e7, anh=1.1, alpha=1.0, detuning=1e7),
sample_rate=1e9
)
drag_gaussian(duration: 1e06, fwhm: 4e07, t0: 5e07, anh: 1.1, alpha: 1.0, detuning: 10000000.0)
hrm_gaussianÂ¶
The hrm_gaussian
waveform is a variant of drag_gaussian
which incorporates a higher order term. The shape is given by
where \(\alpha\) is a dimensionless DRAG parameter, \(\eta\) is the anharmonicity constant, and \(H_2\) is a second order correction coefficient (cf. [1]). Note that when \(H_2\) equals 0 this reduces to an ordinary drag_gaussian
.
The required arguments to hrm_gaussian
are:
duration
: the duration of the waveform, in seconds. The Gaussian will be truncated to \([0, \text{duration}]\)t0
: the center of the Gaussian, in secondsfwhm
: the full width half maximum of the Gaussian, in secondsanh
: the anharmonicity constant \(\eta\), in Hertzalpha
: dimensionless shape parameter \(\alpha\)second_order_hrm_coeff
: the constant \(H_2\).
[1] Warren, W. S. (1984). Effects of arbitrary laser or NMR pulse shapes on population inversion and coherence. The Journal of Chemical Physics, 81(12), 5437â€“5448. doi:10.1063/1.447644
[8]:
from pyquil.quiltwaveforms import HrmGaussianWaveform
plot_waveform(
HrmGaussianWaveform(duration=1e6, t0=5e7, fwhm=4e7, anh=1.1, alpha=1.0, second_order_hrm_coeff=0.5),
sample_rate=1e9
)
hrm_gaussian(duration: 1e06, fwhm: 4e07, t0: 5e07, anh: 1.1, alpha: 1.0, second_order_hrm_coeff: 0.5)
erf_squareÂ¶
The erf_square
waveform is a variant of flat_waveform
with the boundary discontinuities smoothed via the error function (erf), and additional zeropadding.
The required arguments are:
duration
: the duration of the nonzero part of the waveform, in secondsrisetime
: width of each of the rise and fall sections of the pulse, in secondspad_left
: amount of zeropadding to add to the left of the pulse, in secondspad_right
: amount of zeropadding to add to the right of the pulse, in seconds
NOTE: The total duration of the waveform is duration + pad_left + pad_right
; the total duration of the support (nonzero entries) is duration
.
[9]:
from pyquil.quiltwaveforms import ErfSquareWaveform
plot_waveform(
ErfSquareWaveform(duration=1e6, risetime=1e7, pad_left=1e7, pad_right=1e7),
sample_rate=1e9
)
erf_square(duration: 1e06, risetime: 1e07, pad_left: 1e07, pad_right: 1e07)
Capture and KernelsÂ¶
In QuilT, waveforms are used in two places:
in a
PULSE
operation, to specify what signal is generatedin a
CAPTURE
operation, to specify how to resolve a signal into a number
The CAPTURE
operation involves reading in a signal on a suitable signal line, and integrating it with respect to a kernel.
Mathematically, there is a realvalued signal $ s(t), $ corresponding to a reading on the signal line. This is combined with a complexvalued â€śbasebandâ€ť kernel \(k(t)\) to get a resulting IQ value \(z\) by
where \(f\) denotes the frequency of the associated capture frame.
In QuilT, this integrating kernel is specified by a waveform, with the usual convention that is is scaled to satisfy \(\int k(t) \, dt = 1.\) The most common example is the boxcar_kernel
, which corresponds to a flat pulse scaled to satisfy this condition.
boxcar_kernelÂ¶
Because of the normalization condition, the boxcar_kernel
requires only a duration
argument for its construction.
[10]:
from pyquil.quiltwaveforms import BoxcarAveragerKernel
plot_waveform(
BoxcarAveragerKernel(duration=1e6),
sample_rate=1e9
)
boxcar_kernel(duration: 1e06)
The samples should sum to (roughly) one.
[11]:
assert np.isclose(
np.sum(BoxcarAveragerKernel(duration=1e6).samples(1e9)),
1.0
)
Note: The reference implementations of these waveforms makes use of Pythonâ€™s double precision floating point arithmetic. The actual implementation involves a certain amount of hardware dependence. For example, Rigettiâ€™s waveform generation hardware currently makes use of 16 bit fixed point arithmetic.
Compile Time versus Run TimeÂ¶
When thinking about parameters in QuilT, there are two times to consider: * the time of compilation, when a QuilT program is translated to a binary format executable on Rigetti hardware * the time at which the program is run.
All template parameters must be resolved at compile time. The QuilT compiler depends on being able to determine the size and contents of waveforms in advane. In other words, the following is not allowed:
DECLARE theta REAL
PULSE 0 "rf" flat(duration: 1e8, iq: 1.0, phase: theta)
However, the following program is valid, and does something equivalent
DECLARE theta REAL
SHIFTPHASE 0 "rf" theta
PULSE 0 "rf" flat(duration: 1e8, iq: 1.0)
SHIFTPHASE 0 "rf" theta
(Why the second SHIFTPHASE
? To leave the frame 0 "rf"
as it was before we started!)
Explicit control over the runtime phase, frequency, or scale requires the use of one of the following instructions.
SETSCALE
SETPHASE
SHIFTPHASE
SETFREQUENCY
SHIFTFREQUENCY
all of which support runtime parameter arguments.
[ ]:
Parametric Compilation ExamplesÂ¶
In this notebook, we demonstrate parametric compilation by way of three â€śexperimentsâ€ť:
Qubit Spectroscopy, in which we sweep over a detuning parameter,
Power Rabi, in which we sweep over pulse scale,
Time Rabi, in which we sweep over pulse duration.
The intent here is to demonstrate QuilT features; as such we do not do much in the way of data analysis.
[1]:
from typing import Optional
from pyquil import Program, get_qc
from pyquil.quilatom import Qubit, Frame
from pyquil.quilbase import Gate, Pulse, DefCalibration
from pyquil.gates import RX
qc = get_qc('Aspen8')
[2]:
cals = qc.compiler.calibration_program
First, we write a useful little helper, to extract the first pulse from a gate calibration.
[3]:
import numpy as np
def get_pulse(cal: DefCalibration) > Optional[Pulse]:
return next((i for i in cal.instrs if isinstance(i, Pulse)), None)
rx0 = cals.get_calibration(RX(np.pi, 0))
print(get_pulse(rx0))
NONBLOCKING PULSE 0 "rf" drag_gaussian(duration: 1.2e07, fwhm: 3e08, t0: 6e08, anh: 190000000.0, alpha: 0.5128791143256078, scale: 0.63376686965814, phase: 0.0, detuning: 0.0)
Qubit SpectroscopyÂ¶
Here we present a simple Qubit spectroscopy experiment. The general idea is that we scan over a range of frequencies, applying a pulse and measuring the resulting probability that the qubit is excited. There are some natural matters which we do not concern ourselves with, for example the choice of pulse duration, or carefully inspecting the readout values. Instead we wish to demonstrate how this sort of experiment can be done using parametric compilation.
We first define a function which can produce a parametric program to perform this experiment.
[4]:
def qubit_spectroscopy(qubit: int, param: str, *, calibrations: Program, shots: int = 1000) > Program:
""" Generate a program for doing a Qubit spectroscopy experiment.
:param qubit: The qubit index to run on.
:param param: The name of the parameter used for detuning.
:param calibrations: The QPU calibrations, needed in order to identify an appropriate pulse and frame.
:param shots: The number of shots to execute for a single run.
"""
# The basic idea is:
#  shift the frequency by the amount indicated by `param`
#  do an RX(pi) gate
#  measure
# We first find the frame on which the RX(pi) pulse is applied.
cal = calibrations.get_calibration(RX(np.pi, qubit))
pulse = get_pulse(cal)
frame = pulse.frame
# When we construct the program, we are sure to include the frame definition
# (since SHIFTFREQUENCY is applied to this frame).
return Program(
calibrations.frames[pulse.frame],
'DECLARE ro BIT',
f'DECLARE {param} REAL',
f'SHIFTFREQUENCY {frame} {param}',
f'RX(pi) {qubit}',
f'MEASURE {qubit} ro'
).wrap_in_numshots_loop(1000)
print(qubit_spectroscopy(0, 'detuning', calibrations=cals))
DEFFRAME 0 "rf":
DIRECTION: "tx"
INITIALFREQUENCY: 5145219610.47124
CENTERFREQUENCY: 5250000000.0
HARDWAREOBJECT: "q0_rf"
SAMPLERATE: 1000000000.0
DECLARE ro BIT[1]
DECLARE detuning REAL[1]
SHIFTFREQUENCY 0 "rf" detuning[0]
RX(pi) 0
MEASURE 0 ro[0]
Now we compile and run, considering detuning frequencies in the range from 3 MHz to +3 MHz. Note that with parametric compilation we only require one call to the compiler.
[5]:
exe = qc.compiler.native_quil_to_executable(qubit_spectroscopy(0, 'detuning', calibrations=cals))
detunings = np.linspace(3e6, 3e6, 100)
dprobs = []
for detuning in detunings:
results = qc.run(exe, {'detuning': [detuning]})
p1 = np.sum(results)/len(results)
dprobs.append(p1)
[6]:
%matplotlib inline
import matplotlib.pyplot as plt
plt.plot(detunings, dprobs, '.')
[6]:
[<matplotlib.lines.Line2D at 0x7f874b2c3550>]
Power RabiÂ¶
We consider another experiment, in which we vary the waveform amplitude and consider how this effects the probability that a qubit is excited. This is formally quite similar to the previous Qubit spectroscopy.
[7]:
from copy import deepcopy
def power_rabi(qubit: int, param: str, *, calibrations: Program, shots: int = 1000) > Program:
""" Generate a program for doing a power Rabi experiment.
:param qubit: The qubit index to run on.
:param param: The name of the parameter used for amplitude scaling.
:param calibrations: The QPU calibrations, needed in order to identify an appropriate pulse and frame.
:param shots: The number of shots to execute for a single run.
"""
# The basic idea is:
#  set the frame scaling to the amount indicated by `param`
#  perform the pulse of an RX(pi) gate
#  measure
# We first find the frame on which the RX(pi) pulse is applied.
cal = calibrations.get_calibration(RX(np.pi, qubit))
pulse = get_pulse(cal)
frame = pulse.frame
# When we construct the program, we are sure to include the frame definition
# (since SETSCALE is applied to this frame).
return Program(
calibrations.frames[pulse.frame],
'DECLARE ro BIT',
f'DECLARE {param} REAL',
f'SETSCALE {frame} {param}',
f'RX(pi) {qubit}',
f'MEASURE {qubit} ro'
).wrap_in_numshots_loop(1000)
print(power_rabi(0, 'scale', calibrations=cals))
DEFFRAME 0 "rf":
DIRECTION: "tx"
INITIALFREQUENCY: 5145219610.47124
CENTERFREQUENCY: 5250000000.0
HARDWAREOBJECT: "q0_rf"
SAMPLERATE: 1000000000.0
DECLARE ro BIT[1]
DECLARE scale REAL[1]
SETSCALE 0 "rf" scale[0]
RX(pi) 0
MEASURE 0 ro[0]
[8]:
exe = qc.compiler.native_quil_to_executable(power_rabi(0, 'scale', calibrations=cals))
scales = np.linspace(1e4, 1.0, 20)
sprobs = []
for scale in scales:
results = qc.run(exe, {'scale': [scale]})
p1 = np.sum(results)/len(results)
sprobs.append(p1)
[9]:
%matplotlib inline
plt.plot(scales, sprobs, '.')
[9]:
[<matplotlib.lines.Line2D at 0x7f874883f190>]
Time RabiÂ¶
Key to our use of parametric compilation in the previous two experiments is that the variable which we were speeping over (frequency or scale) had an associated QuilT instruction (SHIFTFREQUENCY
or SETSCALE
). In this example we consider a â€śTime Rabiâ€ť experiment, which involves varying the pulse length.
A current limitation of QuilT is that waveforms must be resolved at compile time, and so the duration
field of a template waveform cannot be a runtime parameter. The workaround for our Time Rabi experiment is that we must generate a new program for each value of duration
, and we cannot rely on parametric compilation for this.
[10]:
from copy import deepcopy
def time_rabi(qubit: int, duration: float, *, calibrations: Program, shots: int = 1000) > Program:
""" Generate a program for doing a time Rabi experiment.
:param qubit: The qubit index to run on.
:param duration: The pulse duration, in seconds.
:param calibrations: The QPU calibrations, needed in order to identify an appropriate pulse and frame.
:param shots: The number of shots to execute for a single run.
"""
# The basic idea is:
#  get the pulse associated to an RX(pi) gate
#  perform a modified version of this, with the `duration` updated
#  measure
# We first find the frame on which the RX(pi) pulse is applied.
cal = calibrations.get_calibration(RX(np.pi, qubit))
pulse = get_pulse(cal)
frame = pulse.frame
fdefn = calibrations.frames[frame]
updated_pulse = deepcopy(pulse)
# Note: duration must be aligned to 4 sample boundaries.
updated_pulse.waveform.duration = 4*np.round(duration*fdefn.sample_rate / 4) / fdefn.sample_rate
# When we construct the program, we are sure to include the frame definition
# (since SETSCALE is applied to this frame).
return Program(
fdefn,
'DECLARE ro BIT',
updated_pulse,
f'MEASURE {qubit} ro'
).wrap_in_numshots_loop(1000)
print(time_rabi(0, 1e8, calibrations=cals))
DEFFRAME 0 "rf":
DIRECTION: "tx"
INITIALFREQUENCY: 5145219610.47124
CENTERFREQUENCY: 5250000000.0
HARDWAREOBJECT: "q0_rf"
SAMPLERATE: 1000000000.0
DECLARE ro BIT[1]
NONBLOCKING PULSE 0 "rf" drag_gaussian(duration: 8e09, fwhm: 3e08, t0: 6e08, anh: 190000000.0, alpha: 0.5128791143256078, scale: 0.63376686965814, phase: 0.0, detuning: 0.0)
MEASURE 0 ro[0]
[11]:
times = np.linspace(1e9, 100e9, 20)
tprobs = []
for time in times:
exe = qc.compiler.native_quil_to_executable(time_rabi(0, time, calibrations=cals))
results = qc.run(exe)
p1 = np.sum(results)/len(results)
tprobs.append(p1)
[12]:
%matplotlib inline
plt.plot(times, tprobs, '.')
[12]:
[<matplotlib.lines.Line2D at 0x7f8748836a90>]
RAWCAPTURE on Aspen8Â¶
In this we are going to show how to access â€śrawâ€ť measurement data with Quilt.
[1]:
from pyquil import Program, get_qc
qc = get_qc('Aspen8')
[2]:
cals = qc.compiler.calibration_program
Peeking at a MEASURE calibrationÂ¶
We first take a peek at how a measurement operation is specified. We can dot his by looking at the corresponding calibration. Below we consider the calibration for MEASURE 0
.
[3]:
from pyquil.quilatom import Qubit, Frame
from pyquil.quilbase import Pulse, Capture, DefMeasureCalibration
qubit = Qubit(0)
measure_defn = next(defn for defn in cals.calibrations
if isinstance(defn, DefMeasureCalibration) and defn.qubit == qubit)
print(measure_defn)
DEFCAL MEASURE 0 addr:
FENCE 0
DECLARE q0_unclassified REAL[2]
NONBLOCKING PULSE 0 "ro_tx" flat(duration: 1.68e06, iq: 1.0, scale: 0.04466835921509615, phase: 0.0, detuning: 0.0)
NONBLOCKING CAPTURE 0 "ro_rx" boxcar_kernel(duration: 1.68e06, scale: 1.0, phase: 2.6571617075901393, detuning: 0.0) q0_unclassified[0]
PRAGMA FILTERNODE q0_unclassified "{'module':'lodgepole.filters.io','filter_type':'DataBuffer','source':'q0_ro_rx/filter','publish':true,'params':{},'_type':'FilterNode'}"
PRAGMA LOADMEMORY q0_unclassified "q0_unclassified[0]"
PRAGMA FILTERNODE q0_classified "{'module':'lodgepole.filters.classifiers','filter_type':'SingleQLinear','source':'q0_ro_rx/filter','publish':false,'params':{'a':[1.0,0.0],'threshold':0.000241237408735565},'_type':'FilterNode'}"
PRAGMA FILTERNODE q0 "{'module':'lodgepole.filters.io','filter_type':'DataBuffer','source':'q0_classified','publish':true,'params':{},'_type':'FilterNode'}"
PRAGMA LOADMEMORY q0 "addr"
FENCE 0
There are a few things note about the above:
The basic structure of
MEASURE 0 addr
is to apply a pulse on the"ro_tx"
frame, and then perform a capture on the corresponding"ro_rx"
frame.Although the user may perform
MEASURE 0 ro
, the memory location required for this is a bit. Under the hood,CAPTURE
writes a complex IQ value to theREAL[2]
regionq0_unclassified
.The wrangling in order to map from
q0_unclassified
toaddr
is controlled throughPRAGMA
operations. These are important for downstream processing and execution of the Quil program. Tamper with them at your own risk!
RAWCAPTURE experimentsÂ¶
The value stored in q0_unclassified
has already been processed on hardware: in particular, it is produced by demodulating a passband signal and then integrating against the CAPTURE
waveform. What RAWCAPTURE
does is give you, the user, access to the raw values of that passband signal. In the following, execute programs with RAWCAPTURE
, and plot their results.
Before we begin, it will be useful to get some data associated with the above MEASURE
calibration. In particular, the PULSE
and CAPTURE
operations, as well as the frame definition for 0 "ro_rx"
.
[4]:
pulse = next(i for i in measure_defn.instrs if isinstance(i, Pulse))
print(pulse, "\n")
capture = next(i for i in measure_defn.instrs if isinstance(i, Capture))
print(capture, "\n")
frame = Frame([qubit], "ro_rx")
frame_defn = cals.frames[frame]
print(frame_defn)
NONBLOCKING PULSE 0 "ro_tx" flat(duration: 1.68e06, iq: 1.0, scale: 0.04466835921509615, phase: 0.0, detuning: 0.0)
NONBLOCKING CAPTURE 0 "ro_rx" boxcar_kernel(duration: 1.68e06, scale: 1.0, phase: 2.6571617075901393, detuning: 0.0) q0_unclassified[0]
DEFFRAME 0 "ro_rx":
DIRECTION: "rx"
INITIALFREQUENCY: 7262459787.78838
HARDWAREOBJECT: "q0_ro_rx"
SAMPLERATE: 2000000000.0
An almosttrivial exampleÂ¶
First, letâ€™s just run a RAWCAPTURE
instruction. We will apply this to the above CAPTURE
frame, i.e. 0 "ro_rx"
, and for the same duration the CAPTURE
. The principal difference is that rather than readout to a memory region of length 2, we will need many more. It is easy to compute the size \(n\) of the output, namely
where \(t\) is the duration in seconds, \(f_s\) is the sample rate in Hz (which is part of the frame definition), and $ \left `:nbsphinxmath:lceil x :nbsphinxmath:right :nbsphinxmath:rceil $ denotes the smallest integer not less than :math:`x.
[5]:
from math import ceil
duration = capture.kernel.duration
sample_rate = frame_defn.sample_rate
memory_length = ceil(duration * sample_rate)
raw_capture_no_pulse = Program(
f'DECLARE raw REAL[{memory_length}]',
f'RAWCAPTURE {frame} {duration} raw'
).wrap_in_numshots_loop(1000)
print(raw_capture_no_pulse)
DECLARE raw REAL[3360]
RAWCAPTURE 0 "ro_rx" 1.68e06 raw[0]
[6]:
exe = qc.compiler.native_quil_to_executable(raw_capture_no_pulse)
qc.run(exe)
[7]:
raw_results_no_pulse = qc.qam.read_memory(region_name='raw')
Raw capture results are by default represented as integers in the interval \([2^{15}, 2^{15}]\). For many analyses you may prefer to normalize to the range \([1,1]\).
[8]:
print("shape", raw_results_no_pulse.shape)
print("data", raw_results_no_pulse)
shape (1000, 3360)
data [[ 156. 268. 488. ... 152. 96. 176.]
[704. 68. 588. ... 572. 20. 452.]
[304. 412. 784. ... 76. 628. 668.]
...
[408. 788. 500. ... 80. 116. 868.]
[736. 400. 956. ... 528. 872. 272.]
[388. 608. 432. ... 556. 548. 520.]]
[9]:
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
plt.figure()
plt.gcf().set_size_inches(20.5, 10.5)
plt.plot(np.arange(len(raw_results_no_pulse[0,:]))/sample_rate, raw_results_no_pulse[0,:])
plt.show()
[10]:
avg_results_no_pulse = raw_results_no_pulse.mean(axis=0) / (2**15)
[11]:
plt.psd(avg_results_no_pulse, Fs=sample_rate)
plt.show()
Applying a PULSE
before RAWCAPTURE
Â¶
Recall how measurements are usually done: first there is a pulse on the "ro_tx"
frame, and then a capture on the "ro_rx"
frame. We modify our above program by including the PULSE
operation associated with the vanilla measurement.
[12]:
raw_capture_pulse = Program(
f'DECLARE raw REAL[{memory_length}]',
pulse,
f'RAWCAPTURE {frame} {duration} raw'
).wrap_in_numshots_loop(1000)
print(raw_capture_pulse)
DECLARE raw REAL[3360]
NONBLOCKING PULSE 0 "ro_tx" flat(duration: 1.68e06, iq: 1.0, scale: 0.04466835921509615, phase: 0.0, detuning: 0.0)
RAWCAPTURE 0 "ro_rx" 1.68e06 raw[0]
[13]:
exe = qc.compiler.native_quil_to_executable(raw_capture_pulse)
qc.run(exe)
raw_results_pulse = qc.qam.read_memory(region_name='raw')
avg_results_pulse = raw_results_pulse.mean(axis=0) / 2**15
[14]:
plt.psd(avg_results_pulse, Fs=sample_rate)
plt.show()
Capturing an excited qubitÂ¶
Finally, we extend the above by first exciting the qubit, by applying a RX(pi)
gate.
[15]:
raw_capture_excited = Program(
f'DECLARE raw REAL[{memory_length}]',
f'RX(pi) {qubit}',
pulse,
f'RAWCAPTURE {frame} {duration} raw'
).wrap_in_numshots_loop(1000)
print(raw_capture_excited)
DECLARE raw REAL[3360]
RX(pi) 0
NONBLOCKING PULSE 0 "ro_tx" flat(duration: 1.68e06, iq: 1.0, scale: 0.04466835921509615, phase: 0.0, detuning: 0.0)
RAWCAPTURE 0 "ro_rx" 1.68e06 raw[0]
[16]:
exe = qc.compiler.native_quil_to_executable(raw_capture_excited)
qc.run(exe)
raw_results_excited = qc.qam.read_memory(region_name='raw')
avg_results_excited = raw_results_excited.mean(axis=0) / 2**15
[17]:
plt.psd(avg_results_excited, Fs=sample_rate)
plt.show()
TODOÂ¶
Discuss readout classification.
Some Restrictions May ApplyÂ¶
Performing a RAWCAPTURE
operation places a number of demands on the underlying hardware, and thus comes with a few constraints. We demonstrate these here.
Capture duration exceeds maximum lengthÂ¶
A RAWCAPTURE
operation can capture at most 8192 samples per shot, which puts a limit of \(\frac{8192}{f_s}\) seconds for the duration, where \(f_s\) is the frameâ€™s sample rate.
[18]:
duration = 5e6
samples = ceil(sample_rate*duration)
rrr = Program(
f'DECLARE raw REAL[{samples}]',
f'RAWCAPTURE 0 "ro_rx" {duration} raw'
).wrap_in_numshots_loop(1)
try:
exe = qc.compiler.native_quil_to_executable(rrr)
except Exception as e:
print(e)
ERROR: QPU Compiler native_quilt_to_binary failed: RAWCAPTURE 0 "ro_rx" 5e06 raw[0] would require 10000 samples, butat most 8192 are allowed. Consider using a duration of < 4.096e06 seconds.
Number of samples in a job exceeds maximumÂ¶
There is a total limit of \(2^{24}\) samples per job, i.e. duration * sample_rate * num_shots
cannot exceed \(2^24\).
[19]:
duration = 1e06
samples = ceil(sample_rate*duration)
rrr = Program(
f'DECLARE raw REAL[{samples}]',
f'RAWCAPTURE 0 "ro_rx" {duration} raw'
).wrap_in_numshots_loop(100000)
try:
exe = qc.compiler.native_quil_to_executable(rrr)
except Exception as e:
print(e)
ERROR: QPU Compiler native_quilt_to_binary failed: RAWCAPTURE would require DMA buffer of size 381.4697265625 MB but the maximum allowed is 32.0 MB.
For duration 1e06 seconds this places a limit of at most 8388 shots.
RAWCAPTURE
precludes the use of other capture operationsÂ¶
Due to the hardware requirements associated with RAWCAPTURE
, the following limits are currently imposed:
there can be at most one
RAWCAPTURE
operation per program, andif a program includes
RAWCAPTURE
, then it cannot also includeCAPTURE
operations.
[20]:
duration = 1e06
samples = ceil(sample_rate*duration)
rrr = Program(
f'DECLARE raw REAL[{samples}]',
'DECLARE ro BIT',
'MEASURE 1 ro',
f'RAWCAPTURE 0 "ro_rx" {duration} raw'
)
try:
exe = qc.compiler.native_quil_to_executable(rrr)
except Exception as e:
print(e)
ERROR: QPU Compiler native_quilt_to_binary failed: Capture conflict: RAWCAPTURE 0 "ro_rx" 1e06 raw[0] precludes the presence of any other capture instructions, but NONBLOCKING CAPTURE 1 "ro_rx" boxcar_kernel(duration: 2.36e06, scale: 1.0, phase: 1.1499233858972862, detuning: 0.0) q1_unclassified[0] was observed.
[ ]:
ProgramÂ¶

class
pyquil.quil.
Program
(*instructions)[source]Â¶ A list of pyQuil instructions that comprise a quantum program.
>>> from pyquil import Program >>> from pyquil.gates import H, CNOT >>> p = Program() >>> p += H(0) >>> p += CNOT(0, 1)
Attributes
Fill in any placeholders and return a list of quil AbstractInstructions.
A list of defined gates on the program.
A list of QuilT calibration definitions.
A mapping from waveform names to their corresponding definitions.
A mapping from QuilT frames to their definitions.
out
(*[,Â calibrations])Serializes the Quil program to a string suitable for submitting to the QVM or QPU.
get_qubits
([indices])Returns all of the qubit indices used in this program, including gate applications and allocated qubits.
is_protoquil
([quilt])Protoquil programs may only contain gates, Pragmas, and RESET.
Program Construction
__iadd__
(other)Concatenate two programs together using +=, returning a new one.
__add__
(other)Concatenate two programs together, returning a new one.
inst
(*instructions)Mutates the Program object by appending new instructions.
gate
(name,Â params,Â qubits)Add a gate to the program.
defgate
(name,Â matrix[,Â parameters])Define a new static gate.
define_noisy_gate
(name,Â qubit_indices,Â kraus_ops)Overload a static ideal gate with a noisy one defined in terms of a Kraus map.
define_noisy_readout
(qubit,Â p00,Â p11)For this program define a classical bit flip readout error channel parametrized by
p00
andp11
.no_noise
()Prevent a noisy gate definition from being applied to the immediately following Gate instruction.
measure
(qubit,Â classical_reg)Measures a qubit at qubit_index and puts the result in classical_reg
reset
([qubit_index])Reset all qubits or just a specific qubit at qubit_index.
measure_all
(*qubit_reg_pairs)Measures many qubits into their specified classical bits, in the order they were entered.
alloc
()Get a new qubit.
declare
(name[,Â memory_type,Â memory_size,Â â€¦])DECLARE a quil variable
wrap_in_numshots_loop
(shots)Wraps a Quil program in a loop that reruns the same program many times.
Control Flow
while_do
(classical_reg,Â q_program)While a classical register at index classical_reg is 1, loop q_program
if_then
(classical_reg,Â if_program[,Â â€¦])If the classical register at index classical reg is 1, run if_program, else run else_program.
Quilt Routines
get_calibration
(instr)Get the calibration corresponding to the provided instruction.
match_calibrations
(instr)Attempt to match a calibration to the provided instruction.
calibrate
(instruction[,Â â€¦])Expand an instruction into its calibrated definition.
Utility Methods
copy
()Perform a shallow copy of this program.
pop
()Pops off the last instruction.
dagger
([inv_dict,Â suffix])Creates the conjugate transpose of the Quil program.
__getitem__
(index)Allows indexing into the program to get an action.
Utility FunctionsÂ¶

pyquil.quil.
get_default_qubit_mapping
(program)[source]Â¶ Takes a program which contains qubit placeholders and provides a mapping to the integers 0 through N1.
The output of this function is suitable for input to
address_qubits()
.

pyquil.quil.
address_qubits
(program, qubit_mapping=None)[source]Â¶ Takes a program which contains placeholders and assigns them all defined values.
Either all qubits must be defined or all undefined. If qubits are undefined, you may provide a qubit mapping to specify how placeholders get mapped to actual qubits. If a mapping is not provided, integers 0 through N are used.
This function will also instantiate any label placeholders.
 Parameters
program â€“ The program.
qubit_mapping â€“ A dictionarylike object that maps from
QubitPlaceholder
toQubit
orint
(but not both).
 Returns
A new Program with all qubit and label placeholders assigned to real qubits and labels.

pyquil.quil.
instantiate_labels
(instructions)[source]Â¶ Takes an iterable of instructions which may contain label placeholders and assigns them all defined values.
 Return type
List
[AbstractInstruction
] Returns
list of instructions with all label placeholders assigned to real labels.

pyquil.quil.
implicitly_declare_ro
(instructions)[source]Â¶ Implicitly declare a register named
ro
for backwards compatibility with Quil 1.There used to be one unnamed hunk of classical memory. Now there are variables with declarations. Instead of:
MEASURE 0 [0]
You must now measure into a named register, idiomatically:
MEASURE 0 ro[0]
The
MEASURE
instruction will emit this (with a deprecation warning) if youâ€™re still using bare integers for classical addresses. However, you must also declare memory in the new scheme:DECLARE ro BIT[8] MEASURE 0 ro[0]
This method will determine if you are in â€śbackwards compatibility modeâ€ť and will declare a readout
ro
register for you. If you program contains any DECLARE commands or if it does not have any MEASURE x ro[x], this will not do anything.This behavior is included for backwards compatibility and will be removed in future releases of PyQuil. Please DECLARE all memory including
ro
. Return type
List
[AbstractInstruction
]

pyquil.quil.
merge_with_pauli_noise
(prog_list, probabilities, qubits)[source]Â¶ Insert pauli noise channels between each item in the list of programs. This noise channel is implemented as a single noisy identity gate acting on the provided qubits. This method does not rely on merge_programs and so avoids the inclusion of redundant Kraus Pragmas that would occur if merge_programs was called directly on programs with distinct noisy gate definitions.
 Parameters
prog_list (
Iterable
[Program
]) â€“ an iterable such as a program or a list of programs. If a program is provided, a single noise gate will be applied after each gate in the program. If a list of programs is provided, the noise gate will be applied after each program.probabilities (
Sequence
[float
]) â€“ The 4^num_qubits list of probabilities specifying the desired pauli channel. There should be either 4 or 16 probabilities specified in the order I, X, Y, Z or II, IX, IY, IZ, XI, XX, XY, etc respectively.qubits (
Sequence
[int
]) â€“ a list of the qubits that the noisy gate should act on.
 Return type
 Returns
A single program with noisy gates inserted between each element of the program list.

pyquil.quil.
merge_programs
(prog_list)[source]Â¶ Merges a list of pyQuil programs into a single one by appending them in sequence. If multiple programs in the list contain the same gate and/or noisy gate definition with identical name, this definition will only be applied once. If different definitions with the same name appear multiple times in the program list, each will be applied once in the order of last occurrence.