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]:
- (x: 2)int64𝟙1, 2
Values:
array([1, 2])
[17]:
sc.array(dims=['x'], values=[1, 2], unit='m')
[17]:
- (x: 2)int64m1, 2
Values:
array([1, 2])
[18]:
sc.array(dims=['x'], values=[1, 2], unit=sc.units.m)
[18]:
- (x: 2)int64m1, 2
Values:
array([1, 2])
[19]:
sc.arange('x', 0, 3, unit=sc.units.s)
[19]:
- (x: 3)int64s0, 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]:
- ()float64kg/m^31.2
Values:
array(1.2)
[21]:
3.4 / sc.units.K
[21]:
- ()float641/K3.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]:
- (x: 3)int642, 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]:
- (x: 3)boolFalse, True, False
Values:
array([False, True, False])
[24]:
sc.scalar('a string')
[24]:
- ()stringa 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]:
- ()int64123
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]:
- ()float64mm1000.0
Values:
array(1000.)
[30]:
sc.to_unit(1.0 * sc.Unit('parsec'), 'm')
[30]:
- ()float64m3.085678e+16
Values:
array(3.085678e+16)
[31]:
sc.to_unit(3.14 * sc.Unit('m/s'), 'km/h')
[31]:
- ()float64km/hr11.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.10/site-packages/scipp/core/unary.py:69, in to_unit(x, unit, copy)
35 def to_unit(
36 x: VariableLikeType, unit: _cpp.Unit | str, *, copy: bool = True
37 ) -> VariableLikeType:
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.10/site-packages/scipp/core/_cpp_wrapper_util.py:27, in call_func(func, *args, **kwargs)
25 return data_group_nary(func, *args, **kwargs)
26 if out is None:
---> 27 return func(*args, **kwargs)
28 else:
29 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]:
- ()int64speed4
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]:
- ()int64dogyear2
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]:
- ()int644492800s2
Values:
array(2)
The multiplier can be moved into the value of the variable by converting to seconds:
[50]:
var.to(unit='s')
[50]:
- ()int64s8985600
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]:
- ()int64dogyear2
Values:
array(2)