Skip to content

Commit

Permalink
Detach prefix util function (#6885)
Browse files Browse the repository at this point in the history
* add detach prefix function

* handling of zero, negative, out of range values

* add reno

* black

* update implementation and add precision handling

* fix round off error

* parameter expression handling

* fix import order

* multiply-or-divide trick

* update logic

* Update qiskit/utils/units.py

Co-authored-by: Matthew Treinish <mtreinish@kortar.org>

* add unit prefix test

* Update qiskit/utils/units.py

Co-authored-by: Matthew Treinish <mtreinish@kortar.org>

Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
  • Loading branch information
nkanazawa1989 and mtreinish authored Aug 19, 2021
1 parent 03183d1 commit 1ea5fd9
Show file tree
Hide file tree
Showing 5 changed files with 270 additions and 11 deletions.
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
124 changes: 114 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


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,114 @@ 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.
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, "µ")

0 comments on commit 1ea5fd9

Please sign in to comment.