Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,30 @@ The main goal of this project is to standardize the handling of stellar spectra
3) Framework to mask telluric features from stellar spectra
4) Implement utility methods to interpolate, normalize and smooth stellar spectra

For information on installation, usage, and contribution check the official [documentation](https://kamuish.github.io/ASTRA/)
For information on installation, usage, and contribution check the official [documentation](https://kamuish.github.io/ASTRA/).

If you use it, please cite the paper:

```
@ARTICLE{2026JOSS...11.9413S,
author = {{Silva}, Andr{\'e} and {Faria}, J. and {Santos}, Nuno and {Sousa}, S{\'e}rgio and {Viana}, Pedro and {Martins}, J.},
title = "{ASTRA: A Python Package for Cross-Instrument Stellar and Telluric Template Construction}",
journal = {The Journal of Open Source Software},
keywords = {astronomy, Python, Cython, Instrumentation and Methods for Astrophysics, Earth and Planetary Astrophysics, Solar and Stellar Astrophysics},
year = 2026,
month = jan,
volume = {11},
number = {117},
eid = {9413},
pages = {9413},
doi = {10.21105/joss.09413},
archivePrefix = {arXiv},
eprint = {2601.10439},
primaryClass = {astro-ph.IM},
adsurl = {https://ui.adsabs.harvard.edu/abs/2026JOSS...11.9413S},
adsnote = {Provided by the SAO/NASA Astrophysics Data System}
}
```

## Installation

Expand Down
19 changes: 19 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,25 @@ Depending on the python version that is in use, the installation of TelFit might

## Changelog

### V1.2.7 (current)

1) Added HARPS-N support
2) Updated plotting capabilities of DataClass


### V1.2.6


1. Added preliminary support for PoET data
2. Updated spawn method of multiprocessing pools
3. Improved frame-rejection tools


### V1.2.4

1. Update init of ESO based pipelines


### V1.2.3 (5th December 2025)

