gammapy.maps.hpx.geom   F
last analyzed

Complexity

Total Complexity 201

Size/Duplication

Total Lines 1427
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 754
dl 0
loc 1427
rs 1.846
c 0
b 0
f 0
wmc 201

62 Methods

Rating   Name   Duplication   Size   Complexity  
A HpxGeom.width() 0 23 5
A HpxGeom._get_nside() 0 5 2
B HpxGeom.to_header() 0 38 7
A HpxGeom.to_wcs_geom() 0 44 4
B HpxGeom.get_index_list() 0 51 6
D HpxGeom.from_header() 0 74 12
A HpxGeom._make_bands_cols() 0 5 2
A HpxGeom.from_hdu() 0 26 2
A HpxGeom.projection() 0 4 1
A HpxGeom.crop() 0 25 3
A HpxGeom.to_cube() 0 8 1
A HpxGeom.to_binsz() 0 28 2
F HpxGeom.get_idx() 0 103 26
A HpxGeom.to_swapped() 0 15 1
A HpxGeom.local_to_global() 0 21 3
A HpxGeom.solid_angle() 0 9 1
A HpxGeom.data_shape_axes() 0 4 1
A HpxGeom.axes_names() 0 4 1
A HpxGeom.get_coord() 0 16 3
A HpxGeom.separation() 0 15 1
B HpxGeom.is_aligned() 0 25 6
A HpxGeom.to_image() 0 3 1
A HpxGeom.downsample() 0 25 4
A HpxGeom.ipix() 0 4 1
A HpxGeom.ordering() 0 4 2
A HpxGeom.__repr__() 0 4 1
A HpxGeom.to_nside() 0 19 2
A HpxGeom.region_mask() 0 30 2
A HpxGeom.is_regular() 0 11 3
A HpxGeom.npix_max() 0 5 1
B HpxGeom.interp_weights() 0 53 5
B HpxGeom.center_skydir() 0 39 7
B HpxGeom._create_lookup() 0 44 6
A HpxGeom._get_neighbors() 0 8 1
A HpxGeom._pad_spatial() 0 27 3
A HpxGeom.pix_to_idx() 0 22 5
A HpxGeom.nside() 0 4 1
A HpxGeom.data_shape() 0 5 1
A HpxGeom.__ne__() 0 2 1
A HpxGeom.__init__() 0 21 3
B HpxGeom.pix_to_coord() 0 32 6
B HpxGeom.coord_to_pix() 0 31 5
A HpxGeom.to_wcs_tiles() 0 58 4
A HpxGeom.region() 0 4 1
A HpxGeom.cutout() 0 26 2
C HpxGeom.create() 0 74 10
A HpxGeom.contains() 0 3 1
A HpxGeom.center_coord() 0 5 1
A HpxGeom.pixel_scales() 0 9 1
A HpxGeom.is_allclose() 0 36 5
A HpxGeom.npix() 0 8 1
A HpxGeom.nest() 0 4 1
C HpxGeom.global_to_local() 0 48 10
A HpxGeom.frame() 0 3 1
A HpxGeom.center_pix() 0 4 1
A HpxGeom.__eq__() 0 5 2
A HpxGeom.order() 0 7 1
A HpxGeom.ndim() 0 4 1
A HpxGeom.is_allsky() 0 4 1
A HpxGeom.axes() 0 4 1
A HpxGeom.upsample() 0 27 4
A HpxGeom.shape_axes() 0 4 1

How to fix   Complexity   

Complexity

Complex classes like gammapy.maps.hpx.geom often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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 numpy as np
5
from astropy import units as u
6
from astropy.coordinates import SkyCoord
7
from astropy.io import fits
8
from astropy.units import Quantity
9
from gammapy.utils.array import is_power2
10
from ..axes import MapAxes
11
from ..coord import MapCoord, skycoord_to_lonlat
12
from ..geom import Geom, pix_tuple_to_idx
13
from ..utils import INVALID_INDEX, coordsys_to_frame, frame_to_coordsys
14
from .io import HPX_FITS_CONVENTIONS, HpxConv
15
from .utils import (
16
    coords_to_vec,
17
    get_nside_from_pix_size,
18
    get_pix_size_from_nside,
19
    get_subpixels,
20
    get_superpixels,
21
    match_hpx_pix,
22
    nside_to_order,
23
    parse_hpxregion,
24
    ravel_hpx_index,
25
    unravel_hpx_index,
26
)
27
28
# Not sure if we should expose this in the docs or not:
29
# HPX_FITS_CONVENTIONS, HpxConv
30
__all__ = ["HpxGeom"]
31
32
33
class HpxGeom(Geom):
34
    """Geometry class for HEALPIX maps.
35
36
    This class performs mapping between partial-sky indices (pixel
37
    number within a HEALPIX region) and all-sky indices (pixel number
38
    within an all-sky HEALPIX map).  Multi-band HEALPIX geometries use
39
    a global indexing scheme that assigns a unique pixel number based
40
    on the all-sky index and band index.  In the single-band case the
41
    global index is the same as the HEALPIX index.
42
43
    By default the constructor will return an all-sky map.
44
    Partial-sky maps can be defined with the ``region`` argument.
45
46
    Parameters
47
    ----------
48
    nside : `~numpy.ndarray`
49
        HEALPIX nside parameter, the total number of pixels is
50
        12*nside*nside.  For multi-dimensional maps one can pass
51
        either a single nside value or a vector of nside values
52
        defining the pixel size for each image plane.  If nside is not
53
        a scalar then its dimensionality should match that of the
54
        non-spatial axes. If nest is True, nside must be a power of 2,
55
        less than 2**30.
56
    nest : bool
57
        True -> 'NESTED', False -> 'RING' indexing scheme
58
    frame : str
59
        Coordinate system, "icrs" | "galactic"
60
    region : str or tuple
61
        Spatial geometry for partial-sky maps.  If none the map will
62
        encompass the whole sky.  String input will be parsed
63
        according to HPX_REG header keyword conventions.  Tuple
64
        input can be used to define an explicit list of pixels
65
        encompassed by the geometry.
66
    axes : list
67
        Axes for non-spatial dimensions.
68
    """
