# Source code for qrisp.qtypes.quantum_float

"""
\********************************************************************************
* Copyright (c) 2023 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
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
"""

import numpy as np
import sympy as sp

from qrisp.arithmetic import (
eq,
geq,
gt,
inpl_mult,
leq,
lt,
neq,
polynomial_encoder,
q_mult,
quantum_bit_shift,
)
from qrisp.core import QuantumVariable
from qrisp.misc import gate_wrap

def signed_int_iso(x, n):
if int(x) < -(2**n) or int(x) >= 2**n:
raise Exception("Applying signed integer isomorphism resulted in overflow")

if x >= 0:
return x % 2**n
else:
return -abs(x) % 2 ** (n + 1)

def signed_int_iso_inv(y, n):
y = y % 2 ** (n + 1)
if y < 2**n:
return y
else:
return -(2 ** (n + 1)) + y

# Truncates a polynomial of the form p(x) = 2**k_0*x*i_0 + 2**k_1*x**i_1 ...
# where every summand where the power of the coefficients does not lie in the interval
# trunc bounds is removed
def trunc_poly(poly, trunc_bounds):
# Convert to sympy polynomial
poly = sp.poly(poly)

# Clip upper bound
poly = poly.trunc(2.0 ** (trunc_bounds))

# Clip lower bound
poly = poly / 2.0 ** trunc_bounds
poly = poly - sp.poly(poly).trunc(1)
poly = poly * 2.0 ** trunc_bounds

return poly.expr.expand()

[docs]class QuantumFloat(QuantumVariable):
r"""
This subclass of :ref:QuantumVariable can represent floating point numbers
(signed and unsigned) up to an 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 amount of mantissa qubits and the -1 indicates the
exponent.

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.

>>> 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.0: 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 can not be stored in a finite amount 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 inplace 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::
are performed modular <https://en.wikipedia.org/wiki/Modular_arithmetic>_.

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

For inplace 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)
>>> 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 is corresponding to
comparison_qbl_0 and the second one is comparison_qbl_1.

"""

def __init__(self, msize, exponent=0, qs=None, name=None, signed=False):
# Boolean to indicate if the float is signed
self.signed = signed

# Exponent
self.exponent = exponent

# Size of the mantissa
self.msize = msize

# Array that consists of (log2(min), log2(max)) where min and max are the
# minimal and maximal values of the absolutes that the QuantumFloat can
# represent.
self.mshape = np.array([exponent, exponent + msize])

# Initialize QuantumVariable
if signed:
super().__init__(msize + 1, qs, name=name)
else:
super().__init__(msize, qs, name=name)

# Define outcome_labels
def decoder(self, i):
if self.signed:
res = signed_int_iso_inv(i, self.size - 1) * 2.0**self.exponent
else:
res = i * 2**self.exponent

if self.exponent >= 0:
return int(res)
else:
return res

# def encoder(self, i):
#     if self.signed:
#         res = int(signed_int_iso(i/2**self.exponent, self.size-1))
#     else:
#         res =  int(i/2**self.exponent)

#     return res

[docs]    def sb_poly(self, m=0):
"""
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
--------

>>> from qrisp import QuantumFloat
>>> x = QuantumFloat(3, -1, signed = True, name = "x")
>>> print(x.sb_poly(5))
0.5*x_0 + 1.0*x_1 + 2.0*x_2 + 28.0*x_3

"""

if m == 0:
m = self.size

