• 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

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

6

7
class NmtField(object):
1✔
8
    """ An :obj:`NmtField` object contains all the information
9
    describing the fields to correlate, including their observed
10
    maps, masks and contaminant templates.
11

12
    Args:
13
        mask (`array`): Array containing a map corresponding to the
14
            field's mask. Should be 1-dimensional for a HEALPix map or
15
            2-dimensional for a map with rectangular (CAR) pixelization.
16
        maps (`array`): Array containing the observed maps for this
17
            field. Should be at least 2-dimensional. The first
18
            dimension corresponds to the number of maps, which should
19
            be 1 for a spin-0 field and 2 otherwise. The other
20
            dimensions should be either ``[npix]`` for HEALPix maps or
21
            ``[ny,nx]`` for maps with rectangular pixels (with the
22
            `y` and `x` dimensions corresponding to latitude and longitude
23
            respectively). For a spin>0 field, the two maps passed should
24
            be the usual Q/U Stokes parameters for polarization, or
25
            :math:`e_1`/:math:`e_2` (:math:`\\gamma_1`/:math:`\\gamma_2` etc.)
26
            in the case of cosmic shear. It is important to note that
27
            NaMaster uses the same polarization convention as HEALPix (i.e.
28
            with the growing colatitude :math:`\\theta`). It is however more
29
            common for galaxy ellipticities to be provided using the IAU
30
            convention (e.g. growing declination). In this case, the sign of
31
            the :math:`e_2`/:math:`\\gamma_2` map should be swapped before
32
            using it to create an :obj:`NmtField`. See more
33
            `here <https://healpix.jpl.nasa.gov/html/intronode12.htm>`_.
34
            If ``None``, this field will only contain a mask but no maps.
35
            The field can then be used to compute a mode-coupling matrix,
36
            for instance, but not actual power spectra.
37
        spin (:obj:`int`): Spin of this field. If ``None`` it will
38
            default to 0 or 2 if ``maps`` contains 1 or 2 maps, respectively.
39
        templates (`array`): Array containing a set of contaminant templates
40
            for this field. This array should have shape ``[ntemp,nmap,...]``,
41
            where ``ntemp`` is the number of templates, ``nmap`` should be 1
42
            for spin-0 fields and 2 otherwise. The other dimensions should be
43
            ``[npix]`` for HEALPix maps or ``[ny,nx]`` for maps with
44
            rectangular pixels. The best-fit contribution from each
45
            contaminant is automatically removed from the maps unless
46
            ``templates=None``.
47
        beam (`array`): Spherical harmonic transform of the instrumental beam
48
            (assumed to be rotationally symmetric - i.e. no :math:`m`
49
            dependence). If ``None``, no beam will be corrected for. Otherwise,
50
            this array should have at least as many elements as the maximum
51
            multipole sampled by the maps + 1 (see ``lmax``).
52
        purify_e (:obj:`bool`): Purify E-modes?
53
        purify_b (:obj:`bool`): Purify B-modes?
54
        n_iter (:obj:`int`): Number of iterations when computing the
55
            :math:`a_{\\ell m}` s of the input maps. See the documentation of
56
            :meth:`~pymaster.utils.map2alm`. If ``None``, it will default to
57
            the internal value (see documentation of
58
            :class:`~pymaster.utils.NmtParams`), which can be accessed via
59
            :meth:`~pymaster.utils.get_default_params`, and modified via
60
            :meth:`~pymaster.utils.set_n_iter_default`.
61
        n_iter_mask (:obj:`int`): Number of iterations when computing the
62
            spherical harmonic transform of the mask. If ``None``, it will
63
            default to the internal value (see documentation of
64
            :class:`~pymaster.utils.NmtParams`), which can be accessed via
65
            :meth:`~pymaster.utils.get_default_params`,
66
            and modified via :meth:`~pymaster.utils.set_n_iter_default`.
67
        lmax (:obj:`int`): Maximum multipole up to which map power spectra
68
            will be computed. If negative or zero, the maximum multipole given
69
            the map resolution will be used (e.g. :math:`3N_{\\rm side}-1`
70
            for HEALPix maps).
71
        lmax_mask (:obj:`int`): Maximum multipole up to which the power
72
            spectrum of the mask will be computed. If negative or zero, the
73
            maximum multipole given the map resolution will be used (e.g.
74
            :math:`3N_{\\rm side}-1` for HEALPix maps).
75
        tol_pinv (:obj:`float`): When computing the pseudo-inverse of the
76
            contaminant covariance matrix. See documentation of
77
            :meth:`~pymaster.utils.moore_penrose_pinvh`. Only relevant if
78
            passing contaminant templates that are likely to be highly
79
            correlated. If ``None``, it will default to the internal value,
80
            which can be accessed via
81
            :meth:`~pymaster.utils.get_default_params`, and modified via
82
            :meth:`~pymaster.utils.set_tol_pinv_default`.
83
        wcs (`WCS`): A WCS object if using rectangular (CAR) pixels (see
84
            `the astropy documentation
85
            <http://docs.astropy.org/en/stable/wcs/index.html>`_).
86
        masked_on_input (:obj:`bool`): Set to ``True`` if the input maps and
87
            templates are already multiplied by the mask. Note that this is
88
            not advisable if you're using purification, as correcting for this
89
            usually incurs inaccuracies around the mask edges that may lead
90
            to significantly biased power spectra.
91
        lite (:obj:`bool`): Set to ``True`` if you want to only store the bare
92
            minimum necessary to run a standard pseudo-:math:`C_\\ell` with
93
            deprojection and purification, but you don't care about
94
            deprojection bias. This will significantly reduce the memory taken
95
            up by the resulting object.
96
    """
