Source code for qarp.blocks._primitives.hea_block

from typing import List, Optional

from sympy import Symbol

from .._block import SimpleBlock, _sorted_symbols


[docs] class HEABlock(SimpleBlock): """Hardware-Efficient Ansatz (HEA) block with configurable entanglement patterns. HEABlock constructs a parameterized quantum circuit by stacking multiple HEA layers, each consisting of single-qubit rotations followed by entangling gates. The architecture supports both linear and brickwork entanglement topologies, with options for real-valued (Ry-only) or complex-valued (Ry-Rz) rotations. This structure provides an expressive ansatz suitable for variational quantum algorithms while maintaining compatibility with near-term quantum hardware constraints. Args: n_qubits: Number of qubits in the circuit. n_layers: Number of HEA layers to stack. real: If True, uses only Ry rotations (real ansatz); if False, includes Rz rotations (complex). linear: If True, uses linear entanglement; if False, uses brickwork entanglement. circular: If True, applies entanglement with periodic boundary conditions. use_cz: If True, uses CZ gates for entanglement; if False, uses CNOT gates. target_qubits: Specific qubits to apply the block to. If None, uses all qubits. name: Optional custom name for the block. """ def __init__( self, n_qubits: int, n_layers: int, real: bool, linear: bool, circular: bool, use_cz: bool, target_qubits: Optional[List[int]] = None, name: Optional[str] = None, ): self.n_layers = n_layers self.real = real self.linear = linear self.circular = circular self.use_cz = use_cz if name is None: name = f"Layered HEA (n={n_layers})" super().__init__( n_qubits=n_qubits, target_qubits=target_qubits, name=name, ) # Populate symbols after super().__init__() — the base sets # self.symbols = None, so assigning before would be clobbered. syms = [] for layer in range(n_layers): for q in range(n_qubits): syms.append(Symbol(f"ry_{layer}_{q}")) if not real: syms.append(Symbol(f"rz_{layer}_{q}")) self.symbols = _sorted_symbols(syms)
[docs] def build_vanilla(self) -> None: # Entangling pairs and gate method are the same for every layer — compute once. if self.linear: pairs = [(q, q + 1) for q in range(self.n_qubits - 1)] else: # brickwork even_pairs = [(2 * q, 2 * q + 1) for q in range(self.n_qubits // 2)] # (n-1)//2 odd pairs, so the last qubit is entangled at odd n too; # leaving it out silently weakens the ansatz to single-qubit # rotations on that wire. odd_pairs = [(2 * q + 1, 2 * q + 2) for q in range((self.n_qubits - 1) // 2)] pairs = even_pairs + odd_pairs # Below 3 qubits the ring edge is already the line edge; appending it # would repeat a pair (an identity layer for symmetric CZ). if self.circular and self.n_qubits > 2: pairs.append((self.n_qubits - 1, 0)) entangle = self.cz if self.use_cz else self.cx for layer in range(self.n_layers): self.ry([(q, Symbol(f"ry_{layer}_{q}")) for q in range(self.n_qubits)]) if not self.real: self.rz([(q, Symbol(f"rz_{layer}_{q}")) for q in range(self.n_qubits)]) entangle(pairs)