In [1]:
%matplotlib inline
import numpy as np
import matplotlib as mpl
from matplotlib import pyplot as plt

from astropy import units as u
from astropy import constants as c
from astropy.coordinates import SkyCoord
from astropy.analytic_functions import blackbody_lambda, blackbody_nu

In [2]:
mpl.rc('figure', figsize=(12, 8))
mpl.rc('lines', linewidth=4)
mpl.rc('font', size=20)
mpl.rc('axes.formatter', limits=(-4, 4))

In [3]:
def texify(unit):
    unit_strings = []
    for unit, power in zip(intensity_unit.bases, intensity_unit.powers):
        if power == 1:
            unit_strings.append(unit.to_string())
        else:
            unit_strings.append(unit.to_string() + "$^{{{:d}}}$".format(power))
    return " ".join(unit_strings)

Astropy quantities

Astropy quantitites are a great way to handle all sorts of messy unit conversions. Careful unit conversions save lives! https://en.wikipedia.org/wiki/Gimli_Glider

The simplest way to create a new quantity object is multiply or divide a number by a Unit instance.


In [4]:
print(type(u.Msun))
u.Msun


<class 'astropy.units.core.Unit'>
Out[4]:
$\mathrm{M_{\odot}}$

A brief word of warning, the pretty printing shown above will take longer than just printing out the values of an array. This is unnoticable in this case, but is evident if you try to print a large array of quantities.


In [5]:
mass = 1 * u.Msun
print(type(mass))
mass


<class 'astropy.units.quantity.Quantity'>
Out[5]:
$1 \; \mathrm{M_{\odot}}$

In [6]:
# quantities subclass numpy ndarray, so you can make handle arrays of quantities like you would
# any other array object
mass.__class__.__bases__


Out[6]:
(numpy.ndarray,)

In [7]:
# we can convert units to other equivalent units
mass.to(u.kg)


Out[7]:
$1.9891 \times 10^{30} \; \mathrm{kg}$

In [8]:
# there are shortcuts for converting to the relevent system, regardless of what type of quantity it is
mass.cgs


Out[8]:
$1.9891 \times 10^{33} \; \mathrm{g}$

In [9]:
mass.si


Out[9]:
$1.9891 \times 10^{30} \; \mathrm{kg}$

In [10]:
# we can inspect their unit and their numeric value with that unit
print(mass.value, mass.unit)


1.0 solMass

In [11]:
# calculations with quantities can produce quantities with new units
average_density = mass / (4 / 3 * np.pi * u.Rearth ** 3)
average_density.cgs


Out[11]:
$1830150 \; \mathrm{\frac{g}{cm^{3}}}$

Constants

Physical constants are found in the astropy.constants module, and work just like units.


In [12]:
# Newton's constant
c.G


Out[12]:
$6.67384 \times 10^{-11} \; \mathrm{\frac{m^{3}}{kg\,s^{2}}}$

In [13]:
# Planck's constant
c.h


Out[13]:
$6.6260696 \times 10^{-34} \; \mathrm{J\,s}$

In [14]:
# speed of light
c.c


Out[14]:
$2.9979246 \times 10^{8} \; \mathrm{\frac{m}{s}}$

Calculations with quantity arrays


In [15]:
# made into a quantity array by multiplying numeric array by unit
R = np.linspace(1, 5) * u.Rearth
v = np.sqrt(2 * c.G * u.Msun / R)
print(v)
v = v.to(u.km / u.s)
print(v)
plt.plot(R, v)
plt.xlabel(r"Radius  [R$_\oplus$]")
plt.ylabel(r"Escape velocity  [km s$^{-1}$]")