97
    def __init__(self, mask, maps, *, spin=None, templates=None, beam=None,
1✔
98
                 purify_e=False, purify_b=False, n_iter=None, n_iter_mask=None,
99
                 tol_pinv=None, wcs=None, lmax=-1, lmax_mask=-1,
100
                 masked_on_input=False, lite=False):
101
        if n_iter_mask is None:
1✔
102
            n_iter_mask = ut.nmt_params.n_iter_mask_default
1✔
103
        if n_iter is None:
1✔
104
            n_iter = ut.nmt_params.n_iter_default
1✔
105
        if tol_pinv is None:
1✔
106
            tol_pinv = ut.nmt_params.tol_pinv_default
1✔
107

108
        # 0. Preliminary initializations
109
        # These first attributes are compulsory for all fields
110
        self.lite = lite
1✔
111
        self.mask = None
1✔
112
        self.beam = None
1✔
113
        self.n_iter = n_iter
1✔
114
        self.n_iter_mask = n_iter_mask
1✔
115
        self.pure_e = purify_e
1✔
116
        self.pure_b = purify_b
1✔
117
        # The alm is only required for non-mask-only maps
118
        self.alm = None
1✔
119
        # The remaining attributes are only required for non-lite maps
120
        self.maps = None
1✔
121
        self.temp = None
1✔
122
        self.alm_temp = None
1✔
123
        # The mask alms are only stored if computed for non-lite maps
124
        self.alm_mask = None
1✔
125
        self.n_temp = 0
1✔
126
        self.Nw = 0
1✔
127
        self.field_noise = 0
1✔
128

129
        # 1. Store mask and beam
130
        # This ensures the mask will have the right type
131
        # and endianness (can cause issues when read from
132
        # some FITS files).
133
        mask = mask.astype(np.float64)
1✔
134
        self.minfo = ut.NmtMapInfo(wcs, mask.shape)
1✔
135
        self.mask = self.minfo.reform_map(mask)
1✔
136
        if lmax <= 0:
1✔
137
            lmax = self.minfo.get_lmax()
1✔
138
        if lmax_mask <= 0:
1✔
139
            lmax_mask = self.minfo.get_lmax()
1✔
140
        self.ainfo = ut.NmtAlmInfo(lmax)
1✔
141
        self.ainfo_mask = ut.NmtAlmInfo(lmax_mask)
1✔
142

143
        # Beam
144
        if isinstance(beam, (list, tuple, np.ndarray)):
1✔
145
            if len(beam) <= lmax:
1✔
146
                raise ValueError("Input beam must have at least %d elements "
1✔
147
                                 "given the input map resolution" % (lmax+1))
148
            beam_use = beam
1✔
149
        else:
150
            if beam is None:
1✔
151
                beam_use = np.ones(lmax+1)
1✔
152
            else:
153
                raise ValueError("Input beam can only be an array or None\n")
1✔
154
        self.beam = beam_use
1✔
155

156
        # If mask-only, just return
157
        if maps is None:
1✔
158
            if spin is None:
1✔
159
                raise ValueError("Please supply field spin")
1✔
160
            self.spin = spin
1✔
161
            self.nmaps = 2 if spin else 1
1✔
162
            return
1✔
163

164
        # 2. Check maps
165
        # As for the mask, ensure dtype is float to avoid
166
        # issues when reading the map from a fits file
167
        maps = np.array(maps, dtype=np.float64)
1✔
168
        if (len(maps) != 1) and (len(maps) != 2):
1✔
169
            raise ValueError("Must supply 1 or 2 maps per field")
1✔
170

171
        if spin is None:
1✔
172
            if len(maps) == 1:
1✔
173
                spin = 0
1✔
174
            else:
175
                spin = 2
1✔
176
        else:
177
            if (((spin != 0) and len(maps) == 1) or
1✔
178
                    ((spin == 0) and len(maps) != 1)):
179
                raise ValueError("Spin-zero fields are "
1✔
180
                                 "associated with a single map")
