Maestro 0.2.11
Unified interface for quantum circuit simulation
Loading...
Searching...
No Matches
Python Guide

Overview

Maestro ships high-performance Python bindings built with nanobind. The bindings expose the full simulation pipeline — circuit construction, backend selection, execution, and expectation-value estimation — in a Pythonic API.

Installation

Install pre-built wheels from PyPI (Linux, macOS, Windows):

pip install qoro-maestro

Or build from source from the repository root:

pip install .

Supported platforms (pre-built wheels):

Platform Architecture Python
Linux x86_64 3.10, 3.11, 3.12
macOS arm64 (Apple Silicon) 3.10, 3.11, 3.12
Windows AMD64 3.10, 3.11, 3.12

That's it — you're ready to import maestro and run quantum simulations.


SimulatorConfig — Shared Configuration

All execution and estimation functions accept a SimulatorConfig object that bundles every simulator knob into a single, reusable value. Create one config and pass it to every call — no need to repeat simulator_type, simulation_type, max_bond_dimension, etc.

import maestro
# Create once, reuse everywhere
config = maestro.SimulatorConfig(
simulator_type=maestro.SimulatorType.QCSim,
simulation_type=maestro.SimulationType.MatrixProductState,
max_bond_dimension=64,
singular_value_threshold=1e-10,
)
result = maestro.simple_execute(qasm, config=config, shots=1024)
estimate = maestro.simple_estimate(qasm, "ZZ;XX", config=config)
fidelity = maestro.mirror_fidelity(qc, config=config, shots=10000)

SimulatorConfig Parameters

Parameter Type Default Description
simulator_type SimulatorType QCSim Simulation backend
simulation_type SimulationType Statevector Simulation method
max_bond_dimension int or None None MPS bond dimension limit
singular_value_threshold float or None None MPS truncation threshold
use_double_precision bool False GPU double precision flag
disable_optimized_swapping bool False Disable MPS swap optimization
lookahead_depth int -1 Swap optimization lookahead depth
mps_measure_no_collapse bool True Use probability-based MPS sampling
Note
When no config is passed, a default SimulatorConfig() is used (QCSim + Statevector with all defaults). You can also modify fields after construction:
config = maestro.SimulatorConfig()
config.simulation_type = maestro.SimulationType.MatrixProductState
config.max_bond_dimension = 128

Quick Start — One-Line Execution

The fastest way to run a circuit is maestro.simple_execute. Pass an OpenQASM 2.0 string and get measurement counts back immediately.

import maestro
qasm = """
OPENQASM 2.0;
include "qelib1.inc";
qreg q[2];
creg c[2];
h q[0];
cx q[0], q[1];
measure q -> c;
"""
result = maestro.simple_execute(qasm, shots=1024)
print(result["counts"]) # e.g. {"00": 512, "11": 512}
print(result["simulator"]) # e.g. "QCSim"
print(result["method"]) # e.g. "Statevector"
print(f"{result['time_taken']:.4f}s")

What's in the result dict?

Key Type Description
counts dict[str, int] Measurement outcome → count
simulator int Backend enum value (see SimulatorType)
method int Simulation method enum value (see SimulationType)
time_taken float Wall-clock time in seconds

Choosing a Simulation Backend

You can explicitly select the simulator type and simulation method via a SimulatorConfig.

Statevector Simulation

config = maestro.SimulatorConfig(
simulator_type=maestro.SimulatorType.QCSim,
simulation_type=maestro.SimulationType.Statevector,
)
result = maestro.simple_execute(qasm, config=config, shots=2000)

Matrix Product State (MPS)

MPS enables simulation of circuits with hundreds of qubits when entanglement is bounded.

mps_config = maestro.SimulatorConfig(
simulator_type=maestro.SimulatorType.QCSim,
simulation_type=maestro.SimulationType.MatrixProductState,
max_bond_dimension=64,
singular_value_threshold=1e-10,
)
result = maestro.simple_execute(qasm, config=mps_config, shots=1000)
print(result["method"]) # "MatrixProductState"

Stabilizer (Clifford-only)

Ultra-fast simulation for circuits that only use Clifford gates (H, S, CX, X, Y, Z).

clifford_qasm = """
OPENQASM 2.0;
include "qelib1.inc";
qreg q[100];
creg c[100];
h q[0];
cx q[0], q[1];
cx q[1], q[2];
measure q -> c;
"""
stab_config = maestro.SimulatorConfig(
simulator_type=maestro.SimulatorType.QCSim,
simulation_type=maestro.SimulationType.Stabilizer,
)
result = maestro.simple_execute(clifford_qasm, config=stab_config, shots=10000)

Available Backends

The table below shows which simulation types are supported by each backend.

SimulationType QCSim Qiskit Aer GPU QuEST
Statevector
MatrixProductState
Stabilizer
TensorNetwork
PauliPropagator
ExtendedStabilizer

Distributed execution: QCSim and Qiskit Aer support p-block composite simulation via CompositeQCSim / CompositeQiskitAer. QuEST natively supports MPI-distributed statevector simulation.

Note
Qiskit Aer support is optional and requires building with AER_INCLUDE_DIR. GPU and QuEST are dynamically-loaded libraries — see the dedicated sections below.

Expectation Values

Use maestro.simple_estimate to compute expectation values of Pauli observables without measurements in the circuit.

import maestro
# Prepare a GHZ state
ghz = """
OPENQASM 2.0;
include "qelib1.inc";
qreg q[3];
h q[0];
cx q[0], q[1];
cx q[1], q[2];
"""
# Estimate multiple Pauli observables at once (semicolon-separated)
result = maestro.simple_estimate(ghz, "ZZZ;XXX;IZI")
obs = result["expectation_values"]
print(f"<ZZZ> = {obs[0]:.4f}") # 1.0 (parity conserved)
print(f"<XXX> = {obs[1]:.4f}") # 1.0
print(f"<IZI> = {obs[2]:.4f}") # 0.0 (maximally mixed marginal)

You can also use a specific backend via config:

mps_config = maestro.SimulatorConfig(
simulator_type=maestro.SimulatorType.QCSim,
simulation_type=maestro.SimulationType.MatrixProductState,
max_bond_dimension=8,
)
result = maestro.simple_estimate(ghz, "ZZ;XX", config=mps_config)

Incremental Time Evolution

maestro.incremental_evolve performs Trotterized time evolution with expectation value measurements at multiple intermediate time points, using a single persistent simulator instance. This avoids the cost of re-simulating from scratch at each measurement point.

