• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

LSSTDESC / NaMaster / 8195278510

07 Mar 2024 09:52PM UTC coverage: 92.907%. First build
8195278510

Pull #184

github

web-flow
Merge 4cb24553c into 2b35f37af
Pull Request #184: Catalog PCLs

14 of 86 new or added lines in 4 files covered. (16.28%)

1205 of 1297 relevant lines covered (92.91%)

0.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.15
/pymaster/utils.py
1
from pymaster import nmtlib as lib
1✔
2
import numpy as np
1✔
3
import os
1✔
4
import healpy as hp
1✔
5

6

7
def _setenv(name, value, keep=False):
1✔
8
    """Set the named environment variable to the given value. If keep==False
9
    (the default), existing values are overwritten. If the value is None, then
10
    it's deleted from the environment. If keep==True, then this function does
11
    nothing if the variable already has a value."""
12
    if name in os.environ and keep:
1✔
13
        return
×
14
    elif name in os.environ and value is None:
1✔
15
        del os.environ[name]
×
16
    elif value is not None:
1✔
17
        os.environ[name] = str(value)
×
18

19

20
def _getenv(name, default=None):
1✔
21
    """Return the value of the named environment variable, or default
22
    if it's not set"""
23
    try:
1✔
24
        return os.environ[name]
1✔
25
    except KeyError:
1✔
26
        return default
1✔
27

28

29
try:
1✔
30
    # From pixell:
31
    # Initialize DUCC's thread num variable from OMP's if it's not already set.
32
    # This must be done before importing ducc0 for the first time. Doing this
33
    # limits wasted memory from ducc allocating too big a thread pool. For
34
    # computes with many cores, this can save GBs of memory.
35
    _setenv("DUCC0_NUM_THREADS", _getenv("OMP_NUM_THREADS"), keep=True)
1✔
36
    import ducc0  # noqa
1✔
37
    HAVE_DUCC = True
1✔
38
except ModuleNotFoundError:
×
39
    HAVE_DUCC = False
×
40

41

42
class NmtParams(object):
1✔
43
    """ Class that holds the values of several parameters controlling the
44
    default behaviour of different NaMaster methods. Currently these are:
45

46
    - ``sht_calculator``: the software package to use to calculate \
47
      spherical harmonic transforms. Defaults to \
48
      `ducc <https://gitlab.mpcdf.mpg.de/mtr/ducc>`_, with the other \
49
      choice being `healpy <https://healpy.readthedocs.io/en/latest/>`_.
50
    - ``n_iter_default``: number of iterations to use when computing \
51
      `map2alm` spherical harmonic transforms of most maps. Defaults to 3.
52
    - ``n_iter_mask_default``: number of iterations to use when computing \
53
      `map2alm` spherical harmonic transforms of masks. Defaults to 3.
54
    - ``tol_pinv_default``: relative eigenvalue threshold to use when \
55
      computing matrix pseudo-inverses.
56

57
    All variables can be changed using the ``set_`` methods described below,
58
    and their current values can be checked with :meth:`get_default_params`.
59
    Note that, except for ``sht_calculator``, all of these variables can
60
    be tweaked when calling different various NaMaster functions. The
61
    values stored in this object only hold the values they default to if
62
    they are not set in those function calls.
63
    """
64
    def __init__(self):
1✔
65
        self.sht_calculator = 'ducc' if HAVE_DUCC else 'healpy'
1✔
66
        self.n_iter_default = 3
1✔
67
        self.n_iter_mask_default = 3
1✔
68
        self.tol_pinv_default = 1E-10
1✔
69

70

71
nmt_params = NmtParams()
1✔
72

73

74
def set_sht_calculator(calc_name):
1✔
75
    """ Select default spherical harmonic transform calculator.
76

77
    Args:
78
        calc_name (:obj:`str`): Calculator name. Allowed options
79
            are ``'ducc'`` or ``'healpy'``.
80
    """
81
    if calc_name not in ['ducc', 'healpy']:
1✔
82
        raise KeyError("SHT calculator must be 'ducc' or 'healpy'")
1✔
83
    if (calc_name == 'ducc') and (not HAVE_DUCC):
1✔
84
        raise ValueError("ducc not found. "
1✔
85
                         "Select a different SHT calculator.")
86
    nmt_params.sht_calculator = calc_name
1✔
87

88

89
def set_n_iter_default(n_iter, mask=False):
1✔
90
    """ Select the number of Jacobi iterations to use when computing
91
    `map2alm` spherical harmonic transforms.
92

93
    Args:
94
        n_iter (:obj:`int`): Number of iterations.
95
        mask (:obj:`bool`): If ``True``, ``n_iter`` will be the
96
            number of iterations used when computing mask transforms.
97
            Otherwise, it will be the number used for all other
98
            transforms.
99
    """
100
    if n_iter < 0:
1✔
101
        raise ValueError('n_iter must be positive.')
1✔
102
    if mask:
1✔
103
        nmt_params.n_iter_mask_default = int(n_iter)
1✔
104
    else:
105
        nmt_params.n_iter_default = int(n_iter)
1✔
106

107

108
def set_tol_pinv_default(tol_pinv):
1✔
109
    """ Select the relative eigenvalue threshold to use when
110
    computing matrix pseudo-inverses. Check the docstring of
111
    :meth:`moore_penrose_pinvh`.
112

113
    Args:
114
        tol_pinv (:obj:`float`): Relative eigenvalue threshold.
115
    """
116
    if (tol_pinv > 1) or (tol_pinv < 0):
