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

1# (c) Stefan Countryman 2016-2018 

2 

3""" 

4Define class containing a HEALPix skymap and convenience methods for 

5manipulating, creating, and reformating skymaps. 

6""" 

7 

8from abc import ABC, abstractproperty, abstractmethod 

9from copy import deepcopy 

10from deprecated import deprecated 

11from llama.utils import memoize 

12from llama.filehandler import TriggerList 

13from llama.files.healpix.utils import ( 

14 dangle_rad, 

15 sky_area, 

16) 

17 

18 

19@deprecated 

20def psf_gaussian(delta_angle, sigma, degrees=True): 

21 """Return a 2-D Gaussian PDF function to be used as a PSF (point spread 

22 function) on the sphere. Note that this Gaussian PDF *does not* take 

23 account of curvature and is accurate only for small angular spreads. 

24 

25 Parameters 

26 ---------- 

27 delta_angle : array_like 

28 A scalar or array of angular distances from the center of the Gaussian. 

29 Must be in same units (degrees or radians) as sigma. 

30 sigma : float 

31 The standard deviation of the Gaussian. Must be in same units (degrees 

32 or radians) as delta_angle. 

33 degrees : bool, optional 

34 Whether angles are given in degrees. If False, units are assumed to be 

35 in radians. 

36 

37 Returns 

38 ------- 

39 densities : array_like 

40 The values of the Gaussian at the distances given by delta_angle in 

41 units of [1/steradian] (even if inputs are given in degrees!). 

42 

43 >>> import numpy as np 

44 >>> psf_gaussian(1, 1, degrees=False) == 1/(2*np.pi) * np.exp(-1/2) 

45 True 

46 """ 

47 import numpy as np 

48 two_sigma2 = 2*sigma**2 

49 densities = np.exp(-delta_angle**2/two_sigma2) / (np.pi*two_sigma2) 

50 if degrees: 

51 from astropy.units import Quantity 

52 return Quantity(densities, "1/deg^2").to("1/steradian").value 

53 return densities 

54 

55 

56@deprecated 

57def psf_gaussian_scales(sigma): 

58 """Get the characteristic maximum and minimum scales of a Gaussian PSF for 

59 a given angle sigma. 

60 

61 Parameters 

62 ---------- 

63 sigma : int or float 

64 The angular standard deviation of the Gaussian PSF in degrees or 

65 radians. 

66 

67 Returns 

68 ------- 

69 min_scale : float 

70 The scale of inter-pixel distances required to capture the features of 

71 a Gaussian with the given standard deviation. In same units as 

72 ``sigma``. 

73 max_scale : float 

74 The diameter of a pixel mesh centered on the Gaussian mean that is 

75 large enough to capture almost all of the probability in the 

76 distribution. In same units as ``sigma``. 

77 """ 

78 return (sigma / 10., sigma * 10) 

79 

80 

81class SkyMap(ABC): 

82 """An array representing a skymap.""" 

83 

84 # pylint: disable=invalid-name 

85 def __init__(self, pixels, ra, dec, degrees=True, area_per_pixel=None): 

86 """Take an input array or list of pixels. ``pixels``, ``ra``, and 

87 ``dec`` must have the same dimensions. 

88 

89 Parameters 

90 ---------- 

91 pixels : array-like 

92 Pixel values. 

93 ra : array-like 

94 Right Ascensions of the pixels. 

95 dec : array-like 

96 Declinations of the pixels. 

97 degrees : bool, optional 

98 Whether input angular positions are given in degrees. If ``False``, 

99 assume radians. 

100 area_per_pixel : astropy.Quantity, optional 

101 Area per pixel. Can be a scalar value (if all pixels are the same) 

102 or an array of pixel areas corresponding to the pixels given in 

103 ``pixels``. If none is provided (DEFAULT), just divide the total 

104 area of the sky by the number of pixels. 

105 

106 CURRENTLY ASSUMES EQUATORIAL COORDINATE SYSTEM. 

107 """ 

108 import numpy as np 

109 if not (len(pixels) == len(ra) and len(pixels) == len(dec)): 

110 raise ValueError("ra, dec, and pixels must have same length.") 

111 self.pixels = np.array(pixels) 

112 if degrees: 

113 self.ra = np.array(ra) 

