Files
claude-scientific-skills/scientific-skills/pennylane/references/advanced_features.md

15 KiB

Advanced Features in PennyLane

Table of Contents

  1. Templates and Layers
  2. Transforms
  3. Pulse Programming
  4. Catalyst and JIT Compilation
  5. Adaptive Circuits
  6. Noise Models
  7. Resource Estimation

Templates and Layers

Built-in Templates

import pennylane as qml
from pennylane.templates import *
from pennylane import numpy as np

dev = qml.device('default.qubit', wires=4)

# Strongly Entangling Layers
@qml.qnode(dev)
def circuit_sel(weights):
    StronglyEntanglingLayers(weights, wires=range(4))
    return qml.expval(qml.PauliZ(0))

# Generate appropriately shaped weights
n_layers = 3
n_wires = 4
shape = StronglyEntanglingLayers.shape(n_layers, n_wires)
weights = np.random.random(shape)

result = circuit_sel(weights)

Basic Entangler Layers

@qml.qnode(dev)
def circuit_bel(weights):
    # Simple entangling layer
    BasicEntanglerLayers(weights, wires=range(4))
    return qml.expval(qml.PauliZ(0))

n_layers = 2
weights = np.random.random((n_layers, 4))

Random Layers

@qml.qnode(dev)
def circuit_random(weights):
    # Random circuit structure
    RandomLayers(weights, wires=range(4))
    return qml.expval(qml.PauliZ(0))

n_layers = 5
weights = np.random.random((n_layers, 4))

Simplified Two Design

@qml.qnode(dev)
def circuit_s2d(weights):
    # Simplified two-design
    SimplifiedTwoDesign(initial_layer_weights=weights[0],
                       weights=weights[1:],
                       wires=range(4))
    return qml.expval(qml.PauliZ(0))

Particle-Conserving Layers

@qml.qnode(dev)
def circuit_particle_conserving(weights):
    # Preserve particle number (useful for chemistry)
    ParticleConservingU1(weights, wires=range(4))
    return qml.expval(qml.PauliZ(0))

shape = ParticleConservingU1.shape(n_layers=2, n_wires=4)
weights = np.random.random(shape)

Embedding Templates

# Angle embedding
@qml.qnode(dev)
def angle_embed(features):
    AngleEmbedding(features, wires=range(4))
    return qml.expval(qml.PauliZ(0))

features = np.array([0.1, 0.2, 0.3, 0.4])

# Amplitude embedding
@qml.qnode(dev)
def amplitude_embed(features):
    AmplitudeEmbedding(features, wires=range(2), normalize=True)
    return qml.expval(qml.PauliZ(0))

features = np.array([0.5, 0.5, 0.5, 0.5])

# IQP embedding
@qml.qnode(dev)
def iqp_embed(features):
    IQPEmbedding(features, wires=range(4), n_repeats=2)
    return qml.expval(qml.PauliZ(0))

Custom Templates

def custom_layer(weights, wires):
    """Define custom template."""
    n_wires = len(wires)

    # Rotation layer
    for i, wire in enumerate(wires):
        qml.RY(weights[i], wires=wire)

    # Entanglement pattern
    for i in range(0, n_wires-1, 2):
        qml.CNOT(wires=[wires[i], wires[i+1]])

    for i in range(1, n_wires-1, 2):
        qml.CNOT(wires=[wires[i], wires[i+1]])

@qml.qnode(dev)
def circuit_custom(weights, n_layers):
    for i in range(n_layers):
        custom_layer(weights[i], wires=range(4))
    return qml.expval(qml.PauliZ(0))

Transforms

Circuit Transformations

# Cancel adjacent inverse operations
from pennylane import transforms

@transforms.cancel_inverses
@qml.qnode(dev)
def circuit():
    qml.Hadamard(wires=0)
    qml.Hadamard(wires=0)  # These cancel
    qml.RX(0.5, wires=1)
    return qml.expval(qml.PauliZ(0))

# Merge rotations
@transforms.merge_rotations
@qml.qnode(dev)
def circuit():
    qml.RX(0.1, wires=0)
    qml.RX(0.2, wires=0)  # These merge into single RX(0.3)
    return qml.expval(qml.PauliZ(0))

