Source code for qolumbina.utils.state_initialization

"""Input-state preparation circuit builders.

This module contains two independent preparation helpers:

- :class:`PureStateInitialization` builds circuits for pure input states, either
  from a single-qubit gate list or from a state-vector amplitude list.
- :class:`MixedStateInitialization` builds purification circuits for separable
  computational-basis mixed inputs by appending control/environment qubits.

Both helpers expose the generated preparation circuit through :attr:`qc`, so the
caller can compose a tested circuit manually without running the full execution
framework.
"""

from enum import Enum, auto
import math
from typing import Literal, Optional, cast
import warnings

from qiskit import QuantumCircuit
from qiskit.quantum_info import Statevector


[docs] class InitStateType(Enum): """Supported encodings for pure-state initialization input.""" GATE_LIST = auto() STATE_VECTOR = auto()
[docs] def infer_init_state_type(init_state: list) -> InitStateType: """Infer how a pure initial state is encoded. Supported types: - ``list[str]``: single-qubit gate list, such as ``["x", "i"]``. - ``list[int | float | complex]``: state-vector amplitudes. Args: init_state: User-provided pure-state initialization data. Returns: The inferred encoding type. Raises: TypeError: If ``init_state`` is not a list or contains unsupported element types. ValueError: If ``init_state`` is an empty list. """ if not isinstance(init_state, list): raise TypeError("init_state must be a list") if len(init_state) == 0: raise ValueError("init_state cannot be empty") if all(isinstance(x, str) for x in init_state): return InitStateType.GATE_LIST if all(isinstance(x, (int, float, complex)) for x in init_state): return InitStateType.STATE_VECTOR raise TypeError( "init_state falls out of the candidate types" )
[docs] class PureStateInitialization: r"""Build a pure-state preparation circuit. ``PureStateInitialization`` creates a ``QuantumCircuit`` containing only input-state preparation operations. Callers can execute it directly or compose another circuit onto it. The resulting circuit is available as :attr:`qc`. ``init_state`` supports two encodings: - Gate-list encoding, for example ``["x", "i", "h"]``. Each string is a non-parameterized single-qubit gate applied to the corresponding target. - State-vector encoding, for example ``[0, 0, 0, 1]`` for :math:`\lvert 11\rangle`. """ def __init__( self, init_state: list[str] | list[float] | list[complex], init_qubits: Literal["all"] | list[int] = "all", predefined_qubit_count: int | None = None, predefined_clbit_count: int | None = None ) -> None: r"""Create a pure-state preparation circuit. Args: init_state: Pure-state description. A ``list[str]`` is treated as a gate list; a numeric list is treated as a state vector. init_qubits: Target qubits for the preparation. Use ``"all"`` to infer the circuit width from ``init_state``. Use ``list[int]`` to prepare only selected qubits in a larger circuit. predefined_qubit_count: Total qubit count for the generated circuit. Required when ``init_qubits`` is a list. Ignored with a warning if ``init_qubits="all"`` and the count disagrees with ``init_state``. predefined_clbit_count: Number of classical bits in the generated circuit. If ``None``, it defaults to the total qubit count. Raises: ValueError: If gate-list length, state-vector dimension, or qubit count constraints are violated. TypeError: If ``init_state`` or ``init_qubits`` has an unsupported type. """ self._input_state_data = init_state self._init_qubits = init_qubits self._input_data_type = infer_init_state_type(init_state) self._total_qubits = predefined_qubit_count self._total_clbits = predefined_clbit_count self._state_vector: Optional[list[float] | list[complex]] = None self._gate_list: Optional[list[str]] = None self.qc: QuantumCircuit self._run() SINGLE_QUBIT_GATE_MAP = { "i": lambda qc, q: qc.id(q), "x": lambda qc, q: qc.x(q), "y": lambda qc, q: qc.y(q), "z": lambda qc, q: qc.z(q), "h": lambda qc, q: qc.h(q), "s": lambda qc, q: qc.s(q), "sdg": lambda qc, q: qc.sdg(q), "t": lambda qc, q: qc.t(q), "tdg": lambda qc, q: qc.tdg(q), } APPLY = { InitStateType.GATE_LIST: lambda: "_prepare_via_gate_list", InitStateType.STATE_VECTOR: lambda: "_prepare_via_state_vector", } def _apply_single_qubit_gate(self, gate: str, qubit: int) -> None: """Apply one supported single-qubit gate to the preparation circuit.""" gate = gate.lower() if gate not in self.SINGLE_QUBIT_GATE_MAP: raise ValueError(f"undefined single-qubit gate: {gate}") gate_op = self.SINGLE_QUBIT_GATE_MAP[gate] if gate_op is not None: gate_op(self.qc, qubit) def _prepare_via_gate_list(self) -> None: """Apply a per-target single-qubit gate list.""" self._gate_list = cast(list[str], self._input_state_data) if self._init_qubits == "all": for qubit, gate in enumerate(self._gate_list): self._apply_single_qubit_gate(gate, qubit) elif isinstance(self._init_qubits, list): if len(self._gate_list) != len(self._init_qubits): raise ValueError( f"the length of gate list ({len(self._gate_list)}) " f"does not match that of specified qubits ({len(self._init_qubits)})" ) for qubit, gate in zip(self._init_qubits, self._gate_list): self._apply_single_qubit_gate(gate, qubit) def _prepare_via_state_vector(self) -> None: """Initialize the selected target qubits from a state-vector list.""" self._state_vector = cast(list[float], self._input_state_data) state = cast(list[float], self._state_vector) target_qubits = self.qc.qubits if self._init_qubits == "all" else self._init_qubits if len(self._state_vector) != 2 ** len(target_qubits): raise ValueError( f"the dimension of state vector ({len(self._state_vector)}) " f"does not match that of Hilbert space ({2 ** len(target_qubits)})" ) self.qc.initialize(Statevector(state), target_qubits) def _run(self) -> None: """Resolve dimensions, allocate the circuit, and apply preparation.""" init_state = self._input_state_data infer_type = self._input_data_type init_qubits = self._init_qubits if init_qubits == "all": if infer_type == InitStateType.GATE_LIST: num_qubits = len(init_state) elif infer_type == InitStateType.STATE_VECTOR: num_qubits = int(math.log2(len(init_state))) if self._total_qubits is not None and self._total_qubits != num_qubits: warnings.warn( f"the total qubit count ({self._total_qubits}) is inconsistent with " f"the inferred qubit count ({num_qubits}) from init_state; " f"the latter one is used." ) elif isinstance(init_qubits, list): if self._total_qubits is None: raise ValueError( f"predefined_qubit_count must be provided when init_qubits is not 'all'" ) num_qubits = self._total_qubits else: raise ValueError( f"unsupported init_qubits specification: {init_qubits}" ) if self._total_clbits is not None: num_clbits = self._total_clbits else: num_clbits = num_qubits self.qc = QuantumCircuit(num_qubits, num_clbits) getattr(self, self.APPLY[infer_type]())()
[docs] class MixedStateInitialization: r"""Build a separable mixed-state preparation circuit. ``MixedStateInitialization`` creates a purification circuit for separable computational-basis mixed inputs. It appends one control/environment qubit per target system qubit. For one target qubit, the reduced state after tracing out the control qubit is :math:`\rho = \cos^2(\theta / 2)\lvert 0\rangle\langle 0\rvert + \sin^2(\theta / 2)\lvert 1\rangle\langle 1\rvert`, where ``theta`` is the matching entry in ``mixed_state_angles``. The generated circuit is available as :attr:`qc`. System qubits keep indices ``0..system_qubit_count-1``. Control qubits are appended after the maximum system-qubit index, so they occupy ``system_qubit_count..num_total_qubits-1``. Example: Build only the mixed-state preparation circuit and then compose a tested circuit manually: .. code-block:: python init = MixedStateInitialization( system_qubit_count=3, target_qubits=[0, 2], mixed_state_angles=[theta_0, theta_2], ) tested_qc = QuantumCircuit(3) tested_qc.cx(0, 1) qc = init.qc.copy() qc.compose(tested_qc, qubits=range(init.num_system_qubits), inplace=True) Args: system_qubit_count: Number of system qubits in the circuit under test. System qubits keep their original indices ``0..n-1``. target_qubits: System qubits whose reduced input states are prepared as mixed states. If ``"all"`` and ``mixed_state_angles`` is ``None``, all system qubits are targeted. If ``"all"`` and angles are provided, the targets default to low-index system qubits ``[0, 1, ..., m - 1]`` where ``m`` is the number of angles. mixed_state_angles: Optional Ry angles for the appended control qubits. If ``None``, each target uses ``pi / 2``. predefined_clbit_count: Number of classical bits to allocate in the preparation circuit. If ``None``, no classical bits are allocated. Attributes: qc: The generated preparation circuit. target_qubits: Resolved target system-qubit indices. control_qubits: Appended control/environment qubit indices. control_angles: Resolved ``Ry`` angles, one per control qubit. num_system_qubits: Number of system qubits. num_control_qubits: Number of appended control qubits. num_total_qubits: Total number of qubits in :attr:`qc`. """ def __init__( self, system_qubit_count: int, target_qubits: Literal["all"] | list[int] = "all", mixed_state_angles: list[float] | None = None, predefined_clbit_count: int | None = None ) -> None: self.num_system_qubits = system_qubit_count self._target_qubits_data = target_qubits self._mixed_state_angles = mixed_state_angles self.num_total_clbits = 0 if predefined_clbit_count is None else predefined_clbit_count self.control_angles: list[float] = [] self.target_qubits: list[int] = [] self.control_qubits: list[int] = [] self.num_control_qubits = 0 self.num_total_qubits = 0 self.qc: QuantumCircuit self._run() def _resolve_configuration(self) -> tuple[list[float], list[int]]: """Resolve user defaults into angle and target-qubit lists.""" if not isinstance(self.num_system_qubits, int) or self.num_system_qubits < 1: raise ValueError("system_qubit_count must be a positive integer") target_qubits_data = self._target_qubits_data angles_data = self._mixed_state_angles if target_qubits_data is None: raise ValueError("target_qubits cannot be None") if target_qubits_data != "all" and not isinstance(target_qubits_data, list): raise TypeError("target_qubits must be 'all' or list[int]") if angles_data is not None: if not isinstance(angles_data, list) or len(angles_data) == 0: raise ValueError( "mixed_state_angles must be a non-empty list when provided" ) if not all(isinstance(theta, (int, float)) for theta in angles_data): raise TypeError("mixed_state_angles must contain real numbers") angles = [float(theta) for theta in angles_data] else: if target_qubits_data == "all": angles = [math.pi / 2] * self.num_system_qubits else: angles = [math.pi / 2] * len(target_qubits_data) if target_qubits_data == "all": if angles_data is None: target_qubits = list(range(self.num_system_qubits)) else: target_qubits = list(range(len(angles))) else: if len(target_qubits_data) == 0: raise ValueError("target_qubits must be a non-empty list or 'all'") if not all(isinstance(qubit, int) for qubit in target_qubits_data): raise TypeError("target_qubits must contain integers") target_qubits = list(target_qubits_data) if len(angles) != len(target_qubits): raise ValueError( "mixed_state_angles and target_qubits must have the same length, " f"but got {len(angles)} and {len(target_qubits)}" ) if len(set(target_qubits)) != len(target_qubits): raise ValueError("target_qubits cannot contain duplicates") if any( qubit < 0 or qubit >= self.num_system_qubits for qubit in target_qubits ): raise ValueError( "target_qubits must be valid system-qubit indices " f"in [0, {self.num_system_qubits - 1}]" ) return angles, target_qubits def _run(self) -> None: """Allocate the purification circuit and add control-target gates.""" self.control_angles, self.target_qubits = self._resolve_configuration() self.num_control_qubits = len(self.control_angles) self.control_qubits = list( range( self.num_system_qubits, self.num_system_qubits + self.num_control_qubits ) ) self.num_total_qubits = self.num_system_qubits + self.num_control_qubits self.qc = QuantumCircuit(self.num_total_qubits, self.num_total_clbits) for control_qubit, theta in zip(self.control_qubits, self.control_angles): self.qc.ry(theta, control_qubit) for control_qubit, target_qubit in zip(self.control_qubits, self.target_qubits): self.qc.cx(control_qubit, target_qubit)