Hide keyboard shortcuts

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

1from builtins import zip 

2import numbers 

3import numpy as np 

4import warnings 

5import healpy as hp 

6from matplotlib import colors 

7from matplotlib import ticker 

8import matplotlib.pyplot as plt 

9import matplotlib.cm as cm 

10from matplotlib.ticker import FuncFormatter 

11import matplotlib as mpl 

12from matplotlib.patches import Ellipse 

13from matplotlib.collections import PatchCollection 

14 

15from lsst.sims.maf.utils import optimalBins, percentileClipping 

16from .plotHandler import BasePlotter, applyZPNorm 

17 

18from lsst.sims.utils import _equatorialFromGalactic, _healbin 

19from .perceptual_rainbow import makePRCmap 

20perceptual_rainbow = makePRCmap() 

21import numpy.ma as ma 

22 

23__all__ = ['setColorLims', 'setColorMap', 'HealpixSkyMap', 'HealpixPowerSpectrum', 

24 'HealpixHistogram', 'OpsimHistogram', 'BaseHistogram', 

25 'BaseSkyMap', 'HealpixSDSSSkyMap', 'LambertSkyMap'] 

26 

27baseDefaultPlotDict = {'title': None, 'xlabel': None, 'label': None, 

28 'logScale': False, 'percentileClip': None, 'normVal': None, 'zp': None, 

29 'cbarFormat': None, 'cmap': perceptual_rainbow, 'cbar_edge': True, 'nTicks': 10, 

30 'colorMin': None, 'colorMax': None, 

31 'xMin': None, 'xMax': None, 'yMin': None, 'yMax': None, 

32 'labelsize': None, 'fontsize': None, 'figsize': None, 'subplot': 111} 

33 

34 

35def setColorLims(metricValue, plotDict): 

36 """Set up color bar limits.""" 

37 # Use plot dict if these values are set. 

38 colorMin = plotDict['colorMin'] 

39 colorMax = plotDict['colorMax'] 

40 # If not, try to use percentile clipping. 

41 if (plotDict['percentileClip'] is not None) & (np.size(metricValue.compressed()) > 0): 

42 pcMin, pcMax = percentileClipping(metricValue.compressed(), percentile=plotDict['percentileClip']) 

43 if colorMin is None: 

44 colorMin = pcMin 

45 if colorMax is None: 

46 colorMax = pcMax 

47 # If not, just use the data limits. 

48 if colorMin is None: 

49 colorMin = metricValue.compressed().min() 

50 if colorMax is None: 

51 colorMax = metricValue.compressed().max() 

52 # But make sure there is some range on the colorbar 

53 if colorMin == colorMax: 

54 colorMin = colorMin - 0.5 

55 colorMax = colorMax + 0.5 

56 return np.sort([colorMin, colorMax]) 

57 

58 

59def setColorMap(plotDict): 

60 cmap = plotDict['cmap'] 

61 if cmap is None: 

62 cmap = 'perceptual_rainbow' 

63 if type(cmap) == str: 

64 cmap = getattr(cm, cmap) 

65 # Set background and masked pixel colors default healpy white and gray. 

66 cmap.set_over(cmap(1.0)) 

67 cmap.set_under('w') 

68 cmap.set_bad('gray') 

69 return cmap 

70 

71 

72class HealpixSkyMap(BasePlotter): 

73 """ 

74 Generate a sky map of healpix metric values using healpy's mollweide view. 

75 """ 

76 def __init__(self): 

77 super(HealpixSkyMap, self).__init__() 

78 # Set the plotType 

79 self.plotType = 'SkyMap' 

80 self.objectPlotter = False 

81 # Set up the default plotting parameters. 

82 self.defaultPlotDict = {} 

83 self.defaultPlotDict.update(baseDefaultPlotDict) 

84 self.defaultPlotDict.update({'rot': (0, 0, 0), 'flip': 'astro', 'coord': 'C', 

85 'nside': 8, 'reduceFunc': np.mean}) 

86 # Note: for alt/az sky maps using the healpix plotter, you can use 

87 # {'rot': (90, 90, 90), 'flip': 'geo'} 

88 self.healpy_visufunc = hp.mollview 

89 self.healpy_visufunc_params = {} 

90 self.ax = None 

91 self.im = None 

92 

93 def __call__(self, metricValueIn, slicer, userPlotDict, fignum=None): 

94 """ 

95 Parameters 

96 ---------- 

97 metricValue : numpy.ma.MaskedArray 

98 slicer : lsst.sims.maf.slicers.HealpixSlicer 

99 userPlotDict: dict 

100 Dictionary of plot parameters set by user (overrides default values). 

101 fignum : int 

102 Matplotlib figure number to use (default = None, starts new figure). 

103 

104 Returns 

105 ------- 

106 int 

107 Matplotlib figure number used to create the plot. 

108 """ 

109 # Override the default plotting parameters with user specified values. 

110 plotDict = {} 

111 plotDict.update(self.defaultPlotDict) 

112 plotDict.update(userPlotDict) 

