"""
Provides utilities to read and write fits files
"""
import re
from pathlib import Path
from typing import Tuple
import astropy.io.fits as fits
import git
import solarnet_metadata
from astropy import units as u
from astropy.io import ascii
from astropy.time import Time
from solarnet_metadata.schema import SOLARNETSchema
import padre_craft
from padre_craft import log
CUSTOM_ATTRS_PATH = (
padre_craft._data_directory / "fits" / "fits_keywords_primaryhdu.yaml"
)
FITS_HDR_KEYTOCOMMENT = ascii.read(
padre_craft._data_directory / "fits" / "fits_keywords_dict.csv", format="csv"
)
FITS_HDR_KEYTOCOMMENT.add_index("keyword")
# Dict[str, Tuple[str, str, str]]
# Lib <n><a>: (Library Name, Library Version, Library URL)
PRLIBS = {
"1A": (
"padre_craft",
padre_craft.__version__,
"https://github.com/PADRESat/padre_craft.git",
),
"1B": (
"solarnet_metadata",
solarnet_metadata.__version__,
"https://github.com/IHDE-Alliance/solarnet_metadata.git",
),
}
# =============================================================================
# Mandatory data description keywords (sections 15.4, 5.1, 5.2, 5.6.2)
# =============================================================================
[docs]
def get_bunit(data_level: str, data_type: str) -> Tuple[str, str]:
"""
Get the bunit and comment for a given data level and data type.
Parameters
----------
data_level : str
Data Processing step (e.g., 'L0', 'L1')
data_type : str
Type of data being processed
Returns
-------
tuple
A tuple of (bunit, comment)
"""
bunit = None
match data_level.lower():
case "l0":
bunit = u.dimensionless_unscaled
case "l1":
match data_type.lower():
case "photon":
bunit = u.count
case "housekeeping":
bunit = u.count
case "spectrum":
bunit = u.count
case _:
raise ValueError(f"Units Undefined for Data Type: {data_type}")
case _:
raise ValueError(f"Units Undefined for Data Level: {data_level}")
comment = get_comment("BUNIT")
return bunit.to_string(), comment
# =============================================================================
# Optional pipeline processing keywords (sections 18, 8, 8.1, 8.2)
# =============================================================================
[docs]
def get_prstep(n: int = 1) -> Tuple[str, str]:
"""
Get the processing step description and standard comment for FITS header.
This function returns a tuple containing the processing step description and
the corresponding standard comment based on the Processing step.
Parameters
----------
n : int, optional
Processing step number, default is 1.
1: Raw to L1
2: L1 to L2
3: L2 to L3
4: L3 to L4
Returns
-------
tuple
A tuple of (processing_step_description, standard_comment)
Raises
------
ValueError
If the Processing step number is not in the range 1-4.
"""
value = None
match n:
case 1:
value = "PROCESS Raw to L1"
case 2:
value = "PROCESS L1 to L2"
case 3:
value = "PROCESS L2 to L3"
case 4:
value = "PROCESS L3 to L4"
case _:
raise ValueError(f"Processing Undefined for n={n}")
comment = get_comment(f"PRSTEP{n}")
return value, comment
[docs]
def get_prproc(n: int = 1) -> Tuple[str, str]:
"""
Get the processing procedure description and standard comment for FITS header.
This function returns a tuple containing the processing procedure description and
the corresponding standard comment based on the Processing step.
Parameters
----------
n : int, optional
Processing step number, default is 1.
Returns
-------
tuple
A tuple of (processing_procedure_description, standard_comment)
"""
value = None
match n:
case _:
value = "padre_craft.calibration.process_file"
comment = get_comment(f"PRPROC{n}")
return value, comment
[docs]
def get_prpver(n: int = 1) -> Tuple[str, str]:
"""
Get the processing version and standard comment for FITS header.
This function returns a tuple containing the processing version and
the corresponding standard comment based on the Processing step.
Parameters
----------
n : int, optional
Processing step number, default is 1.
Returns
-------
tuple
A tuple of (processing_version, standard_comment)
"""
value = padre_craft.__version__
comment = get_comment(f"PRPVER{n}")
return value, comment
[docs]
def get_prlib(n: int = 1, a: str = "A") -> Tuple[str, str]:
"""
Get the processing library description and standard comment for FITS header.
This function returns a tuple containing the processing library description and
the corresponding standard comment based on the Processing step.
Parameters
----------
n : int, optional
Processing step number, default is 1.
a : str, optional
Library version, default is A.
Returns
-------
tuple
A tuple of (processing_library_description, standard_comment)
"""
prlib, _, _ = PRLIBS.get(f"{n}{a}", (None, None, None))
if prlib:
return prlib, get_comment(f"PRLIB{n}{a}")
else:
raise ValueError(f"Library Undefined for n={n} and a={a}")
[docs]
def get_prver(n: int = 1, a: str = "A") -> Tuple[str, str]:
"""
Get the processing version and standard comment for FITS header.
This function returns a tuple containing the processing version and
the corresponding standard comment based on the Processing step.
Parameters
----------
n : int, optional
Processing step number, default is 1.
a : str, optional
Library version, default is A.
Returns
-------
tuple
A tuple of (processing_version, standard_comment)
"""
_, prver, _ = PRLIBS.get(f"{n}{a}", (None, None, None))
if prver:
return prver, get_comment(f"PRVER{n}{a}")
else:
raise ValueError(f"Library Undefined for n={n} and a={a}")
[docs]
def get_prhsh(n: int = 1, a: str = "A") -> Tuple[str, str]:
"""
Get the processing hash and standard comment for FITS header.
This function returns a tuple containing the processing hash and
the corresponding standard comment based on the Processing step.
Parameters
----------
n : int, optional
Processing step number, default is 1.
a : str, optional
Library version, default is A.
Returns
-------
tuple
A tuple of (processing_hash, standard_comment)
"""
lib, version, url = PRLIBS.get(f"{n}{a}", (None, None, None))
if not url:
raise ValueError(f"Library Undefined for n={n} and a={a}")
try:
# Try Locally
match a:
case "A":
repo = git.Repo(padre_craft.__file__, search_parent_directories=True)
hexsha = repo.head.object.hexsha
case _:
raise ModuleNotFoundError(f"Library Version Undefined for a={a}")
except (ValueError, ModuleNotFoundError, git.InvalidGitRepositoryError) as _:
# Not Available Locally - Use the Remote
remote_info = git.cmd.Git().ls_remote(url)
remote_info = remote_info.split()
# formatted as Dict[tag, hexsha]
remote_tags = {
remote_info[i + 1]: remote_info[i] for i in range(0, len(remote_info), 2)
}
# Look for Tag
hexsha = None
if "dev" not in version:
version_formattings = [f"v{version}", f"{version}"]
# Search Various Version Formats
for version_format in version_formattings:
hexsha = remote_tags.get(f"refs/tags/{version_format}", None)
if hexsha is not None:
break
if hexsha is None:
log.warning(f"Version {version} not found in Tags for {url}. Using HEAD.")
hexsha = remote_tags.get("HEAD", None)
# header[f"PRBRA{n}A"] = (
# repo.active_branch.name,
# get_std_comment(f"PRBRA{n}A"),
# )
comment = get_comment(f"PRHSH{n}{a}")
return hexsha, comment