# Commute measurements to end
@transforms.commute_controlled
@qml.qnode(dev)
def circuit():
    qml.Hadamard(wires=0)
    qml.CNOT(wires=[0, 1])
    return qml.expval(qml.PauliZ(0))

Parameter Broadcasting

# Execute circuit with multiple parameter sets
@qml.qnode(dev)
def circuit(x):
    qml.RX(x, wires=0)
    return qml.expval(qml.PauliZ(0))

# Broadcast over parameters
params = np.array([0.1, 0.2, 0.3, 0.4])
results = circuit(params)  # Returns array of results

Metric Tensor

# Compute quantum geometric tensor
@qml.qnode(dev)
def variational_circuit(params):
    for i, param in enumerate(params):
        qml.RY(param, wires=i % 4)
    for i in range(3):
        qml.CNOT(wires=[i, i+1])
    return qml.expval(qml.PauliZ(0))

params = np.array([0.1, 0.2, 0.3, 0.4], requires_grad=True)

# Get metric tensor (useful for quantum natural gradient)
metric_tensor = qml.metric_tensor(variational_circuit)(params)

Tape Manipulation

with qml.tape.QuantumTape() as tape:
    qml.Hadamard(wires=0)
    qml.CNOT(wires=[0, 1])
    qml.RX(0.5, wires=1)
    qml.expval(qml.PauliZ(0))

# Inspect tape
print("Operations:", tape.operations)
print("Observables:", tape.observables)

# Transform tape
expanded_tape = transforms.expand_tape(tape)
optimized_tape = transforms.cancel_inverses(tape)

Decomposition

# Decompose operations into native gate set
@qml.qnode(dev)
def circuit():
    qml.U3(0.1, 0.2, 0.3, wires=0)  # Arbitrary single-qubit gate
    return qml.expval(qml.PauliZ(0))

# Decompose U3 into RZ, RY
decomposed = qml.transforms.decompose(circuit, gate_set={qml.RZ, qml.RY, qml.CNOT})

Pulse Programming

Pulse-Level Control

from pennylane import pulse

# Define pulse envelope
def gaussian_pulse(t, amplitude, sigma):
    return amplitude * np.exp(-(t**2) / (2 * sigma**2))

# Create pulse program
dev_pulse = qml.device('default.qubit', wires=2)

@qml.qnode(dev_pulse)
def pulse_circuit():
    # Apply pulse to qubit
    pulse.drive(
        amplitude=lambda t: gaussian_pulse(t, 1.0, 0.5),
        phase=0.0,
        freq=5.0,
        wires=0,
        duration=2.0
    )

    return qml.expval(qml.PauliZ(0))

Pulse Sequences

@qml.qnode(dev_pulse)
def pulse_sequence():
    # Sequence of pulses
    duration = 1.0

    # X pulse
    pulse.drive(
        amplitude=lambda t: np.sin(np.pi * t / duration),
        phase=0.0,
        freq=5.0,
        wires=0,
        duration=duration
    )

    # Y pulse
    pulse.drive(
        amplitude=lambda t: np.sin(np.pi * t / duration),
        phase=np.pi/2,
        freq=5.0,
        wires=0,
        duration=duration
    )

    return qml.expval(qml.PauliZ(0))

Optimal Control

def optimize_pulse(target_gate):
    """Optimize pulse to implement target gate."""

    def pulse_fn(t, params):
        # Parameterized pulse
        return params[0] * np.sin(params[1] * t + params[2])

    @qml.qnode(dev_pulse)
    def pulse_circuit(params):
        pulse.drive(
            amplitude=lambda t: pulse_fn(t, params),
            phase=0.0,
            freq=5.0,
            wires=0,
            duration=2.0
        )
        return qml.expval(qml.PauliZ(0))

    # Cost: fidelity with target
    def cost(params):
        result_state = pulse_circuit(params)
        target_state = target_gate()
        return 1 - np.abs(np.vdot(result_state, target_state))**2

    # Optimize
    opt = qml.AdamOptimizer(stepsize=0.01)
    params = np.random.random(3, requires_grad=True)

    for i in range(100):
        params = opt.step(cost, params)

    return params

Catalyst and JIT Compilation

Basic JIT Compilation

from catalyst import qjit