Motivation

When simulating time evolution, you often want to measure observables at multiple time points. The naïve approach builds a fresh circuit for each measurement point (init + k Trotter steps), paying O(Σ step_i) total gate cost. For n measurement points evenly spaced over N total steps, this is O(n × N / 2) — roughly n/2 times the cost of a single full evolution.

incremental_evolve keeps the MPS state alive between measurements and evolves forward incrementally. Total cost: O(N) regardless of how many measurement points you request.

Approach Total Steps Executed Example (N=300, 20 points)
Fresh circuit per point Σ step_i ≈ n×N/2 ~3,000
incremental_evolve N 300
Speedup **~10×**

Usage

import maestro
from maestro.circuits import QuantumCircuit
# 1. Build the initial state preparation circuit
init = QuantumCircuit()
init.x(0) # e.g. excite qubit 0
# 2. Build a SINGLE Trotter step circuit
step = QuantumCircuit()
step.rx(0, 0.1)
step.rx(1, 0.2)
step.cx(0, 1)
step.rz(1, 0.05)
step.cx(0, 1)
# 3. Specify which step indices to measure at
measure_at = [10, 20, 30, 50, 100]
# 4. Run incremental evolution
result = maestro.incremental_evolve(
init_circuit=init,
trotter_step=step,
measure_at_steps=measure_at,
observables=["ZI", "IZ", "ZZ"],
config=maestro.SimulatorConfig(
simulation_type=maestro.SimulationType.MatrixProductState,
max_bond_dimension=64,
),
)
# 5. Access results
for i, s in enumerate(result["steps"]):
evs = result["expectation_values"][i]
print(f"Step {s}: <ZI>={evs[0]:.4f} <IZ>={evs[1]:.4f} <ZZ>={evs[2]:.4f}")
print(f"Total time: {result['time_taken']:.2f}s")

Parameters

Parameter Type Default Description
init_circuit QuantumCircuit State preparation circuit (executed once)
trotter_step QuantumCircuit Single Trotter step circuit (applied repeatedly)
measure_at_steps list[int] Step indices at which to compute expectation values
observables list[str] or str Pauli strings to measure (list or ;-separated)
config SimulatorConfig SimulatorConfig() Simulator configuration

Return Value

Returns a dict with:

Key Type Description
expectation_values list[list[float]] Outer list = measurement points, inner list = one value per observable
steps list[int] Step indices where measurements were taken (sorted)
time_taken float Wall-clock time for the entire evolution (seconds)
simulator int Backend enum value
method int Simulation method enum value

Implementation Notes

  • The function creates a single simulator and drives it directly via ExecuteNonMeasurements(), bypassing the usual per-call create/destroy cycle.
  • Expectation values are computed via ExpectationValue() which is non-destructive for MPS — the state is not modified by measurement.
  • Measurement steps are automatically sorted; duplicates are ignored.
  • The GIL is released during gate execution for maximum parallelism.
  • Circuit optimization is disabled to preserve gate ordering.
  • Works with all simulation backends (Statevector, MPS, TensorNetwork, etc.). The MPS backend benefits the most from this approach.
Note
This function is designed for Trotterized time evolution workflows where the same step circuit is applied repeatedly. For general stateful simulation (branching, checkpointing, parameter sweeps), a more general stateful simulator API may be added in a future release.

QuantumCircuit Builder

For a more Pythonic workflow (similar to Qiskit), use the QuantumCircuit class to build circuits programmatically without writing QASM strings.

Building and Running a Circuit

from maestro.circuits import QuantumCircuit
qc = QuantumCircuit()
# Build a Bell state
qc.h(0)
qc.cx(0, 1)
qc.measure([(0, 0), (1, 1)]) # (qubit, classical_bit) pairs
# Execute with defaults
result = qc.execute(shots=1000)
print(result["counts"]) # {"00": ~500, "11": ~500}

Available Gates

Gate Method Parameters
Pauli-X qc.x(qubit)
Pauli-Y qc.y(qubit)
Pauli-Z qc.z(qubit)
Hadamard qc.h(qubit)
S qc.s(qubit)
S† qc.sdg(qubit)
T qc.t(qubit)
T† qc.tdg(qubit)
√X qc.sx(qubit)
√X† qc.sxdg(qubit)
K qc.k(qubit)
Phase qc.p(qubit, λ) λ (radians)
Rx qc.rx(qubit, θ) θ (radians)
Ry qc.ry(qubit, θ) θ (radians)
Rz qc.rz(qubit, θ) θ (radians)
U qc.u(qubit, θ, φ, λ) 3 angles
CNOT qc.cx(ctrl, tgt)
CY qc.cy(ctrl, tgt)
CZ qc.cz(ctrl, tgt)
CH qc.ch(ctrl, tgt)
CSX qc.csx(ctrl, tgt)
CSX† qc.csxdg(ctrl, tgt)
SWAP qc.swap(q1, q2)
CP qc.cp(ctrl, tgt, λ) λ
CRx qc.crx(ctrl, tgt, θ) θ
CRy qc.cry(ctrl, tgt, θ) θ
CRz qc.crz(ctrl, tgt, θ) θ
CU qc.cu(ctrl, tgt, θ, φ, λ, γ) 4 angles
Toffoli qc.ccx(c1, c2, tgt)
Fredkin qc.cswap(ctrl, q1, q2)

Measurements

# Measure specific qubits to specific classical bits
qc.measure([(0, 0), (1, 1), (2, 2)])
# Or measure all qubits at once
qc.measure_all()

Expectation Values with QuantumCircuit

qc = QuantumCircuit()
qc.h(0)
qc.cx(0, 1)
# No measurements needed for estimation
result = qc.estimate(observables=["ZZ", "XX", "YY"])
vals = result["expectation_values"]
print(f"<ZZ> = {vals[0]:.4f}") # 1.0
print(f"<XX> = {vals[1]:.4f}") # 1.0
print(f"<YY> = {vals[2]:.4f}") # -1.0
# With a specific backend config
mps_config = maestro.SimulatorConfig(
simulation_type=maestro.SimulationType.MatrixProductState,
max_bond_dimension=16,
)
result = qc.estimate(observables=["ZZ", "XX"], config=mps_config)

Mirror Fidelity

Mirror fidelity measures how well a circuit "undoes itself". Maestro constructs the mirror circuit by appending the adjoint (inverse) of every gate in reverse order and returns P(|0…0⟩) — the probability of returning to the initial state. A value of 1.0 indicates a perfect simulation.

