Advanced Qiskit Topics

Running on Real Quantum Hardware #

Connecting to IBM Quantum #

from qiskit_ibm_runtime import QiskitRuntimeService

# Load your account
service = QiskitRuntimeService(channel="ibm_quantum")

# List available backends
backends = service.backends()
for backend in backends:
    print(f"Backend: {backend.name}")
    print(f"  Qubits: {backend.num_qubits}")
    print(f"  Status: {backend.status().status_msg}\n")

# Select a backend
backend = service.backend("ibm_brisbane")  # Example backend name

Submitting Jobs to Real Hardware #

from qiskit import QuantumCircuit, transpile

# Create your circuit
qc = QuantumCircuit(2, 2)
qc.h(0)
qc.cx(0, 1)
qc.measure([0, 1], [0, 1])

# Transpile for the specific backend
transpiled_qc = transpile(qc, backend=backend, optimization_level=3)

# Submit job
job = backend.run(transpiled_qc, shots=1024)

# Monitor job status
print(f"Job ID: {job.job_id()}")
print(f"Job Status: {job.status()}")

# Get results (this will wait for job completion)
result = job.result()
counts = result.get_counts()
print("Results:", counts)

Using Qiskit Runtime #

from qiskit_ibm_runtime import Session, Sampler, Estimator

# Create a session
with Session(service=service, backend="ibm_brisbane") as session:
    # Use Sampler for sampling circuits
    sampler = Sampler(session=session)
    job = sampler.run(qc, shots=1000)
    result = job.result()
    print(result)

Circuit Optimization and Transpilation #

Understanding Transpilation #

Transpilation converts your quantum circuit to match the constraints of specific hardware.

from qiskit import transpile
from qiskit.transpiler import CouplingMap, Layout

# Create a circuit
qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0, 1)
qc.cx(1, 2)

# Basic transpilation
transpiled = transpile(qc, backend=backend)

# Custom transpilation with optimization levels
# Level 0: Just mapping, no optimization
# Level 1: Light optimization
# Level 2: Medium optimization
# Level 3: Heavy optimization
transpiled = transpile(
    qc,
    backend=backend,
    optimization_level=3,
    seed_transpiler=42  # For reproducibility
)

print(f"Original depth: {qc.depth()}")
print(f"Transpiled depth: {transpiled.depth()}")

Custom Pass Manager #

from qiskit.transpiler import PassManager
from qiskit.transpiler.passes import (
    Unroller,
    Optimize1qGates,
    CXCancellation,
    CommutativeCancellation
)

# Create custom pass manager
pm = PassManager()
pm.append(Unroller(['u', 'cx']))
pm.append(Optimize1qGates())
pm.append(CXCancellation())
pm.append(CommutativeCancellation())

# Apply passes
optimized_qc = pm.run(qc)

Noise Simulation and Mitigation #

Creating Noise Models #

from qiskit_aer.noise import (
    NoiseModel,
    depolarizing_error,
    thermal_relaxation_error
)

# Create a noise model
noise_model = NoiseModel()

# Add depolarizing error
error_1q = depolarizing_error(0.001, 1)  # 1-qubit error
error_2q = depolarizing_error(0.01, 2)   # 2-qubit error

# Add errors to gates
noise_model.add_all_qubit_quantum_error(error_1q, ['h', 'x', 'y', 'z'])
noise_model.add_all_qubit_quantum_error(error_2q, ['cx'])

# Add thermal relaxation
t1 = 50e3  # T1 time (ns)
t2 = 70e3  # T2 time (ns)
gate_time = 100  # Gate time (ns)

thermal_error = thermal_relaxation_error(t1, t2, gate_time)
noise_model.add_all_qubit_quantum_error(thermal_error, ['h', 'x'])

print(noise_model)

Simulating with Noise #

from qiskit_aer import AerSimulator

# Create noisy simulator
noisy_simulator = AerSimulator(noise_model=noise_model)

# Run circuit with noise
job = noisy_simulator.run(transpiled_qc, shots=1024)
result = job.result()
noisy_counts = result.get_counts()

print("Noisy results:", noisy_counts)

Noise Mitigation #

from qiskit.ignis.mitigation.measurement import (
    complete_meas_cal,
    CompleteMeasFitter
)

# Generate calibration circuits
qr = QuantumRegister(2)
meas_calibs, state_labels = complete_meas_cal(qr=qr)

# Run calibration circuits
cal_job = simulator.run(meas_calibs, shots=1000)
cal_results = cal_job.result()

# Create measurement fitter
meas_fitter = CompleteMeasFitter(cal_results, state_labels)

# Apply mitigation
mitigated_results = meas_fitter.filter.apply(result)
mitigated_counts = mitigated_results.get_counts()

Advanced Circuit Techniques #

Dynamic Circuits (Mid-Circuit Measurements) #

from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister

# Create registers
qr = QuantumRegister(2, 'q')
cr = ClassicalRegister(2, 'c')
qc = QuantumCircuit(qr, cr)

# Perform mid-circuit measurement
qc.h(0)
qc.measure(0, 0)

# Conditional operation based on measurement
qc.x(1).c_if(cr[0], 1)  # Apply X if measurement was 1

qc.measure(1, 1)

Pulse-Level Control #

from qiskit import pulse
from qiskit.pulse import library as pulse_lib

# Get backend's pulse properties
backend = service.backend("ibm_brisbane")

