User Guide¶
This guide covers all features of the Enhanced Quantum Backend Selector.
How It Works¶
The library provides intelligent backend selection through transpilation-based error analysis:
- Transpiles your circuit to each backend's native gate set
- Calculates the total expected error from all gates, readouts and idle time on the qubits used
- Averages over multiple transpilation runs for stability (default: 3)
- Ranks backends by their actual expected error
Basic Usage¶
from enhanced_quantum_backend_selector import BackendSelector
from qiskit import QuantumCircuit
selector = BackendSelector(backends)
rec = selector.select_backend(circuit)
backend = rec.backend
Understanding Scores¶
The score is based on transpilation-based error estimation:
rec = selector.select_backend(qc)
print(f"Backend: {rec.backend.name}")
print(f"Score: {rec.score_data.score:.3f}") # Success probability, 0 to 1
print(f"Expected error: {1 - rec.score_data.score:.2%}")
# Human-readable reasons
for reason in rec.score_data.reasons:
print(f" • {reason}")
Score interpretation:
- Score is the estimated success probability (ESP), between 0 and 1:
0.98means a 98% chance the circuit runs without error - Incompatible backends (too few qubits, offline, or transpilation failed) have
compatible=Falseand a score of0 - Pass
seed=toBackendSelectorfor reproducible scores - Higher scores are better:
0.99(1% error) is better than0.95(5% error)
Advanced Features¶
Get Full Rankings¶
# Get all backends ranked
all_rankings = selector.rank_backends(qc)
for rec in all_rankings:
print(f"{rec.rank}. {rec.backend.name}: {rec.score_data.score:.3f}")
# Get only top N
top_3 = selector.rank_backends(qc, top_n=3)
Minimum Score Filtering¶
# Require at least a 90% success probability (at most 10% error)
try:
rec = selector.select_backend(qc, min_score=0.9)
except RuntimeError as e:
print(f"No backends met threshold: {e}")
Compatibility Filtering¶
# Strict mode (default) - only compatible backends
rec = selector.select_backend(qc, require_compatible=True)
# Lenient mode - include incompatible backends in ranking
rec = selector.select_backend(qc, require_compatible=False)
Configuring Transpilation Runs¶
# Default: 3 runs (balanced)
selector = BackendSelector(backends)
# Fast: 1 run
selector_fast = BackendSelector(backends, transpilation_runs=1)
# Accurate: 5 runs
selector_accurate = BackendSelector(backends, transpilation_runs=5)
Fractional Gates for Chemistry¶
For chemistry and VQE circuits, fractional gates can improve results:
from qiskit_ibm_runtime import QiskitRuntimeService
service = QiskitRuntimeService()
# Include both versions to compare
backends = [
service.backend('ibm_torino'),
service.backend('ibm_torino', use_fractional_gates=True),
]
selector = BackendSelector(backends)
rec = selector.select_backend(chemistry_circuit)
Performance¶
Parallel Processing¶
Multiple backends are scored in parallel (up to 10 workers) automatically.
What to Expect¶
Transpilation dominates the run time; scoring a transpiled circuit takes milliseconds (about 0.1 s for a 120-qubit QFT with ~46,000 operations). Measured on a 12-core laptop with Qiskit's fake IBM devices (127-156 qubits), seeded, 3 transpilation runs per backend:
| Circuit | 1 backend | 10 backends |
|---|---|---|
| GHZ, 100 qubits | < 0.1 s | 0.3 s |
| QFT, 20 qubits | < 0.1 s | 0.8 s |
| QFT, 60 qubits | 1.1 s | 4.7 s |
| QFT, 120 qubits | 0.7 s | 9.4 s |
| EfficientSU2 (full entanglement), 40 qubits x 5 reps | 0.7 s | 9.7 s |
With real IBM backends, add the time to fetch each backend's calibration data and
status. Use transpilation_runs=1 when speed matters more than stability.
Limitations¶
The score is an estimate built from the backend's reported calibration data. Keep these in mind when comparing backends:
- Independent errors only. The success probability multiplies
(1 - error)for every gate and readout, assuming errors are independent. Crosstalk and correlated errors are not modelled. - Idle error is approximate. Each qubit's idle time (waiting on other qubits, or explicit delays) is charged decoherence from its T1/T2, assuming the circuit is scheduled as late as possible. Dynamical decoupling, which suppresses idle error, is not modelled, so with it enabled idle error is overestimated. Backends that don't report gate durations or T1/T2 get no idle error.
- Readout counted for every used qubit, even if the circuit doesn't measure it. This matches Estimator-style workloads, which measure all active qubits.
- Queue time is ignored. The best-scoring backend may have the longest queue.
- Your transpilation may differ. Backends are scored with
optimization_level=2. If you transpile with different settings or your own pass manager, the circuit you run can use different qubits than the one that was scored. - Broken couplers. Some devices report gates with an error of 1.0. If the
transpiler routes through one, the success probability for that run is effectively
zero. On circuits large enough that every run does this, all backends score about
0and the ranking reflects which backend needs the fewest broken gates, which is a weak signal. - Missing calibration data is filled in with the mean error of the same gate on
that device; gates with no data anywhere count as error-free and are listed in the
score's
reasons. - Calibration data changes. Scores reflect the calibration at the time of the call. Re-run the selection rather than caching results for long.
Best Practices¶
1. Handle Exceptions¶
try:
rec = selector.select_backend(qc)
except RuntimeError as e:
# No backend can run the circuit, or none met min_score
print(f"Selection failed: {e}")
2. Validate Recommendations¶
rec = selector.select_backend(qc)
# Check error rate
error_pct = (1 - rec.score_data.score) * 100
if error_pct > 10:
print(f"Warning: {error_pct:.1f}% expected error")
3. Compare Multiple Backends¶
top_5 = selector.rank_backends(qc, top_n=5)
for rec in top_5:
error_pct = (1 - rec.score_data.score) * 100
print(f"#{rec.rank}: {rec.backend.name} - {error_pct:.2f}% error")
Example: Production Backend Selection¶
def select_production_backend(circuit, backends):
"""Select backend with error handling."""
selector = BackendSelector(backends)
try:
rec = selector.select_backend(circuit, require_compatible=True)
error_pct = (1 - rec.score_data.score) * 100
if error_pct < 10:
return rec.backend, "optimal"
else:
return rec.backend, "acceptable"
except RuntimeError as e:
# No backend can run this circuit; each backend's reasons say why
print(f"Warning: {e}")
for rec in selector.rank_backends(circuit):
print(f" {rec.backend.name}: {rec.score_data.reasons[0]}")
return None, "unavailable"
Next Steps¶
- Examples - More code examples
- API Reference - Complete API documentation
- Architecture - System design details