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.
Circuit diagrams
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:
- Declarations: classical memory allocation (where measurement results are stored)
- Quantum instructions: gates and measurements that operate on qubits
- 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 namedrowith 2 bits. Every measurement needs a place to store its result, and this line creates that storage.H 0applies the Hadamard gate to qubit 0, placing it in an equal superposition of|0>and|1>.CNOT 0 1applies 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 ofro.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 namedro. This translates to the Quil instructionDECLARE ro BIT[2]. The namero(short for “readout”) is a convention, but you can use any name. The return valuerois a reference you use later when specifying where measurement results should go. -
p += H(0)appends the instructionH 0to 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)appendsCNOT 0 1to 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])appendsMEASURE 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 theroregister. -
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 returnedWavefunctionobject 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 namedro. You must declare classical memory before you can use it inMEASUREinstructions. Other types includeREALandOCTET, butBITis 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 exampleRX(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 bitro[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 withProgram()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 instructionH 0. Other common gates includeRX(theta, n),RZ(theta, n),CZ(n, m), andSWAP(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 fromdeclare). Example:MEASURE(0, ro[0])producesMEASURE 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)addsDECLARE ro BIT[2]to the program and returns aMemoryReferenceyou can index withro[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 aQuantumComputerconnected 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:
| Feature | PyQuil | Qiskit | Cirq |
|---|---|---|---|
| Developed by | Rigetti | IBM | |
| Language model | Quil instruction set | Python API | Python API |
| Hardware | Rigetti QPU (QCS) | IBM Quantum | Google Sycamore |
| Simulator | QVM (Rigetti) | Aer | Cirq Simulator |
| Best for | Rigetti hardware, low-level control | IBM hardware, education | Google 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
- Create an account at qcs.rigetti.com
- Generate an API token from your QCS dashboard
- Configure your local environment with
qcs configureor 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?