Source code for tno.quantum.utils._bit_vector

"""This module contains the ``BitVector`` class and ``BitVectorLike`` type alias."""

from __future__ import annotations

import sys
from collections.abc import Iterable, Iterator, Mapping, Sequence
from copy import deepcopy
from typing import TYPE_CHECKING, Any, SupportsInt, overload

import numpy as np
from numpy.typing import ArrayLike

from tno.quantum.utils.serialization import Serializable
from tno.quantum.utils.validation import check_arraylike, check_binary

if TYPE_CHECKING:
    from typing import Self

    from numpy.typing import DTypeLike, NDArray

    if sys.version_info >= (3, 11):
        from typing import Self
    else:
        from typing_extensions import Self

if sys.version_info >= (3, 10):
    from typing import TypeAlias

    BitVectorLike: TypeAlias = (
        str | Mapping[Any, SupportsInt] | Sequence[SupportsInt] | ArrayLike
    )
else:
    # Creating a type alias in Python 3.9
    from typing import Union

    BitVectorLike = Union[
        str,
        Mapping[Any, SupportsInt],
        Sequence[SupportsInt],
        ArrayLike,
    ]


[docs]class BitVector(Sequence[np.uint8], Serializable): """Class representing a vector of bits. This class can be initialized from multiple datatypes, such as strings, dictionaries, lists, and NumPy arrays. The input data is converted and stored as a Numpy :py:class:`~numpy.ndarray`. Example: >>> from tno.quantum.utils import BitVector >>> >>> BitVector("1001") BitVector(1001) >>> BitVector([1, 0, 0, 1]) BitVector(1001) >>> BitVector((1, 0, 0, 1)) BitVector(1001) """
[docs] def __init__( self, bits: BitVectorLike, ) -> None: """Init :py:class:`BitVector`. Args: bits: Bits used to construct a binary vector. Raises: TypeError: If `bits` is not of a supported type. ValueError: If `bits` contains items that can not be converted to int. """ error_msg = ( "The provided input `bits` must be a str, " "Mapping[Any | SupportsInt], Sequence or 1-dim ArrayLike " f"but was of {type(bits)}." ) if not isinstance(bits, (str, Mapping, Sequence)): try: bits = check_arraylike(bits, "bits", ndim=1) except ValueError as exception: raise TypeError(error_msg) from exception elif isinstance(bits, Mapping): try: bits = [int(bit) for bit in bits.values()] except ValueError as exception: raise TypeError(error_msg) from exception self._bits = np.fromiter( (check_binary(bit, f"{bit}") for bit in bits), dtype=np.uint8, count=len(bits), )
@property def bits(self) -> NDArray[np.uint8]: """Returns bits as :py:class:`~numpy.ndarray`.""" return self._bits
[docs] def __str__(self) -> str: """Returns string representation of binary vector.""" return "".join(map(str, self.bits))
[docs] def __len__(self) -> int: """Returns length of the binary vector.""" return len(self.bits)
[docs] def __iter__(self) -> Iterator[np.uint8]: """Returns iterator for the binary vector.""" return iter(self.bits)
@overload def __getitem__(self, index: int) -> np.uint8: ... @overload def __getitem__(self, index: slice) -> BitVector: ...
[docs] def __getitem__(self, index: int | slice) -> np.uint8 | BitVector: """Get integer or slice object from BitVector object.""" if isinstance(index, int): # Ensure the return type is np.uint8 single_result: np.uint8 = self.bits[index] return single_result return BitVector(self.bits[index])
[docs] def __repr__(self) -> str: """Returns detailed string representation of the binary vector.""" return f"{self.__class__.__name__}({self.__str__()})"
[docs] def __eq__(self, other: Any) -> bool: """Checks if two bitvector instances are equal.""" if isinstance(other, BitVector): return np.array_equal(self.bits, other.bits) return False
[docs] def __array__(self, dtype: DTypeLike = None, *, copy: bool = True) -> NDArray[Any]: """Returns the bitvector as an array.""" return np.array(self.bits, dtype=dtype, copy=copy)
[docs] def __hash__(self) -> int: """Provides unique hash for bitvector.""" return hash((self.__class__.__name__, tuple(self.bits)))
[docs] def to_ising(self) -> NDArray[np.int8]: """Returns the current bitvector as an array in Lenz-Ising format. In particular, every zero bit is mapped to 1 and every one bit is mapped to -1. """ return 1 - 2 * self.bits.astype(np.int8)
[docs] def flip_indices(self, *indices: int, inplace: bool = False) -> BitVector: """Flip the bits at the specified indices. Args: indices: Indices of the bits to flip. Each index must be an integer greater than or equal to 0 and less than the length of the bit vector. Repeated indices will be applied only once. inplace: If ``True``, modify current instance. If ``False``, return a new instance. Returns: A new :py:class:`BitVector` instance with the specified bits flipped. """ indices_list = np.fromiter(indices, dtype=int) if inplace: self.bits[indices_list] = -self.bits[indices_list] + 1 return self bits_copy = deepcopy(self.bits) bits_copy[indices_list] = -bits_copy[indices_list] + 1 return BitVector(bits_copy)
[docs] @classmethod def concatenate_bit_vectors(cls, bit_vectors: Iterable[BitVectorLike]) -> Self: """Concatenate multiple bitvectors into a single :py:class:`BitVector` instance. Args: bit_vectors: A list of objects that can be converted to :py:class:`BitVector`. Returns: A new instance of the class with the concatenated bit vectors. If no bit vectors are provided (i.e., the list is empty), an empty :py:class:`BitVector` is returned. """ if not bit_vectors: return cls([]) concatenated_bit_vectors = np.concatenate( [BitVector(bit_vector_like).bits for bit_vector_like in bit_vectors] ) return cls(concatenated_bit_vectors)
[docs] def concatenate(self, other: BitVectorLike) -> BitVector: """Concatenate a bitvector with a :py:const:`~tno.quantum.utils.BitVectorLike` to a bitvector. Args: other: The :py:const:`~tno.quantum.utils.BitVectorLike` instance to concatenate with. Returns: A new :py:class:`BitVector` instance with concatenated bits. """ # noqa: E501 return BitVector.concatenate_bit_vectors(bit_vectors=[self, other])
def _serialize(self) -> dict[str, Any]: """Serialize to dict.""" return {"bits": str(self)}