Physical units#

All variables in Scipp have a physical unit. Variables are used for coordinates, data, and attributes, therefore, all of these have a unit.

Basic Operations#

Units are encoded by the scipp.Unit class. Instances of this class can be constructed from strings:

[1]:
import scipp as sc

length = sc.Unit('m')
length
[1]:
m

scipp.Unit defines mathematical operators for combining units:

[2]:
area = length * length
area
[2]:
m^2
[3]:
volume = length * length * length
volume
[3]:
m^3
[4]:
also_volume = length ** 3
also_volume
[4]:
m^3
[5]:
sc.Unit('dimensionless') / length
[5]:
1/m
[6]:
speed = length / sc.Unit('s')
speed
[6]:
m/s

Invalid operations raise exceptions:

[7]:
speed + length
---------------------------------------------------------------------------
UnitError                                 Traceback (most recent call last)
Cell In[7], line 1
----> 1 speed + length

UnitError: Cannot add m/s and m.

It is also possible to construct composite units directly from strings:

[8]:
sc.Unit('km')
[8]:
km
[9]:
sc.Unit('m/s')
[9]:
m/s
[10]:
sc.Unit('counts')
[10]:
counts
[11]:
sc.Unit('kg*m^2/s^2')
[11]:
J

For convenience, the scipp.units module provides some frequently used units. See scipp.units for a list of those units.

[12]:
sc.units.kg
[12]:
kg
[13]:
sc.units.m / sc.units.s
[13]:
m/s
[14]:
sc.units.dimensionless
[14]:
dimensionless

Use repr to see a unit in terms of SI (plus extensions) base units:

[15]:
repr(sc.Unit('V/L'))
[15]:
'Unit(1000*m**-1*kg*s**-3*A**-1)'

This is especially helpful when it is unclear what a particular unit represents.

Constructing Variables with Units#

Variables with units can be constructed using the units argument in the constructor or in creation functions. When not specified explicitly, the unit of a variable usually (see below) defaults to dimensionless (a.k.a. one). That is, the variable is considered dimensionless in terms of units (not to be confused with array dimensions).

[16]:
# same as sc.array(dims=['x'], values=[1, 2])
# and     sc.array(dims=['x'], values=[1, 2], unit='dimensionless')
sc.array(dims=['x'], values=[1, 2], unit='one')
[16]:
Show/Hide data repr Show/Hide attributes
scipp.Variable (272 Bytes)
    • (x: 2)
      int64
      𝟙
      1, 2
      Values:
      array([1, 2])
[17]:
sc.array(dims=['x'], values=[1, 2], unit='m')
[17]:
Show/Hide data repr Show/Hide attributes
scipp.Variable (272 Bytes)
    • (x: 2)
      int64
      m
      1, 2
      Values:
      array([1, 2])
[18]:
sc.array(dims=['x'], values=[1, 2], unit=sc.units.m)
[18]:
Show/Hide data repr Show/Hide attributes
scipp.Variable (272 Bytes)
    • (x: 2)
      int64
      m
      1, 2
      Values:
      array([1, 2])
[19]:
sc.arange('x', 0, 3, unit=sc.units.s)
[19]:
Show/Hide data repr Show/Hide attributes
scipp.Variable (280 Bytes)
    • (x: 3)
      int64
      s
      0, 1, 2
      Values:
      array([0, 1, 2])

Scalars can also be constructed using multiplication or division of a number and a unit (in addition to scipp.scalar):

[20]:
1.2 * sc.Unit('kg/m^3')
[20]:
Show/Hide data repr Show/Hide attributes
scipp.Variable (264 Bytes)
    • ()
      float64
      kg/m^3
      1.2
      Values:
      array(1.2)
[21]:
3.4 / sc.units.K
[21]:
Show/Hide data repr Show/Hide attributes
scipp.Variable (264 Bytes)
    • ()
      float64
      1/K
      3.4
      Values:
      array(3.4)

Variables Without Units#

It is not always meaningful to assign a unit to a variable. For example, what is the unit of a string or a truth value? For this reason, Scipp allows variables to have no unit by setting unit=None:

[22]:
sc.array(dims=['x'], values=[2, 4, 6], unit=None)
[22]:
Show/Hide data repr Show/Hide attributes
scipp.Variable (280 Bytes)
    • (x: 3)
      int64
      2, 4, 6
      Values:
      array([2, 4, 6])

For non-numeric dtypes, the unit defaults to None:

[23]:
sc.array(dims=['x'], values=[False, True, False])
[23]:
Show/Hide data repr Show/Hide attributes
scipp.Variable (259 Bytes)
    • (x: 3)
      bool
      False, True, False
      Values:
      array([False, True, False])
[24]:
sc.scalar('a string')
[24]:
Show/Hide data repr Show/Hide attributes
scipp.Variable (272 Bytes)
    • ()
      string
      a string
      Values:
      'a string'

Indices are also non-physical quantities, so they should typically be defined without a unit, too. To help with this, Scipp provides sc.index:

[25]:
sc.index(123)
[25]:
Show/Hide data repr Show/Hide attributes
scipp.Variable (264 Bytes)
    • ()
      int64
      123
      Values:
      array(123)

Variables without units can interoperate like variables with units. But the two groups cannot be combined:

[26]:
sc.scalar(1, unit='one') * sc.scalar(2, unit=None)
---------------------------------------------------------------------------
UnitError                                 Traceback (most recent call last)
Cell In[26], line 1
----> 1 sc.scalar(1, unit='one') * sc.scalar(2, unit=None)

UnitError: Cannot multiply with operand of unit 'None'.

Supported Units#

Scipp supports a great number of units through LLNL’s Units library. See in particular Defined Units.

INFO

The LLNL/Units library is considered an implementation detail of Scipp. Using SI units is safe but other unit systems should be used with discretion. This applies especially to non-standard units like LLNL/Unit’s custom (counting) units.

Base Units#

All SI base units are supported with the following names:

Name

Unit

‘m’

meter

‘s’

second

‘kg’

kilogram

‘K’

kelvin

‘A’

ampere

‘mol’

mole

‘cd’

candela

In addition, the following base units are supported for cases not covered by SI.

name

Unit

‘rad’

radian

‘count’

single object counting

Derived units#

Many derived units can also be specified as arguments to sc.Unit. Some examples are

Name

Unit

‘Hz’

hertz

‘J’

joule

‘V’

volt

‘W’

watt

‘angstrom’ / ‘Å’

ångström

‘eV’

electron volt

‘L’

liter

‘min’

minute

‘D’ / ‘day’

day

Units can be modified with SI prefixes, for instance

[27]:
print(sc.Unit('mm'), sc.Unit('microsecond'),
      sc.Unit('micro s'), sc.Unit('us'), sc.Unit('MJ'))
mm µs µs µs MJ

You can also specify exponents for units or exponentiate the Unit object:

[28]:
print(sc.Unit('m^2'), sc.Unit('m**2'), sc.Unit('m')**2)
m^2 m^2 m^2

Conversion Between Units of Different Scales#

Data can be converted between compatible units using sc.to_unit. Only conversions between units of the same physical dimensions are possible.

[29]:
sc.to_unit(1.0 * sc.units.m, 'mm')
[29]:
Show/Hide data repr Show/Hide attributes
scipp.Variable (264 Bytes)
    • ()
      float64
      mm
      1000.0
      Values:
      array(1000.)
[30]:
sc.to_unit(1.0 * sc.Unit('parsec'), 'm')
[30]:
Show/Hide data repr Show/Hide attributes
scipp.Variable (264 Bytes)
    • ()
      float64
      m
      3.085678e+16
      Values:
      array(3.085678e+16)
[31]:
sc.to_unit(3.14 * sc.Unit('m/s'), 'km/h')
[31]:
Show/Hide data repr Show/Hide attributes
scipp.Variable (264 Bytes)
    • ()
      float64
      km/hr
      11.303999999999998
      Values:
      array(11.304)
[32]:
sc.to_unit(1.0 * sc.Unit('s'), 'm')
---------------------------------------------------------------------------
UnitError                                 Traceback (most recent call last)
Cell In[32], line 1
----> 1 sc.to_unit(1.0 * sc.Unit('s'), 'm')

File ~/work/scipp/scipp/.tox/docs/lib/python3.9/site-packages/scipp/core/unary.py:69, in to_unit(x, unit, copy)
     35 def to_unit(
     36     x: _cpp.Variable, unit: _Union[_cpp.Unit, str], *, copy: bool = True
     37 ) -> _cpp.Variable:
     38     """Convert the variable to a different unit.
     39
     40     Raises a :class:`scipp.UnitError` if the input unit is not compatible
   (...)
     67       <scipp.Variable> ()    float64             [mm]  1200
     68     """
---> 69     return _call_cpp_func(_cpp.to_unit, x=x, unit=unit, copy=copy)

File ~/work/scipp/scipp/.tox/docs/lib/python3.9/site-packages/scipp/core/_cpp_wrapper_util.py:15, in call_func(func, out, *args, **kwargs)
     13     return data_group_nary(func, *args, **kwargs)
     14 if out is None:
---> 15     return func(*args, **kwargs)
     16 else:
     17     return func(*args, **kwargs, out=out)

UnitError: Conversion from `s` to `m` is not valid.

Unit Aliases#

It is possible to define custom aliases for units. This can be used to

  • guide string formatting to prefer a certain unit, e.g. angstrom over nm

  • define domain specific units that can be expressed in terms of other units to

    • guide string formatting

    • construct units from strings with custom names

Prioritizing Units in String Formatting#

When dealing with crystals or molecules, it is often convenient to use angstrom as a unit. But by default, string formatting tends to prefer different bases in composite units. For example

[33]:
sc.Unit('us/angstrom**2')
[33]:
9290304000000s/ft^2

This result is not very useful. We can make the formatter prefer angstrom by defining it as an alias of itself (or alternatively of '10^-10 m'):

[34]:
sc.units.aliases['angstrom'] = 'angstrom'
sc.Unit('us/angstrom**2')
[34]:
µs/angstrom^2

Note that ‘angstrom’ is predefined and simple units involving it, such as sc.Unit('angstrom/s') are formatted properly without the alias. But the alias improves more complicated cases like the one above.

Aliases are global and stay in effect until they are removed. This can be done using sc.units.aliases.clear() to remove all aliases or, to remove only one, using

[35]:
del sc.units.aliases['angstrom']
sc.Unit('us/angstrom**2')
[35]:
9290304000000s/ft^2

Alternatively, a context manager can be used to remove aliases automatically:

[36]:
with sc.units.aliases.scoped(angstrom='angstrom'):
    print(sc.Unit('us/angstrom**2'))
print(sc.Unit('us/angstrom**2'))
µs/angstrom^2
9290304000000s/ft^2

But note that the context manager uses the global alias table and affects code outside of the context. Details are explained in scipp.units.aliases.scoped.

See scipp.units.UnitAliases for the full API of sc.units.aliases.

Defining New Units#

It is possible to define completely new units as aliases as long as they can be expressed in terms of other units. For example, define an speed unit:

[37]:
sc.units.aliases['speed'] = 'm/s'

In practice, a unit alias might refer to a characteristic speed, length, or time in a concrete physical system. Or it might refer to a customary unit in a scientific field.

After defining the alias, the string formatter prioritizes ‘speed’ over ‘m/s’ when appropriate:

[38]:
print(sc.Unit('m/s'))
print(sc.Unit('km/s'))
print(sc.Unit('kg*m/s'))
speed
kspeed
N*s

And we can also construct units using ‘speed’ as an argument:

[39]:
print(sc.Unit('speed'))
print(sc.Unit('kg*mspeed**2'))
speed
µJ

This also works in variables:

[40]:
sc.scalar(4, unit='speed')
[40]:
Show/Hide data repr Show/Hide attributes
scipp.Variable (264 Bytes)
    • ()
      int64
      speed
      4
      Values:
      array(4)

Using repr, we can see that ‘speed’ is not a fundamentally new unit but simply expressed in terms of ‘m’ and ‘s’:

[41]:
repr(sc.Unit('speed'))
[41]:
'Unit(m*s**-1)'

Defining Scaled Units#

In the previous example, ‘speed’ was defined as a combination of ‘m’ and ‘s’. It is also possible to define aliases of scaled units. Examples of scaled units are millisecond, hour, or the previously used angstrom. These units work by encoding a scale factor (‘multiplier’) in the unit. This multiplier can be any floating point number, so for example, we can define a dog year as 52 days or 4492800 seconds:

[42]:
sc.units.aliases['dogyear'] = sc.scalar(4492800, unit='s')

Now we can use ‘dogyear’ as a unit:

[43]:
sc.Unit('dogyear')
[43]:
dogyear

We can see that the above is indeed a scaled unit by using repr:

[44]:
repr(sc.Unit('dogyear'))
[44]:
'Unit(4.4928e+06*s)'

Alternatively, we can specify the multiplier in the unit directly:

[45]:
sc.Unit('4492800s') == sc.Unit('dogyear')
[45]:
True

We can use this to define the same alias as before but without going through a Scipp variable:

[46]:
sc.units.aliases.clear()
sc.units.aliases['dogyear'] = '4492800s'
[47]:
repr(sc.Unit('dogyear'))
[47]:
'Unit(4.4928e+06*s)'

Note that the unit multiplier is not the same as a value in a variable:

[48]:
var = sc.scalar(2, unit='dogyear')
var
[48]:
Show/Hide data repr Show/Hide attributes
scipp.Variable (264 Bytes)
    • ()
      int64
      dogyear
      2
      Values:
      array(2)

Removing the alias reveals the multiplier (hover the mouse over the unit if it is abbreviated):

[49]:
del sc.units.aliases['dogyear']
var
[49]:
Show/Hide data repr Show/Hide attributes
scipp.Variable (264 Bytes)
    • ()
      int64
      4492800s
      2
      Values:
      array(2)

The multiplier can be moved into the value of the variable by converting to seconds:

[50]:
var.to(unit='s')
[50]:
Show/Hide data repr Show/Hide attributes
scipp.Variable (264 Bytes)
    • ()
      int64
      s
      8985600
      Values:
      array(8985600)

Conversion also works the other way around:

[51]:
sc.units.aliases['dogyear'] = '4492800s'
sc.scalar(8985600, unit='s').to(unit='dogyear')
[51]:
Show/Hide data repr Show/Hide attributes
scipp.Variable (264 Bytes)
    • ()
      int64
      dogyear
      2
      Values:
      array(2)