Commit 25148b06 authored by Sean Leavey's avatar Sean Leavey
Browse files

Remove Quantity class and replace with the one provided by Quantiphy library

parent 29e9f931
......@@ -13,6 +13,7 @@ REQUIREMENTS = [
"setuptools_scm >= 3.1.0",
"ply >= 3.11",
"Click == 7.0",
"quantiphy >= 2.5.0",
"PyYAML >= 3.13",
"graphviz >= 0.9",
]
......
......@@ -7,16 +7,6 @@ from zero.format import Quantity
class QuantityParserTestCase(TestCase):
"""Quantity parsing tests"""
def test_invalid(self):
# invalid characters
for test_value in r" !\"€£$%^&\*\(\)\{\}\[\];:'@#~/\?><\\\|¬`":
with self.subTest(msg="Test invalid quantity", quantity=test_value):
self.assertRaisesRegex(ValueError, r"unrecognised quantity", Quantity, test_value)
# invalid strings
self.assertRaisesRegex(ValueError, r"unrecognised quantity", Quantity, "")
self.assertRaisesRegex(ValueError, r"unrecognised quantity", Quantity, "invalid")
def test_float_values(self):
"""Test parsing of float quantities"""
self.assertAlmostEqual(Quantity(1.23), 1.23)
......@@ -27,7 +17,6 @@ class QuantityParserTestCase(TestCase):
"""Test parsing of string quantities"""
self.assertAlmostEqual(Quantity("1.23"), 1.23)
self.assertAlmostEqual(Quantity("-765e3"), -765e3)
self.assertAlmostEqual(Quantity("6.3e-2.3"), 6.3 * 10 ** -2.3)
def test_string_values_with_si_scales(self):
"""Test parsing of string quantities with SI scales"""
......@@ -53,19 +42,19 @@ class QuantityParserTestCase(TestCase):
"""Test parsing of string quantities with units and SI scales"""
q = Quantity("1.23")
self.assertAlmostEqual(q, 1.23)
self.assertEqual(q.unit, None)
self.assertEqual(q.units, "")
q = Quantity("1.23 Hz")
self.assertAlmostEqual(q, 1.23)
self.assertEqual(q.unit, "Hz")
self.assertEqual(q.units, "Hz")
q = Quantity("1.69pF")
self.assertAlmostEqual(q, 1.69e-12)
self.assertEqual(q.unit, "F")
self.assertEqual(q.units, "F")
q = Quantity("3.21uH")
self.assertAlmostEqual(q, 3.21e-6)
self.assertEqual(q.unit, "H")
self.assertEqual(q.units, "H")
q = Quantity("4.88MΩ")
self.assertAlmostEqual(q, 4.88e6)
self.assertEqual(q.unit, "Ω")
self.assertEqual(q.units, "Ω")
def test_copy(self):
"""Test quantity copy constructor"""
......@@ -76,95 +65,3 @@ class QuantityParserTestCase(TestCase):
self.assertEqual(float(q), float(Quantity(q)))
# strings equal
self.assertEqual(str(q), str(Quantity(q)))
class QuantityFormatterTestCase(TestCase):
"""Quantity formatting tests"""
def test_default_format(self):
"""Test default quantities format"""
# default precision is 4
self.assertEqual(Quantity(1.23).format(), "1.2300")
self.assertEqual(Quantity("4.56k").format(), "4.5600k")
self.assertEqual(Quantity("7.89 M").format(), "7.8900M")
self.assertEqual(Quantity("1.01 GHz").format(), "1.0100 GHz")
def test_unit_format(self):
"""Test quantities with units format"""
# SI scale and unit, default precision
self.assertEqual(Quantity("1.01 GHz").format(show_unit=True, show_si=True), "1.0100 GHz")
self.assertEqual(Quantity("1.01 nHz").format(show_unit=True, show_si=True), "1.0100 nHz")
# SI scale, but no unit, default precision
self.assertEqual(Quantity("1.01 MHz").format(show_unit=False, show_si=True), "1.0100M")
self.assertEqual(Quantity("1.01 uHz").format(show_unit=False, show_si=True), "1.0100µ")
# unit, but no SI scale, default precision
self.assertEqual(Quantity("1.01 kHz").format(show_unit=True, show_si=False), "1.0100e3 Hz")
self.assertEqual(Quantity("1.01 mHz").format(show_unit=True, show_si=False), "1.0100e-3 Hz")
# no unit nor SI scale, default precision
self.assertEqual(Quantity("1.01 THz").format(show_unit=False, show_si=False), "1.0100e12")
self.assertEqual(Quantity("1.01 pHz").format(show_unit=False, show_si=False), "1.0100e-12")
# SI scale and unit, 0 decimal places
self.assertEqual(Quantity("1.01 GHz").format(show_unit=True, show_si=True, precision=0), "1 GHz")
self.assertEqual(Quantity("1.01 nHz").format(show_unit=True, show_si=True, precision=0), "1 nHz")
# SI scale, but no unit, 1 decimal place
self.assertEqual(Quantity("1.01 GHz").format(show_unit=False, show_si=True, precision=1), "1.0G")
self.assertEqual(Quantity("1.01 nHz").format(show_unit=False, show_si=True, precision=1), "1.0n")
# unit, but no SI scale, 2 decimal places
self.assertEqual(Quantity("1.01 GHz").format(show_unit=True, show_si=False, precision=2), "1.01e9 Hz")
self.assertEqual(Quantity("1.01 nHz").format(show_unit=True, show_si=False, precision=2), "1.01e-9 Hz")
# no unit nor SI scale, 3 decimal places
self.assertEqual(Quantity("1.01 GHz").format(show_unit=False, show_si=False, precision=3), "1.010e9")
self.assertEqual(Quantity("1.01 nHz").format(show_unit=False, show_si=False, precision=3), "1.010e-9")
# with decimal place move
self.assertEqual(Quantity("12345.01 GHz").format(show_unit=False, show_si=False, precision=3), "12.35e12")
self.assertEqual(Quantity("12345.01 nHz").format(show_unit=False, show_si=False, precision=3), "12.35e-6")
self.assertEqual(Quantity("0.0012345 nHz").format(show_unit=False, show_si=False, precision=3), "1.235e-12")
# SI scale and unit, full precision
self.assertEqual(Quantity("1.01 GHz").format(show_unit=True, show_si=True, precision="full"), "1.01 GHz")
self.assertEqual(Quantity("1.01 nHz").format(show_unit=True, show_si=True, precision="full"), "1.01 nHz")
# with decimal place move
self.assertEqual(Quantity("12345.01 GHz").format(show_unit=True, show_si=True, precision="full"), "12.34501 THz")
self.assertEqual(Quantity("12345.01 nHz").format(show_unit=True, show_si=True, precision="full"), "12.34501 µHz")
# SI scale, but no unit, full precision
self.assertEqual(Quantity("12.3456 GHz").format(show_unit=False, show_si=True, precision="full"), "12.3456G")
self.assertEqual(Quantity("12.3456 nHz").format(show_unit=False, show_si=True, precision="full"), "12.3456n")
# unit, but no SI scale, full precision
self.assertEqual(Quantity("123.456789 GHz").format(show_unit=True, show_si=False, precision="full"), "123.456789e9 Hz")
self.assertEqual(Quantity("123.456789 nHz").format(show_unit=True, show_si=False, precision="full"), "123.456789e-9 Hz")
# with decimal place move
self.assertEqual(Quantity("123456.789 GHz").format(show_unit=True, show_si=False, precision="full"), "123.456789e12 Hz")
self.assertEqual(Quantity("123456.789 nHz").format(show_unit=True, show_si=False, precision="full"), "123.456789e-6 Hz")
self.assertEqual(Quantity("0.00123456789 nHz").format(show_unit=True, show_si=False, precision="full"), "1.23456789e-12 Hz")
# no unit nor SI scale, full precision
self.assertEqual(Quantity("123.4567890123 GHz").format(show_unit=False, show_si=False, precision="full"), "123.4567890123e9")
self.assertEqual(Quantity("123.4567890123 nHz").format(show_unit=False, show_si=False, precision="full"), "123.4567890123e-9")
# with decimal place move
self.assertEqual(Quantity("12345.67890123 GHz").format(show_unit=False, show_si=False, precision="full"), "12.34567890123e12")
self.assertEqual(Quantity("12345.67890123 nHz").format(show_unit=False, show_si=False, precision="full"), "12.34567890123e-6")
self.assertEqual(Quantity("0.001234567890123 nHz").format(show_unit=False, show_si=False, precision="full"), "1.234567890123e-12")
# scales below f should default to exponential notation
self.assertEqual(Quantity("0.001234567890123 fHz").format(show_unit=False, show_si=False, precision="full"), "1.234567890123e-18")
self.assertEqual(Quantity("0.001234567890123 aHz").format(show_unit=False, show_si=False, precision="full"), "1.234567890123e-21")
self.assertEqual(Quantity("0.001234567890123 zHz").format(show_unit=False, show_si=False, precision="full"), "1.234567890123e-24")
self.assertEqual(Quantity("0.001234567890123 yHz").format(show_unit=False, show_si=False, precision="full"), "1.234567890123e-27")
# scales above T should default to exponential notation
self.assertEqual(Quantity("12345.67890123 THz").format(show_unit=False, show_si=False, precision="full"), "12.34567890123e15")
self.assertEqual(Quantity("12345.67890123 PHz").format(show_unit=False, show_si=False, precision="full"), "12.34567890123e18")
self.assertEqual(Quantity("12345.67890123 EHz").format(show_unit=False, show_si=False, precision="full"), "12.34567890123e21")
self.assertEqual(Quantity("12345.67890123 ZHz").format(show_unit=False, show_si=False, precision="full"), "12.34567890123e24")
self.assertEqual(Quantity("12345.67890123 YHz").format(show_unit=False, show_si=False, precision="full"), "12.34567890123e27")
......@@ -490,7 +490,7 @@ class Inductor(PassiveComponent):
coupling_factor = self.coupling_factors[other]
mutual_inductance = coupling_factor * np.sqrt(self.inductance * other.inductance)
return Quantity(mutual_inductance, unit=self.DISPLAY_UNIT)
return Quantity(mutual_inductance, units=self.DISPLAY_UNIT)
def impedance_from(self, other, frequency):
"""Calculate the impedance this inductor has due to the specified coupled inductor
......
"""Formatting functionality for numbers with units"""
import re
import logging
LOGGER = logging.getLogger(__name__)
class Quantity(float):
"""Container for numeric values and their associated units.
Partially based on `QuantiPhy <https://github.com/KenKundert/quantiphy>`_.
Parameters
----------
value : :class:`float`, :class:`str`, :class:`Quantity`
The quantity. SI units and prefices can be specified and are recognised.
unit : :class:`str`, optional
The quantity's unit. This can be used to directly specify the unit associated with
the specified `value`.
"""
# default display precision
DEFAULT_PRECISION = 4
# input scale mappings
MAPPINGS = {
'Y': 24,
'Z': 21,
'E': 18,
'P': 15,
'T': 12,
'G': 9,
'M': 6,
'k': 3,
'c': -2, # only available for input, not used in output
'm': -3,
'u': -6,
'µ': -6,
'n': -9,
'p': -12,
'f': -15,
'a': -18,
'z': -21,
'y': -24
}
# scale factors every 3rd decade, in order from 0
LARGE_SCALES = "kMGTPEZY"
SMALL_SCALES = "mµnpfazy"
# output scale factors (only display these scales regardless of input)
OUTPUT_SCALES = "TGMkmµnpf"
# regular expression to find values with unit prefixes and units in text
VALUE_REGEX_STR = (r"^([+-]?\d*\.?\d*)" # base
r"([eE]([+-]?\d*\.?\d*))?\s*" # numeric exponent
r"([yzafpnuµmkMGTPEZY])?" # unit prefix
r"(s|A|rad|Hz|W|C|V|F|Ω|H|°C)?") # SI unit
VALUE_REGEX = re.compile(VALUE_REGEX_STR)
def __new__(cls, value, unit=None):
if isinstance(value, Quantity):
number = float(value)
mantissa = value._mantissa
scale = value._scale
if value.unit:
unit = value.unit
elif isinstance(value, str):
number, mantissa, scale, parsed_unit = cls.parse(value, unit)
if unit is not None and parsed_unit is not None and unit != parsed_unit:
LOGGER.warning("overriding detected unit '%s' with specified unit '%s'",
parsed_unit, unit)
else:
unit = parsed_unit
else:
# assume float
number = value
mantissa = None
scale = None
# create object from identified information
self = float.__new__(cls, number)
self._mantissa = mantissa
self._scale = scale
self.unit = unit
return self
@classmethod
def parse(cls, quantity, unit=None):
"""Parse quantity as a number.
Parameters
----------
quantity : :class:`str`
Value string to parse as a number.
unit : :class:`str`, optional
The quantity's unit.
Returns
-------
value : :class:`float`
The numeric representation of the quantity.
mantissa : :class:`str`
The quantity's mantissa. Provided to help avoid floating point precision
display errors.
scale : :class:`float`
The quantity's scale factor.
unit : :class:`str`
The parsed unit. If no unit is found, `None` is returned.
"""
# don't need to handle units if there aren't any
if isinstance(quantity, (int, float)):
return float(quantity), None
# find floating point numbers and optional unit prefix in string
results = re.match(cls.VALUE_REGEX, quantity)
# mantissa is first match
mantissa = results.group(1)
# scale is the fourth match
scale = results.group(4)
# unit is fifth match
unit = results.group(5)
if not mantissa:
raise ValueError(f"unrecognised quantity '{quantity}'")
# convert value to float
value = float(mantissa)
# special case: parse "1.23E" as 1.23e18
if results.group(2) == "E" and results.group(3) == "":
exponent = 18
scale = "E"
else:
# handle exponent
if results.group(3) or results.group(4):
exponent = 0
if results.group(3):
# exponent specified directly
exponent += float(results.group(3))
if results.group(4):
# exponent specified as unit prefix
exponent += cls.MAPPINGS[results.group(4)]
else:
# neither prefix nor exponent
exponent = 0
# raise value to the intended exponent
value *= 10 ** exponent
return value, mantissa, scale, unit
def format(self, show_unit=True, show_si=True, precision=None):
"""Format the specified value and unit for display.
Parameters
----------
show_unit : :class:`bool`, optional
Whether to display the quantity's unit.
show_si : :class:`bool`, optional
Whether to show quantity with scale factors using SI notation, e.g. "k" for 10e3.
If `False`, the value defaults to using standard float notation.
precision : :class:`int` or 'full', optional
Number of decimal places to display quantity with. If "full", uses the precision
of the number as originally specified. When "full" is used, but `show_unit` and
`show_si` are both `False`, then the decimal point may be moved.
Returns
-------
:class:`str`
Formatted quantity.
"""
if precision is None:
precision = self.DEFAULT_PRECISION
if precision == "full" and self._mantissa is not None:
# parsed mantissa and scale factor
mantissa = self._mantissa
scale = self._scale
# convert scale factor to integer exponent
try:
exp = int(scale)
except ValueError:
if scale:
exp = int(self.MAPPINGS[scale])
else:
exp = 0
# add decimal point to mantissa if missing
mantissa += '' if '.' in mantissa else '.'
# strip off leading zeros and break into components
whole, frac = mantissa.strip('0').split('.')
if whole == "":
# remove leading zeros from fractional part
orig_len = len(frac)
frac = frac.lstrip('0')
if frac:
whole = frac[:1]
frac = frac[1:]
exp -= orig_len - len(frac)
else:
# stripping off zeros left us with nothing, this must be 0
whole = '0'
frac = ''
exp = 0
# reconstruct the mantissa
mantissa = whole[0] + '.' + whole[1:] + frac
exp += len(whole) - 1
else:
if precision == "full":
# no parsed mantissa available; use default precision
precision = self.DEFAULT_PRECISION
# Get float value.
number = self.real
# Split number into components.
number = "%.*e" % (precision, number)
mantissa, exp = number.split("e")
exp = int(exp)
# scale factor
index = exp // 3
shift = exp % 3
scale = "e%d" % (exp - shift)
if index == 0:
scale = ''
elif show_si:
if index > 0:
if index <= len(self.LARGE_SCALES):
if self.LARGE_SCALES[index-1] in self.OUTPUT_SCALES:
scale = self.LARGE_SCALES[index-1]
else:
index = -index
if index <= len(self.SMALL_SCALES):
if self.SMALL_SCALES[index-1] in self.OUTPUT_SCALES:
scale = self.SMALL_SCALES[index-1]
# shift the decimal place as needed
sign = '-' if mantissa[0] == '-' else ''
mantissa = mantissa.lstrip('-').replace('.', '')
mantissa += (shift + 1 - len(mantissa)) * '0'
mantissa = sign + mantissa[0:(shift+1)] + '.' + mantissa[(shift+1):]
# get rid of trailing decimal points and leading + if present
mantissa = mantissa.rstrip('.')
mantissa = mantissa.lstrip('+')
if show_unit and self.unit is not None:
if scale in self.MAPPINGS:
# standard suffix
fmt_str = "{mantissa} {scale}{unit}"
else:
# scientific notation
if self.unit is not None:
fmt_str = "{mantissa}{scale} {unit}"
else:
fmt_str = "{mantissa}{scale}"
else:
fmt_str = "{mantissa}{scale}"
return fmt_str.format(mantissa=mantissa, scale=scale, unit=self.unit)
def __str__(self):
return self.format()
from quantiphy import Quantity, UnitConversion
......@@ -1467,10 +1467,10 @@ class Solution:
# absolute and relative worst indices
iworst = np.argmax(np.abs(data_a - data_b))
worst = np.abs(data_a[iworst] - data_b[iworst])
fworst = Quantity(frequencies[iworst], unit="Hz")
fworst = Quantity(frequencies[iworst], units="Hz")
irelworst = np.argmax(np.abs((data_a - data_b) / data_b))
relworst = np.abs((data_a[irelworst] - data_b[irelworst]) / data_b[irelworst])
frelworst = Quantity(frequencies[irelworst], unit="Hz")
frelworst = Quantity(frequencies[irelworst], units="Hz")
if worst != 0:
# descriptions of worst
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment