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)

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

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()).

Combined Noise Simulation

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

  1. Coherent over-rotations (systematic Rx/Ry/Rz)
  2. ZZ crosstalk (Rz on spectator neighbours)
  3. T1 amplitude damping (probabilistic reset to |0⟩)
  4. 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: Coherent gate over-rotations
nm.set_all_coherent_depolarizing(n_qubits, 0.005)
# Layer 2: ZZ crosstalk between neighbours
for i in range(n_qubits - 1):
nm.set_crosstalk(i, i + 1, 0.008)
# Layer 3: 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 4: 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'

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
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_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_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?

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