返回 Skills
k-dense-ai/scientific-agent-skills· MIT 内容可用

pennylane

Hardware-agnostic quantum ML framework with automatic differentiation. Use when training quantum circuits via gradients, building hybrid quantum-classical models, or needing device portability across IBM/Google/Rigetti/IonQ. Best for variational algorithms (VQE, QAOA), quantum neural networks, and integration with PyTorch or JAX. For hardware-specific optimizations use qiskit (IBM) or cirq (Google); for open quantum systems use qutip.

安装

与 skills.sh 相同的 Command / Prompt 安装方式


name: pennylane description: Hardware-agnostic quantum ML framework with automatic differentiation. Use when training quantum circuits via gradients, building hybrid quantum-classical models, or needing device portability across IBM/Google/Rigetti/IonQ. Best for variational algorithms (VQE, QAOA), quantum neural networks, and integration with PyTorch or JAX. For hardware-specific optimizations use qiskit (IBM) or cirq (Google); for open quantum systems use qutip. license: Apache-2.0 license allowed-tools:

  • Read
  • Bash
  • Python metadata: {"version": "1.1", "skill-author": "K-Dense Inc."}

PennyLane

Overview

PennyLane is a quantum computing library that enables training quantum computers like neural networks. It provides automatic differentiation of quantum circuits, device-independent programming, and seamless integration with classical machine learning frameworks.

Installation

PennyLane 0.45.0 requires Python 3.11 or newer. Install using uv with pinned versions for reproducible environments:

uv pip install "pennylane==0.45.0"

For quantum hardware access, install the plugin matching the target provider. Start from a clean environment when adding or upgrading Qiskit because its dependency graph is strict.

# IBM Quantum
uv pip install "pennylane-qiskit==0.45.0"

# Amazon Braket
uv pip install "amazon-braket-pennylane-plugin==1.34.1"

# Google Cirq
uv pip install "pennylane-cirq==0.44.0"

# Rigetti Forest
uv pip install "pennylane-rigetti==0.40.0"

# IonQ
uv pip install "pennylane-ionq==0.45.0"

# High-performance local simulators
uv pip install "pennylane-lightning==0.45.0"

# Catalyst JIT compilation
uv pip install "pennylane-catalyst==0.15.0"

Quick Start

Build a quantum circuit and optimize its parameters:

import pennylane as qml
from pennylane import numpy as np

# Create device
dev = qml.device('default.qubit', wires=2)

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

# Optimize parameters
opt = qml.GradientDescentOptimizer(stepsize=0.1)
params = np.array([0.1, 0.2], requires_grad=True)

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

Core Capabilities

1. Quantum Circuit Construction

Build circuits with gates, measurements, and state preparation. See references/quantum_circuits.md for:

  • Single and multi-qubit gates
  • Controlled operations and conditional logic
  • Mid-circuit measurements and adaptive circuits
  • Various measurement types (expectation, probability, samples)
  • Circuit inspection and debugging

2. Quantum Machine Learning

Create hybrid quantum-classical models. See references/quantum_ml.md for:

  • Integration with PyTorch and JAX
  • Quantum neural networks and variational classifiers
  • Data encoding strategies (angle, amplitude, basis, IQP)
  • Training hybrid models with backpropagation
  • Transfer learning with quantum circuits

3. Quantum Chemistry

Simulate molecules and compute ground state energies. See references/quantum_chemistry.md for:

  • Molecular Hamiltonian generation
  • Variational Quantum Eigensolver (VQE)
  • UCCSD ansatz for chemistry
  • Geometry optimization and dissociation curves
  • Molecular property calculations

4. Device Management

Execute on simulators or quantum hardware. See references/devices_backends.md for:

  • Built-in simulators (default.qubit, lightning.qubit, default.mixed)
  • Hardware plugins (IBM, Amazon Braket, Google, Rigetti, IonQ)
  • Device selection and configuration
  • Performance optimization and caching
  • GPU acceleration and JIT compilation

5. Optimization

Train quantum circuits with various optimizers. See references/optimization.md for:

  • Built-in optimizers (Adam, gradient descent, momentum, RMSProp)
  • Gradient computation methods (backprop, parameter-shift, adjoint)
  • Variational algorithms (VQE, QAOA)
  • Training strategies (learning rate schedules, mini-batches)
  • Handling barren plateaus and local minima

6. Advanced Features

Leverage templates, transforms, and compilation. See references/advanced_features.md for:

  • Circuit templates and layers
  • Transforms and circuit optimization
  • Pulse-level programming
  • Catalyst JIT compilation
  • Noise models and error mitigation
  • Resource estimation

Common Workflows

Train a Variational Classifier

# 1. Define ansatz
@qml.qnode(dev)
def classifier(x, weights):
    # Encode data
    qml.AngleEmbedding(x, wires=range(4))

    # Variational layers
    qml.StronglyEntanglingLayers(weights, wires=range(4))

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

# 2. Train
opt = qml.AdamOptimizer(stepsize=0.01)
weights = np.random.random((3, 4, 3))  # 3 layers, 4 wires

for epoch in range(100):
    for x, y in zip(X_train, y_train):
        weights = opt.step(lambda w: (classifier(x, w) - y)**2, weights)

Run VQE for Molecular Ground State

from pennylane import qchem

# 1. Build Hamiltonian
symbols = ['H', 'H']
geometry = np.array([[0.0, 0.0, -0.66140414], [0.0, 0.0, 0.66140414]])
molecule = qchem.Molecule(symbols, geometry)
H, n_qubits = qchem.molecular_hamiltonian(molecule)
hf_state = qchem.hf_state(electrons=2, orbitals=n_qubits)
singles, doubles = qchem.excitations(electrons=2, orbitals=n_qubits)
s_wires, d_wires = qchem.excitations_to_wires(singles, doubles)

# 2. Define ansatz
@qml.qnode(dev)
def vqe_circuit(params):
    qml.BasisState(hf_state, wires=range(n_qubits))
    qml.UCCSD(params, wires=range(n_qubits), s_wires=s_wires, d_wires=d_wires)
    return qml.expval(H)

# 3. Optimize
opt = qml.AdamOptimizer(stepsize=0.1)
params = np.zeros(len(singles) + len(doubles), requires_grad=True)

for i in range(100):
    params, energy = opt.step_and_cost(vqe_circuit, params)
    print(f"Step {i}: Energy = {energy:.6f} Ha")

Switch Between Devices

# Same circuit, different backends
circuit_def = lambda dev: qml.qnode(dev)(circuit_function)

# Test on simulator
dev_sim = qml.device('default.qubit', wires=4)
result_sim = circuit_def(dev_sim)(params)

# Run on quantum hardware
from qiskit_ibm_runtime import QiskitRuntimeService

service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False, min_num_qubits=4)
dev_hw = qml.device('qiskit.remote', wires=backend.num_qubits, backend=backend)
result_hw = circuit_def(dev_hw)(params)

Detailed Documentation

For comprehensive coverage of specific topics, consult the reference files:

  • Getting started: references/getting_started.md - Installation, basic concepts, first steps
  • Quantum circuits: references/quantum_circuits.md - Gates, measurements, circuit patterns
  • Quantum ML: references/quantum_ml.md - Hybrid models, framework integration, QNNs
  • Quantum chemistry: references/quantum_chemistry.md - VQE, molecular Hamiltonians, chemistry workflows
  • Devices: references/devices_backends.md - Simulators, hardware plugins, device configuration
  • Optimization: references/optimization.md - Optimizers, gradients, variational algorithms
  • Advanced: references/advanced_features.md - Templates, transforms, JIT compilation, noise

Best Practices

  1. Start with simulators - Test on default.qubit before deploying to hardware
  2. Use parameter-shift for hardware - Backpropagation only works on simulators
  3. Choose appropriate encodings - Match data encoding to problem structure
  4. Initialize carefully - Use small random values to avoid barren plateaus
  5. Monitor gradients - Check for vanishing gradients in deep circuits
  6. Cache devices - Reuse device objects to reduce initialization overhead
  7. Profile circuits - Use qml.specs() to analyze circuit complexity
  8. Test locally - Validate on simulators before submitting to hardware
  9. Use templates - Leverage built-in templates for common circuit patterns
  10. Compile when possible - Use Catalyst JIT for performance-critical code

Resources

附带文件

references/advanced_features.md
# Advanced Features in PennyLane

