diff --git a/noche/headers/header_base_v1.ini b/noche/headers/header_base_v1.ini index 7e770eabd192bed880f889997ebe039d29f5f19d..90ba1e930e8b9769801403450c327ea432680b97 100644 --- a/noche/headers/header_base_v1.ini +++ b/noche/headers/header_base_v1.ini @@ -47,6 +47,7 @@ OBSTYPE = | Observation type [Detector] DETECTOR = | Detector identifier DETSIZE = | [px] Physical CCD dimensions +DETROT = | [deg] Rotation offset of the detector XPIXSZ = | [um] Pixel X axis size YPIXSZ = | [um] Pixel Y axis size PIXSCALE = | [arcsec/px] Plate scale in binning 1 diff --git a/noche/noche.py b/noche/noche.py index 0ab931d68273cd5c4378a3845d562bd1559acbf7..482feb41905292c3dbe7d9fd239cde4b9fe58a3d 100644 --- a/noche/noche.py +++ b/noche/noche.py @@ -2,6 +2,7 @@ import configparser from pathlib import Path from configparser import DuplicateOptionError from ast import literal_eval +import sys import numpy as np from astropy.coordinates import SkyCoord, EarthLocation @@ -44,10 +45,12 @@ class Noche: header_template_path_ini : str Path to the configuration file containing the base header. """ - + if debug: log.setLevel('DEBUG') - + + log.debug(sys._getframe().f_code.co_name) + self._coord = None self._location = None self._obstime = None @@ -69,8 +72,11 @@ class Noche: Returns ------- list of str - Names of available observatory configuration files (without .ini extension). + Names of available observatory configuration files + (without .ini extension). """ + log.debug(sys._getframe().f_code.co_name) + data_dir = Path(__file__).parent / self._obs_dir return [f.stem for f in data_dir.glob("*.ini")] @@ -84,6 +90,7 @@ class Noche: path : str Configuration file in .ini format """ + log.debug(sys._getframe().f_code.co_name) if not path: path = Path(__file__).parent / self._head_dir / "header_base_v1.ini" @@ -117,11 +124,11 @@ class Noche: name : str One of the supported observatories. """ + log.debug(sys._getframe().f_code.co_name) data_dir = Path(__file__).parent / self._obs_dir ini_path = data_dir / f"{name}.ini" - log.debug(ini_path) if not ini_path.exists(): msg = f"Observatory config '{ini_path}' not found." # raise FileNotFoundError(msg) @@ -141,6 +148,7 @@ class Noche: path : str Configuration file in .ini format. """ + log.debug(sys._getframe().f_code.co_name) config = self._load_config(path) @@ -156,68 +164,105 @@ class Noche: if k in self.header: self.header[k] = val - #self.header["DETSIZE"] = f'[1:{self.header["NAXIS1"]},1:{self.header["NAXIS2"]}]' - if fits_file: self.fill_from_fits_file(path, fits_file) - - self._update() + else: + self._update() + def fill_from_fits_file(self, path=None, fits_file=None): """ Load relevant header info from a fits file of the specific observatory. - - - [Mapping] section defines how the new keywords are related - to the old ones. - For example: FOCUSPOS = TELFOCUS - - - [Formula] section defines the formula to apply to a header - value to match the specifics of the new one. - For example: FOCUSPOS = (x)/1000 # Transforming mm in um. - + + - [Mapping] defines how the new keywords are related to the old ones. + - [Formula] defines formulas to apply *before* generating derived keywords. + - [Tweak] defines formulas to apply *after* generating all keywords. + Parameters ---------- path : str Configuration file in .ini format. fits_file : str - FITS file position. - + FITS file path. """ + log.debug(sys._getframe().f_code.co_name) - config = self._load_config(path) - + config = self._load_config(path) filename = Path(fits_file) self.load_fits(filename) fits_file_header = self.hdu.header - - sections = config.sections() - + + # Load formulas (if sections exist) + pre_formulas = dict(config["Formula"]) if "Formula" in config else {} + post_formulas = dict(config["Tweak"]) if "Tweak" in config else {} + + # Apply Mapping + PreFormula loc = config["Mapping"] for k in loc.keys(): + + if not (k in self.header): + log.error(f"{k} not in header") + continue + fits_keyword = loc[k] fits_value = fits_file_header[fits_keyword] val = self._parse(fits_value) self.header[k] = val log.info(f"{k:<8} : {val}") - - loc = config["Formula"] - for k in loc.keys(): - fits_formula = loc[k] - x = self.header[k] - self.header[k] = eval(fits_formula) - log.info(f"{k:<8} : from {x} to {self.header[k]}") - del x - + + if k in pre_formulas: + x = self.header[k] + try: + self.header[k] = eval(pre_formulas[k]) + log.warning(f"{k:<8} (Pre): from {x} to {self.header[k]}") + except Exception as e: + log.error(f"Failed to evaluate PreFormula for {k}: {e}") + # # Apply Mapping + # loc = config["Mapping"] + # for k in loc.keys(): + + # if not (k in self.header): + # log.error(f"{k} not in header") + # continue + + # fits_keyword = loc[k] + # fits_value = fits_file_header[fits_keyword] + # val = self._parse(fits_value) + # self.header[k] = val + # log.info(f"{k:<8} : {val}") + + # # Apply PreFormula + # for k, formula in pre_formulas.items(): + # if k in self.header: + # x = self.header[k] + # try: + # self.header[k] = eval(formula) + # log.warning(f"{k:<8} (Pre): from {x} to {self.header[k]}") + # except Exception as e: + # log.error(f"Failed to evaluate PreFormula for {k}: {e}") + # else: + # log.debug(f"PreFormula skipped: {k} not in header") + + # Set coordinates (calls _update(), which triggers set_wcs and others) self.set_coordinates(self.header["RA"], self.header["DEC"], obstime=self.header["DATE-OBS"]) - - self._update() - + + # Apply PostFormula + for k, formula in post_formulas.items(): + if k in self.header: + x = self.header[k] + try: + self.header[k] = eval(formula) + log.warning(f"{k:<8} (Post): from {x} to {self.header[k]}") + except Exception as e: + log.error(f"Failed to evaluate PostFormula for {k}: {e}") + else: + log.warning(f"PostFormula skipped: {k} not in header") + self.header["FILEORIG"] = filename.name - - + def set_location(self, lon, lat, alt): """ Location of the observatory, Separate so that it is @@ -232,6 +277,7 @@ class Noche: alt : float Elevation in meters. """ + log.debug(sys._getframe().f_code.co_name) self._location = EarthLocation(lon, lat, alt) @@ -248,6 +294,7 @@ class Noche: obstime : str, astropy.time.Time Time of the observation. """ + log.debug(sys._getframe().f_code.co_name) time = Time(obstime) self._obstime = time @@ -259,7 +306,7 @@ class Noche: self.header['DATE-OBS'] = time.isot self.header['MJD-OBS'] = time.mjd - self._update() + #self._update() def set_object(self, objname, update_coord=False, obstime=None): @@ -275,16 +322,17 @@ class Noche: obstime : str or astropy.time.Time, optional Observation time to associate with coordinates. """ + log.debug(sys._getframe().f_code.co_name) + + if obstime: + time = Time(obstime) + self.set_obstime(time) if update_coord: try: coord = SkyCoord.from_name(objname) self._coord = coord - if obstime: - time = Time(obstime) - self.set_obstime(time) - log.info("Found catalog name") coorstr = coord.to_string(style='hmsdms', sep=' ', precision=1, pad=True) log.info(f"Corresponds to {coorstr}") @@ -315,6 +363,7 @@ class Noche: obstime : str or astropy.time.Time, optional Observation time. """ + log.debug(sys._getframe().f_code.co_name) coord = SkyCoord(ra=ra, dec=dec, unit=(u.hourangle, u.deg)) self._coord = coord @@ -325,8 +374,8 @@ class Noche: self.header['RA'] = coord.ra.to_string(unit=u.hourangle, sep=':', pad=True, precision=1) - self.header['DEC'] = coord.dec.to_string(unit=u.deg, sep=':', pad=True, - precision=1) + self.header['DEC'] = coord.dec.to_string(unit=u.deg, sep=':', + pad=True, precision=1) self.header['RA_DEG'] = coord.ra.deg self.header['DEC_DEG'] = coord.dec.deg @@ -342,6 +391,7 @@ class Noche: ValueError If observation time or location is not set. """ + log.debug(sys._getframe().f_code.co_name) if self._obstime == None or self._location == None: raise ValueError("Observation Time, Instrument parameters must be set.") @@ -365,11 +415,11 @@ class Noche: # Position angle: with respect to Celestial North Pole north_celestial = SkyCoord(ra=0*u.deg, dec=90*u.deg, frame='icrs') - posang = self._coord.position_angle(north_celestial).to(u.deg).value - self.header['POSANGLE'] = round(posang, 2) + posangle = self._coord.position_angle(north_celestial).to(u.deg).value + self.header['POSANGLE'] = round(posangle, 2) # Parallactic angle: between local meridian and celestial axis - parangle = (posang - altaz.az.deg + 360) % 360 + parangle = (posangle - altaz.az.deg + 360) % 360 if parangle > 180: parangle -= 360 # Wrap to [-180, 180] @@ -390,11 +440,11 @@ class Noche: ValueError If coordinates or location are not set. """ + log.debug(sys._getframe().f_code.co_name) if self._coord == None or self._location == None: raise ValueError("Observation Coordinates, Instrument parameters must be set.") - detsize = self.header['DETSIZE'].strip('[]') x_str, y_str = detsize.split(',') @@ -407,20 +457,22 @@ class Noche: cdelt2 = round(self.header["PIXSCALE"]*self.header["YBINNING"]*u.arcsec.to(u.deg), 7) if not angle: - angle = self.header["DEROTANG"] + angle = self.header["DEROTANG"] + self.header["DETROT"] angle = np.deg2rad(angle) crval_ra = self._coord.ra.deg crval_dec = self._coord.dec.deg - self.header['CRPIX1'] = crpix[0] + flip = -1 # East to the left + + self.header['CRPIX1'] = crpix[0] self.header['CRPIX2'] = crpix[1] self.header['CRVAL1'] = crval_ra self.header['CRVAL2'] = crval_dec - self.header['CDELT1'] = cdelt1 + self.header['CDELT1'] = cdelt1 * flip self.header['CDELT2'] = cdelt2 - self.header["PC1_1"] = +np.cos(angle) *-1 # E to left + self.header["PC1_1"] = +np.cos(angle) self.header["PC1_2"] = -np.sin(angle) self.header["PC2_1"] = +np.sin(angle) self.header["PC2_2"] = +np.cos(angle) @@ -435,6 +487,7 @@ class Noche: ValueError If coordinates or location are not set. """ + log.debug(sys._getframe().f_code.co_name) if self._coord == None or self._location == None: raise ValueError("Observation Time, Observing location must be set.") @@ -467,12 +520,14 @@ class Noche: """ List the header keywords that are still empty """ + log.debug(sys._getframe().f_code.co_name) for k in self.header: if self.header[k] == None: print(k, self.header[k]) def load_fits(self, fits_file): + log.debug(sys._getframe().f_code.co_name) filename = fits_file hdul = fits.open(filename) @@ -487,6 +542,7 @@ class Noche: @staticmethod def _load_config(path): + log.debug(sys._getframe().f_code.co_name) config = configparser.ConfigParser(inline_comment_prefixes=('#',)) @@ -504,8 +560,10 @@ class Noche: If coordinates, observation and location are provided, then additional keywords can be filled """ + log.debug(sys._getframe().f_code.co_name) if self._coord != None and self._obstime != None and self._location != None: + self.set_altaz_and_parallactic() self.set_ambient() self.set_wcs() @@ -525,6 +583,7 @@ class Noche: int, float, bool, or str Parsed value. """ + # log.debug(sys._getframe().f_code.co_name) try: val = val.strip() diff --git a/noche/observatories/abobservatory.ini b/noche/observatories/abobservatory.ini index 72ef2bced39ca8ffc88aaaacaf2116eaaa443126..3bd9992d1fb5d93bf8ee6fea8bb173a7d6637ec3 100644 --- a/noche/observatories/abobservatory.ini +++ b/noche/observatories/abobservatory.ini @@ -5,14 +5,14 @@ FOCALLEN = 1170 # [mm] Telescope focal length OBS-LONG = -11.2430135 # [deg] Observatory longitude (East > 0) OBS-LAT = 43.5235203 # [deg] Observatory latitude (North > 0) OBS-ELEV = 1025 # [m] Observatory altitude above sea level -DEROTANG = 0 # [deg] Rotator angle, if any INSTRUME = ABOb instrument # Instrument name OBSTYPE = Imaging # Observation type DETECTOR = Atik 383 L # Detector identifier DETSIZE = [1:3354,1:2529] # [px] [1:x,1:y] Physical CCD dimensions +DETROT = -89.67 # [deg] Rotation offset of the detector XPIXSZ = 5.4 # [um] Pixel X axis size YPIXSZ = 5.4 # [um] Pixel Y axis size -PIXSCALE = 36 # [arcsec/px] Plate scale in binning 1 +PIXSCALE = 1.0 # [arcsec/px] Plate scale in binning 1 GAIN = # [e-/ADU] Gain RDNOISE = # [e- RMS] Readout noise @@ -38,3 +38,6 @@ SWCREATE = SWCREATE # Software that created FILEORIG [Formula] + +[Tweak] +CDELT2 = -1*x diff --git a/noche/observatories/grt.ini b/noche/observatories/grt.ini index 3f9216819f38a560fef7e6a588c607a09e1e8947..09e0df83e82e26e420ec360563697556ef6a53d4 100644 --- a/noche/observatories/grt.ini +++ b/noche/observatories/grt.ini @@ -5,11 +5,11 @@ FOCALLEN = 1520 # [mm] Telescope focal length OBS-LONG = 14.020563 # [deg] Observatory longitude (East > 0) OBS-LAT = 37.9391183 # [deg] Observatory latitude (North > 0) OBS-ELEV = 606 # [m] Observatory altitude above sea level -DEROTANG = 0 # [deg] Rotator angle, if any INSTRUME = GRT instrument # Instrument name OBSTYPE = Imaging # Observation type DETECTOR = Moravian CMOS C4-16000 # Detector identifier DETSIZE = [1:4096,1:4096] # [px] [1:x,1:y] Physical CCD dimensions +DETROT = +5.0 # [deg] Rotation offset of the detector XPIXSZ = 9 # [um] Pixel X axis size YPIXSZ = 9 # [um] Pixel Y axis size PIXSCALE = 1.22 # [arcsec/px] Plate scale in binning 1 @@ -37,3 +37,8 @@ YBINNING = YBINNING # Binning factor in Y SWCREATE = SWCREATE # Software that created FILEORIG [Formula] + +[Tweak] +CDELT2 = -1*x +CRPIX1 = x+134 +CRPIX2 = x-500 diff --git a/noche/observatories/oarpaf.ini b/noche/observatories/oarpaf.ini index c9870feb39c6c6241ec206d73cade7b3fd6d1c6e..1b69ab8a5622e3000944969d6f1e088af2fd02ea 100644 --- a/noche/observatories/oarpaf.ini +++ b/noche/observatories/oarpaf.ini @@ -5,11 +5,11 @@ FOCALLEN = 6400 # [mm] Telescope focal length OBS-LONG = 9.2034 # [deg] Observatory longitude (East > 0) OBS-LAT = 44.5912 # [deg] Observatory latitude (North > 0) OBS-ELEV = 1469 # [m] Observatory altitude above sea level -DEROTANG = -89.67 # [deg] Rotator angle, if any INSTRUME = Cerbero # Instrument name OBSTYPE = Imaging # Observation type DETECTOR = SBIG STX-16081 # Detector identifier DETSIZE = [1:4144,1:4126] # [px] [1:x,1:y] Physical CCD dimensions +DETROT = -89.67 # [deg] Rotation offset of the detector XPIXSZ = 9 # [um] Pixel X axis size YPIXSZ = 9 # [um] Pixel Y axis size PIXSCALE = 0.283 # [arcsec/px] Plate scale in binning 1 @@ -37,4 +37,8 @@ TEMPERAT = HIERARCH CAM AMBIENT # [C] Ambient temperature SWCREATE = SWCREATE # Software that created FILEORIG [Formula] -RA = (x)/15 +RA = x/15 + +[Tweak] +CRPIX1 = x+234 +CRPIX2 = x-100 diff --git a/noche/observatories/ogg.ini b/noche/observatories/ogg.ini index 0e3b4bbed9517c3baf4709d9f07eafec16612ef1..8fc94bb9aa0faa8aac735b61baa61dbd202fdd7a 100644 --- a/noche/observatories/ogg.ini +++ b/noche/observatories/ogg.ini @@ -6,13 +6,13 @@ OBS-LONG = 16.9 # [deg] Observatory longitude (East > 0) OBS-LAT = 38.29 # [deg] Observatory latitude (North > 0) OBS-ELEV = 456 # [m] Observatory altitude above sea level INSTRUME = OGG obs instrument # Instrument name -DEROTANG = 0 # [deg] Rotator angle, if any OBSTYPE = Imaging # Observation type DETECTOR = CMOS QHY268 # Detector identifier DETSIZE = [1:6280,1:4210] # [px] [1:x,1:y] Physical CCD dimensions +DETROT = 180 # [deg] Rotation offset of the detector XPIXSZ = 3.7 # [um] Pixel X axis size YPIXSZ = 3.7 # [um] Pixel Y axis size -PIXSCALE = 4644 # [arcsec/px] Plate scale in binning 1 +PIXSCALE = 1 # [arcsec/px] Plate scale in binning 1 GAIN = # [e-/ADU] Gain RDNOISE = # [e- RMS] Readout noise @@ -37,3 +37,8 @@ TEMPERAT = EXT-TEMP # [C] Ambient temperature SWCREATE = SWCREATE # Software that created FILEORIG [Formula] + +[Tweak] +CDELT2 = -1*x +CRPIX1 = x-400 +CRPIX2 = x+100 diff --git a/noche/observatories/opc.ini b/noche/observatories/opc.ini index 6de629217c5a8f1801450247045ecbd14445b918..5615e5f01402676dd4eabe4dfac706f5fb5554b3 100644 --- a/noche/observatories/opc.ini +++ b/noche/observatories/opc.ini @@ -5,11 +5,11 @@ FOCALLEN = 6400 # [mm] Telescope focal length OBS-LONG = 11.2430135 # [deg] Observatory longitude (East > 0) OBS-LAT = 43.5235203 # [deg] Observatory latitude (North > 0) OBS-ELEV = 450 # [m] Observatory altitude above sea level -DEROTANG = -90.0 # [deg] Rotator angle, if any INSTRUME = OPC Instrument # Instrument name OBSTYPE = Imaging # Observation type DETECTOR = Moravian G4 - KAF09000 # Detector identifier DETSIZE = [1:4144,1:4126] # [px] [1:x,1:y] Physical CCD dimensions +DETROT = +0.0 # [deg] Rotation offset of the detector XPIXSZ = 12 # [um] Pixel X axis size YPIXSZ = 12 # [um] Pixel Y axis size PIXSCALE = 0.38 # [arcsec/px] Plate scale in binning 1 @@ -21,8 +21,8 @@ NAXIS1 = NAXIS1 # [px] X Dimensions of detector NAXIS2 = NAXIS2 # [px] Y dimension of detector # OBSERVER = # Observer name OBJECT = OBJECT # Name of observed object -RA = RA # In sexagesimal or decimal format -DEC = DEC # In sexagesimal or decimal format +RA = RA_OBJ # In sexagesimal or decimal format +DEC = DEC_OBJ # In sexagesimal or decimal format DATE-OBS = DATE-OBS # [YYYY-MM-DDTHH:MM:SS] UTC observation date FOCUSTEM = FOCUSTEM # [C] Focuser temperature FOCUSPOS = FOCUSPOS # [um] Focuser position @@ -37,4 +37,6 @@ YBINNING = YBINNING # Binning factor in Y SWCREATE = SWCREATE # Software that created FILEORIG [Formula] -RA = (x)/15 +#RA = (x)/15 + +[Tweak] diff --git a/noche/observatories/ossfoligno.ini b/noche/observatories/ossfoligno.ini index 9920a998b35f5f58d39311c8d5d559d133a16fff..51b45fad7519b2f5caeb294a68cb6eff20f9b50c 100644 --- a/noche/observatories/ossfoligno.ini +++ b/noche/observatories/ossfoligno.ini @@ -5,7 +5,6 @@ FOCALLEN = 1950 # [mm] Telescope focal length OBS-LONG = -11.2430135 # [deg] Observatory longitude (East > 0) OBS-LAT = 43.5235203 # [deg] Observatory latitude (North > 0) OBS-ELEV = 1025 # [m] Observatory altitude above sea level -DEROTANG = 0 # [deg] Rotator angle, if any INSTRUME = Foligno instrument # Instrument name OBSTYPE = Imaging # Observation type DETECTOR = QHY174MM # Detector identifier @@ -37,3 +36,6 @@ TEMPERAT = AOCAMBT # [C] Ambient temperature SWCREATE = SWCREATE # Software that created FILEORIG [Formula] + + +[Tweak]