-
Notifications
You must be signed in to change notification settings - Fork 50
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Vectorized variants of to_bits
and from_bits
#1142
Changes from all commits
6637af9
96cf992
d6d7cc3
685f4bb
aa32062
539c4b1
70ad494
35c81f1
1c9b370
8f6c7e9
063e49b
7b79cf1
73681bb
013ff0f
9f8dddc
e1fa90a
0a99fff
88e831a
86d7eee
3aa7e0a
15394e5
80069f8
39f391b
b86b529
74dd54c
613e153
a7d82fa
6ad1163
2c741b4
6e94ca8
01d3511
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -77,10 +77,30 @@ def get_classical_domain(self) -> Iterable[Any]: | |
def to_bits(self, x) -> List[int]: | ||
"""Yields individual bits corresponding to binary representation of x""" | ||
|
||
def to_bits_array(self, x_array: NDArray[Any]) -> 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]): | ||
"""Combine individual bits to form x""" | ||
|
||
def from_bits_array(self, bits_array: NDArray[np.uint8]): | ||
"""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_classical_val(self, val: Any, debug_str: str = 'val'): | ||
"""Raises an exception if `val` is not a valid classical value for this type. | ||
|
@@ -90,17 +110,6 @@ def assert_valid_classical_val(self, val: Any, debug_str: str = 'val'): | |
debug_str: Optional debugging information to use in exception messages. | ||
""" | ||
|
||
@abc.abstractmethod | ||
def is_symbolic(self) -> bool: | ||
"""Returns True if this qdtype is parameterized with symbolic objects.""" | ||
|
||
def iteration_length_or_zero(self) -> SymbolicInt: | ||
"""Safe version of iteration length. | ||
|
||
Returns the iteration_length if the type has it or else zero. | ||
""" | ||
return getattr(self, 'iteration_length', 0) | ||
|
||
def assert_valid_classical_val_array(self, val_array: NDArray[Any], debug_str: str = 'val'): | ||
"""Raises an exception if `val_array` is not a valid array of classical values | ||
for this type. | ||
|
@@ -116,6 +125,17 @@ def assert_valid_classical_val_array(self, val_array: NDArray[Any], debug_str: s | |
for val in val_array.reshape(-1): | ||
self.assert_valid_classical_val(val) | ||
|
||
@abc.abstractmethod | ||
def is_symbolic(self) -> bool: | ||
"""Returns True if this qdtype is parameterized with symbolic objects.""" | ||
|
||
def iteration_length_or_zero(self) -> SymbolicInt: | ||
"""Safe version of iteration length. | ||
|
||
Returns the iteration_length if the type has it or else zero. | ||
""" | ||
return getattr(self, 'iteration_length', 0) | ||
|
||
def __str__(self): | ||
return f'{self.__class__.__name__}({self.num_qubits})' | ||
|
||
|
@@ -324,10 +344,43 @@ def to_bits(self, x: int) -> List[int]: | |
self.assert_valid_classical_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]: | ||
"""Returns the big-endian bitstrings specified by the given integers. | ||
|
||
Args: | ||
x_array: An integer or array of unsigned integers. | ||
""" | ||
if is_symbolic(self.bitsize): | ||
raise ValueError(f"Cannot compute bits for symbolic {self.bitsize=}") | ||
|
||
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: | ||
"""Combine individual bits to form x""" | ||
return int("".join(str(x) for x in bits), 2) | ||
|
||
def from_bits_array(self, bits_array: NDArray[np.uint8]) -> NDArray[np.integer]: | ||
"""Returns the integer specified by the given big-endian bitstrings. | ||
|
||
Args: | ||
bits_array: A bitstring or array of bitstrings, each of which has the 1s bit (LSB) at the end. | ||
Returns: | ||
An array of integers; one for each bitstring. | ||
""" | ||
bitstrings = np.atleast_2d(bits_array) | ||
if bitstrings.shape[1] > 64: | ||
raise NotImplementedError() | ||
basis = 2 ** np.arange(bitstrings.shape[1] - 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'): | ||
if not isinstance(val, (int, np.integer)): | ||
raise ValueError(f"{debug_str} should be an integer, not {val!r}") | ||
|
@@ -486,12 +539,31 @@ def num_int(self) -> SymbolicInt: | |
return self.bitsize - self.num_frac - int(self.signed) | ||
|
||
@property | ||
def fxp_dtype_str(self) -> str: | ||
return f'fxp-{"us"[self.signed]}{self.bitsize}/{self.num_frac}' | ||
def fxp_dtype_template(self) -> Fxp: | ||
"""A template of the `Fxp` data type for classical values. | ||
|
||
- 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. | ||
""" | ||
if is_symbolic(self.bitsize) or is_symbolic(self.num_frac): | ||
raise ValueError( | ||
"Cannot construct Fxp template for symbolic bitsizes {self.bitsize=}, {self.num_frac=}" | ||
) | ||
|
||
@property | ||
def _fxp_dtype(self) -> Fxp: | ||
return Fxp(None, dtype=self.fxp_dtype_str) | ||
return 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 is_symbolic(self) -> bool: | ||
return is_symbolic(self.bitsize, self.num_frac) | ||
|
@@ -517,7 +589,7 @@ def to_bits( | |
sign = int(x < 0) | ||
x = abs(x) | ||
fxp = x if isinstance(x, Fxp) else Fxp(x) | ||
bits = [int(x) for x in fxp.like(self._fxp_dtype).bin()] | ||
bits = [int(x) for x in fxp.like(self.fxp_dtype_template).bin()] | ||
if self.signed and not complement: | ||
bits[0] = sign | ||
return bits | ||
|
@@ -526,7 +598,14 @@ def from_bits(self, bits: Sequence[int]) -> Fxp: | |
"""Combine individual bits to form x""" | ||
bits_bin = "".join(str(x) for x in bits[:]) | ||
fxp_bin = "0b" + bits_bin[: -self.num_frac] + "." + bits_bin[-self.num_frac :] | ||
return Fxp(fxp_bin, dtype=self.fxp_dtype_str) | ||
return Fxp(fxp_bin, like=self.fxp_dtype_template) | ||
|
||
def from_bits_array(self, bits_array: NDArray[np.uint8]): | ||
assert isinstance(self.bitsize, int), "cannot convert to bits for symbolic bitsize" | ||
# TODO figure out why `np.vectorize` is not working here | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. open an issue and link? Do you have any theories? as I understand it: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's something to do with how Fxp interacts with numpy. Fxp has some inbuilt support to operate over NDArrays, so perhaps mixing the order up causes issues. I didn't investigate more though. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An See https://github.com/francof2a/fxpmath?tab=readme-ov-file#arithmetic for more details |
||
return Fxp( | ||
[self.from_bits(bitstring) for bitstring in bits_array.reshape(-1, self.bitsize)] | ||
) | ||
|
||
def to_fixed_width_int(self, x: Union[float, Fxp]) -> int: | ||
"""Returns the interpretation of the binary representation of `x` as an integer. Requires `x` to be nonnegative.""" | ||
|
@@ -546,19 +625,37 @@ def __attrs_post_init__(self): | |
def get_classical_domain(self) -> Iterable[Fxp]: | ||
qint = QIntOnesComp(self.bitsize) if self.signed else QUInt(self.bitsize) | ||
for x in qint.get_classical_domain(): | ||
yield Fxp(x / 2**self.num_frac, dtype=self.fxp_dtype_str) | ||
yield Fxp(x / 2**self.num_frac).like(self.fxp_dtype_template) | ||
|
||
def _assert_valid_classical_val(self, val: Union[float, Fxp], debug_str: str = 'val'): | ||
fxp_val = val if isinstance(val, Fxp) else Fxp(val) | ||
if fxp_val.get_val() != fxp_val.like(self._fxp_dtype).get_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}" | ||
) | ||
|
||
def assert_valid_classical_val(self, val: Union[float, Fxp], debug_str: str = 'val'): | ||
# TODO: Asserting a valid value here opens a can of worms because classical data, except integers, | ||
# is currently not propagated correctly through Bloqs | ||
pass | ||
assert isinstance(val, Fxp) | ||
assert val.overflow == 'wrap' | ||
assert val.shifting == 'trunc' | ||
self._assert_valid_classical_val(val, debug_str) | ||
|
||
def float_to_fxp( | ||
self, val: Union[float, int], *, raw: bool = False, require_exact: bool = True | ||
) -> Fxp: | ||
"""Convert a floating point value to an Fxp constant of this dtype. | ||
|
||
If `raw` is True, then returns `val / 2**self.n_frac` instead. | ||
|
||
Args: | ||
val: Floating point value. | ||
raw: Convert from a raw integer value instead | ||
require_exact: If True, represent the input `val` exactly and raise | ||
a ValueError if it cannot be represented. | ||
""" | ||
if require_exact: | ||
self._assert_valid_classical_val(val if not raw else val / 2**self.num_frac) | ||
return Fxp(val, raw=raw, like=self.fxp_dtype_template) | ||
|
||
def __str__(self): | ||
if self.signed: | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm sure you know this, but as far as I understand it
np.vectorize
will use a python for-loop under-the-hood and you don't get any special performance improvements by using it. You get the correct api and broadcasting behavior, however.why is the signature argument needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Without the signature, it tries to pack each output as a single entry in the array, and fails when we return a vector that needs to be treated as an additional dimension