PyQuil Beginner Free 2/7 in series 20 min read

Hello World in PyQuil

Write your first quantum program in PyQuil, build a Bell state using Quil instructions, run it on the QVM simulator, and understand the output.

What you'll learn

  • pyquil
  • quil
  • rigetti
  • bell state
  • qvm
  • quantum programming
  • python

Prerequisites

  • Basic Python (variables, functions, loops)
  • No quantum physics background needed

PyQuil is Rigetti Computing’s Python library for quantum programming. It stands apart from other quantum frameworks like Qiskit (IBM) and Cirq (Google) because of how it represents quantum programs. In PyQuil, every quantum program is a Quil (Quantum Instruction Language) program, a low-level instruction set that maps closely to what quantum processors actually execute.

When you write p += H(0) in PyQuil, you are adding an H 0 instruction to a Quil program. When you write p += CNOT(0, 1), you are appending CNOT 0 1. When you call qc.compile(p), the quilc compiler translates your abstract Quil program into native gates that Rigetti hardware can run directly. This explicitness is intentional: PyQuil gives you close control over what the hardware actually executes, rather than hiding the compilation step behind layers of abstraction.

If you want to understand quantum computing at the instruction level, PyQuil is an excellent place to start.

Setup and Installation

pip install pyquil

To run programs, you also need the Quantum Virtual Machine (QVM) and Quilc compiler from Rigetti’s Forest SDK, or use pyquil’s built-in WavefunctionSimulator for quick local testing.

Quil: The Instruction Set

Before writing code, it helps to understand what Quil actually is. Quil (Quantum Instruction Language) is Rigetti’s quantum instruction set. It plays a role similar to assembly language for classical processors: a program is a sequence of instructions, each specifying an operation on specific qubits or classical registers.

A Quil program has three parts:

  1. Declarations: classical memory allocation (where measurement results are stored)
  2. Quantum instructions: gates and measurements that operate on qubits
  3. Control flow: pragmas, resets, and other directives

Here is a complete Quil program in raw text form:

DECLARE ro BIT[2]
H 0
CNOT 0 1
MEASURE 0 ro[0]
MEASURE 1 ro[1]

Line by line:

  • DECLARE ro BIT[2] allocates a classical register named ro with 2 bits. Every measurement needs a place to store its result, and this line creates that storage.
  • H 0 applies the Hadamard gate to qubit 0, placing it in an equal superposition of |0> and |1>.
  • CNOT 0 1 applies a controlled-NOT gate with qubit 0 as the control and qubit 1 as the target. If qubit 0 is |1>, qubit 1 flips.
  • MEASURE 0 ro[0] measures qubit 0 and writes the result (0 or 1) into the first classical bit of ro.
  • MEASURE 1 ro[1] measures qubit 1 and writes the result into the second classical bit.

One important detail: Quil uses qubit indices directly. There is no qubit register declaration. Qubits are implicitly available by index (0, 1, 2, …), so you simply refer to them by number.

When you print(p) a PyQuil Program object, the output is actual Quil source code. What you see printed is exactly what gets sent to the compiler or simulator.

Example 1: Bell State with WavefunctionSimulator

The WavefunctionSimulator runs locally without any external services. It computes the full statevector of your program, which is useful for verifying that your circuit produces the correct quantum state before you run it on hardware or a sampling simulator.

from pyquil import Program
from pyquil.gates import H, CNOT, MEASURE
from pyquil.quilbase import Declare
from pyquil.api import WavefunctionSimulator

# Build the Bell state program
p = Program()

# Declare classical memory to store measurement results
ro = p.declare('ro', 'BIT', 2)

# Apply gates: H on qubit 0, then CNOT from 0 to 1
p += H(0)
p += CNOT(0, 1)
p += MEASURE(0, ro[0])
p += MEASURE(1, ro[1])

print("Quil program:")
print(p)

# Simulate the wavefunction
wf_sim = WavefunctionSimulator()
wavefunction = wf_sim.wavefunction(p)
print("\nWavefunction amplitudes:")
print(wavefunction)

Expected Output


Quil program:
DECLARE ro BIT[2]
H 0
CNOT 0 1
MEASURE 0 ro[0]
MEASURE 1 ro[1]

Wavefunction amplitudes:
(0.7071067812+0j)|00> + (0.7071067812+0j)|11>

The wavefunction shows equal amplitude on |00> and |11>, confirming the Bell state before measurement collapses it.

Line-by-Line Breakdown

