QuantumFloat#

class QuantumFloat(msize: int | Array, exponent: int | Array = 0, qs: Any = None, name: str | None = None, signed: bool = False)[source]#

This subclass of QuantumVariable represents signed or unsigned floats to arbitrary precision.

The technical details of the employed arithmetic can be found in this article.

To create a QuantumFloat we call the constructor:

>>> from qrisp import QuantumFloat
>>> a = QuantumFloat(3, -1, signed = False)

Here, the 3 indicates the number of mantissa qubits and the -1 indicates the exponent.

Note

msize and exponent also accept a jax.Array under Jasp tracing, but only in the sense of a 0-d, traced scalar (e.g. a value computed from a measurement inside @jaspify/make_jaspr). Never an actual multi-element array.

For unsigned QuantumFloats, the decoder function is given by

\[f_{k}(i) = i2^{k}\]

Where \(k\) is the exponent.

We can check which values can be represented:

>>> for i in range(2**a.size): print(a.decoder(i))
0.0
0.5
1.0
1.5
2.0
2.5
3.0
3.5

We see \(2^3 = 8\) values, because we have 3 mantissa qubits. The exponent is -1, implying the precision is \(0.5 = 2^{-1}\).

For signed QuantumFloats, the decoder function is

\[\begin{split}f_{k}^{n}(i) = \begin{cases} i2^{k} & \text{if } i < 2^n \\ (i - 2^{n+1})2^k & \text{else} \end{cases}\end{split}\]

Where \(k\) is again the exponent and \(n\) is the mantissa size.

Another example:

>>> b = QuantumFloat(2, -2, signed = True)
>>> for i in range(2**b.size): print(b.decoder(i))
0.0
0.25
0.5
0.75
-1.0
-0.75
-0.5
-0.25

Here, we have \(2^2 = 4\) values and their signed equivalents. Their precision is \(0.25 = 2^{-2}\).

Arithmetic

Many operations known from classical arithmetic work for QuantumFloats in infix notation.

Addition:

>>> a[:] = 1.5
>>> b[:] = 0.25
>>> c = a + b
>>> print(c)
{1.75: 1.0}

Subtraction:

>>> d = a - c
>>> print(d)
{-0.25: 1.0}

Multiplication:

>>> e = d * b
>>> print(e)
{-0.0625: 1.0}

And even division:

>>> a = QuantumFloat(3)
>>> b = QuantumFloat(3)
>>> a[:] = 7
>>> b[:] = 2
>>> c = a/b
>>> print(c)
{3.5: 1.0}

Floor division:

>>> d = a//b
>>> print(d)
{3: 1.0}

Inversion:

>>> a = QuantumFloat(3, -1)
>>> a[:] = 3.5
>>> b = a**-1
>>> print(b)
{0.25: 1.0}

Note that the latter is only an approximate result. This is because in many cases, the results of division cannot be stored in a finite number of qubits, forcing us to approximate. To get a better approximation we can use the q_div and qf_inversion functions and specify the precision:

>>> from qrisp import q_div, qf_inversion
>>> a = QuantumFloat(3)
>>> a[:] = 1
>>> b = QuantumFloat(3)
>>> b[:] = 7
>>> c = q_div(a, b, prec = 6)
>>> print(c)
{0.140625: 1.0}

Comparing with the classical result (0.1428571428):

>>> 1/7 - 0.140625
0.002232142857142849

We see that the result is inside the expected precision of \(2^{-6} = 0.015625\).

In-place Operations

Further supported operations are in-place addition, subtraction (with both classical and quantum values):

>>> a = QuantumFloat(4, signed = True)
>>> a[:] = 4
>>> b = QuantumFloat(4)
>>> b[:] = 3
>>> a += b
>>> print(a)
{7: 1.0}
>>> a -= 2
>>> print(a)
{5: 1.0}

Warning

Additions that would result in overflow, raise no errors. Instead, the additions are performed modular.

>>> c = QuantumFloat(3)
>>> c += 9
>>> print(c)
{1: 1.0}

For in-place multiplications, only classical values are allowed:

>>> a *= -3
>>> print(a)
{-15: 1.0}

Note

In-place multiplications can change the mantissa size to prevent overflow errors. If you want to prevent this behavior, look into inpl_mult.

>>> a.size
7

Bitshifts

Bitshifts can be executed for free (i.e. not requiring any quantum gates). We can either use the exp_shift method or use the infix operators. Note that the bitshifts work in-place.

>>> a.exp_shift(3)
>>> print(a)
{-120: 1.0}
>>> a >>= 5
>>> print(a)
{-3.75: 1.0}

Comparisons

QuantumFloats can be compared to Python floats using the established operators. The return values are QuantumBools:

>>> from qrisp import h
>>> a = QuantumFloat(4)
>>> _ = h(a[2])
>>> print(a)
{0: 0.5, 4: 0.5}
>>> comparison_qbl_0 = (a < 4 )
>>> print(comparison_qbl_0)
{False: 0.5, True: 0.5}

Comparison to other QuantumFloats also works:

>>> b = QuantumFloat(3)
>>> b[:] = 4
>>> comparison_qbl_1 = (a == b)
>>> comparison_qbl_1.qs.statevector()
sqrt(2)*(|0>*|True>*|4>*|False> + |4>*|False>*|4>*|True>)/2

The first tensor factor containing a boolean value corresponds to comparison_qbl_0 and the second one is comparison_qbl_1.

Methods#

As an inheritor, addtionally to the methods listed here, QuantumFloats support all methods of QuantumVariable.

QuantumFloat.exp_shift(shift)

Performs an internal bit shift.

QuantumFloat.quantum_bit_shift(shift_amount)

Performs a bit shift in the quantum device.

QuantumFloat.significant(k)

Returns the qubit with significance \(k\).

QuantumFloat.sign()

Returns the sign qubit.

QuantumFloat.add_sign()

Turns an unsigned QuantumFloat into its signed version.

QuantumFloat.truncate(value)

Receives a regular float and returns the float that is closest to the input but can still be encoded.

QuantumFloat.sb_poly([m])

Returns the semi-boolean polynomial of this QuantumFloat where m specifies the image extension parameter.

QuantumFloat.get_ev(**mes_kwargs)

Retrieves the expectation value of self.