dev = qml.device('lightning.qubit', wires=4)

@qjit  # Just-in-time compile
@qml.qnode(dev)
def compiled_circuit(x):
    qml.RX(x, wires=0)
    qml.Hadamard(wires=1)
    qml.CNOT(wires=[0, 1])
    return qml.expval(qml.PauliZ(0))

# First call compiles, subsequent calls are fast
result = compiled_circuit(0.5)

Compiled Control Flow

@qjit
@qml.qnode(dev)
def circuit_with_loops(n):
    qml.Hadamard(wires=0)

    # Compiled for loop
    @qml.for_loop(0, n, 1)
    def loop_body(i):
        qml.RX(0.1 * i, wires=0)

    loop_body()

    return qml.expval(qml.PauliZ(0))

result = circuit_with_loops(10)

Compiled While Loops

@qjit
@qml.qnode(dev)
def circuit_while():
    qml.Hadamard(wires=0)

    # Compiled while loop
    @qml.while_loop(lambda i: i < 10)
    def loop_body(i):
        qml.RX(0.1, wires=0)
        return i + 1

    loop_body(0)

    return qml.expval(qml.PauliZ(0))

Autodiff with JIT

@qjit
@qml.qnode(dev)
def circuit(params):
    qml.RX(params[0], wires=0)
    qml.RY(params[1], wires=1)
    return qml.expval(qml.PauliZ(0))

# Compiled gradient
grad_fn = qjit(qml.grad(circuit))

params = np.array([0.1, 0.2])
gradients = grad_fn(params)

Adaptive Circuits

Mid-Circuit Measurements with Feedback

dev = qml.device('default.qubit', wires=3)

@qml.qnode(dev)
def adaptive_circuit():
    # Prepare state
    qml.Hadamard(wires=0)
    qml.CNOT(wires=[0, 1])

    # Mid-circuit measurement
    m0 = qml.measure(0)

    # Conditional operation based on measurement
    qml.cond(m0, qml.PauliX)(wires=2)

    # Another measurement
    m1 = qml.measure(1)

    # More complex conditional
    qml.cond(m0 & m1, qml.Hadamard)(wires=2)

    return qml.expval(qml.PauliZ(2))

Dynamic Circuit Depth

@qml.qnode(dev)
def dynamic_depth_circuit(max_depth):
    qml.Hadamard(wires=0)

    converged = False
    depth = 0

    while not converged and depth < max_depth:
        # Apply layer
        qml.RX(0.1 * depth, wires=0)

        # Check convergence via measurement
        m = qml.measure(0, reset=True)

        if m == 1:
            converged = True

        depth += 1

    return qml.expval(qml.PauliZ(0))

Quantum Error Correction

def bit_flip_code():
    """3-qubit bit flip error correction."""

    @qml.qnode(dev)
    def circuit():
        # Encode logical qubit
        qml.CNOT(wires=[0, 1])
        qml.CNOT(wires=[0, 2])

        # Simulate error
        qml.PauliX(wires=1)  # Bit flip on qubit 1

        # Syndrome measurement
        qml.CNOT(wires=[0, 3])
        qml.CNOT(wires=[1, 3])
        s1 = qml.measure(3)

        qml.CNOT(wires=[1, 4])
        qml.CNOT(wires=[2, 4])
        s2 = qml.measure(4)

        # Correction
        qml.cond(s1 & ~s2, qml.PauliX)(wires=0)
        qml.cond(s1 & s2, qml.PauliX)(wires=1)
        qml.cond(~s1 & s2, qml.PauliX)(wires=2)

        return qml.expval(qml.PauliZ(0))

    return circuit()

Noise Models

Built-in Noise Channels

dev_noisy = qml.device('default.mixed', wires=2)

@qml.qnode(dev_noisy)
def noisy_circuit():
    qml.Hadamard(wires=0)

    # Depolarizing noise
    qml.DepolarizingChannel(0.1, wires=0)

    qml.CNOT(wires=[0, 1])

    # Amplitude damping (energy loss)
    qml.AmplitudeDamping(0.05, wires=0)

    # Phase damping (dephasing)
    qml.PhaseDamping(0.05, wires=1)

    # Bit flip error
    qml.BitFlip(0.01, wires=0)

    # Phase flip error
    qml.PhaseFlip(0.01, wires=1)

    return qml.expval(qml.PauliZ(0))