1. Addition of new routines to oversample the stellar template
Expand Down
111 changes: 41 additions & 70 deletions docs/spectra/access_spectra.ipynb

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/spectra/selecting_spectra.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
],
"metadata": {
"kernelspec": {
"display_name": ".venv",
"display_name": "ASTRA",
"language": "python",
"name": "python3"
},
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "ASTRA-spectra"
version = "1.2.6"
version = "1.2.7"
description = "Interface to open stellar spectra and construct stellar and telluric models"
readme = "README.md"
requires-python = ">=3.11,<=3.12.8"
Expand Down
98 changes: 74 additions & 24 deletions src/ASTRA/Instruments/ESO_PIPELINE.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def __init__(
"INS NAME": "INSTRUME",
"INS MODE": f"HIERARCH {KW_identifier} INS MODE",
"PROG ID": f"HIERARCH {KW_identifier} OBS PROG ID",
"OBS NAME": "HIERARCH ESO OBS NAME",
"OBS NAME": f"HIERARCH {KW_identifier} OBS NAME",
}
if override_KW_map is not None:
for key, value in override_KW_map.items():
Expand All @@ -131,7 +131,11 @@ def __init__(
file_path=file_path,
frameID=frameID,
KW_map=KW_map,
available_indicators=(available_indicators if override_indicators is None else override_indicators),
available_indicators=(
available_indicators
if override_indicators is None
else override_indicators
),
user_configs=user_configs,
reject_subInstruments=reject_subInstruments,
quiet_user_params=quiet_user_params,
Expand All @@ -156,7 +160,9 @@ def __init__(
if key in self.file_path.stem:
break
else:
raise custom_exceptions.InvalidConfiguration(f"{self.name} can't recognize {self.file_path}")
raise custom_exceptions.InvalidConfiguration(
f"{self.name} can't recognize {self.file_path}"
)

def load_instrument_specific_KWs(self, header) -> None:
if self._internal_configs["use_old_pipeline"]:
Expand All @@ -169,7 +175,9 @@ def load_instrument_specific_KWs(self, header) -> None:
version = drs_version.split("/")[-1]

# TODO: Check the number of the DRS version that has this fixed
version_sum = lambda a: sum(a * 10**b for a, b in zip(map(int, a.split(".")[::-1]), range(3)))
version_sum = lambda a: sum(
a * 10**b for a, b in zip(map(int, a.split(".")[::-1]), range(3))
)

if version_sum(version) < version_sum("3.2.1"):
# For older versions we always need to use the
Expand All @@ -183,9 +191,13 @@ def load_instrument_specific_KWs(self, header) -> None:
if berv_factor is not None:
logger.warning("Recomputing the BERV from BERV_factor keyword")
new_berv = (berv_factor - 1) * SPEED_OF_LIGHT * kilometer_second
diff_berv = (new_berv - self.observation_info["BERV"]).to(meter_second).value
diff_berv = (
(new_berv - self.observation_info["BERV"]).to(meter_second).value
)
if abs(diff_berv) > 10:
logger.warning(f"Difference between BERV and BERV_factor is of {diff_berv} [m/s]")
logger.warning(
f"Difference between BERV and BERV_factor is of {diff_berv} [m/s]"
)

self.observation_info["BERV"] = new_berv

Expand Down Expand Up @@ -250,7 +262,9 @@ def load_telemetry_info(self, header):
}

for name, endKW in ambi_KWs.items():
self.observation_info[name] = header[f"HIERARCH {self.KW_identifier} METEO {endKW}"]
self.observation_info[name] = header[
f"HIERARCH {self.KW_identifier} METEO {endKW}"
]
if "temperature" in name: # store temperature in KELVIN for TELFIT
self.observation_info[name] = convert_temperature(
self.observation_info[name],
Expand All @@ -259,7 +273,9 @@ def load_telemetry_info(self, header):
)

if self.observation_info["relative_humidity"] == 255:
logger.warning(f"{self.name} has an invalid value in the humidity sensor...")
logger.warning(
f"{self.name} has an invalid value in the humidity sensor..."
)
self.observation_info["relative_humidity"] = np.nan

self.observation_info["airmass"] = header["AIRMASS"]
Expand All @@ -271,13 +287,19 @@ def _load_ESO_DRS_KWs(self, header):
)

# Load BERV info + previous RV
self.observation_info["MAX_BERV"] = header[f"HIERARCH {self.KW_identifier} QC BERVMAX"] * kilometer_second
self.observation_info["BERV"] = header[f"HIERARCH {self.KW_identifier} QC BERV"] * kilometer_second
self.observation_info["MAX_BERV"] = (
header[f"HIERARCH {self.KW_identifier} QC BERVMAX"] * kilometer_second
)
self.observation_info["BERV"] = (
header[f"HIERARCH {self.KW_identifier} QC BERV"] * kilometer_second
)

berv_factor = header.get(f"HIERARCH {self.KW_identifier} QC BERV FACTOR", None)
self.observation_info["BERV_FACTOR"] = berv_factor

self.observation_info["DRS_RV"] = header[f"HIERARCH {self.KW_identifier} QC CCF RV"] * kilometer_second
self.observation_info["DRS_RV"] = (
header[f"HIERARCH {self.KW_identifier} QC CCF RV"] * kilometer_second
)
self.observation_info["DRS_RV_ERR"] = (
header[f"HIERARCH {self.KW_identifier} QC CCF RV ERROR"] * kilometer_second
)
Expand All @@ -301,7 +323,9 @@ def _load_ESO_DRS_KWs(self, header):
if obs_date.hour < 16: # noqa: PLR2004
# If before 4pm, the observation was from previous day
obs_date = obs_date - datetime.timedelta(days=1)
self.observation_info["DATE_NIGHT"] = datetime.datetime.strftime(obs_date, r"%Y-%m-%d")
self.observation_info["DATE_NIGHT"] = datetime.datetime.strftime(
obs_date, r"%Y-%m-%d"
)

def load_ESO_DRS_S2D_data(self, overload_SCIDATA_key=None):
if self._internal_configs["use_old_pipeline"]:
Expand All @@ -313,11 +337,15 @@ def load_ESO_DRS_S2D_data(self, overload_SCIDATA_key=None):
self.wavelengths = hdulist["WAVEDATA_VAC_BARY"].data
self.qual_data = hdulist["QUALDATA"].data

SCIDATA_KEY = "SCIDATA" if overload_SCIDATA_key is None else overload_SCIDATA_key
SCIDATA_KEY = (
"SCIDATA" if overload_SCIDATA_key is None else overload_SCIDATA_key
)
ERRDATA_KEY = "ERRDATA"

if self._internal_configs["Telluric_Corrected"]:
logger.info("Loading S2D file from a non-DRS source (telluric corrected file)")
logger.info(
"Loading S2D file from a non-DRS source (telluric corrected file)"
)
SCIDATA_KEY += "_CORR"
ERRDATA_KEY += "_CORR"

Expand All @@ -327,7 +355,12 @@ def load_ESO_DRS_S2D_data(self, overload_SCIDATA_key=None):
if self._internal_configs["apply_FluxCorr"]:
logger.debug("Starting chromatic flux correction")
keyword = f"HIERARCH {self.KW_identifier} QC ORDER%d FLUX CORR"
flux_corr = np.array([hdulist[0].header[keyword % o] for o in range(1, self.N_orders + 1)])
flux_corr = np.array(
[
hdulist[0].header[keyword % o]
for o in range(1, self.N_orders + 1)
]
)
fit_nb = (flux_corr != 1.0).sum()

ignore = self.N_orders - fit_nb
Expand All @@ -341,8 +374,12 @@ def load_ESO_DRS_S2D_data(self, overload_SCIDATA_key=None):
# corr_model = np.zeros_like(hdu[5].data, dtype=np.float32)
corr_model = np.polyval(coeff, hdulist[5].data)

corr_model[flux_corr == 1] = 1 # orders where the CORR FACTOR are 1 do not have correction!
self.spectra = self.spectra / corr_model # correct from chromatic variations
corr_model[flux_corr == 1] = (
1 # orders where the CORR FACTOR are 1 do not have correction!
)
self.spectra = (
self.spectra / corr_model
) # correct from chromatic variations
self.flux_atmos_balance_corrected = True
# TODO: understand if we want to include the factor in uncertainties or not!
# self.uncertainties = self.uncertainties / corr_model # maintain the SNR in the corrected spectrum
Expand All @@ -354,7 +391,9 @@ def load_ESO_DRS_S2D_data(self, overload_SCIDATA_key=None):
# / corr_model

if self._internal_configs["apply_FluxBalance_Norm"]:
logger.info("Normalizing the flux balance distribution due to dispersion")
logger.info(
"Normalizing the flux balance distribution due to dispersion"
)
# The physical sizes of the pixels (on the CCD) are the same
# The flux that reaches eeach pixel is different, due to dispersion
# The spectra will have a trend, even after removing the instrumental effect
Expand Down Expand Up @@ -384,8 +423,12 @@ def load_ESO_DRS_S1D_data(self):
logger.warning("SBART using air wavelengths!")

self.wavelengths = full_data[wave_kw].reshape((1, self.array_size[1]))
self.spectra = full_data["flux"].reshape((1, self.array_size[1])).astype(np.float64)
self.uncertainties = full_data["error"].reshape((1, self.array_size[1])).astype(np.float64)
self.spectra = (
full_data["flux"].reshape((1, self.array_size[1])).astype(np.float64)
)
self.uncertainties = (
full_data["error"].reshape((1, self.array_size[1])).astype(np.float64)
)
self.qual_data = full_data["quality"].reshape((1, self.array_size[1]))
self.build_mask(bypass_QualCheck=False)

Expand Down Expand Up @@ -420,7 +463,8 @@ def check_header_QC_ESO_DRS(self, header):
if not self.is_skysub:
extra_checks = {
f"HIERARCH {self.KW_identifier}" + " QC SCIRED DRIFT CHECK": 0,
f"HIERARCH {self.KW_identifier}" + " QC SCIRED DRIFT FLUX_RATIO CHECK": 0,
f"HIERARCH {self.KW_identifier}"
+ " QC SCIRED DRIFT FLUX_RATIO CHECK": 0,
f"HIERARCH {self.KW_identifier}" + " QC SCIRED DRIFT CHI2 CHECK": 0,
}
nonfatal_QC_flags = {**nonfatal_QC_flags, **extra_checks}
Expand Down Expand Up @@ -448,17 +492,23 @@ def check_header_QC_ESO_DRS(self, header):
self._status.store_warning(KW_WARNING(msg))

if self._status.number_warnings > 0:
logger.warning("Found {} warning flags in the header KWs", self._status.number_warnings)
logger.warning(
"Found {} warning flags in the header KWs", self._status.number_warnings
)

espdr_to_num = lambda x: int("".join(x.split(".")))
espdrversion = header["ESO PRO REC1 PIPE ID"].split("/")[-1]

if self._internal_configs["USE_APPROX_BERV_CORRECTION"]:
if espdr_to_num(espdrversion) >= espdr_to_num("3.2.0"):
logger.critical(f"Using approximated BERV correction in espdr/{espdrversion}")
logger.critical(
f"Using approximated BERV correction in espdr/{espdrversion}"
)
else:
if espdr_to_num(espdrversion) < espdr_to_num("3.2.0"):
logger.critical(f"Not using approximated BERV correction in espdr/{espdrversion}")
logger.critical(
f"Not using approximated BERV correction in espdr/{espdrversion}"
)

@property
def bare_fname(self) -> str:
Expand Down
Loading
Loading