-
Notifications
You must be signed in to change notification settings - Fork 51
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
QFxp: use integer values for classical sim instead of fxpmath.Fxp
#1204
Changes from 6 commits
97d23ce
655359d
c0ded49
efafb00
7fe3852
b1c82a9
024b1d7
ae0f8a6
2243fb3
3943bf3
f95b6d9
1a16e41
6365904
92fe9d6
09e2bff
75d605e
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 |
---|---|---|
|
@@ -530,6 +530,23 @@ class QFxp(QDType): | |
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: | ||
anurudhp marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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, 2) 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`. | ||
|
||
|
||
Attributes: | ||
bitsize: The total number of qubits used to represent the integer and | ||
fractional part combined. | ||
|
@@ -563,56 +580,109 @@ def is_symbolic(self) -> bool: | |
|
||
@property | ||
def _int_qdtype(self) -> Union[QUInt, QInt]: | ||
"""The corresponding integer type used to represent raw values of this type. | ||
|
||
This raw integer value is used in the classical simulator to represent values | ||
of QFxp registers. | ||
"""The corresponding dtype for the raw integer representation. | ||
|
||
For example, QFxp(6, 2) 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 class docstring section on "Classical Simulation" for more details. | ||
""" | ||
return QInt(self.bitsize) if self.signed else QUInt(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 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 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 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 to_fixed_width_int(self, x: Union[float, Fxp], *, require_exact: bool = False) -> int: | ||
"""Returns the interpretation of the binary representation of `x` as an integer.""" | ||
bits = self._fxp_to_bits(x, require_exact=require_exact) | ||
def to_fixed_width_int( | ||
self, x: Union[float, 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. | ||
""" | ||
bits = self._fxp_to_bits(x, require_exact=require_exact, complement=complement) | ||
return self._int_qdtype.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)' | ||
else: | ||
return f'QFxp({self.bitsize}, {self.num_frac})' | ||
|
||
# Experimental `fxpmath.Fxp` support. | ||
# This support is currently 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). | ||
|
||
@property | ||
def fxp_dtype_template(self) -> Fxp: | ||
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. why are we using the word "template" here? because it doesn't actually have a value associated with 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'd also make this a method instead of a property 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 used the term from the fxpmath readme: https://github.com/francof2a/fxpmath#:~:text=It%20is%20a%20good%20idea%20create%20Fxp%20objects%20like%20template%3A This is used to type-cast Fxp values, instead of being an empty container, hence a property. Example usage: 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've updated the docstring to explain how to use this function 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. you're never going to want to plumb through any of the options? |
||
"""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. | ||
Usage: | ||
anurudhp marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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 | ||
anurudhp marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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. | ||
|
||
Notes: | ||
anurudhp marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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). | ||
""" | ||
if is_symbolic(self.bitsize) or is_symbolic(self.num_frac): | ||
raise ValueError( | ||
|
@@ -632,7 +702,7 @@ def fxp_dtype_template(self) -> Fxp: | |
|
||
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) | ||
yield 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 | ||
|
@@ -655,7 +725,7 @@ def _fxp_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_template).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 | ||
|
@@ -664,11 +734,11 @@ def _from_bits_to_fxp(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, like=self.fxp_dtype_template) | ||
return Fxp(fxp_bin, 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_template).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}" | ||
) | ||
|
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.
There's some docstrings and comments hiding around the code. Can you add one or two short paragraphs to the class docstring for QFxp to describe the relationship to the classical simulation protocol and how we'll use these "raw ints" in the classical simulation protocol.
Comments don't show up in the docs and may be missed, and docstrings on private methods may be missed. You can also add a line to the docstrings of the public methods that says something like 'see the class docstring for details on the classical simulation format'
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.
Updated the class docstring