69
70
    is_hpx = True
71
    is_region = False
72
73
    def __init__(self, nside, nest=True, frame="icrs", region=None, axes=None):
74
        from healpy.pixelfunc import check_nside
75
76
        check_nside(nside, nest=nest)
77
78
        self._nside = np.array(nside, ndmin=1)
79
        self._axes = MapAxes.from_default(axes, n_spatial_axes=1)
80
81
        if self.nside.size > 1 and self.nside.shape != self.shape_axes:
82
            raise ValueError(
83
                "Wrong dimensionality for nside. nside must "
84
                "be a scalar or have a dimensionality consistent "
85
                "with the axes argument."
86
            )
87
88
        self._frame = frame
89
        self._nest = nest
90
        self._ipix = None
91
        self._region = region
92
        self._create_lookup(region)
93
        self._npix = self._npix * np.ones(self.shape_axes, dtype=int)
94
95
    def _create_lookup(self, region):
96
        """Create local-to-global pixel lookup table."""
97
        if isinstance(region, str):
98
            ipix = [
99
                self.get_index_list(nside, self._nest, region)
100
                for nside in self._nside.flat
101
            ]
102
103
            self._ipix = [
104
                ravel_hpx_index((p, i * np.ones_like(p)), np.ravel(self.npix_max))
105
                for i, p in enumerate(ipix)
106
            ]
107
            self._region = region
108
            self._indxschm = "EXPLICIT"
109
            self._npix = np.array([len(t) for t in self._ipix])
110
            if self.nside.ndim > 1:
111
                self._npix = self._npix.reshape(self.nside.shape)
112
            self._ipix = np.concatenate(self._ipix)
113
114
        elif isinstance(region, tuple):
115
            region = [np.asarray(t) for t in region]
116
            m = np.any(np.stack([t >= 0 for t in region]), axis=0)
117
            region = [t[m] for t in region]
118
119
            self._ipix = ravel_hpx_index(region, self.npix_max)
120
            self._ipix = np.unique(self._ipix)
121
            region = unravel_hpx_index(self._ipix, self.npix_max)
122
            self._region = "explicit"
123
            self._indxschm = "EXPLICIT"
124
            if len(region) == 1:
125
                self._npix = np.array([len(region[0])])
126
            else:
127
                self._npix = np.zeros(self.shape_axes, dtype=int)
128
                idx = np.ravel_multi_index(region[1:], self.shape_axes)
129
                cnt = np.unique(idx, return_counts=True)
130
                self._npix.flat[cnt[0]] = cnt[1]
131
132
        elif region is None:
133
            self._region = None
134
            self._indxschm = "IMPLICIT"
135
            self._npix = self.npix_max
136
137
        else:
138
            raise ValueError(f"Invalid region string: {region!r}")
139
140
    def local_to_global(self, idx_local):
141
        """Compute a local index (partial-sky) from a global (all-sky) index.
142
143
        Returns
144
        -------
145
        idx_global : tuple
146
            A tuple of pixel index vectors with global HEALPIX pixel indices
147
        """
148
        if self._ipix is None:
149
            return idx_local
150
151
        if self.nside.size > 1:
152
            idx = ravel_hpx_index(idx_local, self._npix)
153
        else:
154
            idx_tmp = tuple(
155
                [idx_local[0]] + [np.zeros(t.shape, dtype=int) for t in idx_local[1:]]
156
            )
157
            idx = ravel_hpx_index(idx_tmp, self._npix)
158
159
        idx_global = unravel_hpx_index(self._ipix[idx], self.npix_max)
160
        return idx_global[:1] + tuple(idx_local[1:])
161
162
    def global_to_local(self, idx_global, ravel=False):
163
        """Compute global (all-sky) index from a local (partial-sky) index.
164
165
        Parameters
166
        ----------
167
        idx_global : tuple
168
            A tuple of pixel indices with global HEALPix pixel indices.
169
        ravel : bool
170
            Return a raveled index.
171
172
        Returns
173
        -------
174
        idx_local : tuple
175
            A tuple of pixel indices with local HEALPIX pixel indices.
176
        """
177
        if (
178
            isinstance(idx_global, int)
179
            or (isinstance(idx_global, tuple) and isinstance(idx_global[0], int))
180
            or isinstance(idx_global, np.ndarray)
181
        ):
182
            idx_global = unravel_hpx_index(np.array(idx_global, ndmin=1), self.npix_max)
