MPS Simulator¶
Note
The MPS simulator is only available on the cloud distribution of Selene. Users require nexus access to consume time on the MPS simulator. This notebook demonstrated a 100-qubit brickwork program emulation with a MPS simulator.
For problems using 32-qubits or less, users should attempt brute-force (statevector) simulation prior to MPS.
For \chi values less than 256, Users should attempt MPS simulation on CPU, prior to GPU.
This document provides a minimal explanation of Matrix Product State (MPS) simulators, focusing on how a quantum circuit defines and is represented as a wavefunction, and how MPS enables scalable simulation.
A quantum program defines a wavefunction with exponentially many amplitudes. Brute Force methods (statevector) are limited to 32-qubits.
MPS factorizes this wavefunction into local tensors. Efficiency depends on entanglement, not qubit count alone. MPS simulators enable scalable quantum program emulation when entanglement is limited. Bond dimension \(\chi\) controls the accuracy-performance tradeoff.
Please see simulator guidance documentation on limitations and guidance for both bruteforce and MPS simulators.
Quantum Circuits and the Wavefunction¶
An \((n)\)-qubit quantum circuit defines a sequence of unitary operations applied to an initial quantum state, commonly \(\lvert \psi_0 \rangle = \lvert 0 0 \dots 0 \rangle\).
After applying the circuit unitary \(U\), the system is described by the wavefunction, \(\lvert \psi \rangle = U \lvert \psi_0 \rangle = \sum_{i_1,\dots,i_n \in \{0,1\}} c_{i_1 \dots i_n} \lvert i_1 \dots i_n \rangle\). The coefficients \(c_{i_1 \dots i_n}\) are complex amplitudes that fully specify the quantum state.
Exponential Scaling of the Statevector¶
A full wavefunction representation requires storing \(2^n\) amplitudes.
Memory scaling: \(O(2^n)\)
Runtime scaling: \(O(2^n)\) for many operations
This exponential cost limits statevector simulation to relatively small numbers of qubits.
Motivation for MPS Representation¶
Many quantum circuits of practical interest generate limited or structured entanglement, even when the number of qubits is large.
Matrix Product States exploit this by factorizing the wavefunction into a chain of local tensors that captures only the entanglement that is physically present.
Matrix Product State Form¶
In an MPS representation, the wavefunction is written as, \(\lvert \psi \rangle = \sum_{i_1,\dots,i_n} A^{[1]}_{i_1} A^{[2]}_{i_2} \dots A^{[n]}_{i_n} \lvert i_1 \dots i_n \rangle\).
Where:
\(A^{[k]}_{i_k}\) is a tensor associated with qubit \(k\)
Neighboring tensors are contracted along internal indices
The internal index size is the bond dimension, \(\chi\)
At the boundaries, tensors reduce to vectors.
Bond Dimension and Entanglement¶
The bond dimension \(\chi\):
Controls how much entanglement the MPS can represent
Grows with the Schmidt rank across bipartitions
Determines simulation cost
Bond Dimension |
Interpretation |
|---|---|
\(\chi = 1\) |
Product state (no entanglement) |
Small \(\chi\) |
Weakly entangled states |
Large \(\chi\) |
Strongly entangled states |
If \(\chi\)grows exponentially, MPS loses its efficiency advantage.
Representing a Quantum Circuit with MPS¶
An MPS simulator applies quantum gates directly to the tensor network:
1. Initialization¶
The initial state \(\lvert 0 \dots 0 \rangle\) is represented exactly with bond dimension \(\chi = 1\).
2. Single-Qubit Gates¶
A single-qubit unitary acts locally on one tensor:
\(A^{[k]} \leftarrow U^{[k]} A^{[k]}\)
This operation is efficient and does not change the bond dimension.
3. Two-Qubit Gates¶
For a two-qubit gate acting on neighboring qubits:
Merge the two tensors
Apply the gate
Perform a singular value decomposition (SVD)
Truncate singular values to enforce a maximum \(\chi\)
This step introduces controlled approximation when truncation is applied.
4. Measurement and Output¶
The final MPS represents the circuit’s output wavefunction and is used to compute:
Measurement probabilities
Samples from the output distribution
Accuracy and Performance Tradeoff¶
MPS simulators are exact if no truncation is applied, and approximate otherwise.
Accuracy improves with larger \(\chi\)
Runtime and memory scale as: \(O(n \chi^3)\)
This leads to a tunable tradeoff between accuracy and performance.
When to Use MPS Simulation¶
MPS simulators are well-suited for:
Circuits with limited entanglement growth
One-dimensional or near-local connectivity
Variational algorithms (VQE, shallow QAOA)
Large qubit counts with moderate depth
They are less effective for:
Deep random circuits
Volume-law entanglement
Highly non-local gate patterns
Code Sample¶
This code sample generates a 100-qubit brickwork program with 10 layers. This is uploaded to Nexus and then submitted for noiseless MPS simulation.
from guppylang import guppy
from guppylang.std.builtins import array, result, comptime
from guppylang.std.quantum import qubit, h, cx, t, measure_array
n_qubits = 100
@guppy
def main() -> None:
n_layers = 16
qs = array(qubit() for _ in range(100))
for _ in range(n_layers):
for i in range(comptime(100//2)):
q0 = 2*i
q1 = 2*i+1
h(qs[q0])
h(qs[q1])
cx(qs[q0], qs[q1])
t(qs[q1])
cx(qs[q0], qs[q1])
for i in range(comptime(100//2 - 2)):
q0 = 2*i+1
q1 = 2*i+2
h(qs[q0])
h(qs[q1])
cx(qs[q0], qs[q1])
t(qs[q1])
cx(qs[q0], qs[q1])
result("c", measure_array(qs))
hugr_program = main.compile()
import qnexus as qnx
project = qnx.projects.get_or_create("project_mps")
qnx.context.set_active_project(project)
hugr_ref = qnx.hugr.upload(hugr_program, name="brickwork program", project=project)
The SelenePlus config enables user modification of MPS simulator settings, such as the amount of entanglement from the quantum state that is actually simulated.
The backend is set to
"auto". The other options are"gpu"(only available withHelios-1E) or"cpu". The option “auto” allows nexus to determine the best choice of backend, based on user inputs.This targets x86 CPU architecture.
The
chi(\(\chi\)) parameter is set to 32.The threshold for entanglement cutoff is set to
0.01.
import qnexus as qnx
config = qnx.SelenePlusConfig(
n_qubits=100,
runtime=qnx.models.SimpleRuntime(),
error_model=qnx.models.NoErrorModel(),
simulator=qnx.models.MatrixProductStateSimulator(
backend="auto",
seed=123141,
zero_threshold=0.01,
chi=128,
),
)
selene_lean_job_ref = qnx.start_execute_job(
programs=[hugr_ref],
n_shots=[100],
backend_config=config,
name=f"Brickwork MPS Simulation with {config.n_qubits} qubits",
)
qnx.jobs.wait_for(selene_lean_job_ref, timeout=3600)
result = qnx.jobs.results(selene_lean_job_ref)[0].download_result()
The outcome from the Selene emulator is available as a hugr.result.qsystem.QsysResult object.
counts = result.collated_counts()
The QsysResult object is converted into a counts dictionary with the bitstring outcome as the keys and probability as the value. This can be stored inside a Pandas DataFrame.
import pandas as pd
digitized_counts = {c[0][1]: r/100 for c, r in counts.items() if r > 0}
df = pd.DataFrame(digitized_counts.items(), columns=["Outcome", "Probability"]).sort_values("Probability", ascending=False)
df.head()
| Outcome | Probability | |
|---|---|---|
| 0 | 1111000000000011111111111111111111111111111100... | 0.01 |
| 1 | 1100111100111111110000111111001100111100001111... | 0.01 |
| 2 | 0111111111111100000000000000001111111111000000... | 0.01 |
| 3 | 0011111100000000000000001111111111111111000000... | 0.01 |
| 4 | 0100000000000000000000001111111111111100000011... | 0.01 |