13 KiB
Qiskit Patterns: The Four-Step Workflow
Qiskit Patterns provide a general framework for solving domain-specific quantum computing problems in four stages: Map, Optimize, Execute, and Post-process.
Overview
The patterns framework enables seamless composition of quantum capabilities and supports heterogeneous computing infrastructure (CPU/GPU/QPU). Execute locally, through cloud services, or via Qiskit Serverless.
The Four Steps
Problem → [Map] → [Optimize] → [Execute] → [Post-process] → Solution
1. Map
Translate classical problems into quantum circuits and operators
2. Optimize
Prepare circuits for target hardware through transpilation
3. Execute
Run circuits on quantum hardware using primitives
4. Post-process
Extract and refine results with classical computation
Step 1: Map
Goal
Transform domain-specific problems into quantum representations (circuits, operators, Hamiltonians).
Key Decisions
Choose Output Type:
- Sampler: For bitstring outputs (optimization, search)
- Estimator: For expectation values (chemistry, physics)
Design Circuit Structure:
from qiskit import QuantumCircuit
from qiskit.circuit import Parameter
import numpy as np
# Example: Parameterized circuit for VQE
def create_ansatz(num_qubits, depth):
qc = QuantumCircuit(num_qubits)
params = []
for d in range(depth):
# Rotation layer
for i in range(num_qubits):
theta = Parameter(f'θ_{d}_{i}')
params.append(theta)
qc.ry(theta, i)
# Entanglement layer
for i in range(num_qubits - 1):
qc.cx(i, i + 1)
return qc, params
ansatz, params = create_ansatz(num_qubits=4, depth=2)
Considerations
- Hardware topology: Design with backend coupling map in mind
- Gate efficiency: Minimize two-qubit gates
- Measurement basis: Determine required measurements
Domain-Specific Examples
Chemistry: Molecular Hamiltonian
from qiskit_nature.second_q.drivers import PySCFDriver
from qiskit_nature.second_q.mappers import JordanWignerMapper
# Define molecule
driver = PySCFDriver(atom='H 0 0 0; H 0 0 0.735', basis='sto3g')
problem = driver.run()
# Map to qubit Hamiltonian
mapper = JordanWignerMapper()
hamiltonian = mapper.map(problem.hamiltonian)
Optimization: QAOA Circuit
from qiskit.circuit import QuantumCircuit, Parameter
def qaoa_circuit(graph, p):
"""Create QAOA circuit for MaxCut problem"""
num_qubits = len(graph.nodes())
qc = QuantumCircuit(num_qubits)
# Initial superposition
qc.h(range(num_qubits))
# Alternating layers
betas = [Parameter(f'β_{i}') for i in range(p)]
gammas = [Parameter(f'γ_{i}') for i in range(p)]
for i in range(p):
# Problem Hamiltonian
for edge in graph.edges():
qc.cx(edge[0], edge[1])
qc.rz(2 * gammas[i], edge[1])
qc.cx(edge[0], edge[1])
# Mixer Hamiltonian
qc.rx(2 * betas[i], range(num_qubits))
return qc
Step 2: Optimize
Goal
Transform abstract circuits to hardware-compatible ISA (Instruction Set Architecture) circuits.
Transpilation
from qiskit import transpile
# Basic transpilation
qc_isa = transpile(qc, backend=backend, optimization_level=3)
# With specific initial layout
qc_isa = transpile(
qc,
backend=backend,
optimization_level=3,
initial_layout=[0, 2, 4, 6], # Map to specific physical qubits
seed_transpiler=42 # Reproducibility
)
Pre-optimization Tips
- Test with simulators first:
from qiskit_aer import AerSimulator
sim = AerSimulator.from_backend(backend)
qc_test = transpile(qc, sim, optimization_level=3)
print(f"Estimated depth: {qc_test.depth()}")
- Analyze transpilation results:
print(f"Original gates: {qc.size()}")
print(f"Transpiled gates: {qc_isa.size()}")
print(f"Two-qubit gates: {qc_isa.count_ops().get('cx', 0)}")
- Consider circuit cutting for large circuits:
# For circuits too large for available hardware
# Use circuit cutting techniques to split into smaller subcircuits
Step 3: Execute
Goal
Run ISA circuits on quantum hardware using primitives.
Using Sampler
from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler
service = QiskitRuntimeService()
backend = service.backend("ibm_brisbane")
# Transpile first
qc_isa = transpile(qc, backend=backend, optimization_level=3)
# Execute
sampler = Sampler(backend)
job = sampler.run([qc_isa], shots=10000)
result = job.result()
counts = result[0].data.meas.get_counts()
Using Estimator
from qiskit_ibm_runtime import EstimatorV2 as Estimator
from qiskit.quantum_info import SparsePauliOp
service = QiskitRuntimeService()
backend = service.backend("ibm_brisbane")
# Transpile
qc_isa = transpile(qc, backend=backend, optimization_level=3)
# Define observable
observable = SparsePauliOp(["ZZZZ", "XXXX"])
# Execute
estimator = Estimator(backend)
job = estimator.run([(qc_isa, observable)])
result = job.result()
expectation_value = result[0].data.evs
Execution Modes
Session Mode (Iterative):
from qiskit_ibm_runtime import Session
with Session(backend=backend) as session:
sampler = Sampler(session=session)
# Multiple iterations
for iteration in range(max_iterations):
qc_iteration = update_circuit(params[iteration])
qc_isa = transpile(qc_iteration, backend=backend)
job = sampler.run([qc_isa], shots=1000)
result = job.result()
# Update parameters
params[iteration + 1] = optimize_params(result)
Batch Mode (Parallel):
from qiskit_ibm_runtime import Batch
with Batch(backend=backend) as batch:
sampler = Sampler(session=batch)
# Submit all jobs at once
jobs = []
for qc in circuit_list:
qc_isa = transpile(qc, backend=backend)
job = sampler.run([qc_isa], shots=1000)
jobs.append(job)
# Collect results
results = [job.result() for job in jobs]
Error Mitigation
from qiskit_ibm_runtime import Options
options = Options()
options.resilience_level = 2 # 0=none, 1=light, 2=moderate, 3=heavy
options.optimization_level = 3
sampler = Sampler(backend, options=options)
Step 4: Post-process
Goal
Extract meaningful results from quantum measurements using classical computation.
Result Processing
For Sampler (Bitstrings):
counts = result[0].data.meas.get_counts()
# Convert to probabilities
total_shots = sum(counts.values())
probabilities = {state: count/total_shots for state, count in counts.items()}
# Find most probable state
max_state = max(counts, key=counts.get)
print(f"Most probable state: {max_state} ({counts[max_state]}/{total_shots})")
For Estimator (Expectation Values):
expectation_value = result[0].data.evs
std_dev = result[0].data.stds # Standard deviation
print(f"Energy: {expectation_value} ± {std_dev}")
Domain-Specific Post-Processing
Chemistry: Ground State Energy
def post_process_chemistry(result, nuclear_repulsion):
"""Extract ground state energy"""
electronic_energy = result[0].data.evs
total_energy = electronic_energy + nuclear_repulsion
return total_energy
Optimization: MaxCut Solution
def post_process_maxcut(counts, graph):
"""Find best cut from measurement results"""
def compute_cut_value(bitstring, graph):
cut_value = 0
for edge in graph.edges():
if bitstring[edge[0]] != bitstring[edge[1]]:
cut_value += 1
return cut_value
# Find bitstring with maximum cut
best_cut = 0
best_string = None
for bitstring, count in counts.items():
cut = compute_cut_value(bitstring, graph)
if cut > best_cut:
best_cut = cut
best_string = bitstring
return best_string, best_cut
Advanced Post-Processing
Error Mitigation Post-Processing:
# Apply additional classical error mitigation
from qiskit.result import marginal_counts
# Marginalize to relevant qubits
relevant_qubits = [0, 1, 2]
marginal = marginal_counts(counts, indices=relevant_qubits)
Statistical Analysis:
import numpy as np
def analyze_results(results_list):
"""Analyze multiple runs for statistics"""
energies = [r[0].data.evs for r in results_list]
mean_energy = np.mean(energies)
std_energy = np.std(energies)
confidence_interval = 1.96 * std_energy / np.sqrt(len(energies))
return {
'mean': mean_energy,
'std': std_energy,
'95% CI': (mean_energy - confidence_interval, mean_energy + confidence_interval)
}
Visualization:
from qiskit.visualization import plot_histogram
import matplotlib.pyplot as plt
# Visualize results
plot_histogram(counts, figsize=(12, 6))
plt.title("Measurement Results")
plt.show()
Complete Example: VQE for Chemistry
from qiskit import QuantumCircuit, transpile
from qiskit_ibm_runtime import QiskitRuntimeService, EstimatorV2 as Estimator, Session
from qiskit.quantum_info import SparsePauliOp
from scipy.optimize import minimize
import numpy as np
# 1. MAP: Create parameterized circuit
def create_ansatz(num_qubits):
qc = QuantumCircuit(num_qubits)
params = []
for i in range(num_qubits):
theta = f'θ_{i}'
params.append(theta)
qc.ry(theta, i)
for i in range(num_qubits - 1):
qc.cx(i, i + 1)
return qc, params
# Define Hamiltonian (example: H2 molecule)
hamiltonian = SparsePauliOp(["IIZZ", "ZZII", "XXII", "IIXX"], coeffs=[0.3, 0.3, 0.1, 0.1])
# 2. OPTIMIZE: Connect and prepare
service = QiskitRuntimeService()
backend = service.backend("ibm_brisbane")
ansatz, param_names = create_ansatz(num_qubits=4)
# 3. EXECUTE: Run VQE
def cost_function(params):
# Bind parameters
bound_circuit = ansatz.assign_parameters({param_names[i]: params[i] for i in range(len(params))})
# Transpile
qc_isa = transpile(bound_circuit, backend=backend, optimization_level=3)
# Execute
job = estimator.run([(qc_isa, hamiltonian)])
result = job.result()
energy = result[0].data.evs
return energy
with Session(backend=backend) as session:
estimator = Estimator(session=session)
# Classical optimization loop
initial_params = np.random.random(len(param_names)) * 2 * np.pi
result = minimize(cost_function, initial_params, method='COBYLA')
# 4. POST-PROCESS: Extract ground state energy
ground_state_energy = result.fun
optimized_params = result.x
print(f"Ground state energy: {ground_state_energy}")
print(f"Optimized parameters: {optimized_params}")
Best Practices
1. Iterate Locally First
Test the full workflow with simulators before using hardware:
from qiskit.primitives import StatevectorEstimator
estimator = StatevectorEstimator()
# Test workflow locally
2. Use Sessions for Iterative Algorithms
VQE, QAOA, and other variational algorithms benefit from sessions.
3. Choose Appropriate Shots
- Development/testing: 100-1000 shots
- Production: 10,000+ shots
4. Monitor Convergence
energies = []
def cost_function_with_tracking(params):
energy = cost_function(params)
energies.append(energy)
print(f"Iteration {len(energies)}: E = {energy}")
return energy
5. Save Results
import json
results_data = {
'energy': float(ground_state_energy),
'parameters': optimized_params.tolist(),
'iterations': len(energies),
'backend': backend.name
}
with open('vqe_results.json', 'w') as f:
json.dump(results_data, f, indent=2)
Qiskit Serverless
For large-scale workflows, use Qiskit Serverless for distributed computation:
from qiskit_serverless import ServerlessClient, QiskitFunction
client = ServerlessClient()
# Define serverless function
@QiskitFunction()
def run_vqe_serverless(hamiltonian, ansatz):
# Your VQE implementation
pass
# Execute remotely
job = run_vqe_serverless(hamiltonian, ansatz)
result = job.result()
Common Workflow Patterns
Pattern 1: Parameter Sweep
# Map → Optimize once → Execute many → Post-process
qc_isa = transpile(parameterized_circuit, backend=backend)
with Batch(backend=backend) as batch:
sampler = Sampler(session=batch)
results = []
for param_set in parameter_sweep:
bound_qc = qc_isa.assign_parameters(param_set)
job = sampler.run([bound_qc], shots=1000)
results.append(job.result())
Pattern 2: Iterative Refinement
# Map → (Optimize → Execute → Post-process) repeated
with Session(backend=backend) as session:
estimator = Estimator(session=session)
for iteration in range(max_iter):
qc = update_circuit(params)
qc_isa = transpile(qc, backend=backend)
result = estimator.run([(qc_isa, observable)]).result()
params = update_params(result)
Pattern 3: Ensemble Measurement
# Map → Optimize → Execute many observables → Post-process
qc_isa = transpile(qc, backend=backend)
observables = [obs1, obs2, obs3, obs4]
jobs = [(qc_isa, obs) for obs in observables]
estimator = Estimator(backend)
result = estimator.run(jobs).result()
expectation_values = [r.data.evs for r in result]