# Create a pulse schedule
with pulse.build(backend) as my_schedule:
    # Define a Gaussian pulse
    gaussian_pulse = pulse_lib.Gaussian(
        duration=160,
        amp=0.5,
        sigma=40
    )
    
    # Play pulse on drive channel of qubit 0
    drive_chan = pulse.drive_channel(0)
    pulse.play(gaussian_pulse, drive_chan)
    
    # Measure
    pulse.measure_all()

# Execute pulse schedule
job = backend.run(my_schedule)

Circuit Synthesis and Decomposition #

Unitary Synthesis #

import numpy as np
from qiskit.quantum_info import Operator
from qiskit.synthesis import OneQubitEulerDecomposer

# Define a unitary matrix
unitary_matrix = np.array([
    [np.cos(np.pi/4), -np.sin(np.pi/4)],
    [np.sin(np.pi/4), np.cos(np.pi/4)]
])

# Create operator
operator = Operator(unitary_matrix)

# Synthesize circuit
qc = QuantumCircuit(1)
qc.unitary(operator, [0])

# Decompose into basic gates
decomposed = qc.decompose()
print(decomposed)

Two-Qubit Gate Decomposition #

from qiskit.synthesis import TwoQubitBasisDecomposer
from qiskit.circuit.library import CXGate

# Create decomposer
decomposer = TwoQubitBasisDecomposer(CXGate())

# Decompose a unitary into CX and single-qubit gates
two_qubit_unitary = Operator.from_label('CZ')
circuit = decomposer(two_qubit_unitary)

Performance Optimization #

Circuit Caching #

from functools import lru_cache

@lru_cache(maxsize=None)
def create_qft_circuit(n):
    """Cached QFT circuit creation"""
    qc = QuantumCircuit(n)
    # ... QFT implementation
    return qc

# First call computes and caches
qft_3 = create_qft_circuit(3)

# Second call retrieves from cache
qft_3_cached = create_qft_circuit(3)

Batch Job Submission #

# Submit multiple circuits at once
circuits = [qc1, qc2, qc3, qc4]
transpiled_circuits = transpile(circuits, backend=backend)

# Run as batch
job = backend.run(transpiled_circuits, shots=1024)
results = job.result()

# Access individual results
for i, circuit in enumerate(circuits):
    counts = results.get_counts(i)
    print(f"Circuit {i} results: {counts}")

Quantum State and Process Tomography #

State Tomography #

from qiskit.ignis.verification.tomography import (
    state_tomography_circuits,
    StateTomographyFitter
)
from qiskit.quantum_info import state_fidelity

# Create a Bell state
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)

# Generate tomography circuits
tomo_circuits = state_tomography_circuits(qc, [0, 1])

# Run tomography circuits
job = simulator.run(tomo_circuits, shots=5000)
result = job.result()

# Fit the result to reconstruct state
fitter = StateTomographyFitter(result, tomo_circuits)
rho = fitter.fit()

# Calculate fidelity
ideal_state = Statevector.from_label('00')
fidelity = state_fidelity(rho, ideal_state)
print(f"State fidelity: {fidelity}")

Process Tomography #

from qiskit.ignis.verification.tomography import (
    process_tomography_circuits,
    ProcessTomographyFitter
)

# Define the process (e.g., Hadamard gate)
qc = QuantumCircuit(1)
qc.h(0)

# Generate process tomography circuits
tomo_circuits = process_tomography_circuits(qc, [0])

# Run and fit
job = simulator.run(tomo_circuits, shots=5000)
result = job.result()
fitter = ProcessTomographyFitter(result, tomo_circuits)
chi = fitter.fit()

Debugging and Profiling #

Statevector Simulation #

from qiskit.quantum_info import Statevector

# Get the statevector at any point
qc = QuantumCircuit(2)
qc.h(0)

# Get statevector
state = Statevector(qc)
print("Statevector:", state)

# Visualize
from qiskit.visualization import plot_state_qsphere
plot_state_qsphere(state)

Unitary Simulation #

from qiskit.quantum_info import Operator

# Get the unitary matrix
unitary = Operator(qc)
print("Unitary matrix:\n", unitary.data)

Circuit Profiling #

import time

# Time circuit execution
start = time.time()
job = simulator.run(qc, shots=1000)
result = job.result()
elapsed = time.time() - start

print(f"Execution time: {elapsed:.3f}s")
print(f"Circuit depth: {qc.depth()}")
print(f"Gate count: {sum(qc.count_ops().values())}")

Best Practices #

1. Always Transpile for Target Backend #

# Good
transpiled = transpile(qc, backend=backend, optimization_level=3)
job = backend.run(transpiled)

# Avoid running raw circuits on hardware

2. Use Error Mitigation #

# Apply measurement error mitigation
# Use dynamical decoupling
# Implement zero-noise extrapolation

3. Minimize Circuit Depth #

# Use native gates when possible
# Apply optimization passes
# Remove unnecessary gates

4. Handle Job Failures Gracefully #

try:
    result = job.result()
except Exception as e:
    print(f"Job failed: {e}")
    # Implement retry logic

Further Learning Resources #

Conclusion #

You now have the tools to:

  • Run circuits on real quantum hardware
  • Optimize circuits for specific backends
  • Simulate and mitigate noise
  • Implement advanced quantum algorithms
  • Debug and profile quantum programs

Continue exploring, experimenting, and building quantum applications with Qiskit!