OpenFF Units

Units of measure for biomolecular software.

OpenFF Units is based on Pint. Its Quantity, Unit, and Measurement types inherit from Pint’s, and add improved support for serialization and deserialization. OpenFF Units improves support for biomolecular software by providing a system of units that are compatible with OpenMM and providing functions to convert to OpenMM units and back. It also provides atomic masses with units, as well as some other useful maps.

Installation

We recommend installing OpenFF Units with the Conda package manager. If you don’t yet have a Conda distribution installed, we recommend MambaForge for most users. The openff-units package can be installed from Conda Forge:

conda install -c conda-forge openff-units

Using OpenFF Units

OpenFF Units provides the Quantity class, which represents a numerical value with units. A Quantity can be created by providing a value and units:

>>> from openff.units import unit, Quantity
>>> 
>>> Quantity(1.007, unit.amu)
<Quantity(1.007, 'unified_atomic_mass_unit')>

The unit singleton value is a registry of units, but also exposes the Quantity, Unit, and Measurement classes so you don’t have to import them individually. Even easier, multiplying a number by the appropriate unit also provides a Quantity:

>>> mass_proton = 1.007 * unit.amu
>>> mass_proton == unit.Quantity(1.007, unit.amu)
True

Quantity can also wrap NumPy arrays. It’s best to wrap an array of floats in a quantity, rather than have an array of quantities:

>>> import numpy as np
>>> 
>>> box_vectors = np.array([
...     [5.0, 0.0, 0.0],
...     [0.0, 5.0, 0.0],
...     [0.0, 0.0, 5.0],
... ]) * unit.nanometer

When constructed like this, Quantity is transparent; it will pass any attributes it doesn’t have through to the inner value. This means that an quantity-wrapped array can be used exactly as though it were an array — the units are just checked silently in the background:

>>> from numpy.random import rand
>>> 
>>> trajectory = 10 * rand(10, 10000, 3) * unit.nanometer
>>> centroids = trajectory.mean(axis=1)[..., None]
>>> last_water = trajectory[:, 97:99, :]
>>> last_water_recentered = last_water - centroids

This transparency works with most container types, so it’s usually best to have Quantity be the outermost wrapper type.

Complex units can be constructed by combining units with the usual arithmetic operations:

>>> boltzmann_constant = 8.314462618e-3 * unit.kilojoule / unit.kelvin / unit.avogadro_number

Some common constants are provided as units as well:

>>> boltzmann_constant = 1.0 * unit.boltzmann_constant

Adding or subtracting different units with the same dimensions just works:

>>> 1.0 * unit.angstrom + 1.0 * unit.nanometer
<Quantity(11.0, 'angstrom')>

But quantities with different dimensions raise an exception:

>>> 1.0 * unit.angstrom + 1.0 * unit.nanojoule
Traceback (most recent call last):
...
pint.errors.DimensionalityError: Cannot convert from 'angstrom' ([length]) to 'nanojoule' ([length] ** 2 * [mass] / [time] ** 2)

Quantities can be converted between units with the .to() method:

>>> (1.0 * unit.nanometer).to(unit.angstrom)
<Quantity(10.0, 'angstrom')>

Or with the .ito() method for in-place transformations:

>>> quantity = 10.0 * unit.angstrom
>>> quantity.ito(unit.nanometer)
>>> quantity
<Quantity(1.0, 'nanometer')>

The underlying value without units can be retrieved with the .m or .magnitude properties. Just make sure it’s in the units you expect first:

>>> quantity = (1.0 * unit.k_B).to_base_units()
>>> assert quantity.units == unit.kilogram * unit.meter**2 / unit.kelvin / unit.second**2
>>> quantity.magnitude
1.380649e-23

Alternatively, specify the target units of the output magnitude with .m_as:

>>> quantity = 1.0 * unit.k_B
>>> quantity.m_as(unit.kilogram * unit.meter**2 / unit.kelvin / unit.second**2)
1.380649e-23

OpenFF Units also provides the from_openmm and to_openmm functions to convert between OpenFF quantities and OpenMM quantities:

>>> from openff.units.openmm import from_openmm, to_openmm
>>>
>>> quantity = 10.0 * unit.angstrom
>>> omm_quant = to_openmm(quantity)
>>> omm_quant
Quantity(value=10.0, unit=angstrom)
>>> type(omm_quant)
<class 'openmm.unit.quantity.Quantity'>
>>> quant_roundtrip = from_openmm(omm_quant)
>>> quant_roundtrip
<Quantity(10.0, 'angstrom')>
>>> type(quant_roundtrip)
<class 'openff.units.units.Quantity'>

For more details, see the API reference.