变分与混合算法

变分量子算法(VQE、QAOA)和量子机器学习模型是 NISQ 时代最实用的范式。 本节分别展示纯经典优化器驱动版本和 PyTorch 集成版本,以及量子‑经典混合模型 和量子神经网络 / 卷积分类器。

Variational Quantum Eigensolver (VQE) — complete example.

Source: examples/2_advanced/algorithms/vqe.py
Status: pass

Demonstrates:

  • Building a UCCSD ansatz for molecular Hamiltonians

  • Computing energy expectation values via Pauli decomposition

  • Classical optimisation loop (COBYLA) to find ground-state energy

  • Using uniqc ansatz + measurement modules

Usage: python vqe.py [–molecule NAME] [–maxiter N]

References: Peruzzo et al. (2014). “A variational eigenvalue solver on a photonic quantum processor.” Nature Communications 5, 4213. https://arxiv.org/abs/1304.3061

Source code

#!/usr/bin/env python
"""Variational Quantum Eigensolver (VQE) — complete example.

Demonstrates:
  * Building a UCCSD ansatz for molecular Hamiltonians
  * Computing energy expectation values via Pauli decomposition
  * Classical optimisation loop (COBYLA) to find ground-state energy
  * Using uniqc ansatz + measurement modules

Usage:
    python vqe.py [--molecule NAME] [--maxiter N]

References:
    Peruzzo et al. (2014). "A variational eigenvalue solver on a photonic
    quantum processor." Nature Communications 5, 4213.
    https://arxiv.org/abs/1304.3061

[doc-require: ]
"""

import argparse
import sys
import numpy as np

sys.path.insert(0, str(__file__).rsplit("/", 2)[0])

from uniqc import Circuit
from uniqc.simulator import Simulator
from uniqc import uccsd_ansatz
from uniqc import pauli_expectation


# ── Toy Hamiltonian: H₂ (STO-3G, 2-electron, 4 spin-orbital) ──────────
# Bravyi–Kitaev mapped, simplified for demonstration.
# E_nuclear ≈ 0.72 Ha
H2_HAMILTONIAN = [
    ("I0", -0.8105),
    ("Z0", +0.1720),
    ("Z1", -0.2258),
    ("Z2", +0.1720),
    ("Z3", -0.2258),
    ("Z0Z1", +0.1205),
    ("Z0Z2", +0.0455),
    ("Z0Z3", +0.1660),
    ("Z1Z2", +0.1660),
    ("Z1Z3", +0.1205),
    ("Z2Z3", +0.0455),
    ("X0X1Y2Y3", -0.0455),
    ("Y0Y1X2X3", +0.0455),
]
H2_NUCLEAR = 0.72


def vqe_energy(params, hamiltonian, n_qubits, n_electrons):
    """Evaluate ⟨ψ(θ)|H|ψ(θ)⟩ using the UCCSD ansatz.

    For each Pauli string in the Hamiltonian, builds the measurement
    circuit and computes the expectation value via the statevector simulator.

    Args:
        params: Variational parameters (array).
        hamiltonian: List of (pauli_string, coefficient) tuples.
        n_qubits: Number of qubits (spin-orbitals).
        n_electrons: Number of electrons.

    Returns:
        Total energy (float).
    """
    # Build the ansatz circuit; pad with identities on every qubit so that
    # zero-parameter ansätze still keep the full ``n_qubits`` register
    # (the circuit builder otherwise prunes unused qubits).
    circuit = uccsd_ansatz(n_qubits, n_electrons, params=params)
    for q in range(n_qubits):
        circuit.identity(q)

    # Simulate statevector
    sim = Simulator(backend_type="statevector")
    # For energy evaluation, we use direct statevector overlap
    # (in practice, you'd use pauli_expectation with shot-based simulation)
    sv = np.asarray(sim.simulate_statevector(circuit.originir), dtype=complex)

    energy = 0.0
    for pauli_str, coeff in hamiltonian:
        # For demonstration: compute Pauli expectation via statevector
        # Build observable matrix and compute <ψ|P|ψ>
        n = n_qubits
        obs = _pauli_matrix(pauli_str, n)
        exp_val = np.real(sv.conj() @ obs @ sv)
        energy += coeff * exp_val

    return energy


def _pauli_matrix(pauli_str, n_qubits):
    """Build the full 2^n × 2^n matrix for a Pauli string."""
    I = np.eye(2)
    X = np.array([[0, 1], [1, 0]])
    Y = np.array([[0, -1j], [1j, 0]])
    Z = np.array([[1, 0], [0, -1]])
    pauli_map = {"I": I, "X": X, "Y": Y, "Z": Z}

    # Parse pauli_str like "Z0Z1" or "X0X1Y2Y3"
    ops = {}
    for ch in pauli_str:
        if ch in "IXYZ":
            current_op = ch
        elif ch.isdigit():
            ops[int(ch)] = current_op

    matrices = []
    for i in range(n_qubits):
        matrices.append(pauli_map.get(ops.get(i, "I"), I))

    result = matrices[0]
    for m in matrices[1:]:
        result = np.kron(result, m)
    return result


def run_vqe(molecule="H2", maxiter=100):
    """Run the VQE optimisation loop.

    Args:
        molecule: Molecule name (currently only "H2").
        maxiter: Maximum optimiser iterations.

    Returns:
        Optimised energy (float).
    """
    if molecule != "H2":
        raise ValueError(f"Unsupported molecule: {molecule}")

    n_qubits = 4
    n_electrons = 2

    # UCCSD: 2 singles + 1 double = 3 parameters for H₂
    from itertools import combinations
    occupied = list(range(n_electrons))
    virtual = list(range(n_electrons, n_qubits))
    n_singles = len(occupied) * len(virtual)
    n_doubles = len(list(combinations(occupied, 2))) * len(list(combinations(virtual, 2)))
    n_params = n_singles + n_doubles

    print(f"VQE for {molecule}")
    print(f"  Qubits: {n_qubits}, Electrons: {n_electrons}")
    print(f"  UCCSD parameters: {n_params} ({n_singles} singles + {n_doubles} doubles)")
    print(f"  Hamiltonian terms: {len(H2_HAMILTONIAN)}")
    print(f"  Nuclear repulsion: {H2_NUCLEAR:.4f} Ha")
    print()

    # Simple gradient-free optimisation (coordinate descent)
    params = np.zeros(n_params)
    best_energy = float("inf")
    step = 0.1

    for iteration in range(maxiter):
        improved = False
        for i in range(n_params):
            # Try positive step
            params[i] += step
            e_plus = vqe_energy(params, H2_HAMILTONIAN, n_qubits, n_electrons)

            # Try negative step
            params[i] -= 2 * step
            e_minus = vqe_energy(params, H2_HAMILTONIAN, n_qubits, n_electrons)

            # Reset
            params[i] += step
            e_curr = vqe_energy(params, H2_HAMILTONIAN, n_qubits, n_electrons)

            if e_plus < e_curr and e_plus < e_minus:
                params[i] += step
                improved = True
            elif e_minus < e_curr:
                params[i] -= step
                improved = True

        energy = vqe_energy(params, H2_HAMILTONIAN, n_qubits, n_electrons)
        total = energy + H2_NUCLEAR

        if total < best_energy:
            best_energy = total

        if iteration % 10 == 0:
            print(f"  Iter {iteration:3d}: E = {total:.6f} Ha")

        if not improved:
            step *= 0.5
            if step < 1e-6:
                break

    total = vqe_energy(params, H2_HAMILTONIAN, n_qubits, n_electrons) + H2_NUCLEAR
    print(f"\n  Final energy: {total:.6f} Ha")
    print(f"  Parameters: {params}")
    print(f"  Exact FCI:   -1.137274 Ha")

    return total


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="VQE example")
    parser.add_argument("--molecule", default="H2", help="Molecule (default: H2)")
    parser.add_argument("--maxiter", type=int, default=100, help="Max iterations")
    args = parser.parse_args()

    run_vqe(molecule=args.molecule, maxiter=args.maxiter)

Stdout

VQE for H2
  Qubits: 4, Electrons: 2
  UCCSD parameters: 5 (4 singles + 1 doubles)
  Hamiltonian terms: 13
  Nuclear repulsion: 0.7200 Ha

  Iter   0: E = -0.584500 Ha
  Iter  10: E = -0.584500 Ha

  Final energy: -0.584500 Ha
  Parameters: [ 0.   0.   0.   0.  -0.1]
  Exact FCI:   -1.137274 Ha

VQE for H2 molecule using TorchQuantum backend with native PyTorch autograd.

Source: examples/2_advanced/algorithms/vqe_pytorch.py
Status: skip — missing requirements: torchquantum (torch + torchquantum installed)

Demonstrates variational quantum eigensolver with Adam optimizer, using TorchQuantum’s differentiable simulation (no parameter-shift rule).

Source code

"""VQE for H2 molecule using TorchQuantum backend with native PyTorch autograd.

Demonstrates variational quantum eigensolver with Adam optimizer,
using TorchQuantum's differentiable simulation (no parameter-shift rule).

[doc-require: torchquantum]
"""

try:
    import torch
    from uniqc import VQESolver, build_h2_hamiltonian
except ImportError as e:
    print(f"Required dependencies not available: {e}")
    print("Install with: pip install unified-quantum[pytorch]")
    print('Then install TorchQuantum manually: pip install "torchquantum @ git+https://github.com/Agony5757/torchquantum.git@fix/optional-qiskit-deps"')
    raise SystemExit(1)


def main():
    print("=" * 60)
    print("VQE for H2 Molecule — TorchQuantum Backend")
    print("=" * 60)

    # Build H2 Hamiltonian
    pauli_terms, nuclear_repulsion = build_h2_hamiltonian()
    n_qubits = 4
    depth = 2
    n_params = 2 * n_qubits * depth  # 16 params

    print(f"\nMolecule: H2 (STO-3G, 4 qubits)")
    print(f"Nuclear repulsion: {nuclear_repulsion:.4f}")
    print(f"Pauli terms: {len(pauli_terms)}")
    print(f"Ansatz: HEA depth={depth}, params={n_params}")
    print(f"Exact FCI energy: -1.137274 Ha\n")

    # Create solver
    solver = VQESolver(
        hamiltonian=pauli_terms,
        nuclear_repulsion=nuclear_repulsion,
        n_qubits=n_qubits,
        n_params=n_params,
        lr=0.05,
    )

    # Optimize
    final_energy, optimal_params = solver.solve(n_iters=100, verbose=True)

    print(f"\nFinal energy: {final_energy:.6f} Ha")
    print(f"Optimal params: {optimal_params[:4].tolist()} ...")
    print(f"\nExpected: ~-1.10 Ha (simplified Hamiltonian)")


if __name__ == "__main__":
    main()

Note

Example skipped during pre-doc-execution: missing requirements: torchquantum (torch + torchquantum installed)

Quantum Approximate Optimization Algorithm (QAOA) — complete example.

Source: examples/2_advanced/algorithms/qaoa.py
Status: pass

Demonstrates:

  • Building a QAOA ansatz for MaxCut

  • Evaluating the cost function via Pauli measurements

  • Classical optimisation to find approximate solutions

  • Using uniqc ansatz + measurement modules

Usage: python qaoa.py [–p LAYERS] [–maxiter N] [–graph FILE]

References: Farhi, E. et al. (2014). “A Quantum Approximate Optimization Algorithm.” arXiv:1411.4028.

Source code

#!/usr/bin/env python
"""Quantum Approximate Optimization Algorithm (QAOA) — complete example.

Demonstrates:
  * Building a QAOA ansatz for MaxCut
  * Evaluating the cost function via Pauli measurements
  * Classical optimisation to find approximate solutions
  * Using uniqc ansatz + measurement modules

Usage:
    python qaoa.py [--p LAYERS] [--maxiter N] [--graph FILE]

References:
    Farhi, E. et al. (2014). "A Quantum Approximate Optimization Algorithm."
    arXiv:1411.4028.

[doc-require: ]
"""

import argparse
import sys
import numpy as np

sys.path.insert(0, str(__file__).rsplit("/", 2)[0])

from uniqc import Circuit
from uniqc.simulator import Simulator
from uniqc import qaoa_ansatz


# ── Example graph: triangle (3-node ring) ──────────────────────────────
TRIANGLE_EDGES = [(0, 1), (1, 2), (0, 2)]


def maxcut_hamiltonian(edges, n_nodes):
    """Build the MaxCut cost Hamiltonian.

    H_C = -½ Σ_{(i,j)∈E} (1 - Z_i Z_j)

    Maximising -H_C ⟺ maximising the number of cut edges.

    Args:
        edges: List of (i, j) tuples.
        n_nodes: Number of graph nodes.

    Returns:
        List of (pauli_string, coefficient) tuples.
    """
    hamiltonian = [("I" + str(0), -len(edges) / 2.0)]  # constant term (dummy)
    # We'll handle the constant separately
    terms = []
    for i, j in edges:
        terms.append((f"Z{i}Z{j}", 0.5))  # -½ × (-Z_i Z_j) = +½ Z_i Z_j
    return terms, -len(edges) / 2.0  # (pauli_terms, constant_offset)


def qaoa_energy(betas_gammas, cost_terms, p, n_qubits):
    """Evaluate ⟨ψ(β,γ)|H_C|ψ(β,γ)⟩.

    Args:
        betas_gammas: Concatenated [β₁...βₚ, γ₁...γₚ].
        cost_terms: Pauli terms from maxcut_hamiltonian.
        p: Number of QAOA layers.
        n_qubits: Number of qubits.

    Returns:
        Expected cut value (float).
    """
    betas = betas_gammas[:p]
    gammas = betas_gammas[p:]

    circuit = qaoa_ansatz(cost_terms, p=p, betas=betas, gammas=gammas)

    sim = Simulator(backend_type="statevector")
    sv = np.asarray(sim.simulate_statevector(circuit.originir), dtype=complex)

    # Compute expectation value of each Pauli term
    energy = 0.0
    for pauli_str, coeff in cost_terms:
        obs = _pauli_matrix(pauli_str, n_qubits)
        exp_val = np.real(sv.conj() @ obs @ sv)
        energy += coeff * exp_val

    return energy


def _pauli_matrix(pauli_str, n_qubits):
    """Build matrix for a Pauli string."""
    I = np.eye(2)
    Z = np.array([[1, 0], [0, -1]])
    pauli_map = {"I": I, "Z": Z, "X": np.array([[0, 1], [1, 0]]),
                 "Y": np.array([[0, -1j], [1j, 0]])}

    ops = {}
    current_op = None
    for ch in pauli_str:
        if ch in "IXYZ":
            current_op = ch
        elif ch.isdigit():
            ops[int(ch)] = current_op

    matrices = [pauli_map.get(ops.get(i, "I"), I) for i in range(n_qubits)]
    result = matrices[0]
    for m in matrices[1:]:
        result = np.kron(result, m)
    return result


def run_qaoa(p=2, maxiter=80, edges=None):
    """Run QAOA for MaxCut.

    Args:
        p: Number of QAOA layers.
        maxiter: Maximum optimiser iterations.
        edges: Graph edges. Defaults to triangle.

    Returns:
        Best energy and parameters.
    """
    if edges is None:
        edges = TRIANGLE_EDGES

    n_nodes = max(max(i, j) for i, j in edges) + 1
    cost_terms, const_offset = maxcut_hamiltonian(edges, n_nodes)

    print(f"QAOA for MaxCut")
    print(f"  Graph: {len(edges)} edges, {n_nodes} nodes")
    print(f"  Layers (p): {p}")
    print(f"  Max possible cut: {len(edges)}")
    print()

    # Coordinate-descent optimisation
    rng = np.random.default_rng(42)
    params = rng.uniform(0, np.pi, size=2 * p)
    best_energy = float("inf")
    best_params = params.copy()
    step = 0.2

    for iteration in range(maxiter):
        improved = False
        for i in range(2 * p):
            original = params[i]

            params[i] = original + step
            e_plus = qaoa_energy(params, cost_terms, p, n_nodes)

            params[i] = original - step
            e_minus = qaoa_energy(params, cost_terms, p, n_nodes)

            params[i] = original
            e_curr = qaoa_energy(params, cost_terms, p, n_nodes)

            if e_plus < e_curr and e_plus <= e_minus:
                params[i] = original + step
                improved = True
            elif e_minus < e_curr:
                params[i] = original - step
                improved = True

        energy = qaoa_energy(params, cost_terms, p, n_nodes)
        cut_value = -(energy + const_offset)

        if energy < best_energy:
            best_energy = energy
            best_params = params.copy()

        if iteration % 20 == 0:
            print(f"  Iter {iteration:3d}: cut ≈ {cut_value:.3f}")

        if not improved:
            step *= 0.5
            if step < 1e-6:
                break

    energy = qaoa_energy(best_params, cost_terms, p, n_nodes)
    cut_value = -(energy + const_offset)
    print(f"\n  Best cut value: {cut_value:.3f} / {len(edges)}")
    print(f"  Approximation ratio: {cut_value / len(edges):.3f}")

    return cut_value, best_params


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="QAOA MaxCut example")
    parser.add_argument("-p", type=int, default=2, help="QAOA layers")
    parser.add_argument("--maxiter", type=int, default=80, help="Max iterations")
    args = parser.parse_args()

    run_qaoa(p=args.p, maxiter=args.maxiter)

Stdout

QAOA for MaxCut
  Graph: 3 edges, 3 nodes
  Layers (p): 2
  Max possible cut: 3

  Iter   0: cut ≈ 1.608
  Iter  20: cut ≈ 2.000

  Best cut value: 2.000 / 3
  Approximation ratio: 0.667

QAOA for MaxCut using TorchQuantum backend with native PyTorch autograd.

Source: examples/2_advanced/algorithms/qaoa_pytorch.py
Status: skip — missing requirements: torchquantum (torch + torchquantum installed)

Demonstrates Quantum Approximate Optimization Algorithm on a triangle graph with Adam optimizer, using TorchQuantum’s differentiable simulation.

Source code

"""QAOA for MaxCut using TorchQuantum backend with native PyTorch autograd.

Demonstrates Quantum Approximate Optimization Algorithm on a triangle graph
with Adam optimizer, using TorchQuantum's differentiable simulation.

[doc-require: torchquantum]
"""

try:
    import torch
    from uniqc import QAOASolver
except ImportError as e:
    print(f"Required dependencies not available: {e}")
    print("Install with: pip install unified-quantum[pytorch]")
    print('Then install TorchQuantum manually: pip install "torchquantum @ git+https://github.com/Agony5757/torchquantum.git@fix/optional-qiskit-deps"')
    raise SystemExit(1)


def main():
    print("=" * 60)
    print("QAOA for MaxCut — TorchQuantum Backend")
    print("=" * 60)

    # Triangle graph: 3 nodes, 3 edges
    edges = [(0, 1), (1, 2), (0, 2)]
    n_qubits = 3
    p = 2  # QAOA depth

    print(f"\nGraph: Triangle (3 nodes, 3 edges)")
    print(f"Edges: {edges}")
    print(f"QAOA depth p={p}")
    print(f"Max cut value (exact): 2.0\n")

    # Create solver
    solver = QAOASolver(
        edges=edges,
        n_qubits=n_qubits,
        p=p,
        lr=0.05,
    )

    # Optimize
    cut_value, optimal_params = solver.solve(n_iters=100, verbose=True)

    print(f"\nFinal cut value: {cut_value:.6f}")
    print(f"Optimal gammas: {optimal_params[:p].tolist()}")
    print(f"Optimal betas:  {optimal_params[p:].tolist()}")
    print(f"Approximation ratio: {cut_value / 2.0:.4f}")


if __name__ == "__main__":
    main()

Note

Example skipped during pre-doc-execution: missing requirements: torchquantum (torch + torchquantum installed)

QAOA Variants – XY Mixer, Warm-Start, and MA-QAOA.

Source: examples/2_advanced/algorithms/qaoa_variants.py
Status: pass

Demonstrates:

  • Standard QAOA with XY mixer for constrained optimization

  • Warm-start QAOA with custom initial state

  • MA-QAOA with per-term angles

Usage: python qaoa_variants.py [–p LAYERS] [–n-nodes N]

References: Hadfield, S. et al. (2019). “From the Quantum Approximate Optimization Algorithm to a Quantum Alternating Operator Ansatz.” arXiv:1709.03489.

Egger, D.J. et al. (2021). "Warm-starting quantum optimization."
arXiv:2009.10095.

Hadir, M. et al. (2023). "Multi-Angle Quantum Approximate Optimization
Algorithm." arXiv:2305.04881.

Source code

#!/usr/bin/env python
"""QAOA Variants -- XY Mixer, Warm-Start, and MA-QAOA.

Demonstrates:
  * Standard QAOA with XY mixer for constrained optimization
  * Warm-start QAOA with custom initial state
  * MA-QAOA with per-term angles

Usage:
    python qaoa_variants.py [--p LAYERS] [--n-nodes N]

References:
    Hadfield, S. et al. (2019). "From the Quantum Approximate Optimization
    Algorithm to a Quantum Alternating Operator Ansatz." arXiv:1709.03489.

    Egger, D.J. et al. (2021). "Warm-starting quantum optimization."
    arXiv:2009.10095.

    Hadir, M. et al. (2023). "Multi-Angle Quantum Approximate Optimization
    Algorithm." arXiv:2305.04881.

[doc-require: ]
"""

import argparse
import sys

import numpy as np

sys.path.insert(0, str(__file__).rsplit("/", 2)[0])

from uniqc import Circuit
from uniqc.simulator import Simulator
from uniqc.algorithms.core.ansatz import qaoa_ansatz


def _build_maxcut_hamiltonian(edges):
    """Build MaxCut cost Hamiltonian for a graph.

    H_C = -1/2 sum_{(i,j) in E} (1 - Z_i Z_j)
    """
    hamiltonian = []
    for i, j in edges:
        # -1/2 * (1 - Z_i Z_j) = -1/2*I + 1/2*Z_i Z_j
        hamiltonian.append((f"Z{i}Z{j}", 0.5))
    return hamiltonian


def _print_circuit_info(circuit, label):
    """Print circuit statistics."""
    print(f"\n  {label}:")
    print(f"    Qubits used: {circuit.max_qubit + 1}")
    print(f"    Gate count: {len(circuit.opcode_list)}")


def demo_xy_mixer(p=2, n_nodes=4):
    """Demonstrate QAOA with XY mixer for constrained optimization."""
    print("=" * 60)
    print("Demo 1: XY Mixer")
    print("=" * 60)

    # Ring graph (constrained to exactly n_nodes/2 cuts in optimal solution)
    edges = [(i, (i + 1) % n_nodes) for i in range(n_nodes)]
    H = _build_maxcut_hamiltonian(edges)

    print(f"\n  Graph: Ring with {n_nodes} nodes")
    print(f"  Edges: {edges}")

    # Standard QAOA
    c_standard = qaoa_ansatz(H, p=p)
    print(f"\n  Standard QAOA (X mixer):")
    print(f"    Parameters: betas={p}, gammas={p} (total: {2*p})")
    _print_circuit_info(c_standard, "Standard circuit")

    # XY mixer QAOA
    c_xy = qaoa_ansatz(H, p=p, mixer="xy")
    print(f"\n  XY Mixer QAOA:")
    print(f"    Parameters: betas={p}, gammas={p} (total: {2*p})")
    print(f"    Note: XY mixer preserves excitation number")
    _print_circuit_info(c_xy, "XY mixer circuit")

    # Verify both produce valid statevectors
    sim = Simulator(backend_type="statevector")
    sv_std = sim.simulate_statevector(c_standard.originir)
    sv_xy = sim.simulate_statevector(c_xy.originir)
    print(f"\n  Standard norm: {np.linalg.norm(sv_std):.10f}")
    print(f"  XY mixer norm: {np.linalg.norm(sv_xy):.10f}")


def demo_warm_start(p=2, n_nodes=4):
    """Demonstrate warm-start QAOA with custom initial state."""
    print("\n" + "=" * 60)
    print("Demo 2: Warm-Start QAOA")
    print("=" * 60)

    # Simple line graph
    edges = [(i, i + 1) for i in range(n_nodes - 1)]
    H = _build_maxcut_hamiltonian(edges)

    print(f"\n  Graph: Line with {n_nodes} nodes")
    print(f"  Edges: {edges}")

    # Standard QAOA (starts from uniform superposition)
    c_standard = qaoa_ansatz(H, p=p)
    print(f"\n  Standard QAOA (uniform superposition):")
    print(f"    Initial: Hadamards on all qubits")
    _print_circuit_info(c_standard, "Standard circuit")

    # Warm-start: custom initial state
    # Example: start from a greedy solution (alternate 0/1 pattern)
    warm_state = Circuit()
    for i in range(n_nodes):
        if i % 2 == 0:
            warm_state.x(i)

    c_warm = qaoa_ansatz(H, p=p, initial_state=warm_state)
    print(f"\n  Warm-Start QAOA (greedy solution):")
    print(f"    Initial: |0101...> (alternating pattern)")
    _print_circuit_info(c_warm, "Warm-start circuit")

    # Compare circuit depths
    print(f"\n  Comparison:")
    print(f"    Standard: {len(c_standard.opcode_list)} gates")
    print(f"    Warm-start: {len(c_warm.opcode_list)} gates")

    # Verify both produce valid statevectors
    sim = Simulator(backend_type="statevector")
    sv_std = sim.simulate_statevector(c_standard.originir)
    sv_warm = sim.simulate_statevector(c_warm.originir)
    print(f"\n  Standard norm: {np.linalg.norm(sv_std):.10f}")
    print(f"  Warm-start norm: {np.linalg.norm(sv_warm):.10f}")


def demo_ma_qaoa(p=2, n_nodes=4):
    """Demonstrate MA-QAOA with per-term angles."""
    print("\n" + "=" * 60)
    print("Demo 3: MA-QAOA (Multi-Angle)")
    print("=" * 60)

    # Triangle graph
    edges = [(0, 1), (1, 2), (0, 2)]
    H = _build_maxcut_hamiltonian(edges)

    n_terms = len(edges)
    print(f"\n  Graph: Triangle")
    print(f"  Edges (Hamiltonian terms): {n_terms}")

    # Standard QAOA
    c_standard = qaoa_ansatz(H, p=p)
    std_params = 2 * p  # p betas + p gammas
    print(f"\n  Standard QAOA:")
    print(f"    Parameters: {std_params} (p betas + p gammas)")

    # MA-QAOA: each term gets its own gamma, each qubit gets its own beta
    c_ma = qaoa_ansatz(H, p=p, multi_angle=True)
    ma_betas = n_nodes * p
    ma_gammas = n_terms * p
    ma_params = ma_betas + ma_gammas
    print(f"\n  MA-QAOA:")
    print(f"    Parameters: {ma_params} ({n_terms} terms x {p} layers gammas + {n_nodes} qubits x {p} layers betas)")
    print(f"    Improvement: {ma_params - std_params} extra parameters")

    _print_circuit_info(c_standard, "Standard circuit")
    _print_circuit_info(c_ma, "MA-QAOA circuit")

    # Verify both produce valid statevectors
    sim = Simulator(backend_type="statevector")
    sv_std = sim.simulate_statevector(c_standard.originir)
    sv_ma = sim.simulate_statevector(c_ma.originir)
    print(f"\n  Standard norm: {np.linalg.norm(sv_std):.10f}")
    print(f"  MA-QAOA norm: {np.linalg.norm(sv_ma):.10f}")


def run_demo(p, n_nodes):
    """Run all QAOA variant demos."""
    print(f"\n{'=' * 60}")
    print(f"QAOA Variants Demo (p={p}, nodes={n_nodes})")
    print(f"{'=' * 60}")

    demo_xy_mixer(p, n_nodes)
    demo_warm_start(p, n_nodes)
    demo_ma_qaoa(p, n_nodes)

    print("\n" + "=" * 60)
    print("Demo Complete")
    print("=" * 60)


def main():
    parser = argparse.ArgumentParser(description="QAOA Variants Demo")
    parser.add_argument(
        "-p", "--p-layers", type=int, default=2,
        help="Number of QAOA layers (default: 2)"
    )
    parser.add_argument(
        "-n", "--n-nodes", type=int, default=4,
        help="Number of graph nodes (default: 4)"
    )
    args = parser.parse_args()

    if args.p_layers < 1:
        parser.error("-p must be at least 1")
    if args.n_nodes < 3:
        parser.error("-n must be at least 3")

    run_demo(args.p_layers, args.n_nodes)


if __name__ == "__main__":
    main()

Stdout

============================================================
QAOA Variants Demo (p=2, nodes=4)
============================================================
============================================================
Demo 1: XY Mixer
============================================================

  Graph: Ring with 4 nodes
  Edges: [(0, 1), (1, 2), (2, 3), (3, 0)]

  Standard QAOA (X mixer):
    Parameters: betas=2, gammas=2 (total: 4)

  Standard circuit:
    Qubits used: 4
    Gate count: 52

  XY Mixer QAOA:
    Parameters: betas=2, gammas=2 (total: 4)
    Note: XY mixer preserves excitation number

  XY mixer circuit:
    Qubits used: 4
    Gate count: 40

  Standard norm: 1.0000000000
  XY mixer norm: 1.0000000000

============================================================
Demo 2: Warm-Start QAOA
============================================================

  Graph: Line with 4 nodes
  Edges: [(0, 1), (1, 2), (2, 3)]

  Standard QAOA (uniform superposition):
    Initial: Hadamards on all qubits

  Standard circuit:
    Qubits used: 4
    Gate count: 46

  Warm-Start QAOA (greedy solution):
    Initial: |0101...> (alternating pattern)

  Warm-start circuit:
    Qubits used: 4
    Gate count: 44

  Comparison:
    Standard: 46 gates
    Warm-start: 44 gates

  Standard norm: 1.0000000000
  Warm-start norm: 1.0000000000

============================================================
Demo 3: MA-QAOA (Multi-Angle)
============================================================

  Graph: Triangle
  Edges (Hamiltonian terms): 3

  Standard QAOA:
    Parameters: 4 (p betas + p gammas)

  MA-QAOA:
    Parameters: 14 (3 terms x 2 layers gammas + 4 qubits x 2 layers betas)
    Improvement: 10 extra parameters

  Standard circuit:
    Qubits used: 3
    Gate count: 39

  MA-QAOA circuit:
    Qubits used: 3
    Gate count: 39

  Standard norm: 1.0000000000
  MA-QAOA norm: 1.0000000000

============================================================
Demo Complete
============================================================

ADAPT-VQE Example.

Source: examples/2_advanced/algorithms/adapt_vqe.py
Status: pass

This script demonstrates how to implement ADAPT-VQE (Adaptively Parametrised Variational Quantum Eigensolver) using the existing algorithm components in uniqc.

ADAPT-VQE iteratively builds an ansatz by selecting operators from a pool based on gradient magnitude, rather than using a fixed ansatz structure.

This is an EXAMPLE script, not a reusable algorithm module. Users can copy and adapt this code for their specific use cases.

References:

  • Grimsley et al., “Adaptively parametric ansatz” (2019)

Source code

#!/usr/bin/env python3
"""ADAPT-VQE Example.

This script demonstrates how to implement ADAPT-VQE (Adaptively Parametrised
Variational Quantum Eigensolver) using the existing algorithm components in uniqc.

ADAPT-VQE iteratively builds an ansatz by selecting operators from a pool
based on gradient magnitude, rather than using a fixed ansatz structure.

This is an EXAMPLE script, not a reusable algorithm module. Users can copy and
adapt this code for their specific use cases.

References:
- Grimsley et al., "Adaptively parametric ansatz" (2019)
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Callable, List, Optional, Tuple
import numpy as np

from uniqc.algorithms.core.ansatz._operator_pool import OperatorPool, compute_operator_gradient
from uniqc.algorithms.core.measurement.pauli_expectation import pauli_expectation
from uniqc.algorithms.core.ansatz._pauli_unitary import _apply_cost_unitary
from uniqc.circuit_builder import Circuit
from uniqc.simulator import Simulator


@dataclass
class ADAPTVQEResult:
    """Result of ADAPT-VQE optimization."""

    energy: float
    params: np.ndarray
    selected_operators: List[Tuple[str, float, float]]  # (op, coeff, gradient)
    history: List[float] = field(default_factory=list)
    n_iterations: int = 0
    converged: bool = False


def _build_adapt_circuit(
    operators: List[Tuple[str, float]],
    params: np.ndarray,
    n_qubits: int = 1,
) -> Circuit:
    """Build the ADAPT ansatz circuit from selected operators and parameters."""
    circuit = Circuit(n_qubits)

    for i, (pauli_str, coeff) in enumerate(operators):
        theta = float(params[i]) if i < len(params) else 0.0
        _apply_cost_unitary(circuit, [(pauli_str, coeff)], theta)

    return circuit


def _energy(
    circuit: Circuit,
    hamiltonian: List[Tuple[str, float]],
    shots: Optional[int] = None,
) -> float:
    """Compute expectation value of Hamiltonian."""
    total = 0.0
    for pauli, coeff in hamiltonian:
        total += float(coeff) * pauli_expectation(circuit, pauli, shots=shots)
    return total


def _optimize_circuit(
    circuit: Circuit,
    hamiltonian: List[Tuple[str, float]],
    n_params: int,
    shots: Optional[int] = None,
) -> Tuple[float, np.ndarray]:
    """Optimize parameters using COBYLA (simple derivative-free method)."""
    try:
        from scipy.optimize import minimize
    except ImportError:
        raise ImportError("scipy is required for ADAPT-VQE optimization")

    history: List[float] = []

    def objective(params: np.ndarray) -> float:
        # Rebuild circuit with current parameters
        circ = _build_adapt_circuit(
            [(f"I{''.join(['I'] * (n_params - 1))}", 0.0)],  # placeholder
            params,
        )
        # Actually rebuild with the operators from the closure
        circ = _build_adapt_circuit(operators_from_closure, params)
        e = _energy(circ, hamiltonian, shots)
        history.append(e)
        return e

    # Store operators for the closure
    global operators_from_closure
    operators_from_closure = []

    # Use simple random initialization
    init_params = np.random.uniform(-np.pi / 4, np.pi / 4, size=n_params)

    # Simple optimization
    result = minimize(objective, init_params, method="COBYLA", options={"maxiter": 50})

    return float(result.fun), np.asarray(result.x)


# Global for closure
operators_from_closure = []


def adapt_vqe(
    hamiltonian: List[Tuple[str, float]],
    operator_pool: OperatorPool,
    *,
    n_qubits: Optional[int] = None,
    max_iterations: int = 20,
    convergence_threshold: float = 1e-4,
    shots: Optional[int] = None,
    verbose: bool = True,
) -> ADAPTVQEResult:
    """Run ADAPT-VQE optimization.

    Args:
        hamiltonian: List of (pauli_string, coefficient) tuples.
        operator_pool: Pool of operators to choose from.
        n_qubits: Number of qubits. Auto-detected if not provided.
        max_iterations: Maximum ADAPT iterations.
        convergence_threshold: Stop when gradient norm falls below this.
        shots: Shots for expectation value. None = statevector.
        verbose: Print progress.

    Returns:
        ADAPTVQEResult with optimal energy and parameters.

    Example:
        >>> from uniqc.examples.algorithms.adapt_vqe import adapt_vqe, OperatorPool
        >>> H = [("ZZ", -1.0), ("ZI", 0.5), ("IZ", 0.5)]
        >>> pool = OperatorPool.minimal_pool(n_qubits=2)
        >>> result = adapt_vqe(H, pool, n_qubits=2)
        >>> print(f"Energy: {result.energy:.6f}")
    """
    global operators_from_closure

    # Auto-detect n_qubits
    if n_qubits is None:
        max_idx = 0
        for pauli, _ in hamiltonian:
            for ch in pauli:
                if ch.isdigit():
                    max_idx = max(max_idx, int(ch))
        n_qubits = max_idx + 1

    selected_operators: List[Tuple[str, float]] = []
    params: np.ndarray = np.array([])
    history: List[float] = []

    if verbose:
        print(f"ADAPT-VQE starting with {len(operator_pool)} operators in pool")
        print(f"Hamiltonian: {hamiltonian}")
        print("-" * 60)

    for iteration in range(max_iterations):
        # Build current ansatz circuit
        if selected_operators:
            current_circuit = _build_adapt_circuit(selected_operators, params, n_qubits)
            current_energy = _energy(current_circuit, hamiltonian, shots)
            history.append(current_energy)
        else:
            current_energy = 0.0
            current_circuit = Circuit(n_qubits)

        if verbose:
            print(f"Iter {iteration + 1}: E = {current_energy:.8f}, "
                  f"n_operators = {len(selected_operators)}")

        # Compute gradients for all pool operators
        gradients: List[Tuple[int, str, float, float]] = []

        for i, (pauli_str, coeff) in enumerate(operator_pool.operators()):
            try:
                gradient = compute_operator_gradient(
                    current_circuit,
                    (pauli_str, coeff),
                    hamiltonian,
                    shots,
                    n_qubits=n_qubits,
                )
                gradients.append((i, pauli_str, coeff, gradient))
            except Exception:
                # Skip operators that fail gradient computation
                continue

        if not gradients:
            if verbose:
                print("  No valid gradients, stopping.")
            break

        # Find operator with largest gradient
        gradients.sort(key=lambda x: x[3], reverse=True)
        best_idx, best_pauli, best_coeff, best_grad = gradients[0]

        if verbose:
            print(f"  Best operator: {best_pauli}, gradient = {best_grad:.8f}")

        # Check convergence
        if best_grad < convergence_threshold:
            if verbose:
                print(f"  Converged (gradient {best_grad:.2e} < {convergence_threshold:.2e})")
            break

        # Add operator to ansatz
        selected_operators.append((best_pauli, best_coeff))

        # Remove from pool (optional - for faster subsequent iterations)
        # pool.remove_operator(best_idx)

        # Re-optimize all parameters
        new_params = np.random.uniform(-np.pi / 4, np.pi / 4, size=len(selected_operators))

        # Simple optimization loop
        operators_from_closure = selected_operators

        for _ in range(30):  # Simple SGD-like optimization
            for j in range(len(selected_operators)):
                # Finite difference gradient
                eps = np.pi / 8
                test_params_plus = new_params.copy()
                test_params_plus[j] += eps
                test_params_minus = new_params.copy()
                test_params_minus[j] -= eps

                circ_plus = _build_adapt_circuit(selected_operators, test_params_plus, n_qubits)
                circ_minus = _build_adapt_circuit(selected_operators, test_params_minus, n_qubits)

                e_plus = _energy(circ_plus, hamiltonian, shots)
                e_minus = _energy(circ_minus, hamiltonian, shots)
                grad_j = (e_plus - e_minus) / (2 * eps)

                # Simple gradient descent step
                lr = 0.1
                new_params[j] -= lr * grad_j

        params = new_params

    # Final energy
    if selected_operators:
        final_circuit = _build_adapt_circuit(selected_operators, params, n_qubits)
        final_energy = _energy(final_circuit, hamiltonian, shots)
    else:
        final_energy = 0.0

    if verbose:
        print("-" * 60)
        print(f"Final energy: {final_energy:.8f}")
        print(f"Selected {len(selected_operators)} operators")

    return ADAPTVQEResult(
        energy=final_energy,
        params=params,
        selected_operators=[
            (op, coeff, 0.0) for op, coeff in selected_operators
        ],  # gradient not stored
        history=history,
        n_iterations=iteration + 1,
        converged=best_grad < convergence_threshold if gradients else False,
    )


def main():
    """Run ADAPT-VQE on a simple 2-qubit Hamiltonian."""
    print("=" * 60)
    print("ADAPT-VQE Example: 2-Qubit Hamiltonian")
    print("=" * 60)

    # Simple Hamiltonian: H = -Z0Z1 + 0.5*I
    # Ground state energy should be -1.0 (for the ZZ term)
    hamiltonian = [
        ("ZZ", -1.0),
        ("II", 0.5),
    ]

    # Use minimal operator pool
    pool = OperatorPool.minimal_pool(n_qubits=2)

    print(f"Using {len(pool)} operators from minimal pool")
    print()

    result = adapt_vqe(
        hamiltonian,
        pool,
        n_qubits=2,
        max_iterations=10,
        convergence_threshold=1e-4,
        verbose=True,
    )

    print()
    print("=" * 60)
    print("ADAPT-VQE Results:")
    print(f"  Energy: {result.energy:.8f}")
    print(f"  Iterations: {result.n_iterations}")
    print(f"  Converged: {result.converged}")
    print(f"  Selected operators: {len(result.selected_operators)}")
    for op, coeff, grad in result.selected_operators:
        print(f"    {op} (coeff={coeff:.2f})")

    # Compare with VQE using full HEA
    print()
    print("Comparison with standard VQE (HEA):")
    from uniqc.algorithms.workflows.vqe_workflow import run_vqe_workflow

    vqe_result = run_vqe_workflow(hamiltonian, n_qubits=2, depth=2)
    print(f"  VQE Energy: {vqe_result.energy:.8f}")
    print(f"  VQE Success: {vqe_result.success}")


if __name__ == "__main__":
    main()

Stdout

============================================================
ADAPT-VQE Example: 2-Qubit Hamiltonian
============================================================
Using 5 operators from minimal pool

ADAPT-VQE starting with 5 operators in pool
Hamiltonian: [('ZZ', -1.0), ('II', 0.5)]
------------------------------------------------------------
Iter 1: E = 0.00000000, n_operators = 0
  Best operator: XI, gradient = 0.00000000
  Converged (gradient 0.00e+00 < 1.00e-04)
------------------------------------------------------------
Final energy: 0.00000000
Selected 0 operators

============================================================
ADAPT-VQE Results:
  Energy: 0.00000000
  Iterations: 1
  Converged: True
  Selected operators: 0

Comparison with standard VQE (HEA):
  VQE Energy: -0.50000000
  VQE Success: True

Ansatz 配置

Hardware-Efficient Ansatz (HEA) – Configuration Options.

Source: examples/2_advanced/algorithms/hea_options.py
Status: pass

Demonstrates:

  • Configurable rotation gates (RX, RY, RZ)

  • Configurable entangling gates (CNOT, CZ, CRX, XX)

  • Entanglement topologies (LINEAR, RING, FULL, BRICKWORK)

  • Parameter counting with hea_param_count()

Usage: python hea_options.py [–n-qubits N] [–depth L]

References: Kandala, A. et al. (2017). “Hardware-efficient variational quantum eigensolver for small molecules and quantum magnets.” Nature 549, 242-246. https://doi.org/10.1038/nature23879

Source code

#!/usr/bin/env python
"""Hardware-Efficient Ansatz (HEA) -- Configuration Options.

