Circuit Optimization
QCOS includes a powerful circuit optimization engine that reduces circuit depth, minimizes gate count, and improves execution fidelity. This guide covers optimization techniques and best practices.
Why Optimize?β
Quantum computers are noisy. Every gate introduces errors:
| Error Source | Typical Rate |
|---|---|
| Single-qubit gate | 0.01-0.1% |
| Two-qubit gate | 0.5-2% |
| Readout | 1-5% |
| Decoherence | Continuous |
Optimization reduces errors by:
- Reducing circuit depth (less time for decoherence)
- Minimizing gate count (fewer error opportunities)
- Using native gates (no additional decomposition errors)
- Optimal qubit mapping (shorter paths, better qubits)
Optimization Levelsβ
QCOS provides four optimization levels:
Level 0: No Optimizationβ
job = client.run(circuit, backend="ibm_brisbane", optimization_level=0)
- Basic validation only
- Circuit submitted as-is
- Use for debugging or pre-optimized circuits
Level 1: Light Optimizationβ
job = client.run(circuit, backend="ibm_brisbane", optimization_level=1)
- Gate cancellation (adjacent inverses)
- Basic gate fusion
- Virtual Z optimization
- Fast compilation
Level 2: Standard Optimization (Default)β
job = client.run(circuit, backend="ibm_brisbane", optimization_level=2)
- All Level 1 optimizations
- Qubit routing and mapping
- Gate decomposition to native gates
- Layout optimization for connectivity
Level 3: Heavy Optimizationβ
job = client.run(circuit, backend="ibm_brisbane", optimization_level=3)
- All Level 2 optimizations
- Multiple routing passes
- Advanced gate synthesis
- Noise-aware compilation
- Slowest but best results
Optimization Pipelineβ
βββββββββββββββ ββββββββββββββββ βββββββββββββββββββ
β Input β β β Stage 1 β β β Stage 2 β
β Circuit β β Synthesis β β Mapping β
βββββββββββββββ ββββββββββββββββ βββββββββββββββββββ
β β
βββββββββββββββ ββββββββββββββββ βββββββββββββββββββ
β Output β β β Stage 4 β β β Stage 3 β
β Circuit β β Scheduling β β Routing β
βββββββββββββββ ββββββββββββββββ βββββββββββββββββββ
Stage 1: Synthesisβ
Converts abstract gates to native gate set:
| Target | Native Gates |
|---|---|
| IBM | CX, ID, RZ, SX, X |
| IBM Heron | CZ, RZ, SX, X |
| IonQ | GPI, GPI2, MS |
| Rigetti | CZ, RX, RZ |
Stage 2: Mappingβ
Assigns logical qubits to physical qubits:
# View qubit mapping
job = client.run(circuit, backend="ibm_brisbane")
print(f"Mapping: {job.metrics.qubit_mapping}")
# Output: {0: 12, 1: 15, 2: 18}
Stage 3: Routingβ
Inserts SWAP gates for non-adjacent interactions:
# Original: CX(0, 5) - qubits not connected
# After routing: SWAP + CX or CX chain
print(f"Inserted SWAPs: {job.metrics.swap_count}")
Stage 4: Schedulingβ
Parallelizes gates and adds timing:
# Dynamic decoupling for idle qubits
job = client.run(
circuit,
backend="ibm_brisbane",
enable_dynamic_decoupling=True
)
Optimization Metricsβ
Get detailed metrics after optimization:
job = client.run(circuit, backend="ibm_brisbane", optimization_level=3)
metrics = job.metrics
print(f"Original depth: {metrics.original_depth}")
print(f"Optimized depth: {metrics.optimized_depth}")
print(f"Depth reduction: {metrics.depth_reduction_percent}%")
print(f"Original gates: {metrics.original_gate_count}")
print(f"Optimized gates: {metrics.optimized_gate_count}")
print(f"SWAP insertions: {metrics.swap_count}")
print(f"Two-qubit gates: {metrics.two_qubit_gate_count}")
Example Output:
Original depth: 120
Optimized depth: 72
Depth reduction: 40.0%
Original gates: 450
Optimized gates: 285
SWAP insertions: 8
Two-qubit gates: 45
Standalone Optimizationβ
Optimize circuits without execution:
result = client.optimize(
circuit,
target_backend="ibm_brisbane",
optimization_level=3
)
# Get optimized circuit
optimized = result.circuit
# View metrics
print(f"Reduction: {result.depth_reduction_percent}%")
# Export for later use
qasm = optimized.to_qasm()
Save Optimized Circuitβ
# Optimize once, run many times
result = client.optimize(circuit, target_backend="ibm_brisbane")
optimized = result.circuit
# Run multiple times with level 0 (already optimized)
for params in parameter_values:
optimized.bind_parameters(params)
job = client.run(
optimized,
backend="ibm_brisbane",
optimization_level=0 # Skip re-optimization
)
Advanced Optimizationβ
Preserve Layoutβ
Keep your qubit assignment:
result = client.optimize(
circuit,
target_backend="ibm_brisbane",
preserve_layout=True,
initial_layout={0: 0, 1: 1, 2: 2}
)
Custom Basis Gatesβ
Force specific gate decomposition:
result = client.optimize(
circuit,
basis_gates=["cx", "rz", "x", "sx"],
optimization_level=3
)
Noise-Aware Optimizationβ
Use calibration data for better mapping:
result = client.optimize(
circuit,
target_backend="ibm_brisbane",
use_calibration_data=True # Use latest calibration
)
Optimization Strategiesβ
Strategy 1: Reduce Two-Qubit Gatesβ
Two-qubit gates have 10-100x more error:
# Before: Multiple CX gates
circuit.cx(0, 1)
circuit.cx(1, 2)
circuit.cx(2, 3)
# Use optimization to find better decomposition
result = client.optimize(
circuit,
target_backend="ibm_brisbane",
optimization_goal="two_qubit_gates"
)
Strategy 2: Minimize Depthβ
For algorithms limited by decoherence:
result = client.optimize(
circuit,
target_backend="ibm_brisbane",
optimization_goal="depth"
)
Strategy 3: Use High-Fidelity Qubitsβ
Select best qubits based on calibration:
# Get backend calibration
backend = client.backend("ibm_brisbane")
calibration = backend.calibration
# Find best qubits
best_qubits = sorted(
calibration.qubits,
key=lambda q: q.readout_error + q.cx_error
)[:circuit.num_qubits]
# Use as initial layout
result = client.optimize(
circuit,
target_backend="ibm_brisbane",
initial_layout={i: q.index for i, q in enumerate(best_qubits)}
)
Parameterized Circuitsβ
For variational algorithms, optimize the template once:
from softqcos.circuits import QuantumCircuit, Parameter
# Create parameterized circuit
theta = [Parameter(f"ΞΈ{i}") for i in range(4)]
circuit = QuantumCircuit(4, 4)
for i, t in enumerate(theta):
circuit.ry(t, i)
circuit.cx(0, 1)
circuit.cx(2, 3)
circuit.cx(1, 2)
circuit.measure_all()
# Optimize template
result = client.optimize(circuit, target_backend="ibm_brisbane")
template = result.circuit
# Use template with different parameters
for values in optimization_loop:
bound = template.bind_parameters(dict(zip(theta, values)))
job = client.run(bound, backend="ibm_brisbane", optimization_level=0)
Benchmarkingβ
Compare optimization levels:
results = []
for level in range(4):
job = client.run(
circuit,
backend="ibm_brisbane",
optimization_level=level
)
result = job.result()
results.append({
"level": level,
"depth": job.metrics.optimized_depth,
"gates": job.metrics.optimized_gate_count,
"fidelity": result.quality_metrics.fidelity_estimate,
"time": job.metrics.optimization_time_ms
})
# Compare
for r in results:
print(f"Level {r['level']}: depth={r['depth']}, "
f"fidelity={r['fidelity']:.3f}, time={r['time']}ms")
Example Output:
Level 0: depth=120, fidelity=0.72, time=10ms
Level 1: depth=98, fidelity=0.78, time=45ms
Level 2: depth=85, fidelity=0.84, time=120ms
Level 3: depth=72, fidelity=0.89, time=350ms
Best Practicesβ
Doβ
β Use Level 2-3 for production workloads β Optimize parameterized templates once β Check optimization metrics before running β Use noise-aware compilation for critical circuits β Consider backend topology when designing circuits
Don'tβ
β Use Level 0 unless debugging β Re-optimize already optimized circuits β Ignore swap insertions (they add significant error) β Assume more gates = more optimization needed
Creditsβ
Optimization consumes credits based on circuit size:
| Circuit Size | Credit Cost |
|---|---|
| < 100 gates | Free |
| 100-500 gates, Level 1-2 | 1 credit |
| 100-500 gates, Level 3 | 2 credits |
| 500+ gates | 3+ credits |
Free tier: 5 optimizations/day Pro tier: 1000 credits/month (first 100 free)