113 

114 # Check if we have a valid HEALpix slicer 

115 if 'Heal' in slicer.slicerName: 

116 # Update the metric data with zeropoint or normalization. 

117 metricValue = applyZPNorm(metricValueIn, plotDict) 

118 else: 

119 # Bin the values up on a healpix grid. 

120 metricValue = _healbin(slicer.slicePoints['ra'], slicer.slicePoints['dec'], 

121 metricValueIn.filled(slicer.badval), nside=plotDict['nside'], 

122 reduceFunc=plotDict['reduceFunc'], fillVal=slicer.badval) 

123 metricValue = ma.array(metricValue) 

124 metricValue = applyZPNorm(metricValue, plotDict) 

125 

126 # Generate a Mollweide full-sky plot. 

127 fig = plt.figure(fignum, figsize=plotDict['figsize']) 

128 # Set up color bar limits. 

129 clims = setColorLims(metricValue, plotDict) 

130 cmap = setColorMap(plotDict) 

131 # Set log scale? 

132 norm = None 

133 if plotDict['logScale']: 

134 norm = 'log' 

135 # Avoid trying to log scale when zero is in the range. 

136 if (norm == 'log') & ((clims[0] <= 0 <= clims[1]) or (clims[0] >= 0 >= clims[1])): 

137 # Try something simple 

138 above = metricValue[np.where(metricValue > 0)] 

139 if len(above) > 0: 

140 clims[0] = above.max() 

141 # If still bad, give up and turn off norm 

142 if ((clims[0] <= 0 <= clims[1]) or (clims[0] >= 0 >= clims[1])): 

143 norm = None 

144 warnings.warn("Using norm was set to log, but color limits pass through 0. " 

145 "Adjusting so plotting doesn't fail") 

146 if plotDict['coord'] == 'C': 

147 notext = True 

148 else: 

149 notext = False 

150 

151 visufunc_params = {'title': plotDict['title'], 

152 'cbar': False, 

153 'min': clims[0], 

154 'max': clims[1], 

155 'rot': plotDict['rot'], 

156 'flip': plotDict['flip'], 

157 'coord': plotDict['coord'], 

158 'cmap': cmap, 

159 'norm': norm, 

160 'sub': plotDict['subplot'], 

161 'fig':fig.number, 

162 'notext': notext} 

163 visufunc_params.update(self.healpy_visufunc_params) 

164 self.healpy_visufunc(metricValue.filled(slicer.badval), **visufunc_params) 

165 

166 # Add a graticule (grid) over the globe. 

167 hp.graticule(dpar=30, dmer=30, verbose=False) 

168 # Add colorbar (not using healpy default colorbar because we want more tickmarks). 

169 self.ax = plt.gca() 

170 im = self.ax.get_images()[0] 

171 # Add label. 

172 if plotDict['label'] is not None: 

173 plt.figtext(0.8, 0.8, '%s' % (plotDict['label'])) 

174 # Make a color bar. Supress silly colorbar warnings. 

175 with warnings.catch_warnings(): 

176 warnings.simplefilter("ignore") 

177 cb = plt.colorbar(im, shrink=0.75, aspect=25, pad=0.1, orientation='horizontal', 

178 format=plotDict['cbarFormat'], extendrect=True) 

179 cb.set_label(plotDict['xlabel'], fontsize=plotDict['fontsize']) 

180 if plotDict['labelsize'] is not None: 

181 cb.ax.tick_params(labelsize=plotDict['labelsize']) 

182 if norm == 'log': 

183 tick_locator = ticker.LogLocator(numticks=plotDict['nTicks']) 

184 cb.locator = tick_locator 

185 cb.update_ticks() 

186 if (plotDict['nTicks'] is not None) & (norm != 'log'): 

187 tick_locator = ticker.MaxNLocator(nbins=plotDict['nTicks']) 

188 cb.locator = tick_locator 

189 cb.update_ticks() 

190 # If outputing to PDF, this fixes the colorbar white stripes 

191 if plotDict['cbar_edge']: 

192 cb.solids.set_edgecolor("face") 

193 return fig.number 

194 

195 

196class HealpixPowerSpectrum(BasePlotter): 

197 def __init__(self): 

198 self.plotType = 'PowerSpectrum' 

199 self.objectPlotter = False 

200 self.defaultPlotDict = {} 

201 self.defaultPlotDict.update(baseDefaultPlotDict) 

202 self.defaultPlotDict.update({'maxl': None, 'removeDipole': True, 'linestyle': '-'}) 

203 

204 def __call__(self, metricValue, slicer, userPlotDict, fignum=None): 

205 """ 

206 Generate and plot the power spectrum of metricValue (calculated on a healpix grid). 

207 """ 

208 if 'Healpix' not in slicer.slicerName: 

209 raise ValueError('HealpixPowerSpectrum for use with healpix metricBundles.') 

210 plotDict = {} 

211 plotDict.update(self.defaultPlotDict) 

212 plotDict.update(userPlotDict) 

213 

214 fig = plt.figure(fignum, figsize=plotDict['figsize']) 

215 ax = fig.add_subplot(plotDict['subplot']) 

216 # If the mask is True everywhere (no data), just plot zeros 

217 if False not in metricValue.mask: 

218 return None 

219 if plotDict['removeDipole']: 

220 cl = hp.anafast(hp.remove_dipole(metricValue.filled(slicer.badval)), lmax=plotDict['maxl']) 

221 else: 

222 cl = hp.anafast(metricValue.filled(slicer.badval), lmax=plotDict['maxl']) 

223 ell = np.arange(np.size(cl)) 

224 if plotDict['removeDipole']: 

225 condition = (ell > 1) 

226 else: 

227 condition = (ell > 0) 

228 ell = ell[condition] 

229 cl = cl[condition] 

230 # Plot the results. 

231 plt.plot(ell, (cl * ell * (ell + 1)) / 2.0 / np.pi, 

232 color=plotDict['color'], linestyle=plotDict['linestyle'], label=plotDict['label']) 

233 if cl.max() > 0 and plotDict['logScale']: 

234 plt.yscale('log') 

235 plt.xlabel(r'$l$', fontsize=plotDict['fontsize']) 

236 plt.ylabel(r'$l(l+1)C_l/(2\pi)$', fontsize=plotDict['fontsize']) 

237 if plotDict['labelsize'] is not None: 

238 plt.tick_params(axis='x', labelsize=plotDict['labelsize']) 

239 plt.tick_params(axis='y', labelsize=plotDict['labelsize']) 

240 if plotDict['title'] is not None: 

241 plt.title(plotDict['title']) 

242 # Return figure number (so we can reuse/add onto/save this figure if desired). 

243 return fig.number 

244 

245 

246class HealpixHistogram(BasePlotter): 

247 def __init__(self): 

248 self.plotType = 'Histogram' 

249 self.objectPlotter = False 

250 self.defaultPlotDict = {} 

251 self.defaultPlotDict.update(baseDefaultPlotDict) 

252 self.defaultPlotDict.update({'ylabel': 'Area (1000s of square degrees)', 

253 'bins': None, 'binsize': None, 'cumulative': False, 

254 'scale': None, 'linestyle': '-'}) 

255 self.baseHist = BaseHistogram() 

256 

257 def __call__(self, metricValue, slicer, userPlotDict, fignum=None): 

258 """ 

259 Histogram metricValue for all healpix points. 

260 """ 

261 if 'Healpix' not in slicer.slicerName: 

262 raise ValueError('HealpixHistogram is for use with healpix slicer.') 

263 plotDict = {} 

264 plotDict.update(self.defaultPlotDict) 

265 plotDict.update(userPlotDict) 

266 if plotDict['scale'] is None: 

267 plotDict['scale'] = (hp.nside2pixarea(slicer.nside, degrees=True) / 1000.0) 

268 fignum = self.baseHist(metricValue, slicer, plotDict, fignum=fignum) 

269 return fignum 

270 

271 

272class OpsimHistogram(BasePlotter): 

273 def __init__(self): 

274 self.plotType = 'Histogram' 

275 self.objectPlotter = False 

276 self.defaultPlotDict = {} 

277 self.defaultPlotDict.update(baseDefaultPlotDict) 

278 self.defaultPlotDict.update({'ylabel': 'Number of Fields', 'yaxisformat': '%d', 

279 'bins': None, 'binsize': None, 'cumulative': False, 

280 'scale': 1.0, 'linestyle': '-'}) 

281 self.baseHist = BaseHistogram() 

282 

283 def __call__(self, metricValue, slicer, userPlotDict, fignum=None): 

284 """ 

285 Histogram metricValue for all healpix points. 

286 """ 

287 if slicer.slicerName != 'OpsimFieldSlicer': 

288 raise ValueError('OpsimHistogram is for use with OpsimFieldSlicer.') 

289 plotDict = {} 

290 plotDict.update(self.defaultPlotDict) 

291 plotDict.update(userPlotDict) 

292 fignum = self.baseHist(metricValue, slicer, plotDict, fignum=fignum) 

293 return fignum 

294 

295 

296class BaseHistogram(BasePlotter): 

297 def __init__(self): 

298 self.plotType = 'Histogram' 

299 self.objectPlotter = False 

300 self.defaultPlotDict = {} 

301 self.defaultPlotDict.update(baseDefaultPlotDict) 

302 self.defaultPlotDict.update({'ylabel': 'Count', 'bins': None, 'binsize': None, 'cumulative': False, 

303 'scale': 1.0, 'yaxisformat': '%.3f', 'linestyle': '-'}) 

304 

305 def __call__(self, metricValueIn, slicer, userPlotDict, fignum=None): 

306 """ 

307 Plot a histogram of metricValues (such as would come from a spatial slicer). 

308 """ 

309 # Adjust metric values by zeropoint or normVal, and use 'compressed' version of masked array. 

310 plotDict = {} 

311 plotDict.update(self.defaultPlotDict) 

312 plotDict.update(userPlotDict) 

313 metricValue = applyZPNorm(metricValueIn, plotDict) 

314 metricValue = metricValue.compressed() 

315 # Toss any NaNs or infs 

316 metricValue = metricValue[np.isfinite(metricValue)] 

317 # Determine percentile clipped X range, if set. (and xmin/max not set). 

318 if plotDict['xMin'] is None and plotDict['xMax'] is None: 

319 if plotDict['percentileClip']: 

320 plotDict['xMin'], plotDict['xMax'] = percentileClipping(metricValue, 

321 percentile=plotDict['percentileClip']) 

322 # Set the histogram range values, to avoid cases of trying to histogram single-valued data. 

323 # First we try to use the range specified by a user, if there is one. Then use the data if not. 

324 # all of this only works if plotDict is not cumulative. 

325 histRange = [plotDict['xMin'], plotDict['xMax']] 

326 if histRange[0] is None: 

327 histRange[0] = metricValue.min() 

328 if histRange[1] is None: 

329 histRange[1] = metricValue.max() 

330 # Need to have some range of values on the histogram, or it will fail. 

331 if histRange[0] == histRange[1]: 

332 warnings.warn('Histogram range was single-valued; expanding default range.') 

333 histRange[1] = histRange[0] + 1.0 

334 # Set up the bins for the histogram. User specified 'bins' overrides 'binsize'. 

335 # Note that 'bins' could be a single number or an array, simply passed to plt.histogram. 

336 if plotDict['bins'] is not None: 

337 bins = plotDict['bins'] 

338 elif plotDict['binsize'] is not None: 

339 # If generating a cumulative histogram, want to use full range of data (but with given binsize). 

340 # .. but if user set histRange to be wider than full range of data, then 

341 # extend bins to cover this range, so we can make prettier plots. 

342 if plotDict['cumulative']: 

343 if plotDict['xMin'] is not None: 

344 # Potentially, expand the range for the cumulative histogram. 

345 bmin = np.min([metricValue.min(), plotDict['xMin']]) 

346 else: 

347 bmin = metricValue.min() 

348 if plotDict['xMax'] is not None: 

349 bmax = np.max([metricValue.max(), plotDict['xMax']]) 

350 else: 

351 bmax = metricValue.max() 

352 bins = np.arange(bmin, bmax + plotDict['binsize'] / 2.0, plotDict['binsize']) 

353 # Otherwise, not cumulative so just use metric values, without potential expansion. 

354 else: 

355 bins = np.arange(histRange[0], histRange[1] + plotDict['binsize'] / 2.0, plotDict['binsize']) 

356 # Catch edge-case where there is only 1 bin value 

357 if bins.size < 2: 

358 bins = np.arange(bins.min() - plotDict['binsize'] * 2.0, 

359 bins.max() + plotDict['binsize'] * 2.0, plotDict['binsize']) 

360 else: 

361 # If user did not specify bins or binsize, then we try to figure out a good number of bins. 

362 bins = optimalBins(metricValue) 

363 # Generate plots. 

364 fig = plt.figure(fignum, figsize=plotDict['figsize']) 

365 ax = fig.add_subplot(plotDict['subplot']) 

366 # Check if any data falls within histRange, because otherwise histogram generation will fail. 

367 if isinstance(bins, np.ndarray): 

368 condition = ((metricValue >= bins.min()) & (metricValue <= bins.max())) 

369 else: 

370 condition = ((metricValue >= histRange[0]) & (metricValue <= histRange[1])) 

371 plotValue = metricValue[condition] 

372 if len(plotValue) == 0: 

373 # No data is within histRange/bins. So let's just make a simple histogram anyway. 

374 n, b, p = plt.hist(metricValue, bins=50, histtype='step', cumulative=plotDict['cumulative'], 

375 log=plotDict['logScale'], label=plotDict['label'], 

376 color=plotDict['color']) 

377 else: 

378 # There is data to plot, and we've already ensured histRange/bins are more than single value. 

379 n, b, p = plt.hist(metricValue, bins=bins, range=histRange, 

380 histtype='step', log=plotDict['logScale'], 

381 cumulative=plotDict['cumulative'], 

382 label=plotDict['label'], color=plotDict['color']) 

383 hist_ylims = plt.ylim() 

384 if n.max() > hist_ylims[1]: 

385 plt.ylim(top = n.max()) 

386 if n.min() < hist_ylims[0] and not plotDict['logScale']: 

387 plt.ylim(bottom = n.min()) 

388 # Fill in axes labels and limits. 

389 # Option to use 'scale' to turn y axis into area or other value. 

390 

391 def mjrFormatter(y, pos): 

392 if not isinstance(plotDict['scale'], numbers.Number): 

393 raise ValueError('plotDict["scale"] must be a number to scale the y axis.') 

394 return plotDict['yaxisformat'] % (y * plotDict['scale']) 

395 

396 ax.yaxis.set_major_formatter(FuncFormatter(mjrFormatter)) 

397 # Set optional x, y limits. 

398 if 'xMin' in plotDict: 

399 plt.xlim(left=plotDict['xMin']) 

400 if 'xMax' in plotDict: 

401 plt.xlim(right=plotDict['xMax']) 

402 if 'yMin' in plotDict: 

403 plt.ylim(bottom=plotDict['yMin']) 

404 if 'yMax' in plotDict: 

405 plt.ylim(top=plotDict['yMax']) 

406 # Set/Add various labels. 

407 plt.xlabel(plotDict['xlabel'], fontsize=plotDict['fontsize']) 

408 plt.ylabel(plotDict['ylabel'], fontsize=plotDict['fontsize']) 

409 plt.title(plotDict['title']) 

410 if plotDict['labelsize'] is not None: 

411 plt.tick_params(axis='x', labelsize=plotDict['labelsize']) 

412 plt.tick_params(axis='y', labelsize=plotDict['labelsize']) 

413 # Return figure number 

414 return fig.number 

415 

416 

417class BaseSkyMap(BasePlotter): 

418 def __init__(self): 

419 self.plotType = 'SkyMap' 

420 self.objectPlotter = False # unless 'metricIsColor' is true.. 

421 self.defaultPlotDict = {} 

422 self.defaultPlotDict.update(baseDefaultPlotDict) 

423 self.defaultPlotDict.update({'projection': 'aitoff', 'radius': np.radians(1.75), 'alpha': 1.0, 

424 'plotMask': False, 'metricIsColor': False, 'cbar': True, 

425 'raCen': 0.0, 'mwZone': True, 'bgcolor': 'gray'}) 

426 

427 def _plot_tissot_ellipse(self, lon, lat, radius, ax=None, **kwargs): 

428 """Plot Tissot Ellipse/Tissot Indicatrix 

429 

430 Parameters 

431 ---------- 

432 lon : float or array_like 

433 longitude-like of ellipse centers (radians) 

434 lat : float or array_like 

435 latitude-like of ellipse centers (radians) 

436 radius : float or array_like 

437 radius of ellipses (radians) 

438 ax : Axes object (optional) 

439 matplotlib axes instance on which to draw ellipses. 

440 

441 Other Parameters 

442 ---------------- 

443 other keyword arguments will be passed to matplotlib.patches.Ellipse. 

444 

445 # The code in this method adapted from astroML, which is BSD-licensed. 

446 # See http: //github.com/astroML/astroML for details. 

447 """ 

448 # Code adapted from astroML, which is BSD-licensed. 

449 # See http: //github.com/astroML/astroML for details. 

450 ellipses = [] 

451 if ax is None: 

452 ax = plt.gca() 

453 for l, b, diam in np.broadcast(lon, lat, radius * 2.0): 

454 el = Ellipse((l, b), diam / np.cos(b), diam, **kwargs) 

455 ellipses.append(el) 

456 return ellipses 

457 

458 def _plot_ecliptic(self, raCen=0, ax=None): 

459 """ 

460 Plot a red line at location of ecliptic. 

461 """ 

462 if ax is None: 

463 ax = plt.gca() 

464 ecinc = 23.439291 * (np.pi / 180.0) 

465 ra_ec = np.arange(0, np.pi * 2., (np.pi * 2. / 360.)) 

466 dec_ec = np.sin(ra_ec) * ecinc 

467 lon = -(ra_ec - raCen - np.pi) % (np.pi * 2) - np.pi 

468 ax.plot(lon, dec_ec, 'r.', markersize=1.8, alpha=0.4) 

469 

470 def _plot_mwZone(self, raCen=0, peakWidth=np.radians(10.), taperLength=np.radians(80.), ax=None): 

471 """ 

472 Plot blue lines to mark the milky way galactic exclusion zone. 

473 """ 

474 if ax is None: 

475 ax = plt.gca() 

476 # Calculate galactic coordinates for mw location. 

477 step = 0.02 

478 galL = np.arange(-np.pi, np.pi + step / 2., step) 

479 val = peakWidth * np.cos(galL / taperLength * np.pi / 2.) 

480 galB1 = np.where(np.abs(galL) <= taperLength, val, 0) 

481 galB2 = np.where(np.abs(galL) <= taperLength, -val, 0) 

482 # Convert to ra/dec. 

483 # Convert to lon/lat and plot. 

484 ra, dec = _equatorialFromGalactic(galL, galB1) 

485 lon = -(ra - raCen - np.pi) % (np.pi * 2) - np.pi 

486 ax.plot(lon, dec, 'b.', markersize=1.8, alpha=0.4) 

487 ra, dec = _equatorialFromGalactic(galL, galB2) 

488 lon = -(ra - raCen - np.pi) % (np.pi * 2) - np.pi 

489 ax.plot(lon, dec, 'b.', markersize=1.8, alpha=0.4) 

490 

491 def __call__(self, metricValueIn, slicer, userPlotDict, fignum=None): 

492 """ 

493 Plot the sky map of metricValue for a generic spatial slicer. 

494 """ 