symbols = [sp.symbols(self.name + "_" + str(i)) for i in range(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]

return 2**self.exponent * poly

def encode(self, encoding_number, rounding=False, permit_dirtyness=False):
if rounding:
# Round value to closest fitting number
outcome_labels = [self.decoder(i) for i in range(2**self.size)]
encoding_number = outcome_labels[
np.argmin(np.abs(encoding_number - np.array(outcome_labels)))
]

super().encode(encoding_number, permit_dirtyness=permit_dirtyness)

@gate_wrap(permeability="args", is_qfree=True)
def __mul__(self, other):
if isinstance(other, QuantumFloat):
return q_mult(self, other)
elif isinstance(other, int):
bit_shift = 0
while not other % 2:
other = other >> 1
bit_shift += 1

if self.signed or other < 0:
output_qf = QuantumFloat(
self.msize + int(np.ceil(np.log2(abs(other)))),
self.exponent,
signed=True,
)
else:
output_qf = QuantumFloat(
self.msize + int(np.ceil(np.log2(abs(other)))),
self.exponent,
signed=False,
)

polynomial_encoder([self], output_qf, other * sp.Symbol("x"))

output_qf << bit_shift

return output_qf
else:
raise Exception(
"QuantumFloat multiplication for type " + str(type(other)) + ""
" not implemented (available are QuantumFloat and int)"
)

@gate_wrap(permeability="args", is_qfree=True)
if isinstance(other, QuantumFloat):
elif isinstance(other, (int, float)):
res = self.duplicate(init=True)
res += other
return res
else:
raise Exception(
"Addition with type " + str(type(other)) + " not implemented"
)

@gate_wrap(permeability="args", is_qfree=True)
def __sub__(self, other):
from qrisp.arithmetic import sbp_sub

if isinstance(other, QuantumFloat):
return sbp_sub(self, other)
elif isinstance(other, (int, float)):
res = self.duplicate(init=True)
res -= other
return res
else:
raise Exception(
"Subtraction with type " + str(type(other)) + " not implemented"
)

__rmul__ = __mul__

@gate_wrap(permeability="args", is_qfree=True)
def __rsub__(self, other):
from qrisp import x
from qrisp.arithmetic import sbp_sub

if isinstance(other, QuantumFloat):
return sbp_sub(other, self)
elif isinstance(other, (int, float)):
res = self.duplicate(init=True)
if not res.signed:
x(res)
res -= other - 2**res.exponent
return res
else:
raise Exception(
"Subtraction with type " + str(type(other)) + " not implemented"
)

@gate_wrap(permeability="args", is_qfree=True)
def __truediv__(self, other):
from qrisp.arithmetic import q_div

return q_div(self, other)

@gate_wrap(permeability="args", is_qfree=True)
def __floordiv__(self, other):
if self.signed or other.signed:
raise Exception("Floor division not implemented for signed QuantumFloats")

if self.exponent < 0 or other.exponent < 0:
raise Exception(
"Tried to perform floor division on non-integer QuantumFloats"
)
from qrisp.arithmetic import q_div

return q_div(self, other, prec=0)

@gate_wrap(permeability="args", is_qfree=True)
def __pow__(self, power):
if power != -1:
raise Exception("Currently the only supported power is -1")

from qrisp.arithmetic import qf_inversion

return qf_inversion(self)

@gate_wrap(permeability=, is_qfree=True)
if isinstance(other, QuantumFloat):
input_qf_list = [other]
poly = sp.symbols("x")

polynomial_encoder(input_qf_list, self, poly)

elif isinstance(other, (int, float)):
# self.incr(other)

if not int(other / 2**self.exponent) == other / 2**self.exponent:
raise Exception(
"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 Exception(
"In-place addition for type " + str(type(other)) + " not implemented"
)

return self

@gate_wrap(permeability=, is_qfree=True)
def __isub__(self, other):
if isinstance(other, QuantumFloat):
input_qf_list = [other]
poly = -sp.symbols("x")

polynomial_encoder(input_qf_list, self, poly)

elif isinstance(other, (int, float)):
if not int(other / 2**self.exponent) == other / 2**self.exponent:
raise Exception(
"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 Exception(
"In-place substraction for type "
+ str(type(other))
+ " not implemented"
)

return self

@gate_wrap(permeability=[], is_qfree=True)
def __imul__(self, other):
inpl_mult(self, other)

return self

def __rshift__(self, k):
self.exp_shift(-k)
return self

def __lshift__(self, k):
self.exp_shift(k)
return self

def __lt__(self, other):
if not isinstance(other, (QuantumFloat, int, float)):
raise Exception(f"Comparison with type {type(other)} not implemented")

return lt(self, other)

def __gt__(self, other):
if not isinstance(other, (QuantumFloat, int, float)):
raise Exception(f"Comparison with type {type(other)} not implemented")

return gt(self, other)

def __le__(self, other):
if not isinstance(other, (QuantumFloat, int, float)):
raise Exception(f"Comparison with type {type(other)} not implemented")

return leq(self, other)

def __ge__(self, other):
if not isinstance(other, (QuantumFloat, int, float)):
raise Exception(f"Comparison with type {type(other)} not implemented")

return geq(self, other)

def __eq__(self, other):
if not isinstance(other, (QuantumFloat, int, float)):
raise Exception(f"Comparison with type {type(other)} not implemented")

return eq(self, other)

def __ne__(self, other):
if not isinstance(other, (QuantumFloat, int, float)):
raise Exception(f"Comparison with type {type(other)} not implemented")

return neq(self, other)

[docs]    def exp_shift(self, shift):
"""
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_bitshift>.

Parameters
----------
shift : int
The amount to shift.

Raises
------
Exception
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: ─────

Live QuantumVariables:
---------------------
QuantumFloat a

"""
if not isinstance(shift, int):
raise Exception("Tried to shift QuantumFloat exponent by non-integer value")

self.exponent += shift
self.mshape = self.mshape + shift

def reduce(self, qubits, verify=False):
QuantumVariable.reduce(self, qubits, verify)

try:
self.mshape -= len(qubits)
self.msize -= len(qubits)
except TypeError:
self.mshape -= 1
self.msize -= 1

def extend(self, amount, position=-1):
QuantumVariable.extend(self, amount, position=position)

self.mshape += amount
self.msize += amount

"""
Turns an unsigned QuantumFloat into its signed version.

Raises
------
Exception
Tried to add sign to signed QuantumFloat.

Examples
--------

>>> from qrisp import QuantumFloat
>>> qf = QuantumFloat(4)
>>> qf.signed
False
>>> qf.signed
True

"""

if self.signed:
raise Exception(r'Tried to add sign to signed QuantumFloat')

self.extend(1, self.size - 1)
self.mshape -= 1
self.msize -= 1
self.signed = True

[docs]    def sign(self):
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.

publication <https://ieeexplore.ieee.org/document/9815035>_.

.. warning::

Performing an X gate on this qubit does not flip the sign! Use inplace

>>> from qrisp import QuantumFloat
>>> qf = QuantumFloat(3, signed = True)
>>> qf[:] = 3
>>> qf *= -1
>>> print(qf)
{-3: 1.0}

Raises
------
Exception
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.6667, False: 0.3333}