1✔
117
        raise ValueError('tol_pinv must be between 0 and 1.')
1✔
118
    nmt_params.tol_pinv_default = tol_pinv
1✔
119

120

121
def get_default_params():
1✔
122
    """ Returns a dictionary with the current values of all
123
    default parameters.
124
    """
125
    return {k: getattr(nmt_params, k)
1✔
126
            for k in ['sht_calculator',
127
                      'n_iter_default',
128
                      'n_iter_mask_default',
129
                      'tol_pinv_default']}
130

131

132
class _SHTInfo(object):
1✔
133
    def __init__(self, nring, theta, phi0, nphi, weight,
1✔
134
                 is_CAR=False, nx_short=-1, nx_full=-1, nside=-1):
135
        self.nring = nring
1✔
136
        self.theta = theta
1✔
137
        self.phi0 = phi0
1✔
138
        self.nphi = nphi
1✔
139
        self.is_CAR = is_CAR
1✔
140
        self.nx_short = nx_short
1✔
141
        self.nx_full = nx_full
1✔
142
        # Compute the starting point of each ring
143
        off = np.cumsum(nphi)
1✔
144
        off = np.concatenate([[0], off[:-1]])
1✔
145
        self.offsets = off.astype(np.uint64, copy=False)
1✔
146
        self.weight = weight
1✔
147
        self.nside = nside
1✔
148

149
    @classmethod
1✔
150
    def from_rectpix(cls, n_theta, theta_min, d_theta,
1✔
151
                     n_phi, d_phi, phi0):
152
        th = theta_min + np.arange(n_theta+1)*d_theta
1✔
153
        nring = n_theta
1✔
154
        theta = th[:-1].astype(np.float64)
1✔
155
        phi0 = np.zeros(n_theta, dtype=np.float64)+phi0
1✔
156
        nx_short = n_phi
1✔
157
        nx_full = int(2*np.pi/d_phi)
1✔
158
        nphi = np.zeros(n_theta, dtype=np.uint64)+nx_full
1✔
159

160
        # CC weights according to ducc0
161
        i_theta_0 = int(theta_min/d_theta+0.5)
1✔
162
        # +1 below because both poles get a ring
163
        nth_full = int(np.pi/d_theta+0.5)+1
1✔
164
        w = ducc0.sht.experimental.get_gridweights('CC', nth_full)
1✔
165
        w *= d_phi/(2*np.pi)
1✔
166
        weight_th = w[i_theta_0:i_theta_0+n_theta]
1✔
167
        # Alternatively, we could use the pixel area as below
168
        # weight_th = (np.cos(th[:-1])-np.cos(th[1:]))*d_phi
169

170
        return cls(nring=nring, theta=theta, phi0=phi0,
1✔
171
                   nphi=nphi, weight=weight_th, is_CAR=True,
172
                   nx_short=nx_short, nx_full=nx_full)
173

174
    @classmethod
1✔
175
    def from_nside(cls, nside):
1✔
176
        npix = 12*nside*nside
1✔
177
        rings = np.arange(4*nside-1)
1✔
178
        nring = len(rings)
1✔
179
        theta = np.zeros(nring, np.float64)
1✔
180
        phi0 = np.zeros(nring, np.float64)
1✔
181
        nphi = np.zeros(nring, np.uint64)
1✔
182
        rings = rings+1
1✔
183
        northrings = np.where(rings > 2*nside,
1✔
184
                              4*nside-rings,
185
                              rings)
186
        # Handle polar cap
187
        cap = np.where(northrings < nside)[0]
1✔
188
        theta[cap] = 2*np.arcsin(northrings[cap]/(6**0.5*nside))
1✔
189
        nphi[cap] = 4*northrings[cap]
1✔
190
        phi0[cap] = np.pi/(4*northrings[cap])
1✔
191
        # Handle rest
192
        rest = np.where(northrings >= nside)[0]
1✔
193
        theta[rest] = np.arccos((2*nside-northrings[rest]) *
1✔
194
                                (8*nside/npix))
195
        nphi[rest] = 4*nside
1✔
196
        phi0[rest] = np.pi/(4*nside) * \
1✔
197
            (((northrings[rest]-nside) & 1) == 0)
198
        # Above assumed northern hemisphere. Fix southern
199
        south = np.where(northrings != rings)[0]
1✔
200
        theta[south] = np.pi-theta[south]
1✔
201
        weight = 4*np.pi/npix
1✔
202
        return cls(nring=nring, theta=theta, phi0=phi0,
1✔
203
                   nphi=nphi, weight=weight, nside=nside)
204

205
    def pad_map(self, maps):
1✔
206
        if not self.is_CAR:
1✔
207
            return maps
1✔
208
        # Shapes
209
        pre = maps.shape[:-1]
1✔
210
        shape_short = pre + (self.nring, self.nx_short)
1✔
211
        shape_pad = pre + (self.nring, self.nx_full)
1✔
212
        shape_flat = pre + (self.nring*self.nx_full,)
1✔
213
        # Init to zero
214
        maps_pad = np.zeros(shape_pad)
1✔
215
        # Copy over
216
        maps_pad[..., :self.nx_short] = maps.reshape(shape_short)
1✔
217
        # Flatten
218
        maps_pad = maps_pad.reshape(shape_flat)
1✔
219
        return maps_pad
1✔
220

221
    def unpad_map(self, maps):
1✔
222
        if not self.is_CAR:
1✔
223
            return maps
1✔
224
        pre = maps.shape[:-1]
