Source code for ctao_datamodel._fits
"""Serialization of models to and from FITS headers."""
import re
import warnings
from astropy import units as u
from astropy.io.fits import Card, Header
from pydantic import BaseModel
from ._visitor import (
extract_keyword_mapping,
flatten_model_instance,
get_field_metadata,
unflatten_model_instance,
)
__all__ = [
"instance_to_fits_header",
"fits_header_to_flat_dict",
"fits_header_to_instance",
]
def _get_field_from_instance(model_instance: BaseModel, flat_key: str, sep: str = "."):
"""Return FieldInfo for an instance at a flattened key."""
path = flat_key.split(sep)
field = path.pop()
instance = model_instance
for attr in path:
# handle expanded list, where the key is an integer:
if isinstance(instance, list) and re.match("^[0-9]+$", attr):
list_index = int(attr)
instance = instance[list_index]
continue
# normal case
instance = getattr(instance, attr)
# now instance is the sub-model, so get the field:
return instance.__class__.model_fields[field]
[docs]
def instance_to_fits_header(
model_instance: BaseModel, use_short: bool = True, hieararch_namespace: str = "CTAO"
) -> Header:
"""
Convert a model instance to a FITS header.
The resulting header will have a Card for each keyword. Long keywords will use the
FITS HIERARCH standard in the given namespace
Parameters
----------
model_instance: BaseModel
model instance to serialize
use_short: bool
if True, replace any log keywords that have fits_keyword mapping with their short form.
hierarch_namespace: str
starting string for the HIERARCH keyword
Returns
-------
Header:
FITS header suitable for writing to a file.
"""
flat = flatten_model_instance(model_instance, separator=" ", to_string=True)
cards = []
for k, v in flat.items():
field = _get_field_from_instance(model_instance, k, sep=" ")
unit = get_field_metadata(field=field, metadata_key="unit")
desc = get_field_metadata(field=field, metadata_key="description")
fits = get_field_metadata(field=field, metadata_key="fits_keyword")
if unit:
unit = u.Unit(unit)
comment = f"[{unit:fits}] {desc}"
else:
comment = desc
if fits and use_short:
cards.append(Card(keyword=fits, value=v, comment=comment))
else:
cards.append(
Card(
keyword=f"HIERARCH {hieararch_namespace} {k.upper()}",
value=v,
comment=desc,
)
)
return Header(cards)
def get_fits_to_ctao_mapping(model: type[BaseModel], sep: str = ".") -> dict[str, str]:
"""Return a dict mapping FITS keyword to flat CTAO keyword."""
# get the mapping between FITS key and CTAO key
ctao_to_fits = extract_keyword_mapping(model, metadata_key="fits_keyword", sep=sep)
# make reverse mapping:
fits_to_ctao = dict()
for ctao_key, fits_key_list in ctao_to_fits.items():
for fits_key in fits_key_list:
fits_to_ctao[fits_key] = ctao_key
return fits_to_ctao
[docs]
def fits_header_to_flat_dict(
header: Header, model: type[BaseModel], ignore_extra_keys: bool = True
) -> dict[str, str]:
"""
Turn a FITS header back into a flat dict of CTAO keywords.
Parameters
----------
header: Header
FITS header with keys and values to extract
model: type[BaseModel]
Model to use for schema and key mapping
ignore_extra_keys: bool
If False, issue warnings for keys in header that do not
map to model.
Returns
-------
dict[str,str]:
mapping of CTAO keyword to string value.
"""
# get the mapping
fits_to_ctao = get_fits_to_ctao_mapping(model=model)
# build the flat dict
flattened = dict()
for card in header.cards:
fits_key = card.keyword
value = card.value
if fits_key.startswith("CTAO "):
# Hierarchical keys can just be converted:
ctao_key = ".".join(fits_key.replace("CTAO ", "").lower().split(" "))
flattened[ctao_key] = value
elif fits_key in fits_to_ctao:
ctao_key = fits_to_ctao[fits_key]
flattened[ctao_key] = value
else:
if not ignore_extra_keys:
warnings.warn(f"Key '{fits_key}' is not in model {model.__name__}")
return flattened
[docs]
def fits_header_to_instance(
header: Header, model: type[BaseModel], ignore_extra_keys: bool = True
) -> BaseModel:
"""
Turn a FITS header back into an instance of a model.
Parameters
----------
header: Header
FITS header with keys and values to extract
model: type[BaseModel]
Model to use for schema and key mapping
ignore_extra_keys: bool
If False, issue warnings for keys in header that do not
map to model.
Returns
-------
BaseModel:
instance of model provided
"""
flattened = fits_header_to_flat_dict(
header=header, model=model, ignore_extra_keys=ignore_extra_keys
)
return unflatten_model_instance(
flattened, model=model, parent_key="", separator="."
)