Source code for qarp.blocks._primitives.ucc_block

from collections import abc
from typing import List, Optional, Self, cast

from sympy import Symbol

from qarp.operators import FermionOperator, GroupingStrategy, JordanWigner, Mapping, QubitOperator
from qarp.operators.onv import Onv
from qarp.operators.ucc import ucc_doubles, ucc_singles

from .._block import CompositeBlockBase, _as_symbol
from .trotter_block import TrotterAnsatzBlock


[docs] class UCCBlock(CompositeBlockBase): def __init__( self, occupation_number_vector: Onv, singles: bool = True, doubles: bool = True, paired_doubles: bool = False, generalised: bool = False, spin_conserving: bool = True, mapping: Optional[Mapping] = None, order: int = 1, time: float = 1.0, steps: int = 1, grouping: Optional[GroupingStrategy] = None, symbol_postfix: str = "", target_qubits: Optional[List[int]] = None, name: str = "UCC", ): """Unitary Coupled-Cluster (UCC) ansatz block for quantum chemistry simulations. Pattern B (composite) — wraps a single ``TrotterAnsatzBlock`` child that Trotterises the UCC excitation generators. Args: occupation_number_vector: Reference occupation-number vector (abab list) defining the initial state. singles: If True, includes single excitation operators. doubles: If True, includes double excitation operators. paired_doubles: If True, restricts doubles to paired (same-spatial-orbital) excitations. generalised: If True, uses generalized (non-canonical) excitation operators. spin_conserving: If True, only includes spin-conserving excitations. mapping: Fermion-to-qubit mapping (default: Jordan-Wigner). order: Trotter decomposition order (1 or any even integer ≥ 2 — forwarded to :class:`TrotterAnsatzBlock`'s Suzuki recursion). time: Evolution time parameter (typically 1.0 for ansatz). steps: Number of Trotter steps for time evolution. grouping: Term-partitioning strategy forwarded to the child :class:`TrotterAnsatzBlock`; ``None`` → ``FullyCommuting()``. Each group costs one shared basis-change Clifford, so grouping reduces depth; ``NoGrouping()`` gives the termwise circuit. symbol_postfix: String appended to parameter symbol names for disambiguation. target_qubits: Specific qubits to apply the block to. If None, uses all qubits. name: Optional custom name for the block. """ if mapping is None: mapping = JordanWigner() n_qubits = len(mapping.encode_state(occupation_number_vector)) super().__init__( n_qubits, target_qubits=target_qubits, name=name, ) self.occupation_number_vector = occupation_number_vector self.singles = singles self.doubles = doubles self.paired_doubles = paired_doubles self.generalised = generalised self.spin_conserving = spin_conserving self.mapping = mapping self.order = order self.time = time self.steps = steps self.grouping = grouping # Pairing list: index i belongs to qubit_exponents[i]. Deliberately # generation-ordered (singles then doubles) — the public `symbols` # registry is the canonically sorted view of the same set. self._pairing_symbols: List[Symbol] = [] operators: List[FermionOperator] = [] if singles: sops, ssymbs = ucc_singles( self.occupation_number_vector, spin_conserving=self.spin_conserving, generalised=self.generalised, ) operators += sops self._pairing_symbols += ssymbs if doubles: dops, dsymbs = ucc_doubles( self.occupation_number_vector, spin_conserving=self.spin_conserving, generalised=self.generalised, paired=self.paired_doubles, ) operators += dops self._pairing_symbols += dsymbs if symbol_postfix != "": self._pairing_symbols = [Symbol(s.name + symbol_postfix) for s in self._pairing_symbols] # A list in, a list out. self.qubit_exponents = cast(List[QubitOperator], self.mapping.encode_operator(operators)) self.symbols = self._pairing_symbols @property def symbol_qop_pairs(self): """Pairing surface: (symbol, encoded generator), generation-ordered. Mirrors ``TrotterAnsatzBlock.symbol_qop_pairs``. Use this — never the sorted ``symbols`` registry — when positional symbol↔operator correspondence matters. """ return list(zip(self._pairing_symbols, self.qubit_exponents, strict=True))
[docs] def replace_symbols(self, new_parameters: abc.Mapping[Symbol | str, Symbol | str]) -> Self: new_object = super().replace_symbols(new_parameters) renames = {_as_symbol(k): _as_symbol(v) for k, v in new_parameters.items()} if hasattr(new_object, "_pairing_symbols"): new_object._pairing_symbols = [renames.get(s, s) for s in new_object._pairing_symbols] return new_object
[docs] def build_vanilla(self) -> None: if self.symbols is None: raise RuntimeError("Cannot build UCCBlock: no symbols defined") tab = TrotterAnsatzBlock( n_qubits=self.n_qubits, qubit_exponents=self.qubit_exponents, symbols=self._pairing_symbols, steps=self.steps, time=self.time, order=self.order, grouping=self.grouping, imaginary=True, ) self.add_wired_child(tab)