"""Variational Quantum Deflation (VQD) circuit components."""
__all__ = ["vqd_circuit", "vqd_overlap_circuit"]
from typing import List, Optional
import numpy as np
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(
f"Expected {expected} parameters (n_qubits={n_qubits} × "
f"n_layers={n_layers}), got {len(params)}"
)
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_circuit(
circuit: Circuit,
ansatz_params: List[float],
prev_states: List[np.ndarray],
qubits: Optional[List[int]] = None,
penalty: float = 10.0,
n_layers: int = 2,
) -> None:
r"""Apply a VQD ansatz circuit to *circuit*.
Variational Quantum Deflation (VQD) is a hybrid algorithm for finding
excited states of a Hamiltonian one at a time. It minimises the
cost function
.. math::
C(\boldsymbol{\theta}) =
\langle\psi(\boldsymbol{\theta})|H|\psi(\boldsymbol{\theta})\rangle
+ \sum_i \beta_i\,|\langle\psi(\boldsymbol{\theta})|\phi_i\rangle|^2
where :math:`|\phi_i\rangle` are previously found lower-energy states
and :math:`\beta_i` are penalty coefficients.
This function **only** constructs the parameterised ansatz on the
circuit. The overlap penalty terms are evaluated separately (see
:func:`vqd_overlap_circuit`) and combined by a classical optimiser.
Args:
circuit: Quantum circuit to operate on (mutated in-place).
ansatz_params: Parameters for the HEA ansatz.
prev_states: List of previously found state vectors (used by
the classical optimiser, not directly in this circuit).
qubits: Qubit indices. ``None`` means all qubits of *circuit*.
penalty: Penalty coefficient :math:`\beta` (used by the caller).
n_layers: Number of HEA layers.
Raises:
ValueError: If *prev_states* is empty (use VQE for the ground state).
Example:
>>> from uniqc.circuit_builder import Circuit
>>> import numpy as np
>>> c = Circuit(2)
>>> gs = np.array([1, 0, 0, 0], dtype=complex)
>>> vqd_circuit(c, [0.1]*4, prev_states=[gs], n_layers=2)
"""
if qubits is None:
qubits = list(range(circuit.qubit_num))
if len(prev_states) == 0:
raise ValueError(
"prev_states is empty. Use VQE (not VQD) for the ground state."
)
_hea_ansatz(circuit, ansatz_params, n_layers, qubits)
[docs]
def vqd_overlap_circuit(
prev_state: np.ndarray,
ansatz_params: List[float],
n_layers: int = 2,
qubits: Optional[List[int]] = 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(
f"prev_state length {dim} is not a power of 2."
)
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(
f"State vector length {dim} does not match {n} qubits (expected {2**n})."
)
# Normalise
norm = np.linalg.norm(state)
if norm == 0:
raise ValueError("State vector is zero.")
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])