181
        self.spin = spin
1✔
182
        self.nmaps = 2 if spin else 1
1✔
183

184
        maps = self.minfo.reform_map(maps)
1✔
185

186
        pure_any = self.pure_e or self.pure_b
1✔
187
        if pure_any and self.spin != 2:
1✔
188
            raise ValueError("Purification only implemented for spin-2 fields")
1✔
189

190
        # 3. Check templates
191
        if isinstance(templates, (list, tuple, np.ndarray)):
1✔
192
            templates = np.array(templates, dtype=np.float64)
1✔
193
            if (len(templates[0]) != len(maps)):
1✔
194
                raise ValueError("Each template must have the same number of "
1✔
195
                                 "components as the map.")
196
            templates = self.minfo.reform_map(templates)
1✔
197
            self.n_temp = len(templates)
1✔
198
        else:
199
            if templates is not None:
1✔
200
                raise ValueError("Input templates can only be an array "
1✔
201
                                 "or None")
202
        w_temp = templates is not None
1✔
203

204
        # 4. Temporarily store unmasked maps if purifying
205
        maps_unmasked = None
1✔
206
        temp_unmasked = None
1✔
207
        if pure_any:
1✔
208
            maps_unmasked = np.array(maps)
1✔
209
            if w_temp:
1✔
210
                temp_unmasked = np.array(templates)
1✔
211
            if masked_on_input:
1✔
212
                good = self.mask > 0
1✔
213
                goodm = self.mask[good]
1✔
214
                maps_unmasked[:, good] /= goodm[None, :]
1✔
215
                if w_temp:
1✔
216
                    temp_unmasked[:, :, good] /= goodm[None, None, :]
1✔
217

218
        # 5. Mask all maps
219
        if w_temp:
1✔
220
            templates = np.array(templates)
1✔
221
        if not masked_on_input:
1✔
222
            maps *= self.mask[None, :]
1✔
223
            if w_temp:
1✔
224
                templates *= self.mask[None, None, :]
1✔
225

226
        # 6. Compute template normalization matrix and deproject
227
        if w_temp:
1✔
228
            M = np.array([[self.minfo.si.dot_map(t1, t2)
1✔
229
                           for t1 in templates]
230
                          for t2 in templates])
231
            iM = ut.moore_penrose_pinvh(M, tol_pinv)
1✔
232
            prods = np.array([self.minfo.si.dot_map(t, maps)
1✔
233
                              for t in templates])
234
            alphas = np.dot(iM, prods)
1✔
235
            self.iM = iM
1✔
236
            self.alphas = alphas
1✔
237
            maps = maps - np.sum(alphas[:, None, None]*templates,
1✔
238
                                 axis=0)
239
            # Do it also for unmasked maps if needed
240
            if pure_any:
1✔
241
                maps_unmasked = maps_unmasked - \
1✔
242
                    np.sum(alphas[:, None, None]*temp_unmasked, axis=0)
243

244
        # 7. Compute alms
245
        # - If purifying, do so at the same time.
246
        alm_temp = None
1✔
247
        alm_mask = None
1✔
248
        if pure_any:
1✔
249
            task = [self.pure_e, self.pure_b]
1✔
250
            alm_mask = self.get_mask_alms()
1✔
251
            self.alm, maps = self._purify(self.mask, alm_mask, maps_unmasked,
1✔
252
                                          n_iter=n_iter, task=task)
253
            if w_temp and (not self.lite):
1✔
254
                alm_temp = np.array([self._purify(self.mask, alm_mask, t,
1✔
255
                                                  n_iter=n_iter,
256
                                                  task=task)[0]
257
                                     for t in temp_unmasked])
258
                # IMPORTANT: at this stage, maps and self.alm contain the
259
                # purified map and SH coefficients. However, although alm_temp
260
                # contains the purified SH coefficients, templates contains the
261
                # ***non-purified*** maps. This is to speed up the calculation
262
                # of the deprojection bias.
263
        else:
264
            self.alm = ut.map2alm(maps, self.spin, self.minfo,
1✔
265
                                  self.ainfo, n_iter=n_iter)
266
            if w_temp and (not self.lite):
1✔
267
                alm_temp = np.array([ut.map2alm(t, self.spin, self.minfo,
1✔
268
                                                self.ainfo, n_iter=n_iter)
269
                                     for t in templates])
270

271
        # 8. Store any additional information needed
272
        if not self.lite:
1✔
273
            self.maps = maps.copy()
1✔
274
            if w_temp:
1✔
275
                self.temp = templates.copy()
1✔
276
                self.alm_temp = alm_temp
1✔
277

278
    def is_compatible(self, other):
1✔
279
        """ Returns ``True`` if the pixelization of this :obj:`NmtField`
280
        is compatible with that of another one (``other``).
281
        """