1✔
225
        shape_pad = pre + (self.nring, self.nx_full)
1✔
226
        shape_flat = pre + (self.nring*self.nx_short,)
1✔
227
        maps_unpad = maps.reshape(shape_pad)[..., :self.nx_short]
1✔
228
        return maps_unpad.reshape(shape_flat)
1✔
229

230
    def times_weight(self, m):
1✔
231
        if self.is_CAR:  # Weight depends on theta
1✔
232
            npix = m.shape[-1]
1✔
233
            nx = npix // self.nring
1✔
234
            pre = m.shape[:-1]
1✔
235
            shape_2d = pre + (self.nring, nx)
1✔
236
            shape_flat = pre + (self.nring*nx,)
1✔
237
            return (m.reshape(shape_2d) *
1✔
238
                    self.weight[:, None]).reshape(shape_flat)
239
        else:  # Weight is constant in healpix
240
            return m*self.weight
1✔
241

242
    def dot_map(self, m1, m2):
1✔
243
        return np.sum(self.times_weight(m1*m2))
1✔
244

245

246
class NmtMapInfo(object):
1✔
247
    """
248
    This object contains information about the pixelization of
249
    a curved-sky map. :obj:`NmtMapInfo` objects can be compared
250
    with one another using ``==`` and ``!=`` to check for fields
251
    with compatible pixelizations.
252

253
    Args:
254
        wcs (`WCS`): a WCS object (see the `astropy
255
            <http://docs.astropy.org/en/stable/wcs/index.html>`_
256
            documentation). If ``None``, HEALPix pixelization is
257
            assumed, and ``axes`` should be a 1-element
258
            sequence with the number of pixels of the map.
259
        axes (`array`): shape of the maps (length-2 for CAR maps,
260
            length-1 for HEALPix).
261
    """
262
    def __init__(self, wcs, axes):
1✔
263
        if wcs is None:
1✔
264
            is_healpix = True
1✔
265
            nside = 2
1✔
266
            while 12 * nside * nside != axes[0]:
1✔
267
                nside *= 2
1✔
268
                if nside > 65536:
1✔
269
                    raise ValueError("Something is wrong "
1✔
270
                                     "with your input arrays")
271
            npix = 12*nside*nside
1✔
272
            flip_th = False
1✔
273
            flip_ph = False
1✔
274
            theta_min = -1
1✔
275
            theta_max = -1
1✔
276
            phi0 = -1
1✔
277
            dth = -1
1✔
278
            dph = -1
1✔
279
            nx = -1
1✔
280
            ny = -1
1✔
281
            si = _SHTInfo.from_nside(nside)
1✔
282
        else:
283
            is_healpix = False
1✔
284
            nside = -1
1✔
285

286
            d_ra, d_dec = wcs.wcs.cdelt[:2]
1✔
287
            ra0, dec0 = wcs.wcs.crval[:2]
1✔
288
            ix0, iy0 = wcs.wcs.crpix[:2]
1✔
289
            typra = wcs.wcs.ctype[0]
1✔
290
            typdec = wcs.wcs.ctype[1]
1✔
291
            try:
1✔
292
                ny, nx = axes
1✔
293
            except ValueError:
1✔
294
                raise ValueError("Input maps must be 2D if not HEALPix")
1✔
295
            npix = ny*nx
1✔
296

297
            dth = np.fabs(np.radians(d_dec))
1✔
298
            dph = np.fabs(np.radians(d_ra))
1✔
299

300
            # Check if projection type is CAR
301
            if not ((typra[-3:] == 'CAR') and (typdec[-3:] == 'CAR')):
1✔
302
                raise ValueError("Maps must have CAR pixelization")
1✔
303

304
            # Check if reference pixel is consistent with CC
305
            if np.fabs(dec0) > 1E-3:
1✔
306
                raise ValueError("Reference pixel must be at the equator")
1✔
307

308
            # Check d_ra, d_dec are CC
309
            if (np.fabs(round(2*np.pi/dph) - 2 * np.pi / dph) > 0.01) or \
1✔
310
               (np.fabs(round(np.pi/dth) - np.pi / dth) > 0.01):
311
                raise ValueError("The pixels should divide the sphere exactly")
1✔
312

313
            # Is colatitude decreasing? (i.e. is declination increasing?)
314
            flip_th = d_dec > 0
1✔
315
            flip_ph = d_ra < 0
1✔
316

317
            # Get map edges
318
            # Theta
319
            coord0 = np.zeros([1, len(wcs.wcs.crpix)])
1✔
320
            coord1 = coord0.copy()
1✔
321
            coord1[0, 1] = ny-1
1✔
322
            edges = [wcs.wcs_pix2world(coord0, 0)[0, 1],
1✔
323
                     wcs.wcs_pix2world(coord1, 0)[0, 1]]
324
            theta_min = np.radians(90-max(edges))
1✔
325
            theta_max = np.radians(90-min(edges))
1✔
326
            # Phi
327
            coord0 = np.zeros([1, len(wcs.wcs.crpix)])
1✔
328
            if flip_ph:
1✔
329
                coord0[0, 0] = nx-1
1✔
330
            phi0 = wcs.wcs_pix2world(coord0, 0)[0, 0]
1✔
331
            if np.isnan(phi0):
1✔
332
                raise ValueError("There is something wrong with the azimuths")
1✔
333
            phi0 = np.radians(phi0)
1✔
334

335
            # Check if ix0,iy0 + ra0, dec0 mean this is CC
336
            if (theta_min < 0) or (theta_max > np.pi) or np.isnan(edges).any():