This is useful for benchmarking simulator accuracy, characterising noise from approximate methods (e.g. MPS with low bond dimension), and for circuit validation.

By default, mirror fidelity uses shot-based sampling (1024 shots). For exact results on small circuits, pass full_amplitude=True.

Module-Level Function

import maestro
from maestro.circuits import QuantumCircuit
qc = QuantumCircuit()
qc.h(0)
qc.cx(0, 1)
qc.rx(0, 3.14159 / 4)
# Default: shot-based (1024 shots)
fidelity = maestro.mirror_fidelity(qc)
print(f"Mirror fidelity: {fidelity:.4f}") # ~1.0
# More shots for tighter estimate
fidelity = maestro.mirror_fidelity(qc, shots=10000)
# Exact (small circuits only)
fidelity = maestro.mirror_fidelity(qc, full_amplitude=True)

Circuit Method

qc = QuantumCircuit()
qc.h(0)
qc.cx(0, 1)
qc.s(0)
fidelity = qc.mirror_fidelity()
print(f"Mirror fidelity: {fidelity:.4f}") # ~1.0
# With MPS (scales to 50+ qubits)
mps_config = maestro.SimulatorConfig(
simulator_type=maestro.SimulatorType.QCSim,
simulation_type=maestro.SimulationType.MatrixProductState,
max_bond_dimension=64,
)
fidelity = qc.mirror_fidelity(config=mps_config, shots=10000)
# Exact mode (small circuits only)
fidelity = qc.mirror_fidelity(full_amplitude=True)
Note
Measurements in the original circuit are automatically skipped when building the mirror circuit — only unitary gate operations are mirrored. All gate types are supported: self-inverse gates (H, X, CX, etc.), paired gates (S↔S†, T↔T†, √X↔√X†), and parametric gates (Rx, Ry, Rz, U, CP, CRx, CRy, CRz, CU).

Inner Product

The inner product computes ⟨ψ₁|ψ₂⟩ between two circuits' output states, where |ψᵢ⟩ = Uᵢ|0⟩. Internally, Maestro builds the combined circuit U₂ followed by U₁† and evaluates ⟨0|U₁†U₂|0⟩ via the efficient ProjectOnZero operation — this is particularly fast with the MPS backend as it avoids constructing the full statevector.

The result is a complex number: magnitude gives the overlap (fidelity when squared), and phase captures relative phase information.

Module-Level Function

import maestro
from maestro.circuits import QuantumCircuit
# Two identical circuits → overlap = 1.0
qc1 = QuantumCircuit()
qc1.h(0)
qc1.cx(0, 1)
qc2 = QuantumCircuit()
qc2.h(0)
qc2.cx(0, 1)
overlap = maestro.inner_product(qc1, qc2)
print(f"<psi1|psi2> = {overlap}") # (1+0j)
print(f"|<psi1|psi2>| = {abs(overlap)}") # 1.0

Orthogonal States

# |+> and |-> are orthogonal
qc_plus = QuantumCircuit()
qc_plus.h(0)
qc_minus = QuantumCircuit()
qc_minus.x(0)
qc_minus.h(0)
overlap = maestro.inner_product(qc_plus, qc_minus)
print(f"|<+|->| = {abs(overlap):.10f}") # 0.0

Circuit Method

qc1 = QuantumCircuit()
qc1.h(0)
qc1.cx(0, 1)
qc2 = QuantumCircuit()
qc2.h(0)
qc2.cx(0, 1)
# Equivalent to maestro.inner_product(qc1, qc2)
overlap = qc1.inner_product(qc2)
print(f"|overlap| = {abs(overlap):.4f}") # 1.0
# With MPS backend (scales to large qubit counts)
mps_config = maestro.SimulatorConfig(
simulator_type=maestro.SimulatorType.QCSim,
simulation_type=maestro.SimulationType.MatrixProductState,
max_bond_dimension=64,
)
overlap = qc1.inner_product(qc2, config=mps_config)
Note
Measurements in either circuit are automatically skipped — only unitary gate operations contribute to the inner product.

Noise Simulation

Maestro provides a comprehensive noise simulation suite with multiple approaches, each targeting a different trade-off between speed and accuracy:

Function Overhead Accuracy Best For
noisy_estimate Zero Per-qubit Pauli damping Fast ansatz screening
noisy_estimate_montecarlo N × noiseless Gate-by-gate accurate Training with realistic noise
noisy_execute N × noiseless Gate-by-gate + shot noise Shot-based workflows
coherent_estimate N × noiseless Coherent rotation errors Coherent error analysis
coherent_execute N × noiseless Coherent + shot noise Shot-based coherent noise
full_noise_execute N × noiseless All layers combined Realistic device simulation
full_noise_estimate N × noiseless All layers combined Hardware-accurate estimation
noisy_fidelity N × inner_product All layers combined MPS-native fidelity

All noise functions accept a config parameter for backend selection.

Creating a Noise Model

The NoiseModel class defines per-qubit Pauli noise channels. Each qubit can have independent X, Y, and Z error probabilities.

import maestro
nm = maestro.NoiseModel()
# Depolarizing noise: equal X, Y, Z error rates (p/3 each)
nm.set_depolarizing(qubit=0, p=0.01)
# Set the same depolarizing rate on all qubits at once
nm.set_all_depolarizing(num_qubits=5, p=0.005)
# Dephasing noise: only Z errors (T2 relaxation)
nm.set_dephasing(qubit=1, p=0.02)
# Bit-flip noise: only X errors
nm.set_bit_flip(qubit=2, p=0.01)
# Custom Pauli channel: specify px, py, pz independently
nm.set_qubit_noise(qubit=3, px=0.005, py=0.002, pz=0.01)

Analytical Noisy Estimation (Zero Overhead)

noisy_estimate runs a single noiseless simulation and analytically damps each expectation value based on the noise model. This is exact for a single-layer Pauli channel and provides a fast first-order approximation.

from maestro.circuits import QuantumCircuit
qc = QuantumCircuit()
qc.h(0)
qc.cx(0, 1)
nm = maestro.NoiseModel()
nm.set_all_depolarizing(2, 0.05)
result = maestro.noisy_estimate(qc, ['ZZ', 'XX', 'YY'], nm)
print(result['expectation_values']) # Noise-attenuated values
print(result['ideal_expectation_values']) # Noiseless reference
print(result['time_taken']) # Same as noiseless
Note
The analytical approach does not capture depth-dependent noise accumulation — a 100-layer circuit and a 1-layer circuit with the same qubits get the same damping. For depth-accurate noise, use noisy_estimate_montecarlo.

