qpoint 1.13.0__cp313-cp313-macosx_10_13_universal2.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
qpoint/qmap_class.py ADDED
@@ -0,0 +1,1378 @@
1
+ import numpy as np
2
+ from .qpoint_class import QPoint
3
+ import ctypes as ct
4
+
5
+ from . import _libqpoint as lib
6
+ from ._libqpoint import libqp as qp
7
+
8
+ __all__ = ["QMap", "check_map", "check_proj"]
9
+
10
+
11
+ # healpix bookkeeping
12
+ def nside2npix(nside):
13
+ """
14
+ Convert healpix resolution (nside) to the number of healpix pixels in a full-sky map.
15
+ """
16
+ return 12 * nside * nside
17
+
18
+
19
+ def npix2nside(npix):
20
+ """
21
+ Convert the number of healpix pixels in a full-sky map to healpix resolution (nside).
22
+ """
23
+ nside = np.sqrt(npix / 12.0)
24
+ if nside != np.floor(nside):
25
+ raise ValueError(
26
+ "Invalid number of healpix pixels, must be npix = 12 * nside**2"
27
+ )
28
+ return int(nside)
29
+
30
+
31
+ def check_map(map_in, copy=False, partial=False, dtype=np.double):
32
+ """
33
+ Return a properly transposed and memory-aligned map and its nside.
34
+
35
+ Arguments
36
+ ---------
37
+ map_in : map or list of maps
38
+ Input map(s)
39
+ copy : bool, optional
40
+ If True, ensure that output map does not share memory with
41
+ the input map. Use if you do not want in-place operations to
42
+ modify the map contents.
43
+ partial : bool, optional
44
+ If True, the map is not checked to ensure a proper healpix nside,
45
+ and the number of pixels is returned instead.
46
+
47
+ Returns
48
+ -------
49
+ map_out : numpy.ndarray
50
+ Properly shaped and memory-aligned map, copied from the input
51
+ if necessary.
52
+ nside or npix : int
53
+ If partial is False, the map nside. Otherwise, the number of pixels.
54
+ """
55
+ map_out = np.atleast_2d(map_in)
56
+ if np.argmax(map_out.shape) == 0:
57
+ map_out = map_out.T
58
+ if partial:
59
+ dim2 = len(map_out[0])
60
+ else:
61
+ dim2 = npix2nside(len(map_out[0]))
62
+ map_out = lib.check_input("map", map_out, dtype=dtype)
63
+ if copy and np.may_share_memory(map_in, map_out):
64
+ map_out = map_out.copy()
65
+ return map_out, dim2
66
+
67
+
68
+ def check_proj(proj_in, copy=False, partial=False):
69
+ """
70
+ Return a properly transposed and memory-aligned projection map,
71
+ its nside, and the map dimension.
72
+
73
+ Arguments
74
+ ---------
75
+ proj_in : map or list of maps
76
+ Input projection matrix map
77
+ copy : bool, optional
78
+ If True, ensure that output map does not share memory with
79
+ the input map. Use if you do not want in-place operations to
80
+ modify the map contents.
81
+ partial : bool, optional
82
+ If True, the map is not checked to ensure a proper healpix nside,
83
+ and the number of pixels is returned instead.
84
+
85
+ Returns
86
+ -------
87
+ map_out : numpy.ndarray
88
+ Properly shaped and memory-aligned map, copied from the input
89
+ if necessary.
90
+ nside or npix : int
91
+ If partial is False, the map nside. Otherwise, the number of pixels.
92
+ nmap : int
93
+ The map size this projection matrix is intended to invert, i.e.
94
+ the solution to `len(proj) = nmap * (nmap + 1) / 2`. Raises an
95
+ error if an integer solution is not found.
96
+ """
97
+ proj_out, dim2 = check_map(proj_in, copy=copy, partial=partial)
98
+ nmap = int((np.sqrt(8 * len(proj_out) + 1) - 1)) // 2
99
+ if nmap * (nmap + 1) // 2 != len(proj_out):
100
+ raise ValueError("proj has incompatible shape")
101
+ return proj_out, dim2, nmap
102
+
103
+
104
+ class QMap(QPoint):
105
+ """
106
+ Quaternion-based mapmaker that generates per-channel pointing
107
+ on-the-fly.
108
+ """
109
+
110
+ def __init__(
111
+ self,
112
+ nside=None,
113
+ pol=True,
114
+ vpol=False,
115
+ source_map=None,
116
+ source_pol=True,
117
+ source_vpol=False,
118
+ q_bore=None,
119
+ ctime=None,
120
+ q_hwp=None,
121
+ **kwargs,
122
+ ):
123
+ """
124
+ Initialize the internal structures and data depo for
125
+ mapmaking and/or timestream generation.
126
+
127
+ Arguments
128
+ ---------
129
+ nside : int, optional
130
+ Resolution of the output maps. Default: None
131
+ If None, dest is not initialized.
132
+ pol : bool, optional
133
+ If True, output maps are polarized.
134
+ vpol : bool, optional
135
+ If True, output maps contain V polarization.
136
+ source_map : array_like, optional
137
+ If supplied, passed to :meth:`init_source` to initialize the source
138
+ map structure.
139
+ source_pol : bool, optional
140
+ If True, source_map is polarized. See :meth:`init_source` for
141
+ details.
142
+ source_vpol : bool, optional
143
+ If True, source_map contains V polarization.
144
+ q_bore, ctime, q_hwp : array_like, optional
145
+ Boresight pointing data. See :meth:`init_point` for details. If
146
+ not supplied, the pointing structure is left uninitialized.
147
+
148
+ Notes
149
+ -----
150
+ Remaining keyword arguments are passed to the
151
+ :meth:`~qpoint.qpoint_class.QPoint.set` method.
152
+
153
+ An internal :attr:`depo` dictionary attribute stores the source and output
154
+ maps, timestreams, and pointing data for retrieval by the user.
155
+ Only pointers to these arrays in memory are passed to the C
156
+ library. To ensure that extraneous copies of data are not made,
157
+ supply these methods with C-contiguous arrays of the correct shape.
158
+ """
159
+ super(QMap, self).__init__(**kwargs)
160
+
161
+ self.depo = dict()
162
+ """
163
+ Dictionary of source and output maps, timetreams and pointing data.
164
+ Pointers to these arrays in memory are passed to the C library.
165
+ """
166
+
167
+ self.reset()
168
+
169
+ if nside is not None:
170
+ self.init_dest(nside=nside, pol=pol, vpol=vpol)
171
+
172
+ if source_map is not None:
173
+ self.init_source(source_map, pol=source_pol, vpol=source_vpol)
174
+
175
+ if q_bore is not None:
176
+ self.init_point(q_bore, ctime=ctime, q_hwp=q_hwp)
177
+
178
+ def source_is_init(self):
179
+ """
180
+ Return True if the source map is initialized, otherwise False.
181
+ """
182
+ if not hasattr(self, "_source"):
183
+ return False
184
+ if not self._source.contents.init:
185
+ return False
186
+ return True
187
+
188
+ def init_source(
189
+ self,
190
+ source_map,
191
+ pol=True,
192
+ pixels=None,
193
+ nside=None,
194
+ vpol=False,
195
+ reset=False,
196
+ update=False,
197
+ ):
198
+ """
199
+ Initialize the source map structure. Timestreams are
200
+ produced by scanning this map.
201
+
202
+ Arguments
203
+ ---------
204
+ source_map : array_like
205
+ Input map. Must be of shape `(N, npix)`, where `N` can be
206
+ 1, 3, 6, 9, or 18.
207
+ pol : bool, optional
208
+ If `True`, and the map shape is `(3, npix)`, then input is a
209
+ polarized map (and not T + first derivatives).
210
+ pixels : 1D array_like, optional
211
+ Array of pixel numbers for each map index, if `source_map` is
212
+ a partial map.
213
+ nside : int, optional
214
+ map dimension. If `pixels` is supplied, this argument is required.
215
+ Otherwise, the nside is determined from the input map.
216
+ vpol : bool, optional
217
+ If `True`, and the input map shape is `(4, npix)`, then input is
218
+ a polarized map that includes V polarization.
219
+ reset : bool, optional
220
+ If `True`, and if the structure has already been initialized,
221
+ it is reset and re-initialized with the new map. If `False`,
222
+ a `RuntimeError` is raised if the structure has already been
223
+ initialized.
224
+ update : bool, optional
225
+ If `True`, and if the structure has already been initialized,
226
+ the supplied `source_map` is replaced in the existing source
227
+ structure rather than reinitializing from scratch.
228
+
229
+ Notes
230
+ -----
231
+ This method will automatically determine the type of map
232
+ given its shape. Note that for `N=3`, the `pol` keyword argument
233
+ should be used to disambiguate the two map types. By default,
234
+ a polarized map with `(T,Q,U)` components is assumed.
235
+
236
+ * A map of shape `(1, npix)` or `(npix,)` contains only a `T` map.
237
+ * A map of shape `(3, npix)` contains `(T, Q, U)` if `pol` is `True`,
238
+ or `(T, dTdt, dTdp)` if `pol` is `False`.
239
+ * A map of shape `(4, npix)` contains `(T, Q, U, V)`.
240
+ * A map of shape `(6, npix)` contains `(T, dTdt, dTdp, dT2dt2,
241
+ dT2dpdt, dT2dp2)`.
242
+ * A map of shape `(9, npix)` contains `(T, Q, U, dTdt, dQdt, dUdt,
243
+ dTdp, dQdp, dUdp)`.
244
+ * A map of shape `(18, npix)` contains all the columns of the
245
+ 9-column map, followed by `(dT2dt2, dQ2dt2, dU2dt2, dT2dpdt,
246
+ dQ2dpdt, dU2dpdt, dT2dp2, dQ2dp2, dU2dp2)`.
247
+ """
248
+
249
+ if self.source_is_init():
250
+ if reset:
251
+ self.reset_source()
252
+
253
+ elif update:
254
+ source = self._source.contents
255
+ if (
256
+ source_map.squeeze().shape[-1]
257
+ != self.depo["source_map"].squeeze().shape[-1]
258
+ ):
259
+ raise ValueError("source_map shape mismatch")
260
+ source_map, _ = check_map(source_map, partial=True)
261
+ source.num_vec = len(source_map)
262
+ source.vec_mode = lib.get_vec_mode(source_map, pol, vpol)
263
+ source.vec1d = lib.as_ctypes(source_map.ravel())
264
+ self.depo["source_map"] = source_map
265
+ if qp.qp_reshape_map(self._source):
266
+ raise RuntimeError("Error reshaping source map")
267
+ return
268
+
269
+ else:
270
+ raise RuntimeError("source already initialized")
271
+
272
+ if pixels is None:
273
+ partial = False
274
+ else:
275
+ partial = True
276
+ if nside is None:
277
+ raise ValueError("nside required for partial maps")
278
+
279
+ # check map shape and create pointer
280
+ smap, snside = check_map(source_map, partial=partial)
281
+ if not partial:
282
+ nside = snside
283
+ npix = nside2npix(nside)
284
+ else:
285
+ npix = len(pixels)
286
+
287
+ # store map
288
+ self.depo["source_map"] = smap
289
+ self.depo["source_nside"] = nside
290
+ if partial:
291
+ self.depo["source_pixels"] = pixels
292
+
293
+ # initialize
294
+ source = self._source.contents
295
+ source.partial = partial
296
+ source.nside = nside
297
+ source.npix = npix
298
+ source.pixinfo_init = 0
299
+ source.pixinfo = None
300
+ source.pixhash_init = 0
301
+ source.pixhash = None
302
+ source.num_vec = len(source_map)
303
+ source.vec_mode = lib.get_vec_mode(smap, pol, vpol)
304
+ source.vec1d = lib.as_ctypes(smap.ravel())
305
+ source.vec1d_init = lib.QP_ARR_INIT_PTR
306
+ source.vec = None
307
+ source.vec_init = 0
308
+ source.num_proj = 0
309
+ source.proj_mode = 0
310
+ source.proj = None
311
+ source.proj_init = 0
312
+ source.proj1d = None
313
+ source.proj1d_init = 0
314
+ source.init = lib.QP_STRUCT_INIT
315
+
316
+ if partial:
317
+ if qp.qp_init_map_pixhash(self._source, pixels, npix):
318
+ raise RuntimeError("Error initializing source pixhash")
319
+
320
+ if qp.qp_reshape_map(self._source):
321
+ raise RuntimeError("Error reshaping source map")
322
+
323
+ def reset_source(self):
324
+ """
325
+ Reset the source map structure. Must be reinitialized to
326
+ produce more timestreams.
327
+ """
328
+ if hasattr(self, "_source"):
329
+ qp.qp_free_map(self._source)
330
+ self.depo.pop("source_map", None)
331
+ self.depo.pop("source_nside", None)
332
+ self.depo.pop("source_pixels", None)
333
+ self._source = ct.pointer(lib.qp_map_t())
334
+
335
+ def source_is_pol(self):
336
+ """
337
+ Return True if the source map is polarized, otherwise False.
338
+ Raise an error if source map is not initialized.
339
+ """
340
+ if not self.source_is_init():
341
+ raise RuntimeError("source map not initialized")
342
+
343
+ if self._source.contents.vec_mode in [2, 3, 5, 7]:
344
+ return True
345
+ return False
346
+
347
+ def source_is_vpol(self):
348
+ """
349
+ Return True if the source map contains V polarization,
350
+ otherwise False.
351
+ Raise an error if source map is not initialized.
352
+ """
353
+ if not self.source_is_init():
354
+ raise RuntimeError("source map not initialized")
355
+
356
+ return self._source.contents.vec_mode == 3
357
+
358
+ def dest_is_init(self):
359
+ """
360
+ Return True if the dest map is initialized, otherwise False.
361
+ """
362
+ if not hasattr(self, "_dest"):
363
+ return False
364
+ if not self._dest.contents.init:
365
+ return False
366
+ return True
367
+
368
+ def init_dest(
369
+ self,
370
+ nside=None,
371
+ pol=True,
372
+ vec=None,
373
+ proj=None,
374
+ pixels=None,
375
+ vpol=False,
376
+ copy=False,
377
+ reset=False,
378
+ update=False,
379
+ ):
380
+ """
381
+ Initialize the destination map structure. Timestreams are binned
382
+ and projection matrices accumulated into this structure.
383
+
384
+ Arguments
385
+ ---------
386
+ nside : int, optional
387
+ map dimension. If `pixels` is supplied, this argument is required.
388
+ Otherwise, the default is 256.
389
+ pol : bool, optional
390
+ If True, a polarized map will be created.
391
+ vec : array_like or bool, optional, shape (N, npix)
392
+ If supplied, nside and pol are determined from this map, and
393
+ the vector (binned signal) map is initialized from this.
394
+ If False, accumulation of this map from timestreams is disabled.
395
+ proj : array_like or bool, optional, shape (N*(N+1)/2, npix)
396
+ Array of upper-triangular elements of the projection matrix
397
+ for each pixel. If not supplied, a blank map of the appropriate
398
+ shape is created. If False, accumulation of the projection matrix
399
+ is disabled.
400
+ pixels : 1D array_like, optional
401
+ Array of pixel numbers for each map index, if `vec` and `proj` are
402
+ partial maps.
403
+ vpol : bool, optional
404
+ If True, a polarized map including V polarization will be created.
405
+ copy : bool, optional
406
+ If True and vec/proj are supplied, make copies of these inputs
407
+ to avoid in-place operations.
408
+ reset : bool, optional
409
+ If True, and if the structure has already been initialized,
410
+ it is reset and re-initialized with the new map. If False,
411
+ a RuntimeError is raised if the structure has already been
412
+ initialized.
413
+ update : bool, optional
414
+ If True, and if the structure has already been initialized,
415
+ the supplied vec and proj are replaced in the existing dest
416
+ structure rather than reinitializing from scratch.
417
+ """
418
+
419
+ if vec is False and proj is False:
420
+ raise ValueError("one of vec or proj must not be False")
421
+
422
+ if self.dest_is_init():
423
+ if reset:
424
+ self.reset_dest()
425
+
426
+ elif update:
427
+ # update map data with same shape
428
+
429
+ dest = self._dest.contents
430
+ ret = ()
431
+
432
+ if self.depo["vec"] is not False:
433
+ if vec is None:
434
+ vec = np.zeros_like(self.depo["vec"])
435
+ if vec.squeeze().shape[-1] != self.depo["vec"].squeeze().shape[-1]:
436
+ raise ValueError("vec shape mismatch")
437
+ vec, _ = check_map(vec, copy=copy, partial=True)
438
+ dest.num_vec = len(vec)
439
+ dest.vec_mode = lib.get_vec_mode(vec, pol, vpol)
440
+ dest.vec1d = lib.as_ctypes(vec.ravel())
441
+ self.depo["vec"] = vec
442
+ ret += (vec.squeeze(),)
443
+
444
+ if self.depo["proj"] is not False:
445
+ if proj is None:
446
+ proj = np.zeros_like(self.depo["proj"])
447
+ if (
448
+ proj.squeeze().shape[-1]
449
+ != self.depo["proj"].squeeze().shape[-1]
450
+ ):
451
+ raise ValueError("proj shape mismatch")
452
+ proj, _ = check_map(proj, copy=copy, partial=True)
453
+ dest.num_proj = len(proj)
454
+ dest.proj_mode = lib.get_proj_mode(proj, pol, vpol)
455
+ dest.proj1d = lib.as_ctypes(proj.ravel())
456
+ self.depo["proj"] = proj
457
+ ret += (proj.squeeze(),)
458
+
459
+ if qp.qp_reshape_map(self._dest):
460
+ raise RuntimeError("Error reshaping dest map")
461
+
462
+ if len(ret) == 1:
463
+ return ret[0]
464
+ return ret
465
+
466
+ else:
467
+ raise RuntimeError("dest already initialized")
468
+
469
+ if pixels is None:
470
+ if nside is None:
471
+ nside = 256
472
+ npix = nside2npix(nside)
473
+ partial = False
474
+ else:
475
+ if nside is None:
476
+ raise ValueError("nside required for partial maps")
477
+ npix = len(pixels)
478
+ partial = True
479
+
480
+ if vec is None:
481
+ if vpol:
482
+ vec = np.zeros((4, npix), dtype=np.double)
483
+ elif pol:
484
+ vec = np.zeros((3, npix), dtype=np.double)
485
+ else:
486
+ vec = np.zeros((1, npix), dtype=np.double)
487
+ vdim2 = npix if partial else nside
488
+ elif vec is not False:
489
+ vec, vdim2 = check_map(vec, copy=copy, partial=partial)
490
+ if not partial:
491
+ nside = vdim2
492
+ npix = nside2npix(nside)
493
+ elif vdim2 != npix:
494
+ raise ValueError("vec has incompatible shape")
495
+ if len(vec) == 1:
496
+ pol = False
497
+ vpol = False
498
+ elif len(vec) == 3:
499
+ pol = True
500
+ vpol = False
501
+ elif len(vec) == 4:
502
+ pol = True
503
+ vpol = True
504
+ else:
505
+ raise ValueError("vec has incompatible shape")
506
+
507
+ if proj is None:
508
+ if vpol:
509
+ proj = np.zeros((10, npix), dtype=np.double)
510
+ elif pol:
511
+ proj = np.zeros((6, npix), dtype=np.double)
512
+ else:
513
+ proj = np.zeros((1, npix), dtype=np.double)
514
+ pdim2 = npix if partial else nside
515
+ elif proj is not False:
516
+ proj, pdim2, pnmap = check_proj(proj, copy=copy, partial=partial)
517
+
518
+ if vec is not False:
519
+ if pnmap != len(vec):
520
+ raise ValueError("proj has incompatible shape")
521
+ if len(proj) != [[1, 6][pol], 10][vpol]:
522
+ raise ValueError("proj has incompatible shape")
523
+ if pdim2 != vdim2:
524
+ raise ValueError("proj has incompatible nside")
525
+ else:
526
+ if len(proj) == 1:
527
+ pol = False
528
+ vpol = False
529
+ elif len(proj) == 6:
530
+ pol = True
531
+ vpol = False
532
+ elif len(proj) == 10:
533
+ pol = True
534
+ vpol = True
535
+ else:
536
+ raise ValueError("proj has incompatible shape")
537
+ if not partial:
538
+ nside = pdim2
539
+ npix = nside2npix(nside)
540
+ elif pdim2 != npix:
541
+ raise ValueError("proj has incompatible shape")
542
+
543
+ # store arrays for later retrieval
544
+ self.depo["vec"] = vec
545
+ self.depo["proj"] = proj
546
+ self.depo["dest_nside"] = nside
547
+
548
+ if partial:
549
+ self.depo["dest_pixels"] = pixels
550
+
551
+ # initialize
552
+ ret = ()
553
+ dest = self._dest.contents
554
+ dest.nside = nside
555
+ dest.npix = npix
556
+ dest.partial = partial
557
+ dest.pixinfo_init = 0
558
+ dest.pixinfo = None
559
+ dest.pixhash_init = 0
560
+ dest.pixhash = None
561
+ if vec is not False:
562
+ dest.num_vec = len(vec)
563
+ dest.vec_mode = lib.get_vec_mode(vec, pol, vpol)
564
+ dest.vec1d = lib.as_ctypes(vec.ravel())
565
+ dest.vec1d_init = lib.QP_ARR_INIT_PTR
566
+ ret += (vec.squeeze(),)
567
+ if proj is not False:
568
+ dest.num_proj = len(proj)
569
+ dest.proj_mode = lib.get_proj_mode(proj, pol, vpol)
570
+ dest.proj1d = lib.as_ctypes(proj.ravel())
571
+ dest.proj1d_init = lib.QP_ARR_INIT_PTR
572
+ ret += (proj.squeeze(),)
573
+ dest.vec = None
574
+ dest.vec_init = 0
575
+ dest.proj = None
576
+ dest.proj_init = 0
577
+ dest.init = lib.QP_STRUCT_INIT
578
+
579
+ if partial:
580
+ if qp.qp_init_map_pixhash(self._dest, pixels, npix):
581
+ raise RuntimeError("Error initializing dest pixhash")
582
+
583
+ if qp.qp_reshape_map(self._dest):
584
+ raise RuntimeError("Error reshaping dest map")
585
+
586
+ # return
587
+ if len(ret) == 1:
588
+ return ret[0]
589
+ return ret
590
+
591
+ def reset_dest(self):
592
+ """
593
+ Reset the destination map structure.
594
+ Must be reinitialized to continue mapmaking.
595
+ """
596
+ if hasattr(self, "_dest"):
597
+ qp.qp_free_map(self._dest)
598
+ self.depo.pop("vec", None)
599
+ self.depo.pop("proj", None)
600
+ self.depo.pop("dest_nside", None)
601
+ self.depo.pop("dest_pixels", None)
602
+ self._dest = ct.pointer(lib.qp_map_t())
603
+
604
+ def dest_is_pol(self):
605
+ """
606
+ Return True if the destination map is polarized, otherwise False.
607
+ Raise an error if destination map is not initialized.
608
+ """
609
+ if not self.dest_is_init():
610
+ raise RuntimeError("dest map not initialized")
611
+
612
+ if 2 in [self._dest.contents.vec_mode, self._dest.contents.proj_mode]:
613
+ return True
614
+ if 3 in [self._dest.contents.vec_mode, self._dest.contents.proj_mode]:
615
+ return True
616
+ return False
617
+
618
+ def dest_is_vpol(self):
619
+ """
620
+ Return True if the destination map contains V polarization,
621
+ otherwise False.
622
+ Raise an error if destination map is not initialized.
623
+ """
624
+ if not self.dest_is_init():
625
+ raise RuntimeError("dest map not initialized")
626
+
627
+ if 3 in [self._dest.contents.vec_mode, self._dest.contents.proj_mode]:
628
+ return True
629
+ return False
630
+
631
+ def point_is_init(self):
632
+ """
633
+ Return True if the point map is initialized, otherwise False.
634
+ """
635
+ if not hasattr(self, "_point"):
636
+ return False
637
+ if not self._point.contents.init:
638
+ return False
639
+ return True
640
+
641
+ def init_point(self, q_bore=None, ctime=None, q_hwp=None):
642
+ """
643
+ Initialize or update the boresight pointing data structure.
644
+
645
+ Arguments
646
+ ---------
647
+ q_bore : array_like, optional
648
+ Boresight pointing quaternion, of shape (nsamp, 4).
649
+ If supplied, the pointing structure is reset if already
650
+ initialized.
651
+ ctime : array_like, optional
652
+ time since the UTC epoch. If not None, the time array
653
+ is updated to this. Shape must be (nsamp,)
654
+ q_hwp : array_like, optional
655
+ Waveplate quaternion. If not None, the quaternion is
656
+ updated to this. Shape must be (nsamp, 4)
657
+ """
658
+
659
+ if not hasattr(self, "_point"):
660
+ self.reset_point()
661
+
662
+ point = self._point.contents
663
+
664
+ if q_bore is not None:
665
+ if point.init:
666
+ self.reset_point()
667
+ point = self._point.contents
668
+ q_bore = lib.check_input("q_bore", np.atleast_2d(q_bore), quat=True)
669
+ n = q_bore.size // 4
670
+ point.n = n
671
+ self.depo["q_bore"] = q_bore
672
+ point.q_bore = lib.as_ctypes(q_bore)
673
+ point.q_bore_init = lib.QP_ARR_INIT_PTR
674
+ point.init = lib.QP_STRUCT_INIT
675
+
676
+ if not point.init:
677
+ raise RuntimeError("point not initialized")
678
+
679
+ n = point.n
680
+
681
+ if ctime is False:
682
+ point.ctime_init = 0
683
+ point.ctime = None
684
+ elif ctime is not None:
685
+ ctime = lib.check_input("ctime", ctime, shape=(n,))
686
+ self.depo["ctime"] = ctime
687
+ point.ctime_init = lib.QP_ARR_INIT_PTR
688
+ point.ctime = lib.as_ctypes(ctime)
689
+
690
+ if q_hwp is False:
691
+ point.q_hwp_init = 0
692
+ point.q_hwp = None
693
+ elif q_hwp is not None:
694
+ q_hwp = lib.check_input("q_hwp", q_hwp, shape=(n, 4), quat=True)
695
+ self.depo["q_hwp"] = q_hwp
696
+ point.q_hwp_init = lib.QP_ARR_INIT_PTR
697
+ point.q_hwp = lib.as_ctypes(q_hwp)
698
+
699
+ def reset_point(self):
700
+ """
701
+ Reset the pointing data structure.
702
+ """
703
+ if hasattr(self, "_point"):
704
+ qp.qp_free_point(self._point)
705
+ self.depo.pop("q_bore", None)
706
+ self.depo.pop("ctime", None)
707
+ self.depo.pop("q_hwp", None)
708
+ self._point = ct.pointer(lib.qp_point_t())
709
+
710
+ def init_detarr(
711
+ self,
712
+ q_off,
713
+ weight=None,
714
+ gain=None,
715
+ mueller=None,
716
+ tod=None,
717
+ flag=None,
718
+ weights=None,
719
+ do_diff=False,
720
+ write=False,
721
+ ):
722
+ """
723
+ Initialize the detector listing structure. Detector properties and
724
+ timestreams are passed to and from the mapmaker through this structure.
725
+
726
+ Arguments
727
+ ---------
728
+ q_off : array_like
729
+ Array of offset quaternions, of shape (ndet, 4).
730
+ weight : array_like, optional
731
+ Per-channel mapmaking weights, of shape (ndet,) or a constant.
732
+ Default : 1.
733
+ gain : array_like, optional
734
+ Per-channel gains, of shape (ndet,) or a constant.
735
+ Default : 1.
736
+ mueller : array_like, optional
737
+ Per-channel polarization efficiencies, of shape(ndet, 4).
738
+ Default : [1., 1., 0., 1.] per det.
739
+ tod : array_like, optional
740
+ Timestream array, of shape (ndet, nsamp). nsamp must match that of
741
+ the pointing structure. If not supplied and `write` is True, then
742
+ a zero-filled timestream array is initialized.
743
+ flag : array_like, optional
744
+ Flag array, of shape (ndet, nsamp), for excluding data from
745
+ mapmaking. nsamp must match that of the pointing structure.
746
+ If not supplied, a zero-filled array is initialized (i.e. no
747
+ flagged samples).
748
+ weights : array_like, optional
749
+ Weight array, of shape (ndet, nsamp), for weighting each sample of
750
+ data. nsamp must match that of the pointing structure. If not
751
+ supplied, this option is not used.
752
+ do_diff : bool, optional
753
+ If True, initialize pairs of arrays for pair-differenced mapmaking.
754
+ write : bool, optional
755
+ If True, the timestreams are ensured writable and created if
756
+ necessary.
757
+ """
758
+
759
+ self.reset_detarr()
760
+
761
+ # check inputs
762
+ q_off = lib.check_input("q_off", np.atleast_2d(q_off), quat=True)
763
+ n = q_off.size // 4
764
+ weight = lib.check_input("weight", weight, shape=(n,), fill=1)
765
+ gain = lib.check_input("gain", gain, shape=(n,), fill=1)
766
+ mueller = lib.check_input(
767
+ "mueller", mueller, shape=(n, 4), fill=np.array([1, 1, 0, 1])
768
+ )
769
+
770
+ ns = self._point.contents.n
771
+ shape = (n, ns)
772
+
773
+ if tod is not None:
774
+ tod = np.atleast_2d(tod)
775
+
776
+ if write:
777
+ tod = lib.check_output("tod", tod, shape=shape, fill=0)
778
+ self.depo["tod"] = tod
779
+ elif tod is not None:
780
+ tod = lib.check_input("tod", tod, shape=shape)
781
+ self.depo["tod"] = tod
782
+ if flag is not None:
783
+ flag = lib.check_input(
784
+ "flag", np.atleast_2d(flag), dtype=np.uint8, shape=shape
785
+ )
786
+ self.depo["flag"] = flag
787
+ if weights is not None:
788
+ weights = lib.check_input("weights", np.atleast_2d(weights), shape=shape)
789
+ self.depo["weights"] = weights
790
+
791
+ # populate array
792
+ dets = (lib.qp_det_t * n)()
793
+ for idx, (q, w, g, m) in enumerate(zip(q_off, weight, gain, mueller)):
794
+ dets[idx].init = lib.QP_STRUCT_INIT
795
+ dets[idx].q_off = lib.as_ctypes(q)
796
+ dets[idx].weight = w
797
+ dets[idx].gain = g
798
+ dets[idx].mueller = lib.as_ctypes(m)
799
+ if tod is not None:
800
+ dets[idx].n = ns
801
+ dets[idx].tod_init = lib.QP_ARR_INIT_PTR
802
+ dets[idx].tod = lib.as_ctypes(tod[idx])
803
+ else:
804
+ dets[idx].tod_init = 0
805
+ if flag is not None:
806
+ dets[idx].n = ns
807
+ dets[idx].flag_init = lib.QP_ARR_INIT_PTR
808
+ dets[idx].flag = lib.as_ctypes(flag[idx])
809
+ else:
810
+ dets[idx].flag_init = 0
811
+ if weights is not None:
812
+ dets[idx].n = ns
813
+ dets[idx].weights_init = lib.QP_ARR_INIT_PTR
814
+ dets[idx].weights = lib.as_ctypes(weights[idx])
815
+
816
+ detarr = lib.qp_detarr_t()
817
+ detarr.n = n
818
+ detarr.init = lib.QP_STRUCT_INIT
819
+ detarr.arr_init = lib.QP_ARR_INIT_PTR
820
+ detarr.arr = dets
821
+ detarr.diff = 0
822
+
823
+ if do_diff:
824
+ detarr.diff = 1
825
+
826
+ self._detarr = ct.pointer(detarr)
827
+
828
+ def reset_detarr(self):
829
+ """
830
+ Reset the detector array structure.
831
+ """
832
+ if hasattr(self, "_detarr") and self._detarr is not None:
833
+ qp.qp_free_detarr(self._detarr)
834
+ self._detarr = None
835
+ self.depo.pop("tod", None)
836
+ self.depo.pop("flag", None)
837
+ self.depo.pop("weights", None)
838
+
839
+ def reset(self):
840
+ """
841
+ Reset the internal data structures, and clear the data depo.
842
+ """
843
+ if not hasattr(self, "depo"):
844
+ self.depo = dict()
845
+ self.reset_source()
846
+ self.reset_dest()
847
+ self.reset_point()
848
+ self.reset_detarr()
849
+ self.depo.clear()
850
+
851
+ def from_tod(
852
+ self,
853
+ q_off,
854
+ tod=None,
855
+ count_hits=True,
856
+ weight=None,
857
+ gain=None,
858
+ mueller=None,
859
+ flag=None,
860
+ weights=None,
861
+ do_diff=False,
862
+ **kwargs,
863
+ ):
864
+ """
865
+ Calculate signal and hits maps for given detectors.
866
+
867
+ Arguments
868
+ ---------
869
+ q_off : array_like
870
+ quaternion offset array, of shape (ndet, 4)
871
+ tod : array_like, optional
872
+ output array for timestreams, of shape (ndet, nsamp)
873
+ if not supplied, only the projection map is populated.
874
+ count_hits : bool, optional
875
+ if True (default), populate projection map.
876
+ weight : array_like, optional
877
+ array of channel weights, of shape (ndet,). Defaults to 1 if not
878
+ supplied.
879
+ gain : array_like, optional
880
+ Per-channel gains, of shape (ndet,) or a constant.
881
+ Default : 1.
882
+ mueller : array_like, optional
883
+ array of Mueller matrix A/B/C elements, of shape (ndet,3). Defaults to
884
+ [1, 1, 0] per channel if not supplied.
885
+ flag : array_like, optional
886
+ array of flag timestreams for each channel, of shape (ndet, nsamp).
887
+ weights : array_like, optional
888
+ array of weight timestreams for each channel, of shape (ndet, nsamp).
889
+ do_diff: do timestream differencing. Assumes first half of tods are
890
+ one pair and the second half are the other.
891
+
892
+ Returns
893
+ -------
894
+ vec : array_like, optional
895
+ binned signal map, if tod is supplied
896
+ proj : array_like, optional
897
+ binned projection matrix map, if count_hits is True
898
+
899
+ Notes
900
+ -----
901
+ The remaining keyword arguments are passed to the
902
+ :meth:`~qpoint.qpoint_class.QPoint.set` method.
903
+ """
904
+
905
+ self.set(**kwargs)
906
+
907
+ # initialize detectors
908
+ self.init_detarr(
909
+ q_off,
910
+ weight=weight,
911
+ gain=gain,
912
+ mueller=mueller,
913
+ tod=tod,
914
+ flag=flag,
915
+ weights=weights,
916
+ do_diff=do_diff,
917
+ )
918
+
919
+ # check modes
920
+ return_vec = True
921
+ if tod is None or tod is False or self.depo["vec"] is False:
922
+ return_vec = False
923
+ return_proj = True
924
+ if not count_hits or self.depo["proj"] is False:
925
+ if return_vec is False:
926
+ raise RuntimeError("Nothing to do")
927
+ return_proj = False
928
+
929
+ # cache modes
930
+ dest = self._dest.contents
931
+ vec_mode = dest.vec_mode
932
+ if return_vec is False:
933
+ dest.vec_mode = 0
934
+ proj_mode = dest.proj_mode
935
+ if return_proj is False:
936
+ dest.proj_mode = 0
937
+
938
+ # run
939
+ if qp.qp_tod2map(self._memory, self._detarr, self._point, self._dest):
940
+ raise RuntimeError(qp.qp_get_error_string(self._memory))
941
+
942
+ # reset modes
943
+ dest.vec_mode = vec_mode
944
+ dest.proj_mode = proj_mode
945
+
946
+ # clean up
947
+ self.reset_detarr()
948
+
949
+ # return
950
+ ret = ()
951
+ if return_vec:
952
+ ret += (self.depo["vec"].squeeze(),)
953
+ if return_proj:
954
+ ret += (self.depo["proj"].squeeze(),)
955
+ if len(ret) == 1:
956
+ return ret[0]
957
+ return ret
958
+
959
+ def to_tod(self, q_off, gain=None, mueller=None, tod=None, flag=None, **kwargs):
960
+ """
961
+ Calculate signal TOD from source map for multiple channels.
962
+
963
+ Arguments
964
+ ---------
965
+ q_off : array_like
966
+ quaternion offset array, of shape (ndet, 4)
967
+ gain : array_like, optional
968
+ Per-channel gains, of shape (ndet,) or a constant.
969
+ Default : 1.
970
+ mueller : array_like, optional
971
+ array of Mueller matrix A/B/C/D elements, of shape (ndet, 4). Defaults t
972
+ [1, 1, 0, 1] per channel if not supplied.
973
+ tod : array_like, optional
974
+ output array for timestreams, of shape (ndet, nsamp)
975
+ use this keyword argument for in-place computation.
976
+
977
+ Returns
978
+ -------
979
+ tod : array_like
980
+ A timestream sampled from the input map for each requested detector.
981
+ The output array shape is (ndet, nsamp).
982
+
983
+ Notes
984
+ -----
985
+ The remaining keyword arguments are passed to the
986
+ :meth:`~qpoint.qpoint_class.QPoint.set` method.
987
+ """
988
+
989
+ self.set(**kwargs)
990
+
991
+ # initialize detectors
992
+ self.init_detarr(
993
+ q_off, gain=gain, mueller=mueller, tod=tod, flag=flag, write=True
994
+ )
995
+
996
+ # run
997
+ if qp.qp_map2tod(self._memory, self._detarr, self._point, self._source):
998
+ raise RuntimeError(qp.qp_get_error_string(self._memory))
999
+ tod = self.depo.pop("tod")
1000
+
1001
+ # clean up
1002
+ self.reset_detarr()
1003
+
1004
+ # return
1005
+ return tod
1006
+
1007
+ def proj_cond(self, proj=None, mode=None, partial=False):
1008
+ """
1009
+ Hits-normalized projection matrix condition number for
1010
+ each pixel.
1011
+
1012
+ Arguments
1013
+ ---------
1014
+ proj : array_like
1015
+ Projection matrix, of shape (N*(N+1)/2, npix).
1016
+ If None, obtained from the depo.
1017
+ mode : {None, 1, -1, 2, -2, inf, -inf, 'fro'}, optional
1018
+ condition number order. See `numpy.linalg.cond`.
1019
+ Default: None (2-norm from SVD)
1020
+ partial : bool, optional
1021
+ If True, the map is not checked to ensure a proper healpix nside.
1022
+
1023
+ Returns
1024
+ -------
1025
+ cond : array_like
1026
+ Condition number of each pixel.
1027
+ """
1028
+
1029
+ # check inputs
1030
+ if proj is None:
1031
+ proj = self.depo["proj"]
1032
+ if proj is None or proj is False:
1033
+ raise ValueError("missing proj")
1034
+ proj, _, nmap = check_proj(proj, copy=True, partial=partial)
1035
+ nproj = len(proj)
1036
+
1037
+ # normalize
1038
+ m = proj[0].astype(bool)
1039
+ proj[:, m] /= proj[0, m]
1040
+ proj[:, ~m] = np.inf
1041
+
1042
+ # return if unpolarized
1043
+ if nmap == 1:
1044
+ return proj[0]
1045
+
1046
+ # projection matrix indices
1047
+ idx = np.zeros((nmap, nmap), dtype=int)
1048
+ rtri, ctri = np.triu_indices(nmap)
1049
+ idx[rtri, ctri] = idx[ctri, rtri] = np.arange(nproj)
1050
+
1051
+ # calculate for each pixel
1052
+ proj[:, ~m] = 0
1053
+ cond = np.linalg.cond(proj[idx].transpose(2, 0, 1), p=mode)
1054
+ cond[~m] = np.inf
1055
+ # threshold at machine precision
1056
+ cond[cond > 1.0 / np.finfo(float).eps] = np.inf
1057
+ return cond
1058
+
1059
+ def solve_map(
1060
+ self,
1061
+ vec=None,
1062
+ proj=None,
1063
+ mask=None,
1064
+ copy=True,
1065
+ return_proj=False,
1066
+ return_mask=False,
1067
+ partial=None,
1068
+ fill=0,
1069
+ cond=None,
1070
+ cond_thresh=1e6,
1071
+ method="exact",
1072
+ ):
1073
+ """
1074
+ Solve for a map, given the binned map and the projection matrix
1075
+ for each pixel.
1076
+
1077
+ Arguments
1078
+ ---------
1079
+ vec : array_like, optional
1080
+ A map or list of N maps. Default to the `"vec"` entry in
1081
+ :attr:`depo`.
1082
+ proj : array_like, optional
1083
+ An array of upper-triangular projection matrices for each pixel, of
1084
+ shape (N*(N+1)/2, npix). Default to the `"proj"` entry in
1085
+ :attr:`depo`.
1086
+ mask : array_like, optional
1087
+ A mask of shape (npix,), evaluates to True where pixels are valid.
1088
+ The input mask in converted to a boolean array if supplied.
1089
+ copy : bool, optional
1090
+ if False, do the computation in-place so that the input maps are
1091
+ modified. Otherwise, a copy is created prior to solving.
1092
+ Default: False.
1093
+ return_proj : bool, optional
1094
+ if True, return the Cholesky-decomposed projection matrix.
1095
+ if False, and inplace is True, the input projection matrix
1096
+ is not modified.
1097
+ return_mask : bool, optional
1098
+ if True, return the mask array, updated with any pixels
1099
+ that could not be solved.
1100
+ partial : bool, optional
1101
+ If True, the map is not checked to ensure a proper healpix nside.
1102
+ fill : scalar, optional
1103
+ Fill the solved map where proj == 0 with this value. Default: 0.
1104
+ cond : array_like, optional
1105
+ A map of condition number per pixel. If not supplied, this will be
1106
+ calculated using :meth:`proj_cond`
1107
+ cond_thresh : scalar, optional
1108
+ A threshold to place on the condition number to exclude pixels
1109
+ prior to solving. Reduce this to avoid `LinAlgError` due to
1110
+ singular matrices.
1111
+ method : string, optional
1112
+ Map inversion method. If "exact", invert the pointing matrix directly
1113
+ If "cho", use Cholesky decomposition to solve. Default: "exact".
1114
+
1115
+ Returns
1116
+ -------
1117
+ map : array_like
1118
+ A solved map or set of maps, in shape (N, npix).
1119
+ proj_out : array_like
1120
+ The upper triangular elements of the decomposed projection matrix,
1121
+ (if method is 'cho') or of the matrix inverse (if method is 'exact'),
1122
+ if requested, in shape (N*(N+1)/2, npix).
1123
+ mask : array_like
1124
+ 1-d array, True for valid pixels, if `return_mask` is True
1125
+ """
1126
+
1127
+ # check if we're dealing with a partial map
1128
+ if partial is None:
1129
+ if vec is None and "dest_pixels" in self.depo:
1130
+ partial = True
1131
+ else:
1132
+ partial = False
1133
+
1134
+ # ensure properly shaped arrays
1135
+ if vec is None:
1136
+ vec = self.depo["vec"]
1137
+ if vec is None or vec is False:
1138
+ raise ValueError("missing vec")
1139
+ vec, nside = check_map(vec, copy=copy, partial=partial)
1140
+
1141
+ if proj is None:
1142
+ proj = self.depo["proj"]
1143
+ if proj is None or proj is False:
1144
+ raise ValueError("missing proj")
1145
+ pcopy = True if not return_proj else copy
1146
+ proj, pnside, nmap = check_proj(proj, copy=pcopy, partial=partial)
1147
+
1148
+ if pnside != nside or nmap != len(vec):
1149
+ raise ValueError("vec and proj have incompatible shapes")
1150
+ nproj = len(proj)
1151
+
1152
+ # deal with mask
1153
+ if mask is None:
1154
+ mask = np.ones(len(vec[0]), dtype=bool)
1155
+ else:
1156
+ mcopy = True if not return_mask else copy
1157
+ mask, mnside = check_map(mask, copy=mcopy, partial=partial)
1158
+ if mnside != nside:
1159
+ raise ValueError("mask has incompatible shape")
1160
+ mask = mask.squeeze().astype(bool)
1161
+ mask &= proj[0].astype(bool)
1162
+
1163
+ # if unpolarized, just divide
1164
+ if len(vec) == 1:
1165
+ vec = vec.squeeze()
1166
+ proj = proj.squeeze()
1167
+ vec[mask] /= proj[mask]
1168
+ vec[~mask] = fill
1169
+ ret = (vec,) + (proj,) * return_proj + (mask,) * return_mask
1170
+ if len(ret) == 1:
1171
+ return ret[0]
1172
+ return ret
1173
+
1174
+ # projection matrix indices
1175
+ idx = np.zeros((nmap, nmap), dtype=int)
1176
+ rtri, ctri = np.triu_indices(nmap)
1177
+ idx[rtri, ctri] = idx[ctri, rtri] = np.arange(nproj)
1178
+
1179
+ # solve
1180
+ if method == "exact":
1181
+ if cond is None:
1182
+ cond = self.proj_cond(proj=proj, partial=partial)
1183
+ mask &= cond < cond_thresh
1184
+ vec[:, ~mask] = 0
1185
+ proj[..., ~mask] = np.eye(nmap)[rtri, ctri][:, None]
1186
+ # numpy 2 requires b (ie vec) to have shape (..., M, K) not (..., M).
1187
+ # Use K = 1 with np.newaxis.
1188
+ vec[:] = np.linalg.solve(
1189
+ proj[idx].transpose(2, 0, 1), vec.transpose()[..., np.newaxis]
1190
+ )[..., 0].transpose()
1191
+ vec[:, ~mask] = fill
1192
+ proj[:, ~mask] = 0
1193
+ ret = (vec,) + return_proj * (proj,) + return_mask * (mask,)
1194
+ if len(ret) == 1:
1195
+ return ret[0]
1196
+ return ret
1197
+
1198
+ if method != "cho":
1199
+ raise ValueError("Unrecognized method {}".format(method))
1200
+
1201
+ # slow method, loop over pixels
1202
+ from scipy.linalg import cho_factor, cho_solve
1203
+
1204
+ for ii, (m, A, v) in enumerate(zip(mask, proj[idx].T, vec.T)):
1205
+ if not m:
1206
+ proj[:, ii] = 0
1207
+ vec[:, ii] = fill
1208
+ continue
1209
+ try:
1210
+ vec[:, ii] = cho_solve(cho_factor(A, False, True), v, True)
1211
+ except:
1212
+ mask[ii] = False
1213
+ proj[:, ii] = 0
1214
+ vec[:, ii] = fill
1215
+ else:
1216
+ proj[:, ii] = A[rtri, ctri]
1217
+
1218
+ # return
1219
+ ret = (vec,) + (proj,) * return_proj + (mask,) * return_mask
1220
+ if len(ret) == 1:
1221
+ return ret[0]
1222
+ return ret
1223
+
1224
+ def solve_map_cho(self, *args, **kwargs):
1225
+ """
1226
+ Solve for a map, given the binned map and the projection matrix
1227
+ for each pixel, using Cholesky decomposition. This method
1228
+ uses the scipy.linalg.cho_factor and scipy.linalg.cho_solve
1229
+ functions internally.
1230
+
1231
+ Arguments
1232
+ ---------
1233
+ vec : array_like, optional
1234
+ A map or list of N maps. Default to the `"vec"` entry in
1235
+ :attr:`depo`.
1236
+ proj : array_like, optional
1237
+ An array of upper-triangular projection matrices for each pixel, of
1238
+ shape (N*(N+1)/2, npix). Default to the `"proj"` entry in
1239
+ :attr:`depo`.
1240
+ mask : array_like, optional
1241
+ A mask of shape (npix,), evaluates to True where pixels are valid.
1242
+ The input mask in converted to a boolean array if supplied.
1243
+ copy : bool, optional
1244
+ if False, do the computation in-place so that the input maps are
1245
+ modified. Otherwise, a copy is created prior to solving.
1246
+ Default: False.
1247
+ return_proj : bool, optional
1248
+ if True, return the Cholesky-decomposed projection matrix.
1249
+ if False, and inplace is True, the input projection matrix
1250
+ is not modified.
1251
+ return_mask : bool, optional
1252
+ if True, return the mask array, updated with any pixels
1253
+ that could not be solved.
1254
+ partial : bool, optional
1255
+ If True, the map is not checked to ensure a proper healpix nside.
1256
+ fill : scalar, optional
1257
+ Fill the solved map where proj == 0 with this value. Default: 0.
1258
+ cond : array_like, optional
1259
+ A map of condition number per pixel. If not supplied, this will be
1260
+ calculated using :meth:`proj_cond`
1261
+ cond_thresh : scalar, optional
1262
+ A threshold to place on the condition number to exclude pixels
1263
+ prior to solving. Reduce this to avoid `LinAlgError` due to
1264
+ singular matrices.
1265
+
1266
+ Returns
1267
+ -------
1268
+ map : array_like
1269
+ A solved map or set of maps, in shape (N, npix).
1270
+ proj_out : array_like
1271
+ The upper triangular elements of the decomposed projection matrix,
1272
+ if requested, in shape (N*(N+1)/2, npix).
1273
+ mask : array_like
1274
+ 1-d array, True for valid pixels, if `return_mask` is True
1275
+ """
1276
+ kwargs["method"] = "cho"
1277
+ return self.solve_map(*args, **kwargs)
1278
+
1279
+ def unsolve_map(
1280
+ self,
1281
+ map_in,
1282
+ proj=None,
1283
+ mask=None,
1284
+ copy=True,
1285
+ return_proj=False,
1286
+ return_mask=False,
1287
+ partial=None,
1288
+ fill=0,
1289
+ ):
1290
+ """
1291
+ Invert the solved map to recover the binned vec array.
1292
+
1293
+ Arguments
1294
+ ---------
1295
+ map_in : array_like
1296
+ A map or list of N maps.
1297
+ proj : array_like, optional
1298
+ An array of upper-triangular projection matrices for each pixel, of
1299
+ shape (N*(N+1)/2, npix). Default to the `"proj"` entry in
1300
+ :attr:`depo`.
1301
+ mask : array_like, optional
1302
+ A mask of shape (npix,), evaluates to True where pixels are valid.
1303
+ The input mask in converted to a boolean array if supplied.
1304
+ copy : bool, optional
1305
+ if False, do the computation in-place so that the input maps are
1306
+ modified. Otherwise, a copy is created prior to solving.
1307
+ Default: False.
1308
+ return_proj : bool, optional
1309
+ if True, return the Cholesky-decomposed projection matrix.
1310
+ if False, and inplace is True, the input projection matrix
1311
+ is not modified.
1312
+ return_mask : bool, optional
1313
+ if True, return the mask array, updated with any pixels
1314
+ that could not be solved.
1315
+ partial : bool, optional
1316
+ If True, the map is not checked to ensure a proper healpix nside.
1317
+ fill : scalar, optional
1318
+ Fill the solved map where proj == 0 with this value. Default: 0.
1319
+
1320
+ Returns
1321
+ -------
1322
+ vec : array_like
1323
+ A binned map or set of maps, in shape (N, npix).
1324
+ proj_out : array_like
1325
+ The upper triangular elements of the projection matrix,
1326
+ if requested, in shape (N*(N+1)/2, npix).
1327
+ mask : array_like
1328
+ 1-d array, True for valid pixels, if `return_mask` is True
1329
+ """
1330
+
1331
+ # check if we're dealing with a partial map
1332
+ if partial is None:
1333
+ partial = "dest_pixels" in self.depo
1334
+
1335
+ # ensure properly shaped arrays
1336
+ map_in, nside = check_map(map_in, copy=copy, partial=partial)
1337
+
1338
+ if proj is None:
1339
+ proj = self.depo["proj"]
1340
+ if proj is None or proj is False:
1341
+ raise ValueError("missing proj")
1342
+ pcopy = True if not return_proj else copy
1343
+ proj, pnside, nmap = check_proj(proj, copy=pcopy, partial=partial)
1344
+
1345
+ if pnside != nside or nmap != len(map_in):
1346
+ raise ValueError("map_in and proj have incompatible shapes")
1347
+ nproj = len(proj)
1348
+
1349
+ # deal with mask
1350
+ if mask is None:
1351
+ mask = np.ones(len(map_in[0]), dtype=bool)
1352
+ else:
1353
+ mcopy = True if not return_mask else copy
1354
+ mask, mnside = check_map(mask, copy=mcopy, partial=partial)
1355
+ if mnside != nside:
1356
+ raise ValueError("mask has incompatible shape")
1357
+ mask = mask.squeeze().astype(bool)
1358
+ mask &= proj[0].astype(bool)
1359
+
1360
+ if len(map_in) == 1:
1361
+ # if unpolarized, just multiply
1362
+ map_in = map_in.squeeze()
1363
+ proj = proj.squeeze()
1364
+ map_in[mask] *= proj[mask]
1365
+ map_in[~mask] = fill
1366
+ else:
1367
+ # projection matrix indices
1368
+ idx = np.zeros((nmap, nmap), dtype=int)
1369
+ rtri, ctri = np.triu_indices(nmap)
1370
+ idx[rtri, ctri] = idx[ctri, rtri] = np.arange(nproj)
1371
+ map_in[:] = np.einsum("ij...,j...->i...", proj[idx], map_in)
1372
+ map_in[:, ~mask] = fill
1373
+
1374
+ # return
1375
+ ret = (map_in,) + (proj,) * return_proj + (mask,) * return_mask
1376
+ if len(ret) == 1:
1377
+ return ret[0]
1378
+ return ret