Source code for qdk_chemistry.data.unitary_representation.containers.block_encoding

"""QDK/Chemistry block encoding container module."""

# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

import os
import tempfile
from abc import abstractmethod
from dataclasses import asdict, dataclass
from typing import TYPE_CHECKING, Any

import h5py
import numpy as np

from qdk_chemistry.data._hashing import _hash_array, _hash_int, _hash_str, _hash_uint

from .base import UnitaryContainer

if TYPE_CHECKING:
    from qdk_chemistry.data import Wavefunction

__all__ = ["BlockEncodingContainer", "ControlledOperation", "LCUContainer", "Select"]


[docs] class BlockEncodingContainer(UnitaryContainer): """Abstract base class for block encoding containers.""" # Class attribute for filename validation _data_type_name = "block_encoding_container" # Serialization version for this class _serialization_version = "0.2.0"
[docs] @property @abstractmethod def power(self) -> int: """Number of times to apply the block encoding."""
[docs] @dataclass(frozen=True) class ControlledOperation: """A single controlled unitary operation in the SELECT oracle.""" ctrl_state: int """Integer encoding of the control state that activates this operation.""" operation: str """Operation descriptor (e.g., a Pauli string like ``"XZI"``)."""
[docs] def to_hdf5(self, group: h5py.Group) -> None: """Save the ControlledOperation to an HDF5 group. Args: group: HDF5 group to write attributes to. """ group.attrs["ctrl_state"] = self.ctrl_state group.attrs["operation"] = self.operation
[docs] @classmethod def from_hdf5(cls, group: h5py.Group) -> "ControlledOperation": """Load a ControlledOperation from an HDF5 group. Args: group: HDF5 group to read attributes from. Returns: ControlledOperation: The deserialized instance. """ return cls( ctrl_state=int(group.attrs["ctrl_state"]), operation=str(group.attrs["operation"]), )
[docs] @dataclass(frozen=True) class Select: """Class representing the SELECT oracle for block encoding.""" controlled_operations: list[ControlledOperation] """List of controlled operations.""" phases: np.ndarray """Array of +1/-1 phase corrections (uncontrolled global phase per term).""" num_prepare_ancillas: int """Number of control qubits.""" num_target_qubits: int """Number of target (system) qubits.""" prepare_qubits: list[int] """List of qubit indices for the prepare register.""" target_qubits: list[int] """List of qubit indices for the target (system) register."""
[docs] def to_hdf5(self, group: h5py.Group) -> None: """Save the Select oracle to an HDF5 group. Args: group: HDF5 group to write phases, attributes, and controlled operations to. """ group.create_dataset("phases", data=self.phases) group.attrs["num_prepare_ancillas"] = self.num_prepare_ancillas group.attrs["num_target_qubits"] = self.num_target_qubits group.attrs["prepare_qubits"] = self.prepare_qubits group.attrs["target_qubits"] = self.target_qubits ops_group = group.create_group("controlled_operations") for i, op in enumerate(self.controlled_operations): op.to_hdf5(ops_group.create_group(f"op_{i}"))
[docs] @classmethod def from_hdf5(cls, group: h5py.Group) -> "Select": """Load a Select oracle from an HDF5 group. Args: group: HDF5 group to read phases, attributes, and controlled operations from. Returns: Select: The deserialized instance. """ ops_group = group["controlled_operations"] controlled_ops = [ ControlledOperation.from_hdf5(ops_group[key]) for key in sorted(ops_group.keys(), key=lambda k: int(k.split("_")[1])) ] return cls( controlled_operations=controlled_ops, phases=np.array(group["phases"]), num_prepare_ancillas=int(group.attrs["num_prepare_ancillas"]), num_target_qubits=int(group.attrs["num_target_qubits"]), prepare_qubits=list(group.attrs["prepare_qubits"]), target_qubits=list(group.attrs["target_qubits"]), )
[docs] class LCUContainer(BlockEncodingContainer): r"""Container for a Linear Combination of Unitaries (LCU) decomposition. Stores the pre-computed PREPARE and SELECT sub-objects that define a block-encoding circuit. This container is agnostic to the specific method used to compute these objects — that logic lives in the builder. .. math:: W = \text{PREPARE}^\dagger \cdot \text{SELECT} \cdot \text{PREPARE} """ # Class attribute for filename validation _data_type_name = "lcu_container" # Serialization version for this class _serialization_version = "0.2.0"
[docs] def __init__( self, prepare: "Wavefunction", select: Select, power: int = 1, ) -> None: r"""Initialize an LCUContainer. Args: prepare: The prepare wavefunction encoding coefficients for the block encoded Hamiltonian. select: The select oracle for controlled operations. power: Number of times to apply the block encoding (for B[H]^power). """ self._power = power self.prepare = prepare self.select = select super().__init__()
[docs] @property def power(self) -> int: """Number of times to apply the block encoding. Returns: int: The power value. """ return self._power
[docs] @property def num_prepare_ancillas(self) -> int: """Number of qubits in the prepare ancillary register. Derived from the orbital basis size of the prepare wavefunction. Returns: int: The ancilla qubit count. """ return self.prepare.get_orbitals().num_modes()
[docs] @property def num_qubits(self) -> int: """Total number of qubits (system + ancilla). Returns: int: The combined qubit count. """ return self.select.num_target_qubits + self.num_prepare_ancillas
[docs] @property def type(self) -> str: """Get the type of the unitary container. Returns: str: The type string ``"lcu"``. """ return "lcu"
[docs] def to_json(self) -> dict[str, Any]: """Save the LCUContainer to a JSON-serializable dictionary. Returns: dict[str, Any]: Dictionary representation including container type, power, prepare, and select fields. """ data: dict[str, Any] = { "container_type": self.type, "power": self.power, "prepare": self.prepare.to_json(), "select": asdict(self.select) | {"phases": self.select.phases.tolist()}, } return self._add_json_version(data)
[docs] def to_hdf5(self, group: h5py.Group) -> None: """Save the LCUContainer to an HDF5 group. Args: group: HDF5 group to write container data to. """ self._add_hdf5_version(group) group.attrs["container_type"] = self.type group.attrs["power"] = self.power _wavefunction_to_hdf5(self.prepare, group.create_group("prepare")) self.select.to_hdf5(group.create_group("select"))
[docs] @classmethod def from_json(cls, json_data: dict[str, Any]) -> "LCUContainer": """Create an LCUContainer from a JSON dictionary. Args: json_data: Dictionary containing the serialized LCU data. Returns: LCUContainer: The deserialized instance. """ from qdk_chemistry.data import Wavefunction # noqa: PLC0415 cls._validate_json_version(cls._serialization_version, json_data) prepare = Wavefunction.from_json(json_data["prepare"]) sel_data = json_data["select"] controlled_ops = [ ControlledOperation( ctrl_state=op["ctrl_state"], operation=op["operation"], ) for op in sel_data["controlled_operations"] ] select = Select( controlled_operations=controlled_ops, phases=np.array(sel_data["phases"], dtype=int), num_prepare_ancillas=sel_data["num_prepare_ancillas"], num_target_qubits=sel_data["num_target_qubits"], prepare_qubits=sel_data["prepare_qubits"], target_qubits=sel_data["target_qubits"], ) return cls( power=json_data.get("power", 1), prepare=prepare, select=select, )
[docs] @classmethod def from_hdf5(cls, group: h5py.Group) -> "LCUContainer": """Load an LCUContainer from an HDF5 group. Args: group: HDF5 group to read container data from. Returns: LCUContainer: The deserialized instance. """ prepare = _wavefunction_from_hdf5(group["prepare"]) select = Select.from_hdf5(group["select"]) power = int(group.attrs["power"]) return cls( power=power, prepare=prepare, select=select, )
[docs] def get_summary(self) -> str: """Get a human-readable summary of the LCU container. Returns: str: Multi-line summary describing power, prepare, and select settings. """ return ( f"LCU Container:\n" f" Power: {self.power}\n" f" Prepare: {self.num_prepare_ancillas} qubits, statevector shape {len(self.prepare.get_coefficients())}\n" f" Select: {self.num_prepare_ancillas} control qubits, {self.select.num_target_qubits} target qubits," f" {len(self.select.controlled_operations)} controlled operations" )
def _hash_update(self, h) -> None: """Feed identifying data into the hasher.""" _hash_str(h, "lcu_container") _hash_int(h, self._power) # Hash prepare wavefunction via its own content_hash _hash_str(h, self.prepare.content_hash(truncate_chars=0)) # Hash select oracle _hash_uint(h, len(self.select.controlled_operations)) for op in self.select.controlled_operations: _hash_int(h, op.ctrl_state) _hash_str(h, op.operation) _hash_array(h, self.select.phases) _hash_int(h, self.select.num_prepare_ancillas) _hash_int(h, self.select.num_target_qubits)
[docs] def eigenvalue_from_phase(self, phase_fraction: float) -> float: """Not applicable for a raw block encoding. A plain block encoding does not define an eigenvalue-phase relationship on its own. Use :class:`~qdk_chemistry.data.unitary_representation.containers.quantum_walk.LCUWalkContainer` for QPE with qubitization. Raises: NotImplementedError: Always. """ raise NotImplementedError( "LCUContainer does not define an eigenvalue-phase relationship. Wrap it in an LCUWalkContainer to use QPE." )
def _wavefunction_to_hdf5(wavefunction: "Wavefunction", group: h5py.Group) -> None: """Serialize a Wavefunction into an h5py Group via a temp-file bridge. The C++ Wavefunction only exposes file-based HDF5 I/O. This helper writes to a temporary file and copies the contents into *group*. Args: wavefunction: The wavefunction to serialize. group: Target HDF5 group to populate. """ fd, tmp_path = tempfile.mkstemp(suffix=".wavefunction.h5") os.close(fd) try: wavefunction.to_hdf5_file(tmp_path) with h5py.File(tmp_path, "r") as src: for key in src: src.copy(key, group) for attr_name, attr_val in src.attrs.items(): group.attrs[attr_name] = attr_val finally: os.unlink(tmp_path) def _wavefunction_from_hdf5(group: h5py.Group) -> "Wavefunction": """Deserialize a Wavefunction from an h5py Group via a temp-file bridge. The C++ Wavefunction only exposes file-based HDF5 I/O. This helper copies *group* into a temporary file and loads from it. Args: group: Source HDF5 group containing serialized wavefunction data. Returns: Wavefunction: The deserialized wavefunction. """ from qdk_chemistry.data import Wavefunction # noqa: PLC0415 fd, tmp_path = tempfile.mkstemp(suffix=".wavefunction.h5") os.close(fd) try: with h5py.File(tmp_path, "w") as dst: for key in group: group.copy(key, dst) for attr_name, attr_val in group.attrs.items(): dst.attrs[attr_name] = attr_val return Wavefunction.from_hdf5_file(tmp_path) finally: os.unlink(tmp_path)