Gate-by-gate Monte Carlo Estimation (Accurate)

noisy_estimate_montecarlo injects random Pauli errors after every gate, runs noiseless estimation on each noisy circuit, and averages the results. This captures how noise compounds through the circuit depth.

result = maestro.noisy_estimate_montecarlo(
qc, ['ZZ', 'XX'], nm,
noise_realizations=200, # More samples = less variance
seed=42 # For reproducibility
)
print(result['expectation_values']) # Gate-by-gate accurate
print(result['ideal_expectation_values']) # Noiseless reference
print(result['noise_realizations']) # 200

This is the recommended approach for QML training with realistic noise. The cost is noise_realizations * noiseless_time. Works with any backend:

mps_config = maestro.SimulatorConfig(
simulation_type=maestro.SimulationType.MatrixProductState,
max_bond_dimension=64,
)
result = maestro.noisy_estimate_montecarlo(
qc, ['ZZ'], nm,
noise_realizations=100,
config=mps_config,
seed=42
)

Monte Carlo Noisy Execution (Shot-based)

noisy_execute is the shot-based counterpart: it injects gate-by-gate noise and returns measurement counts rather than expectation values.

qc = QuantumCircuit()
qc.h(0)
qc.cx(0, 1)
qc.measure_all()
nm = maestro.NoiseModel()
nm.set_all_depolarizing(2, 0.05)
result = maestro.noisy_execute(
qc, nm,
shots=1024,
noise_realizations=64,
seed=42
)
print(result['counts']) # Aggregated counts across all realizations

Noise Simulation Parameters

Parameter Type Default Description
noise_realizations int 100 / 64 Independent noise samples
seed int or None None RNG seed for reproducibility
config SimulatorConfig SimulatorConfig() Simulator configuration

Coherent Noise Simulation

Coherent noise models systematic calibration errors — e.g. over/under-rotation of gates — rather than stochastic Pauli errors. Instead of randomly inserting X, Y, Z gates, coherent noise injects deterministic rotation gates (Rx, Ry, Rz) after every gate in the circuit.

The key difference from Pauli noise:

  • Pauli noise averages out over shots (each shot sees a different random error)
  • Coherent noise is deterministic within a single noise realization — all shots from one circuit see the same systematic error. The ± sign of the rotation is sampled per-realization.
nm = maestro.NoiseModel()
# From a depolarizing probability: epsilon = 2*arcsin(sqrt(p))
nm.set_coherent_depolarizing(qubit=0, p=0.01)
# Apply to all qubits at once
nm.set_all_coherent_depolarizing(num_qubits=5, p=0.001)
# Convenience alias
nm.set_coherent_strength(num_qubits=5, p=0.001)
# Per-axis control: explicit rotation angles (radians)
nm.set_coherent_rotation(qubit=0, rx=0.01, ry=0.0, rz=0.05)
# Dephasing-like: Z-axis rotation only
nm.set_coherent_dephasing(qubit=1, p=0.02)
# Bit-flip-like: X-axis rotation only
nm.set_coherent_bit_flip(qubit=2, p=0.01)
# Check if coherent noise is configured
print(nm.has_coherent()) # True

Coherent Estimate — the coherent analogue of noisy_estimate_montecarlo:

result = maestro.coherent_estimate(
qc, ['ZZ', 'XX', 'YY'], nm,
noise_realizations=200,
seed=42
)
print(result['expectation_values']) # Coherent-noisy values
print(result['ideal_expectation_values']) # Noiseless reference
print(result['noise_type']) # 'coherent'

Coherent Execute — the shot-based counterpart:

result = maestro.coherent_execute(
qc, nm,
shots=1024,
noise_realizations=64,
seed=42
)
print(result['counts']) # Aggregated counts
print(result['noise_type']) # 'coherent'
Note
Coherent noise uses rotation gates, so it requires a simulation backend that supports continuous rotations — MPS, Statevector, or TensorNetwork. Stabilizer simulation will not work.
Method Description
nm.set_coherent_depolarizing(q, p) Rz angle from depolarizing probability
nm.set_coherent_dephasing(q, p) Rz rotation from dephasing probability
nm.set_coherent_bit_flip(q, p) Rx rotation from bit-flip probability
nm.set_coherent_rotation(q, rx, ry, rz) Explicit per-axis angles (radians)
nm.set_all_coherent_depolarizing(n, p) Uniform coherent noise on all qubits
nm.set_all_coherent_dephasing(n, p) Uniform coherent dephasing on all qubits
nm.set_coherent_strength(n, p) Alias for set_all_coherent_depolarizing
nm.has_coherent() Check if any coherent noise is configured

T1 Amplitude Damping

T1 amplitude damping models energy relaxation — the spontaneous decay from |1⟩ to |0⟩ over time. Maestro implements this using the quantum trajectory method: after each gate, with probability γ, the qubit is probabilistically reset to |0⟩.

nm = maestro.NoiseModel()
# Set per-gate decay probability directly
nm.set_t1(qubit=0, gamma=0.001)
# Set uniform T1 on all qubits
nm.set_all_t1(num_qubits=5, gamma=0.0003)
# Compute gamma from physical time constants (most accurate)
# gamma = 1 - exp(-gate_time / T1)
nm.set_t1_from_time(qubit=0, gate_time_s=500e-9, t1_time_s=200e-6)
# Check if T1 is configured
print(nm.has_t1()) # True
Note
T1 damping is asymmetric — it only decays |1⟩ → |0⟩, never the reverse. This is physically correct: thermal relaxation at millikelvin temperatures overwhelmingly favours the ground state.
Method Description
nm.set_t1(q, gamma) Per-gate T1 decay probability
nm.set_all_t1(n, gamma) Uniform T1 on all qubits
nm.set_t1_from_time(q, gate_time_s, t1_time_s) T1 from physical time constants
nm.has_t1() Check if T1 is configured

ZZ Crosstalk

ZZ crosstalk models parasitic coupling between neighbouring qubits. When a gate acts on qubit q1, the spectator qubit q2 accumulates an unwanted Rz rotation. This is one of the dominant error sources on superconducting devices.

