Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detach prefix util function #6885

Merged
merged 18 commits into from
Aug 19, 2021
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion qiskit/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
local_hardware_info
is_main_process
apply_prefix
detach_prefix

Algorithm Utilities
===================
Expand Down Expand Up @@ -62,7 +63,7 @@
from .deprecation import deprecate_function
from .multiprocessing import local_hardware_info
from .multiprocessing import is_main_process
from .units import apply_prefix
from .units import apply_prefix, detach_prefix

from .circuit_utils import summarize_circuits
from .entangler_map import get_entangler_map, validate_entangler_map
Expand Down
123 changes: 113 additions & 10 deletions qiskit/utils/units.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,14 @@

"""SI unit utilities"""

from typing import Tuple, Optional, Union

def apply_prefix(value: float, unit: str) -> float:
import numpy as np

from qiskit.circuit.parameterexpression import ParameterExpression
Comment on lines +18 to +19
Copy link
Member

Choose a reason for hiding this comment

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

What's the use case for allowing ParameterExpression here? It seems more likely that the caller would be responsible for ensuring they're passing floats, rather than us.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That is the best scenario, but we have one exception

delay = Parameter("delay")

qc = QuantumCircuit(1)
qc.delay(delay, 0, unit="us")

qc = transpile(qc, backend)

In this code, we parametrize the delay in units of us. The transpiler needs to convert the SI time unit into dt to submit this circuit to backend. First, it applies prefix, then it multiplies backend._configuration.dt. ParameterExpression supports simple multiplication, thus these operation should be done while the actual value is parametrized. Note that unbound parameter expression cannot be converted into float.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Separate logic is removed with Decimal, but the import is still kept for type annotation.



def apply_prefix(value: Union[float, ParameterExpression], unit: str) -> float:
"""
Given a SI unit prefix and value, apply the prefix to convert to
standard SI unit.
Expand All @@ -25,16 +31,113 @@ def apply_prefix(value: float, unit: str) -> float:
Returns:
Converted value.

.. note::

This may induce tiny value error due to internal representation of float object.
See https://docs.python.org/3/tutorial/floatingpoint.html for details.

Raises:
Exception: If the units aren't recognized.
ValueError: If the ``units`` aren't recognized.
"""
downfactors = {"p": 1e12, "n": 1e9, "u": 1e6, "µ": 1e6, "m": 1e3}
upfactors = {"k": 1e3, "M": 1e6, "G": 1e9}
if not unit:
prefactors = {
"f": -15,
"p": -12,
"n": -9,
"u": -6,
"µ": -6,
"m": -3,
"k": 3,
"M": 6,
"G": 9,
"T": 12,
"P": 15,
}

if not unit or len(unit) == 1:
# for example, "m" can represent meter
return value
if unit[0] in downfactors:
return value / downfactors[unit[0]]
elif unit[0] in upfactors:
return value * upfactors[unit[0]]

if unit[0] not in prefactors:
raise ValueError(f"Could not understand unit: {unit}")

pow10 = prefactors[unit[0]]

# to avoid round-off error of prefactor
if pow10 < 0:
return value / pow(10, -pow10)

return value * pow(10, pow10)


def detach_prefix(value: float, decimal: Optional[int] = None) -> Tuple[float, str]:
"""
Given a SI unit value, find the most suitable prefix to scale the value.

For example, the ``value = 1.3e8`` will be converted into a tuple of ``(130.0, "M")``,
which represents a scaled value and auxiliary unit that may be used to display the value.
In above example, that value might be displayed as ``130 MHz`` (unit is arbitrary here).

Example:

>>> value, prefix = detach_prefix(1e4)
>>> print(f"{value} {prefix}Hz")
10 kHz

Args:
value: The number to find prefix.
decimal: Optional. An arbitrary integer number to represent a precision of the value.
If specified, it tries to round the mantissa and adjust the prefix to rounded value.
For example, 999_999.91 will become 999.9999 k with ``decimal=4``,
while 1.0 M with ``decimal=3`` or less.

Returns:
A tuple of scaled value and prefix.

Note:
This may induce tiny value error due to internal representation of float object.
See https://docs.python.org/3/tutorial/floatingpoint.html for details.
nkanazawa1989 marked this conversation as resolved.
Show resolved Hide resolved

Raises:
ValueError: If the ``value`` is out of range.
ValueError: If the ``value`` is not real number.
"""
prefactors = {
-15: "f",
-12: "p",
-9: "n",
-6: "µ",
-3: "m",
0: "",
3: "k",
6: "M",
9: "G",
12: "T",
15: "P",
}

if not np.isreal(value):
raise ValueError(f"Input should be real number. Cannot convert {value}.")

if np.abs(value) != 0:
pow10 = int(np.floor(np.log10(np.abs(value)) / 3) * 3)
else:
pow10 = 0

# to avoid round-off error of prefactor
if pow10 > 0:
mant = value / pow(10, pow10)
else:
raise Exception(f"Could not understand units: {unit}")
mant = value * pow(10, -pow10)

