# 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])
```
