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:

  1. Merge the two tensors

  2. Apply the gate

  3. Perform a singular value decomposition (SVD)

  4. 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 with Helios-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