Backends Guide¶
Divi’s execution layer is built around CircuitRunner: every backend exposes the same submission API so programs can swap simulators or the cloud service without changing algorithm code. This guide covers the bundled runners, ExecutionResult, and Qoro job configuration.
Backend Architecture¶
All backends in Divi implement the CircuitRunner interface, providing a consistent API regardless of the underlying execution environment. This powerful abstraction allows you to develop your quantum programs locally and then switch to a different backend, like cloud hardware, with a single line of code.
Understanding ExecutionResult¶
All backend submit_circuits() methods return an ExecutionResult object, which provides a unified interface for handling both synchronous and asynchronous execution.
- Result Format:
The
resultsattribute is a list of dictionaries, each containing:label(str): The circuit label from your input dictionaryresults(dict): The execution results (bitstring counts for sampling mode, or expectation values for expectation mode)
Example:
[ {"label": "circuit_0", "results": {"00": 500, "11": 500}}, {"label": "circuit_1", "results": {"01": 1000}} ]
- For Synchronous Backends (like
MaestroSimulatorandQiskitSimulator): Results are available immediately after submission:
from divi.backends import MaestroSimulator backend = MaestroSimulator() result = backend.submit_circuits({"circuit_0": qasm_string}) # Access results directly for circuit_result in result.results: label = circuit_result["label"] counts = circuit_result["results"] print(f"{label}: {counts}")
- For Asynchronous Backends (like
QoroService): For cloud-based backends, you need to wait for the job to complete and then fetch the results:
from divi.backends import QoroService service = QoroService() result = service.submit_circuits({"circuit_0": qasm_string}) # Wait for the job to complete service.poll_job_status(result, loop_until_complete=True) # Fetch the results completed_result = service.get_job_results(result) # Access the results for circuit_result in completed_result.results: label = circuit_result["label"] counts = circuit_result["results"] print(f"{label}: {counts}")
Note: When using high-level algorithms such as VQE or QAOA, you do not handle ExecutionResult yourself; the circuit pipeline submits circuits and collects results. The examples above are for direct submit_circuits() use.
Available Backends¶
Divi ships three CircuitRunner implementations:
MaestroSimulator— A high-performance local simulator, recommended as the default for development and testing. Supports Pauli-channel noise viamaestro.NoiseModel(see Noisy simulation).QiskitSimulator— A convenience wrapper around Qiskit’sAerSimulatorwith thread-count control. Use this when you need device-calibrated noise from a Qiskit fake backend or an arbitraryqiskit_aer.noise.NoiseModel.QoroService— A cloud-based quantum computing service for accessing powerful simulators and real quantum hardware.
MaestroSimulator¶
MaestroSimulator is the recommended runner for local development, testing, and research. It is powered by Qoro’s C++ quantum simulator (qoro-maestro) and automatically selects between Statevector and MatrixProductState methods based on circuit width.
Key Features:
Native C++ Core: Backed by
qoro-maestro, a compiled simulator designed for low per-circuit overhead.Auto Method Selection: Switches from Statevector to MatrixProductState for circuits exceeding 22 qubits (configurable via
MaestroConfig’smps_qubit_threshold), so a single backend handles both narrow and wide registers.Multiple Simulation Methods: Statevector, MatrixProductState, Stabilizer, ExtendedStabilizer, TensorNetwork, PauliPropagator.
Pauli-channel noise: Analytical exact-mean expectation values or Monte-Carlo sampling under a
maestro.NoiseModel, configured directly onMaestroConfig— see Noisy simulation.
Configuring MaestroSimulator¶
Simulator configuration is carried by a dedicated
MaestroConfig object — the same pattern
QoroService uses with
ExecutionConfig, so the mental model is the same
whether you run locally or in the cloud. Field semantics are documented in
the maestro Python bindings guide,
since each MaestroConfig field maps directly to the
identically-named field on maestro’s SimulatorConfig.
Constructing MaestroSimulator with no config gives you
automatic simulation-method selection: Statevector for narrow circuits, MPS
above mps_qubit_threshold (default 22 qubits) so wide registers do not try
to store a full statevector.
from divi.backends import MaestroSimulator, MaestroConfig
# Default — auto-selects Statevector or MPS based on circuit size
backend = MaestroSimulator()
# Explicit MPS for large circuits
backend = MaestroSimulator(
shots=5000,
config=MaestroConfig(
simulation_type="MatrixProductState",
max_bond_dimension=64,
),
)
Noisy simulation¶
Pass a maestro.NoiseModel via MaestroConfig to
enable Pauli-channel noise.
Building a noise model
Each set_* method on a NoiseModel adds a Pauli channel to the
qubits it touches; calling more than one composes them, it does not
overwrite.
import maestro
# Start with uniform 1 % depolarizing on every qubit of a 2-qubit circuit.
noise_model = maestro.NoiseModel()
noise_model.set_all_depolarizing(num_qubits=2, p=0.01)
# Add stronger dephasing on top, per-qubit (composes with the above).
noise_model.set_dephasing(qubit=0, p=0.005)
noise_model.set_dephasing(qubit=1, p=0.02)
# Add an asymmetric Pauli channel on qubit 0 (also composes).
noise_model.set_qubit_noise(qubit=0, px=0.002, py=0.001, pz=0.003)
Choosing an execution mode
noise_model and
noise_realizations together select
one of five dispatch scenarios across four C++ entry points
(noisy_execute covers two of the rows below):
|
Backend |
Notes |
|---|---|---|
(not set) |
|
No noise applied. |
|
|
Exact analytical mean: applies per-Pauli damping coefficients to noiseless expectation values. Deterministic; zero Monte-Carlo overhead. |
|
|
One random Pauli error pattern per circuit; counts are stochastic.
For statistical accuracy set an explicit count (e.g. |
Positive |
|
N independent Pauli-injection passes; mean of expectation values. Converges to analytical as N → ∞. |
Positive |
|
Total shots is always |
Note
noise_realizations=1 (expval) is not the same as noise_realizations=None:
the former is one random Pauli sampling; the latter is the exact analytical mean.
Analytical noisy expectation values
from divi.backends import MaestroSimulator, MaestroConfig
# noise_realizations defaults to None → analytical noisy_estimate
sim = MaestroSimulator(config=MaestroConfig(noise_model=noise_model))
result = sim.submit_circuits({"c0": qasm_string}, ham_ops="ZI;IZ")
expvals = result.results[0]["results"] # {"ZI": <float>, "IZ": <float>}
Monte Carlo noisy expectation values
sim_mc = MaestroSimulator(
config=MaestroConfig(
noise_model=noise_model,
noise_realizations=20,
noise_seed=42,
)
)
result = sim_mc.submit_circuits({"c0": qasm_string}, ham_ops="ZI;IZ")
expvals = result.results[0]["results"]
Noisy sampling (counts mode)
sim_sample = MaestroSimulator(
shots=5000,
config=MaestroConfig(
noise_model=noise_model,
noise_realizations=10,
noise_seed=0,
),
)
result = sim_sample.submit_circuits({"c0": qasm_string})
counts = result.results[0]["results"]
# 5000 shots distributed across 10 random Pauli error patterns.
For reproducibility under noisy execution see Operational notes below.
QiskitSimulator¶
QiskitSimulator wraps Qiskit’s AerSimulator with thread-count control and Qiskit-native noise configuration. Use it when you need device-calibrated noise from a Qiskit fake backend, or when you have an existing qiskit_aer.noise.NoiseModel you want to run as-is. For Pauli-channel noise written from scratch, MaestroSimulator’s noisy paths are usually faster.
from divi.backends import QiskitSimulator
# Reproducible noisy simulation
backend = QiskitSimulator(
shots=10000,
n_processes=8,
qiskit_backend="auto", # Auto-select a Qiskit fake backend by qubit count
simulation_seed=42 # Deterministic results for debugging
)
# Noisy simulation to mimic real hardware
from qiskit_ibm_runtime.fake_provider import FakeManilaV2
backend = QiskitSimulator(
shots=5000,
qiskit_backend=FakeManilaV2(), # Use a fake backend with a realistic noise model
n_processes=2
)
QoroService¶
QoroService talks to the Qoro cloud API, giving programs access to advanced simulators, tensor-network backends, and real QPUs. It supports two execution modes: sampling mode (measurement counts) and expectation mode (Pauli expectation values, simulation-only).
Two layers of batching
QoroService participates in two complementary batching mechanisms:
Backend-level packing (
use_circuit_packing, enabled by default) — within a singlesubmit_circuitscall, circuits are packed into one cloud job, amortizing the per-job scheduler cost.Ensemble-level merging (
BatchConfigonrun()) — merges submissions from multiple programs into onesubmit_circuitscall. See Circuit Batching.
The two compose: ensemble-level merging yields one large
submit_circuits call per flush, which backend-level packing then sends
as a single cloud job.
Submitting and Monitoring Jobs¶
from divi.backends import QoroService
service = QoroService()
# Sampling mode — submit circuits, poll, fetch results
result = service.submit_circuits({"c0": qasm_string_1, "c1": qasm_string_2})
service.poll_job_status(result, loop_until_complete=True)
completed = service.get_job_results(result)
# [{'label': 'c0', 'results': {'0011': 2000}}, ...]
# Expectation mode — pass ham_ops (semicolon-separated Pauli terms)
result = service.submit_circuits({"c0": qasm_string}, ham_ops="XYZ;XXZ;ZIZ")
service.poll_job_status(result, loop_until_complete=True)
completed = service.get_job_results(result)
# [{'label': 'c0', 'results': {'XYZ': 0.5, 'XXZ': -0.3, 'ZIZ': 1.0}}]
# Cancel a job
service.cancel_job(result)
Note
Bitstring Ordering: QoroService returns bitstrings in Little Endian ordering (least significant bit first, rightmost bit is qubit 0), but Hamiltonian operators passed via the ham_ops parameter should follow Big Endian ordering (most significant bit first, leftmost bit is qubit 0). For example, a 4-qubit system with qubits labeled 0-3: the bitstring "0011" in results represents qubit 0=1, qubit 1=1, qubit 2=0, qubit 3=0 (reading right to left), while the Hamiltonian operator "ZIZI" applies Z to qubit 0, I to qubit 1, Z to qubit 2, and I to qubit 3 (reading left to right).
Configuring Jobs with JobConfig¶
The QoroService uses a JobConfig object to manage settings for job submissions. You can configure it in two ways:
Default Configuration: Set a default
JobConfigwhen you initialize the service. This configuration will apply to all jobs unless you override it.Override Configuration: For a specific job, you can provide an
override_job_configto thesubmit_circuitsmethod.
from divi.backends import QoroService, JobConfig
# 1. Set a custom default configuration for the service
default_config = JobConfig(
shots=500,
simulator_cluster="qoro_maestro",
use_circuit_packing=True,
tag="default_run"
)
service = QoroService(job_config=default_config)
# 2. Override the default configuration for a single job
override = JobConfig(shots=2000, tag="high_shot_run")
execution_result = service.submit_circuits(circuits, override_job_config=override)
# This job will run with 2000 shots and the tag 'high_shot_run',
# but will still use 'qoro_maestro' and circuit packing from the default config.
You can also update the service’s default configuration after construction:
from divi.backends import ExecutionConfig, JobConfig
# Update the service's default job configuration
service.job_config = JobConfig(shots=2000, simulator_cluster="qoro_maestro")
# Update the service's default execution configuration
service.execution_config = ExecutionConfig(bond_dimension=512)
The job_config setter automatically resolves string target names and
defaults to the qoro_maestro simulator cluster when neither
simulator_cluster nor qpu_system is set, just like the constructor does.
Execution Configuration¶
Control the simulator backend, simulation method, bond dimension, and runtime
metadata for your jobs using ExecutionConfig. Like JobConfig,
you can configure it in two ways:
Default Configuration: Set a default
ExecutionConfigwhen you initialize the service. This configuration will apply to all jobs unless you override it.Per-submission Override: Pass an
execution_configtosubmit_circuitsto override the default for a single job. Non-None fields in the override take precedence.
from divi.backends import (
QoroService, ExecutionConfig, Simulator, SimulationMethod
)
# 1. Set a service-level default execution configuration
default_exec = ExecutionConfig(
bond_dimension=256,
simulator=Simulator.QCSim,
simulation_method=SimulationMethod.MatrixProductState,
)
service = QoroService(execution_config=default_exec)
# All submissions use the default execution config
result = service.submit_circuits(circuits)
# 2. Override specific fields for a single submission
override = ExecutionConfig(bond_dimension=512, api_meta={"optimization_level": 2})
result = service.submit_circuits(circuits, override_execution_config=override)
# Uses bond_dimension=512 and api_meta from the override,
# but keeps simulator and simulation_method from the default.
# Retrieve the configuration to verify
retrieved = service.get_execution_config(result)
print(retrieved.bond_dimension) # 512
All ExecutionConfig fields are optional; only the fields you provide are
sent to the service. You can update the configuration later with
set_execution_config as long as the job is still PENDING; each call
replaces the previous execution configuration for that job.
Note
Execution configuration can only be set on jobs in PENDING status. Attempting to set it on a running or completed job will raise a 409 Conflict error.
Warning
The bond_dimension field is subject to tier-based caps. Free-tier users are limited to a maximum of 32. Exceeding the cap returns a 403 Forbidden error.
The api_meta field accepts runtime pass-through metadata. Allowed keys are documented on ExecutionConfig in Backends (e.g. optimization_level, resilience_level, max_execution_time).
Backend Selection Guide¶
Choosing the right backend depends on what stage of development you’re in.
For Development and Testing, use
MaestroSimulator— for exact noiseless simulation, for analytical Pauli-channel noise, or for fast Monte Carlo over a hand-written noise model.For Qiskit-native noise, use
QiskitSimulator— its strength is plugging into Qiskit fake backends and arbitraryqiskit_aer.noise.NoiseModelinstances built from device calibration data.For Production Runs, use
QoroServicefor cloud simulation, real quantum hardware, and scalable execution.For Research, start with
MaestroSimulatorfor prototyping, then useQoroServicefor validation against real hardware.
Backend Comparison¶
Feature |
|||
|---|---|---|---|
Use Case |
Default local simulation; Pauli-channel noise |
Qiskit-native noise (fake backends, calibrated models) |
Production & real hardware |
Simulation Engine |
Qoro C++ (qoro-maestro) |
Qiskit Aer |
Cloud (Maestro / Aer / hardware) |
Noise Support |
|
Qiskit fake backends & noise models |
Hardware noise (real QPUs) |
Seed / Reproducibility |
|
|
N/A |
Depth Tracking |
|
|
|
Depth Tracking¶
All backends accept track_depth=True on construction to record per-batch depths on CircuitRunner. After submissions, use average_depth(), std_depth(), and clear_depth_history() as needed.
backend = MaestroSimulator(track_depth=True)
Operational notes¶
MaestroSimulator and many qubits: See Configuring MaestroSimulator above for the auto-MPS threshold and the
MaestroConfigfields that control it (mps_qubit_threshold,simulation_type,max_bond_dimension). Note that switching to MPS changes memory and runtime scaling — it is not a generic “make it faster” switch.QiskitSimulator:
n_processesandshotstrade throughput, memory, and statistical noise; there is no single knob—balance them for your machine and accuracy needs.
Shot reproducibility:
set_seed()is a no-op — Maestro’s noiseless simulators seed their measurement RNG from system entropy.noise_seedpins Pauli error patterns for noisy execution (each circuit getsnoise_seed + i), so noisy expval runs are fully reproducible; noisy sampling counts still vary because Maestro’s measurement sampler re-seeds from entropy each call. For reproducible noiseless counts, useQiskitSimulatorwithsimulation_seed.QoroService latency: Client-side wait time is dominated by how you poll; tune
polling_intervalandmax_retriesonQoroService. For fast inner loops, use a local simulator; cloud queue time is outside the client library.
Next Steps¶
tutorials/backends/qasm_thru_service.py and tutorials/backends/backend_properties_conversion.py — Qoro submission and backend-from-metadata workflows
Backends — full
CircuitRunner,JobConfig, andExecutionConfigreferencePipelines — how programs drive backends through the pipeline
Improving Results with Error Mitigation — error mitigation on noisy hardware