Source code for uniqc.algorithms.core.circuits.vqd

"""Variational Quantum Deflation (VQD) circuit components."""

__all__ = ["vqd_circuit", "vqd_ansatz", "vqd_overlap_circuit", "vqd_example"]

import warnings

import numpy as np

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


def _hea_ansatz(
    circuit: Circuit,
    params: list[float],
    n_layers: int,
    qubits: list[int],
) -> None:
    r"""Apply a Hardware-Efficient Ansatz (HEA) to the circuit.

    Each layer consists of:
    1. ``Ry`` rotation on every qubit.
    2. A chain of CNOT gates between adjacent qubits.

    The total number of parameters required is ``n_qubits * n_layers``.

    Args:
        circuit: Quantum circuit to operate on (mutated in-place).
        params: Rotation angles.  Length must equal ``len(qubits) * n_layers``.
        n_layers: Number of repeating layers.
        qubits: Qubit indices to apply the ansatz on.

    Raises:
        ValueError: Parameter count does not match ``n_qubits * n_layers``.

    Example:
        >>> from uniqc.circuit_builder import Circuit
        >>> c = Circuit(2)
        >>> _hea_ansatz(c, [0.1, 0.2, 0.3, 0.4], n_layers=2, qubits=[0, 1])
    """
    n_qubits = len(qubits)
    expected = n_qubits * n_layers
    if len(params) != expected:
        raise ValueError(
            format_enriched_message(
                f"Expected {expected} parameters (n_qubits={n_qubits} × n_layers={n_layers}), got {len(params)}",
                "circuit_validation",
            )
        )

    idx = 0
    for _ in range(n_layers):
        # Single-qubit Ry rotations
        for q in qubits:
            circuit.ry(q, params[idx])
            idx += 1
        # Entangling CNOT chain
        for i in range(n_qubits - 1):
            circuit.cnot(qubits[i], qubits[i + 1])


