Coverage for python/astro_metadata_translator/translators/megaprime.py: 35%
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# This file is part of astro_metadata_translator.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (http://www.lsst.org).
6# See the LICENSE file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# Use of this source code is governed by a 3-clause BSD-style
10# license that can be found in the LICENSE file.
12"""Metadata translation code for CFHT MegaPrime FITS headers"""
14__all__ = ("MegaPrimeTranslator", )
16import re
17import posixpath
19from astropy.io import fits
20from astropy.coordinates import EarthLocation, Angle
21import astropy.units as u
23from ..translator import cache_translation, CORRECTIONS_RESOURCE_ROOT
24from .fits import FitsTranslator
25from .helpers import tracking_from_degree_headers, altaz_from_degree_headers
28class MegaPrimeTranslator(FitsTranslator):
29 """Metadata translator for CFHT MegaPrime standard headers.
30 """
32 name = "MegaPrime"
33 """Name of this translation class"""
35 supported_instrument = "MegaPrime"
36 """Supports the MegaPrime instrument."""
38 default_resource_root = posixpath.join(CORRECTIONS_RESOURCE_ROOT, "CFHT")
39 """Default resource path root to use to locate header correction files."""
41 # CFHT Megacam has no rotator, and the instrument angle on sky is set to
42 # +Y=N, +X=W which we define as a 0 degree rotation.
43 _const_map = {"boresight_rotation_angle": Angle(0*u.deg),
44 "boresight_rotation_coord": "sky",
45 "detector_group": None}
47 _trivial_map = {"physical_filter": "FILTER",
48 "dark_time": ("DARKTIME", dict(unit=u.s)),
49 "exposure_time": ("EXPTIME", dict(unit=u.s)),
50 "observation_id": "OBSID",
51 "object": "OBJECT",
52 "science_program": "RUNID",
53 "exposure_id": "EXPNUM",
54 "visit_id": "EXPNUM",
55 "detector_serial": "CCDNAME",
56 "relative_humidity": ["RELHUMID", "HUMIDITY"],
57 "temperature": (["TEMPERAT", "AIRTEMP"], dict(unit=u.deg_C)),
58 "boresight_airmass": ["AIRMASS", "BORE-AIRMASS"]}
60 @cache_translation
61 def to_datetime_begin(self):
62 # Docstring will be inherited. Property defined in properties.py
63 # We know it is UTC
64 value = self._from_fits_date_string(self._header["DATE-OBS"],
65 time_str=self._header["UTC-OBS"], scale="utc")
66 self._used_these_cards("DATE-OBS", "UTC-OBS")
67 return value
69 @cache_translation
70 def to_datetime_end(self):
71 # Docstring will be inherited. Property defined in properties.py
72 # Older files are missing UTCEND
73 if self.is_key_ok("UTCEND"):
74 # We know it is UTC
75 value = self._from_fits_date_string(self._header["DATE-OBS"],
76 time_str=self._header["UTCEND"], scale="utc")
77 self._used_these_cards("DATE-OBS", "UTCEND")
78 else:
79 # Take a guess by adding on the exposure time
80 value = self.to_datetime_begin() + self.to_exposure_time()
81 return value
83 @cache_translation
84 def to_location(self):
85 """Calculate the observatory location.
87 Returns
88 -------
89 location : `astropy.coordinates.EarthLocation`
90 An object representing the location of the telescope.
91 """
92 # Height is not in some MegaPrime files. Use the value from
93 # EarthLocation.of_site("CFHT")
94 # Some data uses OBS-LONG, OBS-LAT, other data uses LONGITUD and
95 # LATITUDE
96 for long_key, lat_key in (("LONGITUD", "LATITUDE"), ("OBS-LONG", "OBS-LAT")):
97 if self.are_keys_ok([long_key, lat_key]):
98 value = EarthLocation.from_geodetic(self._header[long_key], self._header[lat_key], 4215.0)
99 self._used_these_cards(long_key, lat_key)
100 break
101 else:
102 value = EarthLocation.of_site("CFHT")
103 return value
105 @cache_translation
106 def to_detector_name(self):
107 # Docstring will be inherited. Property defined in properties.py
108 if self.is_key_ok("EXTNAME"):
109 name = self._header["EXTNAME"]
110 # Only valid name has form "ccdNN"
111 if re.match(r"ccd\d+$", name):
112 self._used_these_cards("EXTNAME")
113 return name
115 # Dummy value, intended for PHU (need something to get filename)
116 return "ccd99"
118 @cache_translation
119 def to_detector_num(self):
120 name = self.to_detector_name()
121 return int(name[3:])
123 @cache_translation
124 def to_observation_type(self):
125 """Calculate the observation type.
127 Returns
128 -------
129 typ : `str`
130 Observation type. Normalized to standard set.
131 """
132 obstype = self._header["OBSTYPE"].strip().lower()
133 self._used_these_cards("OBSTYPE")
134 if obstype == "object":
135 return "science"
136 return obstype
138 @cache_translation
139 def to_tracking_radec(self):
140 """Calculate the tracking RA/Dec for this observation.
142 Currently will be `None` for geocentric apparent coordinates.
143 Additionally, can be `None` for non-science observations.
145 The method supports multiple versions of header defining tracking
146 coordinates.
148 Returns
149 -------
150 coords : `astropy.coordinates.SkyCoord`
151 The tracking coordinates.
152 """
153 radecsys = ("RADECSYS", "OBJRADEC", "RADESYS")
154 radecpairs = (("RA_DEG", "DEC_DEG"), ("BORE-RA", "BORE-DEC"))
155 return tracking_from_degree_headers(self, radecsys, radecpairs)
157 @cache_translation
158 def to_altaz_begin(self):
159 # Docstring will be inherited. Property defined in properties.py
160 return altaz_from_degree_headers(self, (("TELALT", "TELAZ"), ("BORE-ALT", "BORE-AZ")),
161 self.to_datetime_begin())
163 @cache_translation
164 def to_detector_exposure_id(self):
165 # Docstring will be inherited. Property defined in properties.py
166 return self.to_exposure_id() * 36 + self.to_detector_num()
168 @cache_translation
169 def to_pressure(self):
170 # Docstring will be inherited. Property defined in properties.py
171 # Can be either AIRPRESS in Pa or PRESSURE in mbar
172 for key, unit in (("PRESSURE", u.hPa), ("AIRPRESS", u.Pa)):
173 if self.is_key_ok(key):
174 return self.quantity_from_card(key, unit)
175 else:
176 raise KeyError(f"{self._log_prefix}: Could not find pressure keywords in header")
178 @cache_translation
179 def to_observation_counter(self):
180 """Return the lifetime exposure number.
182 Returns
183 -------
184 sequence : `int`
185 The observation counter.
186 """
187 return self.to_exposure_id()
189 @classmethod
190 def determine_translatable_headers(cls, filename, primary=None):
191 """Given a file return all the headers usable for metadata translation.
193 MegaPrime files are multi-extension FITS with a primary header and
194 each detector stored in a subsequent extension. MegaPrime uses
195 ``INHERIT=F`` therefore the primary header will always be ignored
196 if given.
198 Parameters
199 ----------
200 filename : `str`
201 Path to a file in a format understood by this translator.
202 primary : `dict`-like, optional
203 The primary header obtained by the caller. This is sometimes
204 already known, for example if a system is trying to bootstrap
205 without already knowing what data is in the file. Will be
206 ignored.
208 Yields
209 ------
210 headers : iterator of `dict`-like
211 Each detector header in turn. The supplied header will never be
212 included.
214 Notes
215 -----
216 This translator class is specifically tailored to raw MegaPrime data
217 and is not designed to work with general FITS files. The normal
218 paradigm is for the caller to have read the first header and then
219 called `determine_translator()` on the result to work out which
220 translator class to then call to obtain the real headers to be used for
221 translation.
222 """
223 # Since we want to scan many HDUs we use astropy directly to keep
224 # the file open rather than continually opening and closing it
225 # as we go to each HDU.
226 with fits.open(filename) as fits_file:
227 for hdu in fits_file:
228 # Astropy <=4.2 strips the EXTNAME header but some CFHT data
229 # have two EXTNAME headers and the CCD number is in the
230 # second one.
231 if hdu.name == "PRIMARY":
232 continue
234 if hdu.name.startswith("ccd"):
235 # It may only be some data files that are broken so
236 # handle the expected form.
237 yield hdu.header
238 continue
240 # Some test data at least has the EXTNAME as
241 # COMPRESSED_IMAGE but the EXTVER as the detector number.
242 if hdu.name == "COMPRESSED_IMAGE":
243 header = hdu.header
245 # Astropy strips EXTNAME so put it back for the translator
246 header["EXTNAME"] = f"ccd{hdu.ver:02d}"
247 yield header