1✔
337
                raise ValueError("The colatitude map edges are "
1✔
338
                                 "outside the sphere")
339

340
            if np.fabs(nx*d_ra) > 360:
1✔
341
                raise ValueError("Seems like you're wrapping the "
1✔
342
                                 "sphere more than once")
343

344
            si = _SHTInfo.from_rectpix(ny, theta_min, dth,
1✔
345
                                       nx, dph, phi0)
346

347
        # Store values
348
        self.is_healpix = is_healpix
1✔
349
        self.nside = nside
1✔
350
        self.npix = npix
1✔
351
        self.nx = nx
1✔
352
        self.ny = ny
1✔
353
        self.flip_th = flip_th
1✔
354
        self.flip_ph = flip_ph
1✔
355
        self.theta_min = theta_min
1✔
356
        self.theta_max = theta_max
1✔
357
        self.d_theta = dth
1✔
358
        self.d_phi = dph
1✔
359
        self.phi0 = phi0
1✔
360
        self.si = si
1✔
361

362
    def __eq__(self, other):
1✔
363
        # Is it the same thing?
364
        if self is other:
1✔
365
            return True
1✔
366
        # Is it the same type?
367
        if type(other) is not type(self):
1✔
368
            return False
1✔
369

370
        # If HEALPix
371
        if self.is_healpix:
1✔
372
            if not other.is_healpix:
1✔
373
                return False
1✔
374
            if other.nside != self.nside:
1✔
375
                return False
1✔
376
            return True
1✔
377

378
        # If CAR
379
        if other.is_healpix:
1✔
380
            return False
1✔
381
        for att in ['npix', 'nx', 'ny', 'theta_min',
1✔
382
                    'theta_max', 'phi0', 'd_theta', 'd_phi']:
383
            a_this = getattr(self, att, None)
1✔
384
            a_other = getattr(other, att, None)
1✔
385
            if a_this != a_other:
1✔
386
                return False
1✔
387
        return True
1✔
388

389
    def reform_map(self, maps):
1✔
390
        """ Modifies a map to make it compatible with the
391
        standards used by NaMaster for map manipulation (e.g.
392
        spherical harmonic transforms). This includes
393
        flattening 2D maps, and flipping their coordinate
394
        axes if required by their associated WCS information.
395
        HEALPix maps are unmodified.
396

397
        Args:
398
            maps (`array`): 2D (for HEALPix) or 3D (for CAR)
399
                array containing a set of maps.
400

401
        Returns:
402
            (`array`): Reformed map.
403
        """
404
        if not self._map_compatible(maps):
1✔
405
            raise ValueError("Incompatible map!")
1✔
406
        if self.is_healpix:
1✔
407
            return maps
1✔
408

409
        # Flipping dimensions
410
        if self.flip_th:
1✔
411
            maps = maps[..., ::-1, :]
1✔
412
        if self.flip_ph:
1✔
413
            maps = maps[..., :, ::-1]
1✔
414
        # Flatten last two dimensions
415
        maps = maps.reshape(maps.shape[:-2]+(self.npix,))
1✔
416
        return maps
1✔
417

418
    def _map_compatible(self, mp):
1✔
419
        if self.is_healpix:
1✔
420
            return mp.shape[-1] == self.npix
1✔
421
        else:
422
            return mp.shape[-2:] == (self.ny, self.nx)
1✔
423

424
    def get_lmax(self):
1✔
425
        """ Returns the default maximum multipole :math:`\\ell_{\\rm max}`
426
        associated with this pixelization scheme. This will be
427
        :math:`3 N_{\\rm side}-1` for HEALPix or
428
        :math:`\\pi/{\\rm min}(\\Delta\\theta,\\Delta\\varphi)` for
429
        CAR maps (with :math:`\\Delta\\theta` and :math:`\\Delta\\varphi`
430
        the constant intervals of colatitude and azimuth in radians).
431
        """
432
        if self.is_healpix:
1✔
433
            return 3*self.nside-1
1✔
434
        else:
435
            dxmin = min(self.d_theta, self.d_phi)
1✔
436
            return int(np.pi/dxmin)
1✔
437

438

439
class NmtAlmInfo(object):
1✔
440
    """ Object containing information useful to manipulate sets of
441
    spherical harmonic coefficients :math:`a_{\\ell m}`.
442

443
    Args:
444
        lmax (:obj:`int`): Maximum multipole :math:`\\ell_{\\rm max}`
445
            to which the :math:`a_{\\ell m}` s are calculated.
446
    """
447
    def __init__(self, lmax):
1✔
448
        self.lmax = lmax
1✔
449
        self.mmax = self.lmax
1✔
450
        m = np.arange(self.mmax+1)
