diff --git a/qualtran/__init__.py b/qualtran/__init__.py index c56b46dae5..861928f7e1 100644 --- a/qualtran/__init__.py +++ b/qualtran/__init__.py @@ -53,7 +53,7 @@ QVarT, ) -from ._infra.data_types import ( +from .dtype import ( QCDType, CDType, QDType, diff --git a/qualtran/_infra/data_types.py b/qualtran/_infra/data_types.py index 9c08fb1dd0..c953f2cf7d 100644 --- a/qualtran/_infra/data_types.py +++ b/qualtran/_infra/data_types.py @@ -13,1826 +13,5 @@ # limitations under the License. """Quantum data type definitions.""" -import abc -import itertools -import warnings -from enum import Enum -from functools import cached_property -from typing import ( - Any, - cast, - Generic, - Iterable, - List, - Optional, - Sequence, - Tuple, - TYPE_CHECKING, - TypeVar, - Union, -) - -import attrs -import numpy as np -from numpy.typing import NDArray - -from qualtran.symbolics import bit_length, is_symbolic, SymbolicInt - -if TYPE_CHECKING: - import fxpmath - import galois - -T = TypeVar('T') - - -class BitEncoding(Generic[T], metaclass=abc.ABCMeta): - @property - @abc.abstractmethod - def bitsize(self) -> SymbolicInt: ... - - @abc.abstractmethod - def get_domain(self) -> Iterable[T]: - """Yields all possible classical (computational basis state) values representable - by this type.""" - - @abc.abstractmethod - def to_bits(self, x: T) -> List[int]: - """Yields individual bits corresponding to binary representation of x""" - - def to_bits_array(self, x_array: NDArray) -> NDArray[np.uint8]: - """Yields an NDArray of bits corresponding to binary representations of the input elements. - - Often, converting an array can be performed faster than converting each element individually. - This operation accepts any NDArray of values, and the output array satisfies - `output_shape = input_shape + (self.bitsize,)`. - """ - return np.vectorize( - lambda x: np.asarray(self.to_bits(x), dtype=np.uint8), signature='()->(n)' - )(x_array) - - @abc.abstractmethod - def from_bits(self, bits: Sequence[int]) -> T: - """Combine individual bits to form x""" - - def from_bits_array(self, bits_array: NDArray[np.uint8]) -> NDArray: - """Combine individual bits to form classical values. - - Often, converting an array can be performed faster than converting each element individually. - This operation accepts any NDArray of bits such that the last dimension equals `self.bitsize`, - and the output array satisfies `output_shape = input_shape[:-1]`. - """ - return np.vectorize(self.from_bits, signature='(n)->()')(bits_array) - - @abc.abstractmethod - def assert_valid_val(self, val: T, debug_str: str = 'val') -> None: - """Raises an exception if `val` is not a valid classical value for this type. - - Args: - val: A classical value that should be in the domain of this QDType. - debug_str: Optional debugging information to use in exception messages. - """ - - def assert_valid_val_array(self, val_array: NDArray, debug_str: str = 'val') -> None: - """Raises an exception if `val_array` is not a valid array of classical values - for this type. - - Often, validation on an array can be performed faster than validating each element - individually. - - Args: - val_array: A numpy array of classical values. Each value should be in the domain - of this QDType. - debug_str: Optional debugging information to use in exception messages. - """ - for val in val_array.reshape(-1): - self.assert_valid_val(val, debug_str=debug_str) - - -@attrs.frozen -class _BitEncodingShim(BitEncoding[T]): - """Shim an old-style QDType to follow the BitEncoding interface. - - Before the introduction of classical data types (QCDType and CDType), QDType classes - described how to encode values into bits (for classical simulation) and qubits (for - quantum programs). The encoding schemes don't care whether the substrate is bits or - qubits but the CompositeBloq type-checking does care; so we've moved the encoding - logic to descendants of `BitEncoding`. Each `QCDType` "has a" BitEncoding and "is a" - quantum data type or classical data type. - - This shim uses encoding logic found in the methods of an old-style QDType to satisfy - the BitEncoding interface for backwards compatibility. Developers with custom QDTypes - should port their custom data types to use a BitEncoding. - - """ - - qdtype: 'QDType[T]' - - @property - def bitsize(self) -> SymbolicInt: - return self.qdtype.num_qubits - - def get_domain(self) -> Iterable[T]: - yield from self.qdtype.get_classical_domain() - - def to_bits(self, x: T) -> List[int]: - return self.qdtype.to_bits(x) - - def to_bits_array(self, x_array: NDArray) -> NDArray[np.uint8]: - return np.vectorize( - lambda x: np.asarray(self.qdtype.to_bits(x), dtype=np.uint8), signature='()->(n)' - )(x_array) - - def from_bits(self, bits: Sequence[int]) -> T: - return self.qdtype.from_bits(bits) - - def from_bits_array(self, bits_array: NDArray[np.uint8]) -> NDArray: - return np.vectorize(self.qdtype.from_bits, signature='(n)->()')(bits_array) - - def assert_valid_val(self, val: T, debug_str: str = 'val') -> None: - return self.qdtype.assert_valid_classical_val(val, debug_str=debug_str) - - def assert_valid_val_array(self, val_array: NDArray, debug_str: str = 'val') -> None: - for val in val_array.reshape(-1): - self.qdtype.assert_valid_classical_val(val) - - -@attrs.frozen -class ShapedQCDType: - qcdtype: 'QCDType' - shape: Tuple[int, ...] = attrs.field( - default=tuple(), converter=lambda v: (v,) if isinstance(v, int) else tuple(v) - ) - - -class QCDType(Generic[T], metaclass=abc.ABCMeta): - """The abstract interface for quantum/classical quantum computing data types.""" - - @property - @abc.abstractmethod - def _bit_encoding(self) -> BitEncoding[T]: - """The class describing how bits are encoded in this datatype.""" - - @property - def num_bits(self) -> int: - """Number of bits (quantum and classical) required to represent a single instance of - this data type.""" - return self.num_qubits + self.num_cbits - - @property - @abc.abstractmethod - def num_qubits(self) -> int: - """Number of qubits required to represent a single instance of this data type.""" - - @property - @abc.abstractmethod - def num_cbits(self) -> int: - """Number of classical bits required to represent a single instance of this data type.""" - - def get_classical_domain(self) -> Iterable[T]: - """Yields all possible classical (computational basis state) values representable - by this type.""" - yield from self._bit_encoding.get_domain() - - def to_bits(self, x: T) -> List[int]: - """Yields individual bits corresponding to binary representation of x""" - return self._bit_encoding.to_bits(x) - - def to_bits_array(self, x_array: NDArray) -> NDArray[np.uint8]: - """Yields an NDArray of bits corresponding to binary representations of the input elements. - - Often, converting an array can be performed faster than converting each element individually. - This operation accepts any NDArray of values, and the output array satisfies - `output_shape = input_shape + (self.bitsize,)`. - """ - return self._bit_encoding.to_bits_array(x_array) - - def from_bits(self, bits: Sequence[int]) -> T: - """Combine individual bits to form x""" - return self._bit_encoding.from_bits(bits) - - def from_bits_array(self, bits_array: NDArray[np.uint8]) -> NDArray: - """Combine individual bits to form classical values. - - Often, converting an array can be performed faster than converting each element individually. - This operation accepts any NDArray of bits such that the last dimension equals `self.bitsize`, - and the output array satisfies `output_shape = input_shape[:-1]`. - """ - return self._bit_encoding.from_bits_array(bits_array) - - def assert_valid_classical_val(self, val: T, debug_str: str = 'val') -> None: - """Raises an exception if `val` is not a valid classical value for this type. - - Args: - val: A classical value that should be in the domain of this QDType. - debug_str: Optional debugging information to use in exception messages. - """ - return self._bit_encoding.assert_valid_val(val=val, debug_str=debug_str) - - def assert_valid_classical_val_array(self, val_array: NDArray, debug_str: str = 'val') -> None: - """Raises an exception if `val_array` is not a valid array of classical values - for this type. - - Often, validation on an array can be performed faster than validating each element - individually. - - Args: - val_array: A numpy array of classical values. Each value should be in the domain - of this QDType. - debug_str: Optional debugging information to use in exception messages. - """ - return self._bit_encoding.assert_valid_val_array(val_array=val_array, debug_str=debug_str) - - def is_symbolic(self) -> bool: - """Returns True if this dtype is parameterized with symbolic objects.""" - return is_symbolic(self._bit_encoding.bitsize) - - def iteration_length_or_zero(self) -> SymbolicInt: - """Safe version of iteration length. - - Returns the iteration_length if the type has it or else zero. - """ - # TODO: remove https://github.com/quantumlib/Qualtran/issues/1716 - return getattr(self, 'iteration_length', 0) - - def __getitem__(self, shape): - """QInt(8)[20] returns a size-20 array of QInt(8)""" - return ShapedQCDType(qcdtype=self, shape=shape) - - @classmethod - def _pkg_(cls): - return 'qualtran' - - def __str__(self): - return f'{self.__class__.__name__}({self.num_bits})' - - -class QDType(QCDType[T], metaclass=abc.ABCMeta): - """The abstract interface for quantum data types.""" - - @property - def _bit_encoding(self) -> BitEncoding[T]: - """The class describing how bits are encoded in this datatype.""" - warnings.warn( - f"{self} must provide a BitEncoding. " - f"This shim will become an error in the future. " - f"Omitting this may cause infinite loops.", - DeprecationWarning, - ) - return _BitEncodingShim(self) - - @property - def num_qubits(self) -> int: - return cast(int, self._bit_encoding.bitsize) - - @property - def num_cbits(self) -> int: - """QDTypes have zero qubits.""" - return 0 - - def __str__(self): - return f'{self.__class__.__name__}({self.num_qubits})' - - -class CDType(QCDType[T], metaclass=abc.ABCMeta): - """The abstract interface for classical data types.""" - - @property - def num_qubits(self) -> int: - """CDTypes have zero qubits.""" - return 0 - - @property - def num_cbits(self) -> int: - return cast(int, self._bit_encoding.bitsize) - - def __str__(self): - return f'{self.__class__.__name__}({self.num_cbits})' - - -@attrs.frozen -class _Bit(BitEncoding[int]): - """A single quantum or classical bit. The smallest addressable unit of data. - - Use either `QBit()` or `CBit()` for quantum or classical implementations, respectively. - """ - - @property - def bitsize(self) -> int: - return 1 - - def get_domain(self) -> Iterable[int]: - yield from (0, 1) - - def assert_valid_val(self, val: int, debug_str: str = 'val'): - if not (val == 0 or val == 1): - raise ValueError(f"Bad bit value: {val} in {debug_str}") - - def to_bits(self, x: int) -> List[int]: - """Yields individual bits corresponding to binary representation of x""" - self.assert_valid_val(x) - return [int(x)] - - def from_bits(self, bits: Sequence[int]) -> int: - """Combine individual bits to form x""" - assert len(bits) == 1 - return bits[0] - - def assert_valid_val_array( - self, val_array: NDArray[np.integer], debug_str: str = 'val' - ) -> None: - if not np.all((val_array == 0) | (val_array == 1)): - raise ValueError(f"Bad bit value array in {debug_str}") - - -@attrs.frozen -class QBit(QDType[int]): - """A single qubit. The smallest addressable unit of quantum data.""" - - @cached_property - def _bit_encoding(self) -> BitEncoding[int]: - return _Bit() - - def is_symbolic(self) -> bool: - return False - - def __str__(self) -> str: - return 'QBit()' - - -@attrs.frozen -class CBit(CDType[int]): - """A single classical bit. The smallest addressable unit of classical data.""" - - @cached_property - def _bit_encoding(self) -> BitEncoding[int]: - return _Bit() - - def is_symbolic(self) -> bool: - return False - - def __str__(self) -> str: - return 'CBit()' - - -@attrs.frozen -class QAny(QDType[Any]): - """Opaque bag-of-qubits type.""" - - bitsize: SymbolicInt - - @property - def _bit_encoding(self) -> BitEncoding[Any]: - return _UInt(self.bitsize) - - def __attrs_post_init__(self): - if is_symbolic(self.bitsize): - return - - if not isinstance(self.bitsize, int): - raise ValueError(f"Bad bitsize for QAny: {self.bitsize}") - - def get_classical_domain(self) -> Iterable[Any]: - raise TypeError(f"Ambiguous domain for {self}. Please use a more specific type.") - - def assert_valid_classical_val(self, val: Any, debug_str: str = 'val'): - pass - - def assert_valid_classical_val_array(self, val_array: NDArray, debug_str: str = 'val'): - pass - - -@attrs.frozen -class _Int(BitEncoding[int]): - """Signed integer of a given bitsize. - - Use `QInt` or `CInt` for quantum or classical implementations, respectively. - - A two's complement representation is used for negative integers. - Here (and throughout Qualtran), we use a big-endian bit convention. - The most significant bit is at index 0. - """ - - bitsize: SymbolicInt - - def get_domain(self) -> Iterable[int]: - max_val = 1 << (self.bitsize - 1) - return range(-max_val, max_val) - - def to_bits(self, x: int) -> List[int]: - if is_symbolic(self.bitsize): - raise ValueError(f"cannot compute bits with symbolic {self.bitsize=}") - - self.assert_valid_val(x) - return [int(b) for b in np.binary_repr(x, width=self.bitsize)] - - def from_bits(self, bits: Sequence[int]) -> int: - sign = bits[0] - x = ( - 0 - if self.bitsize == 1 - else _UInt(self.bitsize - 1).from_bits([1 - x if sign else x for x in bits[1:]]) - ) - return ~x if sign else x - - def assert_valid_val(self, val: int, debug_str: str = 'val'): - if not isinstance(val, (int, np.integer)): - raise ValueError(f"{debug_str} should be an integer, not {val!r}") - if val < -(2 ** (self.bitsize - 1)): - raise ValueError(f"Too-small classical {self}: {val} encountered in {debug_str}") - if val >= 2 ** (self.bitsize - 1): - raise ValueError(f"Too-large classical {self}: {val} encountered in {debug_str}") - - def assert_valid_val_array(self, val_array: NDArray[np.integer], debug_str: str = 'val'): - if np.any(val_array < -(2 ** (self.bitsize - 1))): - raise ValueError(f"Too-small classical {self}s encountered in {debug_str}") - if np.any(val_array >= 2 ** (self.bitsize - 1)): - raise ValueError(f"Too-large classical {self}s encountered in {debug_str}") - - -@attrs.frozen -class QInt(QDType[int]): - """Signed quantum integer of a given bitsize. - - A two's complement representation is used for negative integers. - Here (and throughout Qualtran), we use a big-endian bit convention. - The most significant bit is at index 0. - - Args: - bitsize: The number of qubits used to represent the integer. - """ - - bitsize: SymbolicInt - - @cached_property - def _bit_encoding(self) -> BitEncoding[int]: - return _Int(self.bitsize) - - -@attrs.frozen -class CInt(CDType[int]): - """Signed classical integer of a given bitsize. - - A two's complement representation is used for negative integers. - Here (and throughout Qualtran), we use a big-endian bit convention. - The most significant bit is at index 0. - - Args: - bitsize: The number of qubits used to represent the integer. - """ - - bitsize: SymbolicInt - - @cached_property - def _bit_encoding(self) -> BitEncoding[int]: - return _Int(self.bitsize) - - -@attrs.frozen -class _IntOnesComp(BitEncoding[int]): - """Ones' complement signed integer of a given bitsize. - - This contrasts with `_Int` by using the ones' complement representation for negative - integers. - Here (and throughout Qualtran), we use a big-endian bit convention. - The most significant bit is at index 0. - """ - - bitsize: SymbolicInt - - def __attrs_post_init__(self): - if isinstance(self.bitsize, int): - if self.bitsize == 1: - raise ValueError("bitsize must be > 1.") - - def to_bits(self, x: int) -> List[int]: - self.assert_valid_val(x) - return [int(x < 0)] + [y ^ int(x < 0) for y in _UInt(self.bitsize - 1).to_bits(abs(x))] - - def from_bits(self, bits: Sequence[int]) -> int: - x = _UInt(self.bitsize).from_bits([b ^ bits[0] for b in bits[1:]]) - return (-1) ** int(bits[0]) * x - - def get_domain(self) -> Iterable[int]: - max_val = 1 << (self.bitsize - 1) - return range(-max_val + 1, max_val) - - def assert_valid_val(self, val: int, debug_str: str = 'val') -> None: - if not isinstance(val, (int, np.integer)): - raise ValueError(f"{debug_str} should be an integer, not {val!r}") - max_val = 1 << (self.bitsize - 1) - if not -max_val <= val <= max_val: - raise ValueError( - f"Classical value {val} must be in range [-{max_val}, +{max_val}] in {debug_str}" - ) - - -@attrs.frozen -class QIntOnesComp(QDType[int]): - """Ones' complement signed quantum integer of a given bitsize. - - This contrasts with `QInt` by using the ones' complement representation for negative - integers. - Here (and throughout Qualtran), we use a big-endian bit convention. - The most significant bit is at index 0. - - Args: - bitsize: The number of qubits used to represent the integer. - """ - - bitsize: SymbolicInt - - def __attrs_post_init__(self): - if isinstance(self.bitsize, int): - if self.bitsize == 1: - raise ValueError("bitsize must be > 1.") - - @cached_property - def _bit_encoding(self) -> BitEncoding[int]: - return _IntOnesComp(self.bitsize) - - -@attrs.frozen -class CIntOnesComp(CDType[int]): - """Ones' complement signed classical integer of a given bitsize. - - This contrasts with `CInt` by using the ones' complement representation for negative - integers. - Here (and throughout Qualtran), we use a big-endian bit convention. - The most significant bit is at index 0. - - Args: - bitsize: The number of classical bits used to represent the integer. - """ - - bitsize: SymbolicInt - - def __attrs_post_init__(self): - if isinstance(self.bitsize, int): - if self.bitsize == 1: - raise ValueError("bitsize must be > 1.") - - @cached_property - def _bit_encoding(self) -> BitEncoding[int]: - return _IntOnesComp(self.bitsize) - - -@attrs.frozen -class _UInt(BitEncoding[int]): - """Unsigned integer of a given bitsize. - - Here (and throughout Qualtran), we use a big-endian bit convention. The most significant - bit is at index 0. - """ - - bitsize: SymbolicInt - - def get_domain(self) -> Iterable[int]: - return range(2**self.bitsize) - - def to_bits(self, x: int) -> List[int]: - self.assert_valid_val(x) - return [int(x) for x in f'{int(x):0{self.bitsize}b}'] - - def to_bits_array(self, x_array: NDArray[np.integer]) -> NDArray[np.uint8]: - if is_symbolic(self.bitsize): - raise ValueError(f"Cannot compute bits for symbolic {self.bitsize=}") - - if self.bitsize > 64: - return np.vectorize( - lambda x: np.asarray(self.to_bits(x), dtype=np.uint8), signature='()->(n)' - )(x_array) - - w = int(self.bitsize) - x = np.atleast_1d(x_array) - if not np.issubdtype(x.dtype, np.uint): - assert np.all(x >= 0) - assert np.iinfo(x.dtype).bits <= 64 - x = x.astype(np.uint64) - assert w <= np.iinfo(x.dtype).bits - mask = 2 ** np.arange(w - 1, 0 - 1, -1, dtype=x.dtype).reshape((w, 1)) - return (x & mask).astype(bool).astype(np.uint8).T - - def from_bits(self, bits: Sequence[int]) -> int: - return int("".join(str(x) for x in bits), 2) - - def from_bits_array(self, bits_array: NDArray[np.uint8]) -> NDArray[np.uint64]: - bitstrings = np.atleast_2d(bits_array) - if bitstrings.shape[1] != self.bitsize: - raise ValueError(f"Input bitsize {bitstrings.shape[1]} does not match {self.bitsize=}") - - if self.bitsize > 64: - # use the default vectorized `from_bits` - return np.vectorize(self.from_bits, signature='(n)->()')(bits_array) - - basis = 2 ** np.arange(self.bitsize - 1, 0 - 1, -1, dtype=np.uint64) - return np.sum(basis * bitstrings, axis=1, dtype=np.uint64) # type: ignore[return-value] - - def assert_valid_val(self, val: int, debug_str: str = 'val') -> None: - if not isinstance(val, (int, np.integer)): - raise ValueError(f"{debug_str} should be an integer, not {val!r}") - if val < 0: - raise ValueError(f"Negative classical value encountered in {debug_str}") - if val >= 2**self.bitsize: - raise ValueError(f"Too-large classical value encountered in {debug_str}") - - def assert_valid_val_array( - self, val_array: NDArray[np.integer], debug_str: str = 'val' - ) -> None: - if np.any(val_array < 0): - raise ValueError(f"Negative classical values encountered in {debug_str}") - if np.any(val_array >= 2**self.bitsize): - raise ValueError(f"Too-large classical values encountered in {debug_str}") - - -@attrs.frozen -class QUInt(QDType[int]): - """Unsigned quantum integer of a given bitsize. - - Here (and throughout Qualtran), we use a big-endian bit convention. The most significant - bit is at index 0. - - Args: - bitsize: The number of qubits used to represent the integer. - """ - - bitsize: SymbolicInt - - @cached_property - def _bit_encoding(self) -> BitEncoding[int]: - return _UInt(self.bitsize) - - -@attrs.frozen -class CUInt(CDType[int]): - """Unsigned classical integer of a given bitsize. - - Here (and throughout Qualtran), we use a big-endian bit convention. The most significant - bit is at index 0. - - Args: - bitsize: The number of classical bits used to represent the integer. - """ - - bitsize: SymbolicInt - - @cached_property - def _bit_encoding(self) -> BitEncoding[int]: - return _UInt(self.bitsize) - - -@attrs.frozen -class _BUInt(BitEncoding[int]): - """Unsigned integer whose values are bounded within a range. - - Args: - bitsize: The number of bits used to represent the integer. - bound: The bound (exclusive) - """ - - bitsize: SymbolicInt - bound: SymbolicInt - - def __attrs_post_init__(self): - if is_symbolic(self.bitsize) or is_symbolic(self.bound): - return - - if self.bound > 2**self.bitsize: - raise ValueError( - "BUInt value bound is too large for given bitsize. " - f"{self.bound} vs {2**self.bitsize}" - ) - - def get_domain(self) -> Iterable[int]: - if isinstance(self.bound, int): - return range(0, self.bound) - raise ValueError(f'Classical domain not defined for {self}') - - def assert_valid_val(self, val: int, debug_str: str = 'val') -> None: - if not isinstance(val, (int, np.integer)): - raise ValueError(f"{debug_str} should be an integer, not {val!r}") - if val < 0: - raise ValueError(f"Negative classical value encountered in {debug_str}") - if val >= self.bound: - raise ValueError(f"Too-large classical value encountered in {debug_str}") - - def to_bits(self, x: int) -> List[int]: - """Yields individual bits corresponding to binary representation of x""" - self.assert_valid_val(x) - return _UInt(self.bitsize).to_bits(x) - - def from_bits(self, bits: Sequence[int]) -> int: - """Combine individual bits to form x""" - val = _UInt(self.bitsize).from_bits(bits) - self.assert_valid_val(val) - return val - - def assert_valid_val_array(self, val_array: NDArray[np.integer], debug_str: str = 'val'): - if np.any(val_array < 0): - raise ValueError(f"Negative classical values encountered in {debug_str}") - if np.any(val_array >= self.bound): - raise ValueError(f"Too-large classical values encountered in {debug_str}") - - -@attrs.frozen -class BQUInt(QDType[int]): - """Unsigned quantum integer whose values are bounded within a range. - - LCU methods often make use of coherent for-loops via UnaryIteration, iterating over a range - of values stored as a superposition over the `SELECT` register. Such (nested) coherent - for-loops can be represented using a `Tuple[Register(dtype=BQUInt), ...]` where the - i'th entry stores the bitsize and iteration length of i'th - nested for-loop. - - One useful feature when processing such nested for-loops is to flatten out a composite index, - represented by a tuple of indices (i, j, ...), one for each selection register into a single - integer that can be used to index a flat target register. An example of such a mapping - function is described in Eq.45 of https://arxiv.org/abs/1805.03662. A general version of this - mapping function can be implemented using `numpy.ravel_multi_index` and `numpy.unravel_index`. - - Examples: - We can flatten a 2D for-loop as follows - - >>> import numpy as np - >>> N, M = 10, 20 - >>> flat_indices = set() - >>> for x in range(N): - ... for y in range(M): - ... flat_idx = x * M + y - ... assert np.ravel_multi_index((x, y), (N, M)) == flat_idx - ... assert np.unravel_index(flat_idx, (N, M)) == (x, y) - ... flat_indices.add(flat_idx) - >>> assert len(flat_indices) == N * M - - Similarly, we can flatten a 3D for-loop as follows - >>> import numpy as np - >>> N, M, L = 10, 20, 30 - >>> flat_indices = set() - >>> for x in range(N): - ... for y in range(M): - ... for z in range(L): - ... flat_idx = x * M * L + y * L + z - ... assert np.ravel_multi_index((x, y, z), (N, M, L)) == flat_idx - ... assert np.unravel_index(flat_idx, (N, M, L)) == (x, y, z) - ... flat_indices.add(flat_idx) - >>> assert len(flat_indices) == N * M * L - - Args: - bitsize: The number of qubits used to represent the integer. - iteration_length: The length of the iteration range. - """ - - bitsize: SymbolicInt - iteration_length: SymbolicInt = attrs.field() - - def __attrs_post_init__(self): - if not self.is_symbolic(): - if self.iteration_length > 2**self.bitsize: - raise ValueError( - f"{self} iteration length is too large for given bitsize. " - f"{self.iteration_length} vs {2**self.bitsize}" - ) - - @iteration_length.default - def _default_iteration_length(self): - return 2**self.bitsize - - @property - def bound(self) -> SymbolicInt: - return self.iteration_length - - def is_symbolic(self) -> bool: - return is_symbolic(self.bitsize, self.iteration_length) - - @property - def _bit_encoding(self) -> BitEncoding[int]: - return _BUInt(self.bitsize, self.iteration_length) - - def __str__(self): - return f'{self.__class__.__name__}({self.bitsize}, {self.iteration_length})' - - -@attrs.frozen -class BCUInt(CDType[int]): - """Unsigned classical integer whose values are bounded within a range. - - Args: - bitsize: The number of bits used to represent the integer. - bound: The value bound (exclusive). - """ - - bitsize: SymbolicInt - bound: SymbolicInt = attrs.field() - - def __attrs_post_init__(self): - if not self.is_symbolic(): - if self.bound > 2**self.bitsize: - raise ValueError( - f"{self} bound is too large for given bitsize. " - f"{self.bound} vs {2**self.bitsize}" - ) - - @bound.default - def _default_bound(self): - return 2**self.bitsize - - def is_symbolic(self) -> bool: - return is_symbolic(self.bitsize, self.bound) - - @property - def _bit_encoding(self) -> BitEncoding[int]: - return _BUInt(self.bitsize, self.bound) - - def __str__(self): - return f'{self.__class__.__name__}({self.bitsize}, {self.bound})' - - -@attrs.frozen -class _Fxp(BitEncoding[int]): - r"""Fixed point type to represent real numbers. - - To hook into the classical simulator, we use fixed-width integers to represent - values of this type. See `to_fixed_width_int` for details. - In particular, the user should call `QFxp.to_fixed_width_int(float_value)` - before passing a value to `bloq.call_classically`. - - See https://github.com/quantumlib/Qualtran/issues/1219 for discussion on alternatives - and future upgrades. - - - Args: - bitsize: The total number of qubits used to represent the integer and - fractional part combined. - num_frac: The number of qubits used to represent the fractional part of the real number. - signed: Whether the number is signed or not. - """ - - bitsize: SymbolicInt - num_frac: SymbolicInt - signed: bool = False - - def __attrs_post_init__(self): - if not is_symbolic(self.bitsize) and self.bitsize == 1 and self.signed: - raise ValueError("bitsize must be > 1.") - if not is_symbolic(self.bitsize) and not is_symbolic(self.num_frac): - if self.signed and self.bitsize == self.num_frac: - raise ValueError("num_frac must be less than bitsize if the Fxp is signed.") - if self.bitsize < self.num_frac: - raise ValueError("bitsize must be >= num_frac.") - - @property - def num_int(self) -> SymbolicInt: - """Number of bits for the integral part.""" - return self.bitsize - self.num_frac - - @property - def _int_encoding(self) -> Union[_UInt, _Int]: - # The corresponding dtype for the raw integer encoding. - return _Int(self.bitsize) if self.signed else _UInt(self.bitsize) - - def get_domain(self) -> Iterable[int]: - # Use the classical domain for the underlying raw integer encoding. - yield from self._int_encoding.get_domain() - - def to_bits(self, x: int) -> List[int]: - # Use the underlying raw integer encoding. - return self._int_encoding.to_bits(x) - - def from_bits(self, bits: Sequence[int]) -> int: - # Use the underlying raw integer encoding. - return self._int_encoding.from_bits(bits) - - def assert_valid_val(self, val: int, debug_str: str = 'val'): - # Verify using the underlying raw integer encoding. - self._int_encoding.assert_valid_val(val, debug_str) - - def to_fixed_width_int( - self, - x: Union[float, 'fxpmath.Fxp'], - *, - require_exact: bool = False, - complement: bool = True, - ) -> int: - """Returns the interpretation of the binary representation of `x` as an integer. - - The returned value is an integer equal to `round(x * 2**self.num_frac)`. - That is, the input value `x` is converted to a fixed-point binary value - of `self.num_int` integral bits and `self.num_frac` fractional bits, - and then re-interpreted as an integer by dropping the decimal point. - - Args: - x: input real number - require_exact: Raise `ValueError` if `x` cannot be exactly represented. - complement: Use twos-complement rather than sign-magnitude representation of negative values. - """ - bits = self._fxp_to_bits(x, require_exact=require_exact, complement=complement) - return self._int_encoding.from_bits(bits) - - def float_from_fixed_width_int(self, x: int) -> float: - """Helper to convert from the fixed-width-int representation to a true floating point value. - - Here `x` is the internal value used by the classical simulator. - See `to_fixed_width_int` for conventions. - """ - return x / 2**self.num_frac - - def __str__(self): - if self.signed: - return f'_Fxp({self.bitsize}, {self.num_frac}, True)' - else: - return f'_Fxp({self.bitsize}, {self.num_frac})' - - def fxp_dtype_template(self) -> 'fxpmath.Fxp': - """A template of the `fxpmath.Fxp` data type for classical values. - - To construct an `fxpmath.Fxp` with this config, one can use: - `Fxp(float_value, like=_Fxp(...).fxp_dtype_template)`, - or given an existing value `some_fxp_value: Fxp`: - `some_fxp_value.like(_Fxp(...).fxp_dtype_template)`. - - The following Fxp configuration is used: - - op_sizing='same' and const_op_sizing='same' ensure that the returned - object is not resized to a bigger fixed point number when doing - operations with other Fxp objects. - - shifting='trunc' ensures that when shifting the Fxp integer to - left / right; the digits are truncated and no rounding occurs - - overflow='wrap' ensures that when performing operations where result - overflows, the overflowed digits are simply discarded. - - Support for `fxpmath.Fxp` is experimental, and does not hook into the classical - simulator protocol. Once the library choice for fixed-point classical real - values is finalized, the code will be updated to use the new functionality - instead of delegating to raw integer values (see above). - """ - import fxpmath - - if is_symbolic(self.bitsize) or is_symbolic(self.num_frac): - raise ValueError( - f"Cannot construct Fxp template for symbolic bitsizes: {self.bitsize=}, {self.num_frac=}" - ) - - return fxpmath.Fxp( - None, - n_word=self.bitsize, - n_frac=self.num_frac, - signed=self.signed, - op_sizing='same', - const_op_sizing='same', - shifting='trunc', - overflow='wrap', - ) - - def _get_domain_fxp(self) -> Iterable['fxpmath.Fxp']: - import fxpmath - - for x in self._int_encoding.get_domain(): - yield fxpmath.Fxp(x / 2**self.num_frac, like=self.fxp_dtype_template()) - - def _fxp_to_bits( - self, x: Union[float, 'fxpmath.Fxp'], require_exact: bool = True, complement: bool = True - ) -> List[int]: - """Yields individual bits corresponding to binary representation of `x`. - - Args: - x: The value to encode. - require_exact: Raise `ValueError` if `x` cannot be exactly represented. - complement: Use twos-complement rather than sign-magnitude representation of negative values. - - Raises: - ValueError: If `x` is negative but this `_Fxp` is not signed. - """ - import fxpmath - - if require_exact: - self._assert_valid_val(x) - if x < 0 and not self.signed: - raise ValueError(f"unsigned _Fxp cannot represent {x}.") - if self.signed and not complement: - sign = int(x < 0) - x = abs(x) - fxp = x if isinstance(x, fxpmath.Fxp) else fxpmath.Fxp(x) - bits = [int(x) for x in fxp.like(self.fxp_dtype_template()).bin()] - if self.signed and not complement: - bits[0] = sign - return bits - - def _from_bits_to_fxp(self, bits: Sequence[int]) -> 'fxpmath.Fxp': - import fxpmath - - if is_symbolic(self.num_frac): - raise ValueError(f"Symbolic {self.num_frac} cannot be represented using Fxp") - bits_bin = "".join(str(x) for x in bits[:]) - fxp_bin = "0b" + bits_bin[: -int(self.num_frac)] + "." + bits_bin[-int(self.num_frac) :] - return fxpmath.Fxp(fxp_bin, like=self.fxp_dtype_template()) - - def _assert_valid_val(self, val: Union[float, 'fxpmath.Fxp'], debug_str: str = 'val'): - import fxpmath - - fxp_val = val if isinstance(val, fxpmath.Fxp) else fxpmath.Fxp(val) - if fxp_val.get_val() != fxp_val.like(self.fxp_dtype_template()).get_val(): - raise ValueError( - f"{debug_str}={val} cannot be accurately represented using Fxp {fxp_val}" - ) - - -@attrs.frozen -class QFxp(QDType[int]): - r"""Fixed point quantum type to represent real numbers. - - A real number can be approximately represented in fixed point using `num_int` - bits for the integer part and `num_frac` bits for the fractional part. If the - real number is signed we store negative values in two's complement form. The first - bit can therefore be treated as the sign bit in such cases (0 for +, 1 for -). - In total there are `bitsize = (num_int + num_frac)` bits used to represent the number. - E.g. Using `(bitsize = 8, num_frac = 6, signed = False)` then - $\pi \approx 3.140625 = 11.001001$, where the . represents the decimal place. - - We can specify a fixed point real number by the tuple bitsize, num_frac and - signed, with num_int determined as `(bitsize - num_frac)`. - - **Classical Simulation:** - - To hook into the classical simulator, we use fixed-width integers to represent - values of this type. See `to_fixed_width_int` for details. - In particular, the user should call `QFxp.to_fixed_width_int(float_value)` - before passing a value to `bloq.call_classically`. - - The corresponding raw qdtype is either an QUInt (when `signed=False`) or - QInt (when `signed=True`) of the same bitsize. This is the data type used - to represent classical values during simulation, and convert to and from bits - for intermediate values. - - For example, `QFxp(6, 4)` has 2 int bits and 4 frac bits, and the corresponding - int type is `QUInt(6)`. So a true classical value of `10.0011` will have a raw - integer representation of `100011`. - - Args: - bitsize: The total number of qubits used to represent the integer and - fractional part combined. - num_frac: The number of qubits used to represent the fractional part of the real number. - signed: Whether the number is signed or not. - """ - - bitsize: SymbolicInt - num_frac: SymbolicInt - signed: bool = False - - def __attrs_post_init__(self): - if not is_symbolic(self.bitsize) and self.bitsize == 1 and self.signed: - raise ValueError("num_qubits must be > 1.") - if not is_symbolic(self.bitsize) and not is_symbolic(self.num_frac): - if self.signed and self.bitsize == self.num_frac: - raise ValueError("num_frac must be less than bitsize if the QFxp is signed.") - if self.bitsize < self.num_frac: - raise ValueError("bitsize must be >= num_frac.") - - @property - def _bit_encoding(self) -> _Fxp: - return _Fxp(bitsize=self.bitsize, num_frac=self.num_frac, signed=self.signed) - - @property - def num_int(self) -> SymbolicInt: - """Number of bits for the integral part.""" - return self._bit_encoding.num_int - - def is_symbolic(self) -> bool: - return is_symbolic(self.bitsize, self.num_frac) - - def to_fixed_width_int( - self, - x: Union[float, 'fxpmath.Fxp'], - *, - require_exact: bool = False, - complement: bool = True, - ) -> int: - """Returns the interpretation of the binary representation of `x` as an integer. - - See class docstring section on "Classical Simulation" for more details on - the choice of this representation. - - The returned value is an integer equal to `round(x * 2**self.num_frac)`. - That is, the input value `x` is converted to a fixed-point binary value - of `self.num_int` integral bits and `self.num_frac` fractional bits, - and then re-interpreted as an integer by dropping the decimal point. - - For example, consider `QFxp(6, 4).to_fixed_width_int(1.5)`. As `1.5` is `0b01.1000` - in this representation, the returned value would be `0b011000` = 24. - - For negative values, we use twos complement form. So in - `QFxp(6, 4, signed=True).to_fixed_width_int(-1.5)`, the input is `0b10.1000`, - which is interpreted as `0b101000` = -24. - - Args: - x: input floating point value - require_exact: Raise `ValueError` if `x` cannot be exactly represented. - complement: Use twos-complement rather than sign-magnitude representation of negative values. - """ - return self._bit_encoding.to_fixed_width_int( - x=x, require_exact=require_exact, complement=complement - ) - - def float_from_fixed_width_int(self, x: int) -> float: - """Helper to convert from the fixed-width-int representation to a true floating point value. - - Here `x` is the internal value used by the classical simulator. - See `to_fixed_width_int` for conventions. - - See class docstring section on "Classical Simulation" for more details on - the choice of this representation. - """ - return self._bit_encoding.float_from_fixed_width_int(x=x) - - def __str__(self): - if self.signed: - return f'QFxp({self.bitsize}, {self.num_frac}, True)' - else: - return f'QFxp({self.bitsize}, {self.num_frac})' - - def fxp_dtype_template(self) -> 'fxpmath.Fxp': - """A template of the `Fxp` data type for classical values. - - To construct an `Fxp` with this config, one can use: - `Fxp(float_value, like=QFxp(...).fxp_dtype_template)`, - or given an existing value `some_fxp_value: Fxp`: - `some_fxp_value.like(QFxp(...).fxp_dtype_template)`. - - The following Fxp configuration is used: - - op_sizing='same' and const_op_sizing='same' ensure that the returned - object is not resized to a bigger fixed point number when doing - operations with other Fxp objects. - - shifting='trunc' ensures that when shifting the Fxp integer to - left / right; the digits are truncated and no rounding occurs - - overflow='wrap' ensures that when performing operations where result - overflows, the overflowed digits are simply discarded. - - Support for `fxpmath.Fxp` is experimental, and does not hook into the classical - simulator protocol. Once the library choice for fixed-point classical real - values is finalized, the code will be updated to use the new functionality - instead of delegating to raw integer values (see above). - """ - return self._bit_encoding.fxp_dtype_template() - - -@attrs.frozen -class CFxp(CDType[int]): - r"""Fixed point classical type to represent real numbers. - - This follows the same conventions as `QFxp`. See that class documentation for details. - - Args: - bitsize: The total number of qubits used to represent the integer and - fractional part combined. - num_frac: The number of qubits used to represent the fractional part of the real number. - signed: Whether the number is signed or not. - """ - - bitsize: SymbolicInt - num_frac: SymbolicInt - signed: bool = False - - def __attrs_post_init__(self): - if not is_symbolic(self.bitsize) and self.bitsize == 1 and self.signed: - raise ValueError("num_qubits must be > 1.") - if not is_symbolic(self.bitsize) and not is_symbolic(self.num_frac): - if self.signed and self.bitsize == self.num_frac: - raise ValueError("num_frac must be less than bitsize if the QFxp is signed.") - if self.bitsize < self.num_frac: - raise ValueError("bitsize must be >= num_frac.") - - @property - def _bit_encoding(self) -> _Fxp: - return _Fxp(bitsize=self.bitsize, num_frac=self.num_frac, signed=self.signed) - - @property - def num_int(self) -> SymbolicInt: - """Number of bits for the integral part.""" - return self._bit_encoding.num_int - - def is_symbolic(self) -> bool: - return is_symbolic(self.bitsize, self.num_frac) - - def __str__(self): - if self.signed: - return f'CFxp({self.bitsize}, {self.num_frac}, True)' - else: - return f'CFxp({self.bitsize}, {self.num_frac})' - - -@attrs.frozen -class _MontgomeryUInt(BitEncoding[int]): - r"""Montgomery form of an unsigned integer of a given width bitsize which wraps around upon - overflow. - - Any MontgomeryUInt can be treated as a UInt, but not - every UInt can be treated as a MontgomeryUInt. Montgomery form is used in order to compute - fast modular multiplication. - """ - - bitsize: SymbolicInt - modulus: Optional[SymbolicInt] = None - - def get_domain(self) -> Iterable[int]: - if self.modulus is None or is_symbolic(self.modulus): - return range(2**self.bitsize) - return range(1, int(self.modulus)) - - def to_bits(self, x: int) -> List[int]: - self.assert_valid_val(x) - return [int(x) for x in f'{int(x):0{self.bitsize}b}'] - - def from_bits(self, bits: Sequence[int]) -> int: - return int("".join(str(x) for x in bits), 2) - - def assert_valid_val(self, val: int, debug_str: str = 'val') -> None: - if not isinstance(val, (int, np.integer)): - raise ValueError(f"{debug_str} should be an integer, not {val!r}") - if val < 0: - raise ValueError(f"Negative classical value encountered in {debug_str}") - if val >= 2**self.bitsize: - raise ValueError(f"Too-large classical value encountered in {debug_str}") - - def assert_valid_val_array( - self, val_array: NDArray[np.integer], debug_str: str = 'val' - ) -> None: - if np.any(val_array < 0): - raise ValueError(f"Negative classical values encountered in {debug_str}") - if np.any(val_array >= 2**self.bitsize): - raise ValueError(f"Too-large classical values encountered in {debug_str}") - - def montgomery_inverse(self, xm: int) -> int: - """Returns the modular inverse of an integer in montgomery form. - - Args: - xm: An integer in montgomery form. - """ - assert self.modulus is not None and not is_symbolic(self.modulus) - return ( - (pow(xm, -1, self.modulus)) * pow(2, 2 * self.bitsize, int(self.modulus)) - ) % self.modulus - - def montgomery_product(self, xm: int, ym: int) -> int: - """Returns the modular product of two integers in montgomery form. - - Args: - xm: The first montgomery form integer for the product. - ym: The second montgomery form integer for the product. - """ - assert self.modulus is not None and not is_symbolic(self.modulus) - return (xm * ym * pow(2, -int(self.bitsize), int(self.modulus))) % self.modulus - - def montgomery_to_uint(self, xm: int) -> int: - """Converts an integer in montgomery form to a normal form integer. - - Args: - xm: An integer in montgomery form. - """ - assert self.modulus is not None and not is_symbolic(self.modulus) - return (xm * pow(2, -int(self.bitsize), int(self.modulus))) % self.modulus - - def uint_to_montgomery(self, x: int) -> int: - """Converts an integer into montgomery form. - - Args: - x: An integer. - """ - assert self.modulus is not None and not is_symbolic(self.modulus) - return (x * pow(2, int(self.bitsize), int(self.modulus))) % self.modulus - - -@attrs.frozen -class QMontgomeryUInt(QDType[int]): - r"""Montgomery form of an unsigned integer of a given width bitsize which wraps around upon - overflow. - - Similar to unsigned integer types in C. Any intended wrap around effect is - expected to be handled by the developer. Any QMontgomeryUInt can be treated as a QUInt, but not - every QUInt can be treated as a QMontgomeryUInt. Montgomery form is used in order to compute - fast modular multiplication. - - In order to convert an unsigned integer from a finite field x % p into Montgomery form you - first must choose a value r > p where gcd(r, p) = 1. Typically, this value is a power of 2. - - Conversion to Montgomery form is given by - `[x] = (x * r) % p` - - Conversion from Montgomery form to normal form is given by - `x = REDC([x])` - - Pseudocode for REDC(u) can be found in the resource below. - - Args: - bitsize: The number of qubits used to represent the integer. - modulus: The modulus p. - - References: - [Montgomery modular multiplication](https://en.wikipedia.org/wiki/Montgomery_modular_multiplication). - - [Performance Analysis of a Repetition Cat Code Architecture: Computing 256-bit Elliptic Curve Logarithm in 9 Hours with 126133 Cat Qubits](https://arxiv.org/abs/2302.06639). - Gouzien et al. 2023. - We follow Montgomery form as described in the above paper; namely, r = 2^bitsize. - """ - - bitsize: SymbolicInt - modulus: Optional[SymbolicInt] = None - - @cached_property - def _bit_encoding(self) -> _MontgomeryUInt: - return _MontgomeryUInt(self.bitsize, self.modulus) - - def montgomery_inverse(self, xm: int) -> int: - """Returns the modular inverse of an integer in montgomery form. - - Args: - xm: An integer in montgomery form. - """ - return self._bit_encoding.montgomery_inverse(xm) - - def montgomery_product(self, xm: int, ym: int) -> int: - """Returns the modular product of two integers in montgomery form. - - Args: - xm: The first montgomery form integer for the product. - ym: The second montgomery form integer for the product. - """ - return self._bit_encoding.montgomery_product(xm, ym) - - def montgomery_to_uint(self, xm: int) -> int: - """Converts an integer in montgomery form to a normal form integer. - - Args: - xm: An integer in montgomery form. - """ - return self._bit_encoding.montgomery_to_uint(xm) - - def uint_to_montgomery(self, x: int) -> int: - """Converts an integer into montgomery form. - - Args: - x: An integer. - """ - return self._bit_encoding.uint_to_montgomery(x) - - def __str__(self): - if self.modulus is not None: - modstr = f', {self.modulus}' - else: - modstr = '' - return f'{self.__class__.__name__}({self.bitsize}{modstr})' - - -@attrs.frozen -class CMontgomeryUInt(CDType[int]): - r"""Montgomery form of an unsigned integer of a given width bitsize which wraps around upon - overflow. - - This is a classical version of QMontgomeryUInt. See the documentation for that class. - """ - - bitsize: SymbolicInt - modulus: Optional[SymbolicInt] = None - - @cached_property - def _bit_encoding(self) -> _MontgomeryUInt: - return _MontgomeryUInt(self.bitsize, self.modulus) - - def __str__(self): - if self.modulus is not None: - modstr = f', {self.modulus}' - else: - modstr = '' - return f'{self.__class__.__name__}({self.bitsize}{modstr})' - - -def _poly_converter(p) -> Union['galois.Poly', None]: - import galois - - if p is None: - return None - if isinstance(p, galois.Poly): - return p - return galois.Poly.Degrees(p) - - -@attrs.frozen -class _GF(BitEncoding['galois.FieldArray']): - r"""Galois Field type to represent elements of a finite field.""" - - characteristic: SymbolicInt - degree: SymbolicInt - irreducible_poly: Optional['galois.Poly'] = attrs.field(converter=_poly_converter) - - @irreducible_poly.default - def _irreducible_poly_default(self): - if is_symbolic(self.characteristic, self.degree): - return None - - from galois import GF - - return GF( # type: ignore[call-overload] - int(self.characteristic), int(self.degree), compile='python-calculate' - ).irreducible_poly - - @cached_property - def order(self) -> SymbolicInt: - return self.characteristic**self.degree - - @cached_property - def bitsize(self) -> SymbolicInt: - """Bitsize of qubit register required to represent a single instance of this data type.""" - return bit_length(self.order - 1) - - def get_domain(self) -> Iterable[Any]: - yield from self.gf_type.elements - - @cached_property - def _uint_encoder(self) -> _UInt: - return _UInt(self.bitsize) - - @cached_property - def gf_type(self): - from galois import GF - - poly = self.irreducible_poly if self.degree > 1 else None - - return GF( # type: ignore[call-overload] - int(self.characteristic), - int(self.degree), - irreducible_poly=poly, - verify=False, - repr='poly', - compile='python-calculate', - ) - - def to_bits(self, x) -> List[int]: - self.assert_valid_val(x) - return self._uint_encoder.to_bits(int(x)) - - def from_bits(self, bits: Sequence[int]): - return self.gf_type(self._uint_encoder.from_bits(bits)) - - def from_bits_array(self, bits_array: NDArray[np.uint8]): - return self.gf_type(self._uint_encoder.from_bits_array(bits_array)) - - def assert_valid_val(self, val: Any, debug_str: str = 'val'): - if not isinstance(val, self.gf_type): - raise ValueError(f"{debug_str} should be a {self.gf_type}, not {val!r}") - - def assert_valid_val_array(self, val_array: NDArray[Any], debug_str: str = 'val'): - if np.any(val_array < 0): - raise ValueError(f"Negative classical values encountered in {debug_str}") - if np.any(val_array >= self.order): - raise ValueError(f"Too-large classical values encountered in {debug_str}") - - -@attrs.frozen -class QGF(QDType['galois.FieldArray']): - r"""Galois Field type to represent elements of a finite field. - - A Finite Field or Galois Field is a field that contains finite number of elements. The order - of a finite field is the number of elements in the field, which is either a prime number or - a prime power. For every prime number $p$ and every positive integer $m$ there are fields of - order $p^m$, all of which are isomorphic. When m=1, the finite field of order p can be - constructed via integers modulo p. - - Elements of a Galois Field $GF(p^m)$ may be conveniently viewed as polynomials - $a_{0} + a_{1}x + ... + a_{m−1}x_{m−1}$, where $a_0, a_1, ..., a_{m−1} \in F(p)$. - $GF(p^m)$ addition is defined as the component-wise (polynomial) addition over F(p) and - multiplication is defined as polynomial multiplication modulo an irreducible polynomial of - degree $m$. The selection of the specific irreducible polynomial affects the representation - of the given field, but all fields of a fixed size are isomorphic. - - The data type uses the [Galois library](https://mhostetter.github.io/galois/latest/) to - perform arithmetic over Galois Fields. By default, the Conway polynomial $C_{p, m}$ is used - as the irreducible polynomial. - - Args: - characteristic: The characteristic $p$ of the field $GF(p^m)$. - The characteristic must be prime. - degree: The degree $m$ of the field $GF(p^{m})$. The degree must be a positive integer. - irreducible_poly: Optional `galois.Poly` instance that defines the field arithmetic. - This parameter is passed to `galois.GF(..., irreducible_poly=irreducible_poly, verify=False)`. - - References: - [Finite Field](https://en.wikipedia.org/wiki/Finite_field) - - [Intro to Prime Fields](https://mhostetter.github.io/galois/latest/tutorials/intro-to-prime-fields/) - - [Intro to Extension Fields](https://mhostetter.github.io/galois/latest/tutorials/intro-to-extension-fields/) - """ - - characteristic: SymbolicInt - degree: SymbolicInt - irreducible_poly: Optional['galois.Poly'] = attrs.field(converter=_poly_converter) - - @irreducible_poly.default - def _irreducible_poly_default(self): - if is_symbolic(self.characteristic, self.degree): - return None - - from galois import GF - - return GF( # type: ignore[call-overload] - int(self.characteristic), int(self.degree), compile='python-calculate' - ).irreducible_poly - - @cached_property - def _bit_encoding(self) -> _GF: - return _GF( - characteristic=self.characteristic, - degree=self.degree, - irreducible_poly=self.irreducible_poly, - ) - - @property - def order(self) -> SymbolicInt: - return self._bit_encoding.order - - @property - def bitsize(self) -> SymbolicInt: - """Bitsize of qubit register required to represent a single instance of this data type.""" - return self._bit_encoding.bitsize - - @property - def gf_type(self): - return self._bit_encoding.gf_type - - def is_symbolic(self) -> bool: - return is_symbolic(self.characteristic, self.order) - - def iteration_length_or_zero(self) -> SymbolicInt: - return self.order - - def __str__(self): - return f'QGF({self.characteristic}**{self.degree})' - - -@attrs.frozen -class CGF(CDType['galois.FieldArray']): - r"""Galois Field classical type to represent elements of a finite field. - - See QGF for documentation. - """ - - characteristic: SymbolicInt - degree: SymbolicInt - irreducible_poly: Optional['galois.Poly'] = attrs.field(converter=_poly_converter) - - @irreducible_poly.default - def _irreducible_poly_default(self): - if is_symbolic(self.characteristic, self.degree): - return None - - from galois import GF - - return GF( # type: ignore[call-overload] - int(self.characteristic), int(self.degree), compile='python-calculate' - ).irreducible_poly - - @cached_property - def _bit_encoding(self) -> _GF: - return _GF( - characteristic=self.characteristic, - degree=self.degree, - irreducible_poly=self.irreducible_poly, - ) - - @property - def order(self) -> SymbolicInt: - return self._bit_encoding.order - - @property - def bitsize(self) -> SymbolicInt: - """Bitsize of qubit register required to represent a single instance of this data type.""" - return self._bit_encoding.bitsize - - @property - def gf_type(self): - return self._bit_encoding.gf_type - - def is_symbolic(self) -> bool: - return is_symbolic(self.characteristic, self.order) - - def iteration_length_or_zero(self) -> SymbolicInt: - return self.order - - def __str__(self): - return f'CGF({self.characteristic}**{self.degree})' - - -@attrs.frozen -class _GFPoly(BitEncoding): - r"""Univariate Polynomials with coefficients in a Galois Field GF($p^m$). - - Args: - degree: The degree $n$ of the univariate polynomial $f(x)$ represented by this type. - gf: An instance of `_GF` that represents the galois field $GF(p^m)$ over which the - univariate polynomial $f(x)$ is defined. - - """ - - degree: SymbolicInt - gf: _GF - - @cached_property - def bitsize(self) -> SymbolicInt: - return self.gf.bitsize * (self.degree + 1) - - def get_domain(self) -> Iterable[Any]: - """Yields all possible classical (computational basis state) values representable - by this type.""" - from galois import Poly - - for it in itertools.product(self.gf.gf_type.elements, repeat=(self.degree + 1)): - yield Poly(self.gf.gf_type(it), field=self.gf.gf_type) - - def to_gf_coefficients(self, f_x: 'galois.Poly') -> 'galois.Array': - """Returns a big-endian array of coefficients of the polynomial f(x).""" - f_x_coeffs = self.gf.gf_type.Zeros(self.degree + 1) - f_x_coeffs[self.degree - f_x.degree :] = f_x.coeffs - return f_x_coeffs - - def from_gf_coefficients(self, f_x: 'galois.Array') -> 'galois.Poly': - """Expects a big-endian array of coefficients that represent a polynomial f(x).""" - import galois - - return galois.Poly(f_x, field=self.gf.gf_type) - - def to_bits(self, x) -> List[int]: - """Returns individual bits corresponding to binary representation of x""" - import galois - - self.assert_valid_val(x) - assert isinstance(x, galois.Poly) - return self.gf.to_bits_array(self.to_gf_coefficients(x)).reshape(-1).tolist() - - def from_bits(self, bits: Sequence[int]): - """Combine individual bits to form x""" - reshaped_bits = np.array(bits).reshape((int(self.degree) + 1, int(self.gf.bitsize))) - return self.from_gf_coefficients(self.gf.from_bits_array(reshaped_bits)) # type: ignore - - def assert_valid_val(self, val: Any, debug_str: str = 'val'): - """Raises an exception if `val` is not a valid classical value for this type. - - Args: - val: A classical value that should be in the domain of this QDType. - debug_str: Optional debugging information to use in exception messages. - """ - import galois - - if not isinstance(val, galois.Poly): - raise ValueError(f"{debug_str} should be a {galois.Poly}, not {val!r}") - if val.field is not self.gf.gf_type: - raise ValueError( - f"{debug_str} should be defined over {self.gf.gf_type}, not {val.field}" - ) - if val.degree > self.degree: - raise ValueError(f"{debug_str} should have a degree <= {self.degree}, not {val.degree}") - - -@attrs.frozen -class QGFPoly(QDType): - r"""Quantum Univariate Polynomials with coefficients in a Galois Field GF($p^m$). - - This data type represents a degree-$n$ univariate polynomials - $f(x)=\sum_{i=0}^{n} a_i x^{i}$ where the coefficients $a_{i}$ of the polynomial - belong to a Galois Field $GF(p^{m})$. - - The data type uses the [Galois library](https://mhostetter.github.io/galois/latest/) to - perform arithmetic over polynomials defined over Galois Fields using the - [galois.Poly](https://mhostetter.github.io/galois/latest/api/galois.Poly/). - - Args: - degree: The degree $n$ of the univariate polynomial $f(x)$ represented by this type. - qgf: An instance of `QGF` that represents the galois field $GF(p^m)$ over which the - univariate polynomial $f(x)$ is defined. - - References: - [Polynomials over finite fields](https://mhostetter.github.io/galois/latest/api/galois.Poly/). - `galois` documentation. - - - [Polynomial Arithmetic](https://mhostetter.github.io/galois/latest/basic-usage/poly-arithmetic/). - `galois` documentation. - """ - - degree: SymbolicInt - qgf: QGF - - @cached_property - def _bit_encoding(self) -> _GFPoly: - return _GFPoly(self.degree, self.qgf._bit_encoding) - - @property - def bitsize(self) -> SymbolicInt: - return self._bit_encoding.bitsize - - def to_gf_coefficients(self, f_x: 'galois.Poly') -> 'galois.Array': - """Returns a big-endian array of coefficients of the polynomial f(x).""" - return self._bit_encoding.to_gf_coefficients(f_x) - - def from_gf_coefficients(self, f_x: 'galois.Array') -> 'galois.Poly': - """Expects a big-endian array of coefficients that represent a polynomial f(x).""" - return self._bit_encoding.from_gf_coefficients(f_x) - - @cached_property - def _quint_equivalent(self) -> QUInt: - return QUInt(self.num_qubits) - - def is_symbolic(self) -> bool: - return is_symbolic(self.degree, self.qgf) - - def iteration_length_or_zero(self) -> SymbolicInt: - return self.qgf.order - - def __str__(self): - return f'QGFPoly({self.degree}, {self.qgf!s})' - - -@attrs.frozen -class CGFPoly(CDType): - r"""Classical Univariate Polynomials with coefficients in a Galois Field GF($p^m$). - - This is a "classical" version of QGFPoly. - """ - - degree: SymbolicInt - qgf: QGF - - @cached_property - def _bit_encoding(self) -> _GFPoly: - return _GFPoly(self.degree, self.qgf._bit_encoding) - - def is_symbolic(self) -> bool: - return is_symbolic(self.degree, self.qgf) - - def iteration_length_or_zero(self) -> SymbolicInt: - return self.qgf.order - - def __str__(self): - return f'CGFPoly({self.degree}, {self.qgf!s})' - - -_QAnyInt = (QInt, QUInt, BQUInt, QMontgomeryUInt) -_QAnyUInt = (QUInt, BQUInt, QMontgomeryUInt, QGF) - - -class QDTypeCheckingSeverity(Enum): - """The level of type checking to enforce""" - - LOOSE = 0 - """Allow most type conversions between QAnyInt, QFxp and QAny.""" - - ANY = 1 - """Disallow numeric type conversions but allow QAny and single bit conversion.""" - - STRICT = 2 - """Strictly enforce type checking between registers. Only single bit conversions are allowed.""" - - -def _check_uint_fxp_consistent(a: Union[QUInt, BQUInt, QMontgomeryUInt, QGF], b: QFxp) -> bool: - """A uint / qfxp is consistent with a whole or totally fractional unsigned QFxp.""" - if b.signed: - return False - return a.num_qubits == b.num_qubits and (b.num_frac == 0 or b.num_int == 0) - - -def check_dtypes_consistent( - dtype_a: QCDType, - dtype_b: QCDType, - type_checking_severity: QDTypeCheckingSeverity = QDTypeCheckingSeverity.LOOSE, -) -> bool: - """Check if two types are consistent given our current definition on consistent types. - - Args: - dtype_a: The dtype to check against the reference. - dtype_b: The reference dtype. - type_checking_severity: Severity of type checking to perform. - - Returns: - True if the types are consistent. - """ - same_dtypes = dtype_a == dtype_b - same_n_qubits = dtype_a.num_qubits == dtype_b.num_qubits - if same_dtypes: - # Same types are always ok. - return True - elif dtype_a.num_qubits == 1 and same_n_qubits: - # Single qubit types are ok. - return True - if type_checking_severity == QDTypeCheckingSeverity.STRICT: - return False - if isinstance(dtype_a, QAny) or isinstance(dtype_b, QAny): - # QAny -> any dtype and any dtype -> QAny - return same_n_qubits - if type_checking_severity == QDTypeCheckingSeverity.ANY: - return False - if isinstance(dtype_a, _QAnyInt) and isinstance(dtype_b, _QAnyInt): - # A subset of the integers should be freely interchangeable. - return same_n_qubits - elif isinstance(dtype_a, _QAnyUInt) and isinstance(dtype_b, QFxp): - # unsigned Fxp which is wholly an integer or < 1 part is a uint. - return _check_uint_fxp_consistent(dtype_a, dtype_b) - elif isinstance(dtype_b, _QAnyUInt) and isinstance(dtype_a, QFxp): - # unsigned Fxp which is wholy an integer or < 1 part is a uint. - return _check_uint_fxp_consistent(dtype_b, dtype_a) - else: - return False +# pylint: disable=wildcard-import,unused-wildcard-import +from qualtran.dtype import * diff --git a/qualtran/_infra/data_types_test.py b/qualtran/_infra/data_types_test.py index 355ed49d57..aadbe3d24c 100644 --- a/qualtran/_infra/data_types_test.py +++ b/qualtran/_infra/data_types_test.py @@ -37,7 +37,8 @@ QMontgomeryUInt, QUInt, ) -from qualtran._infra.data_types import _Fxp, _QAnyInt +from qualtran.dtype._fxp import _Fxp +from qualtran.dtype.testing import _QAnyInt from qualtran.symbolics import ceil, is_symbolic, log2 diff --git a/qualtran/dtype/__init__.py b/qualtran/dtype/__init__.py index bb233e268a..0f557b9765 100644 --- a/qualtran/dtype/__init__.py +++ b/qualtran/dtype/__init__.py @@ -15,52 +15,28 @@ """Data type objects for your quantum programs.""" -from qualtran._infra.data_types import ( - QCDType, - CDType, - QDType, - QAny, - QBit, - CBit, - QInt, - CInt, - QIntOnesComp, - CIntOnesComp, - QUInt, - CUInt, - BQUInt, - BCUInt, - QFxp, - CFxp, - QMontgomeryUInt, - CMontgomeryUInt, - QGF, - CGF, - QGFPoly, - CGFPoly, -) +from ._base import QCDType, CDType, QDType, ShapedQCDType, BitEncoding + +from ._any import QAny + +from ._bit import QBit, CBit + +from ._uint import QUInt, CUInt + +from ._int import QInt, CInt + +from ._int_ones_complement import QIntOnesComp, CIntOnesComp -__all__ = [ - 'QCDType', - 'CDType', - 'QDType', - 'QAny', - 'QBit', - 'CBit', - 'QInt', - 'CInt', - 'QIntOnesComp', - 'CIntOnesComp', - 'QUInt', - 'CUInt', - 'BQUInt', - 'BCUInt', - 'QFxp', - 'CFxp', - 'QMontgomeryUInt', - 'CMontgomeryUInt', - 'QGF', - 'CGF', - 'QGFPoly', - 'CGFPoly', -] +from ._buint import BQUInt, BCUInt + +from ._fxp import QFxp, CFxp + +from ._montgomery_uint import QMontgomeryUInt, CMontgomeryUInt + +from .gf import QGF, CGF, QGFPoly, CGFPoly + +from .testing import ( + check_dtypes_consistent, + QDTypeCheckingSeverity, + assert_to_and_from_bits_array_consistent, +) diff --git a/qualtran/dtype/_any.py b/qualtran/dtype/_any.py new file mode 100644 index 0000000000..428e4122c5 --- /dev/null +++ b/qualtran/dtype/_any.py @@ -0,0 +1,50 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Any, Iterable + +import attrs +from numpy.typing import NDArray + +from qualtran.symbolics import is_symbolic, SymbolicInt + +from ._base import BitEncoding, QDType +from ._uint import _UInt + + +@attrs.frozen +class QAny(QDType[Any]): + """Opaque bag-of-qubits type.""" + + bitsize: SymbolicInt + + @property + def _bit_encoding(self) -> BitEncoding[Any]: + return _UInt(self.bitsize) + + def __attrs_post_init__(self): + if is_symbolic(self.bitsize): + return + + if not isinstance(self.bitsize, int): + raise ValueError(f"Bad bitsize for QAny: {self.bitsize}") + + def get_classical_domain(self) -> Iterable[Any]: + raise TypeError(f"Ambiguous domain for {self}. Please use a more specific type.") + + def assert_valid_classical_val(self, val: Any, debug_str: str = 'val'): + pass + + def assert_valid_classical_val_array(self, val_array: NDArray, debug_str: str = 'val'): + pass diff --git a/qualtran/dtype/_any_test.py b/qualtran/dtype/_any_test.py new file mode 100644 index 0000000000..f897f34c8f --- /dev/null +++ b/qualtran/dtype/_any_test.py @@ -0,0 +1,22 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from qualtran.dtype import assert_to_and_from_bits_array_consistent, QAny + + +def test_qany_to_and_from_bits(): + assert list(QAny(4).to_bits(10)) == [1, 0, 1, 0] + + assert_to_and_from_bits_array_consistent(QAny(4), range(16)) diff --git a/qualtran/dtype/_base.py b/qualtran/dtype/_base.py new file mode 100644 index 0000000000..b889fced93 --- /dev/null +++ b/qualtran/dtype/_base.py @@ -0,0 +1,289 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import abc +import warnings +from typing import cast, Generic, Iterable, List, Sequence, Tuple, TypeVar + +import attrs +import numpy as np +from numpy.typing import NDArray + +from qualtran.symbolics import is_symbolic, SymbolicInt + +T = TypeVar('T') + + +class BitEncoding(Generic[T], metaclass=abc.ABCMeta): + @property + @abc.abstractmethod + def bitsize(self) -> SymbolicInt: ... + + @abc.abstractmethod + def get_domain(self) -> Iterable[T]: + """Yields all possible classical (computational basis state) values representable + by this type.""" + + @abc.abstractmethod + def to_bits(self, x: T) -> List[int]: + """Yields individual bits corresponding to binary representation of x""" + + def to_bits_array(self, x_array: NDArray) -> NDArray[np.uint8]: + """Yields an NDArray of bits corresponding to binary representations of the input elements. + + Often, converting an array can be performed faster than converting each element individually. + This operation accepts any NDArray of values, and the output array satisfies + `output_shape = input_shape + (self.bitsize,)`. + """ + return np.vectorize( + lambda x: np.asarray(self.to_bits(x), dtype=np.uint8), signature='()->(n)' + )(x_array) + + @abc.abstractmethod + def from_bits(self, bits: Sequence[int]) -> T: + """Combine individual bits to form x""" + + def from_bits_array(self, bits_array: NDArray[np.uint8]) -> NDArray: + """Combine individual bits to form classical values. + + Often, converting an array can be performed faster than converting each element individually. + This operation accepts any NDArray of bits such that the last dimension equals `self.bitsize`, + and the output array satisfies `output_shape = input_shape[:-1]`. + """ + return np.vectorize(self.from_bits, signature='(n)->()')(bits_array) + + @abc.abstractmethod + def assert_valid_val(self, val: T, debug_str: str = 'val') -> None: + """Raises an exception if `val` is not a valid classical value for this type. + + Args: + val: A classical value that should be in the domain of this QDType. + debug_str: Optional debugging information to use in exception messages. + """ + + def assert_valid_val_array(self, val_array: NDArray, debug_str: str = 'val') -> None: + """Raises an exception if `val_array` is not a valid array of classical values + for this type. + + Often, validation on an array can be performed faster than validating each element + individually. + + Args: + val_array: A numpy array of classical values. Each value should be in the domain + of this QDType. + debug_str: Optional debugging information to use in exception messages. + """ + for val in val_array.reshape(-1): + self.assert_valid_val(val, debug_str=debug_str) + + +@attrs.frozen +class _BitEncodingShim(BitEncoding[T]): + """Shim an old-style QDType to follow the BitEncoding interface. + + Before the introduction of classical data types (QCDType and CDType), QDType classes + described how to encode values into bits (for classical simulation) and qubits (for + quantum programs). The encoding schemes don't care whether the substrate is bits or + qubits but the CompositeBloq type-checking does care; so we've moved the encoding + logic to descendants of `BitEncoding`. Each `QCDType` "has a" BitEncoding and "is a" + quantum data type or classical data type. + + This shim uses encoding logic found in the methods of an old-style QDType to satisfy + the BitEncoding interface for backwards compatibility. Developers with custom QDTypes + should port their custom data types to use a BitEncoding. + + """ + + qdtype: 'QDType[T]' + + @property + def bitsize(self) -> SymbolicInt: + return self.qdtype.num_qubits + + def get_domain(self) -> Iterable[T]: + yield from self.qdtype.get_classical_domain() + + def to_bits(self, x: T) -> List[int]: + return self.qdtype.to_bits(x) + + def to_bits_array(self, x_array: NDArray) -> NDArray[np.uint8]: + return np.vectorize( + lambda x: np.asarray(self.qdtype.to_bits(x), dtype=np.uint8), signature='()->(n)' + )(x_array) + + def from_bits(self, bits: Sequence[int]) -> T: + return self.qdtype.from_bits(bits) + + def from_bits_array(self, bits_array: NDArray[np.uint8]) -> NDArray: + return np.vectorize(self.qdtype.from_bits, signature='(n)->()')(bits_array) + + def assert_valid_val(self, val: T, debug_str: str = 'val') -> None: + return self.qdtype.assert_valid_classical_val(val, debug_str=debug_str) + + def assert_valid_val_array(self, val_array: NDArray, debug_str: str = 'val') -> None: + for val in val_array.reshape(-1): + self.qdtype.assert_valid_classical_val(val) + + +@attrs.frozen +class ShapedQCDType: + qcdtype: 'QCDType' + shape: Tuple[int, ...] = attrs.field( + default=tuple(), converter=lambda v: (v,) if isinstance(v, int) else tuple(v) + ) + + +class QCDType(Generic[T], metaclass=abc.ABCMeta): + """The abstract interface for quantum/classical quantum computing data types.""" + + @property + @abc.abstractmethod + def _bit_encoding(self) -> BitEncoding[T]: + """The class describing how bits are encoded in this datatype.""" + + @property + def num_bits(self) -> int: + """Number of bits (quantum and classical) required to represent a single instance of + this data type.""" + return self.num_qubits + self.num_cbits + + @property + @abc.abstractmethod + def num_qubits(self) -> int: + """Number of qubits required to represent a single instance of this data type.""" + + @property + @abc.abstractmethod + def num_cbits(self) -> int: + """Number of classical bits required to represent a single instance of this data type.""" + + def get_classical_domain(self) -> Iterable[T]: + """Yields all possible classical (computational basis state) values representable + by this type.""" + yield from self._bit_encoding.get_domain() + + def to_bits(self, x: T) -> List[int]: + """Yields individual bits corresponding to binary representation of x""" + return self._bit_encoding.to_bits(x) + + def to_bits_array(self, x_array: NDArray) -> NDArray[np.uint8]: + """Yields an NDArray of bits corresponding to binary representations of the input elements. + + Often, converting an array can be performed faster than converting each element individually. + This operation accepts any NDArray of values, and the output array satisfies + `output_shape = input_shape + (self.bitsize,)`. + """ + return self._bit_encoding.to_bits_array(x_array) + + def from_bits(self, bits: Sequence[int]) -> T: + """Combine individual bits to form x""" + return self._bit_encoding.from_bits(bits) + + def from_bits_array(self, bits_array: NDArray[np.uint8]) -> NDArray: + """Combine individual bits to form classical values. + + Often, converting an array can be performed faster than converting each element individually. + This operation accepts any NDArray of bits such that the last dimension equals `self.bitsize`, + and the output array satisfies `output_shape = input_shape[:-1]`. + """ + return self._bit_encoding.from_bits_array(bits_array) + + def assert_valid_classical_val(self, val: T, debug_str: str = 'val') -> None: + """Raises an exception if `val` is not a valid classical value for this type. + + Args: + val: A classical value that should be in the domain of this QDType. + debug_str: Optional debugging information to use in exception messages. + """ + return self._bit_encoding.assert_valid_val(val=val, debug_str=debug_str) + + def assert_valid_classical_val_array(self, val_array: NDArray, debug_str: str = 'val') -> None: + """Raises an exception if `val_array` is not a valid array of classical values + for this type. + + Often, validation on an array can be performed faster than validating each element + individually. + + Args: + val_array: A numpy array of classical values. Each value should be in the domain + of this QDType. + debug_str: Optional debugging information to use in exception messages. + """ + return self._bit_encoding.assert_valid_val_array(val_array=val_array, debug_str=debug_str) + + def is_symbolic(self) -> bool: + """Returns True if this dtype is parameterized with symbolic objects.""" + return is_symbolic(self._bit_encoding.bitsize) + + def iteration_length_or_zero(self) -> SymbolicInt: + """Safe version of iteration length. + + Returns the iteration_length if the type has it or else zero. + """ + # TODO: remove https://github.com/quantumlib/Qualtran/issues/1716 + return getattr(self, 'iteration_length', 0) + + def __getitem__(self, shape): + """QInt(8)[20] returns a size-20 array of QInt(8)""" + return ShapedQCDType(qcdtype=self, shape=shape) + + @classmethod + def _pkg_(cls): + return 'qualtran' + + def __str__(self): + return f'{self.__class__.__name__}({self.num_bits})' + + +class QDType(QCDType[T], metaclass=abc.ABCMeta): + """The abstract interface for quantum data types.""" + + @property + def _bit_encoding(self) -> BitEncoding[T]: + """The class describing how bits are encoded in this datatype.""" + warnings.warn( + f"{self} must provide a BitEncoding. " + f"This shim will become an error in the future. " + f"Omitting this may cause infinite loops.", + DeprecationWarning, + ) + return _BitEncodingShim(self) + + @property + def num_qubits(self) -> int: + return cast(int, self._bit_encoding.bitsize) + + @property + def num_cbits(self) -> int: + """QDTypes have zero qubits.""" + return 0 + + def __str__(self): + return f'{self.__class__.__name__}({self.num_qubits})' + + +class CDType(QCDType[T], metaclass=abc.ABCMeta): + """The abstract interface for classical data types.""" + + @property + def num_qubits(self) -> int: + """CDTypes have zero qubits.""" + return 0 + + @property + def num_cbits(self) -> int: + return cast(int, self._bit_encoding.bitsize) + + def __str__(self): + return f'{self.__class__.__name__}({self.num_cbits})' diff --git a/qualtran/dtype/_base_test.py b/qualtran/dtype/_base_test.py new file mode 100644 index 0000000000..f8a8ceef16 --- /dev/null +++ b/qualtran/dtype/_base_test.py @@ -0,0 +1,150 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Any, Iterable, List, Sequence + +import attrs +import galois +import numpy as np +import pytest +from numpy.typing import NDArray + +from qualtran.dtype import BQUInt, QBit, QDType, QGF, QGFPoly, QInt, QUInt +from qualtran.symbolics import is_symbolic + + +@pytest.mark.parametrize('qdtype', [QBit(), QInt(4), QUInt(4), BQUInt(3, 5)]) +def test_domain_and_validation(qdtype: QDType): + for v in qdtype.get_classical_domain(): + qdtype.assert_valid_classical_val(v) + + +@pytest.mark.parametrize( + 'qdtype', + [ + QBit(), + QInt(4), + QUInt(4), + BQUInt(3, 5), + QGF(2, 8), + QGFPoly(4, QGF(characteristic=2, degree=2)), + ], +) +def test_domain_and_validation_arr(qdtype: QDType): + arr = np.array(list(qdtype.get_classical_domain())) + qdtype.assert_valid_classical_val_array(arr) + + +def test_validation_errs(): + with pytest.raises(ValueError): + QBit().assert_valid_classical_val(-1) + + with pytest.raises(ValueError): + QBit().assert_valid_classical_val('|0>') # type: ignore[arg-type] + + with pytest.raises(ValueError): + QUInt(3).assert_valid_classical_val(8) + + with pytest.raises(ValueError): + BQUInt(3, 5).assert_valid_classical_val(-1) + + with pytest.raises(ValueError): + BQUInt(3, 5).assert_valid_classical_val(6) + + with pytest.raises(ValueError): + QInt(4).assert_valid_classical_val(-9) + + with pytest.raises(ValueError): + QUInt(3).assert_valid_classical_val(-1) + + with pytest.raises(ValueError): + QUInt(3).assert_valid_classical_val(-1) + + with pytest.raises(ValueError): + QGF(2, 8).assert_valid_classical_val(2**8) # type: ignore[arg-type] + + with pytest.raises(ValueError): + qgf = QGF(2, 3) + poly = galois.Poly(qgf.gf_type([1, 2, 3, 4, 5, 6, 7]), field=qgf.gf_type) + QGFPoly(4, qgf).assert_valid_classical_val(poly) + + +def test_validate_arrays(): + rs = np.random.RandomState(52) + arr = rs.choice([0, 1], size=(23, 4)) + QBit().assert_valid_classical_val_array(arr) + + arr = rs.choice([-1, 1], size=(23, 4)) + with pytest.raises(ValueError): + QBit().assert_valid_classical_val_array(arr) + + +@attrs.frozen +class LegacyBQUInt(QDType): + """For testing: this doesn't use a `BitEncoding`, so it will go via `_BitEncodingShim`""" + + bitsize: int + iteration_length: int = attrs.field() + + @iteration_length.default + def _default_iteration_length(self): + return 2**self.bitsize + + def is_symbolic(self) -> bool: + return is_symbolic(self.bitsize, self.iteration_length) + + @property + def num_qubits(self): + return self.bitsize + + def get_classical_domain(self) -> Iterable[Any]: + if isinstance(self.iteration_length, int): + return range(0, self.iteration_length) + raise ValueError(f'Classical Domain not defined for expression: {self.iteration_length}') + + def assert_valid_classical_val(self, val: int, debug_str: str = 'val'): + if not isinstance(val, (int, np.integer)): + raise ValueError(f"{debug_str} should be an integer, not {val!r}") + if val < 0: + raise ValueError(f"Negative classical value encountered in {debug_str}") + if val >= self.iteration_length: + raise ValueError(f"Too-large classical value encountered in {debug_str}") + + def to_bits(self, x: int) -> List[int]: + """Yields individual bits corresponding to binary representation of x""" + self.assert_valid_classical_val(x, debug_str='val') + return QUInt(self.bitsize).to_bits(x) + + def from_bits(self, bits: Sequence[int]) -> int: + """Combine individual bits to form x""" + return QUInt(self.bitsize).from_bits(bits) + + def assert_valid_classical_val_array( + self, val_array: NDArray[np.integer], debug_str: str = 'val' + ): + if np.any(val_array < 0): + raise ValueError(f"Negative classical values encountered in {debug_str}") + if np.any(val_array >= self.iteration_length): + raise ValueError(f"Too-large classical values encountered in {debug_str}") + + def __str__(self): + return f'BQUInt({self.bitsize}, {self.iteration_length})' + + +def test_legacy_qcdtype(): + t = LegacyBQUInt(8, 230) + assert str(t) == 'BQUInt(8, 230)' + assert t._bit_encoding.to_bits(5) == QUInt(8).to_bits(5) + t.assert_valid_classical_val(229) + with pytest.raises(ValueError): + t.assert_valid_classical_val(230) diff --git a/qualtran/dtype/_bit.py b/qualtran/dtype/_bit.py new file mode 100644 index 0000000000..8baa9e1c67 --- /dev/null +++ b/qualtran/dtype/_bit.py @@ -0,0 +1,87 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from functools import cached_property +from typing import Iterable, List, Sequence + +import attrs +import numpy as np +from numpy.typing import NDArray + +from ._base import BitEncoding, CDType, QDType + + +@attrs.frozen +class _Bit(BitEncoding[int]): + """A single quantum or classical bit. The smallest addressable unit of data. + + Use either `QBit()` or `CBit()` for quantum or classical implementations, respectively. + """ + + @property + def bitsize(self) -> int: + return 1 + + def get_domain(self) -> Iterable[int]: + yield from (0, 1) + + def assert_valid_val(self, val: int, debug_str: str = 'val'): + if not (val == 0 or val == 1): + raise ValueError(f"Bad bit value: {val} in {debug_str}") + + def to_bits(self, x: int) -> List[int]: + """Yields individual bits corresponding to binary representation of x""" + self.assert_valid_val(x) + return [int(x)] + + def from_bits(self, bits: Sequence[int]) -> int: + """Combine individual bits to form x""" + assert len(bits) == 1 + return bits[0] + + def assert_valid_val_array( + self, val_array: NDArray[np.integer], debug_str: str = 'val' + ) -> None: + if not np.all((val_array == 0) | (val_array == 1)): + raise ValueError(f"Bad bit value array in {debug_str}") + + +@attrs.frozen +class QBit(QDType[int]): + """A single qubit. The smallest addressable unit of quantum data.""" + + @cached_property + def _bit_encoding(self) -> BitEncoding[int]: + return _Bit() + + def is_symbolic(self) -> bool: + return False + + def __str__(self) -> str: + return 'QBit()' + + +@attrs.frozen +class CBit(CDType[int]): + """A single classical bit. The smallest addressable unit of classical data.""" + + @cached_property + def _bit_encoding(self) -> BitEncoding[int]: + return _Bit() + + def is_symbolic(self) -> bool: + return False + + def __str__(self) -> str: + return 'CBit()' diff --git a/qualtran/dtype/_bit_test.py b/qualtran/dtype/_bit_test.py new file mode 100644 index 0000000000..2dc6ddb15b --- /dev/null +++ b/qualtran/dtype/_bit_test.py @@ -0,0 +1,40 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from qualtran.dtype import assert_to_and_from_bits_array_consistent, CBit, QBit + + +def test_bit(): + qbit = QBit() + assert qbit.num_qubits == 1 + assert qbit.num_cbits == 0 + assert qbit.num_bits == 1 + assert str(qbit) == 'QBit()' + + cbit = CBit() + assert cbit.num_cbits == 1 + assert cbit.num_qubits == 0 + assert cbit.num_bits == 1 + assert str(CBit()) == 'CBit()' + + +def test_qbit_to_and_from_bits(): + assert list(QBit().to_bits(0)) == [0] + assert list(QBit().to_bits(1)) == [1] + with pytest.raises(ValueError): + QBit().to_bits(2) + + assert_to_and_from_bits_array_consistent(QBit(), [0, 1]) diff --git a/qualtran/dtype/_buint.py b/qualtran/dtype/_buint.py new file mode 100644 index 0000000000..ef1d9cc00b --- /dev/null +++ b/qualtran/dtype/_buint.py @@ -0,0 +1,190 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Iterable, List, Sequence + +import attrs +import numpy as np +from numpy.typing import NDArray + +from qualtran.symbolics import is_symbolic, SymbolicInt + +from ._base import BitEncoding, CDType, QDType +from ._uint import _UInt + + +@attrs.frozen +class _BUInt(BitEncoding[int]): + """Unsigned integer whose values are bounded within a range. + + Args: + bitsize: The number of bits used to represent the integer. + bound: The bound (exclusive) + """ + + bitsize: SymbolicInt + bound: SymbolicInt + + def __attrs_post_init__(self): + if is_symbolic(self.bitsize) or is_symbolic(self.bound): + return + + if self.bound > 2**self.bitsize: + raise ValueError( + "BUInt value bound is too large for given bitsize. " + f"{self.bound} vs {2 ** self.bitsize}" + ) + + def get_domain(self) -> Iterable[int]: + if isinstance(self.bound, int): + return range(0, self.bound) + raise ValueError(f'Classical domain not defined for {self}') + + def assert_valid_val(self, val: int, debug_str: str = 'val') -> None: + if not isinstance(val, (int, np.integer)): + raise ValueError(f"{debug_str} should be an integer, not {val!r}") + if val < 0: + raise ValueError(f"Negative classical value encountered in {debug_str}") + if val >= self.bound: + raise ValueError(f"Too-large classical value encountered in {debug_str}") + + def to_bits(self, x: int) -> List[int]: + """Yields individual bits corresponding to binary representation of x""" + self.assert_valid_val(x) + return _UInt(self.bitsize).to_bits(x) + + def from_bits(self, bits: Sequence[int]) -> int: + """Combine individual bits to form x""" + val = _UInt(self.bitsize).from_bits(bits) + self.assert_valid_val(val) + return val + + def assert_valid_val_array(self, val_array: NDArray[np.integer], debug_str: str = 'val'): + if np.any(val_array < 0): + raise ValueError(f"Negative classical values encountered in {debug_str}") + if np.any(val_array >= self.bound): + raise ValueError(f"Too-large classical values encountered in {debug_str}") + + +@attrs.frozen +class BQUInt(QDType[int]): + """Unsigned quantum integer whose values are bounded within a range. + + LCU methods often make use of coherent for-loops via UnaryIteration, iterating over a range + of values stored as a superposition over the `SELECT` register. Such (nested) coherent + for-loops can be represented using a `Tuple[Register(dtype=BQUInt), ...]` where the + i'th entry stores the bitsize and iteration length of i'th + nested for-loop. + + One useful feature when processing such nested for-loops is to flatten out a composite index, + represented by a tuple of indices (i, j, ...), one for each selection register into a single + integer that can be used to index a flat target register. An example of such a mapping + function is described in Eq.45 of https://arxiv.org/abs/1805.03662. A general version of this + mapping function can be implemented using `numpy.ravel_multi_index` and `numpy.unravel_index`. + + Examples: + We can flatten a 2D for-loop as follows + + >>> import numpy as np + >>> N, M = 10, 20 + >>> flat_indices = set() + >>> for x in range(N): + ... for y in range(M): + ... flat_idx = x * M + y + ... assert np.ravel_multi_index((x, y), (N, M)) == flat_idx + ... assert np.unravel_index(flat_idx, (N, M)) == (x, y) + ... flat_indices.add(flat_idx) + >>> assert len(flat_indices) == N * M + + Similarly, we can flatten a 3D for-loop as follows + >>> import numpy as np + >>> N, M, L = 10, 20, 30 + >>> flat_indices = set() + >>> for x in range(N): + ... for y in range(M): + ... for z in range(L): + ... flat_idx = x * M * L + y * L + z + ... assert np.ravel_multi_index((x, y, z), (N, M, L)) == flat_idx + ... assert np.unravel_index(flat_idx, (N, M, L)) == (x, y, z) + ... flat_indices.add(flat_idx) + >>> assert len(flat_indices) == N * M * L + + Args: + bitsize: The number of qubits used to represent the integer. + iteration_length: The length of the iteration range. + """ + + bitsize: SymbolicInt + iteration_length: SymbolicInt = attrs.field() + + def __attrs_post_init__(self): + if not self.is_symbolic(): + if self.iteration_length > 2**self.bitsize: + raise ValueError( + f"{self} iteration length is too large for given bitsize. " + f"{self.iteration_length} vs {2 ** self.bitsize}" + ) + + @iteration_length.default + def _default_iteration_length(self): + return 2**self.bitsize + + @property + def bound(self) -> SymbolicInt: + return self.iteration_length + + def is_symbolic(self) -> bool: + return is_symbolic(self.bitsize, self.iteration_length) + + @property + def _bit_encoding(self) -> BitEncoding[int]: + return _BUInt(self.bitsize, self.iteration_length) + + def __str__(self): + return f'{self.__class__.__name__}({self.bitsize}, {self.iteration_length})' + + +@attrs.frozen +class BCUInt(CDType[int]): + """Unsigned classical integer whose values are bounded within a range. + + Args: + bitsize: The number of bits used to represent the integer. + bound: The value bound (exclusive). + """ + + bitsize: SymbolicInt + bound: SymbolicInt = attrs.field() + + def __attrs_post_init__(self): + if not self.is_symbolic(): + if self.bound > 2**self.bitsize: + raise ValueError( + f"{self} bound is too large for given bitsize. " + f"{self.bound} vs {2 ** self.bitsize}" + ) + + @bound.default + def _default_bound(self): + return 2**self.bitsize + + def is_symbolic(self) -> bool: + return is_symbolic(self.bitsize, self.bound) + + @property + def _bit_encoding(self) -> BitEncoding[int]: + return _BUInt(self.bitsize, self.bound) + + def __str__(self): + return f'{self.__class__.__name__}({self.bitsize}, {self.bound})' diff --git a/qualtran/dtype/_buint_test.py b/qualtran/dtype/_buint_test.py new file mode 100644 index 0000000000..bd69d08f68 --- /dev/null +++ b/qualtran/dtype/_buint_test.py @@ -0,0 +1,47 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +import sympy + +from qualtran.dtype import assert_to_and_from_bits_array_consistent, BQUInt +from qualtran.symbolics import is_symbolic + + +def test_bounded_quint(): + qint_3 = BQUInt(2, 3) + assert str(qint_3) == 'BQUInt(2, 3)' + + assert qint_3.bitsize == 2 + assert qint_3.iteration_length == 3 + with pytest.raises(ValueError, match="iteration length is too large.*"): + BQUInt(4, 76) + n = sympy.symbols('x') + l = sympy.symbols('l') + qint_8 = BQUInt(n, l) + assert qint_8.num_qubits == n + assert qint_8.iteration_length == l + assert is_symbolic(BQUInt(sympy.Symbol('x'), 2)) + assert is_symbolic(BQUInt(2, sympy.Symbol('x'))) + assert is_symbolic(BQUInt(*sympy.symbols('x y'))) + + +def test_bounded_quint_to_and_from_bits(): + bquint4 = BQUInt(4, 12) + assert [*bquint4.get_classical_domain()] == [*range(0, 12)] + assert list(bquint4.to_bits(10)) == [1, 0, 1, 0] + with pytest.raises(ValueError): + BQUInt(4, 12).to_bits(13) + + assert_to_and_from_bits_array_consistent(bquint4, range(0, 12)) diff --git a/qualtran/dtype/_fxp.py b/qualtran/dtype/_fxp.py new file mode 100644 index 0000000000..ce165a64c4 --- /dev/null +++ b/qualtran/dtype/_fxp.py @@ -0,0 +1,398 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Iterable, List, Sequence, TYPE_CHECKING, Union + +import attrs + +from qualtran.symbolics import is_symbolic, SymbolicInt + +from ._base import BitEncoding, CDType, QDType + +if TYPE_CHECKING: + import fxpmath + + +@attrs.frozen +class _Fxp(BitEncoding[int]): + r"""Fixed point type to represent real numbers. + + To hook into the classical simulator, we use fixed-width integers to represent + values of this type. See `to_fixed_width_int` for details. + In particular, the user should call `QFxp.to_fixed_width_int(float_value)` + before passing a value to `bloq.call_classically`. + + See https://github.com/quantumlib/Qualtran/issues/1219 for discussion on alternatives + and future upgrades. + + + Args: + bitsize: The total number of qubits used to represent the integer and + fractional part combined. + num_frac: The number of qubits used to represent the fractional part of the real number. + signed: Whether the number is signed or not. + """ + + bitsize: SymbolicInt + num_frac: SymbolicInt + signed: bool = False + + def __attrs_post_init__(self): + if not is_symbolic(self.bitsize) and self.bitsize == 1 and self.signed: + raise ValueError("bitsize must be > 1.") + if not is_symbolic(self.bitsize) and not is_symbolic(self.num_frac): + if self.signed and self.bitsize == self.num_frac: + raise ValueError("num_frac must be less than bitsize if the Fxp is signed.") + if self.bitsize < self.num_frac: + raise ValueError("bitsize must be >= num_frac.") + + @property + def num_int(self) -> SymbolicInt: + """Number of bits for the integral part.""" + return self.bitsize - self.num_frac + + @property + def _int_encoding(self) -> BitEncoding[int]: + # The corresponding dtype for the raw integer encoding. + from qualtran.dtype._int import _Int + from qualtran.dtype._uint import _UInt + + return _Int(self.bitsize) if self.signed else _UInt(self.bitsize) + + def get_domain(self) -> Iterable[int]: + # Use the classical domain for the underlying raw integer encoding. + yield from self._int_encoding.get_domain() + + def to_bits(self, x: int) -> List[int]: + # Use the underlying raw integer encoding. + return self._int_encoding.to_bits(x) + + def from_bits(self, bits: Sequence[int]) -> int: + # Use the underlying raw integer encoding. + return self._int_encoding.from_bits(bits) + + def assert_valid_val(self, val: int, debug_str: str = 'val'): + # Verify using the underlying raw integer encoding. + self._int_encoding.assert_valid_val(val, debug_str) + + def to_fixed_width_int( + self, + x: Union[float, 'fxpmath.Fxp'], + *, + require_exact: bool = False, + complement: bool = True, + ) -> int: + """Returns the interpretation of the binary representation of `x` as an integer. + + The returned value is an integer equal to `round(x * 2**self.num_frac)`. + That is, the input value `x` is converted to a fixed-point binary value + of `self.num_int` integral bits and `self.num_frac` fractional bits, + and then re-interpreted as an integer by dropping the decimal point. + + Args: + x: input real number + require_exact: Raise `ValueError` if `x` cannot be exactly represented. + complement: Use twos-complement rather than sign-magnitude representation of negative values. + """ + bits = self._fxp_to_bits(x, require_exact=require_exact, complement=complement) + return self._int_encoding.from_bits(bits) + + def float_from_fixed_width_int(self, x: int) -> float: + """Helper to convert from the fixed-width-int representation to a true floating point value. + + Here `x` is the internal value used by the classical simulator. + See `to_fixed_width_int` for conventions. + """ + return x / 2**self.num_frac + + def __str__(self): + if self.signed: + return f'_Fxp({self.bitsize}, {self.num_frac}, True)' + else: + return f'_Fxp({self.bitsize}, {self.num_frac})' + + def fxp_dtype_template(self) -> 'fxpmath.Fxp': + """A template of the `fxpmath.Fxp` data type for classical values. + + To construct an `fxpmath.Fxp` with this config, one can use: + `Fxp(float_value, like=_Fxp(...).fxp_dtype_template)`, + or given an existing value `some_fxp_value: Fxp`: + `some_fxp_value.like(_Fxp(...).fxp_dtype_template)`. + + The following Fxp configuration is used: + - op_sizing='same' and const_op_sizing='same' ensure that the returned + object is not resized to a bigger fixed point number when doing + operations with other Fxp objects. + - shifting='trunc' ensures that when shifting the Fxp integer to + left / right; the digits are truncated and no rounding occurs + - overflow='wrap' ensures that when performing operations where result + overflows, the overflowed digits are simply discarded. + + Support for `fxpmath.Fxp` is experimental, and does not hook into the classical + simulator protocol. Once the library choice for fixed-point classical real + values is finalized, the code will be updated to use the new functionality + instead of delegating to raw integer values (see above). + """ + import fxpmath + + if is_symbolic(self.bitsize) or is_symbolic(self.num_frac): + raise ValueError( + f"Cannot construct Fxp template for symbolic bitsizes: {self.bitsize=}, {self.num_frac=}" + ) + + return fxpmath.Fxp( + None, + n_word=self.bitsize, + n_frac=self.num_frac, + signed=self.signed, + op_sizing='same', + const_op_sizing='same', + shifting='trunc', + overflow='wrap', + ) + + def _get_domain_fxp(self) -> Iterable['fxpmath.Fxp']: + import fxpmath + + for x in self._int_encoding.get_domain(): + yield fxpmath.Fxp(x / 2**self.num_frac, like=self.fxp_dtype_template()) + + def _fxp_to_bits( + self, x: Union[float, 'fxpmath.Fxp'], require_exact: bool = True, complement: bool = True + ) -> List[int]: + """Yields individual bits corresponding to binary representation of `x`. + + Args: + x: The value to encode. + require_exact: Raise `ValueError` if `x` cannot be exactly represented. + complement: Use twos-complement rather than sign-magnitude representation of negative values. + + Raises: + ValueError: If `x` is negative but this `_Fxp` is not signed. + """ + import fxpmath + + if require_exact: + self._assert_valid_val(x) + if x < 0 and not self.signed: + raise ValueError(f"unsigned _Fxp cannot represent {x}.") + if self.signed and not complement: + sign = int(x < 0) + x = abs(x) + fxp = x if isinstance(x, fxpmath.Fxp) else fxpmath.Fxp(x) + bits = [int(x) for x in fxp.like(self.fxp_dtype_template()).bin()] + if self.signed and not complement: + bits[0] = sign + return bits + + def _from_bits_to_fxp(self, bits: Sequence[int]) -> 'fxpmath.Fxp': + import fxpmath + + if is_symbolic(self.num_frac): + raise ValueError(f"Symbolic {self.num_frac} cannot be represented using Fxp") + bits_bin = "".join(str(x) for x in bits[:]) + fxp_bin = "0b" + bits_bin[: -int(self.num_frac)] + "." + bits_bin[-int(self.num_frac) :] + return fxpmath.Fxp(fxp_bin, like=self.fxp_dtype_template()) + + def _assert_valid_val(self, val: Union[float, 'fxpmath.Fxp'], debug_str: str = 'val'): + import fxpmath + + fxp_val = val if isinstance(val, fxpmath.Fxp) else fxpmath.Fxp(val) + if fxp_val.get_val() != fxp_val.like(self.fxp_dtype_template()).get_val(): + raise ValueError( + f"{debug_str}={val} cannot be accurately represented using Fxp {fxp_val}" + ) + + +@attrs.frozen +class QFxp(QDType[int]): + r"""Fixed point quantum type to represent real numbers. + + A real number can be approximately represented in fixed point using `num_int` + bits for the integer part and `num_frac` bits for the fractional part. If the + real number is signed we store negative values in two's complement form. The first + bit can therefore be treated as the sign bit in such cases (0 for +, 1 for -). + In total there are `bitsize = (num_int + num_frac)` bits used to represent the number. + E.g. Using `(bitsize = 8, num_frac = 6, signed = False)` then + $\pi \approx 3.140625 = 11.001001$, where the . represents the decimal place. + + We can specify a fixed point real number by the tuple bitsize, num_frac and + signed, with num_int determined as `(bitsize - num_frac)`. + + **Classical Simulation:** + + To hook into the classical simulator, we use fixed-width integers to represent + values of this type. See `to_fixed_width_int` for details. + In particular, the user should call `QFxp.to_fixed_width_int(float_value)` + before passing a value to `bloq.call_classically`. + + The corresponding raw qdtype is either an QUInt (when `signed=False`) or + QInt (when `signed=True`) of the same bitsize. This is the data type used + to represent classical values during simulation, and convert to and from bits + for intermediate values. + + For example, `QFxp(6, 4)` has 2 int bits and 4 frac bits, and the corresponding + int type is `QUInt(6)`. So a true classical value of `10.0011` will have a raw + integer representation of `100011`. + + Args: + bitsize: The total number of qubits used to represent the integer and + fractional part combined. + num_frac: The number of qubits used to represent the fractional part of the real number. + signed: Whether the number is signed or not. + """ + + bitsize: SymbolicInt + num_frac: SymbolicInt + signed: bool = False + + def __attrs_post_init__(self): + if not is_symbolic(self.bitsize) and self.bitsize == 1 and self.signed: + raise ValueError("num_qubits must be > 1.") + if not is_symbolic(self.bitsize) and not is_symbolic(self.num_frac): + if self.signed and self.bitsize == self.num_frac: + raise ValueError("num_frac must be less than bitsize if the QFxp is signed.") + if self.bitsize < self.num_frac: + raise ValueError("bitsize must be >= num_frac.") + + @property + def _bit_encoding(self) -> _Fxp: + return _Fxp(bitsize=self.bitsize, num_frac=self.num_frac, signed=self.signed) + + @property + def num_int(self) -> SymbolicInt: + """Number of bits for the integral part.""" + return self._bit_encoding.num_int + + def is_symbolic(self) -> bool: + return is_symbolic(self.bitsize, self.num_frac) + + def to_fixed_width_int( + self, + x: Union[float, 'fxpmath.Fxp'], + *, + require_exact: bool = False, + complement: bool = True, + ) -> int: + """Returns the interpretation of the binary representation of `x` as an integer. + + See class docstring section on "Classical Simulation" for more details on + the choice of this representation. + + The returned value is an integer equal to `round(x * 2**self.num_frac)`. + That is, the input value `x` is converted to a fixed-point binary value + of `self.num_int` integral bits and `self.num_frac` fractional bits, + and then re-interpreted as an integer by dropping the decimal point. + + For example, consider `QFxp(6, 4).to_fixed_width_int(1.5)`. As `1.5` is `0b01.1000` + in this representation, the returned value would be `0b011000` = 24. + + For negative values, we use twos complement form. So in + `QFxp(6, 4, signed=True).to_fixed_width_int(-1.5)`, the input is `0b10.1000`, + which is interpreted as `0b101000` = -24. + + Args: + x: input floating point value + require_exact: Raise `ValueError` if `x` cannot be exactly represented. + complement: Use twos-complement rather than sign-magnitude representation of negative values. + """ + return self._bit_encoding.to_fixed_width_int( + x=x, require_exact=require_exact, complement=complement + ) + + def float_from_fixed_width_int(self, x: int) -> float: + """Helper to convert from the fixed-width-int representation to a true floating point value. + + Here `x` is the internal value used by the classical simulator. + See `to_fixed_width_int` for conventions. + + See class docstring section on "Classical Simulation" for more details on + the choice of this representation. + """ + return self._bit_encoding.float_from_fixed_width_int(x=x) + + def __str__(self): + if self.signed: + return f'QFxp({self.bitsize}, {self.num_frac}, True)' + else: + return f'QFxp({self.bitsize}, {self.num_frac})' + + def fxp_dtype_template(self) -> 'fxpmath.Fxp': + """A template of the `Fxp` data type for classical values. + + To construct an `Fxp` with this config, one can use: + `Fxp(float_value, like=QFxp(...).fxp_dtype_template)`, + or given an existing value `some_fxp_value: Fxp`: + `some_fxp_value.like(QFxp(...).fxp_dtype_template)`. + + The following Fxp configuration is used: + - op_sizing='same' and const_op_sizing='same' ensure that the returned + object is not resized to a bigger fixed point number when doing + operations with other Fxp objects. + - shifting='trunc' ensures that when shifting the Fxp integer to + left / right; the digits are truncated and no rounding occurs + - overflow='wrap' ensures that when performing operations where result + overflows, the overflowed digits are simply discarded. + + Support for `fxpmath.Fxp` is experimental, and does not hook into the classical + simulator protocol. Once the library choice for fixed-point classical real + values is finalized, the code will be updated to use the new functionality + instead of delegating to raw integer values (see above). + """ + return self._bit_encoding.fxp_dtype_template() + + +@attrs.frozen +class CFxp(CDType[int]): + r"""Fixed point classical type to represent real numbers. + + This follows the same conventions as `QFxp`. See that class documentation for details. + + Args: + bitsize: The total number of qubits used to represent the integer and + fractional part combined. + num_frac: The number of qubits used to represent the fractional part of the real number. + signed: Whether the number is signed or not. + """ + + bitsize: SymbolicInt + num_frac: SymbolicInt + signed: bool = False + + def __attrs_post_init__(self): + if not is_symbolic(self.bitsize) and self.bitsize == 1 and self.signed: + raise ValueError("num_qubits must be > 1.") + if not is_symbolic(self.bitsize) and not is_symbolic(self.num_frac): + if self.signed and self.bitsize == self.num_frac: + raise ValueError("num_frac must be less than bitsize if the QFxp is signed.") + if self.bitsize < self.num_frac: + raise ValueError("bitsize must be >= num_frac.") + + @property + def _bit_encoding(self) -> _Fxp: + return _Fxp(bitsize=self.bitsize, num_frac=self.num_frac, signed=self.signed) + + @property + def num_int(self) -> SymbolicInt: + """Number of bits for the integral part.""" + return self._bit_encoding.num_int + + def is_symbolic(self) -> bool: + return is_symbolic(self.bitsize, self.num_frac) + + def __str__(self): + if self.signed: + return f'CFxp({self.bitsize}, {self.num_frac}, True)' + else: + return f'CFxp({self.bitsize}, {self.num_frac})' diff --git a/qualtran/dtype/_fxp_test.py b/qualtran/dtype/_fxp_test.py new file mode 100644 index 0000000000..2ed638a283 --- /dev/null +++ b/qualtran/dtype/_fxp_test.py @@ -0,0 +1,155 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import math + +import numpy as np +import pytest +import sympy + +from qualtran.dtype import assert_to_and_from_bits_array_consistent, QFxp, QUInt +from qualtran.dtype._fxp import _Fxp +from qualtran.symbolics import is_symbolic + +RS = np.random.RandomState(52) + + +def test_qfxp(): + qfp_16 = QFxp(16, 15) + assert str(qfp_16) == 'QFxp(16, 15)' + assert qfp_16.num_qubits == 16 + assert qfp_16.num_int == 1 + assert qfp_16.fxp_dtype_template().dtype == 'fxp-u16/15' + + qfp_16 = QFxp(16, 15, signed=True) + assert str(qfp_16) == 'QFxp(16, 15, True)' + assert qfp_16.num_qubits == 16 + assert qfp_16.num_int == 1 + assert qfp_16.fxp_dtype_template().dtype == 'fxp-s16/15' + + with pytest.raises(ValueError, match="num_qubits must be > 1."): + QFxp(1, 1, signed=True) + QFxp(1, 1, signed=False) + with pytest.raises(ValueError, match="num_frac must be less than.*"): + QFxp(4, 4, signed=True) + with pytest.raises(ValueError, match="bitsize must be >= .*"): + QFxp(4, 5) + b = sympy.symbols('b') + f = sympy.symbols('f') + qfp = QFxp(b, f) + assert qfp.num_qubits == b + assert qfp.num_int == b - f + qfp = QFxp(b, f, True) + assert qfp.num_qubits == b + assert qfp.num_int == b - f + assert is_symbolic(QFxp(*sympy.symbols('x y'))) + + +def test_qfxp_to_and_from_bits(): + assert_to_and_from_bits_array_consistent( + QFxp(4, 3, False), [QFxp(4, 3, False).to_fixed_width_int(x) for x in [1 / 2, 1 / 4, 3 / 8]] + ) + assert_to_and_from_bits_array_consistent( + QFxp(4, 3, True), + [ + QFxp(4, 3, True).to_fixed_width_int(x) + for x in [1 / 2, -1 / 2, 1 / 4, -1 / 4, -3 / 8, 3 / 8] + ], + ) + + +def test_qfxp_to_fixed_width_int(): + assert QFxp(6, 4).to_fixed_width_int(1.5) == 24 == 1.5 * 2**4 + assert QFxp(6, 4, signed=True).to_fixed_width_int(1.5) == 24 == 1.5 * 2**4 + assert QFxp(6, 4, signed=True).to_fixed_width_int(-1.5) == -24 == -1.5 * 2**4 + + +def test_qfxp_from_fixed_width_int(): + qfxp = QFxp(6, 4) + for x_int in qfxp.get_classical_domain(): + x_float = qfxp.float_from_fixed_width_int(x_int) + x_int_roundtrip = qfxp.to_fixed_width_int(x_float) + assert x_int == x_int_roundtrip + + for float_val in [1.5, 1.25]: + assert qfxp.float_from_fixed_width_int(qfxp.to_fixed_width_int(float_val)) == float_val + + +def test_qfxp_to_and_from_bits_using_fxp(): + # QFxp: Negative numbers are stored as twos complement + qfxp_4_3 = _Fxp(4, 3, True) + assert list(qfxp_4_3._fxp_to_bits(0.5)) == [0, 1, 0, 0] + assert qfxp_4_3._from_bits_to_fxp(qfxp_4_3._fxp_to_bits(0.5)).get_val() == 0.5 + assert list(qfxp_4_3._fxp_to_bits(-0.5)) == [1, 1, 0, 0] + assert qfxp_4_3._from_bits_to_fxp(qfxp_4_3._fxp_to_bits(-0.5)).get_val() == -0.5 + assert list(qfxp_4_3._fxp_to_bits(0.625)) == [0, 1, 0, 1] + assert qfxp_4_3._from_bits_to_fxp(qfxp_4_3._fxp_to_bits(+0.625)).get_val() == +0.625 + assert qfxp_4_3._from_bits_to_fxp(qfxp_4_3._fxp_to_bits(-0.625)).get_val() == -0.625 + assert list(qfxp_4_3._fxp_to_bits(-(1 - 0.625))) == [1, 1, 0, 1] + assert qfxp_4_3._from_bits_to_fxp(qfxp_4_3._fxp_to_bits(0.375)).get_val() == 0.375 + assert qfxp_4_3._from_bits_to_fxp(qfxp_4_3._fxp_to_bits(-0.375)).get_val() == -0.375 + with pytest.raises(ValueError): + _ = qfxp_4_3._fxp_to_bits(0.1) + assert list(qfxp_4_3._fxp_to_bits(0.7, require_exact=False)) == [0, 1, 0, 1] + assert list(qfxp_4_3._fxp_to_bits(0.7, require_exact=False, complement=False)) == [0, 1, 0, 1] + assert list(qfxp_4_3._fxp_to_bits(-0.7, require_exact=False)) == [1, 0, 1, 1] + assert list(qfxp_4_3._fxp_to_bits(-0.7, require_exact=False, complement=False)) == [1, 1, 0, 1] + + with pytest.raises(ValueError): + _ = qfxp_4_3._fxp_to_bits(1.5) + + assert ( + qfxp_4_3._from_bits_to_fxp(qfxp_4_3._fxp_to_bits(1 / 2 + 1 / 4 + 1 / 8)) + == 1 / 2 + 1 / 4 + 1 / 8 + ) + assert ( + qfxp_4_3._from_bits_to_fxp(qfxp_4_3._fxp_to_bits(-1 / 2 - 1 / 4 - 1 / 8)) + == -1 / 2 - 1 / 4 - 1 / 8 + ) + with pytest.raises(ValueError): + _ = qfxp_4_3._fxp_to_bits(1 / 2 + 1 / 4 + 1 / 8 + 1 / 16) + + for qfxp in [QFxp(4, 3, True), QFxp(3, 3, False), QFxp(7, 3, False), QFxp(7, 3, True)]: + for x in qfxp._bit_encoding._get_domain_fxp(): + assert qfxp._bit_encoding._from_bits_to_fxp(qfxp._bit_encoding._fxp_to_bits(x)) == x + + assert list(_Fxp(7, 3, True)._fxp_to_bits(-4.375)) == [1] + [0, 1, 1] + [1, 0, 1] + assert list(_Fxp(7, 3, True)._fxp_to_bits(+4.625)) == [0] + [1, 0, 0] + [1, 0, 1] + + +@pytest.mark.parametrize('val', [RS.uniform(-1, 1) for _ in range(10)]) +@pytest.mark.parametrize('width', [*range(2, 20, 2)]) +@pytest.mark.parametrize('signed', [True, False]) +def test_fixed_point(val, width, signed): + if (val < 0) and not signed: + with pytest.raises(ValueError): + _ = _Fxp(width + int(signed), width, signed=signed)._fxp_to_bits( + val, require_exact=False, complement=False + ) + else: + bits = _Fxp(width + int(signed), width, signed=signed)._fxp_to_bits( + val, require_exact=False, complement=False + ) + if signed: + sign, bits = bits[0], bits[1:] + assert sign == (1 if val < 0 else 0) + val = abs(val) + approx_val = math.fsum([b * (1 / 2 ** (1 + i)) for i, b in enumerate(bits)]) + assert math.isclose(val, approx_val, abs_tol=1 / 2**width), ( + f'{val}:{approx_val}:{width}', + bits, + ) + with pytest.raises(ValueError): + _ = QFxp(width, width).to_fixed_width_int(-val) + bits_from_int = QUInt(width).to_bits(QFxp(width, width).to_fixed_width_int(val)) + assert bits == bits_from_int diff --git a/qualtran/dtype/_int.py b/qualtran/dtype/_int.py new file mode 100644 index 0000000000..c4b4750f2f --- /dev/null +++ b/qualtran/dtype/_int.py @@ -0,0 +1,111 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from functools import cached_property +from typing import Iterable, List, Sequence + +import attrs +import numpy as np +from numpy.typing import NDArray + +from qualtran.symbolics import is_symbolic, SymbolicInt + +from ._base import BitEncoding, CDType, QDType +from ._uint import _UInt + + +@attrs.frozen +class _Int(BitEncoding[int]): + """Signed integer of a given bitsize. + + Use `QInt` or `CInt` for quantum or classical implementations, respectively. + + A two's complement representation is used for negative integers. + Here (and throughout Qualtran), we use a big-endian bit convention. + The most significant bit is at index 0. + """ + + bitsize: SymbolicInt + + def get_domain(self) -> Iterable[int]: + max_val = 1 << (self.bitsize - 1) + return range(-max_val, max_val) + + def to_bits(self, x: int) -> List[int]: + if is_symbolic(self.bitsize): + raise ValueError(f"cannot compute bits with symbolic {self.bitsize=}") + + self.assert_valid_val(x) + return [int(b) for b in np.binary_repr(x, width=self.bitsize)] + + def from_bits(self, bits: Sequence[int]) -> int: + sign = bits[0] + x = ( + 0 + if self.bitsize == 1 + else _UInt(self.bitsize - 1).from_bits([1 - x if sign else x for x in bits[1:]]) + ) + return ~x if sign else x + + def assert_valid_val(self, val: int, debug_str: str = 'val'): + if not isinstance(val, (int, np.integer)): + raise ValueError(f"{debug_str} should be an integer, not {val!r}") + if val < -(2 ** (self.bitsize - 1)): + raise ValueError(f"Too-small classical {self}: {val} encountered in {debug_str}") + if val >= 2 ** (self.bitsize - 1): + raise ValueError(f"Too-large classical {self}: {val} encountered in {debug_str}") + + def assert_valid_val_array(self, val_array: NDArray[np.integer], debug_str: str = 'val'): + if np.any(val_array < -(2 ** (self.bitsize - 1))): + raise ValueError(f"Too-small classical {self}s encountered in {debug_str}") + if np.any(val_array >= 2 ** (self.bitsize - 1)): + raise ValueError(f"Too-large classical {self}s encountered in {debug_str}") + + +@attrs.frozen +class QInt(QDType[int]): + """Signed quantum integer of a given bitsize. + + A two's complement representation is used for negative integers. + Here (and throughout Qualtran), we use a big-endian bit convention. + The most significant bit is at index 0. + + Args: + bitsize: The number of qubits used to represent the integer. + """ + + bitsize: SymbolicInt + + @cached_property + def _bit_encoding(self) -> BitEncoding[int]: + return _Int(self.bitsize) + + +@attrs.frozen +class CInt(CDType[int]): + """Signed classical integer of a given bitsize. + + A two's complement representation is used for negative integers. + Here (and throughout Qualtran), we use a big-endian bit convention. + The most significant bit is at index 0. + + Args: + bitsize: The number of qubits used to represent the integer. + """ + + bitsize: SymbolicInt + + @cached_property + def _bit_encoding(self) -> BitEncoding[int]: + return _Int(self.bitsize) diff --git a/qualtran/dtype/_int_ones_complement.py b/qualtran/dtype/_int_ones_complement.py new file mode 100644 index 0000000000..cde4e42cbc --- /dev/null +++ b/qualtran/dtype/_int_ones_complement.py @@ -0,0 +1,113 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from functools import cached_property +from typing import Iterable, List, Sequence + +import attrs +import numpy as np + +from qualtran.symbolics import SymbolicInt + +from ._base import BitEncoding, CDType, QDType +from ._uint import _UInt + + +@attrs.frozen +class _IntOnesComp(BitEncoding[int]): + """Ones' complement signed integer of a given bitsize. + + This contrasts with `_Int` by using the ones' complement representation for negative + integers. + Here (and throughout Qualtran), we use a big-endian bit convention. + The most significant bit is at index 0. + """ + + bitsize: SymbolicInt + + def __attrs_post_init__(self): + if isinstance(self.bitsize, int): + if self.bitsize == 1: + raise ValueError("bitsize must be > 1.") + + def to_bits(self, x: int) -> List[int]: + self.assert_valid_val(x) + return [int(x < 0)] + [y ^ int(x < 0) for y in _UInt(self.bitsize - 1).to_bits(abs(x))] + + def from_bits(self, bits: Sequence[int]) -> int: + x = _UInt(self.bitsize).from_bits([b ^ bits[0] for b in bits[1:]]) + return (-1) ** int(bits[0]) * x + + def get_domain(self) -> Iterable[int]: + max_val = 1 << (self.bitsize - 1) + return range(-max_val + 1, max_val) + + def assert_valid_val(self, val: int, debug_str: str = 'val') -> None: + if not isinstance(val, (int, np.integer)): + raise ValueError(f"{debug_str} should be an integer, not {val!r}") + max_val = 1 << (self.bitsize - 1) + if not -max_val <= val <= max_val: + raise ValueError( + f"Classical value {val} must be in range [-{max_val}, +{max_val}] in {debug_str}" + ) + + +@attrs.frozen +class QIntOnesComp(QDType[int]): + """Ones' complement signed quantum integer of a given bitsize. + + This contrasts with `QInt` by using the ones' complement representation for negative + integers. + Here (and throughout Qualtran), we use a big-endian bit convention. + The most significant bit is at index 0. + + Args: + bitsize: The number of qubits used to represent the integer. + """ + + bitsize: SymbolicInt + + def __attrs_post_init__(self): + if isinstance(self.bitsize, int): + if self.bitsize == 1: + raise ValueError("bitsize must be > 1.") + + @cached_property + def _bit_encoding(self) -> BitEncoding[int]: + return _IntOnesComp(self.bitsize) + + +@attrs.frozen +class CIntOnesComp(CDType[int]): + """Ones' complement signed classical integer of a given bitsize. + + This contrasts with `CInt` by using the ones' complement representation for negative + integers. + Here (and throughout Qualtran), we use a big-endian bit convention. + The most significant bit is at index 0. + + Args: + bitsize: The number of classical bits used to represent the integer. + """ + + bitsize: SymbolicInt + + def __attrs_post_init__(self): + if isinstance(self.bitsize, int): + if self.bitsize == 1: + raise ValueError("bitsize must be > 1.") + + @cached_property + def _bit_encoding(self) -> BitEncoding[int]: + return _IntOnesComp(self.bitsize) diff --git a/qualtran/dtype/_int_ones_complement_test.py b/qualtran/dtype/_int_ones_complement_test.py new file mode 100644 index 0000000000..81aa4eaf3b --- /dev/null +++ b/qualtran/dtype/_int_ones_complement_test.py @@ -0,0 +1,46 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +import sympy + +from qualtran.dtype import assert_to_and_from_bits_array_consistent, QIntOnesComp +from qualtran.symbolics import is_symbolic + + +def test_qint_ones(): + qint_8 = QIntOnesComp(8) + assert str(qint_8) == 'QIntOnesComp(8)' + assert qint_8.num_qubits == 8 + with pytest.raises(ValueError, match="bitsize must be > 1."): + QIntOnesComp(1) + n = sympy.symbols('x') + qint_8 = QIntOnesComp(n) + assert qint_8.num_qubits == n + assert is_symbolic(QIntOnesComp(sympy.Symbol('x'))) + + +def test_qintonescomp_to_and_from_bits(): + qintones4 = QIntOnesComp(4) + assert list(qintones4.to_bits(-2)) == [1, 1, 0, 1] + assert list(qintones4.to_bits(2)) == [0, 0, 1, 0] + assert [*qintones4.get_classical_domain()] == [*range(-7, 8)] + for x in range(-7, 8): + assert qintones4.from_bits(qintones4.to_bits(x)) == x + with pytest.raises(ValueError): + qintones4.to_bits(8) + with pytest.raises(ValueError): + qintones4.to_bits(-8) + + assert_to_and_from_bits_array_consistent(qintones4, range(-7, 8)) diff --git a/qualtran/dtype/_int_test.py b/qualtran/dtype/_int_test.py new file mode 100644 index 0000000000..b051c87943 --- /dev/null +++ b/qualtran/dtype/_int_test.py @@ -0,0 +1,59 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +import sympy + +from qualtran.dtype import assert_to_and_from_bits_array_consistent, QInt +from qualtran.symbolics import is_symbolic + + +def test_qint(): + qint_8 = QInt(8) + assert qint_8.num_qubits == 8 + assert qint_8.num_cbits == 0 + assert qint_8.num_bits == 8 + assert str(qint_8) == 'QInt(8)' + n = sympy.symbols('x') + qint_8 = QInt(n) + assert qint_8.num_qubits == n + assert str(qint_8) == 'QInt(x)' + assert is_symbolic(QInt(sympy.Symbol('x'))) + + +def test_qint_to_and_from_bits(): + qint4 = QInt(4) + assert [*qint4.get_classical_domain()] == [*range(-8, 8)] + for x in range(-8, 8): + assert qint4.from_bits(qint4.to_bits(x)) == x + assert list(qint4.to_bits(-2)) == [1, 1, 1, 0] + assert list(QInt(4).to_bits(2)) == [0, 0, 1, 0] + # MSB at lowest index -- big-endian + assert qint4.from_bits([0, 0, 0, 1]) == 1 + assert qint4.from_bits([0, 0, 0, 1]) < qint4.from_bits([0, 1, 0, 0]) + assert qint4.from_bits(qint4.to_bits(-2)) == -2 + assert qint4.from_bits(qint4.to_bits(2)) == 2 + with pytest.raises(ValueError): + QInt(4).to_bits(10) + + assert_to_and_from_bits_array_consistent(qint4, range(-8, 8)) + + +def test_iter_bits_twos(): + assert QInt(4).to_bits(0) == [0, 0, 0, 0] + assert QInt(4).to_bits(1) == [0, 0, 0, 1] + assert QInt(4).to_bits(-2) == [1, 1, 1, 0] + assert QInt(4).to_bits(-3) == [1, 1, 0, 1] + with pytest.raises(ValueError): + _ = QInt(2).to_bits(100) diff --git a/qualtran/dtype/_montgomery_uint.py b/qualtran/dtype/_montgomery_uint.py new file mode 100644 index 0000000000..df6612d1b8 --- /dev/null +++ b/qualtran/dtype/_montgomery_uint.py @@ -0,0 +1,209 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from functools import cached_property +from typing import Iterable, List, Optional, Sequence + +import attrs +import numpy as np +from numpy.typing import NDArray + +from qualtran.symbolics import is_symbolic, SymbolicInt + +from ._base import BitEncoding, CDType, QDType + + +@attrs.frozen +class _MontgomeryUInt(BitEncoding[int]): + r"""Montgomery form of an unsigned integer of a given width bitsize which wraps around upon + overflow. + + Any MontgomeryUInt can be treated as a UInt, but not + every UInt can be treated as a MontgomeryUInt. Montgomery form is used in order to compute + fast modular multiplication. + """ + + bitsize: SymbolicInt + modulus: Optional[SymbolicInt] = None + + def get_domain(self) -> Iterable[int]: + if self.modulus is None or is_symbolic(self.modulus): + return range(2**self.bitsize) + return range(1, int(self.modulus)) + + def to_bits(self, x: int) -> List[int]: + self.assert_valid_val(x) + return [int(x) for x in f'{int(x):0{self.bitsize}b}'] + + def from_bits(self, bits: Sequence[int]) -> int: + return int("".join(str(x) for x in bits), 2) + + def assert_valid_val(self, val: int, debug_str: str = 'val') -> None: + if not isinstance(val, (int, np.integer)): + raise ValueError(f"{debug_str} should be an integer, not {val!r}") + if val < 0: + raise ValueError(f"Negative classical value encountered in {debug_str}") + if val >= 2**self.bitsize: + raise ValueError(f"Too-large classical value encountered in {debug_str}") + + def assert_valid_val_array( + self, val_array: NDArray[np.integer], debug_str: str = 'val' + ) -> None: + if np.any(val_array < 0): + raise ValueError(f"Negative classical values encountered in {debug_str}") + if np.any(val_array >= 2**self.bitsize): + raise ValueError(f"Too-large classical values encountered in {debug_str}") + + def montgomery_inverse(self, xm: int) -> int: + """Returns the modular inverse of an integer in montgomery form. + + Args: + xm: An integer in montgomery form. + """ + assert self.modulus is not None and not is_symbolic(self.modulus) + return ( + (pow(xm, -1, self.modulus)) * pow(2, 2 * self.bitsize, int(self.modulus)) + ) % self.modulus + + def montgomery_product(self, xm: int, ym: int) -> int: + """Returns the modular product of two integers in montgomery form. + + Args: + xm: The first montgomery form integer for the product. + ym: The second montgomery form integer for the product. + """ + assert self.modulus is not None and not is_symbolic(self.modulus) + return (xm * ym * pow(2, -int(self.bitsize), int(self.modulus))) % self.modulus + + def montgomery_to_uint(self, xm: int) -> int: + """Converts an integer in montgomery form to a normal form integer. + + Args: + xm: An integer in montgomery form. + """ + assert self.modulus is not None and not is_symbolic(self.modulus) + return (xm * pow(2, -int(self.bitsize), int(self.modulus))) % self.modulus + + def uint_to_montgomery(self, x: int) -> int: + """Converts an integer into montgomery form. + + Args: + x: An integer. + """ + assert self.modulus is not None and not is_symbolic(self.modulus) + return (x * pow(2, int(self.bitsize), int(self.modulus))) % self.modulus + + +@attrs.frozen +class QMontgomeryUInt(QDType[int]): + r"""Montgomery form of an unsigned integer of a given width bitsize which wraps around upon + overflow. + + Similar to unsigned integer types in C. Any intended wrap around effect is + expected to be handled by the developer. Any QMontgomeryUInt can be treated as a QUInt, but not + every QUInt can be treated as a QMontgomeryUInt. Montgomery form is used in order to compute + fast modular multiplication. + + In order to convert an unsigned integer from a finite field x % p into Montgomery form you + first must choose a value r > p where gcd(r, p) = 1. Typically, this value is a power of 2. + + Conversion to Montgomery form is given by + `[x] = (x * r) % p` + + Conversion from Montgomery form to normal form is given by + `x = REDC([x])` + + Pseudocode for REDC(u) can be found in the resource below. + + Args: + bitsize: The number of qubits used to represent the integer. + modulus: The modulus p. + + References: + [Montgomery modular multiplication](https://en.wikipedia.org/wiki/Montgomery_modular_multiplication). + + [Performance Analysis of a Repetition Cat Code Architecture: Computing 256-bit Elliptic Curve Logarithm in 9 Hours with 126133 Cat Qubits](https://arxiv.org/abs/2302.06639). + Gouzien et al. 2023. + We follow Montgomery form as described in the above paper; namely, r = 2^bitsize. + """ + + bitsize: SymbolicInt + modulus: Optional[SymbolicInt] = None + + @cached_property + def _bit_encoding(self) -> _MontgomeryUInt: + return _MontgomeryUInt(self.bitsize, self.modulus) + + def montgomery_inverse(self, xm: int) -> int: + """Returns the modular inverse of an integer in montgomery form. + + Args: + xm: An integer in montgomery form. + """ + return self._bit_encoding.montgomery_inverse(xm) + + def montgomery_product(self, xm: int, ym: int) -> int: + """Returns the modular product of two integers in montgomery form. + + Args: + xm: The first montgomery form integer for the product. + ym: The second montgomery form integer for the product. + """ + return self._bit_encoding.montgomery_product(xm, ym) + + def montgomery_to_uint(self, xm: int) -> int: + """Converts an integer in montgomery form to a normal form integer. + + Args: + xm: An integer in montgomery form. + """ + return self._bit_encoding.montgomery_to_uint(xm) + + def uint_to_montgomery(self, x: int) -> int: + """Converts an integer into montgomery form. + + Args: + x: An integer. + """ + return self._bit_encoding.uint_to_montgomery(x) + + def __str__(self): + if self.modulus is not None: + modstr = f', {self.modulus}' + else: + modstr = '' + return f'{self.__class__.__name__}({self.bitsize}{modstr})' + + +@attrs.frozen +class CMontgomeryUInt(CDType[int]): + r"""Montgomery form of an unsigned integer of a given width bitsize which wraps around upon + overflow. + + This is a classical version of QMontgomeryUInt. See the documentation for that class. + """ + + bitsize: SymbolicInt + modulus: Optional[SymbolicInt] = None + + @cached_property + def _bit_encoding(self) -> _MontgomeryUInt: + return _MontgomeryUInt(self.bitsize, self.modulus) + + def __str__(self): + if self.modulus is not None: + modstr = f', {self.modulus}' + else: + modstr = '' + return f'{self.__class__.__name__}({self.bitsize}{modstr})' diff --git a/qualtran/dtype/_montgomery_uint_test.py b/qualtran/dtype/_montgomery_uint_test.py new file mode 100644 index 0000000000..6b80f05524 --- /dev/null +++ b/qualtran/dtype/_montgomery_uint_test.py @@ -0,0 +1,60 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +import sympy + +from qualtran.dtype import QMontgomeryUInt +from qualtran.symbolics import is_symbolic + + +def test_qmontgomeryuint(): + qmontgomeryuint_8 = QMontgomeryUInt(8) + assert str(qmontgomeryuint_8) == 'QMontgomeryUInt(8)' + assert qmontgomeryuint_8.num_qubits == 8 + # works + QMontgomeryUInt(1) + n = sympy.symbols('x') + qmontgomeryuint_8 = QMontgomeryUInt(n) + assert qmontgomeryuint_8.num_qubits == n + assert is_symbolic(QMontgomeryUInt(sympy.Symbol('x'))) + + +@pytest.mark.parametrize('p', [13, 17, 29]) +@pytest.mark.parametrize('val', [1, 5, 7, 9]) +def test_qmontgomeryuint_operations(val, p): + qmontgomeryuint_8 = QMontgomeryUInt(8, p) + # Convert value to montgomery form and get the modular inverse. + val_m = qmontgomeryuint_8.uint_to_montgomery(val) + mod_inv = qmontgomeryuint_8.montgomery_inverse(val_m) + + # Calculate the product in montgomery form and convert back to normal form for assertion. + assert ( + qmontgomeryuint_8.montgomery_to_uint(qmontgomeryuint_8.montgomery_product(val_m, mod_inv)) + == 1 + ) + + +@pytest.mark.parametrize('p', [13, 17, 29]) +@pytest.mark.parametrize('val', [1, 5, 7, 9]) +def test_qmontgomeryuint_conversions(val, p): + qmontgomeryuint_8 = QMontgomeryUInt(8, p) + assert val == qmontgomeryuint_8.montgomery_to_uint(qmontgomeryuint_8.uint_to_montgomery(val)) + + +@pytest.mark.parametrize('bitsize', range(1, 6)) +def test_montgomery_bit_conversion(bitsize): + dtype = QMontgomeryUInt(bitsize) + for v in range(1 << bitsize): + assert v == dtype.from_bits(dtype.to_bits(v)) diff --git a/qualtran/dtype/_uint.py b/qualtran/dtype/_uint.py new file mode 100644 index 0000000000..3f022536fe --- /dev/null +++ b/qualtran/dtype/_uint.py @@ -0,0 +1,128 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from functools import cached_property +from typing import Iterable, List, Sequence + +import attrs +import numpy as np +from numpy.typing import NDArray + +from qualtran.symbolics import is_symbolic, SymbolicInt + +from ._base import BitEncoding, CDType, QDType + + +@attrs.frozen +class _UInt(BitEncoding[int]): + """Unsigned integer of a given bitsize. + + Here (and throughout Qualtran), we use a big-endian bit convention. The most significant + bit is at index 0. + """ + + bitsize: SymbolicInt + + def get_domain(self) -> Iterable[int]: + return range(2**self.bitsize) + + def to_bits(self, x: int) -> List[int]: + self.assert_valid_val(x) + return [int(x) for x in f'{int(x):0{self.bitsize}b}'] + + def to_bits_array(self, x_array: NDArray[np.integer]) -> NDArray[np.uint8]: + if is_symbolic(self.bitsize): + raise ValueError(f"Cannot compute bits for symbolic {self.bitsize=}") + + if self.bitsize > 64: + return np.vectorize( + lambda x: np.asarray(self.to_bits(x), dtype=np.uint8), signature='()->(n)' + )(x_array) + + w = int(self.bitsize) + x = np.atleast_1d(x_array) + if not np.issubdtype(x.dtype, np.uint): + assert np.all(x >= 0) + assert np.iinfo(x.dtype).bits <= 64 + x = x.astype(np.uint64) + assert w <= np.iinfo(x.dtype).bits + mask = 2 ** np.arange(w - 1, 0 - 1, -1, dtype=x.dtype).reshape((w, 1)) + return (x & mask).astype(bool).astype(np.uint8).T + + def from_bits(self, bits: Sequence[int]) -> int: + return int("".join(str(x) for x in bits), 2) + + def from_bits_array(self, bits_array: NDArray[np.uint8]) -> NDArray[np.uint64]: + bitstrings = np.atleast_2d(bits_array) + if bitstrings.shape[1] != self.bitsize: + raise ValueError(f"Input bitsize {bitstrings.shape[1]} does not match {self.bitsize=}") + + if self.bitsize > 64: + # use the default vectorized `from_bits` + return np.vectorize(self.from_bits, signature='(n)->()')(bits_array) + + basis = 2 ** np.arange(self.bitsize - 1, 0 - 1, -1, dtype=np.uint64) + return np.sum(basis * bitstrings, axis=1, dtype=np.uint64) # type: ignore[return-value] + + def assert_valid_val(self, val: int, debug_str: str = 'val') -> None: + if not isinstance(val, (int, np.integer)): + raise ValueError(f"{debug_str} should be an integer, not {val!r}") + if val < 0: + raise ValueError(f"Negative classical value encountered in {debug_str}") + if val >= 2**self.bitsize: + raise ValueError(f"Too-large classical value encountered in {debug_str}") + + def assert_valid_val_array( + self, val_array: NDArray[np.integer], debug_str: str = 'val' + ) -> None: + if np.any(val_array < 0): + raise ValueError(f"Negative classical values encountered in {debug_str}") + if np.any(val_array >= 2**self.bitsize): + raise ValueError(f"Too-large classical values encountered in {debug_str}") + + +@attrs.frozen +class QUInt(QDType[int]): + """Unsigned quantum integer of a given bitsize. + + Here (and throughout Qualtran), we use a big-endian bit convention. The most significant + bit is at index 0. + + Args: + bitsize: The number of qubits used to represent the integer. + """ + + bitsize: SymbolicInt + + @cached_property + def _bit_encoding(self) -> BitEncoding[int]: + return _UInt(self.bitsize) + + +@attrs.frozen +class CUInt(CDType[int]): + """Unsigned classical integer of a given bitsize. + + Here (and throughout Qualtran), we use a big-endian bit convention. The most significant + bit is at index 0. + + Args: + bitsize: The number of classical bits used to represent the integer. + """ + + bitsize: SymbolicInt + + @cached_property + def _bit_encoding(self) -> BitEncoding[int]: + return _UInt(self.bitsize) diff --git a/qualtran/dtype/_uint_test.py b/qualtran/dtype/_uint_test.py new file mode 100644 index 0000000000..27cc01b953 --- /dev/null +++ b/qualtran/dtype/_uint_test.py @@ -0,0 +1,93 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import pytest +import sympy + +from qualtran.dtype import assert_to_and_from_bits_array_consistent, QUInt +from qualtran.symbolics import is_symbolic + + +def test_quint(): + qint_8 = QUInt(8) + assert str(qint_8) == 'QUInt(8)' + + assert qint_8.num_qubits == 8 + # works + QUInt(1) + n = sympy.symbols('x') + qint_8 = QUInt(n) + assert qint_8.num_qubits == n + assert is_symbolic(QUInt(sympy.Symbol('x'))) + + +def test_quint_to_and_from_bits(): + quint4 = QUInt(4) + assert [*quint4.get_classical_domain()] == [*range(0, 16)] + assert list(quint4.to_bits(10)) == [1, 0, 1, 0] + assert quint4.from_bits(quint4.to_bits(10)) == 10 + # MSB at lowest index -- big-endian + assert quint4.from_bits([0, 0, 0, 1]) == 1 + assert quint4.from_bits([0, 0, 0, 1]) < quint4.from_bits([1, 0, 0, 0]) + + for x in range(16): + assert quint4.from_bits(quint4.to_bits(x)) == x + with pytest.raises(ValueError): + quint4.to_bits(16) + + with pytest.raises(ValueError): + quint4.to_bits(-1) + + assert_to_and_from_bits_array_consistent(quint4, range(0, 16)) + + +def test_bits_to_int(): + cirq = pytest.importorskip('cirq') + rs = np.random.RandomState(52) + bitstrings = rs.choice([0, 1], size=(100, 23)) + + nums = QUInt(23).from_bits_array(bitstrings) + assert nums.shape == (100,) + + for num, bs in zip(nums, bitstrings): + ref_num = cirq.big_endian_bits_to_int(bs.tolist()) + assert num == ref_num + + # check one input bitstring instead of array of input bitstrings. + (num,) = QUInt(2).from_bits_array(np.array([1, 0])) + assert num == 2 + + +def test_int_to_bits(): + cirq = pytest.importorskip('cirq') + rs = np.random.RandomState(52) + nums = rs.randint(0, 2**23 - 1, size=(100,), dtype=np.uint64) + bitstrings = QUInt(23).to_bits_array(nums) + assert bitstrings.shape == (100, 23) + + for num, bs in zip(nums, bitstrings): + ref_bs = cirq.big_endian_int_to_bits(int(num), bit_count=23) + np.testing.assert_array_equal(ref_bs, bs) + + # check bounds + with pytest.raises(AssertionError): + QUInt(8).to_bits_array(np.array([4, -2])) + + +def test_iter_bits(): + assert QUInt(2).to_bits(0) == [0, 0] + assert QUInt(2).to_bits(1) == [0, 1] + assert QUInt(2).to_bits(2) == [1, 0] + assert QUInt(2).to_bits(3) == [1, 1] diff --git a/qualtran/dtype/gf/__init__.py b/qualtran/dtype/gf/__init__.py new file mode 100644 index 0000000000..0441766b26 --- /dev/null +++ b/qualtran/dtype/gf/__init__.py @@ -0,0 +1,16 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ._gf import CGF, QGF +from ._gf_poly import CGFPoly, QGFPoly diff --git a/qualtran/dtype/gf/_gf.py b/qualtran/dtype/gf/_gf.py new file mode 100644 index 0000000000..ccb2e86f7b --- /dev/null +++ b/qualtran/dtype/gf/_gf.py @@ -0,0 +1,245 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from functools import cached_property +from typing import Any, Iterable, List, Optional, Sequence, TYPE_CHECKING, Union + +import attrs +import numpy as np +from numpy.typing import NDArray + +from qualtran.symbolics import bit_length, is_symbolic, SymbolicInt + +from .._base import BitEncoding, CDType, QDType + +if TYPE_CHECKING: + import galois + + +def _poly_converter(p) -> Union['galois.Poly', None]: + import galois + + if p is None: + return None + if isinstance(p, galois.Poly): + return p + return galois.Poly.Degrees(p) + + +@attrs.frozen +class _GF(BitEncoding['galois.FieldArray']): + r"""Galois Field type to represent elements of a finite field.""" + + characteristic: SymbolicInt + degree: SymbolicInt + irreducible_poly: Optional['galois.Poly'] = attrs.field(converter=_poly_converter) + + @irreducible_poly.default + def _irreducible_poly_default(self): + if is_symbolic(self.characteristic, self.degree): + return None + + from galois import GF + + return GF( # type: ignore[call-overload] + int(self.characteristic), int(self.degree), compile='python-calculate' + ).irreducible_poly + + @cached_property + def order(self) -> SymbolicInt: + return self.characteristic**self.degree + + @cached_property + def bitsize(self) -> SymbolicInt: + """Bitsize of qubit register required to represent a single instance of this data type.""" + return bit_length(self.order - 1) + + def get_domain(self) -> Iterable[Any]: + yield from self.gf_type.elements + + @cached_property + def _uint_encoder(self) -> BitEncoding[int]: + from qualtran.dtype._uint import _UInt + + return _UInt(self.bitsize) + + @cached_property + def gf_type(self): + from galois import GF + + poly = self.irreducible_poly if self.degree > 1 else None + + return GF( # type: ignore[call-overload] + int(self.characteristic), + int(self.degree), + irreducible_poly=poly, + verify=False, + repr='poly', + compile='python-calculate', + ) + + def to_bits(self, x) -> List[int]: + self.assert_valid_val(x) + return self._uint_encoder.to_bits(int(x)) + + def from_bits(self, bits: Sequence[int]): + return self.gf_type(self._uint_encoder.from_bits(bits)) + + def from_bits_array(self, bits_array: NDArray[np.uint8]): + return self.gf_type(self._uint_encoder.from_bits_array(bits_array)) + + def assert_valid_val(self, val: Any, debug_str: str = 'val'): + if not isinstance(val, self.gf_type): + raise ValueError(f"{debug_str} should be a {self.gf_type}, not {val!r}") + + def assert_valid_val_array(self, val_array: NDArray[Any], debug_str: str = 'val'): + if np.any(val_array < 0): + raise ValueError(f"Negative classical values encountered in {debug_str}") + if np.any(val_array >= self.order): + raise ValueError(f"Too-large classical values encountered in {debug_str}") + + +@attrs.frozen +class QGF(QDType['galois.FieldArray']): + r"""Galois Field type to represent elements of a finite field. + + A Finite Field or Galois Field is a field that contains finite number of elements. The order + of a finite field is the number of elements in the field, which is either a prime number or + a prime power. For every prime number $p$ and every positive integer $m$ there are fields of + order $p^m$, all of which are isomorphic. When m=1, the finite field of order p can be + constructed via integers modulo p. + + Elements of a Galois Field $GF(p^m)$ may be conveniently viewed as polynomials + $a_{0} + a_{1}x + ... + a_{m−1}x_{m−1}$, where $a_0, a_1, ..., a_{m−1} \in F(p)$. + $GF(p^m)$ addition is defined as the component-wise (polynomial) addition over F(p) and + multiplication is defined as polynomial multiplication modulo an irreducible polynomial of + degree $m$. The selection of the specific irreducible polynomial affects the representation + of the given field, but all fields of a fixed size are isomorphic. + + The data type uses the [Galois library](https://mhostetter.github.io/galois/latest/) to + perform arithmetic over Galois Fields. By default, the Conway polynomial $C_{p, m}$ is used + as the irreducible polynomial. + + Args: + characteristic: The characteristic $p$ of the field $GF(p^m)$. + The characteristic must be prime. + degree: The degree $m$ of the field $GF(p^{m})$. The degree must be a positive integer. + irreducible_poly: Optional `galois.Poly` instance that defines the field arithmetic. + This parameter is passed to `galois.GF(..., irreducible_poly=irreducible_poly, verify=False)`. + + References: + [Finite Field](https://en.wikipedia.org/wiki/Finite_field) + + [Intro to Prime Fields](https://mhostetter.github.io/galois/latest/tutorials/intro-to-prime-fields/) + + [Intro to Extension Fields](https://mhostetter.github.io/galois/latest/tutorials/intro-to-extension-fields/) + """ + + characteristic: SymbolicInt + degree: SymbolicInt + irreducible_poly: Optional['galois.Poly'] = attrs.field(converter=_poly_converter) + + @irreducible_poly.default + def _irreducible_poly_default(self): + if is_symbolic(self.characteristic, self.degree): + return None + + from galois import GF + + return GF( # type: ignore[call-overload] + int(self.characteristic), int(self.degree), compile='python-calculate' + ).irreducible_poly + + @cached_property + def _bit_encoding(self) -> _GF: + return _GF( + characteristic=self.characteristic, + degree=self.degree, + irreducible_poly=self.irreducible_poly, + ) + + @property + def order(self) -> SymbolicInt: + return self._bit_encoding.order + + @property + def bitsize(self) -> SymbolicInt: + """Bitsize of qubit register required to represent a single instance of this data type.""" + return self._bit_encoding.bitsize + + @property + def gf_type(self): + return self._bit_encoding.gf_type + + def is_symbolic(self) -> bool: + return is_symbolic(self.characteristic, self.order) + + def iteration_length_or_zero(self) -> SymbolicInt: + return self.order + + def __str__(self): + return f'QGF({self.characteristic}**{self.degree})' + + +@attrs.frozen +class CGF(CDType['galois.FieldArray']): + r"""Galois Field classical type to represent elements of a finite field. + + See QGF for documentation. + """ + + characteristic: SymbolicInt + degree: SymbolicInt + irreducible_poly: Optional['galois.Poly'] = attrs.field(converter=_poly_converter) + + @irreducible_poly.default + def _irreducible_poly_default(self): + if is_symbolic(self.characteristic, self.degree): + return None + + from galois import GF + + return GF( # type: ignore[call-overload] + int(self.characteristic), int(self.degree), compile='python-calculate' + ).irreducible_poly + + @cached_property + def _bit_encoding(self) -> _GF: + return _GF( + characteristic=self.characteristic, + degree=self.degree, + irreducible_poly=self.irreducible_poly, + ) + + @property + def order(self) -> SymbolicInt: + return self._bit_encoding.order + + @property + def bitsize(self) -> SymbolicInt: + """Bitsize of qubit register required to represent a single instance of this data type.""" + return self._bit_encoding.bitsize + + @property + def gf_type(self): + return self._bit_encoding.gf_type + + def is_symbolic(self) -> bool: + return is_symbolic(self.characteristic, self.order) + + def iteration_length_or_zero(self) -> SymbolicInt: + return self.order + + def __str__(self): + return f'CGF({self.characteristic}**{self.degree})' diff --git a/qualtran/dtype/gf/_gf_poly.py b/qualtran/dtype/gf/_gf_poly.py new file mode 100644 index 0000000000..8187fe84a5 --- /dev/null +++ b/qualtran/dtype/gf/_gf_poly.py @@ -0,0 +1,185 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import itertools +from functools import cached_property +from typing import Any, Iterable, List, Sequence, TYPE_CHECKING + +import attrs +import numpy as np + +from qualtran.symbolics import is_symbolic, SymbolicInt + +from .._base import BitEncoding, CDType, QDType +from ._gf import _GF, QGF + +if TYPE_CHECKING: + import galois + + import qualtran.dtype as qdt + + +@attrs.frozen +class _GFPoly(BitEncoding): + r"""Univariate Polynomials with coefficients in a Galois Field GF($p^m$). + + Args: + degree: The degree $n$ of the univariate polynomial $f(x)$ represented by this type. + gf: An instance of `_GF` that represents the galois field $GF(p^m)$ over which the + univariate polynomial $f(x)$ is defined. + + """ + + degree: SymbolicInt + gf: _GF + + @cached_property + def bitsize(self) -> SymbolicInt: + return self.gf.bitsize * (self.degree + 1) + + def get_domain(self) -> Iterable[Any]: + """Yields all possible classical (computational basis state) values representable + by this type.""" + from galois import Poly + + for it in itertools.product(self.gf.gf_type.elements, repeat=(self.degree + 1)): + yield Poly(self.gf.gf_type(it), field=self.gf.gf_type) + + def to_gf_coefficients(self, f_x: 'galois.Poly') -> 'galois.Array': + """Returns a big-endian array of coefficients of the polynomial f(x).""" + f_x_coeffs = self.gf.gf_type.Zeros(self.degree + 1) + f_x_coeffs[self.degree - f_x.degree :] = f_x.coeffs + return f_x_coeffs + + def from_gf_coefficients(self, f_x: 'galois.Array') -> 'galois.Poly': + """Expects a big-endian array of coefficients that represent a polynomial f(x).""" + import galois + + return galois.Poly(f_x, field=self.gf.gf_type) + + def to_bits(self, x) -> List[int]: + """Returns individual bits corresponding to binary representation of x""" + import galois + + self.assert_valid_val(x) + assert isinstance(x, galois.Poly) + return self.gf.to_bits_array(self.to_gf_coefficients(x)).reshape(-1).tolist() + + def from_bits(self, bits: Sequence[int]): + """Combine individual bits to form x""" + reshaped_bits = np.array(bits).reshape((int(self.degree) + 1, int(self.gf.bitsize))) + return self.from_gf_coefficients(self.gf.from_bits_array(reshaped_bits)) # type: ignore + + def assert_valid_val(self, val: Any, debug_str: str = 'val'): + """Raises an exception if `val` is not a valid classical value for this type. + + Args: + val: A classical value that should be in the domain of this QDType. + debug_str: Optional debugging information to use in exception messages. + """ + import galois + + if not isinstance(val, galois.Poly): + raise ValueError(f"{debug_str} should be a {galois.Poly}, not {val!r}") + if val.field is not self.gf.gf_type: + raise ValueError( + f"{debug_str} should be defined over {self.gf.gf_type}, not {val.field}" + ) + if val.degree > self.degree: + raise ValueError(f"{debug_str} should have a degree <= {self.degree}, not {val.degree}") + + +@attrs.frozen +class QGFPoly(QDType): + r"""Quantum Univariate Polynomials with coefficients in a Galois Field GF($p^m$). + + This data type represents a degree-$n$ univariate polynomials + $f(x)=\sum_{i=0}^{n} a_i x^{i}$ where the coefficients $a_{i}$ of the polynomial + belong to a Galois Field $GF(p^{m})$. + + The data type uses the [Galois library](https://mhostetter.github.io/galois/latest/) to + perform arithmetic over polynomials defined over Galois Fields using the + [galois.Poly](https://mhostetter.github.io/galois/latest/api/galois.Poly/). + + Args: + degree: The degree $n$ of the univariate polynomial $f(x)$ represented by this type. + qgf: An instance of `QGF` that represents the galois field $GF(p^m)$ over which the + univariate polynomial $f(x)$ is defined. + + References: + [Polynomials over finite fields](https://mhostetter.github.io/galois/latest/api/galois.Poly/). + `galois` documentation. + + + [Polynomial Arithmetic](https://mhostetter.github.io/galois/latest/basic-usage/poly-arithmetic/). + `galois` documentation. + """ + + degree: SymbolicInt + qgf: QGF + + @cached_property + def _bit_encoding(self) -> _GFPoly: + return _GFPoly(self.degree, self.qgf._bit_encoding) + + @property + def bitsize(self) -> SymbolicInt: + return self._bit_encoding.bitsize + + def to_gf_coefficients(self, f_x: 'galois.Poly') -> 'galois.Array': + """Returns a big-endian array of coefficients of the polynomial f(x).""" + return self._bit_encoding.to_gf_coefficients(f_x) + + def from_gf_coefficients(self, f_x: 'galois.Array') -> 'galois.Poly': + """Expects a big-endian array of coefficients that represent a polynomial f(x).""" + return self._bit_encoding.from_gf_coefficients(f_x) + + @cached_property + def _quint_equivalent(self) -> 'qdt.QUInt': + from qualtran.dtype import QUInt + + return QUInt(self.num_qubits) + + def is_symbolic(self) -> bool: + return is_symbolic(self.degree, self.qgf) + + def iteration_length_or_zero(self) -> SymbolicInt: + return self.qgf.order + + def __str__(self): + return f'QGFPoly({self.degree}, {self.qgf!s})' + + +@attrs.frozen +class CGFPoly(CDType): + r"""Classical Univariate Polynomials with coefficients in a Galois Field GF($p^m$). + + This is a "classical" version of QGFPoly. + """ + + degree: SymbolicInt + qgf: QGF + + @cached_property + def _bit_encoding(self) -> _GFPoly: + return _GFPoly(self.degree, self.qgf._bit_encoding) + + def is_symbolic(self) -> bool: + return is_symbolic(self.degree, self.qgf) + + def iteration_length_or_zero(self) -> SymbolicInt: + return self.qgf.order + + def __str__(self): + return f'CGFPoly({self.degree}, {self.qgf!s})' diff --git a/qualtran/dtype/gf/_gf_poly_test.py b/qualtran/dtype/gf/_gf_poly_test.py new file mode 100644 index 0000000000..aa173b610a --- /dev/null +++ b/qualtran/dtype/gf/_gf_poly_test.py @@ -0,0 +1,34 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sympy + +from qualtran.dtype import assert_to_and_from_bits_array_consistent, QGF, QGFPoly +from qualtran.symbolics import ceil, is_symbolic, log2 + + +def test_qgf_poly(): + qgf_poly_4_8 = QGFPoly(4, QGF(characteristic=2, degree=3)) + assert str(qgf_poly_4_8) == 'QGFPoly(4, QGF(2**3))' + assert qgf_poly_4_8.num_qubits == 5 * 3 + n, p, m = sympy.symbols('n, p, m', integer=True, positive=True) + qgf_poly_n_pm = QGFPoly(n, QGF(characteristic=p, degree=m)) + assert qgf_poly_n_pm.num_qubits == (n + 1) * ceil(log2(p**m)) + assert is_symbolic(qgf_poly_n_pm) + + +def test_qgf_poly_to_and_from_bits(): + qgf_4 = QGF(2, 2) + qgf_poly_2_4 = QGFPoly(2, qgf_4) + assert_to_and_from_bits_array_consistent(qgf_poly_2_4, [*qgf_poly_2_4.get_classical_domain()]) diff --git a/qualtran/dtype/gf/_gf_test.py b/qualtran/dtype/gf/_gf_test.py new file mode 100644 index 0000000000..cbea7df468 --- /dev/null +++ b/qualtran/dtype/gf/_gf_test.py @@ -0,0 +1,55 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import pytest +import sympy + +from qualtran.dtype import assert_to_and_from_bits_array_consistent, QGF +from qualtran.symbolics import ceil, is_symbolic, log2 + + +def test_qgf(): + qgf_256 = QGF(characteristic=2, degree=8) + assert str(qgf_256) == 'QGF(2**8)' + assert qgf_256.num_qubits == 8 + p, m = sympy.symbols('p, m', integer=True, positive=True) + qgf_pm = QGF(characteristic=p, degree=m) + assert qgf_pm.num_qubits == ceil(log2(p**m)) + assert is_symbolic(qgf_pm) + + +def test_qgf_to_and_from_bits(): + from galois import GF + + qgf_256 = QGF(2, 8) + gf256 = GF(2**8) + assert [*qgf_256.get_classical_domain()] == [*range(256)] + a, b = qgf_256.to_bits(gf256(21)), qgf_256.to_bits(gf256(22)) + c = qgf_256.from_bits(list(np.bitwise_xor(a, b))) + assert c == gf256(21) + gf256(22) + for x in gf256.elements: + assert x == gf256.Vector(qgf_256.to_bits(x)) + + with pytest.raises(ValueError): + qgf_256.to_bits(21) # type: ignore[arg-type] + assert_to_and_from_bits_array_consistent(qgf_256, gf256([*range(256)])) + + +def test_qgf_with_default_poly_is_compatible(): + qgf_one = QGF(2, 4) + + qgf_two = QGF(2, 4, irreducible_poly=qgf_one.gf_type.irreducible_poly) + + assert qgf_one == qgf_two diff --git a/qualtran/dtype/testing.py b/qualtran/dtype/testing.py new file mode 100644 index 0000000000..a5ab3e61e2 --- /dev/null +++ b/qualtran/dtype/testing.py @@ -0,0 +1,109 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from enum import Enum +from typing import Any, Sequence, Union + +import numpy as np +from numpy.typing import NDArray + +from ._any import QAny +from ._base import QCDType, QDType +from ._buint import BQUInt +from ._fxp import QFxp +from ._int import QInt +from ._montgomery_uint import QMontgomeryUInt +from ._uint import QUInt +from .gf import QGF + +_QAnyInt = (QInt, QUInt, BQUInt, QMontgomeryUInt) +_QAnyUInt = (QUInt, BQUInt, QMontgomeryUInt, QGF) + + +def assert_to_and_from_bits_array_consistent(qdtype: QDType, values: Union[Sequence[Any], NDArray]): + values = np.asanyarray(values) + bits_array = qdtype.to_bits_array(values) + + # individual values + for val, bits in zip(values.reshape(-1), bits_array.reshape(-1, qdtype.num_qubits)): + assert np.all(bits == qdtype.to_bits(val)) + + # round trip + values_roundtrip = qdtype.from_bits_array(bits_array) + assert np.all(values_roundtrip == values) + + +class QDTypeCheckingSeverity(Enum): + """The level of type checking to enforce""" + + LOOSE = 0 + """Allow most type conversions between QAnyInt, QFxp and QAny.""" + + ANY = 1 + """Disallow numeric type conversions but allow QAny and single bit conversion.""" + + STRICT = 2 + """Strictly enforce type checking between registers. Only single bit conversions are allowed.""" + + +def _check_uint_fxp_consistent( + a: Union['QUInt', 'BQUInt', 'QMontgomeryUInt', 'QGF'], b: 'QFxp' +) -> bool: + """A uint / qfxp is consistent with a whole or totally fractional unsigned QFxp.""" + if b.signed: + return False + return a.num_qubits == b.num_qubits and (b.num_frac == 0 or b.num_int == 0) + + +def check_dtypes_consistent( + dtype_a: QCDType, + dtype_b: QCDType, + type_checking_severity: QDTypeCheckingSeverity = QDTypeCheckingSeverity.LOOSE, +) -> bool: + """Check if two types are consistent given our current definition on consistent types. + + Args: + dtype_a: The dtype to check against the reference. + dtype_b: The reference dtype. + type_checking_severity: Severity of type checking to perform. + + Returns: + True if the types are consistent. + """ + same_dtypes = dtype_a == dtype_b + same_n_qubits = dtype_a.num_qubits == dtype_b.num_qubits + if same_dtypes: + # Same types are always ok. + return True + elif dtype_a.num_qubits == 1 and same_n_qubits: + # Single qubit types are ok. + return True + if type_checking_severity == QDTypeCheckingSeverity.STRICT: + return False + if isinstance(dtype_a, QAny) or isinstance(dtype_b, QAny): + # QAny -> any dtype and any dtype -> QAny + return same_n_qubits + if type_checking_severity == QDTypeCheckingSeverity.ANY: + return False + if isinstance(dtype_a, _QAnyInt) and isinstance(dtype_b, _QAnyInt): + # A subset of the integers should be freely interchangeable. + return same_n_qubits + elif isinstance(dtype_a, _QAnyUInt) and isinstance(dtype_b, QFxp): + # unsigned Fxp which is wholly an integer or < 1 part is a uint. + return _check_uint_fxp_consistent(dtype_a, dtype_b) + elif isinstance(dtype_b, _QAnyUInt) and isinstance(dtype_a, QFxp): + # unsigned Fxp which is wholy an integer or < 1 part is a uint. + return _check_uint_fxp_consistent(dtype_b, dtype_a) + else: + return False diff --git a/qualtran/dtype/testing_test.py b/qualtran/dtype/testing_test.py new file mode 100644 index 0000000000..dc023cc636 --- /dev/null +++ b/qualtran/dtype/testing_test.py @@ -0,0 +1,82 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from qualtran.dtype import ( + BQUInt, + check_dtypes_consistent, + QAny, + QBit, + QFxp, + QGF, + QInt, + QIntOnesComp, + QMontgomeryUInt, + QUInt, +) +from qualtran.dtype.testing import _QAnyInt + + +@pytest.mark.parametrize('qdtype', [QIntOnesComp(4), QFxp(4, 4), QInt(4), QUInt(4), BQUInt(4, 5)]) +def test_qany_consistency(qdtype): + # All Types with correct bitsize are ok with QAny + assert check_dtypes_consistent(qdtype, QAny(4)) + + +@pytest.mark.parametrize('qdtype', [QUInt(4), BQUInt(4, 5), QMontgomeryUInt(4)]) +def test_type_errors_fxp_uint(qdtype): + assert check_dtypes_consistent(qdtype, QFxp(4, 4)) + assert check_dtypes_consistent(qdtype, QFxp(4, 0)) + assert not check_dtypes_consistent(qdtype, QFxp(4, 2)) + assert not check_dtypes_consistent(qdtype, QFxp(4, 3, True)) + assert not check_dtypes_consistent(qdtype, QFxp(4, 0, True)) + + +@pytest.mark.parametrize('qdtype', [QInt(4), QIntOnesComp(4)]) +def test_type_errors_fxp_int(qdtype): + assert not check_dtypes_consistent(qdtype, QFxp(4, 0)) + assert not check_dtypes_consistent(qdtype, QFxp(4, 4)) + + +def test_type_errors_fxp(): + assert not check_dtypes_consistent(QFxp(4, 4), QFxp(4, 0)) + assert not check_dtypes_consistent(QFxp(4, 3, signed=True), QFxp(4, 0)) + assert not check_dtypes_consistent(QFxp(4, 3), QFxp(4, 0)) + + +@pytest.mark.parametrize( + 'qdtype_a', [QUInt(4), BQUInt(4, 5), QMontgomeryUInt(4), QInt(4), QIntOnesComp(4)] +) +@pytest.mark.parametrize( + 'qdtype_b', [QUInt(4), BQUInt(4, 5), QMontgomeryUInt(4), QInt(4), QIntOnesComp(4)] +) +def test_type_errors_matrix(qdtype_a, qdtype_b): + if qdtype_a == qdtype_b: + assert check_dtypes_consistent(qdtype_a, qdtype_b) + elif isinstance(qdtype_a, _QAnyInt) and isinstance(qdtype_b, _QAnyInt): + assert check_dtypes_consistent(qdtype_a, qdtype_b) + else: + assert not check_dtypes_consistent(qdtype_a, qdtype_b) + + +def test_single_qubit_consistency(): + assert str(QBit()) == 'QBit()' + assert check_dtypes_consistent(QBit(), QBit()) + assert check_dtypes_consistent(QBit(), QInt(1)) + assert check_dtypes_consistent(QInt(1), QBit()) + assert check_dtypes_consistent(QAny(1), QBit()) + assert check_dtypes_consistent(BQUInt(1), QBit()) + assert check_dtypes_consistent(QFxp(1, 1), QBit()) + assert check_dtypes_consistent(QGF(characteristic=2, degree=1), QBit())