Units and Quantities¶
⚠️ Warning: under construction!
This document briefly demonstrates φtorch’s functionalities related to physical units and (unitful) quantities.
Units¶
Creating and manipulating units¶
The easiest way to get started is to import some pre-defined units:
>>> from phytorch.units import si
>>> si.kg, si.nanohertz
(<Unit: kg>, <Unit: nHz>)
You can then combine them (multiply and divide by units and numbers and raise to a real power) to form new ones!
>>> si.kg * si.m / si.s**2
<Unit: kg m s^(2)^(-1)>
Note
Note how the string representation of the unit encodes its “creation history”. This is not a feature and will likely change in the future.
Every unit has a value and a dimension:
>>> si.eV.value, si.eV.dimension
(1.602176634e-19, <UnitBase: [M^(1) L^(2) T^(-2)]>)
This tells us that an electronvolt is \(\approx 1.6 \times 10^{-19}\) of the base units for \(\text{mass} \times \text{length}^2 / \text{time}^2\), which happens to be energy, for which the base unit is the joule. Let’s check:
>>> si.eV.dimension == si.joule.dimension
True
Converting units¶
Units of the same dimension can be converted to each other. For example, we can calculate the age of the Universe in… days?… from the Hubble constant (don’t trust all those digits, though):
>>> from phytorch.units import astro
>>> (H0 := 100 * si.km / si.s / astro.Mpc)
<Unit: 100 km s^(-1) Mpc^(-1)>
>>> age = 1 / H0
>>> age.to(si.day)
3571386089689.083
Note that the result of the conversion is a pure number! This is equivalent to
>>> (age / si.day).value
3571386089689.083
Note that age / si.day is still a unit, albeit a dimensionless one:
>>> (age / si.day).dimension
<UnitBase: []>
Trying to convert incompatible units (with different dimensions) will result in an error:
>>> from phytorch.units.astro import lightyear
>>> age.to(lightyear)
TypeError: Cannot convert 1 ((100 km s^(-1) Mpc^(-1))^(-1)),
aka [T^(1)], to lyr, aka [L^(1)]
Converting units to AstroPy¶
AstroPy’s units module is the inspiration behind
phytorch.units, and so a φtorch Unit can be easily converted to an astropy.units.Unit:
>>> age_ap = age.toAstropy('age')
>>> (type(age_ap), age_ap, age_ap.represents, age_ap.to('day'))
(astropy.units.core.Unit,
Unit("age"),
Unit("3.08568e+17 s"),
3571386089689.083)
Todo
Convert astropy.units.Units to φtorch Units.
Constants¶
phytorch.constants provides universal constants as defined by CODATA.
Additionally, phytorch.constants.astro defines some astronomical… ahem… constants. See the documentation for a full list.
Constants are nothing more than Units with a few presentational bells and whistles:
>>> from phytorch.constants import c, m_e
>>> c, m_e
(<speed of light in vacuum: c = 299792458 (m s^(-1))>,
<electron mass: m_e = 9.1093837015e-31 kg>)
>>> (E_e := m_e * c**2)
<Unit: m_e c^(2)>
>>> E_e.to(si.keV)
510.9989499961642
Last one: size of the Universe in Plank lengths:
>>> from phytorch.constants import G, ħ
>>> (age * c).to((ħ * G / c**3)**0.5)
5.7234956907907186e+60
Quantities¶
Quantities—a combination of a Tensor and a Unit—allow unit information to be propagated through
numerical calculations, automatically deriving the units of resulting quantities while also acting as a safeguard
against nonsensical operations, like taking the logarithm of unitful quantities.
Creating quantities¶
A Quantity can be created by multiplying (or dividing) a Tensor and a Unit:
>>> import torch
>>> (q := torch.rand((2, 3)) * si.hour)
Quantity(
TensorQuantity([[0.9705, 0.9657, 0.6912],
[0.6088, 0.1473, 0.9951]]) h)
Todo
Presentation details are still to be ironed out.
Warning
Creating a Quantity backed by a non-float-typed Tensor is largely undefined and may not behave as one’d expect.
Similarly to a Unit, the value and unit of a Quantity can be accessed as attributes:
>>> q.value
tensor([[0.9705, 0.9657, 0.6912],
[0.6088, 0.1473, 0.9951]])
>>> q.unit
<Unit: h>
Note that value is a view of the underlying data, so changes to it will be reflected
back on the Quantity:
>>> q.value[0, :] = 1.
>>> q
Quantity(
TensorQuantity([[1.0000, 1.0000, 1.0000],
[0.6088, 0.1473, 0.9951]]) h)
The same is true of the Tensor used to create the Quantity:
>>> (q := (t := torch.rand(4)) * si.hour)
Quantity(TensorQuantity([0.3346, 0.9776, 0.3983, 0.4014]) h)
>>> t[2] = 3.14
>>> q
Quantity(TensorQuantity([0.3346, 0.9776, 3.1400, 0.4014]) h)
But note that
>>> q.value is not t
True
Converting quantities¶
Quantities can be converted to different units of the same dimension using to:
>>> q.to(si.minute)
Quantity(TensorQuantity([ 20.0731, 58.6554, 188.4000, 24.0832]) min)
Note that this creates an entirely different Quantity backed by a separate Tensor, so the original is not
modified. Unless you convert to a unit that is equal to the current one: in that case some computation is spared,
and the original quantity is returned:
>>> q.to(60*si.minute) is q
True
Note further that in this case the unit remains the same object, so you cannot expect that
quantity.to(unit).unit is unit but only that quantity.to(unit).unit == unit.
Of course, wrong conversions raise exceptions:
>>> q.to(si.kilometer)
TypeError: Cannot convert h, aka [T^(1)], to km, aka [L^(1)]
Manipulating quantities¶
φtorch’s quantities support the full set of mathematical operations defined by PyTorch[*] in a
way that makes sense for the unit information that they carry:
For starters, you can only add / subtract quantites with compatible units:[†]
>>> t(1.) * h + t(15.) * min Quantity(TensorQuantity(1.2500) h) >>> t(1.) * h + t(15.) * km UnitError: expected [L^(-1) T^(1)] but got dimensionless.
Numbers and pure
Tensors are considered dimensionless quantities:>>> t(1.) * h + 5. UnitError: expected [T^(1)] but got dimensionless.
Comparisons also only work with compatible quantities:
>>> t(1.) * h <= t([30., 60., 90.]) * min tensor([False, True, True]) >>> t(1.) * Unit(apple=1) > t(1.) * Unit(orange=1) UnitError: expected [apple^(1)] but got [orange^(1)].
On the other hand, you can multiply / divide any quantities:
>>> (t(120.) * km) / (t(72.) * min) Quantity(TensorQuantity(1.6667) km min^(-1))
Note that the numeric
valueis now just the result of the operation on thevalues of the originalQuantities, unlike in addition when the secondQuantitywas converted.And raise a
Quantityto a scalar power>>> (t(3.14) * km)**2 Quantity(TensorQuantity(9.8596) km^(2)) >>> (t(3.14) * km)**torch.tensor(2.) Quantity(TensorQuantity(9.8596) km^(2)) >>> (t(3.14) * km).sqrt() Quantity(TensorQuantity(1.7720) km^(1/2))
Raising each element to a different power results in different units, so it is forbidden:
>>> (t(3.14) * km) ** torch.tensor([2., 3., 4.]) ValueError: only one element tensors can be converted to Python scalars
Since dimensionless units are still units, this extends also to the case of a dimensionless
Quantityas base of exponentation. Finally, raising to the power of aQuantityis allowed only if it’s dimensionless and has a singleitem:>>> (t(2.) * km) ** ((t(1.) * h) / (t(30.) * min)) Quantity(TensorQuantity(4.) km^(2))
Most mathematical single-argument functions are only allowed for dimensionless quantities:
>>> torch.exp(t(1.) * km) UnitError: expected [] but got [L^(1)]. >>> torch.sin((t(1.) * h) / (t(15.) * min)) tensor(-0.7568)
Note that the result is an ordinary
Tensor! And that theradianis defined as a dimensionless unit with unit scale:>>> from phytorch.units import angular >>> angular.radian.value, angular.radian.dimension (Fraction(1, 1), <UnitBase: []>) >>> torch.sin(t(4.) * angular.rad) tensor(-0.7568)
All in all, PyTorch countains countless[citation needed] operations. The user is encouraged to try them out and see for themselves if the results are sensible.
Footnotes