Source code for qarp.blocks._primitives.layer_block

from typing import List, Optional, Union

from sympy import Symbol

import qarpx as qx

from .._block import SimpleBlock, as_param

# GateType → (Block method name, takes_params)
# Only gates covered by the Block builder API are listed.
_GATE_BUILDER = {
    qx.GateType.H: ("h", False),
    qx.GateType.X: ("x", False),
    qx.GateType.Y: ("y", False),
    qx.GateType.Z: ("z", False),
    qx.GateType.S: ("s", False),
    qx.GateType.Sdg: ("sdg", False),
    qx.GateType.T: ("t", False),
    qx.GateType.Tdg: ("tdg", False),
    qx.GateType.Rx: ("rx", True),
    qx.GateType.Ry: ("ry", True),
    qx.GateType.Rz: ("rz", True),
    qx.GateType.P: ("p", True),
    qx.GateType.U: ("u", True),
    qx.GateType.CX: ("cx", False),
    qx.GateType.CY: ("cy", False),
    qx.GateType.CZ: ("cz", False),
    qx.GateType.SWAP: ("swap", False),
    qx.GateType.CCX: ("ccx", False),
    qx.GateType.RZZ: ("rzz", True),
}


_to_param = as_param  # the single §13 coercion; linear expressions now accepted here too


[docs] class LayerBlock(SimpleBlock): def __init__( self, gate_type: qx.GateType, n_qubits: int, qubit_indices: Optional[List[int]] = None, overlapping: int = 0, periodic_boundary: bool = False, parameters: Optional[List[Union[float, Symbol]]] = None, target_qubits: Optional[List[int]] = None, name: str = "LayerBlock", ): """Constructs a layer of gates with a specified GateType. Args: gate_type: The qarpx GateType for the gates in this layer. n_qubits: The number of qubits to include. qubit_indices: Optional list of specific qubit indices to apply gates to. overlapping: The number of qubits that overlap between consecutive gates (default 0). periodic_boundary: Whether to use periodic boundary conditions (default False). parameters: Parameters for parametric gates. Either one set of params reused for all gates, or a flattened list of params for each gate individually. Angles are in radians. target_qubits: The target qubits this block acts on when added to a larger block. name: The name of the block. """ super().__init__(n_qubits, target_qubits, name=name) self.gate_type = gate_type if qubit_indices is None: qubit_indices = list(range(self.n_qubits)) self.qubit_indices = qubit_indices self.overlapping = overlapping self.periodic_boundary = periodic_boundary self.parameters = parameters if parameters is not None else [] self.gate_arity = qx.gate_num_qubits(gate_type) self.params_per_gate = qx.gate_num_params(gate_type) self._validate_inputs() def _validate_inputs(self) -> None: if not self.qubit_indices: raise ValueError("qubit_indices cannot be empty") if any(q >= self.n_qubits for q in self.qubit_indices): raise ValueError("All qubit indices must be less than n_qubits") if self.periodic_boundary and len(self.qubit_indices) % self.gate_arity > self.overlapping: raise ValueError( "Periodic boundaries are not compatible with the gate arity and the overlapping" ) max_overlap = self.gate_arity - 1 if ( not isinstance(self.overlapping, int) or self.overlapping < 0 or self.overlapping > max_overlap ): raise ValueError( f"overlapping must be an integer between 0 and {max_overlap} " f"for {self.gate_arity}-qubit gates" ) if self.params_per_gate > 0: if not self.parameters: raise ValueError( f"Parameters are required for {self.gate_type}. " f"Expected either {self.params_per_gate} parameter(s) for all gates " f"or {self._count_gates() * self.params_per_gate} parameter(s) total." ) n_gates = self._count_gates() if ( len(self.parameters) != self.params_per_gate and len(self.parameters) != n_gates * self.params_per_gate ): raise ValueError( f"Number of parameters must be either {self.params_per_gate} " f"(same params for all {n_gates} gates) " f"or {n_gates * self.params_per_gate} (individual params per gate), " f"but got {len(self.parameters)}" ) if self.gate_type not in _GATE_BUILDER: raise ValueError( f"Unsupported gate type: {self.gate_type}. " f"Supported gates: {list(_GATE_BUILDER.keys())}" ) def _count_gates(self) -> int: """Count the number of gates that will be applied in the layer.""" indices = self.qubit_indices.copy() gate_count = 0 if self.gate_arity == 1: gate_count = len([q for q in indices if q < self.n_qubits]) elif self.gate_arity == 2: if self.periodic_boundary: if len(indices) % 2 == 1 or self.overlapping == 1: indices.append(indices[0]) step = self.gate_arity - self.overlapping for i in range(0, len(indices) - 1, step): if i + 1 < len(indices): if indices[i] < self.n_qubits and indices[i + 1] < self.n_qubits: gate_count += 1 elif self.gate_arity == 3: if self.periodic_boundary: remainder = len(indices) % 3 if remainder == 1 or self.overlapping == 2: indices.extend([indices[0], indices[1]]) elif remainder == 2 or self.overlapping == 1: indices.append(indices[0]) step = self.gate_arity - self.overlapping for i in range(0, len(indices) - 2, step): if i + 2 < len(indices): if all(q < self.n_qubits for q in indices[i : i + 3]): gate_count += 1 return gate_count def _update_parameters_attr(self, symbol_map) -> None: # ``parameters`` mirrors constructor input; remap it alongside the # C++ command-buffer transform (the base hook is a no-op). if self.parameters: self.parameters = [symbol_map.get(p, p) for p in self.parameters] def _get_gate_params(self, gate_index: int) -> list: """Return the parameters for a specific gate, converted to qx.Param.""" if not self.parameters or self.params_per_gate == 0: return [] if len(self.parameters) == self.params_per_gate: raw = self.parameters else: start = gate_index * self.params_per_gate raw = self.parameters[start : start + self.params_per_gate] return [_to_param(v) for v in raw] def _apply_gate(self, qubits: List[int], params: list) -> None: """Call the correct Block builder method for this layer's gate type.""" method_name, takes_params = _GATE_BUILDER[self.gate_type] method = getattr(self, method_name) if takes_params: method(*qubits, *params) else: method(*qubits)
[docs] def build_vanilla(self) -> None: gate_count = 0 if self.gate_arity == 1: for q in self.qubit_indices: if q < self.n_qubits: self._apply_gate([q], self._get_gate_params(gate_count)) gate_count += 1 elif self.gate_arity == 2: indices = self.qubit_indices.copy() if self.periodic_boundary: if len(indices) % 2 == 1 or self.overlapping == 1: indices.append(indices[0]) step = self.gate_arity - self.overlapping for i in range(0, len(indices) - 1, step): if i + 1 < len(indices): q0, q1 = indices[i], indices[i + 1] if q0 < self.n_qubits and q1 < self.n_qubits: self._apply_gate([q0, q1], self._get_gate_params(gate_count)) gate_count += 1 elif self.gate_arity == 3: indices = self.qubit_indices.copy() if self.periodic_boundary: remainder = len(indices) % 3 if remainder == 1 or self.overlapping == 2: indices.extend([indices[0], indices[1]]) elif remainder == 2 or self.overlapping == 1: indices.append(indices[0]) step = self.gate_arity - self.overlapping for i in range(0, len(indices) - 2, step): if i + 2 < len(indices): q0, q1, q2 = indices[i], indices[i + 1], indices[i + 2] if all(q < self.n_qubits for q in [q0, q1, q2]): self._apply_gate([q0, q1, q2], self._get_gate_params(gate_count)) gate_count += 1
@property def arity(self) -> int: """Return the arity of the gate type.""" return self.gate_arity def __repr__(self) -> str: return ( f"LayerBlock(gate_type={self.gate_type}, n_qubits={self.n_qubits}, " f"arity={self.gate_arity}, periodic_boundary={self.periodic_boundary}, " f"overlapping={self.overlapping})" )