[  1.15532160e-05   1.11086939e-05   1.07118259e-05   1.03546641e-05
   1.00310006e-05   9.73590635e-06   9.46541158e-06   9.21627809e-06
   8.98583467e-06   8.77185553e-06   8.57246913e-06   8.38608884e-06
   8.21135949e-06   8.04711571e-06   7.89234904e-06   7.74618178e-06
   7.60784597e-06   7.47666638e-06   7.35204655e-06   7.23345738e-06
   7.12042765e-06   7.01253614e-06   6.90940499e-06   6.81069414e-06
   6.71609657e-06   6.62533433e-06   6.53815508e-06   6.45432912e-06
   6.37364683e-06   6.29591652e-06   6.22096246e-06   6.14862323e-06
   6.07875024e-06   6.01120648e-06   5.94586534e-06   5.88260965e-06
   5.82133080e-06   5.76192791e-06   5.70430718e-06   5.64838125e-06
   5.59406863e-06   5.54129322e-06   5.48998384e-06   5.44007386e-06
   5.39150080e-06   5.34420602e-06   5.29813442e-06   5.25323417e-06
   5.20945645e-06   5.16675527e-06] m(3/2) solMass(1/2) / (earthRad(1/2) kg(1/2) s)
[ 6451.85077635  6203.60902807  5981.97958137  5782.52390238  5601.77519058
  5436.98091785  5285.92411482  5146.79643593  5018.10615939  4898.61029942
  4787.26370154  4683.18031768  4585.60336065  4493.88202771  4407.45315031
  4325.82658334  4248.57346551  4175.31670694  4105.72322132  4039.49753665
  3976.37650446  3916.12489104  3858.53168206  3803.40696802  3750.57930567
  3699.89347169  3651.2085416   3604.39623961  3559.33951544  3515.9313121
  3474.07349496  3433.67591798  3394.65560652  3356.93604017  3320.44652124
  3285.12161724  3250.90066721  3217.72734344  3185.54926149  3154.31763219
  3123.9869505   3094.51471662  3065.86118556  3037.98914172  3010.86369563
  2984.45210026  2958.72358483  2933.64920402  2909.2017011   2885.35538332] km / s
Out[15]:
<matplotlib.text.Text at 0x7fcf5c86f780>

Quantities as sanity checks


In [16]:
obscure_quantity = 42 * c.G * c.m_e ** 2 / c.k_B ** 2 * c.c ** 3 * (5700 * u.K) ** -2 * u.Msun / u.Mpc

In [17]:
obscure_quantity


Out[17]:
$1.0119265 \times 10^{-5} \; \mathrm{\frac{m^{6}\,kg\,M_{\odot}}{Mpc\,J^{2}\,s^{5}}}$

In [18]:
# what the heck is a m^6 kg Msun / (Mpc J^2 s^5)??
obscure_quantity.decompose()


Out[18]:
$652.31149 \; \mathrm{\frac{m}{s}}$

In [19]:
# will fail!
obscure_quantity.to(u.m)


---------------------------------------------------------------------------
UnitConversionError                       Traceback (most recent call last)
<ipython-input-19-ed638fd84d3a> in <module>()
      1 # will fail!
----> 2 obscure_quantity.to(u.m)

/usr/lib64/python3.5/site-packages/astropy/units/quantity.py in to(self, unit, equivalencies)
    764         unit = Unit(unit)
    765         new_val = self.unit.to(unit, self.view(np.ndarray),
--> 766                                equivalencies=equivalencies)
    767         return self._new_view(new_val, unit)
    768 