114 self.dec = np.array(dec) 

115 else: 

116 # these pylint disables are for spurious pylint errors from numpy; 

117 # E1111=assignment-from-no-return 

118 self.ra = np.degrees(ra) # pylint: disable=E1111 

119 # pylint: disable=assignment-from-no-return 

120 self.dec = np.degrees(dec) 

121 if area_per_pixel is not None: 

122 self.area_per_pixel = area_per_pixel 

123 

124 @classmethod 

125 def _from_dict(cls, dictionary): 

126 """Create a new instance from a dictionary of the sort returned by 

127 ``to_dict``.""" 

128 pixels = dictionary.pop('pixels') 

129 return cls(pixels, **dictionary) 

130 

131 def _to_dict(self): 

132 """Return a dictionary whose keys are the attributes of this instance. 

133 Can be used to initialize a new identical skymap instance.""" 

134 return {'pixels': self.pixels, 'ra': self.ra, 'dec': self.dec, 

135 'area_per_pixel': self.area_per_pixel, 'degrees': True} 

136 

137 @property 

138 def npix(self): 

139 return len(self.pixels) 

140 

141 @abstractmethod 

142 def dangle(self, ra, dec, degrees=True): 

143 """Get a new skymap with the same pixelization where each pixel value 

144 is the distance from that pixel to the point with right ascension 

145 ``ra`` and declination ``dec``. By default, ``ra`` and ``dec`` are in 

146 degrees, though this can be overridden by setting ``degrees=False``, 

147 in which case they will be interpretted as radians. The measured angle 

148 differences in the returned skymap will always be in degrees. Dot 

149 product formula per-pixel is: 

150 

151 sin(DEC1)*sin(DEC2) + cos(DEC1)*cos(DEC2)*cos(RA2-RA1) 

152 """ 

153 raise NotImplementedError() 

154 # TODO Implement 

155 

156 def map(self, function): 

157 """Map a function to the pixel locations of this skymap and return an 

158 identically sized and ordered skymap with the output values of that 

159 function as its pixel values. ``function`` must take vector arguments 

160 ``ra`` and ``dec`` and return a list of pixel values of the same 

161 length. No normalization takes place.""" 

162 result = self._to_dict() 

163 result['pixels'] = function(self.ra, self.dec) 

164 if len(result['pixels']) != self.npix: 

165 raise ValueError('mapped function produced non-matching output ' 

166 'length.') 

167 return self._from_dict(result) 

168 

169 @property 

170 def ra(self): 

171 """Right Ascensions in degrees.""" 

172 return self._ra 

173 

174 @ra.setter 

175 def ra(self, ra): 

176 """Set the Right Ascensions in degrees.""" 

177 self._ra = ra 

178 

179 @property 

180 def dec(self): 

181 """Declinations in degrees.""" 

182 return self._dec 

183 

184 @dec.setter 

185 def dec(self, dec): 

186 """Set the Declinations in degrees.""" 

187 self._dec = dec 

188 

189 @property 

190 def area_per_pixel(self): 

191 """Get the solid angle per pixel. Can be set to a 

192 scalar value or a vector with the same length as the list of pixels; in 

193 either case, the pixel areas are returned as a vector for each pixel. 

194 If not set, assumes that all pixel areas are identical and that the 

195 pixel areas sum to a full sphere.""" 

196 try: 

197 return self._area_per_pixel 

198 except AttributeError: 

199 self.area_per_pixel = sky_area() / self.npix 

200 return self.area_per_pixel 

201 

202 @area_per_pixel.setter 

203 def area_per_pixel(self, area_per_pixel): 

204 import numpy as np 

205 from astropy.units import Quantity 

206 if not (isinstance(area_per_pixel, Quantity) and 

207 area_per_pixel.unit.physical_type == 'solid angle'): 

208 raise ValueError("area_per_pixel must be an astropy.units.Quantity" 

209 "of physical_type solid angle.") 

210 # assign all pixels same value if given a scalar quantity 

211 if area_per_pixel.shape == (): 

212 self._area_per_pixel = np.ones_like(self.pixels)*area_per_pixel 

213 elif area_per_pixel.shape == (self.npix,): 

214 self._area_per_pixel = area_per_pixel 

215 else: 