1✔
451
        self.mstart = (m*(2*self.lmax+1-m)//2).astype(np.uint64, copy=False)
1✔
452
        self.nelem = int(np.max(self.mstart) + (self.lmax+1))
1✔
453

454
    def __eq__(self, other):
1✔
455
        if self is other:
1✔
456
            return True
1✔
457
        if type(other) is not type(self):
1✔
458
            return False
1✔
459
        if self.lmax != other.lmax:
1✔
460
            return False
1✔
461
        return True
1✔
462

463

464
def mask_apodization(mask_in, aposize, apotype="C1"):
1✔
465
    """ Apodizes a mask with an given apodization scale using different
466
    methods. A given pixel is determined to be "masked" if its value is 0.
467
    This method only works for HEALPix maps. Three apodization methods are
468
    currently implemented:
469

470
    - **"C1"** apodization: all pixels are multiplied by a factor :math:`f` \
471
      given by
472

473
      .. math::
474
        f=\\left\\{
475
        \\begin{array}{cc}
476
            x-\\sin(2\\pi x)/(2\\pi) & x<1\\\\
477
            1 & {\\rm otherwise}
478
        \\end{array}
479
        \\right..
480

481
      where :math:`x=\\sqrt{(1-\\cos\\theta)/(1-\\cos\\theta_*)}`, with
482
      :math:`\\theta_*` the apodization scale, and :math:`\\theta` the
483
      separation between a pixel and its nearest masked pixel (i.e. where
484
      the mask takes a zero value). See `Grain et al. 2009
485
      <https://arxiv.org/abs/0903.2350>`_.
486
    - **"C2"** apodization: similar to "C1", but using the apodization
487
      function:
488

489
      .. math::
490
        f=\\left\\{
491
        \\begin{array}{cc}
492
            \\frac{1}{2}\\left[1-\\cos(\\pi x)\\right] & x<1\\\\
493
            1 & {\\rm otherwise}
494
        \\end{array}
495
        \\right..
496

497
    - **"Smooth"** apodization: this is carried out in three steps:
498

499
      1. All pixels within a disk of radius :math:`2.5\\theta_*` of a
500
         masked pixel are masked.
501
      2. The resulting map is smoothed with a Gaussian window function
502
         with standard deviation :math:`\\theta_*`.
503
      3. The resulting map is multiplied by the original mask to ensure
504
         that all pixels that were previously masked are still masked.
505

506
    Args:
507
        mask_in (`array`): Input mask, provided as an array of floats
508
            corresponding to a HEALPix map in RING order.
509
        aposize (:obj:`float`): Apodization scale in degrees.
510
        apotype (:obj:`str`): Apodization type.
511

512
    Returns:
513
        (`array`): Apodized mask.
514
    """
515
    if apotype not in ['C1', 'C2', 'Smooth']:
1✔
516
        raise ValueError(f"Apodization type {apotype} unknown. "
1✔
517
                         "Choose from ['C1', 'C2', 'Smooth']")
518

519
    m = lib.apomask(mask_in.astype("float64"), len(mask_in),
1✔
520
                    aposize, apotype)
521

522
    if apotype != 'Smooth':
1✔
523
        return m
1✔
524

525
    # Smooth
526
    minfo = NmtMapInfo(None, mask_in.shape)
1✔
527
    lmax = 3*minfo.nside-1
1✔
528
    ainfo = NmtAlmInfo(lmax)
1✔
529
    ls = np.arange(lmax+1)
1✔
530
    beam = np.exp(-0.5*ls*(ls+1)*np.radians(aposize)**2)
1✔
531
    alm = map2alm(np.array([m]), 0, minfo, ainfo,
1✔
532
                  n_iter=3)[0]
533
    alm = hp.almxfl(alm, beam, mmax=ainfo.mmax)
1✔
534
    m = alm2map(np.array([alm]), 0, minfo, ainfo)[0]
1✔
535
    # Multiply by original mask
536
    m *= mask_in
1✔
537
    return m
1✔
538

539

540
def mask_apodization_flat(mask_in, lx, ly, aposize, apotype="C1"):
1✔
541
    """ Apodizes a flat-sky mask. See the docstrings of
542
    :meth:`mask_apodization` for a description of the different
543
    methods implemented.
544

545
    Args:
546
        mask_in (`array`): Input mask, provided as a 2D array of
547
            shape ``(ny, nx)``.
548
        lx (:obj:`float`): Patch size in the x axis (radians).
549
        ly (:obj:`float`): Patch size in the y axis (radians).
550
        aposize (:obj:`float`): Apodization scale in degrees.
551
        apotype (:obj:`str`): Apodization type.
552

553
    Returns:
554
        (`array`): Apodized mask.
555
    """
556
    if mask_in.ndim != 2:
1✔
557
        raise ValueError("Mask must be a 2D array")
1✔
558
    nx = len(mask_in[0])
1✔
559
    ny = len(mask_in)
1✔
560
    mask_apo_flat = lib.apomask_flat(
1✔
561
        nx, ny, lx, ly, mask_in.flatten().astype("float64"),
562
        nx * ny, aposize, apotype
563
    )
564
    return mask_apo_flat.reshape([ny, nx])
1✔
565

566

567
def synfast_spherical(nside, cls, spin_arr, beam=None, seed=-1,
1✔
568
                      wcs=None, lmax=None):
569
    """ Generates full-sky Gaussian random fields according to a given
570
    power spectrum. This function should produce outputs similar to
571
    healpy's `synfast <https://healpy.readthedocs.io/en/latest/generated/healpy.sphtfunc.synfast.html>`_.
572

573
    Args:
574
        nside (:obj:`int`): HEALpix resolution parameter.
575
            If you want rectangular pixel maps, ignore this parameter
576
            and pass a WCS object as ``wcs`` (see below).
577
        cls (`array`): Array contiaining power spectra. Shape
578
            should be ``(n_cls, n_ell)``, where ``n_cls`` is the
579
            number of power spectra needed to define all the fields.
580
            This should be ``n_cls = n_maps * (n_maps + 1) / 2``,
581
            where ``n_maps`` is the total number of maps required
582
            (1 for each spin-0 field, 2 for each spin>0 field).
583
            Power spectra must be provided only for the upper-triangular
584
            part of the full power spectrum matrix in row-major order
585
            (e.g. if ``n_maps=3``, there will be 6 power spectra ordered
586
            as [1x1, 1x2, 1x3, 2x2, 2x3, 3x3]).
587
        spin_arr (`array`): Array containing the spins of all the
588
            fields to generate.
589
        beam (`array`): 2D array containing the instrumental beam
590
            of each field to simulate (the output map(s) will be
591
            convolved with it).
592
        seed (:obj:`int`): Seed for the random number generator. If
593
            negative, a random seed will be used.
594
        wcs (`WCS`): A WCS object if using rectangular pixels (see `the astropy
595
            documentation
596
            <http://docs.astropy.org/en/stable/wcs/index.html>`_).
597
        lmax (:obj:`int`): Maximum multipole up to which the spherical
598
            harmonic coefficients of the maps will be generated.
599

600
    Returns:
601
        (`array`): A set of full-sky maps (1 for each spin-0 field, 2 for \
602
            each spin-s field).
603
    """  # noqa
604
    if seed < 0:
1✔
605
        seed = np.random.randint(50000000)
1✔
606

607
    if wcs is None:
1✔
608
        wtshape = [12*nside*nside]
1✔
609
    else:
610
        n_ra = int(round(360./np.fabs(wcs.wcs.cdelt[0])))
×
611
        n_dec = int(round(180./np.fabs(wcs.wcs.cdelt[1])))+1
×
612
        wtshape = [n_dec, n_ra]
×
613
    minfo = NmtMapInfo(wcs, wtshape)
1✔
614

615
    if wcs is not None:  # Check that theta_min and theta_max are 0 and pi
1✔
616
        if (np.fabs(minfo.theta_min) > 1E-4) or \
×
617
           (np.fabs(minfo.theta_max - np.pi) > 1E-4):
618
            raise ValueError("Given your WCS, the map wouldn't cover the "
×
619
                             "whole sphere exactly")
620

621
    if lmax is None:
1✔
622
        lmax = minfo.get_lmax()
1✔
623

624
    spin_arr = np.array(spin_arr).astype(np.int32)
1✔
625
    nfields = len(spin_arr)
1✔
626

627
    if np.any(spin_arr < 0):
1✔
628
        raise ValueError("Spins must be positive")
1✔
629
    nmap_arr = np.array([1+int(s != 0) for s in spin_arr])
1✔
630
    map_first = np.concatenate([[0], np.cumsum(nmap_arr)[:-1]])
1✔
631
    nmaps = np.sum(nmap_arr)
1✔
632

633
    ncls = (nmaps * (nmaps + 1)) // 2
1✔
634
    if ncls != len(cls):
1✔
635
        raise ValueError(
1✔
636
            f"Must provide all Cls necessary to simulate all field ({ncls}).")
637
    lmax_cls = len(cls[0]) - 1
1✔
638
    lmax = min(lmax_cls, lmax)
1✔
639
    ainfo = NmtAlmInfo(lmax)
1✔
640

641
    # 1. Generate alms
642
    # Note that, if `new=False` stops being allowed in healpy, we'll need
643
    # to change the Cl ordering.
644
    alms = np.array(hp.synalm(cls, lmax=lmax, mmax=lmax, new=False))
1✔
645

646
    # 2. Multiply by beam
647
    if beam is not None:
1✔
648
        if len(beam) != nfields:
1✔
649
            raise ValueError("Must provide one beam per field")
1✔
650
        if len(beam[0]) < lmax + 1:
1✔
651
            raise ValueError(f"The beam should be provided to ell = {lmax}")
1✔
652
        beam_per = []
1✔
653
        for ifield, n in enumerate(nmap_arr):
1✔
654
            for i in range(n):
1✔
655
                beam_per.append(beam[ifield])
1✔
656
        alms = np.array([hp.almxfl(alm, bl, mmax=lmax)
1✔
657
                         for alm, bl in zip(alms, beam_per)])
658

659
    # 3. SHT back to real space
660
    maps = np.concatenate([alm2map(alms[i0:i0+n], s, minfo, ainfo)
1✔
661
                           for i0, n, s in zip(map_first, nmap_arr, spin_arr)])
662

663
    if minfo.is_healpix:
1✔
664
        maps = maps.reshape([nmaps, minfo.npix])
1✔
665
    else:
666
        maps = maps.reshape([nmaps, minfo.ny, minfo.nx])
×
667
        if minfo.flip_th:
×
668
            maps = maps[:, ::-1, :]
×
669
        if minfo.flip_ph:
×
670
            maps = maps[:, :, ::-1]
×
671

672
    return maps
1✔
673

674

675
def _toeplitz_sanity(l_toeplitz, l_exact, dl_band, lmax, fl1, fl2):
1✔
676
    if l_toeplitz > 0:
1✔
677
        if (fl1.pure_e or fl1.pure_b or
1✔
678
                fl2.pure_e or fl2.pure_b):
679
            raise ValueError("Can't use Toeplitz approximation "
1✔
680
                             "with purification.")
681
        if (l_exact <= 0) or (dl_band < 0):
1✔
682
            raise ValueError("`l_exact` and `dl_band` must be "
1✔
683
                             "positive numbers")
684
        if l_exact > l_toeplitz:
1✔
685
            raise ValueError("`l_exact` must be `<= l_toeplitz")
1✔
686
        if ((l_toeplitz >= lmax) or (l_exact >= lmax) or
1✔
687
                (dl_band >= lmax)):
688
            raise ValueError("`l_toeplitz`, `l_exact` and `dl_band` "
1✔
689
                             "must be smaller than `l_max`")
690

691

692
def synfast_flat(nx, ny, lx, ly, cls, spin_arr, beam=None, seed=-1):
1✔
693
    """
694
    Generates flat-sky Gaussian random fields according to a given
695
    power spectrum. This is the flat-sky equivalent of
696
    :meth:`synfast_spherical`.
697

698
    Args:
699
        nx (:obj:`int`): Number of pixels in the x axis.
700
        ny (:obj:`int`): Number of pixels in the y axis.
701
        lx (:obj:`float`): Patch size in the x axis (radians).
702
        ly (:obj:`float`): Patch size in the y axis (radians).
703
        cls (`array`): Array contiaining power spectra. Shape
704
            should be ``(n_cls, n_ell)``, where ``n_cls`` is the
705
            number of power spectra needed to define all the fields.
706
            This should be ``n_cls = n_maps * (n_maps + 1) / 2``,
707
            where ``n_maps`` is the total number of maps required
708
            (1 for each spin-0 field, 2 for each spin>0 field).
709
            Power spectra must be provided only for the upper-triangular
710
            part of the full power spectrum matrix in row-major order
711
            (e.g. if ``n_maps=3``, there will be 6 power spectra ordered
712
            as [1x1, 1x2, 1x3, 2x2, 2x3, 3x3]). The power spectra are
713
            assumed to be sampled at all integer multipoles from
714
            :math:`\\ell=0` to ``n_ell-1``.
715
        spin_arr (`array`): Array containing the spins of all the
716
            fields to generate.
717
        beam (`array`): 2D array containing the instrumental beam
718
            of each field to simulate (the output map(s) will be
719
            convolved with it).
720
        seed (:obj:`int`): Seed for the random number generator. If
721
            negative, a random seed will be used.
722

723
    Returns:
724
        (`array`): An array of flat-sky maps (1 for each spin-0 field, 2 for \
725
            each spin-s field) with shape ``(nmaps, ny, nx)``.
726
    """
727
    if seed < 0:
1✔
728
        seed = np.random.randint(50000000)
1✔
729

730
    spin_arr = np.array(spin_arr).astype(np.int32)
1✔
731
    nfields = len(spin_arr)
1✔
732

733
    if np.any(spin_arr < 0):
1✔
734
        raise ValueError("Spins must be positive")
1✔
735
    nmaps = int(1 * np.sum(spin_arr == 0) + 2 * np.sum(spin_arr != 0))
1✔
736

737
    ncls = (nmaps * (nmaps + 1)) // 2
1✔
738
    if ncls != len(cls):
1✔
739
        raise ValueError(
1✔
740
            "Must provide all Cls necessary to simulate all "
741
            "fields (%d)." % ncls
742
        )
743
    lmax = len(cls[0]) - 1
1✔
744

745
    if beam is None:
1✔
746
        beam = np.ones([nfields, lmax + 1])
1✔
747
    else:
748
        if len(beam) != nfields:
1✔
749
            raise ValueError("Must provide one beam per field")
1✔
750
        if len(beam[0]) != lmax + 1:
1✔
751
            raise ValueError(
1✔
752
                "The beam should have as many multipoles as the power spectrum"
753
            )
754

755
    data = lib.synfast_new_flat(
1✔
756
        nx, ny, lx, ly, spin_arr, seed, cls, beam, nmaps * nx * ny
757
    )
758

759
    maps = data.reshape([nmaps, ny, nx])
1✔
760

761
    return maps
1✔
762

763

764
def moore_penrose_pinvh(mat, tol_pinv):
1✔
765
    """ Compute the Moore-Penrose pseudo-inverse of a
766
    Hermitian matrix. This is done by diagonalising
767
    the matrix, setting the inverse of all eigenvales
768
    below a given threshold to zero, and reconstructing
769
    the inverse matrix from the modified eigenvalues.
770
    The inverse of all eigenvalues smaller than a factor
771
    ``tol_pinv`` times the largest eigenvalue will be set
772
    to zero. If ``tol_pinv <= 0``, the standard inverse
773
    is computed.
774

775
    Args:
776
        mat (`array`): 2D Hermitian matrix.
777
        tol_pinv (:obj:`float`): Relative eigenvalue
778
            threshold.
779

780
    Returns:
781
        (`array`): Pseudo-inverse of input matrix.
782
    """
783
    if (tol_pinv is None) or (tol_pinv <= 0):
1✔
784
        return np.linalg.inv(mat)
×
785

786
    w, v = np.linalg.eigh(mat)
1✔
787
    badw = w < tol_pinv*np.max(w)
1✔
788
    w_inv = 1./w
1✔
789
    w_inv[badw] = 0.
1✔
790
    pinv = np.dot(v, np.dot(np.diag(w_inv), v.T))
1✔
791
    return pinv
1✔
792

793

794
def _ducc_kwargs(spin, sht_info, alm_info):
1✔
795
    kwargs = {'spin': spin, 'theta': sht_info.theta, 'nphi': sht_info.nphi,
1✔
796
              'phi0': sht_info.phi0, 'ringstart': sht_info.offsets,
797
              'lmax': alm_info.lmax, 'mmax': alm_info.mmax,
798
              'mstart': alm_info.mstart, 'nthreads': 0}
799
    return kwargs
1✔
800

801

802
def _alm2map_ducc0(alm, spin, sht_info, alm_info):
1✔
803
    kwargs = _ducc_kwargs(spin, sht_info, alm_info)
1✔
804
    maps = ducc0.sht.experimental.synthesis(alm=alm, **kwargs)
1✔
805
    return maps
1✔
806

807

808
def _map2alm_ducc0(map, spin, sht_info, alm_info):
1✔
809
    kwargs = _ducc_kwargs(spin, sht_info, alm_info)
1✔
810
    alm = ducc0.sht.experimental.adjoint_synthesis(
1✔
811
        map=sht_info.times_weight(map), **kwargs)
812
    return alm
1✔
813

814

815
def _alm2map_healpy(alm, spin, sht_info, alm_info):
1✔
816
    if sht_info.is_CAR:
1✔
817
        raise ValueError("Can't use healpy for CAR maps")
1✔
818
    kwargs = {'lmax': alm_info.lmax, 'mmax': alm_info.mmax}
1✔
819
    if spin == 0:
1✔
820
        map = [hp.alm2map(alm[0], nside=sht_info.nside, **kwargs)]
1✔
821
    else:
822
        map = hp.alm2map_spin(alm, sht_info.nside, spin, **kwargs)
1✔
823
    return np.array(map)
1✔
824

825

826
def _map2alm_healpy(map, spin, sht_info, alm_info):
1✔
827
    if sht_info.is_CAR:
1✔
828
        raise ValueError("Can't use healpy for CAR maps")
1✔
829
    kwargs = {'lmax': alm_info.lmax, 'mmax': alm_info.mmax}
1✔
830
    if spin == 0:
1✔
831
        alm = [hp.map2alm(map[0], iter=0, **kwargs)]
1✔
832
    else:
833
        alm = hp.map2alm_spin(map, spin, **kwargs)
1✔
834
    return np.array(alm)
1✔
835

836

837
_m2a_d = {'ducc': _map2alm_ducc0,
1✔
838
          'healpy': _map2alm_healpy}
839
_a2m_d = {'ducc': _alm2map_ducc0,
1✔
840
          'healpy': _alm2map_healpy}
841

842

843
def _catalog2alm_ducc0(values, positions, spin, lmax):
1✔
NEW
844
    if values.ndim == 1:
×
NEW
845
        values = values.reshape((1, -1))
×
NEW
846
    alm = ducc0.sht.adjoint_synthesis_general(lmax=lmax, map=values,
×
847
                                              loc=positions.T, spin=int(spin))
NEW
848
    return alm
×
849

850

851
def map2alm(map, spin, map_info, alm_info, *, n_iter):
1✔
852
    """ Computes the spherical harmonic transform (SHT)
853
    for a set of input maps. The SHT implementation to
854
    be used can be selected via :meth:`set_sht_calculator`
855
    (see the documentation of :class:`NmtParams`).
856

857
    Args:
858
        map (`array`): 2D array with shape ``(nmaps, npix)``
859
            where  ``nmaps`` is either 1 (for spin-0 fields)
860
            or 2 (for spin-s fields), and ``npix`` is the
861
            number of pixels. If using CAR rectangular
862
            pixels, the map should be provided flattened.
863
        spin (:obj:`int`): Field spin.
864
        map_info (:class:`NmtMapInfo`): Object describing
865
            the pixelization of the input map.
866
        alm_info (:class:`NmtAlmInfo`): Object describing
867
            the structure of the output spherical harmonic
868
            coefficients.
869
        n_iter (:obj:`int`): Number of Jacobi iterations used
870
            to improve the accuracy of the SHT.
871

872
    Returns:
873
        (`array`): Harmonic coefficients :math:`a_{\\ell m}` \
874
            of the input map. A set of two arrays (E and B \
875
            modes) is returned if ``spin>0``.
876
    """
877
    map = map_info.si.pad_map(map)
1✔
878
    m2a = _m2a_d[nmt_params.sht_calculator]
1✔
879
    a2m = _a2m_d[nmt_params.sht_calculator]
1✔
880
    alm = m2a(map, spin, map_info.si, alm_info)
1✔
881
    for i in range(n_iter):
1✔
882
        dmap = a2m(alm, spin, map_info.si, alm_info)-map
1✔
883
        alm -= m2a(dmap, spin, map_info.si, alm_info)
1✔
884
    return alm
1✔
885

886

887
def alm2map(alm, spin, map_info, alm_info):
1✔
888
    """ Computes the inverse spherical harmonic transform
889
    (SHT) for a set of input :math:`a_{\\ell m}` s. The SHT
890
    implementation to be used can be selected via
891
    :meth:`set_sht_calculator` (see the documentation of
892
    :class:`NmtParams`).
893

894
    Args:
895
        alm (`array`): 2D array with shape ``(nmaps, n_lm)``
896
            where  ``nmaps`` is either 1 (for spin-0 fields)
897
            or 2 (for spin-s fields), and ``n_lm`` is the
898
            number of harmonic coefficients.
899
        spin (:obj:`int`): Field spin.
900
        map_info (:class:`NmtMapInfo`): Object describing
901
            the pixelization of the output map.
902
        alm_info (:class:`NmtAlmInfo`): Object describing
903
            the structure of the input spherical harmonic
904
            coefficients.
905

906
    Returns:
907
        (`array`): Map reconstructed from the input \
908
            :math:`a_{\\ell m}` s. A set of two arrays \
909
            (e.g. Q and U Stokes parameters) is returned \
910
            if ``spin>0``.
911
    """
912
    a2m = _a2m_d[nmt_params.sht_calculator]
1✔
913
    map = a2m(alm, spin, map_info.si, alm_info)
1✔
914
    return map_info.si.unpad_map(map)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc