Skip to content
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

Merged
merged 16 commits into from
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 97 additions & 27 deletions qualtran/_infra/data_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)`.
Copy link
Collaborator

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'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the class docstring



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.
Expand Down Expand Up @@ -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:
Copy link
Collaborator

Choose a reason for hiding this comment

The 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?

"""Prepare an empty `fxpmath.Fxp` value container for experimental fixed point support.

This constructs a `Fxp` object with no value. To assign the returned object a fixed point value, 
use ... [idk, exaplain how to use it].

Fxp support is experimental and doesn't hook into the classical simulator protocol etc etc

This corresponds to the Fxp constructor arguments:
 - op sizing, etc etc
"""

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd also make this a method instead of a property

Copy link
Contributor Author

@anurudhp anurudhp Jul 29, 2024

Choose a reason for hiding this comment

The 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: some_fxp_value.like(QFxp(...).fxp_dtype_template)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the docstring to explain how to use this function

Copy link
Collaborator

Choose a reason for hiding this comment

The 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(
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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}"
)
Expand Down
21 changes: 19 additions & 2 deletions qualtran/_infra/data_types_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,13 @@ def test_qfxp():
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'
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'
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)
Expand Down Expand Up @@ -365,6 +365,23 @@ def test_qfxp_to_and_from_bits():
)


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 = QFxp(4, 3, True)
Expand Down
2 changes: 1 addition & 1 deletion qualtran/bloqs/mean_estimation/arctan_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def test_arctan(selection_bitsize, target_bitsize):
y = -2 * np.arctan(x) / np.pi
bits = QFxp(target_bitsize + 1, target_bitsize, True).to_bits(
QFxp(target_bitsize + 1, target_bitsize, True).to_fixed_width_int(
y, require_exact=False
y, require_exact=False, complement=False
)
)
sign, y_bin = bits[0], bits[1:]
Expand Down
6 changes: 3 additions & 3 deletions qualtran/bloqs/rotations/phase_gradient.py
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ def phase_dtype(self) -> QFxp:

@cached_property
def gamma_fxp(self) -> Fxp:
return Fxp(abs(self.gamma), dtype=self.gamma_dtype.fxp_dtype_template.dtype)
return Fxp(abs(self.gamma), dtype=self.gamma_dtype.fxp_dtype_template().dtype)

@cached_method
def scaled_val(self, x: int) -> int:
Expand All @@ -461,15 +461,15 @@ def scaled_val(self, x: int) -> int:
# However, `x` should be interpreted as per the fixed point specification given in self.x_dtype.
# If `self.x_dtype` uses `n_frac` bits to represent the fractional part, `x` should be divided by
# 2**n_frac (in other words, right shifted by n_frac)
x_fxp = Fxp(x / 2**self.x_dtype.num_frac, dtype=self.x_dtype.fxp_dtype_template.dtype)
x_fxp = Fxp(x / 2**self.x_dtype.num_frac, dtype=self.x_dtype.fxp_dtype_template().dtype)
# Similarly, `self.gamma` should be represented as a fixed point number using appropriate number
# of bits for integer and fractional part. This is done in self.gamma_fxp
# Compute the result = x_fxp * gamma_fxp
result = _mul_via_repeated_add(x_fxp, self.gamma_fxp, self.phase_dtype.bitsize)
# Since the phase gradient register is a fixed point register with `n_word=0`, we discard the integer
# part of `result`. This is okay because the adding `x.y` to the phase gradient register will impart
# a phase of `exp(i * 2 * np.pi * x.y)` which is same as `exp(i * 2 * np.pi * y)`
assert 0 <= result < 1 and result.dtype == self.phase_dtype.fxp_dtype_template.dtype
assert 0 <= result < 1 and result.dtype == self.phase_dtype.fxp_dtype_template().dtype
# Convert the `self.phase_bitsize`-bit fraction into back to an integer and return the result.
# Sign of `gamma` affects whether we add or subtract into the phase gradient register and thus
# can be ignored during the fixed point arithmetic analysis.
Expand Down
4 changes: 2 additions & 2 deletions qualtran/bloqs/rotations/quantum_variable_rotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ def find_optimal_phase_grad_size(gamma_fxp: Fxp, cost_dtype: QFxp, eps: float) -
from qualtran.bloqs.rotations.phase_gradient import _mul_via_repeated_add

cost_val = (2**cost_dtype.bitsize - 1) / (2**cost_dtype.num_frac)
cost_fxp = Fxp(cost_val, dtype=cost_dtype.fxp_dtype_template.dtype)
cost_fxp = Fxp(cost_val, dtype=cost_dtype.fxp_dtype_template().dtype)
expected_val = (gamma_fxp.get_val() * cost_val) % 1

def is_good_phase_grad_size(phase_bitsize: int):
Expand Down Expand Up @@ -461,7 +461,7 @@ def num_additions(self) -> int:

@cached_property
def gamma_fxp(self) -> Fxp:
return Fxp(abs(self.gamma), dtype=self.gamma_dtype.fxp_dtype_template.dtype)
return Fxp(abs(self.gamma), dtype=self.gamma_dtype.fxp_dtype_template().dtype)

@cached_property
def gamma_dtype(self) -> QFxp:
Expand Down
52 changes: 52 additions & 0 deletions qualtran/simulation/classical_sim.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,58 @@
"drawer = ClassicalSimGraphDrawer(cbloq, vals=dict(q0=1, q1=0))\n",
"drawer.get_svg()"
]
},
{
"cell_type": "markdown",
"id": "10",
"metadata": {},
"source": [
"## Quantum Data Types\n",
"\n",
"To convert back and forth between classical values and bitstrings, we use the `QDType.to_bits` and `QDType.from_bits` functions.\n",
"\n",
"\n",
"### QFxp classical values\n",
"\n",
"Currently, QFxp classical values are represented as fixed-width integers.\n",
"See the class docstring for QFxp for precise details.\n",
"To convert from true floating point values to this representation and vice-versa,\n",
"users can use `QFxp.to_fixed_width_int` and `QFxp.float_from_fixed_width_int` respectively."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "11",
"metadata": {},
"outputs": [],
"source": [
"from qualtran import QFxp\n",
"\n",
"@frozen\n",
"class DoubleFxp(Bloq):\n",
" \"\"\"Bloq with a QFxp of 4 bits (0 int, 4 frac).\n",
" \n",
" This bloq doubles the input value inplace, discarding any overflow\n",
" \"\"\"\n",
" num_frac: int = 4\n",
" \n",
" @property\n",
" def signature(self) -> Signature:\n",
" return Signature.build_from_dtypes(x=QFxp(self.num_frac, self.num_frac))\n",
" \n",
" def on_classical_vals(self, x) -> dict[str, 'ClassicalValT']:\n",
" \"\"\"Double the input value, discarding overflow\"\"\"\n",
" return {'x': (x * 2) % (2**self.num_frac)}\n",
"\n",
"\n",
"bloq_with_qfxp = DoubleFxp()\n",
"x_float = 0.25\n",
"x_as_int = QFxp(4, 4).to_fixed_width_int(x_float)\n",
"(x_out_as_int,) = bloq_with_qfxp.call_classically(x=x_as_int)\n",
"x_out_float = QFxp(4, 4).float_from_fixed_width_int(x_out_as_int)\n",
"assert x_out_float == 0.5"
]
}
],
"metadata": {
Expand Down
Loading