stdpipe 0.4.0__py3-none-any.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.
stdpipe/catalogs.py ADDED
@@ -0,0 +1,942 @@
1
+ """
2
+ Module containing the routines for handling various online catalogues.
3
+ """
4
+
5
+ import os, posixpath, shutil, tempfile
6
+ import numpy as np
7
+
8
+ from astropy.table import Table, MaskedColumn, vstack
9
+ from astroquery.vizier import Vizier
10
+ from astroquery.xmatch import XMatch
11
+ from astroquery.imcce import Skybot
12
+ from astroquery.ipac.ned import Ned
13
+ from astropy.coordinates import SkyCoord
14
+ from astropy import units as u
15
+ from astropy.time import Time
16
+
17
+ from . import astrometry
18
+
19
+ catalogs = {
20
+ 'ps1': {'vizier': 'II/349/ps1', 'name': 'PanSTARRS DR1'},
21
+ 'gaiadr2': {'vizier': 'I/345/gaia2', 'name': 'Gaia DR2', 'extra': ['E(BR/RP)']},
22
+ 'gaiaedr3': {'vizier': 'I/350/gaiaedr3', 'name': 'Gaia EDR3'},
23
+ 'gaiadr3syn': {
24
+ 'vizier': 'I/360/syntphot',
25
+ 'name': 'Gaia DR3 synthetic photometry',
26
+ 'extra': ['**', '_RAJ2000', '_DEJ2000'],
27
+ },
28
+ 'usnob1': {'vizier': 'I/284/out', 'name': 'USNO-B1'},
29
+ 'gsc': {'vizier': 'I/271/out', 'name': 'GSC 2.2'},
30
+ 'skymapper': {
31
+ 'vizier': 'II/379/smssdr4',
32
+ 'name': 'SkyMapper DR4',
33
+ 'extra': [
34
+ '_RAJ2000',
35
+ '_DEJ2000',
36
+ 'e_uPSF',
37
+ 'e_vPSF',
38
+ 'e_gPSF',
39
+ 'e_rPSF',
40
+ 'e_iPSF',
41
+ 'e_zPSF',
42
+ ],
43
+ },
44
+ 'vsx': {'vizier': 'B/vsx/vsx', 'name': 'AAVSO VSX'},
45
+ 'apass': {'vizier': 'II/336/apass9', 'name': 'APASS DR9'},
46
+ 'sdss': {
47
+ 'vizier': 'V/154/sdss16',
48
+ 'name': 'SDSS DR16',
49
+ 'extra': ['_RAJ2000', '_DEJ2000'],
50
+ },
51
+ 'atlas': {
52
+ 'vizier': 'J/ApJ/867/105/refcat2',
53
+ 'name': 'ATLAS-REFCAT2',
54
+ 'extra': [
55
+ '_RAJ2000',
56
+ '_DEJ2000',
57
+ 'e_Gmag',
58
+ 'e_gmag',
59
+ 'e_rmag',
60
+ 'e_imag',
61
+ 'e_zmag',
62
+ 'e_Jmag',
63
+ 'e_Kmag',
64
+ ],
65
+ },
66
+ '2mass': {
67
+ 'vizier': 'II/246/out',
68
+ 'name': '2MASS PSC',
69
+ },
70
+ 'vhs': {
71
+ 'vizier': 'II/367/vhs_dr5',
72
+ 'name': 'Vista Hemisphere Survey DR5',
73
+ 'extra': [
74
+ 'e_Jap3',
75
+ 'e_Hap3',
76
+ 'e_Ksap3',
77
+ ]
78
+ },
79
+ }
80
+
81
+
82
+ def _detect_catalog_type(cat):
83
+ """
84
+ Detect catalog type from column names.
85
+
86
+ Parameters
87
+ ----------
88
+ cat : astropy.table.Table
89
+ Catalog table to inspect
90
+
91
+ Returns
92
+ -------
93
+ catalog_type : str or None
94
+ Detected catalog type ('ps1', 'gaiadr2', 'skymapper', 'apass',
95
+ 'gaiadr3syn', 'sdss', '2mass', 'vhs') or None if cannot detect
96
+ """
97
+ colnames = set(cat.colnames)
98
+
99
+ # Check most specific patterns first to avoid ambiguity
100
+
101
+ # Gaia DR3 synthetic photometry - has Fu, Fg, Fr, Fi, Fz flux columns
102
+ if {'Fu', 'Fg', 'Fr', 'Fi', 'Fz'}.issubset(colnames):
103
+ return 'gaiadr3syn'
104
+
105
+ # Gaia DR2 - has Gmag, BPmag, RPmag
106
+ if {'Gmag', 'BPmag', 'RPmag'}.issubset(colnames):
107
+ return 'gaiadr2'
108
+
109
+ # SkyMapper - has gPSF, rPSF, iPSF, zPSF (PSF suffix is distinctive)
110
+ if {'gPSF', 'rPSF', 'iPSF', 'zPSF'}.issubset(colnames):
111
+ return 'skymapper'
112
+
113
+ # APASS - has both SDSS-like (g_mag, r_mag, i_mag) AND Johnson (B_mag, V_mag)
114
+ if {'g_mag', 'r_mag', 'i_mag', 'B_mag', 'V_mag'}.issubset(colnames):
115
+ return 'apass'
116
+
117
+ # SDSS DR16 - has umag, gmag, rmag, imag, zmag (but NOT ymag)
118
+ # Need to distinguish from PS1 which also has gmag, rmag, imag, zmag
119
+ if {'umag', 'gmag', 'rmag', 'imag', 'zmag'}.issubset(colnames):
120
+ if 'ymag' not in colnames: # SDSS doesn't have y band
121
+ return 'sdss'
122
+
123
+ # Pan-STARRS or ATLAS - both have gmag, rmag, imag, zmag, ymag
124
+ # Cannot distinguish between them from columns alone (identical augmentation anyway)
125
+ if {'gmag', 'rmag', 'imag', 'zmag', 'ymag'}.issubset(colnames):
126
+ return 'ps1' # Could be 'atlas' too, but augmentation is identical
127
+
128
+ # 2MASS
129
+ if {'Jmag', 'Hmag', 'Kmag'}.issubset(colnames):
130
+ return '2mass'
131
+
132
+ # VHS
133
+ if {'Jap3', 'Hap3', 'Ksap3'}.issubset(colnames):
134
+ return 'vhs'
135
+
136
+ # Could not detect
137
+ return None
138
+
139
+
140
+ def _augment_ps1(cat, verbose=False):
141
+ """Augment Pan-STARRS DR1 or ATLAS-RefCat2 catalog with additional bands."""
142
+ log = (verbose if callable(verbose) else print) if verbose else lambda *args, **kwargs: None
143
+ try:
144
+ log("Augmenting the catalogue with Johnson-Cousins photometry using PS1 mags")
145
+
146
+ # Alternative PS1 transfromation from https://arxiv.org/pdf/1706.06147.pdf, Stetson, seems better with Landolt than official one
147
+ # cat['B'] = cat['gmag'] + 0.199 + 0.540*(cat['gmag'] - cat['rmag']) + 0.016*(cat['gmag'] - cat['rmag'])**2
148
+ # cat['V'] = cat['gmag'] - 0.020 - 0.498*(cat['gmag'] - cat['rmag']) - 0.008*(cat['gmag'] - cat['rmag'])**2
149
+ # cat['R'] = cat['rmag'] - 0.163 - 0.086*(cat['gmag'] - cat['rmag']) - 0.061*(cat['gmag'] - cat['rmag'])**2
150
+ # cat['I'] = cat['imag'] - 0.387 - 0.123*(cat['gmag'] - cat['rmag']) - 0.034*(cat['gmag'] - cat['rmag'])**2
151
+
152
+ # My own fit on Landolt+Stetson standards from https://arxiv.org/pdf/2205.06186.pdf
153
+ pB1, pB2 = (
154
+ [
155
+ 0.10339527794499666,
156
+ -0.492149523946056,
157
+ 1.2093816061394638,
158
+ 0.061925048331498395,
159
+ ],
160
+ [
161
+ -0.2571974580267897,
162
+ 0.9211495207523038,
163
+ -0.8243222108864755,
164
+ 0.0619250483314976,
165
+ ],
166
+ )
167
+ pV1, pV2 = (
168
+ [
169
+ -0.011452922062676726,
170
+ -9.949308251868327e-05,
171
+ -0.4650511584366353,
172
+ -0.007076854914511554,
173
+ ],
174
+ [
175
+ 0.012749150754020416,
176
+ 0.057554580469724864,
177
+ -0.09019328095355343,
178
+ -0.007076854914511329,
179
+ ],
180
+ )
181
+ pR1, pR2 = (
182
+ [
183
+ 0.004905242602502597,
184
+ -0.046545625824660514,
185
+ 0.07830702317352654,
186
+ -0.08438139204305026,
187
+ ],
188
+ [
189
+ -0.07782426914647306,
190
+ 0.14090289318728444,
191
+ -0.3634922073369279,
192
+ -0.08438139204305031,
193
+ ],
194
+ )
195
+ pI1, pI2 = (
196
+ [
197
+ -0.02239162647929074,
198
+ 0.04401240100377888,
199
+ -0.038500349283596795,
200
+ -0.19509051168348646,
201
+ ],
202
+ [
203
+ 0.014586929059030904,
204
+ -0.025228407778416825,
205
+ -0.21476143248697746,
206
+ -0.19509051168348637,
207
+ ],
208
+ )
209
+
210
+ cat['Bmag'] = (
211
+ cat['gmag']
212
+ + np.polyval(pB1, cat['gmag'] - cat['rmag'])
213
+ + np.polyval(pB2, cat['rmag'] - cat['imag'])
214
+ )
215
+ cat['Vmag'] = (
216
+ cat['gmag']
217
+ + np.polyval(pV1, cat['gmag'] - cat['rmag'])
218
+ + np.polyval(pV2, cat['rmag'] - cat['imag'])
219
+ )
220
+ cat['Rmag'] = (
221
+ cat['rmag']
222
+ + np.polyval(pR1, cat['gmag'] - cat['rmag'])
223
+ + np.polyval(pR2, cat['rmag'] - cat['imag'])
224
+ )
225
+ cat['Imag'] = (
226
+ cat['imag']
227
+ + np.polyval(pI1, cat['gmag'] - cat['rmag'])
228
+ + np.polyval(pI2, cat['rmag'] - cat['imag'])
229
+ )
230
+
231
+ cat['e_Bmag'] = cat['e_gmag']
232
+ cat['e_Vmag'] = cat['e_gmag']
233
+ cat['e_Rmag'] = cat['e_rmag']
234
+ cat['e_Imag'] = cat['e_imag']
235
+
236
+ # Copies of columns for convenience
237
+ for _ in ['B', 'V', 'R', 'I']:
238
+ cat[_] = cat[_ + 'mag']
239
+
240
+ cat['good'] = (cat['gmag'] - cat['rmag'] > -0.5) & (cat['gmag'] - cat['rmag'] < 2.5)
241
+ cat['good'] &= (cat['rmag'] - cat['imag'] > -0.5) & (cat['rmag'] - cat['imag'] < 2.0)
242
+ cat['good'] &= (cat['imag'] - cat['zmag'] > -0.5) & (cat['imag'] - cat['zmag'] < 1.0)
243
+
244
+ # to SDSS, zero points and color terms from https://arxiv.org/pdf/1203.0297.pdf
245
+ cat['g_SDSS'] = (
246
+ cat['gmag']
247
+ + 0.013
248
+ + 0.145 * (cat['gmag'] - cat['rmag'])
249
+ + 0.019 * (cat['gmag'] - cat['rmag']) ** 2
250
+ )
251
+ cat['r_SDSS'] = (
252
+ cat['rmag']
253
+ - 0.001
254
+ + 0.004 * (cat['gmag'] - cat['rmag'])
255
+ + 0.007 * (cat['gmag'] - cat['rmag']) ** 2
256
+ )
257
+ cat['i_SDSS'] = (
258
+ cat['imag']
259
+ - 0.005
260
+ + 0.011 * (cat['gmag'] - cat['rmag'])
261
+ + 0.010 * (cat['gmag'] - cat['rmag']) ** 2
262
+ )
263
+ cat['z_SDSS'] = cat['zmag']
264
+
265
+ except KeyError as e:
266
+ log(f"Warning: Missing required column for PS1 augmentation: {e}")
267
+
268
+
269
+ def _augment_gaiadr2(cat, verbose=False):
270
+ """Augment Gaia DR2 catalog with additional bands."""
271
+ log = (verbose if callable(verbose) else print) if verbose else lambda *args, **kwargs: None
272
+ try:
273
+ log("Augmenting the catalogue with Johnson-Cousins photometry using Gaia DR2 mags")
274
+
275
+ # My simple Gaia DR2 to Johnson conversion based on Stetson standards
276
+ pB = [
277
+ -0.05927724559795761,
278
+ 0.4224326324292696,
279
+ 0.626219707920836,
280
+ -0.011211539139725953,
281
+ ]
282
+ pV = [
283
+ 0.0017624722901609662,
284
+ 0.15671377090187089,
285
+ 0.03123927839356175,
286
+ 0.041448557506784556,
287
+ ]
288
+ pR = [
289
+ 0.02045449129406191,
290
+ 0.054005149296716175,
291
+ -0.3135475489352255,
292
+ 0.020545083667168156,
293
+ ]
294
+ pI = [
295
+ 0.005092289380850884,
296
+ 0.07027022935721515,
297
+ -0.7025553064161775,
298
+ -0.02747532184796779,
299
+ ]
300
+
301
+ pCB = [876.4047401692277, 5.114021693079334, -2.7332873314449326, 0]
302
+ pCV = [98.03049528983964, 20.582521666713028, 0.8690079603974803, 0]
303
+ pCR = [347.42190542330945, 39.42482430363565, 0.8626828845232541, 0]
304
+ pCI = [79.4028706486939, 9.176899238787003, -0.7826315256072135, 0]
305
+
306
+ g = cat['Gmag']
307
+ bp_rp = cat['BPmag'] - cat['RPmag']
308
+
309
+ # phot_bp_rp_excess_factor == E(BR/RP) == E_BR_RP_
310
+ Cstar = cat['E_BR_RP_'] - np.polyval(
311
+ [-0.00445024, 0.0570293, -0.02810592, 1.20477819], bp_rp
312
+ )
313
+
314
+ # https://www.cosmos.esa.int/web/gaia/dr2-known-issues#PhotometrySystematicEffectsAndResponseCurves
315
+ gcorr = g.copy()
316
+ gcorr[(g > 2) & (g < 6)] = (
317
+ -0.047344
318
+ + 1.16405 * g[(g > 2) & (g < 6)]
319
+ - 0.046799 * g[(g > 2) & (g < 6)] ** 2
320
+ + 0.0035015 * g[(g > 2) & (g < 6)] ** 3
321
+ )
322
+ gcorr[(g > 6) & (g < 16)] = g[(g > 6) & (g < 16)] - 0.0032 * (g[(g > 6) & (g < 16)] - 6)
323
+ gcorr[g > 16] = g[g > 16] - 0.032
324
+ g = gcorr
325
+
326
+ cat['Bmag'] = g + np.polyval(pB, bp_rp) + np.polyval(pCB, Cstar)
327
+ cat['Vmag'] = g + np.polyval(pV, bp_rp) + np.polyval(pCV, Cstar)
328
+ cat['Rmag'] = g + np.polyval(pR, bp_rp) + np.polyval(pCR, Cstar)
329
+ cat['Imag'] = g + np.polyval(pI, bp_rp) + np.polyval(pCI, Cstar)
330
+
331
+ # Rough estimation of magnitude error, just from G band
332
+ for _ in ['B', 'V', 'R', 'I', 'g', 'r']:
333
+ cat['e_' + _ + 'mag'] = cat['e_Gmag']
334
+
335
+ # Copies of columns for convenience
336
+ for _ in ['B', 'V', 'R', 'I']:
337
+ cat[_] = cat[_ + 'mag']
338
+
339
+ # to PS1 - FIXME: there are some uncorrected color and magnitude trends!
340
+ cat['gmag'] = (
341
+ cat['Bmag']
342
+ - 0.108
343
+ - 0.485 * (cat['Bmag'] - cat['Vmag'])
344
+ - 0.032 * (cat['Bmag'] - cat['Vmag']) ** 2
345
+ )
346
+ cat['rmag'] = (
347
+ cat['Vmag']
348
+ + 0.082
349
+ - 0.462 * (cat['Bmag'] - cat['Vmag'])
350
+ + 0.041 * (cat['Bmag'] - cat['Vmag']) ** 2
351
+ )
352
+
353
+ # to SDSS, from https://gea.esac.esa.int/archive/documentation/GDR2/Data_processing/chap_cu5pho/sec_cu5pho_calibr/ssec_cu5pho_PhotTransf.html
354
+ cat['g_SDSS'] = g - (0.13518 - 0.46245 * bp_rp - 0.25171 * bp_rp**2 + 0.021349 * bp_rp**3)
355
+ cat['r_SDSS'] = g - (-0.12879 + 0.24662 * bp_rp - 0.027464 * bp_rp**2 - 0.049465 * bp_rp**3)
356
+ cat['i_SDSS'] = g - (-0.29676 + 0.64728 * bp_rp - 0.10141 * bp_rp**2)
357
+
358
+ except KeyError as e:
359
+ log(f"Warning: Missing required column for Gaia DR2 augmentation: {e}")
360
+
361
+
362
+ def _augment_skymapper(cat, verbose=False):
363
+ """Augment SkyMapper DR4 catalog with additional bands."""
364
+ log = (verbose if callable(verbose) else print) if verbose else lambda *args, **kwargs: None
365
+ try:
366
+ log("Augmenting the catalogue with Pan-STARRS photometry using SkyMapper DR4 mags")
367
+
368
+ # SkyMapper DR4 to PS1 griz, my fit based on Pancino et al. (2022)
369
+ pg1, pg2 = (
370
+ [-0.07715320986152466, 0.2694597282089696, 0.04069379065128178, 0.01396290714542747],
371
+ [0.026097008342026252, -0.14040957287568073, 0.133647539780504, 0.013962907145427432],
372
+ )
373
+ pr1, pr2 = (
374
+ [0.08779280979185472, -0.23257704629617004, 0.1890698144343673, -0.008125550119663026],
375
+ [-0.06273832689338121, 0.21909317812693613, -0.23340488268623696, -0.00812555011966309],
376
+ )
377
+ pi1, pi2 = (
378
+ [
379
+ 0.03553380678975111,
380
+ -0.021174189684500792,
381
+ -0.028159666883815007,
382
+ 0.0009748746568893062,
383
+ ],
384
+ [0.00911922467970264, -0.0362286983251751, 0.1403094994141109, 0.0009748746568892609],
385
+ )
386
+ pz1, pz2 = (
387
+ [
388
+ 0.08071260245520126,
389
+ -0.051693023216670575,
390
+ -0.0739439627982131,
391
+ -0.0020460270205769223,
392
+ ],
393
+ [
394
+ 0.09720715174271254,
395
+ -0.32063637962189184,
396
+ 0.37918283208242526,
397
+ -0.0020460270205769305,
398
+ ],
399
+ )
400
+ py1, py2 = (
401
+ [0.038781034592287725, -0.11040188064275973, 0.08235396198116865, 0.006980454415779221],
402
+ [-0.0649739656901001, 0.205320995228645, -0.28233276303592, 0.006980454415779424],
403
+ )
404
+
405
+ cat['gmag'] = (
406
+ cat['gPSF']
407
+ + np.polyval(pg1, cat['gPSF'] - cat['rPSF'])
408
+ + np.polyval(pg2, cat['rPSF'] - cat['iPSF'])
409
+ )
410
+ cat['rmag'] = (
411
+ cat['rPSF']
412
+ + np.polyval(pr1, cat['gPSF'] - cat['rPSF'])
413
+ + np.polyval(pr2, cat['rPSF'] - cat['iPSF'])
414
+ )
415
+ cat['imag'] = (
416
+ cat['iPSF']
417
+ + np.polyval(pi1, cat['gPSF'] - cat['rPSF'])
418
+ + np.polyval(pi2, cat['rPSF'] - cat['iPSF'])
419
+ )
420
+ cat['zmag'] = (
421
+ cat['zPSF']
422
+ + np.polyval(pz1, cat['gPSF'] - cat['rPSF'])
423
+ + np.polyval(pz2, cat['rPSF'] - cat['iPSF'])
424
+ )
425
+ cat['ymag'] = (
426
+ cat['zPSF']
427
+ + np.polyval(py1, cat['gPSF'] - cat['rPSF'])
428
+ + np.polyval(py2, cat['rPSF'] - cat['iPSF'])
429
+ )
430
+
431
+ for _ in ['g', 'r', 'i', 'z']:
432
+ cat['e_' + _ + 'mag'] = cat['e_' + _ + 'PSF']
433
+ cat['e_ymag'] = cat['e_zPSF']
434
+
435
+ log("Augmenting the catalogue with Johnson-Cousins photometry using SkyMapper DR4 mags")
436
+
437
+ # SkyMapper DR4 to Johnson-Cousins BVRI, my fit based on Pancino et al. (2022)
438
+ pB1, pB2 = (
439
+ [-0.22773918482205113, 0.1818124624962873, 1.0021365492384895, 0.10762635377473588],
440
+ [-0.004034933919297649, 0.08214592357213418, -0.07535454054888649, 0.10762635377473558],
441
+ )
442
+ pV1, pV2 = (
443
+ [
444
+ -0.02545732895304914,
445
+ 0.03256423830249228,
446
+ -0.33074199873567045,
447
+ -0.002938730214382037,
448
+ ],
449
+ [-0.007342074336918033, 0.08255055271047995, -0.14349325478829064, -0.0029387302143822],
450
+ )
451
+ pR1, pR2 = (
452
+ [0.07296699439306827, -0.1943702618426095, 0.15375263988851387, -0.08547735652048871],
453
+ [-0.07378125129406726, 0.18462924970775316, -0.40720945890364135, -0.08547735652048903],
454
+ )
455
+ pI1, pI2 = (
456
+ [
457
+ -0.00925391710305653,
458
+ 0.046223960182760516,
459
+ -0.06889215990613289,
460
+ -0.19321699685334734,
461
+ ],
462
+ [
463
+ 0.01197866152020802,
464
+ -0.044370062623186206,
465
+ -0.05231484699406009,
466
+ -0.19321699685334745,
467
+ ],
468
+ )
469
+
470
+ cat['Bmag'] = (
471
+ cat['gPSF']
472
+ + np.polyval(pB1, cat['gPSF'] - cat['rPSF'])
473
+ + np.polyval(pB2, cat['rPSF'] - cat['iPSF'])
474
+ )
475
+ cat['Vmag'] = (
476
+ cat['gPSF']
477
+ + np.polyval(pV1, cat['gPSF'] - cat['rPSF'])
478
+ + np.polyval(pV2, cat['rPSF'] - cat['iPSF'])
479
+ )
480
+ cat['Rmag'] = (
481
+ cat['rPSF']
482
+ + np.polyval(pR1, cat['gPSF'] - cat['rPSF'])
483
+ + np.polyval(pR2, cat['rPSF'] - cat['iPSF'])
484
+ )
485
+ cat['Imag'] = (
486
+ cat['iPSF']
487
+ + np.polyval(pI1, cat['gPSF'] - cat['rPSF'])
488
+ + np.polyval(pI2, cat['rPSF'] - cat['iPSF'])
489
+ )
490
+
491
+ cat['e_Bmag'] = cat['e_gmag']
492
+ cat['e_Vmag'] = cat['e_gmag']
493
+ cat['e_Rmag'] = cat['e_rmag']
494
+ cat['e_Imag'] = cat['e_imag']
495
+
496
+ # Copies of columns for convenience
497
+ for _ in ['B', 'V', 'R', 'I']:
498
+ cat[_] = cat[_ + 'mag']
499
+
500
+ except KeyError as e:
501
+ log(f"Warning: Missing required column for SkyMapper augmentation: {e}")
502
+
503
+
504
+ def _augment_apass(cat, verbose=False):
505
+ """Augment APASS DR9 catalog with additional bands."""
506
+ log = (verbose if callable(verbose) else print) if verbose else lambda *args, **kwargs: None
507
+ try:
508
+ log("Augmenting the catalogue with Cousins R and I using APASS mags")
509
+
510
+ # My own fit based on Landolt standards
511
+ cat['Rmag'] = (
512
+ cat['r_mag']
513
+ - 0.157
514
+ - 0.087 * (cat['g_mag'] - cat['r_mag'])
515
+ - 0.014 * (cat['g_mag'] - cat['r_mag']) ** 2
516
+ )
517
+ cat['e_Rmag'] = cat['e_r_mag']
518
+
519
+ cat['Imag'] = (
520
+ cat['i_mag']
521
+ - 0.354
522
+ - 0.118 * (cat['g_mag'] - cat['r_mag'])
523
+ - 0.004 * (cat['g_mag'] - cat['r_mag']) ** 2
524
+ )
525
+ cat['e_Imag'] = cat['e_i_mag']
526
+
527
+ # Copies of columns for convenience
528
+ for _ in ['B', 'V', 'R', 'I']:
529
+ cat[_] = cat[_ + 'mag']
530
+
531
+ except KeyError as e:
532
+ log(f"Warning: Missing required column for APASS augmentation: {e}")
533
+
534
+
535
+ def _augment_gaiadr3syn(cat, verbose=False):
536
+ """Augment Gaia DR3 Synthetic Photometry catalog with additional bands."""
537
+ log = (verbose if callable(verbose) else print) if verbose else lambda *args, **kwargs: None
538
+ try:
539
+ # Compute magnitude errors from flux errors
540
+ for name in ['U', 'B', 'V', 'R', 'I', 'u', 'g', 'r', 'i', 'z', 'y']:
541
+ cat['e_' + name + 'mag'] = 2.5 / np.log(10) * cat['e_F' + name] / cat['F' + name]
542
+
543
+ log("Converting the catalogue Sloan magnitudes to Pan-STARRS ones")
544
+
545
+ # umag, gmag, rmag, imag and zmag are Sloan ugriz magnitudes! Let's get PS1 ones instead
546
+ # Fits are based on clean Landolt sample from https://arxiv.org/pdf/2205.06186
547
+ pg = [-0.030414391501015867, -0.09960002492299584, -0.002910024005294562]
548
+ pr = [-0.009566553708653305, 0.014924591443344211, -0.003928147919030857]
549
+ pi = [-0.010802807724098494, 0.01124900218746879, 0.01274293783734852]
550
+ pz = [-0.0031896767661109523, 0.06537983414287968, 0.007695587806229381]
551
+
552
+ for _ in ['u', 'g', 'r', 'i', 'z']:
553
+ cat[_ + '_SDSS'] = cat[_ + 'mag']
554
+
555
+ cat['gmag'] = cat['g_SDSS'] + np.polyval(pg, cat['g_SDSS'] - cat['r_SDSS'])
556
+ cat['rmag'] = cat['r_SDSS'] + np.polyval(pr, cat['g_SDSS'] - cat['r_SDSS'])
557
+ cat['imag'] = cat['i_SDSS'] + np.polyval(pi, cat['g_SDSS'] - cat['r_SDSS'])
558
+ cat['zmag'] = cat['z_SDSS'] + np.polyval(pz, cat['g_SDSS'] - cat['r_SDSS'])
559
+
560
+ except KeyError as e:
561
+ log(f"Warning: Missing required column for Gaia DR3 synphot augmentation: {e}")
562
+
563
+
564
+ def _augment_sdss(cat, verbose=False):
565
+ """Augment SDSS DR16 catalog with additional bands."""
566
+ log = (verbose if callable(verbose) else print) if verbose else lambda *args, **kwargs: None
567
+ try:
568
+ log("Converting the catalogue Sloan magnitudes to AB ones")
569
+
570
+ # Zero point biases from https://www.sdss4.org/dr16/algorithms/fluxcal/#SDSStoAB
571
+ cat['umag'] -= 0.04
572
+ cat['zmag'] += 0.02
573
+
574
+ for _ in ['u', 'g', 'r', 'i', 'z']:
575
+ cat[_ + '_SDSS'] = cat[_ + 'mag']
576
+
577
+ except KeyError as e:
578
+ log(f"Warning: Missing required column for SDSS augmentation: {e}")
579
+
580
+ def _augment_2mass(cat, verbose=False):
581
+ """Augment 2MASS catalog with additional bands."""
582
+ log = (verbose if callable(verbose) else print) if verbose else lambda *args, **kwargs: None
583
+ try:
584
+ log("Renaming the catalogue K magnitudes to Ks ones")
585
+
586
+ cat['Ksmag'] = cat['Kmag']
587
+ cat['e_Ksmag'] = cat['e_Kmag']
588
+
589
+ except KeyError as e:
590
+ log(f"Warning: Missing required column for 2MASS augmentation: {e}")
591
+
592
+
593
+ def _augment_vhs(cat, verbose=False):
594
+ """Augment VHS catalog with 2MASS-system JHKs magnitudes."""
595
+ log = (verbose if callable(verbose) else print) if verbose else lambda *args, **kwargs: None
596
+ try:
597
+ log("Augmenting the catalogue with 2MASS magnitudes")
598
+
599
+ for _ in ['J', 'H', 'Ks']:
600
+ cat[f"{_}mag"] = cat[f"{_}ap3"]
601
+ cat[f"e_{_}mag"] = cat[f"e_{_}ap3"]
602
+
603
+ def color_term(a, b, coeff):
604
+ # Color-term correction that degrades gracefully to zero (i.e. keeps the
605
+ # uncorrected VISTA magnitude) where the auxiliary band is missing, so that
606
+ # e.g. a field lacking H-band coverage does not wipe out the J magnitudes.
607
+ term = coeff * (np.ma.filled(cat[a].astype(float), np.nan)
608
+ - np.ma.filled(cat[b].astype(float), np.nan))
609
+ n_bad = int(np.count_nonzero(np.isnan(term)))
610
+ if n_bad:
611
+ log(f"Warning: {n_bad}/{len(cat)} stars lack {a} or {b}; "
612
+ f"their magnitudes are left uncorrected for the {a}-{b} colour term")
613
+ return np.nan_to_num(term, nan=0.0)
614
+
615
+ # VISTA to 2MASS equations from https://ui.adsabs.harvard.edu/abs/2013A%26A...552A.101S
616
+ cat['Jmag'] += color_term('Jap3', 'Hap3', 0.070)
617
+ cat['Hmag'] += color_term('Hap3', 'Ksap3', -0.035)
618
+ cat['Ksmag'] += color_term('Jap3', 'Ksap3', -0.011)
619
+
620
+ except KeyError as e:
621
+ log(f"Warning: Missing required column for VHS augmentation: {e}")
622
+
623
+
624
+ def augment_cat_bands(cat, catalog=None, verbose=False):
625
+ """
626
+ Augment catalog with additional photometric bands via magnitude conversions.
627
+
628
+ Parameters
629
+ ----------
630
+ cat : astropy.table.Table
631
+ Input catalog to augment. Modified in-place.
632
+ catalog : str, optional
633
+ Catalog type: 'ps1', 'atlas', 'gaiadr2', 'skymapper', 'apass',
634
+ 'gaiadr3syn', 'sdss', '2mass', 'vhs'. If None, auto-detect from columns.
635
+ verbose : bool or callable, optional
636
+ Logging control.
637
+
638
+ Returns
639
+ -------
640
+ cat : astropy.table.Table
641
+ The input catalog with additional magnitude columns.
642
+ """
643
+ # Setup logging
644
+ log = (verbose if callable(verbose) else print) if verbose else lambda *args, **kwargs: None
645
+
646
+ # Auto-detect if not specified
647
+ if catalog is None:
648
+ catalog = _detect_catalog_type(cat)
649
+ if catalog is None:
650
+ log("Warning: Could not detect catalog type. Skipping augmentation.")
651
+ return cat
652
+ log(f"Auto-detected catalog type: {catalog}")
653
+
654
+ # Dispatch to appropriate helper
655
+ if catalog == 'ps1' or catalog == 'atlas':
656
+ _augment_ps1(cat, verbose=verbose)
657
+ elif catalog == 'gaiadr2':
658
+ _augment_gaiadr2(cat, verbose=verbose)
659
+ elif catalog == 'skymapper':
660
+ _augment_skymapper(cat, verbose=verbose)
661
+ elif catalog == 'apass':
662
+ _augment_apass(cat, verbose=verbose)
663
+ elif catalog == 'gaiadr3syn':
664
+ _augment_gaiadr3syn(cat, verbose=verbose)
665
+ elif catalog == 'sdss':
666
+ _augment_sdss(cat, verbose=verbose)
667
+ elif catalog == '2mass':
668
+ _augment_2mass(cat, verbose=verbose)
669
+ elif catalog == 'vhs':
670
+ _augment_vhs(cat, verbose=verbose)
671
+ else:
672
+ log(f"Warning: Unknown catalog type '{catalog}'. Skipping augmentation.")
673
+
674
+ return cat
675
+
676
+
677
+ def get_cat_vizier(
678
+ ra0,
679
+ dec0,
680
+ sr0,
681
+ catalog='ps1',
682
+ limit=-1,
683
+ filters={},
684
+ extra=[],
685
+ get_distance=False,
686
+ augment_bands=True,
687
+ verbose=False,
688
+ ):
689
+ """Download any catalogue from Vizier.
690
+
691
+ The catalogue may be any Vizier identifier. For a number of popular catalogues,
692
+ additional photometric bands are computed from analytical conversion formulae:
693
+
694
+ - ``ps1`` — Pan-STARRS DR1, augmented with Johnson-Cousins B, V, R, I
695
+ - ``gaiadr2`` — Gaia DR2, augmented with Johnson-Cousins B, V, R, I, Pan-STARRS and SDSS
696
+ - ``gaiaedr3`` — Gaia eDR3
697
+ - ``gaiadr3syn`` — Gaia DR3 synthetic photometry from XP spectra
698
+ - ``skymapper`` — SkyMapper DR1.1
699
+ - ``vsx`` — AAVSO Variable Stars Index
700
+ - ``apass`` — AAVSO APASS DR9
701
+ - ``sdss`` — SDSS DR16
702
+ - ``atlas`` — ATLAS-RefCat2, augmented with Johnson-Cousins B, V, R, I
703
+ - ``usnob1`` — USNO-B1
704
+ - ``gsc`` — Guide Star Catalogue 2.2
705
+ - ``2mass`` — 2MASS Point Source Catalogue
706
+ - ``vhs`` — VISTA Hemisphere Survey DR5, augmented with 2MASS-system J, H, Ks
707
+
708
+ Parameters
709
+ ----------
710
+ ra0 : float
711
+ Right Ascension of the field center in degrees.
712
+ dec0 : float
713
+ Declination of the field center in degrees.
714
+ sr0 : float
715
+ Field radius in degrees.
716
+ catalog : str, optional
717
+ Any Vizier catalogue identifier or a short name from the list above.
718
+ limit : int, optional
719
+ Maximum number of rows to return (``-1`` for unlimited).
720
+ filters : dict, optional
721
+ Column filters applied server-side. Keys are column names; values are
722
+ Vizier filter expressions.
723
+ extra : list of str, optional
724
+ Additional column names to include beyond the defaults.
725
+ get_distance : bool, optional
726
+ If True, include a ``_r`` column with angular distance from field center.
727
+ augment_bands : bool, optional
728
+ If True (default), augment photometry with derived bands where supported.
729
+ verbose : bool or callable, optional
730
+ Whether to show verbose messages. May be boolean or a ``print``-like callable.
731
+
732
+ Returns
733
+ -------
734
+ astropy.table.Table or None
735
+ Catalogue as returned by Vizier, with additional columns for supported
736
+ catalogues, or None if the query fails.
737
+ """
738
+
739
+ # Simple Wrapper around print for logging in verbose mode only
740
+ log = (verbose if callable(verbose) else print) if verbose else lambda *args, **kwargs: None
741
+
742
+ # TODO: add positional errors
743
+
744
+ if catalog in catalogs:
745
+ # For some catalogs we may have some additional information
746
+ vizier_id = catalogs.get(catalog).get('vizier')
747
+ name = catalogs.get(catalog).get('name')
748
+
749
+ columns = (
750
+ ['*', 'RAJ2000', 'DEJ2000', 'e_RAJ2000', 'e_DEJ2000']
751
+ + extra
752
+ + catalogs.get(catalog).get('extra', [])
753
+ )
754
+ else:
755
+ vizier_id = catalog
756
+ name = catalog
757
+ columns = ['*', 'RAJ2000', 'DEJ2000', 'e_RAJ2000', 'e_DEJ2000'] + extra
758
+
759
+ log('Requesting from VizieR:', vizier_id, 'columns:', columns)
760
+ log('Center: %.3f %.3f' % (ra0, dec0), 'radius: %.3f' % sr0)
761
+ log('Filters:', filters)
762
+
763
+ vizier = Vizier(row_limit=limit, columns=columns, column_filters=filters)
764
+ cats = vizier.query_region(
765
+ SkyCoord(ra0, dec0, unit='deg'), radius=sr0 * u.deg, catalog=vizier_id
766
+ )
767
+
768
+ if not cats or not len(cats) == 1:
769
+ cats = vizier.query_region(
770
+ SkyCoord(ra0, dec0, unit='deg'),
771
+ radius=sr0 * u.deg,
772
+ catalog=vizier_id,
773
+ cache=False,
774
+ )
775
+
776
+ if not cats or not len(cats) == 1:
777
+ log('Error requesting catalogue', catalog)
778
+ return None
779
+
780
+ cat = cats[0]
781
+ cat.meta['vizier_id'] = vizier_id
782
+ cat.meta['name'] = name
783
+
784
+ log('Got', len(cat), 'entries with', len(cat.colnames), 'columns')
785
+
786
+ # Fix _RAJ2000/_DEJ2000
787
+ if '_RAJ2000' in cat.keys() and '_DEJ2000' in cat.keys() and not 'RAJ2000' in cat.keys():
788
+ cat.rename_columns(['_RAJ2000', '_DEJ2000'], ['RAJ2000', 'DEJ2000'])
789
+
790
+ if get_distance and 'RAJ2000' in cat.colnames and 'DEJ2000' in cat.colnames:
791
+ log("Augmenting the catalogue with distances from field center")
792
+ cat['_r'] = astrometry.spherical_distance(ra0, dec0, cat['RAJ2000'], cat['DEJ2000'])
793
+
794
+ if augment_bands or True:
795
+ # Augment catalogue with additional bandpasses
796
+ augment_cat_bands(cat, catalog=catalog if catalog in catalogs else None, verbose=verbose)
797
+
798
+ return cat
799
+
800
+
801
+ def xmatch_objects(obj, catalog='ps1', sr=3 / 3600, col_ra='ra', col_dec='dec', augment_bands=True):
802
+ """Cross-match object list with a Vizier catalogue using the CDS XMatch service.
803
+
804
+ Parameters
805
+ ----------
806
+ obj : astropy.table.Table
807
+ Table of objects to cross-match.
808
+ catalog : str, optional
809
+ Any Vizier catalogue identifier or a short name recognized by :func:`get_cat_vizier`.
810
+ sr : float, optional
811
+ Cross-matching radius in degrees.
812
+ col_ra : str, optional
813
+ Column name in ``obj`` containing Right Ascension values.
814
+ col_dec : str, optional
815
+ Column name in ``obj`` containing Declination values.
816
+ augment_bands : bool, optional
817
+ If True (default), augment photometry with derived bands where supported.
818
+
819
+ Returns
820
+ -------
821
+ astropy.table.Table
822
+ Table of matched objects augmented with columns from the Vizier catalogue.
823
+ """
824
+
825
+ if catalog in catalogs:
826
+ vizier_id = catalogs.get(catalog)['vizier']
827
+ else:
828
+ vizier_id = catalog
829
+
830
+ xcat = XMatch().query(
831
+ cat1=obj,
832
+ cat2='vizier:' + vizier_id,
833
+ max_distance=sr * u.deg,
834
+ colRA1=col_ra,
835
+ colDec1=col_dec,
836
+ )
837
+
838
+ if augment_bands:
839
+ # Augment catalogue with additional bandpasses
840
+ augment_cat_bands(xcat, catalog=catalog if catalog in catalogs else None)
841
+
842
+ return xcat
843
+
844
+
845
+ def xmatch_skybot(obj, sr=10 / 3600, time=None, col_ra='ra', col_dec='dec', col_id='id'):
846
+ """Cross-match object list with Solar System object positions using SkyBoT.
847
+
848
+ Queries SkyBoT for all Solar System objects in a cone covering the input
849
+ field at the specified time, then cross-matches them against the input list
850
+ within ``sr``.
851
+
852
+ Parameters
853
+ ----------
854
+ obj : astropy.table.Table
855
+ Table of objects to cross-match.
856
+ sr : float, optional
857
+ Cross-matching radius in degrees.
858
+ time : astropy.time.Time or datetime or str
859
+ Observation time.
860
+ col_ra : str, optional
861
+ Column name in ``obj`` containing Right Ascension values.
862
+ col_dec : str, optional
863
+ Column name in ``obj`` containing Declination values.
864
+ col_id : str, optional
865
+ Column name in ``obj`` containing unique object identifiers.
866
+
867
+ Returns
868
+ -------
869
+ astropy.table.Table or None
870
+ Table of matched Solar System objects with a ``col_id`` column referencing
871
+ the matched input objects, or None if no matches are found.
872
+ """
873
+
874
+ ra0, dec0, sr0 = astrometry.get_objects_center(obj)
875
+
876
+ try:
877
+ # Query SkyBot for (a bit larger than) our FOV at our time
878
+ xcat = Skybot.cone_search(
879
+ SkyCoord(ra0, dec0, unit='deg'), (sr0 + 2.0 * sr) * u.deg, Time(time)
880
+ )
881
+ except (RuntimeError, KeyError, ConnectionError, OSError):
882
+ # Nothing found in SkyBot
883
+ return None
884
+
885
+ if xcat is None or len(xcat) == 0:
886
+ return None
887
+
888
+ # Cross-match objects
889
+ oidx, cidx, dist = astrometry.spherical_match(
890
+ obj[col_ra], obj[col_dec], xcat['RA'], xcat['DEC'], 10 / 3600
891
+ )
892
+
893
+ # Annotate the table with id from objects so that it is possible to identify the matches
894
+ xcat[col_id] = MaskedColumn(len(xcat), dtype=np.dtype(obj[col_id][0]))
895
+ xcat[col_id].mask = True
896
+ xcat[col_id][cidx] = obj[col_id][oidx]
897
+ xcat[col_id][cidx].mask = False
898
+
899
+ return xcat
900
+
901
+
902
+ def xmatch_ned(obj, sr=3 / 3600, col_ra='ra', col_dec='dec', col_id='id'):
903
+ """Cross-match object list with NED database entries.
904
+
905
+ .. note::
906
+ Queries NED one object at a time, so this is slow for large lists.
907
+
908
+ Parameters
909
+ ----------
910
+ obj : astropy.table.Table
911
+ Table of objects to cross-match.
912
+ sr : float, optional
913
+ Cross-matching radius in degrees.
914
+ col_ra : str, optional
915
+ Column name in ``obj`` containing Right Ascension values.
916
+ col_dec : str, optional
917
+ Column name in ``obj`` containing Declination values.
918
+ col_id : str, optional
919
+ Column name in ``obj`` containing unique object identifiers.
920
+
921
+ Returns
922
+ -------
923
+ astropy.table.Table or list
924
+ Table of NED objects with an ``id`` column referencing the matched input
925
+ objects, or an empty list if no matches are found.
926
+ """
927
+
928
+ xcat = []
929
+
930
+ # FIXME: is there more optimal way to query NED for multiple sky positions?..
931
+ for row in obj:
932
+ res = Ned().query_region(SkyCoord(row[col_ra], row[col_dec], unit='deg'), sr * u.deg)
933
+
934
+ if res:
935
+ res['id'] = row[col_id]
936
+
937
+ xcat.append(res)
938
+
939
+ if xcat:
940
+ xcat = vstack(xcat, join_type='exact', metadata_conflicts='silent')
941
+
942
+ return xcat