Let’s walk through each line of the code to understand what it does:

  • Program() creates an empty Quil program. This is the main container that you add instructions to. Think of it as a blank document that will hold your sequence of quantum instructions.

  • p.declare('ro', 'BIT', 2) declares 2 classical bits named ro. This translates to the Quil instruction DECLARE ro BIT[2]. The name ro (short for “readout”) is a convention, but you can use any name. The return value ro is a reference you use later when specifying where measurement results should go.

  • p += H(0) appends the instruction H 0 to the Quil program. The Hadamard gate transforms qubit 0 from the |0> state into an equal superposition: (|0> + |1>) / sqrt(2).

  • p += CNOT(0, 1) appends CNOT 0 1 to the program. This entangles qubit 0 and qubit 1. After this gate, the two-qubit state is (|00> + |11>) / sqrt(2), a Bell state. The qubits are now correlated: measuring one determines the other.

  • p += MEASURE(0, ro[0]) appends MEASURE 0 ro[0] to the program. This measures qubit 0 in the computational basis and stores the result (0 or 1) in the first classical bit of the ro register.

  • WavefunctionSimulator() creates a local simulator that directly computes the statevector. It requires the quilc service running locally, or uses the built-in simulator depending on your PyQuil version. Unlike the QVM (which samples bitstrings), the WavefunctionSimulator gives you the exact amplitudes of every basis state.

  • wf_sim.wavefunction(p) executes the program and returns the quantum state before measurement. The returned Wavefunction object contains the full complex amplitude vector. You can inspect individual amplitudes, compute probabilities, or print the state in human-readable form.

Example 2: Sampling with the QVM

For sampling with the QVM, the system runs repeated shots and returns classical bitstrings, simulating what real hardware returns. This is the more realistic way to interact with quantum programs, because real quantum processors do not give you access to the wavefunction. They only return measurement outcomes.

What Are QVM and quilc?

Before running this example, it helps to understand the two services involved:

quilc is the Rigetti compiler. It translates abstract Quil (the code you write) into native Quil, which uses Rigetti’s native gate set: RX, RZ, and CZ. These are the gates that Rigetti’s superconducting qubits can physically execute. The quilc compiler also performs circuit optimization, reducing gate count and circuit depth where possible.

QVM (Quantum Virtual Machine) is a high-performance simulator that executes native Quil programs. It is the reference simulator for Rigetti hardware, meaning it faithfully reproduces the behavior of a Rigetti QPU (including qubit topology constraints when configured to do so). The QVM returns measurement results as arrays of classical bits, exactly like real hardware.

Both quilc and QVM run as local services. You start them from the command line before running your Python code:

quilc -S &   # Start the compiler server
qvm -S &     # Start the QVM server

Running the Sampling Example

from pyquil import Program, get_qc
from pyquil.gates import H, CNOT, MEASURE
from pyquil.quilbase import Declare

# Build Bell state program
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])

# Use a 2-qubit QVM (requires quilc + qvm running locally)
qc = get_qc('2q-qvm')
p.wrap_in_numshots_loop(1000)  # Run 1000 shots

result = qc.run(qc.compile(p))
print("Sample results (first 5):", result.readout_data['ro'][:5].tolist())

# Count outcomes
from collections import Counter
counts = Counter(map(tuple, result.readout_data['ro'].tolist()))
print("Counts:", dict(counts))

Expected Output


Sample results (first 5): [[0, 0], [1, 1], [0, 0], [1, 1], [0, 0]]
Counts: {(0, 0): 501, (1, 1): 499}

Understanding get_qc

The function get_qc('2q-qvm') creates a QuantumComputer object that uses a 2-qubit QVM backend. The string '2q-qvm' specifies two things: the number of qubits (2) and that the backend is a QVM simulator. To run on real hardware, you replace this string with a real processor name, for example get_qc('Aspen-M-3').

The qc.compile(p) call sends your program to quilc, which compiles it into native gates. Then qc.run() sends the compiled program to the QVM (or hardware) for execution and returns the measurement results.

Understanding Quil Assembly

When you print a PyQuil program, you see the full Quil assembly that represents your circuit. Here is the complete Quil program from the Bell state example:

DECLARE ro BIT[2]    # Classical register declaration
H 0                  # Hadamard gate on qubit 0
CNOT 0 1             # CNOT: control=0, target=1
MEASURE 0 ro[0]      # Measure qubit 0 into ro[0]
MEASURE 1 ro[1]      # Measure qubit 1 into ro[1]

Each line is a single Quil instruction. Here is what each instruction type does:

  • DECLARE allocates classical memory. DECLARE ro BIT[2] creates a 2-bit register named ro. You must declare classical memory before you can use it in MEASURE instructions. Other types include REAL and OCTET, but BIT is by far the most common.

  • Gate instructions (H 0, CNOT 0 1) apply unitary operations to qubits. The gate name comes first, followed by the qubit indices. Single-qubit gates take one index; two-qubit gates take two. Parameterized gates include the parameter in parentheses, for example RX(pi/2) 0.

  • MEASURE performs a projective measurement. MEASURE 0 ro[0] collapses qubit 0 to either |0> or |1> and writes the result to classical bit ro[0]. After measurement, the qubit is in a definite classical state.

Notice that Quil has no qubit declaration instruction. Qubits are implicitly available by index, starting from 0. You simply refer to them by number in your gate and measurement instructions. This is different from Qiskit, which requires you to create a QuantumRegister explicitly.

Key PyQuil Objects

