1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185
|
.. _sunpy-tutorial-units:
*****
Units
*****
In this section of the tutorial you will learn about representing physical units in sunpy.
All functions in sunpy that accept or return numbers associated with physical quantities do so using `astropy.units.Quantity` objects.
These objects represent a number (or an array of numbers) and a unit.
This means sunpy is always explicit about the units associated with a value.
Quantities and units are powerful tools for keeping track of variables with a physical meaning and make it straightforward to convert the same physical quantity into different units.
By the end of this section of the tutorial, you will learn how to create a `~astropy.units.Quantity`, perform basic arithmetic with a `~astropy.units.Quantity`, convert a `~astropy.units.Quantity` to different units, and write a function that ensures the inputs have the correct units.
Adding Units to Data
====================
To use units we must first import them from Astropy.
It is standard practice to import the units module as ``u``:
.. code-block:: python
>>> import astropy.units as u
We can create a `~astropy.units.Quantity` by multiplying a number by a unit:
.. code-block:: python
>>> length = 10 * u.meter
>>> length
<Quantity 10. m>
A `~astropy.units.Quantity` can be decomposed into its unit and numerical value using the ``.unit`` and ``.value`` attributes:
.. doctest-requires:: numpy>=2.0.0
>>> length.value
np.float64(10.0)
>>> length.unit
Unit("m")
Arithmetic With Units
=====================
`~astropy.units.Quantity` objects propagate units through arithmetic operations:
.. code-block:: python
>>> distance_start = 10 * u.mm
>>> distance_end = 23 * u.km
>>> displacement = distance_end - distance_start
>>> displacement
<Quantity 22.99999 km>
>>> time = 15 * u.minute
>>> speed = displacement / time
>>> speed
<Quantity 1.53333267 km / min>
However, operations with incompatible units raise an error:
.. code-block:: python
>>> displacement + time
Traceback (most recent call last):
...
astropy...UnitConversionError: Can only apply 'add' function to quantities with compatible dimensions
Converting Units
================
`~astropy.units.Quantity` objects can also be converted to other units or unit systems:
.. code-block:: python
>>> length.to(u.km)
<Quantity 0.01 km>
>>> length.cgs
<Quantity 1000. cm>
Unit Equivalencies
==================
It is commonplace to convert between units which are only compatible under certain assumptions.
For example, in spectroscopy, spectral energy and wavelength are equivalent given the relation :math:`E=hc/\lambda`.
If we try to convert a wavelength to energy using what we learned in the previous section, we get an exception because length and energy are, in general, not compatible units:
.. code-block:: python
>>> length.to(u.keV)
Traceback (most recent call last):
...
astropy...UnitConversionError: 'm' (length) and 'keV' (energy/torque/work) are not convertible
However, we can perform this conversion using the `~astropy.units.spectral` equivalency:
.. code-block:: python
>>> length.to(u.keV, equivalencies=u.spectral())
<Quantity 1.23984198e-10 keV>
An equivalency common in solar physics is conversion of angular distances in the plane of the sky to physical distances on the Sun.
To perform this conversion, sunpy provides `~sunpy.coordinates.utils.solar_angle_equivalency`, which requires specifying the location at which that angular distance was measured:
.. code-block:: python
>>> from sunpy.coordinates import get_earth
>>> from sunpy.coordinates.utils import solar_angle_equivalency
>>> length.to(u.arcsec, equivalencies=solar_angle_equivalency(get_earth("2013-10-28")))
INFO: Apparent body location accounts for 495.82 seconds of light travel time [sunpy.coordinates.ephemeris]
<Quantity 1.38763748e-05 arcsec>
Note that in the above example we made use of `sunpy.coordinates.get_earth`.
We will talk more about coordinates in the :ref:`sunpy-tutorial-coordinates` section of this tutorial.
For now, it is just important to know that this function returns the location of the Earth on 2013 October 28.
Dropping Units
==============
Not every package in the scientific Python ecosystem understands units.
As such, it is sometimes necessary to drop the units before passing `~astropy.units.Quantity` to such functions.
As shown above, you can retrieve the just the numerical value of a `~astropy.units.Quantity`:
.. doctest-requires:: numpy>=2.0.0
>>> length.to_value()
np.float64(10.0)
>>> length.to_value(u.km)
np.float64(0.01)
Quantities as function arguments
================================
When calling a function that relies on inputs corresponding to physical quantities, there is often an implicit assumption that these input arguments are expressed in the expected units of that function.
For instance, if we define a function to calculate speed as above, the inputs should correspond to a distance and a time:
.. code-block:: python
>>> def speed(length, time):
... return length / time
However, this assumes that the two arguments passed in have units consistent with distance and time without checking.
The `~astropy.units.quantity_input` decorator, combined with `function annotations <https://python-3-for-scientists.readthedocs.io/en/latest/python3_features.html#function-annotations>`__, enforces compatible units on the function inputs:
.. code-block:: python
>>> @u.quantity_input
... def speed(length: u.m, time: u.s):
... return length / time
Now when this function is called, if the inputs are not convertible to the units specified, an error will be raised stating that the units are incorrect or missing:
.. code-block:: python
>>> speed(1*u.m, 10*u.m) # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
astropy...UnitsError: Argument 'time' to function 'speed' must be in units convertible to 's'.
>>> speed(1*u.m, 10) # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
TypeError: Argument 'time' to function 'speed' has no 'unit' attribute. You should pass in an astropy Quantity instead.
The units of the inputs need only be compatible with those in the function definition.
For example, passing in a time in minutes still works even though we specified ``time: u.s``:
.. code-block:: python
>>> speed(1*u.m, 1*u.minute)
<Quantity 1. m / min>
Note that the units of the output are dependent on the units of the inputs.
To ensure consistent units on the output of our function, we add an additional function annotation to force the output to always be converted to m/s before returning an answer:
.. code-block:: python
>>> @u.quantity_input
... def speed(length: u.m, time: u.s) -> u.m/u.s:
... return length / time
>>> speed(1*u.m, 1*u.minute)
<Quantity 0.01666667 m / s>
|