282
        if self.minfo != other.minfo:
1✔
283
            return False
1✔
284
        if self.ainfo != other.ainfo:
1✔
285
            return False
1✔
286
        if self.ainfo_mask != other.ainfo_mask:
1✔
287
            return False
1✔
288
        return True
1✔
289

290
    def get_mask(self):
1✔
291
        """ Get this field's mask.
292

293
        Returns:
294
            (`array`): 1D array containing the field's mask.
295
        """
296
        return self.mask
1✔
297

298
    def get_mask_alms(self):
1✔
299
        """ Get the :math:`a_{\\ell m}` coefficients of this field's mask.
300
        Note that, in most cases, the mask :math:`a_{\\ell n}` s are not
301
        computed when generating the field. When calling this function for
302
        the first time, if they have not been calculated, they will be
303
        (which may be a slow operation), and stored internally for any future
304
        calls.
305

306
        Returns:
307
            (`array`): 1D array containing the mask :math:`a_{\\ell m}` s.
308
        """
309
        if self.alm_mask is None:
1✔
310
            amask = ut.map2alm(np.array([self.mask]), 0,
1✔
311
                               self.minfo, self.ainfo_mask,
312
                               n_iter=self.n_iter_mask)[0]
313
            if not self.lite:  # Store while we're at it
1✔
314
                self.alm_mask = amask
1✔
315
        else:
316
            amask = self.alm_mask
1✔
317
        return amask
1✔
318

319
    def get_maps(self):
1✔
320
        """ Get this field's set of maps. The returned maps will be
321
        masked, contaminant-deprojected, and purified (if so required
322
        when generating this :obj:`NmtField`).
323

324
        Returns:
325
            (`array`): 2D array containing the field's maps.
326
        """
327
        if self.maps is None:
1✔
328
            raise ValueError("Input maps unavailable for this field")
1✔
329
        return self.maps
1✔
330

331
    def get_alms(self):
1✔
332
        """ Get the :math:`a_{\\ell m}` coefficients of this field. They
333
        include the effects of masking, as well as contaminant deprojection
334
        and purification (if required when generating this
335
        :obj:`NmtField`).
336

337
        Returns:
338
            (`array`): 2D array containing the field's :math:`a_{\\ell m}` s.
339
        """
340
        if self.alm is None:
1✔
341
            raise ValueError("Mask-only fields have no alms")
1✔
342
        return self.alm
1✔
343

344
    def get_templates(self):
1✔
345
        """ Get this field's set of contaminant templates maps. The
346
        returned maps will have the mask applied to them.
347

348
        Returns:
349
            (`array`): 3D array containing the field's contaminant maps.
350
        """
351
        if self.temp is None:
1✔
352
            raise ValueError("Input templates unavailable for this field")
1✔
353
        return self.temp
1✔
354

355
    def _purify(self, mask, alm_mask, maps_u, n_iter, task=[False, True],
1✔
356
                return_maps=True):
357
        # 1. Spin-0 mask bit
358
        # Multiply by mask
359
        maps = maps_u*mask[None, :]
1✔
360
        # Compute alms
361
        alms = ut.map2alm(maps, 2, self.minfo,
1✔
362
                          self.ainfo_mask, n_iter=n_iter)
363

364
        # 2. Spin-1 mask bit
365
        # Compute spin-1 mask
366
        ls = np.arange(self.ainfo_mask.lmax+1)
1✔
367
        # The minus sign is because of the definition of E-modes
368
        fl = -np.sqrt((ls+1.0)*ls)
1✔
369
        walm = hp.almxfl(alm_mask, fl,
1✔
370
                         mmax=self.ainfo_mask.mmax)
371
        walm = np.array([walm, walm*0])
1✔
372
        wmap = ut.alm2map(walm, 1, self.minfo, self.ainfo_mask)
1✔
373
        # Product with spin-1 mask
374
        maps = np.array([wmap[0]*maps_u[0]+wmap[1]*maps_u[1],
1✔
375
                         wmap[0]*maps_u[1]-wmap[1]*maps_u[0]])
376
        # Compute SHT, multiply by
377
        # 2*sqrt((l+1)!(l-2)!/((l-1)!(l+2)!)) and add to alms
378
        palm = ut.map2alm(maps, 1, self.minfo,
1✔
379
                          self.ainfo, n_iter=n_iter)
380
        fl[2:] = 2/np.sqrt((ls[2:]+2.0)*(ls[2:]-1.0))
1✔
381
        fl[:2] = 0
1✔
382
        for ipol, purify in enumerate(task):
1✔
383
            if purify:
1✔
384
                alms[ipol] += hp.almxfl(palm[ipol],
1✔
385
                                        fl[:self.ainfo.lmax+1],
386
                                        mmax=self.ainfo.mmax)
387

388
        # 3. Spin-2 mask bit