## Table of Contents
1. [Templates and Layers](#templates-and-layers)
2. [Transforms](#transforms)
3. [Pulse Programming](#pulse-programming)
4. [Catalyst and JIT Compilation](#catalyst-and-jit-compilation)
5. [Adaptive Circuits](#adaptive-circuits)
6. [Noise Models](#noise-models)
7. [Resource Estimation](#resource-estimation)

## Templates and Layers

### Built-in Templates

```python
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

```python
@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

```python
@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

```python
@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

```python
@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

```python
# 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

```python
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

```python
# 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

```python
# 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

```python
# 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

```python
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.decompose(tape, gate_set={qml.RX, qml.RY, qml.RZ, qml.CNOT})
optimized_tape = transforms.cancel_inverses(tape)
```

### Decomposition

```python
# 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

```python
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

```python
@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

```python
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

```python
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

```python
@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

```python
@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

```python
@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

```python
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

```python
@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

```python
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

```python
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

```python
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

```python
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

```python
@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

```python
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

```python
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
references/devices_backends.md
# Devices and Backends in PennyLane

## Table of Contents
1. [Built-in Simulators](#built-in-simulators)
2. [Hardware Plugins](#hardware-plugins)
3. [Device Selection](#device-selection)
4. [Device Configuration](#device-configuration)
5. [Custom Devices](#custom-devices)
6. [Performance Optimization](#performance-optimization)

## Built-in Simulators

### default.qubit

General-purpose state vector simulator:

```python
import pennylane as qml

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

# For sampling mode, set shots on the QNode with qml.set_shots
dev = qml.device('default.qubit', wires=4)

# Specify wire labels
dev = qml.device('default.qubit', wires=['a', 'b', 'c', 'd'])
```

### default.mixed

Mixed-state simulator for noisy quantum systems:

```python
# Supports density matrix simulation
dev = qml.device('default.mixed', wires=2)

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

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

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

    # Amplitude damping
    qml.AmplitudeDamping(0.05, wires=1)

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

### Machine Learning Interfaces

Use `default.qubit` with the QNode interface for PyTorch or JAX. The old interface-specific device names are not needed in current PennyLane examples, and TensorFlow support is no longer maintained as of PennyLane v0.44.

```python
# PyTorch
dev = qml.device('default.qubit', wires=4)

@qml.qnode(dev, interface="torch")
def torch_circuit(weights):
    qml.RX(weights[0], wires=0)
    return qml.expval(qml.Z(0))
```

### lightning.qubit

High-performance C++ simulator:

```python
# Faster than default.qubit
dev = qml.device('lightning.qubit', wires=20)

# Supports larger systems efficiently
@qml.qnode(dev)
def large_circuit():
    for i in range(20):
        qml.Hadamard(wires=i)

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

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

### default.clifford

Efficient simulator for Clifford circuits:

```python
# Only supports Clifford gates (H, S, CNOT, etc.)
dev = qml.device('default.clifford', wires=100)

@qml.qnode(dev)
def clifford_circuit():
    qml.Hadamard(wires=0)
    qml.CNOT(wires=[0, 1])
    qml.S(wires=1)
    # Cannot use RX, RY, RZ, etc.

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

## Hardware Plugins

### IBM Quantum (Qiskit)

```bash
# Install plugin
uv pip install "pennylane-qiskit==0.45.0"
```

```python
import pennylane as qml

# Use IBM simulator
dev = qml.device('qiskit.aer', wires=2)

# Use IBM quantum hardware
from qiskit_ibm_runtime import QiskitRuntimeService

service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False, min_num_qubits=2)
dev = qml.device(
    'qiskit.remote',
    wires=backend.num_qubits,
    backend=backend,
)

@qml.qnode(dev)
def circuit():
    qml.Hadamard(wires=0)
    qml.CNOT(wires=[0, 1])
    return qml.expval(qml.PauliZ(0))

sampled_circuit = qml.set_shots(circuit, shots=1024)
```

### Amazon Braket

```bash
# Install plugin
uv pip install "amazon-braket-pennylane-plugin==1.34.1"
```

```python
# Use Braket simulators
dev = qml.device(
    'braket.local.qubit',
    wires=2
)

# Use AWS simulators
dev = qml.device(
    'braket.aws.qubit',
    device_arn='arn:aws:braket:::device/quantum-simulator/amazon/sv1',
    wires=4,
    s3_destination_folder=('amazon-braket-outputs', 'outputs')
)

# Use quantum hardware (IonQ, Rigetti, etc.)
dev = qml.device(
    'braket.aws.qubit',
    device_arn='arn:aws:braket:us-east-1::device/qpu/ionq/Harmony',
    wires=11,
    s3_destination_folder=('amazon-braket-outputs', 'outputs')
)
```

### Google Cirq

```bash
# Install plugin
uv pip install "pennylane-cirq==0.44.0"
```

```python
# Use Cirq simulator
dev = qml.device('cirq.simulator', wires=2)

# Use Cirq with qsim (faster)
dev = qml.device('cirq.qsim', wires=20)

# Use Google quantum hardware (if you have access)
dev = qml.device(
    'cirq.pasqal',
    wires=2,
    device='rainbow',
)
```

### Rigetti Forest

```bash
# Install plugin
uv pip install "pennylane-rigetti==0.40.0"
```

```python
# Use QVM (Quantum Virtual Machine)
dev = qml.device('rigetti.qvm', device='4q-qvm')

# Use Rigetti QPU
dev = qml.device('rigetti.qpu', device='Aspen-M-3')
```

### IonQ

```bash
# Install plugin
uv pip install "pennylane-ionq==0.45.0"
```

```python
# Use IonQ hardware
dev = qml.device(
    'ionq.simulator',  # or 'ionq.qpu'
    wires=11,
    api_key='your_api_key'
)
```

### Xanadu Hardware (Borealis)

```python
# Photonic quantum computer
dev = qml.device(
    'strawberryfields.remote',
    backend='borealis',
)
```

## Device Selection

### Choosing the Right Device

```python
def select_device(n_qubits, use_hardware=False, noise_model=None):
    """Select appropriate device based on requirements."""

    if use_hardware:
        # Use real quantum hardware
        if n_qubits <= 11:
            return qml.device('ionq.qpu', wires=n_qubits)
        elif n_qubits <= 127:
            raise ValueError("Pass a qiskit.remote backend object for IBM hardware")
        else:
            raise ValueError(f"No hardware available for {n_qubits} qubits")

    elif noise_model:
        # Use noisy simulator
        return qml.device('default.mixed', wires=n_qubits)

    else:
        # Use ideal simulator
        if n_qubits <= 20:
            return qml.device('lightning.qubit', wires=n_qubits)
        else:
            return qml.device('default.qubit', wires=n_qubits)

# Usage
dev = select_device(n_qubits=10, use_hardware=False)
```

### Device Capabilities

```python
# Check device capabilities
dev = qml.device('default.qubit', wires=4)

print("Device name:", dev.name)
print("Number of wires:", dev.num_wires)
print("Supports shots:", dev.shots is not None)

# Check supported operations
print("Supported gates:", dev.operations)

# Check supported observables
print("Supported observables:", dev.observables)
```

## Device Configuration

### Setting Shots

```python
# Exact simulation (no shots)
dev = qml.device('default.qubit', wires=2)

@qml.qnode(dev)
def exact_circuit():
    qml.Hadamard(wires=0)
    return qml.expval(qml.PauliZ(0))

result = exact_circuit()  # Returns exact expectation

# Sampling mode (with shots)
dev_sampled = qml.device('default.qubit', wires=2)

@qml.set_shots(1000)
@qml.qnode(dev_sampled)
def sampled_circuit():
    qml.Hadamard(wires=0)
    return qml.expval(qml.PauliZ(0))

result = sampled_circuit()  # Estimated from samples
```

### Dynamic Shots

```python
# Change shots per execution
dev = qml.device('default.qubit', wires=2)

@qml.qnode(dev)
def circuit():
    qml.Hadamard(wires=0)
    return qml.expval(qml.PauliZ(0))

# Different shot numbers
result_100 = qml.set_shots(circuit, shots=100)()
result_1000 = qml.set_shots(circuit, shots=1000)()
result_exact = qml.set_shots(circuit, shots=None)()  # Exact
```

### Analytic Mode vs Finite Shots

```python
# Compare analytic vs sampled
dev_analytic = qml.device('default.qubit', wires=2)
dev_sampled = qml.device('default.qubit', wires=2)

@qml.qnode(dev_analytic)
def circuit_analytic(x):
    qml.RX(x, wires=0)
    return qml.expval(qml.PauliZ(0))

@qml.qnode(dev_sampled)
def circuit_sampled(x):
    qml.RX(x, wires=0)
    return qml.expval(qml.PauliZ(0))

import numpy as np
x = np.pi / 4

print(f"Analytic: {circuit_analytic(x)}")
print(f"Sampled: {qml.set_shots(circuit_sampled, shots=1000)(x)}")
print(f"Exact value: {np.cos(x)}")
```

### Seed for Reproducibility

```python
# Set random seed
dev = qml.device('default.qubit', wires=2, seed=42)

@qml.set_shots(1000)
@qml.qnode(dev)
def circuit():
    qml.Hadamard(wires=0)
    return qml.sample(qml.PauliZ(0))

# Reproducible results
samples1 = circuit()
samples2 = circuit()  # Same as samples1 if seed is set
```

## Custom Devices

### Creating a Custom Device

```python
from pennylane.devices import DefaultQubit

class CustomDevice(DefaultQubit):
    """Custom quantum device with additional features."""

    name = 'Custom device'
    short_name = 'custom'
    pennylane_requires = '>=0.30.0'
    version = '0.1.0'
    author = 'Your Name'

    def __init__(self, wires, shots=None, **kwargs):
        super().__init__(wires=wires, shots=shots)
        # Custom initialization

    def apply(self, operations, **kwargs):
        """Apply operations with custom logic."""
        # Custom operation handling
        for op in operations:
            # Log or modify operations
            print(f"Applying: {op.name}")

        # Call parent implementation
        super().apply(operations, **kwargs)

# Use custom device
dev = CustomDevice(wires=4)
```

### Plugin Development

```python
# Define custom plugin operations
class CustomGate(qml.operation.Operation):
    """Custom quantum gate."""

    num_wires = 1
    num_params = 1
    par_domain = 'R'

    def decomposition(self):
        """Decompose into standard gates."""
        theta = self.parameters[0]
        wires = self.wires

        return [
            qml.RY(theta / 2, wires=wires),
            qml.RZ(theta, wires=wires),
            qml.RY(-theta / 2, wires=wires)
        ]

# Register with device
qml.ops.CustomGate = CustomGate
```

## Performance Optimization

### Batch Execution

```python
# Execute multiple parameter sets efficiently
dev = qml.device('default.qubit', wires=2)

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

# Batch parameters
params_batch = np.random.random((100, 2))

# Vectorized execution (faster)
results = [circuit(p) for p in params_batch]
```

### Device Caching

```python
# Cache device for reuse
_device_cache = {}

def get_device(n_qubits, device_type='default.qubit'):
    """Get or create cached device."""
    key = (device_type, n_qubits)

    if key not in _device_cache:
        _device_cache[key] = qml.device(device_type, wires=n_qubits)

    return _device_cache[key]

# Reuse devices
dev1 = get_device(4)
dev2 = get_device(4)  # Returns same device
```

### JIT Compilation with Catalyst

```python
# Install Catalyst
# uv pip install "pennylane-catalyst==0.15.0"

import pennylane as qml
from catalyst import qjit

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

@qjit  # Just-in-time compilation
@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)
```

### Parallel Execution

```python
from multiprocessing import Pool

def run_circuit(params):
    """Run circuit with given parameters."""
    dev = qml.device('default.qubit', wires=4)

    @qml.qnode(dev)
    def circuit(p):
        # Circuit definition
        return qml.expval(qml.PauliZ(0))

    return circuit(params)

# Parallel execution
param_list = [np.random.random(10) for _ in range(100)]

with Pool(processes=4) as pool:
    results = pool.map(run_circuit, param_list)
```

### GPU Acceleration

```python
# Use GPU-accelerated devices if available
try:
    dev = qml.device('lightning.gpu', wires=20)
except Exception:
    dev = qml.device('lightning.qubit', wires=20)

@qml.qnode(dev)
def gpu_circuit():
    # Large circuit benefits from GPU
    for i in range(20):
        qml.Hadamard(wires=i)

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

    return [qml.expval(qml.PauliZ(i)) for i in range(20)]
```

## Best Practices

1. **Start with simulators** - Test on `default.qubit` before hardware
2. **Use lightning for speed** - Switch to `lightning.qubit` for larger circuits
3. **Match device to task** - Use `default.mixed` for noise studies
4. **Cache devices** - Reuse device objects to avoid initialization overhead
5. **Set appropriate shots** - Balance accuracy vs speed
6. **Check capabilities** - Verify device supports required operations
7. **Handle hardware errors** - Implement retries and error mitigation
8. **Monitor costs** - Track hardware usage and costs
9. **Use JIT when possible** - Compile circuits with Catalyst for speedup
10. **Test locally first** - Validate on simulators before submitting to hardware

## Device Comparison

| Device | Type | Max Qubits | Speed | Noise | Use Case |
|--------|------|-----------|-------|-------|----------|
| default.qubit | Simulator | ~25 | Medium | No | General purpose |
| lightning.qubit | Simulator | ~30 | Fast | No | Large circuits |
| default.mixed | Simulator | ~15 | Slow | Yes | Noise studies |
| default.clifford | Simulator | 100+ | Very fast | No | Clifford circuits |
| IBM Quantum | Hardware | 127 | Slow | Yes | Real experiments |
| IonQ | Hardware | 11 | Slow | Low | High fidelity |
| Rigetti | Hardware | 80 | Slow | Yes | Research |
| Borealis | Hardware | 216 | Slow | Yes | Photonic QC |
references/getting_started.md
# Getting Started with PennyLane

## What is PennyLane?

PennyLane is a cross-platform Python library for quantum computing, quantum machine learning, and quantum chemistry. It enables training quantum computers like neural networks through automatic differentiation and seamless integration with classical machine learning frameworks.

## Installation

Install PennyLane using uv. PennyLane 0.45.0 requires Python 3.11 or newer:

```bash
uv pip install "pennylane==0.45.0"
```

For specific device plugins (IBM, Amazon Braket, Google, Rigetti, etc.):

```bash
# IBM Qiskit
uv pip install "pennylane-qiskit==0.45.0"

# Amazon Braket
uv pip install "amazon-braket-pennylane-plugin==1.34.1"

# Google Cirq
uv pip install "pennylane-cirq==0.44.0"

# Rigetti
uv pip install "pennylane-rigetti==0.40.0"
```

## Core Concepts

### Quantum Nodes (QNodes)

A QNode is a quantum function that can be evaluated on a quantum device. It combines a quantum circuit definition with a device:

```python
import pennylane as qml

# Define a device
dev = qml.device('default.qubit', wires=2)

# Create a QNode
@qml.qnode(dev)
def circuit(params):
    qml.RX(params[0], wires=0)
    qml.RY(params[1], wires=1)
    qml.CNOT(wires=[0, 1])
    return qml.expval(qml.PauliZ(0))
```

### Devices

Devices execute quantum circuits. PennyLane supports:
- **Simulators**: `default.qubit`, `default.mixed`, `lightning.qubit`
- **Hardware**: Access through plugins (IBM, Amazon Braket, Rigetti, etc.)

```python
# Local simulator
dev = qml.device('default.qubit', wires=4)

# Lightning high-performance simulator
dev = qml.device('lightning.qubit', wires=10)
```

### Measurements

PennyLane supports various measurement types:

```python
@qml.qnode(dev)
def measure_circuit():
    qml.Hadamard(wires=0)
    # Expectation value
    return qml.expval(qml.PauliZ(0))

@qml.qnode(dev)
def measure_probs():
    qml.Hadamard(wires=0)
    # Probability distribution
    return qml.probs(wires=[0, 1])

@qml.qnode(dev)
def measure_samples():
    qml.Hadamard(wires=0)
    # Sample measurements
    return qml.sample(qml.PauliZ(0))
```

## Basic Workflow

### 1. Build a Circuit

```python
import pennylane as qml
import numpy as np

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

@qml.qnode(dev)
def quantum_circuit(weights):
    # Apply gates
    qml.RX(weights[0], wires=0)
    qml.RY(weights[1], wires=1)
    qml.CNOT(wires=[0, 1])
    qml.RZ(weights[2], wires=2)

    # Measure
    return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
```

### 2. Compute Gradients

```python
# Automatic differentiation
grad_fn = qml.grad(quantum_circuit)
weights = np.array([0.1, 0.2, 0.3])
gradients = grad_fn(weights)
```

### 3. Optimize Parameters

```python
from pennylane import numpy as np

# Define optimizer
opt = qml.GradientDescentOptimizer(stepsize=0.1)

# Optimization loop
weights = np.array([0.1, 0.2, 0.3], requires_grad=True)
for i in range(100):
    weights = opt.step(quantum_circuit, weights)
    if i % 20 == 0:
        print(f"Step {i}: Cost = {quantum_circuit(weights)}")
```

## Device-Independent Programming

Write circuits once, run anywhere:

```python
# Same circuit, different backends
@qml.qnode(qml.device('default.qubit', wires=2))
def circuit_simulator(x):
    qml.RX(x, wires=0)
    return qml.expval(qml.PauliZ(0))

# Switch to IBM hardware after configuring qiskit-ibm-runtime credentials
from qiskit_ibm_runtime import QiskitRuntimeService

service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False, min_num_qubits=2)

@qml.qnode(qml.device('qiskit.remote', wires=backend.num_qubits, backend=backend))
def circuit_hardware(x):
    qml.RX(x, wires=0)
    return qml.expval(qml.PauliZ(0))
```

## Common Patterns

### Parameterized Circuits

```python
@qml.qnode(dev)
def parameterized_circuit(params, x):
    # Encode data
    qml.RX(x, wires=0)

    # Apply parameterized layers
    for param in params:
        qml.RY(param, wires=0)
        qml.CNOT(wires=[0, 1])

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

### Circuit Templates

Use built-in templates for common patterns:

```python
from pennylane.templates import StronglyEntanglingLayers

@qml.qnode(dev)
def template_circuit(weights):
    StronglyEntanglingLayers(weights, wires=range(3))
    return qml.expval(qml.PauliZ(0))

# Generate random weights for template
n_layers = 2
n_wires = 3
shape = StronglyEntanglingLayers.shape(n_layers, n_wires)
weights = np.random.random(shape)
```

## Debugging and Visualization

### Print Circuit Structure

```python
print(qml.draw(circuit)(params))
print(qml.draw_mpl(circuit)(params))  # Matplotlib visualization
```

### Inspect Operations

```python
with qml.tape.QuantumTape() as tape:
    qml.Hadamard(wires=0)
    qml.CNOT(wires=[0, 1])

print(tape.operations)
print(tape.measurements)
```

## Next Steps

For detailed information on specific topics:
- **Building circuits**: See `references/quantum_circuits.md`
- **Quantum ML**: See `references/quantum_ml.md`
- **Chemistry applications**: See `references/quantum_chemistry.md`
- **Device management**: See `references/devices_backends.md`
- **Optimization**: See `references/optimization.md`
- **Advanced features**: See `references/advanced_features.md`

## Resources

- Official docs: https://docs.pennylane.ai
- Codebook: https://pennylane.ai/codebook
- QML demos: https://pennylane.ai/qml/demonstrations
- Community forum: https://discuss.pennylane.ai
references/optimization.md
# Optimization in PennyLane

## Table of Contents
1. [Built-in Optimizers](#built-in-optimizers)
2. [Gradient Computation](#gradient-computation)
3. [Variational Algorithms](#variational-algorithms)
4. [QAOA](#qaoa-quantum-approximate-optimization-algorithm)
5. [Training Strategies](#training-strategies)
6. [Optimization Challenges](#optimization-challenges)

## Built-in Optimizers

### Gradient Descent Optimizer

```python
import pennylane as qml
from pennylane import numpy as np

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

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

# Initialize optimizer
opt = qml.GradientDescentOptimizer(stepsize=0.1)

# Initialize parameters
params = np.array([0.1, 0.2], requires_grad=True)

# Training loop
for i in range(100):
    params = opt.step(cost_function, params)

    if i % 10 == 0:
        print(f"Step {i}: Cost = {cost_function(params):.6f}")
```

### Adam Optimizer

```python
# Adaptive learning rate optimizer
opt = qml.AdamOptimizer(stepsize=0.01, beta1=0.9, beta2=0.999)

params = np.random.random(10, requires_grad=True)

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

    if i % 10 == 0:
        print(f"Step {i}: Cost = {cost:.6f}")
```

### Momentum Optimizer

```python
# Gradient descent with momentum
opt = qml.MomentumOptimizer(stepsize=0.01, momentum=0.9)

params = np.random.random(5, requires_grad=True)

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

### AdaGrad Optimizer

```python
# Adaptive gradient algorithm
opt = qml.AdagradOptimizer(stepsize=0.1)

params = np.random.random(8, requires_grad=True)

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

### RMSProp Optimizer

```python
# Root mean square propagation
opt = qml.RMSPropOptimizer(stepsize=0.01, decay=0.9, eps=1e-8)

params = np.random.random(6, requires_grad=True)

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

### Nesterov Momentum Optimizer

```python
# Nesterov accelerated gradient
opt = qml.NesterovMomentumOptimizer(stepsize=0.01, momentum=0.9)

params = np.random.random(4, requires_grad=True)

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

## Gradient Computation

### Automatic Differentiation

```python
# Backpropagation (for simulators)
@qml.qnode(dev, diff_method='backprop')
def circuit_backprop(params):
    qml.RX(params[0], wires=0)
    qml.RY(params[1], wires=1)
    return qml.expval(qml.PauliZ(0))

# Compute gradient
grad_fn = qml.grad(circuit_backprop)
params = np.array([0.1, 0.2], requires_grad=True)
gradients = grad_fn(params)
```

### Parameter-Shift Rule

```python
# Hardware-compatible gradient method
@qml.qnode(dev, diff_method='parameter-shift')
def circuit_param_shift(params):
    qml.RX(params[0], wires=0)
    qml.RY(params[1], wires=1)
    qml.CNOT(wires=[0, 1])
    return qml.expval(qml.PauliZ(0))

# Works on quantum hardware
grad_fn = qml.grad(circuit_param_shift)
gradients = grad_fn(params)
```

### Finite Differences

```python
# Numerical gradient approximation
@qml.qnode(dev, diff_method='finite-diff')
def circuit_finite_diff(params):
    qml.RX(params[0], wires=0)
    return qml.expval(qml.PauliZ(0))

grad_fn = qml.grad(circuit_finite_diff)
gradients = grad_fn(params)
```

### Adjoint Method

```python
# Efficient gradient for state vector simulators
@qml.qnode(dev, diff_method='adjoint')
def circuit_adjoint(params):
    qml.RX(params[0], wires=0)
    qml.RY(params[1], wires=1)
    return qml.expval(qml.PauliZ(0))

grad_fn = qml.grad(circuit_adjoint)
gradients = grad_fn(params)
```

### Custom Gradients

```python
@qml.qnode(dev, diff_method='parameter-shift')
def circuit(params):
    qml.RX(params[0], wires=0)
    qml.RY(params[1], wires=1)
    return qml.expval(qml.PauliZ(0))

# Compute Hessian
hessian_fn = qml.jacobian(qml.grad(circuit))
hessian = hessian_fn(params)
```

### Stochastic Parameter-Shift

```python
# For circuits with many parameters
@qml.qnode(dev, diff_method='spsa')  # Simultaneous Perturbation Stochastic Approximation
def large_circuit(params):
    for i, param in enumerate(params):
        qml.RY(param, wires=i % 4)
    return qml.expval(qml.PauliZ(0))

# Efficient for high-dimensional parameter spaces
opt = qml.SPSAOptimizer(maxiter=100)
params = np.random.random(100, requires_grad=True)
params = opt.minimize(large_circuit, params)
```

## Variational Algorithms

### Variational Quantum Eigensolver (VQE)

```python
# Ground state energy calculation
def vqe(hamiltonian, ansatz, n_qubits):
    """VQE implementation."""

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

    @qml.qnode(dev)
    def cost_fn(params):
        ansatz(params, wires=range(n_qubits))
        return qml.expval(hamiltonian)

    # Initialize parameters
    n_params = 10  # Depends on ansatz
    params = np.random.random(n_params, requires_grad=True)

    # Optimize
    opt = qml.AdamOptimizer(stepsize=0.1)

    energies = []
    for i in range(100):
        params, energy = opt.step_and_cost(cost_fn, params)
        energies.append(energy)

        if i % 10 == 0:
            print(f"Step {i}: Energy = {energy:.6f}")

    return params, energy, energies

# Example usage
from pennylane import qchem

symbols = ['H', 'H']
geometry = np.array([[0.0, 0.0, -0.66140414], [0.0, 0.0, 0.66140414]])
molecule = qchem.Molecule(symbols, geometry)
H, n_qubits = qchem.molecular_hamiltonian(molecule)

def simple_ansatz(params, wires):
    qml.BasisState(qchem.hf_state(2, n_qubits), wires=wires)
    for i, param in enumerate(params):
        qml.RY(param, wires=i % len(wires))
    for i in range(len(wires)-1):
        qml.CNOT(wires=[i, i+1])

params, energy, history = vqe(H, simple_ansatz, n_qubits)
```

### Quantum Natural Gradient

```python
# More efficient optimization for variational circuits
@qml.qnode(dev)
def circuit(params):
    for i, param in enumerate(params):
        qml.RY(param, wires=i)
    return qml.expval(qml.PauliZ(0))

# Use quantum natural gradient
opt = qml.QNGOptimizer(stepsize=0.01)
params = np.random.random(4, requires_grad=True)

for i in range(100):
    params, cost = opt.step_and_cost(circuit, params)
```

### Rotosolve

```python
# Analytical parameter update
opt = qml.RotosolveOptimizer()

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

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

for i in range(20):  # Converges quickly
    params = opt.step(cost_fn, params)
```

### Quantum Analytic Descent

```python
# Hybrid quantum-classical optimization
opt = qml.QNSPSAOptimizer(stepsize=0.01)

params = np.random.random(10, requires_grad=True)

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

## QAOA (Quantum Approximate Optimization Algorithm)

### Basic QAOA

```python
from pennylane import qaoa
import networkx as nx

# Define problem: MaxCut on a graph
edges = [(0, 1), (1, 2), (2, 0)]
graph = nx.Graph(edges)

# Cost and mixer Hamiltonians
cost_h, mixer_h = qaoa.maxcut(graph)

# QAOA circuit
def qaoa_layer(gamma, alpha):
    """Single QAOA layer."""
    qaoa.cost_layer(gamma, cost_h)
    qaoa.mixer_layer(alpha, mixer_h)

@qml.qnode(dev)
def qaoa_circuit(params, depth):
    """Full QAOA circuit."""
    # Initialize in superposition
    for wire in range(3):
        qml.Hadamard(wires=wire)

    # Apply QAOA layers
    for i in range(depth):
        gamma = params[i]
        alpha = params[depth + i]
        qaoa_layer(gamma, alpha)

    # Measure in computational basis
    return qml.expval(cost_h)

# Optimize
depth = 3
params = np.random.uniform(0, 2*np.pi, 2*depth, requires_grad=True)

opt = qml.AdamOptimizer(stepsize=0.1)

for i in range(100):
    params = opt.step(lambda p: -qaoa_circuit(p, depth), params)  # Minimize negative = maximize

    if i % 10 == 0:
        print(f"Step {i}: Cost = {-qaoa_circuit(params, depth):.4f}")
```

### QAOA for MaxCut

```python
import networkx as nx

# Create graph
G = nx.cycle_graph(4)

# Generate cost Hamiltonian
cost_h, mixer_h = qaoa.maxcut(G, constrained=False)

n_wires = len(G.nodes)
dev = qml.device('default.qubit', wires=n_wires)

def qaoa_maxcut(params, depth):
    """QAOA for MaxCut problem."""

    @qml.qnode(dev)
    def circuit(gammas, betas):
        # Initialize
        for wire in range(n_wires):
            qml.Hadamard(wires=wire)

        # QAOA layers
        for gamma, beta in zip(gammas, betas):
            # Cost layer
            for edge in G.edges:
                wire1, wire2 = edge
                qml.CNOT(wires=[wire1, wire2])
                qml.RZ(gamma, wires=wire2)
                qml.CNOT(wires=[wire1, wire2])

            # Mixer layer
            for wire in range(n_wires):
                qml.RX(2 * beta, wires=wire)

        return qml.expval(cost_h)

    gammas = params[:depth]
    betas = params[depth:]
    return circuit(gammas, betas)

# Optimize
depth = 3
params = np.random.uniform(0, 2*np.pi, 2*depth, requires_grad=True)

opt = qml.AdamOptimizer(0.1)
for i in range(100):
    params = opt.step(lambda p: -qaoa_maxcut(p, depth), params)
```

### QAOA for QUBO

```python
def qaoa_qubo(Q, depth):
    """QAOA for Quadratic Unconstrained Binary Optimization."""

    n = len(Q)
    dev = qml.device('default.qubit', wires=n)

    # Build cost Hamiltonian from QUBO matrix
    coeffs = []
    obs = []

    for i in range(n):
        for j in range(i, n):
            if Q[i][j] != 0:
                if i == j:
                    coeffs.append(-Q[i][j] / 2)
                    obs.append(qml.PauliZ(i))
                else:
                    coeffs.append(-Q[i][j] / 4)
                    obs.append(qml.PauliZ(i) @ qml.PauliZ(j))

    cost_h = qml.Hamiltonian(coeffs, obs)

    @qml.qnode(dev)
    def circuit(params):
        # Initialize
        for wire in range(n):
            qml.Hadamard(wires=wire)

        # QAOA layers
        for i in range(depth):
            gamma = params[i]
            beta = params[depth + i]

            # Cost layer
            for coeff, op in zip(coeffs, obs):
                qml.exp(op, -1j * gamma * coeff)

            # Mixer layer
            for wire in range(n):
                qml.RX(2 * beta, wires=wire)

        return qml.expval(cost_h)

    return circuit

# Example QUBO
Q = np.array([[1, -2], [-2, 1]])
circuit = qaoa_qubo(Q, depth=2)

params = np.random.random(4, requires_grad=True)
opt = qml.AdamOptimizer(0.1)

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

## Training Strategies

### Learning Rate Scheduling

```python
def train_with_schedule(circuit, initial_params, n_epochs):
    """Train with learning rate decay."""

    params = initial_params
    base_lr = 0.1
    decay_rate = 0.95
    decay_steps = 10

    for epoch in range(n_epochs):
        # Update learning rate
        lr = base_lr * (decay_rate ** (epoch // decay_steps))
        opt = qml.GradientDescentOptimizer(stepsize=lr)

        # Training step
        params = opt.step(circuit, params)

        if epoch % 10 == 0:
            print(f"Epoch {epoch}: LR = {lr:.4f}, Cost = {circuit(params):.4f}")

    return params
```

### Mini-Batch Training

```python
def minibatch_train(circuit, X, y, batch_size=32, n_epochs=100):
    """Mini-batch training for quantum circuits."""

    params = np.random.random(10, requires_grad=True)
    opt = qml.AdamOptimizer(stepsize=0.01)

    n_samples = len(X)

    for epoch in range(n_epochs):
        # Shuffle data
        indices = np.random.permutation(n_samples)
        X_shuffled = X[indices]
        y_shuffled = y[indices]

        # Mini-batch updates
        for i in range(0, n_samples, batch_size):
            X_batch = X_shuffled[i:i+batch_size]
            y_batch = y_shuffled[i:i+batch_size]

            # Compute batch cost
            def batch_cost(p):
                predictions = np.array([circuit(x, p) for x in X_batch])
                return np.mean((predictions - y_batch) ** 2)

            params = opt.step(batch_cost, params)

        if epoch % 10 == 0:
            loss = batch_cost(params)
            print(f"Epoch {epoch}: Loss = {loss:.4f}")

    return params
```

### Early Stopping

```python
def train_with_early_stopping(circuit, params, X_train, X_val, patience=10):
    """Train with early stopping based on validation loss."""

    opt = qml.AdamOptimizer(stepsize=0.01)

    best_val_loss = float('inf')
    patience_counter = 0
    best_params = params.copy()

    for epoch in range(1000):
        # Training step
        params = opt.step(lambda p: cost_fn(p, X_train), params)

        # Validation
        val_loss = cost_fn(params, X_val)

        if val_loss < best_val_loss:
            best_val_loss = val_loss
            best_params = params.copy()
            patience_counter = 0
        else:
            patience_counter += 1

        if patience_counter >= patience:
            print(f"Early stopping at epoch {epoch}")
            break

    return best_params
```

### Gradient Clipping

```python
def train_with_gradient_clipping(circuit, params, max_norm=1.0):
    """Train with gradient clipping to prevent exploding gradients."""

    opt = qml.GradientDescentOptimizer(stepsize=0.1)

    for i in range(100):
        # Compute gradients
        grad_fn = qml.grad(circuit)
        grads = grad_fn(params)

        # Clip gradients
        grad_norm = np.linalg.norm(grads)
        if grad_norm > max_norm:
            grads = grads * (max_norm / grad_norm)

        # Manual update with clipped gradients
        params = params - opt.stepsize * grads

        if i % 10 == 0:
            print(f"Step {i}: Grad norm = {grad_norm:.4f}")

    return params
```

## Optimization Challenges

### Barren Plateaus

```python
def detect_barren_plateau(circuit, params, n_samples=100):
    """Detect barren plateau by measuring gradient variance."""

    grad_fn = qml.grad(circuit)
    grad_variances = []

    for _ in range(n_samples):
        # Random initialization
        random_params = np.random.uniform(-np.pi, np.pi, len(params))

        # Compute gradient
        grads = grad_fn(random_params)
        grad_variances.append(np.var(grads))

    mean_var = np.mean(grad_variances)

    print(f"Mean gradient variance: {mean_var:.6f}")

    if mean_var < 1e-6:
        print("Warning: Barren plateau detected!")

    return mean_var
```

### Parameter Initialization

```python
def initialize_params_smart(n_params, strategy='small_random'):
    """Smart parameter initialization strategies."""

    if strategy == 'small_random':
        # Small random values
        return np.random.uniform(-0.1, 0.1, n_params, requires_grad=True)

    elif strategy == 'xavier':
        # Xavier initialization
        return np.random.normal(0, 1/np.sqrt(n_params), n_params, requires_grad=True)

    elif strategy == 'identity':
        # Start near identity (zeros for rotations)
        return np.zeros(n_params, requires_grad=True)

    elif strategy == 'layerwise':
        # Layer-dependent initialization
        return np.array([0.1 / (i+1) for i in range(n_params)], requires_grad=True)
```

### Local Minima Escape

```python
def train_with_restarts(circuit, n_restarts=5):
    """Multiple random restarts to escape local minima."""

    best_cost = float('inf')
    best_params = None

    for restart in range(n_restarts):
        # Random initialization
        params = np.random.uniform(-np.pi, np.pi, 10, requires_grad=True)

        # Optimize
        opt = qml.AdamOptimizer(stepsize=0.1)
        for i in range(100):
            params = opt.step(circuit, params)

        # Check if better
        cost = circuit(params)
        if cost < best_cost:
            best_cost = cost
            best_params = params

        print(f"Restart {restart}: Cost = {cost:.6f}")

    return best_params, best_cost
```

## Best Practices

1. **Choose appropriate optimizer** - Adam for general use, QNG for variational circuits
2. **Use parameter-shift on hardware** - Backprop only works on simulators
3. **Initialize carefully** - Avoid barren plateaus with smart initialization
4. **Monitor gradients** - Check for vanishing/exploding gradients
5. **Use learning rate schedules** - Decay learning rate over time
6. **Try multiple restarts** - Escape local minima
7. **Validate on test set** - Prevent overfitting
8. **Profile optimization** - Identify bottlenecks
9. **Clip gradients** - Prevent instability
10. **Start shallow** - Use fewer layers initially, then grow
references/quantum_chemistry.md
# Quantum Chemistry with PennyLane

## Table of Contents
1. [Molecular Hamiltonians](#molecular-hamiltonians)
2. [Variational Quantum Eigensolver (VQE)](#variational-quantum-eigensolver-vqe)
3. [Molecular Structure](#molecular-structure)
4. [Basis Sets and Mapping](#basis-sets-and-mapping)
5. [Excited States](#excited-states)
6. [Quantum Chemistry Workflows](#quantum-chemistry-workflows)

## Molecular Hamiltonians

### Building Molecular Hamiltonians

```python
import pennylane as qml
from pennylane import qchem
import numpy as np

# Define molecule
symbols = ['H', 'H']
geometry = np.array([[0.0, 0.0, -0.66140414], [0.0, 0.0, 0.66140414]])
molecule = qchem.Molecule(
    symbols,
    geometry,
    charge=0,
    mult=1,
    basis_name='sto-3g',
)

# Generate Hamiltonian
hamiltonian, n_qubits = qchem.molecular_hamiltonian(
    molecule,
    mapping='jordan_wigner',
)

print(f"Hamiltonian: {hamiltonian}")
print(f"Number of qubits needed: {n_qubits}")
```

### Jordan-Wigner Transformation

```python
# Hamiltonian is automatically in qubit form via Jordan-Wigner
# Manual transformation:
from pennylane import fermi

# Fermionic operators
a_0 = fermi.FermiC(0)  # Creation operator
a_1 = fermi.FermiA(1)  # Annihilation operator

# Convert to qubits
qubit_op = qml.jordan_wigner(a_0 * a_1)
```

### Bravyi-Kitaev Transformation

```python
# Alternative mapping (more efficient for some systems)
from pennylane.qchem import bravyi_kitaev

# Build Hamiltonian with Bravyi-Kitaev
hamiltonian, n_qubits = qchem.molecular_hamiltonian(
    molecule,
    mapping='bravyi_kitaev'
)
```

### Custom Hamiltonians

```python
# Build Hamiltonian from coefficients and operators
coeffs = [0.2, -0.8, 0.5]
obs = [
    qml.PauliZ(0),
    qml.PauliZ(0) @ qml.PauliZ(1),
    qml.PauliX(0) @ qml.PauliX(1)
]

H = qml.Hamiltonian(coeffs, obs)

# Or use simplified syntax
H = 0.2 * qml.PauliZ(0) - 0.8 * qml.PauliZ(0) @ qml.PauliZ(1) + 0.5 * qml.PauliX(0) @ qml.PauliX(1)
```

## Variational Quantum Eigensolver (VQE)

### Basic VQE Implementation

```python
from pennylane import numpy as np

# Define device
dev = qml.device('default.qubit', wires=n_qubits)

# Hartree-Fock state preparation
hf_state = qchem.hf_state(electrons=2, orbitals=n_qubits)

def ansatz(params, wires):
    """Variational ansatz."""
    qml.BasisState(hf_state, wires=wires)

    for i in range(len(wires)):
        qml.RY(params[i], wires=i)

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

@qml.qnode(dev)
def vqe_circuit(params):
    ansatz(params, wires=range(n_qubits))
    return qml.expval(hamiltonian)

# Optimize
opt = qml.GradientDescentOptimizer(stepsize=0.4)
params = np.random.normal(0, np.pi, n_qubits, requires_grad=True)

for n in range(100):
    params, energy = opt.step_and_cost(vqe_circuit, params)

    if n % 20 == 0:
        print(f"Step {n}: Energy = {energy:.8f} Ha")

print(f"Final ground state energy: {energy:.8f} Ha")
```

### UCCSD Ansatz

```python
# Singles and doubles excitations
singles, doubles = qchem.excitations(electrons=2, orbitals=n_qubits)
s_wires, d_wires = qchem.excitations_to_wires(singles, doubles)

@qml.qnode(dev)
def uccsd_circuit(params):
    # Hartree-Fock reference
    qml.BasisState(hf_state, wires=range(n_qubits))

    # UCCSD ansatz
    qml.UCCSD(params, wires=range(n_qubits), s_wires=s_wires, d_wires=d_wires)

    return qml.expval(hamiltonian)

# Initialize parameters
n_params = len(singles) + len(doubles)
params = np.zeros(n_params, requires_grad=True)

# Optimize
opt = qml.AdamOptimizer(stepsize=0.1)
for n in range(100):
    params, energy = opt.step_and_cost(uccsd_circuit, params)
```

### Adaptive VQE

```python
def adaptive_vqe(hamiltonian, n_qubits, max_gates=10):
    """Adaptive VQE: Grow ansatz iteratively."""
    dev = qml.device('default.qubit', wires=n_qubits)

    # Start with HF state
    operations = []
    params = []

    hf_state = qchem.hf_state(electrons=2, orbitals=n_qubits)

    @qml.qnode(dev)
    def circuit(p):
        qml.BasisState(hf_state, wires=range(n_qubits))

        for op, param in zip(operations, p):
            op(param)

        return qml.expval(hamiltonian)

    # Iteratively add gates
    for _ in range(max_gates):
        # Find best gate to add
        best_op = None
        best_improvement = 0

        for candidate_op in generate_candidates():
            # Test adding this operation
            test_ops = operations + [candidate_op]
            test_params = params + [0.0]

            improvement = evaluate_improvement(test_ops, test_params)

            if improvement > best_improvement:
                best_improvement = improvement
                best_op = candidate_op

        if best_improvement < threshold:
            break

        operations.append(best_op)
        params.append(0.0)

        # Optimize current ansatz
        opt = qml.AdamOptimizer(stepsize=0.1)
        for _ in range(50):
            params = opt.step(circuit, params)

    return circuit, params
```

## Molecular Structure

### Defining Molecules

```python
# Simple diatomic
h2_symbols = ['H', 'H']
h2_coords = np.array([[0.0, 0.0, -0.66140414], [0.0, 0.0, 0.66140414]])

# Water molecule
h2o_symbols = ['O', 'H', 'H']
h2o_coords = np.array([
    [0.0, 0.0, 0.0],      # O
    [0.757, 0.586, 0.0],  # H
    [-0.757, 0.586, 0.0], # H
])

# From XYZ format
symbols, geometry = qchem.read_structure('molecule.xyz')
molecule = qchem.Molecule(symbols, geometry)
```

### Geometry Optimization

```python
def optimize_geometry(symbols, initial_coords, basis='sto-3g'):
    """Optimize molecular geometry."""

    def energy_surface(coords):
        molecule = qchem.Molecule(symbols, coords, basis_name=basis)
        H, n_qubits = qchem.molecular_hamiltonian(
            molecule
        )

        # Run VQE to get energy
        energy = run_vqe(H, n_qubits)
        return energy

    # Classical optimization of nuclear coordinates
    from scipy.optimize import minimize

    result = minimize(
        energy_surface,
        initial_coords,
        method='BFGS',
        options={'gtol': 1e-5}
    )

    return result.x, result.fun

optimized_coords, min_energy = optimize_geometry(h2_symbols, h2_coords)
print(f"Optimized geometry: {optimized_coords}")
print(f"Energy: {min_energy} Ha")
```

### Bond Dissociation Curves

```python
def dissociation_curve(symbols, axis=2, distances=None):
    """Calculate potential energy surface."""

    if distances is None:
        distances = np.linspace(0.5, 3.0, 20)

    energies = []

    for d in distances:
        coords = np.array([[0.0, 0.0, 0.0], [0.0, 0.0, d]])
        molecule = qchem.Molecule(symbols, coords, basis_name='sto-3g')

        H, n_qubits = qchem.molecular_hamiltonian(
            molecule
        )

        energy = run_vqe(H, n_qubits)
        energies.append(energy)

        print(f"Distance: {d:.2f} Å, Energy: {energy:.6f} Ha")

    return distances, energies

# H2 dissociation
distances, energies = dissociation_curve(['H', 'H'])

import matplotlib.pyplot as plt
plt.plot(distances, energies)
plt.xlabel('Bond length (Å)')
plt.ylabel('Energy (Ha)')
plt.title('H2 Dissociation Curve')
plt.show()
```

## Basis Sets and Mapping

### Basis Set Selection

```python
# Minimal basis (fastest, least accurate)
molecule = qchem.Molecule(symbols, coords, basis_name='sto-3g')
H_sto3g, n_qubits = qchem.molecular_hamiltonian(
    molecule
)

# Double-zeta basis
molecule = qchem.Molecule(symbols, coords, basis_name='6-31g')
H_631g, n_qubits = qchem.molecular_hamiltonian(
    molecule
)

# Large basis (slower, more accurate)
molecule = qchem.Molecule(symbols, coords, basis_name='cc-pvdz')
H_ccpvdz, n_qubits = qchem.molecular_hamiltonian(
    molecule
)
```

### Active Space Selection

```python
# Select active orbitals
active_electrons = 2
active_orbitals = 2

molecule = qchem.Molecule(symbols, coords, basis_name='sto-3g')
H_active, n_qubits = qchem.molecular_hamiltonian(
    molecule,
    active_electrons=active_electrons,
    active_orbitals=active_orbitals,
)

print(f"Full system: {len(symbols)} electrons")
print(f"Active space: {active_electrons} electrons in {active_orbitals} orbitals")
print(f"Qubits needed: {n_qubits}")
```

### Fermion-to-Qubit Mappings

```python
# Jordan-Wigner (default)
H_jw, n_q_jw = qchem.molecular_hamiltonian(
    molecule, mapping='jordan_wigner'
)

# Bravyi-Kitaev
H_bk, n_q_bk = qchem.molecular_hamiltonian(
    molecule, mapping='bravyi_kitaev'
)

# Parity
H_parity, n_q_parity = qchem.molecular_hamiltonian(
    molecule, mapping='parity'
)

print(f"Jordan-Wigner terms: {len(H_jw.ops)}")
print(f"Bravyi-Kitaev terms: {len(H_bk.ops)}")
```

## Excited States

### Quantum Subspace Expansion

```python
def quantum_subspace_expansion(hamiltonian, ground_state_params, excitations):
    """Calculate excited states via subspace expansion."""

    @qml.qnode(dev)
    def ground_state():
        ansatz(ground_state_params, wires=range(n_qubits))
        return qml.state()

    # Get ground state
    psi_0 = ground_state()

    # Generate excited state basis
    basis = [psi_0]

    for exc in excitations:
        @qml.qnode(dev)
        def excited_state():
            ansatz(ground_state_params, wires=range(n_qubits))
            # Apply excitation
            apply_excitation(exc)
            return qml.state()

        psi_exc = excited_state()
        basis.append(psi_exc)

    # Build Hamiltonian matrix in subspace
    n_basis = len(basis)
    H_matrix = np.zeros((n_basis, n_basis))

    for i in range(n_basis):
        for j in range(n_basis):
            H_matrix[i, j] = np.vdot(basis[i], hamiltonian @ basis[j])

    # Diagonalize
    eigenvalues, eigenvectors = np.linalg.eigh(H_matrix)

    return eigenvalues, eigenvectors
```

### SSVQE (Subspace-Search VQE)

```python
def ssvqe(hamiltonian, n_states=3):
    """Calculate multiple states simultaneously."""

    def cost_function(params):
        states = []

        for i in range(n_states):
            @qml.qnode(dev)
            def state_i():
                ansatz(params[i], wires=range(n_qubits))
                return qml.state()

            states.append(state_i())

        # Energy expectation
        energies = [np.vdot(s, hamiltonian @ s) for s in states]

        # Orthogonality penalty
        penalty = 0
        for i in range(n_states):
            for j in range(i+1, n_states):
                overlap = np.abs(np.vdot(states[i], states[j]))
                penalty += overlap ** 2

        return sum(energies) + 1000 * penalty

    # Initialize parameters for all states
    params = [np.random.random(n_params) for _ in range(n_states)]

    opt = qml.AdamOptimizer(stepsize=0.01)
    for _ in range(100):
        params = opt.step(cost_function, params)

    return params
```

## Quantum Chemistry Workflows

### Full VQE Workflow

```python
def full_chemistry_workflow(symbols, coords, basis='sto-3g'):
    """Complete quantum chemistry calculation."""

    print("1. Building molecular Hamiltonian...")
    molecule = qchem.Molecule(symbols, coords, basis_name=basis)
    H, n_qubits = qchem.molecular_hamiltonian(
        molecule
    )

    print(f"   Molecule: {' '.join(symbols)}")
    print(f"   Qubits: {n_qubits}")
    print(f"   Hamiltonian terms: {len(H.ops)}")

    print("\n2. Preparing Hartree-Fock state...")
    n_electrons = sum(qchem.atomic_numbers[s] for s in symbols)
    hf_state = qchem.hf_state(n_electrons, n_qubits)

    print("\n3. Running VQE...")
    energy, params = run_vqe(H, n_qubits, hf_state)

    print(f"\n4. Results:")
    print(f"   Ground state energy: {energy:.8f} Ha")

    print("\n5. Computing properties...")
    dipole = compute_dipole_moment(symbols, coords, params)
    print(f"   Dipole moment: {dipole:.4f} D")

    return {
        'energy': energy,
        'params': params,
        'dipole': dipole
    }

results = full_chemistry_workflow(['H', 'H'], h2_coords)
```

### Molecular Property Calculation

```python
def compute_molecular_properties(symbols, coords, vqe_params):
    """Calculate molecular properties from VQE solution."""

    # Energy
    molecule = qchem.Molecule(symbols, coords)
    H, n_qubits = qchem.molecular_hamiltonian(molecule)
    energy = vqe_circuit(vqe_params)

    # Dipole moment
    dipole_obs = qchem.dipole_moment(molecule)

    @qml.qnode(dev)
    def dipole_circuit(axis):
        ansatz(vqe_params, wires=range(n_qubits))
        return qml.expval(dipole_obs[axis])

    dipole = [dipole_circuit(i) for i in range(3)]
    dipole_magnitude = np.linalg.norm(dipole)

    # Particle number (sanity check)
    @qml.qnode(dev)
    def particle_number():
        ansatz(vqe_params, wires=range(n_qubits))
        N_op = qchem.particle_number(n_qubits)
        return qml.expval(N_op)

    n_particles = particle_number()

    return {
        'energy': energy,
        'dipole_moment': dipole_magnitude,
        'dipole_vector': dipole,
        'particle_number': n_particles
    }
```

### Reaction Energy Calculation

```python
def reaction_energy(reactants, products):
    """Calculate energy of chemical reaction."""

    # Calculate energies of reactants
    E_reactants = 0
    for molecule in reactants:
        symbols, coords = molecule
        mol = qchem.Molecule(symbols, coords)
        H, n_qubits = qchem.molecular_hamiltonian(mol)
        E_reactants += run_vqe(H, n_qubits)

    # Calculate energies of products
    E_products = 0
    for molecule in products:
        symbols, coords = molecule
        mol = qchem.Molecule(symbols, coords)
        H, n_qubits = qchem.molecular_hamiltonian(mol)
        E_products += run_vqe(H, n_qubits)

    # Reaction energy
    delta_E = E_products - E_reactants

    print(f"Reactant energy: {E_reactants:.6f} Ha")
    print(f"Product energy: {E_products:.6f} Ha")
    print(f"Reaction energy: {delta_E:.6f} Ha ({delta_E * 627.5:.2f} kcal/mol)")

    return delta_E

# Example: H2 dissociation
reactants = [((['H', 'H'], h2_coords_bonded))]
products = [((['H'], [0, 0, 0]), (['H'], [10, 0, 0]))]  # Separated atoms

delta_E = reaction_energy(reactants, products)
```

## Best Practices

1. **Start with small basis sets** - Use STO-3G for testing, upgrade for production
2. **Use active space** - Reduce qubits by selecting relevant orbitals
3. **Choose appropriate mapping** - Bravyi-Kitaev often reduces circuit depth
4. **Initialize with HF** - Start VQE from Hartree-Fock state
5. **Validate results** - Compare with classical methods (FCI, CCSD)
6. **Consider symmetries** - Exploit molecular symmetries to reduce complexity
7. **Use UCCSD for accuracy** - UCCSD ansatz is chemically motivated
8. **Monitor convergence** - Check gradient norms and energy variance
9. **Account for correlation** - Ensure ansatz captures electron correlation
10. **Benchmark thoroughly** - Test on known systems before novel molecules
references/quantum_circuits.md
# Quantum Circuits in PennyLane

## Table of Contents
1. [Basic Gates and Operations](#basic-gates-and-operations)
2. [Multi-Qubit Gates](#multi-qubit-gates)
3. [Controlled Operations](#controlled-operations)
4. [Measurements](#measurements)
5. [Circuit Construction Patterns](#circuit-construction-patterns)
6. [Dynamic Circuits](#dynamic-circuits)
7. [Circuit Inspection](#circuit-inspection)

## Basic Gates and Operations

### Single-Qubit Gates

```python
import pennylane as qml

# Pauli gates
qml.PauliX(wires=0)  # X gate (bit flip)
qml.PauliY(wires=0)  # Y gate
qml.PauliZ(wires=0)  # Z gate (phase flip)

# Hadamard gate (superposition)
qml.Hadamard(wires=0)

# Phase gates
qml.S(wires=0)       # S gate (π/2 phase)
qml.T(wires=0)       # T gate (π/4 phase)
qml.PhaseShift(phi, wires=0)  # Arbitrary phase

# Rotation gates (parameterized)
qml.RX(theta, wires=0)  # Rotation around X-axis
qml.RY(theta, wires=0)  # Rotation around Y-axis
qml.RZ(theta, wires=0)  # Rotation around Z-axis

# General single-qubit rotation
qml.Rot(phi, theta, omega, wires=0)

# Universal gate (any single-qubit unitary)
qml.U3(theta, phi, delta, wires=0)
```

### Basis State Preparation

```python
# Computational basis state
qml.BasisState([1, 0, 1], wires=[0, 1, 2])  # |101⟩

# Amplitude encoding
amplitudes = [0.5, 0.5, 0.5, 0.5]  # Must be normalized
qml.MottonenStatePreparation(amplitudes, wires=[0, 1])
```

## Multi-Qubit Gates

### Two-Qubit Gates

```python
# CNOT (Controlled-NOT)
qml.CNOT(wires=[0, 1])  # control=0, target=1

# CZ (Controlled-Z)
qml.CZ(wires=[0, 1])

# SWAP gate
qml.SWAP(wires=[0, 1])

# Controlled rotations
qml.CRX(theta, wires=[0, 1])
qml.CRY(theta, wires=[0, 1])
qml.CRZ(theta, wires=[0, 1])

# Ising coupling gates
qml.IsingXX(phi, wires=[0, 1])
qml.IsingYY(phi, wires=[0, 1])
qml.IsingZZ(phi, wires=[0, 1])
```

### Multi-Qubit Gates

```python
# Toffoli gate (CCNOT)
qml.Toffoli(wires=[0, 1, 2])  # control=0,1, target=2

# Multi-controlled X
qml.MultiControlledX(control_wires=[0, 1, 2], wires=3)

# Multi-qubit Pauli rotations
qml.MultiRZ(theta, wires=[0, 1, 2])
```

## Controlled Operations

### General Controlled Operations

```python
# Apply controlled version of any operation
qml.ctrl(qml.RX(0.5, wires=1), control=0)

# Multiple control qubits
qml.ctrl(qml.RY(0.3, wires=2), control=[0, 1])

# Negative controls (activate when control is |0⟩)
qml.ctrl(qml.Hadamard(wires=2), control=0, control_values=[0])
```

### Conditional Operations

```python
@qml.qnode(dev)
def conditional_circuit():
    qml.Hadamard(wires=0)

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

    # Apply gate conditionally
    qml.cond(m, qml.PauliX)(wires=1)

    return qml.expval(qml.PauliZ(1))
```

## Measurements

### Expectation Values

```python
@qml.qnode(dev)
def measure_expectation():
    qml.Hadamard(wires=0)

    # Single observable
    return qml.expval(qml.PauliZ(0))

@qml.qnode(dev)
def measure_tensor():
    qml.Hadamard(wires=0)
    qml.Hadamard(wires=1)

    # Tensor product of observables
    return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
```

### Probability Distributions

```python
@qml.qnode(dev)
def measure_probabilities():
    qml.Hadamard(wires=0)
    qml.CNOT(wires=[0, 1])

    # Probabilities of all basis states
    return qml.probs(wires=[0, 1])  # Returns [p(|00⟩), p(|01⟩), p(|10⟩), p(|11⟩)]
```

### Samples and Counts

```python
@qml.set_shots(1000)
@qml.qnode(dev)
def measure_samples():
    qml.Hadamard(wires=0)

    # Raw samples
    return qml.sample(qml.PauliZ(0))

@qml.set_shots(1000)
@qml.qnode(dev)
def measure_counts():
    qml.Hadamard(wires=0)
    qml.CNOT(wires=[0, 1])

    # Count occurrences
    return qml.counts(wires=[0, 1])
```

### Variance

```python
@qml.qnode(dev)
def measure_variance():
    qml.RX(0.5, wires=0)

    # Variance of observable
    return qml.var(qml.PauliZ(0))
```

### Mid-Circuit Measurements

```python
@qml.qnode(dev)
def mid_circuit_measure():
    qml.Hadamard(wires=0)

    # Measure qubit 0 during circuit
    m0 = qml.measure(0)

    # Use measurement result
    qml.cond(m0, qml.PauliX)(wires=1)

    # Final measurement
    return qml.expval(qml.PauliZ(1))
```

## Circuit Construction Patterns

### Layer-Based Construction

```python
def layer(weights, wires):
    """Single layer of parameterized gates."""
    for i, wire in enumerate(wires):
        qml.RY(weights[i], wires=wire)

    for wire in wires[:-1]:
        qml.CNOT(wires=[wire, wire+1])

@qml.qnode(dev)
def layered_circuit(weights):
    n_layers = len(weights)
    wires = range(4)

    for i in range(n_layers):
        layer(weights[i], wires)

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

### Data Encoding

```python
def angle_encoding(x, wires):
    """Encode classical data as rotation angles."""
    for i, wire in enumerate(wires):
        qml.RX(x[i], wires=wire)

def amplitude_encoding(x, wires):
    """Encode data as quantum state amplitudes."""
    qml.MottonenStatePreparation(x, wires=wires)

def basis_encoding(x, wires):
    """Encode binary data in computational basis."""
    for i, val in enumerate(x):
        if val:
            qml.PauliX(wires=i)
```

### Ansatz Patterns

```python
# Hardware-efficient ansatz
def hardware_efficient_ansatz(weights, wires):
    n_layers = len(weights) // len(wires)

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

        # Entanglement layer
        for wire in wires[:-1]:
            qml.CNOT(wires=[wire, wire+1])

# Alternating layered ansatz
def alternating_ansatz(weights, wires):
    for w in weights:
        for wire in wires:
            qml.RX(w[wire], wires=wire)
        for wire in wires[:-1]:
            qml.CNOT(wires=[wire, wire+1])
```

## Dynamic Circuits

### For Loops

```python
@qml.qnode(dev)
def dynamic_for_loop(n_iterations):
    qml.Hadamard(wires=0)

    # Dynamic for loop
    for i in range(n_iterations):
        qml.RX(0.1 * i, wires=0)

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

### While Loops (with Catalyst)

```python
from catalyst import qjit

compiled_dev = qml.device("lightning.qubit", wires=1)

@qjit  # Just-in-time compilation with Catalyst
@qml.qnode(compiled_dev)
def dynamic_while_loop():
    qml.Hadamard(wires=0)

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

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

### Adaptive Circuits

```python
@qml.qnode(dev)
def adaptive_circuit():
    qml.Hadamard(wires=0)

    # Measure and adapt
    m = qml.measure(0)

    # Different paths based on measurement
    if m:
        qml.RX(0.5, wires=1)
    else:
        qml.RY(0.5, wires=1)

    return qml.expval(qml.PauliZ(1))
```

## Circuit Inspection

### Drawing Circuits

```python
# Text representation
print(qml.draw(circuit)(params))

# ASCII art
print(qml.draw(circuit, wire_order=[0,1,2])(params))

# Matplotlib visualization
fig, ax = qml.draw_mpl(circuit)(params)
```

### Analyzing Circuit Structure

```python
# Get circuit specs
specs = qml.specs(circuit)(params)
print(f"Gates: {specs['gate_sizes']}")
print(f"Depth: {specs['depth']}")
print(f"Parameters: {specs['num_trainable_params']}")

# Resource estimation
resources = qml.specs(circuit)(params)["resources"]
print(f"Total gates: {resources.num_gates}")
```

### Tape Inspection

```python
# Record operations
with qml.tape.QuantumTape() as tape:
    qml.Hadamard(wires=0)
    qml.CNOT(wires=[0, 1])
    qml.expval(qml.PauliZ(0))

# Inspect tape contents
print("Operations:", tape.operations)
print("Measurements:", tape.measurements)
print("Wires used:", tape.wires)
```

### Circuit Transformations

```python
# Expand composite operations to a target gate set
expanded = qml.transforms.decompose(tape, gate_set={qml.RX, qml.RY, qml.RZ, qml.CNOT})

# Cancel adjacent operations
optimized = qml.transforms.cancel_inverses(tape)

# Commute measurements to end
commuted = qml.transforms.commute_controlled(tape)
```

## Best Practices

1. **Use native gates** - Prefer gates supported by target device
2. **Minimize circuit depth** - Reduce decoherence effects
3. **Encode efficiently** - Choose encoding matching data structure
4. **Reuse circuits** - Cache compiled circuits when possible
5. **Validate measurements** - Ensure observables are Hermitian
6. **Check qubit count** - Verify device has sufficient wires
7. **Profile circuits** - Use `qml.specs()` to analyze complexity

## Common Patterns

### Bell State Preparation

```python
@qml.qnode(dev)
def bell_state():
    qml.Hadamard(wires=0)
    qml.CNOT(wires=[0, 1])
    return qml.state()  # Returns |Φ+⟩ = (|00⟩ + |11⟩)/√2
```

### GHZ State

```python
@qml.qnode(dev)
def ghz_state(n_qubits):
    qml.Hadamard(wires=0)
    for i in range(n_qubits-1):
        qml.CNOT(wires=[0, i+1])
    return qml.state()
```

### Quantum Fourier Transform

```python
def qft(wires):
    """Quantum Fourier Transform."""
    n_wires = len(wires)
    for i in range(n_wires):
        qml.Hadamard(wires=wires[i])
        for j in range(i+1, n_wires):
            qml.CRZ(np.pi / (2**(j-i)), wires=[wires[j], wires[i]])
```

### Inverse QFT

```python
def inverse_qft(wires):
    """Inverse Quantum Fourier Transform."""
    n_wires = len(wires)
    for i in range(n_wires-1, -1, -1):
        for j in range(n_wires-1, i, -1):
            qml.CRZ(-np.pi / (2**(j-i)), wires=[wires[j], wires[i]])
        qml.Hadamard(wires=wires[i])
```
references/quantum_ml.md
# Quantum Machine Learning with PennyLane

## Table of Contents
1. [Hybrid Quantum-Classical Models](#hybrid-quantum-classical-models)
2. [Framework Integration](#framework-integration)
3. [Quantum Neural Networks](#quantum-neural-networks)
4. [Variational Classifiers](#variational-classifiers)
5. [Training and Optimization](#training-and-optimization)
6. [Data Encoding Strategies](#data-encoding-strategies)
7. [Transfer Learning](#transfer-learning)

## Hybrid Quantum-Classical Models

### Basic Hybrid Model

```python
import pennylane as qml
import numpy as np

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

@qml.qnode(dev)
def quantum_layer(inputs, weights):
    # Encode classical data
    for i, inp in enumerate(inputs):
        qml.RY(inp, wires=i)

    # Parameterized quantum circuit
    for wire in range(4):
        qml.RX(weights[wire], wires=wire)

    for wire in range(3):
        qml.CNOT(wires=[wire, wire+1])

    # Measure
    return [qml.expval(qml.PauliZ(i)) for i in range(4)]

# Use in classical workflow
inputs = np.array([0.1, 0.2, 0.3, 0.4])
weights = np.random.random(4)
output = quantum_layer(inputs, weights)
```

### Quantum-Classical Pipeline

```python
def hybrid_model(x, quantum_weights, classical_weights):
    # Classical preprocessing
    x_preprocessed = np.tanh(classical_weights['pre'] @ x)

    # Quantum layer
    quantum_out = quantum_layer(x_preprocessed, quantum_weights)

    # Classical postprocessing
    output = classical_weights['post'] @ quantum_out

    return output
```

## Framework Integration

### PyTorch Integration

```python
import torch
import pennylane as qml

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

@qml.qnode(dev, interface='torch')
def quantum_circuit(inputs, weights):
    qml.RY(inputs[0], wires=0)
    qml.RY(inputs[1], wires=1)
    qml.RX(weights[0], wires=0)
    qml.RX(weights[1], wires=1)
    qml.CNOT(wires=[0, 1])
    return qml.expval(qml.PauliZ(0))

# Create PyTorch layer
class QuantumLayer(torch.nn.Module):
    def __init__(self, n_qubits):
        super().__init__()
        self.n_qubits = n_qubits
        self.weights = torch.nn.Parameter(torch.randn(n_qubits))

    def forward(self, x):
        return torch.stack([quantum_circuit(xi, self.weights) for xi in x])

# Use in PyTorch model
class HybridModel(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.classical_1 = torch.nn.Linear(10, 2)
        self.quantum = QuantumLayer(2)
        self.classical_2 = torch.nn.Linear(1, 2)

    def forward(self, x):
        x = torch.relu(self.classical_1(x))
        x = self.quantum(x)
        x = self.classical_2(x.unsqueeze(1))
        return x

# Training loop
model = HybridModel()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
criterion = torch.nn.CrossEntropyLoss()

for epoch in range(100):
    optimizer.zero_grad()
    outputs = model(inputs)
    loss = criterion(outputs, labels)
    loss.backward()
    optimizer.step()
```

### PyTorch `TorchLayer` Integration

Use `qml.qnn.TorchLayer` when the quantum circuit should behave like a `torch.nn.Module`. The QNode must have an `inputs` argument for data, and every other argument is treated as a trainable weight with a shape declared in `weight_shapes`.

```python
import torch
import pennylane as qml

n_qubits = 2
dev = qml.device("default.qubit", wires=n_qubits)

@qml.qnode(dev)
def qnode(inputs, weights):
    qml.AngleEmbedding(inputs, wires=range(n_qubits))
    qml.StronglyEntanglingLayers(weights, wires=range(n_qubits))
    return [qml.expval(qml.Z(i)) for i in range(n_qubits)]

weight_shapes = {"weights": (3, n_qubits, 3)}
qlayer = qml.qnn.TorchLayer(qnode, weight_shapes)

model = torch.nn.Sequential(
    torch.nn.Linear(4, n_qubits),
    qlayer,
    torch.nn.Linear(n_qubits, 2),
)
```

### JAX Integration

```python
import jax
import jax.numpy as jnp
import pennylane as qml

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

@qml.qnode(dev, interface='jax')
def quantum_circuit(inputs, weights):
    qml.RY(inputs[0], wires=0)
    qml.RY(inputs[1], wires=1)
    qml.RX(weights[0], wires=0)
    qml.RX(weights[1], wires=1)
    qml.CNOT(wires=[0, 1])
    return qml.expval(qml.PauliZ(0))

# JAX-compatible training
@jax.jit
def loss_fn(weights, x, y):
    predictions = quantum_circuit(x, weights)
    return jnp.mean((predictions - y) ** 2)

# Compute gradients with JAX
grad_fn = jax.grad(loss_fn)

# Training
weights = jnp.array([0.1, 0.2])
for i in range(100):
    grads = grad_fn(weights, x_train, y_train)
    weights = weights - 0.01 * grads
```

### TensorFlow Status

TensorFlow support is no longer maintained as of PennyLane v0.44, and `qml.qnn.keras.KerasLayer` has been removed. Prefer PyTorch (`qml.qnn.TorchLayer`) or JAX/Optax for new quantum machine learning code.

## Quantum Neural Networks

### Variational Quantum Circuit (VQC)

```python
from pennylane import numpy as np

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

def variational_block(weights, wires):
    """Single layer of variational circuit."""
    for i, wire in enumerate(wires):
        qml.RY(weights[i, 0], wires=wire)
        qml.RZ(weights[i, 1], wires=wire)

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

@qml.qnode(dev)
def quantum_neural_network(inputs, weights):
    # Encode inputs
    for i, inp in enumerate(inputs):
        qml.RY(inp, wires=i)

    # Apply variational layers
    n_layers = len(weights)
    for layer_weights in weights:
        variational_block(layer_weights, wires=range(4))

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

# Initialize weights
n_layers = 3
n_wires = 4
weights_shape = (n_layers, n_wires, 2)
weights = np.random.random(weights_shape, requires_grad=True)
```

### Quantum Convolutional Neural Network

```python
def conv_layer(weights, wires):
    """Quantum convolutional layer."""
    n_wires = len(wires)

    # Apply local unitaries
    for i in range(n_wires):
        qml.RY(weights[i], wires=wires[i])

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

def pooling_layer(wires):
    """Quantum pooling (measure and discard)."""
    measurements = []
    for i in range(0, len(wires), 2):
        measurements.append(qml.measure(wires[i]))
    return measurements

@qml.qnode(dev)
def qcnn(inputs, weights):
    # Encode image data
    for i, pixel in enumerate(inputs):
        qml.RY(pixel, wires=i)

    # Convolutional layers
    conv_layer(weights[0], wires=range(8))
    pooling_layer(wires=range(0, 8, 2))

    conv_layer(weights[1], wires=range(1, 8, 2))
    pooling_layer(wires=range(1, 8, 4))

    return qml.expval(qml.PauliZ(1))
```

### Quantum Recurrent Neural Network

```python
def qrnn_cell(x, hidden, weights):
    """Single QRNN cell."""
    @qml.qnode(dev)
    def cell(x, h, w):
        # Encode input and hidden state
        qml.RY(x, wires=0)
        qml.RY(h, wires=1)

        # Apply recurrent transformation
        qml.RX(w[0], wires=0)
        qml.RX(w[1], wires=1)
        qml.CNOT(wires=[0, 1])
        qml.RY(w[2], wires=1)

        return qml.expval(qml.PauliZ(1))

    return cell(x, hidden, weights)

def qrnn_sequence(sequence, weights):
    """Process sequence with QRNN."""
    hidden = 0.0
    outputs = []

    for x in sequence:
        hidden = qrnn_cell(x, hidden, weights)
        outputs.append(hidden)

    return outputs
```

## Variational Classifiers

### Binary Classification

```python
dev = qml.device('default.qubit', wires=2)

@qml.qnode(dev)
def variational_classifier(x, weights):
    # Feature map
    qml.RY(x[0], wires=0)
    qml.RY(x[1], wires=1)

    # Variational layers
    for w in weights:
        qml.RX(w[0], wires=0)
        qml.RX(w[1], wires=1)
        qml.CNOT(wires=[0, 1])
        qml.RY(w[2], wires=0)
        qml.RY(w[3], wires=1)

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

def cost_function(weights, X, y):
    """Binary cross-entropy loss."""
    predictions = np.array([variational_classifier(x, weights) for x in X])
    predictions = (predictions + 1) / 2  # Map [-1, 1] to [0, 1]
    return -np.mean(y * np.log(predictions) + (1 - y) * np.log(1 - predictions))

# Training
n_layers = 2
n_params_per_layer = 4
weights = np.random.random((n_layers, n_params_per_layer), requires_grad=True)

opt = qml.GradientDescentOptimizer(stepsize=0.1)
for i in range(100):
    weights = opt.step(lambda w: cost_function(w, X_train, y_train), weights)
```

### Multi-Class Classification

```python
@qml.qnode(dev)
def multiclass_circuit(x, weights):
    # Encode input
    for i, val in enumerate(x):
        qml.RY(val, wires=i)

    # Variational circuit
    for layer_weights in weights:
        for i, w in enumerate(layer_weights):
            qml.RY(w, wires=i)
        for i in range(len(x)-1):
            qml.CNOT(wires=[i, i+1])

    # Multiple outputs for classes
    return [qml.expval(qml.PauliZ(i)) for i in range(3)]

def softmax(x):
    exp_x = np.exp(x - np.max(x))
    return exp_x / exp_x.sum()

def predict_class(x, weights):
    logits = multiclass_circuit(x, weights)
    return softmax(logits)
```

## Training and Optimization

### Gradient-Based Training

```python
# Automatic differentiation
@qml.qnode(dev, diff_method='backprop')
def circuit_backprop(x, weights):
    # ... circuit definition
    return qml.expval(qml.PauliZ(0))

# Parameter shift rule
@qml.qnode(dev, diff_method='parameter-shift')
def circuit_param_shift(x, weights):
    # ... circuit definition
    return qml.expval(qml.PauliZ(0))

# Finite differences
@qml.qnode(dev, diff_method='finite-diff')
def circuit_finite_diff(x, weights):
    # ... circuit definition
    return qml.expval(qml.PauliZ(0))
```

### Mini-Batch Training

```python
def batch_cost(weights, X_batch, y_batch):
    predictions = np.array([variational_classifier(x, weights) for x in X_batch])
    return np.mean((predictions - y_batch) ** 2)

# Mini-batch training
batch_size = 32
n_epochs = 100

for epoch in range(n_epochs):
    for i in range(0, len(X_train), batch_size):
        X_batch = X_train[i:i+batch_size]
        y_batch = y_train[i:i+batch_size]

        weights = opt.step(lambda w: batch_cost(w, X_batch, y_batch), weights)
```

### Learning Rate Scheduling

```python
def train_with_schedule(weights, X, y, n_epochs):
    initial_lr = 0.1
    decay = 0.95

    for epoch in range(n_epochs):
        lr = initial_lr * (decay ** epoch)
        opt = qml.GradientDescentOptimizer(stepsize=lr)

        weights = opt.step(lambda w: cost_function(w, X, y), weights)

        if epoch % 10 == 0:
            print(f"Epoch {epoch}, Loss: {cost_function(weights, X, y)}")

    return weights
```

## Data Encoding Strategies

### Angle Encoding

```python
def angle_encoding(x, wires):
    """Encode features as rotation angles."""
    for i, feature in enumerate(x):
        qml.RY(feature, wires=wires[i])
```

### Amplitude Encoding

```python
def amplitude_encoding(x, wires):
    """Encode features as state amplitudes."""
    # Normalize
    x_norm = x / np.linalg.norm(x)
    qml.MottonenStatePreparation(x_norm, wires=wires)
```

### Basis Encoding

```python
def basis_encoding(x, wires):
    """Encode binary features in computational basis."""
    for i, bit in enumerate(x):
        if bit:
            qml.PauliX(wires=wires[i])
```

### IQP Encoding

```python
def iqp_encoding(x, wires):
    """Instantaneous Quantum Polynomial encoding."""
    # Hadamard layer
    for wire in wires:
        qml.Hadamard(wires=wire)

    # Encode features
    for i, feature in enumerate(x):
        qml.RZ(feature, wires=wires[i])

    # Entanglement
    for i in range(len(wires)-1):
        qml.IsingZZ(x[i] * x[i+1], wires=[wires[i], wires[i+1]])
```

### Hamiltonian Encoding

```python
def hamiltonian_encoding(x, wires, time=1.0):
    """Encode via Hamiltonian evolution."""
    # Build Hamiltonian from features
    coeffs = x
    obs = [qml.PauliZ(i) for i in wires]

    H = qml.Hamiltonian(coeffs, obs)

    # Apply time evolution
    qml.ApproxTimeEvolution(H, time, n=10)
```

## Transfer Learning

### Pre-trained Quantum Model

```python
# Train on large dataset
pretrained_weights = train_quantum_model(large_dataset)

# Fine-tune on specific task
def fine_tune(pretrained_weights, small_dataset, n_epochs=50):
    # Freeze early layers
    frozen_weights = pretrained_weights[:-1]  # All but last layer
    trainable_weights = pretrained_weights[-1:]  # Only last layer

    @qml.qnode(dev)
    def transfer_circuit(x, trainable):
        # Apply frozen layers
        for layer_w in frozen_weights:
            variational_block(layer_w, wires=range(4))

        # Apply trainable layer
        variational_block(trainable, wires=range(4))

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

    # Train only last layer
    opt = qml.AdamOptimizer(stepsize=0.01)
    for epoch in range(n_epochs):
        trainable_weights = opt.step(
            lambda w: cost_function(w, small_dataset),
            trainable_weights
        )

    return np.concatenate([frozen_weights, trainable_weights])
```

### Classical-to-Quantum Transfer

```python
# Use classical network for feature extraction
import torch.nn as nn

classical_extractor = nn.Sequential(
    nn.Conv2d(3, 16, 3),
    nn.ReLU(),
    nn.MaxPool2d(2),
    nn.Flatten(),
    nn.Linear(16*13*13, 4)  # Output 4 features for quantum circuit
)

# Quantum classifier
@qml.qnode(dev)
def quantum_classifier(features, weights):
    angle_encoding(features, wires=range(4))
    variational_block(weights, wires=range(4))
    return qml.expval(qml.PauliZ(0))

# Combined model
def hybrid_transfer_model(image, classical_weights, quantum_weights):
    features = classical_extractor(image)
    return quantum_classifier(features, quantum_weights)
```

## Best Practices

1. **Start simple** - Begin with small circuits and scale up
2. **Choose encoding wisely** - Match encoding to data structure
3. **Use maintained interfaces** - Prefer PyTorch or JAX for new quantum ML projects
4. **Monitor gradients** - Check for vanishing/exploding gradients (barren plateaus)
5. **Regularize** - Add L2 regularization to prevent overfitting
6. **Validate hardware compatibility** - Test on simulators before hardware
7. **Batch efficiently** - Use vectorization when possible
8. **Cache compilations** - Reuse compiled circuits for inference