Demonstrates:
  * Configurable rotation gates (RX, RY, RZ)
  * Configurable entangling gates (CNOT, CZ, CRX, XX)
  * Entanglement topologies (LINEAR, RING, FULL, BRICKWORK)
  * Parameter counting with hea_param_count()

Usage:
    python hea_options.py [--n-qubits N] [--depth L]

References:
    Kandala, A. et al. (2017). "Hardware-efficient variational quantum
    eigensolver for small molecules and quantum magnets."
    Nature 549, 242-246. https://doi.org/10.1038/nature23879

[doc-require: ]
"""

import argparse
import sys

import numpy as np

sys.path.insert(0, str(__file__).rsplit("/", 2)[0])

from uniqc import Circuit
from uniqc.simulator import Simulator
from uniqc.algorithms.core.ansatz import (
    hea,
    hea_param_count,
    RotationGate,
    EntanglingGate,
    EntanglementTopology,
)


def _print_circuit_info(circuit, label):
    """Print circuit statistics."""
    print(f"\n  {label}:")
    print(f"    Qubits used: {circuit.max_qubit + 1}")
    print(f"    Gate count: {len(circuit.opcode_list)}")


def demo_rotation_gates(n_qubits=4, depth=2):
    """Demonstrate different rotation gate configurations."""
    print("=" * 60)
    print("Demo 1: Rotation Gate Configurations")
    print("=" * 60)

    # Default: RZ + RY (backward compatible)
    n_default = hea_param_count(n_qubits, depth, rotation_gates=["rz", "ry"])
    c_default = hea(n_qubits, depth, rotation_gates=["rz", "ry"])
    print(f"\n  Default (RZ + RY):")
    print(f"    Parameters: {n_default}")
    _print_circuit_info(c_default, "RZ+RY circuit")

    # RX only
    n_rx = hea_param_count(n_qubits, depth, rotation_gates=["rx"])
    c_rx = hea(n_qubits, depth, rotation_gates=["rx"])
    print(f"\n  RX only:")
    print(f"    Parameters: {n_rx}")
    _print_circuit_info(c_rx, "RX circuit")

    # RX + RY + RZ (full rotation)
    n_full = hea_param_count(n_qubits, depth, rotation_gates=["rx", "ry", "rz"])
    c_full = hea(n_qubits, depth, rotation_gates=["rx", "ry", "rz"])
    print(f"\n  RX + RY + RZ (full):")
    print(f"    Parameters: {n_full}")
    _print_circuit_info(c_full, "Full rotation circuit")

    # Verify all circuits produce valid statevectors
    sim = Simulator(backend_type="statevector")
    for label, c in [("RZ+RY", c_default), ("RX", c_rx), ("RX+RY+RZ", c_full)]:
        sv = sim.simulate_statevector(c.originir)
        norm = np.linalg.norm(sv)
        print(f"\n  {label} statevector norm: {norm:.10f}")


def demo_entangling_gates(n_qubits=4, depth=1):
    """Demonstrate different entangling gate configurations."""
    print("\n" + "=" * 60)
    print("Demo 2: Entangling Gate Configurations")
    print("=" * 60)

    # Non-parametric gates (0 extra params per edge)
    print("\n  Non-parametric gates:")
    for gate in ["cnot", "cz", "iswap"]:
        n = hea_param_count(n_qubits, depth, entangling_gate=gate, topology="ring")
        c = hea(n_qubits, depth, entangling_gate=gate, topology="ring")
        print(f"    {gate.upper():8s}: {n:3d} params")
        _print_circuit_info(c, f"{gate.upper()} circuit")

    # Parametric gates (extra params per edge)
    print("\n  Parametric gates (extra params per edge):")
    for gate in ["crx", "xx", "yy", "zz"]:
        n = hea_param_count(n_qubits, depth, entangling_gate=gate, topology="ring")
        c = hea(n_qubits, depth, entangling_gate=gate, topology="ring")
        print(f"    {gate.upper():8s}: {n:3d} params")
        _print_circuit_info(c, f"{gate.upper()} circuit")

    # Verify all circuits produce valid statevectors
    sim = Simulator(backend_type="statevector")
    for gate in ["cnot", "cz", "crx", "xx"]:
        c = hea(n_qubits, depth, entangling_gate=gate, topology="ring")
        sv = sim.simulate_statevector(c.originir)
        print(f"\n  {gate.upper()} statevector norm: {np.linalg.norm(sv):.10f}")


def demo_topologies(n_qubits=4, depth=1):
    """Demonstrate different entanglement topologies."""
    print("\n" + "=" * 60)
    print("Demo 3: Entanglement Topologies")
    print("=" * 60)

    for topo in ["linear", "ring", "full", "star", "brickwork"]:
        n = hea_param_count(n_qubits, depth, topology=topo)
        c = hea(n_qubits, depth, topology=topo)
        print(f"\n  {topo.upper():10s}:")
        print(f"    Parameters: {n}")
        _print_circuit_info(c, f"{topo.upper()} circuit")

    # Custom topology
    print("\n  CUSTOM (user-defined edges):")
    custom_edges = [(0, 1), (0, 2), (0, 3)]  # Star from qubit 0
    n = hea_param_count(n_qubits, depth, topology="custom", custom_edges=custom_edges)
    c = hea(n_qubits, depth, topology="custom", custom_edges=custom_edges)
    print(f"    Edges: {custom_edges}")
    print(f"    Parameters: {n}")
    _print_circuit_info(c, "Custom circuit")


def run_demo(n_qubits, depth):
    """Run all HEA configuration demos."""
    print(f"\n{'=' * 60}")
    print(f"HEA Configuration Demo (n_qubits={n_qubits}, depth={depth})")
    print(f"{'=' * 60}")

    demo_rotation_gates(n_qubits, depth)
    demo_entangling_gates(n_qubits, depth)
    demo_topologies(n_qubits, depth)

    print("\n" + "=" * 60)
    print("Demo Complete")
    print("=" * 60)


def main():
    parser = argparse.ArgumentParser(description="HEA Configuration Options Demo")
    parser.add_argument(
        "--n-qubits", type=int, default=4,
        help="Number of qubits (default: 4)"
    )
    parser.add_argument(
        "--depth", type=int, default=2,
        help="Ansatz depth/layers (default: 2)"
    )
    args = parser.parse_args()

    if args.n_qubits < 2:
        parser.error("--n-qubits must be at least 2")
    if args.depth < 0:
        parser.error("--depth must be non-negative")

    run_demo(args.n_qubits, args.depth)


if __name__ == "__main__":
    main()

Stdout

============================================================
HEA Configuration Demo (n_qubits=4, depth=2)
============================================================
============================================================
Demo 1: Rotation Gate Configurations
============================================================

  Default (RZ + RY):
    Parameters: 16

  RZ+RY circuit:
    Qubits used: 4
    Gate count: 24

  RX only:
    Parameters: 8

  RX circuit:
    Qubits used: 4
    Gate count: 16

  RX + RY + RZ (full):
    Parameters: 24

  Full rotation circuit:
    Qubits used: 4
    Gate count: 32

  RZ+RY statevector norm: 1.0000000000

  RX statevector norm: 1.0000000000

  RX+RY+RZ statevector norm: 1.0000000000

============================================================
Demo 2: Entangling Gate Configurations
============================================================

  Non-parametric gates:
    CNOT    :  16 params

  CNOT circuit:
    Qubits used: 4
    Gate count: 24
    CZ      :  16 params

  CZ circuit:
    Qubits used: 4
    Gate count: 24
    ISWAP   :  16 params

  ISWAP circuit:
    Qubits used: 4
    Gate count: 24

  Parametric gates (extra params per edge):
    CRX     :  24 params

  CRX circuit:
    Qubits used: 4
    Gate count: 24
    XX      :  24 params

  XX circuit:
    Qubits used: 4
    Gate count: 24
    YY      :  24 params

  YY circuit:
    Qubits used: 4
    Gate count: 24
    ZZ      :  24 params

  ZZ circuit:
    Qubits used: 4
    Gate count: 24

  CNOT statevector norm: 1.0000000000

  CZ statevector norm: 1.0000000000

  CRX statevector norm: 1.0000000000

  XX statevector norm: 1.0000000000

============================================================
Demo 3: Entanglement Topologies
============================================================

  LINEAR    :
    Parameters: 16

  LINEAR circuit:
    Qubits used: 4
    Gate count: 22

  RING      :
    Parameters: 16

  RING circuit:
    Qubits used: 4
    Gate count: 24

  FULL      :
    Parameters: 16

  FULL circuit:
    Qubits used: 4
    Gate count: 28

  STAR      :
    Parameters: 16

  STAR circuit:
    Qubits used: 4
    Gate count: 22

  BRICKWORK :
    Parameters: 16

  BRICKWORK circuit:
    Qubits used: 4
    Gate count: 19

  CUSTOM (user-defined edges):
    Edges: [(0, 1), (0, 2), (0, 3)]
    Parameters: 16

  Custom circuit:
    Qubits used: 4
    Gate count: 22

============================================================
Demo Complete
============================================================

Hamiltonian Variational Ansatz (HVA) – complete example.

Source: examples/2_advanced/algorithms/hva_example.py
Status: pass

Demonstrates:

  • Building HVA with commuting Hamiltonian groups

  • Hartree-Fock initial state preparation

  • Energy evaluation and optimization

Usage: python hva_example.py [–p-layers L] [–n-iterations N]

References: Wecker, D. et al. (2015). “Hackett, and A. Aspuru-Guzik, Progress toward practical quantum variational algorithms.” Phys. Rev. A 92, 060303.

Kivlichan, I.D. et al. (2018). "Quantum Simulation of Electronic
Structure with Linear Depth and Connectivity."
Phys. Rev. Lett. 120, 110501.

Source code

#!/usr/bin/env python
"""Hamiltonian Variational Ansatz (HVA) -- complete example.

