Package gavo :: Package helpers :: Module fitstricks
[frames] | no frames]

Source Code for Module gavo.helpers.fitstricks

  1  """ 
  2  Helpers for manipulating FITS files. 
  3   
  4  In contrast to fitstools, this is not for online processing or import of 
  5  files, but just for the manipulation before processing. 
  6   
  7  Rough guideline: if it's about writing fixed fits files, it probably belongs 
  8  here, otherwise it goes to fitstools. 
  9   
 10  Realistically, this module has been hemorraghing functions to 
 11  fitstools and probably should be removed completely. 
 12   
 13  One important function it has grown is FITS header templates.  These 
 14  can be used by processors.  If these use custom templates, they should 
 15  register them (or regret it later).  See registerTemplate's docstring. 
 16  """ 
 17   
 18  #c Copyright 2008-2019, the GAVO project 
 19  #c 
 20  #c This program is free software, covered by the GNU GPL.  See the 
 21  #c COPYING file in the source distribution. 
 22   
 23   
 24  import re 
 25   
 26  from gavo import base 
 27  from gavo.utils import pyfits 
 28   
 29  DEFAULT_IGNORED_HEADERS = ["simple", "bitpix", "naxis", "imageh",  
 30          "imagew", "naxis1", "naxis2", "datamin", "datamax", "date"] 
 31   