Here are the most important objects and functions you will use in PyQuil:

  • Program: the main container for Quil instructions. You create one with Program() and add instructions using +=. A Program can also be initialized with instructions directly: Program(H(0), CNOT(0, 1)).

  • H(n), X(n), CNOT(n, m): gate constructors that return Quil instruction objects. H(0) returns an object representing the instruction H 0. Other common gates include RX(theta, n), RZ(theta, n), CZ(n, m), and SWAP(n, m).

  • MEASURE(qubit, classical_bit): creates a measurement instruction. The first argument is the qubit index, the second is a reference to a classical bit (obtained from declare). Example: MEASURE(0, ro[0]) produces MEASURE 0 ro[0].

  • p.declare(name, type, size): declares classical memory on a Program and returns a reference to it. p.declare('ro', 'BIT', 2) adds DECLARE ro BIT[2] to the program and returns a MemoryReference you can index with ro[0], ro[1].

  • WavefunctionSimulator: a local statevector simulator. It computes the exact quantum state of your program, returning all amplitudes. Useful for debugging and verification, but it does not scale to large qubit counts (the state vector grows as 2^n).

  • get_qc(name): factory function that returns a QuantumComputer connected to either a QVM simulator or real Rigetti hardware. Pass a string like '2q-qvm' for a simulated backend, or a real processor name like 'Aspen-M-3' for hardware access.

PyQuil vs Qiskit vs Cirq

If you are choosing between quantum frameworks, here is how they compare:

FeaturePyQuilQiskitCirq
Developed byRigettiIBMGoogle
Language modelQuil instruction setPython APIPython API
HardwareRigetti QPU (QCS)IBM QuantumGoogle Sycamore
SimulatorQVM (Rigetti)AerCirq Simulator
Best forRigetti hardware, low-level controlIBM hardware, educationGoogle hardware, NISQ research

PyQuil’s distinguishing feature is its close relationship to Quil, the instruction set. Every PyQuil program is, at its core, a Quil program. This gives you more visibility into what the compiler does and what the hardware receives. Qiskit and Cirq provide higher-level abstractions that can be more convenient but hide more of the compilation process.

If your goal is to run on Rigetti hardware, PyQuil is the natural choice. If you want the largest hardware fleet and the most educational resources, Qiskit and IBM Quantum are hard to beat. If you are doing NISQ research with Google’s processors, Cirq is your tool.

Execution on Rigetti Hardware

To execute programs on Rigetti’s physical QPUs, you access the Quantum Cloud Services (QCS) platform. QCS is Rigetti’s cloud infrastructure for submitting quantum programs to real superconducting processors. It handles authentication, job scheduling, and result retrieval.

Getting Access

  1. Create an account at qcs.rigetti.com
  2. Generate an API token from your QCS dashboard
  3. Configure your local environment with qcs configure or set the appropriate environment variables

Running on Hardware

from pyquil import get_qc

# Replace '2q-qvm' with a real processor name from QCS
qc = get_qc('Aspen-M-3')  # Example Rigetti QPU name
result = qc.run(qc.compile(p))

When you call get_qc('Aspen-M-3'), PyQuil creates a QuantumComputer object connected to real hardware. The qc.compile(p) call sends your program to quilc, which compiles it into native gates for that specific processor. This compilation step accounts for the processor’s qubit topology (which qubits are physically connected), its native gate set (RX, RZ, CZ), and performs optimizations to reduce circuit depth. The compiled program is then submitted to the hardware for execution.

Results come back as arrays of classical bits, in the same format as QVM results. The key difference is noise: real hardware introduces errors from decoherence, gate infidelity, and measurement errors. Your Bell state results on hardware will not be a perfect 50/50 split between |00> and |11>. You will also see small counts of |01> and |10> from noise.

Common Mistakes

Here are pitfalls that new PyQuil users frequently encounter:

Forgetting to declare classical memory

Every MEASURE instruction needs a classical bit to write its result to. If you write MEASURE(0, ro[0]) without first calling p.declare('ro', 'BIT', 2), you will get an error. Always declare your classical registers before using them in measurements.

# Wrong: ro is not defined
p = Program()
p += H(0)
p += MEASURE(0, ro[0])  # NameError: 'ro' is not defined

# Correct: declare ro first
p = Program()
ro = p.declare('ro', 'BIT', 1)
p += H(0)
p += MEASURE(0, ro[0])

Confusing WavefunctionSimulator with NumpyWavefunctionSimulator

PyQuil includes two wavefunction simulators. WavefunctionSimulator connects to the quilc service and may require external services running. NumpyWavefunctionSimulator is a pure-Python, self-contained simulator that does not need any external services. If you want the simplest local testing experience, use NumpyWavefunctionSimulator:

from pyquil.simulation import NumpyWavefunctionSimulator

Running get_qc without QVM services

Calling get_qc('2q-qvm') creates a QuantumComputer that connects to a local QVM server and quilc compiler. If these services are not running, you will get a connection error. Start them first:

quilc -S &   # Start the compiler
qvm -S &     # Start the simulator

If you do not want to run external services, use NumpyWavefunctionSimulator for quick experiments instead.

Forgetting wrap_in_numshots_loop

When using get_qc and qc.run(), you need to specify how many times the program should run (how many “shots” to take). Call p.wrap_in_numshots_loop(n) before compiling. Without this, you may get only a single shot, which is rarely useful for understanding the probability distribution of your program’s output.

Was this tutorial helpful?