From eecd599b1b3643c5afd1e54a76a47ab42c9477f4 Mon Sep 17 00:00:00 2001 From: Christian Hill Date: Mon, 1 Aug 2022 12:30:17 +0200 Subject: [PATCH] Implement an 'undef' Units functionality --- README.rst | 56 ----- setup.py | 4 +- src/pyqn/atom_unit.py | 54 ++-- src/pyqn/base_unit.py | 445 ++++++++++++++++++++++----------- src/pyqn/dimensions.py | 35 ++- src/pyqn/list_base_units.py | 39 +-- src/pyqn/qn_array.py | 19 +- src/pyqn/quantity.py | 152 ++++++----- src/pyqn/si.py | 49 ++-- src/pyqn/symbol.py | 1 + src/pyqn/units.py | 188 ++++++++------ tests/test_conversions.py | 55 ++-- tests/test_quantity.py | 111 ++++---- tests/test_undef.py | 18 ++ tests/test_units.py | 130 +++++----- tests/test_units_collisions.py | 16 +- 16 files changed, 787 insertions(+), 585 deletions(-) create mode 100644 tests/test_undef.py diff --git a/README.rst b/README.rst index f5c6fe8..6e7ca91 100644 --- a/README.rst +++ b/README.rst @@ -10,59 +10,3 @@ transforming physical quantities and their units. Units are specified as strings using a simple and flexible syntax, and may be compared, output in different formats and manipulated using a variety of predefined Python methods. - - - -Installation: -============= - -The PyQn package can be installed either from PyPI_ using pip - -.. code-block:: bash - - python3 -m pip install pyqn - -or from the source by running (one of the two) from the project source directory. - -.. code-block:: bash - - # either - python setup.py install - - # or - python3 -m pip install . - - - -Examples: -========= - -Units ------ -The units of physical quantities are represented by the ``Units`` class. A -``Units`` object is instantiated from a valid units string and supports ions, -isotopologues, as well as a few special species. This object contains -attributes including the dimensions, HTML and LaTeX representations, and -methods for conversion to different compatible units. - -.. code-block:: pycon - - >>> from pyqn.units import Units - >>> u1 = Units('km') - - >>> u2 = Units('hr') - - >>> u3 = u1/u2 - - >>> print(u3) - km.hr-1 - - >>> u4 = Units('m/s') - - >>> u3.conversion(u4) # OK: can convert from km/hr to m/s - Out[7]: 0.2777777777777778 - - >>> u3.conversion(u2) # Oops: can't convert from km/hr to m! - ... - UnitsError: Failure in units conversion: units km.hr-1[L.T-1] and hr[T] have - different dimensions diff --git a/setup.py b/setup.py index ea1e683..f3a66c1 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( name="pyqn", - version="1.3", + version="1.3.2", description="A package for managing physical units and quantities", long_description=long_description, long_description_content_type="text/x-rst", @@ -40,7 +40,7 @@ ], extras_require={"dev": ["black", "pytest-cov", "tox", "ipython"]}, # package_data will include all the resolved globs into both the wheel and sdist - #package_data={}, + # package_data={}, # no need for MANIFEST.in, which should be reserved only for build-time files project_urls={ "Bug Reports": "https://github.com/xnx/pyqn/issues", diff --git a/src/pyqn/atom_unit.py b/src/pyqn/atom_unit.py index 6485ca5..f148149 100644 --- a/src/pyqn/atom_unit.py +++ b/src/pyqn/atom_unit.py @@ -23,39 +23,45 @@ # along with PyQn. If not, see import sys -from pyparsing import Word, Group, Literal, Suppress, ParseException, oneOf,\ - Optional +from pyparsing import Word, Group, Literal, Suppress, ParseException, oneOf, Optional from .si import si_prefixes from .base_unit import BaseUnit, base_unit_stems from .dimensions import Dimensions # pyparsing stuff for parsing unit strings: -caps = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' +caps = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" lowers = caps.lower() -letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_0' -digits = '123456789' -exponent = Word(digits + '-') +letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_0" +digits = "123456789" +exponent = Word(digits + "-") prefix = oneOf(list(si_prefixes.keys())) -ustem = Word(letters + 'Å' + 'Ω') -uatom = (Group( '1' | (Optional(prefix) + ustem)) + Optional(exponent))\ - | (Group( '1' | ustem) + Optional(exponent)) +ustem = Word(letters + "Å" + "Ω") +uatom = (Group("1" | (Optional(prefix) + ustem)) + Optional(exponent)) | ( + Group("1" | ustem) + Optional(exponent) +) # floating point equality and its negation, to some suitable tolerance -def feq(f1, f2, tol=1.e-10): - return abs(f1-f2) <= tol -def fneq(f1, f2, tol=1.e-10): +def feq(f1, f2, tol=1.0e-10): + return abs(f1 - f2) <= tol + + +def fneq(f1, f2, tol=1.0e-10): return not feq(f1, f2, tol) + class UnitsError(Exception): """ An Exception class for errors that might occur whilst manipulating units. """ + def __init__(self, error_str): self.error_str = error_str + def __str__(self): return self.error_str + class AtomUnit(object): """ AtomUnit is a class to represent a single BaseUnit, possibly with an SI @@ -64,22 +70,21 @@ class AtomUnit(object): """ def __init__(self, prefix, base_unit, exponent=1): - """ Initialize the AtomUnit object. """ + """Initialize the AtomUnit object.""" self.base_unit = base_unit self.exponent = exponent - self.dims = self.base_unit.dims ** self.exponent + self.dims = self.base_unit.dims**self.exponent # get the SI prefix (if present), and its 'factor' (10 raised to its # the power it represents - self.si_fac = 1. + self.si_fac = 1.0 self.prefix = prefix if prefix is not None: try: self.si_prefix = si_prefixes[prefix] except KeyError: - raise UnitsError('Invalid or unsupported SI prefix: %s' - % prefix) + raise UnitsError("Invalid or unsupported SI prefix: %s" % prefix) self.si_fac = self.si_prefix.fac # now calculate the factor relating this AtomUnit to its # corresponding SI unit: @@ -97,7 +102,7 @@ def parse(self, s_unit_atom): uatom_data = uatom.parseString(s_unit_atom) except ParseException: raise - raise UnitsError('Invalid unit atom syntax: %s' % s_unit_atom) + raise UnitsError("Invalid unit atom syntax: %s" % s_unit_atom) # uatom_data comes back as (([prefix], ), [exponent]) if len(uatom_data[0]) == 1: @@ -111,7 +116,7 @@ def parse(self, s_unit_atom): # properly (ie to mmHg, not to milli-mHg): if stem not in base_unit_stems: prefix = None - stem = ''.join(uatom_data[0]) + stem = "".join(uatom_data[0]) try: base_unit = base_unit_stems[stem] except: @@ -124,18 +129,18 @@ def parse(self, s_unit_atom): return AtomUnit(prefix, base_unit, exponent) def __pow__(self, power): - """ Return the current AtomUnit raised to a specified power. """ + """Return the current AtomUnit raised to a specified power.""" return AtomUnit(self.prefix, self.base_unit, self.exponent * power) def __str__(self): - """ String representation of this AtomUnit. """ - s = '' + """String representation of this AtomUnit.""" + s = "" if self.prefix: s = self.prefix - s_exponent = '' + s_exponent = "" if self.exponent != 1: s_exponent = str(self.exponent) - return ''.join([s, str(self.base_unit), s_exponent]) + return "".join([s, str(self.base_unit), s_exponent]) def __repr__(self): return str(self) @@ -150,4 +155,3 @@ def prefix_base_eq(self, other): if self.prefix == other.prefix and self.base_unit == other.base_unit: return True return False - diff --git a/src/pyqn/base_unit.py b/src/pyqn/base_unit.py index 833c4a7..e59b8f5 100644 --- a/src/pyqn/base_unit.py +++ b/src/pyqn/base_unit.py @@ -23,18 +23,18 @@ from .dimensions import * + class BaseUnit(object): """ A BaseUnit is a commonly-used single unit without a prefix, for example: m (metre, length), erg (energy), bar (pressure). BaseUnit objects have a description and know their dimensions in terms of powers of Length, - Time, Mass, etc. (described by a Dimensions object). + Time, Mass, etc. (described by a Dimensions object). """ - def __init__(self, stem, name, unit_type, fac, description, latex, - dims=None): - """ Initialize the BaseUnit object. """ + def __init__(self, stem, name, unit_type, fac, description, latex, dims=None): + """Initialize the BaseUnit object.""" # BaseUnit stem, e.g. 'm', 'g', 'bar', ... self.stem = stem # BaseUnit name, e.g. 'metre', 'gram', 'bar', ... @@ -57,154 +57,300 @@ def __eq__(self, other): return True def __str__(self): - """ String representation of the BaseUnit is just its 'stem'. """ + """String representation of the BaseUnit is just its 'stem'.""" return self.stem -base_units = ( -('Unity', [ -BaseUnit('1', 'unity', 'unity', 1., '', '1', d_dimensionless), -]), - -('SI unit stems', [ -BaseUnit('m', 'metre', 'length', 1., '', 'm', d_length), -BaseUnit('s', 'second', 'time', 1., '', 's', d_time), -BaseUnit('g', 'gram', 'mass', 1.e-3, '', 'g', d_mass), -BaseUnit('K', 'kelvin', 'temperature', 1., '', 'K', Dimensions(Theta=1)), -BaseUnit('mol', 'mole', 'amount', 1., '', 'mol', Dimensions(Q=1)), -BaseUnit('A', 'amp', 'current', 1., '', 'A', Dimensions(C=1)), -BaseUnit('cd', 'candela', 'luminous intensity', 1., '', 'cd', Dimensions(I=1)) -]), - -('Derived SI units', [ -BaseUnit('N', 'newton', 'force', 1., '', 'N', d_force), -BaseUnit('J', 'joule', 'energy', 1., '', 'J', d_energy), -BaseUnit('W', 'watt', 'power', 1., '', 'W', d_energy / d_time), -BaseUnit('Pa', 'pascal', 'pressure', 1., '', 'Pa', d_pressure), -BaseUnit('C', 'coulomb', 'charge', 1., '', 'C', d_charge), -BaseUnit('V', 'volt', 'voltage', 1., '', 'V', d_voltage), -BaseUnit('Ω', 'ohm', 'electric resistance', 1, '', r'\Omega', - d_voltage / d_current), -BaseUnit('F', 'farad', 'capacitance', 1., '', 'F', d_charge / d_voltage), -BaseUnit('Wb', 'weber', 'magnetic flux', 1, '', 'Wb', d_magnetic_flux), -BaseUnit('H', 'henry', 'inductance', 1, '', 'H', d_magnetic_flux / d_current), -BaseUnit('S', 'siemens', 'electric conductance', 1, '', 'S', - d_current / d_voltage), -BaseUnit('T', 'tesla', 'magnetic field strength', 1., '', 'T', - d_magfield_strength), -BaseUnit('Hz', 'hertz', 'cyclic frequency', 1., '', 'Hz', d_time**-1), -BaseUnit('rad', 'radian', 'angle', 1., '', 'rad', d_dimensionless), -BaseUnit('sr', 'steradian', 'solid angle', 1., '', 'sr', d_dimensionless), -]), - -('Non-SI pressure units', [ -BaseUnit('bar', 'bar', 'pressure', 1.e5, '', 'bar', d_pressure), -BaseUnit('atm', 'atmosphere', 'pressure', 1.01325e5, '', 'atm', d_pressure), -BaseUnit('Torr', 'torr', 'pressure', 133.322368, '', 'Torr', d_pressure), -# (but see e.g. Wikipedia for the precise relationship between Torr and mmHg -BaseUnit('mmHg', 'millimetres of mercury', 'pressure', 133.322368, '', 'mmHg', - d_pressure), -]), - -('cgs units', [ -BaseUnit('dyn', 'dyne', 'force', 1.e-5, '', 'dyn', d_force), -BaseUnit('erg', 'erg', 'energy', 1.e-7, '', 'erg', d_energy), -BaseUnit('k', 'kayser', 'wavenumber', 100., '', 'k', d_length**-1), -BaseUnit('D', 'debye', 'electric dipole moment', 1.e-21/299792458., '', 'D', - d_charge * d_length), -BaseUnit('hbar', 'hbar', 'angular momentum', 1.05457148e-34, '', r'\hbar', - Dimensions(L=2, M=1, T=-1)), -BaseUnit('e', 'electron charge', 'charge', 1.602176565e-19, '', 'e', d_charge), -]), - -('Angular units', [ -BaseUnit('deg', 'degree', 'angle', 0.017453292519943295, '', 'deg', - d_dimensionless), -BaseUnit('arcmin', 'arcminute', 'angle', 2.908882086657216e-4, '', 'arcmin', - d_dimensionless), -# NB we can't allow 'as' for arcseconds because of ambiguity with attoseconds -BaseUnit('asec', 'arcsecond', 'angle', 4.84813681109536e-6, '', 'asec', - d_dimensionless), -]), - -('Non-SI energy units', [ -BaseUnit('eV', 'electron volt', 'energy', 1.602176487e-19, '', 'eV', - d_energy), -BaseUnit('E_h', 'hartree', 'energy', 4.35974394e-18, '', 'E_h', d_energy), -BaseUnit('cal', 'thermodynamic calorie', 'energy', 4.184, '', 'cal', d_energy), -BaseUnit('Ry', 'rydberg', 'energy', 13.60569253 * 1.602176487e-19, '', 'Ry', - d_energy), -]), - -('Non-SI mass units', [ -BaseUnit('u', 'atomic mass unit', 'mass', 1.660538921e-27, '', 'u', d_mass), -BaseUnit('amu', 'atomic mass unit', 'mass', 1.660538921e-27, '', 'am', d_mass), -BaseUnit('Da', 'dalton', 'mass', 1.660538921e-27, '', 'Da', d_mass), -BaseUnit('m_e', 'electron mass', 'mass', 9.10938291e-31, '', 'm_e', d_mass), -]), -('Non-SI units of length, area and volume', [ -# Non-SI length units -BaseUnit('Å', 'angstrom', 'length', 1.e-10, '', r'\AA', d_length), -BaseUnit('a0', 'bohr', 'length', 5.2917721092e-11, '', 'a_0', d_length), -# Non-SI area units -BaseUnit('b', 'barn', 'area', 1.e-28, '', 'b', d_area), -# Non-SI volume units -BaseUnit('l', 'litre', 'volume', 1.e-3, '', 'l', d_volume), -BaseUnit('L', 'litre', 'volume', 1.e-3, '', 'L', d_volume), -]), - -('Non-SI time units', [ -BaseUnit('min', 'minute', 'time', 60., '', 'min', d_time), -BaseUnit('hr', 'hour', 'time', 3600., '', 'hr', d_time), -BaseUnit('h', 'hour', 'time', 3600., '', 'h', d_time), -BaseUnit('dy', 'day', 'time', 86400., '', 'd', d_time), -BaseUnit('yr', 'year', 'time', 31557600., '', 'yr', d_time), -]), - -('Astronomical units', [ -BaseUnit('AU', 'astronomical unit', 'length', 1.495978707e11, '', 'AU', - d_length), -BaseUnit('pc', 'parsec', 'length', 3.085677637634e16, '', 'pc', d_length), -BaseUnit('ly', 'light-year', 'length', 9.4607304725808e15, '', 'ly', d_length), -]), - -('Imperial, customary and US units', [ -# NB we can't use 'in' for inch because of a clash with min -BaseUnit('inch', 'inch', 'length', 0.0254, '', 'in', d_length), -BaseUnit('ft', 'foot', 'length', .3048, '', 'ft', d_length), -# NB we can't use 'yd' for yard because of a clash with yd (yoctodays!) -BaseUnit('yard', 'yard', 'length', .9144, '', 'yd', d_length), -BaseUnit('fur', 'furlong', 'length', 201.168, '', 'furlong', d_length), -BaseUnit('mi', 'mile', 'length', 1609.344, '', 'mi', d_length), -BaseUnit('gal', 'Imperial (UK) gallon', 'volume', 4.54609e-3, '', 'gal', - d_volume), -BaseUnit('pt', 'Imperial (UK) pint', 'volume', 5.6826125e-4, '', 'pt', - d_volume), -BaseUnit('USgal', 'US liquid gallon', 'volume', 3.785411783e-3, '', 'USgal', - d_volume), -BaseUnit('USpt', 'US liquid pint', 'volume', 4.73176472875e-4, '', 'USpt', - d_volume), -BaseUnit('st', 'stone', 'mass', 6.35029318, '', 'st', d_mass), -BaseUnit('lb', 'pound', 'mass', 0.45359237, '', 'lb', d_mass), -BaseUnit('oz', 'ounce', 'mass', 0.028349523125, '', 'oz', d_mass), -]), - -('Maritime units', [ -BaseUnit('NM', 'nautical mile', 'length', 1852., '', 'NM', d_length), -BaseUnit('kn', 'knot', 'speed', 1852., '', 'kn', d_length/d_time), -]), - -('Silly units', [ -BaseUnit('fir', 'fikin', 'mass', 40.8233133, '', 'fir', d_mass), -BaseUnit('ftn', 'fortnight', 'time', 1.2096e6, '', 'ftn', d_time), -]), - -('Miscellaneous units', [ -BaseUnit('Td', 'townsend', 'reduced electric field', 1.e-21, '', 'Td', - d_voltage * d_area), -BaseUnit('Jy', 'jansky', 'spectral flux density', 1.e-26, '', 'Jy', - d_energy / d_area) # W.m-2.s-1 -]), +base_units = ( + ( + "Unity", + [ + BaseUnit("1", "unity", "unity", 1.0, "", "1", d_dimensionless), + ], + ), + ( + "SI unit stems", + [ + BaseUnit("m", "metre", "length", 1.0, "", "m", d_length), + BaseUnit("s", "second", "time", 1.0, "", "s", d_time), + BaseUnit("g", "gram", "mass", 1.0e-3, "", "g", d_mass), + BaseUnit("K", "kelvin", "temperature", 1.0, "", "K", Dimensions(Theta=1)), + BaseUnit("mol", "mole", "amount", 1.0, "", "mol", Dimensions(Q=1)), + BaseUnit("A", "amp", "current", 1.0, "", "A", Dimensions(C=1)), + BaseUnit( + "cd", "candela", "luminous intensity", 1.0, "", "cd", Dimensions(I=1) + ), + ], + ), + ( + "Derived SI units", + [ + BaseUnit("N", "newton", "force", 1.0, "", "N", d_force), + BaseUnit("J", "joule", "energy", 1.0, "", "J", d_energy), + BaseUnit("W", "watt", "power", 1.0, "", "W", d_energy / d_time), + BaseUnit("Pa", "pascal", "pressure", 1.0, "", "Pa", d_pressure), + BaseUnit("C", "coulomb", "charge", 1.0, "", "C", d_charge), + BaseUnit("V", "volt", "voltage", 1.0, "", "V", d_voltage), + BaseUnit( + "Ω", + "ohm", + "electric resistance", + 1, + "", + r"\Omega", + d_voltage / d_current, + ), + BaseUnit("F", "farad", "capacitance", 1.0, "", "F", d_charge / d_voltage), + BaseUnit("Wb", "weber", "magnetic flux", 1, "", "Wb", d_magnetic_flux), + BaseUnit( + "H", "henry", "inductance", 1, "", "H", d_magnetic_flux / d_current + ), + BaseUnit( + "S", + "siemens", + "electric conductance", + 1, + "", + "S", + d_current / d_voltage, + ), + BaseUnit( + "T", + "tesla", + "magnetic field strength", + 1.0, + "", + "T", + d_magfield_strength, + ), + BaseUnit("Hz", "hertz", "cyclic frequency", 1.0, "", "Hz", d_time**-1), + BaseUnit("rad", "radian", "angle", 1.0, "", "rad", d_dimensionless), + BaseUnit("sr", "steradian", "solid angle", 1.0, "", "sr", d_dimensionless), + ], + ), + ( + "Non-SI pressure units", + [ + BaseUnit("bar", "bar", "pressure", 1.0e5, "", "bar", d_pressure), + BaseUnit("atm", "atmosphere", "pressure", 1.01325e5, "", "atm", d_pressure), + BaseUnit("Torr", "torr", "pressure", 133.322368, "", "Torr", d_pressure), + # (but see e.g. Wikipedia for the precise relationship between Torr and mmHg + BaseUnit( + "mmHg", + "millimetres of mercury", + "pressure", + 133.322368, + "", + "mmHg", + d_pressure, + ), + ], + ), + ( + "cgs units", + [ + BaseUnit("dyn", "dyne", "force", 1.0e-5, "", "dyn", d_force), + BaseUnit("erg", "erg", "energy", 1.0e-7, "", "erg", d_energy), + BaseUnit("k", "kayser", "wavenumber", 100.0, "", "k", d_length**-1), + BaseUnit( + "D", + "debye", + "electric dipole moment", + 1.0e-21 / 299792458.0, + "", + "D", + d_charge * d_length, + ), + BaseUnit( + "hbar", + "hbar", + "angular momentum", + 1.05457148e-34, + "", + r"\hbar", + Dimensions(L=2, M=1, T=-1), + ), + BaseUnit( + "e", "electron charge", "charge", 1.602176565e-19, "", "e", d_charge + ), + ], + ), + ( + "Angular units", + [ + BaseUnit( + "deg", + "degree", + "angle", + 0.017453292519943295, + "", + "deg", + d_dimensionless, + ), + BaseUnit( + "arcmin", + "arcminute", + "angle", + 2.908882086657216e-4, + "", + "arcmin", + d_dimensionless, + ), + # NB we can't allow 'as' for arcseconds because of ambiguity with attoseconds + BaseUnit( + "asec", + "arcsecond", + "angle", + 4.84813681109536e-6, + "", + "asec", + d_dimensionless, + ), + ], + ), + ( + "Non-SI energy units", + [ + BaseUnit( + "eV", "electron volt", "energy", 1.602176487e-19, "", "eV", d_energy + ), + BaseUnit("E_h", "hartree", "energy", 4.35974394e-18, "", "E_h", d_energy), + BaseUnit( + "cal", "thermodynamic calorie", "energy", 4.184, "", "cal", d_energy + ), + BaseUnit( + "Ry", + "rydberg", + "energy", + 13.60569253 * 1.602176487e-19, + "", + "Ry", + d_energy, + ), + ], + ), + ( + "Non-SI mass units", + [ + BaseUnit("u", "atomic mass unit", "mass", 1.660538921e-27, "", "u", d_mass), + BaseUnit( + "amu", "atomic mass unit", "mass", 1.660538921e-27, "", "am", d_mass + ), + BaseUnit("Da", "dalton", "mass", 1.660538921e-27, "", "Da", d_mass), + BaseUnit("m_e", "electron mass", "mass", 9.10938291e-31, "", "m_e", d_mass), + ], + ), + ( + "Non-SI units of length, area and volume", + [ + # Non-SI length units + BaseUnit("Å", "angstrom", "length", 1.0e-10, "", r"\AA", d_length), + BaseUnit("a0", "bohr", "length", 5.2917721092e-11, "", "a_0", d_length), + # Non-SI area units + BaseUnit("b", "barn", "area", 1.0e-28, "", "b", d_area), + # Non-SI volume units + BaseUnit("l", "litre", "volume", 1.0e-3, "", "l", d_volume), + BaseUnit("L", "litre", "volume", 1.0e-3, "", "L", d_volume), + ], + ), + ( + "Non-SI time units", + [ + BaseUnit("min", "minute", "time", 60.0, "", "min", d_time), + BaseUnit("hr", "hour", "time", 3600.0, "", "hr", d_time), + BaseUnit("h", "hour", "time", 3600.0, "", "h", d_time), + BaseUnit("dy", "day", "time", 86400.0, "", "d", d_time), + BaseUnit("yr", "year", "time", 31557600.0, "", "yr", d_time), + ], + ), + ( + "Astronomical units", + [ + BaseUnit( + "AU", "astronomical unit", "length", 1.495978707e11, "", "AU", d_length + ), + BaseUnit("pc", "parsec", "length", 3.085677637634e16, "", "pc", d_length), + BaseUnit( + "ly", "light-year", "length", 9.4607304725808e15, "", "ly", d_length + ), + ], + ), + ( + "Imperial, customary and US units", + [ + # NB we can't use 'in' for inch because of a clash with min + BaseUnit("inch", "inch", "length", 0.0254, "", "in", d_length), + BaseUnit("ft", "foot", "length", 0.3048, "", "ft", d_length), + # NB we can't use 'yd' for yard because of a clash with yd (yoctodays!) + BaseUnit("yard", "yard", "length", 0.9144, "", "yd", d_length), + BaseUnit("fur", "furlong", "length", 201.168, "", "furlong", d_length), + BaseUnit("mi", "mile", "length", 1609.344, "", "mi", d_length), + BaseUnit( + "gal", "Imperial (UK) gallon", "volume", 4.54609e-3, "", "gal", d_volume + ), + BaseUnit( + "pt", "Imperial (UK) pint", "volume", 5.6826125e-4, "", "pt", d_volume + ), + BaseUnit( + "USgal", + "US liquid gallon", + "volume", + 3.785411783e-3, + "", + "USgal", + d_volume, + ), + BaseUnit( + "USpt", + "US liquid pint", + "volume", + 4.73176472875e-4, + "", + "USpt", + d_volume, + ), + BaseUnit("st", "stone", "mass", 6.35029318, "", "st", d_mass), + BaseUnit("lb", "pound", "mass", 0.45359237, "", "lb", d_mass), + BaseUnit("oz", "ounce", "mass", 0.028349523125, "", "oz", d_mass), + ], + ), + ( + "Maritime units", + [ + BaseUnit("NM", "nautical mile", "length", 1852.0, "", "NM", d_length), + BaseUnit("kn", "knot", "speed", 1852.0, "", "kn", d_length / d_time), + ], + ), + ( + "Silly units", + [ + BaseUnit("fir", "fikin", "mass", 40.8233133, "", "fir", d_mass), + BaseUnit("ftn", "fortnight", "time", 1.2096e6, "", "ftn", d_time), + ], + ), + ( + "Miscellaneous units", + [ + BaseUnit( + "Td", + "townsend", + "reduced electric field", + 1.0e-21, + "", + "Td", + d_voltage * d_area, + ), + BaseUnit( + "Jy", + "jansky", + "spectral flux density", + 1.0e-26, + "", + "Jy", + d_energy / d_area, + ), # W.m-2.s-1 + ], + ), ) # create a dictionary mapping the BaseUnit stems (as keys) to the BaseUnit @@ -212,4 +358,3 @@ def __str__(self): for base_unit_group in base_units: for base_unit in base_unit_group[1]: base_unit_stems[base_unit.stem] = base_unit - diff --git a/src/pyqn/dimensions.py b/src/pyqn/dimensions.py index 168c250..ecaf6fb 100644 --- a/src/pyqn/dimensions.py +++ b/src/pyqn/dimensions.py @@ -22,24 +22,32 @@ # You should have received a copy of the GNU General Public License # along with PyQn. If not, see + class Dimensions(object): # these are the abbreviations for Length, Mass, Time, Temperature, - # Quantity (amount of substance), Current, and Luminous Intensity: - dim_names = ['L', 'M', 'T', 'Theta', 'Q', 'C', 'I'] - dim_desc = ['length', 'mass', 'time', 'temperature', 'amount', - 'current', 'luminous intensity'] + # Quantity (amount of substance), Current, and Luminous Intensity: + dim_names = ["L", "M", "T", "Theta", "Q", "C", "I"] + dim_desc = [ + "length", + "mass", + "time", + "temperature", + "amount", + "current", + "luminous intensity", + ] dim_index = {} for i, dim_name in enumerate(dim_names): dim_index[dim_name] = i def __init__(self, dims=None, **kwargs): - self.dims = [0]*7 + self.dims = [0] * 7 if dims: # initialize by dims array if not kwargs: self.dims = dims else: - print('bad initialisation of Dimensions object') + print("bad initialisation of Dimensions object") sys.exit(1) else: # initialize by keyword arguments @@ -70,11 +78,11 @@ def __str__(self): if self.dims[i] != 0: this_s_dim = dim_name if self.dims[i] != 1: - this_s_dim += '%d' % self.dims[i] + this_s_dim += "%d" % self.dims[i] s_dims.append(this_s_dim) if len(s_dims) == 0: - return '[dimensionless]' - return '.'.join(s_dims) + return "[dimensionless]" + return ".".join(s_dims) def __repr__(self): return str(self.dims) @@ -88,6 +96,7 @@ def __eq__(self, other): def __ne__(self, other): return not self.__eq__(other) + d_dimensionless = Dimensions() d_quantity = Dimensions(Q=1) d_frequency = Dimensions(T=-1) @@ -101,7 +110,7 @@ def __ne__(self, other): d_pressure = d_force / d_area d_current = Dimensions(C=1) d_charge = d_current * Dimensions(T=1) -d_voltage = d_energy / d_charge # 1 V = 1 J/C -d_magfield_strength = d_voltage * d_time / d_area # 1 T = 1 V.s/m^2 -d_magnetic_flux = d_voltage * d_time # 1 Wb = 1 V.s -d_temperature = Dimensions(Theta=1) \ No newline at end of file +d_voltage = d_energy / d_charge # 1 V = 1 J/C +d_magfield_strength = d_voltage * d_time / d_area # 1 T = 1 V.s/m^2 +d_magnetic_flux = d_voltage * d_time # 1 Wb = 1 V.s +d_temperature = Dimensions(Theta=1) diff --git a/src/pyqn/list_base_units.py b/src/pyqn/list_base_units.py index 5ee2108..cab1cfd 100644 --- a/src/pyqn/list_base_units.py +++ b/src/pyqn/list_base_units.py @@ -2,40 +2,47 @@ import sys from base_unit import base_units + def list_units_text(): for group_name, base_unit_group in base_units: print() print(group_name) - print('='*len(group_name)) - print('{0:6s} {1:30s} {2:20s}'.format('Unit', 'Name', 'Dimensions')) - print('{0:6s} {1:30s} {2:20s}'.format('----', '----', '----------')) + print("=" * len(group_name)) + print("{0:6s} {1:30s} {2:20s}".format("Unit", "Name", "Dimensions")) + print("{0:6s} {1:30s} {2:20s}".format("----", "----", "----------")) for base_unit in base_unit_group: - print('{0:6s} {1:30s} {2:20s}'.format(unicode(base_unit.stem), - base_unit.name, base_unit.dims)) + print( + "{0:6s} {1:30s} {2:20s}".format( + unicode(base_unit.stem), base_unit.name, base_unit.dims + ) + ) + def list_units_html(): for group_name, base_unit_group in base_units: - print('

{0}

'.format(group_name)) + print("

{0}

".format(group_name)) print('') - print('') + print("") for base_unit in base_unit_group: # NB we need to encode('utf-8') to pipe in that encoding to the # shell, e.g. to create a file - print('' - .format(unicode(base_unit.stem), base_unit.name, - base_unit.dims).encode('utf-8')) - print('
UnitNameDimensions
UnitNameDimensions
{0:s}{1:s}{2:s}
') - + print( + "{0:s}{1:s}{2:s}".format( + unicode(base_unit.stem), base_unit.name, base_unit.dims + ).encode("utf-8") + ) + print("") + try: list_type = sys.argv[1] - assert list_type in ('text', 'html') + assert list_type in ("text", "html") except (IndexError, AssertionError): - print('usage:\n{0} '.format(sys.argv[0])) - print('where is text or html') + print("usage:\n{0} ".format(sys.argv[0])) + print("where is text or html") sys.exit(1) -if list_type == 'text': +if list_type == "text": list_units_text() else: list_units_html() diff --git a/src/pyqn/qn_array.py b/src/pyqn/qn_array.py index 7e4cbdb..d832b61 100644 --- a/src/pyqn/qn_array.py +++ b/src/pyqn/qn_array.py @@ -1,19 +1,30 @@ from .symbol import Symbol import numpy as np + class qnArrayError(Exception): def __init__(self, error_str): self.error_str = error_str + def __str__(self): return self.error_str + class qnArray(Symbol): - def __init__(self, name=None, latex=None, html=None, values=None, - units=None, sd=None, definition=None): + def __init__( + self, + name=None, + latex=None, + html=None, + values=None, + units=None, + sd=None, + definition=None, + ): Symbol.__init__(self, name, latex, html, definition) - if type(values)==list: + if type(values) == list: self.values = np.array(values) - elif type(values)==numpy.ndarray: + elif type(values) == numpy.ndarray: self.values = values else: raise qnArrayError diff --git a/src/pyqn/quantity.py b/src/pyqn/quantity.py index 077a1ac..e913d91 100644 --- a/src/pyqn/quantity.py +++ b/src/pyqn/quantity.py @@ -22,21 +22,26 @@ import re import math -#import numpy as np + +# import numpy as np from .symbol import Symbol from .units import Units, UnitsError + class QuantityError(Exception): """ An Exception class for errors that might occur whilst manipulating Quantity objects. """ + def __init__(self, error_str): self.error_str = error_str + def __str__(self): return self.error_str + class Quantity(Symbol): """ A Python class representing a physical quantity. This class extends the @@ -47,8 +52,16 @@ class Quantity(Symbol): """ - def __init__(self, name=None, latex=None, html=None, value=None, - units=None, sd=None, definition=None): + def __init__( + self, + name=None, + latex=None, + html=None, + value=None, + units=None, + sd=None, + definition=None, + ): """ Initialize the Quantity object: set up its name as for the base, Symbol class, and set the quantity's value, sd and units (which @@ -66,16 +79,16 @@ def __init__(self, name=None, latex=None, html=None, value=None, self.units = Units(units) def __str__(self): - """ A simple string representation of the Quantity. """ + """A simple string representation of the Quantity.""" if self.name: - return '%s = %s %s' % (self.name, self.value, self.units) + return "%s = %s %s" % (self.name, self.value, self.units) else: - return '%s %s' % (self.value, self.units) - + return "%s %s" % (self.value, self.units) + __repr__ = __str__ - def value_as_str(self, nsd_digits=2, small=1.e-3, large=1.e5): + def value_as_str(self, nsd_digits=2, small=1.0e-3, large=1.0e5): """ Return a string representation of the parameter and its standard deviation in the conventional format used in @@ -84,13 +97,13 @@ def value_as_str(self, nsd_digits=2, small=1.e-3, large=1.e5): 0.04857623(71) for 4.857623e-2 +/- 7.1e-7 -0.412(40) for -0.412 +/- 0.04 1.7324(38)e7 for 17324000 +/- 380 - + Notes: This routine has not been rigorously tested, because I wrote it at 1am. Therefore, it would be a good idea to output the raw parameter values and standard deviations somewhere as well as using the output from this routine... - + Arguments: nsd_digits: the number of digits in the standard deviation (the default is 2) @@ -98,20 +111,20 @@ def value_as_str(self, nsd_digits=2, small=1.e-3, large=1.e5): scientific notation (e) large: parameter values larger than large are output in scientific notation (e-) - + """ N, sd = self.value, self.sd - + if not sd: return str(N) absN = abs(N) - if (absN < small or absN > large) and absN != 0.: + if (absN < small or absN > large) and absN != 0.0: # scientific notation power = int(math.floor(math.log(absN, 10))) N *= pow(10, -power) sd *= pow(10, -power) - dp = int(-math.log(sd,10))+nsd_digits + dp = int(-math.log(sd, 10)) + nsd_digits if dp < 0: dp = 0 sd_digits = int(round(sd * pow(10, dp))) @@ -123,7 +136,7 @@ def value_as_str(self, nsd_digits=2, small=1.e-3, large=1.e5): if dp < 0: dp = 0 sd_digits = int(round(sd * pow(10, dp))) - fmt = "%." + str(dp) + "f(" + str(sd_digits) + ')' + fmt = "%." + str(dp) + "f(" + str(sd_digits) + ")" return fmt % N @@ -136,14 +149,14 @@ def as_str(self, b_name=True, b_sd=True, b_units=True): s = [] if b_name and self.name: - s.append('%s = ' % self.name) + s.append("%s = " % self.name) if b_sd: s.append(self.value_as_str(b_sd)) else: s.append(str(self.value)) if b_units and self.units.has_units(): - s.append(' %s' % str(self.units)) - return ''.join(s) + s.append(" %s" % str(self.units)) + return "".join(s) def convert_units_to(self, new_units, force=None): """ @@ -158,30 +171,30 @@ def convert_units_to(self, new_units, force=None): to_units = Units(new_units) fac = self.units.conversion(to_units, force) -# self.value *= fac -# if self.sd is not None: -# self.sd *= fac -# self.units = to_units + # self.value *= fac + # if self.sd is not None: + # self.sd *= fac + # self.units = to_units if self.sd is not None: - return Quantity(value = self.value*fac, units = new_units, sd = self.sd*fac) + return Quantity(value=self.value * fac, units=new_units, sd=self.sd * fac) else: - return Quantity(value = self.value*fac, units = new_units) - -# def draw_from_dist(self, shape=None): -# """ -# Return a value or number array of values drawn from the normal -# distribution described by this Quantity's mean and standard -# deviation. shape is the shape of the NumPy array to return, or -# None (the default) to return a single scalar value from the -# distribution. -# -# """ -# -# if self.sd is None: -# raise ValueError('Quantity instance {} has no defined standard' -# ' deviation.'.format(self.name)) -# -# return np.random.normal(loc=self.value, scale=self.sd, size=shape) + return Quantity(value=self.value * fac, units=new_units) + + # def draw_from_dist(self, shape=None): + # """ + # Return a value or number array of values drawn from the normal + # distribution described by this Quantity's mean and standard + # deviation. shape is the shape of the NumPy array to return, or + # None (the default) to return a single scalar value from the + # distribution. + # + # """ + # + # if self.sd is None: + # raise ValueError('Quantity instance {} has no defined standard' + # ' deviation.'.format(self.name)) + # + # return np.random.normal(loc=self.value, scale=self.sd, size=shape) def __add__(self, other): """ @@ -195,13 +208,15 @@ def __add__(self, other): if self.value is None or other.value is None: raise ValueError if self.units != other.units: - raise UnitsError('Can\'t add two quantities with different' - ' units: %s and %s' % (self.units, other.units)) + raise UnitsError( + "Can't add two quantities with different" + " units: %s and %s" % (self.units, other.units) + ) if self.sd is None or other.sd is None: sd = None else: sd = math.hypot(self.sd, other.sd) - return Quantity(value=self.value+other.value, units=self.units, sd=sd) + return Quantity(value=self.value + other.value, units=self.units, sd=sd) def __sub__(self, other): """ @@ -215,13 +230,15 @@ def __sub__(self, other): if self.value is None or other.value is None: raise ValueError if self.units != other.units: - raise UnitsError('Can\'t subtract two quantities with different' - ' units: %s and %s' % (self.units, other.units)) + raise UnitsError( + "Can't subtract two quantities with different" + " units: %s and %s" % (self.units, other.units) + ) if self.sd is None or other.sd is None: sd = None else: sd = math.hypot(self.sd, other.sd) - return Quantity(value=self.value-other.value, units=self.units, sd=sd) + return Quantity(value=self.value - other.value, units=self.units, sd=sd) def __mul__(self, other): """ @@ -237,7 +254,7 @@ def __mul__(self, other): sd = None else: sd = abs(other) * self.sd - return Quantity(value=self.value*other, units=self.unit, sd=sd) + return Quantity(value=self.value * other, units=self.unit, sd=sd) else: if type(other) != Quantity: raise TypeError @@ -247,8 +264,7 @@ def __mul__(self, other): if not self.sd or not other.sd: sd = None else: - sd = value * math.hypot(self.sd/self.value, - other.sd/other.value) + sd = value * math.hypot(self.sd / self.value, other.sd / other.value) units = self.units * other.units return Quantity(value=value, units=units, sd=sd) @@ -266,7 +282,7 @@ def __truediv__(self, other): sd = None else: sd = abs(other) / self.sd - return Quantity(value=self.value/other, units=self.units, sd=sd) + return Quantity(value=self.value / other, units=self.units, sd=sd) else: if type(other) != Quantity: raise TypeError @@ -276,54 +292,52 @@ def __truediv__(self, other): if not self.sd or not other.sd: sd = None else: - sd = value * math.hypot(self.sd/self.value, - other.sd/other.value) + sd = value * math.hypot(self.sd / self.value, other.sd / other.value) units = self.units / other.units return Quantity(value=value, units=units, sd=sd) def __rtruediv__(self, other): return self.__truediv__(other) - + def __pow__(self, power): - return Quantity(value = self.value**power, - units = self.units**power) + return Quantity(value=self.value**power, units=self.units**power) @classmethod - def parse(self, s_quantity, name=None, units=None, sd=None, - definition=None): + def parse(self, s_quantity, name=None, units=None, sd=None, definition=None): s_quantity = s_quantity.strip() - if '=' in s_quantity: - fields = s_quantity.split('=') + if "=" in s_quantity: + fields = s_quantity.split("=") s_name = fields[0].strip() if s_name: name = s_name s_quantity = fields[1].strip() - fields = s_quantity.split(' ') + fields = s_quantity.split(" ") s_valsd = fields[0] if len(fields) == 2: units = Units(fields[1]) # force lower case, and replace Fortran-style 'D'/'d' exponents s_valsd = s_valsd.lower() - s_valsd = s_valsd.replace('d','e') - if 'e' in s_valsd: - s_mantsd, s_exp = s_valsd.split('e') + s_valsd = s_valsd.replace("d", "e") + if "e" in s_valsd: + s_mantsd, s_exp = s_valsd.split("e") exp = int(s_exp) else: s_mantsd = s_valsd exp = 0 - patt = r'([+-]?\d*\.?\d*)\(?(\d+)?\)?' + patt = r"([+-]?\d*\.?\d*)\(?(\d+)?\)?" m = re.match(patt, s_mantsd) if not m: - raise QuantityError('Failed to parse string into quantity:\n'\ - '%s' % s_mantsd) + raise QuantityError( + "Failed to parse string into quantity:\n" "%s" % s_mantsd + ) s_mantissa, s_sd = m.groups() mantissa = float(s_mantissa) value = mantissa * 10**exp sd = None if s_sd: - if '.' in s_mantissa: - ndp = len(s_mantissa) - s_mantissa.index('.') - 1 + if "." in s_mantissa: + ndp = len(s_mantissa) - s_mantissa.index(".") - 1 else: ndp = 0 - sd = float(s_sd) * 10**(exp-ndp) + sd = float(s_sd) * 10 ** (exp - ndp) return Quantity(name=name, value=value, units=units, sd=sd) diff --git a/src/pyqn/si.py b/src/pyqn/si.py index 16f8db2..b88ed93 100644 --- a/src/pyqn/si.py +++ b/src/pyqn/si.py @@ -1,4 +1,4 @@ - # si.py +# si.py # A class representing the SI prefixes (SIPrefix) and a list of the SI # base units (si_unit_stems): length (L), mass (M), time (T), temperature # (Theta), amount of substance (Q), current (C) and luminous intensity (I). @@ -22,36 +22,39 @@ # You should have received a copy of the GNU General Public License # along with PyQn. If not, see + class SIPrefix(object): - """ A little class describing SI prefixes. """ + """A little class describing SI prefixes.""" + def __init__(self, prefix, name, power): self.prefix = prefix self.name = name self.power = power self.fac = 10**power + # Here are the SI prefixes that we recognise. -si_prefixes = { 'y': SIPrefix('y', 'yocto', -24), - 'z': SIPrefix('z', 'zepto', -21), - 'a': SIPrefix('a', 'atto', -18), - 'f': SIPrefix('f', 'femto', -15), - 'p': SIPrefix('p', 'pico', -12), - 'n': SIPrefix('n', 'nano', -9), - 'μ': SIPrefix('μ', 'micro', -6), - 'm': SIPrefix('m', 'milli', -3), - 'c': SIPrefix('c', 'centi', -2), - 'd': SIPrefix('d', 'deci', -1), - 'k': SIPrefix('k', 'kilo', 3), - 'M': SIPrefix('M', 'mega', 6), - 'G': SIPrefix('G', 'giga', 9), - 'T': SIPrefix('T', 'tera', 12), - 'P': SIPrefix('P', 'peta', 15), - 'E': SIPrefix('E', 'exa', 18), - 'Z': SIPrefix('Z', 'zetta', 21), - 'Y': SIPrefix('Y', 'yotta', 24), - } +si_prefixes = { + "y": SIPrefix("y", "yocto", -24), + "z": SIPrefix("z", "zepto", -21), + "a": SIPrefix("a", "atto", -18), + "f": SIPrefix("f", "femto", -15), + "p": SIPrefix("p", "pico", -12), + "n": SIPrefix("n", "nano", -9), + "μ": SIPrefix("μ", "micro", -6), + "m": SIPrefix("m", "milli", -3), + "c": SIPrefix("c", "centi", -2), + "d": SIPrefix("d", "deci", -1), + "k": SIPrefix("k", "kilo", 3), + "M": SIPrefix("M", "mega", 6), + "G": SIPrefix("G", "giga", 9), + "T": SIPrefix("T", "tera", 12), + "P": SIPrefix("P", "peta", 15), + "E": SIPrefix("E", "exa", 18), + "Z": SIPrefix("Z", "zetta", 21), + "Y": SIPrefix("Y", "yotta", 24), +} # The base SI unit stems for length, time, mass, amount of substance, # thermodynamic temperature, luminous intenstiy and current respectively: -si_unit_stems = ('m', 's', 'g', 'mol', 'K', 'cd', 'A') - +si_unit_stems = ("m", "s", "g", "mol", "K", "cd", "A") diff --git a/src/pyqn/symbol.py b/src/pyqn/symbol.py index 4b7115e..648d487 100644 --- a/src/pyqn/symbol.py +++ b/src/pyqn/symbol.py @@ -21,6 +21,7 @@ # You should have received a copy of the GNU General Public License # along with PyQn. If not, see + class Symbol(object): """ A Python class representing a symbol - typically the label for a physical diff --git a/src/pyqn/units.py b/src/pyqn/units.py index 4228dcc..bb094c6 100644 --- a/src/pyqn/units.py +++ b/src/pyqn/units.py @@ -22,13 +22,13 @@ import copy from .dimensions import Dimensions -from .dimensions import (d_dimensionless, d_length, d_energy, d_time, - d_temperature) +from .dimensions import d_dimensionless, d_length, d_energy, d_time, d_temperature from .atom_unit import AtomUnit, UnitsError, feq -h, NA, c, kB = (6.62607015e-34, 6.02214076e+23, 299792458.0, 1.380649e-23) +h, NA, c, kB = (6.62607015e-34, 6.02214076e23, 299792458.0, 1.380649e-23) -class Units(object): + +class Units: """ A class to represent the units of a physical quantity. @@ -41,15 +41,26 @@ def __init__(self, units): """ + self.undef = False + self.dims = None + if type(units) is Units: + if units.undef: + self.undef = True + return self.__init__(units.atom_units) elif type(units) is str: + if units == "undef": + self.undef = True + return self.__init__(self.parse(units).atom_units) elif type(units) is list: self.atom_units = copy.deepcopy(units) else: - raise TypeError('Attempt to initialize Units object with' - ' argument units of type %s' % type(units)) + raise TypeError( + "Attempt to initialize Units object with" + " argument units of type %s" % type(units) + ) # also get the dimensions of the units self.dims = self.get_dims() @@ -78,7 +89,7 @@ def parse(self, s_compoundunit): """ - div_fields = s_compoundunit.split('/') + div_fields = s_compoundunit.split("/") ndiv_fields = len(div_fields) compound_unit = Units.parse_mult_units(div_fields[0]) for div_field in div_fields[1:]: @@ -93,9 +104,9 @@ def parse_mult_units(self, munit): """ atom_units = [] - for s_unit in munit.split('.'): + for s_unit in munit.split("."): atom_unit = AtomUnit.parse(s_unit) - if atom_unit.base_unit.stem != '1': + if atom_unit.base_unit.stem != "1": # the unity 'unit' is not really a unit atom_units.append(atom_unit) return Units(atom_units) @@ -107,13 +118,13 @@ def _find_atom(self, atom_unit): """ - for i,my_atom_unit in enumerate(self.atom_units): + for i, my_atom_unit in enumerate(self.atom_units): if my_atom_unit.prefix_base_eq(atom_unit): return i return None def __mul__(self, other): - """ Return the product of this Units object with another. """ + """Return the product of this Units object with another.""" if other == 1: return copy.deepcopy(self) @@ -137,19 +148,20 @@ def __mul__(self, other): del product.atom_units[i] product.dims = product.get_dims() return product + def __rmul__(self, other): if type(other) == str: other = Units(other) elif other == 1: - other = Units('1') + other = Units("1") return self.__mul__(other) def __truediv__(self, other): - """ Return the ratio of this Units divided by another. """ + """Return the ratio of this Units divided by another.""" if type(other) == str: other = Units(other) elif other == 1: - other = Units('1') + other = Units("1") ratio = Units(self.atom_units) for other_atom_unit in other.atom_units: i = ratio._find_atom(other_atom_unit) @@ -166,37 +178,48 @@ def __truediv__(self, other): del ratio.atom_units[i] ratio.dims = ratio.get_dims() return ratio - + def __rdiv__(self, other): if type(other) == str: other = Units(other) elif other == 1: - other = Units('1') + other = Units("1") return other.__truediv__(self) - + def __pow__(self, power): result_atom_units = [] for atom_unit in self.atom_units: result_atom_units.append(atom_unit**power) return Units(result_atom_units) - - def __str__(self): - """ String representation of this Units. """ - return '.'.join([str(atom_unit) for atom_unit in self.atom_units]) - __repr__ = __str__ + def __repr__(self): + """String representation of this Units.""" + if self.undef: + return "undef" + return ".".join([str(atom_unit) for atom_unit in self.atom_units]) + + __str__ = __repr__ def __eq__(self, other): - """ Test for equality with another Units object. """ + """Test for equality with another Units object.""" + if other is None: return False - elif other == 1: + + if self.undef: + return False + + if other == 1: return self.get_dims() == d_dimensionless + + if other.undef: + return False + if self.get_dims() != other.get_dims(): - # obviously the units aren't the same if they have different dimensions + # obviously the units aren't the same if they have different dimensions return False # if two Units objects have the same dimensions, they are equal if - # their conversion factors to SI units are the same: + # their conversion factors to SI units are the same: if feq(self.to_si(), other.to_si()): return True return False @@ -205,38 +228,42 @@ def __ne__(self, other): return not self == other def to_si(self): - """ Return the factor needed to convert this Units to SI. """ - fac = 1. + """Return the factor needed to convert this Units to SI.""" + fac = 1.0 for atom_unit in self.atom_units: fac *= atom_unit.si_fac return fac @property def html(self): + if self.undef: + return "undef" + h = [] n = len(self.atom_units) - for i,atom_unit in enumerate(self.atom_units): - h.extend([atom_unit.prefix or '', atom_unit.base_unit.stem]) + for i, atom_unit in enumerate(self.atom_units): + h.extend([atom_unit.prefix or "", atom_unit.base_unit.stem]) if atom_unit.exponent != 1: - h.append('{:d}'.format(atom_unit.exponent)) - if i < n-1: - h.append(' ') - return ''.join(h) - + h.append("{:d}".format(atom_unit.exponent)) + if i < n - 1: + h.append(" ") + return "".join(h) @property def latex(self): + if self.undef: + return r"\mathrm{undef}" + e = [] n = len(self.atom_units) for i, atom_unit in enumerate(self.atom_units): # TODO use proper LaTeX for prefix. - e.extend([atom_unit.prefix or '', atom_unit.base_unit.latex]) + e.extend([atom_unit.prefix or "", atom_unit.base_unit.latex]) if atom_unit.exponent != 1: - e.append('^{' + str(atom_unit.exponent) + '}') - if i < n-1: - e.append(r'\,') - return r'\mathrm{' + ''.join(e) + '}' - + e.append("^{" + str(atom_unit.exponent) + "}") + if i < n - 1: + e.append(r"\,") + return r"\mathrm{" + "".join(e) + "}" def conversion(self, other, force=None, strict=False): """ @@ -246,82 +273,93 @@ def conversion(self, other, force=None, strict=False): force='spec': spectroscopic units: conversions between [L-1], [M,L2,T-2], [T-1] are allowed (i.e. cm-1, s-1, and J can be interconverted through factors of h, hc). - + """ if type(other) == str: other = Units(other) - + conversion_method = { - 'spec': self.spec_conversion, - 'mol': self.mol_conversion, - 'kBT': self.kBT_conversion - } + "spec": self.spec_conversion, + "mol": self.mol_conversion, + "kBT": self.kBT_conversion, + } self_dims, other_dims = self.get_dims(), other.get_dims() if self_dims != other_dims: try: return conversion_method[force](other) except KeyError: - raise UnitsError('Failure in units conversion: units %s[%s] and' - ' %s[%s] have different dimensions' - % (self, self.get_dims(), other, other.get_dims())) + raise UnitsError( + "Failure in units conversion: units %s[%s] and" + " %s[%s] have different dimensions" + % (self, self.get_dims(), other, other.get_dims()) + ) return self.to_si() / other.to_si() - + def kBT_conversion(self, other): from_dims = self.get_dims() to_dims = other.get_dims() fac = self.to_si() - + if from_dims == d_energy and to_dims == d_temperature: fac = fac / kB elif from_dims == d_temperature and to_dims == d_energy: fac = fac * kB else: - raise UnitsError('Failure in conversion of units: was expecting to ' - 'covert between energy and temperature') - return fac/other.to_si() + raise UnitsError( + "Failure in conversion of units: was expecting to " + "covert between energy and temperature" + ) + return fac / other.to_si() def mol_conversion(self, other): - from_dims = self.get_dims() #original unit dimensions - to_dims = other.get_dims() #desired unit dimensions - fac = self.to_si() #factor needed to conver to SI units - + from_dims = self.get_dims() # original unit dimensions + to_dims = other.get_dims() # desired unit dimensions + fac = self.to_si() # factor needed to conver to SI units + if from_dims.dims[4] == to_dims.dims[4]: - raise UnitsError('Failure in conversion of units: no ' - 'different in quantity dimensions between %s and %s' - % from_dims, to_dims) + raise UnitsError( + "Failure in conversion of units: no " + "different in quantity dimensions between %s and %s" % from_dims, + to_dims, + ) elif from_dims.dims[4] > to_dims.dims[4]: - fac = fac/(NA**(from_dims.dims[4]-to_dims.dims[4])) + fac = fac / (NA ** (from_dims.dims[4] - to_dims.dims[4])) else: - fac = fac*(NA**(to_dims.dims[4]-from_dims.dims[4])) - return fac/other.to_si() - + fac = fac * (NA ** (to_dims.dims[4] - from_dims.dims[4])) + return fac / other.to_si() + def spec_conversion(self, other): d_wavenumber = d_length**-1 d_frequency = d_time**-1 d_wavelength = d_length - + from_dims = self.get_dims() to_dims = other.get_dims() fac = self.to_si() if from_dims == d_wavenumber: - fac *= h*c + fac *= h * c elif from_dims == d_frequency: fac *= h elif from_dims != d_energy: - raise UnitsError('Failure in conversion of spectroscopic units:' - ' I only recognise from-units of wavenumber, energy and' - ' frequency but got %s' % str(self)) + raise UnitsError( + "Failure in conversion of spectroscopic units:" + " I only recognise from-units of wavenumber, energy and" + " frequency but got %s" % str(self) + ) if to_dims == d_wavenumber: - fac /= h*c + fac /= h * c elif to_dims == d_frequency: fac /= h elif to_dims != d_energy: - raise UnitsError('Failure in conversion of spectroscopic units:' - ' I only recognise to-units of wavenumber, energy and' - ' frequency but got %s' % str(other)) + raise UnitsError( + "Failure in conversion of spectroscopic units:" + " I only recognise to-units of wavenumber, energy and" + " frequency but got %s" % str(other) + ) return fac / other.to_si() + def convert(from_units, to_units): return Units(from_units).conversion(to_units) diff --git a/tests/test_conversions.py b/tests/test_conversions.py index 2c91aa0..56b7179 100644 --- a/tests/test_conversions.py +++ b/tests/test_conversions.py @@ -8,14 +8,15 @@ import unittest from pyqn.units import Units, UnitsError + class UnitsConversionCheck(unittest.TestCase): """Unit tests for unit conversions within the Units class.""" def test_regular_units_conversion(self): - u1 = Units('m.s-1') - u2 = Units('cm.s-1') - u3 = Units('ft.hr-1') - u4 = Units('m.s-2') + u1 = Units("m.s-1") + u2 = Units("cm.s-1") + u3 = Units("ft.hr-1") + u4 = Units("m.s-2") self.assertAlmostEqual(u1.conversion(u2), 100) self.assertAlmostEqual(u2.conversion(u1), 0.01) @@ -24,35 +25,33 @@ def test_regular_units_conversion(self): u1.conversion(u4) def test_litres(self): - u1 = Units('l') - self.assertEqual(u1.to_si(), 1.e-3) - u2 = Units('L') - self.assertEqual(u1.to_si(), 1.e-3) + u1 = Units("l") + self.assertEqual(u1.to_si(), 1.0e-3) + u2 = Units("L") + self.assertEqual(u1.to_si(), 1.0e-3) def test_molar_units_conversion(self): - u1 = Units('kJ') - u2 = Units('J/mol') + u1 = Units("kJ") + u2 = Units("J/mol") with self.assertRaises(UnitsError) as cm: u1.conversion(u2, strict=True) - self.assertAlmostEqual(u1.conversion(u2, force='mol'), 1.660e-21) - + self.assertAlmostEqual(u1.conversion(u2, force="mol"), 1.660e-21) + def test_kBT_units_conversion(self): - u1 = Units('K') - u2 = Units('J') - - self.assertAlmostEqual(u1.conversion(u2, force='kBT'), 1.38064852e-23) - + u1 = Units("K") + u2 = Units("J") + + self.assertAlmostEqual(u1.conversion(u2, force="kBT"), 1.38064852e-23) + def test_spec_conversions(self): - u1 = Units('J') - u2 = Units('cm-1') - u3 = Units('s-1') - - self.assertAlmostEqual(u1.conversion(u2, force='spec'), - 5.0341165675427096e+22) - self.assertAlmostEqual(u1.conversion(u3, force='spec'), - 1.5091901796421518e+33) - -if __name__ == '__main__': - unittest.main() + u1 = Units("J") + u2 = Units("cm-1") + u3 = Units("s-1") + + self.assertAlmostEqual(u1.conversion(u2, force="spec"), 5.0341165675427096e22) + self.assertAlmostEqual(u1.conversion(u3, force="spec"), 1.5091901796421518e33) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_quantity.py b/tests/test_quantity.py index f1864f9..ce84778 100644 --- a/tests/test_quantity.py +++ b/tests/test_quantity.py @@ -3,78 +3,79 @@ from pyqn.dimensions import Dimensions, d_energy from pyqn.units import UnitsError + class QuantityManipulations(unittest.TestCase): def test_quantity_init(self): pass - + def test_quantity_multiplication(self): - q1 = Quantity(value=22.4,units='m/s') - q2 = Quantity(value=2,units='s') - - q3 = q1*q2 - self.assertAlmostEqual(q3.value,44.8) - self.assertEqual(q3.units.dims.dims,(1,0,0,0,0,0,0)) - + q1 = Quantity(value=22.4, units="m/s") + q2 = Quantity(value=2, units="s") + + q3 = q1 * q2 + self.assertAlmostEqual(q3.value, 44.8) + self.assertEqual(q3.units.dims.dims, (1, 0, 0, 0, 0, 0, 0)) + def test_quantity_division(self): - q1 = Quantity(value=39, units='J') - q2 = Quantity(value=5, units='s') - q4 = Quantity(value=0, units='m') - - q3 = q1/q2 - self.assertAlmostEqual(q3.value,7.8) - self.assertEqual(q3.units.dims,Dimensions(M=1,L=2,T=-3)) - q3 = q2/q1 - self.assertAlmostEqual(q3.value,0.128205128205) - self.assertEqual(q3.units.dims,Dimensions(M=-1,L=-2,T=3)) - + q1 = Quantity(value=39, units="J") + q2 = Quantity(value=5, units="s") + q4 = Quantity(value=0, units="m") + + q3 = q1 / q2 + self.assertAlmostEqual(q3.value, 7.8) + self.assertEqual(q3.units.dims, Dimensions(M=1, L=2, T=-3)) + q3 = q2 / q1 + self.assertAlmostEqual(q3.value, 0.128205128205) + self.assertEqual(q3.units.dims, Dimensions(M=-1, L=-2, T=3)) + with self.assertRaises(ZeroDivisionError) as cm: - q3 = q1/q4 - - + q3 = q1 / q4 + def test_quantity_addition(self): - q1 = Quantity(value = 20.5, units = 'J') - q2 = Quantity(value = 30.7, units = 'kg.m2.s-2') - q3 = Quantity(value = 5.1, units = 'K') - - q4 = q1+q2 - self.assertEqual(q4.value,51.2) - self.assertEqual(q4.units.dims,Dimensions(M=1,L=2,T=-2)) - + q1 = Quantity(value=20.5, units="J") + q2 = Quantity(value=30.7, units="kg.m2.s-2") + q3 = Quantity(value=5.1, units="K") + + q4 = q1 + q2 + self.assertEqual(q4.value, 51.2) + self.assertEqual(q4.units.dims, Dimensions(M=1, L=2, T=-2)) + with self.assertRaises(UnitsError) as cm: q4 = q1 + q3 - + def test_quantity_subtraction(self): - q1 = Quantity(value = 20.5, units = 'J') - q2 = Quantity(value = 30.7, units = 'kg.m2.s-2') - q3 = Quantity(value = 5.1, units = 'K') - - q4 = q1-q2 - self.assertEqual(q4.value,-10.2) - self.assertEqual(q4.units.dims,Dimensions(M=1,L=2,T=-2)) - + q1 = Quantity(value=20.5, units="J") + q2 = Quantity(value=30.7, units="kg.m2.s-2") + q3 = Quantity(value=5.1, units="K") + + q4 = q1 - q2 + self.assertEqual(q4.value, -10.2) + self.assertEqual(q4.units.dims, Dimensions(M=1, L=2, T=-2)) + with self.assertRaises(UnitsError) as cm: q4 = q1 - q3 - + def test_quantity_exponent(self): - q1 = Quantity(value = 1.2, units = 'J') - q2 = Quantity(value = -5, units = 's') - + q1 = Quantity(value=1.2, units="J") + q2 = Quantity(value=-5, units="s") + q3 = q1**4 q4 = q2**-1 - self.assertEqual(q3.value,2.0736) - self.assertEqual(q3.units.dims,Dimensions(M=4,L=8,T=-8)) + self.assertEqual(q3.value, 2.0736) + self.assertEqual(q3.units.dims, Dimensions(M=4, L=8, T=-8)) self.assertEqual(q4.value, -0.2) - self.assertEqual(q4.units.dims,Dimensions(T=-1)) - + self.assertEqual(q4.units.dims, Dimensions(T=-1)) + q5 = q1**0 - self.assertEqual(q5.value,1) - self.assertNotEqual(q1.units.dims,q5.units.dims) - + self.assertEqual(q5.value, 1) + self.assertNotEqual(q1.units.dims, q5.units.dims) + def test_quantity_conversion(self): - #q1 = Quantity(value = 200, units = 'J') - #q2 = q1.convert_units_to('eV') - #self.assertAlmostEqual(q2.value,1.2483019242e+21,places=2) + # q1 = Quantity(value = 200, units = 'J') + # q2 = q1.convert_units_to('eV') + # self.assertAlmostEqual(q2.value,1.2483019242e+21,places=2) pass - -if __name__ == '__main__': + + +if __name__ == "__main__": unittest.main() diff --git a/tests/test_undef.py b/tests/test_undef.py new file mode 100644 index 0000000..bf01d33 --- /dev/null +++ b/tests/test_undef.py @@ -0,0 +1,18 @@ +import unittest +from pyqn.units import Units + + +class TestUndef(unittest.TestCase): + def test_undef(self): + u1 = Units("undef") + self.assertTrue(u1.undef) + self.assertEqual(repr(u1), "undef") + self.assertEqual(u1.html, "undef") + + u2 = Units("cm.hr-1/m") + + for u3 in (Units("undef"), 1, None): + self.assertFalse(u1 == u3) + self.assertTrue(u1 != u3) + self.assertFalse(u2 == u3) + self.assertTrue(u2 != u3) diff --git a/tests/test_units.py b/tests/test_units.py index 1e80d35..b9bab05 100644 --- a/tests/test_units.py +++ b/tests/test_units.py @@ -7,104 +7,106 @@ import unittest from pyqn.units import Units -from pyqn.dimensions import Dimensions,d_energy +from pyqn.dimensions import Dimensions, d_energy + class UnitsCheck(unittest.TestCase): """Unit tests for the Units class.""" def test_units_algebra(self): - u1 = Units('m.s-1') + u1 = Units("m.s-1") self.assertTrue(u1.has_units()) - u2 = Units('cm.hr-1/m') + u2 = Units("cm.hr-1/m") - self.assertEqual(str(u1), 'm.s-1') - self.assertEqual(str(u2), 'cm.hr-1.m-1') + self.assertEqual(str(u1), "m.s-1") + self.assertEqual(str(u2), "cm.hr-1.m-1") - self.assertEqual(u1*u2, Units('cm.s-1.hr-1')) - self.assertEqual(u1/u2, Units('m2.s-1.cm-1.hr')) - self.assertEqual(u2/u1, Units('cm.hr-1.s/m2')) + self.assertEqual(u1 * u2, Units("cm.s-1.hr-1")) + self.assertEqual(u1 / u2, Units("m2.s-1.cm-1.hr")) + self.assertEqual(u2 / u1, Units("cm.hr-1.s/m2")) - u3 = Units('J.A-1') - u4 = Units('J.A-1') + u3 = Units("J.A-1") + u4 = Units("J.A-1") u3_over_u4 = u3 / u4 self.assertFalse(u3_over_u4.has_units()) def test_units_multiplication(self): - u1 = Units('m.s-1') + u1 = Units("m.s-1") u2 = u1 * 1 - + self.assertEqual(u2, u1) - self.assertNotEqual(id(u1), id(u2)) - + self.assertNotEqual(id(u1), id(u2)) + u2 = 1 * u1 self.assertEqual(u2, u1) - self.assertNotEqual(id(u1), id(u2)) - - u1 = Units('m.s-1') - u2 = Units('J') - self.assertEqual('kg.m.s-1' * u1, u2) - - u1 = Units('m.s-1') - u2 = Units('J') - self.assertEqual(u1 * 'kg.m.s-1', u2) - - u1 = Units('mm2') - u2 = Units('g/m2') + self.assertNotEqual(id(u1), id(u2)) + + u1 = Units("m.s-1") + u2 = Units("J") + self.assertEqual("kg.m.s-1" * u1, u2) + + u1 = Units("m.s-1") + u2 = Units("J") + self.assertEqual(u1 * "kg.m.s-1", u2) + + u1 = Units("mm2") + u2 = Units("g/m2") u3 = u1 * u2 - self.assertAlmostEqual(u3.to_si(), 1.e-9) - self.assertTupleEqual(u3.get_dims().dims,(0, 1, 0, 0, 0, 0, 0)) - - u4 = Units('J/s') + self.assertAlmostEqual(u3.to_si(), 1.0e-9) + self.assertTupleEqual(u3.get_dims().dims, (0, 1, 0, 0, 0, 0, 0)) + + u4 = Units("J/s") u5 = u4 * u2 - self.assertAlmostEqual(u5.to_si(), 1.e-3) - self.assertEqual(u5.get_dims().dims,(0,2,-3,0,0,0,0)) + self.assertAlmostEqual(u5.to_si(), 1.0e-3) + self.assertEqual(u5.get_dims().dims, (0, 2, -3, 0, 0, 0, 0)) def test_units_division(self): - u1 = Units('eV.mm') - u2 = Units('K.cm') - + u1 = Units("eV.mm") + u2 = Units("K.cm") + u3 = u1 / u2 - self.assertEqual(u3.get_dims().dims,(2,1,-2,-1,0,0,0)) - self.assertAlmostEqual(u3.to_si(),1.6*10**(-20)) - - u4 = Units('s-1') - u5 = u2 / ('eV'*u4) - self.assertEqual(u5.get_dims(),Dimensions(T=1,Theta=1,L=1)/d_energy) - + self.assertEqual(u3.get_dims().dims, (2, 1, -2, -1, 0, 0, 0)) + self.assertAlmostEqual(u3.to_si(), 1.6 * 10 ** (-20)) + + u4 = Units("s-1") + u5 = u2 / ("eV" * u4) + self.assertEqual(u5.get_dims(), Dimensions(T=1, Theta=1, L=1) / d_energy) + def test_units_power(self): - u1 = Units('J.m') - u2 = u1 ** 2 - self.assertEqual(u2.get_dims(),Dimensions(M=2,L=6,T=-4)) - + u1 = Units("J.m") + u2 = u1**2 + self.assertEqual(u2.get_dims(), Dimensions(M=2, L=6, T=-4)) + u3 = u1 ** (-1) - self.assertEqual(u3.get_dims(),Dimensions(M=-1,L=-3,T=2)) - + self.assertEqual(u3.get_dims(), Dimensions(M=-1, L=-3, T=2)) + u4 = u1 ** (-2) - self.assertEqual(u4.get_dims(),Dimensions(M=-2,L=-6,T=4)) - - u5 = u1 ** 0 - self.assertEqual(u5.get_dims(),Dimensions()) - + self.assertEqual(u4.get_dims(), Dimensions(M=-2, L=-6, T=4)) + + u5 = u1**0 + self.assertEqual(u5.get_dims(), Dimensions()) + def test_units_algebra_dimensions(self): - u1 = Units('m') - u2 = Units('m.s-1') + u1 = Units("m") + u2 = Units("m.s-1") u3 = u1 * u2 - u4 = Units('m2.s-1') + u4 = Units("m2.s-1") self.assertEqual(u3.dims, u4.dims) u3 = u1 / u2 - u4 = Units('s') + u4 = Units("s") self.assertEqual(u3.dims, u4.dims) def test_unicode_units(self): - u1 = Units('kΩ') - self.assertEqual(str(u1), 'kΩ') + u1 = Units("kΩ") + self.assertEqual(str(u1), "kΩ") def test_html(self): - u1 = Units('m.s-1') - self.assertEqual(u1.html, 'm s-1') - u2 = Units('μs.J/m3') - self.assertEqual(u2.html, 'μs J m-3') + u1 = Units("m.s-1") + self.assertEqual(u1.html, "m s-1") + u2 = Units("μs.J/m3") + self.assertEqual(u2.html, "μs J m-3") + -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_units_collisions.py b/tests/test_units_collisions.py index f198d86..e9e5930 100644 --- a/tests/test_units_collisions.py +++ b/tests/test_units_collisions.py @@ -12,6 +12,7 @@ from pyqn.base_unit import base_units from pyqn.si import si_prefixes + class ConflictsCheck(unittest.TestCase): """ Check that there are no conflicts amongst the allowed derived and @@ -20,15 +21,20 @@ class ConflictsCheck(unittest.TestCase): """ def test_units_conflicts(self): - prefixes = ['']; prefixes.extend(si_prefixes) + prefixes = [""] + prefixes.extend(si_prefixes) seen_units = [] for base_unit_group in base_units: for base_unit in base_unit_group[1]: for si_prefix in prefixes: - this_unit = '%s%s' % (si_prefix, base_unit.stem) - self.assertEqual(this_unit in seen_units, False, - 'Clash with unit: %s' % this_unit) + this_unit = "%s%s" % (si_prefix, base_unit.stem) + self.assertEqual( + this_unit in seen_units, + False, + "Clash with unit: %s" % this_unit, + ) seen_units.append(this_unit) -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main()