Plotting Examples
One of the core features of HPMOC is its robust and convenient suite of plotting tools. This page includes several examples showcasing how to use HPMOC for sophisticated plots. An emphasis is made on showing how to navigate different tools and projections, and how to quickly iterate to get a perfect plot for your purposes.
HPMOC uses the astropy.wcs.WCS
implementation of FITS projections for plotting. Details of the projection definitions and their associated FITS header keywords are given in Calabretta and Greisen (2002).
Setup
We’ll start by importing packages and picking some skymap files of various types from the test skymaps included in the HPMOC source repository. The loading and plotting instructions in this document work with any skymaps produced by any of the major LIGO pipelines (in particular, anything downloaded from GraceDB should work).
[1]:
from pathlib import Path
import numpy as np
from matplotlib import pyplot as plt
from hpmoc import PartialUniqSkymap
from hpmoc.plot import get_wcs, plot, gridplot
DATA = Path(".").absolute().parent.parent.parent/"tests"/"data"
NUNIQ_FITS = DATA/'S200105ae.fits'
NUNIQ_FITS_GZ = DATA/'S200105ae.fits.gz'
BAYESTAR_NEST_FITS_GZ_1024 = DATA/'S200316bj-1-Preliminary.fits.gz'
CWB_NEST_FITS_GZ_128 = DATA/'S200114f-3-Initial.fits.gz'
CWB_RING_FITS_GZ_128 = DATA/'S200129m-3-Initial.fits.gz'
Basic Plotting
We can easily load and display a skymap using the default plot settings, which produce a Mollweide plot, using hpmoc.partial.PartialUniqSkymap.plot (a thin wrapper around hpmoc.plot.plot):
[2]:
m = PartialUniqSkymap.read(NUNIQ_FITS, strategy='ligo')
# Replace the above line with the following (the GraceDB URL of the same skymap)
# if you don't have the test files handy:
# m = PartialUniqSkymap.read(graceid, 'bayestar.multiorder.fits,0',
# strategy='gracedb')
m.plot()
[2]:
<astropy.visualization.wcsaxes.core.WCSAxes at 0x11a40df90>
We can modify various properties; for example, we can double the default pixel height (vertical resolution) of the rendered image by doubling the height
; the pixel width
will not change, and so the aspect ratio will change:
[3]:
m.plot(height=360)
[3]:
<astropy.visualization.wcsaxes.core.WCSAxes at 0x121034590>
We can also change the size of the figure itself, either by creating a new matplotlib.figure.Figure
and passing it as the fig
argument, or by simply passing the same keywords we would use for Figure
as the fig
argument to plot
:
[4]:
m.plot(fig={'figsize': (12, 8)}, height=360, width=720)
[4]:
<astropy.visualization.wcsaxes.core.WCSAxes at 0x1206c1910>
We can also add color bars with the cbar
argument:
[5]:
m.plot(cbar=True)
[5]:
<astropy.visualization.wcsaxes.core.WCSAxes at 0x1208c4c90>
We can plot all-sky fixed HEALPix order skymaps with implicit pixel indices by passing the data arrays to plot
as well. We can do this by instantiating a PartialUniqSkymap using the pixels and ordering:
[6]:
from astropy.table import Table
t1 = Table.read(CWB_RING_FITS_GZ_128)
PartialUniqSkymap(t1['PROB'].ravel(), t1.meta['ORDERING']).compress().plot()
[6]:
<astropy.visualization.wcsaxes.core.WCSAxes at 0x12589ab10>
or by passing the arguments as a tuple to hpmoc.plot.plot, which does the same thing behind the scenes for us:
[7]:
t2 = Table.read(CWB_NEST_FITS_GZ_128)
plot((t2['PROB'].ravel(), t2.meta['ORDERING']))
[7]:
<astropy.visualization.wcsaxes.core.WCSAxes at 0x120e17790>
As you can see, this worked on both a RING-ordered and a NESTED-ordered skymap:
[8]:
print(t1.meta['ORDERING'], t2.meta['ORDERING'])
RING NESTED
One advantage of loading into PartialUniqSkymap first is that we can call the compress method to eliminate redundant pixels first. This will not have a tremendous effect if we are doing a one-off plot, but it can lead to multiple orders of magnitude of performance increase (particularly with BAYESTAR skymaps) if we are going to work with the skymaps repeatedly. If you load a skymap with PartialUniqSkymap.read, as we did earlier, all of this is handled for you; the skymap is even read in small chunks to avoid ever using much of a memory footprint. It also takes care of the slight differences between LIGO pipelines’ skymap formats for you. Nonetheless, if you come across a HEALPix skymap that’s not already supported, you can still load and plot it in HPMOC.
There are many other arguments available to tweak the look and feel of the plot; see some of the specific examples below as well as the full documentation in hpmoc.plot.plot.
Projections
There are, however, many other projections available. The ones with built-in HPMOC support are discussed in this section.
Cylindrical and Pseudocylindrical projections
Mollweide Projection
The Mollweide (MOL) projection is an example of a pseudocylindrical projection. We can explicitly select the Mollweide projection by passing "Mollweide"
, or its FITS projection code, "MOL"
(or any of the aliases specified in hpmoc.plot.get_wcs), as the projection
argument to plot
[9]:
m.plot(projection='Mollview') # same as projection='MOL'
[9]:
<astropy.visualization.wcsaxes.core.WCSAxes at 0x12751a190>
Hammer-Aitoff Projection
Other useful pseudocylindrical projections include the Hammer-Aitoff projection (AIT)
[10]:
m.plot(projection='AIT')
[10]:
<astropy.visualization.wcsaxes.core.WCSAxes at 0x12092b150>
Cylindrical equal-area
the cylindrical equal-area projection, which preserves areas
[11]:
m.plot(projection='CEA')
[11]:
<astropy.visualization.wcsaxes.core.WCSAxes at 0x127552d10>
Plate Carée Projection
and the (Cylindrical) plate carée (CAR), AKA Cartesian, projection, which maps latitude and longitude coordinates one-to-one.
[12]:
m.plot(projection='CAR')
[12]:
<astropy.visualization.wcsaxes.core.WCSAxes at 0x1261a7110>
Zenithal Projections
Zenithal projections are radially symmetric in their distortion and tend to be better suited to plotting zoomed images.
Slant Orthographic Projection
One exception is the slant orthographic (SIN) projection, which (with default settings) looks like a globe viewed from a great distance with the image plotted on its surface:
[13]:
m.plot(projection='SIN')
[13]:
<astropy.visualization.wcsaxes.core.WCSAxes at 0x126c964d0>
/Users/s/anaconda3/envs/hpmoc-dev/lib/python3.7/site-packages/astropy/visualization/wcsaxes/grid_paths.py:73: RuntimeWarning: invalid value encountered in greater
discontinuous = step[1:] > DISCONT_FACTOR * step[:-1]
/Users/s/anaconda3/envs/hpmoc-dev/lib/python3.7/site-packages/astropy/visualization/wcsaxes/grid_paths.py:73: RuntimeWarning: invalid value encountered in greater
discontinuous = step[1:] > DISCONT_FACTOR * step[:-1]
/Users/s/anaconda3/envs/hpmoc-dev/lib/python3.7/site-packages/astropy/visualization/wcsaxes/grid_paths.py:73: RuntimeWarning: invalid value encountered in greater
discontinuous = step[1:] > DISCONT_FACTOR * step[:-1]
/Users/s/anaconda3/envs/hpmoc-dev/lib/python3.7/site-packages/astropy/visualization/wcsaxes/grid_paths.py:73: RuntimeWarning: invalid value encountered in greater
discontinuous = step[1:] > DISCONT_FACTOR * step[:-1]
Note that WCSAxes
don’t handle the tick labels well for slant orthographic projections; you will get warnings and missing or strangely-positioned tick labels, as in the plots on this page, when plotting this projection. You can fix the labels manually by playing with ax.coord
, but the easiest solution is to increase the plot resolution (at the expense of plotting speed), as is done in all of the remaining SIN skymaps in this document using the height
and width
parameters:
[14]:
m.plot(height=720, width=720, projection='SIN')
[14]:
<astropy.visualization.wcsaxes.core.WCSAxes at 0x1268333d0>
/Users/s/anaconda3/envs/hpmoc-dev/lib/python3.7/site-packages/astropy/visualization/wcsaxes/grid_paths.py:73: RuntimeWarning: invalid value encountered in greater
discontinuous = step[1:] > DISCONT_FACTOR * step[:-1]
/Users/s/anaconda3/envs/hpmoc-dev/lib/python3.7/site-packages/astropy/visualization/wcsaxes/grid_paths.py:73: RuntimeWarning: invalid value encountered in greater
discontinuous = step[1:] > DISCONT_FACTOR * step[:-1]
/Users/s/anaconda3/envs/hpmoc-dev/lib/python3.7/site-packages/astropy/visualization/wcsaxes/grid_paths.py:73: RuntimeWarning: invalid value encountered in greater
discontinuous = step[1:] > DISCONT_FACTOR * step[:-1]
/Users/s/anaconda3/envs/hpmoc-dev/lib/python3.7/site-packages/astropy/visualization/wcsaxes/grid_paths.py:73: RuntimeWarning: invalid value encountered in greater
discontinuous = step[1:] > DISCONT_FACTOR * step[:-1]
Gnomonic Projection
The gnomonic (TAN) projection is most often uzed for zoomed views of a specific region of the sky. Its default zoom level in HPMOC is chosen to be the same as that used by healpy.gnomview
.
[15]:
m.plot(projection='TAN')
[15]:
<astropy.visualization.wcsaxes.core.WCSAxes at 0x125a66d90>
Zenithal Equidistant Projection
The zenithal equidistant (ARC) projection is similarly suitable for this purpose, though by default it produces an all-sky view
[16]:
m.plot(projection='ARC')
[16]:
<astropy.visualization.wcsaxes.core.WCSAxes at 0x126990910>
Zenithal Equal-Area Projection
as does the zenithal equal-area (ZEA) projection:
[17]:
m.plot(projection='ZEA')
[17]:
<astropy.visualization.wcsaxes.core.WCSAxes at 0x125a5cf90>
Arbitrary WCS
Projections
These are likely to be the most useful projections for most purposes. However, any valid astropy.wcs.WCS
instance capable of being plotted on an astropy.visualization.wcsaxes.WCSAxes
can be passed as the projection argument. This is, in fact, how hpmoc.plot.plot works behind the scenes; you can separate this step out explicitly by calling hpmoc.plot.get_wcs to construct a WCS
instance
[18]:
w = get_wcs('MOL')
w
[18]:
WCS Keywords
Number of WCS axes: 2
CTYPE : 'RA---MOL' 'DEC--MOL'
CRVAL : 180.0 0.0
CRPIX : 180.5 90.5
PC1_1 PC1_2 : 1.0 0.0
PC2_1 PC2_2 : 0.0 1.0
CDELT : -0.9003163161571062 0.9003163161571062
NAXIS : 360.0 180
which can be used as the projection
argument. Note that hpmoc.plot.plot will not automatically select the frame around the plot when passed a WCS
instance as the projection, so we need to explicitly specify an elliptical frame:
[19]:
m.plot(projection=w, frame_class='elliptical')
[19]:
<astropy.visualization.wcsaxes.core.WCSAxes at 0x1267531d0>
Being able to use arbitrary WCS
instances as the projection makes it easy to overplot HEALPix skymaps on the same axes as images from the many observatories that store data using FITS.
Regardless of how a projection is specified, a WCSAxes
instance will always be returned, and its wcs
field will hold the resulting projection:
[20]:
m.plot().wcs
[20]:
WCS Keywords
Number of WCS axes: 2
CTYPE : 'RA---MOL' 'DEC--MOL'
CRVAL : 180.0 0.0
CRPIX : 180.5 90.5
PC1_1 PC1_2 : 1.0 0.0
PC2_1 PC2_2 : 0.0 1.0
CDELT : -0.9003163161571062 0.9003163161571062
NAXIS : 360.0 180
Rotations
hpmoc.plot.plot
puts a right ascension of 180 degrees at the center of the skymap (the default used by the LIGO collaboration for their skymaps). We can rotate the skymap to have a different center with the rot
argument, which specifies the three Euler angles (rotating about the ZXZ
reference axes) for rotating the skymap.
2-Angle Rotations
In practice, one usually simply wants to specify the coordinates at the center of the image. HPMOC handles this automatically for all built-in projections when provided only the right ascension and declination of the central point for the rot
argument. For example, we can center a high-probability region at RA=60, dec=-15 by passing those as the rot
argument:
[21]:
m.plot(rot=(60, -15))
[21]:
<astropy.visualization.wcsaxes.core.WCSAxes at 0x1267c07d0>
Again, this works for any built-in projection and is usually what you want to do. The final angle corresponding to the FITS WCS LONPOLE
keyword is calculated automatically so that the rotation is well-defined and the vertical direction at the center of the image points towards the north pole.
3-Angle Rotations
Sometimes, however, this isn’t enough. You may need to specify a custom WCS
for which HPMOC cannot automatically calculate LONPOLE
, or you may want to apply an additional rotation to your plot. This section briefly discusses how rot
is translated into FITS WCS keywords and how their interpretation varies accross different categories of projection.
The details of how rotations are set vary by projection; in particular, the valid angle combinations are not always intuitive. One can figure out the valid angles by referencing the projection definitions in Calabretta and Greisen (2002), though in practice a little bit of trial an error usually suffices to figure out the correct choices. The three components of the rot
argument tuple are used directly as the
CDELT1, CDELT2, LONPOLE
FITS header fields (explained in that document) for specifying a WCS.
HPMOC uses these coordinate definitions, non-intuitive properties and all, in the belief that cross-compatibility (with FITS in general and ``astropy.wcs.WCS`` in particular) outweigh the slight learning curve; that the unintuitive behaviors can be quickly learned with a little bit of documentation; and that the exceptional cases where ``LONPOLE`` need be specified are too rare to justify additional code complexity.
The effect of the rot
arguments is mostly within a certain category of projections; we will therefore discuss rotations in (pseudo)cylindrical and zenithal projections with separate examples.
Rotations in Cylindrical and Pseudocylindrical Projections
In this section we will consider the Mollweide projection, though the conclusions should mostly apply to the other (pseudo)cylindrical projections (Hammer-Aitoff and plate carée) as well.
We can center the skymap on a right-ascension of 60 degrees as follows:
[22]:
m.plot(rot=(60, 0, 0))
[22]:
<astropy.visualization.wcsaxes.core.WCSAxes at 0x1274b2510>
In the Mollweide projection, we can center the plot on a point with positive declination by providing it as the second angle in rot
. For example, we can center the plot on RA = 60, DEC = 15 as follows:
[23]:
m.plot(rot=(60, 15, 0))
[23]:
<astropy.visualization.wcsaxes.core.WCSAxes at 0x126452690>
We will get an error if we try to set the second Euler angle to -15 degrees, however, as we might want to do in this case to center the skymap on the region of highest probability. This can be fixed by setting the final angle to 180.
[24]:
m.plot(rot=(60, -15, 180))
[24]:
<astropy.visualization.wcsaxes.core.WCSAxes at 0x1267b0ad0>
Rotations in Zenithal Projections
In this section we will consider the slant orthographic projection, though the conclusions should mostly apply to the other zenithal projections (gnomonic, zenithal equidistant, zenithal equal-area) as well.
The rot
argument works more straightforwardly in zenithal projections, with a caveat. We can specify any right ascension and declination, e.g. a positive declination
[25]:
m.plot(height=720, width=720, projection='SIN', rot=(60, 15, 0))
[25]:
<astropy.visualization.wcsaxes.core.WCSAxes at 0x1270fa810>
/Users/s/anaconda3/envs/hpmoc-dev/lib/python3.7/site-packages/astropy/visualization/wcsaxes/grid_paths.py:73: RuntimeWarning: invalid value encountered in greater
discontinuous = step[1:] > DISCONT_FACTOR * step[:-1]
/Users/s/anaconda3/envs/hpmoc-dev/lib/python3.7/site-packages/astropy/visualization/wcsaxes/grid_paths.py:73: RuntimeWarning: invalid value encountered in greater
discontinuous = step[1:] > DISCONT_FACTOR * step[:-1]
/Users/s/anaconda3/envs/hpmoc-dev/lib/python3.7/site-packages/astropy/visualization/wcsaxes/grid_paths.py:73: RuntimeWarning: invalid value encountered in greater
discontinuous = step[1:] > DISCONT_FACTOR * step[:-1]
/Users/s/anaconda3/envs/hpmoc-dev/lib/python3.7/site-packages/astropy/visualization/wcsaxes/grid_paths.py:73: RuntimeWarning: invalid value encountered in greater
discontinuous = step[1:] > DISCONT_FACTOR * step[:-1]
or negative one
[26]:
m.plot(height=720, width=720, projection='SIN', rot=(60, -15, 0))
[26]:
<astropy.visualization.wcsaxes.core.WCSAxes at 0x12737db90>
/Users/s/anaconda3/envs/hpmoc-dev/lib/python3.7/site-packages/astropy/visualization/wcsaxes/grid_paths.py:73: RuntimeWarning: invalid value encountered in greater
discontinuous = step[1:] > DISCONT_FACTOR * step[:-1]
/Users/s/anaconda3/envs/hpmoc-dev/lib/python3.7/site-packages/astropy/visualization/wcsaxes/grid_paths.py:73: RuntimeWarning: invalid value encountered in greater
discontinuous = step[1:] > DISCONT_FACTOR * step[:-1]
/Users/s/anaconda3/envs/hpmoc-dev/lib/python3.7/site-packages/astropy/visualization/wcsaxes/grid_paths.py:73: RuntimeWarning: invalid value encountered in greater
discontinuous = step[1:] > DISCONT_FACTOR * step[:-1]
/Users/s/anaconda3/envs/hpmoc-dev/lib/python3.7/site-packages/astropy/visualization/wcsaxes/grid_paths.py:73: RuntimeWarning: invalid value encountered in greater
discontinuous = step[1:] > DISCONT_FACTOR * step[:-1]
but, by default, the image will be rotated. Fortunately, the final rot
tuple (corresponding to FITS LONPOLE
) simply rotates about the central reference axis, and the image can be oriented right-side-up by setting it to 180 (the default LONPOLE
value HPMOC uses for zenithal projections):
[27]:
m.plot(height=720, width=720, projection='SIN', rot=(60, -15, 180))
[27]:
<astropy.visualization.wcsaxes.core.WCSAxes at 0x127ee2d50>
/Users/s/anaconda3/envs/hpmoc-dev/lib/python3.7/site-packages/astropy/visualization/wcsaxes/grid_paths.py:73: RuntimeWarning: invalid value encountered in greater
discontinuous = step[1:] > DISCONT_FACTOR * step[:-1]
/Users/s/anaconda3/envs/hpmoc-dev/lib/python3.7/site-packages/astropy/visualization/wcsaxes/grid_paths.py:73: RuntimeWarning: invalid value encountered in greater
discontinuous = step[1:] > DISCONT_FACTOR * step[:-1]
/Users/s/anaconda3/envs/hpmoc-dev/lib/python3.7/site-packages/astropy/visualization/wcsaxes/grid_paths.py:73: RuntimeWarning: invalid value encountered in greater
discontinuous = step[1:] > DISCONT_FACTOR * step[:-1]
/Users/s/anaconda3/envs/hpmoc-dev/lib/python3.7/site-packages/astropy/visualization/wcsaxes/grid_paths.py:73: RuntimeWarning: invalid value encountered in greater
discontinuous = step[1:] > DISCONT_FACTOR * step[:-1]
Again, 2-angle rotations do this automatically; we can therefore make this same plot with
[28]:
m.plot(height=720, width=720, projection='SIN', rot=(60, -15))
[28]:
<astropy.visualization.wcsaxes.core.WCSAxes at 0x128182450>
/Users/s/anaconda3/envs/hpmoc-dev/lib/python3.7/site-packages/astropy/visualization/wcsaxes/grid_paths.py:73: RuntimeWarning: invalid value encountered in greater
discontinuous = step[1:] > DISCONT_FACTOR * step[:-1]
/Users/s/anaconda3/envs/hpmoc-dev/lib/python3.7/site-packages/astropy/visualization/wcsaxes/grid_paths.py:73: RuntimeWarning: invalid value encountered in greater
discontinuous = step[1:] > DISCONT_FACTOR * step[:-1]
/Users/s/anaconda3/envs/hpmoc-dev/lib/python3.7/site-packages/astropy/visualization/wcsaxes/grid_paths.py:73: RuntimeWarning: invalid value encountered in greater
discontinuous = step[1:] > DISCONT_FACTOR * step[:-1]
/Users/s/anaconda3/envs/hpmoc-dev/lib/python3.7/site-packages/astropy/visualization/wcsaxes/grid_paths.py:73: RuntimeWarning: invalid value encountered in greater
discontinuous = step[1:] > DISCONT_FACTOR * step[:-1]
Zoomed Plots
It’s possible to zoom in on an area of interest by modifying the hdelta
and vdelta
parameters, which override the angular widths of the reference pixels. If we’re zooming in on an area of interest, however, we’d be better served using a zenithal projection, e.g. the gnomonic projection (equivalent to that used by healpy.gnomview
). We can choose a different projection from one of those specified in `hpmoc.plot
<../hpmoc.plot.rst#hpmoc.plot.plot>`__, specifying it by name (or one
of its aliases), saving us the trouble of setting up a custom world coordinate system (astropy.wcs.WCS
) instance. A gnomonic plot, centered at the default location with default pixel sizes (hdelta
and vdelta
automatically calculated), looks like this:
[29]:
m.plot(projection='TAN', rot=(60, -15))
[29]:
<astropy.visualization.wcsaxes.core.WCSAxes at 0x1206138d0>
At this zoom level, we can clearly see the individual HEALPix pixels, but it’s hard to see the larger structure of the central probability region. We can zoom out by tweaking vdelta
and hdelta
, which in practice is easiest to do by trial and error until your plot looks how you’d like it to:
[30]:
m.plot(projection='TAN', vdelta=0.3, hdelta=0.3, rot=(60, -15))
[30]:
<astropy.visualization.wcsaxes.core.WCSAxes at 0x128a7b910>
Fixed Angular Width
You can also figure out exactly what hdelta
and vdelta
mean by, again, referencing Calabretta and Greisen (2002) and looking up the relevant projection’s interpretation of CDELT1
and CDELT
(the width and height, respectively, of the reference pixel in a given projection, which, it should be noted, does not correspond to the angular width of that pixel in most cases).
This is usually not worth the trouble, but can sometimes be useful, particularly when automating plotting, though in this case the problem can sometimes most easily be solved by choosing an appropriate projection; for example, if you want a circular window of radius 5 degrees, you can easily plot it using the zenithal equidistant projection:
[31]:
from hpmoc.plot import ARC_CDELT_BASE
radius_deg = 5
delta = ARC_CDELT_BASE * min(radius_deg, 180) / 180
m.plot(projection='ARC', hdelta=delta, vdelta=delta,
rot=(60, -15), frame_class='elliptical')
[31]:
<astropy.visualization.wcsaxes.core.WCSAxes at 0x12840b890>
Scatterplots
You can plot point sources, including their error regions, by passing collections of points (as hpmoc.points.PointsTuple instances) to plot
. You can specify each point source using its right ascension, declination (both in degrees), (optional) error region size (in degrees), and (optional) label, i.e. the name of that source (unnamed sources will be labeled with their index within the PointsTuple
):
[32]:
from hpmoc.points import PointsTuple, Rgba
pts1 = PointsTuple(
[
(271., 31., 2., 'One'),
(355., -44., 3.),
(15., -62., 5.),
(16., -60., 3.),
(10., -30., 10.),
]
)
m.plot(pts1)
[32]:
<astropy.visualization.wcsaxes.core.WCSAxes at 0x128dc1150>
The font is a little small for the point source labels; we can fix this by passing keyword arguments for WCSAxes.text
as the scatter_labels
argument:
[33]:
m.plot(pts1, scatter_labels={'fontsize': 16})
[33]:
<astropy.visualization.wcsaxes.core.WCSAxes at 0x1287d2410>
We can also increase the DPI of the figure to increase legibility:
[34]:
m.plot(pts1, fig={'dpi': 100})
[34]:
<astropy.visualization.wcsaxes.core.WCSAxes at 0x128bf8450>
You can plot multiple sets of points, which can be distinguished by color, marker, and legend label (M1 location from here):
[35]:
from astropy.units import Unit
pts2 = PointsTuple(
points=[
(179., 16., 2.),
(62, -14, 3., 'Candidate'),
(
(5*Unit('hourangle')+34.5*Unit('arcmin')).to('deg').value,
(22*Unit('deg')+1*Unit('arcmin')).to('deg').value,
(6*Unit('arcmin')).to('deg').value,
'M1'
)
],
rgba=Rgba(0.6, 0.2, 1, 0.5),
marker='2',
label='More points',
)
m.plot(pts1, pts2, fig={'dpi': 100})
plt.legend()
[35]:
<matplotlib.legend.Legend at 0x128d38c10>
You can also bundle point sources with a related skymap by appending them to the point_sources field of a PartialUniqSkymap
:
[36]:
m.point_sources = [pts1, pts2]
m.plot(fig={'dpi': 100})
plt.legend()
[36]:
<matplotlib.legend.Legend at 0x127a88e50>
Again, we can rotate and zoom in on a region of interest; our point sources will now be plotted automatically (since they are contained in the point_sources
field):
[37]:
m.plot(fig={'dpi': 120}, rot=(70, 5), vdelta=0.3, hdelta=0.3, projection='TAN')
[37]:
<astropy.visualization.wcsaxes.core.WCSAxes at 0x127e21290>
Contours around Credible Regions
We can also add contours around credible/containment regions of the plot. For example, to get the smallest region containing 90% of the GW probability, run
[38]:
m.plot(fig={'dpi': 100}, cr=[.9])
[38]:
<astropy.visualization.wcsaxes.core.WCSAxes at 0x1299a9350>
Or, with our zoomed plot:
[39]:
m.plot(fig={'dpi': 120}, rot=(70, 5), vdelta=0.3, hdelta=0.3, projection='TAN', cr=[0.9])
[39]:
<astropy.visualization.wcsaxes.core.WCSAxes at 0x129ec0cd0>
We can easily add the 50% contour as well:
[40]:
m.plot(fig={'dpi': 120}, cr=[0.5, 0.9])
[40]:
<astropy.visualization.wcsaxes.core.WCSAxes at 0x12a204fd0>
[41]:
m.plot(fig={'dpi': 120}, rot=(70, 5), vdelta=0.3, hdelta=0.3, projection='TAN', cr=[0.5, 0.9])
[41]:
<astropy.visualization.wcsaxes.core.WCSAxes at 0x12a9fc890>
Our example “Candidate” event is clearly within both contours.
Multi-layered plots
Sometimes it is desirable to overlay several images. For example, we can visualize both the skymap itself and the size of its pixels by plotting them as a semi-transparent overlay using the alpha
keyword and the same axis as the original skymap. We can include colorbars for both and pick colormaps which are visually distinguishable. We also tweak the figure size and cbar
keyword arguments to improve the layout of the final result:
[42]:
ax = m.plot(fig={'figsize': (6, 5)}, cbar={'pad': 0.05}, cmap='bone_r')
m.orders(as_skymap=True).plot(ax=ax, alpha=0.15, cmap='rainbow', cbar={'pad': 0.1})
ax.set_title('S200105ae')
[42]:
Text(0.5, 1.0, 'S200105ae')
We can also get rid of the point sources again, since they are visually distracting, and increase the opacity of the pixel sizes (ORDER
) to emphasize that layer:
[43]:
m.point_sources = []
ax = m.plot(fig={'figsize': (6, 5)}, cbar={'pad': 0.05}, cmap='bone_r')
m.orders(as_skymap=True).plot(ax=ax, alpha=0.4, cmap='rainbow', cbar={'pad': 0.1})
ax.set_title('S200105ae')
[43]:
Text(0.5, 1.0, 'S200105ae')
If we wanted to emphasize the pixel size completely, we could plot it alone and then simply plot some contours from the original skymap over it. We can entirely hide an image (but not the contours or associated point sources) by setting cmap=None
and label and color our contours with the cr_format
and cr_kwargs
arguments:
[44]:
ax = m.orders(as_skymap=True).plot(projection='CAR', fig={'dpi': 120}, cbar=True)
m.plot(ax=ax, cmap=None, cr=[0.5, 0.9, 0.99],
cr_format=lambda l, _: f"{l*100:2.0f}% CR",
cr_kwargs={'colors': ['aquamarine', 'teal', 'blue']})
[44]:
<astropy.visualization.wcsaxes.core.WCSAxes at 0x12d1f5750>
You can, of course, use this approach to layer skymaps over images from arbitrary FITS sources. Let’s plot a random Swift-BAT high-SNR FITS image (downloaded from astroquery
) alongside our second set of point sources with only the credible region boundaries from our skymap showing:
[45]:
from astropy.wcs import WCS
from astroquery.skyview import SkyView
# get a Swift-BAT high-SNR FITS image from around M1 (Crab Nebula)
hdu = SkyView.get_images(position='M1', survey='BAT SNR 150-195')[0][0]
ax = plt.subplot(1, 1, 1, projection=WCS(hdu.header))
ax.imshow(hdu.data, cmap='gist_heat_r')
m.plot(pts2, ax=ax, cmap=None, cr=[0.5, 0.9])
[45]:
<matplotlib.axes._subplots.WCSAxesSubplot at 0x12e8ab910>
We can also interpolate a standard FITS image (as long as the astropy.wcs.WCS
coordinate transforms are implemented in FITS) using HPMOC. This lets us put images in arbitrary projections into the same pixelization, making it easy to plot them on the arbitrary axes, do analyses with them, etc. You can see that the results are indistinguishable from the original:
[46]:
# interpolate the FITS image
mh = PartialUniqSkymap(hdu.data, WCS(hdu.header))
# plot a comparison
fig = plt.figure(figsize=(8, 4))
axw = fig.add_subplot(1, 2, 1, projection=WCS(hdu.header))
axw.imshow(hdu.data, cmap='gist_heat_r')
axh = mh.plot(fig=axw.figure, subplot=(1, 2, 2), projection=WCS(hdu.header))
axh.grid(False)
More usefully, we can do an all-sky plot of an arbitrarily-projected FITS image with almost no effort. We can label the missing regions as gray and increase the resolution of the image to capture the small bright region in the center:
[47]:
mh.plot(missing_color='gray', width=1000, height=500)
[47]:
<astropy.visualization.wcsaxes.core.WCSAxes at 0x12eb00710>
Note that by default, nearest neighbor interpolation will be used (at around 5-10 times the resolution of the original image), and units (and particularly area calculations/conversions) will not be handled automatically.
Subplots
You can plot to subplots using the subplot argument. For example, we can manually make a grid of subplots using plt.subplot
and pass the corresponding axes to plot. We can accomplish the same thing by passing the subplot
argument to plot
:
[48]:
m2 = PartialUniqSkymap.read(BAYESTAR_NEST_FITS_GZ_1024, strategy='ligo')
ax1 = m.plot(fig={'figsize': (16, 4)}, subplot=(1, 2, 1), cr=[0.9])
ax1.set_title('S200105ae')
ax2 = m2.plot(fig=ax1.figure, subplot=(1, 2, 2), cr=[0.9])
ax2.set_title('S200316bj')
[48]:
Text(0.5, 1.0, 'S200316bj')
This also works with matplotlib.gridspec.SubplotSpec
as the subplot
argument (if you want to plot to a specific axes on a matplotlib.gridspec.GridSpec
). We can also get the axis manually with matplotlib.figure.Figure.add_subplot
and pass it as the ax
argument.
Grid Plots
It is often useful to compare skymaps, or look at the same skymap with different sets of point sources, plot settings, or projections. The quickest way to do this using HPMOC is to use hpmoc.plot.gridplot, which uses GridSpec
behind the scenes. We can quickly do the same as above:
[49]:
m.gridplot(m2, cr=[0.9])
[49]:
(GridSpec(1, 2, width_ratios=[2.0, 2.0]),
[[<matplotlib.axes._subplots.WCSAxesSubplot at 0x12f67f4d0>,
<matplotlib.axes._subplots.WCSAxesSubplot at 0x12f68c1d0>]])
You can also specify multiple projections for each skymap:
[50]:
m.gridplot(m2, projections=['ARC', 'ZEA', 'TAN'])
[50]:
(GridSpec(1, 6, width_ratios=[1.0, 1.0, 1.0, 1.0, 1.0, 1.0]),
[[<matplotlib.axes._subplots.WCSAxesSubplot at 0x12f8fe810>,
<matplotlib.axes._subplots.WCSAxesSubplot at 0x12f641e10>],
[<matplotlib.axes._subplots.WCSAxesSubplot at 0x12f388e50>,
<matplotlib.axes._subplots.WCSAxesSubplot at 0x12f254610>],
[<matplotlib.axes._subplots.WCSAxesSubplot at 0x12ff35390>,
<matplotlib.axes._subplots.WCSAxesSubplot at 0x12f25a890>]])
Specify how many columns (where different projections of the same skymap are grouped into a single column) appear on each row:
[51]:
m.gridplot(m2, projections=['CAR', 'ZEA', 'TAN'], ncols=1)
[51]:
(GridSpec(2, 3, width_ratios=[2.0, 1.0, 1.0]),
[[<matplotlib.axes._subplots.WCSAxesSubplot at 0x130811810>,
<matplotlib.axes._subplots.WCSAxesSubplot at 0x12f67fd90>],
[<matplotlib.axes._subplots.WCSAxesSubplot at 0x1308119d0>,
<matplotlib.axes._subplots.WCSAxesSubplot at 0x130777710>],
[<matplotlib.axes._subplots.WCSAxesSubplot at 0x130f10490>,
<matplotlib.axes._subplots.WCSAxesSubplot at 0x12d58c7d0>]])
The first return value is the GridSpec
used to arrange the subplots, while the second is a list whose rows correspond to each projection in the projections
argument and are themselves lists of axes corresponding to each skymap. In other words, gridplot(...)[1][p][s]
is the axes containing a plot of the s skymap using the p projection. You can use this to modify specific subplots, add titles to all plots, etc:
[52]:
maps = [m, *[PartialUniqSkymap.read(f, strategy='ligo') for f in
(BAYESTAR_NEST_FITS_GZ_1024, CWB_NEST_FITS_GZ_128,
CWB_RING_FITS_GZ_128)]]
gs, axs = gridplot(*maps, projections=['CEA', 'TAN'])
for *axx, title in zip(*axs, ('S200105ae', 'S200316bj-1-Preliminary',
'S200114f-3-Initial', 'S200129m-3-Initial')):
for ax in axx:
ax.set_title(title)
We can add scatter plots of points of interest using the scatters
argument to pass a list of PointsTuple instances for each skymap, and we can pass separate keyword arguments to hpmoc.plot.plot for each subplot (following the same shape as the returned axes). We can use this, for example, to adjust the zoom level and rotation of the gnomonic projections separately from the all-sky views and to set the
vmax
values for the color maps consistently across projections. Arguments shared across all subplots can be passed as keyword arguments to gridplot, as we will do for vmin=0
:
[53]:
maps = [m, *[PartialUniqSkymap.read(f, strategy='ligo') for f in
(BAYESTAR_NEST_FITS_GZ_1024, CWB_NEST_FITS_GZ_128,
CWB_RING_FITS_GZ_128)]]
gs, axs = gridplot(*maps, scatters=[[pts2]]*4, projections=['CEA', 'TAN'],
subplot_kwargs=[[{'vmax': s.max().value} for s in maps],
[{'vdelta': 0.4, 'hdelta': 0.4,
'rot': (70, 5), 'vmax': s.max().value}
for s in maps]], vmin=0, ncols=1, cr=[0.9])
for *axx, title in zip(*axs, ('S200105ae', 'S200316bj-1-Preliminary',
'S200114f-3-Initial', 'S200129m-3-Initial')):
for ax in axx:
ax.set_title(title)
/Users/s/anaconda3/envs/hpmoc-dev/lib/python3.7/site-packages/astropy/visualization/wcsaxes/core.py:234: UserWarning: No contour levels were found within the data range.
cset = super().contour(*args, **kwargs)
We can add colorbars and layer our images as we were able to do with plot
, though this time instead of passing ax
to our second invocation of the plotting function, we pass the returned GridSpec
and axes list (after tweaking some of the spacing arguments to make the layout look better). We can use this approach to make quite intricate visualizations for multiple skymaps with fairly little effort:
[54]:
maps = [m, *[PartialUniqSkymap.read(f, strategy='ligo') for f in
(BAYESTAR_NEST_FITS_GZ_1024, CWB_NEST_FITS_GZ_128,
CWB_RING_FITS_GZ_128)]]
gs, axs = gridplot(*maps, wspace=0.2, hspace=0.05, wshrink=0.7,
scatters=[[pts2]]*4, ncols=1, top=0.95,
projections=['CEA', 'TAN'], height=360,
subplot_kwargs=[[{'width': 720, 'vmax': s.max().value}
for s in maps],
[{'width': 360, 'vdelta': 0.2,
'hdelta': 0.2, 'cbar': True,
'rot': (70, 5), 'vmax': s.max().value}
for s in maps]],
cr=[0.5, 0.9, 0.99], vmin=0., cmap='bone_r',
cr_kwargs={'colors': ['aquamarine', 'teal', 'blue']})
for *axx, title in zip(*axs, ('S200105ae', 'S200316bj-1-Preliminary',
'S200114f-3-Initial', 'S200129m-3-Initial')):
for ax in axx:
ax.set_title(title)
orders = [s.orders(as_skymap=True) for s in maps]
gridplot(*orders, projections=axs, fig=gs, alpha=0.3,
cmap='rainbow', vmin=0,
subplot_kwargs=[[{'cbar': {'shrink': 0.5}, 'vmax': o.max()}
for o in orders], [{'cmap': None}]*4])
plt.suptitle("Order vs. Probability Density Near Candidate & M1")
/Users/s/anaconda3/envs/hpmoc-dev/lib/python3.7/site-packages/astropy/visualization/wcsaxes/core.py:234: UserWarning: No contour levels were found within the data range.
cset = super().contour(*args, **kwargs)
/Users/s/anaconda3/envs/hpmoc-dev/lib/python3.7/site-packages/hpmoc/plot.py:886: MatplotlibDeprecationWarning: Adding an axes using the same arguments as a previous axes currently reuses the earlier instance. In a future version, a new instance will always be created and returned. Meanwhile, this warning can be suppressed, and the future behavior ensured, by passing a unique label to each axes instance.
frame_class=frame_class)
[54]:
Text(0.5, 0.98, 'Order vs. Probability Density Near Candidate & M1')