183
184
        if self.nside.size == 1:
185
            idx = np.array(idx_global[0], ndmin=1)
186
        else:
187
            idx = ravel_hpx_index(idx_global, self.npix_max)
188
189
        if self._ipix is not None:
190
            retval = np.full(idx.size, -1, "i")
191
            m = np.isin(idx.flat, self._ipix)
192
            retval[m] = np.searchsorted(self._ipix, idx.flat[m])
193
            retval = retval.reshape(idx.shape)
194
        else:
195
            retval = idx
196
197
        if self.nside.size == 1:
198
            idx_local = tuple([retval] + list(idx_global[1:]))
199
        else:
200
            idx_local = unravel_hpx_index(retval, self._npix)
201
202
        m = np.any(np.stack([t == INVALID_INDEX.int for t in idx_local]), axis=0)
203
        for i, t in enumerate(idx_local):
204
            idx_local[i][m] = INVALID_INDEX.int
205
206
        if not ravel:
207
            return idx_local
208
        else:
209
            return ravel_hpx_index(idx_local, self.npix)
210
211
    def cutout(self, position, width, **kwargs):
212
        """Create a cutout around a given position.
213
214
        Parameters
215
        ----------
216
        position : `~astropy.coordinates.SkyCoord`
217
            Center position of the cutout region.
218
        width : `~astropy.coordinates.Angle` or `~astropy.units.Quantity`
219
            Diameter of the circular cutout region.
220
221
        Returns
222
        -------
223
        cutout : `~gammapy.maps.WcsNDMap`
224
            Cutout map
225
        """
226
        if not self.is_regular:
227
            raise ValueError("Can only do a cutout from a regular map.")
228
229
        width = u.Quantity(width, "deg").value
230
        return self.create(
231
            nside=self.nside,
232
            nest=self.nest,
233
            width=width,
234
            skydir=position,
235
            frame=self.frame,
236
            axes=self.axes,
237
        )
238
239
    def coord_to_pix(self, coords):
240
        import healpy as hp
241
242
        coords = MapCoord.create(
243
            coords, frame=self.frame, axis_names=self.axes.names
244
        ).broadcasted
245
        theta, phi = coords.theta, coords.phi
246
247
        if self.axes:
248
            idxs = self.axes.coord_to_idx(coords, clip=True)
249
            bins = self.axes.coord_to_pix(coords)
250
251
            # FIXME: Figure out how to handle coordinates out of
252
            # bounds of non-spatial dimensions
253
            if self.nside.size > 1:
254
                nside = self.nside[tuple(idxs)]
255
            else:
256
                nside = self.nside
257
258
            m = ~np.isfinite(theta)
259
            theta[m] = 0.0
260
            phi[m] = 0.0
261
            pix = hp.ang2pix(nside, theta, phi, nest=self.nest)
262
            pix = tuple([pix]) + bins
263
            if np.any(m):
264
                for p in pix:
265
                    p[m] = INVALID_INDEX.int
266
        else:
267
            pix = (hp.ang2pix(self.nside, theta, phi, nest=self.nest),)
268
269
        return pix
270
271
    def pix_to_coord(self, pix):
272
        import healpy as hp
273
274
        if self.axes:
275
            bins = []
276
            vals = []
277
            for i, ax in enumerate(self.axes):
278
                bins += [pix[1 + i]]
279
                vals += [ax.pix_to_coord(pix[1 + i])]
280
281
            idxs = pix_tuple_to_idx(bins)
282
283
            if self.nside.size > 1:
284
                nside = self.nside[idxs]
285
            else:
286
                nside = self.nside
287
288
            ipix = np.round(pix[0]).astype(int)
289
            m = ipix == INVALID_INDEX.int
290
            ipix[m] = 0
291
            theta, phi = hp.pix2ang(nside, ipix, nest=self.nest)
292
            coords = [np.degrees(phi), np.degrees(np.pi / 2.0 - theta)]
293
            coords = tuple(coords + vals)
294
            if np.any(m):
295
                for c in coords:
296
                    c[m] = INVALID_INDEX.float
297
        else:
298
            ipix = np.round(pix[0]).astype(int)
299
            theta, phi = hp.pix2ang(self.nside, ipix, nest=self.nest)
300
            coords = (np.degrees(phi), np.degrees(np.pi / 2.0 - theta))
301
302
        return coords
303
304
    def pix_to_idx(self, pix, clip=False):
305
        # FIXME: Look for better method to clip HPX indices
306
        # TODO: copy idx to avoid modifying input pix?
307
        # pix_tuple_to_idx seems to always make a copy!?
308
        idx = pix_tuple_to_idx(pix)
309
        idx_local = self.global_to_local(idx)
310
        for i, _ in enumerate(idx):
311
312
            if clip:
313
                if i > 0:
314
                    np.clip(idx[i], 0, self.axes[i - 1].nbin - 1, out=idx[i])
315
                else:
316
                    np.clip(idx[i], 0, None, out=idx[i])
317
            else:
318
                if i > 0:
319
                    mask = (idx[i] < 0) | (idx[i] >= self.axes[i - 1].nbin)
320
                    np.putmask(idx[i], mask, -1)
321
                else:
322
                    mask = (idx_local[i] < 0) | (idx[i] < 0)
