Skip to content

Commit 1b2cdee

Browse files
nsmith-pre-commit-ci[bot]eduardo-rodrigues
authored
feat: add Pint integration for unit conversions (#273)
* feat: add Pint integration for unit conversions * Better test coverage * style: pre-commit fixes * lint * style: pre-commit fixes * More lint * more lint * Construct explicitly * Add license statement to pint.py * Add trivial docstring to test_pint.py * style: pre-commit fixes * Apply suggestions from code review Co-authored-by: Eduardo Rodrigues <[email protected]> * Advertise pint on README --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Eduardo Rodrigues <[email protected]>
1 parent 6804332 commit 1b2cdee

File tree

6 files changed

+171
-2
lines changed

6 files changed

+171
-2
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ repos:
3030
- id: mypy
3131
files: src
3232
args: []
33+
additional_dependencies: [pint]
3334

3435
- repo: https://github.com/codespell-project/codespell
3536
rev: v2.4.1

README.rst

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ The package is also available on `conda-forge`_, and installable with
7171
Getting started
7272
---------------
7373

74-
The package contains 2 modules, ``constants`` and ``units``,
74+
The package contains 2 core modules, ``constants`` and ``units``,
7575
whose names are self-explanatory.
7676
It may be more readable to import quantities explicitly from each of the modules
7777
though everything is available from the top-level as ``from hepunits import ...``.
@@ -137,6 +137,30 @@ to ensure an explicit conversion to the desired unit dividing by it (GeV in the
137137
0.0005
138138
139139
140+
Pint integration
141+
~~~~~~~~~~~~~~~~
142+
The package can interoperate with `Pint`_, which provides a more full-featured units
143+
and quantities system. Pint is an optional dependency of ``hepunits``.
144+
When Pint is installed, ``hepunits`` units and constants can be used to create Pint
145+
quantities, and Pint quantities can be converted to ``hepunits`` units, as shown below.
146+
147+
.. code-block:: pycon
148+
149+
>>> import pint
150+
>>> import hepunits
151+
>>> from hepunits.pint import to_clhep, from_clhep
152+
>>> ureg = pint.UnitRegistry()
153+
>>> g = 9.8 * ureg.meter / ureg.second**2
154+
>>> g
155+
<Quantity(9.8, 'meter / second ** 2')>
156+
>>> to_clhep(g)
157+
9.800000000000001e-15
158+
>>> from_clhep(hepunits.c_light, ureg.meter / ureg.second)
159+
<Quantity(299792458.0, 'meter / second')>
160+
>>> from_clhep(hepunits.c_light, ureg.fathom / ureg.fortnight)
161+
<Quantity(1.98287528e+14, 'fathom / fortnight')>
162+
163+
.. _Pint: https://pint.readthedocs.io/
140164

141165
.. |Scikit-HEP| image:: https://scikit-hep.org/assets/images/Scikit--HEP-Project-blue.svg
142166
:target: https://scikit-hep.org

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,17 @@ dynamic = ["version"]
4242
all = [
4343
"pytest-cov>=2.8.0",
4444
"pytest>=6",
45+
"pint",
4546
]
4647
dev = [
4748
"pytest-cov>=2.8.0",
4849
"pytest>=6",
50+
"pint",
4951
]
5052
test = [
5153
"pytest-cov>=2.8.0",
5254
"pytest>=6",
55+
"pint",
5356
]
5457

5558
[project.urls]

src/hepunits/pint.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Licensed under a 3-clause BSD style license, see LICENSE.
2+
"""
3+
Conversion routines between CLHEP, the HEP System of Units, and Pint units.
4+
5+
CLHEP adopts the approach where all the quantities are stored in the base unit
6+
system, effectively dimensionless. Pint offers to store both the magnitude and
7+
the dimensionality of the unit, which is helpful in deducing and/or validating
8+
the resulting unit of formulas. This module offers conversion routines between
9+
Pint's default base unit system and CLHEP.
10+
"""
11+
12+
from __future__ import annotations
13+
14+
try:
15+
import pint
16+
except ImportError as exc:
17+
msg = "Pint is required to use hepunits.pint."
18+
raise ImportError(msg) from exc
19+
20+
# TODO: support more unit conversions
21+
_clhep_base_units = {
22+
"[length]": "millimeter",
23+
"[time]": "nanosecond",
24+
"[mass]": "MeV * millimeter**-2 * nanosecond**2",
25+
"[current]": "elementary_charge / nanosecond",
26+
}
27+
28+
29+
def _unit_from(val: pint.Quantity | pint.Unit) -> pint.Unit:
30+
"""Extract the dimensionality from a Pint Quantity or Unit."""
31+
# Grabbing the type is a quick way to be in the correct unit registry
32+
# see e.g. https://github.com/hgrecco/pint/issues/2207
33+
unit = type(val.units) if isinstance(val, pint.Quantity) else type(val)
34+
out = unit("dimensionless")
35+
for dim, exponent in val.dimensionality.items():
36+
if dim not in _clhep_base_units:
37+
msg = f"Unsupported dimension {dim} in {val}"
38+
raise ValueError(msg)
39+
out *= unit(_clhep_base_units[dim]) ** exponent
40+
41+
return out
42+
43+
44+
def to_clhep(val: pint.Quantity | pint.Unit) -> float:
45+
"""
46+
Convert a Pint Quantity or Unit to CLHEP base units.
47+
48+
Parameters
49+
----------
50+
val : pint.Quantity or pint.Unit
51+
The value to convert.
52+
53+
Returns
54+
-------
55+
float
56+
The value in CLHEP base units (dimensionless).
57+
58+
Examples
59+
--------
60+
>>> ureg = pint.UnitRegistry()
61+
>>> g = 9.8 * ureg.meter / ureg.second**2
62+
>>> g
63+
<Quantity(9.8, 'meter / second ** 2')>
64+
>>> to_clhep(g)
65+
9.800000000000001e-15
66+
"""
67+
clhep_unit = _unit_from(val)
68+
q = pint.Quantity(1.0, val) if isinstance(val, pint.Unit) else val
69+
return q.to(clhep_unit).magnitude # type: ignore[no-any-return]
70+
71+
72+
def from_clhep(val: float, unit: pint.Unit) -> pint.Quantity:
73+
"""
74+
Convert a value in CLHEP base units to a Pint Quantity.
75+
76+
Parameters
77+
----------
78+
val : float
79+
The value in CLHEP base units (dimensionless).
80+
unit : pint.Unit
81+
The desired output unit.
82+
83+
Returns
84+
-------
85+
pint.Quantity
86+
The value in the desired unit.
87+
88+
Examples
89+
--------
90+
>>> ureg = pint.UnitRegistry()
91+
>>> from_clhep(hepunits.c_light, ureg.meter / ureg.second)
92+
<Quantity(299792458.0, 'meter / second')>
93+
"""
94+
clhep_unit = _unit_from(unit)
95+
return pint.Quantity(val, clhep_unit).to(unit)

tests/test_missing_all.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ def filter_module(item: str) -> bool:
1414
@pytest.mark.parametrize(
1515
"module",
1616
[
17-
hepunits,
1817
hepunits.units,
1918
hepunits.constants,
2019
hepunits.constants.constants,

tests/test_pint.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#!/usr/bin/env python
2+
# Licensed under a 3-clause BSD style license, see LICENSE.
3+
"""
4+
Tests for the hepunits.pint module.
5+
"""
6+
7+
import pint
8+
import pytest
9+
from pytest import approx
10+
11+
import hepunits
12+
from hepunits.pint import from_clhep, to_clhep
13+
14+
15+
def test_pint_constants():
16+
ureg = pint.UnitRegistry()
17+
18+
# These three constants set the relationship between mass, length, time, and charge
19+
# TODO: check all 7 SI defining constants (hence also supporting more unit conversions)
20+
pint_c = to_clhep(1 * ureg.speed_of_light)
21+
assert pint_c == approx(hepunits.c_light, rel=1e-15)
22+
pint_h = to_clhep(1 * ureg.planck_constant)
23+
assert pint_h == approx(hepunits.h_Planck, rel=1e-15)
24+
pint_e = to_clhep(1 * ureg.elementary_charge)
25+
assert pint_e == approx(1.0, rel=1e-15)
26+
27+
28+
def test_pint_roundtrip():
29+
ureg = pint.UnitRegistry()
30+
31+
assert to_clhep(3 * ureg.mm) == approx(3.0)
32+
assert to_clhep(3 * ureg.cm) == approx(30.0)
33+
assert to_clhep(2 * ureg.ohm) == approx(2.0 * hepunits.ohm)
34+
assert to_clhep(ureg.coulomb) == approx(hepunits.coulomb)
35+
36+
assert from_clhep(hepunits.c_light, ureg.meter / ureg.second).m == approx(
37+
(1.0 * ureg.c).to(ureg.meter / ureg.second).m, rel=1e-15
38+
)
39+
assert from_clhep(hepunits.tesla, ureg.tesla).m == approx(
40+
(1 * ureg.tesla).m, rel=1e-15
41+
)
42+
43+
44+
def test_unsupported_dimension():
45+
ureg = pint.UnitRegistry()
46+
with pytest.raises(ValueError, match="Unsupported dimension"):
47+
to_clhep(1 * ureg.kelvin)

0 commit comments

Comments
 (0)