From 97d23ce5a60331befca0c64cbde798c0d38293d9 Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Sat, 27 Jul 2024 18:22:31 -0700 Subject: [PATCH 01/13] reorganize `QFxp` code that uses `fxpmath.Fxp` --- qualtran/_infra/data_types.py | 147 +++++++++++------- qualtran/_infra/data_types_test.py | 74 +++++---- qualtran/bloqs/bookkeeping/cast_test.py | 4 +- .../trotter/grid_ham/inverse_sqrt_test.py | 9 +- .../complex_phase_oracle_test.py | 4 +- qualtran/bloqs/rotations/phase_gradient.py | 6 +- .../rotations/quantum_variable_rotation.py | 6 +- 7 files changed, 149 insertions(+), 101 deletions(-) diff --git a/qualtran/_infra/data_types.py b/qualtran/_infra/data_types.py index 4dff14add..c56924f0e 100644 --- a/qualtran/_infra/data_types.py +++ b/qualtran/_infra/data_types.py @@ -521,8 +521,9 @@ class QFxp(QDType): 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 require an additional bit to store the sign (0 for - +, 1 for -). In total there are `bitsize = (n_sign + num_int + num_frac)` bits used + 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 = (n_sign + 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. @@ -533,34 +534,110 @@ class QFxp(QDType): 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. If signed is true the - number of integer bits is reduced by 1. + 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.num_qubits) and self.num_qubits == 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 num_qubits(self): return self.bitsize @property def num_int(self) -> SymbolicInt: - return self.bitsize - self.num_frac - int(self.signed) + return self.bitsize - self.num_frac + + def is_symbolic(self) -> bool: + return is_symbolic(self.bitsize, self.num_frac) @property - def fxp_dtype_str(self) -> str: - return f'fxp-{"us"[self.signed]}{self.bitsize}/{self.num_frac}' + 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. + + 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`. + """ + return QInt(self.bitsize) if self.signed else QUInt(self.bitsize) + + def get_classical_domain(self) -> Iterable[int]: + yield from self._int_qdtype.get_classical_domain() + + def to_bits(self, x) -> List[int]: + return self._int_qdtype.to_bits(x) + + def from_bits(self, bits: Sequence[int]): + return self._int_qdtype.from_bits(bits) + + def assert_valid_classical_val(self, val: int, debug_str: str = 'val'): + 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. Requires `x` to be nonnegative.""" + if x < 0: + raise ValueError("x must be >= 0.") + return int(''.join(str(b) for b in self._fxp_to_bits(x, require_exact=require_exact)), 2) + + 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(self) -> Fxp: - return Fxp(None, dtype=self.fxp_dtype_str) + 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( + f"Cannot construct Fxp template for symbolic bitsizes: {self.bitsize=}, {self.num_frac=}" + ) - def is_symbolic(self) -> bool: - return is_symbolic(self.bitsize, self.num_frac) + 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 to_bits( + 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 _fxp_to_bits( self, x: Union[float, Fxp], require_exact: bool = True, complement: bool = True ) -> List[int]: """Yields individual bits corresponding to binary representation of `x`. @@ -581,62 +658,24 @@ 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 - def from_bits(self, bits: Sequence[int]) -> Fxp: + 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, dtype=self.fxp_dtype_str) - - 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 - 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.""" - if x < 0: - raise ValueError("x must be >= 0.") - return int(''.join(str(b) for b in self.to_bits(x, require_exact=False)), 2) - - 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 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.") - - 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) + 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).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 - - def __str__(self): - if self.signed: - return f'QFxp({self.bitsize}, {self.num_frac}, True)' - else: - return f'QFxp({self.bitsize}, {self.num_frac})' - @attrs.frozen class QMontgomeryUInt(QDType): diff --git a/qualtran/_infra/data_types_test.py b/qualtran/_infra/data_types_test.py index d12fbeb3d..6088c59a4 100644 --- a/qualtran/_infra/data_types_test.py +++ b/qualtran/_infra/data_types_test.py @@ -97,12 +97,14 @@ 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_str == '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 == 0 - assert qfp_16.fxp_dtype_str == 'fxp-s16/15' + assert qfp_16.num_int == 1 + assert qfp_16.fxp_dtype_template.dtype == 'fxp-s16/15' + with pytest.raises(ValueError, match="num_qubits must be > 1."): QFxp(1, 1, signed=True) QFxp(1, 1, signed=False) @@ -117,7 +119,7 @@ def test_qfxp(): assert qfp.num_int == b - f qfp = QFxp(b, f, True) assert qfp.num_qubits == b - assert qfp.num_int == b - f - 1 + assert qfp.num_int == b - f assert is_symbolic(QFxp(*sympy.symbols('x y'))) @@ -353,42 +355,48 @@ def test_qintonescomp_to_and_from_bits(): def test_qfxp_to_and_from_bits(): # QFxp: Negative numbers are stored as ones complement qfxp_4_3 = QFxp(4, 3, True) - assert list(qfxp_4_3.to_bits(0.5)) == [0, 1, 0, 0] - assert qfxp_4_3.from_bits(qfxp_4_3.to_bits(0.5)).get_val() == 0.5 - assert list(qfxp_4_3.to_bits(-0.5)) == [1, 1, 0, 0] - assert qfxp_4_3.from_bits(qfxp_4_3.to_bits(-0.5)).get_val() == -0.5 - assert list(qfxp_4_3.to_bits(0.625)) == [0, 1, 0, 1] - assert qfxp_4_3.from_bits(qfxp_4_3.to_bits(+0.625)).get_val() == +0.625 - assert qfxp_4_3.from_bits(qfxp_4_3.to_bits(-0.625)).get_val() == -0.625 - assert list(qfxp_4_3.to_bits(-(1 - 0.625))) == [1, 1, 0, 1] - assert qfxp_4_3.from_bits(qfxp_4_3.to_bits(0.375)).get_val() == 0.375 - assert qfxp_4_3.from_bits(qfxp_4_3.to_bits(-0.375)).get_val() == -0.375 + assert list(qfxp_4_3._fxp_to_bits(0.5)) == [0, 1, 0, 0] + assert qfxp_4_3._from_bits_to_fxp(qfxp_4_3._fxp_to_bits(0.5)).get_val() == 0.5 + assert list(qfxp_4_3._fxp_to_bits(-0.5)) == [1, 1, 0, 0] + assert qfxp_4_3._from_bits_to_fxp(qfxp_4_3._fxp_to_bits(-0.5)).get_val() == -0.5 + assert list(qfxp_4_3._fxp_to_bits(0.625)) == [0, 1, 0, 1] + assert qfxp_4_3._from_bits_to_fxp(qfxp_4_3._fxp_to_bits(+0.625)).get_val() == +0.625 + assert qfxp_4_3._from_bits_to_fxp(qfxp_4_3._fxp_to_bits(-0.625)).get_val() == -0.625 + assert list(qfxp_4_3._fxp_to_bits(-(1 - 0.625))) == [1, 1, 0, 1] + assert qfxp_4_3._from_bits_to_fxp(qfxp_4_3._fxp_to_bits(0.375)).get_val() == 0.375 + assert qfxp_4_3._from_bits_to_fxp(qfxp_4_3._fxp_to_bits(-0.375)).get_val() == -0.375 with pytest.raises(ValueError): - _ = qfxp_4_3.to_bits(0.1) - assert list(qfxp_4_3.to_bits(0.7, require_exact=False)) == [0, 1, 0, 1] - assert list(qfxp_4_3.to_bits(0.7, require_exact=False, complement=False)) == [0, 1, 0, 1] - assert list(qfxp_4_3.to_bits(-0.7, require_exact=False)) == [1, 0, 1, 1] - assert list(qfxp_4_3.to_bits(-0.7, require_exact=False, complement=False)) == [1, 1, 0, 1] + _ = qfxp_4_3._fxp_to_bits(0.1) + assert list(qfxp_4_3._fxp_to_bits(0.7, require_exact=False)) == [0, 1, 0, 1] + assert list(qfxp_4_3._fxp_to_bits(0.7, require_exact=False, complement=False)) == [0, 1, 0, 1] + assert list(qfxp_4_3._fxp_to_bits(-0.7, require_exact=False)) == [1, 0, 1, 1] + assert list(qfxp_4_3._fxp_to_bits(-0.7, require_exact=False, complement=False)) == [1, 1, 0, 1] with pytest.raises(ValueError): - _ = qfxp_4_3.to_bits(1.5) + _ = qfxp_4_3._fxp_to_bits(1.5) - assert qfxp_4_3.from_bits(qfxp_4_3.to_bits(1 / 2 + 1 / 4 + 1 / 8)) == 1 / 2 + 1 / 4 + 1 / 8 - assert qfxp_4_3.from_bits(qfxp_4_3.to_bits(-1 / 2 - 1 / 4 - 1 / 8)) == -1 / 2 - 1 / 4 - 1 / 8 + assert ( + qfxp_4_3._from_bits_to_fxp(qfxp_4_3._fxp_to_bits(1 / 2 + 1 / 4 + 1 / 8)) + == 1 / 2 + 1 / 4 + 1 / 8 + ) + assert ( + qfxp_4_3._from_bits_to_fxp(qfxp_4_3._fxp_to_bits(-1 / 2 - 1 / 4 - 1 / 8)) + == -1 / 2 - 1 / 4 - 1 / 8 + ) with pytest.raises(ValueError): - _ = qfxp_4_3.to_bits(1 / 2 + 1 / 4 + 1 / 8 + 1 / 16) + _ = 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(): - assert qfxp.from_bits(qfxp.to_bits(x)) == x + for x in qfxp._get_classical_domain_fxp(): + assert qfxp._from_bits_to_fxp(qfxp._fxp_to_bits(x)) == x - assert list(QFxp(7, 3, True).to_bits(-4.375)) == [1] + [0, 1, 1] + [1, 0, 1] - assert list(QFxp(7, 3, True).to_bits(+4.625)) == [0] + [1, 0, 0] + [1, 0, 1] + 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_to_and_from_bits_array_consistent(QFxp(4, 3, False), [1 / 2, 1 / 4, 3 / 8]) - assert_to_and_from_bits_array_consistent( - QFxp(4, 3, True), [1 / 2, -1 / 2, 1 / 4, -1 / 4, -3 / 8, 3 / 8] - ) + # assert_to_and_from_bits_array_consistent(QFxp(4, 3, False), [1 / 2, 1 / 4, 3 / 8]) + # assert_to_and_from_bits_array_consistent( + # QFxp(4, 3, True), [1 / 2, -1 / 2, 1 / 4, -1 / 4, -3 / 8, 3 / 8] + # ) def test_iter_bits(): @@ -416,11 +424,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).to_bits( + _ = QFxp(width + int(signed), width, signed=signed)._fxp_to_bits( val, require_exact=False, complement=False ) else: - bits = QFxp(width + int(signed), width, signed=signed).to_bits( + bits = QFxp(width + int(signed), width, signed=signed)._fxp_to_bits( val, require_exact=False, complement=False ) if signed: diff --git a/qualtran/bloqs/bookkeeping/cast_test.py b/qualtran/bloqs/bookkeeping/cast_test.py index 7390258e9..bc299672d 100644 --- a/qualtran/bloqs/bookkeeping/cast_test.py +++ b/qualtran/bloqs/bookkeeping/cast_test.py @@ -40,7 +40,9 @@ def test_cast_classical_sim(): assert b == 9 c = Cast(QFxp(8, 8), QUInt(8)) - assert c.call_classically(reg=1.2) == (1,) # type: ignore + val = 1.2 + val_as_int = QFxp(8, 8).to_fixed_width_int(val) + assert c.call_classically(reg=val_as_int) == (val_as_int,) # type: ignore def test_cast_unsiged_signed(): diff --git a/qualtran/bloqs/chemistry/trotter/grid_ham/inverse_sqrt_test.py b/qualtran/bloqs/chemistry/trotter/grid_ham/inverse_sqrt_test.py index 2991fd061..129610948 100644 --- a/qualtran/bloqs/chemistry/trotter/grid_ham/inverse_sqrt_test.py +++ b/qualtran/bloqs/chemistry/trotter/grid_ham/inverse_sqrt_test.py @@ -116,8 +116,7 @@ def test_multiply_float_int(): float_width = 24 int_width = 8 val = np.random.random() - fp_bits = QFxp(float_width, float_width).to_bits(val, require_exact=False) - fp_int = int(''.join(str(b) for b in fp_bits), 2) + fp_int = QFxp(float_width, float_width).to_fixed_width_int(val) int_val = np.random.randint(0, 2**int_width - 1) result = multiply_fixed_point_float_by_int(fp_int, int_val, float_width, int_width) assert abs(result / 2**float_width - int_val * val) <= int_width * 2 ** ( @@ -129,9 +128,7 @@ def test_multiply_floats(): float_width = 24 a = np.random.random() b = np.random.random() - bits = QFxp(float_width, float_width).to_bits(a, require_exact=False) - fp_a = int(''.join(str(b) for b in bits), 2) - bits = QFxp(float_width, float_width).to_bits(b, require_exact=False) - fp_b = int(''.join(str(b) for b in bits), 2) + fp_a = QFxp(float_width, float_width).to_fixed_width_int(a) + fp_b = QFxp(float_width, float_width).to_fixed_width_int(b) result = multiply_fixed_point_floats(fp_a, fp_b, float_width) assert abs(result / 2**float_width - a * b) <= (float_width + 1) / 2**float_width diff --git a/qualtran/bloqs/mean_estimation/complex_phase_oracle_test.py b/qualtran/bloqs/mean_estimation/complex_phase_oracle_test.py index a8d26f510..6055b5cd1 100644 --- a/qualtran/bloqs/mean_estimation/complex_phase_oracle_test.py +++ b/qualtran/bloqs/mean_estimation/complex_phase_oracle_test.py @@ -70,7 +70,9 @@ def test_phase_oracle(bitsize: int, arctan_bitsize: int): for x in range(2**bitsize): output_val = -2 * np.arctan(x, dtype=np.double) / np.pi output_bits = QFxp(arctan_bitsize, arctan_bitsize).to_bits( - np.abs(output_val), require_exact=False + QFxp(arctan_bitsize, arctan_bitsize).to_fixed_width_int( + np.abs(output_val), require_exact=False + ) ) approx_val = np.sign(output_val) * math.fsum( [b * (1 / 2 ** (1 + i)) for i, b in enumerate(output_bits)] diff --git a/qualtran/bloqs/rotations/phase_gradient.py b/qualtran/bloqs/rotations/phase_gradient.py index 1ddc8725c..e230fe08e 100644 --- a/qualtran/bloqs/rotations/phase_gradient.py +++ b/qualtran/bloqs/rotations/phase_gradient.py @@ -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_str) + return Fxp(abs(self.gamma), dtype=self.gamma_dtype.fxp_dtype_template.dtype) @cached_method def scaled_val(self, x: int) -> int: @@ -461,7 +461,7 @@ 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_str) + 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 @@ -469,7 +469,7 @@ def scaled_val(self, x: int) -> int: # 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_str + 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. diff --git a/qualtran/bloqs/rotations/quantum_variable_rotation.py b/qualtran/bloqs/rotations/quantum_variable_rotation.py index 1559b7e94..96ab168d7 100644 --- a/qualtran/bloqs/rotations/quantum_variable_rotation.py +++ b/qualtran/bloqs/rotations/quantum_variable_rotation.py @@ -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_str) + 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): @@ -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_str) + return Fxp(abs(self.gamma), dtype=self.gamma_dtype.fxp_dtype_template.dtype) @cached_property def gamma_dtype(self) -> QFxp: @@ -472,7 +472,7 @@ def gamma_dtype(self) -> QFxp: # The reference assumes that cost register always stores a fraction between [0, 1). We # do not have this assumption and therefore, we also need to add self.cost_dtype.num_int # to the gamma bitsize. - n_int = smax(0, bit_length(sympy.Abs(self.gamma))) + n_int = smax(0, bit_length(sabs(self.gamma))) n_frac = self.cost_dtype.num_int + self.b_phase return QFxp(bitsize=n_int + n_frac, num_frac=n_frac, signed=False) From 655359d1dc7193a8697a269d03b49252663053fd Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Sat, 27 Jul 2024 19:13:18 -0700 Subject: [PATCH 02/13] correctly convert float/Fxp to raw int --- qualtran/_infra/data_types.py | 7 +++---- qualtran/bloqs/mean_estimation/arctan_test.py | 4 +++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/qualtran/_infra/data_types.py b/qualtran/_infra/data_types.py index c56924f0e..84ded68e3 100644 --- a/qualtran/_infra/data_types.py +++ b/qualtran/_infra/data_types.py @@ -587,10 +587,9 @@ def assert_valid_classical_val(self, val: int, debug_str: str = 'val'): 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. Requires `x` to be nonnegative.""" - if x < 0: - raise ValueError("x must be >= 0.") - return int(''.join(str(b) for b in self._fxp_to_bits(x, require_exact=require_exact)), 2) + """Returns the interpretation of the binary representation of `x` as an integer.""" + bits = self._fxp_to_bits(x, require_exact, require_exact) + return self._int_qdtype.from_bits(bits) def __str__(self): if self.signed: diff --git a/qualtran/bloqs/mean_estimation/arctan_test.py b/qualtran/bloqs/mean_estimation/arctan_test.py index d8ae49f80..7f5102932 100644 --- a/qualtran/bloqs/mean_estimation/arctan_test.py +++ b/qualtran/bloqs/mean_estimation/arctan_test.py @@ -30,7 +30,9 @@ def test_arctan(selection_bitsize, target_bitsize): inp = f'0b_{x:0{selection_bitsize}b}_0_{0:0{target_bitsize}b}' y = -2 * np.arctan(x) / np.pi bits = QFxp(target_bitsize + 1, target_bitsize, True).to_bits( - y, require_exact=False, complement=False + QFxp(target_bitsize + 1, target_bitsize, True).to_fixed_width_int( + y, require_exact=False + ) ) sign, y_bin = bits[0], bits[1:] y_bin_str = ''.join(str(b) for b in y_bin) From c0ded4920a0108f784d0836e26a012d73abeed58 Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Sat, 27 Jul 2024 19:15:05 -0700 Subject: [PATCH 03/13] lint --- qualtran/_infra/data_types.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/qualtran/_infra/data_types.py b/qualtran/_infra/data_types.py index 84ded68e3..2714cdf3c 100644 --- a/qualtran/_infra/data_types.py +++ b/qualtran/_infra/data_types.py @@ -597,13 +597,11 @@ def __str__(self): 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). - """ + # 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: From efafb0052410f4d30f1c9877b24e9e956e2ee587 Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Sat, 27 Jul 2024 19:40:21 -0700 Subject: [PATCH 04/13] add back test --- qualtran/_infra/data_types_test.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/qualtran/_infra/data_types_test.py b/qualtran/_infra/data_types_test.py index 6088c59a4..d65606c8f 100644 --- a/qualtran/_infra/data_types_test.py +++ b/qualtran/_infra/data_types_test.py @@ -353,7 +353,20 @@ def test_qintonescomp_to_and_from_bits(): def test_qfxp_to_and_from_bits(): - # QFxp: Negative numbers are stored as ones complement + assert_to_and_from_bits_array_consistent( + QFxp(4, 3, False), [QFxp(4, 3, False).to_fixed_width_int(x) for x in [1 / 2, 1 / 4, 3 / 8]] + ) + assert_to_and_from_bits_array_consistent( + QFxp(4, 3, True), + [ + QFxp(4, 3, True).to_fixed_width_int(x) + for x in [1 / 2, -1 / 2, 1 / 4, -1 / 4, -3 / 8, 3 / 8] + ], + ) + + +def test_qfxp_to_and_from_bits_using_fxp(): + # QFxp: Negative numbers are stored as twos complement qfxp_4_3 = QFxp(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 @@ -393,11 +406,6 @@ def test_qfxp_to_and_from_bits(): 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_to_and_from_bits_array_consistent(QFxp(4, 3, False), [1 / 2, 1 / 4, 3 / 8]) - # assert_to_and_from_bits_array_consistent( - # QFxp(4, 3, True), [1 / 2, -1 / 2, 1 / 4, -1 / 4, -3 / 8, 3 / 8] - # ) - def test_iter_bits(): assert QUInt(2).to_bits(0) == [0, 0] From 7fe385242530acb367d9d10c4b38e11a98839dfa Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Mon, 29 Jul 2024 10:13:45 -0700 Subject: [PATCH 05/13] fix docstring --- qualtran/_infra/data_types.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qualtran/_infra/data_types.py b/qualtran/_infra/data_types.py index 2714cdf3c..8826f1d9c 100644 --- a/qualtran/_infra/data_types.py +++ b/qualtran/_infra/data_types.py @@ -523,12 +523,12 @@ class QFxp(QDType): 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 = (n_sign + 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. + 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 - n_sign)`. + signed, with num_int determined as `(bitsize - num_frac)`. Attributes: bitsize: The total number of qubits used to represent the integer and From 024b1d7cd41b029015344578c39a9129504cb028 Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Mon, 29 Jul 2024 13:41:12 -0700 Subject: [PATCH 06/13] fix arg --- qualtran/_infra/data_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qualtran/_infra/data_types.py b/qualtran/_infra/data_types.py index 8826f1d9c..9b567db38 100644 --- a/qualtran/_infra/data_types.py +++ b/qualtran/_infra/data_types.py @@ -588,7 +588,7 @@ def assert_valid_classical_val(self, val: int, debug_str: str = 'val'): 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) + bits = self._fxp_to_bits(x, require_exact=require_exact) return self._int_qdtype.from_bits(bits) def __str__(self): From ae0f8a6e4c94892d1e475b03fb969c50c334d9ee Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Mon, 29 Jul 2024 13:54:12 -0700 Subject: [PATCH 07/13] update docstring and add test for to_fixed_width_int --- qualtran/_infra/data_types.py | 15 ++++++++++++++- qualtran/_infra/data_types_test.py | 6 ++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/qualtran/_infra/data_types.py b/qualtran/_infra/data_types.py index 9b567db38..eb02ba1e5 100644 --- a/qualtran/_infra/data_types.py +++ b/qualtran/_infra/data_types.py @@ -587,7 +587,20 @@ def assert_valid_classical_val(self, val: int, debug_str: str = 'val'): 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.""" + """Returns the interpretation of the binary representation of `x` as an integer. + + The returned value is an integer equal to `round(x * 2**self.num_frac)`. + That is, the input value `x` is converted to a fixed-point binary value + of `self.num_int` integral bits and `self.num_frac` fractional bits, + and then re-interpreted as an integer by dropping the decimal point. + + 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. + """ bits = self._fxp_to_bits(x, require_exact=require_exact) return self._int_qdtype.from_bits(bits) diff --git a/qualtran/_infra/data_types_test.py b/qualtran/_infra/data_types_test.py index d65606c8f..dbe89e35e 100644 --- a/qualtran/_infra/data_types_test.py +++ b/qualtran/_infra/data_types_test.py @@ -365,6 +365,12 @@ 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_to_and_from_bits_using_fxp(): # QFxp: Negative numbers are stored as twos complement qfxp_4_3 = QFxp(4, 3, True) From 2243fb37fdcf6256918d9cf1ed202ecde5795431 Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Mon, 29 Jul 2024 13:58:39 -0700 Subject: [PATCH 08/13] add `float_from_fixed_width_int` helper --- qualtran/_infra/data_types.py | 8 ++++++++ qualtran/_infra/data_types_test.py | 11 +++++++++++ 2 files changed, 19 insertions(+) diff --git a/qualtran/_infra/data_types.py b/qualtran/_infra/data_types.py index eb02ba1e5..5fd381a5f 100644 --- a/qualtran/_infra/data_types.py +++ b/qualtran/_infra/data_types.py @@ -604,6 +604,14 @@ def to_fixed_width_int(self, x: Union[float, Fxp], *, require_exact: bool = Fals bits = self._fxp_to_bits(x, require_exact=require_exact) 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. + """ + return x / 2**self.num_frac + def __str__(self): if self.signed: return f'QFxp({self.bitsize}, {self.num_frac}, True)' diff --git a/qualtran/_infra/data_types_test.py b/qualtran/_infra/data_types_test.py index dbe89e35e..977e49530 100644 --- a/qualtran/_infra/data_types_test.py +++ b/qualtran/_infra/data_types_test.py @@ -371,6 +371,17 @@ def test_qfxp_to_fixed_width_int(): 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) From 3943bf3576c339ff2c36c12f0ad8ff259fe58832 Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Mon, 29 Jul 2024 14:27:04 -0700 Subject: [PATCH 09/13] fix complement --- qualtran/_infra/data_types.py | 11 +++++++++-- qualtran/bloqs/mean_estimation/arctan_test.py | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/qualtran/_infra/data_types.py b/qualtran/_infra/data_types.py index 5fd381a5f..6843b7e61 100644 --- a/qualtran/_infra/data_types.py +++ b/qualtran/_infra/data_types.py @@ -586,7 +586,9 @@ def from_bits(self, bits: Sequence[int]): def assert_valid_classical_val(self, val: int, debug_str: str = 'val'): 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: + 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. The returned value is an integer equal to `round(x * 2**self.num_frac)`. @@ -600,8 +602,13 @@ def to_fixed_width_int(self, x: Union[float, Fxp], *, require_exact: bool = Fals 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) + 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: diff --git a/qualtran/bloqs/mean_estimation/arctan_test.py b/qualtran/bloqs/mean_estimation/arctan_test.py index 7f5102932..67ccbc99a 100644 --- a/qualtran/bloqs/mean_estimation/arctan_test.py +++ b/qualtran/bloqs/mean_estimation/arctan_test.py @@ -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:] From f95b6d91f041a3916abc46b4a343998e89202adc Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Mon, 29 Jul 2024 14:37:36 -0700 Subject: [PATCH 10/13] move fxp docstring, explain fxp template --- qualtran/_infra/data_types.py | 42 +++++++++++-------- qualtran/_infra/data_types_test.py | 4 +- qualtran/bloqs/rotations/phase_gradient.py | 6 +-- .../rotations/quantum_variable_rotation.py | 4 +- 4 files changed, 32 insertions(+), 24 deletions(-) diff --git a/qualtran/_infra/data_types.py b/qualtran/_infra/data_types.py index 6843b7e61..3e6341a30 100644 --- a/qualtran/_infra/data_types.py +++ b/qualtran/_infra/data_types.py @@ -625,22 +625,30 @@ def __str__(self): 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: """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: + + 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. + + Notes: + 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( @@ -660,7 +668,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 @@ -683,7 +691,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 @@ -692,11 +700,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}" ) diff --git a/qualtran/_infra/data_types_test.py b/qualtran/_infra/data_types_test.py index 977e49530..2ed327ac9 100644 --- a/qualtran/_infra/data_types_test.py +++ b/qualtran/_infra/data_types_test.py @@ -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) diff --git a/qualtran/bloqs/rotations/phase_gradient.py b/qualtran/bloqs/rotations/phase_gradient.py index e230fe08e..2546e9322 100644 --- a/qualtran/bloqs/rotations/phase_gradient.py +++ b/qualtran/bloqs/rotations/phase_gradient.py @@ -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: @@ -461,7 +461,7 @@ 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 @@ -469,7 +469,7 @@ def scaled_val(self, x: int) -> int: # 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. diff --git a/qualtran/bloqs/rotations/quantum_variable_rotation.py b/qualtran/bloqs/rotations/quantum_variable_rotation.py index 96ab168d7..a4f5795a6 100644 --- a/qualtran/bloqs/rotations/quantum_variable_rotation.py +++ b/qualtran/bloqs/rotations/quantum_variable_rotation.py @@ -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): @@ -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: From 1a16e413f71ad344b027c23f5a12d438c93678e5 Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Mon, 29 Jul 2024 14:51:39 -0700 Subject: [PATCH 11/13] explain usage in notebook --- qualtran/_infra/data_types.py | 6 +++ qualtran/simulation/classical_sim.ipynb | 52 +++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/qualtran/_infra/data_types.py b/qualtran/_infra/data_types.py index 3e6341a30..7efd7c7a8 100644 --- a/qualtran/_infra/data_types.py +++ b/qualtran/_infra/data_types.py @@ -530,6 +530,12 @@ 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: + 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`. + Attributes: bitsize: The total number of qubits used to represent the integer and fractional part combined. diff --git a/qualtran/simulation/classical_sim.ipynb b/qualtran/simulation/classical_sim.ipynb index ba6ca3c46..c3760b3b5 100644 --- a/qualtran/simulation/classical_sim.ipynb +++ b/qualtran/simulation/classical_sim.ipynb @@ -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": { From 6365904db78bc012393a7e09538eb5fe113c7dff Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Mon, 29 Jul 2024 15:04:42 -0700 Subject: [PATCH 12/13] more docstrings for the underlying int type --- qualtran/_infra/data_types.py | 42 +++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/qualtran/_infra/data_types.py b/qualtran/_infra/data_types.py index 7efd7c7a8..b8c7baadb 100644 --- a/qualtran/_infra/data_types.py +++ b/qualtran/_infra/data_types.py @@ -530,12 +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: 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. @@ -569,27 +580,38 @@ 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( @@ -597,6 +619,9 @@ def to_fixed_width_int( ) -> 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, @@ -622,6 +647,9 @@ def float_from_fixed_width_int(self, x: int) -> float: 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 From 09e2bff5990cad056c3810887bf2e4a87a961d57 Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Tue, 30 Jul 2024 10:05:16 -0700 Subject: [PATCH 13/13] docstring nits --- qualtran/_infra/data_types.py | 63 +++++++++++++++++------------------ 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/qualtran/_infra/data_types.py b/qualtran/_infra/data_types.py index b8c7baadb..1bbd7334d 100644 --- a/qualtran/_infra/data_types.py +++ b/qualtran/_infra/data_types.py @@ -525,26 +525,27 @@ class QFxp(QDType): 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. + $\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`. + ### Classical Simulation - 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. + 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`. - 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`. + 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: @@ -573,6 +574,7 @@ def num_qubits(self): @property def num_int(self) -> SymbolicInt: + """Number of bits for the integral part.""" return self.bitsize - self.num_frac def is_symbolic(self) -> bool: @@ -662,27 +664,24 @@ def __str__(self): def fxp_dtype_template(self) -> Fxp: """A template of the `Fxp` data type for classical values. - Usage: - - 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)`. + 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. - - Notes: - 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). + - 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). """ if is_symbolic(self.bitsize) or is_symbolic(self.num_frac): raise ValueError(