Source code for sofia_redux.instruments.flitecam.hdcheck
# Licensed under a 3-clause BSD style license - see LICENSE.rst
import os
from astropy import log
from astropy.io import fits
import configobj
__all__ = ['validate_header', 'hdcheck']
[docs]
def validate_header(header, keywords):
"""
Validate all keywords in a header against the keywords requirement.
Parameters
----------
header : astropy.io.fits.Header
keywords : configobj.ConfigObj
dripconf : bool
If True, will check the configuration file for the required
parameter before checking headers (using getpar)
Returns
-------
bool
True if all keywords in the header were validated, False otherwise
"""
# Check a few important mode keywords
nodding = header.get('NODDING', False)
dithering = header.get('DITHER', False)
# Add any that are True to the requirement set
req_set = ['*']
if nodding:
req_set.append('nodding')
if dithering:
req_set.append('dithering')
# Flag to track overall validity
valid = True
# Loop through keywords, checking against requirements
reqdict = keywords.dict()
for key, req in reqdict.items():
# Retrieve requirements
try:
req_category = str(req['requirement']).strip()
except KeyError:
req_category = '*'
try:
req_dtype = str(req['dtype']).strip()
except KeyError:
req_dtype = 'str'
try:
req_drange = req['drange']
except KeyError:
req_drange = None
# Get type class corresponding to string
if req_dtype == 'bool':
req_dtype_class = bool
elif req_dtype == 'int':
req_dtype_class = int
elif req_dtype == 'float':
req_dtype_class = float
else:
req_dtype_class = str
# Check if key is required for this data type
if req_category not in req_set:
continue
# Retrieve value from header and/or config file
val = header.get(key, None)
valtype = type(val)
stype = valtype.__name__
# Check if required key is present
if val is None:
valid = False
msg = f'Required keyword {key} not found'
log.warning(msg)
continue
# Check if key matches required type
if req_dtype in ['str', 'bool', 'int']:
# Use exact type for str, bool, int
if stype != req_dtype:
valid = False
msg = f'Required keyword {key} has wrong ' \
f'type {stype}; should be {req_dtype}'
log.warning(msg)
continue
elif req_dtype == 'float':
# Allow any number type for float types
if stype not in ['float', 'int']:
valid = False
msg = f'Required keyword {key} has wrong ' \
f'type {stype}; should be {req_dtype}'
log.warning(msg)
continue
# Check if value meets range requirements
if req_drange is not None:
# Check for enum first -- ignore any others if
# present. May be used for strings, bools, or numerical
# equality.
if 'enum' in req_drange:
enum = req_drange['enum']
# Make into list if enum is a single value
if type(enum) is not list:
enum = [enum]
# Cast to data type
if req_dtype == 'bool':
enum = [True if str(e).strip().lower() == 'true'
else False for e in enum]
else:
try:
enum = [req_dtype_class(e) for e in enum]
except ValueError as error:
msg = f'Error in header configuration file for ' \
f'key {key}'
log.error(msg)
raise error
# Case-insensitive comparison for strings
if stype == 'str':
enum = [str(e).strip().upper() for e in enum]
if val.strip().upper() not in enum:
valid = False
msg = f'Required keyword {key} has wrong ' \
f'value {val}; should be in {enum}'
log.warning(msg)
continue
else:
if val not in enum:
valid = False
msg = f'Required keyword {key} has wrong ' \
f'value {val}; should be in {enum}'
log.warning(msg)
continue
# Check for a minimum requirement
# (numerical value must be >= minimum)
else:
if 'min' in req_drange and stype in ['int', 'float']:
try:
minval = req_dtype_class(req_drange['min'])
except ValueError as error:
msg = f'Error in header configuration file for ' \
f'key {key}'
log.error(msg)
raise error
if val < minval:
valid = False
msg = f'Required keyword {key} has wrong ' \
f'value {val}; should be >= {minval}'
log.warning(msg)
continue
# Check for a maximum requirement
# (numerical value must be <= maximum)
if 'max' in req_drange and stype in ['int', 'float']:
try:
maxval = req_dtype_class(req_drange['max'])
except ValueError as error:
msg = f'Error in header configuration file for ' \
f'key {key}'
log.error(msg)
raise error
if val > maxval:
valid = False
msg = f'Required keyword {key} has wrong ' \
f'value {val}; should be <= {maxval}'
log.warning(msg)
continue
return valid
[docs]
def hdcheck(headers, kwfile):
"""
Checks file headers against validity criteria
Checks if the headers of the input files satisfy criteria described
in a separate file, kept in the calibration data directory. This
file should be a configuration file in INI format, specifying the
keyword name, condition, data type, and any range requirements.
Parameters
----------
headers : list of astropy.io.fits.Header
Headers to check.
kwfile : str
Keyword definition file to use.
Returns
-------
bool
True if headers of all files are correct; False otherwise
"""
if os.path.isfile(kwfile):
try:
reqconf = configobj.ConfigObj(kwfile)
except configobj.ConfigObjError as error:
msg = 'Error while loading header configuration file.'
log.error(msg)
raise error
else:
msg = f'{kwfile} is invalid file name for header configuration'
log.error(msg)
raise IOError(msg)
allvalid = True
for header in headers:
if not isinstance(header, fits.Header):
log.error('Could not read FITS header')
allvalid = False
continue
header_ok = validate_header(header, reqconf)
allvalid &= header_ok
if not header_ok:
log.error(f"File has wrong header: "
f"{header.get('FILENAME', 'UNKNOWN')}")
return allvalid