nm = maestro.NoiseModel()
# Set symmetric ZZ coupling between qubits 0 and 1
# After a gate on q0, Rz(strength) is applied on q1, and vice versa
nm.set_crosstalk(q1=0, q2=1, strength=0.005)
# Set nearest-neighbour crosstalk on a chain
for i in range(n_qubits - 1):
nm.set_crosstalk(i, i + 1, 0.008)
print(nm.has_crosstalk()) # True
Method Description
nm.set_crosstalk(q1, q2, strength) Symmetric ZZ coupling
nm.has_crosstalk() Check if crosstalk is configured

Readout Error

Readout error models classical measurement errors — the probability of reporting the wrong bit value when measuring a qubit. On real hardware, readout errors are asymmetric: the false-positive rate P(1|0) is typically much lower than the false-negative rate P(0|1).

Maestro applies readout error as a post-measurement classical channel: after all quantum noise and measurement, each bit is independently flipped according to its readout error rates.

nm = maestro.NoiseModel()
# Asymmetric readout error (matches IBM backend properties)
# P(measure 1 | prepared 0) = 0.3%
# P(measure 0 | prepared 1) = 6.0%
nm.set_readout_error(qubit=0, p_meas1_prep0=0.003, p_meas0_prep1=0.06)
# Symmetric readout error (same rate both directions)
nm.set_readout_error_symmetric(qubit=1, p_error=0.01)
# Apply uniform symmetric readout error to all qubits
nm.set_all_readout_error(num_qubits=5, p_error=0.02)
print(nm.has_readout_error()) # True

Readout error is applied automatically by noisy_execute and full_noise_execute. For path integral queries, use qc.noisy_prob() which applies an analytic first-order readout correction.

Method Description
nm.set_readout_error(q, p10, p01) Asymmetric readout error
nm.set_readout_error_symmetric(q, p) Symmetric readout error
nm.set_all_readout_error(n, p) Uniform symmetric readout on all qubits
nm.has_readout_error() Check if readout error is configured

Two-Qubit Depolarizing

Two-qubit depolarizing models correlated errors on qubit pairs after two-qubit gates (CX, CZ, etc.). The channel applies one of 15 non-identity two-qubit Pauli operators with equal probability p/15:

Λ(ρ) = (1−p)ρ + (p/15) Σ_{P ∈ {I,X,Y,Z}⊗2 \ {II}} PρP†

This is separate from per-qubit depolarizing — it captures the joint error that occurs specifically during two-qubit interactions.

nm = maestro.NoiseModel()
# Set two-qubit depolarizing for a specific qubit pair
# Stored symmetrically: set_2q_depolarizing(0,1,p) == set_2q_depolarizing(1,0,p)
nm.set_2q_depolarizing(q1=0, q2=1, p=1.8e-3)
# Typical IBM Heron 2Q error rates
nm.set_2q_depolarizing(0, 1, 4.5e-3)
nm.set_2q_depolarizing(1, 2, 3.2e-3)
print(nm.has_any_2q_depolarizing()) # True
Note
Two-qubit depolarizing is applied only after 2-qubit gates on the specified qubit pair. It is injected in addition to any per-qubit noise.
Method Description
nm.set_2q_depolarizing(q1, q2, p) Correlated 2Q depolarizing channel
nm.has_any_2q_depolarizing() Check if any 2Q depolarizing is set

Gate-Type-Specific Noise

Real hardware has different error rates for single-qubit and two-qubit gates. Gate-type-specific noise lets you set separate depolarizing rates that are applied only after the corresponding gate type:

  • 1Q gate noise: applied after single-qubit gates (H, X, Rx, etc.)
  • 2Q gate noise: applied after two-qubit gates (CX, CZ, etc.)

These are in addition to the base set_depolarizing() channel, which applies after all gates regardless of type.

nm = maestro.NoiseModel()
# Per-qubit depolarizing after 1Q gates only
nm.set_1q_gate_depolarizing(qubit=0, p=2.3e-4)
# Per-qubit depolarizing after 2Q gates only
nm.set_2q_gate_depolarizing(qubit=0, p=1.8e-3)
# Bulk: apply to all qubits at once
nm.set_all_1q_gate_depolarizing(num_qubits=5, p=2.3e-4)
nm.set_all_2q_gate_depolarizing(num_qubits=5, p=1.8e-3)
print(nm.has_1q_gate_noise()) # True
print(nm.has_2q_gate_noise()) # True

Typical usage — matching IBM backend calibration data:

nm = maestro.NoiseModel()
# From IBM backend.properties():
# Average 1Q gate error: ~2.3e-4
# Average 2Q gate error: ~1.8e-3
n = 127 # IBM Eagle/Heron qubit count
nm.set_all_1q_gate_depolarizing(n, 2.3e-4)
nm.set_all_2q_gate_depolarizing(n, 1.8e-3)
# Combine with readout error for full device model
nm.set_all_readout_error(n, 0.01)
Method Description
nm.set_1q_gate_depolarizing(q, p) Depolarizing after 1Q gates only
nm.set_2q_gate_depolarizing(q, p) Depolarizing after 2Q gates only
nm.set_all_1q_gate_depolarizing(n, p) Bulk 1Q gate noise on all qubits
nm.set_all_2q_gate_depolarizing(n, p) Bulk 2Q gate noise on all qubits
nm.has_1q_gate_noise() Check if 1Q gate noise is set
nm.has_2q_gate_noise() Check if 2Q gate noise is set

Readout-Corrected Path Integral Probability

The qc.noisy_prob() method computes the probability of a target state with analytic readout error correction via the path integral simulator. This avoids Monte Carlo sampling entirely — it uses a first-order expansion that requires only n+1 path integral evaluations (the target state plus n single-bit-flipped variants):

P_noisy(b) ≈ Π(1−pᵢ)·P(b) + Σᵢ pᵢ·Π_{j≠i}(1−pⱼ)·P(b⊕eᵢ)

from maestro.circuits import QuantumCircuit
qc = QuantumCircuit()
qc.h(0)
qc.cx(0, 1)
nm = maestro.NoiseModel()
nm.set_readout_error(0, 0.003, 0.06)
nm.set_readout_error(1, 0.005, 0.04)
result = qc.noisy_prob('11', nm)
print(result['probability']) # Readout-corrected probability
print(result['target_state']) # '11'
print(result['has_readout_error']) # True
print(result['time_taken']) # seconds
Key Type Description
probability float Readout-corrected P(target)
target_state str The queried bitstring
has_readout_error bool Whether correction was applied
time_taken float Computation time in seconds
Note
When no readout error is configured on the noise model, noisy_prob returns the exact noiseless probability (equivalent to qc.prob()).

Correlated (Time-Correlated) Noise

Correlated noise models non-Markovian dephasing — noise that has temporal correlations between successive gates, as found on real superconducting hardware. This is implemented as an Ornstein-Uhlenbeck → AR(1) process:

After each gate, a dephasing rotation Rz(y[k]) is injected, where:

y[k] = Ω · y[k-1] + η[k],   η ~ N(0, σ_η²)

The per-qubit AR(1) state is maintained across the circuit, giving time-correlated (coloured) noise rather than white noise.

nm = maestro.NoiseModel()
# From Ornstein-Uhlenbeck parameters (recommended)
nm.set_correlated_ou(qubit=0, sigma=15.0, alpha=0.5, gate_time=100e-9)
# Set on all qubits at once
nm.set_all_correlated_ou(num_qubits=20, sigma=15.0,
alpha=0.5, gate_time=100e-9)
# From total noise power P_tot = N·σ²·π·α·t_g
nm.set_all_correlated_from_power(num_qubits=20, power=1e-3,
alpha=0.5, gate_time=100e-9)
# Direct AR(1) coefficients
nm.set_correlated_ar1(qubit=0, phi=0.135, sigma_eta=2.35e-3)

set_correlated_ou(qubit, sigma, alpha, gate_time, after_1q=True, after_2q=True)

Set correlated noise on a single qubit from Ornstein-Uhlenbeck (OU) process parameters. The continuous OU process dX = −θ·X·dt + σ·dW is discretized into the AR(1) form used during simulation:

θ        = 1 / (α · gate_time)
Ω (phi)  = exp(−θ · gate_time)
σ_η²     = (σ² / 2θ) · (1 − Ω²)

Parameters:

  • qubit (int): Qubit index.
  • sigma (float): OU diffusion coefficient (noise strength). Larger values produce stronger dephasing.
  • alpha (float): Correlation time in gate-time units (τ_corr / t_g). Values > 1 give long-range correlations; values < 1 approach white noise.
  • gate_time (float): Gate duration in seconds (e.g. 100e-9 for 100 ns).
  • after_1q (bool, default True): Inject noise after single-qubit gates.
  • after_2q (bool, default True): Inject noise after two-qubit gates.
nm = maestro.NoiseModel()
nm.set_correlated_ou(0, sigma=15.0, alpha=0.5, gate_time=100e-9)

set_correlated_ar1(qubit, phi, sigma_eta, after_1q=True, after_2q=True)

Set AR(1) correlated dephasing on a single qubit using direct AR(1) coefficients. After every gate, Rz(y[k]) is injected where:

y[k] = phi · y[k-1] + η[k],   η ~ N(0, σ_η²)

Parameters:

  • qubit (int): Qubit index.
  • phi (float): AR(1) autoregressive coefficient (0 < phi < 1).
  • sigma_eta (float): Driving noise standard deviation.
  • after_1q (bool, default True): Inject noise after single-qubit gates.
  • after_2q (bool, default True): Inject noise after two-qubit gates.
nm = maestro.NoiseModel()
nm.set_correlated_ar1(0, phi=0.135, sigma_eta=2.35e-3)

set_all_correlated_ou(num_qubits, sigma, alpha, gate_time, after_1q=True, after_2q=True)

Set identical OU correlated noise on qubits [0, num_qubits). Convenience wrapper that calls set_correlated_ou on each qubit with the same parameters.

nm = maestro.NoiseModel()
nm.set_all_correlated_ou(20, sigma=15.0, alpha=0.5, gate_time=100e-9)

set_all_correlated_from_power(num_qubits, power, alpha, gate_time, after_1q=True, after_2q=True)

Set correlated noise on all qubits from a total noise power budget. Derives the OU diffusion coefficient σ from:

P_tot = N · σ² · π · α · t_g   →   σ = sqrt(P_tot / (N · π · α · t_g))

Parameters:

  • num_qubits (int): Number of qubits N.
  • power (float): Total noise power P_tot.
  • alpha (float): Correlation time in gate-time units.
  • gate_time (float): Gate duration in seconds.
  • after_1q (bool, default True): Inject noise after single-qubit gates.
  • after_2q (bool, default True): Inject noise after two-qubit gates.
nm = maestro.NoiseModel()
nm.set_all_correlated_from_power(20, power=1e-3, alpha=0.5, gate_time=100e-9)

Injection granularity flags:

All setters accept optional after_1q (default: True) and after_2q (default: True) flags to control which gate types trigger noise injection:

# Only inject after 2Q gates (common simplification)
nm.set_all_correlated_ou(20, sigma=15.0, alpha=0.5,
gate_time=100e-9,
after_1q=False, after_2q=True)
# Inject after all gates (paper's approach)
nm.set_all_correlated_ou(20, sigma=15.0, alpha=0.5,
gate_time=100e-9,
after_1q=True, after_2q=True)
Method Description
nm.set_correlated_ar1(q, phi, sigma_eta) Direct AR(1) parameters
nm.set_correlated_ou(q, sigma, alpha, gate_time) From OU parameters
nm.set_all_correlated_ou(n, sigma, alpha, gate_time) OU on all qubits
nm.set_all_correlated_from_power(n, P, alpha, gate_time) From noise power
nm.has_correlated() Check if correlated noise is set

Combined Noise Simulation

The full_noise_execute, full_noise_estimate, and noisy_fidelity functions apply all configured noise layers in a single call, using Monte Carlo averaging. The noise is injected per gate in physical order:

  1. Correlated dephasing (time-correlated Rz via AR(1) per qubit)
  2. Coherent over-rotations (systematic Rx/Ry/Rz)
  3. ZZ crosstalk (Rz on spectator neighbours)
  4. T1 amplitude damping (probabilistic reset to |0⟩)
  5. Pauli noise (stochastic X/Y/Z errors)

This enables realistic device-level simulation with a single noise model.

# Build a noise model matching your hardware
nm = maestro.NoiseModel()
# Layer 1: Time-correlated dephasing
nm.set_all_correlated_ou(n_qubits, sigma=15.0, alpha=0.5,
gate_time=100e-9)
# Layer 2: Coherent gate over-rotations
nm.set_all_coherent_depolarizing(n_qubits, 0.005)
# Layer 3: ZZ crosstalk between neighbours
for i in range(n_qubits - 1):
nm.set_crosstalk(i, i + 1, 0.008)
# Layer 4: T1 amplitude damping
for q in range(n_qubits):
nm.set_t1_from_time(q, gate_time_s=500e-9, t1_time_s=200e-6)
# Layer 5: Pauli (incoherent) noise
nm.set_all_depolarizing(n_qubits, 0.005)
# Check if any noise is configured
print(nm.has_any()) # True