323
                    np.putmask(idx[i], mask, -1)
324
325
        return tuple(idx)
326
327
    @property
328
    def axes(self):
329
        """List of non-spatial axes."""
330
        return self._axes
331
332
    @property
333
    def axes_names(self):
334
        """All axes names"""
335
        return ["skycoord"] + self.axes.names
336
337
    @property
338
    def shape_axes(self):
339
        """Shape of non-spatial axes."""
340
        return self.axes.shape
341
342
    @property
343
    def data_shape(self):
344
        """Shape of the Numpy data array matching this geometry."""
345
        npix_shape = tuple([np.max(self.npix)])
346
        return (npix_shape + self.axes.shape)[::-1]
347
348
    @property
349
    def data_shape_axes(self):
350
        """Shape of data of the non-spatial axes and unit spatial axes."""
351
        return self.axes.shape[::-1] + (1,)
352
353
    @property
354
    def ndim(self):
355
        """Number of dimensions (int)."""
356
        return len(self._axes) + 2
357
358
    @property
359
    def ordering(self):
360
        """HEALPix ordering ('NESTED' or 'RING')."""
361
        return "NESTED" if self.nest else "RING"
362
363
    @property
364
    def nside(self):
365
        """NSIDE in each band."""
366
        return self._nside
367
368
    @property
369
    def order(self):
370
        """ORDER in each band (``NSIDE = 2 ** ORDER``).
371
372
        Set to -1 for bands with NSIDE that is not a power of 2.
373
        """
374
        return nside_to_order(self.nside)
375
376
    @property
377
    def nest(self):
378
        """Is HEALPix order nested? (bool)."""
379
        return self._nest
380
381
    @property
382
    def npix(self):
383
        """Number of pixels in each band.
384
385
        For partial-sky geometries this can
386
        be less than the number of pixels for the band NSIDE.
387
        """
388
        return self._npix
389
390
    @property
391
    def npix_max(self):
392
        """Max. number of pixels"""
393
        maxpix = 12 * self.nside**2
394
        return maxpix * np.ones(self.shape_axes, dtype=int)
395
396
    @property
397
    def frame(self):
398
        return self._frame
399
400
    @property
401
    def projection(self):
402
        """Map projection."""
403
        return "HPX"
404
405
    @property
406
    def region(self):
407
        """Region string."""
408
        return self._region
409
410
    @property
411
    def is_allsky(self):
412
        """Flag for all-sky maps."""
413
        return self._region is None
414
415
    @property
416
    def is_regular(self):
417
        """Flag identifying whether this geometry is regular in non-spatial dimensions.
418
419
        False for multi-resolution or irregular geometries.
420
        If true all image planes have the same pixel geometry.
421
        """
422
        if self.nside.size > 1 or self.region == "explicit":
423
            return False
424
        else:
425
            return True
426
427
    @property
428
    def center_coord(self):
429
        """Map coordinates of the center of the geometry (tuple)."""
430
        lon, lat, frame = skycoord_to_lonlat(self.center_skydir)
431
        return tuple([lon, lat]) + self.axes.center_coord
432
433
    @property
434
    def center_pix(self):
435
        """Pixel coordinates of the center of the geometry (tuple)."""
436
        return self.coord_to_pix(self.center_coord)
437
438
    @property
439
    def center_skydir(self):
440
        """Sky coordinate of the center of the geometry.
441
442
        Returns
443
        -------
444
        center : `~astropy.coordinates.SkyCoord`
445
            Center position
446
        """
447
        # TODO: simplify
448
        import healpy as hp
449
450
        if self.is_allsky:
451
            lon, lat = 0.0, 0.0
452
        elif self.region == "explicit":
453
            idx = unravel_hpx_index(self._ipix, self.npix_max)
454
            nside = self._get_nside(idx)
455
            vec = hp.pix2vec(nside, idx[0], nest=self.nest)
456
            vec = np.array([np.mean(t) for t in vec])
457
            lonlat = hp.vec2ang(vec, lonlat=True)
458
            lon, lat = lonlat[0], lonlat[1]
459
        else:
460
            tokens = parse_hpxregion(self.region)
461
            if tokens[0] in ["DISK", "DISK_INC"]:
462
                lon, lat = float(tokens[1]), float(tokens[2])
463
            elif tokens[0] == "HPX_PIXEL":
464
                nside_pix = int(tokens[2])
465
                ipix_pix = int(tokens[3])
466
                if tokens[1] == "NESTED":
467
                    nest_pix = True
468
                elif tokens[1] == "RING":
469
                    nest_pix = False
470
                else:
471
                    raise ValueError(f"Invalid ordering scheme: {tokens[1]!r}")
472
                theta, phi = hp.pix2ang(nside_pix, ipix_pix, nest_pix)
473
                lat = np.degrees((np.pi / 2) - theta)
474
                lon = np.degrees(phi)
475
476
        return SkyCoord(lon, lat, frame=self.frame, unit="deg")
0 ignored issues
show
introduced by
The variable lat does not seem to be defined for all execution paths.
Loading history...
introduced by
The variable lon does not seem to be defined for all execution paths.
Loading history...
477
478
    @property
479
    def pixel_scales(self):
480
        self.angle_ = """Pixel scale.
481
482
        Returns
483
        -------
484
        angle: `~astropy.coordinates.Angle`
485
        """
486
        return get_pix_size_from_nside(self.nside) * u.deg
487
488
    def interp_weights(self, coords, idxs=None):
489
        """Get interpolation weights for given coords
490
491
        Parameters
492
        ----------
493
        coords : `MapCoord` or dict
494
            Input coordinates
495
        idxs : `~numpy.ndarray`
496
            Indices for non-spatial axes.
497
498
        Returns
499
        -------
500
        weights : `~numpy.ndarray`
501
            Interpolation weights
502
        """
503
        import healpy as hp
504
505
        coords = MapCoord.create(coords, frame=self.frame).broadcasted
506
507
        if idxs is None:
508
            idxs = self.coord_to_idx(coords, clip=True)[1:]
509
510
        theta, phi = coords.theta, coords.phi
511
512
        m = ~np.isfinite(theta)
513
        theta[m] = 0
514
        phi[m] = 0
515
516
        if not self.is_regular:
517
            nside = self.nside[tuple(idxs)]
518
        else:
519
            nside = self.nside
520
521
        pix, wts = hp.get_interp_weights(nside, theta, phi, nest=self.nest)
522
        wts[:, m] = 0
523
        pix[:, m] = INVALID_INDEX.int
524
525
        if not self.is_regular:
526
            pix_local = [self.global_to_local([pix] + list(idxs))[0]]
527
        else:
528
            pix_local = [self.global_to_local(pix, ravel=True)]
529
530
        # If a pixel lies outside of the geometry set its index to the center pixel
531
        m = pix_local[0] == INVALID_INDEX.int
532
        if m.any():
533
            coords_ctr = [coords.lon, coords.lat]
534
            coords_ctr += [ax.pix_to_coord(t) for ax, t in zip(self.axes, idxs)]
535
            idx_ctr = self.coord_to_idx(coords_ctr)
536
            idx_ctr = self.global_to_local(idx_ctr)
537
            pix_local[0][m] = (idx_ctr[0] * np.ones(pix.shape, dtype=int))[m]
538
539
        pix_local += [np.broadcast_to(t, pix_local[0].shape) for t in idxs]
540
        return pix_local, wts
541
542
    @property
543
    def ipix(self):
544
        """HEALPIX pixel and band indices for every pixel in the map."""
545
        return self.get_idx()
546
547
    def is_aligned(self, other):
548
        """Check if HEALPIx geoms and extra axes are aligned.
549
550
        Parameters
551
        ----------
552
        other : `HpxGeom`
553
            Other geom.
554
555
        Returns
556
        -------
557
        aligned : bool
558
            Whether geometries are aligned
559
        """
560
        for axis, otheraxis in zip(self.axes, other.axes):
561
            if axis != otheraxis:
562
                return False
563
564
        if not self.nside == other.nside:
565
            return False
566
        elif not self.frame == other.frame:
567
            return False
568
        elif not self.nest == other.nest:
569
            return False
570
        else:
571
            return True
572
573
    def to_nside(self, nside):
574
        """Upgrade or downgrade the reoslution to a given nside
575
576
        Parameters
577
        ----------
578
        nside : int
579
            Nside
580
581
        Returns
582
        -------
583
        geom : `~HpxGeom`
584
            A HEALPix geometry object.
585
        """
586
        if not self.is_regular:
587
            raise ValueError("Upgrade and degrade only implemented for standard maps")
588
589
        axes = copy.deepcopy(self.axes)
590
        return self.__class__(
591
            nside=nside, nest=self.nest, frame=self.frame, region=self.region, axes=axes
592
        )
593
594
    def to_binsz(self, binsz):
595
        """Change pixel size of the geometry.
596
597
        Parameters
598
        ----------
599
        binsz : float or `~astropy.units.Quantity`
600
            New pixel size. A float is assumed to be in degree.
601
602
        Returns
603
        -------
604
        geom : `WcsGeom`
605
            Geometry with new pixel size.
606
        """
607
        binsz = u.Quantity(binsz, "deg").value
608
609
        if self.is_allsky:
610
            return self.create(
611
                binsz=binsz,
612
                frame=self.frame,
613
                axes=copy.deepcopy(self.axes),
614
            )
615
        else:
616
            return self.create(
617
                skydir=self.center_skydir,
618
                binsz=binsz,
619
                width=self.width.to_value("deg"),
620
                frame=self.frame,
621
                axes=copy.deepcopy(self.axes),
622
            )
623
624
    def separation(self, center):
625
        """Compute sky separation wrt a given center.
626
627
        Parameters
628
        ----------
629
        center : `~astropy.coordinates.SkyCoord`
630
            Center position
631
632
        Returns
633
        -------
634
        separation : `~astropy.coordinates.Angle`
635
            Separation angle array (1D)
636
        """
637
        coord = self.to_image().get_coord()
638
        return center.separation(coord.skycoord)
639
640
    def to_swapped(self):
641
        """Geometry copy with swapped ORDERING (NEST->RING or vice versa).
642
643
        Returns
644
        -------
645
        geom : `~HpxGeom`
646
            A HEALPix geometry object.
647
        """
648
        axes = copy.deepcopy(self.axes)
649
        return self.__class__(
650
            self.nside,
651
            not self.nest,
652
            frame=self.frame,
653
            region=self.region,
654
            axes=axes,
655
        )