"""
if not self.signed:
raise Exception("Tried to retrieve sign qubit of unsigned QuantumFloat")

return self[-1]

def init_from(
self, other, ignore_rounding_errors=False, ignore_overflow_errors=False
):
copy_qf(
self,
other,
ignore_rounding_errors=ignore_rounding_errors,
ignore_overflow_errors=ignore_overflow_errors,
)

def incr(self, x=None):
from qrisp.arithmetic.incrementation import increment

if x is None:
x = 2**self.exponent
increment(self, x)

def __hash__(self):
return id(self)

[docs]    def significant(self, k):
"""
Returns the qubit with significance $k$.

Parameters
----------
k : int
The significance.

Raises
------
Exception
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))
{4.25: 1.0}

The qubit with significance $2$ corresponds to the value $4 = 2^{2}$.

"""

sig_list = list(range(self.mshape, self.mshape))

if k not in sig_list:
raise Exception(
f"Tried to retrieve invalid significant {k} "
f"from QuantumFloat with mantissa shape {self.mshape}"
)

return self[sig_list.index(k)]

[docs]    def truncate(self, x):
"""
Receives a regular float and returns the float that is closest to the input but
can still be encoded.

Parameters
----------
x : float
A float that is supposed to be truncated.

Returns
-------
float
The truncated float.

Examples
--------

We create a QuantumFloat and round a value to fit the encoder and subsequently
initiate:

>>> from qrisp import QuantumFloat
>>> qf = QuantumFloat(4, -1)
>>> value = 0.5102341
>>> qf[:] = value
Exception: Value 0.5102341 not supported by encoder.
>>> rounded_value = qf.truncate(value)
>>> rounded_value
0.5
>>> qf[:] = rounded_value
>>> print(qf)
{0.5: 1.0}

"""
decoder_values = np.array([self.decoder(i) for i in range(2**self.size)])

return decoder_values[np.argmin(np.abs(decoder_values - x))]

[docs]    def get_ev(self, **mes_kwargs):
"""
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()])