Combined Execute (shot-based):

qc = QuantumCircuit()
qc.h(0)
qc.cx(0, 1)
qc.measure_all()
result = qc.full_noise_execute(
nm, shots=1024,
noise_realizations=64, seed=42
)
print(result['counts']) # Aggregated counts
print(result['noise_type']) # 'combined'

Combined Estimate (expectation values):

qc = QuantumCircuit()
qc.h(0)
qc.cx(0, 1)
result = qc.full_noise_estimate(
['ZZ', 'XX'], nm,
noise_realizations=100, seed=42
)
print(result['expectation_values']) # Noisy values
print(result['ideal_expectation_values']) # Noiseless reference
print(result['noise_type']) # 'combined'

Noisy Fidelity (MPS-native, no shots):

Computes |⟨ψ_ideal|ψ_noisy⟩|² averaged over noise realizations. Uses inner_product — O(N·χ²) cost, no 2^N statevector needed.

qc = QuantumCircuit()
qc.h(0)
qc.cx(0, 1)
result = qc.noisy_fidelity(
nm, noise_realizations=200, seed=42
)
print(result['fidelity']) # Mean |⟨ψ_ideal|ψ_noisy⟩|²
print(result['infidelity']) # 1 - fidelity
print(result['std_error']) # Standard error of the mean

noisy_fidelity return dict:

Key Type Description
fidelity float Mean fidelity across noise realizations
infidelity float 1 − fidelity
std_error float Standard error of fidelity estimate
time_taken float Wall-clock seconds
noise_realizations int Number of noise samples used

All Noise Functions

Function Noise Type Overhead Best For
noisy_estimate Pauli Zero Fast ansatz screening
noisy_estimate_montecarlo Pauli N × noiseless Training with realistic noise
noisy_execute Pauli N × noiseless Shot-based Pauli noise
coherent_estimate Coherent N × noiseless Coherent error analysis
coherent_execute Coherent N × noiseless Shot-based coherent noise
full_noise_execute All layers N × noiseless Realistic device simulation
full_noise_estimate All layers N × noiseless Hardware-accurate estimation
noisy_fidelity All layers N × inner_product MPS-native fidelity
qc.noisy_prob(target, nm) Readout O(n) PI evals Path integral readout correction

All noise functions are also available as bound methods on QuantumCircuit: qc.noisy_execute(nm), qc.coherent_execute(nm), qc.full_noise_execute(nm), qc.noisy_fidelity(nm), qc.noisy_prob(target, nm), etc.

All NoiseModel Methods

Method Category Description
nm.set_depolarizing(q, p) Pauli Symmetric depolarizing (px=py=pz=p/3)
nm.set_dephasing(q, p) Pauli Pure phase-flip (Z only)
nm.set_bit_flip(q, p) Pauli Pure bit-flip (X only)
nm.set_qubit_noise(q, px, py, pz) Pauli Custom Pauli channel
nm.set_all_depolarizing(n, p) Pauli Uniform depolarizing on all qubits
nm.set_all_dephasing(n, p) Pauli Uniform dephasing on all qubits
nm.set_1q_gate_depolarizing(q, p) Gate-specific Depolarizing after 1Q gates only
nm.set_2q_gate_depolarizing(q, p) Gate-specific Depolarizing after 2Q gates only
nm.set_all_1q_gate_depolarizing(n, p) Gate-specific Bulk 1Q gate noise
nm.set_all_2q_gate_depolarizing(n, p) Gate-specific Bulk 2Q gate noise
nm.set_2q_depolarizing(q1, q2, p) 2Q correlated Correlated 2Q Pauli channel
nm.set_readout_error(q, p10, p01) Readout Asymmetric readout error
nm.set_readout_error_symmetric(q, p) Readout Symmetric readout error
nm.set_all_readout_error(n, p) Readout Uniform readout error
nm.set_coherent_depolarizing(q, p) Coherent Rz from depolarizing probability
nm.set_coherent_dephasing(q, p) Coherent Rz from dephasing probability
nm.set_coherent_bit_flip(q, p) Coherent Rx from bit-flip probability
nm.set_coherent_rotation(q, rx, ry, rz) Coherent Explicit rotation angles
nm.set_all_coherent_depolarizing(n, p) Coherent Uniform coherent noise
nm.set_coherent_strength(n, p) Coherent Alias for above
nm.set_correlated_ar1(q, phi, sigma_eta) Correlated Direct AR(1) parameters
nm.set_correlated_ou(q, sigma, alpha, gt) Correlated From OU parameters
nm.set_all_correlated_ou(n, sigma, alpha, gt) Correlated OU on all qubits
nm.set_all_correlated_from_power(n, P, alpha, gt) Correlated From total noise power
nm.set_t1(q, gamma) T1 Per-gate decay probability
nm.set_all_t1(n, gamma) T1 Uniform T1 decay
nm.set_t1_from_time(q, t_gate, t1) T1 T1 from physical constants
nm.set_crosstalk(q1, q2, strength) Crosstalk Symmetric ZZ coupling
nm.has_any() Query Any noise configured?
nm.has_correlated() Query Correlated noise configured?

Advanced: Object-Oriented Simulator Control

For full control over the simulation lifecycle, use the Maestro class directly to create, configure, and destroy simulators.

import maestro
m = maestro.Maestro()
# Create a simulator handle
# Defaults to QCSim + MatrixProductState
handle = m.create_simulator(
maestro.SimulatorType.QCSim,
maestro.SimulationType.Statevector
)
# Get the raw simulator object
sim = m.get_simulator(handle)
# Clean up when done
m.destroy_simulator(handle)
Note
The Maestro class is primarily used for handle-based lifecycle management. For most Python workflows, the QuantumCircuit API or the simple_execute / simple_estimate convenience functions are recommended.

QuEST Backend

Maestro supports the QuEST simulator as an alternative CPU backend. QuEST is loaded dynamically as a shared library (libmaestroquest), so it is available only when the library has been built and installed.

QuEST natively supports MPI-distributed statevector simulation, enabling execution across multiple nodes for larger qubit counts.

Checking Availability

