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 :ref:`start` at the top of the page.
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.
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
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 region-name type([count])? (SHARING parent-region-name (OFFSET (offset-count offset-type)+))?
The non-keyword items have the following allowable values:
region-name
: any non-keyword formal name.type
: one ofREAL
,BIT
,OCTET
, orINTEGER
parent-region-name
: any non-keyword formal name previously used asregion-name
in a differentDECLARE
statement.offset-count
: a nonnegative integer.offset-type
: the same allowable values astype
.
Here are some examples:
DECLARE beta REAL[32] DECLARE ro BIT[128] DECLARE beta-bits BIT[1436] SHARING beta DECLARE fourth-bit-in-beta1 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
beta-bits
, 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
calledfourth-bit-in-beta1
which overlays the fourth bit of the bit representation of theREAL
valuebeta[1]
.
Quil 1.0 is not compatible with Quil 2 in the following ways:
- The unnamed memory references
[n]
and[n-m]
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
.
- 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 long-term 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 pre-installed on a QMI. The entire platform is called Quantum Cloud Services (QCS).
By way of example, let's consider the following pyQuil 1.9 program, which computes the natural bond distance in molecular hydrogen using a VQE-type 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("9q-square-qvm")
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.9-style MEASURE
instructions into 2.0-style
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 :py:class:`~pyquil.quil.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("9q-square-qvm")
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
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 implementation-dependent.
On the QPU, INTEGER
refers to an unsigned integer stored in a 48-bit wide little-endian word, and REAL
refers to
a 48-bit wide little-endian fixed point number of type <0.48>. In general, these datatypes are implementation-dependent.
OCTET
always refers to an 8-bit 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 64-bit integer. REAL is an IEEE-754 double-precision floating-point number.
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 user-specifiable 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": "2018-09-11T18:40:19.253286", "call_log": { "__init__('<pyquil.api._qvm.QVM object at 0x1027e3940>', '<pyquil.api._base_connection.ForestConnection object at 0x1027e3588>', )": { "timestamp_in": "2018-09-11T18:40:18.967750", "timestamp_out": "2018-09-11T18:40:18.968170", "return_value": "None" }, "run('<pyquil.api._qvm.QVM object at 0x1027e3940>', )": { "timestamp_in": "2018-09-11T18: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 (clang-900.0.39.2)]", "pyquil_version": "2.0.0-internal.1" } }
Please attach such a logfile to any request for support.
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)