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/qpoint_class.py ADDED
@@ -0,0 +1,1974 @@
1
+ import numpy as np
2
+ from warnings import warn
3
+ from . import _libqpoint as lib
4
+ from ._libqpoint import libqp as qp
5
+ from ._libqpoint import check_input, check_inputs, check_output
6
+
7
+ __all__ = ["QPoint", "check_input", "check_inputs", "check_output"]
8
+
9
+
10
+ class QPoint(object):
11
+ def __init__(self, update_iers=False, **kwargs):
12
+ """
13
+ Initialize a `QPoint` memory instance for keeping track of pointing
14
+ corrections over time.
15
+
16
+ If `update_iers` is `True`, attempt to call the method
17
+ :meth:`update_bulletin_a` to update the internal IERS-A database
18
+ (requires `astropy` version 1.2 or newer).
19
+
20
+ Any remaining keyword arguments are passed to :meth:`set` to update
21
+ memory.
22
+ """
23
+
24
+ # initialize memory
25
+ self._memory = qp.qp_init_memory()
26
+
27
+ # try to update IERS-A bulletin
28
+ if update_iers:
29
+ self.update_bulletin_a()
30
+
31
+ # collect all parameter functions
32
+ self._funcs = lib.qp_funcs
33
+ self._all_funcs = dict()
34
+ for k in self._funcs:
35
+ self._all_funcs.update(**self._funcs[k])
36
+
37
+ # set any requested parameters
38
+ self.set(**kwargs)
39
+
40
+ def print_memory(self):
41
+ """
42
+ Print current memory state in C.
43
+ """
44
+ qp.qp_print_memory(self._memory)
45
+
46
+ def __del__(self):
47
+ """
48
+ Free memory before deleting the object
49
+ """
50
+ qp.qp_free_memory(self._memory)
51
+
52
+ def _set(self, key, val):
53
+ """
54
+ Set a single parameter to the given value. See :meth:`set` for a list
55
+ of parameter names.
56
+ """
57
+ if key not in self._all_funcs:
58
+ raise KeyError("Unknown parameter {}".format(key))
59
+ val = self._all_funcs[key]["check_set"](val)
60
+ self._all_funcs[key]["set"](self._memory, val)
61
+
62
+ def _get(self, key):
63
+ """
64
+ Get the value for a single parameter. See :meth:`set` for a list of
65
+ parameter names.
66
+ """
67
+ if key not in self._all_funcs:
68
+ raise KeyError("Unknown parameter {}".format(key))
69
+ val = self._all_funcs[key]["get"](self._memory)
70
+ return self._all_funcs[key]["check_get"](val)
71
+
72
+ def set(self, **kwargs):
73
+ """
74
+ Set computation options.
75
+
76
+ Arguments
77
+ ---------
78
+ rate_daber : {'never', 'once', 'always'}, or float
79
+ Rate at which the diurnal aberration correction is applied in seconds
80
+ (NB: this can only be applied always or never)
81
+ rate_lonlat : {'never', 'once', 'always'}, or float
82
+ Rate at which observer's lon and lat are updated
83
+ rate_wobble : {'never', 'once', 'always'}, or float
84
+ Rate at which the polar motion correction is updated
85
+ (NB: these are not estimated for dates beyond a year from now)
86
+ rate_dut1 : {'never', 'once', 'always'}, or float
87
+ Rate at which the ut1-utc correction is updated
88
+ (NB: this is not estimated for dates beyond a year from now)
89
+ rate_erot : {'never', 'once', 'always'}, or float
90
+ Rate at which the earth's rotation angle is updated
91
+ rate_npb : {'never', 'once', 'always'}, or float
92
+ Rate at which the nutation/precession/frame-bias terms are updated
93
+ rate_aaber : {'never', 'once', 'always'}, or float
94
+ Rate at which the annual aberration correction
95
+ (due to the earth's orbital velocity) is updated
96
+ rate_ref : {'never', 'once', 'always'}, or float
97
+ Rate at which the refaction correction is updated (NB: this
98
+ correction can also be updated manually -- see :meth:`refraction`)
99
+ accuracy : 'low' or 'high'
100
+ If 'low', use a truncated form (2000b) for the NPB correction,
101
+ which is much faster but less accurate. If 'high' (default), use
102
+ the full 2006/2000a form.
103
+ mean_aber : bool
104
+ If True, apply the aberration correction as an average for the
105
+ entire field of view. This is gives a 1-2 arcsec deviation
106
+ at the edges of the SPIDER field of view.
107
+ fast_aber : bool
108
+ If True, apply the aberration correction using the exact
109
+ angle and quaternion rotation. Otherwise, use a small-angle
110
+ approximation for the aberration quaternion (default).
111
+ fast_math : bool
112
+ If True, use polynomial approximations for trig functions
113
+ polconv : 'cosmo' or 'iau'
114
+ Specify the 'cosmo' or 'iau' polarization convention
115
+ pix_order : 'nest' or 'ring'
116
+ HEALPix pixel ordering
117
+ interp_pix : bool
118
+ If True, interpolate between pixels in scanning the source map.
119
+ fast_pix : bool
120
+ If True, use `vec2pix` to get pixel number directly from the
121
+ quaternion instead of `ang2pix` from ra/dec.
122
+ error_missing : bool
123
+ If True, raise an error if reading/writing missing pixels.
124
+ nan_missing : bool
125
+ If True, fill samples from missing pixels with NaN.
126
+ Only used if `error_missing` is False.
127
+ interp_missing : bool
128
+ If True and `interp_pix` is True, drop missing neighbors
129
+ and reweight remaining neighbors. Overrides `nan_missing`.
130
+ num_threads : bool
131
+ Number of openMP threads to use for mapmaking.
132
+ temperature : float
133
+ Ambient temperature, Celcius. For computing refraction corrections.
134
+ pressure : float
135
+ Ambient pressure, mbar. For computing refraction corrections.
136
+ humidity : float
137
+ Relative humidity, fraction. For computing refraction corrections.
138
+ frequency : float
139
+ Observer frequency, GHz. For computing refraction corrections.
140
+ dut1 : float
141
+ UT1 correction
142
+ ref_delta : float
143
+ Refraction correction
144
+ """
145
+
146
+ for k in kwargs:
147
+ try:
148
+ self._set(k, kwargs[k])
149
+ except KeyError:
150
+ continue
151
+
152
+ def get(self, *args):
153
+ """
154
+ Returns a dictionary of the requested state parameters. If no
155
+ parameters are supplied, then all are returned. If a single parameter
156
+ is supplied, then just that value is returned. See :meth:`set` for a
157
+ list of parameter names.
158
+
159
+ Can also select 'options', 'rates', 'weather', or 'params' to return
160
+ all of that subset of parameters.
161
+ """
162
+ state = dict()
163
+ if not len(args):
164
+ for k in self._funcs:
165
+ state[k] = dict()
166
+ for kk in self._funcs[k]:
167
+ state[k][kk] = self._get(kk)
168
+ return state
169
+
170
+ for arg in args:
171
+ if arg in self._funcs:
172
+ state[arg] = dict()
173
+ for k in self._funcs[arg]:
174
+ state[arg][k] = self._get(k)
175
+ else:
176
+ state[arg] = self._get(arg)
177
+ if len(args) == 1:
178
+ return state[args[0]]
179
+ return state
180
+
181
+ def reset_rates(self):
182
+ """
183
+ Reset update counters for each state. Useful to force an updated
184
+ correction term at the beginning of each chunk.
185
+ """
186
+ qp.qp_reset_rates(self._memory)
187
+
188
+ def reset_inv_rates(self):
189
+ """
190
+ Reset update counters for each inverse state. Useful to force an updated
191
+ correction term at the beginning of each chunk.
192
+ """
193
+ qp.qp_reset_inv_rates(self._memory)
194
+
195
+ def refraction(self, *args, **kwargs):
196
+ """refraction(q, **kwargs)
197
+ refraction(delta)
198
+
199
+ Update refraction parameters
200
+
201
+ Arguments
202
+ ---------
203
+ q : quaternion or array of quaternions
204
+ Observer orientation in horizon coordinates
205
+ temperature : float
206
+ Ambient temperature, Celcius
207
+ pressure : float
208
+ Ambient pressure, mbar
209
+ humidity : float
210
+ Ambient relative humidity, fraction
211
+ frequency : float
212
+ Observing frequency, GHz
213
+ delta : float
214
+ The refraction correction itself, in degrees
215
+
216
+ Returns
217
+ -------
218
+ delta : array_like
219
+ Refraction correction computed at each input orientation
220
+
221
+ Notes
222
+ -----
223
+ If `q` is given, then the refraction correction in degrees
224
+ is calculated, stored and returned after updating any other given
225
+ parameters. Otherwise, the correction is returned w/out recalculating.
226
+
227
+ Alternatively, if a single numerical argument, or the `delta` keyword
228
+ argument is given, then the correction is stored with this value
229
+ instead of being recalculated.
230
+
231
+ Numpy-vectorized for the `q` argument. Note that this is not
232
+ an efficient vectorization, and only the last calculated value is
233
+ stored for use in the coordinate conversion functions.
234
+ """
235
+
236
+ if len(args) == 1 and len(kwargs) == 0:
237
+ v = args[0]
238
+ if np.isscalar(v):
239
+ self._set("ref_delta", v)
240
+ return v
241
+
242
+ if "delta" in kwargs:
243
+ v = kwargs.get("delta")
244
+ self._set("ref_delta", v)
245
+ return v
246
+
247
+ arg_names = ["q"] + list(self._funcs["weather"])
248
+ for idx, a in enumerate(args):
249
+ kwargs[arg_names[idx]] = a
250
+
251
+ for w in self._funcs["weather"]:
252
+ if w in kwargs:
253
+ self._set(w, kwargs.get(w))
254
+
255
+ q = kwargs.get("q", None)
256
+ if q is not None:
257
+
258
+ def func(x0, x1, x2, x3):
259
+ q = np.ascontiguousarray([x0, x1, x2, x3])
260
+ return qp.qp_update_ref(self._memory, q)
261
+
262
+ fvec = np.vectorize(func, [np.double])
263
+ if q.size // 4 > 1:
264
+ q = q.transpose()
265
+ delta = fvec(q[0], q[1], q[2], q[3])
266
+ if delta.shape == ():
267
+ return delta[()]
268
+ return delta
269
+ return self._get("ref_delta")
270
+
271
+ def gmst(self, ctime, **kwargs):
272
+ """
273
+ Return Greenwich mean sidereal time for given ctimes.
274
+ Vectorized.
275
+
276
+ Arguments
277
+ ---------
278
+ ctime : array_like
279
+ Unix time in seconds UTC
280
+
281
+ Returns
282
+ -------
283
+ gmst : array_like
284
+ Greenwich mean sidereal time of the observer
285
+
286
+ Notes
287
+ -----
288
+ Any keywords accepted by the :meth:`set` method can also be passed here,
289
+ and will be processed prior to calculation.
290
+ """
291
+
292
+ self.set(**kwargs)
293
+
294
+ ctime = check_input("ctime", np.atleast_1d(ctime))
295
+ n = ctime.size
296
+
297
+ gmst = check_output("gmst", shape=ctime.shape)
298
+ qp.qp_gmstn(self._memory, ctime, gmst, n)
299
+
300
+ if n == 1:
301
+ return gmst[0]
302
+ return gmst
303
+
304
+ def lmst(self, ctime, lon, **kwargs):
305
+ """
306
+ Return local mean sidereal time for given ctimes and longitudes.
307
+ Vectorized, input arguments must be broadcastable to the same shape.
308
+
309
+ Arguments
310
+ ---------
311
+ ctime : array_like
312
+ Unix time in seconds UTC
313
+ lon : array_like
314
+ Observer longitude (degrees)
315
+
316
+ Returns
317
+ -------
318
+ lmst : array_like
319
+ Local mean sidereal time of the observer
320
+
321
+ Notes
322
+ -----
323
+ Any keywords accepted by the :meth:`set` method can also be passed here,
324
+ and will be processed prior to calculation.
325
+ """
326
+
327
+ self.set(**kwargs)
328
+
329
+ ctime, lon = check_inputs(ctime, lon)
330
+ n = ctime.size
331
+
332
+ lmst = check_output("lmst", shape=ctime.shape)
333
+ qp.qp_lmstn(self._memory, ctime, lon, lmst, n)
334
+
335
+ if n == 1:
336
+ return lmst[0]
337
+ return lmst
338
+
339
+ def dipole(self, ctime, ra, dec, **kwargs):
340
+ """
341
+ Return dipole amplitude in the given equatorial direction.
342
+ Vectorized, input arguments must be broadcastable to the same shape.
343
+
344
+ Arguments
345
+ ---------
346
+ ctime : array_like
347
+ Unix time in seconds UTC
348
+ ra : array_like
349
+ Right ascension on the sky, in degrees.
350
+ dec : array_like
351
+ Declination on the sky, in degrees
352
+
353
+ Returns
354
+ -------
355
+ dipole : array_like
356
+ Dipole amplitude in K
357
+
358
+ Notes
359
+ -----
360
+ Any keywords accepted by the :meth:`set` method can also be passed here,
361
+ and will be processed prior to calculation.
362
+ """
363
+
364
+ self.set(**kwargs)
365
+
366
+ ctime, ra, dec = check_inputs(ctime, ra, dec)
367
+ n = ctime.size
368
+
369
+ dipole = check_output("dipole", shape=ctime.shape, **kwargs)
370
+ qp.qp_dipolen(self._memory, ctime, ra, dec, dipole, n)
371
+
372
+ if n == 1:
373
+ return dipole[0]
374
+ return dipole
375
+
376
+ def bore2dipole(self, q_off, ctime, q_bore, **kwargs):
377
+ """
378
+ Calculate dipole timestream for given offset and boresight pointing.
379
+
380
+ Arguments
381
+ ---------
382
+ q_off : quaternion
383
+ Detector offset quaternion for a single detector, calculated using
384
+ :meth:`det_offset`
385
+ ctime : array_like
386
+ Array of unix times in seconds UTC
387
+ q_bore : quaternion or array of quaternions
388
+ Array of quaternions encoding the boresight orientation on the sky
389
+ (as output by :meth:`azel2radec` or similar). Broadcastable to the
390
+ same length as `ctime`.
391
+
392
+ Returns
393
+ -------
394
+ dipole : array_like
395
+ Dipole amplitude in K
396
+
397
+ Notes
398
+ -----
399
+ Any keywords accepted by the :meth:`set` method can also be passed here,
400
+ and will be processed prior to calculation.
401
+ """
402
+
403
+ self.set(**kwargs)
404
+
405
+ q_off = check_input("q_off", q_off, shape=(4,), quat=True)
406
+ ctime = check_input("ctime", ctime)
407
+ n = ctime.size
408
+ q_bore = check_input("q_bore", q_bore, shape=(n, 4), quat=True)
409
+
410
+ dipole = check_output("dipole", shape=ctime.shape, **kwargs)
411
+ qp.qp_bore2dipole(self._memory, q_off, ctime, q_bore, dipole, n)
412
+
413
+ if n == 1:
414
+ return dipole[0]
415
+ return dipole
416
+
417
+ def det_offset(self, delta_az, delta_el, delta_psi):
418
+ """
419
+ Return quaternion corresponding to the requested detector centroid
420
+ offset from boresight. Vectorized, input arguments must be
421
+ broadcastable to the same shape.
422
+
423
+ Arguments
424
+ ---------
425
+ delta_az : array_like
426
+ Azimuthal centroid offset of the detector in degrees
427
+ delta_el : array_like
428
+ Elevation centroid offset of the detector in degrees
429
+ delta_psi : array_like
430
+ Polarization offset of the detector from vertical in degrees
431
+
432
+ Returns
433
+ -------
434
+ q : array_like
435
+ Detector centroid offset quaternion for each detector
436
+ """
437
+
438
+ delta_az, delta_el, delta_psi = check_inputs(delta_az, delta_el, delta_psi)
439
+ ndet = delta_az.size
440
+
441
+ quat = check_output("quat", shape=(ndet, 4))
442
+ qp.qp_det_offsetn(delta_az, delta_el, delta_psi, quat, ndet)
443
+
444
+ if ndet == 1:
445
+ return quat[0]
446
+ return quat
447
+
448
+ def bore_offset(
449
+ self, q_bore, ang1=None, ang2=None, ang3=None, post=False, inplace=False
450
+ ):
451
+ """
452
+ Apply a fixed or variable offset to the boresight quaternion.
453
+ Input arguments must be broadcastable to the same shape.
454
+
455
+ Arguments
456
+ ---------
457
+ q_bore : array_like
458
+ boresight pointing quaternion
459
+ ang1 : array_like, optional
460
+ Azimuthal or ra offset in degrees
461
+ ang2 : array_like, optional
462
+ Elevation or dec offset in degrees
463
+ ang3 : array_like, optional
464
+ Position angle offset in degrees
465
+ post : bool, optional
466
+ If False, apply offset as an az/el/pa pre-rotation
467
+ If True, apply offset as an ra/dec/pa post-rotation
468
+ inplace : bool, optional
469
+ If True, apply the rotation in-place in memory.
470
+
471
+ Returns
472
+ -------
473
+ q_bore : array_like
474
+ Offset boresight quaternion
475
+ """
476
+ q_bore = check_input(
477
+ "q_bore", np.atleast_2d(q_bore), quat=True, inplace=inplace, output=True
478
+ )
479
+ n = q_bore.shape[0]
480
+ if all([a is None for a in [ang1, ang2, ang3]]):
481
+ raise ValueError("One of ang1, ang2, ang3 is required")
482
+ ang1, ang2, ang3 = check_inputs(ang1, ang2, ang3, shape=(n,))
483
+ qp.qp_bore_offset(self._memory, q_bore, ang1, ang2, ang3, n, int(post))
484
+ return q_bore
485
+
486
+ def hwp_quat(self, theta):
487
+ """
488
+ Return quaternion corresponding to rotation by 2 * theta, where theta is
489
+ the physical waveplate angle. Vectorized.
490
+
491
+ Arguments
492
+ ---------
493
+ theta : array_like
494
+ HWP physical angle in degrees
495
+
496
+ Returns
497
+ -------
498
+ q : array_like
499
+ Quaternion for each hwp angle
500
+ """
501
+ theta = check_input("theta", np.atleast_1d(theta))
502
+ n = theta.size
503
+
504
+ quat = check_output("quat", shape=(n, 4))
505
+ qp.qp_hwp_quatn(theta, quat, n)
506
+
507
+ if n == 1:
508
+ return quat[0]
509
+ return quat
510
+
511
+ def azel2bore(self, az, el, pitch, roll, lon, lat, ctime, q=None, **kwargs):
512
+ """
513
+ Estimate the quaternion for the boresight orientation on the sky given
514
+ the attitude (az/el/pitch/roll), location on the earth (lon/lat) and
515
+ ctime. Input vectors must be numpy-array-like and broadcastable to the
516
+ same shape.
517
+
518
+ Arguments
519
+ ---------
520
+ az : array_like
521
+ Boresight azimuth in degrees
522
+ el : array_like
523
+ Boresight elevation in degrees
524
+ pitch : array_like
525
+ Boresight pitch in degrees. If `None`, this term is ignored.
526
+ roll : array_like
527
+ Boresight roll in degrees. If `None`, this term is ignored.
528
+ lon : array_like
529
+ Observer longitude in degrees
530
+ lat : array_like
531
+ Observer latitude in degrees
532
+ ctime : array_like
533
+ Unix time in seconds UTC
534
+ q : array_like, optional
535
+ Output quaternion array initialized by user. Supply this
536
+ for in-place computation.
537
+
538
+ Returns
539
+ -------
540
+ q : array_like
541
+ Nx4 numpy array of quaternions for each supplied timestamp.
542
+
543
+ Notes
544
+ -----
545
+ Any keywords accepted by the :meth:`set` method can also be passed here,
546
+ and will be processed prior to calculation.
547
+ """
548
+
549
+ self.set(**kwargs)
550
+
551
+ az, el, pitch, roll, lon, lat, ctime = check_inputs(
552
+ az, el, pitch, roll, lon, lat, ctime
553
+ )
554
+ n = az.size
555
+
556
+ # identity quaternion
557
+ q = check_output("q", q, shape=(n, 4), fill=[1, 0, 0, 0])
558
+
559
+ qp.qp_azel2bore(self._memory, az, el, pitch, roll, lon, lat, ctime, q, n)
560
+
561
+ return q
562
+
563
+ def azelpsi2bore(self, az, el, psi, pitch, roll, lon, lat, ctime, q=None, **kwargs):
564
+ """
565
+ Estimate the quaternion for the boresight orientation on the sky given
566
+ the attitude (az/el/psi/pitch/roll), location on the earth (lon/lat) and
567
+ ctime. Input vectors must be numpy-array-like and broadcastable to the
568
+ same shape.
569
+
570
+ This is an augmented version of azel2bore to accept rotations of the focal
571
+ plane about the optical axis.
572
+
573
+ Arguments
574
+ ---------
575
+ az : array_like
576
+ Boresight azimuth in degrees
577
+ el : array_like
578
+ Boresight elevation in degrees
579
+ psi : array_like
580
+ Boresight rotation angle in degrees
581
+ pitch : array_like
582
+ Boresight pitch in degrees. If `None`, this term is ignored.
583
+ roll : array_like
584
+ Boresight roll in degrees. If `None`, this term is ignored.
585
+ lon : array_like
586
+ Observer longitude in degrees
587
+ lat : array_like
588
+ Observer latitude in degrees
589
+ ctime : array_like
590
+ Unix time in seconds UTC
591
+ q : array_like, optional
592
+ Output quaternion array initialized by user. Supply this
593
+ for in-place computation.
594
+
595
+ Returns
596
+ -------
597
+ q : array_like
598
+ Nx4 numpy array of quaternions for each supplied timestamp.
599
+
600
+ Notes
601
+ -----
602
+ Any keywords accepted by the :meth:`set` method can also be passed here,
603
+ and will be processed prior to calculation.
604
+ """
605
+
606
+ self.set(**kwargs)
607
+
608
+ az, el, psi, pitch, roll, lon, lat, ctime = check_inputs(
609
+ az, el, psi, pitch, roll, lon, lat, ctime
610
+ )
611
+ n = az.size
612
+
613
+ # identity quaternion
614
+ q = check_output("q", q, shape=(n, 4), fill=[1, 0, 0, 0])
615
+
616
+ qp.qp_azelpsi2bore(
617
+ self._memory, az, el, psi, pitch, roll, lon, lat, ctime, q, n
618
+ )
619
+
620
+ return q
621
+
622
+ def bore2radec(
623
+ self,
624
+ q_off,
625
+ ctime,
626
+ q_bore,
627
+ q_hwp=None,
628
+ sindec=False,
629
+ return_pa=False,
630
+ ra=None,
631
+ dec=None,
632
+ pa=None,
633
+ sin2psi=None,
634
+ cos2psi=None,
635
+ **kwargs,
636
+ ):
637
+ """
638
+ Calculate the orientation on the sky for a detector offset from the
639
+ boresight. Detector offsets are defined assuming the boresight is
640
+ pointed toward the horizon, and that the boresight polarization axis is
641
+ along the vertical.
642
+
643
+ Arguments
644
+ ---------
645
+ q_off : quaternion
646
+ Detector offset quaternion for a single detector, calculated using
647
+ :meth:`det_offset`.
648
+ ctime : array_like
649
+ Unix time in seconds UTC, broadcastable to shape (N,),
650
+ the long dimension of `q_bore`.
651
+ q_bore : quaternion or array of quaternions
652
+ Nx4 array of quaternions encoding the boresight orientation on the
653
+ sky (as output by :meth:`azel2radec` or equivalent)
654
+ q_hwp : quaternion or array of quaternions, optional
655
+ HWP angle quaternions calculated using :meth:`hwp_quat`. Must be
656
+ broadcastable to the same shape as `q_bore`.
657
+ sindec : bool, optional
658
+ If `True`, return sin(dec) instead of dec in degrees
659
+ (default False).
660
+ return_pa : bool, optional
661
+ If `True`, return pa instead of sin2psi / cos2psi
662
+
663
+ Returns
664
+ -------
665
+ ra : array_like
666
+ Detector right ascension in degrees
667
+ dec/sindec : array_like
668
+ Detector declination in degrees or sin(dec) if `sindec` is `True`.
669
+ pa/sin2psi : array_like
670
+ Detector polarization orientation if `return_pa` is `True`, or
671
+ sin(2*pa) if `return_pa` is `False`.
672
+ cos2psi : array_like
673
+ detector polarization orientation cos(2*pa), if `return_pa` is `False`.
674
+
675
+ Notes
676
+ -----
677
+ Any keywords accepted by the :meth:`set` method can also be passed here,
678
+ and will be processed prior to calculation.
679
+
680
+ Pre-allocated output arguments can also be supplied as input keywords
681
+ for in-place operation.
682
+ """
683
+
684
+ self.set(**kwargs)
685
+
686
+ q_off = check_input("q_off", q_off, quat=True)
687
+ q_bore = check_input("q_bore", np.atleast_2d(q_bore), quat=True)
688
+ shape = (q_bore.size // 4,)
689
+ if ctime is None:
690
+ if not self.get("mean_aber"):
691
+ raise ValueError("ctime required if mean_aber is False")
692
+ ctime = np.zeros(shape, dtype=q_bore.dtype)
693
+ ctime = check_input("ctime", ctime, shape=shape)
694
+ pars = dict(shape=ctime.shape, dtype=np.double)
695
+ ra = check_output("ra", ra, **pars)
696
+ dec = check_output("dec", dec, **pars)
697
+ if return_pa:
698
+ if sindec:
699
+ raise ValueError("Cannot use sindec with return_pa=True")
700
+ pa = check_output("pa", pa, **pars)
701
+ else:
702
+ sin2psi = check_output("sin2psi", sin2psi, **pars)
703
+ cos2psi = check_output("cos2psi", cos2psi, **pars)
704
+ n = ctime.size
705
+
706
+ if q_hwp is None:
707
+ if return_pa:
708
+ qp.qp_bore2radecpa(self._memory, q_off, ctime, q_bore, ra, dec, pa, n)
709
+ elif sindec:
710
+ qp.qp_bore2rasindec(
711
+ self._memory, q_off, ctime, q_bore, ra, dec, sin2psi, cos2psi, n
712
+ )
713
+ else:
714
+ qp.qp_bore2radec(
715
+ self._memory, q_off, ctime, q_bore, ra, dec, sin2psi, cos2psi, n
716
+ )
717
+ else:
718
+ q_hwp = check_input("q_hwp", q_hwp, shape=q_bore.shape)
719
+ if return_pa:
720
+ qp.qp_bore2radecpa_hwp(
721
+ self._memory, q_off, ctime, q_bore, q_hwp, ra, dec, pa, n
722
+ )
723
+ elif sindec:
724
+ qp.qp_bore2rasindec_hwp(
725
+ self._memory,
726
+ q_off,
727
+ ctime,
728
+ q_bore,
729
+ q_hwp,
730
+ ra,
731
+ dec,
732
+ sin2psi,
733
+ cos2psi,
734
+ n,
735
+ )
736
+ else:
737
+ qp.qp_bore2radec_hwp(
738
+ self._memory,
739
+ q_off,
740
+ ctime,
741
+ q_bore,
742
+ q_hwp,
743
+ ra,
744
+ dec,
745
+ sin2psi,
746
+ cos2psi,
747
+ n,
748
+ )
749
+
750
+ if return_pa:
751
+ if n == 1:
752
+ return ra[0], dec[0], pa[0]
753
+ return ra, dec, pa
754
+
755
+ if n == 1:
756
+ return ra[0], dec[0], sin2psi[0], cos2psi[0]
757
+ return ra, dec, sin2psi, cos2psi
758
+
759
+ def azel2radec(
760
+ self,
761
+ delta_az,
762
+ delta_el,
763
+ delta_psi,
764
+ az,
765
+ el,
766
+ pitch,
767
+ roll,
768
+ lon,
769
+ lat,
770
+ ctime,
771
+ hwp=None,
772
+ sindec=False,
773
+ return_pa=False,
774
+ ra=None,
775
+ dec=None,
776
+ pa=None,
777
+ sin2psi=None,
778
+ cos2psi=None,
779
+ **kwargs,
780
+ ):
781
+ """
782
+ Estimate the orientation on the sky for a detector offset from
783
+ boresight, given the boresight attitude (az/el/pitch/roll), location on
784
+ the earth (lon/lat) and UTC time. Input vectors must be
785
+ numpy-array-like and broadcastable to the same shape. Detector offsets
786
+ are defined assuming the boresight is pointed toward the horizon, and
787
+ that the boresight polarization axis is along the horizontal.
788
+
789
+ Arguments
790
+ ---------
791
+ delta_az : float
792
+ Azimuthal offset of the detector in degrees
793
+ delta_el : float
794
+ Elevation offset of the detector in degrees
795
+ delta_psi : float
796
+ Polarization offset of the detector in degrees
797
+ az : array_like
798
+ Boresight azimuth in degrees
799
+ el : array_like
800
+ Boresight elevation in degrees
801
+ pitch : array_like
802
+ Boresight pitch in degrees. If None, this term is ignored.
803
+ roll : array_like
804
+ Boresight roll in degrees. If None, this term is ignored.
805
+ lon : array_like
806
+ Observer longitude in degrees.
807
+ lat : array_like
808
+ Observer latitude in degrees.
809
+ ctime : array_like
810
+ Unix time in seconds UTC
811
+ hwp : array_like, optional
812
+ HWP angles in degrees
813
+ sindec : bool, optional
814
+ If `True`, return sin(dec) instead of dec in degrees (default False)
815
+ return_pa : bool, optional
816
+ If `True`, return pa instead of sin2psi/cos2psi
817
+
818
+ Returns
819
+ -------
820
+ ra : array_like
821
+ Detector right ascension in degrees
822
+ dec/sindec : array_like
823
+ Detector declination in degrees
824
+ pa : array_like
825
+ Detector position angle, if `return_pa` is True
826
+ sin2psi : array_like
827
+ Detector polarization orientation, if `return_pa` is False
828
+ cos2psi : array_like
829
+ Detector polarization orientation, if `return_pa` is False
830
+
831
+ Notes
832
+ -----
833
+ Any keywords accepted by the :meth:`set` method can also be passed here,
834
+ and will be processed prior to calculation.
835
+ """
836
+
837
+ self.set(**kwargs)
838
+
839
+ az, el, pitch, roll, lon, lat, ctime = check_inputs(
840
+ az, el, pitch, roll, lon, lat, ctime
841
+ )
842
+
843
+ ra = check_output("ra", ra, shape=az.shape, dtype=np.double)
844
+ dec = check_output("dec", dec, shape=az.shape, dtype=np.double)
845
+ if return_pa:
846
+ pa = check_output("pa", pa, shape=az.shape, dtype=np.double)
847
+ else:
848
+ sin2psi = check_output("sin2psi", sin2psi, shape=az.shape, dtype=np.double)
849
+ cos2psi = check_output("cos2psi", cos2psi, shape=az.shape, dtype=np.double)
850
+ n = az.size
851
+
852
+ if hwp is None:
853
+ if return_pa:
854
+ qp.qp_azel2radecpa(
855
+ self._memory,
856
+ delta_az,
857
+ delta_el,
858
+ delta_psi,
859
+ az,
860
+ el,
861
+ pitch,
862
+ roll,
863
+ lon,
864
+ lat,
865
+ ctime,
866
+ ra,
867
+ dec,
868
+ pa,
869
+ n,
870
+ )
871
+ elif sindec:
872
+ qp.qp_azel2rasindec(
873
+ self._memory,
874
+ delta_az,
875
+ delta_el,
876
+ delta_psi,
877
+ az,
878
+ el,
879
+ pitch,
880
+ roll,
881
+ lon,
882
+ lat,
883
+ ctime,
884
+ ra,
885
+ dec,
886
+ sin2psi,
887
+ cos2psi,
888
+ n,
889
+ )
890
+ else:
891
+ qp.qp_azel2radec(
892
+ self._memory,
893
+ delta_az,
894
+ delta_el,
895
+ delta_psi,
896
+ az,
897
+ el,
898
+ pitch,
899
+ roll,
900
+ lon,
901
+ lat,
902
+ ctime,
903
+ ra,
904
+ dec,
905
+ sin2psi,
906
+ cos2psi,
907
+ n,
908
+ )
909
+ else:
910
+ hwp = check_input("hwp", hwp, shape=az.shape)
911
+
912
+ if return_pa:
913
+ qp.qp_azel2radec_hwp(
914
+ self._memory,
915
+ delta_az,
916
+ delta_el,
917
+ delta_psi,
918
+ az,
919
+ el,
920
+ pitch,
921
+ roll,
922
+ lon,
923
+ lat,
924
+ ctime,
925
+ hwp,
926
+ ra,
927
+ dec,
928
+ pa,
929
+ n,
930
+ )
931
+ elif sindec:
932
+ qp.qp_azel2rasindec_hwp(
933
+ self._memory,
934
+ delta_az,
935
+ delta_el,
936
+ delta_psi,
937
+ az,
938
+ el,
939
+ pitch,
940
+ roll,
941
+ lon,
942
+ lat,
943
+ ctime,
944
+ hwp,
945
+ ra,
946
+ dec,
947
+ sin2psi,
948
+ cos2psi,
949
+ n,
950
+ )
951
+ else:
952
+ qp.qp_azel2radec_hwp(
953
+ self._memory,
954
+ delta_az,
955
+ delta_el,
956
+ delta_psi,
957
+ az,
958
+ el,
959
+ pitch,
960
+ roll,
961
+ lon,
962
+ lat,
963
+ ctime,
964
+ hwp,
965
+ ra,
966
+ dec,
967
+ sin2psi,
968
+ cos2psi,
969
+ n,
970
+ )
971
+
972
+ if return_pa:
973
+ return ra, dec, pa
974
+ return ra, dec, sin2psi, cos2psi
975
+
976
+ def azelpsi2radec(
977
+ self,
978
+ delta_az,
979
+ delta_el,
980
+ delta_psi,
981
+ az,
982
+ el,
983
+ psi,
984
+ pitch,
985
+ roll,
986
+ lon,
987
+ lat,
988
+ ctime,
989
+ hwp=None,
990
+ sindec=False,
991
+ return_pa=False,
992
+ ra=None,
993
+ dec=None,
994
+ pa=None,
995
+ sin2psi=None,
996
+ cos2psi=None,
997
+ **kwargs,
998
+ ):
999
+ """
1000
+ Estimate the orientation on the sky for a detector offset from
1001
+ boresight, given the boresight attitude (az/el/pitch/roll), location on
1002
+ the earth (lon/lat) and UTC time. Input vectors must be
1003
+ numpy-array-like and broadcastable to the same shape. Detector offsets
1004
+ are defined assuming the boresight is pointed toward the horizon, and
1005
+ that the boresight polarization axis is along the horizontal.
1006
+
1007
+ This is agumented from azel2radec(), to accept rotations of the focal
1008
+ plane about the optical axis.
1009
+
1010
+ Arguments
1011
+ ---------
1012
+ delta_az : float
1013
+ Azimuthal offset of the detector in degrees
1014
+ delta_el : float
1015
+ Elevation offset of the detector in degrees
1016
+ delta_psi : float
1017
+ Polarization offset of the detector in degrees
1018
+ az : array_like
1019
+ Boresight azimuth in degrees
1020
+ el : array_like
1021
+ Boresight elevation in degrees
1022
+ psi : array_like
1023
+ Boresight rotation in degrees
1024
+ pitch : array_like
1025
+ Boresight pitch in degrees. If None, this term is ignored.
1026
+ roll : array_like
1027
+ Boresight roll in degrees. If None, this term is ignored.
1028
+ lon : array_like
1029
+ Observer longitude in degrees.
1030
+ lat : array_like
1031
+ Observer latitude in degrees.
1032
+ ctime : array_like
1033
+ Unix time in seconds UTC
1034
+ hwp : array_like, optional
1035
+ HWP angles in degrees
1036
+ sindec : bool, optional
1037
+ If `True`, return sin(dec) instead of dec in degrees (default False)
1038
+ return_pa : bool, optional
1039
+ If `True`, return pa instead of sin2psi/cos2psi
1040
+
1041
+ Returns
1042
+ -------
1043
+ ra : array_like
1044
+ Detector right ascension in degrees
1045
+ dec/sindec : array_like
1046
+ Detector declination in degrees
1047
+ pa : array_like
1048
+ Detector position angle, if `return_pa` is True
1049
+ sin2psi : array_like
1050
+ Detector polarization orientation, if `return_pa` is False
1051
+ cos2psi : array_like
1052
+ Detector polarization orientation, if `return_pa` is False
1053
+
1054
+ Notes
1055
+ -----
1056
+ Any keywords accepted by the :meth:`set` method can also be passed here,
1057
+ and will be processed prior to calculation.
1058
+ """
1059
+
1060
+ self.set(**kwargs)
1061
+
1062
+ az, el, psi, pitch, roll, lon, lat, ctime = check_inputs(
1063
+ az, el, psi, pitch, roll, lon, lat, ctime
1064
+ )
1065
+
1066
+ ra = check_output("ra", ra, shape=az.shape, dtype=np.double)
1067
+ dec = check_output("dec", dec, shape=az.shape, dtype=np.double)
1068
+ if return_pa:
1069
+ pa = check_output("pa", pa, shape=az.shape, dtype=np.double)
1070
+ else:
1071
+ sin2psi = check_output("sin2psi", sin2psi, shape=az.shape, dtype=np.double)
1072
+ cos2psi = check_output("cos2psi", cos2psi, shape=az.shape, dtype=np.double)
1073
+ n = az.size
1074
+
1075
+ if hwp is None:
1076
+ if return_pa:
1077
+ qp.qp_azelpsi2radecpa(
1078
+ self._memory,
1079
+ delta_az,
1080
+ delta_el,
1081
+ delta_psi,
1082
+ az,
1083
+ el,
1084
+ psi,
1085
+ pitch,
1086
+ roll,
1087
+ lon,
1088
+ lat,
1089
+ ctime,
1090
+ ra,
1091
+ dec,
1092
+ pa,
1093
+ n,
1094
+ )
1095
+ elif sindec:
1096
+ qp.qp_azelpsi2rasindec(
1097
+ self._memory,
1098
+ delta_az,
1099
+ delta_el,
1100
+ delta_psi,
1101
+ az,
1102
+ el,
1103
+ psi,
1104
+ pitch,
1105
+ roll,
1106
+ lon,
1107
+ lat,
1108
+ ctime,
1109
+ ra,
1110
+ dec,
1111
+ sin2psi,
1112
+ cos2psi,
1113
+ n,
1114
+ )
1115
+ else:
1116
+ qp.qp_azelpsi2radec(
1117
+ self._memory,
1118
+ delta_az,
1119
+ delta_el,
1120
+ delta_psi,
1121
+ az,
1122
+ el,
1123
+ psi,
1124
+ pitch,
1125
+ roll,
1126
+ lon,
1127
+ lat,
1128
+ ctime,
1129
+ ra,
1130
+ dec,
1131
+ sin2psi,
1132
+ cos2psi,
1133
+ n,
1134
+ )
1135
+ else:
1136
+ hwp = check_input("hwp", hwp, shape=az.shape)
1137
+
1138
+ if return_pa:
1139
+ qp.qp_azelpsi2radec_hwp(
1140
+ self._memory,
1141
+ delta_az,
1142
+ delta_el,
1143
+ delta_psi,
1144
+ az,
1145
+ el,
1146
+ psi,
1147
+ pitch,
1148
+ roll,
1149
+ lon,
1150
+ lat,
1151
+ ctime,
1152
+ hwp,
1153
+ ra,
1154
+ dec,
1155
+ pa,
1156
+ n,
1157
+ )
1158
+ elif sindec:
1159
+ qp.qp_azelpsi2rasindec_hwp(
1160
+ self._memory,
1161
+ delta_az,
1162
+ delta_el,
1163
+ delta_psi,
1164
+ az,
1165
+ el,
1166
+ psi,
1167
+ pitch,
1168
+ roll,
1169
+ lon,
1170
+ lat,
1171
+ ctime,
1172
+ hwp,
1173
+ ra,
1174
+ dec,
1175
+ sin2psi,
1176
+ cos2psi,
1177
+ n,
1178
+ )
1179
+ else:
1180
+ qp.qp_azelpsi2radec_hwp(
1181
+ self._memory,
1182
+ delta_az,
1183
+ delta_el,
1184
+ delta_psi,
1185
+ az,
1186
+ el,
1187
+ psi,
1188
+ pitch,
1189
+ roll,
1190
+ lon,
1191
+ lat,
1192
+ ctime,
1193
+ hwp,
1194
+ ra,
1195
+ dec,
1196
+ sin2psi,
1197
+ cos2psi,
1198
+ n,
1199
+ )
1200
+
1201
+ if return_pa:
1202
+ return ra, dec, pa
1203
+ return ra, dec, sin2psi, cos2psi
1204
+
1205
+ def radec2azel(
1206
+ self, ra, dec, pa, lon, lat, ctime, az=None, el=None, hpa=None, **kwargs
1207
+ ):
1208
+ """
1209
+ Estimate the horizon coordinates for a given set of equatorial coordinates
1210
+ (ra/dec/psi), location on the earth (lon/lat) and UTC time. Input vectors
1211
+ must be numpy-array-like and broadcastable to the same shape.
1212
+
1213
+ Arguments
1214
+ ---------
1215
+ ra : array_like
1216
+ Right ascension angle
1217
+ dec : array_like
1218
+ Declination angle
1219
+ pa : array_like
1220
+ Position angle in equatorial coordinates
1221
+ lon : array_like
1222
+ Observer longitude in degrees.
1223
+ lat : array_like
1224
+ Observer latitude in degrees.
1225
+ ctime : array_like
1226
+ Unix time in seconds UTC
1227
+
1228
+ Returns
1229
+ -------
1230
+ az : array_like
1231
+ Azimuth in degrees
1232
+ el : array_like
1233
+ Elevation in degrees
1234
+ hpa : array_like
1235
+ Position angle in horizon coordinates
1236
+
1237
+ Notes
1238
+ -----
1239
+ Any keywords accepted by the :meth:`set` method can also be passed here,
1240
+ and will be processed prior to calculation.
1241
+ """
1242
+
1243
+ self.set(**kwargs)
1244
+
1245
+ ra, dec, pa, lon, lat, ctime = check_inputs(ra, dec, pa, lon, lat, ctime)
1246
+
1247
+ az = check_output("az", az, shape=ra.shape, dtype=np.double)
1248
+ el = check_output("el", el, shape=ra.shape, dtype=np.double)
1249
+ hpa = check_output("hpa", hpa, shape=ra.shape, dtype=np.double)
1250
+ n = ra.size
1251
+
1252
+ qp.qp_radec2azel(self._memory, ra, dec, pa, lon, lat, ctime, az, el, hpa, n)
1253
+
1254
+ return az, el, hpa
1255
+
1256
+ def radecpa2quat(self, ra, dec, pa, **kwargs):
1257
+ """
1258
+ Calculate quaternion for input ra/dec/pa. Vectorized, input arguments
1259
+ must be broadcastable to the same shape.
1260
+
1261
+ Arguments
1262
+ ---------
1263
+ ra : array_like
1264
+ Right ascension angle
1265
+ dec : array_like
1266
+ Declination angle
1267
+ pa : array_like
1268
+ Position angle
1269
+
1270
+ Returns
1271
+ -------
1272
+ q : array_like
1273
+ Quaternion constructed from the input angles.
1274
+
1275
+ Notes
1276
+ -----
1277
+ Any keywords accepted by the :meth:`set` method can also be passed here,
1278
+ and will be processed prior to calculation.
1279
+ """
1280
+ self.set(**kwargs)
1281
+
1282
+ ra, dec, pa = check_inputs(ra, dec, pa)
1283
+ n = ra.size
1284
+ quat = check_output("quat", shape=(n, 4), dtype=np.double, **kwargs)
1285
+ qp.qp_radecpa2quatn(self._memory, ra, dec, pa, quat, n)
1286
+
1287
+ if n == 1:
1288
+ return quat[0]
1289
+ return quat
1290
+
1291
+ def quat2radecpa(self, quat, **kwargs):
1292
+ """
1293
+ Calculate ra/dec/pa for input quaternion(s).
1294
+
1295
+ Arguments
1296
+ ---------
1297
+ q : array_like
1298
+ Pointing quaternion
1299
+
1300
+ Returns
1301
+ -------
1302
+ ra : array_like
1303
+ Right ascension angle
1304
+ dec : array_like
1305
+ Declination angle
1306
+ pa : array_like
1307
+ Position angle
1308
+
1309
+ Notes
1310
+ -----
1311
+ Any keywords accepted by the :meth:`set` method can also be passed here,
1312
+ and will be processed prior to calculation.
1313
+ """
1314
+ self.set(**kwargs)
1315
+
1316
+ quat = check_input("quat", np.atleast_2d(quat), quat=True)
1317
+ n = quat.shape[0]
1318
+ ra = check_output("ra", shape=(n,), dtype=np.double, **kwargs)
1319
+ dec = check_output("dec", shape=(n,), dtype=np.double, **kwargs)
1320
+ pa = check_output("pa", shape=(n,), dtype=np.double, **kwargs)
1321
+
1322
+ qp.qp_quat2radecpan(self._memory, quat, ra, dec, pa, n)
1323
+ if n == 1:
1324
+ return ra[0], dec[0], pa[0]
1325
+ return ra, dec, pa
1326
+
1327
+ def quat2pixpa(self, quat, nside=256, **kwargs):
1328
+ """
1329
+ Calculate ra/dec/pa for input quaternion(s).
1330
+
1331
+ Arguments
1332
+ ---------
1333
+ q : array_like
1334
+ Pointing quaternion
1335
+
1336
+ Returns
1337
+ -------
1338
+ pix : array_like
1339
+ Pixel number(s) corresponding to the input positions(s).
1340
+ pa : array_like
1341
+ Position angle
1342
+
1343
+ Notes
1344
+ -----
1345
+ Any keywords accepted by the :meth:`set` method can also be passed here,
1346
+ and will be processed prior to calculation.
1347
+ """
1348
+ self.set(**kwargs)
1349
+
1350
+ quat = check_input("quat", np.atleast_2d(quat), quat=True)
1351
+ n = quat.shape[0]
1352
+ pix = check_output("pix", shape=(n,), dtype=int, **kwargs)
1353
+ pa = check_output("pa", shape=(n,), dtype=np.double, **kwargs)
1354
+
1355
+ qp.qp_quat2pixpan(self._memory, quat, pix, pa, n)
1356
+ if n == 1:
1357
+ return pix[0], pa[0]
1358
+ return pix, pa
1359
+
1360
+ def radec2pix(self, ra, dec, nside=256, **kwargs):
1361
+ """
1362
+ Calculate HEALpix pixel number for given ra/dec and nside. Vectorized,
1363
+ input arguments must be broadcastable to the same shape.
1364
+
1365
+ Arguments
1366
+ ---------
1367
+ ra : array_like
1368
+ Right ascension angle
1369
+ dec : array_like
1370
+ Declination angle
1371
+ nside : int
1372
+ HEALpix resolution parameter
1373
+
1374
+ Returns
1375
+ -------
1376
+ pix : array_like
1377
+ Pixel number(s) corresponding to the input positions(s).
1378
+
1379
+ Notes
1380
+ -----
1381
+ Any keywords accepted by the :meth:`set` method can also be passed here,
1382
+ and will be processed prior to calculation.
1383
+ """
1384
+
1385
+ self.set(**kwargs)
1386
+
1387
+ ra, dec = check_inputs(ra, dec)
1388
+ n = ra.size
1389
+
1390
+ pix = check_output("pix", shape=ra.shape, dtype=int, **kwargs)
1391
+ qp.qp_radec2pixn(self._memory, ra, dec, nside, pix, n)
1392
+
1393
+ if n == 1:
1394
+ return pix[0]
1395
+ return pix
1396
+
1397
+ def rotate_quat(self, quat, coord=["C", "G"], inplace=True, **kwargs):
1398
+ """
1399
+ Rotate a quaternion from one coordinate system to another.
1400
+ Supported coordinates:
1401
+
1402
+ C: celestial (equatorial) coordinates
1403
+ G: galactic coordinates
1404
+
1405
+ Arguments
1406
+ ---------
1407
+ quat : array_like
1408
+ array of quaternions, of shape (n, 4)
1409
+ coord : list, optional
1410
+ 2-element list of input and output coordinates
1411
+ inplace : bool, optional
1412
+ If True, apply the rotation in-place on the input quaternion.
1413
+ Otherwise, return a copy of the input array. Default: True.
1414
+
1415
+ Returns
1416
+ -------
1417
+ quat : array_like
1418
+ rotated quaternion array
1419
+
1420
+ Notes
1421
+ -----
1422
+ Any keywords accepted by the :meth:`set` method can also be passed here,
1423
+ and will be processed prior to calculation.
1424
+ """
1425
+
1426
+ self.set(**kwargs)
1427
+
1428
+ quat = check_input(
1429
+ "quat", np.atleast_2d(quat), quat=True, inplace=inplace, output=True
1430
+ )
1431
+ n = quat.size // 4
1432
+
1433
+ if coord[0] == "C" and coord[1] == "G":
1434
+ qp.qp_radec2gal_quatn(self._memory, quat, n)
1435
+ elif coord[0] == "G" and coord[1] == "C":
1436
+ qp.qp_gal2radec_quatn(self._memory, quat, n)
1437
+ else:
1438
+ raise ValueError("unsupported coord {}".format(coord))
1439
+
1440
+ return quat.squeeze()
1441
+
1442
+ def rotate_coord(
1443
+ self,
1444
+ ra,
1445
+ dec,
1446
+ pa=None,
1447
+ sin2psi=None,
1448
+ cos2psi=None,
1449
+ coord=["C", "G"],
1450
+ inplace=True,
1451
+ **kwargs,
1452
+ ):
1453
+ """
1454
+ Rotate coordinates from one coordinate system to another. Vectorized,
1455
+ input arguments must be broadcastable to the same shape.
1456
+ Supported coordinates:
1457
+
1458
+ C: celestial (equatorial) coordinates
1459
+ G: galactic coordinates
1460
+
1461
+ Arguments
1462
+ ---------
1463
+ ra, dec, pa : array_like
1464
+ -- or --
1465
+ ra, dec, sin2psi, cos2psi : array_like
1466
+ arrays of coordinates, of shape (n,)
1467
+ If none of pa, sin2psi or cos2psi are supplied,
1468
+ pa = 0 is assumed.
1469
+ coord : list, optional
1470
+ 2-element list of input and output coordinates.
1471
+ Supported systems: C, G.
1472
+ inplace : bool, optional
1473
+ If True, apply the rotation in-place on the input coordinates.
1474
+ Otherwise, return a copy of the input array. Default: True.
1475
+
1476
+ Returns
1477
+ -------
1478
+ ra, dec, pa : array_like
1479
+ -- or --
1480
+ ra, dec, sin2psi, cos2psi : array_like
1481
+ rotated coordinate arrays, same form as input
1482
+
1483
+ Notes
1484
+ -----
1485
+ Any keywords accepted by the :meth:`set` method can also be passed here,
1486
+ and will be processed prior to calculation.
1487
+ """
1488
+ self.set(**kwargs)
1489
+
1490
+ do_pa = True
1491
+ if pa is None:
1492
+ if sin2psi is None and cos2psi is None:
1493
+ pass
1494
+ elif sin2psi is None or cos2psi is None:
1495
+ raise KeyError("both sin2psi and cos2psi arguments required")
1496
+ else:
1497
+ do_pa = False
1498
+ else:
1499
+ if sin2psi is not None or cos2psi is not None:
1500
+ raise KeyError(
1501
+ "ambiguous pol arguments, supply either pa "
1502
+ "or sin2psi/cos2psi only"
1503
+ )
1504
+
1505
+ ra, dec, pa, sin2psi, cos2psi = check_inputs(
1506
+ ra, dec, pa, sin2psi, cos2psi, inplace=inplace, output=True
1507
+ )
1508
+ n = ra.size
1509
+
1510
+ if coord[0] == "C" and coord[1] == "G":
1511
+ if do_pa:
1512
+ qp.qp_radecpa2galn(self._memory, ra, dec, pa, n)
1513
+ else:
1514
+ qp.qp_radec2galn(self._memory, ra, dec, sin2psi, cos2psi, n)
1515
+ elif coord[0] == "G" and coord[1] == "C":
1516
+ if do_pa:
1517
+ qp.qp_gal2radecpan(self._memory, ra, dec, pa, n)
1518
+ else:
1519
+ qp.qp_gal2radecn(self._memory, ra, dec, sin2psi, cos2psi, n)
1520
+
1521
+ if n == 1:
1522
+ if do_pa:
1523
+ return ra[0], dec[0], pa[0]
1524
+ return ra[0], dec[0], sin2psi[0], cos2psi[0]
1525
+ if do_pa:
1526
+ return ra, dec, pa
1527
+ return ra, dec, sin2psi, cos2psi
1528
+
1529
+ def radec2gal(
1530
+ self, ra, dec, pa=None, sin2psi=None, cos2psi=None, inplace=True, **kwargs
1531
+ ):
1532
+ """
1533
+ Rotate celestial (equatorial) coordinates to galactic coordinates. This
1534
+ is equivalent to calling :meth:`rotate_coord` with `coord=['C', 'G']`.
1535
+ Vectorized, input arguments must be broadcastable to the same shape.
1536
+
1537
+ Arguments
1538
+ ---------
1539
+ ra, dec, pa : array_like
1540
+ -- or --
1541
+ ra, dec, sin2psi, cos2psi : array_like
1542
+ arrays of coordinates, of shape (n,). If none of pa, sin2psi or
1543
+ cos2psi are supplied, pa = 0 is assumed.
1544
+ inplace : bool, optional
1545
+ If True, apply the rotation in-place on the input quaternion.
1546
+ Otherwise, return a copy of the input array. Default: True.
1547
+
1548
+ Returns
1549
+ -------
1550
+ l, b, pa : array_like
1551
+ -- or --
1552
+ l, b, sin2psi, cos2psi : array_like
1553
+ rotated coordinate arrays, same form as input
1554
+
1555
+ Notes
1556
+ -----
1557
+ Any keywords accepted by the :meth:`set` method can also be passed here,
1558
+ and will be processed prior to calculation.
1559
+ """
1560
+ return self.rotate_coord(
1561
+ ra, dec, pa, sin2psi, cos2psi, coord=["C", "G"], inplace=inplace, **kwargs
1562
+ )
1563
+
1564
+ def gal2radec(
1565
+ self, ra, dec, pa=None, sin2psi=None, cos2psi=None, inplace=True, **kwargs
1566
+ ):
1567
+ """
1568
+ Rotate galactic coordinates to celestial (equatorial) coordinates. This
1569
+ is equivalent to calling :meth:`rotate_coord` with `coord=['G', 'C']`.
1570
+ Vectorized, input arguments must be broadcastable to the same shape.
1571
+
1572
+ Arguments
1573
+ ---------
1574
+ l, b, pa : array_like
1575
+ -- or --
1576
+ l, b, sin2psi, cos2psi : array_like
1577
+ arrays of coordinates, of shape (n,). If none of pa, sin2psi or
1578
+ cos2psi are supplied, pa = 0 is assumed.
1579
+ inplace : bool, optional
1580
+ If True, apply the rotation in-place on the input quaternion.
1581
+ Otherwise, return a copy of the input array. Default: True.
1582
+
1583
+ Returns
1584
+ -------
1585
+ ra, dec, pa : array_like
1586
+ -- or --
1587
+ ra, dec, sin2psi, cos2psi : array_like
1588
+ rotated coordinate arrays, same form as input
1589
+
1590
+ Notes
1591
+ -----
1592
+ Any keywords accepted by the :meth:`set` method can also be passed here,
1593
+ and will be processed prior to calculation.
1594
+ """
1595
+ return self.rotate_coord(
1596
+ ra, dec, pa, sin2psi, cos2psi, coord=["G", "C"], inplace=inplace, **kwargs
1597
+ )
1598
+
1599
+ def rotate_map(
1600
+ self, map_in, coord=["C", "G"], map_out=None, interp_pix=True, **kwargs
1601
+ ):
1602
+ """
1603
+ Rotate a polarized 3-x-npix map from one coordinate system to another.
1604
+ Supported coordinates:
1605
+
1606
+ C = celestial (equatorial J2000)
1607
+ G = galactic
1608
+
1609
+ Arguments
1610
+ ---------
1611
+ map_in : array_like
1612
+ Input map, of shape (3, N)
1613
+ coord : list, optional
1614
+ 2-element list of input and output coordinates.
1615
+ Supported systems: C, G.
1616
+ map_out : array_like, optional
1617
+ Rotated output map, for inplace operation. Same shape as `map_in`.
1618
+ interp_pix : bool, optional
1619
+ If True, interpolate the rotated map.
1620
+
1621
+ Returns
1622
+ -------
1623
+ map_out : array_like
1624
+ Rotated output map.
1625
+
1626
+ Notes
1627
+ -----
1628
+ Any keywords accepted by the :meth:`set` method can also be passed here,
1629
+ and will be processed prior to calculation.
1630
+
1631
+ Only full-sky maps are currently supported.
1632
+ """
1633
+
1634
+ warn("This code is buggy, use at your own risk", UserWarning)
1635
+
1636
+ interp_orig = self.get("interp_pix")
1637
+ self.set(interp_pix=interp_pix, **kwargs)
1638
+
1639
+ from .qmap_class import check_map
1640
+
1641
+ map_in, nside = check_map(map_in)
1642
+ map_out = check_output(
1643
+ "map_out", map_out, shape=map_in.shape, dtype=map_in.dtype, fill=0
1644
+ )
1645
+
1646
+ try:
1647
+ coord_in = coord[0]
1648
+ coord_out = coord[1]
1649
+ except:
1650
+ raise ValueError("unable to parse coord")
1651
+
1652
+ map_in_p = lib.pointer_2d(map_in)
1653
+ map_out_p = lib.pointer_2d(map_out)
1654
+
1655
+ qp.qp_rotate_map(self._memory, nside, map_in_p, coord_in, map_out_p, coord_out)
1656
+
1657
+ self.set(interp_pix=interp_orig)
1658
+ return map_out
1659
+
1660
+ def quat2pix(self, quat, nside=256, pol=True, **kwargs):
1661
+ """
1662
+ Calculate HEALpix pixel number and optional polarization angle
1663
+ for a given orientation.
1664
+
1665
+ Arguments
1666
+ ---------
1667
+ quat : quaternion or array of quaternions
1668
+ Pointing orientation(s)
1669
+ nside : int, optional
1670
+ HEALpix resolution parameter
1671
+ pol : bool, optional
1672
+ If True, return sin2psi and cos2psi along with the pixel number(s)
1673
+
1674
+ Returns
1675
+ -------
1676
+ pix : array_like
1677
+ Pixel number(s) for the given input quaternion(s)
1678
+ sin2psi : array_like
1679
+ cos2psi : array_like
1680
+ Polarization coefficients, if `pol` is `True`.
1681
+
1682
+ Notes
1683
+ -----
1684
+ Any keywords accepted by the :meth:`set` method can also be passed here,
1685
+ and will be processed prior to calculation.
1686
+ """
1687
+
1688
+ self.set(**kwargs)
1689
+
1690
+ quat = check_input("quat", np.atleast_2d(quat), quat=True)
1691
+
1692
+ n = quat.shape[0]
1693
+ shape = (n,)
1694
+ pix = check_output("pix", shape=shape, dtype=int, **kwargs)
1695
+ sin2psi = check_output("sin2psi", shape=shape, **kwargs)
1696
+ cos2psi = check_output("cos2psi", shape=shape, **kwargs)
1697
+ qp.qp_quat2pixn(self._memory, quat, nside, pix, sin2psi, cos2psi, n)
1698
+
1699
+ if n == 1:
1700
+ pix, sin2psi, cos2psi = pix[0], sin2psi[0], cos2psi[0]
1701
+ if pol:
1702
+ return pix, sin2psi, cos2psi
1703
+ return pix
1704
+
1705
+ def bore2pix(
1706
+ self,
1707
+ q_off,
1708
+ ctime,
1709
+ q_bore,
1710
+ q_hwp=None,
1711
+ nside=256,
1712
+ pol=True,
1713
+ return_pa=False,
1714
+ **kwargs,
1715
+ ):
1716
+ """
1717
+ Calculate the orientation on the sky for a detector offset from the
1718
+ boresight. Detector offsets are defined assuming the boresight is
1719
+ pointed toward the horizon, and that the boresight polarization axis is
1720
+ along the vertical.
1721
+
1722
+ Arguments
1723
+ ---------
1724
+ q_off : quaternion
1725
+ Detector offset quaternion for a single detector,
1726
+ calculated using :meth:`det_offset`.
1727
+ ctime : array_like
1728
+ Unix times in seconds UTC, broadcastable to shape (N,),
1729
+ the long dimenions of `q_bore`.
1730
+ q_bore : quaternion or array of quaternions
1731
+ Nx4 array of quaternions encoding the boresight orientation on the
1732
+ sky (as output by :meth:`azel2radec` or equivalent)
1733
+ q_hwp : quaternion or array of quaternions, optional
1734
+ HWP angle quaternions calculated using :meth:`hwp_quat`. Must be
1735
+ broadcastable to the same shape as `q_bore`.
1736
+ nside : int, optional
1737
+ HEALpix map dimension. Default: 256.
1738
+ pol : bool, optional
1739
+ If `False`, return only the pixel timestream
1740
+ return_pa : bool, optional
1741
+ If `True`, return pa instead of sin2psi / cos2psi
1742
+
1743
+ Returns
1744
+ -------
1745
+ pix : array_like
1746
+ Detector pixel number
1747
+ pa/sin2psi : array_like
1748
+ Detector polarization orientation if `return_pa` is `True`, or
1749
+ sin(2*pa) if `return_pa` is `False`.
1750
+ cos2psi : array_like
1751
+ detector polarization orientation cos(2*pa), if `return_pa` is `False`.
1752
+
1753
+ Notes
1754
+ -----
1755
+ Any keywords accepted by the :meth:`set` method can also be passed here,
1756
+ and will be processed prior to calculation.
1757
+ """
1758
+
1759
+ self.set(**kwargs)
1760
+
1761
+ q_off = check_input("q_off", q_off, quat=True)
1762
+ q_bore = check_input("q_bore", q_bore, quat=True)
1763
+ if ctime is None:
1764
+ if not self.get("mean_aber"):
1765
+ raise ValueError("ctime required if mean_aber is False")
1766
+ ctime = np.zeros((q_bore.size // 4,), dtype=q_bore.dtype)
1767
+ ctime = check_input("ctime", ctime)
1768
+ pix = check_output("pix", shape=ctime.shape, dtype=int, **kwargs)
1769
+ if return_pa:
1770
+ pa = check_output("pa", shape=ctime.shape, **kwargs)
1771
+ else:
1772
+ sin2psi = check_output("sin2psi", shape=ctime.shape, **kwargs)
1773
+ cos2psi = check_output("cos2psi", shape=ctime.shape, **kwargs)
1774
+ n = ctime.size
1775
+
1776
+ if q_hwp is None:
1777
+ if return_pa:
1778
+ qp.qp_bore2pixpa(self._memory, q_off, ctime, q_bore, nside, pix, pa, n)
1779
+ else:
1780
+ qp.qp_bore2pix(
1781
+ self._memory, q_off, ctime, q_bore, nside, pix, sin2psi, cos2psi, n
1782
+ )
1783
+ else:
1784
+ q_hwp = check_input("q_hwp", q_hwp, shape=q_bore.shape)
1785
+
1786
+ if return_pa:
1787
+ qp.qp_bore2pixpa_hwp(
1788
+ self._memory, q_off, ctime, q_bore, q_hwp, nside, pix, pa, n
1789
+ )
1790
+ else:
1791
+ qp.qp_bore2pix_hwp(
1792
+ self._memory,
1793
+ q_off,
1794
+ ctime,
1795
+ q_bore,
1796
+ q_hwp,
1797
+ nside,
1798
+ pix,
1799
+ sin2psi,
1800
+ cos2psi,
1801
+ n,
1802
+ )
1803
+
1804
+ if pol is True:
1805
+ if return_pa:
1806
+ return pix, pa
1807
+ return pix, sin2psi, cos2psi
1808
+ return pix
1809
+
1810
+ def get_interp_val(self, map_in, ra, dec, nest=False):
1811
+ """
1812
+ Interpolate map pixels to these coordinates. Uses a C implementation
1813
+ of the bilinear interpolation method `get_interpol()` as implemented
1814
+ in the equivalent healpix_cxx / healpy function.
1815
+
1816
+ Arguments
1817
+ ---------
1818
+ map_in : array_like
1819
+ A single healpix map or list of maps which to interpolate from.
1820
+ ra, dec: array_like
1821
+ Timestreams of coordinates to interpolate to, in degrees,
1822
+ broadcastable to a common shape (nsample,)
1823
+ nest: bool, optional
1824
+ If True, input map is in the nested pixel ordering scheme.
1825
+ Otherwise, ring ordering is assumed.
1826
+ Default: False.
1827
+
1828
+ Returns
1829
+ -------
1830
+ values : array_like
1831
+ Array of interpolated map values, of shape (nmap, nsample).
1832
+ """
1833
+
1834
+ pix_order = self.get("pix_order")
1835
+ if nest:
1836
+ self.set(pix_order="nest")
1837
+ else:
1838
+ self.set(pix_order="ring")
1839
+
1840
+ ra, dec = check_inputs(ra, dec)
1841
+ n = ra.size
1842
+
1843
+ from .qmap_class import check_map
1844
+
1845
+ map_in, nside = check_map(map_in)
1846
+
1847
+ val = check_output("value", shape=(len(map_in), n))
1848
+
1849
+ for m, v in zip(map_in, val):
1850
+ qp.qp_get_interp_valn(self._memory, nside, m, ra, dec, v, n)
1851
+
1852
+ self.set(pix_order=pix_order)
1853
+
1854
+ v = v.squeeze()
1855
+ if not v.shape:
1856
+ return v[()]
1857
+ return v
1858
+
1859
+ def update_bulletin_a(self, start_year=2000):
1860
+ """
1861
+ Update the IERS Bulletin A database using astropy, and return the stored
1862
+ entries. Issues an ImportWarning if astropy version 1.2 or newer is not
1863
+ found.
1864
+
1865
+ Arguments
1866
+ ---------
1867
+ start_year : int, optional
1868
+ Oldest year for which data should be stored.
1869
+
1870
+ Returns
1871
+ -------
1872
+ mjd : array_like
1873
+ Modified Julian date
1874
+ dut1 : array_like
1875
+ UT1-UTC time correction
1876
+ x : array_like
1877
+ y : array_like
1878
+ Polar motion (wobble) corrections
1879
+ """
1880
+ try:
1881
+ from astropy.utils.iers import IERS_Auto
1882
+ except ImportError:
1883
+ warn(
1884
+ "Compatible Astropy not found. Install astropy v1.2 or newer "
1885
+ "for accurate polar motion and UT1 corrections",
1886
+ ImportWarning,
1887
+ )
1888
+ return
1889
+
1890
+ columns = ["MJD", "UT1_UTC", "PM_x", "PM_y", "year"]
1891
+ iers_table = IERS_Auto.open()[columns].as_array()
1892
+
1893
+ # check year
1894
+ year = iers_table["year"] + 1900
1895
+ (wraps,) = np.where(np.ediff1d(year) < 0)
1896
+ for idx in wraps:
1897
+ year[idx + 1 :] += 100
1898
+ iers_table["year"] = year
1899
+ iers_table = iers_table[year >= start_year]
1900
+
1901
+ # check MJD
1902
+ mjds = iers_table["MJD"]
1903
+ mjd_min = int(mjds.min())
1904
+ mjd_max = int(mjds.max())
1905
+
1906
+ # update table
1907
+ dut1 = np.array(iers_table["UT1_UTC"])
1908
+ x = np.array(iers_table["PM_x"])
1909
+ y = np.array(iers_table["PM_y"])
1910
+ qp.qp_set_iers_bulletin_a(self._memory, mjd_min, mjd_max, dut1, x, y)
1911
+
1912
+ return mjds, dut1, x, y
1913
+
1914
+ def load_bulletin_a(self, filename, columns=["mjd", "dut1", "x", "y"], **kwargs):
1915
+ """
1916
+ Load IERS Bulletin A from file and store in memory. The file must be
1917
+ readable using `numpy.loadtxt` with `unpack=True`, and is assumed to be
1918
+ sorted by mjd.
1919
+
1920
+ Arguments
1921
+ ---------
1922
+ filename : string
1923
+ Name of the text file containing IERS Bulletin A parameters.
1924
+ columns : list of strings
1925
+ list of columns as they appear in the file.
1926
+ A KeyError is raise if the list does not contain
1927
+ each of ['mjd', 'dut1', 'x', 'y'].
1928
+
1929
+ Any other keyword arguments are passed to the `numpy.loadtxt` function
1930
+
1931
+ Returns
1932
+ -------
1933
+ mjd : array_like
1934
+ Modified Julian date
1935
+ dut1 : array_like
1936
+ UT1-UTC time correction
1937
+ x : array_like
1938
+ y : array_like
1939
+ Polar motion corrections
1940
+ """
1941
+
1942
+ req_columns = ["mjd", "dut1", "x", "y"]
1943
+ if not set(req_columns) <= set(columns):
1944
+ raise KeyError(
1945
+ "Missing columns {}".format(list(set(req_columns) - set(columns)))
1946
+ )
1947
+ kwargs["unpack"] = True
1948
+ data = np.loadtxt(filename, **kwargs)
1949
+ mjd, x, y, dut1 = (data[columns.index(x)] for x in req_columns)
1950
+ mjd_min, mjd_max = int(mjd[0]), int(mjd[-1])
1951
+
1952
+ try:
1953
+ qp.qp_set_iers_bulletin_a(self._memory, mjd_min, mjd_max, dut1, x, y)
1954
+ except:
1955
+ raise RuntimeError(
1956
+ "Error loading Bulletin A data from file {}".format(filename)
1957
+ )
1958
+
1959
+ return mjd, dut1, x, y
1960
+
1961
+ def get_bulletin_a(self, mjd):
1962
+ """
1963
+ Return dut1/x/y for given mjd. Numpy-vectorized.
1964
+ """
1965
+
1966
+ def func(x):
1967
+ return lib.qp_get_bulletin_a(self._memory, x)
1968
+
1969
+ fvec = np.vectorize(func, [np.double] * 3)
1970
+
1971
+ out = fvec(mjd)
1972
+ if out[0].shape == ():
1973
+ return tuple(x[()] for x in out)
1974
+ return out