495 if 'ra' not in slicer.slicePoints or 'dec' not in slicer.slicePoints: 

496 errMessage = 'SpatialSlicer must contain "ra" and "dec" in slicePoints metadata.' 

497 errMessage += ' SlicePoints only contains keys %s.' % (slicer.slicePoints.keys()) 

498 raise ValueError(errMessage) 

499 plotDict = {} 

500 plotDict.update(self.defaultPlotDict) 

501 plotDict.update(userPlotDict) 

502 metricValue = applyZPNorm(metricValueIn, plotDict) 

503 

504 fig = plt.figure(fignum, figsize=plotDict['figsize']) 

505 # other projections available include 

506 # ['aitoff', 'hammer', 'lambert', 'mollweide', 'polar', 'rectilinear'] 

507 ax = fig.add_subplot(plotDict['subplot'], projection=plotDict['projection']) 

508 # Set up valid datapoints and colormin/max values. 

509 if plotDict['plotMask']: 

510 # Plot all data points. 

511 mask = np.ones(len(metricValue), dtype='bool') 

512 else: 

513 # Only plot points which are not masked. Flip numpy ma mask where 'False' == 'good'. 

514 good = ~metricValue.mask 

515 

516 # Add ellipses at RA/Dec locations - but don't add colors yet. 

517 lon = -(slicer.slicePoints['ra'][good] - plotDict['raCen'] - np.pi) % (np.pi * 2) - np.pi 

518 ellipses = self._plot_tissot_ellipse(lon, slicer.slicePoints['dec'][good], 

519 plotDict['radius'], rasterized=True, ax=ax) 

520 if plotDict['metricIsColor']: 

521 current = None 

522 for ellipse, mVal in zip(ellipses, metricValue.data[good]): 

523 if mVal[3] > 1: 

524 ellipse.set_alpha(1.0) 

525 ellipse.set_facecolor((mVal[0], mVal[1], mVal[2])) 

526 ellipse.set_edgecolor('k') 

527 current = ellipse 

528 else: 

529 ellipse.set_alpha(mVal[3]) 

530 ellipse.set_color((mVal[0], mVal[1], mVal[2])) 

531 ax.add_patch(ellipse) 

532 if current: 

533 ax.add_patch(current) 

534 else: 

535 # Determine color min/max values. metricValue.compressed = non-masked points. 

536 clims = setColorLims(metricValue, plotDict) 

537 # Determine whether or not to use auto-log scale. 

538 if plotDict['logScale'] == 'auto': 

539 if clims[0] > 0: 

540 if np.log10(clims[1]) - np.log10(clims[0]) > 3: 

541 plotDict['logScale'] = True 

542 else: 

543 plotDict['logScale'] = False 

544 else: 

545 plotDict['logScale'] = False 

546 if plotDict['logScale']: 

547 # Move min/max values to things that can be marked on the colorbar. 

548 #clims[0] = 10 ** (int(np.log10(clims[0]))) 

549 #clims[1] = 10 ** (int(np.log10(clims[1]))) 

550 norml = colors.LogNorm() 

551 p = PatchCollection(ellipses, cmap=plotDict['cmap'], alpha=plotDict['alpha'], 

552 linewidth=0, edgecolor=None, norm=norml, rasterized=True) 

553 else: 

554 p = PatchCollection(ellipses, cmap=plotDict['cmap'], alpha=plotDict['alpha'], 

555 linewidth=0, edgecolor=None, rasterized=True) 

556 p.set_array(metricValue.data[good]) 

557 p.set_clim(clims) 

558 ax.add_collection(p) 

559 # Add color bar (with optional setting of limits) 

560 if plotDict['cbar']: 

561 cb = plt.colorbar(p, aspect=25, extendrect=True, orientation='horizontal', 

562 format=plotDict['cbarFormat']) 

563 # If outputing to PDF, this fixes the colorbar white stripes 

564 if plotDict['cbar_edge']: 

565 cb.solids.set_edgecolor("face") 

566 cb.set_label(plotDict['xlabel'], fontsize=plotDict['fontsize']) 

567 cb.ax.tick_params(labelsize=plotDict['labelsize']) 

568 tick_locator = ticker.MaxNLocator(nbins=plotDict['nTicks']) 

569 cb.locator = tick_locator 

570 cb.update_ticks() 

571 # Add ecliptic 

572 self._plot_ecliptic(plotDict['raCen'], ax=ax) 

573 if plotDict['mwZone']: 

574 self._plot_mwZone(plotDict['raCen'], ax=ax) 

575 ax.grid(True, zorder=1) 

576 ax.xaxis.set_ticklabels([]) 

577 if plotDict['bgcolor'] is not None: 

578 ax.set_facecolor(plotDict['bgcolor']) 

579 # Add label. 

580 if plotDict['label'] is not None: 

581 plt.figtext(0.75, 0.9, '%s' % plotDict['label'], fontsize=plotDict['fontsize']) 

582 if plotDict['title'] is not None: 

583 plt.text(0.5, 1.09, plotDict['title'], horizontalalignment='center', 

584 transform=ax.transAxes, fontsize=plotDict['fontsize']) 

585 return fig.number 

586 

587 

588class HealpixSDSSSkyMap(BasePlotter): 

589 def __init__(self): 

590 self.plotType = 'SkyMap' 

591 self.objectPlotter = False 

592 self.defaultPlotDict = {} 

593 self.defaultPlotDict.update(baseDefaultPlotDict) 

594 self.defaultPlotDict.update({'cbarFormat': '%.2f', 

595 'raMin': -90, 'raMax': 90, 'raLen': 45, 

596 'decMin': -2., 'decMax': 2.}) 

597 

598 def __call__(self, metricValueIn, slicer, userPlotDict, fignum=None): 

599 

600 """ 

601 Plot the sky map of metricValue using healpy cartview plots in thin strips. 

602 raMin: Minimum RA to plot (deg) 

603 raMax: Max RA to plot (deg). Note raMin/raMax define the centers that will be plotted. 

604 raLen: Length of the plotted strips in degrees 

605 decMin: minimum dec value to plot 

606 decMax: max dec value to plot 

607 metricValueIn: metric values 

608 """ 

609 plotDict = {} 

610 plotDict.update(self.defaultPlotDict) 

611 plotDict.update(userPlotDict) 

612 metricValue = applyZPNorm(metricValueIn, plotDict) 

613 norm = None 

614 if plotDict['logScale']: 

615 norm = 'log' 

616 clims = setColorLims(metricValue, plotDict) 

617 cmap = setColorMap(plotDict) 

618 racenters = np.arange(plotDict['raMin'], plotDict['raMax'], plotDict['raLen']) 

619 nframes = racenters.size 

620 fig = plt.figure(fignum) 

621 # Do not specify or use plotDict['subplot'] because this is done in each call to hp.cartview. 

622 for i, racenter in enumerate(racenters): 

623 if i == 0: 

624 useTitle = plotDict['title'] + ' /n' + '%i < RA < %i' % (racenter - plotDict['raLen'], 

625 racenter + plotDict['raLen']) 

626 else: 

627 useTitle = '%i < RA < %i' % (racenter - plotDict['raLen'], racenter + plotDict['raLen']) 

628 hp.cartview(metricValue.filled(slicer.badval), title=useTitle, cbar=False, 

629 min=clims[0], max=clims[1], flip='astro', rot=(racenter, 0, 0), 

630 cmap=cmap, norm=norm, lonra=[-plotDict['raLen'], plotDict['raLen']], 

631 latra=[plotDict['decMin'], plotDict['decMax']], sub=(nframes + 1, 1, i + 1), fig=fig) 

632 hp.graticule(dpar=20, dmer=20, verbose=False) 

633 # Add colorbar (not using healpy default colorbar because want more tickmarks). 

634 ax = fig.add_axes([0.1, .15, .8, .075]) # left, bottom, width, height 

635 # Add label. 

636 if plotDict['label'] is not None: 

637 plt.figtext(0.8, 0.9, '%s' % plotDict['label']) 

638 # Make the colorbar as a seperate figure, 

639 # from http: //matplotlib.org/examples/api/colorbar_only.html 

640 cnorm = colors.Normalize(vmin=clims[0], vmax=clims[1]) 

641 # supress silly colorbar warnings 

642 with warnings.catch_warnings(): 

643 warnings.simplefilter("ignore") 

644 cb = mpl.colorbar.ColorbarBase(ax, cmap=cmap, norm=cnorm, 

645 orientation='horizontal', format=plotDict['cbarFormat']) 

646 cb.set_label(plotDict['xlabel']) 

647 cb.ax.tick_params(labelsize=plotDict['labelsize']) 

648 if norm == 'log': 

649 tick_locator = ticker.LogLocator(numticks=plotDict['nTicks']) 

650 cb.locator = tick_locator 

651 cb.update_ticks() 

652 if (plotDict['nTicks'] is not None) & (norm != 'log'): 

653 tick_locator = ticker.MaxNLocator(nbins=plotDict['nTicks']) 

654 cb.locator = tick_locator 

655 cb.update_ticks() 

656 # If outputing to PDF, this fixes the colorbar white stripes 

657 if plotDict['cbar_edge']: 

658 cb.solids.set_edgecolor("face") 

659 fig = plt.gcf() 

660 return fig.number 

661 

662 

663def project_lambert(longitude, latitude): 

664 """Project from RA,dec to plane 

665 https://en.wikipedia.org/wiki/Lambert_azimuthal_equal-area_projection 

666 """ 

667 

668 # flipping the sign on latitude goes north pole or south pole centered 

669 r_polar = 2*np.cos((np.pi/2+latitude)/2.) 

670 # Add pi/2 so north is up 

671 theta_polar = longitude + np.pi/2 

672 

673 x = r_polar * np.cos(theta_polar) 

674 y = r_polar * np.sin(theta_polar) 

675 return x, y 

676 

677 

678def draw_grat(ax): 

679 """Draw some graticule lines on an axis 

680 """ 

681 decs = np.radians(90.-np.array([20, 40, 60, 80])) 

682 ra = np.radians(np.arange(0, 361, 1)) 

683 for dec in decs: 

684 temp_dec = ra*0+dec 

685 x, y = project_lambert(ra, temp_dec) 

686 ax.plot(x, y, 'k--', alpha=0.5) 

687 

688 ras = np.radians(np.arange(0, 360+45, 45)) 

689 dec = np.radians(90.-np.arange(0, 81, 1)) 

690 for ra in ras: 

691 temp_ra = dec*0 + ra 

692 x, y = project_lambert(temp_ra, dec) 

693 ax.plot(x, y, 'k--', alpha=0.5) 

694 

695 return ax 

696 

697class LambertSkyMap(BasePlotter): 

698 """ 

699 Use basemap and contour to make a Lambertian projection. 

700 Note that the plotDict can include a 'basemap' key with a dictionary of 

701 arbitrary kwargs to use with the call to Basemap. 

702 """ 

703 

704 def __init__(self): 

705 self.plotType = 'SkyMap' 

706 self.objectPlotter = False 

707 self.defaultPlotDict = {} 

708 self.defaultPlotDict.update(baseDefaultPlotDict) 

709 self.defaultPlotDict.update({'basemap': {'projection': 'nplaea', 'boundinglat': 1, 'lon_0': 180, 

710 'resolution': None, 'celestial': False, 'round': False}, 

711 'levels': 200, 'cbarFormat': '%i', 'norm': None}) 

712 

713 def __call__(self, metricValueIn, slicer, userPlotDict, fignum=None): 

714 

715 if 'ra' not in slicer.slicePoints or 'dec' not in slicer.slicePoints: 

716 errMessage = 'SpatialSlicer must contain "ra" and "dec" in slicePoints metadata.' 

717 errMessage += ' SlicePoints only contains keys %s.' % (slicer.slicePoints.keys()) 

718 raise ValueError(errMessage) 

719 

720 plotDict = {} 

721 plotDict.update(self.defaultPlotDict) 

722 plotDict.update(userPlotDict) 

723 

724 metricValue = applyZPNorm(metricValueIn, plotDict) 

725 clims = setColorLims(metricValue, plotDict) 

726 # Calculate the levels to use for the contour 

727 if np.size(plotDict['levels']) > 1: 

728 levels = plotDict['levels'] 

729 else: 

730 step = (clims[1] - clims[0]) / plotDict['levels'] 

731 levels = np.arange(clims[0], clims[1] + step, step) 

732 

733 fig = plt.figure(fignum, figsize=plotDict['figsize']) 

734 ax = fig.add_subplot(plotDict['subplot']) 

735 

736 x, y = project_lambert(slicer.slicePoints['ra'], slicer.slicePoints['dec']) 

737 # Contour the plot first to remove any anti-aliasing artifacts. Doesn't seem to work though. See: 

738 # http: //stackoverflow.com/questions/15822159/aliasing-when-saving-matplotlib\ 

739 # -filled-contour-plot-to-pdf-or-eps 

740 # tmpContour = m.contour(np.degrees(slicer.slicePoints['ra']), 

741 # np.degrees(slicer.slicePoints['dec']), 

742 # metricValue.filled(np.min(clims)-1), levels, tri=True, 

743 # cmap=plotDict['cmap'], ax=ax, latlon=True, 

744 # lw=1) 

745 

746 # Set masked values to be below the lowest contour level. 

747 if plotDict['norm'] == 'log': 

748 z_val = metricValue.filled(np.min(clims)-0.9) 

749 norm = colors.LogNorm(vmin=z_val.min(), vmax=z_val.max()) 

750 else: 

751 norm = plotDict['norm'] 

752 tcf = ax.tricontourf(x, y, metricValue.filled(np.min(clims)-0.9), levels, 

753 cmap=plotDict['cmap'], norm=norm) 

754 

755 ax = draw_grat(ax) 

756 

757 ax.set_xticks([]) 

758 ax.set_yticks([]) 

759 alt_limit = 10. 

760 x, y = project_lambert(0, np.radians(alt_limit)) 

761 max_val = np.max(np.abs([x, y])) 

762 ax.set_xlim([-max_val, max_val]) 

763 ax.set_ylim([-max_val, max_val]) 

764 

765 # Try to fix the ugly pdf contour problem 

766 for c in tcf.collections: 

767 c.set_edgecolor("face") 

768 

769 cb = plt.colorbar(tcf, format=plotDict['cbarFormat']) 

770 cb.set_label(plotDict['xlabel']) 

771 if plotDict['labelsize'] is not None: 

772 cb.ax.tick_params(labelsize=plotDict['labelsize']) 

773 # Pop in an extra line to raise the title a bit 

774 ax.set_title(plotDict['title']+'\n ') 

775 # If outputing to PDF, this fixes the colorbar white stripes 

776 if plotDict['cbar_edge']: 

777 cb.solids.set_edgecolor("face") 

778 return fig.number