389
        # Compute spin-0 mask
390
        # The extra minus sign is because of the scalar SHT below
391
        # (E-mode definition for spin=0).
392
        fl[2:] = -np.sqrt((ls[2:]+2.0)*(ls[2:]-1.0))
1✔
393
        fl[:2] = 0
1✔
394
        walm[0] = hp.almxfl(walm[0], fl, mmax=self.ainfo_mask.mmax)
1✔
395
        wmap = ut.alm2map(walm, 2, self.minfo, self.ainfo_mask)
1✔
396
        # Product with spin-2 mask
397
        maps = np.array([wmap[0]*maps_u[0]+wmap[1]*maps_u[1],
1✔
398
                         wmap[0]*maps_u[1]-wmap[1]*maps_u[0]])
399
        # Compute SHT, multiply by
400
        # sqrt((l-2)!/(l+2)!) and add to alms
401
        palm = np.array([ut.map2alm(np.array([m]), 0, self.minfo,
1✔
402
                                    self.ainfo, n_iter=n_iter)[0]
403
                         for m in maps])
404
        fl[2:] = 1/np.sqrt((ls[2:]+2.0)*(ls[2:]+1.0) *
1✔
405
                           ls[2:]*(ls[2:]-1))
406
        fl[:2] = 0
1✔
407
        for ipol, purify in enumerate(task):
1✔
408
            if purify:
1✔
409
                alms[ipol] += hp.almxfl(palm[ipol],
1✔
410
                                        fl[:self.ainfo.lmax+1],
411
                                        mmax=self.ainfo.mmax)
412

413
        if return_maps:
1✔
414
            # 4. Compute purified map if needed
415
            maps = ut.alm2map(alms, 2, self.minfo, self.ainfo)
1✔
416
            return alms, maps
1✔
417
        return alms
1✔
418

419

420
class NmtFieldFlat(object):
1✔
421
    """ An :obj:`NmtFieldFlat` object contains all the information
422
    describing the flat-sky fields to correlate, including their observed
423
    maps, masks and contaminant templates.
424

425
    Args:
426
        lx (:obj:`float`): Size of the patch in the `x` direction (in
427
            radians).
428
        ly (:obj:`float`): Size of the patch in the `y` direction (in
429
            radians).
430
        mask (`array`): 2D array of shape ``(ny, nx)`` containing the
431
            field's mask.
432
        maps (`array`): Array containing the observed maps for this
433
            field. Should be at 3-dimensional. The first dimension
434
            corresponds to the number of maps, which should be 1 for a
435
            spin-0 field and 2 otherwise. The other dimensions should be
436
            ``(ny, nx)``. If ``None``, this field will only contain a
437
            mask but no maps. The field can then be used to compute a
438
            mode-coupling matrix, for instance, but not actual power
439
            spectra.
440
        spin (:obj:`int`): Spin of this field. If ``None`` it will
441
            default to 0 or 2 if ``maps`` contains 1 or 2 maps, respectively.
442
        templates (`array`): Array containing a set of contaminant templates
443
            for this field. This array should have shape
444
            ``[ntemp,nmap,ny,nx]``, where ``ntemp`` is the number of
445
            templates, and ``nmap`` should be 1 for spin-0 fields and, 2
446
            otherwise. The best-fit contribution from each contaminant is
447
            automatically removed from the maps unless ``templates=None``.
448
        beam (`array`): Spherical harmonic transform of the instrumental beam
449
            (assumed to be rotationally symmetric). Should be 2D, with shape
450
            ``(2,nl)``. ``beam[0]`` contains the values of :math:`\\ell` for
451
            which de beam is defined, with ``beam[1]`` containing the
452
            corresponding beam values. If None, no beam will be corrected for.
453
        purify_e (:obj:`bool`): Purify E-modes?
454
        purify_b (:obj:`bool`): Purify B-modes?
455
        tol_pinv (:obj:`float`): When computing the pseudo-inverse of the
456
            contaminant covariance matrix. See documentation of
457
            :meth:`~pymaster.utils.moore_penrose_pinvh`. Only relevant if
458
            passing contaminant templates that are likely to be highly
459
            correlated. If ``None``, it will default to the internal value,
460
            which can be accessed via
461
            :meth:`~pymaster.utils.get_default_params`, and modified via
462
            :meth:`~pymaster.utils.set_tol_pinv_default`.
463
        masked_on_input (:obj:`bool`): Set to ``True`` if the input maps and
464
            templates are already multiplied by the mask. Note that this is
465
            not advisable if you're using purification, as correcting for this
466
            usually incurs inaccuracies around the mask edges that may lead
467
            to significantly biased power spectra.
468
        lite (:obj:`bool`): Set to ``True`` if you want to only store the bare
469
            minimum necessary to run a standard pseudo-:math:`C_\\ell` with
470
            deprojection and purification, but you don't care about
471
            deprojection bias. This will significantly reduce the memory taken
472
            up by the resulting object.
473
    """
