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

"""Hamiltonian Variational Ansatz (HVA).

Constructs an ansatz that alternates between groups of commuting Hamiltonian
terms, suitable for quantum chemistry simulations.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import numpy as np

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

from ._pauli_unitary import _apply_cost_unitary

__all__ = ["hva"]

if TYPE_CHECKING:
    from uniqc.circuit_builder.parameter import Parameters


[docs] def hva( hamiltonian_groups: list[list[tuple[str, float]]], p: int = 1, qubits: list[int] | None = None, params: Parameters | np.ndarray | None = None, hf_state: list[int] | None = None, ) -> Circuit: """Build a Hamiltonian Variational Ansatz (HVA) circuit. The HVA alternates between applying exponentials of commuting Hamiltonian groups. Each group is applied with an independent variational parameter. Args: hamiltonian_groups: List of commuting Hamiltonian groups. Each group is a list of ``(pauli_string, coefficient)`` tuples. Groups should contain mutually commuting operators. p: Number of ansatz layers (repetitions of the full group cycle). qubits: Qubit indices. ``None`` → auto-detect from hamiltonian. params: Variational parameters. Length must equal ``len(hamiltonian_groups) * p``. ``None`` → random initialization. hf_state: Qubit indices to initialize in |1> (Hartree-Fock state). ``None`` → all qubits start in |0>. Returns: A :class:`Circuit` object. Raises: ValueError: Parameter count mismatch or empty groups. Example: >>> # Hubbard model example with two groups: hopping and interaction >>> hopping = [("X0X1", 1.0), ("Y0Y1", 1.0)] >>> interaction = [("Z0Z1", 0.5)] >>> groups = [hopping, interaction] >>> c = hva(groups, p=2) """ if not hamiltonian_groups: raise ValueError( format_enriched_message( "hamiltonian_groups must contain at least one group", "circuit_validation", ) ) # Determine qubit set from all groups from ._pauli_unitary import _parse_pauli_string all_qubits = set() for group in hamiltonian_groups: for pauli_str, _ in group: for _, q in _parse_pauli_string(pauli_str): all_qubits.add(q) n_qubits = max(all_qubits) + 1 if all_qubits else 0 if qubits is None: qubits = list(range(n_qubits)) else: qubits = list(qubits) n_groups = len(hamiltonian_groups) n_params = n_groups * p # Import Parameters for auto-generation from uniqc.circuit_builder.parameter import Parameters as ParamClass if params is None: # Auto-generate named Parameters params = ParamClass("theta_hva", size=n_params) rng = np.random.default_rng(0) params.bind(list(rng.uniform(0, np.pi, size=n_params))) elif isinstance(params, ParamClass): if len(params) != n_params: raise ValueError( format_enriched_message( f"Expected {n_params} parameters (n_groups={n_groups} × p={p}), got {len(params)}", "circuit_validation", ) ) if not params[0].is_bound: rng = np.random.default_rng(0) params.bind(list(rng.uniform(0, np.pi, size=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 (n_groups={n_groups} × p={p}), got {len(params_arr)}", "circuit_validation", ) ) params = ParamClass("theta_hva", size=n_params) params.bind(list(params_arr.flatten())) circuit = Circuit() # Apply Hartree-Fock initial state if specified if hf_state is not None: for q in hf_state: circuit.x(q) # HVA layers for layer in range(p): for g, group in enumerate(hamiltonian_groups): if not group: continue theta = params[layer * n_groups + g].evaluate() _apply_cost_unitary(circuit, group, theta) # Attach parameters to circuit for traceability circuit._params = params return circuit