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://forest-server.qcs.rigetti.com
user_id = 4fd12391-11eb-52ec-35c2-262765ae4c4f

[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 QPU-engaged. It would have no effect if you are running locally, but is important if you are running on the QMI. By default, it runs the exec_on_engage.sh shell script. It’s best to leave the configuration as is, and edit that script. More documentation about exec_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:

  1. The runtime environment, set using export in Unix-based platforms, and setx in Windows.

  2. QCS configuration files noted above: .qcs_config and .forest_config, or their user-configured file paths. The two are not interchangeable; each config value will be looked up in exactly one of these files.

  3. The default values specified in pyquil.api._config. These should not be changed by any Pyquil user, as doing so may lead to difficult-to-debug behavior.

These are all the configuration options available to you, and where they can be set:

PyQuil Configuration Options

Setting

Environment

Configuration File

Forest Server URL

Source of device information

FOREST_URL

.qcs_config

Key: url

Dispatch URL

Provides QPU engagement authorization

FOREST_DISPATCH_URL

.qcs_config

Key: dispatch_url

User Authentication Token Path

File path to the authentication token obtained from qcs.

USER_AUTH_TOKEN_PATH

.qcs_config

Key: user_auth_token_path

QCS URL

QCS website

QCS_URL

.qcs_config

Key: qcs_url

QPU URL

Send binaries to the QPU

QPU_URL

.forest_config

Key: qpu_endpoint_address

QVM URL

Simulator

QCS_URL

.forest_config

Key: qvm_address

QPU Compiler URL

Send native Quil and receive a binary

QPU_COMPILER_URL

.forest_config

Key: qpu_compiler_address

Local Compiler URL

Send Quil and receive native Quil

QUILC_URL

.forest_config

Key: qpu_compiler_address

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, near-term 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 QubitPlaceholders 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:

  1. Declare a register called flag_register to use as a boolean test for looping.

  2. Initialize this register to 1, so our while loop will execute. This is often called the loop preamble or loop initialization.

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

  4. 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
JUMP-UNLESS @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 bit

  • AND which operates on two classical bits

  • IOR which operates on two classical bits

  • MOVE which moves the value of a classical bit at one classical address into another

  • EXCHANGE 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 if-statement. 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
JUMP-WHEN @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("2q-qvm")
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 + (5-2i)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 = (5-2j) * 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 + (5-2j)*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 Suzuki-Trotter 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.46875-30j)*I + (-16.734375+15j)*X0*Y1*Z3 + (71.5625-144.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)