474
    def __init__(self, lx, ly, mask, maps, spin=None, templates=None,
1✔
475
                 beam=None, purify_e=False, purify_b=False,
476
                 tol_pinv=None, masked_on_input=False, lite=False):
477
        self.fl = None
1✔
478

479
        if tol_pinv is None:
1✔
480
            tol_pinv = ut.nmt_params.tol_pinv_default
1✔
481

482
        pure_e = 0
1✔
483
        if purify_e:
1✔
484
            pure_e = 1
1✔
485
        pure_b = 0
1✔
486
        if purify_b:
1✔
487
            pure_b = 1
1✔
488
        masked_input = 0
1✔
489
        if masked_on_input:
1✔
490
            masked_input = 1
1✔
491

492
        if (lx < 0) or (ly < 0):
1✔
493
            raise ValueError("Must supply sensible dimensions for "
1✔
494
                             "flat-sky field")
495
        # Flatten arrays and check dimensions
496
        shape_2D = np.shape(mask)
1✔
497
        self.ny = shape_2D[0]
1✔
498
        self.nx = shape_2D[1]
1✔
499

500
        if maps is None:
1✔
501
            mask_only = True
1✔
502
            if spin is None:
1✔
503
                raise ValueError("Please supply field spin")
1✔
504
            lite = True
1✔
505
        else:
506
            mask_only = False
1✔
507
            # As in the curved case, to ensure right type and endianness (and
508
            # solve the problems when reading it from a fits file)
509
            maps = np.array(maps, dtype=np.float64)
1✔
510

511
            nmaps = len(maps)
1✔
512
            if (nmaps != 1) and (nmaps != 2):
1✔
513
                raise ValueError("Must supply 1 or 2 maps per field")
1✔
514

515
            if spin is None:
1✔
516
                if nmaps == 1:
1✔
517
                    spin = 0
1✔
518
                else:
519
                    spin = 2
1✔
520
            else:
521
                if (((spin != 0) and nmaps == 1) or
1✔
522
                        ((spin == 0) and nmaps != 1)):
523
                    raise ValueError("Spin-zero fields are "
1✔
524
                                     "associated with a single map")
525

526
        if (pure_e or pure_b) and spin != 2:
1✔
527
            raise ValueError("Purification only implemented for spin-2 fields")
1✔
528

529
        # Flatten mask
530
        msk = (mask.astype(np.float64)).flatten()
1✔
531

532
        if (not mask_only):
1✔
533
            # Flatten maps
534
            mps = []
1✔
535
            for m in maps:
1✔
536
                if np.shape(m) != shape_2D:
1✔
537
                    raise ValueError("Mask and maps don't have the same shape")
1✔
538
                mps.append((m.astype(np.float64)).flatten())
1✔
539
            mps = np.array(mps)
1✔
540

541
        # Flatten templates
542
        if isinstance(templates, (list, tuple, np.ndarray)):
1✔
543
            tmps = []
1✔
544
            for t in templates:
1✔
545
                tmp = []
1✔
546
                if len(t) != nmaps:
1✔
547
                    raise ValueError("Maps and templates should have the "
1✔
548
                                     "same number of maps")
549
                for m in t:
1✔
550
                    if np.shape(m) != shape_2D:
1✔
551
                        raise ValueError("Mask and templates don't have "
1✔
552
                                         "the same shape")
553
                    tmp.append((m.astype(np.float64)).flatten())
1✔
554
                tmps.append(tmp)
1✔
555
            tmps = np.array(tmps)
1✔
556
        else:
557
            if templates is not None:
1✔
558
                raise ValueError("Input templates can only be an array "
1✔
559
                                 "or None")
560

561
        # Form beam
562
        if isinstance(beam, (list, tuple, np.ndarray)):
1✔
563
            beam_use = beam
1✔
564
        else:
565
            if beam is None:
1✔
566
                beam_use = np.array([[-1.], [-1.]])
1✔
567
            else:
568
                raise ValueError("Input beam can only be an array or "
1✔
569
                                 "None")
570

571
        if mask_only:
1✔
572
            self.fl = lib.field_alloc_empty_flat(self.nx, self.ny,
1✔
573
                                                 lx, ly, spin,
574
                                                 msk, beam_use,
575
                                                 pure_e, pure_b)
576
        else:
577
            # Generate field
578
            if isinstance(templates, (list, tuple, np.ndarray)):
1✔
579
                self.fl = lib.field_alloc_new_flat(self.nx, self.ny, lx, ly,
1✔
580
                                                   spin, msk, mps, tmps,
581
                                                   beam_use, pure_e, pure_b,
582
                                                   tol_pinv, masked_input,
583
                                                   int(lite))
