Skip to content

Commit

Permalink
Add rational exponent functionality to Units
Browse files Browse the repository at this point in the history
  • Loading branch information
xnx committed Apr 22, 2024
1 parent 489eed4 commit 80cf56b
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 8 deletions.
4 changes: 3 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

setup(
name="pyqn",
version="1.3.2",
version="1.4",
description="A package for managing physical units and quantities",
long_description=long_description,
long_description_content_type="text/x-rst",
Expand All @@ -27,6 +27,8 @@
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3 :: Only",
"Operating System :: OS Independent",
],
Expand Down
9 changes: 7 additions & 2 deletions src/pyqn/atom_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
# along with PyQn. If not, see <http://www.gnu.org/licenses/>

import sys
from fractions import Fraction
from pyparsing import Word, Group, Literal, Suppress, ParseException, oneOf, Optional
from .si import si_prefixes
from .base_unit import BaseUnit, base_unit_stems
Expand All @@ -33,13 +34,14 @@
lowers = caps.lower()
letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_0"
digits = "123456789"
exponent = Word(digits + "-")
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)
)


# floating point equality and its negation, to some suitable tolerance
def feq(f1, f2, tol=1.0e-10):
return abs(f1 - f2) <= tol
Expand Down Expand Up @@ -125,7 +127,10 @@ def parse(self, s_unit_atom):
# if there is an exponent, determine what it is (default is 1)
exponent = 1
if len(uatom_data) == 2:
exponent = int(uatom_data[1])
if "/" in uatom_data:
exponent = int(uatom_data[1])
else:
exponent = Fraction(uatom_data[1])
return AtomUnit(prefix, base_unit, exponent)

def __pow__(self, power):
Expand Down
28 changes: 23 additions & 5 deletions src/pyqn/units.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
# You should have received a copy of the GNU General Public License
# along with PyQn. If not, see <http://www.gnu.org/licenses/>

import re
import copy
from .dimensions import Dimensions
from .dimensions import d_dimensionless, d_length, d_energy, d_time, d_temperature
Expand Down Expand Up @@ -82,18 +83,35 @@ def get_dims(self):
return dims

@classmethod
def parse(self, s_compoundunit):
def parse(self, s_compoundunit, no_divided_units=False):
"""
Parse the string s_compoundunit and return the corresponding
Units object.
"""

if no_divided_units:
# s_compoundunit does not consist of any units separated by '/'
# so parse immediately as a sequence of multiplied units.
return Units.parse_mult_units(s_compoundunit)

# We need to temporarily identify rational exponents (e.g. "Pa-1/2")
# and replace the / with : so that we can split up the atomic units
# properly.
patt = r"\d+/\d+"
rational_exponents = re.findall(patt, s_compoundunit)
for e in rational_exponents:
es = e.replace("/", ":")
s_compoundunit = s_compoundunit.replace(e, es)

div_fields = s_compoundunit.split("/")
ndiv_fields = len(div_fields)
compound_unit = Units.parse_mult_units(div_fields[0])
# don't forget to put back the "/" character in any rational exponents.
compound_unit = Units.parse_mult_units(div_fields[0].replace(":", "/"))
for div_field in div_fields[1:]:
compound_unit = compound_unit / Units.parse(div_field)
div_field = div_field.replace(":", "/")
compound_unit = compound_unit / Units.parse(
div_field, no_divided_units=True
)
return compound_unit

@classmethod
Expand Down Expand Up @@ -244,7 +262,7 @@ def html(self):
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("<sup>{:d}</sup>".format(atom_unit.exponent))
h.append(f"<sup>{atom_unit.exponent}</sup>")
if i < n - 1:
h.append(" ")
return "".join(h)
Expand Down
9 changes: 9 additions & 0 deletions tests/test_conversions.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ def test_spec_conversions(self):
self.assertAlmostEqual(u1.conversion(u2, force="spec"), 5.0341165675427096e22)
self.assertAlmostEqual(u1.conversion(u3, force="spec"), 1.5091901796421518e33)

def test_rational_units_conversion(self):
u1 = Units("Hz-1/2")
u2 = Units("ns1/2")
self.assertAlmostEqual(u1.conversion(u2), 10**4.5)

u3 = Units("m-3/2")
u4 = Units("inch-3/2")
self.assertAlmostEqual(u3.conversion(u4), 0.0254**1.5)


if __name__ == "__main__":
unittest.main()
29 changes: 29 additions & 0 deletions tests/test_units.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
# Unit tests for the Units class.

import unittest
from fractions import Fraction
from pyqn.units import Units
from pyqn.dimensions import Dimensions, d_energy

Expand Down Expand Up @@ -107,6 +108,34 @@ def test_html(self):
u2 = Units("μs.J/m3")
self.assertEqual(u2.html, "μs J m<sup>-3</sup>")

def test_units_with_rational_powers(self):
u1 = Units("m-3.Pa-1/2")
u2 = Units("cm-3.bar-1/2")
self.assertEqual(str(u1), "m-3.Pa-1/2")
self.assertEqual(u1.dims, u2.dims)
self.assertEqual(u1.html, "m<sup>-3</sup> Pa<sup>-1/2</sup>")

self.assertEqual(u1.dims, Dimensions(M=-0.5, T=1, L=-2.5))

u3 = Units("kg1/2.m-3/2")
self.assertEqual(str(u3), "kg1/2.m-3/2")
self.assertEqual(u3.html, "kg<sup>1/2</sup> m<sup>-3/2</sup>")

def test_rational_units_algebra(self):
u1 = Units("m-3.Pa-1/2")
u2 = Units("m3")
u3 = u1 * u2
self.assertEqual(str(u3), "Pa-1/2")

u4 = Units("Pa-2/3.s4")
u5 = u1 / u4
u6 = Units("m-3.s4.Pa1/6")
self.assertEqual(str(u6), "m-3.s4.Pa1/6")
self.assertEqual(
u6.dims,
Dimensions(M=Fraction("1/6"), T=Fraction("11/3"), L=Fraction("-19/6")),
)


if __name__ == "__main__":
unittest.main()

0 comments on commit 80cf56b

Please sign in to comment.