1 | # Licensed under a 3-clause BSD style license - see LICENSE.rst |
||
2 | """Utilities for dealing with HEALPix projections and mappings.""" |
||
3 | import copy |
||
4 | import re |
||
5 | import numpy as np |
||
6 | from astropy.coordinates import SkyCoord |
||
7 | from astropy.io import fits |
||
8 | from astropy.units import Quantity |
||
9 | from astropy import units as u |
||
10 | from astropy.utils import lazyproperty |
||
11 | from gammapy.utils.array import is_power2 |
||
12 | from .io import HPX_FITS_CONVENTIONS |
||
13 | from .utils import ( |
||
14 | get_subpixels, |
||
15 | get_superpixels, |
||
16 | parse_hpxregion, |
||
17 | unravel_hpx_index, |
||
18 | ravel_hpx_index, |
||
19 | coords_to_vec, |
||
20 | get_pix_size_from_nside, |
||
21 | get_nside_from_pix_size, |
||
22 | nside_to_order |
||
23 | ) |
||
24 | from .io import HpxConv |
||
25 | from ..utils import INVALID_INDEX, coordsys_to_frame, frame_to_coordsys |
||
26 | from ..geom import Geom, pix_tuple_to_idx |
||
27 | from ..coord import MapCoord, skycoord_to_lonlat |
||
28 | from ..axes import MapAxes |
||
29 | |||
30 | |||
31 | # Not sure if we should expose this in the docs or not: |
||
32 | # HPX_FITS_CONVENTIONS, HpxConv |
||
33 | __all__ = ["HpxGeom"] |
||
34 | |||
35 | |||
36 | class HpxGeom(Geom): |
||
37 | """Geometry class for HEALPIX maps. |
||
38 | |||
39 | This class performs mapping between partial-sky indices (pixel |
||
40 | number within a HEALPIX region) and all-sky indices (pixel number |
||
41 | within an all-sky HEALPIX map). Multi-band HEALPIX geometries use |
||
42 | a global indexing scheme that assigns a unique pixel number based |
||
43 | on the all-sky index and band index. In the single-band case the |
||
44 | global index is the same as the HEALPIX index. |
||
45 | |||
46 | By default the constructor will return an all-sky map. |
||
47 | Partial-sky maps can be defined with the ``region`` argument. |
||
48 | |||
49 | Parameters |
||
50 | ---------- |
||
51 | nside : `~numpy.ndarray` |
||
52 | HEALPIX nside parameter, the total number of pixels is |
||
53 | 12*nside*nside. For multi-dimensional maps one can pass |
||
54 | either a single nside value or a vector of nside values |
||
55 | defining the pixel size for each image plane. If nside is not |
||
56 | a scalar then its dimensionality should match that of the |
||
57 | non-spatial axes. |
||
58 | nest : bool |
||
59 | True -> 'NESTED', False -> 'RING' indexing scheme |
||
60 | frame : str |
||
61 | Coordinate system, "icrs" | "galactic" |
||
62 | region : str or tuple |
||
63 | Spatial geometry for partial-sky maps. If none the map will |
||
64 | encompass the whole sky. String input will be parsed |
||
65 | according to HPX_REG header keyword conventions. Tuple |
||
66 | input can be used to define an explicit list of pixels |
||
67 | encompassed by the geometry. |
||
68 | axes : list |
||
69 | Axes for non-spatial dimensions. |
||
70 | """ |
||
71 | is_hpx = True |
||
72 | is_region = False |
||
73 | |||
74 | def __init__( |
||
75 | self, nside, nest=True, frame="icrs", region=None, axes=None |
||
76 | ): |
||
77 | |||
78 | # FIXME: Require NSIDE to be power of two when nest=True |
||
79 | |||
80 | self._nside = np.array(nside, ndmin=1) |
||
81 | self._axes = MapAxes.from_default(axes, n_spatial_axes=1) |
||
82 | |||
83 | if self.nside.size > 1 and self.nside.shape != self.shape_axes: |
||
84 | raise ValueError( |
||
85 | "Wrong dimensionality for nside. nside must " |
||
86 | "be a scalar or have a dimensionality consistent " |
||
87 | "with the axes argument." |
||
88 | ) |
||
89 | |||
90 | self._nest = nest |
||
91 | self._frame = frame |
||
92 | |||
93 | self._ipix = None |
||
94 | self._region = region |
||
95 | self._create_lookup(region) |
||
96 | self._npix = self._npix * np.ones(self.shape_axes, dtype=int) |
||
97 | |||
98 | def _create_lookup(self, region): |
||
99 | """Create local-to-global pixel lookup table.""" |
||
100 | if isinstance(region, str): |
||
101 | ipix = [ |
||
102 | self.get_index_list(nside, self._nest, region) |
||
103 | for nside in self._nside.flat |
||
104 | ] |
||
105 | |||
106 | self._ipix = [ |
||
107 | ravel_hpx_index((p, i * np.ones_like(p)), np.ravel(self.npix_max)) |
||
108 | for i, p in enumerate(ipix) |
||
109 | ] |
||
110 | self._region = region |
||
111 | self._indxschm = "EXPLICIT" |
||
112 | self._npix = np.array([len(t) for t in self._ipix]) |
||
113 | if self.nside.ndim > 1: |
||
114 | self._npix = self._npix.reshape(self.nside.shape) |
||
115 | self._ipix = np.concatenate(self._ipix) |
||
116 | |||
117 | elif isinstance(region, tuple): |
||
118 | region = [np.asarray(t) for t in region] |
||
119 | m = np.any(np.stack([t >= 0 for t in region]), axis=0) |
||
120 | region = [t[m] for t in region] |
||
121 | |||
122 | self._ipix = ravel_hpx_index(region, self.npix_max) |
||
123 | self._ipix = np.unique(self._ipix) |
||
124 | region = unravel_hpx_index(self._ipix, self.npix_max) |
||
125 | self._region = "explicit" |
||
126 | self._indxschm = "EXPLICIT" |
||
127 | if len(region) == 1: |
||
128 | self._npix = np.array([len(region[0])]) |
||
129 | else: |
||
130 | self._npix = np.zeros(self.shape_axes, dtype=int) |
||
131 | idx = np.ravel_multi_index(region[1:], self.shape_axes) |
||
132 | cnt = np.unique(idx, return_counts=True) |
||
133 | self._npix.flat[cnt[0]] = cnt[1] |
||
134 | |||
135 | elif region is None: |
||
136 | self._region = None |
||
137 | self._indxschm = "IMPLICIT" |
||
138 | self._npix = self.npix_max |
||
139 | |||
140 | else: |
||
141 | raise ValueError(f"Invalid region string: {region!r}") |
||
142 | |||
143 | def local_to_global(self, idx_local): |
||
144 | """Compute a local index (partial-sky) from a global (all-sky) index. |
||
145 | |||
146 | Returns |
||
147 | ------- |
||
148 | idx_global : tuple |
||
149 | A tuple of pixel index vectors with global HEALPIX pixel indices |
||
150 | """ |
||
151 | if self._ipix is None: |
||
152 | return idx_local |
||
153 | |||
154 | if self.nside.size > 1: |
||
155 | idx = ravel_hpx_index(idx_local, self._npix) |
||
156 | else: |
||
157 | idx_tmp = tuple( |
||
158 | [idx_local[0]] + [np.zeros(t.shape, dtype=int) for t in idx_local[1:]] |
||
159 | ) |
||
160 | idx = ravel_hpx_index(idx_tmp, self._npix) |
||
161 | |||
162 | idx_global = unravel_hpx_index(self._ipix[idx], self.npix_max) |
||
163 | return idx_global[:1] + tuple(idx_local[1:]) |
||
164 | |||
165 | def global_to_local(self, idx_global, ravel=False): |
||
166 | """Compute global (all-sky) index from a local (partial-sky) index. |
||
167 | |||
168 | Parameters |
||
169 | ---------- |
||
170 | idx_global : tuple |
||
171 | A tuple of pixel indices with global HEALPix pixel indices. |
||
172 | ravel : bool |
||
173 | Return a raveled index. |
||
174 | |||
175 | Returns |
||
176 | ------- |
||
177 | idx_local : tuple |
||
178 | A tuple of pixel indices with local HEALPIX pixel indices. |
||
179 | """ |
||
180 | if ( |
||
181 | isinstance(idx_global, int) |
||
182 | or (isinstance(idx_global, tuple) and isinstance(idx_global[0], int)) |
||
183 | or isinstance(idx_global, np.ndarray) |
||
184 | ): |
||
185 | idx_global = unravel_hpx_index(np.array(idx_global, ndmin=1), self.npix_max) |
||
186 | |||
187 | if self.nside.size == 1: |
||
188 | idx = np.array(idx_global[0], ndmin=1) |
||
189 | else: |
||
190 | idx = ravel_hpx_index(idx_global, self.npix_max) |
||
191 | |||
192 | if self._ipix is not None: |
||
193 | retval = np.full(idx.size, -1, "i") |
||
194 | m = np.isin(idx.flat, self._ipix) |
||
195 | retval[m] = np.searchsorted(self._ipix, idx.flat[m]) |
||
196 | retval = retval.reshape(idx.shape) |
||
197 | else: |
||
198 | retval = idx |
||
199 | |||
200 | if self.nside.size == 1: |
||
201 | idx_local = tuple([retval] + list(idx_global[1:])) |
||
202 | else: |
||
203 | idx_local = unravel_hpx_index(retval, self._npix) |
||
204 | |||
205 | m = np.any(np.stack([t == INVALID_INDEX.int for t in idx_local]), axis=0) |
||
206 | for i, t in enumerate(idx_local): |
||
207 | idx_local[i][m] = INVALID_INDEX.int |
||
208 | |||
209 | if not ravel: |
||
210 | return idx_local |
||
211 | else: |
||
212 | return ravel_hpx_index(idx_local, self.npix) |
||
213 | |||
214 | def cutout(self, position, width, **kwargs): |
||
215 | """Create a cutout around a given position. |
||
216 | |||
217 | Parameters |
||
218 | ---------- |
||
219 | position : `~astropy.coordinates.SkyCoord` |
||
220 | Center position of the cutout region. |
||
221 | width : `~astropy.coordinates.Angle` or `~astropy.units.Quantity` |
||
222 | Diameter of the circular cutout region. |
||
223 | |||
224 | Returns |
||
225 | ------- |
||
226 | cutout : `~gammapy.maps.WcsNDMap` |
||
227 | Cutout map |
||
228 | """ |
||
229 | if not self.is_regular: |
||
230 | raise ValueError("Can only do a cutout from a regular map.") |
||
231 | |||
232 | width = u.Quantity(width, "deg").value |
||
233 | return self.create( |
||
234 | nside=self.nside, |
||
235 | nest=self.nest, |
||
236 | width=width, |
||
237 | skydir=position, |
||
238 | frame=self.frame, |
||
239 | axes=self.axes |
||
240 | ) |
||
241 | |||
242 | def coord_to_pix(self, coords): |
||
243 | import healpy as hp |
||
244 | |||
245 | coords = MapCoord.create(coords, frame=self.frame, axis_names=self.axes.names).broadcasted |
||
246 | theta, phi = coords.theta, coords.phi |
||
247 | |||
248 | if self.axes: |
||
249 | idxs = self.axes.coord_to_idx(coords, clip=True) |
||
250 | bins = self.axes.coord_to_pix(coords) |
||
251 | |||
252 | # FIXME: Figure out how to handle coordinates out of |
||
253 | # bounds of non-spatial dimensions |
||
254 | if self.nside.size > 1: |
||
255 | nside = self.nside[tuple(idxs)] |
||
256 | else: |
||
257 | nside = self.nside |
||
258 | |||
259 | m = ~np.isfinite(theta) |
||
260 | theta[m] = 0.0 |
||
261 | phi[m] = 0.0 |
||
262 | pix = hp.ang2pix(nside, theta, phi, nest=self.nest) |
||
263 | pix = tuple([pix]) + bins |
||
264 | if np.any(m): |
||
265 | for p in pix: |
||
266 | p[m] = INVALID_INDEX.int |
||
267 | else: |
||
268 | pix = (hp.ang2pix(self.nside, theta, phi, nest=self.nest),) |
||
269 | |||
270 | return pix |
||
271 | |||
272 | def pix_to_coord(self, pix): |
||
273 | import healpy as hp |
||
274 | |||
275 | if self.axes: |
||
276 | bins = [] |
||
277 | vals = [] |
||
278 | for i, ax in enumerate(self.axes): |
||
279 | bins += [pix[1 + i]] |
||
280 | vals += [ax.pix_to_coord(pix[1 + i])] |
||
281 | |||
282 | idxs = pix_tuple_to_idx(bins) |
||
283 | |||
284 | if self.nside.size > 1: |
||
285 | nside = self.nside[idxs] |
||
286 | else: |
||
287 | nside = self.nside |
||
288 | |||
289 | ipix = np.round(pix[0]).astype(int) |
||
290 | m = ipix == INVALID_INDEX.int |
||
291 | ipix[m] = 0 |
||
292 | theta, phi = hp.pix2ang(nside, ipix, nest=self.nest) |
||
293 | coords = [np.degrees(phi), np.degrees(np.pi / 2.0 - theta)] |
||
294 | coords = tuple(coords + vals) |
||
295 | if np.any(m): |
||
296 | for c in coords: |
||
297 | c[m] = INVALID_INDEX.float |
||
298 | else: |
||
299 | ipix = np.round(pix[0]).astype(int) |
||
300 | theta, phi = hp.pix2ang(self.nside, ipix, nest=self.nest) |
||
301 | coords = (np.degrees(phi), np.degrees(np.pi / 2.0 - theta)) |
||
302 | |||
303 | return coords |
||
304 | |||
305 | def pix_to_idx(self, pix, clip=False): |
||
306 | # FIXME: Look for better method to clip HPX indices |
||
307 | # TODO: copy idx to avoid modifying input pix? |
||
308 | # pix_tuple_to_idx seems to always make a copy!? |
||
309 | idx = pix_tuple_to_idx(pix) |
||
310 | idx_local = self.global_to_local(idx) |
||
311 | for i, _ in enumerate(idx): |
||
312 | |||
313 | if clip: |
||
314 | if i > 0: |
||
315 | np.clip(idx[i], 0, self.axes[i - 1].nbin - 1, out=idx[i]) |
||
316 | else: |
||
317 | np.clip(idx[i], 0, None, out=idx[i]) |
||
318 | else: |
||
319 | if i > 0: |
||
320 | mask = (idx[i] < 0) | (idx[i] >= self.axes[i - 1].nbin) |
||
321 | np.putmask(idx[i], mask, -1) |
||
322 | else: |
||
323 | mask = (idx_local[i] < 0) | (idx[i] < 0) |
||
324 | np.putmask(idx[i], mask, -1) |
||
325 | |||
326 | return tuple(idx) |
||
327 | |||
328 | @property |
||
329 | def axes(self): |
||
330 | """List of non-spatial axes.""" |
||
331 | return self._axes |
||
332 | |||
333 | @property |
||
334 | def axes_names(self): |
||
335 | """All axes names""" |
||
336 | return ["skycoord"] + self.axes.names |
||
337 | |||
338 | @property |
||
339 | def shape_axes(self): |
||
340 | """Shape of non-spatial axes.""" |
||
341 | return self.axes.shape |
||
342 | |||
343 | @property |
||
344 | def data_shape(self): |
||
345 | """Shape of the Numpy data array matching this geometry.""" |
||
346 | npix_shape = tuple([np.max(self.npix)]) |
||
347 | return (npix_shape + self.axes.shape)[::-1] |
||
348 | |||
349 | @property |
||
350 | def data_shape_axes(self): |
||
351 | """Shape of data of the non-spatial axes and unit spatial axes.""" |
||
352 | return self.axes.shape[::-1] + (1,) |
||
353 | |||
354 | @property |
||
355 | def ndim(self): |
||
356 | """Number of dimensions (int).""" |
||
357 | return len(self._axes) + 2 |
||
358 | |||
359 | @property |
||
360 | def ordering(self): |
||
361 | """HEALPix ordering ('NESTED' or 'RING').""" |
||
362 | return "NESTED" if self.nest else "RING" |
||
363 | |||
364 | @property |
||
365 | def nside(self): |
||
366 | """NSIDE in each band.""" |
||
367 | return self._nside |
||
368 | |||
369 | @property |
||
370 | def order(self): |
||
371 | """ORDER in each band (``NSIDE = 2 ** ORDER``). |
||
372 | |||
373 | Set to -1 for bands with NSIDE that is not a power of 2. |
||
374 | """ |
||
375 | return nside_to_order(self.nside) |
||
376 | |||
377 | @property |
||
378 | def nest(self): |
||
379 | """Is HEALPix order nested? (bool).""" |
||
380 | return self._nest |
||
381 | |||
382 | @property |
||
383 | def npix(self): |
||
384 | """Number of pixels in each band. |
||
385 | |||
386 | For partial-sky geometries this can |
||
387 | be less than the number of pixels for the band NSIDE. |
||
388 | """ |
||
389 | return self._npix |
||
390 | |||
391 | @property |
||
392 | def npix_max(self): |
||
393 | """Max. number of pixels""" |
||
394 | maxpix = 12 * self.nside ** 2 |
||
395 | return maxpix * np.ones(self.shape_axes, dtype=int) |
||
396 | |||
397 | @property |
||
398 | def frame(self): |
||
399 | return self._frame |
||
400 | |||
401 | @property |
||
402 | def projection(self): |
||
403 | """Map projection.""" |
||
404 | return "HPX" |
||
405 | |||
406 | @property |
||
407 | def region(self): |
||
408 | """Region string.""" |
||
409 | return self._region |
||
410 | |||
411 | @property |
||
412 | def is_allsky(self): |
||
413 | """Flag for all-sky maps.""" |
||
414 | return self._region is None |
||
415 | |||
416 | @property |
||
417 | def is_regular(self): |
||
418 | """Flag identifying whether this geometry is regular in non-spatial dimensions. |
||
419 | |||
420 | False for multi-resolution or irregular geometries. |
||
421 | If true all image planes have the same pixel geometry. |
||
422 | """ |
||
423 | if self.nside.size > 1 or self.region == "explicit": |
||
424 | return False |
||
425 | else: |
||
426 | return True |
||
427 | |||
428 | @property |
||
429 | def center_coord(self): |
||
430 | """Map coordinates of the center of the geometry (tuple).""" |
||
431 | lon, lat, frame = skycoord_to_lonlat(self.center_skydir) |
||
432 | return tuple([lon, lat]) + self.axes.center_coord |
||
433 | |||
434 | @property |
||
435 | def center_pix(self): |
||
436 | """Pixel coordinates of the center of the geometry (tuple).""" |
||
437 | return self.coord_to_pix(self.center_coord) |
||
438 | |||
439 | @property |
||
440 | def center_skydir(self): |
||
441 | """Sky coordinate of the center of the geometry. |
||
442 | |||
443 | Returns |
||
444 | ------- |
||
445 | center : `~astropy.coordinates.SkyCoord` |
||
446 | Center position |
||
447 | """ |
||
448 | # TODO: simplify |
||
449 | import healpy as hp |
||
450 | |||
451 | if self.is_allsky: |
||
452 | lon, lat = 0., 0. |
||
453 | elif self.region == "explicit": |
||
454 | idx = unravel_hpx_index(self._ipix, self.npix_max) |
||
455 | nside = self._get_nside(idx) |
||
456 | vec = hp.pix2vec(nside, idx[0], nest=self.nest) |
||
457 | vec = np.array([np.mean(t) for t in vec]) |
||
458 | lonlat = hp.vec2ang(vec, lonlat=True) |
||
459 | lon, lat = lonlat[0], lonlat[1] |
||
460 | else: |
||
461 | tokens = parse_hpxregion(self.region) |
||
462 | if tokens[0] in ["DISK", "DISK_INC"]: |
||
463 | lon, lat = float(tokens[1]), float(tokens[2]) |
||
464 | elif tokens[0] == "HPX_PIXEL": |
||
465 | nside_pix = int(tokens[2]) |
||
466 | ipix_pix = int(tokens[3]) |
||
467 | if tokens[1] == "NESTED": |
||
468 | nest_pix = True |
||
469 | elif tokens[1] == "RING": |
||
470 | nest_pix = False |
||
471 | else: |
||
472 | raise ValueError(f"Invalid ordering scheme: {tokens[1]!r}") |
||
473 | theta, phi = hp.pix2ang(nside_pix, ipix_pix, nest_pix) |
||
474 | lat = np.degrees((np.pi / 2) - theta) |
||
475 | lon = np.degrees(phi) |
||
476 | |||
477 | return SkyCoord(lon, lat, frame=self.frame, unit="deg") |
||
478 | |||
479 | @property |
||
480 | def pixel_scales(self): |
||
481 | self.angle_ = """Pixel scale. |
||
482 | |||
483 | Returns |
||
484 | ------- |
||
485 | angle: `~astropy.coordinates.Angle` |
||
486 | """ |
||
487 | return get_pix_size_from_nside(self.nside) * u.deg |
||
488 | |||
489 | def interp_weights(self, coords, idxs=None): |
||
490 | """Get interpolation weights for given coords |
||
491 | |||
492 | Parameters |
||
493 | ---------- |
||
494 | coords : `MapCoord` or dict |
||
495 | Input coordinates |
||
496 | idxs : `~numpy.ndarray` |
||
497 | Indices for non-spatial axes. |
||
498 | |||
499 | Returns |
||
500 | ------- |
||
501 | weights : `~numpy.ndarray` |
||
502 | Interpolation weights |
||
503 | """ |
||
504 | import healpy as hp |
||
505 | |||
506 | coords = MapCoord.create(coords, frame=self.frame).broadcasted |
||
507 | |||
508 | if idxs is None: |
||
509 | idxs = self.coord_to_idx(coords, clip=True)[1:] |
||
510 | |||
511 | theta, phi = coords.theta, coords.phi |
||
512 | |||
513 | m = ~np.isfinite(theta) |
||
514 | theta[m] = 0 |
||
515 | phi[m] = 0 |
||
516 | |||
517 | if not self.is_regular: |
||
518 | nside = self.nside[tuple(idxs)] |
||
519 | else: |
||
520 | nside = self.nside |
||
521 | |||
522 | pix, wts = hp.get_interp_weights(nside, theta, phi, nest=self.nest) |
||
523 | wts[:, m] = 0 |
||
524 | pix[:, m] = INVALID_INDEX.int |
||
525 | |||
526 | if not self.is_regular: |
||
527 | pix_local = [self.global_to_local([pix] + list(idxs))[0]] |
||
528 | else: |
||
529 | pix_local = [self.global_to_local(pix, ravel=True)] |
||
530 | |||
531 | # If a pixel lies outside of the geometry set its index to the center pixel |
||
532 | m = pix_local[0] == INVALID_INDEX.int |
||
533 | if m.any(): |
||
534 | coords_ctr = [coords.lon, coords.lat] |
||
535 | coords_ctr += [ax.pix_to_coord(t) for ax, t in zip(self.axes, idxs)] |
||
536 | idx_ctr = self.coord_to_idx(coords_ctr) |
||
537 | idx_ctr = self.global_to_local(idx_ctr) |
||
538 | pix_local[0][m] = (idx_ctr[0] * np.ones(pix.shape, dtype=int))[m] |
||
539 | |||
540 | pix_local += [np.broadcast_to(t, pix_local[0].shape) for t in idxs] |
||
541 | return pix_local, wts |
||
542 | |||
543 | @property |
||
544 | def ipix(self): |
||
545 | """HEALPIX pixel and band indices for every pixel in the map.""" |
||
546 | return self.get_idx() |
||
547 | |||
548 | def is_aligned(self, other): |
||
549 | """Check if HEALPIx geoms and extra axes are aligned. |
||
550 | |||
551 | Parameters |
||
552 | ---------- |
||
553 | other : `HpxGeom` |
||
554 | Other geom. |
||
555 | |||
556 | Returns |
||
557 | ------- |
||
558 | aligned : bool |
||
559 | Whether geometries are aligned |
||
560 | """ |
||
561 | for axis, otheraxis in zip(self.axes, other.axes): |
||
562 | if axis != otheraxis: |
||
563 | return False |
||
564 | |||
565 | if not self.nside == other.nside: |
||
566 | return False |
||
567 | elif not self.frame == other.frame: |
||
568 | return False |
||
569 | elif not self.nest == other.nest: |
||
570 | return False |
||
571 | else: |
||
572 | return True |
||
573 | |||
574 | def to_nside(self, nside): |
||
575 | """Upgrade or downgrade the reoslution to a given nside |
||
576 | |||
577 | Parameters |
||
578 | ---------- |
||
579 | nside : int |
||
580 | Nside |
||
581 | |||
582 | Returns |
||
583 | ------- |
||
584 | geom : `~HpxGeom` |
||
585 | A HEALPix geometry object. |
||
586 | """ |
||
587 | if not self.is_regular: |
||
588 | raise ValueError("Upgrade and degrade only implemented for standard maps") |
||
589 | |||
590 | axes = copy.deepcopy(self.axes) |
||
591 | return self.__class__( |
||
592 | nside=nside, |
||
593 | nest=self.nest, |
||
594 | frame=self.frame, |
||
595 | region=self.region, |
||
596 | axes=axes |
||
597 | ) |
||
598 | |||
599 | def to_binsz(self, binsz): |
||
600 | """Change pixel size of the geometry. |
||
601 | |||
602 | Parameters |
||
603 | ---------- |
||
604 | binsz : float or `~astropy.units.Quantity` |
||
605 | New pixel size. A float is assumed to be in degree. |
||
606 | |||
607 | Returns |
||
608 | ------- |
||
609 | geom : `WcsGeom` |
||
610 | Geometry with new pixel size. |
||
611 | """ |
||
612 | binsz = u.Quantity(binsz, "deg").value |
||
613 | |||
614 | if self.is_allsky: |
||
615 | return self.create( |
||
616 | binsz=binsz, |
||
617 | frame=self.frame, |
||
618 | axes=copy.deepcopy(self.axes), |
||
619 | ) |
||
620 | else: |
||
621 | return self.create( |
||
622 | skydir=self.center_skydir, |
||
623 | binsz=binsz, |
||
624 | width=self.width.to_value("deg"), |
||
625 | frame=self.frame, |
||
626 | axes=copy.deepcopy(self.axes), |
||
627 | ) |
||
628 | |||
629 | def separation(self, center): |
||
630 | """Compute sky separation wrt a given center. |
||
631 | |||
632 | Parameters |
||
633 | ---------- |
||
634 | center : `~astropy.coordinates.SkyCoord` |
||
635 | Center position |
||
636 | |||
637 | Returns |
||
638 | ------- |
||
639 | separation : `~astropy.coordinates.Angle` |
||
640 | Separation angle array (1D) |
||
641 | """ |
||
642 | coord = self.to_image().get_coord() |
||
643 | return center.separation(coord.skycoord) |
||
644 | |||
645 | def to_swapped(self): |
||
646 | """Geometry copy with swapped ORDERING (NEST->RING or vice versa). |
||
647 | |||
648 | Returns |
||
649 | ------- |
||
650 | geom : `~HpxGeom` |
||
651 | A HEALPix geometry object. |
||
652 | """ |
||
653 | axes = copy.deepcopy(self.axes) |
||
654 | return self.__class__( |
||
655 | self.nside, not self.nest, frame=self.frame, region=self.region, axes=axes, |
||
656 | ) |
||
657 | |||
658 | def to_image(self): |
||
659 | return self.__class__( |
||
660 | np.max(self.nside), self.nest, frame=self.frame, region=self.region |
||
661 | ) |
||
662 | |||
663 | def to_cube(self, axes): |
||
664 | axes = copy.deepcopy(self.axes) + axes |
||
665 | return self.__class__( |
||
666 | np.max(self.nside), |
||
667 | self.nest, |
||
668 | frame=self.frame, |
||
669 | region=self.region, |
||
670 | axes=axes, |
||
671 | ) |
||
672 | |||
673 | def _get_neighbors(self, idx): |
||
674 | import healpy as hp |
||
675 | |||
676 | nside = self._get_nside(idx) |
||
677 | idx_nb = (hp.get_all_neighbours(nside, idx[0], nest=self.nest),) |
||
678 | idx_nb += tuple([t[None, ...] * np.ones_like(idx_nb[0]) for t in idx[1:]]) |
||
679 | |||
680 | return idx_nb |
||
681 | |||
682 | def _pad_spatial(self, pad_width): |
||
683 | if self.is_allsky: |
||
684 | raise ValueError("Cannot pad an all-sky map.") |
||
685 | |||
686 | idx = self.get_idx(flat=True) |
||
687 | idx_r = ravel_hpx_index(idx, self.npix_max) |
||
688 | |||
689 | # TODO: Pre-filter indices to find those close to the edge |
||
690 | idx_nb = self._get_neighbors(idx) |
||
691 | idx_nb = ravel_hpx_index(idx_nb, self.npix_max) |
||
692 | |||
693 | for _ in range(pad_width): |
||
694 | mask_edge = np.isin(idx_nb, idx_r, invert=True) |
||
695 | idx_edge = idx_nb[mask_edge] |
||
696 | idx_edge = np.unique(idx_edge) |
||
697 | idx_r = np.sort(np.concatenate((idx_r, idx_edge))) |
||
698 | idx_nb = unravel_hpx_index(idx_edge, self.npix_max) |
||
699 | idx_nb = self._get_neighbors(idx_nb) |
||
700 | idx_nb = ravel_hpx_index(idx_nb, self.npix_max) |
||
701 | |||
702 | idx = unravel_hpx_index(idx_r, self.npix_max) |
||
703 | return self.__class__( |
||
704 | self.nside.copy(), |
||
705 | self.nest, |
||
706 | frame=self.frame, |
||
707 | region=idx, |
||
708 | axes=copy.deepcopy(self.axes), |
||
709 | ) |
||
710 | |||
711 | def crop(self, crop_width): |
||
712 | if self.is_allsky: |
||
713 | raise ValueError("Cannot crop an all-sky map.") |
||
714 | |||
715 | idx = self.get_idx(flat=True) |
||
716 | idx_r = ravel_hpx_index(idx, self.npix_max) |
||
717 | |||
718 | # TODO: Pre-filter indices to find those close to the edge |
||
719 | idx_nb = self._get_neighbors(idx) |
||
720 | idx_nb = ravel_hpx_index(idx_nb, self.npix_max) |
||
721 | |||
722 | for _ in range(crop_width): |
||
723 | # Mask of pixels that have at least one neighbor not |
||
724 | # contained in the geometry |
||
725 | mask_edge = np.any(np.isin(idx_nb, idx_r, invert=True), axis=0) |
||
726 | idx_r = idx_r[~mask_edge] |
||
727 | idx_nb = idx_nb[:, ~mask_edge] |
||
728 | |||
729 | idx = unravel_hpx_index(idx_r, self.npix_max) |
||
730 | return self.__class__( |
||
731 | self.nside.copy(), |
||
732 | self.nest, |
||
733 | frame=self.frame, |
||
734 | region=idx, |
||
735 | axes=copy.deepcopy(self.axes), |
||
736 | ) |
||
737 | |||
738 | def upsample(self, factor): |
||
739 | if not is_power2(factor): |
||
740 | raise ValueError("Upsample factor must be a power of 2.") |
||
741 | |||
742 | if self.is_allsky: |
||
743 | return self.__class__( |
||
744 | self.nside * factor, |
||
745 | self.nest, |
||
746 | frame=self.frame, |
||
747 | region=self.region, |
||
748 | axes=copy.deepcopy(self.axes), |
||
749 | ) |
||
750 | |||
751 | idx = list(self.get_idx(flat=True)) |
||
752 | nside = self._get_nside(idx) |
||
753 | |||
754 | idx_new = get_subpixels(idx[0], nside, nside * factor, nest=self.nest) |
||
755 | for i in range(1, len(idx)): |
||
756 | idx[i] = idx[i][..., None] * np.ones(idx_new.shape, dtype=int) |
||
757 | |||
758 | idx[0] = idx_new |
||
759 | return self.__class__( |
||
760 | self.nside * factor, |
||
761 | self.nest, |
||
762 | frame=self.frame, |
||
763 | region=tuple(idx), |
||
764 | axes=copy.deepcopy(self.axes), |
||
765 | ) |
||
766 | |||
767 | def downsample(self, factor, axis_name=None): |
||
768 | if not is_power2(factor): |
||
769 | raise ValueError("Downsample factor must be a power of 2.") |
||
770 | if axis_name is not None: |
||
771 | raise ValueError("Currently the only valid axis name is None.") |
||
772 | |||
773 | if self.is_allsky: |
||
774 | return self.__class__( |
||
775 | self.nside // factor, |
||
776 | self.nest, |
||
777 | frame=self.frame, |
||
778 | region=self.region, |
||
779 | axes=copy.deepcopy(self.axes), |
||
780 | ) |
||
781 | |||
782 | idx = list(self.get_idx(flat=True)) |
||
783 | nside = self._get_nside(idx) |
||
784 | idx_new = get_superpixels(idx[0], nside, nside // factor, nest=self.nest) |
||
785 | idx[0] = idx_new |
||
786 | return self.__class__( |
||
787 | self.nside // factor, |
||
788 | self.nest, |
||
789 | frame=self.frame, |
||
790 | region=tuple(idx), |
||
791 | axes=copy.deepcopy(self.axes), |
||
792 | ) |
||
793 | |||
794 | @classmethod |
||
795 | def create( |
||
796 | cls, |
||
797 | nside=None, |
||
798 | binsz=None, |
||
799 | nest=True, |
||
800 | frame="icrs", |
||
801 | region=None, |
||
802 | axes=None, |
||
803 | skydir=None, |
||
804 | width=None, |
||
805 | ): |
||
806 | """Create an HpxGeom object. |
||
807 | |||
808 | Parameters |
||
809 | ---------- |
||
810 | nside : int or `~numpy.ndarray` |
||
811 | HEALPix NSIDE parameter. This parameter sets the size of |
||
812 | the spatial pixels in the map. |
||
813 | binsz : float or `~numpy.ndarray` |
||
814 | Approximate pixel size in degrees. An NSIDE will be |
||
815 | chosen that corresponds to a pixel size closest to this |
||
816 | value. This option is superseded by nside. |
||
817 | nest : bool |
||
818 | True for HEALPIX "NESTED" indexing scheme, False for "RING" scheme |
||
819 | frame : {"icrs", "galactic"}, optional |
||
820 | Coordinate system, either Galactic ("galactic") or Equatorial ("icrs"). |
||
821 | skydir : tuple or `~astropy.coordinates.SkyCoord` |
||
822 | Sky position of map center. Can be either a SkyCoord |
||
823 | object or a tuple of longitude and latitude in deg in the |
||
824 | coordinate system of the map. |
||
825 | region : str |
||
826 | HPX region string. Allows for partial-sky maps. |
||
827 | width : float |
||
828 | Diameter of the map in degrees. If set the map will |
||
829 | encompass all pixels within a circular region centered on |
||
830 | ``skydir``. |
||
831 | axes : list |
||
832 | List of axes for non-spatial dimensions. |
||
833 | |||
834 | Returns |
||
835 | ------- |
||
836 | geom : `~HpxGeom` |
||
837 | A HEALPix geometry object. |
||
838 | |||
839 | Examples |
||
840 | -------- |
||
841 | >>> from gammapy.maps import HpxGeom, MapAxis |
||
842 | >>> axis = MapAxis.from_bounds(0,1,2) |
||
843 | >>> geom = HpxGeom.create(nside=16) |
||
844 | >>> geom = HpxGeom.create(binsz=0.1, width=10.0) |
||
845 | >>> geom = HpxGeom.create(nside=64, width=10.0, axes=[axis]) |
||
846 | >>> geom = HpxGeom.create(nside=[32,64], width=10.0, axes=[axis]) |
||
847 | """ |
||
848 | if nside is None and binsz is None: |
||
849 | raise ValueError("Either nside or binsz must be defined.") |
||
850 | |||
851 | if nside is None and binsz is not None: |
||
852 | nside = get_nside_from_pix_size(binsz) |
||
853 | |||
854 | if skydir is None: |
||
855 | lon, lat = (0.0, 0.0) |
||
856 | elif isinstance(skydir, tuple): |
||
857 | lon, lat = skydir |
||
858 | elif isinstance(skydir, SkyCoord): |
||
859 | lon, lat, frame = skycoord_to_lonlat(skydir, frame=frame) |
||
860 | else: |
||
861 | raise ValueError(f"Invalid type for skydir: {type(skydir)!r}") |
||
862 | |||
863 | if region is None and width is not None: |
||
864 | region = f"DISK({lon},{lat},{width/2})" |
||
865 | |||
866 | return cls(nside, nest=nest, frame=frame, region=region, axes=axes) |
||
867 | |||
868 | @classmethod |
||
869 | def from_header(cls, header, hdu_bands=None, format=None): |
||
870 | """Create an HPX object from a FITS header. |
||
871 | |||
872 | Parameters |
||
873 | ---------- |
||
874 | header : `~astropy.io.fits.Header` |
||
875 | The FITS header |
||
876 | hdu_bands : `~astropy.io.fits.BinTableHDU` |
||
877 | The BANDS table HDU. |
||
878 | format : str, optional |
||
879 | FITS convention. If None the format is guessed. The following |
||
880 | formats are supported: |
||
881 | |||
882 | - "gadf" |
||
883 | - "fgst-ccube" |
||
884 | - "fgst-ltcube" |
||
885 | - "fgst-bexpcube" |
||
886 | - "fgst-srcmap" |
||
887 | - "fgst-template" |
||
888 | - "fgst-srcmap-sparse" |
||
889 | - "galprop" |
||
890 | - "galprop2" |
||
891 | - "healpy" |
||
892 | |||
893 | Returns |
||
894 | ------- |
||
895 | hpx : `~HpxGeom` |
||
896 | HEALPix geometry. |
||
897 | """ |
||
898 | if format is None: |
||
899 | format = HpxConv.identify_hpx_format(header) |
||
900 | |||
901 | conv = HPX_FITS_CONVENTIONS[format] |
||
902 | |||
903 | axes = MapAxes.from_table_hdu(hdu_bands, format=format) |
||
904 | |||
905 | if header["PIXTYPE"] != "HEALPIX": |
||
906 | raise ValueError( |
||
907 | f"Invalid header PIXTYPE: {header['PIXTYPE']} (must be HEALPIX)" |
||
908 | ) |
||
909 | |||
910 | if header["ORDERING"] == "RING": |
||
911 | nest = False |
||
912 | elif header["ORDERING"] == "NESTED": |
||
913 | nest = True |
||
914 | else: |
||
915 | raise ValueError( |
||
916 | f"Invalid header ORDERING: {header['ORDERING']} (must be RING or NESTED)" |
||
917 | ) |
||
918 | |||
919 | if hdu_bands is not None and "NSIDE" in hdu_bands.columns.names: |
||
920 | nside = hdu_bands.data.field("NSIDE").reshape(axes.shape).astype(int) |
||
921 | elif "NSIDE" in header: |
||
922 | nside = header["NSIDE"] |
||
923 | elif "ORDER" in header: |
||
924 | nside = 2 ** header["ORDER"] |
||
925 | else: |
||
926 | raise ValueError("Failed to extract NSIDE or ORDER.") |
||
927 | |||
928 | try: |
||
929 | frame = coordsys_to_frame(header[conv.frame]) |
||
930 | except KeyError: |
||
931 | frame = header.get("COORDSYS", "icrs") |
||
932 | |||
933 | try: |
||
934 | region = header["HPX_REG"] |
||
935 | except KeyError: |
||
936 | try: |
||
937 | region = header["HPXREGION"] |
||
938 | except KeyError: |
||
939 | region = None |
||
940 | |||
941 | return cls(nside, nest, frame=frame, region=region, axes=axes) |
||
942 | |||
943 | @classmethod |
||
944 | def from_hdu(cls, hdu, hdu_bands=None): |
||
945 | """Create an HPX object from a BinTable HDU. |
||
946 | |||
947 | Parameters |
||
948 | ---------- |
||
949 | hdu : `~astropy.io.fits.BinTableHDU` |
||
950 | The FITS HDU |
||
951 | hdu_bands : `~astropy.io.fits.BinTableHDU` |
||
952 | The BANDS table HDU |
||
953 | |||
954 | Returns |
||
955 | ------- |
||
956 | hpx : `~HpxGeom` |
||
957 | HEALPix geometry. |
||
958 | """ |
||
959 | # FIXME: Need correct handling of IMPLICIT and EXPLICIT maps |
||
960 | |||
961 | # if HPX region is not defined then geometry is defined by |
||
962 | # the set of all pixels in the table |
||
963 | if "HPX_REG" not in hdu.header: |
||
964 | pix = (hdu.data.field("PIX"), hdu.data.field("CHANNEL")) |
||
965 | else: |
||
966 | pix = None |
||
967 | |||
968 | return cls.from_header(hdu.header, hdu_bands=hdu_bands, pix=pix) |
||
969 | |||
970 | def to_header(self, format="gadf", **kwargs): |
||
971 | """Build and return FITS header for this HEALPIX map.""" |
||
972 | header = fits.Header() |
||
973 | format = kwargs.get("format", HPX_FITS_CONVENTIONS[format]) |
||
974 | |||
975 | # FIXME: For some sparse maps we may want to allow EXPLICIT |
||
976 | # with an empty region string |
||
977 | indxschm = kwargs.get("indxschm", None) |
||
978 | |||
979 | if indxschm is None: |
||
980 | if self._region is None: |
||
981 | indxschm = "IMPLICIT" |
||
982 | elif self.is_regular == 1: |
||
983 | indxschm = "EXPLICIT" |
||
984 | else: |
||
985 | indxschm = "LOCAL" |
||
986 | |||
987 | if "FGST" in format.convname.upper(): |
||
988 | header["TELESCOP"] = "GLAST" |
||
989 | header["INSTRUME"] = "LAT" |
||
990 | |||
991 | header[format.frame] = frame_to_coordsys(self.frame) |
||
992 | header["PIXTYPE"] = "HEALPIX" |
||
993 | header["ORDERING"] = self.ordering |
||
994 | header["INDXSCHM"] = indxschm |
||
995 | header["ORDER"] = np.max(self.order) |
||
996 | header["NSIDE"] = np.max(self.nside) |
||
997 | header["FIRSTPIX"] = 0 |
||
998 | header["LASTPIX"] = np.max(self.npix_max) - 1 |
||
999 | header["HPX_CONV"] = format.convname.upper() |
||
1000 | |||
1001 | if self.frame == "icrs": |
||
1002 | header["EQUINOX"] = (2000.0, "Equinox of RA & DEC specifications") |
||
1003 | |||
1004 | if self.region: |
||
1005 | header["HPX_REG"] = self._region |
||
1006 | |||
1007 | return header |
||
1008 | |||
1009 | def _make_bands_cols(self): |
||
1010 | cols = [] |
||
1011 | if self.nside.size > 1: |
||
1012 | cols += [fits.Column("NSIDE", "I", array=np.ravel(self.nside))] |
||
1013 | return cols |
||
1014 | |||
1015 | @staticmethod |
||
1016 | def get_index_list(nside, nest, region): |
||
1017 | """Get list of pixels indices for all the pixels in a region. |
||
1018 | |||
1019 | Parameters |
||
1020 | ---------- |
||
1021 | nside : int |
||
1022 | HEALPIX nside parameter |
||
1023 | nest : bool |
||
1024 | True for 'NESTED', False = 'RING' |
||
1025 | region : str |
||
1026 | HEALPIX region string |
||
1027 | |||
1028 | Returns |
||
1029 | ------- |
||
1030 | ilist : `~numpy.ndarray` |
||
1031 | List of pixel indices. |
||
1032 | """ |
||
1033 | import healpy as hp |
||
1034 | |||
1035 | # TODO: this should return something more friendly than a tuple |
||
1036 | # e.g. a namedtuple or a dict |
||
1037 | tokens = parse_hpxregion(region) |
||
1038 | |||
1039 | reg_type = tokens[0] |
||
1040 | if reg_type == "DISK": |
||
1041 | lon, lat = float(tokens[1]), float(tokens[2]) |
||
1042 | radius = np.radians(float(tokens[3])) |
||
1043 | vec = coords_to_vec(lon, lat)[0] |
||
1044 | ilist = hp.query_disc(nside, vec, radius, inclusive=False, nest=nest) |
||
1045 | elif reg_type == "DISK_INC": |
||
1046 | lon, lat = float(tokens[1]), float(tokens[2]) |
||
1047 | radius = np.radians(float(tokens[3])) |
||
1048 | vec = coords_to_vec(lon, lat)[0] |
||
1049 | fact = int(tokens[4]) |
||
1050 | ilist = hp.query_disc( |
||
1051 | nside, vec, radius, inclusive=True, nest=nest, fact=fact |
||
1052 | ) |
||
1053 | elif reg_type == "HPX_PIXEL": |
||
1054 | nside_pix = int(tokens[2]) |
||
1055 | if tokens[1] == "NESTED": |
||
1056 | ipix_ring = hp.nest2ring(nside_pix, int(tokens[3])) |
||
1057 | elif tokens[1] == "RING": |
||
1058 | ipix_ring = int(tokens[3]) |
||
1059 | else: |
||
1060 | raise ValueError(f"Invalid ordering scheme: {tokens[1]!r}") |
||
1061 | ilist = match_hpx_pix(nside, nest, nside_pix, ipix_ring) |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
Loading history...
|
|||
1062 | else: |
||
1063 | raise ValueError(f"Invalid region type: {reg_type!r}") |
||
1064 | |||
1065 | return ilist |
||
1066 | |||
1067 | @property |
||
1068 | def width(self): |
||
1069 | """Width of the map""" |
||
1070 | # TODO: simplify |
||
1071 | import healpy as hp |
||
1072 | |||
1073 | if self.is_allsky: |
||
1074 | width = 180. |
||
1075 | elif self.region == "explicit": |
||
1076 | idx = unravel_hpx_index(self._ipix, self.npix_max) |
||
1077 | nside = self._get_nside(idx) |
||
1078 | ang = hp.pix2ang(nside, idx[0], nest=self.nest, lonlat=True) |
||
1079 | dirs = SkyCoord(ang[0], ang[1], unit="deg", frame=self.frame) |
||
1080 | width = np.max(dirs.separation(self.center_skydir)) |
||
1081 | else: |
||
1082 | tokens = parse_hpxregion(self.region) |
||
1083 | if tokens[0] in {"DISK", "DISK_INC"}: |
||
1084 | width = float(tokens[3]) |
||
1085 | elif tokens[0] == "HPX_PIXEL": |
||
1086 | pix_size = get_pix_size_from_nside(int(tokens[2])) |
||
1087 | width = 2.0 * pix_size |
||
1088 | |||
1089 | return u.Quantity(width, "deg") |
||
1090 | |||
1091 | def _get_nside(self, idx): |
||
1092 | if self.nside.size > 1: |
||
1093 | return self.nside[tuple(idx[1:])] |
||
1094 | else: |
||
1095 | return self.nside |
||
1096 | |||
1097 | def to_wcs_geom(self, proj="AIT", oversample=2, width_pix=None): |
||
1098 | """Make a WCS projection appropriate for this HPX pixelization. |
||
1099 | |||
1100 | Parameters |
||
1101 | ---------- |
||
1102 | proj : str |
||
1103 | Projection type of WCS geometry. |
||
1104 | oversample : float |
||
1105 | Oversampling factor for WCS map. This will be the |
||
1106 | approximate ratio of the width of a HPX pixel to a WCS |
||
1107 | pixel. If this parameter is None then the width will be |
||
1108 | set from ``width_pix``. |
||
1109 | width_pix : int |
||
1110 | Width of the WCS geometry in pixels. The pixel size will |
||
1111 | be set to the number of pixels satisfying ``oversample`` |
||
1112 | or ``width_pix`` whichever is smaller. If this parameter |
||
1113 | is None then the width will be set from ``oversample``. |
||
1114 | |||
1115 | Returns |
||
1116 | ------- |
||
1117 | wcs : `~gammapy.maps.WcsGeom` |
||
1118 | WCS geometry |
||
1119 | """ |
||
1120 | from gammapy.maps import WcsGeom |
||
1121 | |||
1122 | pix_size = get_pix_size_from_nside(self.nside) |
||
1123 | binsz = np.min(pix_size) / oversample |
||
1124 | width = 2.0 * self.width.to_value("deg") + np.max(pix_size) |
||
1125 | |||
1126 | if width_pix is not None and int(width / binsz) > width_pix: |
||
1127 | binsz = width / width_pix |
||
1128 | |||
1129 | if width > 90.0: |
||
1130 | width = min(360.0, width), min(180.0, width) |
||
1131 | |||
1132 | axes = copy.deepcopy(self.axes) |
||
1133 | |||
1134 | return WcsGeom.create( |
||
1135 | width=width, |
||
1136 | binsz=binsz, |
||
1137 | frame=self.frame, |
||
1138 | axes=axes, |
||
1139 | skydir=self.center_skydir, |
||
1140 | proj=proj, |
||
1141 | ) |
||
1142 | |||
1143 | def to_wcs_tiles(self, nside_tiles=4, margin="0 deg"): |
||
1144 | """Create WCS tiles geometries from HPX geometry with given nside. |
||
1145 | |||
1146 | The HEALPix geom is divide into superpixels defined by nside_tiles, |
||
1147 | which are then represented by a WCS geometry using a tangential |
||
1148 | projection. The number of WCS tiles is given by the number of pixels |
||
1149 | for the given nside_tiles. |
||
1150 | |||
1151 | Parameters |
||
1152 | ---------- |
||
1153 | nside_tiles : int |
||
1154 | Nside for super pixel tiles. Usually nsi |
||
1155 | margin : Angle |
||
1156 | Width margin of the wcs tile |
||
1157 | |||
1158 | Return |
||
1159 | ------ |
||
1160 | wcs_tiles : list |
||
1161 | List of WCS tile geoms. |
||
1162 | """ |
||
1163 | import healpy as hp |
||
1164 | from gammapy.maps import WcsGeom |
||
1165 | |||
1166 | margin = u.Quantity(margin) |
||
1167 | |||
1168 | if nside_tiles >= self.nside: |
||
1169 | raise ValueError(f"nside_tiles must be < {self.nside}") |
||
1170 | |||
1171 | if not self.is_allsky: |
||
1172 | raise ValueError("to_wcs_tiles() is only supported for all sky geoms") |
||
1173 | |||
1174 | binsz = np.degrees(hp.nside2resol(self.nside)) * u.deg |
||
1175 | |||
1176 | hpx = self.to_image().to_nside(nside=nside_tiles) |
||
1177 | wcs_tiles = [] |
||
1178 | |||
1179 | for pix in range(int(hpx.npix)): |
||
1180 | skydir = hpx.pix_to_coord([pix]) |
||
1181 | vtx = hp.boundaries( |
||
1182 | nside=hpx.nside, pix=pix, nest=hpx.nest, step=1 |
||
1183 | ) |
||
1184 | |||
1185 | lon, lat = hp.vec2ang(vtx.T, lonlat=True) |
||
1186 | boundaries = SkyCoord(lon * u.deg, lat * u.deg, frame=hpx.frame) |
||
1187 | |||
1188 | # Compute maximum separation between all pairs of boundaries and take it |
||
1189 | # as width |
||
1190 | width = boundaries.separation(boundaries[:, np.newaxis]).max() |
||
1191 | |||
1192 | wcs_tile_geom = WcsGeom.create( |
||
1193 | skydir=(float(skydir[0]), float(skydir[1])), |
||
1194 | width=width + margin, |
||
1195 | binsz=binsz, |
||
1196 | frame=hpx.frame, |
||
1197 | proj="TAN", |
||
1198 | axes=self.axes |
||
1199 | ) |
||
1200 | wcs_tiles.append(wcs_tile_geom) |
||
1201 | |||
1202 | return wcs_tiles |
||
1203 | |||
1204 | def get_idx(self, idx=None, local=False, flat=False, sparse=False, mode="center", axis_name=None): |
||
1205 | # TODO: simplify this!!! |
||
1206 | if idx is not None and np.any(np.array(idx) >= np.array(self.shape_axes)): |
||
1207 | raise ValueError(f"Image index out of range: {idx!r}") |
||
1208 | |||
1209 | # Regular all- and partial-sky maps |
||
1210 | if self.is_regular: |
||
1211 | pix = [np.arange(np.max(self._npix))] |
||
1212 | if idx is None: |
||
1213 | for ax in self.axes: |
||
1214 | if mode == "edges" and ax.name == axis_name: |
||
1215 | pix += [np.arange(-0.5, ax.nbin, dtype=float)] |
||
1216 | else: |
||
1217 | pix += [np.arange(ax.nbin, dtype=int)] |
||
1218 | else: |
||
1219 | pix += [t for t in idx] |
||
1220 | |||
1221 | pix = np.meshgrid(*pix[::-1], indexing="ij", sparse=sparse)[::-1] |
||
1222 | pix = self.local_to_global(pix) |
||
1223 | |||
1224 | # Non-regular all-sky |
||
1225 | elif self.is_allsky and not self.is_regular: |
||
1226 | |||
1227 | shape = (np.max(self.npix),) |
||
1228 | if idx is None: |
||
1229 | shape = shape + self.shape_axes |
||
1230 | else: |
||
1231 | shape = shape + (1,) * len(self.axes) |
||
1232 | pix = [np.full(shape, -1, dtype=int) for i in range(1 + len(self.axes))] |
||
1233 | for idx_img in np.ndindex(self.shape_axes): |
||
1234 | |||
1235 | if idx is not None and idx_img != idx: |
||
1236 | continue |
||
1237 | |||
1238 | npix = self._npix[idx_img] |
||
1239 | if idx is None: |
||
1240 | s_img = (slice(0, npix),) + idx_img |
||
1241 | else: |
||
1242 | s_img = (slice(0, npix),) + (0,) * len(self.axes) |
||
1243 | |||
1244 | pix[0][s_img] = np.arange(self._npix[idx_img]) |
||
1245 | for j in range(len(self.axes)): |
||
1246 | pix[j + 1][s_img] = idx_img[j] |
||
1247 | pix = [p.T for p in pix] |
||
1248 | |||
1249 | # Explicit pixel indices |
||
1250 | else: |
||
1251 | |||
1252 | if idx is not None: |
||
1253 | npix_sum = np.concatenate(([0], np.cumsum(self._npix))) |
||
1254 | idx_ravel = np.ravel_multi_index(idx, self.shape_axes) |
||
1255 | s = slice(npix_sum[idx_ravel], npix_sum[idx_ravel + 1]) |
||
1256 | else: |
||
1257 | s = slice(None) |
||
1258 | pix_flat = unravel_hpx_index(self._ipix[s], self.npix_max) |
||
1259 | |||
1260 | shape = (np.max(self.npix),) |
||
1261 | if idx is None: |
||
1262 | shape = shape + self.shape_axes |
||
1263 | else: |
||
1264 | shape = shape + (1,) * len(self.axes) |
||
1265 | pix = [np.full(shape, -1, dtype=int) for _ in range(1 + len(self.axes))] |
||
1266 | |||
1267 | for idx_img in np.ndindex(self.shape_axes): |
||
1268 | |||
1269 | if idx is not None and idx_img != idx: |
||
1270 | continue |
||
1271 | |||
1272 | npix = int(self._npix[idx_img]) |
||
1273 | if idx is None: |
||
1274 | s_img = (slice(0, npix),) + idx_img |
||
1275 | else: |
||
1276 | s_img = (slice(0, npix),) + (0,) * len(self.axes) |
||
1277 | |||
1278 | if self.axes: |
||
1279 | m = np.all( |
||
1280 | np.stack([pix_flat[i + 1] == t for i, t in enumerate(idx_img)]), |
||
1281 | axis=0, |
||
1282 | ) |
||
1283 | pix[0][s_img] = pix_flat[0][m] |
||
1284 | else: |
||
1285 | pix[0][s_img] = pix_flat[0] |
||
1286 | |||
1287 | for j in range(len(self.axes)): |
||
1288 | pix[j + 1][s_img] = idx_img[j] |
||
1289 | |||
1290 | pix = [p.T for p in pix] |
||
1291 | |||
1292 | if local: |
||
1293 | pix = self.global_to_local(pix) |
||
1294 | |||
1295 | if flat: |
||
1296 | pix = tuple([p[p != INVALID_INDEX.int] for p in pix]) |
||
1297 | |||
1298 | return pix |
||
1299 | |||
1300 | def region_mask(self, regions): |
||
1301 | """Create a mask from a given list of regions |
||
1302 | |||
1303 | The mask is filled such that a pixel inside the region is filled with |
||
1304 | "True". To invert the mask, e.g. to create a mask with exclusion regions |
||
1305 | the tilde (~) operator can be used (see example below). |
||
1306 | |||
1307 | Parameters |
||
1308 | ---------- |
||
1309 | regions : str, `~regions.Region` or list of `~regions.Region` |
||
1310 | Region or list of regions (pixel or sky regions accepted). |
||
1311 | A region can be defined as a string ind DS9 format as well. |
||
1312 | See http://ds9.si.edu/doc/ref/region.html for details. |
||
1313 | |||
1314 | Returns |
||
1315 | ------- |
||
1316 | mask_map : `~gammapy.maps.WcsNDMap` of boolean type |
||
1317 | Boolean region mask |
||
1318 | |||
1319 | """ |
||
1320 | from gammapy.maps import Map, RegionGeom |
||
1321 | |||
1322 | if not self.is_regular: |
||
1323 | raise ValueError("Multi-resolution maps not supported yet") |
||
1324 | |||
1325 | # TODO: use spatial coordinates only... |
||
1326 | geom = RegionGeom.from_regions(regions) |
||
1327 | coords = self.get_coord() |
||
1328 | mask = geom.contains(coords) |
||
1329 | return Map.from_geom(self, data=mask) |
||
1330 | |||
1331 | def get_coord(self, idx=None, flat=False, sparse=False, mode="center", axis_name=None): |
||
1332 | if mode == "edges" and axis_name is None: |
||
1333 | raise ValueError("Mode 'edges' requires axis name to be defined") |
||
1334 | |||
1335 | pix = self.get_idx(idx=idx, flat=flat, sparse=sparse, mode=mode, axis_name=axis_name) |
||
1336 | data = self.pix_to_coord(pix) |
||
1337 | |||
1338 | coords = MapCoord.create( |
||
1339 | data=data, frame=self.frame, axis_names=self.axes.names |
||
1340 | ) |
||
1341 | |||
1342 | return coords |
||
1343 | |||
1344 | def contains(self, coords): |
||
1345 | idx = self.coord_to_idx(coords) |
||
1346 | return np.all(np.stack([t != INVALID_INDEX.int for t in idx]), axis=0) |
||
1347 | |||
1348 | def solid_angle(self): |
||
1349 | """Solid angle array (`~astropy.units.Quantity` in ``sr``). |
||
1350 | |||
1351 | The array has the same dimensionality as ``map.nside`` |
||
1352 | since all pixels have the same solid angle. |
||
1353 | """ |
||
1354 | import healpy as hp |
||
1355 | |||
1356 | return Quantity(hp.nside2pixarea(self.nside), "sr") |
||
1357 | |||
1358 | def __repr__(self): |
||
1359 | lon, lat = self.center_skydir.data.lon.deg, self.center_skydir.data.lat.deg |
||
1360 | return ( |
||
1361 | f"{self.__class__.__name__}\n\n" |
||
1362 | f"\taxes : {self.axes_names}\n" |
||
1363 | f"\tshape : {self.data_shape[::-1]}\n" |
||
1364 | f"\tndim : {self.ndim}\n" |
||
1365 | f"\tnside : {self.nside[0]}\n" |
||
1366 | f"\tnested : {self.nest}\n" |
||
1367 | f"\tframe : {self.frame}\n" |
||
1368 | f"\tprojection : {self.projection}\n" |
||
1369 | f"\tcenter : {lon:.1f} deg, {lat:.1f} deg\n" |
||
1370 | ) |
||
1371 | |||
1372 | def __eq__(self, other): |
||
1373 | if not isinstance(other, self.__class__): |
||
1374 | return NotImplemented |
||
1375 | |||
1376 | if self.is_allsky and other.is_allsky is False: |
||
1377 | return NotImplemented |
||
1378 | |||
1379 | # check overall shape and axes compatibility |
||
1380 | if self.data_shape != other.data_shape: |
||
1381 | return False |
||
1382 | |||
1383 | for axis, otheraxis in zip(self.axes, other.axes): |
||
1384 | if axis != otheraxis: |
||
1385 | return False |
||
1386 | |||
1387 | return ( |
||
1388 | self.nside == other.nside |
||
1389 | and self.frame == other.frame |
||
1390 | and self.order == other.order |
||
1391 | and self.nest == other.nest |
||
1392 | ) |
||
1393 | |||
1394 | def __ne__(self, other): |
||
1395 | return not self.__eq__(other) |
||
1396 |