qconduit 0.0.6
A PyTorch-native quantum statevector plumbing library for quantum machine learning
Requires Python: >=3.10
Keywords: quantum computing, quantum machine learning, pytorch, variational quantum algorithms, VQE, QAOA, quantum statevector, quantum simulation, quantum chemistry, quantum optimization

# Quantum Conduit

<div align="center">

**The world's first PyTorch-native quantum statevector plumbing library for quantum machine learning**

[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
[![PyTorch 2.1+](https://img.shields.io/badge/PyTorch-2.1+-orange.svg)](https://pytorch.org/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![PyPI version](https://badge.fury.io/py/qconduit.svg)](https://badge.fury.io/py/qconduit)
[![Version](https://img.shields.io/badge/version-0.0.4-blue.svg)](https://github.com/seansimms/Quantum_Conduit)
[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.17599984.svg)](https://doi.org/10.5281/zenodo.17599984)

[Features](#features) • [Installation](#installation) • [Quick Start](#quick-start) • [Documentation](#api-reference) • [Examples](#examples)

</div>

---

Quantum Conduit is a minimal, PyTorch-native quantum statevector library designed specifically for quantum machine learning applications. Unlike high-level quantum frameworks, Quantum Conduit provides clean, low-level abstractions that integrate seamlessly with PyTorch's computational graph, enabling native autograd support and batch processing for quantum operations.

## Table of Contents

- [Features](#features)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Architecture](#architecture)
- [Examples](#examples)
- [API Reference](#api-reference)
- [Use Cases](#use-cases)
- [Performance Considerations](#performance-considerations)
- [Comparison with Alternatives](#comparison-with-alternatives)
- [Contributing](#contributing)
- [License](#license)

## Features

Quantum Conduit provides a comprehensive set of quantum computing primitives optimized for machine learning:

### Core Capabilities

- **🔬 Statevector Backend**: Pure quantum state operations with full batch support
- **🌊 Density Matrix Backend**: Mixed quantum states for noise modeling (optimized for small systems)
- **⚛️ Standard Gate Library**: Complete set of single- and two-qubit gates (I, X, Y, Z, H, S, T, CNOT, RX, RY, RZ)
- **🖥️ Device Abstraction**: Seamless CPU and CUDA support with automatic device management
- **🧩 QuantumModule**: PyTorch-native module system compatible with `torch.nn.Module`
- **🔌 Circuit IR**: Structured circuit representation with simulation and visualization
- **🔍 Diagnostics**: State validation, fidelity computation, and debugging tools

### Quantum Machine Learning

- **📊 Parametric Ansätze**: Hardware-efficient and custom ansätze for variational algorithms
- **🔍 VQE Algorithm**: Built-in Variational Quantum Eigensolver for ground-state energy estimation
- **🎯 QAOA Algorithm**: Quantum Approximate Optimization Algorithm for MaxCut/Ising problems
- **🌡️ Adiabatic Evolution**: Adiabatic quantum computing with configurable schedules and circuit building
- **🎛️ Variational Scaffolding**: High-level APIs for running VQE and QAOA with result objects
- **🤖 Hybrid Quantum-Classical**: Seamless integration with PyTorch neural networks
- **📈 Parameter-Shift Gradients**: Quantum-aware gradient computation via parameter-shift rule
- **🔄 Full Autograd Support**: Native PyTorch differentiation throughout the stack
- **🏋️ Training Infrastructure**: Complete VQE training loop with callbacks and history tracking

### Advanced Features

- **🎯 Pauli Operators**: Complete support for Pauli-term and Pauli-sum Hamiltonians
- **🌪️ Noise Models**: Standard quantum channels, enhanced Kraus channels, and circuit-level noise simulation
- **📦 Batch Processing**: Efficient batch operations for training quantum models
- **🎨 Extensible Design**: Clean abstractions for custom gates, ansätze, and algorithms
- **🐛 Debug Mode**: Built-in debugging with normalization checks and validation
- **🎲 Sampling Utilities**: Bitstring sampling and probability distribution analysis
- **⏱️ Time Evolution**: Trotterization and exact Hamiltonian time evolution (dual APIs)
- **⚙️ Optimizer Factory**: Convenient optimizer creation utilities
- **🔬 Experimental Tools**: Parameter sweep utilities for algorithm exploration
- **🔬 Exact Solvers**: Exact diagonalization for benchmarking and validation (small systems)
- **🏗️ Pre-built Models**: Standard quantum many-body models (spin chains, chemistry models)
- **🧬 Fermion-to-Qubit Mappings**: Jordan-Wigner and Bravyi-Kitaev transforms for quantum chemistry
- **🔬 Quantum State Tomography**: Density matrix reconstruction from Pauli measurements
- **⚙️ Circuit Transpilation**: Gate decomposition and basis set conversion for hardware compatibility
- **🔌 Quantum Channels API**: Textbook quantum noise channels with Kraus operators
- **📥 I/O Support**: OpenQASM 2.0 and JSON IR import/export for circuit interchange
- **🔗 PyTorch Integration**: Native `nn.Module` integration with parameter-shift gradients
- **📊 Visualization**: Circuit drawing, Bloch sphere visualization, and circuit analysis

## Installation

### Requirements

- Python 3.10 or higher
- PyTorch 2.1 or higher

### Install from PyPI (Recommended)

```bash
pip install qconduit
```

### Install from Source

For the latest development version:

```bash
git clone https://github.com/seansimms/Quantum_Conduit.git
cd Quantum_Conduit
pip install -e .
```

### Development Installation

For development with testing and linting tools:

```bash
pip install -e ".[dev]"
```

### CUDA Support

CUDA support is automatically available if PyTorch was installed with CUDA support. No additional configuration is required.

## Quick Start

### Example 1: Basic Quantum Operations

```python
import torch
import qconduit as qc

# Create a 1-qubit zero state
state = qc.zero_state(n_qubits=1)

# Apply Hadamard gate
h_gate = qc.H()
state = qc.apply_gate(state, h_gate, qubit=0, n_qubits=1)

# Compute probabilities
probs = qc.measure_probs(state, n_qubits=1)
print(f"Probabilities: {probs}")  # [0.5, 0.5]

# Measure Z expectation
z_exp = qc.measure_expectation_z(state, qubit=0, n_qubits=1)
print(f"<Z>: {z_exp}")  # ~0.0
```

### Example 2: Variational Quantum Eigensolver (VQE)

```python
import torch
import qconduit as qc
from qconduit.algorithms import VQE
from qconduit.layers import HardwareEfficientAnsatz

# Define a 2-qubit Hamiltonian (diagonal)
hamiltonian = torch.tensor([0.0, 0.5, 0.5, 1.0], dtype=torch.float32)

# Create ansatz
ansatz = HardwareEfficientAnsatz(n_qubits=2, depth=2)

# Create VQE instance (hamiltonian can be tensor or PauliSum)
vqe = VQE(ansatz=ansatz, hamiltonian=hamiltonian)

# Initialize parameters
params = torch.nn.Parameter(0.1 * torch.randn(ansatz.num_parameters))

# Optimize
optimizer = torch.optim.Adam([params], lr=0.1)
for step in range(50):
    optimizer.zero_grad()
    energy = vqe.energy(params)
    energy.backward()
    optimizer.step()
    if (step + 1) % 10 == 0:
        print(f"Step {step + 1}: energy = {energy.item():.6f}")
```

### Example 3: Hybrid Quantum-Classical Model

```python
import torch
import torch.nn as nn
from qconduit.layers import QuantumBlock

class HybridClassifier(nn.Module):
    def __init__(self):
        super().__init__()
        # Quantum block: 2 qubits, depth 1, 2 input features
        self.quantum = QuantumBlock(n_qubits=2, depth=1, in_features=2)
        # Classical head
        self.head = nn.Linear(2, 2)  # 2 classes
    
    def forward(self, x):
        q_features = self.quantum(x)  # Quantum expectations
        return self.head(q_features)  # Classical classification

# Use like any PyTorch model
model = HybridClassifier()
x = torch.randn(32, 2)  # Batch of 32 samples
logits = model(x)
```

### Example 4: Noise Modeling

```python
import torch
import qconduit as qc
from qconduit.noise import DepolarizingChannel
from qconduit.layers import HardwareEfficientAnsatz

# Create a noisy quantum circuit
ansatz = HardwareEfficientAnsatz(n_qubits=2, depth=2)
params = torch.randn(ansatz.num_parameters)

# Build state
state = ansatz(params)

# Apply noise model
noise = DepolarizingChannel(p=0.1)  # 10% depolarizing noise
rho = noise.apply_statevector(state, n_qubits=2)

# Compute noisy expectation values
import qconduit as qc
probs = qc.measure_probs_dm(rho)
print(f"Noisy probabilities: {probs}")
```

### Example 5: Circuit IR

```python
from qconduit.circuit import QuantumCircuit

# Create and simulate a Bell state circuit
circuit = QuantumCircuit(n_qubits=2)
circuit.add_gate("H", [0])
circuit.add_gate("CNOT", [1, 0])

# Simulate the circuit
state = circuit.simulate_state()

# Visualize the circuit
print(circuit.to_text_diagram())
# Output:
# q0: ─H──⊕─
# q1: ────●─

# Analyze circuit properties
print(f"Depth: {circuit.depth()}")  # 2
print(f"Gate counts: {circuit.gate_counts()}")  # {'H': 1, 'CNOT': 1}
print(f"Number of gates: {circuit.num_gates()}")  # 2

# Parametric gates
circuit2 = QuantumCircuit(n_qubits=1)
circuit2.add_gate("RX", [0], params=[0.5])  # Rotation gate with angle
state2 = circuit2.simulate_state()
```

### Example 6: Diagnostics and Debug Mode

```python
import qconduit as qc
from qconduit.diagnostics import state_norm, fidelity, bloch_vector, assert_normalized

# Check state normalization
state = qc.zero_state(n_qubits=1)
norm = state_norm(state)
print(f"State norm: {norm}")  # 1.0
assert_normalized(state)  # Validates norm ≈ 1

# Compute fidelity between states
state1 = qc.zero_state(n_qubits=1)
h_gate = qc.H()
state2 = qc.apply_gate(state1, h_gate, qubit=0, n_qubits=1)
f = fidelity(state1, state2)
print(f"Fidelity: {f}")  # 0.5 (states are orthogonal)

# Compute Bloch vector for single-qubit state
bloch = bloch_vector(state2)
print(f"Bloch vector (x, y, z): {bloch}")  # [1.0, 0.0, 0.0] for |+⟩

# Enable debug mode for validation
qc.set_debug_enabled(True)
# Operations now include automatic normalization checks
# This helps catch bugs during development

# Use context manager for temporary debug mode
with qc.debug_context(True):
    # Debug checks enabled here
    state = qc.apply_gate(state, h_gate, qubit=0, n_qubits=1)
# Debug mode restored to previous state
```

### Example 7: QAOA for MaxCut

```python
from qconduit.algorithms import QAOAAnsatz, ising_maxcut_hamiltonian, Edge, VQE
import torch

# Define a graph (triangle: 3 nodes, 3 edges)
edges = [Edge(0, 1), Edge(1, 2), Edge(2, 0)]
hamiltonian = ising_maxcut_hamiltonian(num_nodes=3, edges=edges)

# Create QAOA ansatz (p is the number of QAOA layers)
qaoa = QAOAAnsatz(n_qubits=3, problem_hamiltonian=hamiltonian, p=2)

# Optimize with VQE
vqe = VQE(ansatz=qaoa, hamiltonian=hamiltonian)
params = torch.nn.Parameter(0.1 * torch.randn(qaoa.num_parameters))

optimizer = torch.optim.Adam([params], lr=0.1)
for step in range(50):
    optimizer.zero_grad()
    energy = vqe.energy(params)
    energy.backward()
    optimizer.step()
    if (step + 1) % 10 == 0:
        print(f"Step {step + 1}: energy = {energy.item():.6f}")
```

### Example 8: VQE Training with Callbacks

```python
from qconduit.training import VQETrainer, TrainingCallback, EarlyStoppingConfig
from qconduit.algorithms import VQE
import torch

# Define callback for logging
class LoggingCallback(TrainingCallback):
    def __call__(self, info):
        if info.step % 10 == 0:
            print(f"Step {info.step}: energy = {info.energy:.6f}")

# Configure early stopping
early_stop = EarlyStoppingConfig(patience=10, min_delta=1e-6)

# Create VQE and optimizer
vqe = VQE(ansatz=ansatz, hamiltonian=hamiltonian)
params = torch.nn.Parameter(0.1 * torch.randn(ansatz.num_parameters))
optimizer = torch.optim.Adam([params], lr=0.1)

# Train VQE with callbacks and early stopping
trainer = VQETrainer(vqe, optimizer=optimizer)
history = trainer.train(
    params,
    max_steps=100,
    callbacks=[LoggingCallback()],
    early_stopping=early_stop,
)

print(f"Best energy: {history.best_energy():.6f}")
print(f"Final energy: {history.final_energy():.6f}")
```

### Example 9: Sampling and Analysis

```python
from qconduit.sampling import sample_bitstrings_state, bitstring_counts, kl_divergence
import qconduit as qc

# Create a quantum state
state = qc.zero_state(n_qubits=3)
state = qc.apply_gate(state, qc.H(), qubit=0, n_qubits=3)
state = qc.apply_gate(state, qc.H(), qubit=1, n_qubits=3)

# Sample bitstrings from the state
samples = sample_bitstrings_state(state, n_qubits=3, n_shots=1000)

# Count occurrences
counts = bitstring_counts(samples)
print(f"Sample counts: {counts}")

# Compare probability distributions using KL divergence
probs1 = qc.measure_probs(state, n_qubits=3)
probs2 = qc.measure_probs(qc.zero_state(n_qubits=3), n_qubits=3)
# Convert probability tensors to dictionaries for kl_divergence
probs1_dict = {format(i, f'0{3}b'): float(probs1[i].item()) for i in range(len(probs1))}
probs2_dict = {format(i, f'0{3}b'): float(probs2[i].item()) for i in range(len(probs2))}
kl = kl_divergence(probs1_dict, probs2_dict)
print(f"KL divergence: {kl:.6f}")
```

### Example 10: Time Evolution

```python
from qconduit.time_evolution import time_evolve_state, build_trotter_circuit
from qconduit.operators import PauliTerm, PauliSum
import qconduit as qc

# Create a simple Hamiltonian (transverse field Ising model)
hamiltonian = PauliSum.from_terms([
    PauliTerm(1.0, ("Z", "Z")),  # Interaction
    PauliTerm(0.5, ("X", "I")),  # Transverse field
    PauliTerm(0.5, ("I", "X")),
])

# Evolve state under the Hamiltonian
state = qc.zero_state(n_qubits=2)
evolved_state = time_evolve_state(
    state, hamiltonian, t=0.5, n_steps=10, n_qubits=2
)

# Build Trotter circuit for the same evolution
circuit = build_trotter_circuit(
    hamiltonian, t=0.5, n_steps=10, n_qubits=2, order=1  # First-order Trotter
)
state_from_circuit = circuit.simulate_state()

print("Time evolution complete")
```

### Example 11: Exact Diagonalization

```python
from qconduit.exact import exact_eigensystem, exact_ground_state, paulisum_to_dense
from qconduit.operators import PauliTerm, PauliSum
import torch

# Create a simple Hamiltonian
hamiltonian = PauliSum.from_terms([
    PauliTerm(1.0, ("Z", "Z")),
    PauliTerm(0.5, ("X", "I")),
    PauliTerm(0.5, ("I", "X")),
])

# Convert to dense matrix
dense_matrix = paulisum_to_dense(hamiltonian, num_qubits=2)
print(f"Dense matrix shape: {dense_matrix.shape}")  # (4, 4)

# Compute full eigensystem
eigenvalues, eigenvectors = exact_eigensystem(hamiltonian, num_qubits=2)
print(f"Eigenvalues: {eigenvalues}")

# Get just the ground state
ground_energy, ground_state = exact_ground_state(hamiltonian, num_qubits=2)
print(f"Ground state energy: {ground_energy.item():.6f}")
```

### Example 12: Pre-built Models

```python
from qconduit.models import (
    transverse_field_ising_chain,
    heisenberg_xxz_chain,
    ising_zz_chain,
    two_qubit_generic_chemistry_like,
    diagonal_z_field,
)

# Transverse field Ising model (TFIM)
tfim = transverse_field_ising_chain(
    num_sites=4,
    j_coupling=1.0,
    h_field=0.5,
    periodic=True  # Periodic boundary conditions
)

# Heisenberg XXZ chain
heisenberg = heisenberg_xxz_chain(
    num_sites=3,
    j_coupling=1.0,
    delta=0.5,  # Anisotropy parameter
    periodic=False
)

# Ising ZZ chain (no transverse field)
ising = ising_zz_chain(
    num_sites=4,
    j_coupling=1.0,
    periodic=True
)

# Two-qubit chemistry-like model
chemistry_ham = two_qubit_generic_chemistry_like(
    c_i=0.0,      # Identity coefficient
    c_z0=0.5,     # Z⊗I coefficient
    c_z1=0.3,     # I⊗Z coefficient
    c_z0z1=0.1,   # Z⊗Z coefficient
    c_xx=0.0,     # X⊗X coefficient
    c_yy=0.0      # Y⊗Y coefficient
)

# Diagonal Z field
z_field = diagonal_z_field(num_qubits=3, local_fields=[0.5, 0.5, 0.5])

# Use with VQE or exact diagonalization
from qconduit.exact import exact_ground_state
energy, state = exact_ground_state(tfim, num_qubits=4)
print(f"TFIM ground energy: {energy.item():.6f}")
```

### Example 13: Adiabatic Evolution

```python
from qconduit.adiabatic import (
    AdiabaticConfig,
    linear_schedule,
    polynomial_schedule,
    adiabatic_evolve_state,
    build_adiabatic_circuit,
    build_x_mixer_hamiltonian,
    interpolate_paulisum,
)
from qconduit.operators import PauliSum, PauliTerm
import qconduit as qc
import torch

# Define initial (mixer) and final (problem) Hamiltonians
h_mixer = build_x_mixer_hamiltonian(num_qubits=3)  # -sum_i X_i
h_problem = PauliSum.from_terms([
    PauliTerm(1.0, ("Z", "Z", "I")),
    PauliTerm(1.0, ("I", "Z", "Z")),
])

# Create schedule (linear interpolation)
num_steps = 20
schedule = linear_schedule(num_steps)

# Configure adiabatic evolution
config = AdiabaticConfig(
    total_time=1.0,
    num_steps=num_steps,
    schedule=schedule,
    trotter_steps_per_interval=5
)

# Evolve state adiabatically
initial_state = qc.zero_state(n_qubits=3)
# Prepare |+⟩^⊗n state
for i in range(3):
    initial_state = qc.apply_gate(initial_state, qc.H(), qubit=i, n_qubits=3)

final_state = adiabatic_evolve_state(
    initial_state,
    h_mixer,
    h_problem,
    config
)

# Build adiabatic circuit for visualization
circuit = build_adiabatic_circuit(
    n_qubits=3, h_mixer=h_mixer, h_problem=h_problem, config=config
)
print(circuit.to_text_diagram())
```

### Example 14: Fermion-to-Qubit Mappings

```python
from qconduit.fermion import (
    FermionOperator,
    FermionTerm,
    FermionOpSymbol,
    jordan_wigner,
    bravyi_kitaev,
)

# Create a fermionic operator (e.g., a^†_0 a_1 + a^†_1 a_0)
term1 = FermionTerm(
    coeff=1.0,
    operators=((0, "+"), (1, "-"))  # a^†_0 a_1
)
term2 = FermionTerm(
    coeff=1.0,
    operators=((1, "+"), (0, "-"))  # a^†_1 a_0
)

fermion_op = FermionOperator([term1, term2])

# Map to qubits using Jordan-Wigner transform
jw_hamiltonian = jordan_wigner(fermion_op, n_spin_orbitals=2)
print(f"Jordan-Wigner: {len(jw_hamiltonian.terms)} Pauli terms")

# Map to qubits using Bravyi-Kitaev transform
bk_hamiltonian = bravyi_kitaev(fermion_op, n_spin_orbitals=2)
print(f"Bravyi-Kitaev: {len(bk_hamiltonian.terms)} Pauli terms")

# Use the mapped Hamiltonian with VQE or exact diagonalization
from qconduit.exact import exact_ground_state
energy, state = exact_ground_state(jw_hamiltonian, num_qubits=2)
print(f"Ground energy: {energy.item():.6f}")
```

### Example 15: Noisy Circuit Simulation

```python
from qconduit.noise import NoiseConfig, simulate_noisy_circuit_dm, sample_noisy_circuit_dm
from qconduit.circuit import QuantumCircuit
from qconduit.noise import DepolarizingChannel
import qconduit as qc

# Create a quantum circuit
circuit = QuantumCircuit(n_qubits=2)
circuit.add_gate("H", [0])
circuit.add_gate("CNOT", [0, 1])
circuit.add_gate("RX", [1], params=[0.5])

# Configure noise: depolarizing noise on qubit 0, amplitude damping on qubit 1
from qconduit.noise import AmplitudeDampingChannel
noise_config = NoiseConfig(
    per_qubit_channels={
        0: DepolarizingChannel(p=0.01),  # 1% depolarizing on qubit 0
        1: AmplitudeDampingChannel(gamma=0.05),  # Amplitude damping on qubit 1
    }
)

# Simulate noisy circuit (returns density matrix)
rho = simulate_noisy_circuit_dm(circuit, noise=noise_config)
print(f"Density matrix shape: {rho.shape}")  # (4, 4)

# Sample bitstrings from noisy circuit
samples = sample_noisy_circuit_dm(
    circuit,
    noise=noise_config,
    n_shots=1000
)

# Analyze results
from qconduit.sampling import bitstring_counts
counts = bitstring_counts(samples)
print(f"Sample distribution: {counts}")
```

### Example 16: Quantum State Tomography

```python
from qconduit.measurement import (
    single_qubit_pauli_expectations_from_statevector,
    reconstruct_single_qubit_density_from_pauli,
    two_qubit_pauli_expectations_from_statevector,
    reconstruct_two_qubit_density_from_pauli,
    pauli_expectation_from_statevector,
)
import qconduit as qc

# Create a quantum state
state = qc.zero_state(n_qubits=1)
state = qc.apply_gate(state, qc.H(), qubit=0, n_qubits=1)
state = qc.apply_gate(state, qc.RY(0.5), qubit=0, n_qubits=1)

# Measure Pauli expectations
ex_x, ex_y, ex_z = single_qubit_pauli_expectations_from_statevector(state)
print(f"Pauli expectations: X={ex_x:.4f}, Y={ex_y:.4f}, Z={ex_z:.4f}")

# Reconstruct density matrix from measurements
rho_reconstructed = reconstruct_single_qubit_density_from_pauli(ex_x, ex_y, ex_z)

# Compare with actual density matrix
rho_actual = qc.dm_from_statevector(state)
fidelity = qc.fidelity(rho_actual, rho_reconstructed)
print(f"Reconstruction fidelity: {fidelity.item():.6f}")

# Two-qubit tomography
state_2q = qc.zero_state(n_qubits=2)
state_2q = qc.apply_gate(state_2q, qc.H(), qubit=0, n_qubits=2)
state_2q = qc.apply_two_qubit_gate(state_2q, qc.CNOT(), qubit1=0, qubit2=1, n_qubits=2)

# Get all two-qubit Pauli expectations
pauli_expectations = two_qubit_pauli_expectations_from_statevector(state_2q)
rho_2q = reconstruct_two_qubit_density_from_pauli(pauli_expectations)
```

### Example 17: Variational Algorithm Scaffolding

```python
from qconduit.variational import (
    VariationalAnsatz,
    HardwareEfficientAnsatz,
    LayeredEntanglerAnsatz,
    run_vqe,
    run_qaoa,
    VQEResult,
    QAOAResult,
)
from qconduit.operators import PauliSum, PauliTerm
import torch

# Create a Hamiltonian
hamiltonian = PauliSum.from_terms([
    PauliTerm(1.0, ("Z", "Z")),
    PauliTerm(0.5, ("X", "I")),
    PauliTerm(0.5, ("I", "X")),
])

# High-level VQE API
ansatz = HardwareEfficientAnsatz(num_qubits=2, num_layers=2)
initial_params = torch.randn(ansatz.num_parameters)

result = run_vqe(
    hamiltonian=hamiltonian,
    ansatz=ansatz,
    initial_params=initial_params,
    max_iterations=100,
    learning_rate=0.1,
)

print(f"Ground state energy: {result.optimal_value:.6f}")
print(f"Optimal parameters: {result.optimal_params}")
print(f"Converged: {result.converged}")

# High-level QAOA API
qaoa_result = run_qaoa(
    cost_hamiltonian=hamiltonian,
    num_qubits=2,
    depth=2,
    max_iterations=100,
    learning_rate=0.05,
)

print(f"QAOA optimal energy: {qaoa_result.optimal_value:.6f}")
```

### Example 18: Circuit Transpilation

```python
from qconduit.transpile import (
    decompose_h_to_rz_rx_rz,
    transpile_to_rx_rz_cx_basis,
    transpile_to_clifford_t,
    summarize_gate_counts,
    estimate_circuit_depth,
    GateCountSummary,
)
from qconduit.circuit import QuantumCircuit
import qconduit as qc

# Create a circuit with various gates
circuit = QuantumCircuit(n_qubits=3)
circuit.add_gate("H", [0])
circuit.add_gate("CNOT", [0, 1])
circuit.add_gate("T", [1])
circuit.add_gate("S", [2])
circuit.add_gate("RX", [0], params=[0.5])

# Transpile to RX, RZ, CNOT basis (common hardware basis)
transpiled = transpile_to_rx_rz_cx_basis(circuit)
print("Transpiled circuit:")
print(transpiled.to_text_diagram())

# Transpile to Clifford+T basis
clifford_t = transpile_to_clifford_t(circuit)
print("\nClifford+T circuit:")
print(clifford_t.to_text_diagram())

# Analyze gate counts
summary = summarize_gate_counts(clifford_t)
print(f"\nGate counts: {summary.counts}")
print(f"T-count: {summary.t_count}")
print(f"Clifford count: {summary.clifford_count}")

# Estimate circuit depth
depth = estimate_circuit_depth(transpiled)
print(f"Circuit depth: {depth}")
```

### Example 19: Enhanced Kraus Channels

```python
from qconduit.noise import (
    KrausChannel,
    bit_flip_channel,
    phase_flip_channel,
    bit_phase_flip_channel,
    generalized_amplitude_damping_channel,
    two_qubit_depolarizing_channel,
    compose_kraus_channels,
    apply_kraus_channel_to_statevector,
    apply_kraus_channel_to_density_matrix,
)
import qconduit as qc
import torch

# Create various noise channels
bit_flip = bit_flip_channel(p=0.01)  # 1% bit flip probability
phase_flip = phase_flip_channel(p=0.02)  # 2% phase flip probability
bit_phase_flip = bit_phase_flip_channel(p=0.005)  # 0.5% bit-phase flip

# Generalized amplitude damping (with temperature)
amp_damp = generalized_amplitude_damping_channel(
    gamma=0.1,  # Damping rate
    n_th=0.1,   # Thermal population
)

# Two-qubit depolarizing channel
two_qubit_depol = two_qubit_depolarizing_channel(p=0.01)

# Compose channels (apply sequentially)
combined = compose_kraus_channels(bit_flip, phase_flip)

# Apply to statevector
state = qc.zero_state(n_qubits=1)
state = qc.apply_gate(state, qc.H(), qubit=0, n_qubits=1)
noisy_state = apply_kraus_channel_to_statevector(
    state, bit_flip, qubit=0, n_qubits=1
)

# Apply to density matrix
rho = qc.dm_from_statevector(state)
noisy_rho = apply_kraus_channel_to_density_matrix(
    rho, phase_flip, qubit=0, n_qubits=1
)

# Check channel properties
print(f"Bit flip channel is trace-preserving: {bit_flip.is_trace_preserving()}")
print(f"Kraus operators: {len(bit_flip.kraus_ops)}")
```

### Example 20: Exact Time Evolution

```python
from qconduit.evolution import (
    exact_time_evolution_statevector,
    TrotterOrder,
    TrotterSchedule,
    evolve_state_trotter,
)
from qconduit.operators import PauliSum, PauliTerm
import qconduit as qc

# Create a Hamiltonian
hamiltonian = PauliSum.from_terms([
    PauliTerm(1.0, ("Z", "Z")),
    PauliTerm(0.5, ("X", "I")),
    PauliTerm(0.5, ("I", "X")),
])

# Exact time evolution (for small systems)
state = qc.zero_state(n_qubits=2)
state = qc.apply_gate(state, qc.H(), qubit=0, n_qubits=2)

evolved_exact = exact_time_evolution_statevector(
    state, hamiltonian, time=0.5
)

# Trotterized evolution (for larger systems)
from qconduit.evolution import TrotterOrder, TrotterSchedule

schedule = TrotterSchedule(
    num_steps=10,     # Number of steps
    total_time=0.5,   # Total evolution time
    order=1,          # TrotterOrder.FIRST (1) or TrotterOrder.SECOND (2)
)

evolved_trotter = evolve_state_trotter(
    state,
    hamiltonian,
    schedule,
)

# Compare results
fidelity = qc.fidelity(
    qc.dm_from_statevector(evolved_exact),
    qc.dm_from_statevector(evolved_trotter)
)
print(f"Fidelity between exact and Trotter: {fidelity.item():.6f}")
```

### Example 21: Quantum Channels

```python
import torch
from qconduit.channels import (
    KrausChannel,
    DepolarizingChannel,
    BitFlipChannel,
    AmplitudeDampingChannel,
    apply_circuit_with_noise,
)
from qconduit.circuit import QuantumCircuit

# Create standard noise channels
depol = DepolarizingChannel(p=0.01)  # 1% depolarizing noise
bit_flip = BitFlipChannel(p=0.05)    # 5% bit-flip probability
amp_damp = AmplitudeDampingChannel(gamma=0.1)  # Amplitude damping

# Apply channel to a density matrix
from qconduit.backend import zero_dm_state, dm_from_statevector
from qconduit.backend.statevector import zero_state, apply_gate
import qconduit as qc

rho = zero_dm_state(n_qubits=1)
# Apply depolarizing channel
rho_noisy = depol.apply_to_density(rho)

# Apply channel to a statevector (returns density matrix)
psi = zero_state(n_qubits=1)
psi = apply_gate(psi, qc.H(), qubit=0, n_qubits=1)
rho_mixed = depol.apply_to_statevector(psi)

# Simulate noisy circuit
circuit = QuantumCircuit(n_qubits=2)
circuit.add_gate("H", [0])
circuit.add_gate("CNOT", [0, 1])

# Apply noise after each gate
channel_locations = [
    (0, DepolarizingChannel(p=0.01)),  # Noise after H gate
    (1, DepolarizingChannel(p=0.01)),  # Noise after CNOT gate
]
rho_final = apply_circuit_with_noise(circuit, channel_locations)
print(f"Final density matrix shape: {rho_final.shape}")  # (4, 4)
```

### Example 22: Batched Operations

```python
import torch
from qconduit.batched import (
    BatchedState,
    apply_circuit_to_batched_states,
    evaluate_expectations_for_params_batched,
)
from qconduit.variational import HardwareEfficientAnsatz
from qconduit.operators import PauliSum
from qconduit.circuit import QuantumCircuit

# Create a batch of states
states = torch.randn(10, 4, dtype=torch.complex128)  # 10 states, 2 qubits each
states = states / torch.linalg.norm(states, dim=1, keepdim=True)
batched = BatchedState(states, n_qubits=2)

# Apply circuit to all states at once
circuit = QuantumCircuit(n_qubits=2)
circuit.add_gate("H", [0])
circuit.add_gate("CNOT", [0, 1])
result = apply_circuit_to_batched_states(circuit, batched)
print(f"Result shape: {result.states.shape}")  # (10, 4)

# Batch evaluate many parameter sets
ansatz = HardwareEfficientAnsatz(num_qubits=2, num_layers=2)
params_batch = torch.randn(100, ansatz.num_parameters)  # 100 parameter vectors
H = PauliSum.from_label("ZZ")
energies = evaluate_expectations_for_params_batched(ansatz, params_batch, H)
print(f"Energies shape: {energies.shape}")  # (100,)
```

### Example 23: Circuit I/O

```python
from qconduit.io import (
    parse_qasm_string,
    parse_qasm_file,
    export_circuit_to_qasm,
    circuit_to_json,
    json_to_circuit,
    dump_json_circuit,
    load_json_circuit,
)
from qconduit.circuit import QuantumCircuit

# Parse OpenQASM 2.0
qasm = """OPENQASM 2.0;
qreg q[2];
h q[0];
cx q[0],q[1];
"""
circuit = parse_qasm_string(qasm)

# Export to QASM
qasm_output = export_circuit_to_qasm(circuit)
print(qasm_output)

# JSON IR roundtrip
json_data = circuit_to_json(circuit)
circuit_restored = json_to_circuit(json_data)

# File I/O
dump_json_circuit(circuit, "circuit.json")
circuit_loaded = load_json_circuit("circuit.json")

# Parse from file
circuit_from_file = parse_qasm_file("circuit.qasm")
```

### Example 24: PyTorch nn.Module Integration

```python
import torch
import torch.nn as nn
from qconduit.torch import QuantumModule
from qconduit.variational import HardwareEfficientAnsatz
from qconduit.operators import PauliSum, PauliTerm

# Create a quantum module
ansatz = HardwareEfficientAnsatz(num_qubits=2, num_layers=2)
H = PauliSum.from_terms([PauliTerm(1.0, ("Z", "Z"))])
module = QuantumModule(
    ansatz=ansatz,
    hamiltonian=H,
    gradient_method="parameter_shift"  # or "autograd"
)

# Use in PyTorch training loop
optimizer = torch.optim.Adam(module.parameters(), lr=0.1)
for epoch in range(100):
    optimizer.zero_grad()
    energy = module()  # Forward pass
    energy.backward()  # Backward pass (uses parameter-shift rule)
    optimizer.step()
    print(f"Epoch {epoch}: Energy = {energy.item():.6f}")

# Integrate with classical neural networks
class HybridModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.classical = nn.Linear(10, 2)
        self.quantum = QuantumModule(ansatz, H)
        self.head = nn.Linear(1, 1)
    
    def forward(self, x):
        classical_out = self.classical(x)
        quantum_energy = self.quantum()
        return self.head(torch.cat([classical_out, quantum_energy.unsqueeze(0)]))
```

### Example 25: Visualization

```python
from qconduit.viz import (
    print_circuit,
    to_text,
    bloch_coords_from_statevector,
    plot_bloch_vector,
    circuit_summary,
    print_circuit_summary,
)
from qconduit.circuit import QuantumCircuit
import qconduit as qc

# Text circuit drawing
circuit = QuantumCircuit(n_qubits=3)
circuit.add_gate("H", [0])
circuit.add_gate("CNOT", [0, 1])
circuit.add_gate("RX", [2], [0.5])
print_circuit(circuit)
# Output:
# q0: [H]─●───────
# q1: ────⊕───────
# q2: ───────[RX(0.5)]

# Bloch sphere coordinates
state = qc.zero_state(n_qubits=2)
state = qc.apply_gate(state, qc.H(), qubit=0, n_qubits=2)
coords = bloch_coords_from_statevector(state, qubit_index=0, n_qubits=2)
print(f"Bloch coordinates (x, y, z): {coords}")  # (1.0, 0.0, 0.0) for |+⟩

# Plot Bloch vector (requires matplotlib)
try:
    plot_bloch_vector(coords)
except RuntimeError:
    print("matplotlib not available")

# Circuit summary
summary = circuit_summary(circuit)
print_circuit_summary(circuit)
# Output:
# Circuit Summary
# ==================================================
# Qubits: 3
# Total Gates: 3
# Estimated Depth: 2
# T-count: 0
# Clifford-count: 2
# Parameters: 1
# Uses Parametric Gates: True
```

## Architecture

### Design Principles

Quantum Conduit is built on three core principles:

1. **PyTorch-Native**: All operations integrate seamlessly with PyTorch's autograd system, enabling end-to-end differentiation of quantum-classical hybrid models.

2. **Minimal Abstractions**: The library provides "plumbing" rather than high-level abstractions, giving you direct control over quantum states and operations.

3. **Batch-First**: All operations support batched inputs, enabling efficient training of quantum models on classical data.

### Library Structure

```
qconduit/
├── core/           # Core abstractions (Device, QuantumModule)
├── backend/        # Statevector and density matrix backends
├── gates/          # Standard quantum gates
├── circuit/        # Circuit IR (GateOp, QuantumCircuit)
├── layers/         # Parametric ansätze and hybrid blocks
├── algorithms/     # Quantum algorithms (VQE, QAOA)
├── operators/      # Pauli operators and expectations
├── grad/           # Gradient computation (parameter-shift)
├── noise/          # Noise models and quantum channels
├── channels/       # Quantum channels API with Kraus operators
├── batched/        # Batched operations for efficient training
├── io/             # OpenQASM 2.0 and JSON IR import/export
├── torch/          # PyTorch nn.Module integration
├── viz/            # Visualization and circuit analysis
├── diagnostics/    # State validation and debugging tools
├── training/       # Training loops and utilities
├── sampling/       # Bitstring sampling and analysis
├── time_evolution/ # Trotterization and time evolution
├── evolution/      # Alternative evolution API (exact + enhanced Trotter)
├── optim/          # Optimizer factory utilities
├── experiments/    # Parameter sweep utilities
├── exact/          # Exact diagonalization for small systems
├── models/         # Pre-built quantum many-body models
├── adiabatic/      # Adiabatic quantum computing
├── fermion/        # Fermion-to-qubit mappings
├── measurement/    # Measurement and quantum state tomography
├── variational/    # Variational algorithm scaffolding
└── transpile/      # Gate decomposition and circuit transpilation
```

### Key Components

- **Device Abstraction**: Unified interface for CPU and CUDA operations
- **Statevector Backend**: Efficient pure-state simulation with O(2^n) memory
- **Density Matrix Backend**: Mixed-state simulation for noise (O(4^n) memory, small systems)
- **Gate Library**: Standard gates with gradient-preserving implementations
- **Module System**: `QuantumModule` base class compatible with PyTorch's module system
- **Circuit IR**: Structured circuit representation with simulation and visualization
- **Diagnostics**: State validation, fidelity computation, and debug mode integration
- **Training Infrastructure**: Complete training loops with callbacks and history tracking
- **Sampling**: Bitstring sampling and probability distribution analysis
- **Time Evolution**: Trotterization for Hamiltonian simulation
- **Evolution**: Alternative evolution API with exact and enhanced Trotter methods
- **Optimizers**: Factory utilities for optimizer creation
- **Experiments**: Parameter sweep utilities for algorithm exploration
- **Exact Solvers**: Exact diagonalization for benchmarking and validation
- **Pre-built Models**: Standard quantum many-body models (spin chains, chemistry)
- **Adiabatic Evolution**: Adiabatic quantum computing with configurable schedules
- **Fermion-to-Qubit Mappings**: Jordan-Wigner and Bravyi-Kitaev transforms
- **Measurement/Tomography**: Quantum state tomography and Pauli expectation measurements
- **Variational Scaffolding**: High-level APIs for VQE and QAOA algorithms
- **Transpilation**: Gate decomposition and basis set conversion for hardware
- **Quantum Channels**: Textbook quantum noise channels with Kraus operators, channel composition, and circuit integration
- **Batched Operations**: Efficient batch processing for multiple circuits, parameter sets, and states
- **I/O Support**: OpenQASM 2.0 parser/exporter and JSON IR for circuit interchange
- **PyTorch Integration**: Native `nn.Module` wrapper with parameter-shift gradients
- **Visualization**: Circuit text drawing, Bloch sphere coordinates, and circuit summary utilities

## Examples

The `examples/` directory contains complete, runnable examples:

- **`vqe_h2.py`**: Variational Quantum Eigensolver for finding ground-state energy
- **`hybrid_classifier.py`**: Hybrid quantum-classical neural network for classification

### Quantum Textbook Algorithms (G23)

```python
from qconduit.quantum.algorithms import grover
from qconduit.quantum.simulation import initial_state


def oracle(x: int) -> int:
    return 1 if x == 3 else 0


# Prepare |00> and run Grover's search
state = initial_state(2)
solution = grover(oracle, n_qubits=2)
print("Grover found solution:", solution)
```

This educational toolkit follows the treatment in Nielsen & Chuang,
*Quantum Computation and Quantum Information*.

Run examples directly:

```bash
python examples/vqe_h2.py
python examples/hybrid_classifier.py
```

## API Reference

### Core Abstractions

```python
import qconduit as qc

# Device management
device = qc.device("sv_cpu")  # or "sv_cuda"
default = qc.default_device()

# Quantum module base class
class MyQuantumLayer(qc.QuantumModule):
    def forward(self, x):
        # Your quantum operations
        pass
```

### Backend Operations

```python
# Statevector operations
state = qc.zero_state(n_qubits=2, batch_shape=(10,))  # Batched states

# Gate application
gate = qc.H()
state = qc.apply_gate(state, gate, qubit=0, n_qubits=2)
state = qc.apply_two_qubit_gate(state, qc.CNOT(), qubit1=0, qubit2=1, n_qubits=2)

# Measurements
probs = qc.measure_probs(state, n_qubits=2)
z_exp = qc.measure_expectation_z(state, qubit=0, n_qubits=2)

# Density matrix operations (for noise modeling)
rho = qc.zero_dm_state(n_qubits=2)  # Create |00><00|
rho = qc.dm_from_statevector(state)  # Convert statevector to density matrix

# Apply Kraus operators (for noise channels)
kraus_ops = (E0, E1, E2)  # Tuple of 2x2 matrices
rho = qc.apply_kraus_single_qubit(rho, kraus_ops, qubit=0, n_qubits=2)

# Density matrix measurements
probs_dm = qc.measure_probs_dm(rho)
z_exp_dm = qc.measure_expectation_z_dm(rho, qubit=0, n_qubits=2)
```

### Gates

```python
# Single-qubit gates
I_gate = qc.I()
X_gate = qc.X()
Y_gate = qc.Y()
Z_gate = qc.Z()
H_gate = qc.H()
S_gate = qc.S()
T_gate = qc.T()

# Parametric gates (preserve gradients)
theta = torch.tensor(0.5, requires_grad=True)
RX_gate = qc.RX(theta)
RY_gate = qc.RY(theta)
RZ_gate = qc.RZ(theta)

# Two-qubit gates
cnot = qc.CNOT(control_first=True)

# Utility
is_unitary = qc.is_unitary(gate_matrix)
```

### Layers and Ansätze

```python
from qconduit.layers import HardwareEfficientAnsatz, QuantumBlock, ParametricAnsatz

# Hardware-efficient ansatz
ansatz = HardwareEfficientAnsatz(n_qubits=4, depth=3)
params = torch.randn(ansatz.num_parameters)
state = ansatz(params)  # Forward pass

# Hybrid quantum-classical block
quantum_block = QuantumBlock(n_qubits=2, depth=2, in_features=10)
classical_features = torch.randn(32, 10)  # Batch of 32
quantum_features = quantum_block(classical_features)  # Shape: (32, 2)

# Custom ansatz
class MyAnsatz(ParametricAnsatz):
    def forward(self, params):
        state = qc.zero_state(n_qubits=self.n_qubits)
        # Your custom circuit
        return state
```

### Algorithms

```python
from qconduit.algorithms import VQE
from qconduit.operators import PauliTerm, PauliSum

# VQE with diagonal Hamiltonian
hamiltonian_diag = torch.tensor([0.0, 0.5, 0.5, 1.0])
vqe = VQE(ansatz=ansatz, hamiltonian=hamiltonian_diag)

# VQE with Pauli-sum Hamiltonian
pauli_ham = PauliSum.from_terms([
    PauliTerm(1.0, ("Z", "I")),
    PauliTerm(0.5, ("X", "X")),
])
vqe = VQE(ansatz=ansatz, hamiltonian=pauli_ham)

# Compute