[docs]    def quantum_bit_shift(self, shift_amount):
"""
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
bitshift 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 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

Parameters
----------
shift_amount : int or QuantumFloat
The amount to shift.

Raises
------
Exception
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.

::

from qrisp import QuantumFloat, QuantumBool, h
qf = QuantumFloat(4)
qf[:] = 1
qbl = QuantumBool()
h(qbl)

with qbl:
qf.quantum_bit_shift(2)

Evaluate the result

>>> print(qf.qs.statevector())
sqrt(2)*(|1>*|False> + |4>*|True>)/2

"""
quantum_bit_shift(self, shift_amount)

def create_output_qf(operands, op):
if isinstance(op, sp.core.expr.Expr):
from qrisp.arithmetic.poly_tools import expr_to_list

expr_list = expr_to_list(op)

for i in range(len(expr_list)):
if not isinstance(expr_list[i], sp.Symbol):
expr_list[i].pop(0)

operands.sort(key=lambda x: x.name)

def prod(iter):
iter = list(iter)
a = iter
for i in range(1, len(iter)):
a *= iter[i]

return a

from sympy import Abs, Poly, Symbol

poly = Poly(op)
monom_list = [
a * prod(x**k for x, k in zip(poly.gens, mon))
for a, mon in zip(poly.coeffs(), poly.monoms())
]

max_value_dic = {Symbol(qf.name): 2.0 ** qf.mshape for qf in operands}
min_value_dic = {Symbol(qf.name): 2.0 ** qf.mshape for qf in operands}

abs_poly = sum([Abs(monom) for monom in monom_list], 0)

min_poly_value = min(
[float(Abs(monom).subs(min_value_dic)) for monom in monom_list]
)

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)))

msize = max_sig - min_sig
exponent = min_sig

signed = bool(sum([int(operand.signed) for operand in operands]))

return QuantumFloat(msize, exponent=exponent, signed=signed)

signed = operands.signed or operands.signed
exponent = min(operands.exponent, operands.exponent)

max_sig = int(
np.ceil(
np.log2(int(2 ** operands.mshape + 2 ** operands.mshape))
)
)
msize = max_sig - exponent + 1
return QuantumFloat(
)

if op == "mul":
signed = operands.signed or operands.signed

if operands.reg == operands.reg and (
operands.signed and operands.signed
):
signed = False

return QuantumFloat(
operands.msize
+ operands.msize
+ operands.signed * operands.signed,
operands.exponent + operands.exponent,
operands.qs,
signed=signed,
name="mul_res*",
)

if op == "sub":
exponent = min(operands.exponent, operands.exponent)
max_sig = int(
np.ceil(
np.log2(int(2 ** operands.mshape + 2 ** operands.mshape))
)
)
msize = max_sig - exponent + 1

return QuantumFloat(
msize, exponent, operands.qs, signed=True, name="sub_res*"
)

# Initiates the value of qf2 into qf1 where qf1 has to hold the value 0
def copy_qf(qf1, qf2, ignore_overflow_errors=False, ignore_rounding_errors=False):
# Lists that translate Qubit index => Significance
qf1_sign_list = [qf1.exponent + i for i in range(qf1.size)]
qf2_sign_list = [qf2.exponent + i for i in range(qf2.size)]

# Check overflow/underflow
if max(qf1_sign_list) < max(qf2_sign_list) and not ignore_overflow_errors:
raise Exception(
"Copy operation would result in overflow "
"(use ignore_overflow_errors = True)"
)

if min(qf1_sign_list) > min(qf2_sign_list) and not ignore_rounding_errors:
raise Exception(
"Copy operation would result in rounding "
"(use ignore_rounding_errors = True)"
)

qs = qf1.qs

if qf2.signed:
if not qf1.signed:
raise Exception("Tried to copy signed into unsigend float")

# Remove last entry from significance list (last qubit is the sign qubit)
qf2_sign_list.pop(-1)
qf1_sign_list.pop(-1)

for i in range(len(qf1_sign_list)):
# If we are in a realm where both floats have overlapping significance
# => CNOT into each other
if qf1_sign_list[i] in qf2_sign_list:
qf2_index = qf2_sign_list.index(qf1_sign_list[i])
qs.cx(qf2[qf2_index], qf1[i])
continue

# Otherwise copy the sign bit into the bits of higher significance than qf2
if qf1_sign_list[i] > max(qf2_sign_list) and qf2.signed:
qs.cx(qf2[-1], qf1[i])

# Copy the sign bit
if qf2.signed:
qs.cx(qf2[-1], qf1[-1])