Add support for Qiskit from IBM: software stack for quantum computing and algorithms research. Build, optimize, and execute quantum workloads at scale.

This commit is contained in:
Timothy Kassis
2025-11-30 08:46:41 -05:00
parent 7763491813
commit 7e8deebf96
12 changed files with 3127 additions and 9 deletions

View File

@@ -0,0 +1,533 @@
# 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:**
```python
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**
```python
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**
```python
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
```python
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
1. **Test with simulators first**:
```python
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()}")
```
2. **Analyze transpilation results**:
```python
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)}")
```
3. **Consider circuit cutting** for large circuits:
```python
# 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
```python
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
```python
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):**
```python
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):**
```python
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
```python
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):**
```python
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):**
```python
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**
```python
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**
```python
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:**
```python
# 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:**
```python
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:**
```python
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
```python
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:
```python
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
```python
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
```python
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:
```python
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
```python
# 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
```python
# 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
```python
# 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]
```