Skip to content
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ repos:
- id: mypy
files: src
args: []
additional_dependencies: [pint]

- repo: https://github.com/codespell-project/codespell
rev: v2.4.1
Expand Down
26 changes: 25 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ The package is also available on `conda-forge`_, and installable with
Getting started
---------------

The package contains 2 modules, ``constants`` and ``units``,
The package contains 2 core modules, ``constants`` and ``units``,
whose names are self-explanatory.
It may be more readable to import quantities explicitly from each of the modules
though everything is available from the top-level as ``from hepunits import ...``.
Expand Down Expand Up @@ -137,6 +137,30 @@ to ensure an explicit conversion to the desired unit dividing by it (GeV in the
0.0005


Pint integration
~~~~~~~~~~~~~~~~
The package can interoperate with `Pint`_, which provides a more full-featured units
and quantities system. Pint is an optional dependency of ``hepunits``.
When Pint is installed, ``hepunits`` units and constants can be used to create Pint
quantities, and Pint quantities can be converted to ``hepunits`` units, as shown below.

.. code-block:: pycon

>>> import pint
>>> import hepunits
>>> from hepunits.pint import to_clhep, from_clhep
>>> ureg = pint.UnitRegistry()
>>> g = 9.8 * ureg.meter / ureg.second**2
>>> g
<Quantity(9.8, 'meter / second ** 2')>
>>> to_clhep(g)
9.800000000000001e-15
>>> from_clhep(hepunits.c_light, ureg.meter / ureg.second)
<Quantity(299792458.0, 'meter / second')>
>>> from_clhep(hepunits.c_light, ureg.fathom / ureg.fortnight)
<Quantity(1.98287528e+14, 'fathom / fortnight')>

.. _Pint: https://pint.readthedocs.io/

.. |Scikit-HEP| image:: https://scikit-hep.org/assets/images/Scikit--HEP-Project-blue.svg
:target: https://scikit-hep.org
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,17 @@ dynamic = ["version"]
all = [
"pytest-cov>=2.8.0",
"pytest>=6",
"pint",
]
dev = [
"pytest-cov>=2.8.0",
"pytest>=6",
"pint",
]
test = [
"pytest-cov>=2.8.0",
"pytest>=6",
"pint",
]

[project.urls]
Expand Down
95 changes: 95 additions & 0 deletions src/hepunits/pint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Licensed under a 3-clause BSD style license, see LICENSE.
"""
Conversion routines between CLHEP, the HEP System of Units, and Pint units.

CLHEP adopts the approach where all the quantities are stored in the base unit
system, effectively dimensionless. Pint offers to store both the magnitude and
the dimensionality of the unit, which is helpful in deducing and/or validating
the resulting unit of formulas. This module offers conversion routines between
Pint's default base unit system and CLHEP.
"""

from __future__ import annotations

try:
import pint
except ImportError as exc:
msg = "Pint is required to use hepunits.pint."
raise ImportError(msg) from exc

# TODO: support more unit conversions
_clhep_base_units = {
"[length]": "millimeter",
"[time]": "nanosecond",
"[mass]": "MeV * millimeter**-2 * nanosecond**2",
"[current]": "elementary_charge / nanosecond",
}


def _unit_from(val: pint.Quantity | pint.Unit) -> pint.Unit:
"""Extract the dimensionality from a Pint Quantity or Unit."""
# Grabbing the type is a quick way to be in the correct unit registry
# see e.g. https://github.com/hgrecco/pint/issues/2207
unit = type(val.units) if isinstance(val, pint.Quantity) else type(val)
out = unit("dimensionless")
for dim, exponent in val.dimensionality.items():
if dim not in _clhep_base_units:
msg = f"Unsupported dimension {dim} in {val}"
raise ValueError(msg)
out *= unit(_clhep_base_units[dim]) ** exponent

return out


def to_clhep(val: pint.Quantity | pint.Unit) -> float:
"""
Convert a Pint Quantity or Unit to CLHEP base units.

Parameters
----------
val : pint.Quantity or pint.Unit
The value to convert.

Returns
-------
float
The value in CLHEP base units (dimensionless).

Examples
--------
>>> ureg = pint.UnitRegistry()
>>> g = 9.8 * ureg.meter / ureg.second**2
>>> g
<Quantity(9.8, 'meter / second ** 2')>
>>> to_clhep(g)
9.800000000000001e-15
"""
clhep_unit = _unit_from(val)
q = pint.Quantity(1.0, val) if isinstance(val, pint.Unit) else val
return q.to(clhep_unit).magnitude # type: ignore[no-any-return]


def from_clhep(val: float, unit: pint.Unit) -> pint.Quantity:
"""
Convert a value in CLHEP base units to a Pint Quantity.

Parameters
----------
val : float
The value in CLHEP base units (dimensionless).
unit : pint.Unit
The desired output unit.

Returns
-------
pint.Quantity
The value in the desired unit.

Examples
--------
>>> ureg = pint.UnitRegistry()
>>> from_clhep(hepunits.c_light, ureg.meter / ureg.second)
<Quantity(299792458.0, 'meter / second')>
"""
clhep_unit = _unit_from(unit)
return pint.Quantity(val, clhep_unit).to(unit)
1 change: 0 additions & 1 deletion tests/test_missing_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ def filter_module(item: str) -> bool:
@pytest.mark.parametrize(
"module",
[
hepunits,
hepunits.units,
hepunits.constants,
hepunits.constants.constants,
Expand Down
47 changes: 47 additions & 0 deletions tests/test_pint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env python
# Licensed under a 3-clause BSD style license, see LICENSE.
"""
Tests for the hepunits.pint module.
"""

import pint
import pytest
from pytest import approx

import hepunits
from hepunits.pint import from_clhep, to_clhep


def test_pint_constants():
ureg = pint.UnitRegistry()

# These three constants set the relationship between mass, length, time, and charge
# TODO: check all 7 SI defining constants (hence also supporting more unit conversions)
pint_c = to_clhep(1 * ureg.speed_of_light)
assert pint_c == approx(hepunits.c_light, rel=1e-15)
pint_h = to_clhep(1 * ureg.planck_constant)
assert pint_h == approx(hepunits.h_Planck, rel=1e-15)
pint_e = to_clhep(1 * ureg.elementary_charge)
assert pint_e == approx(1.0, rel=1e-15)


def test_pint_roundtrip():
ureg = pint.UnitRegistry()

assert to_clhep(3 * ureg.mm) == approx(3.0)
assert to_clhep(3 * ureg.cm) == approx(30.0)
assert to_clhep(2 * ureg.ohm) == approx(2.0 * hepunits.ohm)
assert to_clhep(ureg.coulomb) == approx(hepunits.coulomb)

assert from_clhep(hepunits.c_light, ureg.meter / ureg.second).m == approx(
(1.0 * ureg.c).to(ureg.meter / ureg.second).m, rel=1e-15
)
assert from_clhep(hepunits.tesla, ureg.tesla).m == approx(
(1 * ureg.tesla).m, rel=1e-15
)


def test_unsupported_dimension():
ureg = pint.UnitRegistry()
with pytest.raises(ValueError, match="Unsupported dimension"):
to_clhep(1 * ureg.kelvin)