/usr/lib64/python3.5/site-packages/astropy/units/core.py in to(self, other, value, equivalencies)
   1006             If units are inconsistent
   1007         """
-> 1008         return self._get_converter(other, equivalencies=equivalencies)(value)
   1009 
   1010     def in_units(self, other, value=1.0, equivalencies=[]):

/usr/lib64/python3.5/site-packages/astropy/units/core.py in _get_converter(self, other, equivalencies)
    908                             pass
    909 
--> 910             raise exc
    911 
    912     @deprecated('1.0')

/usr/lib64/python3.5/site-packages/astropy/units/core.py in _get_converter(self, other, equivalencies)
    894         try:
    895             return self._apply_equivalencies(
--> 896                 self, other, self._normalize_equivalencies(equivalencies))
    897         except UnitsError as exc:
    898             # Last hope: maybe other knows how to do it?

/usr/lib64/python3.5/site-packages/astropy/units/core.py in _apply_equivalencies(self, unit, other, equivalencies)
    878         raise UnitConversionError(
    879             "{0} and {1} are not convertible".format(
--> 880                 unit_str, other_str))
    881 
    882     def _get_converter(self, other, equivalencies=[]):

UnitConversionError: 'kg m6 solMass / (J2 Mpc s5)' (speed) and 'm' (length) are not convertible

In [20]:
# addition works for like units
(1 * u.m) + (1 * u.cm)


Out[20]:
$1.01 \; \mathrm{m}$

In [21]:
# and fails for the wrong dimensions
(1 * u.m) + (1 * u.s)


---------------------------------------------------------------------------
UnitConversionError                       Traceback (most recent call last)
/usr/lib64/python3.5/site-packages/astropy/units/quantity_helper.py in get_converter(from_unit, to_unit)
     21     try:
---> 22         scale = from_unit._to(to_unit)
     23     except UnitsError:

/usr/lib64/python3.5/site-packages/astropy/units/core.py in _to(self, other)
    974         raise UnitConversionError(
--> 975             "'{0!r}' is not a scaled version of '{1!r}'".format(self, other))
    976 

UnitConversionError: 'Unit("s")' is not a scaled version of 'Unit("m")'

During handling of the above exception, another exception occurred:

UnitConversionError                       Traceback (most recent call last)
/usr/lib64/python3.5/site-packages/astropy/units/quantity_helper.py in get_converters_and_unit(f, *units)
    282             converters[changeable] = get_converter(units[changeable],
--> 283                                                    units[fixed])
    284         except UnitsError:

/usr/lib64/python3.5/site-packages/astropy/units/quantity_helper.py in get_converter(from_unit, to_unit)
     24         return from_unit._apply_equivalencies(
---> 25                 from_unit, to_unit, get_current_unit_registry().equivalencies)
     26     if scale == 1.:

/usr/lib64/python3.5/site-packages/astropy/units/core.py in _apply_equivalencies(self, unit, other, equivalencies)
    879             "{0} and {1} are not convertible".format(
--> 880                 unit_str, other_str))
    881 

UnitConversionError: 's' (time) and 'm' (length) are not convertible

During handling of the above exception, another exception occurred:

UnitConversionError                       Traceback (most recent call last)
<ipython-input-21-1c053e535f91> in <module>()
      1 # and fails for the wrong dimensions
----> 2 (1 * u.m) + (1 * u.s)

/usr/lib64/python3.5/site-packages/astropy/units/quantity.py in __array_prepare__(self, obj, context)
    384         # the unit the output from the ufunc will have.
    385         if function in UFUNC_HELPERS:
--> 386             converters, result_unit = UFUNC_HELPERS[function](function, *units)
    387         else:
    388             raise TypeError("Unknown ufunc {0}.  Please raise issue on "

/usr/lib64/python3.5/site-packages/astropy/units/quantity_helper.py in helper_twoarg_invariant(f, unit1, unit2)
    292 
    293 def helper_twoarg_invariant(f, unit1, unit2):
--> 294     return get_converters_and_unit(f, unit1, unit2)
    295 
    296 UFUNC_HELPERS[np.add] = helper_twoarg_invariant

/usr/lib64/python3.5/site-packages/astropy/units/quantity_helper.py in get_converters_and_unit(f, *units)
    286                 "Can only apply '{0}' function to quantities "
    287                 "with compatible dimensions"
--> 288                 .format(f.__name__))
    289 
    290         return converters, units[fixed]

UnitConversionError: Can only apply 'add' function to quantities with compatible dimensions

Unit equivalencies

Equivalencies allow you to do unit conversions under certain physical assumptions. For instance, it makes sense to talk about converting wavelength to frequency when you are discussing the properties of light waves in a vacuum. See http://docs.astropy.org/en/stable/units/equivalencies.html#unit-equivalencies.

Spectral equivalence


In [22]:
wavelengths = np.linspace(0.1, 1, 100) * u.micron

# will fail without the correct equivalency passed in!
frequencies = wavelengths.to(u.THz, equivalencies=u.spectral()) 

plt.plot(wavelengths, frequencies)
plt.xlabel(r"$\lambda$  [$\mu$m]")
plt.ylabel(r"$\nu$  [THz]")


Out[22]:
<matplotlib.text.Text at 0x7fcf853b67f0>

Spectral energy density equivalencies


In [23]:
intensity_unit = blackbody_nu(wavelengths[0], temperature=1e3 * u.K).unit
wavelengths = np.logspace(-1, 1, 100) * u.micron 
temperatures = np.linspace(5e3, 1e4, 5) * u.K

for T in temperatures:
    plt.plot(wavelengths, blackbody_nu(wavelengths, temperature=T), 
             label='{:.2e}'.format(T))
    
plt.xscale('log')
plt.legend(loc='best')
plt.xlabel(r'$\lambda$  [$\mu$m]')
plt.ylabel('$I_\\nu$  [{}]'.format(texify(intensity_unit)))


Out[23]:
<matplotlib.text.Text at 0x7fcf594b2d30>

In [24]:
intensity_unit = blackbody_lambda(wavelengths[0], temperature=1e3 * u.K).unit
for T in temperatures:
    plt.plot(wavelengths, blackbody_lambda(wavelengths, temperature=T), 
             label='{:.2e}'.format(T))
    
plt.xscale('log')
plt.legend(loc='best')
plt.xlabel(r'$\lambda$  [$\mu$m]')
plt.ylabel('$I_\\lambda$  [{}]'.format(texify(intensity_unit)))


Out[24]:
<matplotlib.text.Text at 0x7fcf5969ed30>

In [25]:
T = 1e4 * u.K
solid_angle = ((1 * u.arcsec) ** 2).to(u.sr)
f_nu = blackbody_nu(wavelengths, temperature=T) * solid_angle
f_lambda = blackbody_lambda(wavelengths, temperature=T) * solid_angle

print(f_nu.unit)
print(f_lambda.unit)

# I_nu.to(I_lambda.unit) # would fail

# for conversion of spectral energy density, we need to specify what part of the spectra we're looking at
f_lambda_converted = f_nu.to(f_lambda.unit, equivalencies=u.spectral_density(wavelengths))

print(f_lambda_converted.unit)

# shouldn't raise any exceptions!
assert np.all(np.isclose(f_lambda.value, f_lambda_converted.value))


erg / (cm2 Hz s)
erg / (Angstrom cm2 s)
erg / (Angstrom cm2 s)

Other cool equivalencies

Doppler shifts (for both radio velocities and optical velocities), dimensionless angles, parallax.

Words of warning

Quantity arrays will often break functions that aren't prepared for them. Simple numpy operations still work, but for more complicated routines you'll have to convert to the units you want and then take the underlying array with the quantity.value attribute.


In [26]:
# e.g., from the example above
np.isclose(f_lambda, f_lambda_converted)


---------------------------------------------------------------------------
UnitsError                                Traceback (most recent call last)
<ipython-input-26-c8322b046358> in <module>()
      1 # e.g., from the example above
----> 2 np.isclose(f_lambda, f_lambda_converted)

/usr/lib64/python3.5/site-packages/numpy/core/numeric.py in isclose(a, b, rtol, atol, equal_nan)
   2558     yfin = isfinite(y)
   2559     if all(xfin) and all(yfin):
-> 2560         return within_tol(x, y, atol, rtol)
   2561     else:
   2562         finite = xfin & yfin

/usr/lib64/python3.5/site-packages/numpy/core/numeric.py in within_tol(x, y, atol, rtol)
   2541     def within_tol(x, y, atol, rtol):
   2542         with errstate(invalid='ignore'):
-> 2543             result = less_equal(abs(x-y), atol + rtol * abs(y))
   2544         if isscalar(a) and isscalar(b):
   2545             result = bool(result)

/usr/lib64/python3.5/site-packages/astropy/units/quantity.py in __array_prepare__(self, obj, context)
    406                                      "argument is not a quantity (unless the "
    407                                      "latter is all zero/infinity/nan)"
--> 408                                      .format(function.__name__))
    409             except TypeError:
    410                 # _can_have_arbitrary_unit failed: arg could not be compared

UnitsError: Can only apply 'add' function to dimensionless quantities when other argument is not a quantity (unless the latter is all zero/infinity/nan)

Even when you think you have a dimensionless array, it can still be a dimensionless quantity.


In [27]:
print((1 * u.m) / (2 * u.m), type((1 * u.m) / (2 * u.m)))


0.5 <class 'astropy.units.quantity.Quantity'>

Using units in your own code

You can use the decorator quality_input as a clean way of ensuring your functions get the proper input.


In [29]:
@u.quantity_input(angle=u.arcsec, distance=u.Mpc)
def angle_to_size(angle, distance):
    return angle.to(u.radian).value * distance

# this should work
angle_to_size(1 * u.arcsec, 25 * u.Mpc).to(u.kpc)


Out[29]:
$0.12120342 \; \mathrm{kpc}$

In [30]:
# quantity_input only checks for convertability, not that it's the same unit
angle_to_size(1 * u.arcmin, 25 * u.Mpc).to(u.kpc)


Out[30]:
$7.2722052 \; \mathrm{kpc}$

In [31]:
# this should raise an error
angle_to_size(1 * u.m, 25 * u.Mpc)


---------------------------------------------------------------------------
UnitsError                                Traceback (most recent call last)
<ipython-input-31-43d6ad8e56af> in <module>()
      1 # this should raise an error
----> 2 angle_to_size(1 * u.m, 25 * u.Mpc)

/usr/lib64/python3.5/site-packages/astropy/utils/decorators.py in angle_to_size(angle, distance)
    858             name = func.__name__
    859 
--> 860         func = make_function_with_signature(func, name=name, **wrapped_args)
    861         func = functools.update_wrapper(func, wrapped, assigned=assigned,
    862                                         updated=updated)

/usr/lib64/python3.5/site-packages/astropy/units/decorators.py in wrapper(*func_args, **func_kwargs)
    114                                              " '{2}'.".format(param.name,
    115                                                      wrapped_function.__name__,
--> 116                                                      target_unit.to_string()))
    117 
    118                     # Either there is no .unit or no .is_equivalent

UnitsError: Argument 'angle' to function 'angle_to_size' must be in units convertible to 'arcsec'.

SkyCoord

The SkyCoord class, from astropy.coordinates, is a convenient way of dealing with astronomical coordinate systems.

http://docs.astropy.org/en/stable/coordinates/index.html


In [32]:
coord = SkyCoord(45, 30, unit=u.deg)

In [33]:
# ICRS is the reference frame
coord


Out[33]:
<SkyCoord (ICRS): (ra, dec) in deg
    ( 45.,  30.)>

In [34]:
# we can transform between coordinate frames
coord.fk4


Out[34]:
<SkyCoord (FK4: equinox=B1950.000, obstime=B1950.000): (ra, dec) in deg
    ( 44.24694135,  29.80187815)>

In [35]:
coord.fk5


Out[35]:
<SkyCoord (FK5: equinox=J2000.000): (ra, dec) in deg
    ( 45.00000965,  29.99999788)>

In [36]:
coord.galactic


Out[36]:
<SkyCoord (Galactic): (l, b) in deg
    ( 153.52136545, -25.12784127)>

In [37]:
# latitude and longitude are accessed with ra and dec (when in icrs or fk frames)
coord.ra


Out[37]:
$45^\circ00{}^\prime00{}^{\prime\prime}$

In [38]:
coord.dec


Out[38]:
$30^\circ00{}^\prime00{}^{\prime\prime}$

The attributes ra and dec are Angles. They are subclasses of Quantity, and so they behave similarly, but have more specific functionality. See http://docs.astropy.org/en/stable/coordinates/angles.html#working-with-angles for more details.

You can get nice string representations of angles for all your inane legacy software requirements.


In [40]:
print(coord.to_string())
print(coord.to_string('dms'))
print(coord.to_string('hmsdms'))
print(coord.to_string('hmsdms', sep=':'))
print(coord.to_string('hmsdms', sep=' '))


45 30
45d00m00s 30d00m00s
03h00m00s +30d00m00s
03:00:00 +30:00:00
03 00 00 +30 00 00

Matching coordinates

There are lots of specific use cases outlined here, but let's go over a simple catalog matching exercise.


In [41]:
# need network connection
center_coord = SkyCoord.from_name('M31')
center_coord


Out[41]:
<SkyCoord (ICRS): (ra, dec) in deg
    ( 10.6847929,  41.269065)>

In [42]:
# some mock coordinates
n = 500
ra_values = np.random.randn(n) + center_coord.ra.deg
dec_values = np.random.randn(n) + center_coord.dec.deg
coords = SkyCoord(ra_values, dec_values, unit=u.deg)

plt.scatter(coords.ra.deg, coords.dec.deg, s=100, 
            edgecolor='k', label='Parent sample')
plt.xlim(plt.xlim()[::-1]) # ra increases right to left
plt.xlabel("Right ascension  [deg]")
plt.ylabel("Declination  [deg]")


Out[42]:
<matplotlib.text.Text at 0x7fcf580ede10>

In [43]:
# mock measurements
n_sample = 100
astrometric_noise = 1 * u.arcsec

sample_indices = np.random.choice(np.arange(len(coords)), n_sample)
sample_ra = coords[sample_indices].ra.deg
sample_dec = coords[sample_indices].dec.deg

angles = 2 * np.pi * np.random.rand(n_sample)
dr = astrometric_noise.to(u.deg).value * np.random.randn(n_sample)
dx = np.cos(angles) * dr - np.sin(angles) * dr
dy = np.sin(angles) * dr + np.cos(angles) * dr
sample_coords = SkyCoord(sample_ra + dx, sample_dec + dy, unit=u.deg)

plt.scatter(coords.ra.deg, coords.dec.deg, s=100, 
            edgecolor='k', marker='o', alpha=0.8, label='Parent sample')
plt.scatter(sample_coords.ra.deg, sample_coords.dec.deg, s=100, 
            edgecolor='k', marker='v', alpha=0.8, label='Child sample')
plt.xlim(plt.xlim()[::-1]) # ra increases right to left
plt.xlabel("Right ascension  [deg]")
plt.ylabel("Declination  [deg]")
plt.legend(bbox_to_anchor=(1, 1))


Out[43]:
<matplotlib.legend.Legend at 0x7fcf58088668>

In [47]:
# match_to_catalog_sky will return indices into coords of the closest matching objects,
# the angular separation, and the physical distance (ignored here)
idx, sep, dist = sample_coords.match_to_catalog_sky(coords)

ideal_sep = astrometric_noise.to(u.deg) * np.random.randn(int(1e6))
plt.hist(np.abs(ideal_sep.to(u.arcsec)), histtype='step', lw=4, bins='auto', normed=True, label='Ideal')
plt.hist(sep.arcsec, histtype='step', lw=4, bins='auto', normed=True, label='Data')
plt.xlim(0, 3)
plt.xlabel("Separation  [arcsec]")
plt.legend(loc='best')


Out[47]:
<matplotlib.legend.Legend at 0x7fcf4a76ceb8>

In [46]:
plt.scatter(coords[idx].ra.deg, coords[idx].dec.deg, s=100, 
            edgecolor='k', marker='o', alpha=0.8, label='Matched parent sample')
plt.scatter(sample_coords.ra.deg, sample_coords.dec.deg, s=100, 
            edgecolor='k', marker='v', alpha=0.8, label='Child sample')
plt.xlim(plt.xlim()[::-1]) # ra increases right to left
plt.xlabel("Right ascension  [deg]")
plt.ylabel("Declination  [deg]")
plt.legend(bbox_to_anchor=(1, 1))


Out[46]:
<matplotlib.legend.Legend at 0x7fcf4a7e84e0>

In [ ]: