Maestro 0.2.5
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.


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. Maestro supports several backends; the most common CPU options are shown here.

Statevector Simulation

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

Matrix Product State (MPS)

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

result = maestro.simple_execute(
qasm,
simulator_type=maestro.SimulatorType.QCSim,
simulation_type=maestro.SimulationType.MatrixProductState,
max_bond_dimension=64,
singular_value_threshold=1e-10,
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;
"""
result = maestro.simple_execute(
clifford_qasm,
simulator_type=maestro.SimulatorType.QCSim,
simulation_type=maestro.SimulationType.Stabilizer,
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:

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

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"],
simulator_type=maestro.SimulatorType.QCSim,
simulation_type=maestro.SimulationType.Statevector
)
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

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)
fidelity = qc.mirror_fidelity(
simulator_type=maestro.SimulatorType.QCSim,
simulation_type=maestro.SimulationType.MatrixProductState,
shots=10000,
max_bond_dimension=64,
)
# 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)
overlap = qc1.inner_product(
qc2,
simulator_type=maestro.SimulatorType.QCSim,
simulation_type=maestro.SimulationType.MatrixProductState,
max_bond_dimension=64,
)

Parameters

Parameter Type Default Description
circuit_1 / self QuantumCircuit First circuit (bra state)
circuit_2 / other QuantumCircuit Second circuit (ket state)
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
Note
Measurements in either circuit are automatically skipped — only unitary gate operations contribute to the inner product.

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;
"""
result = maestro.simple_execute(
qasm,
simulator_type=maestro.SimulatorType.QuestSim,
simulation_type=maestro.SimulationType.Statevector,
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];
"""
result = maestro.simple_estimate(
bell, "ZZ;XX;YY",
simulator_type=maestro.SimulatorType.QuestSim,
simulation_type=maestro.SimulationType.Statevector
)
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()
result = qc.execute(
simulator_type=maestro.SimulatorType.QuestSim,
simulation_type=maestro.SimulationType.Statevector,
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
result = maestro.simple_execute(
qasm,
simulator_type=maestro.SimulatorType.Gpu,
simulation_type=maestro.SimulationType.Statevector,
shots=1000
)
print(result["counts"])
# GPU MPS
result_mps = maestro.simple_execute(
qasm,
simulator_type=maestro.SimulatorType.Gpu,
simulation_type=maestro.SimulationType.MatrixProductState,
max_bond_dimension=64,
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];
"""
result = maestro.simple_estimate(
bell, "ZZ;XX;YY",
simulator_type=maestro.SimulatorType.Gpu,
simulation_type=maestro.SimulationType.Statevector
)
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()
result = qc.execute(
simulator_type=maestro.SimulatorType.Gpu,
simulation_type=maestro.SimulationType.Statevector,
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)")
result = maestro.simple_execute(qasm, simulator_type=sim_type, 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