import maestro
# Check if the QuEST library is installed
if maestro.is_quest_available():
print("QuEST is available")
else:
print("QuEST not found — install libmaestroquest")
# Explicitly initialise QuEST (optional — done automatically on first use)
maestro.init_quest()

Running a Circuit on QuEST

QuEST only supports Statevector simulation. Requesting any other simulation type (MPS, Stabilizer, etc.) will raise an error.

qasm = """
OPENQASM 2.0;
include "qelib1.inc";
qreg q[2];
creg c[2];
h q[0];
cx q[0], q[1];
measure q -> c;
"""
quest_config = maestro.SimulatorConfig(
simulator_type=maestro.SimulatorType.QuestSim,
simulation_type=maestro.SimulationType.Statevector,
)
result = maestro.simple_execute(qasm, config=quest_config, shots=1000)
print(result["counts"]) # {"00": ~500, "11": ~500}

Expectation Values with QuEST

bell = """
OPENQASM 2.0;
include "qelib1.inc";
qreg q[2];
h q[0];
cx q[0], q[1];
"""
quest_config = maestro.SimulatorConfig(
simulator_type=maestro.SimulatorType.QuestSim,
)
result = maestro.simple_estimate(bell, "ZZ;XX;YY", config=quest_config)
vals = result["expectation_values"]
print(f"<ZZ> = {vals[0]:.4f}") # 1.0
print(f"<XX> = {vals[1]:.4f}") # 1.0
print(f"<YY> = {vals[2]:.4f}") # -1.0

QuantumCircuit with QuEST

The programmatic QuantumCircuit API works with QuEST too:

from maestro.circuits import QuantumCircuit
qc = QuantumCircuit()
qc.h(0)
qc.cx(0, 1)
qc.measure_all()
quest_config = maestro.SimulatorConfig(
simulator_type=maestro.SimulatorType.QuestSim,
)
result = qc.execute(config=quest_config, shots=100)
print(result["counts"])
Note
QuEST only supports SimulationType.Statevector. Passing MatrixProductState, Stabilizer, TensorNetwork, or PauliPropagator will raise an exception: "QuestSim only supports Statevector".

GPU Backend

Maestro supports GPU-accelerated simulation via a dynamically-loaded CUDA library (libmaestro_gpu_simulators). Like QuEST, the GPU backend is optional and loaded at runtime only when requested.

Note
The GPU backend is not included in the open-source version of Maestro. Contact Qoro Quantum for access.

Unlike QuEST, the GPU backend supports multiple simulation types including Statevector, MPS, Tensor Network, and Pauli Propagation.

Checking Availability

import maestro
# Check if the GPU library is installed
if maestro.is_gpu_available():
print("GPU backend is available")
else:
print("GPU not found — install libmaestro_gpu_simulators")
# Explicitly initialise GPU (optional — done automatically on first use)
maestro.init_gpu()

Running a Circuit on GPU

import maestro
maestro.init_gpu()
qasm = """
OPENQASM 2.0;
include "qelib1.inc";
qreg q[2];
creg c[2];
h q[0];
cx q[0], q[1];
measure q -> c;
"""
# GPU Statevector
gpu_sv_config = maestro.SimulatorConfig(
simulator_type=maestro.SimulatorType.Gpu,
simulation_type=maestro.SimulationType.Statevector,
)
result = maestro.simple_execute(qasm, config=gpu_sv_config, shots=1000)
print(result["counts"])
# GPU MPS
gpu_mps_config = maestro.SimulatorConfig(
simulator_type=maestro.SimulatorType.Gpu,
simulation_type=maestro.SimulationType.MatrixProductState,
max_bond_dimension=64,
)
result_mps = maestro.simple_execute(qasm, config=gpu_mps_config, shots=1000)
print(result_mps["counts"])

Expectation Values on GPU

bell = """
OPENQASM 2.0;
include "qelib1.inc";
qreg q[2];
h q[0];
cx q[0], q[1];
"""
gpu_config = maestro.SimulatorConfig(
simulator_type=maestro.SimulatorType.Gpu,
)
result = maestro.simple_estimate(bell, "ZZ;XX;YY", config=gpu_config)
vals = result["expectation_values"]
print(f"<ZZ> = {vals[0]:.4f}") # 1.0
print(f"<XX> = {vals[1]:.4f}") # 1.0
print(f"<YY> = {vals[2]:.4f}") # -1.0

QuantumCircuit with GPU

from maestro.circuits import QuantumCircuit
maestro.init_gpu()
qc = QuantumCircuit()
qc.h(0)
qc.cx(0, 1)
qc.measure_all()
gpu_config = maestro.SimulatorConfig(
simulator_type=maestro.SimulatorType.Gpu,
)
result = qc.execute(config=gpu_config, shots=2048)
print(result["counts"])

Supported Simulation Types

SimulationType GPU Support
Statevector
MatrixProductState
TensorNetwork
PauliPropagator
Stabilizer
ExtendedStabilizer

Fallback Pattern

For portable scripts that should work with or without GPU:

import maestro
# Pick the best available backend
if maestro.init_gpu():
sim_type = maestro.SimulatorType.Gpu
print("Using GPU")
elif maestro.init_quest():
sim_type = maestro.SimulatorType.QuestSim
print("Using QuEST")
else:
sim_type = maestro.SimulatorType.QCSim
print("Using CPU (QCSim)")
config = maestro.SimulatorConfig(simulator_type=sim_type)
result = maestro.simple_execute(qasm, config=config, shots=1000)
print(result["counts"])

Enumerations Reference

SimulatorType

Value Description Dynamic?
SimulatorType.QCSim QCSim CPU simulator No (built-in)
SimulatorType.CompositeQCSim Composite (distributed) CPU simulator No (built-in)
SimulatorType.QuestSim QuEST simulator Yes (libmaestroquest)
SimulatorType.Gpu GPU-accelerated simulator (requires CUDA) Yes (libmaestro_gpu_simulators)

SimulationType

Value Description Supported By
SimulationType.Statevector Full statevector simulation QCSim, Aer, GPU, QuEST
SimulationType.MatrixProductState MPS / tensor-train simulation QCSim, Aer, GPU
SimulationType.Stabilizer Clifford stabiliser simulation QCSim, Aer
SimulationType.TensorNetwork Tensor network contraction QCSim, Aer, GPU
SimulationType.PauliPropagator Pauli frame propagation QCSim, GPU
SimulationType.ExtendedStabilizer Extended stabiliser decomposition Aer only