Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# (c) Stefan Countryman 2016-2018
3"""
4More complex plotting commands for joint events in more human-readable formats,
5e.g. the GW skymap plots from O2 with their overlaid crosses marking neutrino
6positions.
7"""
9# TODO actually import this into the healpix skymaps or otherwise make it
10# available for joint plots
11from llama.files.healpix.skymap import MollviewMixin
12from llama.filehandler import FileHandler
13from llama.filehandler.mixins import FileExtensionMixin
15DPI = 200
16WIDTH = 6 # [inches]
17HEIGHT = 4 # [inches]
18OUTLINE_STROKE = 1.2 # thickness of outline surrounding text
19TITLE_OUTLINE_STROKE = 2 # thickness of outline surrounding plot titles
20OUTLINE_COLOR = (1, 1, 1, 1) # outlines of text and neutrino markers
21LEFT_SHIFT_COEFF = 1/20. # quadratic curve dec labels away from meridian
22LEFT_SHIFT_BASE = -20 # baseline shift from leftmost meridian [deg]
23XMARGIN = 0.4 # additional margin in x (make sure text fits) [inches]
24TOPMARGIN = -0.0 # additional margin at top [inches]
25BOTTOMMARGIN = -0.0 # additional margin at bottom [inches]
26NEUTRINO_COLOR = 'green'
29def default_cmap():
30 """Get the matplotlib colormap."""
31 from matplotlib import cm
32 # cmap = 'magma_r' # colormap to use (matplotlib.org/users/colormaps.html)
33 # colormap to use (matplotlib.org/users/colormaps.html).
34 # NOTE that due to a little bug in healpy
35 # (stackoverflow.com/questions/34023932), you'll get a hideous background color
36 # (corresponding to the min. value color for your cmap) behind the plot. this
37 # gets fixed with the line after the next one.
39 # O2 classic color scheme
40 cmap = cm.gist_heat_r
42 # cmap = cm.BuPu
43 # cmap = cm.bone_r
44 # cmap = cm.YlGnBu
45 # cmap = cm.gnuplot2_r
46 # cmap = cm.CMRmap_r
47 cmap.set_under((0, 0, 0, 0)) # transparent background
48 return cmap
51DEC_X_OFFSET = -0.37 # [inches]
52DEC_Y_OFFSET = -0.02 # [inches]
53RA_X_OFFSET = 0 # [inches]
54RA_Y_OFFSET = 0.06 # [inches]
55N_X_OFFSET = 0.08 # [inches]
56N_Y_OFFSET = 0.08 # [inches]
57CENTRAL_LON = 180 # longitude to place at center of skymap [deg]
58CENTRAL_LAT = 0 # latitude to place at center of skymap [deg]
59PSI = 0 # additional rotation of skymap about center
60DELTA_PARALLEL = 15 # space between parallels in the graticule [deg]
61DELTA_MERIDIAN = 30 # space between meridians in the graticule [deg]
62GRATICULE_COLOR = "#B0B0B0"
63GRATICULE_LABEL_COLOR = (0.2, 0.2, 0.2)
64TITLES = (
65 '1-detector Skymap (LHO)',
66 '2-detector Skymap (LHO, LLO)',
67 '3-detector Skymap (LHO, LLO, Virgo)',
68)
69DIRNAMES = ('1det', '2det', '3det')
70DEFAULT_SCATTER_MARKER = "$\\bigodot$" # bullseye hides less GW density
71MERIDIAN_FONT_SIZE = 11
72FONT_SIZE = 14 # matplotlib font size
73UNCERTAINTY_ALPHA = 0.4 # opacity in [0,1] for scatterplot uncertainty discs
74DEFAULT_PLOT_EXTENSION = 'pdf' # file type to save plots as
77def outline_effect():
78 """Get a ``matplotlib.patheffects.withStroke`` effect that outlines text
79 nicely to improve plot readability."""
80 from matplotlib.patheffects import withStroke
81 return withStroke(
82 linewidth=OUTLINE_STROKE,
83 foreground=OUTLINE_COLOR,
84 )
87# pylint: disable=too-many-arguments
88def plot_healpix(
89 skymap,
90 title=None,
91 unit="Probability Density",
92 central_longitude=CENTRAL_LON,
93 central_latitude=CENTRAL_LAT,
94 rotation_about_center=PSI,
95 dpi=DPI,
96 cbar=None,
97 cmap=None,
98 alpha=None,
99 **kwargs
100):
101 """Plot a healpix skymap in a LLAMA-specific style. Works on skymap objects
102 with a HEALpy-esque ``mollview`` method. Returns a figure object for this
103 skymap plot. No colorbar by default. Extra kwargs are passed to
104 ``skymap.mollview``. Define the opacity of the skymap with ``alpha``
105 (must be in [0,1]). In particular, use ``hold=True`` to use the current
106 axis for this plot."""
107 import numpy as np
108 import healpy as hp
109 from matplotlib.patheffects import withStroke
110 from matplotlib.transforms import ScaledTranslation
111 from matplotlib import pyplot as plt
112 if cmap is None:
113 cmap = default_cmap()
114 # make the fonts bigger than the default 10pt
115 plt.rcParams['font.size'] = FONT_SIZE
116 if alpha:
117 if alpha < 0 or alpha > 1:
118 raise ValueError("``alpha`` must be in [0,1].")
119 # TODO create a custom cmap based off of ``cmap`` and ``alpha``
120 # arguments with ``alpha`` opacity and redefine ``cmap`` as that.
121 skymap.mollview(
122 unit=unit,
123 rot=(central_longitude, central_latitude, rotation_about_center),
124 cbar=cbar,
125 cmap=cmap,
126 **kwargs
127 )
128 # size the figure
129 fig = plt.gcf()
130 fig.set_size_inches(WIDTH, HEIGHT)
131 fig.set_dpi(dpi)
132 if title is not None:
133 plt.title(
134 title,
135 path_effects=[
136 withStroke(
137 linewidth=TITLE_OUTLINE_STROKE,
138 foreground='w',
139 ),
140 ],
141 )
142 # add extra margin so that the declination labels fit
143 x_1, x_2, y_1, y_2 = plt.axis()
144 plt.axis((x_1-XMARGIN, x_2+XMARGIN, y_1-TOPMARGIN, y_2+BOTTOMMARGIN))
145 # Make a matplotlib translation to offset text labels by a bit so that
146 # they don't cover up the scatter plot markers they are labeling. See:
147 # https://matplotlib.org/users/transforms_tutorial.html
148 transdata = plt.gca().transData
149 dectrans = transdata + ScaledTranslation(DEC_X_OFFSET, DEC_Y_OFFSET,
150 fig.dpi_scale_trans)
151 ratrans = transdata + ScaledTranslation(RA_X_OFFSET, RA_Y_OFFSET,
152 fig.dpi_scale_trans)
153 # add a graticule
154 hp.visufunc.graticule(
155 DELTA_PARALLEL,
156 DELTA_MERIDIAN,
157 color=GRATICULE_COLOR,
158 )
159 # label declinations
160 for dec in np.arange(DELTA_PARALLEL-90, 90, DELTA_PARALLEL):
161 hp.projtext(
162 # curve dec labels away from leftmost meridian
163 LEFT_SHIFT_BASE+abs(dec*LEFT_SHIFT_COEFF)**2,
164 dec,
165 # label dec, right-pad to avoid hitting plot
166 str(dec) + '$\\degree$',
167 lonlat=True,
168 color=GRATICULE_LABEL_COLOR,
169 transform=dectrans,
170 horizontalalignment='right',
171 verticalalignment='center',
172 path_effects=[outline_effect()],
173 )
174 # label right ascensions
175 for right_ascension in np.arange(DELTA_MERIDIAN, 360, DELTA_MERIDIAN):
176 hp.projtext(
177 right_ascension,
178 0,
179 str(right_ascension) + '$\\degree$',
180 lonlat=True,
181 color=GRATICULE_LABEL_COLOR,
182 transform=ratrans,
183 horizontalalignment='center',
184 verticalalignment='bottom',
185 fontsize=MERIDIAN_FONT_SIZE,
186 path_effects=[outline_effect()],
187 )
188 return fig
191# pylint: disable=too-many-arguments
192def add_scatterplot(
193 right_ascensions,
194 declinations,
195 marker=DEFAULT_SCATTER_MARKER,
196 color=NEUTRINO_COLOR,
197 zorder=1000,
198 **kwargs
199):
200 """Add a scatterplot to the current HEALpy axis. Useful for plotting
201 neutrinos etc.; remaining ``kwargs`` are passed to
202 ``healpy.projscatter``. Returns the current figure."""
203 import numpy as np
204 import healpy as hp
205 from matplotlib.colors import ColorConverter
206 from matplotlib.transforms import ScaledTranslation
207 from matplotlib import pyplot as plt
208 # make the fonts bigger than the default 10pt
209 plt.rcParams['font.size'] = FONT_SIZE
210 events = np.array([right_ascensions, declinations])
211 # canonicalize input colors as RGBA
212 color = ColorConverter.to_rgba(color)
213 # uncertainty_color = color[0:3] + (UNCERTAINTY_ALPHA,)
215 # add an 'x' at each event location
216 hp.projscatter(
217 events,
218 lonlat=True,
219 edgecolor=OUTLINE_COLOR,
220 linewidth=0.5,
221 facecolor=color,
222 rasterized=False,
223 marker=marker,
224 s=(2*plt.rcParams['lines.markersize']) ** 2, # double default size
225 zorder=zorder,
226 **kwargs
227 )
228 # Make a matplotlib translation to offset text labels by a bit so that
229 # they don't cover up the scatter plot markers they are labeling. See:
230 # https://matplotlib.org/users/transforms_tutorial.html
231 fig = plt.gcf()
232 transdata = plt.gca().transData
233 ntrans = transdata + ScaledTranslation(N_X_OFFSET, N_Y_OFFSET,
234 fig.dpi_scale_trans)
235 # label events
236 for i, event in enumerate(events.transpose()):
237 hp.projtext(
238 event[0],
239 event[1],
240 str(i+1),
241 lonlat=True,
242 color=color,
243 transform=ntrans,
244 zorder=zorder,
245 path_effects=[outline_effect()],
246 )
247 return fig
250class JointSkymapScatter(FileHandler, MollviewMixin, FileExtensionMixin):
251 """
252 Plot a base skymap but with a scatterplot added on top. This is a bit
253 different from joint skymaps that contain information on pixelwise
254 likelihood products since it is only meant to be a human-readable
255 representation.
257 Required Class Attributes
258 -------------------------
259 The following class attributes must be defined in subclasses, either
260 manually or programmatically (see: ``FileHandler.set_class_attributes`` and
261 its implementations in subclasses).
263 ``POINT_SOURCES``
264 ~~~~~~~~~~~~~~~~~
265 A dictionary mapping ``TriggerList`` and ``HEALPixPSF`` subclass instances
266 (i.e. pointlike-source lists) to a dict of style properties that can be
267 fed to ``matplotlib.pyplot.scatterplot`` as kwargs. In particular, you MUST
268 define ``marker`` and ``color`` in order to distinguish between different
269 types of sources.
271 ``BACKGROUND_SKYMAP``
272 ~~~~~~~~~~~~~~~~~~~~~
273 The ``HEALPixRepresentableFileHandler`` containing skymaps that should
274 be used as the continuous background (against which point-sources will
275 be plotted).
276 """
278 POINT_SOURCES = None
279 BACKGROUND_SKYMAP = None
281 _REQUIRED = ("POINT_SOURCES", "BACKGROUND_SKYMAP")
283 @classmethod
284 def set_class_attributes(cls, subclass):
285 """See ``FileHandler.set_class_attributes``; this method additionally
286 sets the ``DEPENDENCIES`` and ``FILENAME`` attributes based on
287 ``subclass.POINT_SOURCES`` and ``subclass.BACKGROUND_SKYMAP``."""
288 subclass.DEPENDENCIES = tuple(
289 [subclass.BACKGROUND_SKYMAP] +
290 sorted(
291 subclass.POINT_SOURCES.keys(),
292 key=lambda s: s.DETECTORS[0].name
293 )
294 )
295 assert all(len(d.DETECTORS) == 1 for d in subclass.DEPENDENCIES)
296 subclass.FILENAME = 'scatterplot_{}.{}'.format(
297 '-'.join(
298 d.DETECTORS[0].abbrev.lower()
299 for d in subclass.DEPENDENCIES
300 ),
301 subclass.FILEEXT,
302 )
303 super().set_class_attributes(subclass)
304 return subclass
306 def mollview(self, **kwargs):
307 """Return a Mollweide projection of this skymap as a ``matplotlib``
308 figure. Remaining ``**kwargs`` are passed to ``healpy.mollview``."""
309 # TODO make sure we can handle MULTIPLE background skymaps.
310 import numpy as np
311 skymap = self.BACKGROUND_SKYMAP(self).get_healpix()[0]
312 if 'title' not in kwargs:
313 if skymap.title is not None:
314 kwargs['title'] = skymap.title
315 else:
316 names = ([self.BACKGROUND_SKYMAP.DETECTORS[0].name] +
317 [p.DETECTORS[0].name for p in self.POINT_SOURCES])
318 kwargs['title'] = f"{'/'.join(names)} {self.eventid}"
319 fig = plot_healpix(
320 skymap,
321 unit=skymap.unit,
322 **kwargs
323 )
324 for filehandler, plot_properties in self.POINT_SOURCES.items():
325 point_sources = np.array(filehandler(self).source_locations())
326 if len(point_sources):
327 add_scatterplot(
328 right_ascensions=point_sources[:, 0],
329 declinations=point_sources[:, 1],
330 **plot_properties
331 )
332 return fig
334 def _generate(self):
335 self.mollview().savefig(self.fullpath)