Source code for uniqc.algorithms.core.ansatz.uccsd

"""UCCSD (Unitary Coupled-Cluster Singles and Doubles) ansatz.

Implements a simplified UCCSD ansatz for variational quantum chemistry
simulations.  Each single/double excitation is parameterised by an
independent variational angle.
"""

__all__ = ["uccsd_ansatz"]

from itertools import combinations
from typing import TYPE_CHECKING, Union

import numpy as np

from uniqc._error_hints import format_enriched_message
from uniqc.circuit_builder import Circuit

if TYPE_CHECKING:
    from uniqc.circuit_builder.parameter import Parameters


def _single_excitation(
    circuit: Circuit,
    p: int,
    q: int,
    theta: float,
) -> None:
    """Apply a single-excitation gate G^{pq}(θ).

    Maps |0_p 1_q> → cos(θ)|0_p 1_q> + sin(θ)|1_p 0_q>.
    Uses a Givens-rotation decomposition with 2 CNOTs.
    """
    # G^{pq}(θ) = CX(p,q); Ry(q, -θ); CX(p,q)
    # This maps |01> → cos(θ)|01> - sin(θ)|10> in the {p,q} subspace
    # (when p is the higher orbital and q the lower)
    circuit.cx(p, q)
    if abs(theta) > 1e-15:
        circuit.ry(q, float(-theta))
    circuit.cx(p, q)


def _double_excitation(
    circuit: Circuit,
    i: int,
    j: int,
    a: int,
    b: int,
    theta: float,
) -> None:
    """Apply a double-excitation gate.

    Uses 8 CNOTs + 1 Ry for the standard UCCSD double excitation
    decomposition.
    """
    # Standard decomposition for exp(θ (a†_b a†_a a_j a_i - h.c.))
    # Uses the 8-CNOT decomposition
    # Step 1: CNOT cascade from occupied to virtual
    circuit.cx(i, j)
    circuit.cx(j, a)
    circuit.cx(a, b)

    # Step 2: Ry on target
    if abs(theta / 2) > 1e-15:
        circuit.ry(b, float(theta / 2))

    # Step 3: Undo and redo with different control phase
    circuit.cx(a, b)
    if abs(theta / 2) > 1e-15:
        circuit.ry(b, float(-theta / 2))
    circuit.cx(j, b)
    if abs(theta / 2) > 1e-15:
        circuit.ry(b, float(theta / 2))
    circuit.cx(a, b)
    if abs(theta / 2) > 1e-15:
        circuit.ry(b, float(-theta / 2))

    # Step 4: Undo cascade
    circuit.cx(j, b)
    circuit.cx(i, b)
    circuit.cx(i, j)


[docs] def uccsd_ansatz( n_qubits: int, n_electrons: int, qubits: list[int] | None = None, params: Union["Parameters", np.ndarray] | None = None, ) -> Circuit: """Build a UCCSD (Unitary Coupled-Cluster Singles and Doubles) ansatz. Occupies the first *n_electrons* spin-orbitals and allows single excitations from occupied → virtual and double excitations from pairs of occupied → pairs of virtual. Args: n_qubits: Total number of qubits (spin-orbitals). n_electrons: Number of occupied spin-orbitals. qubits: Qubit indices. ``None`` → ``list(range(n_qubits))``. params: Variational parameters. ``None`` → zeros (no excitation). Returns: A :class:`Circuit` object. Raises: ValueError: *n_electrons* > *n_qubits*. ValueError: *params* length does not match the expected count. Example: >>> from uniqc.algorithms.core.ansatz import uccsd_ansatz >>> c = uccsd_ansatz(n_qubits=4, n_electrons=2) """ if n_electrons > n_qubits: raise ValueError( format_enriched_message( f"n_electrons ({n_electrons}) must not exceed n_qubits ({n_qubits})", "circuit_validation" ) ) if qubits is None: qubits = list(range(n_qubits)) else: qubits = list(qubits) occupied = list(range(n_electrons)) virtual = list(range(n_electrons, n_qubits)) # Count singles and doubles n_singles = len(occupied) * len(virtual) n_doubles = len(list(combinations(occupied, 2))) * len(list(combinations(virtual, 2))) n_params = n_singles + n_doubles # Import Parameters for auto-generation from uniqc.circuit_builder.parameter import Parameters as ParamClass if params is None: # Auto-generate named Parameters (initialized to zero) params = ParamClass("theta_uccsd", size=n_params) params.bind([0.0] * n_params) elif isinstance(params, ParamClass): if len(params) != n_params: raise ValueError( format_enriched_message( f"Expected {n_params} parameters ({n_singles} singles + {n_doubles} doubles), got {len(params)}", "circuit_validation", ) ) if not params[0].is_bound: params.bind([0.0] * n_params) else: # Convert np.ndarray to Parameters params_arr = np.asarray(params) if len(params_arr) != n_params: raise ValueError( format_enriched_message( f"Expected {n_params} parameters " f"({n_singles} singles + {n_doubles} doubles), " f"got {len(params_arr)}", "circuit_validation", ) ) params = ParamClass("theta_uccsd", size=n_params) params.bind(list(params_arr.flatten())) circuit = Circuit() # Hartree-Fock initial state: occupy first n_electrons qubits for i in occupied: circuit.x(qubits[i]) idx = 0 # Single excitations: occupied → virtual for occ in occupied: for virt in virtual: val = params[idx].evaluate() if abs(val) > 1e-15: _single_excitation(circuit, qubits[occ], qubits[virt], val) idx += 1 # Double excitations: pairs of occupied → pairs of virtual for i, j in combinations(occupied, 2): for a, b in combinations(virtual, 2): val = params[idx].evaluate() if abs(val) > 1e-15: _double_excitation(circuit, qubits[i], qubits[j], qubits[a], qubits[b], val) idx += 1 # Attach parameters to circuit for traceability circuit._params = params return circuit