Hello World with Perceval (Photonic Quantum Computing)
Build your first photonic quantum circuit with Quandela's Perceval framework - create a beam splitter circuit, demonstrate the Hong-Ou-Mandel effect, and understand how photons encode quantum information.
Perceval is Quandela’s open-source Python framework for photonic quantum computing, using linear optics: networks of beam splitters and phase shifters that manipulate individual photon paths at room temperature. Unlike superconducting or trapped-ion platforms, photonic quantum computers encode information in the presence or absence of photons across optical modes, and they operate without cryogenic cooling.
This tutorial covers installation, the physics of linear optics, the beam splitter transformation, the Hong-Ou-Mandel effect (with a full mathematical derivation), dual-rail qubit encoding, boson sampling, process tomography, noise modeling, and running circuits on Quandela hardware.
How Photonic Quantum Computing Works
The quantum state of a photonic system is described by how many photons occupy each optical mode, written as a Fock state: |1,0,1,0⟩ means one photon in mode 0, zero in mode 1, one in mode 2, zero in mode 3. Each mode corresponds to a physical waveguide or optical path on a photonic chip.
Perceval uses dual-rail encoding: a logical qubit lives across two adjacent modes, with |1,0⟩ encoding logical |0⟩ and |0,1⟩ encoding logical |1⟩. A single photon occupies exactly one of the two rails, and the qubit state is determined by which rail contains the photon.
This is different from Xanadu’s Strawberry Fields, which uses continuous-variable squeezed light. See the continuous-variable glossary entry. If your goal is Gaussian boson sampling with squeezed light, Strawberry Fields is the better match; Perceval is the right tool for discrete single-photon circuits and Quandela hardware.
Installation
pip install perceval-quandela
Verify the installation:
import perceval as pcvl
print(pcvl.__version__) # should print 0.11.x or later
Perceval requires Python 3.10 or newer. All code examples in this tutorial assume import perceval as pcvl and import math at the top of the script.
The Physics of Linear Optics
Two components form the building blocks of every photonic quantum circuit: the beam splitter (BS) and the phase shifter (PS).
The Beam Splitter
A beam splitter combines two optical modes. Physically, it is a partially reflective mirror or a directional coupler on a photonic chip. The BS transforms the creation operators of the two input modes into linear combinations of the output mode operators.
For a 50/50 beam splitter using Perceval’s default Ry convention, the transformation is:
a†_out = (a†_in + b†_in) / sqrt(2)
b†_out = (-a†_in + b†_in) / sqrt(2)
This corresponds to the unitary matrix:
U_BS = 1/sqrt(2) * [[1, 1],
[-1, 1]]
Note the sign convention: Perceval’s default BS() uses the Ry convention, which places the negative sign on the lower-left entry. Other frameworks sometimes use the Hadamard convention [[1, 1], [1, -1]] / sqrt(2) or the Rx convention with imaginary entries [[1, i], [i, 1]] / sqrt(2). Perceval supports all three via the convention parameter of BS(). When comparing results across frameworks, always check which convention is in use.
What the Beam Splitter Does to Photon States
Consider a single photon in mode 0, written as |1,0⟩. Applying the BS transformation:
a†_in |vac⟩ = |1,0⟩
After BS: (a†_out + b†_out) / sqrt(2) * |vac⟩
= (|1,0⟩ + |0,1⟩) / sqrt(2)
Wait, that is not quite right. We need to invert the BS matrix to express input operators in terms of output operators, or equivalently apply the matrix to the state vector directly. Applying the unitary to the Fock state amplitude vector [1, 0]:
U_BS * [1, 0]^T = [1/sqrt(2), -1/sqrt(2)]^T
So the output state is (|1,0⟩ - |0,1⟩) / sqrt(2): a superposition of the photon in mode 0 and mode 1, with a relative minus sign from the Ry convention.
The Phase Shifter
A phase shifter PS(phi) acts on a single mode, multiplying the photon amplitude by e^(i*phi). Its unitary matrix is the 1x1 matrix [e^(i*phi)]. In dual-rail encoding, applying a phase shifter to one rail implements a relative phase between the |0⟩ and |1⟩ components of the qubit, which corresponds to a Z rotation on the Bloch sphere.
Core Concepts: Modes, Fock States, and the Circuit API
Modes are the optical paths a photon can travel. Fock states describe which modes are occupied: pcvl.BasicState([1, 0]) puts one photon in mode 0 and none in mode 1. Beam splitters (BS()) mix two modes; phase shifters (PS(phi)) shift the phase of a single mode.
Building Circuits
The pcvl.Circuit(m) constructor creates a circuit with m modes. Use .add(port, component) to append components, where port is the starting mode index.
import perceval as pcvl
from perceval.components import BS, PS
import math
# Create a 2-mode circuit
circuit = pcvl.Circuit(2)
circuit.add(0, BS()) # 50/50 beam splitter on modes 0 and 1
circuit.add(0, PS(phi=math.pi / 4)) # phase shift of π/4 on mode 0
Displaying Circuits
Use pcvl.pdisplay() to render a circuit diagram. In a Jupyter notebook, this produces a graphical SVG rendering. In a terminal, it falls back to a text representation.
pcvl.pdisplay(circuit)
Inspecting the Unitary Matrix
Every linear optical circuit has a corresponding unitary matrix that describes how it transforms the mode amplitudes. Retrieve it with compute_unitary().
import perceval as pcvl
from perceval.components import BS
import numpy as np
circuit = pcvl.Circuit(2)
circuit.add(0, BS())
U = circuit.compute_unitary()
pcvl.pdisplay(U)
# Verify it matches the expected BS matrix (Ry convention)
# Expected: [[cos(pi/4), sin(pi/4)], [-sin(pi/4), cos(pi/4)]]
# = [[1/sqrt(2), 1/sqrt(2)], [-1/sqrt(2), 1/sqrt(2)]]
print(np.allclose(np.abs(np.array(U)), 1/np.sqrt(2))) # True
The compute_unitary() method also accepts use_symbolic=True for symbolic computation, which is useful when circuit parameters are left as free variables.
Your First Circuit: The Hong-Ou-Mandel Effect
The Hong-Ou-Mandel (HOM) effect is one of the clearest demonstrations of quantum behavior available in photonic systems. Here is the setup: send exactly one photon down each input of a 50/50 beam splitter. Classically, you would expect four equally probable outcomes: both reflect (|2,0⟩), both transmit (|0,2⟩), photon A transmits and B reflects (|1,1⟩), or photon A reflects and B transmits (|1,1⟩). You would expect to see |1,1⟩ about half the time.
Quantum mechanically, the two |1,1⟩ paths interfere destructively and completely cancel. You never see |1,1⟩. Both photons always exit the same port together. This bunching happens because photons are bosons, and indistinguishable bosons constructively interfere when they bunch and destructively interfere when they split.
Mathematical Derivation of the HOM Effect
The input state is one photon in each mode:
|ψ_in⟩ = a†_0 a†_1 |vac⟩ = |1,1⟩
The beam splitter transforms the input creation operators into output operators. Using the Ry convention:
a†_0 → (c†_0 - c†_1) / sqrt(2)
a†_1 → (c†_0 + c†_1) / sqrt(2)
where c†_0 and c†_1 are the output mode creation operators. Substituting:
|ψ_out⟩ = [(c†_0 - c†_1) / sqrt(2)] * [(c†_0 + c†_1) / sqrt(2)] |vac⟩
Expanding the product:
= (1/2) * (c†_0 + c†_1)(c†_0 - c†_1) |vac⟩
= (1/2) * (c†_0 c†_0 - c†_0 c†_1 + c†_1 c†_0 - c†_1 c†_1) |vac⟩
Because creation operators for bosons commute (c†_0 c†_1 = c†_1 c†_0), the two cross terms cancel:
= (1/2) * ((c†_0)^2 - (c†_1)^2) |vac⟩
Applying (c†)^2 |vac⟩ = sqrt(2) |2⟩:
= (1/2) * (sqrt(2) |2,0⟩ - sqrt(2) |0,2⟩)
= (|2,0⟩ - |0,2⟩) / sqrt(2)
The |1,1⟩ amplitude is exactly zero. This is not an approximation or a limit; it is an exact cancellation that follows from the bosonic commutation relations. The HOM null at |1,1⟩ is purely quantum mechanical and has no classical analog.
HOM Effect in Perceval Code
import perceval as pcvl
from perceval.components import BS
from perceval.algorithm import Sampler
# Circuit: a single 50/50 beam splitter
circuit = pcvl.Circuit(2)
circuit.add(0, BS())
# Create a processor using the SLOS simulation backend
processor = pcvl.Processor("SLOS", circuit)
# Input state: one photon in each mode
processor.with_input(pcvl.BasicState([1, 1]))
# Sample 1000 output events
sampler = Sampler(processor)
result = sampler.sample_count(1000)
print(result)
# Expected output (approximately):
# {BasicState([2, 0]): 500, BasicState([0, 2]): 500}
# BasicState([1, 1]) never appears - this is the HOM effect
The photons must be indistinguishable: same wavelength, polarization, and temporal mode. Any which-path information partially restores the |1,1⟩ output. The HOM dip is a standard benchmark for photon source quality.
Using the Analyzer for Exact Probabilities
The Sampler gives statistical results from finite samples. For verification and debugging, the Analyzer computes exact probability distributions without any sampling noise. This is the preferred tool when you need to confirm that a circuit behaves correctly.
import perceval as pcvl
from perceval.components import BS
from perceval.algorithm import Analyzer
# Build the HOM circuit
circuit = pcvl.Circuit(2)
circuit.add(0, BS())
processor = pcvl.Processor("SLOS", circuit)
# Define which input and output states to analyze
input_states = [pcvl.BasicState([1, 1])]
output_states = [
pcvl.BasicState([2, 0]),
pcvl.BasicState([1, 1]),
pcvl.BasicState([0, 2]),
]
analyzer = Analyzer(processor, input_states, output_states)
analyzer.compute(normalize=True)
# Display the probability table
pcvl.pdisplay(analyzer)
# Shows a table with rows = input states, columns = output states
# For |1,1⟩ input:
# |2,0⟩: 0.5 |1,1⟩: 0.0 |0,2⟩: 0.5
The Analyzer produces a distribution matrix accessible via analyzer.distribution. Each cell contains the exact probability of a given output state for a given input state. Use analyzer.col(output_state) to look up the column index for a specific output.
This is much cleaner than running the Sampler with a large shot count and hoping for convergence. Use the Analyzer for verification and the Sampler when you need to model realistic finite-statistics experiments.
Building a Mach-Zehnder Interferometer
A Mach-Zehnder interferometer is two beam splitters with a phase shift in between. It is the photonic analog of a single-qubit rotation: a single photon enters one port and the output probability depends on the phase.
import perceval as pcvl
from perceval.components import BS, PS
from perceval.algorithm import Sampler
import math
# Mach-Zehnder: BS - phase - BS
circuit = pcvl.Circuit(2)
circuit.add(0, BS()) # first 50/50 beam splitter
circuit.add(0, PS(phi=math.pi / 2)) # phase shift of π/2 on mode 0
circuit.add(0, BS()) # second 50/50 beam splitter
processor = pcvl.Processor("SLOS", circuit)
processor.with_input(pcvl.BasicState([1, 0])) # single photon in mode 0
sampler = Sampler(processor)
result = sampler.sample_count(1000)
print(result)
# With phase = π/2: roughly equal outputs
# Change phi to 0 for complete transmission, π for complete reflection
Change phi to 0 for full transmission, math.pi for full reflection. The Mach-Zehnder interferometer demonstrates that any single-qubit rotation can be decomposed into a sequence of beam splitters and phase shifters, which is the foundation of the Reck decomposition for universal linear optical circuits.
Dual-Rail Encoding: How Photons Become Qubits
In dual-rail encoding, a logical qubit occupies two physical modes:
- Logical
|0⟩=|1,0⟩(photon in the first mode) - Logical
|1⟩=|0,1⟩(photon in the second mode)
This means a 4-qubit circuit requires at least 8 optical modes (2 per qubit). Confusing modes with qubits is one of the most common mistakes when starting with photonic quantum computing.
The Beam Splitter as a Hadamard Gate
A 50/50 beam splitter on a dual-rail qubit acts as a Hadamard-like gate, creating an equal superposition from a computational basis state.
import perceval as pcvl
from perceval.components import BS
from perceval.algorithm import Sampler
# logical |0> = photon in left mode = BasicState([1, 0])
circuit = pcvl.Circuit(2)
circuit.add(0, BS()) # acts like Hadamard on the dual-rail qubit
processor = pcvl.Processor("SLOS", circuit)
processor.with_input(pcvl.BasicState([1, 0])) # logical |0>
sampler = Sampler(processor)
result = sampler.sample_count(1000)
print(result)
# {BasicState([1, 0]): ~500, BasicState([0, 1]): ~500}
# Equal superposition of logical |0> and |1>
Note that the BS in Perceval’s Ry convention introduces a relative phase between the two output amplitudes (one gets a minus sign). This differs from the textbook Hadamard gate H = [[1, 1], [1, -1]] / sqrt(2) by a sign flip in one entry. In practice, this distinction matters only when you care about the global phase convention of your gates, for example when composing multi-qubit circuits.
The Phase Shifter as a Z Rotation
A phase shifter PS(phi) on one mode of a dual-rail qubit implements a Z rotation. It adds a relative phase between the |0⟩ and |1⟩ components.
import perceval as pcvl
from perceval.components import PS
from perceval.algorithm import Analyzer
import math
# Apply PS(π) to mode 1 (the |1⟩ rail)
circuit = pcvl.Circuit(2)
circuit.add(1, PS(phi=math.pi)) # phase shift on mode 1 only
processor = pcvl.Processor("SLOS", circuit)
# Analyze: logical |1⟩ = |0,1⟩ should pick up a phase of e^(iπ) = -1
# This is equivalent to the Pauli Z gate: Z|1⟩ = -|1⟩
input_states = [pcvl.BasicState([1, 0]), pcvl.BasicState([0, 1])]
output_states = [pcvl.BasicState([1, 0]), pcvl.BasicState([0, 1])]
analyzer = Analyzer(processor, input_states, output_states)
analyzer.compute(normalize=True)
pcvl.pdisplay(analyzer)
# |1,0⟩ → |1,0⟩ with probability 1 (logical |0⟩ unchanged)
# |0,1⟩ → |0,1⟩ with probability 1 (logical |1⟩ picks up phase -1, invisible in probability)
The phase flip is invisible in the probability distribution because probabilities are the squared magnitudes of amplitudes. To observe the phase, you need to interfere the state with a reference, for example by following the PS with another beam splitter (as in the Mach-Zehnder interferometer).
Process Tomography on the Beam Splitter
Process tomography reconstructs the transfer matrix of an optical component by probing it with a complete set of input states and measuring all output probabilities. For a 2-mode component, the computational basis inputs are |0,0⟩, |1,0⟩, |0,1⟩, and |1,1⟩.
import perceval as pcvl
from perceval.components import BS
from perceval.algorithm import Analyzer
circuit = pcvl.Circuit(2)
circuit.add(0, BS())
processor = pcvl.Processor("SLOS", circuit)
# Probe with all single-photon basis states
input_states = [pcvl.BasicState([1, 0]), pcvl.BasicState([0, 1])]
output_states = [pcvl.BasicState([1, 0]), pcvl.BasicState([0, 1])]
analyzer = Analyzer(processor, input_states, output_states)
analyzer.compute(normalize=True)
pcvl.pdisplay(analyzer)
# Expected probability table:
# |1,0⟩ |0,1⟩
# |1,0⟩ 0.5 0.5
# |0,1⟩ 0.5 0.5
#
# Both inputs split 50/50, confirming the beam splitter is balanced.
The probability table tells you the modulus squared of each matrix element. To recover the full complex unitary (including phases), use the compute_unitary() method directly, or use Perceval’s built-in ProcessTomography class from perceval.algorithm for more sophisticated reconstruction when working with noisy processors.
import numpy as np
# Direct unitary extraction
U = circuit.compute_unitary()
print("Unitary matrix:")
pcvl.pdisplay(U)
# Verify unitarity: U† U = I
U_np = np.array(U)
print("U†U =", np.round(U_np.conj().T @ U_np, 6))
# Should print the 2x2 identity matrix
Multi-Photon Boson Sampling
Boson sampling is the application where photonic quantum computers first demonstrated a computational advantage. The core idea: inject n single photons into an m-mode linear optical network described by a random unitary, then measure the output photon distribution. Computing the probability of any specific output requires evaluating the permanent of an n x n submatrix of the unitary, and the matrix permanent is #P-hard to compute classically.
A 3-Photon Boson Sampling Example
import perceval as pcvl
from perceval.algorithm import Sampler
import numpy as np
from scipy.stats import unitary_group
# Generate a Haar-random 6x6 unitary matrix
np.random.seed(42)
random_U = unitary_group.rvs(6)
# Build a circuit from the unitary using Perceval's decomposition
circuit = pcvl.Circuit.decomposition(
pcvl.Matrix(random_U),
pcvl.Circuit(2), # base component for decomposition
shape="triangle" # Reck (triangular) decomposition
)
# Create processor and inject 3 photons into the first 3 modes
processor = pcvl.Processor("SLOS", circuit)
input_state = pcvl.BasicState([1, 1, 1, 0, 0, 0]) # 3 photons in 6 modes
processor.with_input(input_state)
# Sample the output distribution
sampler = Sampler(processor)
result = sampler.sample_count(10000)
print("Output distribution (top 10 outcomes):")
for state, count in sorted(result.items(), key=lambda x: -x[1])[:10]:
print(f" {state}: {count}")
The Reck decomposition breaks the random unitary into a triangular mesh of beam splitters and phase shifters. For an m-mode unitary, this requires m(m-1)/2 beam splitters and up to m phase shifters.
Why does this matter? In 2020, the Jiuzhang experiment (using a different photonic approach with squeezed states) and in 2021, Xanadu’s Borealis demonstrated sampling tasks that would take classical supercomputers thousands of years. Quandela’s approach with single photons and programmable linear optical networks targets the same class of problems.
Boson Sampling Verification: The Heavy Output Test
After running a boson sampling experiment, how do you verify that the device produced correct quantum samples? One approach is the heavy output generation (HOG) test.
The idea: for a Haar-random circuit, compute the exact output probabilities using the Analyzer. Sort the outputs by probability and identify the “heavy” outputs (those with above-median probability). A genuine quantum device produces heavy outputs more than 2/3 of the time, while a classical random sampler produces them only about 1/2 of the time.
import perceval as pcvl
from perceval.algorithm import Analyzer, Sampler
import numpy as np
from scipy.stats import unitary_group
# Build a small boson sampling instance (2 photons, 4 modes)
np.random.seed(123)
random_U = unitary_group.rvs(4)
circuit = pcvl.Circuit.decomposition(
pcvl.Matrix(random_U),
pcvl.Circuit(2),
shape="triangle"
)
processor = pcvl.Processor("SLOS", circuit)
input_state = pcvl.BasicState([1, 1, 0, 0])
processor.with_input(input_state)
# Step 1: Compute exact probabilities with the Analyzer
# Use output_states="*" to enumerate all valid output states
output_states = [
pcvl.BasicState([2, 0, 0, 0]),
pcvl.BasicState([0, 2, 0, 0]),
pcvl.BasicState([0, 0, 2, 0]),
pcvl.BasicState([0, 0, 0, 2]),
pcvl.BasicState([1, 1, 0, 0]),
pcvl.BasicState([1, 0, 1, 0]),
pcvl.BasicState([1, 0, 0, 1]),
pcvl.BasicState([0, 1, 1, 0]),
pcvl.BasicState([0, 1, 0, 1]),
pcvl.BasicState([0, 0, 1, 1]),
]
analyzer = Analyzer(processor, [input_state], output_states)
analyzer.compute(normalize=True)
# Extract probabilities from the distribution matrix
probs = {}
for i, out_state in enumerate(output_states):
col_idx = analyzer.col(out_state)
if col_idx is not None:
prob = float(analyzer.distribution[0][col_idx])
probs[out_state] = prob
# Step 2: Find the median probability
median_prob = np.median(list(probs.values()))
# Step 3: Identify heavy outputs
heavy_outputs = {s for s, p in probs.items() if p >= median_prob}
print(f"Median probability: {median_prob:.4f}")
print(f"Heavy outputs: {len(heavy_outputs)} of {len(probs)}")
# Step 4: Sample and compute the heavy output fraction
sampler = Sampler(processor)
samples = sampler.sample_count(5000)
heavy_count = sum(count for state, count in samples.items() if state in heavy_outputs)
total_count = sum(samples.values())
heavy_fraction = heavy_count / total_count
print(f"Heavy output fraction: {heavy_fraction:.3f}")
print(f"Expected for quantum: > 0.667, for classical random: ~0.5")
The CNOT Gate Challenge: Why Two-Qubit Gates Are Hard
Linear optics preserves total photon number. A CNOT gate needs to flip the target qubit (move a photon from one rail to the other) conditioned on the state of the control qubit. But photons do not interact with each other in a linear optical medium. Beam splitters and phase shifters transform mode amplitudes independently of how many photons are in other modes. This is the fundamental obstacle for photonic quantum computing.
The KLM (Knill-Laflamme-Milburn) protocol, published in 2001, showed that universal quantum computation is possible with linear optics alone, but at a cost:
- Ancilla photons: A CNOT gate requires 2 additional ancilla photons prepared in specific states.
- Measurement-based feed-forward: After the gate operation, 2 of the 4 photons are measured. The measurement outcomes determine whether the gate succeeded.
- Success probability: The basic KLM CNOT succeeds with probability 1/4. When it fails, the computation must be restarted or the error must be corrected.
- Teleportation boosting: By using more ancilla photons and offline-prepared entangled states, the success probability can be boosted closer to 1, but this requires a large overhead in photon resources.
The resource count for a single KLM CNOT: 4 photons total (2 data + 2 ancilla), 2 post-selected Bell-state measurements, and roughly 6 beam splitters. Scaling this to hundreds of CNOT gates is the central engineering challenge for photonic quantum computing.
Perceval provides implementations of the KLM CNOT and other probabilistic gates in its component library. In practice, Quandela’s hardware uses a combination of probabilistic gates and active multiplexing to improve effective success rates.
Modeling Photon Loss
In real photonic hardware, photons are lost through absorption, scattering, and imperfect coupling at every optical component. Photon loss is the dominant noise source in photonic quantum computing, and it scales with circuit depth (more components means more loss).
Perceval models loss using the LC (Loss Channel) component, which is equivalent to a beam splitter that couples the signal mode to a virtual “loss” mode.
import perceval as pcvl
from perceval.components import BS
from perceval.components import catalog
from perceval.algorithm import Analyzer
# Build a HOM circuit with loss on both input modes
circuit = pcvl.Circuit(2)
circuit.add(0, pcvl.LC(0.1)) # 10% loss on mode 0
circuit.add(1, pcvl.LC(0.1)) # 10% loss on mode 1
circuit.add(0, BS()) # 50/50 beam splitter
processor = pcvl.Processor("SLOS", circuit)
processor.with_input(pcvl.BasicState([1, 1]))
# With loss, some photons are absorbed before reaching the BS
# The output distribution now includes states with fewer total photons
input_states = [pcvl.BasicState([1, 1])]
output_states = [
pcvl.BasicState([2, 0]),
pcvl.BasicState([1, 1]),
pcvl.BasicState([0, 2]),
pcvl.BasicState([1, 0]), # one photon lost
pcvl.BasicState([0, 1]), # one photon lost
pcvl.BasicState([0, 0]), # both photons lost
]
analyzer = Analyzer(processor, input_states, output_states)
analyzer.compute(normalize=False) # don't normalize, to see absolute probabilities
pcvl.pdisplay(analyzer)
# With 10% loss per mode:
# - |1,1⟩ output is no longer perfectly zero (the HOM dip degrades)
# - Single-photon outputs |1,0⟩ and |0,1⟩ appear (partial loss events)
# - The HOM visibility drops from 100% to roughly 81%
The HOM visibility is defined as V = 1 - P(|1,1⟩) / P_classical(|1,1⟩). With perfect indistinguishability and no loss, V = 1. Loss degrades the visibility because a lost photon is effectively a “distinguishable” event: the detector cannot tell whether the photon was lost or took a different path. For loss rate η per mode, the HOM visibility scales approximately as V ≈ (1 - η)^2.
Quandela Ascella Hardware
The Ascella processor is Quandela’s cloud-accessible photonic quantum computer. Key specifications:
- Photon source: Semiconductor quantum dot emitting single photons on demand, with indistinguishability > 95%
- Photonic chip: Silicon-based, with integrated beam splitters and thermo-optic phase shifters
- Scale: Up to 6 input photons across 12 optical modes
- Chip insertion loss: Approximately 3-5 dB (meaning roughly 50-70% of photons survive the chip)
- Reconfiguration: Phase shifters are voltage-controlled and can be reprogrammed between runs
Running on Quandela Hardware
The same Perceval code runs on Quandela’s Ascella processor by swapping the backend to a RemoteProvider. This requires an account at cloud.quandela.com and an active API token. Real hardware results include photon loss and imperfect indistinguishability not present in simulation.
import perceval as pcvl
# Connect to the Quandela cloud
provider = pcvl.RemoteProvider(token="YOUR_TOKEN")
# List available processors
for proc_name in provider.available_processors():
print(proc_name)
# Use the cloud simulator (mimics Ascella behavior)
processor = provider.get_processor("sim:ascella")
processor.with_input(pcvl.BasicState([1, 0]))
sampler = pcvl.algorithm.Sampler(processor)
result = sampler.sample_count(200)
print(result)
To run on the actual hardware (not the simulator), replace "sim:ascella" with "qpu:ascella". Hardware jobs are queued and may take longer to complete. The RemoteProvider requires an active internet connection for all operations.
Common Mistakes
These are the pitfalls that trip up most newcomers to photonic quantum computing with Perceval.
1. Confusing Modes with Qubits
A dual-rail qubit uses 2 modes. A 4-qubit circuit needs at least 8 modes. If you create pcvl.Circuit(4) and expect 4 qubits, you only have 2 dual-rail qubits. Always double-check your mode count.
2. Misinterpreting Multi-Photon Fock States
States like |2,0⟩ (two photons in one mode) are perfectly valid physical states in linear optics. They appear naturally in the HOM effect. But in dual-rail encoding, |2,0⟩ does not correspond to any computational basis state. It represents a “bunched” state outside the qubit subspace. Any circuit that produces significant |n,0⟩ or |0,n⟩ amplitudes with n > 1 is leaking out of the computational basis.
3. Using the Sampler When the Analyzer Suffices
If you need exact probabilities for verification, use the Analyzer. The Sampler introduces statistical noise from finite sampling, and you may need thousands of shots to distinguish a probability of 0.0 from 0.001. The Analyzer gives you the exact answer in one call.
4. Beam Splitter Convention Mismatch
Perceval’s default BS() uses the Ry convention: [[cos(θ/2), sin(θ/2)], [-sin(θ/2), cos(θ/2)]] with θ=π for a 50/50 split. This differs from the Hadamard convention [[1, 1], [1, -1]] / sqrt(2) used by some textbooks and other frameworks. If you are porting a circuit from another framework, explicitly check the BS convention by printing circuit.compute_unitary() and comparing the matrix entries.
You can specify a different convention explicitly:
from perceval.components import BS, BSConvention
bs_hadamard = BS(convention=BSConvention.H) # Hadamard convention
bs_rx = BS(convention=BSConvention.Rx) # Rx convention with imaginary entries
5. Forgetting RemoteProvider Requirements
pcvl.RemoteProvider requires both an internet connection and a valid Quandela account token. The token expires periodically. If your cloud jobs fail silently, check your token validity first.
Next Steps
This tutorial covered the foundations: linear optics physics, the HOM effect, dual-rail encoding, boson sampling, loss modeling, and hardware access. Here are paths forward:
- Perceval reference: The Perceval Reference covers the full component library, all simulation backends, and advanced Analyzer usage.
- KLM protocol: The official Quandela documentation at perceval.quandela.net includes a detailed tutorial on implementing the KLM CNOT gate.
- Boson sampling deep dive: Explore larger instances with 4+ photons and verify results against classical permanent calculations.
- Quantum algorithms on photonic hardware: Implement Grover’s search or variational algorithms using Perceval’s dual-rail encoding and the catalog of pre-built gates.
- Noise characterization: Use process tomography to benchmark real Ascella hardware and quantify how photon loss and imperfect indistinguishability affect algorithm fidelity.
Was this tutorial helpful?