rms-starcat 0.0.1__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.

Potentially problematic release.


This version of rms-starcat might be problematic. Click here for more details.

starcat/starcatalog.py ADDED
@@ -0,0 +1,414 @@
1
+ ################################################################################
2
+ # starcat/starcatalog.py
3
+ ################################################################################
4
+
5
+ import inspect
6
+ import numpy as np
7
+
8
+ DPR = 180/np.pi
9
+ RPD = np.pi/180
10
+ AS_TO_DEG = 1/3600.
11
+ AS_TO_RAD = AS_TO_DEG * RPD
12
+ MAS_TO_DEG = AS_TO_DEG / 1000.
13
+ MAS_TO_RAD = MAS_TO_DEG * RPD
14
+ YEAR_TO_SEC = 1/365.25/86400.
15
+
16
+ TWOPI = 2*np.pi
17
+ HALFPI = np.pi/2
18
+
19
+ #===============================================================================
20
+ #
21
+ # Jacobson B-V photometry vs. stellar spectral classification
22
+ #
23
+ # Data from
24
+ # Zombeck, M. V. Handbook of Space Astronomy and Astrophysics,
25
+ # Cambridge, UK: Cambridge University Press, 2nd ed., pp. 68-70
26
+ # http://ads.harvard.edu/books/hsaa/
27
+ # as transcribed:
28
+ # http://www.vendian.org/mncharity/dir3/starcolor/details.html
29
+ #
30
+ # The tables below only include main sequence stars, but the temperature
31
+ # difference between main sequence stars and giant stars is minimal for our
32
+ # purposes. Missing values have been linearly interpolated.
33
+ #
34
+ #===============================================================================
35
+
36
+ SCLASS_TO_B_MINUS_V = {
37
+ 'O5': -0.32,
38
+ 'O6': -0.32,
39
+ 'O7': -0.32,
40
+ 'O8': -0.31,
41
+ 'O9': -0.31,
42
+ 'O9.5': -0.30,
43
+ 'B0': -0.30,
44
+ 'B0.5': -0.28,
45
+ 'B1': -0.26,
46
+ 'B2': -0.24,
47
+ 'B3': -0.20,
48
+ 'B4': -0.18, # Interp
49
+ 'B5': -0.16,
50
+ 'B6': -0.14,
51
+ 'B7': -0.12,
52
+ 'B8': -0.09,
53
+ 'B9': -0.06,
54
+ 'A0': +0.00,
55
+ 'A1': +0.02, # Interp
56
+ 'A2': +0.04, # Interp
57
+ 'A3': +0.06,
58
+ 'A4': +0.10, # Interp
59
+ 'A5': +0.14,
60
+ 'A6': +0.16, # Interp
61
+ 'A7': +0.19,
62
+ 'A8': +0.23, # Interp
63
+ 'A9': +0.27, # Interp
64
+ 'F0': +0.31,
65
+ 'F1': +0.33, # Interp
66
+ 'F2': +0.36,
67
+ 'F3': +0.38, # Interp
68
+ 'F4': +0.41, # Interp
69
+ 'F5': +0.43,
70
+ 'F6': +0.47, # Interp
71
+ 'F7': +0.51, # Interp
72
+ 'F8': +0.54,
73
+ 'F9': +0.56, # Interp
74
+ 'G0': +0.59,
75
+ 'G1': +0.61, # Interp
76
+ 'G2': +0.63,
77
+ 'G3': +0.64, # Interp
78
+ 'G4': +0.65, # Interp
79
+ 'G5': +0.66,
80
+ 'G6': +0.69, # Interp
81
+ 'G7': +0.72, # Interp
82
+ 'G8': +0.74,
83
+ 'G9': +0.78, # Interp
84
+ 'K0': +0.82,
85
+ 'K1': +0.87, # Interp
86
+ 'K2': +0.92,
87
+ 'K3': +0.99, # Interp
88
+ 'K4': +1.07, # Interp
89
+ 'K5': +1.15,
90
+ 'K6': +1.22, # Interp
91
+ 'K7': +1.30,
92
+ 'K8': +1.33, # Interp
93
+ 'K9': +1.37, # Interp
94
+ 'M0': +1.41,
95
+ 'M1': +1.48,
96
+ 'M2': +1.52,
97
+ 'M3': +1.55,
98
+ 'M4': +1.56,
99
+ 'M5': +1.61,
100
+ 'M6': +1.72,
101
+ 'M7': +1.84,
102
+ 'M8': +2.00
103
+ }
104
+
105
+ SCLASS_TO_SURFACE_TEMP = {
106
+ 'O5': 38000,
107
+ 'O6': 38000,
108
+ 'O7': 38000,
109
+ 'O8': 35000,
110
+ 'O9': 35000,
111
+ 'O9.5': 31900,
112
+ 'B0': 30000,
113
+ 'B0.5': 27000,
114
+ 'B1': 24200,
115
+ 'B2': 22100,
116
+ 'B3': 18800,
117
+ 'B4': 17600, # Interp
118
+ 'B5': 16400,
119
+ 'B6': 15400,
120
+ 'B7': 14500,
121
+ 'B8': 13400,
122
+ 'B9': 12400,
123
+ 'A0': 10800,
124
+ 'A1': 10443, # Interp
125
+ 'A2': 10086, # Interp
126
+ 'A3': 9730,
127
+ 'A4': 9175, # Interp
128
+ 'A5': 8620,
129
+ 'A6': 8405, # Interp
130
+ 'A7': 8190,
131
+ 'A8': 7873, # Interp
132
+ 'A9': 7557, # Interp
133
+ 'F0': 7240,
134
+ 'F1': 7085, # Interp
135
+ 'F2': 6930,
136
+ 'F3': 6800, # Interp
137
+ 'F4': 6670, # Interp
138
+ 'F5': 6540,
139
+ 'F6': 6427, # Interp
140
+ 'F7': 6313, # Interp
141
+ 'F8': 6200,
142
+ 'F9': 6060, # Interp
143
+ 'G0': 5920,
144
+ 'G1': 5850, # Interp
145
+ 'G2': 5780,
146
+ 'G3': 5723, # Interp
147
+ 'G4': 5667, # Interp
148
+ 'G5': 5610,
149
+ 'G6': 5570, # Interp
150
+ 'G7': 5530, # Interp
151
+ 'G8': 5490,
152
+ 'G9': 5365, # Interp
153
+ 'K0': 5240,
154
+ 'K1': 5010, # Interp
155
+ 'K2': 4780,
156
+ 'K3': 4706, # Interp
157
+ 'K4': 4632, # Interp
158
+ 'K5': 4558, # Interp
159
+ 'K6': 4484, # Interp
160
+ 'K7': 4410,
161
+ 'K8': 4247, # Interp
162
+ 'K9': 4083, # Interp
163
+ 'M0': 3800, # M class from https://arxiv.org/abs/0903.3371
164
+ 'M1': 3600,
165
+ 'M2': 3400,
166
+ 'M3': 3250,
167
+ 'M4': 3100,
168
+ 'M5': 2800,
169
+ 'M6': 2600,
170
+ 'M7': 2500,
171
+ 'M8': 2300,
172
+ }
173
+
174
+
175
+ #===============================================================================
176
+ #
177
+ # STAR Superclass
178
+ #
179
+ #===============================================================================
180
+
181
+ class Star(object):
182
+ """A holder for star attributes.
183
+
184
+ This is the base class that defines attributes common to all
185
+ star catalogs."""
186
+
187
+ def __init__(self):
188
+ """Constructor for Star superclass."""
189
+
190
+ self.ra = None
191
+ """Right ascension at J2000 epoch (radians)"""
192
+
193
+ self.ra_sigma = None
194
+ """Right ascension error (radians)"""
195
+
196
+ self.dec = None
197
+ """Declination at J2000 epoch (radians)"""
198
+
199
+ self.dec_sigma = None
200
+ """Declination error (radians)"""
201
+
202
+ self.vmag = None
203
+ """Visual magnitude"""
204
+
205
+ self.vmag_sigma = None
206
+ """Visual magnitude error"""
207
+
208
+ self.pm_ra = None
209
+ """Proper motion in RA (radians/sec)"""
210
+
211
+ self.pm_ra_sigma = None
212
+ """Proper motion in RA error (radians/sec)"""
213
+
214
+ self.pm_dec = None
215
+ """Proper motion in DEC (radians/sec)"""
216
+
217
+ self.pm_dec_sigma = None
218
+ """Proper motion in DEC error (radians/sec)"""
219
+
220
+ self.unique_number = None
221
+ """Unique catalog number"""
222
+
223
+ def __str__(self):
224
+ ret = 'UNIQUE ID %d' % (self.unique_number)
225
+
226
+ if self.ra is not None:
227
+ ret += ' | RA %.7f' % (self.ra)
228
+ if self.ra_sigma is not None:
229
+ ret += ' [+/- %.7f]' % (self.ra_sigma)
230
+
231
+ ra_deg = self.ra*DPR/15 # In hours
232
+ hh = int(ra_deg)
233
+ mm = int((ra_deg-hh)*60)
234
+ ss = (ra_deg-hh-mm/60.)*3600
235
+ ret += ' (%02dh%02dm%05.3fs' % (hh,mm,ss)
236
+ if self.ra_sigma is not None:
237
+ ret += ' +/- %.4fs' % (self.ra_sigma*DPR*3600)
238
+ ret += ')'
239
+
240
+ if self.dec is not None:
241
+ ret += ' | DEC %.7f' % (self.dec)
242
+ if self.dec_sigma is not None:
243
+ ret += ' [+/- %.7f]' % (self.dec_sigma)
244
+
245
+ dec_deg = self.dec*DPR # In degrees
246
+ neg = '+'
247
+ if dec_deg < 0.:
248
+ neg = '-'
249
+ dec_deg = -dec_deg
250
+ dd = int(dec_deg)
251
+ mm = int((dec_deg-dd)*60)
252
+ ss = (dec_deg-dd-mm/60.)*3600
253
+ ret += ' (%s%03dd%02dm%05.3fs' % (neg,dd,mm,ss)
254
+
255
+ if self.dec_sigma is not None:
256
+ ret += ' +/- %.4fs' % (self.dec_sigma*DPR*3600)
257
+ ret += ')'
258
+
259
+ ret += '\n'
260
+
261
+ if self.vmag is not None:
262
+ ret += 'VMAG %6.3f ' % (self.vmag)
263
+ if self.vmag_sigma is not None:
264
+ ret += '+/- %6.3f ' % (self.vmag_sigma)
265
+
266
+ if self.pm_ra is not None:
267
+ ret += ' | PM RA %.3f mas/yr ' % (self.pm_ra/MAS_TO_RAD/YEAR_TO_SEC)
268
+ if self.pm_ra_sigma:
269
+ ret += '+/- %.3f ' % (self.pm_ra_sigma/MAS_TO_RAD/YEAR_TO_SEC)
270
+
271
+ if self.pm_dec is not None:
272
+ ret += ' | PM DEC %.3f mas/yr ' % (self.pm_dec/MAS_TO_RAD/YEAR_TO_SEC)
273
+ if self.pm_dec_sigma:
274
+ ret += '+/- %.3f ' % (self.pm_dec_sigma/MAS_TO_RAD/YEAR_TO_SEC)
275
+
276
+ ret += '\n'
277
+
278
+ return ret
279
+
280
+ def to_dict(self):
281
+ attribs = inspect.getmembers(self, lambda a:not(inspect.isroutine(a)))
282
+ attribs = [a for a in attribs if not(a[0].startswith('__') and a[0].endswith('__'))]
283
+ return dict(attribs)
284
+
285
+ def from_dict(self, d):
286
+ for key in list(d.keys()):
287
+ setattr(self, key, d[key])
288
+
289
+ def ra_dec_with_pm(self, tdb):
290
+ """Return the star's RA and DEC adjusted for proper motion.
291
+
292
+ If no proper motion is available, the original RA and DEC are returned.
293
+
294
+ Input:
295
+ tdb time since the J2000 epoch in seconds
296
+ """
297
+
298
+ if self.pm_ra is None or self.pm_dec is None:
299
+ return (self.ra, self.dec)
300
+
301
+ return (self.ra + tdb*self.pm_ra, self.dec + tdb*self.pm_dec)
302
+
303
+
304
+ class StarCatalog(object):
305
+ def __init__(self):
306
+ pass
307
+
308
+ def count_stars(self, **kwargs):
309
+ """Count the stars that match the given search criteria."""
310
+ count = 0
311
+ for result in self.find_stars(full_result=False, **kwargs):
312
+ count += 1
313
+ return count
314
+
315
+ def find_stars(self, **kwargs):
316
+ """Find the stars that match the given search criteria.
317
+
318
+ Optional arguments: DEFAULT
319
+ ra_min, ra_max 0, 2PI RA range in radians
320
+ dec_min, dec_max -PI, PI DEC range in radians
321
+ vmag_min, vmag_max ALL Magnitude range
322
+ """
323
+
324
+ kwargs = kwargs.copy() # Private copy so pop doesn't mutate
325
+ ra_min = np.clip(kwargs.pop('ra_min', 0), 0., TWOPI)
326
+ ra_max = np.clip(kwargs.pop('ra_max', TWOPI), 0., TWOPI)
327
+ dec_min = np.clip(kwargs.pop('dec_min', -HALFPI), -HALFPI, HALFPI)
328
+ dec_max = np.clip(kwargs.pop('dec_max', HALFPI), -HALFPI, HALFPI)
329
+
330
+ if ra_min > ra_max:
331
+ if dec_min > dec_max:
332
+ # Split into four searches
333
+ for star in self._find_stars(0., ra_max, -HALFPI, dec_max,
334
+ **kwargs):
335
+ yield star
336
+ for star in self._find_stars(ra_min, TWOPI, -HALFPI, dec_max,
337
+ **kwargs):
338
+ yield star
339
+ for star in self._find_stars(0., ra_max, dec_min, HALFPI,
340
+ **kwargs):
341
+ yield star
342
+ for star in self._find_stars(ra_min, TWOPI, dec_min, HALFPI,
343
+ **kwargs):
344
+ yield star
345
+ else:
346
+ # Split into two searches - RA
347
+ for star in self._find_stars(0., ra_max, dec_min, dec_max,
348
+ **kwargs):
349
+ yield star
350
+ for star in self._find_stars(ra_min, TWOPI, dec_min, dec_max,
351
+ **kwargs):
352
+ yield star
353
+ else:
354
+ if dec_min > dec_max:
355
+ # Split into two searches - DEC
356
+ for star in self._find_stars(ra_min, ra_max, -HALFPI, dec_max,
357
+ **kwargs):
358
+ yield star
359
+ for star in self._find_stars(ra_min, ra_max, dec_min, HALFPI,
360
+ **kwargs):
361
+ yield star
362
+ else:
363
+ # No need to split at all
364
+ for star in self._find_stars(ra_min, ra_max,
365
+ dec_min, dec_max, **kwargs):
366
+ yield star
367
+
368
+ def _find_stars(self, **kwargs):
369
+ assert False
370
+
371
+ @staticmethod
372
+ def sclass_from_bv(b, v):
373
+ """Return a star's spectral class given photometric B and V."""
374
+ bmv = b-v
375
+
376
+ best_temp = None
377
+ best_sclass = None
378
+ best_resid = 1e38
379
+
380
+ min_bmv = 1e38
381
+ max_bmv = -1e38
382
+ for sclass, sbmv in SCLASS_TO_B_MINUS_V.items():
383
+ min_bmv = min(min_bmv, sbmv)
384
+ max_bmv = max(max_bmv, sbmv)
385
+ resid = abs(sbmv-bmv)
386
+ if resid < best_resid:
387
+ best_resid = resid
388
+ best_sclass = sclass
389
+
390
+ if min_bmv <= bmv <= max_bmv:
391
+ return best_sclass
392
+
393
+ return None
394
+
395
+ @staticmethod
396
+ def temperature_from_sclass(sclass):
397
+ """Return a star's temperature (K) given its spectral class."""
398
+ if sclass[-1] == '*': # This happens on some SPICE catalog stars
399
+ sclass = sclass[:-1]
400
+ sclass = sclass.strip().upper()
401
+ try:
402
+ return SCLASS_TO_SURFACE_TEMP[sclass]
403
+ except KeyError:
404
+ return None
405
+
406
+ @staticmethod
407
+ def bmv_from_sclass(sclass):
408
+ if sclass[-1] == '*': # This happens on some SPICE catalog stars
409
+ sclass = sclass[:-1]
410
+ sclass = sclass.strip().upper()
411
+ try:
412
+ return SCLASS_TO_B_MINUS_V[sclass]
413
+ except KeyError:
414
+ return None