Introduction to atomman: Unit conversions

Lucas M. Hale, lucas.hale@nist.gov, Materials Science and Engineering Division, NIST.

Disclaimers

1. Introduction

The atomman.unitconvert submodule includes tools for handling unit conversions within calculations. The unitconvert module is built around the numericalunits package, extending it with useful functions and tools.

Units are handled in the following manner:

  1. Parameters are ‘set’ using functions that take value(s) and unit fields. The functions convert the values to common working units.

  2. All calculations are performed in the compatible working units.

  3. When finished, ‘get’ functions convert values from the working units to whatever units you want.

Note that units are not tracked throughout the calculation, only conversions are performed at the beginning and end. This is advantageous as calculations and functions can be implemented without caring about the units, and there is no extra overhead. The disadvantage is that there is no explicit checking of compatible conversions, although implicit checking is possible (see Section #4, or the numericalunits documentation.)

Library Imports

[1]:
# Standard Python libraries
import datetime

# http://www.numpy.org/
import numpy as np

# https://github.com/usnistgov/atomman
import atomman as am
import atomman.unitconvert as uc

# Show atomman version
print('atomman version =', am.__version__)

# Show date of Notebook execution
print('Notebook executed on', datetime.date.today())
atomman version = 1.4.10
Notebook executed on 2023-07-28

2. Basics

2.1. unit dictionary

The unit dictionary stores all units defined by numericalunits. This keeps the namespace clean and allows for units to be accessed by string.

[2]:
print(list(uc.unit.keys()))
['pi', 'm', 'kg', 's', 'C', 'K', 'cm', 'mm', 'um', 'nm', 'pm', 'fm', 'km', 'angstrom', 'Å', 'lightyear', 'astro_unit', 'pc', 'kpc', 'Mpc', 'Gpc', 'inch', 'foot', 'mile', 'thou', 'L', 'mL', 'uL', 'nL', 'pL', 'fL', 'aL', 'kL', 'ML', 'GL', 'ms', 'us', 'ns', 'ps', 'fs', 'minute', 'hour', 'day', 'week', 'year', 'Hz', 'mHz', 'kHz', 'MHz', 'GHz', 'THz', 'PHz', 'rtHz', 'rpm', 'Hz·2π', 'mHz·2π', 'kHz·2π', 'MHz·2π', 'GHz·2π', 'THz·2π', 'PHz·2π', 'rpm·2π', 'g', 'mg', 'ug', 'ng', 'pg', 'fg', 'tonne', 'amu', 'Da', 'kDa', 'lbm', 'J', 'mJ', 'uJ', 'nJ', 'pJ', 'fJ', 'kJ', 'MJ', 'GJ', 'erg', 'eV', 'meV', 'keV', 'MeV', 'GeV', 'TeV', 'btu', 'smallcal', 'kcal', 'Wh', 'kWh', 'NA', 'mol', 'mmol', 'umol', 'nmol', 'pmol', 'fmol', 'M', 'mM', 'uM', 'nM', 'pM', 'fM', 'N', 'mN', 'uN', 'nN', 'pN', 'fN', 'kN', 'MN', 'GN', 'dyn', 'lbf', 'Pa', 'hPa', 'kPa', 'MPa', 'GPa', 'bar', 'mbar', 'cbar', 'dbar', 'kbar', 'Mbar', 'atm', 'torr', 'mtorr', 'psi', 'W', 'mW', 'uW', 'nW', 'pW', 'kW', 'MW', 'GW', 'TW', 'horsepower_imperial', 'horsepower_metric', 'Gal', 'mGal', 'uGal', 'eotvos', 'degFinterval', 'degCinterval', 'mK', 'uK', 'nK', 'pK', 'mC', 'uC', 'nC', 'Ah', 'mAh', 'A', 'mA', 'uA', 'nA', 'pA', 'fA', 'V', 'mV', 'uV', 'nV', 'kV', 'MV', 'GV', 'TV', 'ohm', 'mohm', 'kohm', 'Mohm', 'Gohm', 'Ω', 'mΩ', 'kΩ', 'MΩ', 'GΩ', 'S', 'mS', 'uS', 'nS', 'T', 'mT', 'uT', 'nT', 'G', 'mG', 'uG', 'kG', 'Oe', 'Wb', 'F', 'uF', 'nF', 'pF', 'fF', 'aF', 'H', 'mH', 'uH', 'nH', 'c0', 'mu0', 'μ0', 'eps0', 'ε0', 'Z0', 'hPlanck', 'hbar', 'ħ', 'kB', 'GNewton', 'sigmaSB', 'σSB', 'alphaFS', 'αFS', 'Rgas', 'e', 'uBohr', 'uNuc', 'aBohr', 'me', 'mp', 'mn', 'Rinf', 'Ry', 'Hartree', 'ARichardson', 'Phi0', 'KJos', 'RKlitz', 'REarth', 'g0', 'Msolar', 'MEarth']

2.2. Working units

By default, atomman defines working units in:

  • length = ‘angstrom’ = ‘Å’

  • mass = ‘amu’

  • energy = ‘eV’

  • charge = ‘e’

  • temperature = ‘K’

All other units are derived relative to these.

[3]:
print('angstrom =', uc.unit['angstrom'])
print('amu =     ', uc.unit['amu'])
print('eV =      ', uc.unit['eV'])
print('e =       ', uc.unit['e'])
print('K =       ', uc.unit['K'])
print('nm =      ', uc.unit['nm'])
print('g =       ', uc.unit['g'])
print('J =       ', uc.unit['J'])
print('ps =      ', uc.unit['ps'])
angstrom = 1.0
amu =      1.0
eV =       1.0
e =        1.0
K =        1.0
nm =       10.0
g =        6.022140762081123e+23
J =        6.241509074460763e+18
ps =       98.22694750253277

2.3. reset_units()

The working units can be altered using reset_units(). You can specify up to four out of five of length, mass, time, energy, and charge. If less than four values are given, SI units are used. Temperature is always ‘K’ when values are specified.

[4]:
# Reset working units such that length is in 'nm', mass is in 'g' and time is in 'ps'
uc.reset_units(length='nm', mass='g', time='ps')

print('angstrom =', uc.unit['angstrom'])
print('amu =     ', uc.unit['amu'])
print('eV =      ', uc.unit['eV'])
print('e =       ', uc.unit['e'])
print('K =       ', uc.unit['K'])
print('nm =      ', uc.unit['nm'])
print('g =       ', uc.unit['g'])
print('J =       ', uc.unit['J'])
print('ps =      ', uc.unit['ps'])
angstrom = 0.09999999999999999
amu =      1.6605390666e-24
eV =       1.6021766339999996e-22
e =        1.602176634e-19
K =        1.0
nm =       0.9999999999999999
g =        1.0
J =        0.0009999999999999998
ps =       1.0

Alternatively, if you call reset_units without arguments it will use the default numericalunits option and generate random working units. This can be useful for debugging code (see Section #4, or the numericalunits documentation).

[5]:
# Reset working units to random values
uc.reset_units()

print('angstrom =', uc.unit['angstrom'])
print('amu =     ', uc.unit['amu'])
print('eV =      ', uc.unit['eV'])
print('e =       ', uc.unit['e'])
print('K =       ', uc.unit['K'])
print('nm =      ', uc.unit['nm'])
print('g =       ', uc.unit['g'])
print('J =       ', uc.unit['J'])
print('ps =      ', uc.unit['ps'])
angstrom = 1.4191243424094714e-12
amu =      2.8577039616406894e-27
eV =       5.328594872559533e-26
e =        1.6325720115916934e-18
K =        0.17176160943193114
nm =       1.4191243424094715e-11
g =        0.0017209495513357104
J =        3.325847325120542e-07
ps =       3.2281474606461375e-11
[6]:
# Return working units to atomman's default
uc.reset_units(length='angstrom', mass='amu', energy='eV', charge='e')

2.4. Setting and getting static values

Static numerical values can be set and get in one of two ways:

  • set by multiplying value by units, and get by dividing by units.

  • use the set_in_units() and get_in_units() functions.

2.4.1. Direct setting and getting

[7]:
# Convert volume from angstrom^3 to nm^3
print('10 angstrom^3 =')
volume = 10 * uc.unit['angstrom']**3

print(volume / uc.unit['nm']**3, 'nm^3')
10 angstrom^3 =
0.01 nm^3
[8]:
# Show Pa = kg/(m*s^2)
print('5.5 kg/(m*s^2) =')
pressure = 5.5 * uc.unit['kg'] / (uc.unit['m']*uc.unit['s']**2)

print(pressure / uc.unit['Pa'], 'Pa')
5.5 kg/(m*s^2) =
5.5 Pa
[9]:
# Show that conversions work with arrays
stress = np.array([[1.1, 1.2, 1.3],
                   [1.2, 2.2, 2.3],
                   [1.3, 2.3, 3.3]]) * uc.unit['GPa']

print(stress / uc.unit['MPa'], 'MPa')
[[1100. 1200. 1300.]
 [1200. 2200. 2300.]
 [1300. 2300. 3300.]] MPa

2.4.2. parse()

As the above example shows, expressing complex units can get messy and unclear. The parse() function makes this easier by allowing complex units to be parsed from strings.

[10]:
# Convert volume from angstrom^3 to nm^3
print('10 angstrom^3 =')
volume = 10 * uc.parse('angstrom^3')

print(volume / uc.parse('nm^3'), 'nm^3')
10 angstrom^3 =
0.01 nm^3
[11]:
# Show Pa = kg/(m*s^2)
print('5.5 kg/(m*s^2) =')
pressure = 5.5 * uc.parse('kg/(m*s^2)')

print(pressure / uc.parse('Pa'), 'Pa')
5.5 kg/(m*s^2) =
5.5 Pa

2.4.3. set_in_units() and get_in_units()

Both functions take a value and a unit string, call parse on the unit string and perform the correct * or /.

[12]:
# Convert volume from angstrom^3 to nm^3
print('10 angstrom^3 =')
volume = uc.set_in_units(10, 'angstrom^3')

print(uc.get_in_units(volume, 'nm^3'), 'nm^3')
10 angstrom^3 =
0.01 nm^3
[13]:
# Show Pa = kg/(m*s^2)
print('5.5 kg/(m*s^2) =')
pressure = uc.set_in_units(5.5, 'kg/(m*s^2)')

print(uc.get_in_units(pressure, 'Pa'), 'Pa')
5.5 kg/(m*s^2) =
5.5 Pa
[14]:
# Show that conversions work with arrays
stress = uc.set_in_units(np.array([[1.1, 1.2, 1.3],
                                   [1.2, 2.2, 2.3],
                                   [1.3, 2.3, 3.3]]), 'GPa')

print(uc.get_in_units(stress, 'MPa'), 'MPa')
[[1100. 1200. 1300.]
 [1200. 2200. 2300.]
 [1300. 2300. 3300.]] MPa

2.5. set_literal()

Values can also be read in from strings with set_literal().

[15]:
# Convert volume from angstrom^3 to nm^3
print('10 angstrom^3 =')
volume = uc.set_literal('10 angstrom^3')

print(uc.get_in_units(volume, 'nm^3'), 'nm^3')
10 angstrom^3 =
0.01 nm^3
[16]:
# Show that conversions work with arrays
stress = uc.set_literal("""[[1.1, 1.2, 1.3],
                            [1.2, 2.2, 2.3],
                            [1.3, 2.3, 3.3]] GPa""")

print(uc.get_in_units(stress, 'MPa'), 'MPa')
[[1100. 1200. 1300.]
 [1200. 2200. 2300.]
 [1300. 2300. 3300.]] MPa

3. Data model representations

In addition to the basic conversions, unitconvert also allows for the values to be returned as and extracted from a DataModelDict. This provides a means of representing the data equivalently in either JSON or XML.

3.1. model()

Values can be converted into a structured data model using model().

[17]:
# Set length as 4 nm
length = uc.set_in_units(4, 'nm')

# Transform length into a model with units in angstrom
lmodel = uc.model(length, 'angstrom')

# Print lmodel as XML
print(lmodel.xml(full_document=False))
<value>40.0</value><unit>angstrom</unit>
[18]:
# Set list of temperatures in K
temperatures = uc.set_in_units([10,20,30,40,50], 'K')

# Transform temperatures into a model with units in K
tmodel = uc.model(temperatures, 'K')

# Print tmodel as JSON
print(tmodel.json(indent=2))
{
  "value": [
    10.0,
    20.0,
    30.0,
    40.0,
    50.0
  ],
  "unit": "K"
}

For equivalent JSON/XML representation, values with 2 or more dimensions are flattened and the shape is included in the model.

[19]:
# Set stress tensor in 'MPa'
stress = uc.set_in_units(np.array([[1.1, 0.0, 0.0],
                                   [0.0, 2.0, 0.5],
                                   [0.0, 0.5, -1.4]]), 'MPa')

# Transform stress into a model with units in kPa
smodel = uc.model(stress, 'kPa')

# Print smodel as JSON
print(smodel.json())
print()

# Print smodel as XML
print(smodel.xml(full_document=False))
{"value": [1100.0000000000002, 0.0, 0.0, 0.0, 2000.0000000000002, 500.00000000000006, 0.0, 500.00000000000006, -1400.0], "shape": [3, 3], "unit": "kPa"}

<value>1100.0000000000002</value><value>0.0</value><value>0.0</value><value>0.0</value><value>2000.0000000000002</value><value>500.00000000000006</value><value>0.0</value><value>500.00000000000006</value><value>-1400.0</value><shape>3</shape><shape>3</shape><unit>kPa</unit>

3.2. value_unit()

Values can then be read back in from a model, XML or JSON using value_unit().

[20]:
# Read lmode to set length
length = uc.value_unit(lmodel)

# Print length in nm
print(uc.get_in_units(length, 'nm'), 'nm')
4.0 nm
[21]:
# Read tmodel to set temperatures
temperatures = uc.value_unit(tmodel)

# Print temperatures in K
print(uc.get_in_units(temperatures, 'K'), 'K')
[10. 20. 30. 40. 50.] K
[22]:
# Read smodel to set stress
stress = uc.value_unit(smodel)

# Print stress in 'MPa'
print(uc.get_in_units(stress, 'MPa'), 'MPa')
[[ 1.1  0.   0. ]
 [ 0.   2.   0.5]
 [ 0.   0.5 -1.4]] MPa

3.3. error_unit()

Standard errors associated with each given value can also be included in the model, which can then be retrieved using error_unit().

[23]:
# Generate realistic-looking nonsense
xcoordinate = np.array([1, 2, 3, 4, 5]) + 0.2 * np.random.rand(5) - 0.1
xcoorderror = np.array([0.2, 0.2, 0.2, 0.2, 0.2]) + 0.02 * np.random.rand(5) - 0.01

# Assign units to nonsense
xcoordinate = uc.set_in_units(xcoordinate, 'cm')
xcoorderror = uc.set_in_units(xcoorderror, 'cm')

# Generate model of nonsense with error
model = uc.model(xcoordinate, 'm', error=xcoorderror)
print(model.json(indent=2))
{
  "value": [
    0.01004403654185057,
    0.020656878915057856,
    0.029214965887450112,
    0.040306012922664336,
    0.04954549658639971
  ],
  "error": [
    0.0019546543962901085,
    0.0019815859329668237,
    0.0019447626366320926,
    0.0019429728156574704,
    0.002085436036518905
  ],
  "unit": "m"
}

Errors can then be similarly extracted from the model using error_unit()

[24]:
# Read realistic-looking nonsense back in
print('value =', uc.get_in_units(uc.value_unit(model), 'mm'), 'mm')
print('error =', uc.get_in_units(uc.error_unit(model), 'mm'), 'mm')
value = [10.04403654 20.65687892 29.21496589 40.30601292 49.54549659] mm
error = [1.9546544  1.98158593 1.94476264 1.94297282 2.08543604] mm

4. Unit debugging

There is no explicit unit control with unitconvert, but correct unit conversions can still be debugged and tested by seeing if changing the working units changes values.

[25]:
# Print valid conversion
print('57 atm =', end=' ')
time = uc.set_in_units(57, 'atm')
print(uc.get_in_units(time, 'GPa'), 'GPa')

# Reset working units to random values
uc.reset_units()

# Print valid conversion again showing same results
print('57 atm =', end=' ')
time = uc.set_in_units(57, 'atm')
print(uc.get_in_units(time, 'GPa'), 'GPa')
57 atm = 0.005775525 GPa
57 atm = 0.005775524999999999 GPa
[26]:
# Print invalid conversion
print('57 s =', end=' ')
time = uc.set_in_units(57, 's')
print(uc.get_in_units(time, 'GPa'), 'GPa')

# Reset working units to random values
uc.reset_units()

# Print invalid conversion again showing different results
print('57 s =', end=' ')
time = uc.set_in_units(57, 's')
print(uc.get_in_units(time, 'GPa'), 'GPa')
57 s = 3.111008195072701e-09 GPa
57 s = 0.0033804807501133854 GPa