[docs] def vqd_ansatz( n_qubits: int, ansatz_params: list[float], prev_states: list[np.ndarray], qubits: list[int] | None = None, penalty: float = 10.0, n_layers: int = 2, ) -> Circuit: r"""Build a VQD ansatz circuit fragment (variational style). Returns a fresh :class:`Circuit`. ``prev_states`` is accepted to keep the VQD signature, but is only used by the classical optimiser. """ if qubits is None: qubits = list(range(n_qubits)) if len(prev_states) == 0: raise ValueError( format_enriched_message( "prev_states is empty. Use VQE (not VQD) for the ground state.", "circuit_validation" ) ) fragment = Circuit() _hea_ansatz(fragment, ansatz_params, n_layers, qubits) return fragment
[docs] def vqd_circuit( *args, ansatz_params: list[float] | None = None, prev_states: list[np.ndarray] | None = None, qubits: list[int] | None = None, penalty: float = 10.0, n_layers: int = 2, ): r"""Build (or apply) a VQD ansatz. Two calling conventions: .. code-block:: python # Variational fragment style (recommended; see also vqd_ansatz): c = vqd_circuit(2, ansatz_params=[0.1]*4, prev_states=[gs], n_layers=2) # Legacy in-place (deprecated): c = Circuit(2) vqd_circuit(c, [0.1]*4, prev_states=[gs], n_layers=2) """ if len(args) >= 1 and isinstance(args[0], Circuit): circuit_in = args[0] if len(args) >= 2 and ansatz_params is None: ansatz_params = args[1] if len(args) >= 3 and prev_states is None: prev_states = args[2] warnings.warn( "vqd_circuit(circuit, ansatz_params, prev_states, ...) (in-place form) is " "deprecated. Use vqd_ansatz(n_qubits, ansatz_params, prev_states, ...) " "and add_circuit().", DeprecationWarning, stacklevel=2, ) if qubits is None: qubits = list(range(circuit_in.qubit_num)) if not prev_states: raise ValueError( format_enriched_message( "prev_states is empty. Use VQE (not VQD) for the ground state.", "circuit_validation" ) ) _hea_ansatz(circuit_in, ansatz_params, n_layers, qubits) return None # Fragment-style call if len(args) >= 1 and isinstance(args[0], int): n_qubits = args[0] elif qubits is not None: n_qubits = max(qubits) + 1 else: raise TypeError( format_enriched_message("vqd_circuit requires n_qubits as first positional arg", "circuit_validation") ) if ansatz_params is None or prev_states is None: raise TypeError( format_enriched_message("vqd_circuit requires ansatz_params and prev_states", "circuit_validation") ) return vqd_ansatz( n_qubits, ansatz_params, prev_states, qubits=qubits, penalty=penalty, n_layers=n_layers, )
[docs] def vqd_overlap_circuit( prev_state: np.ndarray, ansatz_params: list[float], n_layers: int = 2, qubits: list[int] | None = None, ) -> Circuit: r"""Build a circuit to compute :math:`|\langle\psi(\boldsymbol{\theta})|\phi\rangle|^2`. Uses the **swap test**: an ancilla qubit controls SWAPs between the ansatz register and a register prepared in *prev_state*. Measuring the ancilla in the computational basis gives an estimate of the overlap. Circuit layout (2 data qubits):: ancilla: ──H──●──────●──●──────●── Measure | | | | data_A: ──[ansatz]──SWAP──[ansatz]──SWAP── | | | | data_B: ──[prev]──SWAP──[prev]──SWAP── Args: prev_state: State vector :math:`|\phi\rangle` of dimension :math:`2^n`. ansatz_params: Parameters for the HEA ansatz. n_layers: Number of HEA layers. qubits: Data qubit indices for the ansatz register. ``None`` means ``[0, 1, …, n-1]`` where *n* is inferred from ``prev_state``. Returns: A new :class:`Circuit` containing the swap-test circuit with the ancilla measured. Raises: ValueError: *prev_state* is not a power-of-2 length. Example: >>> import numpy as np >>> gs = np.array([1, 0, 0, 0], dtype=complex) >>> circ = vqd_overlap_circuit(gs, [0.1]*4, n_layers=2) """ dim = len(prev_state) n = int(np.log2(dim)) if 2**n != dim: raise ValueError(format_enriched_message(f"prev_state length {dim} is not a power of 2.", "circuit_validation")) if qubits is None: qubits = list(range(n)) # Total qubits: 1 ancilla + n (ansatz) + n (prev_state) total = 1 + 2 * n circ = Circuit() ancilla = 0 data_a = list(range(1, 1 + n)) # ansatz register data_b = list(range(1 + n, 1 + 2 * n)) # prev-state register # Prepare prev_state on data_b using state preparation _prepare_state(circ, prev_state, data_b) # Apply ansatz on data_a _hea_ansatz(circ, ansatz_params, n_layers, data_a) # Swap test circ.h(ancilla) for i in range(n): circ.cnot(ancilla, data_a[i]) circ.cnot(ancilla, data_b[i]) # Controlled-SWAP decomposition: CSWAP(ancilla, a, b) # = CNOT(b, a) — H(b) — T(b) — CNOT(a, b) — T†(a) — CNOT(ancilla, b) # — T(a) — CNOT(a, b) — T†(b) — H(b) — CNOT(ancilla, a) # Simpler: just use three CNOTs with ancilla control # Standard decomposition of Toffoli-based CSWAP: circ.cnot(data_b[i], data_a[i]) circ.cnot(ancilla, data_b[i]) circ.cnot(data_b[i], data_a[i]) circ.cnot(ancilla, data_b[i]) circ.cnot(data_a[i], data_b[i]) circ.h(ancilla) circ.measure(ancilla) return circ
def _prepare_state( circuit: Circuit, state: np.ndarray, qubits: list[int], ) -> None: """Prepare an arbitrary state vector on the given qubits using multiplexed rotations. For small state vectors this uses a simple Schmidt-decomposition based preparation. Normalises *state* if needed. Args: circuit: Circuit to modify in-place. state: Target state vector. qubits: Qubit indices. """ n = len(qubits) dim = len(state) if dim != 2**n: raise ValueError( format_enriched_message( f"State vector length {dim} does not match {n} qubits (expected {2**n}).", "circuit_validation" ) ) # Normalise norm = np.linalg.norm(state) if norm == 0: raise ValueError(format_enriched_message("State vector is zero.", "circuit_validation")) state = state / norm # Use state preparation via multiplexed Ry rotations (Schmidt decomposition) # This is a simplified recursive approach _state_prep_recursive(circuit, state, qubits) def _state_prep_recursive( circuit: Circuit, state: np.ndarray, qubits: list[int], ) -> None: """Recursively prepare a state vector using controlled Ry rotations.""" n = len(qubits) dim = len(state) if n == 1: # Single qubit: just Ry alpha = float(state[0]) beta = float(state[1]) if dim > 1 else 0.0 amp = np.sqrt(abs(alpha) ** 2 + abs(beta) ** 2) if amp < 1e-15: return theta = 2 * np.arccos(np.clip(abs(alpha) / amp, 0, 1)) circuit.ry(qubits[0], theta) if beta != 0 and alpha != 0: phase_diff = np.angle(beta) - np.angle(alpha) if abs(phase_diff) > 1e-10: circuit.rz(qubits[0], phase_diff) return # Split state into two halves (for most-significant qubit control) half = dim // 2 top = state[:half] bot = state[half:] norm_top = np.linalg.norm(top) norm_bot = np.linalg.norm(bot) total = np.sqrt(norm_top**2 + norm_bot**2) if total < 1e-15: return theta = 2 * np.arccos(np.clip(norm_top / total, 0, 1)) circuit.ry(qubits[0], theta) if norm_top > 1e-15: _state_prep_recursive(circuit, top / norm_top, qubits[1:]) # Apply X to flip to bottom half circuit.x(qubits[0]) if norm_bot > 1e-15: _state_prep_recursive(circuit, bot / norm_bot, qubits[1:]) circuit.x(qubits[0])
[docs] def vqd_example() -> Circuit: """Return a small VQD ansatz fragment for tests/docs.""" gs = np.array([1, 0, 0, 0], dtype=complex) return vqd_ansatz(2, [0.1] * 4, prev_states=[gs], n_layers=2)