.. _units-in-scb:
.. create a gold color role
.. raw:: html
.. role:: gold
.. create a role that makes bolded text colored blue as well
.. raw:: html
.. role:: bold
========================================
:gold:`Working With Units in Scarabaeus`
========================================
Last revised by Z. Ellis on 2025 JAN 15
--------------------
:bold:`Introduction`
--------------------
Many aspects of working in Scarabaeus necessitate unitized values. To support this end, Scarabaeus employs
multiple classes that work in conjuction to make performing these sorts of calculations as simple
as possible. Working with units in Scarabaeus should feel as natural as working with them
"by hand".
In this document, we will walk through a section of the data hierarchy (see below) outlined in
:ref:`Scarabaeus Data Flow ` that defines how unitized values are structured within Scarabaeus,
as well as best practices for their utilization, their importance in performing calculations, and examples
of their most common use-cases.
.. image:: ../../../images/units_dataflow_dark.png
:class: only-dark
:align: center
.. image:: ../../../images/units_dataflow_light.png
:class: only-light
:align: center
----
----------------------------
:bold:`The Dimensions Class`
----------------------------
What are Dimensions
===================
The foundational unit class in Scarabaeus is the ``Dimensions`` class. ``Dimensions`` objects represent dimensionality,
or fundamental physical quantities — they do not contain any scaling factors, only relational powers to Scarabaeus'
four base units:
#. Mass
#. Length
#. Time
#. Angle
These quantities are derived from three of the SI unit system's base units: Mass from the kilogram,
Length from the meter, and Time from the second. Additionally, a fourth "dimension" — Angle — is
included due to its relevance to many unitary applications despite the inherent non-dimensionality
of angles.
The ``Dimensions`` class is rarely invoked directly while working in Scarabaeus, but it is necessary for
all operations involving units. From the data flow image in the introduction, we can see that ``Dimensions``
are one of the two components required to construct a ``Unit``, the other being scalings which we will
explore in the next section. To begin, let's walk through the construction process of a ``Dimensions`` object.
Read through the :ref:`full Dimensions documentation ` for a complete list of
every method and property.
----
Constructing Dimensions
=======================
The ``Dimensions`` constructor is very small, with only a single input: ``dim_pwrs``. This is a
A 1x4 vector of integers representing the power relations to each of the four base dimensions as noted above.
For example, Length would be passed as ``[0, 1, 0, 0]``, Area as ``[0, 2, 0, 0]``, and Volume as ``[0, 3, 0, 0]``.
See the code below for a few more examples.
.. code-block:: python
import scarabaeus as scb
# construct a dimension of Length
dim_length = scb.Dimensions([0, 1, 0, 0])
# a dimension of Angle
dim_angle = scb.Dimensions([0, 0, 0, 1])
# or a dimension of Area
dim_area = scb.Dimensions([0, 2, 0, 0])
## we can also create compound dimensions
# for example Force
dim_force = scb.Dimensions([1, 1, -2, 0])
# or even dimensionless objects
dimless = scb.Dimensions([0, 0, 0, 0])
In the above code, we also created ``Dimensions`` objects that span multiple dimensions like Force, defined as Mass
multiplied by Length per Time squared. Angles do not contribute to this dimension and so are given a zero power.
Another important case to notice is the dimensionless ``Dimensions`` object, created when all four dimensional
powers are set to zero.
----
Data Held in Dimensions
=======================
Aside from the dimensional powers we passed to the object upon construction, there is one more
piece of information held within a ``Dimensions`` object: its name.
A ``Dimensions``' name property is dynamically created during initialization based upon the given
dimensional powers. It is the dimensional power array written in words. To better explain,
let's look at a few examples of the name property for the ``Dimensions`` objects we created
in the previous section:
.. code-block:: python
# length is automatically assigned
print(dim_length.name)
>>> 'Length'
# known compound dimensions like Force are also recognized
print(dim_force.name)
>>> 'Force'
## names are automatically generated if they do not match a
## known dimension
# create an unnamed dimension
dim_no_name = scb.Dimensions([3, 5, -2, 0])
# the dimension's name will be given in words
print(dim_no_name.name)
>>> 'cubic Mass times Length to the 5th per square Time'
.. note:: The ``name`` property is returned by the ``Dimensions`` class' ``__repr__`` method and so simply printing a ``Dimensions`` object is sufficient to access its name. The ``name`` property is specifically accessed in the above code to highlight its storage within the class.
Notice that for cases where the constructed dimension does not have a defined named, it is given
one using the powers in its dimensional array. To see a list of all named dimensions and their
respective powers, call ``Dimensions.disp_named_dims()``. To define new named dimensions,
call ``Dimensions.def_new_named_dim()``.
----
-----------------------
:bold:`The Units Class`
-----------------------
What are Units
==============
Now that we've defined the ``Dimensions`` class, it's time to use them to build a ``Units`` object.
Where ``Dimensions`` are not used directly very often, ``Units`` crop up in almost every script,
acting as Scarabaeus' representation of, from `NIST `_:
"...a particular physical quantity, defined and adopted by convention, with which other particular quantities of the same kind are compared to express their value."
From the diagram at the beginning of this document, we can see that the next step up from a
``Dimensions`` object is the ``Units`` object, which contains not only the dimensional information
held by a ``Dimensions`` object, but additionally each dimension's respective scaling.
These scalings are applied with respect to Scarabaeus' four base units, similar to its four base dimensions:
#. Grams
#. Meters
#. Seconds
#. Radians
Each index of the scaling vector corresponds to its respective base unit. Read through the :ref:`full Units documentation `
for a complete list of every method and property.
----
Constructing Units
==================
For the vast majority of cases, you should :bold:`not` instantiate ``Units`` objects with their usual
constructor. We will cover the preferred way shortly, but it is useful to quickly run through the "nitty gritty"
method first.
Where the ``Dimensions`` class takes in a 1x4 vector of dimensional powers, the ``Units`` class also connects
another 1x4 vector of floats.
Also, notice that, by :ref:`Scarabaeus' coding conventions `, we instantiate our units under the
"Generate Units" header.
.. code-block:: python
import scarabaeus as scb
#------------------#
# Generate Units #
#------------------#
# first create a dimension of length
dim_length = scb.Dimensions([0, 1, 0, 0])
# and then apply a scaling to that dimension (1000 m = 1 km)
km = scb.Units(dim_length, [0, 1e3, 0, 0])
As noted earlier, the most common (and efficient) way to create units is to request them from the ``get_units()`` method.
This method takes in a string or a list of strings and returns a ``Units`` object for each query in the list by performing
a similar set of steps we took when creating a unit the hard way above. Input strings follows the same format as
any metric unit: [prefix][base unit].
For example, passing the string ``'m'`` will first create a ``Dimensions`` object of Length, and then apply
a scaling of one to it, returning a Scarabaeus representation of a meter. To create a kilometer representation,
we append the the kilo prefix as ``'km'``. The method recognizes it and instead of scaling by one it scales
by one thousand.
.. code-block:: python
# request a single unit
m = scb.Units.get_units('m')
# request multiple units in one line
km, rad, N, sec, unitless = scb.Units.get_units(['km', 'rad', 'N', 'sec', 'unitless'])
# request one of get_unit's predefined unit sets
kg, km, sec, rad, deg, N, mu = scb.Units.get_units('common')
.. note:: Remember that, to ensure that your script runs as efficiently as possible, it is important to request only the units you need. ``get_units()`` sets are generally used for convenience during development. Once you've finished development on a script, don't forget to replace the unit set line with a request for only the units you utilize in your code.
Notice that ``get_units()`` supports requests for more than just base units with prefixes. Many compound and
special units, like Newtons or days, can also be fetched. It is important to note, however, that not all of these units
interact with metric prefixes, for example a milliday. To see a list of all recognized special units, as well as their
compatibility with prefixes, call the ``disp_named_units()`` method. To define a new named unit, as well as its
prefix compatibility, use the ``def_new_named_units()`` method.
.. code-block:: python
# millidays don't exist -> an error is raised
day = scb.Units.get_units('mday')
>>> ValueError: The requested unit [day] is incompatible with the given prefix.
.. note:: Currently ``get_units()`` does not support requests for unnamed compound units like ``'km/s'``. Instead you would need to request kilometers and seconds separately and create km/s yourself by division.
Finally, it is important to note that, just like when working "by hand", you can perform
mathematical operations on units in Scarabaeus. These can be done in-line with Python's
usual math operations, as well as to set a new variable for instances where you plan to
reuse some compound unit multiple times.
.. code-block:: python
# square a unit in-line
print(km**2)
>>> 'km^2'
# create a new unit by performing operations on other units
km_per_sec = km / sec
print(km_per_sec)
>>> 'km/s'
----
Data Held in Units
==================
As we've seen while constructing a ``Units`` object, each instance contains the dimensions of the unit
and its scalings. Just like ``Dimensions``, it also holds a string describing its name which is
dynamically generated during construction.
.. code-block:: python
# all of the properties contained within a Units object
print(f'The name of the unit: {km.name}')
print(f'Its dimensions : {km.dimensions}')
print(f'Its scalings : {km.scales}')
>>> The name of the unit: km
>>> Its dimensions : Length
>>> Its scalings : [ 0 1000 0 0]
This name generation extends to compound units as well:
.. code-block:: python
# create a compound unit
some_unit = km**3 / (rad * sec**2)
print(f'The name of the unit: {some_unit.name}')
print(f'Its dimensions : {some_unit.dimensions}')
print(f'Its scalings : {some_unit.scales}')
>>> The name of the unit: km^3/(sec^2*rad)
>>> Its dimensions : Cubic Length per Square Time per Angle
>>> Its scalings : [ 0. 1000. 1. 1.]
.. note:: Name generation occurs in order from left to right that Scarabaeus base units are defined in: g, m, sec, rad. This means that even though we gave the denominator in rad * sec^2, the name returns as sec^2 * rad. These are equivalent, but the behavior is still worth noting.
----
-----------------------------
:bold:`The ArrayWUnits Class`
-----------------------------
What are ArrayWUnits
====================
It's finally time to attach values to the units we've spent the last two sections building up. We'll
do this with Scarabaeus' ``ArrayWUnits`` (read "array with units") class, or AWU for short.
With our ``Units`` created once at the top of a Scarabaeus script, we then assign numerical values to them using
AWU's, which act as our "numbers". Consider how you might solve the below equation:
.. math::
x = \frac{ab}{c}
Where :math:`a = 1 kg`, :math:`b = 1 m`, and :math:`c = 1 s^2`. You would probably first separate the numerical
values from each variable and calculate the resulting number, then separate the units of each variable and calculate
the resulting unit, and finally recombine the value and the unit to get :math:`1 kg\frac{m}{s^2}`. AWU's perform
this same process for you in your code, making them extremely powerful for doing physical calculations!
This same principle can be taken a step further and applied to matrix math:
.. math::
M = A * B
.. math::
\begin{split}
A = \begin{bmatrix}
1 & 0 & 0 \\
0 & 1 & 0 \\
0 & 0 & 1
\end{bmatrix}
\quad \quad B = \begin{bmatrix}
1 & 0 & 0 \\
0 & 1 & 0 \\
0 & 0 & 1
\end{bmatrix}
\end{split}
Where each element of :math:`A` has units of :math:`m` and each element of :math:`B` has units of :math:`Hz`. The solution
would be a 3x3 identity matrix with units :math:`\frac{m}{s}`.
To ensure flexibility between operations like the first example and the second, ``ArrayWUnits`` treats :bold:`all values as matrices`.
For an AWU, a scalar value is simply a 1x1 matrix.
Read through the :ref:`full ArrayWUnits documentation ` for a complete list of
every method and property.
----
Constructing ArrayWUnits
========================
To create an AWU, we must pass in its values, as well as the units we want to assign to them. There
are two cases for this:
#. Homogeneous unit input:
* Each numerical value is given in the same unit.
* Scalar values are necessarily homogeneous.
#. Heterogeneous unit input:
* One or more numerical values are given in different units.
* Non-scalar values do not have to be heterogeneous.
We will begin by constructing an AWU of the first case — one with homogeneous units. Note that we
must also generate ``Units`` to pass into our constructor.
.. code-block:: python
import scarabaeus as scb
import numpy as np
#------------------#
# Generate Units #
#------------------#
kg, km, sec, rad, unitless = scb.Units.get_units(['kg', 'km', 'sec', 'rad', 'unitless'])
# create a scalar AWU (homogeneous)
one_km = scb.ArrayWUnits(1, kg)
# create a homogeneous vector AWU
km_vec = scb.ArrayWUnits([[1], [2], [3]], km /sec)
# create a unitless AWU without needing to define unitless unit
untlss_awu = scb.ArrayWUnits(1, None)
This was the simpler of the two cases because we can easily assign the same unit to all values.
We were able to create a basic one kilogram, as well as a column vector with units of kilometers
per second. Also notice that we didn't need to define the unitless ``Units`` object to create a unitless
value. This is a special case where passing no unit defaults to a unitless AWU. We still created a unitless
object for the next section.
Now let's look at the slightly more complicated second case — a non-homogeneous (heterogeneous)
AWU. Since AWU's accept either lists or `Numpy arrays `_,
we'll use Numpy to more easily create both of our inputs.
.. code-block:: python
# create matrix defining units
non_hom_uns = np.array([[km , unitless, unitless],
[unitless, rad , unitless],
[unitless, unitless, sec ]])
# pass with values
non_hom_mat = scb.ArrayWUnits(np.eye(3), non_hom_uns)
The above code creates a matrix:
.. math::
\begin{bmatrix}
1 [km] & 0 & 0 \\
0 & 1 [rad] & 0 \\
0 & 0 & 1 [sec]
\end{bmatrix}
Non-homogeneous AWU's tend to represent things like state vectors while homogeneous AWU's tend to represent
things like position or velocity vectors.
----
Data Held in ArrayWUnits
========================
``ArrayWUnits`` objects contain much more information than their simpler constituents ``Dimensions`` and
``Units``. Throughout the following, we will go over each of these properties using a scalar and a
matrix AWU.
.. code-block:: python
# scalar AWU
km_1x1 = scb.ArrayWUnits(1, km)
# matrix AWU
km_3x3 = scb.ArrayWUnits(np.eye(3), km)
The "numbers" of the AWU are stored in the ``values`` property, while the associated units are stored
in the ``units`` property. Both of these properties are stored internally as `Numpy arrays `_,
but in the case of a scalar (1x1) AWU, they are converted to their respective singular data types when accessed
externally.
.. code-block:: python
# internally, values and units are stored as np.arrays
print(f'Internal values: {km_1x1._values} have type {type(km_1x1._values)}')
print(f'Internal units : {km_1x1._units} have type {type(km_1x1._units)}')
# externally, for scalar AWU's, they're returned as their element objects
print(f'External values: {km_1x1.values} have type {type(km_1x1.values)}')
print(f'External units : {km_1x1.units} have type {type(km_1x1.units)}')
>>> Internal values: 1 have type np.array
>>> Internal units : km have type np.array
>>> External values: 1 have type int
>>> External units : km have type Units
This is to ensure parity within the class itself while also allowing for scalar AWU's to act like
scalars instead of 1x1 matrices.
Additionally, AWU's also contain information about their number of columns and rows by the ``shape`` property,
the number of elements they contain by the ``size`` property, and whether or not all of its values
share the same unit or not by the ``homogeneous_units`` property.
----
-----------------------------
:bold:`Examples of Use-Cases`
-----------------------------
Now that we've gone through all of the classes that represent units in Scarabaeus, it's time
to look at a few common use-cases for them. Because the units are very rarely useful without numerical values attached,
this section will focus almost solely on utilizing ``ArrayWUnits``.
Use-Case #1 - Manipulating Units
================================
Before we look into AWU use-cases, let's quickly look at how we can most effectively use ``Units`` in
Scarabaeus. To make things as simple as possible, we'll use the common unit set from ``get_units()``. Remember
that this set is normally only used for development as it is unlikely you will require all of these units in your
script and creating unused units adds, albeit small, unnecessary overhead to your code.
.. code-block:: python
#------------------#
# Generate Units #
#------------------#
kg, km, sec, rad, deg, N, mu = scb.Units.get_units('common')
## you can multiply units by numpy arrays or lists to:
# make column vectors
cm_m_km_col = m * np.array([[10**-2], [1], [10**3]])
print(cm_m_km_col)
>>> [[cm]
[m]
[km]]
# or row vectors
cm_m_km_row = m * [10**-2, 1, 10**3]
print(cm_m_km_row)
>>> [cm m km]
three_kms = km * np.ones(3)
print(three_kms)
>>> [km km km]
# as well as matrices
cm_m_km_mat = m * (np.array([10**-2, 1, 10**3]).transpose() * np.eye(3))
print(cm_m_km_mat)
>>> [[cm 0.0 0.0]
[0.0 m 0.0]
[0.0 0.0 km]]
After we generated our units, we also created a column and row vector, as well as a matrix, of units.
You can see how this makes our lives easier when defining AWU's. As another example, let's use concatenation
to create a 6x1 column vector with position and velocity units:
.. code-block:: python
pos_vel_vec = np.concatenate((km * np.ones((3, 1)), (km/sec) * np.ones((3, 1))))
print(pos_vel_vec)
>>> [[km]
[km]
[km]
[km/sec]
[km/sec]
[km/sec]]
As you can see, ``Units`` objects interact with both lists and numpy arrays just like a number would. We can use this
to our advantage, especially when working with AWU's.
Use-Case #2 - Utilizing Enclosed Data in an AWU
===============================================
Earlier in this document we walked through all of the different pieces of data held within an AWU, but
we didn't talk about why they were useful. In this first use-case, we will explore a few different patterns
that are common when working with AWU's and how to utilize the data held within them to make coding
easier for ourselves in Scarabaeus. We will use the ``Units`` generated in the previous use-case. The first
pattern will be input validation:
.. code-block:: python
# set up two AWU's for the examples
awu_one = scb.ArrayWUnits(1, km)
awu_two = scb.ArrayWUnits([[1], [2], [3], [4], [5], [5]], pos_vel_vec)
#-----------------------------#
# access the values of an AWU #
#-----------------------------#
if awu_one.values == awu_two.values:
print('Values equal for one and two.')
else:
print('Values not equal for one and two.')
>>> 'Values not equal for one and two.'
#----------------------------#
# access the units of an AWU #
#----------------------------#
if awu_one.units == awu_two.units:
print('Units equal for one and two.')
else:
print('Units not equal for one and two.')
>>> 'Units not equal for one and two.'
# this is useful when you need to grab the units from one AWU and put them in another as well
awu_three = scb.ArrayWUnits(100, rad)
new_awu = scb.ArrayWUnits(np.eye(3), awu_three.units)
Use-Case #3 - Appending to AWU's
================================
Another useful pattern for AWU's is the ``append()`` function, which, as the name entails,
appends a given AWU to another AWU. It is important to note that depending on the inputs given,
``ArrayWUnits.append()`` will funciton differently. See below:
.. code-block:: python
#------------------#
# Generate Units #
#------------------#
kg, km, unitless = scb.Units.get_units(['kg', 'km', 'unitless'])
#--------------------#
# append to a vector #
#--------------------#
one = scb.ArrayWUnits([1, 2, 3], kg)
two = scb.ArrayWUnits(4, kg)
print(f'Before appending:\n'
f'{one}\n'
f'{two}')
one = one.append(two)
print(f'After appending:\n'
f'{one}')
>>> Before appending:
[1. 2. 3.] [kg ... kg]
4.0 kg
>>> After appending:
[1. 2. 3. 4.] [kg ... kg]
#--------------------#
# append to a matrix #
#--------------------#
three_mat = np.array([[km , unitless, unitless],
[unitless, km , unitless],
[unitless, unitless, km ]])
three = scb.ArrayWUnits(np.eye(3), three_mat)
four_row = scb.ArrayWUnits(np.ones((1, 3)), km * np.ones((1, 3)))
four_col = scb.ArrayWUnits(np.ones((3, 1)), km * np.ones((3, 1)))
print(f'Before appending:\n'
f'three :\n{three}\n\n'
f'four_row:\n{four_row}\n\n'
f'four_col:\n{four_col}')
>>> Before appending:
three:
[[1. 0. 0.]
[0. 1. 0.]
[0. 0. 1.]] [non-homogeneous]
four_row:
[[1. 1. 1.]] [km ... km]
four_col:
[[1.] [[km]
[1.] [⋮ ]
[1.]] [km]]
# unspecified axis flattens both and then appends
three_no_ax = three.append(four_row)
print(three_no_ax)
>>> [1. 0. 0. 0. 1. 0. 0. 0. 1. 1. 1. 1.] [non-homogeneous]
# along first axis to place below
three_ax_0 = three.append(four_row, 0)
print(three_ax_0)
>>> [[1. 0. 0.]
[0. 1. 0.]
[0. 0. 1.]
[1. 1. 1.]] [non-homogeneous]
# along second axis to place beside
three_ax_1 = three.append(four_col, 1)
print(three_ax_1)
>>> [[1. 0. 0. 1.]
[0. 1. 0. 1.]
[0. 0. 1. 1.]] [non-homogeneous]
Use-Case #4 - Converting AWU's
==============================
There are many instances where you may want to convert an AWU from one unit to another. Instead of
having to find and apply conversion ratios yourself, you can use AWU's ``convert_to()``
method.
.. code-block:: python
#------------------#
# Generate Units #
#------------------#
sec, day, cm, m, km = scb.Units.get_units(['sec', 'day', 'cm', 'm', 'km'])
#-------------------------#
# converting scalar AWU's #
#-------------------------#
# create a unit for a week
week = scb.ArrayWUnits(7, day)
# convert it to seconds
week_sec = week.convert_to(sec)
print(f'A week in seconds = {week_sec}')
>>> A week in seconds = 604800.0 sec
# and back to days
week = week_sec.convert_to(day)
print(f'A week in days = {week}')
>>> A week in days = 7.0 day
#-----------------------------#
# converting non-scalar AWU's #
#-----------------------------#
# array of different units with same dimensions
cm_m_km = scb.ArrayWUnits([1, 1, 1], [cm, m, km])
print(f'Before conversion:\n'
f'{cm_m_km}')
>>> Before conversion:
[1. 1. 1.] [non-homogeneous]
# convert all to kilometers
all_km = cm_m_km.convert_to(km)
print(f'After conversion:\n'
f'{all_km}')
>>> After conversion:
[1.e-05 1.e-03 1.e+00] [km ... km]
This conversion method can also be used in conjunction with some of the input validation we went over
in Use-Case #2 to enforce a specific unit type when performing calculations. Suppose we have a function
``compute_schwarzschild_radius()`` that requires our calculations be performed using
`CGS units `_.
The equation for the Schwarzschild radius of a black hole is:
.. math::
r_s = \frac{2GM}{c^2}
Where :math:`G` and :math:`c` are physical constants whose values are irrelevant to our example. The important fact
to note is that :math:`M` must be measured in grams for our case and so we want to make sure any input we receive goes
into our equation as grams. We can do this by first ensuring that our input is of dimension Mass (we can't convert
Angular units to Mass units). Once we know that the conversion is possible, we can then use ``convert_to()`` to
guarantee that no matter what unit we received, we will be performing our calculations with grams.
.. code-block:: python
#------------------#
# Generate Units #
#------------------#
g, cm, sec = scb.Units.get_units(['g', 'cm', 'sec'])
def compute_schwarzschild_radius(mass : scb.ArrayWUnits) -> scb.ArrayWUnits:
"""
Example method to illustrate unit enforcement.
Parameters
----------
mass : ArrayWUnits of dimension Mass
The mass of the object.
Returns
-------
r_s : ArrayWUnits of dimension Length
The computed Schwarzschild radius for the given mass.
"""
#-----------------------------------#
# input validation and conditioning #
#-----------------------------------#
# first ensure that unit conversion is possible (dimension of Mass)
if mass.units.dimensions.name != 'Mass':
bad_dim_err = ('Argument [mass] must be of dimension Mass. '
f'Received: {mass.units.dimensions.name}.')
raise TypeError(bad_dim_err)
# conversion is possible -> enforce grams
mass = mass.convert_to(g)
#--------------------#
# compute and return #
#--------------------#
# from: https://www.physics.rutgers.edu/~abrooks/342/constants.html
# NOTE: Scarabaeus does not use CGS units, normally these values should come from Scarabaeus'
# Constants library
G = scb.ArrayWUnits(6.6743e-8, cm**3 / (g*s**2))
c = scb.ArrayWUnits(2.9979e10, cm / sec)
# calculate
r_s = (2*G*mass) / c**2
return r_s
Note that the above code follows Scarabaeus coding conventions. It also makes our code more flexibile, since
any unit of Mass may be taken in without breaking our equation.
----
-----------------------
:bold:`Further Reading`
-----------------------
Congratulations! You now know how units work in Scarabaeus! It's time to see how they're
used in more complex interactions. The next section, :ref:`Epochs and Time in Scarabaeus `,
serves as a good starting point for working with units.