# ********************************************************************************
# * Copyright (c) 2026 the Qrisp authors
# *
# * This program and the accompanying materials are made available under the
# * terms of the Eclipse Public License 2.0 which is available at
# * http://www.eclipse.org/legal/epl-2.0.
# *
# * This Source Code may also be made available under the following Secondary
# * Licenses when the conditions for such availability set forth in the Eclipse
# * Public License, v. 2.0 are satisfied: GNU General Public License, version 2
# * with the GNU Classpath Exception which is
# * available at https://www.gnu.org/software/classpath/license.html.
# *
# * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
# ********************************************************************************
"""Defines the QuantumFloat type for arbitrary-precision signed/unsigned quantum floating-point numbers."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, TypeGuard
import jax.numpy as jnp
import numpy as np
import sympy as sp
from jax import Array, jit
from jax.core import Tracer
from qrisp.core import QuantumVariable, cx, x
from qrisp.environments import conjugate, invert
from qrisp.jasp import check_for_tracing_mode
from qrisp.misc import gate_wrap
if TYPE_CHECKING:
from collections.abc import Iterable
from qrisp.circuit.qubit import Qubit
from qrisp.qtypes.quantum_bool import QuantumBool
from qrisp.qtypes.quantum_modulus import QuantumModulus
def _signed_int_iso(value: int | Array, n: int) -> Array:
"""Compute the signed integer isomorphism for a given bit-width.
This function maps an integer ``value`` from the signed range
[-2^n, 2^n - 1] into the unsigned range [0, 2^(n+1) - 1].
This is equivalent to the mathematical operation: value % 2^(n+1).
Parameters
----------
value : int or jax.Array
The signed integer or array of integers to be transformed.
n : int
The bit-width for the signed integer representation.
Returns
-------
jax.Array
A jnp.int64 array where each element of ``value`` has been mapped to
the unsigned range [0, 2^(n+1) - 1].
"""
# 1. Modular wrap: Ensure value is within [0, 2**(n+1) - 1]
mask = (jnp.int64(1) << (n + 1)) - 1
return jnp.int64(value) & mask
@jit
def _signed_int_iso_inv(y: int | Array, n: int) -> Array:
"""Compute the inverse signed integer isomorphism for a given bit-width.
This function maps an integer `y` from the unsigned range [0, 2^(n+1) - 1]
back into the signed range [-2^n, 2^n - 1]. It performs a manual
sign-extension by treating the n-th bit of `y` as the sign bit.
Parameters
----------
y : int or jax.Array
The unsigned integer or array of integers to be transformed.
n : int
The bit-width for the signed integer representation.
Returns
-------
jax.Array
A jnp.int64 array where each element of `y` has been mapped to
the signed range [-2^n, 2^n - 1].
"""
# 1. Modular wrap: Ensure y is within [0, 2**(n+1) - 1]
mask = (jnp.int64(1) << (n + 1)) - 1
y_wrapped = jnp.int64(y) & mask
# 2. Sign extension: If bit 'n' is set, the number is negative.
# In two's complement, we subtract 2**(n+1) from values >= 2**n.
sign_bit = jnp.int64(1) << n
return jnp.where(y_wrapped & sign_bit, y_wrapped - (jnp.int64(1) << (n + 1)), y_wrapped)
[docs]
class QuantumFloat(QuantumVariable):
r"""This subclass of :ref:`QuantumVariable` represents signed or unsigned floats to arbitrary precision.
The technical details of the employed arithmetic can be found in this
`article <https://ieeexplore.ieee.org/document/9815035>`_.
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
.. math::
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
.. math::
f_{k}^{n}(i) = \begin{cases} i2^{k} & \text{if } i < 2^n \\ (i - 2^{n+1})2^k &
\text{else} \end{cases}
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 :meth:`q_div <qrisp.q_div>` and
:meth:`qf_inversion <qrisp.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 <https://en.wikipedia.org/wiki/Modular_arithmetic>`_.
>>> 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
:meth:`inpl_mult <qrisp.inpl_mult>`.
>>> a.size
7
**Bitshifts**
Bitshifts can be executed for free (i.e. not requiring any quantum gates). We can
either use the :meth:`exp_shift <qrisp.QuantumFloat.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 :ref:`QuantumBools <QuantumBool>`:
>>> 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``.
"""
signed: bool
exponent: int | Array
traced_attributes: list[str]
static_attributes: list[str]
def __init__(
self,
msize: int | Array,
exponent: int | Array = 0,
qs: Any = None,
name: str | None = None,
signed: bool = False,
) -> None:
"""Construct a QuantumFloat with the given mantissa size, exponent, and sign.
.. note::
The ``jax.Array`` accepted by ``msize`` and ``exponent`` is a
0-d, traced scalar under Jasp tracing (e.g. a value computed from
a measurement inside ``@jaspify``/``make_jaspr``), not an actual
multi-element array. Outside of tracing, both are plain Python
integers.
Parameters
----------
msize : int or jax.Array
The number of mantissa qubits.
exponent : int or jax.Array, optional
The exponent, determining the precision. The default is 0.
qs : QuantumSession, optional
A QuantumSession object, where the QuantumFloat is supposed to be
registered. The default is None.
name : str, optional
A name which uniquely identifies the QuantumFloat. The default is None.
signed : bool, optional
If ``True``, an additional qubit is allocated to represent the
sign. The default is False.
"""
# Boolean to indicate if the float is signed
self.signed = signed
# Exponent
self.exponent = exponent
# Initialize QuantumVariable
if signed:
super().__init__(msize + 1, qs, name=name)
else:
super().__init__(msize, qs, name=name)
self.traced_attributes = ["exponent"]
self.static_attributes = ["signed"]
@property
def msize(self) -> int:
"""The number of mantissa qubits (excludes the sign qubit, if any).
Returns
-------
int
The mantissa size.
"""
return self.size - self.signed
@property
def mshape(self) -> tuple[int | Array, int | Array]:
"""Return the mantissa's significance bounds.
For a ``QuantumFloat`` with exponent ``k`` and mantissa size ``n``,
the result is ``(k, k + n)``. The lower bound is inclusive and the
upper bound is exclusive: mantissa qubit ``i`` has significance ``k + i``.
Returns
-------
tuple[int | jax.Array, int | jax.Array]
The inclusive lower and exclusive upper significance bounds.
"""
return (self.exponent, self.exponent + self.msize)
# Define outcome_labels
def decoder(self, i: int | Array) -> int | float | Array: # pyright: ignore[reportIncompatibleMethodOverride]
"""Convert a measurement outcome (integer) back to a human-readable value.
Parameters
----------
i : int or jax.Array
The integer outcome of a measurement of this QuantumFloat's qubits.
Returns
-------
int, float, or jax.Array
The decoded value: an ``int`` or ``float`` outside of tracing
mode (depending on whether the exponent is non-negative), or a
traced ``jax.Array`` while tracing.
"""
if self.signed:
res = _signed_int_iso_inv(i, self.msize) * jnp.float64(2) ** self.exponent
else:
res = i * jnp.float64(2) ** self.exponent
if check_for_tracing_mode():
return res
if self.exponent >= 0:
return int(res)
return float(res)
def jdecoder(self, i: int | Array) -> int | float | Array:
"""JAX-traceable version of :meth:`decoder`, used internally during tracing.
Parameters
----------
i : int or jax.Array
The integer outcome of a measurement of this QuantumFloat's qubits.
Returns
-------
int, float, or jax.Array
The decoded value, see :meth:`decoder`.
"""
return self.decoder(i)
def encoder(self, i: int | float | bool | np.integer | np.floating | Tracer) -> int | Array: # pyright: ignore[reportIncompatibleMethodOverride]
"""Convert a human-readable value to an integer that represents the measurement result.
Also validates that the input value can be represented within the bounds of the provided
QuantumFloat in static mode.
.. note::
Unlike the base :meth:`QuantumVariable.encoder
<qrisp.QuantumVariable.encoder>`, this parameter is named ``i``
(not ``value``) for historical reasons specific to QuantumFloat.
This does not accept ``complex``: the bounds/sign checks below
order ``i`` with ``<``/``>``, which complex values don't support.
Parameters
----------
i : int, float, bool, np.integer, np.floating, or jax.core.Tracer
A human-readable, real-valued number.
Returns
-------
int or jax.Array
The integer encoding the given value.
"""
# check if the encoding number is negative while the QuantumFloat is unsigned.
# We do this before converting to integer to prevent wrapping.
if not check_for_tracing_mode() and not self.signed and i < 0: # pyright: ignore[reportOperatorIssue]
raise ValueError("Tried to encode negative number in an unsigned QuantumFloat")
# the following check is based on the math for fixed point arithmetic which varies according to the
# size, exponent, and whether the QuantumFloat is signed or unsigned.
# calculate the integer bounds based on mantissa size (msize)
max_int = (1 << self.msize) - 1
if self.signed:
# Signed range: -2^msize to 2^msize - 1
min_int = -(1 << self.msize)
else:
# Unsigned range: 0 to 2^msize - 1
min_int = 0
# convert those integer bounds into actual Float values
# using the exponent.
scaling_factor = 2**self.exponent
max_float = max_int * scaling_factor
min_float = min_int * scaling_factor
# compare the input 'i' against the float limits.
# we do this before converting to integer to prevent wrapping.
if not check_for_tracing_mode():
is_out_of_bounds = (i > max_float) or (i < min_float)
# add a check that the provided value is safe to be encoded in the provided QuantumFloat
if is_out_of_bounds:
sign_description = "signed" if self.signed else "unsigned"
raise ValueError(
f"Not enough qubits to encode value {i} in {sign_description} QuantumFloat"
+ f" of {self.msize} qubits and exponent {self.exponent}."
)
if self.signed:
res = _signed_int_iso(i / jnp.float64(2**self.exponent), self.msize)
else:
res = i / jnp.float64(2) ** self.exponent
if isinstance(res, (int, float)):
return int(res)
return res.astype(int)
[docs]
def sb_poly(self, m: int = 0) -> sp.Expr:
"""Returns the semi-boolean polynomial of this `QuantumFloat` where `m` specifies the image extension parameter.
For the technical details we refer to:
https://ieeexplore.ieee.org/document/9815035
Parameters
----------
m : int, optional
Image extension parameter. The default is 0.
Returns
-------
Sympy expression
The semi-boolean polynomial of this QuantumFloat.
Examples
--------
The polynomial's symbols are named after this QuantumFloat's ``hash``
(to guarantee uniqueness across QuantumFloats), so we inspect its
coefficients rather than its literal string representation:
>>> from qrisp import QuantumFloat
>>> import sympy as sp
>>> x = QuantumFloat(3, -1, signed = True, name = "x")
>>> [float(c) for c in sp.Poly(x.sb_poly(5)).coeffs()]
[0.5, 1.0, 2.0, 28.0]
"""
if m == 0:
m = self.size
symbols = sp.symbols(f"{hash(self)}_0:{self.size}")
poly = sum(2.0**i * symbols[i] for i in range(self.size))
if self.signed:
poly += (2.0 ** (m + 1) - 2.0 ** (self.size)) * symbols[-1]
# sympy's Symbol arithmetic isn't precisely typed, and self.exponent
# can be a traced jax.Array -- both are real Expr-producing operations
# at runtime.
return 2**self.exponent * poly # pyright: ignore[reportReturnType]
def encode( # pyright: ignore[reportIncompatibleMethodOverride]
self,
encoding_number: int | float | bool | np.integer | np.floating | Tracer,
rounding: bool = False,
permit_dirtyness: bool = False,
) -> None:
"""Initialize a QuantumFloat to a specific value.
.. note::
Unlike the base :meth:`QuantumVariable.encode
<qrisp.QuantumVariable.encode>`, this method additionally accepts
``rounding`` (inserted before ``permit_dirtyness``, for
historical reasons specific to QuantumFloat).
This does not accept ``complex`` (see :meth:`encoder
<qrisp.QuantumFloat.encoder>`, which this delegates to).
Parameters
----------
encoding_number : int, float, bool, np.integer, np.floating, or jax.core.Tracer
The value to encode.
rounding : bool, optional
If ``True``, round ``encoding_number`` to the value this
QuantumFloat can represent that is closest to it, before
encoding. The default is False.
permit_dirtyness : bool, optional
Suppresses the error message when calling encode on dirty
qubits. The default is False.
Returns
-------
None
"""
value = encoding_number
if rounding:
# Round to the closest representable value the same way truncate()
# does: representable values form a uniform grid, so the nearest
# one is found directly by rounding and clipping, in O(1) --
# no need to enumerate all 2**size outcomes to search for it.
value = self.truncate(encoding_number)
super().encode(value, permit_dirtyness=permit_dirtyness)
@gate_wrap(permeability="args", is_qfree=True)
def __mul__(self, other: QuantumFloat | int | np.integer) -> QuantumFloat:
"""Multiply this QuantumFloat by another QuantumFloat or a classical int."""
if check_for_tracing_mode():
# NOTE: Local import to avoid a circular import (qrisp.alg_primitives.arithmetic imports from qrisp.qtypes).
from qrisp.alg_primitives.arithmetic import jasp_multiplyer, jasp_squaring
if isinstance(other, QuantumFloat):
if self is other:
return jasp_squaring(self)
return jasp_multiplyer(other, self)
raise TypeError(f"Tried to multiply class {type(other)} with QuantumFloat")
# NOTE: Local import to avoid a circular import (qrisp.alg_primitives.arithmetic imports from qrisp.qtypes).
from qrisp.alg_primitives.arithmetic import polynomial_encoder, q_mult
if isinstance(other, QuantumFloat):
return q_mult(self, other)
if isinstance(other, (int, np.integer)):
if other == 0:
# Multiplying by the classical scalar 0 always yields 0, regardless
# of self's state, so no entanglement with self is needed. Handled
# separately since the bit-shift/log2 logic below assumes other != 0.
# Attached to self.qs (like every other branch here) even though no
# gates act on it: gate_wrap only merges sessions its gates touch.
return QuantumFloat(1, self.exponent, self.qs, signed=self.signed)
bit_shift = 0
while not other % 2:
other = other >> 1
bit_shift += 1
output_qf = QuantumFloat(
self.msize + int(np.ceil(np.log2(abs(other)))),
self.exponent,
signed=bool(self.signed or other < 0),
)
# int.__mul__ doesn't know about Symbol's __rmul__, but this works fine at runtime.
polynomial_encoder([self], output_qf, other * sp.Symbol("x")) # pyright: ignore[reportOperatorIssue]
output_qf.exp_shift(bit_shift)
return output_qf
raise TypeError(
f"QuantumFloat multiplication for type {type(other)} not implemented (available are QuantumFloat and int)"
)
@gate_wrap(permeability="args", is_qfree=True)
def __add__(self, other: QuantumFloat | int | float | Tracer) -> QuantumFloat:
"""Add another QuantumFloat or a classical scalar to this QuantumFloat."""
# NOTE: Local import to avoid a circular import (qrisp.alg_primitives.arithmetic imports from qrisp.qtypes).
from qrisp.alg_primitives.arithmetic import sbp_add
if isinstance(other, QuantumFloat):
if check_for_tracing_mode():
# TODO: res only matches self's own shape, not the sum's, so
# any of other's bits outside that range (below self.exponent
# or above self's mantissa) are silently dropped whenever the
# operands' exponents differ. A correct fix needs a
# sign-extension-aware way to widen a QuantumFloat's value
# into a larger register under tracing: gidney_adder
# zero-pads a narrower operand to match a wider target, which
# corrupts a signed negative operand (its sign bit must be
# replicated into the new high bits, not zero-filled).
res = self.duplicate()
cx(self, res)
res += other
return res
return sbp_add(self, other)
if isinstance(other, (int, float, Tracer)):
res = self.duplicate()
cx(self, res)
res += other
return res
raise TypeError(f"Addition with type {type(other)} not implemented")
@gate_wrap(permeability="args", is_qfree=True)
def __sub__(self, other: QuantumFloat | int | float | Tracer) -> QuantumFloat:
"""Subtract another QuantumFloat or a classical scalar from this QuantumFloat."""
# NOTE: Local import to avoid a circular import (qrisp.alg_primitives.arithmetic imports from qrisp.qtypes).
from qrisp.alg_primitives.arithmetic import sbp_sub
if isinstance(other, QuantumFloat):
if check_for_tracing_mode():
# TODO: see the identical gap in __add__ above.
res = self.duplicate()
cx(self, res)
res -= other
return res
return sbp_sub(self, other)
if isinstance(other, (int, float, Tracer)):
res = self.duplicate()
cx(self, res)
res -= other
return res
raise TypeError(f"Subtraction with type {type(other)} not implemented")
__radd__ = __add__
__rmul__ = __mul__
@gate_wrap(permeability="args", is_qfree=True)
def __rsub__(self, other: QuantumFloat | int | float) -> QuantumFloat:
"""Subtract this QuantumFloat from a classical scalar or QuantumFloat."""
# NOTE: Local import to avoid a circular import (qrisp.alg_primitives.arithmetic imports from qrisp.qtypes).
from qrisp.alg_primitives.arithmetic import sbp_sub
if isinstance(other, QuantumFloat):
return sbp_sub(other, self)
if isinstance(other, (int, float)):
res = self.duplicate(init=True)
if not res.signed:
res.add_sign()
x(res)
res += other + 2**res.exponent
return res
raise TypeError(f"Subtraction with type {type(other)} not implemented")
@gate_wrap(permeability="args", is_qfree=True)
def __truediv__(self, other: QuantumFloat) -> QuantumFloat:
"""Divide this QuantumFloat by another QuantumFloat."""
# NOTE: Local import to avoid a circular import (qrisp.alg_primitives.arithmetic imports from qrisp.qtypes).
from qrisp.alg_primitives.arithmetic import q_div
return q_div(self, other)
@gate_wrap(permeability="args", is_qfree=True)
def __floordiv__(self, other: QuantumFloat) -> QuantumFloat:
"""Floor-divide this (unsigned, integer) QuantumFloat by another one."""
if self.signed or other.signed:
raise NotImplementedError("Floor division not implemented for signed QuantumFloats")
if self.exponent < 0 or other.exponent < 0:
raise ValueError("Tried to perform floor division on non-integer QuantumFloats")
# NOTE: Local import to avoid a circular import (qrisp.alg_primitives.arithmetic imports from qrisp.qtypes).
from qrisp.alg_primitives.arithmetic import q_div
return q_div(self, other, prec=0)
@gate_wrap(permeability="args", is_qfree=True)
def __pow__(self, power: int) -> QuantumFloat:
"""Raise this QuantumFloat to an integer power (-1 means inversion)."""
if not isinstance(power, (int, np.integer)):
raise TypeError(f"QuantumFloat exponentiation requires an integer power, got {type(power)}")
if power == -1:
# NOTE: Local import to avoid a circular import (qrisp.alg_primitives.arithmetic imports from qrisp.qtypes).
from qrisp.alg_primitives.arithmetic import qf_inversion
return qf_inversion(self)
if power < 0:
raise NotImplementedError(
f"QuantumFloat exponentiation only supports inversion (power=-1) for negative powers, got power={power}"
)
if power == 0:
res = self.duplicate()
res[:] = 1
return res
temp_results = [QuantumFloat((i + 1) * self.size) for i in range(power)]
res = QuantumFloat(self.size * power)
with conjugate(_power_conjugator)(self, power, temp_results):
cx(temp_results[-1], res)
for qv in temp_results:
qv.delete()
return res
@gate_wrap(permeability=[1], is_qfree=True)
def __iadd__(self, other: QuantumFloat | int | float | np.integer | np.floating | Array) -> QuantumFloat:
"""Add another QuantumFloat or a classical scalar to this QuantumFloat, in place."""
if check_for_tracing_mode():
# NOTE: Local import to avoid a circular import (qrisp.alg_primitives.arithmetic imports from qrisp.qtypes).
from qrisp.alg_primitives.arithmetic.adders import gidney_adder
if isinstance(other, QuantumFloat):
starting_digit = jnp.maximum(other.exponent, self.exponent)
gidney_adder(
other[starting_digit - other.exponent :],
self[starting_digit - self.exponent :],
)
elif isinstance(other, (int, float, np.integer, np.floating)) or (
isinstance(other, Tracer) and isinstance(other, Array)
):
# gidney_adder's stub predates encoder() returning a traced
# jax.Array here; a concrete int or a traced Array both work
# at runtime.
gidney_adder(self.encoder(other), self) # pyright: ignore[reportArgumentType]
else:
raise TypeError(f"Don't know how to handle quantum addition with type {type(other)}")
return self
# NOTE: Local import to avoid a circular import (qrisp.alg_primitives.arithmetic imports from qrisp.qtypes).
from qrisp.alg_primitives.arithmetic import polynomial_encoder
if isinstance(other, QuantumFloat):
input_qf_list = [other]
poly = sp.symbols("x")
polynomial_encoder(input_qf_list, self, poly)
elif isinstance(other, (int, float, np.number)):
scaled = other / 2**self.exponent
if int(scaled) != scaled:
raise ValueError(
"Tried to perform in-place addition with invalid number. QuantumFloat precision too low."
)
input_qf_list = []
poly = sp.sympify(other)
polynomial_encoder(input_qf_list, self, poly)
else:
raise TypeError(f"In-place addition for type {type(other)} not implemented")
return self
@gate_wrap(permeability=[1], is_qfree=True)
def __isub__(self, other: QuantumFloat | int | float | np.integer | np.floating | Array) -> QuantumFloat:
"""Subtract another QuantumFloat or a classical scalar from this QuantumFloat, in place."""
if check_for_tracing_mode():
with invert():
self.__iadd__(other)
return self
# NOTE: Local import to avoid a circular import (qrisp.alg_primitives.arithmetic imports from qrisp.qtypes).
from qrisp.alg_primitives.arithmetic import polynomial_encoder
if isinstance(other, QuantumFloat):
input_qf_list = [other]
poly = -sp.symbols("x")
polynomial_encoder(input_qf_list, self, poly)
elif isinstance(other, (int, float, np.integer, np.floating)):
scaled = other / 2**self.exponent
if int(scaled) != scaled:
raise ValueError(
"Tried to perform in-place subtraction with invalid number. QuantumFloat precision too low."
)
input_qf_list = []
poly = -sp.sympify(other)
polynomial_encoder(input_qf_list, self, poly)
else:
raise TypeError(f"In-place subtraction for type {type(other)} not implemented")
return self
@gate_wrap(permeability=[], is_qfree=True)
def __imul__(self, other: int | float | np.integer | np.floating) -> QuantumFloat:
"""Multiply this QuantumFloat by a classical scalar, in place."""
# NOTE: Local import to avoid a circular import (qrisp.alg_primitives.arithmetic imports from qrisp.qtypes).
from qrisp.alg_primitives.arithmetic import inpl_mult
inpl_mult(self, other)
return self
def __irshift__(self, k: int) -> QuantumFloat:
"""Shift this QuantumFloat's exponent down by k (a free, gate-less bitshift)."""
self.exp_shift(-k)
return self
def __ilshift__(self, k: int) -> QuantumFloat:
"""Shift this QuantumFloat's exponent up by k (a free, gate-less bitshift)."""
self.exp_shift(k)
return self
def __lt__(self, other: QuantumFloat | int | float) -> "QuantumBool":
"""Compare this QuantumFloat to another QuantumFloat or a classical scalar (<)."""
# NOTE: Local import to avoid a circular import (qrisp.alg_primitives.arithmetic imports from qrisp.qtypes).
from qrisp.alg_primitives.arithmetic import gidney_adder, lt, uint_lt
if check_for_tracing_mode():
return uint_lt(self, other, gidney_adder) # pyright: ignore[reportReturnType]
if not isinstance(other, (QuantumFloat, int, float)):
raise TypeError(f"Comparison with type {type(other)} not implemented")
return lt(self, other) # pyright: ignore[reportReturnType]
def __gt__(self, other: QuantumFloat | int | float) -> "QuantumBool":
"""Compare this QuantumFloat to another QuantumFloat or a classical scalar (>)."""
# NOTE: Local import to avoid a circular import (qrisp.alg_primitives.arithmetic imports from qrisp.qtypes).
from qrisp.alg_primitives.arithmetic import gidney_adder, gt, uint_gt
if check_for_tracing_mode():
return uint_gt(self, other, gidney_adder) # pyright: ignore[reportReturnType]
if not isinstance(other, (QuantumFloat, int, float)):
raise TypeError(f"Comparison with type {type(other)} not implemented")
return gt(self, other) # pyright: ignore[reportReturnType]
def __le__(self, other: QuantumFloat | int | float) -> "QuantumBool":
"""Compare this QuantumFloat to another QuantumFloat or a classical scalar (<=)."""
# NOTE: Local import to avoid a circular import (qrisp.alg_primitives.arithmetic imports from qrisp.qtypes).
from qrisp.alg_primitives.arithmetic import gidney_adder, leq, uint_le
if check_for_tracing_mode():
return uint_le(self, other, gidney_adder)
if not isinstance(other, (QuantumFloat, int, float)):
raise TypeError(f"Comparison with type {type(other)} not implemented")
return leq(self, other)
def __ge__(self, other: QuantumFloat | int | float) -> "QuantumBool":
"""Compare this QuantumFloat to another QuantumFloat or a classical scalar (>=)."""
# NOTE: Local import to avoid a circular import (qrisp.alg_primitives.arithmetic imports from qrisp.qtypes).
from qrisp.alg_primitives.arithmetic import geq, gidney_adder, uint_ge
if check_for_tracing_mode():
return uint_ge(self, other, gidney_adder)
if not isinstance(other, (QuantumFloat, int, float)):
raise TypeError(f"Comparison with type {type(other)} not implemented")
return geq(self, other)
def __eq__(self, other: object) -> "QuantumBool":
"""Compare this QuantumFloat to another QuantumFloat or a classical scalar (==)."""
# NOTE: Local import to avoid a circular import (qrisp.alg_primitives.arithmetic imports from qrisp.qtypes).
from qrisp.alg_primitives.arithmetic import eq
if not check_for_tracing_mode() and not isinstance(other, (QuantumFloat, int, float)):
raise TypeError(f"Comparison with type {type(other)} not implemented")
return eq(self, other)
def __ne__(self, other: object) -> "QuantumBool":
"""Compare this QuantumFloat to another QuantumFloat or a classical scalar (!=)."""
# NOTE: Local import to avoid a circular import (qrisp.alg_primitives.arithmetic imports from qrisp.qtypes).
from qrisp.alg_primitives.arithmetic import neq
if not check_for_tracing_mode() and not isinstance(other, (QuantumFloat, int, float)):
raise TypeError(f"Comparison with type {type(other)} not implemented")
return neq(self, other)
[docs]
def exp_shift(self, shift: int) -> None:
"""Performs an internal bit shift.
Note that this method doesn't cost any quantum gates. For the quantum
version of this method, see
:meth:`quantum_bit_shift<qrisp.QuantumFloat.quantum_bit_shift>`.
Parameters
----------
shift : int
The amount to shift.
Raises
------
TypeError
Tried to shift QuantumFloat exponent by non-integer value
Examples
--------
We create a QuantumFloat and perform a bitshift:
>>> from qrisp import QuantumFloat
>>> a = QuantumFloat(4)
>>> a[:] = 2
>>> a.exp_shift(2)
>>> print(a)
{8: 1.0}
>>> print(a.qs)
QuantumCircuit:
---------------
a.0: ─────
┌───┐
a.1: ┤ X ├
└───┘
a.2: ─────
<BLANKLINE>
a.3: ─────
<BLANKLINE>
Live QuantumVariables:
----------------------
QuantumFloat a
"""
if not isinstance(shift, int):
raise TypeError("Tried to shift QuantumFloat exponent by non-integer value")
self.exponent += shift
[docs]
def add_sign(self) -> None:
"""Turns an unsigned QuantumFloat into its signed version.
Raises
------
ValueError
Tried to add sign to signed QuantumFloat.
Examples
--------
>>> from qrisp import QuantumFloat
>>> qf = QuantumFloat(4)
>>> qf.signed
False
>>> qf.add_sign()
>>> qf.signed
True
"""
if self.signed:
raise ValueError("Tried to add sign to signed QuantumFloat")
self.extend(1, self.size)
self.signed = True
[docs]
def sign(self) -> "Qubit":
r"""Returns the sign qubit.
This qubit is in state $\ket{1}$ if the QuantumFloat holds a negative value and
in state $\ket{0}$ otherwise.
For more information about the encoding of negative numbers check the
`publication <https://ieeexplore.ieee.org/document/9815035>`_.
.. warning::
Performing an X gate on this qubit does not flip the sign! Use in-place
multiplication instead.
>>> from qrisp import QuantumFloat
>>> qf = QuantumFloat(3, signed = True)
>>> qf[:] = 3
>>> qf *= -1
>>> print(qf)
{-3: 1.0}
Raises
------
ValueError
Tried to retrieve sign qubit of unsigned QuantumFloat.
Returns
-------
Qubit
The qubit holding the sign.
Examples
--------
We create a QuantumFloat, initiate a state that has probability 2/3 of being
negative and entangle a QuantumBool with the sign qubit.
>>> from qrisp import QuantumFloat, QuantumBool, cx
>>> qf = QuantumFloat(4, signed = True)
>>> n_amp = 1/3**0.5
>>> qf[:] = {-1 : n_amp, -2 : n_amp, 1 : n_amp}
>>> qbl = QuantumBool()
>>> _ = cx(qf.sign(), qbl)
>>> print(qbl)
{True: 0.66667, False: 0.33333}
"""
if not self.signed:
raise ValueError("Tried to retrieve sign qubit of unsigned QuantumFloat")
return self[-1] # pyright: ignore[reportReturnType]
def init_from(
self, other: QuantumFloat, ignore_rounding_errors: bool = False, ignore_overflow_errors: bool = False
) -> None:
"""Initialize this (zero-valued) QuantumFloat with the value of another one.
Parameters
----------
other : QuantumFloat
The QuantumFloat to copy the value from.
ignore_rounding_errors : bool, optional
If ``True``, don't raise if ``other`` has more precision than
this QuantumFloat can represent. The default is False.
ignore_overflow_errors : bool, optional
If ``True``, don't raise if ``other`` can represent larger
magnitudes than this QuantumFloat. The default is False.
"""
copy_qf(
self,
other,
ignore_rounding_errors=ignore_rounding_errors,
ignore_overflow_errors=ignore_overflow_errors,
)
def incr(self, value: int | float | np.integer | np.floating | None = None) -> None:
"""Increment this QuantumFloat in place by a classical value.
Parameters
----------
value : int, float, np.integer, or np.floating, optional
The value to increment by. The default is this QuantumFloat's
smallest representable increment, ``2**self.exponent``.
"""
# NOTE: Local import to avoid a circular import (qrisp.alg_primitives.arithmetic imports from qrisp.qtypes).
from qrisp.alg_primitives.arithmetic.adders.incrementation import increment
if value is None:
# increment() is eager-only (Python-level control flow on the
# amount), so self.exponent is concretely int here even though
# its class-level type also allows a traced jax.Array.
value = 2**self.exponent # pyright: ignore[reportAssignmentType]
increment(self, value)
def __hash__(self) -> int:
"""Hash by object identity (QuantumFloats define __eq__, which disables the default hash)."""
return id(self)
[docs]
def significant(self, k: int) -> "Qubit":
"""Returns the qubit with significance $k$.
Parameters
----------
k : int
The significance.
Raises
------
ValueError
Tried to retrieve invalid significant from QuantumFloat
Returns
-------
Qubit
The Qubit with significance $k$.
Examples
--------
We create a QuantumFloat and flip a qubit of specified significance.
>>> from qrisp import QuantumFloat, x
>>> qf = QuantumFloat(6, -3)
>>> _ = x(qf.significant(-2))
>>> print(qf)
{0.25: 1.0}
The qubit with significance $-2$ corresponds to the value $0.25 = 2^{-2}$.
>>> _ = x(qf.significant(2))
>>> print(qf)
{4.25: 1.0}
The qubit with significance $2$ corresponds to the value $4 = 2^{2}$.
"""
min_sig, max_sig = self.mshape
if not min_sig <= k < max_sig:
raise ValueError(
f"Tried to retrieve invalid significant {k} from QuantumFloat with mantissa shape {self.mshape}"
)
return self[k - min_sig] # pyright: ignore[reportReturnType]
[docs]
def truncate(self, value: int | float | bool | np.integer | np.floating | Tracer) -> int | float | Array:
"""Receives a regular float and returns the float that is closest to the input but can still be encoded.
Parameters
----------
value : int, float, bool, np.integer, np.floating, or jax.core.Tracer
A real-valued number that is supposed to be truncated.
Returns
-------
int, float, or jax.Array
The truncated value: an ``int`` or ``float`` outside of tracing
mode (depending on whether the exponent is non-negative, see
:meth:`decoder <qrisp.QuantumFloat.decoder>`), or a traced
``jax.Array`` while tracing.
Examples
--------
We create a QuantumFloat and round a value to the closest one it can
represent. Note that directly encoding an unrepresentable value (like
``0.5102341`` below, which doesn't fit this QuantumFloat's precision
of $2^{-1} = 0.5$) already truncates silently, so ``truncate`` is
most useful when you want to know the resulting value ahead of time:
>>> from qrisp import QuantumFloat
>>> qf = QuantumFloat(4, -1)
>>> value = 0.5102341
>>> rounded_value = qf.truncate(value)
>>> rounded_value
0.5
>>> qf[:] = rounded_value
>>> print(qf)
{0.5: 1.0}
"""
# Clip in floating point before converting to int64: converting a
# float far outside int64's range is platform-dependent behavior, not
# a guaranteed saturating clamp.
# 2.0**self.msize is computed via jax (not plain Python float
# exponentiation) so an oversized msize (>= 1024) saturates to inf
# instead of raising OverflowError.
bound = jnp.float64(2.0) ** self.msize
res = jnp.round(value / jnp.float64(2) ** self.exponent)
res = jnp.minimum(bound - 1, res)
if self.signed:
res = jnp.maximum(-bound, res)
res = _signed_int_iso(jnp.int64(res), self.size)
else:
res = jnp.maximum(0.0, res)
res = jnp.int64(res)
return self.decoder(res)
[docs]
def get_ev(self, **mes_kwargs: Any) -> float:
"""Retrieves the expectation value of self.
Parameters
----------
**mes_kwargs : dict
Keyword arguments for the measurement. See :meth:`qrisp.QuantumVariable.get_measurement` for more information.
Returns
-------
float
The expectation value.
Examples
--------
We set up a QuantumFloat in uniform superposition and retrieve the expectation value:
>>> from qrisp import QuantumFloat, h
>>> qf = QuantumFloat(4)
>>> _ = h(qf)
>>> qf.get_ev()
7.5
"""
mes_res = self.get_measurement(**mes_kwargs)
return sum(k * v for k, v in mes_res.items()) # pyright: ignore[reportReturnType]
[docs]
def quantum_bit_shift(self, shift_amount: int | QuantumFloat) -> None:
"""Performs a bit shift in the quantum device.
While :meth:`exp_shift<qrisp.QuantumFloat.exp_shift>` performs a bit shift
in the compiler (thus costing no quantum gates), this method performs the
bit shift on the hardware.
This has the advantage that it can be controlled if called within a
:ref:`ControlEnvironment` and furthermore admits bit shifts based on the
state of a QuantumFloat.
.. note::
Bit shifts based on a QuantumFloat are currently only possible
if both self and ``shift_amount`` are unsigned.
.. warning::
Quantum bit shifting extends the QuantumFloat (ie. it allocates
additional qubits).
Parameters
----------
shift_amount : int or QuantumFloat
The amount to shift.
Raises
------
TypeError
Tried to shift QuantumFloat exponent by non-integer value
Exception
Quantum-quantum bitshifting is currently only supported for unsigned arguments
Examples
--------
We create a QuantumFloat and a QuantumBool to perform a controlled bit
shift, then evaluate the resulting (superposed) state:
::
from qrisp import QuantumFloat, QuantumBool, h
qf = QuantumFloat(4)
qf[:] = 1
qbl = QuantumBool()
h(qbl)
with qbl:
qf.quantum_bit_shift(2)
print(qf.qs.statevector())
# Yields
# sqrt(2)*(|1>*|False> + |4>*|True>)/2
"""
# NOTE: Local import to avoid a circular import (qrisp.alg_primitives.arithmetic imports from qrisp.qtypes).
from qrisp.alg_primitives.arithmetic import quantum_bit_shift
quantum_bit_shift(self, shift_amount)
def _power_conjugator(base: QuantumFloat, power: int, temp_results: list[QuantumFloat]) -> None:
"""Conjugator for QuantumFloat.__pow__: fills temp_results[i] with base**(i + 1).
Parameters
----------
base : QuantumFloat
The QuantumFloat being raised to a power.
power : int
The power ``base`` is being raised to.
temp_results : list[QuantumFloat]
Freshly allocated QuantumFloats, one per power from 1 to ``power``,
filled in place: ``temp_results[i]`` ends up holding ``base**(i + 1)``.
"""
# NOTE: Local import to avoid a circular import (qrisp.alg_primitives.arithmetic imports from qrisp.qtypes).
from qrisp.alg_primitives.arithmetic import jasp_multiplyer
cx(base, temp_results[0])
for i in range(power - 1):
(temp_results[i + 1] << jasp_multiplyer)(base, temp_results[i])
def _addsub_bounds(op0: QuantumFloat, op1: QuantumFloat) -> tuple[int | Array, int | Array]:
"""Compute the (exponent, max_sig) bounds for an add/sub output QuantumFloat.
Comparisons like min/max need concrete values to branch on, which a jax
tracer can't provide -- so this only uses jnp when actually tracing (see
:func:`check_for_tracing_mode`). Outside of tracing, jnp.minimum/maximum
would silently turn a plain-int exponent into a 0-d jax.Array, which then
breaks any later ``2**exponent`` with a negative exponent (jax's
integer_pow rejects negative integer exponents).
Parameters
----------
op0 : QuantumFloat
The first operand.
op1 : QuantumFloat
The second operand.
Returns
-------
tuple[int or jax.Array, int or jax.Array]
The (exponent, max_sig) bounds for sizing the output QuantumFloat.
"""
if check_for_tracing_mode():
exponent = jnp.minimum(op0.exponent, op1.exponent)
max_sig = jnp.maximum(op0.mshape[1], op1.mshape[1]) + 1
else:
exponent = min(op0.exponent, op1.exponent)
max_sig = max(op0.mshape[1], op1.mshape[1]) + 1
return exponent, max_sig
def _prod(values: Iterable[sp.Expr]) -> sp.Expr:
"""Multiply an iterable of sympy expressions together.
Seeded from the first element (rather than ``math.prod``'s default seed
of ``1``) so this also accepts non-numeric multiplicative types; here,
the powers of sympy Symbols making up one monomial of a polynomial.
Parameters
----------
values : Iterable[sympy.Expr]
The values to multiply together. Must be non-empty.
Returns
-------
sympy.Expr
The product of every element in ``values``.
"""
values = list(values)
res = values[0]
for value in values[1:]:
# sympy's Expr stubs don't model __imul__ precisely enough for pyright
# here, even though multiplying two Exprs is a real, supported operation.
res *= value # pyright: ignore[reportOperatorIssue]
return res
def _all_quantum_modulus(operands: list[QuantumFloat]) -> TypeGuard[list[QuantumModulus]]:
"""Check whether every operand is a QuantumModulus, narrowing the list element type.
A plain ``all(isinstance(operand, QuantumModulus) for operand in operands)``
is just as correct at runtime, but pyright can't propagate a narrowed
element type out of that expression -- wrapping it in a
:data:`~typing.TypeGuard`-annotated function is what lets callers use
``operands[i].m``/``.modulus`` afterwards without a type: ignore.
"""
# NOTE: Local import to avoid a circular import (QuantumModulus subclasses QuantumFloat, so
# qrisp.qtypes can only expose QuantumModulus after this module has finished loading).
from qrisp.qtypes import QuantumModulus
return all(isinstance(operand, QuantumModulus) for operand in operands)
def _polynomial_output_qf(operands: list[QuantumFloat], op: sp.Expr) -> QuantumFloat:
"""Size the output QuantumFloat for a polynomial-encoding operation.
Parameters
----------
operands : list[QuantumFloat]
The QuantumFloats participating in the polynomial. Every operand's
``name`` must be unique: ``op`` refers to each operand by
``sympy.Symbol(operand.name)``, so a repeated name would make two
distinct operands indistinguishable in the polynomial.
op : sympy.Expr
The polynomial expression being encoded.
Returns
-------
QuantumFloat
A freshly allocated QuantumFloat, sized to hold the result of ``op``
without overflow.
Raises
------
ValueError
If two or more operands share the same ``name``.
"""
# NOTE: Local import to avoid a circular import (qrisp.alg_primitives.arithmetic imports from qrisp.qtypes).
from qrisp.alg_primitives.arithmetic.poly_tools import expr_to_list
# Only called for its validation side effect (raises if op isn't
# actually a polynomial); sp.Poly() below doesn't catch that on its own.
_ = expr_to_list(op)
names = [operand.name for operand in operands]
if len(set(names)) != len(names):
duplicates = {name for name in names if names.count(name) > 1}
raise ValueError(
f"Duplicate QuantumFloat name(s) {sorted(duplicates)} among operands; "
"polynomial encoding requires every operand to have a unique name."
)
# sympy's type stubs don't model Poly's/Abs's dynamic attribute
# surface, so pyright can't see .gens/.coeffs()/.monoms()/.subs()
# here even though they're all real Poly/Basic members.
poly = sp.Poly(op) # pyright: ignore[reportAttributeAccessIssue]
monom_list = [
a * _prod(sym**k for sym, k in zip(poly.gens, mon)) # pyright: ignore[reportAttributeAccessIssue]
for a, mon in zip(poly.coeffs(), poly.monoms()) # pyright: ignore[reportAttributeAccessIssue]
]
max_value_dic = {sp.Symbol(qf.name): 2.0 ** qf.mshape[1] for qf in operands}
min_value_dic = {sp.Symbol(qf.name): 2.0 ** qf.mshape[0] for qf in operands}
abs_poly = sum((sp.Abs(monom) for monom in monom_list), 0) # pyright: ignore[reportCallIssue, reportArgumentType]
min_poly_value = min(float(sp.Abs(monom).subs(min_value_dic)) for monom in monom_list) # pyright: ignore[reportAttributeAccessIssue]
max_poly_value = float(abs_poly.subs(max_value_dic))
min_sig = int(np.floor(np.log2(min_poly_value)))
max_sig = int(np.ceil(np.log2(max_poly_value)))
return QuantumFloat(
max_sig - min_sig,
exponent=min_sig,
signed=any(operand.signed for operand in operands),
)
def create_output_qf(operands: list[QuantumFloat], op: str | sp.Expr) -> QuantumFloat:
"""Determine the appropriately-sized output QuantumFloat for an arithmetic operation.
Parameters
----------
operands : list[QuantumFloat]
The QuantumFloats participating in the operation.
op : str or sympy.Expr
Either one of "add", "sub", "mul", or a sympy expression describing
a polynomial encoding (see :func:`polynomial_encoder <qrisp.polynomial_encoder>`).
Returns
-------
QuantumFloat
A freshly allocated QuantumFloat, sized to hold the result of ``op``
without overflow.
"""
if isinstance(op, sp.Expr):
return _polynomial_output_qf(operands, op)
if _all_quantum_modulus(operands):
res = operands[0].duplicate()
if op == "mul":
res.m = (
operands[0].m
+ operands[1].m
- (int(np.ceil(np.log2((operands[0].modulus - 1) ** 2) + 1)) - operands[0].size)
)
return res
if op == "add":
signed = operands[0].signed or operands[1].signed
exponent, max_sig = _addsub_bounds(operands[0], operands[1])
# TODO: max_sig already includes the extra bit a sum can carry into, so
# this "+ 1" allocates one more mantissa qubit than an add/sub result
# actually needs (verified: every possible result still fits in
# max_sig - exponent). Left in place because removing it changes the
# output size of every QuantumFloat +/- for existing callers -- code
# that pre-allocates a same-sized register to inject a computation
# into (e.g. via QuantumArray's `<<`) can silently break if the two
# sizes stop matching. Tightening this needs that class of caller
# audited first, and should probably ship as a documented,
# version-flagged compatibility change rather than a quiet resize.
msize = max_sig - exponent + 1
return QuantumFloat(msize, exponent, operands[0].qs, signed=signed, name="add_res*")
if op == "mul":
signed = operands[0].signed or operands[1].signed
if operands[0].reg == operands[1].reg and (operands[0].signed and operands[1].signed):
signed = False
return QuantumFloat(
operands[0].msize + operands[1].msize + operands[0].signed * operands[1].signed,
operands[0].exponent + operands[1].exponent,
operands[0].qs,
signed=signed,
name="mul_res*",
)
if op == "sub":
exponent, max_sig = _addsub_bounds(operands[0], operands[1])
# TODO: see the identical "+ 1" note in the "add" branch above.
msize = max_sig - exponent + 1
return QuantumFloat(msize, exponent, operands[0].qs, signed=True, name="sub_res*")
raise ValueError(f"Don't know how to create output QuantumFloat for operation {op}")
# Initiates the value of qf2 into qf1 where qf1 has to hold the value 0
def copy_qf(
qf1: QuantumFloat, qf2: QuantumFloat, ignore_overflow_errors: bool = False, ignore_rounding_errors: bool = False
) -> None:
"""Initiate the value of qf2 into qf1, where qf1 has to hold the value 0.
Parameters
----------
qf1 : QuantumFloat
The (zero-valued) QuantumFloat to copy the value into.
qf2 : QuantumFloat
The QuantumFloat to copy the value from.
ignore_overflow_errors : bool, optional
If ``True``, don't raise if qf2 can represent larger magnitudes than
qf1. The default is False.
ignore_rounding_errors : bool, optional
If ``True``, don't raise if qf2 has more precision than qf1 can
represent. The default is False.
"""
# Each QuantumFloat's qubit i has significance qf.exponent + i, a
# contiguous run -- so its bounds are plain arithmetic, no list needed.
qf1_sig_range = range(qf1.exponent, qf1.exponent + qf1.size)
qf2_sig_range = range(qf2.exponent, qf2.exponent + qf2.size)
# Check overflow/underflow
if max(qf1_sig_range) < max(qf2_sig_range) and not ignore_overflow_errors:
raise ValueError("Copy operation would result in overflow (use ignore_overflow_errors = True)")
if min(qf1_sig_range) > min(qf2_sig_range) and not ignore_rounding_errors:
raise ValueError("Copy operation would result in rounding (use ignore_rounding_errors = True)")
qs = qf1.qs
# Qubit counts to copy, excluding the sign qubit (the last one) when qf2
# is signed -- it's handled on its own below.
qf1_len = qf1.size
qf2_len = qf2.size
if qf2.signed:
if not qf1.signed:
raise ValueError("Tried to copy signed into unsigned float")
qf1_len -= 1
qf2_len -= 1
qf1_start = qf1.exponent
qf2_start = qf2.exponent
# Highest significance in qf2's own mantissa range. For a signed,
# zero-mantissa qf2 (qf2_len == 0), this is qf2_start - 1, so every
# significance in qf1 at or above qf2_start (there being no actual qf2
# mantissa bit to overlap with) is still sign-extended below.
qf2_max = qf2_start + qf2_len - 1
# QuantumVariable.qs/__getitem__ aren't typed precisely enough for pyright
# to see qs as a QuantumSession (with .cx) here rather than the
# TracingQuantumSession union member, or single-index __getitem__ as
# returning a Qubit rather than DynamicQubitArray -- both hold in this
# non-tracing, single-qubit-index context.
for i in range(qf1_len):
significance = qf1_start + i
# If we are in a realm where both floats have overlapping significance
# => CNOT into each other
rel_index = significance - qf2_start
if 0 <= rel_index < qf2_len:
qs.cx(qf2[rel_index], qf1[i]) # pyright: ignore[reportAttributeAccessIssue, reportArgumentType]
continue
# Otherwise copy the sign bit into the bits of higher significance than qf2
if qf2.signed and significance > qf2_max:
qs.cx(qf2[-1], qf1[i]) # pyright: ignore[reportAttributeAccessIssue, reportArgumentType]
# Copy the sign bit
if qf2.signed:
qs.cx(qf2[-1], qf1[-1]) # pyright: ignore[reportAttributeAccessIssue, reportArgumentType]