Demonstrates:
  * Building HVA with commuting Hamiltonian groups
  * Hartree-Fock initial state preparation
  * Energy evaluation and optimization

Usage:
    python hva_example.py [--p-layers L] [--n-iterations N]

References:
    Wecker, D. et al. (2015). "Hackett, and A. Aspuru-Guzik,
    Progress toward practical quantum variational algorithms."
    Phys. Rev. A 92, 060303.

    Kivlichan, I.D. et al. (2018). "Quantum Simulation of Electronic
    Structure with Linear Depth and Connectivity."
    Phys. Rev. Lett. 120, 110501.

[doc-require: ]
"""

import argparse
import sys

import numpy as np

sys.path.insert(0, str(__file__).rsplit("/", 2)[0])

from uniqc import Circuit
from uniqc.simulator import Simulator
from uniqc.algorithms.core.ansatz import hva
from uniqc.algorithms.core.measurement import pauli_expectation


def _print_circuit_info(circuit, label):
    """Print circuit statistics."""
    print(f"\n  {label}:")
    print(f"    Qubits used: {circuit.max_qubit + 1}")
    print(f"    Gate count: {len(circuit.opcode_list)}")


def demo_hva_basic(p=2):
    """Demonstrate basic HVA construction."""
    print("=" * 60)
    print("Demo 1: Basic HVA Construction")
    print("=" * 60)

    # Define commuting Hamiltonian groups (simplified Hubbard model)
    # Group 1: Hopping terms (X and Y on same pairs)
    hopping = [
        ("X0X1", 1.0),
        ("Y0Y1", 1.0),
    ]
    # Group 2: Interaction term (Z on same pair)
    interaction = [
        ("Z0Z1", 0.5),
    ]
    groups = [hopping, interaction]

    print(f"\n  Hamiltonian groups:")
    print(f"    Group 1 (hopping): {hopping}")
    print(f"    Group 2 (interaction): {interaction}")

    n_params = len(groups) * p
    print(f"\n  HVA configuration:")
    print(f"    Layers (p): {p}")
    print(f"    Groups: {len(groups)}")
    print(f"    Parameters: {n_params} ({len(groups)} groups x {p} layers)")

    # Build the HVA circuit
    c = hva(groups, p=p)
    _print_circuit_info(c, "HVA circuit")

    # Verify statevector validity
    sim = Simulator(backend_type="statevector")
    sv = sim.simulate_statevector(c.originir)
    print(f"\n  Statevector norm: {np.linalg.norm(sv):.10f}")


def demo_hva_hf_state(p=2):
    """Demonstrate HVA with Hartree-Fock initial state."""
    print("\n" + "=" * 60)
    print("Demo 2: HVA with Hartree-Fock Initial State")
    print("=" * 60)

    # Define groups for a 4-qubit system
    groups = [
        [("X0X1", 1.0), ("X1X2", 1.0)],
        [("Y0Y1", 1.0), ("Y1Y2", 1.0)],
        [("Z0Z1", 0.5), ("Z1Z2", 0.5)],
    ]

    print(f"\n  Hamiltonian groups: 3 groups")
    print(f"    Group 1: X hopping")
    print(f"    Group 2: Y hopping")
    print(f"    Group 3: ZZ interactions")

    # Without Hartree-Fock (all qubits start in |0>)
    c_no_hf = hva(groups, p=p)
    print(f"\n  Without Hartree-Fock:")
    print(f"    Initial state: |0000>")
    _print_circuit_info(c_no_hf, "Circuit")

    # With Hartree-Fock (first 2 qubits in |1>)
    c_hf = hva(groups, p=p, hf_state=[0, 1])
    print(f"\n  With Hartree-Fock:")
    print(f"    Initial state: |1100>")
    _print_circuit_info(c_hf, "Circuit")

    # Verify both produce valid statevectors
    sim = Simulator(backend_type="statevector")
    sv_no_hf = sim.simulate_statevector(c_no_hf.originir)
    sv_hf = sim.simulate_statevector(c_hf.originir)
    print(f"\n  Without HF norm: {np.linalg.norm(sv_no_hf):.10f}")
    print(f"  With HF norm: {np.linalg.norm(sv_hf):.10f}")


def demo_hva_energy(p=3, max_iter=20):
    """Demonstrate HVA energy optimization."""
    print("\n" + "=" * 60)
    print("Demo 3: HVA Energy Optimization")
    print("=" * 60)

    # Simple 2-qubit Hamiltonian: H = -Z0Z1 + 0.5*I
    # Ground state: |00> or |11> with E = -1.0 + 0.5 = -0.5
    H = [
        ("Z0Z1", -1.0),
        ("II", 0.5),
    ]
    groups = [[("Z0Z1", -1.0)]]  # Single group containing ZZ

    print(f"\n  Hamiltonian: H = -Z0Z1 + 0.5*I")
    print(f"  Expected ground state: |00> or |11>")
    print(f"  Expected energy: -0.5")

    def energy(params):
        """Compute expectation value of H."""
        c = hva(groups, p=p, params=params)
        e = 0.0
        for pauli, coeff in H:
            e += coeff * pauli_expectation(c, pauli)
        return e

    # Random initial parameters
    n_params = len(groups) * p
    params = np.random.uniform(-np.pi/4, np.pi/4, size=n_params)

    print(f"\n  Optimization (coordinate descent):")
    best_energy = float("inf")
    best_params = params.copy()
    step = 0.1

    for iteration in range(max_iter):
        improved = False
        for i in range(n_params):
            # Try positive step
            params[i] += step
            e_plus = energy(params)
            # Try negative step
            params[i] -= 2 * step
            e_minus = energy(params)
            # Reset
            params[i] += step
            e_curr = energy(params)

            if e_plus < e_curr and e_plus < e_minus:
                params[i] += step
                improved = True
            elif e_minus < e_curr:
                params[i] -= step
                improved = True

        e = energy(params)
        if e < best_energy:
            best_energy = e
            best_params = params.copy()

        if iteration % 5 == 0:
            print(f"    Iter {iteration:2d}: E = {e:.6f}")

        if not improved:
            step *= 0.5
            if step < 1e-6:
                break

    print(f"\n  Final energy: {best_energy:.6f}")
    print(f"  Expected:     -0.500000")
    print(f"  Accuracy:     {abs(best_energy - (-0.5)):.6f}")


def run_demo(p, max_iter):
    """Run all HVA demos."""
    print(f"\n{'=' * 60}")
    print(f"HVA Example (p={p}, iterations={max_iter})")
    print(f"{'=' * 60}")

    demo_hva_basic(p)
    demo_hva_hf_state(p)
    demo_hva_energy(p, max_iter)

    print("\n" + "=" * 60)
    print("Demo Complete")
    print("=" * 60)


def main():
    parser = argparse.ArgumentParser(description="HVA Example")
    parser.add_argument(
        "-p", "--p-layers", type=int, default=2,
        help="Number of HVA layers (default: 2)"
    )
    parser.add_argument(
        "-n", "--n-iterations", type=int, default=20,
        help="Max optimization iterations (default: 20)"
    )
    args = parser.parse_args()

    if args.p_layers < 1:
        parser.error("-p must be at least 1")
    if args.n_iterations < 1:
        parser.error("-n must be at least 1")

    run_demo(args.p_layers, args.n_iterations)


if __name__ == "__main__":
    main()

Stdout

============================================================
HVA Example (p=2, iterations=20)
============================================================
============================================================
Demo 1: Basic HVA Construction
============================================================

  Hamiltonian groups:
    Group 1 (hopping): [('X0X1', 1.0), ('Y0Y1', 1.0)]
    Group 2 (interaction): [('Z0Z1', 0.5)]

  HVA configuration:
    Layers (p): 2
    Groups: 2
    Parameters: 4 (2 groups x 2 layers)

  HVA circuit:
    Qubits used: 2
    Gate count: 42

  Statevector norm: 1.0000000000

============================================================
Demo 2: HVA with Hartree-Fock Initial State
============================================================

  Hamiltonian groups: 3 groups
    Group 1: X hopping
    Group 2: Y hopping
    Group 3: ZZ interactions

  Without Hartree-Fock:
    Initial state: |0000>

  Circuit:
    Qubits used: 3
    Gate count: 84

  With Hartree-Fock:
    Initial state: |1100>

  Circuit:
    Qubits used: 3
    Gate count: 86

  Without HF norm: 1.0000000000
  With HF norm: 1.0000000000

============================================================
Demo 3: HVA Energy Optimization
============================================================

  Hamiltonian: H = -Z0Z1 + 0.5*I
  Expected ground state: |00> or |11>
  Expected energy: -0.5

  Optimization (coordinate descent):
    Iter  0: E = -0.500000
    Iter  5: E = -0.500000
    Iter 10: E = -0.500000
    Iter 15: E = -0.500000

  Final energy: -0.500000
  Expected:     -0.500000
  Accuracy:     0.000000

============================================================
Demo Complete
============================================================

Symbolic Parameters for Variational Circuits.

Source: examples/2_advanced/algorithms/parameters_demo.py
Status: pass

Demonstrates:

  • Auto-generation of Parameters when params=None

  • Manual binding workflow with Parameters objects

  • Pre-computing parameter counts with hea_param_count()

  • Symbolic arithmetic with Parameter objects

Usage: python parameters_demo.py

Source code

#!/usr/bin/env python
"""Symbolic Parameters for Variational Circuits.

Demonstrates:
  * Auto-generation of Parameters when params=None
  * Manual binding workflow with Parameters objects
  * Pre-computing parameter counts with hea_param_count()
  * Symbolic arithmetic with Parameter objects

Usage:
    python parameters_demo.py

[doc-require: ]
"""

import sys

import numpy as np

sys.path.insert(0, str(__file__).rsplit("/", 2)[0])

from uniqc import Circuit
from uniqc.simulator import Simulator
from uniqc.algorithms.core.ansatz import hea, hea_param_count, qaoa_ansatz, hva
from uniqc.circuit_builder.parameter import Parameter, Parameters


def demo_auto_generation():
    """Demonstrate auto-generation of Parameters when params=None."""
    print("=" * 60)
    print("Demo 1: Auto-Generated Parameters")
    print("=" * 60)

    # HEA without params - auto-generates Parameters
    c = hea(n_qubits=4, depth=2)

    print(f"\n  Calling hea(n_qubits=4, depth=2) without params:")
    print(f"\n  Circuit._params type: {type(c._params).__name__}")
    print(f"  Parameters name: {c._params.name}")
    print(f"  Parameters count: {len(c._params)}")
    print(f"  Parameter names: {c._params.names}")

    # Show first few values
    values = [c._params[i].evaluate() for i in range(min(4, len(c._params)))]
    print(f"  First 4 values: {[f'{v:.4f}' for v in values]}")

    # QAOA auto-generates separate betas and gammas
    H = [("Z0Z1", 1.0), ("Z1Z2", 0.5)]
    c_qaoa = qaoa_ansatz(H, p=2)

    print(f"\n  QAOA auto-generated parameters:")
    print(f"    betas: {c_qaoa._params['betas'].name}, len={len(c_qaoa._params['betas'])}")
    print(f"    gammas: {c_qaoa._params['gammas'].name}, len={len(c_qaoa._params['gammas'])}")


def demo_hea_param_count():
    """Demonstrate pre-computing parameter counts."""
    print("\n" + "=" * 60)
    print("Demo 2: Pre-computing Parameter Counts")
    print("=" * 60)

    n_qubits = 4
    depth = 2

    print(f"\n  For n_qubits={n_qubits}, depth={depth}:")
    print(f"\n  {'Configuration':<30} {'Parameters':<12}")
    print(f"  {'-'*42}")

    configs = [
        ("RZ+RY (default)", dict(rotation_gates=["rz", "ry"])),
        ("RX only", dict(rotation_gates=["rx"])),
        ("RX+RY+RZ", dict(rotation_gates=["rx", "ry", "rz"])),
        ("CNOT (default)", dict(entangling_gate="cnot")),
        ("CZ", dict(entangling_gate="cz")),
        ("XX (parametric)", dict(entangling_gate="xx", topology="ring")),
        ("Linear topology", dict(topology="linear")),
        ("Full topology", dict(topology="full")),
    ]

    for label, kwargs in configs:
        count = hea_param_count(n_qubits, depth, **kwargs)
        print(f"  {label:<30} {count:<12}")

    print(f"\n  Use hea_param_count() to determine array size before building:")
    print(f"\n  n_params = hea_param_count(4, 2, rotation_gates=['rx', 'ry'])")
    n_params = hea_param_count(4, 2, rotation_gates=["rx", "ry"])
    print(f"  # n_params = {n_params}")
    print(f"  params = np.zeros(n_params)")


def demo_manual_binding():
    """Demonstrate manual Parameters binding workflow."""
    print("\n" + "=" * 60)
    print("Demo 3: Manual Parameters Binding")
    print("=" * 60)

    # Step 1: Determine parameter count
    n_qubits = 4
    depth = 2
    n_params = hea_param_count(n_qubits, depth)
    print(f"\n  Step 1: Determine parameter count")
    print(f"    n_params = hea_param_count({n_qubits}, {depth}) = {n_params}")

    # Step 2: Create Parameters object
    print(f"\n  Step 2: Create Parameters object")
    params = Parameters("my_ansatz_params", size=n_params)
    print(f"    params = Parameters('my_ansatz_params', size={n_params})")
    print(f"    names: {params.names}")

    # Step 3: Bind values
    print(f"\n  Step 3: Bind values")
    values = [0.1 * (i + 1) for i in range(n_params)]
    params.bind(values)
    print(f"    params.bind([0.1, 0.2, ..., {values[-1]:.1f}])  # {n_params} values")
    print(f"    Bound: {params[0].is_bound}")

    # Step 4: Use in circuit
    print(f"\n  Step 4: Build circuit")
    c = hea(n_qubits, depth, params=params)
    print(f"    circuit = hea({n_qubits}, {depth}, params=params)")
    print(f"    circuit._params is params: {c._params is params}")

    # Step 5: Verify statevector
    sim = Simulator(backend_type="statevector")
    sv = sim.simulate_statevector(c.originir)
    print(f"\n  Step 5: Verify circuit")
    print(f"    Statevector norm: {np.linalg.norm(sv):.10f}")

    # Step 6: Rebind for new optimization run
    print(f"\n  Step 6: Rebind for new optimization")
    new_values = [0.5] * n_params
    params.bind(new_values)
    c2 = hea(n_qubits, depth, params=params)
    print(f"    params.bind([0.5, 0.5, ..., 0.5])  # {n_params} values")
    print(f"    Rebuilt circuit successfully")


def demo_symbolic_arithmetic():
    """Demonstrate symbolic arithmetic with Parameter objects."""
    print("\n" + "=" * 60)
    print("Demo 4: Symbolic Arithmetic")
    print("=" * 60)

    # Create parameters
    theta = Parameter("theta")
    phi = Parameter("phi")

    print(f"\n  Creating parameters:")
    print(f"    theta = Parameter('theta')")
    print(f"    phi = Parameter('phi')")

    # Arithmetic operations create sympy expressions
    expr1 = theta + phi / 2
    expr2 = theta * 2 - phi
    expr3 = -theta

    print(f"\n  Arithmetic expressions (sympy):")
    print(f"    theta + phi/2 = {expr1}")
    print(f"    theta*2 - phi = {expr2}")
    print(f"    -theta = {expr3}")

    # Evaluate with specific values using substitution
    theta.bind(1.0)
    phi.bind(2.0)

    print(f"\n  After binding theta=1.0, phi=2.0:")
    # Use subs to substitute the symbol values
    subs_dict = {theta.symbol: 1.0, phi.symbol: 2.0}
    print(f"    theta + phi/2 = {float(expr1.subs(subs_dict).evalf())}")
    print(f"    theta*2 - phi = {float(expr2.subs(subs_dict).evalf())}")

    # Show that Parameters array works similarly
    print(f"\n  Parameters array:")
    params = Parameters("alpha", size=3)
    params.bind([0.5, 1.0, 1.5])
    for i in range(3):
        print(f"    params[{i}].name = '{params[i].name}', value = {params[i].evaluate()}")


def run_demo():
    """Run all parameter demos."""
    print("\n" + "=" * 60)
    print("Symbolic Parameters Demo")
    print("=" * 60)

    demo_auto_generation()
    demo_hea_param_count()
    demo_manual_binding()
    demo_symbolic_arithmetic()

    print("\n" + "=" * 60)
    print("Demo Complete")
    print("=" * 60)


def main():
    run_demo()


if __name__ == "__main__":
    main()

Stdout

============================================================
Symbolic Parameters Demo
============================================================
============================================================
Demo 1: Auto-Generated Parameters
============================================================

  Calling hea(n_qubits=4, depth=2) without params:

  Circuit._params type: Parameters
  Parameters name: theta_hea
  Parameters count: 16
  Parameter names: ['theta_hea_0', 'theta_hea_1', 'theta_hea_2', 'theta_hea_3', 'theta_hea_4', 'theta_hea_5', 'theta_hea_6', 'theta_hea_7', 'theta_hea_8', 'theta_hea_9', 'theta_hea_10', 'theta_hea_11', 'theta_hea_12', 'theta_hea_13', 'theta_hea_14', 'theta_hea_15']
  First 4 values: ['4.0021', '1.6951', '0.2574', '0.1038']

  QAOA auto-generated parameters:
    betas: betas_qaoa, len=2
    gammas: gammas_qaoa, len=2

============================================================
Demo 2: Pre-computing Parameter Counts
============================================================

  For n_qubits=4, depth=2:

  Configuration                  Parameters  
  ------------------------------------------
  RZ+RY (default)                16          
  RX only                        8           
  RX+RY+RZ                       24          
  CNOT (default)                 16          
  CZ                             16          
  XX (parametric)                24          
  Linear topology                16          
  Full topology                  16          

  Use hea_param_count() to determine array size before building:

  n_params = hea_param_count(4, 2, rotation_gates=['rx', 'ry'])
  # n_params = 16
  params = np.zeros(n_params)

============================================================
Demo 3: Manual Parameters Binding
============================================================

  Step 1: Determine parameter count
    n_params = hea_param_count(4, 2) = 16

  Step 2: Create Parameters object
    params = Parameters('my_ansatz_params', size=16)
    names: ['my_ansatz_params_0', 'my_ansatz_params_1', 'my_ansatz_params_2', 'my_ansatz_params_3', 'my_ansatz_params_4', 'my_ansatz_params_5', 'my_ansatz_params_6', 'my_ansatz_params_7', 'my_ansatz_params_8', 'my_ansatz_params_9', 'my_ansatz_params_10', 'my_ansatz_params_11', 'my_ansatz_params_12', 'my_ansatz_params_13', 'my_ansatz_params_14', 'my_ansatz_params_15']

  Step 3: Bind values
    params.bind([0.1, 0.2, ..., 1.6])  # 16 values
    Bound: True

  Step 4: Build circuit
    circuit = hea(4, 2, params=params)
    circuit._params is params: True

  Step 5: Verify circuit
    Statevector norm: 1.0000000000

  Step 6: Rebind for new optimization
    params.bind([0.5, 0.5, ..., 0.5])  # 16 values
    Rebuilt circuit successfully

============================================================
Demo 4: Symbolic Arithmetic
============================================================

  Creating parameters:
    theta = Parameter('theta')
    phi = Parameter('phi')

  Arithmetic expressions (sympy):
    theta + phi/2 = phi/2 + theta
    theta*2 - phi = -phi + 2*theta
    -theta = -theta

  After binding theta=1.0, phi=2.0:
    theta + phi/2 = 2.0
    theta*2 - phi = 0.0

  Parameters array:
    params[0].name = 'alpha_0', value = 0.5
    params[1].name = 'alpha_1', value = 1.0
    params[2].name = 'alpha_2', value = 1.5

============================================================
Demo Complete
============================================================

量子‑经典混合模型

Hybrid Classical-Quantum Model using TorchQuantum backend.

Source: examples/2_advanced/algorithms/hybrid_model.py
Status: skip — missing requirements: torchquantum (torch + torchquantum installed)

Demonstrates a hybrid architecture: Classical encoder → Quantum circuit → Classical decoder, for 2D binary classification.

Source code

"""Hybrid Classical-Quantum Model using TorchQuantum backend.

Demonstrates a hybrid architecture: Classical encoder → Quantum circuit
→ Classical decoder, for 2D binary classification.

[doc-require: torchquantum]
"""

try:
    import torch
    import torch.nn as nn
    from sklearn.datasets import make_moons
    from sklearn.preprocessing import StandardScaler

    from uniqc import HybridQCLModel
except ImportError as e:
    print(f"Required dependencies not available: {e}")
    print("Install with: pip install unified-quantum[pytorch] scikit-learn")
    print('Then install TorchQuantum manually: pip install "torchquantum @ git+https://github.com/Agony5757/torchquantum.git@fix/optional-qiskit-deps"')
    raise SystemExit(1)


def main():
    print("=" * 60)
    print("Hybrid Classical-Quantum Model — TorchQuantum Backend")
    print("=" * 60)

    # Generate dataset
    X_np, y_np = make_moons(n_samples=100, noise=0.15, random_state=42)
    scaler = StandardScaler()
    X_np = scaler.fit_transform(X_np)
    X = torch.tensor(X_np, dtype=torch.float32)
    y = torch.tensor(y_np, dtype=torch.float32).unsqueeze(1)

    # Model
    model = HybridQCLModel(
        n_features=2,
        n_qubits=4,
        quantum_depth=2,
        classical_hidden=16,
    )
    optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
    loss_fn = nn.BCELoss()

    n_params = sum(p.numel() for p in model.parameters())
    print(f"\nDataset: make_moons (100 samples)")
    print(f"Model: HybridQCLModel (classical → quantum → classical)")
    print(f"Total parameters: {n_params}")
    print(f"  Encoder:  {sum(p.numel() for p in model.encoder.parameters())}")
    print(f"  Quantum:  {model.quantum_params.numel()}")
    print(f"  Decoder:  {sum(p.numel() for p in model.decoder.parameters())}\n")

    # Training
    for epoch in range(50):
        optimizer.zero_grad()
        y_pred = model(X)
        loss = loss_fn(y_pred, y)
        loss.backward()
        optimizer.step()

        if (epoch + 1) % 10 == 0:
            acc = ((y_pred > 0.5).float() == y).float().mean()
            print(f"  Epoch {epoch + 1:3d} | Loss: {loss.item():.4f} | Acc: {acc:.4f}")

    # Final accuracy
    with torch.no_grad():
        y_pred = model(X)
        acc = ((y_pred > 0.5).float() == y).float().mean()
    print(f"\nFinal accuracy: {acc:.4f}")


if __name__ == "__main__":
    main()

Note

Example skipped during pre-doc-execution: missing requirements: torchquantum (torch + torchquantum installed)

QNN Binary Classifier using TorchQuantum backend.

Source: examples/2_advanced/algorithms/qnn_classifier.py
Status: skip — missing requirements: torchquantum (torch + torchquantum installed)

Demonstrates a Quantum Neural Network for binary classification on a synthetic moons dataset with native PyTorch autograd.

Source code

"""QNN Binary Classifier using TorchQuantum backend.

Demonstrates a Quantum Neural Network for binary classification on
a synthetic moons dataset with native PyTorch autograd.

[doc-require: torchquantum]
"""

try:
    import torch
    import torch.nn as nn
    from sklearn.datasets import make_moons
    from sklearn.preprocessing import StandardScaler

    from uniqc import QNNClassifier
except ImportError as e:
    print(f"Required dependencies not available: {e}")
    print("Install with: pip install unified-quantum[pytorch] scikit-learn")
    print('Then install TorchQuantum manually: pip install "torchquantum @ git+https://github.com/Agony5757/torchquantum.git@fix/optional-qiskit-deps"')
    raise SystemExit(1)


def main():
    print("=" * 60)
    print("QNN Binary Classifier — TorchQuantum Backend")
    print("=" * 60)

    # Generate moons dataset
    X_np, y_np = make_moons(n_samples=100, noise=0.15, random_state=42)
    scaler = StandardScaler()
    X_np = scaler.fit_transform(X_np)
    X = torch.tensor(X_np, dtype=torch.float32)
    y = torch.tensor(y_np, dtype=torch.float32)

    # Model
    n_qubits = 4
    model = QNNClassifier(n_qubits=n_qubits, n_features=2, depth=2)
    optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
    loss_fn = nn.BCELoss()

    print(f"\nDataset: make_moons (100 samples, 2 features)")
    print(f"Model: QNN (n_qubits={n_qubits}, depth=2)")
    print(f"Parameters: {sum(p.numel() for p in model.parameters())}\n")

    # Training
    for epoch in range(50):
        optimizer.zero_grad()
        y_pred = model(X)
        loss = loss_fn(y_pred, y)
        loss.backward()
        optimizer.step()

        if (epoch + 1) % 10 == 0:
            acc = ((y_pred > 0.5).float() == y).float().mean()
            print(f"  Epoch {epoch + 1:3d} | Loss: {loss.item():.4f} | Acc: {acc:.4f}")

    # Final accuracy
    with torch.no_grad():
        y_pred = model(X)
        acc = ((y_pred > 0.5).float() == y).float().mean()
    print(f"\nFinal accuracy: {acc:.4f}")


if __name__ == "__main__":
    main()

Note

Example skipped during pre-doc-execution: missing requirements: torchquantum (torch + torchquantum installed)

QCNN Quantum State Classifier using TorchQuantum backend.

Source: examples/2_advanced/algorithms/qcnn_classifier.py
Status: skip — missing requirements: torchquantum (torch + torchquantum installed)

Demonstrates a Quantum Convolutional Neural Network for classifying quantum states (GHZ vs product states) with native PyTorch autograd.

Source code

"""QCNN Quantum State Classifier using TorchQuantum backend.

Demonstrates a Quantum Convolutional Neural Network for classifying
quantum states (GHZ vs product states) with native PyTorch autograd.

[doc-require: torchquantum]
"""

try:
    import torch
    import torch.nn as nn

    from uniqc import QCNNClassifier
except ImportError as e:
    print(f"Required dependencies not available: {e}")
    print("Install with: pip install unified-quantum[pytorch]")
    print('Then install TorchQuantum manually: pip install "torchquantum @ git+https://github.com/Agony5757/torchquantum.git@fix/optional-qiskit-deps"')
    raise SystemExit(1)


def generate_state_dataset(n_qubits: int, n_samples: int):
    """Generate dataset of GHZ (label=1) and |0...0> (label=0) states."""
    from uniqc import Circuit
    from uniqc.simulator import Simulator

    sim = Simulator(backend_type="statevector")

    states = []
    labels = []

    for i in range(n_samples):
        if i < n_samples // 2:
            # |0...0> state
            c = Circuit(n_qubits)
            sv = sim.simulate_statevector(c.originir)
            labels.append(0.0)
        else:
            # GHZ state: (|0...0> + |1...1>) / sqrt(2)
            c = Circuit(n_qubits)
            c.h(0)
            for q in range(1, n_qubits):
                c.cx(0, q)
            sv = sim.simulate_statevector(c.originir)
            labels.append(1.0)

        states.append(torch.tensor(sv, dtype=torch.float32))

    return states, labels


def main():
    print("=" * 60)
    print("QCNN State Classifier — TorchQuantum Backend")
    print("=" * 60)

    n_qubits = 4
    n_samples = 20

    print(f"\nTask: Classify GHZ vs |0...0> states")
    print(f"Qubits: {n_qubits}")
    print(f"Model: QCNN\n")

    # QCNN model
    model = QCNNClassifier(n_qubits=n_qubits, n_classes=2)
    optimizer = torch.optim.Adam(model.parameters(), lr=0.01)

    print(f"Parameters: {sum(p.numel() for p in model.parameters())}\n")

    # Simple training loop with synthetic labels
    # Since QCNN operates on the quantum state directly, we train
    # the conv/pool parameters to distinguish states
    labels = torch.tensor(
        [0.0] * (n_samples // 2) + [1.0] * (n_samples // 2), dtype=torch.float32
    )

    for epoch in range(50):
        total_loss = 0.0
        correct = 0

        for i in range(n_samples):
            optimizer.zero_grad()
            output = model()
            target = labels[i]
            loss = ((output - target) ** 2).mean()
            loss.backward()
            optimizer.step()
            total_loss += loss.item()
            correct += int((output.item() > 0.5) == target.item())

        if (epoch + 1) % 10 == 0:
            avg_loss = total_loss / n_samples
            acc = correct / n_samples
            print(f"  Epoch {epoch + 1:3d} | Loss: {avg_loss:.4f} | Acc: {acc:.4f}")

    print("\nTraining complete.")


if __name__ == "__main__":
    main()

Note

Example skipped during pre-doc-execution: missing requirements: torchquantum (torch + torchquantum installed)