"""Variational Quantum Deflation (VQD) circuit components."""
__all__ = ["vqd_circuit", "vqd_ansatz", "vqd_overlap_circuit", "vqd_example"]
import numpy as np
from uniqc._error_hints import format_enriched_message
from uniqc.algorithms.core.state_preparation import rotation_prepare
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(
n_qubits: int | None = None,
*,
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,
) -> Circuit:
r"""Build a VQD ansatz circuit fragment.
.. code-block:: python
# Variational fragment style (see also vqd_ansatz):
c = vqd_circuit(2, ansatz_params=[0.1]*4, prev_states=[gs], n_layers=2)
``n_qubits`` may be ``None`` if ``qubits`` is given, in which case it is
inferred as ``max(qubits) + 1``.
"""
if n_qubits is None:
if 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.
``qubits`` names the ansatz register exactly. The ancilla and previous-state
register are allocated from the lowest unused non-negative indices.
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"))
data_a = list(range(n)) if qubits is None else [int(qubit) for qubit in qubits]
if len(data_a) != n:
raise ValueError(
format_enriched_message(
f"Expected {n} ansatz qubits, got {len(data_a)}",
"circuit_validation",
)
)
if len(set(data_a)) != len(data_a) or any(qubit < 0 for qubit in data_a):
raise ValueError(
format_enriched_message(
"qubits must contain distinct non-negative indices",
"circuit_validation",
)
)
used = set(data_a)
available = (qubit for qubit in range(max(data_a, default=-1) + 2 * n + 2) if qubit not in used)
ancilla = next(available)
data_b = [next(available) for _ in range(n)]
circ = Circuit(max([ancilla, *data_a, *data_b], default=-1) + 1)
# Prepare prev_state on data_b using state preparation
rotation_prepare(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 ansatz_qubit, previous_qubit in zip(data_a, data_b, strict=True):
circ.cswap(ancilla, ansatz_qubit, previous_qubit)
circ.h(ancilla)
circ.measure(ancilla)
return circ
[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)