|
Maestro 0.2.11
Unified interface for quantum circuit simulation
|
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.
Install pre-built wheels from PyPI (Linux, macOS, Windows):
Or build from source from the repository root:
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.
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.
| 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 |
The fastest way to run a circuit is maestro.simple_execute. Pass an OpenQASM 2.0 string and get measurement counts back immediately.
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 |
You can explicitly select the simulator type and simulation method via a SimulatorConfig.
MPS enables simulation of circuits with hundreds of qubits when entanglement is bounded.
Ultra-fast simulation for circuits that only use Clifford gates (H, S, CX, X, Y, Z).
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.
Use maestro.simple_estimate to compute expectation values of Pauli observables without measurements in the circuit.
You can also use a specific backend via config:
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.
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×** |
| 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 |
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 |
For a more Pythonic workflow (similar to Qiskit), use the QuantumCircuit class to build circuits programmatically without writing QASM strings.
| 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) | — |
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.
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.
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.
The NoiseModel class defines per-qubit Pauli noise channels. Each qubit can have independent X, Y, and Z error probabilities.
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.
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.
This is the recommended approach for QML training with realistic noise. The cost is noise_realizations * noiseless_time. Works with any backend:
noisy_execute is the shot-based counterpart: it injects gate-by-gate noise and returns measurement counts rather than expectation values.
| 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 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:
Coherent Estimate — the coherent analogue of noisy_estimate_montecarlo:
Coherent Execute — the shot-based counterpart:
| 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 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⟩.
| 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 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.
| Method | Description |
|---|---|
| nm.set_crosstalk(q1, q2, strength) | Symmetric ZZ coupling |
| nm.has_crosstalk() | Check if crosstalk is configured |
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.
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 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.
| 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 |
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:
These are in addition to the base set_depolarizing() channel, which applies after all gates regardless of type.
Typical usage — matching IBM backend calibration data:
| 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 |
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ᵢ)
| 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 |
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.
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:
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:
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.
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:
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:
| 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 |
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:
This enables realistic device-level simulation with a single noise model.
Combined Execute (shot-based):
Combined Estimate (expectation values):
Noisy Fidelity (MPS-native, no shots):
Computes |⟨ψ_ideal|ψ_noisy⟩|² averaged over noise realizations. Uses inner_product — O(N·χ²) cost, no 2^N statevector needed.
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 |
| 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.
| 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? |
For full control over the simulation lifecycle, use the Maestro class directly to create, configure, and destroy simulators.
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.
QuEST only supports Statevector simulation. Requesting any other simulation type (MPS, Stabilizer, etc.) will raise an error.
The programmatic QuantumCircuit API works with QuEST too:
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.
Unlike QuEST, the GPU backend supports multiple simulation types including Statevector, MPS, Tensor Network, and Pauli Propagation.
| SimulationType | GPU Support |
|---|---|
| Statevector | ✅ |
| MatrixProductState | ✅ |
| TensorNetwork | ✅ |
| PauliPropagator | ✅ |
| Stabilizer | ❌ |
| ExtendedStabilizer | ❌ |
For portable scripts that should work with or without GPU:
| 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) |
| 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 |