Source code for qarp.blocks._state_preparation.multi_onv_state_block

from typing import Dict, List, Optional, Tuple

import numpy as np

from ...operators import JordanWigner, Mapping
from .. import SimpleBlock
from .._prepares_known_state import prepares_known_state
from .sparse_state_block import _prepare_sparse_circuit


[docs] @prepares_known_state class MultiONVStateBlock(SimpleBlock): def __init__( self, onv_coefficients: Dict[Tuple[int, ...], complex], mapping: Optional[Mapping] = None, target_qubits: Optional[List[int]] = None, name: str = "MultiONV", ): """Prepare a superposition of occupation-number vectors under a fermion-to-qubit mapping — a CI-vector reference (CASCI/CISD) rather than the single determinant ``MappedONVStateBlock`` loads. Each ONV is mapped independently through ``mapping.encode_state`` (a fixed linear bit transform for JW/BK/Parity — ``qarp/operators/ _mappings.py`` — carrying no relative phase of its own, the same convention ``MappedONVStateBlock`` already relies on for a single determinant), and the resulting mapped bitstring/coefficient pairs are handed to the same ancilla-free sparse construction ``SparseStateBlock`` uses, so this block stays ancilla-free and scales with the number of determinants rather than ``2**n_qubits``. Determinant sign convention: the coefficient of ONV ``b`` multiplies ``a†_{p₁} a†_{p₂} … a†_{pₖ} |vac⟩`` with ``p₁ < p₂ < … < pₖ`` the occupied spin-orbital indices in abab order (``2·spatial + 0`` for α, ``+1`` for β). This is qarp's Jordan–Wigner convention (Z-string on the lower indices, as in openfermion), so it is exactly what products of ``FermionOperator`` creation operators give. pyscf CI vectors use an αα…ββ… string order that needs a per-determinant sign to reach it — convert them with ``qarp.operators.pyscf.onv_coefficients_from_civec``. Under Bravyi–Kitaev pass ``BravyiKitaev(n_qubits=len(onv))`` so the operator transform and ``encode_state`` agree on the register width (``encode_state`` sizes its β matrix by ``len(onv)``). Args: onv_coefficients: dict mapping occupation-number vectors — as tuples of 0/1, abab order (``qarp/operators/onv.py``) — to complex CI coefficients. All vectors must have equal length. Normalized internally. mapping: fermion-to-qubit mapping (default ``JordanWigner()``). target_qubits, name: standard Block kwargs. """ if not onv_coefficients: raise ValueError("onv_coefficients dictionary cannot be empty.") lengths = {len(onv) for onv in onv_coefficients} if len(lengths) != 1: raise ValueError("All occupation-number vectors must have the same length.") n_qubits = lengths.pop() for onv in onv_coefficients: if not all(bit in (0, 1) for bit in onv): raise ValueError(f"onv {onv} contains invalid values; only 0 and 1 are allowed.") if mapping is None: mapping = JordanWigner() self.mapping = mapping self.onv_coefficients: Dict[Tuple[int, ...], complex] = dict(onv_coefficients) mapped: Dict[Tuple[int, ...], complex] = {} for onv, coeff in self.onv_coefficients.items(): basis = tuple(mapping.encode_state(list(onv))) if basis in mapped: raise ValueError( f"ONV {onv} maps to the same basis state {basis} as another " f"entry: the two are distinct determinants, but " f"{type(mapping).__name__}.encode_state is not injective on them." ) mapped[basis] = complex(coeff) norm = sum(abs(amp) ** 2 for amp in mapped.values()) ** 0.5 if norm < 1e-15: raise ValueError("Amplitudes cannot all be zero.") self._mapped_amplitudes: Dict[Tuple[int, ...], complex] = { basis: amp / norm for basis, amp in mapped.items() } super().__init__(n_qubits, target_qubits=target_qubits, name=name)
[docs] def build_vanilla(self) -> None: _prepare_sparse_circuit(self, self._mapped_amplitudes)
[docs] def target_statevector(self) -> np.ndarray: """The normalized, mapping-encoded CI coefficients, LSB-first (§1).""" psi = np.zeros(2**self.n_qubits, dtype=complex) for basis, amp in self._mapped_amplitudes.items(): idx = sum(bit << i for i, bit in enumerate(basis)) psi[idx] = amp return psi