Tutorial 10: Hybrid Quantum-Classical Algorithms¶
Time: 30 minutes
Level: Advanced
Concepts: Parameterized circuits, classical optimization loop, gradient estimation
Try it interactively
This tutorial is also available as a Jupyter notebook you can run locally:
Open 18_hybrid_algorithms.ipynb
Overview¶
Hybrid algorithms combine quantum and classical processing in a loop:
┌─────────────┐ ┌──────────────┐ ┌──────────────────┐
│ Parameterize│────>│ Run Quantum │────>│ Classical │
│ Circuit │ │ Circuit │ │ Optimizer │
│ theta │<────│ <O>(theta) │<────│ theta' = update() │
└─────────────┘ └──────────────┘ └──────────────────┘
This pattern underlies VQE, QAOA, quantum machine learning, and more.
Building a Parameter Sweep¶
import quantsdk as qs
import math
def parameterized_circuit(theta: float) -> qs.Circuit:
"""Create a circuit parameterized by theta."""
circuit = qs.Circuit(2, name=f"param-theta={theta:.2f}")
circuit.h(0)
circuit.ry(1, theta)
circuit.cx(0, 1)
circuit.measure_all()
return circuit
# Sweep theta and observe how results change
for theta in [0, math.pi/4, math.pi/2, math.pi]:
circuit = parameterized_circuit(theta)
result = qs.run(circuit, shots=1000)
print(f"theta={theta:.2f}: {result.counts}")
Gradient Estimation¶
The parameter shift rule lets us estimate gradients of quantum circuits:
\[\frac{\partial \langle O \rangle}{\partial \theta} = \frac{\langle O \rangle_{\theta + \pi/2} - \langle O \rangle_{\theta - \pi/2}}{2}\]
import quantsdk as qs
import math
def expectation_z(theta: float, shots: int = 4096) -> float:
"""Measure <Z> for a parameterized circuit."""
circuit = qs.Circuit(1, name="param")
circuit.ry(0, theta)
circuit.measure(0)
result = qs.run(circuit, shots=shots)
# <Z> = P(0) - P(1)
p0 = result.counts.get('0', 0) / shots
p1 = result.counts.get('1', 0) / shots
return p0 - p1
def parameter_shift_gradient(theta: float) -> float:
"""Estimate gradient using parameter shift rule."""
shift = math.pi / 2
return (expectation_z(theta + shift) - expectation_z(theta - shift)) / 2
# Gradient descent to minimize <Z> (find theta = pi, giving |1>)
theta = 0.1 # Initial guess
learning_rate = 0.5
print(f"Goal: Find theta that minimizes <Z>")
print(f"Expected: theta = pi (state |1>, <Z> = -1)")
print()
for step in range(20):
grad = parameter_shift_gradient(theta)
exp_z = expectation_z(theta)
if step % 5 == 0:
print(f"Step {step:2d}: theta={theta:.3f}, <Z>={exp_z:.3f}, grad={grad:.3f}")
theta -= learning_rate * grad
print(f"\nFinal: theta={theta:.3f} (expected: {math.pi:.3f})")
print(f"Final <Z>: {expectation_z(theta):.3f} (expected: -1.000)")
Quantum Machine Learning Pattern¶
import quantsdk as qs
import math
def quantum_classifier(features: list[float], weights: list[float]) -> qs.Circuit:
"""A simple quantum classifier.
Args:
features: Input data (2 features).
weights: Trainable parameters (4 weights).
"""
circuit = qs.Circuit(2, name="q-classifier")
# Data encoding layer
circuit.ry(0, features[0])
circuit.ry(1, features[1])
# Trainable layer 1
circuit.ry(0, weights[0])
circuit.ry(1, weights[1])
circuit.cx(0, 1)
# Trainable layer 2
circuit.ry(0, weights[2])
circuit.ry(1, weights[3])
circuit.measure_all()
return circuit
# Example: classify a data point
features = [0.5, 1.2]
weights = [0.1, 0.3, 0.5, 0.7]
circuit = quantum_classifier(features, weights)
result = qs.run(circuit, shots=1000)
# Interpret: probability of measuring |00> = class 0, else class 1
p_class_0 = result.counts.get('00', 0) / 1000
print(f"P(class 0) = {p_class_0:.3f}")
print(f"P(class 1) = {1 - p_class_0:.3f}")
Design Patterns¶
1. Variational Ansatz Pattern¶
def variational_layer(circuit, params, layer_idx):
"""Add a variational layer to a circuit."""
n = circuit.num_qubits
offset = layer_idx * n
# Single-qubit rotations
for i in range(n):
circuit.ry(i, params[offset + i])
# Entangling layer
for i in range(n - 1):
circuit.cx(i, i + 1)
return circuit
2. Measurement and Post-Processing¶
def expectation_value(circuit, observable, shots=4096):
"""Estimate expectation value of an observable."""
circuit_copy = circuit.copy()
circuit_copy.measure_all()
result = qs.run(circuit_copy, shots=shots)
# Post-process based on observable type
exp_val = 0.0
for bitstring, count in result.counts.items():
eigenvalue = observable(bitstring)
exp_val += eigenvalue * count / shots
return exp_val
Key Takeaways¶
- Hybrid algorithms are the dominant paradigm for near-term quantum computing
- The parameter shift rule gives exact gradients (not finite differences)
- Shot noise limits precision — more shots = less noise = slower
- Barren plateaus can make optimization hard for deep circuits
- QuantSDK's fluent API makes it easy to build parameterized circuits
What's Next?¶
Congratulations! You've completed all 10 tutorials. Here are some next steps:
- API Reference — explore the full API
- Backend Guides — run on real hardware
- Contributing — help build QuantSDK