584
            else:
585
                self.fl = lib.field_alloc_new_notemp_flat(self.nx, self.ny,
1✔
586
                                                          lx, ly, spin,
587
                                                          msk, mps,
588
                                                          beam_use, pure_e,
589
                                                          pure_b, masked_input,
590
                                                          int(lite))
591
        self.lite = lite
1✔
592

593
    def __del__(self):
1✔
594
        if self.fl is not None:
1✔
595
            if lib.field_flat_free is not None:
1✔
596
                lib.field_flat_free(self.fl)
1✔
597
            self.fl = None
1✔
598

599
    def get_mask(self):
1✔
600
        """ Returns this field's mask as a 2D array with shape
601
        ``(ny, nx)``.
602

603
        Returns:
604
            (`array`): Mask.
605
        """
606
        msk = lib.get_mask_flat(self.fl,
1✔
607
                                int(self.fl.npix)).reshape([self.ny,
608
                                                            self.nx])
609

610
        return msk
1✔
611

612
    def get_maps(self):
1✔
613
        """ Returns a 3D array with shape ``(nmap, ny, nx)`` corresponding
614
        to the observed maps for this field. If the field was initialized
615
        with contaminant templates, the maps returned by this function
616
        have their best-fit contribution from these contaminants removed.
617

618
        Returns:
619
            (`array`): Maps.
620
        """
621
        if self.lite:
1✔
622
            raise ValueError("Input maps unavailable for lightweight fields. "
1✔
623
                             "To use this function, create an `NmtFieldFlat` "
624
                             "object with `lite=False`.")
625
        maps = np.zeros([self.fl.nmaps, self.fl.npix])
1✔
626
        for imap in range(self.fl.nmaps):
1✔
627
            maps[imap, :] = lib.get_map_flat(self.fl, imap, int(self.fl.npix))
1✔
628
        mps = maps.reshape([self.fl.nmaps, self.ny, self.nx])
1✔
629

630
        return mps
1✔
631

632
    def get_templates(self):
1✔
633
        """ Returns a 4D array with shape ``(ntemp, nmap, ny, nx)``,
634
        corresponding to the contaminant templates passed when initializing
635
        this field.
636

637
        Return:
638
            (`array`): Contaminant maps.
639
        """
640
        if self.lite:
1✔
641
            raise ValueError("Input maps unavailable for lightweight fields. "
1✔
642
                             "To use this function, create an `NmtFieldFlat` "
643
                             "object with `lite=False`.")
644
        temp = np.zeros([self.fl.ntemp, self.fl.nmaps, self.fl.npix])
1✔
645
        for itemp in range(self.fl.ntemp):
1✔
646
            for imap in range(self.fl.nmaps):
1✔
647
                temp[itemp, imap, :] = lib.get_temp_flat(
1✔
648
                    self.fl, itemp, imap, int(self.fl.npix)
649
                )
650
        tmps = temp.reshape([self.fl.ntemp, self.fl.nmaps, self.ny, self.nx])
1✔
651

652
        return tmps
1✔
653

654
    def get_ell_sampling(self):
1✔
655
        """ Returns the finest :math:`\\ell` sampling at which intermediate
656
        power spectra calculated involving this field are evaluated.
657

658
        Return:
659
            (`array`): Array of :math:`\\ell` values.
660
        """
661
        ells = lib.get_ell_sampling_flat(self.fl, int(self.fl.fs.n_ell))
1✔
662
        return ells
1✔
663

664

665
class NmtFieldCatalog(NmtField):
1✔
666
    """
667
    """
668
    def __init__(self, positions, weights, field, lmax, lmax_mask=-1,
1✔
669
                 spin=None, beam=None, field_is_weighted=False, lonlat=False):
670
        # 0. Preliminary initializations
NEW
671
        if ut.HAVE_DUCC:
×
NEW
672
            self.sht_calculator = 'ducc'
×
673
        else:
NEW
674
            raise ValueError("DUCC is needed, but currently not installed.")
×
675

676
        # These first attributes are compulsory for all fields
NEW
677
        self.lite = True
×
NEW
678
        self.mask = None
×
NEW
679
        self.beam = None
×
NEW
680
        self.n_iter = None
×
NEW
681
        self.n_iter_mask = None
×
NEW
682
        self.pure_e = False
×
NEW
683
        self.pure_b = False
×
NEW
684
        self.Nw = 0.
×
NEW
685
        self.field_noise = 0.
×
686

687
        # The remaining attributes are only required for non-lite maps
NEW
688
        self.maps = None
×
NEW
689
        self.temp = None
×
NEW
690
        self.alm_temp = None
×
NEW
691
        self.minfo = None
×
NEW
692
        self.n_temp = 0
×
693

694
        # Sanity checks on positions and weights
NEW
695
        self.positions = np.array(positions, dtype=np.float64)
