1
2 '''Spectractor submodule containing classes which interact with ESO MIDAS.
3 Interaction with ESO MIDAS was made through pymidas module.
4 With pyMidas we calculate heliocentric correction and
5 with pyephem we calculate precession.
6 Midworker class is the main part submodule.
7
8 @author: Dmitry Nasonov
9 '''
10
11 import os, sys, shutil
12 from datetime import datetime
13 from math import pi
14
15 import numpy as np
16
17 try:
18 from pymidas import midas
19 except ImportError, err:
20 print >> sys.stderr, str(err) + ": We use ESO MIDAS and PyMidas for, eg. ",
21 print "heliocentric RV correction. Get it here: http://www.eso.org/esomidas"
22 sys.exit(True)
23
24 try:
25 from coords.astrodate import utc2jd, jd2jyear
26 except ImportError, err:
27 print >> sys.stderr, str(err) + ": coords module required, ",
28 print >> sys.stderr, "download and install it from ",
29 print >> sys.stderr, "http://www.scipy.org/AstroLib/ ",
30 print >> sys.stderr, "or https://www.stsci.edu/trac/ssb/astrolib"
31
32 sys.exit(True)
33
34 try:
35 from ephem import Equatorial
36
37 except ImportError, err:
38 print >> sys.stderr, str(err) + ": pyephem module required ",
39 print >> sys.stderr, "for coordinates precessing."
40
41 try:
42 import pyfits
43 except ImportError, err:
44 print >> sys.stderr, str(err), "PyFITS is required for read and write FITS"
45
46 from serve import Logger
47
48 spacer = lambda *args: " %s " % ' '.join(map(str, args))
49 commer = lambda arg: ",".join(map(str, arg))
50 convcoord = lambda cr, rest: (int(cr), int(divmod(rest*60, 1)[0]),
51 round(divmod(rest*60, 1)[1]*60, 3))
52
53
55 """Calculate heliocentric RV correction for number of observation times.
56 @param params: list of observation parameters: date, time, coordinates.
57 See docstrings of methods of this class for details
58 @param dtype: data type of resulting array, must be dict or list
59 @param obscoord: Observatory coordinates. Default is for 6-m telescope
60 (BTA), SAO RAS
61 """
62 - def __init__(self, params, dtype, obscoord=None, verbose=False):
63 self.params = params
64 self.verbose = verbose
65 if not obscoord:
66 obscoord = ((41, 26, 30), (43, 39, 12))
67 self.obscoord = spacer(commer(obscoord[0]), commer(obscoord[1]))
68 self.dtype = dtype
69 self.fdata = dtype()
70 self.intcoord = lambda h, m, s: (int(h), int(m), float(s))
71 self.cmpbary = lambda date, time: ' '.join(map(','.join, (
72 map(str, date), map(str, time))))
73
75 """Wrapper for comp/prec MIDAS routine.
76 @param date: tuple of year, month and day
77 @param time: tuple of hours and minutes
78 @return: ra and dec as tuples
79 """
80 minuhour = (time[0]/24.) + (time[1]/1440.)
81 compspec_str = ",".join(map(str, date)) + '.' + \
82 str(int(minuhour*10000))
83 ra, dec, epoch = objcoords
84 coords_str = spacer(commer(ra), commer(dec), str(epoch))
85 cmpr = midas.computePrec(coords_str, compspec_str)
86 ra = self.intcoord(cmpr[6], cmpr[8], cmpr[10])
87 dec = self.intcoord(cmpr[13], cmpr[15], cmpr[17])
88
89 return ra, dec
90
91 - def compbary(self, date, time, ra, dec):
92 """Wrapper for comp/bary MIDAS routine.
93 @param date: tuple of year, month and day
94 @param time: tuple of hours and minutes
95 @param ra: tuple of coords
96 @param dec: tuple of coords
97 @return: heliocentric and barycentric radial velocity corrections
98 """
99 compbary_str = self.cmpbary(date, time)
100 coords_str = spacer(','.join(map(str, ra)), ','.join(map(str, dec)))
101 cmbr = midas.computeBary(compbary_str, coords_str, self.obscoord)
102 hcor = round(float(cmbr[20]), 4)
103 bcor = round(float(cmbr[14]), 4)
104 return hcor, bcor
105
106 - def filler(self, ID, jd, jyr, ra, dec, brcor, hlcor):
107 """Dummy function for filling resulting array"""
108 if self.dtype is dict:
109 self.fdata[ID] = (jyr, round(jd-2400000.5, 5), (ra, dec),
110 brcor, hlcor)
111 elif self.dtype is list:
112 self.fdata.append((ID, jyr, round(jd-2400000.5, 5), (ra, dec),
113 brcor, hlcor))
114
116 """Convert datetime of observetion to JD. Pay attention to indices!
117 """
118 if type(spec[0]) is not type(datetime(1970, 1, 1)):
119 date, time = spec[:3], spec[3:5]
120 jd = utc2jd(datetime(*spec[:5]))
121 else:
122 date, time = spec[0].timetuple()[:5]
123 jd = utc2jd(spec[0])
124 return date, time, jd
125
126 - def veltimes(self, objcoords, midprecess=False):
127 """Compute heliocentric correction for the set of observation times.
128 @param objcoords: Object coordinates with its epoch as tuple:
129 (ra, dec, epoch). ra and dec are also tuples.
130 @param midprecess: Precess coordinates using ESO MIDAS comp/bary
131 routine, use pyephem instead
132 @return: list of (spectra) parameters, including heliocentric and
133 barycentric radial velocity correction
134 """
135 for ID, spec in sorted(self.params.items()):
136 date, time, jd = self.getdatime(spec)
137 jyr = round(jd2jyear(jd), 5)
138 if midprecess or abs(objcoords[2]-2000) > .01:
139
140 ra, dec = self.precess_midas(date, time, objcoords)
141 elif not midprecess:
142
143 ra, dec = precess(jd-2400000.5, objcoords, verbose=self.verbose)
144 if self.verbose:
145 print "Precessed coordinates:", ra, dec
146 helrvcor, barrvcor = self.compbary(date, time, ra, dec)
147 self.filler(ID, jd, jyr, ra, dec, barrvcor, helrvcor)
148 return self.fdata
149
151 """Compute heliocentric correction for the set of coordinates.
152 """
153 for ID, spec in sorted(self.params.items()):
154 date, time, jd = self.getdatime(spec)
155 jyr = round(jd2jyear(jd), 5)
156 objcoords = spec[5:8]
157 if not objcoords[2]:
158 objcoords[2] = jyr
159 if midprecess or abs(objcoords[2]-2000) > .01:
160
161 ra, dec = self.precess_midas(date, time, objcoords)
162 elif not midprecess:
163
164 ra, dec = precess(jd-2400000.5, objcoords, verbose=self.verbose)
165 if self.verbose:
166 print "Precessed coordinates:", ra, dec
167 helrvcor, barrvcor = self.compbary(date, time, ra, dec)
168 self.filler(ID, jd, jyr, ra, dec, barrvcor, helrvcor)
169 return self.fdata
170
171
172 -def precess(mjd, objcoords, verbose=False):
173 """Precess coords using pyephem from J2000 to mjd.
174 @param mjd: Modified julian date: JD - 2400000.5
175 @param objcoords: Object coordinates with its epoch as tuple
176 @return: ra, dec as tuples
177 """
178
179
180
181
182
183 mjd = mjd - 15019.5
184 if verbose:
185 print "We get the coords", objcoords
186
187 dv = lambda arg: ":".join(map(str, arg))
188 coord = Equatorial(dv(objcoords[0]), dv(objcoords[1]))
189 precessed_coords = Equatorial(coord, epoch=mjd)
190 ra, dec = precessed_coords.to_radec()
191
192 ra, dec = convcoord(*divmod(ra*12/pi, 1)), convcoord(*divmod(dec*180/pi, 1))
193 if verbose:
194 print "Precess to", mjd, ra, dec, "with J2000 coords", objcoords
195 return ra, dec
196
197
199 """Class with simple wrappers for some ESO MIDAS routines
200 """
201 - def __init__(self, usedisp=False, verbose=False):
202 self.verbose = bool(verbose)
203 self.usedisp = usedisp
204 self.ext = 'bdf'
205 self.med = 35
206 self.avermed = lambda m: ",".join(('median', str(m), str(m), 'data'))
207
208 - def plot(self, bnam, plt="row", cut=2010, over=False):
209 """Wrapper for Midas functions plot/row, plot/colu"""
210 if over:
211 over = "over"
212 else:
213 over = ""
214 if self.verbose:
215 print "Plot", over, plt, bnam, "cut on pixel", cut
216 cmd = " ".join((over+"plot/"+plt, bnam, "@"+str(cut/2)))
217 midas.do(cmd)
218
219 - def readesc(self, fname, desc="npix"):
220 """Wrapper for read/desc Midas procedure"""
221 nx, ny = midas.readDesc(fname, desc)[-2:]
222 return int(nx), int(ny)
223
224 - def loima(self, img, chan=0):
225 """Load image with autocuts"""
226 if not (os.path.isfile(img) or os.path.isfile(img+'.'+self.ext)):
227 return None, None
228 nx, ny = self.readesc(img)
229 dx, dy = map(int, midas.writeOut('{ididev(2)} {ididev(3)}'))
230 if nx > dx:
231 i = round(- (nx/(dx - 10.)) - 1, 2)
232 if ny > dy:
233 j = round(- (ny/(dy - 10.)) - 1, 2)
234 scale = ",".join((str(i), str(j), 'a'))
235 if self.verbose:
236 print "Scale to", scale
237 midas.loadImag(img, str(chan), 'scale='+scale, 'center=c,c')
238 midas.writeKeyw("log/i/4/1", "2")
239
240 midas.statistImag(img)
241 midas.do('@a autocuts ' +img+ ' SIGMA')
242 midas.writeKeyw("log/i/4/1", "0")
243 return i, j
244
245 - def savebdf(self, name, bdf_pth, wridesc=True):
246 """Save a MIDAS bdf image, using existing FITS file
247 Also write descriptors, load it on display
248 """
249 midas.indiskFits(name, bdf_pth)
250 if wridesc:
251 midas.writeDesc(bdf_pth, 'start/d/1/2 1.,1.')
252 midas.writeDesc(bdf_pth, 'step/d/1/2 1.,1.')
253 if self.usedisp:
254 self.loima(bdf_pth)
255
256 - def crebias(self, files, biasname="bias", med=None, delfits=False):
257 """Create bias frame by averaging a set of images.
258 @param med: value of low and high interval in median averaging
259 """
260 if not med:
261 med = self.med
262 catname = biasname+".cat"
263 midas.createIcat(catname, "NULL")
264 for file_pth in files:
265 midas.addIcat(catname, file_pth)
266 midas.averageImag(biasname, "=", catname, "? +", self.avermed(med))
267 self.pltgra(biasname, "row", 1000, over=False)
268 midas.outdiskFits(biasname, biasname+'.fits')
269
270 - def creflat(self, files, flatname="flat", delcat=False, averopt=""):
271 """Create flat field average image
272 @param delcat: delete created cat, if icat is present
273 """
274 catname = flatname+".cat"
275 midas.createIcat(catname, "NULL")
276 for file_pth in files:
277 midas.addIcat(catname, file_pth)
278 midas.averageImag(flatname, "=", catname, "?", averopt)
279 self.pltgra(flatname, "col", 1000, over=False)
280
281 - def filtcosm(self, specname, ns=2, cosm="cosm"):
282 """Wrapper for filter/cosm Midas routine, which removes cosmic
283 ray events from CCD image and replace them by a local median value.
284 @param specname: input image name. Resulting image will be with the
285 same name and the 'f' postfix
286 @param ns: threshold for the detection of cosmic rays
287 param cosm: frame containing mask for the giving image
288 """
289
290 resnam = specname.split(".")[0]+"f"
291 params = ",".join(map(str, (0, 2, 8, ns, 2)))
292 if self.usedisp:
293 i, j = self.loima(specname)
294 midas.filterCosm(specname, resnam, params, cosm)
295 if self.usedisp:
296 scale = ",".join((str(i), str(j), 'max'))
297 midas.loadImag(cosm, "scale="+scale, "cuts=0,1")
298
299 - def cycle(self, beg, end, plt, fcat=False, pfix=""):
300 k = True
301 if fcat:
302 midas.createIcat(fcat, "NULL")
303 else:
304 namlst = []
305 for bnum in xrange(beg, end+1):
306 bnam = 'l'+str(bnum).zfill(3) + pfix
307 if os.path.isfile(bnam+'.'+self.ext):
308 if k:
309 if self.verbose:
310 print "Add first", bnam, "image"
311 nx, ny = self.readesc(bnam)
312 cut = ny if plt == "row" else nx
313 self.plot(bnam, plt, cut)
314 firstnam, k = bnam, False
315 else:
316 if self.verbose:
317 print "Add", bnam, "image"
318 self.plot(bnam, plt, cut, over=True)
319 if fcat:
320 midas.addIcat(fcat, bnam)
321 else:
322 namlst.append(bnam)
323 if not fcat:
324 namlst = ",".join(namlst)
325 else:
326 namlst = fcat
327 return firstnam, namlst, cut
328
329 - def pltgra(self, resnam, plt, cut, over=True, midlo=False):
330 midas.setGrap("color=2")
331 self.plot(resnam, plt, cut, over=over)
332 midas.setGrap("color=1")
333 if midlo:
334 midas.do("@@ lo_ima " + resnam)
335 elif self.usedisp:
336 self.loima(resnam)
337
338 - def crebiasref(self, beg, end, resnam='bias', icat=False, plt="row",
339 med=None):
340 """Create bias frame from set of biases.
341 @param resnam: resulting frame name
342 @param med: value of low and high interval in median averaging
343 @param plt: plotting mode, could be row or col.
344 """
345 firstnam, namlst, cut = self.cycle(beg, end, plt, fcat=icat)
346 if not med:
347 med = self.med
348 midas.averageImag(resnam, "=", namlst, "? +", self.avermed(med))
349 self.pltgra(resnam, plt, cut)
350
351 - def creflatref(self, beg, end, resnam=None, icat='flat.cat', plt="col",
352 pfix="d", averopt="", delcat=False):
353 """Create flat field average image
354 @param delcat: delete created cat, if icat is present
355 @param pfix: postfix of image filename, the "d" value means
356 dark substracted
357 """
358 firstnam, namlst, cut = self.cycle(beg, end, plt, fcat=icat, pfix=pfix)
359 if not resnam:
360 resnam = firstnam.replace('d', 'ad')
361 midas.averageImag(resnam, "=", namlst, "?", averopt)
362 self.pltgra(resnam, plt, cut)
363 if delcat and icat:
364 os.remove(icat)
365
366 - def subdark(self, beg, end, biasname="bias", pfix="d"):
367 """Substract bias from images"""
368 for bnum in xrange(beg, end+1):
369 bnam = 'l'+str(bnum).zfill(3)
370 if os.path.isfile(bnam+'.'+self.ext):
371 nameo = bnam + pfix
372 midas.computeImag(nameo, "=", bnam, "-", biasname)
373 midas.statistImag(nameo)
374 if self.usedisp:
375 self.loima(nameo)
376 midas.copyDd(bnam, "*,3", nameo)
377 os.remove(bnam+'.'+self.ext)
378
379
381 """Class for image processing with MIDAS, against MIDAS sripting language =)
382 We use MIDAS routines through pyMidas, but replace it with numpy and pyfits,
383 if it is possible.
384
385 This code also heavily depends of some specific characteristics of SAO RAS
386 spectral archive, mostly on spectra taken on Nesmyth Echelle Spectrograph of
387 6-meter telescope.
388 It is not pipeline package, but a set of non-interactive methods for packet
389 processing. It contains methods for averaging images, substracting bias
390 frame etc.
391 @param dest_dir: destination dir for moving processed fits files. Could be
392 omitted.
393 @param usedisp: use Midas display for loading images using Midas routines
394 """
395 - def __init__(self, dest_dir="arch", usedisp=False, verbose=False):
396 self.dest_dir = dest_dir
397 self.verbose = bool(verbose)
398 self.usedisp = usedisp
399 self.fitsfixmode = {True: 'fix', False: 'silentfix'}[self.verbose]
400 self.sep, self.postfix, self.ext = '_', '.', 'fits'
401
402 self.imgcut_dct = {2052: (3, 2048, 4, 2039),
403
404
405
406 2068: (0, 2048, 2, 2048),
407 1050: (5, 1040, 2, 1157)
408 }
409 self.nesbadrows = [(338, 337, 339), (568, 567, 569),
410 (336, 336, 335), (335, 336), (1044, 1044, 1043, 1042),
411 (1043, 1044), (1042, 1044), (433, 433, 434, 435),
412 (434, 433), (435, 433), (185, 185, 184), (184, 185), (1903, 1903, 1902),
413 (1902, 1903), (1229, 1229, 1228), (1228, 1229), (47, 47, 48), (48, 47)]
414 self.rowdoc = lambda d, xs: sum([d[:, 1992-r] for r in xs]) / len(xs)
415 self.mw = MidWorker(usedisp=usedisp, verbose=verbose)
416 self.Lg = Logger(fitsfixmode=self.fitsfixmode, verbose=verbose)
417
418 - def prepare(self, files, log=True, crop=True, rot=True, badrow=False,
419 substbias=False, cleanhead=True, filmove=True, process=True):
420 """Method for packet preparing and/or logging of raw images,
421 mostly taken on spectrographs of 6-meter telescope.
422 @param files: list of raw image parameters with its paths
423 @param log: create log, if True. if "logonly", create log
424 without image processing
425 @param crop: crop each image depending on its shape, which is
426 determine used CCD chip
427 @param rot: rotate image clockwise
428 @param badrow: mask bad rows on NES CCD chip
429 @param substbias: substract bias frame, if the filename
430 bias image is given
431 @param cleanhead: remove empty strings in fits headers
432 @param filmove: move or not source files (raw images) to
433 self.arch_dir, after they are have been processed
434 @param process: process images, if True. Otherwise all
435 of the processing procedures (crop, rot, badrow, substbias)
436 will be omitted
437 """
438 if substbias and process and type(substbias) is str:
439 self.bias = pyfits.getdata(substbias)
440 self.substbias = True
441 else:
442 self.substbias = False
443 self.crop = crop
444 self.rot = rot
445 self.badrow = badrow
446 if files and filmove and not os.path.isdir(self.dest_dir):
447 os.mkdir(self.dest_dir)
448 for (file_pth, num, date, fflag, keymode) in sorted(files):
449 if self.verbose:
450 print pyfits.info(file_pth)
451 print "Open file", file_pth
452 curfts = pyfits.open(file_pth)
453 if cleanhead:
454
455 del curfts[0].header['']
456 if log:
457 self.Lg.logger(curfts, num, date, file_pth, keymode=keymode)
458 if log == "logonly":
459 continue
460 if file_pth in self.Lg.biases:
461 if filmove:
462 shutil.move(file_pth, self.dest_dir)
463 continue
464 elif file_pth in self.Lg.flats:
465 newname = self.Lg.flats[file_pth]
466 hilim = 9000
467 elif file_pth in self.Lg.calib:
468 newname = self.Lg.calib[file_pth]
469 hilim = 5000
470 else:
471 date = "".join((str(date[0]), str(date[1]).zfill(2),
472 str(date[2]).zfill(2)))
473 newname = os.path.join(self.dest_dir, "".join((date, self.sep,
474 num, self.postfix, self.ext)))
475 hilim = 2600
476 if process:
477 self.processdata(curfts, hilim=hilim)
478 self.savefits(curfts, newname)
479 if filmove:
480 shutil.move(file_pth, self.dest_dir)
481 if file_pth in self.Lg.flats:
482 continue
483 elif file_pth in self.Lg.calib:
484 self.mw.filtcosm(newname, cosm="c"+num)
485 continue
486 self.mw.savebdf(newname, "l"+num+"d.bdf")
487
488 - def crebias(self, biasname="bias", files=None, filmove=True, log=True):
489 """Average bias frames with crebias Midworker method"""
490 if files:
491 self.prepare(files, process=False, cleanhead=False,
492 log=log, filmove=filmove)
493 elif not self.Lg.biases:
494 print "No biases, return"
495 return
496 for file_pth, newname in self.Lg.biases.items():
497 if not os.path.isfile(newname):
498 shutil.copy2(file_pth, newname)
499 self.mw.crebias(self.Lg.biases.values(), biasname=biasname)
500
501 - def creflat(self, flatname="flat"):
502 """Average flat field frames with creflat Midworker method.
503 After averaging make a filter/cosm procedure
504 """
505 if self.Lg.flats:
506 self.mw.creflat(self.Lg.flats.values(), flatname=flatname)
507 self.mw.filtcosm(flatname)
508
510 """Process image data: substract bias, crop, rotate, mask bad rows"""
511 data = curfts[0].data
512 dshape = data.shape
513 if self.substbias:
514 data -= self.bias
515 if self.badrow:
516 self.nesbadrow(data, hilim=hilim)
517 if self.crop and dshape[0] in self.imgcut_dct:
518 x1, x2, y1, y2 = self.imgcut_dct[dshape[0]]
519
520 data = data[y1:y2, x1:x2]
521 if self.rot and dshape[0] >= 2052:
522 data = np.rot90(data)
523
524
525
526
527 curfts[0].data = data
528 curfts[0].scale('float32', 'old')
529
531 """Save current fits"""
532 if os.path.isfile(newname):
533 bcknam = newname.replace("."+self.ext, "-backup."+self.ext)
534 shutil.move(newname, bcknam)
535 try:
536 curfts.writeto(newname)
537 except pyfits.core.VerifyError:
538 curfts.verify(self.fitsfixmode)
539 curfts.writeto(newname)
540 curfts.close()
541
542 - def nesbadrow(self, d, lowcut=-25, hilim=600):
543 """Mask bad pixels in raw images taken with Uppsala NES chip.
544 """
545 for row in self.nesbadrows:
546 d[:, 2052-row[0]-60] = self.rowdoc(d, row[1:])
547 d[:, 1992] = (d[:, 1991] + d[:, 1993]) / 2
548 d[:, 1997] = (d[:, 1996] + d[:, 1998]) / 2
549 d[:, 2005] = (d[:, 2004] + d[:, 2006]) / 2
550 d[:, 2008] = (d[:, 2007] + d[:, 2009]) / 2
551 d[:, 2010] = (d[:, 2009] + d[:, 2011]) / 2
552 d[:, 2014] = (d[:, 2013] + d[:, 2015]) / 2
553 d[:, 2027] = (d[:, 2026] + d[:, 2028]) / 2
554 for i in xrange(2036, 2038):
555 d[i, :22] = (d[i-1, :22] + d[i-2, :22])/2
556 d[i, 1337:1569] = (d[i-1, 1337:1569] + d[i-2, 1337:1569])/2
557 for i in xrange(2038, 2040):
558 d[i, :24] = (d[i-1, :24] + d[i-2, :24])/2
559 d[i, 1337:1569] = (d[i-1, 1337:1569] + d[i-2, 1337:1569])/2
560 for i in xrange(2040, 2042):
561 d[i, :26] = d[i-1, :26]
562 d[i, 1125:1692] = (d[i-1, 1125:1692] + d[i-2, 1125:1692])/2
563 for i in xrange(2042, 2044):
564 d[i, :34] = d[i-1, :34]
565
566
567
568
569
570 zonlim = (1, 118, 1988)
571 zone = d[zonlim[0]:zonlim[1], zonlim[2]:]
572 med = min(np.median(zone), hilim)
573 hilim = max(np.mean(d), hilim)
574 d[zonlim[0]:zonlim[1], zonlim[2]:][np.where(zone > hilim)] = med
575
576 d[:280, 2004:2011][np.where(d[:280, 2004:2011] > hilim)] = med
577 d[np.where(d < lowcut)] = lowcut
578
579 - def savelog(self, filenam="night", wdir=""):
580 """Save observation log to file."""
581 if os.path.isfile(os.path.join(wdir, filenam + ".log")):
582 shutil.move(os.path.join(wdir, filenam + ".log"),
583 os.path.join(wdir, filenam + ".old"))
584 logfile = open(os.path.join(wdir, filenam+".log"), "w")
585 log = self.Lg.logtostr()
586 for line in log:
587 print >> logfile, line
588 print >> logfile
589 log = self.Lg.journaltostr()
590 for line in log:
591 print >> logfile, line
592 logfile.close()
593