Custom Noise Models

def custom_noise(p):
    """Custom noise channel."""
    # Kraus operators for custom noise
    K0 = np.sqrt(1 - p) * np.eye(2)
    K1 = np.sqrt(p/3) * np.array([[0, 1], [1, 0]])  # X
    K2 = np.sqrt(p/3) * np.array([[0, -1j], [1j, 0]])  # Y
    K3 = np.sqrt(p/3) * np.array([[1, 0], [0, -1]])  # Z

    return [K0, K1, K2, K3]

@qml.qnode(dev_noisy)
def circuit_custom_noise():
    qml.Hadamard(wires=0)

    # Apply custom noise
    qml.QubitChannel(custom_noise(0.1), wires=0)

    return qml.expval(qml.PauliZ(0))

Noise-Aware Training

def train_with_noise(circuit, params, noise_level):
    """Train considering hardware noise."""

    dev_ideal = qml.device('default.qubit', wires=4)
    dev_noisy = qml.device('default.mixed', wires=4)

    @qml.qnode(dev_noisy)
    def noisy_circuit(p):
        circuit(p)

        # Add noise after each gate
        for wire in range(4):
            qml.DepolarizingChannel(noise_level, wires=wire)

        return qml.expval(qml.PauliZ(0))

    # Optimize noisy circuit
    opt = qml.AdamOptimizer(stepsize=0.01)

    for i in range(100):
        params = opt.step(noisy_circuit, params)

    return params

Resource Estimation

Count Operations

@qml.qnode(dev)
def circuit(params):
    for i, param in enumerate(params):
        qml.RY(param, wires=i % 4)
    for i in range(3):
        qml.CNOT(wires=[i, i+1])
    return qml.expval(qml.PauliZ(0))

params = np.random.random(10)

# Get resource information
specs = qml.specs(circuit)(params)

print(f"Total gates: {specs['num_operations']}")
print(f"Circuit depth: {specs['depth']}")
print(f"Gate types: {specs['gate_types']}")
print(f"Gate sizes: {specs['gate_sizes']}")
print(f"Trainable params: {specs['num_trainable_params']}")

Estimate Execution Time

import time

def estimate_runtime(circuit, params, n_runs=10):
    """Estimate circuit execution time."""

    times = []
    for _ in range(n_runs):
        start = time.time()
        result = circuit(params)
        times.append(time.time() - start)

    mean_time = np.mean(times)
    std_time = np.std(times)

    print(f"Mean execution time: {mean_time*1000:.2f} ms")
    print(f"Std deviation: {std_time*1000:.2f} ms")

    return mean_time

Resource Requirements

def estimate_resources(n_qubits, depth):
    """Estimate computational resources."""

    # Classical simulation cost
    state_vector_size = 2**n_qubits * 16  # bytes (complex128)

    # Number of operations
    n_operations = depth * n_qubits

    print(f"Qubits: {n_qubits}")
    print(f"Circuit depth: {depth}")
    print(f"State vector size: {state_vector_size / 1e9:.2f} GB")
    print(f"Number of operations: {n_operations}")

    # Approximate simulation time (very rough)
    gate_time = 1e-6  # seconds per gate (varies by device)
    total_time = n_operations * gate_time * 2**n_qubits

    print(f"Estimated simulation time: {total_time:.4f} seconds")

    return {
        'memory': state_vector_size,
        'operations': n_operations,
        'time': total_time
    }

estimate_resources(n_qubits=20, depth=100)

Best Practices

  1. Use templates - Leverage built-in templates for common patterns
  2. Apply transforms - Optimize circuits with transforms before execution
  3. Compile with JIT - Use Catalyst for performance-critical code
  4. Consider noise - Include noise models for realistic hardware simulation
  5. Estimate resources - Profile circuits before running on hardware
  6. Use adaptive circuits - Implement mid-circuit measurements for flexibility
  7. Optimize pulses - Fine-tune pulse parameters for hardware control
  8. Cache compilations - Reuse compiled circuits
  9. Monitor performance - Track execution times and resource usage
  10. Test thoroughly - Validate on simulators before hardware deployment