656
657
    def to_image(self):
658
        return self.__class__(
659
            np.max(self.nside), self.nest, frame=self.frame, region=self.region
660
        )
661
662
    def to_cube(self, axes):
663
        axes = copy.deepcopy(self.axes) + axes
664
        return self.__class__(
665
            np.max(self.nside),
666
            self.nest,
667
            frame=self.frame,
668
            region=self.region,
669
            axes=axes,
670
        )
671
672
    def _get_neighbors(self, idx):
673
        import healpy as hp
674
675
        nside = self._get_nside(idx)
676
        idx_nb = (hp.get_all_neighbours(nside, idx[0], nest=self.nest),)
677
        idx_nb += tuple([t[None, ...] * np.ones_like(idx_nb[0]) for t in idx[1:]])
678
679
        return idx_nb
680
681
    def _pad_spatial(self, pad_width):
682
        if self.is_allsky:
683
            raise ValueError("Cannot pad an all-sky map.")
684
685
        idx = self.get_idx(flat=True)
686
        idx_r = ravel_hpx_index(idx, self.npix_max)
687
688
        # TODO: Pre-filter indices to find those close to the edge
689
        idx_nb = self._get_neighbors(idx)
690
        idx_nb = ravel_hpx_index(idx_nb, self.npix_max)
691
692
        for _ in range(pad_width):
693
            mask_edge = np.isin(idx_nb, idx_r, invert=True)
694
            idx_edge = idx_nb[mask_edge]
695
            idx_edge = np.unique(idx_edge)
696
            idx_r = np.sort(np.concatenate((idx_r, idx_edge)))
697
            idx_nb = unravel_hpx_index(idx_edge, self.npix_max)
698
            idx_nb = self._get_neighbors(idx_nb)
699
            idx_nb = ravel_hpx_index(idx_nb, self.npix_max)
700
701
        idx = unravel_hpx_index(idx_r, self.npix_max)
702
        return self.__class__(
703
            self.nside.copy(),
704
            self.nest,
705
            frame=self.frame,
706
            region=idx,
707
            axes=copy.deepcopy(self.axes),
708
        )
709
710
    def crop(self, crop_width):
711
        if self.is_allsky:
712
            raise ValueError("Cannot crop an all-sky map.")
713
714
        idx = self.get_idx(flat=True)
715
        idx_r = ravel_hpx_index(idx, self.npix_max)
716
717
        # TODO: Pre-filter indices to find those close to the edge
718
        idx_nb = self._get_neighbors(idx)
719
        idx_nb = ravel_hpx_index(idx_nb, self.npix_max)
720
721
        for _ in range(crop_width):
722
            # Mask of pixels that have at least one neighbor not
723
            # contained in the geometry
724
            mask_edge = np.any(np.isin(idx_nb, idx_r, invert=True), axis=0)
725
            idx_r = idx_r[~mask_edge]
726
            idx_nb = idx_nb[:, ~mask_edge]
727
728
        idx = unravel_hpx_index(idx_r, self.npix_max)
729
        return self.__class__(
730
            self.nside.copy(),
731
            self.nest,
732
            frame=self.frame,
733
            region=idx,
734
            axes=copy.deepcopy(self.axes),
735
        )
736
737
    def upsample(self, factor):
738
        if not is_power2(factor):
739
            raise ValueError("Upsample factor must be a power of 2.")
740
741
        if self.is_allsky:
742
            return self.__class__(
743
                self.nside * factor,
744
                self.nest,
745
                frame=self.frame,
746
                region=self.region,
747
                axes=copy.deepcopy(self.axes),
748
            )
749
750
        idx = list(self.get_idx(flat=True))
751
        nside = self._get_nside(idx)
752
753
        idx_new = get_subpixels(idx[0], nside, nside * factor, nest=self.nest)
754
        for i in range(1, len(idx)):
755
            idx[i] = idx[i][..., None] * np.ones(idx_new.shape, dtype=int)
756
757
        idx[0] = idx_new
758
        return self.__class__(
759
            self.nside * factor,
760
            self.nest,
761
            frame=self.frame,
762
            region=tuple(idx),
763
            axes=copy.deepcopy(self.axes),
764
        )
765
766
    def downsample(self, factor, axis_name=None):
767
        if not is_power2(factor):
768
            raise ValueError("Downsample factor must be a power of 2.")
769
        if axis_name is not None:
770
            raise ValueError("Currently the only valid axis name is None.")
771
772
        if self.is_allsky:
773
            return self.__class__(
774
                self.nside // factor,
775
                self.nest,
776
                frame=self.frame,
777
                region=self.region,
778
                axes=copy.deepcopy(self.axes),
779
            )
780
781
        idx = list(self.get_idx(flat=True))
782
        nside = self._get_nside(idx)