if decimal is not None:
# Corner case handling
# For example, 999_999.99 can be rounded to 1000.0 k rather than 1.0 M.
mant = np.round(mant, decimal)
if mant >= 1000:
mant /= 1000
pow10 += 3

if pow10 not in prefactors:
raise ValueError(f"Value is out of range: {value}")

return mant, prefactors[pow10]
8 changes: 8 additions & 0 deletions releasenotes/notes/add-detach-prefix-088e96b88ba29927.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
features:
- |
Add utility function :py:func:`~qiskit.utils.units.detach_prefix` that is a counterpart of
:py:func:`~qiskit.utils.units.apply_prefix`.
The new function returns a tuple of scaled value and prefix from a given float value.
For example, a value ``1.3e8`` will be converted into ``(130, "M")`` that can be
used to display a value in the user friendly format, such as `130 MHz`.
13 changes: 13 additions & 0 deletions test/python/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2021.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""Qiskit utilities tests."""
133 changes: 133 additions & 0 deletions test/python/utils/test_units.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2021.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""Test for unit conversion functions."""

from ddt import ddt, data

from qiskit.test import QiskitTestCase
from qiskit.utils import apply_prefix, detach_prefix


@ddt
class TestUnitConversion(QiskitTestCase):
"""Test the unit conversion utilities."""

def test_apply_prefix(self):
"""Test applying prefix to value."""
ref_values = [
([1.0, "THz"], 1e12),
([1.0, "GHz"], 1e9),
([1.0, "MHz"], 1e6),
([1.0, "kHz"], 1e3),
([1.0, "mHz"], 1e-3),
([1.0, "µHz"], 1e-6),
([1.0, "uHz"], 1e-6),
([1.0, "nHz"], 1e-9),
([1.0, "pHz"], 1e-12),
]

for args, ref_ret in ref_values:
self.assertEqual(apply_prefix(*args), ref_ret)

def test_not_convert_meter(self):
"""Test not apply prefix to meter."""
self.assertEqual(apply_prefix(1.0, "m"), 1.0)

def test_detach_prefix(self):
"""Test detach prefix from the value."""
ref_values = [
(1e12, (1.0, "T")),
(1e11, (100.0, "G")),
(1e10, (10.0, "G")),
(1e9, (1.0, "G")),
(1e8, (100.0, "M")),
(1e7, (10.0, "M")),
(1e6, (1.0, "M")),
(1e5, (100.0, "k")),
(1e4, (10.0, "k")),
(1e3, (1.0, "k")),
(100, (100.0, "")),
(10, (10.0, "")),
(1.0, (1.0, "")),
(0.1, (100.0, "m")),
(0.01, (10.0, "m")),
(1e-3, (1.0, "m")),
(1e-4, (100.0, "µ")),
(1e-5, (10.0, "µ")),
(1e-6, (1.0, "µ")),
(1e-7, (100.0, "n")),
(1e-8, (10.0, "n")),
(1e-9, (1.0, "n")),
(1e-10, (100.0, "p")),
(1e-11, (10.0, "p")),
(1e-12, (1.0, "p")),
]

for arg, ref_rets in ref_values:
self.assertTupleEqual(detach_prefix(arg), ref_rets)

def test_detach_prefix_with_zero(self):
"""Test detach prefix by input zero."""
self.assertTupleEqual(detach_prefix(0.0), (0.0, ""))

def test_detach_prefix_with_negative(self):
"""Test detach prefix by input negative values."""
self.assertTupleEqual(detach_prefix(-1.234e7), (-12.34, "M"))

def test_detach_prefix_with_value_too_large(self):
"""Test detach prefix by input too large value."""
with self.assertRaises(Exception):
self.assertTupleEqual(detach_prefix(1e20), (1e20, ""))

def test_detach_prefix_with_value_too_small(self):
"""Test detach prefix by input too small value."""
with self.assertRaises(Exception):
self.assertTupleEqual(detach_prefix(1e-20), (1e-20, ""))

def test_rounding(self):
"""Test detach prefix with decimal specification."""
ret = detach_prefix(999_999.991)
self.assertTupleEqual(ret, (999.999991, "k"))

ret = detach_prefix(999_999.991, decimal=4)
self.assertTupleEqual(ret, (1.0, "M"))

ret = detach_prefix(999_999.991, decimal=5)
self.assertTupleEqual(ret, (999.99999, "k"))

@data(
-20.791378538739863,
9.242757760406565,
2.7366806276451543,
9.183776167253349,
7.658091886606501,
-12.21553566621071,
8.914055281578145,
1.2518807770035825,
-6.652899195646036,
-4.647159596697976,
)
def test_get_same_value_after_attach_detach(self, value: float):
"""Test if same value can be obtained."""
unit = "Hz"

for prefix in ["P", "T", "G", "k", "m", "µ", "n", "p", "f"]:
scaled_val = apply_prefix(value, prefix + unit)
test_val, ret_prefix = detach_prefix(scaled_val)
self.assertAlmostEqual(test_val, value)
self.assertEqual(prefix, ret_prefix)

def test_get_symbol_mu(self):
"""Test if µ is returned rather than u."""
_, prefix = detach_prefix(3e-6)
self.assertEqual(prefix, "µ")