Files
claude-scientific-skills/scientific-skills/qiskit/references/patterns.md

13 KiB
Raw Blame History

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

  1. 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()}")
  1. 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)}")
  1. 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]