783
        idx_new = get_superpixels(idx[0], nside, nside // factor, nest=self.nest)
784
        idx[0] = idx_new
785
        return self.__class__(
786
            self.nside // factor,
787
            self.nest,
788
            frame=self.frame,
789
            region=tuple(idx),
790
            axes=copy.deepcopy(self.axes),
791
        )
792
793
    @classmethod
794
    def create(
795
        cls,
796
        nside=None,
797
        binsz=None,
798
        nest=True,
799
        frame="icrs",
800
        region=None,
801
        axes=None,
802
        skydir=None,
803
        width=None,
804
    ):
805
        """Create an HpxGeom object.
806
807
        Parameters
808
        ----------
809
        nside : int or `~numpy.ndarray`
810
            HEALPix NSIDE parameter.  This parameter sets the size of
811
            the spatial pixels in the map. If nest is True, nside must be a
812
            power of 2, less than 2**30.
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) # doctest: +SKIP
844
        >>> geom = HpxGeom.create(binsz=0.1, width=10.0) # doctest: +SKIP
845
        >>> geom = HpxGeom.create(nside=64, width=10.0, axes=[axis]) # doctest: +SKIP
846
        >>> geom = HpxGeom.create(nside=[32,64], width=10.0, axes=[axis]) # doctest: +SKIP
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)
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.0
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")
0 ignored issues
show
introduced by
The variable width does not seem to be defined for all execution paths.
Loading history...
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
        Returns
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(nside=hpx.nside, pix=pix, nest=hpx.nest, step=1)
1182
1183
            lon, lat = hp.vec2ang(vtx.T, lonlat=True)
1184
            boundaries = SkyCoord(lon * u.deg, lat * u.deg, frame=hpx.frame)
1185
1186
            # Compute maximum separation between all pairs of boundaries and take it
1187
            # as width
1188
            width = boundaries.separation(boundaries[:, np.newaxis]).max()
1189
1190
            wcs_tile_geom = WcsGeom.create(
1191
                skydir=(float(skydir[0]), float(skydir[1])),
1192
                width=width + margin,
1193
                binsz=binsz,
1194
                frame=hpx.frame,
1195
                proj="TAN",
1196
                axes=self.axes,
1197
            )
1198
            wcs_tiles.append(wcs_tile_geom)
1199
1200
        return wcs_tiles
1201
1202
    def get_idx(
1203
        self,
1204
        idx=None,
1205
        local=False,
1206
        flat=False,
1207
        sparse=False,
1208
        mode="center",
1209
        axis_name=None,
1210
    ):
1211
        # TODO: simplify this!!!
1212
        if idx is not None and np.any(np.array(idx) >= np.array(self.shape_axes)):
1213
            raise ValueError(f"Image index out of range: {idx!r}")
1214
1215
        # Regular all- and partial-sky maps
1216
        if self.is_regular:
1217
            pix = [np.arange(np.max(self._npix))]
1218
            if idx is None:
1219
                for ax in self.axes:
1220
                    if mode == "edges" and ax.name == axis_name:
1221
                        pix += [np.arange(-0.5, ax.nbin, dtype=float)]
1222
                    else:
1223
                        pix += [np.arange(ax.nbin, dtype=int)]
1224
            else:
1225
                pix += [t for t in idx]
1226
1227
            pix = np.meshgrid(*pix[::-1], indexing="ij", sparse=sparse)[::-1]
1228
            pix = self.local_to_global(pix)
1229
1230
        # Non-regular all-sky
1231
        elif self.is_allsky and not self.is_regular:
1232
1233
            shape = (np.max(self.npix),)
1234
            if idx is None:
1235
                shape = shape + self.shape_axes
1236
            else:
1237
                shape = shape + (1,) * len(self.axes)
1238
            pix = [np.full(shape, -1, dtype=int) for i in range(1 + len(self.axes))]
1239
            for idx_img in np.ndindex(self.shape_axes):
1240
1241
                if idx is not None and idx_img != idx:
1242
                    continue
1243
1244
                npix = self._npix[idx_img]
1245
                if idx is None:
1246
                    s_img = (slice(0, npix),) + idx_img
1247
                else:
1248
                    s_img = (slice(0, npix),) + (0,) * len(self.axes)
1249
1250
                pix[0][s_img] = np.arange(self._npix[idx_img])
1251
                for j in range(len(self.axes)):
1252
                    pix[j + 1][s_img] = idx_img[j]
1253
            pix = [p.T for p in pix]
1254
1255
        # Explicit pixel indices
1256
        else:
1257
1258
            if idx is not None:
1259
                npix_sum = np.concatenate(([0], np.cumsum(self._npix)))
1260
                idx_ravel = np.ravel_multi_index(idx, self.shape_axes)
1261
                s = slice(npix_sum[idx_ravel], npix_sum[idx_ravel + 1])
1262
            else:
1263
                s = slice(None)
1264
            pix_flat = unravel_hpx_index(self._ipix[s], self.npix_max)
1265
1266
            shape = (np.max(self.npix),)
1267
            if idx is None:
1268
                shape = shape + self.shape_axes
1269
            else:
1270
                shape = shape + (1,) * len(self.axes)
1271
            pix = [np.full(shape, -1, dtype=int) for _ in range(1 + len(self.axes))]
1272
1273
            for idx_img in np.ndindex(self.shape_axes):
1274
1275
                if idx is not None and idx_img != idx:
1276
                    continue
1277
1278
                npix = int(self._npix[idx_img])
1279
                if idx is None:
1280
                    s_img = (slice(0, npix),) + idx_img
1281
                else:
1282
                    s_img = (slice(0, npix),) + (0,) * len(self.axes)
1283
1284
                if self.axes:
1285
                    m = np.all(
1286
                        np.stack([pix_flat[i + 1] == t for i, t in enumerate(idx_img)]),
1287
                        axis=0,
1288
                    )
1289
                    pix[0][s_img] = pix_flat[0][m]
1290
                else:
1291
                    pix[0][s_img] = pix_flat[0]
1292
1293
                for j in range(len(self.axes)):
1294
                    pix[j + 1][s_img] = idx_img[j]
1295
1296
            pix = [p.T for p in pix]
1297
1298
        if local:
1299
            pix = self.global_to_local(pix)
1300
1301
        if flat:
1302
            pix = tuple([p[p != INVALID_INDEX.int] for p in pix])
1303
1304
        return pix
1305
1306
    def region_mask(self, regions):
1307
        """Create a mask from a given list of regions
1308
1309
        The mask is filled such that a pixel inside the region is filled with
1310
        "True". To invert the mask, e.g. to create a mask with exclusion regions
1311
        the tilde (~) operator can be used (see example below).
1312
1313
        Parameters
1314
        ----------
1315
        regions : str, `~regions.Region` or list of `~regions.Region`
1316
            Region or list of regions (pixel or sky regions accepted).
1317
            A region can be defined as a string ind DS9 format as well.
1318
            See http://ds9.si.edu/doc/ref/region.html for details.
1319
1320
        Returns
1321
        -------
1322
        mask_map : `~gammapy.maps.WcsNDMap` of boolean type
1323
            Boolean region mask
1324
1325
        """
1326
        from gammapy.maps import Map, RegionGeom
1327
1328
        if not self.is_regular:
1329
            raise ValueError("Multi-resolution maps not supported yet")
1330
1331
        # TODO: use spatial coordinates only...
1332
        geom = RegionGeom.from_regions(regions)
1333
        coords = self.get_coord()
1334
        mask = geom.contains(coords)
1335
        return Map.from_geom(self, data=mask)
1336
1337
    def get_coord(
1338
        self, idx=None, flat=False, sparse=False, mode="center", axis_name=None
1339
    ):
1340
        if mode == "edges" and axis_name is None:
1341
            raise ValueError("Mode 'edges' requires axis name to be defined")
1342
1343
        pix = self.get_idx(
1344
            idx=idx, flat=flat, sparse=sparse, mode=mode, axis_name=axis_name
1345
        )
1346
        data = self.pix_to_coord(pix)
1347
1348
        coords = MapCoord.create(
1349
            data=data, frame=self.frame, axis_names=self.axes.names
1350
        )
1351
1352
        return coords
1353
1354
    def contains(self, coords):
1355
        idx = self.coord_to_idx(coords)
1356
        return np.all(np.stack([t != INVALID_INDEX.int for t in idx]), axis=0)
1357
1358
    def solid_angle(self):
1359
        """Solid angle array (`~astropy.units.Quantity` in ``sr``).
1360
1361
        The array has the same dimensionality as ``map.nside``
1362
        since all pixels have the same solid angle.
1363
        """
1364
        import healpy as hp
1365
1366
        return Quantity(hp.nside2pixarea(self.nside), "sr")
1367
1368
    def __repr__(self):
1369
        lon, lat = self.center_skydir.data.lon.deg, self.center_skydir.data.lat.deg
1370
        return (
1371
            f"{self.__class__.__name__}\n\n"
1372
            f"\taxes       : {self.axes_names}\n"
1373
            f"\tshape      : {self.data_shape[::-1]}\n"
1374
            f"\tndim       : {self.ndim}\n"
1375
            f"\tnside      : {self.nside[0]}\n"
1376
            f"\tnested     : {self.nest}\n"
1377
            f"\tframe      : {self.frame}\n"
1378
            f"\tprojection : {self.projection}\n"
1379
            f"\tcenter     : {lon:.1f} deg, {lat:.1f} deg\n"
1380
        )
1381
1382
    def is_allclose(self, other, rtol_axes=1e-6, atol_axes=1e-6):
1383
        """Compare two data IRFs for equivalency
1384
1385
        Parameters
1386
        ----------
1387
        other :  `HpxGeom`
1388
            Geom to compare against
1389
        rtol_axes : float
1390
            Relative tolerance for the axes comparison.
1391
        atol_axes : float
1392
            Relative tolerance for the axes comparison.
1393
1394
        Returns
1395
        -------
1396
        is_allclose : bool
1397
            Whether the geometry is all close.
1398
        """
1399
        if not isinstance(other, self.__class__):
1400
            return TypeError(f"Cannot compare {type(self)} and {type(other)}")
1401
1402
        if self.is_allsky and not other.is_allsky:
1403
            return False
1404
1405
        if self.data_shape != other.data_shape:
1406
            return False
1407
1408
        axes_eq = self.axes.is_allclose(other.axes, rtol=rtol_axes, atol=atol_axes)
1409
1410
        hpx_eq = (
1411
            self.nside == other.nside
1412
            and self.frame == other.frame
1413
            and self.order == other.order
1414
            and self.nest == other.nest
1415
        )
1416
1417
        return axes_eq and hpx_eq
1418
1419
    def __eq__(self, other):
1420
        if not isinstance(other, self.__class__):
1421
            return False
1422
1423
        return self.is_allclose(other=other)
1424
1425
    def __ne__(self, other):
1426
        return not self.__eq__(other)
1427