×
NEW
696
        self.weights = np.array(weights, dtype=np.float64)
×
NEW
697
        if np.shape(self.positions) != (2, len(self.weights)):
×
NEW
698
            raise ValueError("Positions must be 2D array of shape"
×
699
                             " (2, len(weights)).")
NEW
700
        if not lonlat and not (np.logical_and(self.positions[0] > 0.,
×
701
                               self.positions[0] < np.pi)).all():
NEW
702
            raise ValueError("First dimension of positions must be colatitude"
×
703
                             " in radians, between 0 and pi.")
NEW
704
        if not lonlat and not (np.logical_and(self.positions[1] > 0.,
×
705
                               self.positions[1] < 2.*np.pi)).all():
NEW
706
            raise ValueError("Second dimension of positions must be longitude"
×
707
                             " in radians, between 0 and 2*pi.")
NEW
708
        if lonlat and not (np.logical_and(self.positions[0] > 0.,
×
709
                                          self.positions[0] < 360.)).all():
NEW
710
            raise ValueError("First dimension of positions must be longitude"
×
711
                             " in degrees, between 0 and 360.")
NEW
712
        if lonlat and not (np.logical_and(self.positions[1] > -90.,
×
713
                                          self.positions[1] < 90.)).all():
NEW
714
            raise ValueError("Second dimension of positions must be latitude"
×
715
                             " in degree, between -90 and 90.")
NEW
716
        if lonlat:
×
NEW
717
            self.positions[0] = np.radians(self.positions[1] + 90.)
×
NEW
718
            self.positions[1] = np.radians(self.positions[0])
×
719

720
        # 1. Compute mask alms and beam
721
        # Sanity checks
NEW
722
        if lmax_mask <= 0:
×
NEW
723
            lmax_mask = 2*lmax
×
NEW
724
        self.ainfo_mask = ut.NmtAlmInfo(lmax_mask)
×
725
        # Mask alms
NEW
726
        self.alm_mask = ut._catalog2alm_ducc0(self.weights, self.positions,
×
727
                                              spin=0, lmax=lmax_mask)[0]
728
        # Beam
NEW
729
        if isinstance(beam, (list, tuple, np.ndarray)):
×
NEW
730
            if len(beam) <= lmax:
×
NEW
731
                raise ValueError("Input beam must have at least %d elements "
×
732
                                 "given the input maximum "
733
                                 "multipole" % (lmax+1))
NEW
734
            beam_use = beam
×
735
        else:
NEW
736
            if beam is None:
×
NEW
737
                beam_use = np.ones(lmax+1)
×
738
            else:
NEW
739
                raise ValueError("Input beam can only be an array or None\n")
×
NEW
740
        self.beam = beam_use
×
741

742
        # 2. Compute field alms
743
        # If only positions and weights, just return here
NEW
744
        if field is None:
×
NEW
745
            if lmax <= 0:
×
NEW
746
                raise ValueError("If field is None, lmax needs to be "
×
747
                                 "provided.")
NEW
748
            if spin is None:
×
NEW
749
                raise ValueError("If field is None, spin needs to be "
×
750
                                 "provided.")
NEW
751
            self.ainfo = ut.NmtAlmInfo(lmax)
×
NEW
752
            self.spin = spin
×
NEW
753
            return
×
NEW
754
        self.ainfo = ut.NmtAlmInfo(lmax)
×
NEW
755
        self.field = np.array(field, dtype=np.float64)
×
756
        # Sanity checks
NEW
757
        if spin is None:
×
NEW
758
            spin = 0 if field.ndim == 1 else 2
×
NEW
759
        if spin == 2 and np.shape(self.field) != (2, len(self.weights)):
×
NEW
760
            raise ValueError("Field has wrong shape.")
×
NEW
761
        if spin == 0 and (self.field.ndim != 1
×
762
                          or len(self.field) != len(self.weights)):
NEW
763
            raise ValueError("Field has wrong shape.")
×
NEW
764
        self.spin = spin
×
NEW
765
        self.nmaps = 2 if spin else 1
×
NEW
766
        if not field_is_weighted:
×
NEW
767
            self.field *= self.weights
×
NEW
768
        self.alm = ut._catalog2alm_ducc0(self.field, self.positions,
×
769
                                         spin=spin, lmax=lmax)
770

771
        # 3. Compute Poisson and field noise bias on mask pseudo-C_ell
NEW
772
        self.Nw = np.sum(self.weights**2.)/(4.*np.pi)
×
NEW
773
        self.field_noise = np.sum(self.field**2)/(4*np.pi*self.nmaps)
×
774

775
    def get_field_noise(self):
1✔
776
        """ Get the field's total estimated noise variance, assuming the
777
        expected variance at every position is given by the mean square.
778
        """
NEW
779
        return self.field_noise
×
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