216 raise ValueError("area_per_pixel must be scalar or a vector of the" 

217 " same length as pixels.") 

218 

219 @memoize(method=True) 

220 def integrate(self): 

221 """If ``pixels`` is given in units of inverse solid angle, return the 

222 integral of the pixel values over the whole sky. If ``pixels`` is 

223 dimensionless, assume the pixel values are probability per-pixel and 

224 simply return the sum of pixel values. Either way, the integral is 

225 dimensionless and equals the total probability of the SkyMap (given the 

226 stated assumptions).""" 

227 from astropy.units import Unit 

228 if self.unit == Unit(""): 

229 return sum(self.pixels) 

230 if (self.unit**-1).physical_type == 'solid angle': 

231 return sum(self.pixels*self.area_per_pixel) 

232 raise ValueError("Cannot integrate ``pixels`` with units of: " 

233 "``{}``".format(self.unit)) 

234 

235 @property 

236 def unit(self): 

237 """Get the units of the pixels of this skymap. If ``pixels`` is already 

238 an ``astropy.Quantity``, simply return its ``unit``. Otherwise, return 

239 ``astropy.units.Unit("")`` (i.e. dimensionless).""" 

240 try: 

241 return self.pixels.units 

242 except AttributeError: 

243 from astropy.units import Unit 

244 return Unit("") 

245 

246 @memoize(method=True) 

247 def normalize(self, norm_to=1): 

248 """Scale the pixel values of this skymap so that they integrate to the 

249 value specified by ``norm_to`` (DEFAULT: 1). Returns a new skymap with 

250 the same units as ``self.pixels`` (assuming ``self.integrate`` is 

251 defined for ``self.units`` used in ``self.pixels``.""" 

252 normalized = deepcopy(self) 

253 normalized.pixels = (normalized.pixels/self.integrate())*norm_to 

254 return normalized 

255 

256 @property 

257 @memoize(method=True) 

258 def as_pdf(self): 

259 """Return a skymap whose pixels represent proability/pixel 

260 (rather than probability per solid angle, i.e. a proper PDF) and 

261 putting them into units of probability per solid angle. Returns a new 

262 skymap with units of inverse solid angle. If this skymap is already a 

263 PDF in units of inverse solid angle, this operation returns the 

264 original skymap.""" 

265 from astropy.units import Unit 

266 if self.unit == Unit(""): 

267 pdf = deepcopy(self) 

268 pdf.pixels = pdf.pixels / pdf.area_per_pixel 

269 return pdf 

270 if (self.unit**-1).physical_type == 'solid angle': 

271 return self 

272 raise ValueError("Cannot convert ``pixels`` with units of: " 

273 "``{}`` to a PDF.".format(self.unit)) 

274 

275 @memoize(method=True) 

276 def confidence_region_size(self, percent=90): 

277 """Calculate the size of the confidence region in square degrees. By 

278 default, finds the 90% confidence region, though the percentage can be 

279 manually specified. Does NOT normalize pixels before summing them (in 

280 case the total probability does not add to 1, e.g. if we are already 

281 dealing with a partial skymap).""" 

282 import numpy as np 

283 from astropy.units import Unit 

284 pdf = self.as_pdf 

285 sorted_inds = pdf.pixels.argsort()[::-1] 

286 cumsum = (pdf.area_per_pixel*pdf.pixels)[sorted_inds].cumsum() 

287 assert cumsum.unit == Unit("") 

288 final_ind = np.nonzero(cumsum.value <= (percent/100.))[0][-1] 

289 return sum(pdf.area_per_pixel[sorted_inds][:final_ind+1]) 

290 

291 

292class HEALPixRepresentableFileHandler(TriggerList): 

293 """ 

294 A ``FileHandler`` class containing triggers that can be represented in 

295 HEALPix. Provides ``get_healpix`` and ``get_healpix_descriptions`` methods 

296 for this purpose. Implementations need not store data in a HEALPix format 

297 natively, as long as they provide a ``get_healpix`` method for returning 

298 valid ``HEALPixSkyMap`` instances. 

299 """ 

300 

301 @abstractmethod 

302 def get_healpix(self, nest=None, template_skymap=None): 

303 """Return a list of HEALPix skymaps representing the triggers in this 

304 ``FileHandler``.""" 

305 

306 def get_healpix_descriptions(self, skymap_indices=None): 

307 """Get a list of descriptive strings for each HEALPix skymap contained 

308 in this file. 

309 

310 Parameters 

311 ---------- 

312 skymap_indices : list 

313 A list of tuples describing the index of each skymap used to 

314 produce this output skymap, with each entry in the tuple 

315 corresponding to a detector. In this case, the descriptive strings 

316 will be generated based on this ``FileHandler`` instance's 

317 ``detector`` property and the indices of the skymap_indices in 

318 ``skymap_indices``. The ordering of the returned descriptive 

319 strings will match the ordering of skymap_indices. Note that for 

320 single-detector skymaps, the elements of the ``skymap_indices`` 

321 list should just be single-element tuples corresponding to the 

322 index of each skymap. 

323 

324 Returns 

325 ------- 

326 descriptions : list 

327 Descriptions of each HEALPix skymap in the order in which they 

328 appear in the file. 

329 """ 

330 if skymap_indices is None: 

331 raise ValueError('Must provide skymap indices.') 

332 descriptions = list() 

333 for ituple in skymap_indices: 

334 fmt = ('{}_' + '-{}_'.join(['{:03d}'.format(s) for s in ituple])) 

335 descriptions.append(fmt.format(*self.DETECTORS)) 

336 return descriptions 

337 

338 

339class HEALPixPSF(HEALPixRepresentableFileHandler): 

340 """ 

341 A class with utility functions for generating skymaps in-memory that 

342 follow the HEALPix parameters (NSIDE, ORDERING) of the file described by a 

343 template ``HEALPixSkyMapFileHandler``. Used to dynamically generate skymaps 

344 that are compatible with the template skymap based on some PSF (Gaussian by 

345 default). Implementations of this class should contain point-like events 

346 with some sort of point-spread function that can be used to generate 

347 skymaps. 

348 """ 

349 

350 @abstractproperty 

351 def template_skymap_filehandler(self): 

352 """The ``HEALPixSkyMapFileHandler`` whose HEALPix parameters should be 

353 used as a template for the ``HEALPixSkyMap`` output by this 

354 ``FileHandler``. It is assumed that this ``HEALPixSkyMapFileHandler`` 

355 only returns one skymap in its list; consequently, only the first 

356 skymap loaded by this file handler will be used. Note that the 

357 template skymap file handler is likely not a dependency of this file 

358 handler; it is only used as a default for formatting generated skymaps 

359 from this file handler's data.""" 

360 

361 @abstractmethod 

362 def source_locations(self): 

363 """Returns a list of tuples of ``(RA, Dec, sigma)`` for all point-like 

364 sources, where ``(RA, Dec)`` is the central sky position and ``sigma`` 

365 is the standard deviation of the Gaussian PSF. Since this depends on 

366 the structure of data in the relevant file handler, it must be 

367 specified for each subclass.""" 

368 

369 def get_healpix(self, nest=None, template_skymap=None): 

370 """Generate a list of ``HEALPixSkyMap`` instances from the events 

371 contained in this FileHandler. 

372 

373 Parameters 

374 ---------- 

375 nest : bool, optional 

376 If ``True``, return skymaps with NESTED ordering. If False, return 

377 skymaps with RING ordering. Overrides the ordering (but not NSIDE) 

378 defined in ``template_skymap_filehandler``. 

379 template_skymap : HEALPixSkyMap, optional 

380 Provide a ``HEALPixSkyMap`` as a template to override the ones 

381 provided by ``template_skymap_filehandler``. Also overrides 

382 ``nest`` (if provided). 

383 

384 Returns 

385 ------- 

386 skymaps : list 

387 A list of ``HEALPixSkyMap`` instances of the triggers in this 

388 ``FileHandler`` that conform to the ``HEALPixSkyMap`` parameters in 

389 this class's ``template_skymap_filehandler`` 

390 """ 

391 if template_skymap is None: 

392 templatefh = self.template_skymap_filehandler(self) 

393 template_skymap = templatefh.get_healpix()[0] 

394 if nest is not None: 

395 if nest: 

396 template_skymap = template_skymap.to_nest() 

397 else: 

398 template_skymap = template_skymap.to_ring() 

399 return [template_skymap.psf_gaussian(*p) 

400 for p in self.source_locations()] 

401 

402 

403class MollviewMixin(ABC): 

404 """Defines an interface for plotting Mollweide projections.""" 

405 

406 @abstractmethod 

407 def mollview(self, **kwargs): 

408 """Return a Mollweide projection of this skymap as a ``matplotlib`` 

409 figure.""" 

410 

411 

412class HEALPixSkyMap(SkyMap, MollviewMixin): 

413 """ 

414 A HEALPix skymap object residing in memory, including methods for 

415 working with the skymap and combining skymaps with each other. 

416 """ 

417 

418 def __init__(self, pixels, nest=True, title=None): 

419 """Take an input array of pixel values and the HEALPix ORDER parameter, 

420 "NEST" if ``nest=True`` (DEFAULT), "RING" if ``nest=False``. 

421 

422 Optionally provide a ``title`` for plots made from this skymap 

423 describing what it shows. 

424 

425 CURRENTLY ASSUMES EQUATORIAL COORDINATE SYSTEM. 

426 """ 

427 import numpy as np 

428 self.nest = nest 

429 self.pixels = np.array(pixels) 

430 self.title = title 

431 

432 # pylint: disable=C0103 

433 @classmethod 

434 @memoize(method=True) 

435 def nside2ang(cls, nside, nest=True, degrees=True): 

436 """Return (RA, Dec) in degrees for the pixels corresponding to a 

437 HEALPix skymap with ``NSIDE=nside`` where ``nest=True`` (DEFAULT) 

438 indicates NEST ordering and ``nest=False`` indicates RING ordering. If 

439 ``degrees=True`` (DEFAULT), the return value will be in degrees; 

440 otherwise, radians. 

441 

442 Examples 

443 -------- 

444 >>> import numpy as np 

445 >>> ra, dec = HEALPixSkyMap.nside2ang(1, nest=True, degrees=True) 

446 >>> all(1e-13 > ra - np.array([ 

447 ... 45., 135., 225., 315., 0., 90., 180., 270., 45., 135., 

448 ... 225., 315. 

449 ... ])) 

450 True 

451 """ 

452 import numpy as np 

453 if not degrees: 

454 ra, dec = cls.nside2ang(nside, nest=nest, degrees=True) 

455 return (np.radians(ra), np.radians(dec)) 

456 import healpy as hp 

457 return hp.pix2ang(nside, range(hp.pixelfunc.nside2npix(nside)), 

458 nest=nest, lonlat=True) 

459 

460 def ang2pix(self, ra, dec, degrees=True): 

461 """Return the pixel index on this skymap at right ascensions ``ra`` and 

462 declinations ``dec``.""" 

463 import numpy as np 

464 import healpy as hp 

465 if not degrees: 

466 ra = dec * (180./np.pi) 

467 dec = dec * (180./np.pi) 

468 return hp.ang2pix(self.nside, ra, dec, nest=self.nest, lonlat=True) 

469 

470 def _to_dict(self): 

471 """Return a dictionary whose keys are the attributes of this instance. 

472 Can be used to initialize a new identical skymap instance.""" 

473 return dict(nest=self.nest, pixels=self.pixels) 

474 

475 @classmethod 

476 def _read_from_hdf5_group(cls, group): 

477 """Read a HEALPix skymap from the provided HDF5 group and return a new 

478 instance containing the data in that HDF5 group. 

479 

480 >>> import tempfile, os, h5py 

481 >>> f = tempfile.NamedTemporaryFile(suffix='.hdf5', delete=False) 

482 >>> fname = f.name 

483 >>> f.close() 

484 >>> f = h5py.File(fname, 'w') 

485 >>> g = f.create_group('foo') 

486 >>> skymap = HEALPixSkyMap(range(12)) 

487 >>> skymap == HEALPixSkyMap._read_from_hdf5_group( 

488 ... skymap._write_to_hdf5_group(g) 

489 ... ) 

490 True 

491 >>> os.remove(fname) 

492 """ 

493 skymap_dict = dict() 

494 for key in group: 

495 skymap_dict[key] = group[key][()] 

496 return cls._from_dict(skymap_dict) 

497 

498 def _write_to_hdf5_group(self, group, compressed=True): 

499 """Write this HEALPix skymap to the provided HDF5 group. Optionally, 

500 require compression by setting ``compressed`` to ``True`` (DEFAULT: 

501 ``False``).""" 

502 skymap_dict = self._to_dict() 

503 for key, val in skymap_dict.items(): 

504 if compressed and key in ['pixels']: 

505 group.create_dataset(key, data=val, compression='gzip') 

506 else: 

507 group.create_dataset(key, data=val) 

508 return group 

509 

510 @property 

511 def ra(self): # pylint: disable=C0103 

512 """Right Ascensions in degrees.""" 

513 return self.nside2ang(self.nside, nest=self.nest)[0] 

514 

515 @property 

516 def dec(self): 

517 """Declinations in degrees.""" 

518 return self.nside2ang(self.nside, nest=self.nest)[1] 

519 

520 @property 

521 def nside(self): 

522 """The NSIDE HEALPix parameter indicating how many times each base 

523 pixel has been subdivided in each of the 2 dimensions on the sphere.""" 

524 import healpy.pixelfunc as pxlfnc 

525 return pxlfnc.npix2nside(self.npix) 

526 

527 def to_ring(self): 

528 """Return a skymap with the same pixel values and RING ordering.""" 

529 if not self.nest: 

530 return self 

531 import healpy.pixelfunc as pxlfnc 

532 return type(self)( 

533 self.pixels[pxlfnc.nest2ring(self.nside, range(self.npix))], 

534 nest=False 

535 ) 

536 

537 def to_nest(self): 

538 """Return a skymap with the same pixel values and NESTED ordering.""" 

539 if self.nest: 

540 return self 

541 import healpy.pixelfunc as pxlfnc 

542 return type(self)( 

543 self.pixels[pxlfnc.ring2nest(self.nside, range(self.npix))], 

544 nest=True 

545 ) 

546 

547 def conform_skymaps(self, *args): 

548 """Make sure all skymaps contained in ``*args`` have the same ordering 

549 (NESTED or RING) as the current skymap (``self``), reordering the 

550 pixels if necessary, and returning a tuple of the (possibly reordered) 

551 input skymaps.""" 

552 # make sure all sizes are the same 

553 if len(set([skymap.npix for skymap in args])) != 1: 

554 raise ValueError('Cannot conform skymaps of different sizes.') 

555 if self.nest: 

556 return tuple([skymap.to_nest() for skymap in args]) 

557 return tuple([skymap.to_ring() for skymap in args]) 

558 

559 def dangle(self, ra, dec, degrees=True): # pylint: disable=C0103 

560 """Get a new skymap with the same pixelization where each pixel value 

561 is the distance from that pixel to the point with right ascension 

562 ``ra`` and declination ``dec``. By default, ``ra`` and ``dec`` are in 

563 degrees, though this can be overridden by setting ``degrees=False``, 

564 in which case they will be interpretted as radians. The measured angle 

565 differences in the returned skymap will always be in degrees. Dot 

566 product formula per-pixel is: 

567 

568 sin(DEC1)*sin(DEC2) + cos(DEC1)*cos(DEC2)*cos(RA2-RA1) 

569 

570 So we get the angle by taking the arccos of this function. 

571 

572 Examples 

573 -------- 

574 We should find that the distance from any pixel to the North pole is 

575 equal to 90 minus the declination (within some small error): 

576 

577 >>> skymap = HEALPixSkyMap([1]*12) 

578 >>> skymap.dangle(32, 90).pixels + skymap.dec - 90 < 1e-12 

579 array([ True, True, True, True, True, True, True, True, True, 

580 True, True, True]) 

581 

582 Likewise, the distance from any pixel to the South pole should be 

583 equal to 90 plus the declination: 

584 

585 >>> skymap.dangle(359, -90).pixels - skymap.dec - 90. < 1e-12 

586 array([ True, True, True, True, True, True, True, True, True, 

587 True, True, True]) 

588 """ 

589 import numpy as np 

590 if degrees: 

591 ra = np.radians(ra) # pylint: disable=assignment-from-no-return 

592 dec = np.radians(dec) # pylint: disable=assignment-from-no-return 

593 mapra = self.nside2ang(self.nside, nest=self.nest, degrees=False)[0] 

