diff --git a/qualtran/DataTypes.ipynb b/qualtran/DataTypes.ipynb
index 018219dc8..07c83a2dc 100644
--- a/qualtran/DataTypes.ipynb
+++ b/qualtran/DataTypes.ipynb
@@ -13,10 +13,70 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 1,
"id": "780e26eb-b50a-46fd-aec9-a74d68201c2f",
"metadata": {},
- "outputs": [],
+ "outputs": [
+ {
+ "data": {
+ "image/svg+xml": [
+ ""
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
"source": [
"from qualtran import BloqBuilder\n",
"from qualtran.bloqs.basic_gates import ZeroState, PlusState\n",
@@ -56,10 +116,18 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 2,
"id": "9da1ffe1-b214-43aa-82f6-098dffec10eb",
"metadata": {},
- "outputs": [],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "2\n"
+ ]
+ }
+ ],
"source": [
"from qualtran import Signature, Register, QBit\n",
"\n",
@@ -88,10 +156,18 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 3,
"id": "578c71e2-3b6b-43d1-bb91-88c94ae4f70e",
"metadata": {},
- "outputs": [],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "64\n"
+ ]
+ }
+ ],
"source": [
"from qualtran import QUInt\n",
"\n",
@@ -116,10 +192,75 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 4,
"id": "fb259875-b438-4ee8-882c-092bd9711682",
"metadata": {},
- "outputs": [],
+ "outputs": [
+ {
+ "data": {
+ "image/svg+xml": [
+ ""
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
"source": [
"from qualtran.bloqs.arithmetic import Negate\n",
"\n",
@@ -141,10 +282,136 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 5,
"id": "9b5fdb88-9a4d-4ccc-801f-76769df67214",
"metadata": {},
- "outputs": [],
+ "outputs": [
+ {
+ "data": {
+ "image/svg+xml": [
+ ""
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
"source": [
"from qualtran.bloqs.basic_gates import XGate\n",
"\n",
@@ -187,10 +454,21 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 6,
"id": "bbe875d9-7a15-488c-ad19-02cd0487765a",
"metadata": {},
- "outputs": [],
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[0, 0, 1, 1, 0, 0, 0, 0]"
+ ]
+ },
+ "execution_count": 6,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
"QUInt(8).to_bits(x=0x30)"
]
@@ -211,10 +489,114 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 7,
"id": "3ecf4057-9ea9-4854-8b66-d5b041a8cc49",
"metadata": {},
- "outputs": [],
+ "outputs": [
+ {
+ "data": {
+ "image/svg+xml": [
+ ""
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
"source": [
"bb = BloqBuilder()\n",
"\n",
@@ -255,10 +637,22 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 8,
"id": "9f4e4a13-e112-4b7e-95e1-6fee0168d989",
"metadata": {},
- "outputs": [],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "same True\n",
+ "1bit True\n",
+ "qany True False\n",
+ "qint True False\n",
+ "diff False\n"
+ ]
+ }
+ ],
"source": [
"from qualtran import QDTypeCheckingSeverity, check_dtypes_consistent\n",
"\n",
@@ -283,49 +677,158 @@
"source": [
"## `QDType`, `CDType`, and `QCDType`\n",
"\n",
- "Quantum variables are essential when authoring quantum programs, but we live in a classical world. Measuring a qubit yields a classical bit, and a program can do classical branching (choosing which quantum operations to execute based on a classical bit). Each data type we've seen so far is a quantum data type and inherits from `QDType`. "
+ "Quantum variables are essential when authoring quantum programs, but we live in a classical world. Measuring a qubit yields a classical bit, and a program can do classical branching (choosing which quantum operations to execute based on a classical bit). Each data type we've seen so far is a quantum data type and inherits from `QDType`. There is a more general base class: `QCDType` that includes both quantum and classical data types. Classical data types inherit from `CDType`"
]
},
{
"cell_type": "code",
- "execution_count": null,
- "id": "06c5c158-555b-48cd-b612-e00d03b82f70",
+ "execution_count": 9,
+ "id": "25191c43-18d1-49fd-9a76-363cc3b7548c",
"metadata": {},
"outputs": [],
"source": [
- "from qualtran import QDType\n",
- "\n",
- "print(\"QBit() is QDType:\", isinstance(QBit(), QDType), \"; num_qubits =\", QBit().num_qubits)\n",
- "print(\"QUInt(4) is QDType:\", isinstance(QUInt(4), QDType), \"; num_qubits =\", QUInt(4).num_qubits)"
+ "# Make a list of all the built-in data types\n",
+ "import qualtran.dtype as qdt\n",
+ "\n",
+ "dtypes = [\n",
+ " qdt.QBit(), qdt.CBit(),\n",
+ " qdt.QAny(8),\n",
+ " qdt.QInt(8), qdt.CInt(8),\n",
+ " qdt.QIntOnesComp(8), qdt.CIntOnesComp(8),\n",
+ " qdt.QUInt(8), qdt.CUInt(8),\n",
+ " qdt.BQUInt(8, 230), qdt.BCUInt(8, 230),\n",
+ " qdt.QFxp(8, 2), qdt.CFxp(8, 2)\n",
+ "]"
]
},
{
"cell_type": "markdown",
- "id": "c434a68e-b691-4a35-bc81-49b7159728f8",
+ "id": "701ebb85-8e44-4456-95da-ef2ead979c5f",
"metadata": {},
"source": [
- "There is a more general base class: `QCDType` that includes both quantum and classical data types. Classical data types inherit from `CDType`"
+ "### Table of data type properties"
]
},
{
"cell_type": "code",
- "execution_count": null,
- "id": "c983e4cb-ac84-497b-883f-3a71abf6878f",
+ "execution_count": 10,
+ "id": "7c38eb9b-60d5-4d0c-b282-19f2638bc483",
"metadata": {},
- "outputs": [],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "dtype QCDType? QDType? CDType? bits qubits cbits\n",
+ "--------------------------------------------------------------------------------\n",
+ "QBit() True True False 1 1 0\n",
+ "CBit() True False True 1 0 1\n",
+ "QAny(8) True True False 8 8 0\n",
+ "QInt(8) True True False 8 8 0\n",
+ "CInt(8) True False True 8 0 8\n",
+ "QIntOnesComp(8) True True False 8 8 0\n",
+ "CIntOnesComp(8) True False True 8 0 8\n",
+ "QUInt(8) True True False 8 8 0\n",
+ "CUInt(8) True False True 8 0 8\n",
+ "BQUInt(8, 230) True True False 8 8 0\n",
+ "BCUInt(8, 230) True False True 8 0 8\n",
+ "QFxp(8, 2) True True False 8 8 0\n",
+ "CFxp(8, 2) True True False 8 8 0\n"
+ ]
+ }
+ ],
"source": [
- "from qualtran import QCDType, QDType, CDType, CBit\n",
- "\n",
- "dtypes = [QBit(), QUInt(4), CBit()]\n",
- "\n",
- "print(f\"{'dtype':10} {'QCDType?':9s} {'QDType?':9s} {'CDType?':9s}\"\n",
- " f\"{'bits':>6s} {'qubits':>6s} {'cbits':>6s}\"\n",
- " )\n",
+ "print(f\"{'dtype':15} {'QCDType?':9s} \"\n",
+ " f\"{'QDType?':9s} {'CDType?':9s} \"\n",
+ " f\"{'bits':>6s} {'qubits':>6s} {'cbits':>6s}\")\n",
+ "print(\"-\"*80)\n",
+ "for dtype in dtypes:\n",
+ " print(f\"{dtype!s:15} {isinstance(dtype, qdt.QCDType)!s:9} \"\n",
+ " f\"{isinstance(dtype, qdt.QDType)!s:9} {isinstance(dtype, qdt.CDType)!s:9}\"\n",
+ " f\"{dtype.num_bits:6d} {dtype.num_qubits:6d} {dtype.num_cbits:6d}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c2089b62-36ce-4c90-b794-4e5e8338de47",
+ "metadata": {},
+ "source": [
+ "### Table of data type encoding examples"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "id": "dd0c1fd9-94d3-45fd-8766-c18615fc5123",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "dtype val encoding\n",
+ "------------------------------------------------------------\n",
+ "QBit() 0 0\n",
+ " 1 1\n",
+ "CBit() 0 0\n",
+ " 1 1\n",
+ "QAny(8) [opaque encoding]\n",
+ "QInt(8) -128 1,0,0,0,0,0,0,0\n",
+ " 127 0,1,1,1,1,1,1,1\n",
+ "CInt(8) -128 1,0,0,0,0,0,0,0\n",
+ " 127 0,1,1,1,1,1,1,1\n",
+ "QIntOnesComp(8) -127 1,0,0,0,0,0,0,0\n",
+ " 127 0,1,1,1,1,1,1,1\n",
+ "CIntOnesComp(8) -127 1,0,0,0,0,0,0,0\n",
+ " 127 0,1,1,1,1,1,1,1\n",
+ "QUInt(8) 0 0,0,0,0,0,0,0,0\n",
+ " 255 1,1,1,1,1,1,1,1\n",
+ "CUInt(8) 0 0,0,0,0,0,0,0,0\n",
+ " 255 1,1,1,1,1,1,1,1\n",
+ "BQUInt(8, 230) 0 0,0,0,0,0,0,0,0\n",
+ " 229 1,1,1,0,0,1,0,1\n",
+ "BCUInt(8, 230) 0 0,0,0,0,0,0,0,0\n",
+ " 229 1,1,1,0,0,1,0,1\n",
+ "QFxp(8, 2) 0 0,0,0,0,0,0,0,0\n",
+ " 255 1,1,1,1,1,1,1,1\n",
+ "CFxp(8, 2) 0 0,0,0,0,0,0,0,0\n",
+ " 255 1,1,1,1,1,1,1,1\n"
+ ]
+ }
+ ],
+ "source": [
+ "print(f\"{'dtype':15} {'val':>6s} \"\n",
+ " f\"{'encoding':s}\")\n",
"print(\"-\"*60)\n",
"for dtype in dtypes:\n",
- " print(f\"{dtype!s:10} {isinstance(dtype, QCDType)!s:9} {isinstance(dtype, QDType)!s:9} {isinstance(dtype, CDType)!s:9}\"\n",
- " f\"{dtype.num_bits:6d} {dtype.num_qubits:6d} {dtype.num_cbits:6d}\"\n",
- " )"
+ " if isinstance(dtype, QAny):\n",
+ " print(f\"{dtype!s:15} {'':6s} [opaque encoding]\")\n",
+ " continue\n",
+ " vals = list(dtype.get_classical_domain())\n",
+ "\n",
+ " val = vals[0] # take the minimum\n",
+ " val_bits = dtype.to_bits(val)\n",
+ " val_bits_str = ','.join(str(bit) for bit in val_bits)\n",
+ " print(f\"{dtype!s:15} {val!s:>6} {val_bits_str}\")\n",
+ "\n",
+ " val = vals[-1] # take the maximum\n",
+ " val_bits = dtype.to_bits(val)\n",
+ " val_bits_str = ','.join(str(bit) for bit in val_bits)\n",
+ " print(f\"{'':15} {val!s:>6} {val_bits_str}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a40933a4-1656-4a5a-b352-a09daf626518",
+ "metadata": {},
+ "source": [
+ "## Adding a new type\n",
+ "\n",
+ "You can add custom types by implementing the `_BitEncoding` interface and defining a light-weight `QDType` and/or `CDType`.\n",
+ "\n",
+ " 1. Define a class that implements the `BitEncoding[T]` abstract interface. the generic `T` should be the type of the analogous Python classical type. For example, `_Int` inherits from `BitEncoding[int]` and defines methods `to_bits`, `from_bits`, etc.\n",
+ " 2. Define a _quantum_ data type class that inherits from the `QDType[T]` base class. Override the abstract property `_bit_encoding` to return an instance of the bit encoding class you defined before. For example, `QInt` inherits from `QDType[int]` and its `_bit_encoding` property returns `_Int(self.bitsize)`. \n",
+ " 3. Perform an analogous step to deinfe a _classical_ data type class that inherits from `CDType[T]` (but returns the same `BitEncoding` implementation). For example, `CInt` inherits from `CDType[int]` and its `_bit_encoding` property returns `_Int(self.bitsize)`. "
]
}
],
diff --git a/qualtran/__init__.py b/qualtran/__init__.py
index 1b92b7e18..eb24ec9b0 100644
--- a/qualtran/__init__.py
+++ b/qualtran/__init__.py
@@ -58,10 +58,15 @@
QBit,
CBit,
QInt,
+ CInt,
QIntOnesComp,
+ CIntOnesComp,
QUInt,
+ CUInt,
BQUInt,
+ BCUInt,
QFxp,
+ CFxp,
QMontgomeryUInt,
QGF,
QGFPoly,
diff --git a/qualtran/_infra/data_types.py b/qualtran/_infra/data_types.py
index 00d3d49a0..40ec76706 100644
--- a/qualtran/_infra/data_types.py
+++ b/qualtran/_infra/data_types.py
@@ -15,22 +15,140 @@
import abc
+import warnings
from enum import Enum
from functools import cached_property
-from typing import Any, Iterable, List, Optional, Sequence, Union
+from typing import Any, cast, Generic, Iterable, List, Optional, Sequence, TypeVar, Union
import attrs
import galois
import numpy as np
-from fxpmath import Fxp
from numpy.typing import NDArray
from qualtran.symbolics import bit_length, is_symbolic, SymbolicInt
+T = TypeVar('T')
-class QCDType(metaclass=abc.ABCMeta):
+
+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)
+
+
+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
@@ -47,49 +165,47 @@ def num_qubits(self) -> int:
def num_cbits(self) -> int:
"""Number of classical bits required to represent a single instance of this data type."""
- @abc.abstractmethod
- def get_classical_domain(self) -> Iterable[Any]:
+ 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()
- @abc.abstractmethod
- def to_bits(self, x) -> List[int]:
+ 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[Any]) -> NDArray[np.uint8]:
+ 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)
+ return self._bit_encoding.to_bits_array(x_array)
- @abc.abstractmethod
- def from_bits(self, bits: Sequence[int]):
+ 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]):
+ 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)
+ return self._bit_encoding.from_bits_array(bits_array)
- @abc.abstractmethod
- def assert_valid_classical_val(self, val: Any, debug_str: str = 'val'):
+ 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[Any], debug_str: str = 'val'):
+ 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.
@@ -101,27 +217,42 @@ def assert_valid_classical_val_array(self, val_array: NDArray[Any], debug_str: s
of this QDType.
debug_str: Optional debugging information to use in exception messages.
"""
- for val in val_array.reshape(-1):
- self.assert_valid_classical_val(val)
+ return self._bit_encoding.assert_valid_val_array(val_array=val_array, debug_str=debug_str)
- @abc.abstractmethod
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 __str__(self):
- return f'{self.__class__.__name__}({self.num_qubits})'
+ return f'{self.__class__.__name__}({self.num_bits})'
-class QDType(QCDType, metaclass=abc.ABCMeta):
+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."""
@@ -131,7 +262,7 @@ def __str__(self):
return f'{self.__class__.__name__}({self.num_qubits})'
-class CDType(QCDType, metaclass=abc.ABCMeta):
+class CDType(QCDType[T], metaclass=abc.ABCMeta):
"""The abstract interface for classical data types."""
@property
@@ -139,29 +270,35 @@ 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})'
-class _Bit(metaclass=abc.ABCMeta):
+@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.
"""
- def get_classical_domain(self) -> Iterable[int]:
+ @property
+ def bitsize(self) -> int:
+ return 1
+
+ def get_domain(self) -> Iterable[int]:
yield from (0, 1)
- def assert_valid_classical_val(self, val: int, debug_str: str = 'val'):
+ def assert_valid_val(self, val: int, debug_str: str = 'val'):
if not (val == 0 or val == 1):
- raise ValueError(f"Bad {self} value {val} in {debug_str}")
-
- def is_symbolic(self) -> bool:
- return False
+ raise ValueError(f"Bad bit value: {val} in {debug_str}")
- def to_bits(self, x) -> List[int]:
+ def to_bits(self, x: int) -> List[int]:
"""Yields individual bits corresponding to binary representation of x"""
- self.assert_valid_classical_val(x)
+ self.assert_valid_val(x)
return [int(x)]
def from_bits(self, bits: Sequence[int]) -> int:
@@ -169,95 +306,84 @@ def from_bits(self, bits: Sequence[int]) -> int:
assert len(bits) == 1
return bits[0]
- def assert_valid_classical_val_array(
+ 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 {self} value array in {debug_str}")
-
- def __str__(self):
- return f'{self.__class__.__name__}()'
+ raise ValueError(f"Bad bit value array in {debug_str}")
@attrs.frozen
-class QBit(_Bit, QDType):
+class QBit(QDType[int]):
"""A single qubit. The smallest addressable unit of quantum data."""
- @property
- def num_qubits(self):
- return 1
+ @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(_Bit, CDType):
+class CBit(CDType[int]):
"""A single classical bit. The smallest addressable unit of classical data."""
- @property
- def num_cbits(self) -> int:
- return 1
+ @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):
+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()
-
- @property
- def num_qubits(self):
- return self.bitsize
+ 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 to_bits(self, x) -> List[int]:
- # TODO: Raise an error once usage of `QAny` is minimized across the library
- return QUInt(self.bitsize).to_bits(x)
-
- def from_bits(self, bits: Sequence[int]) -> int:
- # TODO: Raise an error once usage of `QAny` is minimized across the library
- return QUInt(self.bitsize).from_bits(bits)
-
- def is_symbolic(self) -> bool:
- return is_symbolic(self.bitsize)
-
- def assert_valid_classical_val(self, val, debug_str: str = 'val'):
+ def assert_valid_classical_val(self, val: Any, debug_str: str = 'val'):
pass
- def assert_valid_classical_val_array(self, val_array, debug_str: str = 'val'):
+ def assert_valid_classical_val_array(self, val_array: NDArray, debug_str: str = 'val'):
pass
@attrs.frozen
-class QInt(QDType):
- """Signed Integer of a given width bitsize.
-
- A two's complement representation is used for negative integers.
+class _Int(BitEncoding[int]):
+ """Signed integer of a given bitsize.
- Here (and throughout Qualtran), we use a big-endian bit convention. The most significant
- bit is at index 0.
+ Use `QInt` or `CInt` for quantum or classical implementations, respectively.
- Args:
- bitsize: The number of qubits used to represent the integer.
+ 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
- @property
- def num_qubits(self):
- return self.bitsize
-
- def is_symbolic(self) -> bool:
- return is_symbolic(self.bitsize)
-
- def get_classical_domain(self) -> Iterable[int]:
+ def get_domain(self) -> Iterable[int]:
max_val = 1 << (self.bitsize - 1)
return range(-max_val, max_val)
@@ -265,7 +391,7 @@ 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_classical_val(x)
+ 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:
@@ -273,11 +399,11 @@ def from_bits(self, bits: Sequence[int]) -> int:
x = (
0
if self.bitsize == 1
- else QUInt(self.bitsize - 1).from_bits([1 - x if sign else x for x in bits[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_classical_val(self, val: int, debug_str: str = 'val'):
+ 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)):
@@ -285,27 +411,39 @@ def assert_valid_classical_val(self, val: int, debug_str: str = 'val'):
if val >= 2 ** (self.bitsize - 1):
raise ValueError(f"Too-large classical {self}: {val} encountered in {debug_str}")
- def assert_valid_classical_val_array(
- self, val_array: NDArray[np.integer], debug_str: str = 'val'
- ):
+ 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}")
- def __str__(self):
- return f'QInt({self.bitsize})'
-
@attrs.frozen
-class QIntOnesComp(QDType):
- """Signed Integer of a given width bitsize.
+class QInt(QDType[int]):
+ """Signed quantum integer of a given bitsize.
- In contrast to `QInt`, this data type uses the ones' complement representation for negative
- integers.
+ 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.
- 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.
@@ -313,31 +451,41 @@ class QIntOnesComp(QDType):
bitsize: SymbolicInt
- def __attrs_post_init__(self):
- if isinstance(self.bitsize, int):
- if self.num_qubits == 1:
- raise ValueError("num_qubits must be > 1.")
+ @cached_property
+ def _bit_encoding(self) -> BitEncoding[int]:
+ return _Int(self.bitsize)
- @property
- def num_qubits(self):
- return self.bitsize
- def is_symbolic(self) -> bool:
- return is_symbolic(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_classical_val(x)
- return [int(x < 0)] + [y ^ int(x < 0) for y in QUInt(self.bitsize - 1).to_bits(abs(x))]
+ 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 = QUInt(self.bitsize).from_bits([b ^ bits[0] for b in bits[1:]])
+ x = _UInt(self.bitsize).from_bits([b ^ bits[0] for b in bits[1:]])
return (-1) ** bits[0] * x
- def get_classical_domain(self) -> Iterable[int]:
+ def get_domain(self) -> Iterable[int]:
max_val = 1 << (self.bitsize - 1)
return range(-max_val + 1, max_val)
- def assert_valid_classical_val(self, val, debug_str: str = '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)
@@ -348,14 +496,13 @@ def assert_valid_classical_val(self, val, debug_str: str = 'val'):
@attrs.frozen
-class QUInt(QDType):
- """Unsigned integer of a given width bitsize which wraps around upon overflow.
+class QIntOnesComp(QDType[int]):
+ """Ones' complement signed quantum integer of a given bitsize.
- Any intended wrap around effect is expected to be handled by the developer, similar
- to an unsigned integer type in C.
-
- Here (and throughout Qualtran), we use a big-endian bit convention. The most significant
- bit is at index 0.
+ 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.
@@ -363,18 +510,56 @@ class QUInt(QDType):
bitsize: SymbolicInt
- @property
- def num_qubits(self):
- return self.bitsize
+ def __attrs_post_init__(self):
+ if isinstance(self.bitsize, int):
+ if self.bitsize == 1:
+ raise ValueError("bitsize must be > 1.")
- def is_symbolic(self) -> bool:
- return is_symbolic(self.bitsize)
+ @cached_property
+ def _bit_encoding(self) -> BitEncoding[int]:
+ return _IntOnesComp(self.bitsize)
- def get_classical_domain(self) -> Iterable[Any]:
+
+@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_classical_val(x)
+ 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]:
@@ -382,8 +567,9 @@ def to_bits_array(self, x_array: NDArray[np.integer]) -> NDArray[np.uint8]:
raise ValueError(f"Cannot compute bits for symbolic {self.bitsize=}")
if self.bitsize > 64:
- # use the default vectorized `to_bits`
- return super().to_bits_array(x_array)
+ 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)
@@ -405,12 +591,12 @@ def from_bits_array(self, bits_array: NDArray[np.uint8]) -> NDArray[np.integer]:
if self.bitsize > 64:
# use the default vectorized `from_bits`
- return super().from_bits_array(bits_array)
+ 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)
- def assert_valid_classical_val(self, val: int, debug_str: str = '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}")
if val < 0:
@@ -418,29 +604,115 @@ def assert_valid_classical_val(self, val: int, debug_str: str = 'val'):
if val >= 2**self.bitsize:
raise ValueError(f"Too-large classical value encountered in {debug_str}")
- def assert_valid_classical_val_array(
+ 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 __str__(self):
- return f'QUInt({self.bitsize})'
-
@attrs.frozen
-class BQUInt(QDType):
- """Unsigned integer whose values are bounded within a range.
+class QUInt(QDType[int]):
+ """Unsigned quantum integer of a given bitsize.
- 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.
+ Here (and throughout Qualtran), we use a big-endian bit convention. The most significant
+ bit is at index 0.
- One useful feature when processing such nested for-loops is to flatten out a composite index,
+ 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
@@ -485,7 +757,7 @@ def __attrs_post_init__(self):
if not self.is_symbolic():
if self.iteration_length > 2**self.bitsize:
raise ValueError(
- "BQUInt iteration length is too large for given bitsize. "
+ f"{self} iteration length is too large for given bitsize. "
f"{self.iteration_length} vs {2**self.bitsize}"
)
@@ -493,78 +765,65 @@ def __attrs_post_init__(self):
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 num_qubits(self):
- return self.bitsize
+ def _bit_encoding(self) -> BitEncoding[int]:
+ return _BUInt(self.bitsize, self.iteration_length)
- 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 __str__(self):
+ return f'{self.__class__.__name__}({self.bitsize}, {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)
+@attrs.frozen
+class BCUInt(CDType[int]):
+ """Unsigned classical integer whose values are bounded within a range.
- def from_bits(self, bits: Sequence[int]) -> int:
- """Combine individual bits to form x"""
- return QUInt(self.bitsize).from_bits(bits)
+ Args:
+ bitsize: The number of bits used to represent the integer.
+ bound: The value bound (exclusive).
+ """
- 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}")
+ bitsize: SymbolicInt
+ bound: SymbolicInt = attrs.field()
- def __str__(self):
- return f'{self.__class__.__name__}({self.bitsize}, {self.iteration_length})'
+ 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
-@attrs.frozen
-class QFxp(QDType):
- r"""Fixed point type to represent real numbers.
+ def is_symbolic(self) -> bool:
+ return is_symbolic(self.bitsize, self.bound)
- 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.
+ @property
+ def _bit_encoding(self) -> BitEncoding[int]:
+ return _BUInt(self.bitsize, self.bound)
- We can specify a fixed point real number by the tuple bitsize, num_frac and
- signed, with num_int determined as `(bitsize - num_frac)`.
+ def __str__(self):
+ return f'{self.__class__.__name__}({self.bitsize}, {self.bound})'
- **Classical Simulation:**
+
+@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`.
- 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`.
-
See https://github.com/quantumlib/Qualtran/issues/1219 for discussion on alternatives
and future upgrades.
@@ -581,104 +840,83 @@ class QFxp(QDType):
signed: bool = False
def __attrs_post_init__(self):
- if not is_symbolic(self.num_qubits) and self.num_qubits == 1 and self.signed:
- raise ValueError("num_qubits must be > 1.")
+ 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 QFxp is signed.")
+ 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_qubits(self):
- return self.bitsize
-
@property
def num_int(self) -> SymbolicInt:
"""Number of bits for the integral part."""
return self.bitsize - self.num_frac
- def is_symbolic(self) -> bool:
- return is_symbolic(self.bitsize, self.num_frac)
-
@property
- def _int_qdtype(self) -> Union[QUInt, QInt]:
- # The corresponding dtype for the raw integer representation.
- # See class docstring section on "Classical Simulation" for more details.
- return QInt(self.bitsize) if self.signed else QUInt(self.bitsize)
+ 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_classical_domain(self) -> Iterable[int]:
- # Use the classical domain for the underlying raw integer type.
- # See class docstring section on "Classical Simulation" for more details.
- yield from self._int_qdtype.get_classical_domain()
+ 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) -> List[int]:
- # Use the underlying raw integer type.
- # See class docstring section on "Classical Simulation" for more details.
- return self._int_qdtype.to_bits(x)
+ 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]):
- # Use the underlying raw integer type.
- # See class docstring section on "Classical Simulation" for more details.
- return self._int_qdtype.from_bits(bits)
+ def from_bits(self, bits: Sequence[int]) -> int:
+ # Use the underlying raw integer encoding.
+ return self._int_encoding.from_bits(bits)
- def assert_valid_classical_val(self, val: int, debug_str: str = 'val'):
- # Verify using the underlying raw integer type.
- # See class docstring section on "Classical Simulation" for more details.
- self._int_qdtype.assert_valid_classical_val(val, debug_str)
+ 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, Fxp], *, require_exact: bool = False, complement: bool = True
+ 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
+ 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_qdtype.from_bits(bits)
+ 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.
-
- See class docstring section on "Classical Simulation" for more details on
- the choice of this representation.
"""
return x / 2**self.num_frac
def __str__(self):
if self.signed:
- return f'QFxp({self.bitsize}, {self.num_frac}, True)'
+ return f'_Fxp({self.bitsize}, {self.num_frac}, True)'
else:
- return f'QFxp({self.bitsize}, {self.num_frac})'
+ return f'_Fxp({self.bitsize}, {self.num_frac})'
- def fxp_dtype_template(self) -> Fxp:
- """A template of the `Fxp` data type for classical values.
+ def fxp_dtype_template(self) -> 'fxpmath.Fxp':
+ """A template of the `fxpmath.Fxp` data type for classical values.
- To construct an `Fxp` with this config, one can use:
- `Fxp(float_value, like=QFxp(...).fxp_dtype_template)`,
+ 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(QFxp(...).fxp_dtype_template)`.
+ `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
@@ -694,12 +932,14 @@ def fxp_dtype_template(self) -> Fxp:
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 Fxp(
+ return fxpmath.Fxp(
None,
n_word=self.bitsize,
n_frac=self.num_frac,
@@ -710,12 +950,14 @@ def fxp_dtype_template(self) -> Fxp:
overflow='wrap',
)
- def _get_classical_domain_fxp(self) -> Iterable[Fxp]:
- for x in self._int_qdtype.get_classical_domain():
- yield Fxp(x / 2**self.num_frac, like=self.fxp_dtype_template())
+ 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, Fxp], require_exact: bool = True, complement: bool = True
+ self, x: Union[float, 'fxpmath.Fxp'], require_exact: bool = True, complement: bool = True
) -> List[int]:
"""Yields individual bits corresponding to binary representation of `x`.
@@ -725,36 +967,225 @@ def _fxp_to_bits(
complement: Use twos-complement rather than sign-magnitude representation of negative values.
Raises:
- ValueError: If `x` is negative but this `QFxp` is not signed.
+ ValueError: If `x` is negative but this `_Fxp` is not signed.
"""
+ import fxpmath
+
if require_exact:
- self._assert_valid_classical_val(x)
+ self._assert_valid_val(x)
if x < 0 and not self.signed:
- raise ValueError(f"unsigned QFxp cannot represent {x}.")
+ 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, Fxp) else Fxp(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]) -> Fxp:
+ 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 Fxp(fxp_bin, like=self.fxp_dtype_template())
+ 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
- def _assert_valid_classical_val(self, val: Union[float, Fxp], debug_str: str = 'val'):
- fxp_val = val if isinstance(val, Fxp) else Fxp(val)
+ 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 QMontgomeryUInt(QDType):
r"""Montgomery form of an unsigned integer of a given width bitsize which wraps around upon
diff --git a/qualtran/_infra/data_types_test.py b/qualtran/_infra/data_types_test.py
index 577ab9a52..142675ad5 100644
--- a/qualtran/_infra/data_types_test.py
+++ b/qualtran/_infra/data_types_test.py
@@ -13,8 +13,9 @@
# limitations under the License.
import math
import random
-from typing import Any, Sequence, Union
+from typing import Any, Iterable, List, Sequence, Union
+import attrs
import galois
import numpy as np
import pytest
@@ -36,7 +37,7 @@
QMontgomeryUInt,
QUInt,
)
-from qualtran._infra.data_types import _QAnyInt
+from qualtran._infra.data_types import _Fxp, _QAnyInt
from qualtran.symbolics import ceil, is_symbolic, log2
@@ -71,7 +72,7 @@ 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="num_qubits must be > 1."):
+ with pytest.raises(ValueError, match="bitsize must be > 1."):
QIntOnesComp(1)
n = sympy.symbols('x')
qint_8 = QIntOnesComp(n)
@@ -98,7 +99,7 @@ def test_bounded_quint():
assert qint_3.bitsize == 2
assert qint_3.iteration_length == 3
- with pytest.raises(ValueError, match="BQUInt iteration length.*"):
+ with pytest.raises(ValueError, match="iteration length is too large.*"):
BQUInt(4, 76)
n = sympy.symbols('x')
l = sympy.symbols('l')
@@ -493,7 +494,7 @@ def test_qfxp_from_fixed_width_int():
def test_qfxp_to_and_from_bits_using_fxp():
# QFxp: Negative numbers are stored as twos complement
- qfxp_4_3 = QFxp(4, 3, True)
+ 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]
@@ -526,11 +527,11 @@ def test_qfxp_to_and_from_bits_using_fxp():
_ = 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._get_classical_domain_fxp():
- assert qfxp._from_bits_to_fxp(qfxp._fxp_to_bits(x)) == x
+ 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(QFxp(7, 3, True)._fxp_to_bits(-4.375)) == [1] + [0, 1, 1] + [1, 0, 1]
- assert list(QFxp(7, 3, True)._fxp_to_bits(+4.625)) == [0] + [1, 0, 0] + [1, 0, 1]
+ 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]
def test_iter_bits():
@@ -558,11 +559,11 @@ def test_iter_bits_twos():
def test_fixed_point(val, width, signed):
if (val < 0) and not signed:
with pytest.raises(ValueError):
- _ = QFxp(width + int(signed), width, signed=signed)._fxp_to_bits(
+ _ = _Fxp(width + int(signed), width, signed=signed)._fxp_to_bits(
val, require_exact=False, complement=False
)
else:
- bits = QFxp(width + int(signed), width, signed=signed)._fxp_to_bits(
+ bits = _Fxp(width + int(signed), width, signed=signed)._fxp_to_bits(
val, require_exact=False, complement=False
)
if signed:
@@ -593,3 +594,64 @@ def test_qgf_with_default_poly_is_compatible():
qgf_two = QGF(2, 4, irreducible_poly=qgf_one.gf_type.irreducible_poly)
assert qgf_one == qgf_two
+
+
+@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/bloqs/basic_gates/z_basis_test.py b/qualtran/bloqs/basic_gates/z_basis_test.py
index 614246241..35de62edc 100644
--- a/qualtran/bloqs/basic_gates/z_basis_test.py
+++ b/qualtran/bloqs/basic_gates/z_basis_test.py
@@ -122,7 +122,7 @@ def test_zero_effect_manual():
with pytest.raises(AssertionError):
bloq.call_classically(q=1)
- with pytest.raises(ValueError, match=r'Bad QBit\(\) value \[0\, 0\, 0\]'):
+ with pytest.raises(ValueError, match=r'Bad bit value: \[0\, 0\, 0\]'):
bloq.call_classically(q=[0, 0, 0]) # type: ignore[arg-type]
diff --git a/qualtran/dtype/__init__.py b/qualtran/dtype/__init__.py
index cf68b81ab..45395c41b 100644
--- a/qualtran/dtype/__init__.py
+++ b/qualtran/dtype/__init__.py
@@ -11,24 +11,30 @@
# 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.
+# isort:skip_file
"""Data type objects for your quantum programs."""
from qualtran._infra.data_types import (
- BQUInt,
- CBit,
+ QCDType,
CDType,
+ QDType,
QAny,
QBit,
- QCDType,
- QDType,
- QFxp,
- QGF,
- QGFPoly,
+ CBit,
QInt,
+ CInt,
QIntOnesComp,
- QMontgomeryUInt,
+ CIntOnesComp,
QUInt,
+ CUInt,
+ BQUInt,
+ BCUInt,
+ QFxp,
+ CFxp,
+ QMontgomeryUInt,
+ QGF,
+ QGFPoly,
)
__all__ = [
@@ -39,10 +45,15 @@
'QBit',
'CBit',
'QInt',
+ 'CInt',
'QIntOnesComp',
+ 'CIntOnesComp',
'QUInt',
+ 'CUInt',
'BQUInt',
+ 'BCUInt',
'QFxp',
+ 'CFxp',
'QMontgomeryUInt',
'QGF',
'QGFPoly',
diff --git a/qualtran/simulation/classical_sim.py b/qualtran/simulation/classical_sim.py
index 6f8cc20c6..66cd5de2f 100644
--- a/qualtran/simulation/classical_sim.py
+++ b/qualtran/simulation/classical_sim.py
@@ -56,9 +56,9 @@
def _numpy_dtype_from_qlt_dtype(dtype: 'QCDType') -> Type:
# TODO: Move to a method on QCDType. https://github.com/quantumlib/Qualtran/issues/1437.
- from qualtran._infra.data_types import CBit, QBit, QInt, QUInt
+ from qualtran._infra.data_types import CBit, QAny, QBit, QInt, QUInt
- if isinstance(dtype, QUInt):
+ if isinstance(dtype, (QUInt, QAny)):
if dtype.bitsize <= 8:
return np.uint8
elif dtype.bitsize <= 16:
diff --git a/qualtran/simulation/classical_sim_test.py b/qualtran/simulation/classical_sim_test.py
index 751f1985e..689ab99e9 100644
--- a/qualtran/simulation/classical_sim_test.py
+++ b/qualtran/simulation/classical_sim_test.py
@@ -78,7 +78,7 @@ def test_dtype_validation():
# bad integer
vals2 = {**vals, 'one_bit_int': 2}
- with pytest.raises(ValueError, match=r'Bad QBit().*one_bit_int'):
+ with pytest.raises(ValueError, match=r'Bad bit value.*one_bit_int'):
sim._update_assign_from_vals(regs, binst, vals2) # type: ignore[arg-type]
# int is a numpy int