32 -def copyFields(header, cardList, 33 ignoredHeaders=frozenset(DEFAULT_IGNORED_HEADERS)):
34 """copies over all cards from cardList into header, excluding headers 35 named in ignoredHeaders. 36 37 ignoredHeaders must be all lowercase. 38 """ 39 for card in cardList: 40 if card.keyword=="COMMENT": 41 header.add_comment(card.value) 42 elif card.keyword=="HISTORY": 43 header.add_history(card.value) 44 elif card.keyword=="": 45 header.append(pyfits.Card("", card.value), end=True) 46 elif card.keyword.lower() in ignoredHeaders: 47 pass 48 else: 49 header.set(card.keyword, card.value, card.comment)
50 51
52 -def getHeaderAsDict(hdr):
53 """returns the "normal" key-value pairs from hdr in a dictionary. 54 55 Comment, history and blank cards are excluded; the comments from the 56 cards are lost, too. 57 """ 58 ignored = frozenset(["", "HISTORY", "COMMENT"]) 59 return dict((k,v) for k,v in hdr.iteritems() 60 if k not in ignored)
61 62
63 -def _makeHeaderSequence(keyTpl, commentTpl):
64 try: 65 return [ 66 (keyTpl%ind, commentTpl%numeral) 67 for ind, numeral in [ 68 (1, "1st"), 69 (2, "2nd"), 70 (3, "3rd"),]] 71 except TypeError: 72 raise base.ReportableError("Invalid header sequence templates: %r %r"%( 73 keyTpl, commentTpl))
74 75 76 # FITS header template for a minimal pixel array 77 MINIMAL_IMAGE_TEMPLATE = [ 78 pyfits.Card("SIMPLE", True), 79 pyfits.Card("EXTEND", True), 80 ("BITPIX", "Array data type"), 81 pyfits.Card("NAXIS", 2), 82 ("NAXIS1", "Number of elements along 1st axis"), 83 ("NAXIS2", "Number of elements along 2nd axis"), 84 ("BZERO", "Zero point of pixel scaling function"), 85 ("BSCALE", "Slope of pixel scaling function"),] 86 87 88 WCS_TEMPLATE = [ 89 pyfits.Card(value="-------------------- Spatial WCS"), 90 ('EQUNIOX', "Equinox of RA and Dec"), 91 ('RADESYS', "Reference System used for RA and Dec"), 92 ('WCSAXES', "Number of FITS axes covered by WCS"), 93 ('CTYPE1', "Projection on axis 1"), 94 ('CTYPE2', "Projection on axis 2"), 95 ('LONPOLE', "See sect 2.4 of WCS paper II"), 96 ('LATPOLE', "See sect 2.4 of WCS paper II"), 97 ('CRVAL1', "Longitude of reference point"), 98 ('CRVAL2', "Latitude of reference point"), 99 ('CRPIX1', "X reference pixel"), 100 ('CRPIX2', "Y reference pixel"), 101 ('CUNIT1', "X pixel scale units"), 102 ('CUNIT2', "Y pixel scale units"), 103 ('CD1_1', "(1,1) Full transformation matrix"), 104 ('CD1_2', "(1,2) Full transformation matrix"), 105 ('CD2_1', "(2,1) Full transformation matrix"), 106 ('CD2_2', "(2,2) Full transformation matrix"), 107 ('PC1_1', "(1,1) Transformation matrix"), 108 ('PC1_2', "(1,2) Transformation matrix"), 109 ('PC2_1', "(2,1) Transformation matrix"), 110 ('PC2_2', "(2,2) Transformation matrix"), 111 ('A_ORDER', "Correction polynomial order, axis 1"), 112 ('A_0_0', "Axis 1 correction polynomial, coefficient"), 113 ('A_0_1', "Axis 1 correction polynomial, coefficient"), 114 ('A_0_2', "Axis 1 correction polynomial, coefficient"), 115 ('A_1_0', "Axis 1 correction polynomial, coefficient"), 116 ('A_1_1', "Axis 1 correction polynomial, coefficient"), 117 ('A_2_0', "Axis 1 correction polynomial, coefficient"), 118 ('B_ORDER', "Correction polynomial order, axis 2"), 119 ('B_0_0', "Axis 2 correction polynomial, coefficient"), 120 ('B_0_1', "Axis 2 correction polynomial, coefficient"), 121 ('B_0_2', "Axis 2 correction polynomial, coefficient"), 122 ('B_1_0', "Axis 2 correction polynomial, coefficient"), 123 ('B_1_1', "Axis 2 correction polynomial, coefficient"), 124 ('B_2_0', "Axis 2 correction polynomial, coefficient"), 125 ('AP_ORDER', "Inverse polynomial order, axis 1"), 126 ('AP_0_0', "Axis 1 inverse polynomial, coefficient"), 127 ('AP_0_1', "Axis 1 inverse polynomial, coefficient"), 128 ('AP_0_2', "Axis 1 inverse polynomial, coefficient"), 129 ('AP_1_0', "Axis 1 inverse polynomial, coefficient"), 130 ('AP_1_1', "Axis 1 inverse polynomial, coefficient"), 131 ('AP_2_0', "Axis 1 inverse polynomial, coefficient"), 132 ('BP_ORDER', "Inverse polynomial order, axis 2"), 133 ('BP_0_0', "Axis 2 inverse polynomial, coefficient"), 134 ('BP_0_1', "Axis 2 inverse polynomial, coefficient"), 135 ('BP_0_2', "Axis 2 inverse polynomial, coefficient"), 136 ('BP_1_0', "Axis 2 inverse polynomial, coefficient"), 137 ('BP_1_1', "Axis 2 inverse polynomial, coefficient"), 138 ('BP_2_0', "Axis 2 inverse polynomial, coefficient"),] 139 140 # Internal representation of Tuvikene et al FITS headers for plates 141 # See https://www.plate-archive.org/wiki/index.php/FITS_header_format 142 WFPDB_TEMPLATE = MINIMAL_IMAGE_TEMPLATE+[ 143 pyfits.Card(value="-------------------- Original data of observation"), 144 ("DATEORIG", "Original recorded date of the observation"), 145 ("TMS-ORIG", "Start of the observation (logs)"), 146 ("TME-ORIG", "End of the observation (logs)"), 147 ("TIMEFLAG", "Quality flag of the recorded observation time"), 148 ("RA-ORIG", "RA of plate center as given in source"), 149 ("DEC-ORIG", "Dec of plate center as given in source"), 150 ("COORFLAG", "Quality flag of the recorded coordinates"), 151 ("OBJECT", "Observed object or field"), 152 ("OBJTYPE", "Object type as in WFPDB"), 153 ("EXPTIME", " [s] Exposure time of the first exposure"), 154 ("NUMEXP", "Number of exposures"), 155 ]+_makeHeaderSequence( 156 "DATEOR%d", "Original recorded date of the %s exposure" 157 )+_makeHeaderSequence( 158 "TMS-OR%d", "Start of %s exposure (logs)" 159 )+_makeHeaderSequence( 160 "TME-OR%d", "End of %s exposure (logs)" 161 )+_makeHeaderSequence( 162 "OBJECT%d", "Object name for %s exposure" 163 )+_makeHeaderSequence( 164 "OBJTYP%d", "Object type of %s OBJECT" 165 )+_makeHeaderSequence( 166 "EXPTIM%d", " [s] Exposure time %s exposure")+[ 167 168 pyfits.Card(value="-------------------- Observatory and instrument"), 169 ("OBSERVAT", "Observatory name"), 170 ("SITENAME", "Observatory site name."), 171 ("SITELONG", " [deg] East longitude of observatory"), 172 ("SITELAT", " [deg] Latitude of observatory"), 173 ("SITEELEV", " [m] Elevation of the observatory"), 174 ("TELESCOP", "Telescope name"), 175 ("TELAPER", " [m] Clear aperture of the telescope"), 176 ("TELFOC", " [m] Focal length of the telescope"), 177 ("TELSCALE", " [arcsec/mm] Plate scale"), 178 ("INSTRUME", "Instrument name"), 179 ("DETNAM", "Detector name"), 180 ("METHOD", "Observation method as in WFPDB"), 181 ("FILTER", "Filter type"), 182 ("PRISM", "Objective prism used"), 183 ("PRISMANG", " [deg] Angle of the objective prism"), 184 ("DISPERS", " [Angstrom/mm] Dispersion"), 185 ("GRATING", "Fix this comment."), 186 ("FOCUS", "Focus value (from logbook)."), 187 ("TEMPERAT", "Air temperature (from logbook)"), 188 ("CALMNESS", "Calmness (seeing conditions), scale 1-5"), 189 ("SHARPNES", "Sharpness, scale 1-5"), 190 ("TRANSPAR", "Transparency, scale 1-5"), 191 ("SKYCOND", "Notes on sky conditions (logs)"), 192 ("OBSERVER", ""), 193 ("OBSNOTES", "Observer notes (logs)"), 194 195 pyfits.Card(value="-------------------- Photographic plate"), 196 ("PLATENUM", "Plate number in logs"), 197 ("WFPDB-ID", "Plate identifier in WFPDB"), 198 ("SERIES", "Series or survey of plate"), 199 ("PLATEFMT", "Informal designation of plate format"), 200 ("PLATESZ1", " [cm] Plate size along axis1"), 201 ("PLATESZ2", " [cm] Plate size along axis2"), 202 ("FOV1", "Field of view along axis 1"), 203 ("FOV2", "Field of view along axis 2"), 204 ("EMULSION", "Type of the photographic emulsion"), 205 ("PQUALITY", "Quality of the plate"), 206 ("PLATNOTE", "Notes about the plate"), 207 208 pyfits.Card(value="-------------------- Derived observation data"), 209 ("DATE-OBS", "UT date and time of obs. start"), 210 ]+_makeHeaderSequence( 211 "DT-OBS%d", "UT d/t of start of %s exposure")+[ 212 ("DATE-AVG", "UT d/t mid-point of observation"), 213 ]+_makeHeaderSequence( 214 "DT-AVG%d", "UT d/t mid-point of %s exposure")+[ 215 ("DATE-END", "UT d/t end of observation"), 216 ]+_makeHeaderSequence( 217 "DT-END%d", "UT d/t of end of %s exposure")+[ 218 ("YEAR", "Julian year at start of obs"), 219 ]+_makeHeaderSequence( 220 "YEAR%d", "Julian year at start of %s obs")+[ 221 ("YEAR-AVG", "Julian year at mid-point of obs"), 222 ]+_makeHeaderSequence( 223 "YR-AVG%d", "Julian year at mid-point of %s obs")+[ 224 ("JD", "Julian date at start of obs"), 225 ]+_makeHeaderSequence( 226 "JD%d", "Julian date at start of %s obs")+[ 227 ("JD-AVG", "Julian date at mid-point of obs") 228 ]+_makeHeaderSequence( 229 "JD-AVG%d", "Julian date at mid-point of %s obs")+[ 230 ("JD-AVG", "Julian date at mid-point of obs"), 231 ]+_makeHeaderSequence( 232 "JD-AVG%d", "Julian date at mid-point of %s obs")+[ 233 ("RA", "ICRS center of plate RA h:m:s"), 234 ("DEC", "ICRS center of plate Dec d:m:s"), 235 ("RA_DEG", "[deg] ICRS center of plate RA"), 236 ("DEC_DEG", "[deg] ICRS center of plate Dec"), 237 ]+_makeHeaderSequence( 238 "RA_DEG%d", " [deg] ICRS center RA %s obs" 239 )+_makeHeaderSequence( 240 "DEC_DE%d", " [deg] ICRS center DEC %s obs")+[ 241 ("AIRMASS", "Airmass at mean epoch"), 242 ("HA", "Hour angle at mean epoch"), 243 ("ZD", "Zenith distance at mean epoch"), 244 245 pyfits.Card(value="-------------------- Scan details"), 246 ("SCANNER", "Scanner hardware used"), 247 ("SCANRES1", " [in-1] Scan resolution along axis 1"), 248 ("SCANRES2", " [in-1] Scan resolution along axis 2"), 249 ("PIXSIZE1", " [um] Pixel size along axis 1"), 250 ("PIXSIZE2", " [um] Pixel size along axis 2"), 251 ("SCANSOFT", "Scan software used"), 252 ("SCANGAM", "Scan gamma value"), 253 ("SCANFOC", "Scan focus"), 254 ("WEDGE", "Photometric step-wedge type"), 255 ("DATESCAN", "UT scan date and time"), 256 ("SCANAUTH", "Author of the scan"), 257 ("SCANNOTE", "Notes about the scan"), 258 ("DATAMIN", "Min pixel value in image"), 259 ("DATAMAX", "Max pixel value in image"), 260 261 pyfits.Card(value="-------------------- Data files"), 262 ("FILENAME", "Filename of this file"), 263 ("FN-WEDGE", "Filename of the wedge scan"), 264 ("FN-PRE", "Filename of the preview image"), 265 ("FN-COVER", "Filename of the envelope image"), 266 ("FN-LOGB", "Filename of the logbook image"), 267 ("ORIGIN", "Origin of this file"), 268 ("DATE", "File last changed"), 269 270 ] + WCS_TEMPLATE + [ 271 272 pyfits.Card(value="-------------------- Other header cards"),] 273 274 275 _TEMPLATE_NAMES = [ 276 ("minimal", MINIMAL_IMAGE_TEMPLATE), 277 ("wfpdb", WFPDB_TEMPLATE),] 278 279
280 -def registerTemplate(templateName, template):
281 """registers a named FITS header template. 282 283 Registering lets DaCHS figure out the template from a history entry 284 it leaves, so it's certainly a good idea to do that. 285 286 For templateName, use something containing a bit of your signature 287 (e.g., ariAncientPlate rather than just ancientPlate). 288 """ 289 _TEMPLATE_NAMES.append((templateName, template))
290 291
292 -def getTemplateForName(templateName):
293 """returns the FITS template sequence for templateName. 294 295 A NotFoundError is raised if no such template exists. 296 """ 297 for name, template in _TEMPLATE_NAMES: 298 if name==templateName: 299 return template 300 raise base.NotFoundError(templateName, "FITS template", 301 "registred templates", hint="If you used a custom template," 302 " have you called fitstricks.registerTemplate(name, template)" 303 " for it?")
304 305
306 -def getNameForTemplate(template):
307 """returns the name under which the FITS template has been registred. 308 """ 309 for name, namedTemplate in _TEMPLATE_NAMES: 310 if template is namedTemplate: 311 return name 312 raise base.NotFoundError("template "+str(id(template)), 313 "FITS template", 314 "registred templates", hint="If you used a custom template," 315 " have you called fitstricks.registerTemplate(name, template)" 316 " for it?")
317 318
319 -def getTemplateNameFromHistory(hdr):
320 """returns the template name used for generating hdr. 321 322 A ReportableError is raised if the info signature is missing. 323 """ 324 for card in hdr["HISTORY"]: 325 mat = re.search("GAVO DaCHS template used: (\w+)", card) 326 if mat: 327 return mat.group(1) 328 raise base.ReportableError("DaCHS template signature not found.", 329 hint="This means that a function needed to figure out which" 330 " FITS template DaCHS used to generate that header, and no" 331 " such information was found in the Header's HISTORY cards." 332 " Either this file hasn't been written by DaCHS FITS templating" 333 " engine, or some intervening thing hosed the history.")
334 335
336 -def _applyTemplate(hdr, template, values):
337 """helps makeHeaderFromTemplate. 338 339 Specifically, it moves items in values mentioned in template into 340 header in template's order. hdr and values are modified in that process. 341 """ 342 for tp in template: 343 if isinstance(tp, pyfits.Card): 344 tp.value = values.pop(tp.keyword, tp.value) 345 hdr.append(tp, end=True) 346 else: 347 key, comment = tp 348 argKey = key.replace("-", "_") 349 if values.get(argKey) is not None: 350 try: 351 val = values[argKey] 352 if isinstance(val, unicode): 353 val = val.encode("ascii") 354 355 hdr.append(pyfits.Card(key, val, comment), end=True) 356 except Exception as ex: 357 if hasattr(ex, "args") and isinstance(ex.args[0], basestring): 358 ex.args = ("While constructing card %s: %s"%( 359 key, ex.args[0]),)+ex.args[1:] 360 raise 361 362 values.pop(argKey, None)
363 364
365 -def _copyMissingCards(newHdr, oldHdr):
366 """helps makeHeaderFromTemplate. 367 368 Specifically, it copies over all cards from oldHder to newHdr not yet 369 present there. It will also move over history and comment cards. 370 371 This will modify newHdr in place. 372 """ 373 commentCs, historyCs = [], [] 374 375 for card in oldHdr.cards: 376 if card.keyword=="COMMENT": 377 commentCs.append(card) 378 elif card.keyword=="HISTORY": 379 if not "GAVO DaCHS template used" in card.value: 380 historyCs.append(card) 381 elif card.keyword: 382 if card.keyword not in newHdr: 383 newHdr.append(card, end=True) 384 385 for card in historyCs: 386 newHdr.append(card, end=True) 387 newHdr.append(pyfits.Card(value=""), end=True) 388 for card in commentCs: 389 newHdr.append(card, end=True)
390 391
392 -def makeHeaderFromTemplate(template, originalHeader=None, **values):
393 """returns a new pyfits.Header from template with values filled in. 394 395 template usually is the name of a template previously registered with 396 registerTemplate, or one of DaCHS predefined template names (currently, 397 minimal and wfpdb). In a pinch, you can also pass in an immediate 398 headers. 399 400 originalHeader can be a pre-existing header; the history and comment 401 cards are copied over from it, and if any of its other cards have not 402 yet been added to the header, they will be added in the order that they 403 apprear there. 404 405 values for which no template item is given are added in random order 406 after the template unless an originalHeader is passed. In that case, 407 they are assumed to originate there and are ignored. 408 """ 409 values = values.copy() 410 hdr = pyfits.Header() 411 412 if isinstance(template, basestring): 413 templateName = template 414 template = getTemplateForName(templateName) 415 else: 416 try: 417 templateName = getNameForTemplate(template) 418 except base.NotFoundError: 419 base.notifyWarning("Using anonymous FITS template.") 420 templateName = "anonymous" 421 422 _applyTemplate(hdr, template, values) 423 424 if originalHeader: 425 _copyMissingCards(hdr, originalHeader) 426 elif values: 427 base.ui.notifyWarning("The following values were left after" 428 " applying a FITS template and will be added in random order: %s"% 429 (", ").join(values.keys())) 430 for key, value in values.iteritems(): 431 hdr.append(pyfits.Card(key, value), end=True) 432 433 434 hdr.add_history("GAVO DaCHS template used: "+templateName) 435 return hdr
436 437
438 -def updateTemplatedHeader(hdr, templateName=None, **kwargs):
439 """return hdr updated with kwargs. 440 441 hdr is assumed to have been created with makeHeaderFromTemplate 442 and contain the template name in a history entry. 443 444 You can pass in templateName to keep DaCHS from trying to get things 445 from the header. 446 447 [It is probably better to use makeHeaderFromTemplate directly, passing 448 in the orginalHeader; that preserves the order of non-templated 449 headers]. 450 """ 451 if templateName is None: 452 templateName = getTemplateNameFromHistory(hdr) 453 template = getTemplateForName(templateName) 454 455 vals = getHeaderAsDict(hdr) 456 vals.update(kwargs) 457 458 res = makeHeaderFromTemplate(template, originalHeader=hdr, **vals) 459 return res
460