594 mapdec = self.nside2ang(self.nside, nest=self.nest, degrees=False)[1] 

595 return type(self)(np.degrees(dangle_rad(ra, dec, mapra, mapdec)), 

596 nest=self.nest) 

597 

598 # pylint: disable=C0103,R0913 

599 def psf_gaussian(self, ra, dec, sigma, degrees=True, normalize=True): 

600 """Calculate a Gaussian PSF at all pixels in this skymap. 

601 

602 Parameters 

603 ---------- 

604 ra : float 

605 Right Ascension of the Gaussian mean. 

606 dec : float 

607 Declination of the Gaussian mean. 

608 sigma : float 

609 Standard deviation of the Gaussian PSF. 

610 degrees : bool, optional 

611 If true, input arguments (``ra``, ``dec``, ``sigma``) are assumed 

612 to be in degrees. Otherwise, they are assumed to be in radians. 

613 normalize : bool, optional 

614 If true, normalize the returned skymap so that the integral over 

615 area is 1 (useful for when the Gaussian is meant to represent a 

616 probability density function). 

617 

618 Returns 

619 ------- 

620 skymap : HEALPixSkyMap 

621 A new skymap with the same pixelization where a 2-D Gaussian PSF 

622 (point spread function) centered at ``(ra, dec)`` with standard 

623 deviation ``sigma`` has been calculated for each pixel in this 

624 skymap. 

625 """ 

626 import numpy as np 

627 if not degrees: 

628 # pylint: disable=assignment-from-no-return 

629 sigma = np.degrees(sigma) 

630 min_scale, max_scale = psf_gaussian_scales(sigma) 

631 skymap = type(self)( 

632 psf_gaussian(self.dangle(ra, dec, degrees=degrees).pixels, sigma, 

633 degrees=degrees), 

634 nest=self.nest 

635 ) 

636 if normalize: 

637 skymap = skymap.normalize() 

638 return skymap 

639 

640 def __eq__(self, other): 

641 """Check whether two skymaps are the same WITHOUT conforming them 

642 first. This means that two instances representing the SAME skymap with 

643 different ordering will not be equal.""" 

644 if self.npix != other.npix: 

645 return False 

646 if self.nest != other.nest: 

647 return False 

648 return all(self.pixels == other.pixels) 

649 

650 def __add__(self, other): 

651 return type(self)(self.pixels + self.conform_skymaps(other)[0].pixels, 

652 nest=self.nest) 

653 

654 def __sub__(self, other): 

655 return type(self)(self.pixels - self.conform_skymaps(other)[0].pixels, 

656 nest=self.nest) 

657 

658 def __mul__(self, other): 

659 return type(self)(self.pixels * self.conform_skymaps(other)[0].pixels, 

660 nest=self.nest) 

661 

662 def __div__(self, other): 

663 return type(self)(self.pixels / self.conform_skymaps(other)[0].pixels, 

664 nest=self.nest) 

665 

666 def __pow__(self, power): 

667 return type(self)(self.pixels ** power, nest=self.nest) 

668 

669 def __len__(self): 

670 return self.npix 

671 

672 def __getitem__(self, key): 

673 return self.pixels[key] 

674 

675 def __setitem__(self, key, value): 

676 self.pixels[key] = value 

677 

678 def __repr__(self): 

679 fmt = '{}({}, nest={})' 

680 dic = self._to_dict() 

681 return fmt.format(type(self).__name__, dic['pixels'], dic['nest']) 

682 

683 def mollview(self, **kwargs): 

684 """Return a Mollweide projection of this skymap as a ``matplotlib`` 

685 figure with the correct nesting. Remaining ``**kwargs`` are passed to 

686 ``healpy.mollview``. Use the LIGO standard rotation where 180 degrees 

687 is at the center of the map.""" 

688 import healpy as hp 

689 import matplotlib.pyplot as plt 

690 if self.title is not None and "title" not in kwargs: 

691 kwargs['title'] = self.title 

692 kwargs['rot'] = kwargs.get('rot', (180, 0, 0)) 

693 hp.mollview(self.pixels, nest=self.nest, **kwargs) 

694 hp.graticule(dpar=15, dmer=30) 

695 return plt.gcf()